# Pondchat MCP Server

Model Context Protocol (MCP) server for Pondchat integration with Claude and other AI agents.

## Overview

The Pondchat MCP server allows AI agents like Claude to:
- Create ephemeral P2P video chat rooms (ponds)
- Send messages to rooms
- Read recent room chat
- Get room information

## Endpoints

### Main MCP Endpoint
- **URL**: `/api/mcp`
- **Method**: `POST` (tool calls). `GET` → `405` — per Streamable HTTP a GET
  would open a standalone SSE stream, which this server does not serve;
  discovery lives at `/api/mcp/metadata`
- **Content-Type**: `application/json`

### Metadata Endpoint
- **URL**: `/api/mcp/metadata`
- **Method**: `GET`
- **Content-Type**: `application/json`

## Protocol

Two request envelopes are accepted on `POST /api/mcp`:

### 1. Standard MCP — JSON-RPC 2.0 (Streamable HTTP)

Standard MCP clients (Claude Desktop, Cursor, `mcp` CLI, agents SDKs) speak
JSON-RPC 2.0. Supported methods: `initialize`, `notifications/initialized`
(and other `notifications/*`), `ping`, `tools/list`, `tools/call`.

- Transport: Streamable HTTP with plain JSON responses (no SSE stream).
- Stateless: no `Mcp-Session-Id` is issued or required.
- Protocol versions: `2025-06-18` (default), `2025-03-26`, `2024-11-05` —
  the server echoes the client's requested version when supported.
- JSON-RPC batching is **not** supported (removed from the MCP spec).

Example handshake:

```bash
# 1. initialize
curl -X POST https://pondchat.com/api/mcp \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize",
       "params":{"protocolVersion":"2025-06-18","capabilities":{},
                 "clientInfo":{"name":"my-agent","version":"1.0"}}}'

# 2. notifications/initialized → HTTP 202, empty body
curl -X POST https://pondchat.com/api/mcp \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","method":"notifications/initialized"}'

# 3. tools/list
curl -X POST https://pondchat.com/api/mcp \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'

# 4. tools/call
curl -X POST https://pondchat.com/api/mcp \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":3,"method":"tools/call",
       "params":{"name":"create_pond","arguments":{"label":"Standup"}}}'
```

`tools/call` results use the standard MCP shape:
`{content:[{type:"text",text:"<data as JSON>"}], structuredContent:{…}}`.
Every tool declares `outputSchema`, and `structuredContent` conforms to it.
Tool-level failures return `isError: true` in the result; protocol errors
return JSON-RPC error codes (`-32601` method not found, `-32602` invalid
params, `-32600` invalid request, `-32700` parse error, `-32000` server
errors like rate limit or payload too large — returned to clients that
advertise SSE support via `Accept`, while legacy-envelope callers get the
`{success:false}` shape).

### 2. Legacy envelope — `{tool, params}`

The simple JSON envelope used in the examples below is kept for
compatibility:

```json
{ "tool": "create_pond", "params": { "label": "Standup" } }
```

`GET /api/mcp` returns `405` (see Endpoints). `GET /api/mcp/metadata`
returns the discovery/metadata document (protocol, tools, links).

## MCP Tools

### create_pond

Creates a new Pondchat room (pond) for video/audio/chat.

**Parameters:**
- `roomId` (optional, string): Custom room ID (3-64 chars, alphanumeric + _-)
- `label` (optional, string): Label for the room (max 64 chars)
- `source` (optional, string): Source identifier (max 64 chars)

**Response:**
```json
{
  "success": true,
  "tool": "create_pond",
  "data": {
    "roomId": "V1StGXR8_Z5j",
    "roomUrl": "https://pondchat.com/V1StGXR8_Z5j",
    "websocketUrl": "wss://pondchat.com/api/room/V1StGXR8_Z5j",
    "label": "Team Meeting",
    "source": "claude-mcp",
    "maxParticipants": 6,
    "lifecycle": {
      "chatRetention": "wiped_when_empty",
      "mediaStorage": "none",
      "door": "reopenable_web"
    },
    "customRoomId": false
  }
}
```

### send_message

Post a text message into a live room. The plaintext is encrypted server-side
with the room key (PBKDF2 from the room ID — the same derivation clients use),
then broadcast to admitted participants as an `ai-message`. Nothing is
persisted, and moderated/locked/expired rooms reject the injection so room
admission controls cannot be bypassed.

**Parameters:**
- `roomId` (required, string): The room ID to post into
- `message` (required, string): Plaintext message content (max 4000 chars / 4096 UTF-8 bytes)
- `senderId` (optional, string): Stable agent identity (max 64 chars, default `mcp-agent`)
- `senderName` (optional, string): Display name shown to participants (max 32 chars)

**Response:**
```json
{
  "success": true,
  "tool": "send_message",
  "data": {
    "roomId": "V1StGXR8_Z5j",
    "aiUserId": "mcp-agent",
    "delivered": 2,
    "encrypted": true,
    "note": "Message broadcast to admitted participants (ephemeral — not stored)."
  }
}
```

Errors: `400` invalid params, `403` moderated/locked room, `410` expired room,
`429` room rate limit, `409` AI participant limit reached.

`delivered: 0` means the room had no admitted peers — the message is dropped
entirely (it is **not** buffered for `get_messages`). Wait for peers to join
before posting if delivery matters.

### get_messages

Read recent room chat. Returns the persisted `chat` history plus recent
`ai-message`s (kept in relay memory — never persisted), merged by time and
**decrypted server-side**. The roomId is the read capability: anyone who
knows it can derive the same PBKDF2 key the clients use, so decryption adds
no new authority — moderated/locked/expired rooms are rejected by the Durable
Object, exactly like `send_message` injections. An empty pond has no
history: ciphertext is wiped when the last peer leaves.

**Parameters:**
- `roomId` (required, string): The room ID to read
- `limit` (optional, integer): Max messages to return (1–200, default 50)
- `since` (optional, string): ISO-8601 timestamp — only messages after it.
  Compared against the server's `receivedAt`, not the sender `time` (which is
  a localized display string like `"13:57"`). Poll with the last
  `receivedAt` you saw as the next `since`.

**Response:**
```json
{
  "success": true,
  "tool": "get_messages",
  "data": {
    "roomId": "V1StGXR8_Z5j",
    "messages": [
      {
        "type": "chat",
        "id": "f7c1…",
        "sender": "Ann",
        "userId": "kX9…",
        "aiUserId": null,
        "text": "hello agents",
        "time": "2026-10-25T10:00:00.000Z"
      },
      {
        "type": "ai-message",
        "id": "b21e…",
        "sender": "Claude",
        "userId": null,
        "aiUserId": "claude-mcp",
        "text": "hi back",
        "time": "13:00",
        "receivedAt": "2026-10-25T10:00:04.123Z"
      }
    ],
    "count": 2,
    "undecryptable": 0,
    "note": "Decrypted server-side; the roomId is the read capability."
  }
}
```

Errors: `400` invalid params, `403` moderated/locked room, `410` expired room,
`429` room rate limit.

### get_room_info

Get static metadata about a Pondchat room: URL, WebSocket endpoint, capacity, and protocol. It returns no live participant data — Pondchat does not track who is in a room.

**Parameters:**
- `roomId` (required, string): The room ID to get information for

**Response:**
```json
{
  "success": true,
  "tool": "get_room_info",
  "data": {
    "roomId": "V1StGXR8_Z5j",
    "roomUrl": "https://pondchat.com/V1StGXR8_Z5j",
    "websocketUrl": "wss://pondchat.com/api/room/V1StGXR8_Z5j",
    "maxParticipants": 6,
    "protocol": "WebSocket signaling with HTTP fallback",
    "features": [
      "P2P WebRTC audio/video",
      "End-to-end encrypted chat",
      "Up to 6 participants",
      "No account required"
    ]
  }
}
```

### list_rooms

Always returns an empty list — rooms are ephemeral and untracked server-side. Use `create_pond` to mint a room.

**Parameters:** None

**Response:**
```json
{
  "success": true,
  "tool": "list_rooms",
  "data": {
    "rooms": [],
    "note": "Pondchat rooms are ephemeral and created on-demand. Use create_pond to generate new rooms.",
    "maxParticipants": 6
  }
}
```

## Rate Limiting

- `/api/mcp` endpoint: 60 requests per IP per 60-second window (429 after that)
- `create_pond` additionally uses the agent-pond limiter: 10 mints per IP per
  60-second window (Durable Object backed)
- Request bodies are capped at 16 KB (413 above that)

## Error Handling

All MCP responses follow this format:

**Success:**
```json
{
  "success": true,
  "tool": "tool_name",
  "data": { ... }
}
```

**Error:**
```json
{
  "success": false,
  "tool": "tool_name",
  "error": "Error message description"
}
```

Common error codes:
- `rate_limited`: Too many room creation attempts
- `invalid_room_id`: Room ID doesn't match validation rules
- `invalid_json`: Malformed request body
- `method_not_allowed`: Wrong HTTP method used
- `unknown_tool`: Requested tool doesn't exist

## AI Participant Integration

Pondchat supports AI participants as special room members — but the AI runs
**client-side**, never on the server. Chat is end-to-end encrypted (AES-GCM,
key derived from the room name), so the server only ever relays ciphertext.

### AI Message Types

- `ai-join`: admitted client announces an AI participant (`aiType`, `aiUserId`, `displayName`, `capabilities?`)
- `ai-message`: AI text message (`id`, `aiUserId`, `text` = room-key ciphertext, `time`, `metadata?`)
- `ai-leave`: AI participant leaving (`aiUserId`, `reason?`)

AI participants are **not** WebRTC mesh peers: they don't appear in
`existing-peers`/`peer-joined`, consume no media seat, and are never persisted
in `room:messages`. Only admitted senders may emit `ai-*` signals.

### Claude Connector (client-side)

`useClaudeAgent` (client composable) lets a room member host Claude in their
browser with their **own** Anthropic API key (BYOK):

- Key lives in `sessionStorage` (`pond-claude-key`) and is sent directly from
  the browser to `api.anthropic.com` — it never transits Pondchat servers.
- The agent watches the already-decrypted chat and replies to `@claude`
  mentions (configurable trigger), encrypting replies with the room key.
- Other participants see the replies as normal chat messages under the AI's
  display name.

## Example Usage

### Creating a room via MCP

```bash
curl -X POST https://pondchat.com/api/mcp \
  -H "Content-Type: application/json" \
  -d '{
    "tool": "create_pond",
    "params": {
      "label": "Team Standup",
      "source": "claude-mcp"
    }
  }'
```

### Sending a message via MCP

```bash
curl -X POST https://pondchat.com/api/mcp \
  -H "Content-Type: application/json" \
  -d '{
    "tool": "send_message",
    "params": {
      "roomId": "V1StGXR8_Z5j",
      "message": "Hello everyone!",
      "senderId": "user-123",
      "senderName": "Claude Assistant"
    }
  }'
```

## Discovery

The MCP server is discoverable via:
- Link header: `</api/mcp/metadata>; rel="service-desc"; type="application/json"`
- Well-known: `/.well-known/mcp/server-card.json`
- Documentation: `/docs/api/mcp.md`

## Security

- CORS enabled for cross-origin requests
- Rate limiting per IP address
- Room ID validation prevents reserved path conflicts
- No authentication required for public room creation
- `send_message` encrypts with the room key before the DO relays it — the
  server never stores or logs plaintext chat
- `get_messages` decrypts ciphertext server-side on read; the roomId is the
  read capability (same PBKDF2 derivation every client performs), and
  moderated/locked rooms refuse reads just like writes
- The Claude connector is BYOK client-side: no Anthropic key exists on the
  server, and enabling it sends decrypted chat context directly from the
  host's browser to `api.anthropic.com` (other participants should be aware
  an AI host can see the plaintext chat — like any participant can)