mcpill 1.3.0 → 1.6.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 CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.6.0
4
+
5
+ - `HELLO-MCP.md` template (scaffolded by `mcpill init`) now includes a commented-out `## Hook:` section stub with inline field explanations for all four Claude Code event types
6
+
7
+ ## 1.5.0
8
+
9
+ - `mcpill init` scaffolds `.mcpill/server/hooks/` with a commented `example-hook.md` stub covering all four Claude Code event types (`PreToolUse`, `PostToolUse`, `PreCompact`, `Stop`)
10
+ - `PILL.md` template now includes commented-out `## Hook:` and `## Prompt:` section stubs with inline field explanations
11
+ - `mcpill compile` merges user-defined hooks from `PILL.md` `## Hook:` blocks and `.mcpill/server/hooks/*.md` files alongside auto-generated `replaces:` hooks into a unified `.claude/settings.json` list; all four event types supported
12
+ - `pill-agent-guide.md` documents `## Hook:` sections, `.mcpill/server/hooks/*.md` files, and the `--no-hooks` flag
13
+
14
+ ## 1.4.0
15
+
16
+ - `mcpill compile` and `mcpill validate` now auto-run `npm install` in the server directory when `node_modules` is absent and the server has declared dependencies
17
+
3
18
  ## 1.3.0
4
19
 
5
20
  - `PILL.md` tool sections now accept a `replaces` field (comma-separated native tool names, e.g. `replaces: Read, Bash`); `mcpill compile` reads these and generates the `PreToolUse` hook without requiring a separate `AGENT.md`
package/README.md CHANGED
@@ -46,9 +46,11 @@ Scaffolds a new project in the current directory:
46
46
  - `.mcpill/pill-agent-guide.md` — agent instructions read by Claude when building from `PILL.md`
47
47
  - `.mcpill/server.md` — server config + resources
48
48
  - `.mcpill/server/prompts/greeting.md` — example prompt
49
+ - `.mcpill/server/hooks/example-hook.md` — stub hook file (see file for format; all fields commented out by default)
49
50
  - `.mcpill/HELLO-MCP.md` — ready-to-run example (copy into `PILL.md` to try it)
50
51
  - `.claude/commands/create-pill.md` — `/create-pill` slash command; say `/create-pill` in Claude Code to start the guided pill-creation workflow
51
- - `package.json` — `{ type: "module", dependencies: { mcpill-runtime } }` — run `npm install`
52
+ - `README.md` — quickstart guide covering project layout and tool-editing workflow
53
+ - `package.json` — `{ type: "module", dependencies: { mcpill-runtime } }` — deps are installed automatically by `mcpill compile` and `mcpill validate`
52
54
 
53
55
  ### `mcpill compile`
54
56
 
package/dist/cli.js CHANGED
@@ -3,8 +3,8 @@
3
3
  // src/cli.ts
4
4
  import { Command } from "commander";
5
5
  import { fileURLToPath } from "url";
6
- import { dirname, join as join5 } from "path";
7
- import { readFileSync as readFileSync4 } from "fs";
6
+ import { dirname, join as join6 } from "path";
7
+ import { readFileSync as readFileSync5 } from "fs";
8
8
 
9
9
  // src/commands/init.ts
10
10
  import fs from "fs";
@@ -22,6 +22,7 @@ Instructions for building an mcpill MCP server from a filled \`PILL.md\`.
22
22
  - \`## Tool: <name>\` \u2192 one tool per section (repeatable)
23
23
  - \`## Resource: <name>\` \u2192 static resource (optional, repeatable)
24
24
  - \`## Prompt: <name>\` \u2192 prompt template (optional, repeatable)
25
+ - \`## Hook: <name>\` \u2192 hook definition (optional, repeatable) \u2014 compiled directly by \`mcpill compile\`
25
26
 
26
27
  2. **Write** the server files into the pre-created scaffold at \`.mcpill/\`:
27
28
 
@@ -76,6 +77,16 @@ Instructions for building an mcpill MCP server from a filled \`PILL.md\`.
76
77
  \`\`\`
77
78
  Omit \`## Args\` if the prompt takes no arguments.
78
79
 
80
+ \`.mcpill/server/hooks/{hook-name}.md\` \u2014 optional; one file per standalone hook (alternative to \`## Hook:\` blocks in PILL.md):
81
+ \`\`\`
82
+ trigger: PreToolUse
83
+ matcher: .*
84
+ command: echo "example"
85
+ \`\`\`
86
+ Valid trigger values: \`PreToolUse\` | \`PostToolUse\` | \`PreCompact\` | \`Stop\`.
87
+ \`matcher\` is a regex matched against the tool name (PreToolUse/PostToolUse only); omit to match all tools.
88
+ \`command\` stdout is shown to Claude; exit code != 0 blocks the tool call (PreToolUse only).
89
+
79
90
  3. **Translate** each tool's \`behavior\` field into a working JS async handler.
80
91
  - Implement the described logic fully \u2014 no stubs, no TODOs.
81
92
  - Handler is self-contained: no top-level imports.
@@ -84,8 +95,9 @@ Instructions for building an mcpill MCP server from a filled \`PILL.md\`.
84
95
  - Allowed parameter types: \`string | number | boolean\`. Default unknown types to \`string\`.
85
96
 
86
97
  4. **Run** \`mcpill compile\`.
87
- - If the pill's \`AGENT.md\` has a \`## Tools\` table with a \`replaces\` column, compile also writes a \`PreToolUse\` hook into \`.claude/settings.json\` that warns Claude to use the pill's MCP tools instead of native file tools.
88
- - To skip hook generation: \`mcpill compile --no-hooks\`.
98
+ - If the pill's \`AGENT.md\` has a \`## Tools\` table with a \`replaces\` column, compile writes a \`PreToolUse\` hook into \`.claude/settings.json\` that warns Claude to use the pill's MCP tools instead of native file tools.
99
+ - If PILL.md has \`## Hook:\` sections (or \`.mcpill/server/hooks/*.md\` files exist), compile writes those hooks into \`.claude/settings.json\` under the corresponding event key. These coexist with the auto-generated \`replaces:\` hooks \u2014 both are merged.
100
+ - To skip all hook generation: \`mcpill compile --no-hooks\`.
89
101
 
90
102
  5. **Report** what was generated and show the user the next commands:
91
103
  \`\`\`
@@ -153,6 +165,40 @@ behavior: |
153
165
 
154
166
  ---
155
167
 
168
+ <!-- Add a ## Prompt: section to define a reusable prompt template.
169
+ The build agent creates .mcpill/server/prompts/<name>.md for each.
170
+ Remove comment markers to activate.
171
+
172
+ ## Prompt: my-prompt
173
+
174
+ description: Describe what this prompt does.
175
+ args:
176
+ - query (string): The question or topic.
177
+ behavior: |
178
+ Research {{query}} and provide a structured summary.
179
+
180
+ -->
181
+
182
+ ---
183
+
184
+ <!-- Add a ## Hook: section to run a shell command when Claude Code fires an event.
185
+ Run: mcpill compile after editing to apply changes to .claude/settings.json.
186
+ Remove comment markers to activate.
187
+
188
+ trigger \u2014 event that fires the hook: PreToolUse | PostToolUse | PreCompact | Stop
189
+ matcher \u2014 regex matched against tool name (PreToolUse/PostToolUse only); blank = all tools
190
+ command \u2014 shell command; stdout shown to Claude; exit!=0 blocks (PreToolUse)
191
+
192
+ ## Hook: example-hook
193
+
194
+ trigger: PreToolUse
195
+ matcher: .*
196
+ command: echo "example hook \u2014 replace this command"
197
+
198
+ -->
199
+
200
+ ---
201
+
156
202
  ## Next Steps
157
203
 
158
204
  Fill in your server above (replace or extend the example tools), then tell Claude:
@@ -227,6 +273,24 @@ description: Ask the server to introduce itself.
227
273
  behavior: |
228
274
  A prompt (no args) with a single user message:
229
275
  "Introduce yourself \u2014 what is hello-mcp and what can it do?"
276
+
277
+ ---
278
+
279
+ <!-- Add a ## Hook: section to run a shell command when Claude Code fires an event.
280
+ Run: mcpill compile after editing to apply changes to .claude/settings.json.
281
+ Remove comment markers to activate.
282
+
283
+ trigger \u2014 event that fires the hook: PreToolUse | PostToolUse | PreCompact | Stop
284
+ matcher \u2014 regex matched against tool name (PreToolUse/PostToolUse only); blank = all tools
285
+ command \u2014 shell command; stdout shown to Claude; exit!=0 blocks (PreToolUse)
286
+
287
+ ## Hook: example-hook
288
+
289
+ trigger: PreToolUse
290
+ matcher: .*
291
+ command: echo "example hook \u2014 replace this command"
292
+
293
+ -->
230
294
  `;
231
295
  var SERVER_MD_TEMPLATE = `## Config
232
296
  name: my-server
@@ -238,6 +302,37 @@ name: Status
238
302
  ---
239
303
  The server is running.
240
304
  `;
305
+ var EXAMPLE_HOOK_MD = `# example-hook
306
+
307
+ <!--
308
+ Stub hook file \u2014 fill in the fields below and run: mcpill compile
309
+
310
+ TRIGGER \u2014 which event fires this hook (required):
311
+ PreToolUse \u2014 runs before Claude calls a tool
312
+ PostToolUse \u2014 runs after a tool returns a result
313
+ PreCompact \u2014 runs before Claude compacts/summarizes its context
314
+ Stop \u2014 runs when Claude finishes responding
315
+
316
+ MATCHER \u2014 regex matched against the tool name (optional):
317
+ Used for PreToolUse and PostToolUse only; ignored for PreCompact and Stop.
318
+ Omit or leave blank to match all tools.
319
+ Examples: Read|Write|Edit (specific tools)
320
+ Bash (Bash tool only)
321
+ .* (all tools \u2014 same as omitting)
322
+
323
+ COMMAND \u2014 shell command to run when the hook fires (required):
324
+ stdout is shown to Claude as context before/after the tool call.
325
+ Exit code != 0 blocks the tool call (PreToolUse only).
326
+ Examples: echo "use pill tools instead of native file tools"
327
+ ./scripts/pre-check.sh
328
+
329
+ Copy the fields below the closing --> and fill them in to activate this hook:
330
+
331
+ trigger: PreToolUse
332
+ matcher: .*
333
+ command: echo "example hook \u2014 replace this command"
334
+ -->
335
+ `;
241
336
  var GREETING_PROMPT_MD = `# greeting
242
337
 
243
338
  Generate a greeting
@@ -365,8 +460,10 @@ ${projectName}/
365
460
  \u251C\u2500\u2500 mcpill.config.json \u2190 compiled config
366
461
  \u251C\u2500\u2500 tools/ \u2190 one .md file per tool
367
462
  \u2502 \u2514\u2500\u2500 my-tool.md
368
- \u2514\u2500\u2500 prompts/ \u2190 one .md file per prompt (optional)
369
- \u2514\u2500\u2500 greeting.md
463
+ \u251C\u2500\u2500 prompts/ \u2190 one .md file per prompt (optional)
464
+ \u2502 \u2514\u2500\u2500 greeting.md
465
+ \u2514\u2500\u2500 hooks/ \u2190 one .md file per hook (optional)
466
+ \u2514\u2500\u2500 example-hook.md
370
467
  \`\`\`
371
468
 
372
469
  ## Editing tools manually
@@ -405,10 +502,12 @@ async function runInit(opts) {
405
502
  }
406
503
  fs.mkdirSync(path.join(serverDir, "tools"), { recursive: true });
407
504
  fs.mkdirSync(path.join(serverDir, "prompts"), { recursive: true });
505
+ fs.mkdirSync(path.join(serverDir, "hooks"), { recursive: true });
408
506
  fs.mkdirSync(path.join(targetDir, ".claude", "commands"), { recursive: true });
409
507
  const serverMd = SERVER_MD_TEMPLATE.replace("name: my-server", `name: ${projectName}`);
410
508
  fs.writeFileSync(path.join(mcpillDir, "server.md"), serverMd);
411
509
  fs.writeFileSync(path.join(serverDir, "prompts", "greeting.md"), GREETING_PROMPT_MD);
510
+ fs.writeFileSync(path.join(serverDir, "hooks", "example-hook.md"), EXAMPLE_HOOK_MD);
412
511
  fs.writeFileSync(
413
512
  path.join(serverDir, "mcpill.config.json"),
414
513
  JSON.stringify({ name: projectName, transport: "stdio", port: 3333 }, null, 2)
@@ -438,6 +537,7 @@ async function runInit(opts) {
438
537
  console.log("\u2713 .mcpill/HELLO-MCP.md \u2190 copy into PILL.md to see a working example instantly");
439
538
  console.log("\u2713 .mcpill/server.md \u2190 or edit source files directly");
440
539
  console.log("\u2713 .mcpill/server/prompts/greeting.md");
540
+ console.log("\u2713 .mcpill/server/hooks/example-hook.md \u2190 stub hook (see file for format)");
441
541
  console.log("\u2713 .mcpill/pill-agent-guide.md \u2190 agent instructions (read by Claude)");
442
542
  console.log("\u2713 .mcpill/pill-user-guide.md \u2190 start here");
443
543
  console.log("\u2713 .claude/commands/create-pill.md \u2190 /create-pill skill (say: /create-pill)");
@@ -610,13 +710,27 @@ async function loadResources(mcpillDir) {
610
710
  import fs5 from "fs";
611
711
  import path6 from "path";
612
712
 
713
+ // src/commands/ensure-deps.ts
714
+ import { execSync } from "child_process";
715
+ import { existsSync, readFileSync } from "fs";
716
+ import { join } from "path";
717
+ function ensureDeps(serverDir) {
718
+ if (!existsSync(join(serverDir, "mcpill.config.json"))) return;
719
+ if (!existsSync(join(serverDir, "package.json"))) return;
720
+ if (existsSync(join(serverDir, "node_modules"))) return;
721
+ const pkg2 = JSON.parse(readFileSync(join(serverDir, "package.json"), "utf-8"));
722
+ if (!pkg2.dependencies || Object.keys(pkg2.dependencies).length === 0) return;
723
+ console.log("Installing server dependencies\u2026");
724
+ execSync("npm install", { cwd: serverDir, stdio: "inherit" });
725
+ }
726
+
613
727
  // src/compiler/hooks.ts
614
- import { readFileSync as readFileSync2, existsSync, writeFileSync, mkdirSync } from "fs";
615
- import { join as join2 } from "path";
728
+ import { readFileSync as readFileSync3, existsSync as existsSync2, writeFileSync, mkdirSync } from "fs";
729
+ import { join as join3 } from "path";
616
730
 
617
731
  // src/compiler/parse.ts
618
- import { readdirSync, readFileSync } from "fs";
619
- import { join } from "path";
732
+ import { readdirSync, readFileSync as readFileSync2 } from "fs";
733
+ import { join as join2 } from "path";
620
734
  function extractSections(content) {
621
735
  const sections = /* @__PURE__ */ new Map();
622
736
  const pieces = ("\n" + content).split("\n## ");
@@ -747,7 +861,7 @@ function parsePromptFile(content) {
747
861
  return { name, description, args, messages };
748
862
  }
749
863
  function parseServerDir(dir) {
750
- const serverMdContent = readFileSync(join(dir, "server.md"), "utf-8");
864
+ const serverMdContent = readFileSync2(join2(dir, "server.md"), "utf-8");
751
865
  const sections = extractSections(serverMdContent);
752
866
  const configBody = sections.get("Config") ?? "";
753
867
  const configKv = parseKvBlock(configBody);
@@ -758,7 +872,7 @@ function parseServerDir(dir) {
758
872
  };
759
873
  const resourcesBody = sections.get("Resources") ?? "";
760
874
  const resources = parseResourcesSection(resourcesBody);
761
- const toolsDir = join(dir, "server", "tools");
875
+ const toolsDir = join2(dir, "server", "tools");
762
876
  const tools = [];
763
877
  let toolFiles = [];
764
878
  try {
@@ -766,9 +880,9 @@ function parseServerDir(dir) {
766
880
  } catch {
767
881
  }
768
882
  for (const file of toolFiles) {
769
- tools.push(parseToolFile(readFileSync(join(toolsDir, file), "utf-8")));
883
+ tools.push(parseToolFile(readFileSync2(join2(toolsDir, file), "utf-8")));
770
884
  }
771
- const promptsDir = join(dir, "server", "prompts");
885
+ const promptsDir = join2(dir, "server", "prompts");
772
886
  const prompts = [];
773
887
  let promptFiles = [];
774
888
  try {
@@ -776,7 +890,7 @@ function parseServerDir(dir) {
776
890
  } catch {
777
891
  }
778
892
  for (const file of promptFiles) {
779
- prompts.push(parsePromptFile(readFileSync(join(promptsDir, file), "utf-8")));
893
+ prompts.push(parsePromptFile(readFileSync2(join2(promptsDir, file), "utf-8")));
780
894
  }
781
895
  return { config, tools, prompts, resources };
782
896
  }
@@ -854,12 +968,65 @@ function buildHookEntry(pillName, tools) {
854
968
  hooks: [{ type: "command", command: `echo ${JSON.stringify(message)}` }]
855
969
  };
856
970
  }
971
+ function parsePillMdHooks(content) {
972
+ const stripped = content.replace(/<!--[\s\S]*?-->/g, "");
973
+ const hooks = [];
974
+ const pieces = ("\n" + stripped).split("\n## ");
975
+ for (const piece of pieces) {
976
+ if (!piece.toLowerCase().startsWith("hook:")) continue;
977
+ const nlIdx = piece.indexOf("\n");
978
+ const body = nlIdx === -1 ? "" : piece.slice(nlIdx + 1);
979
+ const kv = parseKvBlock(body);
980
+ const trigger = kv["trigger"]?.trim();
981
+ const matcher = kv["matcher"]?.trim();
982
+ const command = kv["command"]?.trim();
983
+ if (!trigger || !command) continue;
984
+ hooks.push({ event: trigger, ...matcher ? { matcher } : {}, command });
985
+ }
986
+ return hooks;
987
+ }
988
+ function parseHookFile(content) {
989
+ const stripped = content.replace(/<!--[\s\S]*?-->/g, "");
990
+ const kv = parseKvBlock(stripped);
991
+ const trigger = kv["trigger"]?.trim();
992
+ const matcher = kv["matcher"]?.trim();
993
+ const command = kv["command"]?.trim();
994
+ if (!trigger || !command) return null;
995
+ return { event: trigger, ...matcher ? { matcher } : {}, command };
996
+ }
997
+ function mergeUserHooksIntoSettings(baseDir, hookBlocks) {
998
+ if (hookBlocks.length === 0) return;
999
+ const claudeDir = join3(baseDir, ".claude");
1000
+ const settingsPath = join3(claudeDir, "settings.json");
1001
+ let settings = {};
1002
+ if (existsSync2(settingsPath)) {
1003
+ settings = JSON.parse(readFileSync3(settingsPath, "utf-8"));
1004
+ }
1005
+ if (!settings.hooks) settings.hooks = {};
1006
+ for (const block of hookBlocks) {
1007
+ const existing = settings.hooks[block.event] ?? [];
1008
+ const newEntry = {
1009
+ ...block.matcher !== void 0 ? { matcher: block.matcher } : {},
1010
+ hooks: [{ type: "command", command: block.command }]
1011
+ };
1012
+ const matcherKey = block.matcher ?? "";
1013
+ const idx = existing.findIndex((e) => (e.matcher ?? "") === matcherKey);
1014
+ if (idx !== -1) {
1015
+ existing[idx] = newEntry;
1016
+ } else {
1017
+ existing.push(newEntry);
1018
+ }
1019
+ settings.hooks[block.event] = existing;
1020
+ }
1021
+ mkdirSync(claudeDir, { recursive: true });
1022
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
1023
+ }
857
1024
  function mergeHookIntoSettings(baseDir, hookEntry) {
858
- const claudeDir = join2(baseDir, ".claude");
859
- const settingsPath = join2(claudeDir, "settings.json");
1025
+ const claudeDir = join3(baseDir, ".claude");
1026
+ const settingsPath = join3(claudeDir, "settings.json");
860
1027
  let settings = {};
861
- if (existsSync(settingsPath)) {
862
- settings = JSON.parse(readFileSync2(settingsPath, "utf-8"));
1028
+ if (existsSync2(settingsPath)) {
1029
+ settings = JSON.parse(readFileSync3(settingsPath, "utf-8"));
863
1030
  }
864
1031
  if (!settings.hooks) settings.hooks = {};
865
1032
  if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
@@ -917,6 +1084,7 @@ async function runValidate(baseDir) {
917
1084
  console.error("No pill directories found \u2014 run mcpill compile first");
918
1085
  process.exit(1);
919
1086
  }
1087
+ ensureDeps(serverDir);
920
1088
  await validateOne(serverDir);
921
1089
  if (!fs5.existsSync(path6.join(mcpillDir, ".no-hooks"))) {
922
1090
  const agentMdPath = fs5.existsSync(path6.join(baseDir, "AGENT.md")) ? path6.join(baseDir, "AGENT.md") : fs5.existsSync(path6.join(mcpillDir, "AGENT.md")) ? path6.join(mcpillDir, "AGENT.md") : null;
@@ -1017,13 +1185,13 @@ async function runServer(opts) {
1017
1185
  }
1018
1186
 
1019
1187
  // src/commands/compile.ts
1020
- import { resolve, join as join4 } from "path";
1021
- import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, existsSync as existsSync2 } from "fs";
1188
+ import { resolve, join as join5 } from "path";
1189
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, existsSync as existsSync3, readdirSync as readdirSync2 } from "fs";
1022
1190
  import { pathToFileURL as pathToFileURL2 } from "url";
1023
1191
 
1024
1192
  // src/compiler/serialize.ts
1025
1193
  import { writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
1026
- import { join as join3 } from "path";
1194
+ import { join as join4 } from "path";
1027
1195
  function serializeResources(resources) {
1028
1196
  return resources.map((r) => {
1029
1197
  let fm = `uri: ${r.uri}`;
@@ -1037,8 +1205,8 @@ ${r.content}`;
1037
1205
  }).join("\n---\n");
1038
1206
  }
1039
1207
  function serializeServerDir(doc, dir) {
1040
- mkdirSync2(join3(dir, "server", "tools"), { recursive: true });
1041
- mkdirSync2(join3(dir, "server", "prompts"), { recursive: true });
1208
+ mkdirSync2(join4(dir, "server", "tools"), { recursive: true });
1209
+ mkdirSync2(join4(dir, "server", "prompts"), { recursive: true });
1042
1210
  let serverMd = "## Config\n";
1043
1211
  serverMd += `name: ${doc.config.name}
1044
1212
  `;
@@ -1054,7 +1222,7 @@ ${serializeResources(doc.resources)}
1054
1222
  } else {
1055
1223
  serverMd += "\n## Resources\n";
1056
1224
  }
1057
- writeFileSync2(join3(dir, "server.md"), serverMd);
1225
+ writeFileSync2(join4(dir, "server.md"), serverMd);
1058
1226
  console.log("\u2713 server.md updated");
1059
1227
  for (const tool of doc.tools) {
1060
1228
  let md = `# ${tool.name}
@@ -1078,7 +1246,7 @@ ${cleanHandler}
1078
1246
  \`\`\`
1079
1247
  `;
1080
1248
  }
1081
- writeFileSync2(join3(dir, "server", "tools", `${tool.name}.md`), md);
1249
+ writeFileSync2(join4(dir, "server", "tools", `${tool.name}.md`), md);
1082
1250
  }
1083
1251
  for (const prompt of doc.prompts) {
1084
1252
  let md = `# ${prompt.name}
@@ -1102,7 +1270,7 @@ ${cleanHandler}
1102
1270
  `;
1103
1271
  }
1104
1272
  }
1105
- writeFileSync2(join3(dir, "server", "prompts", `${prompt.name}.md`), md);
1273
+ writeFileSync2(join4(dir, "server", "prompts", `${prompt.name}.md`), md);
1106
1274
  }
1107
1275
  console.log(`\u2713 tools/ updated (${doc.tools.length} files)`);
1108
1276
  console.log(`\u2713 prompts/ updated (${doc.prompts.length} files)`);
@@ -1239,21 +1407,21 @@ ${r.content}`;
1239
1407
  }
1240
1408
  async function runCompile(opts) {
1241
1409
  const baseDir = resolve(opts.dir ?? process.cwd());
1242
- const mcpillDir = join4(baseDir, ".mcpill");
1243
- const serverDir = join4(mcpillDir, "server");
1410
+ const mcpillDir = join5(baseDir, ".mcpill");
1411
+ const serverDir = join5(mcpillDir, "server");
1244
1412
  if (opts.toMd) {
1245
- const configPath = join4(serverDir, "mcpill.config.json");
1246
- if (!existsSync2(configPath)) {
1413
+ const configPath = join5(serverDir, "mcpill.config.json");
1414
+ if (!existsSync3(configPath)) {
1247
1415
  throw new Error("No pill found \u2014 run mcpill compile first");
1248
1416
  }
1249
1417
  const config = await loadConfig(serverDir);
1250
1418
  const resources = await loadResources(serverDir);
1251
- const promptsPath = join4(serverDir, "prompts.json");
1252
- const prompts = JSON.parse(readFileSync3(promptsPath, "utf-8"));
1253
- const toolsPath = join4(serverDir, "tools.js");
1254
- const handlers = existsSync2(toolsPath) ? extractHandlers(readFileSync3(toolsPath, "utf-8")) : /* @__PURE__ */ new Map();
1419
+ const promptsPath = join5(serverDir, "prompts.json");
1420
+ const prompts = JSON.parse(readFileSync4(promptsPath, "utf-8"));
1421
+ const toolsPath = join5(serverDir, "tools.js");
1422
+ const handlers = existsSync3(toolsPath) ? extractHandlers(readFileSync4(toolsPath, "utf-8")) : /* @__PURE__ */ new Map();
1255
1423
  let tools = [];
1256
- if (existsSync2(toolsPath)) {
1424
+ if (existsSync3(toolsPath)) {
1257
1425
  const mod = await import(pathToFileURL2(toolsPath).href);
1258
1426
  tools = mod.default.map((t) => {
1259
1427
  const schema = {};
@@ -1284,13 +1452,13 @@ async function runCompile(opts) {
1284
1452
  return;
1285
1453
  }
1286
1454
  const doc = parseServerDir(mcpillDir);
1287
- const toolsJsPath = join4(serverDir, "tools.js");
1288
- const existing = existsSync2(toolsJsPath) ? extractHandlers(readFileSync3(toolsJsPath, "utf-8")) : /* @__PURE__ */ new Map();
1455
+ const toolsJsPath = join5(serverDir, "tools.js");
1456
+ const existing = existsSync3(toolsJsPath) ? extractHandlers(readFileSync4(toolsJsPath, "utf-8")) : /* @__PURE__ */ new Map();
1289
1457
  const { doc: mergedDoc, stubbed } = mergeHandlers(doc, existing);
1290
- const pillMdPath = join4(baseDir, "PILL.md");
1458
+ const pillMdPath = join5(baseDir, "PILL.md");
1291
1459
  let pillContent = null;
1292
- if (existsSync2(pillMdPath)) {
1293
- pillContent = readFileSync3(pillMdPath, "utf-8");
1460
+ if (existsSync3(pillMdPath)) {
1461
+ pillContent = readFileSync4(pillMdPath, "utf-8");
1294
1462
  const serverSection = ("\n" + pillContent).split("\n## ").find((s) => s.startsWith("Server\n"));
1295
1463
  if (serverSection) {
1296
1464
  const kv = parseKvBlock(serverSection.slice("Server\n".length));
@@ -1307,18 +1475,19 @@ async function runCompile(opts) {
1307
1475
  console.warn(`\u26A0 Stub generated for tool: ${name}`);
1308
1476
  }
1309
1477
  mkdirSync3(serverDir, { recursive: true });
1310
- writeFileSync3(join4(serverDir, "mcpill.config.json"), JSON.stringify(mergedDoc.config, null, 2));
1311
- writeFileSync3(join4(serverDir, "prompts.json"), JSON.stringify(mergedDoc.prompts, null, 2));
1312
- writeFileSync3(join4(serverDir, "resources.md"), serializeResourcesMd(mergedDoc.resources));
1478
+ writeFileSync3(join5(serverDir, "mcpill.config.json"), JSON.stringify(mergedDoc.config, null, 2));
1479
+ writeFileSync3(join5(serverDir, "prompts.json"), JSON.stringify(mergedDoc.prompts, null, 2));
1480
+ writeFileSync3(join5(serverDir, "resources.md"), serializeResourcesMd(mergedDoc.resources));
1313
1481
  writeFileSync3(toolsJsPath, generateToolsJs(mergedDoc.tools));
1482
+ ensureDeps(serverDir);
1314
1483
  console.log(`\u2713 .mcpill/server/ updated from .mcpill/server.md, .mcpill/server/tools/, .mcpill/server/prompts/`);
1315
1484
  if (opts.noHooks) {
1316
- writeFileSync3(join4(mcpillDir, ".no-hooks"), "");
1485
+ writeFileSync3(join5(mcpillDir, ".no-hooks"), "");
1317
1486
  return;
1318
1487
  }
1319
- const agentMdPath = existsSync2(join4(baseDir, "AGENT.md")) ? join4(baseDir, "AGENT.md") : existsSync2(join4(mcpillDir, "AGENT.md")) ? join4(mcpillDir, "AGENT.md") : null;
1488
+ const agentMdPath = existsSync3(join5(baseDir, "AGENT.md")) ? join5(baseDir, "AGENT.md") : existsSync3(join5(mcpillDir, "AGENT.md")) ? join5(mcpillDir, "AGENT.md") : null;
1320
1489
  const pillTools = pillContent ? parsePillMdTools(pillContent) : [];
1321
- const agentTools = agentMdPath ? parseAgentMdTools(readFileSync3(agentMdPath, "utf-8")) : [];
1490
+ const agentTools = agentMdPath ? parseAgentMdTools(readFileSync4(agentMdPath, "utf-8")) : [];
1322
1491
  const toolMap = new Map([...pillTools, ...agentTools].map((t) => [t.name, t]));
1323
1492
  const allTools = Array.from(toolMap.values());
1324
1493
  const hookEntry = buildHookEntry(mergedDoc.config.name, allTools);
@@ -1326,6 +1495,25 @@ async function runCompile(opts) {
1326
1495
  mergeHookIntoSettings(baseDir, hookEntry);
1327
1496
  console.log(`\u2713 .claude/settings.json updated with PreToolUse hook`);
1328
1497
  }
1498
+ const pillHooks = pillContent ? parsePillMdHooks(pillContent) : [];
1499
+ const hooksDir = join5(mcpillDir, "server", "hooks");
1500
+ const fileHooks = [];
1501
+ if (existsSync3(hooksDir)) {
1502
+ let hookFiles = [];
1503
+ try {
1504
+ hookFiles = readdirSync2(hooksDir).filter((f) => f.endsWith(".md"));
1505
+ } catch {
1506
+ }
1507
+ for (const file of hookFiles) {
1508
+ const entry = parseHookFile(readFileSync4(join5(hooksDir, file), "utf-8"));
1509
+ if (entry) fileHooks.push(entry);
1510
+ }
1511
+ }
1512
+ const allUserHooks = [...pillHooks, ...fileHooks];
1513
+ if (allUserHooks.length > 0) {
1514
+ mergeUserHooksIntoSettings(baseDir, allUserHooks);
1515
+ console.log(`\u2713 .claude/settings.json updated with ${allUserHooks.length} user-defined hook(s)`);
1516
+ }
1329
1517
  }
1330
1518
 
1331
1519
  // src/commands/pack.ts
@@ -1374,7 +1562,7 @@ async function runPublish(dir, access) {
1374
1562
  // src/cli.ts
1375
1563
  var pkgDir = dirname(fileURLToPath(import.meta.url));
1376
1564
  var pkg = JSON.parse(
1377
- readFileSync4(join5(pkgDir, "../package.json"), "utf-8")
1565
+ readFileSync5(join6(pkgDir, "../package.json"), "utf-8")
1378
1566
  );
1379
1567
  var program = new Command();
1380
1568
  program.name("mcpill").version(pkg.version);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcpill",
3
- "version": "1.3.0",
3
+ "version": "1.6.0",
4
4
  "type": "module",
5
5
  "description": "CLI for building, validating, and publishing MCP servers using the pill format.",
6
6
  "homepage": "https://mcpill.ruco.dev",
@@ -11,6 +11,11 @@
11
11
  "bugs": {
12
12
  "url": "https://github.com/ruco-dev/mcpill/issues"
13
13
  },
14
+ "files": [
15
+ "dist",
16
+ "README.md",
17
+ "CHANGELOG.md"
18
+ ],
14
19
  "bin": {
15
20
  "mcpill": "./dist/cli.js"
16
21
  },