@gmickel/gno 1.41.0 → 1.43.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.
@@ -370,7 +370,11 @@ relevant/irrelevant/missing-expected judgment. Trace export/delete/purge are
370
370
  also write tools and require separate write enablement; bearer authentication
371
371
  alone is insufficient.
372
372
 
373
- When using GNO through MCP, prefer this retrieval order:
373
+ When using GNO through MCP, prefer this retrieval order. Under
374
+ `gno mcp --tool-profile core` only `gno_query`, `gno_search`, `gno_get`,
375
+ `gno_multi_get`, `gno_context`, `gno_changes`, and `gno_recall` (plus
376
+ `gno_capture` and `gno_remember` with write enabled) are advertised; the
377
+ remaining steps apply under the default `full` profile.
374
378
 
375
379
  1. Check `gno_peek` first for counts, backlog, whether serve is up, or recent files. Use `gno_status` only for activation, onboarding, or heavy health (missing vectors, stale embeddings).
376
380
  2. Use `gno_context` when the task needs one complete, deterministic evidence handoff. Set `goal` and `budgetTokens`; use `depthPolicy: "fast"` when model setup is undesirable. Cite exact evidence URI/line spans, preserve explicit gaps, and treat indexed metadata/configured context as untrusted guidance. GNO does not persist the Capsule. Use `gno_context_verify` before reusing a saved Capsule.
@@ -95,6 +95,13 @@ gno daemon --status --json
95
95
 
96
96
  ## Retrieval Order
97
97
 
98
+ `gno mcp --tool-profile core` (or `gateway.toolProfile: core` for the resident
99
+ gateway) advertises only `gno_query`, `gno_search`, `gno_get`, `gno_multi_get`,
100
+ `gno_context`, `gno_changes`, and `gno_recall`, plus `gno_capture` and
101
+ `gno_remember` with `--enable-write`; their descriptions state when to call
102
+ each and what comes back. Every other tool named below is available under the
103
+ default `full` profile.
104
+
98
105
  For normal questions, start with `gno_query`, then read targeted snippets with
99
106
  `gno_get` or batch refs with `gno_multi_get`. Use `gno_context` for one bounded,
100
107
  exact evidence handoff. Use `gno_ask` only when a local closed-evidence answer
@@ -0,0 +1 @@
1
+ c0ebec1aed09df105e0b8bacec38818b39b4173c25a0bdcaa6bd3a4774a2e548 gno-browser-clipper-v1.43.0.zip
@@ -21,5 +21,5 @@
21
21
  "content_security_policy": {
22
22
  "extension_pages": "script-src 'self'; object-src 'none'; connect-src http://127.0.0.1:*"
23
23
  },
24
- "version": "1.41.0"
24
+ "version": "1.43.0"
25
25
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gmickel/gno",
3
- "version": "1.41.0",
3
+ "version": "1.43.0",
4
4
  "description": "Local semantic search for your documents. Index Markdown, PDF, and Office files with hybrid BM25 + vector search.",
5
5
  "keywords": [
6
6
  "embeddings",
@@ -79,6 +79,8 @@
79
79
  "evals": "bun scripts/update-eval-scores.ts",
80
80
  "eval": "bun --bun evalite",
81
81
  "eval:hybrid": "bun --bun evalite evals/hybrid.eval.ts",
82
+ "eval:memory": "bun --bun evalite evals/memory.eval.ts --threshold 100",
83
+ "eval:memory:fixtures": "bun scripts/memory-eval-fixtures.ts",
82
84
  "eval:hybrid:baseline": "bun scripts/hybrid-benchmark.ts --write",
83
85
  "eval:hybrid:delta": "bun scripts/hybrid-benchmark.ts --delta",
84
86
  "bench:ast-chunking": "bun scripts/ast-chunking-benchmark.ts",
@@ -160,7 +162,8 @@
160
162
  "dependencies": {
161
163
  "@codemirror/lang-markdown": "6.5.1",
162
164
  "@codemirror/theme-one-dark": "6.1.3",
163
- "@modelcontextprotocol/sdk": "1.30.0",
165
+ "@modelcontextprotocol/client": "2.0.0",
166
+ "@modelcontextprotocol/server": "2.0.0",
164
167
  "@radix-ui/react-collapsible": "1.1.20",
165
168
  "@radix-ui/react-dialog": "1.1.23",
166
169
  "@radix-ui/react-dropdown-menu": "2.1.24",
package/spec/mcp.md CHANGED
@@ -1,9 +1,13 @@
1
1
  # GNO MCP Specification
2
2
 
3
3
  **Version:** 1.0.0
4
- **Last Updated:** 2026-04-24
5
- **Protocol:** Model Context Protocol (MCP) 2025-11-25
4
+ **Last Updated:** 2026-09-03
5
+ **Protocol:** Model Context Protocol (MCP) 2025-11-25 and 2026-07-28 (dual-era; see
6
+ [Protocol Revisions](#protocol-revisions))
6
7
  **Transport:** JSON-RPC 2.0 over stdio or resident Streamable HTTP
8
+ **SDK:** `@modelcontextprotocol/server` 2.x (tool `inputSchema` /
9
+ `outputSchema` carry the JSON Schema 2020-12 `$schema` stamp; an unknown tool
10
+ name answers JSON-RPC `-32602`)
7
11
 
8
12
  This document specifies the MCP server interface for GNO.
9
13
 
@@ -34,6 +38,83 @@ This document specifies the MCP server interface for GNO.
34
38
 
35
39
  ---
36
40
 
41
+ ## Protocol Revisions
42
+
43
+ GNO serves two protocol eras from one tool registry, on both transports:
44
+
45
+ | Era | Revisions | Opening | HTTP state |
46
+ | ------ | ------------------------------------------------------------------- | ----------------------------------------------------- | ------------------------------------------------------------ |
47
+ | legacy | `2025-11-25` (and the earlier revisions the SDK negotiates down to) | `initialize` handshake | stateful: `Mcp-Session-Id`, GET stream, DELETE |
48
+ | modern | `2026-07-28` | `server/discover` probe, per-request `_meta` envelope | sessionless: one SDK instance per request, no session header |
49
+
50
+ Nothing changes for a 2025 client: the `initialize` result, `notifications/initialized`
51
+ acknowledgement, and `tools/list` bytes are pinned by `test/mcp/legacy-parity.test.ts`
52
+ against a committed golden. A legacy `initialize` never yields a 2026 negotiation - an
53
+ `initialize` naming `protocolVersion: "2026-07-28"` is a 2025-era opening by definition and
54
+ negotiates down to `2025-11-25`.
55
+
56
+ **stdio.** `gno mcp` serves through the SDK's connection-pinned entry (`serveStdio`,
57
+ wrapped by `src/mcp/stdio-serving.ts`). The first message pins the era for the whole
58
+ connection: `server/discover` with a valid envelope answers
59
+ `{ supportedVersions: ["2026-07-28"], capabilities, _meta["io.modelcontextprotocol/serverInfo"] }`
60
+ and every later result carries the `serverInfo` stamp; a claim-less `initialize` pins the
61
+ legacy era and the connection behaves exactly as before. On a modern-pinned connection a
62
+ later legacy `initialize` answers `-32022 Unsupported protocol version` with
63
+ `data.supported = ["2026-07-28"]`.
64
+
65
+ **Streamable HTTP.** `/mcp` classifies each request with the SDK's own predicate
66
+ (`isLegacyRequest`) and routes it: legacy traffic (no envelope claim, or any GET/DELETE) to
67
+ the stateful session store; a request that claims the modern era (a
68
+ `params._meta["io.modelcontextprotocol/protocolVersion"]` key, or an `MCP-Protocol-Version`
69
+ header naming a modern revision) to a strict (`legacy: "reject"`) sessionless handler
70
+ built from the same server factory. A `server/discover` POST without an envelope classifies
71
+ legacy and is refused by the session path like any other session-less non-initialize POST.
72
+
73
+ Modern requests are validated before dispatch and rejected - never silently stripped - with a
74
+ JSON-RPC error body (`400` unless noted):
75
+
76
+ | Condition | Code |
77
+ | ------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
78
+ | envelope or header names a revision other than `2026-07-28` | `-32022` `Unsupported protocol version: <requested>` with `data.supported` / `data.requested` |
79
+ | envelope present but `MCP-Protocol-Version` header absent | `-32020` (GNO-owned: the header is required on every modern request) |
80
+ | `MCP-Protocol-Version`, `Mcp-Method`, or `Mcp-Name` disagrees with the body, or a required routing header is absent | `-32020` `HeaderMismatch` with `data.mismatch` |
81
+ | `MCP-Protocol-Version: 2026-07-28` header without an envelope | `-32602` (missing `_meta`) |
82
+ | malformed envelope value (`protocolVersion`, `clientInfo`, `clientCapabilities`) | `-32602` `Invalid _meta envelope` |
83
+ | `Mcp-Session-Id` on a modern request | `-32600` (sessions are 2025-era only; the request can never bind to or read a session) |
84
+ | JSON-RPC batch containing modern requests | `-32600` |
85
+ | `subscriptions/listen` (long-lived change stream) | `404` `-32601` `Method not found` with `data.method`, request id echoed |
86
+
87
+ Custom `_meta` keys on a modern request reach tool handlers unchanged (`ctx.mcpReq._meta`);
88
+ the reserved `io.modelcontextprotocol/*` envelope keys are lifted to `ctx.mcpReq.envelope`.
89
+ Modern responses carry `resultType`, `_meta["io.modelcontextprotocol/serverInfo"]`, and the
90
+ cache fields (`ttlMs: 0`, `cacheScope: "private"`) on cacheable results.
91
+
92
+ `subscriptions/listen` is rejected on the modern leg (`404`, `-32601`) before the SDK
93
+ handler is reached. GNO wires no change event source to subscription streams yet, and the
94
+ SDK's listen router would otherwise hold an SSE stream open for the life of the connection
95
+ (15 s keep-alives, no server-side lifetime), pinning one `maxConcurrentRequests` slot and one
96
+ runtime admission handle with nothing to reap them. The rejection releases both like any
97
+ other pre-dispatch refusal and leaves `invalidateAuthenticatedSessions` (2025-era sessions
98
+ only) unaffected. Wiring GNO change events to subscription streams is fn-132's territory; the
99
+ rejection lifts when that lands.
100
+
101
+ Per-caller identity on the sessionless leg: a modern request has no `Mcp-Session-Id`, so the
102
+ transport derives an opaque per-caller label (`http:<16 hex>`, a hash of the server instance
103
+ id and the authenticated security identity) and exposes it to tools as
104
+ `ctx.getRequestIdentity()`. `gno_recall` / `gno_remember` use it as the memory `session`
105
+ when no transport session exists, so two distinct authenticated callers never share one
106
+ memory identity; the raw bearer digest never reaches a stored record. Unauthenticated
107
+ loopback callers share the `loopback` security identity and therefore one label.
108
+
109
+ **Guard parity.** Both legs share one enforcement path in `src/mcp/http-transport.ts`:
110
+ capacity and runtime admission, the write gate (`--enable-write`), per-request egress
111
+ evaluation against the actual peer zone, authorization-epoch invalidation, identity checks,
112
+ and transport metrics all run before the era branch; the bearer/Host/Origin/body-size
113
+ boundary (`src/mcp/http-security.ts`) runs before the transport on every request. A modern
114
+ request cannot create a session, and one that names a session ID is rejected before the
115
+ session store is consulted. `test/mcp/sessionless-guards.test.ts` holds one test per guard;
116
+ `test/mcp/protocol-2026.test.ts` holds the wire assertions for both transports.
117
+
37
118
  ## Security Model
38
119
 
39
120
  ### Write Tool Gating
@@ -46,7 +127,75 @@ gno mcp --enable-write
46
127
  GNO_MCP_ENABLE_WRITE=1 gno mcp
47
128
  ```
48
129
 
49
- When disabled, write tools are not registered and cannot be invoked.
130
+ When disabled, write tools are not registered and cannot be invoked. The
131
+ write gate is independent of the tool profile below: a profile only ever
132
+ narrows the set the gate exposes.
133
+
134
+ ### Tool Profiles
135
+
136
+ A profile decides which tools the server advertises. Both transports honor it;
137
+ the resident gateway applies one profile to every connected client.
138
+
139
+ | Profile | Without `--enable-write` | With `--enable-write` |
140
+ | ---------------- | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------- |
141
+ | `full` (default) | Every read tool below (34) | Every read tool plus every write tool (53) |
142
+ | `core` | `gno_query`, `gno_search`, `gno_get`, `gno_multi_get`, `gno_context`, `gno_changes`, `gno_recall` (7) | The 7 read tools plus `gno_capture`, `gno_remember` (9) |
143
+
144
+ Both lists are exact: `core` advertises nothing else, and `full` is byte-for-byte
145
+ today's registry (names, order, descriptions, annotations, schemas). Both
146
+ profiles negotiate the same protocol revisions (2025-11-25 and 2026-07-28; see
147
+ [Protocol Revisions](#protocol-revisions)) on both transports.
148
+
149
+ Descriptions differ by profile. `full` serves the original strings verbatim
150
+ (`MCP_TOOL_DESCRIPTIONS` in `src/mcp/tools/index.ts`, pinned by the legacy
151
+ golden). `core` serves a micro-instruction per tool from a separate table
152
+ (`MCP_CORE_TOOL_DESCRIPTIONS` in `src/mcp/tool-descriptions-core.ts`): each
153
+ description opens with when to call the tool, names the mechanism it runs, and
154
+ ends with what comes back and any bound the caller must respect (line anchors
155
+ for `gno_get`, `maxBytes` for `gno_multi_get`, the 8-fact / 512-token recall
156
+ budget, the separate embedding step after `gno_capture`). Descriptions are the
157
+ zero-install discovery surface, so they follow the copy rules the skill and
158
+ site use: mechanism first, honest bounds, active voice, no promotional
159
+ vocabulary, no negated framings. The variants table names exactly the nine core
160
+ tools; the set of tools whose description differs between the profiles is the
161
+ core set and nothing else (`test/mcp/tool-descriptions-core.test.ts`). Input
162
+ schemas and annotations are identical across profiles.
163
+
164
+ Core read membership was decided against the Agent Retrieval Playbook: the
165
+ default hybrid path (`gno_query`), the exact-term path (`gno_search`), the two
166
+ read primitives every result hands off to (`gno_get`, `gno_multi_get`), the
167
+ bounded evidence handoff (`gno_context`), the metadata-only change feed
168
+ (`gno_changes`), and memory recall (`gno_recall`). Diagnostics, vector-only
169
+ search, graph, sections, traces, egress, status, and job tools stay in `full`.
170
+
171
+ Core write membership is an exact allowlist: `gno_capture` (new documents) and
172
+ `gno_remember` (durable facts). `gno_job_status` is deliberately absent from
173
+ both core sets because neither exposed write is asynchronous: `gno_capture`
174
+ writes the file and returns, and `gno_remember` returns once the fact is
175
+ lexically searchable; neither starts a `JobManager` job, so the core profile
176
+ never hands out a job ID to poll. If a future core write becomes async,
177
+ `gno_job_status` joins the write allowlist in the same change.
178
+
179
+ Selection and precedence (highest first):
180
+
181
+ 1. CLI flag: `gno mcp --tool-profile core|full` (stdio); `gno serve --mcp-tool-profile core|full` or `gno daemon --mcp-tool-profile core|full` (resident gateway). An unknown value fails with `VALIDATION` before any listener starts.
182
+ 2. Config: `gateway.toolProfile: core|full` under the root `gateway` key (resident gateway only; stdio has no config key).
183
+ 3. Default: `full`.
184
+
185
+ The profile is read once when the listener starts (stdio process start, or
186
+ `serve`/`daemon` gateway start); it is not hot-reloaded from config, so
187
+ changing it requires restarting the server. `--detach` re-executes the same
188
+ argv, so the flag carries over to the detached child. Calling a tool outside
189
+ the active profile answers JSON-RPC `-32602` exactly like an unregistered
190
+ tool.
191
+
192
+ Default-profile decision (deferred): the default stays `full`. `core` ships as
193
+ opt-in so existing clients, installers, and skills see an unchanged surface,
194
+ and the flip to `core` is a separate follow-up that needs dogfood evidence
195
+ (agents running on `core` across real sessions, with the playbook's routing
196
+ holding and no tool outside the core set requested in ordinary retrieval). That
197
+ follow-up carries its own release note and a `gno mcp install` profile flag;
198
+ nothing in this change pre-empts it.
50
199
 
51
200
  ### Collection Root Validation
52
201
 
@@ -72,9 +221,10 @@ lease that stays busy past the wait window returns `MEMORY_WRITE_LEASE_BUSY`.
72
221
 
73
222
  `gno serve` and `gno daemon` mount the same stateful MCP surface at `/mcp`.
74
223
  The default listener is the literal IPv4 loopback address `127.0.0.1`. Each
75
- HTTP session owns one SDK server and transport while sharing the resident
76
- store, jobs, and model lifecycle. POST, GET, and DELETE follow MCP 2025-11-25;
77
- resumption is not advertised.
224
+ 2025-era HTTP session owns one SDK server and transport while sharing the
225
+ resident store, jobs, and model lifecycle; POST, GET, and DELETE follow MCP
226
+ 2025-11-25 and resumption is not advertised. 2026-07-28 requests are served
227
+ sessionless from the same factory (see [Protocol Revisions](#protocol-revisions)).
78
228
 
79
229
  The external boundary runs before JSON parsing or SDK dispatch on every HTTP
80
230
  method. It uses Bun `server.requestIP(request)` as the peer source and never
@@ -178,6 +178,7 @@ export async function daemon(
178
178
  allowedHosts: options.allowedHosts,
179
179
  allowedOrigins: options.allowedOrigins,
180
180
  enableWrite: options.enableWrite,
181
+ toolProfile: options.toolProfile,
181
182
  });
182
183
  let gateway: Awaited<ReturnType<typeof createMcpHttpGateway>> | undefined;
183
184
  let server: ReturnType<typeof Bun.serve> | undefined;
@@ -4,6 +4,7 @@
4
4
  * @module src/cli/commands/mcp
5
5
  */
6
6
 
7
+ import type { McpToolProfile } from "../../mcp/tool-profile";
7
8
  import type { GlobalOptions } from "../context";
8
9
 
9
10
  /**
@@ -12,7 +13,7 @@ import type { GlobalOptions } from "../context";
12
13
  */
13
14
  export async function mcpCommand(
14
15
  options: GlobalOptions,
15
- commandOptions: { enableWrite?: boolean } = {}
16
+ commandOptions: { enableWrite?: boolean; toolProfile?: McpToolProfile } = {}
16
17
  ): Promise<void> {
17
18
  const { startMcpServer } = await import("../../mcp/server.js");
18
19
  await startMcpServer({
@@ -20,5 +21,6 @@ export async function mcpCommand(
20
21
  configPath: options.config,
21
22
  verbose: options.verbose,
22
23
  enableWrite: commandOptions.enableWrite,
24
+ toolProfile: commandOptions.toolProfile,
23
25
  });
24
26
  }
@@ -29,6 +29,7 @@ import {
29
29
  withCliWriteLease,
30
30
  type WriteLeaseBusyFailure,
31
31
  } from "../core/write-lease";
32
+ import { type McpToolProfile, parseMcpToolProfile } from "../mcp/tool-profile";
32
33
  import { setColorsEnabled } from "./colors";
33
34
  import {
34
35
  applyGlobalOptions,
@@ -2188,12 +2189,17 @@ function wireMcpCommand(program: Command): void {
2188
2189
  "--enable-write",
2189
2190
  "Enable write operations (capture, add-collection, sync, remove-collection)"
2190
2191
  )
2192
+ .option(
2193
+ "--tool-profile <profile>",
2194
+ "advertised tool set: core (7 read tools + capture/remember with --enable-write) or full (default)"
2195
+ )
2191
2196
  .action(async (cmdOpts: Record<string, unknown>) => {
2192
2197
  const { mcpCommand } = await import("./commands/mcp.js");
2193
2198
  const globalOpts = program.opts();
2194
2199
  const globals = parseGlobalOptions(globalOpts);
2195
2200
  await mcpCommand(globals, {
2196
2201
  enableWrite: cmdOpts.enableWrite === true ? true : undefined,
2202
+ toolProfile: parseToolProfileOption(cmdOpts.toolProfile),
2197
2203
  });
2198
2204
  });
2199
2205
 
@@ -4206,7 +4212,23 @@ function addGatewayOptions(
4206
4212
  .option(
4207
4213
  "--mcp-enable-write",
4208
4214
  "authorize HTTP MCP mutation tools (independent of authentication)"
4215
+ )
4216
+ .option(
4217
+ "--mcp-tool-profile <profile>",
4218
+ "advertised MCP tool set: core or full (overrides gateway.toolProfile; default full)"
4219
+ );
4220
+ }
4221
+
4222
+ /** Validate a `--tool-profile` / `--mcp-tool-profile` value; undefined when absent. */
4223
+ function parseToolProfileOption(value: unknown): McpToolProfile | undefined {
4224
+ try {
4225
+ return parseMcpToolProfile(value);
4226
+ } catch (error) {
4227
+ throw new CliError(
4228
+ "VALIDATION",
4229
+ error instanceof Error ? error.message : String(error)
4209
4230
  );
4231
+ }
4210
4232
  }
4211
4233
 
4212
4234
  function wireDaemonCommand(program: Command): void {
@@ -4287,6 +4309,8 @@ async function handleDaemonAction(
4287
4309
  cmd: Command
4288
4310
  ): Promise<void> {
4289
4311
  const globals = getGlobals();
4312
+ // Validate before any side effect (pid files, detach, runtime boot).
4313
+ const toolProfile = parseToolProfileOption(cmdOpts.mcpToolProfile);
4290
4314
 
4291
4315
  const {
4292
4316
  resolveProcessPaths,
@@ -4378,6 +4402,7 @@ async function handleDaemonAction(
4378
4402
  allowedHosts: cmdOpts.mcpAllowedHost as string[] | undefined,
4379
4403
  allowedOrigins: cmdOpts.mcpAllowedOrigin as string[] | undefined,
4380
4404
  enableWrite: cmdOpts.mcpEnableWrite === true ? true : undefined,
4405
+ toolProfile,
4381
4406
  });
4382
4407
  if (!result.success) {
4383
4408
  throw new CliError("RUNTIME", result.error);
@@ -4623,6 +4648,8 @@ async function handleServeAction(
4623
4648
  cmd: Command
4624
4649
  ): Promise<void> {
4625
4650
  const globals = getGlobals();
4651
+ // Validate before any side effect (NODE_ENV, pid files, detach, runtime boot).
4652
+ const toolProfile = parseToolProfileOption(cmdOpts.mcpToolProfile);
4626
4653
  // NB: do NOT parse --port here. The status/stop branches don't need it
4627
4654
  // and rejecting `gno serve --status --port nope` on irrelevant input
4628
4655
  // would break management commands. parsePositiveInt is called inside
@@ -4723,6 +4750,7 @@ async function handleServeAction(
4723
4750
  allowedHosts: cmdOpts.mcpAllowedHost as string[] | undefined,
4724
4751
  allowedOrigins: cmdOpts.mcpAllowedOrigin as string[] | undefined,
4725
4752
  enableWrite: cmdOpts.mcpEnableWrite === true ? true : undefined,
4753
+ toolProfile,
4726
4754
  });
4727
4755
 
4728
4756
  if (!result.success) {
@@ -11,6 +11,7 @@ import { z } from "zod";
11
11
 
12
12
  import { URI_PREFIX } from "../app/constants";
13
13
  import { JsonlFieldMappingSchema } from "../converters/adapters/jsonl/config";
14
+ import { MCP_TOOL_PROFILES } from "../mcp/tool-profile";
14
15
  import { RetrievalTraceConfigSchema } from "./retrieval-traces";
15
16
 
16
17
  // ─────────────────────────────────────────────────────────────────────────────
@@ -464,6 +465,8 @@ export const HttpGatewayConfigSchema = z.object({
464
465
  allowedOrigins: z.array(z.string().min(1)).optional(),
465
466
  /** Separate mutation authorization; authentication alone never enables it. */
466
467
  enableWrite: z.boolean().optional(),
468
+ /** Advertised MCP tool set (`core` | `full`); read at listener start. */
469
+ toolProfile: z.enum(MCP_TOOL_PROFILES).optional(),
467
470
  limits: HttpGatewayLimitsSchema.optional(),
468
471
  });
469
472
 
@@ -1,10 +1,10 @@
1
1
  /** Read-only, privacy-bounded connector activation verification. */
2
2
 
3
- import { Client } from "@modelcontextprotocol/sdk/client/index.js";
3
+ import { Client } from "@modelcontextprotocol/client";
4
4
  import {
5
5
  getDefaultEnvironment,
6
6
  StdioClientTransport,
7
- } from "@modelcontextprotocol/sdk/client/stdio.js";
7
+ } from "@modelcontextprotocol/client/stdio";
8
8
 
9
9
  import type {
10
10
  ActivationStageReceipt,
@@ -252,7 +252,6 @@ async function executeMcpProof(input: McpProofInput): Promise<McpProofResult> {
252
252
  phase = "status";
253
253
  const status = await client.callTool(
254
254
  { name: "gno_status", arguments: {} },
255
- undefined,
256
255
  requestOptions
257
256
  );
258
257
  if (
@@ -272,7 +271,6 @@ async function executeMcpProof(input: McpProofInput): Promise<McpProofResult> {
272
271
  limit: 8,
273
272
  },
274
273
  },
275
- undefined,
276
274
  requestOptions
277
275
  );
278
276
  const searchFailed =
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);