make-runner-mcp 2.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Davinder S. Mahal
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,359 @@
1
+ # Adapting a project's Makefile for make-runner-mcp
2
+
3
+ **Audience: an AI coding agent**, of any capability level. Give this file
4
+ to an agent (attach it, paste it, or point at its path) with an instruction
5
+ like: *"Follow MAKEFILE-GUIDE.md exactly, in order, to make this project's
6
+ Makefile(s) compatible with make-runner-mcp."* The agent should be working
7
+ inside the target project's own repo — this file is not about
8
+ make-runner-mcp's own code.
9
+
10
+ This guide is written to be followed literally, step by step, with
11
+ copy-pasteable commands, rather than inferred from general Make knowledge.
12
+ **If a step's outcome is ambiguous, or you are not sure what to do, stop and
13
+ say so instead of guessing.** Every step below tells you exactly what to run
14
+ and what "stop and ask a human" looks like for that step.
15
+
16
+ ## 0. The rule, stated once, precisely
17
+
18
+ An AI agent calling a target through make-runner-mcp may pass:
19
+
20
+ | Allowed | Example | Not allowed | Why |
21
+ |---|---|---|---|
22
+ | A flag from this exact list: `-n --dry-run --just-print --recon -B --always-make -k --keep-going -s --silent --quiet -i --ignore-errors -q --question` | `-n` | Any other flag, e.g. `-C`, `-f`, `-j`, `--eval` | Only these are allowlisted; everything else is rejected |
23
+ | `VAR=value`, where `value` matches `^[A-Za-z0-9 _.,+/@:^~=-]*$` | `ARGS=install symfony/console --no-dev` | `VAR=value` where value contains `` ; & | $ ` ' " < > ( ) { } # \ * ? [ ] `` or a newline | Those characters could break out of the recipe line make later runs through a real shell |
24
+ | `VAR=value` for any variable name **except**: `SHELL MAKE MAKEFLAGS MAKEFILES MAKELEVEL MAKECMDGOALS VPATH GPATH PATH` | `ARGS=...`, `ENV=staging` | `SHELL=...`, `PATH=...`, etc. | These control how make itself executes, not what a target does |
25
+ | — | — | Any bare word with no `VAR=` in front of it, e.g. `install`, `--no-dev`, `symfony/console` on its own | make would treat it as an *extra build goal*, which could run a second, unvetted target |
26
+
27
+ Everything in this guide exists to get a project's Makefile to fit inside
28
+ this table without changing what `make <target>` does for a human typing it
29
+ directly.
30
+
31
+ ## 1. Find every file that defines targets
32
+
33
+ Run, from the project root:
34
+
35
+ ```bash
36
+ test -f Makefile && echo "root Makefile: OK" || echo "STOP: no root Makefile — make-runner-mcp requires <project root>/Makefile to exist"
37
+ grep -n '^\s*\(-\|s\)\?include\s' Makefile
38
+ ```
39
+
40
+ The second command lists any `include`/`-include`/`sinclude` lines in the
41
+ root Makefile. Open every file they point to, and repeat the same `grep`
42
+ on each of those (includes can nest). Write down the full list of files —
43
+ you'll re-check all of them in the steps below. If any include target uses
44
+ a `$(...)` variable in its path (e.g. `include $(ENV).mk`), make-runner-mcp
45
+ cannot resolve it either — note this file as **not scanned**, and flag it
46
+ in your final report (§8).
47
+
48
+ make-runner-mcp also follows the catch-all forwarding idiom automatically
49
+ — a bare `%:` rule whose recipe runs `$(MAKE) -C <dir> ...` — so targets
50
+ only reachable that way don't need anything from this guide either; see
51
+ §2's note on it.
52
+
53
+ ### 1a. When neither `include` nor forwarding actually connects the files
54
+
55
+ Two cases §1 flags as unresolvable — an include path built from a `$(VAR)`,
56
+ or a second Makefile that's genuinely a separate, independently-invoked
57
+ Makefile with no real `include`/forwarding link to the root one at all —
58
+ mean make-runner-mcp can't discover that file's targets by parsing, because
59
+ real `make -f Makefile <target>` wouldn't find them either. For exactly
60
+ this situation (not as a substitute for a real `include` you could add
61
+ instead — prefer that when it's an option, since it keeps the linked file's
62
+ variables in scope), add a plain comment to the file make-runner-mcp does
63
+ parse:
64
+
65
+ ```makefile
66
+ ## make-runner: also-read docker/Makefile
67
+ ```
68
+
69
+ This is a hint to make-runner-mcp only — it has no effect on what `make`
70
+ itself does with this file, so a human running `make docker-stuff` by hand
71
+ sees no change in behavior. One or more space-separated paths are allowed
72
+ on the same line, each resolved relative to the file the comment is in
73
+ (same resolution rule as `include`). A target found this way is executed
74
+ directly against that other file (`make -f docker/Makefile <target>`, run
75
+ from `docker/`'s own directory) rather than through the root Makefile — so
76
+ it will **not** see variables the root Makefile sets for it (unlike a real
77
+ `include`), and its own tool description in the MCP tool list will say
78
+ `via docker/Makefile` so a calling agent knows it's a separate file. If a
79
+ target's recipe actually depends on a variable only the root Makefile
80
+ defines, this marker isn't the right fix — restructure so that variable is
81
+ set inside the linked file itself, or use a real `include` instead.
82
+
83
+ ## 2. Detect the incompatible pattern
84
+
85
+ Run this across the root Makefile and every included file you found in
86
+ step 1 (repeat per file, or point `grep -r` at the whole project if all
87
+ your make files live under one directory):
88
+
89
+ ```bash
90
+ grep -n "MAKECMDGOALS" Makefile
91
+ grep -n 'filter-out \$@' Makefile
92
+ grep -n "wordlist" Makefile
93
+ grep -n "^%:" Makefile
94
+ ```
95
+
96
+ Any hit means at least one target relies on bare-word passthrough (the
97
+ `$(MAKECMDGOALS)` / catch-all-rule pattern) — this is what §0's "no bare
98
+ words" rule breaks. For each hit, find which real target's recipe consumes
99
+ it (read a few lines above/below the match) and add that target's name to
100
+ a list: **targets to convert**.
101
+
102
+ A `^%:` hit whose recipe contains `$(MAKE)` with a `-C` flag (forwarding
103
+ any undefined goal to another directory's Makefile, e.g. `%:` →
104
+ `@$(MAKE) -C docker $@`) is a *different*, already-supported idiom —
105
+ make-runner-mcp discovers and exposes those forwarded targets
106
+ automatically. Do not add it to **targets to convert**, and skip §4's
107
+ "remove the dead catch-all rule" for it. Only a `%:` rule that swallows
108
+ bare-word `$(MAKECMDGOALS)` arguments (typically paired with an `@:`
109
+ no-op recipe, no `$(MAKE) -C ...` call) is the incompatible pattern this
110
+ guide's §3–4 converts or removes.
111
+
112
+ If none of the four commands return anything, skip to §6 — there's nothing
113
+ to convert, only the checks in §5–§7 still apply.
114
+
115
+ ## 3. Convert each target on your list
116
+
117
+ For **each** target name in "targets to convert," do exactly this:
118
+
119
+ 1. Open the file it's defined in.
120
+ 2. Find its recipe (the indented line(s) under `target:`).
121
+ 3. Replace every use of `$(MAKECMDGOALS)`, `$(filter-out $@,$(MAKECMDGOALS))`,
122
+ or similar with a single variable named `ARGS`.
123
+ 4. Rewrite (or write, if missing) its `##` comment to say, verbatim style:
124
+ `## <what it does>. Pass args via ARGS, e.g. ARGS="<realistic example>"`
125
+
126
+ Template — replace the bracketed parts:
127
+
128
+ ```makefile
129
+ # BEFORE
130
+ [TARGET]: ## [old comment, if any]
131
+ [COMMAND] $(filter-out $@,$(MAKECMDGOALS))
132
+
133
+ # AFTER
134
+ [TARGET]: ## [what it does]. Pass args via ARGS, e.g. ARGS="[realistic example value]"
135
+ [COMMAND] $(ARGS)
136
+ ```
137
+
138
+ Concrete worked example:
139
+
140
+ ```makefile
141
+ # BEFORE
142
+ composer: ## Run composer
143
+ docker compose exec app composer $(filter-out $@,$(MAKECMDGOALS))
144
+
145
+ # AFTER
146
+ composer: ## Run composer inside the app container. Pass args via ARGS, e.g. ARGS="install symfony/console --no-dev"
147
+ docker compose exec app composer $(ARGS)
148
+ ```
149
+
150
+ Do not rename the target itself. Do not change what command it runs
151
+ (`docker compose exec app composer ...` stays the same) — only how it
152
+ receives its arguments changes.
153
+
154
+ ## 4. Remove the dead catch-all rule, if it's now unused
155
+
156
+ After converting every target on your list, run step 2's commands again:
157
+
158
+ ```bash
159
+ grep -n "MAKECMDGOALS" Makefile
160
+ grep -n "^%:" Makefile
161
+ ```
162
+
163
+ If `^%:` still has a match and nothing else references `MAKECMDGOALS`
164
+ anymore, that catch-all rule (and its `@:` no-op recipe, and any
165
+ `.PHONY` entry naming it) is now dead code — delete it. If `MAKECMDGOALS`
166
+ still has matches, another target still depends on it: go back to §3 and
167
+ convert that one too, then re-run this check.
168
+
169
+ ## 5. Check every target name against the hard-coded denylist
170
+
171
+ make-runner-mcp always blocks target names containing `deploy`, `destroy`,
172
+ `prod`, `publish`, or `release` as a substring (checked after removing any
173
+ `-`, `_`, `.` characters), plus anything starting with `rm-`. Run this to
174
+ find every target name and flag likely false positives:
175
+
176
+ ```bash
177
+ grep -hoE '^[A-Za-z0-9][A-Za-z0-9_.-]*:($|[^=])' Makefile | sed 's/:.*//' | sort -u | while read -r t; do
178
+ norm=$(echo "$t" | tr -d '_.-' | tr '[:upper:]' '[:lower:]')
179
+ case "$norm" in
180
+ *deploy*|*destroy*|*prod*|*publish*|*release*)
181
+ echo "CHECK: '$t' will be blocked (matches denylist word)" ;;
182
+ esac
183
+ case "$t" in
184
+ rm-*) echo "CHECK: '$t' will be blocked (starts with rm-)" ;;
185
+ esac
186
+ done
187
+ ```
188
+
189
+ (The `:($|[^=])` — colon followed by end-of-line or a non-`=` character — is
190
+ deliberate, not decoration: it's what keeps a variable assignment like
191
+ `RELEASE_TAG := v1.0.0` from being misread as a target named `RELEASE_TAG`
192
+ and wrongly flagged here. A plain `name:` pattern would catch that false
193
+ positive whenever there's no space before `:=`.)
194
+
195
+ For every line this prints: **do not rename the target yourself.** Decide
196
+ only whether it's a correct block (a real deploy/destroy/publish/release
197
+ target — leave it, this is working as intended) or a false positive (an
198
+ unrelated target that happens to contain the substring, e.g.
199
+ `producer-report`, `reproduce-fixture`). List every false positive in your
200
+ final report (§8) as `NEEDS HUMAN DECISION` — a human decides whether to
201
+ rename it.
202
+
203
+ ## 6. Check for reliance on reserved variables
204
+
205
+ ```bash
206
+ grep -nE '\$\((SHELL|MAKE|MAKEFLAGS|MAKEFILES|MAKELEVEL|MAKECMDGOALS|VPATH|GPATH|PATH)\)' Makefile
207
+ ```
208
+
209
+ A match by itself is not necessarily a problem — Makefiles routinely use
210
+ `$(MAKE)` for recursive calls or `$(SHELL)` internally. It's only a problem
211
+ if a target's **documentation, comment, or README instructions tell a
212
+ caller to override one of these on the command line** (e.g. "run `make
213
+ build SHELL=/bin/bash`"). If you find that, list it in your final report as
214
+ `NEEDS HUMAN DECISION` — this pattern cannot be supported through
215
+ make-runner-mcp and the target needs to be restructured so callers don't
216
+ need to override that variable.
217
+
218
+ ## 7. Backfill missing or oversized descriptions
219
+
220
+ ```bash
221
+ grep -nE '^[A-Za-z0-9][A-Za-z0-9_.-]*:($|[^=])' Makefile | grep -v '##'
222
+ ```
223
+
224
+ Every line this prints is a target with no `##` comment — it will show up
225
+ to an AI agent as a generic `Run 'make <name>'` description instead of a
226
+ useful one. Add one: `target: ## <one-sentence description>`. Keep each
227
+ comment under 200 characters (longer ones get truncated) and don't put
228
+ secrets, internal hostnames, or anything sensitive in it — the comment is
229
+ sent to every calling agent on every tool listing, whether or not the
230
+ target is ever invoked.
231
+
232
+ ## 8. Validate your own `ARGS` example values
233
+
234
+ For every `ARGS="..."` example you wrote into a comment in §3, check the
235
+ example text itself against this pattern (letters, digits, spaces, and
236
+ `` _ . , + / @ : ^ ~ = - `` only):
237
+
238
+ ```bash
239
+ python3 -c "import re,sys; s=sys.argv[1]; print('OK' if re.fullmatch(r'[A-Za-z0-9 _.,+/@:^~=-]*', s) else 'INVALID')" 'install symfony/console --no-dev'
240
+ ```
241
+
242
+ (Swap in each example value you wrote.) If it prints `INVALID`, your
243
+ example itself wouldn't work through make-runner-mcp — pick a different,
244
+ representative example, or note in your final report that this target's
245
+ real-world arguments don't fit the safe character set and needs a human
246
+ decision (do not try to work around this — see §9).
247
+
248
+ ## 9. Test your changes
249
+
250
+ **Always do this — it needs nothing but `make` itself:**
251
+
252
+ ```bash
253
+ make -n [target] ARGS="[the example value from its comment]"
254
+ ```
255
+
256
+ `-n` is a dry run: it prints the command make *would* run without running
257
+ it. Confirm the printed line has your `ARGS` value substituted in the
258
+ right place, and that the target isn't silently doing nothing. Do this for
259
+ every target you touched.
260
+
261
+ **If make-runner-mcp is available in this environment** (i.e. you can `cd`
262
+ into its repo and run Node), do this deeper check too — it exercises the
263
+ actual server, not just `make` directly:
264
+
265
+ ```bash
266
+ cd /path/to/make-runner-mcp
267
+ PROJECT_DIR=/path/to/the/project node -e '
268
+ const { spawn } = require("child_process");
269
+ // MCP_TRANSPORT=stdio is required here: make-runner-mcp defaults to the
270
+ // http transport (for sandboxed-agent use, see its own README), and this
271
+ // probe talks to it over stdin/stdout, not a network port.
272
+ const child = spawn("node", ["server.js"], {
273
+ env: { ...process.env, PROJECT_DIR: process.env.PROJECT_DIR, MCP_TRANSPORT: "stdio" },
274
+ stdio: ["pipe", "pipe", "pipe"],
275
+ });
276
+ let buf = "";
277
+ child.stdout.on("data", (d) => {
278
+ buf += d.toString();
279
+ let i;
280
+ while ((i = buf.indexOf("\n")) >= 0) {
281
+ const line = buf.slice(0, i);
282
+ buf = buf.slice(i + 1);
283
+ if (line.trim()) console.log(line);
284
+ }
285
+ });
286
+ function send(o) { child.stdin.write(JSON.stringify(o) + "\n"); }
287
+ send({ jsonrpc: "2.0", id: 1, method: "initialize", params: { protocolVersion: "2024-11-05", capabilities: {}, clientInfo: { name: "t", version: "1" } } });
288
+ setTimeout(() => send({ jsonrpc: "2.0", id: 2, method: "tools/list", params: {} }), 300);
289
+ // Replace TOOL_NAME and the ARGS example with the target you are testing:
290
+ setTimeout(() => send({ jsonrpc: "2.0", id: 3, method: "tools/call", params: { name: "make__TOOL_NAME", arguments: { args: ["ARGS=install symfony/console --no-dev"] } } }), 700);
291
+ setTimeout(() => { child.kill(); process.exit(0); }, 1200);
292
+ '
293
+ ```
294
+
295
+ Check the `tools/list` response includes the target you converted, with
296
+ the description you wrote, and the `tools/call` response shows the
297
+ expected output with no `isError: true`.
298
+
299
+ If neither `make` nor Node/make-runner-mcp is available in your
300
+ environment at all, say so explicitly in your final report rather than
301
+ claiming you tested something you didn't run.
302
+
303
+ ## 10. Final report — always produce this
304
+
305
+ End your work with a plain-text summary in this exact shape:
306
+
307
+ ```
308
+ CONVERTED (MAKECMDGOALS -> ARGS):
309
+ - <target>: <one line: what changed>
310
+ - ...
311
+
312
+ REMOVED:
313
+ - <catch-all rule / dead code removed, or "none">
314
+
315
+ DESCRIPTIONS ADDED/UPDATED:
316
+ - <target>: <new comment text>
317
+ - ...
318
+
319
+ NEEDS HUMAN DECISION:
320
+ - <target>: <exact reason — denylist false positive / reserved-variable dependency / args don't fit the safe character set / include path uses a variable / etc.>
321
+ - ...
322
+
323
+ TESTED:
324
+ - <target>: <"dry-run only" | "dry-run + inspector" | "not tested — reason">
325
+ - ...
326
+
327
+ NOT TOUCHED (and why):
328
+ - <target>: <e.g. "already uses ARGS-style args, no change needed">
329
+ ```
330
+
331
+ Do not omit sections — write "none" where nothing applies. This is what a
332
+ human reviews to decide what to actually merge.
333
+
334
+ ## 11. Guardrails — do not do these, under any circumstance
335
+
336
+ - **Don't rename or restructure a target to dodge the hard-coded
337
+ denylist** (`deploy`, `destroy`, `prod`, `publish`, `release`, `rm-*`).
338
+ If it's genuinely one of those operations, leave it blocked — that's the
339
+ system working correctly. Only §5's narrow false-positive case goes in
340
+ your report, and even then a human decides, not you.
341
+ - **Don't try to route around the `VAR=value` character restriction** —
342
+ no base64/hex-encoding a value and decoding it in the recipe, no string
343
+ concatenation tricks, no alternate delimiters. If a target's real
344
+ arguments don't fit the safe character set (§8), report it as
345
+ `NEEDS HUMAN DECISION`. Do not invent a workaround.
346
+ - **Don't edit `.mcp-make-config.json` as a substitute for fixing a
347
+ target.** That file only narrows which targets are exposed
348
+ (`deny`/`allow`) and which env vars reach `make` (`envAllowlist`) — it
349
+ has no effect on argument handling and won't make an incompatible target
350
+ work.
351
+ - **Don't edit make-runner-mcp's own `server.js`** from inside a target
352
+ project's repo to widen the allowed flags or reserved-variable list. If
353
+ a project genuinely needs something currently blocked, that's a
354
+ deliberate, separate change to make-runner-mcp itself — report it, don't
355
+ make it yourself.
356
+ - **Don't mark something as tested if you didn't actually run a command
357
+ and see its output.** If you couldn't test (missing tooling, no
358
+ container running, etc.), say so plainly in §10 rather than guessing
359
+ that it probably works.