@gmickel/gno 1.17.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.
- package/README.md +14 -2
- package/assets/skill/SKILL.md +17 -1
- package/assets/skill/mcp-reference.md +21 -0
- package/package.json +2 -2
- package/src/cli/commands/daemon.ts +69 -2
- package/src/cli/commands/models/pull.ts +13 -3
- package/src/cli/commands/status.ts +2 -0
- package/src/cli/detach.ts +37 -20
- package/src/cli/program.ts +74 -27
- package/src/config/index.ts +3 -0
- package/src/config/types.ts +37 -0
- package/src/core/job-manager.ts +19 -0
- package/src/core/mutation-generations.ts +33 -0
- package/src/llm/cache.ts +13 -3
- package/src/llm/nodeLlamaCpp/adapter.ts +10 -1
- package/src/llm/nodeLlamaCpp/lifecycle.ts +71 -0
- package/src/mcp/context.ts +161 -0
- package/src/mcp/http-security.ts +477 -0
- package/src/mcp/http-session.ts +272 -0
- package/src/mcp/http-transport.ts +370 -0
- package/src/mcp/resources/index.ts +141 -134
- package/src/mcp/server.ts +19 -79
- package/src/mcp/tools/add-collection.ts +3 -1
- package/src/mcp/tools/capture.ts +3 -0
- package/src/mcp/tools/clear-collection-embeddings.ts +2 -0
- package/src/mcp/tools/context.ts +9 -8
- package/src/mcp/tools/embed.ts +62 -52
- package/src/mcp/tools/index-cmd.ts +88 -74
- package/src/mcp/tools/index.ts +22 -2
- package/src/mcp/tools/remove-collection.ts +2 -0
- package/src/mcp/tools/status.ts +11 -0
- package/src/mcp/tools/sync.ts +16 -14
- package/src/mcp/tools/workspace-write.ts +7 -3
- package/src/serve/background-runtime.ts +12 -212
- package/src/serve/embed-scheduler.ts +74 -43
- package/src/serve/index.ts +9 -0
- package/src/serve/jobs.ts +78 -80
- package/src/serve/public/components/HealthCenter.tsx +74 -1
- package/src/serve/public/globals.built.css +1 -1
- package/src/serve/public/pages/Dashboard.tsx +1 -0
- package/src/serve/resident-admission.ts +159 -0
- package/src/serve/resident-background-work.ts +39 -0
- package/src/serve/resident-request.ts +55 -0
- package/src/serve/resident-runtime.ts +490 -0
- package/src/serve/resident-status.ts +96 -0
- package/src/serve/routes/api.ts +263 -167
- package/src/serve/routes/mcp.ts +69 -0
- package/src/serve/server.ts +191 -37
- package/src/serve/status-model.ts +51 -0
- package/src/serve/status.ts +5 -0
- package/src/store/sqlite/adapter.ts +26 -9
package/src/core/job-manager.ts
CHANGED
|
@@ -46,6 +46,7 @@ export interface JobRecord {
|
|
|
46
46
|
result?: SyncResult;
|
|
47
47
|
typedResult?: JobResult;
|
|
48
48
|
error?: string;
|
|
49
|
+
progress?: { current: number; total: number; currentFile?: string };
|
|
49
50
|
serverInstanceId: string;
|
|
50
51
|
}
|
|
51
52
|
|
|
@@ -148,6 +149,24 @@ export class JobManager {
|
|
|
148
149
|
return this.#jobs.get(jobId);
|
|
149
150
|
}
|
|
150
151
|
|
|
152
|
+
getActiveJob(): JobRecord | null {
|
|
153
|
+
if (!this.#activeJobId) return null;
|
|
154
|
+
return this.#jobs.get(this.#activeJobId) ?? null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
updateJobProgress(
|
|
158
|
+
jobId: string,
|
|
159
|
+
progress: { current: number; total: number; currentFile?: string }
|
|
160
|
+
): void {
|
|
161
|
+
const job = this.#jobs.get(jobId);
|
|
162
|
+
if (job) job.progress = progress;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
clear(): void {
|
|
166
|
+
this.#jobs.clear();
|
|
167
|
+
this.#activeJobId = null;
|
|
168
|
+
}
|
|
169
|
+
|
|
151
170
|
listJobs(limit: number = 10): { active: JobRecord[]; recent: JobRecord[] } {
|
|
152
171
|
this.#cleanupExpiredJobs();
|
|
153
172
|
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** Shared mutation detection for resident content and vector generations. */
|
|
2
|
+
|
|
3
|
+
interface SyncMutationCounts {
|
|
4
|
+
filesAdded?: number;
|
|
5
|
+
filesUpdated?: number;
|
|
6
|
+
filesMarkedInactive?: number;
|
|
7
|
+
totalFilesAdded?: number;
|
|
8
|
+
totalFilesUpdated?: number;
|
|
9
|
+
collections?: readonly SyncMutationCounts[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function hasContentMutation(result: SyncMutationCounts): boolean {
|
|
13
|
+
return (
|
|
14
|
+
(result.filesAdded ?? result.totalFilesAdded ?? 0) > 0 ||
|
|
15
|
+
(result.filesUpdated ?? result.totalFilesUpdated ?? 0) > 0 ||
|
|
16
|
+
(result.filesMarkedInactive ?? 0) > 0 ||
|
|
17
|
+
result.collections?.some(hasContentMutation) === true
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function recordContentMutation(
|
|
22
|
+
result: SyncMutationCounts,
|
|
23
|
+
markMutation: (() => void) | undefined
|
|
24
|
+
): void {
|
|
25
|
+
if (hasContentMutation(result)) markMutation?.();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function recordIndexMutation(
|
|
29
|
+
embedded: number,
|
|
30
|
+
markMutation: (() => void) | undefined
|
|
31
|
+
): void {
|
|
32
|
+
if (embedded > 0) markMutation?.();
|
|
33
|
+
}
|
package/src/llm/cache.ts
CHANGED
|
@@ -46,6 +46,7 @@ const HF_PATH_PATTERN = /^([^/]+)\/([^/]+)\/(.+\.gguf)$/;
|
|
|
46
46
|
const GGUF_MAGIC = new Uint8Array([0x47, 0x47, 0x55, 0x46]);
|
|
47
47
|
|
|
48
48
|
type ModelFileOwner = "cache" | "user";
|
|
49
|
+
type ResolveModelFile = typeof import("node-llama-cpp").resolveModelFile;
|
|
49
50
|
|
|
50
51
|
type ValidatedCachedPath =
|
|
51
52
|
| { ok: true; path: string }
|
|
@@ -268,11 +269,16 @@ const MANIFEST_VERSION = "1.0" as const;
|
|
|
268
269
|
export class ModelCache {
|
|
269
270
|
readonly dir: string;
|
|
270
271
|
private readonly manifestPath: string;
|
|
272
|
+
private readonly resolveModelFileFn?: ResolveModelFile;
|
|
271
273
|
private manifest: Manifest | null = null;
|
|
272
274
|
|
|
273
|
-
constructor(
|
|
275
|
+
constructor(
|
|
276
|
+
cacheDir?: string,
|
|
277
|
+
deps?: { resolveModelFile?: ResolveModelFile }
|
|
278
|
+
) {
|
|
274
279
|
this.dir = cacheDir ?? getModelsCachePath();
|
|
275
280
|
this.manifestPath = join(this.dir, "manifest.json");
|
|
281
|
+
this.resolveModelFileFn = deps?.resolveModelFile;
|
|
276
282
|
}
|
|
277
283
|
|
|
278
284
|
/**
|
|
@@ -324,7 +330,8 @@ export class ModelCache {
|
|
|
324
330
|
uri: string,
|
|
325
331
|
type: ModelType,
|
|
326
332
|
onProgress?: ProgressCallback,
|
|
327
|
-
force?: boolean
|
|
333
|
+
force?: boolean,
|
|
334
|
+
signal?: AbortSignal
|
|
328
335
|
): Promise<LlmResult<string>> {
|
|
329
336
|
const parsed = parseModelUri(uri);
|
|
330
337
|
if (!parsed.ok) {
|
|
@@ -364,7 +371,9 @@ export class ModelCache {
|
|
|
364
371
|
}
|
|
365
372
|
|
|
366
373
|
try {
|
|
367
|
-
const
|
|
374
|
+
const resolveModelFile =
|
|
375
|
+
this.resolveModelFileFn ??
|
|
376
|
+
(await import("node-llama-cpp")).resolveModelFile;
|
|
368
377
|
|
|
369
378
|
// Convert to node-llama-cpp format (handles quantization shorthand)
|
|
370
379
|
// node-llama-cpp needs hf: prefix to identify HuggingFace models
|
|
@@ -399,6 +408,7 @@ export class ModelCache {
|
|
|
399
408
|
}
|
|
400
409
|
}
|
|
401
410
|
: undefined,
|
|
411
|
+
signal,
|
|
402
412
|
});
|
|
403
413
|
|
|
404
414
|
const validation = await validateGgufFile(resolvedPath, uri, "cache");
|
|
@@ -27,7 +27,11 @@ import {
|
|
|
27
27
|
} from "../registry";
|
|
28
28
|
import { NodeLlamaCppEmbedding } from "./embedding";
|
|
29
29
|
import { NodeLlamaCppGeneration } from "./generation";
|
|
30
|
-
import {
|
|
30
|
+
import {
|
|
31
|
+
getModelManager,
|
|
32
|
+
type ModelLease,
|
|
33
|
+
type ModelManager,
|
|
34
|
+
} from "./lifecycle";
|
|
31
35
|
import { NodeLlamaCppRerank } from "./rerank";
|
|
32
36
|
|
|
33
37
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -218,6 +222,11 @@ export class LlmAdapter {
|
|
|
218
222
|
return this.manager;
|
|
219
223
|
}
|
|
220
224
|
|
|
225
|
+
/** Acquire an idempotent request lease without transferring manager ownership. */
|
|
226
|
+
acquireModelLease(): ModelLease {
|
|
227
|
+
return this.manager.acquireLease();
|
|
228
|
+
}
|
|
229
|
+
|
|
221
230
|
/**
|
|
222
231
|
* Dispose all resources.
|
|
223
232
|
*/
|
|
@@ -35,6 +35,21 @@ interface CachedModel {
|
|
|
35
35
|
loadedAt: number;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
export interface ModelLease {
|
|
39
|
+
release(): void;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface ModelLifecycleStats {
|
|
43
|
+
activeLeases: number;
|
|
44
|
+
leaseAcquisitions: number;
|
|
45
|
+
leaseReleases: number;
|
|
46
|
+
loadedModels: number;
|
|
47
|
+
loadAttempts: number;
|
|
48
|
+
loadSuccesses: number;
|
|
49
|
+
loadFailures: number;
|
|
50
|
+
inflightLoads: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
38
53
|
let invalidGpuModeWarned = false;
|
|
39
54
|
let invalidBuildModeWarned = false;
|
|
40
55
|
let gpuFallbackWarned = false;
|
|
@@ -134,7 +149,14 @@ export class ModelManager {
|
|
|
134
149
|
new Map();
|
|
135
150
|
private readonly inflightLoads: Map<string, Promise<LlmResult<LoadedModel>>> =
|
|
136
151
|
new Map();
|
|
152
|
+
private readonly leaseDrainWaiters = new Set<() => void>();
|
|
137
153
|
private readonly config: ModelConfig;
|
|
154
|
+
private activeLeases = 0;
|
|
155
|
+
private leaseAcquisitions = 0;
|
|
156
|
+
private leaseReleases = 0;
|
|
157
|
+
private loadAttempts = 0;
|
|
158
|
+
private loadSuccesses = 0;
|
|
159
|
+
private loadFailures = 0;
|
|
138
160
|
|
|
139
161
|
constructor(config: ModelConfig) {
|
|
140
162
|
this.config = config;
|
|
@@ -263,6 +285,7 @@ export class ModelManager {
|
|
|
263
285
|
uri: string,
|
|
264
286
|
type: ModelType
|
|
265
287
|
): Promise<LlmResult<LoadedModel>> {
|
|
288
|
+
this.loadAttempts += 1;
|
|
266
289
|
const timeoutMs = this.config.loadTimeout;
|
|
267
290
|
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
|
268
291
|
let timedOut = false;
|
|
@@ -299,6 +322,7 @@ export class ModelManager {
|
|
|
299
322
|
|
|
300
323
|
this.models.set(uri, cachedModel);
|
|
301
324
|
this.setDisposalTimer(uri);
|
|
325
|
+
this.loadSuccesses += 1;
|
|
302
326
|
|
|
303
327
|
return {
|
|
304
328
|
ok: true,
|
|
@@ -310,6 +334,7 @@ export class ModelManager {
|
|
|
310
334
|
},
|
|
311
335
|
};
|
|
312
336
|
} catch (e) {
|
|
337
|
+
this.loadFailures += 1;
|
|
313
338
|
// Clear timeout on error
|
|
314
339
|
if (timeoutId) {
|
|
315
340
|
clearTimeout(timeoutId);
|
|
@@ -356,6 +381,42 @@ export class ModelManager {
|
|
|
356
381
|
return model;
|
|
357
382
|
}
|
|
358
383
|
|
|
384
|
+
/** Keep warm models alive while one request owns model-backed ports. */
|
|
385
|
+
acquireLease(): ModelLease {
|
|
386
|
+
this.activeLeases += 1;
|
|
387
|
+
this.leaseAcquisitions += 1;
|
|
388
|
+
for (const timer of this.disposalTimers.values()) clearTimeout(timer);
|
|
389
|
+
this.disposalTimers.clear();
|
|
390
|
+
|
|
391
|
+
let released = false;
|
|
392
|
+
return {
|
|
393
|
+
release: () => {
|
|
394
|
+
if (released) return;
|
|
395
|
+
released = true;
|
|
396
|
+
this.activeLeases = Math.max(0, this.activeLeases - 1);
|
|
397
|
+
this.leaseReleases += 1;
|
|
398
|
+
if (this.activeLeases === 0) {
|
|
399
|
+
for (const resolve of this.leaseDrainWaiters) resolve();
|
|
400
|
+
this.leaseDrainWaiters.clear();
|
|
401
|
+
for (const uri of this.models.keys()) this.setDisposalTimer(uri);
|
|
402
|
+
}
|
|
403
|
+
},
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
getLifecycleStats(): ModelLifecycleStats {
|
|
408
|
+
return {
|
|
409
|
+
activeLeases: this.activeLeases,
|
|
410
|
+
leaseAcquisitions: this.leaseAcquisitions,
|
|
411
|
+
leaseReleases: this.leaseReleases,
|
|
412
|
+
loadedModels: this.models.size,
|
|
413
|
+
loadAttempts: this.loadAttempts,
|
|
414
|
+
loadSuccesses: this.loadSuccesses,
|
|
415
|
+
loadFailures: this.loadFailures,
|
|
416
|
+
inflightLoads: this.inflightLoads.size,
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
|
|
359
420
|
/**
|
|
360
421
|
* Check if a model is loaded.
|
|
361
422
|
*/
|
|
@@ -367,6 +428,10 @@ export class ModelManager {
|
|
|
367
428
|
* Dispose a specific model.
|
|
368
429
|
*/
|
|
369
430
|
async dispose(uri: string): Promise<void> {
|
|
431
|
+
if (this.activeLeases > 0) {
|
|
432
|
+
this.resetDisposalTimer(uri);
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
370
435
|
const cached = this.models.get(uri);
|
|
371
436
|
if (!cached) {
|
|
372
437
|
return;
|
|
@@ -393,6 +458,11 @@ export class ModelManager {
|
|
|
393
458
|
* Dispose all loaded models.
|
|
394
459
|
*/
|
|
395
460
|
async disposeAll(): Promise<void> {
|
|
461
|
+
if (this.activeLeases > 0) {
|
|
462
|
+
await new Promise<void>((resolve) => this.leaseDrainWaiters.add(resolve));
|
|
463
|
+
}
|
|
464
|
+
await Promise.allSettled(this.inflightLoads.values());
|
|
465
|
+
|
|
396
466
|
// Clear all timers
|
|
397
467
|
for (const timer of this.disposalTimers.values()) {
|
|
398
468
|
clearTimeout(timer);
|
|
@@ -429,6 +499,7 @@ export class ModelManager {
|
|
|
429
499
|
// ───────────────────────────────────────────────────────────────────────────
|
|
430
500
|
|
|
431
501
|
private setDisposalTimer(uri: string): void {
|
|
502
|
+
if (this.activeLeases > 0) return;
|
|
432
503
|
const timer = setTimeout(() => {
|
|
433
504
|
this.dispose(uri).catch(() => {
|
|
434
505
|
// Ignore disposal errors in timer callback
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/** Shared MCP surface and request-scoped runtime context. */
|
|
2
|
+
|
|
3
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
|
+
// node:async_hooks provides async-local request context; Bun has no separate native equivalent.
|
|
5
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
6
|
+
|
|
7
|
+
import type { Collection, Config } from "../config/types";
|
|
8
|
+
import type { JobManager } from "../core/job-manager";
|
|
9
|
+
import type { ModelLease } from "../llm/nodeLlamaCpp/lifecycle";
|
|
10
|
+
import type { ResidentStatus } from "../serve/status-model";
|
|
11
|
+
import type { SqliteAdapter } from "../store/sqlite/adapter";
|
|
12
|
+
|
|
13
|
+
import { MCP_SERVER_NAME, VERSION } from "../app/constants";
|
|
14
|
+
import { createStandaloneResidentStatus } from "../serve/resident-status";
|
|
15
|
+
import { registerResources } from "./resources/index";
|
|
16
|
+
import { registerTools } from "./tools/index";
|
|
17
|
+
|
|
18
|
+
export interface AsyncMutex {
|
|
19
|
+
acquire(): Promise<() => void>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class Mutex implements AsyncMutex {
|
|
23
|
+
#locked = false;
|
|
24
|
+
readonly #queue: Array<() => void> = [];
|
|
25
|
+
|
|
26
|
+
acquire(): Promise<() => void> {
|
|
27
|
+
return new Promise((resolve) => {
|
|
28
|
+
const tryAcquire = (): void => {
|
|
29
|
+
if (this.#locked) {
|
|
30
|
+
this.#queue.push(tryAcquire);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
this.#locked = true;
|
|
34
|
+
resolve(() => this.#release());
|
|
35
|
+
};
|
|
36
|
+
tryAcquire();
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
#release(): void {
|
|
41
|
+
this.#locked = false;
|
|
42
|
+
this.#queue.shift()?.();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface ToolContextSnapshot {
|
|
47
|
+
config: Config;
|
|
48
|
+
collections: Collection[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface ToolContext {
|
|
52
|
+
store: SqliteAdapter;
|
|
53
|
+
config: Config;
|
|
54
|
+
collections: Collection[];
|
|
55
|
+
actualConfigPath: string;
|
|
56
|
+
indexName: string;
|
|
57
|
+
toolMutex: AsyncMutex;
|
|
58
|
+
jobManager: JobManager;
|
|
59
|
+
serverInstanceId: string;
|
|
60
|
+
writeLockPath: string;
|
|
61
|
+
enableWrite: boolean;
|
|
62
|
+
isShuttingDown: () => boolean;
|
|
63
|
+
getResidentStatus?: () => ResidentStatus;
|
|
64
|
+
acquireModelLease?: () => ModelLease;
|
|
65
|
+
markContentMutation?: () => void;
|
|
66
|
+
markIndexMutation?: () => void;
|
|
67
|
+
runWithSnapshot?<T>(operation: () => Promise<T>): Promise<T>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface CreateToolContextOptions {
|
|
71
|
+
store: SqliteAdapter;
|
|
72
|
+
getConfig: () => Config;
|
|
73
|
+
setConfig?: (config: Config) => void;
|
|
74
|
+
actualConfigPath: string;
|
|
75
|
+
indexName: string;
|
|
76
|
+
toolMutex: AsyncMutex;
|
|
77
|
+
jobManager: JobManager;
|
|
78
|
+
serverInstanceId: string;
|
|
79
|
+
writeLockPath: string;
|
|
80
|
+
enableWrite: boolean;
|
|
81
|
+
isShuttingDown: () => boolean;
|
|
82
|
+
getResidentStatus?: () => ResidentStatus;
|
|
83
|
+
acquireModelLease?: () => ModelLease;
|
|
84
|
+
markContentMutation?: () => void;
|
|
85
|
+
markIndexMutation?: () => void;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Create a transport-neutral MCP context.
|
|
90
|
+
*
|
|
91
|
+
* Config and collection getters resolve from one snapshot captured at the
|
|
92
|
+
* request boundary, so a hot reload cannot mix old and new values mid-call.
|
|
93
|
+
*/
|
|
94
|
+
export function createToolContext(
|
|
95
|
+
options: CreateToolContextOptions
|
|
96
|
+
): ToolContext {
|
|
97
|
+
const requestSnapshot = new AsyncLocalStorage<ToolContextSnapshot>();
|
|
98
|
+
const currentSnapshot = (): ToolContextSnapshot =>
|
|
99
|
+
requestSnapshot.getStore() ??
|
|
100
|
+
(() => {
|
|
101
|
+
const config = options.getConfig();
|
|
102
|
+
return { config, collections: config.collections };
|
|
103
|
+
})();
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
store: options.store,
|
|
107
|
+
get config() {
|
|
108
|
+
return currentSnapshot().config;
|
|
109
|
+
},
|
|
110
|
+
set config(config: Config) {
|
|
111
|
+
options.setConfig?.(config);
|
|
112
|
+
},
|
|
113
|
+
get collections() {
|
|
114
|
+
return currentSnapshot().collections;
|
|
115
|
+
},
|
|
116
|
+
set collections(_collections: Collection[]) {
|
|
117
|
+
// Collections are derived from config. Existing write handlers assign
|
|
118
|
+
// both for backwards compatibility; the config setter is authoritative.
|
|
119
|
+
},
|
|
120
|
+
actualConfigPath: options.actualConfigPath,
|
|
121
|
+
indexName: options.indexName,
|
|
122
|
+
toolMutex: options.toolMutex,
|
|
123
|
+
jobManager: options.jobManager,
|
|
124
|
+
serverInstanceId: options.serverInstanceId,
|
|
125
|
+
writeLockPath: options.writeLockPath,
|
|
126
|
+
enableWrite: options.enableWrite,
|
|
127
|
+
isShuttingDown: options.isShuttingDown,
|
|
128
|
+
getResidentStatus:
|
|
129
|
+
options.getResidentStatus ??
|
|
130
|
+
(() => createStandaloneResidentStatus("stdio")),
|
|
131
|
+
acquireModelLease: options.acquireModelLease,
|
|
132
|
+
markContentMutation: options.markContentMutation,
|
|
133
|
+
markIndexMutation: options.markIndexMutation,
|
|
134
|
+
runWithSnapshot<T>(operation: () => Promise<T>): Promise<T> {
|
|
135
|
+
const config = options.getConfig();
|
|
136
|
+
return requestSnapshot.run(
|
|
137
|
+
{ config, collections: config.collections },
|
|
138
|
+
operation
|
|
139
|
+
);
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Build the contract-identical MCP tool/resource surface for any transport. */
|
|
145
|
+
export function createMcpServerSurface(
|
|
146
|
+
context: ToolContext,
|
|
147
|
+
identity: { name: string; version: string } = {
|
|
148
|
+
name: MCP_SERVER_NAME,
|
|
149
|
+
version: VERSION,
|
|
150
|
+
}
|
|
151
|
+
): McpServer {
|
|
152
|
+
const server = new McpServer(identity, {
|
|
153
|
+
capabilities: {
|
|
154
|
+
tools: { listChanged: false },
|
|
155
|
+
resources: { subscribe: false, listChanged: false },
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
registerTools(server, context);
|
|
159
|
+
registerResources(server, context);
|
|
160
|
+
return server;
|
|
161
|
+
}
|