@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.
Files changed (58) hide show
  1. package/CHANGELOG.md +147 -0
  2. package/LICENSE +21 -0
  3. package/README.md +373 -0
  4. package/dist/src/abi-check.d.ts +19 -0
  5. package/dist/src/abi-check.js +66 -0
  6. package/dist/src/bridge-driver.d.ts +90 -0
  7. package/dist/src/bridge-driver.js +290 -0
  8. package/dist/src/dap-client.d.ts +80 -0
  9. package/dist/src/dap-client.js +296 -0
  10. package/dist/src/dap-driver.d.ts +162 -0
  11. package/dist/src/dap-driver.js +703 -0
  12. package/dist/src/index.d.ts +3 -0
  13. package/dist/src/index.js +175 -0
  14. package/dist/src/state.d.ts +86 -0
  15. package/dist/src/state.js +15 -0
  16. package/dist/src/tools/abi-check.d.ts +3 -0
  17. package/dist/src/tools/abi-check.js +64 -0
  18. package/dist/src/tools/breakpoints.d.ts +3 -0
  19. package/dist/src/tools/breakpoints.js +56 -0
  20. package/dist/src/tools/execution.d.ts +3 -0
  21. package/dist/src/tools/execution.js +75 -0
  22. package/dist/src/tools/helpers.d.ts +27 -0
  23. package/dist/src/tools/helpers.js +134 -0
  24. package/dist/src/tools/inspection.d.ts +3 -0
  25. package/dist/src/tools/inspection.js +141 -0
  26. package/dist/src/tools/lifecycle.d.ts +3 -0
  27. package/dist/src/tools/lifecycle.js +103 -0
  28. package/dist/src/tools/preflight.d.ts +3 -0
  29. package/dist/src/tools/preflight.js +95 -0
  30. package/dist/src/tools/registry.d.ts +15 -0
  31. package/dist/src/tools/registry.js +19 -0
  32. package/dist/src/tools/snapshot.d.ts +3 -0
  33. package/dist/src/tools/snapshot.js +117 -0
  34. package/dist/src/tools/source-maps.d.ts +3 -0
  35. package/dist/src/tools/source-maps.js +232 -0
  36. package/dist/src/tools/sync.d.ts +3 -0
  37. package/dist/src/tools/sync.js +80 -0
  38. package/dist/src/tools/ui-modal.d.ts +3 -0
  39. package/dist/src/tools/ui-modal.js +182 -0
  40. package/package.json +73 -0
  41. package/src/abi-check.ts +97 -0
  42. package/src/bridge-driver.ts +328 -0
  43. package/src/dap-client.ts +336 -0
  44. package/src/dap-driver.ts +810 -0
  45. package/src/index.ts +155 -0
  46. package/src/state.ts +115 -0
  47. package/src/tools/abi-check.ts +66 -0
  48. package/src/tools/breakpoints.ts +59 -0
  49. package/src/tools/execution.ts +105 -0
  50. package/src/tools/helpers.ts +142 -0
  51. package/src/tools/inspection.ts +173 -0
  52. package/src/tools/lifecycle.ts +129 -0
  53. package/src/tools/preflight.ts +95 -0
  54. package/src/tools/registry.ts +34 -0
  55. package/src/tools/snapshot.ts +132 -0
  56. package/src/tools/source-maps.ts +222 -0
  57. package/src/tools/sync.ts +90 -0
  58. package/src/tools/ui-modal.ts +201 -0
package/src/index.ts ADDED
@@ -0,0 +1,155 @@
1
+ #!/usr/bin/env node
2
+ // @i-scope/mcp-server entry point.
3
+ //
4
+ // Wires:
5
+ // StdioServerTransport (stdin/stdout pipes to the MCP client, e.g.
6
+ // Cursor / Claude Desktop) → McpServer → tool registry → DapDriver
7
+ // (spawns the `server.js` shipped inside `@i-scope/dap-adapter` as
8
+ // the DAP child process; no path arithmetic required).
9
+ //
10
+ // Tool groups (each in its own file under ./tools):
11
+ // - lifecycle: debug_launch / debug_disconnect / debug_current_state
12
+ // - breakpoints: debug_set_breakpoints
13
+ // - execution: debug_continue / debug_step_over|into|out
14
+ // - sync: debug_wait_for_paused / debug_read_output
15
+ // - inspection: debug_stack_trace / debug_scopes / debug_variables / debug_evaluate
16
+ // - snapshot: debug_snapshot (composite — equivalent to inspect.cjs)
17
+ // - source-maps: debug_resolve_source (no debug session needed)
18
+ //
19
+ // Cursor wiring (`.cursor/mcp.json`):
20
+ // {
21
+ // "mcpServers": {
22
+ // "iscope-debugger": {
23
+ // "command": "npx",
24
+ // "args": ["-y", "@i-scope/mcp-server"]
25
+ // }
26
+ // }
27
+ // }
28
+ // No ENV variables required — everything is resolved through the
29
+ // installed npm packages (`@i-scope/dap-adapter` →
30
+ // `@i-scope/iscope-bridge-client` → `@i-scope/iscope-bridge`).
31
+ // For monorepo development use the dist path directly:
32
+ // `command: "node", args: ["E:/.../packages/mcp-server/dist/src/index.js"]`.
33
+ //
34
+ // Stdout discipline:
35
+ // StdioServerTransport reads the JSON-RPC wire from stdin and
36
+ // writes it to stdout. ANY accidental console.log poisons the
37
+ // stream. We force all of our diagnostics through stderr and we
38
+ // monkey-patch console.log → console.error in this file so even a
39
+ // stray dependency log lands on the safe channel.
40
+
41
+ import * as os from 'node:os';
42
+
43
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
44
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
45
+
46
+ import { BridgeDriver } from './bridge-driver.js';
47
+ import { DapDriver } from './dap-driver.js';
48
+ import { registerBreakpointTools } from './tools/breakpoints.js';
49
+ import { registerExecutionTools } from './tools/execution.js';
50
+ import { SERVER_INFO, SERVER_INSTRUCTIONS } from './tools/helpers.js';
51
+ import { registerInspectionTools } from './tools/inspection.js';
52
+ import { registerLifecycleTools } from './tools/lifecycle.js';
53
+ import { registerAbiCheckTools } from './tools/abi-check.js';
54
+ import { registerPreflightTools } from './tools/preflight.js';
55
+ import type { ToolDrivers } from './tools/registry.js';
56
+ import { registerSnapshotTools } from './tools/snapshot.js';
57
+ import { registerSourceMapTools } from './tools/source-maps.js';
58
+ import { registerSyncTools } from './tools/sync.js';
59
+ import { registerUIModalTools } from './tools/ui-modal.js';
60
+
61
+ function silenceStdout(): void {
62
+ // Reroute anything that wants to write to stdout (rogue
63
+ // dependency console.log, etc.) onto stderr. The MCP transport
64
+ // owns stdout.
65
+ /* eslint-disable no-console */
66
+ const origLog = console.log.bind(console);
67
+ void origLog;
68
+ console.log = ((...args: unknown[]) => console.error('[stdout-redirect]', ...args)) as typeof console.log;
69
+ console.info = ((...args: unknown[]) => console.error('[stdout-redirect]', ...args)) as typeof console.info;
70
+ /* eslint-enable no-console */
71
+ }
72
+
73
+ async function main(): Promise<void> {
74
+ silenceStdout();
75
+
76
+ // `ISCOPE_EXTENSION_PATH` is optional now — it only acts as a
77
+ // `bundledRoot` override forwarded to the spawned DAP server.
78
+ // The default deployment installs `@i-scope/dap-adapter` and
79
+ // `@i-scope/iscope-bridge` from npm and needs no env at all.
80
+ const extensionPath = process.env['ISCOPE_EXTENSION_PATH'] || undefined;
81
+
82
+ process.stderr.write(
83
+ `[@i-scope/mcp-server v${SERVER_INFO.version}] starting on ${os.platform()} ${os.arch()}` +
84
+ (extensionPath ? `; ISCOPE_EXTENSION_PATH=${extensionPath}` : '') +
85
+ `\n`,
86
+ );
87
+
88
+ const dap = new DapDriver({
89
+ extensionPath,
90
+ log: (line) => process.stderr.write(line + '\n'),
91
+ });
92
+ const bridge = new BridgeDriver({
93
+ bundledRoot: extensionPath,
94
+ log: (line) => process.stderr.write(line + '\n'),
95
+ });
96
+ const drivers: ToolDrivers = { dap, bridge };
97
+
98
+ const server = new McpServer(SERVER_INFO, {
99
+ instructions: SERVER_INSTRUCTIONS,
100
+ capabilities: {
101
+ tools: { listChanged: false },
102
+ logging: {},
103
+ },
104
+ });
105
+
106
+ registerLifecycleTools(server, drivers);
107
+ registerBreakpointTools(server, drivers);
108
+ registerExecutionTools(server, drivers);
109
+ registerSyncTools(server, drivers);
110
+ registerInspectionTools(server, drivers);
111
+ registerSnapshotTools(server, drivers);
112
+ registerSourceMapTools(server, drivers);
113
+ registerUIModalTools(server, drivers);
114
+ registerPreflightTools(server, drivers);
115
+ registerAbiCheckTools(server, drivers);
116
+
117
+ const transport = new StdioServerTransport();
118
+
119
+ // Graceful cleanup so we don't orphan Oscilloscope on ^C / IDE
120
+ // restart. The MCP transport closes naturally when stdin EOFs,
121
+ // but we still need to tear down the DAP child explicitly.
122
+ let shuttingDown = false;
123
+ const shutdown = (signal: string): void => {
124
+ if (shuttingDown) return;
125
+ shuttingDown = true;
126
+ process.stderr.write(`[@i-scope/mcp-server] ${signal} received, shutting down\n`);
127
+ // Fire-and-forget: best-effort disconnect, then exit. Don't
128
+ // await — node's signal handler should be quick.
129
+ Promise.allSettled([
130
+ dap.disconnect(),
131
+ bridge.disconnect(),
132
+ ]).then((results) => {
133
+ for (const r of results) {
134
+ if (r.status === 'rejected')
135
+ process.stderr.write(`[@i-scope/mcp-server] disconnect error: ${r.reason}\n`);
136
+ }
137
+ process.exit(0);
138
+ });
139
+ };
140
+ process.on('SIGINT', () => shutdown('SIGINT'));
141
+ process.on('SIGTERM', () => shutdown('SIGTERM'));
142
+ transport.onclose = (): void => {
143
+ // Client disconnected (Cursor restarted, user removed the
144
+ // server from .cursor/mcp.json, ...). Same cleanup path.
145
+ shutdown('transport-close');
146
+ };
147
+
148
+ await server.connect(transport);
149
+ process.stderr.write('[@i-scope/mcp-server] ready; awaiting MCP requests on stdio\n');
150
+ }
151
+
152
+ main().catch((err) => {
153
+ process.stderr.write(`[@i-scope/mcp-server] fatal: ${err instanceof Error ? err.stack ?? err.message : String(err)}\n`);
154
+ process.exit(1);
155
+ });
package/src/state.ts ADDED
@@ -0,0 +1,115 @@
1
+ // Shared types for the MCP server.
2
+ //
3
+ // Why these live in a dedicated module:
4
+ // - DapDriver, every tool group, and (Phase 2/3) BridgeDriver all
5
+ // need to talk about the same notions: "paused state", "frame",
6
+ // "output entry". Without a single source of truth the field names
7
+ // drift between tools and the AI gets a confusing tool-result
8
+ // surface.
9
+ // - Tool input/output Zod schemas live next to each tool's
10
+ // registration; THESE types are the runtime data exchanged between
11
+ // the driver layer and the tool layer (post-validation). The Zod
12
+ // side is presentation; this side is implementation.
13
+
14
+ /** High-level lifecycle of a single MCP debug session. */
15
+ export type SessionState =
16
+ | 'idle' // No DAP child spawned. Initial / post-disconnect.
17
+ | 'launching' // DAP child spawned, launchRequest in flight.
18
+ | 'running' // Script is running, no break / pause.
19
+ | 'paused' // Engine stopped on a breakpoint / step / error.
20
+ | 'terminated'; // Script finished or crashed; child still around
21
+ // until disconnect tool cleans up.
22
+
23
+ /** DAP `StoppedEvent.reason` values we care about — narrowed to the
24
+ * subset the iScope adapter actually emits. The MCP layer keeps the
25
+ * raw DAP string verbatim so AI doesn't lose nuance. */
26
+ export type StoppedReason =
27
+ | 'breakpoint'
28
+ | 'step'
29
+ | 'exception'
30
+ | 'entry'
31
+ | 'pause'
32
+ | 'goto'
33
+ | string; // forwards-compat catch-all
34
+
35
+ /** Origin of the `source` path returned in a frame.
36
+ *
37
+ * AI uses this to detect "source map gap" without parsing
38
+ * configuration. `unmapped-fallback` is the loudest signal — it means
39
+ * the user launched against `.ts` but for THIS specific line the
40
+ * bundler did not emit a mapping, so we surface the generated `.ajs`
41
+ * position instead of pretending we know the original. */
42
+ export type SourceOrigin =
43
+ | 'mapped' // .ts surfaced through source map
44
+ | 'generated' // raw .ajs (user launched against .ajs)
45
+ | 'unmapped-fallback'; // .ts launch but no mapping for this line
46
+
47
+ /** One stack frame, post-translation. Mirrors DAP `StackFrame` plus
48
+ * the source-map provenance fields. */
49
+ export interface FrameInfo {
50
+ /** DAP frame id. Stable across `stack_trace` calls within ONE
51
+ * paused state; invalidated on resume/step/terminate. */
52
+ id: number;
53
+ /** Frame name from the engine (`probe`, `Host script block`, ...). */
54
+ name: string;
55
+ /** Source path the user would recognise — `.ts` if we mapped,
56
+ * `.ajs` otherwise. */
57
+ source: string;
58
+ /** Provenance of `source` (see {@link SourceOrigin}). */
59
+ sourceOrigin: SourceOrigin;
60
+ /** 1-based start line of the active statement in `source`. */
61
+ line: number;
62
+ /** 1-based start column in `source`. */
63
+ column: number;
64
+ /** 1-based end line for statement highlight; may equal `line`. */
65
+ endLine?: number;
66
+ /** 1-based end column (exclusive of trailing `;`). */
67
+ endColumn?: number;
68
+ /** Always populated when an `.ajs` exists, even for mapped
69
+ * frames — useful when the AI wants to cross-reference with raw
70
+ * engine offsets / engine logs. */
71
+ generatedSource?: string;
72
+ /** 1-based line in the generated `.ajs` regardless of mapping. */
73
+ generatedLine?: number;
74
+ /** 1-based column in the generated `.ajs`. */
75
+ generatedColumn?: number;
76
+ }
77
+
78
+ /** Snapshot of "where execution is right now" produced after every
79
+ * state transition into `paused`. */
80
+ export interface StoppedInfo {
81
+ reason: StoppedReason;
82
+ /** Innermost-frame error description when reason==='exception'. */
83
+ description?: string;
84
+ /** Full call stack, innermost first. */
85
+ frames: FrameInfo[];
86
+ }
87
+
88
+ /** One buffered `OutputEvent`. Cursor pagination uses `id` (monotonic
89
+ * insertion order). */
90
+ export interface OutputEntry {
91
+ /** Monotonic, gapless integer. Starts at 1. AI passes the highest
92
+ * seen `id` back as `cursor` to read the tail. */
93
+ id: number;
94
+ /** DAP category: `stdout` / `stderr` / `console` / `important` ... */
95
+ category: string;
96
+ /** Raw text from the engine / adapter. May or may not end in `\n`
97
+ * — we do not normalise. */
98
+ text: string;
99
+ /** Unix epoch ms when this MCP server received the event. */
100
+ timestamp: number;
101
+ }
102
+
103
+ /** Result returned by execution-control / sync tools after waiting on
104
+ * the next state transition. Either "we paused" (with details) or
105
+ * "the script ended" (`terminated: true`) — never both. */
106
+ export type ExecutionTransition =
107
+ | { state: 'paused'; stopped: StoppedInfo }
108
+ | { state: 'terminated'; exitInfo?: TerminationInfo }
109
+ | { state: 'timeout'; waitedMs: number };
110
+
111
+ export interface TerminationInfo {
112
+ /** Best-effort reason from the adapter / helper. Empty when the
113
+ * script ended cleanly via `DBGEID_STOP`. */
114
+ reason?: string;
115
+ }
@@ -0,0 +1,66 @@
1
+ // system_abi_check — wire ABI probe via `protocol/version` (no COM).
2
+
3
+ import { z } from 'zod';
4
+
5
+ import { fail, ok } from './helpers.js';
6
+ import type { ToolGroupRegistrar } from './registry.js';
7
+
8
+ const policySchema = z.enum(['strict', 'warn', 'ignore']).optional();
9
+
10
+ const warningShape = z.object({
11
+ clientAbi: z.string(),
12
+ serverAbi: z.string(),
13
+ reason: z.string(),
14
+ });
15
+
16
+ export const registerAbiCheckTools: ToolGroupRegistrar = (server, { bridge }) => {
17
+
18
+ server.registerTool(
19
+ 'system_abi_check',
20
+ {
21
+ title: 'Check iScopeBridge wire ABI',
22
+ description:
23
+ 'Probe the bundled iScopeBridge.exe JSON-RPC wire ABI via `protocol/version` ' +
24
+ '(no Oscilloscope, no COM, no debug session). Returns client vs server semver, ' +
25
+ 'compatibility kind (ok / warn / fatal), and actionable hints when the helper ' +
26
+ 'binary is older or newer than @i-scope/iscope-bridge-client expects. Call before ' +
27
+ 'debug_launch when upgrading packages or after copying a custom helper binary.',
28
+ inputSchema: {
29
+ policy: policySchema.describe(
30
+ 'Semver policy: strict (default) — fatal on MAJOR mismatch; warn — emit warnings only; ignore — skip checks.',
31
+ ),
32
+ },
33
+ outputSchema: {
34
+ ok: z.boolean(),
35
+ clientAbi: z.string(),
36
+ serverAbi: z.string(),
37
+ server: z.string(),
38
+ buildTime: z.string().optional(),
39
+ compatKind: z.enum(['ok', 'warn', 'fatal']),
40
+ compatReason: z.string(),
41
+ policy: policySchema,
42
+ warnings: z.array(warningShape),
43
+ userHints: z.array(z.string()),
44
+ },
45
+ annotations: {
46
+ title: 'Check iScopeBridge wire ABI',
47
+ readOnlyHint: true,
48
+ idempotentHint: true,
49
+ },
50
+ },
51
+ async (args) => {
52
+ try {
53
+ const result = await bridge.abiCheck(args.policy);
54
+ return ok(result as unknown as Record<string, unknown>);
55
+ } catch (e) {
56
+ return fail(`system_abi_check failed: ${errMsg(e)}`);
57
+ }
58
+ },
59
+ );
60
+ };
61
+
62
+ function errMsg(e: unknown): string {
63
+ if (e instanceof Error) return e.message;
64
+ if (typeof e === 'string') return e;
65
+ try { return JSON.stringify(e); } catch { return String(e); }
66
+ }
@@ -0,0 +1,59 @@
1
+ // debug_set_breakpoints.
2
+
3
+ import { z } from 'zod';
4
+ import type { DebugProtocol } from '@vscode/debugprotocol';
5
+
6
+ import { fail, ok } from './helpers.js';
7
+ import type { ToolGroupRegistrar } from './registry.js';
8
+
9
+ const setBreakpointsInput = {
10
+ source: z.string().describe(
11
+ 'Absolute or workspace-relative source path. `.ts` is fine — the adapter will map lines to the generated `.ajs` automatically. ⚠ REPLACE semantics: the list of lines you pass becomes THE complete breakpoint set for THIS source file. To clear all breakpoints in this file pass lines: []. Existing breakpoints in OTHER source files are NOT touched.',
12
+ ),
13
+ lines: z.array(z.number().int().positive()).describe(
14
+ '1-based line numbers in `source`. Empty array clears all breakpoints in `source`. Lines outside executable code (comments, blank lines) are silently dropped by the engine — they will come back `verified: false`.',
15
+ ),
16
+ };
17
+
18
+ export const registerBreakpointTools: ToolGroupRegistrar = (server, { dap }) => {
19
+ server.registerTool(
20
+ 'debug_set_breakpoints',
21
+ {
22
+ title: 'Set breakpoints',
23
+ description:
24
+ 'Replace breakpoints for ONE source file. Returns one `{line, verified}` per requested line in the same order. May be called before debug_launch (the adapter buffers them until configurationDone) or while paused (mid-run set is forwarded as individual SetResetBrkPnt RPCs). Calling while the script is running is fine but the engine cannot install BPs until the next pause — verified flags reflect best effort.',
25
+ inputSchema: setBreakpointsInput,
26
+ outputSchema: {
27
+ breakpoints: z.array(z.object({
28
+ line: z.number().optional(),
29
+ verified: z.boolean(),
30
+ message: z.string().optional(),
31
+ })),
32
+ },
33
+ annotations: {
34
+ title: 'Set breakpoints',
35
+ idempotentHint: true,
36
+ destructiveHint: false,
37
+ },
38
+ },
39
+ async (args) => {
40
+ try {
41
+ const body = await dap.setBreakpoints(args.source, args.lines);
42
+ const bps = (body?.breakpoints ?? []).map((bp: DebugProtocol.Breakpoint) => ({
43
+ line: bp.line,
44
+ verified: bp.verified === true,
45
+ message: bp.message,
46
+ }));
47
+ return ok({ breakpoints: bps });
48
+ } catch (e) {
49
+ return fail(`debug_set_breakpoints failed: ${errMsg(e)}`);
50
+ }
51
+ },
52
+ );
53
+ };
54
+
55
+ function errMsg(e: unknown): string {
56
+ if (e instanceof Error) return e.message;
57
+ if (typeof e === 'string') return e;
58
+ try { return JSON.stringify(e); } catch { return String(e); }
59
+ }
@@ -0,0 +1,105 @@
1
+ // debug_continue / debug_step_over / debug_step_into / debug_step_out.
2
+ //
3
+ // All four tools share the same shape: send a DAP request, wait for
4
+ // the next state transition (stopped / terminated / timeout), return
5
+ // it as the response. The AI doesn't need to call a separate
6
+ // `debug_wait_for_paused` afterwards.
7
+
8
+ import { z } from 'zod';
9
+
10
+ import { fail, ok, shapeTransition } from './helpers.js';
11
+ import type { ToolGroupRegistrar } from './registry.js';
12
+ import type { DapDriver } from '../dap-driver.js';
13
+ import type { DapClient } from '../dap-client.js';
14
+
15
+ const stepInput = {
16
+ timeoutMs: z.number().int().positive().optional().describe(
17
+ 'Max ms to wait for the resulting paused/terminated event. Defaults to 30 000. If the script keeps running past this timeout, returns {state:"timeout"}; you can then call debug_wait_for_paused to keep waiting.',
18
+ ),
19
+ };
20
+
21
+ const transitionShape = z.object({
22
+ state: z.enum(['paused', 'terminated', 'timeout']),
23
+ stopped: z.unknown().optional(),
24
+ exitInfo: z.unknown().optional(),
25
+ waitedMs: z.number().optional(),
26
+ });
27
+
28
+ function makeResume(
29
+ dap: DapDriver,
30
+ label: string,
31
+ dapCall: (dc: DapClient) => Promise<unknown>,
32
+ ) {
33
+ return async (args: { timeoutMs?: number }) => {
34
+ try {
35
+ const t = await dap.resumeAndWait(dapCall, args.timeoutMs);
36
+ return ok({ transition: shapeTransition(t) });
37
+ } catch (e) {
38
+ return fail(`${label} failed: ${errMsg(e)}`);
39
+ }
40
+ };
41
+ }
42
+
43
+ export const registerExecutionTools: ToolGroupRegistrar = (server, { dap }) => {
44
+ server.registerTool(
45
+ 'debug_continue',
46
+ {
47
+ title: 'Continue',
48
+ description:
49
+ 'Resume the script. Returns the next paused state (breakpoint / step done / exception) or `terminated` if the script finishes. The response contains the new stack — no need to call debug_stack_trace separately when stopped on a breakpoint.',
50
+ inputSchema: stepInput,
51
+ outputSchema: { transition: transitionShape },
52
+ annotations: { title: 'Continue', idempotentHint: false },
53
+ },
54
+ makeResume(dap, 'debug_continue',
55
+ (dc) => dc.request('continue', { threadId: 1 })),
56
+ );
57
+
58
+ server.registerTool(
59
+ 'debug_step_over',
60
+ {
61
+ title: 'Step over',
62
+ description:
63
+ 'Step over: execute the current statement and pause on the next one in the SAME frame (do not descend into function calls).',
64
+ inputSchema: stepInput,
65
+ outputSchema: { transition: transitionShape },
66
+ annotations: { title: 'Step over', idempotentHint: false },
67
+ },
68
+ makeResume(dap, 'debug_step_over',
69
+ (dc) => dc.request('next', { threadId: 1 })),
70
+ );
71
+
72
+ server.registerTool(
73
+ 'debug_step_into',
74
+ {
75
+ title: 'Step into',
76
+ description:
77
+ 'Step into: execute the current statement and, if it calls a function, pause on the first statement of that function.',
78
+ inputSchema: stepInput,
79
+ outputSchema: { transition: transitionShape },
80
+ annotations: { title: 'Step into', idempotentHint: false },
81
+ },
82
+ makeResume(dap, 'debug_step_into',
83
+ (dc) => dc.request('stepIn', { threadId: 1 })),
84
+ );
85
+
86
+ server.registerTool(
87
+ 'debug_step_out',
88
+ {
89
+ title: 'Step out',
90
+ description:
91
+ 'Step out: resume until the current frame returns, then pause in the caller.',
92
+ inputSchema: stepInput,
93
+ outputSchema: { transition: transitionShape },
94
+ annotations: { title: 'Step out', idempotentHint: false },
95
+ },
96
+ makeResume(dap, 'debug_step_out',
97
+ (dc) => dc.request('stepOut', { threadId: 1 })),
98
+ );
99
+ };
100
+
101
+ function errMsg(e: unknown): string {
102
+ if (e instanceof Error) return e.message;
103
+ if (typeof e === 'string') return e;
104
+ try { return JSON.stringify(e); } catch { return String(e); }
105
+ }
@@ -0,0 +1,142 @@
1
+ // Helpers shared across tool groups.
2
+ //
3
+ // Two concerns live here:
4
+ // 1. Pack a CallToolResult that has BOTH structuredContent (the
5
+ // JSON the AI cares about) AND a `content` text mirror (so MCP
6
+ // clients that ignore structuredContent still get something
7
+ // readable). The MCP SDK validates structuredContent against
8
+ // `outputSchema` when one is declared.
9
+ // 2. Translate driver-level types (ExecutionTransition, FrameInfo,
10
+ // ...) into AI-friendly objects. The driver types are wire-
11
+ // level; we expose only the fields the AI needs.
12
+
13
+ import type {
14
+ CallToolResult,
15
+ Implementation,
16
+ TextContent,
17
+ } from '@modelcontextprotocol/sdk/types.js';
18
+
19
+ import type {
20
+ ExecutionTransition,
21
+ FrameInfo,
22
+ OutputEntry,
23
+ StoppedInfo,
24
+ } from '../state.js';
25
+
26
+ /** Wrap a structured payload as a CallToolResult with a JSON-text
27
+ * mirror. `structuredContent` is validated by the SDK against the
28
+ * tool's `outputSchema` (when one is declared). */
29
+ export function ok<T extends Record<string, unknown>>(payload: T): CallToolResult {
30
+ const text: TextContent = {
31
+ type: 'text',
32
+ text: JSON.stringify(payload),
33
+ };
34
+ return {
35
+ content: [text],
36
+ structuredContent: payload,
37
+ };
38
+ }
39
+
40
+ /** Build the standard error result. We deliberately use the SDK's
41
+ * `isError` flag rather than throwing, so the AI gets a clear tool-
42
+ * level error response instead of a transport failure. */
43
+ export function fail(message: string, details?: Record<string, unknown>): CallToolResult {
44
+ const text: TextContent = {
45
+ type: 'text',
46
+ text: details
47
+ ? `${message}\n${JSON.stringify(details, null, 2)}`
48
+ : message,
49
+ };
50
+ return {
51
+ content: [text],
52
+ isError: true,
53
+ structuredContent: { error: message, ...(details ?? {}) },
54
+ };
55
+ }
56
+
57
+ /** Drop `undefined` values so JSON.stringify produces a clean
58
+ * output. The MCP SDK's strict JSON-Schema validator may reject
59
+ * payloads where an optional field is explicitly `undefined`
60
+ * (becomes `null` after JSON.parse). */
61
+ export function pruneUndefined<T extends Record<string, unknown>>(o: T): T {
62
+ for (const k of Object.keys(o)) {
63
+ if (o[k] === undefined) delete o[k];
64
+ }
65
+ return o;
66
+ }
67
+
68
+ export function shapeFrame(f: FrameInfo): Record<string, unknown> {
69
+ return pruneUndefined({
70
+ id: f.id,
71
+ name: f.name,
72
+ source: f.source,
73
+ sourceOrigin: f.sourceOrigin,
74
+ line: f.line,
75
+ column: f.column,
76
+ endLine: f.endLine,
77
+ endColumn: f.endColumn,
78
+ generatedSource: f.generatedSource,
79
+ generatedLine: f.generatedLine,
80
+ generatedColumn: f.generatedColumn,
81
+ });
82
+ }
83
+
84
+ export function shapeStopped(s: StoppedInfo): Record<string, unknown> {
85
+ return pruneUndefined({
86
+ reason: s.reason,
87
+ description: s.description,
88
+ frames: s.frames.map(shapeFrame),
89
+ });
90
+ }
91
+
92
+ export function shapeTransition(t: ExecutionTransition): Record<string, unknown> {
93
+ if (t.state === 'paused') return { state: 'paused', stopped: shapeStopped(t.stopped) };
94
+ if (t.state === 'terminated') return pruneUndefined({
95
+ state: 'terminated',
96
+ exitInfo: t.exitInfo ? pruneUndefined({ reason: t.exitInfo.reason }) : undefined,
97
+ });
98
+ return { state: 'timeout', waitedMs: t.waitedMs };
99
+ }
100
+
101
+ export function shapeOutputEntry(e: OutputEntry): Record<string, unknown> {
102
+ return {
103
+ id: e.id,
104
+ category: e.category,
105
+ text: e.text,
106
+ timestamp: e.timestamp,
107
+ };
108
+ }
109
+
110
+ /** Centralised server identity. Bumped together with package.json. */
111
+ export const SERVER_INFO: Implementation = {
112
+ name: '@i-scope/mcp-server',
113
+ version: '0.4.2',
114
+ title: 'iScope Debugger',
115
+ };
116
+
117
+ /** Server instructions surfaced to the LLM by clients that respect
118
+ * the MCP spec (Claude Desktop / Cursor do). Keep concise — the AI
119
+ * reads tool descriptions on each call already; this is for cross-
120
+ * tool workflow rules that don't fit any single tool's description. */
121
+ export const SERVER_INSTRUCTIONS = [
122
+ 'iScope MCP server: drives a `.ajs` (or transpiled-from-`.ts`) debug session in Oscilloscope.exe via the iScope DAP adapter.',
123
+ '',
124
+ 'Workflow rules:',
125
+ '- Call `debug_launch` before any other debug_* tool. State must be "paused" before stack_trace / scopes / variables / evaluate.',
126
+ '- Execution tools (`debug_continue`, `debug_step_*`) return the NEXT paused state (or terminated/timeout) — no separate `debug_wait_for_paused` call needed in the common case.',
127
+ '- `debug_set_breakpoints` REPLACES the breakpoint set for the given source. Passing `lines: []` clears them. Pass the COMPLETE list each time.',
128
+ '- Always finish with `debug_disconnect`. Leaking a session orphans Oscilloscope when this server exits.',
129
+ '- Source maps: pass `.ts` paths to `program` and `source` whenever a TS sibling exists — frames come back in `.ts` with `sourceOrigin: "mapped"`. `sourceOrigin: "unmapped-fallback"` signals that the .ajs.map has a gap; treat returned positions as raw `.ajs` coordinates.',
130
+ '- Use `debug_resolve_source` to translate `.ts ↔ .ajs` positions without launching a session (e.g. when inspecting engine error logs).',
131
+ '',
132
+ 'UI modal control (`ui_modal_*`):',
133
+ '- Use `ui_modal_list` to see if Oscilloscope is currently showing any child dialog (Configure() input form, "Save changes?" prompt, "Unable to..." error).',
134
+ '- A `debug_continue`/`debug_step_*` that silently times out is often the AI signal that a script-level Configure() modal is blocking the engine — list, fill, click OK, and execution will resume.',
135
+ '- These tools work WITHOUT an active debug session (Oscilloscope only needs to be running) and are SAFE to mix with an active DAP session.',
136
+ '',
137
+ 'System diagnostics:',
138
+ '- Use `system_preflight_check` proactively when a launch silently fails / breakpoints are ignored / Oscilloscope reports "Unable to create debuger object!". It probes the registry for the Windows Script Debugger (Machine Debug Manager) and the Oscilloscope COM CLSID — the two prerequisites of the iScope debug stack — and returns a structured verdict with user-actionable hints + download URL when something is missing. Read-only / idempotent / does not require Oscilloscope to be running.',
139
+ '- Use `system_abi_check` when mixing npm package versions or after swapping iScopeBridge.exe — it calls `protocol/version` (no COM) and compares wire ABI semver to @i-scope/iscope-bridge-client. Run before debug_launch if you see ProtocolVersionMismatchError or suspect a stale helper binary.',
140
+ '',
141
+ 'Pause is NOT supported (Oscilloscope COM API has no Pause()). Use breakpoints + step instead.',
142
+ ].join('\n');