@intentius/chant 0.0.22 → 0.1.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/package.json +1 -1
- package/src/cli/commands/init-lexicon/templates/codegen.ts +188 -0
- package/src/cli/commands/init-lexicon/templates/docs.ts +81 -0
- package/src/cli/commands/init-lexicon/templates/examples.ts +35 -0
- package/src/cli/commands/init-lexicon/templates/lint.ts +30 -0
- package/src/cli/commands/init-lexicon/templates/lsp.ts +39 -0
- package/src/cli/commands/init-lexicon/templates/plugin.ts +110 -0
- package/src/cli/commands/init-lexicon/templates/project.ts +182 -0
- package/src/cli/commands/init-lexicon/templates/spec.ts +57 -0
- package/src/cli/commands/init-lexicon/templates/tests.ts +70 -0
- package/src/cli/commands/init-lexicon.ts +12 -774
- package/src/cli/conflict-check.test.ts +43 -0
- package/src/cli/main.ts +1 -1
- package/src/cli/mcp/resource-handlers.ts +227 -0
- package/src/cli/mcp/server.ts +20 -409
- package/src/cli/mcp/state-tools.ts +138 -0
- package/src/cli/mcp/types.ts +45 -0
- package/src/codegen/docs-file-markers.ts +69 -0
- package/src/codegen/docs-rule-scanning.ts +159 -0
- package/src/codegen/docs-sections.ts +159 -0
- package/src/codegen/docs-sidebar.ts +56 -0
- package/src/codegen/docs-types.ts +79 -0
- package/src/codegen/docs.ts +9 -495
- package/src/codegen/typecheck.ts +13 -0
- package/src/composite.test.ts +75 -0
- package/src/composite.ts +37 -0
- package/src/discovery/collect.test.ts +34 -0
- package/src/discovery/collect.ts +25 -0
- package/src/lexicon-plugin-helpers.ts +130 -0
- package/src/toml-emit.ts +182 -0
- package/src/toml-parse.ts +370 -0
- package/src/toml-utils.ts +60 -0
- package/src/toml.ts +5 -602
- package/src/yaml.ts +7 -2
package/src/cli/mcp/server.ts
CHANGED
|
@@ -6,68 +6,17 @@ import { importTool, handleImport } from "./tools/import";
|
|
|
6
6
|
import { explainTool, handleExplain } from "./tools/explain";
|
|
7
7
|
import { scaffoldTool, createScaffoldHandler } from "./tools/scaffold";
|
|
8
8
|
import { searchTool, createSearchHandler } from "./tools/search";
|
|
9
|
-
import { getContext } from "./resources/context";
|
|
10
9
|
import type { LexiconPlugin } from "../../lexicon";
|
|
11
|
-
import type {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import { computeBuildDigest, diffDigests } from "../../state/digest";
|
|
15
|
-
import { takeSnapshot } from "../../state/snapshot";
|
|
16
|
-
import type { StateSnapshot } from "../../state/types";
|
|
17
|
-
import { discoverSpells } from "../../spell/discovery";
|
|
18
|
-
import { generatePrompt } from "../../spell/prompt";
|
|
19
|
-
import { getRuntime } from "../../runtime-adapter";
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* MCP message types
|
|
23
|
-
*/
|
|
24
|
-
interface McpRequest {
|
|
25
|
-
jsonrpc: "2.0";
|
|
26
|
-
id: string | number;
|
|
27
|
-
method: string;
|
|
28
|
-
params?: Record<string, unknown>;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
interface McpResponse {
|
|
32
|
-
jsonrpc: "2.0";
|
|
33
|
-
id: string | number;
|
|
34
|
-
result?: unknown;
|
|
35
|
-
error?: {
|
|
36
|
-
code: number;
|
|
37
|
-
message: string;
|
|
38
|
-
data?: unknown;
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Tool definition for MCP
|
|
44
|
-
*/
|
|
45
|
-
interface ToolDefinition {
|
|
46
|
-
name: string;
|
|
47
|
-
description: string;
|
|
48
|
-
inputSchema: {
|
|
49
|
-
type: "object";
|
|
50
|
-
properties: Record<string, unknown>;
|
|
51
|
-
required?: string[];
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Resource definition for MCP
|
|
57
|
-
*/
|
|
58
|
-
interface ResourceDefinition {
|
|
59
|
-
uri: string;
|
|
60
|
-
name: string;
|
|
61
|
-
description: string;
|
|
62
|
-
mimeType?: string;
|
|
63
|
-
}
|
|
10
|
+
import type { McpRequest, McpResponse, ToolDefinition, ToolHandler, ResourceDefinition } from "./types";
|
|
11
|
+
import { createSnapshotTool, createDiffTool, createSpellDoneTool } from "./state-tools";
|
|
12
|
+
import { buildResourcesList, handleResourcesRead } from "./resource-handlers";
|
|
64
13
|
|
|
65
14
|
/**
|
|
66
15
|
* MCP Server implementation
|
|
67
16
|
*/
|
|
68
17
|
export class McpServer {
|
|
69
18
|
private tools: Map<string, ToolDefinition> = new Map();
|
|
70
|
-
private toolHandlers: Map<string,
|
|
19
|
+
private toolHandlers: Map<string, ToolHandler> = new Map();
|
|
71
20
|
private pluginResources: Map<string, { definition: ResourceDefinition; handler: () => Promise<string> }> = new Map();
|
|
72
21
|
|
|
73
22
|
constructor(plugins?: LexiconPlugin[]) {
|
|
@@ -80,115 +29,14 @@ export class McpServer {
|
|
|
80
29
|
this.registerTool(searchTool, createSearchHandler(plugins ?? []));
|
|
81
30
|
|
|
82
31
|
// Register state tools
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
name: "state-snapshot",
|
|
86
|
-
description: "Capture deployed state for an environment",
|
|
87
|
-
inputSchema: {
|
|
88
|
-
type: "object",
|
|
89
|
-
properties: {
|
|
90
|
-
environment: { type: "string", description: "Target environment" },
|
|
91
|
-
lexicon: { type: "string", description: "Optional — snapshot all lexicons if omitted" },
|
|
92
|
-
},
|
|
93
|
-
required: ["environment"],
|
|
94
|
-
},
|
|
95
|
-
},
|
|
96
|
-
async (params) => {
|
|
97
|
-
const env = params.environment as string;
|
|
98
|
-
const lexiconFilter = params.lexicon as string | undefined;
|
|
99
|
-
const targetPlugins = lexiconFilter
|
|
100
|
-
? (plugins ?? []).filter((p) => p.name === lexiconFilter)
|
|
101
|
-
: (plugins ?? []);
|
|
102
|
-
const pluginsWithDescribe = targetPlugins.filter((p) => p.describeResources);
|
|
103
|
-
if (pluginsWithDescribe.length === 0) return "No plugins implement describeResources";
|
|
104
|
-
const serializers = (plugins ?? []).map((p) => p.serializer);
|
|
105
|
-
const buildResult = await build(resolve("."), serializers);
|
|
106
|
-
if (buildResult.errors.length > 0) return "Build failed";
|
|
107
|
-
const result = await takeSnapshot(env, pluginsWithDescribe, buildResult);
|
|
108
|
-
return { snapshots: result.snapshots.length, warnings: result.warnings, errors: result.errors };
|
|
109
|
-
},
|
|
110
|
-
);
|
|
111
|
-
|
|
112
|
-
this.registerTool(
|
|
113
|
-
{
|
|
114
|
-
name: "state-diff",
|
|
115
|
-
description: "Compare current build declarations against last snapshot's digest",
|
|
116
|
-
inputSchema: {
|
|
117
|
-
type: "object",
|
|
118
|
-
properties: {
|
|
119
|
-
environment: { type: "string", description: "Target environment" },
|
|
120
|
-
lexicon: { type: "string", description: "Optional — diff all lexicons if omitted" },
|
|
121
|
-
},
|
|
122
|
-
required: ["environment"],
|
|
123
|
-
},
|
|
124
|
-
},
|
|
125
|
-
async (params) => {
|
|
126
|
-
const env = params.environment as string;
|
|
127
|
-
const lexiconFilter = params.lexicon as string | undefined;
|
|
128
|
-
const serializers = (plugins ?? []).map((p) => p.serializer);
|
|
129
|
-
const buildResult = await build(resolve("."), serializers);
|
|
130
|
-
if (buildResult.errors.length > 0) return "Build failed";
|
|
131
|
-
const currentDigest = computeBuildDigest(buildResult);
|
|
132
|
-
const lexicons = lexiconFilter ? [lexiconFilter] : buildResult.manifest.lexicons;
|
|
133
|
-
const results: Record<string, unknown> = {};
|
|
134
|
-
for (const lex of lexicons) {
|
|
135
|
-
const content = await readSnapshot(env, lex);
|
|
136
|
-
let previousDigest = undefined;
|
|
137
|
-
if (content) {
|
|
138
|
-
const snapshot: StateSnapshot = JSON.parse(content);
|
|
139
|
-
previousDigest = snapshot.digest;
|
|
140
|
-
}
|
|
141
|
-
results[lex] = diffDigests(currentDigest, previousDigest);
|
|
142
|
-
}
|
|
143
|
-
return results;
|
|
144
|
-
},
|
|
145
|
-
);
|
|
32
|
+
const snapshot = createSnapshotTool(plugins ?? []);
|
|
33
|
+
this.registerTool(snapshot.definition, snapshot.handler);
|
|
146
34
|
|
|
147
|
-
|
|
148
|
-
this.registerTool(
|
|
149
|
-
{
|
|
150
|
-
name: "spell-done",
|
|
151
|
-
description: "Mark a spell task as done",
|
|
152
|
-
inputSchema: {
|
|
153
|
-
type: "object",
|
|
154
|
-
properties: {
|
|
155
|
-
name: { type: "string", description: "Spell name" },
|
|
156
|
-
taskNumber: { type: "number", description: "Task number (1-based)" },
|
|
157
|
-
},
|
|
158
|
-
required: ["name", "taskNumber"],
|
|
159
|
-
},
|
|
160
|
-
},
|
|
161
|
-
async (params) => {
|
|
162
|
-
const { readFileSync, writeFileSync } = await import("node:fs");
|
|
163
|
-
const { spells } = await discoverSpells();
|
|
164
|
-
const name = params.name as string;
|
|
165
|
-
const taskNumber = params.taskNumber as number;
|
|
166
|
-
const spell = spells.get(name);
|
|
167
|
-
if (!spell) return `Spell "${name}" not found`;
|
|
168
|
-
if (taskNumber < 1 || taskNumber > spell.definition.tasks.length) {
|
|
169
|
-
return `Invalid task number ${taskNumber}`;
|
|
170
|
-
}
|
|
171
|
-
const task = spell.definition.tasks[taskNumber - 1];
|
|
172
|
-
if (task.done) return `Task ${taskNumber} is already done`;
|
|
35
|
+
const diff = createDiffTool(plugins ?? []);
|
|
36
|
+
this.registerTool(diff.definition, diff.handler);
|
|
173
37
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
const rewritten = content.replace(
|
|
177
|
-
/task\(("[^"]*"|'[^']*'|`[^`]*`)((?:\s*,\s*\{[^}]*\})?)\)/g,
|
|
178
|
-
(match, desc, opts) => {
|
|
179
|
-
count++;
|
|
180
|
-
if (count !== taskNumber) return match;
|
|
181
|
-
if (opts && opts.includes("done:")) {
|
|
182
|
-
return match.replace(/done:\s*false/, "done: true");
|
|
183
|
-
}
|
|
184
|
-
return `task(${desc}, { done: true })`;
|
|
185
|
-
},
|
|
186
|
-
);
|
|
187
|
-
if (rewritten === content) return `Could not rewrite task ${taskNumber}`;
|
|
188
|
-
writeFileSync(spell.filePath, rewritten);
|
|
189
|
-
return `Task ${taskNumber} marked done: "${task.description}"`;
|
|
190
|
-
},
|
|
191
|
-
);
|
|
38
|
+
const spellDone = createSpellDoneTool();
|
|
39
|
+
this.registerTool(spellDone.definition, spellDone.handler);
|
|
192
40
|
|
|
193
41
|
// Register plugin contributions
|
|
194
42
|
if (plugins) {
|
|
@@ -241,7 +89,7 @@ export class McpServer {
|
|
|
241
89
|
*/
|
|
242
90
|
private registerTool(
|
|
243
91
|
definition: ToolDefinition,
|
|
244
|
-
handler:
|
|
92
|
+
handler: ToolHandler,
|
|
245
93
|
): void {
|
|
246
94
|
this.tools.set(definition.name, definition);
|
|
247
95
|
this.toolHandlers.set(definition.name, handler);
|
|
@@ -276,51 +124,29 @@ export class McpServer {
|
|
|
276
124
|
private async dispatch(method: string, params: Record<string, unknown>): Promise<unknown> {
|
|
277
125
|
switch (method) {
|
|
278
126
|
case "initialize":
|
|
279
|
-
return
|
|
127
|
+
return {
|
|
128
|
+
protocolVersion: "2024-11-05",
|
|
129
|
+
capabilities: { tools: {}, resources: {} },
|
|
130
|
+
serverInfo: { name: "chant", version: "0.1.0" },
|
|
131
|
+
};
|
|
280
132
|
|
|
281
133
|
case "tools/list":
|
|
282
|
-
return this.
|
|
134
|
+
return { tools: Array.from(this.tools.values()) };
|
|
283
135
|
|
|
284
136
|
case "tools/call":
|
|
285
137
|
return this.handleToolsCall(params);
|
|
286
138
|
|
|
287
139
|
case "resources/list":
|
|
288
|
-
return this.
|
|
140
|
+
return buildResourcesList(this.pluginResources);
|
|
289
141
|
|
|
290
142
|
case "resources/read":
|
|
291
|
-
return
|
|
143
|
+
return handleResourcesRead(params, this.pluginResources);
|
|
292
144
|
|
|
293
145
|
default:
|
|
294
146
|
throw new Error(`Unknown method: ${method}`);
|
|
295
147
|
}
|
|
296
148
|
}
|
|
297
149
|
|
|
298
|
-
/**
|
|
299
|
-
* Handle initialize request
|
|
300
|
-
*/
|
|
301
|
-
private handleInitialize(params: Record<string, unknown>): unknown {
|
|
302
|
-
return {
|
|
303
|
-
protocolVersion: "2024-11-05",
|
|
304
|
-
capabilities: {
|
|
305
|
-
tools: {},
|
|
306
|
-
resources: {},
|
|
307
|
-
},
|
|
308
|
-
serverInfo: {
|
|
309
|
-
name: "chant",
|
|
310
|
-
version: "0.1.0",
|
|
311
|
-
},
|
|
312
|
-
};
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
/**
|
|
316
|
-
* Handle tools/list request
|
|
317
|
-
*/
|
|
318
|
-
private handleToolsList(): unknown {
|
|
319
|
-
return {
|
|
320
|
-
tools: Array.from(this.tools.values()),
|
|
321
|
-
};
|
|
322
|
-
}
|
|
323
|
-
|
|
324
150
|
/**
|
|
325
151
|
* Handle tools/call request
|
|
326
152
|
*/
|
|
@@ -331,12 +157,7 @@ export class McpServer {
|
|
|
331
157
|
const handler = this.toolHandlers.get(name);
|
|
332
158
|
if (!handler) {
|
|
333
159
|
return {
|
|
334
|
-
content: [
|
|
335
|
-
{
|
|
336
|
-
type: "text",
|
|
337
|
-
text: `Error: Unknown tool: ${name}`,
|
|
338
|
-
},
|
|
339
|
-
],
|
|
160
|
+
content: [{ type: "text", text: `Error: Unknown tool: ${name}` }],
|
|
340
161
|
isError: true,
|
|
341
162
|
};
|
|
342
163
|
}
|
|
@@ -364,216 +185,6 @@ export class McpServer {
|
|
|
364
185
|
}
|
|
365
186
|
}
|
|
366
187
|
|
|
367
|
-
/**
|
|
368
|
-
* Handle resources/list request — merges core + plugin resources
|
|
369
|
-
*/
|
|
370
|
-
private handleResourcesList(): unknown {
|
|
371
|
-
const resources: ResourceDefinition[] = [
|
|
372
|
-
{
|
|
373
|
-
uri: "chant://context",
|
|
374
|
-
name: "chant Context",
|
|
375
|
-
description: "Lexicon-specific instructions and patterns for chant development",
|
|
376
|
-
mimeType: "text/markdown",
|
|
377
|
-
},
|
|
378
|
-
{
|
|
379
|
-
uri: "chant://examples/list",
|
|
380
|
-
name: "Examples List",
|
|
381
|
-
description: "List of available chant examples",
|
|
382
|
-
mimeType: "application/json",
|
|
383
|
-
},
|
|
384
|
-
{
|
|
385
|
-
uri: "chant://spells",
|
|
386
|
-
name: "Spells",
|
|
387
|
-
description: "List all spells with status, tasks, and lexicon",
|
|
388
|
-
mimeType: "application/json",
|
|
389
|
-
},
|
|
390
|
-
{
|
|
391
|
-
uri: "chant://spell/{name}",
|
|
392
|
-
name: "Spell details",
|
|
393
|
-
description: "Show spell definition and status",
|
|
394
|
-
mimeType: "application/json",
|
|
395
|
-
},
|
|
396
|
-
{
|
|
397
|
-
uri: "chant://spell/{name}/prompt",
|
|
398
|
-
name: "Spell bootstrap prompt",
|
|
399
|
-
description: "Bootstrap prompt for agent consumption",
|
|
400
|
-
mimeType: "text/markdown",
|
|
401
|
-
},
|
|
402
|
-
{
|
|
403
|
-
uri: "chant://state/{environment}",
|
|
404
|
-
name: "State (all lexicons)",
|
|
405
|
-
description: "All lexicon snapshots for an environment",
|
|
406
|
-
mimeType: "application/json",
|
|
407
|
-
},
|
|
408
|
-
{
|
|
409
|
-
uri: "chant://state/{environment}/{lexicon}",
|
|
410
|
-
name: "State (single lexicon)",
|
|
411
|
-
description: "Single lexicon snapshot for an environment",
|
|
412
|
-
mimeType: "application/json",
|
|
413
|
-
},
|
|
414
|
-
];
|
|
415
|
-
|
|
416
|
-
// Merge plugin resources
|
|
417
|
-
for (const { definition } of this.pluginResources.values()) {
|
|
418
|
-
resources.push(definition);
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
return { resources };
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
/**
|
|
425
|
-
* Collect example resources from plugins whose URI contains "examples/"
|
|
426
|
-
*/
|
|
427
|
-
private collectExamples(): Array<{ name: string; description: string }> {
|
|
428
|
-
const examples: Array<{ name: string; description: string }> = [];
|
|
429
|
-
for (const [uri, { definition }] of this.pluginResources.entries()) {
|
|
430
|
-
if (uri.includes("/examples/")) {
|
|
431
|
-
const name = uri.replace(/^chant:\/\/[^/]+\/examples\//, "");
|
|
432
|
-
examples.push({ name, description: definition.description });
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
return examples;
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
/**
|
|
439
|
-
* Handle resources/read request — checks plugin resources after core
|
|
440
|
-
*/
|
|
441
|
-
private async handleResourcesRead(params: Record<string, unknown>): Promise<unknown> {
|
|
442
|
-
const uri = params.uri as string;
|
|
443
|
-
|
|
444
|
-
if (uri === "chant://context") {
|
|
445
|
-
return {
|
|
446
|
-
contents: [
|
|
447
|
-
{
|
|
448
|
-
uri,
|
|
449
|
-
mimeType: "text/markdown",
|
|
450
|
-
text: getContext(),
|
|
451
|
-
},
|
|
452
|
-
],
|
|
453
|
-
};
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
if (uri === "chant://examples/list") {
|
|
457
|
-
return {
|
|
458
|
-
contents: [
|
|
459
|
-
{
|
|
460
|
-
uri,
|
|
461
|
-
mimeType: "application/json",
|
|
462
|
-
text: JSON.stringify(this.collectExamples()),
|
|
463
|
-
},
|
|
464
|
-
],
|
|
465
|
-
};
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
// Spell resources
|
|
469
|
-
if (uri === "chant://spells") {
|
|
470
|
-
const { spells } = await discoverSpells();
|
|
471
|
-
const list = Array.from(spells.entries()).map(([name, s]) => ({
|
|
472
|
-
name,
|
|
473
|
-
status: s.status,
|
|
474
|
-
tasks: `${s.definition.tasks.filter((t) => t.done).length}/${s.definition.tasks.length}`,
|
|
475
|
-
lexicon: s.definition.lexicon ?? null,
|
|
476
|
-
overview: s.definition.overview,
|
|
477
|
-
}));
|
|
478
|
-
return {
|
|
479
|
-
contents: [{ uri, mimeType: "application/json", text: JSON.stringify(list, null, 2) }],
|
|
480
|
-
};
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
if (uri.startsWith("chant://spell/") && uri.endsWith("/prompt")) {
|
|
484
|
-
const name = uri.replace("chant://spell/", "").replace("/prompt", "");
|
|
485
|
-
const { spells } = await discoverSpells();
|
|
486
|
-
const spell = spells.get(name);
|
|
487
|
-
if (!spell) throw new Error(`Spell "${name}" not found`);
|
|
488
|
-
const rt = getRuntime();
|
|
489
|
-
const gitRootResult = await rt.spawn(["git", "rev-parse", "--show-toplevel"]);
|
|
490
|
-
const gitRoot = gitRootResult.stdout.trim();
|
|
491
|
-
const prompt = await generatePrompt(spell.definition, { gitRoot });
|
|
492
|
-
return {
|
|
493
|
-
contents: [{ uri, mimeType: "text/markdown", text: prompt }],
|
|
494
|
-
};
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
if (uri.startsWith("chant://spell/")) {
|
|
498
|
-
const name = uri.replace("chant://spell/", "");
|
|
499
|
-
const { spells } = await discoverSpells();
|
|
500
|
-
const spell = spells.get(name);
|
|
501
|
-
if (!spell) throw new Error(`Spell "${name}" not found`);
|
|
502
|
-
return {
|
|
503
|
-
contents: [{
|
|
504
|
-
uri,
|
|
505
|
-
mimeType: "application/json",
|
|
506
|
-
text: JSON.stringify({
|
|
507
|
-
...spell.definition,
|
|
508
|
-
status: spell.status,
|
|
509
|
-
filePath: spell.filePath,
|
|
510
|
-
}, null, 2),
|
|
511
|
-
}],
|
|
512
|
-
};
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
// State resources: chant://state/{environment} and chant://state/{environment}/{lexicon}
|
|
516
|
-
if (uri.startsWith("chant://state/")) {
|
|
517
|
-
const parts = uri.replace("chant://state/", "").split("/");
|
|
518
|
-
const environment = parts[0];
|
|
519
|
-
const lexicon = parts[1];
|
|
520
|
-
|
|
521
|
-
if (lexicon) {
|
|
522
|
-
const content = await readSnapshot(environment, lexicon);
|
|
523
|
-
if (!content) throw new Error(`No snapshot found for ${environment}/${lexicon}`);
|
|
524
|
-
return {
|
|
525
|
-
contents: [{ uri, mimeType: "application/json", text: content }],
|
|
526
|
-
};
|
|
527
|
-
} else {
|
|
528
|
-
const snapshots = await readEnvironmentSnapshots(environment);
|
|
529
|
-
const result: Record<string, unknown> = {};
|
|
530
|
-
for (const [lex, content] of snapshots) {
|
|
531
|
-
result[lex] = JSON.parse(content);
|
|
532
|
-
}
|
|
533
|
-
return {
|
|
534
|
-
contents: [{ uri, mimeType: "application/json", text: JSON.stringify(result, null, 2) }],
|
|
535
|
-
};
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
if (uri.startsWith("chant://examples/")) {
|
|
540
|
-
// Look up example in plugin resources
|
|
541
|
-
const name = uri.replace("chant://examples/", "");
|
|
542
|
-
for (const [pluginUri, pluginResource] of this.pluginResources.entries()) {
|
|
543
|
-
if (pluginUri.endsWith(`/examples/${name}`)) {
|
|
544
|
-
const text = await pluginResource.handler();
|
|
545
|
-
return {
|
|
546
|
-
contents: [
|
|
547
|
-
{
|
|
548
|
-
uri,
|
|
549
|
-
mimeType: pluginResource.definition.mimeType ?? "text/typescript",
|
|
550
|
-
text,
|
|
551
|
-
},
|
|
552
|
-
],
|
|
553
|
-
};
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
throw new Error(`Example not found: ${name}`);
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
// Check plugin resources
|
|
560
|
-
const pluginResource = this.pluginResources.get(uri);
|
|
561
|
-
if (pluginResource) {
|
|
562
|
-
const text = await pluginResource.handler();
|
|
563
|
-
return {
|
|
564
|
-
contents: [
|
|
565
|
-
{
|
|
566
|
-
uri,
|
|
567
|
-
mimeType: pluginResource.definition.mimeType ?? "text/plain",
|
|
568
|
-
text,
|
|
569
|
-
},
|
|
570
|
-
],
|
|
571
|
-
};
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
throw new Error(`Unknown resource: ${uri}`);
|
|
575
|
-
}
|
|
576
|
-
|
|
577
188
|
/**
|
|
578
189
|
* Start the MCP server on stdio
|
|
579
190
|
*/
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import type { LexiconPlugin } from "../../lexicon";
|
|
3
|
+
import type { ToolDefinition, ToolHandler } from "./types";
|
|
4
|
+
import { readSnapshot } from "../../state/git";
|
|
5
|
+
import { build } from "../../build";
|
|
6
|
+
import { computeBuildDigest, diffDigests } from "../../state/digest";
|
|
7
|
+
import { takeSnapshot } from "../../state/snapshot";
|
|
8
|
+
import type { StateSnapshot } from "../../state/types";
|
|
9
|
+
import { discoverSpells } from "../../spell/discovery";
|
|
10
|
+
|
|
11
|
+
export interface ToolRegistration {
|
|
12
|
+
definition: ToolDefinition;
|
|
13
|
+
handler: ToolHandler;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Create state-snapshot tool definition and handler
|
|
18
|
+
*/
|
|
19
|
+
export function createSnapshotTool(plugins: LexiconPlugin[]): ToolRegistration {
|
|
20
|
+
return {
|
|
21
|
+
definition: {
|
|
22
|
+
name: "state-snapshot",
|
|
23
|
+
description: "Capture deployed state for an environment",
|
|
24
|
+
inputSchema: {
|
|
25
|
+
type: "object",
|
|
26
|
+
properties: {
|
|
27
|
+
environment: { type: "string", description: "Target environment" },
|
|
28
|
+
lexicon: { type: "string", description: "Optional — snapshot all lexicons if omitted" },
|
|
29
|
+
},
|
|
30
|
+
required: ["environment"],
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
handler: async (params) => {
|
|
34
|
+
const env = params.environment as string;
|
|
35
|
+
const lexiconFilter = params.lexicon as string | undefined;
|
|
36
|
+
const targetPlugins = lexiconFilter
|
|
37
|
+
? plugins.filter((p) => p.name === lexiconFilter)
|
|
38
|
+
: plugins;
|
|
39
|
+
const pluginsWithDescribe = targetPlugins.filter((p) => p.describeResources);
|
|
40
|
+
if (pluginsWithDescribe.length === 0) return "No plugins implement describeResources";
|
|
41
|
+
const serializers = plugins.map((p) => p.serializer);
|
|
42
|
+
const buildResult = await build(resolve("."), serializers);
|
|
43
|
+
if (buildResult.errors.length > 0) return "Build failed";
|
|
44
|
+
const result = await takeSnapshot(env, pluginsWithDescribe, buildResult);
|
|
45
|
+
return { snapshots: result.snapshots.length, warnings: result.warnings, errors: result.errors };
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Create state-diff tool definition and handler
|
|
52
|
+
*/
|
|
53
|
+
export function createDiffTool(plugins: LexiconPlugin[]): ToolRegistration {
|
|
54
|
+
return {
|
|
55
|
+
definition: {
|
|
56
|
+
name: "state-diff",
|
|
57
|
+
description: "Compare current build declarations against last snapshot's digest",
|
|
58
|
+
inputSchema: {
|
|
59
|
+
type: "object",
|
|
60
|
+
properties: {
|
|
61
|
+
environment: { type: "string", description: "Target environment" },
|
|
62
|
+
lexicon: { type: "string", description: "Optional — diff all lexicons if omitted" },
|
|
63
|
+
},
|
|
64
|
+
required: ["environment"],
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
handler: async (params) => {
|
|
68
|
+
const env = params.environment as string;
|
|
69
|
+
const lexiconFilter = params.lexicon as string | undefined;
|
|
70
|
+
const serializers = plugins.map((p) => p.serializer);
|
|
71
|
+
const buildResult = await build(resolve("."), serializers);
|
|
72
|
+
if (buildResult.errors.length > 0) return "Build failed";
|
|
73
|
+
const currentDigest = computeBuildDigest(buildResult);
|
|
74
|
+
const lexicons = lexiconFilter ? [lexiconFilter] : buildResult.manifest.lexicons;
|
|
75
|
+
const results: Record<string, unknown> = {};
|
|
76
|
+
for (const lex of lexicons) {
|
|
77
|
+
const content = await readSnapshot(env, lex);
|
|
78
|
+
let previousDigest = undefined;
|
|
79
|
+
if (content) {
|
|
80
|
+
const snapshot: StateSnapshot = JSON.parse(content);
|
|
81
|
+
previousDigest = snapshot.digest;
|
|
82
|
+
}
|
|
83
|
+
results[lex] = diffDigests(currentDigest, previousDigest);
|
|
84
|
+
}
|
|
85
|
+
return results;
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Create spell-done tool definition and handler
|
|
92
|
+
*/
|
|
93
|
+
export function createSpellDoneTool(): ToolRegistration {
|
|
94
|
+
return {
|
|
95
|
+
definition: {
|
|
96
|
+
name: "spell-done",
|
|
97
|
+
description: "Mark a spell task as done",
|
|
98
|
+
inputSchema: {
|
|
99
|
+
type: "object",
|
|
100
|
+
properties: {
|
|
101
|
+
name: { type: "string", description: "Spell name" },
|
|
102
|
+
taskNumber: { type: "number", description: "Task number (1-based)" },
|
|
103
|
+
},
|
|
104
|
+
required: ["name", "taskNumber"],
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
handler: async (params) => {
|
|
108
|
+
const { readFileSync, writeFileSync } = await import("node:fs");
|
|
109
|
+
const { spells } = await discoverSpells();
|
|
110
|
+
const name = params.name as string;
|
|
111
|
+
const taskNumber = params.taskNumber as number;
|
|
112
|
+
const spell = spells.get(name);
|
|
113
|
+
if (!spell) return `Spell "${name}" not found`;
|
|
114
|
+
if (taskNumber < 1 || taskNumber > spell.definition.tasks.length) {
|
|
115
|
+
return `Invalid task number ${taskNumber}`;
|
|
116
|
+
}
|
|
117
|
+
const task = spell.definition.tasks[taskNumber - 1];
|
|
118
|
+
if (task.done) return `Task ${taskNumber} is already done`;
|
|
119
|
+
|
|
120
|
+
const content = readFileSync(spell.filePath, "utf-8");
|
|
121
|
+
let count = 0;
|
|
122
|
+
const rewritten = content.replace(
|
|
123
|
+
/task\(("[^"]*"|'[^']*'|`[^`]*`)((?:\s*,\s*\{[^}]*\})?)\)/g,
|
|
124
|
+
(match, desc, opts) => {
|
|
125
|
+
count++;
|
|
126
|
+
if (count !== taskNumber) return match;
|
|
127
|
+
if (opts && opts.includes("done:")) {
|
|
128
|
+
return match.replace(/done:\s*false/, "done: true");
|
|
129
|
+
}
|
|
130
|
+
return `task(${desc}, { done: true })`;
|
|
131
|
+
},
|
|
132
|
+
);
|
|
133
|
+
if (rewritten === content) return `Could not rewrite task ${taskNumber}`;
|
|
134
|
+
writeFileSync(spell.filePath, rewritten);
|
|
135
|
+
return `Task ${taskNumber} marked done: "${task.description}"`;
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP message types
|
|
3
|
+
*/
|
|
4
|
+
export interface McpRequest {
|
|
5
|
+
jsonrpc: "2.0";
|
|
6
|
+
id: string | number;
|
|
7
|
+
method: string;
|
|
8
|
+
params?: Record<string, unknown>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface McpResponse {
|
|
12
|
+
jsonrpc: "2.0";
|
|
13
|
+
id: string | number;
|
|
14
|
+
result?: unknown;
|
|
15
|
+
error?: {
|
|
16
|
+
code: number;
|
|
17
|
+
message: string;
|
|
18
|
+
data?: unknown;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Tool definition for MCP
|
|
24
|
+
*/
|
|
25
|
+
export interface ToolDefinition {
|
|
26
|
+
name: string;
|
|
27
|
+
description: string;
|
|
28
|
+
inputSchema: {
|
|
29
|
+
type: "object";
|
|
30
|
+
properties: Record<string, unknown>;
|
|
31
|
+
required?: string[];
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Resource definition for MCP
|
|
37
|
+
*/
|
|
38
|
+
export interface ResourceDefinition {
|
|
39
|
+
uri: string;
|
|
40
|
+
name: string;
|
|
41
|
+
description: string;
|
|
42
|
+
mimeType?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type ToolHandler = (params: Record<string, unknown>) => Promise<unknown>;
|