@gmickel/gno 1.16.0 → 1.18.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 (86) hide show
  1. package/README.md +34 -18
  2. package/assets/skill/SKILL.md +25 -6
  3. package/assets/skill/mcp-reference.md +21 -0
  4. package/package.json +2 -2
  5. package/src/app/context-agent-projection.ts +303 -0
  6. package/src/app/context-format.ts +249 -0
  7. package/src/app/context-runtime-contract.ts +325 -0
  8. package/src/app/context-runtime-input.ts +362 -0
  9. package/src/app/context-runtime-types.ts +65 -0
  10. package/src/app/context-runtime.ts +170 -0
  11. package/src/app/context-surface.ts +145 -0
  12. package/src/cli/commands/context-build.ts +149 -0
  13. package/src/cli/commands/context-verify.ts +90 -0
  14. package/src/cli/commands/daemon.ts +69 -2
  15. package/src/cli/commands/models/pull.ts +13 -3
  16. package/src/cli/commands/status.ts +2 -0
  17. package/src/cli/detach.ts +37 -20
  18. package/src/cli/options.ts +4 -0
  19. package/src/cli/program.ts +252 -27
  20. package/src/config/index.ts +3 -0
  21. package/src/config/types.ts +37 -0
  22. package/src/core/context-budget.ts +461 -0
  23. package/src/core/context-capsule-index-schema.ts +15 -0
  24. package/src/core/context-capsule-retrieval-schema.ts +81 -0
  25. package/src/core/context-capsule-schema.ts +473 -0
  26. package/src/core/context-capsule-validation.ts +416 -0
  27. package/src/core/context-capsule-verification.ts +218 -0
  28. package/src/core/context-capsule.ts +439 -0
  29. package/src/core/context-compiler.ts +513 -0
  30. package/src/core/context-evidence-metadata.ts +33 -0
  31. package/src/core/context-evidence.ts +495 -0
  32. package/src/core/context-facets.ts +163 -0
  33. package/src/core/context-guidance.ts +69 -0
  34. package/src/core/context-scope.ts +32 -0
  35. package/src/core/context-verifier-canonical.ts +90 -0
  36. package/src/core/context-verifier-input.ts +66 -0
  37. package/src/core/context-verifier.ts +447 -0
  38. package/src/core/job-manager.ts +19 -0
  39. package/src/core/mutation-generations.ts +33 -0
  40. package/src/core/sections.ts +63 -0
  41. package/src/llm/cache.ts +13 -3
  42. package/src/llm/nodeLlamaCpp/adapter.ts +10 -1
  43. package/src/llm/nodeLlamaCpp/lifecycle.ts +71 -0
  44. package/src/mcp/context.ts +161 -0
  45. package/src/mcp/http-security.ts +477 -0
  46. package/src/mcp/http-session.ts +272 -0
  47. package/src/mcp/http-transport.ts +370 -0
  48. package/src/mcp/resources/index.ts +141 -134
  49. package/src/mcp/server.ts +28 -82
  50. package/src/mcp/tools/add-collection.ts +3 -1
  51. package/src/mcp/tools/capture.ts +3 -0
  52. package/src/mcp/tools/clear-collection-embeddings.ts +2 -0
  53. package/src/mcp/tools/context.ts +230 -0
  54. package/src/mcp/tools/embed.ts +62 -52
  55. package/src/mcp/tools/index-cmd.ts +88 -74
  56. package/src/mcp/tools/index.ts +49 -2
  57. package/src/mcp/tools/remove-collection.ts +2 -0
  58. package/src/mcp/tools/status.ts +11 -0
  59. package/src/mcp/tools/sync.ts +16 -14
  60. package/src/mcp/tools/workspace-write.ts +7 -3
  61. package/src/pipeline/chunk-lookup.ts +33 -0
  62. package/src/pipeline/hybrid.ts +79 -57
  63. package/src/pipeline/types.ts +14 -0
  64. package/src/sdk/client.ts +68 -6
  65. package/src/sdk/index.ts +21 -0
  66. package/src/sdk/types.ts +24 -0
  67. package/src/serve/background-runtime.ts +12 -211
  68. package/src/serve/context-capsule.ts +136 -0
  69. package/src/serve/context.ts +10 -1
  70. package/src/serve/embed-scheduler.ts +74 -43
  71. package/src/serve/index.ts +9 -0
  72. package/src/serve/jobs.ts +78 -80
  73. package/src/serve/public/components/HealthCenter.tsx +74 -1
  74. package/src/serve/public/globals.built.css +1 -1
  75. package/src/serve/public/pages/Dashboard.tsx +1 -0
  76. package/src/serve/resident-admission.ts +159 -0
  77. package/src/serve/resident-background-work.ts +39 -0
  78. package/src/serve/resident-request.ts +55 -0
  79. package/src/serve/resident-runtime.ts +490 -0
  80. package/src/serve/resident-status.ts +96 -0
  81. package/src/serve/routes/api.ts +265 -167
  82. package/src/serve/routes/mcp.ts +69 -0
  83. package/src/serve/server.ts +212 -35
  84. package/src/serve/status-model.ts +51 -0
  85. package/src/serve/status.ts +5 -0
  86. package/src/store/sqlite/adapter.ts +64 -29
@@ -33,6 +33,8 @@ export const CMD = {
33
33
  collectionList: "collection.list",
34
34
  contextList: "context.list",
35
35
  contextCheck: "context.check",
36
+ contextBuild: "context.build",
37
+ contextVerify: "context.verify",
36
38
  modelsList: "models.list",
37
39
  tagsList: "tags.list",
38
40
  linksList: "links.list",
@@ -60,6 +62,8 @@ const FORMAT_SUPPORT: Record<CommandId, OutputFormat[]> = {
60
62
  [CMD.collectionList]: ["terminal", "json", "md"],
61
63
  [CMD.contextList]: ["terminal", "json", "md"],
62
64
  [CMD.contextCheck]: ["terminal", "json", "md"],
65
+ [CMD.contextBuild]: ["terminal", "json", "md"],
66
+ [CMD.contextVerify]: ["terminal", "json", "md"],
63
67
  [CMD.modelsList]: ["terminal", "json"],
64
68
  [CMD.tagsList]: ["terminal", "json", "md"],
65
69
  [CMD.linksList]: ["terminal", "json", "md"],
@@ -210,6 +210,42 @@ function parseCsvValues(raw: unknown): string[] | undefined {
210
210
  return values.length > 0 ? values : undefined;
211
211
  }
212
212
 
213
+ function parseContextInteger(
214
+ name: string,
215
+ value: unknown,
216
+ allowZero = false
217
+ ): number {
218
+ const raw = typeof value === "string" ? value : String(value);
219
+ if (!/^\d+$/.test(raw)) {
220
+ throw new CliError("VALIDATION", `--${name} must be an integer`, {
221
+ details: { contextCode: "invalid_budget" },
222
+ });
223
+ }
224
+ const parsed = Number(raw);
225
+ if (!Number.isSafeInteger(parsed) || (allowZero ? parsed < 0 : parsed < 1)) {
226
+ throw new CliError(
227
+ "VALIDATION",
228
+ `--${name} must be ${allowZero ? "non-negative" : "positive"}`,
229
+ { details: { contextCode: "invalid_budget" } }
230
+ );
231
+ }
232
+ return parsed;
233
+ }
234
+
235
+ function validateContextOutputPath(value: unknown): string | undefined {
236
+ if (value === undefined) return undefined;
237
+ if (typeof value !== "string" || !value.trim() || value === "-") {
238
+ throw new CliError(
239
+ "VALIDATION",
240
+ "--output requires an explicit file path",
241
+ {
242
+ details: { contextCode: "invalid_filter" },
243
+ }
244
+ );
245
+ }
246
+ return value;
247
+ }
248
+
213
249
  // ─────────────────────────────────────────────────────────────────────────────
214
250
  // Program Factory
215
251
  // ─────────────────────────────────────────────────────────────────────────────
@@ -1624,6 +1660,148 @@ function wireManagementCommands(program: Command): void {
1624
1660
  await contextCheck(format as "terminal" | "json" | "md");
1625
1661
  });
1626
1662
 
1663
+ contextCmd
1664
+ .command("build <goal...>")
1665
+ .description("Build a deterministic evidence Capsule")
1666
+ .requiredOption("--budget <tokens>", "global token budget")
1667
+ .option("--bytes <bytes>", "global byte budget")
1668
+ .option("--query <query>", "retrieval query (defaults to goal)")
1669
+ .option(
1670
+ "--query-mode <mode:text>",
1671
+ "structured mode entry (repeatable): term:<text>, intent:<text>, or hyde:<text>",
1672
+ (value: string, previous: string[] = []) => [...previous, value],
1673
+ []
1674
+ )
1675
+ .option(
1676
+ "-c, --collection <name>",
1677
+ "collection filter (repeatable)",
1678
+ (value: string, previous: string[] = []) => [...previous, value],
1679
+ []
1680
+ )
1681
+ .option("--uri-prefix <uri>", "canonical GNO URI prefix")
1682
+ .option("--tags-all <tags>", "require ALL tags (comma-separated)")
1683
+ .option("--tags-any <tags>", "require ANY tag (comma-separated)")
1684
+ .option("--category <values>", "require category match (comma-separated)")
1685
+ .option("--author <text>", "filter by author")
1686
+ .option("--lang <code>", "language hint (BCP-47)")
1687
+ .option("--since <date>", "modified-at lower bound")
1688
+ .option("--until <date>", "modified-at upper bound")
1689
+ .option("--graph", "enable graph neighbor expansion")
1690
+ .option("--fast", "use lexical-first fast retrieval")
1691
+ .option("--thorough", "use a wider retrieval pool")
1692
+ .option("-n, --limit <num>", "maximum retrieved results")
1693
+ .option("-C, --candidate-limit <num>", "maximum rerank candidates")
1694
+ .option("--safety-margin <tokens>", "reserved token margin", "0")
1695
+ .option("--safety-margin-bytes <bytes>", "reserved byte margin", "0")
1696
+ .option("--output <path>", "write only to this explicit file")
1697
+ .option("--json", "canonical JSON output")
1698
+ .option("--md", "readable Markdown output")
1699
+ .action(async (goalParts: string[], cmdOpts: Record<string, unknown>) => {
1700
+ const format = getFormat(cmdOpts);
1701
+ assertFormatSupported(CMD.contextBuild, format);
1702
+ const outputPath = validateContextOutputPath(cmdOpts.output);
1703
+ if (cmdOpts.fast && cmdOpts.thorough) {
1704
+ throw new CliError("VALIDATION", "Choose either --fast or --thorough");
1705
+ }
1706
+ const globals = getGlobals();
1707
+ const { contextBuild } = await import("./commands/context-build");
1708
+ let queryModes: import("../pipeline/types").QueryModeInput[] | undefined;
1709
+ if (Array.isArray(cmdOpts.queryMode) && cmdOpts.queryMode.length > 0) {
1710
+ const { parseQueryModeSpecs } = await import("../pipeline/query-modes");
1711
+ const parsed = parseQueryModeSpecs(cmdOpts.queryMode as string[]);
1712
+ if (!parsed.ok) {
1713
+ throw new CliError("VALIDATION", parsed.error.message, {
1714
+ details: { contextCode: "invalid_filter" },
1715
+ });
1716
+ }
1717
+ queryModes = parsed.value;
1718
+ }
1719
+ const output = await contextBuild(goalParts.join(" "), {
1720
+ configPath: globals.config,
1721
+ indexName: globals.index,
1722
+ budgetTokens: parseContextInteger("budget", cmdOpts.budget),
1723
+ budgetBytes:
1724
+ cmdOpts.bytes === undefined
1725
+ ? undefined
1726
+ : parseContextInteger("bytes", cmdOpts.bytes),
1727
+ safetyMarginTokens: parseContextInteger(
1728
+ "safety-margin",
1729
+ cmdOpts.safetyMargin ?? "0",
1730
+ true
1731
+ ),
1732
+ safetyMarginBytes: parseContextInteger(
1733
+ "safety-margin-bytes",
1734
+ cmdOpts.safetyMarginBytes ?? "0",
1735
+ true
1736
+ ),
1737
+ query: cmdOpts.query as string | undefined,
1738
+ queryModes,
1739
+ collections: cmdOpts.collection as string[],
1740
+ uriPrefix: cmdOpts.uriPrefix as string | undefined,
1741
+ tagsAll: parseCsvValues(cmdOpts.tagsAll),
1742
+ tagsAny: parseCsvValues(cmdOpts.tagsAny),
1743
+ categories: parseCsvValues(cmdOpts.category),
1744
+ author: cmdOpts.author as string | undefined,
1745
+ lang: cmdOpts.lang as string | undefined,
1746
+ since: cmdOpts.since as string | undefined,
1747
+ until: cmdOpts.until as string | undefined,
1748
+ graph: Boolean(cmdOpts.graph),
1749
+ limit:
1750
+ cmdOpts.limit === undefined
1751
+ ? undefined
1752
+ : parseContextInteger("limit", cmdOpts.limit),
1753
+ candidateLimit:
1754
+ cmdOpts.candidateLimit === undefined
1755
+ ? undefined
1756
+ : parseContextInteger("candidate-limit", cmdOpts.candidateLimit),
1757
+ depthPolicy: cmdOpts.fast
1758
+ ? "fast"
1759
+ : cmdOpts.thorough
1760
+ ? "thorough"
1761
+ : "balanced",
1762
+ format: format === "json" ? "json" : "md",
1763
+ });
1764
+ if (outputPath !== undefined) {
1765
+ await Bun.write(outputPath, output);
1766
+ return;
1767
+ }
1768
+ await writeOutput(output, format === "json" ? "json" : "md");
1769
+ });
1770
+
1771
+ contextCmd
1772
+ .command("verify <file>")
1773
+ .description("Verify a saved Context Capsule without rebuilding it")
1774
+ .option("--output <path>", "write only to this explicit file")
1775
+ .option("--json", "canonical JSON receipt")
1776
+ .option("--md", "readable Markdown receipt")
1777
+ .action(
1778
+ async (
1779
+ file: string,
1780
+ cmdOpts: Record<string, unknown>,
1781
+ command: Command
1782
+ ) => {
1783
+ const format = getFormat(cmdOpts);
1784
+ assertFormatSupported(CMD.contextVerify, format);
1785
+ const outputPath = validateContextOutputPath(cmdOpts.output);
1786
+ const globals = getGlobals();
1787
+ const explicitIndexName =
1788
+ command.getOptionValueSourceWithGlobals("index") === "cli"
1789
+ ? globals.index
1790
+ : undefined;
1791
+ const { contextVerify } = await import("./commands/context-verify");
1792
+ const output = await contextVerify(file, {
1793
+ configPath: globals.config,
1794
+ indexName: explicitIndexName,
1795
+ format: format === "json" ? "json" : "md",
1796
+ });
1797
+ if (outputPath !== undefined) {
1798
+ await Bun.write(outputPath, output);
1799
+ return;
1800
+ }
1801
+ await writeOutput(output, format === "json" ? "json" : "md");
1802
+ }
1803
+ );
1804
+
1627
1805
  contextCmd
1628
1806
  .command("rm <uri>")
1629
1807
  .description("Remove context item")
@@ -2628,20 +2806,46 @@ function wireGraphCommand(program: Command): void {
2628
2806
  // Serve Command (web UI)
2629
2807
  // ─────────────────────────────────────────────────────────────────────────────
2630
2808
 
2631
- function wireDaemonCommand(program: Command): void {
2632
- const daemonCmd = program
2633
- .command("daemon")
2634
- .description("Start headless continuous indexing")
2809
+ function addGatewayOptions(
2810
+ command: Command,
2811
+ hostDescription = "HTTP listen address (default: 127.0.0.1)"
2812
+ ): Command {
2813
+ return command
2814
+ .option("--host <address>", hostDescription)
2815
+ .option("--mcp-token-file <path>", "restrictive bearer token file")
2816
+ .option(
2817
+ "--mcp-allowed-host <host>",
2818
+ "exact allowed Host header (repeatable)",
2819
+ (value: string, previous: string[] = []) => [...previous, value]
2820
+ )
2635
2821
  .option(
2636
- "--no-sync-on-start",
2637
- "skip initial sync and only watch future file changes"
2822
+ "--mcp-allowed-origin <origin>",
2823
+ "exact allowed Origin (repeatable)",
2824
+ (value: string, previous: string[] = []) => [...previous, value]
2638
2825
  )
2639
- // Local `--json` so `gno daemon --status --json` parses cleanly in any
2640
- // argv position. Mirrors the serve wiring: gated to --status only.
2641
2826
  .option(
2642
- "--json",
2643
- "JSON output (applies to --status; see process-status schema)"
2827
+ "--mcp-enable-write",
2828
+ "authorize HTTP MCP mutation tools (independent of authentication)"
2644
2829
  );
2830
+ }
2831
+
2832
+ function wireDaemonCommand(program: Command): void {
2833
+ const daemonCmd = addGatewayOptions(
2834
+ program
2835
+ .command("daemon")
2836
+ .description("Start headless continuous indexing")
2837
+ .option("-p, --port <num>", "MCP gateway port", "3000")
2838
+ .option(
2839
+ "--no-sync-on-start",
2840
+ "skip initial sync and only watch future file changes"
2841
+ )
2842
+ // Local `--json` so `gno daemon --status --json` parses cleanly in any
2843
+ // argv position. Mirrors the serve wiring: gated to --status only.
2844
+ .option(
2845
+ "--json",
2846
+ "JSON output (applies to --status; see process-status schema)"
2847
+ )
2848
+ );
2645
2849
 
2646
2850
  // --detach / --status / --stop are mutually exclusive. Use Commander's
2647
2851
  // Option API so the conflict error is the native "option '--status'
@@ -2695,8 +2899,8 @@ function wireDaemonCommand(program: Command): void {
2695
2899
  * the foreground + detached-child paths boot the runtime.
2696
2900
  *
2697
2901
  * Mirrors `handleServeAction` exactly — same helper imports, same JSON
2698
- * gating, same detached-child verification + pid-file cleanup. Daemon has no
2699
- * `--port`, so the foreground path skips port validation entirely.
2902
+ * gating, same detached-child verification + pid-file cleanup. Daemon's port
2903
+ * identifies its headless MCP gateway.
2700
2904
  */
2701
2905
  async function handleDaemonAction(
2702
2906
  cmdOpts: Record<string, unknown>,
@@ -2749,7 +2953,9 @@ async function handleDaemonAction(
2749
2953
  }
2750
2954
 
2751
2955
  if (cmdOpts.detach) {
2956
+ const port = parsePositiveInt("port", cmdOpts.port);
2752
2957
  await runDaemonDetach({
2958
+ port,
2753
2959
  paths,
2754
2960
  spawnDetached,
2755
2961
  argv: resolveCliArgv(cmd),
@@ -2776,6 +2982,8 @@ async function handleDaemonAction(
2776
2982
  installPidFileCleanup(paths.pidFile);
2777
2983
  }
2778
2984
 
2985
+ const port = parsePositiveInt("port", cmdOpts.port);
2986
+
2779
2987
  const { daemon } = await import("./commands/daemon.js");
2780
2988
  const result = await daemon({
2781
2989
  configPath: globals.config,
@@ -2784,6 +2992,12 @@ async function handleDaemonAction(
2784
2992
  verbose: globals.verbose,
2785
2993
  quiet: globals.quiet,
2786
2994
  noSyncOnStart: cmdOpts.syncOnStart === false,
2995
+ port,
2996
+ host: cmdOpts.host as string | undefined,
2997
+ tokenFile: cmdOpts.mcpTokenFile as string | undefined,
2998
+ allowedHosts: cmdOpts.mcpAllowedHost as string[] | undefined,
2999
+ allowedOrigins: cmdOpts.mcpAllowedOrigin as string[] | undefined,
3000
+ enableWrite: cmdOpts.mcpEnableWrite === true ? true : undefined,
2787
3001
  });
2788
3002
  if (!result.success) {
2789
3003
  throw new CliError("RUNTIME", result.error);
@@ -2817,8 +3031,9 @@ async function runDaemonStatus(deps: DaemonStatusDeps): Promise<void> {
2817
3031
  process.stdout.write("gno daemon status\n");
2818
3032
  process.stdout.write(`${"─".repeat(50)}\n`);
2819
3033
  if (status.running) {
2820
- // Daemon is headless — no port to print.
2821
- process.stdout.write(` running yes (pid ${status.pid})\n`);
3034
+ process.stdout.write(
3035
+ ` running yes (pid ${status.pid}, port ${status.port})\n`
3036
+ );
2822
3037
  if (status.version) {
2823
3038
  process.stdout.write(` version ${status.version}\n`);
2824
3039
  }
@@ -2913,6 +3128,7 @@ async function runDaemonStop(deps: DaemonStopDeps): Promise<void> {
2913
3128
  }
2914
3129
 
2915
3130
  interface DaemonDetachDeps {
3131
+ port: number;
2916
3132
  paths: { pidFile: string; logFile: string };
2917
3133
  spawnDetached: typeof import("./detach.js").spawnDetached;
2918
3134
  /**
@@ -2933,24 +3149,28 @@ async function runDaemonDetach(deps: DaemonDetachDeps): Promise<void> {
2933
3149
  argv: childArgv,
2934
3150
  pidFile: deps.paths.pidFile,
2935
3151
  logFile: deps.paths.logFile,
3152
+ port: deps.port,
2936
3153
  });
2937
- // Daemon is headless print pid only (no URL).
3154
+ // Daemon has no Web UI URL; its listener exists solely for MCP.
2938
3155
  process.stdout.write(`PID ${result.pid}\n`);
2939
3156
  }
2940
3157
 
2941
3158
  function wireServeCommand(program: Command): void {
2942
- const serveCmd = program
2943
- .command("serve")
2944
- .description("Start web UI server")
2945
- .option("-p, --port <num>", "port to listen on", "3000")
2946
- // Local `--json` so `gno serve --status --json` parses cleanly in any
2947
- // argv position. Global `--json` already exists on the root program
2948
- // (see line 208); resolution goes through `getFormat()` /
2949
- // `getGlobals()` which precedence-merges local over global.
2950
- .option(
2951
- "--json",
2952
- "JSON output (applies to --status; see process-status schema)"
2953
- );
3159
+ const serveCmd = addGatewayOptions(
3160
+ program
3161
+ .command("serve")
3162
+ .description("Start web UI server")
3163
+ .option("-p, --port <num>", "port to listen on", "3000")
3164
+ // Local `--json` so `gno serve --status --json` parses cleanly in any
3165
+ // argv position. Global `--json` already exists on the root program
3166
+ // (see line 208); resolution goes through `getFormat()` /
3167
+ // `getGlobals()` which precedence-merges local over global.
3168
+ .option(
3169
+ "--json",
3170
+ "JSON output (applies to --status; see process-status schema)"
3171
+ ),
3172
+ "loopback HTTP listen address (default: 127.0.0.1)"
3173
+ );
2954
3174
 
2955
3175
  // --detach / --status / --stop are mutually exclusive. Use Commander's
2956
3176
  // Option API so the conflict error is the native "option '--status'
@@ -3097,6 +3317,11 @@ async function handleServeAction(
3097
3317
  port,
3098
3318
  configPath: globals.config,
3099
3319
  index: globals.index,
3320
+ host: cmdOpts.host as string | undefined,
3321
+ tokenFile: cmdOpts.mcpTokenFile as string | undefined,
3322
+ allowedHosts: cmdOpts.mcpAllowedHost as string[] | undefined,
3323
+ allowedOrigins: cmdOpts.mcpAllowedOrigin as string[] | undefined,
3324
+ enableWrite: cmdOpts.mcpEnableWrite === true ? true : undefined,
3100
3325
  });
3101
3326
 
3102
3327
  if (!result.success) {
@@ -50,6 +50,8 @@ export {
50
50
  CollectionSchema,
51
51
  type Config,
52
52
  ConfigSchema,
53
+ HttpGatewayConfigSchema,
54
+ HttpGatewayLimitsSchema,
53
55
  CONTENT_TYPE_GRAPH_HINTS,
54
56
  type ContentTypeConfig,
55
57
  type ContentTypeGraphHint,
@@ -67,3 +69,4 @@ export {
67
69
  type ScopeType,
68
70
  ScopeTypeSchema,
69
71
  } from "./types";
72
+ export type { HttpGatewayConfig } from "./types";
@@ -245,6 +245,40 @@ export const ModelConfigSchema = z.object({
245
245
 
246
246
  export type ModelConfig = z.infer<typeof ModelConfigSchema>;
247
247
 
248
+ // ────────────────────────────────────────────────────────────────────────────
249
+ // Resident HTTP gateway schema
250
+ // ────────────────────────────────────────────────────────────────────────────
251
+
252
+ export const HttpGatewayLimitsSchema = z.object({
253
+ maxBodyBytes: z
254
+ .number()
255
+ .int()
256
+ .min(1)
257
+ .max(16 * 1024 * 1024)
258
+ .optional(),
259
+ maxRequestsPerMinute: z.number().int().min(1).max(100_000).optional(),
260
+ maxConcurrentRequests: z.number().int().min(1).max(10_000).optional(),
261
+ maxQueuedRequests: z.number().int().min(0).max(10_000).optional(),
262
+ maxSessions: z.number().int().min(1).max(10_000).optional(),
263
+ sessionIdleTimeoutMs: z.number().int().min(1_000).optional(),
264
+ });
265
+
266
+ export const HttpGatewayConfigSchema = z.object({
267
+ /** Literal listen address. Defaults to IPv4 loopback. */
268
+ host: z.string().min(1).optional(),
269
+ /** Bearer token file. Required for wildcard/non-loopback binding. */
270
+ tokenFile: z.string().min(1).optional(),
271
+ /** Exact Host header allowlist. */
272
+ allowedHosts: z.array(z.string().min(1)).optional(),
273
+ /** Exact Origin allowlist. Origin-less non-browser clients remain supported. */
274
+ allowedOrigins: z.array(z.string().min(1)).optional(),
275
+ /** Separate mutation authorization; authentication alone never enables it. */
276
+ enableWrite: z.boolean().optional(),
277
+ limits: HttpGatewayLimitsSchema.optional(),
278
+ });
279
+
280
+ export type HttpGatewayConfig = z.infer<typeof HttpGatewayConfigSchema>;
281
+
248
282
  // ─────────────────────────────────────────────────────────────────────────────
249
283
  // Content Type Schema
250
284
  // ─────────────────────────────────────────────────────────────────────────────
@@ -301,6 +335,9 @@ export const ConfigSchema = z.object({
301
335
 
302
336
  /** Model configuration */
303
337
  models: ModelConfigSchema.optional(),
338
+
339
+ /** Resident Streamable HTTP MCP gateway configuration */
340
+ gateway: HttpGatewayConfigSchema.optional(),
304
341
  });
305
342
 
306
343
  export type Config = Omit<z.infer<typeof ConfigSchema>, "contentTypes"> & {