mcpill 1.2.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,9 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.2.1
4
+
5
+ - All init assets scoped under `.mcpill/` — only `PILL.md` is written to the project root
6
+ - `mcpill init` scaffolds `pill-user-guide.md` inside `.mcpill/` — covers quickstart, project layout, and manual tool editing; replaces any need for a root-level README
7
+ - `HELLO-MCP.md` added to `.mcpill/` — a ready-to-run example developers can copy into `PILL.md`
8
+
3
9
  ## 1.2.0
4
10
 
5
11
  - `mcpill init` now scaffolds `PILL.md` — an intent-level spec the developer fills in plain English
6
- - `PILL.md` embeds agent instructions; tell Claude "Build this PILL.md" to generate source files and compile
12
+ - `mcpill init` generates `pill-agent-guide.md` with the full agent instructions for building from `PILL.md`
13
+ - `PILL.md` references `pill-agent-guide.md`; tell Claude "Build this PILL.md" to generate source files and compile
7
14
  - Example source files (`tools/echo.md`, `prompts/greeting.md`, `server.md`) still scaffolded for direct editing
8
15
  - Removed `tools.js`, `prompts.json`, `resources.md` from `.mcpill/` initial scaffold (generated by `mcpill compile`)
9
16
  - Updated `mcpill init` console output to guide both entry points
package/README.md CHANGED
@@ -42,7 +42,9 @@ mcpill run # start the server
42
42
 
43
43
  Scaffolds a new project in the current directory:
44
44
 
45
+ - `README.md` — quickstart guide for the scaffolded project
45
46
  - `PILL.md` — intent-level spec; fill this and hand to Claude to generate source files
47
+ - `pill-agent-guide.md` — agent instructions read by Claude when building from `PILL.md`
46
48
  - `server.md` — server config + resources
47
49
  - `tools/echo.md` — example tool
48
50
  - `prompts/greeting.md` — example prompt
package/dist/cli.js CHANGED
@@ -9,25 +9,24 @@ import { readFileSync as readFileSync3 } from "fs";
9
9
  // src/commands/init.ts
10
10
  import fs from "fs";
11
11
  import path from "path";
12
- function makePillMd(projectName) {
13
- return `<!-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
14
- FOR THE AGENT \u2014 read this before doing anything else
15
- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
12
+ var PILL_AGENT_GUIDE_MD = `# pill-agent-guide
16
13
 
17
- You are building an mcpill MCP server from this document.
14
+ Instructions for building an mcpill MCP server from a filled \`PILL.md\`.
15
+
16
+ ---
18
17
 
19
- STEPS:
18
+ ## Steps
20
19
 
21
- 1. PARSE the sections below:
22
- - ## Server \u2192 server name, description, transport (+ port if http)
23
- - ## Tool: <name> \u2192 one tool per section (repeatable)
24
- - ## Resource: <name> \u2192 static resource (optional, repeatable)
25
- - ## Prompt: <name> \u2192 prompt template (optional, repeatable)
20
+ 1. **Parse** the sections in \`PILL.md\`:
21
+ - \`## Server\` \u2192 server name, description, transport (+ port if http)
22
+ - \`## Tool: <name>\` \u2192 one tool per section (repeatable)
23
+ - \`## Resource: <name>\` \u2192 static resource (optional, repeatable)
24
+ - \`## Prompt: <name>\` \u2192 prompt template (optional, repeatable)
26
25
 
27
- 2. CREATE a directory named after the server. Generate inside it:
26
+ 2. **Write** the server files into the pre-created scaffold at \`.mcpill/\`:
28
27
 
29
- server.md
30
- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
28
+ \`.mcpill/server.md\`
29
+ \`\`\`
31
30
  ## Config
32
31
  name: {name}
33
32
  transport: {transport}
@@ -38,11 +37,12 @@ STEPS:
38
37
  name: {resource-name}
39
38
  ---
40
39
  {content}
41
- \u2190 omit ## Resources entirely if none specified
42
- \u2190 separate multiple resources with a blank line + --- + blank line
40
+ \`\`\`
41
+ Omit \`## Resources\` entirely if none were specified.
42
+ Separate multiple resources with a blank line + \`---\` + blank line.
43
43
 
44
- tools/{tool-name}.md (one file per ## Tool section)
45
- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
44
+ \`.mcpill/server/tools/{tool-name}.md\` \u2014 one file per \`## Tool\` section:
45
+ \`\`\`
46
46
  # {tool-name}
47
47
 
48
48
  {description}
@@ -55,13 +55,13 @@ STEPS:
55
55
 
56
56
  \`\`\`js
57
57
  async ({ {param} }) => {
58
- // real implementation \u2014 not a stub
59
58
  return { content: [{ type: "text", text: {result} }] };
60
59
  }
61
60
  \`\`\`
61
+ \`\`\`
62
62
 
63
- prompts/{prompt-name}.md (one file per ## Prompt section)
64
- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
63
+ \`.mcpill/server/prompts/{prompt-name}.md\` \u2014 one file per \`## Prompt\` section:
64
+ \`\`\`
65
65
  # {prompt-name}
66
66
 
67
67
  {description}
@@ -73,26 +73,46 @@ STEPS:
73
73
  ## Message
74
74
 
75
75
  > user: {message with {{arg}} placeholders}
76
- \u2190 omit ## Args if the prompt takes no arguments
76
+ \`\`\`
77
+ Omit \`## Args\` if the prompt takes no arguments.
77
78
 
78
- 3. TRANSLATE each tool's \`behavior\` field into a working JS async handler.
79
+ 3. **Translate** each tool's \`behavior\` field into a working JS async handler.
79
80
  - Implement the described logic fully \u2014 no stubs, no TODOs.
80
81
  - Handler is self-contained: no top-level imports.
81
82
  - For file I/O or network calls: use dynamic import inside the handler.
82
- - Return shape must be: { content: [{ type: "text", text: string }] }
83
- - Allowed parameter types: string | number | boolean. Default unknown types to string.
83
+ - Return shape: \`{ content: [{ type: "text", text: string }] }\`
84
+ - Allowed parameter types: \`string | number | boolean\`. Default unknown types to \`string\`.
84
85
 
85
- 4. RUN \`mcpill compile\` (or \`mcpill compile --dir {server-name}\`).
86
+ 4. **Run** \`mcpill compile\`.
86
87
 
87
- 5. REPORT what was generated and show the user the next commands:
88
+ 5. **Report** what was generated and show the user the next commands:
89
+ \`\`\`
88
90
  mcpill validate
89
91
  mcpill run
90
- mcpill pack / mcpill publish
92
+ mcpill pack
93
+ mcpill publish
94
+ \`\`\`
95
+
96
+ ---
97
+
98
+ ## Edge cases
99
+
100
+ | Situation | Handling |
101
+ |---|---|
102
+ | \`transport: http\` without port | Default to \`3333\` |
103
+ | Prompt message without role prefix | Wrap as \`> user: {message}\` |
104
+ | Server name with spaces | Convert to kebab-case for \`name\` field |
105
+ | Input type not \`string\\|number\\|boolean\` | Default to \`string\`, note the assumption |
106
+ | Vague \`behavior\` field | Implement the most literal reasonable interpretation |
107
+ `;
108
+ function makePillMd(projectName) {
109
+ return `<!-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
110
+ FOR THE AGENT \u2014 read this before doing anything else
111
+ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
112
+
113
+ You are building an mcpill MCP server from this document.
114
+ Follow the steps in .mcpill/pill-agent-guide.md.
91
115
 
92
- EDGE CASES:
93
- - transport http without port \u2192 default to 3333
94
- - prompt message without role prefix \u2192 wrap as \`> user: {message}\`
95
- - server name with spaces \u2192 convert to kebab-case for directory and name field
96
116
  \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->
97
117
 
98
118
  # Pill: ${projectName}
@@ -144,6 +164,66 @@ After the source files are generated:
144
164
  - [ ] \`mcpill publish\`
145
165
  `;
146
166
  }
167
+ var HELLO_MCP_MD = `<!-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
168
+ HELLO-MCP \u2014 a ready-to-run example
169
+
170
+ Want to see mcpill in action before building your own server?
171
+
172
+ 1. Copy the content below into your PILL.md (replace everything).
173
+ 2. Tell Claude: "Build this PILL.md"
174
+ 3. Run: mcpill compile && mcpill validate && mcpill run
175
+ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->
176
+
177
+ # Pill: hello-mcp
178
+
179
+ ---
180
+
181
+ ## Server
182
+
183
+ name: hello-mcp
184
+ description: A hello-world MCP server \u2014 see mcpill in action.
185
+ transport: stdio
186
+
187
+ ---
188
+
189
+ ## Resource: server-info
190
+
191
+ uri: info://server
192
+ name: Server Info
193
+ content: |
194
+ hello-mcp is a demo MCP server built with mcpill.
195
+ Tools: ping, echo. Prompt: introduce.
196
+
197
+ ---
198
+
199
+ ## Tool: ping
200
+
201
+ description: Returns pong with the current UTC timestamp.
202
+ inputs:
203
+ output: A pong message with the current timestamp.
204
+ behavior: |
205
+ Return the string "pong \u2014 " followed by new Date().toISOString().
206
+
207
+ ---
208
+
209
+ ## Tool: echo
210
+
211
+ description: Echoes a message back to the caller.
212
+ inputs:
213
+ - message (string): The message to echo.
214
+ output: The same message, echoed back as text.
215
+ behavior: |
216
+ Return the message input unchanged.
217
+
218
+ ---
219
+
220
+ ## Prompt: introduce
221
+
222
+ description: Ask the server to introduce itself.
223
+ behavior: |
224
+ A prompt (no args) with a single user message:
225
+ "Introduce yourself \u2014 what is hello-mcp and what can it do?"
226
+ `;
147
227
  var SERVER_MD_TEMPLATE = `## Config
148
228
  name: my-server
149
229
  transport: stdio
@@ -182,49 +262,102 @@ Generate a greeting
182
262
 
183
263
  > user: Say hello to {{name}}
184
264
  `;
265
+ function makePillUserGuideMd(projectName) {
266
+ return `# ${projectName} \u2014 mcpill user guide
267
+
268
+ ## Quickstart
269
+
270
+ 1. **Describe your server** in \`PILL.md\` \u2014 fill in the \`## Server\` block and add \`## Tool\` sections.
271
+ 2. **Build it** \u2014 open Claude Code and say:
272
+ > "Build this PILL.md"
273
+ Claude reads \`.mcpill/pill-agent-guide.md\` and generates the source files.
274
+ 3. **Compile**
275
+ \`\`\`sh
276
+ mcpill compile
277
+ \`\`\`
278
+ 4. **Test locally**
279
+ \`\`\`sh
280
+ mcpill validate # schema check
281
+ mcpill run # start the server
282
+ \`\`\`
283
+ 5. **Pack & publish**
284
+ \`\`\`sh
285
+ mcpill pack
286
+ mcpill publish
287
+ \`\`\`
288
+
289
+ ## Project layout
290
+
291
+ \`\`\`
292
+ ${projectName}/
293
+ \u251C\u2500\u2500 PILL.md \u2190 describe your server here
294
+ \u2514\u2500\u2500 .mcpill/
295
+ \u251C\u2500\u2500 server.md \u2190 server config (name, transport, resources)
296
+ \u251C\u2500\u2500 pill-agent-guide.md \u2190 instructions Claude follows when building
297
+ \u251C\u2500\u2500 pill-user-guide.md \u2190 this file
298
+ \u2514\u2500\u2500 server/
299
+ \u251C\u2500\u2500 mcpill.config.json \u2190 compiled config
300
+ \u251C\u2500\u2500 tools/ \u2190 one .md file per tool
301
+ \u2502 \u2514\u2500\u2500 echo.md
302
+ \u2514\u2500\u2500 prompts/ \u2190 one .md file per prompt (optional)
303
+ \u2514\u2500\u2500 greeting.md
304
+ \`\`\`
305
+
306
+ ## Editing tools manually
307
+
308
+ Each file in \`.mcpill/server/tools/\` follows this shape:
309
+
310
+ \`\`\`markdown
311
+ # tool-name
312
+
313
+ One-line description.
314
+
315
+ ## Parameters
316
+
317
+ - param (string): what it is
318
+
319
+ ## Handler
320
+
321
+ \`\`\`js
322
+ async ({ param }) => {
323
+ return { content: [{ type: "text", text: param }] };
324
+ }
325
+ \`\`\`
326
+ \`\`\`
327
+
328
+ Run \`mcpill compile\` after any edit to regenerate the server bundle.
329
+ `;
330
+ }
185
331
  async function runInit(opts) {
186
332
  const targetDir = path.resolve(opts.dir ?? process.cwd());
187
333
  const mcpillDir = path.join(targetDir, ".mcpill");
334
+ const serverDir = path.join(mcpillDir, "server");
188
335
  const projectName = path.basename(targetDir);
189
336
  if (fs.existsSync(mcpillDir)) {
190
337
  console.error(".mcpill/ already exists. Remove it manually to re-init.");
191
338
  process.exit(1);
192
339
  }
193
- const configJson = JSON.stringify(
194
- { name: projectName, transport: "stdio", port: 3333 },
195
- null,
196
- 2
197
- );
198
- fs.mkdirSync(mcpillDir, { recursive: true });
199
- fs.writeFileSync(path.join(mcpillDir, "mcpill.config.json"), configJson);
200
- const toolsDir = path.join(targetDir, "tools");
201
- const promptsDir = path.join(targetDir, "prompts");
202
- fs.mkdirSync(toolsDir, { recursive: true });
203
- fs.mkdirSync(promptsDir, { recursive: true });
204
- fs.writeFileSync(path.join(toolsDir, "echo.md"), ECHO_TOOL_MD);
205
- fs.writeFileSync(path.join(promptsDir, "greeting.md"), GREETING_PROMPT_MD);
206
- const serverMd = SERVER_MD_TEMPLATE.replace(
207
- "name: my-server",
208
- `name: ${projectName}`
340
+ fs.mkdirSync(path.join(serverDir, "tools"), { recursive: true });
341
+ fs.mkdirSync(path.join(serverDir, "prompts"), { recursive: true });
342
+ const serverMd = SERVER_MD_TEMPLATE.replace("name: my-server", `name: ${projectName}`);
343
+ fs.writeFileSync(path.join(mcpillDir, "server.md"), serverMd);
344
+ fs.writeFileSync(path.join(serverDir, "tools", "echo.md"), ECHO_TOOL_MD);
345
+ fs.writeFileSync(path.join(serverDir, "prompts", "greeting.md"), GREETING_PROMPT_MD);
346
+ fs.writeFileSync(
347
+ path.join(serverDir, "mcpill.config.json"),
348
+ JSON.stringify({ name: projectName, transport: "stdio", port: 3333 }, null, 2)
209
349
  );
210
- fs.writeFileSync(path.join(targetDir, "server.md"), serverMd);
350
+ fs.writeFileSync(path.join(mcpillDir, "pill-agent-guide.md"), PILL_AGENT_GUIDE_MD);
351
+ fs.writeFileSync(path.join(mcpillDir, "pill-user-guide.md"), makePillUserGuideMd(projectName));
352
+ fs.writeFileSync(path.join(mcpillDir, "HELLO-MCP.md"), HELLO_MCP_MD);
211
353
  fs.writeFileSync(path.join(targetDir, "PILL.md"), makePillMd(projectName));
212
- const pkgJson = JSON.stringify(
213
- {
214
- name: projectName,
215
- version: "0.1.0",
216
- private: true,
217
- scripts: { pack: "mcpill pack", publish: "mcpill publish" }
218
- },
219
- null,
220
- 2
221
- );
222
- fs.writeFileSync(path.join(targetDir, "package.json"), pkgJson);
223
- console.log('\u2713 PILL.md \u2190 describe your server, then tell Claude: "Build this PILL.md"');
224
- console.log("\u2713 server.md \u2190 or edit source files directly");
225
- console.log("\u2713 tools/echo.md");
226
- console.log("\u2713 prompts/greeting.md");
227
- console.log("\u2713 package.json");
354
+ console.log('\u2713 PILL.md \u2190 describe your server, then tell Claude: "Build this PILL.md"');
355
+ console.log("\u2713 .mcpill/HELLO-MCP.md \u2190 copy into PILL.md to see a working example instantly");
356
+ console.log("\u2713 .mcpill/server.md \u2190 or edit source files directly");
357
+ console.log("\u2713 .mcpill/server/tools/echo.md");
358
+ console.log("\u2713 .mcpill/server/prompts/greeting.md");
359
+ console.log("\u2713 .mcpill/pill-agent-guide.md \u2190 agent instructions (read by Claude)");
360
+ console.log("\u2713 .mcpill/pill-user-guide.md \u2190 start here");
228
361
  console.log("");
229
362
  console.log("When ready: mcpill compile && mcpill run");
230
363
  }
@@ -233,7 +366,7 @@ async function runInit(opts) {
233
366
  import path7 from "path";
234
367
  import fs6 from "fs";
235
368
  import { z as z2 } from "mcpill-runtime";
236
- import { createServer } from "mcpster";
369
+ import { createServer, applySetup } from "mcpster";
237
370
 
238
371
  // src/loaders/config.ts
239
372
  import fs2 from "fs";
@@ -425,26 +558,24 @@ async function validateOne(mcpillDir) {
425
558
  console.log(`\u2713 Valid: ${toolCount} tools, ${promptCount} prompts, ${resourceCount} resources`);
426
559
  }
427
560
  async function runValidate(baseDir) {
428
- const pillDirs = fs5.readdirSync(baseDir, { withFileTypes: true }).filter(
429
- (e) => e.isDirectory() && /^\.[a-z][a-z0-9-]*$/.test(e.name) && fs5.existsSync(path6.join(baseDir, e.name, "mcpill.config.json"))
430
- ).map((e) => path6.join(baseDir, e.name));
431
- if (pillDirs.length === 0) {
561
+ const serverDir = path6.join(baseDir, ".mcpill", "server");
562
+ if (!fs5.existsSync(path6.join(serverDir, "mcpill.config.json"))) {
432
563
  console.error("No pill directories found \u2014 run mcpill compile first");
433
564
  process.exit(1);
434
565
  }
435
- for (const mcpillDir of pillDirs) {
436
- await validateOne(mcpillDir);
437
- }
566
+ await validateOne(serverDir);
438
567
  }
439
568
 
440
569
  // src/commands/run.ts
441
570
  async function runServer(opts) {
442
571
  const baseDir = path7.resolve(opts.dir ?? process.cwd());
443
572
  await runValidate(baseDir);
444
- const pillDirs = fs6.readdirSync(baseDir, { withFileTypes: true }).filter(
445
- (e) => e.isDirectory() && /^\.[a-z][a-z0-9-]*$/.test(e.name) && fs6.existsSync(path7.join(baseDir, e.name, "mcpill.config.json"))
446
- ).map((e) => path7.join(baseDir, e.name));
447
- const mcpillDir = pillDirs[0];
573
+ const mcpillDir = path7.join(baseDir, ".mcpill", "server");
574
+ if (!fs6.existsSync(path7.join(mcpillDir, "mcpill.config.json"))) {
575
+ console.error("No pill found \u2014 run mcpill compile first");
576
+ process.exit(1);
577
+ }
578
+ ;
448
579
  const [tools, prompts, resources, config] = await Promise.all([
449
580
  loadTools(mcpillDir),
450
581
  loadPrompts(mcpillDir),
@@ -487,6 +618,15 @@ async function runServer(opts) {
487
618
  resolver: async () => content
488
619
  });
489
620
  }
621
+ applySetup(name, tools.map((t) => t.name), {
622
+ projectPath: baseDir,
623
+ permissions: "restrictive",
624
+ register: true,
625
+ cmdOverride: {
626
+ command: "mcpill",
627
+ args: ["run", "--dir", baseDir]
628
+ }
629
+ });
490
630
  try {
491
631
  await server.start();
492
632
  } catch (err) {
@@ -507,7 +647,7 @@ async function runServer(opts) {
507
647
 
508
648
  // src/commands/compile.ts
509
649
  import { resolve, join as join3 } from "path";
510
- import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync, readdirSync as readdirSync2 } from "fs";
650
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync } from "fs";
511
651
  import { pathToFileURL as pathToFileURL2 } from "url";
512
652
 
513
653
  // src/compiler/parse.ts
@@ -654,7 +794,7 @@ function parseServerDir(dir) {
654
794
  };
655
795
  const resourcesBody = sections.get("Resources") ?? "";
656
796
  const resources = parseResourcesSection(resourcesBody);
657
- const toolsDir = join(dir, "tools");
797
+ const toolsDir = join(dir, "server", "tools");
658
798
  const tools = [];
659
799
  let toolFiles = [];
660
800
  try {
@@ -664,7 +804,7 @@ function parseServerDir(dir) {
664
804
  for (const file of toolFiles) {
665
805
  tools.push(parseToolFile(readFileSync(join(toolsDir, file), "utf-8")));
666
806
  }
667
- const promptsDir = join(dir, "prompts");
807
+ const promptsDir = join(dir, "server", "prompts");
668
808
  const prompts = [];
669
809
  let promptFiles = [];
670
810
  try {
@@ -693,8 +833,8 @@ ${r.content}`;
693
833
  }).join("\n---\n");
694
834
  }
695
835
  function serializeServerDir(doc, dir) {
696
- mkdirSync(join2(dir, "tools"), { recursive: true });
697
- mkdirSync(join2(dir, "prompts"), { recursive: true });
836
+ mkdirSync(join2(dir, "server", "tools"), { recursive: true });
837
+ mkdirSync(join2(dir, "server", "prompts"), { recursive: true });
698
838
  let serverMd = "## Config\n";
699
839
  serverMd += `name: ${doc.config.name}
700
840
  `;
@@ -734,7 +874,7 @@ ${cleanHandler}
734
874
  \`\`\`
735
875
  `;
736
876
  }
737
- writeFileSync(join2(dir, "tools", `${tool.name}.md`), md);
877
+ writeFileSync(join2(dir, "server", "tools", `${tool.name}.md`), md);
738
878
  }
739
879
  for (const prompt of doc.prompts) {
740
880
  let md = `# ${prompt.name}
@@ -758,7 +898,7 @@ ${cleanHandler}
758
898
  `;
759
899
  }
760
900
  }
761
- writeFileSync(join2(dir, "prompts", `${prompt.name}.md`), md);
901
+ writeFileSync(join2(dir, "server", "prompts", `${prompt.name}.md`), md);
762
902
  }
763
903
  console.log(`\u2713 tools/ updated (${doc.tools.length} files)`);
764
904
  console.log(`\u2713 prompts/ updated (${doc.prompts.length} files)`);
@@ -893,24 +1033,20 @@ mimeType: ${r.mimeType}`;
893
1033
  ${r.content}`;
894
1034
  }).join("\n---\n") + "\n";
895
1035
  }
896
- function findPillDirs(baseDir) {
897
- return readdirSync2(baseDir, { withFileTypes: true }).filter(
898
- (e) => e.isDirectory() && /^\.[a-z][a-z0-9-]*$/.test(e.name) && existsSync(join3(baseDir, e.name, "mcpill.config.json"))
899
- ).map((e) => join3(baseDir, e.name));
900
- }
901
1036
  async function runCompile(opts) {
902
1037
  const baseDir = resolve(opts.dir ?? process.cwd());
1038
+ const mcpillDir = join3(baseDir, ".mcpill");
1039
+ const serverDir = join3(mcpillDir, "server");
903
1040
  if (opts.toMd) {
904
- const pillDirs = findPillDirs(baseDir);
905
- if (pillDirs.length === 0) {
906
- throw new Error("No pill directories found \u2014 run mcpill compile first");
1041
+ const configPath = join3(serverDir, "mcpill.config.json");
1042
+ if (!existsSync(configPath)) {
1043
+ throw new Error("No pill found \u2014 run mcpill compile first");
907
1044
  }
908
- const mcpillDir2 = pillDirs[0];
909
- const config = await loadConfig(mcpillDir2);
910
- const resources = await loadResources(mcpillDir2);
911
- const promptsPath = join3(mcpillDir2, "prompts.json");
1045
+ const config = await loadConfig(serverDir);
1046
+ const resources = await loadResources(serverDir);
1047
+ const promptsPath = join3(serverDir, "prompts.json");
912
1048
  const prompts = JSON.parse(readFileSync2(promptsPath, "utf-8"));
913
- const toolsPath = join3(mcpillDir2, "tools.js");
1049
+ const toolsPath = join3(serverDir, "tools.js");
914
1050
  const handlers = existsSync(toolsPath) ? extractHandlers(readFileSync2(toolsPath, "utf-8")) : /* @__PURE__ */ new Map();
915
1051
  let tools = [];
916
1052
  if (existsSync(toolsPath)) {
@@ -940,12 +1076,11 @@ async function runCompile(opts) {
940
1076
  prompts,
941
1077
  resources
942
1078
  };
943
- serializeServerDir(doc2, baseDir);
1079
+ serializeServerDir(doc2, mcpillDir);
944
1080
  return;
945
1081
  }
946
- const doc = parseServerDir(baseDir);
947
- const mcpillDir = join3(baseDir, "." + doc.config.name);
948
- const toolsJsPath = join3(mcpillDir, "tools.js");
1082
+ const doc = parseServerDir(mcpillDir);
1083
+ const toolsJsPath = join3(serverDir, "tools.js");
949
1084
  const existing = existsSync(toolsJsPath) ? extractHandlers(readFileSync2(toolsJsPath, "utf-8")) : /* @__PURE__ */ new Map();
950
1085
  const { doc: mergedDoc, stubbed } = mergeHandlers(doc, existing);
951
1086
  if (opts.strict && stubbed.length > 0) {
@@ -954,33 +1089,22 @@ async function runCompile(opts) {
954
1089
  for (const name of stubbed) {
955
1090
  console.warn(`\u26A0 Stub generated for tool: ${name}`);
956
1091
  }
957
- mkdirSync2(mcpillDir, { recursive: true });
958
- writeFileSync2(join3(mcpillDir, "mcpill.config.json"), JSON.stringify(mergedDoc.config, null, 2));
959
- writeFileSync2(join3(mcpillDir, "prompts.json"), JSON.stringify(mergedDoc.prompts, null, 2));
960
- writeFileSync2(join3(mcpillDir, "resources.md"), serializeResourcesMd(mergedDoc.resources));
1092
+ mkdirSync2(serverDir, { recursive: true });
1093
+ writeFileSync2(join3(serverDir, "mcpill.config.json"), JSON.stringify(mergedDoc.config, null, 2));
1094
+ writeFileSync2(join3(serverDir, "prompts.json"), JSON.stringify(mergedDoc.prompts, null, 2));
1095
+ writeFileSync2(join3(serverDir, "resources.md"), serializeResourcesMd(mergedDoc.resources));
961
1096
  writeFileSync2(toolsJsPath, generateToolsJs(mergedDoc.tools));
962
- const pillDirName = "." + doc.config.name;
963
- console.log(`\u2713 ${pillDirName}/ updated from server.md, tools/, prompts/`);
1097
+ console.log(`\u2713 .mcpill/server/ updated from .mcpill/server.md, .mcpill/server/tools/, .mcpill/server/prompts/`);
964
1098
  }
965
1099
 
966
1100
  // src/commands/pack.ts
967
1101
  import fs7 from "fs";
968
1102
  import path8 from "path";
969
1103
  var SERVER_ENTRY_TEMPLATE = "import { runPill } from 'mcpill-runtime';\nrunPill();\n";
970
- function resolvePillDir(baseDir) {
971
- const entries = fs7.readdirSync(baseDir, { withFileTypes: true });
972
- const pill = entries.find(
973
- (e) => e.isDirectory() && /^\.[a-z][a-z0-9-]*$/.test(e.name) && fs7.existsSync(path8.join(baseDir, e.name, "mcpill.config.json"))
974
- );
975
- if (!pill) {
976
- throw new Error("No pill directory found \u2014 run mcpill compile first");
977
- }
978
- return path8.join(baseDir, pill.name);
979
- }
980
1104
  async function runPack(dir) {
981
1105
  const baseDir = path8.resolve(dir);
982
1106
  await runValidate(baseDir);
983
- const pillDir = resolvePillDir(baseDir);
1107
+ const pillDir = path8.join(baseDir, ".mcpill", "server");
984
1108
  const config = await loadConfig(pillDir);
985
1109
  const { name } = config;
986
1110
  fs7.mkdirSync(path8.join(baseDir, "bin"), { recursive: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcpill",
3
- "version": "1.2.0",
3
+ "version": "1.2.1",
4
4
  "type": "module",
5
5
  "description": "CLI for building, validating, and publishing MCP servers using the pill format.",
6
6
  "homepage": "https://mcpill.ruco.dev",
@@ -20,35 +20,36 @@ describe("runInit", () => {
20
20
  }
21
21
  });
22
22
 
23
- it("writes all four files with expected content in an empty dir", async () => {
23
+ it("scaffolds the expected files and directories", async () => {
24
24
  const base = mkTmp();
25
25
  vi.spyOn(console, "log").mockImplementation(() => {});
26
26
 
27
27
  await runInit({ dir: base });
28
28
 
29
29
  const mcpillDir = path.join(base, ".mcpill");
30
+ const serverDir = path.join(mcpillDir, "server");
31
+
30
32
  expect(fs.existsSync(mcpillDir)).toBe(true);
31
- expect(fs.existsSync(path.join(mcpillDir, "tools.js"))).toBe(true);
32
- expect(fs.existsSync(path.join(mcpillDir, "prompts.json"))).toBe(true);
33
- expect(fs.existsSync(path.join(mcpillDir, "resources.md"))).toBe(true);
34
- expect(fs.existsSync(path.join(mcpillDir, "mcpill.config.json"))).toBe(true);
33
+ expect(fs.existsSync(path.join(mcpillDir, "server.md"))).toBe(true);
34
+ expect(fs.existsSync(path.join(mcpillDir, "pill-agent-guide.md"))).toBe(true);
35
+ expect(fs.existsSync(path.join(mcpillDir, "pill-user-guide.md"))).toBe(true);
36
+ expect(fs.existsSync(path.join(serverDir, "mcpill.config.json"))).toBe(true);
37
+ expect(fs.existsSync(path.join(serverDir, "tools", "echo.md"))).toBe(true);
38
+ expect(fs.existsSync(path.join(serverDir, "prompts", "greeting.md"))).toBe(true);
39
+ expect(fs.existsSync(path.join(base, "PILL.md"))).toBe(true);
40
+ expect(fs.existsSync(path.join(base, "README.md"))).toBe(false);
41
+ expect(fs.existsSync(path.join(base, "package.json"))).toBe(false);
35
42
 
36
43
  const config = JSON.parse(
37
- fs.readFileSync(path.join(mcpillDir, "mcpill.config.json"), "utf-8")
44
+ fs.readFileSync(path.join(serverDir, "mcpill.config.json"), "utf-8")
38
45
  );
39
46
  expect(config).toMatchObject({ name: path.basename(base), transport: "stdio", port: 3333 });
40
47
 
41
- const prompts = JSON.parse(
42
- fs.readFileSync(path.join(mcpillDir, "prompts.json"), "utf-8")
43
- );
44
- expect(Array.isArray(prompts)).toBe(true);
45
- expect(prompts[0].name).toBe("summarize");
46
-
47
- const resources = fs.readFileSync(path.join(mcpillDir, "resources.md"), "utf-8");
48
- expect(resources).toContain("uri: config://app");
48
+ const serverMd = fs.readFileSync(path.join(mcpillDir, "server.md"), "utf-8");
49
+ expect(serverMd).toContain(`name: ${path.basename(base)}`);
49
50
 
50
- const tools = fs.readFileSync(path.join(mcpillDir, "tools.js"), "utf-8");
51
- expect(tools).toContain("read_file");
51
+ const pillMd = fs.readFileSync(path.join(base, "PILL.md"), "utf-8");
52
+ expect(pillMd).toContain(".mcpill/pill-agent-guide.md");
52
53
  });
53
54
 
54
55
  it("exits with code 1 and correct message when .mcpill/ already exists", async () => {
@@ -5,7 +5,7 @@ import os from "os";
5
5
  import { runPack, SERVER_ENTRY_TEMPLATE } from "../commands/pack.js";
6
6
 
7
7
  function scaffoldPill(baseDir: string, name = "test-pill") {
8
- const mcpillDir = path.join(baseDir, ".test-pill");
8
+ const mcpillDir = path.join(baseDir, ".mcpill", "server");
9
9
  fs.mkdirSync(mcpillDir, { recursive: true });
10
10
  fs.writeFileSync(
11
11
  path.join(mcpillDir, "tools.js"),
@@ -52,7 +52,7 @@ describe("runValidate", () => {
52
52
 
53
53
  it("passes on a valid scaffolded pill dir", async () => {
54
54
  const base = mkTmp();
55
- const mcpillDir = path.join(base, ".mcpill");
55
+ const mcpillDir = path.join(base, ".mcpill", "server");
56
56
  scaffoldValid(mcpillDir);
57
57
 
58
58
  const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
@@ -74,7 +74,7 @@ describe("runValidate", () => {
74
74
 
75
75
  it("collects and prints tool error then exits 1", async () => {
76
76
  const base = mkTmp();
77
- const mcpillDir = path.join(base, ".mcpill");
77
+ const mcpillDir = path.join(base, ".mcpill", "server");
78
78
  scaffoldValid(mcpillDir);
79
79
  // Break tools.js: handler is not a function
80
80
  fs.writeFileSync(
@@ -92,7 +92,7 @@ describe("runValidate", () => {
92
92
 
93
93
  it("collects and prints prompt error then exits 1", async () => {
94
94
  const base = mkTmp();
95
- const mcpillDir = path.join(base, ".mcpill");
95
+ const mcpillDir = path.join(base, ".mcpill", "server");
96
96
  scaffoldValid(mcpillDir);
97
97
  fs.writeFileSync(path.join(mcpillDir, "prompts.json"), "not json {{{");
98
98
 
@@ -110,7 +110,7 @@ describe("runValidate", () => {
110
110
 
111
111
  it("collects and prints resource error then exits 1", async () => {
112
112
  const base = mkTmp();
113
- const mcpillDir = path.join(base, ".mcpill");
113
+ const mcpillDir = path.join(base, ".mcpill", "server");
114
114
  scaffoldValid(mcpillDir);
115
115
  // Block missing uri
116
116
  fs.writeFileSync(
@@ -130,7 +130,7 @@ describe("runValidate", () => {
130
130
 
131
131
  it("collects errors from multiple loaders before exiting", async () => {
132
132
  const base = mkTmp();
133
- const mcpillDir = path.join(base, ".mcpill");
133
+ const mcpillDir = path.join(base, ".mcpill", "server");
134
134
  scaffoldValid(mcpillDir);
135
135
  // Break both tools and prompts
136
136
  fs.writeFileSync(
@@ -1,5 +1,5 @@
1
- import { resolve, join, basename } from 'path';
2
- import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync } from 'fs';
1
+ import { resolve, join } from 'path';
2
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
3
3
  import { pathToFileURL } from 'url';
4
4
  import { parseServerDir } from '../compiler/parse.js';
5
5
  import { serializeServerDir } from '../compiler/serialize.js';
@@ -62,38 +62,28 @@ function serializeResourcesMd(resources: ServerDoc['resources']): string {
62
62
  );
63
63
  }
64
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
65
  export async function runCompile(opts: {
77
66
  dir?: string;
78
67
  toMd?: boolean;
79
68
  strict?: boolean;
80
69
  }): Promise<void> {
81
70
  const baseDir = resolve(opts.dir ?? process.cwd());
71
+ const mcpillDir = join(baseDir, '.mcpill');
72
+ const serverDir = join(mcpillDir, 'server');
82
73
 
83
74
  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');
75
+ const configPath = join(serverDir, 'mcpill.config.json');
76
+ if (!existsSync(configPath)) {
77
+ throw new Error('No pill found — run mcpill compile first');
87
78
  }
88
- const mcpillDir = pillDirs[0]!;
89
79
 
90
- const config = await loadConfig(mcpillDir);
91
- const resources = await loadResources(mcpillDir);
80
+ const config = await loadConfig(serverDir);
81
+ const resources = await loadResources(serverDir);
92
82
 
93
- const promptsPath = join(mcpillDir, 'prompts.json');
83
+ const promptsPath = join(serverDir, 'prompts.json');
94
84
  const prompts = JSON.parse(readFileSync(promptsPath, 'utf-8')) as ServerDoc['prompts'];
95
85
 
96
- const toolsPath = join(mcpillDir, 'tools.js');
86
+ const toolsPath = join(serverDir, 'tools.js');
97
87
  const handlers = existsSync(toolsPath)
98
88
  ? extractHandlers(readFileSync(toolsPath, 'utf-8'))
99
89
  : new Map<string, string>();
@@ -130,15 +120,14 @@ export async function runCompile(opts: {
130
120
  resources,
131
121
  };
132
122
 
133
- serializeServerDir(doc, baseDir);
123
+ serializeServerDir(doc, mcpillDir);
134
124
  return;
135
125
  }
136
126
 
137
- // Forward direction: server.md + tools/ + prompts/ → .<name>/
138
- const doc = parseServerDir(baseDir);
139
- const mcpillDir = join(baseDir, '.' + doc.config.name);
127
+ // Forward direction: .mcpill/server.md + .mcpill/server/tools/ + .mcpill/server/prompts/ → .mcpill/server/
128
+ const doc = parseServerDir(mcpillDir);
140
129
 
141
- const toolsJsPath = join(mcpillDir, 'tools.js');
130
+ const toolsJsPath = join(serverDir, 'tools.js');
142
131
  const existing = existsSync(toolsJsPath)
143
132
  ? extractHandlers(readFileSync(toolsJsPath, 'utf-8'))
144
133
  : new Map<string, string>();
@@ -152,12 +141,11 @@ export async function runCompile(opts: {
152
141
  console.warn(`⚠ Stub generated for tool: ${name}`);
153
142
  }
154
143
 
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));
144
+ mkdirSync(serverDir, { recursive: true });
145
+ writeFileSync(join(serverDir, 'mcpill.config.json'), JSON.stringify(mergedDoc.config, null, 2));
146
+ writeFileSync(join(serverDir, 'prompts.json'), JSON.stringify(mergedDoc.prompts, null, 2));
147
+ writeFileSync(join(serverDir, 'resources.md'), serializeResourcesMd(mergedDoc.resources));
159
148
  writeFileSync(toolsJsPath, generateToolsJs(mergedDoc.tools));
160
149
 
161
- const pillDirName = '.' + doc.config.name;
162
- console.log(`✓ ${pillDirName}/ updated from server.md, tools/, prompts/`);
150
+ console.log(`✓ .mcpill/server/ updated from .mcpill/server.md, .mcpill/server/tools/, .mcpill/server/prompts/`);
163
151
  }
@@ -1,25 +1,24 @@
1
1
  import fs from "fs";
2
2
  import path from "path";
3
3
 
4
- function makePillMd(projectName: string): string {
5
- return `<!-- ═══════════════════════════════════════════════════════════════
6
- FOR THE AGENT — read this before doing anything else
7
- ═══════════════════════════════════════════════════════════════
4
+ const PILL_AGENT_GUIDE_MD = `# pill-agent-guide
8
5
 
9
- You are building an mcpill MCP server from this document.
6
+ Instructions for building an mcpill MCP server from a filled \`PILL.md\`.
7
+
8
+ ---
10
9
 
11
- STEPS:
10
+ ## Steps
12
11
 
13
- 1. PARSE the sections below:
14
- - ## Server → server name, description, transport (+ port if http)
15
- - ## Tool: <name> → one tool per section (repeatable)
16
- - ## Resource: <name> → static resource (optional, repeatable)
17
- - ## Prompt: <name> → prompt template (optional, repeatable)
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)
18
17
 
19
- 2. CREATE a directory named after the server. Generate inside it:
18
+ 2. **Write** the server files into the pre-created scaffold at \`.mcpill/\`:
20
19
 
21
- server.md
22
- ─────────
20
+ \`.mcpill/server.md\`
21
+ \`\`\`
23
22
  ## Config
24
23
  name: {name}
25
24
  transport: {transport}
@@ -30,11 +29,12 @@ STEPS:
30
29
  name: {resource-name}
31
30
  ---
32
31
  {content}
33
- ← omit ## Resources entirely if none specified
34
- separate multiple resources with a blank line + --- + blank line
32
+ \`\`\`
33
+ Omit \`## Resources\` entirely if none were specified.
34
+ Separate multiple resources with a blank line + \`---\` + blank line.
35
35
 
36
- tools/{tool-name}.md (one file per ## Tool section)
37
- ─────────────────────
36
+ \`.mcpill/server/tools/{tool-name}.md\` — one file per \`## Tool\` section:
37
+ \`\`\`
38
38
  # {tool-name}
39
39
 
40
40
  {description}
@@ -47,13 +47,13 @@ STEPS:
47
47
 
48
48
  \`\`\`js
49
49
  async ({ {param} }) => {
50
- // real implementation — not a stub
51
50
  return { content: [{ type: "text", text: {result} }] };
52
51
  }
53
52
  \`\`\`
53
+ \`\`\`
54
54
 
55
- prompts/{prompt-name}.md (one file per ## Prompt section)
56
- ─────────────────────────
55
+ \`.mcpill/server/prompts/{prompt-name}.md\` — one file per \`## Prompt\` section:
56
+ \`\`\`
57
57
  # {prompt-name}
58
58
 
59
59
  {description}
@@ -65,26 +65,47 @@ STEPS:
65
65
  ## Message
66
66
 
67
67
  > user: {message with {{arg}} placeholders}
68
- ← omit ## Args if the prompt takes no arguments
68
+ \`\`\`
69
+ Omit \`## Args\` if the prompt takes no arguments.
69
70
 
70
- 3. TRANSLATE each tool's \`behavior\` field into a working JS async handler.
71
+ 3. **Translate** each tool's \`behavior\` field into a working JS async handler.
71
72
  - Implement the described logic fully — no stubs, no TODOs.
72
73
  - Handler is self-contained: no top-level imports.
73
74
  - For file I/O or network calls: use dynamic import inside the handler.
74
- - Return shape must be: { content: [{ type: "text", text: string }] }
75
- - Allowed parameter types: string | number | boolean. Default unknown types to string.
75
+ - Return shape: \`{ content: [{ type: "text", text: string }] }\`
76
+ - Allowed parameter types: \`string | number | boolean\`. Default unknown types to \`string\`.
76
77
 
77
- 4. RUN \`mcpill compile\` (or \`mcpill compile --dir {server-name}\`).
78
+ 4. **Run** \`mcpill compile\`.
78
79
 
79
- 5. REPORT what was generated and show the user the next commands:
80
+ 5. **Report** what was generated and show the user the next commands:
81
+ \`\`\`
80
82
  mcpill validate
81
83
  mcpill run
82
- mcpill pack / mcpill publish
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.
83
108
 
84
- EDGE CASES:
85
- - transport http without port → default to 3333
86
- - prompt message without role prefix → wrap as \`> user: {message}\`
87
- - server name with spaces → convert to kebab-case for directory and name field
88
109
  ════════════════════════════════════════════════════════════════ -->
89
110
 
90
111
  # Pill: ${projectName}
@@ -137,6 +158,67 @@ After the source files are generated:
137
158
  `;
138
159
  }
139
160
 
161
+ const HELLO_MCP_MD = `<!-- ═══════════════════════════════════════════════════════════════
162
+ HELLO-MCP — a ready-to-run example
163
+
164
+ Want to see mcpill in action before building your own server?
165
+
166
+ 1. Copy the content below into your PILL.md (replace everything).
167
+ 2. Tell Claude: "Build this PILL.md"
168
+ 3. Run: mcpill compile && mcpill validate && mcpill run
169
+ ═══════════════════════════════════════════════════════════════ -->
170
+
171
+ # Pill: hello-mcp
172
+
173
+ ---
174
+
175
+ ## Server
176
+
177
+ name: hello-mcp
178
+ description: A hello-world MCP server — see mcpill in action.
179
+ transport: stdio
180
+
181
+ ---
182
+
183
+ ## Resource: server-info
184
+
185
+ uri: info://server
186
+ name: Server Info
187
+ content: |
188
+ hello-mcp is a demo MCP server built with mcpill.
189
+ Tools: ping, echo. Prompt: introduce.
190
+
191
+ ---
192
+
193
+ ## Tool: ping
194
+
195
+ description: Returns pong with the current UTC timestamp.
196
+ inputs:
197
+ output: A pong message with the current timestamp.
198
+ behavior: |
199
+ Return the string "pong — " followed by new Date().toISOString().
200
+
201
+ ---
202
+
203
+ ## Tool: echo
204
+
205
+ description: Echoes a message back to the caller.
206
+ inputs:
207
+ - message (string): The message to echo.
208
+ output: The same message, echoed back as text.
209
+ behavior: |
210
+ Return the message input unchanged.
211
+
212
+ ---
213
+
214
+ ## Prompt: introduce
215
+
216
+ description: Ask the server to introduce itself.
217
+ behavior: |
218
+ A prompt (no args) with a single user message:
219
+ "Introduce yourself — what is hello-mcp and what can it do?"
220
+ `;
221
+
140
222
  // Embedded content of src/templates/server.md — tsup does not copy non-TS assets,
141
223
  // so the template is inlined here. Keep in sync with src/templates/server.md.
142
224
  const SERVER_MD_TEMPLATE = `## Config
@@ -180,9 +262,77 @@ Generate a greeting
180
262
  > user: Say hello to {{name}}
181
263
  `;
182
264
 
265
+ function makePillUserGuideMd(projectName: string): string {
266
+ return `# ${projectName} — mcpill user guide
267
+
268
+ ## Quickstart
269
+
270
+ 1. **Describe your server** in \`PILL.md\` — fill in the \`## Server\` block and add \`## Tool\` sections.
271
+ 2. **Build it** — open Claude Code and say:
272
+ > "Build this PILL.md"
273
+ Claude reads \`.mcpill/pill-agent-guide.md\` and generates the source files.
274
+ 3. **Compile**
275
+ \`\`\`sh
276
+ mcpill compile
277
+ \`\`\`
278
+ 4. **Test locally**
279
+ \`\`\`sh
280
+ mcpill validate # schema check
281
+ mcpill run # start the server
282
+ \`\`\`
283
+ 5. **Pack & publish**
284
+ \`\`\`sh
285
+ mcpill pack
286
+ mcpill publish
287
+ \`\`\`
288
+
289
+ ## Project layout
290
+
291
+ \`\`\`
292
+ ${projectName}/
293
+ ├── PILL.md ← describe your server here
294
+ └── .mcpill/
295
+ ├── server.md ← server config (name, transport, resources)
296
+ ├── pill-agent-guide.md ← instructions Claude follows when building
297
+ ├── pill-user-guide.md ← this file
298
+ └── server/
299
+ ├── mcpill.config.json ← compiled config
300
+ ├── tools/ ← one .md file per tool
301
+ │ └── echo.md
302
+ └── prompts/ ← one .md file per prompt (optional)
303
+ └── greeting.md
304
+ \`\`\`
305
+
306
+ ## Editing tools manually
307
+
308
+ Each file in \`.mcpill/server/tools/\` follows this shape:
309
+
310
+ \`\`\`markdown
311
+ # tool-name
312
+
313
+ One-line description.
314
+
315
+ ## Parameters
316
+
317
+ - param (string): what it is
318
+
319
+ ## Handler
320
+
321
+ \`\`\`js
322
+ async ({ param }) => {
323
+ return { content: [{ type: "text", text: param }] };
324
+ }
325
+ \`\`\`
326
+ \`\`\`
327
+
328
+ Run \`mcpill compile\` after any edit to regenerate the server bundle.
329
+ `;
330
+ }
331
+
183
332
  export async function runInit(opts: { dir?: string }): Promise<void> {
184
333
  const targetDir = path.resolve(opts.dir ?? process.cwd());
185
334
  const mcpillDir = path.join(targetDir, ".mcpill");
335
+ const serverDir = path.join(mcpillDir, "server");
186
336
  const projectName = path.basename(targetDir);
187
337
 
188
338
  if (fs.existsSync(mcpillDir)) {
@@ -190,46 +340,30 @@ export async function runInit(opts: { dir?: string }): Promise<void> {
190
340
  process.exit(1);
191
341
  }
192
342
 
193
- const configJson = JSON.stringify(
194
- { name: projectName, transport: "stdio", port: 3333 },
195
- null,
196
- 2,
197
- );
198
-
199
- fs.mkdirSync(mcpillDir, { recursive: true });
200
- fs.writeFileSync(path.join(mcpillDir, "mcpill.config.json"), configJson);
201
-
202
- const toolsDir = path.join(targetDir, "tools");
203
- const promptsDir = path.join(targetDir, "prompts");
204
- fs.mkdirSync(toolsDir, { recursive: true });
205
- fs.mkdirSync(promptsDir, { recursive: true });
206
- fs.writeFileSync(path.join(toolsDir, "echo.md"), ECHO_TOOL_MD);
207
- fs.writeFileSync(path.join(promptsDir, "greeting.md"), GREETING_PROMPT_MD);
343
+ fs.mkdirSync(path.join(serverDir, "tools"), { recursive: true });
344
+ fs.mkdirSync(path.join(serverDir, "prompts"), { recursive: true });
208
345
 
209
- const serverMd = SERVER_MD_TEMPLATE.replace(
210
- "name: my-server",
211
- `name: ${projectName}`,
346
+ const serverMd = SERVER_MD_TEMPLATE.replace("name: my-server", `name: ${projectName}`);
347
+ fs.writeFileSync(path.join(mcpillDir, "server.md"), serverMd);
348
+ fs.writeFileSync(path.join(serverDir, "tools", "echo.md"), ECHO_TOOL_MD);
349
+ fs.writeFileSync(path.join(serverDir, "prompts", "greeting.md"), GREETING_PROMPT_MD);
350
+ fs.writeFileSync(
351
+ path.join(serverDir, "mcpill.config.json"),
352
+ JSON.stringify({ name: projectName, transport: "stdio", port: 3333 }, null, 2),
212
353
  );
213
- fs.writeFileSync(path.join(targetDir, "server.md"), serverMd);
214
- fs.writeFileSync(path.join(targetDir, "PILL.md"), makePillMd(projectName));
354
+ fs.writeFileSync(path.join(mcpillDir, "pill-agent-guide.md"), PILL_AGENT_GUIDE_MD);
355
+ fs.writeFileSync(path.join(mcpillDir, "pill-user-guide.md"), makePillUserGuideMd(projectName));
356
+ fs.writeFileSync(path.join(mcpillDir, "HELLO-MCP.md"), HELLO_MCP_MD);
215
357
 
216
- const pkgJson = JSON.stringify(
217
- {
218
- name: projectName,
219
- version: "0.1.0",
220
- private: true,
221
- scripts: { pack: "mcpill pack", publish: "mcpill publish" },
222
- },
223
- null,
224
- 2,
225
- );
226
- fs.writeFileSync(path.join(targetDir, "package.json"), pkgJson);
358
+ fs.writeFileSync(path.join(targetDir, "PILL.md"), makePillMd(projectName));
227
359
 
228
- console.log("✓ PILL.md ← describe your server, then tell Claude: \"Build this PILL.md\"");
229
- console.log("✓ server.md or edit source files directly");
230
- console.log("✓ tools/echo.md");
231
- console.log("✓ prompts/greeting.md");
232
- console.log("✓ package.json");
360
+ console.log("✓ PILL.md ← describe your server, then tell Claude: \"Build this PILL.md\"");
361
+ console.log("✓ .mcpill/HELLO-MCP.md copy into PILL.md to see a working example instantly");
362
+ console.log("✓ .mcpill/server.md ← or edit source files directly");
363
+ console.log("✓ .mcpill/server/tools/echo.md");
364
+ console.log("✓ .mcpill/server/prompts/greeting.md");
365
+ console.log("✓ .mcpill/pill-agent-guide.md ← agent instructions (read by Claude)");
366
+ console.log("✓ .mcpill/pill-user-guide.md ← start here");
233
367
  console.log("");
234
368
  console.log("When ready: mcpill compile && mcpill run");
235
369
  }
@@ -6,25 +6,11 @@ import { loadConfig } from "../loaders/config.js";
6
6
  export const SERVER_ENTRY_TEMPLATE =
7
7
  "import { runPill } from 'mcpill-runtime';\nrunPill();\n";
8
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
9
  export async function runPack(dir: string): Promise<void> {
24
10
  const baseDir = path.resolve(dir);
25
11
  await runValidate(baseDir);
26
12
 
27
- const pillDir = resolvePillDir(baseDir);
13
+ const pillDir = path.join(baseDir, ".mcpill", "server");
28
14
  const config = await loadConfig(pillDir);
29
15
  const { name } = config;
30
16
 
@@ -1,7 +1,7 @@
1
1
  import path from "path";
2
2
  import fs from "fs";
3
3
  import { z } from "mcpill-runtime";
4
- import { createServer } from "mcpster";
4
+ import { createServer, applySetup } from "mcpster";
5
5
  import { loadConfig } from "../loaders/config.js";
6
6
  import { loadTools } from "../loaders/tools.js";
7
7
  import { loadPrompts } from "../loaders/prompts.js";
@@ -19,17 +19,11 @@ export async function runServer(opts: {
19
19
 
20
20
  await runValidate(baseDir);
21
21
 
22
- const pillDirs = fs
23
- .readdirSync(baseDir, { withFileTypes: true })
24
- .filter(
25
- (e) =>
26
- e.isDirectory() &&
27
- /^\.[a-z][a-z0-9-]*$/.test(e.name) &&
28
- fs.existsSync(path.join(baseDir, e.name, "mcpill.config.json")),
29
- )
30
- .map((e) => path.join(baseDir, e.name));
31
-
32
- const mcpillDir = pillDirs[0]!;
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
+ };
33
27
 
34
28
  const [tools, prompts, resources, config] = await Promise.all([
35
29
  loadTools(mcpillDir),
@@ -82,6 +76,16 @@ export async function runServer(opts: {
82
76
  });
83
77
  }
84
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
+
85
89
  try {
86
90
  await server.start();
87
91
  } catch (err) {
@@ -49,22 +49,11 @@ async function validateOne(mcpillDir: string): Promise<void> {
49
49
  }
50
50
 
51
51
  export async function runValidate(baseDir: string): Promise<void> {
52
- const pillDirs = fs
53
- .readdirSync(baseDir, { withFileTypes: true })
54
- .filter(
55
- (e) =>
56
- e.isDirectory() &&
57
- /^\.[a-z][a-z0-9-]*$/.test(e.name) &&
58
- fs.existsSync(path.join(baseDir, e.name, "mcpill.config.json")),
59
- )
60
- .map((e) => path.join(baseDir, e.name));
61
-
62
- if (pillDirs.length === 0) {
52
+ const serverDir = path.join(baseDir, ".mcpill", "server");
53
+ if (!fs.existsSync(path.join(serverDir, "mcpill.config.json"))) {
63
54
  console.error("No pill directories found — run mcpill compile first");
64
55
  process.exit(1);
65
56
  }
66
57
 
67
- for (const mcpillDir of pillDirs) {
68
- await validateOne(mcpillDir);
69
- }
58
+ await validateOne(serverDir);
70
59
  }
@@ -208,7 +208,7 @@ export function parseServerDir(dir: string): ServerDoc {
208
208
  const resourcesBody = sections.get('Resources') ?? '';
209
209
  const resources = parseResourcesSection(resourcesBody);
210
210
 
211
- const toolsDir = join(dir, 'tools');
211
+ const toolsDir = join(dir, 'server', 'tools');
212
212
  const tools: ToolDoc[] = [];
213
213
  let toolFiles: string[] = [];
214
214
  try {
@@ -220,7 +220,7 @@ export function parseServerDir(dir: string): ServerDoc {
220
220
  tools.push(parseToolFile(readFileSync(join(toolsDir, file), 'utf-8')));
221
221
  }
222
222
 
223
- const promptsDir = join(dir, 'prompts');
223
+ const promptsDir = join(dir, 'server', 'prompts');
224
224
  const prompts: PromptDoc[] = [];
225
225
  let promptFiles: string[] = [];
226
226
  try {
@@ -43,8 +43,8 @@ export function serializeServerDoc(doc: ServerDoc): string {
43
43
  }
44
44
 
45
45
  export function serializeServerDir(doc: ServerDoc, dir: string): void {
46
- mkdirSync(join(dir, 'tools'), { recursive: true });
47
- mkdirSync(join(dir, 'prompts'), { recursive: true });
46
+ mkdirSync(join(dir, 'server', 'tools'), { recursive: true });
47
+ mkdirSync(join(dir, 'server', 'prompts'), { recursive: true });
48
48
 
49
49
  let serverMd = '## Config\n';
50
50
  serverMd += `name: ${doc.config.name}\n`;
@@ -73,7 +73,7 @@ export function serializeServerDir(doc: ServerDoc, dir: string): void {
73
73
  .trim();
74
74
  md += `\n## Handler\n\n\`\`\`js\n${cleanHandler}\n\`\`\`\n`;
75
75
  }
76
- writeFileSync(join(dir, 'tools', `${tool.name}.md`), md);
76
+ writeFileSync(join(dir, 'server', 'tools', `${tool.name}.md`), md);
77
77
  }
78
78
 
79
79
  for (const prompt of doc.prompts) {
@@ -92,7 +92,7 @@ export function serializeServerDir(doc: ServerDoc, dir: string): void {
92
92
  md += `> ${msg.role}: ${msg.content}\n`;
93
93
  }
94
94
  }
95
- writeFileSync(join(dir, 'prompts', `${prompt.name}.md`), md);
95
+ writeFileSync(join(dir, 'server', 'prompts', `${prompt.name}.md`), md);
96
96
  }
97
97
 
98
98
  console.log(`✓ tools/ updated (${doc.tools.length} files)`);