@dougbots/avenor-mcp 0.16.1 → 0.18.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/dist/index.d.ts CHANGED
@@ -6,5 +6,19 @@ declare function isAllowedOrigin(originHeader: string | null | undefined): boole
6
6
  declare function isAllowedHost(hostHeader: string | null | undefined, port: number): boolean;
7
7
  declare function getMcpAuthToken(): string;
8
8
  declare const server: McpServer;
9
+ interface HttpServerOptions {
10
+ authToken: string;
11
+ /** 0 binds an ephemeral port; the bound port is reported back. */
12
+ port?: number;
13
+ }
14
+ /**
15
+ * Serve the MCP endpoint over HTTP with the transport's web-standard
16
+ * implementation: Hono already hands out a Web `Request`, and the request
17
+ * checks below answer before the transport sees a message.
18
+ */
19
+ declare function startHttpServer(options: HttpServerOptions): Promise<{
20
+ port: number;
21
+ stop: () => Promise<void>;
22
+ }>;
9
23
  //#endregion
10
- export { getMcpAuthToken, isAllowedHost, isAllowedOrigin, parseBearerToken, server };
24
+ export { HttpServerOptions, getMcpAuthToken, isAllowedHost, isAllowedOrigin, parseBearerToken, server, startHttpServer };
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env bun
2
2
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
- import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
4
+ import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
5
5
  import { Hono } from "hono";
6
6
  import { z } from "zod";
7
7
  import { THINKING_LEVELS, ToolError, answerPermissionTool, createHostMessageProvider, eventsTool, getSupervisorClient, isGenerationMismatchError, killTool, messageTool, peersDetails, peersTool, renderPeerList, resultOutput, resultTool, shutdownTool, spawnTool, statusTool, validateSpawnSelection, workflowCancelTool, workflowCompleteTool, workflowControllerStatusTool, workflowEventsTool, workflowGateTool, workflowInspectTool, workflowStatusTool, workflowWaitTool } from "@dougbots/avenor-core";
@@ -64,7 +64,7 @@ var message_input_flat_default = {
64
64
  description: "Flat input schema for the avenor_message tool, compiled from schemas/message.input.json's per-action union so an MCP client publishes one object whose property types survive: an integer stays an integer and an object stays an object. Every action's properties appear here, so a field belonging to another action is not a schema error. Per-action admission (required fields, unknown properties, types and limits) belongs to the router, which answers a mismatch at once with the contract's typed invalid_request and its repair text. Generated by internal/messaging/cmd/genschema; do not edit.",
65
65
  properties: {
66
66
  "action": {
67
- "description": "Action to perform: tell — Deliver a message without creating a reply edge; ask — Deliver a message and wait for one reply; reply — Answer an ask this surface received; resume — Continue a finished run in a new run; withdraw — Retract an ask this caller sent; control_reply — Answer a claude-channel control request; registered only on the claude-channel backend.",
67
+ "description": "Action to perform: tell — Deliver a message without creating a reply edge; ask — Deliver a message and wait for one reply; reply — Answer an ask this surface received; resume — Continue a finished run in a new run; withdraw — Retract an ask this caller sent; control_reply — Retained in the contract for the retired claude-channel backend's reply path; no surface can deliver one.",
68
68
  "enum": [
69
69
  "tell",
70
70
  "ask",
@@ -100,7 +100,7 @@ var message_input_flat_default = {
100
100
  "type": "string"
101
101
  },
102
102
  "target": {
103
- "description": "Recipient: a reserved self alias, a broker/runtime id, a host-surface public run id, or a peer label. Prior run: a reserved self alias, a broker/runtime id, a host-surface public run id, or a peer label. Opaque claude-channel control message id, passed through unchanged.",
103
+ "description": "Recipient: a reserved self alias, a broker/runtime id, a host-surface public run id, or a peer label. Prior run: a reserved self alias, a broker/runtime id, a host-surface public run id, or a peer label. Opaque control message id, passed through unchanged.",
104
104
  "type": "string"
105
105
  },
106
106
  "timeout_ms": {
@@ -204,7 +204,7 @@ server.registerTool("spawn", {
204
204
  roster_file,
205
205
  roster_entry
206
206
  });
207
- return spawnTool({
207
+ return passthroughResult(spawnTool({
208
208
  agent,
209
209
  dir: repo_dir,
210
210
  prompt,
@@ -218,7 +218,7 @@ server.registerTool("spawn", {
218
218
  rosterEntry: roster_entry,
219
219
  serverUrl: server_url,
220
220
  supervisorId: supervisor_id
221
- });
221
+ }));
222
222
  });
223
223
  server.registerTool("status", {
224
224
  description: "Get lifecycle status of one or all runs. Use lifecycle view for compact polling. Without run_id, returns an object with runs (array of status objects) and count.",
@@ -228,11 +228,11 @@ server.registerTool("status", {
228
228
  supervisor_id: z.string().optional().describe("Supervisor ID for multi-supervisor mode")
229
229
  }
230
230
  }, async ({ run_id, view, supervisor_id }) => {
231
- return statusTool({
231
+ return passthroughResult(statusTool({
232
232
  runId: run_id,
233
233
  supervisorId: supervisor_id,
234
234
  view
235
- });
235
+ }));
236
236
  });
237
237
  server.registerTool("result", {
238
238
  description: "Retrieve the complete final output of a run without transcript or event details. Waits by default; set wait=false to retrieve an already-completed run without blocking",
@@ -243,12 +243,12 @@ server.registerTool("result", {
243
243
  supervisor_id: z.string().optional().describe("Supervisor ID for multi-supervisor mode")
244
244
  }
245
245
  }, async ({ run_id, wait, timeout, supervisor_id }) => {
246
- return resultTool({
246
+ return passthroughResult(resultTool({
247
247
  runId: run_id,
248
248
  supervisorId: supervisor_id,
249
249
  wait,
250
250
  timeout
251
- });
251
+ }));
252
252
  });
253
253
  server.registerTool("answer_permission", {
254
254
  description: "Answer a pending permission request",
@@ -260,13 +260,13 @@ server.registerTool("answer_permission", {
260
260
  message: z.string().optional().describe("Optional write-in response text. Required only when the selected option declares requiresMessage: true.")
261
261
  }
262
262
  }, async ({ run_id, option_id, request_id, supervisor_id, message }) => {
263
- return answerPermissionTool({
263
+ return passthroughResult(answerPermissionTool({
264
264
  runId: run_id,
265
265
  optionId: option_id,
266
266
  requestId: request_id,
267
267
  supervisorId: supervisor_id,
268
268
  message
269
- });
269
+ }));
270
270
  });
271
271
  /**
272
272
  * The message and peers tools publish the flat schema compiled from the
@@ -315,6 +315,19 @@ async function withProvider(run) {
315
315
  return run(await messageProvider());
316
316
  }
317
317
  }
318
+ /**
319
+ * The pass-through tools answer with the core tool's result object, which the
320
+ * MCP result schema carries as free-form fields beside `content`. The schema
321
+ * defaults `content` to an empty list, so sending it explicitly is what a
322
+ * client already ends up with, and it keeps the handler's declared result
323
+ * type honest without hiding the result fields.
324
+ */
325
+ async function passthroughResult(result) {
326
+ return {
327
+ ...await result,
328
+ content: []
329
+ };
330
+ }
318
331
  /** A router tool error is a failure result on every surface; anything else is the transport's to report. */
319
332
  function toolFailure(error) {
320
333
  if (!(error instanceof ToolError)) throw error;
@@ -380,12 +393,12 @@ server.registerTool("workflow_cancel", {
380
393
  supervisor_id: z.string().optional().describe("Supervisor ID for multi-supervisor mode")
381
394
  }
382
395
  }, async ({ workflow_id, actor, reason, supervisor_id }) => {
383
- return workflowCancelTool({
396
+ return passthroughResult(workflowCancelTool({
384
397
  workflow_id,
385
398
  actor,
386
399
  reason,
387
400
  supervisorId: supervisor_id
388
- });
401
+ }));
389
402
  });
390
403
  server.registerTool("events", {
391
404
  description: "Get event log for a run",
@@ -396,12 +409,12 @@ server.registerTool("events", {
396
409
  supervisor_id: z.string().optional().describe("Supervisor ID for multi-supervisor mode")
397
410
  }
398
411
  }, async ({ run_id, types, limit, supervisor_id }) => {
399
- return eventsTool({
412
+ return passthroughResult(eventsTool({
400
413
  runId: run_id,
401
414
  types,
402
415
  limit,
403
416
  supervisorId: supervisor_id
404
- });
417
+ }));
405
418
  });
406
419
  server.registerTool("kill", {
407
420
  description: "Terminate a run without shutting down the supervisor. Other runs and the supervisor itself keep running.",
@@ -410,10 +423,10 @@ server.registerTool("kill", {
410
423
  supervisor_id: z.string().optional().describe("Supervisor ID for multi-supervisor mode")
411
424
  }
412
425
  }, async ({ run_id, supervisor_id }) => {
413
- return killTool({
426
+ return passthroughResult(killTool({
414
427
  runId: run_id,
415
428
  supervisorId: supervisor_id
416
- });
429
+ }));
417
430
  });
418
431
  server.registerTool("shutdown", {
419
432
  description: "Shut down the supervisor",
@@ -422,10 +435,10 @@ server.registerTool("shutdown", {
422
435
  force: z.boolean().optional().describe("Force shutdown instead of graceful")
423
436
  }
424
437
  }, async ({ supervisor_id, force }) => {
425
- return shutdownTool({
438
+ return passthroughResult(shutdownTool({
426
439
  supervisorId: supervisor_id,
427
440
  force
428
- });
441
+ }));
429
442
  });
430
443
  server.registerTool("workflow_status", {
431
444
  description: "Get lightweight status for a workflow instance",
@@ -434,10 +447,10 @@ server.registerTool("workflow_status", {
434
447
  supervisor_id: z.string().optional().describe("Supervisor ID for multi-supervisor mode")
435
448
  }
436
449
  }, async ({ workflow_id, supervisor_id }) => {
437
- return workflowStatusTool({
450
+ return passthroughResult(workflowStatusTool({
438
451
  workflowId: workflow_id,
439
452
  supervisorId: supervisor_id
440
- });
453
+ }));
441
454
  });
442
455
  server.registerTool("workflow_wait", {
443
456
  description: "Wait for a workflow to reach a terminal state or until timeout",
@@ -447,11 +460,11 @@ server.registerTool("workflow_wait", {
447
460
  supervisor_id: z.string().optional().describe("Supervisor ID for multi-supervisor mode")
448
461
  }
449
462
  }, async ({ workflow_id, timeout, supervisor_id }) => {
450
- return workflowWaitTool({
463
+ return passthroughResult(workflowWaitTool({
451
464
  workflowId: workflow_id,
452
465
  timeout,
453
466
  supervisorId: supervisor_id
454
- });
467
+ }));
455
468
  });
456
469
  server.registerTool("workflow_inspect", {
457
470
  description: "Return the full instance detail for a workflow",
@@ -460,10 +473,10 @@ server.registerTool("workflow_inspect", {
460
473
  supervisor_id: z.string().optional().describe("Supervisor ID for multi-supervisor mode")
461
474
  }
462
475
  }, async ({ workflow_id, supervisor_id }) => {
463
- return workflowInspectTool({
476
+ return passthroughResult(workflowInspectTool({
464
477
  workflowId: workflow_id,
465
478
  supervisorId: supervisor_id
466
- });
479
+ }));
467
480
  });
468
481
  server.registerTool("workflow_events", {
469
482
  description: "Read log events from a workflow instance's event log",
@@ -474,12 +487,12 @@ server.registerTool("workflow_events", {
474
487
  supervisor_id: z.string().optional().describe("Supervisor ID for multi-supervisor mode")
475
488
  }
476
489
  }, async ({ workflow_id, after_seq, limit, supervisor_id }) => {
477
- return workflowEventsTool({
490
+ return passthroughResult(workflowEventsTool({
478
491
  workflowId: workflow_id,
479
492
  afterSeq: after_seq,
480
493
  limit,
481
494
  supervisorId: supervisor_id
482
- });
495
+ }));
483
496
  });
484
497
  server.registerTool("workflow_complete", {
485
498
  description: "Atomically complete a machine/external handoff activation",
@@ -497,10 +510,10 @@ server.registerTool("workflow_complete", {
497
510
  }
498
511
  }, async (args) => {
499
512
  const { supervisor_id, ...rest } = args;
500
- return workflowCompleteTool({
513
+ return passthroughResult(workflowCompleteTool({
501
514
  ...rest,
502
515
  supervisorId: supervisor_id
503
- });
516
+ }));
504
517
  });
505
518
  server.registerTool("workflow_gate", {
506
519
  description: "Record a gate decision on a parked awaiting_gate activation",
@@ -529,10 +542,10 @@ server.registerTool("workflow_gate", {
529
542
  }
530
543
  }, async (args) => {
531
544
  const { supervisor_id, ...rest } = args;
532
- return workflowGateTool({
545
+ return passthroughResult(workflowGateTool({
533
546
  ...rest,
534
547
  supervisorId: supervisor_id
535
- });
548
+ }));
536
549
  });
537
550
  server.registerTool("workflow_controller_status", {
538
551
  description: "Show workflow controller status. With controller_id, returns that controller's full status; without it, lists all controllers with summary state. Create, enable, and disable are CLI-only (avenor workflow controller ...).",
@@ -541,36 +554,54 @@ server.registerTool("workflow_controller_status", {
541
554
  supervisor_id: z.string().optional().describe("Supervisor ID for multi-supervisor mode")
542
555
  }
543
556
  }, async ({ controller_id, supervisor_id }) => {
544
- return workflowControllerStatusTool({
557
+ return passthroughResult(workflowControllerStatusTool({
545
558
  controllerId: controller_id,
546
559
  supervisorId: supervisor_id
547
- });
560
+ }));
548
561
  });
549
- if (import.meta.main) if (process.env.MCP_TRANSPORT !== "sse") {
550
- const transport = new StdioServerTransport();
551
- await server.connect(transport);
552
- } else {
553
- const port = Number(process.env.MCP_PORT ?? 3748);
554
- const authToken = getMcpAuthToken();
562
+ /**
563
+ * Serve the MCP endpoint over HTTP with the transport's web-standard
564
+ * implementation: Hono already hands out a Web `Request`, and the request
565
+ * checks below answer before the transport sees a message.
566
+ */
567
+ async function startHttpServer(options) {
568
+ const transport = new WebStandardStreamableHTTPServerTransport({ sessionIdGenerator: () => crypto.randomUUID() });
569
+ let boundPort = options.port ?? 0;
555
570
  const app = new Hono();
556
- const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => crypto.randomUUID() });
557
571
  app.all("/mcp", (c) => {
558
572
  const request = c.req.raw;
559
573
  const originHeader = request.headers.get("origin");
560
574
  const hostHeader = request.headers.get("host");
561
575
  const token = parseBearerToken(request.headers.get("authorization"));
562
576
  if (!isAllowedOrigin(originHeader)) return c.text("Forbidden origin", 403);
563
- if (!isAllowedHost(hostHeader, port)) return c.text("Forbidden host", 403);
564
- if (token !== authToken) return c.text("Unauthorized", 401, { "WWW-Authenticate": "Bearer" });
565
- return transport.handleRequest(c.req.raw, c.res, c.req.raw.body);
577
+ if (!isAllowedHost(hostHeader, boundPort)) return c.text("Forbidden host", 403);
578
+ if (token !== options.authToken) return c.text("Unauthorized", 401, { "WWW-Authenticate": "Bearer" });
579
+ return transport.handleRequest(request);
566
580
  });
567
581
  await server.connect(transport);
568
- Bun.serve({
582
+ const http = Bun.serve({
569
583
  fetch: app.fetch,
570
- port,
584
+ port: boundPort,
571
585
  hostname: "127.0.0.1"
572
586
  });
587
+ boundPort = http.port ?? boundPort;
588
+ return {
589
+ port: boundPort,
590
+ stop: async () => {
591
+ await server.close();
592
+ await http.stop(true);
593
+ }
594
+ };
595
+ }
596
+ if (import.meta.main) if (process.env.MCP_TRANSPORT !== "sse") {
597
+ const transport = new StdioServerTransport();
598
+ await server.connect(transport);
599
+ } else {
600
+ const { port } = await startHttpServer({
601
+ authToken: getMcpAuthToken(),
602
+ port: Number(process.env.MCP_PORT ?? 3748)
603
+ });
573
604
  console.error(`avenor MCP server listening on :${port}`);
574
605
  }
575
606
  //#endregion
576
- export { getMcpAuthToken, isAllowedHost, isAllowedOrigin, parseBearerToken, server };
607
+ export { getMcpAuthToken, isAllowedHost, isAllowedOrigin, parseBearerToken, server, startHttpServer };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dougbots/avenor-mcp",
3
- "version": "0.16.1",
3
+ "version": "0.18.0",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -26,11 +26,12 @@
26
26
  },
27
27
  "scripts": {
28
28
  "build": "tsdown",
29
+ "typecheck": "tsc --noEmit",
29
30
  "test": "bun test",
30
31
  "dev": "tsdown --watch"
31
32
  },
32
33
  "dependencies": {
33
- "@dougbots/avenor-core": "^0.16.1",
34
+ "@dougbots/avenor-core": "^0.18.0",
34
35
  "@modelcontextprotocol/sdk": "^1.29.0",
35
36
  "hono": "^4.12.19",
36
37
  "zod": "^4.4.3"