@expanse-ade/mcp 0.9.0 β 0.9.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/dist/index.js +34 -6
- package/dist/index.js.map +1 -1
- package/package.json +13 -12
package/dist/index.js
CHANGED
|
@@ -93,7 +93,7 @@ import { McpServer as McpServer5 } from "@modelcontextprotocol/sdk/server/mcp.js
|
|
|
93
93
|
// package.json
|
|
94
94
|
var package_default = {
|
|
95
95
|
name: "@expanse-ade/mcp",
|
|
96
|
-
version: "0.9.
|
|
96
|
+
version: "0.9.1",
|
|
97
97
|
packageManager: "pnpm@9.15.9",
|
|
98
98
|
description: "MCP server layer for Canvas ADE \xE2\u20AC\u201D lets AI agents in Terminal boards orchestrate the canvas (command board / swarm).",
|
|
99
99
|
type: "module",
|
|
@@ -656,14 +656,21 @@ function registerBarrierTools(server, orchestrator) {
|
|
|
656
656
|
|
|
657
657
|
// src/server/resourceSubscriptions.ts
|
|
658
658
|
import {
|
|
659
|
+
ErrorCode,
|
|
660
|
+
McpError,
|
|
659
661
|
SubscribeRequestSchema,
|
|
660
662
|
UnsubscribeRequestSchema
|
|
661
663
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
664
|
+
var SUBSCRIBABLE = /* @__PURE__ */ new Set([ATTENTION_URI]);
|
|
662
665
|
function installResourceSubscriptions(server) {
|
|
663
666
|
const uris = /* @__PURE__ */ new Set();
|
|
664
667
|
server.server.registerCapabilities({ resources: { subscribe: true } });
|
|
665
668
|
server.server.setRequestHandler(SubscribeRequestSchema, async (req) => {
|
|
666
|
-
|
|
669
|
+
const { uri } = req.params;
|
|
670
|
+
if (!SUBSCRIBABLE.has(uri)) {
|
|
671
|
+
throw new McpError(ErrorCode.InvalidParams, `resource is not subscribable: ${uri}`);
|
|
672
|
+
}
|
|
673
|
+
uris.add(uri);
|
|
667
674
|
return {};
|
|
668
675
|
});
|
|
669
676
|
server.server.setRequestHandler(UnsubscribeRequestSchema, async (req) => {
|
|
@@ -754,6 +761,9 @@ import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
|
754
761
|
function rpcError(code, message) {
|
|
755
762
|
return { jsonrpc: "2.0", error: { code, message }, id: null };
|
|
756
763
|
}
|
|
764
|
+
function ownsSession(owner, caller) {
|
|
765
|
+
return owner.tier === caller.tier && owner.boardId === caller.boardId;
|
|
766
|
+
}
|
|
757
767
|
var SessionManager = class {
|
|
758
768
|
constructor(factory) {
|
|
759
769
|
this.factory = factory;
|
|
@@ -762,6 +772,8 @@ var SessionManager = class {
|
|
|
762
772
|
transports = /* @__PURE__ */ new Map();
|
|
763
773
|
/** Per-session teardown (M5 notifier unsubscribe + in-flight barrier cancel). */
|
|
764
774
|
disposers = /* @__PURE__ */ new Map();
|
|
775
|
+
/** π PKG-N1: the {tier,boardId} of the token that CREATED each session (ownership key). */
|
|
776
|
+
owners = /* @__PURE__ */ new Map();
|
|
765
777
|
/** POST /mcp: reuse an existing session, or open a new one on initialize. */
|
|
766
778
|
async handlePost(req, res, ctx) {
|
|
767
779
|
const sid = req.header(HEADER_SESSION_ID);
|
|
@@ -771,6 +783,10 @@ var SessionManager = class {
|
|
|
771
783
|
res.status(404).json(rpcError(-32001, "Session not found"));
|
|
772
784
|
return;
|
|
773
785
|
}
|
|
786
|
+
if (!this.isOwner(sid, ctx)) {
|
|
787
|
+
res.status(403).json(rpcError(-32003, "Forbidden: session belongs to another token"));
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
774
790
|
await existing.handleRequest(req, res, req.body);
|
|
775
791
|
return;
|
|
776
792
|
}
|
|
@@ -784,6 +800,7 @@ var SessionManager = class {
|
|
|
784
800
|
onsessioninitialized: (id) => {
|
|
785
801
|
this.transports.set(id, transport);
|
|
786
802
|
this.disposers.set(id, dispose);
|
|
803
|
+
this.owners.set(id, ctx);
|
|
787
804
|
}
|
|
788
805
|
});
|
|
789
806
|
transport.onclose = () => {
|
|
@@ -792,13 +809,14 @@ var SessionManager = class {
|
|
|
792
809
|
this.transports.delete(id);
|
|
793
810
|
this.disposers.get(id)?.();
|
|
794
811
|
this.disposers.delete(id);
|
|
812
|
+
this.owners.delete(id);
|
|
795
813
|
}
|
|
796
814
|
};
|
|
797
815
|
await server.connect(transport);
|
|
798
816
|
await transport.handleRequest(req, res, req.body);
|
|
799
817
|
}
|
|
800
|
-
/** GET (SSE) and DELETE /mcp: route to the named session. */
|
|
801
|
-
async handleSession(req, res) {
|
|
818
|
+
/** GET (SSE) and DELETE /mcp: route to the named session, gated on token ownership. */
|
|
819
|
+
async handleSession(req, res, ctx) {
|
|
802
820
|
const sid = req.header(HEADER_SESSION_ID);
|
|
803
821
|
if (sid === void 0) {
|
|
804
822
|
res.status(400).json(rpcError(-32e3, "Missing session ID"));
|
|
@@ -809,8 +827,17 @@ var SessionManager = class {
|
|
|
809
827
|
res.status(404).json(rpcError(-32001, "Session not found"));
|
|
810
828
|
return;
|
|
811
829
|
}
|
|
830
|
+
if (!this.isOwner(sid, ctx)) {
|
|
831
|
+
res.status(403).json(rpcError(-32003, "Forbidden: session belongs to another token"));
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
812
834
|
await transport.handleRequest(req, res);
|
|
813
835
|
}
|
|
836
|
+
/** π True iff the presenting token's {tier,boardId} matches the session creator's. */
|
|
837
|
+
isOwner(sid, ctx) {
|
|
838
|
+
const owner = this.owners.get(sid);
|
|
839
|
+
return owner !== void 0 && ownsSession(owner, ctx);
|
|
840
|
+
}
|
|
814
841
|
/**
|
|
815
842
|
* Tear down every live session (called on app quit). Uses `allSettled` so one
|
|
816
843
|
* transport whose `close()` rejects can't short-circuit the loop and leak the
|
|
@@ -828,6 +855,7 @@ var SessionManager = class {
|
|
|
828
855
|
}
|
|
829
856
|
this.disposers.clear();
|
|
830
857
|
this.transports.clear();
|
|
858
|
+
this.owners.clear();
|
|
831
859
|
}
|
|
832
860
|
}
|
|
833
861
|
};
|
|
@@ -852,10 +880,10 @@ async function createMcpHttpServer(deps) {
|
|
|
852
880
|
sessions.handlePost(req, res, ctxFromAuth(req.auth)).catch(next);
|
|
853
881
|
});
|
|
854
882
|
app.get(MCP_PATH, (req, res, next) => {
|
|
855
|
-
sessions.handleSession(req, res).catch(next);
|
|
883
|
+
sessions.handleSession(req, res, ctxFromAuth(req.auth)).catch(next);
|
|
856
884
|
});
|
|
857
885
|
app.delete(MCP_PATH, (req, res, next) => {
|
|
858
|
-
sessions.handleSession(req, res).catch(next);
|
|
886
|
+
sessions.handleSession(req, res, ctxFromAuth(req.auth)).catch(next);
|
|
859
887
|
});
|
|
860
888
|
const httpServer = createServer(app);
|
|
861
889
|
await new Promise((resolve) => httpServer.listen(0, "127.0.0.1", () => resolve()));
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/server/mcpHttp.ts","../src/constants.ts","../src/auth/verifier.ts","../src/security/origin.ts","../src/security/host.ts","../src/server/factory.ts","../package.json","../src/resources/boards.ts","../src/resources/boardStates.ts","../src/resources/attention.ts","../src/resources/output.ts","../src/resources/result.ts","../src/resources/memory.ts","../src/prompts/index.ts","../src/server/tools/spawnBoard.ts","../src/server/tools/closeBoard.ts","../src/server/tools/configureBoard.ts","../src/server/tools/handoffPrompt.ts","../src/server/tools/promptSchema.ts","../src/server/tools/assignPrompt.ts","../src/server/tools/writeResult.ts","../src/server/tools/interrupt.ts","../src/server/tools/relayPrompt.ts","../src/server/tools/barriers.ts","../src/server/barrierWaiter.ts","../src/server/resourceSubscriptions.ts","../src/server/attentionNotifier.ts","../src/server/transport.ts","../src/auth/tokens.ts","../src/auth/mint.ts","../src/auth/scopes.ts","../src/config/mcpJson.ts","../src/orchestrator/mock.ts"],"sourcesContent":["import { createServer, type Server } from 'node:http'\r\nimport express, { type Express } from 'express'\r\nimport { requireBearerAuth } from '@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js'\r\nimport type { AuthInfo } from '@modelcontextprotocol/sdk/server/auth/types.js'\r\nimport { MCP_PATH } from '../constants'\r\nimport type { BoardId, Tier } from '../types'\r\nimport type { Orchestrator } from '../orchestrator/Orchestrator'\r\nimport { createVerifier } from '../auth/verifier'\r\nimport type { TokenStore } from '../auth/tokens'\r\nimport { originGuard } from '../security/origin'\r\nimport { hostGuard } from '../security/host'\r\nimport { ServerFactory, type SessionCtx } from './factory'\r\nimport { SessionManager } from './transport'\r\n\r\nexport interface McpServerDeps {\r\n orchestrator: Orchestrator\r\n tokens: TokenStore\r\n /**\r\n * Optional single command-orchestrator board id (BUG-021). When set, `relay_prompt` is\r\n * restricted to the token bound to this board, so a second orchestrator-tier token can't\r\n * drive orchestration cables it doesn't own. Omit to keep the prior open-to-any-orchestrator\r\n * behaviour (correct for a single-orchestrator-token deployment).\r\n */\r\n commandBoardId?: BoardId\r\n}\r\n\r\nexport interface RunningMcpServer {\r\n app: Express\r\n httpServer: Server\r\n port: number\r\n setAllowedOrigins(origins: readonly string[]): void\r\n close(): Promise<void>\r\n}\r\n\r\n/** Re-derive the session context from the server-verified bearer token. */\r\nexport function ctxFromAuth(auth: AuthInfo | undefined): SessionCtx {\r\n const extra = (auth?.extra ?? {}) as { tier?: unknown; boardId?: unknown }\r\n const tier: Tier = extra.tier === 'orchestrator' ? 'orchestrator' : 'worker'\r\n const boardId = typeof extra.boardId === 'string' ? extra.boardId : ''\r\n return { tier, scopes: auth?.scopes ?? [], boardId }\r\n}\r\n\r\n/**\r\n * Creates the loopback streamable-HTTP MCP server and starts listening on an\r\n * ephemeral 127.0.0.1 port. Pipeline: originGuard -> requireBearerAuth -> /mcp.\r\n * NO OAuth discovery routes are mounted (resourceMetadataUrl is left unset), so\r\n * MCP clients use the static per-board bearer token without a \"needs auth\" flag.\r\n */\r\nexport async function createMcpHttpServer(deps: McpServerDeps): Promise<RunningMcpServer> {\r\n const app = express()\r\n\r\n // DNS-rebinding defence in two layers: Host (always required) THEN Origin. These\r\n // run BEFORE any body parsing so a non-loopback request is 403'd outright and its\r\n // body is never parsed (no pre-auth parse / memory pressure).\r\n app.use(hostGuard())\r\n let allowedOrigins: readonly string[] = []\r\n app.use(originGuard(() => allowedOrigins))\r\n\r\n const verifier = createVerifier(deps.tokens)\r\n app.use(MCP_PATH, requireBearerAuth({ verifier }))\r\n\r\n // Parse the JSON body only on /mcp, only after Host/Origin/bearer have passed, and\r\n // with an explicit cap (MCP control messages are small; reject oversized bodies).\r\n app.use(MCP_PATH, express.json({ limit: '1mb' }))\r\n\r\n const sessions = new SessionManager(new ServerFactory(deps.orchestrator, deps.commandBoardId))\r\n\r\n app.post(MCP_PATH, (req, res, next) => {\r\n sessions.handlePost(req, res, ctxFromAuth(req.auth)).catch(next)\r\n })\r\n app.get(MCP_PATH, (req, res, next) => {\r\n sessions.handleSession(req, res).catch(next)\r\n })\r\n app.delete(MCP_PATH, (req, res, next) => {\r\n sessions.handleSession(req, res).catch(next)\r\n })\r\n\r\n const httpServer = createServer(app)\r\n await new Promise<void>((resolve) => httpServer.listen(0, '127.0.0.1', () => resolve()))\r\n const address = httpServer.address()\r\n const port = typeof address === 'object' && address !== null ? address.port : 0\r\n allowedOrigins = [`http://127.0.0.1:${port}`, `http://localhost:${port}`]\r\n\r\n return {\r\n app,\r\n httpServer,\r\n port,\r\n setAllowedOrigins(origins) {\r\n allowedOrigins = origins\r\n },\r\n async close() {\r\n await sessions.closeAll()\r\n await new Promise<void>((resolve, reject) => {\r\n httpServer.close((err) => (err ? reject(err) : resolve()))\r\n })\r\n }\r\n }\r\n}\r\n","/** The single MCP endpoint path. */\r\nexport const MCP_PATH = '/mcp'\r\n\r\n/** Session-id header (MCP spec; routing only β authority is the bearer token). */\r\nexport const HEADER_SESSION_ID = 'mcp-session-id'\r\n\r\n/** Phase 0 proof tools. */\r\nexport const TOOL_PING = 'ping'\r\nexport const TOOL_ORCHESTRATOR_PING = 'orchestrator_ping'\r\n\r\n/** Phase 3 lifecycle tools (write path). Orchestrator-tier only. */\r\nexport const TOOL_SPAWN_BOARD = 'spawn_board'\r\nexport const TOOL_CLOSE_BOARD = 'close_board'\r\nexport const TOOL_CONFIGURE_BOARD = 'configure_board'\r\n\r\n/** Phase 4 dispatch tools (write into another board's PTY). Orchestrator-tier only. */\r\nexport const TOOL_HANDOFF_PROMPT = 'handoff_prompt'\r\nexport const TOOL_ASSIGN_PROMPT = 'assign_prompt'\r\nexport const TOOL_INTERRUPT = 'interrupt'\r\nexport const TOOL_RELAY_PROMPT = 'relay_prompt'\r\n\r\n/**\r\n * Phase 4 worker-tier WRITE tool (T4.4) β the FIRST tool a worker may call to mutate\r\n * state: a worker records its OWN board's structured result. Registered for BOTH tiers\r\n * and bound to the caller's `ctx.boardId` (a worker can't forge another board's result).\r\n */\r\nexport const TOOL_WRITE_RESULT = 'write_result'\r\n\r\n/**\r\n * Board types an orchestrator may spawn (T3.1). A closed allowlist β spawn is a\r\n * WRITE, so an unknown/forward type is rejected, never forwarded to the host.\r\n * (Read surfaces like `canvas://boards` keep `type` an open string for forward\r\n * compatibility; the write path is deliberately stricter.)\r\n */\r\nexport const SPAWNABLE_BOARD_TYPES = ['terminal', 'browser', 'planning'] as const\r\n\r\n/**\r\n * Hard cap on the chars returned by ONE `canvas://board/{id}/output` page (T1.4 π).\r\n * The MCP output budget is ~25k; we never emit a larger page even if the host\r\n * over-returns. MUST match the app accessor's page size (`MAX_OUTPUT_PAGE` in\r\n * `src/main/ptyOutput.ts`) so the tail-anchored cursor math lines up across the\r\n * two repos. Unit = UTF-16 code units (JS `String.length`), matching the host ring.\r\n */\r\nexport const MAX_OUTPUT_PAGE = 25_000\r\n\r\n/**\r\n * Phase 5 (M5) BARRIER tools β orchestrator-tier blocking waits over the host status\r\n * stream. READ-ONLY (no PTY write / human confirm / audit β those are for dispatch tools).\r\n */\r\nexport const TOOL_WAIT_FOR_IDLE = 'wait_for_idle'\r\nexport const TOOL_WAIT_FOR_ALL = 'wait_for_all'\r\n\r\n/**\r\n * Default backstop deadline for a barrier wait (30 min) when the tool's `timeoutMs` is\r\n * omitted. Env-tunable via `CANVAS_ADE_BARRIER_TIMEOUT_MS` (finite, > 0, else ignored).\r\n * A per-call `timeoutMs` β€ 0 or non-finite opts out entirely (mirrors the mcpConfirm\r\n * 10-min backstop convention β settle-and-report never throws on expiry).\r\n */\r\nexport const DEFAULT_BARRIER_TIMEOUT_MS = 30 * 60_000\r\n","import { InvalidTokenError } from '@modelcontextprotocol/sdk/server/auth/errors.js'\nimport type { OAuthTokenVerifier } from '@modelcontextprotocol/sdk/server/auth/provider.js'\nimport type { AuthInfo } from '@modelcontextprotocol/sdk/server/auth/types.js'\nimport type { TokenStore } from './tokens'\n\n/**\n * A bearer-token verifier over our in-memory token store. The capability tier\n * is carried in `extra.{tier,boardId}` and re-derived server-side on every\n * request. Throwing InvalidTokenError makes requireBearerAuth return 401 (not\n * 500). This file (with tokens.ts) is the only place SDK auth is imported.\n */\nexport function createVerifier(store: TokenStore): OAuthTokenVerifier {\n return {\n async verifyAccessToken(token: string): Promise<AuthInfo> {\n const row = store.get(token)\n if (!row) throw new InvalidTokenError('Unknown or revoked token')\n return {\n token,\n clientId: row.boardId,\n scopes: row.scopes,\n expiresAt: row.expiresAt,\n extra: { tier: row.tier, boardId: row.boardId }\n }\n }\n }\n}\n","import type { RequestHandler } from 'express'\n\n/**\n * Rejects requests whose Origin is not in the allowlist β a DNS-rebinding guard\n * (so a web page can't drive the local MCP server). Requests with NO Origin\n * header (a CLI MCP client / non-browser) pass: the bearer token is the real\n * authority. `getAllowed` is a getter so the allowlist can be set AFTER\n * listen(0) resolves the real ephemeral port. We own this rather than relying on\n * the transport's @deprecated DNS-rebinding flags.\n */\nexport function originGuard(getAllowed: () => readonly string[]): RequestHandler {\n return (req, res, next) => {\n const origin = req.headers.origin\n if (origin === undefined) {\n next()\n return\n }\n if (getAllowed().includes(origin)) {\n next()\n return\n }\n res.status(403).json({ error: 'forbidden_origin' })\n }\n}\n","import { isIP } from 'node:net'\r\nimport type { RequestHandler } from 'express'\r\n\r\n/** Exact loopback host spellings (the common cases; IP forms are checked below too). */\r\nconst LOOPBACK_HOSTS = new Set(['localhost', '127.0.0.1', '::1', '0:0:0:0:0:0:0:1'])\r\n\r\n/**\r\n * Extract the hostname from a `Host` header value, dropping an optional `:port`.\r\n * Handles the three shapes that reach a loopback listener:\r\n * - `localhost` / `localhost:5173` β `localhost`\r\n * - `127.0.0.1` / `127.0.0.1:443` β `127.0.0.1`\r\n * - `[::1]` / `[::1]:5173` (bracketed v6) β `::1`\r\n * - `::1` (bare v6 literal, no port) β `::1`\r\n */\r\nfunction parseHostname(host: string): string {\r\n const h = host.trim()\r\n if (h.startsWith('[')) {\r\n const end = h.indexOf(']')\r\n return end === -1 ? h.slice(1) : h.slice(1, end)\r\n }\r\n // A bare IPv6 literal (e.g. `::1`) has multiple colons and no `[..]` β it is\r\n // the hostname itself, never `host:port`. Only a single colon means a port.\r\n if ((h.match(/:/g)?.length ?? 0) > 1) return h\r\n const idx = h.indexOf(':')\r\n return idx === -1 ? h : h.slice(0, idx)\r\n}\r\n\r\n/**\r\n * True only for the loopback hosts the server is bound to. Case-insensitive;\r\n * a `:port` suffix is ignored. Anything else (a public name, a suffix attack\r\n * like `localhost.evil.com`, a non-loopback IP, or an empty value) is false.\r\n */\r\nexport function isLoopbackHost(host: string): boolean {\r\n if (host === '') return false\r\n const h = parseHostname(host).toLowerCase()\r\n if (LOOPBACK_HOSTS.has(h)) return true\r\n // The whole 127.0.0.0/8 block is loopback β accept any 127.x.x.x (a browser or\r\n // CLI may use a non-.1 address). `isIP` confirms it is a well-formed IPv4 literal\r\n // so a name like `127.evil.com` can't slip through the prefix check.\r\n if (isIP(h) === 4 && h.startsWith('127.')) return true\r\n return false\r\n}\r\n\r\n/**\r\n * Rejects requests whose `Host` header is not a loopback host β the MANDATORY\r\n * DNS-rebinding mitigation. Binding to 127.0.0.1 and validating `Origin` alone is\r\n * INSUFFICIENT: a browser can be tricked into sending a request whose `Host`\r\n * resolves to the loopback listener (TS-SDK CVE-2025-66414, fixed upstream in\r\n * sdk 1.24.0). For Expanse the exact vector is a Browser board previewing a\r\n * malicious `localhost` page. Unlike `Origin` (absent for non-browser CLI\r\n * clients, so missing-Origin passes), a conformant HTTP/1.1 request ALWAYS\r\n * carries `Host`, so a missing or non-loopback `Host` is rejected with 403.\r\n * Pairs with `originGuard` + the per-board bearer token (defence in depth).\r\n */\r\nexport function hostGuard(): RequestHandler {\r\n return (req, res, next) => {\r\n const host = req.headers.host\r\n if (host !== undefined && isLoopbackHost(host)) {\r\n next()\r\n return\r\n }\r\n res.status(403).json({ error: 'forbidden_host' })\r\n }\r\n}\r\n","import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\r\nimport pkg from '../../package.json' with { type: 'json' }\r\nimport type { BoardId, Scope, Tier } from '../types'\r\nimport type { Orchestrator } from '../orchestrator/Orchestrator'\r\nimport { TOOL_ORCHESTRATOR_PING, TOOL_PING } from '../constants'\r\nimport { registerBoardResources } from '../resources/boards'\r\nimport { registerPrompts } from '../prompts/index'\r\nimport { registerSpawnBoard } from './tools/spawnBoard'\r\nimport { registerCloseBoard } from './tools/closeBoard'\r\nimport { registerConfigureBoard } from './tools/configureBoard'\r\nimport { registerHandoffPrompt } from './tools/handoffPrompt'\r\nimport { registerAssignPrompt } from './tools/assignPrompt'\r\nimport { registerWriteResult } from './tools/writeResult'\r\nimport { registerInterrupt } from './tools/interrupt'\r\nimport { registerRelayPrompt } from './tools/relayPrompt'\r\nimport { registerBarrierTools } from './tools/barriers'\r\nimport { installResourceSubscriptions } from './resourceSubscriptions'\r\nimport { createAttentionNotifier } from './attentionNotifier'\r\n\r\n/** Per-session context, derived from the validated bearer token. */\r\nexport interface SessionCtx {\r\n tier: Tier\r\n scopes: Scope[]\r\n boardId: BoardId\r\n}\r\n\r\n// Version is sourced from package.json so the handshake never drifts from the\r\n// published version (clients log/compat-check serverInfo.version).\r\nconst SERVER_INFO = { name: 'canvas-ade-mcp', version: pkg.version }\r\n\r\n/**\r\n * Builds a fresh McpServer per session, registering ONLY the tools the session's\r\n * tier is allowed. The capability split is structural (by registration) β a\r\n * worker's tools/list never even contains an orchestrator tool. Never\r\n * register-all-then-gate-in-handler.\r\n */\r\nexport class ServerFactory {\r\n /**\r\n * @param commandBoardId Optional single command-orchestrator board id (BUG-021). When set,\r\n * `relay_prompt` is restricted to that token-bound identity so a second orchestrator-tier\r\n * token can't drive cables it doesn't own. Left undefined β relay open to any orchestrator\r\n * (the prior single-token behaviour).\r\n */\r\n constructor(\r\n private readonly orchestrator: Orchestrator,\r\n private readonly commandBoardId?: BoardId\r\n ) {}\r\n\r\n getServer(ctx: SessionCtx): { server: McpServer; dispose: () => void } {\r\n const server = new McpServer(SERVER_INFO)\r\n const disposers: Array<() => void> = []\r\n\r\n // ping β both tiers.\r\n server.registerTool(TOOL_PING, { description: 'Health check. Returns \"pong\".' }, async () => ({\r\n content: [{ type: 'text', text: 'pong' }]\r\n }))\r\n\r\n // Orchestrator-only tools β registered ONLY for the orchestrator tier, so a\r\n // worker's tools/list never even contains them (the capability split is\r\n // structural, never a per-handler check).\r\n if (ctx.tier === 'orchestrator') {\r\n server.registerTool(\r\n TOOL_ORCHESTRATOR_PING,\r\n { description: 'Orchestrator-only health check. Returns \"orchestrator-pong\".' },\r\n async () => ({ content: [{ type: 'text', text: 'orchestrator-pong' }] })\r\n )\r\n // Lifecycle write tools (Phase 3+).\r\n registerSpawnBoard(server, this.orchestrator)\r\n registerCloseBoard(server, this.orchestrator)\r\n registerConfigureBoard(server, this.orchestrator)\r\n // Dispatch write tools (Phase 4) β write into another board's PTY.\r\n registerHandoffPrompt(server, this.orchestrator)\r\n registerAssignPrompt(server, this.orchestrator)\r\n registerInterrupt(server, this.orchestrator)\r\n // relay_prompt is bound to the designated command orchestrator when one is set (BUG-021).\r\n registerRelayPrompt(server, this.orchestrator, ctx, this.commandBoardId)\r\n // M5 barriers β orchestrator-tier; dispose cancels any in-flight wait on session close.\r\n disposers.push(registerBarrierTools(server, this.orchestrator))\r\n }\r\n\r\n // write_result (T4.4) β the FIRST worker-tier WRITE tool. Registered for BOTH tiers\r\n // (OUTSIDE the orchestrator-only block) and bound to ctx.boardId so a worker can only\r\n // record its OWN board's result, never forge another's.\r\n registerWriteResult(server, this.orchestrator, ctx)\r\n\r\n registerBoardResources(server, this.orchestrator)\r\n registerPrompts(server)\r\n\r\n // M5 attention push (both tiers β observation is safe). Subscribe wiring MUST precede\r\n // connect (registerCapabilities is connect-gated); getServer always runs before connect.\r\n const subs = installResourceSubscriptions(server)\r\n const notifier = createAttentionNotifier({\r\n server,\r\n orchestrator: this.orchestrator,\r\n isSubscribed: subs.isSubscribed\r\n })\r\n disposers.push(() => notifier.dispose())\r\n\r\n return {\r\n server,\r\n dispose: () => {\r\n for (const d of disposers) d()\r\n }\r\n }\r\n }\r\n}\r\n","{\r\n \"name\": \"@expanse-ade/mcp\",\r\n \"version\": \"0.9.0\",\r\n \"packageManager\": \"pnpm@9.15.9\",\r\n \"description\": \"MCP server layer for Canvas ADE \\u00e2\\u20ac\\u201d lets AI agents in Terminal boards orchestrate the canvas (command board / swarm).\",\r\n \"type\": \"module\",\r\n \"repository\": {\r\n \"type\": \"git\",\r\n \"url\": \"git+https://github.com/ch923dev/canvas-ade-mcp.git\"\r\n },\r\n \"publishConfig\": {\r\n \"access\": \"public\"\r\n },\r\n \"main\": \"./dist/index.js\",\r\n \"types\": \"./dist/index.d.ts\",\r\n \"exports\": {\r\n \".\": {\r\n \"types\": \"./dist/index.d.ts\",\r\n \"import\": \"./dist/index.js\"\r\n }\r\n },\r\n \"files\": [\r\n \"dist\"\r\n ],\r\n \"engines\": {\r\n \"node\": \">=20\"\r\n },\r\n \"scripts\": {\r\n \"build\": \"tsup\",\r\n \"typecheck\": \"tsc --noEmit\",\r\n \"test\": \"vitest run --project contract\",\r\n \"test:live\": \"vitest run --project live\",\r\n \"lint\": \"eslint .\",\r\n \"format\": \"prettier --write .\",\r\n \"format:check\": \"prettier --check .\"\r\n },\r\n \"license\": \"MIT\",\r\n \"dependencies\": {\r\n \"@modelcontextprotocol/sdk\": \"^1.29.0\",\r\n \"express\": \"^5.2.1\",\r\n \"zod\": \"^4.4.3\"\r\n },\r\n \"devDependencies\": {\r\n \"@eslint/js\": \"^10.0.1\",\r\n \"@modelcontextprotocol/inspector\": \"^0.21.2\",\r\n \"@types/express\": \"^5.0.6\",\r\n \"@types/node\": \"^25.9.1\",\r\n \"eslint\": \"^10.4.1\",\r\n \"eslint-config-prettier\": \"^10.1.8\",\r\n \"prettier\": \"^3.8.3\",\r\n \"tsup\": \"^8.5.1\",\r\n \"typescript\": \"^6.0.3\",\r\n \"typescript-eslint\": \"^8.60.0\",\r\n \"vitest\": \"^4.1.7\"\r\n }\r\n}\r\n","import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js'\r\nimport type { Orchestrator } from '../orchestrator/Orchestrator'\r\nimport { registerBoardStatesResource } from './boardStates'\r\nimport { registerAttentionResource } from './attention'\r\nimport { registerBoardOutputResource } from './output'\r\nimport { registerBoardResultResource } from './result'\r\nimport { registerMemoryResources } from './memory'\r\n\r\n/**\r\n * Registers the read-only board observation resources. Available to BOTH tiers β\r\n * observation is safe (no write, no cross-agent influence):\r\n *\r\n * - `canvas://boards` β the full board list (id/type/title + coarse status bucket).\r\n * - `canvas://board/{id}/status` β one board's coarse status bucket (T1.1). The\r\n * bucket is derived host-side from the live runtime (terminal PTY + browser load\r\n * state) and is the same value an agent sees in `canvas://boards` and a human sees\r\n * on the board's on-canvas status pill β one source of truth.\r\n */\r\nexport function registerBoardResources(server: McpServer, orchestrator: Orchestrator): void {\r\n server.registerResource(\r\n 'boards',\r\n 'canvas://boards',\r\n { description: 'List of boards currently on the canvas.', mimeType: 'application/json' },\r\n async (uri) => ({\r\n contents: [{ uri: uri.href, text: JSON.stringify(await orchestrator.listBoards()) }]\r\n })\r\n )\r\n\r\n server.registerResource(\r\n 'board-status',\r\n new ResourceTemplate('canvas://board/{id}/status', { list: undefined }),\r\n {\r\n description:\r\n \"A single board's coarse status bucket (idle/running/awaiting-review/blocked/failed/static).\",\r\n mimeType: 'application/json'\r\n },\r\n async (uri, variables) => {\r\n const id = Array.isArray(variables.id) ? variables.id[0] : variables.id\r\n if (!id) throw new Error('canvas://board/{id}/status: missing board id')\r\n const status = await orchestrator.boardStatus(id)\r\n return { contents: [{ uri: uri.href, text: JSON.stringify({ id, status }) }] }\r\n }\r\n )\r\n\r\n registerBoardStatesResource(server, orchestrator)\r\n registerAttentionResource(server, orchestrator)\r\n registerBoardOutputResource(server, orchestrator)\r\n registerBoardResultResource(server, orchestrator)\r\n registerMemoryResources(server, orchestrator)\r\n}\r\n","import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\r\nimport type { BoardSummary, Orchestrator } from '../orchestrator/Orchestrator'\r\nimport type { BoardId } from '../types'\r\n\r\n/**\r\n * Roll up a board list into `{ statusBucket: BoardId[] }`, preserving board order\r\n * within each bucket and omitting buckets with no boards. The compact \"who is in\r\n * which state\" view β counts are `array.length`; full per-board detail stays in\r\n * `canvas://boards`.\r\n */\r\nexport function groupBoardsByStatus(boards: BoardSummary[]): Record<string, BoardId[]> {\r\n const grouped: Record<string, BoardId[]> = {}\r\n for (const b of boards) {\r\n ;(grouped[b.status] ??= []).push(b.id)\r\n }\r\n return grouped\r\n}\r\n\r\n/**\r\n * Registers the read-only `canvas://board-states` roll-up resource (both tiers β\r\n * observation is safe). Lets an orchestrator see the swarm's shape at a glance\r\n * (how many boards are running / blocked / failed) without paging the full list.\r\n */\r\nexport function registerBoardStatesResource(server: McpServer, orchestrator: Orchestrator): void {\r\n server.registerResource(\r\n 'board-states',\r\n 'canvas://board-states',\r\n {\r\n description: 'Boards on the canvas grouped by status bucket ({ bucket: boardId[] }).',\r\n mimeType: 'application/json'\r\n },\r\n async (uri) => ({\r\n contents: [\r\n {\r\n uri: uri.href,\r\n text: JSON.stringify(groupBoardsByStatus(await orchestrator.listBoards()))\r\n }\r\n ]\r\n })\r\n )\r\n}\r\n","import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\r\nimport type { BoardSummary, Orchestrator } from '../orchestrator/Orchestrator'\r\n\r\n/** The canonical URI of the read-only attention resource (M5 notifier pushes updates here). */\r\nexport const ATTENTION_URI = 'canvas://attention'\r\n\r\n/**\r\n * Status buckets that mean \"a human's attention is needed\": a worker waiting on a\r\n * review/decision, blocked on a permission prompt, or failed. (`running`/`idle`/\r\n * `static` do not need a human.) Buckets are emitted host-side: `failed` already\r\n * flows today (a browser that fails to load); `blocked`/`awaiting-review` start\r\n * flowing when their detection lands (M8 permission detection + the terminal\r\n * awaiting-input wire) β this resource already surfaces them once they do.\r\n */\r\nexport const ATTENTION_BUCKETS: ReadonlySet<string> = new Set([\r\n 'blocked',\r\n 'awaiting-review',\r\n 'failed'\r\n])\r\n\r\n/** The subset of boards currently in an attention bucket (order preserved). */\r\nexport function selectAttention(boards: BoardSummary[]): BoardSummary[] {\r\n return boards.filter((b) => ATTENTION_BUCKETS.has(b.status))\r\n}\r\n\r\n/**\r\n * Registers the read-only `canvas://attention` resource (both tiers β observation is\r\n * safe): the boards needing a human, with full detail so the agent knows which board\r\n * and why. The on-canvas \"needs-you\" queue (M5 / SB-1) is the human-visible twin.\r\n */\r\nexport function registerAttentionResource(server: McpServer, orchestrator: Orchestrator): void {\r\n server.registerResource(\r\n 'attention',\r\n ATTENTION_URI,\r\n {\r\n description: 'Boards needing a human (blocked / awaiting-review / failed).',\r\n mimeType: 'application/json'\r\n },\r\n async (uri) => ({\r\n contents: [\r\n { uri: uri.href, text: JSON.stringify(selectAttention(await orchestrator.listBoards())) }\r\n ]\r\n })\r\n )\r\n}\r\n","import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js'\r\nimport type { Variables } from '@modelcontextprotocol/sdk/shared/uriTemplate.js'\r\nimport type { BoardOutput, Orchestrator } from '../orchestrator/Orchestrator'\r\nimport { MAX_OUTPUT_PAGE } from '../constants'\r\n\r\n/** First value of a templated URI variable (templates yield string | string[]). */\r\nfunction first(v: string | string[] | undefined): string | undefined {\r\n return Array.isArray(v) ? v[0] : v\r\n}\r\n\r\n/**\r\n * Read one capped page of a board's scrollback and serialize it. Shared by both the\r\n * tail (no cursor) and `?cursor` templates. **π Hard-caps the page at\r\n * `MAX_OUTPUT_PAGE` here too** β defense in depth, so a misbehaving host can never\r\n * make this resource dump more than one MCP page (the host already caps; this is the\r\n * belt-and-suspenders the security checklist requires). `nextCursor`/`droppedOlder`\r\n * pass through unchanged so the agent always knows whether to keep paging.\r\n */\r\nasync function readPage(\r\n orchestrator: Orchestrator,\r\n uriHref: string,\r\n variables: Variables\r\n): Promise<{ contents: Array<{ uri: string; text: string }> }> {\r\n const id = first(variables.id)\r\n if (!id) throw new Error('canvas://board/{id}/output: missing board id')\r\n // A cursor, when supplied, must be a non-negative integer (chars-from-end). A\r\n // malformed value is NOT silently coerced to \"newest tail\" β that would loop an\r\n // agent forever (it pages with garbage, keeps getting page 1). Fail loud instead.\r\n const rawCursor = first(variables.cursor)\r\n let opts: { cursor: number } | undefined\r\n if (rawCursor !== undefined) {\r\n const cursor = Number(rawCursor)\r\n if (!Number.isInteger(cursor) || cursor < 0) {\r\n throw new Error(`canvas://board/${id}/output: invalid cursor \"${rawCursor}\"`)\r\n }\r\n opts = { cursor }\r\n }\r\n\r\n const out = await orchestrator.boardOutput(id, opts)\r\n // π never emit more than one page, whatever the host returned. Keep the NEWEST\r\n // MAX_OUTPUT_PAGE chars (the contract is tail-anchored β `text` is the newest\r\n // slice), NOT the oldest. When we have to drop, the dropped chars are OLDER and\r\n // remain unconsumed, so the cursor MUST advance by exactly what we kept (= the\r\n // dropped older content is still reachable on the next, older page).\r\n let page: BoardOutput = out\r\n if (out.text.length > MAX_OUTPUT_PAGE) {\r\n const text = out.text.slice(-MAX_OUTPUT_PAGE)\r\n page = {\r\n ...out,\r\n text,\r\n returned: text.length,\r\n nextCursor: (opts?.cursor ?? 0) + text.length\r\n }\r\n }\r\n return { contents: [{ uri: uriHref, text: JSON.stringify(page) }] }\r\n}\r\n\r\n/**\r\n * Registers the read-only, capped, paginated `canvas://board/{id}/output` resource\r\n * (T1.4 π β both tiers; observation is safe). Two disjoint templates back ONE\r\n * logical resource because the SDK's UriTemplate compiles `{?cursor}` into a\r\n * MANDATORY query group β a URI with no `?cursor` won't match it. So:\r\n * - `canvas://board/{id}/output` β newest tail page (no cursor)\r\n * - `canvas://board/{id}/output{?cursor}` β an older page at the given cursor\r\n * Their regexes are mutually exclusive (`β¦/output$` vs `β¦/output\\?cursor=β¦$`), so\r\n * each concrete read routes to exactly one. Both delegate to `readPage`.\r\n */\r\nexport function registerBoardOutputResource(server: McpServer, orchestrator: Orchestrator): void {\r\n const meta = {\r\n description:\r\n \"A capped, paginated, ANSI-stripped page of a board's recent output (tail-anchored; pass nextCursor as ?cursor for older).\",\r\n mimeType: 'application/json'\r\n }\r\n server.registerResource(\r\n 'board-output',\r\n new ResourceTemplate('canvas://board/{id}/output', { list: undefined }),\r\n meta,\r\n (uri, variables) => readPage(orchestrator, uri.href, variables)\r\n )\r\n server.registerResource(\r\n 'board-output-paged',\r\n new ResourceTemplate('canvas://board/{id}/output{?cursor}', { list: undefined }),\r\n meta,\r\n (uri, variables) => readPage(orchestrator, uri.href, variables)\r\n )\r\n}\r\n","import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js'\r\nimport type { Orchestrator } from '../orchestrator/Orchestrator'\r\n\r\n/**\r\n * Registers the read-only `canvas://board/{id}/result` resource (T1.5 β both tiers;\r\n * observation is safe). Returns a board's STRUCTURED last result (a verdict + summary\r\n * + references, not raw logs β raw scrollback is `canvas://board/{id}/output`). v1 is\r\n * an observational shell: every board reads `{ present: false }` until M4's\r\n * `write_result` lets a worker record one; the resource shape is stable across that.\r\n */\r\nexport function registerBoardResultResource(server: McpServer, orchestrator: Orchestrator): void {\r\n server.registerResource(\r\n 'board-result',\r\n new ResourceTemplate('canvas://board/{id}/result', { list: undefined }),\r\n {\r\n description:\r\n \"A board's structured last result (verdict + summary + references, not raw logs). Empty shell until a result is recorded.\",\r\n mimeType: 'application/json'\r\n },\r\n async (uri, variables) => {\r\n const id = Array.isArray(variables.id) ? variables.id[0] : variables.id\r\n if (!id) throw new Error('canvas://board/{id}/result: missing board id')\r\n const result = await orchestrator.boardResult(id)\r\n return { contents: [{ uri: uri.href, text: JSON.stringify(result) }] }\r\n }\r\n )\r\n}\r\n","import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js'\r\nimport type { Orchestrator } from '../orchestrator/Orchestrator'\r\n\r\n/**\r\n * Registers the read-only project-memory resources (T1.7 β both tiers; π PASSIVE\r\n * context only, these expose no action):\r\n * - `canvas://memory` β the project memory index.\r\n * - `canvas://board/{id}/summary` β a board's memory summary.\r\n * Backed by the sibling Brain/Memory engine's `.canvas/memory/`. When that subsystem\r\n * is absent (it ships on a separate track), each resource returns the empty shell\r\n * `{ present: false, text: '' }` β graceful, never an error.\r\n */\r\nexport function registerMemoryResources(server: McpServer, orchestrator: Orchestrator): void {\r\n server.registerResource(\r\n 'memory',\r\n 'canvas://memory',\r\n {\r\n description: 'Project memory index (passive context). Empty shell when no memory exists.',\r\n mimeType: 'application/json'\r\n },\r\n async (uri) => ({\r\n contents: [{ uri: uri.href, text: JSON.stringify(await orchestrator.projectMemory()) }]\r\n })\r\n )\r\n\r\n server.registerResource(\r\n 'board-summary',\r\n new ResourceTemplate('canvas://board/{id}/summary', { list: undefined }),\r\n {\r\n description: \"A board's memory summary (passive context). Empty shell when none exists.\",\r\n mimeType: 'application/json'\r\n },\r\n async (uri, variables) => {\r\n const id = Array.isArray(variables.id) ? variables.id[0] : variables.id\r\n if (!id) throw new Error('canvas://board/{id}/summary: missing board id')\r\n return { contents: [{ uri: uri.href, text: JSON.stringify(await orchestrator.boardSummary(id)) }] }\r\n }\r\n )\r\n}\r\n","import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\n\n/** Phase 0 placeholder β prompt templates (review_worktree, etc.) land later. */\nexport function registerPrompts(_server: McpServer): void {\n // no prompts yet\n}\n","import { z } from 'zod'\r\nimport type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\r\nimport type { Orchestrator } from '../../orchestrator/Orchestrator'\r\nimport { SPAWNABLE_BOARD_TYPES, TOOL_SPAWN_BOARD } from '../../constants'\r\n\r\n/**\r\n * Register the `spawn_board` lifecycle tool (T3.1) β the first WRITE tool. The\r\n * CALLER (ServerFactory) gates this to the orchestrator tier by only invoking\r\n * `registerSpawnBoard` for that tier; a worker's `tools/list` never contains it.\r\n * The closed `type` enum is the input-validation guard: an unknown type is\r\n * rejected by the SDK's schema check BEFORE the orchestrator is ever called, so a\r\n * bad type can never reach the host's board factory.\r\n *\r\n * The orchestrator (Canvas ADE MAIN) mints the board id and drives the canvas via\r\n * the command channel; the tool just surfaces that server-issued id to the agent.\r\n */\r\nexport function registerSpawnBoard(server: McpServer, orchestrator: Orchestrator): void {\r\n server.registerTool(\r\n TOOL_SPAWN_BOARD,\r\n {\r\n description:\r\n 'Create a new board on the canvas. type is one of terminal | browser | planning. ' +\r\n 'Optional prompt (terminal launch command / agent task) and cwd (working directory). ' +\r\n 'Returns the new board id. Subject to a concurrency cap.',\r\n inputSchema: {\r\n type: z.enum(SPAWNABLE_BOARD_TYPES),\r\n prompt: z.string().optional(),\r\n cwd: z.string().optional()\r\n }\r\n },\r\n async (args) => {\r\n const { id } = await orchestrator.spawnBoard({\r\n type: args.type,\r\n prompt: args.prompt,\r\n cwd: args.cwd\r\n })\r\n return { content: [{ type: 'text', text: id }] }\r\n }\r\n )\r\n}\r\n","import { z } from 'zod'\r\nimport type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\r\nimport type { Orchestrator } from '../../orchestrator/Orchestrator'\r\nimport { TOOL_CLOSE_BOARD } from '../../constants'\r\n\r\n/**\r\n * Register the `close_board` lifecycle tool (T3.2) β a WRITE tool. Gated to the\r\n * orchestrator tier by the CALLER (ServerFactory). The host drains the board's PTY\r\n * gracefully before removing it. `id` is required + non-empty (an empty id is\r\n * rejected by the schema before the orchestrator is called).\r\n */\r\nexport function registerCloseBoard(server: McpServer, orchestrator: Orchestrator): void {\r\n server.registerTool(\r\n TOOL_CLOSE_BOARD,\r\n {\r\n description:\r\n 'Close a board by id (graceful PTY drain, then removed from the canvas). ' +\r\n 'Idempotent β closing an already-gone board succeeds.',\r\n inputSchema: { id: z.string().min(1) }\r\n },\r\n async (args) => {\r\n await orchestrator.closeBoard(args.id)\r\n return { content: [{ type: 'text', text: `closed ${args.id}` }] }\r\n }\r\n )\r\n}\r\n","import { z } from 'zod'\r\nimport type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\r\nimport type { BoardConfig, Orchestrator } from '../../orchestrator/Orchestrator'\r\nimport { TOOL_CONFIGURE_BOARD } from '../../constants'\r\n\r\n/**\r\n * Register the `configure_board` lifecycle tool (T3.3) β a WRITE tool, orchestrator\r\n * tier only (gated by the CALLER). Changes a board's durable per-type config\r\n * (shell / launchCommand / cwd). `id` is required; at least one config field must be\r\n * present (an empty change is rejected before the orchestrator is called). The host\r\n * additionally filters the patch to the board type's patchable keys.\r\n */\r\nexport function registerConfigureBoard(server: McpServer, orchestrator: Orchestrator): void {\r\n server.registerTool(\r\n TOOL_CONFIGURE_BOARD,\r\n {\r\n description:\r\n 'Change a board config by id. At least one of shell | launchCommand | cwd is required. ' +\r\n 'Applies to the board type that owns the key (shell/launchCommand/cwd are terminal config).',\r\n inputSchema: {\r\n id: z.string().min(1),\r\n shell: z.string().optional(),\r\n launchCommand: z.string().optional(),\r\n cwd: z.string().optional()\r\n }\r\n },\r\n async (args) => {\r\n const config: BoardConfig = {}\r\n if (args.shell !== undefined) config.shell = args.shell\r\n if (args.launchCommand !== undefined) config.launchCommand = args.launchCommand\r\n if (args.cwd !== undefined) config.cwd = args.cwd\r\n if (Object.keys(config).length === 0) {\r\n return {\r\n isError: true,\r\n content: [\r\n { type: 'text', text: 'configure_board: at least one of shell/launchCommand/cwd required' }\r\n ]\r\n }\r\n }\r\n await orchestrator.configureBoard(args.id, config)\r\n return { content: [{ type: 'text', text: `configured ${args.id}` }] }\r\n }\r\n )\r\n}\r\n","import { z } from 'zod'\r\nimport type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\r\nimport type { Orchestrator } from '../../orchestrator/Orchestrator'\r\nimport { TOOL_HANDOFF_PROMPT } from '../../constants'\r\nimport { dispatchPromptSchema } from './promptSchema'\r\n\r\n/**\r\n * Register the `handoff_prompt` DISPATCH tool (T4.3) β the first tool that writes into\r\n * another agent's PTY. The CALLER (ServerFactory) gates it to the orchestrator tier by\r\n * only invoking `registerHandoffPrompt` for that tier; a worker's `tools/list` never\r\n * contains it (the capability split is structural, never a per-handler check).\r\n *\r\n * π The host (Canvas ADE MAIN) owns the real safety: it resolves the OPAQUE board id\r\n * (never a label), rejects any non-terminal target before any write, mints a single-use\r\n * nonce, BLOCKS on a mandatory human confirm, and audits the action. This tool is the\r\n * thin transport: validate non-empty inputs, forward to the orchestrator, and surface\r\n * the returned {@link BoardResult} as text. Blocking β it resolves only after the target\r\n * terminal goes idle.\r\n */\r\nexport function registerHandoffPrompt(server: McpServer, orchestrator: Orchestrator): void {\r\n server.registerTool(\r\n TOOL_HANDOFF_PROMPT,\r\n {\r\n description:\r\n 'Hand off a prompt to a target terminal board by id: write it into that board ' +\r\n 'and block until the board goes idle, then return its structured last result. ' +\r\n 'Terminal targets only; requires human confirmation. boardId + prompt are required.',\r\n inputSchema: {\r\n boardId: z.string().min(1),\r\n prompt: dispatchPromptSchema\r\n }\r\n },\r\n async (args) => {\r\n const result = await orchestrator.handoffPrompt(args.boardId, args.prompt)\r\n return { content: [{ type: 'text', text: JSON.stringify(result) }] }\r\n }\r\n )\r\n}\r\n","import { z } from 'zod'\r\n\r\n/**\r\n * True if `s` contains any character that must never reach a target board's PTY\r\n * through a dispatch payload: any C0 control char EXCEPT TAB (0x09), plus DEL\r\n * (0x7f). This blocks CR (0x0d) and LF (0x0a) β which would let one human-approved\r\n * \"line\" smuggle extra commands β and Ctrl-C (0x03), ESC (0x1b), and other\r\n * terminal control bytes.\r\n */\r\nfunction hasControlChar(s: string): boolean {\r\n for (let i = 0; i < s.length; i++) {\r\n const c = s.charCodeAt(i)\r\n if (c === 0x09) continue // TAB is allowed\r\n if (c < 0x20 || c === 0x7f) return true\r\n }\r\n return false\r\n}\r\n\r\n/**\r\n * A non-empty dispatch prompt with no embedded control characters. The host\r\n * (Canvas ADE MAIN) is the real gate β it strips/rejects these before any PTY\r\n * write β but this transport-layer refinement is defence in depth: a hostile or\r\n * malformed payload is rejected at the tool schema, never forwarded to the\r\n * orchestrator. Shared by assign_prompt / handoff_prompt / relay_prompt.\r\n */\r\nexport const dispatchPromptSchema = z\r\n .string()\r\n .min(1)\r\n .refine((s) => !hasControlChar(s), {\r\n message: 'prompt must not contain control characters (CR/LF/C0/DEL)'\r\n })\r\n","import { z } from 'zod'\r\nimport type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\r\nimport type { Orchestrator } from '../../orchestrator/Orchestrator'\r\nimport { TOOL_ASSIGN_PROMPT } from '../../constants'\r\nimport { dispatchPromptSchema } from './promptSchema'\r\n\r\n/**\r\n * Register the `assign_prompt` DISPATCH tool (T4.4) β the FIRE-AND-FORGET sibling of\r\n * `handoff_prompt`. The CALLER (ServerFactory) gates it to the orchestrator tier by only\r\n * invoking `registerAssignPrompt` for that tier; a worker's `tools/list` never contains\r\n * it (the capability split is structural, never a per-handler check).\r\n *\r\n * π The host (Canvas ADE MAIN) owns the real safety, identical to handoff_prompt: it\r\n * resolves the OPAQUE board id (never a label), rejects any non-terminal target before\r\n * any write, mints a single-use nonce, BLOCKS on a mandatory human confirm, and audits\r\n * the action. The ONLY difference from handoff is that this returns the moment the write\r\n * lands β it does NOT block on await-idle or return a structured result. This tool is the\r\n * thin transport: validate non-empty inputs, forward to {@link Orchestrator.dispatchPrompt},\r\n * and surface a short ack.\r\n */\r\nexport function registerAssignPrompt(server: McpServer, orchestrator: Orchestrator): void {\r\n server.registerTool(\r\n TOOL_ASSIGN_PROMPT,\r\n {\r\n description:\r\n 'Assign a prompt to a target terminal board by id: write it into that board ' +\r\n 'and return immediately (fire-and-forget β no waiting for the board to finish). ' +\r\n 'Terminal targets only; requires human confirmation. boardId + prompt are required.',\r\n inputSchema: {\r\n boardId: z.string().min(1),\r\n prompt: dispatchPromptSchema\r\n }\r\n },\r\n async (args) => {\r\n await orchestrator.dispatchPrompt(args.boardId, args.prompt)\r\n return { content: [{ type: 'text', text: `assigned prompt to ${args.boardId}` }] }\r\n }\r\n )\r\n}\r\n","import { z } from 'zod'\r\nimport type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\r\nimport type { Orchestrator } from '../../orchestrator/Orchestrator'\r\nimport type { SessionCtx } from '../factory'\r\nimport { TOOL_WRITE_RESULT } from '../../constants'\r\n\r\n/**\r\n * Register the `write_result` tool (T4.4) β the FIRST worker-tier WRITE tool. A worker\r\n * records its OWN board's structured result (verdict + summary + references), which feeds\r\n * the `canvas://board/{id}/result` resource (T1.5). Registered for BOTH tiers (the caller\r\n * β ServerFactory β invokes this OUTSIDE the orchestrator-only block).\r\n *\r\n * π The target board is BOUND to the caller's `ctx.boardId` (derived from the verified\r\n * token), and there is NO client-supplied boardId input β so a worker can only write its\r\n * OWN result and can never forge another board's. Unlike the dispatch tools this performs\r\n * no PTY write and needs no human confirm: the agent is reporting its own outcome, not\r\n * dispatching into another shell.\r\n */\r\nexport function registerWriteResult(\r\n server: McpServer,\r\n orchestrator: Orchestrator,\r\n ctx: SessionCtx\r\n): void {\r\n server.registerTool(\r\n TOOL_WRITE_RESULT,\r\n {\r\n description:\r\n \"Record THIS board's structured last result (status / summary / references). \" +\r\n 'All fields optional. Writes only the calling board (no target id is accepted).',\r\n inputSchema: {\r\n status: z.string().optional(),\r\n summary: z.string().optional(),\r\n refs: z.array(z.string()).optional()\r\n }\r\n },\r\n async (args) => {\r\n // π ctx.boardId is derived from the token (no client input). If the token\r\n // carried no boardId it falls back to '' β writing to board '' would silently\r\n // no-op or hit a sentinel while the agent believes its result was recorded.\r\n // Refuse loudly instead of writing to an unbound board.\r\n if (!ctx.boardId) {\r\n return {\r\n isError: true,\r\n content: [{ type: 'text', text: 'write_result: no board bound to this session' }]\r\n }\r\n }\r\n await orchestrator.writeResult(ctx.boardId, {\r\n status: args.status,\r\n summary: args.summary,\r\n refs: args.refs\r\n })\r\n return { content: [{ type: 'text', text: 'result recorded' }] }\r\n }\r\n )\r\n}\r\n","import { z } from 'zod'\r\nimport type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\r\nimport type { Orchestrator } from '../../orchestrator/Orchestrator'\r\nimport { TOOL_INTERRUPT } from '../../constants'\r\n\r\n/**\r\n * Register the `interrupt` DISPATCH tool (T4.5) β send Ctrl-C to a target terminal's PTY\r\n * to stop a runaway/long-running command. The CALLER (ServerFactory) gates it to the\r\n * orchestrator tier by only invoking `registerInterrupt` for that tier; a worker's\r\n * `tools/list` never contains it (the capability split is structural).\r\n *\r\n * π The host (Canvas ADE MAIN) owns the real safety, identical to assign_prompt: resolve\r\n * the OPAQUE board id (never a label), reject any non-terminal target before any write,\r\n * mint a single-use nonce, BLOCK on a mandatory human confirm, and audit. Content-less β\r\n * the only input is the target id. This tool is the thin transport: validate, forward to\r\n * {@link Orchestrator.interrupt}, and surface a short ack.\r\n */\r\nexport function registerInterrupt(server: McpServer, orchestrator: Orchestrator): void {\r\n server.registerTool(\r\n TOOL_INTERRUPT,\r\n {\r\n description:\r\n 'Interrupt a target terminal board by id: send Ctrl-C to stop its running ' +\r\n 'command. Terminal targets only; requires human confirmation. boardId is required.',\r\n inputSchema: {\r\n boardId: z.string().min(1)\r\n }\r\n },\r\n async (args) => {\r\n await orchestrator.interrupt(args.boardId)\r\n return { content: [{ type: 'text', text: `interrupted ${args.boardId}` }] }\r\n }\r\n )\r\n}\r\n","import { z } from 'zod'\r\nimport type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\r\nimport type { Orchestrator } from '../../orchestrator/Orchestrator'\r\nimport type { BoardId } from '../../types'\r\nimport type { SessionCtx } from '../factory'\r\nimport { TOOL_RELAY_PROMPT } from '../../constants'\r\nimport { dispatchPromptSchema } from './promptSchema'\r\n\r\n/**\r\n * Register the `relay_prompt` agent-to-agent DISPATCH tool (T4.6, the M4 gate). A dispatch\r\n * from board A (`sourceId`) to board B (`targetId`) is authorized by an ORCHESTRATION\r\n * connector AβB β the spatial cable IS the route. The CALLER (ServerFactory) gates it to\r\n * the orchestrator tier (a worker's `tools/list` never contains it).\r\n *\r\n * π The host (Canvas ADE MAIN) owns the real safety: it validates the directed\r\n * orchestration edge `sourceId β targetId` exists and both ends are terminals\r\n * (terminal β terminal only, one-directional, never Browser β PTY), mints a single-use\r\n * nonce, BLOCKS on a mandatory human confirm, and audits β then writes into the target's\r\n * PTY. This tool is the thin transport: validate non-empty inputs, forward to\r\n * {@link Orchestrator.relayPrompt}, surface a short ack.\r\n *\r\n * π Caller-identity binding (BUG-021 part 2): `sourceId` is caller-supplied and the cable\r\n * (not the caller) is the only authorization the host checks, so ANY orchestrator-tier token\r\n * could exploit ANY existing cable β fine while exactly one orchestrator token exists, a hole\r\n * the moment multiple are minted. When the host designates a single command board via\r\n * `commandBoardId`, this tool restricts `relay_prompt` to that one token-bound identity (the\r\n * `ctx.boardId` is derived from the verified bearer token, never client input β same trust\r\n * basis as `write_result`'s board binding). Left `undefined`, behaviour is unchanged (open to\r\n * any orchestrator-tier caller) so existing single-token deployments keep working.\r\n */\r\nexport function registerRelayPrompt(\r\n server: McpServer,\r\n orchestrator: Orchestrator,\r\n ctx: SessionCtx,\r\n commandBoardId?: BoardId\r\n): void {\r\n server.registerTool(\r\n TOOL_RELAY_PROMPT,\r\n {\r\n description:\r\n 'Relay a prompt from one terminal board to another along an orchestration ' +\r\n 'connector (sourceId β targetId): the cable must already exist and both boards ' +\r\n 'must be terminals. Requires human confirmation. sourceId, targetId, prompt required.',\r\n inputSchema: {\r\n sourceId: z.string().min(1),\r\n targetId: z.string().min(1),\r\n prompt: dispatchPromptSchema\r\n }\r\n },\r\n async (args) => {\r\n // π BUG-021: when a command board is designated, only that token-bound identity may\r\n // relay β a second orchestrator token (bound to another board) can't exploit a cable\r\n // it doesn't own. ctx.boardId comes from the verified token, so it can't be forged.\r\n if (commandBoardId !== undefined && ctx.boardId !== commandBoardId) {\r\n return {\r\n isError: true,\r\n content: [\r\n { type: 'text', text: 'relay_prompt: caller is not the designated command orchestrator' }\r\n ]\r\n }\r\n }\r\n await orchestrator.relayPrompt(args.sourceId, args.targetId, args.prompt)\r\n return { content: [{ type: 'text', text: `relayed ${args.sourceId} β ${args.targetId}` }] }\r\n }\r\n )\r\n}\r\n","import { z } from 'zod'\r\nimport type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\r\nimport type { Orchestrator } from '../../orchestrator/Orchestrator'\r\nimport { DEFAULT_BARRIER_TIMEOUT_MS, TOOL_WAIT_FOR_ALL, TOOL_WAIT_FOR_IDLE } from '../../constants'\r\nimport { waitForBoards, type BarrierBoardResult } from '../barrierWaiter'\r\n\r\n/**\r\n * Resolve the effective backstop: an explicit per-call `timeoutMs` wins (β€ 0 / non-finite\r\n * opts out, handled downstream by waitForBoards), else the validated env override, else the\r\n * 30-min default. (env validation mirrors BUG-023: reject non-positive / non-finite.)\r\n */\r\nexport function resolveBarrierTimeout(arg?: number): number {\r\n if (arg !== undefined) return arg\r\n const env = process.env.CANVAS_ADE_BARRIER_TIMEOUT_MS\r\n if (env !== undefined) {\r\n const n = Number(env)\r\n if (Number.isFinite(n) && n > 0) return n\r\n }\r\n return DEFAULT_BARRIER_TIMEOUT_MS\r\n}\r\n\r\n/**\r\n * Register the M5 BARRIER tools β orchestrator-tier blocking waits over the host status\r\n * stream. The CALLER (ServerFactory) gates them to the orchestrator tier (registered only in\r\n * that block); a worker's tools/list never contains them (structural split). READ-ONLY: no\r\n * PTY write, no human confirm, no audit (those are dispatch-tool concerns). Returns a\r\n * `dispose()` that cancels any in-flight waits (called on session close to avoid a leaked\r\n * orchestrator subscription).\r\n */\r\nexport function registerBarrierTools(server: McpServer, orchestrator: Orchestrator): () => void {\r\n const active = new Set<() => void>()\r\n\r\n const run = async (targets: string[], timeoutMs: number): Promise<BarrierBoardResult[]> => {\r\n const handle = waitForBoards({ orchestrator, targets, timeoutMs })\r\n active.add(handle.cancel)\r\n try {\r\n return await handle.promise\r\n } finally {\r\n active.delete(handle.cancel)\r\n }\r\n }\r\n\r\n server.registerTool(\r\n TOOL_WAIT_FOR_IDLE,\r\n {\r\n description:\r\n 'Block until a target board leaves the running state, then report how it settled ' +\r\n '(idle/awaiting-review/blocked/failed/static/gone, or timed-out). Returns the board ' +\r\n \"id + status (+ the board's last write_result when idle). boardId is required; \" +\r\n 'optional timeoutMs (omit for the default backstop; <=0 to wait indefinitely).',\r\n inputSchema: {\r\n boardId: z.string().min(1),\r\n timeoutMs: z.number().optional()\r\n }\r\n },\r\n async (args) => {\r\n const results = await run([args.boardId], resolveBarrierTimeout(args.timeoutMs))\r\n const r = results[0] ?? { id: args.boardId, status: 'timed-out' }\r\n return { content: [{ type: 'text', text: JSON.stringify(r) }] }\r\n }\r\n )\r\n\r\n server.registerTool(\r\n TOOL_WAIT_FOR_ALL,\r\n {\r\n description:\r\n 'Block until EVERY target board has left the running state, then report each one ' +\r\n '(same statuses as wait_for_idle) plus allIdle (true when every target settled to ' +\r\n 'idle). boardIds is a non-empty array; optional timeoutMs (omit for the default ' +\r\n 'backstop; <=0 to wait indefinitely).',\r\n inputSchema: {\r\n boardIds: z.array(z.string().min(1)).min(1),\r\n timeoutMs: z.number().optional()\r\n }\r\n },\r\n async (args) => {\r\n const boards = await run(args.boardIds, resolveBarrierTimeout(args.timeoutMs))\r\n const allIdle = boards.every((b) => b.status === 'idle')\r\n return { content: [{ type: 'text', text: JSON.stringify({ boards, allIdle }) }] }\r\n }\r\n )\r\n\r\n return () => {\r\n for (const cancel of active) cancel()\r\n }\r\n}\r\n","import type { BoardResult, BoardStatusChange, Orchestrator } from '../orchestrator/Orchestrator'\r\n\r\n/** One target's settled outcome. `status` is a bucket, `'gone'`, or `'timed-out'`. */\r\nexport interface BarrierBoardResult {\r\n id: string\r\n status: string\r\n result?: BoardResult\r\n}\r\n\r\n/** Settled = anything that is not actively `running`. */\r\nconst isSettled = (status: string): boolean => status !== 'running'\r\n\r\nexport interface BarrierHandle {\r\n promise: Promise<BarrierBoardResult[]>\r\n /** Force teardown (session close): unsubscribe + resolve unsettled targets as `gone`. */\r\n cancel: () => void\r\n}\r\n\r\n/**\r\n * Wait until every `targets` board has left `running`, event-driven off\r\n * `orchestrator.subscribeStatus` β never a poll. Level-triggered: an already-settled (or\r\n * absent β `gone`) target resolves on the initial read with no edge needed. On the backstop\r\n * deadline, unsettled targets resolve `timed-out` (the promise NEVER rejects β settle-and-report).\r\n * A `timeoutMs` β€ 0 or non-finite opts out of the backstop. Output preserves `targets` order.\r\n */\r\nexport function waitForBoards(opts: {\r\n orchestrator: Pick<Orchestrator, 'listBoards' | 'subscribeStatus' | 'boardResult'>\r\n targets: string[]\r\n timeoutMs: number\r\n}): BarrierHandle {\r\n const { orchestrator, targets, timeoutMs } = opts\r\n const order = targets.slice()\r\n const pending = new Set(targets)\r\n const settled = new Map<string, BarrierBoardResult>()\r\n let done = false\r\n let unsub: () => void = () => {}\r\n let timer: ReturnType<typeof setTimeout> | undefined\r\n let resolveFn!: (r: BarrierBoardResult[]) => void\r\n const promise = new Promise<BarrierBoardResult[]>((resolve) => {\r\n resolveFn = resolve\r\n })\r\n\r\n const finish = (fillStatus: string): void => {\r\n if (done) return\r\n done = true\r\n unsub()\r\n if (timer) clearTimeout(timer)\r\n resolveFn(order.map((id) => settled.get(id) ?? { id, status: fillStatus }))\r\n }\r\n\r\n const recordSettle = async (id: string, status: string): Promise<void> => {\r\n if (done || !pending.has(id)) return\r\n let entry: BarrierBoardResult = { id, status }\r\n if (status === 'idle') {\r\n const r = await orchestrator.boardResult(id)\r\n if (r.present) entry = { id, status, result: r }\r\n }\r\n if (done || !pending.has(id)) return // a concurrent finish/duplicate edge won the race\r\n settled.set(id, entry)\r\n pending.delete(id)\r\n if (pending.size === 0) finish('timed-out')\r\n }\r\n\r\n // Subscribe FIRST so no edge between the initial read and subscription is missed.\r\n unsub = orchestrator.subscribeStatus((change: BoardStatusChange) => {\r\n if (isSettled(change.status)) void recordSettle(change.id, change.status)\r\n })\r\n\r\n // Level-trigger: read current state once; settle already-settled / absent targets.\r\n void (async () => {\r\n const boards = await orchestrator.listBoards()\r\n if (done) return\r\n const current = new Map(boards.map((b) => [b.id, b.status]))\r\n for (const id of order) {\r\n if (done) return\r\n const st = current.get(id)\r\n if (st === undefined) await recordSettle(id, 'gone')\r\n else if (isSettled(st)) await recordSettle(id, st)\r\n }\r\n })()\r\n\r\n if (Number.isFinite(timeoutMs) && timeoutMs > 0) {\r\n timer = setTimeout(() => finish('timed-out'), timeoutMs)\r\n // Don't let the backstop hold the event loop open (best-effort; not present in all envs).\r\n ;(timer as { unref?: () => void }).unref?.()\r\n }\r\n\r\n return { promise, cancel: () => finish('gone') }\r\n}\r\n","import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\r\nimport {\r\n SubscribeRequestSchema,\r\n UnsubscribeRequestSchema\r\n} from '@modelcontextprotocol/sdk/types.js'\r\n\r\nexport interface ResourceSubscriptions {\r\n /** True when a client has an active `resources/subscribe` for this exact URI. */\r\n isSubscribed(uri: string): boolean\r\n}\r\n\r\n/**\r\n * Manually wire `resources/subscribe` / `resources/unsubscribe` for one session β the SDK's\r\n * high-level McpServer does NOT do this (sdk 1.29.0). Registers the `resources.subscribe`\r\n * capability + the two request handlers, tracking the subscribed URIs in a per-session Set.\r\n * The AttentionNotifier consults `isSubscribed` so it only pushes to clients that asked.\r\n * MUST be called BEFORE `server.connect(transport)` (registerCapabilities is connect-gated).\r\n */\r\nexport function installResourceSubscriptions(server: McpServer): ResourceSubscriptions {\r\n const uris = new Set<string>()\r\n server.server.registerCapabilities({ resources: { subscribe: true } })\r\n server.server.setRequestHandler(SubscribeRequestSchema, async (req) => {\r\n uris.add(req.params.uri)\r\n return {}\r\n })\r\n server.server.setRequestHandler(UnsubscribeRequestSchema, async (req) => {\r\n uris.delete(req.params.uri)\r\n return {}\r\n })\r\n return { isSubscribed: (uri) => uris.has(uri) }\r\n}\r\n","import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\r\nimport type { Orchestrator } from '../orchestrator/Orchestrator'\r\nimport { ATTENTION_BUCKETS, ATTENTION_URI } from '../resources/attention'\r\n\r\nexport interface AttentionNotifier {\r\n /** Unsubscribe from the orchestrator status stream (called on session close). */\r\n dispose(): void\r\n}\r\n\r\n/**\r\n * Per session: push `notifications/resources/updated` on `canvas://attention` whenever the\r\n * MEMBERSHIP of the attention set changes (a board enters or leaves blocked/awaiting-review/\r\n * failed). A change WITHIN the set (blockedβfailed) or outside it (runningβidle) emits\r\n * nothing β the resource membership is unchanged. Gated on a live `resources/subscribe` for\r\n * the URI; the emit is wrapped so a post-close `sendResourceUpdated` (\"Not connected\") can't\r\n * throw into the orchestrator fan-out.\r\n */\r\nexport function createAttentionNotifier(deps: {\r\n server: McpServer\r\n orchestrator: Orchestrator\r\n isSubscribed: (uri: string) => boolean\r\n}): AttentionNotifier {\r\n const { server, orchestrator, isSubscribed } = deps\r\n const inAttention = new Set<string>()\r\n\r\n const unsub = orchestrator.subscribeStatus((change) => {\r\n const nowAttn = ATTENTION_BUCKETS.has(change.status)\r\n const wasAttn = inAttention.has(change.id)\r\n if (nowAttn === wasAttn) return // membership unchanged\r\n if (nowAttn) inAttention.add(change.id)\r\n else inAttention.delete(change.id)\r\n if (!isSubscribed(ATTENTION_URI)) return\r\n try {\r\n server.server.sendResourceUpdated({ uri: ATTENTION_URI })\r\n } catch {\r\n // post-close / not-connected emit β drop it; the fan-out must not throw\r\n }\r\n })\r\n\r\n return { dispose: unsub }\r\n}\r\n","import { randomUUID } from 'node:crypto'\r\nimport type { Request, Response } from 'express'\r\nimport { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'\r\nimport { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js'\r\nimport { HEADER_SESSION_ID } from '../constants'\r\nimport type { ServerFactory, SessionCtx } from './factory'\r\n\r\nfunction rpcError(code: number, message: string): unknown {\r\n return { jsonrpc: '2.0', error: { code, message }, id: null }\r\n}\r\n\r\n/**\r\n * Owns the per-session transport map and the stateful streamable-HTTP routing.\r\n * This is the ONLY module importing the SDK transport β isolated so a future SDK\r\n * v2 bump (which renames the transport) is a one-file change.\r\n */\r\nexport class SessionManager {\r\n private readonly transports = new Map<string, StreamableHTTPServerTransport>()\r\n /** Per-session teardown (M5 notifier unsubscribe + in-flight barrier cancel). */\r\n private readonly disposers = new Map<string, () => void>()\r\n\r\n constructor(private readonly factory: ServerFactory) {}\r\n\r\n /** POST /mcp: reuse an existing session, or open a new one on initialize. */\r\n async handlePost(req: Request, res: Response, ctx: SessionCtx): Promise<void> {\r\n const sid = req.header(HEADER_SESSION_ID)\r\n\r\n if (sid !== undefined) {\r\n const existing = this.transports.get(sid)\r\n if (!existing) {\r\n res.status(404).json(rpcError(-32001, 'Session not found'))\r\n return\r\n }\r\n await existing.handleRequest(req, res, req.body)\r\n return\r\n }\r\n\r\n if (!isInitializeRequest(req.body)) {\r\n res\r\n .status(400)\r\n .json(rpcError(-32000, 'Bad Request: no session ID and not an initialize request'))\r\n return\r\n }\r\n\r\n const { server, dispose } = this.factory.getServer(ctx)\r\n const transport = new StreamableHTTPServerTransport({\r\n sessionIdGenerator: () => randomUUID(),\r\n onsessioninitialized: (id) => {\r\n this.transports.set(id, transport)\r\n this.disposers.set(id, dispose)\r\n }\r\n })\r\n transport.onclose = () => {\r\n const id = transport.sessionId\r\n if (id !== undefined) {\r\n this.transports.delete(id)\r\n this.disposers.get(id)?.()\r\n this.disposers.delete(id)\r\n }\r\n }\r\n\r\n await server.connect(transport)\r\n await transport.handleRequest(req, res, req.body)\r\n }\r\n\r\n /** GET (SSE) and DELETE /mcp: route to the named session. */\r\n async handleSession(req: Request, res: Response): Promise<void> {\r\n const sid = req.header(HEADER_SESSION_ID)\r\n if (sid === undefined) {\r\n res.status(400).json(rpcError(-32000, 'Missing session ID'))\r\n return\r\n }\r\n const transport = this.transports.get(sid)\r\n if (!transport) {\r\n res.status(404).json(rpcError(-32001, 'Session not found'))\r\n return\r\n }\r\n await transport.handleRequest(req, res)\r\n }\r\n\r\n /**\r\n * Tear down every live session (called on app quit). Uses `allSettled` so one\r\n * transport whose `close()` rejects can't short-circuit the loop and leak the\r\n * remaining sessions; the map is always cleared.\r\n */\r\n async closeAll(): Promise<void> {\r\n try {\r\n await Promise.allSettled([...this.transports.values()].map((t) => t.close()))\r\n } finally {\r\n for (const dispose of this.disposers.values()) {\r\n try {\r\n dispose()\r\n } catch {\r\n // a teardown throw must not abort the rest\r\n }\r\n }\r\n this.disposers.clear()\r\n this.transports.clear()\r\n }\r\n }\r\n}\r\n","import type { AuthRow } from '../types'\n\n/**\n * In-memory per-board token store. Tokens are minted out-of-band when a board\n * spawns and revoked when it closes β no OAuth flow. In-memory by design:\n * sessions die on MAIN restart (correct for a single-user desktop app).\n */\nexport class TokenStore {\n private readonly rows = new Map<string, AuthRow>()\n\n mint(token: string, row: AuthRow): void {\n this.rows.set(token, row)\n }\n\n revoke(token: string): void {\n this.rows.delete(token)\n }\n\n get(token: string): AuthRow | undefined {\n return this.rows.get(token)\n }\n}\n","import { randomBytes } from 'node:crypto'\nimport type { TokenStore } from './tokens'\nimport type { AuthRow, BoardId, Tier } from '../types'\nimport { defaultScopesFor } from './scopes'\n\nexport interface MintedToken {\n token: string\n row: AuthRow\n}\n\n/**\n * Board-lifetime default expiry (~1 year). The SDK's requireBearerAuth REJECTS a\n * token whose `expiresAt` is not a number (\"Token has no expiration time\"), so a\n * minted token MUST carry one β a missing expiry is not a valid \"never expires\".\n * The value is set far out (board lifetime, revoked on close) so it never kills a\n * long agent run mid-session; pass a short `ttlSeconds` only for a deliberately\n * short-lived token. (ADR 0002, D3.)\n */\nconst DEFAULT_TTL_SECONDS = 365 * 24 * 60 * 60\n\n/**\n * Mint a crypto-random per-board bearer token, store it, and return token + row.\n * Scopes come from the tier (ADR 0002, D2). Always carries a board-lifetime\n * `expiresAt` (the SDK requires a numeric expiry); override with `ttlSeconds`.\n */\nexport function mintBoardToken(\n store: TokenStore,\n input: { boardId: BoardId; tier: Tier; ttlSeconds?: number }\n): MintedToken {\n const token = randomBytes(32).toString('hex')\n const ttl = input.ttlSeconds ?? DEFAULT_TTL_SECONDS\n const row: AuthRow = {\n boardId: input.boardId,\n tier: input.tier,\n scopes: defaultScopesFor(input.tier),\n expiresAt: Math.floor(Date.now() / 1000) + ttl\n }\n store.mint(token, row)\n return { token, row }\n}\n","import type { Scope, Tier } from '../types'\n\n// The Phase 1 scope vocabulary (ADR 0002, D2). Scopes are carried by the token\n// and consumed by per-tool gating in Phases 3+. Tier registration in the factory\n// remains the primary capability gate; scopes are the finer-grained future lever.\nexport const SCOPE_READ: Scope = 'read'\nexport const SCOPE_DISPATCH: Scope = 'dispatch'\nexport const SCOPE_SPAWN: Scope = 'spawn'\nexport const SCOPE_GIT_WRITE: Scope = 'git:write'\nexport const SCOPE_ANSWER_PERMISSION: Scope = 'answer_permission'\n\nconst WORKER_SCOPES: readonly Scope[] = [SCOPE_READ]\nconst ORCHESTRATOR_SCOPES: readonly Scope[] = [\n SCOPE_READ,\n SCOPE_DISPATCH,\n SCOPE_SPAWN,\n SCOPE_GIT_WRITE,\n SCOPE_ANSWER_PERMISSION\n]\n\n/** Default scopes granted to a freshly-minted token of the given tier. */\nexport function defaultScopesFor(tier: Tier): Scope[] {\n return [...(tier === 'orchestrator' ? ORCHESTRATOR_SCOPES : WORKER_SCOPES)]\n}\n","import { writeFileSync } from 'node:fs'\r\nimport { join } from 'node:path'\r\n\r\nexport interface McpJson {\r\n mcpServers: {\r\n 'canvas-ade': {\r\n type: 'http'\r\n url: string\r\n headers: { Authorization: string }\r\n }\r\n }\r\n}\r\n\r\n/**\r\n * Pure builder for a board worktree's project-scoped .mcp.json: a single loopback\r\n * streamable-HTTP server entry carrying the board's bearer token. No OAuth\r\n * discovery is referenced (ADR 0001) β the static bearer token is the authority.\r\n */\r\nexport function buildMcpJson(port: number, token: string): McpJson {\r\n return {\r\n mcpServers: {\r\n 'canvas-ade': {\r\n type: 'http',\r\n url: `http://127.0.0.1:${port}/mcp`,\r\n headers: { Authorization: `Bearer ${token}` }\r\n }\r\n }\r\n }\r\n}\r\n\r\n/**\r\n * Write .mcp.json into a board's worktree dir. Returns the written file path.\r\n * Written owner-only (0600): the file embeds a plaintext bearer token, so it must\r\n * not be world-readable on a multi-user box. (POSIX mode is a no-op on Windows.)\r\n */\r\nexport function writeMcpJson(dir: string, port: number, token: string): string {\r\n const file = join(dir, '.mcp.json')\r\n writeFileSync(file, JSON.stringify(buildMcpJson(port, token), null, 2) + '\\n', {\r\n encoding: 'utf8',\r\n mode: 0o600\r\n })\r\n return file\r\n}\r\n","import type { BoardId } from '../types'\r\nimport type {\r\n BoardConfig,\r\n BoardOutput,\r\n BoardResult,\r\n BoardResultInput,\r\n BoardStatusChange,\r\n BoardSummary,\r\n MemoryDoc,\r\n Orchestrator\r\n} from './Orchestrator'\r\n\r\n/** A no-op Orchestrator for contract tests and standalone runs. */\r\nexport class MockOrchestrator implements Orchestrator {\r\n async listBoards(): Promise<BoardSummary[]> {\r\n return []\r\n }\r\n\r\n async spawnBoard(_input: {\r\n type: string\r\n prompt?: string\r\n cwd?: string\r\n }): Promise<{ id: BoardId }> {\r\n return { id: 'mock-board' }\r\n }\r\n\r\n async closeBoard(_boardId: BoardId): Promise<void> {}\r\n\r\n async configureBoard(_boardId: BoardId, _config: BoardConfig): Promise<void> {}\r\n\r\n async dispatchPrompt(_boardId: BoardId, _text: string): Promise<void> {}\r\n\r\n async writeResult(_boardId: BoardId, _result: BoardResultInput): Promise<void> {}\r\n\r\n async interrupt(_boardId: BoardId): Promise<void> {}\r\n\r\n async relayPrompt(_sourceId: BoardId, _targetId: BoardId, _text: string): Promise<void> {}\r\n\r\n async handoffPrompt(_boardId: BoardId, _text: string): Promise<BoardResult> {\r\n return { present: false }\r\n }\r\n\r\n async gitDiff(_boardId: BoardId): Promise<string> {\r\n return ''\r\n }\r\n\r\n async boardStatus(_boardId: BoardId): Promise<string> {\r\n return 'idle'\r\n }\r\n\r\n async boardOutput(_boardId: BoardId, _opts?: { cursor?: number }): Promise<BoardOutput> {\r\n return { text: '', total: 0, returned: 0, droppedOlder: false }\r\n }\r\n\r\n async boardResult(_boardId: BoardId): Promise<BoardResult> {\r\n return { present: false }\r\n }\r\n\r\n async projectMemory(): Promise<MemoryDoc> {\r\n return { present: false, text: '' }\r\n }\r\n\r\n async boardSummary(_boardId: BoardId): Promise<MemoryDoc> {\r\n return { present: false, text: '' }\r\n }\r\n\r\n /** @internal subscribers for the M5 status stream. */\r\n private readonly statusListeners = new Set<(change: BoardStatusChange) => void>()\r\n\r\n subscribeStatus(listener: (change: BoardStatusChange) => void): () => void {\r\n this.statusListeners.add(listener)\r\n return () => {\r\n this.statusListeners.delete(listener)\r\n }\r\n }\r\n\r\n /** Test seam: drive a status change through the subscription fan-out. */\r\n __emitStatus(change: BoardStatusChange): void {\r\n for (const cb of this.statusListeners) {\r\n try {\r\n cb(change)\r\n } catch {\r\n // isolate a throwing listener (same discipline as the app-side fan-out)\r\n }\r\n }\r\n }\r\n}\r\n"],"mappings":"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","names":["McpServer","ResourceTemplate","ResourceTemplate","ResourceTemplate","ResourceTemplate","z","z","z","z","z","z","z","z","z","z","z","z","z","z","z","z","z","McpServer"]}
|
|
1
|
+
{"version":3,"sources":["../src/server/mcpHttp.ts","../src/constants.ts","../src/auth/verifier.ts","../src/security/origin.ts","../src/security/host.ts","../src/server/factory.ts","../package.json","../src/resources/boards.ts","../src/resources/boardStates.ts","../src/resources/attention.ts","../src/resources/output.ts","../src/resources/result.ts","../src/resources/memory.ts","../src/prompts/index.ts","../src/server/tools/spawnBoard.ts","../src/server/tools/closeBoard.ts","../src/server/tools/configureBoard.ts","../src/server/tools/handoffPrompt.ts","../src/server/tools/promptSchema.ts","../src/server/tools/assignPrompt.ts","../src/server/tools/writeResult.ts","../src/server/tools/interrupt.ts","../src/server/tools/relayPrompt.ts","../src/server/tools/barriers.ts","../src/server/barrierWaiter.ts","../src/server/resourceSubscriptions.ts","../src/server/attentionNotifier.ts","../src/server/transport.ts","../src/auth/tokens.ts","../src/auth/mint.ts","../src/auth/scopes.ts","../src/config/mcpJson.ts","../src/orchestrator/mock.ts"],"sourcesContent":["import { createServer, type Server } from 'node:http'\nimport express, { type Express } from 'express'\nimport { requireBearerAuth } from '@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js'\nimport type { AuthInfo } from '@modelcontextprotocol/sdk/server/auth/types.js'\nimport { MCP_PATH } from '../constants'\nimport type { BoardId, Tier } from '../types'\nimport type { Orchestrator } from '../orchestrator/Orchestrator'\nimport { createVerifier } from '../auth/verifier'\nimport type { TokenStore } from '../auth/tokens'\nimport { originGuard } from '../security/origin'\nimport { hostGuard } from '../security/host'\nimport { ServerFactory, type SessionCtx } from './factory'\nimport { SessionManager } from './transport'\n\nexport interface McpServerDeps {\n orchestrator: Orchestrator\n tokens: TokenStore\n /**\n * Optional single command-orchestrator board id (BUG-021). When set, `relay_prompt` is\n * restricted to the token bound to this board, so a second orchestrator-tier token can't\n * drive orchestration cables it doesn't own. Omit to keep the prior open-to-any-orchestrator\n * behaviour (correct for a single-orchestrator-token deployment).\n */\n commandBoardId?: BoardId\n}\n\nexport interface RunningMcpServer {\n app: Express\n httpServer: Server\n port: number\n setAllowedOrigins(origins: readonly string[]): void\n close(): Promise<void>\n}\n\n/** Re-derive the session context from the server-verified bearer token. */\nexport function ctxFromAuth(auth: AuthInfo | undefined): SessionCtx {\n const extra = (auth?.extra ?? {}) as { tier?: unknown; boardId?: unknown }\n const tier: Tier = extra.tier === 'orchestrator' ? 'orchestrator' : 'worker'\n const boardId = typeof extra.boardId === 'string' ? extra.boardId : ''\n return { tier, scopes: auth?.scopes ?? [], boardId }\n}\n\n/**\n * Creates the loopback streamable-HTTP MCP server and starts listening on an\n * ephemeral 127.0.0.1 port. Pipeline: originGuard -> requireBearerAuth -> /mcp.\n * NO OAuth discovery routes are mounted (resourceMetadataUrl is left unset), so\n * MCP clients use the static per-board bearer token without a \"needs auth\" flag.\n */\nexport async function createMcpHttpServer(deps: McpServerDeps): Promise<RunningMcpServer> {\n const app = express()\n\n // DNS-rebinding defence in two layers: Host (always required) THEN Origin. These\n // run BEFORE any body parsing so a non-loopback request is 403'd outright and its\n // body is never parsed (no pre-auth parse / memory pressure).\n app.use(hostGuard())\n let allowedOrigins: readonly string[] = []\n app.use(originGuard(() => allowedOrigins))\n\n const verifier = createVerifier(deps.tokens)\n app.use(MCP_PATH, requireBearerAuth({ verifier }))\n\n // Parse the JSON body only on /mcp, only after Host/Origin/bearer have passed, and\n // with an explicit cap (MCP control messages are small; reject oversized bodies).\n app.use(MCP_PATH, express.json({ limit: '1mb' }))\n\n const sessions = new SessionManager(new ServerFactory(deps.orchestrator, deps.commandBoardId))\n\n app.post(MCP_PATH, (req, res, next) => {\n sessions.handlePost(req, res, ctxFromAuth(req.auth)).catch(next)\n })\n app.get(MCP_PATH, (req, res, next) => {\n sessions.handleSession(req, res, ctxFromAuth(req.auth)).catch(next)\n })\n app.delete(MCP_PATH, (req, res, next) => {\n sessions.handleSession(req, res, ctxFromAuth(req.auth)).catch(next)\n })\n\n const httpServer = createServer(app)\n await new Promise<void>((resolve) => httpServer.listen(0, '127.0.0.1', () => resolve()))\n const address = httpServer.address()\n const port = typeof address === 'object' && address !== null ? address.port : 0\n allowedOrigins = [`http://127.0.0.1:${port}`, `http://localhost:${port}`]\n\n return {\n app,\n httpServer,\n port,\n setAllowedOrigins(origins) {\n allowedOrigins = origins\n },\n async close() {\n await sessions.closeAll()\n await new Promise<void>((resolve, reject) => {\n httpServer.close((err) => (err ? reject(err) : resolve()))\n })\n }\n }\n}\n","/** The single MCP endpoint path. */\nexport const MCP_PATH = '/mcp'\n\n/** Session-id header (MCP spec; routing only β authority is the bearer token). */\nexport const HEADER_SESSION_ID = 'mcp-session-id'\n\n/** Phase 0 proof tools. */\nexport const TOOL_PING = 'ping'\nexport const TOOL_ORCHESTRATOR_PING = 'orchestrator_ping'\n\n/** Phase 3 lifecycle tools (write path). Orchestrator-tier only. */\nexport const TOOL_SPAWN_BOARD = 'spawn_board'\nexport const TOOL_CLOSE_BOARD = 'close_board'\nexport const TOOL_CONFIGURE_BOARD = 'configure_board'\n\n/** Phase 4 dispatch tools (write into another board's PTY). Orchestrator-tier only. */\nexport const TOOL_HANDOFF_PROMPT = 'handoff_prompt'\nexport const TOOL_ASSIGN_PROMPT = 'assign_prompt'\nexport const TOOL_INTERRUPT = 'interrupt'\nexport const TOOL_RELAY_PROMPT = 'relay_prompt'\n\n/**\n * Phase 4 worker-tier WRITE tool (T4.4) β the FIRST tool a worker may call to mutate\n * state: a worker records its OWN board's structured result. Registered for BOTH tiers\n * and bound to the caller's `ctx.boardId` (a worker can't forge another board's result).\n */\nexport const TOOL_WRITE_RESULT = 'write_result'\n\n/**\n * Board types an orchestrator may spawn (T3.1). A closed allowlist β spawn is a\n * WRITE, so an unknown/forward type is rejected, never forwarded to the host.\n * (Read surfaces like `canvas://boards` keep `type` an open string for forward\n * compatibility; the write path is deliberately stricter.)\n */\nexport const SPAWNABLE_BOARD_TYPES = ['terminal', 'browser', 'planning'] as const\n\n/**\n * Hard cap on the chars returned by ONE `canvas://board/{id}/output` page (T1.4 π).\n * The MCP output budget is ~25k; we never emit a larger page even if the host\n * over-returns. MUST match the app accessor's page size (`MAX_OUTPUT_PAGE` in\n * `src/main/ptyOutput.ts`) so the tail-anchored cursor math lines up across the\n * two repos. Unit = UTF-16 code units (JS `String.length`), matching the host ring.\n */\nexport const MAX_OUTPUT_PAGE = 25_000\n\n/**\n * Phase 5 (M5) BARRIER tools β orchestrator-tier blocking waits over the host status\n * stream. READ-ONLY (no PTY write / human confirm / audit β those are for dispatch tools).\n */\nexport const TOOL_WAIT_FOR_IDLE = 'wait_for_idle'\nexport const TOOL_WAIT_FOR_ALL = 'wait_for_all'\n\n/**\n * Default backstop deadline for a barrier wait (30 min) when the tool's `timeoutMs` is\n * omitted. Env-tunable via `CANVAS_ADE_BARRIER_TIMEOUT_MS` (finite, > 0, else ignored).\n * A per-call `timeoutMs` β€ 0 or non-finite opts out entirely (mirrors the mcpConfirm\n * 10-min backstop convention β settle-and-report never throws on expiry).\n */\nexport const DEFAULT_BARRIER_TIMEOUT_MS = 30 * 60_000\n","import { InvalidTokenError } from '@modelcontextprotocol/sdk/server/auth/errors.js'\nimport type { OAuthTokenVerifier } from '@modelcontextprotocol/sdk/server/auth/provider.js'\nimport type { AuthInfo } from '@modelcontextprotocol/sdk/server/auth/types.js'\nimport type { TokenStore } from './tokens'\n\n/**\n * A bearer-token verifier over our in-memory token store. The capability tier\n * is carried in `extra.{tier,boardId}` and re-derived server-side on every\n * request. Throwing InvalidTokenError makes requireBearerAuth return 401 (not\n * 500). This file (with tokens.ts) is the only place SDK auth is imported.\n */\nexport function createVerifier(store: TokenStore): OAuthTokenVerifier {\n return {\n async verifyAccessToken(token: string): Promise<AuthInfo> {\n const row = store.get(token)\n if (!row) throw new InvalidTokenError('Unknown or revoked token')\n return {\n token,\n clientId: row.boardId,\n scopes: row.scopes,\n expiresAt: row.expiresAt,\n extra: { tier: row.tier, boardId: row.boardId }\n }\n }\n }\n}\n","import type { RequestHandler } from 'express'\n\n/**\n * Rejects requests whose Origin is not in the allowlist β a DNS-rebinding guard\n * (so a web page can't drive the local MCP server). Requests with NO Origin\n * header (a CLI MCP client / non-browser) pass: the bearer token is the real\n * authority. `getAllowed` is a getter so the allowlist can be set AFTER\n * listen(0) resolves the real ephemeral port. We own this rather than relying on\n * the transport's @deprecated DNS-rebinding flags.\n */\nexport function originGuard(getAllowed: () => readonly string[]): RequestHandler {\n return (req, res, next) => {\n const origin = req.headers.origin\n if (origin === undefined) {\n next()\n return\n }\n if (getAllowed().includes(origin)) {\n next()\n return\n }\n res.status(403).json({ error: 'forbidden_origin' })\n }\n}\n","import { isIP } from 'node:net'\nimport type { RequestHandler } from 'express'\n\n/** Exact loopback host spellings (the common cases; IP forms are checked below too). */\nconst LOOPBACK_HOSTS = new Set(['localhost', '127.0.0.1', '::1', '0:0:0:0:0:0:0:1'])\n\n/**\n * Extract the hostname from a `Host` header value, dropping an optional `:port`.\n * Handles the three shapes that reach a loopback listener:\n * - `localhost` / `localhost:5173` β `localhost`\n * - `127.0.0.1` / `127.0.0.1:443` β `127.0.0.1`\n * - `[::1]` / `[::1]:5173` (bracketed v6) β `::1`\n * - `::1` (bare v6 literal, no port) β `::1`\n */\nfunction parseHostname(host: string): string {\n const h = host.trim()\n if (h.startsWith('[')) {\n const end = h.indexOf(']')\n return end === -1 ? h.slice(1) : h.slice(1, end)\n }\n // A bare IPv6 literal (e.g. `::1`) has multiple colons and no `[..]` β it is\n // the hostname itself, never `host:port`. Only a single colon means a port.\n if ((h.match(/:/g)?.length ?? 0) > 1) return h\n const idx = h.indexOf(':')\n return idx === -1 ? h : h.slice(0, idx)\n}\n\n/**\n * True only for the loopback hosts the server is bound to. Case-insensitive;\n * a `:port` suffix is ignored. Anything else (a public name, a suffix attack\n * like `localhost.evil.com`, a non-loopback IP, or an empty value) is false.\n */\nexport function isLoopbackHost(host: string): boolean {\n if (host === '') return false\n const h = parseHostname(host).toLowerCase()\n if (LOOPBACK_HOSTS.has(h)) return true\n // The whole 127.0.0.0/8 block is loopback β accept any 127.x.x.x (a browser or\n // CLI may use a non-.1 address). `isIP` confirms it is a well-formed IPv4 literal\n // so a name like `127.evil.com` can't slip through the prefix check.\n if (isIP(h) === 4 && h.startsWith('127.')) return true\n return false\n}\n\n/**\n * Rejects requests whose `Host` header is not a loopback host β the MANDATORY\n * DNS-rebinding mitigation. Binding to 127.0.0.1 and validating `Origin` alone is\n * INSUFFICIENT: a browser can be tricked into sending a request whose `Host`\n * resolves to the loopback listener (TS-SDK CVE-2025-66414, fixed upstream in\n * sdk 1.24.0). For Expanse the exact vector is a Browser board previewing a\n * malicious `localhost` page. Unlike `Origin` (absent for non-browser CLI\n * clients, so missing-Origin passes), a conformant HTTP/1.1 request ALWAYS\n * carries `Host`, so a missing or non-loopback `Host` is rejected with 403.\n * Pairs with `originGuard` + the per-board bearer token (defence in depth).\n */\nexport function hostGuard(): RequestHandler {\n return (req, res, next) => {\n const host = req.headers.host\n if (host !== undefined && isLoopbackHost(host)) {\n next()\n return\n }\n res.status(403).json({ error: 'forbidden_host' })\n }\n}\n","import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport pkg from '../../package.json' with { type: 'json' }\nimport type { BoardId, Scope, Tier } from '../types'\nimport type { Orchestrator } from '../orchestrator/Orchestrator'\nimport { TOOL_ORCHESTRATOR_PING, TOOL_PING } from '../constants'\nimport { registerBoardResources } from '../resources/boards'\nimport { registerPrompts } from '../prompts/index'\nimport { registerSpawnBoard } from './tools/spawnBoard'\nimport { registerCloseBoard } from './tools/closeBoard'\nimport { registerConfigureBoard } from './tools/configureBoard'\nimport { registerHandoffPrompt } from './tools/handoffPrompt'\nimport { registerAssignPrompt } from './tools/assignPrompt'\nimport { registerWriteResult } from './tools/writeResult'\nimport { registerInterrupt } from './tools/interrupt'\nimport { registerRelayPrompt } from './tools/relayPrompt'\nimport { registerBarrierTools } from './tools/barriers'\nimport { installResourceSubscriptions } from './resourceSubscriptions'\nimport { createAttentionNotifier } from './attentionNotifier'\n\n/** Per-session context, derived from the validated bearer token. */\nexport interface SessionCtx {\n tier: Tier\n scopes: Scope[]\n boardId: BoardId\n}\n\n// Version is sourced from package.json so the handshake never drifts from the\n// published version (clients log/compat-check serverInfo.version).\nconst SERVER_INFO = { name: 'canvas-ade-mcp', version: pkg.version }\n\n/**\n * Builds a fresh McpServer per session, registering ONLY the tools the session's\n * tier is allowed. The capability split is structural (by registration) β a\n * worker's tools/list never even contains an orchestrator tool. Never\n * register-all-then-gate-in-handler.\n */\nexport class ServerFactory {\n /**\n * @param commandBoardId Optional single command-orchestrator board id (BUG-021). When set,\n * `relay_prompt` is restricted to that token-bound identity so a second orchestrator-tier\n * token can't drive cables it doesn't own. Left undefined β relay open to any orchestrator\n * (the prior single-token behaviour).\n */\n constructor(\n private readonly orchestrator: Orchestrator,\n private readonly commandBoardId?: BoardId\n ) {}\n\n getServer(ctx: SessionCtx): { server: McpServer; dispose: () => void } {\n const server = new McpServer(SERVER_INFO)\n const disposers: Array<() => void> = []\n\n // ping β both tiers.\n server.registerTool(TOOL_PING, { description: 'Health check. Returns \"pong\".' }, async () => ({\n content: [{ type: 'text', text: 'pong' }]\n }))\n\n // Orchestrator-only tools β registered ONLY for the orchestrator tier, so a\n // worker's tools/list never even contains them (the capability split is\n // structural, never a per-handler check).\n if (ctx.tier === 'orchestrator') {\n server.registerTool(\n TOOL_ORCHESTRATOR_PING,\n { description: 'Orchestrator-only health check. Returns \"orchestrator-pong\".' },\n async () => ({ content: [{ type: 'text', text: 'orchestrator-pong' }] })\n )\n // Lifecycle write tools (Phase 3+).\n registerSpawnBoard(server, this.orchestrator)\n registerCloseBoard(server, this.orchestrator)\n registerConfigureBoard(server, this.orchestrator)\n // Dispatch write tools (Phase 4) β write into another board's PTY.\n registerHandoffPrompt(server, this.orchestrator)\n registerAssignPrompt(server, this.orchestrator)\n registerInterrupt(server, this.orchestrator)\n // relay_prompt is bound to the designated command orchestrator when one is set (BUG-021).\n registerRelayPrompt(server, this.orchestrator, ctx, this.commandBoardId)\n // M5 barriers β orchestrator-tier; dispose cancels any in-flight wait on session close.\n disposers.push(registerBarrierTools(server, this.orchestrator))\n }\n\n // write_result (T4.4) β the FIRST worker-tier WRITE tool. Registered for BOTH tiers\n // (OUTSIDE the orchestrator-only block) and bound to ctx.boardId so a worker can only\n // record its OWN board's result, never forge another's.\n registerWriteResult(server, this.orchestrator, ctx)\n\n registerBoardResources(server, this.orchestrator)\n registerPrompts(server)\n\n // M5 attention push (both tiers β observation is safe). Subscribe wiring MUST precede\n // connect (registerCapabilities is connect-gated); getServer always runs before connect.\n const subs = installResourceSubscriptions(server)\n const notifier = createAttentionNotifier({\n server,\n orchestrator: this.orchestrator,\n isSubscribed: subs.isSubscribed\n })\n disposers.push(() => notifier.dispose())\n\n return {\n server,\n dispose: () => {\n for (const d of disposers) d()\n }\n }\n }\n}\n","{\n \"name\": \"@expanse-ade/mcp\",\n \"version\": \"0.9.1\",\n \"packageManager\": \"pnpm@9.15.9\",\n \"description\": \"MCP server layer for Canvas ADE \\u00e2\\u20ac\\u201d lets AI agents in Terminal boards orchestrate the canvas (command board / swarm).\",\n \"type\": \"module\",\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/ch923dev/canvas-ade-mcp.git\"\n },\n \"publishConfig\": {\n \"access\": \"public\"\n },\n \"main\": \"./dist/index.js\",\n \"types\": \"./dist/index.d.ts\",\n \"exports\": {\n \".\": {\n \"types\": \"./dist/index.d.ts\",\n \"import\": \"./dist/index.js\"\n }\n },\n \"files\": [\n \"dist\"\n ],\n \"engines\": {\n \"node\": \">=20\"\n },\n \"scripts\": {\n \"build\": \"tsup\",\n \"typecheck\": \"tsc --noEmit\",\n \"test\": \"vitest run --project contract\",\n \"test:live\": \"vitest run --project live\",\n \"lint\": \"eslint .\",\n \"format\": \"prettier --write .\",\n \"format:check\": \"prettier --check .\"\n },\n \"license\": \"MIT\",\n \"dependencies\": {\n \"@modelcontextprotocol/sdk\": \"^1.29.0\",\n \"express\": \"^5.2.1\",\n \"zod\": \"^4.4.3\"\n },\n \"devDependencies\": {\n \"@eslint/js\": \"^10.0.1\",\n \"@modelcontextprotocol/inspector\": \"^0.21.2\",\n \"@types/express\": \"^5.0.6\",\n \"@types/node\": \"^25.9.1\",\n \"eslint\": \"^10.4.1\",\n \"eslint-config-prettier\": \"^10.1.8\",\n \"prettier\": \"^3.8.3\",\n \"tsup\": \"^8.5.1\",\n \"typescript\": \"^6.0.3\",\n \"typescript-eslint\": \"^8.60.0\",\n \"vitest\": \"^4.1.7\"\n }\n}\n","import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport type { Orchestrator } from '../orchestrator/Orchestrator'\nimport { registerBoardStatesResource } from './boardStates'\nimport { registerAttentionResource } from './attention'\nimport { registerBoardOutputResource } from './output'\nimport { registerBoardResultResource } from './result'\nimport { registerMemoryResources } from './memory'\n\n/**\n * Registers the read-only board observation resources. Available to BOTH tiers β\n * observation is safe (no write, no cross-agent influence):\n *\n * - `canvas://boards` β the full board list (id/type/title + coarse status bucket).\n * - `canvas://board/{id}/status` β one board's coarse status bucket (T1.1). The\n * bucket is derived host-side from the live runtime (terminal PTY + browser load\n * state) and is the same value an agent sees in `canvas://boards` and a human sees\n * on the board's on-canvas status pill β one source of truth.\n */\nexport function registerBoardResources(server: McpServer, orchestrator: Orchestrator): void {\n server.registerResource(\n 'boards',\n 'canvas://boards',\n { description: 'List of boards currently on the canvas.', mimeType: 'application/json' },\n async (uri) => ({\n contents: [{ uri: uri.href, text: JSON.stringify(await orchestrator.listBoards()) }]\n })\n )\n\n server.registerResource(\n 'board-status',\n new ResourceTemplate('canvas://board/{id}/status', { list: undefined }),\n {\n description:\n \"A single board's coarse status bucket (idle/running/awaiting-review/blocked/failed/static).\",\n mimeType: 'application/json'\n },\n async (uri, variables) => {\n const id = Array.isArray(variables.id) ? variables.id[0] : variables.id\n if (!id) throw new Error('canvas://board/{id}/status: missing board id')\n const status = await orchestrator.boardStatus(id)\n return { contents: [{ uri: uri.href, text: JSON.stringify({ id, status }) }] }\n }\n )\n\n registerBoardStatesResource(server, orchestrator)\n registerAttentionResource(server, orchestrator)\n registerBoardOutputResource(server, orchestrator)\n registerBoardResultResource(server, orchestrator)\n registerMemoryResources(server, orchestrator)\n}\n","import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport type { BoardSummary, Orchestrator } from '../orchestrator/Orchestrator'\nimport type { BoardId } from '../types'\n\n/**\n * Roll up a board list into `{ statusBucket: BoardId[] }`, preserving board order\n * within each bucket and omitting buckets with no boards. The compact \"who is in\n * which state\" view β counts are `array.length`; full per-board detail stays in\n * `canvas://boards`.\n */\nexport function groupBoardsByStatus(boards: BoardSummary[]): Record<string, BoardId[]> {\n const grouped: Record<string, BoardId[]> = {}\n for (const b of boards) {\n ;(grouped[b.status] ??= []).push(b.id)\n }\n return grouped\n}\n\n/**\n * Registers the read-only `canvas://board-states` roll-up resource (both tiers β\n * observation is safe). Lets an orchestrator see the swarm's shape at a glance\n * (how many boards are running / blocked / failed) without paging the full list.\n */\nexport function registerBoardStatesResource(server: McpServer, orchestrator: Orchestrator): void {\n server.registerResource(\n 'board-states',\n 'canvas://board-states',\n {\n description: 'Boards on the canvas grouped by status bucket ({ bucket: boardId[] }).',\n mimeType: 'application/json'\n },\n async (uri) => ({\n contents: [\n {\n uri: uri.href,\n text: JSON.stringify(groupBoardsByStatus(await orchestrator.listBoards()))\n }\n ]\n })\n )\n}\n","import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport type { BoardSummary, Orchestrator } from '../orchestrator/Orchestrator'\n\n/** The canonical URI of the read-only attention resource (M5 notifier pushes updates here). */\nexport const ATTENTION_URI = 'canvas://attention'\n\n/**\n * Status buckets that mean \"a human's attention is needed\": a worker waiting on a\n * review/decision, blocked on a permission prompt, or failed. (`running`/`idle`/\n * `static` do not need a human.) Buckets are emitted host-side: `failed` already\n * flows today (a browser that fails to load); `blocked`/`awaiting-review` start\n * flowing when their detection lands (M8 permission detection + the terminal\n * awaiting-input wire) β this resource already surfaces them once they do.\n */\nexport const ATTENTION_BUCKETS: ReadonlySet<string> = new Set([\n 'blocked',\n 'awaiting-review',\n 'failed'\n])\n\n/** The subset of boards currently in an attention bucket (order preserved). */\nexport function selectAttention(boards: BoardSummary[]): BoardSummary[] {\n return boards.filter((b) => ATTENTION_BUCKETS.has(b.status))\n}\n\n/**\n * Registers the read-only `canvas://attention` resource (both tiers β observation is\n * safe): the boards needing a human, with full detail so the agent knows which board\n * and why. The on-canvas \"needs-you\" queue (M5 / SB-1) is the human-visible twin.\n */\nexport function registerAttentionResource(server: McpServer, orchestrator: Orchestrator): void {\n server.registerResource(\n 'attention',\n ATTENTION_URI,\n {\n description: 'Boards needing a human (blocked / awaiting-review / failed).',\n mimeType: 'application/json'\n },\n async (uri) => ({\n contents: [\n { uri: uri.href, text: JSON.stringify(selectAttention(await orchestrator.listBoards())) }\n ]\n })\n )\n}\n","import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport type { Variables } from '@modelcontextprotocol/sdk/shared/uriTemplate.js'\nimport type { BoardOutput, Orchestrator } from '../orchestrator/Orchestrator'\nimport { MAX_OUTPUT_PAGE } from '../constants'\n\n/** First value of a templated URI variable (templates yield string | string[]). */\nfunction first(v: string | string[] | undefined): string | undefined {\n return Array.isArray(v) ? v[0] : v\n}\n\n/**\n * Read one capped page of a board's scrollback and serialize it. Shared by both the\n * tail (no cursor) and `?cursor` templates. **π Hard-caps the page at\n * `MAX_OUTPUT_PAGE` here too** β defense in depth, so a misbehaving host can never\n * make this resource dump more than one MCP page (the host already caps; this is the\n * belt-and-suspenders the security checklist requires). `nextCursor`/`droppedOlder`\n * pass through unchanged so the agent always knows whether to keep paging.\n */\nasync function readPage(\n orchestrator: Orchestrator,\n uriHref: string,\n variables: Variables\n): Promise<{ contents: Array<{ uri: string; text: string }> }> {\n const id = first(variables.id)\n if (!id) throw new Error('canvas://board/{id}/output: missing board id')\n // A cursor, when supplied, must be a non-negative integer (chars-from-end). A\n // malformed value is NOT silently coerced to \"newest tail\" β that would loop an\n // agent forever (it pages with garbage, keeps getting page 1). Fail loud instead.\n const rawCursor = first(variables.cursor)\n let opts: { cursor: number } | undefined\n if (rawCursor !== undefined) {\n const cursor = Number(rawCursor)\n if (!Number.isInteger(cursor) || cursor < 0) {\n throw new Error(`canvas://board/${id}/output: invalid cursor \"${rawCursor}\"`)\n }\n opts = { cursor }\n }\n\n const out = await orchestrator.boardOutput(id, opts)\n // π never emit more than one page, whatever the host returned. Keep the NEWEST\n // MAX_OUTPUT_PAGE chars (the contract is tail-anchored β `text` is the newest\n // slice), NOT the oldest. When we have to drop, the dropped chars are OLDER and\n // remain unconsumed, so the cursor MUST advance by exactly what we kept (= the\n // dropped older content is still reachable on the next, older page).\n let page: BoardOutput = out\n if (out.text.length > MAX_OUTPUT_PAGE) {\n const text = out.text.slice(-MAX_OUTPUT_PAGE)\n page = {\n ...out,\n text,\n returned: text.length,\n nextCursor: (opts?.cursor ?? 0) + text.length\n }\n }\n return { contents: [{ uri: uriHref, text: JSON.stringify(page) }] }\n}\n\n/**\n * Registers the read-only, capped, paginated `canvas://board/{id}/output` resource\n * (T1.4 π β both tiers; observation is safe). Two disjoint templates back ONE\n * logical resource because the SDK's UriTemplate compiles `{?cursor}` into a\n * MANDATORY query group β a URI with no `?cursor` won't match it. So:\n * - `canvas://board/{id}/output` β newest tail page (no cursor)\n * - `canvas://board/{id}/output{?cursor}` β an older page at the given cursor\n * Their regexes are mutually exclusive (`β¦/output$` vs `β¦/output\\?cursor=β¦$`), so\n * each concrete read routes to exactly one. Both delegate to `readPage`.\n */\nexport function registerBoardOutputResource(server: McpServer, orchestrator: Orchestrator): void {\n const meta = {\n description:\n \"A capped, paginated, ANSI-stripped page of a board's recent output (tail-anchored; pass nextCursor as ?cursor for older).\",\n mimeType: 'application/json'\n }\n server.registerResource(\n 'board-output',\n new ResourceTemplate('canvas://board/{id}/output', { list: undefined }),\n meta,\n (uri, variables) => readPage(orchestrator, uri.href, variables)\n )\n server.registerResource(\n 'board-output-paged',\n new ResourceTemplate('canvas://board/{id}/output{?cursor}', { list: undefined }),\n meta,\n (uri, variables) => readPage(orchestrator, uri.href, variables)\n )\n}\n","import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport type { Orchestrator } from '../orchestrator/Orchestrator'\n\n/**\n * Registers the read-only `canvas://board/{id}/result` resource (T1.5 β both tiers;\n * observation is safe). Returns a board's STRUCTURED last result (a verdict + summary\n * + references, not raw logs β raw scrollback is `canvas://board/{id}/output`). v1 is\n * an observational shell: every board reads `{ present: false }` until M4's\n * `write_result` lets a worker record one; the resource shape is stable across that.\n */\nexport function registerBoardResultResource(server: McpServer, orchestrator: Orchestrator): void {\n server.registerResource(\n 'board-result',\n new ResourceTemplate('canvas://board/{id}/result', { list: undefined }),\n {\n description:\n \"A board's structured last result (verdict + summary + references, not raw logs). Empty shell until a result is recorded.\",\n mimeType: 'application/json'\n },\n async (uri, variables) => {\n const id = Array.isArray(variables.id) ? variables.id[0] : variables.id\n if (!id) throw new Error('canvas://board/{id}/result: missing board id')\n const result = await orchestrator.boardResult(id)\n return { contents: [{ uri: uri.href, text: JSON.stringify(result) }] }\n }\n )\n}\n","import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport type { Orchestrator } from '../orchestrator/Orchestrator'\n\n/**\n * Registers the read-only project-memory resources (T1.7 β both tiers; π PASSIVE\n * context only, these expose no action):\n * - `canvas://memory` β the project memory index.\n * - `canvas://board/{id}/summary` β a board's memory summary.\n * Backed by the sibling Brain/Memory engine's `.canvas/memory/`. When that subsystem\n * is absent (it ships on a separate track), each resource returns the empty shell\n * `{ present: false, text: '' }` β graceful, never an error.\n */\nexport function registerMemoryResources(server: McpServer, orchestrator: Orchestrator): void {\n server.registerResource(\n 'memory',\n 'canvas://memory',\n {\n description: 'Project memory index (passive context). Empty shell when no memory exists.',\n mimeType: 'application/json'\n },\n async (uri) => ({\n contents: [{ uri: uri.href, text: JSON.stringify(await orchestrator.projectMemory()) }]\n })\n )\n\n server.registerResource(\n 'board-summary',\n new ResourceTemplate('canvas://board/{id}/summary', { list: undefined }),\n {\n description: \"A board's memory summary (passive context). Empty shell when none exists.\",\n mimeType: 'application/json'\n },\n async (uri, variables) => {\n const id = Array.isArray(variables.id) ? variables.id[0] : variables.id\n if (!id) throw new Error('canvas://board/{id}/summary: missing board id')\n return { contents: [{ uri: uri.href, text: JSON.stringify(await orchestrator.boardSummary(id)) }] }\n }\n )\n}\n","import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\n\n/** Phase 0 placeholder β prompt templates (review_worktree, etc.) land later. */\nexport function registerPrompts(_server: McpServer): void {\n // no prompts yet\n}\n","import { z } from 'zod'\nimport type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport type { Orchestrator } from '../../orchestrator/Orchestrator'\nimport { SPAWNABLE_BOARD_TYPES, TOOL_SPAWN_BOARD } from '../../constants'\n\n/**\n * Register the `spawn_board` lifecycle tool (T3.1) β the first WRITE tool. The\n * CALLER (ServerFactory) gates this to the orchestrator tier by only invoking\n * `registerSpawnBoard` for that tier; a worker's `tools/list` never contains it.\n * The closed `type` enum is the input-validation guard: an unknown type is\n * rejected by the SDK's schema check BEFORE the orchestrator is ever called, so a\n * bad type can never reach the host's board factory.\n *\n * The orchestrator (Canvas ADE MAIN) mints the board id and drives the canvas via\n * the command channel; the tool just surfaces that server-issued id to the agent.\n */\nexport function registerSpawnBoard(server: McpServer, orchestrator: Orchestrator): void {\n server.registerTool(\n TOOL_SPAWN_BOARD,\n {\n description:\n 'Create a new board on the canvas. type is one of terminal | browser | planning. ' +\n 'Optional prompt (terminal launch command / agent task) and cwd (working directory). ' +\n 'Returns the new board id. Subject to a concurrency cap.',\n inputSchema: {\n type: z.enum(SPAWNABLE_BOARD_TYPES),\n prompt: z.string().optional(),\n cwd: z.string().optional()\n }\n },\n async (args) => {\n const { id } = await orchestrator.spawnBoard({\n type: args.type,\n prompt: args.prompt,\n cwd: args.cwd\n })\n return { content: [{ type: 'text', text: id }] }\n }\n )\n}\n","import { z } from 'zod'\nimport type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport type { Orchestrator } from '../../orchestrator/Orchestrator'\nimport { TOOL_CLOSE_BOARD } from '../../constants'\n\n/**\n * Register the `close_board` lifecycle tool (T3.2) β a WRITE tool. Gated to the\n * orchestrator tier by the CALLER (ServerFactory). The host drains the board's PTY\n * gracefully before removing it. `id` is required + non-empty (an empty id is\n * rejected by the schema before the orchestrator is called).\n */\nexport function registerCloseBoard(server: McpServer, orchestrator: Orchestrator): void {\n server.registerTool(\n TOOL_CLOSE_BOARD,\n {\n description:\n 'Close a board by id (graceful PTY drain, then removed from the canvas). ' +\n 'Idempotent β closing an already-gone board succeeds.',\n inputSchema: { id: z.string().min(1) }\n },\n async (args) => {\n await orchestrator.closeBoard(args.id)\n return { content: [{ type: 'text', text: `closed ${args.id}` }] }\n }\n )\n}\n","import { z } from 'zod'\nimport type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport type { BoardConfig, Orchestrator } from '../../orchestrator/Orchestrator'\nimport { TOOL_CONFIGURE_BOARD } from '../../constants'\n\n/**\n * Register the `configure_board` lifecycle tool (T3.3) β a WRITE tool, orchestrator\n * tier only (gated by the CALLER). Changes a board's durable per-type config\n * (shell / launchCommand / cwd). `id` is required; at least one config field must be\n * present (an empty change is rejected before the orchestrator is called). The host\n * additionally filters the patch to the board type's patchable keys.\n */\nexport function registerConfigureBoard(server: McpServer, orchestrator: Orchestrator): void {\n server.registerTool(\n TOOL_CONFIGURE_BOARD,\n {\n description:\n 'Change a board config by id. At least one of shell | launchCommand | cwd is required. ' +\n 'Applies to the board type that owns the key (shell/launchCommand/cwd are terminal config).',\n inputSchema: {\n id: z.string().min(1),\n shell: z.string().optional(),\n launchCommand: z.string().optional(),\n cwd: z.string().optional()\n }\n },\n async (args) => {\n const config: BoardConfig = {}\n if (args.shell !== undefined) config.shell = args.shell\n if (args.launchCommand !== undefined) config.launchCommand = args.launchCommand\n if (args.cwd !== undefined) config.cwd = args.cwd\n if (Object.keys(config).length === 0) {\n return {\n isError: true,\n content: [\n { type: 'text', text: 'configure_board: at least one of shell/launchCommand/cwd required' }\n ]\n }\n }\n await orchestrator.configureBoard(args.id, config)\n return { content: [{ type: 'text', text: `configured ${args.id}` }] }\n }\n )\n}\n","import { z } from 'zod'\nimport type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport type { Orchestrator } from '../../orchestrator/Orchestrator'\nimport { TOOL_HANDOFF_PROMPT } from '../../constants'\nimport { dispatchPromptSchema } from './promptSchema'\n\n/**\n * Register the `handoff_prompt` DISPATCH tool (T4.3) β the first tool that writes into\n * another agent's PTY. The CALLER (ServerFactory) gates it to the orchestrator tier by\n * only invoking `registerHandoffPrompt` for that tier; a worker's `tools/list` never\n * contains it (the capability split is structural, never a per-handler check).\n *\n * π The host (Canvas ADE MAIN) owns the real safety: it resolves the OPAQUE board id\n * (never a label), rejects any non-terminal target before any write, mints a single-use\n * nonce, BLOCKS on a mandatory human confirm, and audits the action. This tool is the\n * thin transport: validate non-empty inputs, forward to the orchestrator, and surface\n * the returned {@link BoardResult} as text. Blocking β it resolves only after the target\n * terminal goes idle.\n */\nexport function registerHandoffPrompt(server: McpServer, orchestrator: Orchestrator): void {\n server.registerTool(\n TOOL_HANDOFF_PROMPT,\n {\n description:\n 'Hand off a prompt to a target terminal board by id: write it into that board ' +\n 'and block until the board goes idle, then return its structured last result. ' +\n 'Terminal targets only; requires human confirmation. boardId + prompt are required.',\n inputSchema: {\n boardId: z.string().min(1),\n prompt: dispatchPromptSchema\n }\n },\n async (args) => {\n const result = await orchestrator.handoffPrompt(args.boardId, args.prompt)\n return { content: [{ type: 'text', text: JSON.stringify(result) }] }\n }\n )\n}\n","import { z } from 'zod'\n\n/**\n * True if `s` contains any character that must never reach a target board's PTY\n * through a dispatch payload: any C0 control char EXCEPT TAB (0x09), plus DEL\n * (0x7f). This blocks CR (0x0d) and LF (0x0a) β which would let one human-approved\n * \"line\" smuggle extra commands β and Ctrl-C (0x03), ESC (0x1b), and other\n * terminal control bytes.\n */\nfunction hasControlChar(s: string): boolean {\n for (let i = 0; i < s.length; i++) {\n const c = s.charCodeAt(i)\n if (c === 0x09) continue // TAB is allowed\n if (c < 0x20 || c === 0x7f) return true\n }\n return false\n}\n\n/**\n * A non-empty dispatch prompt with no embedded control characters. The host\n * (Canvas ADE MAIN) is the real gate β it strips/rejects these before any PTY\n * write β but this transport-layer refinement is defence in depth: a hostile or\n * malformed payload is rejected at the tool schema, never forwarded to the\n * orchestrator. Shared by assign_prompt / handoff_prompt / relay_prompt.\n */\nexport const dispatchPromptSchema = z\n .string()\n .min(1)\n .refine((s) => !hasControlChar(s), {\n message: 'prompt must not contain control characters (CR/LF/C0/DEL)'\n })\n","import { z } from 'zod'\nimport type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport type { Orchestrator } from '../../orchestrator/Orchestrator'\nimport { TOOL_ASSIGN_PROMPT } from '../../constants'\nimport { dispatchPromptSchema } from './promptSchema'\n\n/**\n * Register the `assign_prompt` DISPATCH tool (T4.4) β the FIRE-AND-FORGET sibling of\n * `handoff_prompt`. The CALLER (ServerFactory) gates it to the orchestrator tier by only\n * invoking `registerAssignPrompt` for that tier; a worker's `tools/list` never contains\n * it (the capability split is structural, never a per-handler check).\n *\n * π The host (Canvas ADE MAIN) owns the real safety, identical to handoff_prompt: it\n * resolves the OPAQUE board id (never a label), rejects any non-terminal target before\n * any write, mints a single-use nonce, BLOCKS on a mandatory human confirm, and audits\n * the action. The ONLY difference from handoff is that this returns the moment the write\n * lands β it does NOT block on await-idle or return a structured result. This tool is the\n * thin transport: validate non-empty inputs, forward to {@link Orchestrator.dispatchPrompt},\n * and surface a short ack.\n */\nexport function registerAssignPrompt(server: McpServer, orchestrator: Orchestrator): void {\n server.registerTool(\n TOOL_ASSIGN_PROMPT,\n {\n description:\n 'Assign a prompt to a target terminal board by id: write it into that board ' +\n 'and return immediately (fire-and-forget β no waiting for the board to finish). ' +\n 'Terminal targets only; requires human confirmation. boardId + prompt are required.',\n inputSchema: {\n boardId: z.string().min(1),\n prompt: dispatchPromptSchema\n }\n },\n async (args) => {\n await orchestrator.dispatchPrompt(args.boardId, args.prompt)\n return { content: [{ type: 'text', text: `assigned prompt to ${args.boardId}` }] }\n }\n )\n}\n","import { z } from 'zod'\nimport type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport type { Orchestrator } from '../../orchestrator/Orchestrator'\nimport type { SessionCtx } from '../factory'\nimport { TOOL_WRITE_RESULT } from '../../constants'\n\n/**\n * Register the `write_result` tool (T4.4) β the FIRST worker-tier WRITE tool. A worker\n * records its OWN board's structured result (verdict + summary + references), which feeds\n * the `canvas://board/{id}/result` resource (T1.5). Registered for BOTH tiers (the caller\n * β ServerFactory β invokes this OUTSIDE the orchestrator-only block).\n *\n * π The target board is BOUND to the caller's `ctx.boardId` (derived from the verified\n * token), and there is NO client-supplied boardId input β so a worker can only write its\n * OWN result and can never forge another board's. Unlike the dispatch tools this performs\n * no PTY write and needs no human confirm: the agent is reporting its own outcome, not\n * dispatching into another shell.\n */\nexport function registerWriteResult(\n server: McpServer,\n orchestrator: Orchestrator,\n ctx: SessionCtx\n): void {\n server.registerTool(\n TOOL_WRITE_RESULT,\n {\n description:\n \"Record THIS board's structured last result (status / summary / references). \" +\n 'All fields optional. Writes only the calling board (no target id is accepted).',\n inputSchema: {\n status: z.string().optional(),\n summary: z.string().optional(),\n refs: z.array(z.string()).optional()\n }\n },\n async (args) => {\n // π ctx.boardId is derived from the token (no client input). If the token\n // carried no boardId it falls back to '' β writing to board '' would silently\n // no-op or hit a sentinel while the agent believes its result was recorded.\n // Refuse loudly instead of writing to an unbound board.\n if (!ctx.boardId) {\n return {\n isError: true,\n content: [{ type: 'text', text: 'write_result: no board bound to this session' }]\n }\n }\n await orchestrator.writeResult(ctx.boardId, {\n status: args.status,\n summary: args.summary,\n refs: args.refs\n })\n return { content: [{ type: 'text', text: 'result recorded' }] }\n }\n )\n}\n","import { z } from 'zod'\nimport type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport type { Orchestrator } from '../../orchestrator/Orchestrator'\nimport { TOOL_INTERRUPT } from '../../constants'\n\n/**\n * Register the `interrupt` DISPATCH tool (T4.5) β send Ctrl-C to a target terminal's PTY\n * to stop a runaway/long-running command. The CALLER (ServerFactory) gates it to the\n * orchestrator tier by only invoking `registerInterrupt` for that tier; a worker's\n * `tools/list` never contains it (the capability split is structural).\n *\n * π The host (Canvas ADE MAIN) owns the real safety, identical to assign_prompt: resolve\n * the OPAQUE board id (never a label), reject any non-terminal target before any write,\n * mint a single-use nonce, BLOCK on a mandatory human confirm, and audit. Content-less β\n * the only input is the target id. This tool is the thin transport: validate, forward to\n * {@link Orchestrator.interrupt}, and surface a short ack.\n */\nexport function registerInterrupt(server: McpServer, orchestrator: Orchestrator): void {\n server.registerTool(\n TOOL_INTERRUPT,\n {\n description:\n 'Interrupt a target terminal board by id: send Ctrl-C to stop its running ' +\n 'command. Terminal targets only; requires human confirmation. boardId is required.',\n inputSchema: {\n boardId: z.string().min(1)\n }\n },\n async (args) => {\n await orchestrator.interrupt(args.boardId)\n return { content: [{ type: 'text', text: `interrupted ${args.boardId}` }] }\n }\n )\n}\n","import { z } from 'zod'\nimport type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport type { Orchestrator } from '../../orchestrator/Orchestrator'\nimport type { BoardId } from '../../types'\nimport type { SessionCtx } from '../factory'\nimport { TOOL_RELAY_PROMPT } from '../../constants'\nimport { dispatchPromptSchema } from './promptSchema'\n\n/**\n * Register the `relay_prompt` agent-to-agent DISPATCH tool (T4.6, the M4 gate). A dispatch\n * from board A (`sourceId`) to board B (`targetId`) is authorized by an ORCHESTRATION\n * connector AβB β the spatial cable IS the route. The CALLER (ServerFactory) gates it to\n * the orchestrator tier (a worker's `tools/list` never contains it).\n *\n * π The host (Canvas ADE MAIN) owns the real safety: it validates the directed\n * orchestration edge `sourceId β targetId` exists and both ends are terminals\n * (terminal β terminal only, one-directional, never Browser β PTY), mints a single-use\n * nonce, BLOCKS on a mandatory human confirm, and audits β then writes into the target's\n * PTY. This tool is the thin transport: validate non-empty inputs, forward to\n * {@link Orchestrator.relayPrompt}, surface a short ack.\n *\n * π Caller-identity binding (BUG-021 part 2): `sourceId` is caller-supplied and the cable\n * (not the caller) is the only authorization the host checks, so ANY orchestrator-tier token\n * could exploit ANY existing cable β fine while exactly one orchestrator token exists, a hole\n * the moment multiple are minted. When the host designates a single command board via\n * `commandBoardId`, this tool restricts `relay_prompt` to that one token-bound identity (the\n * `ctx.boardId` is derived from the verified bearer token, never client input β same trust\n * basis as `write_result`'s board binding). Left `undefined`, behaviour is unchanged (open to\n * any orchestrator-tier caller) so existing single-token deployments keep working.\n */\nexport function registerRelayPrompt(\n server: McpServer,\n orchestrator: Orchestrator,\n ctx: SessionCtx,\n commandBoardId?: BoardId\n): void {\n server.registerTool(\n TOOL_RELAY_PROMPT,\n {\n description:\n 'Relay a prompt from one terminal board to another along an orchestration ' +\n 'connector (sourceId β targetId): the cable must already exist and both boards ' +\n 'must be terminals. Requires human confirmation. sourceId, targetId, prompt required.',\n inputSchema: {\n sourceId: z.string().min(1),\n targetId: z.string().min(1),\n prompt: dispatchPromptSchema\n }\n },\n async (args) => {\n // π BUG-021: when a command board is designated, only that token-bound identity may\n // relay β a second orchestrator token (bound to another board) can't exploit a cable\n // it doesn't own. ctx.boardId comes from the verified token, so it can't be forged.\n if (commandBoardId !== undefined && ctx.boardId !== commandBoardId) {\n return {\n isError: true,\n content: [\n { type: 'text', text: 'relay_prompt: caller is not the designated command orchestrator' }\n ]\n }\n }\n await orchestrator.relayPrompt(args.sourceId, args.targetId, args.prompt)\n return { content: [{ type: 'text', text: `relayed ${args.sourceId} β ${args.targetId}` }] }\n }\n )\n}\n","import { z } from 'zod'\nimport type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport type { Orchestrator } from '../../orchestrator/Orchestrator'\nimport { DEFAULT_BARRIER_TIMEOUT_MS, TOOL_WAIT_FOR_ALL, TOOL_WAIT_FOR_IDLE } from '../../constants'\nimport { waitForBoards, type BarrierBoardResult } from '../barrierWaiter'\n\n/**\n * Resolve the effective backstop: an explicit per-call `timeoutMs` wins (β€ 0 / non-finite\n * opts out, handled downstream by waitForBoards), else the validated env override, else the\n * 30-min default. (env validation mirrors BUG-023: reject non-positive / non-finite.)\n */\nexport function resolveBarrierTimeout(arg?: number): number {\n if (arg !== undefined) return arg\n const env = process.env.CANVAS_ADE_BARRIER_TIMEOUT_MS\n if (env !== undefined) {\n const n = Number(env)\n if (Number.isFinite(n) && n > 0) return n\n }\n return DEFAULT_BARRIER_TIMEOUT_MS\n}\n\n/**\n * Register the M5 BARRIER tools β orchestrator-tier blocking waits over the host status\n * stream. The CALLER (ServerFactory) gates them to the orchestrator tier (registered only in\n * that block); a worker's tools/list never contains them (structural split). READ-ONLY: no\n * PTY write, no human confirm, no audit (those are dispatch-tool concerns). Returns a\n * `dispose()` that cancels any in-flight waits (called on session close to avoid a leaked\n * orchestrator subscription).\n */\nexport function registerBarrierTools(server: McpServer, orchestrator: Orchestrator): () => void {\n const active = new Set<() => void>()\n\n const run = async (targets: string[], timeoutMs: number): Promise<BarrierBoardResult[]> => {\n const handle = waitForBoards({ orchestrator, targets, timeoutMs })\n active.add(handle.cancel)\n try {\n return await handle.promise\n } finally {\n active.delete(handle.cancel)\n }\n }\n\n server.registerTool(\n TOOL_WAIT_FOR_IDLE,\n {\n description:\n 'Block until a target board leaves the running state, then report how it settled ' +\n '(idle/awaiting-review/blocked/failed/static/gone, or timed-out). Returns the board ' +\n \"id + status (+ the board's last write_result when idle). boardId is required; \" +\n 'optional timeoutMs (omit for the default backstop; <=0 to wait indefinitely).',\n inputSchema: {\n boardId: z.string().min(1),\n timeoutMs: z.number().optional()\n }\n },\n async (args) => {\n const results = await run([args.boardId], resolveBarrierTimeout(args.timeoutMs))\n const r = results[0] ?? { id: args.boardId, status: 'timed-out' }\n return { content: [{ type: 'text', text: JSON.stringify(r) }] }\n }\n )\n\n server.registerTool(\n TOOL_WAIT_FOR_ALL,\n {\n description:\n 'Block until EVERY target board has left the running state, then report each one ' +\n '(same statuses as wait_for_idle) plus allIdle (true when every target settled to ' +\n 'idle). boardIds is a non-empty array; optional timeoutMs (omit for the default ' +\n 'backstop; <=0 to wait indefinitely).',\n inputSchema: {\n boardIds: z.array(z.string().min(1)).min(1),\n timeoutMs: z.number().optional()\n }\n },\n async (args) => {\n const boards = await run(args.boardIds, resolveBarrierTimeout(args.timeoutMs))\n const allIdle = boards.every((b) => b.status === 'idle')\n return { content: [{ type: 'text', text: JSON.stringify({ boards, allIdle }) }] }\n }\n )\n\n return () => {\n for (const cancel of active) cancel()\n }\n}\n","import type { BoardResult, BoardStatusChange, Orchestrator } from '../orchestrator/Orchestrator'\n\n/** One target's settled outcome. `status` is a bucket, `'gone'`, or `'timed-out'`. */\nexport interface BarrierBoardResult {\n id: string\n status: string\n result?: BoardResult\n}\n\n/** Settled = anything that is not actively `running`. */\nconst isSettled = (status: string): boolean => status !== 'running'\n\nexport interface BarrierHandle {\n promise: Promise<BarrierBoardResult[]>\n /** Force teardown (session close): unsubscribe + resolve unsettled targets as `gone`. */\n cancel: () => void\n}\n\n/**\n * Wait until every `targets` board has left `running`, event-driven off\n * `orchestrator.subscribeStatus` β never a poll. Level-triggered: an already-settled (or\n * absent β `gone`) target resolves on the initial read with no edge needed. On the backstop\n * deadline, unsettled targets resolve `timed-out` (the promise NEVER rejects β settle-and-report).\n * A `timeoutMs` β€ 0 or non-finite opts out of the backstop. Output preserves `targets` order.\n */\nexport function waitForBoards(opts: {\n orchestrator: Pick<Orchestrator, 'listBoards' | 'subscribeStatus' | 'boardResult'>\n targets: string[]\n timeoutMs: number\n}): BarrierHandle {\n const { orchestrator, targets, timeoutMs } = opts\n const order = targets.slice()\n const pending = new Set(targets)\n const settled = new Map<string, BarrierBoardResult>()\n let done = false\n let unsub: () => void = () => {}\n let timer: ReturnType<typeof setTimeout> | undefined\n let resolveFn!: (r: BarrierBoardResult[]) => void\n const promise = new Promise<BarrierBoardResult[]>((resolve) => {\n resolveFn = resolve\n })\n\n const finish = (fillStatus: string): void => {\n if (done) return\n done = true\n unsub()\n if (timer) clearTimeout(timer)\n resolveFn(order.map((id) => settled.get(id) ?? { id, status: fillStatus }))\n }\n\n const recordSettle = async (id: string, status: string): Promise<void> => {\n if (done || !pending.has(id)) return\n let entry: BarrierBoardResult = { id, status }\n if (status === 'idle') {\n const r = await orchestrator.boardResult(id)\n if (r.present) entry = { id, status, result: r }\n }\n if (done || !pending.has(id)) return // a concurrent finish/duplicate edge won the race\n settled.set(id, entry)\n pending.delete(id)\n if (pending.size === 0) finish('timed-out')\n }\n\n // Subscribe FIRST so no edge between the initial read and subscription is missed.\n unsub = orchestrator.subscribeStatus((change: BoardStatusChange) => {\n if (isSettled(change.status)) void recordSettle(change.id, change.status)\n })\n\n // Level-trigger: read current state once; settle already-settled / absent targets.\n void (async () => {\n const boards = await orchestrator.listBoards()\n if (done) return\n const current = new Map(boards.map((b) => [b.id, b.status]))\n for (const id of order) {\n if (done) return\n const st = current.get(id)\n if (st === undefined) await recordSettle(id, 'gone')\n else if (isSettled(st)) await recordSettle(id, st)\n }\n })()\n\n if (Number.isFinite(timeoutMs) && timeoutMs > 0) {\n timer = setTimeout(() => finish('timed-out'), timeoutMs)\n // Don't let the backstop hold the event loop open (best-effort; not present in all envs).\n ;(timer as { unref?: () => void }).unref?.()\n }\n\n return { promise, cancel: () => finish('gone') }\n}\n","import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport {\n ErrorCode,\n McpError,\n SubscribeRequestSchema,\n UnsubscribeRequestSchema\n} from '@modelcontextprotocol/sdk/types.js'\nimport { ATTENTION_URI } from '../resources/attention'\n\nexport interface ResourceSubscriptions {\n /** True when a client has an active `resources/subscribe` for this exact URI. */\n isSubscribed(uri: string): boolean\n}\n\n/**\n * π The ONLY URI the server ever pushes `notifications/resources/updated` for is\n * `canvas://attention` (the AttentionNotifier is its sole producer). A subscribe to any other\n * URI is dead weight AND lets a hostile in-session client grow the per-session Set without\n * bound (unbounded client-controlled allocation in MAIN) β so accept ONLY the allowlist and\n * reject the rest.\n */\nconst SUBSCRIBABLE: ReadonlySet<string> = new Set([ATTENTION_URI])\n\n/**\n * Manually wire `resources/subscribe` / `resources/unsubscribe` for one session β the SDK's\n * high-level McpServer does NOT do this (sdk 1.29.0). Registers the `resources.subscribe`\n * capability + the two request handlers, tracking the subscribed URIs in a per-session Set.\n * The AttentionNotifier consults `isSubscribed` so it only pushes to clients that asked.\n * MUST be called BEFORE `server.connect(transport)` (registerCapabilities is connect-gated).\n */\nexport function installResourceSubscriptions(server: McpServer): ResourceSubscriptions {\n const uris = new Set<string>()\n server.server.registerCapabilities({ resources: { subscribe: true } })\n server.server.setRequestHandler(SubscribeRequestSchema, async (req) => {\n const { uri } = req.params\n // π Bound the per-session Set to the allowlist: a subscribe to a URI that never updates\n // fails loudly (InvalidParams) instead of silently never firing + allocating forever.\n if (!SUBSCRIBABLE.has(uri)) {\n throw new McpError(ErrorCode.InvalidParams, `resource is not subscribable: ${uri}`)\n }\n uris.add(uri)\n return {}\n })\n server.server.setRequestHandler(UnsubscribeRequestSchema, async (req) => {\n uris.delete(req.params.uri)\n return {}\n })\n return { isSubscribed: (uri) => uris.has(uri) }\n}\n","import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport type { Orchestrator } from '../orchestrator/Orchestrator'\nimport { ATTENTION_BUCKETS, ATTENTION_URI } from '../resources/attention'\n\nexport interface AttentionNotifier {\n /** Unsubscribe from the orchestrator status stream (called on session close). */\n dispose(): void\n}\n\n/**\n * Per session: push `notifications/resources/updated` on `canvas://attention` whenever the\n * MEMBERSHIP of the attention set changes (a board enters or leaves blocked/awaiting-review/\n * failed). A change WITHIN the set (blockedβfailed) or outside it (runningβidle) emits\n * nothing β the resource membership is unchanged. Gated on a live `resources/subscribe` for\n * the URI; the emit is wrapped so a post-close `sendResourceUpdated` (\"Not connected\") can't\n * throw into the orchestrator fan-out.\n */\nexport function createAttentionNotifier(deps: {\n server: McpServer\n orchestrator: Orchestrator\n isSubscribed: (uri: string) => boolean\n}): AttentionNotifier {\n const { server, orchestrator, isSubscribed } = deps\n const inAttention = new Set<string>()\n\n const unsub = orchestrator.subscribeStatus((change) => {\n const nowAttn = ATTENTION_BUCKETS.has(change.status)\n const wasAttn = inAttention.has(change.id)\n if (nowAttn === wasAttn) return // membership unchanged\n if (nowAttn) inAttention.add(change.id)\n else inAttention.delete(change.id)\n if (!isSubscribed(ATTENTION_URI)) return\n try {\n server.server.sendResourceUpdated({ uri: ATTENTION_URI })\n } catch {\n // post-close / not-connected emit β drop it; the fan-out must not throw\n }\n })\n\n return { dispose: unsub }\n}\n","import { randomUUID } from 'node:crypto'\nimport type { Request, Response } from 'express'\nimport { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'\nimport { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js'\nimport { HEADER_SESSION_ID } from '../constants'\nimport type { ServerFactory, SessionCtx } from './factory'\n\nfunction rpcError(code: number, message: string): unknown {\n return { jsonrpc: '2.0', error: { code, message }, id: null }\n}\n\n/**\n * π PKG-N1 β does the presenting token own this session? A session's McpServer (its tool set\n * + the boardId bound into write_result/relay_prompt) is frozen at init from the CREATING\n * token's {tier,boardId}. `requireBearerAuth` validates any token globally but binds it to no\n * session, so reuse/GET/DELETE routed purely by Mcp-Session-Id would let any valid token drive\n * someone else's session at the creator's tier (a confused-deputy bypass of the structural tier\n * split). Bind every reuse to the creator's identity: same tier AND same boardId.\n */\nfunction ownsSession(owner: SessionCtx, caller: SessionCtx): boolean {\n return owner.tier === caller.tier && owner.boardId === caller.boardId\n}\n\n/**\n * Owns the per-session transport map and the stateful streamable-HTTP routing.\n * This is the ONLY module importing the SDK transport β isolated so a future SDK\n * v2 bump (which renames the transport) is a one-file change.\n */\nexport class SessionManager {\n private readonly transports = new Map<string, StreamableHTTPServerTransport>()\n /** Per-session teardown (M5 notifier unsubscribe + in-flight barrier cancel). */\n private readonly disposers = new Map<string, () => void>()\n /** π PKG-N1: the {tier,boardId} of the token that CREATED each session (ownership key). */\n private readonly owners = new Map<string, SessionCtx>()\n\n constructor(private readonly factory: ServerFactory) {}\n\n /** POST /mcp: reuse an existing session, or open a new one on initialize. */\n async handlePost(req: Request, res: Response, ctx: SessionCtx): Promise<void> {\n const sid = req.header(HEADER_SESSION_ID)\n\n if (sid !== undefined) {\n const existing = this.transports.get(sid)\n if (!existing) {\n res.status(404).json(rpcError(-32001, 'Session not found'))\n return\n }\n // π PKG-N1: bind reuse to the creating token β a globally-valid bearer from a\n // different board/tier must not drive someone else's session.\n if (!this.isOwner(sid, ctx)) {\n res.status(403).json(rpcError(-32003, 'Forbidden: session belongs to another token'))\n return\n }\n await existing.handleRequest(req, res, req.body)\n return\n }\n\n if (!isInitializeRequest(req.body)) {\n res\n .status(400)\n .json(rpcError(-32000, 'Bad Request: no session ID and not an initialize request'))\n return\n }\n\n const { server, dispose } = this.factory.getServer(ctx)\n const transport = new StreamableHTTPServerTransport({\n sessionIdGenerator: () => randomUUID(),\n onsessioninitialized: (id) => {\n this.transports.set(id, transport)\n this.disposers.set(id, dispose)\n this.owners.set(id, ctx) // π PKG-N1: record the creating token's identity\n }\n })\n transport.onclose = () => {\n const id = transport.sessionId\n if (id !== undefined) {\n this.transports.delete(id)\n this.disposers.get(id)?.()\n this.disposers.delete(id)\n this.owners.delete(id)\n }\n }\n\n await server.connect(transport)\n await transport.handleRequest(req, res, req.body)\n }\n\n /** GET (SSE) and DELETE /mcp: route to the named session, gated on token ownership. */\n async handleSession(req: Request, res: Response, ctx: SessionCtx): Promise<void> {\n const sid = req.header(HEADER_SESSION_ID)\n if (sid === undefined) {\n res.status(400).json(rpcError(-32000, 'Missing session ID'))\n return\n }\n const transport = this.transports.get(sid)\n if (!transport) {\n res.status(404).json(rpcError(-32001, 'Session not found'))\n return\n }\n // π PKG-N1: same ownership gate as POST reuse β the GET-SSE stream + the DELETE teardown\n // must also be refused to a token that did not create the session.\n if (!this.isOwner(sid, ctx)) {\n res.status(403).json(rpcError(-32003, 'Forbidden: session belongs to another token'))\n return\n }\n await transport.handleRequest(req, res)\n }\n\n /** π True iff the presenting token's {tier,boardId} matches the session creator's. */\n private isOwner(sid: string, ctx: SessionCtx): boolean {\n const owner = this.owners.get(sid)\n return owner !== undefined && ownsSession(owner, ctx)\n }\n\n /**\n * Tear down every live session (called on app quit). Uses `allSettled` so one\n * transport whose `close()` rejects can't short-circuit the loop and leak the\n * remaining sessions; the map is always cleared.\n */\n async closeAll(): Promise<void> {\n try {\n await Promise.allSettled([...this.transports.values()].map((t) => t.close()))\n } finally {\n for (const dispose of this.disposers.values()) {\n try {\n dispose()\n } catch {\n // a teardown throw must not abort the rest\n }\n }\n this.disposers.clear()\n this.transports.clear()\n this.owners.clear()\n }\n }\n}\n","import type { AuthRow } from '../types'\n\n/**\n * In-memory per-board token store. Tokens are minted out-of-band when a board\n * spawns and revoked when it closes β no OAuth flow. In-memory by design:\n * sessions die on MAIN restart (correct for a single-user desktop app).\n */\nexport class TokenStore {\n private readonly rows = new Map<string, AuthRow>()\n\n mint(token: string, row: AuthRow): void {\n this.rows.set(token, row)\n }\n\n revoke(token: string): void {\n this.rows.delete(token)\n }\n\n get(token: string): AuthRow | undefined {\n return this.rows.get(token)\n }\n}\n","import { randomBytes } from 'node:crypto'\nimport type { TokenStore } from './tokens'\nimport type { AuthRow, BoardId, Tier } from '../types'\nimport { defaultScopesFor } from './scopes'\n\nexport interface MintedToken {\n token: string\n row: AuthRow\n}\n\n/**\n * Board-lifetime default expiry (~1 year). The SDK's requireBearerAuth REJECTS a\n * token whose `expiresAt` is not a number (\"Token has no expiration time\"), so a\n * minted token MUST carry one β a missing expiry is not a valid \"never expires\".\n * The value is set far out (board lifetime, revoked on close) so it never kills a\n * long agent run mid-session; pass a short `ttlSeconds` only for a deliberately\n * short-lived token. (ADR 0002, D3.)\n */\nconst DEFAULT_TTL_SECONDS = 365 * 24 * 60 * 60\n\n/**\n * Mint a crypto-random per-board bearer token, store it, and return token + row.\n * Scopes come from the tier (ADR 0002, D2). Always carries a board-lifetime\n * `expiresAt` (the SDK requires a numeric expiry); override with `ttlSeconds`.\n */\nexport function mintBoardToken(\n store: TokenStore,\n input: { boardId: BoardId; tier: Tier; ttlSeconds?: number }\n): MintedToken {\n const token = randomBytes(32).toString('hex')\n const ttl = input.ttlSeconds ?? DEFAULT_TTL_SECONDS\n const row: AuthRow = {\n boardId: input.boardId,\n tier: input.tier,\n scopes: defaultScopesFor(input.tier),\n expiresAt: Math.floor(Date.now() / 1000) + ttl\n }\n store.mint(token, row)\n return { token, row }\n}\n","import type { Scope, Tier } from '../types'\n\n// The Phase 1 scope vocabulary (ADR 0002, D2). Scopes are carried by the token\n// and consumed by per-tool gating in Phases 3+. Tier registration in the factory\n// remains the primary capability gate; scopes are the finer-grained future lever.\nexport const SCOPE_READ: Scope = 'read'\nexport const SCOPE_DISPATCH: Scope = 'dispatch'\nexport const SCOPE_SPAWN: Scope = 'spawn'\nexport const SCOPE_GIT_WRITE: Scope = 'git:write'\nexport const SCOPE_ANSWER_PERMISSION: Scope = 'answer_permission'\n\nconst WORKER_SCOPES: readonly Scope[] = [SCOPE_READ]\nconst ORCHESTRATOR_SCOPES: readonly Scope[] = [\n SCOPE_READ,\n SCOPE_DISPATCH,\n SCOPE_SPAWN,\n SCOPE_GIT_WRITE,\n SCOPE_ANSWER_PERMISSION\n]\n\n/** Default scopes granted to a freshly-minted token of the given tier. */\nexport function defaultScopesFor(tier: Tier): Scope[] {\n return [...(tier === 'orchestrator' ? ORCHESTRATOR_SCOPES : WORKER_SCOPES)]\n}\n","import { writeFileSync } from 'node:fs'\nimport { join } from 'node:path'\n\nexport interface McpJson {\n mcpServers: {\n 'canvas-ade': {\n type: 'http'\n url: string\n headers: { Authorization: string }\n }\n }\n}\n\n/**\n * Pure builder for a board worktree's project-scoped .mcp.json: a single loopback\n * streamable-HTTP server entry carrying the board's bearer token. No OAuth\n * discovery is referenced (ADR 0001) β the static bearer token is the authority.\n */\nexport function buildMcpJson(port: number, token: string): McpJson {\n return {\n mcpServers: {\n 'canvas-ade': {\n type: 'http',\n url: `http://127.0.0.1:${port}/mcp`,\n headers: { Authorization: `Bearer ${token}` }\n }\n }\n }\n}\n\n/**\n * Write .mcp.json into a board's worktree dir. Returns the written file path.\n * Written owner-only (0600): the file embeds a plaintext bearer token, so it must\n * not be world-readable on a multi-user box. (POSIX mode is a no-op on Windows.)\n */\nexport function writeMcpJson(dir: string, port: number, token: string): string {\n const file = join(dir, '.mcp.json')\n writeFileSync(file, JSON.stringify(buildMcpJson(port, token), null, 2) + '\\n', {\n encoding: 'utf8',\n mode: 0o600\n })\n return file\n}\n","import type { BoardId } from '../types'\nimport type {\n BoardConfig,\n BoardOutput,\n BoardResult,\n BoardResultInput,\n BoardStatusChange,\n BoardSummary,\n MemoryDoc,\n Orchestrator\n} from './Orchestrator'\n\n/** A no-op Orchestrator for contract tests and standalone runs. */\nexport class MockOrchestrator implements Orchestrator {\n async listBoards(): Promise<BoardSummary[]> {\n return []\n }\n\n async spawnBoard(_input: {\n type: string\n prompt?: string\n cwd?: string\n }): Promise<{ id: BoardId }> {\n return { id: 'mock-board' }\n }\n\n async closeBoard(_boardId: BoardId): Promise<void> {}\n\n async configureBoard(_boardId: BoardId, _config: BoardConfig): Promise<void> {}\n\n async dispatchPrompt(_boardId: BoardId, _text: string): Promise<void> {}\n\n async writeResult(_boardId: BoardId, _result: BoardResultInput): Promise<void> {}\n\n async interrupt(_boardId: BoardId): Promise<void> {}\n\n async relayPrompt(_sourceId: BoardId, _targetId: BoardId, _text: string): Promise<void> {}\n\n async handoffPrompt(_boardId: BoardId, _text: string): Promise<BoardResult> {\n return { present: false }\n }\n\n async gitDiff(_boardId: BoardId): Promise<string> {\n return ''\n }\n\n async boardStatus(_boardId: BoardId): Promise<string> {\n return 'idle'\n }\n\n async boardOutput(_boardId: BoardId, _opts?: { cursor?: number }): Promise<BoardOutput> {\n return { text: '', total: 0, returned: 0, droppedOlder: false }\n }\n\n async boardResult(_boardId: BoardId): Promise<BoardResult> {\n return { present: false }\n }\n\n async projectMemory(): Promise<MemoryDoc> {\n return { present: false, text: '' }\n }\n\n async boardSummary(_boardId: BoardId): Promise<MemoryDoc> {\n return { present: false, text: '' }\n }\n\n /** @internal subscribers for the M5 status stream. */\n private readonly statusListeners = new Set<(change: BoardStatusChange) => void>()\n\n subscribeStatus(listener: (change: BoardStatusChange) => void): () => void {\n this.statusListeners.add(listener)\n return () => {\n this.statusListeners.delete(listener)\n }\n }\n\n /** Test seam: drive a status change through the subscription fan-out. */\n __emitStatus(change: BoardStatusChange): void {\n for (const cb of this.statusListeners) {\n try {\n cb(change)\n } catch {\n // isolate a throwing listener (same discipline as the app-side fan-out)\n }\n }\n }\n}\n"],"mappings":"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","names":["McpServer","ResourceTemplate","ResourceTemplate","ResourceTemplate","ResourceTemplate","z","z","z","z","z","z","z","z","z","z","z","z","z","z","z","z","z","McpServer"]}
|
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@expanse-ade/mcp",
|
|
3
|
-
"version": "0.9.
|
|
4
|
-
"
|
|
3
|
+
"version": "0.9.1",
|
|
4
|
+
"packageManager": "pnpm@9.15.9",
|
|
5
|
+
"description": "MCP server layer for Canvas ADE \u00e2\u20ac\u201d lets AI agents in Terminal boards orchestrate the canvas (command board / swarm).",
|
|
5
6
|
"type": "module",
|
|
6
7
|
"repository": {
|
|
7
8
|
"type": "git",
|
|
@@ -24,6 +25,15 @@
|
|
|
24
25
|
"engines": {
|
|
25
26
|
"node": ">=20"
|
|
26
27
|
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "tsup",
|
|
30
|
+
"typecheck": "tsc --noEmit",
|
|
31
|
+
"test": "vitest run --project contract",
|
|
32
|
+
"test:live": "vitest run --project live",
|
|
33
|
+
"lint": "eslint .",
|
|
34
|
+
"format": "prettier --write .",
|
|
35
|
+
"format:check": "prettier --check ."
|
|
36
|
+
},
|
|
27
37
|
"license": "MIT",
|
|
28
38
|
"dependencies": {
|
|
29
39
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
@@ -42,14 +52,5 @@
|
|
|
42
52
|
"typescript": "^6.0.3",
|
|
43
53
|
"typescript-eslint": "^8.60.0",
|
|
44
54
|
"vitest": "^4.1.7"
|
|
45
|
-
},
|
|
46
|
-
"scripts": {
|
|
47
|
-
"build": "tsup",
|
|
48
|
-
"typecheck": "tsc --noEmit",
|
|
49
|
-
"test": "vitest run --project contract",
|
|
50
|
-
"test:live": "vitest run --project live",
|
|
51
|
-
"lint": "eslint .",
|
|
52
|
-
"format": "prettier --write .",
|
|
53
|
-
"format:check": "prettier --check ."
|
|
54
55
|
}
|
|
55
|
-
}
|
|
56
|
+
}
|