feat(relay): expand to dev-c role + python/ts MCP fallback shims

queue.ts and server.ts now know about dev-c alongside pm/dev-a/dev-b
so the four-role coordination paradigm works end-to-end. start.sh
opens a fourth window for dev-c. call.py and call.ts are HTTP shims
that agents can use when the MCP relay tools aren't registered in
their session (the kickoff prompts reference call.py by path as a
fallback).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
adlee-was-taken
2026-05-05 17:49:21 -04:00
parent 29146439bb
commit dd0010db62
5 changed files with 152 additions and 41 deletions

View File

@@ -10,11 +10,6 @@ import { RelayQueue, isRole } from "./queue.ts";
const PORT = 7331;
const queue = new RelayQueue();
const mcpServer = new Server(
{ name: "relay", version: "0.1.0" },
{ capabilities: { tools: {} } }
);
const TOOLS = [
{
name: "post_message",
@@ -25,12 +20,12 @@ const TOOLS = [
properties: {
from: {
type: "string",
enum: ["pm", "dev-a", "dev-b"],
enum: ["pm", "dev-a", "dev-b", "dev-c"],
description: "Your role name",
},
to: {
type: "string",
enum: ["pm", "dev-a", "dev-b"],
enum: ["pm", "dev-a", "dev-b", "dev-c"],
description: "Recipient role name",
},
kind: {
@@ -55,7 +50,7 @@ const TOOLS = [
properties: {
for: {
type: "string",
enum: ["pm", "dev-a", "dev-b"],
enum: ["pm", "dev-a", "dev-b", "dev-c"],
description: "Your role name",
},
},
@@ -71,7 +66,7 @@ const TOOLS = [
properties: {
for: {
type: "string",
enum: ["pm", "dev-a", "dev-b"],
enum: ["pm", "dev-a", "dev-b", "dev-c"],
description: "Your role name",
},
},
@@ -80,41 +75,36 @@ const TOOLS = [
},
];
mcpServer.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
const a = args as Record<string, string>;
function handleToolCall(name: string, args: Record<string, string>) {
if (name === "post_message") {
if (!isRole(a.from)) {
return { content: [{ type: "text" as const, text: `Error: unknown role "${a.from}"` }], isError: true };
if (!isRole(args.from)) {
return { content: [{ type: "text" as const, text: `Error: unknown role "${args.from}"` }], isError: true };
}
if (!isRole(a.to)) {
return { content: [{ type: "text" as const, text: `Error: unknown role "${a.to}"` }], isError: true };
if (!isRole(args.to)) {
return { content: [{ type: "text" as const, text: `Error: unknown role "${args.to}"` }], isError: true };
}
const kind = a.kind as "status" | "question" | "directive" | "free";
const msg = queue.post(a.from, a.to, kind, a.body);
const kind = args.kind as "status" | "question" | "directive" | "free";
const msg = queue.post(args.from, args.to, kind, args.body);
const ts = new Date(msg.ts).toTimeString().slice(0, 8);
const preview = a.body.slice(0, 60).replace(/\n/g, " ");
const ellipsis = a.body.length > 60 ? "..." : "";
process.stdout.write(`[${ts}] ${a.from}${a.to} [${kind}] "${preview}${ellipsis}"\n`);
const preview = args.body.slice(0, 60).replace(/\n/g, " ");
const ellipsis = args.body.length > 60 ? "..." : "";
process.stdout.write(`[${ts}] ${args.from}${args.to} [${kind}] "${preview}${ellipsis}"\n`);
return { content: [{ type: "text" as const, text: JSON.stringify({ id: msg.id }) }] };
}
if (name === "read_messages") {
if (!isRole(a.for)) {
return { content: [{ type: "text" as const, text: `Error: unknown role "${a.for}"` }], isError: true };
if (!isRole(args.for)) {
return { content: [{ type: "text" as const, text: `Error: unknown role "${args.for}"` }], isError: true };
}
const messages = queue.read(a.for);
const messages = queue.read(args.for);
return { content: [{ type: "text" as const, text: JSON.stringify(messages) }] };
}
if (name === "list_pending") {
if (!isRole(a.for)) {
return { content: [{ type: "text" as const, text: `Error: unknown role "${a.for}"` }], isError: true };
if (!isRole(args.for)) {
return { content: [{ type: "text" as const, text: `Error: unknown role "${args.for}"` }], isError: true };
}
const result = queue.pending(a.for);
const result = queue.pending(args.for);
return { content: [{ type: "text" as const, text: JSON.stringify(result) }] };
}
@@ -122,7 +112,20 @@ mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => {
content: [{ type: "text" as const, text: `Error: unknown tool "${name}"` }],
isError: true,
};
});
}
function makeServer() {
const srv = new Server(
{ name: "relay", version: "0.1.0" },
{ capabilities: { tools: {} } }
);
srv.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
srv.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
return handleToolCall(name, args as Record<string, string>);
});
return srv;
}
const transports = new Map<string, SSEServerTransport>();
@@ -132,7 +135,8 @@ const httpServer = http.createServer(async (req, res) => {
const transport = new SSEServerTransport("/message", res);
transports.set(transport.sessionId, transport);
transport.onclose = () => transports.delete(transport.sessionId);
await mcpServer.connect(transport);
// Each connection gets its own Server instance so multiple clients can coexist.
await makeServer().connect(transport);
} else if (req.method === "POST" && req.url?.startsWith("/message")) {
const url = new URL(req.url, `http://127.0.0.1:${PORT}`);
const sessionId = url.searchParams.get("sessionId") ?? "";