@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.
- package/README.md +1 -0
- package/assets/skill/SKILL.md +22 -1
- package/assets/skill/cli-reference.md +48 -0
- package/assets/skill/mcp-reference.md +31 -0
- package/browser-extension/artifacts/{gno-browser-clipper-v1.40.0.zip → gno-browser-clipper-v1.42.0.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.42.0.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +3 -2
- package/spec/cli.md +146 -7
- package/spec/db/schema.sql +17 -0
- package/spec/mcp.md +350 -6
- package/spec/output-schemas/memory-recall.schema.json +159 -0
- package/spec/output-schemas/memory-remember.schema.json +164 -0
- package/spec/output-schemas/status.schema.json +269 -54
- package/src/cli/commands/daemon.ts +1 -0
- package/src/cli/commands/mcp.ts +3 -1
- package/src/cli/commands/memory.ts +491 -0
- package/src/cli/commands/status.ts +23 -4
- package/src/cli/options.ts +4 -0
- package/src/cli/program.ts +155 -0
- package/src/config/types.ts +10 -0
- package/src/core/audit-provenance.ts +91 -0
- package/src/core/audit-workspace.ts +17 -0
- package/src/core/connector-verifier.ts +2 -4
- package/src/core/memory-diagnostics.ts +144 -0
- package/src/core/memory-fence.ts +239 -0
- package/src/core/memory-recall.ts +269 -0
- package/src/core/memory-record.ts +435 -0
- package/src/core/memory-remember.ts +425 -0
- package/src/core/memory-types.ts +211 -0
- package/src/core/memory.ts +87 -0
- package/src/ingestion/sync.ts +17 -0
- package/src/mcp/AGENTS.md +7 -1
- package/src/mcp/CLAUDE.md +7 -1
- package/src/mcp/context.ts +37 -7
- package/src/mcp/http-egress.ts +2 -0
- package/src/mcp/http-modern.ts +214 -0
- package/src/mcp/http-security.ts +5 -0
- package/src/mcp/http-session.ts +4 -3
- package/src/mcp/http-transport.ts +81 -12
- package/src/mcp/resources/index.ts +3 -6
- package/src/mcp/server.ts +18 -16
- package/src/mcp/stdio-serving.ts +45 -0
- package/src/mcp/tool-descriptions-core.ts +56 -0
- package/src/mcp/tool-profile.ts +112 -0
- package/src/mcp/tools/index.ts +286 -126
- package/src/mcp/tools/memory-recall.ts +122 -0
- package/src/mcp/tools/memory-remember.ts +177 -0
- package/src/mcp/tools/memory-shared.ts +86 -0
- package/src/pipeline/search.ts +2 -0
- package/src/pipeline/types.ts +8 -0
- package/src/sdk/client.ts +94 -1
- package/src/sdk/index.ts +13 -0
- package/src/sdk/types.ts +28 -0
- package/src/serve/routes/api.ts +167 -0
- package/src/serve/routes/mcp.ts +1 -0
- package/src/serve/server.ts +27 -0
- package/src/store/migrations/027-memory-scopes.ts +37 -0
- package/src/store/migrations/index.ts +2 -0
- package/src/store/sqlite/adapter.ts +127 -3
- package/src/store/types.ts +54 -0
- 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
|
+
}
|
package/src/ingestion/sync.ts
CHANGED
|
@@ -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
|
|
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
|
|
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/context.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** Shared MCP surface and request-scoped runtime context. */
|
|
2
2
|
|
|
3
|
-
import { McpServer } from "@modelcontextprotocol/
|
|
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
|
-
{
|
|
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:
|
|
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:
|
|
207
|
-
resources: { subscribe: false, listChanged:
|
|
236
|
+
tools: { listChanged: true },
|
|
237
|
+
resources: { subscribe: false, listChanged: true },
|
|
208
238
|
},
|
|
209
239
|
});
|
|
210
240
|
registerTools(server, context);
|
package/src/mcp/http-egress.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/mcp/http-security.ts
CHANGED
|
@@ -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,
|
package/src/mcp/http-session.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/** Isolated stateful MCP server/transport ownership for HTTP sessions. */
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
import {
|
|
4
|
+
type McpServer,
|
|
5
|
+
WebStandardStreamableHTTPServerTransport,
|
|
6
|
+
} from "@modelcontextprotocol/server";
|
|
6
7
|
|
|
7
8
|
import type { ToolContext } from "./context";
|
|
8
9
|
|