Spaces:
Sleeping
Sleeping
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.
|