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/src/commands/init.ts
DELETED
|
@@ -1,353 +0,0 @@
|
|
|
1
|
-
import fs from "fs";
|
|
2
|
-
import path from "path";
|
|
3
|
-
|
|
4
|
-
const PILL_AGENT_GUIDE_MD = `# pill-agent-guide
|
|
5
|
-
|
|
6
|
-
Instructions for building an mcpill MCP server from a filled \`PILL.md\`.
|
|
7
|
-
|
|
8
|
-
---
|
|
9
|
-
|
|
10
|
-
## Steps
|
|
11
|
-
|
|
12
|
-
1. **Parse** the sections in \`PILL.md\`:
|
|
13
|
-
- \`## Server\` → server name, description, transport (+ port if http)
|
|
14
|
-
- \`## Tool: <name>\` → one tool per section (repeatable)
|
|
15
|
-
- \`## Resource: <name>\` → static resource (optional, repeatable)
|
|
16
|
-
- \`## Prompt: <name>\` → prompt template (optional, repeatable)
|
|
17
|
-
|
|
18
|
-
2. **Write** the server files into the pre-created scaffold at \`.mcpill/\`:
|
|
19
|
-
|
|
20
|
-
\`.mcpill/server.md\`
|
|
21
|
-
\`\`\`
|
|
22
|
-
## Config
|
|
23
|
-
name: {name}
|
|
24
|
-
transport: {transport}
|
|
25
|
-
port: {port} ← only if transport is http
|
|
26
|
-
|
|
27
|
-
## Resources
|
|
28
|
-
uri: {uri}
|
|
29
|
-
name: {resource-name}
|
|
30
|
-
---
|
|
31
|
-
{content}
|
|
32
|
-
\`\`\`
|
|
33
|
-
Omit \`## Resources\` entirely if none were specified.
|
|
34
|
-
Separate multiple resources with a blank line + \`---\` + blank line.
|
|
35
|
-
|
|
36
|
-
\`.mcpill/server/tools/{tool-name}.md\` — one file per \`## Tool\` section:
|
|
37
|
-
\`\`\`
|
|
38
|
-
# {tool-name}
|
|
39
|
-
|
|
40
|
-
{description}
|
|
41
|
-
|
|
42
|
-
## Parameters
|
|
43
|
-
|
|
44
|
-
- {param} ({type}): {description}
|
|
45
|
-
|
|
46
|
-
## Handler
|
|
47
|
-
|
|
48
|
-
\`\`\`js
|
|
49
|
-
async ({ {param} }) => {
|
|
50
|
-
return { content: [{ type: "text", text: {result} }] };
|
|
51
|
-
}
|
|
52
|
-
\`\`\`
|
|
53
|
-
\`\`\`
|
|
54
|
-
|
|
55
|
-
\`.mcpill/server/prompts/{prompt-name}.md\` — one file per \`## Prompt\` section:
|
|
56
|
-
\`\`\`
|
|
57
|
-
# {prompt-name}
|
|
58
|
-
|
|
59
|
-
{description}
|
|
60
|
-
|
|
61
|
-
## Args
|
|
62
|
-
|
|
63
|
-
- {arg} ({type}): {description}
|
|
64
|
-
|
|
65
|
-
## Message
|
|
66
|
-
|
|
67
|
-
> user: {message with {{arg}} placeholders}
|
|
68
|
-
\`\`\`
|
|
69
|
-
Omit \`## Args\` if the prompt takes no arguments.
|
|
70
|
-
|
|
71
|
-
3. **Translate** each tool's \`behavior\` field into a working JS async handler.
|
|
72
|
-
- Implement the described logic fully — no stubs, no TODOs.
|
|
73
|
-
- Handler is self-contained: no top-level imports.
|
|
74
|
-
- For file I/O or network calls: use dynamic import inside the handler.
|
|
75
|
-
- Return shape: \`{ content: [{ type: "text", text: string }] }\`
|
|
76
|
-
- Allowed parameter types: \`string | number | boolean\`. Default unknown types to \`string\`.
|
|
77
|
-
|
|
78
|
-
4. **Run** \`mcpill compile\`.
|
|
79
|
-
|
|
80
|
-
5. **Report** what was generated and show the user the next commands:
|
|
81
|
-
\`\`\`
|
|
82
|
-
mcpill validate
|
|
83
|
-
mcpill run
|
|
84
|
-
mcpill pack
|
|
85
|
-
mcpill publish
|
|
86
|
-
\`\`\`
|
|
87
|
-
|
|
88
|
-
---
|
|
89
|
-
|
|
90
|
-
## Edge cases
|
|
91
|
-
|
|
92
|
-
| Situation | Handling |
|
|
93
|
-
|---|---|
|
|
94
|
-
| \`transport: http\` without port | Default to \`3333\` |
|
|
95
|
-
| Prompt message without role prefix | Wrap as \`> user: {message}\` |
|
|
96
|
-
| Server name with spaces | Convert to kebab-case for \`name\` field |
|
|
97
|
-
| Input type not \`string\\|number\\|boolean\` | Default to \`string\`, note the assumption |
|
|
98
|
-
| Vague \`behavior\` field | Implement the most literal reasonable interpretation |
|
|
99
|
-
`;
|
|
100
|
-
|
|
101
|
-
function makePillMd(projectName: string): string {
|
|
102
|
-
return `<!-- ═══════════════════════════════════════════════════════════════
|
|
103
|
-
FOR THE AGENT — read this before doing anything else
|
|
104
|
-
═══════════════════════════════════════════════════════════════
|
|
105
|
-
|
|
106
|
-
You are building an mcpill MCP server from this document.
|
|
107
|
-
Follow the steps in .mcpill/pill-agent-guide.md.
|
|
108
|
-
|
|
109
|
-
════════════════════════════════════════════════════════════════ -->
|
|
110
|
-
|
|
111
|
-
# Pill: ${projectName}
|
|
112
|
-
|
|
113
|
-
---
|
|
114
|
-
|
|
115
|
-
## Server
|
|
116
|
-
|
|
117
|
-
name: ${projectName}
|
|
118
|
-
description:
|
|
119
|
-
transport: stdio
|
|
120
|
-
|
|
121
|
-
---
|
|
122
|
-
|
|
123
|
-
## Tool: my-tool
|
|
124
|
-
|
|
125
|
-
description: Describe what this tool does.
|
|
126
|
-
inputs:
|
|
127
|
-
- param (string): A parameter.
|
|
128
|
-
output: What the tool returns.
|
|
129
|
-
behavior: |
|
|
130
|
-
Describe the logic here. Claude will implement it.
|
|
131
|
-
|
|
132
|
-
---
|
|
133
|
-
|
|
134
|
-
## Next Steps
|
|
135
|
-
|
|
136
|
-
Fill in your server above (replace or extend the example tools), then tell Claude:
|
|
137
|
-
|
|
138
|
-
> "Build this PILL.md"
|
|
139
|
-
|
|
140
|
-
After the source files are generated:
|
|
141
|
-
|
|
142
|
-
- [ ] \`mcpill compile\`
|
|
143
|
-
- [ ] \`mcpill validate\`
|
|
144
|
-
- [ ] \`mcpill run\`
|
|
145
|
-
- [ ] \`mcpill pack\`
|
|
146
|
-
- [ ] \`mcpill publish\`
|
|
147
|
-
`;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const HELLO_MCP_MD = `<!-- ═══════════════════════════════════════════════════════════════
|
|
151
|
-
HELLO-MCP — a ready-to-run example
|
|
152
|
-
|
|
153
|
-
Want to see mcpill in action before building your own server?
|
|
154
|
-
|
|
155
|
-
1. Copy the content below into your PILL.md (replace everything).
|
|
156
|
-
2. Tell Claude: "Build this PILL.md"
|
|
157
|
-
3. Run: mcpill compile && mcpill validate && mcpill run
|
|
158
|
-
═══════════════════════════════════════════════════════════════ -->
|
|
159
|
-
|
|
160
|
-
# Pill: hello-mcp
|
|
161
|
-
|
|
162
|
-
---
|
|
163
|
-
|
|
164
|
-
## Server
|
|
165
|
-
|
|
166
|
-
name: hello-mcp
|
|
167
|
-
description: A hello-world MCP server — see mcpill in action.
|
|
168
|
-
transport: stdio
|
|
169
|
-
|
|
170
|
-
---
|
|
171
|
-
|
|
172
|
-
## Resource: server-info
|
|
173
|
-
|
|
174
|
-
uri: info://server
|
|
175
|
-
name: Server Info
|
|
176
|
-
content: |
|
|
177
|
-
hello-mcp is a demo MCP server built with mcpill.
|
|
178
|
-
Tools: ping, echo. Prompt: introduce.
|
|
179
|
-
|
|
180
|
-
---
|
|
181
|
-
|
|
182
|
-
## Tool: ping
|
|
183
|
-
|
|
184
|
-
description: Returns pong with the current UTC timestamp.
|
|
185
|
-
inputs:
|
|
186
|
-
output: A pong message with the current timestamp.
|
|
187
|
-
behavior: |
|
|
188
|
-
Return the string "pong — " followed by new Date().toISOString().
|
|
189
|
-
|
|
190
|
-
---
|
|
191
|
-
|
|
192
|
-
## Tool: echo
|
|
193
|
-
|
|
194
|
-
description: Echoes a message back to the caller.
|
|
195
|
-
inputs:
|
|
196
|
-
- message (string): The message to echo.
|
|
197
|
-
output: The same message, echoed back as text.
|
|
198
|
-
behavior: |
|
|
199
|
-
Return the message input unchanged.
|
|
200
|
-
|
|
201
|
-
---
|
|
202
|
-
|
|
203
|
-
## Prompt: introduce
|
|
204
|
-
|
|
205
|
-
description: Ask the server to introduce itself.
|
|
206
|
-
behavior: |
|
|
207
|
-
A prompt (no args) with a single user message:
|
|
208
|
-
"Introduce yourself — what is hello-mcp and what can it do?"
|
|
209
|
-
`;
|
|
210
|
-
|
|
211
|
-
// Embedded content of src/templates/server.md — tsup does not copy non-TS assets,
|
|
212
|
-
// so the template is inlined here. Keep in sync with src/templates/server.md.
|
|
213
|
-
const SERVER_MD_TEMPLATE = `## Config
|
|
214
|
-
name: my-server
|
|
215
|
-
transport: stdio
|
|
216
|
-
|
|
217
|
-
## Resources
|
|
218
|
-
uri: info://status
|
|
219
|
-
name: Status
|
|
220
|
-
---
|
|
221
|
-
The server is running.
|
|
222
|
-
`;
|
|
223
|
-
|
|
224
|
-
const GREETING_PROMPT_MD = `# greeting
|
|
225
|
-
|
|
226
|
-
Generate a greeting
|
|
227
|
-
|
|
228
|
-
## Args
|
|
229
|
-
|
|
230
|
-
- name (string): The name to greet
|
|
231
|
-
|
|
232
|
-
## Message
|
|
233
|
-
|
|
234
|
-
> user: Say hello to {{name}}
|
|
235
|
-
`;
|
|
236
|
-
|
|
237
|
-
function makePillUserGuideMd(projectName: string): string {
|
|
238
|
-
return `# ${projectName} — mcpill user guide
|
|
239
|
-
|
|
240
|
-
## Quickstart
|
|
241
|
-
|
|
242
|
-
1. **Describe your server** in \`PILL.md\` — fill in the \`## Server\` block and add \`## Tool\` sections.
|
|
243
|
-
2. **Build it** — open Claude Code and say:
|
|
244
|
-
> "Build this PILL.md"
|
|
245
|
-
Claude reads \`.mcpill/pill-agent-guide.md\` and generates the source files.
|
|
246
|
-
3. **Compile**
|
|
247
|
-
\`\`\`sh
|
|
248
|
-
mcpill compile
|
|
249
|
-
\`\`\`
|
|
250
|
-
4. **Test locally**
|
|
251
|
-
\`\`\`sh
|
|
252
|
-
mcpill validate # schema check
|
|
253
|
-
mcpill run # start the server
|
|
254
|
-
\`\`\`
|
|
255
|
-
5. **Pack & publish**
|
|
256
|
-
\`\`\`sh
|
|
257
|
-
mcpill pack
|
|
258
|
-
mcpill publish
|
|
259
|
-
\`\`\`
|
|
260
|
-
|
|
261
|
-
## Project layout
|
|
262
|
-
|
|
263
|
-
\`\`\`
|
|
264
|
-
${projectName}/
|
|
265
|
-
├── PILL.md ← describe your server here
|
|
266
|
-
└── .mcpill/
|
|
267
|
-
├── server.md ← server config (name, transport, resources)
|
|
268
|
-
├── pill-agent-guide.md ← instructions Claude follows when building
|
|
269
|
-
├── pill-user-guide.md ← this file
|
|
270
|
-
└── server/
|
|
271
|
-
├── mcpill.config.json ← compiled config
|
|
272
|
-
├── tools/ ← one .md file per tool
|
|
273
|
-
│ └── my-tool.md
|
|
274
|
-
└── prompts/ ← one .md file per prompt (optional)
|
|
275
|
-
└── greeting.md
|
|
276
|
-
\`\`\`
|
|
277
|
-
|
|
278
|
-
## Editing tools manually
|
|
279
|
-
|
|
280
|
-
Each file in \`.mcpill/server/tools/\` follows this shape:
|
|
281
|
-
|
|
282
|
-
\`\`\`markdown
|
|
283
|
-
# tool-name
|
|
284
|
-
|
|
285
|
-
One-line description.
|
|
286
|
-
|
|
287
|
-
## Parameters
|
|
288
|
-
|
|
289
|
-
- param (string): what it is
|
|
290
|
-
|
|
291
|
-
## Handler
|
|
292
|
-
|
|
293
|
-
\`\`\`js
|
|
294
|
-
async ({ param }) => {
|
|
295
|
-
return { content: [{ type: "text", text: param }] };
|
|
296
|
-
}
|
|
297
|
-
\`\`\`
|
|
298
|
-
\`\`\`
|
|
299
|
-
|
|
300
|
-
Run \`mcpill compile\` after any edit to regenerate the server bundle.
|
|
301
|
-
`;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
export async function runInit(opts: { dir?: string }): Promise<void> {
|
|
305
|
-
const targetDir = path.resolve(opts.dir ?? process.cwd());
|
|
306
|
-
const mcpillDir = path.join(targetDir, ".mcpill");
|
|
307
|
-
const serverDir = path.join(mcpillDir, "server");
|
|
308
|
-
const projectName = path.basename(targetDir);
|
|
309
|
-
|
|
310
|
-
if (fs.existsSync(mcpillDir)) {
|
|
311
|
-
console.error(".mcpill/ already exists. Remove it manually to re-init.");
|
|
312
|
-
process.exit(1);
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
fs.mkdirSync(path.join(serverDir, "tools"), { recursive: true });
|
|
316
|
-
fs.mkdirSync(path.join(serverDir, "prompts"), { recursive: true });
|
|
317
|
-
|
|
318
|
-
const serverMd = SERVER_MD_TEMPLATE.replace("name: my-server", `name: ${projectName}`);
|
|
319
|
-
fs.writeFileSync(path.join(mcpillDir, "server.md"), serverMd);
|
|
320
|
-
fs.writeFileSync(path.join(serverDir, "prompts", "greeting.md"), GREETING_PROMPT_MD);
|
|
321
|
-
fs.writeFileSync(
|
|
322
|
-
path.join(serverDir, "mcpill.config.json"),
|
|
323
|
-
JSON.stringify({ name: projectName, transport: "stdio", port: 3333 }, null, 2),
|
|
324
|
-
);
|
|
325
|
-
fs.writeFileSync(path.join(mcpillDir, "pill-agent-guide.md"), PILL_AGENT_GUIDE_MD);
|
|
326
|
-
fs.writeFileSync(path.join(mcpillDir, "pill-user-guide.md"), makePillUserGuideMd(projectName));
|
|
327
|
-
fs.writeFileSync(path.join(mcpillDir, "HELLO-MCP.md"), HELLO_MCP_MD);
|
|
328
|
-
|
|
329
|
-
fs.writeFileSync(path.join(targetDir, "PILL.md"), makePillMd(projectName));
|
|
330
|
-
|
|
331
|
-
const pkgJsonPath = path.join(targetDir, "package.json");
|
|
332
|
-
if (!fs.existsSync(pkgJsonPath)) {
|
|
333
|
-
const pkg = {
|
|
334
|
-
name: projectName,
|
|
335
|
-
version: "0.1.0",
|
|
336
|
-
type: "module",
|
|
337
|
-
dependencies: {
|
|
338
|
-
"mcpill-runtime": "latest",
|
|
339
|
-
},
|
|
340
|
-
};
|
|
341
|
-
fs.writeFileSync(pkgJsonPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
console.log("✓ PILL.md ← describe your server, then tell Claude: \"Build this PILL.md\"");
|
|
345
|
-
console.log("✓ package.json ← Run: npm install");
|
|
346
|
-
console.log("✓ .mcpill/HELLO-MCP.md ← copy into PILL.md to see a working example instantly");
|
|
347
|
-
console.log("✓ .mcpill/server.md ← or edit source files directly");
|
|
348
|
-
console.log("✓ .mcpill/server/prompts/greeting.md");
|
|
349
|
-
console.log("✓ .mcpill/pill-agent-guide.md ← agent instructions (read by Claude)");
|
|
350
|
-
console.log("✓ .mcpill/pill-user-guide.md ← start here");
|
|
351
|
-
console.log("");
|
|
352
|
-
console.log("Next: npm install && mcpill compile && mcpill run");
|
|
353
|
-
}
|
package/src/commands/pack.ts
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
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
|
-
export async function runPack(dir: string): Promise<void> {
|
|
10
|
-
const baseDir = path.resolve(dir);
|
|
11
|
-
await runValidate(baseDir);
|
|
12
|
-
|
|
13
|
-
const pillDir = path.join(baseDir, ".mcpill", "server");
|
|
14
|
-
const config = await loadConfig(pillDir);
|
|
15
|
-
const { name } = config;
|
|
16
|
-
|
|
17
|
-
fs.mkdirSync(path.join(baseDir, "bin"), { recursive: true });
|
|
18
|
-
fs.writeFileSync(path.join(baseDir, "bin", "server.js"), SERVER_ENTRY_TEMPLATE);
|
|
19
|
-
|
|
20
|
-
const pkgPath = path.join(baseDir, "package.json");
|
|
21
|
-
let pkg: Record<string, unknown> = {};
|
|
22
|
-
if (fs.existsSync(pkgPath)) {
|
|
23
|
-
pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8")) as Record<string, unknown>;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
pkg.type = "module";
|
|
27
|
-
pkg.bin = { [name]: "bin/server.js" };
|
|
28
|
-
|
|
29
|
-
const deps = (pkg.dependencies as Record<string, string> | undefined) ?? {};
|
|
30
|
-
if (!deps["mcpill-runtime"]) {
|
|
31
|
-
deps["mcpill-runtime"] = "^0.1.0";
|
|
32
|
-
}
|
|
33
|
-
pkg.dependencies = deps;
|
|
34
|
-
|
|
35
|
-
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
36
|
-
|
|
37
|
-
console.log(`✓ ${name} packed — bin/server.js + package.json ready`);
|
|
38
|
-
}
|
package/src/commands/publish.ts
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
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
|
-
}
|
package/src/commands/run.ts
DELETED
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
import path from "path";
|
|
2
|
-
import fs from "fs";
|
|
3
|
-
import { z } from "mcpill-runtime";
|
|
4
|
-
import { createServer, applySetup } from "mcpster";
|
|
5
|
-
import { loadConfig } from "../loaders/config.js";
|
|
6
|
-
import { loadTools } from "../loaders/tools.js";
|
|
7
|
-
import { loadPrompts } from "../loaders/prompts.js";
|
|
8
|
-
import { loadResources } from "../loaders/resources.js";
|
|
9
|
-
import { runValidate } from "./validate.js";
|
|
10
|
-
|
|
11
|
-
type ZodRawShape = Record<string, z.ZodTypeAny>;
|
|
12
|
-
|
|
13
|
-
export async function runServer(opts: {
|
|
14
|
-
dir?: string;
|
|
15
|
-
transport?: "stdio" | "http";
|
|
16
|
-
port?: number;
|
|
17
|
-
}): Promise<void> {
|
|
18
|
-
const baseDir = path.resolve(opts.dir ?? process.cwd());
|
|
19
|
-
|
|
20
|
-
await runValidate(baseDir);
|
|
21
|
-
|
|
22
|
-
const mcpillDir = path.join(baseDir, ".mcpill", "server");
|
|
23
|
-
if (!fs.existsSync(path.join(mcpillDir, "mcpill.config.json"))) {
|
|
24
|
-
console.error("No pill found — run mcpill compile first");
|
|
25
|
-
process.exit(1);
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
const [tools, prompts, resources, config] = await Promise.all([
|
|
29
|
-
loadTools(mcpillDir),
|
|
30
|
-
loadPrompts(mcpillDir),
|
|
31
|
-
loadResources(mcpillDir),
|
|
32
|
-
loadConfig(mcpillDir),
|
|
33
|
-
]);
|
|
34
|
-
|
|
35
|
-
const transport = opts.transport ?? config.transport;
|
|
36
|
-
const port = opts.port ?? config.port;
|
|
37
|
-
const { name } = config;
|
|
38
|
-
|
|
39
|
-
const server = createServer({
|
|
40
|
-
name,
|
|
41
|
-
version: "0.1.0",
|
|
42
|
-
transport,
|
|
43
|
-
http: { port },
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
for (const { name: toolName, description, schema, handler } of tools) {
|
|
47
|
-
server.defineTool({
|
|
48
|
-
name: toolName,
|
|
49
|
-
description,
|
|
50
|
-
schema: z.object(schema as ZodRawShape),
|
|
51
|
-
handler: handler as (input: Record<string, unknown>) => Promise<unknown>,
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
for (const { name: promptName, description, messages } of prompts) {
|
|
56
|
-
server.definePrompt({
|
|
57
|
-
name: promptName,
|
|
58
|
-
description,
|
|
59
|
-
handler: async (args: Record<string, string>) =>
|
|
60
|
-
messages
|
|
61
|
-
.map((m) =>
|
|
62
|
-
m.content.replace(
|
|
63
|
-
/\{\{(\w+)\}\}/g,
|
|
64
|
-
(_, k: string) => String(args[k] ?? "")
|
|
65
|
-
)
|
|
66
|
-
)
|
|
67
|
-
.join("\n"),
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
for (const { uri, name: resourceName, content } of resources) {
|
|
72
|
-
server.defineResource({
|
|
73
|
-
uri,
|
|
74
|
-
description: resourceName ?? uri,
|
|
75
|
-
resolver: async () => content,
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
applySetup(name, tools.map((t) => t.name), {
|
|
80
|
-
projectPath: baseDir,
|
|
81
|
-
permissions: 'restrictive',
|
|
82
|
-
register: true,
|
|
83
|
-
cmdOverride: {
|
|
84
|
-
command: 'mcpill',
|
|
85
|
-
args: ['run', '--dir', baseDir],
|
|
86
|
-
},
|
|
87
|
-
})
|
|
88
|
-
|
|
89
|
-
try {
|
|
90
|
-
await server.start();
|
|
91
|
-
} catch (err) {
|
|
92
|
-
const nodeErr = err as NodeJS.ErrnoException;
|
|
93
|
-
if (nodeErr.code === "EADDRINUSE") {
|
|
94
|
-
console.error(
|
|
95
|
-
`Port ${port} is already in use. Use --port to specify another.`
|
|
96
|
-
);
|
|
97
|
-
process.exit(1);
|
|
98
|
-
}
|
|
99
|
-
throw err;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
process.stderr.write(
|
|
103
|
-
`✓ MCPill running — ${tools.length} tools, ${prompts.length} prompts, ${resources.length} resources [${transport}]\n`
|
|
104
|
-
);
|
|
105
|
-
}
|
package/src/commands/validate.ts
DELETED
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import fs from "fs";
|
|
2
|
-
import path from "path";
|
|
3
|
-
import { loadConfig } from "../loaders/config.js";
|
|
4
|
-
import { loadTools } from "../loaders/tools.js";
|
|
5
|
-
import { loadPrompts } from "../loaders/prompts.js";
|
|
6
|
-
import { loadResources } from "../loaders/resources.js";
|
|
7
|
-
|
|
8
|
-
async function validateOne(mcpillDir: string): Promise<void> {
|
|
9
|
-
const errors: string[] = [];
|
|
10
|
-
let toolCount = 0;
|
|
11
|
-
let promptCount = 0;
|
|
12
|
-
let resourceCount = 0;
|
|
13
|
-
|
|
14
|
-
try {
|
|
15
|
-
const tools = await loadTools(mcpillDir);
|
|
16
|
-
toolCount = tools.length;
|
|
17
|
-
} catch (err) {
|
|
18
|
-
errors.push(err instanceof Error ? err.message : String(err));
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
try {
|
|
22
|
-
const prompts = await loadPrompts(mcpillDir);
|
|
23
|
-
promptCount = prompts.length;
|
|
24
|
-
} catch (err) {
|
|
25
|
-
errors.push(err instanceof Error ? err.message : String(err));
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
try {
|
|
29
|
-
const resources = await loadResources(mcpillDir);
|
|
30
|
-
resourceCount = resources.length;
|
|
31
|
-
} catch (err) {
|
|
32
|
-
errors.push(err instanceof Error ? err.message : String(err));
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
try {
|
|
36
|
-
await loadConfig(mcpillDir);
|
|
37
|
-
} catch (err) {
|
|
38
|
-
errors.push(err instanceof Error ? err.message : String(err));
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
if (errors.length > 0) {
|
|
42
|
-
for (const error of errors) {
|
|
43
|
-
console.error(error);
|
|
44
|
-
}
|
|
45
|
-
process.exit(1);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
console.log(`✓ Valid: ${toolCount} tools, ${promptCount} prompts, ${resourceCount} resources`);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export async function runValidate(baseDir: string): Promise<void> {
|
|
52
|
-
const serverDir = path.join(baseDir, ".mcpill", "server");
|
|
53
|
-
if (!fs.existsSync(path.join(serverDir, "mcpill.config.json"))) {
|
|
54
|
-
console.error("No pill directories found — run mcpill compile first");
|
|
55
|
-
process.exit(1);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
await validateOne(serverDir);
|
|
59
|
-
}
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
import type { ServerDoc } from './types.js';
|
|
2
|
-
|
|
3
|
-
function dedent(s: string): string {
|
|
4
|
-
const lines = s.split('\n');
|
|
5
|
-
const nonEmpty = lines.filter((l) => l.trim());
|
|
6
|
-
if (nonEmpty.length === 0) return s;
|
|
7
|
-
const minIndent = nonEmpty.reduce((min, l) => {
|
|
8
|
-
const m = l.match(/^([ \t]*)/);
|
|
9
|
-
return Math.min(min, m?.[1]?.length ?? 0);
|
|
10
|
-
}, Infinity);
|
|
11
|
-
return lines.map((l) => l.slice(minIndent === Infinity ? 0 : minIndent)).join('\n');
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function extractHandlers(toolsJsContent: string): Map<string, string> {
|
|
15
|
-
const result = new Map<string, string>();
|
|
16
|
-
|
|
17
|
-
// Marker-based extraction: // @handler:name ... // @end-handler:name
|
|
18
|
-
// Allow leading whitespace before the // so indented markers in tools.js are matched.
|
|
19
|
-
const markerRegex = /[ \t]*\/\/ @handler:([^\n]+)\n([\s\S]*?)\n[ \t]*\/\/ @end-handler:[^\n]+/g;
|
|
20
|
-
let match: RegExpExecArray | null;
|
|
21
|
-
while ((match = markerRegex.exec(toolsJsContent)) !== null) {
|
|
22
|
-
result.set(match[1]!.trim(), dedent(match[2]!).trim());
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
if (result.size > 0) return result;
|
|
26
|
-
|
|
27
|
-
// Fallback: find name: "toolname" then locate handler: expression via brace counting
|
|
28
|
-
const nameRegex = /name:\s*["']([^"']+)["']/g;
|
|
29
|
-
while ((match = nameRegex.exec(toolsJsContent)) !== null) {
|
|
30
|
-
const toolName = match[1]!;
|
|
31
|
-
const handlerKeyIdx = toolsJsContent.indexOf('handler:', match.index);
|
|
32
|
-
if (handlerKeyIdx === -1) continue;
|
|
33
|
-
const expr = extractExpression(toolsJsContent, handlerKeyIdx + 'handler:'.length);
|
|
34
|
-
if (expr) result.set(toolName, expr);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
return result;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function extractExpression(src: string, start: number): string | null {
|
|
41
|
-
let i = start;
|
|
42
|
-
while (i < src.length && /[ \t]/.test(src[i]!)) i++;
|
|
43
|
-
const exprStart = i;
|
|
44
|
-
let depth = 0;
|
|
45
|
-
let inStr: string | null = null;
|
|
46
|
-
|
|
47
|
-
while (i < src.length) {
|
|
48
|
-
const ch = src[i]!;
|
|
49
|
-
if (inStr) {
|
|
50
|
-
if (ch === '\\') { i += 2; continue; }
|
|
51
|
-
if (ch === inStr) inStr = null;
|
|
52
|
-
} else if (ch === '"' || ch === "'" || ch === '`') {
|
|
53
|
-
inStr = ch;
|
|
54
|
-
} else if (ch === '{' || ch === '(' || ch === '[') {
|
|
55
|
-
depth++;
|
|
56
|
-
} else if (ch === '}' || ch === ')' || ch === ']') {
|
|
57
|
-
depth--;
|
|
58
|
-
if (depth === 0) {
|
|
59
|
-
// Peek ahead: if followed by => this is an arrow function params group,
|
|
60
|
-
// so continue scanning to capture the body too.
|
|
61
|
-
const rest = src.slice(i + 1);
|
|
62
|
-
const arrowMatch = rest.match(/^[ \t]*=>/);
|
|
63
|
-
if (arrowMatch) {
|
|
64
|
-
i += 1 + arrowMatch[0].length;
|
|
65
|
-
while (i < src.length && /[ \t]/.test(src[i]!)) i++;
|
|
66
|
-
// Reset depth — next balanced group is the body
|
|
67
|
-
continue;
|
|
68
|
-
}
|
|
69
|
-
return src.slice(exprStart, i + 1).trim();
|
|
70
|
-
}
|
|
71
|
-
} else if (depth === 0 && (ch === ',' || ch === ';' || ch === '\n')) {
|
|
72
|
-
break;
|
|
73
|
-
}
|
|
74
|
-
i++;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
if (i > exprStart) return src.slice(exprStart, i).trim();
|
|
78
|
-
return null;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export function mergeHandlers(
|
|
82
|
-
doc: ServerDoc,
|
|
83
|
-
existing: Map<string, string>,
|
|
84
|
-
): { doc: ServerDoc; stubbed: string[] } {
|
|
85
|
-
const stubbed: string[] = [];
|
|
86
|
-
|
|
87
|
-
const tools = doc.tools.map((tool) => {
|
|
88
|
-
if (tool.handler !== undefined) return tool;
|
|
89
|
-
const found = existing.get(tool.name);
|
|
90
|
-
if (found !== undefined) return { ...tool, handler: found };
|
|
91
|
-
stubbed.push(tool.name);
|
|
92
|
-
return {
|
|
93
|
-
...tool,
|
|
94
|
-
handler: `async () => ({ content: [{ type: 'text', text: 'TODO: implement ${tool.name}' }] })`,
|
|
95
|
-
};
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
return { doc: { ...doc, tools }, stubbed };
|
|
99
|
-
}
|