@i-scope/mcp-server 0.4.2
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 +147 -0
- package/LICENSE +21 -0
- package/README.md +373 -0
- package/dist/src/abi-check.d.ts +19 -0
- package/dist/src/abi-check.js +66 -0
- package/dist/src/bridge-driver.d.ts +90 -0
- package/dist/src/bridge-driver.js +290 -0
- package/dist/src/dap-client.d.ts +80 -0
- package/dist/src/dap-client.js +296 -0
- package/dist/src/dap-driver.d.ts +162 -0
- package/dist/src/dap-driver.js +703 -0
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.js +175 -0
- package/dist/src/state.d.ts +86 -0
- package/dist/src/state.js +15 -0
- package/dist/src/tools/abi-check.d.ts +3 -0
- package/dist/src/tools/abi-check.js +64 -0
- package/dist/src/tools/breakpoints.d.ts +3 -0
- package/dist/src/tools/breakpoints.js +56 -0
- package/dist/src/tools/execution.d.ts +3 -0
- package/dist/src/tools/execution.js +75 -0
- package/dist/src/tools/helpers.d.ts +27 -0
- package/dist/src/tools/helpers.js +134 -0
- package/dist/src/tools/inspection.d.ts +3 -0
- package/dist/src/tools/inspection.js +141 -0
- package/dist/src/tools/lifecycle.d.ts +3 -0
- package/dist/src/tools/lifecycle.js +103 -0
- package/dist/src/tools/preflight.d.ts +3 -0
- package/dist/src/tools/preflight.js +95 -0
- package/dist/src/tools/registry.d.ts +15 -0
- package/dist/src/tools/registry.js +19 -0
- package/dist/src/tools/snapshot.d.ts +3 -0
- package/dist/src/tools/snapshot.js +117 -0
- package/dist/src/tools/source-maps.d.ts +3 -0
- package/dist/src/tools/source-maps.js +232 -0
- package/dist/src/tools/sync.d.ts +3 -0
- package/dist/src/tools/sync.js +80 -0
- package/dist/src/tools/ui-modal.d.ts +3 -0
- package/dist/src/tools/ui-modal.js +182 -0
- package/package.json +73 -0
- package/src/abi-check.ts +97 -0
- package/src/bridge-driver.ts +328 -0
- package/src/dap-client.ts +336 -0
- package/src/dap-driver.ts +810 -0
- package/src/index.ts +155 -0
- package/src/state.ts +115 -0
- package/src/tools/abi-check.ts +66 -0
- package/src/tools/breakpoints.ts +59 -0
- package/src/tools/execution.ts +105 -0
- package/src/tools/helpers.ts +142 -0
- package/src/tools/inspection.ts +173 -0
- package/src/tools/lifecycle.ts +129 -0
- package/src/tools/preflight.ts +95 -0
- package/src/tools/registry.ts +34 -0
- package/src/tools/snapshot.ts +132 -0
- package/src/tools/source-maps.ts +222 -0
- package/src/tools/sync.ts +90 -0
- package/src/tools/ui-modal.ts +201 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// debug_stack_trace / debug_scopes / debug_variables / debug_evaluate.
|
|
2
|
+
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import type { DebugProtocol } from '@vscode/debugprotocol';
|
|
5
|
+
|
|
6
|
+
import { fail, ok, shapeFrame } from './helpers.js';
|
|
7
|
+
import type { ToolGroupRegistrar } from './registry.js';
|
|
8
|
+
|
|
9
|
+
const variableShape = z.object({
|
|
10
|
+
name: z.string(),
|
|
11
|
+
type: z.string().optional(),
|
|
12
|
+
value: z.string(),
|
|
13
|
+
variablesReference: z.number(),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export const registerInspectionTools: ToolGroupRegistrar = (server, { dap }) => {
|
|
17
|
+
server.registerTool(
|
|
18
|
+
'debug_stack_trace',
|
|
19
|
+
{
|
|
20
|
+
title: 'Get call stack',
|
|
21
|
+
description:
|
|
22
|
+
'Full JScript call stack at the current pause point. Frames are innermost-first (frames[0] is where execution stopped, frames[last] is the global script body). Each frame.source is `.ts` when source-map mapping succeeded (sourceOrigin: "mapped") or `.ajs` otherwise (sourceOrigin: "generated" or "unmapped-fallback"). State must be "paused".',
|
|
23
|
+
inputSchema: {},
|
|
24
|
+
outputSchema: {
|
|
25
|
+
frames: z.array(z.unknown()),
|
|
26
|
+
},
|
|
27
|
+
annotations: {
|
|
28
|
+
title: 'Get call stack',
|
|
29
|
+
readOnlyHint: true,
|
|
30
|
+
idempotentHint: true,
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
async () => {
|
|
34
|
+
try {
|
|
35
|
+
const body = await dap.stackTrace();
|
|
36
|
+
const frames = await Promise.all(
|
|
37
|
+
(body?.stackFrames ?? []).map(
|
|
38
|
+
async (f: DebugProtocol.StackFrame) => shapeFrame(await dap.classifyFrame(f)),
|
|
39
|
+
),
|
|
40
|
+
);
|
|
41
|
+
return ok({ frames });
|
|
42
|
+
} catch (e) {
|
|
43
|
+
return fail(`debug_stack_trace failed: ${errMsg(e)}`);
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
server.registerTool(
|
|
49
|
+
'debug_scopes',
|
|
50
|
+
{
|
|
51
|
+
title: 'Get scopes for a frame',
|
|
52
|
+
description:
|
|
53
|
+
'Return the variable scopes (Locals, Globals, ...) available for the given frame. Each scope has a `variablesReference` you pass to debug_variables to enumerate its contents. The iScope adapter exposes one "Locals" scope per frame; Globals are surfaced as a special `[Globals]` row inside Locals.',
|
|
54
|
+
inputSchema: {
|
|
55
|
+
frameId: z.number().int().describe(
|
|
56
|
+
'Frame id from debug_stack_trace.frames[].id. Stable only within ONE paused state — invalidated by step / continue / disconnect.',
|
|
57
|
+
),
|
|
58
|
+
},
|
|
59
|
+
outputSchema: {
|
|
60
|
+
scopes: z.array(z.object({
|
|
61
|
+
name: z.string(),
|
|
62
|
+
variablesReference: z.number(),
|
|
63
|
+
expensive: z.boolean().optional(),
|
|
64
|
+
})),
|
|
65
|
+
},
|
|
66
|
+
annotations: {
|
|
67
|
+
title: 'Get scopes for a frame',
|
|
68
|
+
readOnlyHint: true,
|
|
69
|
+
idempotentHint: true,
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
async (args) => {
|
|
73
|
+
try {
|
|
74
|
+
const body = await dap.scopes(args.frameId);
|
|
75
|
+
const scopes = (body?.scopes ?? []).map((s: DebugProtocol.Scope) => ({
|
|
76
|
+
name: s.name,
|
|
77
|
+
variablesReference: s.variablesReference,
|
|
78
|
+
expensive: s.expensive,
|
|
79
|
+
}));
|
|
80
|
+
return ok({ scopes });
|
|
81
|
+
} catch (e) {
|
|
82
|
+
return fail(`debug_scopes failed: ${errMsg(e)}`);
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
server.registerTool(
|
|
88
|
+
'debug_variables',
|
|
89
|
+
{
|
|
90
|
+
title: 'Get variables for a scope or compound',
|
|
91
|
+
description:
|
|
92
|
+
'Enumerate variables under the given `variablesReference`. Pass the reference returned by debug_scopes (top-level) or by a previous debug_variables call on a compound (drill-down). Each child variable with `variablesReference > 0` is itself expandable. Returns name, type (Num/Str/Obj/Arr/...), value (string render) and the reference for further drill-down.',
|
|
93
|
+
inputSchema: {
|
|
94
|
+
variablesReference: z.number().int().positive().describe(
|
|
95
|
+
'Handle from debug_scopes.scopes[].variablesReference or a previous debug_variables result. Invalidated on step / continue / disconnect.',
|
|
96
|
+
),
|
|
97
|
+
},
|
|
98
|
+
outputSchema: {
|
|
99
|
+
variables: z.array(variableShape),
|
|
100
|
+
},
|
|
101
|
+
annotations: {
|
|
102
|
+
title: 'Get variables',
|
|
103
|
+
readOnlyHint: true,
|
|
104
|
+
idempotentHint: true,
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
async (args) => {
|
|
108
|
+
try {
|
|
109
|
+
const body = await dap.variables(args.variablesReference);
|
|
110
|
+
const variables = (body?.variables ?? []).map((v: DebugProtocol.Variable) => ({
|
|
111
|
+
name: v.name,
|
|
112
|
+
type: v.type,
|
|
113
|
+
value: v.value,
|
|
114
|
+
variablesReference: v.variablesReference ?? 0,
|
|
115
|
+
}));
|
|
116
|
+
return ok({ variables });
|
|
117
|
+
} catch (e) {
|
|
118
|
+
return fail(`debug_variables failed: ${errMsg(e)}`);
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
server.registerTool(
|
|
124
|
+
'debug_evaluate',
|
|
125
|
+
{
|
|
126
|
+
title: 'Evaluate an expression',
|
|
127
|
+
description:
|
|
128
|
+
'Evaluate a JScript expression in the context of a paused frame. Context "watch" is for Watch-pane semantics (no side effects); "hover" for tool-tip rendering; "repl" for free-form input. The expression runs through the engine\'s ParceExpressions path — it sees the frame\'s real local scope, so `myVar.field[3]` works as in the editor. Returns the result string and an optional `variablesReference` when the result is compound (drill in via debug_variables).',
|
|
129
|
+
inputSchema: {
|
|
130
|
+
expression: z.string().describe(
|
|
131
|
+
'JScript expression to evaluate. Engine quirks: ES3 only, no arrow funcs / let / const; `JSON.stringify` is polyfilled by the iScope runtime but only inside user scripts — at watch-time we hit the raw engine, so prefer plain property access.',
|
|
132
|
+
),
|
|
133
|
+
frameId: z.number().int().optional().describe(
|
|
134
|
+
'Frame id from debug_stack_trace.frames[].id. Omit to evaluate in the innermost frame (default).',
|
|
135
|
+
),
|
|
136
|
+
context: z.enum(['watch', 'hover', 'repl']).optional().describe(
|
|
137
|
+
'Default "watch".',
|
|
138
|
+
),
|
|
139
|
+
},
|
|
140
|
+
outputSchema: {
|
|
141
|
+
result: z.string(),
|
|
142
|
+
type: z.string().optional(),
|
|
143
|
+
variablesReference: z.number(),
|
|
144
|
+
},
|
|
145
|
+
annotations: {
|
|
146
|
+
title: 'Evaluate expression',
|
|
147
|
+
readOnlyHint: true,
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
async (args) => {
|
|
151
|
+
try {
|
|
152
|
+
const body = await dap.evaluate(
|
|
153
|
+
args.expression,
|
|
154
|
+
args.frameId,
|
|
155
|
+
args.context ?? 'watch',
|
|
156
|
+
);
|
|
157
|
+
return ok({
|
|
158
|
+
result: body?.result ?? '',
|
|
159
|
+
type: body?.type,
|
|
160
|
+
variablesReference: body?.variablesReference ?? 0,
|
|
161
|
+
});
|
|
162
|
+
} catch (e) {
|
|
163
|
+
return fail(`debug_evaluate failed: ${errMsg(e)}`);
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
);
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
function errMsg(e: unknown): string {
|
|
170
|
+
if (e instanceof Error) return e.message;
|
|
171
|
+
if (typeof e === 'string') return e;
|
|
172
|
+
try { return JSON.stringify(e); } catch { return String(e); }
|
|
173
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// debug_launch / debug_disconnect / debug_current_state.
|
|
2
|
+
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
|
|
5
|
+
import { fail, ok, pruneUndefined, shapeStopped, shapeTransition } from './helpers.js';
|
|
6
|
+
import type { ToolGroupRegistrar } from './registry.js';
|
|
7
|
+
|
|
8
|
+
const launchInput = {
|
|
9
|
+
program: z.string().describe(
|
|
10
|
+
'Absolute or workspace-relative path to the script to debug. Accepts .ajs, .apn, .aps (engine-native), OR .ts (auto-resolved to a sibling .ajs). Pass .ts whenever a TS sibling exists — frames will come back in TS with sourceOrigin="mapped". The adapter rejects other extensions before they reach Oscilloscope to avoid a modal "syntax error" wedge.',
|
|
11
|
+
),
|
|
12
|
+
mwfFile: z.string().optional().describe(
|
|
13
|
+
'Override mwf oscillogram path. If omitted, falls back to the `iScopeAjs.mwfPath` setting or env OSCILLOGRAM_MWF. Must point at a real .mwf, otherwise Oscilloscope shows a modal "Please load oscillogram file…" that wedges the COM thread (the helper validates first, but only when this field is non-empty).',
|
|
14
|
+
),
|
|
15
|
+
autoLaunch: z.boolean().optional().describe(
|
|
16
|
+
'Default true. When true and Oscilloscope.exe is not running, the helper launches it. When false the helper attaches to an already-running instance — and will FAIL if none is found.',
|
|
17
|
+
),
|
|
18
|
+
openFileBeforeRun: z.boolean().nullable().optional().describe(
|
|
19
|
+
'Tri-state. true = always OpenFile(mwfFile); false = never OpenFile (use whatever is loaded); null/undefined = smart (OpenFile only if we launched Oscilloscope AND have a path). The `null` default matches the extension behaviour and is usually right.',
|
|
20
|
+
),
|
|
21
|
+
stopOnEntry: z.union([z.boolean(), z.literal('auto')]).optional().describe(
|
|
22
|
+
'Halt at the first executable line. true=always, false=never (default), "auto"=stop only if no breakpoint is set in `program`. Useful for short scripts that finish before you can react.',
|
|
23
|
+
),
|
|
24
|
+
oscilloscopePath: z.string().optional().describe(
|
|
25
|
+
'Override Oscilloscope.exe path. Empty/omitted = helper resolves via registry + default install locations.',
|
|
26
|
+
),
|
|
27
|
+
helperPath: z.string().optional().describe(
|
|
28
|
+
'Override iScopeBridge.exe path. Empty/omitted = bundled helper.',
|
|
29
|
+
),
|
|
30
|
+
outFiles: z.array(z.string()).optional().describe(
|
|
31
|
+
'Glob array of additional .ajs bundles to preload source maps for. `${workspaceFolder}` is substituted. Needed when breakpoints sit in .ts files whose generated .ajs the adapter hasn\'t opened on its own.',
|
|
32
|
+
),
|
|
33
|
+
timeoutMs: z.number().int().positive().optional().describe(
|
|
34
|
+
'Override per-DAP-request timeout for the initial spawn + launch round-trip. Defaults to 30 000 ms.',
|
|
35
|
+
),
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const transitionShape = z.object({
|
|
39
|
+
state: z.enum(['paused', 'running', 'terminated', 'timeout']),
|
|
40
|
+
stopped: z.unknown().optional(),
|
|
41
|
+
exitInfo: z.unknown().optional(),
|
|
42
|
+
waitedMs: z.number().optional(),
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
export const registerLifecycleTools: ToolGroupRegistrar = (server, { dap }) => {
|
|
46
|
+
server.registerTool(
|
|
47
|
+
'debug_launch',
|
|
48
|
+
{
|
|
49
|
+
title: 'Launch debug session',
|
|
50
|
+
description:
|
|
51
|
+
'Spawn the iScope DAP adapter and start `program` under Oscilloscope.exe. Returns the immediate state: "paused" (stopOnEntry hit / startup halt), "running" (script is executing), or "timeout" (running but no event in 1.5 s — call debug_wait_for_paused next). State must be "idle" or "terminated"; call debug_disconnect first if a previous session is still active.',
|
|
52
|
+
inputSchema: launchInput,
|
|
53
|
+
outputSchema: { transition: transitionShape },
|
|
54
|
+
annotations: {
|
|
55
|
+
title: 'Launch debug session',
|
|
56
|
+
idempotentHint: false,
|
|
57
|
+
openWorldHint: true, // spawns processes, touches FS
|
|
58
|
+
destructiveHint: false,
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
async (args) => {
|
|
62
|
+
try {
|
|
63
|
+
const t = await dap.launch(args);
|
|
64
|
+
return ok({ transition: shapeTransition(t) });
|
|
65
|
+
} catch (e) {
|
|
66
|
+
return fail(`debug_launch failed: ${errMsg(e)}`);
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
server.registerTool(
|
|
72
|
+
'debug_disconnect',
|
|
73
|
+
{
|
|
74
|
+
title: 'Disconnect debug session',
|
|
75
|
+
description:
|
|
76
|
+
'Cleanly tear down the active debug session: DAP disconnectRequest → helper shutdown with closePolicy=ifWeLaunched → Oscilloscope graceful close (only if we launched it). Always call this before exiting your AI workflow — leaking a session orphans Oscilloscope when this server exits.',
|
|
77
|
+
inputSchema: {},
|
|
78
|
+
outputSchema: { state: z.string() },
|
|
79
|
+
annotations: {
|
|
80
|
+
title: 'Disconnect debug session',
|
|
81
|
+
idempotentHint: true,
|
|
82
|
+
destructiveHint: false,
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
async () => {
|
|
86
|
+
try {
|
|
87
|
+
await dap.disconnect();
|
|
88
|
+
return ok({ state: dap.getState() });
|
|
89
|
+
} catch (e) {
|
|
90
|
+
return fail(`debug_disconnect failed: ${errMsg(e)}`);
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
server.registerTool(
|
|
96
|
+
'debug_current_state',
|
|
97
|
+
{
|
|
98
|
+
title: 'Inspect current debug state',
|
|
99
|
+
description:
|
|
100
|
+
'Lock-free snapshot of where the session is right now. Returns state (idle/launching/running/paused/terminated). For state="paused" also returns the StoppedInfo (reason, optional exception description, frames[]). Cheap; safe to call any time without waiting on anything.',
|
|
101
|
+
inputSchema: {},
|
|
102
|
+
outputSchema: {
|
|
103
|
+
state: z.enum(['idle', 'launching', 'running', 'paused', 'terminated']),
|
|
104
|
+
stopped: z.unknown().optional(),
|
|
105
|
+
terminationInfo: z.unknown().optional(),
|
|
106
|
+
},
|
|
107
|
+
annotations: {
|
|
108
|
+
title: 'Inspect current debug state',
|
|
109
|
+
readOnlyHint: true,
|
|
110
|
+
idempotentHint: true,
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
async () => {
|
|
114
|
+
const state = dap.getState();
|
|
115
|
+
const stopped = dap.getStoppedSnapshot();
|
|
116
|
+
const term = dap.getTerminationInfo();
|
|
117
|
+
const payload: Record<string, unknown> = { state };
|
|
118
|
+
if (stopped) payload['stopped'] = shapeStopped(stopped);
|
|
119
|
+
if (term) payload['terminationInfo'] = pruneUndefined({ reason: term.reason });
|
|
120
|
+
return ok(payload);
|
|
121
|
+
},
|
|
122
|
+
);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
function errMsg(e: unknown): string {
|
|
126
|
+
if (e instanceof Error) return e.message;
|
|
127
|
+
if (typeof e === 'string') return e;
|
|
128
|
+
try { return JSON.stringify(e); } catch { return String(e); }
|
|
129
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// system_preflight_check — proactive environment diagnostic for the
|
|
2
|
+
// iScope debug stack.
|
|
3
|
+
//
|
|
4
|
+
// The tool wraps the `preflight/check` JSON-RPC method of
|
|
5
|
+
// `iScopeBridge.exe` (helper-cpp). It performs a registry-only probe
|
|
6
|
+
// of:
|
|
7
|
+
// 1. The Windows Script Debugger (Machine Debug Manager, pdm.dll)
|
|
8
|
+
// that Oscilloscope's JScript debug engine builds upon. When
|
|
9
|
+
// this CLSID is missing, Oscilloscope shows the "Unable to
|
|
10
|
+
// create debuger object!" dialog at first `executeFile` and
|
|
11
|
+
// silently declines to run scripts in debug mode.
|
|
12
|
+
// 2. The Oscilloscope ScopeAppDbgCtrl CLSID — a side-check that
|
|
13
|
+
// lets an AI agent distinguish "Script Debugger missing" from
|
|
14
|
+
// "Oscilloscope not installed" failure modes.
|
|
15
|
+
//
|
|
16
|
+
// When an AI should call this tool:
|
|
17
|
+
// - Proactively on the first `debug_launch` failure that the user
|
|
18
|
+
// describes as "BP ignored" / "Oscilloscope says 'Unable to
|
|
19
|
+
// create debuger object'" / "F5 starts then nothing happens".
|
|
20
|
+
// - Whenever a launch fails with a generic "engine returned but
|
|
21
|
+
// emitted no sink events" symptom in the Debug Console.
|
|
22
|
+
//
|
|
23
|
+
// The tool does NOT require an active debug session, does NOT require
|
|
24
|
+
// Oscilloscope to be running, and is idempotent / read-only.
|
|
25
|
+
|
|
26
|
+
import { z } from 'zod';
|
|
27
|
+
|
|
28
|
+
import { fail, ok } from './helpers.js';
|
|
29
|
+
import type { ToolGroupRegistrar } from './registry.js';
|
|
30
|
+
|
|
31
|
+
const scriptDebuggerShape = z.object({
|
|
32
|
+
registered: z.boolean(),
|
|
33
|
+
registryClsid: z.string(),
|
|
34
|
+
registryName: z.string(),
|
|
35
|
+
inprocServerPath: z.string(),
|
|
36
|
+
fileExists: z.boolean().nullable(),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const oscilloscopeShape = z.object({
|
|
40
|
+
comRegistered: z.boolean(),
|
|
41
|
+
registryClsid: z.string(),
|
|
42
|
+
registryName: z.string(),
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
export const registerPreflightTools: ToolGroupRegistrar = (server, { bridge }) => {
|
|
46
|
+
|
|
47
|
+
server.registerTool(
|
|
48
|
+
'system_preflight_check',
|
|
49
|
+
{
|
|
50
|
+
title: 'Check iScope debug prerequisites',
|
|
51
|
+
description:
|
|
52
|
+
'Probe the local machine for the iScope debug stack prerequisites: the Windows Script ' +
|
|
53
|
+
'Debugger (Machine Debug Manager, pdm.dll — required by Oscilloscope to expose JScript ' +
|
|
54
|
+
'debugging) and the Oscilloscope ScopeAppDbgCtrl COM registration. Call this proactively ' +
|
|
55
|
+
'when the user reports "breakpoints are ignored", "F5 just sits there", or whenever ' +
|
|
56
|
+
'debug_launch / debug_continue silently fails — the most common root cause is a missing ' +
|
|
57
|
+
'Machine Debug Manager, which the user can fix by installing Build Tools / Remote Tools ' +
|
|
58
|
+
'for Visual Studio. Registry-only; safe to call at any time, never auto-launches anything.',
|
|
59
|
+
inputSchema: {},
|
|
60
|
+
outputSchema: {
|
|
61
|
+
ok: z.boolean(),
|
|
62
|
+
verdict: z.enum([
|
|
63
|
+
'ok',
|
|
64
|
+
'script-debugger-not-registered',
|
|
65
|
+
'script-debugger-file-missing',
|
|
66
|
+
'unknown',
|
|
67
|
+
]),
|
|
68
|
+
scriptDebugger: scriptDebuggerShape,
|
|
69
|
+
oscilloscope: oscilloscopeShape,
|
|
70
|
+
userHints: z.array(z.string()),
|
|
71
|
+
deprecatedAdvice: z.array(z.string()),
|
|
72
|
+
downloadUrl: z.string().optional(),
|
|
73
|
+
},
|
|
74
|
+
annotations: {
|
|
75
|
+
title: 'Check iScope debug prerequisites',
|
|
76
|
+
readOnlyHint: true,
|
|
77
|
+
idempotentHint: true,
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
async () => {
|
|
81
|
+
try {
|
|
82
|
+
const result = await bridge.preflightCheck();
|
|
83
|
+
return ok(result as unknown as Record<string, unknown>);
|
|
84
|
+
} catch (e) {
|
|
85
|
+
return fail(`system_preflight_check failed: ${errMsg(e)}`);
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
function errMsg(e: unknown): string {
|
|
92
|
+
if (e instanceof Error) return e.message;
|
|
93
|
+
if (typeof e === 'string') return e;
|
|
94
|
+
try { return JSON.stringify(e); } catch { return String(e); }
|
|
95
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Tool registry pattern.
|
|
2
|
+
//
|
|
3
|
+
// Why this exists:
|
|
4
|
+
// `index.ts` should be a thin shell — `new McpServer` + transport
|
|
5
|
+
// + a sequence of `registerXxxTools()` calls. Each tool group
|
|
6
|
+
// (lifecycle, breakpoints, execution, sync, inspection, snapshot,
|
|
7
|
+
// source-maps) owns its own file and exports a single `register`
|
|
8
|
+
// function that registers every tool in the group against the
|
|
9
|
+
// given server instance.
|
|
10
|
+
//
|
|
11
|
+
// This pattern serves two future goals:
|
|
12
|
+
// - Phase 2/3 will add control_* and data_* groups; the
|
|
13
|
+
// registration code in index.ts grows by one line per group.
|
|
14
|
+
// - Composing only a subset of groups for tests (e.g. inspection
|
|
15
|
+
// tools alone) becomes trivial — instantiate the driver and
|
|
16
|
+
// call only the groups you want.
|
|
17
|
+
|
|
18
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
19
|
+
|
|
20
|
+
import type { BridgeDriver } from '../bridge-driver.js';
|
|
21
|
+
import type { DapDriver } from '../dap-driver.js';
|
|
22
|
+
|
|
23
|
+
/** Drivers passed to every tool-group registrar. Phase 1 tools touch
|
|
24
|
+
* only `dap`; Phase 2/3 tools will use `bridge` as well. */
|
|
25
|
+
export interface ToolDrivers {
|
|
26
|
+
dap: DapDriver;
|
|
27
|
+
bridge: BridgeDriver;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Uniform shape every tool-group file must export.
|
|
31
|
+
*
|
|
32
|
+
* Registrars MUST NOT call `server.connect()` themselves — that is
|
|
33
|
+
* the responsibility of `index.ts` after every group has registered. */
|
|
34
|
+
export type ToolGroupRegistrar = (server: McpServer, drivers: ToolDrivers) => void;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// debug_snapshot — composite "give me everything" tool.
|
|
2
|
+
//
|
|
3
|
+
// Why a composite tool when the AI could call stack_trace / scopes /
|
|
4
|
+
// variables individually:
|
|
5
|
+
// - One round-trip = lower latency on stdio MCP transport.
|
|
6
|
+
// - Atomic: the whole snapshot is taken under a SINGLE mutex
|
|
7
|
+
// acquisition, so no risk of step happening between the
|
|
8
|
+
// stack_trace and the variables fetch and yielding mismatched
|
|
9
|
+
// frame ids.
|
|
10
|
+
// - Mirrors the existing `scripts/inspect.cjs` headless tool, just
|
|
11
|
+
// surfaced over MCP — easy mental model.
|
|
12
|
+
|
|
13
|
+
import { z } from 'zod';
|
|
14
|
+
|
|
15
|
+
import { fail, ok, pruneUndefined, shapeFrame } from './helpers.js';
|
|
16
|
+
import type { ToolGroupRegistrar } from './registry.js';
|
|
17
|
+
import type { DapDriver } from '../dap-driver.js';
|
|
18
|
+
|
|
19
|
+
const variableShape = z.object({
|
|
20
|
+
name: z.string(),
|
|
21
|
+
type: z.string().optional(),
|
|
22
|
+
value: z.string(),
|
|
23
|
+
variablesReference: z.number(),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const scopeShape = z.object({
|
|
27
|
+
name: z.string(),
|
|
28
|
+
variablesReference: z.number(),
|
|
29
|
+
expensive: z.boolean().optional(),
|
|
30
|
+
variables: z.array(variableShape).optional(),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const frameSnapshotShape = z.object({
|
|
34
|
+
frame: z.unknown(),
|
|
35
|
+
scopes: z.array(scopeShape),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
export const registerSnapshotTools: ToolGroupRegistrar = (server, { dap }) => {
|
|
39
|
+
server.registerTool(
|
|
40
|
+
'debug_snapshot',
|
|
41
|
+
{
|
|
42
|
+
title: 'Snapshot full debug state',
|
|
43
|
+
description:
|
|
44
|
+
'Collect stack trace + scopes + (optionally) one level of variables for every frame in one call. Equivalent to calling debug_stack_trace, then debug_scopes for every frame, then debug_variables for every scope — but atomically under one mutex acquisition. Use this when you want a "where am I and what do the locals look like?" answer in a single round-trip. State must be "paused".',
|
|
45
|
+
inputSchema: {
|
|
46
|
+
expandScopes: z.boolean().optional().describe(
|
|
47
|
+
'Default true. When true, expand each scope\'s top-level variables; nested compounds are NOT recursed (you still need debug_variables for drill-down). When false, only scope handles are returned.',
|
|
48
|
+
),
|
|
49
|
+
frameLimit: z.number().int().positive().optional().describe(
|
|
50
|
+
'Cap the number of frames returned. Defaults to 20. Innermost frames are kept; outer frames trimmed.',
|
|
51
|
+
),
|
|
52
|
+
},
|
|
53
|
+
outputSchema: {
|
|
54
|
+
state: z.literal('paused'),
|
|
55
|
+
reason: z.string(),
|
|
56
|
+
description: z.string().optional(),
|
|
57
|
+
frames: z.array(frameSnapshotShape),
|
|
58
|
+
},
|
|
59
|
+
annotations: {
|
|
60
|
+
title: 'Snapshot full debug state',
|
|
61
|
+
readOnlyHint: true,
|
|
62
|
+
idempotentHint: true,
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
async (args) => {
|
|
66
|
+
try {
|
|
67
|
+
const expand = args.expandScopes !== false;
|
|
68
|
+
const limit = args.frameLimit ?? 20;
|
|
69
|
+
const result = await snapshot(dap, expand, limit);
|
|
70
|
+
return ok(result);
|
|
71
|
+
} catch (e) {
|
|
72
|
+
return fail(`debug_snapshot failed: ${errMsg(e)}`);
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
async function snapshot(
|
|
79
|
+
dap: DapDriver,
|
|
80
|
+
expandScopes: boolean,
|
|
81
|
+
frameLimit: number,
|
|
82
|
+
): Promise<Record<string, unknown>> {
|
|
83
|
+
if (dap.getState() !== 'paused') {
|
|
84
|
+
throw new Error(`debug_snapshot requires 'paused' state, got '${dap.getState()}'`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const stackBody = await dap.stackTrace();
|
|
88
|
+
const allFrames = stackBody?.stackFrames ?? [];
|
|
89
|
+
const slice = allFrames.slice(0, frameLimit);
|
|
90
|
+
const frames: Record<string, unknown>[] = [];
|
|
91
|
+
|
|
92
|
+
for (const f of slice) {
|
|
93
|
+
const classified = await dap.classifyFrame(f);
|
|
94
|
+
const scopesBody = await dap.scopes(f.id);
|
|
95
|
+
const scopesRaw = scopesBody?.scopes ?? [];
|
|
96
|
+
|
|
97
|
+
const scopes: Record<string, unknown>[] = [];
|
|
98
|
+
for (const s of scopesRaw) {
|
|
99
|
+
const scope: Record<string, unknown> = pruneUndefined({
|
|
100
|
+
name: s.name,
|
|
101
|
+
variablesReference: s.variablesReference,
|
|
102
|
+
expensive: s.expensive,
|
|
103
|
+
});
|
|
104
|
+
if (expandScopes && s.variablesReference > 0) {
|
|
105
|
+
const vbody = await dap.variables(s.variablesReference);
|
|
106
|
+
scope['variables'] = (vbody?.variables ?? []).map((v) => ({
|
|
107
|
+
name: v.name,
|
|
108
|
+
type: v.type,
|
|
109
|
+
value: v.value,
|
|
110
|
+
variablesReference: v.variablesReference ?? 0,
|
|
111
|
+
}));
|
|
112
|
+
}
|
|
113
|
+
scopes.push(scope);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
frames.push({ frame: shapeFrame(classified), scopes });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const stopped = dap.getStoppedSnapshot();
|
|
120
|
+
return pruneUndefined({
|
|
121
|
+
state: 'paused' as const,
|
|
122
|
+
reason: stopped?.reason ?? 'unknown',
|
|
123
|
+
description: stopped?.description,
|
|
124
|
+
frames,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function errMsg(e: unknown): string {
|
|
129
|
+
if (e instanceof Error) return e.message;
|
|
130
|
+
if (typeof e === 'string') return e;
|
|
131
|
+
try { return JSON.stringify(e); } catch { return String(e); }
|
|
132
|
+
}
|