mcpill 1.1.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/.claude/commands/add-card.md +9 -0
- package/.claude/commands/append-card.md +10 -0
- package/.claude/commands/play-card.md +14 -0
- package/.claude/commands/turn.md +28 -0
- package/.claude/settings.json +10 -0
- package/.flowdeck/.flowdeckignore +5 -0
- package/.flowdeck/AGENT.md +46 -0
- package/.flowdeck/TODO.md.template +14 -0
- package/.flowdeck/_discard/start/DISCARD.md +8 -0
- package/.flowdeck/_discard/start/TODO.md +35 -0
- package/.flowdeck/_discard/start/turn.log +28 -0
- package/.flowdeck/_energy/ADR.md.template +100 -0
- package/.flowdeck/_energy/CLAUDE.md.template +95 -0
- package/.flowdeck/_energy/GENERALINSIGHTS.md.template +57 -0
- package/.flowdeck/_energy/MISSION.md.template +89 -0
- package/.flowdeck/_energy/OPEN-QUESTIONS.md.template +109 -0
- package/.flowdeck/_energy/PROJECTINSIGHTS.md.template +64 -0
- package/.flowdeck/_energy/SPEC.md.template +101 -0
- package/.flowdeck/_frozen/FROZEN.md +4 -0
- package/.flowdeck/_meld/MELD.md +4 -0
- package/.flowdeck/_stock/STOCK.md +4 -0
- package/.flowdeck/reframe/TODO.md +71 -0
- package/CHANGELOG.md +8 -0
- package/FLOWDECK.md +17 -0
- package/README.md +85 -0
- package/dist/cli.js +954 -0
- package/package.json +34 -0
- package/src/__tests__/init.test.ts +74 -0
- package/src/__tests__/loaders/config.test.ts +54 -0
- package/src/__tests__/loaders/prompts.test.ts +116 -0
- package/src/__tests__/loaders/resources.test.ts +86 -0
- package/src/__tests__/loaders/tools.test.ts +128 -0
- package/src/__tests__/pack.test.ts +98 -0
- package/src/__tests__/validate.test.ts +152 -0
- package/src/cli.ts +76 -0
- package/src/commands/compile.ts +163 -0
- package/src/commands/init.ts +145 -0
- package/src/commands/pack.ts +52 -0
- package/src/commands/publish.ts +17 -0
- package/src/commands/run.ts +101 -0
- package/src/commands/validate.ts +70 -0
- package/src/compiler/merge-tools.ts +99 -0
- package/src/compiler/parse.ts +236 -0
- package/src/compiler/serialize.ts +100 -0
- package/src/compiler/types.ts +27 -0
- package/src/loaders/config.ts +25 -0
- package/src/loaders/prompts.ts +60 -0
- package/src/loaders/resources.ts +54 -0
- package/src/loaders/tools.ts +63 -0
- package/src/templates/prompts/greeting.md +11 -0
- package/src/templates/server.md +9 -0
- package/src/templates/tools/echo.md +15 -0
- package/tsconfig.json +10 -0
- package/tsup.config.ts +13 -0
- package/vitest.config.ts +12 -0
|
@@ -0,0 +1,152 @@
|
|
|
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 { runValidate } from "../commands/validate.js";
|
|
6
|
+
|
|
7
|
+
// Sets up a minimal valid .<name>/ pill dir using CJS tools.js so dynamic
|
|
8
|
+
// import works without a mcpill-runtime resolution in the temp tree.
|
|
9
|
+
function scaffoldValid(mcpillDir: string) {
|
|
10
|
+
fs.mkdirSync(mcpillDir, { recursive: true });
|
|
11
|
+
fs.writeFileSync(
|
|
12
|
+
path.join(mcpillDir, "tools.js"),
|
|
13
|
+
`module.exports = [{ name: "t", description: "d", schema: {}, handler: async () => ({}) }]`
|
|
14
|
+
);
|
|
15
|
+
fs.writeFileSync(
|
|
16
|
+
path.join(mcpillDir, "prompts.json"),
|
|
17
|
+
JSON.stringify([{ name: "p", args: {}, messages: [] }])
|
|
18
|
+
);
|
|
19
|
+
fs.writeFileSync(
|
|
20
|
+
path.join(mcpillDir, "resources.md"),
|
|
21
|
+
"uri: config://app\n---\nContent."
|
|
22
|
+
);
|
|
23
|
+
fs.writeFileSync(
|
|
24
|
+
path.join(mcpillDir, "mcpill.config.json"),
|
|
25
|
+
JSON.stringify({ name: "srv", transport: "stdio", port: 3333 })
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe("runValidate", () => {
|
|
30
|
+
const dirs: string[] = [];
|
|
31
|
+
|
|
32
|
+
function mkTmp(): string {
|
|
33
|
+
const d = fs.mkdtempSync(path.join(os.tmpdir(), "mcpill-validate-"));
|
|
34
|
+
dirs.push(d);
|
|
35
|
+
return d;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function mockExit() {
|
|
39
|
+
return vi
|
|
40
|
+
.spyOn(process, "exit")
|
|
41
|
+
.mockImplementation((_code?: number | string | null) => {
|
|
42
|
+
throw new Error("process.exit called");
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
afterEach(() => {
|
|
47
|
+
vi.restoreAllMocks();
|
|
48
|
+
for (const d of dirs.splice(0)) {
|
|
49
|
+
fs.rmSync(d, { recursive: true, force: true });
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("passes on a valid scaffolded pill dir", async () => {
|
|
54
|
+
const base = mkTmp();
|
|
55
|
+
const mcpillDir = path.join(base, ".mcpill");
|
|
56
|
+
scaffoldValid(mcpillDir);
|
|
57
|
+
|
|
58
|
+
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
59
|
+
await runValidate(base);
|
|
60
|
+
expect(logSpy).toHaveBeenCalledWith("✓ Valid: 1 tools, 1 prompts, 1 resources");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("exits with code 1 when no pill directories found", async () => {
|
|
64
|
+
const base = mkTmp();
|
|
65
|
+
const exitSpy = mockExit();
|
|
66
|
+
const errSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
67
|
+
|
|
68
|
+
await expect(runValidate(base)).rejects.toThrow("process.exit called");
|
|
69
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
70
|
+
expect(errSpy).toHaveBeenCalledWith(
|
|
71
|
+
"No pill directories found — run mcpill compile first"
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("collects and prints tool error then exits 1", async () => {
|
|
76
|
+
const base = mkTmp();
|
|
77
|
+
const mcpillDir = path.join(base, ".mcpill");
|
|
78
|
+
scaffoldValid(mcpillDir);
|
|
79
|
+
// Break tools.js: handler is not a function
|
|
80
|
+
fs.writeFileSync(
|
|
81
|
+
path.join(mcpillDir, "tools.js"),
|
|
82
|
+
`module.exports = [{ name: "t", description: "d", schema: {}, handler: "bad" }]`
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const exitSpy = mockExit();
|
|
86
|
+
const errSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
87
|
+
|
|
88
|
+
await expect(runValidate(base)).rejects.toThrow("process.exit called");
|
|
89
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
90
|
+
expect(errSpy).toHaveBeenCalledWith("Tool 't': handler must be a function");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("collects and prints prompt error then exits 1", async () => {
|
|
94
|
+
const base = mkTmp();
|
|
95
|
+
const mcpillDir = path.join(base, ".mcpill");
|
|
96
|
+
scaffoldValid(mcpillDir);
|
|
97
|
+
fs.writeFileSync(path.join(mcpillDir, "prompts.json"), "not json {{{");
|
|
98
|
+
|
|
99
|
+
const exitSpy = mockExit();
|
|
100
|
+
const errSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
101
|
+
|
|
102
|
+
await expect(runValidate(base)).rejects.toThrow("process.exit called");
|
|
103
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
104
|
+
expect(
|
|
105
|
+
errSpy.mock.calls.some((c) =>
|
|
106
|
+
String(c[0]).startsWith("prompts.json is not valid JSON:")
|
|
107
|
+
)
|
|
108
|
+
).toBe(true);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("collects and prints resource error then exits 1", async () => {
|
|
112
|
+
const base = mkTmp();
|
|
113
|
+
const mcpillDir = path.join(base, ".mcpill");
|
|
114
|
+
scaffoldValid(mcpillDir);
|
|
115
|
+
// Block missing uri
|
|
116
|
+
fs.writeFileSync(
|
|
117
|
+
path.join(mcpillDir, "resources.md"),
|
|
118
|
+
"name: No Uri\n---\nContent."
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
const exitSpy = mockExit();
|
|
122
|
+
const errSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
123
|
+
|
|
124
|
+
await expect(runValidate(base)).rejects.toThrow("process.exit called");
|
|
125
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
126
|
+
expect(errSpy).toHaveBeenCalledWith(
|
|
127
|
+
"resources.md block 1 is missing required frontmatter field: uri"
|
|
128
|
+
);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("collects errors from multiple loaders before exiting", async () => {
|
|
132
|
+
const base = mkTmp();
|
|
133
|
+
const mcpillDir = path.join(base, ".mcpill");
|
|
134
|
+
scaffoldValid(mcpillDir);
|
|
135
|
+
// Break both tools and prompts
|
|
136
|
+
fs.writeFileSync(
|
|
137
|
+
path.join(mcpillDir, "tools.js"),
|
|
138
|
+
`module.exports = [{ description: "d", schema: {}, handler: async () => ({}) }]`
|
|
139
|
+
);
|
|
140
|
+
fs.writeFileSync(path.join(mcpillDir, "prompts.json"), "bad json");
|
|
141
|
+
|
|
142
|
+
const exitSpy = mockExit();
|
|
143
|
+
const errSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
144
|
+
|
|
145
|
+
await expect(runValidate(base)).rejects.toThrow("process.exit called");
|
|
146
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
147
|
+
// Both errors must have been printed
|
|
148
|
+
const messages = errSpy.mock.calls.map((c) => String(c[0]));
|
|
149
|
+
expect(messages.some((m) => m.includes("Tool at index 0 is missing"))).toBe(true);
|
|
150
|
+
expect(messages.some((m) => m.startsWith("prompts.json is not valid JSON:"))).toBe(true);
|
|
151
|
+
});
|
|
152
|
+
});
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { fileURLToPath } from "url";
|
|
3
|
+
import { dirname, join } from "path";
|
|
4
|
+
import { readFileSync } from "fs";
|
|
5
|
+
import { runInit } from "./commands/init.js";
|
|
6
|
+
import { runServer } from "./commands/run.js";
|
|
7
|
+
import { runValidate } from "./commands/validate.js";
|
|
8
|
+
import { runCompile } from "./commands/compile.js";
|
|
9
|
+
import { runPack } from "./commands/pack.js";
|
|
10
|
+
import { runPublish } from "./commands/publish.js";
|
|
11
|
+
|
|
12
|
+
const pkgDir = dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
const pkg = JSON.parse(
|
|
14
|
+
readFileSync(join(pkgDir, "../package.json"), "utf-8")
|
|
15
|
+
) as { version: string };
|
|
16
|
+
|
|
17
|
+
const program = new Command();
|
|
18
|
+
|
|
19
|
+
program.name("mcpill").version(pkg.version);
|
|
20
|
+
|
|
21
|
+
program
|
|
22
|
+
.command("init")
|
|
23
|
+
.description("Scaffold a new .mcpill/ directory")
|
|
24
|
+
.option("--dir <path>", "Target directory")
|
|
25
|
+
.action(async (opts: { dir?: string }) => {
|
|
26
|
+
await runInit(opts);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
program
|
|
30
|
+
.command("run")
|
|
31
|
+
.description("Start the MCP server")
|
|
32
|
+
.option("--transport <transport>", "Transport type: stdio or http")
|
|
33
|
+
.option("--port <n>", "Port number (HTTP only)", parseInt)
|
|
34
|
+
.option("--dir <path>", "Directory containing .mcpill/")
|
|
35
|
+
.action(
|
|
36
|
+
async (opts: { transport?: "stdio" | "http"; port?: number; dir?: string }) => {
|
|
37
|
+
await runServer(opts);
|
|
38
|
+
}
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
program
|
|
42
|
+
.command("validate")
|
|
43
|
+
.description("Validate .mcpill/ configuration")
|
|
44
|
+
.option("--dir <path>", "Directory containing .mcpill/")
|
|
45
|
+
.action(async (opts: { dir?: string }) => {
|
|
46
|
+
const { resolve } = await import("path");
|
|
47
|
+
await runValidate(resolve(opts.dir ?? process.cwd()));
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
program
|
|
51
|
+
.command("compile")
|
|
52
|
+
.description("Compile server.md ↔ .mcpill/ files")
|
|
53
|
+
.option("--dir <path>", "Directory containing server.md and .mcpill/")
|
|
54
|
+
.option("--to-md", "Reverse: generate server.md from .mcpill/ files")
|
|
55
|
+
.option("--strict", "Error on missing tool handlers instead of generating stubs")
|
|
56
|
+
.action(async (opts: { dir?: string; toMd?: boolean; strict?: boolean }) => {
|
|
57
|
+
await runCompile(opts);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
program
|
|
61
|
+
.command("pack")
|
|
62
|
+
.description("Prepare pill for npm distribution")
|
|
63
|
+
.option("--dir <path>", "pill project root", ".")
|
|
64
|
+
.action(({ dir }: { dir: string }) => runPack(dir));
|
|
65
|
+
|
|
66
|
+
program
|
|
67
|
+
.command("publish")
|
|
68
|
+
.description("Pack and publish pill to npm")
|
|
69
|
+
.option("--dir <path>", "pill project root", ".")
|
|
70
|
+
.option("--access <level>", "npm access level", "public")
|
|
71
|
+
.action(({ dir, access }: { dir: string; access: string }) => runPublish(dir, access));
|
|
72
|
+
|
|
73
|
+
program.parseAsync(process.argv).catch((err) => {
|
|
74
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
75
|
+
process.exit(1);
|
|
76
|
+
});
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { resolve, join, basename } from 'path';
|
|
2
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync } from 'fs';
|
|
3
|
+
import { pathToFileURL } from 'url';
|
|
4
|
+
import { parseServerDir } from '../compiler/parse.js';
|
|
5
|
+
import { serializeServerDir } from '../compiler/serialize.js';
|
|
6
|
+
import { extractHandlers, mergeHandlers } from '../compiler/merge-tools.js';
|
|
7
|
+
import { loadConfig } from '../loaders/config.js';
|
|
8
|
+
import { loadResources } from '../loaders/resources.js';
|
|
9
|
+
import type { ServerDoc, ToolDoc } from '../compiler/types.js';
|
|
10
|
+
|
|
11
|
+
const zodTypeNameMap: Record<string, 'string' | 'number' | 'boolean'> = {
|
|
12
|
+
ZodString: 'string',
|
|
13
|
+
ZodNumber: 'number',
|
|
14
|
+
ZodBoolean: 'boolean',
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
function generateToolsJs(tools: ToolDoc[]): string {
|
|
18
|
+
const lines: string[] = [`import { z } from 'mcpill-runtime';\n\nexport default [`];
|
|
19
|
+
|
|
20
|
+
for (const tool of tools) {
|
|
21
|
+
const schemaLines = Object.entries(tool.schema).map(([key, def]) => {
|
|
22
|
+
const zodCall = def.description
|
|
23
|
+
? `z.${def.type}().describe(${JSON.stringify(def.description)})`
|
|
24
|
+
: `z.${def.type}()`;
|
|
25
|
+
return ` ${key}: ${zodCall},`;
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Indent handler lines 6 spaces; dedent+trim is applied on extract so this round-trips cleanly.
|
|
29
|
+
const handlerBody = (tool.handler ?? '').split('\n').map((l) => ` ${l}`).join('\n');
|
|
30
|
+
|
|
31
|
+
lines.push(
|
|
32
|
+
` {`,
|
|
33
|
+
` name: ${JSON.stringify(tool.name)},`,
|
|
34
|
+
` description: ${JSON.stringify(tool.description)},`,
|
|
35
|
+
` schema: {`,
|
|
36
|
+
...schemaLines,
|
|
37
|
+
` },`,
|
|
38
|
+
` handler:`,
|
|
39
|
+
` // @handler:${tool.name}`,
|
|
40
|
+
handlerBody,
|
|
41
|
+
` // @end-handler:${tool.name}`,
|
|
42
|
+
` ,`,
|
|
43
|
+
` },`,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
lines.push(`];`);
|
|
48
|
+
return lines.join('\n') + '\n';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function serializeResourcesMd(resources: ServerDoc['resources']): string {
|
|
52
|
+
if (resources.length === 0) return '';
|
|
53
|
+
return (
|
|
54
|
+
resources
|
|
55
|
+
.map((r) => {
|
|
56
|
+
let fm = `uri: ${r.uri}`;
|
|
57
|
+
if (r.name) fm += `\nname: ${r.name}`;
|
|
58
|
+
if (r.mimeType) fm += `\nmimeType: ${r.mimeType}`;
|
|
59
|
+
return `${fm}\n---\n${r.content}`;
|
|
60
|
+
})
|
|
61
|
+
.join('\n---\n') + '\n'
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function findPillDirs(baseDir: string): string[] {
|
|
66
|
+
return readdirSync(baseDir, { withFileTypes: true })
|
|
67
|
+
.filter(
|
|
68
|
+
(e) =>
|
|
69
|
+
e.isDirectory() &&
|
|
70
|
+
/^\.[a-z][a-z0-9-]*$/.test(e.name) &&
|
|
71
|
+
existsSync(join(baseDir, e.name, 'mcpill.config.json')),
|
|
72
|
+
)
|
|
73
|
+
.map((e) => join(baseDir, e.name));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function runCompile(opts: {
|
|
77
|
+
dir?: string;
|
|
78
|
+
toMd?: boolean;
|
|
79
|
+
strict?: boolean;
|
|
80
|
+
}): Promise<void> {
|
|
81
|
+
const baseDir = resolve(opts.dir ?? process.cwd());
|
|
82
|
+
|
|
83
|
+
if (opts.toMd) {
|
|
84
|
+
const pillDirs = findPillDirs(baseDir);
|
|
85
|
+
if (pillDirs.length === 0) {
|
|
86
|
+
throw new Error('No pill directories found — run mcpill compile first');
|
|
87
|
+
}
|
|
88
|
+
const mcpillDir = pillDirs[0]!;
|
|
89
|
+
|
|
90
|
+
const config = await loadConfig(mcpillDir);
|
|
91
|
+
const resources = await loadResources(mcpillDir);
|
|
92
|
+
|
|
93
|
+
const promptsPath = join(mcpillDir, 'prompts.json');
|
|
94
|
+
const prompts = JSON.parse(readFileSync(promptsPath, 'utf-8')) as ServerDoc['prompts'];
|
|
95
|
+
|
|
96
|
+
const toolsPath = join(mcpillDir, 'tools.js');
|
|
97
|
+
const handlers = existsSync(toolsPath)
|
|
98
|
+
? extractHandlers(readFileSync(toolsPath, 'utf-8'))
|
|
99
|
+
: new Map<string, string>();
|
|
100
|
+
|
|
101
|
+
let tools: ToolDoc[] = [];
|
|
102
|
+
if (existsSync(toolsPath)) {
|
|
103
|
+
const mod = (await import(pathToFileURL(toolsPath).href)) as {
|
|
104
|
+
default: Array<{ name: string; description: string; schema: Record<string, unknown> }>;
|
|
105
|
+
};
|
|
106
|
+
tools = mod.default.map((t) => {
|
|
107
|
+
const schema: ToolDoc['schema'] = {};
|
|
108
|
+
for (const [key, val] of Object.entries(t.schema)) {
|
|
109
|
+
const def = (val as { _def: { typeName: string; description?: string } })._def;
|
|
110
|
+
const type = zodTypeNameMap[def.typeName] ?? 'string';
|
|
111
|
+
schema[key] = def.description ? { type, description: def.description } : { type };
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
name: t.name,
|
|
115
|
+
description: t.description,
|
|
116
|
+
schema,
|
|
117
|
+
handler: handlers.get(t.name),
|
|
118
|
+
};
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const doc: ServerDoc = {
|
|
123
|
+
config: {
|
|
124
|
+
name: config.name,
|
|
125
|
+
transport: config.transport,
|
|
126
|
+
...(config.port !== 3333 ? { port: config.port } : {}),
|
|
127
|
+
},
|
|
128
|
+
tools,
|
|
129
|
+
prompts,
|
|
130
|
+
resources,
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
serializeServerDir(doc, baseDir);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Forward direction: server.md + tools/ + prompts/ → .<name>/
|
|
138
|
+
const doc = parseServerDir(baseDir);
|
|
139
|
+
const mcpillDir = join(baseDir, '.' + doc.config.name);
|
|
140
|
+
|
|
141
|
+
const toolsJsPath = join(mcpillDir, 'tools.js');
|
|
142
|
+
const existing = existsSync(toolsJsPath)
|
|
143
|
+
? extractHandlers(readFileSync(toolsJsPath, 'utf-8'))
|
|
144
|
+
: new Map<string, string>();
|
|
145
|
+
|
|
146
|
+
const { doc: mergedDoc, stubbed } = mergeHandlers(doc, existing);
|
|
147
|
+
|
|
148
|
+
if (opts.strict && stubbed.length > 0) {
|
|
149
|
+
throw new Error('Missing handlers for: ' + stubbed.join(', '));
|
|
150
|
+
}
|
|
151
|
+
for (const name of stubbed) {
|
|
152
|
+
console.warn(`⚠ Stub generated for tool: ${name}`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
mkdirSync(mcpillDir, { recursive: true });
|
|
156
|
+
writeFileSync(join(mcpillDir, 'mcpill.config.json'), JSON.stringify(mergedDoc.config, null, 2));
|
|
157
|
+
writeFileSync(join(mcpillDir, 'prompts.json'), JSON.stringify(mergedDoc.prompts, null, 2));
|
|
158
|
+
writeFileSync(join(mcpillDir, 'resources.md'), serializeResourcesMd(mergedDoc.resources));
|
|
159
|
+
writeFileSync(toolsJsPath, generateToolsJs(mergedDoc.tools));
|
|
160
|
+
|
|
161
|
+
const pillDirName = '.' + doc.config.name;
|
|
162
|
+
console.log(`✓ ${pillDirName}/ updated from server.md, tools/, prompts/`);
|
|
163
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
4
|
+
const TOOLS_JS = `import { z } from "mcpill-runtime";
|
|
5
|
+
|
|
6
|
+
export default [
|
|
7
|
+
{
|
|
8
|
+
name: "read_file",
|
|
9
|
+
description: "Read the contents of a file",
|
|
10
|
+
schema: {
|
|
11
|
+
path: z.string().describe("Path to the file to read"),
|
|
12
|
+
},
|
|
13
|
+
handler: async ({ path: filePath }) => {
|
|
14
|
+
return { content: \`Contents of \${filePath}\` };
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
];
|
|
18
|
+
`;
|
|
19
|
+
|
|
20
|
+
const PROMPTS_JSON = JSON.stringify(
|
|
21
|
+
[
|
|
22
|
+
{
|
|
23
|
+
name: "summarize",
|
|
24
|
+
description: "Summarize the given text",
|
|
25
|
+
args: {
|
|
26
|
+
text: { type: "string" },
|
|
27
|
+
},
|
|
28
|
+
messages: [
|
|
29
|
+
{
|
|
30
|
+
role: "user",
|
|
31
|
+
content: "Please summarize the following text:\n\n{{text}}",
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
null,
|
|
37
|
+
2
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
const RESOURCES_MD = `uri: config://app
|
|
41
|
+
name: App Config
|
|
42
|
+
---
|
|
43
|
+
This resource exposes the application configuration.
|
|
44
|
+
`;
|
|
45
|
+
|
|
46
|
+
// Embedded content of src/templates/server.md — tsup does not copy non-TS assets,
|
|
47
|
+
// so the template is inlined here. Keep in sync with src/templates/server.md.
|
|
48
|
+
const SERVER_MD_TEMPLATE = `## Config
|
|
49
|
+
name: my-server
|
|
50
|
+
transport: stdio
|
|
51
|
+
|
|
52
|
+
## Resources
|
|
53
|
+
uri: info://status
|
|
54
|
+
name: Status
|
|
55
|
+
---
|
|
56
|
+
The server is running.
|
|
57
|
+
`;
|
|
58
|
+
|
|
59
|
+
const ECHO_TOOL_MD = `# echo
|
|
60
|
+
|
|
61
|
+
Echo a message back
|
|
62
|
+
|
|
63
|
+
## Parameters
|
|
64
|
+
|
|
65
|
+
- message (string): The message to echo
|
|
66
|
+
|
|
67
|
+
## Handler
|
|
68
|
+
|
|
69
|
+
\`\`\`js
|
|
70
|
+
async ({ message }) => {
|
|
71
|
+
return { content: [{ type: "text", text: message }] };
|
|
72
|
+
}
|
|
73
|
+
\`\`\`
|
|
74
|
+
`;
|
|
75
|
+
|
|
76
|
+
const GREETING_PROMPT_MD = `# greeting
|
|
77
|
+
|
|
78
|
+
Generate a greeting
|
|
79
|
+
|
|
80
|
+
## Args
|
|
81
|
+
|
|
82
|
+
- name (string): The name to greet
|
|
83
|
+
|
|
84
|
+
## Message
|
|
85
|
+
|
|
86
|
+
> user: Say hello to {{name}}
|
|
87
|
+
`;
|
|
88
|
+
|
|
89
|
+
export async function runInit(opts: { dir?: string }): Promise<void> {
|
|
90
|
+
const targetDir = path.resolve(opts.dir ?? process.cwd());
|
|
91
|
+
const mcpillDir = path.join(targetDir, ".mcpill");
|
|
92
|
+
const projectName = path.basename(targetDir);
|
|
93
|
+
|
|
94
|
+
if (fs.existsSync(mcpillDir)) {
|
|
95
|
+
console.error(".mcpill/ already exists. Remove it manually to re-init.");
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const configJson = JSON.stringify(
|
|
100
|
+
{ name: projectName, transport: "stdio", port: 3333 },
|
|
101
|
+
null,
|
|
102
|
+
2,
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
fs.mkdirSync(mcpillDir, { recursive: true });
|
|
106
|
+
fs.writeFileSync(path.join(mcpillDir, "tools.js"), TOOLS_JS);
|
|
107
|
+
fs.writeFileSync(path.join(mcpillDir, "prompts.json"), PROMPTS_JSON);
|
|
108
|
+
fs.writeFileSync(path.join(mcpillDir, "resources.md"), RESOURCES_MD);
|
|
109
|
+
fs.writeFileSync(path.join(mcpillDir, "mcpill.config.json"), configJson);
|
|
110
|
+
|
|
111
|
+
const toolsDir = path.join(targetDir, "tools");
|
|
112
|
+
const promptsDir = path.join(targetDir, "prompts");
|
|
113
|
+
fs.mkdirSync(toolsDir, { recursive: true });
|
|
114
|
+
fs.mkdirSync(promptsDir, { recursive: true });
|
|
115
|
+
fs.writeFileSync(path.join(toolsDir, "echo.md"), ECHO_TOOL_MD);
|
|
116
|
+
fs.writeFileSync(path.join(promptsDir, "greeting.md"), GREETING_PROMPT_MD);
|
|
117
|
+
|
|
118
|
+
const serverMd = SERVER_MD_TEMPLATE.replace(
|
|
119
|
+
"name: my-server",
|
|
120
|
+
`name: ${projectName}`,
|
|
121
|
+
);
|
|
122
|
+
fs.writeFileSync(path.join(targetDir, "server.md"), serverMd);
|
|
123
|
+
|
|
124
|
+
const pkgJson = JSON.stringify(
|
|
125
|
+
{
|
|
126
|
+
name: projectName,
|
|
127
|
+
version: "0.1.0",
|
|
128
|
+
private: true,
|
|
129
|
+
scripts: { pack: "mcpill pack", publish: "mcpill publish" },
|
|
130
|
+
},
|
|
131
|
+
null,
|
|
132
|
+
2,
|
|
133
|
+
);
|
|
134
|
+
fs.writeFileSync(path.join(targetDir, "package.json"), pkgJson);
|
|
135
|
+
|
|
136
|
+
console.log("✓ .mcpill/tools.js");
|
|
137
|
+
console.log("✓ .mcpill/prompts.json");
|
|
138
|
+
console.log("✓ .mcpill/resources.md");
|
|
139
|
+
console.log("✓ .mcpill/mcpill.config.json");
|
|
140
|
+
console.log("✓ tools/echo.md");
|
|
141
|
+
console.log("✓ prompts/greeting.md");
|
|
142
|
+
console.log("✓ server.md");
|
|
143
|
+
console.log("✓ package.json");
|
|
144
|
+
console.log("Edit your tools, then run: mcpill run");
|
|
145
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { runValidate } from "./validate.js";
|
|
4
|
+
import { loadConfig } from "../loaders/config.js";
|
|
5
|
+
|
|
6
|
+
export const SERVER_ENTRY_TEMPLATE =
|
|
7
|
+
"import { runPill } from 'mcpill-runtime';\nrunPill();\n";
|
|
8
|
+
|
|
9
|
+
function resolvePillDir(baseDir: string): string {
|
|
10
|
+
const entries = fs.readdirSync(baseDir, { withFileTypes: true });
|
|
11
|
+
const pill = entries.find(
|
|
12
|
+
(e) =>
|
|
13
|
+
e.isDirectory() &&
|
|
14
|
+
/^\.[a-z][a-z0-9-]*$/.test(e.name) &&
|
|
15
|
+
fs.existsSync(path.join(baseDir, e.name, "mcpill.config.json")),
|
|
16
|
+
);
|
|
17
|
+
if (!pill) {
|
|
18
|
+
throw new Error("No pill directory found — run mcpill compile first");
|
|
19
|
+
}
|
|
20
|
+
return path.join(baseDir, pill.name);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function runPack(dir: string): Promise<void> {
|
|
24
|
+
const baseDir = path.resolve(dir);
|
|
25
|
+
await runValidate(baseDir);
|
|
26
|
+
|
|
27
|
+
const pillDir = resolvePillDir(baseDir);
|
|
28
|
+
const config = await loadConfig(pillDir);
|
|
29
|
+
const { name } = config;
|
|
30
|
+
|
|
31
|
+
fs.mkdirSync(path.join(baseDir, "bin"), { recursive: true });
|
|
32
|
+
fs.writeFileSync(path.join(baseDir, "bin", "server.js"), SERVER_ENTRY_TEMPLATE);
|
|
33
|
+
|
|
34
|
+
const pkgPath = path.join(baseDir, "package.json");
|
|
35
|
+
let pkg: Record<string, unknown> = {};
|
|
36
|
+
if (fs.existsSync(pkgPath)) {
|
|
37
|
+
pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8")) as Record<string, unknown>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
pkg.type = "module";
|
|
41
|
+
pkg.bin = { [name]: "bin/server.js" };
|
|
42
|
+
|
|
43
|
+
const deps = (pkg.dependencies as Record<string, string> | undefined) ?? {};
|
|
44
|
+
if (!deps["mcpill-runtime"]) {
|
|
45
|
+
deps["mcpill-runtime"] = "^0.1.0";
|
|
46
|
+
}
|
|
47
|
+
pkg.dependencies = deps;
|
|
48
|
+
|
|
49
|
+
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
50
|
+
|
|
51
|
+
console.log(`✓ ${name} packed — bin/server.js + package.json ready`);
|
|
52
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { spawnSync } from "child_process";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { runPack } from "./pack.js";
|
|
4
|
+
|
|
5
|
+
export async function runPublish(dir: string, access: string): Promise<void> {
|
|
6
|
+
const baseDir = path.resolve(dir);
|
|
7
|
+
await runPack(baseDir);
|
|
8
|
+
|
|
9
|
+
const result = spawnSync("npm", ["publish", "--access", access], {
|
|
10
|
+
cwd: baseDir,
|
|
11
|
+
stdio: "inherit",
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
if (result.status !== 0) {
|
|
15
|
+
throw new Error("npm publish failed");
|
|
16
|
+
}
|
|
17
|
+
}
|