@ggui-ai/mcp-server 0.14.0 → 0.15.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/README.md CHANGED
@@ -43,6 +43,26 @@ This package deliberately does **not** embed cloud-specific wiring (AWS, DynamoD
43
43
  Redis), and it does not implement authoring, pairing, or UI generation — those are
44
44
  separate packages and protocol flows.
45
45
 
46
+ ## TypeScript build
47
+
48
+ ### `tsc` under `moduleResolution: NodeNext` reports errors inside `node_modules/@modelcontextprotocol/ext-apps`
49
+
50
+ Your own code is fine; the errors sit in a dependency's declarations. With `moduleResolution: "NodeNext"` (or `Node16`), `tsc` reports TS2834 and TS2339 inside `node_modules/@modelcontextprotocol/ext-apps/dist/src/app.d.ts`, and TS2305 / TS2460 on `@ggui-ai/protocol`'s `host-context.d.ts` for the three names it re-exports from there (`McpUiDisplayMode`, `McpUiHostContext`, `McpUiHostCapabilities`).
51
+
52
+ **Cause.** `@modelcontextprotocol/ext-apps` 1.7.5 ships extensionless relative specifiers in its own `.d.ts` files — a defect in the published package, tracked upstream at [modelcontextprotocol/ext-apps#704](https://github.com/modelcontextprotocol/ext-apps/issues/704). `@ggui-ai/*` re-exports those names rather than forking a frozen spec surface, so the defect surfaces on our line. Every `@ggui-ai/*` declaration itself carries explicit `.js` extensions since 0.15.0.
53
+
54
+ **Fix.** Until ext-apps ships the upstream fix, set the TypeScript switch for third-party declaration defects in your `tsconfig.json`:
55
+
56
+ ```json
57
+ {
58
+ "compilerOptions": {
59
+ "skipLibCheck": true
60
+ }
61
+ }
62
+ ```
63
+
64
+ `skipLibCheck` skips type-checking of `.d.ts` files only — your own sources still typecheck fully. The dependency bump follows the day the upstream fix is published; nothing in your code changes.
65
+
46
66
  ## License
47
67
 
48
68
  Apache-2.0
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * POST <universalMcpPath> — DATA PLANE: agent+runtime tools
5
5
  * (default `/mcp`)
6
- * POST <pathPrefix>/:appId — data plane, per-tenant variant
6
+ * POST <pathPrefix>/:appId — data plane, per-app variant
7
7
  * (opt-in via `perAppRouting`)
8
8
  * POST /control — CONTROL PLANE: design-time
9
9
  * spec/discovery (anonymous) +
@@ -31,6 +31,7 @@
31
31
  * (`agent` / `runtime` / `protocol` / `ops`) and the wire-name prefix
32
32
  * rules.
33
33
  */
34
+ import { type JsonValue } from "@ggui-ai/protocol";
34
35
  import type { AuthAdapter, AuthResult } from "@ggui-ai/mcp-server-core";
35
36
  import type { HandlerContext, SharedHandler } from "@ggui-ai/mcp-server-handlers";
36
37
  import type { Express } from "express";
@@ -39,7 +40,7 @@ import type { ZodRawShape } from "zod";
39
40
  import { type BuildMcpServerOptions, type ServerInfo } from "./build-mcp.js";
40
41
  import type { Logger } from "./logger.js";
41
42
  import type { McpService } from "./mcp-mounts.js";
42
- /** Per-tenant URL routing shape — mirrors `CreateGguiServerOptions.perAppRouting`. */
43
+ /** Per-app URL routing shape — mirrors `CreateGguiServerOptions.perAppRouting`. */
43
44
  interface PerAppRouting {
44
45
  readonly paramName: string;
45
46
  readonly paramPattern: string;
@@ -61,6 +62,13 @@ export interface ErrorMapperResult {
61
62
  readonly message: string;
62
63
  /** Response headers to set before the JSON body is written. */
63
64
  readonly headers?: Readonly<Record<string, string>>;
65
+ /**
66
+ * JSON-RPC 2.0 error `data` — any JSON value the deployment wants the
67
+ * client to read alongside `code` / `message` (a structured reason, a
68
+ * retry hint). Serialized verbatim on `error.data`; omitted from the
69
+ * body when absent, so a mapper that never sets it changes nothing.
70
+ */
71
+ readonly data?: JsonValue;
64
72
  }
65
73
  interface MountOptions {
66
74
  /** Express app to mount onto. */
@@ -92,9 +100,9 @@ interface MountOptions {
92
100
  readonly als: AsyncLocalStorage<HandlerContext>;
93
101
  /** Identity → appId resolution rule (SPEC §12.2). */
94
102
  readonly appIdFromIdentity: (result: AuthResult) => string;
95
- /** Universal endpoint path (default `/mcp`; cloud overrides to `/`). */
103
+ /** Universal endpoint path (default `/mcp`; a deployment may serve it at `/`). */
96
104
  readonly universalMcpPath: string;
97
- /** Per-tenant endpoint shape — absent = universal-only deployment. */
105
+ /** Per-app endpoint shape — absent = universal-only deployment. */
98
106
  readonly perAppRouting?: PerAppRouting;
99
107
  /** Whether OAuth is enabled (adds `WWW-Authenticate` on 401). */
100
108
  readonly oauthEnabled: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"mcp-endpoint-routes.d.ts","sourceRoot":"","sources":["../src/mcp-endpoint-routes.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"mcp-endpoint-routes.d.ts","sourceRoot":"","sources":["../src/mcp-endpoint-routes.ts"],"names":[],"mappings":"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"}
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * POST <universalMcpPath> — DATA PLANE: agent+runtime tools
5
5
  * (default `/mcp`)
6
- * POST <pathPrefix>/:appId — data plane, per-tenant variant
6
+ * POST <pathPrefix>/:appId — data plane, per-app variant
7
7
  * (opt-in via `perAppRouting`)
8
8
  * POST /control — CONTROL PLANE: design-time
9
9
  * spec/discovery (anonymous) +
@@ -31,13 +31,50 @@
31
31
  * (`agent` / `runtime` / `protocol` / `ops`) and the wire-name prefix
32
32
  * rules.
33
33
  */
34
- import { isRecord } from "@ggui-ai/protocol";
34
+ import { MCP_ERROR_CODES, isRecord } from "@ggui-ai/protocol";
35
35
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
36
36
  import { randomUUID } from "node:crypto";
37
37
  import { resolveIdentity, UnauthenticatedError } from "./auth.js";
38
38
  import { buildMcpServer } from "./build-mcp.js";
39
39
  import { CONTROL_PATH, DATA_PLANE_AUDIENCES, filterHandlersByAudience, } from "./control-service.js";
40
40
  import { buildWwwAuthenticate, resolveIssuerUrl } from "./oauth.js";
41
+ /** The JSON-RPC error object a mapped result becomes on the wire. */
42
+ function jsonRpcError(mapped) {
43
+ return {
44
+ code: mapped.code,
45
+ message: mapped.message,
46
+ ...(mapped.data !== undefined ? { data: mapped.data } : {}),
47
+ };
48
+ }
49
+ /** The two statuses an authorization refusal may carry — a mapper is bounded to them. */
50
+ const AUTHORIZATION_REFUSAL_STATUSES = new Set([401, 403]);
51
+ /**
52
+ * A deployment's error mapper may attach JSON-RPC `data` (and its own
53
+ * `code` / `message` / headers) to a per-app authorization refusal, so a
54
+ * client can read a structured reason instead of parsing prose. The
55
+ * refusal stays a 401 or a 403: a mapper answering any other status, or
56
+ * throwing, is ignored and logged, and the default-deny 403 stands
57
+ * byte-identical to a deployment with no mapper at all.
58
+ */
59
+ function mapAuthorizationRefusal(err, errorMapper, log) {
60
+ if (!errorMapper)
61
+ return undefined;
62
+ let mapped;
63
+ try {
64
+ mapped = errorMapper(err);
65
+ }
66
+ catch (mapperErr) {
67
+ log.warn("error_mapper_failed", { error: String(mapperErr) });
68
+ return undefined;
69
+ }
70
+ if (mapped === undefined)
71
+ return undefined;
72
+ if (!AUTHORIZATION_REFUSAL_STATUSES.has(mapped.status)) {
73
+ log.warn("per_app_authorize_mapper_out_of_bounds", { status: mapped.status });
74
+ return undefined;
75
+ }
76
+ return mapped;
77
+ }
41
78
  /**
42
79
  * Resolve the resource path that `WWW-Authenticate` should point at
43
80
  * for the current request. Per-app `/mcp` requests
@@ -113,7 +150,7 @@ export function mountMcpEndpoints(opts) {
113
150
  identity = { identity: { kind: "builder" }, source: "anonymous" };
114
151
  }
115
152
  else {
116
- // Diagnostic: did a Bearer credential actually reach the pod on
153
+ // Diagnostic: did a Bearer credential actually reach the server on
117
154
  // this request? This splits "the client/transport never sent one"
118
155
  // from "we received it but the adapter rejected it" — otherwise
119
156
  // indistinguishable in `auth_failed`. Only a short, non-secret
@@ -145,7 +182,7 @@ export function mountMcpEndpoints(opts) {
145
182
  }
146
183
  res.status(401).json({
147
184
  jsonrpc: "2.0",
148
- error: { code: -32000, message: err.message },
185
+ error: { code: MCP_ERROR_CODES.UNAUTHORIZED, message: err.message },
149
186
  id: null,
150
187
  });
151
188
  return;
@@ -171,7 +208,10 @@ export function mountMcpEndpoints(opts) {
171
208
  reqLogger.warn("federated_identity_rejected", { route: req.path });
172
209
  res.status(403).json({
173
210
  jsonrpc: "2.0",
174
- error: { code: -32000, message: "federated identities are not permitted on this route" },
211
+ error: {
212
+ code: MCP_ERROR_CODES.UNAUTHORIZED,
213
+ message: "federated identities are not permitted on this route",
214
+ },
175
215
  id: null,
176
216
  });
177
217
  return;
@@ -196,7 +236,7 @@ export function mountMcpEndpoints(opts) {
196
236
  res.status(401).json({
197
237
  jsonrpc: "2.0",
198
238
  error: {
199
- code: -32000,
239
+ code: MCP_ERROR_CODES.UNAUTHORIZED,
200
240
  message: `${opsToolName} is an operator tool and needs an authenticated caller. ` +
201
241
  `Present a bearer token this deployment accepts, or complete the OAuth flow ` +
202
242
  `advertised in WWW-Authenticate (universal connector keys come from the ` +
@@ -207,10 +247,10 @@ export function mountMcpEndpoints(opts) {
207
247
  return;
208
248
  }
209
249
  }
210
- // Per-tenant URL routing. When `perAppRouting`
250
+ // Per-app URL routing. When `perAppRouting`
211
251
  // is configured AND the request matched the per-app path
212
252
  // `/:${paramName}/mcp`, Express populates `req.params[paramName]`
213
- // with the validated tenant id. Use it as `ctx.appId` for this
253
+ // with the validated app id. Use it as `ctx.appId` for this
214
254
  // request, overriding `appIdFromIdentity`. The universal `/mcp`
215
255
  // route doesn't have the param so it falls through to the
216
256
  // identity-based resolution.
@@ -218,11 +258,14 @@ export function mountMcpEndpoints(opts) {
218
258
  const hasUrlAppId = typeof urlAppId === "string" && urlAppId.length > 0;
219
259
  // Per-app authorize hook — when the deployment configured
220
260
  // `perAppRouting.authorize` AND the request matched the per-app
221
- // path, invoke the callback. Throwing collapses to a 403 before
222
- // the MCP handler ever sees the request, which is the boundary
223
- // that prevents cross-user blueprint reads when pod tools bypass
261
+ // path, invoke the callback. Throwing refuses before the MCP
262
+ // handler ever sees the request, which is the boundary that
263
+ // prevents cross-user blueprint reads when a deployment's own tools bypass
224
264
  // AppSync owner-auth via raw DDB. Universal-endpoint requests
225
- // skip this entirely (no urlAppId).
265
+ // skip this entirely (no urlAppId). The deployment's `errorMapper`
266
+ // may give the refusal a structured JSON-RPC `data` (bounded to
267
+ // 401 / 403, see `mapAuthorizationRefusal`); otherwise — and for
268
+ // every mapping outside those bounds — the default-deny 403 stands.
226
269
  if (hasUrlAppId && perAppRouting?.authorize) {
227
270
  try {
228
271
  await perAppRouting.authorize(urlAppId, identity);
@@ -232,9 +275,17 @@ export function mountMcpEndpoints(opts) {
232
275
  urlAppId,
233
276
  reason: err instanceof Error ? err.message : String(err),
234
277
  });
235
- res.status(403).json({
278
+ const mapped = mapAuthorizationRefusal(err, errorMapper, reqLogger);
279
+ if (mapped?.headers)
280
+ res.set(mapped.headers);
281
+ res.status(mapped?.status ?? 403).json({
236
282
  jsonrpc: "2.0",
237
- error: { code: -32000, message: "Forbidden" },
283
+ // #836: a first-party server never answers -32000 — that is the SDK
284
+ // client's own ConnectionClosed number, so a refusal and a dropped
285
+ // socket would read the same. An authorization refusal is UNAUTHORIZED.
286
+ error: mapped
287
+ ? jsonRpcError(mapped)
288
+ : { code: MCP_ERROR_CODES.UNAUTHORIZED, message: "Unauthorized" },
238
289
  id: null,
239
290
  });
240
291
  return;
@@ -248,11 +299,11 @@ export function mountMcpEndpoints(opts) {
248
299
  // handler should branch on the specific mechanism.
249
300
  authSource: identity.source,
250
301
  // Identity is the canonical source of two mutually-exclusive
251
- // hosted fields: `apiKeyHash` for kind=app, `userId` for kind=user.
252
- // Threading them onto HandlerContext here means hosted handlers
253
- // (the K8s ggui-protocol pod's billing gate + per-user blueprint
254
- // scoping) can read identity directly without a parallel pod-only
255
- // context shape; OSS handlers continue to ignore both fields.
302
+ // fields: `apiKeyHash` for kind=app, `userId` for kind=user.
303
+ // Threading them onto HandlerContext here means a deployment's own
304
+ // handlers (per-key metering, per-user scoping) can read identity
305
+ // directly without a parallel context shape; the handlers this
306
+ // package ships ignore both fields.
256
307
  ...(identity.identity.kind === "app" ? { apiKeyHash: identity.identity.apiKeyHash } : {}),
257
308
  ...(identity.identity.kind === "user" ? { userId: identity.identity.userId } : {}),
258
309
  // What the credential itself may act on, when the adapter
@@ -301,7 +352,7 @@ export function mountMcpEndpoints(opts) {
301
352
  res.set(mapped.headers);
302
353
  res.status(mapped.status).json({
303
354
  jsonrpc: "2.0",
304
- error: { code: mapped.code, message: mapped.message },
355
+ error: jsonRpcError(mapped),
305
356
  id: null,
306
357
  });
307
358
  }
@@ -316,7 +367,9 @@ export function mountMcpEndpoints(opts) {
316
367
  }
317
368
  };
318
369
  const methodNotAllowed = (_req, res) => {
319
- res.status(405).json({
370
+ // A 405 names what IS allowed (RFC 9110 §15.5.6); every mount here is
371
+ // POST-only.
372
+ res.set("Allow", "POST").status(405).json({
320
373
  jsonrpc: "2.0",
321
374
  error: {
322
375
  code: -32000,
@@ -341,24 +394,24 @@ export function mountMcpEndpoints(opts) {
341
394
  anonymousOpsChallenge: controlOpsToolNames,
342
395
  });
343
396
  // Universal endpoint — `appId` resolved from the auth identity via
344
- // `appIdFromIdentity`. Cloud `mcp.ggui.ai` deployments resolve this
345
- // to `User.defaultAppId` via the auth-adapter; OSS deployments fall
346
- // through to userId / DEFAULT_BUILDER_APP_ID.
397
+ // `appIdFromIdentity`. A deployment's auth adapter may resolve it
398
+ // (e.g. to the user's default app); without one it falls through to
399
+ // userId / DEFAULT_BUILDER_APP_ID.
347
400
  //
348
- // Path defaults to `/mcp` (Streamable HTTP convention). Cloud
349
- // `mcp.ggui.ai` overrides to `/` so the bare-root URL is the
350
- // universal endpoint — domain already says "mcp", no path repeat.
401
+ // Path defaults to `/mcp` (Streamable HTTP convention). A deployment
402
+ // whose hostname already says "mcp" may serve it at `/` so the
403
+ // bare-root URL is the universal endpoint — no path repeat.
351
404
  // Exposes audience tags ['agent', 'runtime'] — runtime tools stay
352
405
  // routable on the same endpoint but invisible to the agent's
353
406
  // `tools/list` via the `_meta.ui.visibility: ['app']` filter.
354
407
  app.post(universalMcpPath, agentMcpHandler);
355
- // Per-tenant endpoint — only mounted when the deployment opts in
408
+ // Per-app endpoint — only mounted when the deployment opts in
356
409
  // via `perAppRouting`. The same handler reads `req.params[paramName]`
357
410
  // and uses it as `ctx.appId` for the request.
358
411
  //
359
412
  // When `pathPrefix` is set, the route mounts at
360
- // `${pathPrefix}/:${paramName}` — cloud uses `/apps` so URLs are
361
- // `mcp.ggui.ai/apps/<appId>`. The prefix segments per-tenant traffic
413
+ // `${pathPrefix}/:${paramName}` — e.g. `/apps`, so URLs read
414
+ // `<host>/apps/<appId>`. The prefix segments per-app traffic
362
415
  // from system routes (`/health`, `/oauth/*`, `/.well-known/*`,
363
416
  // `/r/*`) so an opaque appId can never shadow a future static path.
364
417
  //
@@ -386,6 +439,12 @@ export function mountMcpEndpoints(opts) {
386
439
  });
387
440
  const route = pathPrefix !== undefined ? `${pathPrefix}/:${paramName}` : `/:${paramName}`;
388
441
  app.post(route, agentMcpHandler);
442
+ // The per-app endpoint is POST-only like every other mount: GET (a
443
+ // client's optional SSE listener) and DELETE (session terminate) read
444
+ // the transport's 405, never Express's text/html 404 — a 404 says "no
445
+ // such resource" and a client retries it every turn (ggui#850).
446
+ app.get(route, methodNotAllowed);
447
+ app.delete(route, methodNotAllowed);
389
448
  }
390
449
  // /control — the control plane. Hosts every `audience: ['protocol']`
391
450
  // tool (design-time spec/discovery, anonymous) and every
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ggui-ai/mcp-server",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "description": "Self-hosted MCP server for the ggui protocol. An HTTP/MCP binding layer over @ggui-ai/mcp-server-handlers and the reference adapters from @ggui-ai/mcp-server-core — run it via `ggui serve`.",
5
5
  "keywords": [
6
6
  "ggui",
@@ -32,17 +32,17 @@
32
32
  "resend": "^6.12.2",
33
33
  "ws": "^8.20.1",
34
34
  "zod": "^4.3.6",
35
- "@ggui-ai/console": "0.14.0",
36
- "@ggui-ai/design": "0.14.0",
37
- "@ggui-ai/iframe-runtime": "0.14.0",
38
- "@ggui-ai/mcp-server-handlers": "0.14.0",
39
- "@ggui-ai/mcp-server-core": "0.14.0",
40
- "@ggui-ai/negotiator": "0.14.0",
41
- "@ggui-ai/preview-a2ui": "0.14.0",
42
- "@ggui-ai/project-config": "0.14.0",
43
- "@ggui-ai/protocol": "0.14.0",
44
- "@ggui-ai/ui-registry": "0.14.0",
45
- "@ggui-ai/ui-gen": "0.14.0"
35
+ "@ggui-ai/console": "0.15.0",
36
+ "@ggui-ai/design": "0.15.0",
37
+ "@ggui-ai/mcp-server-handlers": "0.15.0",
38
+ "@ggui-ai/iframe-runtime": "0.15.0",
39
+ "@ggui-ai/mcp-server-core": "0.15.0",
40
+ "@ggui-ai/negotiator": "0.15.0",
41
+ "@ggui-ai/preview-a2ui": "0.15.0",
42
+ "@ggui-ai/project-config": "0.15.0",
43
+ "@ggui-ai/protocol": "0.15.0",
44
+ "@ggui-ai/ui-registry": "0.15.0",
45
+ "@ggui-ai/ui-gen": "0.15.0"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@types/express": "^5.0.0",
@@ -51,7 +51,7 @@
51
51
  "@types/ws": "^8.5.10",
52
52
  "typescript": "^5.0.0",
53
53
  "vitest": "^3.2.6",
54
- "@ggui-ai/protocol-conformance": "0.14.0"
54
+ "@ggui-ai/protocol-conformance": "0.15.0"
55
55
  },
56
56
  "repository": {
57
57
  "type": "git",