@ggui-ai/mcp-server 0.2.0-alpha.4 → 0.4.0-rc.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/dist/admin-blueprints-transport.d.ts.map +1 -1
- package/dist/admin-blueprints-transport.js +2 -1
- package/dist/admin-oauth-providers-transport.d.ts.map +1 -1
- package/dist/admin-oauth-providers-transport.js +7 -5
- package/dist/api-renders-routes.d.ts +85 -0
- package/dist/api-renders-routes.d.ts.map +1 -0
- package/dist/api-renders-routes.js +372 -0
- package/dist/build-mcp.d.ts +2 -2
- package/dist/build-mcp.d.ts.map +1 -1
- package/dist/build-mcp.js +64 -5
- package/dist/code-routes.d.ts +47 -0
- package/dist/code-routes.d.ts.map +1 -0
- package/dist/code-routes.js +81 -0
- package/dist/code-store-fs.js +2 -2
- package/dist/console-auth.d.ts +10 -10
- package/dist/console-auth.d.ts.map +1 -1
- package/dist/console-auth.js +5 -5
- package/dist/console-blueprint-routes.d.ts +71 -0
- package/dist/console-blueprint-routes.d.ts.map +1 -0
- package/dist/console-blueprint-routes.js +348 -0
- package/dist/console-chat-routes.d.ts +80 -0
- package/dist/console-chat-routes.d.ts.map +1 -0
- package/dist/console-chat-routes.js +204 -0
- package/dist/console-config-routes.d.ts +37 -0
- package/dist/console-config-routes.d.ts.map +1 -0
- package/dist/console-config-routes.js +91 -0
- package/dist/console-headers.d.ts +1 -1
- package/dist/console-info-routes.d.ts +84 -0
- package/dist/console-info-routes.d.ts.map +1 -0
- package/dist/console-info-routes.js +135 -0
- package/dist/console-keys-routes.d.ts +50 -0
- package/dist/console-keys-routes.d.ts.map +1 -0
- package/dist/console-keys-routes.js +222 -0
- package/dist/console-llm-keys-routes.d.ts +47 -0
- package/dist/console-llm-keys-routes.d.ts.map +1 -0
- package/dist/console-llm-keys-routes.js +443 -0
- package/dist/console-mcp-tools-routes.d.ts +41 -0
- package/dist/console-mcp-tools-routes.d.ts.map +1 -0
- package/dist/console-mcp-tools-routes.js +60 -0
- package/dist/console-registry-routes.d.ts +66 -0
- package/dist/console-registry-routes.d.ts.map +1 -0
- package/dist/console-registry-routes.js +276 -0
- package/dist/console-session-routes.d.ts +89 -0
- package/dist/console-session-routes.d.ts.map +1 -0
- package/dist/console-session-routes.js +385 -0
- package/dist/console-sessions-routes.d.ts +52 -0
- package/dist/console-sessions-routes.d.ts.map +1 -0
- package/dist/console-sessions-routes.js +106 -0
- package/dist/console-static-routes.d.ts +54 -0
- package/dist/console-static-routes.d.ts.map +1 -0
- package/dist/console-static-routes.js +190 -0
- package/dist/console-theme-routes.d.ts +3 -3
- package/dist/console-theme-routes.js +1 -1
- package/dist/console-timeline.d.ts +5 -5
- package/dist/console-timeline.d.ts.map +1 -1
- package/dist/console-timeline.js +27 -26
- package/dist/console-welcome.js +2 -2
- package/dist/email-login.d.ts.map +1 -1
- package/dist/email-login.js +2 -3
- package/dist/ggui-session-channel/action-ingress.d.ts +54 -0
- package/dist/ggui-session-channel/action-ingress.d.ts.map +1 -0
- package/dist/ggui-session-channel/action-ingress.js +228 -0
- package/dist/ggui-session-channel/channel-subscriptions.d.ts +97 -0
- package/dist/ggui-session-channel/channel-subscriptions.d.ts.map +1 -0
- package/dist/ggui-session-channel/channel-subscriptions.js +224 -0
- package/dist/ggui-session-channel/internal-types.d.ts +102 -0
- package/dist/ggui-session-channel/internal-types.d.ts.map +1 -0
- package/dist/ggui-session-channel/internal-types.js +6 -0
- package/dist/ggui-session-channel/outbound.d.ts +81 -0
- package/dist/ggui-session-channel/outbound.d.ts.map +1 -0
- package/dist/ggui-session-channel/outbound.js +174 -0
- package/dist/ggui-session-channel/socket-router.d.ts +38 -0
- package/dist/ggui-session-channel/socket-router.d.ts.map +1 -0
- package/dist/ggui-session-channel/socket-router.js +213 -0
- package/dist/ggui-session-channel/subscribe.d.ts +165 -0
- package/dist/ggui-session-channel/subscribe.d.ts.map +1 -0
- package/dist/ggui-session-channel/subscribe.js +370 -0
- package/dist/ggui-session-channel/subscriber-lifecycle.d.ts +40 -0
- package/dist/ggui-session-channel/subscriber-lifecycle.d.ts.map +1 -0
- package/dist/ggui-session-channel/subscriber-lifecycle.js +123 -0
- package/dist/ggui-session-channel.d.ts +425 -0
- package/dist/ggui-session-channel.d.ts.map +1 -0
- package/dist/ggui-session-channel.js +262 -0
- package/dist/health-routes.d.ts +76 -0
- package/dist/health-routes.d.ts.map +1 -0
- package/dist/health-routes.js +145 -0
- package/dist/index.d.ts +10 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -9
- package/dist/instructions-presets.d.ts +3 -3
- package/dist/instructions-presets.js +24 -24
- package/dist/llm-backed-negotiator.d.ts +68 -67
- package/dist/llm-backed-negotiator.d.ts.map +1 -1
- package/dist/llm-backed-negotiator.js +82 -221
- package/dist/mcp-apps-outbound.d.ts +47 -48
- package/dist/mcp-apps-outbound.d.ts.map +1 -1
- package/dist/mcp-apps-outbound.js +154 -177
- package/dist/mcp-endpoint-routes.d.ts +88 -0
- package/dist/mcp-endpoint-routes.d.ts.map +1 -0
- package/dist/mcp-endpoint-routes.js +359 -0
- package/dist/mcp-mounts.d.ts +2 -76
- package/dist/mcp-mounts.d.ts.map +1 -1
- package/dist/mcp-mounts.js +0 -76
- package/dist/oauth-as-routes.d.ts +60 -0
- package/dist/oauth-as-routes.d.ts.map +1 -0
- package/dist/oauth-as-routes.js +82 -0
- package/dist/oauth-clients-routes.d.ts +39 -0
- package/dist/oauth-clients-routes.d.ts.map +1 -0
- package/dist/oauth-clients-routes.js +87 -0
- package/dist/oauth-login-types.d.ts +1 -20
- package/dist/oauth-login-types.d.ts.map +1 -1
- package/dist/oauth-login-types.js +30 -7
- package/dist/oauth-login.d.ts.map +1 -1
- package/dist/oauth-login.js +3 -2
- package/dist/oauth-providers-store.d.ts.map +1 -1
- package/dist/oauth-providers-store.js +5 -5
- package/dist/oauth.d.ts +9 -8
- package/dist/oauth.d.ts.map +1 -1
- package/dist/oauth.js +41 -19
- package/dist/pairing-transport.d.ts.map +1 -1
- package/dist/pairing-transport.js +2 -1
- package/dist/request-context.d.ts +2 -2
- package/dist/request-context.js +2 -2
- package/dist/reserved-validators.d.ts.map +1 -1
- package/dist/reserved-validators.js +9 -1
- package/dist/route-param.d.ts +9 -0
- package/dist/route-param.d.ts.map +1 -0
- package/dist/route-param.js +10 -0
- package/dist/runtime-bundle-route.d.ts +43 -0
- package/dist/runtime-bundle-route.d.ts.map +1 -0
- package/dist/runtime-bundle-route.js +80 -0
- package/dist/schema-compat.d.ts +64 -62
- package/dist/schema-compat.d.ts.map +1 -1
- package/dist/schema-compat.js +23 -51
- package/dist/server.d.ts +179 -193
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +644 -3759
- package/dist/storage.d.ts +5 -5
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +5 -5
- package/dist/thread-transport.d.ts.map +1 -1
- package/dist/thread-transport.js +4 -3
- package/dist/user-session-auth.d.ts +7 -21
- package/dist/user-session-auth.d.ts.map +1 -1
- package/dist/user-session-auth.js +7 -28
- package/package.json +16 -15
- package/dist/mcp-apps-inbound.d.ts +0 -86
- package/dist/mcp-apps-inbound.d.ts.map +0 -1
- package/dist/mcp-apps-inbound.js +0 -283
- package/dist/render-channel.d.ts +0 -694
- package/dist/render-channel.d.ts.map +0 -1
- package/dist/render-channel.js +0 -1775
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP wire endpoints — the audience-routed JSON-RPC surfaces.
|
|
3
|
+
*
|
|
4
|
+
* POST <universalMcpPath> — agent+runtime tools (default `/mcp`)
|
|
5
|
+
* POST <pathPrefix>/:appId — per-tenant variant (opt-in via
|
|
6
|
+
* `perAppRouting`)
|
|
7
|
+
* POST /protocol — design-time spec/discovery tools
|
|
8
|
+
* POST /ops — operator-class management tools
|
|
9
|
+
* POST <service.path> — isolated MCP services (path IS
|
|
10
|
+
* the audience)
|
|
11
|
+
* GET/DELETE on each — 405 (stateless server; no
|
|
12
|
+
* streaming continuation /
|
|
13
|
+
* session-terminate verbs)
|
|
14
|
+
*
|
|
15
|
+
* Every route shares ONE request pipeline (`makeMcpHandler`): resolve
|
|
16
|
+
* identity via the AuthAdapter (anonymous services synthesize a
|
|
17
|
+
* builder identity on missing/invalid bearers), apply the per-app
|
|
18
|
+
* authorize hook, build a fresh `McpServer` + Streamable HTTP
|
|
19
|
+
* transport per request (stateless), and dispatch under the
|
|
20
|
+
* AsyncLocalStorage-scoped `HandlerContext`. The difference between
|
|
21
|
+
* routes is ONLY the handler set each exposes.
|
|
22
|
+
*
|
|
23
|
+
* See `docs/development/audience-routes.md` for the audience taxonomy
|
|
24
|
+
* (`agent` / `runtime` / `protocol` / `ops`) and the wire-name prefix
|
|
25
|
+
* rules.
|
|
26
|
+
*/
|
|
27
|
+
import type { AuthAdapter, AuthResult } from "@ggui-ai/mcp-server-core";
|
|
28
|
+
import type { HandlerContext, SharedHandler } from "@ggui-ai/mcp-server-handlers";
|
|
29
|
+
import type { Express } from "express";
|
|
30
|
+
import type { AsyncLocalStorage } from "node:async_hooks";
|
|
31
|
+
import type { ZodRawShape } from "zod";
|
|
32
|
+
import { type BuildMcpServerOptions, type ServerInfo } from "./build-mcp.js";
|
|
33
|
+
import type { Logger } from "./logger.js";
|
|
34
|
+
import type { McpService } from "./mcp-mounts.js";
|
|
35
|
+
/** Per-tenant URL routing shape — mirrors `CreateGguiServerOptions.perAppRouting`. */
|
|
36
|
+
interface PerAppRouting {
|
|
37
|
+
readonly paramName: string;
|
|
38
|
+
readonly paramPattern: string;
|
|
39
|
+
readonly pathPrefix?: string;
|
|
40
|
+
readonly authorize?: (urlAppId: string, identity: AuthResult) => Promise<void>;
|
|
41
|
+
}
|
|
42
|
+
interface MountOptions {
|
|
43
|
+
/** Express app to mount onto. */
|
|
44
|
+
readonly app: Express;
|
|
45
|
+
/** Structured logger; per-request children carry `requestId`. */
|
|
46
|
+
readonly logger: Logger;
|
|
47
|
+
/** Auth adapter every route resolves bearers against. */
|
|
48
|
+
readonly auth: AuthAdapter;
|
|
49
|
+
/** Server identity forwarded to every per-request `buildMcpServer`. */
|
|
50
|
+
readonly info: ServerInfo;
|
|
51
|
+
/** Full composed handler list (audience filtering happens here). */
|
|
52
|
+
readonly handlers: ReadonlyArray<SharedHandler<ZodRawShape, ZodRawShape>>;
|
|
53
|
+
/** Validated isolated-service list (`validateMcpServices` output). */
|
|
54
|
+
readonly mcpServices: ReadonlyArray<McpService>;
|
|
55
|
+
/** Request-scoped HandlerContext storage shared with the handlers. */
|
|
56
|
+
readonly als: AsyncLocalStorage<HandlerContext>;
|
|
57
|
+
/** Identity → appId resolution rule (SPEC §12.2). */
|
|
58
|
+
readonly appIdFromIdentity: (result: AuthResult) => string;
|
|
59
|
+
/** Universal endpoint path (default `/mcp`; cloud overrides to `/`). */
|
|
60
|
+
readonly universalMcpPath: string;
|
|
61
|
+
/** Per-tenant endpoint shape — absent = universal-only deployment. */
|
|
62
|
+
readonly perAppRouting?: PerAppRouting;
|
|
63
|
+
/** Whether OAuth is enabled (adds `WWW-Authenticate` on 401). */
|
|
64
|
+
readonly oauthEnabled: boolean;
|
|
65
|
+
/** Operator-configured issuer URL override (OAuth). */
|
|
66
|
+
readonly oauthIssuerUrl?: string;
|
|
67
|
+
/** Operator-supplied error → HTTP/JSON-RPC mapping hook. */
|
|
68
|
+
readonly errorMapper?: (err: unknown) => {
|
|
69
|
+
readonly status: number;
|
|
70
|
+
readonly code: number;
|
|
71
|
+
readonly message: string;
|
|
72
|
+
} | undefined;
|
|
73
|
+
/**
|
|
74
|
+
* Per-boot `buildMcpServer` options. Assembled once by the composer
|
|
75
|
+
* (every input is fixed at composition time); the handler spreads a
|
|
76
|
+
* fresh object per request so the builder never sees a shared
|
|
77
|
+
* mutable reference.
|
|
78
|
+
*/
|
|
79
|
+
readonly buildMcpOptions: BuildMcpServerOptions;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Mount the universal / per-app / protocol / ops / service MCP
|
|
83
|
+
* endpoints onto the express app. Returns nothing — the routes
|
|
84
|
+
* self-register.
|
|
85
|
+
*/
|
|
86
|
+
export declare function mountMcpEndpoints(opts: MountOptions): void;
|
|
87
|
+
export {};
|
|
88
|
+
//# sourceMappingURL=mcp-endpoint-routes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcp-endpoint-routes.d.ts","sourceRoot":"","sources":["../src/mcp-endpoint-routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AACxE,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAElF,OAAO,KAAK,EAAE,OAAO,EAAqB,MAAM,SAAS,CAAC;AAC1D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAE1D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,KAAK,CAAC;AAEvC,OAAO,EAAkB,KAAK,qBAAqB,EAAE,KAAK,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAC7F,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAGlD,sFAAsF;AACtF,UAAU,aAAa;IACrB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,UAAU,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAChF;AAED,UAAU,YAAY;IACpB,iCAAiC;IACjC,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB,iEAAiE;IACjE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,yDAAyD;IACzD,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAC3B,uEAAuE;IACvE,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B,oEAAoE;IACpE,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC,aAAa,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC,CAAC;IAC1E,sEAAsE;IACtE,QAAQ,CAAC,WAAW,EAAE,aAAa,CAAC,UAAU,CAAC,CAAC;IAChD,sEAAsE;IACtE,QAAQ,CAAC,GAAG,EAAE,iBAAiB,CAAC,cAAc,CAAC,CAAC;IAChD,qDAAqD;IACrD,QAAQ,CAAC,iBAAiB,EAAE,CAAC,MAAM,EAAE,UAAU,KAAK,MAAM,CAAC;IAC3D,wEAAwE;IACxE,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,sEAAsE;IACtE,QAAQ,CAAC,aAAa,CAAC,EAAE,aAAa,CAAC;IACvC,iEAAiE;IACjE,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;IAC/B,uDAAuD;IACvD,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC,4DAA4D;IAC5D,QAAQ,CAAC,WAAW,CAAC,EAAE,CACrB,GAAG,EAAE,OAAO,KACT;QAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS,CAAC;IAC9F;;;;;OAKG;IACH,QAAQ,CAAC,eAAe,EAAE,qBAAqB,CAAC;CACjD;AAyBD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CA4V1D"}
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP wire endpoints — the audience-routed JSON-RPC surfaces.
|
|
3
|
+
*
|
|
4
|
+
* POST <universalMcpPath> — agent+runtime tools (default `/mcp`)
|
|
5
|
+
* POST <pathPrefix>/:appId — per-tenant variant (opt-in via
|
|
6
|
+
* `perAppRouting`)
|
|
7
|
+
* POST /protocol — design-time spec/discovery tools
|
|
8
|
+
* POST /ops — operator-class management tools
|
|
9
|
+
* POST <service.path> — isolated MCP services (path IS
|
|
10
|
+
* the audience)
|
|
11
|
+
* GET/DELETE on each — 405 (stateless server; no
|
|
12
|
+
* streaming continuation /
|
|
13
|
+
* session-terminate verbs)
|
|
14
|
+
*
|
|
15
|
+
* Every route shares ONE request pipeline (`makeMcpHandler`): resolve
|
|
16
|
+
* identity via the AuthAdapter (anonymous services synthesize a
|
|
17
|
+
* builder identity on missing/invalid bearers), apply the per-app
|
|
18
|
+
* authorize hook, build a fresh `McpServer` + Streamable HTTP
|
|
19
|
+
* transport per request (stateless), and dispatch under the
|
|
20
|
+
* AsyncLocalStorage-scoped `HandlerContext`. The difference between
|
|
21
|
+
* routes is ONLY the handler set each exposes.
|
|
22
|
+
*
|
|
23
|
+
* See `docs/development/audience-routes.md` for the audience taxonomy
|
|
24
|
+
* (`agent` / `runtime` / `protocol` / `ops`) and the wire-name prefix
|
|
25
|
+
* rules.
|
|
26
|
+
*/
|
|
27
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
28
|
+
import { randomUUID } from "node:crypto";
|
|
29
|
+
import { resolveIdentity, UnauthenticatedError } from "./auth.js";
|
|
30
|
+
import { buildMcpServer } from "./build-mcp.js";
|
|
31
|
+
import { buildWwwAuthenticate, resolveIssuerUrl } from "./oauth.js";
|
|
32
|
+
/**
|
|
33
|
+
* Resolve the resource path that `WWW-Authenticate` should point at
|
|
34
|
+
* for the current request. Per-app `/mcp` requests
|
|
35
|
+
* get `${pathPrefix}/${appId}` so RFC 9728 discovery resolves to the
|
|
36
|
+
* per-app metadata; universal-route requests get `''` which collapses
|
|
37
|
+
* back to the universal `${issuer}/.well-known/oauth-protected-resource`.
|
|
38
|
+
*
|
|
39
|
+
* Defense in depth: even when `perAppRouting` is configured, we
|
|
40
|
+
* reject empty or whitespace-only `appId` values rather than emitting
|
|
41
|
+
* an obviously-wrong `${pathPrefix}//.well-known/...` URL — falling
|
|
42
|
+
* back to universal is the safer behavior.
|
|
43
|
+
*/
|
|
44
|
+
function resolveWwwAuthResourcePath(req, perAppRouting) {
|
|
45
|
+
if (perAppRouting === undefined)
|
|
46
|
+
return "";
|
|
47
|
+
const { paramName, pathPrefix = "" } = perAppRouting;
|
|
48
|
+
const appId = req.params[paramName];
|
|
49
|
+
if (typeof appId !== "string" || appId.length === 0)
|
|
50
|
+
return "";
|
|
51
|
+
return `${pathPrefix}/${appId}`;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Mount the universal / per-app / protocol / ops / service MCP
|
|
55
|
+
* endpoints onto the express app. Returns nothing — the routes
|
|
56
|
+
* self-register.
|
|
57
|
+
*/
|
|
58
|
+
export function mountMcpEndpoints(opts) {
|
|
59
|
+
const { app, logger, auth, info, handlers, mcpServices, als, appIdFromIdentity, universalMcpPath, perAppRouting, oauthEnabled, oauthIssuerUrl, errorMapper, buildMcpOptions, } = opts;
|
|
60
|
+
/**
|
|
61
|
+
* Audience filter — returns the subset of `handlers` whose
|
|
62
|
+
* `audience` tag intersects `allowed`. Read on route mounting so
|
|
63
|
+
* each MCP route exposes only its audience's tools.
|
|
64
|
+
*
|
|
65
|
+
* Handlers with `audience: undefined` default to ['agent'] — every
|
|
66
|
+
* such handler is agent-runtime callable.
|
|
67
|
+
*/
|
|
68
|
+
const filterHandlersByAudience = (set, allowed) => set.filter((h) => {
|
|
69
|
+
const tags = h.audience ?? ["agent"];
|
|
70
|
+
return tags.some((t) => allowed.includes(t));
|
|
71
|
+
});
|
|
72
|
+
const makeMcpHandler = (routeHandlers, handlerOpts) => async (req, res) => {
|
|
73
|
+
const requestId = typeof req.headers["x-request-id"] === "string"
|
|
74
|
+
? req.headers["x-request-id"]
|
|
75
|
+
: randomUUID();
|
|
76
|
+
const reqLogger = logger.child({ requestId });
|
|
77
|
+
// Auth is OPTIONAL on anonymous services and REQUIRED otherwise.
|
|
78
|
+
// Always attempt to resolve a presented credential: an anonymous
|
|
79
|
+
// service with a valid bearer still resolves to the real identity
|
|
80
|
+
// (so it can offer authenticated capabilities — e.g. `/dev`'s ops
|
|
81
|
+
// tools read `ctx.userId`), while a missing/unauthenticated
|
|
82
|
+
// credential falls back to the synthesized anonymous builder so
|
|
83
|
+
// public reads (docs, protocol) work bearer-less. This is what makes
|
|
84
|
+
// `source: 'anonymous'` distinguishable from an authenticated caller,
|
|
85
|
+
// per the `McpService.anonymous` contract — resolving a present
|
|
86
|
+
// bearer is the only way a handler can tell the two apart.
|
|
87
|
+
let identity;
|
|
88
|
+
try {
|
|
89
|
+
identity = await resolveIdentity(auth, req);
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
if (err instanceof UnauthenticatedError) {
|
|
93
|
+
if (handlerOpts?.anonymous) {
|
|
94
|
+
identity = { identity: { kind: "builder" }, source: "anonymous" };
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
// Diagnostic: did a Bearer credential actually reach the pod on
|
|
98
|
+
// this request? This splits "the client/transport never sent one"
|
|
99
|
+
// from "we received it but the adapter rejected it" — otherwise
|
|
100
|
+
// indistinguishable in `auth_failed`. Only a short, non-secret
|
|
101
|
+
// prefix is logged (a JWT header is public), never the credential.
|
|
102
|
+
const rawAuth = req.headers["authorization"];
|
|
103
|
+
const authHeaderPresent = typeof rawAuth === "string" && rawAuth.length > 0;
|
|
104
|
+
reqLogger.warn("auth_failed", {
|
|
105
|
+
reason: err.message,
|
|
106
|
+
authHeaderPresent,
|
|
107
|
+
authHeaderPrefix: authHeaderPresent ? rawAuth.slice(0, 12) : null,
|
|
108
|
+
// Bearer-stripped token length — a transit-truncation check
|
|
109
|
+
// (compare against the minted length); never the credential.
|
|
110
|
+
tokenLen: authHeaderPresent
|
|
111
|
+
? rawAuth.replace(/^Bearer\s+/i, "").length
|
|
112
|
+
: 0,
|
|
113
|
+
path: req.path,
|
|
114
|
+
});
|
|
115
|
+
// OAuth-discovery clients (Claude Desktop, claude.ai, etc.)
|
|
116
|
+
// read this header to find the resource-metadata URL and
|
|
117
|
+
// begin the OAuth dance. Pure-bearer clients ignore it.
|
|
118
|
+
//
|
|
119
|
+
// Per-app routes point at the per-app resource-metadata
|
|
120
|
+
// document so RFC 9728 discovery resolves
|
|
121
|
+
// to a per-app `resource` URL. Universal routes keep the
|
|
122
|
+
// bare metadata path.
|
|
123
|
+
if (oauthEnabled) {
|
|
124
|
+
const wwwAuthResourcePath = resolveWwwAuthResourcePath(req, perAppRouting);
|
|
125
|
+
res.setHeader("WWW-Authenticate", buildWwwAuthenticate(resolveIssuerUrl(req, oauthIssuerUrl), wwwAuthResourcePath));
|
|
126
|
+
}
|
|
127
|
+
res.status(401).json({
|
|
128
|
+
jsonrpc: "2.0",
|
|
129
|
+
error: { code: -32000, message: err.message },
|
|
130
|
+
id: null,
|
|
131
|
+
});
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
reqLogger.error("auth_unexpected_error", { error: String(err) });
|
|
137
|
+
res.status(500).json({
|
|
138
|
+
jsonrpc: "2.0",
|
|
139
|
+
error: { code: -32603, message: "Internal server error" },
|
|
140
|
+
id: null,
|
|
141
|
+
});
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// Operator-surface guard: federated end-user identities (source:'oidc',
|
|
146
|
+
// minted by the OIDC verify adapter) must never reach operator-class
|
|
147
|
+
// routes (/ops) or design-time spec routes (/protocol). Audience
|
|
148
|
+
// filtering only shapes tools/list; it does NOT stop a direct
|
|
149
|
+
// tools/call, so this is a route-level authorization gate.
|
|
150
|
+
if (handlerOpts?.rejectFederated && identity.source === "oidc") {
|
|
151
|
+
reqLogger.warn("federated_identity_rejected", { route: req.path });
|
|
152
|
+
res.status(403).json({
|
|
153
|
+
jsonrpc: "2.0",
|
|
154
|
+
error: { code: -32000, message: "federated identities are not permitted on this route" },
|
|
155
|
+
id: null,
|
|
156
|
+
});
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
// Per-tenant URL routing. When `perAppRouting`
|
|
160
|
+
// is configured AND the request matched the per-app path
|
|
161
|
+
// `/:${paramName}/mcp`, Express populates `req.params[paramName]`
|
|
162
|
+
// with the validated tenant id. Use it as `ctx.appId` for this
|
|
163
|
+
// request, overriding `appIdFromIdentity`. The universal `/mcp`
|
|
164
|
+
// route doesn't have the param so it falls through to the
|
|
165
|
+
// identity-based resolution.
|
|
166
|
+
const urlAppId = perAppRouting !== undefined ? req.params[perAppRouting.paramName] : undefined;
|
|
167
|
+
const hasUrlAppId = typeof urlAppId === "string" && urlAppId.length > 0;
|
|
168
|
+
// Per-app authorize hook — when the deployment configured
|
|
169
|
+
// `perAppRouting.authorize` AND the request matched the per-app
|
|
170
|
+
// path, invoke the callback. Throwing collapses to a 403 before
|
|
171
|
+
// the MCP handler ever sees the request, which is the boundary
|
|
172
|
+
// that prevents cross-user blueprint reads when pod tools bypass
|
|
173
|
+
// AppSync owner-auth via raw DDB. Universal-endpoint requests
|
|
174
|
+
// skip this entirely (no urlAppId).
|
|
175
|
+
if (hasUrlAppId && perAppRouting?.authorize) {
|
|
176
|
+
try {
|
|
177
|
+
await perAppRouting.authorize(urlAppId, identity);
|
|
178
|
+
}
|
|
179
|
+
catch (err) {
|
|
180
|
+
reqLogger.warn("per_app_authorize_denied", {
|
|
181
|
+
urlAppId,
|
|
182
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
183
|
+
});
|
|
184
|
+
res.status(403).json({
|
|
185
|
+
jsonrpc: "2.0",
|
|
186
|
+
error: { code: -32000, message: "Forbidden" },
|
|
187
|
+
id: null,
|
|
188
|
+
});
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
const ctx = {
|
|
193
|
+
appId: hasUrlAppId ? urlAppId : appIdFromIdentity(identity),
|
|
194
|
+
requestId,
|
|
195
|
+
// Identity is the canonical source of two mutually-exclusive
|
|
196
|
+
// hosted fields: `apiKeyHash` for kind=app, `userId` for kind=user.
|
|
197
|
+
// Threading them onto HandlerContext here means hosted handlers
|
|
198
|
+
// (the K8s ggui-protocol pod's billing gate + per-user blueprint
|
|
199
|
+
// scoping) can read identity directly without a parallel pod-only
|
|
200
|
+
// context shape; OSS handlers continue to ignore both fields.
|
|
201
|
+
...(identity.identity.kind === "app" ? { apiKeyHash: identity.identity.apiKeyHash } : {}),
|
|
202
|
+
...(identity.identity.kind === "user" ? { userId: identity.identity.userId } : {}),
|
|
203
|
+
};
|
|
204
|
+
reqLogger.debug?.("mcp_request", { appId: ctx.appId });
|
|
205
|
+
const mcp = buildMcpServer(info, routeHandlers, () => als.getStore() ?? ctx, reqLogger, {
|
|
206
|
+
...buildMcpOptions,
|
|
207
|
+
});
|
|
208
|
+
const transport = new StreamableHTTPServerTransport({
|
|
209
|
+
sessionIdGenerator: undefined,
|
|
210
|
+
});
|
|
211
|
+
res.on("close", () => {
|
|
212
|
+
transport.close().catch(() => undefined);
|
|
213
|
+
mcp.close().catch(() => undefined);
|
|
214
|
+
});
|
|
215
|
+
try {
|
|
216
|
+
await mcp.connect(transport);
|
|
217
|
+
await als.run(ctx, () => transport.handleRequest(req, res, req.body));
|
|
218
|
+
}
|
|
219
|
+
catch (err) {
|
|
220
|
+
reqLogger.error("mcp_handle_failed", { error: String(err) });
|
|
221
|
+
if (!res.headersSent) {
|
|
222
|
+
let mapped;
|
|
223
|
+
if (errorMapper) {
|
|
224
|
+
try {
|
|
225
|
+
mapped = errorMapper(err);
|
|
226
|
+
}
|
|
227
|
+
catch (mapperErr) {
|
|
228
|
+
// Defensive: a thrown mapper degrades to the default 500
|
|
229
|
+
// rather than letting the inner failure escape the handler.
|
|
230
|
+
reqLogger.warn("error_mapper_failed", {
|
|
231
|
+
error: String(mapperErr),
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (mapped) {
|
|
236
|
+
res.status(mapped.status).json({
|
|
237
|
+
jsonrpc: "2.0",
|
|
238
|
+
error: { code: mapped.code, message: mapped.message },
|
|
239
|
+
id: null,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
res.status(500).json({
|
|
244
|
+
jsonrpc: "2.0",
|
|
245
|
+
error: { code: -32603, message: "Internal server error" },
|
|
246
|
+
id: null,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
const methodNotAllowed = (_req, res) => {
|
|
253
|
+
res.status(405).json({
|
|
254
|
+
jsonrpc: "2.0",
|
|
255
|
+
error: {
|
|
256
|
+
code: -32000,
|
|
257
|
+
message: "Method not allowed (stateless server).",
|
|
258
|
+
},
|
|
259
|
+
id: null,
|
|
260
|
+
});
|
|
261
|
+
};
|
|
262
|
+
// Three audience-filtered handler sets. Each set is the
|
|
263
|
+
// subset of `handlers` whose `audience` tag intersects the route's
|
|
264
|
+
// allowed list. Handlers without an explicit tag default to
|
|
265
|
+
// ['agent'] — every such handler is agent-runtime callable.
|
|
266
|
+
const agentRouteHandlers = filterHandlersByAudience(handlers, ["agent", "runtime"]);
|
|
267
|
+
const protocolRouteHandlers = filterHandlersByAudience(handlers, ["protocol"]);
|
|
268
|
+
const opsRouteHandlers = filterHandlersByAudience(handlers, ["ops"]);
|
|
269
|
+
const agentMcpHandler = makeMcpHandler(agentRouteHandlers);
|
|
270
|
+
const protocolMcpHandler = makeMcpHandler(protocolRouteHandlers, { rejectFederated: true });
|
|
271
|
+
const opsMcpHandler = makeMcpHandler(opsRouteHandlers, { rejectFederated: true });
|
|
272
|
+
// Universal endpoint — `appId` resolved from the auth identity via
|
|
273
|
+
// `appIdFromIdentity`. Cloud `mcp.ggui.ai` deployments resolve this
|
|
274
|
+
// to `User.defaultAppId` via the auth-adapter; OSS deployments fall
|
|
275
|
+
// through to userId / DEFAULT_BUILDER_APP_ID.
|
|
276
|
+
//
|
|
277
|
+
// Path defaults to `/mcp` (Streamable HTTP convention). Cloud
|
|
278
|
+
// `mcp.ggui.ai` overrides to `/` so the bare-root URL is the
|
|
279
|
+
// universal endpoint — domain already says "mcp", no path repeat.
|
|
280
|
+
// Exposes audience tags ['agent', 'runtime'] — runtime tools stay
|
|
281
|
+
// routable on the same endpoint but invisible to the agent's
|
|
282
|
+
// `tools/list` via the `_meta.ui.visibility: ['app']` filter.
|
|
283
|
+
app.post(universalMcpPath, agentMcpHandler);
|
|
284
|
+
// Per-tenant endpoint — only mounted when the deployment opts in
|
|
285
|
+
// via `perAppRouting`. The same handler reads `req.params[paramName]`
|
|
286
|
+
// and uses it as `ctx.appId` for the request.
|
|
287
|
+
//
|
|
288
|
+
// When `pathPrefix` is set, the route mounts at
|
|
289
|
+
// `${pathPrefix}/:${paramName}` — cloud uses `/apps` so URLs are
|
|
290
|
+
// `mcp.ggui.ai/apps/<appId>`. The prefix segments per-tenant traffic
|
|
291
|
+
// from system routes (`/health`, `/oauth/*`, `/.well-known/*`,
|
|
292
|
+
// `/r/*`) so an opaque appId can never shadow a future static path.
|
|
293
|
+
//
|
|
294
|
+
// Without `pathPrefix`, the route mounts bare. The `paramPattern`
|
|
295
|
+
// constraint is the only collision defense — fine when the pattern
|
|
296
|
+
// guarantees non-collision (e.g. UUIDs).
|
|
297
|
+
//
|
|
298
|
+
// `path-to-regexp` v8 (express@5) dropped the `:param(pattern)`
|
|
299
|
+
// inline-regex syntax, so the pattern is enforced via a single
|
|
300
|
+
// `app.param` validator (anchored full-match) rather than baked into
|
|
301
|
+
// the route string. Registered once here; Express resolves it at
|
|
302
|
+
// dispatch for EVERY route declaring `paramName` — the per-app
|
|
303
|
+
// well-known route in the OAuth family AND this MCP route —
|
|
304
|
+
// regardless of registration order. A value failing the pattern
|
|
305
|
+
// 404s before any handler runs.
|
|
306
|
+
if (perAppRouting !== undefined) {
|
|
307
|
+
const { paramName, paramPattern, pathPrefix } = perAppRouting;
|
|
308
|
+
const appIdPattern = new RegExp(`^(?:${paramPattern})$`);
|
|
309
|
+
app.param(paramName, (_req, res, next, val) => {
|
|
310
|
+
if (typeof val !== "string" || !appIdPattern.test(val)) {
|
|
311
|
+
res.status(404).json({ error: "not_found" });
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
next();
|
|
315
|
+
});
|
|
316
|
+
const route = pathPrefix !== undefined ? `${pathPrefix}/:${paramName}` : `/:${paramName}`;
|
|
317
|
+
app.post(route, agentMcpHandler);
|
|
318
|
+
}
|
|
319
|
+
// /protocol — design-time spec/discovery surface.
|
|
320
|
+
// Hosts the `audience: ['protocol']` tools (`ggui_describe_*`,
|
|
321
|
+
// `ggui_get_example_blueprints`, `ggui_validate_blueprint`,
|
|
322
|
+
// `ggui_list_available_primitives`, `ggui_get_blueprint_boilerplate`).
|
|
323
|
+
// Strips spec-discovery noise off the agent's runtime `tools/list`
|
|
324
|
+
// — agents that need format docs hit `/protocol` explicitly.
|
|
325
|
+
// Always mounted; the route has the same auth chain as `/mcp` for
|
|
326
|
+
// v1 (operators MAY narrow auth in their own middleware later).
|
|
327
|
+
// Empty when the deployment didn't wire any protocol-tagged handlers.
|
|
328
|
+
app.post("/protocol", protocolMcpHandler);
|
|
329
|
+
app.get("/protocol", methodNotAllowed);
|
|
330
|
+
app.delete("/protocol", methodNotAllowed);
|
|
331
|
+
// /ops — operator-class management surface. Hosts the
|
|
332
|
+
// `audience: ['ops']` tools (`ggui_set_provider_key`, `ggui_get_credit_balance`,
|
|
333
|
+
// etc.). Always mounted; same auth chain as `/mcp`. Empty when the
|
|
334
|
+
// deployment didn't wire any ops-tagged handlers.
|
|
335
|
+
app.post("/ops", opsMcpHandler);
|
|
336
|
+
app.get("/ops", methodNotAllowed);
|
|
337
|
+
app.delete("/ops", methodNotAllowed);
|
|
338
|
+
// Isolated MCP services — each at its own HTTP path with its own
|
|
339
|
+
// tool namespace. Bypasses audience filtering (the path IS the
|
|
340
|
+
// audience). Each service builds its own MCP request handler via
|
|
341
|
+
// `makeMcpHandler(svc.handlers)`, reusing the same auth chain +
|
|
342
|
+
// identity-resolution as the canonical routes — the difference is
|
|
343
|
+
// ONLY the handler set the route exposes.
|
|
344
|
+
//
|
|
345
|
+
// Validation already ran in the composer via `validateMcpServices`;
|
|
346
|
+
// here we just iterate the validated list.
|
|
347
|
+
for (const svc of mcpServices) {
|
|
348
|
+
// `anonymous: true` skips the auth chain and synthesizes a
|
|
349
|
+
// builder-kind identity with `source: 'anonymous'`. Default
|
|
350
|
+
// (undefined / false) preserves the auth-required posture of
|
|
351
|
+
// every canonical route.
|
|
352
|
+
const svcMcpHandler = makeMcpHandler(svc.handlers, svc.anonymous ? { anonymous: true } : undefined);
|
|
353
|
+
app.post(svc.path, svcMcpHandler);
|
|
354
|
+
app.get(svc.path, methodNotAllowed);
|
|
355
|
+
app.delete(svc.path, methodNotAllowed);
|
|
356
|
+
}
|
|
357
|
+
app.get(universalMcpPath, methodNotAllowed);
|
|
358
|
+
app.delete(universalMcpPath, methodNotAllowed);
|
|
359
|
+
}
|
package/dist/mcp-mounts.d.ts
CHANGED
|
@@ -20,45 +20,8 @@
|
|
|
20
20
|
* `buildMcpServer` registration, `toolCount`, telemetry, and logging
|
|
21
21
|
* all Just Work.
|
|
22
22
|
*/
|
|
23
|
-
import type {
|
|
23
|
+
import type { SharedHandler } from "@ggui-ai/mcp-server-handlers";
|
|
24
24
|
import type { ZodRawShape } from "zod";
|
|
25
|
-
import type { WiredActionContext, WiredActionRouter } from "./render-channel.js";
|
|
26
|
-
/**
|
|
27
|
-
* Runtime ctx the mount-router hands the mount handler. Structurally a
|
|
28
|
-
* superset of `HandlerContext` (so the mount's `handler(input, ctx)`
|
|
29
|
-
* signature stays unchanged) PLUS the wired-action-only fields from
|
|
30
|
-
* {@link WiredActionContext}.
|
|
31
|
-
*
|
|
32
|
-
* Why the type is exported: TS-authored mount tools that want to read
|
|
33
|
-
* `ctx.sendPropsUpdate` / `ctx.renderId` import this and narrow their
|
|
34
|
-
* `handler` parameter (e.g. `async handler(input, ctx) { const wired =
|
|
35
|
-
* ctx as WiredMountContext; … }`). JS-authored mounts (.mjs) read the
|
|
36
|
-
* fields structurally — they're present on the runtime object whether
|
|
37
|
-
* or not the static type knows about them.
|
|
38
|
-
*
|
|
39
|
-
* The static `SharedHandler.handler` signature deliberately stays
|
|
40
|
-
* narrow on `HandlerContext`. Widening that type would force every
|
|
41
|
-
* shared handler (ggui-native + mounted) to acknowledge a wired-only
|
|
42
|
-
* surface, even handlers that never run through the wired-action path
|
|
43
|
-
* (e.g. `ggui_render`, blueprint search). The structural superset here
|
|
44
|
-
* keeps the canonical contract narrow without sacrificing access for
|
|
45
|
-
* mount tools that opt in.
|
|
46
|
-
*/
|
|
47
|
-
/**
|
|
48
|
-
* Intersection (not interface-extension) because `HandlerContext` declares
|
|
49
|
-
* `renderId?` as optional — the canonical context shape any handler may
|
|
50
|
-
* see — whereas `WiredActionContext` declares it as required (the
|
|
51
|
-
* wired-action dispatcher always knows the active render at invocation
|
|
52
|
-
* time). Interface-extends rejects "narrowing optional → required" via
|
|
53
|
-
* TS2320 ("cannot simultaneously extend"), but an intersection composes
|
|
54
|
-
* the two perfectly: optional ∧ required ≡ required.
|
|
55
|
-
*
|
|
56
|
-
* Surface for consumers stays identical — a TS-authored mount that types
|
|
57
|
-
* its `handler` parameter as `WiredMountContext` reads `renderId: string`
|
|
58
|
-
* (no `| undefined`) + every `HandlerContext` field
|
|
59
|
-
* (`appId`, `requestId`, optional `apiKeyHash`).
|
|
60
|
-
*/
|
|
61
|
-
export type WiredMountContext = Omit<HandlerContext, "renderId"> & WiredActionContext;
|
|
62
25
|
/**
|
|
63
26
|
* One named bundle of tool handlers the server should aggregate onto
|
|
64
27
|
* its MCP surface.
|
|
@@ -111,7 +74,7 @@ export declare function validateServicePath(p: string): ServicePath;
|
|
|
111
74
|
* MCP server with its own tool namespace.
|
|
112
75
|
*
|
|
113
76
|
* Use a **service** when the handler set is conceptually a distinct
|
|
114
|
-
* MCP server (
|
|
77
|
+
* MCP server (e.g. `<host>/docs`, `<host>/playground/todos`).
|
|
115
78
|
* Use a **mount** when the handlers should appear alongside
|
|
116
79
|
* ggui-native tools on the shared `/mcp` surface (fixtures, external
|
|
117
80
|
* MCPs aggregated for one session's view).
|
|
@@ -200,41 +163,4 @@ export declare function validateMcpServices(services: ReadonlyArray<McpService>
|
|
|
200
163
|
* `mounts` is empty, so callers never mutate the input reference.
|
|
201
164
|
*/
|
|
202
165
|
export declare function composeHandlersWithMounts(baseHandlers: ReadonlyArray<SharedHandler<ZodRawShape, ZodRawShape>>, mounts: ReadonlyArray<McpServerMount> | undefined): ReadonlyArray<SharedHandler<ZodRawShape, ZodRawShape>>;
|
|
203
|
-
/**
|
|
204
|
-
* Build a {@link WiredActionRouter} that dispatches wired-action
|
|
205
|
-
* hits to the matching mount handler's `handler(input, ctx)`. Zero-config composition for
|
|
206
|
-
* OSS `ggui serve` — when the operator declares `ggui.json#mcpMounts`,
|
|
207
|
-
* every mounted tool automatically becomes wire-dispatchable from
|
|
208
|
-
* a generated UI's `useAction` without additional glue.
|
|
209
|
-
*
|
|
210
|
-
* Ownership + scoping:
|
|
211
|
-
* - Only MOUNT handlers participate. ggui-native handlers
|
|
212
|
-
* (`ggui_render`, `ggui_handshake`, etc.) are platform tools and
|
|
213
|
-
* deliberately NOT exposed as wire-dispatchable actions — a
|
|
214
|
-
* component that tried to dispatch `ggui_render` would bypass the
|
|
215
|
-
* agentic-loop contract.
|
|
216
|
-
* - Name collisions across mounts are prevented at aggregation
|
|
217
|
-
* time by {@link composeHandlersWithMounts}, so the first-match
|
|
218
|
-
* lookup here is safe.
|
|
219
|
-
*
|
|
220
|
-
* Context synthesis:
|
|
221
|
-
* - Each invocation gets a fresh {@link HandlerContext} with the
|
|
222
|
-
* caller-supplied `appId` (typically the session's appId) + a
|
|
223
|
-
* fresh request id. Mount handlers that read from storage scope
|
|
224
|
-
* to this appId, matching the `/mcp` ingress behavior.
|
|
225
|
-
* - The session-channel dispatcher additionally hands a
|
|
226
|
-
* {@link WiredActionContext}. The runtime ctx the mount handler
|
|
227
|
-
* sees is a structural superset
|
|
228
|
-
* ({@link WiredMountContext}) so a JS-authored mount can call
|
|
229
|
-
* `ctx.sendPropsUpdate({...})` directly (closed over `ctx.renderId`).
|
|
230
|
-
* The static
|
|
231
|
-
* `SharedHandler.handler(input, ctx: HandlerContext)` shape stays
|
|
232
|
-
* untouched — tooling that doesn't read the wired fields keeps its
|
|
233
|
-
* existing types.
|
|
234
|
-
*
|
|
235
|
-
* Returns `null` when `mounts` is empty/absent, signaling the
|
|
236
|
-
* composer to OMIT the `wiredActionRouter` opt entirely so servers
|
|
237
|
-
* with no mounts behave as if the router did not exist.
|
|
238
|
-
*/
|
|
239
|
-
export declare function composeWiredActionRouterFromMounts(mounts: ReadonlyArray<McpServerMount> | undefined, resolveContext: () => HandlerContext): WiredActionRouter | null;
|
|
240
166
|
//# sourceMappingURL=mcp-mounts.d.ts.map
|
package/dist/mcp-mounts.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcp-mounts.d.ts","sourceRoot":"","sources":["../src/mcp-mounts.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,KAAK,EAAE,
|
|
1
|
+
{"version":3,"file":"mcp-mounts.d.ts","sourceRoot":"","sources":["../src/mcp-mounts.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAClE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,KAAK,CAAC;AAEvC;;;;;;;;GAQG;AACH,MAAM,WAAW,cAAc;IAC7B;;;;;OAKG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;;;;;;OAMG;IACH,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC,aAAa,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC,CAAC;CAC3E;AAED;;;GAGG;AACH,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG;IAAE,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAA;CAAE,CAAC;AAsBvE;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,MAAM,GAAG,WAAW,CAiB1D;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,MAAM,WAAW,UAAU;IACzB;;;;OAIG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC,aAAa,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC,CAAC;IAC1E;;;;;;;;;;;;;;;;OAgBG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC;CAC9B;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,aAAa,CAAC,UAAU,CAAC,GAAG,SAAS,GAC9C,aAAa,CAAC,UAAU,CAAC,CAyC3B;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,yBAAyB,CACvC,YAAY,EAAE,aAAa,CAAC,aAAa,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC,EACpE,MAAM,EAAE,aAAa,CAAC,cAAc,CAAC,GAAG,SAAS,GAChD,aAAa,CAAC,aAAa,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC,CAoDxD"}
|
package/dist/mcp-mounts.js
CHANGED
|
@@ -140,79 +140,3 @@ export function composeHandlersWithMounts(baseHandlers, mounts) {
|
|
|
140
140
|
}
|
|
141
141
|
return aggregated;
|
|
142
142
|
}
|
|
143
|
-
/**
|
|
144
|
-
* Build a {@link WiredActionRouter} that dispatches wired-action
|
|
145
|
-
* hits to the matching mount handler's `handler(input, ctx)`. Zero-config composition for
|
|
146
|
-
* OSS `ggui serve` — when the operator declares `ggui.json#mcpMounts`,
|
|
147
|
-
* every mounted tool automatically becomes wire-dispatchable from
|
|
148
|
-
* a generated UI's `useAction` without additional glue.
|
|
149
|
-
*
|
|
150
|
-
* Ownership + scoping:
|
|
151
|
-
* - Only MOUNT handlers participate. ggui-native handlers
|
|
152
|
-
* (`ggui_render`, `ggui_handshake`, etc.) are platform tools and
|
|
153
|
-
* deliberately NOT exposed as wire-dispatchable actions — a
|
|
154
|
-
* component that tried to dispatch `ggui_render` would bypass the
|
|
155
|
-
* agentic-loop contract.
|
|
156
|
-
* - Name collisions across mounts are prevented at aggregation
|
|
157
|
-
* time by {@link composeHandlersWithMounts}, so the first-match
|
|
158
|
-
* lookup here is safe.
|
|
159
|
-
*
|
|
160
|
-
* Context synthesis:
|
|
161
|
-
* - Each invocation gets a fresh {@link HandlerContext} with the
|
|
162
|
-
* caller-supplied `appId` (typically the session's appId) + a
|
|
163
|
-
* fresh request id. Mount handlers that read from storage scope
|
|
164
|
-
* to this appId, matching the `/mcp` ingress behavior.
|
|
165
|
-
* - The session-channel dispatcher additionally hands a
|
|
166
|
-
* {@link WiredActionContext}. The runtime ctx the mount handler
|
|
167
|
-
* sees is a structural superset
|
|
168
|
-
* ({@link WiredMountContext}) so a JS-authored mount can call
|
|
169
|
-
* `ctx.sendPropsUpdate({...})` directly (closed over `ctx.renderId`).
|
|
170
|
-
* The static
|
|
171
|
-
* `SharedHandler.handler(input, ctx: HandlerContext)` shape stays
|
|
172
|
-
* untouched — tooling that doesn't read the wired fields keeps its
|
|
173
|
-
* existing types.
|
|
174
|
-
*
|
|
175
|
-
* Returns `null` when `mounts` is empty/absent, signaling the
|
|
176
|
-
* composer to OMIT the `wiredActionRouter` opt entirely so servers
|
|
177
|
-
* with no mounts behave as if the router did not exist.
|
|
178
|
-
*/
|
|
179
|
-
export function composeWiredActionRouterFromMounts(mounts, resolveContext) {
|
|
180
|
-
if (!mounts || mounts.length === 0)
|
|
181
|
-
return null;
|
|
182
|
-
const byName = new Map();
|
|
183
|
-
for (const mount of mounts) {
|
|
184
|
-
for (const h of mount.handlers) {
|
|
185
|
-
// First-write-wins matches `composeHandlersWithMounts`'s
|
|
186
|
-
// collision-rejection order; the compose path throws before
|
|
187
|
-
// reaching this builder, so a duplicate here is dead code.
|
|
188
|
-
if (!byName.has(h.name))
|
|
189
|
-
byName.set(h.name, h);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
return {
|
|
193
|
-
has(toolName) {
|
|
194
|
-
return byName.has(toolName);
|
|
195
|
-
},
|
|
196
|
-
async invoke(toolName, input, wiredCtx) {
|
|
197
|
-
const handler = byName.get(toolName);
|
|
198
|
-
if (!handler) {
|
|
199
|
-
// Unreachable in normal use — the session channel has()-gates
|
|
200
|
-
// before calling invoke. Thrown errors surface as TOOL_THREW
|
|
201
|
-
// envelopes, so the caller still gets a canonical shape.
|
|
202
|
-
throw new Error(`wiredActionRouter(mounts): no handler registered for '${toolName}'`);
|
|
203
|
-
}
|
|
204
|
-
// Synthesize the runtime ctx — structural superset of
|
|
205
|
-
// HandlerContext + WiredActionContext. The static
|
|
206
|
-
// `SharedHandler.handler` accepts `HandlerContext`; mounts that
|
|
207
|
-
// need the wired fields read them off the same `ctx` argument
|
|
208
|
-
// (TS via `WiredMountContext`, JS structurally).
|
|
209
|
-
const baseCtx = resolveContext();
|
|
210
|
-
const ctx = {
|
|
211
|
-
...baseCtx,
|
|
212
|
-
renderId: wiredCtx.renderId,
|
|
213
|
-
sendPropsUpdate: wiredCtx.sendPropsUpdate,
|
|
214
|
-
};
|
|
215
|
-
return handler.handler(input, ctx);
|
|
216
|
-
},
|
|
217
|
-
};
|
|
218
|
-
}
|