@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.
- package/assets/skill/SKILL.md +5 -1
- package/assets/skill/mcp-reference.md +7 -0
- package/browser-extension/artifacts/{gno-browser-clipper-v1.41.0.zip → gno-browser-clipper-v1.43.0.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.43.0.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +5 -2
- package/spec/mcp.md +156 -6
- package/src/cli/commands/daemon.ts +1 -0
- package/src/cli/commands/mcp.ts +3 -1
- package/src/cli/program.ts +28 -0
- package/src/config/types.ts +3 -0
- package/src/core/connector-verifier.ts +2 -4
- 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-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 +251 -134
- package/src/mcp/tools/memory-shared.ts +10 -4
- package/src/serve/routes/mcp.ts +1 -0
- package/src/serve/server.ts +1 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.41.0.zip.sha256 +0 -1
package/assets/skill/SKILL.md
CHANGED
|
@@ -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
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
c0ebec1aed09df105e0b8bacec38818b39b4173c25a0bdcaa6bd3a4774a2e548 gno-browser-clipper-v1.43.0.zip
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gmickel/gno",
|
|
3
|
-
"version": "1.
|
|
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/
|
|
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-
|
|
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
|
|
76
|
-
store, jobs, and model lifecycle
|
|
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;
|
package/src/cli/commands/mcp.ts
CHANGED
|
@@ -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
|
}
|
package/src/cli/program.ts
CHANGED
|
@@ -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) {
|
package/src/config/types.ts
CHANGED
|
@@ -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/
|
|
3
|
+
import { Client } from "@modelcontextprotocol/client";
|
|
4
4
|
import {
|
|
5
5
|
getDefaultEnvironment,
|
|
6
6
|
StdioClientTransport,
|
|
7
|
-
} from "@modelcontextprotocol/
|
|
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
|
|
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);
|