@winstonsayno/mcp-gateway 0.3.0 → 1.0.0
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.
- package/CHANGELOG.md +41 -0
- package/README.md +280 -19
- package/dashboard/index.html +79 -22
- package/dist/auth/middleware.d.ts +21 -2
- package/dist/auth/middleware.d.ts.map +1 -1
- package/dist/auth/middleware.js +63 -13
- package/dist/auth/middleware.js.map +1 -1
- package/dist/auth/ratelimit.d.ts +16 -1
- package/dist/auth/ratelimit.d.ts.map +1 -1
- package/dist/auth/ratelimit.js +15 -8
- package/dist/auth/ratelimit.js.map +1 -1
- package/dist/auth/scopes.d.ts +40 -0
- package/dist/auth/scopes.d.ts.map +1 -0
- package/dist/auth/scopes.js +76 -0
- package/dist/auth/scopes.js.map +1 -0
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +99 -10
- package/dist/config/loader.js.map +1 -1
- package/dist/gateway/api.d.ts +30 -0
- package/dist/gateway/api.d.ts.map +1 -1
- package/dist/gateway/api.js +308 -11
- package/dist/gateway/api.js.map +1 -1
- package/dist/gateway/index.d.ts +4 -0
- package/dist/gateway/index.d.ts.map +1 -1
- package/dist/gateway/index.js +55 -2
- package/dist/gateway/index.js.map +1 -1
- package/dist/gateway/supervisor.d.ts +1 -0
- package/dist/gateway/supervisor.d.ts.map +1 -1
- package/dist/gateway/supervisor.js +12 -1
- package/dist/gateway/supervisor.js.map +1 -1
- package/dist/index.d.ts +14 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp/catalog.d.ts +30 -0
- package/dist/mcp/catalog.d.ts.map +1 -0
- package/dist/mcp/catalog.js +78 -0
- package/dist/mcp/catalog.js.map +1 -0
- package/dist/mcp/endpoint.d.ts +132 -0
- package/dist/mcp/endpoint.d.ts.map +1 -0
- package/dist/mcp/endpoint.js +714 -0
- package/dist/mcp/endpoint.js.map +1 -0
- package/dist/mcp/llm-schemas.d.ts +36 -0
- package/dist/mcp/llm-schemas.d.ts.map +1 -0
- package/dist/mcp/llm-schemas.js +69 -0
- package/dist/mcp/llm-schemas.js.map +1 -0
- package/dist/mcp/naming.d.ts +53 -0
- package/dist/mcp/naming.d.ts.map +1 -0
- package/dist/mcp/naming.js +71 -0
- package/dist/mcp/naming.js.map +1 -0
- package/dist/middleware/cors.d.ts +2 -0
- package/dist/middleware/cors.d.ts.map +1 -1
- package/dist/middleware/cors.js +25 -19
- package/dist/middleware/cors.js.map +1 -1
- package/dist/monitor/audit.d.ts +60 -0
- package/dist/monitor/audit.d.ts.map +1 -0
- package/dist/monitor/audit.js +155 -0
- package/dist/monitor/audit.js.map +1 -0
- package/dist/monitor/index.d.ts +19 -0
- package/dist/monitor/index.d.ts.map +1 -1
- package/dist/monitor/index.js +88 -0
- package/dist/monitor/index.js.map +1 -1
- package/dist/proxy/index.d.ts +25 -2
- package/dist/proxy/index.d.ts.map +1 -1
- package/dist/proxy/index.js +171 -16
- package/dist/proxy/index.js.map +1 -1
- package/dist/registry/index.d.ts +16 -2
- package/dist/registry/index.d.ts.map +1 -1
- package/dist/registry/index.js +37 -1
- package/dist/registry/index.js.map +1 -1
- package/dist/utils/tool-filter.d.ts +24 -0
- package/dist/utils/tool-filter.d.ts.map +1 -0
- package/dist/utils/tool-filter.js +48 -0
- package/dist/utils/tool-filter.js.map +1 -0
- package/dist/utils/types.d.ts +116 -2
- package/dist/utils/types.d.ts.map +1 -1
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,47 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
9
9
|
|
|
10
10
|
## [Unreleased]
|
|
11
11
|
|
|
12
|
+
## [1.0.0] - 2026-10-07
|
|
13
|
+
|
|
14
|
+
First stable release. From here on mcp-gateway follows semver: `/api/v1`, `/mcp`, configuration keys, CLI and
|
|
15
|
+
root library exports only change in backward-compatible ways within 1.x (see
|
|
16
|
+
[docs/api-reference.md#stability-and-versioning](docs/api-reference.md#stability-and-versioning)).
|
|
17
|
+
This release contains everything developed as v0.5 – v0.8.
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
- **Documentation** in `docs/`: API reference (REST, `/mcp`, error codes, stability policy), configuration reference and deployment guide (Docker, Kubernetes manifests, reverse proxy, security checklist, systemd).
|
|
21
|
+
- **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).
|
|
22
|
+
- Dependabot config (npm root + JS client, Gradle Kotlin client, GitHub Actions, Docker), pull-request template, issue-template links, `SECURITY.md`.
|
|
23
|
+
- **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"`.
|
|
24
|
+
- **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.
|
|
25
|
+
- `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*.
|
|
26
|
+
- JS client: `history()` (filters + cursor), `listResources`, `listResourceTemplates`, `readResource`, `listPrompts`, `getPrompt`.
|
|
27
|
+
- `SessionInfo.capabilities`; `McpProxy#getCatalog`, `#hasCapability`; registry `setCatalog` / `getAllResources` / `getAllResourceTemplates` / `getAllPrompts`; `MetricsCollector#setAuditStore` / `#queryRequests`; exports `SqliteAuditStore`, `sqliteAvailable`, `AuditStore`, catalog helpers and resource / prompt types.
|
|
28
|
+
- **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`.
|
|
29
|
+
- **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.
|
|
30
|
+
- **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.
|
|
31
|
+
- CI jobs for both clients.
|
|
32
|
+
- **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`.
|
|
33
|
+
- **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.
|
|
34
|
+
- `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.
|
|
35
|
+
- `McpProxy.request()` for arbitrary upstream methods and an optional `AbortSignal` on `callTool()` (`ERR_CANCELLED`).
|
|
36
|
+
- Tool `title`, `outputSchema` and `annotations` are kept from upstream `tools/list` and exposed on `/api/v1/tools` and `/mcp`.
|
|
37
|
+
- Request records carry `via: "rest" | "mcp"`.
|
|
38
|
+
- Library exports: `McpEndpoint`, `buildToolIndex`, `prefixedName`, `DOWNSTREAM_PROTOCOL_VERSIONS`, `ERR_RATE_LIMITED`, types `McpEndpointConfig`, `ToolNaming`, `McpSessionSummary`; `Gateway#getMcpEndpoint()`.
|
|
39
|
+
- Conformance tests with the official `@modelcontextprotocol/sdk` client (list, call, ping, `list_changed`, cancellation, session termination, auth).
|
|
40
|
+
- CORS allows the `Mcp-Session-Id`, `MCP-Protocol-Version` and `Last-Event-ID` request headers and exposes `Mcp-Session-Id`.
|
|
41
|
+
|
|
42
|
+
### Changed
|
|
43
|
+
- 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+.
|
|
44
|
+
- 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.
|
|
45
|
+
- `AuthConfig.apiKeys` is typed `Array<string | ApiKeyConfig>` (was `string[]`); existing configs are unchanged. `createAuthMiddleware()` returns an `AuthMiddleware` (a `RequestHandler` with an optional `resolveClient`).
|
|
46
|
+
|
|
47
|
+
## [0.4.0] - 2026-10-07
|
|
48
|
+
|
|
49
|
+
### Added
|
|
50
|
+
- **Readiness probe** `GET /api/v1/health/ready`: always public, `200` when every enabled server is connected and not `degraded` (or at least `?min=N`), otherwise `503`; `503 shutting_down` during graceful shutdown. Body carries only counts. `computeReadiness()` is exported for library use. README documents liveness vs. readiness with a Kubernetes example.
|
|
51
|
+
- **Per-server tool filtering**: `servers[].tools.allow` / `servers[].tools.deny` glob patterns (`*`, `?`; deny wins). Hidden tools are removed from discovery, counts and routing; calling one with an explicit `server` returns `403`. Applied to `tools/list_changed` updates and on hot reload. `isToolAllowed` / `filterTools` are exported for library use.
|
|
52
|
+
|
|
12
53
|
## [0.3.0] - 2026-10-07
|
|
13
54
|
|
|
14
55
|
### Added
|
package/README.md
CHANGED
|
@@ -11,10 +11,11 @@ Route · Authenticate · Rate-limit · Monitor — all your [Model Context Proto
|
|
|
11
11
|
[](LICENSE)
|
|
12
12
|
[](https://nodejs.org)
|
|
13
13
|
[](https://www.typescriptlang.org)
|
|
14
|
-
[](https://www.npmjs.com/package/@winstonsayno/mcp-gateway)
|
|
15
|
+
[](https://github.com/HarrisonCN/mcp-gateway/actions/workflows/ci.yml)
|
|
16
|
+
[](https://github.com/HarrisonCN/mcp-gateway/pkgs/container/mcp-gateway)
|
|
16
17
|
|
|
17
|
-
[English](#) · [中文](docs/README.zh-CN.md) · [
|
|
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,19 +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
|
|
80
|
+
- **Tool filtering** — per-server `tools.allow` / `tools.deny` glob patterns hide tools you don't want exposed (and block calls to them)
|
|
75
81
|
- **YAML/JSON config** — simple, declarative configuration with env var overrides
|
|
76
82
|
- **Docker-ready** — official Docker image, Compose examples included
|
|
77
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
|
|
78
86
|
|
|
79
87
|
## Quick Start
|
|
80
88
|
|
|
@@ -142,6 +150,65 @@ curl -X POST http://localhost:4000/api/v1/tools/call \
|
|
|
142
150
|
-d '{"tool": "create_issue", "server": "github", "arguments": {"title": "Bug report", "body": "..."}}'
|
|
143
151
|
```
|
|
144
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
|
+
|
|
145
212
|
## API Reference
|
|
146
213
|
|
|
147
214
|
| Method | Path | Description |
|
|
@@ -149,25 +216,62 @@ curl -X POST http://localhost:4000/api/v1/tools/call \
|
|
|
149
216
|
| `GET` | `/api/v1/health` | Gateway health and server summary |
|
|
150
217
|
| `GET` | `/api/v1/servers` | List all registered servers |
|
|
151
218
|
| `GET` | `/api/v1/servers/:id` | Get server details and tools |
|
|
152
|
-
| `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) |
|
|
153
220
|
| `POST` | `/api/v1/tools/call` | Invoke a tool |
|
|
154
221
|
| `POST` | `/api/v1/servers/:id/reconnect` | Reconnect a server now (resets backoff) |
|
|
155
222
|
| `GET` | `/api/v1/health/live` | Liveness probe — always public, returns only `{"status":"ok"}` |
|
|
223
|
+
| `GET` | `/api/v1/health/ready` | Readiness probe — always public; `200` when servers are ready, else `503` (`?min=N`) |
|
|
156
224
|
| `GET` | `/api/v1/metrics` | Aggregated metrics (JSON or Prometheus) |
|
|
157
|
-
| `GET` | `/api/v1/
|
|
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)) |
|
|
158
232
|
|
|
159
233
|
`/health` and `/metrics` are unauthenticated by default; set `auth.protect.health` / `auth.protect.metrics`
|
|
160
|
-
to require auth for them too (`/health/live` always
|
|
234
|
+
to require auth for them too (`/health/live` and `/health/ready` always stay public for Docker / Kubernetes probes).
|
|
161
235
|
Every other route requires auth when it is enabled.
|
|
162
236
|
`/metrics` returns JSON by default (`?window=<ms>`); with `monitor.prometheus: true` it returns the
|
|
163
237
|
Prometheus text format when the client asks for `text/plain` (as Prometheus does) or passes `?format=prometheus`.
|
|
164
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
|
+
|
|
165
268
|
`POST /api/v1/tools/call` responses:
|
|
166
269
|
|
|
167
270
|
| Status | Meaning |
|
|
168
271
|
|--------|---------|
|
|
169
272
|
| `200` | Tool returned a result |
|
|
170
273
|
| `400` | Invalid body (`tool` must be a string, `arguments` an object) or malformed JSON |
|
|
274
|
+
| `403` | The tool is hidden by the server's `tools` filter (also when `"server"` is passed explicitly), or outside the caller's scope |
|
|
171
275
|
| `404` | Unknown tool or server |
|
|
172
276
|
| `409` | Tool name is exposed by several servers — pass `"server"` to choose |
|
|
173
277
|
| `429` | Rate limited (see `Retry-After`) |
|
|
@@ -185,7 +289,12 @@ logLevel: info # debug | info | warn | error
|
|
|
185
289
|
auth:
|
|
186
290
|
strategy: api-key # none | api-key | jwt (oauth2 is not implemented and is rejected)
|
|
187
291
|
apiKeys:
|
|
188
|
-
- "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 }
|
|
189
298
|
protect:
|
|
190
299
|
health: false # true → /api/v1/health requires auth
|
|
191
300
|
metrics: false # true → /api/v1/metrics requires auth (configure your scraper)
|
|
@@ -215,6 +324,21 @@ monitor:
|
|
|
215
324
|
corsOrigins:
|
|
216
325
|
- "https://your-app.com"
|
|
217
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
|
+
|
|
218
342
|
servers:
|
|
219
343
|
- id: my-server # Unique identifier
|
|
220
344
|
name: My Server # Display name
|
|
@@ -227,6 +351,9 @@ servers:
|
|
|
227
351
|
enabled: true
|
|
228
352
|
timeout: 30000 # ms, includes time queued behind maxConcurrency
|
|
229
353
|
maxConcurrency: 10 # max in-flight tool calls for this server
|
|
354
|
+
tools: # optional: expose only some tools (globs: * and ?, deny wins)
|
|
355
|
+
allow: ["read_*", "list_*"]
|
|
356
|
+
deny: ["*_secret"]
|
|
230
357
|
reconnect: # optional per-server override of the reconnect block
|
|
231
358
|
maxAttempts: 5
|
|
232
359
|
|
|
@@ -259,10 +386,104 @@ With `mcp-gateway start` the config file is watched (disable with `--no-watch`).
|
|
|
259
386
|
| `auth` (strategy, keys, JWT secret, `protect`) | `monitor.retentionHours` |
|
|
260
387
|
| `rateLimit` (counters reset when it changes) | `healthCheckIntervalMs` |
|
|
261
388
|
| `corsOrigins`, `monitor.requestLog`, `monitor.prometheus` | `dashboard` |
|
|
262
|
-
| `reconnect`, `logLevel` | |
|
|
389
|
+
| `reconnect`, `logLevel` | `mcp.enabled`, `mcp.path`, `audit` |
|
|
390
|
+
| `mcp.toolNaming`, `mcp.pageSize`, session limits, `mcp.allowedOrigins` | |
|
|
263
391
|
|
|
264
392
|
An invalid file is rejected and the running config is kept. `MCP_GATEWAY_*` env overrides keep precedence.
|
|
265
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
|
+
|
|
463
|
+
### Tool filtering
|
|
464
|
+
|
|
465
|
+
Expose only part of a server's tools — e.g. make a filesystem server read-only, or drop tools that
|
|
466
|
+
clash with another server's names:
|
|
467
|
+
|
|
468
|
+
```yaml
|
|
469
|
+
servers:
|
|
470
|
+
- id: filesystem
|
|
471
|
+
name: Filesystem (read-only)
|
|
472
|
+
transport: stdio
|
|
473
|
+
command: npx
|
|
474
|
+
args: ["-y", "@modelcontextprotocol/server-filesystem", "/data"]
|
|
475
|
+
tools:
|
|
476
|
+
allow: ["read_*", "list_*", "search_files", "get_file_info"]
|
|
477
|
+
deny: ["*_media_file"]
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
- Patterns are globs matched against the whole tool name, case-sensitive: `*` = any characters, `?` = one character.
|
|
481
|
+
- A tool is exposed when it matches an `allow` pattern (or `allow` is absent / empty) **and** no `deny` pattern. Deny wins.
|
|
482
|
+
- Hidden tools are absent from `GET /tools`, `GET /servers/:id`, tool counts and metrics, and don't cause
|
|
483
|
+
name conflicts with other servers. Calling one returns `404` (auto-routing) or `403` (explicit `"server"`).
|
|
484
|
+
- The filter is applied to every tool list, including updates via `notifications/tools/list_changed`.
|
|
485
|
+
Changing it in the config file is applied by hot reload (the server is reconnected).
|
|
486
|
+
|
|
266
487
|
### Reconnect & server status
|
|
267
488
|
|
|
268
489
|
Each server's `health.status` is one of `online`, `degraded` (connected, but the health ping failed),
|
|
@@ -280,7 +501,7 @@ Prometheus series added: `mcp_gateway_server_up`, `mcp_gateway_server_status{sta
|
|
|
280
501
|
docker run -p 4000:4000 \
|
|
281
502
|
-v $(pwd)/mcp-gateway.yml:/app/mcp-gateway.yml \
|
|
282
503
|
-e GITHUB_TOKEN=ghp_... \
|
|
283
|
-
ghcr.io/
|
|
504
|
+
ghcr.io/harrisoncn/mcp-gateway:latest
|
|
284
505
|
|
|
285
506
|
# Or with Docker Compose (gateway + Prometheus; see examples/docker)
|
|
286
507
|
cd examples/docker
|
|
@@ -290,6 +511,31 @@ GATEWAY_API_KEY=change-me docker compose up
|
|
|
290
511
|
The image's `HEALTHCHECK` uses the always-public `/api/v1/health/live`, so it keeps working when
|
|
291
512
|
`auth.protect.health` is on.
|
|
292
513
|
|
|
514
|
+
### Liveness vs. readiness (Kubernetes, load balancers)
|
|
515
|
+
|
|
516
|
+
| Probe | Answers | Use it for |
|
|
517
|
+
|-------|---------|------------|
|
|
518
|
+
| `GET /api/v1/health/live` | always `200 {"status":"ok"}` while the process serves HTTP | restart a hung container |
|
|
519
|
+
| `GET /api/v1/health/ready` | `200` when the upstream servers are ready, otherwise `503` | only route traffic to gateways that can serve tool calls |
|
|
520
|
+
|
|
521
|
+
A server counts as ready when it is enabled, connected and not `degraded` (failing health pings).
|
|
522
|
+
By default **every** enabled server must be ready; `?min=N` requires at least `N` instead (useful when
|
|
523
|
+
some servers are optional). With no servers configured the gateway is ready. While shutting down it
|
|
524
|
+
answers `503 {"status":"shutting_down"}` so load balancers drain it first. The body only carries counts:
|
|
525
|
+
|
|
526
|
+
```json
|
|
527
|
+
{ "status": "not_ready", "servers": { "ready": 1, "total": 2, "required": 2 } }
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
```yaml
|
|
531
|
+
# Kubernetes
|
|
532
|
+
livenessProbe:
|
|
533
|
+
httpGet: { path: /api/v1/health/live, port: 4000 }
|
|
534
|
+
readinessProbe:
|
|
535
|
+
httpGet: { path: /api/v1/health/ready, port: 4000 } # or /api/v1/health/ready?min=1
|
|
536
|
+
periodSeconds: 10
|
|
537
|
+
```
|
|
538
|
+
|
|
293
539
|
## Dashboard
|
|
294
540
|
|
|
295
541
|
Open `http://localhost:4000/dashboard`. When auth is enabled, paste an API key (or JWT) in the header:
|
|
@@ -300,7 +546,7 @@ to stop serving it.
|
|
|
300
546
|
## Embed as a Library
|
|
301
547
|
|
|
302
548
|
```typescript
|
|
303
|
-
import { Gateway, loadConfig } from 'mcp-gateway';
|
|
549
|
+
import { Gateway, loadConfig } from '@winstonsayno/mcp-gateway';
|
|
304
550
|
|
|
305
551
|
const config = await loadConfig('./mcp-gateway.yml');
|
|
306
552
|
const gateway = new Gateway(config);
|
|
@@ -312,16 +558,18 @@ await gateway.start();
|
|
|
312
558
|
process.on('SIGTERM', () => gateway.stop());
|
|
313
559
|
```
|
|
314
560
|
|
|
315
|
-
## What's New
|
|
561
|
+
## What's New in v1.0
|
|
316
562
|
|
|
317
563
|
| Feature | Description |
|
|
318
564
|
|---------|-------------|
|
|
319
|
-
| **
|
|
320
|
-
| **
|
|
321
|
-
| **
|
|
322
|
-
| **
|
|
323
|
-
| **
|
|
324
|
-
| **
|
|
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 |
|
|
325
573
|
|
|
326
574
|
## What's New in v0.2.0
|
|
327
575
|
|
|
@@ -335,6 +583,14 @@ process.on('SIGTERM', () => gateway.stop());
|
|
|
335
583
|
| **Web Dashboard** | Live monitoring UI at `/dashboard` |
|
|
336
584
|
| **4 Bug Fixes** | Concurrency, id collision, handle leaks, timeouts |
|
|
337
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
|
+
|
|
338
594
|
## Roadmap
|
|
339
595
|
|
|
340
596
|
| Feature | Status |
|
|
@@ -346,10 +602,15 @@ process.on('SIGTERM', () => gateway.stop());
|
|
|
346
602
|
| Automatic reconnect with backoff | ✅ Done |
|
|
347
603
|
| Config hot reload | ✅ Done (v0.2.0) |
|
|
348
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) |
|
|
349
610
|
| Redis-backed rate limiting | 📋 Planned |
|
|
350
611
|
| OAuth2 / OIDC auth | 📋 Planned |
|
|
351
|
-
| Tool-level access control
|
|
352
|
-
| 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) |
|
|
353
614
|
| Multi-tenant mode | 📋 Planned |
|
|
354
615
|
| OpenTelemetry tracing | 📋 Planned |
|
|
355
616
|
|
package/dashboard/index.html
CHANGED
|
@@ -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
|
-
<!--
|
|
199
|
+
<!-- Request history -->
|
|
193
200
|
<div class="section">
|
|
194
|
-
<div class="section-title">
|
|
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>
|
|
198
|
-
<tbody id="requests-tbody"><tr><td colspan="
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
382
|
-
|
|
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
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|