mcpill 1.3.0 → 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 CHANGED
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.5.0
4
+
5
+ - `mcpill init` scaffolds `.mcpill/server/hooks/` with a commented `example-hook.md` stub covering all four Claude Code event types (`PreToolUse`, `PostToolUse`, `PreCompact`, `Stop`)
6
+ - `PILL.md` template now includes commented-out `## Hook:` and `## Prompt:` section stubs with inline field explanations
7
+ - `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
8
+ - `pill-agent-guide.md` documents `## Hook:` sections, `.mcpill/server/hooks/*.md` files, and the `--no-hooks` flag
9
+
10
+ ## 1.4.0
11
+
12
+ - `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
13
+
3
14
  ## 1.3.0
4
15
 
5
16
  - `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,10 @@ 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
+ - `package.json` — `{ type: "module", dependencies: { mcpill-runtime } }` — deps are installed automatically by `mcpill compile` and `mcpill validate`
52
53
 
53
54
  ### `mcpill compile`
54
55
 
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:
@@ -238,6 +284,37 @@ name: Status
238
284
  ---
239
285
  The server is running.
240
286
  `;
287
+ var EXAMPLE_HOOK_MD = `# example-hook
288
+
289
+ <!--
290
+ Stub hook file \u2014 fill in the fields below and run: mcpill compile
291
+
292
+ TRIGGER \u2014 which event fires this hook (required):
293
+ PreToolUse \u2014 runs before Claude calls a tool
294
+ PostToolUse \u2014 runs after a tool returns a result
295
+ PreCompact \u2014 runs before Claude compacts/summarizes its context
296
+ Stop \u2014 runs when Claude finishes responding
297
+
298
+ MATCHER \u2014 regex matched against the tool name (optional):
299
+ Used for PreToolUse and PostToolUse only; ignored for PreCompact and Stop.
300
+ Omit or leave blank to match all tools.
301
+ Examples: Read|Write|Edit (specific tools)
302
+ Bash (Bash tool only)
303
+ .* (all tools \u2014 same as omitting)
304
+
305
+ COMMAND \u2014 shell command to run when the hook fires (required):
306
+ stdout is shown to Claude as context before/after the tool call.
307
+ Exit code != 0 blocks the tool call (PreToolUse only).
308
+ Examples: echo "use pill tools instead of native file tools"
309
+ ./scripts/pre-check.sh
310
+
311
+ Copy the fields below the closing --> and fill them in to activate this hook:
312
+
313
+ trigger: PreToolUse
314
+ matcher: .*
315
+ command: echo "example hook \u2014 replace this command"
316
+ -->
317
+ `;
241
318
  var GREETING_PROMPT_MD = `# greeting
242
319
 
243
320
  Generate a greeting
@@ -365,8 +442,10 @@ ${projectName}/
365
442
  \u251C\u2500\u2500 mcpill.config.json \u2190 compiled config
366
443
  \u251C\u2500\u2500 tools/ \u2190 one .md file per tool
367
444
  \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
445
+ \u251C\u2500\u2500 prompts/ \u2190 one .md file per prompt (optional)
446
+ \u2502 \u2514\u2500\u2500 greeting.md
447
+ \u2514\u2500\u2500 hooks/ \u2190 one .md file per hook (optional)
448
+ \u2514\u2500\u2500 example-hook.md
370
449
  \`\`\`
371
450
 
372
451
  ## Editing tools manually
@@ -405,10 +484,12 @@ async function runInit(opts) {
405
484
  }
406
485
  fs.mkdirSync(path.join(serverDir, "tools"), { recursive: true });
407
486
  fs.mkdirSync(path.join(serverDir, "prompts"), { recursive: true });
487
+ fs.mkdirSync(path.join(serverDir, "hooks"), { recursive: true });
408
488
  fs.mkdirSync(path.join(targetDir, ".claude", "commands"), { recursive: true });
409
489
  const serverMd = SERVER_MD_TEMPLATE.replace("name: my-server", `name: ${projectName}`);
410
490
  fs.writeFileSync(path.join(mcpillDir, "server.md"), serverMd);
411
491
  fs.writeFileSync(path.join(serverDir, "prompts", "greeting.md"), GREETING_PROMPT_MD);
492
+ fs.writeFileSync(path.join(serverDir, "hooks", "example-hook.md"), EXAMPLE_HOOK_MD);
412
493
  fs.writeFileSync(
413
494
  path.join(serverDir, "mcpill.config.json"),
414
495
  JSON.stringify({ name: projectName, transport: "stdio", port: 3333 }, null, 2)
@@ -438,6 +519,7 @@ async function runInit(opts) {
438
519
  console.log("\u2713 .mcpill/HELLO-MCP.md \u2190 copy into PILL.md to see a working example instantly");
439
520
  console.log("\u2713 .mcpill/server.md \u2190 or edit source files directly");
440
521
  console.log("\u2713 .mcpill/server/prompts/greeting.md");
522
+ console.log("\u2713 .mcpill/server/hooks/example-hook.md \u2190 stub hook (see file for format)");
441
523
  console.log("\u2713 .mcpill/pill-agent-guide.md \u2190 agent instructions (read by Claude)");
442
524
  console.log("\u2713 .mcpill/pill-user-guide.md \u2190 start here");
443
525
  console.log("\u2713 .claude/commands/create-pill.md \u2190 /create-pill skill (say: /create-pill)");
@@ -610,13 +692,27 @@ async function loadResources(mcpillDir) {
610
692
  import fs5 from "fs";
611
693
  import path6 from "path";
612
694
 
695
+ // src/commands/ensure-deps.ts
696
+ import { execSync } from "child_process";
697
+ import { existsSync, readFileSync } from "fs";
698
+ import { join } from "path";
699
+ function ensureDeps(serverDir) {
700
+ if (!existsSync(join(serverDir, "mcpill.config.json"))) return;
701
+ if (!existsSync(join(serverDir, "package.json"))) return;
702
+ if (existsSync(join(serverDir, "node_modules"))) return;
703
+ const pkg2 = JSON.parse(readFileSync(join(serverDir, "package.json"), "utf-8"));
704
+ if (!pkg2.dependencies || Object.keys(pkg2.dependencies).length === 0) return;
705
+ console.log("Installing server dependencies\u2026");
706
+ execSync("npm install", { cwd: serverDir, stdio: "inherit" });
707
+ }
708
+
613
709
  // src/compiler/hooks.ts
614
- import { readFileSync as readFileSync2, existsSync, writeFileSync, mkdirSync } from "fs";
615
- import { join as join2 } from "path";
710
+ import { readFileSync as readFileSync3, existsSync as existsSync2, writeFileSync, mkdirSync } from "fs";
711
+ import { join as join3 } from "path";
616
712
 
617
713
  // src/compiler/parse.ts
618
- import { readdirSync, readFileSync } from "fs";
619
- import { join } from "path";
714
+ import { readdirSync, readFileSync as readFileSync2 } from "fs";
715
+ import { join as join2 } from "path";
620
716
  function extractSections(content) {
621
717
  const sections = /* @__PURE__ */ new Map();
622
718
  const pieces = ("\n" + content).split("\n## ");
@@ -747,7 +843,7 @@ function parsePromptFile(content) {
747
843
  return { name, description, args, messages };
748
844
  }
749
845
  function parseServerDir(dir) {
750
- const serverMdContent = readFileSync(join(dir, "server.md"), "utf-8");
846
+ const serverMdContent = readFileSync2(join2(dir, "server.md"), "utf-8");
751
847
  const sections = extractSections(serverMdContent);
752
848
  const configBody = sections.get("Config") ?? "";
753
849
  const configKv = parseKvBlock(configBody);
@@ -758,7 +854,7 @@ function parseServerDir(dir) {
758
854
  };
759
855
  const resourcesBody = sections.get("Resources") ?? "";
760
856
  const resources = parseResourcesSection(resourcesBody);
761
- const toolsDir = join(dir, "server", "tools");
857
+ const toolsDir = join2(dir, "server", "tools");
762
858
  const tools = [];
763
859
  let toolFiles = [];
764
860
  try {
@@ -766,9 +862,9 @@ function parseServerDir(dir) {
766
862
  } catch {
767
863
  }
768
864
  for (const file of toolFiles) {
769
- tools.push(parseToolFile(readFileSync(join(toolsDir, file), "utf-8")));
865
+ tools.push(parseToolFile(readFileSync2(join2(toolsDir, file), "utf-8")));
770
866
  }
771
- const promptsDir = join(dir, "server", "prompts");
867
+ const promptsDir = join2(dir, "server", "prompts");
772
868
  const prompts = [];
773
869
  let promptFiles = [];
774
870
  try {
@@ -776,7 +872,7 @@ function parseServerDir(dir) {
776
872
  } catch {
777
873
  }
778
874
  for (const file of promptFiles) {
779
- prompts.push(parsePromptFile(readFileSync(join(promptsDir, file), "utf-8")));
875
+ prompts.push(parsePromptFile(readFileSync2(join2(promptsDir, file), "utf-8")));
780
876
  }
781
877
  return { config, tools, prompts, resources };
782
878
  }
@@ -854,12 +950,65 @@ function buildHookEntry(pillName, tools) {
854
950
  hooks: [{ type: "command", command: `echo ${JSON.stringify(message)}` }]
855
951
  };
856
952
  }
953
+ function parsePillMdHooks(content) {
954
+ const stripped = content.replace(/<!--[\s\S]*?-->/g, "");
955
+ const hooks = [];
956
+ const pieces = ("\n" + stripped).split("\n## ");
957
+ for (const piece of pieces) {
958
+ if (!piece.toLowerCase().startsWith("hook:")) continue;
959
+ const nlIdx = piece.indexOf("\n");
960
+ const body = nlIdx === -1 ? "" : piece.slice(nlIdx + 1);
961
+ const kv = parseKvBlock(body);
962
+ const trigger = kv["trigger"]?.trim();
963
+ const matcher = kv["matcher"]?.trim();
964
+ const command = kv["command"]?.trim();
965
+ if (!trigger || !command) continue;
966
+ hooks.push({ event: trigger, ...matcher ? { matcher } : {}, command });
967
+ }
968
+ return hooks;
969
+ }
970
+ function parseHookFile(content) {
971
+ const stripped = content.replace(/<!--[\s\S]*?-->/g, "");
972
+ const kv = parseKvBlock(stripped);
973
+ const trigger = kv["trigger"]?.trim();
974
+ const matcher = kv["matcher"]?.trim();
975
+ const command = kv["command"]?.trim();
976
+ if (!trigger || !command) return null;
977
+ return { event: trigger, ...matcher ? { matcher } : {}, command };
978
+ }
979
+ function mergeUserHooksIntoSettings(baseDir, hookBlocks) {
980
+ if (hookBlocks.length === 0) return;
981
+ const claudeDir = join3(baseDir, ".claude");
982
+ const settingsPath = join3(claudeDir, "settings.json");
983
+ let settings = {};
984
+ if (existsSync2(settingsPath)) {
985
+ settings = JSON.parse(readFileSync3(settingsPath, "utf-8"));
986
+ }
987
+ if (!settings.hooks) settings.hooks = {};
988
+ for (const block of hookBlocks) {
989
+ const existing = settings.hooks[block.event] ?? [];
990
+ const newEntry = {
991
+ ...block.matcher !== void 0 ? { matcher: block.matcher } : {},
992
+ hooks: [{ type: "command", command: block.command }]
993
+ };
994
+ const matcherKey = block.matcher ?? "";
995
+ const idx = existing.findIndex((e) => (e.matcher ?? "") === matcherKey);
996
+ if (idx !== -1) {
997
+ existing[idx] = newEntry;
998
+ } else {
999
+ existing.push(newEntry);
1000
+ }
1001
+ settings.hooks[block.event] = existing;
1002
+ }
1003
+ mkdirSync(claudeDir, { recursive: true });
1004
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
1005
+ }
857
1006
  function mergeHookIntoSettings(baseDir, hookEntry) {
858
- const claudeDir = join2(baseDir, ".claude");
859
- const settingsPath = join2(claudeDir, "settings.json");
1007
+ const claudeDir = join3(baseDir, ".claude");
1008
+ const settingsPath = join3(claudeDir, "settings.json");
860
1009
  let settings = {};
861
- if (existsSync(settingsPath)) {
862
- settings = JSON.parse(readFileSync2(settingsPath, "utf-8"));
1010
+ if (existsSync2(settingsPath)) {
1011
+ settings = JSON.parse(readFileSync3(settingsPath, "utf-8"));
863
1012
  }
864
1013
  if (!settings.hooks) settings.hooks = {};
865
1014
  if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
@@ -917,6 +1066,7 @@ async function runValidate(baseDir) {
917
1066
  console.error("No pill directories found \u2014 run mcpill compile first");
918
1067
  process.exit(1);
919
1068
  }
1069
+ ensureDeps(serverDir);
920
1070
  await validateOne(serverDir);
921
1071
  if (!fs5.existsSync(path6.join(mcpillDir, ".no-hooks"))) {
922
1072
  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 +1167,13 @@ async function runServer(opts) {
1017
1167
  }
1018
1168
 
1019
1169
  // 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";
1170
+ import { resolve, join as join5 } from "path";
1171
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, existsSync as existsSync3, readdirSync as readdirSync2 } from "fs";
1022
1172
  import { pathToFileURL as pathToFileURL2 } from "url";
1023
1173
 
1024
1174
  // src/compiler/serialize.ts
1025
1175
  import { writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
1026
- import { join as join3 } from "path";
1176
+ import { join as join4 } from "path";
1027
1177
  function serializeResources(resources) {
1028
1178
  return resources.map((r) => {
1029
1179
  let fm = `uri: ${r.uri}`;
@@ -1037,8 +1187,8 @@ ${r.content}`;
1037
1187
  }).join("\n---\n");
1038
1188
  }
1039
1189
  function serializeServerDir(doc, dir) {
1040
- mkdirSync2(join3(dir, "server", "tools"), { recursive: true });
1041
- mkdirSync2(join3(dir, "server", "prompts"), { recursive: true });
1190
+ mkdirSync2(join4(dir, "server", "tools"), { recursive: true });
1191
+ mkdirSync2(join4(dir, "server", "prompts"), { recursive: true });
1042
1192
  let serverMd = "## Config\n";
1043
1193
  serverMd += `name: ${doc.config.name}
1044
1194
  `;
@@ -1054,7 +1204,7 @@ ${serializeResources(doc.resources)}
1054
1204
  } else {
1055
1205
  serverMd += "\n## Resources\n";
1056
1206
  }
1057
- writeFileSync2(join3(dir, "server.md"), serverMd);
1207
+ writeFileSync2(join4(dir, "server.md"), serverMd);
1058
1208
  console.log("\u2713 server.md updated");
1059
1209
  for (const tool of doc.tools) {
1060
1210
  let md = `# ${tool.name}
@@ -1078,7 +1228,7 @@ ${cleanHandler}
1078
1228
  \`\`\`
1079
1229
  `;
1080
1230
  }
1081
- writeFileSync2(join3(dir, "server", "tools", `${tool.name}.md`), md);
1231
+ writeFileSync2(join4(dir, "server", "tools", `${tool.name}.md`), md);
1082
1232
  }
1083
1233
  for (const prompt of doc.prompts) {
1084
1234
  let md = `# ${prompt.name}
@@ -1102,7 +1252,7 @@ ${cleanHandler}
1102
1252
  `;
1103
1253
  }
1104
1254
  }
1105
- writeFileSync2(join3(dir, "server", "prompts", `${prompt.name}.md`), md);
1255
+ writeFileSync2(join4(dir, "server", "prompts", `${prompt.name}.md`), md);
1106
1256
  }
1107
1257
  console.log(`\u2713 tools/ updated (${doc.tools.length} files)`);
1108
1258
  console.log(`\u2713 prompts/ updated (${doc.prompts.length} files)`);
@@ -1239,21 +1389,21 @@ ${r.content}`;
1239
1389
  }
1240
1390
  async function runCompile(opts) {
1241
1391
  const baseDir = resolve(opts.dir ?? process.cwd());
1242
- const mcpillDir = join4(baseDir, ".mcpill");
1243
- const serverDir = join4(mcpillDir, "server");
1392
+ const mcpillDir = join5(baseDir, ".mcpill");
1393
+ const serverDir = join5(mcpillDir, "server");
1244
1394
  if (opts.toMd) {
1245
- const configPath = join4(serverDir, "mcpill.config.json");
1246
- if (!existsSync2(configPath)) {
1395
+ const configPath = join5(serverDir, "mcpill.config.json");
1396
+ if (!existsSync3(configPath)) {
1247
1397
  throw new Error("No pill found \u2014 run mcpill compile first");
1248
1398
  }
1249
1399
  const config = await loadConfig(serverDir);
1250
1400
  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();
1401
+ const promptsPath = join5(serverDir, "prompts.json");
1402
+ const prompts = JSON.parse(readFileSync4(promptsPath, "utf-8"));
1403
+ const toolsPath = join5(serverDir, "tools.js");
1404
+ const handlers = existsSync3(toolsPath) ? extractHandlers(readFileSync4(toolsPath, "utf-8")) : /* @__PURE__ */ new Map();
1255
1405
  let tools = [];
1256
- if (existsSync2(toolsPath)) {
1406
+ if (existsSync3(toolsPath)) {
1257
1407
  const mod = await import(pathToFileURL2(toolsPath).href);
1258
1408
  tools = mod.default.map((t) => {
1259
1409
  const schema = {};
@@ -1284,13 +1434,13 @@ async function runCompile(opts) {
1284
1434
  return;
1285
1435
  }
1286
1436
  const doc = parseServerDir(mcpillDir);
1287
- const toolsJsPath = join4(serverDir, "tools.js");
1288
- const existing = existsSync2(toolsJsPath) ? extractHandlers(readFileSync3(toolsJsPath, "utf-8")) : /* @__PURE__ */ new Map();
1437
+ const toolsJsPath = join5(serverDir, "tools.js");
1438
+ const existing = existsSync3(toolsJsPath) ? extractHandlers(readFileSync4(toolsJsPath, "utf-8")) : /* @__PURE__ */ new Map();
1289
1439
  const { doc: mergedDoc, stubbed } = mergeHandlers(doc, existing);
1290
- const pillMdPath = join4(baseDir, "PILL.md");
1440
+ const pillMdPath = join5(baseDir, "PILL.md");
1291
1441
  let pillContent = null;
1292
- if (existsSync2(pillMdPath)) {
1293
- pillContent = readFileSync3(pillMdPath, "utf-8");
1442
+ if (existsSync3(pillMdPath)) {
1443
+ pillContent = readFileSync4(pillMdPath, "utf-8");
1294
1444
  const serverSection = ("\n" + pillContent).split("\n## ").find((s) => s.startsWith("Server\n"));
1295
1445
  if (serverSection) {
1296
1446
  const kv = parseKvBlock(serverSection.slice("Server\n".length));
@@ -1307,18 +1457,19 @@ async function runCompile(opts) {
1307
1457
  console.warn(`\u26A0 Stub generated for tool: ${name}`);
1308
1458
  }
1309
1459
  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));
1460
+ writeFileSync3(join5(serverDir, "mcpill.config.json"), JSON.stringify(mergedDoc.config, null, 2));
1461
+ writeFileSync3(join5(serverDir, "prompts.json"), JSON.stringify(mergedDoc.prompts, null, 2));
1462
+ writeFileSync3(join5(serverDir, "resources.md"), serializeResourcesMd(mergedDoc.resources));
1313
1463
  writeFileSync3(toolsJsPath, generateToolsJs(mergedDoc.tools));
1464
+ ensureDeps(serverDir);
1314
1465
  console.log(`\u2713 .mcpill/server/ updated from .mcpill/server.md, .mcpill/server/tools/, .mcpill/server/prompts/`);
1315
1466
  if (opts.noHooks) {
1316
- writeFileSync3(join4(mcpillDir, ".no-hooks"), "");
1467
+ writeFileSync3(join5(mcpillDir, ".no-hooks"), "");
1317
1468
  return;
1318
1469
  }
1319
- const agentMdPath = existsSync2(join4(baseDir, "AGENT.md")) ? join4(baseDir, "AGENT.md") : existsSync2(join4(mcpillDir, "AGENT.md")) ? join4(mcpillDir, "AGENT.md") : null;
1470
+ const agentMdPath = existsSync3(join5(baseDir, "AGENT.md")) ? join5(baseDir, "AGENT.md") : existsSync3(join5(mcpillDir, "AGENT.md")) ? join5(mcpillDir, "AGENT.md") : null;
1320
1471
  const pillTools = pillContent ? parsePillMdTools(pillContent) : [];
1321
- const agentTools = agentMdPath ? parseAgentMdTools(readFileSync3(agentMdPath, "utf-8")) : [];
1472
+ const agentTools = agentMdPath ? parseAgentMdTools(readFileSync4(agentMdPath, "utf-8")) : [];
1322
1473
  const toolMap = new Map([...pillTools, ...agentTools].map((t) => [t.name, t]));
1323
1474
  const allTools = Array.from(toolMap.values());
1324
1475
  const hookEntry = buildHookEntry(mergedDoc.config.name, allTools);
@@ -1326,6 +1477,25 @@ async function runCompile(opts) {
1326
1477
  mergeHookIntoSettings(baseDir, hookEntry);
1327
1478
  console.log(`\u2713 .claude/settings.json updated with PreToolUse hook`);
1328
1479
  }
1480
+ const pillHooks = pillContent ? parsePillMdHooks(pillContent) : [];
1481
+ const hooksDir = join5(mcpillDir, "server", "hooks");
1482
+ const fileHooks = [];
1483
+ if (existsSync3(hooksDir)) {
1484
+ let hookFiles = [];
1485
+ try {
1486
+ hookFiles = readdirSync2(hooksDir).filter((f) => f.endsWith(".md"));
1487
+ } catch {
1488
+ }
1489
+ for (const file of hookFiles) {
1490
+ const entry = parseHookFile(readFileSync4(join5(hooksDir, file), "utf-8"));
1491
+ if (entry) fileHooks.push(entry);
1492
+ }
1493
+ }
1494
+ const allUserHooks = [...pillHooks, ...fileHooks];
1495
+ if (allUserHooks.length > 0) {
1496
+ mergeUserHooksIntoSettings(baseDir, allUserHooks);
1497
+ console.log(`\u2713 .claude/settings.json updated with ${allUserHooks.length} user-defined hook(s)`);
1498
+ }
1329
1499
  }
1330
1500
 
1331
1501
  // src/commands/pack.ts
@@ -1374,7 +1544,7 @@ async function runPublish(dir, access) {
1374
1544
  // src/cli.ts
1375
1545
  var pkgDir = dirname(fileURLToPath(import.meta.url));
1376
1546
  var pkg = JSON.parse(
1377
- readFileSync4(join5(pkgDir, "../package.json"), "utf-8")
1547
+ readFileSync5(join6(pkgDir, "../package.json"), "utf-8")
1378
1548
  );
1379
1549
  var program = new Command();
1380
1550
  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.5.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
  },