stitchkit 0.43.1 → 0.44.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.
Files changed (63) hide show
  1. package/README.md +12 -4
  2. package/dist/cli.js +2 -2
  3. package/dist/contract/define.d.ts +18 -0
  4. package/dist/contract/define.d.ts.map +1 -1
  5. package/dist/contract/index.d.ts +1 -1
  6. package/dist/contract/index.d.ts.map +1 -1
  7. package/dist/{index-310bfer5.js → index-36f1xkdb.js} +1 -1
  8. package/dist/{index-esqmem78.js → index-4fyn9f09.js} +2 -1
  9. package/dist/{index-h2wdcsby.js → index-mvjyq9h9.js} +176 -12
  10. package/dist/{index-gex6gxhe.js → index-yh0p5ddn.js} +34 -2
  11. package/dist/index.js +4 -1
  12. package/dist/internal/secure-fetch.d.ts +21 -1
  13. package/dist/internal/secure-fetch.d.ts.map +1 -1
  14. package/dist/node.js +2 -2
  15. package/dist/observability/audit.d.ts.map +1 -1
  16. package/dist/observability/event.d.ts +16 -0
  17. package/dist/observability/event.d.ts.map +1 -1
  18. package/dist/observability/index.d.ts +1 -1
  19. package/dist/observability/index.d.ts.map +1 -1
  20. package/dist/observability/index.js +30 -1
  21. package/dist/observability/trace.d.ts +10 -0
  22. package/dist/observability/trace.d.ts.map +1 -1
  23. package/dist/server/implement.d.ts.map +1 -1
  24. package/dist/server/index.js +3 -3
  25. package/dist/server/types.d.ts +14 -2
  26. package/dist/server/types.d.ts.map +1 -1
  27. package/dist/tools/execute.d.ts +1 -1
  28. package/dist/tools/execute.d.ts.map +1 -1
  29. package/dist/tools/mcp-app.d.ts +3 -0
  30. package/dist/tools/mcp-app.d.ts.map +1 -1
  31. package/dist/tools/mcp-handler.d.ts +34 -23
  32. package/dist/tools/mcp-handler.d.ts.map +1 -1
  33. package/dist/tools/mcp-round.d.ts +43 -0
  34. package/dist/tools/mcp-round.d.ts.map +1 -0
  35. package/dist/tools/mcp-stdio.d.ts +11 -18
  36. package/dist/tools/mcp-stdio.d.ts.map +1 -1
  37. package/dist/tools/mcp-trace.d.ts +4 -0
  38. package/dist/tools/mcp-trace.d.ts.map +1 -0
  39. package/dist/tools/mcp.d.ts +24 -3
  40. package/dist/tools/mcp.d.ts.map +1 -1
  41. package/dist/tools/mount-download.d.ts +2 -2
  42. package/dist/tools/mount-download.d.ts.map +1 -1
  43. package/dist/tools/mount-upload.d.ts +1 -1
  44. package/dist/tools/mount-upload.d.ts.map +1 -1
  45. package/dist/tools/mount-wait.d.ts +3 -3
  46. package/dist/tools/mount-wait.d.ts.map +1 -1
  47. package/dist/tools/mount.d.ts +1 -1
  48. package/dist/tools/mount.d.ts.map +1 -1
  49. package/dist/tools/native-mcp.d.ts +3 -2
  50. package/dist/tools/native-mcp.d.ts.map +1 -1
  51. package/dist/tools/oauth-provider.d.ts +77 -7
  52. package/dist/tools/oauth-provider.d.ts.map +1 -1
  53. package/dist/tools/runtime-tool.d.ts +27 -20
  54. package/dist/tools/runtime-tool.d.ts.map +1 -1
  55. package/dist/tools/toolkit.d.ts +5 -5
  56. package/dist/tools/toolkit.d.ts.map +1 -1
  57. package/dist/tools/view-file.d.ts +1 -1
  58. package/dist/tools/view-file.d.ts.map +1 -1
  59. package/dist/tools.d.ts +5 -5
  60. package/dist/tools.d.ts.map +1 -1
  61. package/dist/tools.js +816 -197
  62. package/llms-full.txt +412 -78
  63. package/package.json +5 -4
package/dist/tools.js CHANGED
@@ -2,7 +2,7 @@ import {
2
2
  inputIsQuery,
3
3
  signJwt,
4
4
  verifyPkce
5
- } from "./index-310bfer5.js";
5
+ } from "./index-36f1xkdb.js";
6
6
  import {
7
7
  DEFAULT_CORS_ALLOW_HEADERS
8
8
  } from "./index-6jypn22c.js";
@@ -16,6 +16,7 @@ import {
16
16
  createToolRunner,
17
17
  executeToolMethod,
18
18
  fetchGuarded,
19
+ fetchPinnedDocument,
19
20
  flattenToolJsonSchema,
20
21
  formatToolError,
21
22
  isObjectPresentationSchema,
@@ -25,7 +26,7 @@ import {
25
26
  toolErrorFromResult,
26
27
  toolResultFromError,
27
28
  writeDownload
28
- } from "./index-h2wdcsby.js";
29
+ } from "./index-mvjyq9h9.js";
29
30
  import {
30
31
  toJsonSchema
31
32
  } from "./index-frfyw9fa.js";
@@ -34,12 +35,15 @@ import {
34
35
  } from "./index-x3fcszf8.js";
35
36
  import {
36
37
  AppError,
38
+ getRequestContext,
37
39
  getTraceId,
38
40
  isRecord,
39
41
  mergeMeta,
40
42
  parseTrailingWildcard,
43
+ resolvePropagationContext,
44
+ runWithRequestContext,
41
45
  typedEntries
42
- } from "./index-gex6gxhe.js";
46
+ } from "./index-yh0p5ddn.js";
43
47
 
44
48
  // src/tools/agent.ts
45
49
  import { jsonSchema, tool } from "ai";
@@ -54,7 +58,12 @@ function defineRuntimeTool(definition) {
54
58
  function createRuntimeToolFactory(config) {
55
59
  function parseContext(context) {
56
60
  const parsed = config.context.parse(context);
57
- return { ...parsed, params: undefined, input: context.input };
61
+ return {
62
+ ...context,
63
+ ...parsed,
64
+ params: undefined,
65
+ input: context.input
66
+ };
58
67
  }
59
68
  function define(definition) {
60
69
  if (definition.output !== undefined) {
@@ -104,7 +113,8 @@ function runtimeToolIdentity(definition) {
104
113
  scope: definition.identity.scope,
105
114
  meta: definition.identity.meta,
106
115
  annotations: definition.annotations,
107
- ui: definition.ui
116
+ ui: definition.ui,
117
+ mcp: definition.mcp
108
118
  };
109
119
  }
110
120
  function runtimeToolMountable(definition, assertName = true) {
@@ -334,7 +344,10 @@ function buildToolManifest(config) {
334
344
  }));
335
345
  }
336
346
  // src/tools/mcp.ts
337
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
347
+ import {
348
+ createRequestStateCodec,
349
+ McpServer
350
+ } from "@modelcontextprotocol/server";
338
351
  import { z as z2 } from "zod";
339
352
 
340
353
  // src/tools/mcp-app.ts
@@ -358,12 +371,271 @@ function inlineMcpAppBundle(html) {
358
371
  return html.replace(EXT_APPS_BUNDLE_PLACEHOLDER, () => bundle);
359
372
  }
360
373
 
374
+ // src/tools/mcp-round.ts
375
+ import {
376
+ acceptedContent,
377
+ CLIENT_INFO_META_KEY,
378
+ inputRequired,
379
+ inputResponse,
380
+ PROTOCOL_VERSION_META_KEY
381
+ } from "@modelcontextprotocol/server";
382
+ function transportContext(context, toolName, outcome, round) {
383
+ const protocolVersionValue = isRecord(context.mcpReq.envelope) ? Reflect.get(context.mcpReq.envelope, PROTOCOL_VERSION_META_KEY) : undefined;
384
+ const protocolVersion = typeof protocolVersionValue === "string" ? protocolVersionValue : undefined;
385
+ const clientInfoValue = isRecord(context.mcpReq.envelope) ? Reflect.get(context.mcpReq.envelope, CLIENT_INFO_META_KEY) : undefined;
386
+ const clientInfo = isRecord(clientInfoValue) && typeof clientInfoValue.name === "string" && typeof clientInfoValue.version === "string" ? { name: clientInfoValue.name, version: clientInfoValue.version } : undefined;
387
+ return {
388
+ signal: context.mcpReq.signal,
389
+ mcp: {
390
+ era: context.mcpReq.envelope ? "modern" : "legacy",
391
+ method: context.mcpReq.method,
392
+ toolName,
393
+ ...protocolVersion !== undefined && { protocolVersion },
394
+ ...clientInfo !== undefined && { clientInfo },
395
+ ...outcome !== undefined && { outcome },
396
+ ...round !== undefined && { round }
397
+ }
398
+ };
399
+ }
400
+ function stableValue(value) {
401
+ if (Array.isArray(value))
402
+ return value.map(stableValue);
403
+ if (value === null || typeof value !== "object")
404
+ return value;
405
+ const result = {};
406
+ for (const key of Object.keys(value).sort()) {
407
+ result[key] = stableValue(Reflect.get(value, key));
408
+ }
409
+ return result;
410
+ }
411
+ async function argumentsDigest(args) {
412
+ const bytes = new TextEncoder().encode(JSON.stringify(stableValue(args)));
413
+ const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", bytes));
414
+ let binary = "";
415
+ for (const byte of digest)
416
+ binary += String.fromCodePoint(byte);
417
+ return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
418
+ }
419
+ function operationIdentity(tool2) {
420
+ return {
421
+ toolName: tool2.name,
422
+ serviceName: tool2.method.serviceName,
423
+ action: tool2.method.key,
424
+ method: tool2.method.method,
425
+ ...tool2.method.scope !== undefined && { scope: tool2.method.scope }
426
+ };
427
+ }
428
+ function sameIdentity(left, right) {
429
+ return left.toolName === right.toolName && left.serviceName === right.serviceName && left.action === right.action && left.method === right.method && left.scope === right.scope;
430
+ }
431
+ async function runRoundSuccess(tool2, rawArgs, runTool, context) {
432
+ return runTool({
433
+ ...tool2,
434
+ method: {
435
+ ...tool2.method,
436
+ outputSchema: undefined,
437
+ handler: () => {
438
+ return;
439
+ }
440
+ }
441
+ }, rawArgs, context);
442
+ }
443
+ async function runRoundFailure(tool2, rawArgs, runTool, context, code, message) {
444
+ return runTool({
445
+ ...tool2,
446
+ method: {
447
+ ...tool2.method,
448
+ outputSchema: undefined,
449
+ handler: () => {
450
+ throw new AppError(code, message, 400);
451
+ }
452
+ }
453
+ }, rawArgs, context);
454
+ }
455
+ function validatePolicy(tool2, policy, maxRounds) {
456
+ if (!Number.isInteger(maxRounds) || maxRounds < 1) {
457
+ throw new Error("[stitchkit] multiRound.serving.maxRounds must be a positive integer");
458
+ }
459
+ if (policy.inputRequired.length === 0) {
460
+ throw new Error(`[stitchkit] MCP tool "${tool2.name}" must declare at least one input round`);
461
+ }
462
+ if (policy.inputRequired.length > maxRounds) {
463
+ throw new Error(`[stitchkit] MCP tool "${tool2.name}" declares ${policy.inputRequired.length} input rounds, exceeding maxRounds ${maxRounds}`);
464
+ }
465
+ const keys = new Set;
466
+ for (const request of policy.inputRequired) {
467
+ if (keys.has(request.key)) {
468
+ throw new Error(`[stitchkit] MCP tool "${tool2.name}" declares duplicate input key "${request.key}"`);
469
+ }
470
+ keys.add(request.key);
471
+ }
472
+ }
473
+ async function failedResolution(options) {
474
+ const result = await runRoundFailure(options.tool, options.rawArgs, options.runTool, options.context, options.code, options.message);
475
+ return { kind: "response", response: options.formatFailure(result) };
476
+ }
477
+ async function resolveMcpRound(options) {
478
+ const { policy } = options;
479
+ if (!policy) {
480
+ return {
481
+ kind: "continue",
482
+ context: transportContext(options.context, options.tool.name)
483
+ };
484
+ }
485
+ if (!options.runtime) {
486
+ throw new Error(`[stitchkit] MCP tool "${options.tool.name}" declares inputRequired but no multiRound.state key is configured`);
487
+ }
488
+ validatePolicy(options.tool, policy, options.runtime.maxRounds);
489
+ const requests = policy.inputRequired;
490
+ const state = options.context.mcpReq.requestState();
491
+ const digest = await argumentsDigest(options.rawArgs);
492
+ if (!state) {
493
+ if (isRecord(options.context.mcpReq.inputResponses)) {
494
+ return failedResolution({
495
+ ...options,
496
+ context: transportContext(options.context, options.tool.name, "invalid", 0),
497
+ code: "INVALID_REQUEST_STATE",
498
+ message: "Input responses require a valid continuation state"
499
+ });
500
+ }
501
+ const guarded = await runRoundSuccess(options.tool, options.rawArgs, options.runTool, transportContext(options.context, options.tool.name, "input_required", 0));
502
+ if (!guarded.ok) {
503
+ return { kind: "response", response: options.formatFailure(guarded) };
504
+ }
505
+ const request2 = requests[0];
506
+ if (!request2)
507
+ throw new Error("[stitchkit] validated MRTR policy has no first round");
508
+ const requestState = await options.runtime.codec.mint({
509
+ identity: operationIdentity(options.tool),
510
+ argumentsDigest: digest,
511
+ round: 0,
512
+ accepted: {}
513
+ }, options.context);
514
+ return {
515
+ kind: "response",
516
+ response: inputRequired({
517
+ inputRequests: {
518
+ [request2.key]: inputRequired.elicit({
519
+ message: request2.message,
520
+ requestedSchema: request2.schema
521
+ })
522
+ },
523
+ requestState
524
+ })
525
+ };
526
+ }
527
+ const invalidState = !sameIdentity(state.identity, operationIdentity(options.tool)) || state.argumentsDigest !== digest || !Number.isInteger(state.round) || state.round < 0 || state.round >= requests.length || state.round >= options.runtime.maxRounds || !isRecord(state.accepted);
528
+ if (invalidState) {
529
+ return failedResolution({
530
+ ...options,
531
+ context: transportContext(options.context, options.tool.name, "invalid", state.round),
532
+ code: "INVALID_REQUEST_STATE",
533
+ message: "Continuation state does not match this operation, arguments or round"
534
+ });
535
+ }
536
+ const request = requests[state.round];
537
+ if (!request)
538
+ throw new Error("[stitchkit] validated MRTR state points outside its policy");
539
+ const view = inputResponse(options.context.mcpReq.inputResponses, request.key);
540
+ if (view.kind !== "elicit") {
541
+ return failedResolution({
542
+ ...options,
543
+ context: transportContext(options.context, options.tool.name, "invalid", state.round),
544
+ code: "INVALID_INPUT_RESPONSE",
545
+ message: "Expected an elicitation response for the current round"
546
+ });
547
+ }
548
+ if (view.action !== "accept") {
549
+ const outcome = view.action === "decline" ? "declined" : "cancelled";
550
+ return failedResolution({
551
+ ...options,
552
+ context: transportContext(options.context, options.tool.name, outcome, state.round),
553
+ code: view.action === "decline" ? "INPUT_DECLINED" : "INPUT_CANCELLED",
554
+ message: view.action === "decline" ? "Required input was declined" : "Required input was cancelled"
555
+ });
556
+ }
557
+ const content = acceptedContent(options.context.mcpReq.inputResponses, request.key, request.schema);
558
+ if (!content) {
559
+ return failedResolution({
560
+ ...options,
561
+ context: transportContext(options.context, options.tool.name, "invalid", state.round),
562
+ code: "INVALID_INPUT_RESPONSE",
563
+ message: "Accepted input failed its declared schema"
564
+ });
565
+ }
566
+ const accepted = { ...state.accepted, [request.key]: content };
567
+ const nextRound = state.round + 1;
568
+ const nextRequest = requests[nextRound];
569
+ if (nextRequest) {
570
+ const guarded = await runRoundSuccess(options.tool, options.rawArgs, options.runTool, transportContext(options.context, options.tool.name, "input_required", nextRound));
571
+ if (!guarded.ok) {
572
+ return { kind: "response", response: options.formatFailure(guarded) };
573
+ }
574
+ const requestState = await options.runtime.codec.mint({
575
+ identity: state.identity,
576
+ argumentsDigest: state.argumentsDigest,
577
+ round: nextRound,
578
+ accepted
579
+ }, options.context);
580
+ return {
581
+ kind: "response",
582
+ response: inputRequired({
583
+ inputRequests: {
584
+ [nextRequest.key]: inputRequired.elicit({
585
+ message: nextRequest.message,
586
+ requestedSchema: nextRequest.schema
587
+ })
588
+ },
589
+ requestState
590
+ })
591
+ };
592
+ }
593
+ return {
594
+ kind: "continue",
595
+ context: {
596
+ ...transportContext(options.context, options.tool.name, "complete", state.round),
597
+ mcpInput: accepted
598
+ }
599
+ };
600
+ }
601
+
602
+ // src/tools/mcp-trace.ts
603
+ import {
604
+ BAGGAGE_META_KEY,
605
+ TRACEPARENT_META_KEY,
606
+ TRACESTATE_META_KEY
607
+ } from "@modelcontextprotocol/server";
608
+ function runInMcpRequestContext(context, toolName, body) {
609
+ const ambient = getRequestContext();
610
+ const metadata = context.mcpReq._meta;
611
+ const propagation = metadata ? {
612
+ traceparent: metadata[TRACEPARENT_META_KEY],
613
+ tracestate: metadata[TRACESTATE_META_KEY],
614
+ baggage: metadata[BAGGAGE_META_KEY]
615
+ } : undefined;
616
+ const request = {
617
+ ...ambient ?? {
618
+ source: "mcp",
619
+ method: "MCP",
620
+ path: `/mcp/${toolName}`,
621
+ startedAt: process.hrtime.bigint()
622
+ },
623
+ trace: resolvePropagationContext(propagation, ambient?.trace),
624
+ source: "mcp",
625
+ method: "MCP",
626
+ path: `/mcp/${toolName}`,
627
+ startedAt: process.hrtime.bigint(),
628
+ dimensions: ambient?.dimensions ? { ...ambient.dimensions } : undefined,
629
+ error: undefined
630
+ };
631
+ return runWithRequestContext(request, body);
632
+ }
633
+
361
634
  // src/tools/native-mcp.ts
362
- import { CallToolResultSchema } from "@modelcontextprotocol/sdk/types.js";
635
+ import {
636
+ isCallToolResult
637
+ } from "@modelcontextprotocol/server";
363
638
  import { z } from "zod";
364
- function isCallToolResult(value) {
365
- return CallToolResultSchema.safeParse(value).success;
366
- }
367
639
  function mountPreparedRuntimeMcp(server, tools, config) {
368
640
  for (const { definition, descriptor } of tools) {
369
641
  const { mountable } = descriptor;
@@ -381,35 +653,47 @@ function mountPreparedRuntimeMcp(server, tools, config) {
381
653
  "ui/resourceUri": definition.ui.resourceUri
382
654
  };
383
655
  }
384
- server.registerTool(definition.name, toolConfig, async (rawArgs) => {
656
+ server.registerTool(definition.name, toolConfig, async (rawArgs, mcpContext) => runInMcpRequestContext(mcpContext, definition.name, async () => {
385
657
  const args = isRecord(rawArgs) ? rawArgs : {};
386
- const result = await executeToolMethod(mountable.method, definition.name, args, { ...config.context, source: "mcp" }, config.hooks, config.lifecycle, config.coerceJsonArgs ?? true, config.onOutputStrip ? (paths) => config.onOutputStrip?.(definition.name, paths) : undefined);
387
- if (!result.ok || !definition.present?.mcp) {
658
+ const runTool = (tool2, toolArgs, roundContext) => executeToolMethod(tool2.method, definition.name, toolArgs, { ...config.context, ...roundContext, source: "mcp" }, config.hooks, config.lifecycle, config.coerceJsonArgs ?? true, config.onOutputStrip ? (paths) => config.onOutputStrip?.(definition.name, paths) : undefined);
659
+ const round = await resolveMcpRound({
660
+ tool: mountable,
661
+ rawArgs: args,
662
+ context: mcpContext,
663
+ policy: mountable.method.mcp,
664
+ runtime: config.multiRoundRuntime,
665
+ runTool,
666
+ formatFailure: (result) => config.formatResult(result, "none", definition.name)
667
+ });
668
+ if (round.kind === "response")
669
+ return round.response;
670
+ if (!definition.present?.mcp) {
671
+ const result = await runTool(mountable, args, round.context);
388
672
  return config.formatResult(result, descriptor.outputMode, definition.name);
389
673
  }
390
- const presented = await definition.present.mcp(result.data);
391
- if ("structuredContent" in presented || "isError" in presented) {
392
- return config.formatResult({
393
- ok: false,
394
- code: "INTERNAL_SERVER_ERROR",
395
- details: {
396
- message: "Runtime MCP presenter cannot set framework-owned structuredContent or isError"
397
- }
398
- }, "none", definition.name);
399
- }
400
- const structured = config.formatResult(result, descriptor.outputMode, definition.name).structuredContent;
401
- const response = {
402
- ...presented,
403
- ...structured && { structuredContent: structured }
404
- };
405
- if (isCallToolResult(response))
674
+ const presentedResult = await executeToolMethod(mountable.method, definition.name, args, { ...config.context, ...round.context, source: "mcp" }, config.hooks, config.lifecycle, config.coerceJsonArgs ?? true, config.onOutputStrip ? (paths) => config.onOutputStrip?.(definition.name, paths) : undefined, undefined, async (data) => {
675
+ const presented = await definition.present?.mcp?.(data);
676
+ if (!presented) {
677
+ throw new AppError("INTERNAL_SERVER_ERROR", "Runtime MCP presenter did not return a result", 500);
678
+ }
679
+ if ("structuredContent" in presented || "isError" in presented) {
680
+ throw new AppError("INTERNAL_SERVER_ERROR", "Runtime MCP presenter cannot set framework-owned structuredContent or isError", 500);
681
+ }
682
+ const structured = config.formatResult({ ok: true, data }, descriptor.outputMode, definition.name).structuredContent;
683
+ const response = structured === undefined ? presented : { ...presented, structuredContent: structured };
684
+ if (!isCallToolResult(response)) {
685
+ throw new AppError("INTERNAL_SERVER_ERROR", "Runtime MCP presenter returned an invalid CallToolResult", 500);
686
+ }
406
687
  return response;
407
- return config.formatResult({
408
- ok: false,
409
- code: "INTERNAL_SERVER_ERROR",
410
- details: { message: "Runtime MCP presenter returned an invalid CallToolResult" }
411
- }, "none", definition.name);
412
- });
688
+ });
689
+ if (!presentedResult.ok) {
690
+ return config.formatResult(presentedResult, "none", definition.name);
691
+ }
692
+ if (!isCallToolResult(presentedResult.data)) {
693
+ throw new Error("[stitchkit] Runtime MCP presenter invariant failed");
694
+ }
695
+ return presentedResult.data;
696
+ }));
413
697
  }
414
698
  }
415
699
 
@@ -576,14 +860,13 @@ function textBlock(text) {
576
860
  }
577
861
  function formatMcpResult(result, mode, toolName, errorHint) {
578
862
  if (result.ok) {
579
- const content = textBlock(JSON.stringify(result.data, null, 2));
580
- if (mode === "wrapped") {
581
- return { content, structuredContent: { result: result.data } };
863
+ if (mode === "none")
864
+ return { content: [] };
865
+ const serialized = JSON.stringify(result.data, null, 2);
866
+ if (serialized === undefined) {
867
+ throw new Error(`[stitchkit] MCP tool "${toolName ?? "unknown"}" produced a non-JSON output`);
582
868
  }
583
- if (mode === "direct" && isRecord(result.data)) {
584
- return { content, structuredContent: result.data };
585
- }
586
- return { content };
869
+ return { content: textBlock(serialized), structuredContent: result.data };
587
870
  }
588
871
  return {
589
872
  content: textBlock(JSON.stringify(formatToolError(result, toolName, errorHint), null, 2)),
@@ -601,10 +884,7 @@ function probeSchema(schema, io) {
601
884
  function resolveOutputSchema(outputSchema) {
602
885
  if (!outputSchema)
603
886
  return null;
604
- if (outputSchema instanceof z2.ZodObject) {
605
- return { schema: outputSchema, mode: "direct" };
606
- }
607
- return { schema: z2.object({ result: outputSchema }), mode: "wrapped" };
887
+ return { schema: outputSchema, mode: "direct" };
608
888
  }
609
889
  function reportIncompatible(message, policy, logger, failures) {
610
890
  if (policy === "throw") {
@@ -774,15 +1054,26 @@ function mountPreparedMcp(mcpServer, prepared, config = {}) {
774
1054
  "ui/resourceUri": mountable.method.ui.resourceUri
775
1055
  };
776
1056
  }
777
- mcpServer.registerTool(mountable.name, toolConfig, async (rawArgs) => {
1057
+ mcpServer.registerTool(mountable.name, toolConfig, async (rawArgs, mcpContext) => runInMcpRequestContext(mcpContext, mountable.name, async () => {
778
1058
  const args = isRecord(rawArgs) ? rawArgs : {};
779
1059
  try {
780
- const result = await runTool(mountable, args);
1060
+ const round = await resolveMcpRound({
1061
+ tool: mountable,
1062
+ rawArgs: args,
1063
+ context: mcpContext,
1064
+ policy: mountable.method.mcp,
1065
+ runtime: config.multiRoundRuntime,
1066
+ runTool,
1067
+ formatFailure: (result2) => formatMcpResult(result2, "none", mountable.name, config.errorHint)
1068
+ });
1069
+ if (round.kind === "response")
1070
+ return round.response;
1071
+ const result = await runTool(mountable, args, round.context);
781
1072
  return formatMcpResult(result, descriptor.outputMode, mountable.name, config.errorHint);
782
1073
  } catch (err) {
783
1074
  return formatMcpResult(toolResultFromError(err), "none", mountable.name, config.errorHint);
784
1075
  }
785
- });
1076
+ }));
786
1077
  }
787
1078
  }
788
1079
  function buildMcpServer(config, auth) {
@@ -810,7 +1101,29 @@ function buildMcpServer(config, auth) {
810
1101
  return buildMcpServerFromPrepared(config, auth, prepared);
811
1102
  }
812
1103
  function buildMcpServerFromPrepared(config, auth, prepared) {
813
- const server = new McpServer(config.serverInfo, config.instructions ? { instructions: config.instructions } : undefined);
1104
+ let roundCodec;
1105
+ if (config.multiRound) {
1106
+ const principal = config.multiRound.state.principal(auth);
1107
+ roundCodec = createRequestStateCodec({
1108
+ key: config.multiRound.state.key,
1109
+ ttlSeconds: config.multiRound.state.ttlSeconds,
1110
+ bind: (mcpContext) => `${mcpContext.mcpReq.method}\x00${principal}`
1111
+ });
1112
+ }
1113
+ const dynamicSurface = !config.surfaces && (typeof config.services === "function" || typeof config.runtimeTools === "function");
1114
+ const serverOptions = {
1115
+ ...config.instructions !== undefined && { instructions: config.instructions },
1116
+ ...!dynamicSurface && config.cache?.operations !== undefined && {
1117
+ cacheHints: config.cache.operations
1118
+ },
1119
+ ...config.multiRound?.serving !== undefined && {
1120
+ inputRequired: config.multiRound.serving
1121
+ },
1122
+ ...roundCodec !== undefined && {
1123
+ requestState: { verify: roundCodec.verify }
1124
+ }
1125
+ };
1126
+ const server = new McpServer(config.serverInfo, serverOptions);
814
1127
  const context = config.context?.(auth);
815
1128
  mountPreparedMcp(server, prepared.contractTools, {
816
1129
  context,
@@ -822,7 +1135,13 @@ function buildMcpServerFromPrepared(config, auth, prepared) {
822
1135
  coerceJsonArgs: config.coerceJsonArgs,
823
1136
  flattenUnionInput: config.flattenUnionInput,
824
1137
  errorHint: config.errorHint,
825
- onOutputStrip: config.onOutputStrip
1138
+ onOutputStrip: config.onOutputStrip,
1139
+ ...roundCodec !== undefined && {
1140
+ multiRoundRuntime: {
1141
+ codec: roundCodec,
1142
+ maxRounds: config.multiRound?.serving?.maxRounds ?? 10
1143
+ }
1144
+ }
826
1145
  });
827
1146
  mountPreparedRuntimeMcp(server, prepared.runtimeTools, {
828
1147
  context,
@@ -830,7 +1149,13 @@ function buildMcpServerFromPrepared(config, auth, prepared) {
830
1149
  lifecycle: config.lifecycle,
831
1150
  coerceJsonArgs: config.coerceJsonArgs,
832
1151
  onOutputStrip: config.onOutputStrip,
833
- formatResult: (result, mode, toolName) => formatMcpResult(result, mode, toolName, config.errorHint)
1152
+ formatResult: (result, mode, toolName) => formatMcpResult(result, mode, toolName, config.errorHint),
1153
+ ...roundCodec !== undefined && {
1154
+ multiRoundRuntime: {
1155
+ codec: roundCodec,
1156
+ maxRounds: config.multiRound?.serving?.maxRounds ?? 10
1157
+ }
1158
+ }
834
1159
  });
835
1160
  config.rawTools?.(server, auth);
836
1161
  for (const resource of config.resources ?? []) {
@@ -839,7 +1164,10 @@ function buildMcpServerFromPrepared(config, auth, prepared) {
839
1164
  return server;
840
1165
  }
841
1166
  function mountMcpResource(server, resource) {
842
- server.registerResource(resource.name, resource.uri, { mimeType: resource.mimeType ?? RESOURCE_MIME_TYPE }, async () => ({
1167
+ server.registerResource(resource.name, resource.uri, {
1168
+ mimeType: resource.mimeType ?? RESOURCE_MIME_TYPE,
1169
+ ...resource.cacheHint !== undefined && { cacheHint: resource.cacheHint }
1170
+ }, async () => ({
843
1171
  contents: [
844
1172
  {
845
1173
  uri: resource.uri,
@@ -851,10 +1179,11 @@ function mountMcpResource(server, resource) {
851
1179
  }));
852
1180
  }
853
1181
  // src/tools/mcp-handler.ts
854
- import { randomUUID } from "node:crypto";
855
1182
  import {
856
- WebStandardStreamableHTTPServerTransport
857
- } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
1183
+ createMcpHandler as createSdkMcpHandler,
1184
+ hostHeaderValidationResponse,
1185
+ originValidationResponse
1186
+ } from "@modelcontextprotocol/server";
858
1187
 
859
1188
  // src/tools/oauth-metadata.ts
860
1189
  var PROTECTED_RESOURCE_PATH = "/.well-known/oauth-protected-resource";
@@ -897,53 +1226,26 @@ function oauthProtectedResourceRoute(config) {
897
1226
  }
898
1227
 
899
1228
  // src/tools/mcp-handler.ts
900
- var EVENT_TTL_MS = 10 * 60 * 1000;
901
- var SESSION_TTL_MS = 30 * 60 * 1000;
902
- var SWEEP_INTERVAL_MS = 60 * 1000;
903
- var MAX_EVENTS = 1e4;
904
- var MAX_SESSIONS = 1000;
905
-
906
- class InMemoryEventStore {
907
- events = new Map;
908
- counter = 0;
909
- async storeEvent(streamId, message) {
910
- const eventId = String(++this.counter);
911
- this.events.set(eventId, { streamId, message, timestamp: Date.now() });
912
- if (this.events.size > MAX_EVENTS) {
913
- const oldest = this.events.keys().next().value;
914
- if (oldest !== undefined)
915
- this.events.delete(oldest);
916
- }
917
- return eventId;
918
- }
919
- async getStreamIdForEventId(eventId) {
920
- return this.events.get(eventId)?.streamId;
921
- }
922
- async replayEventsAfter(lastEventId, { send }) {
923
- const anchor = this.events.get(lastEventId);
924
- if (!anchor)
925
- return "";
926
- const lastIdNum = Number(lastEventId);
927
- for (const [eventId, event] of this.events) {
928
- if (event.streamId === anchor.streamId && Number(eventId) > lastIdNum) {
929
- await send(eventId, event.message);
930
- }
931
- }
932
- return anchor.streamId;
933
- }
934
- cleanup() {
935
- const cutoff = Date.now() - EVENT_TTL_MS;
936
- for (const [eventId, event] of this.events) {
937
- if (event.timestamp < cutoff)
938
- this.events.delete(eventId);
939
- }
940
- }
941
- }
942
1229
  function jsonRpcError(code, message, status, headers) {
943
1230
  return Response.json({ jsonrpc: "2.0", error: { code, message }, id: null }, { status, headers });
944
1231
  }
945
- function createMcpHandler(config) {
946
- let buildServer;
1232
+ function defaultHostRejection(request) {
1233
+ const host = request.headers.get("host");
1234
+ if (!host || host === new URL(request.url).host)
1235
+ return;
1236
+ return jsonRpcError(-32000, "Invalid Host header", 403);
1237
+ }
1238
+ function defaultOriginRejection(request) {
1239
+ const origin = request.headers.get("origin");
1240
+ if (!origin)
1241
+ return;
1242
+ try {
1243
+ if (new URL(origin).origin === new URL(request.url).origin)
1244
+ return;
1245
+ } catch {}
1246
+ return jsonRpcError(-32000, "Invalid Origin header", 403);
1247
+ }
1248
+ function prepareStaticSurface(config) {
947
1249
  const preparation = {
948
1250
  logger: config.logger,
949
1251
  extend: config.extend,
@@ -951,7 +1253,6 @@ function createMcpHandler(config) {
951
1253
  schemaValidation: config.schemaValidation
952
1254
  };
953
1255
  if (config.surfaces && config.selectSurface) {
954
- const selectSurface = config.selectSurface;
955
1256
  const preparedByKey = new Map;
956
1257
  const preparedBySurface = new WeakMap;
957
1258
  for (const key in config.surfaces) {
@@ -966,110 +1267,111 @@ function createMcpHandler(config) {
966
1267
  preparedBySurface.set(surface, prepared);
967
1268
  preparedByKey.set(key, prepared);
968
1269
  }
969
- buildServer = (auth) => {
970
- const key = selectSurface(auth);
1270
+ return (auth) => {
1271
+ const key = config.selectSurface(auth);
971
1272
  const prepared = preparedByKey.get(key);
972
1273
  if (!prepared)
973
1274
  throw new Error(`[stitchkit] Unknown MCP surface "${key}"`);
974
1275
  return buildMcpServerFromPrepared(config, auth, prepared);
975
1276
  };
976
- } else if (config.services && typeof config.services !== "function" && typeof config.runtimeTools !== "function") {
1277
+ }
1278
+ if (config.services && typeof config.services !== "function" && typeof config.runtimeTools !== "function") {
977
1279
  const prepared = prepareMcpServerSurface({ services: config.services, runtimeTools: config.runtimeTools }, preparation);
978
- buildServer = (auth) => buildMcpServerFromPrepared(config, auth, prepared);
979
- } else {
980
- buildServer = (auth) => buildMcpServer(config, auth);
1280
+ return (auth) => buildMcpServerFromPrepared(config, auth, prepared);
981
1281
  }
1282
+ return null;
1283
+ }
1284
+ function createMcpHandler(config) {
1285
+ const buildPrepared = prepareStaticSurface(config);
1286
+ const authByCarrier = new WeakMap;
1287
+ let closed = false;
1288
+ let resolveClosed;
1289
+ const closedSignal = new Promise((resolve) => {
1290
+ resolveClosed = resolve;
1291
+ });
1292
+ const sdkHandler = createSdkMcpHandler(({ authInfo }) => {
1293
+ const resolved = authInfo ? authByCarrier.get(authInfo) : undefined;
1294
+ if (!resolved) {
1295
+ throw new Error("[stitchkit] MCP request reached the SDK without resolved auth");
1296
+ }
1297
+ const auth = resolved.value;
1298
+ return buildPrepared ? buildPrepared(auth) : buildMcpServer(config, auth);
1299
+ }, {
1300
+ legacy: config.legacy === "reject" ? "reject" : "stateless",
1301
+ maxSubscriptions: 0
1302
+ });
982
1303
  const unauthorized = () => {
983
1304
  const headers = config.protectedResource ? { "WWW-Authenticate": wwwAuthenticateHeader(config.protectedResource.resource) } : undefined;
984
1305
  return jsonRpcError(-32001, "Authorization required", 401, headers);
985
1306
  };
986
- if ((config.sessionMode ?? "stateless") === "stateless") {
987
- return async (req) => {
988
- const auth = await config.auth(req);
989
- if (!auth)
990
- return unauthorized();
991
- const transport = new WebStandardStreamableHTTPServerTransport({
992
- sessionIdGenerator: undefined,
993
- enableJsonResponse: true
994
- });
995
- const server = buildServer(auth);
996
- await server.connect(transport);
997
- return transport.handleRequest(req);
998
- };
999
- }
1000
- const eventStore = new InMemoryEventStore;
1001
- const sessions = new Map;
1002
- const closeTransport = (transport) => {
1003
- transport.close().catch((err) => {
1004
- console.error("[stitchkit] MCP transport close failed:", err);
1307
+ const observeRejection = (request, response) => {
1308
+ if (response.status < 400 || !config.onTransportRejected)
1309
+ return response;
1310
+ Promise.resolve(config.onTransportRejected({ request, response: response.clone() })).catch(() => {
1311
+ return;
1005
1312
  });
1313
+ return response;
1006
1314
  };
1007
- setInterval(() => {
1008
- eventStore.cleanup();
1009
- const cutoff = Date.now() - SESSION_TTL_MS;
1010
- for (const [id, session] of sessions) {
1011
- if (session.lastSeen < cutoff) {
1012
- sessions.delete(id);
1013
- closeTransport(session.transport);
1014
- }
1015
- }
1016
- }, SWEEP_INTERVAL_MS).unref();
1017
- return async (req) => {
1018
- const auth = await config.auth(req);
1019
- if (!auth)
1020
- return unauthorized();
1021
- const sessionId = req.headers.get("mcp-session-id");
1022
- if (sessionId) {
1023
- const existing = sessions.get(sessionId);
1024
- if (!existing) {
1025
- return jsonRpcError(-32001, "Session not found", 404);
1026
- }
1027
- existing.lastSeen = Date.now();
1028
- if (req.method === "GET") {
1029
- existing.transport.closeStandaloneSSEStream();
1315
+ return {
1316
+ fetch: async (request) => {
1317
+ if (closed) {
1318
+ return observeRejection(request, jsonRpcError(-32000, "MCP handler is closed", 503));
1030
1319
  }
1031
- return existing.transport.handleRequest(req);
1032
- }
1033
- if (sessions.size >= MAX_SESSIONS) {
1034
- let oldestId;
1035
- let oldestSeen = Number.POSITIVE_INFINITY;
1036
- for (const [id, session] of sessions) {
1037
- if (session.lastSeen < oldestSeen) {
1038
- oldestSeen = session.lastSeen;
1039
- oldestId = id;
1040
- }
1320
+ const hostRejection = config.security?.allowedHosts ? hostHeaderValidationResponse(request, [...config.security.allowedHosts]) : defaultHostRejection(request);
1321
+ if (hostRejection)
1322
+ return observeRejection(request, hostRejection);
1323
+ const originRejection = config.security?.allowedOrigins ? originValidationResponse(request, [...config.security.allowedOrigins]) : defaultOriginRejection(request);
1324
+ if (originRejection)
1325
+ return observeRejection(request, originRejection);
1326
+ const auth = await Promise.race([
1327
+ Promise.resolve(config.auth(request)),
1328
+ closedSignal.then(() => null)
1329
+ ]);
1330
+ if (closed) {
1331
+ return observeRejection(request, jsonRpcError(-32000, "MCP handler is closed", 503));
1041
1332
  }
1042
- if (oldestId !== undefined) {
1043
- const evicted = sessions.get(oldestId);
1044
- sessions.delete(oldestId);
1045
- if (evicted)
1046
- closeTransport(evicted.transport);
1333
+ if (auth === null)
1334
+ return observeRejection(request, unauthorized());
1335
+ const authCarrier = {
1336
+ token: "",
1337
+ clientId: config.multiRound?.state.principal(auth) ?? "stitchkit",
1338
+ scopes: []
1339
+ };
1340
+ authByCarrier.set(authCarrier, { value: auth });
1341
+ try {
1342
+ return observeRejection(request, await sdkHandler.fetch(request, { authInfo: authCarrier }));
1343
+ } finally {
1344
+ authByCarrier.delete(authCarrier);
1047
1345
  }
1346
+ },
1347
+ close: async () => {
1348
+ if (closed)
1349
+ return;
1350
+ closed = true;
1351
+ resolveClosed?.();
1352
+ await sdkHandler.close();
1048
1353
  }
1049
- const newSessionId = randomUUID();
1050
- const transport = new WebStandardStreamableHTTPServerTransport({
1051
- sessionIdGenerator: () => newSessionId,
1052
- eventStore
1053
- });
1054
- const server = buildServer(auth);
1055
- sessions.set(newSessionId, { transport, server, lastSeen: Date.now() });
1056
- transport.onclose = () => {
1057
- sessions.delete(newSessionId);
1058
- };
1059
- await server.connect(transport);
1060
- return transport.handleRequest(req);
1354
+ };
1355
+ }
1356
+ function createMcpHttpRoute(config) {
1357
+ return {
1358
+ method: "ALL",
1359
+ path: config.path,
1360
+ handler: (request) => config.handler.fetch(request)
1061
1361
  };
1062
1362
  }
1063
1363
  // src/tools/mcp-stdio.ts
1064
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
1364
+ import { serveStdio } from "@modelcontextprotocol/server/stdio";
1065
1365
  async function createStdioMcpServer(config) {
1066
1366
  const auth = await config.auth;
1067
- const server = buildMcpServer(config, auth);
1068
- await server.connect(new StdioServerTransport);
1069
- return server;
1367
+ return serveStdio(() => buildMcpServer(config, auth), {
1368
+ legacy: config.legacy ?? "serve",
1369
+ maxSubscriptions: 0
1370
+ });
1070
1371
  }
1071
1372
  // src/tools/mount-download.ts
1072
1373
  import { basename, extname, join } from "node:path";
1374
+ import { z as z3 } from "zod";
1073
1375
 
1074
1376
  // src/tools/native-result.ts
1075
1377
  function textResult(text, isError = false) {
@@ -1111,7 +1413,7 @@ function baseNameFor(url) {
1111
1413
  function mountDownload(server, config) {
1112
1414
  const name = config.name ?? "download";
1113
1415
  assertToolName(name, "<native>", "download");
1114
- server.registerTool(name, { description: config.description, inputSchema: config.inputSchema }, async (rawArgs) => {
1416
+ server.registerTool(name, { description: config.description, inputSchema: z3.object(config.inputSchema) }, async (rawArgs) => {
1115
1417
  const args = isRecord(rawArgs) ? rawArgs : {};
1116
1418
  try {
1117
1419
  const url = await config.resolveUrl(args);
@@ -1140,13 +1442,13 @@ function mountDownload(server, config) {
1140
1442
  });
1141
1443
  }
1142
1444
  // src/tools/mount-upload.ts
1143
- import { z as z3 } from "zod";
1445
+ import { z as z4 } from "zod";
1144
1446
  function mountUpload(server, config) {
1145
1447
  const name = config.name ?? "upload";
1146
1448
  assertToolName(name, "<native>", "upload");
1147
1449
  server.registerTool(name, {
1148
1450
  description: config.description,
1149
- inputSchema: { path: z3.string().describe("Path to a local file on this machine") }
1451
+ inputSchema: { path: z4.string().describe("Path to a local file on this machine") }
1150
1452
  }, async (rawArgs) => {
1151
1453
  const args = isRecord(rawArgs) ? rawArgs : {};
1152
1454
  const path = typeof args.path === "string" ? args.path : "";
@@ -1161,10 +1463,11 @@ function mountUpload(server, config) {
1161
1463
  });
1162
1464
  }
1163
1465
  // src/tools/mount-wait.ts
1466
+ import { z as z5 } from "zod";
1164
1467
  function mountWait(server, config) {
1165
1468
  const name = config.name ?? "wait";
1166
1469
  assertToolName(name, "<native>", "wait");
1167
- server.registerTool(name, { description: config.description, inputSchema: config.inputSchema }, async (rawArgs) => {
1470
+ server.registerTool(name, { description: config.description, inputSchema: z5.object(config.inputSchema) }, async (rawArgs) => {
1168
1471
  const args = isRecord(rawArgs) ? rawArgs : {};
1169
1472
  try {
1170
1473
  const { state, timedOut } = await pollUntil({
@@ -1181,7 +1484,35 @@ function mountWait(server, config) {
1181
1484
  });
1182
1485
  }
1183
1486
  // src/tools/oauth-provider.ts
1184
- import { randomUUID as randomUUID2 } from "node:crypto";
1487
+ import { randomUUID } from "node:crypto";
1488
+ import { z as z6 } from "zod";
1489
+ function isPlainDisplayCharacter(character) {
1490
+ const codePoint = character.codePointAt(0);
1491
+ return codePoint !== undefined && codePoint >= 32 && codePoint !== 127 && character !== "<" && character !== ">";
1492
+ }
1493
+ var CimdClientMetadataSchema = z6.object({
1494
+ client_id: z6.url(),
1495
+ redirect_uris: z6.array(z6.url()).min(1),
1496
+ client_name: z6.string().trim().min(1).max(200).refine((value) => Array.from(value).every(isPlainDisplayCharacter), {
1497
+ message: "client_name must be plain display text"
1498
+ }),
1499
+ token_endpoint_auth_method: z6.literal("none"),
1500
+ application_type: z6.enum(["native", "web"]).optional()
1501
+ });
1502
+ function createSecureClientMetadataFetcher(policy = {}) {
1503
+ assertPositiveInteger("cimd.maxBytes", policy.maxBytes ?? DEFAULT_CIMD_MAX_BYTES);
1504
+ assertPositiveNumber("cimd.timeoutMs", policy.timeoutMs ?? DEFAULT_CIMD_TIMEOUT_MS);
1505
+ assertNonNegativeInteger("cimd.maxRedirects", policy.maxRedirects ?? 3);
1506
+ return {
1507
+ fetch: (url, headers) => fetchPinnedDocument(url, {
1508
+ maxBytes: policy.maxBytes ?? DEFAULT_CIMD_MAX_BYTES,
1509
+ timeoutMs: policy.timeoutMs ?? DEFAULT_CIMD_TIMEOUT_MS,
1510
+ maxRedirects: policy.maxRedirects ?? 3,
1511
+ headers,
1512
+ requireHttps: true
1513
+ })
1514
+ };
1515
+ }
1185
1516
  var PUBLIC_CORS2 = {
1186
1517
  "Access-Control-Allow-Origin": "*",
1187
1518
  "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
@@ -1189,6 +1520,214 @@ var PUBLIC_CORS2 = {
1189
1520
  };
1190
1521
  var AS_METADATA_PATH = "/.well-known/oauth-authorization-server";
1191
1522
  var AUTH_CODE_TTL_MS = 60000;
1523
+ var DEFAULT_CIMD_MAX_BYTES = 64 * 1024;
1524
+ var DEFAULT_CIMD_TIMEOUT_MS = 5000;
1525
+ function assertPositiveInteger(name, value) {
1526
+ if (!Number.isInteger(value) || value < 1)
1527
+ throw new Error(`${name} must be a positive integer`);
1528
+ }
1529
+ function assertNonNegativeInteger(name, value) {
1530
+ if (!Number.isInteger(value) || value < 0) {
1531
+ throw new Error(`${name} must be a non-negative integer`);
1532
+ }
1533
+ }
1534
+ function assertPositiveNumber(name, value) {
1535
+ if (!Number.isFinite(value) || value <= 0)
1536
+ throw new Error(`${name} must be positive`);
1537
+ }
1538
+ function absoluteClientUrl(clientId) {
1539
+ try {
1540
+ return new URL(clientId);
1541
+ } catch {
1542
+ return null;
1543
+ }
1544
+ }
1545
+ function assertCimdClientId(clientId) {
1546
+ const url = new URL(clientId);
1547
+ if (url.protocol !== "https:")
1548
+ throw new Error("CIMD client_id must use https");
1549
+ if (url.username || url.password)
1550
+ throw new Error("CIMD client_id cannot contain credentials");
1551
+ if (url.hash)
1552
+ throw new Error("CIMD client_id cannot contain a fragment");
1553
+ if (url.search)
1554
+ throw new Error("CIMD client_id cannot contain a query");
1555
+ if (url.toString() !== clientId) {
1556
+ throw new Error("CIMD client_id must be an exact canonical URL");
1557
+ }
1558
+ return url;
1559
+ }
1560
+ function responseFreshness(headers, policy, now) {
1561
+ const maxTtl = policy.maxTtlMs ?? 3600000;
1562
+ const cacheControl = headers.get("cache-control") ?? "";
1563
+ if (/(?:^|,)\s*no-store\s*(?:,|$)/i.test(cacheControl)) {
1564
+ return { freshnessMs: 0, store: false };
1565
+ }
1566
+ if (/(?:^|,)\s*no-cache\s*(?:,|$)/i.test(cacheControl)) {
1567
+ return { freshnessMs: 0, store: true };
1568
+ }
1569
+ const maxAge = cacheControl.match(/(?:^|,)\s*max-age=(\d+)/i)?.[1];
1570
+ if (maxAge !== undefined) {
1571
+ const age = Number(headers.get("age") ?? 0);
1572
+ return {
1573
+ freshnessMs: Math.max(0, Math.min(maxTtl, (Number(maxAge) - age) * 1000)),
1574
+ store: true
1575
+ };
1576
+ }
1577
+ const expires = headers.get("expires");
1578
+ if (expires) {
1579
+ const expiresAt = Date.parse(expires);
1580
+ if (Number.isFinite(expiresAt)) {
1581
+ const dateAt = Date.parse(headers.get("date") ?? "");
1582
+ const ageMs = Number(headers.get("age") ?? 0) * 1000;
1583
+ const lifetime = Number.isFinite(dateAt) ? expiresAt - dateAt : expiresAt - now;
1584
+ return {
1585
+ freshnessMs: Math.max(0, Math.min(maxTtl, lifetime - ageMs)),
1586
+ store: true
1587
+ };
1588
+ }
1589
+ }
1590
+ return { freshnessMs: Math.min(maxTtl, policy.defaultTtlMs ?? 300000), store: true };
1591
+ }
1592
+ function safeClientName(value) {
1593
+ if (value === undefined)
1594
+ return;
1595
+ const safe = Array.from(value).filter(isPlainDisplayCharacter).join("").trim().slice(0, 200);
1596
+ return safe || undefined;
1597
+ }
1598
+ function parseScopes(value) {
1599
+ if (!value)
1600
+ return [];
1601
+ return [...new Set(value.split(/\s+/u).filter(Boolean))];
1602
+ }
1603
+ function toRegisteredClient(metadata) {
1604
+ return {
1605
+ clientId: metadata.client_id,
1606
+ redirectUris: metadata.redirect_uris,
1607
+ ...metadata.client_name !== undefined && { clientName: metadata.client_name },
1608
+ ...metadata.application_type !== undefined && {
1609
+ applicationType: metadata.application_type
1610
+ }
1611
+ };
1612
+ }
1613
+ function isJsonContentType(value) {
1614
+ const mediaType = value.split(";", 1)[0]?.trim().toLowerCase();
1615
+ return mediaType === "application/json" || mediaType?.endsWith("+json") === true;
1616
+ }
1617
+ function createCimdResolver(options) {
1618
+ const policy = options.cache ?? {};
1619
+ const now = options.now ?? Date.now;
1620
+ const cache = new Map;
1621
+ const inflight = new Map;
1622
+ const maxEntries = policy.maxEntries ?? 256;
1623
+ assertPositiveInteger("cimd.cache.maxEntries", maxEntries);
1624
+ assertNonNegativeInteger("cimd.cache.defaultTtlMs", policy.defaultTtlMs ?? 300000);
1625
+ assertNonNegativeInteger("cimd.cache.maxTtlMs", policy.maxTtlMs ?? 3600000);
1626
+ assertNonNegativeInteger("cimd.cache.negativeTtlMs", policy.negativeTtlMs ?? 1e4);
1627
+ const emit = (event) => {
1628
+ try {
1629
+ options.onCacheEvent?.(event);
1630
+ } catch {}
1631
+ };
1632
+ const touch = (key, value) => {
1633
+ cache.delete(key);
1634
+ cache.set(key, value);
1635
+ while (cache.size > maxEntries) {
1636
+ const oldest = cache.keys().next().value;
1637
+ if (oldest === undefined)
1638
+ break;
1639
+ cache.delete(oldest);
1640
+ }
1641
+ };
1642
+ const resolveOne = async (clientId) => {
1643
+ const url = assertCimdClientId(clientId);
1644
+ const cached = cache.get(clientId);
1645
+ if (cached && cached.expiresAt > now()) {
1646
+ touch(clientId, cached);
1647
+ if (cached.ok) {
1648
+ emit({ clientId, status: "hit", freshnessMs: cached.expiresAt - now() });
1649
+ return cached.client;
1650
+ }
1651
+ emit({ clientId, status: "negative", freshnessMs: cached.expiresAt - now() });
1652
+ throw new Error(cached.message);
1653
+ }
1654
+ emit({ clientId, status: "miss" });
1655
+ const conditionalHeaders = {};
1656
+ if (cached?.ok && cached.etag)
1657
+ conditionalHeaders["if-none-match"] = cached.etag;
1658
+ if (cached?.ok && cached.lastModified) {
1659
+ conditionalHeaders["if-modified-since"] = cached.lastModified;
1660
+ }
1661
+ let cacheFailure = true;
1662
+ try {
1663
+ const response = await options.fetcher.fetch(url, conditionalHeaders);
1664
+ const freshness = responseFreshness(response.headers, policy, now());
1665
+ cacheFailure = freshness.store;
1666
+ if (response.status === 304 && cached?.ok) {
1667
+ const refreshed = {
1668
+ ...cached,
1669
+ expiresAt: now() + freshness.freshnessMs
1670
+ };
1671
+ if (freshness.store)
1672
+ touch(clientId, refreshed);
1673
+ else
1674
+ cache.delete(clientId);
1675
+ emit({ clientId, status: "revalidated", freshnessMs: freshness.freshnessMs });
1676
+ return refreshed.client;
1677
+ }
1678
+ if (response.status !== 200) {
1679
+ throw new Error(`CIMD endpoint returned HTTP ${response.status}`);
1680
+ }
1681
+ const contentType = response.headers.get("content-type") ?? "";
1682
+ if (!isJsonContentType(contentType)) {
1683
+ throw new Error("CIMD endpoint must return a JSON media type");
1684
+ }
1685
+ const decoded = JSON.parse(new TextDecoder().decode(response.body));
1686
+ const metadata = CimdClientMetadataSchema.parse(decoded);
1687
+ if (metadata.client_id !== clientId) {
1688
+ throw new Error("CIMD client_id does not exactly match the requested URL");
1689
+ }
1690
+ if (!metadata.redirect_uris.every((uri) => isRegistrableRedirectUri(uri, metadata.application_type))) {
1691
+ throw new Error("CIMD redirect_uris contains a URI forbidden for the application type");
1692
+ }
1693
+ const client = toRegisteredClient(metadata);
1694
+ if (freshness.store) {
1695
+ touch(clientId, {
1696
+ ok: true,
1697
+ client,
1698
+ expiresAt: now() + freshness.freshnessMs,
1699
+ ...response.headers.get("etag") !== null && {
1700
+ etag: response.headers.get("etag") ?? undefined
1701
+ },
1702
+ ...response.headers.get("last-modified") !== null && {
1703
+ lastModified: response.headers.get("last-modified") ?? undefined
1704
+ }
1705
+ });
1706
+ } else {
1707
+ cache.delete(clientId);
1708
+ }
1709
+ return client;
1710
+ } catch (error) {
1711
+ const message = error instanceof Error ? error.message : "CIMD metadata resolution failed";
1712
+ if (cacheFailure) {
1713
+ touch(clientId, {
1714
+ ok: false,
1715
+ message,
1716
+ expiresAt: now() + (policy.negativeTtlMs ?? 1e4)
1717
+ });
1718
+ }
1719
+ throw error;
1720
+ }
1721
+ };
1722
+ return async (clientId) => {
1723
+ const pending = inflight.get(clientId);
1724
+ if (pending)
1725
+ return pending;
1726
+ const created = resolveOne(clientId).finally(() => inflight.delete(clientId));
1727
+ inflight.set(clientId, created);
1728
+ return created;
1729
+ };
1730
+ }
1192
1731
  function json(body, status = 200, extraHeaders) {
1193
1732
  return new Response(JSON.stringify(body), {
1194
1733
  status,
@@ -1201,6 +1740,8 @@ function oauthError(error, description, status = 400) {
1201
1740
  function isRegistrableRedirectUri(value, applicationType) {
1202
1741
  try {
1203
1742
  const url = new URL(value);
1743
+ if (url.username || url.password || url.hash)
1744
+ return false;
1204
1745
  if (url.protocol === "https:")
1205
1746
  return true;
1206
1747
  if (url.protocol !== "http:")
@@ -1213,6 +1754,17 @@ function isRegistrableRedirectUri(value, applicationType) {
1213
1754
  return false;
1214
1755
  }
1215
1756
  }
1757
+ function isLoopbackRedirectUri(value) {
1758
+ try {
1759
+ const url = new URL(value);
1760
+ if (url.protocol !== "http:")
1761
+ return false;
1762
+ const host = url.hostname.replace(/^\[|\]$/g, "");
1763
+ return host === "127.0.0.1" || host === "::1" || host === "localhost";
1764
+ } catch {
1765
+ return false;
1766
+ }
1767
+ }
1216
1768
  function redirectWith(uri, params) {
1217
1769
  const url = new URL(uri);
1218
1770
  for (const [key, value] of Object.entries(params))
@@ -1240,6 +1792,33 @@ function mountOAuthProvider(config) {
1240
1792
  const registerPath = `${base}/register`;
1241
1793
  const authorizePath = `${base}/authorize`;
1242
1794
  const tokenPath = `${base}/token`;
1795
+ const clientRegistration = config.clientRegistration ?? {};
1796
+ const dcrRegistry = clientRegistration.dcr || undefined;
1797
+ const dcrEnabled = dcrRegistry !== undefined;
1798
+ const cimdConfig = clientRegistration.cimd === false ? undefined : clientRegistration.cimd ?? {};
1799
+ const cimdEnabled = cimdConfig !== undefined;
1800
+ const resolveCimd = cimdEnabled ? createCimdResolver({
1801
+ cache: cimdConfig.cache,
1802
+ fetcher: cimdConfig.fetcher ?? createSecureClientMetadataFetcher(),
1803
+ onCacheEvent: cimdConfig.onCacheEvent
1804
+ }) : undefined;
1805
+ const resolveClient = async (clientId) => {
1806
+ const preRegistered = await clientRegistration.preRegistered?.get(clientId);
1807
+ if (preRegistered)
1808
+ return preRegistered.clientId === clientId ? preRegistered : null;
1809
+ const clientUrl = absoluteClientUrl(clientId);
1810
+ if (clientUrl) {
1811
+ if (!resolveCimd || clientUrl.protocol !== "https:")
1812
+ return null;
1813
+ try {
1814
+ return await resolveCimd(clientId);
1815
+ } catch {
1816
+ return null;
1817
+ }
1818
+ }
1819
+ const dcrClient = await dcrRegistry?.get(clientId);
1820
+ return dcrClient?.clientId === clientId ? dcrClient : null;
1821
+ };
1243
1822
  const redirectToClient = (uri, params) => redirectWith(uri, { ...params, iss: config.issuer });
1244
1823
  const metadataRoute = {
1245
1824
  method: "ALL",
@@ -1251,11 +1830,12 @@ function mountOAuthProvider(config) {
1251
1830
  issuer: config.issuer,
1252
1831
  authorization_endpoint: `${config.issuer}${authorizePath}`,
1253
1832
  token_endpoint: `${config.issuer}${tokenPath}`,
1254
- registration_endpoint: `${config.issuer}${registerPath}`,
1833
+ ...dcrEnabled && { registration_endpoint: `${config.issuer}${registerPath}` },
1255
1834
  response_types_supported: ["code"],
1256
1835
  grant_types_supported: config.refreshTokens ? ["authorization_code", "refresh_token"] : ["authorization_code"],
1257
1836
  code_challenge_methods_supported: ["S256"],
1258
1837
  token_endpoint_auth_methods_supported: ["none"],
1838
+ ...cimdEnabled && { client_id_metadata_document_supported: true },
1259
1839
  authorization_response_iss_parameter_supported: true,
1260
1840
  ...config.scopesSupported && { scopes_supported: config.scopesSupported }
1261
1841
  });
@@ -1279,13 +1859,19 @@ function mountOAuthProvider(config) {
1279
1859
  }
1280
1860
  const applicationType = rawAppType;
1281
1861
  const redirectUris = meta.redirect_uris;
1862
+ const tokenEndpointAuthMethod = meta.token_endpoint_auth_method ?? "none";
1863
+ if (tokenEndpointAuthMethod !== "none") {
1864
+ return oauthError("invalid_client_metadata", 'Only public clients with token_endpoint_auth_method "none" are supported');
1865
+ }
1282
1866
  if (!Array.isArray(redirectUris) || redirectUris.length === 0 || !redirectUris.every((u) => typeof u === "string" && isRegistrableRedirectUri(u, applicationType))) {
1283
1867
  return oauthError("invalid_redirect_uri", applicationType === "web" ? "redirect_uris must be a non-empty array of absolute https URLs (a web client cannot register an http loopback URI)" : "redirect_uris must be a non-empty array of absolute https URLs (http is allowed only on a loopback host)");
1284
1868
  }
1285
- const client = await config.clients.register({
1869
+ if (!dcrRegistry)
1870
+ throw new Error("DCR route mounted without a DCR registry");
1871
+ const client = await dcrRegistry.register({
1286
1872
  redirectUris,
1287
1873
  clientName: typeof meta.client_name === "string" ? meta.client_name : undefined,
1288
- tokenEndpointAuthMethod: typeof meta.token_endpoint_auth_method === "string" ? meta.token_endpoint_auth_method : undefined,
1874
+ tokenEndpointAuthMethod,
1289
1875
  ...applicationType && { applicationType }
1290
1876
  });
1291
1877
  return json({
@@ -1318,7 +1904,7 @@ function mountOAuthProvider(config) {
1318
1904
  if (!clientId || !redirectUri) {
1319
1905
  return oauthError("invalid_request", "client_id and redirect_uri are required");
1320
1906
  }
1321
- const client = await config.clients.get(clientId);
1907
+ const client = await resolveClient(clientId);
1322
1908
  if (!client)
1323
1909
  return oauthError("invalid_client", "Unknown client_id", 401);
1324
1910
  if (!client.redirectUris.includes(redirectUri)) {
@@ -1351,18 +1937,49 @@ function mountOAuthProvider(config) {
1351
1937
  ...state && { state }
1352
1938
  });
1353
1939
  }
1354
- const authRequest = { clientId, redirectUri, scope, resource, state };
1940
+ const requestedScopes = parseScopes(scope);
1941
+ const supportedScopes = new Set(config.scopesSupported ?? []);
1942
+ if (config.scopesSupported && requestedScopes.some((requestedScope2) => !supportedScopes.has(requestedScope2))) {
1943
+ return redirectToClient(redirectUri, {
1944
+ error: "invalid_scope",
1945
+ error_description: "One or more requested scopes are not supported",
1946
+ ...state && { state }
1947
+ });
1948
+ }
1949
+ const requestedScope = requestedScopes.length > 0 ? requestedScopes.join(" ") : undefined;
1950
+ const authRequest = {
1951
+ clientId,
1952
+ redirectUri,
1953
+ scope: requestedScope,
1954
+ resource,
1955
+ state,
1956
+ isLoopbackRedirect: isLoopbackRedirectUri(redirectUri),
1957
+ ...safeClientName(client.clientName) !== undefined && {
1958
+ clientName: safeClientName(client.clientName)
1959
+ },
1960
+ ...client.applicationType !== undefined && {
1961
+ applicationType: client.applicationType
1962
+ },
1963
+ ...absoluteClientUrl(clientId)?.protocol === "https:" && {
1964
+ clientOrigin: new URL(clientId).origin
1965
+ }
1966
+ };
1355
1967
  const result = await config.authorizeUser(req, authRequest);
1356
1968
  if (result instanceof Response)
1357
1969
  return result;
1358
- const code = randomUUID2();
1970
+ const approvedScopes = [...new Set(result.approvedScopes)];
1971
+ if (approvedScopes.some((approvedScope2) => !requestedScopes.includes(approvedScope2) || config.scopesSupported !== undefined && !supportedScopes.has(approvedScope2))) {
1972
+ throw new Error("authorizeUser approvedScopes must be a subset of requested scopes");
1973
+ }
1974
+ const approvedScope = approvedScopes.length > 0 ? approvedScopes.join(" ") : undefined;
1975
+ const code = randomUUID();
1359
1976
  await config.codes.save(code, {
1360
1977
  clientId,
1361
1978
  redirectUri,
1362
1979
  codeChallenge,
1363
1980
  codeChallengeMethod: "S256",
1364
1981
  resource,
1365
- scope,
1982
+ scope: approvedScope,
1366
1983
  userId: result.userId,
1367
1984
  expiresAt: Date.now() + AUTH_CODE_TTL_MS
1368
1985
  });
@@ -1408,7 +2025,7 @@ function mountOAuthProvider(config) {
1408
2025
  const accessToken = await issueAccessToken(data.userId, data.resource, data.clientId, data.scope);
1409
2026
  let refreshToken;
1410
2027
  if (config.refreshTokens) {
1411
- refreshToken = randomUUID2();
2028
+ refreshToken = randomUUID();
1412
2029
  await config.refreshTokens.save(refreshToken, {
1413
2030
  clientId: data.clientId,
1414
2031
  resource: data.resource,
@@ -1438,7 +2055,7 @@ function mountOAuthProvider(config) {
1438
2055
  if (data.clientId !== clientId)
1439
2056
  return oauthError("invalid_grant", "client_id mismatch");
1440
2057
  const accessToken = await issueAccessToken(data.userId, data.resource, data.clientId, data.scope);
1441
- const newRefresh = randomUUID2();
2058
+ const newRefresh = randomUUID();
1442
2059
  await config.refreshTokens.save(newRefresh, data);
1443
2060
  return json({
1444
2061
  access_token: accessToken,
@@ -1451,7 +2068,7 @@ function mountOAuthProvider(config) {
1451
2068
  return oauthError("unsupported_grant_type", `Unsupported grant_type: ${grantType ?? "none"}`);
1452
2069
  }
1453
2070
  };
1454
- return [metadataRoute, registerRoute, authorizeRoute, tokenRoute];
2071
+ return dcrEnabled ? [metadataRoute, registerRoute, authorizeRoute, tokenRoute] : [metadataRoute, authorizeRoute, tokenRoute];
1455
2072
  }
1456
2073
  // src/browser/client-multipart.ts
1457
2074
  function isFileDescriptor(value) {
@@ -1936,7 +2553,7 @@ function summarizeTransports(surface) {
1936
2553
  // src/tools/view-file.ts
1937
2554
  import { readFile, realpath, stat } from "node:fs/promises";
1938
2555
  import { extname as extname2, resolve } from "node:path";
1939
- import { z as z4 } from "zod";
2556
+ import { z as z7 } from "zod";
1940
2557
  var MAX_INLINE_BYTES = 20 * 1024 * 1024;
1941
2558
  var EXT_MIME = {
1942
2559
  ".png": "image/png",
@@ -2036,7 +2653,7 @@ function mountViewFile(server, options = {}) {
2036
2653
  server.registerTool("view_file", {
2037
2654
  description: "View media (image, audio, video) by URL or local path — returns it as content you can SEE / HEAR. Pass several paths to view multiple files at once. Use it on a generation `output` url to inspect the result.",
2038
2655
  inputSchema: {
2039
- paths: z4.union([z4.string(), z4.array(z4.string())]).describe("Media URL(s) or file path(s) to view")
2656
+ paths: z7.union([z7.string(), z7.array(z7.string())]).describe("Media URL(s) or file path(s) to view")
2040
2657
  },
2041
2658
  annotations: { title: "View Media", readOnlyHint: true, idempotentHint: true }
2042
2659
  }, async (args) => {
@@ -2081,7 +2698,9 @@ export {
2081
2698
  createToolLogger,
2082
2699
  createToolInvoker,
2083
2700
  createStdioMcpServer,
2701
+ createSecureClientMetadataFetcher,
2084
2702
  createRuntimeToolFactory,
2703
+ createMcpHttpRoute,
2085
2704
  createMcpHandler,
2086
2705
  createCli,
2087
2706
  collectTools,