Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions docs/ai-assistant/developer-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ Restart the server after setting `API_KEY`. All incoming MCP requests (from Clau

Give your media buying team:

1. **MCP URL**: `http://your-server:8001/mcp/sse/sse` (or your public URL)
1. **MCP URL**: `http://your-server:8001/mcp` (Streamable HTTP, canonical — or your public URL)
2. **API key**: the value you set in `API_KEY`

They'll connect Claude Desktop using the [Claude Desktop Setup Guide](../claude-desktop-setup.md) and complete the business configuration (deal templates, approval thresholds, seller API keys) through the interactive setup wizard.
Expand All @@ -133,7 +133,11 @@ curl http://localhost:8001/health
curl http://localhost:8001/api/v1/setup/status

# MCP tools list (requires running SSE client — use Claude Desktop or curl with SSE)
curl -N http://localhost:8001/mcp/sse/sse
curl -s -X POST http://localhost:8001/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","method":"initialize","id":1,"params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"smoke","version":"1"}}}'
# Legacy SSE (older clients only): curl -N http://localhost:8001/mcp-sse/sse
```

Expected health response:
Expand Down
23 changes: 14 additions & 9 deletions docs/architecture/mcp-server.md
Original file line number Diff line number Diff line change
@@ -1,42 +1,44 @@
# MCP Server

The buyer agent exposes a FastMCP SSE server that allows AI assistants --- Claude Desktop, ChatGPT, Cursor, Windsurf, and others --- to call buyer operations as structured tools. This is distinct from the buyer's outbound MCP client, which calls seller agents.
The buyer agent exposes a FastMCP server that allows AI assistants --- Claude Desktop, ChatGPT, Cursor, Windsurf, and others --- to call buyer operations as structured tools. This is distinct from the buyer's outbound MCP client, which calls seller agents.

!!! info "Two MCP roles"
The buyer agent is both an **MCP client** (calling sellers via `IABMCPClient`) and an **MCP server** (serving AI assistants via `FastMCP`). These are independent: the client speaks to seller endpoints; the server speaks to AI assistants connected by users.

| Direction | Component | Purpose |
|-----------|-----------|---------|
| Outbound | `IABMCPClient` | Buyer calls seller MCP servers to browse inventory and book deals |
| Inbound | FastMCP SSE server | AI assistants call the buyer's MCP server to operate the buyer agent |
| Inbound | FastMCP server | AI assistants call the buyer's MCP server to operate the buyer agent |

---

## Mounting

The MCP server is a [FastMCP](https://github.com/jlowin/fastmcp) SSE application mounted on the main FastAPI process:
The MCP server mounts two transports on the main FastAPI process:

```
FastAPI (port 8001)
POST /bookings
GET /bookings/{job_id}
GET /health
...
MOUNT /mcp/sse <-- FastMCP SSE app
MOUNT /mcp <-- FastMCP Streamable HTTP app (canonical, MCP standard 2025-06-18)
MOUNT /mcp-sse <-- FastMCP SSE app (legacy fallback for older MCP clients)
```

The mount is a single call in `interfaces/api/main.py`:

```python
from ad_buyer.interfaces.mcp_server import mount_mcp
mount_mcp(app) # Creates /mcp/sse
mount_mcp(app) # Creates /mcp (Streamable HTTP) and /mcp-sse (legacy SSE)
```

`mount_mcp` calls `mcp.sse_app()` and mounts the resulting ASGI application under `/mcp/sse`. Due to Starlette sub-app routing, the FastMCP SSE app exposes its own `/sse` path internally, making the canonical client URL `http://<host>:8001/mcp/sse/sse`. Connecting to bare `/mcp/sse` returns a 307 redirect that most MCP clients cannot follow.
`mount_mcp` calls `mcp.streamable_http_app()` (canonical) and `mcp.sse_app()` (legacy fallback).
The canonical client URL is `http://<host>:8001/mcp`. For legacy SSE clients, use `http://<host>:8001/mcp-sse/sse`.

### Auth middleware note

The FastAPI `api_key_auth_middleware` applies to all HTTP paths. The MCP SSE path (`/mcp/sse`) is not in the public path exemption list (`/health`, `/docs`, `/openapi.json`, `/redoc`), so it passes through the key check. When `settings.api_key` is non-empty, MCP clients must send `X-API-Key: <key>` on the initial SSE connection. When `settings.api_key` is empty (default for local development), the middleware skips authentication entirely.
The FastAPI `api_key_auth_middleware` applies to all HTTP paths. Neither `/mcp` nor `/mcp-sse` is in the public path exemption list (`/health`, `/docs`, `/openapi.json`, `/redoc`), so both pass through the key check. When `settings.api_key` is non-empty, MCP clients must send `X-API-Key: <key>` on the initial connection. When `settings.api_key` is empty (default for local development), the middleware skips authentication entirely.

---

Expand Down Expand Up @@ -115,15 +117,18 @@ graph TB

subgraph BuyerAgent["Ad Buyer Agent (port 8001)"]
FastAPI["FastAPI"]
SSE["/mcp/sse/sse<br/>(FastMCP SSE)"]
StreamableHTTP["/mcp<br/>(FastMCP Streamable HTTP — canonical)"]
SSE["/mcp-sse/sse<br/>(FastMCP SSE — legacy fallback)"]
Tools["MCP Tool Functions<br/>(12 categories, 40+ tools)"]
Stores["Store Accessors<br/>DealStore / CampaignStore / OrderStore"]
DB[(SQLite)]
end

SellerMCP["Seller MCP Server<br/>(outbound, separate path)"]

AI -->|"MCP / SSE"| SSE
AI -->|"Streamable HTTP (current)"| StreamableHTTP
AI -.->|"SSE (legacy clients)"| SSE
StreamableHTTP --> FastAPI
SSE --> FastAPI
FastAPI -->|"route to tools"| Tools
Tools --> Stores
Expand Down
8 changes: 5 additions & 3 deletions docs/claude-desktop-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Works on both **Claude Desktop** and **Claude on the web** (claude.ai):
1. Open Claude Desktop or go to [claude.ai](https://claude.ai)
2. Go to **Settings > Integrations**
3. Click **"+ Add Custom Integration"**
4. Enter your buyer agent's MCP URL: `https://your-buyer.example.com/mcp/sse/sse`
4. Enter your buyer agent's MCP URL: `https://your-buyer.example.com/mcp`
5. If prompted for authentication, enter your operator API key
6. Click **Save**

Expand All @@ -41,12 +41,14 @@ For buyer agents running on `localhost`:
{
"mcpServers": {
"buyer-agent": {
"url": "http://localhost:8001/mcp/sse/sse"
"url": "http://localhost:8001/mcp"
}
}
}
```

> **Legacy SSE clients**: If you are using an older MCP client that requires SSE transport, use `http://localhost:8001/mcp-sse/sse` instead. Streamable HTTP (`/mcp`) is the canonical endpoint for all current MCP clients.

4. Save and restart Claude Desktop

> **Note**: The JSON config method is for **local servers only**. Remote servers must use the Settings > Integrations UI.
Expand Down Expand Up @@ -173,7 +175,7 @@ The same MCP endpoint works with other AI platforms:
3. Fully quit and relaunch Claude Desktop — it only reads the config at startup
4. Check Claude Desktop logs for connection errors (macOS: `~/Library/Logs/Claude/`)

### Connection refused on `http://localhost:8001/mcp/sse/sse`
### Connection refused on `http://localhost:8001/mcp`

The buyer server is not running or crashed. Start it with:

Expand Down
18 changes: 10 additions & 8 deletions docs/guides/deployment-ops-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -649,12 +649,14 @@ Pacing alert levels:

The buyer agent exposes its own MCP server for external clients (Claude Desktop, Cursor, Windsurf, custom agents). The MCP server is mounted automatically on the FastAPI app at startup and exposes buyer operations as structured tools.

MCP endpoint:
MCP endpoint (Streamable HTTP, canonical):

```
http://localhost:8001/mcp/sse/sse
http://localhost:8001/mcp
```

Legacy SSE fallback (for older MCP clients): `http://localhost:8001/mcp-sse/sse`

Available tool categories:

| Category | Tools |
Expand All @@ -673,7 +675,7 @@ Add the buyer agent to your Claude Desktop MCP configuration (`~/Library/Applica
"command": "npx",
"args": [
"mcp-remote",
"http://localhost:8001/mcp/sse/sse"
"http://localhost:8001/mcp"
]
}
}
Expand All @@ -684,13 +686,13 @@ Restart Claude Desktop after editing the configuration. The buyer agent tools wi

### Connecting Other MCP Clients

Any client supporting Streamable HTTP (SSE) transport can connect:
Any client supporting Streamable HTTP transport can connect:

```python
from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client

async with streamablehttp_client("http://localhost:8001/mcp/sse/sse") as (read, write, _):
async with streamablehttp_client("http://localhost:8001/mcp") as (read, write, _):
async with ClientSession(read, write) as session:
await session.initialize()
tools = await session.list_tools()
Expand All @@ -705,7 +707,7 @@ The buyer agent acts as an **MCP client** to seller agents (in addition to expos
SELLER_ENDPOINTS=http://seller1.example.com:8000,http://seller2.example.com:8000
```

The buyer's `UnifiedClient` connects to the seller's MCP SSE endpoint at `{base_url}/mcp/sse`. Protocol selection is automatic — MCP for structured tool calls, A2A for discovery and negotiation.
The buyer's `UnifiedClient` connects to the seller's MCP SSE endpoint at `{base_url}/mcp-sse/sse`. Protocol selection is automatic — MCP for structured tool calls, A2A for discovery and negotiation.

**Test seller MCP connectivity manually:**

Expand Down Expand Up @@ -971,7 +973,7 @@ SELLER_ENDPOINTS=http://host.docker.internal:8000
Test the seller's MCP endpoint directly:

```bash
curl -N http://seller.example.com:8000/mcp/sse # Should stream SSE events
curl -N http://seller.example.com:8000/mcp-sse/sse # Should stream SSE events
```

---
Expand Down Expand Up @@ -1019,7 +1021,7 @@ If not installed, install it: `pip install mcp`

```bash
# The SSE endpoint should keep the connection open
curl -N -H "Accept: text/event-stream" http://seller.example.com:8000/mcp/sse
curl -N -H "Accept: text/event-stream" http://seller.example.com:8000/mcp-sse/sse
```

**Check 3 — Firewall / security groups:**
Expand Down
16 changes: 9 additions & 7 deletions docs/multi-client-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ Connect your buyer agent to ChatGPT, OpenAI Codex, Cursor, Windsurf, or any MCP-

Same as [Claude Desktop Setup](claude-desktop-setup.md) — your developer must have deployed the buyer agent and generated credentials.

Your buyer agent MCP endpoint: `https://your-buyer.example.com/mcp/sse/sse`
Your buyer agent MCP endpoint: `https://your-buyer.example.com/mcp` (Streamable HTTP — canonical)

> **Legacy SSE fallback**: Older MCP clients that require SSE transport can connect to `https://your-buyer.example.com/mcp-sse/sse` instead.

---

Expand All @@ -26,7 +28,7 @@ ChatGPT natively supports MCP servers via Developer Mode.

1. Go to **Settings > Connectors** (or **Settings > Apps**)
2. Click **Create**
3. Enter your MCP server URL: `https://your-buyer.example.com/mcp/sse/sse`
3. Enter your MCP server URL: `https://your-buyer.example.com/mcp`
4. Name it: `Buyer Agent`
5. Add a description: `Manage campaigns, deals, pacing, approvals, and seller relationships`
6. Click **Create**
Expand All @@ -49,7 +51,7 @@ Codex supports MCP servers via its config file.
### Option A: CLI

```bash
codex mcp add buyer-agent --url https://your-buyer.example.com/mcp/sse/sse
codex mcp add buyer-agent --url https://your-buyer.example.com/mcp
```

### Option B: Config File
Expand All @@ -58,7 +60,7 @@ Edit `~/.codex/config.toml` (global) or `.codex/config.toml` (project):

```toml
[mcp_servers.buyer-agent]
url = "https://your-buyer.example.com/mcp/sse/sse"
url = "https://your-buyer.example.com/mcp"
bearer_token_env_var = "BUYER_AGENT_API_KEY"
```

Expand Down Expand Up @@ -86,7 +88,7 @@ Create `.cursor/mcp.json` in your project root:
{
"mcpServers": {
"buyer-agent": {
"url": "https://your-buyer.example.com/mcp/sse/sse",
"url": "https://your-buyer.example.com/mcp",
"headers": {
"Authorization": "Bearer sk-operator-XXXXX"
}
Expand All @@ -105,7 +107,7 @@ Create `~/.cursor/mcp.json` with the same format.
{
"mcpServers": {
"buyer-agent": {
"url": "https://your-buyer.example.com/mcp/sse/sse",
"url": "https://your-buyer.example.com/mcp",
"headers": {
"Authorization": "Bearer ${env:BUYER_AGENT_API_KEY}"
}
Expand Down Expand Up @@ -134,7 +136,7 @@ Edit `~/.codeium/windsurf/mcp_config.json`:
{
"mcpServers": {
"buyer-agent": {
"serverUrl": "https://your-buyer.example.com/mcp/sse/sse",
"serverUrl": "https://your-buyer.example.com/mcp",
"headers": {
"Authorization": "Bearer sk-operator-XXXXX"
}
Expand Down
7 changes: 5 additions & 2 deletions src/ad_buyer/interfaces/mcp_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -2884,10 +2884,13 @@ async def help_prompt() -> list[Message]:
def mount_mcp(app: FastAPI) -> None:
"""Mount the MCP server onto a FastAPI application.

Creates a Streamable HTTP endpoint at /mcp (MCP standard 2025-06-18).
Mounts both transports:
- Streamable HTTP at /mcp (current MCP standard, protocol 2025-06-18)
- Legacy SSE at /mcp-sse (deprecated, kept for backwards compat with older clients)

Args:
app: The FastAPI application to mount onto.
"""
app.mount("/mcp", mcp.streamable_http_app())
logger.info("MCP server mounted: Streamable HTTP at /mcp")
app.mount("/mcp-sse", mcp.sse_app())
logger.info("MCP server mounted: Streamable HTTP at /mcp, legacy SSE at /mcp-sse/sse")
27 changes: 18 additions & 9 deletions tests/unit/test_mcp_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ def test_mcp_server_has_instructions(self):
assert len(mcp.instructions) > 0


class TestSSEMounting:
class TestMCPMounting:
"""Test that the MCP server can be mounted in the FastAPI app."""

def test_mount_mcp_function_exists(self):
Expand All @@ -57,36 +57,45 @@ def test_mount_mcp_function_exists(self):
assert callable(mount_mcp)

def test_mount_mcp_adds_route(self):
"""mount_mcp should add the /mcp/sse route to the FastAPI app."""
"""mount_mcp should add both /mcp (Streamable HTTP) and /mcp-sse (legacy SSE) routes."""
from fastapi import FastAPI

from ad_buyer.interfaces.mcp_server import mount_mcp

test_app = FastAPI()
mount_mcp(test_app)

# Check that a route at /mcp/sse is mounted
# Check that routes for both transports are mounted
route_paths = []
for route in test_app.routes:
if hasattr(route, "path"):
route_paths.append(route.path)

# The mount should be at /mcp/sse
assert any("/mcp/sse" in str(p) for p in route_paths), (
f"Expected /mcp/sse in routes, got: {route_paths}"
# Streamable HTTP transport (current MCP standard)
assert any("/mcp" == str(p) or str(p).startswith("/mcp") for p in route_paths), (
f"Expected /mcp (Streamable HTTP) in routes, got: {route_paths}"
)
# Legacy SSE transport (backwards compat for older clients)
assert any("/mcp-sse" in str(p) for p in route_paths), (
f"Expected /mcp-sse (legacy SSE) in routes, got: {route_paths}"
)

def test_buyer_api_app_has_mcp_mounted(self):
"""The buyer API app should have MCP mounted after import."""
"""The buyer API app should have both MCP transports mounted after import."""
from ad_buyer.interfaces.api.main import app

route_paths = []
for route in app.routes:
if hasattr(route, "path"):
route_paths.append(route.path)

assert any("/mcp/sse" in str(p) for p in route_paths), (
f"Expected /mcp/sse in buyer API app routes, got: {route_paths}"
# Streamable HTTP transport (canonical)
assert any("/mcp" == str(p) or (str(p).startswith("/mcp") and not str(p).startswith("/mcp-sse")) for p in route_paths), (
f"Expected /mcp (Streamable HTTP) in buyer API app routes, got: {route_paths}"
)
# Legacy SSE transport
assert any("/mcp-sse" in str(p) for p in route_paths), (
f"Expected /mcp-sse (legacy SSE) in buyer API app routes, got: {route_paths}"
)


Expand Down
Loading
Loading