@nwire/mcp 0.9.2 → 0.10.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/src/index.ts DELETED
@@ -1,348 +0,0 @@
1
- /**
2
- * `@nwire/mcp` — Model Context Protocol server exposing the kernel's
3
- * `CommandRouter` as MCP tools over stdio.
4
- *
5
- * One transport, three surfaces: the CLI calls `kernel.router.run(name, args)`
6
- * directly; Studio dispatches through `/__nwire/run/exec`; AI clients (Claude
7
- * Desktop, Cursor, IDE plugins) speak JSON-RPC 2.0 over stdio per the MCP
8
- * spec and the server routes their `tools/call` to the same command table.
9
- *
10
- * The protocol surface this server implements (per
11
- * https://spec.modelcontextprotocol.io/):
12
- *
13
- * initialize — client handshake, returns server capabilities
14
- * tools/list — returns one tool per registered command
15
- * tools/call — invokes the named command, streams output as
16
- * progress notifications, returns the final result
17
- *
18
- * Transport: stdio. JSON-RPC framed by newline-delimited messages over
19
- * stdin/stdout. Stderr is for server logs (never JSON-RPC).
20
- *
21
- * NOTE: This is a focused MVP — initialize + tools/list + tools/call.
22
- * Resources, prompts, sampling, and notifications are sketched in the
23
- * `serveMcp` switch as TODOs but not yet implemented. The hot path
24
- * (AI client invokes an nwire command) works end-to-end.
25
- */
26
-
27
- import { createKernel, type CommandHandle, type Kernel } from "@nwire/kernel";
28
- import { inspectTools, findInspectTool } from "./inspect.js";
29
- import { writeTools, findWriteTool } from "./write-tools.js";
30
-
31
- // ─── JSON-RPC framing over stdio ───────────────────────────────────
32
-
33
- interface JsonRpcRequest {
34
- readonly jsonrpc: "2.0";
35
- readonly id?: string | number | null;
36
- readonly method: string;
37
- readonly params?: Record<string, unknown>;
38
- }
39
-
40
- interface JsonRpcResponse {
41
- readonly jsonrpc: "2.0";
42
- readonly id: string | number | null;
43
- readonly result?: unknown;
44
- readonly error?: { code: number; message: string; data?: unknown };
45
- }
46
-
47
- interface JsonRpcNotification {
48
- readonly jsonrpc: "2.0";
49
- readonly method: string;
50
- readonly params?: Record<string, unknown>;
51
- }
52
-
53
- const ERR_PARSE = -32700;
54
- const ERR_NOT_FOUND = -32601;
55
- const ERR_INVALID_PARAMS = -32602;
56
- const ERR_INTERNAL = -32603;
57
-
58
- // ─── Server ────────────────────────────────────────────────────────
59
-
60
- export interface ServeMcpOptions {
61
- /** Kernel instance to drive. Defaults to a fresh one. */
62
- readonly kernel?: Kernel;
63
- /** Server name reported on `initialize`. Default `"nwire"`. */
64
- readonly serverName?: string;
65
- /** Server version reported on `initialize`. Default `"0.1.0"`. */
66
- readonly serverVersion?: string;
67
- /**
68
- * Input stream the server reads newline-delimited JSON-RPC from.
69
- * Defaults to `process.stdin`. Tests supply a PassThrough; foreign
70
- * transports (websocket, http) can adapt to this same shape.
71
- */
72
- readonly stdin?: NodeJS.ReadableStream;
73
- /**
74
- * Output stream the server writes responses + notifications to.
75
- * Defaults to `process.stdout`. Stderr is always used for server
76
- * logs and is never overridden.
77
- */
78
- readonly stdout?: NodeJS.WritableStream;
79
- }
80
-
81
- /**
82
- * Start the MCP server on stdio. Blocks the process until stdin closes
83
- * (the client disconnected). Returns the kernel for callers who want
84
- * to register commands before serving:
85
- *
86
- * const k = createKernel();
87
- * k.router.register("dev", devHandler);
88
- * await serveMcp({ kernel: k });
89
- */
90
- export async function serveMcp(options: ServeMcpOptions = {}): Promise<void> {
91
- const kernel = options.kernel ?? createKernel();
92
- const serverName = options.serverName ?? "nwire";
93
- const serverVersion = options.serverVersion ?? "0.1.0";
94
- const stdin = options.stdin ?? process.stdin;
95
- const stdout = options.stdout ?? process.stdout;
96
-
97
- const send = (msg: JsonRpcResponse | JsonRpcNotification): void => {
98
- stdout.write(JSON.stringify(msg) + "\n");
99
- };
100
- const log = (text: string): void => {
101
- process.stderr.write(`[nwire-mcp] ${text}\n`);
102
- };
103
-
104
- log(`server starting (${kernel.router.list().length} commands registered)`);
105
-
106
- await readLines(stdin, async (line) => {
107
- if (!line.trim()) return;
108
- let request: JsonRpcRequest;
109
- try {
110
- request = JSON.parse(line) as JsonRpcRequest;
111
- } catch (err) {
112
- send({
113
- jsonrpc: "2.0",
114
- id: null,
115
- error: { code: ERR_PARSE, message: (err as Error).message },
116
- });
117
- return;
118
- }
119
- await handle(request, { kernel, serverName, serverVersion, send });
120
- });
121
-
122
- log("stdin closed — server exiting");
123
- }
124
-
125
- interface HandleContext {
126
- readonly kernel: Kernel;
127
- readonly serverName: string;
128
- readonly serverVersion: string;
129
- readonly send: (msg: JsonRpcResponse | JsonRpcNotification) => void;
130
- }
131
-
132
- async function handle(req: JsonRpcRequest, ctx: HandleContext): Promise<void> {
133
- // JSON-RPC 2.0: a request with no `id` is a notification. Per the spec
134
- // we must not respond to it. The MCP lifecycle uses
135
- // `notifications/initialized` (and other `notifications/*` methods)
136
- // for client-driven signals — these are silently accepted, never
137
- // answered. Strict MCP clients hang otherwise.
138
- const isNotification = req.id === undefined || req.method.startsWith("notifications/");
139
- if (isNotification) {
140
- return;
141
- }
142
- const id = req.id ?? null;
143
- try {
144
- switch (req.method) {
145
- case "initialize":
146
- ctx.send({
147
- jsonrpc: "2.0",
148
- id,
149
- result: {
150
- protocolVersion: "2024-11-05",
151
- serverInfo: { name: ctx.serverName, version: ctx.serverVersion },
152
- capabilities: { tools: { listChanged: false } },
153
- },
154
- });
155
- return;
156
-
157
- case "tools/list":
158
- ctx.send({
159
- jsonrpc: "2.0",
160
- id,
161
- result: {
162
- tools: [
163
- ...ctx.kernel.router.list().map((name) => ({
164
- name,
165
- description: `Nwire command "${name}"`,
166
- inputSchema: { type: "object", additionalProperties: true },
167
- })),
168
- // Read-only introspection tools that proxy /_nwire/* (or
169
- // fall back to the .nwire/ disk cache).
170
- ...inspectTools.map((t) => ({
171
- name: t.name,
172
- description: t.description,
173
- inputSchema: t.inputSchema,
174
- })),
175
- // Mutating tools that drive the wire via POST /_nwire/*.
176
- // Some target endpoints aren't mounted yet — those tools
177
- // gracefully gap-flag (see write-tools.ts).
178
- ...writeTools.map((t) => ({
179
- name: t.name,
180
- description: t.description,
181
- inputSchema: t.inputSchema,
182
- })),
183
- ],
184
- },
185
- });
186
- return;
187
-
188
- case "tools/call":
189
- await callTool(req, id, ctx);
190
- return;
191
-
192
- default:
193
- ctx.send({
194
- jsonrpc: "2.0",
195
- id,
196
- error: { code: ERR_NOT_FOUND, message: `unknown method "${req.method}"` },
197
- });
198
- }
199
- } catch (err) {
200
- ctx.send({
201
- jsonrpc: "2.0",
202
- id,
203
- error: { code: ERR_INTERNAL, message: (err as Error).message },
204
- });
205
- }
206
- }
207
-
208
- async function callTool(
209
- req: JsonRpcRequest,
210
- id: string | number | null,
211
- ctx: HandleContext,
212
- ): Promise<void> {
213
- const name = req.params?.name as string | undefined;
214
- const args = (req.params?.arguments ?? {}) as Record<string, unknown>;
215
- if (!name) {
216
- ctx.send({
217
- jsonrpc: "2.0",
218
- id,
219
- error: { code: ERR_INVALID_PARAMS, message: "tools/call requires `name`" },
220
- });
221
- return;
222
- }
223
-
224
- // Inspect + write tools live alongside the kernel CommandRouter but
225
- // don't emit progress notifications — they're synchronous HTTP/file
226
- // reads (inspect) or single POSTs (write). Match these BEFORE the
227
- // router-has check so they don't collide.
228
- const httpTool = findInspectTool(name) ?? findWriteTool(name);
229
- if (httpTool) {
230
- try {
231
- const out = await httpTool.run(args);
232
- ctx.send({
233
- jsonrpc: "2.0",
234
- id,
235
- result: {
236
- content: [{ type: "text", text: JSON.stringify(out) }],
237
- isError: false,
238
- },
239
- });
240
- } catch (err) {
241
- ctx.send({
242
- jsonrpc: "2.0",
243
- id,
244
- result: {
245
- content: [{ type: "text", text: (err as Error).message }],
246
- isError: true,
247
- },
248
- });
249
- }
250
- return;
251
- }
252
-
253
- if (!ctx.kernel.router.has(name)) {
254
- ctx.send({
255
- jsonrpc: "2.0",
256
- id,
257
- error: { code: ERR_INVALID_PARAMS, message: `unknown tool "${name}"` },
258
- });
259
- return;
260
- }
261
-
262
- // Forward command lifecycle events as MCP progress notifications.
263
- const handle: CommandHandle = ctx.kernel.run(name, args);
264
- const unsubscribe = handle.on((event) => {
265
- if (event.kind === "command.log") {
266
- ctx.send({
267
- jsonrpc: "2.0",
268
- method: "notifications/progress",
269
- params: {
270
- progressToken: handle.commandId,
271
- message: event.line,
272
- stream: event.stream,
273
- },
274
- });
275
- } else if (event.kind === "command.progress") {
276
- ctx.send({
277
- jsonrpc: "2.0",
278
- method: "notifications/progress",
279
- params: {
280
- progressToken: handle.commandId,
281
- message: event.message,
282
- pct: event.pct,
283
- },
284
- });
285
- }
286
- });
287
-
288
- try {
289
- const result = await handle.promise;
290
- ctx.send({
291
- jsonrpc: "2.0",
292
- id,
293
- result: {
294
- content: [{ type: "text", text: JSON.stringify(result) }],
295
- isError: false,
296
- },
297
- });
298
- } catch (err) {
299
- ctx.send({
300
- jsonrpc: "2.0",
301
- id,
302
- result: {
303
- content: [{ type: "text", text: (err as Error).message }],
304
- isError: true,
305
- },
306
- });
307
- } finally {
308
- unsubscribe();
309
- }
310
- }
311
-
312
- /**
313
- * Read newline-delimited messages from a stream. Resolves when the
314
- * stream ends. Tolerates partial chunks (buffers + splits on \n).
315
- */
316
- function readLines(
317
- stream: NodeJS.ReadableStream,
318
- onLine: (line: string) => Promise<void>,
319
- ): Promise<void> {
320
- return new Promise((resolve) => {
321
- let buffer = "";
322
- stream.on("data", async (chunk: Buffer | string) => {
323
- buffer += typeof chunk === "string" ? chunk : chunk.toString("utf8");
324
- // CRLF-safe: Windows MCP clients write `\r\n` terminated frames.
325
- // Splitting on `\n` alone leaves a trailing `\r` on every line which
326
- // breaks downstream JSON parsing of the next message.
327
- const lines = buffer.split(/\r?\n/);
328
- buffer = lines.pop() ?? "";
329
- for (const line of lines) {
330
- try {
331
- await onLine(line);
332
- } catch (err) {
333
- process.stderr.write(`[nwire-mcp] onLine threw: ${(err as Error).message}\n`);
334
- }
335
- }
336
- });
337
- stream.on("end", () => resolve());
338
- stream.on("close", () => resolve());
339
- });
340
- }
341
-
342
- // Also export the JSON-RPC types so consumers can write their own
343
- // transport adapters (e.g. WebSocket) reusing the dispatch logic.
344
- export type { JsonRpcRequest, JsonRpcResponse, JsonRpcNotification };
345
-
346
- export { inspectTools, findInspectTool, resetInspectDiscoveryCache } from "./inspect.js";
347
- export type { InspectToolDef } from "./inspect.js";
348
- export { writeTools, findWriteTool } from "./write-tools.js";
package/src/inspect.ts DELETED
@@ -1,329 +0,0 @@
1
- /**
2
- * Inspect tools — proxy a running wire's `/_nwire/*` introspection
3
- * endpoints (or fall back to the on-disk `.nwire/` cache) as MCP tools.
4
- *
5
- * Today's `@nwire/mcp` only exposed the kernel's CLI commands. The
6
- * running wire mounts a rich read-only surface (manifest, recent events,
7
- * recent telemetry, dispatch…) at `/_nwire/*`. We adapt that surface
8
- * into MCP tools so an AI client can ask "what actions does this app
9
- * expose?" or "show me the last 100 events" without booting anything.
10
- *
11
- * Discovery follows the same pattern Studio uses (see
12
- * `packages/nwire-studio/vite.config.ts`):
13
- *
14
- * 1. If `NWIRE_INSPECT_URL` is set, trust it.
15
- * 2. Otherwise, probe a small fixed list of ports for a wire that
16
- * answers `GET /_nwire/events/recent?limit=1` with a JSON array.
17
- * 3. Cache the result for 5s so a burst of tool calls is cheap.
18
- *
19
- * For tools that have a disk equivalent (`manifest`, `hooks`,
20
- * `plugins`), we fall through to `${cwd}/.nwire/<file>.json` when no
21
- * live wire answers. That makes the inspect tools usable in a cold
22
- * project too — just run `nwire cache` once.
23
- */
24
-
25
- import { request as httpRequest } from "node:http";
26
- import { createConnection } from "node:net";
27
- import { existsSync, readFileSync } from "node:fs";
28
- import { resolve } from "node:path";
29
-
30
- // ─── Port discovery ────────────────────────────────────────────────
31
-
32
- const DEFAULT_PROBE_PORTS: readonly number[] = [
33
- 3000, 3001, 3002, 3003, 3004, 3005, 3006, 3007, 3008, 3009, 3010, 3030, 3040, 3050, 4000, 8080,
34
- ];
35
-
36
- const PROBE_TIMEOUT_MS = 150;
37
- const CACHE_TTL_MS = 5_000;
38
- const NEGATIVE_CACHE_TTL_MS = 1_500;
39
-
40
- interface DiscoveredUrl {
41
- readonly url: string | undefined;
42
- readonly expires: number;
43
- }
44
-
45
- let discovered: DiscoveredUrl = { url: undefined, expires: 0 };
46
-
47
- /** Reset the URL cache. Tests use this to flush between cases. */
48
- export function resetInspectDiscoveryCache(): void {
49
- discovered = { url: undefined, expires: 0 };
50
- }
51
-
52
- /**
53
- * Discover the base URL of a running wire. Honours `NWIRE_INSPECT_URL`
54
- * first (and does NOT validate it — callers asked for it explicitly),
55
- * then probes a fixed list of ports.
56
- */
57
- export async function discoverInspectUrl(): Promise<string | undefined> {
58
- if (process.env.NWIRE_INSPECT_URL) {
59
- return process.env.NWIRE_INSPECT_URL.replace(/\/$/, "");
60
- }
61
- if (Date.now() < discovered.expires) return discovered.url;
62
- const ports = parsePorts(process.env.NWIRE_PROBE_PORTS) ?? DEFAULT_PROBE_PORTS;
63
- for (const port of ports) {
64
- if (await isNwireWire(port, PROBE_TIMEOUT_MS)) {
65
- const url = `http://127.0.0.1:${port}`;
66
- discovered = { url, expires: Date.now() + CACHE_TTL_MS };
67
- return url;
68
- }
69
- }
70
- discovered = { url: undefined, expires: Date.now() + NEGATIVE_CACHE_TTL_MS };
71
- return undefined;
72
- }
73
-
74
- function parsePorts(raw: string | undefined): readonly number[] | undefined {
75
- if (!raw) return undefined;
76
- const ports = raw
77
- .split(",")
78
- .map((s) => Number(s.trim()))
79
- .filter((n) => n > 0);
80
- return ports.length > 0 ? ports : undefined;
81
- }
82
-
83
- /**
84
- * Probe one port: open a socket, then GET `/_nwire/events/recent?limit=1`
85
- * and require a JSON-array body. This rejects unrelated services on the
86
- * same port (TCP-only probes were too permissive).
87
- */
88
- function isNwireWire(port: number, timeoutMs: number): Promise<boolean> {
89
- return new Promise((resolveProbe) => {
90
- const sock = createConnection({ host: "127.0.0.1", port });
91
- sock.setTimeout(timeoutMs, () => {
92
- sock.destroy();
93
- resolveProbe(false);
94
- });
95
- sock.once("error", () => resolveProbe(false));
96
- sock.once("connect", () => {
97
- sock.destroy();
98
- const req = httpRequest(
99
- { host: "127.0.0.1", port, path: "/_nwire/events/recent?limit=1", method: "GET" },
100
- (res) => {
101
- if (res.statusCode !== 200) {
102
- res.resume();
103
- return resolveProbe(false);
104
- }
105
- let buf = "";
106
- res.setEncoding("utf8");
107
- res.on("data", (c: string) => {
108
- buf += c;
109
- if (buf.length > 1024) res.destroy();
110
- });
111
- res.on("end", () => resolveProbe(buf.trimStart().startsWith("[")));
112
- },
113
- );
114
- req.setTimeout(timeoutMs, () => {
115
- req.destroy();
116
- resolveProbe(false);
117
- });
118
- req.once("error", () => resolveProbe(false));
119
- req.end();
120
- });
121
- });
122
- }
123
-
124
- // ─── HTTP helpers ──────────────────────────────────────────────────
125
-
126
- async function httpGetJson(url: string): Promise<unknown> {
127
- return new Promise((resolveGet, rejectGet) => {
128
- const u = new URL(url);
129
- const req = httpRequest(
130
- { host: u.hostname, port: u.port || 80, path: u.pathname + u.search, method: "GET" },
131
- (res) => {
132
- let buf = "";
133
- res.setEncoding("utf8");
134
- res.on("data", (c: string) => {
135
- buf += c;
136
- });
137
- res.on("end", () => {
138
- if ((res.statusCode ?? 0) >= 400) {
139
- return rejectGet(new Error(`GET ${url} → ${res.statusCode}`));
140
- }
141
- try {
142
- resolveGet(buf.length === 0 ? undefined : JSON.parse(buf));
143
- } catch (err) {
144
- rejectGet(new Error(`GET ${url} returned non-JSON: ${(err as Error).message}`));
145
- }
146
- });
147
- },
148
- );
149
- req.setTimeout(5_000, () => {
150
- req.destroy();
151
- rejectGet(new Error(`GET ${url} timed out`));
152
- });
153
- req.once("error", rejectGet);
154
- req.end();
155
- });
156
- }
157
-
158
- // ─── Disk fallback ─────────────────────────────────────────────────
159
-
160
- function readDiskCache(file: string): unknown | undefined {
161
- const path = resolve(process.cwd(), ".nwire", file);
162
- if (!existsSync(path)) return undefined;
163
- try {
164
- return JSON.parse(readFileSync(path, "utf8"));
165
- } catch {
166
- return undefined;
167
- }
168
- }
169
-
170
- // ─── Tool registry ─────────────────────────────────────────────────
171
-
172
- export interface InspectToolDef {
173
- readonly name: string;
174
- readonly description: string;
175
- readonly inputSchema: Record<string, unknown>;
176
- readonly run: (args: Record<string, unknown>) => Promise<unknown>;
177
- }
178
-
179
- /**
180
- * Resolve the full manifest. Live wire wins; otherwise disk cache.
181
- * Throws a helpful error if neither is available.
182
- */
183
- async function getManifest(): Promise<Record<string, unknown>> {
184
- const url = await discoverInspectUrl();
185
- if (url) {
186
- try {
187
- return (await httpGetJson(`${url}/_nwire/manifest`)) as Record<string, unknown>;
188
- } catch {
189
- // fall through to disk
190
- }
191
- }
192
- const disk = readDiskCache("manifest.json");
193
- if (disk) return disk as Record<string, unknown>;
194
- throw new Error(
195
- "no running wire and no .nwire/manifest.json on disk. Start a wire or run `nwire cache`.",
196
- );
197
- }
198
-
199
- function filterByModule<T extends { module?: string }>(
200
- items: readonly T[],
201
- module: unknown,
202
- ): readonly T[] {
203
- if (typeof module !== "string" || module.length === 0) return items;
204
- return items.filter((it) => it.module === module);
205
- }
206
-
207
- export const inspectTools: readonly InspectToolDef[] = [
208
- {
209
- name: "manifest",
210
- description:
211
- "Return the full .nwire manifest of the running wire (live HTTP) or the on-disk cache.",
212
- inputSchema: { type: "object", additionalProperties: false, properties: {} },
213
- async run() {
214
- return getManifest();
215
- },
216
- },
217
- {
218
- name: "list_actions",
219
- description: 'List actions exposed by the wire. Optional `module` filter (e.g. "submissions").',
220
- inputSchema: {
221
- type: "object",
222
- additionalProperties: false,
223
- properties: { module: { type: "string" } },
224
- },
225
- async run(args) {
226
- const manifest = await getManifest();
227
- const actions = (manifest.actions ?? readDiskCache("actions.json") ?? []) as Array<{
228
- module?: string;
229
- }>;
230
- return filterByModule(actions, args.module);
231
- },
232
- },
233
- {
234
- name: "list_events",
235
- description: "List events declared by the wire. Optional `module` filter.",
236
- inputSchema: {
237
- type: "object",
238
- additionalProperties: false,
239
- properties: { module: { type: "string" } },
240
- },
241
- async run(args) {
242
- const manifest = await getManifest();
243
- const events = (manifest.events ?? readDiskCache("events.json") ?? []) as Array<{
244
- module?: string;
245
- }>;
246
- return filterByModule(events, args.module);
247
- },
248
- },
249
- {
250
- name: "list_hooks",
251
- description: "List runtime hooks (reads .nwire/hooks.json from disk).",
252
- inputSchema: { type: "object", additionalProperties: false, properties: {} },
253
- async run() {
254
- // hooks.json lives on disk; the running wire does not currently
255
- // expose it via /_nwire (it's scanner output, not runtime state).
256
- const disk = readDiskCache("hooks.json");
257
- if (disk) return disk;
258
- // Try the manifest as a last resort — some scans embed hooks there.
259
- try {
260
- const manifest = await getManifest();
261
- return manifest.hooks ?? [];
262
- } catch {
263
- throw new Error(".nwire/hooks.json not found. Run `nwire cache`.");
264
- }
265
- },
266
- },
267
- {
268
- name: "list_plugins",
269
- description:
270
- 'List discovered plugins (reads .nwire/plugins.json). Optional `kind` filter: "plugin" | "module".',
271
- inputSchema: {
272
- type: "object",
273
- additionalProperties: false,
274
- properties: { kind: { type: "string", enum: ["plugin", "module"] } },
275
- },
276
- async run(args) {
277
- let plugins = readDiskCache("plugins.json") as Array<{ kind?: string }> | undefined;
278
- if (!plugins) {
279
- try {
280
- const manifest = await getManifest();
281
- plugins = (manifest.plugins ?? []) as Array<{ kind?: string }>;
282
- } catch {
283
- throw new Error(".nwire/plugins.json not found. Run `nwire cache`.");
284
- }
285
- }
286
- if (typeof args.kind === "string" && args.kind.length > 0) {
287
- return plugins.filter((p) => p.kind === args.kind);
288
- }
289
- return plugins;
290
- },
291
- },
292
- {
293
- name: "recent_events",
294
- description: "GET /_nwire/events/recent?limit=N from the running wire. Requires a live wire.",
295
- inputSchema: {
296
- type: "object",
297
- additionalProperties: false,
298
- properties: { limit: { type: "number", minimum: 1, maximum: 10_000 } },
299
- },
300
- async run(args) {
301
- const url = await discoverInspectUrl();
302
- if (!url) throw new Error("no running wire detected; recent_events requires a live wire.");
303
- const limit = typeof args.limit === "number" ? args.limit : 100;
304
- return httpGetJson(`${url}/_nwire/events/recent?limit=${limit}`);
305
- },
306
- },
307
- {
308
- name: "recent_telemetry",
309
- description:
310
- "GET /_nwire/telemetry/recent?limit=N from the running wire. Requires a live wire.",
311
- inputSchema: {
312
- type: "object",
313
- additionalProperties: false,
314
- properties: { limit: { type: "number", minimum: 1, maximum: 10_000 } },
315
- },
316
- async run(args) {
317
- const url = await discoverInspectUrl();
318
- if (!url) {
319
- throw new Error("no running wire detected; recent_telemetry requires a live wire.");
320
- }
321
- const limit = typeof args.limit === "number" ? args.limit : 100;
322
- return httpGetJson(`${url}/_nwire/telemetry/recent?limit=${limit}`);
323
- },
324
- },
325
- ];
326
-
327
- export function findInspectTool(name: string): InspectToolDef | undefined {
328
- return inspectTools.find((t) => t.name === name);
329
- }