mcpill 1.3.0 → 1.6.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 +15 -0
- package/README.md +3 -1
- package/dist/cli.js +236 -48
- package/package.json +6 -1
- package/MANUAL-TEST-GUIDE-SPEC.md +0 -270
- package/src/__tests__/compile.test.ts +0 -203
- package/src/__tests__/init.test.ts +0 -75
- package/src/__tests__/loaders/config.test.ts +0 -54
- package/src/__tests__/loaders/prompts.test.ts +0 -116
- package/src/__tests__/loaders/resources.test.ts +0 -86
- package/src/__tests__/loaders/tools.test.ts +0 -128
- package/src/__tests__/pack.test.ts +0 -98
- package/src/__tests__/validate.test.ts +0 -152
- package/src/cli.ts +0 -77
- package/src/commands/compile.ts +0 -196
- package/src/commands/init.ts +0 -453
- package/src/commands/pack.ts +0 -38
- package/src/commands/publish.ts +0 -17
- package/src/commands/run.ts +0 -105
- package/src/commands/validate.ts +0 -87
- package/src/compiler/hooks.ts +0 -140
- package/src/compiler/merge-tools.ts +0 -99
- package/src/compiler/parse.ts +0 -236
- package/src/compiler/serialize.ts +0 -100
- package/src/compiler/types.ts +0 -27
- package/src/loaders/config.ts +0 -25
- package/src/loaders/prompts.ts +0 -60
- package/src/loaders/resources.ts +0 -54
- package/src/loaders/tools.ts +0 -68
- package/src/templates/prompts/greeting.md +0 -11
- package/src/templates/server.md +0 -9
- package/src/templates/tools/echo.md +0 -15
- package/tsconfig.json +0 -10
- package/tsup.config.ts +0 -13
- package/vitest.config.ts +0 -12
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.6.0
|
|
4
|
+
|
|
5
|
+
- `HELLO-MCP.md` template (scaffolded by `mcpill init`) now includes a commented-out `## Hook:` section stub with inline field explanations for all four Claude Code event types
|
|
6
|
+
|
|
7
|
+
## 1.5.0
|
|
8
|
+
|
|
9
|
+
- `mcpill init` scaffolds `.mcpill/server/hooks/` with a commented `example-hook.md` stub covering all four Claude Code event types (`PreToolUse`, `PostToolUse`, `PreCompact`, `Stop`)
|
|
10
|
+
- `PILL.md` template now includes commented-out `## Hook:` and `## Prompt:` section stubs with inline field explanations
|
|
11
|
+
- `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
|
|
12
|
+
- `pill-agent-guide.md` documents `## Hook:` sections, `.mcpill/server/hooks/*.md` files, and the `--no-hooks` flag
|
|
13
|
+
|
|
14
|
+
## 1.4.0
|
|
15
|
+
|
|
16
|
+
- `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
|
|
17
|
+
|
|
3
18
|
## 1.3.0
|
|
4
19
|
|
|
5
20
|
- `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,11 @@ 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
|
-
- `
|
|
52
|
+
- `README.md` — quickstart guide covering project layout and tool-editing workflow
|
|
53
|
+
- `package.json` — `{ type: "module", dependencies: { mcpill-runtime } }` — deps are installed automatically by `mcpill compile` and `mcpill validate`
|
|
52
54
|
|
|
53
55
|
### `mcpill compile`
|
|
54
56
|
|
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
|
|
7
|
-
import { readFileSync as
|
|
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
|
|
88
|
-
-
|
|
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:
|
|
@@ -227,6 +273,24 @@ description: Ask the server to introduce itself.
|
|
|
227
273
|
behavior: |
|
|
228
274
|
A prompt (no args) with a single user message:
|
|
229
275
|
"Introduce yourself \u2014 what is hello-mcp and what can it do?"
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
<!-- Add a ## Hook: section to run a shell command when Claude Code fires an event.
|
|
280
|
+
Run: mcpill compile after editing to apply changes to .claude/settings.json.
|
|
281
|
+
Remove comment markers to activate.
|
|
282
|
+
|
|
283
|
+
trigger \u2014 event that fires the hook: PreToolUse | PostToolUse | PreCompact | Stop
|
|
284
|
+
matcher \u2014 regex matched against tool name (PreToolUse/PostToolUse only); blank = all tools
|
|
285
|
+
command \u2014 shell command; stdout shown to Claude; exit!=0 blocks (PreToolUse)
|
|
286
|
+
|
|
287
|
+
## Hook: example-hook
|
|
288
|
+
|
|
289
|
+
trigger: PreToolUse
|
|
290
|
+
matcher: .*
|
|
291
|
+
command: echo "example hook \u2014 replace this command"
|
|
292
|
+
|
|
293
|
+
-->
|
|
230
294
|
`;
|
|
231
295
|
var SERVER_MD_TEMPLATE = `## Config
|
|
232
296
|
name: my-server
|
|
@@ -238,6 +302,37 @@ name: Status
|
|
|
238
302
|
---
|
|
239
303
|
The server is running.
|
|
240
304
|
`;
|
|
305
|
+
var EXAMPLE_HOOK_MD = `# example-hook
|
|
306
|
+
|
|
307
|
+
<!--
|
|
308
|
+
Stub hook file \u2014 fill in the fields below and run: mcpill compile
|
|
309
|
+
|
|
310
|
+
TRIGGER \u2014 which event fires this hook (required):
|
|
311
|
+
PreToolUse \u2014 runs before Claude calls a tool
|
|
312
|
+
PostToolUse \u2014 runs after a tool returns a result
|
|
313
|
+
PreCompact \u2014 runs before Claude compacts/summarizes its context
|
|
314
|
+
Stop \u2014 runs when Claude finishes responding
|
|
315
|
+
|
|
316
|
+
MATCHER \u2014 regex matched against the tool name (optional):
|
|
317
|
+
Used for PreToolUse and PostToolUse only; ignored for PreCompact and Stop.
|
|
318
|
+
Omit or leave blank to match all tools.
|
|
319
|
+
Examples: Read|Write|Edit (specific tools)
|
|
320
|
+
Bash (Bash tool only)
|
|
321
|
+
.* (all tools \u2014 same as omitting)
|
|
322
|
+
|
|
323
|
+
COMMAND \u2014 shell command to run when the hook fires (required):
|
|
324
|
+
stdout is shown to Claude as context before/after the tool call.
|
|
325
|
+
Exit code != 0 blocks the tool call (PreToolUse only).
|
|
326
|
+
Examples: echo "use pill tools instead of native file tools"
|
|
327
|
+
./scripts/pre-check.sh
|
|
328
|
+
|
|
329
|
+
Copy the fields below the closing --> and fill them in to activate this hook:
|
|
330
|
+
|
|
331
|
+
trigger: PreToolUse
|
|
332
|
+
matcher: .*
|
|
333
|
+
command: echo "example hook \u2014 replace this command"
|
|
334
|
+
-->
|
|
335
|
+
`;
|
|
241
336
|
var GREETING_PROMPT_MD = `# greeting
|
|
242
337
|
|
|
243
338
|
Generate a greeting
|
|
@@ -365,8 +460,10 @@ ${projectName}/
|
|
|
365
460
|
\u251C\u2500\u2500 mcpill.config.json \u2190 compiled config
|
|
366
461
|
\u251C\u2500\u2500 tools/ \u2190 one .md file per tool
|
|
367
462
|
\u2502 \u2514\u2500\u2500 my-tool.md
|
|
368
|
-
\
|
|
369
|
-
|
|
463
|
+
\u251C\u2500\u2500 prompts/ \u2190 one .md file per prompt (optional)
|
|
464
|
+
\u2502 \u2514\u2500\u2500 greeting.md
|
|
465
|
+
\u2514\u2500\u2500 hooks/ \u2190 one .md file per hook (optional)
|
|
466
|
+
\u2514\u2500\u2500 example-hook.md
|
|
370
467
|
\`\`\`
|
|
371
468
|
|
|
372
469
|
## Editing tools manually
|
|
@@ -405,10 +502,12 @@ async function runInit(opts) {
|
|
|
405
502
|
}
|
|
406
503
|
fs.mkdirSync(path.join(serverDir, "tools"), { recursive: true });
|
|
407
504
|
fs.mkdirSync(path.join(serverDir, "prompts"), { recursive: true });
|
|
505
|
+
fs.mkdirSync(path.join(serverDir, "hooks"), { recursive: true });
|
|
408
506
|
fs.mkdirSync(path.join(targetDir, ".claude", "commands"), { recursive: true });
|
|
409
507
|
const serverMd = SERVER_MD_TEMPLATE.replace("name: my-server", `name: ${projectName}`);
|
|
410
508
|
fs.writeFileSync(path.join(mcpillDir, "server.md"), serverMd);
|
|
411
509
|
fs.writeFileSync(path.join(serverDir, "prompts", "greeting.md"), GREETING_PROMPT_MD);
|
|
510
|
+
fs.writeFileSync(path.join(serverDir, "hooks", "example-hook.md"), EXAMPLE_HOOK_MD);
|
|
412
511
|
fs.writeFileSync(
|
|
413
512
|
path.join(serverDir, "mcpill.config.json"),
|
|
414
513
|
JSON.stringify({ name: projectName, transport: "stdio", port: 3333 }, null, 2)
|
|
@@ -438,6 +537,7 @@ async function runInit(opts) {
|
|
|
438
537
|
console.log("\u2713 .mcpill/HELLO-MCP.md \u2190 copy into PILL.md to see a working example instantly");
|
|
439
538
|
console.log("\u2713 .mcpill/server.md \u2190 or edit source files directly");
|
|
440
539
|
console.log("\u2713 .mcpill/server/prompts/greeting.md");
|
|
540
|
+
console.log("\u2713 .mcpill/server/hooks/example-hook.md \u2190 stub hook (see file for format)");
|
|
441
541
|
console.log("\u2713 .mcpill/pill-agent-guide.md \u2190 agent instructions (read by Claude)");
|
|
442
542
|
console.log("\u2713 .mcpill/pill-user-guide.md \u2190 start here");
|
|
443
543
|
console.log("\u2713 .claude/commands/create-pill.md \u2190 /create-pill skill (say: /create-pill)");
|
|
@@ -610,13 +710,27 @@ async function loadResources(mcpillDir) {
|
|
|
610
710
|
import fs5 from "fs";
|
|
611
711
|
import path6 from "path";
|
|
612
712
|
|
|
713
|
+
// src/commands/ensure-deps.ts
|
|
714
|
+
import { execSync } from "child_process";
|
|
715
|
+
import { existsSync, readFileSync } from "fs";
|
|
716
|
+
import { join } from "path";
|
|
717
|
+
function ensureDeps(serverDir) {
|
|
718
|
+
if (!existsSync(join(serverDir, "mcpill.config.json"))) return;
|
|
719
|
+
if (!existsSync(join(serverDir, "package.json"))) return;
|
|
720
|
+
if (existsSync(join(serverDir, "node_modules"))) return;
|
|
721
|
+
const pkg2 = JSON.parse(readFileSync(join(serverDir, "package.json"), "utf-8"));
|
|
722
|
+
if (!pkg2.dependencies || Object.keys(pkg2.dependencies).length === 0) return;
|
|
723
|
+
console.log("Installing server dependencies\u2026");
|
|
724
|
+
execSync("npm install", { cwd: serverDir, stdio: "inherit" });
|
|
725
|
+
}
|
|
726
|
+
|
|
613
727
|
// src/compiler/hooks.ts
|
|
614
|
-
import { readFileSync as
|
|
615
|
-
import { join as
|
|
728
|
+
import { readFileSync as readFileSync3, existsSync as existsSync2, writeFileSync, mkdirSync } from "fs";
|
|
729
|
+
import { join as join3 } from "path";
|
|
616
730
|
|
|
617
731
|
// src/compiler/parse.ts
|
|
618
|
-
import { readdirSync, readFileSync } from "fs";
|
|
619
|
-
import { join } from "path";
|
|
732
|
+
import { readdirSync, readFileSync as readFileSync2 } from "fs";
|
|
733
|
+
import { join as join2 } from "path";
|
|
620
734
|
function extractSections(content) {
|
|
621
735
|
const sections = /* @__PURE__ */ new Map();
|
|
622
736
|
const pieces = ("\n" + content).split("\n## ");
|
|
@@ -747,7 +861,7 @@ function parsePromptFile(content) {
|
|
|
747
861
|
return { name, description, args, messages };
|
|
748
862
|
}
|
|
749
863
|
function parseServerDir(dir) {
|
|
750
|
-
const serverMdContent =
|
|
864
|
+
const serverMdContent = readFileSync2(join2(dir, "server.md"), "utf-8");
|
|
751
865
|
const sections = extractSections(serverMdContent);
|
|
752
866
|
const configBody = sections.get("Config") ?? "";
|
|
753
867
|
const configKv = parseKvBlock(configBody);
|
|
@@ -758,7 +872,7 @@ function parseServerDir(dir) {
|
|
|
758
872
|
};
|
|
759
873
|
const resourcesBody = sections.get("Resources") ?? "";
|
|
760
874
|
const resources = parseResourcesSection(resourcesBody);
|
|
761
|
-
const toolsDir =
|
|
875
|
+
const toolsDir = join2(dir, "server", "tools");
|
|
762
876
|
const tools = [];
|
|
763
877
|
let toolFiles = [];
|
|
764
878
|
try {
|
|
@@ -766,9 +880,9 @@ function parseServerDir(dir) {
|
|
|
766
880
|
} catch {
|
|
767
881
|
}
|
|
768
882
|
for (const file of toolFiles) {
|
|
769
|
-
tools.push(parseToolFile(
|
|
883
|
+
tools.push(parseToolFile(readFileSync2(join2(toolsDir, file), "utf-8")));
|
|
770
884
|
}
|
|
771
|
-
const promptsDir =
|
|
885
|
+
const promptsDir = join2(dir, "server", "prompts");
|
|
772
886
|
const prompts = [];
|
|
773
887
|
let promptFiles = [];
|
|
774
888
|
try {
|
|
@@ -776,7 +890,7 @@ function parseServerDir(dir) {
|
|
|
776
890
|
} catch {
|
|
777
891
|
}
|
|
778
892
|
for (const file of promptFiles) {
|
|
779
|
-
prompts.push(parsePromptFile(
|
|
893
|
+
prompts.push(parsePromptFile(readFileSync2(join2(promptsDir, file), "utf-8")));
|
|
780
894
|
}
|
|
781
895
|
return { config, tools, prompts, resources };
|
|
782
896
|
}
|
|
@@ -854,12 +968,65 @@ function buildHookEntry(pillName, tools) {
|
|
|
854
968
|
hooks: [{ type: "command", command: `echo ${JSON.stringify(message)}` }]
|
|
855
969
|
};
|
|
856
970
|
}
|
|
971
|
+
function parsePillMdHooks(content) {
|
|
972
|
+
const stripped = content.replace(/<!--[\s\S]*?-->/g, "");
|
|
973
|
+
const hooks = [];
|
|
974
|
+
const pieces = ("\n" + stripped).split("\n## ");
|
|
975
|
+
for (const piece of pieces) {
|
|
976
|
+
if (!piece.toLowerCase().startsWith("hook:")) continue;
|
|
977
|
+
const nlIdx = piece.indexOf("\n");
|
|
978
|
+
const body = nlIdx === -1 ? "" : piece.slice(nlIdx + 1);
|
|
979
|
+
const kv = parseKvBlock(body);
|
|
980
|
+
const trigger = kv["trigger"]?.trim();
|
|
981
|
+
const matcher = kv["matcher"]?.trim();
|
|
982
|
+
const command = kv["command"]?.trim();
|
|
983
|
+
if (!trigger || !command) continue;
|
|
984
|
+
hooks.push({ event: trigger, ...matcher ? { matcher } : {}, command });
|
|
985
|
+
}
|
|
986
|
+
return hooks;
|
|
987
|
+
}
|
|
988
|
+
function parseHookFile(content) {
|
|
989
|
+
const stripped = content.replace(/<!--[\s\S]*?-->/g, "");
|
|
990
|
+
const kv = parseKvBlock(stripped);
|
|
991
|
+
const trigger = kv["trigger"]?.trim();
|
|
992
|
+
const matcher = kv["matcher"]?.trim();
|
|
993
|
+
const command = kv["command"]?.trim();
|
|
994
|
+
if (!trigger || !command) return null;
|
|
995
|
+
return { event: trigger, ...matcher ? { matcher } : {}, command };
|
|
996
|
+
}
|
|
997
|
+
function mergeUserHooksIntoSettings(baseDir, hookBlocks) {
|
|
998
|
+
if (hookBlocks.length === 0) return;
|
|
999
|
+
const claudeDir = join3(baseDir, ".claude");
|
|
1000
|
+
const settingsPath = join3(claudeDir, "settings.json");
|
|
1001
|
+
let settings = {};
|
|
1002
|
+
if (existsSync2(settingsPath)) {
|
|
1003
|
+
settings = JSON.parse(readFileSync3(settingsPath, "utf-8"));
|
|
1004
|
+
}
|
|
1005
|
+
if (!settings.hooks) settings.hooks = {};
|
|
1006
|
+
for (const block of hookBlocks) {
|
|
1007
|
+
const existing = settings.hooks[block.event] ?? [];
|
|
1008
|
+
const newEntry = {
|
|
1009
|
+
...block.matcher !== void 0 ? { matcher: block.matcher } : {},
|
|
1010
|
+
hooks: [{ type: "command", command: block.command }]
|
|
1011
|
+
};
|
|
1012
|
+
const matcherKey = block.matcher ?? "";
|
|
1013
|
+
const idx = existing.findIndex((e) => (e.matcher ?? "") === matcherKey);
|
|
1014
|
+
if (idx !== -1) {
|
|
1015
|
+
existing[idx] = newEntry;
|
|
1016
|
+
} else {
|
|
1017
|
+
existing.push(newEntry);
|
|
1018
|
+
}
|
|
1019
|
+
settings.hooks[block.event] = existing;
|
|
1020
|
+
}
|
|
1021
|
+
mkdirSync(claudeDir, { recursive: true });
|
|
1022
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
1023
|
+
}
|
|
857
1024
|
function mergeHookIntoSettings(baseDir, hookEntry) {
|
|
858
|
-
const claudeDir =
|
|
859
|
-
const settingsPath =
|
|
1025
|
+
const claudeDir = join3(baseDir, ".claude");
|
|
1026
|
+
const settingsPath = join3(claudeDir, "settings.json");
|
|
860
1027
|
let settings = {};
|
|
861
|
-
if (
|
|
862
|
-
settings = JSON.parse(
|
|
1028
|
+
if (existsSync2(settingsPath)) {
|
|
1029
|
+
settings = JSON.parse(readFileSync3(settingsPath, "utf-8"));
|
|
863
1030
|
}
|
|
864
1031
|
if (!settings.hooks) settings.hooks = {};
|
|
865
1032
|
if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
|
|
@@ -917,6 +1084,7 @@ async function runValidate(baseDir) {
|
|
|
917
1084
|
console.error("No pill directories found \u2014 run mcpill compile first");
|
|
918
1085
|
process.exit(1);
|
|
919
1086
|
}
|
|
1087
|
+
ensureDeps(serverDir);
|
|
920
1088
|
await validateOne(serverDir);
|
|
921
1089
|
if (!fs5.existsSync(path6.join(mcpillDir, ".no-hooks"))) {
|
|
922
1090
|
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 +1185,13 @@ async function runServer(opts) {
|
|
|
1017
1185
|
}
|
|
1018
1186
|
|
|
1019
1187
|
// src/commands/compile.ts
|
|
1020
|
-
import { resolve, join as
|
|
1021
|
-
import { readFileSync as
|
|
1188
|
+
import { resolve, join as join5 } from "path";
|
|
1189
|
+
import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, existsSync as existsSync3, readdirSync as readdirSync2 } from "fs";
|
|
1022
1190
|
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
1023
1191
|
|
|
1024
1192
|
// src/compiler/serialize.ts
|
|
1025
1193
|
import { writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
|
|
1026
|
-
import { join as
|
|
1194
|
+
import { join as join4 } from "path";
|
|
1027
1195
|
function serializeResources(resources) {
|
|
1028
1196
|
return resources.map((r) => {
|
|
1029
1197
|
let fm = `uri: ${r.uri}`;
|
|
@@ -1037,8 +1205,8 @@ ${r.content}`;
|
|
|
1037
1205
|
}).join("\n---\n");
|
|
1038
1206
|
}
|
|
1039
1207
|
function serializeServerDir(doc, dir) {
|
|
1040
|
-
mkdirSync2(
|
|
1041
|
-
mkdirSync2(
|
|
1208
|
+
mkdirSync2(join4(dir, "server", "tools"), { recursive: true });
|
|
1209
|
+
mkdirSync2(join4(dir, "server", "prompts"), { recursive: true });
|
|
1042
1210
|
let serverMd = "## Config\n";
|
|
1043
1211
|
serverMd += `name: ${doc.config.name}
|
|
1044
1212
|
`;
|
|
@@ -1054,7 +1222,7 @@ ${serializeResources(doc.resources)}
|
|
|
1054
1222
|
} else {
|
|
1055
1223
|
serverMd += "\n## Resources\n";
|
|
1056
1224
|
}
|
|
1057
|
-
writeFileSync2(
|
|
1225
|
+
writeFileSync2(join4(dir, "server.md"), serverMd);
|
|
1058
1226
|
console.log("\u2713 server.md updated");
|
|
1059
1227
|
for (const tool of doc.tools) {
|
|
1060
1228
|
let md = `# ${tool.name}
|
|
@@ -1078,7 +1246,7 @@ ${cleanHandler}
|
|
|
1078
1246
|
\`\`\`
|
|
1079
1247
|
`;
|
|
1080
1248
|
}
|
|
1081
|
-
writeFileSync2(
|
|
1249
|
+
writeFileSync2(join4(dir, "server", "tools", `${tool.name}.md`), md);
|
|
1082
1250
|
}
|
|
1083
1251
|
for (const prompt of doc.prompts) {
|
|
1084
1252
|
let md = `# ${prompt.name}
|
|
@@ -1102,7 +1270,7 @@ ${cleanHandler}
|
|
|
1102
1270
|
`;
|
|
1103
1271
|
}
|
|
1104
1272
|
}
|
|
1105
|
-
writeFileSync2(
|
|
1273
|
+
writeFileSync2(join4(dir, "server", "prompts", `${prompt.name}.md`), md);
|
|
1106
1274
|
}
|
|
1107
1275
|
console.log(`\u2713 tools/ updated (${doc.tools.length} files)`);
|
|
1108
1276
|
console.log(`\u2713 prompts/ updated (${doc.prompts.length} files)`);
|
|
@@ -1239,21 +1407,21 @@ ${r.content}`;
|
|
|
1239
1407
|
}
|
|
1240
1408
|
async function runCompile(opts) {
|
|
1241
1409
|
const baseDir = resolve(opts.dir ?? process.cwd());
|
|
1242
|
-
const mcpillDir =
|
|
1243
|
-
const serverDir =
|
|
1410
|
+
const mcpillDir = join5(baseDir, ".mcpill");
|
|
1411
|
+
const serverDir = join5(mcpillDir, "server");
|
|
1244
1412
|
if (opts.toMd) {
|
|
1245
|
-
const configPath =
|
|
1246
|
-
if (!
|
|
1413
|
+
const configPath = join5(serverDir, "mcpill.config.json");
|
|
1414
|
+
if (!existsSync3(configPath)) {
|
|
1247
1415
|
throw new Error("No pill found \u2014 run mcpill compile first");
|
|
1248
1416
|
}
|
|
1249
1417
|
const config = await loadConfig(serverDir);
|
|
1250
1418
|
const resources = await loadResources(serverDir);
|
|
1251
|
-
const promptsPath =
|
|
1252
|
-
const prompts = JSON.parse(
|
|
1253
|
-
const toolsPath =
|
|
1254
|
-
const handlers =
|
|
1419
|
+
const promptsPath = join5(serverDir, "prompts.json");
|
|
1420
|
+
const prompts = JSON.parse(readFileSync4(promptsPath, "utf-8"));
|
|
1421
|
+
const toolsPath = join5(serverDir, "tools.js");
|
|
1422
|
+
const handlers = existsSync3(toolsPath) ? extractHandlers(readFileSync4(toolsPath, "utf-8")) : /* @__PURE__ */ new Map();
|
|
1255
1423
|
let tools = [];
|
|
1256
|
-
if (
|
|
1424
|
+
if (existsSync3(toolsPath)) {
|
|
1257
1425
|
const mod = await import(pathToFileURL2(toolsPath).href);
|
|
1258
1426
|
tools = mod.default.map((t) => {
|
|
1259
1427
|
const schema = {};
|
|
@@ -1284,13 +1452,13 @@ async function runCompile(opts) {
|
|
|
1284
1452
|
return;
|
|
1285
1453
|
}
|
|
1286
1454
|
const doc = parseServerDir(mcpillDir);
|
|
1287
|
-
const toolsJsPath =
|
|
1288
|
-
const existing =
|
|
1455
|
+
const toolsJsPath = join5(serverDir, "tools.js");
|
|
1456
|
+
const existing = existsSync3(toolsJsPath) ? extractHandlers(readFileSync4(toolsJsPath, "utf-8")) : /* @__PURE__ */ new Map();
|
|
1289
1457
|
const { doc: mergedDoc, stubbed } = mergeHandlers(doc, existing);
|
|
1290
|
-
const pillMdPath =
|
|
1458
|
+
const pillMdPath = join5(baseDir, "PILL.md");
|
|
1291
1459
|
let pillContent = null;
|
|
1292
|
-
if (
|
|
1293
|
-
pillContent =
|
|
1460
|
+
if (existsSync3(pillMdPath)) {
|
|
1461
|
+
pillContent = readFileSync4(pillMdPath, "utf-8");
|
|
1294
1462
|
const serverSection = ("\n" + pillContent).split("\n## ").find((s) => s.startsWith("Server\n"));
|
|
1295
1463
|
if (serverSection) {
|
|
1296
1464
|
const kv = parseKvBlock(serverSection.slice("Server\n".length));
|
|
@@ -1307,18 +1475,19 @@ async function runCompile(opts) {
|
|
|
1307
1475
|
console.warn(`\u26A0 Stub generated for tool: ${name}`);
|
|
1308
1476
|
}
|
|
1309
1477
|
mkdirSync3(serverDir, { recursive: true });
|
|
1310
|
-
writeFileSync3(
|
|
1311
|
-
writeFileSync3(
|
|
1312
|
-
writeFileSync3(
|
|
1478
|
+
writeFileSync3(join5(serverDir, "mcpill.config.json"), JSON.stringify(mergedDoc.config, null, 2));
|
|
1479
|
+
writeFileSync3(join5(serverDir, "prompts.json"), JSON.stringify(mergedDoc.prompts, null, 2));
|
|
1480
|
+
writeFileSync3(join5(serverDir, "resources.md"), serializeResourcesMd(mergedDoc.resources));
|
|
1313
1481
|
writeFileSync3(toolsJsPath, generateToolsJs(mergedDoc.tools));
|
|
1482
|
+
ensureDeps(serverDir);
|
|
1314
1483
|
console.log(`\u2713 .mcpill/server/ updated from .mcpill/server.md, .mcpill/server/tools/, .mcpill/server/prompts/`);
|
|
1315
1484
|
if (opts.noHooks) {
|
|
1316
|
-
writeFileSync3(
|
|
1485
|
+
writeFileSync3(join5(mcpillDir, ".no-hooks"), "");
|
|
1317
1486
|
return;
|
|
1318
1487
|
}
|
|
1319
|
-
const agentMdPath =
|
|
1488
|
+
const agentMdPath = existsSync3(join5(baseDir, "AGENT.md")) ? join5(baseDir, "AGENT.md") : existsSync3(join5(mcpillDir, "AGENT.md")) ? join5(mcpillDir, "AGENT.md") : null;
|
|
1320
1489
|
const pillTools = pillContent ? parsePillMdTools(pillContent) : [];
|
|
1321
|
-
const agentTools = agentMdPath ? parseAgentMdTools(
|
|
1490
|
+
const agentTools = agentMdPath ? parseAgentMdTools(readFileSync4(agentMdPath, "utf-8")) : [];
|
|
1322
1491
|
const toolMap = new Map([...pillTools, ...agentTools].map((t) => [t.name, t]));
|
|
1323
1492
|
const allTools = Array.from(toolMap.values());
|
|
1324
1493
|
const hookEntry = buildHookEntry(mergedDoc.config.name, allTools);
|
|
@@ -1326,6 +1495,25 @@ async function runCompile(opts) {
|
|
|
1326
1495
|
mergeHookIntoSettings(baseDir, hookEntry);
|
|
1327
1496
|
console.log(`\u2713 .claude/settings.json updated with PreToolUse hook`);
|
|
1328
1497
|
}
|
|
1498
|
+
const pillHooks = pillContent ? parsePillMdHooks(pillContent) : [];
|
|
1499
|
+
const hooksDir = join5(mcpillDir, "server", "hooks");
|
|
1500
|
+
const fileHooks = [];
|
|
1501
|
+
if (existsSync3(hooksDir)) {
|
|
1502
|
+
let hookFiles = [];
|
|
1503
|
+
try {
|
|
1504
|
+
hookFiles = readdirSync2(hooksDir).filter((f) => f.endsWith(".md"));
|
|
1505
|
+
} catch {
|
|
1506
|
+
}
|
|
1507
|
+
for (const file of hookFiles) {
|
|
1508
|
+
const entry = parseHookFile(readFileSync4(join5(hooksDir, file), "utf-8"));
|
|
1509
|
+
if (entry) fileHooks.push(entry);
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
const allUserHooks = [...pillHooks, ...fileHooks];
|
|
1513
|
+
if (allUserHooks.length > 0) {
|
|
1514
|
+
mergeUserHooksIntoSettings(baseDir, allUserHooks);
|
|
1515
|
+
console.log(`\u2713 .claude/settings.json updated with ${allUserHooks.length} user-defined hook(s)`);
|
|
1516
|
+
}
|
|
1329
1517
|
}
|
|
1330
1518
|
|
|
1331
1519
|
// src/commands/pack.ts
|
|
@@ -1374,7 +1562,7 @@ async function runPublish(dir, access) {
|
|
|
1374
1562
|
// src/cli.ts
|
|
1375
1563
|
var pkgDir = dirname(fileURLToPath(import.meta.url));
|
|
1376
1564
|
var pkg = JSON.parse(
|
|
1377
|
-
|
|
1565
|
+
readFileSync5(join6(pkgDir, "../package.json"), "utf-8")
|
|
1378
1566
|
);
|
|
1379
1567
|
var program = new Command();
|
|
1380
1568
|
program.name("mcpill").version(pkg.version);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcpill",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.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
|
},
|