@poncho-ai/harness 0.2.0 → 0.3.1

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/test/mcp.test.ts CHANGED
@@ -1,69 +1,112 @@
1
- import { describe, expect, it } from "vitest";
2
- import { WebSocketServer } from "ws";
1
+ import { createServer } from "node:http";
2
+ import { describe, expect, it, vi } from "vitest";
3
3
  import { LocalMcpBridge } from "../src/mcp.js";
4
4
 
5
5
  describe("mcp bridge protocol transports", () => {
6
- it("discovers and calls tools over websocket json-rpc", async () => {
7
- const wss = new WebSocketServer({ port: 0 });
8
- await new Promise<void>((resolveOpen) => {
9
- wss.once("listening", () => resolveOpen());
10
- });
11
-
12
- wss.on("connection", (socket) => {
13
- socket.on("message", (data) => {
14
- const msg = JSON.parse(String(data)) as {
15
- id: number;
16
- method: string;
17
- params?: { arguments?: { value?: string } };
18
- };
19
- if (msg.method === "tools/list") {
20
- socket.send(
21
- JSON.stringify({
22
- jsonrpc: "2.0",
23
- id: msg.id,
24
- result: {
25
- tools: [
26
- {
27
- name: "remotePing",
28
- description: "Remote ping",
29
- inputSchema: {
30
- type: "object",
31
- properties: { value: { type: "string" } },
32
- },
6
+ it("discovers and calls tools over streamable HTTP", async () => {
7
+ process.env.LINEAR_TOKEN = "token-123";
8
+ const requests: string[] = [];
9
+ let authHeader = "";
10
+ let deleteSeen = false;
11
+ const session = "session_test";
12
+ const server = createServer(async (req, res) => {
13
+ if (req.method === "DELETE") {
14
+ deleteSeen = true;
15
+ res.statusCode = 200;
16
+ res.end();
17
+ return;
18
+ }
19
+ const chunks: Buffer[] = [];
20
+ for await (const chunk of req) {
21
+ chunks.push(Buffer.from(chunk));
22
+ }
23
+ const body = Buffer.concat(chunks).toString("utf8");
24
+ const payload = body.trim().length > 0 ? (JSON.parse(body) as any) : {};
25
+ authHeader = req.headers.authorization ?? "";
26
+ if (payload.method) {
27
+ requests.push(payload.method);
28
+ }
29
+ if (payload.method === "initialize") {
30
+ res.setHeader("Content-Type", "application/json");
31
+ res.setHeader("Mcp-Session-Id", session);
32
+ res.end(
33
+ JSON.stringify({
34
+ jsonrpc: "2.0",
35
+ id: payload.id,
36
+ result: {
37
+ protocolVersion: "2025-03-26",
38
+ capabilities: { tools: { listChanged: true } },
39
+ serverInfo: { name: "remote", version: "1.0.0" },
40
+ },
41
+ }),
42
+ );
43
+ return;
44
+ }
45
+ if (payload.method === "notifications/initialized") {
46
+ res.statusCode = 202;
47
+ res.end();
48
+ return;
49
+ }
50
+ if (payload.method === "tools/list") {
51
+ expect(req.headers["mcp-session-id"]).toBe(session);
52
+ res.setHeader("Content-Type", "application/json");
53
+ res.end(
54
+ JSON.stringify({
55
+ jsonrpc: "2.0",
56
+ id: payload.id,
57
+ result: {
58
+ tools: [
59
+ {
60
+ name: "remotePing",
61
+ description: "Remote ping",
62
+ inputSchema: {
63
+ type: "object",
64
+ properties: { value: { type: "string" } },
33
65
  },
34
- ],
35
- },
36
- }),
37
- );
38
- } else if (msg.method === "tools/call") {
39
- socket.send(
40
- JSON.stringify({
41
- jsonrpc: "2.0",
42
- id: msg.id,
43
- result: {
44
- result: { echoed: msg.params?.arguments?.value ?? "" },
45
- },
46
- }),
47
- );
48
- }
49
- });
66
+ },
67
+ ],
68
+ },
69
+ }),
70
+ );
71
+ return;
72
+ }
73
+ if (payload.method === "tools/call") {
74
+ res.setHeader("Content-Type", "text/event-stream");
75
+ res.end(
76
+ `data: ${JSON.stringify({
77
+ jsonrpc: "2.0",
78
+ id: payload.id,
79
+ result: { result: { echoed: payload.params?.arguments?.value ?? "" } },
80
+ })}\n\n`,
81
+ );
82
+ return;
83
+ }
84
+ res.statusCode = 404;
85
+ res.end();
50
86
  });
51
-
52
- const address = wss.address();
87
+ await new Promise<void>((resolveOpen) => server.listen(0, () => resolveOpen()));
88
+ const address = server.address();
53
89
  if (!address || typeof address === "string") {
54
- throw new Error("Unexpected websocket address");
90
+ throw new Error("Unexpected server address");
55
91
  }
56
92
 
57
93
  const bridge = new LocalMcpBridge({
58
- mcp: [{ name: "remote", url: `ws://127.0.0.1:${address.port}` }],
94
+ mcp: [
95
+ {
96
+ name: "remote",
97
+ url: `http://127.0.0.1:${address.port}/mcp`,
98
+ auth: { type: "bearer", tokenEnv: "LINEAR_TOKEN" },
99
+ },
100
+ ],
59
101
  });
60
102
 
61
103
  await bridge.startLocalServers();
62
- const tools = await bridge.loadTools();
63
- const tool = tools.find((entry) => entry.name === "remote:remotePing");
104
+ await bridge.discoverTools();
105
+ const tools = await bridge.loadTools(["remote/remotePing"]);
106
+ const tool = tools.find((entry) => entry.name === "remote/remotePing");
64
107
  expect(tool).toBeDefined();
65
108
  const output = await tool?.handler(
66
- { value: "ws" },
109
+ { value: "http" },
67
110
  {
68
111
  agentId: "agent",
69
112
  runId: "run",
@@ -72,11 +115,263 @@ describe("mcp bridge protocol transports", () => {
72
115
  parameters: {},
73
116
  },
74
117
  );
75
- expect(output).toEqual({ echoed: "ws" });
118
+ expect(output).toEqual({ echoed: "http" });
119
+ expect(authHeader).toBe("Bearer token-123");
120
+ expect(requests).toContain("initialize");
121
+ expect(requests).toContain("tools/list");
122
+ expect(requests).toContain("tools/call");
76
123
 
77
124
  await bridge.stopLocalServers();
78
- await new Promise<void>((resolveClose) => {
79
- wss.close(() => resolveClose());
125
+ await new Promise<void>((resolveClose) => server.close(() => resolveClose()));
126
+ expect(deleteSeen).toBe(true);
127
+ });
128
+
129
+ it("fails fast on duplicate server names", () => {
130
+ expect(
131
+ () =>
132
+ new LocalMcpBridge({
133
+ mcp: [
134
+ { name: "dup", url: "https://example.com/a" },
135
+ { name: "dup", url: "https://example.com/b" },
136
+ ],
137
+ }),
138
+ ).toThrow(/Duplicate MCP server name/);
139
+ });
140
+
141
+ it("applies allowlist and denylist policy filters", async () => {
142
+ process.env.LINEAR_TOKEN = "token-123";
143
+ const server = createServer(async (req, res) => {
144
+ if (req.method === "DELETE") {
145
+ res.statusCode = 200;
146
+ res.end();
147
+ return;
148
+ }
149
+ const chunks: Buffer[] = [];
150
+ for await (const chunk of req) chunks.push(Buffer.from(chunk));
151
+ const body = Buffer.concat(chunks).toString("utf8");
152
+ const payload = body.trim().length > 0 ? (JSON.parse(body) as any) : {};
153
+ if (payload.method === "initialize") {
154
+ res.setHeader("Content-Type", "application/json");
155
+ res.setHeader("Mcp-Session-Id", "s");
156
+ res.end(
157
+ JSON.stringify({
158
+ jsonrpc: "2.0",
159
+ id: payload.id,
160
+ result: {
161
+ protocolVersion: "2025-03-26",
162
+ capabilities: { tools: { listChanged: true } },
163
+ serverInfo: { name: "remote", version: "1.0.0" },
164
+ },
165
+ }),
166
+ );
167
+ return;
168
+ }
169
+ if (payload.method === "notifications/initialized") {
170
+ res.statusCode = 202;
171
+ res.end();
172
+ return;
173
+ }
174
+ if (payload.method === "tools/list") {
175
+ res.setHeader("Content-Type", "application/json");
176
+ res.end(
177
+ JSON.stringify({
178
+ jsonrpc: "2.0",
179
+ id: payload.id,
180
+ result: {
181
+ tools: [
182
+ { name: "a", inputSchema: { type: "object", properties: {} } },
183
+ { name: "b", inputSchema: { type: "object", properties: {} } },
184
+ ],
185
+ },
186
+ }),
187
+ );
188
+ return;
189
+ }
190
+ res.setHeader("Content-Type", "application/json");
191
+ res.end(JSON.stringify({ jsonrpc: "2.0", id: payload.id, result: {} }));
192
+ });
193
+ await new Promise<void>((resolveOpen) => server.listen(0, () => resolveOpen()));
194
+ const address = server.address();
195
+ if (!address || typeof address === "string") {
196
+ throw new Error("Unexpected server address");
197
+ }
198
+ const allowBridge = new LocalMcpBridge({
199
+ mcp: [
200
+ {
201
+ name: "remote",
202
+ url: `http://127.0.0.1:${address.port}/mcp`,
203
+ auth: { type: "bearer", tokenEnv: "LINEAR_TOKEN" },
204
+ tools: { mode: "allowlist", include: ["remote/a"] },
205
+ },
206
+ ],
207
+ });
208
+ await allowBridge.startLocalServers();
209
+ await allowBridge.discoverTools();
210
+ const allowTools = await allowBridge.loadTools(["remote/*"]);
211
+ expect(allowTools.map((tool) => tool.name)).toEqual(["remote/a"]);
212
+ await allowBridge.stopLocalServers();
213
+
214
+ const denyBridge = new LocalMcpBridge({
215
+ mcp: [
216
+ {
217
+ name: "remote",
218
+ url: `http://127.0.0.1:${address.port}/mcp`,
219
+ auth: { type: "bearer", tokenEnv: "LINEAR_TOKEN" },
220
+ tools: { mode: "denylist", exclude: ["remote/b"] },
221
+ },
222
+ ],
223
+ });
224
+ await denyBridge.startLocalServers();
225
+ await denyBridge.discoverTools();
226
+ const denyTools = await denyBridge.loadTools(["remote/*"]);
227
+ expect(denyTools.map((tool) => tool.name).sort()).toEqual(["remote/a"]);
228
+ await denyBridge.stopLocalServers();
229
+ await new Promise<void>((resolveClose) => server.close(() => resolveClose()));
230
+ });
231
+
232
+ it("skips discovery when bearer token env value is missing", async () => {
233
+ delete process.env.MISSING_TOKEN_ENV;
234
+ const bridge = new LocalMcpBridge({
235
+ mcp: [
236
+ {
237
+ name: "remote",
238
+ url: "https://example.com/mcp",
239
+ auth: { type: "bearer", tokenEnv: "MISSING_TOKEN_ENV" },
240
+ },
241
+ ],
80
242
  });
243
+ await bridge.startLocalServers();
244
+ await bridge.discoverTools();
245
+ const tools = await bridge.loadTools(["remote/*"]);
246
+ expect(tools).toEqual([]);
247
+ });
248
+
249
+ it("reports auth failures during discovery and call execution", async () => {
250
+ process.env.LINEAR_TOKEN = "token-123";
251
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => undefined);
252
+ const server = createServer(async (req, res) => {
253
+ if (req.method === "DELETE") {
254
+ res.statusCode = 200;
255
+ res.end();
256
+ return;
257
+ }
258
+ const chunks: Buffer[] = [];
259
+ for await (const chunk of req) chunks.push(Buffer.from(chunk));
260
+ const body = Buffer.concat(chunks).toString("utf8");
261
+ const payload = body.trim().length > 0 ? (JSON.parse(body) as any) : {};
262
+ if (payload.method === "initialize") {
263
+ res.statusCode = 401;
264
+ res.end("unauthorized");
265
+ return;
266
+ }
267
+ res.statusCode = 404;
268
+ res.end();
269
+ });
270
+ await new Promise<void>((resolveOpen) => server.listen(0, () => resolveOpen()));
271
+ const address = server.address();
272
+ if (!address || typeof address === "string") throw new Error("Unexpected address");
273
+ const bridge = new LocalMcpBridge({
274
+ mcp: [
275
+ {
276
+ name: "remote",
277
+ url: `http://127.0.0.1:${address.port}/mcp`,
278
+ auth: { type: "bearer", tokenEnv: "LINEAR_TOKEN" },
279
+ },
280
+ ],
281
+ });
282
+ await bridge.startLocalServers();
283
+ await bridge.discoverTools();
284
+ expect(bridge.listDiscoveredTools()).toEqual([]);
285
+ expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('"event":"auth.failed"'));
286
+ await bridge.stopLocalServers();
287
+ await new Promise<void>((resolveClose) => server.close(() => resolveClose()));
288
+ warnSpy.mockRestore();
289
+ });
290
+
291
+ it("returns actionable errors for 403 permission failures", async () => {
292
+ process.env.LINEAR_TOKEN = "token-123";
293
+ const server = createServer(async (req, res) => {
294
+ if (req.method === "DELETE") {
295
+ res.statusCode = 200;
296
+ res.end();
297
+ return;
298
+ }
299
+ const chunks: Buffer[] = [];
300
+ for await (const chunk of req) chunks.push(Buffer.from(chunk));
301
+ const body = Buffer.concat(chunks).toString("utf8");
302
+ const payload = body.trim().length > 0 ? (JSON.parse(body) as any) : {};
303
+ if (payload.method === "initialize") {
304
+ res.setHeader("Content-Type", "application/json");
305
+ res.setHeader("Mcp-Session-Id", "sess");
306
+ res.end(
307
+ JSON.stringify({
308
+ jsonrpc: "2.0",
309
+ id: payload.id,
310
+ result: {
311
+ protocolVersion: "2025-03-26",
312
+ capabilities: { tools: { listChanged: true } },
313
+ serverInfo: { name: "remote", version: "1.0.0" },
314
+ },
315
+ }),
316
+ );
317
+ return;
318
+ }
319
+ if (payload.method === "notifications/initialized") {
320
+ res.statusCode = 202;
321
+ res.end();
322
+ return;
323
+ }
324
+ if (payload.method === "tools/list") {
325
+ res.setHeader("Content-Type", "application/json");
326
+ res.end(
327
+ JSON.stringify({
328
+ jsonrpc: "2.0",
329
+ id: payload.id,
330
+ result: {
331
+ tools: [{ name: "restricted", inputSchema: { type: "object", properties: {} } }],
332
+ },
333
+ }),
334
+ );
335
+ return;
336
+ }
337
+ if (payload.method === "tools/call") {
338
+ res.statusCode = 403;
339
+ res.end("forbidden");
340
+ return;
341
+ }
342
+ res.statusCode = 404;
343
+ res.end();
344
+ });
345
+ await new Promise<void>((resolveOpen) => server.listen(0, () => resolveOpen()));
346
+ const address = server.address();
347
+ if (!address || typeof address === "string") throw new Error("Unexpected address");
348
+ const bridge = new LocalMcpBridge({
349
+ mcp: [
350
+ {
351
+ name: "remote",
352
+ url: `http://127.0.0.1:${address.port}/mcp`,
353
+ auth: { type: "bearer", tokenEnv: "LINEAR_TOKEN" },
354
+ },
355
+ ],
356
+ });
357
+ await bridge.startLocalServers();
358
+ await bridge.discoverTools();
359
+ const tools = await bridge.loadTools(["remote/*"]);
360
+ const restricted = tools.find((tool) => tool.name === "remote/restricted");
361
+ expect(restricted).toBeDefined();
362
+ await expect(
363
+ restricted!.handler(
364
+ {},
365
+ {
366
+ agentId: "agent",
367
+ runId: "run",
368
+ step: 1,
369
+ workingDir: process.cwd(),
370
+ parameters: {},
371
+ },
372
+ ),
373
+ ).rejects.toThrow(/permission denied/i);
374
+ await bridge.stopLocalServers();
375
+ await new Promise<void>((resolveClose) => server.close(() => resolveClose()));
81
376
  });
82
377
  });