Skip to content

Commit 95eb507

Browse files
authored
fix(cli): curate wheels MCP tool surface and fix stats crash (#2139)
* feat(cli): hide cli-only commands from lucli mcp tools/list Uses the mcpHiddenTools() convention from LuCLI 0.3.4 to exclude mcp, d, new, console, start, stop, and browser from MCP auto-discovery. All seven remain callable as CLI subcommands. Agents doing 'tools/list' on the wheels MCP server now see 16 curated tools instead of 23, with stateful/interactive/meta commands filtered out. * fix(cli): guard Left(str,0) in sprintf helper for Lucee 7 The private sprintf() helper used by 'wheels stats' called left(result, match.pos[1] - 1) without guarding against pos=1. Lucee 7 throws 'parameter 2 of the function left can not be 0' where Lucee 6 returned an empty string silently. Added a ternary guard per the cross-engine compatibility pattern documented in the Wheels CLAUDE.md. Exercised via the sandbox-level MCP tools/call stats test. * chore(cli): delete redundant services/MCP.cfc schema registry The parallel schema registry at cli/lucli/services/MCP.cfc was never wired into LuCLI's MCP discovery path — LuCLI auto-discovers tool schemas from Module.cfc public function signatures, not from a schema service. The file also drifted: used wrong tool names (wheels_generate vs auto-discovered 'generate') and was missing notes/stats/db/etc. Rich parameter schemas for MCP tools will be reintroduced in a follow-up PR by adding typed parameters directly to Module.cfc public functions, where LuCLI's buildInputSchema() reads them. * docs(docs): document mcp curation, stats fix, mcp service removal
1 parent 2d2123e commit 95eb507

5 files changed

Lines changed: 58 additions & 289 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ All historical references to "CFWheels" in this changelog have been preserved fo
119119
- Architecture hardening: XSS helpers consolidated, error hooks added, interface verification (#2097)
120120
- CSRF cookie encryption key auto-generated when empty (apps should still set their own for stable cross-deploy cookies) (#2054)
121121
- CI engine testing restructured: 42 jobs reduced to 8 via engine-grouped testing (#1939)
122+
- `wheels mcp wheels` MCP surface curated — 7 CLI-only commands (`mcp`, `d`, `new`, `console`, `start`, `stop`, `browser`) hidden from MCP `tools/list` via the `mcpHiddenTools()` convention (requires LuCLI 0.3.4+). All remain reachable as CLI subcommands. Tool count drops from 23 to 16 for agent consumers.
122123

123124
### Deprecated
124125

@@ -132,11 +133,13 @@ All historical references to "CFWheels" in this changelog have been preserved fo
132133
- Railo compatibility workaround from `$initializeMixins` — Railo is no longer a target (#1987)
133134
- `server.cfc` file (#1902)
134135
- Stale monorepo artifacts after repository flatten (#1988)
136+
- `cli/lucli/services/MCP.cfc` parallel schema registry — never wired into LuCLI's MCP discovery, drifted out of sync with `Module.cfc`. Rich parameter schemas will return via typed parameters directly on Module.cfc functions in a follow-up PR.
135137

136138
### Fixed
137139

138140
- View lookup after `renderText()` / `renderWith()` no longer breaks subsequent partial rendering (#1991)
139141
- Scaffolded apps from `wheels new` now boot correctly (#2096)
142+
- `wheels stats` crash on Lucee 7 — private `sprintf()` helper called `Left(result, 0)` when the format string started with a placeholder. Lucee 7 throws where Lucee 6 returned empty silently. Added a ternary guard per the project's cross-engine compatibility pattern.
140143
- CockroachDB primary key uses `unique_rowid()` instead of `SERIAL` (#1986)
141144
- CockroachDB SQL generation fixes and soft-fail removed from test matrix (#1999)
142145
- CockroachDB `RETURNING` clause identity select (#1993)

cli/lucli/Module.cfc

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,31 @@ component extends="modules.BaseModule" {
9494
return result;
9595
}
9696

97+
// ─────────────────────────────────────────────────
98+
// MCP framework convention — hide CLI-only commands
99+
// ─────────────────────────────────────────────────
100+
101+
/**
102+
* hint: Declare public functions to hide from MCP tools/list.
103+
*
104+
* These remain reachable as CLI subcommands. Hidden because they are
105+
* stateful (start/stop), destructive (new scaffolds a whole project),
106+
* interactive (console), meta (mcp), alias (d), or don't translate to
107+
* single-call MCP semantics (browser). Read by LuCLI >= 0.3.4 per the
108+
* mcpHiddenTools() convention.
109+
*/
110+
public array function mcpHiddenTools() {
111+
return [
112+
"mcp", // meta command — prints MCP setup instructions
113+
"d", // alias for destroy
114+
"new", // scaffolds a whole new Wheels project
115+
"console", // interactive CFML REPL — not usable over stdio
116+
"start", // dev server lifecycle (stateful)
117+
"stop", // dev server lifecycle (stateful)
118+
"browser" // multi-step browser testing flow
119+
];
120+
}
121+
97122
// ─────────────────────────────────────────────────
98123
// generate — Code generation
99124
// ─────────────────────────────────────────────────
@@ -3778,7 +3803,9 @@ component extends="modules.BaseModule" {
37783803
} else {
37793804
value = repeatString(" ", max(0, width - len(value))) & value;
37803805
}
3781-
result = left(result, match.pos[1] - 1) & value & mid(result, match.pos[1] + match.len[1], len(result));
3806+
// Guard: Left(str, 0) throws on Lucee 7 ("parameter 2 cannot be 0")
3807+
var prefix = match.pos[1] > 1 ? left(result, match.pos[1] - 1) : "";
3808+
result = prefix & value & mid(result, match.pos[1] + match.len[1], len(result));
37823809
argIndex++;
37833810
}
37843811
return result;

cli/lucli/services/MCP.cfc

Lines changed: 0 additions & 173 deletions
This file was deleted.

cli/lucli/tests/specs/services/McpSpec.cfc

Lines changed: 0 additions & 115 deletions
This file was deleted.

cli/tests/specs/e2e/McpToolsTest.cfc

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,18 @@ component extends="testbox.system.BaseSpec" {
4242
"validate"
4343
];
4444

45+
// Public functions deliberately hidden from MCP tools/list via
46+
// mcpHiddenTools(). They remain callable as CLI subcommands.
47+
variables.expectedHiddenTools = [
48+
"mcp", // meta command
49+
"d", // alias for destroy
50+
"new", // scaffolds whole project
51+
"console", // interactive REPL
52+
"start", // dev server lifecycle
53+
"stop", // dev server lifecycle
54+
"browser" // multi-step browser flow
55+
];
56+
4557
// Create a temp project directory for tool invocation tests
4658
variables.testDir = getTempDirectory() & "wheels_e2e_mcp_" & createUUID();
4759
directoryCreate(variables.testDir);
@@ -175,6 +187,21 @@ component extends="testbox.system.BaseSpec" {
175187
);
176188
}
177189
});
190+
191+
it("declares mcpHiddenTools() to curate the MCP tool surface", function() {
192+
// The function must exist as a public function returning an array
193+
expect(variables.moduleSource contains "public array function mcpHiddenTools").toBeTrue(
194+
"Module.cfc must declare mcpHiddenTools() to hide CLI-only commands from MCP tools/list"
195+
);
196+
197+
// And it must name every tool we expect to hide
198+
for (var hidden in variables.expectedHiddenTools) {
199+
var pattern = '"' & hidden & '"';
200+
expect(variables.moduleSource contains pattern).toBeTrue(
201+
"mcpHiddenTools() must include '" & hidden & "' — check the returned array"
202+
);
203+
}
204+
});
178205
});
179206

180207
// ─── MCP Tool Invocation via Services ──────────

0 commit comments

Comments
 (0)