mcpill 1.2.4 → 1.3.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 +6 -0
- package/MANUAL-TEST-GUIDE-SPEC.md +270 -0
- package/README.md +17 -0
- package/dist/cli.js +388 -158
- package/package.json +1 -1
- package/src/__tests__/compile.test.ts +203 -0
- package/src/cli.ts +3 -2
- package/src/commands/compile.ts +31 -1
- package/src/commands/init.ts +100 -0
- package/src/commands/validate.ts +29 -1
- package/src/compiler/hooks.ts +140 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.3.0
|
|
4
|
+
|
|
5
|
+
- `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`
|
|
6
|
+
- `AGENT.md` `## Tools` table still supported; AGENT.md wins on name conflict when both sources declare the same tool
|
|
7
|
+
- `mcpill init` template updated to include `replaces` in the example tool section
|
|
8
|
+
|
|
3
9
|
## 1.2.4
|
|
4
10
|
|
|
5
11
|
- 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
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Feature Specification"
|
|
3
|
+
type: planning
|
|
4
|
+
tags: [planning, specification, scoping]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Specification: mcpill compile-hooks — Manual Test Guide
|
|
8
|
+
|
|
9
|
+
**Date:** 2026-05-26
|
|
10
|
+
**Author:** ruco
|
|
11
|
+
**Status:** Draft
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Overview
|
|
16
|
+
|
|
17
|
+
> `mcpill compile` auto-generates a `PreToolUse` hook in `.claude/settings.json` that warns Claude to use the pill's MCP tools instead of native file tools (`Read`, `Bash`, `Edit`, `Write`). The hook derives its matcher and message from the `## Tools` table in `AGENT.md`, making each pill self-enforcing without manual setup. Key behaviours: merge without overwrite, `--no-hooks` opt-out, and `mcpill validate` warning when the hook is absent.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Background & Motivation
|
|
22
|
+
|
|
23
|
+
Without this feature, a pill author must manually wire a `PreToolUse` hook into `.claude/settings.json` every time they publish or update a pill. Agents forget to use the registered MCP tools and fall back to native tools, bypassing the pill entirely. The compile step already has all the information needed (pill name, tool names, which native tools each replaces) — generating the hook at compile time ties it to the source of truth.
|
|
24
|
+
|
|
25
|
+
## Goals
|
|
26
|
+
|
|
27
|
+
- `mcpill compile` writes or updates a `PreToolUse` hook in `<project>/.claude/settings.json` derived from the pill's `AGENT.md ## Tools` table.
|
|
28
|
+
- Existing entries in `settings.json` (e.g. `mcpServers`) are preserved; only the matching `PreToolUse` entry is replaced.
|
|
29
|
+
- `--no-hooks` opts out and writes `.mcpill/.no-hooks` as a sentinel so `validate` skips the check.
|
|
30
|
+
- `mcpill validate` emits a non-fatal warning when `AGENT.md` declares replacements but no hook is present.
|
|
31
|
+
|
|
32
|
+
## Non-Goals
|
|
33
|
+
|
|
34
|
+
- Auto-generating `AGENT.md` — the pill author writes it manually.
|
|
35
|
+
- Writing to the global `~/.claude/settings.json` — scope is always the project root.
|
|
36
|
+
- Enforcing hook presence (fatal error) — the warning is advisory only.
|
|
37
|
+
|
|
38
|
+
## Detailed Design
|
|
39
|
+
|
|
40
|
+
### Data Model
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
AGENT.md ## Tools table — expected columns (order-independent, case-insensitive header):
|
|
44
|
+
| Tool | Description | Replaces |
|
|
45
|
+
|--------------|-------------------|---------------|
|
|
46
|
+
| read-chunks | Read file chunks | Read |
|
|
47
|
+
| run-query | Execute SQL | Bash,Read |
|
|
48
|
+
|
|
49
|
+
"Replaces" cell: comma-separated native tool names (Read, Bash, Edit, Write).
|
|
50
|
+
Omitting the column or leaving a cell blank → no hook generated for that row.
|
|
51
|
+
|
|
52
|
+
.claude/settings.json — hooks section written by compile:
|
|
53
|
+
{
|
|
54
|
+
"hooks": {
|
|
55
|
+
"PreToolUse": [
|
|
56
|
+
{
|
|
57
|
+
"matcher": "Read|Bash",
|
|
58
|
+
"hooks": [{ "type": "command", "command": "echo \"[mcpill:<name>] Use pill tools instead: <tool>→<native>; ... See .mcpill/AGENT.md.\"" }]
|
|
59
|
+
}
|
|
60
|
+
]
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
Merge key: matcher string (exact match). Replace entry if matcher already exists; append otherwise.
|
|
65
|
+
|
|
66
|
+
.mcpill/.no-hooks — presence signals that hook generation was opted out; written by --no-hooks flag.
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### API / Interface
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
# CLI
|
|
73
|
+
mcpill compile [--dir <path>] [--no-hooks]
|
|
74
|
+
|
|
75
|
+
# Programmatic
|
|
76
|
+
runCompile(opts: { dir?: string; toMd?: boolean; strict?: boolean; noHooks?: boolean }): Promise<void>
|
|
77
|
+
|
|
78
|
+
# AGENT.md lookup order
|
|
79
|
+
1. <baseDir>/AGENT.md
|
|
80
|
+
2. <baseDir>/.mcpill/AGENT.md
|
|
81
|
+
(first found wins; if neither exists, hook generation is skipped silently)
|
|
82
|
+
|
|
83
|
+
# Hook message format
|
|
84
|
+
[mcpill:<pillName>] Use pill tools instead: <tool1>→<native1>,<native2>; <tool2>→<native3>. See .mcpill/AGENT.md.
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Behavior
|
|
88
|
+
|
|
89
|
+
1. `mcpill compile` runs the standard compile pipeline (server.md → .mcpill/server/).
|
|
90
|
+
2. If `--no-hooks` was passed: write `.mcpill/.no-hooks`, return early — no hook written.
|
|
91
|
+
3. Locate `AGENT.md` at `baseDir/AGENT.md` or `.mcpill/AGENT.md`; if absent, skip hook generation.
|
|
92
|
+
4. Parse `## Tools` table — find the `replaces` column (case-insensitive). Extract comma-separated native tool names per row. Skip rows with empty/missing replaces cell.
|
|
93
|
+
5. If no rows have replacements, skip hook generation.
|
|
94
|
+
6. Build `HookEntry`: matcher = all unique native tool names joined by `|`; command = static `echo` with pill name and full tool→native summary baked in.
|
|
95
|
+
7. Read `.claude/settings.json` (or start from `{}`). Find existing `hooks.PreToolUse` array entry with same matcher; replace it, or append if not found. Write back.
|
|
96
|
+
8. `mcpill validate` — after structural validation passes, check: if `.mcpill/.no-hooks` absent AND `AGENT.md` declares replacements AND `.claude/settings.json` has no `PreToolUse` entries → emit `⚠ No PreToolUse hook in .claude/settings.json — run mcpill compile to add it`.
|
|
97
|
+
|
|
98
|
+
## Error Handling
|
|
99
|
+
|
|
100
|
+
| Error Case | Behavior | User-Facing Message |
|
|
101
|
+
|---|---|---|
|
|
102
|
+
| `.claude/settings.json` exists but is invalid JSON | `JSON.parse` throws → compile crashes with a JS stack trace | *(raw error)* — fix or delete the malformed settings.json |
|
|
103
|
+
| `AGENT.md` exists but has no `## Tools` section | `parseAgentMdTools` returns `[]` → hook generation silently skipped | *(none)* |
|
|
104
|
+
| `AGENT.md` has `## Tools` but no `replaces` column | `parseAgentMdTools` returns `[]` → silently skipped | *(none)* |
|
|
105
|
+
| Compile run with `--no-hooks`; later `validate` called | `.no-hooks` sentinel present → validate skips hook check entirely | *(none)* |
|
|
106
|
+
| `baseDir/.claude/` does not exist | `mkdirSync(claudeDir, { recursive: true })` creates it | *(none — transparent)* |
|
|
107
|
+
|
|
108
|
+
## Security Considerations
|
|
109
|
+
|
|
110
|
+
- Hook command is a static `echo` — no shell variable expansion, no user-controlled input interpolated into the command string at runtime. `JSON.stringify(message)` in `buildHookEntry` ensures the baked-in string is safely quoted.
|
|
111
|
+
- `settings.json` is scoped to `baseDir/.claude/` — no writes to global config.
|
|
112
|
+
- `AGENT.md` content is read and parsed; no `eval`/`exec` of its contents.
|
|
113
|
+
|
|
114
|
+
## Testing Plan
|
|
115
|
+
|
|
116
|
+
Run all automated tests first: `npm test` — all 6 hook tests in `src/__tests__/compile.test.ts` must pass.
|
|
117
|
+
|
|
118
|
+
Then follow these manual steps against a real pill directory.
|
|
119
|
+
|
|
120
|
+
### Setup
|
|
121
|
+
|
|
122
|
+
Create a scratch pill for testing:
|
|
123
|
+
|
|
124
|
+
```sh
|
|
125
|
+
mkdir /tmp/test-pill && cd /tmp/test-pill
|
|
126
|
+
mcpill init
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Add a minimal `AGENT.md` at the project root:
|
|
130
|
+
|
|
131
|
+
```markdown
|
|
132
|
+
# AGENT
|
|
133
|
+
|
|
134
|
+
## Tools
|
|
135
|
+
|
|
136
|
+
| Tool | Description | Replaces |
|
|
137
|
+
|------|-------------|----------|
|
|
138
|
+
| read-chunks | Read a file in chunks | Read |
|
|
139
|
+
| run-query | Execute a SQL query | Bash,Read |
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
### Test 1 — Happy path: hook is written
|
|
145
|
+
|
|
146
|
+
- [ ] Run `mcpill compile` in `/tmp/test-pill`.
|
|
147
|
+
- [ ] Verify `.claude/settings.json` was created.
|
|
148
|
+
- [ ] Open it and confirm:
|
|
149
|
+
- `hooks.PreToolUse` is an array with one entry.
|
|
150
|
+
- `matcher` is `"Read|Bash"` (or `"Bash|Read"` — order may vary).
|
|
151
|
+
- `hooks[0].command` contains `echo`.
|
|
152
|
+
- The echo string contains `[mcpill:` followed by the pill name (from `.mcpill/server.md` or `PILL.md`).
|
|
153
|
+
- The echo string contains `read-chunks→Read` and `run-query→Bash,Read`.
|
|
154
|
+
- The echo string ends with `See .mcpill/AGENT.md.`
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
### Test 2 — Merge: existing settings.json is preserved
|
|
159
|
+
|
|
160
|
+
- [ ] Add a fake `mcpServers` entry to `.claude/settings.json`:
|
|
161
|
+
```json
|
|
162
|
+
{ "mcpServers": { "some-server": { "command": "node", "args": [] } } }
|
|
163
|
+
```
|
|
164
|
+
- [ ] Run `mcpill compile` again.
|
|
165
|
+
- [ ] Verify `.claude/settings.json` still contains `mcpServers.some-server`.
|
|
166
|
+
- [ ] Verify `hooks.PreToolUse` is also present (not the only key).
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
### Test 3 — Re-compile replaces the hook, not appends
|
|
171
|
+
|
|
172
|
+
- [ ] Change the `Replaces` cell of `run-query` to just `Bash` (remove `Read`).
|
|
173
|
+
- [ ] Run `mcpill compile`.
|
|
174
|
+
- [ ] Verify `hooks.PreToolUse` still has exactly **one** entry (not two).
|
|
175
|
+
- [ ] Verify the matcher reflects the updated set.
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
### Test 4 — `--no-hooks` flag
|
|
180
|
+
|
|
181
|
+
- [ ] Delete `.claude/settings.json` and `.mcpill/.no-hooks` if they exist.
|
|
182
|
+
- [ ] Run `mcpill compile --no-hooks`.
|
|
183
|
+
- [ ] Verify `.claude/settings.json` was **not** created.
|
|
184
|
+
- [ ] Verify `.mcpill/.no-hooks` **was** created (empty file).
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
### Test 5 — `mcpill validate` warns when hook is absent
|
|
189
|
+
|
|
190
|
+
- [ ] Delete `.claude/settings.json` and `.mcpill/.no-hooks`.
|
|
191
|
+
- [ ] Run `mcpill compile --no-hooks` (ensures `.no-hooks` is present, then delete it manually).
|
|
192
|
+
- [ ] Delete `.mcpill/.no-hooks`.
|
|
193
|
+
- [ ] Ensure `AGENT.md` still has a `Replaces` column with values.
|
|
194
|
+
- [ ] Run `mcpill validate`.
|
|
195
|
+
- [ ] Verify output contains `⚠ No PreToolUse hook in .claude/settings.json`.
|
|
196
|
+
- [ ] Verify the exit code is **0** (non-fatal — validate still passes).
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
### Test 6 — `mcpill validate` skips hook check when `.no-hooks` present
|
|
201
|
+
|
|
202
|
+
- [ ] Run `mcpill compile --no-hooks`.
|
|
203
|
+
- [ ] Run `mcpill validate`.
|
|
204
|
+
- [ ] Verify **no** `⚠ No PreToolUse hook` warning appears in output.
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
### Test 7 — No AGENT.md: hook silently skipped
|
|
209
|
+
|
|
210
|
+
- [ ] Move `AGENT.md` out of the project root (e.g. `/tmp/AGENT.md.bak`).
|
|
211
|
+
- [ ] Delete `.claude/settings.json` if present.
|
|
212
|
+
- [ ] Run `mcpill compile`.
|
|
213
|
+
- [ ] Verify `.claude/settings.json` was **not** created.
|
|
214
|
+
- [ ] Restore `AGENT.md`.
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
### Test 8 — AGENT.md in `.mcpill/` subdirectory
|
|
219
|
+
|
|
220
|
+
- [ ] Move `AGENT.md` to `.mcpill/AGENT.md`.
|
|
221
|
+
- [ ] Delete `.claude/settings.json` if present.
|
|
222
|
+
- [ ] Run `mcpill compile`.
|
|
223
|
+
- [ ] Verify `.claude/settings.json` **was** created with the hook.
|
|
224
|
+
- [ ] Move `AGENT.md` back to project root.
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
### Test 9 — AGENT.md with no `replaces` column: no hook
|
|
229
|
+
|
|
230
|
+
- [ ] Replace `AGENT.md` content with a table that has no `Replaces` column:
|
|
231
|
+
```markdown
|
|
232
|
+
## Tools
|
|
233
|
+
| Tool | Description |
|
|
234
|
+
|------|-------------|
|
|
235
|
+
| read-chunks | Read a file |
|
|
236
|
+
```
|
|
237
|
+
- [ ] Delete `.claude/settings.json`.
|
|
238
|
+
- [ ] Run `mcpill compile`.
|
|
239
|
+
- [ ] Verify `.claude/settings.json` was **not** created.
|
|
240
|
+
- [ ] Restore the original `AGENT.md`.
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
### Test 10 — Real pill smoke test (xtage or mcpster)
|
|
245
|
+
|
|
246
|
+
- [ ] Run `mcpill compile` in a pill repo that already has a real `AGENT.md` with tool replacements.
|
|
247
|
+
- [ ] Open `.claude/settings.json` and read the generated hook command.
|
|
248
|
+
- [ ] Start a Claude Code session in that repo and deliberately use `Read` on a file.
|
|
249
|
+
- [ ] Verify the hook fires and the reminder message is shown in the terminal output.
|
|
250
|
+
- [ ] Confirm the message names specific MCP tools and the pill name — not a generic warning.
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
## Open Questions
|
|
255
|
+
|
|
256
|
+
- [ ] Should the hook message include a direct invocation hint (`call read-chunks with path=...`) or keep it as a passive reminder? Currently a passive reminder. Defer until Test 10 reveals whether agents change behavior.
|
|
257
|
+
- [ ] Should `mcpill validate` check that the *specific matcher* in `settings.json` covers *all* declared replacements, or just that any `PreToolUse` entry exists? Currently the latter (any entry = ok). Tighten in a follow-up if agents still miss tools.
|
|
258
|
+
|
|
259
|
+
## Alternatives Considered
|
|
260
|
+
|
|
261
|
+
| Alternative | Pros | Cons | Decision |
|
|
262
|
+
|---|---|---|---|
|
|
263
|
+
| Read AGENT.md at hook-trigger time (not baked in) | Always up-to-date without recompile | I/O on every `Read`/`Bash` call; hook latency spikes | Rejected — bake at compile time |
|
|
264
|
+
| Write to `~/.claude/settings.json` (global) | Works without project setup | Bleeds hook into every project on the machine | Rejected — project-scoped only |
|
|
265
|
+
| Fatal error in `validate` if hook missing | Strong enforcement | Breaks projects that manage hooks separately | Rejected — non-fatal warning only |
|
|
266
|
+
| Generate `AGENT.md` at compile time | Less manual work for pill authors | Inverts ownership — `replaces` is semantic knowledge only the author can declare | Rejected — user writes it manually |
|
|
267
|
+
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
*Made with [mdblu](https://github.com/ruco-ai/mdblu) · source: `templates/SPEC.md.template`*
|
package/README.md
CHANGED
|
@@ -47,6 +47,7 @@ Scaffolds a new project in the current directory:
|
|
|
47
47
|
- `.mcpill/server.md` — server config + resources
|
|
48
48
|
- `.mcpill/server/prompts/greeting.md` — example prompt
|
|
49
49
|
- `.mcpill/HELLO-MCP.md` — ready-to-run example (copy into `PILL.md` to try it)
|
|
50
|
+
- `.claude/commands/create-pill.md` — `/create-pill` slash command; say `/create-pill` in Claude Code to start the guided pill-creation workflow
|
|
50
51
|
- `package.json` — `{ type: "module", dependencies: { mcpill-runtime } }` — run `npm install`
|
|
51
52
|
|
|
52
53
|
### `mcpill compile`
|
|
@@ -57,8 +58,24 @@ Compiles `server.md` + `tools/*.md` + `prompts/*.md` into a `.<name>/` pill arti
|
|
|
57
58
|
mcpill compile # forward: source → pill
|
|
58
59
|
mcpill compile --to-md # reverse: pill → source
|
|
59
60
|
mcpill compile --strict # error on missing tool handlers (default: stub)
|
|
61
|
+
mcpill compile --no-hooks # skip writing PreToolUse hook to .claude/settings.json
|
|
60
62
|
```
|
|
61
63
|
|
|
64
|
+
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:
|
|
65
|
+
|
|
66
|
+
- **`PILL.md`** (recommended) — add a `replaces` field to any `## Tool:` section:
|
|
67
|
+
|
|
68
|
+
```markdown
|
|
69
|
+
## Tool: read-chunks
|
|
70
|
+
|
|
71
|
+
description: Read a file by token-sized chunks.
|
|
72
|
+
replaces: Read
|
|
73
|
+
behavior: |
|
|
74
|
+
...
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
- **`AGENT.md`** — a `## Tools` table with a `Replaces` column (existing format). AGENT.md wins on name conflicts.
|
|
78
|
+
|
|
62
79
|
### `mcpill validate`
|
|
63
80
|
|
|
64
81
|
Validates all pill directories (`.<name>/`) in the project root.
|