@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
@@ -0,0 +1,230 @@
1
+ /** MCP Context Capsule tools over the shared application runtime. */
2
+
3
+ import type { ModelLease } from "../../llm/nodeLlamaCpp/lifecycle";
4
+ import type { EmbeddingPort, RerankPort } from "../../llm/types";
5
+ import type { VectorIndexPort } from "../../store/vector";
6
+ import type { ToolContext } from "../server";
7
+ import type { ToolResult } from "./index";
8
+
9
+ import { formatContextCapsuleAgentJson } from "../../app/context-agent-projection";
10
+ import { formatContextCapsuleVerificationMarkdown } from "../../app/context-format";
11
+ import {
12
+ buildContextCapsule,
13
+ canonicalVerifiedContextCapsuleJson,
14
+ validateContextCapsuleBuildInput,
15
+ verifyContextCapsuleRuntime,
16
+ } from "../../app/context-runtime";
17
+ import {
18
+ contextSurfaceError,
19
+ parseContextBuildSurfaceInput,
20
+ parseContextVerifySurfaceInput,
21
+ } from "../../app/context-surface";
22
+ import { createNonTtyProgressRenderer } from "../../cli/progress";
23
+ import { LlmAdapter } from "../../llm/nodeLlamaCpp/adapter";
24
+ import { resolveDownloadPolicy } from "../../llm/policy";
25
+ import { resolveModelUri } from "../../llm/registry";
26
+ import { createVectorIndexPort } from "../../store/vector";
27
+
28
+ interface ContextToolResultData {
29
+ structuredContent: Record<string, unknown>;
30
+ text: string;
31
+ }
32
+
33
+ const asToolResult = (data: ContextToolResultData): ToolResult => ({
34
+ content: [{ type: "text", text: data.text }],
35
+ structuredContent: data.structuredContent,
36
+ });
37
+
38
+ const asToolError = (error: unknown): ToolResult => {
39
+ const publicError = contextSurfaceError(error);
40
+ return {
41
+ isError: true,
42
+ content: [
43
+ {
44
+ type: "text",
45
+ text: `Error [${publicError.code}]: ${publicError.message}`,
46
+ },
47
+ ],
48
+ structuredContent: {
49
+ error: publicError.code,
50
+ message: publicError.message,
51
+ },
52
+ };
53
+ };
54
+
55
+ const runContextTool = async (
56
+ context: ToolContext,
57
+ operation: () => Promise<ContextToolResultData>
58
+ ): Promise<ToolResult> => {
59
+ if (context.isShuttingDown()) {
60
+ return asToolError(
61
+ Object.assign(new Error("Server is shutting down"), {
62
+ code: "runtime_error",
63
+ })
64
+ );
65
+ }
66
+ const release = await context.toolMutex.acquire();
67
+ try {
68
+ const data = await (context.runWithSnapshot?.(operation) ?? operation());
69
+ return asToolResult(data);
70
+ } catch (error) {
71
+ return asToolError(error);
72
+ } finally {
73
+ release();
74
+ }
75
+ };
76
+
77
+ interface McpModelPorts {
78
+ embedPort: EmbeddingPort | null;
79
+ rerankPort: RerankPort | null;
80
+ vectorIndex: VectorIndexPort | null;
81
+ dispose(): Promise<void>;
82
+ }
83
+
84
+ interface McpModelPortFactory {
85
+ createEmbeddingPort: LlmAdapter["createEmbeddingPort"];
86
+ createRerankPort: LlmAdapter["createRerankPort"];
87
+ acquireModelLease?: LlmAdapter["acquireModelLease"];
88
+ }
89
+
90
+ export const disposeContextModelOwners = async (
91
+ portOwners: readonly { dispose(): Promise<void> }[],
92
+ lease?: ModelLease
93
+ ): Promise<void> => {
94
+ await Promise.allSettled(
95
+ portOwners.map((owner) => Promise.resolve().then(() => owner.dispose()))
96
+ );
97
+ lease?.release();
98
+ };
99
+
100
+ export const createMcpModelPorts = async (
101
+ context: ToolContext,
102
+ collection?: string,
103
+ factoryOverride?: McpModelPortFactory
104
+ ): Promise<McpModelPorts> => {
105
+ const llm = new LlmAdapter(context.config);
106
+ const factory = factoryOverride ?? llm;
107
+ const lease = factory.acquireModelLease?.();
108
+ const policy = resolveDownloadPolicy(process.env, {});
109
+ const progress = createNonTtyProgressRenderer();
110
+ const embedUri = resolveModelUri(
111
+ context.config,
112
+ "embed",
113
+ undefined,
114
+ collection
115
+ );
116
+ let embedPort: EmbeddingPort | null = null;
117
+ let ownedEmbedPort: EmbeddingPort | null = null;
118
+ let rerankPort: RerankPort | null = null;
119
+ let vectorIndex: VectorIndexPort | null = null;
120
+ try {
121
+ const embedResult = await factory.createEmbeddingPort(embedUri, {
122
+ policy,
123
+ onProgress: (value) => progress("embed", value),
124
+ });
125
+ if (embedResult.ok) {
126
+ // Take ownership before init: init failures must not leak the port.
127
+ ownedEmbedPort = embedResult.value;
128
+ const initialized = await ownedEmbedPort.init();
129
+ if (initialized.ok) {
130
+ embedPort = ownedEmbedPort;
131
+ const vectorResult = await createVectorIndexPort(
132
+ context.store.getRawDb(),
133
+ { model: embedUri, dimensions: ownedEmbedPort.dimensions() }
134
+ );
135
+ if (vectorResult.ok) vectorIndex = vectorResult.value;
136
+ }
137
+ }
138
+ const rerankResult = await factory.createRerankPort(
139
+ resolveModelUri(context.config, "rerank", undefined, collection),
140
+ {
141
+ policy,
142
+ onProgress: (value) => progress("rerank", value),
143
+ }
144
+ );
145
+ if (rerankResult.ok) rerankPort = rerankResult.value;
146
+ return {
147
+ embedPort,
148
+ rerankPort,
149
+ vectorIndex,
150
+ async dispose() {
151
+ await disposeContextModelOwners(
152
+ [ownedEmbedPort, rerankPort].filter(
153
+ (port): port is EmbeddingPort | RerankPort => port !== null
154
+ ),
155
+ lease
156
+ );
157
+ },
158
+ };
159
+ } catch (error) {
160
+ await disposeContextModelOwners(
161
+ [ownedEmbedPort, rerankPort].filter(
162
+ (port): port is EmbeddingPort | RerankPort => port !== null
163
+ ),
164
+ lease
165
+ );
166
+ throw error;
167
+ }
168
+ };
169
+
170
+ export const handleContext = (
171
+ args: unknown,
172
+ context: ToolContext
173
+ ): Promise<ToolResult> =>
174
+ runContextTool(context, async () => {
175
+ const parsed = parseContextBuildSurfaceInput(args, context.indexName);
176
+ // This guard is intentionally before any model construction/download.
177
+ validateContextCapsuleBuildInput(
178
+ parsed.input,
179
+ context.indexName,
180
+ context.config.collections.map((collection) => collection.name)
181
+ );
182
+ const useModels = parsed.input.depthPolicy !== "fast";
183
+ const modelPorts = useModels
184
+ ? await createMcpModelPorts(
185
+ context,
186
+ parsed.input.collections?.length === 1
187
+ ? parsed.input.collections[0]
188
+ : undefined
189
+ )
190
+ : null;
191
+ try {
192
+ const capsule = await buildContextCapsule(parsed.input, {
193
+ store: context.store,
194
+ config: context.config,
195
+ indexName: context.indexName,
196
+ vectorIndex: modelPorts?.vectorIndex ?? null,
197
+ embedPort: modelPorts?.embedPort ?? null,
198
+ rerankPort: modelPorts?.rerankPort ?? null,
199
+ });
200
+ return {
201
+ structuredContent: capsule as unknown as Record<string, unknown>,
202
+ // MCP model context receives this projection exactly once. The full
203
+ // canonical capsule remains available to application clients through
204
+ // structuredContent and is deliberately not duplicated in text.
205
+ text: formatContextCapsuleAgentJson(capsule),
206
+ };
207
+ } finally {
208
+ await modelPorts?.dispose();
209
+ }
210
+ });
211
+
212
+ export const handleContextVerify = (
213
+ args: unknown,
214
+ context: ToolContext
215
+ ): Promise<ToolResult> =>
216
+ runContextTool(context, async () => {
217
+ const parsed = parseContextVerifySurfaceInput(args);
218
+ const receipt = await verifyContextCapsuleRuntime(parsed.capsule, {
219
+ store: context.store,
220
+ config: context.config,
221
+ indexName: context.indexName,
222
+ });
223
+ return {
224
+ structuredContent: receipt as unknown as Record<string, unknown>,
225
+ text:
226
+ parsed.format === "md"
227
+ ? formatContextCapsuleVerificationMarkdown(receipt)
228
+ : canonicalVerifiedContextCapsuleJson(receipt),
229
+ };
230
+ });
@@ -9,6 +9,7 @@ import type { ToolContext } from "../server";
9
9
  import { MCP_ERRORS } from "../../core/errors";
10
10
  import { acquireWriteLock, type WriteLockHandle } from "../../core/file-lock";
11
11
  import { JobError } from "../../core/job-manager";
12
+ import { recordIndexMutation } from "../../core/mutation-generations";
12
13
  import { embedBacklog } from "../../embed";
13
14
  import { LlmAdapter } from "../../llm/nodeLlamaCpp/adapter";
14
15
  import { resolveModelUri } from "../../llm/registry";
@@ -81,64 +82,73 @@ export function handleEmbed(
81
82
  "embed",
82
83
  lock,
83
84
  async () => {
84
- // Create LLM adapter with offline policy (fail-fast, no download)
85
- const llm = new LlmAdapter(ctx.config);
86
- const embedResult = await llm.createEmbeddingPort(modelUri, {
87
- policy: { offline: true, allowDownload: false },
88
- });
89
-
90
- if (!embedResult.ok) {
91
- throw new Error(
92
- `MODEL_NOT_FOUND: Embedding model not cached. ` +
93
- `Model: ${modelUri}. ` +
94
- `Run 'gno models pull embed' first.`
95
- );
96
- }
97
-
98
- const embedPort = embedResult.value;
99
-
85
+ const lease = ctx.acquireModelLease?.();
100
86
  try {
101
- // Initialize and get dimensions from port interface
102
- const initResult = await embedPort.init();
103
- if (!initResult.ok) {
104
- throw new Error(initResult.error.message);
105
- }
106
- const dimensions = embedPort.dimensions();
107
-
108
- // Create vector index port
109
- const db = ctx.store.getRawDb();
110
- const vectorResult = await createVectorIndexPort(db, {
111
- model: modelUri,
112
- dimensions,
113
- });
114
- if (!vectorResult.ok) {
115
- throw new Error(vectorResult.error.message);
116
- }
117
- const vectorIndex = vectorResult.value;
118
-
119
- // Create stats port for backlog
120
- const statsPort = createVectorStatsPort(db);
121
-
122
- // Run embedding
123
- const result = await embedBacklog({
124
- statsPort,
125
- embedPort,
126
- vectorIndex,
127
- collection: collection?.name,
128
- modelUri,
129
- batchSize: 32,
87
+ // Create LLM adapter with offline policy (fail-fast, no download)
88
+ const llm = new LlmAdapter(ctx.config);
89
+ const embedResult = await llm.createEmbeddingPort(modelUri, {
90
+ policy: { offline: true, allowDownload: false },
130
91
  });
131
92
 
132
- if (!result.ok) {
133
- throw new Error(result.error.message);
93
+ if (!embedResult.ok) {
94
+ throw new Error(
95
+ `MODEL_NOT_FOUND: Embedding model not cached. ` +
96
+ `Model: ${modelUri}. ` +
97
+ `Run 'gno models pull embed' first.`
98
+ );
134
99
  }
135
100
 
136
- return {
137
- kind: "embed" as const,
138
- value: result.value,
139
- };
101
+ const embedPort = embedResult.value;
102
+
103
+ try {
104
+ // Initialize and get dimensions from port interface
105
+ const initResult = await embedPort.init();
106
+ if (!initResult.ok) {
107
+ throw new Error(initResult.error.message);
108
+ }
109
+ const dimensions = embedPort.dimensions();
110
+
111
+ // Create vector index port
112
+ const db = ctx.store.getRawDb();
113
+ const vectorResult = await createVectorIndexPort(db, {
114
+ model: modelUri,
115
+ dimensions,
116
+ });
117
+ if (!vectorResult.ok) {
118
+ throw new Error(vectorResult.error.message);
119
+ }
120
+ const vectorIndex = vectorResult.value;
121
+
122
+ // Create stats port for backlog
123
+ const statsPort = createVectorStatsPort(db);
124
+
125
+ // Run embedding
126
+ const result = await embedBacklog({
127
+ statsPort,
128
+ embedPort,
129
+ vectorIndex,
130
+ collection: collection?.name,
131
+ modelUri,
132
+ batchSize: 32,
133
+ });
134
+
135
+ if (!result.ok) {
136
+ throw new Error(result.error.message);
137
+ }
138
+ recordIndexMutation(
139
+ result.value.embedded,
140
+ ctx.markIndexMutation
141
+ );
142
+
143
+ return {
144
+ kind: "embed" as const,
145
+ value: result.value,
146
+ };
147
+ } finally {
148
+ await embedPort.dispose();
149
+ }
140
150
  } finally {
141
- await embedPort.dispose();
151
+ lease?.release();
142
152
  }
143
153
  }
144
154
  );
@@ -10,6 +10,10 @@ import type { ToolContext } from "../server";
10
10
  import { MCP_ERRORS } from "../../core/errors";
11
11
  import { acquireWriteLock, type WriteLockHandle } from "../../core/file-lock";
12
12
  import { JobError } from "../../core/job-manager";
13
+ import {
14
+ recordContentMutation,
15
+ recordIndexMutation,
16
+ } from "../../core/mutation-generations";
13
17
  import { normalizeCollectionName } from "../../core/validation";
14
18
  import { embedBacklog } from "../../embed";
15
19
  import { defaultSyncService, withContentTypeRules } from "../../ingestion";
@@ -115,86 +119,96 @@ export function handleIndex(
115
119
  "index",
116
120
  lock,
117
121
  async () => {
118
- // Phase 1: Sync
119
- const syncResult = collection
120
- ? await defaultSyncService
121
- .syncCollection(collection, ctx.store, options)
122
- .then((r) => ({
123
- collections: [r],
124
- totalDurationMs: r.durationMs,
125
- totalFilesProcessed: r.filesProcessed,
126
- totalFilesAdded: r.filesAdded,
127
- totalFilesUpdated: r.filesUpdated,
128
- totalFilesErrored: r.filesErrored,
129
- totalFilesSkipped: r.filesSkipped,
130
- }))
131
- : await defaultSyncService.syncAll(
132
- ctx.collections,
133
- ctx.store,
134
- options
135
- );
136
-
137
- // Phase 2: Embed
138
- const llm = new LlmAdapter(ctx.config);
139
- const embedResult = await llm.createEmbeddingPort(modelUri, {
140
- policy: { offline: true, allowDownload: false },
141
- });
142
-
143
- if (!embedResult.ok) {
144
- throw new Error(
145
- `MODEL_NOT_FOUND: Embedding model not cached. ` +
146
- `Model: ${modelUri}. ` +
147
- `Run 'gno models pull embed' first.`
148
- );
149
- }
150
-
151
- const embedPort = embedResult.value;
152
-
122
+ const lease = ctx.acquireModelLease?.();
153
123
  try {
154
- // Initialize and get dimensions from port interface
155
- const initResult = await embedPort.init();
156
- if (!initResult.ok) {
157
- throw new Error(initResult.error.message);
158
- }
159
- const dimensions = embedPort.dimensions();
160
-
161
- // Create vector index port
162
- const db = ctx.store.getRawDb();
163
- const vectorResult = await createVectorIndexPort(db, {
164
- model: modelUri,
165
- dimensions,
166
- });
167
- if (!vectorResult.ok) {
168
- throw new Error(vectorResult.error.message);
169
- }
170
- const vectorIndex = vectorResult.value;
171
-
172
- // Create stats port for backlog
173
- const statsPort = createVectorStatsPort(db);
174
-
175
- // Run embedding
176
- const backlogResult = await embedBacklog({
177
- statsPort,
178
- embedPort,
179
- vectorIndex,
180
- collection: collection?.name,
181
- modelUri,
182
- batchSize: 32,
124
+ // Phase 1: Sync
125
+ const syncResult = collection
126
+ ? await defaultSyncService
127
+ .syncCollection(collection, ctx.store, options)
128
+ .then((r) => ({
129
+ collections: [r],
130
+ totalDurationMs: r.durationMs,
131
+ totalFilesProcessed: r.filesProcessed,
132
+ totalFilesAdded: r.filesAdded,
133
+ totalFilesUpdated: r.filesUpdated,
134
+ totalFilesErrored: r.filesErrored,
135
+ totalFilesSkipped: r.filesSkipped,
136
+ }))
137
+ : await defaultSyncService.syncAll(
138
+ ctx.collections,
139
+ ctx.store,
140
+ options
141
+ );
142
+ recordContentMutation(syncResult, ctx.markContentMutation);
143
+
144
+ // Phase 2: Embed
145
+ const llm = new LlmAdapter(ctx.config);
146
+ const embedResult = await llm.createEmbeddingPort(modelUri, {
147
+ policy: { offline: true, allowDownload: false },
183
148
  });
184
149
 
185
- if (!backlogResult.ok) {
186
- throw new Error(backlogResult.error.message);
150
+ if (!embedResult.ok) {
151
+ throw new Error(
152
+ `MODEL_NOT_FOUND: Embedding model not cached. ` +
153
+ `Model: ${modelUri}. ` +
154
+ `Run 'gno models pull embed' first.`
155
+ );
187
156
  }
188
157
 
189
- return {
190
- kind: "index" as const,
191
- value: {
192
- sync: syncResult,
193
- embed: backlogResult.value,
194
- },
195
- };
158
+ const embedPort = embedResult.value;
159
+
160
+ try {
161
+ // Initialize and get dimensions from port interface
162
+ const initResult = await embedPort.init();
163
+ if (!initResult.ok) {
164
+ throw new Error(initResult.error.message);
165
+ }
166
+ const dimensions = embedPort.dimensions();
167
+
168
+ // Create vector index port
169
+ const db = ctx.store.getRawDb();
170
+ const vectorResult = await createVectorIndexPort(db, {
171
+ model: modelUri,
172
+ dimensions,
173
+ });
174
+ if (!vectorResult.ok) {
175
+ throw new Error(vectorResult.error.message);
176
+ }
177
+ const vectorIndex = vectorResult.value;
178
+
179
+ // Create stats port for backlog
180
+ const statsPort = createVectorStatsPort(db);
181
+
182
+ // Run embedding
183
+ const backlogResult = await embedBacklog({
184
+ statsPort,
185
+ embedPort,
186
+ vectorIndex,
187
+ collection: collection?.name,
188
+ modelUri,
189
+ batchSize: 32,
190
+ });
191
+
192
+ if (!backlogResult.ok) {
193
+ throw new Error(backlogResult.error.message);
194
+ }
195
+ recordIndexMutation(
196
+ backlogResult.value.embedded,
197
+ ctx.markIndexMutation
198
+ );
199
+
200
+ return {
201
+ kind: "index" as const,
202
+ value: {
203
+ sync: syncResult,
204
+ embed: backlogResult.value,
205
+ },
206
+ };
207
+ } finally {
208
+ await embedPort.dispose();
209
+ }
196
210
  } finally {
197
- await embedPort.dispose();
211
+ lease?.release();
198
212
  }
199
213
  }
200
214
  );
@@ -10,12 +10,17 @@ import { z } from "zod";
10
10
 
11
11
  import type { ToolContext } from "../server";
12
12
 
13
+ import {
14
+ contextBuildSurfaceSchema,
15
+ contextVerifySurfaceSchema,
16
+ } from "../../app/context-surface";
13
17
  import { CAPTURE_MAX_TEXT_BYTES } from "../../core/capture";
14
18
  import { NOTE_PRESETS, type NotePresetId } from "../../core/note-presets";
15
19
  import { normalizeTag } from "../../core/tags";
16
20
  import { handleAddCollection } from "./add-collection";
17
21
  import { handleCapture } from "./capture";
18
22
  import { handleClearCollectionEmbeddings } from "./clear-collection-embeddings";
23
+ import { handleContext, handleContextVerify } from "./context";
19
24
  import { handleEmbed } from "./embed";
20
25
  import { handleGet } from "./get";
21
26
  import { handleIndex } from "./index-cmd";
@@ -72,8 +77,27 @@ export const MCP_TOOL_DESCRIPTIONS = {
72
77
  "Retrieve multiple documents by refs array or glob pattern. Use after gno_search/gno_query to batch top result URIs/docids; set maxBytes and lineNumbers to control context size.",
73
78
  status:
74
79
  "Get index health: collection count, document count, chunk count, embedding backlog, and per-collection stats. Check first when vector/hybrid results look stale or unavailable.",
80
+ context:
81
+ "Compile one deterministic, budgeted, extractive evidence Capsule with exact line spans, coverage gaps, omissions, provenance, and verification fingerprints. Raw search/get tools remain available for manual retrieval.",
82
+ contextVerify:
83
+ "Verify a saved Context Capsule without rebuilding or mutating it. Reports unchanged, stale, missing, reranked, and fingerprint drift states against the active index.",
75
84
  } as const;
76
85
 
86
+ /** Tool names whose execution mutates disk, config, or index state. */
87
+ export const MCP_WRITE_TOOL_NAMES = new Set([
88
+ "gno_capture",
89
+ "gno_add_collection",
90
+ "gno_sync",
91
+ "gno_embed",
92
+ "gno_index",
93
+ "gno_remove_collection",
94
+ "gno_clear_collection_embeddings",
95
+ "gno_create_folder",
96
+ "gno_rename_note",
97
+ "gno_move_note",
98
+ "gno_duplicate_note",
99
+ ]);
100
+
77
101
  // ─────────────────────────────────────────────────────────────────────────────
78
102
  // Shared Input Schemas
79
103
  // ─────────────────────────────────────────────────────────────────────────────
@@ -820,8 +844,9 @@ export async function runTool<T>(
820
844
 
821
845
  // Sequential execution via mutex
822
846
  const release = await ctx.toolMutex.acquire();
847
+ const modelLease = ctx.acquireModelLease?.();
823
848
  try {
824
- const data = await fn();
849
+ const data = await (ctx.runWithSnapshot?.(fn) ?? fn());
825
850
  return {
826
851
  content: [{ type: "text", text: formatText(data) }],
827
852
  structuredContent: data as { [x: string]: unknown },
@@ -837,6 +862,7 @@ export async function runTool<T>(
837
862
  structuredContent: parsedError,
838
863
  };
839
864
  } finally {
865
+ modelLease?.release();
840
866
  release();
841
867
  }
842
868
  }
@@ -860,8 +886,9 @@ export async function runToolNoMutex<T>(
860
886
  };
861
887
  }
862
888
 
889
+ const modelLease = ctx.acquireModelLease?.();
863
890
  try {
864
- const data = await fn();
891
+ const data = await (ctx.runWithSnapshot?.(fn) ?? fn());
865
892
  return {
866
893
  content: [{ type: "text", text: formatText(data) }],
867
894
  structuredContent: data as { [x: string]: unknown },
@@ -876,6 +903,8 @@ export async function runToolNoMutex<T>(
876
903
  content: [{ type: "text", text: `Error: ${message}` }],
877
904
  structuredContent: parsedError,
878
905
  };
906
+ } finally {
907
+ modelLease?.release();
879
908
  }
880
909
  }
881
910
 
@@ -899,6 +928,24 @@ function parseErrorMessage(message: string): { [x: string]: unknown } {
899
928
 
900
929
  export function registerTools(server: McpServer, ctx: ToolContext): void {
901
930
  // Tool IDs use underscores (MCP pattern: ^[a-zA-Z0-9_-]{1,64}$)
931
+ server.registerTool(
932
+ "gno_context",
933
+ {
934
+ description: MCP_TOOL_DESCRIPTIONS.context,
935
+ inputSchema: contextBuildSurfaceSchema,
936
+ },
937
+ (args) => handleContext(args, ctx)
938
+ );
939
+
940
+ server.registerTool(
941
+ "gno_context_verify",
942
+ {
943
+ description: MCP_TOOL_DESCRIPTIONS.contextVerify,
944
+ inputSchema: contextVerifySurfaceSchema,
945
+ },
946
+ (args) => handleContextVerify(args, ctx)
947
+ );
948
+
902
949
  server.tool(
903
950
  "gno_search",
904
951
  MCP_TOOL_DESCRIPTIONS.search,
@@ -89,6 +89,8 @@ export function handleRemoveCollection(
89
89
  if (!mutationResult.ok) {
90
90
  throw mapConfigError(mutationResult.code, mutationResult.error);
91
91
  }
92
+ ctx.markContentMutation?.();
93
+ ctx.markIndexMutation?.();
92
94
 
93
95
  const result: RemoveCollectionResult = {
94
96
  removed: true,