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
package/dist/cli.js
ADDED
|
@@ -0,0 +1,954 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import { fileURLToPath } from "url";
|
|
6
|
+
import { dirname, join as join4 } from "path";
|
|
7
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
8
|
+
|
|
9
|
+
// src/commands/init.ts
|
|
10
|
+
import fs from "fs";
|
|
11
|
+
import path from "path";
|
|
12
|
+
var TOOLS_JS = `import { z } from "mcpill-runtime";
|
|
13
|
+
|
|
14
|
+
export default [
|
|
15
|
+
{
|
|
16
|
+
name: "read_file",
|
|
17
|
+
description: "Read the contents of a file",
|
|
18
|
+
schema: {
|
|
19
|
+
path: z.string().describe("Path to the file to read"),
|
|
20
|
+
},
|
|
21
|
+
handler: async ({ path: filePath }) => {
|
|
22
|
+
return { content: \`Contents of \${filePath}\` };
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
];
|
|
26
|
+
`;
|
|
27
|
+
var PROMPTS_JSON = JSON.stringify(
|
|
28
|
+
[
|
|
29
|
+
{
|
|
30
|
+
name: "summarize",
|
|
31
|
+
description: "Summarize the given text",
|
|
32
|
+
args: {
|
|
33
|
+
text: { type: "string" }
|
|
34
|
+
},
|
|
35
|
+
messages: [
|
|
36
|
+
{
|
|
37
|
+
role: "user",
|
|
38
|
+
content: "Please summarize the following text:\n\n{{text}}"
|
|
39
|
+
}
|
|
40
|
+
]
|
|
41
|
+
}
|
|
42
|
+
],
|
|
43
|
+
null,
|
|
44
|
+
2
|
|
45
|
+
);
|
|
46
|
+
var RESOURCES_MD = `uri: config://app
|
|
47
|
+
name: App Config
|
|
48
|
+
---
|
|
49
|
+
This resource exposes the application configuration.
|
|
50
|
+
`;
|
|
51
|
+
var SERVER_MD_TEMPLATE = `## Config
|
|
52
|
+
name: my-server
|
|
53
|
+
transport: stdio
|
|
54
|
+
|
|
55
|
+
## Resources
|
|
56
|
+
uri: info://status
|
|
57
|
+
name: Status
|
|
58
|
+
---
|
|
59
|
+
The server is running.
|
|
60
|
+
`;
|
|
61
|
+
var ECHO_TOOL_MD = `# echo
|
|
62
|
+
|
|
63
|
+
Echo a message back
|
|
64
|
+
|
|
65
|
+
## Parameters
|
|
66
|
+
|
|
67
|
+
- message (string): The message to echo
|
|
68
|
+
|
|
69
|
+
## Handler
|
|
70
|
+
|
|
71
|
+
\`\`\`js
|
|
72
|
+
async ({ message }) => {
|
|
73
|
+
return { content: [{ type: "text", text: message }] };
|
|
74
|
+
}
|
|
75
|
+
\`\`\`
|
|
76
|
+
`;
|
|
77
|
+
var GREETING_PROMPT_MD = `# greeting
|
|
78
|
+
|
|
79
|
+
Generate a greeting
|
|
80
|
+
|
|
81
|
+
## Args
|
|
82
|
+
|
|
83
|
+
- name (string): The name to greet
|
|
84
|
+
|
|
85
|
+
## Message
|
|
86
|
+
|
|
87
|
+
> user: Say hello to {{name}}
|
|
88
|
+
`;
|
|
89
|
+
async function runInit(opts) {
|
|
90
|
+
const targetDir = path.resolve(opts.dir ?? process.cwd());
|
|
91
|
+
const mcpillDir = path.join(targetDir, ".mcpill");
|
|
92
|
+
const projectName = path.basename(targetDir);
|
|
93
|
+
if (fs.existsSync(mcpillDir)) {
|
|
94
|
+
console.error(".mcpill/ already exists. Remove it manually to re-init.");
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
const configJson = JSON.stringify(
|
|
98
|
+
{ name: projectName, transport: "stdio", port: 3333 },
|
|
99
|
+
null,
|
|
100
|
+
2
|
|
101
|
+
);
|
|
102
|
+
fs.mkdirSync(mcpillDir, { recursive: true });
|
|
103
|
+
fs.writeFileSync(path.join(mcpillDir, "tools.js"), TOOLS_JS);
|
|
104
|
+
fs.writeFileSync(path.join(mcpillDir, "prompts.json"), PROMPTS_JSON);
|
|
105
|
+
fs.writeFileSync(path.join(mcpillDir, "resources.md"), RESOURCES_MD);
|
|
106
|
+
fs.writeFileSync(path.join(mcpillDir, "mcpill.config.json"), configJson);
|
|
107
|
+
const toolsDir = path.join(targetDir, "tools");
|
|
108
|
+
const promptsDir = path.join(targetDir, "prompts");
|
|
109
|
+
fs.mkdirSync(toolsDir, { recursive: true });
|
|
110
|
+
fs.mkdirSync(promptsDir, { recursive: true });
|
|
111
|
+
fs.writeFileSync(path.join(toolsDir, "echo.md"), ECHO_TOOL_MD);
|
|
112
|
+
fs.writeFileSync(path.join(promptsDir, "greeting.md"), GREETING_PROMPT_MD);
|
|
113
|
+
const serverMd = SERVER_MD_TEMPLATE.replace(
|
|
114
|
+
"name: my-server",
|
|
115
|
+
`name: ${projectName}`
|
|
116
|
+
);
|
|
117
|
+
fs.writeFileSync(path.join(targetDir, "server.md"), serverMd);
|
|
118
|
+
const pkgJson = JSON.stringify(
|
|
119
|
+
{
|
|
120
|
+
name: projectName,
|
|
121
|
+
version: "0.1.0",
|
|
122
|
+
private: true,
|
|
123
|
+
scripts: { pack: "mcpill pack", publish: "mcpill publish" }
|
|
124
|
+
},
|
|
125
|
+
null,
|
|
126
|
+
2
|
|
127
|
+
);
|
|
128
|
+
fs.writeFileSync(path.join(targetDir, "package.json"), pkgJson);
|
|
129
|
+
console.log("\u2713 .mcpill/tools.js");
|
|
130
|
+
console.log("\u2713 .mcpill/prompts.json");
|
|
131
|
+
console.log("\u2713 .mcpill/resources.md");
|
|
132
|
+
console.log("\u2713 .mcpill/mcpill.config.json");
|
|
133
|
+
console.log("\u2713 tools/echo.md");
|
|
134
|
+
console.log("\u2713 prompts/greeting.md");
|
|
135
|
+
console.log("\u2713 server.md");
|
|
136
|
+
console.log("\u2713 package.json");
|
|
137
|
+
console.log("Edit your tools, then run: mcpill run");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// src/commands/run.ts
|
|
141
|
+
import path7 from "path";
|
|
142
|
+
import fs6 from "fs";
|
|
143
|
+
import { z as z2 } from "mcpill-runtime";
|
|
144
|
+
import { createServer } from "mcpster";
|
|
145
|
+
|
|
146
|
+
// src/loaders/config.ts
|
|
147
|
+
import fs2 from "fs";
|
|
148
|
+
import path2 from "path";
|
|
149
|
+
var defaults = {
|
|
150
|
+
name: "mcpill-server",
|
|
151
|
+
transport: "stdio",
|
|
152
|
+
port: 3333
|
|
153
|
+
};
|
|
154
|
+
async function loadConfig(mcpillDir) {
|
|
155
|
+
const configPath = path2.join(mcpillDir, "mcpill.config.json");
|
|
156
|
+
if (!fs2.existsSync(configPath)) {
|
|
157
|
+
return { ...defaults };
|
|
158
|
+
}
|
|
159
|
+
const raw = JSON.parse(
|
|
160
|
+
fs2.readFileSync(configPath, "utf-8")
|
|
161
|
+
);
|
|
162
|
+
return { ...defaults, ...raw };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// src/loaders/tools.ts
|
|
166
|
+
import { pathToFileURL } from "url";
|
|
167
|
+
import path3 from "path";
|
|
168
|
+
async function loadTools(mcpillDir) {
|
|
169
|
+
const toolsPath = path3.join(mcpillDir, "tools.js");
|
|
170
|
+
let mod;
|
|
171
|
+
try {
|
|
172
|
+
mod = await import(pathToFileURL(toolsPath).href);
|
|
173
|
+
} catch (err) {
|
|
174
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
175
|
+
throw new Error(`tools.js failed to load: ${msg}`);
|
|
176
|
+
}
|
|
177
|
+
const tools = mod.default;
|
|
178
|
+
if (!Array.isArray(tools)) {
|
|
179
|
+
throw new Error("tools.js failed to load: default export must be an array");
|
|
180
|
+
}
|
|
181
|
+
for (let i = 0; i < tools.length; i++) {
|
|
182
|
+
const tool = tools[i];
|
|
183
|
+
for (const field of ["name", "description", "schema", "handler"]) {
|
|
184
|
+
if (tool[field] === void 0 || tool[field] === null) {
|
|
185
|
+
throw new Error(`Tool at index ${i} is missing required field: ${field}`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (typeof tool["name"] !== "string") {
|
|
189
|
+
throw new Error(`Tool at index ${i} is missing required field: name`);
|
|
190
|
+
}
|
|
191
|
+
if (typeof tool["description"] !== "string") {
|
|
192
|
+
throw new Error(`Tool at index ${i} is missing required field: description`);
|
|
193
|
+
}
|
|
194
|
+
if (typeof tool["schema"] !== "object" || Array.isArray(tool["schema"]) || tool["schema"] === null) {
|
|
195
|
+
throw new Error(`Tool at index ${i} is missing required field: schema`);
|
|
196
|
+
}
|
|
197
|
+
const name = tool["name"];
|
|
198
|
+
for (const [key, value] of Object.entries(tool["schema"])) {
|
|
199
|
+
if (value == null || typeof value !== "object" || !("_def" in value)) {
|
|
200
|
+
throw new Error(
|
|
201
|
+
`Tool '${name}': schema field '${key}' must be a Zod type (e.g. z.string()). Got a plain value instead.`
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (typeof tool["handler"] !== "function") {
|
|
206
|
+
throw new Error(`Tool '${tool["name"]}': handler must be a function`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return tools;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// src/loaders/prompts.ts
|
|
213
|
+
import fs3 from "fs";
|
|
214
|
+
import path4 from "path";
|
|
215
|
+
import { z } from "mcpill-runtime";
|
|
216
|
+
var typeMap = {
|
|
217
|
+
string: z.string(),
|
|
218
|
+
number: z.number(),
|
|
219
|
+
boolean: z.boolean()
|
|
220
|
+
};
|
|
221
|
+
async function loadPrompts(mcpillDir) {
|
|
222
|
+
const promptsPath = path4.join(mcpillDir, "prompts.json");
|
|
223
|
+
let raw;
|
|
224
|
+
try {
|
|
225
|
+
const content = fs3.readFileSync(promptsPath, "utf-8");
|
|
226
|
+
raw = JSON.parse(content);
|
|
227
|
+
} catch (err) {
|
|
228
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
229
|
+
throw new Error(`prompts.json is not valid JSON: ${msg}`);
|
|
230
|
+
}
|
|
231
|
+
if (!Array.isArray(raw)) {
|
|
232
|
+
throw new Error("prompts.json is not valid JSON: must be an array");
|
|
233
|
+
}
|
|
234
|
+
const prompts = [];
|
|
235
|
+
for (const entry of raw) {
|
|
236
|
+
const argsShape = {};
|
|
237
|
+
const args = entry["args"] ?? {};
|
|
238
|
+
for (const [argName, argDef] of Object.entries(args)) {
|
|
239
|
+
const typeName = argDef.type;
|
|
240
|
+
if (!(typeName in typeMap)) {
|
|
241
|
+
throw new Error(
|
|
242
|
+
`Prompt '${entry["name"]}': arg '${argName}' uses unsupported type '${typeName}'. Supported: string, number, boolean`
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
argsShape[argName] = typeMap[typeName];
|
|
246
|
+
}
|
|
247
|
+
prompts.push({
|
|
248
|
+
name: entry["name"],
|
|
249
|
+
description: entry["description"],
|
|
250
|
+
argsShape,
|
|
251
|
+
messages: entry["messages"]
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
return prompts;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// src/loaders/resources.ts
|
|
258
|
+
import fs4 from "fs";
|
|
259
|
+
import path5 from "path";
|
|
260
|
+
function parseFrontmatter(block) {
|
|
261
|
+
const result = {};
|
|
262
|
+
for (const line of block.split("\n")) {
|
|
263
|
+
const colonIdx = line.indexOf(":");
|
|
264
|
+
if (colonIdx === -1) continue;
|
|
265
|
+
const key = line.slice(0, colonIdx).trim();
|
|
266
|
+
const value = line.slice(colonIdx + 1).trim();
|
|
267
|
+
if (key) result[key] = value;
|
|
268
|
+
}
|
|
269
|
+
return result;
|
|
270
|
+
}
|
|
271
|
+
async function loadResources(mcpillDir) {
|
|
272
|
+
const resourcesPath = path5.join(mcpillDir, "resources.md");
|
|
273
|
+
const fileContent = fs4.readFileSync(resourcesPath, "utf-8");
|
|
274
|
+
const blocks = fileContent.split("\n---\n");
|
|
275
|
+
const resources = [];
|
|
276
|
+
for (let i = 0; i < blocks.length; i += 2) {
|
|
277
|
+
const frontmatterBlock = blocks[i] ?? "";
|
|
278
|
+
if (frontmatterBlock.trim() === "") continue;
|
|
279
|
+
const bodyBlock = blocks[i + 1] ?? "";
|
|
280
|
+
const frontmatter = parseFrontmatter(frontmatterBlock);
|
|
281
|
+
if (!frontmatter["uri"]) {
|
|
282
|
+
throw new Error(
|
|
283
|
+
`resources.md block ${i + 1} is missing required frontmatter field: uri`
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
resources.push({
|
|
287
|
+
uri: frontmatter["uri"],
|
|
288
|
+
name: frontmatter["name"],
|
|
289
|
+
mimeType: frontmatter["mimeType"],
|
|
290
|
+
content: bodyBlock.trim()
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
return resources;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// src/commands/validate.ts
|
|
297
|
+
import fs5 from "fs";
|
|
298
|
+
import path6 from "path";
|
|
299
|
+
async function validateOne(mcpillDir) {
|
|
300
|
+
const errors = [];
|
|
301
|
+
let toolCount = 0;
|
|
302
|
+
let promptCount = 0;
|
|
303
|
+
let resourceCount = 0;
|
|
304
|
+
try {
|
|
305
|
+
const tools = await loadTools(mcpillDir);
|
|
306
|
+
toolCount = tools.length;
|
|
307
|
+
} catch (err) {
|
|
308
|
+
errors.push(err instanceof Error ? err.message : String(err));
|
|
309
|
+
}
|
|
310
|
+
try {
|
|
311
|
+
const prompts = await loadPrompts(mcpillDir);
|
|
312
|
+
promptCount = prompts.length;
|
|
313
|
+
} catch (err) {
|
|
314
|
+
errors.push(err instanceof Error ? err.message : String(err));
|
|
315
|
+
}
|
|
316
|
+
try {
|
|
317
|
+
const resources = await loadResources(mcpillDir);
|
|
318
|
+
resourceCount = resources.length;
|
|
319
|
+
} catch (err) {
|
|
320
|
+
errors.push(err instanceof Error ? err.message : String(err));
|
|
321
|
+
}
|
|
322
|
+
try {
|
|
323
|
+
await loadConfig(mcpillDir);
|
|
324
|
+
} catch (err) {
|
|
325
|
+
errors.push(err instanceof Error ? err.message : String(err));
|
|
326
|
+
}
|
|
327
|
+
if (errors.length > 0) {
|
|
328
|
+
for (const error of errors) {
|
|
329
|
+
console.error(error);
|
|
330
|
+
}
|
|
331
|
+
process.exit(1);
|
|
332
|
+
}
|
|
333
|
+
console.log(`\u2713 Valid: ${toolCount} tools, ${promptCount} prompts, ${resourceCount} resources`);
|
|
334
|
+
}
|
|
335
|
+
async function runValidate(baseDir) {
|
|
336
|
+
const pillDirs = fs5.readdirSync(baseDir, { withFileTypes: true }).filter(
|
|
337
|
+
(e) => e.isDirectory() && /^\.[a-z][a-z0-9-]*$/.test(e.name) && fs5.existsSync(path6.join(baseDir, e.name, "mcpill.config.json"))
|
|
338
|
+
).map((e) => path6.join(baseDir, e.name));
|
|
339
|
+
if (pillDirs.length === 0) {
|
|
340
|
+
console.error("No pill directories found \u2014 run mcpill compile first");
|
|
341
|
+
process.exit(1);
|
|
342
|
+
}
|
|
343
|
+
for (const mcpillDir of pillDirs) {
|
|
344
|
+
await validateOne(mcpillDir);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// src/commands/run.ts
|
|
349
|
+
async function runServer(opts) {
|
|
350
|
+
const baseDir = path7.resolve(opts.dir ?? process.cwd());
|
|
351
|
+
await runValidate(baseDir);
|
|
352
|
+
const pillDirs = fs6.readdirSync(baseDir, { withFileTypes: true }).filter(
|
|
353
|
+
(e) => e.isDirectory() && /^\.[a-z][a-z0-9-]*$/.test(e.name) && fs6.existsSync(path7.join(baseDir, e.name, "mcpill.config.json"))
|
|
354
|
+
).map((e) => path7.join(baseDir, e.name));
|
|
355
|
+
const mcpillDir = pillDirs[0];
|
|
356
|
+
const [tools, prompts, resources, config] = await Promise.all([
|
|
357
|
+
loadTools(mcpillDir),
|
|
358
|
+
loadPrompts(mcpillDir),
|
|
359
|
+
loadResources(mcpillDir),
|
|
360
|
+
loadConfig(mcpillDir)
|
|
361
|
+
]);
|
|
362
|
+
const transport = opts.transport ?? config.transport;
|
|
363
|
+
const port = opts.port ?? config.port;
|
|
364
|
+
const { name } = config;
|
|
365
|
+
const server = createServer({
|
|
366
|
+
name,
|
|
367
|
+
version: "0.1.0",
|
|
368
|
+
transport,
|
|
369
|
+
http: { port }
|
|
370
|
+
});
|
|
371
|
+
for (const { name: toolName, description, schema, handler } of tools) {
|
|
372
|
+
server.defineTool({
|
|
373
|
+
name: toolName,
|
|
374
|
+
description,
|
|
375
|
+
schema: z2.object(schema),
|
|
376
|
+
handler
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
for (const { name: promptName, description, messages } of prompts) {
|
|
380
|
+
server.definePrompt({
|
|
381
|
+
name: promptName,
|
|
382
|
+
description,
|
|
383
|
+
handler: async (args) => messages.map(
|
|
384
|
+
(m) => m.content.replace(
|
|
385
|
+
/\{\{(\w+)\}\}/g,
|
|
386
|
+
(_, k) => String(args[k] ?? "")
|
|
387
|
+
)
|
|
388
|
+
).join("\n")
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
for (const { uri, name: resourceName, content } of resources) {
|
|
392
|
+
server.defineResource({
|
|
393
|
+
uri,
|
|
394
|
+
description: resourceName ?? uri,
|
|
395
|
+
resolver: async () => content
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
try {
|
|
399
|
+
await server.start();
|
|
400
|
+
} catch (err) {
|
|
401
|
+
const nodeErr = err;
|
|
402
|
+
if (nodeErr.code === "EADDRINUSE") {
|
|
403
|
+
console.error(
|
|
404
|
+
`Port ${port} is already in use. Use --port to specify another.`
|
|
405
|
+
);
|
|
406
|
+
process.exit(1);
|
|
407
|
+
}
|
|
408
|
+
throw err;
|
|
409
|
+
}
|
|
410
|
+
process.stderr.write(
|
|
411
|
+
`\u2713 MCPill running \u2014 ${tools.length} tools, ${prompts.length} prompts, ${resources.length} resources [${transport}]
|
|
412
|
+
`
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// src/commands/compile.ts
|
|
417
|
+
import { resolve, join as join3 } from "path";
|
|
418
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync, readdirSync as readdirSync2 } from "fs";
|
|
419
|
+
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
420
|
+
|
|
421
|
+
// src/compiler/parse.ts
|
|
422
|
+
import { readdirSync, readFileSync } from "fs";
|
|
423
|
+
import { join } from "path";
|
|
424
|
+
function extractSections(content) {
|
|
425
|
+
const sections = /* @__PURE__ */ new Map();
|
|
426
|
+
const pieces = ("\n" + content).split("\n## ");
|
|
427
|
+
for (const piece of pieces) {
|
|
428
|
+
if (!piece.trim()) continue;
|
|
429
|
+
const nlIdx = piece.indexOf("\n");
|
|
430
|
+
if (nlIdx === -1) continue;
|
|
431
|
+
const name = piece.slice(0, nlIdx).trim();
|
|
432
|
+
const body = piece.slice(nlIdx + 1);
|
|
433
|
+
sections.set(name, body);
|
|
434
|
+
}
|
|
435
|
+
return sections;
|
|
436
|
+
}
|
|
437
|
+
function extractFencedBlock(body, lang) {
|
|
438
|
+
const startMarker = "```" + lang + "\n";
|
|
439
|
+
const start = body.indexOf(startMarker);
|
|
440
|
+
if (start === -1) return null;
|
|
441
|
+
const contentStart = start + startMarker.length;
|
|
442
|
+
const end = body.indexOf("\n```", contentStart);
|
|
443
|
+
if (end === -1) return body.slice(contentStart).trim();
|
|
444
|
+
return body.slice(contentStart, end).trim();
|
|
445
|
+
}
|
|
446
|
+
function parseKvBlock(body) {
|
|
447
|
+
const result = {};
|
|
448
|
+
for (const line of body.split("\n")) {
|
|
449
|
+
const colonIdx = line.indexOf(":");
|
|
450
|
+
if (colonIdx === -1) continue;
|
|
451
|
+
const key = line.slice(0, colonIdx).trim();
|
|
452
|
+
const value = line.slice(colonIdx + 1).trim();
|
|
453
|
+
if (key) result[key] = value;
|
|
454
|
+
}
|
|
455
|
+
return result;
|
|
456
|
+
}
|
|
457
|
+
function parseFrontmatter2(block) {
|
|
458
|
+
return parseKvBlock(block);
|
|
459
|
+
}
|
|
460
|
+
function parseResourcesSection(body) {
|
|
461
|
+
const resources = [];
|
|
462
|
+
const blocks = body.split("\n---\n");
|
|
463
|
+
for (let i = 0; i < blocks.length; i += 2) {
|
|
464
|
+
const frontmatterBlock = (blocks[i] ?? "").trim();
|
|
465
|
+
if (!frontmatterBlock) continue;
|
|
466
|
+
const bodyBlock = blocks[i + 1] ?? "";
|
|
467
|
+
const frontmatter = parseFrontmatter2(frontmatterBlock);
|
|
468
|
+
if (!frontmatter["uri"]) continue;
|
|
469
|
+
resources.push({
|
|
470
|
+
uri: frontmatter["uri"],
|
|
471
|
+
name: frontmatter["name"],
|
|
472
|
+
mimeType: frontmatter["mimeType"],
|
|
473
|
+
content: bodyBlock.trim()
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
return resources;
|
|
477
|
+
}
|
|
478
|
+
function parseToolFile(content) {
|
|
479
|
+
const lines = content.split("\n");
|
|
480
|
+
const h1Line = lines.find((l) => l.startsWith("# "));
|
|
481
|
+
const name = h1Line ? h1Line.slice(2).trim() : "";
|
|
482
|
+
let h1Seen = false;
|
|
483
|
+
let description = "";
|
|
484
|
+
for (const line of lines) {
|
|
485
|
+
if (line.startsWith("# ")) {
|
|
486
|
+
h1Seen = true;
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
if (!h1Seen) continue;
|
|
490
|
+
if (line.startsWith("#")) break;
|
|
491
|
+
const trimmed = line.trim();
|
|
492
|
+
if (trimmed) {
|
|
493
|
+
description = trimmed;
|
|
494
|
+
break;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
const sections = extractSections(content);
|
|
498
|
+
const parametersBody = sections.get("Parameters") ?? "";
|
|
499
|
+
const schema = {};
|
|
500
|
+
for (const line of parametersBody.split("\n")) {
|
|
501
|
+
if (!line.startsWith("- ")) continue;
|
|
502
|
+
const m = line.match(/^- (\w+) \(([^)]+)\): (.+)/);
|
|
503
|
+
if (!m) continue;
|
|
504
|
+
const pName = m[1];
|
|
505
|
+
const typeStr = m[2].split(",")[0].trim();
|
|
506
|
+
const desc = m[3].trim();
|
|
507
|
+
schema[pName] = { type: typeStr, description: desc };
|
|
508
|
+
}
|
|
509
|
+
const handlerBody = sections.get("Handler") ?? "";
|
|
510
|
+
const handler = extractFencedBlock(handlerBody, "js") ?? void 0;
|
|
511
|
+
return { name, description, schema, handler };
|
|
512
|
+
}
|
|
513
|
+
function parsePromptFile(content) {
|
|
514
|
+
const lines = content.split("\n");
|
|
515
|
+
const h1Line = lines.find((l) => l.startsWith("# "));
|
|
516
|
+
const name = h1Line ? h1Line.slice(2).trim() : "";
|
|
517
|
+
let h1Seen = false;
|
|
518
|
+
let description = "";
|
|
519
|
+
for (const line of lines) {
|
|
520
|
+
if (line.startsWith("# ")) {
|
|
521
|
+
h1Seen = true;
|
|
522
|
+
continue;
|
|
523
|
+
}
|
|
524
|
+
if (!h1Seen) continue;
|
|
525
|
+
if (line.startsWith("#")) break;
|
|
526
|
+
const trimmed = line.trim();
|
|
527
|
+
if (trimmed) {
|
|
528
|
+
description = trimmed;
|
|
529
|
+
break;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
const sections = extractSections(content);
|
|
533
|
+
const argsBody = sections.get("Args") ?? "";
|
|
534
|
+
const args = {};
|
|
535
|
+
for (const line of argsBody.split("\n")) {
|
|
536
|
+
if (!line.startsWith("- ")) continue;
|
|
537
|
+
const m = line.match(/^- (\w+) \(([^)]+)\): .+/);
|
|
538
|
+
if (!m) continue;
|
|
539
|
+
args[m[1]] = { type: m[2].trim() };
|
|
540
|
+
}
|
|
541
|
+
const messageBody = sections.get("Message") ?? "";
|
|
542
|
+
const messages = [];
|
|
543
|
+
for (const line of messageBody.split("\n")) {
|
|
544
|
+
if (!line.startsWith("> ")) continue;
|
|
545
|
+
const colonIdx = line.indexOf(": ", 2);
|
|
546
|
+
if (colonIdx === -1) continue;
|
|
547
|
+
const role = line.slice(2, colonIdx).trim();
|
|
548
|
+
const msgContent = line.slice(colonIdx + 2).trim();
|
|
549
|
+
messages.push({ role, content: msgContent });
|
|
550
|
+
}
|
|
551
|
+
return { name, description, args, messages };
|
|
552
|
+
}
|
|
553
|
+
function parseServerDir(dir) {
|
|
554
|
+
const serverMdContent = readFileSync(join(dir, "server.md"), "utf-8");
|
|
555
|
+
const sections = extractSections(serverMdContent);
|
|
556
|
+
const configBody = sections.get("Config") ?? "";
|
|
557
|
+
const configKv = parseKvBlock(configBody);
|
|
558
|
+
const config = {
|
|
559
|
+
name: configKv["name"] ?? "",
|
|
560
|
+
transport: configKv["transport"] ?? "stdio",
|
|
561
|
+
...configKv["port"] ? { port: parseInt(configKv["port"], 10) } : {}
|
|
562
|
+
};
|
|
563
|
+
const resourcesBody = sections.get("Resources") ?? "";
|
|
564
|
+
const resources = parseResourcesSection(resourcesBody);
|
|
565
|
+
const toolsDir = join(dir, "tools");
|
|
566
|
+
const tools = [];
|
|
567
|
+
let toolFiles = [];
|
|
568
|
+
try {
|
|
569
|
+
toolFiles = readdirSync(toolsDir).filter((f) => f.endsWith(".md"));
|
|
570
|
+
} catch {
|
|
571
|
+
}
|
|
572
|
+
for (const file of toolFiles) {
|
|
573
|
+
tools.push(parseToolFile(readFileSync(join(toolsDir, file), "utf-8")));
|
|
574
|
+
}
|
|
575
|
+
const promptsDir = join(dir, "prompts");
|
|
576
|
+
const prompts = [];
|
|
577
|
+
let promptFiles = [];
|
|
578
|
+
try {
|
|
579
|
+
promptFiles = readdirSync(promptsDir).filter((f) => f.endsWith(".md"));
|
|
580
|
+
} catch {
|
|
581
|
+
}
|
|
582
|
+
for (const file of promptFiles) {
|
|
583
|
+
prompts.push(parsePromptFile(readFileSync(join(promptsDir, file), "utf-8")));
|
|
584
|
+
}
|
|
585
|
+
return { config, tools, prompts, resources };
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// src/compiler/serialize.ts
|
|
589
|
+
import { writeFileSync, mkdirSync } from "fs";
|
|
590
|
+
import { join as join2 } from "path";
|
|
591
|
+
function serializeResources(resources) {
|
|
592
|
+
return resources.map((r) => {
|
|
593
|
+
let fm = `uri: ${r.uri}`;
|
|
594
|
+
if (r.name) fm += `
|
|
595
|
+
name: ${r.name}`;
|
|
596
|
+
if (r.mimeType) fm += `
|
|
597
|
+
mimeType: ${r.mimeType}`;
|
|
598
|
+
return `${fm}
|
|
599
|
+
---
|
|
600
|
+
${r.content}`;
|
|
601
|
+
}).join("\n---\n");
|
|
602
|
+
}
|
|
603
|
+
function serializeServerDir(doc, dir) {
|
|
604
|
+
mkdirSync(join2(dir, "tools"), { recursive: true });
|
|
605
|
+
mkdirSync(join2(dir, "prompts"), { recursive: true });
|
|
606
|
+
let serverMd = "## Config\n";
|
|
607
|
+
serverMd += `name: ${doc.config.name}
|
|
608
|
+
`;
|
|
609
|
+
serverMd += `transport: ${doc.config.transport}
|
|
610
|
+
`;
|
|
611
|
+
if (doc.config.port !== void 0) serverMd += `port: ${doc.config.port}
|
|
612
|
+
`;
|
|
613
|
+
if (doc.resources.length > 0) {
|
|
614
|
+
serverMd += `
|
|
615
|
+
## Resources
|
|
616
|
+
${serializeResources(doc.resources)}
|
|
617
|
+
`;
|
|
618
|
+
} else {
|
|
619
|
+
serverMd += "\n## Resources\n";
|
|
620
|
+
}
|
|
621
|
+
writeFileSync(join2(dir, "server.md"), serverMd);
|
|
622
|
+
console.log("\u2713 server.md updated");
|
|
623
|
+
for (const tool of doc.tools) {
|
|
624
|
+
let md = `# ${tool.name}
|
|
625
|
+
|
|
626
|
+
${tool.description}
|
|
627
|
+
|
|
628
|
+
## Parameters
|
|
629
|
+
|
|
630
|
+
`;
|
|
631
|
+
for (const [pName, pDef] of Object.entries(tool.schema)) {
|
|
632
|
+
md += `- ${pName} (${pDef.type}): ${pDef.description ?? pName}
|
|
633
|
+
`;
|
|
634
|
+
}
|
|
635
|
+
if (tool.handler !== void 0) {
|
|
636
|
+
const cleanHandler = tool.handler.split("\n").filter((l) => !l.trim().startsWith("// @handler:") && !l.trim().startsWith("// @end-handler:")).join("\n").trim();
|
|
637
|
+
md += `
|
|
638
|
+
## Handler
|
|
639
|
+
|
|
640
|
+
\`\`\`js
|
|
641
|
+
${cleanHandler}
|
|
642
|
+
\`\`\`
|
|
643
|
+
`;
|
|
644
|
+
}
|
|
645
|
+
writeFileSync(join2(dir, "tools", `${tool.name}.md`), md);
|
|
646
|
+
}
|
|
647
|
+
for (const prompt of doc.prompts) {
|
|
648
|
+
let md = `# ${prompt.name}
|
|
649
|
+
|
|
650
|
+
`;
|
|
651
|
+
if (prompt.description) md += `${prompt.description}
|
|
652
|
+
|
|
653
|
+
`;
|
|
654
|
+
if (Object.keys(prompt.args).length > 0) {
|
|
655
|
+
md += "## Args\n\n";
|
|
656
|
+
for (const [argName, argDef] of Object.entries(prompt.args)) {
|
|
657
|
+
md += `- ${argName} (${argDef.type}): ${argName}
|
|
658
|
+
`;
|
|
659
|
+
}
|
|
660
|
+
md += "\n";
|
|
661
|
+
}
|
|
662
|
+
if (prompt.messages.length > 0) {
|
|
663
|
+
md += "## Message\n\n";
|
|
664
|
+
for (const msg of prompt.messages) {
|
|
665
|
+
md += `> ${msg.role}: ${msg.content}
|
|
666
|
+
`;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
writeFileSync(join2(dir, "prompts", `${prompt.name}.md`), md);
|
|
670
|
+
}
|
|
671
|
+
console.log(`\u2713 tools/ updated (${doc.tools.length} files)`);
|
|
672
|
+
console.log(`\u2713 prompts/ updated (${doc.prompts.length} files)`);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// src/compiler/merge-tools.ts
|
|
676
|
+
function dedent(s) {
|
|
677
|
+
const lines = s.split("\n");
|
|
678
|
+
const nonEmpty = lines.filter((l) => l.trim());
|
|
679
|
+
if (nonEmpty.length === 0) return s;
|
|
680
|
+
const minIndent = nonEmpty.reduce((min, l) => {
|
|
681
|
+
const m = l.match(/^([ \t]*)/);
|
|
682
|
+
return Math.min(min, m?.[1]?.length ?? 0);
|
|
683
|
+
}, Infinity);
|
|
684
|
+
return lines.map((l) => l.slice(minIndent === Infinity ? 0 : minIndent)).join("\n");
|
|
685
|
+
}
|
|
686
|
+
function extractHandlers(toolsJsContent) {
|
|
687
|
+
const result = /* @__PURE__ */ new Map();
|
|
688
|
+
const markerRegex = /[ \t]*\/\/ @handler:([^\n]+)\n([\s\S]*?)\n[ \t]*\/\/ @end-handler:[^\n]+/g;
|
|
689
|
+
let match;
|
|
690
|
+
while ((match = markerRegex.exec(toolsJsContent)) !== null) {
|
|
691
|
+
result.set(match[1].trim(), dedent(match[2]).trim());
|
|
692
|
+
}
|
|
693
|
+
if (result.size > 0) return result;
|
|
694
|
+
const nameRegex = /name:\s*["']([^"']+)["']/g;
|
|
695
|
+
while ((match = nameRegex.exec(toolsJsContent)) !== null) {
|
|
696
|
+
const toolName = match[1];
|
|
697
|
+
const handlerKeyIdx = toolsJsContent.indexOf("handler:", match.index);
|
|
698
|
+
if (handlerKeyIdx === -1) continue;
|
|
699
|
+
const expr = extractExpression(toolsJsContent, handlerKeyIdx + "handler:".length);
|
|
700
|
+
if (expr) result.set(toolName, expr);
|
|
701
|
+
}
|
|
702
|
+
return result;
|
|
703
|
+
}
|
|
704
|
+
function extractExpression(src, start) {
|
|
705
|
+
let i = start;
|
|
706
|
+
while (i < src.length && /[ \t]/.test(src[i])) i++;
|
|
707
|
+
const exprStart = i;
|
|
708
|
+
let depth = 0;
|
|
709
|
+
let inStr = null;
|
|
710
|
+
while (i < src.length) {
|
|
711
|
+
const ch = src[i];
|
|
712
|
+
if (inStr) {
|
|
713
|
+
if (ch === "\\") {
|
|
714
|
+
i += 2;
|
|
715
|
+
continue;
|
|
716
|
+
}
|
|
717
|
+
if (ch === inStr) inStr = null;
|
|
718
|
+
} else if (ch === '"' || ch === "'" || ch === "`") {
|
|
719
|
+
inStr = ch;
|
|
720
|
+
} else if (ch === "{" || ch === "(" || ch === "[") {
|
|
721
|
+
depth++;
|
|
722
|
+
} else if (ch === "}" || ch === ")" || ch === "]") {
|
|
723
|
+
depth--;
|
|
724
|
+
if (depth === 0) {
|
|
725
|
+
const rest = src.slice(i + 1);
|
|
726
|
+
const arrowMatch = rest.match(/^[ \t]*=>/);
|
|
727
|
+
if (arrowMatch) {
|
|
728
|
+
i += 1 + arrowMatch[0].length;
|
|
729
|
+
while (i < src.length && /[ \t]/.test(src[i])) i++;
|
|
730
|
+
continue;
|
|
731
|
+
}
|
|
732
|
+
return src.slice(exprStart, i + 1).trim();
|
|
733
|
+
}
|
|
734
|
+
} else if (depth === 0 && (ch === "," || ch === ";" || ch === "\n")) {
|
|
735
|
+
break;
|
|
736
|
+
}
|
|
737
|
+
i++;
|
|
738
|
+
}
|
|
739
|
+
if (i > exprStart) return src.slice(exprStart, i).trim();
|
|
740
|
+
return null;
|
|
741
|
+
}
|
|
742
|
+
function mergeHandlers(doc, existing) {
|
|
743
|
+
const stubbed = [];
|
|
744
|
+
const tools = doc.tools.map((tool) => {
|
|
745
|
+
if (tool.handler !== void 0) return tool;
|
|
746
|
+
const found = existing.get(tool.name);
|
|
747
|
+
if (found !== void 0) return { ...tool, handler: found };
|
|
748
|
+
stubbed.push(tool.name);
|
|
749
|
+
return {
|
|
750
|
+
...tool,
|
|
751
|
+
handler: `async () => ({ content: [{ type: 'text', text: 'TODO: implement ${tool.name}' }] })`
|
|
752
|
+
};
|
|
753
|
+
});
|
|
754
|
+
return { doc: { ...doc, tools }, stubbed };
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// src/commands/compile.ts
|
|
758
|
+
var zodTypeNameMap = {
|
|
759
|
+
ZodString: "string",
|
|
760
|
+
ZodNumber: "number",
|
|
761
|
+
ZodBoolean: "boolean"
|
|
762
|
+
};
|
|
763
|
+
function generateToolsJs(tools) {
|
|
764
|
+
const lines = [`import { z } from 'mcpill-runtime';
|
|
765
|
+
|
|
766
|
+
export default [`];
|
|
767
|
+
for (const tool of tools) {
|
|
768
|
+
const schemaLines = Object.entries(tool.schema).map(([key, def]) => {
|
|
769
|
+
const zodCall = def.description ? `z.${def.type}().describe(${JSON.stringify(def.description)})` : `z.${def.type}()`;
|
|
770
|
+
return ` ${key}: ${zodCall},`;
|
|
771
|
+
});
|
|
772
|
+
const handlerBody = (tool.handler ?? "").split("\n").map((l) => ` ${l}`).join("\n");
|
|
773
|
+
lines.push(
|
|
774
|
+
` {`,
|
|
775
|
+
` name: ${JSON.stringify(tool.name)},`,
|
|
776
|
+
` description: ${JSON.stringify(tool.description)},`,
|
|
777
|
+
` schema: {`,
|
|
778
|
+
...schemaLines,
|
|
779
|
+
` },`,
|
|
780
|
+
` handler:`,
|
|
781
|
+
` // @handler:${tool.name}`,
|
|
782
|
+
handlerBody,
|
|
783
|
+
` // @end-handler:${tool.name}`,
|
|
784
|
+
` ,`,
|
|
785
|
+
` },`
|
|
786
|
+
);
|
|
787
|
+
}
|
|
788
|
+
lines.push(`];`);
|
|
789
|
+
return lines.join("\n") + "\n";
|
|
790
|
+
}
|
|
791
|
+
function serializeResourcesMd(resources) {
|
|
792
|
+
if (resources.length === 0) return "";
|
|
793
|
+
return resources.map((r) => {
|
|
794
|
+
let fm = `uri: ${r.uri}`;
|
|
795
|
+
if (r.name) fm += `
|
|
796
|
+
name: ${r.name}`;
|
|
797
|
+
if (r.mimeType) fm += `
|
|
798
|
+
mimeType: ${r.mimeType}`;
|
|
799
|
+
return `${fm}
|
|
800
|
+
---
|
|
801
|
+
${r.content}`;
|
|
802
|
+
}).join("\n---\n") + "\n";
|
|
803
|
+
}
|
|
804
|
+
function findPillDirs(baseDir) {
|
|
805
|
+
return readdirSync2(baseDir, { withFileTypes: true }).filter(
|
|
806
|
+
(e) => e.isDirectory() && /^\.[a-z][a-z0-9-]*$/.test(e.name) && existsSync(join3(baseDir, e.name, "mcpill.config.json"))
|
|
807
|
+
).map((e) => join3(baseDir, e.name));
|
|
808
|
+
}
|
|
809
|
+
async function runCompile(opts) {
|
|
810
|
+
const baseDir = resolve(opts.dir ?? process.cwd());
|
|
811
|
+
if (opts.toMd) {
|
|
812
|
+
const pillDirs = findPillDirs(baseDir);
|
|
813
|
+
if (pillDirs.length === 0) {
|
|
814
|
+
throw new Error("No pill directories found \u2014 run mcpill compile first");
|
|
815
|
+
}
|
|
816
|
+
const mcpillDir2 = pillDirs[0];
|
|
817
|
+
const config = await loadConfig(mcpillDir2);
|
|
818
|
+
const resources = await loadResources(mcpillDir2);
|
|
819
|
+
const promptsPath = join3(mcpillDir2, "prompts.json");
|
|
820
|
+
const prompts = JSON.parse(readFileSync2(promptsPath, "utf-8"));
|
|
821
|
+
const toolsPath = join3(mcpillDir2, "tools.js");
|
|
822
|
+
const handlers = existsSync(toolsPath) ? extractHandlers(readFileSync2(toolsPath, "utf-8")) : /* @__PURE__ */ new Map();
|
|
823
|
+
let tools = [];
|
|
824
|
+
if (existsSync(toolsPath)) {
|
|
825
|
+
const mod = await import(pathToFileURL2(toolsPath).href);
|
|
826
|
+
tools = mod.default.map((t) => {
|
|
827
|
+
const schema = {};
|
|
828
|
+
for (const [key, val] of Object.entries(t.schema)) {
|
|
829
|
+
const def = val._def;
|
|
830
|
+
const type = zodTypeNameMap[def.typeName] ?? "string";
|
|
831
|
+
schema[key] = def.description ? { type, description: def.description } : { type };
|
|
832
|
+
}
|
|
833
|
+
return {
|
|
834
|
+
name: t.name,
|
|
835
|
+
description: t.description,
|
|
836
|
+
schema,
|
|
837
|
+
handler: handlers.get(t.name)
|
|
838
|
+
};
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
const doc2 = {
|
|
842
|
+
config: {
|
|
843
|
+
name: config.name,
|
|
844
|
+
transport: config.transport,
|
|
845
|
+
...config.port !== 3333 ? { port: config.port } : {}
|
|
846
|
+
},
|
|
847
|
+
tools,
|
|
848
|
+
prompts,
|
|
849
|
+
resources
|
|
850
|
+
};
|
|
851
|
+
serializeServerDir(doc2, baseDir);
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
const doc = parseServerDir(baseDir);
|
|
855
|
+
const mcpillDir = join3(baseDir, "." + doc.config.name);
|
|
856
|
+
const toolsJsPath = join3(mcpillDir, "tools.js");
|
|
857
|
+
const existing = existsSync(toolsJsPath) ? extractHandlers(readFileSync2(toolsJsPath, "utf-8")) : /* @__PURE__ */ new Map();
|
|
858
|
+
const { doc: mergedDoc, stubbed } = mergeHandlers(doc, existing);
|
|
859
|
+
if (opts.strict && stubbed.length > 0) {
|
|
860
|
+
throw new Error("Missing handlers for: " + stubbed.join(", "));
|
|
861
|
+
}
|
|
862
|
+
for (const name of stubbed) {
|
|
863
|
+
console.warn(`\u26A0 Stub generated for tool: ${name}`);
|
|
864
|
+
}
|
|
865
|
+
mkdirSync2(mcpillDir, { recursive: true });
|
|
866
|
+
writeFileSync2(join3(mcpillDir, "mcpill.config.json"), JSON.stringify(mergedDoc.config, null, 2));
|
|
867
|
+
writeFileSync2(join3(mcpillDir, "prompts.json"), JSON.stringify(mergedDoc.prompts, null, 2));
|
|
868
|
+
writeFileSync2(join3(mcpillDir, "resources.md"), serializeResourcesMd(mergedDoc.resources));
|
|
869
|
+
writeFileSync2(toolsJsPath, generateToolsJs(mergedDoc.tools));
|
|
870
|
+
const pillDirName = "." + doc.config.name;
|
|
871
|
+
console.log(`\u2713 ${pillDirName}/ updated from server.md, tools/, prompts/`);
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
// src/commands/pack.ts
|
|
875
|
+
import fs7 from "fs";
|
|
876
|
+
import path8 from "path";
|
|
877
|
+
var SERVER_ENTRY_TEMPLATE = "import { runPill } from 'mcpill-runtime';\nrunPill();\n";
|
|
878
|
+
function resolvePillDir(baseDir) {
|
|
879
|
+
const entries = fs7.readdirSync(baseDir, { withFileTypes: true });
|
|
880
|
+
const pill = entries.find(
|
|
881
|
+
(e) => e.isDirectory() && /^\.[a-z][a-z0-9-]*$/.test(e.name) && fs7.existsSync(path8.join(baseDir, e.name, "mcpill.config.json"))
|
|
882
|
+
);
|
|
883
|
+
if (!pill) {
|
|
884
|
+
throw new Error("No pill directory found \u2014 run mcpill compile first");
|
|
885
|
+
}
|
|
886
|
+
return path8.join(baseDir, pill.name);
|
|
887
|
+
}
|
|
888
|
+
async function runPack(dir) {
|
|
889
|
+
const baseDir = path8.resolve(dir);
|
|
890
|
+
await runValidate(baseDir);
|
|
891
|
+
const pillDir = resolvePillDir(baseDir);
|
|
892
|
+
const config = await loadConfig(pillDir);
|
|
893
|
+
const { name } = config;
|
|
894
|
+
fs7.mkdirSync(path8.join(baseDir, "bin"), { recursive: true });
|
|
895
|
+
fs7.writeFileSync(path8.join(baseDir, "bin", "server.js"), SERVER_ENTRY_TEMPLATE);
|
|
896
|
+
const pkgPath = path8.join(baseDir, "package.json");
|
|
897
|
+
let pkg2 = {};
|
|
898
|
+
if (fs7.existsSync(pkgPath)) {
|
|
899
|
+
pkg2 = JSON.parse(fs7.readFileSync(pkgPath, "utf-8"));
|
|
900
|
+
}
|
|
901
|
+
pkg2.type = "module";
|
|
902
|
+
pkg2.bin = { [name]: "bin/server.js" };
|
|
903
|
+
const deps = pkg2.dependencies ?? {};
|
|
904
|
+
if (!deps["mcpill-runtime"]) {
|
|
905
|
+
deps["mcpill-runtime"] = "^0.1.0";
|
|
906
|
+
}
|
|
907
|
+
pkg2.dependencies = deps;
|
|
908
|
+
fs7.writeFileSync(pkgPath, JSON.stringify(pkg2, null, 2) + "\n");
|
|
909
|
+
console.log(`\u2713 ${name} packed \u2014 bin/server.js + package.json ready`);
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// src/commands/publish.ts
|
|
913
|
+
import { spawnSync } from "child_process";
|
|
914
|
+
import path9 from "path";
|
|
915
|
+
async function runPublish(dir, access) {
|
|
916
|
+
const baseDir = path9.resolve(dir);
|
|
917
|
+
await runPack(baseDir);
|
|
918
|
+
const result = spawnSync("npm", ["publish", "--access", access], {
|
|
919
|
+
cwd: baseDir,
|
|
920
|
+
stdio: "inherit"
|
|
921
|
+
});
|
|
922
|
+
if (result.status !== 0) {
|
|
923
|
+
throw new Error("npm publish failed");
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
// src/cli.ts
|
|
928
|
+
var pkgDir = dirname(fileURLToPath(import.meta.url));
|
|
929
|
+
var pkg = JSON.parse(
|
|
930
|
+
readFileSync3(join4(pkgDir, "../package.json"), "utf-8")
|
|
931
|
+
);
|
|
932
|
+
var program = new Command();
|
|
933
|
+
program.name("mcpill").version(pkg.version);
|
|
934
|
+
program.command("init").description("Scaffold a new .mcpill/ directory").option("--dir <path>", "Target directory").action(async (opts) => {
|
|
935
|
+
await runInit(opts);
|
|
936
|
+
});
|
|
937
|
+
program.command("run").description("Start the MCP server").option("--transport <transport>", "Transport type: stdio or http").option("--port <n>", "Port number (HTTP only)", parseInt).option("--dir <path>", "Directory containing .mcpill/").action(
|
|
938
|
+
async (opts) => {
|
|
939
|
+
await runServer(opts);
|
|
940
|
+
}
|
|
941
|
+
);
|
|
942
|
+
program.command("validate").description("Validate .mcpill/ configuration").option("--dir <path>", "Directory containing .mcpill/").action(async (opts) => {
|
|
943
|
+
const { resolve: resolve2 } = await import("path");
|
|
944
|
+
await runValidate(resolve2(opts.dir ?? process.cwd()));
|
|
945
|
+
});
|
|
946
|
+
program.command("compile").description("Compile server.md \u2194 .mcpill/ files").option("--dir <path>", "Directory containing server.md and .mcpill/").option("--to-md", "Reverse: generate server.md from .mcpill/ files").option("--strict", "Error on missing tool handlers instead of generating stubs").action(async (opts) => {
|
|
947
|
+
await runCompile(opts);
|
|
948
|
+
});
|
|
949
|
+
program.command("pack").description("Prepare pill for npm distribution").option("--dir <path>", "pill project root", ".").action(({ dir }) => runPack(dir));
|
|
950
|
+
program.command("publish").description("Pack and publish pill to npm").option("--dir <path>", "pill project root", ".").option("--access <level>", "npm access level", "public").action(({ dir, access }) => runPublish(dir, access));
|
|
951
|
+
program.parseAsync(process.argv).catch((err) => {
|
|
952
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
953
|
+
process.exit(1);
|
|
954
|
+
});
|