@winstonsayno/mcp-gateway 0.4.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +225 -19
  3. package/dashboard/index.html +79 -22
  4. package/dist/auth/middleware.d.ts +21 -2
  5. package/dist/auth/middleware.d.ts.map +1 -1
  6. package/dist/auth/middleware.js +63 -13
  7. package/dist/auth/middleware.js.map +1 -1
  8. package/dist/auth/ratelimit.d.ts +16 -1
  9. package/dist/auth/ratelimit.d.ts.map +1 -1
  10. package/dist/auth/ratelimit.js +15 -8
  11. package/dist/auth/ratelimit.js.map +1 -1
  12. package/dist/auth/scopes.d.ts +40 -0
  13. package/dist/auth/scopes.d.ts.map +1 -0
  14. package/dist/auth/scopes.js +76 -0
  15. package/dist/auth/scopes.js.map +1 -0
  16. package/dist/config/loader.d.ts.map +1 -1
  17. package/dist/config/loader.js +91 -10
  18. package/dist/config/loader.js.map +1 -1
  19. package/dist/gateway/api.d.ts +12 -0
  20. package/dist/gateway/api.d.ts.map +1 -1
  21. package/dist/gateway/api.js +264 -11
  22. package/dist/gateway/api.js.map +1 -1
  23. package/dist/gateway/index.d.ts +4 -0
  24. package/dist/gateway/index.d.ts.map +1 -1
  25. package/dist/gateway/index.js +54 -2
  26. package/dist/gateway/index.js.map +1 -1
  27. package/dist/gateway/supervisor.d.ts +1 -0
  28. package/dist/gateway/supervisor.d.ts.map +1 -1
  29. package/dist/gateway/supervisor.js +12 -1
  30. package/dist/gateway/supervisor.js.map +1 -1
  31. package/dist/index.d.ts +11 -1
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +6 -0
  34. package/dist/index.js.map +1 -1
  35. package/dist/mcp/catalog.d.ts +30 -0
  36. package/dist/mcp/catalog.d.ts.map +1 -0
  37. package/dist/mcp/catalog.js +78 -0
  38. package/dist/mcp/catalog.js.map +1 -0
  39. package/dist/mcp/endpoint.d.ts +132 -0
  40. package/dist/mcp/endpoint.d.ts.map +1 -0
  41. package/dist/mcp/endpoint.js +714 -0
  42. package/dist/mcp/endpoint.js.map +1 -0
  43. package/dist/mcp/llm-schemas.d.ts +36 -0
  44. package/dist/mcp/llm-schemas.d.ts.map +1 -0
  45. package/dist/mcp/llm-schemas.js +69 -0
  46. package/dist/mcp/llm-schemas.js.map +1 -0
  47. package/dist/mcp/naming.d.ts +53 -0
  48. package/dist/mcp/naming.d.ts.map +1 -0
  49. package/dist/mcp/naming.js +71 -0
  50. package/dist/mcp/naming.js.map +1 -0
  51. package/dist/middleware/cors.d.ts +2 -0
  52. package/dist/middleware/cors.d.ts.map +1 -1
  53. package/dist/middleware/cors.js +25 -19
  54. package/dist/middleware/cors.js.map +1 -1
  55. package/dist/monitor/audit.d.ts +60 -0
  56. package/dist/monitor/audit.d.ts.map +1 -0
  57. package/dist/monitor/audit.js +155 -0
  58. package/dist/monitor/audit.js.map +1 -0
  59. package/dist/monitor/index.d.ts +19 -0
  60. package/dist/monitor/index.d.ts.map +1 -1
  61. package/dist/monitor/index.js +88 -0
  62. package/dist/monitor/index.js.map +1 -1
  63. package/dist/proxy/index.d.ts +26 -2
  64. package/dist/proxy/index.d.ts.map +1 -1
  65. package/dist/proxy/index.js +175 -18
  66. package/dist/proxy/index.js.map +1 -1
  67. package/dist/registry/index.d.ts +8 -1
  68. package/dist/registry/index.d.ts.map +1 -1
  69. package/dist/registry/index.js +22 -0
  70. package/dist/registry/index.js.map +1 -1
  71. package/dist/utils/types.d.ts +108 -2
  72. package/dist/utils/types.d.ts.map +1 -1
  73. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -9,6 +9,52 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
9
9
 
10
10
  ## [Unreleased]
11
11
 
12
+ ## [1.0.1] - 2026-10-07
13
+
14
+ Bug-fix release; no API or configuration changes.
15
+
16
+ ### Fixed
17
+ - A server whose MCP handshake (`initialize` → `notifications/initialized` → `tools/list`) was still in progress was already reported as connected: `mcp_gateway_server_up` / `up` in `/api/v1/metrics` showed `1`, `/health/ready` counted it, and REST / `/mcp` calls could be forwarded to it before `initialize` had been answered. `McpProxy#isConnected` and `McpProxy#request` now require a completed handshake (calls during it get the usual "not connected" / `503`).
18
+ - Flaky tests: `GET /api/v1/tools?format=mcp` assertion depended on server connect order; the reconnect-metrics test could observe a handshaking server as up (fixed by the above).
19
+
20
+ ### Changed (maintenance)
21
+ - Dependabot ignores semver-major updates (npm root + JS client, Gradle) and keeps the Docker base image on Node 22; majors are adopted deliberately.
22
+
23
+ ## [1.0.0] - 2026-10-07
24
+
25
+ First stable release. From here on mcp-gateway follows semver: `/api/v1`, `/mcp`, configuration keys, CLI and
26
+ root library exports only change in backward-compatible ways within 1.x (see
27
+ [docs/api-reference.md#stability-and-versioning](docs/api-reference.md#stability-and-versioning)).
28
+ This release contains everything developed as v0.5 – v0.8.
29
+
30
+ ### Added
31
+ - **Documentation** in `docs/`: API reference (REST, `/mcp`, error codes, stability policy), configuration reference and deployment guide (Docker, Kubernetes manifests, reverse proxy, security checklist, systemd).
32
+ - **Container image** workflow `.github/workflows/docker.yml`: on release publish, builds `linux/amd64` + `linux/arm64` and pushes `ghcr.io/<owner>/mcp-gateway` tagged `<version>`, `<major>.<minor>`, `<major>` and `latest` (with provenance + SBOM).
33
+ - Dependabot config (npm root + JS client, Gradle Kotlin client, GitHub Actions, Docker), pull-request template, issue-template links, `SECURITY.md`.
34
+ - **Resources & prompts passthrough**: resources, resource templates and prompts of servers announcing those capabilities are listed at connect time and refreshed on `notifications/resources|prompts/list_changed`. REST: `GET /api/v1/resources`, `GET /api/v1/resources/templates`, `POST /api/v1/resources/read`, `GET /api/v1/prompts`, `POST /api/v1/prompts/get` (auto-routing, `409` on ambiguous prompt names, `403` out of scope, `502` / `503` / `504` like tool calls). `/mcp`: `resources/list`, `resources/templates/list`, `resources/read`, `prompts/list`, `prompts/get` (paginated; prompt names follow `toolNaming`; duplicate resource URIs collapsed, lowest server id wins; reads routed by URI, then template, then the only resource server), `resources` / `prompts` capabilities with `list_changed` notifications. Scopes apply by server. Rate limited and recorded with `kind: "resource" | "prompt"`.
35
+ - **Persistent audit log** (`audit: { enabled, path, retentionDays }`, default off): every request record is also written to SQLite through the built-in `node:sqlite` (Node 22.5+, no new dependency; clear startup error on older Node). Metadata only, never arguments or results. Hourly retention pruning.
36
+ - `GET /api/v1/requests` filters (`server`, `tool`, `client`, `success`, `via`, `kind`, `since`, `until`) and cursor paging (`nextCursor`), from the audit log when enabled, else the in-memory log; responses carry `source`. The dashboard's *Request History* panel has filters and *Load older*.
37
+ - JS client: `history()` (filters + cursor), `listResources`, `listResourceTemplates`, `readResource`, `listPrompts`, `getPrompt`.
38
+ - `SessionInfo.capabilities`; `McpProxy#getCatalog`, `#hasCapability`; registry `setCatalog` / `getAllResources` / `getAllResourceTemplates` / `getAllPrompts`; `MetricsCollector#setAuditStore` / `#queryRequests`; exports `SqliteAuditStore`, `sqliteAvailable`, `AuditStore`, catalog helpers and resource / prompt types.
39
+ - **LLM tool schemas**: `GET /api/v1/tools?format=openai|openai-responses|anthropic` returns function-calling definitions (`tools`) plus a `mapping` from LLM tool name to `{ server, tool }`. Names follow `mcp.toolNaming`, are sanitised to `^[a-zA-Z0-9_-]{1,64}$` and de-duplicated; scopes and `?server=` / `?tag=` apply. Exports `toLlmToolSchemas`, `sanitizeToolName`, `LLM_SCHEMA_FORMATS`.
40
+ - **TypeScript client** `@winstonsayno/mcp-gateway-client` in `clients/js` (not published): zero dependencies, `fetch`-based (browser + Node 18+), typed `health`, `ready`, `metrics`, `servers`, `server`, `reconnect`, `listTools`, `toolSchemas`, `callTool`, `callLlmTool`, `requests`, `GatewayError`; `connectMcp()` / `McpSession` helper for `/mcp` (pagination, SSE replies, cancellation). Own tests + an integration test against a real gateway.
41
+ - **Kotlin client** in `clients/kotlin` (not published): OkHttp 4.12 + kotlinx.serialization 1.6, Java 11 bytecode (Android-friendly), same API surface, `McpSession`; Gradle 8.7 wrapper and MockWebServer tests.
42
+ - CI jobs for both clients.
43
+ - **Per-key scopes**: `auth.apiKeys` entries may be objects `{ key, name?, servers?, tools?, rateLimit? }` (plain strings still mean full access). `servers` / `tools` are glob allow-lists (`tools` patterns containing `/` match `<serverId>/<tool>`); `rateLimit` gives the key its own bucket; `name` makes the client id `key:<name>`; `${VAR}` is expanded in object keys. JWTs carry scopes in the `mcp_servers` / `mcp_tools` claims. Enforced on REST (`/tools`, `/servers`, `/servers/:id` hide; `/tools/call`, `/servers/:id/reconnect` → `403`; auto-routing only among allowed servers; restricted keys see only their own `/requests`) and on `/mcp` (`tools/list` hides, `tools/call` → `-32003`, key rate limits). Hot reloadable: open `/mcp` sessions are notified, sessions of removed keys closed. Exports: `isServerInScope`, `isToolInScope`, `filterToolsByScope`, `scopeFromJwt`, types `AccessScope`, `ApiKeyConfig`.
44
+ - **Downstream MCP endpoint** `POST/GET/DELETE /mcp`: the gateway is now an MCP server over Streamable HTTP (protocol `2025-06-18`, `2025-03-26` accepted). Sessions via `Mcp-Session-Id` (bound to the authenticated key / JWT subject, idle expiry, LRU eviction at `maxSessions`), `initialize`, `ping`, aggregated and paginated `tools/list`, `tools/call` routed upstream, `notifications/tools/list_changed` on the `GET` SSE stream whenever the aggregated list changes, and `notifications/cancelled` propagated to the upstream server. JSON-RPC batches are accepted. Reuses auth, the rate limiter (per `tools/call`), `maxConcurrency`, timeouts, metrics and the request log. Origin validation (`mcp.allowedOrigins`, default `corsOrigins`) against DNS rebinding.
45
+ - `mcp` config block: `enabled`, `path`, `toolNaming` (`auto` — prefix `<serverId>__` only on name collisions — or `prefix`), `pageSize`, `sessionIdleTimeoutSeconds`, `maxSessions`, `allowedOrigins`, `instructions`. Everything except `enabled` / `path` hot reloads.
46
+ - `McpProxy.request()` for arbitrary upstream methods and an optional `AbortSignal` on `callTool()` (`ERR_CANCELLED`).
47
+ - Tool `title`, `outputSchema` and `annotations` are kept from upstream `tools/list` and exposed on `/api/v1/tools` and `/mcp`.
48
+ - Request records carry `via: "rest" | "mcp"`.
49
+ - Library exports: `McpEndpoint`, `buildToolIndex`, `prefixedName`, `DOWNSTREAM_PROTOCOL_VERSIONS`, `ERR_RATE_LIMITED`, types `McpEndpointConfig`, `ToolNaming`, `McpSessionSummary`; `Gateway#getMcpEndpoint()`.
50
+ - Conformance tests with the official `@modelcontextprotocol/sdk` client (list, call, ping, `list_changed`, cancellation, session termination, auth).
51
+ - CORS allows the `Mcp-Session-Id`, `MCP-Protocol-Version` and `Last-Event-ID` request headers and exposes `Mcp-Session-Id`.
52
+
53
+ ### Changed
54
+ - Docker image is based on `node:22-alpine` (was 20) so the optional audit log works; it has a writable `/app/data` volume and OCI labels. The npm package still supports Node 20+.
55
+ - README: npm badge points at `@winstonsayno/mcp-gateway`, CI badge, ghcr image name lowercased (`ghcr.io/harrisoncn/mcp-gateway`), library import uses the scoped package name, API-stability section.
56
+ - `AuthConfig.apiKeys` is typed `Array<string | ApiKeyConfig>` (was `string[]`); existing configs are unchanged. `createAuthMiddleware()` returns an `AuthMiddleware` (a `RequestHandler` with an optional `resolveClient`).
57
+
12
58
  ## [0.4.0] - 2026-10-07
13
59
 
14
60
  ### Added
package/README.md CHANGED
@@ -11,10 +11,11 @@ Route · Authenticate · Rate-limit · Monitor — all your [Model Context Proto
11
11
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
12
12
  [![Node.js](https://img.shields.io/badge/node-%3E%3D20.0.0-brightgreen.svg)](https://nodejs.org)
13
13
  [![TypeScript](https://img.shields.io/badge/TypeScript-5.5-blue.svg)](https://www.typescriptlang.org)
14
- [![npm version](https://img.shields.io/badge/npm-v0.2.0-blue.svg)](https://www.npmjs.com/package/mcp-gateway)
15
- [![Docker](https://img.shields.io/badge/docker-ghcr.io-blue.svg)](https://ghcr.io/HarrisonCN/mcp-gateway)
14
+ [![npm version](https://img.shields.io/npm/v/@winstonsayno/mcp-gateway.svg)](https://www.npmjs.com/package/@winstonsayno/mcp-gateway)
15
+ [![CI](https://github.com/HarrisonCN/mcp-gateway/actions/workflows/ci.yml/badge.svg)](https://github.com/HarrisonCN/mcp-gateway/actions/workflows/ci.yml)
16
+ [![Docker](https://img.shields.io/badge/docker-ghcr.io-blue.svg)](https://github.com/HarrisonCN/mcp-gateway/pkgs/container/mcp-gateway)
16
17
 
17
- [English](#) · [中文](docs/README.zh-CN.md) · [Docs](docs/) · [Examples](examples/)
18
+ [English](#) · [中文](docs/README.zh-CN.md) · [API reference](docs/api-reference.md) · [Configuration](docs/configuration.md) · [Deployment](docs/deployment.md) · [Examples](examples/)
18
19
 
19
20
  </div>
20
21
 
@@ -62,20 +63,26 @@ As [MCP](https://modelcontextprotocol.io) becomes the standard protocol for AI a
62
63
  ## Features
63
64
 
64
65
  - **Unified API endpoint** — one URL for all your MCP tools, auto-routed by tool name
66
+ - **MCP endpoint for clients** — `/mcp` speaks MCP Streamable HTTP (2025-06-18 / 2025-03-26), so Claude Code, Cursor or any MCP client sees every upstream tool through one server, with the same auth, limits and metrics
65
67
  - **Every MCP transport** — `stdio`, `streamable-http` (current spec), legacy `sse` (HTTP+SSE) and `websocket` upstream servers, with per-server headers for upstream auth
66
68
  - **Automatic reconnect** — crashed or disconnected servers are reconnected with exponential backoff + jitter; state is visible in `/servers`, `/health`, the dashboard and Prometheus
67
69
  - **Authentication** — API key (constant-time compare), JWT (HS256/384/512), or no-auth; misconfiguration fails closed
68
70
  - **Rate limiting** — per-key sliding-window counter, with standard `X-RateLimit-*` headers
71
+ - **Per-key scopes** — restrict an API key (or a JWT via claims) to some servers / tools and give it its own rate limit; enforced on REST and `/mcp`
69
72
  - **Concurrency limits** — per-server `maxConcurrency`, queued requests count against `timeout`
70
73
  - **Health monitoring** — periodic MCP `ping` health checks with latency (every 30 s, configurable)
71
74
  - **Metrics** — Prometheus-compatible `/metrics` endpoint (monotonic counters) + JSON aggregation
72
75
  - **Config hot reload** — servers, API keys / auth, rate limits, CORS and reconnect policy apply without a restart (disable with `--no-watch`)
73
76
  - **Optional auth for health & metrics** — keep `/health` and `/metrics` public (default) or put them behind auth; the dashboard asks for a key
74
77
  - **Tool discovery** — `GET /api/v1/tools` lists all tools across all servers
78
+ - **Resources & prompts** — `resources/*` and `prompts/*` from every server, aggregated on REST and `/mcp`
79
+ - **Persistent audit log** — optional SQLite history (built-in `node:sqlite`, no dependency) queryable via `GET /api/v1/requests` and the dashboard
75
80
  - **Tool filtering** — per-server `tools.allow` / `tools.deny` glob patterns hide tools you don't want exposed (and block calls to them)
76
81
  - **YAML/JSON config** — simple, declarative configuration with env var overrides
77
82
  - **Docker-ready** — official Docker image, Compose examples included
78
83
  - **TypeScript SDK** — embed the gateway as a library in your own project
84
+ - **Client libraries** — a dependency-free TypeScript client ([`clients/js`](clients/js), browser + Node) and a Kotlin/JVM/Android client ([`clients/kotlin`](clients/kotlin))
85
+ - **LLM tool schemas** — `GET /api/v1/tools?format=openai|anthropic` returns ready-to-use function-calling definitions
79
86
 
80
87
  ## Quick Start
81
88
 
@@ -143,6 +150,65 @@ curl -X POST http://localhost:4000/api/v1/tools/call \
143
150
  -d '{"tool": "create_issue", "server": "github", "arguments": {"title": "Bug report", "body": "..."}}'
144
151
  ```
145
152
 
153
+ ## Use the gateway as an MCP server (`/mcp`)
154
+
155
+ The gateway is itself an MCP server: `http://<host>:4000/mcp` implements the
156
+ [Streamable HTTP transport](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports)
157
+ (protocol `2025-06-18`, `2025-03-26` accepted). Clients get one aggregated, filtered tool list; calls are
158
+ routed to the right upstream server with the gateway's auth, rate limit, `maxConcurrency`, timeouts,
159
+ metrics and request log.
160
+
161
+ **Claude Code**
162
+
163
+ ```bash
164
+ claude mcp add --transport http gateway http://localhost:4000/mcp \
165
+ --header "Authorization: Bearer your-secret-key"
166
+ ```
167
+
168
+ **Cursor** (`~/.cursor/mcp.json` or `.cursor/mcp.json`)
169
+
170
+ ```json
171
+ {
172
+ "mcpServers": {
173
+ "gateway": {
174
+ "url": "http://localhost:4000/mcp",
175
+ "headers": { "Authorization": "Bearer your-secret-key" }
176
+ }
177
+ }
178
+ }
179
+ ```
180
+
181
+ **Clients that only speak stdio** (e.g. older Claude Desktop builds) can bridge with
182
+ [`mcp-remote`](https://www.npmjs.com/package/mcp-remote):
183
+ `npx mcp-remote http://localhost:4000/mcp --header "Authorization: Bearer your-secret-key"`.
184
+
185
+ **Any SDK client**
186
+
187
+ ```ts
188
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
189
+ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
190
+
191
+ const client = new Client({ name: 'my-app', version: '1.0.0' });
192
+ await client.connect(new StreamableHTTPClientTransport(new URL('http://localhost:4000/mcp'), {
193
+ requestInit: { headers: { Authorization: 'Bearer your-secret-key' } },
194
+ }));
195
+ const { tools } = await client.listTools();
196
+ ```
197
+
198
+ What the endpoint does:
199
+
200
+ | | |
201
+ |---|---|
202
+ | `POST /mcp` | JSON-RPC: `initialize`, `ping`, `tools/list` (paginated), `tools/call`, `resources/list`, `resources/templates/list`, `resources/read`, `prompts/list`, `prompts/get`, notifications (incl. `notifications/cancelled`). Batches are accepted. Responses are `application/json`. |
203
+ | `GET /mcp` | SSE stream for server→client notifications: `notifications/tools/list_changed`, `notifications/resources/list_changed` and `notifications/prompts/list_changed` are sent when the aggregated list a session sees changes (a server announces changes, connects, is removed by hot reload, …). |
204
+ | Resources & prompts | Resource URIs are passed through unchanged; when two servers list the same URI the lowest server id wins. `resources/read` is routed by exact URI, then by resource template, then to the only server with resources. Prompt names follow `toolNaming` like tools. `resources/subscribe` is not offered. |
205
+ | `DELETE /mcp` | Ends the session. |
206
+ | Sessions | `initialize` returns `Mcp-Session-Id`; later requests must send it (`400` if missing, `404` if unknown or expired). A session is bound to the API key / JWT subject that created it. Idle sessions expire after `mcp.sessionIdleTimeoutSeconds`. |
207
+ | Tool names | `toolNaming: auto` (default) keeps a tool's name unless two servers expose the same name; then every copy becomes `<serverId>__<tool>`. `prefix` always uses `<serverId>__<tool>`. Ordering is deterministic (server id, then tool name). In `auto` mode the prefixed form is also accepted by `tools/call`. |
208
+ | Errors | Unknown tool / bad params → JSON-RPC `-32602`; rate limit → `-32029` with `data.retryAfter`; server offline or timed out → a normal result with `isError: true` (so the model sees it); upstream JSON-RPC errors are forwarded unchanged; cancelled → `-32800`. |
209
+ | Cancellation | `notifications/cancelled` (or the client dropping the HTTP request) cancels the upstream call, which receives its own `notifications/cancelled`. |
210
+ | Security | Same `auth` as the REST API (`Authorization: Bearer …` or `X-API-Key`). Requests with an `Origin` header are rejected (`403`) unless it matches `mcp.allowedOrigins` (default: `corsOrigins`) — set this when the gateway listens on a reachable address. |
211
+
146
212
  ## API Reference
147
213
 
148
214
  | Method | Path | Description |
@@ -150,13 +216,19 @@ curl -X POST http://localhost:4000/api/v1/tools/call \
150
216
  | `GET` | `/api/v1/health` | Gateway health and server summary |
151
217
  | `GET` | `/api/v1/servers` | List all registered servers |
152
218
  | `GET` | `/api/v1/servers/:id` | Get server details and tools |
153
- | `GET` | `/api/v1/tools` | List all tools (filterable by `?server=` or `?tag=`) |
219
+ | `GET` | `/api/v1/tools` | List all tools (filterable by `?server=` or `?tag=`; `?format=openai\|openai-responses\|anthropic` for LLM schemas) |
154
220
  | `POST` | `/api/v1/tools/call` | Invoke a tool |
155
221
  | `POST` | `/api/v1/servers/:id/reconnect` | Reconnect a server now (resets backoff) |
156
222
  | `GET` | `/api/v1/health/live` | Liveness probe — always public, returns only `{"status":"ok"}` |
157
223
  | `GET` | `/api/v1/health/ready` | Readiness probe — always public; `200` when servers are ready, else `503` (`?min=N`) |
158
224
  | `GET` | `/api/v1/metrics` | Aggregated metrics (JSON or Prometheus) |
159
- | `GET` | `/api/v1/requests` | Recent request log (`?limit=`, max 500) |
225
+ | `GET` | `/api/v1/resources` | Resources of all servers (`?server=`), duplicate URIs collapsed |
226
+ | `GET` | `/api/v1/resources/templates` | Resource templates (`?server=`) |
227
+ | `POST` | `/api/v1/resources/read` | Read a resource: `{"uri": "...", "server"?: "..."}` |
228
+ | `GET` | `/api/v1/prompts` | Prompts of all servers (`?server=`) |
229
+ | `POST` | `/api/v1/prompts/get` | Get a prompt: `{"name": "...", "server"?: "...", "arguments"?: {...}}` |
230
+ | `GET` | `/api/v1/requests` | Request history, newest first (`?limit=` max 500, `server`, `tool`, `client`, `success`, `via`, `kind`, `since`, `until`, `cursor`) |
231
+ | `POST` `GET` `DELETE` | `/mcp` | MCP Streamable HTTP endpoint (see [above](#use-the-gateway-as-an-mcp-server-mcp)) |
160
232
 
161
233
  `/health` and `/metrics` are unauthenticated by default; set `auth.protect.health` / `auth.protect.metrics`
162
234
  to require auth for them too (`/health/live` and `/health/ready` always stay public for Docker / Kubernetes probes).
@@ -164,13 +236,42 @@ Every other route requires auth when it is enabled.
164
236
  `/metrics` returns JSON by default (`?window=<ms>`); with `monitor.prometheus: true` it returns the
165
237
  Prometheus text format when the client asks for `text/plain` (as Prometheus does) or passes `?format=prometheus`.
166
238
 
239
+ ### LLM tool schemas
240
+
241
+ `GET /api/v1/tools?format=openai` (Chat Completions), `openai-responses` (Responses API) or `anthropic` (Messages API)
242
+ returns the tools the caller may use as function-calling definitions, plus a `mapping` from each LLM tool
243
+ name back to the gateway server and tool:
244
+
245
+ ```json
246
+ {
247
+ "format": "anthropic",
248
+ "tools": [{ "name": "github__create_issue", "description": "…", "input_schema": { "type": "object", "properties": { … } } }],
249
+ "mapping": { "github__create_issue": { "server": "github", "tool": "create_issue" } },
250
+ "total": 1
251
+ }
252
+ ```
253
+
254
+ Pass `tools` straight to the provider; when the model calls a tool, look it up in `mapping` and
255
+ `POST /api/v1/tools/call` with that `server` / `tool` (the clients' `callLlmTool()` does this).
256
+ Names follow `mcp.toolNaming`, are sanitised to `^[a-zA-Z0-9_-]{1,64}$` and de-duplicated; `$schema` is stripped and
257
+ `parameters` is always an object schema. Scopes and `?server=` / `?tag=` filters apply.
258
+
259
+ ### Client libraries
260
+
261
+ | | |
262
+ |---|---|
263
+ | **TypeScript / JavaScript** — [`clients/js`](clients/js) | `@winstonsayno/mcp-gateway-client`: zero dependencies, `fetch`-based (browser, Node 18+, Deno, Bun, React Native), typed `health`, `servers`, `listTools`, `toolSchemas`, `callTool`, `callLlmTool`, plus a small MCP-over-`/mcp` session helper |
264
+ | **Kotlin / JVM / Android** — [`clients/kotlin`](clients/kotlin) | OkHttp + kotlinx.serialization, Java 11 bytecode; same API surface, `McpSession` for `/mcp` |
265
+
266
+ Both are in this repository and not yet published to npm / Maven Central.
267
+
167
268
  `POST /api/v1/tools/call` responses:
168
269
 
169
270
  | Status | Meaning |
170
271
  |--------|---------|
171
272
  | `200` | Tool returned a result |
172
273
  | `400` | Invalid body (`tool` must be a string, `arguments` an object) or malformed JSON |
173
- | `403` | The tool is hidden by the server's `tools` filter (also when `"server"` is passed explicitly) |
274
+ | `403` | The tool is hidden by the server's `tools` filter (also when `"server"` is passed explicitly), or outside the caller's scope |
174
275
  | `404` | Unknown tool or server |
175
276
  | `409` | Tool name is exposed by several servers — pass `"server"` to choose |
176
277
  | `429` | Rate limited (see `Retry-After`) |
@@ -188,7 +289,12 @@ logLevel: info # debug | info | warn | error
188
289
  auth:
189
290
  strategy: api-key # none | api-key | jwt (oauth2 is not implemented and is rejected)
190
291
  apiKeys:
191
- - "your-secret-key"
292
+ - "your-secret-key" # full access
293
+ - key: "${APP_KEY}" # scoped key (see "Per-key scopes")
294
+ name: app
295
+ servers: ["github"]
296
+ tools: ["read_*"]
297
+ rateLimit: { limit: 30, windowSeconds: 60 }
192
298
  protect:
193
299
  health: false # true → /api/v1/health requires auth
194
300
  metrics: false # true → /api/v1/metrics requires auth (configure your scraper)
@@ -218,6 +324,21 @@ monitor:
218
324
  corsOrigins:
219
325
  - "https://your-app.com"
220
326
 
327
+ audit: # persistent request history (SQLite, Node 22.5+; default off)
328
+ enabled: false
329
+ path: mcp-gateway-audit.db
330
+ retentionDays: 30 # 0 = keep forever
331
+
332
+ mcp: # downstream MCP endpoint (Streamable HTTP)
333
+ enabled: true # restart required to change
334
+ path: /mcp # restart required to change; not "/" or under /api, /dashboard
335
+ toolNaming: auto # auto | prefix ("<serverId>__<tool>")
336
+ pageSize: 500 # tools per tools/list page
337
+ sessionIdleTimeoutSeconds: 1800
338
+ maxSessions: 1000 # least recently used idle session is evicted beyond this
339
+ # allowedOrigins: ["https://your-app.com"] # browser origins allowed on /mcp (default: corsOrigins)
340
+ # instructions: "Tools for the ACME workspace" # returned from initialize
341
+
221
342
  servers:
222
343
  - id: my-server # Unique identifier
223
344
  name: My Server # Display name
@@ -265,10 +386,80 @@ With `mcp-gateway start` the config file is watched (disable with `--no-watch`).
265
386
  | `auth` (strategy, keys, JWT secret, `protect`) | `monitor.retentionHours` |
266
387
  | `rateLimit` (counters reset when it changes) | `healthCheckIntervalMs` |
267
388
  | `corsOrigins`, `monitor.requestLog`, `monitor.prometheus` | `dashboard` |
268
- | `reconnect`, `logLevel` | |
389
+ | `reconnect`, `logLevel` | `mcp.enabled`, `mcp.path`, `audit` |
390
+ | `mcp.toolNaming`, `mcp.pageSize`, session limits, `mcp.allowedOrigins` | |
269
391
 
270
392
  An invalid file is rejected and the running config is kept. `MCP_GATEWAY_*` env overrides keep precedence.
271
393
 
394
+ ### Per-key scopes
395
+
396
+ Give each app its own key and only the tools it needs. Plain string keys keep full access;
397
+ object entries can be restricted (all fields except `key` are optional):
398
+
399
+ ```yaml
400
+ auth:
401
+ strategy: api-key
402
+ apiKeys:
403
+ - "admin-key" # unrestricted
404
+ - key: ${AURA_GATEWAY_KEY} # ${VAR} is expanded in object keys
405
+ name: aura # client id "key:aura" in logs, metrics and sessions (unique)
406
+ servers: ["github", "fs-*"] # server id globs
407
+ tools: ["read_*", "github/create_issue"] # tool globs; "server/tool" when the pattern has a "/"
408
+ rateLimit: { limit: 30, windowSeconds: 60 } # own bucket instead of the global rateLimit
409
+ ```
410
+
411
+ - A tool must pass the server's own `tools` filter **and** the key's `servers` **and** `tools` lists.
412
+ An absent list means no restriction; an empty list allows nothing.
413
+ - **Discovery hides** what a key may not use: `GET /tools`, `GET /servers`, `GET /servers/:id` (→ `404`), `/mcp` `tools/list`.
414
+ - **Calls are refused**: `POST /tools/call` and `POST /servers/:id/reconnect` → `403`; `/mcp` `tools/call` → JSON-RPC error `-32003`.
415
+ Auto-routing only considers servers the key may use, so a name that collides elsewhere can still be called without `"server"`.
416
+ - On `/mcp`, collision prefixes are computed from what the key can see (a key scoped to one server sees bare names).
417
+ - Restricted keys only see their own entries in `GET /requests`.
418
+ - **JWT**: put globs in the `mcp_servers` / `mcp_tools` claims (array, or a space/comma-separated string), e.g.
419
+ `{"sub": "user-1", "mcp_servers": ["github"], "mcp_tools": "read_* github/create_issue"}`. A malformed claim allows nothing.
420
+ - Hot reloadable: changing scopes applies to the next request; open `/mcp` sessions get `notifications/tools/list_changed`,
421
+ and sessions of removed keys are closed.
422
+
423
+ ### Resources & prompts
424
+
425
+ Servers that announce the `resources` / `prompts` capabilities have their resources, resource templates and
426
+ prompts listed at connect time (and refreshed on `notifications/*/list_changed`). They are available on REST
427
+ (`/api/v1/resources`, `/resources/templates`, `/resources/read`, `/prompts`, `/prompts/get`) and on `/mcp`.
428
+ Reads and gets are forwarded live with the server's `timeout`, counted against the rate limit and recorded in
429
+ metrics / history with `kind: "resource"` or `"prompt"`. Key scopes apply by **server** (`servers` globs);
430
+ `tools` globs and `servers[].tools` filters only concern tools.
431
+
432
+ ```bash
433
+ curl -s localhost:4000/api/v1/resources
434
+ curl -s -X POST localhost:4000/api/v1/resources/read -H 'content-type: application/json' -d '{"uri":"file:///notes/todo.md"}'
435
+ curl -s -X POST localhost:4000/api/v1/prompts/get -H 'content-type: application/json' \
436
+ -d '{"name":"review-code","server":"github","arguments":{"pr":"42"}}'
437
+ ```
438
+
439
+ ### Persistent audit log
440
+
441
+ By default request history lives in memory (`monitor.retentionHours`). Enable the audit log to keep it in SQLite
442
+ across restarts:
443
+
444
+ ```yaml
445
+ audit:
446
+ enabled: true
447
+ path: ./data/mcp-gateway-audit.db # default mcp-gateway-audit.db (WAL mode)
448
+ retentionDays: 30 # pruned hourly; 0 = keep forever
449
+ ```
450
+
451
+ - Uses Node's built-in [`node:sqlite`](https://nodejs.org/api/sqlite.html) (**Node 22.5+**): no extra dependency and
452
+ nothing native to compile. On Node 20 the gateway refuses to start with `audit.enabled: true` and says why. Node may
453
+ print an `ExperimentalWarning` for `node:sqlite`.
454
+ - Only metadata is stored: time, server, tool / URI / prompt, kind, duration, success, error message, client id, `via`
455
+ (`rest` / `mcp`). Arguments and results are never stored.
456
+ - `GET /api/v1/requests` then reads from the database (`"source": "audit"`) and supports filters
457
+ (`server`, `tool`, `client`, `success=true|false`, `via=rest|mcp`, `kind=tool|resource|prompt`, `since` / `until` as ISO
458
+ or epoch ms) and paging (`nextCursor` → `?cursor=`). Restricted keys only ever see their own records.
459
+ The dashboard's *Request History* panel has the same filters and a *Load older* button.
460
+ - Library users can plug in any store: `metrics.setAuditStore(myStore)` with the `AuditStore` interface.
461
+ - Changing `audit` requires a restart.
462
+
272
463
  ### Tool filtering
273
464
 
274
465
  Expose only part of a server's tools — e.g. make a filesystem server read-only, or drop tools that
@@ -310,7 +501,7 @@ Prometheus series added: `mcp_gateway_server_up`, `mcp_gateway_server_status{sta
310
501
  docker run -p 4000:4000 \
311
502
  -v $(pwd)/mcp-gateway.yml:/app/mcp-gateway.yml \
312
503
  -e GITHUB_TOKEN=ghp_... \
313
- ghcr.io/harrisonCN/mcp-gateway:latest
504
+ ghcr.io/harrisoncn/mcp-gateway:latest
314
505
 
315
506
  # Or with Docker Compose (gateway + Prometheus; see examples/docker)
316
507
  cd examples/docker
@@ -355,7 +546,7 @@ to stop serving it.
355
546
  ## Embed as a Library
356
547
 
357
548
  ```typescript
358
- import { Gateway, loadConfig } from 'mcp-gateway';
549
+ import { Gateway, loadConfig } from '@winstonsayno/mcp-gateway';
359
550
 
360
551
  const config = await loadConfig('./mcp-gateway.yml');
361
552
  const gateway = new Gateway(config);
@@ -367,16 +558,18 @@ await gateway.start();
367
558
  process.on('SIGTERM', () => gateway.stop());
368
559
  ```
369
560
 
370
- ## What's New (unreleased)
561
+ ## What's New in v1.0
371
562
 
372
563
  | Feature | Description |
373
564
  |---------|-------------|
374
- | **Remote transports** | `streamable-http`, `sse` and `websocket` servers are now routable (checked against the official MCP SDK servers in the test suite) |
375
- | **Auto reconnect** | Exponential backoff with jitter, `reconnecting` status, manual `POST /servers/:id/reconnect`, Prometheus series |
376
- | **Health pings** | Real MCP `ping` health checks with latency; `degraded` when a connected server stops answering |
377
- | **Tool list updates** | `notifications/tools/list_changed` refreshes the tool registry |
378
- | **Protected health/metrics** | `auth.protect.health` / `auth.protect.metrics`, public `/health/live`, dashboard API-key support |
379
- | **More hot reload** | Auth, API keys, rate limits, CORS, reconnect policy |
565
+ | **Stable API** | Semver from 1.0: `/api/v1`, `/mcp`, config keys, CLI and root exports are stable (see [stability](#api-stability)) |
566
+ | **Docs** | [API reference](docs/api-reference.md), [configuration reference](docs/configuration.md), [deployment guide](docs/deployment.md) (Docker, Kubernetes, reverse proxy) |
567
+ | **Container image** | Multi-arch `ghcr.io/harrisoncn/mcp-gateway` built on every release (Node 22) |
568
+ | **Resources & prompts** | `resources/list`, `resources/templates/list`, `resources/read`, `prompts/list`, `prompts/get` aggregated on REST and `/mcp`, with list-changed notifications and scopes |
569
+ | **Audit log** | Optional persistent request history in SQLite (`node:sqlite`), filterable / pageable `GET /api/v1/requests`, dashboard history |
570
+ | **Clients & LLM schemas** | TypeScript client (`clients/js`), Kotlin client (`clients/kotlin`), `GET /api/v1/tools?format=openai\|openai-responses\|anthropic` |
571
+ | **Per-key scopes** | API keys can carry `servers` / `tools` globs and their own `rateLimit`; JWTs carry `mcp_servers` / `mcp_tools` claims. Enforced on REST and `/mcp`, hot reloadable |
572
+ | **`/mcp` endpoint** | The gateway is an MCP server (Streamable HTTP, 2025-06-18): sessions, aggregated + paginated `tools/list`, deterministic collision naming, routed `tools/call`, `list_changed` notifications, cancellation |
380
573
 
381
574
  ## What's New in v0.2.0
382
575
 
@@ -390,6 +583,14 @@ process.on('SIGTERM', () => gateway.stop());
390
583
  | **Web Dashboard** | Live monitoring UI at `/dashboard` |
391
584
  | **4 Bug Fixes** | Concurrency, id collision, handle leaks, timeouts |
392
585
 
586
+ ## API stability
587
+
588
+ mcp-gateway follows [Semantic Versioning](https://semver.org/) since **1.0.0**. Within 1.x the REST API under
589
+ `/api/v1`, the `/mcp` endpoint behaviour, configuration keys, CLI commands / flags, root library exports and Prometheus
590
+ metric names only change in backward-compatible ways (new fields, endpoints and options may be added — ignore
591
+ unknown fields). Deep imports, log format, the dashboard and the audit database schema are not covered. Details:
592
+ [docs/api-reference.md#stability-and-versioning](docs/api-reference.md#stability-and-versioning).
593
+
393
594
  ## Roadmap
394
595
 
395
596
  | Feature | Status |
@@ -401,10 +602,15 @@ process.on('SIGTERM', () => gateway.stop());
401
602
  | Automatic reconnect with backoff | ✅ Done |
402
603
  | Config hot reload | ✅ Done (v0.2.0) |
403
604
  | Web dashboard UI | ✅ Done (v0.2.0) |
605
+ | Downstream MCP endpoint (`/mcp`) | ✅ Done (v1.0) |
606
+ | Per-key scopes and limits | ✅ Done (v1.0) |
607
+ | JS / Kotlin clients, OpenAI / Anthropic tool schemas | ✅ Done (v1.0) |
608
+ | Resources & prompts passthrough, persistent audit log | ✅ Done (v1.0) |
609
+ | Stable API, docs, container image | ✅ Done (v1.0) |
404
610
  | Redis-backed rate limiting | 📋 Planned |
405
611
  | OAuth2 / OIDC auth | 📋 Planned |
406
- | Tool-level access control (RBAC) | 📋 Planned |
407
- | Request replay & debugging | 📋 Planned |
612
+ | Tool-level access control | ✅ Done via per-key scopes (v0.6) |
613
+ | Request replay & debugging | 📋 Planned (history is available via the audit log) |
408
614
  | Multi-tenant mode | 📋 Planned |
409
615
  | OpenTelemetry tracing | 📋 Planned |
410
616
 
@@ -94,6 +94,13 @@
94
94
  .method { font-size: 10px; font-weight: 700; padding: 2px 6px; border-radius: 4px; font-family: monospace; }
95
95
  .method.post { background: rgba(63,185,80,.15); color: var(--green); }
96
96
  .method.get { background: rgba(88,166,255,.15); color: var(--accent); }
97
+ .filters { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin-bottom: 10px; font-size: 12px; color: var(--muted); }
98
+ .filters input, .filters select { background: var(--surface, #161b22); color: inherit; border: 1px solid var(--border, #30363d); border-radius: 6px; padding: 4px 8px; font-size: 12px; }
99
+ .filters .source { margin-left: auto; }
100
+ .filters button, .more { background: var(--surface); border: 1px solid var(--border); color: var(--text); padding: 4px 12px; border-radius: 6px; cursor: pointer; font-size: 12px; }
101
+ .filters button:hover, .more:hover { border-color: var(--accent); color: var(--accent); }
102
+ .more { display: block; margin: 10px auto 0; }
103
+ .more[hidden] { display: none; }
97
104
 
98
105
  /* ── Code ── */
99
106
  code { font-family: "SFMono-Regular", Consolas, monospace; font-size: 12px; color: var(--purple); }
@@ -189,15 +196,25 @@
189
196
  </div>
190
197
  </div>
191
198
 
192
- <!-- Recent Requests -->
199
+ <!-- Request history -->
193
200
  <div class="section">
194
- <div class="section-title">Recent Requests</div>
201
+ <div class="section-title">Request History</div>
202
+ <form class="filters" id="req-filters" onsubmit="event.preventDefault(); loadRequests(true)">
203
+ <input id="f-server" placeholder="server id" aria-label="Filter by server" size="12" />
204
+ <input id="f-tool" placeholder="tool / URI / prompt" aria-label="Filter by tool" size="16" />
205
+ <select id="f-status" aria-label="Filter by status"><option value="">any status</option><option value="true">ok</option><option value="false">error</option></select>
206
+ <select id="f-via" aria-label="Filter by interface"><option value="">REST + MCP</option><option value="rest">REST</option><option value="mcp">MCP</option></select>
207
+ <select id="f-since" aria-label="Time range"><option value="">all time</option><option value="3600000">last hour</option><option value="86400000">last 24 h</option><option value="604800000">last 7 days</option></select>
208
+ <button class="refresh-btn" type="submit">Apply</button>
209
+ <span class="source" id="req-source"></span>
210
+ </form>
195
211
  <div class="table-wrap">
196
212
  <table>
197
- <thead><tr><th>Time</th><th>Method</th><th>Tool</th><th>Server</th><th>Duration</th><th>Status</th></tr></thead>
198
- <tbody id="requests-tbody"><tr><td colspan="6" class="loader"><span class="spinner"></span></td></tr></tbody>
213
+ <thead><tr><th>Time</th><th>Via</th><th>Tool / resource / prompt</th><th>Server</th><th>Client</th><th>Duration</th><th>Status</th></tr></thead>
214
+ <tbody id="requests-tbody"><tr><td colspan="7" class="loader"><span class="spinner"></span></td></tr></tbody>
199
215
  </table>
200
216
  </div>
217
+ <button class="refresh-btn more" id="req-more" type="button" hidden onclick="loadRequests(false)">Load older</button>
201
218
  </div>
202
219
  </main>
203
220
 
@@ -373,36 +390,76 @@
373
390
  } catch { /* shown via other panels */ }
374
391
  }
375
392
 
376
- async function loadRequests() {
393
+ // History comes from the persistent audit log when it is enabled
394
+ // (audit.enabled), otherwise from the in-memory log; paged with a cursor.
395
+ let reqCursor;
396
+ let reqPaged = false;
397
+
398
+ function requestQuery() {
399
+ const q = new URLSearchParams({ limit: '25' });
400
+ const v = (id) => document.getElementById(id).value.trim();
401
+ if (v('f-server')) q.set('server', v('f-server'));
402
+ if (v('f-tool')) q.set('tool', v('f-tool'));
403
+ if (v('f-status')) q.set('success', v('f-status'));
404
+ if (v('f-via')) q.set('via', v('f-via'));
405
+ if (v('f-since')) q.set('since', String(Date.now() - Number(v('f-since'))));
406
+ return q;
407
+ }
408
+
409
+ function requestRow(r) {
410
+ const color = r.success ? 'green' : 'red';
411
+ const err = r.errorMessage ? ` title="${esc(r.errorMessage)}"` : '';
412
+ const kind = r.kind && r.kind !== 'tool' ? `<span style="color:var(--muted)">${esc(r.kind)}</span> ` : '';
413
+ const via = (r.via ?? 'rest') === 'mcp' ? '<span class="method get">MCP</span>' : '<span class="method post">REST</span>';
414
+ const t = new Date(r.timestamp);
415
+ const today = t.toDateString() === new Date().toDateString();
416
+ return `<tr>
417
+ <td style="color:var(--muted)" title="${esc(t.toISOString())}">${esc(today ? t.toLocaleTimeString() : t.toLocaleString())}</td>
418
+ <td>${via}</td>
419
+ <td>${kind}<code>${esc(r.toolName ?? '—')}</code></td>
420
+ <td>${esc(r.serverId ?? '—')}</td>
421
+ <td style="color:var(--muted)">${esc(r.clientId ?? '—')}</td>
422
+ <td>${fmtDuration(r.durationMs)}</td>
423
+ <td style="color:var(--${color})"${err}>${r.success ? 'ok' : 'error'}</td>
424
+ </tr>`;
425
+ }
426
+
427
+ async function loadRequests(reset = true) {
377
428
  const tbody = document.getElementById('requests-tbody');
429
+ const more = document.getElementById('req-more');
430
+ if (reset) {
431
+ reqCursor = undefined;
432
+ reqPaged = false;
433
+ } else {
434
+ reqPaged = true;
435
+ }
378
436
  try {
379
- const data = await fetchJSON('/api/v1/requests?limit=20');
437
+ const q = requestQuery();
438
+ if (!reset && reqCursor) q.set('cursor', reqCursor);
439
+ const data = await fetchJSON('/api/v1/requests?' + q.toString());
380
440
  const reqs = data.requests ?? [];
381
- if (!reqs.length) {
382
- tbody.innerHTML = '<tr><td colspan="6" class="empty">No requests yet</td></tr>';
441
+ document.getElementById('req-source').textContent =
442
+ data.source === 'audit' ? 'source: persistent audit log' : data.source === 'memory' ? 'source: in-memory (since start)' : '';
443
+ reqCursor = data.nextCursor;
444
+ more.hidden = !reqCursor;
445
+ if (reset && !reqs.length) {
446
+ tbody.innerHTML = '<tr><td colspan="7" class="empty">No requests</td></tr>';
383
447
  return;
384
448
  }
385
- tbody.innerHTML = reqs.map(r => {
386
- const color = r.success ? 'green' : 'red';
387
- const err = r.errorMessage ? ` title="${esc(r.errorMessage)}"` : '';
388
- return `<tr>
389
- <td style="color:var(--muted)">${esc(new Date(r.timestamp).toLocaleTimeString())}</td>
390
- <td><span class="method post">CALL</span></td>
391
- <td><code>${esc(r.toolName ?? '—')}</code></td>
392
- <td>${esc(r.serverId ?? '—')}</td>
393
- <td>${fmtDuration(r.durationMs)}</td>
394
- <td style="color:var(--${color})"${err}>${r.success ? 'ok' : 'error'}</td>
395
- </tr>`;
396
- }).join('');
449
+ const html = reqs.map(requestRow).join('');
450
+ if (reset) tbody.innerHTML = html;
451
+ else tbody.insertAdjacentHTML('beforeend', html);
397
452
  } catch (e) {
398
- tbody.innerHTML = `<tr><td colspan="6" class="empty">${e instanceof AuthError ? 'API key required' : 'Failed to load requests'}</td></tr>`;
453
+ more.hidden = true;
454
+ tbody.innerHTML = `<tr><td colspan="7" class="empty">${e instanceof AuthError ? 'API key required' : 'Failed to load requests'}</td></tr>`;
399
455
  }
400
456
  }
401
457
 
402
458
  async function loadAll() {
403
459
  authMissing = false;
404
460
  await loadMetrics(); // tool call counts feed the tools table
405
- await Promise.allSettled([loadHealth(), loadServers(), loadTools(), loadRequests()]);
461
+ // Don't throw away older pages the user loaded on auto-refresh.
462
+ await Promise.allSettled([loadHealth(), loadServers(), loadTools(), reqPaged ? Promise.resolve() : loadRequests(true)]);
406
463
  notice.classList.toggle('show', authMissing);
407
464
  keyInput.classList.toggle('needed', authMissing);
408
465
  }
@@ -12,13 +12,32 @@
12
12
  * /requests endpoint, and keys sharing a prefix shared a rate-limit bucket).
13
13
  */
14
14
  import type { Request, RequestHandler } from 'express';
15
- import type { AuthConfig } from '../utils/types.js';
15
+ import type { ApiKeyConfig, AuthConfig } from '../utils/types.js';
16
+ import { type AccessScope } from './scopes.js';
16
17
  export type AuthedRequest = Request & {
17
18
  clientId?: string;
18
19
  jwtPayload?: unknown;
20
+ /** Servers / tools / rate limit this client is restricted to (absent = unrestricted). */
21
+ scope?: AccessScope;
19
22
  };
20
- export declare function createAuthMiddleware(config?: AuthConfig): RequestHandler;
23
+ /** An auth middleware that can also look up the current scope of a known client id (api keys). */
24
+ export type AuthMiddleware = RequestHandler & {
25
+ /**
26
+ * Current scope for `clientId`: `{ known: false }` when no configured key
27
+ * produces that id any more. Undefined for strategies whose scopes travel
28
+ * with the credential (JWT).
29
+ */
30
+ resolveClient?: (clientId: string | undefined) => {
31
+ known: boolean;
32
+ scope?: AccessScope;
33
+ };
34
+ };
35
+ /** Normalise `auth.apiKeys` entries (plain strings or objects). */
36
+ export declare function normalizeApiKeys(keys: AuthConfig['apiKeys']): ApiKeyConfig[];
37
+ export declare function createAuthMiddleware(config?: AuthConfig): AuthMiddleware;
21
38
  export declare function fingerprint(key: string): string;
22
39
  /** Constant-time membership check against a pre-hashed key list. */
23
40
  export declare function matchesAnyKey(candidate: string, keyDigests: Buffer[]): boolean;
41
+ /** Constant-time lookup: index of the matching key digest, or -1. */
42
+ export declare function matchKeyIndex(candidate: string, keyDigests: Buffer[]): number;
24
43
  //# sourceMappingURL=middleware.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../../src/auth/middleware.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAGH,OAAO,KAAK,EAAE,OAAO,EAA0B,cAAc,EAAE,MAAM,SAAS,CAAC;AAE/E,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAGpD,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAIlF,wBAAgB,oBAAoB,CAAC,MAAM,CAAC,EAAE,UAAU,GAAG,cAAc,CA2BxE;AAMD,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAE/C;AAED,oEAAoE;AACpE,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,OAAO,CAQ9E"}
1
+ {"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../../src/auth/middleware.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAGH,OAAO,KAAK,EAAE,OAAO,EAA0B,cAAc,EAAE,MAAM,SAAS,CAAC;AAE/E,OAAO,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAElE,OAAO,EAAgB,KAAK,WAAW,EAAE,MAAM,aAAa,CAAC;AAE7D,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG;IACpC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,yFAAyF;IACzF,KAAK,CAAC,EAAE,WAAW,CAAC;CACrB,CAAC;AAEF,kGAAkG;AAClG,MAAM,MAAM,cAAc,GAAG,cAAc,GAAG;IAC5C;;;;OAIG;IACH,aAAa,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,KAAK;QAAE,KAAK,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,WAAW,CAAA;KAAE,CAAC;CAC3F,CAAC;AAEF,mEAAmE;AACnE,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,UAAU,CAAC,SAAS,CAAC,GAAG,YAAY,EAAE,CAI5E;AAaD,wBAAgB,oBAAoB,CAAC,MAAM,CAAC,EAAE,UAAU,GAAG,cAAc,CAmCxE;AAMD,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAE/C;AAED,oEAAoE;AACpE,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,OAAO,CAE9E;AAED,qEAAqE;AACrE,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,MAAM,CAQ7E"}