# Callite

Callite is a lightweight Remote Procedure Call (RPC) library over Redis, designed for communication between components of a distributed system. It uses Redis Streams for request transport and Redis Pub/Sub for response delivery, with pickle serialization.

## Installation

```bash
pip install callite
```

To use the MCP bridge (for AI agent integration):

```bash
pip install callite[mcp]
```

## Prerequisites

A running Redis instance is required. You can start one locally:

```bash
docker run -d -p 6379:6379 redis:alpine
```

Or use the included Docker Compose setup for a full development environment:

```bash
docker-compose up
```

## Usage

### Request/Response (register + execute)

The most common pattern: the client sends a request and waits for a response.

**Server:**

```python
from callite.server import RPCServer

server = RPCServer("redis://localhost:6379/0", "my_service")

@server.register
def healthcheck():
    return "OK"

@server.register
def add(a, b):
    return a + b

server.run_forever()
```

**Client:**

```python
from callite.client import RPCClient

client = RPCClient("redis://localhost:6379/0", "my_service")

# Positional arguments
result = client.execute("add", 1, 2)
print(result)  # 3

# Keyword arguments
result = client.execute("add", a=10, b=20)
print(result)  # 30

# No arguments
status = client.execute("healthcheck")
print(status)  # "OK"

# Close when done
client.close()
```

`execute()` blocks until the server responds or the timeout is reached (default: 30 seconds). If the server-side handler raises an exception, `execute()` re-raises it on the client side.

### Fire-and-Forget (subscribe + publish)

For one-way messages where the client does not need a response.

**Server:**

```python
from callite.server import RPCServer

server = RPCServer("redis://localhost:6379/0", "my_service")

@server.subscribe
def log(message):
    print(f"Received: {message}")

server.run_forever()
```

**Client:**

```python
from callite.client import RPCClient

client = RPCClient("redis://localhost:6379/0", "my_service")

# publish() returns immediately without waiting for a response
client.publish("log", "Something happened")
```

### Combining Both Patterns

A single service can use both `register` and `subscribe`:

```python
from callite.server import RPCServer

server = RPCServer("redis://localhost:6379/0", "my_service")

@server.register
def add(a, b):
    return a + b

@server.subscribe
def log(message):
    print(message)

server.run_forever()
```

```python
from callite.client import RPCClient

client = RPCClient("redis://localhost:6379/0", "my_service")

# Request/response
result = client.execute("add", 3, 4)

# Fire-and-forget
client.publish("log", f"Result was {result}")
```

### MCP Integration (AI Agent Integration)

Callite includes three MCP (Model Context Protocol) components for bridging AI agents to RPC services. All three are available from `callite.mcp`:

```python
from callite.mcp import MCPBridge, MCPHTTPProxy, MCPProxy
```

#### Defining Tools and Prompts

`register_tool` works like `register` but extracts type hints and docstrings to generate rich metadata for MCP tool discovery:

```python
from callite.server import RPCServer

server = RPCServer("redis://localhost:6379/0", "data_service")

@server.register_tool(description="Add two numbers together")
def add(a: int, b: int) -> int:
    """Add two numbers.

    Args:
        a: The first number.
        b: The second number.
    """
    return a + b

server.run_forever()
```

`register_prompt` registers prompt templates that AI agents can discover and invoke:

```python
@server.register_prompt(description="Analyze a dataset")
def analyze(data: str, focus: str = "general") -> str:
    return f"Please analyze the following data with focus on {focus}:\n{data}"
```

#### MCPBridge

A multi-service MCP gateway that discovers callite services via their `__describe__` endpoint and registers all tools, prompts, and resources as MCP primitives. Tool names are prefixed with the service name (e.g. `data_service_add`).

From the command line:

```bash
mcp-callite-bridge --redis redis://localhost:6379/0 --services data_service
```

Or programmatically:

```python
from callite.mcp import MCPBridge

bridge = MCPBridge("redis://localhost:6379/0", ["data_service", "auth_service"])
bridge.run()  # stdio transport by default
```

CLI options:

| Flag | Description | Default |
|---|---|---|
| `--redis` | Redis connection URL | `redis://localhost:6379/0` |
| `--services` | Comma-separated service names | (required) |
| `--transport` | `stdio` or `streamable-http` | `stdio` |
| `--name` | MCP server display name | `callite-bridge` |
| `--timeout` | RPC execution timeout (seconds) | `30` |
| `--queue-prefix` | Redis key prefix | `/callite` |

#### MCPHTTPProxy

An HTTP server that exposes a single callite service over Streamable HTTP. Unlike MCPBridge, tool names are not prefixed with the service name, making it suitable for single-service deployments.

```python
from callite.client import RPCClient
from callite.mcp import MCPHTTPProxy

client = RPCClient("redis://localhost:6379/0", "data_service")
proxy = MCPHTTPProxy(client, host="0.0.0.0", port=8080)
proxy.run()  # serves at /mcp
```

#### MCPProxy

A client-side proxy that manages an external MCP server subprocess (e.g. `uvx mcp-server-sqlite`) over stdio and exposes its tools through a synchronous Python API. Use `register_proxy` on an RPCServer to re-publish external MCP tools as callite RPC methods.

```python
from callite.mcp import MCPProxy
from callite.server import RPCServer

server = RPCServer("redis://localhost:6379/0", "my_service")

proxy = MCPProxy("uvx", ["mcp-server-sqlite", "--db-path", "test.db"])
server.register_proxy(proxy, prefix="sqlite")

server.run_forever()
```

The proxy auto-reconnects if the subprocess crashes, caches the tool list, and is thread-safe.

## Configuration

### Environment Variables

| Variable | Description | Default |
|---|---|---|
| `LOG_LEVEL` | Logging verbosity (`DEBUG`, `INFO`, `ERROR`, etc.) | `ERROR` (server), `INFO` (client) |
| `EXECUTION_TIMEOUT` | Client-side timeout in seconds for `execute()` | `30` |
| `REDIS_URL` | Redis URL (used by the MCP bridge CLI) | `redis://localhost:6379/0` |

### Constructor Options

**RPCServer:**

```python
RPCServer(
    conn_url="redis://localhost:6379/0",
    service="my_service",
    queue_prefix="/callite",       # Redis key prefix
    xread_groupname="generic",     # Consumer group name
)
```

**RPCClient:**

```python
RPCClient(
    conn_url="redis://localhost:6379/0",
    service="my_service",
    execution_timeout=30,          # Timeout in seconds
    queue_prefix="/callite",       # Redis key prefix
)
```

## Docker Development

The included `docker-compose.yml` starts Redis, a sample server, and a sample client:

```bash
docker-compose up
```

This runs the example `main.py` (server) and `healthcheck.py` (client stress test with 100 concurrent threads).

## License

Proprietary
