File size: 4,727 Bytes
7ecfcd4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
# 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.