@stdiobus/workers-registry 1.3.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +932 -0
- package/out/dist/workers/acp-registry/registry-launcher-client.js +52 -0
- package/out/dist/workers/acp-registry/registry-launcher-client.js.map +7 -0
- package/out/dist/workers/acp-registry/registry-launcher-config.json +31 -0
- package/out/dist/workers/acp-worker/index.js +5 -0
- package/out/dist/workers/acp-worker/index.js.map +7 -0
- package/out/dist/workers/echo-worker/echo-worker-config.json +21 -0
- package/out/dist/workers/echo-worker/echo-worker.js +3 -0
- package/out/dist/workers/echo-worker/echo-worker.js.map +7 -0
- package/out/dist/workers/index.d.ts +15 -0
- package/out/dist/workers/index.js +33 -0
- package/out/dist/workers/mcp-echo-server/index.js +3 -0
- package/out/dist/workers/mcp-echo-server/index.js.map +7 -0
- package/out/dist/workers/mcp-echo-server/mcp-echo-server-config.json +21 -0
- package/out/dist/workers/mcp-to-acp-proxy/proxy.js +3 -0
- package/out/dist/workers/mcp-to-acp-proxy/proxy.js.map +7 -0
- package/out/tsc/workers/acp-worker/src/acp/client-capabilities.d.ts +131 -0
- package/out/tsc/workers/acp-worker/src/acp/content-mapper.d.ts +95 -0
- package/out/tsc/workers/acp-worker/src/acp/index.d.ts +13 -0
- package/out/tsc/workers/acp-worker/src/acp/tools.d.ts +119 -0
- package/out/tsc/workers/acp-worker/src/agent.d.ts +113 -0
- package/out/tsc/workers/acp-worker/src/index.d.ts +1 -0
- package/out/tsc/workers/acp-worker/src/mcp/connection.d.ts +54 -0
- package/out/tsc/workers/acp-worker/src/mcp/index.d.ts +10 -0
- package/out/tsc/workers/acp-worker/src/mcp/manager.d.ts +178 -0
- package/out/tsc/workers/acp-worker/src/mcp/types.d.ts +114 -0
- package/out/tsc/workers/acp-worker/src/mcp-proxy/connection.d.ts +80 -0
- package/out/tsc/workers/acp-worker/src/mcp-proxy/converter.d.ts +156 -0
- package/out/tsc/workers/acp-worker/src/mcp-proxy/index.d.ts +2 -0
- package/out/tsc/workers/acp-worker/src/mcp-proxy/state.d.ts +165 -0
- package/out/tsc/workers/acp-worker/src/mcp-proxy/types.d.ts +163 -0
- package/out/tsc/workers/acp-worker/src/registry-launcher/config/api-keys.d.ts +41 -0
- package/out/tsc/workers/acp-worker/src/registry-launcher/config/config.d.ts +17 -0
- package/out/tsc/workers/acp-worker/src/registry-launcher/config/index.d.ts +10 -0
- package/out/tsc/workers/acp-worker/src/registry-launcher/config/types.d.ts +15 -0
- package/out/tsc/workers/acp-worker/src/registry-launcher/index.d.ts +2 -0
- package/out/tsc/workers/acp-worker/src/registry-launcher/log.d.ts +77 -0
- package/out/tsc/workers/acp-worker/src/registry-launcher/registry/index.d.ts +109 -0
- package/out/tsc/workers/acp-worker/src/registry-launcher/registry/resolver.d.ts +67 -0
- package/out/tsc/workers/acp-worker/src/registry-launcher/registry/types.d.ts +105 -0
- package/out/tsc/workers/acp-worker/src/registry-launcher/router/index.d.ts +8 -0
- package/out/tsc/workers/acp-worker/src/registry-launcher/router/message-router.d.ts +150 -0
- package/out/tsc/workers/acp-worker/src/registry-launcher/runtime/agent-runtime.d.ts +58 -0
- package/out/tsc/workers/acp-worker/src/registry-launcher/runtime/index.d.ts +11 -0
- package/out/tsc/workers/acp-worker/src/registry-launcher/runtime/manager.d.ts +82 -0
- package/out/tsc/workers/acp-worker/src/registry-launcher/runtime/types.d.ts +20 -0
- package/out/tsc/workers/acp-worker/src/registry-launcher/stream/index.d.ts +8 -0
- package/out/tsc/workers/acp-worker/src/registry-launcher/stream/ndjson-handler.d.ts +119 -0
- package/out/tsc/workers/acp-worker/src/registry-launcher/test-utils/index.d.ts +234 -0
- package/out/tsc/workers/acp-worker/src/session/index.d.ts +12 -0
- package/out/tsc/workers/acp-worker/src/session/manager.d.ts +63 -0
- package/out/tsc/workers/acp-worker/src/session/session.d.ts +86 -0
- package/out/tsc/workers/acp-worker/src/session/types.d.ts +33 -0
- package/out/tsc/workers/acp-worker/src/test-utils/test-harness.d.ts +80 -0
- package/out/tsc/workers/mcp-echo-server/mcp-echo-server.d.ts +2 -0
- package/package.json +77 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import net from"net";import readline from"readline";import crypto from"crypto";function parseArgs(){const args=process.argv.slice(2);const options={tcp:null,unix:null,agentId:null,flow:null,sessionId:null,prompt:"Hello, agent!",interactive:false,timeout:3e4,help:false};for(let i=0;i<args.length;i++){const arg=args[i];switch(arg){case"--tcp":options.tcp=args[++i];break;case"--unix":options.unix=args[++i];break;case"--agent":options.agentId=args[++i];break;case"--flow":options.flow=args[++i];break;case"--session":options.sessionId=args[++i];break;case"--prompt":options.prompt=args[++i];break;case"--interactive":options.interactive=true;break;case"--timeout":options.timeout=parseInt(args[++i],10);break;case"--help":case"-h":options.help=true;break;default:console.error(`Unknown option: ${arg}`);process.exit(1)}}return options}function showHelp(){console.log(`
|
|
3
|
+
ACP Registry Transit Test Client
|
|
4
|
+
|
|
5
|
+
This client demonstrates sending ACP messages through the Registry Launcher
|
|
6
|
+
transit chain: Client \u2192 stdio Bus \u2192 Registry Launcher \u2192 ACP Agent \u2192 back
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
node registry-launcher-client.js --tcp <host:port> --agent <id> [options]
|
|
10
|
+
node registry-launcher-client.js --unix <path> --agent <id> [options]
|
|
11
|
+
|
|
12
|
+
Connection (one required):
|
|
13
|
+
--tcp <host:port> Connect via TCP (e.g., localhost:9000)
|
|
14
|
+
--unix <path> Connect via Unix socket (e.g., /tmp/stdio_bus.sock)
|
|
15
|
+
|
|
16
|
+
Required:
|
|
17
|
+
--agent <id> Agent ID from ACP Registry to route messages to
|
|
18
|
+
|
|
19
|
+
ACP Flow Options:
|
|
20
|
+
--flow <type> ACP flow to execute:
|
|
21
|
+
initialize - Send initialize request
|
|
22
|
+
session-new - Create new session
|
|
23
|
+
session-prompt - Send prompt to session
|
|
24
|
+
full - Run full flow (init \u2192 new \u2192 prompt)
|
|
25
|
+
--session <id> Session ID for session/prompt (auto-generated if not set)
|
|
26
|
+
--prompt <text> Prompt text for session/prompt (default: "Hello, agent!")
|
|
27
|
+
|
|
28
|
+
Modes:
|
|
29
|
+
--interactive Read JSON from stdin, auto-add agentId to messages
|
|
30
|
+
--timeout <ms> Response timeout in ms (default: 30000)
|
|
31
|
+
|
|
32
|
+
Other:
|
|
33
|
+
--help, -h Show this help message
|
|
34
|
+
|
|
35
|
+
Examples:
|
|
36
|
+
# Run full ACP flow with an agent
|
|
37
|
+
node registry-launcher-client.js --tcp localhost:9000 --agent my-agent --flow full
|
|
38
|
+
|
|
39
|
+
# Send just an initialize request
|
|
40
|
+
node registry-launcher-client.js --tcp localhost:9000 --agent my-agent --flow initialize
|
|
41
|
+
|
|
42
|
+
# Create a new session
|
|
43
|
+
node registry-launcher-client.js --tcp localhost:9000 --agent my-agent --flow session-new
|
|
44
|
+
|
|
45
|
+
# Send a prompt to an existing session
|
|
46
|
+
node registry-launcher-client.js --tcp localhost:9000 --agent my-agent --flow session-prompt --session sess-123
|
|
47
|
+
|
|
48
|
+
# Interactive mode - type JSON messages, agentId added automatically
|
|
49
|
+
node registry-launcher-client.js --tcp localhost:9000 --agent my-agent --interactive
|
|
50
|
+
`)}function generateId(prefix="req"){return`${prefix}-${crypto.randomUUID().slice(0,8)}`}function generateSessionId(){return`sess-${crypto.randomUUID().slice(0,8)}`}function buildInitializeRequest(agentId){return{jsonrpc:"2.0",id:generateId("init"),method:"initialize",agentId,params:{protocolVersion:1,clientCapabilities:{},clientInfo:{name:"registry-launcher-test-client",version:"1.0.0"}}}}function buildAuthenticateRequest(agentId,methodId){return{jsonrpc:"2.0",id:generateId("auth"),method:"authenticate",agentId,params:{methodId}}}function buildSessionNewRequest(agentId){return{jsonrpc:"2.0",id:generateId("session-new"),method:"session/new",agentId,params:{cwd:process.cwd(),mcpServers:[]}}}function buildSessionPromptRequest(agentId,sessionId,promptText){return{jsonrpc:"2.0",id:generateId("prompt"),method:"session/prompt",agentId,params:{sessionId,prompt:[{type:"text",text:promptText}]}}}function createConnection(options){if(options.tcp){const[host,portStr]=options.tcp.split(":");const port=parseInt(portStr,10);if(!host||isNaN(port)){console.error("Invalid TCP address. Use format: host:port");process.exit(1)}console.error(`Connecting to TCP ${host}:${port}...`);return net.createConnection({host,port})}else if(options.unix){console.error(`Connecting to Unix socket ${options.unix}...`);return net.createConnection({path:options.unix})}else{console.error("Error: Must specify --tcp or --unix");process.exit(1)}}function sendRequest(socket,request,timeout){return new Promise((resolve,reject)=>{let buffer="";let timeoutId;const cleanup=()=>{clearTimeout(timeoutId);socket.removeListener("data",onData);socket.removeListener("error",onError);socket.removeListener("close",onClose)};const onData=data=>{buffer+=data.toString();let newlineIndex;while((newlineIndex=buffer.indexOf("\n"))!==-1){const line=buffer.slice(0,newlineIndex);buffer=buffer.slice(newlineIndex+1);if(line.trim()){try{const response=JSON.parse(line);if(response.id===request.id){cleanup();resolve(response);return}else{console.error(`\u2190 Received (other): ${JSON.stringify(response)}`)}}catch(err){console.error(`Error parsing response: ${err.message}`)}}}};const onError=err=>{cleanup();reject(new Error(`Connection error: ${err.message}`))};const onClose=()=>{cleanup();reject(new Error("Connection closed before response received"))};timeoutId=setTimeout(()=>{cleanup();reject(new Error(`Timeout: No response within ${timeout}ms`))},timeout);socket.on("data",onData);socket.on("error",onError);socket.on("close",onClose);console.error(`\u2192 Sending: ${JSON.stringify(request)}`);socket.write(JSON.stringify(request)+"\n")})}async function runInitializeFlow(socket,options){console.error("\n=== Initialize Flow ===");const request=buildInitializeRequest(options.agentId);const response=await sendRequest(socket,request,options.timeout);console.log("\nInitialize Response:");console.log(JSON.stringify(response,null,2));if(response.error){console.error(`\u2717 Initialize failed: ${response.error.message}`)}else{console.error("\u2713 Initialize successful");if(response.result?.protocolVersion){console.error(` Protocol version: ${response.result.protocolVersion}`)}if(response.result?.serverInfo){console.error(` Server: ${response.result.serverInfo.name} v${response.result.serverInfo.version}`)}}return response}async function runAuthenticateFlow(socket,options,methodId){console.error("\n=== Authenticate Flow ===");console.error(` Method: ${methodId}`);const request=buildAuthenticateRequest(options.agentId,methodId);const response=await sendRequest(socket,request,options.timeout);console.log("\nAuthenticate Response:");console.log(JSON.stringify(response,null,2));if(response.error){console.error(`\u2717 Authentication failed: ${response.error.message}`)}else{console.error("\u2713 Authentication successful")}return response}async function runSessionNewFlow(socket,options){console.error("\n=== Session/New Flow ===");const request=buildSessionNewRequest(options.agentId);const response=await sendRequest(socket,request,options.timeout);console.log("\nSession/New Response:");console.log(JSON.stringify(response,null,2));if(response.error){console.error(`\u2717 Session creation failed: ${response.error.message}`)}else{console.error("\u2713 Session created successfully");if(response.result?.sessionId){console.error(` Session ID: ${response.result.sessionId}`)}}return response}async function runSessionPromptFlow(socket,options,sessionId){console.error("\n=== Session/Prompt Flow ===");const request=buildSessionPromptRequest(options.agentId,sessionId,options.prompt);const response=await sendRequest(socket,request,options.timeout);console.log("\nSession/Prompt Response:");console.log(JSON.stringify(response,null,2));if(response.error){console.error(`\u2717 Prompt failed: ${response.error.message}`)}else{console.error("\u2713 Prompt successful");if(response.result?.messages){console.error(` Response messages: ${response.result.messages.length}`)}}return response}async function runFullFlow(socket,options){console.error("\n========================================");console.error("Running Full ACP Flow");console.error(`Agent: ${options.agentId}`);console.error("========================================");const initResponse=await runInitializeFlow(socket,options);if(initResponse.error){console.error("\nFull flow aborted due to initialize failure.");return}const authMethods=initResponse.result?.authMethods||[];if(authMethods.length>0){const openaiMethod=authMethods.find(m=>m.id==="openai-api-key");const apiKeyMethod=authMethods.find(m=>m.id.includes("api-key")||m.id.includes("apikey"));const methodId=openaiMethod?.id||apiKeyMethod?.id||authMethods[0].id;const authResponse=await runAuthenticateFlow(socket,options,methodId);if(authResponse.error){console.error("\nFull flow aborted due to authentication failure.");return}}const sessionResponse=await runSessionNewFlow(socket,options);if(sessionResponse.error){console.error("\nFull flow aborted due to session creation failure.");return}const sessionId=sessionResponse.result?.sessionId||options.sessionId||generateSessionId();await runSessionPromptFlow(socket,options,sessionId);console.error("\n========================================");console.error("Full ACP Flow Complete");console.error("========================================")}async function runSingleFlow(options){const socket=createConnection(options);socket.on("connect",async()=>{console.error("Connected.");try{switch(options.flow){case"initialize":await runInitializeFlow(socket,options);break;case"session-new":await runSessionNewFlow(socket,options);break;case"session-prompt":const sessionId=options.sessionId||generateSessionId();if(!options.sessionId){console.error(`Note: Using generated session ID: ${sessionId}`)}await runSessionPromptFlow(socket,options,sessionId);break;case"full":await runFullFlow(socket,options);break;default:console.error(`Unknown flow: ${options.flow}`);console.error("Valid flows: initialize, session-new, session-prompt, full")}}catch(err){console.error(`
|
|
51
|
+
Error: ${err.message}`)}finally{socket.end()}});socket.on("error",err=>{console.error(`Connection error: ${err.message}`);process.exit(1)});socket.on("close",()=>{console.error("\nConnection closed.");process.exit(0)})}function runInteractive(options){const socket=createConnection(options);let buffer="";const rl=readline.createInterface({input:process.stdin,output:process.stderr,terminal:false});socket.on("connect",()=>{console.error("Connected in interactive mode.");console.error(`Agent ID: ${options.agentId} (will be added to all messages)`);console.error("\nEnter JSON-RPC messages (one per line). agentId will be added automatically.");console.error('Example: {"jsonrpc":"2.0","id":"1","method":"initialize","params":{}}');console.error("Press Ctrl+D to exit.\n")});socket.on("data",data=>{buffer+=data.toString();let newlineIndex;while((newlineIndex=buffer.indexOf("\n"))!==-1){const line=buffer.slice(0,newlineIndex);buffer=buffer.slice(newlineIndex+1);if(line.trim()){try{const response=JSON.parse(line);console.log("\n\u2190 Response:");console.log(JSON.stringify(response,null,2));console.error("")}catch(err){console.error(`Error parsing response: ${err.message}`)}}}});rl.on("line",line=>{if(!line.trim())return;try{const msg=JSON.parse(line);msg.agentId=options.agentId;console.error(`\u2192 Sending (with agentId=${options.agentId}): ${JSON.stringify(msg)}`);socket.write(JSON.stringify(msg)+"\n")}catch(err){console.error(`Invalid JSON: ${err.message}`);console.error("Please enter a valid JSON object.")}});rl.on("close",()=>{console.error("\nClosing connection...");socket.end()});socket.on("error",err=>{console.error(`Connection error: ${err.message}`);rl.close();process.exit(1)});socket.on("close",()=>{console.error("Connection closed.");process.exit(0)})}function main(){const options=parseArgs();if(options.help){showHelp();process.exit(0)}if(!options.tcp&&!options.unix){console.error("Error: Must specify --tcp or --unix connection");console.error("Use --help for usage information.");process.exit(1)}if(!options.agentId){console.error("Error: Must specify --agent <id> for routing");console.error("Use --help for usage information.");process.exit(1)}if(options.interactive){runInteractive(options)}else if(options.flow){runSingleFlow(options)}else{options.flow="full";runSingleFlow(options)}}process.on("uncaughtException",err=>{console.error(`Uncaught exception: ${err.message}`);process.exit(1)});process.on("unhandledRejection",reason=>{console.error(`Unhandled rejection: ${reason}`);process.exit(1)});main();
|
|
52
|
+
//# sourceMappingURL=registry-launcher-client.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../workers-registry/acp-registry/registry-launcher-client.js"],
|
|
4
|
+
"sourcesContent": ["#!/usr/bin/env node\n\n/*\n * Apache License 2.0\n * Copyright (c) 2025\u2013present Raman Marozau, Target Insight Function.\n * Contact: raman@worktif.com\n *\n * This file is part of the stdio bus protocol reference implementation:\n * stdio_bus_kernel_workers (target: <target_stdio_bus_kernel_workers>).\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * SPDX-License-Identifier: Apache-2.0\n */\n\n/**\n * @file registry-launcher-client.js\n * @brief Test client for ACP Registry Transit via stdio Bus\n *\n * This test client demonstrates sending ACP messages through the Registry Launcher\n * transit chain. It shows how to use the agentId field for routing messages to\n * specific agents registered in the ACP Registry.\n *\n * ## Transit Chain\n *\n * ```\n * This Client \u2192 stdio Bus \u2192 Registry Launcher \u2192 ACP Agent \u2192 back\n * ```\n *\n * ## Connection Modes\n *\n * The client supports TCP and Unix socket connections to stdio Bus:\n *\n * - **TCP**: Connect to `--tcp <host:port>` mode\n * ```bash\n * node workers-registry/acp-registry/registry-launcher-client.js --tcp localhost:9000 --agent my-agent\n * ```\n *\n * - **Unix Socket**: Connect to `--unix <path>` mode\n * ```bash\n * node workers-registry/acp-registry/registry-launcher-client.js --unix /tmp/stdio_bus.sock --agent my-agent\n * ```\n *\n * ## Usage\n *\n * ```bash\n * # Start stdio Bus with Registry Launcher configuration. stdio Bus kernel repo: https://github.com/stdiobus/stdiobus\n * ./stdio_bus --config workers-registry/acp-registry/registry-launcher-config.json --tcp localhost:9000\n *\n * # In another terminal, run the test client\n * node workers-registry/acp-registry/registry-launcher-client.js --tcp localhost:9000 --agent my-agent\n *\n * # Run specific ACP flow\n * node workers-registry/acp-registry/registry-launcher-client.js --tcp localhost:9000 --agent my-agent --flow initialize\n * node workers-registry/acp-registry/registry-launcher-client.js --tcp localhost:9000 --agent my-agent --flow session-new\n * node workers-registry/acp-registry/registry-launcher-client.js --tcp localhost:9000 --agent my-agent --flow session-prompt\n *\n * # Run full ACP flow (initialize \u2192 session/new \u2192 session/prompt)\n * node workers-registry/acp-registry/registry-launcher-client.js --tcp localhost:9000 --agent my-agent --flow full\n *\n * # Interactive mode - send custom messages with agentId\n * node workers-registry/acp-registry/registry-launcher-client.js --tcp localhost:9000 --agent my-agent --interactive\n * ```\n *\n * ## Command Line Options\n *\n * | Option | Description |\n * |--------|-------------|\n * | `--tcp <host:port>` | Connect via TCP to specified host and port |\n * | `--unix <path>` | Connect via Unix domain socket |\n * | `--agent <id>` | Agent ID from ACP Registry (required) |\n * | `--flow <type>` | ACP flow to execute: initialize, session-new, session-prompt, full |\n * | `--session <id>` | Session ID for session-based requests (auto-generated if not provided) |\n * | `--prompt <text>` | Prompt text for session/prompt request (default: \"Hello, agent!\") |\n * | `--interactive` | Interactive mode: read JSON from stdin, auto-add agentId |\n * | `--timeout <ms>` | Response timeout in milliseconds (default: 30000) |\n * | `--help` | Show usage information |\n *\n * ## ACP Message Flows\n *\n * ### Initialize Flow\n *\n * Sends an `initialize` request to establish protocol version and capabilities:\n *\n * ```json\n * {\n * \"jsonrpc\": \"2.0\",\n * \"id\": \"init-1\",\n * \"method\": \"initialize\",\n * \"agentId\": \"my-agent\",\n * \"params\": {\n * \"protocolVersion\": 1,\n * \"capabilities\": {},\n * \"clientInfo\": {\n * \"name\": \"registry-launcher-test-client\",\n * \"version\": \"1.0.0\"\n * }\n * }\n * }\n * ```\n *\n * ### Session/New Flow\n *\n * Creates a new session with the agent:\n *\n * ```json\n * {\n * \"jsonrpc\": \"2.0\",\n * \"id\": \"session-new-1\",\n * \"method\": \"session/new\",\n * \"agentId\": \"my-agent\",\n * \"params\": {}\n * }\n * ```\n *\n * ### Session/Prompt Flow\n *\n * Sends a prompt to an existing session:\n *\n * ```json\n * {\n * \"jsonrpc\": \"2.0\",\n * \"id\": \"prompt-1\",\n * \"method\": \"session/prompt\",\n * \"agentId\": \"my-agent\",\n * \"params\": {\n * \"sessionId\": \"sess-123\",\n * \"prompt\": {\n * \"messages\": [\n * {\n * \"role\": \"user\",\n * \"content\": { \"type\": \"text\", \"text\": \"Hello, agent!\" }\n * }\n * ]\n * }\n * }\n * }\n * ```\n *\n * ## Important Notes\n *\n * - The `agentId` field is required for all messages and is used by the Registry\n * Launcher to route messages to the correct agent process\n * - The Registry Launcher removes the `agentId` field before forwarding to the agent\n * - Responses from agents are forwarded unchanged (no agentId added)\n * - Session IDs returned by session/new should be used in subsequent session/prompt calls\n */\n\nimport net from 'net';\nimport readline from 'readline';\nimport crypto from 'crypto';\n\n/**\n * Parse command line arguments.\n * @returns {Object} Parsed options\n */\nfunction parseArgs() {\n const args = process.argv.slice(2);\n const options = {\n tcp: null,\n unix: null,\n agentId: null,\n flow: null,\n sessionId: null,\n prompt: 'Hello, agent!',\n interactive: false,\n timeout: 30000,\n help: false\n };\n\n for (let i = 0; i < args.length; i++) {\n const arg = args[i];\n switch (arg) {\n case '--tcp':\n options.tcp = args[++i];\n break;\n case '--unix':\n options.unix = args[++i];\n break;\n case '--agent':\n options.agentId = args[++i];\n break;\n case '--flow':\n options.flow = args[++i];\n break;\n case '--session':\n options.sessionId = args[++i];\n break;\n case '--prompt':\n options.prompt = args[++i];\n break;\n case '--interactive':\n options.interactive = true;\n break;\n case '--timeout':\n options.timeout = parseInt(args[++i], 10);\n break;\n case '--help':\n case '-h':\n options.help = true;\n break;\n default:\n console.error(`Unknown option: ${arg}`);\n process.exit(1);\n }\n }\n\n return options;\n}\n\n/**\n * Display usage information.\n */\nfunction showHelp() {\n console.log(`\nACP Registry Transit Test Client\n\nThis client demonstrates sending ACP messages through the Registry Launcher\ntransit chain: Client \u2192 stdio Bus \u2192 Registry Launcher \u2192 ACP Agent \u2192 back\n\nUsage:\n node registry-launcher-client.js --tcp <host:port> --agent <id> [options]\n node registry-launcher-client.js --unix <path> --agent <id> [options]\n\nConnection (one required):\n --tcp <host:port> Connect via TCP (e.g., localhost:9000)\n --unix <path> Connect via Unix socket (e.g., /tmp/stdio_bus.sock)\n\nRequired:\n --agent <id> Agent ID from ACP Registry to route messages to\n\nACP Flow Options:\n --flow <type> ACP flow to execute:\n initialize - Send initialize request\n session-new - Create new session\n session-prompt - Send prompt to session\n full - Run full flow (init \u2192 new \u2192 prompt)\n --session <id> Session ID for session/prompt (auto-generated if not set)\n --prompt <text> Prompt text for session/prompt (default: \"Hello, agent!\")\n\nModes:\n --interactive Read JSON from stdin, auto-add agentId to messages\n --timeout <ms> Response timeout in ms (default: 30000)\n\nOther:\n --help, -h Show this help message\n\nExamples:\n # Run full ACP flow with an agent\n node registry-launcher-client.js --tcp localhost:9000 --agent my-agent --flow full\n\n # Send just an initialize request\n node registry-launcher-client.js --tcp localhost:9000 --agent my-agent --flow initialize\n\n # Create a new session\n node registry-launcher-client.js --tcp localhost:9000 --agent my-agent --flow session-new\n\n # Send a prompt to an existing session\n node registry-launcher-client.js --tcp localhost:9000 --agent my-agent --flow session-prompt --session sess-123\n\n # Interactive mode - type JSON messages, agentId added automatically\n node registry-launcher-client.js --tcp localhost:9000 --agent my-agent --interactive\n`);\n}\n\n/**\n * Generate a unique request ID.\n * @param {string} prefix - Prefix for the ID\n * @returns {string} Unique ID\n */\nfunction generateId(prefix = 'req') {\n return `${prefix}-${crypto.randomUUID().slice(0, 8)}`;\n}\n\n/**\n * Generate a unique session ID.\n * @returns {string} Session ID\n */\nfunction generateSessionId() {\n return `sess-${crypto.randomUUID().slice(0, 8)}`;\n}\n\n/**\n * Build an ACP initialize request.\n * @param {string} agentId - Agent ID for routing\n * @returns {Object} JSON-RPC initialize request\n */\nfunction buildInitializeRequest(agentId) {\n return {\n jsonrpc: '2.0',\n id: generateId('init'),\n method: 'initialize',\n agentId,\n params: {\n protocolVersion: 1,\n clientCapabilities: {},\n clientInfo: {\n name: 'registry-launcher-test-client',\n version: '1.0.0'\n }\n }\n };\n}\n\n/**\n * Build an ACP authenticate request.\n * @param {string} agentId - Agent ID for routing\n * @param {string} methodId - Authentication method ID\n * @returns {Object} JSON-RPC authenticate request\n */\nfunction buildAuthenticateRequest(agentId, methodId) {\n return {\n jsonrpc: '2.0',\n id: generateId('auth'),\n method: 'authenticate',\n agentId,\n params: {\n methodId\n }\n };\n}\n\n/**\n * Build an ACP session/new request.\n * @param {string} agentId - Agent ID for routing\n * @returns {Object} JSON-RPC session/new request\n */\nfunction buildSessionNewRequest(agentId) {\n return {\n jsonrpc: '2.0',\n id: generateId('session-new'),\n method: 'session/new',\n agentId,\n params: {\n cwd: process.cwd(),\n mcpServers: []\n }\n };\n}\n\n/**\n * Build an ACP session/prompt request.\n * @param {string} agentId - Agent ID for routing\n * @param {string} sessionId - Session ID\n * @param {string} promptText - Prompt text\n * @returns {Object} JSON-RPC session/prompt request\n */\nfunction buildSessionPromptRequest(agentId, sessionId, promptText) {\n return {\n jsonrpc: '2.0',\n id: generateId('prompt'),\n method: 'session/prompt',\n agentId,\n params: {\n sessionId,\n prompt: [\n {\n type: 'text',\n text: promptText\n }\n ]\n }\n };\n}\n\n/**\n * Create a socket connection to stdio Bus.\n * @param {Object} options - Connection options\n * @returns {net.Socket} Connected socket\n */\nfunction createConnection(options) {\n if (options.tcp) {\n const [host, portStr] = options.tcp.split(':');\n const port = parseInt(portStr, 10);\n if (!host || isNaN(port)) {\n console.error('Invalid TCP address. Use format: host:port');\n process.exit(1);\n }\n console.error(`Connecting to TCP ${host}:${port}...`);\n return net.createConnection({ host, port });\n } else if (options.unix) {\n console.error(`Connecting to Unix socket ${options.unix}...`);\n return net.createConnection({ path: options.unix });\n } else {\n console.error('Error: Must specify --tcp or --unix');\n process.exit(1);\n }\n}\n\n/**\n * Send a request and wait for response.\n * @param {net.Socket} socket - Connected socket\n * @param {Object} request - Request to send\n * @param {number} timeout - Timeout in ms\n * @returns {Promise<Object>} Response object\n */\nfunction sendRequest(socket, request, timeout) {\n return new Promise((resolve, reject) => {\n let buffer = '';\n let timeoutId;\n\n const cleanup = () => {\n clearTimeout(timeoutId);\n socket.removeListener('data', onData);\n socket.removeListener('error', onError);\n socket.removeListener('close', onClose);\n };\n\n const onData = (data) => {\n buffer += data.toString();\n\n let newlineIndex;\n while ((newlineIndex = buffer.indexOf('\\n')) !== -1) {\n const line = buffer.slice(0, newlineIndex);\n buffer = buffer.slice(newlineIndex + 1);\n\n if (line.trim()) {\n try {\n const response = JSON.parse(line);\n // Check if this response matches our request ID\n if (response.id === request.id) {\n cleanup();\n resolve(response);\n return;\n } else {\n // Log other responses (notifications, etc.)\n console.error(`\u2190 Received (other): ${JSON.stringify(response)}`);\n }\n } catch (err) {\n console.error(`Error parsing response: ${err.message}`);\n }\n }\n }\n };\n\n const onError = (err) => {\n cleanup();\n reject(new Error(`Connection error: ${err.message}`));\n };\n\n const onClose = () => {\n cleanup();\n reject(new Error('Connection closed before response received'));\n };\n\n timeoutId = setTimeout(() => {\n cleanup();\n reject(new Error(`Timeout: No response within ${timeout}ms`));\n }, timeout);\n\n socket.on('data', onData);\n socket.on('error', onError);\n socket.on('close', onClose);\n\n // Send the request\n console.error(`\u2192 Sending: ${JSON.stringify(request)}`);\n socket.write(JSON.stringify(request) + '\\n');\n });\n}\n\n/**\n * Run the initialize flow.\n * @param {net.Socket} socket - Connected socket\n * @param {Object} options - Options\n * @returns {Promise<Object>} Initialize response\n */\nasync function runInitializeFlow(socket, options) {\n console.error('\\n=== Initialize Flow ===');\n const request = buildInitializeRequest(options.agentId);\n const response = await sendRequest(socket, request, options.timeout);\n\n console.log('\\nInitialize Response:');\n console.log(JSON.stringify(response, null, 2));\n\n if (response.error) {\n console.error(`\u2717 Initialize failed: ${response.error.message}`);\n } else {\n console.error('\u2713 Initialize successful');\n if (response.result?.protocolVersion) {\n console.error(` Protocol version: ${response.result.protocolVersion}`);\n }\n if (response.result?.serverInfo) {\n console.error(` Server: ${response.result.serverInfo.name} v${response.result.serverInfo.version}`);\n }\n }\n\n return response;\n}\n\n/**\n * Run the authenticate flow.\n * @param {net.Socket} socket - Connected socket\n * @param {Object} options - Options\n * @param {string} methodId - Authentication method ID\n * @returns {Promise<Object>} Authenticate response\n */\nasync function runAuthenticateFlow(socket, options, methodId) {\n console.error('\\n=== Authenticate Flow ===');\n console.error(` Method: ${methodId}`);\n const request = buildAuthenticateRequest(options.agentId, methodId);\n const response = await sendRequest(socket, request, options.timeout);\n\n console.log('\\nAuthenticate Response:');\n console.log(JSON.stringify(response, null, 2));\n\n if (response.error) {\n console.error(`\u2717 Authentication failed: ${response.error.message}`);\n } else {\n console.error('\u2713 Authentication successful');\n }\n\n return response;\n}\n\n/**\n * Run the session/new flow.\n * @param {net.Socket} socket - Connected socket\n * @param {Object} options - Options\n * @returns {Promise<Object>} Session/new response\n */\nasync function runSessionNewFlow(socket, options) {\n console.error('\\n=== Session/New Flow ===');\n const request = buildSessionNewRequest(options.agentId);\n const response = await sendRequest(socket, request, options.timeout);\n\n console.log('\\nSession/New Response:');\n console.log(JSON.stringify(response, null, 2));\n\n if (response.error) {\n console.error(`\u2717 Session creation failed: ${response.error.message}`);\n } else {\n console.error('\u2713 Session created successfully');\n if (response.result?.sessionId) {\n console.error(` Session ID: ${response.result.sessionId}`);\n }\n }\n\n return response;\n}\n\n/**\n * Run the session/prompt flow.\n * @param {net.Socket} socket - Connected socket\n * @param {Object} options - Options\n * @param {string} sessionId - Session ID to use\n * @returns {Promise<Object>} Session/prompt response\n */\nasync function runSessionPromptFlow(socket, options, sessionId) {\n console.error('\\n=== Session/Prompt Flow ===');\n const request = buildSessionPromptRequest(options.agentId, sessionId, options.prompt);\n const response = await sendRequest(socket, request, options.timeout);\n\n console.log('\\nSession/Prompt Response:');\n console.log(JSON.stringify(response, null, 2));\n\n if (response.error) {\n console.error(`\u2717 Prompt failed: ${response.error.message}`);\n } else {\n console.error('\u2713 Prompt successful');\n if (response.result?.messages) {\n console.error(` Response messages: ${response.result.messages.length}`);\n }\n }\n\n return response;\n}\n\n/**\n * Run the full ACP flow (initialize \u2192 session/new \u2192 session/prompt).\n * @param {net.Socket} socket - Connected socket\n * @param {Object} options - Options\n */\nasync function runFullFlow(socket, options) {\n console.error('\\n========================================');\n console.error('Running Full ACP Flow');\n console.error(`Agent: ${options.agentId}`);\n console.error('========================================');\n\n // Step 1: Initialize\n const initResponse = await runInitializeFlow(socket, options);\n if (initResponse.error) {\n console.error('\\nFull flow aborted due to initialize failure.');\n return;\n }\n\n // Step 2: Authenticate if required\n const authMethods = initResponse.result?.authMethods || [];\n if (authMethods.length > 0) {\n // Prefer openai-api-key, then any api-key method, then first available\n const openaiMethod = authMethods.find(m => m.id === 'openai-api-key');\n const apiKeyMethod = authMethods.find(m =>\n m.id.includes('api-key') || m.id.includes('apikey')\n );\n const methodId = openaiMethod?.id || apiKeyMethod?.id || authMethods[0].id;\n\n const authResponse = await runAuthenticateFlow(socket, options, methodId);\n if (authResponse.error) {\n console.error('\\nFull flow aborted due to authentication failure.');\n return;\n }\n }\n\n // Step 3: Create session\n const sessionResponse = await runSessionNewFlow(socket, options);\n if (sessionResponse.error) {\n console.error('\\nFull flow aborted due to session creation failure.');\n return;\n }\n\n // Extract session ID from response\n const sessionId = sessionResponse.result?.sessionId || options.sessionId || generateSessionId();\n\n // Step 4: Send prompt\n await runSessionPromptFlow(socket, options, sessionId);\n\n console.error('\\n========================================');\n console.error('Full ACP Flow Complete');\n console.error('========================================');\n}\n\n/**\n * Run a single flow based on options.\n * @param {Object} options - Parsed options\n */\nasync function runSingleFlow(options) {\n const socket = createConnection(options);\n\n socket.on('connect', async () => {\n console.error('Connected.');\n\n try {\n switch (options.flow) {\n case 'initialize':\n await runInitializeFlow(socket, options);\n break;\n case 'session-new':\n await runSessionNewFlow(socket, options);\n break;\n case 'session-prompt':\n const sessionId = options.sessionId || generateSessionId();\n if (!options.sessionId) {\n console.error(`Note: Using generated session ID: ${sessionId}`);\n }\n await runSessionPromptFlow(socket, options, sessionId);\n break;\n case 'full':\n await runFullFlow(socket, options);\n break;\n default:\n console.error(`Unknown flow: ${options.flow}`);\n console.error('Valid flows: initialize, session-new, session-prompt, full');\n }\n } catch (err) {\n console.error(`\\nError: ${err.message}`);\n } finally {\n socket.end();\n }\n });\n\n socket.on('error', (err) => {\n console.error(`Connection error: ${err.message}`);\n process.exit(1);\n });\n\n socket.on('close', () => {\n console.error('\\nConnection closed.');\n process.exit(0);\n });\n}\n\n/**\n * Run in interactive mode.\n * Reads JSON messages from stdin and adds agentId before sending.\n * @param {Object} options - Parsed options\n */\nfunction runInteractive(options) {\n const socket = createConnection(options);\n let buffer = '';\n\n const rl = readline.createInterface({\n input: process.stdin,\n output: process.stderr,\n terminal: false\n });\n\n socket.on('connect', () => {\n console.error('Connected in interactive mode.');\n console.error(`Agent ID: ${options.agentId} (will be added to all messages)`);\n console.error('\\nEnter JSON-RPC messages (one per line). agentId will be added automatically.');\n console.error('Example: {\"jsonrpc\":\"2.0\",\"id\":\"1\",\"method\":\"initialize\",\"params\":{}}');\n console.error('Press Ctrl+D to exit.\\n');\n });\n\n // Handle incoming responses\n socket.on('data', (data) => {\n buffer += data.toString();\n\n let newlineIndex;\n while ((newlineIndex = buffer.indexOf('\\n')) !== -1) {\n const line = buffer.slice(0, newlineIndex);\n buffer = buffer.slice(newlineIndex + 1);\n\n if (line.trim()) {\n try {\n const response = JSON.parse(line);\n console.log('\\n\u2190 Response:');\n console.log(JSON.stringify(response, null, 2));\n console.error('');\n } catch (err) {\n console.error(`Error parsing response: ${err.message}`);\n }\n }\n }\n });\n\n // Handle user input\n rl.on('line', (line) => {\n if (!line.trim()) return;\n\n try {\n // Parse the user's JSON\n const msg = JSON.parse(line);\n\n // Add agentId for routing\n msg.agentId = options.agentId;\n\n console.error(`\u2192 Sending (with agentId=${options.agentId}): ${JSON.stringify(msg)}`);\n socket.write(JSON.stringify(msg) + '\\n');\n } catch (err) {\n console.error(`Invalid JSON: ${err.message}`);\n console.error('Please enter a valid JSON object.');\n }\n });\n\n rl.on('close', () => {\n console.error('\\nClosing connection...');\n socket.end();\n });\n\n socket.on('error', (err) => {\n console.error(`Connection error: ${err.message}`);\n rl.close();\n process.exit(1);\n });\n\n socket.on('close', () => {\n console.error('Connection closed.');\n process.exit(0);\n });\n}\n\n/**\n * Main entry point.\n */\nfunction main() {\n const options = parseArgs();\n\n if (options.help) {\n showHelp();\n process.exit(0);\n }\n\n // Validate required options\n if (!options.tcp && !options.unix) {\n console.error('Error: Must specify --tcp or --unix connection');\n console.error('Use --help for usage information.');\n process.exit(1);\n }\n\n if (!options.agentId) {\n console.error('Error: Must specify --agent <id> for routing');\n console.error('Use --help for usage information.');\n process.exit(1);\n }\n\n // Determine mode\n if (options.interactive) {\n runInteractive(options);\n } else if (options.flow) {\n runSingleFlow(options);\n } else {\n // Default to full flow if no specific flow or interactive mode\n options.flow = 'full';\n runSingleFlow(options);\n }\n}\n\n// Handle uncaught exceptions\nprocess.on('uncaughtException', (err) => {\n console.error(`Uncaught exception: ${err.message}`);\n process.exit(1);\n});\n\n// Handle unhandled promise rejections\nprocess.on('unhandledRejection', (reason) => {\n console.error(`Unhandled rejection: ${reason}`);\n process.exit(1);\n});\n\nmain();\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "stdio Bus configuration for running the Registry Launcher as a worker pool",
|
|
3
|
+
"_description": "This configuration demonstrates how to use the Registry Launcher to connect ACP clients to any agent in the ACP Registry through stdio Bus",
|
|
4
|
+
"pools": [
|
|
5
|
+
{
|
|
6
|
+
"_comment": "Registry Launcher worker pool - routes messages to ACP Registry agents",
|
|
7
|
+
"id": "registry-launcher",
|
|
8
|
+
"command": "/usr/bin/env",
|
|
9
|
+
"args": [
|
|
10
|
+
"node",
|
|
11
|
+
"./workers-registry/acp-worker/dist/registry-launcher/index.js",
|
|
12
|
+
"./workers-registry/acp-registry/registry-launcher-worker-config.json"
|
|
13
|
+
],
|
|
14
|
+
"instances": 1
|
|
15
|
+
}
|
|
16
|
+
],
|
|
17
|
+
"limits": {
|
|
18
|
+
"_comment_max_input_buffer": "Maximum size of input buffer per connection (1MB default)",
|
|
19
|
+
"max_input_buffer": 1048576,
|
|
20
|
+
"_comment_max_output_queue": "Maximum size of output queue per connection (4MB default)",
|
|
21
|
+
"max_output_queue": 4194304,
|
|
22
|
+
"_comment_max_restarts": "Maximum number of worker restarts within restart_window_sec",
|
|
23
|
+
"max_restarts": 5,
|
|
24
|
+
"_comment_restart_window_sec": "Time window for counting restarts (60 seconds)",
|
|
25
|
+
"restart_window_sec": 60,
|
|
26
|
+
"_comment_drain_timeout_sec": "Timeout for draining connections during graceful shutdown",
|
|
27
|
+
"drain_timeout_sec": 30,
|
|
28
|
+
"_comment_backpressure_timeout_sec": "Timeout for backpressure before dropping messages",
|
|
29
|
+
"backpressure_timeout_sec": 60
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import{Readable,Writable}from"node:stream";import{AgentSideConnection,ndJsonStream}from"@agentclientprotocol/sdk";import{PROTOCOL_VERSION}from"@agentclientprotocol/sdk";import{Client}from"@modelcontextprotocol/sdk/client/index.js";import{StdioClientTransport}from"@modelcontextprotocol/sdk/client/stdio.js";var defaultFactories={createClient:options=>new Client(options),createTransport:options=>new StdioClientTransport(options)};var MCPManager=class{connections=new Map;toolToServer=new Map;onServerCrash;factories;constructor(factories){this.factories={...defaultFactories,...factories}}setOnServerCrash(callback){this.onServerCrash=callback}async connect(servers){for(const serverConfig of servers){try{const transport=this.factories.createTransport({command:serverConfig.command,args:serverConfig.args,env:serverConfig.env});const client=this.factories.createClient({name:"stdio-bus-worker",version:"1.0.0"});await client.connect(transport);const capabilities=client.getServerCapabilities();const connection2={client,transport,config:serverConfig,connected:true,capabilities};this.connections.set(serverConfig.id,connection2);this.setupCrashDetection(serverConfig.id,client);console.error(`[MCP] Connected to server: ${serverConfig.id}`)}catch(error){console.error(`[MCP] Failed to connect to server ${serverConfig.id}:`,error)}}}setupCrashDetection(serverId,client){client.onclose=()=>{const connection2=this.connections.get(serverId);if(connection2&&connection2.connected){connection2.connected=false;connection2.crashError="Server process exited unexpectedly";for(const[toolName,toolServerId]of this.toolToServer.entries()){if(toolServerId===serverId){this.toolToServer.delete(toolName)}}console.error(`[MCP] Server ${serverId} crashed: ${connection2.crashError}`);if(this.onServerCrash){this.onServerCrash(serverId,connection2.crashError)}}}}async listTools(){const allTools=[];this.toolToServer.clear();for(const[serverId,connection2]of this.connections){if(!connection2.connected){continue}try{let cursor;do{const result=await connection2.client.listTools(cursor?{cursor}:void 0);for(const tool of result.tools){allTools.push({name:tool.name,description:tool.description,inputSchema:tool.inputSchema,serverId});this.toolToServer.set(tool.name,serverId)}cursor=result.nextCursor}while(cursor)}catch(error){console.error(`[MCP] Failed to list tools from server ${serverId}:`,error)}}return allTools}async callTool(name,args,serverId){const targetServerId=serverId??this.toolToServer.get(name);if(!targetServerId){throw new Error(`Tool "${name}" not found. Call listTools() first to discover available tools.`)}const connection2=this.connections.get(targetServerId);if(!connection2){throw new Error(`Server "${targetServerId}" not found.`)}if(!connection2.connected){const crashMessage=connection2.crashError||"Server is not connected";throw new Error(`Server "${targetServerId}" is unavailable: ${crashMessage}`)}try{const result=await connection2.client.callTool({name,arguments:args});const content=result.content.map(item=>{if(item.type==="text"){return{type:"text",text:item.text}}else if(item.type==="image"){return{type:"image",data:item.data,mimeType:item.mimeType}}else if(item.type==="resource"){const resource=item.resource;return{type:"resource",resource:{uri:resource.uri,mimeType:resource.mimeType,text:resource.text,blob:resource.blob}}}return{type:"text",text:JSON.stringify(item)}});return{content,isError:result.isError===true}}catch(error){console.error(`[MCP] Failed to call tool "${name}" on server ${targetServerId}:`,error);throw error}}async listResources(){const allResources=[];for(const[serverId,connection2]of this.connections){if(!connection2.connected){continue}try{let cursor;do{const result=await connection2.client.listResources(cursor?{cursor}:void 0);for(const resource of result.resources){allResources.push({uri:resource.uri,name:resource.name,description:resource.description,mimeType:resource.mimeType,serverId})}cursor=result.nextCursor}while(cursor)}catch(error){console.error(`[MCP] Failed to list resources from server ${serverId}:`,error)}}return allResources}async readResource(uri,serverId){let targetServerId=serverId;if(!targetServerId){for(const[,connection3]of this.connections){if(!connection3.connected){continue}try{const result=await connection3.client.readResource({uri});const contents=result.contents.map(item=>{const resourceItem=item;if("text"in resourceItem&&resourceItem.text!==void 0){return{uri:resourceItem.uri,mimeType:resourceItem.mimeType,text:resourceItem.text}}else if("blob"in resourceItem&&resourceItem.blob!==void 0){return{uri:resourceItem.uri,mimeType:resourceItem.mimeType,blob:resourceItem.blob}}return{uri:resourceItem.uri,mimeType:resourceItem.mimeType,text:""}});return{contents}}catch{continue}}throw new Error(`Resource "${uri}" not found on any connected server.`)}const connection2=this.connections.get(targetServerId);if(!connection2){throw new Error(`Server "${targetServerId}" not found.`)}if(!connection2.connected){throw new Error(`Server "${targetServerId}" is not connected.`)}try{const result=await connection2.client.readResource({uri});const contents=result.contents.map(item=>{const resourceItem=item;if("text"in resourceItem&&resourceItem.text!==void 0){return{uri:resourceItem.uri,mimeType:resourceItem.mimeType,text:resourceItem.text}}else if("blob"in resourceItem&&resourceItem.blob!==void 0){return{uri:resourceItem.uri,mimeType:resourceItem.mimeType,blob:resourceItem.blob}}return{uri:resourceItem.uri,mimeType:resourceItem.mimeType,text:""}});return{contents}}catch(error){console.error(`[MCP] Failed to read resource "${uri}" from server ${targetServerId}:`,error);throw error}}getConnection(serverId){return this.connections.get(serverId)}getAllConnections(){return Array.from(this.connections.values()).filter(conn=>conn.connected)}getServerCapabilities(serverId){const connection2=this.connections.get(serverId);return connection2?.connected?connection2.capabilities:void 0}async close(){for(const connection2 of this.connections.values()){try{await connection2.client.close();connection2.connected=false}catch(error){console.error(`[MCP] Error closing connection ${connection2.config.id}:`,error)}}this.connections.clear();this.toolToServer.clear()}abortPendingOperations(){for(const connection2 of this.connections.values()){connection2.connected=false}}isServerCrashed(serverId){const connection2=this.connections.get(serverId);return connection2!==void 0&&!connection2.connected&&connection2.crashError!==void 0}getServerCrashError(serverId){const connection2=this.connections.get(serverId);return connection2?.crashError}getCrashedServers(){const crashed=[];for(const[serverId,connection2]of this.connections){if(!connection2.connected&&connection2.crashError){crashed.push({serverId,error:connection2.crashError})}}return crashed}};var defaultMcpManagerFactory=()=>new MCPManager;var Session=class{id;cwd;mcpManager;cancelled=false;createdAt;history=[];constructor(id,cwd,mcpManagerFactory){this.id=id;this.cwd=cwd;this.mcpManager=(mcpManagerFactory??defaultMcpManagerFactory)();this.createdAt=new Date}isCancelled(){return this.cancelled}cancel(){this.cancelled=true;this.mcpManager.abortPendingOperations()}addHistoryEntry(role,content){this.history.push({role,content,timestamp:new Date})}getHistory(){return[...this.history]}clearHistory(){this.history=[]}getState(){return{id:this.id,cwd:this.cwd,cancelled:this.cancelled,createdAt:this.createdAt,history:[...this.history]}}async close(){await this.mcpManager.close()}};var SessionManager=class{sessions=new Map;mcpManagerFactory;constructor(mcpManagerFactory){this.mcpManagerFactory=mcpManagerFactory}async createSession(cwd,mcpServers){const id=this.generateSessionId();const session=new Session(id,cwd,this.mcpManagerFactory);if(mcpServers&&mcpServers.length>0){await session.mcpManager.connect(mcpServers)}this.sessions.set(id,session);return session}getSession(id){return this.sessions.get(id)}cancelSession(id){const session=this.sessions.get(id);if(session){session.cancel();return true}return false}async closeSession(id){const session=this.sessions.get(id);if(session){await session.close();this.sessions.delete(id);return true}return false}async closeAll(){for(const session of this.sessions.values()){await session.close()}this.sessions.clear()}getAllSessions(){return Array.from(this.sessions.values())}removeSession(id){return this.sessions.delete(id)}generateSessionId(){return crypto.randomUUID()}};var ACPAgent=class{_connection;_sessionManager;_clientCapabilities=null;constructor(connection2){this._connection=connection2;this._sessionManager=new SessionManager}get connection(){return this._connection}get sessionManager(){return this._sessionManager}get clientCapabilities(){return this._clientCapabilities}async initialize(params){this._clientCapabilities=params.clientCapabilities??null;return{protocolVersion:PROTOCOL_VERSION,agentInfo:{name:"stdio-bus-worker",version:"1.0.0"},agentCapabilities:{promptCapabilities:{embeddedContext:true}},authMethods:[]}}async newSession(params){const mcpServers=params.mcpServers?.map(server=>{if("command"in server){return{id:server.name,command:server.command,args:server.args,env:server.env?.reduce((acc,envVar)=>{acc[envVar.name]=envVar.value;return acc},{})}}return null}).filter(s=>s!==null);const session=await this._sessionManager.createSession(params.cwd,mcpServers);return{sessionId:session.id}}async loadSession(_params){return{}}async authenticate(_params){}async prompt(params){const session=this._sessionManager.getSession(params.sessionId);if(!session){throw new Error(`Session not found: ${params.sessionId}`)}if(session.isCancelled()){return{stopReason:"cancelled"}}for(const block of params.prompt){if(session.isCancelled()){return{stopReason:"cancelled"}}if(block.type==="text"){await this._connection.sessionUpdate({sessionId:params.sessionId,update:{sessionUpdate:"agent_message_chunk",content:{type:"text",text:block.text}}})}else if(block.type==="resource_link"){const resourceLink=block;try{const result=await session.mcpManager.readResource(resourceLink.uri);if(result.contents.length>0){const content=result.contents[0];if("text"in content){await this._connection.sessionUpdate({sessionId:params.sessionId,update:{sessionUpdate:"agent_message_chunk",content:{type:"text",text:`[Resource: ${resourceLink.name}]
|
|
3
|
+
${content.text}`}}})}else if("blob"in content){await this._connection.sessionUpdate({sessionId:params.sessionId,update:{sessionUpdate:"agent_message_chunk",content:{type:"text",text:`[Resource: ${resourceLink.name}] (binary data, ${content.blob.length} bytes)`}}})}}}catch{await this._connection.sessionUpdate({sessionId:params.sessionId,update:{sessionUpdate:"agent_message_chunk",content:{type:"text",text:`[Resource link: ${resourceLink.name} (${resourceLink.uri})]`}}})}}else if(block.type==="resource"){const resource=block;if(resource.resource.text!==void 0){await this._connection.sessionUpdate({sessionId:params.sessionId,update:{sessionUpdate:"agent_message_chunk",content:{type:"text",text:`[Embedded resource: ${resource.resource.uri}]
|
|
4
|
+
${resource.resource.text}`}}})}else if(resource.resource.blob!==void 0){await this._connection.sessionUpdate({sessionId:params.sessionId,update:{sessionUpdate:"agent_message_chunk",content:{type:"text",text:`[Embedded resource: ${resource.resource.uri}] (binary data)`}}})}}else if(block.type==="image"){const image=block;await this._connection.sessionUpdate({sessionId:params.sessionId,update:{sessionUpdate:"agent_message_chunk",content:{type:"text",text:`[Image: ${image.mimeType}]`}}})}}if(session.isCancelled()){return{stopReason:"cancelled"}}return{stopReason:"end_turn"}}async cancel(params){this._sessionManager.cancelSession(params.sessionId)}};console.error("[worker] Starting ACP/MCP Protocol Worker...");var inputStream=Readable.toWeb(process.stdin);var outputStream=Writable.toWeb(process.stdout);var stream=ndJsonStream(outputStream,inputStream);var connection=new AgentSideConnection(conn=>new ACPAgent(conn),stream);console.error("[worker] AgentSideConnection established, ready for messages");process.on("SIGTERM",async()=>{console.error("[worker] Received SIGTERM, shutting down...");await connection.closed;process.exit(0)});process.on("SIGINT",async()=>{console.error("[worker] Received SIGINT, shutting down...");await connection.closed;process.exit(0)});process.on("uncaughtException",error=>{console.error("[worker] Uncaught exception:",error);process.exit(1)});process.on("unhandledRejection",(reason,promise)=>{console.error("[worker] Unhandled rejection at:",promise,"reason:",reason)});connection.closed.then(()=>{console.error("[worker] Connection closed");process.exit(0)}).catch(error=>{console.error("[worker] Connection error:",error);process.exit(1)});
|
|
5
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../workers-registry/acp-worker/src/index.ts", "../../../../workers-registry/acp-worker/src/agent.ts", "../../../../workers-registry/acp-worker/src/mcp/manager.ts", "../../../../workers-registry/acp-worker/src/session/session.ts", "../../../../workers-registry/acp-worker/src/session/manager.ts"],
|
|
4
|
+
"sourcesContent": ["/*\n * Apache License 2.0\n * Copyright (c) 2025\u2013present Raman Marozau, Target Insight Function.\n * Contact: raman@worktif.com\n *\n * This file is part of the stdio bus protocol reference implementation:\n * stdio_bus_kernel_workers (target: <target_stdio_bus_kernel_workers>).\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * SPDX-License-Identifier: Apache-2.0\n */\n\n/**\n * ACP/MCP Protocol Worker for stdio Bus kernel\n *\n * This worker implements the Agent Client Protocol (ACP) using official SDKs\n * and connects to MCP servers for tool execution.\n *\n * It runs as a child process of stdio Bus kernel, communicating via stdin/stdout NDJSON.\n *\n * @module index\n */\n\nimport { Readable, Writable } from 'node:stream';\nimport { AgentSideConnection, ndJsonStream } from '@agentclientprotocol/sdk';\nimport { ACPAgent } from './agent.js';\n\n// Log startup message to stderr (not stdout - stdout is for protocol messages)\nconsole.error('[worker] Starting ACP/MCP Protocol Worker...');\n\n/**\n * Convert Node.js stdin to a web ReadableStream.\n * The SDK expects web streams for NDJSON communication.\n */\nconst inputStream = Readable.toWeb(process.stdin) as ReadableStream<Uint8Array>;\n\n/**\n * Convert Node.js stdout to a web WritableStream.\n * The SDK expects web streams for NDJSON communication.\n */\nconst outputStream = Writable.toWeb(process.stdout) as WritableStream<Uint8Array>;\n\n/**\n * Create the NDJSON stream for ACP communication.\n * The SDK handles all NDJSON framing and JSON-RPC protocol details automatically.\n */\nconst stream = ndJsonStream(outputStream, inputStream);\n\n/**\n * Create the AgentSideConnection with stdio transport.\n *\n * The SDK pattern uses a factory function that receives the connection\n * and returns an Agent instance. The SDK handles all NDJSON framing\n * and JSON-RPC protocol details automatically.\n */\nconst connection = new AgentSideConnection(\n (conn) => new ACPAgent(conn),\n stream,\n);\n\n// Log that connection is established\nconsole.error('[worker] AgentSideConnection established, ready for messages');\n\n/**\n * Handle graceful shutdown on SIGTERM.\n *\n * When stdio Bus kernel sends SIGTERM, we should wait for the connection to close\n * and allow pending operations to complete.\n */\nprocess.on('SIGTERM', async () => {\n console.error('[worker] Received SIGTERM, shutting down...');\n // Wait for the connection to close gracefully\n await connection.closed;\n process.exit(0);\n});\n\n/**\n * Handle SIGINT for development convenience.\n */\nprocess.on('SIGINT', async () => {\n console.error('[worker] Received SIGINT, shutting down...');\n await connection.closed;\n process.exit(0);\n});\n\n/**\n * Handle uncaught exceptions to prevent silent failures.\n */\nprocess.on('uncaughtException', (error) => {\n console.error('[worker] Uncaught exception:', error);\n process.exit(1);\n});\n\n/**\n * Handle unhandled promise rejections.\n */\nprocess.on('unhandledRejection', (reason, promise) => {\n console.error('[worker] Unhandled rejection at:', promise, 'reason:', reason);\n});\n\n/**\n * Wait for the connection to close (either normally or due to error).\n * This keeps the process running until the connection ends.\n */\nconnection.closed.then(() => {\n console.error('[worker] Connection closed');\n process.exit(0);\n}).catch((error) => {\n console.error('[worker] Connection error:', error);\n process.exit(1);\n});\n", "/*\n * Apache License 2.0\n * Copyright (c) 2025\u2013present Raman Marozau, Target Insight Function.\n * Contact: raman@worktif.com\n *\n * This file is part of the stdio bus protocol reference implementation:\n * stdio_bus_kernel_workers (target: <target_stdio_bus_kernel_workers>).\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * SPDX-License-Identifier: Apache-2.0\n */\n\n/**\n * Agent Implementation\n *\n * This module implements the ACP Agent interface using @agentclientprotocol/sdk.\n * The Agent handles all ACP protocol methods including initialization,\n * session management, and prompt processing.\n *\n * @module agent\n */\n\nimport type {\n Agent,\n AgentSideConnection,\n AuthenticateRequest,\n AuthenticateResponse,\n CancelNotification,\n ClientCapabilities,\n InitializeRequest,\n InitializeResponse,\n LoadSessionRequest,\n LoadSessionResponse,\n NewSessionRequest,\n NewSessionResponse,\n PromptRequest,\n PromptResponse,\n} from '@agentclientprotocol/sdk';\nimport { PROTOCOL_VERSION } from '@agentclientprotocol/sdk';\nimport { SessionManager } from './session/manager.js';\n\n/**\n * ACP Agent implementation for stdio Bus kernel worker.\n *\n * This class implements the Agent interface from the ACP SDK,\n * handling all protocol methods and coordinating with MCP servers\n * for tool execution.\n */\nexport class ACPAgent implements Agent {\n /**\n * Reference to the AgentSideConnection for sending notifications.\n * Used by prompt processing to send session updates.\n */\n private readonly _connection: AgentSideConnection;\n\n /**\n * Session manager for handling session lifecycle.\n * Manages session creation, lookup, cancellation, and cleanup.\n */\n private readonly _sessionManager: SessionManager;\n\n /**\n * Client capabilities received during initialization.\n * Used to determine what features the client supports.\n */\n private _clientCapabilities: ClientCapabilities | null = null;\n\n /**\n * Creates a new ACP Agent instance.\n *\n * @param connection - The AgentSideConnection for communicating with the client\n */\n constructor(connection: AgentSideConnection) {\n this._connection = connection;\n this._sessionManager = new SessionManager();\n }\n\n /**\n * Get the connection for sending notifications.\n * Used by prompt processing to send session updates.\n */\n get connection(): AgentSideConnection {\n return this._connection;\n }\n\n /**\n * Get the session manager for session operations.\n */\n get sessionManager(): SessionManager {\n return this._sessionManager;\n }\n\n /**\n * Get the client capabilities received during initialization.\n * Returns null if initialize() has not been called yet.\n */\n get clientCapabilities(): ClientCapabilities | null {\n return this._clientCapabilities;\n }\n\n /**\n * Handle ACP initialize request.\n * Returns agent capabilities and info.\n *\n * Stores client capabilities for later use and returns InitializeResponse\n * with agent info and capabilities including promptCapabilities.embeddedContext: true.\n *\n * @param params - The initialization request parameters\n * @returns Promise resolving to InitializeResponse with agent capabilities\n */\n async initialize(params: InitializeRequest): Promise<InitializeResponse> {\n // Store client capabilities for later use\n this._clientCapabilities = params.clientCapabilities ?? null;\n\n // Return InitializeResponse with agent info and capabilities\n return {\n protocolVersion: PROTOCOL_VERSION,\n agentInfo: {\n name: 'stdio-bus-worker',\n version: '1.0.0',\n },\n agentCapabilities: {\n promptCapabilities: {\n embeddedContext: true,\n },\n },\n authMethods: [],\n };\n }\n\n /**\n * Handle ACP session/new request.\n * Creates a new session with MCP server connections.\n *\n * Generates a unique sessionId using crypto.randomUUID(), stores session state,\n * initializes MCP connections from the request params, and returns NewSessionResponse.\n *\n * @param params - The new session request parameters containing cwd and optional mcpServers\n * @returns Promise resolving to NewSessionResponse with session ID\n */\n async newSession(params: NewSessionRequest): Promise<NewSessionResponse> {\n // Convert ACP McpServer[] to MCPServerConfig[] for the session manager\n const mcpServers = params.mcpServers?.map((server) => {\n // Handle stdio type servers (most common for local MCP servers)\n if ('command' in server) {\n return {\n id: server.name,\n command: server.command,\n args: server.args,\n env: server.env?.reduce(\n (acc, envVar) => {\n acc[envVar.name] = envVar.value;\n return acc;\n },\n {} as Record<string, string>,\n ),\n };\n }\n // For HTTP/SSE servers, we'll need to handle them differently\n // For now, skip non-stdio servers\n return null;\n }).filter((s): s is NonNullable<typeof s> => s !== null);\n\n // Create a new session with the session manager\n // This generates a UUID for sessionId and stores session state\n const session = await this._sessionManager.createSession(params.cwd, mcpServers);\n\n // Return NewSessionResponse with the sessionId\n return {\n sessionId: session.id,\n };\n }\n\n /**\n * Handle ACP session/load request.\n * Loads an existing session (optional capability).\n *\n * @param params - The load session request parameters\n * @returns Promise resolving to LoadSessionResponse\n */\n async loadSession(_params: LoadSessionRequest): Promise<LoadSessionResponse> {\n // Session loading is an optional capability\n // Return empty response to indicate session not found\n return {} as LoadSessionResponse;\n }\n\n /**\n * Handle ACP authenticate request.\n * Processes authentication (if required).\n *\n * @param params - The authentication request parameters\n * @returns Promise resolving to AuthenticateResponse or void\n */\n async authenticate(_params: AuthenticateRequest): Promise<AuthenticateResponse | void> {\n // Authentication is optional - no auth methods are declared\n // This is a no-op implementation\n }\n\n /**\n * Handle ACP session/prompt request.\n * Processes user prompts and streams responses.\n *\n * Currently implements echo mode for testing - echoes user prompt as agent response.\n *\n * @param params - The prompt request parameters\n * @returns Promise resolving to PromptResponse with stop reason\n */\n async prompt(params: PromptRequest): Promise<PromptResponse> {\n // Validate session exists\n const session = this._sessionManager.getSession(params.sessionId);\n if (!session) {\n throw new Error(`Session not found: ${params.sessionId}`);\n }\n\n // Check for cancellation before processing\n if (session.isCancelled()) {\n return { stopReason: 'cancelled' };\n }\n\n // Process each content block in the prompt\n // Echo mode: echo user prompt as agent response\n for (const block of params.prompt) {\n // Check for cancellation during processing\n if (session.isCancelled()) {\n return { stopReason: 'cancelled' };\n }\n\n // Handle different content block types\n if (block.type === 'text') {\n // Echo text content as agent_message_chunk\n await this._connection.sessionUpdate({\n sessionId: params.sessionId,\n update: {\n sessionUpdate: 'agent_message_chunk',\n content: {\n type: 'text',\n text: block.text,\n },\n },\n });\n } else if (block.type === 'resource_link') {\n // For resource_link, try to resolve and echo the content\n const resourceLink = block as { type: 'resource_link'; uri: string; name: string };\n try {\n // Try to read the resource from MCP servers\n const result = await session.mcpManager.readResource(resourceLink.uri);\n if (result.contents.length > 0) {\n const content = result.contents[0];\n if ('text' in content) {\n await this._connection.sessionUpdate({\n sessionId: params.sessionId,\n update: {\n sessionUpdate: 'agent_message_chunk',\n content: {\n type: 'text',\n text: `[Resource: ${resourceLink.name}]\\n${content.text}`,\n },\n },\n });\n } else if ('blob' in content) {\n await this._connection.sessionUpdate({\n sessionId: params.sessionId,\n update: {\n sessionUpdate: 'agent_message_chunk',\n content: {\n type: 'text',\n text: `[Resource: ${resourceLink.name}] (binary data, ${content.blob.length} bytes)`,\n },\n },\n });\n }\n }\n } catch {\n // If resource resolution fails, just echo the link info\n await this._connection.sessionUpdate({\n sessionId: params.sessionId,\n update: {\n sessionUpdate: 'agent_message_chunk',\n content: {\n type: 'text',\n text: `[Resource link: ${resourceLink.name} (${resourceLink.uri})]`,\n },\n },\n });\n }\n } else if (block.type === 'resource') {\n // For embedded resource, echo the content\n const resource = block as { type: 'resource'; resource: { uri: string; text?: string; blob?: string } };\n if (resource.resource.text !== undefined) {\n await this._connection.sessionUpdate({\n sessionId: params.sessionId,\n update: {\n sessionUpdate: 'agent_message_chunk',\n content: {\n type: 'text',\n text: `[Embedded resource: ${resource.resource.uri}]\\n${resource.resource.text}`,\n },\n },\n });\n } else if (resource.resource.blob !== undefined) {\n await this._connection.sessionUpdate({\n sessionId: params.sessionId,\n update: {\n sessionUpdate: 'agent_message_chunk',\n content: {\n type: 'text',\n text: `[Embedded resource: ${resource.resource.uri}] (binary data)`,\n },\n },\n });\n }\n } else if (block.type === 'image') {\n // For images, echo a description\n const image = block as { type: 'image'; mimeType: string };\n await this._connection.sessionUpdate({\n sessionId: params.sessionId,\n update: {\n sessionUpdate: 'agent_message_chunk',\n content: {\n type: 'text',\n text: `[Image: ${image.mimeType}]`,\n },\n },\n });\n }\n }\n\n // Final cancellation check\n if (session.isCancelled()) {\n return { stopReason: 'cancelled' };\n }\n\n // Return end_turn as stopReason for successful completion\n return { stopReason: 'end_turn' };\n }\n\n /**\n * Handle ACP session/cancel notification.\n * Cancels ongoing operations for a session.\n *\n * Looks up the session by sessionId and calls session.cancel() to set\n * the cancellation flag and abort pending MCP operations.\n *\n * @param params - The cancel notification parameters containing sessionId\n */\n async cancel(params: CancelNotification): Promise<void> {\n // Look up the session by sessionId and cancel it\n // The session's cancel() method sets the cancellation flag\n // and aborts pending MCP operations\n this._sessionManager.cancelSession(params.sessionId);\n }\n}\n", "/*\n * Apache License 2.0\n * Copyright (c) 2025\u2013present Raman Marozau, Target Insight Function.\n * Contact: raman@worktif.com\n *\n * This file is part of the stdio bus protocol reference implementation:\n * stdio_bus_kernel_workers (target: <target_stdio_bus_kernel_workers>).\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * SPDX-License-Identifier: Apache-2.0\n */\n\n/**\n * MCP Manager\n *\n * Manages multiple MCP server connections for a session.\n * Handles connection lifecycle, tool discovery, and tool invocation.\n *\n * @module mcp/manager\n */\n\nimport { Client } from '@modelcontextprotocol/sdk/client/index.js';\nimport { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';\nimport type { ServerCapabilities } from '@modelcontextprotocol/sdk/types.js';\nimport type {\n MCPBlobResourceContents,\n MCPContent,\n MCPResource,\n MCPResourceContents,\n MCPResourceReadResult,\n MCPServerConfig,\n MCPTextResourceContents,\n MCPTool,\n MCPToolCallResult,\n} from './types.js';\n\n/**\n * Represents an active MCP connection with its client and transport.\n */\nexport interface MCPConnection {\n /** The MCP SDK client instance */\n client: Client;\n /** The stdio transport for the connection */\n transport: StdioClientTransport;\n /** Server configuration */\n config: MCPServerConfig;\n /** Whether the connection is active */\n connected: boolean;\n /** Server capabilities from initialization handshake */\n capabilities?: ServerCapabilities;\n /** Error message if the server crashed */\n crashError?: string;\n}\n\n/**\n * Factory functions for creating MCP SDK instances.\n * Used for dependency injection in tests.\n */\nexport interface MCPFactories {\n /** Factory for creating Client instances */\n createClient: (options: { name: string; version: string }) => Client;\n /** Factory for creating StdioClientTransport instances */\n createTransport: (options: {\n command: string;\n args?: string[];\n env?: Record<string, string>\n }) => StdioClientTransport;\n}\n\n/** Default factories using the real MCP SDK */\nconst defaultFactories: MCPFactories = {\n createClient: (options) => new Client(options),\n createTransport: (options) => new StdioClientTransport(options),\n};\n\n/**\n * Manages MCP server connections for a session.\n */\nexport class MCPManager {\n /** Active connections keyed by server ID */\n private connections: Map<string, MCPConnection> = new Map();\n\n /** Map from tool name to server ID for routing tool calls */\n private toolToServer: Map<string, string> = new Map();\n\n /** Callback for server crash notifications */\n private onServerCrash?: (serverId: string, error: string) => void;\n\n /** Factory functions for creating SDK instances (injectable for testing) */\n private factories: MCPFactories;\n\n /**\n * Create a new MCPManager instance.\n *\n * @param factories - Optional factory functions for dependency injection (used in tests)\n */\n constructor(factories?: Partial<MCPFactories>) {\n this.factories = { ...defaultFactories, ...factories };\n }\n\n /**\n * Set a callback to be notified when a server crashes.\n *\n * @param callback - Function to call when a server crashes\n */\n setOnServerCrash(callback: (serverId: string, error: string) => void): void {\n this.onServerCrash = callback;\n }\n\n /**\n * Connect to MCP servers specified in the configuration.\n *\n * @param servers - Array of MCP server configurations\n */\n async connect(servers: MCPServerConfig[]): Promise<void> {\n for (const serverConfig of servers) {\n try {\n // Create stdio transport for subprocess MCP server\n // Use StdioClientTransport\n const transport = this.factories.createTransport({\n command: serverConfig.command,\n args: serverConfig.args,\n env: serverConfig.env,\n });\n\n // Create MCP client\n // Use Client class\n const client = this.factories.createClient({\n name: 'stdio-bus-worker',\n version: '1.0.0',\n });\n\n // Connect client to transport\n // SDK sends initialize and notifications/initialized\n await client.connect(transport);\n\n // Get and store server capabilities\n // Store server capabilities for feature detection\n const capabilities = client.getServerCapabilities();\n\n // Store the connection\n const connection: MCPConnection = {\n client,\n transport,\n config: serverConfig,\n connected: true,\n capabilities,\n };\n this.connections.set(serverConfig.id, connection);\n\n // Set up crash detection via transport close event\n // Detect server process exit\n this.setupCrashDetection(serverConfig.id, client);\n\n // Log successful connection to stderr\n console.error(`[MCP] Connected to server: ${serverConfig.id}`);\n } catch (error) {\n // Handle connection errors gracefully\n console.error(`[MCP] Failed to connect to server ${serverConfig.id}:`, error);\n // Continue with other servers\n }\n }\n }\n\n /**\n * Set up crash detection for an MCP server connection.\n *\n * @param serverId - The server ID\n * @param client - The MCP client\n */\n private setupCrashDetection(serverId: string, client: Client): void {\n // Listen for client close event which indicates server disconnection\n client.onclose = () => {\n const connection = this.connections.get(serverId);\n if (connection && connection.connected) {\n // Mark server as crashed\n connection.connected = false;\n connection.crashError = 'Server process exited unexpectedly';\n\n // Remove tools from this server from the routing map\n for (const [toolName, toolServerId] of this.toolToServer.entries()) {\n if (toolServerId === serverId) {\n this.toolToServer.delete(toolName);\n }\n }\n\n // Log the crash\n console.error(`[MCP] Server ${serverId} crashed: ${connection.crashError}`);\n\n // Notify callback if set\n if (this.onServerCrash) {\n this.onServerCrash(serverId, connection.crashError);\n }\n }\n };\n }\n\n /**\n * Get all available tools from connected MCP servers.\n *\n * @returns Combined list of tools from all connected servers\n */\n async listTools(): Promise<MCPTool[]> {\n const allTools: MCPTool[] = [];\n\n // Clear the toolToServer map before repopulating\n this.toolToServer.clear();\n\n // Iterate through all connected MCP servers\n for (const [serverId, connection] of this.connections) {\n if (!connection.connected) {\n continue;\n }\n\n try {\n // Handle pagination - keep fetching while there's a nextCursor\n let cursor: string | undefined;\n do {\n // Call client.listTools() on each connection\n // Use client.listTools() to discover available tools\n const result = await connection.client.listTools(cursor ? { cursor } : undefined);\n\n // Map the results to MCPTool[] format with serverId included\n // Store tool definitions (name, description, inputSchema)\n for (const tool of result.tools) {\n allTools.push({\n name: tool.name,\n description: tool.description,\n inputSchema: tool.inputSchema as Record<string, unknown>,\n serverId,\n });\n\n // Track which server provides this tool for routing\n this.toolToServer.set(tool.name, serverId);\n }\n\n // Handle pagination if the server returns nextCursor\n cursor = result.nextCursor;\n } while (cursor);\n } catch (error) {\n // Log error but continue with other servers\n console.error(`[MCP] Failed to list tools from server ${serverId}:`, error);\n }\n }\n\n return allTools;\n }\n\n /**\n * Invoke a tool on the appropriate MCP server.\n * - Finds the server that provides the tool\n * - Calls client.callTool({ name, arguments }) on the appropriate connection\n * - Handles CallToolResult response\n * - Checks isError flag for failures\n * - Returns errors for calls to crashed server\n *\n * @param name - The name of the tool to invoke\n * @param args - The arguments to pass to the tool\n * @param serverId - Optional server ID to call the tool on (if known)\n * @returns The tool call result with content and error status\n * @throws Error if tool is not found or server is not connected\n */\n async callTool(name: string, args: Record<string, unknown>, serverId?: string): Promise<MCPToolCallResult> {\n // Determine which server to call\n const targetServerId = serverId ?? this.toolToServer.get(name);\n\n if (!targetServerId) {\n throw new Error(`Tool \"${name}\" not found. Call listTools() first to discover available tools.`);\n }\n\n // Get the connection for the target server\n const connection = this.connections.get(targetServerId);\n\n if (!connection) {\n throw new Error(`Server \"${targetServerId}\" not found.`);\n }\n\n if (!connection.connected) {\n // Return errors for calls to crashed server\n const crashMessage = connection.crashError || 'Server is not connected';\n throw new Error(`Server \"${targetServerId}\" is unavailable: ${crashMessage}`);\n }\n\n try {\n // Call client.callTool() on the appropriate connection\n // Use client.callTool() to invoke tools\n const result = await connection.client.callTool({\n name,\n arguments: args,\n });\n\n // Map the SDK result to our MCPToolCallResult type\n // Use SDK content types from result\n const content: MCPContent[] = (result.content as Array<{ type: string;[key: string]: unknown }>).map((item) => {\n if (item.type === 'text') {\n return {\n type: 'text' as const,\n text: item.text as string,\n };\n } else if (item.type === 'image') {\n return {\n type: 'image' as const,\n data: item.data as string,\n mimeType: item.mimeType as string,\n };\n } else if (item.type === 'resource') {\n const resource = item.resource as { uri: string; mimeType?: string; text?: string; blob?: string };\n return {\n type: 'resource' as const,\n resource: {\n uri: resource.uri,\n mimeType: resource.mimeType,\n text: resource.text,\n blob: resource.blob,\n },\n };\n }\n // Default to text for unknown types\n return {\n type: 'text' as const,\n text: JSON.stringify(item),\n };\n });\n\n // Check CallToolResult.isError for tool failures\n return {\n content,\n isError: result.isError === true,\n };\n } catch (error) {\n // Log error and re-throw\n console.error(`[MCP] Failed to call tool \"${name}\" on server ${targetServerId}:`, error);\n throw error;\n }\n }\n\n /**\n * Get all available resources from connected MCP servers.\n * - Calls client.listResources() to discover available resources\n * - Stores resource definitions (uri, name, description, mimeType)\n * - Handles pagination via nextCursor if present\n *\n * @returns Combined list of resources from all connected servers\n */\n async listResources(): Promise<MCPResource[]> {\n const allResources: MCPResource[] = [];\n\n // Iterate through all connected MCP servers\n for (const [serverId, connection] of this.connections) {\n if (!connection.connected) {\n continue;\n }\n\n try {\n // Handle pagination - keep fetching while there's a nextCursor\n let cursor: string | undefined;\n do {\n // Call client.listResources() on each connection\n // Use client.listResources() to discover resources\n const result = await connection.client.listResources(cursor ? { cursor } : undefined);\n\n // Map the results to MCPResource[] format with serverId included\n for (const resource of result.resources) {\n allResources.push({\n uri: resource.uri,\n name: resource.name,\n description: resource.description,\n mimeType: resource.mimeType,\n serverId,\n });\n }\n\n // Handle pagination if the server returns nextCursor\n cursor = result.nextCursor;\n } while (cursor);\n } catch (error) {\n // Log error but continue with other servers\n console.error(`[MCP] Failed to list resources from server ${serverId}:`, error);\n }\n }\n\n return allResources;\n }\n\n /**\n * Read a resource from the appropriate MCP server.\n * - Calls client.readResource({ uri }) on the appropriate connection\n * - Handles TextResourceContents and BlobResourceContents\n * - Determines which server handles the URI based on resource list\n *\n * @param uri - The URI of the resource to read\n * @param serverId - Optional server ID to read from (if known)\n * @returns The resource contents\n * @throws Error if resource server is not found or not connected\n */\n async readResource(uri: string, serverId?: string): Promise<MCPResourceReadResult> {\n // Determine which server to use\n let targetServerId = serverId;\n\n // If no server ID provided, try to find the server that provides this resource\n if (!targetServerId) {\n // Search through all connected servers' resources to find the one with this URI\n for (const [, connection] of this.connections) {\n if (!connection.connected) {\n continue;\n }\n\n try {\n // Try to read from this server - if it has the resource, it will succeed\n const result = await connection.client.readResource({ uri });\n\n // Map the SDK result to our MCPResourceReadResult type\n const contents: MCPResourceContents[] = result.contents.map((item) => {\n const resourceItem = item as { uri: string; mimeType?: string; text?: string; blob?: string };\n if ('text' in resourceItem && resourceItem.text !== undefined) {\n return {\n uri: resourceItem.uri,\n mimeType: resourceItem.mimeType,\n text: resourceItem.text,\n } as MCPTextResourceContents;\n } else if ('blob' in resourceItem && resourceItem.blob !== undefined) {\n return {\n uri: resourceItem.uri,\n mimeType: resourceItem.mimeType,\n blob: resourceItem.blob,\n } as MCPBlobResourceContents;\n }\n // Default to text with empty content for unknown types\n return {\n uri: resourceItem.uri,\n mimeType: resourceItem.mimeType,\n text: '',\n } as MCPTextResourceContents;\n });\n\n return { contents };\n } catch {\n // This server doesn't have the resource, try the next one\n continue;\n }\n }\n\n throw new Error(`Resource \"${uri}\" not found on any connected server.`);\n }\n\n // Get the connection for the target server\n const connection = this.connections.get(targetServerId);\n\n if (!connection) {\n throw new Error(`Server \"${targetServerId}\" not found.`);\n }\n\n if (!connection.connected) {\n throw new Error(`Server \"${targetServerId}\" is not connected.`);\n }\n\n try {\n // Call client.readResource() on the appropriate connection\n // Use client.readResource() to read resources\n const result = await connection.client.readResource({ uri });\n\n // Map the SDK result to our MCPResourceReadResult type\n // Handle TextResourceContents and BlobResourceContents\n const contents: MCPResourceContents[] = result.contents.map((item) => {\n const resourceItem = item as { uri: string; mimeType?: string; text?: string; blob?: string };\n if ('text' in resourceItem && resourceItem.text !== undefined) {\n return {\n uri: resourceItem.uri,\n mimeType: resourceItem.mimeType,\n text: resourceItem.text,\n } as MCPTextResourceContents;\n } else if ('blob' in resourceItem && resourceItem.blob !== undefined) {\n return {\n uri: resourceItem.uri,\n mimeType: resourceItem.mimeType,\n blob: resourceItem.blob,\n } as MCPBlobResourceContents;\n }\n // Default to text with empty content for unknown types\n return {\n uri: resourceItem.uri,\n mimeType: resourceItem.mimeType,\n text: '',\n } as MCPTextResourceContents;\n });\n\n return { contents };\n } catch (error) {\n // Log error and re-throw\n console.error(`[MCP] Failed to read resource \"${uri}\" from server ${targetServerId}:`, error);\n throw error;\n }\n }\n\n /**\n * Get a connection by server ID.\n *\n * @param serverId - The server ID to look up\n * @returns The connection or undefined if not found\n */\n getConnection(serverId: string): MCPConnection | undefined {\n return this.connections.get(serverId);\n }\n\n /**\n * Get all active connections.\n *\n * @returns Array of all active connections\n */\n getAllConnections(): MCPConnection[] {\n return Array.from(this.connections.values()).filter((conn) => conn.connected);\n }\n\n /**\n * Get server capabilities for a specific server.\n *\n * @param serverId - The server ID to look up\n * @returns The server capabilities or undefined if not found/connected\n */\n getServerCapabilities(serverId: string): ServerCapabilities | undefined {\n const connection = this.connections.get(serverId);\n return connection?.connected ? connection.capabilities : undefined;\n }\n\n /**\n * Close all MCP server connections.\n */\n async close(): Promise<void> {\n for (const connection of this.connections.values()) {\n try {\n await connection.client.close();\n connection.connected = false;\n } catch (error) {\n console.error(`[MCP] Error closing connection ${connection.config.id}:`, error);\n }\n }\n this.connections.clear();\n this.toolToServer.clear();\n }\n\n /**\n * Abort all pending MCP operations.\n * Called when a session is cancelled to stop in-flight requests.\n */\n abortPendingOperations(): void {\n // Mark all connections as not connected to prevent new operations\n for (const connection of this.connections.values()) {\n connection.connected = false;\n }\n }\n\n /**\n * Check if a server has crashed.\n *\n * @param serverId - The server ID to check\n * @returns True if the server has crashed\n */\n isServerCrashed(serverId: string): boolean {\n const connection = this.connections.get(serverId);\n return connection !== undefined && !connection.connected && connection.crashError !== undefined;\n }\n\n /**\n * Get the crash error for a server.\n *\n * @param serverId - The server ID to check\n * @returns The crash error message or undefined if not crashed\n */\n getServerCrashError(serverId: string): string | undefined {\n const connection = this.connections.get(serverId);\n return connection?.crashError;\n }\n\n /**\n * Get all crashed servers.\n *\n * @returns Array of crashed server IDs with their error messages\n */\n getCrashedServers(): Array<{ serverId: string; error: string }> {\n const crashed: Array<{ serverId: string; error: string }> = [];\n for (const [serverId, connection] of this.connections) {\n if (!connection.connected && connection.crashError) {\n crashed.push({ serverId, error: connection.crashError });\n }\n }\n return crashed;\n }\n}\n", "/**\n * Session\n *\n * Represents a single ACP session with its state and MCP connections.\n * Stores sessionId, cwd, MCP connections, and cancellation flag.\n *\n * @module session/session\n */\n\nimport type { HistoryEntry, SessionState } from './types.js';\nimport { MCPManager } from '../mcp/index.js';\n\n/**\n * Factory function type for creating MCPManager instances.\n * Used for dependency injection in tests.\n */\nexport type MCPManagerFactory = () => MCPManager;\n\n/** Default factory using the real MCPManager */\nconst defaultMcpManagerFactory: MCPManagerFactory = () => new MCPManager();\n\n/**\n * Represents an ACP session.\n *\n * The Session class manages:\n * - Session identification (sessionId per requirements)\n * - Working directory context (cwd)\n * - MCP server connections via MCPManager\n * - Cancellation state for aborting operations\n * - Conversation history\n */\nexport class Session {\n /** Unique session identifier (SessionId type per requirements) */\n readonly id: string;\n\n /** Current working directory for the session */\n readonly cwd: string;\n\n /** Manager for MCP server connections */\n readonly mcpManager: MCPManager;\n\n /** Cancellation flag for aborting pending operations */\n private cancelled: boolean = false;\n\n /** Timestamp when the session was created */\n private createdAt: Date;\n\n /** Conversation history for the session */\n private history: HistoryEntry[] = [];\n\n /**\n * Create a new Session.\n *\n * @param id - Unique session identifier\n * @param cwd - Current working directory for the session\n * @param mcpManagerFactory - Optional factory for creating MCPManager (used in tests)\n */\n constructor(id: string, cwd: string, mcpManagerFactory?: MCPManagerFactory) {\n this.id = id;\n this.cwd = cwd;\n this.mcpManager = (mcpManagerFactory ?? defaultMcpManagerFactory)();\n this.createdAt = new Date();\n }\n\n /**\n * Check if the session has been cancelled.\n *\n * @returns true if the session has been cancelled\n */\n isCancelled(): boolean {\n return this.cancelled;\n }\n\n /**\n * Cancel the session.\n * Sets the cancellation flag and aborts pending MCP operations.\n */\n cancel(): void {\n this.cancelled = true;\n // Abort pending MCP operations by closing all connections\n // This will cause any in-flight requests to fail gracefully\n this.mcpManager.abortPendingOperations();\n }\n\n /**\n * Add an entry to the conversation history.\n *\n * @param role - Role of the message sender ('user' or 'agent')\n * @param content - Content of the message\n */\n addHistoryEntry(role: 'user' | 'agent', content: string): void {\n this.history.push({\n role,\n content,\n timestamp: new Date(),\n });\n }\n\n /**\n * Get the conversation history.\n *\n * @returns Array of history entries\n */\n getHistory(): HistoryEntry[] {\n return [...this.history];\n }\n\n /**\n * Clear the conversation history.\n */\n clearHistory(): void {\n this.history = [];\n }\n\n /**\n * Get the session state.\n *\n * @returns Current session state including id, cwd, cancelled flag, and history\n */\n getState(): SessionState {\n return {\n id: this.id,\n cwd: this.cwd,\n cancelled: this.cancelled,\n createdAt: this.createdAt,\n history: [...this.history],\n };\n }\n\n /**\n * Close the session and cleanup resources.\n * Closes all MCP server connections.\n */\n async close(): Promise<void> {\n await this.mcpManager.close();\n }\n}\n", "/**\n * Session Manager\n *\n * Manages the lifecycle of ACP sessions.\n *\n * @module session/manager\n */\n\nimport { type MCPManagerFactory, Session } from './session.js';\nimport type { MCPServerConfig } from '../mcp/types.js';\n\n/**\n * Manages ACP sessions.\n */\nexport class SessionManager {\n private sessions: Map<string, Session> = new Map();\n private mcpManagerFactory?: MCPManagerFactory;\n\n /**\n * Create a new SessionManager.\n *\n * @param mcpManagerFactory - Optional factory for creating MCPManager instances (used in tests)\n */\n constructor(mcpManagerFactory?: MCPManagerFactory) {\n this.mcpManagerFactory = mcpManagerFactory;\n }\n\n /**\n * Create a new session.\n */\n async createSession(cwd: string, mcpServers?: MCPServerConfig[]): Promise<Session> {\n // TODO: Implement in task 21.2\n const id = this.generateSessionId();\n const session = new Session(id, cwd, this.mcpManagerFactory);\n\n if (mcpServers && mcpServers.length > 0) {\n await session.mcpManager.connect(mcpServers);\n }\n\n this.sessions.set(id, session);\n return session;\n }\n\n /**\n * Get a session by ID.\n */\n getSession(id: string): Session | undefined {\n return this.sessions.get(id);\n }\n\n /**\n * Cancel a session.\n */\n cancelSession(id: string): boolean {\n const session = this.sessions.get(id);\n if (session) {\n session.cancel();\n return true;\n }\n return false;\n }\n\n /**\n * Close and remove a session.\n */\n async closeSession(id: string): Promise<boolean> {\n const session = this.sessions.get(id);\n if (session) {\n await session.close();\n this.sessions.delete(id);\n return true;\n }\n return false;\n }\n\n /**\n * Close all sessions.\n */\n async closeAll(): Promise<void> {\n for (const session of this.sessions.values()) {\n await session.close();\n }\n this.sessions.clear();\n }\n\n /**\n * Get all sessions.\n *\n * @returns Array of all sessions\n */\n getAllSessions(): Session[] {\n return Array.from(this.sessions.values());\n }\n\n /**\n * Remove a session from the manager without closing it.\n *\n * @param id - Session ID to remove\n * @returns true if session was removed, false if not found\n */\n removeSession(id: string): boolean {\n return this.sessions.delete(id);\n }\n\n /**\n * Generate a unique session ID using crypto.randomUUID().\n *\n * Generate unique sessionId using UUID\n *\n * @returns A unique UUID string for the session\n */\n private generateSessionId(): string {\n return crypto.randomUUID();\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["connection", "connection"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"pools": [
|
|
3
|
+
{
|
|
4
|
+
"id": "echo-worker",
|
|
5
|
+
"command": "/usr/bin/env",
|
|
6
|
+
"args": [
|
|
7
|
+
"node",
|
|
8
|
+
"./workers-registry/echo-worker/echo-worker.js"
|
|
9
|
+
],
|
|
10
|
+
"instances": 2
|
|
11
|
+
}
|
|
12
|
+
],
|
|
13
|
+
"limits": {
|
|
14
|
+
"max_input_buffer": 1048576,
|
|
15
|
+
"max_output_queue": 4194304,
|
|
16
|
+
"max_restarts": 5,
|
|
17
|
+
"restart_window_sec": 60,
|
|
18
|
+
"drain_timeout_sec": 30,
|
|
19
|
+
"backpressure_timeout_sec": 60
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import readline from"readline";var shuttingDown=false;var rl=readline.createInterface({input:process.stdin,output:process.stdout,terminal:false});function processMessage(line){if(shuttingDown){return}try{const msg=JSON.parse(line);if(msg.id!==void 0&&msg.method!==void 0){const response={jsonrpc:"2.0",id:msg.id,result:{echo:msg.params||{},method:msg.method,timestamp:new Date().toISOString()}};if(msg.sessionId){response.sessionId=msg.sessionId}console.log(JSON.stringify(response))}else if(msg.method!==void 0&&msg.id===void 0){if(msg.sessionId){const notification={jsonrpc:"2.0",method:"echo.notification",params:{original:msg.method,timestamp:new Date().toISOString()},sessionId:msg.sessionId};console.log(JSON.stringify(notification))}}}catch(err){console.error(`[echo-worker] Error parsing message: ${err.message}`);console.error(`[echo-worker] Raw input: ${line.substring(0,100)}...`)}}function handleShutdown(){if(shuttingDown){return}shuttingDown=true;console.error("[echo-worker] Received SIGTERM, shutting down gracefully...");rl.close()}process.on("SIGTERM",handleShutdown);process.on("SIGINT",handleShutdown);rl.on("line",processMessage);rl.on("close",()=>{console.error("[echo-worker] stdin closed, exiting");process.exit(0)});process.on("uncaughtException",err=>{console.error(`[echo-worker] Uncaught exception: ${err.message}`);console.error(err.stack);process.exit(1)});process.on("unhandledRejection",(reason,promise)=>{console.error("[echo-worker] Unhandled promise rejection:",reason);process.exit(1)});console.error("[echo-worker] Started, waiting for NDJSON messages on stdin...");
|
|
3
|
+
//# sourceMappingURL=echo-worker.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../workers-registry/echo-worker/echo-worker.js"],
|
|
4
|
+
"sourcesContent": ["#!/usr/bin/env node\n\n/*\n * Apache License 2.0\n * Copyright (c) 2025\u2013present Raman Marozau, Target Insight Function.\n * Contact: raman@worktif.com\n *\n * This file is part of the stdio bus protocol reference implementation:\n * stdio_bus_kernel_workers (target: <target_stdio_bus_kernel_workers>).\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * SPDX-License-Identifier: Apache-2.0\n */\n\n/**\n * @file echo-worker/echo-worker.js\n * @brief Simple NDJSON echo worker for testing stdio Bus kernel\n *\n * This is a minimal reference implementation demonstrating the worker-to-daemon\n * contract for Agent Transport OS (stdio Bus kernel). It serves as both a functional test\n * worker and documentation of the NDJSON communication protocol.\n *\n * ## NDJSON Communication Contract\n *\n * stdio Bus kernel workers communicate with the daemon via stdin/stdout using NDJSON\n * (Newline-Delimited JSON) format:\n *\n * - **Input (stdin)**: The daemon sends JSON-RPC messages, one per line.\n * Each message is a complete JSON object terminated by a newline (\\n).\n *\n * - **Output (stdout)**: The worker writes JSON-RPC responses, one per line.\n * Each response MUST be a complete JSON object terminated by a newline.\n * The worker MUST NOT write anything else to stdout (no logs, no debug output).\n *\n * - **Errors (stderr)**: All logging, errors, and debug output MUST go to stderr.\n * The daemon does not process stderr; it's for operator visibility only.\n *\n * ## Message Types\n *\n * 1. **Requests**: Have both `id` and `method` fields. MUST receive a response\n * with the same `id`.\n *\n * 2. **Notifications**: Have `method` but no `id`. MUST NOT receive a response.\n * Workers may optionally send notifications back to the client.\n *\n * 3. **Responses**: Have `id` and either `result` or `error`. Sent by workers\n * in reply to requests.\n *\n * ## Session Affinity\n *\n * Messages may include a `sessionId` field for session-based routing:\n * - The daemon routes all messages with the same `sessionId` to the same worker\n * - Workers MUST preserve `sessionId` in responses when present in requests\n * - This enables stateful conversations within a session\n *\n * ## Graceful Shutdown\n *\n * Workers MUST handle SIGTERM for graceful shutdown:\n * - Stop accepting new messages\n * - Complete any in-flight processing\n * - Exit with code 0\n *\n * The daemon sends SIGTERM during shutdown or when restarting workers.\n *\n * @example\n * // Start the echo worker\n * node workers-registry/echo-worker/echo-worker.js\n *\n * // Send a request (from another terminal or via stdio Bus kernel)\n * echo '{\"jsonrpc\":\"2.0\",\"id\":\"1\",\"method\":\"test\",\"params\":{\"foo\":\"bar\"}}' | node workers-registry/echo-worker/echo-worker.js\n *\n * // Expected response:\n * // {\"jsonrpc\":\"2.0\",\"id\":\"1\",\"result\":{\"echo\":{\"foo\":\"bar\"},\"method\":\"test\",\"timestamp\":\"...\"}}\n *\n * @see spec/agent-transport-os.md for the full normative specification\n * @see docs-internal/integration-for-platforms.md for worker implementation guidance\n */\n\nimport readline from 'readline';\n\n/**\n * Flag to track shutdown state.\n * When true, the worker will not process new messages.\n */\nlet shuttingDown = false;\n\n/**\n * Create readline interface for NDJSON processing.\n *\n * The readline module handles line-based input efficiently, buffering\n * partial lines until a complete newline-terminated message is received.\n * This is essential for NDJSON processing where messages may arrive\n * in chunks over the pipe.\n */\nconst rl = readline.createInterface({\n input: process.stdin,\n output: process.stdout,\n terminal: false\n});\n\n/**\n * Process a single JSON-RPC message from stdin.\n *\n * This function implements the core worker contract:\n * 1. Parse the incoming JSON message\n * 2. Determine message type (request vs notification)\n * 3. Generate appropriate response\n * 4. Preserve sessionId for session affinity\n *\n * @param {string} line - Raw JSON line from stdin (without trailing newline)\n *\n * ## Request Handling\n *\n * For requests (messages with both `id` and `method`):\n * - Generate a response with the same `id`\n * - Include `result` object with echoed data\n * - Preserve `sessionId` if present\n *\n * ## Notification Handling\n *\n * For notifications (messages with `method` but no `id`):\n * - Do NOT send a response (per JSON-RPC 2.0 spec)\n * - Optionally send a notification back if sessionId is present\n *\n * ## Error Handling\n *\n * For malformed JSON:\n * - Log error to stderr (never stdout)\n * - Continue processing subsequent messages\n * - Do NOT crash the worker\n */\nfunction processMessage(line) {\n // Skip processing if we're shutting down\n if (shuttingDown) {\n return;\n }\n\n try {\n const msg = JSON.parse(line);\n\n // Request: has both id and method - MUST send response\n if (msg.id !== undefined && msg.method !== undefined) {\n const response = {\n jsonrpc: '2.0',\n id: msg.id,\n result: {\n echo: msg.params || {},\n method: msg.method,\n timestamp: new Date().toISOString()\n }\n };\n\n // Session affinity: preserve sessionId in response\n // This is REQUIRED for session-based routing to work correctly.\n // The daemon uses sessionId to route responses back to the\n // correct client connection.\n if (msg.sessionId) {\n response.sessionId = msg.sessionId;\n }\n\n // Write response as NDJSON (JSON + newline)\n // console.log automatically adds the newline\n console.log(JSON.stringify(response));\n }\n // Notification: has method but no id - MUST NOT send response\n else if (msg.method !== undefined && msg.id === undefined) {\n // Per JSON-RPC 2.0: notifications don't receive responses.\n // However, workers may send their own notifications to clients.\n // If sessionId is present, we demonstrate sending a notification back.\n if (msg.sessionId) {\n const notification = {\n jsonrpc: '2.0',\n method: 'echo.notification',\n params: {\n original: msg.method,\n timestamp: new Date().toISOString()\n },\n sessionId: msg.sessionId\n };\n console.log(JSON.stringify(notification));\n }\n }\n // Response or other message type: ignore\n // Workers typically don't receive responses, but if they do,\n // they should be silently ignored.\n } catch (err) {\n // Log parse errors to stderr (NEVER stdout)\n // stdout is reserved exclusively for NDJSON protocol messages\n console.error(`[echo-worker] Error parsing message: ${err.message}`);\n console.error(`[echo-worker] Raw input: ${line.substring(0, 100)}...`);\n }\n}\n\n/**\n * Handle graceful shutdown on SIGTERM.\n *\n * The stdio Bus kernel daemon sends SIGTERM when:\n * - The daemon itself is shutting down\n * - The worker needs to be restarted (due to crash or configuration change)\n * - The worker pool is being scaled down\n *\n * Proper SIGTERM handling ensures:\n * - No message loss (in-flight messages complete)\n * - Clean process exit (exit code 0)\n * - Resource cleanup (file handles, connections)\n *\n * Workers that don't handle SIGTERM will receive SIGKILL after the\n * daemon's configured drain timeout (default: 30 seconds).\n */\nfunction handleShutdown() {\n if (shuttingDown) {\n return; // Already shutting down\n }\n\n shuttingDown = true;\n console.error('[echo-worker] Received SIGTERM, shutting down gracefully...');\n\n // Close the readline interface to stop accepting new input\n rl.close();\n}\n\n// Register SIGTERM handler for graceful shutdown\nprocess.on('SIGTERM', handleShutdown);\n\n// Also handle SIGINT (Ctrl+C) for development convenience\nprocess.on('SIGINT', handleShutdown);\n\n// Process each line from stdin as an NDJSON message\nrl.on('line', processMessage);\n\n// Handle stdin close (daemon closed the pipe)\nrl.on('close', () => {\n console.error('[echo-worker] stdin closed, exiting');\n process.exit(0);\n});\n\n// Handle uncaught exceptions\n// Log to stderr and exit with error code\n// The daemon will restart the worker based on restart policy\nprocess.on('uncaughtException', (err) => {\n console.error(`[echo-worker] Uncaught exception: ${err.message}`);\n console.error(err.stack);\n process.exit(1);\n});\n\n// Handle unhandled promise rejections\nprocess.on('unhandledRejection', (reason, promise) => {\n console.error('[echo-worker] Unhandled promise rejection:', reason);\n process.exit(1);\n});\n\n// Log startup to stderr (for debugging)\nconsole.error('[echo-worker] Started, waiting for NDJSON messages on stdin...');\n"],
|
|
5
|
+
"mappings": ";AAyFA,OAAO,aAAc,WAMrB,IAAI,aAAe,MAUnB,IAAM,GAAK,SAAS,gBAAgB,CAClC,MAAO,QAAQ,MACf,OAAQ,QAAQ,OAChB,SAAU,KACZ,CAAC,EAiCD,SAAS,eAAe,KAAM,CAE5B,GAAI,aAAc,CAChB,MACF,CAEA,GAAI,CACF,MAAM,IAAM,KAAK,MAAM,IAAI,EAG3B,GAAI,IAAI,KAAO,QAAa,IAAI,SAAW,OAAW,CACpD,MAAM,SAAW,CACf,QAAS,MACT,GAAI,IAAI,GACR,OAAQ,CACN,KAAM,IAAI,QAAU,CAAC,EACrB,OAAQ,IAAI,OACZ,UAAW,IAAI,KAAK,EAAE,YAAY,CACpC,CACF,EAMA,GAAI,IAAI,UAAW,CACjB,SAAS,UAAY,IAAI,SAC3B,CAIA,QAAQ,IAAI,KAAK,UAAU,QAAQ,CAAC,CACtC,SAES,IAAI,SAAW,QAAa,IAAI,KAAO,OAAW,CAIzD,GAAI,IAAI,UAAW,CACjB,MAAM,aAAe,CACnB,QAAS,MACT,OAAQ,oBACR,OAAQ,CACN,SAAU,IAAI,OACd,UAAW,IAAI,KAAK,EAAE,YAAY,CACpC,EACA,UAAW,IAAI,SACjB,EACA,QAAQ,IAAI,KAAK,UAAU,YAAY,CAAC,CAC1C,CACF,CAIF,OAAS,IAAK,CAGZ,QAAQ,MAAM,wCAAwC,IAAI,OAAO,EAAE,EACnE,QAAQ,MAAM,4BAA4B,KAAK,UAAU,EAAG,GAAG,CAAC,KAAK,CACvE,CACF,CAkBA,SAAS,gBAAiB,CACxB,GAAI,aAAc,CAChB,MACF,CAEA,aAAe,KACf,QAAQ,MAAM,6DAA6D,EAG3E,GAAG,MAAM,CACX,CAGA,QAAQ,GAAG,UAAW,cAAc,EAGpC,QAAQ,GAAG,SAAU,cAAc,EAGnC,GAAG,GAAG,OAAQ,cAAc,EAG5B,GAAG,GAAG,QAAS,IAAM,CACnB,QAAQ,MAAM,qCAAqC,EACnD,QAAQ,KAAK,CAAC,CAChB,CAAC,EAKD,QAAQ,GAAG,oBAAsB,KAAQ,CACvC,QAAQ,MAAM,qCAAqC,IAAI,OAAO,EAAE,EAChE,QAAQ,MAAM,IAAI,KAAK,EACvB,QAAQ,KAAK,CAAC,CAChB,CAAC,EAGD,QAAQ,GAAG,qBAAsB,CAAC,OAAQ,UAAY,CACpD,QAAQ,MAAM,6CAA8C,MAAM,EAClE,QAAQ,KAAK,CAAC,CAChB,CAAC,EAGD,QAAQ,MAAM,gEAAgE",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Auto-generated by build script
|
|
2
|
+
export interface WorkerInfo {
|
|
3
|
+
entrypoint: string;
|
|
4
|
+
types: string | null;
|
|
5
|
+
config: string | null;
|
|
6
|
+
type: 'typescript' | 'javascript';
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const workers: {
|
|
10
|
+
'acp-registry': WorkerInfo;
|
|
11
|
+
'acp-worker': WorkerInfo;
|
|
12
|
+
'echo-worker': WorkerInfo;
|
|
13
|
+
'mcp-echo-server': WorkerInfo;
|
|
14
|
+
'mcp-to-acp-proxy': WorkerInfo;
|
|
15
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Auto-generated by build script
|
|
2
|
+
export const workers = {
|
|
3
|
+
"acp-registry": {
|
|
4
|
+
"entrypoint": "./acp-registry/registry-launcher-client.js",
|
|
5
|
+
"types": null,
|
|
6
|
+
"config": "./acp-registry/registry-launcher-config.json",
|
|
7
|
+
"type": "javascript"
|
|
8
|
+
},
|
|
9
|
+
"acp-worker": {
|
|
10
|
+
"entrypoint": "./acp-worker/index.js",
|
|
11
|
+
"types": "../tsc/workers/acp-worker/src/index.d.ts",
|
|
12
|
+
"config": null,
|
|
13
|
+
"type": "typescript"
|
|
14
|
+
},
|
|
15
|
+
"echo-worker": {
|
|
16
|
+
"entrypoint": "./echo-worker/echo-worker.js",
|
|
17
|
+
"types": null,
|
|
18
|
+
"config": "./echo-worker/echo-worker-config.json",
|
|
19
|
+
"type": "javascript"
|
|
20
|
+
},
|
|
21
|
+
"mcp-echo-server": {
|
|
22
|
+
"entrypoint": "./mcp-echo-server/index.js",
|
|
23
|
+
"types": "../tsc/workers/mcp-echo-server/mcp-echo-server.d.ts",
|
|
24
|
+
"config": "./mcp-echo-server/mcp-echo-server-config.json",
|
|
25
|
+
"type": "typescript"
|
|
26
|
+
},
|
|
27
|
+
"mcp-to-acp-proxy": {
|
|
28
|
+
"entrypoint": "./mcp-to-acp-proxy/proxy.js",
|
|
29
|
+
"types": null,
|
|
30
|
+
"config": null,
|
|
31
|
+
"type": "javascript"
|
|
32
|
+
}
|
|
33
|
+
};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import{Server}from"@modelcontextprotocol/sdk/server/index.js";import{StdioServerTransport}from"@modelcontextprotocol/sdk/server/stdio.js";import{CallToolRequestSchema,ListResourcesRequestSchema,ListToolsRequestSchema,ReadResourceRequestSchema}from"@modelcontextprotocol/sdk/types.js";var server=new Server({name:"mcp-echo-server",version:"1.0.0"},{capabilities:{tools:{},resources:{}}});var tools=[{name:"echo",description:"Echoes the input text back",inputSchema:{type:"object",properties:{text:{type:"string",description:"Text to echo"}},required:["text"]}},{name:"reverse",description:"Reverses the input text",inputSchema:{type:"object",properties:{text:{type:"string",description:"Text to reverse"}},required:["text"]}},{name:"uppercase",description:"Converts input text to uppercase",inputSchema:{type:"object",properties:{text:{type:"string",description:"Text to convert"}},required:["text"]}},{name:"delay",description:"Echoes text after a delay (for testing cancellation)",inputSchema:{type:"object",properties:{text:{type:"string",description:"Text to echo"},ms:{type:"number",description:"Delay in milliseconds"}},required:["text","ms"]}},{name:"error",description:"Always returns an error (for testing error handling)",inputSchema:{type:"object",properties:{message:{type:"string",description:"Error message to return"}},required:["message"]}}];var resources=[{uri:"test://greeting",name:"Greeting",description:"A simple greeting message",mimeType:"text/plain"},{uri:"test://config",name:"Test Config",description:"Test configuration in JSON format",mimeType:"application/json"}];server.setRequestHandler(ListToolsRequestSchema,async()=>{return{tools}});server.setRequestHandler(CallToolRequestSchema,async request=>{const{name,arguments:args}=request.params;switch(name){case"echo":return{content:[{type:"text",text:String(args?.text??"")}]};case"reverse":const text=String(args?.text??"");return{content:[{type:"text",text:text.split("").reverse().join("")}]};case"uppercase":return{content:[{type:"text",text:String(args?.text??"").toUpperCase()}]};case"delay":const delayMs=Number(args?.ms??1e3);await new Promise(resolve=>setTimeout(resolve,delayMs));return{content:[{type:"text",text:String(args?.text??"")}]};case"error":return{content:[{type:"text",text:String(args?.message??"Error occurred")}],isError:true};default:throw new Error(`Unknown tool: ${name}`)}});server.setRequestHandler(ListResourcesRequestSchema,async()=>{return{resources}});server.setRequestHandler(ReadResourceRequestSchema,async request=>{const{uri}=request.params;switch(uri){case"test://greeting":return{contents:[{uri,mimeType:"text/plain",text:"Hello from MCP Echo Server!"}]};case"test://config":return{contents:[{uri,mimeType:"application/json",text:JSON.stringify({version:"1.0.0",features:["echo","reverse","uppercase"],testMode:true})}]};default:throw new Error(`Unknown resource: ${uri}`)}});async function main(){const transport=new StdioServerTransport;await server.connect(transport);console.error("[MCP Echo Server] Started")}main().catch(error=>{console.error("[MCP Echo Server] Fatal error:",error);process.exit(1)});
|
|
3
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../workers-registry/mcp-echo-server/mcp-echo-server.ts"],
|
|
4
|
+
"sourcesContent": ["#!/usr/bin/env node\n\n/*\n * Apache License 2.0\n * Copyright (c) 2025\u2013present Raman Marozau, Target Insight Function.\n * Contact: raman@worktif.com\n *\n * This file is part of the stdio bus protocol reference implementation:\n * stdio_bus_kernel_workers (target: <target_stdio_bus_kernel_workers>).\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * SPDX-License-Identifier: Apache-2.0\n */\n\n/**\n * MCP Echo Server for Testing\n *\n * A simple MCP server that provides echo tools and test resources\n * for integration testing with the stdio Bus kernel worker.\n *\n * @module workers-registry/mcp-echo-server\n */\n\nimport { Server } from '@modelcontextprotocol/sdk/server/index.js';\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';\nimport {\n CallToolRequestSchema,\n ListResourcesRequestSchema,\n ListToolsRequestSchema,\n ReadResourceRequestSchema,\n} from '@modelcontextprotocol/sdk/types.js';\n\n// Create MCP server instance\nconst server = new Server(\n {\n name: 'mcp-echo-server',\n version: '1.0.0',\n },\n {\n capabilities: {\n tools: {},\n resources: {},\n },\n },\n);\n\n// Define available tools\nconst tools = [\n {\n name: 'echo',\n description: 'Echoes the input text back',\n inputSchema: {\n type: 'object' as const,\n properties: {\n text: {\n type: 'string',\n description: 'Text to echo',\n },\n },\n required: ['text'],\n },\n },\n {\n name: 'reverse',\n description: 'Reverses the input text',\n inputSchema: {\n type: 'object' as const,\n properties: {\n text: {\n type: 'string',\n description: 'Text to reverse',\n },\n },\n required: ['text'],\n },\n },\n {\n name: 'uppercase',\n description: 'Converts input text to uppercase',\n inputSchema: {\n type: 'object' as const,\n properties: {\n text: {\n type: 'string',\n description: 'Text to convert',\n },\n },\n required: ['text'],\n },\n },\n {\n name: 'delay',\n description: 'Echoes text after a delay (for testing cancellation)',\n inputSchema: {\n type: 'object' as const,\n properties: {\n text: {\n type: 'string',\n description: 'Text to echo',\n },\n ms: {\n type: 'number',\n description: 'Delay in milliseconds',\n },\n },\n required: ['text', 'ms'],\n },\n },\n {\n name: 'error',\n description: 'Always returns an error (for testing error handling)',\n inputSchema: {\n type: 'object' as const,\n properties: {\n message: {\n type: 'string',\n description: 'Error message to return',\n },\n },\n required: ['message'],\n },\n },\n];\n\n// Define available resources\nconst resources = [\n {\n uri: 'test://greeting',\n name: 'Greeting',\n description: 'A simple greeting message',\n mimeType: 'text/plain',\n },\n {\n uri: 'test://config',\n name: 'Test Config',\n description: 'Test configuration in JSON format',\n mimeType: 'application/json',\n },\n];\n\n// Handle tools/list request\nserver.setRequestHandler(ListToolsRequestSchema, async () => {\n return { tools };\n});\n\n// Handle tools/call request\nserver.setRequestHandler(CallToolRequestSchema, async (request) => {\n const { name, arguments: args } = request.params as { name: string; arguments?: Record<string, unknown> };\n\n switch (name) {\n case 'echo':\n return {\n content: [{ type: 'text', text: String(args?.text ?? '') }],\n };\n\n case 'reverse':\n const text = String(args?.text ?? '');\n return {\n content: [{ type: 'text', text: text.split('').reverse().join('') }],\n };\n\n case 'uppercase':\n return {\n content: [{ type: 'text', text: String(args?.text ?? '').toUpperCase() }],\n };\n\n case 'delay':\n const delayMs = Number(args?.ms ?? 1000);\n await new Promise((resolve) => setTimeout(resolve, delayMs));\n return {\n content: [{ type: 'text', text: String(args?.text ?? '') }],\n };\n\n case 'error':\n return {\n content: [{ type: 'text', text: String(args?.message ?? 'Error occurred') }],\n isError: true,\n };\n\n default:\n throw new Error(`Unknown tool: ${name}`);\n }\n});\n\n// Handle resources/list request\nserver.setRequestHandler(ListResourcesRequestSchema, async () => {\n return { resources };\n});\n\n// Handle resources/read request\nserver.setRequestHandler(ReadResourceRequestSchema, async (request) => {\n const { uri } = request.params as { uri: string };\n\n switch (uri) {\n case 'test://greeting':\n return {\n contents: [\n {\n uri,\n mimeType: 'text/plain',\n text: 'Hello from MCP Echo Server!',\n },\n ],\n };\n\n case 'test://config':\n return {\n contents: [\n {\n uri,\n mimeType: 'application/json',\n text: JSON.stringify({\n version: '1.0.0',\n features: ['echo', 'reverse', 'uppercase'],\n testMode: true,\n }),\n },\n ],\n };\n\n default:\n throw new Error(`Unknown resource: ${uri}`);\n }\n});\n\n// Start the server\nasync function main() {\n const transport = new StdioServerTransport();\n await server.connect(transport);\n console.error('[MCP Echo Server] Started');\n}\n\nmain().catch((error) => {\n console.error('[MCP Echo Server] Fatal error:', error);\n process.exit(1);\n});\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"pools": [
|
|
3
|
+
{
|
|
4
|
+
"id": "mcp-echo-server",
|
|
5
|
+
"command": "/usr/bin/env",
|
|
6
|
+
"args": [
|
|
7
|
+
"node",
|
|
8
|
+
"./workers-registry/mcp-echo-server/dist/mcp-echo-server.js"
|
|
9
|
+
],
|
|
10
|
+
"instances": 1
|
|
11
|
+
}
|
|
12
|
+
],
|
|
13
|
+
"limits": {
|
|
14
|
+
"max_input_buffer": 1048576,
|
|
15
|
+
"max_output_queue": 4194304,
|
|
16
|
+
"max_restarts": 5,
|
|
17
|
+
"restart_window_sec": 60,
|
|
18
|
+
"drain_timeout_sec": 30,
|
|
19
|
+
"backpressure_timeout_sec": 60
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import net from"net";import{createInterface}from"readline";var ACP_HOST=process.env.ACP_HOST||"127.0.0.1";var ACP_PORT=process.env.ACP_PORT||9e3;var AGENT_ID=process.env.AGENT_ID||"default-agent";console.error(`[MCP-APC][proxy] Starting proxy...`);console.error(`[MCP-APC][proxy] Target: ${ACP_HOST}:${ACP_PORT}`);console.error(`[MCP-APC][proxy] Agent ID: ${AGENT_ID}`);var acpSocket=net.connect(ACP_PORT,ACP_HOST);var acpConnected=false;var proxySessionId=null;var acpSessionId=null;var pendingRequests=new Map;var accumulatedText=new Map;acpSocket.on("connect",()=>{console.error("[MCP-APC][proxy] Connected to ACP stdio Bus");acpConnected=true});acpSocket.on("error",err=>{console.error(`[MCP-APC][proxy] ACP connection error: ${err.message}`);process.exit(1)});var acpBuffer="";acpSocket.on("data",data=>{acpBuffer+=data.toString();let newlineIndex;while((newlineIndex=acpBuffer.indexOf("\n"))!==-1){const line=acpBuffer.slice(0,newlineIndex);acpBuffer=acpBuffer.slice(newlineIndex+1);if(line.trim()){try{const acpMsg=JSON.parse(line);console.error(`[MCP-APC][proxy] \u2190 ACP: ${JSON.stringify(acpMsg)}`);if(acpMsg.id===void 0||acpMsg.id===null){console.error(`[MCP-APC][proxy] Processing notification: ${acpMsg.method}`);handleACPNotification(acpMsg)}else{console.error(`[MCP-APC][proxy] Processing response id=${acpMsg.id}`);console.error(`[MCP-APC][proxy] Pending: ${JSON.stringify([...pendingRequests.keys()])}`);const mcpResponse=convertACPtoMCP(acpMsg);if(mcpResponse){console.error(`[MCP-APC][proxy] \u2192 MCP: ${JSON.stringify(mcpResponse)}`);process.stdout.write(JSON.stringify(mcpResponse)+"\n")}else{console.error(`[MCP-APC][proxy] WARNING: No MCP response for id=${acpMsg.id}`)}}}catch(err){console.error(`[MCP-APC][proxy] Error parsing ACP: ${err.message}`)}}}});var rl=createInterface({input:process.stdin,terminal:false});rl.on("line",line=>{if(!line.trim())return;try{const mcpReq=JSON.parse(line);console.error(`[MCP-APC][proxy] \u2190 MCP: ${JSON.stringify(mcpReq)}`);const acpReq=convertMCPtoACP(mcpReq);if(acpReq){console.error(`[MCP-APC][proxy] \u2192 ACP: ${JSON.stringify(acpReq)}`);if(acpConnected){acpSocket.write(JSON.stringify(acpReq)+"\n")}}}catch(err){console.error(`[MCP-APC][proxy] Error parsing MCP: ${err.message}`)}});function handleACPNotification(msg){const{method,params}=msg;if(method==="session/update"&¶ms?.update){const update=params.update;for(const[reqId,pending]of pendingRequests.entries()){if(pending.method==="session/prompt"){if(update.sessionUpdate==="agent_message_chunk"&&update.content?.text){if(!accumulatedText.has(reqId)){accumulatedText.set(reqId,"")}accumulatedText.set(reqId,accumulatedText.get(reqId)+update.content.text)}break}}}}function convertMCPtoACP(mcpReq){const{id,method,params}=mcpReq;if(id===void 0||id===null){return null}pendingRequests.set(id,{method,params});if(!proxySessionId){proxySessionId=`proxy-${Date.now()}`}switch(method){case"initialize":return{jsonrpc:"2.0",id,method:"initialize",agentId:AGENT_ID,sessionId:proxySessionId,params:{protocolVersion:1,clientCapabilities:params?.capabilities||{},clientInfo:params?.clientInfo||{name:"mcp-proxy",version:"1.0.0"}}};case"tools/list":sendMCP({jsonrpc:"2.0",id,result:{tools:[{name:"acp_prompt",description:`Send prompt to ${AGENT_ID}`,inputSchema:{type:"object",properties:{prompt:{type:"string",description:"Prompt text"}},required:["prompt"]}}]}});pendingRequests.delete(id);return null;case"tools/call":const promptText=params?.arguments?.prompt||"";if(!acpSessionId){const sessionReqId=`sess-${id}`;pendingRequests.set(sessionReqId,{method:"session/new",originalId:id,promptText});return{jsonrpc:"2.0",id:sessionReqId,method:"session/new",agentId:AGENT_ID,sessionId:proxySessionId,params:{cwd:process.cwd(),mcpServers:[]}}}pendingRequests.set(id,{method:"session/prompt",params});return{jsonrpc:"2.0",id,method:"session/prompt",agentId:AGENT_ID,sessionId:proxySessionId,params:{sessionId:acpSessionId,prompt:[{type:"text",text:promptText}]}};case"resources/list":sendMCP({jsonrpc:"2.0",id,result:{resources:[]}});pendingRequests.delete(id);return null;case"resources/templates/list":sendMCP({jsonrpc:"2.0",id,result:{resourceTemplates:[]}});pendingRequests.delete(id);return null;case"prompts/list":sendMCP({jsonrpc:"2.0",id,result:{prompts:[]}});pendingRequests.delete(id);return null;default:sendMCP({jsonrpc:"2.0",id,error:{code:-32601,message:`Unknown method: ${method}`}});pendingRequests.delete(id);return null}}function convertACPtoMCP(acpResp){const{id,result,error}=acpResp;const pending=pendingRequests.get(id);if(!pending){console.error(`[MCP-APC][proxy] ERROR: No pending request for id=${id}`);return null}console.error(`[MCP-APC][proxy] Converting ACP->MCP for method: ${pending.method}`);pendingRequests.delete(id);if(error){accumulatedText.delete(id);return{jsonrpc:"2.0",id,error:{code:error.code||-32603,message:error.message||"ACP error"}}}switch(pending.method){case"initialize":return{jsonrpc:"2.0",id,result:{protocolVersion:"2024-11-05",capabilities:{tools:{},resources:{}},serverInfo:result?.agentInfo||{name:"acp-agent",version:"1.0.0"}}};case"session/new":acpSessionId=result?.sessionId;console.error(`[MCP-APC][proxy] ACP session: ${acpSessionId}`);if(pending.originalId&&pending.promptText){const promptReq={jsonrpc:"2.0",id:pending.originalId,method:"session/prompt",agentId:AGENT_ID,sessionId:proxySessionId,params:{sessionId:acpSessionId,prompt:[{type:"text",text:pending.promptText}]}};pendingRequests.set(pending.originalId,{method:"session/prompt"});console.error(`[MCP-APC][proxy] \u2192 ACP: ${JSON.stringify(promptReq)}`);if(acpConnected){acpSocket.write(JSON.stringify(promptReq)+"\n")}}return null;case"session/prompt":const text=accumulatedText.get(id)||"";accumulatedText.delete(id);console.error(`[MCP-APC][proxy] Returning accumulated text (${text.length} chars): "${text.substring(0,50)}..."`);return{jsonrpc:"2.0",id,result:{content:[{type:"text",text:text||"No response"}]}};default:console.error(`[MCP-APC][proxy] WARNING: Unhandled method ${pending.method}, returning raw result`);return{jsonrpc:"2.0",id,result:result||{}}}}function sendMCP(msg){console.error(`[MCP-APC][proxy] \u2192 MCP: ${JSON.stringify(msg)}`);process.stdout.write(JSON.stringify(msg)+"\n")}process.on("SIGTERM",()=>{acpSocket.end();process.exit(0)});process.on("SIGINT",()=>{acpSocket.end();process.exit(0)});
|
|
3
|
+
//# sourceMappingURL=proxy.js.map
|