@winstonsayno/mcp-gateway 4.6.0 → 4.8.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 CHANGED
@@ -9,6 +9,41 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
9
9
 
10
10
  ## [Unreleased]
11
11
 
12
+ ## [4.8.0] - 2026-10-08
13
+
14
+ ### Added
15
+ - **Offline / edge sync** for the edge gateway (`@winstonsayno/mcp-gateway/edge`, new `EdgeSync`, `memoryStore`):
16
+ - **Config snapshots** from a Node gateway acting as control plane (`GET /api/v1/admin/edge/snapshot`, ETag /
17
+ `If-None-Match`): enabled `streamable-http` servers with their known tool catalog, unscoped API keys as `sha256:`
18
+ digests, tool naming, CORS. The last good snapshot is kept in a KV store (Workers KV compatible), so a cold isolate
19
+ boots while the control plane is down. Local servers / headers / settings win over the snapshot.
20
+ - Upstream headers are only included with `includeSecrets` (`?secrets=true`), which needs `admin.configApi: true`.
21
+ - **Offline tool lists**: when an upstream is unreachable, `tools/list` serves the last list or the snapshot catalog.
22
+ - **Offline queue** (`offline.queueTools` globs): calls to an unreachable / 5xx upstream are queued (REST `202`
23
+ with `queued`, MCP `structuredContent: { queued: true, id }`) and replayed on the next sync; rejected calls are
24
+ dropped and reported.
25
+ - **Usage outbox**: live / queued / replayed calls are pushed to `POST /api/v1/admin/edge/sync` and show up in the
26
+ control plane's metrics as client `edge:<edgeId>`; `GET /api/v1/admin/edge/nodes` lists edges.
27
+ - `gw.sync()`, background sync on requests (`syncIntervalMs`, Workers `waitUntil`), `GET /api/v1/edge/status`,
28
+ `POST /api/v1/edge/sync`; `workersHandler()` gains `scheduled` (Cron Triggers); `configFromEnv` reads
29
+ `MCP_GATEWAY_CONTROL_PLANE`, `_CONTROL_KEY`, `_EDGE_ID`, `_QUEUE_TOOLS`, `_SYNC_INTERVAL_MS` and the `MCP_GATEWAY_KV` binding.
30
+ - The GitHub Pages demo simulates the edge control plane endpoints.
31
+
32
+ ## [4.7.0] - 2026-10-08
33
+
34
+ ### Added
35
+ - **Python SDK** (`clients/python`, package `mcp-gateway-client`): stdlib-only (`urllib`), Python ≥ 3.9, typed
36
+ `health` / `ready` / `metrics`, `servers`, `list_tools`, `tool_schemas` + `call_llm_tool`, `call_tool`, resources,
37
+ prompts, approvals; `GatewayError` with `status`, `code`, `retry_after`, `is_policy_error`. `py.typed`.
38
+ - **Go SDK** (`clients/go`, module `github.com/HarrisonCN/mcp-gateway/clients/go`): `net/http` only, Go ≥ 1.21,
39
+ context-aware `Health`, `Ready`, `Servers`, `ListTools`, `ToolSchemas` + `CallLLMTool`, `CallTool`, `Approve` / `Deny`;
40
+ `*mcpgateway.Error` with `Code()`, `IsPolicyError()`, `RetryAfter`.
41
+ - **Swift SDK** (`clients/swift`, SwiftPM `MCPGateway`): async/await, `URLSession` by default with a pluggable
42
+ transport, macOS 12 / iOS 15 / Linux; `GatewayError.isPolicyError`.
43
+ - New **SDKs** CI workflow (`.github/workflows/sdks.yml`): Python 3.9 / 3.12 / 3.13 tests + sdist/wheel build,
44
+ Go 1.21 / stable `vet` + `test -race`, Swift 6.0 `build` + `test` on Linux. Package-registry publishing (PyPI, Go
45
+ module tags, Swift Package Index) is not automated yet.
46
+
12
47
  ## [4.6.0] - 2026-10-08
13
48
 
14
49
  ### Added
package/README.md CHANGED
@@ -84,7 +84,7 @@ As [MCP](https://modelcontextprotocol.io) becomes the standard protocol for AI a
84
84
  - **YAML/JSON config** — simple, declarative configuration with env var overrides
85
85
  - **Docker-ready** — official Docker image, Compose examples included
86
86
  - **TypeScript SDK** — embed the gateway as a library in your own project
87
- - **Client libraries** — a dependency-free TypeScript client ([`clients/js`](clients/js), browser + Node) and a Kotlin/JVM/Android client ([`clients/kotlin`](clients/kotlin))
87
+ - **Client libraries** — a dependency-free TypeScript client ([`clients/js`](clients/js), browser + Node) a Kotlin/JVM/Android client ([`clients/kotlin`](clients/kotlin)), and Python, Go and Swift SDKs ([`clients/python`](clients/python), [`clients/go`](clients/go), [`clients/swift`](clients/swift))
88
88
  - **LLM tool schemas** — `GET /api/v1/tools?format=openai|anthropic` returns ready-to-use function-calling definitions
89
89
 
90
90
  ## Quick Start
@@ -267,6 +267,9 @@ Names follow `mcp.toolNaming`, are sanitised to `^[a-zA-Z0-9_-]{1,64}$` and de-d
267
267
  |---|---|
268
268
  | **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 |
269
269
  | **Kotlin / JVM / Android** — [`clients/kotlin`](clients/kotlin) | OkHttp + kotlinx.serialization, Java 11 bytecode; same API surface, `McpSession` for `/mcp` |
270
+ | **Python** — [`clients/python`](clients/python) | `mcp-gateway-client`: stdlib only, Python ≥ 3.9, typed `health`, `list_tools`, `tool_schemas`, `call_tool`, `call_llm_tool`, approvals, `GatewayError.is_policy_error` |
271
+ | **Go** — [`clients/go`](clients/go) | `github.com/HarrisonCN/mcp-gateway/clients/go`: `net/http` only, Go ≥ 1.21, context-aware, `*mcpgateway.Error` with `IsPolicyError()` |
272
+ | **Swift** — [`clients/swift`](clients/swift) | SwiftPM `MCPGateway`: async/await, `URLSession` (pluggable transport), macOS 12 / iOS 15 / Linux |
270
273
 
271
274
  Both are in this repository and not yet published to npm / Maven Central.
272
275
 
@@ -583,6 +586,23 @@ await gateway.start();
583
586
  process.on('SIGTERM', () => gateway.stop());
584
587
  ```
585
588
 
589
+ ## What's New in v4.8
590
+
591
+ | Feature | Description |
592
+ |---------|-------------|
593
+ | **Edge config sync** | Edges pull ETag'd snapshots from a Node control plane and boot from the cached one offline |
594
+ | **Offline queue** | Queue calls to unreachable upstreams (`offline.queueTools`), replay on the next sync |
595
+ | **Usage outbox** | Edge calls flow back into the control plane's metrics; `/admin/edge/nodes` lists edges |
596
+
597
+ ## What's New in v4.7
598
+
599
+ | Feature | Description |
600
+ |---------|-------------|
601
+ | **Python SDK** | `clients/python` — stdlib only, typed, policy-aware errors |
602
+ | **Go SDK** | `clients/go` — `net/http` only, context-aware |
603
+ | **Swift SDK** | `clients/swift` — async/await, macOS / iOS / Linux |
604
+ | **SDK CI** | New `sdks.yml` workflow builds and tests all three |
605
+
586
606
  ## What's New in v4.6
587
607
 
588
608
  | Feature | Description |
@@ -9,7 +9,9 @@
9
9
  * Config can also come from environment variables with `configFromEnv(env)`:
10
10
  * `MCP_GATEWAY_SERVERS` (JSON array of `{ id, url, headers?, tools? }`),
11
11
  * `MCP_GATEWAY_API_KEYS` (comma separated), `MCP_GATEWAY_TOOL_NAMING`,
12
- * `MCP_GATEWAY_CORS_ORIGINS`.
12
+ * `MCP_GATEWAY_CORS_ORIGINS`; offline / edge sync (4.8): `MCP_GATEWAY_CONTROL_PLANE`,
13
+ * `MCP_GATEWAY_CONTROL_KEY`, `MCP_GATEWAY_EDGE_ID`, `MCP_GATEWAY_QUEUE_TOOLS` (comma separated globs),
14
+ * `MCP_GATEWAY_SYNC_INTERVAL_MS` and a KV namespace bound as `MCP_GATEWAY_KV`.
13
15
  *
14
16
  * @module edge/adapters
15
17
  */
@@ -17,9 +19,16 @@ import { type EdgeConfig } from './index.js';
17
19
  type Env = Record<string, unknown>;
18
20
  /** Edge config from environment variables / Worker bindings. */
19
21
  export declare function configFromEnv(env: Env): EdgeConfig;
20
- /** Cloudflare Workers module handler. The gateway (and its upstream sessions) is reused per isolate. */
22
+ interface WorkersCtx {
23
+ waitUntil(p: Promise<unknown>): void;
24
+ }
25
+ /**
26
+ * Cloudflare Workers module handler. The gateway (and its upstream sessions) is reused per isolate.
27
+ * With sync configured, add a Cron Trigger: `scheduled` pulls config, replays queued calls and pushes usage.
28
+ */
21
29
  export declare function workersHandler(build?: (env: Env) => EdgeConfig): {
22
- fetch(request: Request, env?: Env): Promise<Response>;
30
+ fetch(request: Request, env?: Env, ctx?: WorkersCtx): Promise<Response>;
31
+ scheduled(_event: unknown, env?: Env, ctx?: WorkersCtx): Promise<unknown>;
23
32
  };
24
33
  interface ServeOptions {
25
34
  port?: number;
@@ -1 +1 @@
1
- {"version":3,"file":"adapters.d.ts","sourceRoot":"","sources":["../../src/edge/adapters.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAqB,KAAK,UAAU,EAAoB,MAAM,YAAY,CAAC;AAElF,KAAK,GAAG,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAEnC,gEAAgE;AAChE,wBAAgB,aAAa,CAAC,GAAG,EAAE,GAAG,GAAG,UAAU,CAgBlD;AAED,wGAAwG;AACxG,wBAAgB,cAAc,CAAC,KAAK,GAAE,CAAC,GAAG,EAAE,GAAG,KAAK,UAA0B;mBAI3D,OAAO,QAAO,GAAG,GAAQ,OAAO,CAAC,QAAQ,CAAC;EAQ5D;AAED,UAAU,YAAY;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,0BAA0B;AAC1B,wBAAgB,SAAS,CAAC,MAAM,EAAE,UAAU,EAAE,IAAI,GAAE,YAAiB,GAAG,OAAO,CAK9E;AAED,wBAAwB;AACxB,wBAAgB,QAAQ,CAAC,MAAM,EAAE,UAAU,EAAE,IAAI,GAAE,YAAiB,GAAG,OAAO,CAK7E;AAED,qGAAqG;AACrG,wBAAsB,SAAS,CAAC,MAAM,EAAE,UAAU,EAAE,IAAI,GAAE,YAAiB,GAAG,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAAE,CAAC,CAyBlI"}
1
+ {"version":3,"file":"adapters.d.ts","sourceRoot":"","sources":["../../src/edge/adapters.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAqB,KAAK,UAAU,EAAoB,MAAM,YAAY,CAAC;AAGlF,KAAK,GAAG,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAEnC,gEAAgE;AAChE,wBAAgB,aAAa,CAAC,GAAG,EAAE,GAAG,GAAG,UAAU,CAiClD;AAED,UAAU,UAAU;IAClB,SAAS,CAAC,CAAC,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,IAAI,CAAC;CACtC;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,KAAK,GAAE,CAAC,GAAG,EAAE,GAAG,KAAK,UAA0B;mBAW3D,OAAO,QAAO,GAAG,QAAa,UAAU,GAAG,OAAO,CAAC,QAAQ,CAAC;sBAGzD,OAAO,QAAO,GAAG,QAAa,UAAU,GAAG,OAAO,CAAC,OAAO,CAAC;EAMhF;AAED,UAAU,YAAY;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,0BAA0B;AAC1B,wBAAgB,SAAS,CAAC,MAAM,EAAE,UAAU,EAAE,IAAI,GAAE,YAAiB,GAAG,OAAO,CAK9E;AAED,wBAAwB;AACxB,wBAAgB,QAAQ,CAAC,MAAM,EAAE,UAAU,EAAE,IAAI,GAAE,YAAiB,GAAG,OAAO,CAK7E;AAED,qGAAqG;AACrG,wBAAsB,SAAS,CAAC,MAAM,EAAE,UAAU,EAAE,IAAI,GAAE,YAAiB,GAAG,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAAE,CAAC,CAyBlI"}
@@ -9,7 +9,9 @@
9
9
  * Config can also come from environment variables with `configFromEnv(env)`:
10
10
  * `MCP_GATEWAY_SERVERS` (JSON array of `{ id, url, headers?, tools? }`),
11
11
  * `MCP_GATEWAY_API_KEYS` (comma separated), `MCP_GATEWAY_TOOL_NAMING`,
12
- * `MCP_GATEWAY_CORS_ORIGINS`.
12
+ * `MCP_GATEWAY_CORS_ORIGINS`; offline / edge sync (4.8): `MCP_GATEWAY_CONTROL_PLANE`,
13
+ * `MCP_GATEWAY_CONTROL_KEY`, `MCP_GATEWAY_EDGE_ID`, `MCP_GATEWAY_QUEUE_TOOLS` (comma separated globs),
14
+ * `MCP_GATEWAY_SYNC_INTERVAL_MS` and a KV namespace bound as `MCP_GATEWAY_KV`.
13
15
  *
14
16
  * @module edge/adapters
15
17
  */
@@ -26,24 +28,52 @@ export function configFromEnv(env) {
26
28
  servers = parsed;
27
29
  }
28
30
  const naming = str('MCP_GATEWAY_TOOL_NAMING');
31
+ const list = (k) => str(k)?.split(',').map((x) => x.trim()).filter(Boolean);
32
+ const cp = str('MCP_GATEWAY_CONTROL_PLANE');
33
+ const kv = env.MCP_GATEWAY_KV;
34
+ const interval = Number(str('MCP_GATEWAY_SYNC_INTERVAL_MS'));
29
35
  return {
30
36
  servers,
31
- apiKeys: str('MCP_GATEWAY_API_KEYS')?.split(',').map((k) => k.trim()).filter(Boolean),
32
- toolNaming: naming === 'prefix' ? 'prefix' : 'auto',
33
- corsOrigins: str('MCP_GATEWAY_CORS_ORIGINS')?.split(',').map((o) => o.trim()).filter(Boolean),
37
+ apiKeys: list('MCP_GATEWAY_API_KEYS'),
38
+ // With a control plane, an unset naming comes from the snapshot.
39
+ toolNaming: naming === 'prefix' ? 'prefix' : naming === 'auto' || !cp ? 'auto' : undefined,
40
+ corsOrigins: list('MCP_GATEWAY_CORS_ORIGINS'),
41
+ ...(cp
42
+ ? {
43
+ sync: {
44
+ controlPlane: cp,
45
+ apiKey: str('MCP_GATEWAY_CONTROL_KEY'),
46
+ edgeId: str('MCP_GATEWAY_EDGE_ID'),
47
+ store: kv && typeof kv.get === 'function' ? kv : undefined,
48
+ },
49
+ offline: { queueTools: list('MCP_GATEWAY_QUEUE_TOOLS') },
50
+ ...(Number.isFinite(interval) && str('MCP_GATEWAY_SYNC_INTERVAL_MS') ? { syncIntervalMs: interval } : {}),
51
+ }
52
+ : {}),
34
53
  };
35
54
  }
36
- /** Cloudflare Workers module handler. The gateway (and its upstream sessions) is reused per isolate. */
55
+ /**
56
+ * Cloudflare Workers module handler. The gateway (and its upstream sessions) is reused per isolate.
57
+ * With sync configured, add a Cron Trigger: `scheduled` pulls config, replays queued calls and pushes usage.
58
+ */
37
59
  export function workersHandler(build = configFromEnv) {
38
60
  let gw;
39
61
  let built;
62
+ const get = (env) => {
63
+ if (!gw || built !== env) {
64
+ gw = createEdgeGateway(build(env));
65
+ built = env;
66
+ }
67
+ return gw;
68
+ };
40
69
  return {
41
- fetch(request, env = {}) {
42
- if (!gw || built !== env) {
43
- gw = createEdgeGateway(build(env));
44
- built = env;
45
- }
46
- return gw.fetch(request);
70
+ fetch(request, env = {}, ctx) {
71
+ return get(env).fetch(request, ctx ? (p) => ctx.waitUntil(p) : undefined);
72
+ },
73
+ scheduled(_event, env = {}, ctx) {
74
+ const p = get(env).sync();
75
+ ctx?.waitUntil(p);
76
+ return p;
47
77
  },
48
78
  };
49
79
  }
@@ -1 +1 @@
1
- {"version":3,"file":"adapters.js","sourceRoot":"","sources":["../../src/edge/adapters.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"adapters.js","sourceRoot":"","sources":["../../src/edge/adapters.ts"],"names":[],"mappings":"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"}
@@ -3,6 +3,8 @@
3
3
  * Imports nothing from Node (except `serveNode`, which loads `node:http` lazily).
4
4
  */
5
5
  export { createEdgeGateway, EdgeUpstream, readRpcReply, EDGE_PROTOCOL_VERSION } from './index.js';
6
- export type { EdgeConfig, EdgeServerConfig, EdgeGateway } from './index.js';
6
+ export type { EdgeConfig, EdgeServerConfig, EdgeGateway, EdgeOfflineConfig, EdgeSyncReport } from './index.js';
7
+ export { EdgeSync, memoryStore } from './sync.js';
8
+ export type { EdgeSyncOptions, EdgeSyncStore, EdgeSnapshot, EdgeEvent, QueuedCall, PullResult, EdgeCatalogTool } from './sync.js';
7
9
  export { workersHandler, serveDeno, serveBun, serveNode, configFromEnv } from './adapters.js';
8
10
  //# sourceMappingURL=entry.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"entry.d.ts","sourceRoot":"","sources":["../../src/edge/entry.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,YAAY,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAClG,YAAY,EAAE,UAAU,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAC5E,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,QAAQ,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC"}
1
+ {"version":3,"file":"entry.d.ts","sourceRoot":"","sources":["../../src/edge/entry.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,YAAY,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAClG,YAAY,EAAE,UAAU,EAAE,gBAAgB,EAAE,WAAW,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC/G,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAClD,YAAY,EAAE,eAAe,EAAE,aAAa,EAAE,YAAY,EAAE,SAAS,EAAE,UAAU,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAClI,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,QAAQ,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC"}
@@ -3,5 +3,6 @@
3
3
  * Imports nothing from Node (except `serveNode`, which loads `node:http` lazily).
4
4
  */
5
5
  export { createEdgeGateway, EdgeUpstream, readRpcReply, EDGE_PROTOCOL_VERSION } from './index.js';
6
+ export { EdgeSync, memoryStore } from './sync.js';
6
7
  export { workersHandler, serveDeno, serveBun, serveNode, configFromEnv } from './adapters.js';
7
8
  //# sourceMappingURL=entry.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"entry.js","sourceRoot":"","sources":["../../src/edge/entry.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,YAAY,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAElG,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,QAAQ,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC"}
1
+ {"version":3,"file":"entry.js","sourceRoot":"","sources":["../../src/edge/entry.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,YAAY,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAElG,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAElD,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,QAAQ,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC"}
@@ -14,6 +14,7 @@
14
14
  * @module edge
15
15
  */
16
16
  import type { ToolFilterConfig } from '../utils/types.js';
17
+ import { EdgeSync, type EdgeCatalogTool, type EdgeSyncOptions, type PullResult } from './sync.js';
17
18
  export declare const EDGE_PROTOCOL_VERSION = "2025-06-18";
18
19
  export interface EdgeServerConfig {
19
20
  id: string;
@@ -23,6 +24,15 @@ export interface EdgeServerConfig {
23
24
  headers?: Record<string, string>;
24
25
  tools?: ToolFilterConfig;
25
26
  timeoutMs?: number;
27
+ /** Known tools (from a control-plane snapshot): served by `tools/list` while the upstream is unreachable. */
28
+ catalog?: EdgeCatalogTool[];
29
+ }
30
+ export interface EdgeOfflineConfig {
31
+ /**
32
+ * Tools (globs on the exposed name, or `<server>__<tool>`) whose calls are queued while their upstream is
33
+ * unreachable and replayed on the next sync. Needs `sync`. Only for idempotent / fire-and-forget tools.
34
+ */
35
+ queueTools?: string[];
26
36
  }
27
37
  export interface EdgeConfig {
28
38
  servers: EdgeServerConfig[];
@@ -38,6 +48,11 @@ export interface EdgeConfig {
38
48
  fetch?: typeof fetch;
39
49
  name?: string;
40
50
  version?: string;
51
+ /** Control-plane sync (4.8): config snapshots, usage outbox, offline queue. */
52
+ sync?: EdgeSync | EdgeSyncOptions;
53
+ /** Minimum time between background syncs triggered by requests (default 60 s; 0 = only explicit `sync()`). */
54
+ syncIntervalMs?: number;
55
+ offline?: EdgeOfflineConfig;
41
56
  }
42
57
  interface UpstreamTool {
43
58
  name: string;
@@ -61,11 +76,30 @@ export declare class EdgeUpstream {
61
76
  private post;
62
77
  private init;
63
78
  request(method: string, params?: Json, retried?: boolean): Promise<unknown>;
79
+ /** True while the last tools/list failed and tools come from the stale cache or the snapshot catalog. */
80
+ offline: boolean;
64
81
  tools(maxAgeMs?: number): Promise<UpstreamTool[]>;
82
+ /** Carry the session and tool cache over to a replacement with the same endpoint. */
83
+ adopt(prev: EdgeUpstream): void;
84
+ }
85
+ export interface EdgeSyncReport {
86
+ config: PullResult['status'] | 'disabled';
87
+ etag?: string;
88
+ replayed: number;
89
+ failed: number;
90
+ pending: number;
91
+ pushed: number;
92
+ offline: boolean;
93
+ errors: string[];
65
94
  }
66
95
  export interface EdgeGateway {
67
- fetch(request: Request): Promise<Response>;
96
+ /** Fetch handler; `waitUntil` (Workers `ctx.waitUntil`) keeps background syncs alive after the response. */
97
+ fetch(request: Request, waitUntil?: (p: Promise<unknown>) => void): Promise<Response>;
68
98
  upstreams: ReadonlyMap<string, EdgeUpstream>;
99
+ /** Pull config, replay queued calls, push usage events (no-op report when `sync` is not configured). */
100
+ sync(): Promise<EdgeSyncReport>;
101
+ /** The sync client, when configured. */
102
+ readonly syncClient?: EdgeSync;
69
103
  }
70
104
  /** Build the edge gateway's Fetch handler. */
71
105
  export declare function createEdgeGateway(config: EdgeConfig): EdgeGateway;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/edge/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAGH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAE1D,eAAO,MAAM,qBAAqB,eAAe,CAAC;AAGlD,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,2DAA2D;IAC3D,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,KAAK,CAAC,EAAE,gBAAgB,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,gBAAgB,EAAE,CAAC;IAC5B,8EAA8E;IAC9E,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,4CAA4C;IAC5C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,2FAA2F;IAC3F,UAAU,CAAC,EAAE,MAAM,GAAG,QAAQ,CAAC;IAC/B,2CAA2C;IAC3C,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,mDAAmD;IACnD,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,UAAU,YAAY;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAQD,KAAK,IAAI,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAsBpC,+EAA+E;AAC/E,wBAAsB,YAAY,CAAC,GAAG,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA2BpF;AAYD,2DAA2D;AAC3D,qBAAa,YAAY;IAMrB,QAAQ,CAAC,GAAG,EAAE,gBAAgB;IAC9B,OAAO,CAAC,QAAQ,CAAC,SAAS;IAN5B,OAAO,CAAC,OAAO,CAAC,CAAU;IAC1B,OAAO,CAAC,GAAG,CAAK;IAChB,OAAO,CAAC,UAAU,CAAC,CAAwC;gBAGhD,GAAG,EAAE,gBAAgB,EACb,SAAS,EAAE,OAAO,KAAK;YAG5B,IAAI;YAgBJ,IAAI;IA4BZ,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,IAAI,EAAE,OAAO,UAAQ,GAAG,OAAO,CAAC,OAAO,CAAC;IAezE,KAAK,CAAC,QAAQ,SAAS,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC;CAcxD;AAQD,MAAM,WAAW,WAAW;IAC1B,KAAK,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC3C,SAAS,EAAE,WAAW,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;CAC9C;AAED,8CAA8C;AAC9C,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,UAAU,GAAG,WAAW,CAgKjE"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/edge/index.ts"],"names":[],"mappings":"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"}
@@ -14,6 +14,7 @@
14
14
  * @module edge
15
15
  */
16
16
  import { globToRegExp } from '../utils/tool-filter.js';
17
+ import { EdgeSync } from './sync.js';
17
18
  export const EDGE_PROTOCOL_VERSION = '2025-06-18';
18
19
  const SUPPORTED_VERSIONS = ['2025-06-18', '2025-03-26', '2024-11-05'];
19
20
  const isObj = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
@@ -72,10 +73,14 @@ export async function readRpcReply(res, id) {
72
73
  class RpcError extends Error {
73
74
  code;
74
75
  data;
75
- constructor(code, message, data) {
76
+ transient;
77
+ constructor(code, message, data,
78
+ /** The upstream could not be reached or answered 5xx (worth queueing / retrying). */
79
+ transient = false) {
76
80
  super(message);
77
81
  this.code = code;
78
82
  this.data = data;
83
+ this.transient = transient;
79
84
  }
80
85
  }
81
86
  /** Minimal Streamable HTTP MCP client for one upstream. */
@@ -145,35 +150,180 @@ export class EdgeUpstream {
145
150
  return this.request(method, params, true);
146
151
  }
147
152
  if (!res.ok)
148
- throw new RpcError(-32000, `Upstream "${this.cfg.id}" HTTP ${res.status}`);
153
+ throw new RpcError(-32000, `Upstream "${this.cfg.id}" HTTP ${res.status}`, undefined, res.status >= 500);
149
154
  const reply = await readRpcReply(res, id);
150
155
  if (isObj(reply.error))
151
156
  throw new RpcError(Number(reply.error.code ?? -32603), String(reply.error.message ?? 'error'), reply.error.data);
152
157
  return reply.result;
153
158
  }
159
+ /** True while the last tools/list failed and tools come from the stale cache or the snapshot catalog. */
160
+ offline = false;
154
161
  async tools(maxAgeMs = 30_000) {
155
162
  if (this.toolsCache && Date.now() - this.toolsCache.at < maxAgeMs)
156
163
  return this.toolsCache.tools;
157
164
  const all = [];
158
165
  let cursor;
159
- for (let page = 0; page < 50; page++) {
160
- const r = (await this.request('tools/list', cursor ? { cursor } : undefined));
161
- all.push(...(r?.tools ?? []));
162
- cursor = r?.nextCursor;
163
- if (!cursor)
164
- break;
166
+ try {
167
+ for (let page = 0; page < 50; page++) {
168
+ const r = (await this.request('tools/list', cursor ? { cursor } : undefined));
169
+ all.push(...(r?.tools ?? []));
170
+ cursor = r?.nextCursor;
171
+ if (!cursor)
172
+ break;
173
+ }
165
174
  }
175
+ catch (err) {
176
+ // Offline: keep serving the last known tool list (or the control-plane catalog).
177
+ const known = this.toolsCache?.tools ?? this.cfg.catalog?.filter((t) => allowed(t.name, this.cfg.tools));
178
+ if (known?.length && isTransient(err)) {
179
+ this.offline = true;
180
+ return known;
181
+ }
182
+ throw err;
183
+ }
184
+ this.offline = false;
166
185
  const tools = all.filter((t) => allowed(t.name, this.cfg.tools));
167
186
  this.toolsCache = { at: Date.now(), tools };
168
187
  return tools;
169
188
  }
189
+ /** Carry the session and tool cache over to a replacement with the same endpoint. */
190
+ adopt(prev) {
191
+ this.session = prev.session;
192
+ this.seq = prev.seq;
193
+ this.toolsCache = prev.toolsCache;
194
+ }
195
+ }
196
+ /** Network failure, timeout, or upstream 5xx: the call may succeed later. */
197
+ function isTransient(err) {
198
+ return err instanceof RpcError ? err.transient : true;
170
199
  }
171
200
  /** Build the edge gateway's Fetch handler. */
172
201
  export function createEdgeGateway(config) {
173
202
  const fetchImpl = config.fetch ?? ((...a) => fetch(...a));
174
203
  const upstreams = new Map(config.servers.map((s) => [s.id, new EdgeUpstream(s, fetchImpl)]));
175
204
  const mcpPath = config.mcpPath ?? '/mcp';
176
- const keys = (config.apiKeys ?? []).filter(Boolean);
205
+ let keys = (config.apiKeys ?? []).filter(Boolean);
206
+ let toolNaming = config.toolNaming;
207
+ let corsOrigins = config.corsOrigins;
208
+ const sync = config.sync instanceof EdgeSync ? config.sync : config.sync ? new EdgeSync({ fetch: config.fetch, ...config.sync }) : undefined;
209
+ const queueRes = (config.offline?.queueTools ?? []).map((g) => globToRegExp(g));
210
+ const syncInterval = config.syncIntervalMs ?? 60_000;
211
+ let appliedEtag;
212
+ let lastSync = 0;
213
+ let lastReport;
214
+ let syncing;
215
+ let booted;
216
+ /** Merge a control-plane snapshot with the local config (local servers / headers / settings win). */
217
+ function applySnapshot(snap) {
218
+ if (snap.etag === appliedEtag)
219
+ return;
220
+ appliedEtag = snap.etag;
221
+ const local = new Map(config.servers.map((s) => [s.id, s]));
222
+ const merged = snap.config.servers.map((s) => {
223
+ const l = local.get(s.id);
224
+ return l ? { ...s, ...l, headers: { ...(s.headers ?? {}), ...(l.headers ?? {}) }, catalog: l.catalog ?? s.catalog } : s;
225
+ });
226
+ for (const l of config.servers)
227
+ if (!merged.some((m) => m.id === l.id))
228
+ merged.push(l);
229
+ const next = new Map();
230
+ for (const s of merged) {
231
+ const u = new EdgeUpstream(s, fetchImpl);
232
+ const prev = upstreams.get(s.id);
233
+ if (prev && prev.cfg.url === s.url && JSON.stringify(prev.cfg.headers ?? {}) === JSON.stringify(s.headers ?? {}))
234
+ u.adopt(prev);
235
+ next.set(s.id, u);
236
+ }
237
+ upstreams.clear();
238
+ for (const [k, v] of next)
239
+ upstreams.set(k, v);
240
+ keys = [...new Set([...(config.apiKeys ?? []), ...(snap.config.apiKeys ?? [])].filter(Boolean))];
241
+ toolNaming = config.toolNaming ?? snap.config.toolNaming;
242
+ corsOrigins = config.corsOrigins ?? snap.config.corsOrigins;
243
+ }
244
+ /** First request: apply the cached snapshot, or pull one when nothing is cached. */
245
+ function boot() {
246
+ if (!sync)
247
+ return Promise.resolve();
248
+ booted ??= (async () => {
249
+ const cached = await sync.cached();
250
+ if (cached)
251
+ applySnapshot(cached);
252
+ else
253
+ await runSync();
254
+ })().catch(() => undefined);
255
+ return booted;
256
+ }
257
+ async function runSync() {
258
+ if (!sync)
259
+ return { config: 'disabled', replayed: 0, failed: 0, pending: 0, pushed: 0, offline: false, errors: [] };
260
+ syncing ??= (async () => {
261
+ const errors = [];
262
+ const pulled = await sync.pull();
263
+ if (pulled.snapshot)
264
+ applySnapshot(pulled.snapshot);
265
+ if (pulled.error)
266
+ errors.push(`pull: ${pulled.error}`);
267
+ // Replay queued calls (oldest first); stop at the first upstream that is still unreachable.
268
+ let replayed = 0;
269
+ let failed = 0;
270
+ const done = [];
271
+ const retry = [];
272
+ const down = new Set();
273
+ for (const q of await sync.queued()) {
274
+ if (down.has(q.server)) {
275
+ retry.push(q.id);
276
+ continue;
277
+ }
278
+ const started = Date.now();
279
+ try {
280
+ const u = upstreams.get(q.server);
281
+ if (!u)
282
+ throw new RpcError(-32602, `Unknown server: ${q.server}`);
283
+ await u.request('tools/call', { name: q.tool, arguments: q.arguments });
284
+ replayed++;
285
+ done.push(q.id);
286
+ await sync.record({ ts: new Date().toISOString(), server: q.server, tool: q.tool, durationMs: Date.now() - started, ok: true, mode: 'replayed' });
287
+ }
288
+ catch (err) {
289
+ if (isTransient(err)) {
290
+ down.add(q.server);
291
+ retry.push(q.id);
292
+ }
293
+ else {
294
+ failed++;
295
+ done.push(q.id);
296
+ const msg = err instanceof Error ? err.message : String(err);
297
+ await sync.record({ ts: new Date().toISOString(), server: q.server, tool: q.tool, durationMs: Date.now() - started, ok: false, mode: 'replayed', error: msg });
298
+ }
299
+ }
300
+ }
301
+ if (done.length || retry.length)
302
+ await sync.settle(done, retry);
303
+ const pushed = await sync.push();
304
+ if (pushed.error)
305
+ errors.push(`push: ${pushed.error}`);
306
+ lastSync = Date.now();
307
+ lastReport = {
308
+ config: pulled.status,
309
+ etag: pulled.snapshot?.etag,
310
+ replayed,
311
+ failed,
312
+ pending: retry.length,
313
+ pushed: pushed.sent,
314
+ offline: pulled.status === 'offline' || pulled.status === 'none' || pushed.offline,
315
+ errors,
316
+ };
317
+ return lastReport;
318
+ })().finally(() => {
319
+ syncing = undefined;
320
+ });
321
+ return syncing;
322
+ }
323
+ function record(ev) {
324
+ if (sync)
325
+ void sync.record(ev).catch(() => undefined);
326
+ }
177
327
  const json = (status, body, extra = {}) => new Response(JSON.stringify(body), { status, headers: { 'content-type': 'application/json', ...extra } });
178
328
  async function authorized(req) {
179
329
  if (keys.length === 0)
@@ -202,7 +352,7 @@ export function createEdgeGateway(config) {
202
352
  count.set(t.tool.name, (count.get(t.tool.name) ?? 0) + 1);
203
353
  const list = flat.map((t) => ({
204
354
  ...t,
205
- exposed: config.toolNaming === 'prefix' || (count.get(t.tool.name) ?? 0) > 1 ? `${t.serverId}__${t.tool.name}` : t.tool.name,
355
+ exposed: toolNaming === 'prefix' || (count.get(t.tool.name) ?? 0) > 1 ? `${t.serverId}__${t.tool.name}` : t.tool.name,
206
356
  }));
207
357
  return { list, errors };
208
358
  }
@@ -211,12 +361,36 @@ export function createEdgeGateway(config) {
211
361
  const hit = server ? list.find((t) => t.serverId === server && t.tool.name === name) : list.find((t) => t.exposed === name) ?? list.find((t) => t.tool.name === name);
212
362
  if (!hit)
213
363
  throw new RpcError(-32602, `Unknown tool: ${name}`);
214
- const result = await upstreams.get(hit.serverId).request('tools/call', { name: hit.tool.name, arguments: args });
215
- return { serverId: hit.serverId, result };
364
+ const started = Date.now();
365
+ const ev = { server: hit.serverId, tool: hit.tool.name };
366
+ try {
367
+ const result = await upstreams.get(hit.serverId).request('tools/call', { name: hit.tool.name, arguments: args });
368
+ record({ ts: new Date().toISOString(), ...ev, durationMs: Date.now() - started, ok: true, mode: 'live' });
369
+ return { serverId: hit.serverId, result };
370
+ }
371
+ catch (err) {
372
+ const msg = err instanceof Error ? err.message : String(err);
373
+ const queueable = sync && isTransient(err) && queueRes.some((re) => re.test(hit.exposed) || re.test(`${hit.serverId}__${hit.tool.name}`));
374
+ if (queueable) {
375
+ const q = await sync.enqueue({ server: hit.serverId, tool: hit.tool.name, arguments: args });
376
+ record({ ts: new Date().toISOString(), ...ev, durationMs: Date.now() - started, ok: true, mode: 'queued', error: msg });
377
+ return {
378
+ serverId: hit.serverId,
379
+ queued: q.id,
380
+ result: {
381
+ content: [{ type: 'text', text: `Upstream "${hit.serverId}" is unreachable; the call was queued for delivery (${q.id}).` }],
382
+ structuredContent: { queued: true, id: q.id },
383
+ _meta: { 'mcp-gateway/queued': q.id },
384
+ },
385
+ };
386
+ }
387
+ record({ ts: new Date().toISOString(), ...ev, durationMs: Date.now() - started, ok: false, mode: 'live', error: msg });
388
+ throw err;
389
+ }
216
390
  }
217
391
  function cors(req, res) {
218
392
  const origin = req.headers.get('origin');
219
- const allowedOrigins = config.corsOrigins ?? [];
393
+ const allowedOrigins = corsOrigins ?? [];
220
394
  if (origin && (allowedOrigins.includes('*') || allowedOrigins.includes(origin))) {
221
395
  res.headers.set('access-control-allow-origin', allowedOrigins.includes('*') ? '*' : origin);
222
396
  res.headers.set('access-control-allow-headers', 'authorization, content-type, mcp-session-id, mcp-protocol-version, x-api-key');
@@ -322,18 +496,42 @@ export function createEdgeGateway(config) {
322
496
  const started = Date.now();
323
497
  try {
324
498
  const r = await callTool(b.tool, isObj(b.arguments) ? b.arguments : {}, typeof b.server === 'string' ? b.server : undefined);
325
- return json(200, { result: r.result, server: r.serverId, tool: b.tool, durationMs: Date.now() - started });
499
+ return json(r.queued ? 202 : 200, { result: r.result, server: r.serverId, tool: b.tool, durationMs: Date.now() - started, ...(r.queued ? { queued: r.queued } : {}) });
326
500
  }
327
501
  catch (err) {
328
502
  const e = err instanceof RpcError ? err : new RpcError(-32603, err instanceof Error ? err.message : String(err));
329
503
  return json(e.code === -32602 ? 404 : 502, { error: e.code === -32602 ? 'Not Found' : 'Bad Gateway', message: e.message, code: e.code });
330
504
  }
331
505
  }
506
+ if (path === '/api/v1/edge/status' && req.method === 'GET') {
507
+ return json(200, {
508
+ sync: !!sync,
509
+ edgeId: sync ? await sync.id() : undefined,
510
+ etag: appliedEtag ?? null,
511
+ lastSync: lastSync ? new Date(lastSync).toISOString() : null,
512
+ lastReport: lastReport ?? null,
513
+ outbox: sync ? (await sync.outbox()).length : 0,
514
+ queued: sync ? (await sync.queued()).length : 0,
515
+ offlineServers: [...upstreams.values()].filter((u) => u.offline).map((u) => u.cfg.id),
516
+ });
517
+ }
518
+ if (path === '/api/v1/edge/sync' && req.method === 'POST') {
519
+ return json(sync ? 200 : 404, sync ? await runSync() : { error: 'Not Found', message: 'sync is not configured' });
520
+ }
332
521
  return json(404, { error: 'Not Found' });
333
522
  }
334
523
  return {
335
524
  upstreams,
336
- async fetch(request) {
525
+ syncClient: sync,
526
+ sync: runSync,
527
+ async fetch(request, waitUntil) {
528
+ await boot();
529
+ if (sync && syncInterval > 0 && !syncing && Date.now() - lastSync >= syncInterval) {
530
+ // Mark now so concurrent requests do not each start a sync.
531
+ lastSync = Date.now();
532
+ const p = runSync().catch(() => undefined);
533
+ waitUntil?.(p);
534
+ }
337
535
  return cors(request, await route(request));
338
536
  },
339
537
  };