@nwire/mcp 0.9.1 → 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.
@@ -1,237 +0,0 @@
1
- /**
2
- * Write tools — the mutating counterpart to `inspect.ts`.
3
- *
4
- * Where the inspect tools proxy `GET /_nwire/*` (or fall back to the
5
- * `.nwire/` disk cache), write tools drive the running wire through its
6
- * mutating endpoints:
7
- *
8
- * POST /_nwire/dispatch — invoke a registered action ← `dispatch_action`
9
- * POST /_nwire/command — invoke a defineCommand ← NOT YET MOUNTED
10
- * POST /_nwire/replay — replay a hook recording ← NOT YET MOUNTED
11
- *
12
- * The wire ships only `/_nwire/dispatch` today (see
13
- * `packages/nwire-http/src/inspect.ts`). For tools whose endpoint does
14
- * not yet exist, we ship the tool ANYWAY so the contract is visible in
15
- * `tools/list`, but the call returns `isError: true` with an explicit
16
- * "endpoint not implemented on wire side" message rather than
17
- * synthesizing a fake response. The companion follow-up in
18
- * `@nwire/http` will mount the missing routes.
19
- *
20
- * These tools register alongside `inspectTools` and reuse the same URL
21
- * discovery cache (`discoverInspectUrl`). They do NOT modify the
22
- * existing read-only surface.
23
- */
24
-
25
- import { request as httpRequest } from "node:http";
26
- import { discoverInspectUrl, type InspectToolDef } from "./inspect.js";
27
-
28
- // ─── HTTP helpers ──────────────────────────────────────────────────
29
-
30
- interface HttpResult {
31
- readonly status: number;
32
- readonly body: unknown;
33
- }
34
-
35
- /**
36
- * POST JSON to a URL. Resolves with `{ status, body }` even for 4xx/5xx
37
- * — the caller decides what to do with non-2xx responses, because the
38
- * wire returns useful diagnostics in the body (e.g. action-not-found).
39
- */
40
- async function httpPostJson(url: string, payload: unknown): Promise<HttpResult> {
41
- return new Promise((resolvePost, rejectPost) => {
42
- const u = new URL(url);
43
- const data = JSON.stringify(payload ?? {});
44
- const req = httpRequest(
45
- {
46
- host: u.hostname,
47
- port: u.port || 80,
48
- path: u.pathname + u.search,
49
- method: "POST",
50
- headers: {
51
- "Content-Type": "application/json",
52
- "Content-Length": Buffer.byteLength(data).toString(),
53
- },
54
- },
55
- (res) => {
56
- let buf = "";
57
- res.setEncoding("utf8");
58
- res.on("data", (c: string) => {
59
- buf += c;
60
- });
61
- res.on("end", () => {
62
- const status = res.statusCode ?? 0;
63
- if (buf.length === 0) return resolvePost({ status, body: undefined });
64
- try {
65
- resolvePost({ status, body: JSON.parse(buf) });
66
- } catch {
67
- // Non-JSON body — surface it raw so callers see the truth.
68
- resolvePost({ status, body: buf });
69
- }
70
- });
71
- },
72
- );
73
- req.setTimeout(10_000, () => {
74
- req.destroy();
75
- rejectPost(new Error(`POST ${url} timed out`));
76
- });
77
- req.once("error", rejectPost);
78
- req.write(data);
79
- req.end();
80
- });
81
- }
82
-
83
- // ─── Tools ─────────────────────────────────────────────────────────
84
-
85
- const NO_WIRE =
86
- "no running wire detected; this tool requires a live wire (set NWIRE_INSPECT_URL or start one).";
87
-
88
- /**
89
- * Invoke a registered action via `POST /_nwire/dispatch`. Returns the
90
- * wire's envelope + result on success; surfaces the wire's diagnostic
91
- * body on 4xx/5xx without rewriting it.
92
- */
93
- const dispatchAction: InspectToolDef = {
94
- name: "dispatch_action",
95
- description:
96
- "Invoke a registered action on the running wire (POST /_nwire/dispatch). " +
97
- "Args: { action: string, input: object, user?: { id, roles? } }. " +
98
- "Returns the action result + correlation envelope, or the wire's error body on failure.",
99
- inputSchema: {
100
- type: "object",
101
- additionalProperties: false,
102
- required: ["action"],
103
- properties: {
104
- action: { type: "string" },
105
- input: { type: "object", additionalProperties: true },
106
- user: {
107
- type: "object",
108
- additionalProperties: false,
109
- properties: {
110
- id: { type: "string" },
111
- roles: { type: "array", items: { type: "string" } },
112
- },
113
- },
114
- tenant: { type: "string" },
115
- },
116
- },
117
- async run(args) {
118
- const url = await discoverInspectUrl();
119
- if (!url) throw new Error(NO_WIRE);
120
- const action = args.action as string | undefined;
121
- if (!action) throw new Error("dispatch_action requires `action`");
122
- const user = args.user as { id?: string; roles?: string[] } | undefined;
123
- const tenant = args.tenant as string | undefined;
124
- const payload = {
125
- action,
126
- input: args.input ?? {},
127
- ...(user?.id !== undefined && { userId: user.id }),
128
- ...(tenant !== undefined && { tenant }),
129
- };
130
- const { status, body } = await httpPostJson(`${url}/_nwire/dispatch`, payload);
131
- if (status >= 400) {
132
- throw new Error(
133
- `dispatch failed (${status}): ${typeof body === "string" ? body : JSON.stringify(body)}`,
134
- );
135
- }
136
- return body;
137
- },
138
- };
139
-
140
- /**
141
- * Invoke a `defineCommand` on the wire. The wire does NOT currently
142
- * expose a `/_nwire/command` endpoint (see
143
- * `packages/nwire-http/src/inspect.ts`); this tool gap-flags until that
144
- * endpoint is mounted. The intended shape is documented in the error.
145
- */
146
- const runCommand: InspectToolDef = {
147
- name: "run_command",
148
- description:
149
- "Invoke a registered defineCommand on the running wire by name. " +
150
- "Args: { name: string, args: object }. " +
151
- "NOTE: the wire-side endpoint (/_nwire/command) is not yet mounted — see gap-flag in tool output.",
152
- inputSchema: {
153
- type: "object",
154
- additionalProperties: false,
155
- required: ["name"],
156
- properties: {
157
- name: { type: "string" },
158
- args: { type: "object", additionalProperties: true },
159
- },
160
- },
161
- async run(args) {
162
- const url = await discoverInspectUrl();
163
- if (!url) throw new Error(NO_WIRE);
164
- const name = args.name as string | undefined;
165
- if (!name) throw new Error("run_command requires `name`");
166
-
167
- // Try the canonical endpoint first; if the wire returns 404 we surface
168
- // a clear gap message rather than silently faking something.
169
- const { status, body } = await httpPostJson(`${url}/_nwire/command`, {
170
- name,
171
- args: args.args ?? {},
172
- });
173
- if (status === 404) {
174
- throw new Error(
175
- "endpoint not implemented yet on wire side: POST /_nwire/command. " +
176
- "See packages/nwire-http/src/inspect.ts — only /_nwire/dispatch is mounted today. " +
177
- "Workaround: invoke kernel commands via the CLI (`nwire please ...`) until the route lands.",
178
- );
179
- }
180
- if (status >= 400) {
181
- throw new Error(
182
- `run_command failed (${status}): ${typeof body === "string" ? body : JSON.stringify(body)}`,
183
- );
184
- }
185
- return body;
186
- },
187
- };
188
-
189
- /**
190
- * Replay a `@nwire/hooks` recording on the wire. Same gap as
191
- * `run_command` — `/_nwire/replay` is not yet mounted; tool ships so
192
- * the contract is visible.
193
- */
194
- const replayTrace: InspectToolDef = {
195
- name: "replay_trace",
196
- description:
197
- "Replay a @nwire/hooks recording on the running wire (POST /_nwire/replay). " +
198
- "Args: { recording: object } where `recording` is the output of `record()`. " +
199
- "NOTE: the wire-side endpoint (/_nwire/replay) is not yet mounted — see gap-flag in tool output.",
200
- inputSchema: {
201
- type: "object",
202
- additionalProperties: false,
203
- required: ["recording"],
204
- properties: {
205
- recording: { type: "object", additionalProperties: true },
206
- },
207
- },
208
- async run(args) {
209
- const url = await discoverInspectUrl();
210
- if (!url) throw new Error(NO_WIRE);
211
- const recording = args.recording as Record<string, unknown> | undefined;
212
- if (!recording || typeof recording !== "object") {
213
- throw new Error("replay_trace requires `recording` (object from @nwire/hooks record()).");
214
- }
215
-
216
- const { status, body } = await httpPostJson(`${url}/_nwire/replay`, { recording });
217
- if (status === 404) {
218
- throw new Error(
219
- "endpoint not implemented yet on wire side: POST /_nwire/replay. " +
220
- "See packages/nwire-http/src/inspect.ts — recordings can still be replayed in-process via " +
221
- "`replay()` from @nwire/hooks; HTTP replay against a running wire requires the new route.",
222
- );
223
- }
224
- if (status >= 400) {
225
- throw new Error(
226
- `replay_trace failed (${status}): ${typeof body === "string" ? body : JSON.stringify(body)}`,
227
- );
228
- }
229
- return body;
230
- },
231
- };
232
-
233
- export const writeTools: readonly InspectToolDef[] = [dispatchAction, runCommand, replayTrace];
234
-
235
- export function findWriteTool(name: string): InspectToolDef | undefined {
236
- return writeTools.find((t) => t.name === name);
237
- }