mcpill 1.12.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,12 @@
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
+
3
10
  ## 1.12.0
4
11
 
5
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.
@@ -62,7 +69,7 @@
62
69
 
63
70
  ## 1.2.4
64
71
 
65
- - 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
66
73
 
67
74
  ## 1.2.3
68
75
 
@@ -92,7 +99,7 @@
92
99
 
93
100
  ## 1.1.0
94
101
 
95
- - Renamed to `mcpill`; `@ruco-ai/mcpill` deprecated — install `mcpill` instead
96
- - `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`
97
104
  - Canonical home: https://mcpill.ruco.dev
98
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
 
@@ -1264,32 +1268,6 @@ async function runValidate(baseDir) {
1264
1268
  }
1265
1269
 
1266
1270
  // src/commands/run.ts
1267
- function patchJson(filePath, patcher) {
1268
- if (!fs6.existsSync(filePath)) return;
1269
- try {
1270
- const obj = JSON.parse(fs6.readFileSync(filePath, "utf-8"));
1271
- patcher(obj);
1272
- fs6.writeFileSync(filePath, JSON.stringify(obj, null, 2) + "\n", "utf-8");
1273
- } catch {
1274
- }
1275
- }
1276
- function desktopConfigPaths() {
1277
- const home = os.homedir();
1278
- if (process.platform === "win32") {
1279
- const appData = process.env.APPDATA ?? path7.join(home, "AppData", "Roaming");
1280
- return [path7.join(appData, "Claude", "claude_desktop_config.json")];
1281
- }
1282
- return [path7.join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json")];
1283
- }
1284
- function unregisterSync(name, baseDir) {
1285
- const remove = (obj) => {
1286
- const servers = obj.mcpServers;
1287
- if (servers) delete servers[name];
1288
- };
1289
- patchJson(path7.join(baseDir, ".claude", "settings.json"), remove);
1290
- patchJson(path7.join(os.homedir(), ".claude.json"), remove);
1291
- for (const p of desktopConfigPaths()) patchJson(p, remove);
1292
- }
1293
1271
  async function runServer(opts) {
1294
1272
  const baseDir = path7.resolve(opts.dir ?? process.cwd());
1295
1273
  await runValidate(baseDir);
@@ -1351,16 +1329,6 @@ async function runServer(opts) {
1351
1329
  args: ["run", "--dir", baseDir]
1352
1330
  }
1353
1331
  });
1354
- const cleanup = () => unregisterSync(name, baseDir);
1355
- process.on("exit", cleanup);
1356
- process.on("SIGINT", () => {
1357
- cleanup();
1358
- process.exit(0);
1359
- });
1360
- process.on("SIGTERM", () => {
1361
- cleanup();
1362
- process.exit(0);
1363
- });
1364
1332
  }
1365
1333
  try {
1366
1334
  await server.start();
@@ -1433,7 +1401,7 @@ ${tool.description}
1433
1401
  `;
1434
1402
  }
1435
1403
  if (tool.handler !== void 0) {
1436
- 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();
1437
1405
  md += `
1438
1406
  ## Handler
1439
1407
 
@@ -1485,7 +1453,7 @@ function dedent(s) {
1485
1453
  }
1486
1454
  function extractHandlers(toolsJsContent) {
1487
1455
  const result = /* @__PURE__ */ new Map();
1488
- 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;
1489
1457
  let match;
1490
1458
  while ((match = markerRegex.exec(toolsJsContent)) !== null) {
1491
1459
  result.set(match[1].trim(), dedent(match[2]).trim());
@@ -1781,9 +1749,21 @@ function installClaudeCode() {
1781
1749
  });
1782
1750
  }
1783
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
+ }
1784
1763
  writeJson(settingsPath, s);
1785
1764
  activatePill();
1786
- 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');
1787
1767
  }
1788
1768
 
1789
1769
  function uninstallClaudeCode() {
@@ -1795,6 +1775,13 @@ function uninstallClaudeCode() {
1795
1775
  const gateMarker = '/' + PILL_NAME + '/hooks/';
1796
1776
  if (s.hooks.PreToolUse) s.hooks.PreToolUse = s.hooks.PreToolUse.filter(e => !e.hooks?.[0]?.command?.includes(gateMarker));
1797
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
+ }
1798
1785
  }
1799
1786
  writeJson(settingsPath, s);
1800
1787
  deactivatePill();
@@ -1949,7 +1936,12 @@ function installCodex() {
1949
1936
  existing += '\\n\\n[mcp_servers.' + PILL_NAME + ']\\ncommand = "node"\\nargs = [' + JSON.stringify(INDEX_PATH) + ']\\n';
1950
1937
  writeFileSync(configPath, existing.trim() + '\\n');
1951
1938
  activatePill();
1952
- 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
+ }
1953
1945
  }
1954
1946
 
1955
1947
  function uninstallCodex() {
@@ -1981,10 +1973,15 @@ if (command === 'install') {
1981
1973
  }
1982
1974
  }
1983
1975
  `;
1984
- function generateStandaloneInstallJs(pillName, version, blockingHooks) {
1976
+ function generateStandaloneInstallJs(pillName, version, blockingHooks, userHooks) {
1985
1977
  const hooksData = blockingHooks.map((h) => ({
1986
1978
  required_tool: h.required_tool
1987
1979
  }));
1980
+ const userHooksData = userHooks.map((h) => ({
1981
+ event: h.event,
1982
+ command: h.command,
1983
+ ...h.matcher !== void 0 ? { matcher: h.matcher } : {}
1984
+ }));
1988
1985
  const constants = [
1989
1986
  `#!/usr/bin/env node`,
1990
1987
  `import { readFileSync, writeFileSync, mkdirSync, existsSync, rmSync } from 'fs';`,
@@ -1999,6 +1996,8 @@ function generateStandaloneInstallJs(pillName, version, blockingHooks) {
1999
1996
  `const PILL_VERSION = ${JSON.stringify(version)};`,
2000
1997
  `const HAS_BLOCKING_HOOKS = ${blockingHooks.length > 0};`,
2001
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)};`,
2002
2001
  `const VALID_AGENTS = ['claude-code', 'cursor', 'windsurf', 'github-copilot', 'codex'];`,
2003
2002
  ``,
2004
2003
  `const argv = process.argv.slice(2);`,
@@ -2044,7 +2043,7 @@ function generateDistPackageJson(pillName, version, opts) {
2044
2043
  }
2045
2044
  return JSON.stringify(pkg2, null, 2) + "\n";
2046
2045
  }
2047
- function emitStandalonePackage(baseDir, pillName, version, toolsJsContent, doc, blockingHooks, opts = {}) {
2046
+ function emitStandalonePackage(baseDir, pillName, version, toolsJsContent, doc, blockingHooks, opts = {}, userHooks = []) {
2048
2047
  const distDir = join5(baseDir, ".mcpill", "dist", pillName);
2049
2048
  const hooksDir = join5(distDir, "hooks");
2050
2049
  mkdirSync3(hooksDir, { recursive: true });
@@ -2055,7 +2054,7 @@ function emitStandalonePackage(baseDir, pillName, version, toolsJsContent, doc,
2055
2054
  join5(distDir, "index.js"),
2056
2055
  generateStandaloneIndexJs(pillName, version, doc.prompts, doc.resources, blockingHookTools)
2057
2056
  );
2058
- writeFileSync3(join5(distDir, "install.js"), generateStandaloneInstallJs(pillName, version, blockingHooks));
2057
+ writeFileSync3(join5(distDir, "install.js"), generateStandaloneInstallJs(pillName, version, blockingHooks, userHooks));
2059
2058
  if (blockingHooks.length > 0) {
2060
2059
  writeFileSync3(join5(hooksDir, "pre-tool-gate.js"), generateStandaloneGateScript(pillName, blockingHooks));
2061
2060
  writeFileSync3(join5(hooksDir, "post-analysis-flag.js"), generateStandaloneFlagScript(pillName));
@@ -2086,9 +2085,9 @@ export default [`];
2086
2085
  ...schemaLines,
2087
2086
  ` },`,
2088
2087
  ` handler:`,
2089
- ` // @handler:${tool.name}`,
2088
+ ` // ,handler:${tool.name}`,
2090
2089
  handlerBody,
2091
- ` // @end-handler:${tool.name}`,
2090
+ ` // ,end-handler:${tool.name}`,
2092
2091
  ` ,`,
2093
2092
  ` },`
2094
2093
  );
@@ -2258,7 +2257,18 @@ async function runCompile(opts) {
2258
2257
  mergeUserHooksIntoSettings(baseDir, generatedEntries);
2259
2258
  console.log(`\u2713 .claude/settings.json updated with ${blockingHooks.length} blocking hook(s)`);
2260
2259
  }
2261
- 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);
2262
2272
  console.log(`\u2713 .mcpill/dist/${mergedDoc.config.name}/ updated`);
2263
2273
  }
2264
2274
 
@@ -2296,6 +2306,14 @@ import path9 from "path";
2296
2306
  async function runPublish(dir, access) {
2297
2307
  const baseDir = path9.resolve(dir);
2298
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
+ }
2299
2317
  const result = spawnSync("npm", ["publish", "--access", access], {
2300
2318
  cwd: baseDir,
2301
2319
  stdio: "inherit"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcpill",
3
- "version": "1.12.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",