@mushinjs/plugin-mcp 0.0.0-stage → 0.2.0-next.20261112165327

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Setobox
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,110 @@
1
+ /** The protocol version this client asks for; servers answer with the one they speak. */
2
+ export declare const PROTOCOL_VERSION = "2025-06-18";
3
+ export interface McpTool {
4
+ name: string;
5
+ title?: string;
6
+ description?: string;
7
+ inputSchema: Record<string, any>;
8
+ annotations?: {
9
+ readOnlyHint?: boolean;
10
+ destructiveHint?: boolean;
11
+ title?: string;
12
+ };
13
+ }
14
+ export interface McpCallResult {
15
+ content?: {
16
+ type: string;
17
+ text?: string;
18
+ [key: string]: unknown;
19
+ }[];
20
+ structuredContent?: unknown;
21
+ isError?: boolean;
22
+ }
23
+ interface JsonRpcMessage {
24
+ jsonrpc: '2.0';
25
+ id?: number | string;
26
+ method?: string;
27
+ params?: any;
28
+ result?: any;
29
+ error?: {
30
+ code: number;
31
+ message: string;
32
+ data?: unknown;
33
+ };
34
+ }
35
+ /** Sends JSON-RPC messages to an MCP server and hands back what it sends. */
36
+ interface Transport {
37
+ start(onMessage: (message: JsonRpcMessage) => void, onClose: (error?: Error) => void): Promise<void>;
38
+ /** Sends a message; an HTTP transport may answer it directly. */
39
+ send(message: JsonRpcMessage): Promise<void>;
40
+ close(): Promise<void>;
41
+ /** Lines the server wrote to stderr, for the status page. */
42
+ stderr?: string[];
43
+ }
44
+ export interface StdioOptions {
45
+ command: string;
46
+ args?: string[];
47
+ env?: Record<string, string>;
48
+ cwd?: string;
49
+ }
50
+ /** Runs the server as a child process and talks over stdin and stdout, one JSON message per line. */
51
+ export declare class StdioTransport implements Transport {
52
+ private options;
53
+ private child?;
54
+ stderr: string[];
55
+ constructor(options: StdioOptions);
56
+ start(onMessage: (message: JsonRpcMessage) => void, onClose: (error?: Error) => void): Promise<void>;
57
+ private note;
58
+ send(message: JsonRpcMessage): Promise<void>;
59
+ close(): Promise<void>;
60
+ }
61
+ export interface HttpOptions {
62
+ url: string;
63
+ headers?: Record<string, string>;
64
+ }
65
+ /** Streamable HTTP: every message is a POST; the answer is JSON or a short event stream. */
66
+ export declare class HttpTransport implements Transport {
67
+ private options;
68
+ private session?;
69
+ private version?;
70
+ private onMessage?;
71
+ private aborts;
72
+ constructor(options: HttpOptions);
73
+ start(onMessage: (message: JsonRpcMessage) => void): Promise<void>;
74
+ /** Sets the protocol version header after initialization. */
75
+ setVersion(version: string): void;
76
+ send(message: JsonRpcMessage): Promise<void>;
77
+ /** Reads server-sent events until the answer to `id` arrives. */
78
+ private readEvents;
79
+ close(): Promise<void>;
80
+ }
81
+ /** One connection to an MCP server: initialize, list tools, call tools. */
82
+ export declare class McpClient {
83
+ private transport;
84
+ private hooks;
85
+ private timeoutMs;
86
+ private next;
87
+ private pending;
88
+ server?: {
89
+ name?: string;
90
+ version?: string;
91
+ };
92
+ instructions?: string;
93
+ closed: boolean;
94
+ constructor(transport: Transport, hooks?: {
95
+ onToolsChanged?: () => void;
96
+ onClose?: (error?: Error) => void;
97
+ }, timeoutMs?: number);
98
+ get stderr(): string[];
99
+ connect(): Promise<void>;
100
+ listTools(): Promise<McpTool[]>;
101
+ callTool(name: string, args: unknown, signal?: AbortSignal): Promise<McpCallResult>;
102
+ close(): Promise<void>;
103
+ private request;
104
+ private receive;
105
+ private fail;
106
+ private rejectAll;
107
+ }
108
+ /** The text of a tool result, for the Agent: text parts joined, other parts summarized. */
109
+ export declare function resultText(result: McpCallResult): string;
110
+ export {};
package/dist/client.js ADDED
@@ -0,0 +1,322 @@
1
+ import { spawn } from "node:child_process";
2
+ // src/client.ts
3
+ /** The protocol version this client asks for; servers answer with the one they speak. */
4
+ const PROTOCOL_VERSION = "2025-06-18";
5
+ /** Runs the server as a child process and talks over stdin and stdout, one JSON message per line. */
6
+ var StdioTransport = class {
7
+ options;
8
+ child;
9
+ stderr = [];
10
+ constructor(options) {
11
+ this.options = options;
12
+ }
13
+ start(onMessage, onClose) {
14
+ return new Promise((resolve, reject) => {
15
+ const child = spawn(this.options.command, this.options.args ?? [], {
16
+ cwd: this.options.cwd || void 0,
17
+ env: {
18
+ ...process.env,
19
+ ...this.options.env
20
+ },
21
+ stdio: [
22
+ "pipe",
23
+ "pipe",
24
+ "pipe"
25
+ ],
26
+ shell: process.platform === "win32",
27
+ windowsHide: true
28
+ });
29
+ this.child = child;
30
+ let buffer = "";
31
+ child.stdout.setEncoding("utf8");
32
+ child.stdout.on("data", (chunk) => {
33
+ buffer += chunk;
34
+ let index;
35
+ while ((index = buffer.indexOf("\n")) >= 0) {
36
+ const line = buffer.slice(0, index).trim();
37
+ buffer = buffer.slice(index + 1);
38
+ if (!line) continue;
39
+ try {
40
+ onMessage(JSON.parse(line));
41
+ } catch {
42
+ this.note(`not JSON on stdout: ${line.slice(0, 200)}`);
43
+ }
44
+ }
45
+ });
46
+ child.stderr.setEncoding("utf8");
47
+ child.stderr.on("data", (chunk) => {
48
+ for (const line of chunk.split("\n")) if (line.trim()) this.note(line);
49
+ });
50
+ child.once("spawn", () => resolve());
51
+ child.once("error", (error) => {
52
+ reject(error);
53
+ onClose(error);
54
+ });
55
+ child.once("exit", (code, signal) => onClose(/* @__PURE__ */ new Error(`the server exited (${signal ?? `code ${code}`})`)));
56
+ });
57
+ }
58
+ note(line) {
59
+ this.stderr.push(line.slice(0, 500));
60
+ if (this.stderr.length > 50) this.stderr.shift();
61
+ }
62
+ async send(message) {
63
+ if (!this.child?.stdin?.writable) throw new Error("the server is not running");
64
+ this.child.stdin.write(`${JSON.stringify(message)}\n`);
65
+ }
66
+ async close() {
67
+ const child = this.child;
68
+ this.child = void 0;
69
+ if (!child || child.exitCode !== null) return;
70
+ child.stdin?.end();
71
+ const exited = new Promise((resolve) => child.once("exit", () => resolve()));
72
+ const timer = setTimeout(() => child.kill(), 2e3);
73
+ await exited;
74
+ clearTimeout(timer);
75
+ }
76
+ };
77
+ /** Streamable HTTP: every message is a POST; the answer is JSON or a short event stream. */
78
+ var HttpTransport = class {
79
+ options;
80
+ session;
81
+ version;
82
+ onMessage;
83
+ aborts = /* @__PURE__ */ new Set();
84
+ constructor(options) {
85
+ this.options = options;
86
+ }
87
+ async start(onMessage) {
88
+ this.onMessage = onMessage;
89
+ }
90
+ /** Sets the protocol version header after initialization. */
91
+ setVersion(version) {
92
+ this.version = version;
93
+ }
94
+ async send(message) {
95
+ const abort = new AbortController();
96
+ this.aborts.add(abort);
97
+ try {
98
+ const response = await fetch(this.options.url, {
99
+ method: "POST",
100
+ headers: {
101
+ ...this.options.headers,
102
+ "content-type": "application/json",
103
+ accept: "application/json, text/event-stream",
104
+ ...this.session ? { "mcp-session-id": this.session } : {},
105
+ ...this.version ? { "mcp-protocol-version": this.version } : {}
106
+ },
107
+ body: JSON.stringify(message),
108
+ signal: abort.signal
109
+ });
110
+ const session = response.headers.get("mcp-session-id");
111
+ if (session) this.session = session;
112
+ if (response.status === 202 || response.status === 204) return;
113
+ if (!response.ok) {
114
+ const text = await response.text().catch(() => "");
115
+ throw new Error(`HTTP ${response.status}${text ? `: ${text.slice(0, 200)}` : ""}`);
116
+ }
117
+ const type = response.headers.get("content-type") ?? "";
118
+ if (type.includes("text/event-stream")) await this.readEvents(response, message.id);
119
+ else if (type.includes("application/json")) {
120
+ const body = await response.json();
121
+ for (const item of Array.isArray(body) ? body : [body]) this.onMessage?.(item);
122
+ }
123
+ } finally {
124
+ this.aborts.delete(abort);
125
+ }
126
+ }
127
+ /** Reads server-sent events until the answer to `id` arrives. */
128
+ async readEvents(response, id) {
129
+ const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
130
+ let buffer = "";
131
+ try {
132
+ while (true) {
133
+ const { value, done } = await reader.read();
134
+ if (done) return;
135
+ buffer += value.replace(/\r\n/g, "\n");
136
+ let index;
137
+ while ((index = buffer.indexOf("\n\n")) >= 0) {
138
+ const block = buffer.slice(0, index);
139
+ buffer = buffer.slice(index + 2);
140
+ const data = block.split("\n").filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trimStart()).join("\n");
141
+ if (!data) continue;
142
+ const message = JSON.parse(data);
143
+ this.onMessage?.(message);
144
+ if (id !== void 0 && message.id === id && !message.method) return;
145
+ }
146
+ }
147
+ } finally {
148
+ reader.cancel().catch(() => {});
149
+ }
150
+ }
151
+ async close() {
152
+ for (const abort of this.aborts) abort.abort();
153
+ if (this.session) await fetch(this.options.url, {
154
+ method: "DELETE",
155
+ headers: {
156
+ ...this.options.headers,
157
+ "mcp-session-id": this.session
158
+ }
159
+ }).catch(() => {});
160
+ }
161
+ };
162
+ /** One connection to an MCP server: initialize, list tools, call tools. */
163
+ var McpClient = class {
164
+ transport;
165
+ hooks;
166
+ timeoutMs;
167
+ next = 1;
168
+ pending = /* @__PURE__ */ new Map();
169
+ server;
170
+ instructions;
171
+ closed = false;
172
+ constructor(transport, hooks = {}, timeoutMs = 6e4) {
173
+ this.transport = transport;
174
+ this.hooks = hooks;
175
+ this.timeoutMs = timeoutMs;
176
+ }
177
+ get stderr() {
178
+ return this.transport.stderr ?? [];
179
+ }
180
+ async connect() {
181
+ await this.transport.start((message) => this.receive(message), (error) => this.fail(error));
182
+ const result = await this.request("initialize", {
183
+ protocolVersion: PROTOCOL_VERSION,
184
+ capabilities: {},
185
+ clientInfo: {
186
+ name: "mushin",
187
+ title: "Mushin.js",
188
+ version: "0.1.0"
189
+ }
190
+ });
191
+ this.server = result?.serverInfo;
192
+ this.instructions = result?.instructions;
193
+ if (this.transport instanceof HttpTransport) this.transport.setVersion(result?.protocolVersion ?? "2025-06-18");
194
+ await this.transport.send({
195
+ jsonrpc: "2.0",
196
+ method: "notifications/initialized"
197
+ });
198
+ }
199
+ async listTools() {
200
+ const tools = [];
201
+ let cursor;
202
+ do {
203
+ const page = await this.request("tools/list", cursor ? { cursor } : {});
204
+ tools.push(...page?.tools ?? []);
205
+ cursor = page?.nextCursor;
206
+ } while (cursor);
207
+ return tools;
208
+ }
209
+ callTool(name, args, signal) {
210
+ return this.request("tools/call", {
211
+ name,
212
+ arguments: args ?? {}
213
+ }, signal);
214
+ }
215
+ async close() {
216
+ if (this.closed) return;
217
+ this.closed = true;
218
+ await this.transport.close().catch(() => {});
219
+ this.rejectAll(/* @__PURE__ */ new Error("the connection was closed"));
220
+ }
221
+ request(method, params, signal) {
222
+ if (this.closed) return Promise.reject(/* @__PURE__ */ new Error("the connection is closed"));
223
+ const id = this.next++;
224
+ return new Promise((resolve, reject) => {
225
+ const timer = setTimeout(() => {
226
+ this.pending.delete(id);
227
+ this.transport.send({
228
+ jsonrpc: "2.0",
229
+ method: "notifications/cancelled",
230
+ params: {
231
+ requestId: id,
232
+ reason: "timeout"
233
+ }
234
+ }).catch(() => {});
235
+ reject(/* @__PURE__ */ new Error(`${method} timed out after ${Math.round(this.timeoutMs / 1e3)}s`));
236
+ }, this.timeoutMs);
237
+ this.pending.set(id, {
238
+ resolve,
239
+ reject,
240
+ timer
241
+ });
242
+ signal?.addEventListener("abort", () => {
243
+ const entry = this.pending.get(id);
244
+ if (!entry) return;
245
+ clearTimeout(entry.timer);
246
+ this.pending.delete(id);
247
+ this.transport.send({
248
+ jsonrpc: "2.0",
249
+ method: "notifications/cancelled",
250
+ params: {
251
+ requestId: id,
252
+ reason: "cancelled"
253
+ }
254
+ }).catch(() => {});
255
+ reject(/* @__PURE__ */ new Error("cancelled"));
256
+ }, { once: true });
257
+ this.transport.send({
258
+ jsonrpc: "2.0",
259
+ id,
260
+ method,
261
+ params
262
+ }).catch((error) => {
263
+ const entry = this.pending.get(id);
264
+ if (!entry) return;
265
+ clearTimeout(entry.timer);
266
+ this.pending.delete(id);
267
+ reject(error);
268
+ });
269
+ });
270
+ }
271
+ receive(message) {
272
+ if (message.method) {
273
+ if (message.id !== void 0) {
274
+ const reply = message.method === "ping" ? {
275
+ jsonrpc: "2.0",
276
+ id: message.id,
277
+ result: {}
278
+ } : {
279
+ jsonrpc: "2.0",
280
+ id: message.id,
281
+ error: {
282
+ code: -32601,
283
+ message: `${message.method} is not supported`
284
+ }
285
+ };
286
+ this.transport.send(reply).catch(() => {});
287
+ } else if (message.method === "notifications/tools/list_changed") this.hooks.onToolsChanged?.();
288
+ return;
289
+ }
290
+ const entry = typeof message.id === "number" ? this.pending.get(message.id) : void 0;
291
+ if (!entry) return;
292
+ clearTimeout(entry.timer);
293
+ this.pending.delete(message.id);
294
+ if (message.error) entry.reject(new Error(message.error.message || `error ${message.error.code}`));
295
+ else entry.resolve(message.result);
296
+ }
297
+ fail(error) {
298
+ if (this.closed) return;
299
+ this.closed = true;
300
+ this.rejectAll(error ?? /* @__PURE__ */ new Error("the connection was closed"));
301
+ this.hooks.onClose?.(error);
302
+ }
303
+ rejectAll(error) {
304
+ for (const entry of this.pending.values()) {
305
+ clearTimeout(entry.timer);
306
+ entry.reject(error);
307
+ }
308
+ this.pending.clear();
309
+ }
310
+ };
311
+ /** The text of a tool result, for the Agent: text parts joined, other parts summarized. */
312
+ function resultText(result) {
313
+ return (result.content ?? []).map((part) => {
314
+ if (part.type === "text") return part.text ?? "";
315
+ if (part.type === "resource" && typeof part.resource?.text === "string") return part.resource.text;
316
+ if (part.type === "resource_link") return `[${part.name ?? "resource"}](${part.uri})`;
317
+ return `[${part.type}${part.mimeType ? ` ${part.mimeType}` : ""}]`;
318
+ }).join("\n");
319
+ }
320
+ export { HttpTransport, McpClient, PROTOCOL_VERSION, StdioTransport, resultText };
321
+
322
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","names":[],"sources":["../src/client.ts"],"sourcesContent":["import { type ChildProcess, spawn } from 'node:child_process'\r\n\r\n/** The protocol version this client asks for; servers answer with the one they speak. */\r\nexport const PROTOCOL_VERSION = '2025-06-18'\r\n\r\nexport interface McpTool {\r\n name: string\r\n title?: string\r\n description?: string\r\n inputSchema: Record<string, any>\r\n annotations?: { readOnlyHint?: boolean; destructiveHint?: boolean; title?: string }\r\n}\r\n\r\nexport interface McpCallResult {\r\n content?: { type: string; text?: string; [key: string]: unknown }[]\r\n structuredContent?: unknown\r\n isError?: boolean\r\n}\r\n\r\ninterface JsonRpcMessage {\r\n jsonrpc: '2.0'\r\n id?: number | string\r\n method?: string\r\n params?: any\r\n result?: any\r\n error?: { code: number; message: string; data?: unknown }\r\n}\r\n\r\n/** Sends JSON-RPC messages to an MCP server and hands back what it sends. */\r\ninterface Transport {\r\n start(onMessage: (message: JsonRpcMessage) => void, onClose: (error?: Error) => void): Promise<void>\r\n /** Sends a message; an HTTP transport may answer it directly. */\r\n send(message: JsonRpcMessage): Promise<void>\r\n close(): Promise<void>\r\n /** Lines the server wrote to stderr, for the status page. */\r\n stderr?: string[]\r\n}\r\n\r\nexport interface StdioOptions {\r\n command: string\r\n args?: string[]\r\n env?: Record<string, string>\r\n cwd?: string\r\n}\r\n\r\n/** Runs the server as a child process and talks over stdin and stdout, one JSON message per line. */\r\nexport class StdioTransport implements Transport {\r\n private child?: ChildProcess\r\n stderr: string[] = []\r\n\r\n constructor(private options: StdioOptions) {}\r\n\r\n start(onMessage: (message: JsonRpcMessage) => void, onClose: (error?: Error) => void) {\r\n return new Promise<void>((resolve, reject) => {\r\n const child = spawn(this.options.command, this.options.args ?? [], {\r\n cwd: this.options.cwd || undefined,\r\n env: { ...process.env, ...this.options.env },\r\n stdio: ['pipe', 'pipe', 'pipe'],\r\n shell: process.platform === 'win32',\r\n windowsHide: true,\r\n })\r\n this.child = child\r\n let buffer = ''\r\n child.stdout!.setEncoding('utf8')\r\n child.stdout!.on('data', (chunk: string) => {\r\n buffer += chunk\r\n let index: number\r\n while ((index = buffer.indexOf('\\n')) >= 0) {\r\n const line = buffer.slice(0, index).trim()\r\n buffer = buffer.slice(index + 1)\r\n if (!line) continue\r\n try {\r\n onMessage(JSON.parse(line))\r\n } catch {\r\n this.note(`not JSON on stdout: ${line.slice(0, 200)}`)\r\n }\r\n }\r\n })\r\n child.stderr!.setEncoding('utf8')\r\n child.stderr!.on('data', (chunk: string) => {\r\n for (const line of chunk.split('\\n')) if (line.trim()) this.note(line)\r\n })\r\n child.once('spawn', () => resolve())\r\n child.once('error', (error) => {\r\n reject(error)\r\n onClose(error)\r\n })\r\n child.once('exit', (code, signal) => onClose(new Error(`the server exited (${signal ?? `code ${code}`})`)))\r\n })\r\n }\r\n\r\n private note(line: string) {\r\n this.stderr.push(line.slice(0, 500))\r\n if (this.stderr.length > 50) this.stderr.shift()\r\n }\r\n\r\n async send(message: JsonRpcMessage) {\r\n if (!this.child?.stdin?.writable) throw new Error('the server is not running')\r\n this.child.stdin.write(`${JSON.stringify(message)}\\n`)\r\n }\r\n\r\n async close() {\r\n const child = this.child\r\n this.child = undefined\r\n if (!child || child.exitCode !== null) return\r\n child.stdin?.end()\r\n const exited = new Promise<void>((resolve) => child.once('exit', () => resolve()))\r\n const timer = setTimeout(() => child.kill(), 2000)\r\n await exited\r\n clearTimeout(timer)\r\n }\r\n}\r\n\r\nexport interface HttpOptions {\r\n url: string\r\n headers?: Record<string, string>\r\n}\r\n\r\n/** Streamable HTTP: every message is a POST; the answer is JSON or a short event stream. */\r\nexport class HttpTransport implements Transport {\r\n private session?: string\r\n private version?: string\r\n private onMessage?: (message: JsonRpcMessage) => void\r\n private aborts = new Set<AbortController>()\r\n\r\n constructor(private options: HttpOptions) {}\r\n\r\n async start(onMessage: (message: JsonRpcMessage) => void) {\r\n this.onMessage = onMessage\r\n }\r\n\r\n /** Sets the protocol version header after initialization. */\r\n setVersion(version: string) {\r\n this.version = version\r\n }\r\n\r\n async send(message: JsonRpcMessage) {\r\n const abort = new AbortController()\r\n this.aborts.add(abort)\r\n try {\r\n const response = await fetch(this.options.url, {\r\n method: 'POST',\r\n headers: {\r\n ...this.options.headers,\r\n 'content-type': 'application/json',\r\n accept: 'application/json, text/event-stream',\r\n ...(this.session ? { 'mcp-session-id': this.session } : {}),\r\n ...(this.version ? { 'mcp-protocol-version': this.version } : {}),\r\n },\r\n body: JSON.stringify(message),\r\n signal: abort.signal,\r\n })\r\n const session = response.headers.get('mcp-session-id')\r\n if (session) this.session = session\r\n if (response.status === 202 || response.status === 204) return\r\n if (!response.ok) {\r\n const text = await response.text().catch(() => '')\r\n throw new Error(`HTTP ${response.status}${text ? `: ${text.slice(0, 200)}` : ''}`)\r\n }\r\n const type = response.headers.get('content-type') ?? ''\r\n if (type.includes('text/event-stream')) {\r\n await this.readEvents(response, message.id)\r\n } else if (type.includes('application/json')) {\r\n const body = await response.json()\r\n for (const item of Array.isArray(body) ? body : [body]) this.onMessage?.(item)\r\n }\r\n } finally {\r\n this.aborts.delete(abort)\r\n }\r\n }\r\n\r\n /** Reads server-sent events until the answer to `id` arrives. */\r\n private async readEvents(response: Response, id: JsonRpcMessage['id']) {\r\n const reader = response.body!.pipeThrough(new TextDecoderStream()).getReader()\r\n let buffer = ''\r\n try {\r\n while (true) {\r\n const { value, done } = await reader.read()\r\n if (done) return\r\n buffer += value.replace(/\\r\\n/g, '\\n')\r\n let index: number\r\n while ((index = buffer.indexOf('\\n\\n')) >= 0) {\r\n const block = buffer.slice(0, index)\r\n buffer = buffer.slice(index + 2)\r\n const data = block.split('\\n').filter((line) => line.startsWith('data:')).map((line) => line.slice(5).trimStart()).join('\\n')\r\n if (!data) continue\r\n const message = JSON.parse(data) as JsonRpcMessage\r\n this.onMessage?.(message)\r\n if (id !== undefined && message.id === id && !message.method) return\r\n }\r\n }\r\n } finally {\r\n reader.cancel().catch(() => {})\r\n }\r\n }\r\n\r\n async close() {\r\n for (const abort of this.aborts) abort.abort()\r\n if (this.session) {\r\n await fetch(this.options.url, { method: 'DELETE', headers: { ...this.options.headers, 'mcp-session-id': this.session } }).catch(() => {})\r\n }\r\n }\r\n}\r\n\r\n/** One connection to an MCP server: initialize, list tools, call tools. */\r\nexport class McpClient {\r\n private next = 1\r\n private pending = new Map<number, { resolve: (value: any) => void; reject: (error: Error) => void; timer: ReturnType<typeof setTimeout> }>()\r\n server?: { name?: string; version?: string }\r\n instructions?: string\r\n closed = false\r\n\r\n constructor(\r\n private transport: Transport,\r\n private hooks: { onToolsChanged?: () => void; onClose?: (error?: Error) => void } = {},\r\n private timeoutMs = 60_000,\r\n ) {}\r\n\r\n get stderr() {\r\n return this.transport.stderr ?? []\r\n }\r\n\r\n async connect() {\r\n await this.transport.start((message) => this.receive(message), (error) => this.fail(error))\r\n const result = await this.request('initialize', {\r\n protocolVersion: PROTOCOL_VERSION,\r\n capabilities: {},\r\n clientInfo: { name: 'mushin', title: 'Mushin.js', version: '0.1.0' },\r\n })\r\n this.server = result?.serverInfo\r\n this.instructions = result?.instructions\r\n if (this.transport instanceof HttpTransport) this.transport.setVersion(result?.protocolVersion ?? PROTOCOL_VERSION)\r\n await this.transport.send({ jsonrpc: '2.0', method: 'notifications/initialized' })\r\n }\r\n\r\n async listTools() {\r\n const tools: McpTool[] = []\r\n let cursor: string | undefined\r\n do {\r\n const page = await this.request('tools/list', cursor ? { cursor } : {})\r\n tools.push(...page?.tools ?? [])\r\n cursor = page?.nextCursor\r\n } while (cursor)\r\n return tools\r\n }\r\n\r\n callTool(name: string, args: unknown, signal?: AbortSignal): Promise<McpCallResult> {\r\n return this.request('tools/call', { name, arguments: args ?? {} }, signal)\r\n }\r\n\r\n async close() {\r\n if (this.closed) return\r\n this.closed = true\r\n await this.transport.close().catch(() => {})\r\n this.rejectAll(new Error('the connection was closed'))\r\n }\r\n\r\n private request(method: string, params: unknown, signal?: AbortSignal) {\r\n if (this.closed) return Promise.reject(new Error('the connection is closed'))\r\n const id = this.next++\r\n return new Promise<any>((resolve, reject) => {\r\n const timer = setTimeout(() => {\r\n this.pending.delete(id)\r\n this.transport.send({ jsonrpc: '2.0', method: 'notifications/cancelled', params: { requestId: id, reason: 'timeout' } }).catch(() => {})\r\n reject(new Error(`${method} timed out after ${Math.round(this.timeoutMs / 1000)}s`))\r\n }, this.timeoutMs)\r\n this.pending.set(id, { resolve, reject, timer })\r\n signal?.addEventListener('abort', () => {\r\n const entry = this.pending.get(id)\r\n if (!entry) return\r\n clearTimeout(entry.timer)\r\n this.pending.delete(id)\r\n this.transport.send({ jsonrpc: '2.0', method: 'notifications/cancelled', params: { requestId: id, reason: 'cancelled' } }).catch(() => {})\r\n reject(new Error('cancelled'))\r\n }, { once: true })\r\n this.transport.send({ jsonrpc: '2.0', id, method, params }).catch((error) => {\r\n const entry = this.pending.get(id)\r\n if (!entry) return\r\n clearTimeout(entry.timer)\r\n this.pending.delete(id)\r\n reject(error)\r\n })\r\n })\r\n }\r\n\r\n private receive(message: JsonRpcMessage) {\r\n if (message.method) {\r\n if (message.id !== undefined) {\r\n // Requests from the server: answer pings, decline the rest (no sampling, roots or elicitation).\r\n const reply = message.method === 'ping'\r\n ? { jsonrpc: '2.0' as const, id: message.id, result: {} }\r\n : { jsonrpc: '2.0' as const, id: message.id, error: { code: -32601, message: `${message.method} is not supported` } }\r\n this.transport.send(reply).catch(() => {})\r\n } else if (message.method === 'notifications/tools/list_changed') {\r\n this.hooks.onToolsChanged?.()\r\n }\r\n return\r\n }\r\n const entry = typeof message.id === 'number' ? this.pending.get(message.id) : undefined\r\n if (!entry) return\r\n clearTimeout(entry.timer)\r\n this.pending.delete(message.id as number)\r\n if (message.error) entry.reject(new Error(message.error.message || `error ${message.error.code}`))\r\n else entry.resolve(message.result)\r\n }\r\n\r\n private fail(error?: Error) {\r\n if (this.closed) return\r\n this.closed = true\r\n this.rejectAll(error ?? new Error('the connection was closed'))\r\n this.hooks.onClose?.(error)\r\n }\r\n\r\n private rejectAll(error: Error) {\r\n for (const entry of this.pending.values()) {\r\n clearTimeout(entry.timer)\r\n entry.reject(error)\r\n }\r\n this.pending.clear()\r\n }\r\n}\r\n\r\n/** The text of a tool result, for the Agent: text parts joined, other parts summarized. */\r\nexport function resultText(result: McpCallResult) {\r\n const parts = (result.content ?? []).map((part) => {\r\n if (part.type === 'text') return part.text ?? ''\r\n if (part.type === 'resource' && typeof (part as any).resource?.text === 'string') return (part as any).resource.text\r\n if (part.type === 'resource_link') return `[${(part as any).name ?? 'resource'}](${(part as any).uri})`\r\n return `[${part.type}${(part as any).mimeType ? ` ${(part as any).mimeType}` : ''}]`\r\n })\r\n return parts.join('\\n')\r\n}\r\n"],"mappings":"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"}
@@ -0,0 +1,264 @@
1
+ import { HttpOptions, HttpTransport, McpCallResult, McpClient, McpTool, PROTOCOL_VERSION, StdioOptions, StdioTransport, resultText } from "./client.js";
2
+ import { Context, Risk, Service, z } from "@mushinjs/core";
3
+ declare module '@mushinjs/core' {
4
+ interface Context {
5
+ mcp: Mcp;
6
+ }
7
+ interface Events {
8
+ 'mcp/changed'(): void;
9
+ }
10
+ }
11
+ export interface McpServerRecord {
12
+ /** Part of each tool's action name, `mcp.<id>.<tool>`. */
13
+ id: string;
14
+ name: string;
15
+ transport: 'stdio' | 'http';
16
+ command?: string;
17
+ args?: string[];
18
+ env?: Record<string, string>;
19
+ cwd?: string;
20
+ url?: string;
21
+ headers?: Record<string, string>;
22
+ enabled: boolean;
23
+ /** Tools not offered as actions. */
24
+ disabledTools: string[];
25
+ /** Risk per tool, overriding what the server's hints suggest. */
26
+ risk?: Record<string, Risk>;
27
+ createdAt: number;
28
+ }
29
+ export type McpStatus = 'stopped' | 'connecting' | 'connected' | 'error';
30
+ /** `read_file` → `readFile`, so the name fits an action name segment. */
31
+ export declare function actionSegment(name: string): string;
32
+ /** Risk from the tool's hints: read-only tools read, destructive ones are dangerous, the rest write. */
33
+ export declare function toolRisk(tool: McpTool): Risk;
34
+ declare const serverFields: {
35
+ name: z.ZodString;
36
+ transport: z.ZodEnum<{
37
+ stdio: "stdio";
38
+ http: "http";
39
+ }>;
40
+ command: z.ZodOptional<z.ZodString>;
41
+ args: z.ZodOptional<z.ZodArray<z.ZodString>>;
42
+ env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
43
+ cwd: z.ZodOptional<z.ZodString>;
44
+ url: z.ZodOptional<z.ZodString>;
45
+ headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
46
+ enabled: z.ZodOptional<z.ZodBoolean>;
47
+ };
48
+ /** Connects to MCP servers and offers each of their tools as an action, so the Agent and flows can use them. */
49
+ export declare class Mcp extends Service {
50
+ static inject: string[];
51
+ private home;
52
+ private servers;
53
+ private runtimes;
54
+ private disposed;
55
+ constructor(ctx: Context);
56
+ [Service.init](): AsyncGenerator<() => void, void, unknown>;
57
+ private changed;
58
+ get(id: string): McpServerRecord;
59
+ /** A server as the dashboard and the Agent see it: secrets masked, with status and tools. */
60
+ describe(server: McpServerRecord): {
61
+ env: {
62
+ [k: string]: string;
63
+ } | undefined;
64
+ headers: {
65
+ [k: string]: string;
66
+ } | undefined;
67
+ status: McpStatus;
68
+ error: string | undefined;
69
+ serverInfo: {
70
+ name?: string;
71
+ version?: string;
72
+ } | undefined;
73
+ stderr: string[];
74
+ tools: {
75
+ name: string;
76
+ title: string | undefined;
77
+ description: string | undefined;
78
+ action: string;
79
+ risk: Risk;
80
+ enabled: boolean;
81
+ }[];
82
+ /** Part of each tool's action name, `mcp.<id>.<tool>`. */
83
+ id: string;
84
+ name: string;
85
+ transport: "stdio" | "http";
86
+ command?: string;
87
+ args?: string[];
88
+ cwd?: string;
89
+ url?: string;
90
+ enabled: boolean;
91
+ /** Tools not offered as actions. */
92
+ disabledTools: string[];
93
+ /** Risk per tool, overriding what the server's hints suggest. */
94
+ risk?: Record<string, Risk>;
95
+ createdAt: number;
96
+ };
97
+ list(): {
98
+ env: {
99
+ [k: string]: string;
100
+ } | undefined;
101
+ headers: {
102
+ [k: string]: string;
103
+ } | undefined;
104
+ status: McpStatus;
105
+ error: string | undefined;
106
+ serverInfo: {
107
+ name?: string;
108
+ version?: string;
109
+ } | undefined;
110
+ stderr: string[];
111
+ tools: {
112
+ name: string;
113
+ title: string | undefined;
114
+ description: string | undefined;
115
+ action: string;
116
+ risk: Risk;
117
+ enabled: boolean;
118
+ }[];
119
+ /** Part of each tool's action name, `mcp.<id>.<tool>`. */
120
+ id: string;
121
+ name: string;
122
+ transport: "stdio" | "http";
123
+ command?: string;
124
+ args?: string[];
125
+ cwd?: string;
126
+ url?: string;
127
+ enabled: boolean;
128
+ /** Tools not offered as actions. */
129
+ disabledTools: string[];
130
+ /** Risk per tool, overriding what the server's hints suggest. */
131
+ risk?: Record<string, Risk>;
132
+ createdAt: number;
133
+ }[];
134
+ private save;
135
+ private check;
136
+ create(input: z.infer<z.ZodObject<typeof serverFields>> & {
137
+ id?: string;
138
+ }): Promise<{
139
+ env: {
140
+ [k: string]: string;
141
+ } | undefined;
142
+ headers: {
143
+ [k: string]: string;
144
+ } | undefined;
145
+ status: McpStatus;
146
+ error: string | undefined;
147
+ serverInfo: {
148
+ name?: string;
149
+ version?: string;
150
+ } | undefined;
151
+ stderr: string[];
152
+ tools: {
153
+ name: string;
154
+ title: string | undefined;
155
+ description: string | undefined;
156
+ action: string;
157
+ risk: Risk;
158
+ enabled: boolean;
159
+ }[];
160
+ /** Part of each tool's action name, `mcp.<id>.<tool>`. */
161
+ id: string;
162
+ name: string;
163
+ transport: "stdio" | "http";
164
+ command?: string;
165
+ args?: string[];
166
+ cwd?: string;
167
+ url?: string;
168
+ enabled: boolean;
169
+ /** Tools not offered as actions. */
170
+ disabledTools: string[];
171
+ /** Risk per tool, overriding what the server's hints suggest. */
172
+ risk?: Record<string, Risk>;
173
+ createdAt: number;
174
+ }>;
175
+ update(id: string, patch: Partial<z.infer<z.ZodObject<typeof serverFields>>>): Promise<{
176
+ env: {
177
+ [k: string]: string;
178
+ } | undefined;
179
+ headers: {
180
+ [k: string]: string;
181
+ } | undefined;
182
+ status: McpStatus;
183
+ error: string | undefined;
184
+ serverInfo: {
185
+ name?: string;
186
+ version?: string;
187
+ } | undefined;
188
+ stderr: string[];
189
+ tools: {
190
+ name: string;
191
+ title: string | undefined;
192
+ description: string | undefined;
193
+ action: string;
194
+ risk: Risk;
195
+ enabled: boolean;
196
+ }[];
197
+ /** Part of each tool's action name, `mcp.<id>.<tool>`. */
198
+ id: string;
199
+ name: string;
200
+ transport: "stdio" | "http";
201
+ command?: string;
202
+ args?: string[];
203
+ cwd?: string;
204
+ url?: string;
205
+ enabled: boolean;
206
+ /** Tools not offered as actions. */
207
+ disabledTools: string[];
208
+ /** Risk per tool, overriding what the server's hints suggest. */
209
+ risk?: Record<string, Risk>;
210
+ createdAt: number;
211
+ }>;
212
+ remove(id: string): Promise<{
213
+ deleted: string;
214
+ }>;
215
+ setTool(id: string, tool: string, patch: {
216
+ enabled?: boolean;
217
+ risk?: Risk | null;
218
+ }): Promise<{
219
+ env: {
220
+ [k: string]: string;
221
+ } | undefined;
222
+ headers: {
223
+ [k: string]: string;
224
+ } | undefined;
225
+ status: McpStatus;
226
+ error: string | undefined;
227
+ serverInfo: {
228
+ name?: string;
229
+ version?: string;
230
+ } | undefined;
231
+ stderr: string[];
232
+ tools: {
233
+ name: string;
234
+ title: string | undefined;
235
+ description: string | undefined;
236
+ action: string;
237
+ risk: Risk;
238
+ enabled: boolean;
239
+ }[];
240
+ /** Part of each tool's action name, `mcp.<id>.<tool>`. */
241
+ id: string;
242
+ name: string;
243
+ transport: "stdio" | "http";
244
+ command?: string;
245
+ args?: string[];
246
+ cwd?: string;
247
+ url?: string;
248
+ enabled: boolean;
249
+ /** Tools not offered as actions. */
250
+ disabledTools: string[];
251
+ /** Risk per tool, overriding what the server's hints suggest. */
252
+ risk?: Record<string, Risk>;
253
+ createdAt: number;
254
+ }>;
255
+ /** Connects to a server and registers its tools. */
256
+ start(id: string): Promise<void>;
257
+ stop(id: string): Promise<void>;
258
+ private refreshTools;
259
+ private unregister;
260
+ /** (Re)registers the enabled tools of a connected server as actions. */
261
+ private register;
262
+ private defineActions;
263
+ }
264
+ export { HttpOptions, HttpTransport, Mcp as default, McpCallResult, McpClient, McpTool, PROTOCOL_VERSION, StdioOptions, StdioTransport, resultText };
package/dist/index.js ADDED
@@ -0,0 +1,336 @@
1
+ import { HttpTransport, McpClient, PROTOCOL_VERSION, StdioTransport, resultText } from "./client.js";
2
+ import { MushinError, Service, isMasked, mask, z } from "@mushinjs/core";
3
+ // src/index.ts
4
+ const SERVERS = "mcp/servers";
5
+ /** `read_file` → `readFile`, so the name fits an action name segment. */
6
+ function actionSegment(name) {
7
+ const camel = name.replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(/[^A-Za-z0-9]+/).filter(Boolean).map((word, index) => index ? word[0].toUpperCase() + word.slice(1).toLowerCase() : word.toLowerCase()).join("");
8
+ return /^[a-z]/.test(camel) ? camel : `t${camel}`;
9
+ }
10
+ /** Risk from the tool's hints: read-only tools read, destructive ones are dangerous, the rest write. */
11
+ function toolRisk(tool) {
12
+ if (tool.annotations?.readOnlyHint) return "read";
13
+ if (tool.annotations?.destructiveHint) return "dangerous";
14
+ return "write";
15
+ }
16
+ const record = z.record(z.string(), z.string());
17
+ const serverFields = {
18
+ name: z.string().trim().min(1).max(60),
19
+ transport: z.enum(["stdio", "http"]).describe("stdio runs a local command; http connects to a Streamable HTTP endpoint"),
20
+ command: z.string().optional().describe("stdio: the command, e.g. npx"),
21
+ args: z.array(z.string()).optional().describe("stdio: its arguments, e.g. [\"-y\", \"@modelcontextprotocol/server-filesystem\", \"/data\"]"),
22
+ env: record.optional().describe("stdio: extra environment variables, e.g. API keys"),
23
+ cwd: z.string().optional(),
24
+ url: z.string().url().optional().describe("http: the endpoint URL"),
25
+ headers: record.optional().describe("http: extra headers, e.g. Authorization"),
26
+ enabled: z.boolean().optional()
27
+ };
28
+ /** Connects to MCP servers and offers each of their tools as an action, so the Agent and flows can use them. */
29
+ var Mcp = class extends Service {
30
+ static inject = ["actions", "storage"];
31
+ home;
32
+ servers = /* @__PURE__ */ new Map();
33
+ runtimes = /* @__PURE__ */ new Map();
34
+ disposed = false;
35
+ constructor(ctx) {
36
+ super(ctx, "mcp");
37
+ this.home = ctx;
38
+ }
39
+ async *[Service.init]() {
40
+ for (const key of await this.home.storage.list(SERVERS)) {
41
+ const server = await this.home.storage.get(key);
42
+ if (server?.id) this.servers.set(server.id, server);
43
+ }
44
+ yield* this.defineActions();
45
+ yield () => {
46
+ this.disposed = true;
47
+ for (const id of this.runtimes.keys()) this.stop(id).catch(() => {});
48
+ };
49
+ for (const server of this.servers.values()) if (server.enabled) this.start(server.id).catch(() => {});
50
+ }
51
+ changed() {
52
+ if (!this.disposed) this.home.emit("mcp/changed");
53
+ }
54
+ get(id) {
55
+ const server = this.servers.get(id);
56
+ if (!server) throw new MushinError("not_found", `No MCP server "${id}"`);
57
+ return server;
58
+ }
59
+ /** A server as the dashboard and the Agent see it: secrets masked, with status and tools. */
60
+ describe(server) {
61
+ const runtime = this.runtimes.get(server.id);
62
+ const masked = (values) => values && Object.fromEntries(Object.entries(values).map(([key, value]) => [key, mask(value)]));
63
+ return {
64
+ ...server,
65
+ env: masked(server.env),
66
+ headers: masked(server.headers),
67
+ status: runtime?.status ?? "stopped",
68
+ error: runtime?.error,
69
+ serverInfo: runtime?.client?.server,
70
+ stderr: runtime?.client?.stderr.slice(-20) ?? [],
71
+ tools: (runtime?.tools ?? []).map((tool) => ({
72
+ name: tool.name,
73
+ title: tool.title ?? tool.annotations?.title,
74
+ description: tool.description,
75
+ action: `mcp.${server.id}.${actionSegment(tool.name)}`,
76
+ risk: server.risk?.[tool.name] ?? toolRisk(tool),
77
+ enabled: !server.disabledTools.includes(tool.name)
78
+ }))
79
+ };
80
+ }
81
+ list() {
82
+ return [...this.servers.values()].sort((a, b) => a.createdAt - b.createdAt).map((server) => this.describe(server));
83
+ }
84
+ async save(server) {
85
+ this.servers.set(server.id, server);
86
+ await this.home.storage.set(`${SERVERS}/${server.id}`, server);
87
+ this.changed();
88
+ }
89
+ check(server) {
90
+ if (server.transport === "stdio" && !server.command?.trim()) throw new MushinError("invalid_input", "A stdio server needs a command");
91
+ if (server.transport === "http" && !server.url) throw new MushinError("invalid_input", "An HTTP server needs a URL");
92
+ }
93
+ async create(input) {
94
+ this.check(input);
95
+ let id = input.id ?? actionSegment(input.name.normalize("NFKD").replace(/[^\x20-\x7e]/g, "")) ?? "server";
96
+ if (id === "t") id = "server";
97
+ if (this.servers.has(id)) {
98
+ if (input.id) throw new MushinError("conflict", `An MCP server with id "${id}" already exists`);
99
+ let n = 2;
100
+ while (this.servers.has(`${id}${n}`)) n++;
101
+ id = `${id}${n}`;
102
+ }
103
+ const server = {
104
+ ...input,
105
+ id,
106
+ enabled: input.enabled !== false,
107
+ disabledTools: [],
108
+ createdAt: Date.now()
109
+ };
110
+ await this.save(server);
111
+ if (server.enabled) await this.start(id).catch(() => {});
112
+ return this.describe(this.get(id));
113
+ }
114
+ async update(id, patch) {
115
+ const current = this.get(id);
116
+ const merge = (next, old) => next && Object.fromEntries(Object.entries(next).map(([key, value]) => [key, isMasked(value) && old?.[key] ? old[key] : value]));
117
+ const server = {
118
+ ...current,
119
+ ...patch,
120
+ env: patch.env ? merge(patch.env, current.env) : current.env,
121
+ headers: patch.headers ? merge(patch.headers, current.headers) : current.headers
122
+ };
123
+ this.check(server);
124
+ await this.save(server);
125
+ await this.stop(id);
126
+ if (server.enabled) await this.start(id).catch(() => {});
127
+ return this.describe(this.get(id));
128
+ }
129
+ async remove(id) {
130
+ this.get(id);
131
+ await this.stop(id);
132
+ this.servers.delete(id);
133
+ this.runtimes.delete(id);
134
+ await this.home.storage.delete(`${SERVERS}/${id}`);
135
+ this.changed();
136
+ return { deleted: id };
137
+ }
138
+ async setTool(id, tool, patch) {
139
+ const server = this.get(id);
140
+ const next = {
141
+ ...server,
142
+ disabledTools: [...server.disabledTools],
143
+ risk: { ...server.risk }
144
+ };
145
+ if (patch.enabled !== void 0) next.disabledTools = patch.enabled ? next.disabledTools.filter((name) => name !== tool) : [.../* @__PURE__ */ new Set([...next.disabledTools, tool])];
146
+ if (patch.risk === null) delete next.risk[tool];
147
+ else if (patch.risk) next.risk[tool] = patch.risk;
148
+ await this.save(next);
149
+ this.register(id);
150
+ return this.describe(next);
151
+ }
152
+ /** Connects to a server and registers its tools. */
153
+ async start(id) {
154
+ const server = this.get(id);
155
+ await this.stop(id);
156
+ const runtime = {
157
+ status: "connecting",
158
+ tools: [],
159
+ unregister: []
160
+ };
161
+ this.runtimes.set(id, runtime);
162
+ this.changed();
163
+ const transport = server.transport === "stdio" ? new StdioTransport({
164
+ command: server.command,
165
+ args: server.args,
166
+ env: server.env,
167
+ cwd: server.cwd
168
+ }) : new HttpTransport({
169
+ url: server.url,
170
+ headers: server.headers
171
+ });
172
+ const client = new McpClient(transport, {
173
+ onToolsChanged: () => this.refreshTools(id, runtime).catch(() => {}),
174
+ onClose: (error) => {
175
+ if (this.runtimes.get(id) !== runtime) return;
176
+ this.unregister(runtime);
177
+ runtime.status = "error";
178
+ runtime.error = error?.message ?? "the connection was closed";
179
+ this.home.logger("mcp").warn("MCP server %s stopped: %s", server.name, runtime.error);
180
+ this.changed();
181
+ }
182
+ });
183
+ runtime.client = client;
184
+ try {
185
+ await client.connect();
186
+ if (this.runtimes.get(id) !== runtime) return;
187
+ await this.refreshTools(id, runtime);
188
+ runtime.status = "connected";
189
+ runtime.error = void 0;
190
+ this.home.logger("mcp").info("connected to MCP server %s: %d tool(s)", server.name, runtime.tools.length);
191
+ } catch (error) {
192
+ await client.close();
193
+ if (this.runtimes.get(id) !== runtime) return;
194
+ runtime.status = "error";
195
+ runtime.error = error instanceof Error ? error.message : String(error);
196
+ this.home.logger("mcp").warn("cannot connect to MCP server %s: %s", server.name, runtime.error);
197
+ throw new MushinError("unavailable", `Cannot connect to ${server.name}: ${runtime.error}`);
198
+ } finally {
199
+ this.changed();
200
+ }
201
+ }
202
+ async stop(id) {
203
+ const runtime = this.runtimes.get(id);
204
+ if (!runtime) return;
205
+ this.runtimes.set(id, {
206
+ ...runtime,
207
+ client: void 0,
208
+ status: "stopped",
209
+ error: void 0,
210
+ tools: [],
211
+ unregister: []
212
+ });
213
+ this.unregister(runtime);
214
+ await runtime.client?.close();
215
+ this.changed();
216
+ }
217
+ async refreshTools(id, runtime) {
218
+ if (!runtime.client) return;
219
+ runtime.tools = await runtime.client.listTools();
220
+ this.register(id);
221
+ }
222
+ unregister(runtime) {
223
+ for (const dispose of runtime.unregister.splice(0)) try {
224
+ dispose();
225
+ } catch {}
226
+ }
227
+ /** (Re)registers the enabled tools of a connected server as actions. */
228
+ register(id) {
229
+ const runtime = this.runtimes.get(id);
230
+ const server = this.servers.get(id);
231
+ if (!runtime?.client || !server || this.disposed) return;
232
+ this.unregister(runtime);
233
+ const client = runtime.client;
234
+ const used = /* @__PURE__ */ new Set();
235
+ for (const tool of runtime.tools) {
236
+ if (server.disabledTools.includes(tool.name)) continue;
237
+ let segment = actionSegment(tool.name);
238
+ while (used.has(segment)) segment += "2";
239
+ used.add(segment);
240
+ const schema = tool.inputSchema?.type === "object" ? tool.inputSchema : {
241
+ type: "object",
242
+ properties: {}
243
+ };
244
+ runtime.unregister.push(this.home.actions.define({
245
+ name: `mcp.${server.id}.${segment}`,
246
+ description: `[MCP ${server.name}] ${tool.description ?? tool.title ?? tool.name}`.slice(0, 1024),
247
+ risk: server.risk?.[tool.name] ?? toolRisk(tool),
248
+ input: z.record(z.string(), z.unknown()),
249
+ inputSchema: schema,
250
+ handler: async (input) => {
251
+ const result = await client.callTool(tool.name, input);
252
+ const text = resultText(result);
253
+ if (result.isError) throw new MushinError("failed", text || `${tool.name} failed`);
254
+ return {
255
+ text,
256
+ ...result.structuredContent !== void 0 ? { structured: result.structuredContent } : {}
257
+ };
258
+ }
259
+ }));
260
+ }
261
+ this.changed();
262
+ }
263
+ *defineActions() {
264
+ const actions = this.home.actions;
265
+ const idInput = z.object({ id: z.string().describe("MCP server id from mcp.servers.list") });
266
+ yield actions.define({
267
+ name: "mcp.servers.list",
268
+ description: "List MCP servers with their connection status and tools. Each enabled tool of a connected server is an action named mcp.<server id>.<tool>.",
269
+ risk: "read",
270
+ input: z.object({}),
271
+ handler: () => this.list()
272
+ });
273
+ yield actions.define({
274
+ name: "mcp.servers.create",
275
+ description: "Add an MCP server and connect to it: a local command over stdio (e.g. npx -y @modelcontextprotocol/server-filesystem <dir>) or a Streamable HTTP URL. Its tools become actions for new Agent conversations.",
276
+ risk: "dangerous",
277
+ input: z.object({
278
+ ...serverFields,
279
+ id: z.string().regex(/^[a-z][a-zA-Z0-9]{0,30}$/).optional().describe("Short id used in action names; made from the name when omitted")
280
+ }),
281
+ handler: (input) => this.create(input)
282
+ });
283
+ yield actions.define({
284
+ name: "mcp.servers.update",
285
+ description: "Change an MCP server and reconnect. Masked secrets sent back unchanged keep their stored value.",
286
+ risk: "dangerous",
287
+ input: idInput.extend({
288
+ name: serverFields.name.optional(),
289
+ transport: serverFields.transport.optional(),
290
+ command: serverFields.command,
291
+ args: serverFields.args,
292
+ env: serverFields.env,
293
+ cwd: serverFields.cwd,
294
+ url: serverFields.url,
295
+ headers: serverFields.headers,
296
+ enabled: serverFields.enabled
297
+ }),
298
+ handler: ({ id, ...patch }) => this.update(id, patch)
299
+ });
300
+ yield actions.define({
301
+ name: "mcp.servers.delete",
302
+ description: "Remove an MCP server and its tools.",
303
+ risk: "dangerous",
304
+ input: idInput,
305
+ handler: ({ id }) => this.remove(id)
306
+ });
307
+ yield actions.define({
308
+ name: "mcp.servers.restart",
309
+ description: "Reconnect to an MCP server and reload its tools.",
310
+ risk: "write",
311
+ input: idInput,
312
+ handler: async ({ id }) => {
313
+ await this.start(id);
314
+ return this.describe(this.get(id));
315
+ }
316
+ });
317
+ yield actions.define({
318
+ name: "mcp.tools.update",
319
+ description: "Turn one tool of an MCP server on or off, or set its risk (read, write, dangerous; null for the server's hint).",
320
+ risk: "write",
321
+ input: idInput.extend({
322
+ tool: z.string().describe("The tool name as the server gives it"),
323
+ enabled: z.boolean().optional(),
324
+ risk: z.enum([
325
+ "read",
326
+ "write",
327
+ "dangerous"
328
+ ]).nullable().optional()
329
+ }),
330
+ handler: ({ id, tool, ...patch }) => this.setTool(id, tool, patch)
331
+ });
332
+ }
333
+ };
334
+ export { HttpTransport, Mcp, Mcp as default, McpClient, PROTOCOL_VERSION, StdioTransport, actionSegment, resultText, toolRisk };
335
+
336
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["import { Context, isMasked, mask, MushinError, type Risk, Service, z } from '@mushinjs/core'\r\nimport type {} from '@mushinjs/plugin-storage'\r\nimport { HttpTransport, McpClient, type McpTool, resultText, StdioTransport } from './client'\r\n\r\nexport * from './client'\r\n\r\ndeclare module '@mushinjs/core' {\r\n interface Context {\r\n mcp: Mcp\r\n }\r\n\r\n interface Events {\r\n 'mcp/changed'(): void\r\n }\r\n}\r\n\r\nconst SERVERS = 'mcp/servers'\r\n\r\nexport interface McpServerRecord {\r\n /** Part of each tool's action name, `mcp.<id>.<tool>`. */\r\n id: string\r\n name: string\r\n transport: 'stdio' | 'http'\r\n command?: string\r\n args?: string[]\r\n env?: Record<string, string>\r\n cwd?: string\r\n url?: string\r\n headers?: Record<string, string>\r\n enabled: boolean\r\n /** Tools not offered as actions. */\r\n disabledTools: string[]\r\n /** Risk per tool, overriding what the server's hints suggest. */\r\n risk?: Record<string, Risk>\r\n createdAt: number\r\n}\r\n\r\nexport type McpStatus = 'stopped' | 'connecting' | 'connected' | 'error'\r\n\r\ninterface Runtime {\r\n client?: McpClient\r\n status: McpStatus\r\n error?: string\r\n tools: McpTool[]\r\n /** Removes the tool actions. */\r\n unregister: (() => void)[]\r\n}\r\n\r\n/** `read_file` → `readFile`, so the name fits an action name segment. */\r\nexport function actionSegment(name: string) {\r\n const words = name.replace(/([a-z0-9])([A-Z])/g, '$1 $2').split(/[^A-Za-z0-9]+/).filter(Boolean)\r\n const camel = words.map((word, index) => (index ? word[0].toUpperCase() + word.slice(1).toLowerCase() : word.toLowerCase())).join('')\r\n return /^[a-z]/.test(camel) ? camel : `t${camel}`\r\n}\r\n\r\n/** Risk from the tool's hints: read-only tools read, destructive ones are dangerous, the rest write. */\r\nexport function toolRisk(tool: McpTool): Risk {\r\n if (tool.annotations?.readOnlyHint) return 'read'\r\n if (tool.annotations?.destructiveHint) return 'dangerous'\r\n return 'write'\r\n}\r\n\r\nconst record = z.record(z.string(), z.string())\r\n\r\nconst serverFields = {\r\n name: z.string().trim().min(1).max(60),\r\n transport: z.enum(['stdio', 'http']).describe('stdio runs a local command; http connects to a Streamable HTTP endpoint'),\r\n command: z.string().optional().describe('stdio: the command, e.g. npx'),\r\n args: z.array(z.string()).optional().describe('stdio: its arguments, e.g. [\"-y\", \"@modelcontextprotocol/server-filesystem\", \"/data\"]'),\r\n env: record.optional().describe('stdio: extra environment variables, e.g. API keys'),\r\n cwd: z.string().optional(),\r\n url: z.string().url().optional().describe('http: the endpoint URL'),\r\n headers: record.optional().describe('http: extra headers, e.g. Authorization'),\r\n enabled: z.boolean().optional(),\r\n}\r\n\r\n/** Connects to MCP servers and offers each of their tools as an action, so the Agent and flows can use them. */\r\nexport class Mcp extends Service {\r\n static inject = ['actions', 'storage']\r\n\r\n private home: Context\r\n private servers = new Map<string, McpServerRecord>()\r\n private runtimes = new Map<string, Runtime>()\r\n private disposed = false\r\n\r\n constructor(ctx: Context) {\r\n super(ctx, 'mcp')\r\n this.home = ctx\r\n }\r\n\r\n async *[Service.init]() {\r\n for (const key of await this.home.storage.list(SERVERS)) {\r\n const server = await this.home.storage.get<McpServerRecord>(key)\r\n if (server?.id) this.servers.set(server.id, server)\r\n }\r\n yield* this.defineActions()\r\n yield () => {\r\n this.disposed = true\r\n for (const id of this.runtimes.keys()) this.stop(id).catch(() => {})\r\n }\r\n for (const server of this.servers.values()) {\r\n if (server.enabled) this.start(server.id).catch(() => {})\r\n }\r\n }\r\n\r\n private changed() {\r\n if (!this.disposed) this.home.emit('mcp/changed')\r\n }\r\n\r\n get(id: string) {\r\n const server = this.servers.get(id)\r\n if (!server) throw new MushinError('not_found', `No MCP server \"${id}\"`)\r\n return server\r\n }\r\n\r\n /** A server as the dashboard and the Agent see it: secrets masked, with status and tools. */\r\n describe(server: McpServerRecord) {\r\n const runtime = this.runtimes.get(server.id)\r\n const masked = (values?: Record<string, string>) => values && Object.fromEntries(Object.entries(values).map(([key, value]) => [key, mask(value)]))\r\n return {\r\n ...server,\r\n env: masked(server.env),\r\n headers: masked(server.headers),\r\n status: runtime?.status ?? 'stopped',\r\n error: runtime?.error,\r\n serverInfo: runtime?.client?.server,\r\n stderr: runtime?.client?.stderr.slice(-20) ?? [],\r\n tools: (runtime?.tools ?? []).map((tool) => ({\r\n name: tool.name,\r\n title: tool.title ?? tool.annotations?.title,\r\n description: tool.description,\r\n action: `mcp.${server.id}.${actionSegment(tool.name)}`,\r\n risk: server.risk?.[tool.name] ?? toolRisk(tool),\r\n enabled: !server.disabledTools.includes(tool.name),\r\n })),\r\n }\r\n }\r\n\r\n list() {\r\n return [...this.servers.values()].sort((a, b) => a.createdAt - b.createdAt).map((server) => this.describe(server))\r\n }\r\n\r\n private async save(server: McpServerRecord) {\r\n this.servers.set(server.id, server)\r\n await this.home.storage.set(`${SERVERS}/${server.id}`, server)\r\n this.changed()\r\n }\r\n\r\n private check(server: Pick<McpServerRecord, 'transport' | 'command' | 'url'>) {\r\n if (server.transport === 'stdio' && !server.command?.trim()) throw new MushinError('invalid_input', 'A stdio server needs a command')\r\n if (server.transport === 'http' && !server.url) throw new MushinError('invalid_input', 'An HTTP server needs a URL')\r\n }\r\n\r\n async create(input: z.infer<z.ZodObject<typeof serverFields>> & { id?: string }) {\r\n this.check(input)\r\n let id = input.id ?? actionSegment(input.name.normalize('NFKD').replace(/[^\\x20-\\x7e]/g, '')) ?? 'server'\r\n if (id === 't') id = 'server'\r\n if (this.servers.has(id)) {\r\n if (input.id) throw new MushinError('conflict', `An MCP server with id \"${id}\" already exists`)\r\n let n = 2\r\n while (this.servers.has(`${id}${n}`)) n++\r\n id = `${id}${n}`\r\n }\r\n const server: McpServerRecord = { ...input, id, enabled: input.enabled !== false, disabledTools: [], createdAt: Date.now() }\r\n await this.save(server)\r\n if (server.enabled) await this.start(id).catch(() => {})\r\n return this.describe(this.get(id))\r\n }\r\n\r\n async update(id: string, patch: Partial<z.infer<z.ZodObject<typeof serverFields>>>) {\r\n const current = this.get(id)\r\n // Masked values come back unchanged from forms: keep the stored secret.\r\n const merge = (next: Record<string, string> | undefined, old: Record<string, string> | undefined) => next && Object.fromEntries(Object.entries(next).map(([key, value]) => [key, isMasked(value) && old?.[key] ? old[key] : value]))\r\n const server: McpServerRecord = {\r\n ...current,\r\n ...patch,\r\n env: patch.env ? merge(patch.env, current.env) : current.env,\r\n headers: patch.headers ? merge(patch.headers, current.headers) : current.headers,\r\n }\r\n this.check(server)\r\n await this.save(server)\r\n await this.stop(id)\r\n if (server.enabled) await this.start(id).catch(() => {})\r\n return this.describe(this.get(id))\r\n }\r\n\r\n async remove(id: string) {\r\n this.get(id)\r\n await this.stop(id)\r\n this.servers.delete(id)\r\n this.runtimes.delete(id)\r\n await this.home.storage.delete(`${SERVERS}/${id}`)\r\n this.changed()\r\n return { deleted: id }\r\n }\r\n\r\n async setTool(id: string, tool: string, patch: { enabled?: boolean; risk?: Risk | null }) {\r\n const server = this.get(id)\r\n const next: McpServerRecord = { ...server, disabledTools: [...server.disabledTools], risk: { ...server.risk } }\r\n if (patch.enabled !== undefined) {\r\n next.disabledTools = patch.enabled ? next.disabledTools.filter((name) => name !== tool) : [...new Set([...next.disabledTools, tool])]\r\n }\r\n if (patch.risk === null) delete next.risk![tool]\r\n else if (patch.risk) next.risk![tool] = patch.risk\r\n await this.save(next)\r\n this.register(id)\r\n return this.describe(next)\r\n }\r\n\r\n /** Connects to a server and registers its tools. */\r\n async start(id: string) {\r\n const server = this.get(id)\r\n await this.stop(id)\r\n const runtime: Runtime = { status: 'connecting', tools: [], unregister: [] }\r\n this.runtimes.set(id, runtime)\r\n this.changed()\r\n const transport = server.transport === 'stdio'\r\n ? new StdioTransport({ command: server.command!, args: server.args, env: server.env, cwd: server.cwd })\r\n : new HttpTransport({ url: server.url!, headers: server.headers })\r\n const client = new McpClient(transport, {\r\n onToolsChanged: () => this.refreshTools(id, runtime).catch(() => {}),\r\n onClose: (error) => {\r\n if (this.runtimes.get(id) !== runtime) return\r\n this.unregister(runtime)\r\n runtime.status = 'error'\r\n runtime.error = error?.message ?? 'the connection was closed'\r\n this.home.logger('mcp').warn('MCP server %s stopped: %s', server.name, runtime.error)\r\n this.changed()\r\n },\r\n })\r\n runtime.client = client\r\n try {\r\n await client.connect()\r\n if (this.runtimes.get(id) !== runtime) return\r\n await this.refreshTools(id, runtime)\r\n runtime.status = 'connected'\r\n runtime.error = undefined\r\n this.home.logger('mcp').info('connected to MCP server %s: %d tool(s)', server.name, runtime.tools.length)\r\n } catch (error) {\r\n await client.close()\r\n if (this.runtimes.get(id) !== runtime) return\r\n runtime.status = 'error'\r\n runtime.error = error instanceof Error ? error.message : String(error)\r\n this.home.logger('mcp').warn('cannot connect to MCP server %s: %s', server.name, runtime.error)\r\n throw new MushinError('unavailable', `Cannot connect to ${server.name}: ${runtime.error}`)\r\n } finally {\r\n this.changed()\r\n }\r\n }\r\n\r\n async stop(id: string) {\r\n const runtime = this.runtimes.get(id)\r\n if (!runtime) return\r\n this.runtimes.set(id, { ...runtime, client: undefined, status: 'stopped', error: undefined, tools: [], unregister: [] })\r\n this.unregister(runtime)\r\n await runtime.client?.close()\r\n this.changed()\r\n }\r\n\r\n private async refreshTools(id: string, runtime: Runtime) {\r\n if (!runtime.client) return\r\n runtime.tools = await runtime.client.listTools()\r\n this.register(id)\r\n }\r\n\r\n private unregister(runtime: Runtime) {\r\n for (const dispose of runtime.unregister.splice(0)) {\r\n try {\r\n dispose()\r\n } catch {}\r\n }\r\n }\r\n\r\n /** (Re)registers the enabled tools of a connected server as actions. */\r\n private register(id: string) {\r\n const runtime = this.runtimes.get(id)\r\n const server = this.servers.get(id)\r\n if (!runtime?.client || !server || this.disposed) return\r\n this.unregister(runtime)\r\n const client = runtime.client\r\n const used = new Set<string>()\r\n for (const tool of runtime.tools) {\r\n if (server.disabledTools.includes(tool.name)) continue\r\n let segment = actionSegment(tool.name)\r\n while (used.has(segment)) segment += '2'\r\n used.add(segment)\r\n const schema = tool.inputSchema?.type === 'object' ? tool.inputSchema : { type: 'object', properties: {} }\r\n runtime.unregister.push(this.home.actions.define({\r\n name: `mcp.${server.id}.${segment}`,\r\n description: `[MCP ${server.name}] ${tool.description ?? tool.title ?? tool.name}`.slice(0, 1024),\r\n risk: server.risk?.[tool.name] ?? toolRisk(tool),\r\n input: z.record(z.string(), z.unknown()),\r\n inputSchema: schema,\r\n handler: async (input) => {\r\n const result = await client.callTool(tool.name, input)\r\n const text = resultText(result)\r\n if (result.isError) throw new MushinError('failed', text || `${tool.name} failed`)\r\n return { text, ...(result.structuredContent !== undefined ? { structured: result.structuredContent } : {}) }\r\n },\r\n }))\r\n }\r\n this.changed()\r\n }\r\n\r\n private *defineActions() {\r\n const actions = this.home.actions\r\n const idInput = z.object({ id: z.string().describe('MCP server id from mcp.servers.list') })\r\n\r\n yield actions.define({\r\n name: 'mcp.servers.list',\r\n description: 'List MCP servers with their connection status and tools. Each enabled tool of a connected server is an action named mcp.<server id>.<tool>.',\r\n risk: 'read',\r\n input: z.object({}),\r\n handler: () => this.list(),\r\n })\r\n\r\n yield actions.define({\r\n name: 'mcp.servers.create',\r\n description: 'Add an MCP server and connect to it: a local command over stdio (e.g. npx -y @modelcontextprotocol/server-filesystem <dir>) or a Streamable HTTP URL. Its tools become actions for new Agent conversations.',\r\n risk: 'dangerous',\r\n input: z.object({ ...serverFields, id: z.string().regex(/^[a-z][a-zA-Z0-9]{0,30}$/).optional().describe('Short id used in action names; made from the name when omitted') }),\r\n handler: (input) => this.create(input),\r\n })\r\n\r\n yield actions.define({\r\n name: 'mcp.servers.update',\r\n description: 'Change an MCP server and reconnect. Masked secrets sent back unchanged keep their stored value.',\r\n risk: 'dangerous',\r\n input: idInput.extend({\r\n name: serverFields.name.optional(),\r\n transport: serverFields.transport.optional(),\r\n command: serverFields.command,\r\n args: serverFields.args,\r\n env: serverFields.env,\r\n cwd: serverFields.cwd,\r\n url: serverFields.url,\r\n headers: serverFields.headers,\r\n enabled: serverFields.enabled,\r\n }),\r\n handler: ({ id, ...patch }) => this.update(id, patch),\r\n })\r\n\r\n yield actions.define({\r\n name: 'mcp.servers.delete',\r\n description: 'Remove an MCP server and its tools.',\r\n risk: 'dangerous',\r\n input: idInput,\r\n handler: ({ id }) => this.remove(id),\r\n })\r\n\r\n yield actions.define({\r\n name: 'mcp.servers.restart',\r\n description: 'Reconnect to an MCP server and reload its tools.',\r\n risk: 'write',\r\n input: idInput,\r\n handler: async ({ id }) => {\r\n await this.start(id)\r\n return this.describe(this.get(id))\r\n },\r\n })\r\n\r\n yield actions.define({\r\n name: 'mcp.tools.update',\r\n description: 'Turn one tool of an MCP server on or off, or set its risk (read, write, dangerous; null for the server\\'s hint).',\r\n risk: 'write',\r\n input: idInput.extend({\r\n tool: z.string().describe('The tool name as the server gives it'),\r\n enabled: z.boolean().optional(),\r\n risk: z.enum(['read', 'write', 'dangerous']).nullable().optional(),\r\n }),\r\n handler: ({ id, tool, ...patch }) => this.setTool(id, tool, patch),\r\n })\r\n }\r\n}\r\n\r\nexport default Mcp\r\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,53 @@
1
1
  {
2
2
  "name": "@mushinjs/plugin-mcp",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.2.0-next.20261112165327",
4
+ "description": "Connects to MCP servers and offers their tools as actions for the Agent and flows",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/mushinjs/mushin.git",
10
+ "directory": "packages/plugin-mcp"
11
+ },
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "import": "./dist/index.js"
16
+ },
17
+ "./package.json": "./package.json"
18
+ },
19
+ "files": [
20
+ "dist"
21
+ ],
22
+ "publishConfig": {
23
+ "access": "public"
24
+ },
25
+ "keywords": [
26
+ "mushin-plugin"
27
+ ],
28
+ "mushin": {
29
+ "kind": "app",
30
+ "category": "ai",
31
+ "displayName": {
32
+ "en": "MCP",
33
+ "zh-CN": "MCP 工具"
34
+ },
35
+ "description": {
36
+ "en": "Connect MCP servers (a local command over stdio, or Streamable HTTP); each of their tools becomes an action the Agent can use",
37
+ "zh-CN": "接入 MCP 服务器(本地命令 stdio 或 Streamable HTTP),它们的每个工具都成为智能体可以使用的操作"
38
+ },
39
+ "provides": [
40
+ "mcp"
41
+ ],
42
+ "requires": {
43
+ "mushin": ">=0.1.0",
44
+ "plugins": {
45
+ "@mushinjs/plugin-storage": "*"
46
+ }
47
+ }
48
+ },
49
+ "dependencies": {
50
+ "@mushinjs/core": "0.2.0-next.20261112165327",
51
+ "@mushinjs/plugin-storage": "0.2.0-next.20261112165327"
52
+ }
6
53
  }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.