@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.
- package/bin/chant +20 -0
- package/package.json +18 -17
- package/src/bench.test.ts +1 -1
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/plugin.ts +0 -25
- package/src/cli/commands/__snapshots__/init-lexicon.test.ts.snap +0 -25
- package/src/cli/commands/build.ts +1 -2
- package/src/cli/commands/doctor.ts +8 -3
- 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 +1 -79
- package/src/cli/commands/init.test.ts +44 -4
- package/src/cli/commands/init.ts +69 -26
- package/src/cli/commands/lint.ts +27 -13
- 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/handlers/init.ts +1 -0
- package/src/cli/lsp/server.ts +1 -1
- package/src/cli/main.ts +17 -3
- package/src/cli/mcp/server.test.ts +233 -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 +125 -0
- package/src/cli/mcp/tools/search.ts +98 -0
- package/src/cli/registry.ts +1 -0
- package/src/cli/reporters/stylish.test.ts +212 -1
- package/src/cli/reporters/stylish.ts +133 -36
- package/src/codegen/docs-rules.test.ts +112 -0
- package/src/codegen/docs-rules.ts +129 -0
- package/src/codegen/docs.ts +3 -1
- package/src/codegen/generate-registry.test.ts +1 -1
- package/src/codegen/generate-registry.ts +2 -3
- package/src/codegen/generate-typescript.test.ts +70 -6
- package/src/codegen/generate-typescript.ts +15 -9
- package/src/codegen/generate.ts +1 -12
- package/src/codegen/package.ts +1 -1
- package/src/codegen/typecheck.ts +6 -11
- package/src/composite.test.ts +83 -16
- package/src/composite.ts +7 -5
- package/src/config.ts +4 -0
- package/src/detectLexicon.test.ts +2 -2
- package/src/discovery/collect.test.ts +2 -2
- package/src/discovery/collect.ts +1 -1
- package/src/index.ts +2 -1
- package/src/lexicon-integrity.ts +5 -4
- package/src/lexicon-schema.ts +8 -0
- package/src/lexicon.ts +15 -7
- package/src/lint/config.ts +8 -6
- package/src/lint/declarative.ts +6 -0
- package/src/lint/engine.test.ts +287 -11
- package/src/lint/engine.ts +101 -23
- package/src/lint/rule-registry.test.ts +112 -0
- package/src/lint/rule-registry.ts +118 -0
- package/src/lint/rule.ts +8 -0
- package/src/lint/rules/cor017-composite-name-match.ts +2 -1
- package/src/lint/rules/cor018-composite-prefer-lexicon-type.ts +4 -3
- package/src/lint/rules/declarable-naming-convention.ts +1 -0
- package/src/lint/rules/evl001-non-literal-expression.ts +1 -0
- package/src/lint/rules/evl002-control-flow-resource.ts +1 -0
- package/src/lint/rules/evl003-dynamic-property-access.ts +1 -0
- package/src/lint/rules/evl004-spread-non-const.ts +1 -0
- package/src/lint/rules/evl005-resource-block-body.ts +1 -0
- package/src/lint/rules/evl007-invalid-siblings.ts +1 -0
- package/src/lint/rules/evl009-composite-no-constant.ts +1 -0
- package/src/lint/rules/evl010-composite-no-transform.ts +1 -0
- package/src/lint/rules/export-required.ts +1 -0
- package/src/lint/rules/file-declarable-limit.ts +1 -0
- package/src/lint/rules/flat-declarations.test.ts +8 -7
- package/src/lint/rules/flat-declarations.ts +2 -3
- package/src/lint/rules/no-cyclic-declarable-ref.ts +1 -0
- package/src/lint/rules/no-redundant-type-import.ts +1 -0
- package/src/lint/rules/no-redundant-value-cast.ts +1 -0
- package/src/lint/rules/no-string-ref.ts +1 -0
- package/src/lint/rules/no-unused-declarable-import.ts +1 -0
- package/src/lint/rules/no-unused-declarable.test.ts +8 -0
- package/src/lint/rules/no-unused-declarable.ts +4 -0
- package/src/lint/rules/single-concern-file.ts +1 -0
- package/src/lsp/lexicon-providers.ts +7 -0
- package/src/lsp/types.ts +1 -0
- package/src/resource-attributes.test.ts +79 -0
- package/src/resource-attributes.ts +42 -0
- package/src/runtime-adapter.ts +158 -0
- package/src/runtime.ts +4 -3
- package/src/serializer-walker.test.ts +0 -9
- package/src/serializer-walker.ts +1 -3
- 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
|
@@ -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
|
|
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(
|
|
74
|
+
const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf-8"));
|
|
73
75
|
if (pkg.name === packageName) return dir;
|
|
74
76
|
}
|
|
75
77
|
}
|
package/src/cli/handlers/dev.ts
CHANGED
|
@@ -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
|
|
27
|
+
hint: "Available: chant dev generate, chant dev publish",
|
|
36
28
|
}));
|
|
37
29
|
return 1;
|
|
38
30
|
}
|
package/src/cli/handlers/init.ts
CHANGED
|
@@ -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
|
});
|
package/src/cli/lsp/server.ts
CHANGED
|
@@ -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,
|
|
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(
|
|
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(
|
|
973
|
+
expect(tools).toHaveLength(6);
|
|
745
974
|
});
|
|
746
975
|
});
|
|
747
976
|
});
|
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
|
+
}
|