@henols/vice-mcp 0.1.11 → 0.2.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/backend-detect.mts +58 -8
- package/capability-registry.ts +388 -0
- package/disasm-decoder.ts +28 -4
- package/package.json +12 -1
- package/resources/backend-detect.mjs +30 -2
- package/resources/broker-launch.mjs +166 -34
- package/resources/vice-broker.mjs +25 -4
- package/stock-cia.ts +598 -0
- package/stock-connect.ts +137 -20
- package/stock-derived.ts +67 -14
- package/stock-diagnose.ts +994 -0
- package/stock-dispatch.ts +105 -4
- package/stock-handler.ts +29 -0
- package/stock-memory-search.ts +441 -0
- package/stock-memory.ts +92 -13
- package/stock-protocol.ts +328 -0
- package/stock-recycle.ts +500 -0
- package/stock-run-until.ts +400 -0
- package/stock-runstate.ts +46 -2
- package/stock-sprites.ts +712 -0
- package/stock-symbols.ts +431 -0
- package/stock-timing.ts +562 -0
- package/stock-vicii.ts +318 -0
- package/tools-manifest.stock.json +3031 -223
- package/version.ts +279 -0
- package/vice-proxy.ts +49 -6
package/version.ts
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
// The ONE authoritative implementation of this project's version-resolution
|
|
2
|
+
// algorithm (D-5). Before this file existed the repo carried FOUR
|
|
3
|
+
// hand-maintained version strings and none of them were true:
|
|
4
|
+
// `.claude/mcp/vice/package.json` and `installer/package.json` were stale
|
|
5
|
+
// placeholders CI never touched between releases, `.claude-plugin/plugin.json`
|
|
6
|
+
// was bumped by NO automation at all, and `vice-proxy.ts`'s own
|
|
7
|
+
// `PROXY_VERSION` literal -- advertised to every MCP client over
|
|
8
|
+
// `initialize` -- sat twelve patches behind npm's actual `latest`. This file
|
|
9
|
+
// exists so there is exactly one place that (a) parses the repo-root
|
|
10
|
+
// `VERSION` template, (b) resolves it against a published version per D-2's
|
|
11
|
+
// four rules, and (c) answers "what version am I, right now, at runtime"
|
|
12
|
+
// (D-4) -- every other consumer (the CLI in `scripts/version.mjs`,
|
|
13
|
+
// `vice-proxy.ts`'s `PROXY_VERSION`, CI's `release-on-merge` job) calls INTO
|
|
14
|
+
// this module rather than re-deriving any part of the algorithm locally.
|
|
15
|
+
//
|
|
16
|
+
// Do NOT reimplement the resolution rules (`pinned` / `no-published` /
|
|
17
|
+
// `prefix-differs` / `prefix-matches`) anywhere else in this repo -- not in
|
|
18
|
+
// `scripts/version.mjs`, not in CI YAML, not in `installer/bin/cli.mjs`.
|
|
19
|
+
// `version.test.ts`'s "single-implementation guard" test greps
|
|
20
|
+
// `scripts/version.mjs` for the rule literals to catch exactly that
|
|
21
|
+
// regression.
|
|
22
|
+
//
|
|
23
|
+
// Do NOT import `repo-root.ts` from this file. That module's `repoRoot()`
|
|
24
|
+
// carries a documented side effect (`ensureResourcesInstalled()`, fired at
|
|
25
|
+
// module-load time) this seam must never trigger just by being imported --
|
|
26
|
+
// `scripts/version.mjs` and any future test that imports this module for
|
|
27
|
+
// its pure functions alone would otherwise deploy host launcher resources as
|
|
28
|
+
// a side effect of asking what version something is. Per this repo's own
|
|
29
|
+
// convention (see `hostpath.ts`/`containerpath.ts`), the workspace root
|
|
30
|
+
// arrives here as an explicit argument (`readTemplate(repoRootDir)`) or a
|
|
31
|
+
// caller-supplied lazy thunk (`runtimeVersion({ repoRoot })`), never as an
|
|
32
|
+
// import this module resolves itself.
|
|
33
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
34
|
+
import { join } from "node:path";
|
|
35
|
+
|
|
36
|
+
/** The single self-evident placeholder every derived, publishable version
|
|
37
|
+
* string (the two npm package.json `.version` fields, the installer's
|
|
38
|
+
* `@henols/vice-mcp` dependency pin, and the three plugin-manifest version
|
|
39
|
+
* fields) carries in the working tree (R-2). Valid semver -- `npm pack`
|
|
40
|
+
* accepts it -- but unmistakably not a release. Overwritten at publish time
|
|
41
|
+
* by `npm version` (npm packages) or `scripts/version.mjs stamp` (plugin
|
|
42
|
+
* manifests); never hand-edited. */
|
|
43
|
+
export const DEV_PLACEHOLDER = "0.0.0-dev";
|
|
44
|
+
|
|
45
|
+
export type ResolveRule = "pinned" | "no-published" | "prefix-differs" | "prefix-matches";
|
|
46
|
+
|
|
47
|
+
export interface ResolveResult {
|
|
48
|
+
version: string;
|
|
49
|
+
rule: ResolveRule;
|
|
50
|
+
template: string;
|
|
51
|
+
published: string | null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Leading zeros are rejected (bare "0" is the only zero-value exception) --
|
|
55
|
+
// SemVer 2.0.0 SS2 forbids leading-zero numeric identifiers, and this
|
|
56
|
+
// component is echoed VERBATIM into the resolved string for literal (non-`-`)
|
|
57
|
+
// slots, so "1.00.0" must be rejected here rather than surfacing later as an
|
|
58
|
+
// npm-publish-time failure (MED-2).
|
|
59
|
+
const TEMPLATE_COMPONENT = /^(0|[1-9]\d*|-)$/;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Parse a version TEMPLATE (not a resolved version) into its three
|
|
63
|
+
* dot-separated components. Each component must be either a non-negative
|
|
64
|
+
* integer literal or the literal string `-` (an auto-managed slot, D-2).
|
|
65
|
+
* Throws on anything else: wrong component count, a non-numeric/non-`-`
|
|
66
|
+
* component, a blank/whitespace-only string, OR a literal component
|
|
67
|
+
* appearing after a `-` component (MED-1) -- D-2's "literal prefix" wording,
|
|
68
|
+
* and every row of CONTEXT.md's worked-example table, only ever describe
|
|
69
|
+
* dashes trailing literals, never leading or interleaved. A shape like
|
|
70
|
+
* "-.2.3" or "0.-.2" has no defined resolution semantics and must be
|
|
71
|
+
* rejected at the one seam that owns validating the hand-edited VERSION
|
|
72
|
+
* file, not silently resolved into something CONTEXT.md never specified.
|
|
73
|
+
*/
|
|
74
|
+
export function parseTemplate(raw: string): string[] {
|
|
75
|
+
const trimmed = raw.trim();
|
|
76
|
+
if (trimmed === "") {
|
|
77
|
+
throw new Error(`version.ts: malformed template ${JSON.stringify(raw)} -- empty`);
|
|
78
|
+
}
|
|
79
|
+
const parts = trimmed.split(".");
|
|
80
|
+
if (parts.length !== 3) {
|
|
81
|
+
throw new Error(
|
|
82
|
+
`version.ts: malformed template ${JSON.stringify(raw)} -- expected exactly 3 dot-separated components, got ${parts.length}`
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
let sawDash = false;
|
|
86
|
+
for (const part of parts) {
|
|
87
|
+
if (part === "-") {
|
|
88
|
+
sawDash = true;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (sawDash) {
|
|
92
|
+
throw new Error(
|
|
93
|
+
`version.ts: malformed template ${JSON.stringify(raw)} -- literal component ${JSON.stringify(part)} cannot follow a "-" component; dashes only trail literals (D-2)`
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
if (!TEMPLATE_COMPONENT.test(part)) {
|
|
97
|
+
throw new Error(
|
|
98
|
+
`version.ts: malformed template ${JSON.stringify(raw)} -- component ${JSON.stringify(part)} is neither an integer (no leading zeros) nor "-"`
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return parts;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Parse a PUBLISHED version string into a 3-tuple of numbers, stripping any
|
|
107
|
+
* `-prerelease` / `+build` suffix first. Returns null (never throws) when
|
|
108
|
+
* the input is null, not parseable, or does not have exactly 3 numeric
|
|
109
|
+
* dot-separated components -- callers treat null exactly like "nothing is
|
|
110
|
+
* published" (D-2 rule 4).
|
|
111
|
+
*/
|
|
112
|
+
function parsePublished(published: string | null): number[] | null {
|
|
113
|
+
if (published == null) return null;
|
|
114
|
+
const core = published.split(/[-+]/, 1)[0];
|
|
115
|
+
const parts = core.split(".");
|
|
116
|
+
if (parts.length !== 3) return null;
|
|
117
|
+
// Strict plain-decimal-digit validation (MED-4) -- deliberately NOT
|
|
118
|
+
// `Number(p)` + `Number.isInteger`, which also accepts hex-like literals
|
|
119
|
+
// ("0x2" -> 2), exponential notation ("5e2" -> 500), and whitespace-padded
|
|
120
|
+
// numbers. Those would silently coerce malformed `--published` input or an
|
|
121
|
+
// unexpected `npm view` response into a number that doesn't reflect the
|
|
122
|
+
// original text; this must instead fall back to null (== "no-published",
|
|
123
|
+
// D-2 rule 4) exactly like any other unparseable input, matching the
|
|
124
|
+
// strict digit validation `parseTemplate`'s `TEMPLATE_COMPONENT` already
|
|
125
|
+
// uses for the same kind of input.
|
|
126
|
+
if (!parts.every((p) => /^\d+$/.test(p))) return null;
|
|
127
|
+
return parts.map((p) => Number(p));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* The D-2 resolution algorithm, in full. `template` must already be
|
|
132
|
+
* well-formed (call `parseTemplate` first, or pass through unchanged --
|
|
133
|
+
* this function calls it internally so a malformed template always throws
|
|
134
|
+
* here too). `published` is the raw published-version string (or null);
|
|
135
|
+
* this function does its own stripping/parsing via `parsePublished`.
|
|
136
|
+
*/
|
|
137
|
+
export function resolveVersion(template: string, published: string | null): ResolveResult {
|
|
138
|
+
const components = parseTemplate(template);
|
|
139
|
+
|
|
140
|
+
if (!components.includes("-")) {
|
|
141
|
+
return { version: components.join("."), rule: "pinned", template, published };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const pub = parsePublished(published);
|
|
145
|
+
|
|
146
|
+
if (pub === null) {
|
|
147
|
+
const resolved = components.map((c) => (c === "-" ? "0" : c));
|
|
148
|
+
return { version: resolved.join("."), rule: "no-published", template, published };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const prefixMatches = components.every((c, i) => c === "-" || Number(c) === pub[i]);
|
|
152
|
+
|
|
153
|
+
if (!prefixMatches) {
|
|
154
|
+
const resolved = components.map((c) => (c === "-" ? "0" : c));
|
|
155
|
+
return { version: resolved.join("."), rule: "prefix-differs", template, published };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
let firstDashSeen = false;
|
|
159
|
+
const resolved = components.map((c, i) => {
|
|
160
|
+
if (c !== "-") return c;
|
|
161
|
+
if (!firstDashSeen) {
|
|
162
|
+
firstDashSeen = true;
|
|
163
|
+
// Guard (LOW-1): `pub[i] + 1` on a published component at
|
|
164
|
+
// Number.MAX_SAFE_INTEGER would silently lose precision to float
|
|
165
|
+
// rounding and could fail to actually increment, violating this
|
|
166
|
+
// seam's "always publishes something new" invariant. Purely
|
|
167
|
+
// theoretical for real-world semver (patch counts in the billions are
|
|
168
|
+
// not realistic) but the arithmetic is load-bearing enough to fail
|
|
169
|
+
// loud rather than silently miscompute.
|
|
170
|
+
if (pub[i] >= Number.MAX_SAFE_INTEGER) {
|
|
171
|
+
throw new Error(
|
|
172
|
+
`version.ts: published component ${pub[i]} at index ${i} is at or beyond Number.MAX_SAFE_INTEGER -- refusing to increment (would lose precision)`
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
return String(pub[i] + 1);
|
|
176
|
+
}
|
|
177
|
+
return "0";
|
|
178
|
+
});
|
|
179
|
+
return { version: resolved.join("."), rule: "prefix-matches", template, published };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Compare two plain, fully-resolved (no `-`, no prerelease) semver-shaped
|
|
184
|
+
* version strings as a numeric 3-tuple. Returns -1/0/1. Throws if either
|
|
185
|
+
* side does not parse as 3 numeric components -- this is for comparing
|
|
186
|
+
* RESOLVED versions, not templates.
|
|
187
|
+
*/
|
|
188
|
+
export function compareVersions(a: string, b: string): -1 | 0 | 1 {
|
|
189
|
+
const pa = parsePublished(a);
|
|
190
|
+
const pb = parsePublished(b);
|
|
191
|
+
if (pa === null) throw new Error(`version.ts: compareVersions() cannot parse ${JSON.stringify(a)}`);
|
|
192
|
+
if (pb === null) throw new Error(`version.ts: compareVersions() cannot parse ${JSON.stringify(b)}`);
|
|
193
|
+
for (let i = 0; i < 3; i++) {
|
|
194
|
+
if (pa[i] < pb[i]) return -1;
|
|
195
|
+
if (pa[i] > pb[i]) return 1;
|
|
196
|
+
}
|
|
197
|
+
return 0;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Read `<repoRootDir>/VERSION`, trimmed. Returns the empty string for an
|
|
202
|
+
* existing-but-blank (or whitespace-only) file, and null ONLY when the file
|
|
203
|
+
* is absent (never throws on either outcome) -- do not treat `=== null` as
|
|
204
|
+
* the sole "no template" signal; an empty string is also "no usable
|
|
205
|
+
* template", and every current caller checks for it via truthiness (LOW-3).
|
|
206
|
+
* Does NOT validate the template shape -- `resolveVersion`/`parseTemplate`
|
|
207
|
+
* do that; this function's only job is the filesystem read.
|
|
208
|
+
*/
|
|
209
|
+
export function readTemplate(repoRootDir: string): string | null {
|
|
210
|
+
const path = join(repoRootDir, "VERSION");
|
|
211
|
+
if (!existsSync(path)) return null;
|
|
212
|
+
try {
|
|
213
|
+
return readFileSync(path, "utf8").trim();
|
|
214
|
+
} catch {
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export interface RuntimeVersionOptions {
|
|
220
|
+
pkgJsonPath?: string;
|
|
221
|
+
/** LAZY -- see this file's header and the precedence note below. Only
|
|
222
|
+
* called when `pkgJsonPath`'s own version is absent or is
|
|
223
|
+
* DEV_PLACEHOLDER, so a published tarball (which has a real version and
|
|
224
|
+
* no repo-root VERSION file) never calls this and never risks the
|
|
225
|
+
* stderr note `repoRoot()` implementations may emit. */
|
|
226
|
+
repoRoot?: () => string | undefined;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* D-4's runtime precedence, synchronous and never throwing:
|
|
231
|
+
*
|
|
232
|
+
* 1. `pkgJsonPath`'s own `.version`, when present and not DEV_PLACEHOLDER
|
|
233
|
+
* -- the published-tarball path: `npm version` already stamped it.
|
|
234
|
+
* 2. Otherwise call `opts.repoRoot?.()` and, if it yields a directory,
|
|
235
|
+
* read that directory's `VERSION` template. A pinned template (no `-`)
|
|
236
|
+
* returns verbatim; a template containing `-` resolves every `-` to 0
|
|
237
|
+
* and appends a `-dev` prerelease tag, so a dev checkout never claims
|
|
238
|
+
* to be a release build.
|
|
239
|
+
* 3. Otherwise DEV_PLACEHOLDER.
|
|
240
|
+
*
|
|
241
|
+
* The whole body is wrapped in try/catch: any unexpected failure (a
|
|
242
|
+
* malformed package.json, a repoRoot() thunk that throws, a malformed
|
|
243
|
+
* VERSION template) degrades to DEV_PLACEHOLDER rather than crashing the
|
|
244
|
+
* caller -- this is the one runtime-facing entry point in this file, and a
|
|
245
|
+
* standalone MCP server must never fail to start over a version string.
|
|
246
|
+
*/
|
|
247
|
+
export function runtimeVersion(opts: RuntimeVersionOptions = {}): string {
|
|
248
|
+
try {
|
|
249
|
+
if (opts.pkgJsonPath) {
|
|
250
|
+
try {
|
|
251
|
+
const raw = readFileSync(opts.pkgJsonPath, "utf8");
|
|
252
|
+
const pkg = JSON.parse(raw) as { version?: unknown };
|
|
253
|
+
if (typeof pkg.version === "string" && pkg.version.length > 0 && pkg.version !== DEV_PLACEHOLDER) {
|
|
254
|
+
return pkg.version;
|
|
255
|
+
}
|
|
256
|
+
} catch {
|
|
257
|
+
// No readable/parseable package.json at pkgJsonPath -- fall through.
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const root = opts.repoRoot?.();
|
|
262
|
+
if (root) {
|
|
263
|
+
const template = readTemplate(root);
|
|
264
|
+
if (template) {
|
|
265
|
+
const components = parseTemplate(template);
|
|
266
|
+
if (!components.includes("-")) {
|
|
267
|
+
return components.join(".");
|
|
268
|
+
}
|
|
269
|
+
const resolved = components.map((c) => (c === "-" ? "0" : c));
|
|
270
|
+
return `${resolved.join(".")}-dev`;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
} catch {
|
|
274
|
+
// Degrade to the placeholder below -- see this function's own doc
|
|
275
|
+
// comment for why nothing here may throw.
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return DEV_PLACEHOLDER;
|
|
279
|
+
}
|
package/vice-proxy.ts
CHANGED
|
@@ -100,6 +100,10 @@ import {
|
|
|
100
100
|
// reconnect ladder.
|
|
101
101
|
import { probeInstance, type ProbeResult } from "./vice-probe.ts";
|
|
102
102
|
import { repoRoot } from "./repo-root.ts";
|
|
103
|
+
// The single version-resolution seam (quick-260819-tsz, D-5) -- PROXY_VERSION
|
|
104
|
+
// below is the only consumer in this file; see version.ts's own header for
|
|
105
|
+
// why this file must never re-derive any part of the algorithm itself.
|
|
106
|
+
import { runtimeVersion } from "./version.ts";
|
|
103
107
|
import { hostPath, SET_ENV_HINT } from "./hostpath.ts";
|
|
104
108
|
// The INVERSE direction (host -> container), for inverting a broker grant's
|
|
105
109
|
// own host-local coordinates before useInstance() ever adopts them (this
|
|
@@ -176,6 +180,11 @@ import { CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
|
176
180
|
// and every call site.
|
|
177
181
|
import * as backendDetect from "./backend-detect.mts";
|
|
178
182
|
import * as stockDispatch from "./stock-dispatch.ts";
|
|
183
|
+
// Plan 08-02: the single per-backend capability lookup (BACK-05), consumed
|
|
184
|
+
// only inside the CallToolRequestSchema override's tools[name] miss branch
|
|
185
|
+
// below, strictly after the DENY_LIST check -- see the comment at that call
|
|
186
|
+
// site for why the ordering is load-bearing.
|
|
187
|
+
import { capabilityRefusalMessage } from "./capability-registry.ts";
|
|
179
188
|
|
|
180
189
|
const HERE_DIR = dirname(fileURLToPath(import.meta.url));
|
|
181
190
|
|
|
@@ -254,8 +263,19 @@ process.stdout.on("error", (err) => {
|
|
|
254
263
|
// override that preserves this file's own `{content, isError}` wire
|
|
255
264
|
// contract exactly (see this plan's "Ground truth" section for why
|
|
256
265
|
// MCPServer's OWN tools/call dispatch cannot be used as-is). PROXY_VERSION
|
|
257
|
-
//
|
|
258
|
-
|
|
266
|
+
// no longer survives as a hand-edited literal (quick-260819-tsz, D-4/D-5):
|
|
267
|
+
// it used to say "0.1.0" while npm's actual `latest` was twelve patches
|
|
268
|
+
// ahead, because nothing updated it. It is now derived through
|
|
269
|
+
// `runtimeVersion()` (the ONE seam, `./version.ts`), which reads this
|
|
270
|
+
// package's own `package.json` first (the published-tarball path, where
|
|
271
|
+
// `npm version` already stamped a real number) and falls back to the
|
|
272
|
+
// repo-root `VERSION` template -- rendered as `<resolved>-dev` -- only in a
|
|
273
|
+
// git checkout, degrading to `0.0.0-dev` if neither is available. Reused,
|
|
274
|
+
// unchanged, as MCPServer's own `version` field below.
|
|
275
|
+
const PROXY_VERSION = runtimeVersion({
|
|
276
|
+
pkgJsonPath: join(HERE_DIR, "package.json"),
|
|
277
|
+
repoRoot: () => repoRoot(),
|
|
278
|
+
});
|
|
259
279
|
|
|
260
280
|
// --------------------------------------------------------------- tools/list
|
|
261
281
|
//
|
|
@@ -3176,7 +3196,11 @@ function buildBackendAwareTool(def: ToolDefinition, forkRun: (args: Record<strin
|
|
|
3176
3196
|
}
|
|
3177
3197
|
|
|
3178
3198
|
const tools: Record<string, ReturnType<typeof buildViceTool>> = {};
|
|
3179
|
-
for
|
|
3199
|
+
// Read ONCE and reused below for both the manifest loop and the two
|
|
3200
|
+
// synthetic registrations' own resolveAdvertisedToolDefinition() calls --
|
|
3201
|
+
// never re-read per registration (WR-07, plan 07-16).
|
|
3202
|
+
const manifestTools = readManifestTools();
|
|
3203
|
+
for (const def of manifestTools) {
|
|
3180
3204
|
if (DENY_LIST.includes(def.name)) continue;
|
|
3181
3205
|
tools[def.name] = buildBackendAwareTool(def, (args) => forwardToVice(def.name, args));
|
|
3182
3206
|
}
|
|
@@ -3187,9 +3211,15 @@ for (const def of readManifestTools()) {
|
|
|
3187
3211
|
tools[RESULT_CONTINUE_TOOL.name] = buildViceTool(RESULT_CONTINUE_TOOL, (args) => Promise.resolve(handleResultContinue(args)));
|
|
3188
3212
|
// Backend-AWARE (CR-07): both of these gather evidence over the fork's HTTP
|
|
3189
3213
|
// transport, so on stock they are refused by name rather than advertised and
|
|
3190
|
-
// then failed at the wire.
|
|
3191
|
-
|
|
3192
|
-
|
|
3214
|
+
// then failed at the wire. WR-07 (plan 07-16): the ADVERTISED definition is
|
|
3215
|
+
// now also backend-aware -- resolveAdvertisedToolDefinition() picks the
|
|
3216
|
+
// corrected stock manifest entry on stock (falling back to the synthetic
|
|
3217
|
+
// definition when the manifest has none) and always returns the synthetic
|
|
3218
|
+
// definition unchanged on the fork, so RECYCLE_TOOL/DIAGNOSE_TOOL's literal
|
|
3219
|
+
// fork-worded text below no longer overwrites the stock manifest's own
|
|
3220
|
+
// entry in `tools/list`.
|
|
3221
|
+
tools[RECYCLE_TOOL.name] = buildBackendAwareTool(stockDispatch.resolveAdvertisedToolDefinition(RECYCLE_TOOL, ACTIVE_BACKEND.backend, manifestTools), (args) => handleRecycle(args));
|
|
3222
|
+
tools[DIAGNOSE_TOOL.name] = buildBackendAwareTool(stockDispatch.resolveAdvertisedToolDefinition(DIAGNOSE_TOOL, ACTIVE_BACKEND.backend, manifestTools), (args) => handleDiagnose(args));
|
|
3193
3223
|
|
|
3194
3224
|
const server = new MCPServer({ name: "vice", version: PROXY_VERSION, tools });
|
|
3195
3225
|
await server.startStdio();
|
|
@@ -3243,6 +3273,19 @@ server.getServer().setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3243
3273
|
// .planning/todos/pending/2026-08-05-generic-surface-deny-list-gap-tools-call-nested-vice-disk-list.md.
|
|
3244
3274
|
const tool = tools[name];
|
|
3245
3275
|
if (!tool || !tool.execute) {
|
|
3276
|
+
// Layer 3 (BACK-05, plan 08-02): fires ONLY when the ACTIVE backend's
|
|
3277
|
+
// trimmed manifest (D-07) never registered this name -- i.e. `tools`
|
|
3278
|
+
// has no key for it -- and MUST stay strictly after the DENY_LIST check
|
|
3279
|
+
// above: those four meta-tool names are a confused-deputy bypass hazard
|
|
3280
|
+
// (01.4-01), not a capability gap, and must never be reachable here.
|
|
3281
|
+
// This lookup renders undefined for a genuinely unknown name (or a
|
|
3282
|
+
// same-backend miss), so a real typo still falls through to the generic
|
|
3283
|
+
// message below unchanged. capability-registry.ts is the ONE place to
|
|
3284
|
+
// edit this data -- never hand-add a per-tool special case here.
|
|
3285
|
+
const capabilityRefusal = capabilityRefusalMessage(name, ACTIVE_BACKEND.backend);
|
|
3286
|
+
if (capabilityRefusal !== undefined) {
|
|
3287
|
+
return { content: [{ type: "text", text: capabilityRefusal }], isError: true };
|
|
3288
|
+
}
|
|
3246
3289
|
return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
|
|
3247
3290
|
}
|
|
3248
3291
|
try {
|