mcpill 1.11.0 → 1.13.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,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.13.0
4
+
5
+ - Non-blocking user-defined hooks (`## Hook:` entries with `command:` but no `blocking: true`) are now distributed into the standalone `install.js` and wired into the target agent's config during `<pill> install --agent claude-code` (claude-code only in v1)
6
+ - `HAS_USER_HOOKS` flag in `install.js` is independent of `HAS_BLOCKING_HOOKS` — pills with only informational hooks produce a correct success message
7
+ - `<pill> install --agent codex` prints a warning when the pill defines hooks that Codex cannot run
8
+ - `mcpill compile` auto-generates a `UserPromptSubmit` hook for the pill's first declared tool and merges it into `.claude/settings.json`; hook is a no-op until `~/.<pill-name>/state.json` sets `enabled: true` (written by `<pill-name> install`); idempotent; skipped by `--no-hooks`
9
+
10
+ ## 1.12.0
11
+
12
+ - Fix: `runValidate` used `console.log` for the `✓ Valid:` summary line, writing plain text to stdout and breaking stdio MCP transport (clients tried to parse it as JSON-RPC and crashed). Output is now sent to `process.stderr.write` instead.
13
+ - Bumped `mcpster` dep to `^0.3.2`, which carries the same fix in its `setup.js`
14
+
3
15
  ## 1.11.0
4
16
 
5
17
  - `trigger: UserPromptSubmit` is a valid hook trigger in `PILL.md` `## Hook:` sections and `.mcpill/server/hooks/*.md` files
@@ -57,7 +69,7 @@
57
69
 
58
70
  ## 1.2.4
59
71
 
60
- - Fix: bump `mcpill-runtime` to `^0.1.2` — `0.1.0` dist imported `@ruco-ai/mcpster` (old scoped name), causing `ERR_MODULE_NOT_FOUND` on global install
72
+ - Fix: bump `mcpill-runtime` to `^0.1.2` — `0.1.0` dist imported `,ruco-ai/mcpster` (old scoped name), causing `ERR_MODULE_NOT_FOUND` on global install
61
73
 
62
74
  ## 1.2.3
63
75
 
@@ -87,7 +99,7 @@
87
99
 
88
100
  ## 1.1.0
89
101
 
90
- - Renamed to `mcpill`; `@ruco-ai/mcpill` deprecated — install `mcpill` instead
91
- - `mcpill-runtime` renamed from `@ruco-ai/mcpill-runtime`
102
+ - Renamed to `mcpill`; `,ruco-ai/mcpill` deprecated — install `mcpill` instead
103
+ - `mcpill-runtime` renamed from `,ruco-ai/mcpill-runtime`
92
104
  - Canonical home: https://mcpill.ruco.dev
93
105
  - Added `repository`, `homepage`, and `bugs` metadata to package.json
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  CLI for building, validating, and publishing MCP servers using the pill format.
4
4
 
5
- > **Renamed from `@ruco-ai/mcpill`** — if you have the scoped package, run `npm uninstall -g @ruco-ai/mcpill && npm install -g mcpill`.
5
+ > **Renamed from `,ruco-ai/mcpill`** — if you have the scoped package, run `npm uninstall -g ,ruco-ai/mcpill && npm install -g mcpill`.
6
6
 
7
7
  ## Install
8
8
 
@@ -60,10 +60,13 @@ Compiles `server.md` + `tools/*.md` + `prompts/*.md` into a `.<name>/` pill arti
60
60
  mcpill compile # forward: source → pill
61
61
  mcpill compile --to-md # reverse: pill → source
62
62
  mcpill compile --strict # error on missing tool handlers (default: stub)
63
- mcpill compile --no-hooks # skip writing PreToolUse hook to .claude/settings.json
63
+ mcpill compile --no-hooks # skip writing all hooks to .claude/settings.json
64
64
  ```
65
65
 
66
- 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 (`Read`, `Bash`, etc.). Hook mappings are read from two sources, merged at compile time — rerun `mcpill compile` after any change:
66
+ Compile also writes two auto-generated hooks into `.claude/settings.json`:
67
+
68
+ - **`PreToolUse`** — warns Claude to use the pill's MCP tools instead of native file tools (`Read`, `Bash`, etc.) when the pill declares a `replaces` field.
69
+ - **`UserPromptSubmit`** — on every user message, injects context instructing Claude to call the pill's first declared tool before responding. Present whenever the pill has at least one tool; no-op until `~/.<pill-name>/state.json` sets `enabled: true` (written by `<pill-name> install`). Hook mappings are read from two sources, merged at compile time — rerun `mcpill compile` after any change:
67
70
 
68
71
  - **`PILL.md`** (recommended) — add a `replaces` field to any `## Tool:` section:
69
72
 
@@ -97,6 +100,10 @@ At runtime the gate script exits 1 (blocking the tool call) until `mcp__{pill}__
97
100
 
98
101
  `required_tool` must name a tool defined in the same pill; `mcpill compile` aborts with an error if it doesn't.
99
102
 
103
+ #### Non-blocking hooks and distribution
104
+
105
+ `## Hook:` entries without `blocking: true` but with a `command` field emit soft hooks (context injectors) to `.claude/settings.json`. They are also bundled into the standalone distribution: when an end user runs `<pill> install --agent claude-code`, these hooks are wired into their config automatically. Codex installs print a warning when the pill defines hooks that Codex cannot run.
106
+
100
107
  ### `mcpill validate`
101
108
 
102
109
  Validates all pill directories (`.<name>/`) in the project root.
package/dist/cli.js CHANGED
@@ -97,6 +97,7 @@ Instructions for building an mcpill MCP server from a filled \`PILL.md\`.
97
97
  4. **Run** \`mcpill compile\`.
98
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
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
+ - Compile also auto-generates a \`UserPromptSubmit\` hook for the pill's first declared tool. When the pill is active (\`~/.\${pillName}/state.json\` has \`enabled: true\`), this hook injects context on every user message instructing Claude to call the main tool first. The hook is always present but is a no-op until state.json is written (typically by \`\${pillName} install\`).
100
101
  - To skip all hook generation: \`mcpill compile --no-hooks\`.
101
102
 
102
103
  5. **Report** what was generated and show the user the next commands:
@@ -422,6 +423,10 @@ After writing \`PILL.md\`, tell the user:
422
423
  > "PILL.md is ready. Review the tool descriptions, then say: **Build this PILL.md**"
423
424
 
424
425
  Do not run \`mcpill compile\` or generate source files yourself \u2014 that is the build phase, handled separately by \`pill-agent-guide.md\`.
426
+
427
+ ### Auto-generated UserPromptSubmit hook
428
+
429
+ When the user runs \`mcpill compile\`, a \`UserPromptSubmit\` hook is automatically written to \`.claude/settings.json\`. It injects context on every user message instructing Claude to call the pill's first tool before responding. The hook is always present but is a no-op until \`~/.\${pillName}/state.json\` sets \`enabled: true\` \u2014 this file is written by \`\${pillName} install\` during the distribution flow. To opt out of all hook generation the user can pass \`--no-hooks\` to compile.
425
430
  `;
426
431
  function makePillUserGuideMd(projectName) {
427
432
  return `# ${projectName} \u2014 mcpill user guide
@@ -548,7 +553,6 @@ async function runInit(opts) {
548
553
  // src/commands/run.ts
549
554
  import path7 from "path";
550
555
  import fs6 from "fs";
551
- import os from "os";
552
556
  import { z as z2 } from "mcpill-runtime";
553
557
  import { createServer, applySetup } from "mcpster";
554
558
 
@@ -1232,7 +1236,8 @@ async function validateOne(mcpillDir) {
1232
1236
  }
1233
1237
  process.exit(1);
1234
1238
  }
1235
- console.log(`\u2713 Valid: ${toolCount} tools, ${promptCount} prompts, ${resourceCount} resources`);
1239
+ process.stderr.write(`\u2713 Valid: ${toolCount} tools, ${promptCount} prompts, ${resourceCount} resources
1240
+ `);
1236
1241
  }
1237
1242
  async function runValidate(baseDir) {
1238
1243
  const mcpillDir = path6.join(baseDir, ".mcpill");
@@ -1263,32 +1268,6 @@ async function runValidate(baseDir) {
1263
1268
  }
1264
1269
 
1265
1270
  // src/commands/run.ts
1266
- function patchJson(filePath, patcher) {
1267
- if (!fs6.existsSync(filePath)) return;
1268
- try {
1269
- const obj = JSON.parse(fs6.readFileSync(filePath, "utf-8"));
1270
- patcher(obj);
1271
- fs6.writeFileSync(filePath, JSON.stringify(obj, null, 2) + "\n", "utf-8");
1272
- } catch {
1273
- }
1274
- }
1275
- function desktopConfigPaths() {
1276
- const home = os.homedir();
1277
- if (process.platform === "win32") {
1278
- const appData = process.env.APPDATA ?? path7.join(home, "AppData", "Roaming");
1279
- return [path7.join(appData, "Claude", "claude_desktop_config.json")];
1280
- }
1281
- return [path7.join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json")];
1282
- }
1283
- function unregisterSync(name, baseDir) {
1284
- const remove = (obj) => {
1285
- const servers = obj.mcpServers;
1286
- if (servers) delete servers[name];
1287
- };
1288
- patchJson(path7.join(baseDir, ".claude", "settings.json"), remove);
1289
- patchJson(path7.join(os.homedir(), ".claude.json"), remove);
1290
- for (const p of desktopConfigPaths()) patchJson(p, remove);
1291
- }
1292
1271
  async function runServer(opts) {
1293
1272
  const baseDir = path7.resolve(opts.dir ?? process.cwd());
1294
1273
  await runValidate(baseDir);
@@ -1350,16 +1329,6 @@ async function runServer(opts) {
1350
1329
  args: ["run", "--dir", baseDir]
1351
1330
  }
1352
1331
  });
1353
- const cleanup = () => unregisterSync(name, baseDir);
1354
- process.on("exit", cleanup);
1355
- process.on("SIGINT", () => {
1356
- cleanup();
1357
- process.exit(0);
1358
- });
1359
- process.on("SIGTERM", () => {
1360
- cleanup();
1361
- process.exit(0);
1362
- });
1363
1332
  }
1364
1333
  try {
1365
1334
  await server.start();
@@ -1432,7 +1401,7 @@ ${tool.description}
1432
1401
  `;
1433
1402
  }
1434
1403
  if (tool.handler !== void 0) {
1435
- const cleanHandler = tool.handler.split("\n").filter((l) => !l.trim().startsWith("// @handler:") && !l.trim().startsWith("// @end-handler:")).join("\n").trim();
1404
+ const cleanHandler = tool.handler.split("\n").filter((l) => !l.trim().startsWith("// ,handler:") && !l.trim().startsWith("// ,end-handler:")).join("\n").trim();
1436
1405
  md += `
1437
1406
  ## Handler
1438
1407
 
@@ -1484,7 +1453,7 @@ function dedent(s) {
1484
1453
  }
1485
1454
  function extractHandlers(toolsJsContent) {
1486
1455
  const result = /* @__PURE__ */ new Map();
1487
- const markerRegex = /[ \t]*\/\/ @handler:([^\n]+)\n([\s\S]*?)\n[ \t]*\/\/ @end-handler:[^\n]+/g;
1456
+ const markerRegex = /[ \t]*\/\/ ,handler:([^\n]+)\n([\s\S]*?)\n[ \t]*\/\/ ,end-handler:[^\n]+/g;
1488
1457
  let match;
1489
1458
  while ((match = markerRegex.exec(toolsJsContent)) !== null) {
1490
1459
  result.set(match[1].trim(), dedent(match[2]).trim());
@@ -1780,9 +1749,21 @@ function installClaudeCode() {
1780
1749
  });
1781
1750
  }
1782
1751
  }
1752
+ if (HAS_USER_HOOKS) {
1753
+ if (!s.hooks) s.hooks = {};
1754
+ for (const uh of USER_HOOKS) {
1755
+ if (!s.hooks[uh.event]) s.hooks[uh.event] = [];
1756
+ s.hooks[uh.event] = s.hooks[uh.event].filter(e => e.hooks?.[0]?.command !== uh.command);
1757
+ const entry = uh.event === 'UserPromptSubmit'
1758
+ ? { hooks: [{ type: 'command', command: uh.command }] }
1759
+ : { matcher: uh.matcher ?? '.*', hooks: [{ type: 'command', command: uh.command }] };
1760
+ s.hooks[uh.event].push(entry);
1761
+ }
1762
+ }
1783
1763
  writeJson(settingsPath, s);
1784
1764
  activatePill();
1785
- if (!silent) process.stdout.write('[mcpill] info: ' + (wasInstalled ? 'reinstalled' : 'installed') + ' ' + PILL_NAME + (HAS_BLOCKING_HOOKS ? '' : ' (no blocking hooks \u2014 MCP server only)') + ' for claude-code\\n');
1765
+ const _hookSuffix = (HAS_BLOCKING_HOOKS || HAS_USER_HOOKS) ? '' : ' (no hooks \u2014 MCP server only)';
1766
+ if (!silent) process.stdout.write('[mcpill] info: ' + (wasInstalled ? 'reinstalled' : 'installed') + ' ' + PILL_NAME + _hookSuffix + ' for claude-code\\n');
1786
1767
  }
1787
1768
 
1788
1769
  function uninstallClaudeCode() {
@@ -1794,6 +1775,13 @@ function uninstallClaudeCode() {
1794
1775
  const gateMarker = '/' + PILL_NAME + '/hooks/';
1795
1776
  if (s.hooks.PreToolUse) s.hooks.PreToolUse = s.hooks.PreToolUse.filter(e => !e.hooks?.[0]?.command?.includes(gateMarker));
1796
1777
  if (s.hooks.PostToolUse) s.hooks.PostToolUse = s.hooks.PostToolUse.filter(e => !e.matcher?.startsWith('mcp__' + PILL_NAME + '__'));
1778
+ if (HAS_USER_HOOKS) {
1779
+ for (const uh of USER_HOOKS) {
1780
+ if (s.hooks[uh.event]) {
1781
+ s.hooks[uh.event] = s.hooks[uh.event].filter(e => e.hooks?.[0]?.command !== uh.command);
1782
+ }
1783
+ }
1784
+ }
1797
1785
  }
1798
1786
  writeJson(settingsPath, s);
1799
1787
  deactivatePill();
@@ -1948,7 +1936,12 @@ function installCodex() {
1948
1936
  existing += '\\n\\n[mcp_servers.' + PILL_NAME + ']\\ncommand = "node"\\nargs = [' + JSON.stringify(INDEX_PATH) + ']\\n';
1949
1937
  writeFileSync(configPath, existing.trim() + '\\n');
1950
1938
  activatePill();
1951
- if (!silent) process.stdout.write('[mcpill] info: installed ' + PILL_NAME + ' for codex (MCP server only \u2014 hooks not yet supported for codex)\\n');
1939
+ if (!silent) {
1940
+ if (HAS_USER_HOOKS || HAS_BLOCKING_HOOKS) {
1941
+ process.stderr.write('[mcpill] warning: ' + PILL_NAME + ' defines hooks that Codex does not support \u2014 hook-dependent features will not work\\n');
1942
+ }
1943
+ process.stdout.write('[mcpill] info: installed ' + PILL_NAME + ' for codex (MCP server only \u2014 hooks not yet supported for codex)\\n');
1944
+ }
1952
1945
  }
1953
1946
 
1954
1947
  function uninstallCodex() {
@@ -1980,10 +1973,15 @@ if (command === 'install') {
1980
1973
  }
1981
1974
  }
1982
1975
  `;
1983
- function generateStandaloneInstallJs(pillName, version, blockingHooks) {
1976
+ function generateStandaloneInstallJs(pillName, version, blockingHooks, userHooks) {
1984
1977
  const hooksData = blockingHooks.map((h) => ({
1985
1978
  required_tool: h.required_tool
1986
1979
  }));
1980
+ const userHooksData = userHooks.map((h) => ({
1981
+ event: h.event,
1982
+ command: h.command,
1983
+ ...h.matcher !== void 0 ? { matcher: h.matcher } : {}
1984
+ }));
1987
1985
  const constants = [
1988
1986
  `#!/usr/bin/env node`,
1989
1987
  `import { readFileSync, writeFileSync, mkdirSync, existsSync, rmSync } from 'fs';`,
@@ -1998,6 +1996,8 @@ function generateStandaloneInstallJs(pillName, version, blockingHooks) {
1998
1996
  `const PILL_VERSION = ${JSON.stringify(version)};`,
1999
1997
  `const HAS_BLOCKING_HOOKS = ${blockingHooks.length > 0};`,
2000
1998
  `const HOOKS = ${JSON.stringify(hooksData, null, 2)};`,
1999
+ `const HAS_USER_HOOKS = ${userHooks.length > 0};`,
2000
+ `const USER_HOOKS = ${JSON.stringify(userHooksData, null, 2)};`,
2001
2001
  `const VALID_AGENTS = ['claude-code', 'cursor', 'windsurf', 'github-copilot', 'codex'];`,
2002
2002
  ``,
2003
2003
  `const argv = process.argv.slice(2);`,
@@ -2043,7 +2043,7 @@ function generateDistPackageJson(pillName, version, opts) {
2043
2043
  }
2044
2044
  return JSON.stringify(pkg2, null, 2) + "\n";
2045
2045
  }
2046
- function emitStandalonePackage(baseDir, pillName, version, toolsJsContent, doc, blockingHooks, opts = {}) {
2046
+ function emitStandalonePackage(baseDir, pillName, version, toolsJsContent, doc, blockingHooks, opts = {}, userHooks = []) {
2047
2047
  const distDir = join5(baseDir, ".mcpill", "dist", pillName);
2048
2048
  const hooksDir = join5(distDir, "hooks");
2049
2049
  mkdirSync3(hooksDir, { recursive: true });
@@ -2054,7 +2054,7 @@ function emitStandalonePackage(baseDir, pillName, version, toolsJsContent, doc,
2054
2054
  join5(distDir, "index.js"),
2055
2055
  generateStandaloneIndexJs(pillName, version, doc.prompts, doc.resources, blockingHookTools)
2056
2056
  );
2057
- writeFileSync3(join5(distDir, "install.js"), generateStandaloneInstallJs(pillName, version, blockingHooks));
2057
+ writeFileSync3(join5(distDir, "install.js"), generateStandaloneInstallJs(pillName, version, blockingHooks, userHooks));
2058
2058
  if (blockingHooks.length > 0) {
2059
2059
  writeFileSync3(join5(hooksDir, "pre-tool-gate.js"), generateStandaloneGateScript(pillName, blockingHooks));
2060
2060
  writeFileSync3(join5(hooksDir, "post-analysis-flag.js"), generateStandaloneFlagScript(pillName));
@@ -2085,9 +2085,9 @@ export default [`];
2085
2085
  ...schemaLines,
2086
2086
  ` },`,
2087
2087
  ` handler:`,
2088
- ` // @handler:${tool.name}`,
2088
+ ` // ,handler:${tool.name}`,
2089
2089
  handlerBody,
2090
- ` // @end-handler:${tool.name}`,
2090
+ ` // ,end-handler:${tool.name}`,
2091
2091
  ` ,`,
2092
2092
  ` },`
2093
2093
  );
@@ -2257,7 +2257,18 @@ async function runCompile(opts) {
2257
2257
  mergeUserHooksIntoSettings(baseDir, generatedEntries);
2258
2258
  console.log(`\u2713 .claude/settings.json updated with ${blockingHooks.length} blocking hook(s)`);
2259
2259
  }
2260
- emitStandalonePackage(baseDir, mergedDoc.config.name, pillVersion, toolsJsContent, mergedDoc, blockingHooks, { postinstall: pillPostinstall });
2260
+ if (mergedDoc.tools.length > 0) {
2261
+ const mainToolName = mergedDoc.tools[0].name;
2262
+ const pillName = mergedDoc.config.name;
2263
+ const pillNameUpper = pillName.toUpperCase();
2264
+ const additionalContext = `${pillNameUpper} ACTIVE: You MUST call mcp__${pillName}__${mainToolName} with the user text before responding. If results are relevant, mention them in your reply.`;
2265
+ const hookJson = JSON.stringify({ hookSpecificOutput: { hookEventName: "UserPromptSubmit", additionalContext } });
2266
+ const autoHookCommand = `enabled=$(jq -r '.enabled // false' ~/.${pillName}/state.json 2>/dev/null || echo false); [ "$enabled" = "true" ] && printf '${hookJson}' || true`;
2267
+ mergeUserHooksIntoSettings(baseDir, [{ event: "UserPromptSubmit", command: autoHookCommand }]);
2268
+ console.log(`\u2713 .claude/settings.json updated with UserPromptSubmit auto-hook (main tool: ${mainToolName})`);
2269
+ }
2270
+ const distributedUserHooks = regularHooks.filter((h) => !!h.command);
2271
+ emitStandalonePackage(baseDir, mergedDoc.config.name, pillVersion, toolsJsContent, mergedDoc, blockingHooks, { postinstall: pillPostinstall }, distributedUserHooks);
2261
2272
  console.log(`\u2713 .mcpill/dist/${mergedDoc.config.name}/ updated`);
2262
2273
  }
2263
2274
 
@@ -2295,6 +2306,14 @@ import path9 from "path";
2295
2306
  async function runPublish(dir, access) {
2296
2307
  const baseDir = path9.resolve(dir);
2297
2308
  await runPack(baseDir);
2309
+ const whoami = spawnSync("npm", ["whoami"], { stdio: "pipe", encoding: "utf-8" });
2310
+ if (whoami.status !== 0) {
2311
+ console.log("Not logged in to npm \u2014 running npm login...");
2312
+ const login = spawnSync("npm", ["login"], { stdio: "inherit" });
2313
+ if (login.status !== 0) {
2314
+ throw new Error("npm login failed");
2315
+ }
2316
+ }
2298
2317
  const result = spawnSync("npm", ["publish", "--access", access], {
2299
2318
  cwd: baseDir,
2300
2319
  stdio: "inherit"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcpill",
3
- "version": "1.11.0",
3
+ "version": "1.13.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",
@@ -27,7 +27,7 @@
27
27
  },
28
28
  "dependencies": {
29
29
  "mcpill-runtime": "^0.1.2",
30
- "mcpster": "^0.3.0",
30
+ "mcpster": "^0.3.2",
31
31
  "commander": "^12"
32
32
  },
33
33
  "devDependencies": {