@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
@@ -0,0 +1,222 @@
1
+ // debug_resolve_source — stand-alone source-map translator.
2
+ //
3
+ // Why it exists separate from the debug session tools:
4
+ // - AI workflows often need .ts <-> .ajs translation OUTSIDE of an
5
+ // active debug session — e.g. when inspecting a previously-saved
6
+ // engine error log (`L4 mhd::core/foo.ajs(123): TypeError ...`)
7
+ // and wanting to know what TS line that corresponds to.
8
+ // - No debug-server process, no Oscilloscope dependency. Just disk
9
+ // reads and the SourceMapManager.
10
+ //
11
+ // The Phase 1 implementation loads maps on demand (idempotent
12
+ // cache); Phase 2 may share the cache with DapDriver to avoid
13
+ // double-parsing the same .ajs.map.
14
+
15
+ import * as path from 'node:path';
16
+ import { existsSync } from 'node:fs';
17
+
18
+ import { z } from 'zod';
19
+
20
+ import { SourceMapManager } from '@i-scope/source-map-bridge';
21
+
22
+ import { fail, ok, pruneUndefined } from './helpers.js';
23
+ import type { ToolGroupRegistrar } from './registry.js';
24
+
25
+ // One manager per process. Loads are cached internally, so calling
26
+ // resolve_source repeatedly for the same .ajs is O(1) after the
27
+ // first hit.
28
+ let sharedManager: SourceMapManager | null = null;
29
+ function getManager(): SourceMapManager {
30
+ if (!sharedManager) {
31
+ sharedManager = new SourceMapManager({
32
+ log: (line) => process.stderr.write(`[mcp/sourcemap] ${line}\n`),
33
+ });
34
+ }
35
+ return sharedManager;
36
+ }
37
+
38
+ const resolveInput = {
39
+ file: z.string().describe(
40
+ 'Path to a source file. Either a `.ts` (forward direction — looks for `<base>.ajs` next to it OR via `generatedHint`) or a `.ajs` / `.apn` / `.aps` (reverse direction — loads its sidecar .map).',
41
+ ),
42
+ line: z.number().int().positive().describe(
43
+ '1-based line in `file`. DAP / engine convention.',
44
+ ),
45
+ column: z.number().int().positive().optional().describe(
46
+ '1-based column in `file`. Defaults to 1.',
47
+ ),
48
+ generatedHint: z.string().optional().describe(
49
+ 'When `file` is `.ts` and the generated `.ajs` is NOT a direct sibling, pass the absolute or relative path to the `.ajs` here. Otherwise this MCP tool guesses `<dirname>/<basename>.ajs`.',
50
+ ),
51
+ direction: z.enum(['original->generated', 'generated->original', 'auto']).optional().describe(
52
+ 'Translation direction. "auto" (default) picks based on `file` extension: `.ts` → original->generated, `.ajs`/`.apn`/`.aps` → generated->original. Pass explicitly when you have an unusual extension or want to force a direction (e.g. ask a `.ts` for its OWN line in the SAME file — pass "generated->original" with a `.ts`, the tool will reject it with reason `wrong-direction`).',
53
+ ),
54
+ };
55
+
56
+ export const registerSourceMapTools: ToolGroupRegistrar = (server, _drivers) => {
57
+ void _drivers;
58
+
59
+ server.registerTool(
60
+ 'debug_resolve_source',
61
+ {
62
+ title: 'Resolve .ts ↔ .ajs source position',
63
+ description:
64
+ 'Stand-alone source-map translator. NO debug session required. Pass a `.ts` position to get the `.ajs` it compiles to, or a `.ajs` position to get the `.ts` it came from. Useful for: (1) interpreting engine error logs that reference `.ajs` coordinates, (2) sanity-checking that a `.ts` line you plan to set a breakpoint on actually maps to executable `.ajs` code (otherwise the breakpoint will be `verified: false` after debug_set_breakpoints).',
65
+ inputSchema: resolveInput,
66
+ outputSchema: {
67
+ direction: z.enum(['ts->ajs', 'ajs->ts', 'no-map']),
68
+ input: z.object({ file: z.string(), line: z.number(), column: z.number() }),
69
+ resolved: z.object({
70
+ file: z.string(),
71
+ line: z.number(),
72
+ column: z.number(),
73
+ }).optional(),
74
+ reason: z.string().optional(),
75
+ },
76
+ annotations: {
77
+ title: 'Resolve source position',
78
+ readOnlyHint: true,
79
+ idempotentHint: true,
80
+ },
81
+ },
82
+ async (args) => {
83
+ try {
84
+ const result = await resolve(args);
85
+ return ok(result);
86
+ } catch (e) {
87
+ return fail(`debug_resolve_source failed: ${errMsg(e)}`);
88
+ }
89
+ },
90
+ );
91
+ };
92
+
93
+ async function resolve(args: {
94
+ file: string;
95
+ line: number;
96
+ column?: number;
97
+ generatedHint?: string;
98
+ direction?: 'original->generated' | 'generated->original' | 'auto';
99
+ }): Promise<Record<string, unknown>> {
100
+ const mgr = getManager();
101
+ const file = path.resolve(args.file);
102
+ const col = args.column ?? 1;
103
+ const isTs = /\.ts$/i.test(file);
104
+ const isGen = /\.(ajs|apn|aps)$/i.test(file);
105
+ const requested = args.direction ?? 'auto';
106
+
107
+ // Resolve direction from `requested` + file extension.
108
+ let direction: 'original->generated' | 'generated->original';
109
+ if (requested === 'auto') {
110
+ if (isTs) direction = 'original->generated';
111
+ else if (isGen) direction = 'generated->original';
112
+ else {
113
+ return pruneUndefined({
114
+ direction: 'no-map' as const,
115
+ input: { file, line: args.line, column: col },
116
+ reason: `cannot auto-detect direction for file extension; pass direction explicitly. file=${file}`,
117
+ });
118
+ }
119
+ } else {
120
+ direction = requested;
121
+ // Mismatched explicit direction is a hard error so AI doesn't
122
+ // silently get nonsense (a .ts asked as generated->original
123
+ // would either fail later or fall through to the wrong code
124
+ // path).
125
+ if (direction === 'original->generated' && !isTs && isGen) {
126
+ return pruneUndefined({
127
+ direction: 'no-map' as const,
128
+ input: { file, line: args.line, column: col },
129
+ reason: `wrong-direction: file ${path.basename(file)} looks generated but direction='original->generated'`,
130
+ });
131
+ }
132
+ if (direction === 'generated->original' && isTs) {
133
+ return pruneUndefined({
134
+ direction: 'no-map' as const,
135
+ input: { file, line: args.line, column: col },
136
+ reason: `wrong-direction: file ${path.basename(file)} is .ts but direction='generated->original'`,
137
+ });
138
+ }
139
+ }
140
+ const input = { file, line: args.line, column: col };
141
+
142
+ if (direction === 'original->generated') {
143
+ // Forward direction: load the (hinted-or-sibling) .ajs's map
144
+ // first, THEN ask the manager for the .ts -> .ajs translation.
145
+ const ajsCandidate = args.generatedHint
146
+ ? path.resolve(args.generatedHint)
147
+ : guessSibling(file, '.ajs');
148
+
149
+ if (!existsSync(ajsCandidate)) {
150
+ return pruneUndefined({
151
+ direction: 'no-map' as const,
152
+ input,
153
+ reason: `generated bundle not found: ${ajsCandidate}. Pass an explicit generatedHint if the .ajs is elsewhere.`,
154
+ });
155
+ }
156
+
157
+ const loaded = await mgr.loadFor(ajsCandidate);
158
+ if (!loaded) {
159
+ return pruneUndefined({
160
+ direction: 'no-map' as const,
161
+ input,
162
+ reason: `no source map found for ${ajsCandidate}`,
163
+ });
164
+ }
165
+
166
+ const pos = await mgr.toGenerated(file, args.line, col);
167
+ if (!pos) {
168
+ return pruneUndefined({
169
+ direction: 'ts->ajs' as const,
170
+ input,
171
+ reason: `no mapping for ${path.basename(file)}:${args.line}:${col} in ${path.basename(ajsCandidate)}.map`,
172
+ });
173
+ }
174
+ return pruneUndefined({
175
+ direction: 'ts->ajs' as const,
176
+ input,
177
+ resolved: { file: pos.generatedPath, line: pos.line, column: pos.column },
178
+ });
179
+ }
180
+
181
+ // Reverse direction (`generated->original`): load file's own map.
182
+ if (!existsSync(file)) {
183
+ return pruneUndefined({
184
+ direction: 'no-map' as const,
185
+ input,
186
+ reason: `file not found: ${file}`,
187
+ });
188
+ }
189
+ const loaded = await mgr.loadFor(file);
190
+ if (!loaded) {
191
+ return pruneUndefined({
192
+ direction: 'no-map' as const,
193
+ input,
194
+ reason: `no source map found for ${file}`,
195
+ });
196
+ }
197
+ const orig = await mgr.toOriginal({ generatedPath: file, line: args.line, column: col });
198
+ if (!orig) {
199
+ return pruneUndefined({
200
+ direction: 'ajs->ts' as const,
201
+ input,
202
+ reason: `no mapping for ${path.basename(file)}:${args.line}:${col} (engine-injected helper or non-emitted statement)`,
203
+ });
204
+ }
205
+ return pruneUndefined({
206
+ direction: 'ajs->ts' as const,
207
+ input,
208
+ resolved: { file: orig.originalPath, line: orig.line, column: orig.column },
209
+ });
210
+ }
211
+
212
+ function guessSibling(tsPath: string, generatedExt: string): string {
213
+ const dir = path.dirname(tsPath);
214
+ const base = path.basename(tsPath, path.extname(tsPath));
215
+ return path.join(dir, base + generatedExt);
216
+ }
217
+
218
+ function errMsg(e: unknown): string {
219
+ if (e instanceof Error) return e.message;
220
+ if (typeof e === 'string') return e;
221
+ try { return JSON.stringify(e); } catch { return String(e); }
222
+ }
@@ -0,0 +1,90 @@
1
+ // debug_wait_for_paused / debug_read_output.
2
+
3
+ import { z } from 'zod';
4
+
5
+ import { fail, ok, shapeOutputEntry, shapeTransition } from './helpers.js';
6
+ import type { ToolGroupRegistrar } from './registry.js';
7
+
8
+ const transitionShape = z.object({
9
+ state: z.enum(['paused', 'terminated', 'timeout']),
10
+ stopped: z.unknown().optional(),
11
+ exitInfo: z.unknown().optional(),
12
+ waitedMs: z.number().optional(),
13
+ });
14
+
15
+ export const registerSyncTools: ToolGroupRegistrar = (server, { dap }) => {
16
+ server.registerTool(
17
+ 'debug_wait_for_paused',
18
+ {
19
+ title: 'Wait for next pause',
20
+ description:
21
+ 'Block until the script next pauses (breakpoint, step done, exception) or terminates. Useful when a long-running script is past the timeout of debug_continue / debug_step_*. If the session is already paused / terminated the tool returns immediately.',
22
+ inputSchema: {
23
+ timeoutMs: z.number().int().positive().optional().describe(
24
+ 'Max ms to wait. Defaults to 30 000. Returns {state:"timeout"} when exceeded; the session remains running and you can call again.',
25
+ ),
26
+ },
27
+ outputSchema: { transition: transitionShape },
28
+ annotations: {
29
+ title: 'Wait for next pause',
30
+ idempotentHint: true,
31
+ readOnlyHint: true,
32
+ },
33
+ },
34
+ async (args) => {
35
+ try {
36
+ // Pass `null` as dapCall — we are observing, not
37
+ // resuming. resumeAndWait short-circuits to a
38
+ // snapshot if state is already paused / terminated.
39
+ const t = await dap.resumeAndWait(null, args.timeoutMs);
40
+ return ok({ transition: shapeTransition(t) });
41
+ } catch (e) {
42
+ return fail(`debug_wait_for_paused failed: ${errMsg(e)}`);
43
+ }
44
+ },
45
+ );
46
+
47
+ server.registerTool(
48
+ 'debug_read_output',
49
+ {
50
+ title: 'Read buffered output',
51
+ description:
52
+ 'Read `Host.ReportOut(...)` traces and adapter diagnostics emitted since `cursor`. Cursor-based pagination: pass the `nextCursor` from the previous call to read only new entries. Categories: "stdout" (INFO+WARN), "stderr" (ERR + L4+), "console" (adapter diagnostics / break markers). Buffer holds the last 2000 entries by default — older ones are dropped to keep memory bounded.',
53
+ inputSchema: {
54
+ cursor: z.number().int().nonnegative().optional().describe(
55
+ 'Highest entry id you have already read. Defaults to 0 (read from start).',
56
+ ),
57
+ limit: z.number().int().positive().optional().describe(
58
+ 'Max entries to return in one call. Defaults to 1000.',
59
+ ),
60
+ },
61
+ outputSchema: {
62
+ entries: z.array(z.object({
63
+ id: z.number(),
64
+ category: z.string(),
65
+ text: z.string(),
66
+ timestamp: z.number(),
67
+ })),
68
+ nextCursor: z.number(),
69
+ },
70
+ annotations: {
71
+ title: 'Read buffered output',
72
+ idempotentHint: true,
73
+ readOnlyHint: true,
74
+ },
75
+ },
76
+ async (args) => {
77
+ const { entries, nextCursor } = dap.readOutput(args.cursor ?? 0, args.limit ?? 1000);
78
+ return ok({
79
+ entries: entries.map(shapeOutputEntry),
80
+ nextCursor,
81
+ });
82
+ },
83
+ );
84
+ };
85
+
86
+ function errMsg(e: unknown): string {
87
+ if (e instanceof Error) return e.message;
88
+ if (typeof e === 'string') return e;
89
+ try { return JSON.stringify(e); } catch { return String(e); }
90
+ }
@@ -0,0 +1,201 @@
1
+ // ui_modal_* — interactive control over Oscilloscope's child dialogs.
2
+ //
3
+ // The C++ helper (iScopeBridge.exe) enforces every safety check
4
+ // (oscPid match, main-HWND reject, SendMessageTimeout 500ms); these
5
+ // tools are thin typed wrappers + AI-friendly descriptions.
6
+ //
7
+ // Typical AI workflow when a debug step / continue times out:
8
+ // 1. `ui_modal_list` — is Oscilloscope showing a modal?
9
+ // → If `modals[].signature === 'configure'`, the script is
10
+ // blocked on a `Host.Configure()` form.
11
+ // → If `signature === 'diagnostic'`, Oscilloscope itself is
12
+ // reporting an error; close it with `ui_modal_dismiss`.
13
+ // 2. `ui_modal_fill` for each Edit/ComboBox to enter values.
14
+ // 3. `ui_modal_click({controlId: 1 /* IDOK */})` to submit.
15
+ // 4. Resume the original `debug_continue` — engine should proceed.
16
+
17
+ import { z } from 'zod';
18
+
19
+ import { fail, ok } from './helpers.js';
20
+ import type { ToolGroupRegistrar } from './registry.js';
21
+
22
+ const controlInfoShape = z.object({
23
+ hwnd: z.string(),
24
+ controlId: z.number(),
25
+ className: z.string(),
26
+ text: z.string(),
27
+ isVisible: z.boolean(),
28
+ isEnabled: z.boolean(),
29
+ isChecked: z.boolean().optional(),
30
+ });
31
+
32
+ const modalInfoShape = z.object({
33
+ hwnd: z.string(),
34
+ title: z.string(),
35
+ className: z.string(),
36
+ ownerHwnd: z.string(),
37
+ threadId: z.number(),
38
+ signature: z.enum(['configure', 'diagnostic', 'unknown']),
39
+ controls: z.array(controlInfoShape),
40
+ });
41
+
42
+ export const registerUIModalTools: ToolGroupRegistrar = (server, { bridge }) => {
43
+
44
+ // ---- ui_modal_list -----------------------------------------------------
45
+
46
+ server.registerTool(
47
+ 'ui_modal_list',
48
+ {
49
+ title: 'List Oscilloscope modal dialogs',
50
+ description:
51
+ 'Enumerate every visible top-level dialog owned by Oscilloscope and the controls inside it. ' +
52
+ 'Use this when a debug_continue / debug_step_* unexpectedly times out — the script is most ' +
53
+ 'likely blocked on a Host.Configure() form (signature="configure") or a diagnostic prompt ' +
54
+ '(signature="diagnostic"). Returns oscPid=0 and empty modals[] when Oscilloscope is not ' +
55
+ 'running. SAFE to call regardless of debug session state; never auto-launches Oscilloscope.',
56
+ inputSchema: {},
57
+ outputSchema: {
58
+ oscPid: z.number(),
59
+ mainHwnd: z.string(),
60
+ modals: z.array(modalInfoShape),
61
+ },
62
+ annotations: {
63
+ title: 'List Oscilloscope modal dialogs',
64
+ readOnlyHint: true,
65
+ idempotentHint: true,
66
+ },
67
+ },
68
+ async () => {
69
+ try {
70
+ const result = await bridge.uiModalList();
71
+ return ok(result as unknown as Record<string, unknown>);
72
+ } catch (e) {
73
+ return fail(`ui_modal_list failed: ${errMsg(e)}`);
74
+ }
75
+ },
76
+ );
77
+
78
+ // ---- ui_modal_click ----------------------------------------------------
79
+
80
+ server.registerTool(
81
+ 'ui_modal_click',
82
+ {
83
+ title: 'Click a button in an Oscilloscope modal',
84
+ description:
85
+ 'Programmatically click a button (Win32 WM_COMMAND + BN_CLICKED). Standard controlIds: ' +
86
+ '1=IDOK, 2=IDCANCEL, 6=IDYES, 7=IDNO. Custom buttons in Host.Configure() forms have IDs ' +
87
+ 'reported by ui_modal_list. Always call ui_modal_list first to learn current state and ' +
88
+ 'the right hwnd. Errors -32602 if hwnd belongs to a different process, is the main ' +
89
+ 'window, or the control is disabled.',
90
+ inputSchema: {
91
+ hwnd: z.string().describe('Dialog hwnd from ui_modal_list result (e.g. "0x007D185E").'),
92
+ controlId: z.number().int().describe('Button controlId (1=IDOK, 2=IDCANCEL, 6=IDYES, 7=IDNO, ...).'),
93
+ },
94
+ outputSchema: {
95
+ ok: z.boolean(),
96
+ lastError: z.number().optional(),
97
+ },
98
+ annotations: {
99
+ title: 'Click a button in an Oscilloscope modal',
100
+ destructiveHint: true,
101
+ },
102
+ },
103
+ async (args) => {
104
+ try {
105
+ const result = await bridge.uiModalClick({
106
+ hwnd: args.hwnd,
107
+ controlId: args.controlId,
108
+ });
109
+ return ok(result as unknown as Record<string, unknown>);
110
+ } catch (e) {
111
+ return fail(`ui_modal_click failed: ${errMsg(e)}`);
112
+ }
113
+ },
114
+ );
115
+
116
+ // ---- ui_modal_fill -----------------------------------------------------
117
+
118
+ server.registerTool(
119
+ 'ui_modal_fill',
120
+ {
121
+ title: 'Fill an Edit or ComboBox in an Oscilloscope modal',
122
+ description:
123
+ 'Set the text of a child Edit control (WM_SETTEXT) or a ComboBox (CB_FINDSTRINGEXACT + ' +
124
+ 'CB_SETCURSEL when the value matches an existing item, WM_SETTEXT fallback otherwise). ' +
125
+ 'Returns previousText for diffing/verification. Errors -32602 if the control class is ' +
126
+ 'not Edit/ComboBox, the control is disabled, or the dialog is not Oscilloscope-owned.',
127
+ inputSchema: {
128
+ hwnd: z.string().describe('Dialog hwnd from ui_modal_list.'),
129
+ controlId: z.number().int().describe('Control id from ui_modal_list[*].controls[*].controlId.'),
130
+ text: z.string().describe('New text. UTF-8.'),
131
+ },
132
+ outputSchema: {
133
+ ok: z.boolean(),
134
+ previousText: z.string(),
135
+ lastError: z.number().optional(),
136
+ },
137
+ annotations: {
138
+ title: 'Fill an Edit or ComboBox in an Oscilloscope modal',
139
+ destructiveHint: true,
140
+ },
141
+ },
142
+ async (args) => {
143
+ try {
144
+ const result = await bridge.uiModalFill({
145
+ hwnd: args.hwnd,
146
+ controlId: args.controlId,
147
+ text: args.text,
148
+ });
149
+ return ok(result as unknown as Record<string, unknown>);
150
+ } catch (e) {
151
+ return fail(`ui_modal_fill failed: ${errMsg(e)}`);
152
+ }
153
+ },
154
+ );
155
+
156
+ // ---- ui_modal_dismiss --------------------------------------------------
157
+
158
+ server.registerTool(
159
+ 'ui_modal_dismiss',
160
+ {
161
+ title: 'Dismiss an Oscilloscope modal',
162
+ description:
163
+ 'Explicitly close a modal: ' +
164
+ 'mode="close" → PostMessage(WM_CLOSE) (= the X button); ' +
165
+ 'mode="cancel" → SendMessage(WM_COMMAND, IDCANCEL) (= Esc / Cancel button); ' +
166
+ 'mode="ok" → SendMessage(WM_COMMAND, IDOK) (= Enter / OK button). ' +
167
+ 'Default mode is "close". Prefer ui_modal_click for explicit button-by-id semantics when ' +
168
+ 'the dialog has multiple buttons; use this tool when you just need the modal gone.',
169
+ inputSchema: {
170
+ hwnd: z.string().describe('Dialog hwnd from ui_modal_list.'),
171
+ mode: z.enum(['close', 'cancel', 'ok']).optional().describe('Default "close".'),
172
+ },
173
+ outputSchema: {
174
+ ok: z.boolean(),
175
+ mode: z.enum(['close', 'cancel', 'ok']),
176
+ lastError: z.number().optional(),
177
+ },
178
+ annotations: {
179
+ title: 'Dismiss an Oscilloscope modal',
180
+ destructiveHint: true,
181
+ },
182
+ },
183
+ async (args) => {
184
+ try {
185
+ const result = await bridge.uiModalDismiss({
186
+ hwnd: args.hwnd,
187
+ mode: args.mode,
188
+ });
189
+ return ok(result as unknown as Record<string, unknown>);
190
+ } catch (e) {
191
+ return fail(`ui_modal_dismiss failed: ${errMsg(e)}`);
192
+ }
193
+ },
194
+ );
195
+ };
196
+
197
+ function errMsg(e: unknown): string {
198
+ if (e instanceof Error) return e.message;
199
+ if (typeof e === 'string') return e;
200
+ try { return JSON.stringify(e); } catch { return String(e); }
201
+ }