@gmickel/gno 1.16.0 → 1.18.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 (86) hide show
  1. package/README.md +34 -18
  2. package/assets/skill/SKILL.md +25 -6
  3. package/assets/skill/mcp-reference.md +21 -0
  4. package/package.json +2 -2
  5. package/src/app/context-agent-projection.ts +303 -0
  6. package/src/app/context-format.ts +249 -0
  7. package/src/app/context-runtime-contract.ts +325 -0
  8. package/src/app/context-runtime-input.ts +362 -0
  9. package/src/app/context-runtime-types.ts +65 -0
  10. package/src/app/context-runtime.ts +170 -0
  11. package/src/app/context-surface.ts +145 -0
  12. package/src/cli/commands/context-build.ts +149 -0
  13. package/src/cli/commands/context-verify.ts +90 -0
  14. package/src/cli/commands/daemon.ts +69 -2
  15. package/src/cli/commands/models/pull.ts +13 -3
  16. package/src/cli/commands/status.ts +2 -0
  17. package/src/cli/detach.ts +37 -20
  18. package/src/cli/options.ts +4 -0
  19. package/src/cli/program.ts +252 -27
  20. package/src/config/index.ts +3 -0
  21. package/src/config/types.ts +37 -0
  22. package/src/core/context-budget.ts +461 -0
  23. package/src/core/context-capsule-index-schema.ts +15 -0
  24. package/src/core/context-capsule-retrieval-schema.ts +81 -0
  25. package/src/core/context-capsule-schema.ts +473 -0
  26. package/src/core/context-capsule-validation.ts +416 -0
  27. package/src/core/context-capsule-verification.ts +218 -0
  28. package/src/core/context-capsule.ts +439 -0
  29. package/src/core/context-compiler.ts +513 -0
  30. package/src/core/context-evidence-metadata.ts +33 -0
  31. package/src/core/context-evidence.ts +495 -0
  32. package/src/core/context-facets.ts +163 -0
  33. package/src/core/context-guidance.ts +69 -0
  34. package/src/core/context-scope.ts +32 -0
  35. package/src/core/context-verifier-canonical.ts +90 -0
  36. package/src/core/context-verifier-input.ts +66 -0
  37. package/src/core/context-verifier.ts +447 -0
  38. package/src/core/job-manager.ts +19 -0
  39. package/src/core/mutation-generations.ts +33 -0
  40. package/src/core/sections.ts +63 -0
  41. package/src/llm/cache.ts +13 -3
  42. package/src/llm/nodeLlamaCpp/adapter.ts +10 -1
  43. package/src/llm/nodeLlamaCpp/lifecycle.ts +71 -0
  44. package/src/mcp/context.ts +161 -0
  45. package/src/mcp/http-security.ts +477 -0
  46. package/src/mcp/http-session.ts +272 -0
  47. package/src/mcp/http-transport.ts +370 -0
  48. package/src/mcp/resources/index.ts +141 -134
  49. package/src/mcp/server.ts +28 -82
  50. package/src/mcp/tools/add-collection.ts +3 -1
  51. package/src/mcp/tools/capture.ts +3 -0
  52. package/src/mcp/tools/clear-collection-embeddings.ts +2 -0
  53. package/src/mcp/tools/context.ts +230 -0
  54. package/src/mcp/tools/embed.ts +62 -52
  55. package/src/mcp/tools/index-cmd.ts +88 -74
  56. package/src/mcp/tools/index.ts +49 -2
  57. package/src/mcp/tools/remove-collection.ts +2 -0
  58. package/src/mcp/tools/status.ts +11 -0
  59. package/src/mcp/tools/sync.ts +16 -14
  60. package/src/mcp/tools/workspace-write.ts +7 -3
  61. package/src/pipeline/chunk-lookup.ts +33 -0
  62. package/src/pipeline/hybrid.ts +79 -57
  63. package/src/pipeline/types.ts +14 -0
  64. package/src/sdk/client.ts +68 -6
  65. package/src/sdk/index.ts +21 -0
  66. package/src/sdk/types.ts +24 -0
  67. package/src/serve/background-runtime.ts +12 -211
  68. package/src/serve/context-capsule.ts +136 -0
  69. package/src/serve/context.ts +10 -1
  70. package/src/serve/embed-scheduler.ts +74 -43
  71. package/src/serve/index.ts +9 -0
  72. package/src/serve/jobs.ts +78 -80
  73. package/src/serve/public/components/HealthCenter.tsx +74 -1
  74. package/src/serve/public/globals.built.css +1 -1
  75. package/src/serve/public/pages/Dashboard.tsx +1 -0
  76. package/src/serve/resident-admission.ts +159 -0
  77. package/src/serve/resident-background-work.ts +39 -0
  78. package/src/serve/resident-request.ts +55 -0
  79. package/src/serve/resident-runtime.ts +490 -0
  80. package/src/serve/resident-status.ts +96 -0
  81. package/src/serve/routes/api.ts +265 -167
  82. package/src/serve/routes/mcp.ts +69 -0
  83. package/src/serve/server.ts +212 -35
  84. package/src/serve/status-model.ts +51 -0
  85. package/src/serve/status.ts +5 -0
  86. package/src/store/sqlite/adapter.ts +64 -29
@@ -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,67 +5,28 @@
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
- import { MCP_SERVER_NAME, VERSION, getIndexDbPath } from "../app/constants";
12
+ import {
13
+ DEFAULT_INDEX_NAME,
14
+ MCP_SERVER_NAME,
15
+ VERSION,
16
+ getIndexDbPath,
17
+ } from "../app/constants";
18
+ import { canonicalizeIndexName } from "../app/index-name";
17
19
  import { JobManager } from "../core/job-manager";
18
20
  import { envIsSet } from "../llm/policy";
19
21
  import { MCP_ACTIVATION_VERIFICATION_ENV } from "./activation-verification-mode";
22
+ import {
23
+ createMcpServerSurface,
24
+ createToolContext,
25
+ Mutex,
26
+ type ToolContext,
27
+ } from "./context";
20
28
 
21
- // ─────────────────────────────────────────────────────────────────────────────
22
- // Simple Promise Mutex (avoids async-mutex dependency)
23
- // ─────────────────────────────────────────────────────────────────────────────
24
-
25
- class Mutex {
26
- #locked = false;
27
- readonly #queue: Array<() => void> = [];
28
-
29
- acquire(): Promise<() => void> {
30
- return new Promise((resolve) => {
31
- const tryAcquire = () => {
32
- if (this.#locked) {
33
- this.#queue.push(tryAcquire);
34
- } else {
35
- this.#locked = true;
36
- resolve(() => this.#release());
37
- }
38
- };
39
- tryAcquire();
40
- });
41
- }
42
-
43
- #release(): void {
44
- this.#locked = false;
45
- const next = this.#queue.shift();
46
- if (next) {
47
- next();
48
- }
49
- }
50
- }
51
-
52
- // ─────────────────────────────────────────────────────────────────────────────
53
- // Tool Context
54
- // ─────────────────────────────────────────────────────────────────────────────
55
-
56
- export interface ToolContext {
57
- store: SqliteAdapter;
58
- config: Config;
59
- collections: Collection[];
60
- actualConfigPath: string;
61
- indexName?: string;
62
- toolMutex: Mutex;
63
- jobManager: JobManager;
64
- serverInstanceId: string;
65
- writeLockPath: string;
66
- enableWrite: boolean;
67
- isShuttingDown: () => boolean;
68
- }
29
+ export type { ToolContext } from "./context";
69
30
 
70
31
  // ─────────────────────────────────────────────────────────────────────────────
71
32
  // Server Options
@@ -120,7 +81,7 @@ export async function startMcpServer(options: McpServerOptions): Promise<void> {
120
81
  MCP_ACTIVATION_VERIFICATION_ENV
121
82
  );
122
83
  const init = await initStore({
123
- indexName: options.indexName,
84
+ indexName: canonicalizeIndexName(options.indexName ?? DEFAULT_INDEX_NAME),
124
85
  configPath: options.configPath,
125
86
  syncConfig: !activationVerification,
126
87
  });
@@ -130,21 +91,7 @@ export async function startMcpServer(options: McpServerOptions): Promise<void> {
130
91
  process.exit(1);
131
92
  }
132
93
 
133
- const { store, config, collections, actualConfigPath } = init;
134
-
135
- // Create MCP server
136
- const server = new McpServer(
137
- {
138
- name: MCP_SERVER_NAME,
139
- version: VERSION,
140
- },
141
- {
142
- capabilities: {
143
- tools: { listChanged: false },
144
- resources: { subscribe: false, listChanged: false },
145
- },
146
- }
147
- );
94
+ const { store, config, actualConfigPath } = init;
148
95
 
149
96
  // Sequential execution mutex
150
97
  const toolMutex = new Mutex();
@@ -167,27 +114,26 @@ export async function startMcpServer(options: McpServerOptions): Promise<void> {
167
114
  let shuttingDown = false;
168
115
 
169
116
  // Tool context (passed to all handlers)
170
- const ctx: ToolContext = {
117
+ let currentConfig = config;
118
+ const ctx: ToolContext = createToolContext({
171
119
  store,
172
- config,
173
- collections,
120
+ getConfig: () => currentConfig,
121
+ setConfig: (nextConfig) => {
122
+ currentConfig = nextConfig;
123
+ },
174
124
  actualConfigPath,
175
- indexName: options.indexName,
125
+ indexName: canonicalizeIndexName(options.indexName ?? DEFAULT_INDEX_NAME),
176
126
  toolMutex,
177
127
  jobManager,
178
128
  serverInstanceId,
179
129
  writeLockPath,
180
130
  enableWrite,
181
131
  isShuttingDown: () => shuttingDown,
182
- };
183
-
184
- // Register tools (T10.2)
185
- const { registerTools } = await import("./tools/index.js");
186
- registerTools(server, ctx);
187
-
188
- // Register resources (T10.3)
189
- const { registerResources } = await import("./resources/index.js");
190
- registerResources(server, ctx);
132
+ });
133
+ const server = createMcpServerSurface(ctx, {
134
+ name: MCP_SERVER_NAME,
135
+ version: VERSION,
136
+ });
191
137
 
192
138
  if (options.verbose) {
193
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,