@intentius/chant 0.0.1
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 +365 -0
- package/package.json +22 -0
- package/src/attrref.test.ts +148 -0
- package/src/attrref.ts +50 -0
- package/src/barrel.test.ts +157 -0
- package/src/barrel.ts +101 -0
- package/src/bench.test.ts +227 -0
- package/src/build.test.ts +437 -0
- package/src/build.ts +425 -0
- package/src/builder.test.ts +312 -0
- package/src/builder.ts +56 -0
- package/src/child-project.ts +44 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/README.md +26 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/astro.config.mjs +14 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/package.json +16 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content/docs/index.mdx +8 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content.config.ts +7 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/tsconfig.json +10 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/examples/getting-started/.gitkeep +0 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/justfile +26 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/package.json +29 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/docs.ts +25 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/generate-cli.ts +8 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/generate.ts +74 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/naming.ts +33 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/package.ts +25 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/rollback.ts +45 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/coverage.ts +11 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/generated/.gitkeep +0 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/import/generator.ts +10 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/import/parser.ts +10 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/index.ts +9 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/lint/rules/index.ts +1 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/lint/rules/sample.ts +18 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/lsp/completions.ts +14 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/lsp/hover.ts +14 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/plugin.ts +110 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/serializer.ts +24 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/spec/fetch.ts +21 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/spec/parse.ts +25 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/validate-cli.ts +4 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/validate.ts +24 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/tsconfig.json +10 -0
- package/src/cli/commands/__fixtures__/sample-rule.ts +11 -0
- package/src/cli/commands/__snapshots__/init-lexicon.test.ts.snap +222 -0
- package/src/cli/commands/build.test.ts +149 -0
- package/src/cli/commands/build.ts +344 -0
- package/src/cli/commands/diff.test.ts +148 -0
- package/src/cli/commands/diff.ts +221 -0
- package/src/cli/commands/doctor.test.ts +239 -0
- package/src/cli/commands/doctor.ts +224 -0
- package/src/cli/commands/import.test.ts +379 -0
- package/src/cli/commands/import.ts +335 -0
- package/src/cli/commands/init-lexicon.test.ts +297 -0
- package/src/cli/commands/init-lexicon.ts +993 -0
- package/src/cli/commands/init.test.ts +317 -0
- package/src/cli/commands/init.ts +505 -0
- package/src/cli/commands/licenses.ts +165 -0
- package/src/cli/commands/lint.test.ts +332 -0
- package/src/cli/commands/lint.ts +408 -0
- package/src/cli/commands/list.test.ts +100 -0
- package/src/cli/commands/list.ts +108 -0
- package/src/cli/commands/update.test.ts +38 -0
- package/src/cli/commands/update.ts +207 -0
- package/src/cli/conflict-check.test.ts +255 -0
- package/src/cli/conflict-check.ts +89 -0
- package/src/cli/debug.ts +8 -0
- package/src/cli/format.test.ts +140 -0
- package/src/cli/format.ts +133 -0
- package/src/cli/handlers/build.ts +58 -0
- package/src/cli/handlers/dev.ts +38 -0
- package/src/cli/handlers/init.ts +46 -0
- package/src/cli/handlers/lint.ts +36 -0
- package/src/cli/handlers/misc.ts +57 -0
- package/src/cli/handlers/serve.ts +26 -0
- package/src/cli/index.ts +3 -0
- package/src/cli/lsp/capabilities.ts +46 -0
- package/src/cli/lsp/diagnostics.ts +52 -0
- package/src/cli/lsp/server.test.ts +618 -0
- package/src/cli/lsp/server.ts +393 -0
- package/src/cli/main.test.ts +257 -0
- package/src/cli/main.ts +224 -0
- package/src/cli/mcp/resources/context.ts +59 -0
- package/src/cli/mcp/server.test.ts +747 -0
- package/src/cli/mcp/server.ts +402 -0
- package/src/cli/mcp/tools/build.ts +117 -0
- package/src/cli/mcp/tools/import.ts +48 -0
- package/src/cli/mcp/tools/lint.ts +45 -0
- package/src/cli/plugins.test.ts +31 -0
- package/src/cli/plugins.ts +94 -0
- package/src/cli/registry.ts +73 -0
- package/src/cli/reporters/stylish.test.ts +282 -0
- package/src/cli/reporters/stylish.ts +186 -0
- package/src/cli/watch.test.ts +81 -0
- package/src/cli/watch.ts +101 -0
- package/src/codegen/case.test.ts +30 -0
- package/src/codegen/case.ts +11 -0
- package/src/codegen/coverage.ts +167 -0
- package/src/codegen/docs.ts +634 -0
- package/src/codegen/fetch.test.ts +119 -0
- package/src/codegen/fetch.ts +261 -0
- package/src/codegen/generate-registry.test.ts +118 -0
- package/src/codegen/generate-registry.ts +107 -0
- package/src/codegen/generate-runtime-index.test.ts +81 -0
- package/src/codegen/generate-runtime-index.ts +99 -0
- package/src/codegen/generate-typescript.test.ts +146 -0
- package/src/codegen/generate-typescript.ts +161 -0
- package/src/codegen/generate.ts +206 -0
- package/src/codegen/json-patch.test.ts +113 -0
- package/src/codegen/json-patch.ts +151 -0
- package/src/codegen/json-schema.test.ts +196 -0
- package/src/codegen/json-schema.ts +209 -0
- package/src/codegen/naming.ts +201 -0
- package/src/codegen/package.ts +161 -0
- package/src/codegen/rollback.test.ts +92 -0
- package/src/codegen/rollback.ts +115 -0
- package/src/codegen/topo-sort.test.ts +69 -0
- package/src/codegen/topo-sort.ts +46 -0
- package/src/codegen/typecheck.test.ts +37 -0
- package/src/codegen/typecheck.ts +74 -0
- package/src/codegen/validate.test.ts +86 -0
- package/src/codegen/validate.ts +143 -0
- package/src/composite.test.ts +426 -0
- package/src/composite.ts +243 -0
- package/src/config.test.ts +91 -0
- package/src/config.ts +87 -0
- package/src/declarable.test.ts +160 -0
- package/src/declarable.ts +47 -0
- package/src/detectLexicon.test.ts +236 -0
- package/src/detectLexicon.ts +37 -0
- package/src/discovery/cache.test.ts +78 -0
- package/src/discovery/cache.ts +86 -0
- package/src/discovery/collect.test.ts +269 -0
- package/src/discovery/collect.ts +51 -0
- package/src/discovery/cycles.test.ts +238 -0
- package/src/discovery/cycles.ts +107 -0
- package/src/discovery/files.test.ts +154 -0
- package/src/discovery/files.ts +61 -0
- package/src/discovery/graph.test.ts +476 -0
- package/src/discovery/graph.ts +150 -0
- package/src/discovery/import.test.ts +199 -0
- package/src/discovery/import.ts +20 -0
- package/src/discovery/index.test.ts +272 -0
- package/src/discovery/index.ts +132 -0
- package/src/discovery/resolve.test.ts +267 -0
- package/src/discovery/resolve.ts +54 -0
- package/src/errors.test.ts +138 -0
- package/src/errors.ts +86 -0
- package/src/import/base-parser.test.ts +67 -0
- package/src/import/base-parser.ts +48 -0
- package/src/import/generator.ts +21 -0
- package/src/import/ir-utils.test.ts +103 -0
- package/src/import/ir-utils.ts +87 -0
- package/src/import/parser.ts +41 -0
- package/src/index.ts +60 -0
- package/src/intrinsic-interpolation.test.ts +91 -0
- package/src/intrinsic-interpolation.ts +89 -0
- package/src/intrinsic.test.ts +69 -0
- package/src/intrinsic.ts +43 -0
- package/src/lexicon-integrity.test.ts +94 -0
- package/src/lexicon-integrity.ts +69 -0
- package/src/lexicon-manifest.test.ts +101 -0
- package/src/lexicon-manifest.ts +71 -0
- package/src/lexicon-output.test.ts +182 -0
- package/src/lexicon-output.ts +82 -0
- package/src/lexicon-schema.test.ts +239 -0
- package/src/lexicon-schema.ts +144 -0
- package/src/lexicon.ts +212 -0
- package/src/lint/config-overrides.test.ts +254 -0
- package/src/lint/config.test.ts +644 -0
- package/src/lint/config.ts +375 -0
- package/src/lint/declarative.test.ts +256 -0
- package/src/lint/declarative.ts +187 -0
- package/src/lint/engine.test.ts +465 -0
- package/src/lint/engine.ts +172 -0
- package/src/lint/named-checks.test.ts +37 -0
- package/src/lint/named-checks.ts +33 -0
- package/src/lint/parser.test.ts +129 -0
- package/src/lint/parser.ts +42 -0
- package/src/lint/post-synth.test.ts +113 -0
- package/src/lint/post-synth.ts +76 -0
- package/src/lint/presets/relaxed.json +19 -0
- package/src/lint/presets/strict.json +19 -0
- package/src/lint/rule-loader.test.ts +67 -0
- package/src/lint/rule-loader.ts +67 -0
- package/src/lint/rule-options.test.ts +141 -0
- package/src/lint/rule.test.ts +196 -0
- package/src/lint/rule.ts +98 -0
- package/src/lint/rules/barrel-import-style.test.ts +80 -0
- package/src/lint/rules/barrel-import-style.ts +59 -0
- package/src/lint/rules/composite-scope.ts +55 -0
- package/src/lint/rules/cor017-composite-name-match.test.ts +107 -0
- package/src/lint/rules/cor017-composite-name-match.ts +108 -0
- package/src/lint/rules/cor018-composite-prefer-lexicon-type.test.ts +172 -0
- package/src/lint/rules/cor018-composite-prefer-lexicon-type.ts +167 -0
- package/src/lint/rules/declarable-naming-convention.test.ts +69 -0
- package/src/lint/rules/declarable-naming-convention.ts +70 -0
- package/src/lint/rules/enforce-barrel-import.test.ts +169 -0
- package/src/lint/rules/enforce-barrel-import.ts +81 -0
- package/src/lint/rules/enforce-barrel-ref.test.ts +114 -0
- package/src/lint/rules/enforce-barrel-ref.ts +75 -0
- package/src/lint/rules/evl001-non-literal-expression.test.ts +158 -0
- package/src/lint/rules/evl001-non-literal-expression.ts +149 -0
- package/src/lint/rules/evl002-control-flow-resource.test.ts +110 -0
- package/src/lint/rules/evl002-control-flow-resource.ts +61 -0
- package/src/lint/rules/evl003-dynamic-property-access.test.ts +63 -0
- package/src/lint/rules/evl003-dynamic-property-access.ts +41 -0
- package/src/lint/rules/evl004-spread-non-const.test.ts +130 -0
- package/src/lint/rules/evl004-spread-non-const.ts +111 -0
- package/src/lint/rules/evl005-resource-block-body.test.ts +59 -0
- package/src/lint/rules/evl005-resource-block-body.ts +49 -0
- package/src/lint/rules/evl006-barrel-usage.test.ts +63 -0
- package/src/lint/rules/evl006-barrel-usage.ts +95 -0
- package/src/lint/rules/evl007-invalid-siblings.test.ts +87 -0
- package/src/lint/rules/evl007-invalid-siblings.ts +139 -0
- package/src/lint/rules/evl008-unresolvable-barrel-ref.test.ts +118 -0
- package/src/lint/rules/evl008-unresolvable-barrel-ref.ts +140 -0
- package/src/lint/rules/evl009-composite-no-constant.test.ts +162 -0
- package/src/lint/rules/evl009-composite-no-constant.ts +171 -0
- package/src/lint/rules/evl010-composite-no-transform.test.ts +121 -0
- package/src/lint/rules/evl010-composite-no-transform.ts +69 -0
- package/src/lint/rules/export-required.test.ts +213 -0
- package/src/lint/rules/export-required.ts +158 -0
- package/src/lint/rules/file-declarable-limit.test.ts +148 -0
- package/src/lint/rules/file-declarable-limit.ts +96 -0
- package/src/lint/rules/flat-declarations.test.ts +210 -0
- package/src/lint/rules/flat-declarations.ts +70 -0
- package/src/lint/rules/index.ts +99 -0
- package/src/lint/rules/no-cyclic-declarable-ref.test.ts +135 -0
- package/src/lint/rules/no-cyclic-declarable-ref.ts +178 -0
- package/src/lint/rules/no-redundant-type-import.test.ts +129 -0
- package/src/lint/rules/no-redundant-type-import.ts +85 -0
- package/src/lint/rules/no-redundant-value-cast.test.ts +51 -0
- package/src/lint/rules/no-redundant-value-cast.ts +46 -0
- package/src/lint/rules/no-string-ref.test.ts +100 -0
- package/src/lint/rules/no-string-ref.ts +66 -0
- package/src/lint/rules/no-unused-declarable-import.test.ts +74 -0
- package/src/lint/rules/no-unused-declarable-import.ts +103 -0
- package/src/lint/rules/no-unused-declarable.test.ts +134 -0
- package/src/lint/rules/no-unused-declarable.ts +118 -0
- package/src/lint/rules/prefer-namespace-import.test.ts +102 -0
- package/src/lint/rules/prefer-namespace-import.ts +63 -0
- package/src/lint/rules/single-concern-file.test.ts +156 -0
- package/src/lint/rules/single-concern-file.ts +98 -0
- package/src/lint/rules/stale-barrel-types.ts +60 -0
- package/src/lint/selectors.test.ts +113 -0
- package/src/lint/selectors.ts +188 -0
- package/src/lsp/lexicon-providers.ts +191 -0
- package/src/lsp/types.ts +79 -0
- package/src/mcp/types.ts +22 -0
- package/src/project/scan.test.ts +178 -0
- package/src/project/scan.ts +182 -0
- package/src/project/sync.test.ts +87 -0
- package/src/project/sync.ts +46 -0
- package/src/project-validation.test.ts +64 -0
- package/src/project-validation.ts +79 -0
- package/src/pseudo-parameter.test.ts +39 -0
- package/src/pseudo-parameter.ts +47 -0
- package/src/runtime.ts +68 -0
- package/src/serializer-walker.test.ts +124 -0
- package/src/serializer-walker.ts +83 -0
- package/src/serializer.ts +42 -0
- package/src/sort.test.ts +290 -0
- package/src/sort.ts +58 -0
- package/src/stack-output.ts +82 -0
- package/src/types.test.ts +307 -0
- package/src/types.ts +46 -0
- package/src/utils.test.ts +195 -0
- package/src/utils.ts +46 -0
- package/src/validation.test.ts +308 -0
- package/src/validation.ts +50 -0
|
@@ -0,0 +1,747 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, afterEach } from "bun:test";
|
|
2
|
+
import { McpServer } from "./server";
|
|
3
|
+
import { mkdir, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import type { LexiconPlugin } from "../../lexicon";
|
|
7
|
+
import type { Serializer } from "../../serializer";
|
|
8
|
+
|
|
9
|
+
function createMockPlugin(overrides?: Partial<LexiconPlugin>): LexiconPlugin {
|
|
10
|
+
return {
|
|
11
|
+
name: "mock",
|
|
12
|
+
serializer: { name: "mock", serialize: () => "" } as unknown as Serializer,
|
|
13
|
+
...overrides,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe("McpServer", () => {
|
|
18
|
+
let server: McpServer;
|
|
19
|
+
let testDir: string;
|
|
20
|
+
|
|
21
|
+
beforeEach(async () => {
|
|
22
|
+
server = new McpServer();
|
|
23
|
+
testDir = join(tmpdir(), `chant-mcp-test-${Date.now()}-${Math.random()}`);
|
|
24
|
+
await mkdir(testDir, { recursive: true });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
afterEach(async () => {
|
|
28
|
+
await rm(testDir, { recursive: true, force: true });
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// -----------------------------------------------------------------------
|
|
32
|
+
// Protocol basics
|
|
33
|
+
// -----------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
describe("initialize", () => {
|
|
36
|
+
test("returns server info and capabilities", async () => {
|
|
37
|
+
const response = await server.handleRequest({
|
|
38
|
+
jsonrpc: "2.0",
|
|
39
|
+
id: 1,
|
|
40
|
+
method: "initialize",
|
|
41
|
+
params: {},
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
expect(response.error).toBeUndefined();
|
|
45
|
+
const result = response.result as Record<string, unknown>;
|
|
46
|
+
expect(result.protocolVersion).toBe("2024-11-05");
|
|
47
|
+
expect(result.capabilities).toBeDefined();
|
|
48
|
+
expect((result.serverInfo as Record<string, unknown>).name).toBe("chant");
|
|
49
|
+
expect((result.serverInfo as Record<string, unknown>).version).toBe("0.1.0");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("capabilities include tools and resources", async () => {
|
|
53
|
+
const response = await server.handleRequest({
|
|
54
|
+
jsonrpc: "2.0",
|
|
55
|
+
id: 1,
|
|
56
|
+
method: "initialize",
|
|
57
|
+
params: {},
|
|
58
|
+
});
|
|
59
|
+
const result = response.result as Record<string, unknown>;
|
|
60
|
+
const caps = result.capabilities as Record<string, unknown>;
|
|
61
|
+
expect(caps.tools).toBeDefined();
|
|
62
|
+
expect(caps.resources).toBeDefined();
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe("error handling", () => {
|
|
67
|
+
test("returns error for unknown method", async () => {
|
|
68
|
+
const response = await server.handleRequest({
|
|
69
|
+
jsonrpc: "2.0",
|
|
70
|
+
id: 1,
|
|
71
|
+
method: "unknown/method",
|
|
72
|
+
});
|
|
73
|
+
expect(response.error).toBeDefined();
|
|
74
|
+
expect(response.error?.code).toBe(-32603);
|
|
75
|
+
expect(response.error?.message).toContain("Unknown method");
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// -----------------------------------------------------------------------
|
|
80
|
+
// Core tools
|
|
81
|
+
// -----------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
describe("tools/list", () => {
|
|
84
|
+
test("returns core tools (build, lint, import)", async () => {
|
|
85
|
+
const response = await server.handleRequest({
|
|
86
|
+
jsonrpc: "2.0",
|
|
87
|
+
id: 1,
|
|
88
|
+
method: "tools/list",
|
|
89
|
+
});
|
|
90
|
+
expect(response.error).toBeUndefined();
|
|
91
|
+
|
|
92
|
+
const result = response.result as { tools: Array<{ name: string }> };
|
|
93
|
+
const toolNames = result.tools.map((t) => t.name);
|
|
94
|
+
expect(toolNames).toContain("build");
|
|
95
|
+
expect(toolNames).toContain("lint");
|
|
96
|
+
expect(toolNames).toContain("import");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("each tool has name, description, and inputSchema", async () => {
|
|
100
|
+
const response = await server.handleRequest({
|
|
101
|
+
jsonrpc: "2.0",
|
|
102
|
+
id: 1,
|
|
103
|
+
method: "tools/list",
|
|
104
|
+
});
|
|
105
|
+
const result = response.result as { tools: Array<{ name: string; description: string; inputSchema: unknown }> };
|
|
106
|
+
for (const tool of result.tools) {
|
|
107
|
+
expect(typeof tool.name).toBe("string");
|
|
108
|
+
expect(typeof tool.description).toBe("string");
|
|
109
|
+
expect(tool.inputSchema).toBeDefined();
|
|
110
|
+
expect((tool.inputSchema as Record<string, unknown>).type).toBe("object");
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("build tool schema has path property", async () => {
|
|
115
|
+
const response = await server.handleRequest({
|
|
116
|
+
jsonrpc: "2.0",
|
|
117
|
+
id: 1,
|
|
118
|
+
method: "tools/list",
|
|
119
|
+
});
|
|
120
|
+
const result = response.result as { tools: Array<{ name: string; inputSchema: Record<string, unknown> }> };
|
|
121
|
+
const buildTool = result.tools.find((t) => t.name === "build")!;
|
|
122
|
+
const props = buildTool.inputSchema.properties as Record<string, unknown>;
|
|
123
|
+
expect(props.path).toBeDefined();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("lint tool schema has path and fix properties", async () => {
|
|
127
|
+
const response = await server.handleRequest({
|
|
128
|
+
jsonrpc: "2.0",
|
|
129
|
+
id: 1,
|
|
130
|
+
method: "tools/list",
|
|
131
|
+
});
|
|
132
|
+
const result = response.result as { tools: Array<{ name: string; inputSchema: Record<string, unknown> }> };
|
|
133
|
+
const lintTool = result.tools.find((t) => t.name === "lint")!;
|
|
134
|
+
const props = lintTool.inputSchema.properties as Record<string, unknown>;
|
|
135
|
+
expect(props.path).toBeDefined();
|
|
136
|
+
expect(props.fix).toBeDefined();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("import tool schema has source and output properties", async () => {
|
|
140
|
+
const response = await server.handleRequest({
|
|
141
|
+
jsonrpc: "2.0",
|
|
142
|
+
id: 1,
|
|
143
|
+
method: "tools/list",
|
|
144
|
+
});
|
|
145
|
+
const result = response.result as { tools: Array<{ name: string; inputSchema: Record<string, unknown> }> };
|
|
146
|
+
const importTool = result.tools.find((t) => t.name === "import")!;
|
|
147
|
+
const props = importTool.inputSchema.properties as Record<string, unknown>;
|
|
148
|
+
expect(props.source).toBeDefined();
|
|
149
|
+
expect(props.output).toBeDefined();
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe("tools/call", () => {
|
|
154
|
+
test("calls lint tool successfully", async () => {
|
|
155
|
+
await writeFile(join(testDir, "clean.ts"), `export const config = { a: 1 };`);
|
|
156
|
+
|
|
157
|
+
const response = await server.handleRequest({
|
|
158
|
+
jsonrpc: "2.0",
|
|
159
|
+
id: 1,
|
|
160
|
+
method: "tools/call",
|
|
161
|
+
params: { name: "lint", arguments: { path: testDir } },
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
expect(response.error).toBeUndefined();
|
|
165
|
+
const result = response.result as { content: Array<{ type: string; text: string }> };
|
|
166
|
+
expect(result.content).toBeDefined();
|
|
167
|
+
expect(result.content[0].type).toBe("text");
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("returns isError for unknown tool", async () => {
|
|
171
|
+
const response = await server.handleRequest({
|
|
172
|
+
jsonrpc: "2.0",
|
|
173
|
+
id: 1,
|
|
174
|
+
method: "tools/call",
|
|
175
|
+
params: { name: "unknown-tool", arguments: {} },
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
expect(response.error).toBeUndefined();
|
|
179
|
+
const result = response.result as { content: Array<{ text: string }>; isError: boolean };
|
|
180
|
+
expect(result.isError).toBe(true);
|
|
181
|
+
expect(result.content[0].text).toContain("Unknown tool");
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("returns isError when tool handler throws", async () => {
|
|
185
|
+
const response = await server.handleRequest({
|
|
186
|
+
jsonrpc: "2.0",
|
|
187
|
+
id: 1,
|
|
188
|
+
method: "tools/call",
|
|
189
|
+
params: { name: "import", arguments: { source: "/nonexistent/file.json" } },
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
expect(response.error).toBeUndefined();
|
|
193
|
+
const result = response.result as { content: Array<{ text: string }>; isError: boolean };
|
|
194
|
+
expect(result.isError).toBe(true);
|
|
195
|
+
expect(result.content[0].text).toContain("Error:");
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// -----------------------------------------------------------------------
|
|
200
|
+
// Core resources
|
|
201
|
+
// -----------------------------------------------------------------------
|
|
202
|
+
|
|
203
|
+
describe("resources/list", () => {
|
|
204
|
+
test("returns core resources", async () => {
|
|
205
|
+
const response = await server.handleRequest({
|
|
206
|
+
jsonrpc: "2.0",
|
|
207
|
+
id: 1,
|
|
208
|
+
method: "resources/list",
|
|
209
|
+
});
|
|
210
|
+
expect(response.error).toBeUndefined();
|
|
211
|
+
|
|
212
|
+
const result = response.result as { resources: Array<{ uri: string; name: string; description: string }> };
|
|
213
|
+
const uris = result.resources.map((r) => r.uri);
|
|
214
|
+
expect(uris).toContain("chant://context");
|
|
215
|
+
expect(uris).toContain("chant://examples/list");
|
|
216
|
+
|
|
217
|
+
// Each resource has required fields
|
|
218
|
+
for (const resource of result.resources) {
|
|
219
|
+
expect(typeof resource.uri).toBe("string");
|
|
220
|
+
expect(typeof resource.name).toBe("string");
|
|
221
|
+
expect(typeof resource.description).toBe("string");
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
describe("resources/read", () => {
|
|
227
|
+
test("reads context resource as markdown", async () => {
|
|
228
|
+
const response = await server.handleRequest({
|
|
229
|
+
jsonrpc: "2.0",
|
|
230
|
+
id: 1,
|
|
231
|
+
method: "resources/read",
|
|
232
|
+
params: { uri: "chant://context" },
|
|
233
|
+
});
|
|
234
|
+
expect(response.error).toBeUndefined();
|
|
235
|
+
|
|
236
|
+
const result = response.result as { contents: Array<{ uri: string; text: string; mimeType: string }> };
|
|
237
|
+
expect(result.contents[0].mimeType).toBe("text/markdown");
|
|
238
|
+
expect(result.contents[0].text.length).toBeGreaterThan(0);
|
|
239
|
+
expect(result.contents[0].uri).toBe("chant://context");
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test("reads examples list as empty array without plugins", async () => {
|
|
243
|
+
const response = await server.handleRequest({
|
|
244
|
+
jsonrpc: "2.0",
|
|
245
|
+
id: 1,
|
|
246
|
+
method: "resources/read",
|
|
247
|
+
params: { uri: "chant://examples/list" },
|
|
248
|
+
});
|
|
249
|
+
expect(response.error).toBeUndefined();
|
|
250
|
+
|
|
251
|
+
const result = response.result as { contents: Array<{ text: string }> };
|
|
252
|
+
const examples = JSON.parse(result.contents[0].text);
|
|
253
|
+
expect(Array.isArray(examples)).toBe(true);
|
|
254
|
+
expect(examples).toHaveLength(0);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test("reads examples from plugin resources", async () => {
|
|
258
|
+
const plugin = createMockPlugin({
|
|
259
|
+
name: "test-lex",
|
|
260
|
+
mcpResources: () => [
|
|
261
|
+
{
|
|
262
|
+
uri: "examples/my-example",
|
|
263
|
+
name: "My Example",
|
|
264
|
+
description: "A test example",
|
|
265
|
+
mimeType: "text/typescript",
|
|
266
|
+
handler: async () => "export const x = 1;",
|
|
267
|
+
},
|
|
268
|
+
],
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
const s = new McpServer([plugin]);
|
|
272
|
+
|
|
273
|
+
// List should include the example
|
|
274
|
+
const listResponse = await s.handleRequest({
|
|
275
|
+
jsonrpc: "2.0",
|
|
276
|
+
id: 1,
|
|
277
|
+
method: "resources/read",
|
|
278
|
+
params: { uri: "chant://examples/list" },
|
|
279
|
+
});
|
|
280
|
+
const examples = JSON.parse((listResponse.result as { contents: Array<{ text: string }> }).contents[0].text);
|
|
281
|
+
expect(examples).toHaveLength(1);
|
|
282
|
+
expect(examples[0].name).toBe("my-example");
|
|
283
|
+
expect(examples[0].description).toBe("A test example");
|
|
284
|
+
|
|
285
|
+
// Read the specific example
|
|
286
|
+
const readResponse = await s.handleRequest({
|
|
287
|
+
jsonrpc: "2.0",
|
|
288
|
+
id: 2,
|
|
289
|
+
method: "resources/read",
|
|
290
|
+
params: { uri: "chant://examples/my-example" },
|
|
291
|
+
});
|
|
292
|
+
expect(readResponse.error).toBeUndefined();
|
|
293
|
+
const result = readResponse.result as { contents: Array<{ text: string; mimeType: string }> };
|
|
294
|
+
expect(result.contents[0].mimeType).toBe("text/typescript");
|
|
295
|
+
expect(result.contents[0].text).toBe("export const x = 1;");
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
test("returns error for non-existent example", async () => {
|
|
299
|
+
const response = await server.handleRequest({
|
|
300
|
+
jsonrpc: "2.0",
|
|
301
|
+
id: 1,
|
|
302
|
+
method: "resources/read",
|
|
303
|
+
params: { uri: "chant://examples/nonexistent" },
|
|
304
|
+
});
|
|
305
|
+
expect(response.error).toBeDefined();
|
|
306
|
+
expect(response.error?.message).toContain("not found");
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
test("returns error for unknown resource URI", async () => {
|
|
310
|
+
const response = await server.handleRequest({
|
|
311
|
+
jsonrpc: "2.0",
|
|
312
|
+
id: 1,
|
|
313
|
+
method: "resources/read",
|
|
314
|
+
params: { uri: "chant://unknown" },
|
|
315
|
+
});
|
|
316
|
+
expect(response.error).toBeDefined();
|
|
317
|
+
expect(response.error?.message).toContain("Unknown resource");
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
// -----------------------------------------------------------------------
|
|
322
|
+
// Plugin tool contributions
|
|
323
|
+
// -----------------------------------------------------------------------
|
|
324
|
+
|
|
325
|
+
describe("plugin tools", () => {
|
|
326
|
+
test("appear in tools/list with lexicon:name prefix", async () => {
|
|
327
|
+
const plugin = createMockPlugin({
|
|
328
|
+
name: "test-lex",
|
|
329
|
+
mcpTools: () => [
|
|
330
|
+
{
|
|
331
|
+
name: "analyze",
|
|
332
|
+
description: "Analyze infrastructure",
|
|
333
|
+
inputSchema: { type: "object", properties: { path: { type: "string" } } },
|
|
334
|
+
handler: async () => "analyzed",
|
|
335
|
+
},
|
|
336
|
+
],
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
const s = new McpServer([plugin]);
|
|
340
|
+
const response = await s.handleRequest({ jsonrpc: "2.0", id: 1, method: "tools/list" });
|
|
341
|
+
const result = response.result as { tools: Array<{ name: string; description: string }> };
|
|
342
|
+
const toolNames = result.tools.map((t) => t.name);
|
|
343
|
+
|
|
344
|
+
expect(toolNames).toContain("test-lex:analyze");
|
|
345
|
+
// Core tools still present
|
|
346
|
+
expect(toolNames).toContain("build");
|
|
347
|
+
expect(toolNames).toContain("lint");
|
|
348
|
+
expect(toolNames).toContain("import");
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
test("preserve description and inputSchema", async () => {
|
|
352
|
+
const plugin = createMockPlugin({
|
|
353
|
+
name: "test-lex",
|
|
354
|
+
mcpTools: () => [
|
|
355
|
+
{
|
|
356
|
+
name: "scan",
|
|
357
|
+
description: "Scan for issues",
|
|
358
|
+
inputSchema: {
|
|
359
|
+
type: "object",
|
|
360
|
+
properties: { target: { type: "string" } },
|
|
361
|
+
required: ["target"],
|
|
362
|
+
},
|
|
363
|
+
handler: async () => "ok",
|
|
364
|
+
},
|
|
365
|
+
],
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
const s = new McpServer([plugin]);
|
|
369
|
+
const response = await s.handleRequest({ jsonrpc: "2.0", id: 1, method: "tools/list" });
|
|
370
|
+
const result = response.result as { tools: Array<{ name: string; description: string; inputSchema: Record<string, unknown> }> };
|
|
371
|
+
const tool = result.tools.find((t) => t.name === "test-lex:scan")!;
|
|
372
|
+
|
|
373
|
+
expect(tool.description).toBe("Scan for issues");
|
|
374
|
+
expect(tool.inputSchema.type).toBe("object");
|
|
375
|
+
expect((tool.inputSchema.properties as Record<string, unknown>).target).toBeDefined();
|
|
376
|
+
expect(tool.inputSchema.required).toEqual(["target"]);
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
test("can be called and return result", async () => {
|
|
380
|
+
const plugin = createMockPlugin({
|
|
381
|
+
name: "test-lex",
|
|
382
|
+
mcpTools: () => [
|
|
383
|
+
{
|
|
384
|
+
name: "greet",
|
|
385
|
+
description: "Greet",
|
|
386
|
+
inputSchema: { type: "object", properties: {} },
|
|
387
|
+
handler: async () => "hello from plugin",
|
|
388
|
+
},
|
|
389
|
+
],
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
const s = new McpServer([plugin]);
|
|
393
|
+
const response = await s.handleRequest({
|
|
394
|
+
jsonrpc: "2.0",
|
|
395
|
+
id: 1,
|
|
396
|
+
method: "tools/call",
|
|
397
|
+
params: { name: "test-lex:greet", arguments: {} },
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
expect(response.error).toBeUndefined();
|
|
401
|
+
const result = response.result as { content: Array<{ text: string }>; isError?: boolean };
|
|
402
|
+
expect(result.content[0].text).toContain("hello from plugin");
|
|
403
|
+
expect(result.isError).toBeUndefined();
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
test("handler receives forwarded params", async () => {
|
|
407
|
+
let receivedParams: Record<string, unknown> = {};
|
|
408
|
+
const plugin = createMockPlugin({
|
|
409
|
+
name: "test-lex",
|
|
410
|
+
mcpTools: () => [
|
|
411
|
+
{
|
|
412
|
+
name: "echo",
|
|
413
|
+
description: "Echo params",
|
|
414
|
+
inputSchema: { type: "object", properties: { msg: { type: "string" } } },
|
|
415
|
+
handler: async (params) => {
|
|
416
|
+
receivedParams = params;
|
|
417
|
+
return "ok";
|
|
418
|
+
},
|
|
419
|
+
},
|
|
420
|
+
],
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
const s = new McpServer([plugin]);
|
|
424
|
+
await s.handleRequest({
|
|
425
|
+
jsonrpc: "2.0",
|
|
426
|
+
id: 1,
|
|
427
|
+
method: "tools/call",
|
|
428
|
+
params: { name: "test-lex:echo", arguments: { msg: "hi", extra: 42 } },
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
expect(receivedParams.msg).toBe("hi");
|
|
432
|
+
expect(receivedParams.extra).toBe(42);
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
test("handler error returns isError response", async () => {
|
|
436
|
+
const plugin = createMockPlugin({
|
|
437
|
+
name: "test-lex",
|
|
438
|
+
mcpTools: () => [
|
|
439
|
+
{
|
|
440
|
+
name: "fail",
|
|
441
|
+
description: "Always fails",
|
|
442
|
+
inputSchema: { type: "object", properties: {} },
|
|
443
|
+
handler: async () => {
|
|
444
|
+
throw new Error("intentional failure");
|
|
445
|
+
},
|
|
446
|
+
},
|
|
447
|
+
],
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
const s = new McpServer([plugin]);
|
|
451
|
+
const response = await s.handleRequest({
|
|
452
|
+
jsonrpc: "2.0",
|
|
453
|
+
id: 1,
|
|
454
|
+
method: "tools/call",
|
|
455
|
+
params: { name: "test-lex:fail", arguments: {} },
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
expect(response.error).toBeUndefined();
|
|
459
|
+
const result = response.result as { content: Array<{ text: string }>; isError: boolean };
|
|
460
|
+
expect(result.isError).toBe(true);
|
|
461
|
+
expect(result.content[0].text).toContain("intentional failure");
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
test("handler returning object is serialized as JSON", async () => {
|
|
465
|
+
const plugin = createMockPlugin({
|
|
466
|
+
name: "test-lex",
|
|
467
|
+
mcpTools: () => [
|
|
468
|
+
{
|
|
469
|
+
name: "data",
|
|
470
|
+
description: "Return data",
|
|
471
|
+
inputSchema: { type: "object", properties: {} },
|
|
472
|
+
handler: async () => ({ count: 5, items: ["a", "b"] }),
|
|
473
|
+
},
|
|
474
|
+
],
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
const s = new McpServer([plugin]);
|
|
478
|
+
const response = await s.handleRequest({
|
|
479
|
+
jsonrpc: "2.0",
|
|
480
|
+
id: 1,
|
|
481
|
+
method: "tools/call",
|
|
482
|
+
params: { name: "test-lex:data", arguments: {} },
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
const result = response.result as { content: Array<{ text: string }> };
|
|
486
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
487
|
+
expect(parsed.count).toBe(5);
|
|
488
|
+
expect(parsed.items).toEqual(["a", "b"]);
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
test("multiple plugins contribute tools with namespace isolation", async () => {
|
|
492
|
+
const alpha = createMockPlugin({
|
|
493
|
+
name: "alpha",
|
|
494
|
+
mcpTools: () => [
|
|
495
|
+
{
|
|
496
|
+
name: "scan",
|
|
497
|
+
description: "Alpha scan",
|
|
498
|
+
inputSchema: { type: "object", properties: {} },
|
|
499
|
+
handler: async () => "alpha-result",
|
|
500
|
+
},
|
|
501
|
+
],
|
|
502
|
+
});
|
|
503
|
+
const beta = createMockPlugin({
|
|
504
|
+
name: "beta",
|
|
505
|
+
mcpTools: () => [
|
|
506
|
+
{
|
|
507
|
+
name: "scan",
|
|
508
|
+
description: "Beta scan",
|
|
509
|
+
inputSchema: { type: "object", properties: {} },
|
|
510
|
+
handler: async () => "beta-result",
|
|
511
|
+
},
|
|
512
|
+
],
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
const s = new McpServer([alpha, beta]);
|
|
516
|
+
|
|
517
|
+
// Both appear
|
|
518
|
+
const listRes = await s.handleRequest({ jsonrpc: "2.0", id: 1, method: "tools/list" });
|
|
519
|
+
const tools = (listRes.result as { tools: Array<{ name: string }> }).tools;
|
|
520
|
+
expect(tools.map((t) => t.name)).toContain("alpha:scan");
|
|
521
|
+
expect(tools.map((t) => t.name)).toContain("beta:scan");
|
|
522
|
+
|
|
523
|
+
// Each dispatches to correct handler
|
|
524
|
+
const alphaRes = await s.handleRequest({
|
|
525
|
+
jsonrpc: "2.0",
|
|
526
|
+
id: 2,
|
|
527
|
+
method: "tools/call",
|
|
528
|
+
params: { name: "alpha:scan", arguments: {} },
|
|
529
|
+
});
|
|
530
|
+
expect((alphaRes.result as { content: Array<{ text: string }> }).content[0].text).toContain("alpha-result");
|
|
531
|
+
|
|
532
|
+
const betaRes = await s.handleRequest({
|
|
533
|
+
jsonrpc: "2.0",
|
|
534
|
+
id: 3,
|
|
535
|
+
method: "tools/call",
|
|
536
|
+
params: { name: "beta:scan", arguments: {} },
|
|
537
|
+
});
|
|
538
|
+
expect((betaRes.result as { content: Array<{ text: string }> }).content[0].text).toContain("beta-result");
|
|
539
|
+
});
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
// -----------------------------------------------------------------------
|
|
543
|
+
// Plugin resource contributions
|
|
544
|
+
// -----------------------------------------------------------------------
|
|
545
|
+
|
|
546
|
+
describe("plugin resources", () => {
|
|
547
|
+
test("appear in resources/list with chant://lexicon/uri prefix", async () => {
|
|
548
|
+
const plugin = createMockPlugin({
|
|
549
|
+
name: "test-lex",
|
|
550
|
+
mcpResources: () => [
|
|
551
|
+
{
|
|
552
|
+
uri: "catalog",
|
|
553
|
+
name: "Test Catalog",
|
|
554
|
+
description: "Test resource catalog",
|
|
555
|
+
mimeType: "application/json",
|
|
556
|
+
handler: async () => "[]",
|
|
557
|
+
},
|
|
558
|
+
],
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
const s = new McpServer([plugin]);
|
|
562
|
+
const response = await s.handleRequest({ jsonrpc: "2.0", id: 1, method: "resources/list" });
|
|
563
|
+
const result = response.result as { resources: Array<{ uri: string; name: string }> };
|
|
564
|
+
const uris = result.resources.map((r) => r.uri);
|
|
565
|
+
|
|
566
|
+
expect(uris).toContain("chant://test-lex/catalog");
|
|
567
|
+
expect(uris).toContain("chant://context");
|
|
568
|
+
expect(uris).toContain("chant://examples/list");
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
test("can be read by namespaced URI", async () => {
|
|
572
|
+
const plugin = createMockPlugin({
|
|
573
|
+
name: "test-lex",
|
|
574
|
+
mcpResources: () => [
|
|
575
|
+
{
|
|
576
|
+
uri: "data",
|
|
577
|
+
name: "Test Data",
|
|
578
|
+
description: "Test data",
|
|
579
|
+
mimeType: "application/json",
|
|
580
|
+
handler: async () => JSON.stringify({ key: "value" }),
|
|
581
|
+
},
|
|
582
|
+
],
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
const s = new McpServer([plugin]);
|
|
586
|
+
const response = await s.handleRequest({
|
|
587
|
+
jsonrpc: "2.0",
|
|
588
|
+
id: 1,
|
|
589
|
+
method: "resources/read",
|
|
590
|
+
params: { uri: "chant://test-lex/data" },
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
expect(response.error).toBeUndefined();
|
|
594
|
+
const result = response.result as { contents: Array<{ uri: string; text: string; mimeType: string }> };
|
|
595
|
+
expect(result.contents[0].uri).toBe("chant://test-lex/data");
|
|
596
|
+
expect(result.contents[0].mimeType).toBe("application/json");
|
|
597
|
+
const data = JSON.parse(result.contents[0].text);
|
|
598
|
+
expect(data.key).toBe("value");
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
test("handler is called on read", async () => {
|
|
602
|
+
let handlerCalled = false;
|
|
603
|
+
const plugin = createMockPlugin({
|
|
604
|
+
name: "test-lex",
|
|
605
|
+
mcpResources: () => [
|
|
606
|
+
{
|
|
607
|
+
uri: "lazy",
|
|
608
|
+
name: "Lazy Resource",
|
|
609
|
+
description: "Computed on demand",
|
|
610
|
+
handler: async () => {
|
|
611
|
+
handlerCalled = true;
|
|
612
|
+
return "computed";
|
|
613
|
+
},
|
|
614
|
+
},
|
|
615
|
+
],
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
const s = new McpServer([plugin]);
|
|
619
|
+
// Handler not called on list
|
|
620
|
+
await s.handleRequest({ jsonrpc: "2.0", id: 1, method: "resources/list" });
|
|
621
|
+
expect(handlerCalled).toBe(false);
|
|
622
|
+
|
|
623
|
+
// Handler called on read
|
|
624
|
+
await s.handleRequest({
|
|
625
|
+
jsonrpc: "2.0",
|
|
626
|
+
id: 2,
|
|
627
|
+
method: "resources/read",
|
|
628
|
+
params: { uri: "chant://test-lex/lazy" },
|
|
629
|
+
});
|
|
630
|
+
expect(handlerCalled).toBe(true);
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
test("multiple resources from same plugin", async () => {
|
|
634
|
+
const plugin = createMockPlugin({
|
|
635
|
+
name: "multi",
|
|
636
|
+
mcpResources: () => [
|
|
637
|
+
{
|
|
638
|
+
uri: "types",
|
|
639
|
+
name: "Types",
|
|
640
|
+
description: "Type catalog",
|
|
641
|
+
mimeType: "application/json",
|
|
642
|
+
handler: async () => JSON.stringify(["TypeA", "TypeB"]),
|
|
643
|
+
},
|
|
644
|
+
{
|
|
645
|
+
uri: "config",
|
|
646
|
+
name: "Config",
|
|
647
|
+
description: "Configuration",
|
|
648
|
+
mimeType: "text/yaml",
|
|
649
|
+
handler: async () => "key: value",
|
|
650
|
+
},
|
|
651
|
+
],
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
const s = new McpServer([plugin]);
|
|
655
|
+
const listRes = await s.handleRequest({ jsonrpc: "2.0", id: 1, method: "resources/list" });
|
|
656
|
+
const uris = (listRes.result as { resources: Array<{ uri: string }> }).resources.map((r) => r.uri);
|
|
657
|
+
expect(uris).toContain("chant://multi/types");
|
|
658
|
+
expect(uris).toContain("chant://multi/config");
|
|
659
|
+
|
|
660
|
+
// Read each
|
|
661
|
+
const typesRes = await s.handleRequest({
|
|
662
|
+
jsonrpc: "2.0",
|
|
663
|
+
id: 2,
|
|
664
|
+
method: "resources/read",
|
|
665
|
+
params: { uri: "chant://multi/types" },
|
|
666
|
+
});
|
|
667
|
+
const typesContent = (typesRes.result as { contents: Array<{ text: string }> }).contents[0].text;
|
|
668
|
+
expect(JSON.parse(typesContent)).toEqual(["TypeA", "TypeB"]);
|
|
669
|
+
|
|
670
|
+
const configRes = await s.handleRequest({
|
|
671
|
+
jsonrpc: "2.0",
|
|
672
|
+
id: 3,
|
|
673
|
+
method: "resources/read",
|
|
674
|
+
params: { uri: "chant://multi/config" },
|
|
675
|
+
});
|
|
676
|
+
const configContent = (configRes.result as { contents: Array<{ text: string; mimeType: string }> }).contents[0];
|
|
677
|
+
expect(configContent.text).toBe("key: value");
|
|
678
|
+
expect(configContent.mimeType).toBe("text/yaml");
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
test("plugin resource URI does not shadow core resources", async () => {
|
|
682
|
+
// Even if a plugin URI partially matches core patterns, core should still work
|
|
683
|
+
const plugin = createMockPlugin({
|
|
684
|
+
name: "evil",
|
|
685
|
+
mcpResources: () => [
|
|
686
|
+
{
|
|
687
|
+
uri: "context",
|
|
688
|
+
name: "Evil Context",
|
|
689
|
+
description: "Not the real context",
|
|
690
|
+
handler: async () => "fake",
|
|
691
|
+
},
|
|
692
|
+
],
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
const s = new McpServer([plugin]);
|
|
696
|
+
|
|
697
|
+
// Core context still readable
|
|
698
|
+
const coreRes = await s.handleRequest({
|
|
699
|
+
jsonrpc: "2.0",
|
|
700
|
+
id: 1,
|
|
701
|
+
method: "resources/read",
|
|
702
|
+
params: { uri: "chant://context" },
|
|
703
|
+
});
|
|
704
|
+
expect(coreRes.error).toBeUndefined();
|
|
705
|
+
const coreText = (coreRes.result as { contents: Array<{ text: string }> }).contents[0].text;
|
|
706
|
+
expect(coreText).not.toBe("fake");
|
|
707
|
+
|
|
708
|
+
// Plugin resource is at its own namespaced URI
|
|
709
|
+
const pluginRes = await s.handleRequest({
|
|
710
|
+
jsonrpc: "2.0",
|
|
711
|
+
id: 2,
|
|
712
|
+
method: "resources/read",
|
|
713
|
+
params: { uri: "chant://evil/context" },
|
|
714
|
+
});
|
|
715
|
+
expect(pluginRes.error).toBeUndefined();
|
|
716
|
+
expect((pluginRes.result as { contents: Array<{ text: string }> }).contents[0].text).toBe("fake");
|
|
717
|
+
});
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
// -----------------------------------------------------------------------
|
|
721
|
+
// Server with no plugins (backward compatibility)
|
|
722
|
+
// -----------------------------------------------------------------------
|
|
723
|
+
|
|
724
|
+
describe("no plugins", () => {
|
|
725
|
+
test("server without plugins argument works identically to original", async () => {
|
|
726
|
+
const s = new McpServer();
|
|
727
|
+
const initRes = await s.handleRequest({ jsonrpc: "2.0", id: 1, method: "initialize", params: {} });
|
|
728
|
+
expect(initRes.error).toBeUndefined();
|
|
729
|
+
|
|
730
|
+
const toolsRes = await s.handleRequest({ jsonrpc: "2.0", id: 2, method: "tools/list" });
|
|
731
|
+
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"]);
|
|
734
|
+
|
|
735
|
+
const resourcesRes = await s.handleRequest({ jsonrpc: "2.0", id: 3, method: "resources/list" });
|
|
736
|
+
const resources = (resourcesRes.result as { resources: Array<{ uri: string }> }).resources;
|
|
737
|
+
expect(resources).toHaveLength(2);
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
test("server with empty plugins array works", async () => {
|
|
741
|
+
const s = new McpServer([]);
|
|
742
|
+
const toolsRes = await s.handleRequest({ jsonrpc: "2.0", id: 1, method: "tools/list" });
|
|
743
|
+
const tools = (toolsRes.result as { tools: Array<{ name: string }> }).tools;
|
|
744
|
+
expect(tools).toHaveLength(3);
|
|
745
|
+
});
|
|
746
|
+
});
|
|
747
|
+
});
|