You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: CHANGELOG.md
+1Lines changed: 1 addition & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -13,6 +13,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
13
13
14
14
### Breaking changes
15
15
16
+
- The HTTP transport now rate limits `tools/call`: each authenticated caller gets its own allowance (default 30 per second, burst 60) and anonymous callers share one (default 100 per second). A request over the limit is refused with `429` and a `Retry-After` header. Raise `MCP_RATE_LIMIT` / `MCP_RATE_LIMIT_BURST` / `MCP_RATE_LIMIT_ANONYMOUS` if you run high-throughput automation, or set `MCP_RATE_LIMIT=0` to disable.
16
17
- The HTTP transport no longer forwards a caller's `Authorization: Bearer` header to MediaWiki. Such a request is refused with `401`, because a token minted by the wiki was not issued for this server. Use [hosted OAuth sign-in](docs/deployment.md#hosted-oauth-sign-in), or set `MCP_ALLOW_BEARER_PASSTHROUGH=true` to keep the old behaviour while you migrate; it is deprecated and will be removed. The server now warns at startup when a wiki requires a signed-in user but neither hosted sign-in nor forwarding is available, since no request could then succeed.
17
18
- The server no longer advertises the wikis' own authorization servers, so a client can no longer discover where to mint a token to send here. Without hosted OAuth sign-in enabled, `/.well-known/oauth-protected-resource` now answers `404`, and `list-wikis` stops reporting each wiki's `authorizationServer`. Deployments running the hosted sign-in are unaffected.
18
19
- The `Origin` header is now validated on every bind, and a request carrying an unlisted origin is refused with `403`. If you serve a browser-based client from a public bind, set `MCP_ALLOWED_ORIGINS` before upgrading. Clients that send no `Origin` header, which is most of them, are unaffected.
Copy file name to clipboardExpand all lines: docs/deployment.md
+12-3Lines changed: 12 additions & 3 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -49,7 +49,7 @@ Serve a single wiki for anonymous reads: no sign-in, no writes. Good for public
49
49
50
50
`readOnly: true` together with `allowWikiManagement: false` hides the wiki-management tools (`add-wiki`, `remove-wiki`) and the six write tools (`create-page`, `update-page`, `delete-page`, `undelete-page`, `upload-file`, `upload-file-from-url`) from `tools/list`. What remains is an anonymous, read-only interface.
51
51
52
-
Then run it with `MCP_TRANSPORT=http` behind a reverse proxy that terminates TLS and applies rate limiting (Cloudflare, nginx, and Caddy all work), then set the [Host and Origin allowlists](#security-checklist).
52
+
Then run it with `MCP_TRANSPORT=http` behind a reverse proxy that terminates TLS (Cloudflare, nginx, and Caddy all work), then set the [Host and Origin allowlists](#security-checklist). The server itself [rate limits tool calls](#rate-limiting); IP-level limiting against anonymous floods still belongs at the proxy, which knows the caller's address when this server does not.
53
53
54
54
## Hosted OAuth sign-in
55
55
@@ -242,6 +242,9 @@ Set `MCP_TRANSPORT=http` to select this transport (the Docker image defaults to
242
242
|`MCP_TRUSTED_HOSTS`| unset | Comma-separated **outbound** SSRF-guard exemptions for internal destinations (e.g. `mediawiki.svc`). See [Outbound SSRF guard](#outbound-ssrf-guard). |
243
243
|`MCP_ALLOW_STATIC_FALLBACK`| unset | Allow HTTP startup when a wiki has static credentials, making them a shared fallback identity. See [Security checklist](#security-checklist). |
244
244
|`MCP_ALLOW_BEARER_PASSTHROUGH`| unset | Deprecated. Forward a caller's `Authorization` header to MediaWiki as that caller. Without it such a request is refused with `401`. See [Per-request bearer token](#per-request-bearer-token-http-transport-deprecated). |
245
+
|`MCP_RATE_LIMIT`|`30`| Sustained `tools/call` per second per authenticated caller. `0` disables rate limiting. See [Rate limiting](#rate-limiting). |
246
+
|`MCP_RATE_LIMIT_BURST`| 2 × rate | How far a caller's burst can run ahead of the sustained rate. |
247
+
|`MCP_RATE_LIMIT_ANONYMOUS`|`100`| Sustained `tools/call` per second across **all** anonymous callers combined (burst 2 × the rate, not separately tunable). `0` leaves anonymous traffic unlimited. |
245
248
246
249
`MCP_MAX_REQUEST_BODY` matches nginx's `client_max_body_size 1m`. Raise it if `update-page` calls return 413 on legitimately large edits or your wiki has raised `$wgMaxArticleSize` (MediaWiki default 2 MB). Lower it for a tighter DoS guard.
A request carrying an `Origin` the server cannot parse at all is rejected with a 403. Requests with no `Origin` header pass, because non-browser MCP clients do not send one.
310
313
314
+
### Rate limiting
315
+
316
+
`tools/call` is rate limited per caller: each authenticated caller gets its own allowance (`MCP_RATE_LIMIT`, burst `MCP_RATE_LIMIT_BURST`), and all anonymous callers share one (`MCP_RATE_LIMIT_ANONYMOUS`). A request over the limit is refused with `429` and a `Retry-After` header, and never reaches the wiki. Only `tools/call` is limited; every other request, including subscription streams, passes untouched.
317
+
318
+
The limiter is per-process — replicas each enforce their own allowance — and `mcp_rate_limited_total` on [`/metrics`](operations.md#metrics) counts refusals for tuning.
319
+
311
320
### v1 limitations
312
321
313
322
These apply to the [hosted OAuth sign-in](#hosted-oauth-sign-in) setup:
@@ -335,9 +344,9 @@ The server accepts a standard OAuth 2.1 `Authorization: Bearer` header on each r
335
344
Authorization: Bearer <oauth2-access-token>
336
345
```
337
346
338
-
Use a MediaWiki OAuth2 access token obtained from `Special:OAuthConsumerRegistration/propose/oauth2` on the target wiki, with [Extension:OAuth](https://www.mediawiki.org/wiki/Extension:OAuth) installed. The server forwards it to MediaWiki as that caller's token, so writes are attributable and MediaWiki's per-user rate limits apply. A bearer is scoped to a single MediaWiki OAuth2 realm, and the server pins nothing across requests: one client can address wikis on different authorization servers by sending the right token per request. `list-wikis` reports each OAuth wiki's `authorizationServer`.
347
+
Use a MediaWiki OAuth2 access token obtained from `Special:OAuthConsumerRegistration/propose/oauth2` on the target wiki, with [Extension:OAuth](https://www.mediawiki.org/wiki/Extension:OAuth) installed. The server forwards it to MediaWiki as that caller's token, so writes are attributable and MediaWiki's per-user rate limits apply. A bearer is scoped to a single MediaWiki OAuth2 realm, and the server pins nothing across requests: one client can address wikis on different authorization servers by sending the right token per request. While forwarding is enabled, `list-wikis` reports each OAuth wiki's `authorizationServer` so a caller can see which realm a wiki belongs to.
339
348
340
-
The server no longer advertises the wikis' own authorization servers, so a client cannot discover where to mint such a token: obtain it yourself and configure it on the caller. Only [Hosted OAuth sign-in](#hosted-oauth-sign-in) publishes a protected-resource document, naming this server. While `MCP_ALLOW_BEARER_PASSTHROUGH=true` is set, a bearer-less request is challenged with `401` when no configured wiki is usable without a token; a deployment mixing OAuth and non-OAuth wikis still serves tokenless clients on the wikis that allow anonymous access.
349
+
No OAuth discovery points at the wikis' authorization servers: the only protected-resource document is the one [Hosted OAuth sign-in](#hosted-oauth-sign-in) publishes, and it names this server. A client cannot discover where to mint a wiki token — obtain it yourself and configure it on the caller. While `MCP_ALLOW_BEARER_PASSTHROUGH=true` is set, a bearer-less request is challenged with `401` when no configured wiki is usable without a token; a deployment mixing OAuth and non-OAuth wikis still serves tokenless clients on the wikis that allow anonymous access.
341
350
342
351
**Precedence:** request header (only while `MCP_ALLOW_BEARER_PASSTHROUGH=true`; otherwise refused with `401`) → `config.json``token` → `config.json``username`/`password` → anonymous. The HTTP transport refuses to start with static credentials in `config.json` unless `MCP_ALLOW_STATIC_FALLBACK=true` is set; see [the Security checklist](#security-checklist) for why.
Copy file name to clipboardExpand all lines: docs/operations.md
+1Lines changed: 1 addition & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -90,6 +90,7 @@ Exposed series:
90
90
-`mcp_inflight_requests` — gauge of `/mcp` requests currently being served. Subscription streams are excluded: they are held open by design.
91
91
-`mcp_subscription_streams` — gauge of open change-notification streams (`subscriptions/listen`), the closest measure of connected clients.
92
92
-`mcp_ready_failures_total` — counter of `/ready` probes that returned non-200.
93
+
-`mcp_rate_limited_total{caller}` — counter of `tools/call` requests refused with `429`; the `caller` label is `caller` for authenticated callers and `anonymous` for the shared bucket. A rising `caller` series means authenticated callers hit `MCP_RATE_LIMIT`; a rising `anonymous` series means the shared backstop is engaging.
93
94
-`mcp_proxy_store_upstream_tokens` — gauge of upstream MediaWiki tokens held in the hosted OAuth proxy store. This set grows with cumulative sign-ins over the process lifetime; watch it to size memory and the flush cost below.
94
95
-`mcp_proxy_store_clients` — gauge of registered clients held in the hosted OAuth proxy store. FIFO-capped at 10,000, so this plateaus rather than growing without bound.
95
96
-`mcp_proxy_store_flush_duration_seconds` — histogram of hosted-proxy store durable-flush durations (serialize + encrypt + write). Every upstream-token write flushes the whole store synchronously, so this scales with the token count above. Records successful flushes only.
0 commit comments