@wrongstack/mcp 0.1.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/LICENSE ADDED
@@ -0,0 +1,17 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ Copyright 2026 ECOSTACK TECHNOLOGY OÜ
6
+
7
+ Licensed under the Apache License, Version 2.0 (the "License");
8
+ you may not use this file except in compliance with the License.
9
+ You may obtain a copy of the License at
10
+
11
+ http://www.apache.org/licenses/LICENSE-2.0
12
+
13
+ Unless required by applicable law or agreed to in writing, software
14
+ distributed under the License is distributed on an "AS IS" BASIS,
15
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ See the License for the specific language governing permissions and
17
+ limitations under the License.
@@ -0,0 +1,83 @@
1
+ import { Permission, Tool, ToolRegistry, EventBus, Logger, MCPServerConfig } from '@wrongstack/core';
2
+
3
+ type Transport = 'stdio' | 'sse' | 'streamable-http';
4
+ interface MCPClientOptions {
5
+ name: string;
6
+ transport: Transport;
7
+ command?: string;
8
+ args?: string[];
9
+ env?: Record<string, string>;
10
+ url?: string;
11
+ headers?: Record<string, string>;
12
+ startupTimeoutMs?: number;
13
+ }
14
+ type ConnectionState = 'idle' | 'connecting' | 'connected' | 'disconnected' | 'reconnecting' | 'failed';
15
+ interface MCPTool {
16
+ name: string;
17
+ description?: string;
18
+ inputSchema: Record<string, unknown>;
19
+ }
20
+ interface ToolCallResult {
21
+ content: unknown;
22
+ isError: boolean;
23
+ }
24
+ /**
25
+ * Lightweight stdio MCP client. Supports JSON-RPC 2.0 over newline-delimited JSON.
26
+ * SSE / streamable-http are stubbed and throw on connect — to be filled by a real impl.
27
+ */
28
+ declare class MCPClient {
29
+ readonly opts: MCPClientOptions;
30
+ private state;
31
+ private child?;
32
+ private nextId;
33
+ private readonly pending;
34
+ private rxBuffer;
35
+ private tools;
36
+ /**
37
+ * Guards against multiple concurrent drain-waits. When `stdin.write()`
38
+ * returns false the first waiter sets this flag; any subsequent callers
39
+ * skip the drain wait and emit a warning instead of racing.
40
+ */
41
+ private _drainPending;
42
+ /** Set when a notify() call failed for reasons the caller should know about. */
43
+ private _lastNotifySkipped;
44
+ constructor(opts: MCPClientOptions);
45
+ getState(): ConnectionState;
46
+ listTools(): MCPTool[];
47
+ /** Returns true if a prior notify() call was skipped due to backpressure. */
48
+ hadNotifySkipped(): boolean;
49
+ connect(): Promise<void>;
50
+ callTool(name: string, input: unknown): Promise<ToolCallResult>;
51
+ close(): Promise<void>;
52
+ private request;
53
+ private notify;
54
+ private onData;
55
+ private onLine;
56
+ }
57
+
58
+ declare function wrapMCPTool(serverName: string, mcpTool: MCPTool, client: MCPClient, permission?: Permission): Tool;
59
+
60
+ interface MCPRegistryOptions {
61
+ toolRegistry: ToolRegistry;
62
+ events: EventBus;
63
+ log: Logger;
64
+ }
65
+ declare class MCPRegistry {
66
+ private readonly servers;
67
+ private readonly toolRegistry;
68
+ private readonly events;
69
+ private readonly log;
70
+ constructor(opts: MCPRegistryOptions);
71
+ start(cfg: MCPServerConfig): Promise<void>;
72
+ stop(name: string): Promise<void>;
73
+ restart(name: string): Promise<void>;
74
+ list(): {
75
+ name: string;
76
+ state: ConnectionState;
77
+ toolCount: number;
78
+ }[];
79
+ stopAll(): Promise<void>;
80
+ private attemptConnect;
81
+ }
82
+
83
+ export { type ConnectionState, MCPClient, type MCPClientOptions, MCPRegistry, type MCPRegistryOptions, type MCPTool, type ToolCallResult, type Transport, wrapMCPTool };
package/dist/index.js ADDED
@@ -0,0 +1,332 @@
1
+ import { spawn } from 'child_process';
2
+
3
+ // src/client.ts
4
+ var MCPClient = class {
5
+ constructor(opts) {
6
+ this.opts = opts;
7
+ }
8
+ opts;
9
+ state = "idle";
10
+ child;
11
+ nextId = 1;
12
+ pending = /* @__PURE__ */ new Map();
13
+ rxBuffer = "";
14
+ tools = [];
15
+ /**
16
+ * Guards against multiple concurrent drain-waits. When `stdin.write()`
17
+ * returns false the first waiter sets this flag; any subsequent callers
18
+ * skip the drain wait and emit a warning instead of racing.
19
+ */
20
+ _drainPending = false;
21
+ /** Set when a notify() call failed for reasons the caller should know about. */
22
+ _lastNotifySkipped = false;
23
+ getState() {
24
+ return this.state;
25
+ }
26
+ listTools() {
27
+ return [...this.tools];
28
+ }
29
+ /** Returns true if a prior notify() call was skipped due to backpressure. */
30
+ hadNotifySkipped() {
31
+ return this._lastNotifySkipped;
32
+ }
33
+ async connect() {
34
+ this.state = "connecting";
35
+ if (this.opts.transport !== "stdio") {
36
+ this.state = "failed";
37
+ throw new Error(`MCP transport "${this.opts.transport}" not supported in this build`);
38
+ }
39
+ if (!this.opts.command) {
40
+ this.state = "failed";
41
+ throw new Error('MCP stdio transport requires "command"');
42
+ }
43
+ const child = spawn(this.opts.command, this.opts.args ?? [], {
44
+ env: { ...process.env, ...this.opts.env },
45
+ stdio: ["pipe", "pipe", "pipe"]
46
+ });
47
+ this.child = child;
48
+ child.stdout?.on("data", (chunk) => this.onData(chunk.toString()));
49
+ child.stderr?.on("data", () => {
50
+ });
51
+ child.on("exit", () => {
52
+ this.state = "disconnected";
53
+ });
54
+ child.on("error", () => {
55
+ this.state = "failed";
56
+ });
57
+ const timeout = this.opts.startupTimeoutMs ?? 1e4;
58
+ const initialize = await Promise.race([
59
+ this.request("initialize", {
60
+ protocolVersion: "2024-11-05",
61
+ capabilities: { tools: {} },
62
+ clientInfo: { name: "wrongstack", version: "0.0.1" }
63
+ }),
64
+ new Promise(
65
+ (_, rej) => setTimeout(() => rej(new Error("MCP initialize timeout")), timeout)
66
+ )
67
+ ]);
68
+ if (initialize.error) {
69
+ this.state = "failed";
70
+ throw new Error(`MCP initialize failed: ${initialize.error.message}`);
71
+ }
72
+ try {
73
+ await this.notify("notifications/initialized", {});
74
+ } catch (err) {
75
+ console.warn(
76
+ '[MCP] notify("notifications/initialized") failed for "' + this.opts.name + '": ' + (err instanceof Error ? err.message : String(err))
77
+ );
78
+ }
79
+ const toolsRes = await this.request("tools/list", {});
80
+ if (toolsRes.error) {
81
+ this.tools = [];
82
+ } else {
83
+ const result = toolsRes.result;
84
+ this.tools = result?.tools ?? [];
85
+ }
86
+ this.state = "connected";
87
+ }
88
+ async callTool(name, input) {
89
+ if (this.state !== "connected") {
90
+ throw new Error(`MCP client "${this.opts.name}" not connected (state=${this.state})`);
91
+ }
92
+ const res = await this.request("tools/call", { name, arguments: input });
93
+ if (res.error) {
94
+ return { content: res.error.message, isError: true };
95
+ }
96
+ const result = res.result;
97
+ return {
98
+ content: result?.content ?? "",
99
+ isError: Boolean(result?.isError)
100
+ };
101
+ }
102
+ async close() {
103
+ if (this.child) {
104
+ try {
105
+ this.child.kill();
106
+ } catch {
107
+ }
108
+ }
109
+ this.state = "disconnected";
110
+ }
111
+ request(method, params) {
112
+ const id = this.nextId++;
113
+ const req = { jsonrpc: "2.0", id, method, params };
114
+ return new Promise((resolve, reject) => {
115
+ this.pending.set(id, resolve);
116
+ try {
117
+ this.child?.stdin?.write(JSON.stringify(req) + "\n");
118
+ } catch (err) {
119
+ this.pending.delete(id);
120
+ reject(err);
121
+ }
122
+ });
123
+ }
124
+ async notify(method, params) {
125
+ const req = { jsonrpc: "2.0", method, params };
126
+ const encoded = JSON.stringify(req) + "\n";
127
+ try {
128
+ const ok = this.child?.stdin?.write(encoded);
129
+ if (!ok) {
130
+ if (this._drainPending) {
131
+ this._lastNotifySkipped = true;
132
+ process.emitWarning(
133
+ `[MCP] notify("${method}") skipped: stdin buffer backpressure (already waiting for drain)`
134
+ );
135
+ return;
136
+ }
137
+ this._drainPending = true;
138
+ await new Promise((resolve, reject) => {
139
+ const timeout = setTimeout(() => {
140
+ this._drainPending = false;
141
+ reject(new Error(`MCP notify("${method}") drain timeout`));
142
+ }, 500);
143
+ this.child?.stdin?.once("drain", () => {
144
+ clearTimeout(timeout);
145
+ this._drainPending = false;
146
+ resolve();
147
+ });
148
+ this.child?.stdin?.once("error", (err) => {
149
+ clearTimeout(timeout);
150
+ this._drainPending = false;
151
+ reject(err);
152
+ });
153
+ });
154
+ }
155
+ } catch (err) {
156
+ throw new Error(`[MCP] notify("${method}") failed: ${err instanceof Error ? err.message : String(err)}`);
157
+ }
158
+ }
159
+ onData(s) {
160
+ this.rxBuffer += s;
161
+ let idx = this.rxBuffer.indexOf("\n");
162
+ while (idx !== -1) {
163
+ const line = this.rxBuffer.slice(0, idx).trim();
164
+ this.rxBuffer = this.rxBuffer.slice(idx + 1);
165
+ if (line) this.onLine(line);
166
+ idx = this.rxBuffer.indexOf("\n");
167
+ }
168
+ }
169
+ onLine(line) {
170
+ let msg;
171
+ try {
172
+ msg = JSON.parse(line);
173
+ } catch {
174
+ return;
175
+ }
176
+ if (msg.id !== void 0 && this.pending.has(msg.id)) {
177
+ const resolve = this.pending.get(msg.id);
178
+ this.pending.delete(msg.id);
179
+ resolve?.(msg);
180
+ }
181
+ }
182
+ };
183
+
184
+ // src/wrap-tool.ts
185
+ var MUTATING_RE = /create|update|delete|write|send|set|put|post|patch|remove|rename|move/i;
186
+ function wrapMCPTool(serverName, mcpTool, client, permission = "confirm") {
187
+ const qualifiedName = `mcp__${serverName}__${mcpTool.name}`;
188
+ return {
189
+ name: qualifiedName,
190
+ description: mcpTool.description ?? `${qualifiedName} (MCP tool)`,
191
+ usageHint: `Tool provided by MCP server "${serverName}". ${mcpTool.description ?? ""}`,
192
+ permission,
193
+ mutating: MUTATING_RE.test(mcpTool.name),
194
+ inputSchema: mcpTool.inputSchema ?? { type: "object", properties: {} },
195
+ async execute(input, ctx, opts) {
196
+ const res = await client.callTool(mcpTool.name, input);
197
+ if (res.isError) {
198
+ throw new Error(stringify(res.content));
199
+ }
200
+ return stringify(res.content);
201
+ }
202
+ };
203
+ }
204
+ function stringify(c) {
205
+ if (typeof c === "string") return c;
206
+ if (Array.isArray(c)) {
207
+ return c.map((item) => {
208
+ if (item && typeof item === "object") {
209
+ const t = item.type;
210
+ if (t === "text") return item.text ?? "";
211
+ return JSON.stringify(item);
212
+ }
213
+ return String(item);
214
+ }).join("\n");
215
+ }
216
+ if (c && typeof c === "object") {
217
+ if ("text" in c) {
218
+ return String(c.text);
219
+ }
220
+ return JSON.stringify(c);
221
+ }
222
+ return String(c ?? "");
223
+ }
224
+
225
+ // src/registry.ts
226
+ var MCPRegistry = class {
227
+ servers = /* @__PURE__ */ new Map();
228
+ toolRegistry;
229
+ events;
230
+ log;
231
+ constructor(opts) {
232
+ this.toolRegistry = opts.toolRegistry;
233
+ this.events = opts.events;
234
+ this.log = opts.log;
235
+ }
236
+ async start(cfg) {
237
+ if (cfg.enabled === false) return;
238
+ const slot = {
239
+ cfg,
240
+ state: "idle",
241
+ toolNames: [],
242
+ attempts: 0
243
+ };
244
+ this.servers.set(cfg.name, slot);
245
+ await this.attemptConnect(slot);
246
+ }
247
+ async stop(name) {
248
+ const slot = this.servers.get(name);
249
+ if (!slot) return;
250
+ if (slot.client) await slot.client.close();
251
+ for (const t of slot.toolNames) this.toolRegistry.unregister(t);
252
+ slot.toolNames = [];
253
+ slot.state = "disconnected";
254
+ this.events.emit("mcp.server.disconnected", { name, reason: "stop" });
255
+ }
256
+ async restart(name) {
257
+ const slot = this.servers.get(name);
258
+ if (!slot) throw new Error(`MCP server "${name}" not registered`);
259
+ await this.stop(name);
260
+ slot.attempts = 0;
261
+ await this.attemptConnect(slot);
262
+ }
263
+ list() {
264
+ return Array.from(this.servers.values()).map((s) => ({
265
+ name: s.cfg.name,
266
+ state: s.state,
267
+ toolCount: s.toolNames.length
268
+ }));
269
+ }
270
+ async stopAll() {
271
+ for (const name of Array.from(this.servers.keys())) {
272
+ await this.stop(name);
273
+ }
274
+ }
275
+ async attemptConnect(slot) {
276
+ const MAX_ATTEMPTS = 3;
277
+ let attempt = 0;
278
+ while (attempt < MAX_ATTEMPTS) {
279
+ attempt++;
280
+ slot.state = attempt === 1 ? "connecting" : "reconnecting";
281
+ slot.attempts = attempt;
282
+ try {
283
+ const client = new MCPClient({
284
+ name: slot.cfg.name,
285
+ transport: slot.cfg.transport,
286
+ command: slot.cfg.command,
287
+ args: slot.cfg.args,
288
+ env: slot.cfg.env,
289
+ url: slot.cfg.url,
290
+ headers: slot.cfg.headers,
291
+ startupTimeoutMs: slot.cfg.startupTimeoutMs
292
+ });
293
+ await client.connect();
294
+ slot.client = client;
295
+ const isReconnect = attempt > 1;
296
+ slot.state = "connected";
297
+ const allowed = slot.cfg.allowedTools;
298
+ const wrapped = client.listTools().filter((t) => !allowed || allowed.includes(t.name)).map((t) => wrapMCPTool(slot.cfg.name, t, client, slot.cfg.permission ?? "confirm"));
299
+ for (const tool of wrapped) {
300
+ try {
301
+ this.toolRegistry.register(tool, `mcp:${slot.cfg.name}`);
302
+ slot.toolNames.push(tool.name);
303
+ } catch (err) {
304
+ this.log.warn(`MCP tool "${tool.name}" not registered`, err);
305
+ }
306
+ }
307
+ this.events.emit(isReconnect ? "mcp.server.reconnected" : "mcp.server.connected", {
308
+ name: slot.cfg.name,
309
+ toolCount: slot.toolNames.length
310
+ });
311
+ return;
312
+ } catch (err) {
313
+ this.log.warn(`MCP server "${slot.cfg.name}" connect attempt ${attempt} failed`, err);
314
+ if (attempt >= MAX_ATTEMPTS) {
315
+ this.log.error(`MCP server "${slot.cfg.name}" connect exhausted after ${MAX_ATTEMPTS} attempts`, err);
316
+ slot.state = "failed";
317
+ this.events.emit("mcp.server.disconnected", {
318
+ name: slot.cfg.name,
319
+ reason: err instanceof Error ? err.message : "unknown"
320
+ });
321
+ return;
322
+ }
323
+ const delay = 500 * 2 ** attempt;
324
+ await new Promise((r) => setTimeout(r, delay));
325
+ }
326
+ }
327
+ }
328
+ };
329
+
330
+ export { MCPClient, MCPRegistry, wrapMCPTool };
331
+ //# sourceMappingURL=index.js.map
332
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/client.ts","../src/wrap-tool.ts","../src/registry.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import { spawn, type ChildProcess } from 'node:child_process';\n\nexport type Transport = 'stdio' | 'sse' | 'streamable-http';\n\nexport interface MCPClientOptions {\n name: string;\n transport: Transport;\n command?: string;\n args?: string[];\n env?: Record<string, string>;\n url?: string;\n headers?: Record<string, string>;\n startupTimeoutMs?: number;\n}\n\nexport type ConnectionState =\n | 'idle'\n | 'connecting'\n | 'connected'\n | 'disconnected'\n | 'reconnecting'\n | 'failed';\n\nexport interface MCPTool {\n name: string;\n description?: string;\n inputSchema: Record<string, unknown>;\n}\n\nexport interface ToolCallResult {\n content: unknown;\n isError: boolean;\n}\n\ninterface JsonRpcRequest {\n jsonrpc: '2.0';\n id: number;\n method: string;\n params?: unknown;\n}\n\ninterface JsonRpcResponse {\n jsonrpc: '2.0';\n id: number;\n result?: unknown;\n error?: { code: number; message: string; data?: unknown };\n}\n\n/**\n * Lightweight stdio MCP client. Supports JSON-RPC 2.0 over newline-delimited JSON.\n * SSE / streamable-http are stubbed and throw on connect — to be filled by a real impl.\n */\nexport class MCPClient {\n private state: ConnectionState = 'idle';\n private child?: ChildProcess;\n private nextId = 1;\n private readonly pending = new Map<number, (res: JsonRpcResponse) => void>();\n private rxBuffer = '';\n private tools: MCPTool[] = [];\n /**\n * Guards against multiple concurrent drain-waits. When `stdin.write()`\n * returns false the first waiter sets this flag; any subsequent callers\n * skip the drain wait and emit a warning instead of racing.\n */\n private _drainPending = false;\n /** Set when a notify() call failed for reasons the caller should know about. */\n private _lastNotifySkipped = false;\n\n constructor(public readonly opts: MCPClientOptions) {}\n\n getState(): ConnectionState {\n return this.state;\n }\n\n listTools(): MCPTool[] {\n return [...this.tools];\n }\n\n /** Returns true if a prior notify() call was skipped due to backpressure. */\n hadNotifySkipped(): boolean {\n return this._lastNotifySkipped;\n }\n\n async connect(): Promise<void> {\n this.state = 'connecting';\n if (this.opts.transport !== 'stdio') {\n this.state = 'failed';\n throw new Error(`MCP transport \"${this.opts.transport}\" not supported in this build`);\n }\n if (!this.opts.command) {\n this.state = 'failed';\n throw new Error('MCP stdio transport requires \"command\"');\n }\n\n const child = spawn(this.opts.command, this.opts.args ?? [], {\n env: { ...process.env, ...this.opts.env },\n stdio: ['pipe', 'pipe', 'pipe'],\n });\n this.child = child;\n\n child.stdout?.on('data', (chunk: Buffer) => this.onData(chunk.toString()));\n child.stderr?.on('data', () => {\n // intentionally discard stderr noise from server\n });\n child.on('exit', () => {\n this.state = 'disconnected';\n });\n child.on('error', () => {\n this.state = 'failed';\n });\n\n const timeout = this.opts.startupTimeoutMs ?? 10_000;\n const initialize = await Promise.race([\n this.request('initialize', {\n protocolVersion: '2024-11-05',\n capabilities: { tools: {} },\n clientInfo: { name: 'wrongstack', version: '0.0.1' },\n }),\n new Promise<JsonRpcResponse>((_, rej) =>\n setTimeout(() => rej(new Error('MCP initialize timeout')), timeout),\n ),\n ]);\n if (initialize.error) {\n this.state = 'failed';\n throw new Error(`MCP initialize failed: ${initialize.error.message}`);\n }\n try {\n await this.notify('notifications/initialized', {});\n } catch (err) {\n // some servers don't require this; failures are logged as warnings\n console.warn(\n '[MCP] notify(\"notifications/initialized\") failed for \"' + this.opts.name + '\": ' + (err instanceof Error ? err.message : String(err)),\n );\n }\n const toolsRes = await this.request('tools/list', {});\n if (toolsRes.error) {\n this.tools = [];\n } else {\n const result = toolsRes.result as { tools?: MCPTool[] } | undefined;\n this.tools = result?.tools ?? [];\n }\n this.state = 'connected';\n }\n\n async callTool(name: string, input: unknown): Promise<ToolCallResult> {\n if (this.state !== 'connected') {\n throw new Error(`MCP client \"${this.opts.name}\" not connected (state=${this.state})`);\n }\n const res = await this.request('tools/call', { name, arguments: input });\n if (res.error) {\n return { content: res.error.message, isError: true };\n }\n const result = res.result as { content?: unknown; isError?: boolean } | undefined;\n return {\n content: result?.content ?? '',\n isError: Boolean(result?.isError),\n };\n }\n\n async close(): Promise<void> {\n if (this.child) {\n try {\n this.child.kill();\n } catch {\n // ignore\n }\n }\n this.state = 'disconnected';\n }\n\n private request(method: string, params: unknown): Promise<JsonRpcResponse> {\n const id = this.nextId++;\n const req: JsonRpcRequest = { jsonrpc: '2.0', id, method, params };\n return new Promise((resolve, reject) => {\n this.pending.set(id, resolve);\n try {\n this.child?.stdin?.write(JSON.stringify(req) + '\\n');\n } catch (err) {\n this.pending.delete(id);\n reject(err);\n }\n });\n }\n\n private async notify(method: string, params: unknown): Promise<void> {\n const req = { jsonrpc: '2.0', method, params };\n const encoded = JSON.stringify(req) + '\\n';\n try {\n const ok = this.child?.stdin?.write(encoded);\n if (!ok) {\n // Only the first caller waits for drain; others just warn and return.\n // This avoids a race where two concurrent notify() calls each start\n // their own drain-wait, then both resolve and the buffer is still full.\n if (this._drainPending) {\n this._lastNotifySkipped = true;\n process.emitWarning(\n `[MCP] notify(\"${method}\") skipped: stdin buffer backpressure (already waiting for drain)`,\n );\n return;\n }\n this._drainPending = true;\n await new Promise<void>((resolve, reject) => {\n const timeout = setTimeout(() => {\n this._drainPending = false;\n reject(new Error(`MCP notify(\"${method}\") drain timeout`));\n }, 500);\n this.child?.stdin?.once('drain', () => {\n clearTimeout(timeout);\n this._drainPending = false;\n resolve();\n });\n this.child?.stdin?.once('error', (err) => {\n clearTimeout(timeout);\n this._drainPending = false;\n reject(err);\n });\n });\n }\n } catch (err) {\n throw new Error(`[MCP] notify(\"${method}\") failed: ${err instanceof Error ? err.message : String(err)}`);\n }\n }\n\n private onData(s: string): void {\n this.rxBuffer += s;\n let idx = this.rxBuffer.indexOf('\\n');\n while (idx !== -1) {\n const line = this.rxBuffer.slice(0, idx).trim();\n this.rxBuffer = this.rxBuffer.slice(idx + 1);\n if (line) this.onLine(line);\n idx = this.rxBuffer.indexOf('\\n');\n }\n }\n\n private onLine(line: string): void {\n let msg: JsonRpcResponse;\n try {\n msg = JSON.parse(line) as JsonRpcResponse;\n } catch {\n return;\n }\n if (msg.id !== undefined && this.pending.has(msg.id)) {\n const resolve = this.pending.get(msg.id);\n this.pending.delete(msg.id);\n resolve?.(msg);\n }\n }\n}\n","import type { Tool, Permission } from '@wrongstack/core';\nimport type { MCPClient, MCPTool } from './client.js';\n\nconst MUTATING_RE = /create|update|delete|write|send|set|put|post|patch|remove|rename|move/i;\n\nexport function wrapMCPTool(\n serverName: string,\n mcpTool: MCPTool,\n client: MCPClient,\n permission: Permission = 'confirm',\n): Tool {\n const qualifiedName = `mcp__${serverName}__${mcpTool.name}`;\n return {\n name: qualifiedName,\n description: mcpTool.description ?? `${qualifiedName} (MCP tool)`,\n usageHint: `Tool provided by MCP server \"${serverName}\". ${mcpTool.description ?? ''}`,\n permission,\n mutating: MUTATING_RE.test(mcpTool.name),\n inputSchema: mcpTool.inputSchema ?? { type: 'object', properties: {} },\n async execute(input, ctx, opts) {\n const res = await client.callTool(mcpTool.name, input);\n if (res.isError) {\n throw new Error(stringify(res.content));\n }\n return stringify(res.content);\n },\n };\n}\n\nfunction stringify(c: unknown): string {\n if (typeof c === 'string') return c;\n if (Array.isArray(c)) {\n return c\n .map((item) => {\n if (item && typeof item === 'object') {\n const t = (item as { type?: string; text?: string }).type;\n if (t === 'text') return (item as { text?: string }).text ?? '';\n return JSON.stringify(item);\n }\n return String(item);\n })\n .join('\\n');\n }\n if (c && typeof c === 'object') {\n if ('text' in (c as Record<string, unknown>)) {\n return String((c as Record<string, unknown>).text);\n }\n return JSON.stringify(c);\n }\n return String(c ?? '');\n}\n","import type { EventBus, MCPServerConfig, ToolRegistry, Logger } from '@wrongstack/core';\nimport { MCPClient, type ConnectionState } from './client.js';\nimport { wrapMCPTool } from './wrap-tool.js';\n\ninterface ServerSlot {\n cfg: MCPServerConfig;\n client?: MCPClient;\n state: ConnectionState;\n toolNames: string[];\n attempts: number;\n}\n\nexport interface MCPRegistryOptions {\n toolRegistry: ToolRegistry;\n events: EventBus;\n log: Logger;\n}\n\nexport class MCPRegistry {\n private readonly servers = new Map<string, ServerSlot>();\n private readonly toolRegistry: ToolRegistry;\n private readonly events: EventBus;\n private readonly log: Logger;\n\n constructor(opts: MCPRegistryOptions) {\n this.toolRegistry = opts.toolRegistry;\n this.events = opts.events;\n this.log = opts.log;\n }\n\n async start(cfg: MCPServerConfig): Promise<void> {\n if (cfg.enabled === false) return;\n const slot: ServerSlot = {\n cfg,\n state: 'idle',\n toolNames: [],\n attempts: 0,\n };\n this.servers.set(cfg.name, slot);\n await this.attemptConnect(slot);\n }\n\n async stop(name: string): Promise<void> {\n const slot = this.servers.get(name);\n if (!slot) return;\n if (slot.client) await slot.client.close();\n for (const t of slot.toolNames) this.toolRegistry.unregister(t);\n slot.toolNames = [];\n slot.state = 'disconnected';\n this.events.emit('mcp.server.disconnected', { name, reason: 'stop' });\n }\n\n async restart(name: string): Promise<void> {\n const slot = this.servers.get(name);\n if (!slot) throw new Error(`MCP server \"${name}\" not registered`);\n await this.stop(name);\n slot.attempts = 0;\n await this.attemptConnect(slot);\n }\n\n list(): { name: string; state: ConnectionState; toolCount: number }[] {\n return Array.from(this.servers.values()).map((s) => ({\n name: s.cfg.name,\n state: s.state,\n toolCount: s.toolNames.length,\n }));\n }\n\n async stopAll(): Promise<void> {\n for (const name of Array.from(this.servers.keys())) {\n await this.stop(name);\n }\n }\n\n private async attemptConnect(slot: ServerSlot): Promise<void> {\n const MAX_ATTEMPTS = 3;\n let attempt = 0;\n while (attempt < MAX_ATTEMPTS) {\n attempt++;\n slot.state = attempt === 1 ? 'connecting' : 'reconnecting';\n slot.attempts = attempt;\n try {\n const client = new MCPClient({\n name: slot.cfg.name,\n transport: slot.cfg.transport,\n command: slot.cfg.command,\n args: slot.cfg.args,\n env: slot.cfg.env,\n url: slot.cfg.url,\n headers: slot.cfg.headers,\n startupTimeoutMs: slot.cfg.startupTimeoutMs,\n });\n await client.connect();\n slot.client = client;\n const isReconnect = attempt > 1;\n slot.state = 'connected';\n const allowed = slot.cfg.allowedTools;\n const wrapped = client\n .listTools()\n .filter((t) => !allowed || allowed.includes(t.name))\n .map((t) => wrapMCPTool(slot.cfg.name, t, client, slot.cfg.permission ?? 'confirm'));\n for (const tool of wrapped) {\n try {\n this.toolRegistry.register(tool, `mcp:${slot.cfg.name}`);\n slot.toolNames.push(tool.name);\n } catch (err) {\n this.log.warn(`MCP tool \"${tool.name}\" not registered`, err);\n }\n }\n this.events.emit(isReconnect ? 'mcp.server.reconnected' : 'mcp.server.connected', {\n name: slot.cfg.name,\n toolCount: slot.toolNames.length,\n });\n return; // success\n } catch (err) {\n this.log.warn(`MCP server \"${slot.cfg.name}\" connect attempt ${attempt} failed`, err);\n if (attempt >= MAX_ATTEMPTS) {\n this.log.error(`MCP server \"${slot.cfg.name}\" connect exhausted after ${MAX_ATTEMPTS} attempts`, err);\n slot.state = 'failed';\n this.events.emit('mcp.server.disconnected', {\n name: slot.cfg.name,\n reason: err instanceof Error ? err.message : 'unknown',\n });\n return;\n }\n const delay = 500 * 2 ** attempt;\n await new Promise((r) => setTimeout(r, delay));\n }\n }\n }\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@wrongstack/mcp",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.js"
11
+ }
12
+ },
13
+ "files": [
14
+ "dist"
15
+ ],
16
+ "dependencies": {
17
+ "@wrongstack/core": "0.1.0"
18
+ },
19
+ "devDependencies": {
20
+ "@types/node": "^22.10.0",
21
+ "tsup": "^8.3.5",
22
+ "typescript": "^5.7.2",
23
+ "vitest": "^4.1.6"
24
+ },
25
+ "scripts": {
26
+ "build": "tsup",
27
+ "typecheck": "tsc --noEmit -p tsconfig.test.json",
28
+ "test": "vitest run",
29
+ "clean": "rm -rf dist"
30
+ }
31
+ }