@intentius/chant 0.0.4 → 0.0.8
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 +10 -351
- package/bin/chant +20 -0
- package/package.json +18 -17
- package/src/bench.test.ts +3 -54
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/plugin.ts +8 -23
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/validate.ts +22 -18
- package/src/cli/commands/__snapshots__/init-lexicon.test.ts.snap +8 -23
- package/src/cli/commands/build.ts +1 -2
- package/src/cli/commands/import.test.ts +1 -1
- package/src/cli/commands/import.ts +2 -2
- package/src/cli/commands/init-lexicon.test.ts +0 -3
- package/src/cli/commands/init-lexicon.ts +31 -95
- package/src/cli/commands/init.test.ts +10 -14
- package/src/cli/commands/init.ts +16 -10
- package/src/cli/commands/lint.ts +9 -33
- package/src/cli/commands/list.ts +2 -2
- package/src/cli/commands/update.ts +5 -3
- package/src/cli/conflict-check.test.ts +0 -1
- package/src/cli/handlers/dev.ts +1 -9
- package/src/cli/main.ts +14 -4
- package/src/cli/mcp/server.test.ts +207 -4
- package/src/cli/mcp/server.ts +6 -0
- package/src/cli/mcp/tools/explain.ts +134 -0
- package/src/cli/mcp/tools/scaffold.ts +107 -0
- package/src/cli/mcp/tools/search.ts +98 -0
- package/src/codegen/docs-interpolation.test.ts +2 -2
- package/src/codegen/docs.ts +5 -4
- package/src/codegen/generate-registry.test.ts +2 -2
- package/src/codegen/generate-registry.ts +5 -6
- package/src/codegen/generate-typescript.test.ts +6 -6
- package/src/codegen/generate-typescript.ts +2 -6
- package/src/codegen/generate.ts +1 -12
- package/src/codegen/package.ts +28 -1
- package/src/codegen/typecheck.ts +6 -11
- package/src/codegen/validate.ts +16 -0
- package/src/config.ts +4 -0
- package/src/discovery/files.ts +6 -6
- package/src/discovery/import.ts +1 -1
- package/src/index.ts +1 -2
- package/src/lexicon-integrity.ts +5 -4
- package/src/lexicon.ts +2 -6
- package/src/lint/config.ts +8 -6
- package/src/lint/engine.ts +1 -5
- package/src/lint/rule.ts +0 -18
- package/src/lint/rules/evl009-composite-no-constant.test.ts +24 -8
- package/src/lint/rules/evl009-composite-no-constant.ts +50 -29
- package/src/lint/rules/index.ts +1 -22
- package/src/runtime-adapter.ts +158 -0
- package/src/serializer-walker.test.ts +0 -9
- package/src/serializer-walker.ts +1 -3
- package/src/stack-output.ts +3 -3
- package/src/barrel.test.ts +0 -157
- package/src/barrel.ts +0 -101
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/rollback.ts +0 -45
- package/src/codegen/case.test.ts +0 -30
- package/src/codegen/case.ts +0 -11
- package/src/codegen/rollback.test.ts +0 -92
- package/src/codegen/rollback.ts +0 -115
- package/src/lint/rules/barrel-import-style.test.ts +0 -80
- package/src/lint/rules/barrel-import-style.ts +0 -59
- package/src/lint/rules/enforce-barrel-import.test.ts +0 -169
- package/src/lint/rules/enforce-barrel-import.ts +0 -81
- package/src/lint/rules/enforce-barrel-ref.test.ts +0 -114
- package/src/lint/rules/enforce-barrel-ref.ts +0 -75
- package/src/lint/rules/evl006-barrel-usage.test.ts +0 -63
- package/src/lint/rules/evl006-barrel-usage.ts +0 -95
- package/src/lint/rules/evl008-unresolvable-barrel-ref.test.ts +0 -118
- package/src/lint/rules/evl008-unresolvable-barrel-ref.ts +0 -140
- package/src/lint/rules/prefer-namespace-import.test.ts +0 -102
- package/src/lint/rules/prefer-namespace-import.ts +0 -63
- package/src/lint/rules/stale-barrel-types.ts +0 -60
- package/src/project/scan.test.ts +0 -178
- package/src/project/scan.ts +0 -182
- package/src/project/sync.test.ts +0 -87
- package/src/project/sync.ts +0 -46
|
@@ -81,7 +81,7 @@ describe("McpServer", () => {
|
|
|
81
81
|
// -----------------------------------------------------------------------
|
|
82
82
|
|
|
83
83
|
describe("tools/list", () => {
|
|
84
|
-
test("returns core tools (build, lint, import)", async () => {
|
|
84
|
+
test("returns core tools (build, lint, import, explain, scaffold, search)", async () => {
|
|
85
85
|
const response = await server.handleRequest({
|
|
86
86
|
jsonrpc: "2.0",
|
|
87
87
|
id: 1,
|
|
@@ -94,6 +94,9 @@ describe("McpServer", () => {
|
|
|
94
94
|
expect(toolNames).toContain("build");
|
|
95
95
|
expect(toolNames).toContain("lint");
|
|
96
96
|
expect(toolNames).toContain("import");
|
|
97
|
+
expect(toolNames).toContain("explain");
|
|
98
|
+
expect(toolNames).toContain("scaffold");
|
|
99
|
+
expect(toolNames).toContain("search");
|
|
97
100
|
});
|
|
98
101
|
|
|
99
102
|
test("each tool has name, description, and inputSchema", async () => {
|
|
@@ -148,6 +151,46 @@ describe("McpServer", () => {
|
|
|
148
151
|
expect(props.source).toBeDefined();
|
|
149
152
|
expect(props.output).toBeDefined();
|
|
150
153
|
});
|
|
154
|
+
|
|
155
|
+
test("explain tool schema has path and format properties", async () => {
|
|
156
|
+
const response = await server.handleRequest({
|
|
157
|
+
jsonrpc: "2.0",
|
|
158
|
+
id: 1,
|
|
159
|
+
method: "tools/list",
|
|
160
|
+
});
|
|
161
|
+
const result = response.result as { tools: Array<{ name: string; inputSchema: Record<string, unknown> }> };
|
|
162
|
+
const tool = result.tools.find((t) => t.name === "explain")!;
|
|
163
|
+
const props = tool.inputSchema.properties as Record<string, unknown>;
|
|
164
|
+
expect(props.path).toBeDefined();
|
|
165
|
+
expect(props.format).toBeDefined();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("scaffold tool schema has pattern and lexicon properties", async () => {
|
|
169
|
+
const response = await server.handleRequest({
|
|
170
|
+
jsonrpc: "2.0",
|
|
171
|
+
id: 1,
|
|
172
|
+
method: "tools/list",
|
|
173
|
+
});
|
|
174
|
+
const result = response.result as { tools: Array<{ name: string; inputSchema: Record<string, unknown> }> };
|
|
175
|
+
const tool = result.tools.find((t) => t.name === "scaffold")!;
|
|
176
|
+
const props = tool.inputSchema.properties as Record<string, unknown>;
|
|
177
|
+
expect(props.pattern).toBeDefined();
|
|
178
|
+
expect(props.lexicon).toBeDefined();
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("search tool schema has query, lexicon, and limit properties", async () => {
|
|
182
|
+
const response = await server.handleRequest({
|
|
183
|
+
jsonrpc: "2.0",
|
|
184
|
+
id: 1,
|
|
185
|
+
method: "tools/list",
|
|
186
|
+
});
|
|
187
|
+
const result = response.result as { tools: Array<{ name: string; inputSchema: Record<string, unknown> }> };
|
|
188
|
+
const tool = result.tools.find((t) => t.name === "search")!;
|
|
189
|
+
const props = tool.inputSchema.properties as Record<string, unknown>;
|
|
190
|
+
expect(props.query).toBeDefined();
|
|
191
|
+
expect(props.lexicon).toBeDefined();
|
|
192
|
+
expect(props.limit).toBeDefined();
|
|
193
|
+
});
|
|
151
194
|
});
|
|
152
195
|
|
|
153
196
|
describe("tools/call", () => {
|
|
@@ -194,6 +237,166 @@ describe("McpServer", () => {
|
|
|
194
237
|
expect(result.isError).toBe(true);
|
|
195
238
|
expect(result.content[0].text).toContain("Error:");
|
|
196
239
|
});
|
|
240
|
+
|
|
241
|
+
test("calls explain tool on empty directory", async () => {
|
|
242
|
+
const response = await server.handleRequest({
|
|
243
|
+
jsonrpc: "2.0",
|
|
244
|
+
id: 1,
|
|
245
|
+
method: "tools/call",
|
|
246
|
+
params: { name: "explain", arguments: { path: testDir } },
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
expect(response.error).toBeUndefined();
|
|
250
|
+
const result = response.result as { content: Array<{ type: string; text: string }> };
|
|
251
|
+
expect(result.content[0].type).toBe("text");
|
|
252
|
+
expect(result.content[0].text).toContain("Project Summary");
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
test("calls explain tool with json format", async () => {
|
|
256
|
+
const response = await server.handleRequest({
|
|
257
|
+
jsonrpc: "2.0",
|
|
258
|
+
id: 1,
|
|
259
|
+
method: "tools/call",
|
|
260
|
+
params: { name: "explain", arguments: { path: testDir, format: "json" } },
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
expect(response.error).toBeUndefined();
|
|
264
|
+
const result = response.result as { content: Array<{ text: string }> };
|
|
265
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
266
|
+
expect(parsed.totalEntities).toBe(0);
|
|
267
|
+
expect(parsed.sourceFiles).toBeDefined();
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test("calls scaffold tool with generic fallback", async () => {
|
|
271
|
+
const response = await server.handleRequest({
|
|
272
|
+
jsonrpc: "2.0",
|
|
273
|
+
id: 1,
|
|
274
|
+
method: "tools/call",
|
|
275
|
+
params: { name: "scaffold", arguments: { pattern: "my-service" } },
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
expect(response.error).toBeUndefined();
|
|
279
|
+
const result = response.result as { content: Array<{ text: string }> };
|
|
280
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
281
|
+
expect(parsed.pattern).toBe("my-service");
|
|
282
|
+
expect(parsed.files).toBeDefined();
|
|
283
|
+
expect(parsed.files.length).toBeGreaterThan(0);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
test("calls search tool with no plugins", async () => {
|
|
287
|
+
const response = await server.handleRequest({
|
|
288
|
+
jsonrpc: "2.0",
|
|
289
|
+
id: 1,
|
|
290
|
+
method: "tools/call",
|
|
291
|
+
params: { name: "search", arguments: { query: "bucket" } },
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
expect(response.error).toBeUndefined();
|
|
295
|
+
const result = response.result as { content: Array<{ text: string }> };
|
|
296
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
297
|
+
expect(parsed.query).toBe("bucket");
|
|
298
|
+
expect(parsed.total).toBe(0);
|
|
299
|
+
expect(parsed.results).toEqual([]);
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
// -----------------------------------------------------------------------
|
|
304
|
+
// Search tool with plugins
|
|
305
|
+
// -----------------------------------------------------------------------
|
|
306
|
+
|
|
307
|
+
describe("search with plugins", () => {
|
|
308
|
+
test("searches plugin resource catalogs", async () => {
|
|
309
|
+
const plugin = createMockPlugin({
|
|
310
|
+
name: "test-lex",
|
|
311
|
+
mcpResources: () => [
|
|
312
|
+
{
|
|
313
|
+
uri: "resource-catalog",
|
|
314
|
+
name: "Test Catalog",
|
|
315
|
+
description: "Test resource catalog",
|
|
316
|
+
mimeType: "application/json",
|
|
317
|
+
handler: async () => JSON.stringify([
|
|
318
|
+
{ className: "Bucket", resourceType: "AWS::S3::Bucket", kind: "resource" },
|
|
319
|
+
{ className: "Table", resourceType: "AWS::DynamoDB::Table", kind: "resource" },
|
|
320
|
+
]),
|
|
321
|
+
},
|
|
322
|
+
],
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
const s = new McpServer([plugin]);
|
|
326
|
+
const response = await s.handleRequest({
|
|
327
|
+
jsonrpc: "2.0",
|
|
328
|
+
id: 1,
|
|
329
|
+
method: "tools/call",
|
|
330
|
+
params: { name: "search", arguments: { query: "bucket" } },
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
const result = response.result as { content: Array<{ text: string }> };
|
|
334
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
335
|
+
expect(parsed.total).toBe(1);
|
|
336
|
+
expect(parsed.results[0].className).toBe("Bucket");
|
|
337
|
+
expect(parsed.results[0].lexicon).toBe("test-lex");
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
test("search respects limit parameter", async () => {
|
|
341
|
+
const entries = Array.from({ length: 30 }, (_, i) => ({
|
|
342
|
+
className: `Type${i}`,
|
|
343
|
+
resourceType: `NS::Type${i}`,
|
|
344
|
+
kind: "resource",
|
|
345
|
+
}));
|
|
346
|
+
const plugin = createMockPlugin({
|
|
347
|
+
name: "big",
|
|
348
|
+
mcpResources: () => [
|
|
349
|
+
{
|
|
350
|
+
uri: "resource-catalog",
|
|
351
|
+
name: "Big Catalog",
|
|
352
|
+
description: "Big catalog",
|
|
353
|
+
mimeType: "application/json",
|
|
354
|
+
handler: async () => JSON.stringify(entries),
|
|
355
|
+
},
|
|
356
|
+
],
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
const s = new McpServer([plugin]);
|
|
360
|
+
const response = await s.handleRequest({
|
|
361
|
+
jsonrpc: "2.0",
|
|
362
|
+
id: 1,
|
|
363
|
+
method: "tools/call",
|
|
364
|
+
params: { name: "search", arguments: { query: "type", limit: 5 } },
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
const parsed = JSON.parse((response.result as { content: Array<{ text: string }> }).content[0].text);
|
|
368
|
+
expect(parsed.total).toBe(30);
|
|
369
|
+
expect(parsed.results.length).toBe(5);
|
|
370
|
+
});
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
// -----------------------------------------------------------------------
|
|
374
|
+
// Scaffold tool with plugins
|
|
375
|
+
// -----------------------------------------------------------------------
|
|
376
|
+
|
|
377
|
+
describe("scaffold with plugins", () => {
|
|
378
|
+
test("matches plugin init templates", async () => {
|
|
379
|
+
const plugin = createMockPlugin({
|
|
380
|
+
name: "test-lex",
|
|
381
|
+
initTemplates: () => ({
|
|
382
|
+
"config.ts": "export const config = {};",
|
|
383
|
+
"data-bucket.ts": "export const dataBucket = {};",
|
|
384
|
+
}),
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
const s = new McpServer([plugin]);
|
|
388
|
+
const response = await s.handleRequest({
|
|
389
|
+
jsonrpc: "2.0",
|
|
390
|
+
id: 1,
|
|
391
|
+
method: "tools/call",
|
|
392
|
+
params: { name: "scaffold", arguments: { pattern: "bucket", lexicon: "test-lex" } },
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
const parsed = JSON.parse((response.result as { content: Array<{ text: string }> }).content[0].text);
|
|
396
|
+
expect(parsed.lexicon).toBe("test-lex");
|
|
397
|
+
expect(parsed.files.length).toBe(1);
|
|
398
|
+
expect(parsed.files[0].filename).toBe("data-bucket.ts");
|
|
399
|
+
});
|
|
197
400
|
});
|
|
198
401
|
|
|
199
402
|
// -----------------------------------------------------------------------
|
|
@@ -729,8 +932,8 @@ describe("McpServer", () => {
|
|
|
729
932
|
|
|
730
933
|
const toolsRes = await s.handleRequest({ jsonrpc: "2.0", id: 2, method: "tools/list" });
|
|
731
934
|
const tools = (toolsRes.result as { tools: Array<{ name: string }> }).tools;
|
|
732
|
-
expect(tools).toHaveLength(
|
|
733
|
-
expect(tools.map((t) => t.name).sort()).toEqual(["build", "import", "lint"]);
|
|
935
|
+
expect(tools).toHaveLength(6);
|
|
936
|
+
expect(tools.map((t) => t.name).sort()).toEqual(["build", "explain", "import", "lint", "scaffold", "search"]);
|
|
734
937
|
|
|
735
938
|
const resourcesRes = await s.handleRequest({ jsonrpc: "2.0", id: 3, method: "resources/list" });
|
|
736
939
|
const resources = (resourcesRes.result as { resources: Array<{ uri: string }> }).resources;
|
|
@@ -741,7 +944,7 @@ describe("McpServer", () => {
|
|
|
741
944
|
const s = new McpServer([]);
|
|
742
945
|
const toolsRes = await s.handleRequest({ jsonrpc: "2.0", id: 1, method: "tools/list" });
|
|
743
946
|
const tools = (toolsRes.result as { tools: Array<{ name: string }> }).tools;
|
|
744
|
-
expect(tools).toHaveLength(
|
|
947
|
+
expect(tools).toHaveLength(6);
|
|
745
948
|
});
|
|
746
949
|
});
|
|
747
950
|
});
|
package/src/cli/mcp/server.ts
CHANGED
|
@@ -3,6 +3,9 @@ import { resolve } from "node:path";
|
|
|
3
3
|
import { buildTool, handleBuild } from "./tools/build";
|
|
4
4
|
import { lintTool, handleLint } from "./tools/lint";
|
|
5
5
|
import { importTool, handleImport } from "./tools/import";
|
|
6
|
+
import { explainTool, handleExplain } from "./tools/explain";
|
|
7
|
+
import { scaffoldTool, createScaffoldHandler } from "./tools/scaffold";
|
|
8
|
+
import { searchTool, createSearchHandler } from "./tools/search";
|
|
6
9
|
import { getContext } from "./resources/context";
|
|
7
10
|
import type { LexiconPlugin } from "../../lexicon";
|
|
8
11
|
import type { McpToolContribution, McpResourceContribution } from "../../mcp/types";
|
|
@@ -70,6 +73,9 @@ export class McpServer {
|
|
|
70
73
|
this.registerTool(buildTool, handleBuild);
|
|
71
74
|
this.registerTool(lintTool, handleLint);
|
|
72
75
|
this.registerTool(importTool, handleImport);
|
|
76
|
+
this.registerTool(explainTool, handleExplain);
|
|
77
|
+
this.registerTool(scaffoldTool, createScaffoldHandler(plugins ?? []));
|
|
78
|
+
this.registerTool(searchTool, createSearchHandler(plugins ?? []));
|
|
73
79
|
|
|
74
80
|
// Register plugin contributions
|
|
75
81
|
if (plugins) {
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { resolve } from "path";
|
|
2
|
+
import { discover } from "../../../discovery/index";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Explain tool definition for MCP
|
|
6
|
+
*/
|
|
7
|
+
export const explainTool = {
|
|
8
|
+
name: "explain",
|
|
9
|
+
description: "Analyze a chant project directory and return a structured summary of all discovered entities",
|
|
10
|
+
inputSchema: {
|
|
11
|
+
type: "object" as const,
|
|
12
|
+
properties: {
|
|
13
|
+
path: {
|
|
14
|
+
type: "string",
|
|
15
|
+
description: "Path to the infrastructure directory to analyze",
|
|
16
|
+
},
|
|
17
|
+
format: {
|
|
18
|
+
type: "string",
|
|
19
|
+
enum: ["markdown", "json"],
|
|
20
|
+
description: "Output format (default: markdown)",
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
required: ["path"],
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Handle explain tool invocation
|
|
29
|
+
*/
|
|
30
|
+
export async function handleExplain(params: Record<string, unknown>): Promise<unknown> {
|
|
31
|
+
const path = params.path as string;
|
|
32
|
+
const format = (params.format as "markdown" | "json") ?? "markdown";
|
|
33
|
+
|
|
34
|
+
const infraPath = resolve(path);
|
|
35
|
+
const result = await discover(infraPath);
|
|
36
|
+
|
|
37
|
+
// Group entities by lexicon and kind
|
|
38
|
+
const byLexicon = new Map<string, { resources: string[]; properties: string[] }>();
|
|
39
|
+
|
|
40
|
+
for (const [name, entity] of result.entities) {
|
|
41
|
+
const lexicon = entity.lexicon ?? "unknown";
|
|
42
|
+
if (!byLexicon.has(lexicon)) {
|
|
43
|
+
byLexicon.set(lexicon, { resources: [], properties: [] });
|
|
44
|
+
}
|
|
45
|
+
const group = byLexicon.get(lexicon)!;
|
|
46
|
+
if (entity.kind === "property") {
|
|
47
|
+
group.properties.push(name);
|
|
48
|
+
} else {
|
|
49
|
+
group.resources.push(name);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Collect dependency info
|
|
54
|
+
const crossResourceDeps: Array<{ from: string; to: string }> = [];
|
|
55
|
+
for (const [from, deps] of result.dependencies) {
|
|
56
|
+
for (const to of deps) {
|
|
57
|
+
crossResourceDeps.push({ from, to });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const summary = {
|
|
62
|
+
sourceFiles: result.sourceFiles,
|
|
63
|
+
totalEntities: result.entities.size,
|
|
64
|
+
lexicons: Object.fromEntries(
|
|
65
|
+
Array.from(byLexicon.entries()).map(([lexicon, group]) => [
|
|
66
|
+
lexicon,
|
|
67
|
+
{
|
|
68
|
+
resourceCount: group.resources.length,
|
|
69
|
+
propertyCount: group.properties.length,
|
|
70
|
+
resources: group.resources,
|
|
71
|
+
properties: group.properties,
|
|
72
|
+
},
|
|
73
|
+
]),
|
|
74
|
+
),
|
|
75
|
+
dependencies: crossResourceDeps,
|
|
76
|
+
errors: result.errors.map((e) => e.message),
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
if (format === "json") {
|
|
80
|
+
return summary;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Markdown format
|
|
84
|
+
const lines: string[] = [];
|
|
85
|
+
lines.push(`# Project Summary`);
|
|
86
|
+
lines.push("");
|
|
87
|
+
lines.push(`- **Source files:** ${result.sourceFiles.length}`);
|
|
88
|
+
lines.push(`- **Total entities:** ${result.entities.size}`);
|
|
89
|
+
lines.push("");
|
|
90
|
+
|
|
91
|
+
for (const [lexicon, group] of byLexicon) {
|
|
92
|
+
lines.push(`## Lexicon: ${lexicon}`);
|
|
93
|
+
lines.push("");
|
|
94
|
+
lines.push(`- Resources: ${group.resources.length}`);
|
|
95
|
+
lines.push(`- Properties: ${group.properties.length}`);
|
|
96
|
+
lines.push("");
|
|
97
|
+
|
|
98
|
+
if (group.resources.length > 0) {
|
|
99
|
+
lines.push("### Resources");
|
|
100
|
+
for (const name of group.resources) {
|
|
101
|
+
lines.push(`- \`${name}\``);
|
|
102
|
+
}
|
|
103
|
+
lines.push("");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (group.properties.length > 0) {
|
|
107
|
+
lines.push("### Properties");
|
|
108
|
+
for (const name of group.properties) {
|
|
109
|
+
lines.push(`- \`${name}\``);
|
|
110
|
+
}
|
|
111
|
+
lines.push("");
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (crossResourceDeps.length > 0) {
|
|
116
|
+
lines.push("## Dependencies");
|
|
117
|
+
lines.push("");
|
|
118
|
+
for (const dep of crossResourceDeps) {
|
|
119
|
+
lines.push(`- \`${dep.from}\` → \`${dep.to}\``);
|
|
120
|
+
}
|
|
121
|
+
lines.push("");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (result.errors.length > 0) {
|
|
125
|
+
lines.push("## Errors");
|
|
126
|
+
lines.push("");
|
|
127
|
+
for (const err of result.errors) {
|
|
128
|
+
lines.push(`- ${err.message}`);
|
|
129
|
+
}
|
|
130
|
+
lines.push("");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return lines.join("\n");
|
|
134
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { LexiconPlugin } from "../../../lexicon";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Scaffold tool definition for MCP
|
|
5
|
+
*/
|
|
6
|
+
export const scaffoldTool = {
|
|
7
|
+
name: "scaffold",
|
|
8
|
+
description: "Generate starter code for a common infrastructure pattern",
|
|
9
|
+
inputSchema: {
|
|
10
|
+
type: "object" as const,
|
|
11
|
+
properties: {
|
|
12
|
+
pattern: {
|
|
13
|
+
type: "string",
|
|
14
|
+
description: "Infrastructure pattern to scaffold (e.g. 's3-bucket', 'lambda', 'pipeline')",
|
|
15
|
+
},
|
|
16
|
+
lexicon: {
|
|
17
|
+
type: "string",
|
|
18
|
+
description: "Lexicon to use for scaffolding (e.g. 'aws', 'gitlab'). Auto-detected if omitted.",
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
required: ["pattern"],
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Create a scaffold handler with access to loaded plugins
|
|
27
|
+
*/
|
|
28
|
+
export function createScaffoldHandler(
|
|
29
|
+
plugins: LexiconPlugin[],
|
|
30
|
+
): (params: Record<string, unknown>) => Promise<unknown> {
|
|
31
|
+
return async (params) => {
|
|
32
|
+
const pattern = params.pattern as string;
|
|
33
|
+
const lexiconName = params.lexicon as string | undefined;
|
|
34
|
+
|
|
35
|
+
// Try to find a matching plugin
|
|
36
|
+
const candidates = lexiconName
|
|
37
|
+
? plugins.filter((p) => p.name === lexiconName)
|
|
38
|
+
: plugins;
|
|
39
|
+
|
|
40
|
+
// Search plugin init templates for a pattern match
|
|
41
|
+
for (const plugin of candidates) {
|
|
42
|
+
const templates = plugin.initTemplates?.();
|
|
43
|
+
if (!templates) continue;
|
|
44
|
+
|
|
45
|
+
// Match template filenames against the pattern (case-insensitive substring)
|
|
46
|
+
const lowerPattern = pattern.toLowerCase();
|
|
47
|
+
const matched: Array<{ filename: string; content: string }> = [];
|
|
48
|
+
|
|
49
|
+
for (const [filename, content] of Object.entries(templates)) {
|
|
50
|
+
if (filename.toLowerCase().includes(lowerPattern)) {
|
|
51
|
+
matched.push({ filename, content });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (matched.length > 0) {
|
|
56
|
+
return {
|
|
57
|
+
lexicon: plugin.name,
|
|
58
|
+
pattern,
|
|
59
|
+
files: matched,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Fall back to a generic skeleton
|
|
65
|
+
const configContent = `/**
|
|
66
|
+
* Shared configuration for ${pattern}
|
|
67
|
+
*/
|
|
68
|
+
|
|
69
|
+
// TODO: Import resource types from your lexicon
|
|
70
|
+
// import { ... } from "@intentius/chant-lexicon-<name>";
|
|
71
|
+
|
|
72
|
+
export const config = {
|
|
73
|
+
// Add shared configuration here
|
|
74
|
+
};
|
|
75
|
+
`;
|
|
76
|
+
|
|
77
|
+
const resourceContent = `/**
|
|
78
|
+
* ${pattern} resource definition
|
|
79
|
+
*/
|
|
80
|
+
|
|
81
|
+
// TODO: Import resource types from your lexicon
|
|
82
|
+
// import { ... } from "@intentius/chant-lexicon-<name>";
|
|
83
|
+
// import { config } from "./config";
|
|
84
|
+
|
|
85
|
+
// export const ${toCamelCase(pattern)} = new ResourceType({
|
|
86
|
+
// // Add properties here
|
|
87
|
+
// });
|
|
88
|
+
`;
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
lexicon: lexiconName ?? null,
|
|
92
|
+
pattern,
|
|
93
|
+
files: [
|
|
94
|
+
{ filename: "config.ts", content: configContent },
|
|
95
|
+
{ filename: `${pattern}.ts`, content: resourceContent },
|
|
96
|
+
],
|
|
97
|
+
note: "No lexicon-specific template found. Generic skeleton provided — fill in imports and resource types.",
|
|
98
|
+
};
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function toCamelCase(s: string): string {
|
|
103
|
+
return s
|
|
104
|
+
.split(/[-_]/)
|
|
105
|
+
.map((part, i) => (i === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1)))
|
|
106
|
+
.join("");
|
|
107
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { LexiconPlugin } from "../../../lexicon";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Search tool definition for MCP
|
|
5
|
+
*/
|
|
6
|
+
export const searchTool = {
|
|
7
|
+
name: "search",
|
|
8
|
+
description: "Search the resource catalog across loaded lexicons by keyword",
|
|
9
|
+
inputSchema: {
|
|
10
|
+
type: "object" as const,
|
|
11
|
+
properties: {
|
|
12
|
+
query: {
|
|
13
|
+
type: "string",
|
|
14
|
+
description: "Search query — matches against resource type, class name, and kind",
|
|
15
|
+
},
|
|
16
|
+
lexicon: {
|
|
17
|
+
type: "string",
|
|
18
|
+
description: "Filter results to a specific lexicon (e.g. 'aws', 'gitlab')",
|
|
19
|
+
},
|
|
20
|
+
limit: {
|
|
21
|
+
type: "number",
|
|
22
|
+
description: "Maximum number of results to return (default: 20)",
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
required: ["query"],
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
interface CatalogEntry {
|
|
30
|
+
className: string;
|
|
31
|
+
resourceType: string;
|
|
32
|
+
kind?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Create a search handler with access to loaded plugins
|
|
37
|
+
*/
|
|
38
|
+
export function createSearchHandler(
|
|
39
|
+
plugins: LexiconPlugin[],
|
|
40
|
+
): (params: Record<string, unknown>) => Promise<unknown> {
|
|
41
|
+
return async (params) => {
|
|
42
|
+
const query = params.query as string;
|
|
43
|
+
const lexiconFilter = params.lexicon as string | undefined;
|
|
44
|
+
const limit = (params.limit as number) ?? 20;
|
|
45
|
+
|
|
46
|
+
const lowerQuery = query.toLowerCase();
|
|
47
|
+
const results: Array<CatalogEntry & { lexicon: string; score: number }> = [];
|
|
48
|
+
|
|
49
|
+
const candidates = lexiconFilter
|
|
50
|
+
? plugins.filter((p) => p.name === lexiconFilter)
|
|
51
|
+
: plugins;
|
|
52
|
+
|
|
53
|
+
for (const plugin of candidates) {
|
|
54
|
+
const resources = plugin.mcpResources?.() ?? [];
|
|
55
|
+
const catalog = resources.find((r) => r.uri === "resource-catalog");
|
|
56
|
+
if (!catalog) continue;
|
|
57
|
+
|
|
58
|
+
let entries: CatalogEntry[];
|
|
59
|
+
try {
|
|
60
|
+
const raw = await catalog.handler();
|
|
61
|
+
entries = JSON.parse(raw);
|
|
62
|
+
} catch {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
for (const entry of entries) {
|
|
67
|
+
const fields = [
|
|
68
|
+
entry.resourceType?.toLowerCase() ?? "",
|
|
69
|
+
entry.className?.toLowerCase() ?? "",
|
|
70
|
+
entry.kind?.toLowerCase() ?? "",
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
const match = fields.some((f) => f.includes(lowerQuery));
|
|
74
|
+
if (!match) continue;
|
|
75
|
+
|
|
76
|
+
// Score: prefix match on resourceType or className ranks higher
|
|
77
|
+
const isPrefix = fields.some((f) => f.startsWith(lowerQuery));
|
|
78
|
+
const score = isPrefix ? 1 : 0;
|
|
79
|
+
|
|
80
|
+
results.push({ ...entry, lexicon: plugin.name, score });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Sort: prefix matches first, then alphabetical by resourceType
|
|
85
|
+
results.sort((a, b) => {
|
|
86
|
+
if (a.score !== b.score) return b.score - a.score;
|
|
87
|
+
return (a.resourceType ?? "").localeCompare(b.resourceType ?? "");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const limited = results.slice(0, limit);
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
query,
|
|
94
|
+
total: results.length,
|
|
95
|
+
results: limited.map(({ score: _score, ...entry }) => entry),
|
|
96
|
+
};
|
|
97
|
+
};
|
|
98
|
+
}
|
|
@@ -12,7 +12,7 @@ describe("expandFileMarkers", () => {
|
|
|
12
12
|
mkdirSync(join(dir, "sub"), { recursive: true });
|
|
13
13
|
writeFileSync(
|
|
14
14
|
join(dir, "example.ts"),
|
|
15
|
-
'import
|
|
15
|
+
'import { Bucket } from "@intentius/chant-lexicon-aws";\n\nexport const bucket = new Bucket({\n bucketName: "test",\n});\n',
|
|
16
16
|
);
|
|
17
17
|
writeFileSync(
|
|
18
18
|
join(dir, "sub", "nested.ts"),
|
|
@@ -27,7 +27,7 @@ describe("expandFileMarkers", () => {
|
|
|
27
27
|
test("expands full file marker", () => {
|
|
28
28
|
const result = expandFileMarkers("Before\n\n{{file:example.ts}}\n\nAfter", dir);
|
|
29
29
|
expect(result).toContain('```typescript title="example.ts"');
|
|
30
|
-
expect(result).toContain('import
|
|
30
|
+
expect(result).toContain('import { Bucket } from "@intentius/chant-lexicon-aws";');
|
|
31
31
|
expect(result).toContain("```");
|
|
32
32
|
expect(result).toStartWith("Before\n\n");
|
|
33
33
|
expect(result).toEndWith("\n\nAfter");
|
package/src/codegen/docs.ts
CHANGED
|
@@ -190,10 +190,11 @@ export function docsPipeline(config: DocsConfig): DocsResult {
|
|
|
190
190
|
// Generate pages
|
|
191
191
|
const pages = new Map<string, string>();
|
|
192
192
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
193
|
+
let overviewContent = generateOverview(config, manifest, resources, properties, serviceGroups, rules);
|
|
194
|
+
if (config.examplesDir) {
|
|
195
|
+
overviewContent = expandFileMarkers(overviewContent, config.examplesDir);
|
|
196
|
+
}
|
|
197
|
+
pages.set("index.mdx", overviewContent);
|
|
197
198
|
const suppress = new Set(config.suppressPages ?? []);
|
|
198
199
|
|
|
199
200
|
// Extra pages from lexicon config
|
|
@@ -51,7 +51,7 @@ describe("buildRegistry", () => {
|
|
|
51
51
|
const entries = buildRegistry(resources, naming, testConfig);
|
|
52
52
|
|
|
53
53
|
expect(entries["Bucket"]).toBeDefined();
|
|
54
|
-
expect(entries["Bucket"].attrs).toEqual({
|
|
54
|
+
expect(entries["Bucket"].attrs).toEqual({ Arn: "Arn", DomainName: "DomainName" });
|
|
55
55
|
});
|
|
56
56
|
|
|
57
57
|
test("omits attrs when empty", () => {
|
|
@@ -69,7 +69,7 @@ describe("buildRegistry", () => {
|
|
|
69
69
|
typeName: "Test::S3::Bucket",
|
|
70
70
|
attributes: [],
|
|
71
71
|
properties: [],
|
|
72
|
-
propertyTypes: [{ name: "Bucket_Versioning",
|
|
72
|
+
propertyTypes: [{ name: "Bucket_Versioning", specType: "Versioning" }],
|
|
73
73
|
},
|
|
74
74
|
];
|
|
75
75
|
const naming = makeNaming(resources);
|