@gmickel/gno 1.40.0 → 1.42.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 (62) hide show
  1. package/README.md +1 -0
  2. package/assets/skill/SKILL.md +22 -1
  3. package/assets/skill/cli-reference.md +48 -0
  4. package/assets/skill/mcp-reference.md +31 -0
  5. package/browser-extension/artifacts/{gno-browser-clipper-v1.40.0.zip → gno-browser-clipper-v1.42.0.zip} +0 -0
  6. package/browser-extension/artifacts/gno-browser-clipper-v1.42.0.zip.sha256 +1 -0
  7. package/browser-extension/dist/manifest.json +1 -1
  8. package/package.json +3 -2
  9. package/spec/cli.md +146 -7
  10. package/spec/db/schema.sql +17 -0
  11. package/spec/mcp.md +350 -6
  12. package/spec/output-schemas/memory-recall.schema.json +159 -0
  13. package/spec/output-schemas/memory-remember.schema.json +164 -0
  14. package/spec/output-schemas/status.schema.json +269 -54
  15. package/src/cli/commands/daemon.ts +1 -0
  16. package/src/cli/commands/mcp.ts +3 -1
  17. package/src/cli/commands/memory.ts +491 -0
  18. package/src/cli/commands/status.ts +23 -4
  19. package/src/cli/options.ts +4 -0
  20. package/src/cli/program.ts +155 -0
  21. package/src/config/types.ts +10 -0
  22. package/src/core/audit-provenance.ts +91 -0
  23. package/src/core/audit-workspace.ts +17 -0
  24. package/src/core/connector-verifier.ts +2 -4
  25. package/src/core/memory-diagnostics.ts +144 -0
  26. package/src/core/memory-fence.ts +239 -0
  27. package/src/core/memory-recall.ts +269 -0
  28. package/src/core/memory-record.ts +435 -0
  29. package/src/core/memory-remember.ts +425 -0
  30. package/src/core/memory-types.ts +211 -0
  31. package/src/core/memory.ts +87 -0
  32. package/src/ingestion/sync.ts +17 -0
  33. package/src/mcp/AGENTS.md +7 -1
  34. package/src/mcp/CLAUDE.md +7 -1
  35. package/src/mcp/context.ts +37 -7
  36. package/src/mcp/http-egress.ts +2 -0
  37. package/src/mcp/http-modern.ts +214 -0
  38. package/src/mcp/http-security.ts +5 -0
  39. package/src/mcp/http-session.ts +4 -3
  40. package/src/mcp/http-transport.ts +81 -12
  41. package/src/mcp/resources/index.ts +3 -6
  42. package/src/mcp/server.ts +18 -16
  43. package/src/mcp/stdio-serving.ts +45 -0
  44. package/src/mcp/tool-descriptions-core.ts +56 -0
  45. package/src/mcp/tool-profile.ts +112 -0
  46. package/src/mcp/tools/index.ts +286 -126
  47. package/src/mcp/tools/memory-recall.ts +122 -0
  48. package/src/mcp/tools/memory-remember.ts +177 -0
  49. package/src/mcp/tools/memory-shared.ts +86 -0
  50. package/src/pipeline/search.ts +2 -0
  51. package/src/pipeline/types.ts +8 -0
  52. package/src/sdk/client.ts +94 -1
  53. package/src/sdk/index.ts +13 -0
  54. package/src/sdk/types.ts +28 -0
  55. package/src/serve/routes/api.ts +167 -0
  56. package/src/serve/routes/mcp.ts +1 -0
  57. package/src/serve/server.ts +27 -0
  58. package/src/store/migrations/027-memory-scopes.ts +37 -0
  59. package/src/store/migrations/index.ts +2 -0
  60. package/src/store/sqlite/adapter.ts +127 -3
  61. package/src/store/types.ts +54 -0
  62. package/browser-extension/artifacts/gno-browser-clipper-v1.40.0.zip.sha256 +0 -1
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Transport-neutral memory service: `remember()` and `recall()`.
3
+ *
4
+ * Every surface (CLI, MCP, REST, SDK) is a thin adapter over this module.
5
+ * The service owns the shared write lease for every write path; adapters
6
+ * never take `.mcp-write.lock` themselves (single acquisition point, no
7
+ * nesting — a caller that already holds the lease deadlocks/fails fast).
8
+ *
9
+ * This file is the facade: the contracts live in `memory-types`, validation,
10
+ * the context fence, and receipts in `memory-fence`, the write path in
11
+ * `memory-remember`, and retrieval in `memory-recall`. Import from here.
12
+ *
13
+ * @module src/core/memory
14
+ */
15
+
16
+ import type {
17
+ MemoryCandidate,
18
+ MemoryMatchDiagnostics,
19
+ MemoryServiceDeps,
20
+ RecallInput,
21
+ RecallResult,
22
+ RememberInput,
23
+ RememberResult,
24
+ } from "./memory-types";
25
+
26
+ import { recallFacts } from "./memory-recall";
27
+ import {
28
+ type CandidateQuery,
29
+ findMemoryCandidates,
30
+ rememberFact,
31
+ } from "./memory-remember";
32
+
33
+ export {
34
+ MEMORY_CANDIDATE_POOL,
35
+ MEMORY_EMPTY_RECALL_HINT,
36
+ MEMORY_LEXICAL_LIKELY_THRESHOLD,
37
+ MEMORY_RECALL_MAX_FACTS,
38
+ MEMORY_RECALL_MAX_TOKENS,
39
+ MEMORY_SEMANTIC_LIKELY_THRESHOLD,
40
+ MemoryError,
41
+ } from "./memory-types";
42
+ export type {
43
+ MemoryCandidate,
44
+ MemoryCandidateMatch,
45
+ MemoryDecision,
46
+ MemoryErrorCode,
47
+ MemoryFact,
48
+ MemoryIdentity,
49
+ MemoryMatchDiagnostics,
50
+ MemoryMatchMode,
51
+ MemoryRecallReceipt,
52
+ MemoryServiceDeps,
53
+ MemorySyncState,
54
+ RecalledFact,
55
+ RecallInput,
56
+ RecallResult,
57
+ RememberInput,
58
+ RememberResult,
59
+ } from "./memory-types";
60
+
61
+ export class MemoryService {
62
+ private readonly deps: MemoryServiceDeps;
63
+
64
+ constructor(deps: MemoryServiceDeps) {
65
+ this.deps = deps;
66
+ }
67
+
68
+ /**
69
+ * Candidate pool: BM25 top-16 (any-term) within the scope intersection,
70
+ * current facts only. Similarity by cosine when semantic is ready, else by
71
+ * normalized-token Jaccard. Ordered by similarity desc, ties by recordId.
72
+ */
73
+ findCandidates(input: CandidateQuery): Promise<{
74
+ candidates: MemoryCandidate[];
75
+ matching: MemoryMatchDiagnostics;
76
+ }> {
77
+ return findMemoryCandidates(this.deps, input);
78
+ }
79
+
80
+ remember(input: RememberInput): Promise<RememberResult> {
81
+ return rememberFact(this.deps, input);
82
+ }
83
+
84
+ recall(input: RecallInput): Promise<RecallResult> {
85
+ return recallFacts(this.deps, input);
86
+ }
87
+ }
@@ -60,6 +60,7 @@ import {
60
60
  parseLinks,
61
61
  parseTargetParts,
62
62
  } from "../core/links";
63
+ import { extractMemoryScopes } from "../core/memory-record";
63
64
  import { normalizeTag, validateTag } from "../core/tags";
64
65
  import { defaultChunker } from "./chunker";
65
66
  import {
@@ -1074,6 +1075,22 @@ export class SyncService {
1074
1075
  tagCount: extractedTags.length,
1075
1076
  });
1076
1077
 
1078
+ // 13b. Index managed-memory scopes, memory-managed collections only.
1079
+ // A record that passes the memory validator gets scope rows; a
1080
+ // malformed file clears them and therefore drops out of managed
1081
+ // recall while staying searchable.
1082
+ if (collection.memoryManaged === true) {
1083
+ const memoryScopes = extractMemoryScopes(artifact.markdown);
1084
+ const scopesResult = await store.setDocMemoryScopes(
1085
+ docId,
1086
+ memoryScopes
1087
+ );
1088
+ mustOk(scopesResult, "setDocMemoryScopes", {
1089
+ docId,
1090
+ scopeCount: memoryScopes.length,
1091
+ });
1092
+ }
1093
+
1077
1094
  // 14. Extract and store links (wiki and markdown links)
1078
1095
  const excludedRanges = getExcludedRanges(artifact.markdown);
1079
1096
  const lineOffsets = buildLineOffsets(artifact.markdown);
package/src/mcp/AGENTS.md CHANGED
@@ -6,7 +6,13 @@ GNO's Model Context Protocol server for AI agent integration.
6
6
 
7
7
  ```
8
8
  src/mcp/
9
- ├── server.ts # MCP server setup, stdio transport
9
+ ├── server.ts # MCP server setup (`gno mcp`)
10
+ ├── stdio-serving.ts # Dual-era stdio entry (2025 initialize / 2026 discover)
11
+ ├── http-transport.ts # Resident /mcp gateway: shared guards, era branch
12
+ ├── http-session.ts # 2025-era stateful sessions
13
+ ├── http-modern.ts # 2026-07-28 sessionless leg + modern request checks
14
+ ├── tool-profile.ts # core|full profile allowlists + registrar
15
+ ├── tool-descriptions-core.ts # core-profile description micro-instructions
10
16
  ├── tools/ # Tool implementations
11
17
  │ ├── index.ts # Tool registry
12
18
  │ ├── search.ts # gno_search (BM25)
package/src/mcp/CLAUDE.md CHANGED
@@ -6,7 +6,13 @@ GNO's Model Context Protocol server for AI agent integration.
6
6
 
7
7
  ```
8
8
  src/mcp/
9
- ├── server.ts # MCP server setup, stdio transport
9
+ ├── server.ts # MCP server setup (`gno mcp`)
10
+ ├── stdio-serving.ts # Dual-era stdio entry (2025 initialize / 2026 discover)
11
+ ├── http-transport.ts # Resident /mcp gateway: shared guards, era branch
12
+ ├── http-session.ts # 2025-era stateful sessions
13
+ ├── http-modern.ts # 2026-07-28 sessionless leg + modern request checks
14
+ ├── tool-profile.ts # core|full profile allowlists + registrar
15
+ ├── tool-descriptions-core.ts # core-profile description micro-instructions
10
16
  ├── tools/ # Tool implementations
11
17
  │ ├── index.ts # Tool registry
12
18
  │ ├── search.ts # gno_search (BM25)
@@ -1,6 +1,6 @@
1
1
  /** Shared MCP surface and request-scoped runtime context. */
2
2
 
3
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { McpServer } from "@modelcontextprotocol/server";
4
4
  // node:async_hooks provides async-local request context; Bun has no separate native equivalent.
5
5
  import { AsyncLocalStorage } from "node:async_hooks";
6
6
 
@@ -15,6 +15,7 @@ import type { ModelLease } from "../llm/nodeLlamaCpp/lifecycle";
15
15
  import type { ResidentStatus } from "../serve/status-model";
16
16
  import type { SqliteAdapter } from "../store/sqlite/adapter";
17
17
  import type { StoreResult } from "../store/types";
18
+ import type { McpToolProfile } from "./tool-profile";
18
19
 
19
20
  import { MCP_SERVER_NAME, VERSION } from "../app/constants";
20
21
  import { createStandaloneResidentStatus } from "../serve/resident-status";
@@ -57,6 +58,17 @@ export interface ToolContextSnapshot {
57
58
  caller: EgressCallerContext;
58
59
  authorizationEpoch?: { value: string };
59
60
  };
61
+ /**
62
+ * Opaque per-caller identity the HTTP boundary derived for this request.
63
+ * Set on both Streamable HTTP legs; the 2026-07-28 sessionless leg has no
64
+ * session id, so this is what keeps two modern callers apart.
65
+ */
66
+ requestIdentity?: string;
67
+ }
68
+
69
+ export interface RequestScope {
70
+ egress: NonNullable<ToolContextSnapshot["egress"]>;
71
+ requestIdentity?: string;
60
72
  }
61
73
 
62
74
  export interface ToolContext {
@@ -70,6 +82,8 @@ export interface ToolContext {
70
82
  serverInstanceId: string;
71
83
  writeLockPath: string;
72
84
  enableWrite: boolean;
85
+ /** Advertised tool set; `full` when absent. Narrows, never widens, the write gate. */
86
+ toolProfile?: McpToolProfile;
73
87
  isShuttingDown: () => boolean;
74
88
  getResidentStatus?: () => ResidentStatus;
75
89
  acquireModelLease?: () => ModelLease;
@@ -87,9 +101,12 @@ export interface ToolContext {
87
101
  advanceRequestAuthorizationEpoch?: (epoch: string) => void;
88
102
  getRequestAuthorizationEpoch?: () => string | undefined;
89
103
  getEgressContext?: () => ToolContextSnapshot["egress"];
104
+ /** Per-caller identity of the current request; absent on stdio. */
105
+ getRequestIdentity?: () => string | undefined;
90
106
  runWithEgressContext?<T>(
91
107
  egress: NonNullable<ToolContextSnapshot["egress"]>,
92
- operation: () => Promise<T>
108
+ operation: () => Promise<T>,
109
+ scope?: Omit<RequestScope, "egress">
93
110
  ): Promise<T>;
94
111
  runWithSnapshot?<T>(operation: () => Promise<T>): Promise<T>;
95
112
  }
@@ -105,6 +122,7 @@ export interface CreateToolContextOptions {
105
122
  serverInstanceId: string;
106
123
  writeLockPath: string;
107
124
  enableWrite: boolean;
125
+ toolProfile?: McpToolProfile;
108
126
  isShuttingDown: () => boolean;
109
127
  getResidentStatus?: () => ResidentStatus;
110
128
  acquireModelLease?: () => ModelLease;
@@ -153,6 +171,7 @@ export function createToolContext(
153
171
  serverInstanceId: options.serverInstanceId,
154
172
  writeLockPath: options.writeLockPath,
155
173
  enableWrite: options.enableWrite,
174
+ toolProfile: options.toolProfile,
156
175
  isShuttingDown: options.isShuttingDown,
157
176
  getResidentStatus:
158
177
  options.getResidentStatus ??
@@ -169,23 +188,32 @@ export function createToolContext(
169
188
  getRequestAuthorizationEpoch: () =>
170
189
  requestSnapshot.getStore()?.egress?.authorizationEpoch?.value,
171
190
  getEgressContext: () => requestSnapshot.getStore()?.egress,
191
+ getRequestIdentity: () => requestSnapshot.getStore()?.requestIdentity,
172
192
  runWithEgressContext<T>(
173
193
  egress: NonNullable<ToolContextSnapshot["egress"]>,
174
- operation: () => Promise<T>
194
+ operation: () => Promise<T>,
195
+ scope?: Omit<RequestScope, "egress">
175
196
  ): Promise<T> {
176
197
  const config = options.getConfig();
177
198
  return requestSnapshot.run(
178
- { config, collections: config.collections, egress },
199
+ {
200
+ config,
201
+ collections: config.collections,
202
+ egress,
203
+ requestIdentity: scope?.requestIdentity,
204
+ },
179
205
  operation
180
206
  );
181
207
  },
182
208
  runWithSnapshot<T>(operation: () => Promise<T>): Promise<T> {
183
209
  const config = options.getConfig();
210
+ const current = requestSnapshot.getStore();
184
211
  return requestSnapshot.run(
185
212
  {
186
213
  config,
187
214
  collections: config.collections,
188
- egress: requestSnapshot.getStore()?.egress,
215
+ egress: current?.egress,
216
+ requestIdentity: current?.requestIdentity,
189
217
  },
190
218
  operation
191
219
  );
@@ -201,10 +229,12 @@ export function createMcpServerSurface(
201
229
  version: VERSION,
202
230
  }
203
231
  ): McpServer {
232
+ // `listChanged: true` is the advertised 2025-11-25 contract pinned by
233
+ // test/fixtures/mcp/legacy-2025-11-25.json (SDK v1 always advertised it).
204
234
  const server = new McpServer(identity, {
205
235
  capabilities: {
206
- tools: { listChanged: false },
207
- resources: { subscribe: false, listChanged: false },
236
+ tools: { listChanged: true },
237
+ resources: { subscribe: false, listChanged: true },
208
238
  },
209
239
  });
210
240
  registerTools(server, context);
@@ -51,6 +51,8 @@ export const MCP_HTTP_EGRESS_TOOLS = {
51
51
  gno_multi_get: "source",
52
52
  gno_query: "snippet",
53
53
  gno_query_diagnose: "metadata",
54
+ gno_recall: "source",
55
+ gno_remember: "source",
54
56
  gno_remove_collection: "metadata",
55
57
  gno_rename_note: "metadata",
56
58
  gno_search: "snippet",
@@ -0,0 +1,214 @@
1
+ /** 2026-07-28 (sessionless) leg of the resident Streamable HTTP transport. */
2
+
3
+ import {
4
+ createMcpHandler,
5
+ isLegacyRequest,
6
+ type McpServer,
7
+ PROTOCOL_VERSION_META_KEY,
8
+ } from "@modelcontextprotocol/server";
9
+
10
+ import type { ToolContext } from "./context";
11
+
12
+ export const MCP_LEGACY_PROTOCOL_VERSION = "2025-11-25";
13
+ export const MCP_MODERN_PROTOCOL_VERSION = "2026-07-28";
14
+ /**
15
+ * The exact protocol revisions GNO speaks. Membership here is the only test a
16
+ * revision label passes; there is no ordering, so a future-dated or
17
+ * non-date label (`2027-01-01`, `abc`) is never treated as modern.
18
+ */
19
+ export const MCP_SUPPORTED_PROTOCOL_REVISIONS: ReadonlySet<string> = new Set([
20
+ MCP_LEGACY_PROTOCOL_VERSION,
21
+ MCP_MODERN_PROTOCOL_VERSION,
22
+ ]);
23
+ /** Revisions served by the sessionless leg. */
24
+ const MCP_MODERN_PROTOCOL_REVISIONS: ReadonlySet<string> = new Set([
25
+ MCP_MODERN_PROTOCOL_VERSION,
26
+ ]);
27
+ /**
28
+ * Modern methods the SDK serves as a long-lived stream. GNO wires no change
29
+ * event source to them, and a stream that never ends would pin a capacity
30
+ * slot and an admission handle for the life of the connection, so they are
31
+ * refused before the SDK handler is reached.
32
+ */
33
+ const MCP_UNSUPPORTED_MODERN_STREAM_METHODS: ReadonlySet<string> = new Set([
34
+ "subscriptions/listen",
35
+ ]);
36
+ const MCP_PROTOCOL_VERSION_HEADER = "mcp-protocol-version";
37
+ const MCP_SESSION_HEADER = "mcp-session-id";
38
+ /** SEP-2243 `HeaderMismatch`: the standard headers and the body disagree. */
39
+ const HEADER_MISMATCH_ERROR_CODE = -32_020;
40
+ const INVALID_REQUEST_ERROR_CODE = -32_600;
41
+ const METHOD_NOT_FOUND_ERROR_CODE = -32_601;
42
+ const SERVER_ERROR_CODE = -32_000;
43
+ /** The 2026-07-28 HTTP ladder answers a pre-dispatch method-not-found with 404. */
44
+ const METHOD_NOT_FOUND_HTTP_STATUS = 404;
45
+
46
+ export interface ModernMcpHandler {
47
+ fetch(request: Request, parsedBody: unknown): Promise<Response>;
48
+ close(): Promise<void>;
49
+ }
50
+
51
+ function jsonRpcError(
52
+ status: number,
53
+ code: number,
54
+ message: string,
55
+ data?: unknown,
56
+ id: string | number | null = null
57
+ ): Response {
58
+ return Response.json(
59
+ {
60
+ jsonrpc: "2.0",
61
+ error: { code, message, ...(data === undefined ? {} : { data }) },
62
+ id,
63
+ },
64
+ { status }
65
+ );
66
+ }
67
+
68
+ function jsonRpcMessages(parsedBody: unknown): unknown[] {
69
+ return Array.isArray(parsedBody) ? parsedBody : [parsedBody];
70
+ }
71
+
72
+ function methodOf(message: unknown): string | undefined {
73
+ if (typeof message !== "object" || message === null) return undefined;
74
+ const { method } = message as { method?: unknown };
75
+ return typeof method === "string" ? method : undefined;
76
+ }
77
+
78
+ function echoableId(message: unknown): string | number | null {
79
+ if (typeof message !== "object" || message === null) return null;
80
+ const { id } = message as { id?: unknown };
81
+ return typeof id === "string" || typeof id === "number" ? id : null;
82
+ }
83
+
84
+ function carriesEnvelopeClaim(message: unknown): boolean {
85
+ if (typeof message !== "object" || message === null) return false;
86
+ const meta = (message as { params?: { _meta?: unknown } }).params?._meta;
87
+ return (
88
+ typeof meta === "object" &&
89
+ meta !== null &&
90
+ PROTOCOL_VERSION_META_KEY in meta
91
+ );
92
+ }
93
+
94
+ function namesModernRevision(request: Request): boolean {
95
+ const header = request.headers.get(MCP_PROTOCOL_VERSION_HEADER);
96
+ return header !== null && MCP_MODERN_PROTOCOL_REVISIONS.has(header);
97
+ }
98
+
99
+ /**
100
+ * Whether the transport routes this request to the 2026-07-28 sessionless
101
+ * leg instead of the 2025-era session path.
102
+ *
103
+ * The SDK's own classifier decides legacy-ness so the branch can never
104
+ * disagree with `createMcpHandler`. Only a request that actually claims the
105
+ * modern era - a per-request `_meta` envelope claim (well-formed or not), or
106
+ * an `MCP-Protocol-Version` header naming a modern revision - is served
107
+ * modern; a body the classifier rejects without any such claim keeps the
108
+ * legacy path's established error answers. Body-less methods (GET, DELETE)
109
+ * are 2025 session operations and always legacy; the modern era has no
110
+ * sessions.
111
+ */
112
+ export async function isModernMcpRequest(
113
+ request: Request,
114
+ parsedBody: unknown
115
+ ): Promise<boolean> {
116
+ if (request.method !== "POST") return false;
117
+ if (await isLegacyRequest(request, parsedBody)) return false;
118
+ return (
119
+ jsonRpcMessages(parsedBody).some(carriesEnvelopeClaim) ||
120
+ namesModernRevision(request)
121
+ );
122
+ }
123
+
124
+ /**
125
+ * Refuse a modern request for a method the SDK would serve as a long-lived
126
+ * stream (`subscriptions/listen`). The answer is the 2026-07-28 ladder's
127
+ * pre-dispatch method-not-found (`404`, `-32601`) with the request id echoed,
128
+ * so the client learns the method is absent here rather than waiting on a
129
+ * stream that would never carry an event. The check runs before dispatch and
130
+ * releases nothing itself: the caller finishes the request like any other
131
+ * rejection, so no capacity slot or admission handle outlives the answer.
132
+ */
133
+ export function rejectUnsupportedModernStream(
134
+ parsedBody: unknown
135
+ ): Response | undefined {
136
+ const message = jsonRpcMessages(parsedBody).find((candidate) => {
137
+ const method = methodOf(candidate);
138
+ return (
139
+ method !== undefined && MCP_UNSUPPORTED_MODERN_STREAM_METHODS.has(method)
140
+ );
141
+ });
142
+ if (message === undefined) return undefined;
143
+ const method = methodOf(message) ?? "";
144
+ return jsonRpcError(
145
+ METHOD_NOT_FOUND_HTTP_STATUS,
146
+ METHOD_NOT_FOUND_ERROR_CODE,
147
+ `Method not found: ${method} is not served by this endpoint; GNO change events are not wired to subscription streams`,
148
+ { method },
149
+ echoableId(message)
150
+ );
151
+ }
152
+
153
+ /**
154
+ * GNO-owned pre-dispatch checks for a modern-classified request.
155
+ *
156
+ * - A POST that is not `application/json` is refused with 415, as the
157
+ * session transport refuses it, before the SDK's modern leg sees it.
158
+ * - The 2026-07-28 Streamable HTTP binding requires `MCP-Protocol-Version`
159
+ * on every request; a modern envelope without the header is refused, never
160
+ * served from the body claim alone.
161
+ * - Sessions are 2025-era state. A modern request that names one is a
162
+ * protocol confusion and is rejected before it can touch another
163
+ * identity's session.
164
+ */
165
+ export function rejectMalformedModernRequest(
166
+ request: Request
167
+ ): Response | undefined {
168
+ if (!request.headers.get("content-type")?.includes("application/json")) {
169
+ return jsonRpcError(
170
+ 415,
171
+ SERVER_ERROR_CODE,
172
+ "Unsupported Media Type: Content-Type must be application/json"
173
+ );
174
+ }
175
+ if (request.headers.has(MCP_SESSION_HEADER)) {
176
+ return jsonRpcError(
177
+ 400,
178
+ INVALID_REQUEST_ERROR_CODE,
179
+ `Bad Request: Mcp-Session-Id is not valid on a ${MCP_MODERN_PROTOCOL_VERSION} request; sessions are 2025-era only`
180
+ );
181
+ }
182
+ if (!request.headers.has(MCP_PROTOCOL_VERSION_HEADER)) {
183
+ const body = `the body envelope claims protocol revision ${MCP_MODERN_PROTOCOL_VERSION} but the required MCP-Protocol-Version header is absent`;
184
+ return jsonRpcError(
185
+ 400,
186
+ HEADER_MISMATCH_ERROR_CODE,
187
+ `Bad Request: the request headers and body disagree: ${body}`,
188
+ { mismatch: { header: "(missing)", body } }
189
+ );
190
+ }
191
+ return undefined;
192
+ }
193
+
194
+ /**
195
+ * Sessionless per-request serving for 2026-07-28 clients.
196
+ *
197
+ * Strictly modern (`legacy: "reject"`): every 2025-era request is routed to
198
+ * the stateful session transport before this handler is reached, so a legacy
199
+ * `initialize` can never negotiate a 2026 era here. The factory builds one
200
+ * surface per request from the shared runtime context, so profile, write
201
+ * gate, and egress context are the same objects the session path uses.
202
+ */
203
+ export function createModernMcpHandler(
204
+ context: ToolContext,
205
+ createServer: (context: ToolContext) => McpServer
206
+ ): ModernMcpHandler {
207
+ const handler = createMcpHandler(() => createServer(context), {
208
+ legacy: "reject",
209
+ });
210
+ return {
211
+ fetch: (request, parsedBody) => handler.fetch(request, { parsedBody }),
212
+ close: () => handler.close(),
213
+ };
214
+ }
@@ -11,6 +11,7 @@ import {
11
11
  classifyBindDestination,
12
12
  classifyDestination,
13
13
  } from "../core/destination-classifier";
14
+ import { DEFAULT_MCP_TOOL_PROFILE, type McpToolProfile } from "./tool-profile";
14
15
 
15
16
  export const DEFAULT_HTTP_GATEWAY_HOST = "127.0.0.1";
16
17
  export const DEFAULT_HTTP_GATEWAY_PORT = 3000;
@@ -39,6 +40,7 @@ export interface ResolvedHttpGatewayConfig {
39
40
  allowedHosts: readonly string[];
40
41
  allowedOrigins: readonly string[];
41
42
  enableWrite: boolean;
43
+ toolProfile: McpToolProfile;
42
44
  limits: {
43
45
  maxBodyBytes: number;
44
46
  maxRequestsPerMinute: number;
@@ -56,6 +58,7 @@ export interface HttpGatewayOverrides {
56
58
  allowedHosts?: string[];
57
59
  allowedOrigins?: string[];
58
60
  enableWrite?: boolean;
61
+ toolProfile?: McpToolProfile;
59
62
  }
60
63
 
61
64
  export interface AuthorizedHttpMcpRequest {
@@ -178,6 +181,8 @@ export function resolveHttpGatewayConfig(
178
181
  config?.allowedOrigins ??
179
182
  defaultAllowedOrigins(host, port),
180
183
  enableWrite: overrides.enableWrite ?? config?.enableWrite ?? false,
184
+ toolProfile:
185
+ overrides.toolProfile ?? config?.toolProfile ?? DEFAULT_MCP_TOOL_PROFILE,
181
186
  limits: {
182
187
  maxBodyBytes:
183
188
  config?.limits?.maxBodyBytes ?? DEFAULT_HTTP_MCP_MAX_BODY_BYTES,
@@ -1,8 +1,9 @@
1
1
  /** Isolated stateful MCP server/transport ownership for HTTP sessions. */
2
2
 
3
- import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
-
5
- import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
3
+ import {
4
+ type McpServer,
5
+ WebStandardStreamableHTTPServerTransport,
6
+ } from "@modelcontextprotocol/server";
6
7
 
7
8
  import type { ToolContext } from "./context";
8
9