mcpill 1.2.4 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/README.md +19 -1
- package/dist/cli.js +565 -165
- package/package.json +6 -1
- package/src/__tests__/init.test.ts +0 -75
- package/src/__tests__/loaders/config.test.ts +0 -54
- package/src/__tests__/loaders/prompts.test.ts +0 -116
- package/src/__tests__/loaders/resources.test.ts +0 -86
- package/src/__tests__/loaders/tools.test.ts +0 -128
- package/src/__tests__/pack.test.ts +0 -98
- package/src/__tests__/validate.test.ts +0 -152
- package/src/cli.ts +0 -76
- package/src/commands/compile.ts +0 -166
- package/src/commands/init.ts +0 -353
- package/src/commands/pack.ts +0 -38
- package/src/commands/publish.ts +0 -17
- package/src/commands/run.ts +0 -105
- package/src/commands/validate.ts +0 -59
- package/src/compiler/merge-tools.ts +0 -99
- package/src/compiler/parse.ts +0 -236
- package/src/compiler/serialize.ts +0 -100
- package/src/compiler/types.ts +0 -27
- package/src/loaders/config.ts +0 -25
- package/src/loaders/prompts.ts +0 -60
- package/src/loaders/resources.ts +0 -54
- package/src/loaders/tools.ts +0 -68
- package/src/templates/prompts/greeting.md +0 -11
- package/src/templates/server.md +0 -9
- package/src/templates/tools/echo.md +0 -15
- package/tsconfig.json +0 -10
- package/tsup.config.ts +0 -13
- package/vitest.config.ts +0 -12
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcpill",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "CLI for building, validating, and publishing MCP servers using the pill format.",
|
|
6
6
|
"homepage": "https://mcpill.ruco.dev",
|
|
@@ -11,6 +11,11 @@
|
|
|
11
11
|
"bugs": {
|
|
12
12
|
"url": "https://github.com/ruco-dev/mcpill/issues"
|
|
13
13
|
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"README.md",
|
|
17
|
+
"CHANGELOG.md"
|
|
18
|
+
],
|
|
14
19
|
"bin": {
|
|
15
20
|
"mcpill": "./dist/cli.js"
|
|
16
21
|
},
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, afterEach } from "vitest";
|
|
2
|
-
import fs from "fs";
|
|
3
|
-
import path from "path";
|
|
4
|
-
import os from "os";
|
|
5
|
-
import { runInit } from "../commands/init.js";
|
|
6
|
-
|
|
7
|
-
describe("runInit", () => {
|
|
8
|
-
const dirs: string[] = [];
|
|
9
|
-
|
|
10
|
-
function mkTmp(): string {
|
|
11
|
-
const d = fs.mkdtempSync(path.join(os.tmpdir(), "mcpill-init-"));
|
|
12
|
-
dirs.push(d);
|
|
13
|
-
return d;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
afterEach(() => {
|
|
17
|
-
vi.restoreAllMocks();
|
|
18
|
-
for (const d of dirs.splice(0)) {
|
|
19
|
-
fs.rmSync(d, { recursive: true, force: true });
|
|
20
|
-
}
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it("scaffolds the expected files and directories", async () => {
|
|
24
|
-
const base = mkTmp();
|
|
25
|
-
vi.spyOn(console, "log").mockImplementation(() => {});
|
|
26
|
-
|
|
27
|
-
await runInit({ dir: base });
|
|
28
|
-
|
|
29
|
-
const mcpillDir = path.join(base, ".mcpill");
|
|
30
|
-
const serverDir = path.join(mcpillDir, "server");
|
|
31
|
-
|
|
32
|
-
expect(fs.existsSync(mcpillDir)).toBe(true);
|
|
33
|
-
expect(fs.existsSync(path.join(mcpillDir, "server.md"))).toBe(true);
|
|
34
|
-
expect(fs.existsSync(path.join(mcpillDir, "pill-agent-guide.md"))).toBe(true);
|
|
35
|
-
expect(fs.existsSync(path.join(mcpillDir, "pill-user-guide.md"))).toBe(true);
|
|
36
|
-
expect(fs.existsSync(path.join(serverDir, "mcpill.config.json"))).toBe(true);
|
|
37
|
-
expect(fs.existsSync(path.join(serverDir, "tools", "echo.md"))).toBe(true);
|
|
38
|
-
expect(fs.existsSync(path.join(serverDir, "prompts", "greeting.md"))).toBe(true);
|
|
39
|
-
expect(fs.existsSync(path.join(base, "PILL.md"))).toBe(true);
|
|
40
|
-
expect(fs.existsSync(path.join(base, "README.md"))).toBe(false);
|
|
41
|
-
expect(fs.existsSync(path.join(base, "package.json"))).toBe(false);
|
|
42
|
-
|
|
43
|
-
const config = JSON.parse(
|
|
44
|
-
fs.readFileSync(path.join(serverDir, "mcpill.config.json"), "utf-8")
|
|
45
|
-
);
|
|
46
|
-
expect(config).toMatchObject({ name: path.basename(base), transport: "stdio", port: 3333 });
|
|
47
|
-
|
|
48
|
-
const serverMd = fs.readFileSync(path.join(mcpillDir, "server.md"), "utf-8");
|
|
49
|
-
expect(serverMd).toContain(`name: ${path.basename(base)}`);
|
|
50
|
-
|
|
51
|
-
const pillMd = fs.readFileSync(path.join(base, "PILL.md"), "utf-8");
|
|
52
|
-
expect(pillMd).toContain(".mcpill/pill-agent-guide.md");
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it("exits with code 1 and correct message when .mcpill/ already exists", async () => {
|
|
56
|
-
const base = mkTmp();
|
|
57
|
-
vi.spyOn(console, "log").mockImplementation(() => {});
|
|
58
|
-
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
59
|
-
|
|
60
|
-
await runInit({ dir: base });
|
|
61
|
-
|
|
62
|
-
const exitSpy = vi
|
|
63
|
-
.spyOn(process, "exit")
|
|
64
|
-
.mockImplementation((_code?: number | string | null) => {
|
|
65
|
-
throw new Error("process.exit called");
|
|
66
|
-
});
|
|
67
|
-
const errSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
68
|
-
|
|
69
|
-
await expect(runInit({ dir: base })).rejects.toThrow("process.exit called");
|
|
70
|
-
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
71
|
-
expect(errSpy).toHaveBeenCalledWith(
|
|
72
|
-
".mcpill/ already exists. Remove it manually to re-init."
|
|
73
|
-
);
|
|
74
|
-
});
|
|
75
|
-
});
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, afterEach } from "vitest";
|
|
2
|
-
import fs from "fs";
|
|
3
|
-
import path from "path";
|
|
4
|
-
import os from "os";
|
|
5
|
-
import { loadConfig } from "../../loaders/config.js";
|
|
6
|
-
|
|
7
|
-
describe("loadConfig", () => {
|
|
8
|
-
const dirs: string[] = [];
|
|
9
|
-
|
|
10
|
-
function mkTmp(): string {
|
|
11
|
-
const d = fs.mkdtempSync(path.join(os.tmpdir(), "mcpill-config-"));
|
|
12
|
-
dirs.push(d);
|
|
13
|
-
return d;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
afterEach(() => {
|
|
17
|
-
for (const d of dirs.splice(0)) {
|
|
18
|
-
fs.rmSync(d, { recursive: true, force: true });
|
|
19
|
-
}
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it("returns defaults when no mcpill.config.json exists", async () => {
|
|
23
|
-
const dir = mkTmp();
|
|
24
|
-
const config = await loadConfig(dir);
|
|
25
|
-
expect(config).toEqual({ name: "mcpill-server", transport: "stdio", port: 3333 });
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it("file values override defaults", async () => {
|
|
29
|
-
const dir = mkTmp();
|
|
30
|
-
fs.writeFileSync(
|
|
31
|
-
path.join(dir, "mcpill.config.json"),
|
|
32
|
-
JSON.stringify({ name: "my-server", port: 4000 })
|
|
33
|
-
);
|
|
34
|
-
const config = await loadConfig(dir);
|
|
35
|
-
expect(config).toEqual({ name: "my-server", transport: "stdio", port: 4000 });
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it("CLI opts override file values", async () => {
|
|
39
|
-
const dir = mkTmp();
|
|
40
|
-
fs.writeFileSync(
|
|
41
|
-
path.join(dir, "mcpill.config.json"),
|
|
42
|
-
JSON.stringify({ transport: "http", port: 4444 })
|
|
43
|
-
);
|
|
44
|
-
const config = await loadConfig(dir);
|
|
45
|
-
|
|
46
|
-
// Simulate CLI opt override (same merge logic used in runServer)
|
|
47
|
-
const cliOpts = { transport: "stdio" as const, port: 9999 };
|
|
48
|
-
const finalTransport = cliOpts.transport ?? config.transport;
|
|
49
|
-
const finalPort = cliOpts.port ?? config.port;
|
|
50
|
-
|
|
51
|
-
expect(finalTransport).toBe("stdio");
|
|
52
|
-
expect(finalPort).toBe(9999);
|
|
53
|
-
});
|
|
54
|
-
});
|
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, afterEach } from "vitest";
|
|
2
|
-
import fs from "fs";
|
|
3
|
-
import path from "path";
|
|
4
|
-
import os from "os";
|
|
5
|
-
import { loadPrompts } from "../../loaders/prompts.js";
|
|
6
|
-
|
|
7
|
-
describe("loadPrompts", () => {
|
|
8
|
-
const dirs: string[] = [];
|
|
9
|
-
|
|
10
|
-
function mkTmp(): string {
|
|
11
|
-
const d = fs.mkdtempSync(path.join(os.tmpdir(), "mcpill-prompts-"));
|
|
12
|
-
dirs.push(d);
|
|
13
|
-
return d;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function writePrompts(dir: string, content: string) {
|
|
17
|
-
fs.writeFileSync(path.join(dir, "prompts.json"), content);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
afterEach(() => {
|
|
21
|
-
for (const d of dirs.splice(0)) {
|
|
22
|
-
fs.rmSync(d, { recursive: true, force: true });
|
|
23
|
-
}
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it("parses valid JSON correctly", async () => {
|
|
27
|
-
const dir = mkTmp();
|
|
28
|
-
writePrompts(
|
|
29
|
-
dir,
|
|
30
|
-
JSON.stringify([
|
|
31
|
-
{
|
|
32
|
-
name: "summarize",
|
|
33
|
-
description: "Summarize text",
|
|
34
|
-
args: { text: { type: "string" }, count: { type: "number" } },
|
|
35
|
-
messages: [{ role: "user", content: "Summarize: {{text}}" }],
|
|
36
|
-
},
|
|
37
|
-
])
|
|
38
|
-
);
|
|
39
|
-
const prompts = await loadPrompts(dir);
|
|
40
|
-
expect(prompts).toHaveLength(1);
|
|
41
|
-
expect(prompts[0]!.name).toBe("summarize");
|
|
42
|
-
expect(Object.keys(prompts[0]!.argsShape)).toEqual(["text", "count"]);
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it("throws on invalid JSON", async () => {
|
|
46
|
-
const dir = mkTmp();
|
|
47
|
-
writePrompts(dir, `{ not valid json`);
|
|
48
|
-
await expect(loadPrompts(dir)).rejects.toThrow("prompts.json is not valid JSON:");
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it("throws when file is missing", async () => {
|
|
52
|
-
const dir = mkTmp();
|
|
53
|
-
await expect(loadPrompts(dir)).rejects.toThrow("prompts.json is not valid JSON:");
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it("throws on unsupported arg type", async () => {
|
|
57
|
-
const dir = mkTmp();
|
|
58
|
-
writePrompts(
|
|
59
|
-
dir,
|
|
60
|
-
JSON.stringify([
|
|
61
|
-
{
|
|
62
|
-
name: "greet",
|
|
63
|
-
args: { user: { type: "object" } },
|
|
64
|
-
messages: [],
|
|
65
|
-
},
|
|
66
|
-
])
|
|
67
|
-
);
|
|
68
|
-
await expect(loadPrompts(dir)).rejects.toThrow(
|
|
69
|
-
"Prompt 'greet': arg 'user' uses unsupported type 'object'. Supported: string, number, boolean"
|
|
70
|
-
);
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it("interpolates {{placeholder}} in message content", async () => {
|
|
74
|
-
const dir = mkTmp();
|
|
75
|
-
writePrompts(
|
|
76
|
-
dir,
|
|
77
|
-
JSON.stringify([
|
|
78
|
-
{
|
|
79
|
-
name: "greet",
|
|
80
|
-
args: { name: { type: "string" } },
|
|
81
|
-
messages: [{ role: "user", content: "Hello, {{name}}!" }],
|
|
82
|
-
},
|
|
83
|
-
])
|
|
84
|
-
);
|
|
85
|
-
const prompts = await loadPrompts(dir);
|
|
86
|
-
const { messages } = prompts[0]!;
|
|
87
|
-
|
|
88
|
-
// Replicate the interpolation logic from run.ts
|
|
89
|
-
const args: Record<string, unknown> = { name: "world" };
|
|
90
|
-
const result = messages.map((m) => ({
|
|
91
|
-
role: m.role,
|
|
92
|
-
content: m.content.replace(
|
|
93
|
-
/\{\{(\w+)\}\}/g,
|
|
94
|
-
(_, k: string) => String(args[k] ?? "")
|
|
95
|
-
),
|
|
96
|
-
}));
|
|
97
|
-
|
|
98
|
-
expect(result[0]!.content).toBe("Hello, world!");
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it("handles prompts with no args", async () => {
|
|
102
|
-
const dir = mkTmp();
|
|
103
|
-
writePrompts(
|
|
104
|
-
dir,
|
|
105
|
-
JSON.stringify([
|
|
106
|
-
{
|
|
107
|
-
name: "hello",
|
|
108
|
-
args: {},
|
|
109
|
-
messages: [{ role: "user", content: "Hello!" }],
|
|
110
|
-
},
|
|
111
|
-
])
|
|
112
|
-
);
|
|
113
|
-
const prompts = await loadPrompts(dir);
|
|
114
|
-
expect(prompts[0]!.argsShape).toEqual({});
|
|
115
|
-
});
|
|
116
|
-
});
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, afterEach } from "vitest";
|
|
2
|
-
import fs from "fs";
|
|
3
|
-
import path from "path";
|
|
4
|
-
import os from "os";
|
|
5
|
-
import { loadResources } from "../../loaders/resources.js";
|
|
6
|
-
|
|
7
|
-
describe("loadResources", () => {
|
|
8
|
-
const dirs: string[] = [];
|
|
9
|
-
|
|
10
|
-
function mkTmp(): string {
|
|
11
|
-
const d = fs.mkdtempSync(path.join(os.tmpdir(), "mcpill-resources-"));
|
|
12
|
-
dirs.push(d);
|
|
13
|
-
return d;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function writeResources(dir: string, content: string) {
|
|
17
|
-
fs.writeFileSync(path.join(dir, "resources.md"), content);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
afterEach(() => {
|
|
21
|
-
for (const d of dirs.splice(0)) {
|
|
22
|
-
fs.rmSync(d, { recursive: true, force: true });
|
|
23
|
-
}
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it("parses a single resource block", async () => {
|
|
27
|
-
const dir = mkTmp();
|
|
28
|
-
writeResources(
|
|
29
|
-
dir,
|
|
30
|
-
"uri: config://app\nname: App Config\n---\nThis is the config resource."
|
|
31
|
-
);
|
|
32
|
-
const resources = await loadResources(dir);
|
|
33
|
-
expect(resources).toHaveLength(1);
|
|
34
|
-
expect(resources[0]!.uri).toBe("config://app");
|
|
35
|
-
expect(resources[0]!.name).toBe("App Config");
|
|
36
|
-
expect(resources[0]!.content).toBe("This is the config resource.");
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it("parses multiple resource blocks", async () => {
|
|
40
|
-
const dir = mkTmp();
|
|
41
|
-
writeResources(
|
|
42
|
-
dir,
|
|
43
|
-
[
|
|
44
|
-
"uri: config://app\nname: App Config",
|
|
45
|
-
"Config content here.",
|
|
46
|
-
"uri: data://schema\nname: Schema",
|
|
47
|
-
"Schema content here.",
|
|
48
|
-
].join("\n---\n")
|
|
49
|
-
);
|
|
50
|
-
const resources = await loadResources(dir);
|
|
51
|
-
expect(resources).toHaveLength(2);
|
|
52
|
-
expect(resources[0]!.uri).toBe("config://app");
|
|
53
|
-
expect(resources[1]!.uri).toBe("data://schema");
|
|
54
|
-
expect(resources[1]!.content).toBe("Schema content here.");
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it("throws when a block is missing uri", async () => {
|
|
58
|
-
const dir = mkTmp();
|
|
59
|
-
writeResources(dir, "name: App Config\n---\nSome content.");
|
|
60
|
-
await expect(loadResources(dir)).rejects.toThrow(
|
|
61
|
-
"resources.md block 1 is missing required frontmatter field: uri"
|
|
62
|
-
);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it("handles empty body gracefully", async () => {
|
|
66
|
-
const dir = mkTmp();
|
|
67
|
-
writeResources(dir, "uri: config://app\n---\n");
|
|
68
|
-
const resources = await loadResources(dir);
|
|
69
|
-
expect(resources[0]!.content).toBe("");
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it("throws when resources.md is missing", async () => {
|
|
73
|
-
const dir = mkTmp();
|
|
74
|
-
await expect(loadResources(dir)).rejects.toThrow();
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it("parses optional mimeType from frontmatter", async () => {
|
|
78
|
-
const dir = mkTmp();
|
|
79
|
-
writeResources(
|
|
80
|
-
dir,
|
|
81
|
-
"uri: config://app\nmimeType: application/json\n---\n{}"
|
|
82
|
-
);
|
|
83
|
-
const resources = await loadResources(dir);
|
|
84
|
-
expect(resources[0]!.mimeType).toBe("application/json");
|
|
85
|
-
});
|
|
86
|
-
});
|
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, afterEach } from "vitest";
|
|
2
|
-
import fs from "fs";
|
|
3
|
-
import path from "path";
|
|
4
|
-
import os from "os";
|
|
5
|
-
import { loadTools } from "../../loaders/tools.js";
|
|
6
|
-
|
|
7
|
-
// CJS-format tools.js files work reliably in temp dirs without needing
|
|
8
|
-
// a package.json with "type":"module" — Node exposes module.exports as .default
|
|
9
|
-
// when imported via import().
|
|
10
|
-
|
|
11
|
-
describe("loadTools", () => {
|
|
12
|
-
const dirs: string[] = [];
|
|
13
|
-
|
|
14
|
-
function mkTmp(): string {
|
|
15
|
-
const d = fs.mkdtempSync(path.join(os.tmpdir(), "mcpill-tools-"));
|
|
16
|
-
dirs.push(d);
|
|
17
|
-
return d;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function writeTools(dir: string, content: string) {
|
|
21
|
-
fs.writeFileSync(path.join(dir, "tools.js"), content);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
afterEach(() => {
|
|
25
|
-
for (const d of dirs.splice(0)) {
|
|
26
|
-
fs.rmSync(d, { recursive: true, force: true });
|
|
27
|
-
}
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it("valid tools.js export passes", async () => {
|
|
31
|
-
const dir = mkTmp();
|
|
32
|
-
writeTools(
|
|
33
|
-
dir,
|
|
34
|
-
`module.exports = [
|
|
35
|
-
{ name: "read_file", description: "Read a file", schema: {}, handler: async () => ({}) }
|
|
36
|
-
]`
|
|
37
|
-
);
|
|
38
|
-
const tools = await loadTools(dir);
|
|
39
|
-
expect(tools).toHaveLength(1);
|
|
40
|
-
expect(tools[0]!.name).toBe("read_file");
|
|
41
|
-
expect(tools[0]!.description).toBe("Read a file");
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it("throws when tools.js is missing", async () => {
|
|
45
|
-
const dir = mkTmp();
|
|
46
|
-
await expect(loadTools(dir)).rejects.toThrow("tools.js failed to load:");
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it("throws when tools.js has a syntax error", async () => {
|
|
50
|
-
const dir = mkTmp();
|
|
51
|
-
writeTools(dir, `module.exports = [{ this is broken syntax`);
|
|
52
|
-
await expect(loadTools(dir)).rejects.toThrow("tools.js failed to load:");
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it("throws when default export is not an array", async () => {
|
|
56
|
-
const dir = mkTmp();
|
|
57
|
-
writeTools(dir, `module.exports = { name: "not-an-array" }`);
|
|
58
|
-
await expect(loadTools(dir)).rejects.toThrow(
|
|
59
|
-
"tools.js failed to load: default export must be an array"
|
|
60
|
-
);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it("throws when name is missing", async () => {
|
|
64
|
-
const dir = mkTmp();
|
|
65
|
-
writeTools(
|
|
66
|
-
dir,
|
|
67
|
-
`module.exports = [{ description: "no name", schema: {}, handler: async () => ({}) }]`
|
|
68
|
-
);
|
|
69
|
-
await expect(loadTools(dir)).rejects.toThrow(
|
|
70
|
-
"Tool at index 0 is missing required field: name"
|
|
71
|
-
);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it("throws when description is missing", async () => {
|
|
75
|
-
const dir = mkTmp();
|
|
76
|
-
writeTools(
|
|
77
|
-
dir,
|
|
78
|
-
`module.exports = [{ name: "t", schema: {}, handler: async () => ({}) }]`
|
|
79
|
-
);
|
|
80
|
-
await expect(loadTools(dir)).rejects.toThrow(
|
|
81
|
-
"Tool at index 0 is missing required field: description"
|
|
82
|
-
);
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it("throws when schema is missing", async () => {
|
|
86
|
-
const dir = mkTmp();
|
|
87
|
-
writeTools(
|
|
88
|
-
dir,
|
|
89
|
-
`module.exports = [{ name: "t", description: "d", handler: async () => ({}) }]`
|
|
90
|
-
);
|
|
91
|
-
await expect(loadTools(dir)).rejects.toThrow(
|
|
92
|
-
"Tool at index 0 is missing required field: schema"
|
|
93
|
-
);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it("throws when handler is missing", async () => {
|
|
97
|
-
const dir = mkTmp();
|
|
98
|
-
writeTools(
|
|
99
|
-
dir,
|
|
100
|
-
`module.exports = [{ name: "t", description: "d", schema: {} }]`
|
|
101
|
-
);
|
|
102
|
-
await expect(loadTools(dir)).rejects.toThrow(
|
|
103
|
-
"Tool at index 0 is missing required field: handler"
|
|
104
|
-
);
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it("throws when handler is not a function", async () => {
|
|
108
|
-
const dir = mkTmp();
|
|
109
|
-
writeTools(
|
|
110
|
-
dir,
|
|
111
|
-
`module.exports = [{ name: "t", description: "d", schema: {}, handler: "not-a-function" }]`
|
|
112
|
-
);
|
|
113
|
-
await expect(loadTools(dir)).rejects.toThrow(
|
|
114
|
-
"Tool 't': handler must be a function"
|
|
115
|
-
);
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it("throws when schema field is a plain value instead of a Zod type", async () => {
|
|
119
|
-
const dir = mkTmp();
|
|
120
|
-
writeTools(
|
|
121
|
-
dir,
|
|
122
|
-
`module.exports = [{ name: "foo", description: "d", schema: { input: { type: "string" } }, handler: async () => ({}) }]`
|
|
123
|
-
);
|
|
124
|
-
await expect(loadTools(dir)).rejects.toThrow(
|
|
125
|
-
"Tool 'foo': schema field 'input' must be a Zod type"
|
|
126
|
-
);
|
|
127
|
-
});
|
|
128
|
-
});
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, afterEach } from "vitest";
|
|
2
|
-
import fs from "fs";
|
|
3
|
-
import path from "path";
|
|
4
|
-
import os from "os";
|
|
5
|
-
import { runPack, SERVER_ENTRY_TEMPLATE } from "../commands/pack.js";
|
|
6
|
-
|
|
7
|
-
function scaffoldPill(baseDir: string, name = "test-pill") {
|
|
8
|
-
const mcpillDir = path.join(baseDir, ".mcpill", "server");
|
|
9
|
-
fs.mkdirSync(mcpillDir, { recursive: true });
|
|
10
|
-
fs.writeFileSync(
|
|
11
|
-
path.join(mcpillDir, "tools.js"),
|
|
12
|
-
`module.exports = [{ name: "t", description: "d", schema: {}, handler: async () => ({}) }]`
|
|
13
|
-
);
|
|
14
|
-
fs.writeFileSync(
|
|
15
|
-
path.join(mcpillDir, "prompts.json"),
|
|
16
|
-
JSON.stringify([{ name: "p", args: {}, messages: [] }])
|
|
17
|
-
);
|
|
18
|
-
fs.writeFileSync(
|
|
19
|
-
path.join(mcpillDir, "resources.md"),
|
|
20
|
-
"uri: config://app\n---\nContent."
|
|
21
|
-
);
|
|
22
|
-
fs.writeFileSync(
|
|
23
|
-
path.join(mcpillDir, "mcpill.config.json"),
|
|
24
|
-
JSON.stringify({ name, transport: "stdio", port: 3333 })
|
|
25
|
-
);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
describe("runPack", () => {
|
|
29
|
-
const dirs: string[] = [];
|
|
30
|
-
|
|
31
|
-
function mkTmp(): string {
|
|
32
|
-
const d = fs.mkdtempSync(path.join(os.tmpdir(), "mcpill-pack-"));
|
|
33
|
-
dirs.push(d);
|
|
34
|
-
return d;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function mockExit() {
|
|
38
|
-
return vi
|
|
39
|
-
.spyOn(process, "exit")
|
|
40
|
-
.mockImplementation((_code?: number | string | null) => {
|
|
41
|
-
throw new Error("process.exit called");
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
afterEach(() => {
|
|
46
|
-
vi.restoreAllMocks();
|
|
47
|
-
for (const d of dirs.splice(0)) {
|
|
48
|
-
fs.rmSync(d, { recursive: true, force: true });
|
|
49
|
-
}
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it("writes bin/server.js with SERVER_ENTRY_TEMPLATE", async () => {
|
|
53
|
-
const base = mkTmp();
|
|
54
|
-
scaffoldPill(base);
|
|
55
|
-
vi.spyOn(console, "log").mockImplementation(() => {});
|
|
56
|
-
|
|
57
|
-
await runPack(base);
|
|
58
|
-
|
|
59
|
-
const serverJs = fs.readFileSync(path.join(base, "bin", "server.js"), "utf-8");
|
|
60
|
-
expect(serverJs).toBe(SERVER_ENTRY_TEMPLATE);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it("writes package.json with bin entry and mcpill-runtime dependency", async () => {
|
|
64
|
-
const base = mkTmp();
|
|
65
|
-
scaffoldPill(base);
|
|
66
|
-
vi.spyOn(console, "log").mockImplementation(() => {});
|
|
67
|
-
|
|
68
|
-
await runPack(base);
|
|
69
|
-
|
|
70
|
-
const pkg = JSON.parse(fs.readFileSync(path.join(base, "package.json"), "utf-8"));
|
|
71
|
-
expect(pkg.bin).toHaveProperty("test-pill");
|
|
72
|
-
expect(pkg.dependencies["mcpill-runtime"]).toBe("^0.1.0");
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it("does not overwrite existing name/version in package.json", async () => {
|
|
76
|
-
const base = mkTmp();
|
|
77
|
-
scaffoldPill(base);
|
|
78
|
-
fs.writeFileSync(
|
|
79
|
-
path.join(base, "package.json"),
|
|
80
|
-
JSON.stringify({ name: "my-pill", version: "1.2.3" }, null, 2)
|
|
81
|
-
);
|
|
82
|
-
vi.spyOn(console, "log").mockImplementation(() => {});
|
|
83
|
-
|
|
84
|
-
await runPack(base);
|
|
85
|
-
|
|
86
|
-
const pkg = JSON.parse(fs.readFileSync(path.join(base, "package.json"), "utf-8"));
|
|
87
|
-
expect(pkg.name).toBe("my-pill");
|
|
88
|
-
expect(pkg.version).toBe("1.2.3");
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it("throws when no pill directory is present", async () => {
|
|
92
|
-
const base = mkTmp();
|
|
93
|
-
mockExit();
|
|
94
|
-
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
95
|
-
|
|
96
|
-
await expect(runPack(base)).rejects.toThrow();
|
|
97
|
-
});
|
|
98
|
-
});
|