File size: 5,493 Bytes
cb1e234
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
# MCP server support

SERPent now speaks two protocols from the same deployment:

| Audience | Endpoint |
|---|---|
| REST clients / Swagger UI | `https://<host>/` (docs), `POST /serp/...`, `/scrap/...`, `/ops/...` |
| MCP clients (Claude, Cursor, agents) | `https://<host>/mcp` β€” streamable HTTP transport |

## What changed

| File | Change |
|---|---|
| `mcp_server.py` | **New.** Builds the MCP server from the FastAPI app and mounts it. |
| `app.py` | Two lines: `from mcp_server import mount_mcp_server` and `mcp = mount_mcp_server(app)` after the `include_router` calls. |
| `requirements.txt` | Added `fastmcp>=3.0,<4`. |
| `Dockerfile` | Unchanged β€” same image, same port 7860, same `CMD`. |

## How it works

`FastMCP.from_fastapi()` reads the app's OpenAPI schema and turns every route
into an MCP tool. When a tool is called, the request is dispatched **back into
the same FastAPI app in-process** over an ASGI transport β€” no network hop, no
second server, no duplicated business logic. Add an endpoint, get a tool.

The 12 tools generated today:

```
search                 search_arxiv          search_google_scholar
search_patents         search_brave          search_bing
search_duck            scrap_patent          scrap_patents
ops_keyword_search     ops_get_patent        ops_get_patents_bulk
```

Tool names come from the Python handler names rather than FastAPI's generated
operation ids, so agents see `search_arxiv` instead of
`search_arxiv_serp_search_arxiv_post`. Descriptions and JSON schemas come from
your docstrings and Pydantic models β€” improving a docstring improves the tool.
To pin a specific tool name, set `operation_id="..."` on the route decorator.

## Connecting a client

**Claude Code / any streamable-HTTP client:**

```bash
claude mcp add --transport http serpent https://<your-space>.hf.space/mcp
```

**`.mcp.json` / `claude_desktop_config.json`:**

```json
{
  "mcpServers": {
    "serpent": {
      "type": "http",
      "url": "https://<your-space>.hf.space/mcp"
    }
  }
}
```

**Private HF Space** β€” pass your token:

```json
{
  "mcpServers": {
    "serpent": {
      "type": "http",
      "url": "https://<your-space>.hf.space/mcp",
      "headers": { "Authorization": "Bearer hf_..." }
    }
  }
}
```

**If `SERPENT_API_KEY` is set on the deployment** (see Configuration below),
also pass it, either as `X-API-Key` or reusing the `Authorization: Bearer`
header:

```json
{
  "mcpServers": {
    "serpent": {
      "type": "http",
      "url": "https://<your-space>.hf.space/mcp",
      "headers": { "X-API-Key": "..." }
    }
  }
}
```

**Quick check without a client:**

```bash
curl -X POST http://localhost:7860/mcp \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{
        "protocolVersion":"2025-06-18","capabilities":{},
        "clientInfo":{"name":"curl","version":"1"}}}'
```

Or with the FastMCP CLI: `fastmcp inspect http://localhost:7860/mcp`

## Configuration

All optional, all environment variables:

| Variable | Default | Purpose |
|---|---|---|
| `MCP_ENABLED` | `1` | Set to `0` to run REST-only. |
| `MCP_PATH` | `/mcp` | Where the MCP endpoint lives. |
| `MCP_STATELESS` | `1` | Stateless HTTP β€” no session affinity needed behind the HF Spaces proxy. Set `0` for resumable SSE streams. |
| `MCP_ALLOWED_HOSTS` | `*` | Comma-separated Host allow-list (DNS-rebinding protection). |
| `MCP_ALLOWED_ORIGINS` | `*` | Comma-separated Origin allow-list (browser clients). |
| `SERPENT_API_KEY` | unset | When set, every REST and MCP request must carry a matching `X-API-Key` or `Authorization: Bearer <key>` header (except `/` and `/openapi.json`). Unset = fully open, same as before. Set this to stop the deployment being usable as an anonymous scraping proxy by anyone who finds the URL. |

## Notes and caveats

- **Ordering matters.** `mount_mcp_server(app)` must come after every
  `include_router()` call. Routes registered after it are not exposed as tools,
  and are not reachable at all β€” the MCP app is mounted at the root so that a
  bare `POST /mcp` answers directly instead of 307-redirecting to `/mcp/`
  (a redirect some MCP clients mishandle).
- **Lifespans are combined**, so Playwright still starts up and shuts down
  exactly as before, alongside the MCP session manager.
- **The `ops_*` tools** are exposed but return 503 without
  `OPS_CONSUMER_KEY` / `OPS_CONSUMER_SECRET`. The server instructions tell
  agents to prefer the regular tools, which already fall back to OPS.
- **Auth.** `SERPENT_API_KEY` (see Configuration) gates both REST and MCP
  uniformly via an HTTP-level middleware, so it applies before FastMCP's
  in-process dispatch and needs no per-tool wiring. It's separate from
  whatever HF Space visibility (public/private) or token you also use.
- **Tool count.** 12 tools with six overlapping search backends is a lot of
  surface for a model to choose from. If tool selection gets noisy, pass
  `route_maps=[...]` to `FastMCP.from_fastapi()` in `mcp_server.py` to exclude
  the per-backend tools and keep `search`, `search_arxiv`, `search_patents`,
  `search_google_scholar` and the scrapers.

## Verified

Tested against `fastmcp 3.4.7` / `fastapi 0.141.1`: server boots, all 12 tools
list over HTTP with correct schemas, tool calls round-trip through the real
endpoints, and the existing REST routes (`/`, `/openapi.json`, `/redoc`,
`/serp/*`) are unaffected.