mcpill 1.7.0 → 1.9.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 +11 -0
- package/README.md +22 -16
- package/dist/cli.js +217 -81
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.9.0
|
|
4
|
+
|
|
5
|
+
- `blocking: true` field on `PILL.md` `## Hook:` blocks; `mcpill compile` generates a `.mjs` `PreToolUse` gate script (exits 1 until a required tool has run) and a `PostToolUse` flag script
|
|
6
|
+
- `required_tool`, `gate_window_seconds` (default 120 s), and multiline `instructions` fields supported on blocking hooks
|
|
7
|
+
- Compile-time validation: `required_tool` must name a known tool; error surfaced during `mcpill compile`
|
|
8
|
+
|
|
9
|
+
## 1.8.0
|
|
10
|
+
|
|
11
|
+
- `mcpill run` (stdio) now auto-registers the server in Claude config on start and auto-unregisters on SIGINT, SIGTERM, and process exit — no manual step required
|
|
12
|
+
- `--register` flag and `mcpill unregister` command removed; lifecycle management is now automatic
|
|
13
|
+
|
|
3
14
|
## 1.7.0
|
|
4
15
|
|
|
5
16
|
- `mcpill run` is now side-effect-free by default — stopping the process disconnects the server; no Claude config is written
|
package/README.md
CHANGED
|
@@ -20,8 +20,7 @@ Describe your server in plain English, let Claude generate the source files.
|
|
|
20
20
|
mcpill init # scaffolds PILL.md + example source files
|
|
21
21
|
# fill PILL.md # describe your server: tools, resources, prompts
|
|
22
22
|
# tell Claude: "Build this PILL.md" # agent generates source files + runs compile
|
|
23
|
-
mcpill run # start the server
|
|
24
|
-
mcpill run --register # or: register so Claude manages it persistently
|
|
23
|
+
mcpill run # start the server — registered while running, removed on exit
|
|
25
24
|
```
|
|
26
25
|
|
|
27
26
|
`PILL.md` embeds the agent instructions — Claude knows exactly what to generate and how.
|
|
@@ -79,6 +78,25 @@ Compile also writes a `PreToolUse` hook into `.claude/settings.json` that warns
|
|
|
79
78
|
|
|
80
79
|
- **`AGENT.md`** — a `## Tools` table with a `Replaces` column (existing format). AGENT.md wins on name conflicts.
|
|
81
80
|
|
|
81
|
+
#### Blocking hooks
|
|
82
|
+
|
|
83
|
+
For mandatory workflow steps, add a `## Hook:` section to `PILL.md` with `blocking: true`. Compile generates a hard-enforcement gate instead of a soft reminder:
|
|
84
|
+
|
|
85
|
+
```markdown
|
|
86
|
+
## Hook: my-hook
|
|
87
|
+
|
|
88
|
+
trigger: PreToolUse
|
|
89
|
+
blocking: true
|
|
90
|
+
required_tool: analyze-english
|
|
91
|
+
gate_window_seconds: 120
|
|
92
|
+
instructions: |
|
|
93
|
+
Before calling any other tool, call mcp__my-pill__analyze-english first.
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
At runtime the gate script exits 1 (blocking the tool call) until `mcp__{pill}__analyze-english` has run within the window. After it runs, the flag script records the timestamp and the gate passes for `gate_window_seconds` seconds (default 120). Pill's own MCP tools are always allowed through. The gate is fail-open — if the pill's state file is missing, the gate passes.
|
|
97
|
+
|
|
98
|
+
`required_tool` must name a tool defined in the same pill; `mcpill compile` aborts with an error if it doesn't.
|
|
99
|
+
|
|
82
100
|
### `mcpill validate`
|
|
83
101
|
|
|
84
102
|
Validates all pill directories (`.<name>/`) in the project root.
|
|
@@ -88,23 +106,11 @@ Validates all pill directories (`.<name>/`) in the project root.
|
|
|
88
106
|
Starts the MCP server from the pill artifact.
|
|
89
107
|
|
|
90
108
|
```bash
|
|
91
|
-
mcpill run # stdio (default)
|
|
109
|
+
mcpill run # stdio (default) — registered while running, removed on exit
|
|
92
110
|
mcpill run --transport http --port 3333
|
|
93
|
-
mcpill run --register # also write server entry to Claude config
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
`--register` is opt-in. Without it, running the server has no side effects on Claude's configuration — stopping the process fully disconnects it.
|
|
97
|
-
|
|
98
|
-
### `mcpill unregister`
|
|
99
|
-
|
|
100
|
-
Removes the server entry from all Claude config files — the inverse of `mcpill run --register`.
|
|
101
|
-
|
|
102
|
-
```bash
|
|
103
|
-
mcpill unregister
|
|
104
|
-
mcpill unregister --dir <path>
|
|
105
111
|
```
|
|
106
112
|
|
|
107
|
-
|
|
113
|
+
For stdio transport, the server entry is automatically written to Claude's config on start and removed on exit (SIGINT, SIGTERM, or normal process exit). HTTP transport is self-hosted — Claude connects to a URL, so no registration occurs.
|
|
108
114
|
|
|
109
115
|
### `mcpill pack`
|
|
110
116
|
|
package/dist/cli.js
CHANGED
|
@@ -548,6 +548,7 @@ async function runInit(opts) {
|
|
|
548
548
|
// src/commands/run.ts
|
|
549
549
|
import path7 from "path";
|
|
550
550
|
import fs6 from "fs";
|
|
551
|
+
import os from "os";
|
|
551
552
|
import { z as z2 } from "mcpill-runtime";
|
|
552
553
|
import { createServer, applySetup } from "mcpster";
|
|
553
554
|
|
|
@@ -755,12 +756,45 @@ function extractFencedBlock(body, lang) {
|
|
|
755
756
|
}
|
|
756
757
|
function parseKvBlock(body) {
|
|
757
758
|
const result = {};
|
|
758
|
-
|
|
759
|
+
const lines = body.split("\n");
|
|
760
|
+
let i = 0;
|
|
761
|
+
while (i < lines.length) {
|
|
762
|
+
const line = lines[i];
|
|
759
763
|
const colonIdx = line.indexOf(":");
|
|
760
|
-
if (colonIdx === -1)
|
|
764
|
+
if (colonIdx === -1) {
|
|
765
|
+
i++;
|
|
766
|
+
continue;
|
|
767
|
+
}
|
|
761
768
|
const key = line.slice(0, colonIdx).trim();
|
|
762
769
|
const value = line.slice(colonIdx + 1).trim();
|
|
763
|
-
if (key)
|
|
770
|
+
if (!key) {
|
|
771
|
+
i++;
|
|
772
|
+
continue;
|
|
773
|
+
}
|
|
774
|
+
if (value === "|") {
|
|
775
|
+
i++;
|
|
776
|
+
const blockLines = [];
|
|
777
|
+
let baseIndent = -1;
|
|
778
|
+
while (i < lines.length) {
|
|
779
|
+
const nextLine = lines[i];
|
|
780
|
+
const trimmed = nextLine.trimStart();
|
|
781
|
+
if (trimmed === "") {
|
|
782
|
+
blockLines.push("");
|
|
783
|
+
i++;
|
|
784
|
+
continue;
|
|
785
|
+
}
|
|
786
|
+
const indent = nextLine.length - trimmed.length;
|
|
787
|
+
if (baseIndent === -1) baseIndent = indent;
|
|
788
|
+
if (indent < baseIndent) break;
|
|
789
|
+
blockLines.push(nextLine.slice(baseIndent));
|
|
790
|
+
i++;
|
|
791
|
+
}
|
|
792
|
+
while (blockLines.length > 0 && blockLines[blockLines.length - 1] === "") blockLines.pop();
|
|
793
|
+
result[key] = blockLines.join("\n");
|
|
794
|
+
} else {
|
|
795
|
+
result[key] = value;
|
|
796
|
+
i++;
|
|
797
|
+
}
|
|
764
798
|
}
|
|
765
799
|
return result;
|
|
766
800
|
}
|
|
@@ -975,13 +1009,31 @@ function parsePillMdHooks(content) {
|
|
|
975
1009
|
for (const piece of pieces) {
|
|
976
1010
|
if (!piece.toLowerCase().startsWith("hook:")) continue;
|
|
977
1011
|
const nlIdx = piece.indexOf("\n");
|
|
1012
|
+
const sectionTitle = (nlIdx === -1 ? piece : piece.slice(0, nlIdx)).trim();
|
|
1013
|
+
const hookName = sectionTitle.slice("hook:".length).trim();
|
|
978
1014
|
const body = nlIdx === -1 ? "" : piece.slice(nlIdx + 1);
|
|
979
1015
|
const kv = parseKvBlock(body);
|
|
980
1016
|
const trigger = kv["trigger"]?.trim();
|
|
981
1017
|
const matcher = kv["matcher"]?.trim();
|
|
982
1018
|
const command = kv["command"]?.trim();
|
|
983
|
-
|
|
984
|
-
|
|
1019
|
+
const blocking = kv["blocking"]?.trim().toLowerCase() === "true";
|
|
1020
|
+
if (!trigger || !command && !blocking) continue;
|
|
1021
|
+
const entry = {
|
|
1022
|
+
...hookName ? { name: hookName } : {},
|
|
1023
|
+
event: trigger,
|
|
1024
|
+
...matcher ? { matcher } : {},
|
|
1025
|
+
...command ? { command } : {},
|
|
1026
|
+
...blocking ? { blocking: true } : {}
|
|
1027
|
+
};
|
|
1028
|
+
if (blocking) {
|
|
1029
|
+
const requiredTool = kv["required_tool"]?.trim();
|
|
1030
|
+
if (requiredTool) entry.required_tool = requiredTool;
|
|
1031
|
+
const windowRaw = kv["gate_window_seconds"]?.trim();
|
|
1032
|
+
if (windowRaw) entry.gate_window_seconds = parseInt(windowRaw, 10);
|
|
1033
|
+
const instructions = kv["instructions"]?.trim();
|
|
1034
|
+
if (instructions) entry.instructions = instructions;
|
|
1035
|
+
}
|
|
1036
|
+
hooks.push(entry);
|
|
985
1037
|
}
|
|
986
1038
|
return hooks;
|
|
987
1039
|
}
|
|
@@ -991,8 +1043,23 @@ function parseHookFile(content) {
|
|
|
991
1043
|
const trigger = kv["trigger"]?.trim();
|
|
992
1044
|
const matcher = kv["matcher"]?.trim();
|
|
993
1045
|
const command = kv["command"]?.trim();
|
|
994
|
-
|
|
995
|
-
|
|
1046
|
+
const blocking = kv["blocking"]?.trim().toLowerCase() === "true";
|
|
1047
|
+
if (!trigger || !command && !blocking) return null;
|
|
1048
|
+
const entry = {
|
|
1049
|
+
event: trigger,
|
|
1050
|
+
...matcher ? { matcher } : {},
|
|
1051
|
+
...command ? { command } : {},
|
|
1052
|
+
...blocking ? { blocking: true } : {}
|
|
1053
|
+
};
|
|
1054
|
+
if (blocking) {
|
|
1055
|
+
const requiredTool = kv["required_tool"]?.trim();
|
|
1056
|
+
if (requiredTool) entry.required_tool = requiredTool;
|
|
1057
|
+
const windowRaw = kv["gate_window_seconds"]?.trim();
|
|
1058
|
+
if (windowRaw) entry.gate_window_seconds = parseInt(windowRaw, 10);
|
|
1059
|
+
const instructions = kv["instructions"]?.trim();
|
|
1060
|
+
if (instructions) entry.instructions = instructions;
|
|
1061
|
+
}
|
|
1062
|
+
return entry;
|
|
996
1063
|
}
|
|
997
1064
|
function mergeUserHooksIntoSettings(baseDir, hookBlocks) {
|
|
998
1065
|
if (hookBlocks.length === 0) return;
|
|
@@ -1039,6 +1106,91 @@ function mergeHookIntoSettings(baseDir, hookEntry) {
|
|
|
1039
1106
|
mkdirSync(claudeDir, { recursive: true });
|
|
1040
1107
|
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
1041
1108
|
}
|
|
1109
|
+
function generateGateScript(pillName, requiredTool, windowSeconds, instructions) {
|
|
1110
|
+
const instrJson = JSON.stringify(instructions);
|
|
1111
|
+
return [
|
|
1112
|
+
`import { readFileSync, existsSync } from 'fs';`,
|
|
1113
|
+
`import { homedir } from 'os';`,
|
|
1114
|
+
`import { join } from 'path';`,
|
|
1115
|
+
``,
|
|
1116
|
+
`const PILL_NAME = ${JSON.stringify(pillName)};`,
|
|
1117
|
+
`const REQUIRED_TOOL = ${JSON.stringify(requiredTool)};`,
|
|
1118
|
+
`const GATE_WINDOW_SECONDS = ${windowSeconds};`,
|
|
1119
|
+
`const INSTRUCTIONS = ${instrJson};`,
|
|
1120
|
+
``,
|
|
1121
|
+
`const raw = readFileSync(0, 'utf-8');`,
|
|
1122
|
+
`const input = JSON.parse(raw);`,
|
|
1123
|
+
`const toolName = input.tool_name ?? '';`,
|
|
1124
|
+
``,
|
|
1125
|
+
`if (toolName.startsWith(\`mcp__\${PILL_NAME}__\`)) process.exit(0);`,
|
|
1126
|
+
``,
|
|
1127
|
+
`const stateFile = join(homedir(), \`.\${PILL_NAME}\`, 'state.json');`,
|
|
1128
|
+
`if (!existsSync(stateFile)) process.exit(0);`,
|
|
1129
|
+
``,
|
|
1130
|
+
`const state = JSON.parse(readFileSync(stateFile, 'utf-8'));`,
|
|
1131
|
+
`if (state.enabled === false) process.exit(0);`,
|
|
1132
|
+
``,
|
|
1133
|
+
`const flagFile = join(homedir(), \`.\${PILL_NAME}\`, \`last_\${REQUIRED_TOOL}_run.json\`);`,
|
|
1134
|
+
`if (existsSync(flagFile)) {`,
|
|
1135
|
+
` const flag = JSON.parse(readFileSync(flagFile, 'utf-8'));`,
|
|
1136
|
+
` const elapsedMs = Date.now() - new Date(flag.timestamp).getTime();`,
|
|
1137
|
+
` if (elapsedMs <= GATE_WINDOW_SECONDS * 1000) process.exit(0);`,
|
|
1138
|
+
`}`,
|
|
1139
|
+
``,
|
|
1140
|
+
`process.stderr.write(INSTRUCTIONS + '\\n');`,
|
|
1141
|
+
`process.exit(1);`,
|
|
1142
|
+
``
|
|
1143
|
+
].join("\n");
|
|
1144
|
+
}
|
|
1145
|
+
function generateFlagScript(pillName, requiredTool) {
|
|
1146
|
+
return [
|
|
1147
|
+
`import { writeFileSync, mkdirSync } from 'fs';`,
|
|
1148
|
+
`import { homedir } from 'os';`,
|
|
1149
|
+
`import { join } from 'path';`,
|
|
1150
|
+
``,
|
|
1151
|
+
`const PILL_NAME = ${JSON.stringify(pillName)};`,
|
|
1152
|
+
`const REQUIRED_TOOL = ${JSON.stringify(requiredTool)};`,
|
|
1153
|
+
``,
|
|
1154
|
+
`const dir = join(homedir(), \`.\${PILL_NAME}\`);`,
|
|
1155
|
+
`const flagFile = join(dir, \`last_\${REQUIRED_TOOL}_run.json\`);`,
|
|
1156
|
+
``,
|
|
1157
|
+
`try {`,
|
|
1158
|
+
` mkdirSync(dir, { recursive: true });`,
|
|
1159
|
+
` writeFileSync(flagFile, JSON.stringify({ timestamp: new Date().toISOString() }));`,
|
|
1160
|
+
`} catch (e) {`,
|
|
1161
|
+
` process.stderr.write(\`[\${PILL_NAME}] warning: could not write flag file: \${e.message}\\n\`);`,
|
|
1162
|
+
`}`,
|
|
1163
|
+
``
|
|
1164
|
+
].join("\n");
|
|
1165
|
+
}
|
|
1166
|
+
function applyBlockingHookScripts(baseDir, pillName, hook) {
|
|
1167
|
+
const hooksDir = join3(baseDir, ".mcpill", "server", "hooks");
|
|
1168
|
+
mkdirSync(hooksDir, { recursive: true });
|
|
1169
|
+
const hookName = hook.name;
|
|
1170
|
+
const requiredTool = hook.required_tool;
|
|
1171
|
+
const windowSeconds = hook.gate_window_seconds ?? 120;
|
|
1172
|
+
const instructions = hook.instructions ?? `[mcpill:${pillName}] Run mcp__${pillName}__${requiredTool} before calling other tools.`;
|
|
1173
|
+
writeFileSync(
|
|
1174
|
+
join3(hooksDir, `${hookName}-gate.mjs`),
|
|
1175
|
+
generateGateScript(pillName, requiredTool, windowSeconds, instructions)
|
|
1176
|
+
);
|
|
1177
|
+
writeFileSync(
|
|
1178
|
+
join3(hooksDir, `${hookName}-flag.mjs`),
|
|
1179
|
+
generateFlagScript(pillName, requiredTool)
|
|
1180
|
+
);
|
|
1181
|
+
return [
|
|
1182
|
+
{
|
|
1183
|
+
event: "PreToolUse",
|
|
1184
|
+
matcher: ".*",
|
|
1185
|
+
command: `node .mcpill/server/hooks/${hookName}-gate.mjs`
|
|
1186
|
+
},
|
|
1187
|
+
{
|
|
1188
|
+
event: "PostToolUse",
|
|
1189
|
+
matcher: `mcp__${pillName}__${requiredTool}`,
|
|
1190
|
+
command: `node .mcpill/server/hooks/${hookName}-flag.mjs`
|
|
1191
|
+
}
|
|
1192
|
+
];
|
|
1193
|
+
}
|
|
1042
1194
|
|
|
1043
1195
|
// src/commands/validate.ts
|
|
1044
1196
|
async function validateOne(mcpillDir) {
|
|
@@ -1106,6 +1258,32 @@ async function runValidate(baseDir) {
|
|
|
1106
1258
|
}
|
|
1107
1259
|
|
|
1108
1260
|
// src/commands/run.ts
|
|
1261
|
+
function patchJson(filePath, patcher) {
|
|
1262
|
+
if (!fs6.existsSync(filePath)) return;
|
|
1263
|
+
try {
|
|
1264
|
+
const obj = JSON.parse(fs6.readFileSync(filePath, "utf-8"));
|
|
1265
|
+
patcher(obj);
|
|
1266
|
+
fs6.writeFileSync(filePath, JSON.stringify(obj, null, 2) + "\n", "utf-8");
|
|
1267
|
+
} catch {
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
function desktopConfigPaths() {
|
|
1271
|
+
const home = os.homedir();
|
|
1272
|
+
if (process.platform === "win32") {
|
|
1273
|
+
const appData = process.env.APPDATA ?? path7.join(home, "AppData", "Roaming");
|
|
1274
|
+
return [path7.join(appData, "Claude", "claude_desktop_config.json")];
|
|
1275
|
+
}
|
|
1276
|
+
return [path7.join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json")];
|
|
1277
|
+
}
|
|
1278
|
+
function unregisterSync(name, baseDir) {
|
|
1279
|
+
const remove = (obj) => {
|
|
1280
|
+
const servers = obj.mcpServers;
|
|
1281
|
+
if (servers) delete servers[name];
|
|
1282
|
+
};
|
|
1283
|
+
patchJson(path7.join(baseDir, ".claude", "settings.json"), remove);
|
|
1284
|
+
patchJson(path7.join(os.homedir(), ".claude.json"), remove);
|
|
1285
|
+
for (const p of desktopConfigPaths()) patchJson(p, remove);
|
|
1286
|
+
}
|
|
1109
1287
|
async function runServer(opts) {
|
|
1110
1288
|
const baseDir = path7.resolve(opts.dir ?? process.cwd());
|
|
1111
1289
|
await runValidate(baseDir);
|
|
@@ -1157,7 +1335,7 @@ async function runServer(opts) {
|
|
|
1157
1335
|
resolver: async () => content
|
|
1158
1336
|
});
|
|
1159
1337
|
}
|
|
1160
|
-
if (
|
|
1338
|
+
if (transport === "stdio") {
|
|
1161
1339
|
applySetup(name, tools.map((t) => t.name), {
|
|
1162
1340
|
projectPath: baseDir,
|
|
1163
1341
|
permissions: "restrictive",
|
|
@@ -1167,6 +1345,16 @@ async function runServer(opts) {
|
|
|
1167
1345
|
args: ["run", "--dir", baseDir]
|
|
1168
1346
|
}
|
|
1169
1347
|
});
|
|
1348
|
+
const cleanup = () => unregisterSync(name, baseDir);
|
|
1349
|
+
process.on("exit", cleanup);
|
|
1350
|
+
process.on("SIGINT", () => {
|
|
1351
|
+
cleanup();
|
|
1352
|
+
process.exit(0);
|
|
1353
|
+
});
|
|
1354
|
+
process.on("SIGTERM", () => {
|
|
1355
|
+
cleanup();
|
|
1356
|
+
process.exit(0);
|
|
1357
|
+
});
|
|
1170
1358
|
}
|
|
1171
1359
|
try {
|
|
1172
1360
|
await server.start();
|
|
@@ -1512,9 +1700,26 @@ async function runCompile(opts) {
|
|
|
1512
1700
|
}
|
|
1513
1701
|
}
|
|
1514
1702
|
const allUserHooks = [...pillHooks, ...fileHooks];
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1703
|
+
const blockingHooks = allUserHooks.filter((h) => h.blocking);
|
|
1704
|
+
const regularHooks = allUserHooks.filter((h) => !h.blocking);
|
|
1705
|
+
for (const hook of blockingHooks) {
|
|
1706
|
+
if (!hook.name) throw new Error(`blocking hook is missing a name`);
|
|
1707
|
+
if (!hook.required_tool) throw new Error(`blocking hook '${hook.name}' is missing required_tool`);
|
|
1708
|
+
const toolExists = mergedDoc.tools.some((t) => t.name === hook.required_tool);
|
|
1709
|
+
if (!toolExists) {
|
|
1710
|
+
throw new Error(`blocking hook '${hook.name}' references unknown tool '${hook.required_tool}'`);
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
if (regularHooks.length > 0) {
|
|
1714
|
+
mergeUserHooksIntoSettings(baseDir, regularHooks);
|
|
1715
|
+
console.log(`\u2713 .claude/settings.json updated with ${regularHooks.length} user-defined hook(s)`);
|
|
1716
|
+
}
|
|
1717
|
+
if (blockingHooks.length > 0) {
|
|
1718
|
+
const generatedEntries = blockingHooks.flatMap(
|
|
1719
|
+
(hook) => applyBlockingHookScripts(baseDir, mergedDoc.config.name, hook)
|
|
1720
|
+
);
|
|
1721
|
+
mergeUserHooksIntoSettings(baseDir, generatedEntries);
|
|
1722
|
+
console.log(`\u2713 .claude/settings.json updated with ${blockingHooks.length} blocking hook(s)`);
|
|
1518
1723
|
}
|
|
1519
1724
|
}
|
|
1520
1725
|
|
|
@@ -1561,72 +1766,6 @@ async function runPublish(dir, access) {
|
|
|
1561
1766
|
}
|
|
1562
1767
|
}
|
|
1563
1768
|
|
|
1564
|
-
// src/commands/unregister.ts
|
|
1565
|
-
import path10 from "path";
|
|
1566
|
-
import fs8 from "fs";
|
|
1567
|
-
import os from "os";
|
|
1568
|
-
function patchJson(filePath, patcher) {
|
|
1569
|
-
if (!fs8.existsSync(filePath)) return false;
|
|
1570
|
-
try {
|
|
1571
|
-
const obj = JSON.parse(fs8.readFileSync(filePath, "utf-8"));
|
|
1572
|
-
patcher(obj);
|
|
1573
|
-
fs8.writeFileSync(filePath, JSON.stringify(obj, null, 2) + "\n", "utf-8");
|
|
1574
|
-
return true;
|
|
1575
|
-
} catch {
|
|
1576
|
-
return false;
|
|
1577
|
-
}
|
|
1578
|
-
}
|
|
1579
|
-
function desktopConfigPaths() {
|
|
1580
|
-
const home = os.homedir();
|
|
1581
|
-
if (process.platform === "win32") {
|
|
1582
|
-
const appData = process.env.APPDATA ?? path10.join(home, "AppData", "Roaming");
|
|
1583
|
-
return [path10.join(appData, "Claude", "claude_desktop_config.json")];
|
|
1584
|
-
}
|
|
1585
|
-
return [path10.join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json")];
|
|
1586
|
-
}
|
|
1587
|
-
async function runUnregister(opts) {
|
|
1588
|
-
const baseDir = path10.resolve(opts.dir ?? process.cwd());
|
|
1589
|
-
const mcpillDir = path10.join(baseDir, ".mcpill", "server");
|
|
1590
|
-
const config = await loadConfig(mcpillDir);
|
|
1591
|
-
const { name } = config;
|
|
1592
|
-
let removed = 0;
|
|
1593
|
-
const settingsPath = path10.join(baseDir, ".claude", "settings.json");
|
|
1594
|
-
const removedProject = patchJson(settingsPath, (obj) => {
|
|
1595
|
-
const servers = obj.mcpServers;
|
|
1596
|
-
if (servers && name in servers) {
|
|
1597
|
-
delete servers[name];
|
|
1598
|
-
removed++;
|
|
1599
|
-
}
|
|
1600
|
-
});
|
|
1601
|
-
if (removedProject) process.stderr.write(`removed "${name}" from .claude/settings.json
|
|
1602
|
-
`);
|
|
1603
|
-
const claudeJsonPath = path10.join(os.homedir(), ".claude.json");
|
|
1604
|
-
const removedUser = patchJson(claudeJsonPath, (obj) => {
|
|
1605
|
-
const servers = obj.mcpServers;
|
|
1606
|
-
if (servers && name in servers) {
|
|
1607
|
-
delete servers[name];
|
|
1608
|
-
removed++;
|
|
1609
|
-
}
|
|
1610
|
-
});
|
|
1611
|
-
if (removedUser) process.stderr.write(`removed "${name}" from ~/.claude.json
|
|
1612
|
-
`);
|
|
1613
|
-
for (const configPath of desktopConfigPaths()) {
|
|
1614
|
-
const removedDesktop = patchJson(configPath, (obj) => {
|
|
1615
|
-
const servers = obj.mcpServers;
|
|
1616
|
-
if (servers && name in servers) {
|
|
1617
|
-
delete servers[name];
|
|
1618
|
-
removed++;
|
|
1619
|
-
}
|
|
1620
|
-
});
|
|
1621
|
-
if (removedDesktop) process.stderr.write(`removed "${name}" from Claude Desktop config
|
|
1622
|
-
`);
|
|
1623
|
-
}
|
|
1624
|
-
if (removed === 0) {
|
|
1625
|
-
process.stderr.write(`"${name}" was not registered \u2014 nothing to remove
|
|
1626
|
-
`);
|
|
1627
|
-
}
|
|
1628
|
-
}
|
|
1629
|
-
|
|
1630
1769
|
// src/cli.ts
|
|
1631
1770
|
var pkgDir = dirname(fileURLToPath(import.meta.url));
|
|
1632
1771
|
var pkg = JSON.parse(
|
|
@@ -1637,7 +1776,7 @@ program.name("mcpill").version(pkg.version);
|
|
|
1637
1776
|
program.command("init").description("Scaffold a new .mcpill/ directory").option("--dir <path>", "Target directory").action(async (opts) => {
|
|
1638
1777
|
await runInit(opts);
|
|
1639
1778
|
});
|
|
1640
|
-
program.command("run").description("Start the MCP server").option("--transport <type>", "Transport: stdio (default) or http").option("--port <n>", "Port number (HTTP only)", parseInt).option("--dir <path>", "Project root containing .mcpill/").
|
|
1779
|
+
program.command("run").description("Start the MCP server").option("--transport <type>", "Transport: stdio (default) or http").option("--port <n>", "Port number (HTTP only)", parseInt).option("--dir <path>", "Project root containing .mcpill/").action(
|
|
1641
1780
|
async (opts) => {
|
|
1642
1781
|
await runServer(opts);
|
|
1643
1782
|
}
|
|
@@ -1649,9 +1788,6 @@ program.command("validate").description("Validate .mcpill/ configuration").optio
|
|
|
1649
1788
|
program.command("compile").description("Compile server.md \u2194 .mcpill/ files").option("--dir <path>", "Directory containing server.md and .mcpill/").option("--to-md", "Reverse: generate server.md from .mcpill/ files").option("--strict", "Error on missing tool handlers instead of generating stubs").option("--no-hooks", "Skip writing the PreToolUse hook to .claude/settings.json").action(async (opts) => {
|
|
1650
1789
|
await runCompile({ ...opts, noHooks: opts.hooks === false });
|
|
1651
1790
|
});
|
|
1652
|
-
program.command("unregister").description("Remove server entry from Claude config (undo mcpill run --register)").option("--dir <path>", "Project root containing .mcpill/").action(async (opts) => {
|
|
1653
|
-
await runUnregister(opts);
|
|
1654
|
-
});
|
|
1655
1791
|
program.command("pack").description("Prepare pill for npm distribution").option("--dir <path>", "pill project root", ".").action(({ dir }) => runPack(dir));
|
|
1656
1792
|
program.command("publish").description("Pack and publish pill to npm").option("--dir <path>", "pill project root", ".").option("--access <level>", "npm access level", "public").action(({ dir, access }) => runPublish(dir, access));
|
|
1657
1793
|
program.parseAsync(process.argv).catch((err) => {
|