@wrongstack/mcp 0.1.2 → 0.1.4

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 CHANGED
@@ -1,17 +1,21 @@
1
- Apache License
2
- Version 2.0, January 2004
3
- http://www.apache.org/licenses/
1
+ MIT License
4
2
 
5
- Copyright 2026 ECOSTACK TECHNOLOGY OÜ
3
+ Copyright (c) 2026 ECOSTACK TECHNOLOGY OÜ
6
4
 
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
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:
10
11
 
11
- http://www.apache.org/licenses/LICENSE-2.0
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
12
14
 
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.
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.
package/README.md ADDED
@@ -0,0 +1,198 @@
1
+ # MCP Servers — WrongStack
2
+
3
+ Connect any [Model Context Protocol](https://modelcontextprotocol.io) server to WrongStack and use its tools as if they were built-in.
4
+
5
+ ## Supported Transports
6
+
7
+ | Transport | Description |
8
+ |-----------|-------------|
9
+ | `stdio` | Spawns a local binary; communicates over stdin/stdout |
10
+ | `sse` | HTTP Server-Sent Events for server events; HTTP POST for requests |
11
+ | `streamable-http` | Session-based HTTP with NDJSON responses |
12
+
13
+ ## Quick Start
14
+
15
+ ```json
16
+ // ~/.wrongstack/config.json
17
+ {
18
+ "mcpServers": {
19
+ "filesystem": {
20
+ "enabled": true
21
+ }
22
+ }
23
+ }
24
+ ```
25
+
26
+ Then run `wstack` — the filesystem server is now active with tools like `mcp__filesystem__read_file`, `mcp__filesystem__write_file`, etc.
27
+
28
+ ## Built-in Server Presets
29
+
30
+ WrongStack bundles configs for popular MCP servers. Enable them with:
31
+
32
+ ```bash
33
+ # Add a server (disabled by default)
34
+ wstack mcp add filesystem
35
+
36
+ # Add and enable immediately
37
+ wstack mcp add github --enable
38
+
39
+ # List configured servers
40
+ wstack mcp list
41
+ ```
42
+
43
+ ### Available Presets
44
+
45
+ | Name | Transport | Description | Requires |
46
+ |------|-----------|-------------|----------|
47
+ | `filesystem` | stdio | Read, write, list, and search local files | — |
48
+ | `github` | stdio | Issues, PRs, repos, search, file operations | `GITHUB_PERSONAL_ACCESS_TOKEN` |
49
+ | `context7` | streamable-http | Codebase-aware documentation and Q&A | — |
50
+ | `brave-search` | stdio | Web search | `BRAVE_SEARCH_API_KEY` (free 2k/month) |
51
+ | `block` | stdio | Postgres via SQL | — |
52
+ | `everart` | stdio | AI image generation | `EVERART_API_KEY` |
53
+ | `slack` | stdio | Messaging, channels, search | `SLACK_BOT_TOKEN` + `SLACK_TEAM_ID` |
54
+ | `aws` | stdio | EC2, S3, Lambda, IAM, CloudFormation | AWS credentials |
55
+ | `google-maps` | stdio | Directions, geocoding, places | `GOOGLE_MAPS_API_KEY` |
56
+ | `sentinel` | streamable-http | Security vulnerability scanning | — |
57
+
58
+ ## Manual Configuration
59
+
60
+ ```json
61
+ {
62
+ "mcpServers": {
63
+ "my-server": {
64
+ "name": "my-server",
65
+ "description": "My custom MCP server",
66
+ "transport": "stdio",
67
+ "command": "npx",
68
+ "args": ["-y", "@my-org/mcp-server", "--verbose"],
69
+ "enabled": true,
70
+ "permission": "confirm",
71
+ "allowedTools": ["read", "search"],
72
+ "startupTimeoutMs": 10000
73
+ }
74
+ }
75
+ }
76
+ ```
77
+
78
+ ### Configuration Reference
79
+
80
+ ```typescript
81
+ interface MCPServerConfig {
82
+ name: string;
83
+ transport: 'stdio' | 'sse' | 'streamable-http';
84
+
85
+ // stdio — local binary
86
+ command?: string; // e.g. "npx", "/usr/local/bin/my-mcp-server"
87
+ args?: string[];
88
+ env?: Record<string, string>;
89
+
90
+ // sse / streamable-http — remote
91
+ url?: string;
92
+ headers?: Record<string, string>;
93
+
94
+ // Common options
95
+ enabled?: boolean; // default: true
96
+ allowedTools?: string[]; // whitelist — undefined = all tools
97
+ permission?: Permission; // 'auto' | 'confirm' | 'deny' (default: 'confirm')
98
+ startupTimeoutMs?: number; // default: 10000
99
+ description?: string; // shown in `wstack mcp list`
100
+ }
101
+ ```
102
+
103
+ ## MCP Tool Naming
104
+
105
+ All MCP tools are prefixed with `mcp__<server-name>__` to prevent collision:
106
+
107
+ ```
108
+ mcp__github__list_issues
109
+ mcp__github__create_pull_request
110
+ mcp__filesystem__read_file
111
+ mcp__filesystem__write_file
112
+ ```
113
+
114
+ This means you can have a built-in `read_file` tool AND `mcp__filesystem__read_file` without conflict.
115
+
116
+ ## Permission Model
117
+
118
+ All MCP tools default to `permission: 'confirm'` regardless of whether they look read-only. This means the agent asks before using them. You can:
119
+
120
+ 1. **Trust a server** — set `permission: 'auto'` in the server config (all tools run without asking)
121
+ 2. **Trust specific tools** — use a trust policy config (see `Config` type):
122
+ ```json
123
+ { "mcp__github__*": { "auto": true } }
124
+ ```
125
+ 3. **Deny a server** — set `permission: 'deny'` (tools exist but agent must explicitly request confirmation)
126
+
127
+ ## Reconnection
128
+
129
+ - **stdio servers**: If the child process exits, WrongStack automatically retries up to 3 times with exponential backoff (500ms → 1s → 2s). After 3 failures the server is marked `failed` and tools are unregistered.
130
+ - **HTTP servers (SSE / streamable-http)**: If the SSE read loop errors, the transport transitions to `disconnected` and the registry schedules a reconnect.
131
+
132
+ ## Troubleshooting
133
+
134
+ ### Server fails to start
135
+
136
+ Check that:
137
+ - The `command` is in your `PATH` (or use an absolute path)
138
+ - `npx -y @modelcontextprotocol/server-filesystem .` works standalone
139
+ - The `startupTimeoutMs` is sufficient for slow-starting servers
140
+
141
+ ### Tools not appearing
142
+
143
+ ```bash
144
+ wstack mcp list # shows configured servers + state
145
+ wstack diag # shows MCP health under "MCP Servers:"
146
+ ```
147
+
148
+ ### Connection drops repeatedly
149
+
150
+ This usually means the server process crashes on startup. Try running it manually:
151
+ ```bash
152
+ node /path/to/server/script.js
153
+ # then type: {"jsonrpc":"2.0","id":1,"method":"initialize","params":{...}}
154
+ ```
155
+
156
+ ### `allowedTools` not filtering
157
+
158
+ Restart after changing `allowedTools` — the tool list is cached after discovery.
159
+
160
+ ## Writing Your Own MCP Server
161
+
162
+ Any server that implements the [MCP spec](https://spec.modelcontextprotocol.io) works with WrongStack. Minimal example:
163
+
164
+ ```typescript
165
+ // server.js — stdio MCP server
166
+ import { createInterface } from 'readline';
167
+
168
+ const rl = createInterface({ input: process.stdin, terminal: false });
169
+ rl.on('line', async (line) => {
170
+ const req = JSON.parse(line);
171
+ if (req.method === 'initialize') {
172
+ process.stdout.write(JSON.stringify({
173
+ jsonrpc: '2.0', id: req.id,
174
+ result: { protocolVersion: '2024-11-05', capabilities: { tools: {} }, serverInfo: { name: 'my-server', version: '1.0.0' } }
175
+ }) + '\n');
176
+ process.stdout.flush();
177
+ } else if (req.method === 'tools/list') {
178
+ process.stdout.write(JSON.stringify({
179
+ jsonrpc: '2.0', id: req.id,
180
+ result: { tools: [{ name: 'greet', description: 'Say hello', inputSchema: { type: 'object', properties: { name: { type: 'string' } } } }] }
181
+ }) + '\n');
182
+ process.stdout.flush();
183
+ }
184
+ });
185
+ ```
186
+
187
+ Then in your config:
188
+ ```json
189
+ {
190
+ "mcpServers": {
191
+ "my-server": {
192
+ "transport": "stdio",
193
+ "command": "node",
194
+ "args": ["/path/to/server.js"]
195
+ }
196
+ }
197
+ }
198
+ ```
package/dist/index.d.ts CHANGED
@@ -21,9 +21,18 @@ interface ToolCallResult {
21
21
  content: unknown;
22
22
  isError: boolean;
23
23
  }
24
+ type ExitListener = (name: string, code: number | null, signal: string | null) => void;
24
25
  /**
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.
26
+ * Fired when the server sends `notifications/tools/list_changed`. The
27
+ * client refreshes its cached tool list before invoking listeners, so
28
+ * subscribers can call `listTools()` for the fresh set.
29
+ */
30
+ type ToolsChangedListener = (name: string, tools: MCPTool[]) => void;
31
+ /**
32
+ * Lightweight MCP client supporting three transport types:
33
+ * - stdio: spawns a child process and communicates over pipes
34
+ * - sse: connects to an HTTP SSE endpoint for server events, POST for requests
35
+ * - streamable-http: session-based HTTP transport with NDJSON responses
27
36
  */
28
37
  declare class MCPClient {
29
38
  readonly opts: MCPClientOptions;
@@ -32,27 +41,55 @@ declare class MCPClient {
32
41
  private nextId;
33
42
  private readonly pending;
34
43
  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
- */
44
+ private _tools;
45
+ /** Cached tool list — survives reconnects so the registry can re-register without re-discovering. */
46
+ private _toolsCache?;
41
47
  private _drainPending;
42
- /** Set when a notify() call failed for reasons the caller should know about. */
43
48
  private _lastNotifySkipped;
49
+ private sseTransport?;
50
+ private httpTransport?;
51
+ /** Notified when the stdio child process exits so the registry can attempt reconnect. */
52
+ private readonly exitListeners;
53
+ /** Notified when the server announces a tools/list_changed notification. */
54
+ private readonly toolsChangedListeners;
55
+ /** Notified when an HTTP transport (SSE or streamable-http) disconnects. */
56
+ private readonly disconnectListeners;
44
57
  constructor(opts: MCPClientOptions);
45
58
  getState(): ConnectionState;
46
59
  listTools(): MCPTool[];
47
60
  /** Returns true if a prior notify() call was skipped due to backpressure. */
48
61
  hadNotifySkipped(): boolean;
62
+ /**
63
+ * Register a listener for child-process exit events.
64
+ * The registry uses this to trigger reconnection.
65
+ */
66
+ addExitListener(listener: ExitListener): void;
67
+ removeExitListener(listener: ExitListener): void;
68
+ /**
69
+ * Register a listener for transport disconnect events (SSE / streamable-http).
70
+ * Used by the registry to trigger reconnection for HTTP-based servers.
71
+ */
72
+ addDisconnectListener(listener: () => void): void;
73
+ removeDisconnectListener(listener: () => void): void;
49
74
  connect(): Promise<void>;
75
+ private connectStdio;
76
+ private connectSSE;
77
+ private connectStreamableHTTP;
50
78
  callTool(name: string, input: unknown): Promise<ToolCallResult>;
51
79
  close(): Promise<void>;
52
80
  private request;
53
81
  private notify;
54
82
  private onData;
55
83
  private onLine;
84
+ /**
85
+ * L2-C: refresh the cached tool list when the server announces a
86
+ * `tools/list_changed`. Listeners (the registry) re-wrap and
87
+ * re-register. Failures are swallowed — a stale cache is preferable
88
+ * to a hard crash on a transient notification glitch.
89
+ */
90
+ private handleToolsListChanged;
91
+ addToolsChangedListener(listener: ToolsChangedListener): void;
92
+ removeToolsChangedListener(listener: ToolsChangedListener): void;
56
93
  }
57
94
 
58
95
  declare function wrapMCPTool(serverName: string, mcpTool: MCPTool, client: MCPClient, permission?: Permission): Tool;
@@ -77,7 +114,130 @@ declare class MCPRegistry {
77
114
  toolCount: number;
78
115
  }[];
79
116
  stopAll(): Promise<void>;
117
+ /**
118
+ * Health check — returns 'ok' for connected servers, the current state otherwise.
119
+ * For HTTP-based transports this could also ping the server.
120
+ */
121
+ health(): {
122
+ name: string;
123
+ alive: boolean;
124
+ latencyMs?: number;
125
+ }[];
126
+ /**
127
+ * L2-C: handle `notifications/tools/list_changed` from the server.
128
+ * Unregister the previous wrapper set, then re-register the fresh
129
+ * tool list. The client has already refreshed its cache before
130
+ * dispatching — we just need to re-wrap and re-register.
131
+ */
132
+ private readonly onToolsChanged;
133
+ private readonly onChildExit;
134
+ /** Handles SSE / streamable-http disconnect — same recovery as stdio child exit. */
135
+ private readonly onTransportDisconnect;
136
+ /**
137
+ * L2-B: maximum number of reconnect cycles before staying `failed`.
138
+ * One cycle = one full `attemptConnect` (which itself may try up to 3
139
+ * times). Caps total reconnect storm at ~5 cycles, then the slot
140
+ * needs an explicit `restart()` to re-engage.
141
+ */
142
+ private static readonly MAX_RECONNECT_CYCLES;
143
+ /** Base delay between cycles, in ms. Real delay adds jitter. */
144
+ private static readonly BASE_RECONNECT_DELAY_MS;
145
+ /** Hard ceiling on the inter-cycle delay so the user doesn't wait minutes. */
146
+ private static readonly MAX_RECONNECT_DELAY_MS;
147
+ private scheduleReconnect;
148
+ private attemptReconnect;
80
149
  private attemptConnect;
81
150
  }
82
151
 
83
- export { type ConnectionState, MCPClient, type MCPClientOptions, MCPRegistry, type MCPRegistryOptions, type MCPTool, type ToolCallResult, type Transport, wrapMCPTool };
152
+ interface HttpTransportOptions {
153
+ name: string;
154
+ url: string;
155
+ headers?: Record<string, string>;
156
+ startupTimeoutMs?: number;
157
+ }
158
+ /**
159
+ * SSE-based MCP transport using native fetch.
160
+ *
161
+ * Communication pattern:
162
+ * - Client connects to SSE endpoint to receive server messages (JSON-RPC events)
163
+ * - Client sends JSON-RPC requests via HTTP POST to the same or separate endpoint
164
+ * - Server sends results/errors via the SSE stream
165
+ *
166
+ * The SSE reader parses the SSE protocol (event:, data:, blank line to dispatch).
167
+ */
168
+ declare class SSEReader {
169
+ private buffer;
170
+ private listeners;
171
+ onMessage(cb: (data: {
172
+ jsonrpc?: string;
173
+ method?: string;
174
+ params?: unknown;
175
+ id?: number;
176
+ }) => void): () => void;
177
+ feed(chunk: string): void;
178
+ private dispatch;
179
+ reset(): void;
180
+ }
181
+ /**
182
+ * SSE transport for MCP over HTTP.
183
+ *
184
+ * Uses native fetch API with ReadableStream to consume SSE events.
185
+ * HTTP POST is used to send JSON-RPC requests.
186
+ */
187
+ declare class SSETransport {
188
+ private state;
189
+ private url;
190
+ private headers;
191
+ private timeout;
192
+ private nextId;
193
+ private pending;
194
+ private tools;
195
+ private abortController?;
196
+ private reader?;
197
+ private readerDone;
198
+ private disconnectHandlers;
199
+ private readLoopAbort?;
200
+ private readonly toolsChangedListeners;
201
+ constructor(opts: HttpTransportOptions);
202
+ getState(): ConnectionState;
203
+ listTools(): MCPTool[];
204
+ onDisconnect(cb: () => void): () => void;
205
+ onToolsChanged(cb: (tools: MCPTool[]) => void): () => void;
206
+ /** Refresh tool list when server sends notifications/tools/list_changed. */
207
+ private handleToolsListChanged;
208
+ connect(): Promise<void>;
209
+ private readSSEBody;
210
+ private buildSSEUrl;
211
+ private httpPost;
212
+ callTool(name: string, input: unknown): Promise<ToolCallResult>;
213
+ close(): Promise<void>;
214
+ }
215
+ /**
216
+ * Streamable HTTP transport for MCP.
217
+ *
218
+ * Uses session-based HTTP with NDJSON responses.
219
+ */
220
+ declare class StreamableHTTPTransport {
221
+ private state;
222
+ private url;
223
+ private headers;
224
+ private timeout;
225
+ private nextId;
226
+ private tools;
227
+ private abortController?;
228
+ private sessionId?;
229
+ private disconnectHandlers;
230
+ private readonly toolsChangedListeners;
231
+ constructor(opts: HttpTransportOptions);
232
+ getState(): ConnectionState;
233
+ listTools(): MCPTool[];
234
+ onDisconnect(cb: () => void): () => void;
235
+ onToolsChanged(cb: (tools: MCPTool[]) => void): () => void;
236
+ private handleToolsListChanged;
237
+ connect(): Promise<void>;
238
+ private postRaw;
239
+ callTool(name: string, input: unknown): Promise<ToolCallResult>;
240
+ close(): Promise<void>;
241
+ }
242
+
243
+ export { type ConnectionState, type HttpTransportOptions, MCPClient, type MCPClientOptions, MCPRegistry, type MCPRegistryOptions, type MCPTool, SSEReader, SSETransport, StreamableHTTPTransport, type ToolCallResult, type Transport, wrapMCPTool };