@gmickel/gno 1.17.0 → 1.19.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 (167) hide show
  1. package/README.md +18 -4
  2. package/assets/skill/SKILL.md +44 -1
  3. package/assets/skill/mcp-reference.md +21 -0
  4. package/package.json +3 -2
  5. package/spec/AGENTS.md +83 -0
  6. package/spec/CLAUDE.md +83 -0
  7. package/spec/bench-fixture.schema.json +137 -0
  8. package/spec/cli.md +2894 -0
  9. package/spec/db/schema.sql +442 -0
  10. package/spec/evals-agentic.md +510 -0
  11. package/spec/evals.md +1106 -0
  12. package/spec/mcp.md +2229 -0
  13. package/spec/output-schemas/activation-verification.schema.json +515 -0
  14. package/spec/output-schemas/ask.schema.json +366 -0
  15. package/spec/output-schemas/backlinks.schema.json +131 -0
  16. package/spec/output-schemas/bench-result.schema.json +120 -0
  17. package/spec/output-schemas/capture-receipt.schema.json +143 -0
  18. package/spec/output-schemas/collection-list.schema.json +45 -0
  19. package/spec/output-schemas/context-capsule-v1.schema.json +691 -0
  20. package/spec/output-schemas/context-capsule-verification.schema.json +1338 -0
  21. package/spec/output-schemas/context-list.schema.json +21 -0
  22. package/spec/output-schemas/doctor.schema.json +313 -0
  23. package/spec/output-schemas/error.schema.json +30 -0
  24. package/spec/output-schemas/expansion.schema.json +37 -0
  25. package/spec/output-schemas/get.schema.json +140 -0
  26. package/spec/output-schemas/graph-query.schema.json +99 -0
  27. package/spec/output-schemas/graph.schema.json +371 -0
  28. package/spec/output-schemas/links-list.schema.json +186 -0
  29. package/spec/output-schemas/mcp-add-collection-result.schema.json +23 -0
  30. package/spec/output-schemas/mcp-capture-result.schema.json +152 -0
  31. package/spec/output-schemas/mcp-http-error.schema.json +30 -0
  32. package/spec/output-schemas/mcp-job-list.schema.json +58 -0
  33. package/spec/output-schemas/mcp-job-status.schema.json +224 -0
  34. package/spec/output-schemas/mcp-remove-result.schema.json +39 -0
  35. package/spec/output-schemas/mcp-sync-result.schema.json +41 -0
  36. package/spec/output-schemas/mcp-tag-result.schema.json +33 -0
  37. package/spec/output-schemas/models-list.schema.json +93 -0
  38. package/spec/output-schemas/multi-get.schema.json +103 -0
  39. package/spec/output-schemas/process-status.schema.json +119 -0
  40. package/spec/output-schemas/query-diagnose.schema.json +123 -0
  41. package/spec/output-schemas/resident-status.schema.json +154 -0
  42. package/spec/output-schemas/retrieval-trace-common.schema.json +492 -0
  43. package/spec/output-schemas/retrieval-trace-delete.schema.json +16 -0
  44. package/spec/output-schemas/retrieval-trace-export.schema.json +61 -0
  45. package/spec/output-schemas/retrieval-trace-filters.schema.json +139 -0
  46. package/spec/output-schemas/retrieval-trace-judgment.schema.json +15 -0
  47. package/spec/output-schemas/retrieval-trace-list.schema.json +18 -0
  48. package/spec/output-schemas/retrieval-trace-payloads.schema.json +178 -0
  49. package/spec/output-schemas/retrieval-trace-purge.schema.json +31 -0
  50. package/spec/output-schemas/retrieval-trace-qrels.schema.json +303 -0
  51. package/spec/output-schemas/retrieval-trace-replay.schema.json +286 -0
  52. package/spec/output-schemas/retrieval-trace-show.schema.json +69 -0
  53. package/spec/output-schemas/retrieval-trace-summary.schema.json +65 -0
  54. package/spec/output-schemas/search-result.schema.json +154 -0
  55. package/spec/output-schemas/search-results.schema.json +338 -0
  56. package/spec/output-schemas/similar.schema.json +84 -0
  57. package/spec/output-schemas/status.schema.json +676 -0
  58. package/spec/output-schemas/tags-list.schema.json +48 -0
  59. package/src/app/context-runtime-types.ts +3 -0
  60. package/src/app/context-runtime.ts +15 -1
  61. package/src/cli/commands/ask.ts +106 -36
  62. package/src/cli/commands/context-build.ts +56 -9
  63. package/src/cli/commands/daemon.ts +69 -2
  64. package/src/cli/commands/get.ts +64 -3
  65. package/src/cli/commands/models/pull.ts +13 -3
  66. package/src/cli/commands/query.ts +62 -23
  67. package/src/cli/commands/replay.ts +140 -0
  68. package/src/cli/commands/search.ts +48 -3
  69. package/src/cli/commands/shared.ts +3 -1
  70. package/src/cli/commands/status.ts +2 -0
  71. package/src/cli/commands/trace.ts +200 -0
  72. package/src/cli/commands/vsearch.ts +75 -53
  73. package/src/cli/detach.ts +37 -20
  74. package/src/cli/program.ts +329 -27
  75. package/src/config/index.ts +12 -0
  76. package/src/config/retrieval-traces.ts +56 -0
  77. package/src/config/types.ts +41 -0
  78. package/src/core/context-compiler.ts +11 -4
  79. package/src/core/job-manager.ts +19 -0
  80. package/src/core/mutation-generations.ts +33 -0
  81. package/src/core/retrieval-qrels.ts +405 -0
  82. package/src/core/retrieval-replay-candidate.ts +368 -0
  83. package/src/core/retrieval-replay-types.ts +109 -0
  84. package/src/core/retrieval-replay-validation.ts +89 -0
  85. package/src/core/retrieval-replay.ts +441 -0
  86. package/src/core/retrieval-trace-evidence-origin.ts +175 -0
  87. package/src/core/retrieval-trace-export.ts +113 -0
  88. package/src/core/retrieval-trace-filter-normalization.ts +27 -0
  89. package/src/core/retrieval-trace-filters.ts +19 -0
  90. package/src/core/retrieval-trace-management-helpers.ts +247 -0
  91. package/src/core/retrieval-trace-management-types.ts +132 -0
  92. package/src/core/retrieval-trace-management.ts +422 -0
  93. package/src/core/retrieval-trace-request.ts +141 -0
  94. package/src/core/retrieval-trace-session.ts +494 -0
  95. package/src/core/retrieval-trace.ts +472 -0
  96. package/src/llm/cache.ts +13 -3
  97. package/src/llm/nodeLlamaCpp/adapter.ts +10 -1
  98. package/src/llm/nodeLlamaCpp/lifecycle.ts +71 -0
  99. package/src/mcp/context.ts +161 -0
  100. package/src/mcp/http-security.ts +477 -0
  101. package/src/mcp/http-session.ts +272 -0
  102. package/src/mcp/http-transport.ts +370 -0
  103. package/src/mcp/resources/index.ts +141 -134
  104. package/src/mcp/server.ts +19 -79
  105. package/src/mcp/tools/add-collection.ts +3 -1
  106. package/src/mcp/tools/capture.ts +3 -0
  107. package/src/mcp/tools/clear-collection-embeddings.ts +2 -0
  108. package/src/mcp/tools/context.ts +68 -16
  109. package/src/mcp/tools/embed.ts +62 -52
  110. package/src/mcp/tools/get.ts +35 -1
  111. package/src/mcp/tools/index-cmd.ts +88 -74
  112. package/src/mcp/tools/index.ts +96 -2
  113. package/src/mcp/tools/query.ts +95 -64
  114. package/src/mcp/tools/remove-collection.ts +2 -0
  115. package/src/mcp/tools/search.ts +36 -13
  116. package/src/mcp/tools/status.ts +11 -0
  117. package/src/mcp/tools/sync.ts +16 -14
  118. package/src/mcp/tools/trace.ts +143 -0
  119. package/src/mcp/tools/vsearch.ts +71 -38
  120. package/src/mcp/tools/workspace-write.ts +7 -3
  121. package/src/pipeline/answer.ts +167 -26
  122. package/src/pipeline/graph-retrieval.ts +15 -1
  123. package/src/pipeline/hybrid.ts +151 -43
  124. package/src/pipeline/search.ts +36 -3
  125. package/src/pipeline/trace-metadata.ts +47 -0
  126. package/src/pipeline/types.ts +43 -0
  127. package/src/pipeline/vsearch.ts +101 -38
  128. package/src/sdk/client.ts +380 -71
  129. package/src/sdk/documents.ts +48 -1
  130. package/src/sdk/index.ts +17 -0
  131. package/src/sdk/types.ts +28 -0
  132. package/src/serve/background-runtime.ts +12 -212
  133. package/src/serve/context-capsule.ts +67 -8
  134. package/src/serve/embed-scheduler.ts +74 -43
  135. package/src/serve/index.ts +9 -0
  136. package/src/serve/jobs.ts +78 -80
  137. package/src/serve/public/app.tsx +12 -1
  138. package/src/serve/public/components/HealthCenter.tsx +74 -1
  139. package/src/serve/public/globals.built.css +1 -1
  140. package/src/serve/public/lib/workspace-tabs.ts +2 -0
  141. package/src/serve/public/pages/Dashboard.tsx +11 -0
  142. package/src/serve/public/pages/TraceHistory.tsx +478 -0
  143. package/src/serve/public/pages/trace-history-detail.tsx +224 -0
  144. package/src/serve/resident-admission.ts +159 -0
  145. package/src/serve/resident-background-work.ts +39 -0
  146. package/src/serve/resident-request.ts +55 -0
  147. package/src/serve/resident-runtime.ts +490 -0
  148. package/src/serve/resident-status.ts +96 -0
  149. package/src/serve/retrieval-trace.ts +28 -0
  150. package/src/serve/routes/api.ts +629 -239
  151. package/src/serve/routes/mcp.ts +69 -0
  152. package/src/serve/routes/traces.ts +156 -0
  153. package/src/serve/server.ts +276 -37
  154. package/src/serve/status-model.ts +51 -0
  155. package/src/serve/status.ts +5 -0
  156. package/src/store/index.ts +31 -0
  157. package/src/store/migrations/014-retrieval-traces.ts +303 -0
  158. package/src/store/migrations/index.ts +2 -0
  159. package/src/store/retrieval-trace-codec.ts +384 -0
  160. package/src/store/sqlite/adapter.ts +179 -10
  161. package/src/store/sqlite/retrieval-trace-management-store.ts +341 -0
  162. package/src/store/sqlite/retrieval-trace-retention.ts +349 -0
  163. package/src/store/sqlite/retrieval-trace-rows.ts +267 -0
  164. package/src/store/sqlite/retrieval-trace-store.ts +515 -0
  165. package/src/store/types.ts +297 -0
  166. package/src/store/vector/sqlite-vec.ts +76 -1
  167. package/src/store/vector/types.ts +1 -1
@@ -0,0 +1,272 @@
1
+ /** Isolated stateful MCP server/transport ownership for HTTP sessions. */
2
+
3
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
+
5
+ import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
6
+
7
+ import type { ToolContext } from "./context";
8
+
9
+ import { MCP_SERVER_NAME, VERSION } from "../app/constants";
10
+ import { createMcpServerSurface } from "./context";
11
+
12
+ const DEFAULT_IDLE_TIMEOUT_MS = 5 * 60 * 1000;
13
+ const DEFAULT_MAX_SESSIONS = 32;
14
+ const MAX_REAP_INTERVAL_MS = 30_000;
15
+
16
+ export interface HttpMcpSessionRuntime {
17
+ readonly mcpContext: ToolContext;
18
+ openSession(): () => void;
19
+ }
20
+
21
+ export interface HttpMcpSession {
22
+ readonly id: string;
23
+ readonly securityIdentity: string;
24
+ readonly server: McpServer;
25
+ readonly transport: WebStandardStreamableHTTPServerTransport;
26
+ lastActivityAt: number;
27
+ activeRequests: number;
28
+ }
29
+
30
+ export interface HttpMcpSessionStoreOptions {
31
+ idleTimeoutMs?: number;
32
+ maxSessions?: number;
33
+ now?: () => number;
34
+ sessionIdGenerator?: () => string;
35
+ createServer?: (context: ToolContext) => McpServer;
36
+ }
37
+
38
+ export interface PendingHttpMcpSession {
39
+ readonly server: McpServer;
40
+ readonly transport: WebStandardStreamableHTTPServerTransport;
41
+ get session(): HttpMcpSession | undefined;
42
+ discard(): Promise<void>;
43
+ }
44
+
45
+ interface OwnedHttpMcpSession extends HttpMcpSession {
46
+ releaseRuntimeSession(): void;
47
+ }
48
+
49
+ interface PendingSessionHost {
50
+ readonly server: McpServer;
51
+ readonly transport: WebStandardStreamableHTTPServerTransport;
52
+ session?: OwnedHttpMcpSession;
53
+ discarded: boolean;
54
+ }
55
+
56
+ function isVisibleAscii(value: string): boolean {
57
+ if (value.length === 0) return false;
58
+ for (const character of value) {
59
+ const code = character.charCodeAt(0);
60
+ if (code < 0x21 || code > 0x7e) return false;
61
+ }
62
+ return true;
63
+ }
64
+
65
+ /** Owns exactly one McpServer and Web Standard transport per active session. */
66
+ export class HttpMcpSessionStore {
67
+ readonly #runtime: HttpMcpSessionRuntime;
68
+ readonly #idleTimeoutMs: number;
69
+ readonly #maxSessions: number;
70
+ readonly #now: () => number;
71
+ readonly #sessionIdGenerator: () => string;
72
+ readonly #createServer: (context: ToolContext) => McpServer;
73
+ readonly #sessions = new Map<string, OwnedHttpMcpSession>();
74
+ readonly #pending = new Set<PendingSessionHost>();
75
+ readonly #reapTimer: ReturnType<typeof setInterval>;
76
+ #closed = false;
77
+
78
+ constructor(
79
+ runtime: HttpMcpSessionRuntime,
80
+ options: HttpMcpSessionStoreOptions = {}
81
+ ) {
82
+ this.#runtime = runtime;
83
+ this.#idleTimeoutMs = Math.max(
84
+ 1,
85
+ options.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS
86
+ );
87
+ this.#maxSessions = Math.max(
88
+ 1,
89
+ Math.floor(options.maxSessions ?? DEFAULT_MAX_SESSIONS)
90
+ );
91
+ this.#now = options.now ?? Date.now;
92
+ this.#sessionIdGenerator =
93
+ options.sessionIdGenerator ?? (() => crypto.randomUUID());
94
+ this.#createServer =
95
+ options.createServer ??
96
+ ((context) =>
97
+ createMcpServerSurface(context, {
98
+ name: MCP_SERVER_NAME,
99
+ version: VERSION,
100
+ }));
101
+
102
+ this.#reapTimer = setInterval(
103
+ () => void this.reapIdleSessions(),
104
+ Math.min(this.#idleTimeoutMs, MAX_REAP_INTERVAL_MS)
105
+ );
106
+ this.#reapTimer.unref?.();
107
+ }
108
+
109
+ get size(): number {
110
+ return this.#sessions.size;
111
+ }
112
+
113
+ get maxSessions(): number {
114
+ return this.#maxSessions;
115
+ }
116
+
117
+ get capacityAvailable(): boolean {
118
+ return (
119
+ !this.#closed &&
120
+ this.#sessions.size + this.#pending.size < this.#maxSessions
121
+ );
122
+ }
123
+
124
+ get(sessionId: string): HttpMcpSession | undefined {
125
+ return this.#sessions.get(sessionId);
126
+ }
127
+
128
+ beginRequest(session: HttpMcpSession): void {
129
+ session.activeRequests += 1;
130
+ session.lastActivityAt = this.#now();
131
+ }
132
+
133
+ finishRequest(session: HttpMcpSession): void {
134
+ session.activeRequests = Math.max(0, session.activeRequests - 1);
135
+ session.lastActivityAt = this.#now();
136
+ }
137
+
138
+ async createPendingSession(
139
+ securityIdentity = "loopback"
140
+ ): Promise<PendingHttpMcpSession | null> {
141
+ if (!this.capacityAvailable) return null;
142
+
143
+ const server = this.#createServer(this.#runtime.mcpContext);
144
+ let host: PendingSessionHost;
145
+ const transport = new WebStandardStreamableHTTPServerTransport({
146
+ sessionIdGenerator: () => {
147
+ const sessionId = this.#sessionIdGenerator();
148
+ if (!isVisibleAscii(sessionId)) {
149
+ throw new Error(
150
+ "MCP session ID must contain visible ASCII characters"
151
+ );
152
+ }
153
+ return sessionId;
154
+ },
155
+ // Intentionally no EventStore: this release does not support resumption.
156
+ onsessioninitialized: (sessionId) => {
157
+ if (host.discarded || this.#closed) {
158
+ throw new Error("MCP session store is closed");
159
+ }
160
+ if (this.#sessions.has(sessionId)) {
161
+ throw new Error("MCP session ID collision");
162
+ }
163
+ const releaseRuntimeSession = this.#runtime.openSession();
164
+ host.session = {
165
+ id: sessionId,
166
+ securityIdentity,
167
+ server,
168
+ transport,
169
+ lastActivityAt: this.#now(),
170
+ activeRequests: 0,
171
+ releaseRuntimeSession,
172
+ };
173
+ this.#sessions.set(sessionId, host.session);
174
+ this.#pending.delete(host);
175
+ },
176
+ });
177
+ host = { server, transport, discarded: false };
178
+ this.#pending.add(host);
179
+
180
+ transport.onclose = () => {
181
+ const sessionId = transport.sessionId;
182
+ if (sessionId) this.#releaseSession(sessionId);
183
+ this.#pending.delete(host);
184
+ };
185
+
186
+ try {
187
+ await server.connect(transport);
188
+ } catch (error) {
189
+ this.#pending.delete(host);
190
+ host.discarded = true;
191
+ await server.close().catch(() => undefined);
192
+ throw error;
193
+ }
194
+
195
+ return {
196
+ server,
197
+ transport,
198
+ get session() {
199
+ return host.session;
200
+ },
201
+ discard: async () => {
202
+ if (host.session || host.discarded) return;
203
+ host.discarded = true;
204
+ this.#pending.delete(host);
205
+ await server.close().catch(() => undefined);
206
+ },
207
+ };
208
+ }
209
+
210
+ async closeSession(sessionId: string): Promise<boolean> {
211
+ const session = this.#releaseSession(sessionId);
212
+ if (!session) return false;
213
+ await session.server.close().catch(() => undefined);
214
+ return true;
215
+ }
216
+
217
+ async reapIdleSessions(now = this.#now()): Promise<number> {
218
+ const staleSessionIds = [...this.#sessions.values()]
219
+ .filter(
220
+ (session) =>
221
+ session.activeRequests === 0 &&
222
+ now - session.lastActivityAt >= this.#idleTimeoutMs
223
+ )
224
+ .map((session) => session.id);
225
+ await Promise.allSettled(
226
+ staleSessionIds.map((sessionId) => this.closeSession(sessionId))
227
+ );
228
+ return staleSessionIds.length;
229
+ }
230
+
231
+ async closeAll(): Promise<void> {
232
+ if (this.#closed) return;
233
+ this.#closed = true;
234
+ clearInterval(this.#reapTimer);
235
+
236
+ const sessions = [...this.#sessions.values()];
237
+ this.#sessions.clear();
238
+ for (const session of sessions) session.releaseRuntimeSession();
239
+
240
+ const pending = [...this.#pending];
241
+ this.#pending.clear();
242
+ for (const host of pending) host.discarded = true;
243
+
244
+ await Promise.allSettled([
245
+ ...sessions.map((session) => session.server.close()),
246
+ ...pending.map((host) => host.server.close()),
247
+ ]);
248
+ }
249
+
250
+ async closeSessions(): Promise<void> {
251
+ const sessions = [...this.#sessions.values()];
252
+ this.#sessions.clear();
253
+ for (const session of sessions) session.releaseRuntimeSession();
254
+
255
+ const pending = [...this.#pending];
256
+ this.#pending.clear();
257
+ for (const host of pending) host.discarded = true;
258
+
259
+ await Promise.allSettled([
260
+ ...sessions.map((session) => session.server.close()),
261
+ ...pending.map((host) => host.server.close()),
262
+ ]);
263
+ }
264
+
265
+ #releaseSession(sessionId: string): OwnedHttpMcpSession | undefined {
266
+ const session = this.#sessions.get(sessionId);
267
+ if (!session) return undefined;
268
+ this.#sessions.delete(sessionId);
269
+ session.releaseRuntimeSession();
270
+ return session;
271
+ }
272
+ }
@@ -0,0 +1,370 @@
1
+ /** Web Standard Streamable HTTP request routing for the resident MCP runtime. */
2
+
3
+ import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
4
+
5
+ import type { ResidentRequestHandle } from "../serve/resident-runtime";
6
+ import type {
7
+ HttpMcpSession,
8
+ HttpMcpSessionRuntime,
9
+ HttpMcpSessionStoreOptions,
10
+ PendingHttpMcpSession,
11
+ } from "./http-session";
12
+
13
+ import { HttpMcpSessionStore } from "./http-session";
14
+ import { MCP_WRITE_TOOL_NAMES } from "./tools/index";
15
+
16
+ const DEFAULT_MAX_CONCURRENT_REQUESTS = 64;
17
+ const DEFAULT_MAX_QUEUED_REQUESTS = 0;
18
+ const MCP_HTTP_METHODS = new Set(["DELETE", "GET", "POST"]);
19
+ const MCP_SESSION_HEADER = "mcp-session-id";
20
+
21
+ export interface HttpMcpTransportRuntime extends HttpMcpSessionRuntime {
22
+ readonly isShuttingDown: boolean;
23
+ admitRequest(signal?: AbortSignal): ResidentRequestHandle | null;
24
+ }
25
+
26
+ export interface HttpMcpTransportOptions extends HttpMcpSessionStoreOptions {
27
+ maxConcurrentRequests?: number;
28
+ maxQueuedRequests?: number;
29
+ enableWrite?: boolean;
30
+ }
31
+
32
+ export interface HttpMcpRequestContext {
33
+ identity: string;
34
+ parsedBody?: unknown;
35
+ }
36
+
37
+ export interface HttpMcpTransportStatus {
38
+ activeRequests: number;
39
+ activeSessions: number;
40
+ queuedRequests: number;
41
+ maxConcurrentRequests: number;
42
+ maxQueuedRequests: number;
43
+ maxSessions: number;
44
+ }
45
+
46
+ function jsonRpcError(
47
+ status: number,
48
+ code: number,
49
+ message: string,
50
+ headers?: HeadersInit
51
+ ): Response {
52
+ return Response.json(
53
+ { jsonrpc: "2.0", error: { code, message }, id: null },
54
+ { status, headers }
55
+ );
56
+ }
57
+
58
+ function methodNotAllowed(): Response {
59
+ return jsonRpcError(405, -32_000, "Method not allowed.", {
60
+ Allow: "GET, POST, DELETE",
61
+ });
62
+ }
63
+
64
+ function containsUnauthorizedWrite(parsedBody: unknown): boolean {
65
+ const messages = Array.isArray(parsedBody) ? parsedBody : [parsedBody];
66
+ return messages.some((message) => {
67
+ if (typeof message !== "object" || message === null) return false;
68
+ const candidate = message as {
69
+ method?: unknown;
70
+ params?: { name?: unknown };
71
+ };
72
+ return (
73
+ candidate.method === "tools/call" &&
74
+ typeof candidate.params?.name === "string" &&
75
+ MCP_WRITE_TOOL_NAMES.has(candidate.params.name)
76
+ );
77
+ });
78
+ }
79
+
80
+ function validateInitializeHeaders(request: Request): Response | undefined {
81
+ const accept = request.headers.get("accept");
82
+ if (
83
+ !accept?.includes("application/json") ||
84
+ !accept.includes("text/event-stream")
85
+ ) {
86
+ return jsonRpcError(
87
+ 406,
88
+ -32_000,
89
+ "Not Acceptable: Client must accept both application/json and text/event-stream"
90
+ );
91
+ }
92
+ if (!request.headers.get("content-type")?.includes("application/json")) {
93
+ return jsonRpcError(
94
+ 415,
95
+ -32_000,
96
+ "Unsupported Media Type: Content-Type must be application/json"
97
+ );
98
+ }
99
+ return undefined;
100
+ }
101
+
102
+ function wrapStreamingResponse(
103
+ response: Response,
104
+ finish: () => void
105
+ ): Response {
106
+ if (!response.body) {
107
+ finish();
108
+ return response;
109
+ }
110
+ const reader = response.body.getReader();
111
+ let finished = false;
112
+ const finishOnce = (): void => {
113
+ if (finished) return;
114
+ finished = true;
115
+ finish();
116
+ };
117
+ const body = new ReadableStream<Uint8Array>({
118
+ async pull(controller) {
119
+ try {
120
+ const result = await reader.read();
121
+ if (result.done) {
122
+ controller.close();
123
+ finishOnce();
124
+ return;
125
+ }
126
+ controller.enqueue(result.value);
127
+ } catch (error) {
128
+ controller.error(error);
129
+ finishOnce();
130
+ }
131
+ },
132
+ async cancel(reason) {
133
+ finishOnce();
134
+ await reader.cancel(reason);
135
+ },
136
+ });
137
+ return new Response(body, response);
138
+ }
139
+
140
+ /** Stateful session gateway used by the production `/mcp` route. */
141
+ export class HttpMcpTransport {
142
+ readonly #runtime: HttpMcpTransportRuntime;
143
+ readonly #sessions: HttpMcpSessionStore;
144
+ readonly #maxConcurrentRequests: number;
145
+ readonly #maxQueuedRequests: number;
146
+ readonly #enableWrite: boolean;
147
+ readonly #capacityWaiters: Array<(admitted: boolean) => void> = [];
148
+ #activeRequests = 0;
149
+ #closed = false;
150
+
151
+ constructor(
152
+ runtime: HttpMcpTransportRuntime,
153
+ options: HttpMcpTransportOptions = {}
154
+ ) {
155
+ this.#runtime = runtime;
156
+ this.#sessions = new HttpMcpSessionStore(runtime, options);
157
+ this.#maxConcurrentRequests = Math.max(
158
+ 1,
159
+ Math.floor(
160
+ options.maxConcurrentRequests ?? DEFAULT_MAX_CONCURRENT_REQUESTS
161
+ )
162
+ );
163
+ this.#maxQueuedRequests = Math.max(
164
+ 0,
165
+ Math.floor(options.maxQueuedRequests ?? DEFAULT_MAX_QUEUED_REQUESTS)
166
+ );
167
+ this.#enableWrite = options.enableWrite ?? false;
168
+ }
169
+
170
+ get activeRequests(): number {
171
+ return this.#activeRequests;
172
+ }
173
+
174
+ get activeSessions(): number {
175
+ return this.#sessions.size;
176
+ }
177
+
178
+ get queuedRequests(): number {
179
+ return this.#capacityWaiters.length;
180
+ }
181
+
182
+ getStatus(): HttpMcpTransportStatus {
183
+ return {
184
+ activeRequests: this.activeRequests,
185
+ activeSessions: this.activeSessions,
186
+ queuedRequests: this.queuedRequests,
187
+ maxConcurrentRequests: this.#maxConcurrentRequests,
188
+ maxQueuedRequests: this.#maxQueuedRequests,
189
+ maxSessions: this.#sessions.maxSessions,
190
+ };
191
+ }
192
+
193
+ async handleRequest(
194
+ request: Request,
195
+ context: HttpMcpRequestContext = { identity: "loopback" }
196
+ ): Promise<Response> {
197
+ if (!MCP_HTTP_METHODS.has(request.method)) return methodNotAllowed();
198
+ if (this.#closed || this.#runtime.isShuttingDown)
199
+ return jsonRpcError(503, -32_000, "Resident runtime is unavailable");
200
+ if (!(await this.#acquireCapacity(request.signal))) {
201
+ if (this.#closed || this.#runtime.isShuttingDown)
202
+ return jsonRpcError(503, -32_000, "Resident runtime is unavailable");
203
+ return jsonRpcError(429, -32_000, "Too many requests");
204
+ }
205
+
206
+ const admission = this.#runtime.admitRequest(request.signal);
207
+ if (!admission) {
208
+ this.#releaseCapacity();
209
+ return jsonRpcError(503, -32_000, "Resident runtime is unavailable");
210
+ }
211
+
212
+ let session: HttpMcpSession | undefined;
213
+ let pending: PendingHttpMcpSession | undefined;
214
+ let finished = false;
215
+ const finish = (): void => {
216
+ if (finished) return;
217
+ finished = true;
218
+ if (session) this.#sessions.finishRequest(session);
219
+ this.#releaseCapacity();
220
+ admission.finish();
221
+ };
222
+
223
+ try {
224
+ const sessionId = request.headers.get(MCP_SESSION_HEADER);
225
+ let parsedBody = context.parsedBody;
226
+ if (request.method === "POST" && parsedBody === undefined) {
227
+ try {
228
+ parsedBody = await request.clone().json();
229
+ } catch {
230
+ finish();
231
+ return jsonRpcError(400, -32_700, "Parse error: Invalid JSON");
232
+ }
233
+ }
234
+
235
+ if (sessionId) {
236
+ session = this.#sessions.get(sessionId);
237
+ if (!session) {
238
+ finish();
239
+ return jsonRpcError(404, -32_001, "Session not found");
240
+ }
241
+ if (session.securityIdentity !== context.identity) {
242
+ await this.#sessions.closeSession(sessionId);
243
+ finish();
244
+ return jsonRpcError(403, -32_000, "Forbidden");
245
+ }
246
+ this.#sessions.beginRequest(session);
247
+ } else {
248
+ if (request.method !== "POST") {
249
+ finish();
250
+ return jsonRpcError(
251
+ 400,
252
+ -32_000,
253
+ "Bad Request: Mcp-Session-Id header is required"
254
+ );
255
+ }
256
+ const headerError = validateInitializeHeaders(request);
257
+ if (headerError) {
258
+ finish();
259
+ return headerError;
260
+ }
261
+ if (!isInitializeRequest(parsedBody)) {
262
+ finish();
263
+ return jsonRpcError(
264
+ 400,
265
+ -32_000,
266
+ "Bad Request: No valid session ID provided"
267
+ );
268
+ }
269
+ pending =
270
+ (await this.#sessions.createPendingSession(context.identity)) ??
271
+ undefined;
272
+ if (!pending) {
273
+ finish();
274
+ return jsonRpcError(429, -32_000, "Too many requests");
275
+ }
276
+ }
277
+
278
+ if (request.headers.has("last-event-id")) {
279
+ await pending?.discard();
280
+ finish();
281
+ return jsonRpcError(
282
+ 400,
283
+ -32_000,
284
+ "MCP event resumption is not supported"
285
+ );
286
+ }
287
+
288
+ const transport = session?.transport ?? pending?.transport;
289
+ if (!transport) throw new Error("MCP transport was not created");
290
+ const requestBody = parsedBody;
291
+ if (!this.#enableWrite && containsUnauthorizedWrite(requestBody)) {
292
+ await pending?.discard();
293
+ finish();
294
+ return jsonRpcError(403, -32_000, "Forbidden");
295
+ }
296
+ const response = await transport.handleRequest(
297
+ request,
298
+ requestBody === undefined ? undefined : { parsedBody: requestBody }
299
+ );
300
+
301
+ if (pending) {
302
+ session = pending.session;
303
+ if (!session) await pending.discard();
304
+ else this.#sessions.beginRequest(session);
305
+ }
306
+
307
+ if (response.headers.get("content-type")?.includes("text/event-stream")) {
308
+ return wrapStreamingResponse(response, finish);
309
+ }
310
+ finish();
311
+ return response;
312
+ } catch {
313
+ await pending?.discard();
314
+ finish();
315
+ return jsonRpcError(500, -32_603, "Internal MCP transport error");
316
+ }
317
+ }
318
+
319
+ reapIdleSessions(now?: number): Promise<number> {
320
+ return this.#sessions.reapIdleSessions(now);
321
+ }
322
+
323
+ close(): Promise<void> {
324
+ this.#closed = true;
325
+ for (const resolve of this.#capacityWaiters.splice(0)) resolve(false);
326
+ return this.#sessions.closeAll();
327
+ }
328
+
329
+ invalidateAuthenticatedSessions(): Promise<void> {
330
+ return this.#sessions.closeSessions();
331
+ }
332
+
333
+ #acquireCapacity(signal: AbortSignal): Promise<boolean> {
334
+ if (this.#closed || this.#runtime.isShuttingDown)
335
+ return Promise.resolve(false);
336
+ if (signal.aborted) return Promise.resolve(false);
337
+ if (this.#activeRequests < this.#maxConcurrentRequests) {
338
+ this.#activeRequests += 1;
339
+ return Promise.resolve(true);
340
+ }
341
+ if (this.#capacityWaiters.length >= this.#maxQueuedRequests)
342
+ return Promise.resolve(false);
343
+
344
+ return new Promise((resolve) => {
345
+ const complete = (admitted: boolean): void => {
346
+ signal.removeEventListener("abort", onAbort);
347
+ resolve(admitted);
348
+ };
349
+ const onAbort = (): void => {
350
+ const index = this.#capacityWaiters.indexOf(complete);
351
+ if (index >= 0) this.#capacityWaiters.splice(index, 1);
352
+ complete(false);
353
+ };
354
+ signal.addEventListener("abort", onAbort, { once: true });
355
+ this.#capacityWaiters.push(complete);
356
+ });
357
+ }
358
+
359
+ #releaseCapacity(): void {
360
+ this.#activeRequests = Math.max(0, this.#activeRequests - 1);
361
+ const next = this.#capacityWaiters.shift();
362
+ if (!next) return;
363
+ if (this.#closed || this.#runtime.isShuttingDown) {
364
+ next(false);
365
+ return;
366
+ }
367
+ this.#activeRequests += 1;
368
+ next(true);
369
+ }
370
+ }