@intentius/chant 0.0.5 → 0.0.9

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 (91) hide show
  1. package/bin/chant +20 -0
  2. package/package.json +18 -17
  3. package/src/bench.test.ts +1 -1
  4. package/src/cli/commands/__fixtures__/init-lexicon-output/src/plugin.ts +0 -25
  5. package/src/cli/commands/__snapshots__/init-lexicon.test.ts.snap +0 -25
  6. package/src/cli/commands/build.ts +1 -2
  7. package/src/cli/commands/doctor.ts +8 -3
  8. package/src/cli/commands/import.ts +2 -2
  9. package/src/cli/commands/init-lexicon.test.ts +0 -3
  10. package/src/cli/commands/init-lexicon.ts +1 -79
  11. package/src/cli/commands/init.test.ts +44 -4
  12. package/src/cli/commands/init.ts +69 -26
  13. package/src/cli/commands/lint.ts +27 -13
  14. package/src/cli/commands/list.ts +2 -2
  15. package/src/cli/commands/update.ts +5 -3
  16. package/src/cli/conflict-check.test.ts +0 -1
  17. package/src/cli/handlers/dev.ts +1 -9
  18. package/src/cli/handlers/init.ts +1 -0
  19. package/src/cli/lsp/server.ts +1 -1
  20. package/src/cli/main.ts +17 -3
  21. package/src/cli/mcp/server.test.ts +233 -4
  22. package/src/cli/mcp/server.ts +6 -0
  23. package/src/cli/mcp/tools/explain.ts +134 -0
  24. package/src/cli/mcp/tools/scaffold.ts +125 -0
  25. package/src/cli/mcp/tools/search.ts +98 -0
  26. package/src/cli/registry.ts +1 -0
  27. package/src/cli/reporters/stylish.test.ts +212 -1
  28. package/src/cli/reporters/stylish.ts +133 -36
  29. package/src/codegen/docs-rules.test.ts +112 -0
  30. package/src/codegen/docs-rules.ts +129 -0
  31. package/src/codegen/docs.ts +3 -1
  32. package/src/codegen/generate-registry.test.ts +1 -1
  33. package/src/codegen/generate-registry.ts +2 -3
  34. package/src/codegen/generate-typescript.test.ts +70 -6
  35. package/src/codegen/generate-typescript.ts +15 -9
  36. package/src/codegen/generate.ts +1 -12
  37. package/src/codegen/package.ts +1 -1
  38. package/src/codegen/typecheck.ts +6 -11
  39. package/src/composite.test.ts +83 -16
  40. package/src/composite.ts +7 -5
  41. package/src/config.ts +4 -0
  42. package/src/detectLexicon.test.ts +2 -2
  43. package/src/discovery/collect.test.ts +2 -2
  44. package/src/discovery/collect.ts +1 -1
  45. package/src/index.ts +2 -1
  46. package/src/lexicon-integrity.ts +5 -4
  47. package/src/lexicon-schema.ts +8 -0
  48. package/src/lexicon.ts +15 -7
  49. package/src/lint/config.ts +8 -6
  50. package/src/lint/declarative.ts +6 -0
  51. package/src/lint/engine.test.ts +287 -11
  52. package/src/lint/engine.ts +101 -23
  53. package/src/lint/rule-registry.test.ts +112 -0
  54. package/src/lint/rule-registry.ts +118 -0
  55. package/src/lint/rule.ts +8 -0
  56. package/src/lint/rules/cor017-composite-name-match.ts +2 -1
  57. package/src/lint/rules/cor018-composite-prefer-lexicon-type.ts +4 -3
  58. package/src/lint/rules/declarable-naming-convention.ts +1 -0
  59. package/src/lint/rules/evl001-non-literal-expression.ts +1 -0
  60. package/src/lint/rules/evl002-control-flow-resource.ts +1 -0
  61. package/src/lint/rules/evl003-dynamic-property-access.ts +1 -0
  62. package/src/lint/rules/evl004-spread-non-const.ts +1 -0
  63. package/src/lint/rules/evl005-resource-block-body.ts +1 -0
  64. package/src/lint/rules/evl007-invalid-siblings.ts +1 -0
  65. package/src/lint/rules/evl009-composite-no-constant.ts +1 -0
  66. package/src/lint/rules/evl010-composite-no-transform.ts +1 -0
  67. package/src/lint/rules/export-required.ts +1 -0
  68. package/src/lint/rules/file-declarable-limit.ts +1 -0
  69. package/src/lint/rules/flat-declarations.test.ts +8 -7
  70. package/src/lint/rules/flat-declarations.ts +2 -3
  71. package/src/lint/rules/no-cyclic-declarable-ref.ts +1 -0
  72. package/src/lint/rules/no-redundant-type-import.ts +1 -0
  73. package/src/lint/rules/no-redundant-value-cast.ts +1 -0
  74. package/src/lint/rules/no-string-ref.ts +1 -0
  75. package/src/lint/rules/no-unused-declarable-import.ts +1 -0
  76. package/src/lint/rules/no-unused-declarable.test.ts +8 -0
  77. package/src/lint/rules/no-unused-declarable.ts +4 -0
  78. package/src/lint/rules/single-concern-file.ts +1 -0
  79. package/src/lsp/lexicon-providers.ts +7 -0
  80. package/src/lsp/types.ts +1 -0
  81. package/src/resource-attributes.test.ts +79 -0
  82. package/src/resource-attributes.ts +42 -0
  83. package/src/runtime-adapter.ts +158 -0
  84. package/src/runtime.ts +4 -3
  85. package/src/serializer-walker.test.ts +0 -9
  86. package/src/serializer-walker.ts +1 -3
  87. package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/rollback.ts +0 -45
  88. package/src/codegen/case.test.ts +0 -30
  89. package/src/codegen/case.ts +0 -11
  90. package/src/codegen/rollback.test.ts +0 -92
  91. package/src/codegen/rollback.ts +0 -115
@@ -1,5 +1,6 @@
1
- import { existsSync, mkdirSync, writeFileSync, cpSync, readdirSync, statSync } from "fs";
1
+ import { existsSync, mkdirSync, writeFileSync, cpSync, readdirSync, statSync, readFileSync } from "fs";
2
2
  import { join, resolve } from "path";
3
+ import { createRequire } from "module";
3
4
  import { formatSuccess, formatWarning, formatError } from "../format";
4
5
  import { loadChantConfig } from "../../config";
5
6
  import { loadPlugins } from "../plugins";
@@ -63,13 +64,14 @@ function copyTypeFiles(src: string, dest: string): number {
63
64
  function resolvePackagePath(packageName: string, projectDir: string): string | undefined {
64
65
  // Try resolve from project dir
65
66
  try {
66
- const entryPoint = require.resolve(packageName, { paths: [projectDir] });
67
+ const _require = createRequire(join(projectDir, "package.json"));
68
+ const entryPoint = _require.resolve(packageName);
67
69
  // Walk up from entry point to find package root
68
70
  let dir = entryPoint;
69
71
  while (dir !== "/") {
70
72
  dir = join(dir, "..");
71
73
  if (existsSync(join(dir, "package.json"))) {
72
- const pkg = JSON.parse(require("fs").readFileSync(join(dir, "package.json"), "utf-8"));
74
+ const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf-8"));
73
75
  if (pkg.name === packageName) return dir;
74
76
  }
75
77
  }
@@ -22,7 +22,6 @@ function makePlugin(
22
22
  validate: noopAsync,
23
23
  coverage: noopAsync,
24
24
  package: noopAsync,
25
- rollback: noopAsync,
26
25
  };
27
26
 
28
27
  if (opts.rules) {
@@ -21,18 +21,10 @@ export async function runDevPublish(ctx: CommandContext): Promise<number> {
21
21
  return 0;
22
22
  }
23
23
 
24
- export async function runDevRollback(ctx: CommandContext): Promise<number> {
25
- for (const plugin of ctx.plugins) {
26
- await plugin.rollback({ verbose: ctx.args.verbose });
27
- console.error(formatSuccess(`${plugin.name}: rollback complete`));
28
- }
29
- return 0;
30
- }
31
-
32
24
  export async function runDevUnknown(ctx: CommandContext): Promise<number> {
33
25
  console.error(formatError({
34
26
  message: `Unknown dev subcommand: ${ctx.args.path}`,
35
- hint: "Available: chant dev generate, chant dev publish, chant dev rollback",
27
+ hint: "Available: chant dev generate, chant dev publish",
36
28
  }));
37
29
  return 1;
38
30
  }
@@ -16,6 +16,7 @@ export async function runInit(ctx: CommandContext): Promise<number> {
16
16
  const result = await initCommand({
17
17
  path: args.path === "." ? undefined : args.path,
18
18
  lexicon: args.lexicon,
19
+ template: args.template,
19
20
  force: args.force,
20
21
  skipInstall: true,
21
22
  });
@@ -362,7 +362,7 @@ export class LspServer {
362
362
 
363
363
  if (rules.length === 0) return [];
364
364
 
365
- const diagnostics = await runLint([filePath], rules);
365
+ const { diagnostics } = await runLint([filePath], rules);
366
366
  return toLspDiagnostics(diagnostics);
367
367
  } catch {
368
368
  return [];
package/src/cli/main.ts CHANGED
@@ -4,9 +4,11 @@ import { resolve } from "node:path";
4
4
  import { formatSuccess, formatError } from "./format";
5
5
  import { loadPlugins, resolveProjectLexicons } from "./plugins";
6
6
  import { resolveCommand, type CommandDef, type ParsedArgs } from "./registry";
7
+ import { loadChantConfig } from "../config";
8
+ import { initRuntime } from "../runtime-adapter";
7
9
  import { runBuild } from "./handlers/build";
8
10
  import { runLint } from "./handlers/lint";
9
- import { runDevGenerate, runDevPublish, runDevRollback, runDevUnknown } from "./handlers/dev";
11
+ import { runDevGenerate, runDevPublish, runDevUnknown } from "./handlers/dev";
10
12
  import { runServeLsp, runServeMcp, runServeUnknown } from "./handlers/serve";
11
13
  import { runInit, runInitLexicon } from "./handlers/init";
12
14
  import { runList, runImport, runUpdate, runDoctor } from "./handlers/misc";
@@ -25,6 +27,7 @@ export function parseArgs(args: string[]): ParsedArgs {
25
27
  force: undefined,
26
28
  fix: false,
27
29
  lexicon: undefined,
30
+ template: undefined,
28
31
  watch: false,
29
32
  verbose: false,
30
33
  help: false,
@@ -42,6 +45,8 @@ export function parseArgs(args: string[]): ParsedArgs {
42
45
  result.format = args[++i];
43
46
  } else if (arg === "--lexicon" || arg === "-d") {
44
47
  result.lexicon = args[++i];
48
+ } else if (arg === "--template" || arg === "-t") {
49
+ result.template = args[++i];
45
50
  } else if (arg === "--force") {
46
51
  result.force = true;
47
52
  } else if (arg === "--fix") {
@@ -89,7 +94,6 @@ Commands:
89
94
  Lexicon development:
90
95
  dev generate Generate lexicon artifacts (+ validate + coverage)
91
96
  dev publish Package lexicon for distribution
92
- dev rollback List or restore generation snapshots
93
97
 
94
98
  Servers:
95
99
  serve lsp Start the LSP server (stdio)
@@ -106,6 +110,7 @@ Options:
106
110
  - list: text (default) or json
107
111
  - lint: stylish (default), json, or sarif
108
112
  -d, --lexicon <name> Build only the specified lexicon (e.g. aws, gitlab)
113
+ -t, --template <name> Init template (e.g. node-pipeline, docker-build)
109
114
  --fix Auto-fix fixable issues (lint command)
110
115
  --force Force overwrite existing files (import command)
111
116
  -w, --watch Watch for changes and rebuild/re-lint (build, lint)
@@ -167,7 +172,6 @@ const registry: CommandDef[] = [
167
172
  // Dev subcommands
168
173
  { name: "dev generate", requiresPlugins: true, handler: runDevGenerate },
169
174
  { name: "dev publish", requiresPlugins: true, handler: runDevPublish },
170
- { name: "dev rollback", requiresPlugins: true, handler: runDevRollback },
171
175
 
172
176
  // Serve subcommands
173
177
  { name: "serve lsp", requiresPlugins: true, handler: runServeLsp },
@@ -189,6 +193,16 @@ async function main(): Promise<void> {
189
193
  process.exit(args.help ? 0 : 1);
190
194
  }
191
195
 
196
+ // Initialize runtime adapter early — before plugins or commands run
197
+ const projectPath0 = resolve(args.path === "." ? "." : args.path);
198
+ try {
199
+ const { config } = await loadChantConfig(projectPath0);
200
+ initRuntime(config.runtime);
201
+ } catch {
202
+ // Config may not exist yet (e.g. `chant init`); auto-detect runtime
203
+ initRuntime();
204
+ }
205
+
192
206
  const match = resolveCommand(args, registry);
193
207
  if (!match) {
194
208
  console.error(formatError({
@@ -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,192 @@ 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: () => ({ src: {
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
+ });
400
+
401
+ test("passes template name to initTemplates", async () => {
402
+ const plugin = createMockPlugin({
403
+ name: "test-lex",
404
+ initTemplates: (template?: string) => {
405
+ if (template === "special") {
406
+ return { src: { "special.ts": "export const special = {};" } };
407
+ }
408
+ return { src: { "default.ts": "export const def = {};" } };
409
+ },
410
+ });
411
+
412
+ const s = new McpServer([plugin]);
413
+ const response = await s.handleRequest({
414
+ jsonrpc: "2.0",
415
+ id: 1,
416
+ method: "tools/call",
417
+ params: { name: "scaffold", arguments: { pattern: "special", lexicon: "test-lex", template: "special" } },
418
+ });
419
+
420
+ const parsed = JSON.parse((response.result as { content: Array<{ text: string }> }).content[0].text);
421
+ expect(parsed.lexicon).toBe("test-lex");
422
+ expect(parsed.template).toBe("special");
423
+ expect(parsed.files.length).toBe(1);
424
+ expect(parsed.files[0].filename).toBe("special.ts");
425
+ });
197
426
  });
198
427
 
199
428
  // -----------------------------------------------------------------------
@@ -729,8 +958,8 @@ describe("McpServer", () => {
729
958
 
730
959
  const toolsRes = await s.handleRequest({ jsonrpc: "2.0", id: 2, method: "tools/list" });
731
960
  const tools = (toolsRes.result as { tools: Array<{ name: string }> }).tools;
732
- expect(tools).toHaveLength(3);
733
- expect(tools.map((t) => t.name).sort()).toEqual(["build", "import", "lint"]);
961
+ expect(tools).toHaveLength(6);
962
+ expect(tools.map((t) => t.name).sort()).toEqual(["build", "explain", "import", "lint", "scaffold", "search"]);
734
963
 
735
964
  const resourcesRes = await s.handleRequest({ jsonrpc: "2.0", id: 3, method: "resources/list" });
736
965
  const resources = (resourcesRes.result as { resources: Array<{ uri: string }> }).resources;
@@ -741,7 +970,7 @@ describe("McpServer", () => {
741
970
  const s = new McpServer([]);
742
971
  const toolsRes = await s.handleRequest({ jsonrpc: "2.0", id: 1, method: "tools/list" });
743
972
  const tools = (toolsRes.result as { tools: Array<{ name: string }> }).tools;
744
- expect(tools).toHaveLength(3);
973
+ expect(tools).toHaveLength(6);
745
974
  });
746
975
  });
747
976
  });
@@ -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
+ }