Spaces:
Sleeping
Sleeping
| # 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_..." } | |
| } | |
| } | |
| } | |
| ``` | |
| **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). | | |
| ## 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.** The MCP endpoint inherits whatever protects the Space. If you later | |
| want per-token auth on MCP only, FastMCP takes an `auth=` provider in | |
| `build_mcp_server()`. | |
| - **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. | |