@gmickel/gno 1.17.0 → 1.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (167) hide show
  1. package/README.md +18 -4
  2. package/assets/skill/SKILL.md +44 -1
  3. package/assets/skill/mcp-reference.md +21 -0
  4. package/package.json +3 -2
  5. package/spec/AGENTS.md +83 -0
  6. package/spec/CLAUDE.md +83 -0
  7. package/spec/bench-fixture.schema.json +137 -0
  8. package/spec/cli.md +2894 -0
  9. package/spec/db/schema.sql +442 -0
  10. package/spec/evals-agentic.md +510 -0
  11. package/spec/evals.md +1106 -0
  12. package/spec/mcp.md +2229 -0
  13. package/spec/output-schemas/activation-verification.schema.json +515 -0
  14. package/spec/output-schemas/ask.schema.json +366 -0
  15. package/spec/output-schemas/backlinks.schema.json +131 -0
  16. package/spec/output-schemas/bench-result.schema.json +120 -0
  17. package/spec/output-schemas/capture-receipt.schema.json +143 -0
  18. package/spec/output-schemas/collection-list.schema.json +45 -0
  19. package/spec/output-schemas/context-capsule-v1.schema.json +691 -0
  20. package/spec/output-schemas/context-capsule-verification.schema.json +1338 -0
  21. package/spec/output-schemas/context-list.schema.json +21 -0
  22. package/spec/output-schemas/doctor.schema.json +313 -0
  23. package/spec/output-schemas/error.schema.json +30 -0
  24. package/spec/output-schemas/expansion.schema.json +37 -0
  25. package/spec/output-schemas/get.schema.json +140 -0
  26. package/spec/output-schemas/graph-query.schema.json +99 -0
  27. package/spec/output-schemas/graph.schema.json +371 -0
  28. package/spec/output-schemas/links-list.schema.json +186 -0
  29. package/spec/output-schemas/mcp-add-collection-result.schema.json +23 -0
  30. package/spec/output-schemas/mcp-capture-result.schema.json +152 -0
  31. package/spec/output-schemas/mcp-http-error.schema.json +30 -0
  32. package/spec/output-schemas/mcp-job-list.schema.json +58 -0
  33. package/spec/output-schemas/mcp-job-status.schema.json +224 -0
  34. package/spec/output-schemas/mcp-remove-result.schema.json +39 -0
  35. package/spec/output-schemas/mcp-sync-result.schema.json +41 -0
  36. package/spec/output-schemas/mcp-tag-result.schema.json +33 -0
  37. package/spec/output-schemas/models-list.schema.json +93 -0
  38. package/spec/output-schemas/multi-get.schema.json +103 -0
  39. package/spec/output-schemas/process-status.schema.json +119 -0
  40. package/spec/output-schemas/query-diagnose.schema.json +123 -0
  41. package/spec/output-schemas/resident-status.schema.json +154 -0
  42. package/spec/output-schemas/retrieval-trace-common.schema.json +492 -0
  43. package/spec/output-schemas/retrieval-trace-delete.schema.json +16 -0
  44. package/spec/output-schemas/retrieval-trace-export.schema.json +61 -0
  45. package/spec/output-schemas/retrieval-trace-filters.schema.json +139 -0
  46. package/spec/output-schemas/retrieval-trace-judgment.schema.json +15 -0
  47. package/spec/output-schemas/retrieval-trace-list.schema.json +18 -0
  48. package/spec/output-schemas/retrieval-trace-payloads.schema.json +178 -0
  49. package/spec/output-schemas/retrieval-trace-purge.schema.json +31 -0
  50. package/spec/output-schemas/retrieval-trace-qrels.schema.json +303 -0
  51. package/spec/output-schemas/retrieval-trace-replay.schema.json +286 -0
  52. package/spec/output-schemas/retrieval-trace-show.schema.json +69 -0
  53. package/spec/output-schemas/retrieval-trace-summary.schema.json +65 -0
  54. package/spec/output-schemas/search-result.schema.json +154 -0
  55. package/spec/output-schemas/search-results.schema.json +338 -0
  56. package/spec/output-schemas/similar.schema.json +84 -0
  57. package/spec/output-schemas/status.schema.json +676 -0
  58. package/spec/output-schemas/tags-list.schema.json +48 -0
  59. package/src/app/context-runtime-types.ts +3 -0
  60. package/src/app/context-runtime.ts +15 -1
  61. package/src/cli/commands/ask.ts +106 -36
  62. package/src/cli/commands/context-build.ts +56 -9
  63. package/src/cli/commands/daemon.ts +69 -2
  64. package/src/cli/commands/get.ts +64 -3
  65. package/src/cli/commands/models/pull.ts +13 -3
  66. package/src/cli/commands/query.ts +62 -23
  67. package/src/cli/commands/replay.ts +140 -0
  68. package/src/cli/commands/search.ts +48 -3
  69. package/src/cli/commands/shared.ts +3 -1
  70. package/src/cli/commands/status.ts +2 -0
  71. package/src/cli/commands/trace.ts +200 -0
  72. package/src/cli/commands/vsearch.ts +75 -53
  73. package/src/cli/detach.ts +37 -20
  74. package/src/cli/program.ts +329 -27
  75. package/src/config/index.ts +12 -0
  76. package/src/config/retrieval-traces.ts +56 -0
  77. package/src/config/types.ts +41 -0
  78. package/src/core/context-compiler.ts +11 -4
  79. package/src/core/job-manager.ts +19 -0
  80. package/src/core/mutation-generations.ts +33 -0
  81. package/src/core/retrieval-qrels.ts +405 -0
  82. package/src/core/retrieval-replay-candidate.ts +368 -0
  83. package/src/core/retrieval-replay-types.ts +109 -0
  84. package/src/core/retrieval-replay-validation.ts +89 -0
  85. package/src/core/retrieval-replay.ts +441 -0
  86. package/src/core/retrieval-trace-evidence-origin.ts +175 -0
  87. package/src/core/retrieval-trace-export.ts +113 -0
  88. package/src/core/retrieval-trace-filter-normalization.ts +27 -0
  89. package/src/core/retrieval-trace-filters.ts +19 -0
  90. package/src/core/retrieval-trace-management-helpers.ts +247 -0
  91. package/src/core/retrieval-trace-management-types.ts +132 -0
  92. package/src/core/retrieval-trace-management.ts +422 -0
  93. package/src/core/retrieval-trace-request.ts +141 -0
  94. package/src/core/retrieval-trace-session.ts +494 -0
  95. package/src/core/retrieval-trace.ts +472 -0
  96. package/src/llm/cache.ts +13 -3
  97. package/src/llm/nodeLlamaCpp/adapter.ts +10 -1
  98. package/src/llm/nodeLlamaCpp/lifecycle.ts +71 -0
  99. package/src/mcp/context.ts +161 -0
  100. package/src/mcp/http-security.ts +477 -0
  101. package/src/mcp/http-session.ts +272 -0
  102. package/src/mcp/http-transport.ts +370 -0
  103. package/src/mcp/resources/index.ts +141 -134
  104. package/src/mcp/server.ts +19 -79
  105. package/src/mcp/tools/add-collection.ts +3 -1
  106. package/src/mcp/tools/capture.ts +3 -0
  107. package/src/mcp/tools/clear-collection-embeddings.ts +2 -0
  108. package/src/mcp/tools/context.ts +68 -16
  109. package/src/mcp/tools/embed.ts +62 -52
  110. package/src/mcp/tools/get.ts +35 -1
  111. package/src/mcp/tools/index-cmd.ts +88 -74
  112. package/src/mcp/tools/index.ts +96 -2
  113. package/src/mcp/tools/query.ts +95 -64
  114. package/src/mcp/tools/remove-collection.ts +2 -0
  115. package/src/mcp/tools/search.ts +36 -13
  116. package/src/mcp/tools/status.ts +11 -0
  117. package/src/mcp/tools/sync.ts +16 -14
  118. package/src/mcp/tools/trace.ts +143 -0
  119. package/src/mcp/tools/vsearch.ts +71 -38
  120. package/src/mcp/tools/workspace-write.ts +7 -3
  121. package/src/pipeline/answer.ts +167 -26
  122. package/src/pipeline/graph-retrieval.ts +15 -1
  123. package/src/pipeline/hybrid.ts +151 -43
  124. package/src/pipeline/search.ts +36 -3
  125. package/src/pipeline/trace-metadata.ts +47 -0
  126. package/src/pipeline/types.ts +43 -0
  127. package/src/pipeline/vsearch.ts +101 -38
  128. package/src/sdk/client.ts +380 -71
  129. package/src/sdk/documents.ts +48 -1
  130. package/src/sdk/index.ts +17 -0
  131. package/src/sdk/types.ts +28 -0
  132. package/src/serve/background-runtime.ts +12 -212
  133. package/src/serve/context-capsule.ts +67 -8
  134. package/src/serve/embed-scheduler.ts +74 -43
  135. package/src/serve/index.ts +9 -0
  136. package/src/serve/jobs.ts +78 -80
  137. package/src/serve/public/app.tsx +12 -1
  138. package/src/serve/public/components/HealthCenter.tsx +74 -1
  139. package/src/serve/public/globals.built.css +1 -1
  140. package/src/serve/public/lib/workspace-tabs.ts +2 -0
  141. package/src/serve/public/pages/Dashboard.tsx +11 -0
  142. package/src/serve/public/pages/TraceHistory.tsx +478 -0
  143. package/src/serve/public/pages/trace-history-detail.tsx +224 -0
  144. package/src/serve/resident-admission.ts +159 -0
  145. package/src/serve/resident-background-work.ts +39 -0
  146. package/src/serve/resident-request.ts +55 -0
  147. package/src/serve/resident-runtime.ts +490 -0
  148. package/src/serve/resident-status.ts +96 -0
  149. package/src/serve/retrieval-trace.ts +28 -0
  150. package/src/serve/routes/api.ts +629 -239
  151. package/src/serve/routes/mcp.ts +69 -0
  152. package/src/serve/routes/traces.ts +156 -0
  153. package/src/serve/server.ts +276 -37
  154. package/src/serve/status-model.ts +51 -0
  155. package/src/serve/status.ts +5 -0
  156. package/src/store/index.ts +31 -0
  157. package/src/store/migrations/014-retrieval-traces.ts +303 -0
  158. package/src/store/migrations/index.ts +2 -0
  159. package/src/store/retrieval-trace-codec.ts +384 -0
  160. package/src/store/sqlite/adapter.ts +179 -10
  161. package/src/store/sqlite/retrieval-trace-management-store.ts +341 -0
  162. package/src/store/sqlite/retrieval-trace-retention.ts +349 -0
  163. package/src/store/sqlite/retrieval-trace-rows.ts +267 -0
  164. package/src/store/sqlite/retrieval-trace-store.ts +515 -0
  165. package/src/store/types.ts +297 -0
  166. package/src/store/vector/sqlite-vec.ts +76 -1
  167. package/src/store/vector/types.ts +1 -1
@@ -92,6 +92,9 @@ function formatResourceContent(
92
92
  * Register gno:// resources with the MCP server.
93
93
  */
94
94
  export function registerResources(server: McpServer, ctx: ToolContext): void {
95
+ const withSnapshot = <T>(operation: () => Promise<T>): Promise<T> =>
96
+ ctx.runWithSnapshot?.(operation) ?? operation();
97
+
95
98
  // Resource template for gno://{collection}/{path} URIs
96
99
  const template = new ResourceTemplate(`${URI_PREFIX}{collection}/{+path}`, {
97
100
  list: async () => {
@@ -113,101 +116,103 @@ export function registerResources(server: McpServer, ctx: ToolContext): void {
113
116
  });
114
117
 
115
118
  // Register the template-based resource handler
116
- server.resource("gno-document", template, {}, async (uri, _variables) => {
117
- // Check shutdown before acquiring mutex
118
- if (ctx.isShuttingDown()) {
119
- throw new Error("Server is shutting down");
120
- }
121
-
122
- // Serialize resource reads same as tools (prevent concurrent DB access + shutdown race)
123
- const release = await ctx.toolMutex.acquire();
124
- try {
125
- // Use parseUri for proper URL decoding (handles %20, etc.)
126
- const parsed = parseUri(uri.href);
127
- if (!parsed) {
128
- throw new Error(`Invalid gno:// URI: ${uri.href}`);
129
- }
130
-
131
- const resolution = resolveEffectiveIndex([uri.href], ctx.indexName);
132
- if (!resolution.ok) {
133
- throw new Error(resolution.error);
119
+ server.resource("gno-document", template, {}, (uri, _variables) =>
120
+ withSnapshot(async () => {
121
+ // Check shutdown before acquiring mutex
122
+ if (ctx.isShuttingDown()) {
123
+ throw new Error("Server is shutting down");
134
124
  }
135
- const scoped = await openScopedIndexStore({
136
- activeStore: ctx.store,
137
- activeIndexName: ctx.indexName,
138
- requestedIndexName: resolution.value.indexName,
139
- config: ctx.config,
140
- configPath: ctx.actualConfigPath,
141
- });
142
125
 
126
+ // Serialize resource reads same as tools (prevent concurrent DB access + shutdown race)
127
+ const release = await ctx.toolMutex.acquire();
143
128
  try {
144
- const { collection, path } = parsed;
145
-
146
- // Validate collection exists
147
- const collectionExists = ctx.collections.some(
148
- (c) => c.name === collection
149
- );
150
- if (!collectionExists) {
151
- throw new Error(`Collection not found: ${collection}`);
129
+ // Use parseUri for proper URL decoding (handles %20, etc.)
130
+ const parsed = parseUri(uri.href);
131
+ if (!parsed) {
132
+ throw new Error(`Invalid gno:// URI: ${uri.href}`);
152
133
  }
153
134
 
154
- // Look up document (path is properly decoded by parseUri)
155
- const docResult = await scoped.store.getDocument(collection, path);
156
- if (!docResult.ok) {
157
- throw new Error(
158
- `Failed to lookup document: ${docResult.error.message}`
159
- );
135
+ const resolution = resolveEffectiveIndex([uri.href], ctx.indexName);
136
+ if (!resolution.ok) {
137
+ throw new Error(resolution.error);
160
138
  }
139
+ const scoped = await openScopedIndexStore({
140
+ activeStore: ctx.store,
141
+ activeIndexName: ctx.indexName,
142
+ requestedIndexName: resolution.value.indexName,
143
+ config: ctx.config,
144
+ configPath: ctx.actualConfigPath,
145
+ });
146
+
147
+ try {
148
+ const { collection, path } = parsed;
149
+
150
+ // Validate collection exists
151
+ const collectionExists = ctx.collections.some(
152
+ (c) => c.name === collection
153
+ );
154
+ if (!collectionExists) {
155
+ throw new Error(`Collection not found: ${collection}`);
156
+ }
161
157
 
162
- const doc = docResult.value;
163
- if (!doc) {
164
- throw new Error(`Document not found: ${uri.href}`);
165
- }
158
+ // Look up document (path is properly decoded by parseUri)
159
+ const docResult = await scoped.store.getDocument(collection, path);
160
+ if (!docResult.ok) {
161
+ throw new Error(
162
+ `Failed to lookup document: ${docResult.error.message}`
163
+ );
164
+ }
166
165
 
167
- // Get content
168
- if (!doc.mirrorHash) {
169
- throw new Error(`Document has no indexed content: ${uri.href}`);
170
- }
166
+ const doc = docResult.value;
167
+ if (!doc) {
168
+ throw new Error(`Document not found: ${uri.href}`);
169
+ }
170
+
171
+ // Get content
172
+ if (!doc.mirrorHash) {
173
+ throw new Error(`Document has no indexed content: ${uri.href}`);
174
+ }
175
+
176
+ const contentResult = await scoped.store.getContent(doc.mirrorHash);
177
+ if (!contentResult.ok) {
178
+ throw new Error(
179
+ `Failed to read content: ${contentResult.error.message}`
180
+ );
181
+ }
182
+
183
+ const content = contentResult.value ?? "";
171
184
 
172
- const contentResult = await scoped.store.getContent(doc.mirrorHash);
173
- if (!contentResult.ok) {
174
- throw new Error(
175
- `Failed to read content: ${contentResult.error.message}`
185
+ // Format with header and line numbers
186
+ const formattedContent = formatResourceContent(
187
+ doc,
188
+ content,
189
+ ctx,
190
+ scoped.indexName
191
+ );
192
+
193
+ // Build canonical URI
194
+ const canonicalUri = decorateUriForIndex(
195
+ buildUri(collection, path),
196
+ scoped.indexName
176
197
  );
177
- }
178
198
 
179
- const content = contentResult.value ?? "";
180
-
181
- // Format with header and line numbers
182
- const formattedContent = formatResourceContent(
183
- doc,
184
- content,
185
- ctx,
186
- scoped.indexName
187
- );
188
-
189
- // Build canonical URI
190
- const canonicalUri = decorateUriForIndex(
191
- buildUri(collection, path),
192
- scoped.indexName
193
- );
194
-
195
- return {
196
- contents: [
197
- {
198
- uri: canonicalUri,
199
- mimeType: "text/markdown",
200
- text: formattedContent,
201
- },
202
- ],
203
- };
199
+ return {
200
+ contents: [
201
+ {
202
+ uri: canonicalUri,
203
+ mimeType: "text/markdown",
204
+ text: formattedContent,
205
+ },
206
+ ],
207
+ };
208
+ } finally {
209
+ await scoped.close();
210
+ }
204
211
  } finally {
205
- await scoped.close();
212
+ release();
206
213
  }
207
- } finally {
208
- release();
209
- }
210
- });
214
+ })
215
+ );
211
216
 
212
217
  // Register gno://tags resource for listing tags
213
218
  // Use ResourceTemplate with RFC6570 query expansion for proper routing
@@ -231,65 +236,67 @@ export function registerResources(server: McpServer, ctx: ToolContext): void {
231
236
  "gno-tags",
232
237
  tagsTemplate,
233
238
  { mimeType: "application/json" },
234
- async (uri) => {
235
- // Check shutdown before acquiring mutex
236
- if (ctx.isShuttingDown()) {
237
- throw new Error("Server is shutting down");
238
- }
239
-
240
- const release = await ctx.toolMutex.acquire();
241
- try {
242
- // Parse query params from URI
243
- const url = new URL(uri.href);
244
- const collectionParam = url.searchParams.get("collection") || undefined;
245
- const prefixParam = url.searchParams.get("prefix") || undefined;
246
-
247
- // Normalize and validate collection (case-insensitive)
248
- let collection: string | undefined;
249
- if (collectionParam) {
250
- collection = normalizeCollectionName(collectionParam);
251
- const exists = ctx.collections.some(
252
- (c) => c.name.toLowerCase() === collection
253
- );
254
- if (!exists) {
255
- throw new Error(
256
- `${MCP_ERRORS.NOT_FOUND.code}: Collection not found: ${collectionParam}`
257
- );
258
- }
239
+ (uri) =>
240
+ withSnapshot(async () => {
241
+ // Check shutdown before acquiring mutex
242
+ if (ctx.isShuttingDown()) {
243
+ throw new Error("Server is shutting down");
259
244
  }
260
245
 
261
- // Normalize and validate prefix
262
- let prefix: string | undefined;
263
- if (prefixParam) {
264
- const trimmed = prefixParam.trim().replace(/\/+$/, "");
265
- if (trimmed.length > 0) {
266
- prefix = normalizeTag(trimmed);
267
- if (!validateTag(prefix)) {
246
+ const release = await ctx.toolMutex.acquire();
247
+ try {
248
+ // Parse query params from URI
249
+ const url = new URL(uri.href);
250
+ const collectionParam =
251
+ url.searchParams.get("collection") || undefined;
252
+ const prefixParam = url.searchParams.get("prefix") || undefined;
253
+
254
+ // Normalize and validate collection (case-insensitive)
255
+ let collection: string | undefined;
256
+ if (collectionParam) {
257
+ collection = normalizeCollectionName(collectionParam);
258
+ const exists = ctx.collections.some(
259
+ (c) => c.name.toLowerCase() === collection
260
+ );
261
+ if (!exists) {
268
262
  throw new Error(
269
- `${MCP_ERRORS.INVALID_INPUT.code}: Invalid tag prefix "${prefixParam}"`
263
+ `${MCP_ERRORS.NOT_FOUND.code}: Collection not found: ${collectionParam}`
270
264
  );
271
265
  }
272
266
  }
273
- }
274
267
 
275
- // Get tag counts
276
- const result = await ctx.store.getTagCounts({ collection, prefix });
277
- if (!result.ok) {
278
- throw new Error(`Failed to get tags: ${result.error.message}`);
279
- }
268
+ // Normalize and validate prefix
269
+ let prefix: string | undefined;
270
+ if (prefixParam) {
271
+ const trimmed = prefixParam.trim().replace(/\/+$/, "");
272
+ if (trimmed.length > 0) {
273
+ prefix = normalizeTag(trimmed);
274
+ if (!validateTag(prefix)) {
275
+ throw new Error(
276
+ `${MCP_ERRORS.INVALID_INPUT.code}: Invalid tag prefix "${prefixParam}"`
277
+ );
278
+ }
279
+ }
280
+ }
280
281
 
281
- return {
282
- contents: [
283
- {
284
- uri: uri.href,
285
- mimeType: "application/json",
286
- text: formatTagsContent(result.value, collection, prefix),
287
- },
288
- ],
289
- };
290
- } finally {
291
- release();
292
- }
293
- }
282
+ // Get tag counts
283
+ const result = await ctx.store.getTagCounts({ collection, prefix });
284
+ if (!result.ok) {
285
+ throw new Error(`Failed to get tags: ${result.error.message}`);
286
+ }
287
+
288
+ return {
289
+ contents: [
290
+ {
291
+ uri: uri.href,
292
+ mimeType: "application/json",
293
+ text: formatTagsContent(result.value, collection, prefix),
294
+ },
295
+ ],
296
+ };
297
+ } finally {
298
+ release();
299
+ }
300
+ })
294
301
  );
295
302
  }
package/src/mcp/server.ts CHANGED
@@ -5,14 +5,10 @@
5
5
  * @module src/mcp/server
6
6
  */
7
7
 
8
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
9
8
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
10
9
  // node:path for join/dirname (no Bun path utils)
11
10
  import { dirname, join } from "node:path";
12
11
 
13
- import type { Collection, Config } from "../config/types";
14
- import type { SqliteAdapter } from "../store/sqlite/adapter";
15
-
16
12
  import {
17
13
  DEFAULT_INDEX_NAME,
18
14
  MCP_SERVER_NAME,
@@ -23,55 +19,14 @@ import { canonicalizeIndexName } from "../app/index-name";
23
19
  import { JobManager } from "../core/job-manager";
24
20
  import { envIsSet } from "../llm/policy";
25
21
  import { MCP_ACTIVATION_VERIFICATION_ENV } from "./activation-verification-mode";
22
+ import {
23
+ createMcpServerSurface,
24
+ createToolContext,
25
+ Mutex,
26
+ type ToolContext,
27
+ } from "./context";
26
28
 
27
- // ─────────────────────────────────────────────────────────────────────────────
28
- // Simple Promise Mutex (avoids async-mutex dependency)
29
- // ─────────────────────────────────────────────────────────────────────────────
30
-
31
- class Mutex {
32
- #locked = false;
33
- readonly #queue: Array<() => void> = [];
34
-
35
- acquire(): Promise<() => void> {
36
- return new Promise((resolve) => {
37
- const tryAcquire = () => {
38
- if (this.#locked) {
39
- this.#queue.push(tryAcquire);
40
- } else {
41
- this.#locked = true;
42
- resolve(() => this.#release());
43
- }
44
- };
45
- tryAcquire();
46
- });
47
- }
48
-
49
- #release(): void {
50
- this.#locked = false;
51
- const next = this.#queue.shift();
52
- if (next) {
53
- next();
54
- }
55
- }
56
- }
57
-
58
- // ─────────────────────────────────────────────────────────────────────────────
59
- // Tool Context
60
- // ─────────────────────────────────────────────────────────────────────────────
61
-
62
- export interface ToolContext {
63
- store: SqliteAdapter;
64
- config: Config;
65
- collections: Collection[];
66
- actualConfigPath: string;
67
- indexName: string;
68
- toolMutex: Mutex;
69
- jobManager: JobManager;
70
- serverInstanceId: string;
71
- writeLockPath: string;
72
- enableWrite: boolean;
73
- isShuttingDown: () => boolean;
74
- }
29
+ export type { ToolContext } from "./context";
75
30
 
76
31
  // ─────────────────────────────────────────────────────────────────────────────
77
32
  // Server Options
@@ -136,21 +91,7 @@ export async function startMcpServer(options: McpServerOptions): Promise<void> {
136
91
  process.exit(1);
137
92
  }
138
93
 
139
- const { store, config, collections, actualConfigPath } = init;
140
-
141
- // Create MCP server
142
- const server = new McpServer(
143
- {
144
- name: MCP_SERVER_NAME,
145
- version: VERSION,
146
- },
147
- {
148
- capabilities: {
149
- tools: { listChanged: false },
150
- resources: { subscribe: false, listChanged: false },
151
- },
152
- }
153
- );
94
+ const { store, config, actualConfigPath } = init;
154
95
 
155
96
  // Sequential execution mutex
156
97
  const toolMutex = new Mutex();
@@ -173,10 +114,13 @@ export async function startMcpServer(options: McpServerOptions): Promise<void> {
173
114
  let shuttingDown = false;
174
115
 
175
116
  // Tool context (passed to all handlers)
176
- const ctx: ToolContext = {
117
+ let currentConfig = config;
118
+ const ctx: ToolContext = createToolContext({
177
119
  store,
178
- config,
179
- collections,
120
+ getConfig: () => currentConfig,
121
+ setConfig: (nextConfig) => {
122
+ currentConfig = nextConfig;
123
+ },
180
124
  actualConfigPath,
181
125
  indexName: canonicalizeIndexName(options.indexName ?? DEFAULT_INDEX_NAME),
182
126
  toolMutex,
@@ -185,15 +129,11 @@ export async function startMcpServer(options: McpServerOptions): Promise<void> {
185
129
  writeLockPath,
186
130
  enableWrite,
187
131
  isShuttingDown: () => shuttingDown,
188
- };
189
-
190
- // Register tools (T10.2)
191
- const { registerTools } = await import("./tools/index.js");
192
- registerTools(server, ctx);
193
-
194
- // Register resources (T10.3)
195
- const { registerResources } = await import("./resources/index.js");
196
- registerResources(server, ctx);
132
+ });
133
+ const server = createMcpServerSurface(ctx, {
134
+ name: MCP_SERVER_NAME,
135
+ version: VERSION,
136
+ });
197
137
 
198
138
  if (options.verbose) {
199
139
  console.error(
@@ -14,6 +14,7 @@ import { applyConfigChange } from "../../core/config-mutation";
14
14
  import { MCP_ERRORS } from "../../core/errors";
15
15
  import { acquireWriteLock, type WriteLockHandle } from "../../core/file-lock";
16
16
  import { JobError } from "../../core/job-manager";
17
+ import { recordContentMutation } from "../../core/mutation-generations";
17
18
  import {
18
19
  normalizeCollectionName,
19
20
  validateCollectionRoot,
@@ -152,9 +153,10 @@ export function handleAddCollection(
152
153
  gitPull: args.gitPull ?? false,
153
154
  runUpdateCmd: false,
154
155
  },
155
- ctx.config
156
+ mutationResult.config
156
157
  )
157
158
  );
159
+ recordContentMutation(result, ctx.markContentMutation);
158
160
 
159
161
  return {
160
162
  collections: [result],
@@ -210,6 +210,9 @@ export function handleCapture(
210
210
  serverInstanceId: ctx.serverInstanceId,
211
211
  }) as McpCaptureResult;
212
212
  }
213
+ if (syncResult.status === "added" || syncResult.status === "updated") {
214
+ ctx.markContentMutation?.();
215
+ }
213
216
 
214
217
  let docid = syncResult.docid;
215
218
  let documentId: number | undefined;
@@ -8,6 +8,7 @@ import type { ToolContext } from "../server";
8
8
 
9
9
  import { MCP_ERRORS } from "../../core/errors";
10
10
  import { withWriteLock } from "../../core/file-lock";
11
+ import { recordIndexMutation } from "../../core/mutation-generations";
11
12
  import { resolveModelUri } from "../../llm/registry";
12
13
  import { runTool, type ToolResult } from "./index";
13
14
 
@@ -81,6 +82,7 @@ export function handleClearCollectionEmbeddings(
81
82
  if (!result.ok) {
82
83
  throw new Error(`${result.error.code}: ${result.error.message}`);
83
84
  }
85
+ recordIndexMutation(result.value.deletedVectors, ctx.markIndexMutation);
84
86
 
85
87
  return {
86
88
  ...result.value,
@@ -1,5 +1,7 @@
1
1
  /** MCP Context Capsule tools over the shared application runtime. */
2
2
 
3
+ import type { RetrievalTraceSession } from "../../core/retrieval-trace-session";
4
+ import type { ModelLease } from "../../llm/nodeLlamaCpp/lifecycle";
3
5
  import type { EmbeddingPort, RerankPort } from "../../llm/types";
4
6
  import type { VectorIndexPort } from "../../store/vector";
5
7
  import type { ToolContext } from "../server";
@@ -19,6 +21,10 @@ import {
19
21
  parseContextVerifySurfaceInput,
20
22
  } from "../../app/context-surface";
21
23
  import { createNonTtyProgressRenderer } from "../../cli/progress";
24
+ import {
25
+ finishRetrievalTraceAfterError,
26
+ startRetrievalTraceRequest,
27
+ } from "../../core/retrieval-trace-request";
22
28
  import { LlmAdapter } from "../../llm/nodeLlamaCpp/adapter";
23
29
  import { resolveDownloadPolicy } from "../../llm/policy";
24
30
  import { resolveModelUri } from "../../llm/registry";
@@ -27,11 +33,15 @@ import { createVectorIndexPort } from "../../store/vector";
27
33
  interface ContextToolResultData {
28
34
  structuredContent: Record<string, unknown>;
29
35
  text: string;
36
+ traceId?: string;
30
37
  }
31
38
 
32
39
  const asToolResult = (data: ContextToolResultData): ToolResult => ({
33
40
  content: [{ type: "text", text: data.text }],
34
41
  structuredContent: data.structuredContent,
42
+ ...(data.traceId
43
+ ? { _meta: { gno: { retrievalTrace: { traceId: data.traceId } } } }
44
+ : {}),
35
45
  });
36
46
 
37
47
  const asToolError = (error: unknown): ToolResult => {
@@ -64,7 +74,8 @@ const runContextTool = async (
64
74
  }
65
75
  const release = await context.toolMutex.acquire();
66
76
  try {
67
- return asToolResult(await operation());
77
+ const data = await (context.runWithSnapshot?.(operation) ?? operation());
78
+ return asToolResult(data);
68
79
  } catch (error) {
69
80
  return asToolError(error);
70
81
  } finally {
@@ -82,19 +93,17 @@ interface McpModelPorts {
82
93
  interface McpModelPortFactory {
83
94
  createEmbeddingPort: LlmAdapter["createEmbeddingPort"];
84
95
  createRerankPort: LlmAdapter["createRerankPort"];
85
- dispose: LlmAdapter["dispose"];
96
+ acquireModelLease?: LlmAdapter["acquireModelLease"];
86
97
  }
87
98
 
88
99
  export const disposeContextModelOwners = async (
89
100
  portOwners: readonly { dispose(): Promise<void> }[],
90
- managerOwner: { dispose(): Promise<void> }
101
+ lease?: ModelLease
91
102
  ): Promise<void> => {
92
103
  await Promise.allSettled(
93
104
  portOwners.map((owner) => Promise.resolve().then(() => owner.dispose()))
94
105
  );
95
- await Promise.allSettled([
96
- Promise.resolve().then(() => managerOwner.dispose()),
97
- ]);
106
+ lease?.release();
98
107
  };
99
108
 
100
109
  export const createMcpModelPorts = async (
@@ -104,6 +113,7 @@ export const createMcpModelPorts = async (
104
113
  ): Promise<McpModelPorts> => {
105
114
  const llm = new LlmAdapter(context.config);
106
115
  const factory = factoryOverride ?? llm;
116
+ const lease = factory.acquireModelLease?.();
107
117
  const policy = resolveDownloadPolicy(process.env, {});
108
118
  const progress = createNonTtyProgressRenderer();
109
119
  const embedUri = resolveModelUri(
@@ -151,7 +161,7 @@ export const createMcpModelPorts = async (
151
161
  [ownedEmbedPort, rerankPort].filter(
152
162
  (port): port is EmbeddingPort | RerankPort => port !== null
153
163
  ),
154
- factory
164
+ lease
155
165
  );
156
166
  },
157
167
  };
@@ -160,7 +170,7 @@ export const createMcpModelPorts = async (
160
170
  [ownedEmbedPort, rerankPort].filter(
161
171
  (port): port is EmbeddingPort | RerankPort => port !== null
162
172
  ),
163
- factory
173
+ lease
164
174
  );
165
175
  throw error;
166
176
  }
@@ -179,15 +189,50 @@ export const handleContext = (
179
189
  context.config.collections.map((collection) => collection.name)
180
190
  );
181
191
  const useModels = parsed.input.depthPolicy !== "fast";
182
- const modelPorts = useModels
183
- ? await createMcpModelPorts(
184
- context,
185
- parsed.input.collections?.length === 1
186
- ? parsed.input.collections[0]
187
- : undefined
188
- )
189
- : null;
192
+ const collection =
193
+ parsed.input.collections?.length === 1
194
+ ? parsed.input.collections[0]
195
+ : undefined;
196
+ const modelUris = useModels
197
+ ? [
198
+ resolveModelUri(context.config, "embed", undefined, collection),
199
+ resolveModelUri(context.config, "rerank", undefined, collection),
200
+ ]
201
+ : [];
202
+ let modelPorts: Awaited<ReturnType<typeof createMcpModelPorts>> | null =
203
+ null;
204
+ let traceSession: RetrievalTraceSession | undefined;
190
205
  try {
206
+ const traceStart = await startRetrievalTraceRequest({
207
+ store: context.store,
208
+ config: context.config,
209
+ query: parsed.input.query ?? parsed.input.goal,
210
+ goal: parsed.input.goal,
211
+ filters: {
212
+ limit: parsed.input.limit,
213
+ collection,
214
+ collections: [...(parsed.input.collections ?? [])].sort(),
215
+ lang: parsed.input.lang,
216
+ tagsAll: parsed.input.tagsAll,
217
+ tagsAny: parsed.input.tagsAny,
218
+ since: parsed.input.since,
219
+ until: parsed.input.until,
220
+ categories: parsed.input.categories,
221
+ author: parsed.input.author,
222
+ graph: parsed.input.graph,
223
+ candidateLimit: parsed.input.candidateLimit,
224
+ queryModes: parsed.input.queryModes,
225
+ uriPrefix: parsed.input.uriPrefix ?? undefined,
226
+ },
227
+ pipeline: "context",
228
+ indexName: context.indexName,
229
+ modelUris,
230
+ });
231
+ if (!traceStart.ok) throw new Error(traceStart.error.message);
232
+ traceSession = traceStart.value ?? undefined;
233
+ modelPorts = useModels
234
+ ? await createMcpModelPorts(context, collection)
235
+ : null;
191
236
  const capsule = await buildContextCapsule(parsed.input, {
192
237
  store: context.store,
193
238
  config: context.config,
@@ -195,14 +240,21 @@ export const handleContext = (
195
240
  vectorIndex: modelPorts?.vectorIndex ?? null,
196
241
  embedPort: modelPorts?.embedPort ?? null,
197
242
  rerankPort: modelPorts?.rerankPort ?? null,
243
+ traceSession,
198
244
  });
245
+ const finalized = await traceSession?.finish("completed");
246
+ if (finalized && !finalized.ok) throw new Error(finalized.error.message);
199
247
  return {
200
248
  structuredContent: capsule as unknown as Record<string, unknown>,
201
249
  // MCP model context receives this projection exactly once. The full
202
250
  // canonical capsule remains available to application clients through
203
251
  // structuredContent and is deliberately not duplicated in text.
204
252
  text: formatContextCapsuleAgentJson(capsule),
253
+ traceId: traceSession?.metadata()?.traceId,
205
254
  };
255
+ } catch (cause) {
256
+ await finishRetrievalTraceAfterError(traceSession, cause);
257
+ throw cause;
206
258
  } finally {
207
259
  await modelPorts?.dispose();
208
260
  }