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 +10 -3
- package/README.md +10 -3
- package/dist/cli.js +65 -47
- package/package.json +1 -1
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
|
|
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`;
|
|
96
|
-
- `mcpill-runtime` renamed from
|
|
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
|
|
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
|
|
63
|
+
mcpill compile --no-hooks # skip writing all hooks to .claude/settings.json
|
|
64
64
|
```
|
|
65
65
|
|
|
66
|
-
Compile also writes
|
|
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("//
|
|
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]*\/\/
|
|
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
|
-
|
|
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)
|
|
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
|
-
` //
|
|
2088
|
+
` // ,handler:${tool.name}`,
|
|
2090
2089
|
handlerBody,
|
|
2091
|
-
` //
|
|
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
|
-
|
|
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"
|