├── .gitignore ├── tsconfig.json ├── dockerfile ├── package.json ├── agent-manager.ts ├── config.ts ├── readme.md ├── plan.md ├── index.ts └── a2a-client.ts /.gitignore: -------------------------------------------------------------------------------- 1 | /dist 2 | /node_modules 3 | /build 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "NodeNext", 5 | "moduleResolution": "NodeNext", 6 | "outDir": "./dist", 7 | "rootDir": ".", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "resolveJsonModule": true 13 | }, 14 | "include": ["./**/*.ts"], 15 | "exclude": ["node_modules", "dist"] 16 | } 17 | -------------------------------------------------------------------------------- /dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:22.12-alpine AS builder 2 | 3 | WORKDIR /app 4 | 5 | COPY package.json package-lock.json* ./ 6 | COPY tsconfig.json ./ 7 | COPY *.ts ./ 8 | 9 | RUN npm install 10 | RUN npm run build 11 | 12 | FROM node:22-alpine AS release 13 | 14 | WORKDIR /app 15 | 16 | COPY --from=builder /app/dist /app/dist 17 | COPY --from=builder /app/package.json /app/package.json 18 | COPY --from=builder /app/package-lock.json /app/package-lock.json 19 | 20 | ENV NODE_ENV=production 21 | 22 | RUN npm ci --ignore-scripts --omit=dev 23 | 24 | ENTRYPOINT ["node", "dist/index.js"] 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "a2a-client-mcp-server", 3 | "version": "0.1.0", 4 | "description": "MCP server that acts as an A2A client", 5 | "license": "MIT", 6 | "type": "module", 7 | "bin": { 8 | "a2a-client-mcp-server": "dist/index.js" 9 | }, 10 | "files": [ 11 | "dist" 12 | ], 13 | "scripts": { 14 | "build": "tsc && shx chmod +x dist/*.js", 15 | "prepare": "npm run build", 16 | "watch": "tsc --watch", 17 | "start": "node dist/index.js" 18 | }, 19 | "dependencies": { 20 | "@modelcontextprotocol/sdk": "^1.0.1" 21 | }, 22 | "devDependencies": { 23 | "@types/node": "^22", 24 | "shx": "^0.3.4", 25 | "typescript": "^5.6.2" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /agent-manager.ts: -------------------------------------------------------------------------------- 1 | import { A2AClient } from './a2a-client.js'; 2 | import { AgentConfig, AgentEndpoint } from './config.js'; 3 | 4 | export class AgentManager { 5 | private clients: Map; 6 | private config: AgentConfig; 7 | 8 | constructor() { 9 | this.clients = new Map(); 10 | this.config = new AgentConfig(); 11 | } 12 | 13 | async initialize(): Promise { 14 | const endpoints = this.config.getEndpoints(); 15 | 16 | for (const endpoint of endpoints) { 17 | try { 18 | const client = new A2AClient(endpoint.url); 19 | // 接続テスト 20 | await client.agentCard(); 21 | this.clients.set(endpoint.id, client); 22 | console.error(`Successfully connected to agent ${endpoint.id} at ${endpoint.url}`); 23 | } catch (error) { 24 | console.error(`Failed to connect to agent ${endpoint.id} at ${endpoint.url}:`, error); 25 | } 26 | } 27 | } 28 | 29 | getClientById(id: string): A2AClient | undefined { 30 | return this.clients.get(id); 31 | } 32 | 33 | getAllClients(): Map { 34 | return this.clients; 35 | } 36 | 37 | getEndpoints(): AgentEndpoint[] { 38 | return this.config.getEndpoints(); 39 | } 40 | } -------------------------------------------------------------------------------- /config.ts: -------------------------------------------------------------------------------- 1 | import { randomUUID } from 'crypto'; 2 | 3 | export interface AgentEndpoint { 4 | id: string; // UUID 5 | url: string; 6 | } 7 | 8 | export class AgentConfig { 9 | private endpoints: AgentEndpoint[] = []; 10 | 11 | constructor() { 12 | this.loadEndpoints(); 13 | } 14 | 15 | private loadEndpoints(): void { 16 | const endpointsStr = process.env.A2A_ENDPOINT_URLS; 17 | if (!endpointsStr) { 18 | // 後方互換性のため、単一のエンドポイントもサポート 19 | const singleEndpoint = process.env.A2A_ENDPOINT_URL; 20 | if (singleEndpoint) { 21 | this.endpoints = [{ 22 | id: randomUUID(), 23 | url: singleEndpoint 24 | }]; 25 | } 26 | return; 27 | } 28 | 29 | try { 30 | this.endpoints = endpointsStr.split(',').map(url => ({ 31 | id: randomUUID(), 32 | url: url.trim() 33 | })); 34 | } catch (error) { 35 | console.error('Failed to parse A2A_ENDPOINT_URLS:', error); 36 | this.endpoints = []; 37 | } 38 | } 39 | 40 | getEndpoints(): AgentEndpoint[] { 41 | return this.endpoints; 42 | } 43 | 44 | getEndpointById(id: string): AgentEndpoint | undefined { 45 | return this.endpoints.find(endpoint => endpoint.id === id); 46 | } 47 | 48 | getEndpointByUrl(url: string): AgentEndpoint | undefined { 49 | return this.endpoints.find(endpoint => endpoint.url === url); 50 | } 51 | } -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # A2A Client MCP Server 2 | 3 | An MCP server that acts as a client to the Agent-to-Agent (A2A) protocol, allowing LLMs to interact with A2A agents through the Model Context Protocol (MCP). 4 | 5 | ## Features 6 | 7 | - Connect to any A2A-compatible agent 8 | - Send and receive messages 9 | - Track and manage tasks 10 | - Support for streaming responses 11 | - Query agent capabilities and metadata 12 | 13 | ## Installation 14 | 15 | ```bash 16 | # Install globally 17 | npm install -g a2a-client-mcp-server 18 | 19 | # Or run directly with npx 20 | npx a2a-client-mcp-server 21 | ``` 22 | 23 | ## Configuration 24 | 25 | ### Environment Variables 26 | 27 | - `A2A_ENDPOINT_URL`: URL of the A2A agent to connect to (default: "http://localhost:41241") 28 | 29 | ## Usage with Claude Desktop 30 | 31 | Add this to your `claude_desktop_config.json`: 32 | 33 | ### NPX 34 | 35 | ```bash 36 | npm run build 37 | npm link 38 | ``` 39 | 40 | ```json 41 | { 42 | "mcpServers": { 43 | "a2a-client": { 44 | "command": "npx", 45 | "args": ["-y", "a2a-client-mcp-server"], 46 | "env": { 47 | "A2A_ENDPOINT_URL": "http://localhost:41241" 48 | } 49 | } 50 | } 51 | } 52 | ``` 53 | 54 | ### Docker 55 | 56 | Build the Docker image: 57 | 58 | ```bash 59 | docker build -t a2a-client-mcp-server . 60 | ``` 61 | 62 | Configure Claude Desktop: 63 | 64 | ```json 65 | { 66 | "mcpServers": { 67 | "a2a-client": { 68 | "command": "docker", 69 | "args": [ 70 | "run", 71 | "-i", 72 | "--rm", 73 | "-e", 74 | "A2A_ENDPOINT_URL", 75 | "a2a-client-mcp-server" 76 | ], 77 | "env": { 78 | "A2A_ENDPOINT_URL": "http://localhost:41241" 79 | } 80 | } 81 | } 82 | } 83 | ``` 84 | 85 | ## Available Tools 86 | 87 | ### a2a_send_task 88 | Send a task to an A2A agent 89 | - `message` (string): Message to send to the agent 90 | - `taskId` (string, optional): Task ID (generated if not provided) 91 | 92 | ### a2a_get_task 93 | Get the current state of a task 94 | - `taskId` (string): ID of the task to retrieve 95 | 96 | ### a2a_cancel_task 97 | Cancel a running task 98 | - `taskId` (string): ID of the task to cancel 99 | 100 | ### a2a_send_task_subscribe 101 | Send a task and subscribe to updates (streaming) 102 | - `message` (string): Message to send to the agent 103 | - `taskId` (string, optional): Task ID (generated if not provided) 104 | - `maxUpdates` (number, optional): Maximum updates to receive (default: 10) 105 | 106 | ### a2a_agent_info 107 | Get information about the connected A2A agent 108 | - No parameters required 109 | 110 | ## Resources 111 | 112 | The server provides access to two MCP resources: 113 | 114 | - `a2a://agent-card`: Information about the connected A2A agent 115 | - `a2a://tasks`: List of recent A2A tasks 116 | 117 | ## Example Usage 118 | 119 | This example shows how to use A2A Client MCP Server to interact with a Coder Agent: 120 | 121 | ``` 122 | First, let me explore what A2A agent we're connected to. 123 | 124 | I'll use the a2a_agent_info tool to check the agent details. 125 | 126 | The agent provides a coding service that can generate files based on natural language instructions. Let's create a simple Python script. 127 | 128 | I'll use the a2a_send_task tool to send a request: 129 | 130 | Task: "Create a Python function that calculates the Fibonacci sequence" 131 | 132 | Now I can check the task status using a2a_get_task with the task ID from the previous response. 133 | 134 | The agent has created the requested Python code. I can now retrieve and use this code in my project. 135 | ``` 136 | 137 | ## Development 138 | 139 | ```bash 140 | # Install dependencies 141 | npm install 142 | 143 | # Build the project 144 | npm run build 145 | 146 | # Run in development mode 147 | npm run watch 148 | ``` 149 | 150 | ## License 151 | 152 | MIT 153 | -------------------------------------------------------------------------------- /plan.md: -------------------------------------------------------------------------------- 1 | # 複数エージェント対応計画 2 | 3 | ## 1. 環境変数の形式変更 4 | 5 | ### 現在の形式 6 | ```bash 7 | A2A_ENDPOINT_URL=http://localhost:41241 8 | ``` 9 | 10 | ### 新しい形式 11 | ```bash 12 | A2A_ENDPOINT_URLS=http://localhost:41241,http://localhost:41242,http://localhost:41243 13 | ``` 14 | 15 | ## 2. 実装コード 16 | 17 | ### 設定管理クラス 18 | ```typescript 19 | // config.ts 20 | import { randomUUID } from 'crypto'; 21 | 22 | export interface AgentEndpoint { 23 | id: string; // UUID 24 | url: string; 25 | } 26 | 27 | export class AgentConfig { 28 | private endpoints: AgentEndpoint[] = []; 29 | 30 | constructor() { 31 | this.loadEndpoints(); 32 | } 33 | 34 | private loadEndpoints(): void { 35 | const endpointsStr = process.env.A2A_ENDPOINT_URLS; 36 | if (!endpointsStr) { 37 | // 後方互換性のため、単一のエンドポイントもサポート 38 | const singleEndpoint = process.env.A2A_ENDPOINT_URL; 39 | if (singleEndpoint) { 40 | this.endpoints = [{ 41 | id: randomUUID(), 42 | url: singleEndpoint 43 | }]; 44 | } 45 | return; 46 | } 47 | 48 | try { 49 | this.endpoints = endpointsStr.split(',').map(url => ({ 50 | id: randomUUID(), 51 | url: url.trim() 52 | })); 53 | } catch (error) { 54 | console.error('Failed to parse A2A_ENDPOINT_URLS:', error); 55 | this.endpoints = []; 56 | } 57 | } 58 | 59 | getEndpoints(): AgentEndpoint[] { 60 | return this.endpoints; 61 | } 62 | 63 | getEndpointById(id: string): AgentEndpoint | undefined { 64 | return this.endpoints.find(endpoint => endpoint.id === id); 65 | } 66 | 67 | getEndpointByUrl(url: string): AgentEndpoint | undefined { 68 | return this.endpoints.find(endpoint => endpoint.url === url); 69 | } 70 | } 71 | ``` 72 | 73 | ### エージェントマネージャー 74 | ```typescript 75 | // agent-manager.ts 76 | export class AgentManager { 77 | private clients: Map; 78 | private config: AgentConfig; 79 | 80 | constructor() { 81 | this.clients = new Map(); 82 | this.config = new AgentConfig(); 83 | this.initializeClients(); 84 | } 85 | 86 | private async initializeClients(): Promise { 87 | const endpoints = this.config.getEndpoints(); 88 | 89 | for (const endpoint of endpoints) { 90 | try { 91 | const client = new A2AClient(endpoint.url); 92 | // 接続テスト 93 | await client.agentCard(); 94 | this.clients.set(endpoint.id, client); 95 | console.log(`Successfully connected to agent ${endpoint.id} at ${endpoint.url}`); 96 | } catch (error) { 97 | console.error(`Failed to connect to agent ${endpoint.id} at ${endpoint.url}:`, error); 98 | } 99 | } 100 | } 101 | 102 | getClient(agentId: string): A2AClient | undefined { 103 | return this.clients.get(agentId); 104 | } 105 | 106 | getClientByUrl(url: string): A2AClient | undefined { 107 | const endpoint = this.config.getEndpointByUrl(url); 108 | return endpoint ? this.clients.get(endpoint.id) : undefined; 109 | } 110 | 111 | getAllClients(): Map { 112 | return this.clients; 113 | } 114 | 115 | async sendTask(agentId: string, params: TaskSendParams): Promise { 116 | const client = this.getClient(agentId); 117 | if (!client) { 118 | throw new Error(`Agent ${agentId} not found`); 119 | } 120 | return client.sendTask(params); 121 | } 122 | } 123 | ``` 124 | 125 | ### メインサーバーの修正 126 | ```typescript 127 | // index.ts 128 | const agentManager = new AgentManager(); 129 | 130 | // ツールハンドラーの修正 131 | server.setRequestHandler(CallToolRequestSchema, async (request) => { 132 | const { name, arguments: args } = request.params; 133 | 134 | try { 135 | switch (name) { 136 | case "a2a_send_task": { 137 | const { message, taskId, agentId } = args as { 138 | message: string; 139 | taskId?: string; 140 | agentId?: string; 141 | }; 142 | 143 | if (!agentId) { 144 | throw new Error('agentId is required'); 145 | } 146 | 147 | const result = await agentManager.sendTask(agentId, { 148 | id: taskId || crypto.randomUUID(), 149 | message: { 150 | role: "user", 151 | parts: [{ text: message }], 152 | }, 153 | }); 154 | 155 | return { 156 | content: [ 157 | { 158 | type: "text", 159 | text: JSON.stringify(result, null, 2), 160 | }, 161 | ], 162 | }; 163 | } 164 | // 他のケースも同様に修正 165 | } 166 | } catch (error) { 167 | return { 168 | content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }], 169 | isError: true, 170 | }; 171 | } 172 | }); 173 | ``` 174 | 175 | ## 3. 使用方法 176 | 177 | ### 環境変数の設定例 178 | ```bash 179 | export A2A_ENDPOINT_URLS="http://localhost:41241,http://localhost:41242" 180 | ``` 181 | 182 | ### タスクの送信例 183 | ```typescript 184 | // エージェントのIDを取得 185 | const endpoints = agentManager.getAllClients(); 186 | const agentId = Array.from(endpoints.keys())[0]; // 最初のエージェントのIDを使用 187 | 188 | // タスクを送信 189 | await agentManager.sendTask(agentId, { 190 | id: 'task-123', 191 | message: { 192 | role: "user", 193 | parts: [{ text: "Hello Agent" }], 194 | }, 195 | }); 196 | ``` 197 | 198 | ## 4. 注意点 199 | 200 | 1. **UUIDの使用** 201 | - すべてのエージェントに一意のUUIDが割り当てられる 202 | - エージェントのIDは起動時に自動生成される 203 | 204 | 2. **エラー処理** 205 | - 接続失敗時の適切なエラーハンドリング 206 | - エージェントが見つからない場合のエラーメッセージ 207 | - agentIdが指定されていない場合のエラー 208 | 209 | 3. **設定の検証** 210 | - 環境変数の形式チェック 211 | - URLの形式チェック 212 | 213 | 4. **ログ出力** 214 | - 接続状態のログ(UUIDとURLの両方を表示) 215 | - エラー発生時の詳細なログ -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { Server } from "@modelcontextprotocol/sdk/server/index.js"; 4 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 5 | import { 6 | CallToolRequestSchema, 7 | ListToolsRequestSchema, 8 | ListResourcesRequestSchema, 9 | ReadResourceRequestSchema, 10 | } from "@modelcontextprotocol/sdk/types.js"; 11 | import { AgentManager } from "./agent-manager.js"; 12 | 13 | // Create MCP server 14 | const server = new Server( 15 | { 16 | name: "a2a-client-server", 17 | version: "0.1.0", 18 | }, 19 | { 20 | capabilities: { 21 | tools: {}, 22 | resources: {}, 23 | }, 24 | }, 25 | ); 26 | 27 | // Create agent manager instance 28 | const agentManager = new AgentManager(); 29 | 30 | // List available tools 31 | server.setRequestHandler(ListToolsRequestSchema, async () => ({ 32 | tools: [ 33 | { 34 | name: "a2a_send_task", 35 | description: "Send a task to an A2A agent", 36 | inputSchema: { 37 | type: "object", 38 | properties: { 39 | message: { 40 | type: "string", 41 | description: "Message to send to the agent", 42 | }, 43 | taskId: { 44 | type: "string", 45 | description: "Optional task ID. If not provided, a new UUID will be generated", 46 | }, 47 | agentId: { 48 | type: "string", 49 | description: "Optional agent ID. If not provided, the first available agent will be used", 50 | }, 51 | }, 52 | required: ["message"], 53 | }, 54 | }, 55 | { 56 | name: "a2a_get_task", 57 | description: "Get the current state of a task", 58 | inputSchema: { 59 | type: "object", 60 | properties: { 61 | taskId: { 62 | type: "string", 63 | description: "ID of the task to retrieve", 64 | }, 65 | agentId: { 66 | type: "string", 67 | description: "ID of the agent that handled the task", 68 | }, 69 | }, 70 | required: ["taskId", "agentId"], 71 | }, 72 | }, 73 | { 74 | name: "a2a_cancel_task", 75 | description: "Cancel a running task", 76 | inputSchema: { 77 | type: "object", 78 | properties: { 79 | taskId: { 80 | type: "string", 81 | description: "ID of the task to cancel", 82 | }, 83 | agentId: { 84 | type: "string", 85 | description: "ID of the agent that is handling the task", 86 | }, 87 | }, 88 | required: ["taskId", "agentId"], 89 | }, 90 | }, 91 | { 92 | name: "a2a_send_task_subscribe", 93 | description: "Send a task and subscribe to updates (streaming)", 94 | inputSchema: { 95 | type: "object", 96 | properties: { 97 | message: { 98 | type: "string", 99 | description: "Message to send to the agent", 100 | }, 101 | taskId: { 102 | type: "string", 103 | description: "Optional task ID. If not provided, a new UUID will be generated", 104 | }, 105 | agentId: { 106 | type: "string", 107 | description: "Optional agent ID. If not provided, the first available agent will be used", 108 | }, 109 | maxUpdates: { 110 | type: "number", 111 | description: "Maximum number of updates to receive (default: 10)", 112 | }, 113 | }, 114 | required: ["message"], 115 | }, 116 | }, 117 | { 118 | name: "a2a_agent_info", 119 | description: "Get information about the connected A2A agents", 120 | inputSchema: { 121 | type: "object", 122 | properties: { 123 | agentId: { 124 | type: "string", 125 | description: "Optional agent ID. If not provided, information for all agents will be returned", 126 | }, 127 | }, 128 | }, 129 | }, 130 | ], 131 | })); 132 | 133 | // Handle tool calls 134 | server.setRequestHandler(CallToolRequestSchema, async (request) => { 135 | const { name, arguments: args } = request.params; 136 | 137 | try { 138 | switch (name) { 139 | case "a2a_send_task": { 140 | const { message, taskId, agentId } = args as { message: string; taskId?: string; agentId?: string }; 141 | const client = agentId ? agentManager.getClientById(agentId) : agentManager.getAllClients().values().next().value; 142 | 143 | if (!client) { 144 | throw new Error(`No available agent${agentId ? ` with ID ${agentId}` : ''}`); 145 | } 146 | 147 | const result = await client.sendTask({ 148 | id: taskId || crypto.randomUUID(), 149 | message: { 150 | role: "user", 151 | parts: [{ text: message }], 152 | }, 153 | }); 154 | return { 155 | content: [ 156 | { 157 | type: "text", 158 | text: JSON.stringify(result, null, 2), 159 | }, 160 | ], 161 | }; 162 | } 163 | 164 | case "a2a_get_task": { 165 | const { taskId, agentId } = args as { taskId: string; agentId: string }; 166 | const client = agentManager.getClientById(agentId); 167 | 168 | if (!client) { 169 | throw new Error(`No agent found with ID ${agentId}`); 170 | } 171 | 172 | const result = await client.getTask({ id: taskId }); 173 | return { 174 | content: [ 175 | { 176 | type: "text", 177 | text: JSON.stringify(result, null, 2), 178 | }, 179 | ], 180 | }; 181 | } 182 | 183 | case "a2a_cancel_task": { 184 | const { taskId, agentId } = args as { taskId: string; agentId: string }; 185 | const client = agentManager.getClientById(agentId); 186 | 187 | if (!client) { 188 | throw new Error(`No agent found with ID ${agentId}`); 189 | } 190 | 191 | const result = await client.cancelTask({ id: taskId }); 192 | return { 193 | content: [ 194 | { 195 | type: "text", 196 | text: JSON.stringify(result, null, 2), 197 | }, 198 | ], 199 | }; 200 | } 201 | 202 | case "a2a_send_task_subscribe": { 203 | const { message, taskId, agentId, maxUpdates = 10 } = args as { 204 | message: string; 205 | taskId?: string; 206 | agentId?: string; 207 | maxUpdates?: number; 208 | }; 209 | 210 | const client = agentId ? agentManager.getClientById(agentId) : agentManager.getAllClients().values().next().value; 211 | 212 | if (!client) { 213 | throw new Error(`No available agent${agentId ? ` with ID ${agentId}` : ''}`); 214 | } 215 | 216 | const id = taskId || crypto.randomUUID(); 217 | const stream = client.sendTaskSubscribe({ 218 | id, 219 | message: { 220 | role: "user", 221 | parts: [{ text: message }], 222 | }, 223 | }); 224 | 225 | const updates = []; 226 | let count = 0; 227 | 228 | for await (const event of stream) { 229 | updates.push(event); 230 | count++; 231 | if (count >= maxUpdates) break; 232 | 233 | if (event.final) break; 234 | } 235 | 236 | return { 237 | content: [ 238 | { 239 | type: "text", 240 | text: JSON.stringify({ taskId: id, updates }, null, 2), 241 | }, 242 | ], 243 | }; 244 | } 245 | 246 | case "a2a_agent_info": { 247 | const { agentId } = args as { agentId?: string }; 248 | 249 | if (agentId) { 250 | const client = agentManager.getClientById(agentId); 251 | if (!client) { 252 | throw new Error(`No agent found with ID ${agentId}`); 253 | } 254 | const card = await client.agentCard(); 255 | return { 256 | content: [ 257 | { 258 | type: "text", 259 | text: JSON.stringify(card, null, 2), 260 | }, 261 | ], 262 | }; 263 | } else { 264 | const results = []; 265 | for (const [id, client] of agentManager.getAllClients()) { 266 | try { 267 | const card = await client.agentCard(); 268 | results.push({ agentId: id, card }); 269 | } catch (error) { 270 | results.push({ agentId: id, error: error instanceof Error ? error.message : String(error) }); 271 | } 272 | } 273 | return { 274 | content: [ 275 | { 276 | type: "text", 277 | text: JSON.stringify(results, null, 2), 278 | }, 279 | ], 280 | }; 281 | } 282 | } 283 | 284 | default: 285 | throw new Error(`Unknown tool: ${name}`); 286 | } 287 | } catch (error) { 288 | return { 289 | content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }], 290 | isError: true, 291 | }; 292 | } 293 | }); 294 | 295 | // List available resources 296 | server.setRequestHandler(ListResourcesRequestSchema, async () => { 297 | const endpoints = agentManager.getEndpoints(); 298 | return { 299 | resources: [ 300 | ...endpoints.map(endpoint => ({ 301 | uri: `a2a://agent-card/${endpoint.id}`, 302 | mimeType: "application/json", 303 | name: `A2A Agent Card Information (${endpoint.id})`, 304 | })), 305 | { 306 | uri: "a2a://tasks", 307 | mimeType: "application/json", 308 | name: "Recent A2A Tasks", 309 | }, 310 | ], 311 | }; 312 | }); 313 | 314 | // Read resources 315 | server.setRequestHandler(ReadResourceRequestSchema, async (request) => { 316 | const { uri } = request.params; 317 | 318 | if (uri.startsWith("a2a://agent-card/")) { 319 | const agentId = uri.split("/")[2]; 320 | const client = agentManager.getClientById(agentId); 321 | 322 | if (!client) { 323 | throw new Error(`No agent found with ID ${agentId}`); 324 | } 325 | 326 | try { 327 | const card = await client.agentCard(); 328 | return { 329 | contents: [ 330 | { 331 | uri, 332 | mimeType: "application/json", 333 | text: JSON.stringify(card, null, 2), 334 | }, 335 | ], 336 | }; 337 | } catch (error) { 338 | throw new Error(`Failed to read agent card: ${error instanceof Error ? error.message : String(error)}`); 339 | } 340 | } else if (uri === "a2a://tasks") { 341 | return { 342 | contents: [ 343 | { 344 | uri, 345 | mimeType: "application/json", 346 | text: JSON.stringify({ tasks: [] }, null, 2), 347 | }, 348 | ], 349 | }; 350 | } 351 | 352 | throw new Error(`Resource not found: ${uri}`); 353 | }); 354 | 355 | async function runServer() { 356 | console.error("Starting A2A Client MCP Server"); 357 | 358 | // Initialize agent manager 359 | await agentManager.initialize(); 360 | 361 | const transport = new StdioServerTransport(); 362 | await server.connect(transport); 363 | console.error("A2A Client MCP Server running on stdio"); 364 | } 365 | 366 | runServer().catch((error) => { 367 | console.error("Fatal error running server:", error); 368 | process.exit(1); 369 | }); 370 | -------------------------------------------------------------------------------- /a2a-client.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A2A client implementation based on the sample client from the A2A protocol. 3 | * This is a simplified version focusing on core functionality. 4 | */ 5 | 6 | // Type definitions 7 | export interface Message { 8 | role: "user" | "agent"; 9 | parts: MessagePart[]; 10 | } 11 | 12 | export interface MessagePart { 13 | text: string; 14 | type?: "text"; 15 | } 16 | 17 | export interface TaskIdParams { 18 | id: string; 19 | } 20 | 21 | export interface TaskQueryParams extends TaskIdParams {} 22 | 23 | export interface TaskSendParams { 24 | id: string; 25 | message: Message; 26 | } 27 | 28 | export interface Task { 29 | id: string; 30 | status: TaskStatus; 31 | artifacts?: Artifact[]; 32 | sessionId?: string | null; 33 | metadata?: Record | null; 34 | } 35 | 36 | export interface TaskStatus { 37 | state: TaskState; 38 | message?: Message | null; 39 | timestamp?: string; 40 | } 41 | 42 | export type TaskState = 43 | | "submitted" 44 | | "working" 45 | | "input-required" 46 | | "completed" 47 | | "canceled" 48 | | "failed" 49 | | "unknown"; 50 | 51 | export interface Artifact { 52 | name?: string | null; 53 | description?: string | null; 54 | parts: MessagePart[]; 55 | index?: number; 56 | append?: boolean | null; 57 | metadata?: Record | null; 58 | lastChunk?: boolean | null; 59 | } 60 | 61 | export interface TaskStatusUpdateEvent { 62 | id: string; 63 | status: TaskStatus; 64 | final?: boolean; 65 | metadata?: Record | null; 66 | } 67 | 68 | export interface TaskArtifactUpdateEvent { 69 | id: string; 70 | artifact: Artifact; 71 | final?: boolean; 72 | metadata?: Record | null; 73 | } 74 | 75 | export interface AgentProvider { 76 | organization: string; 77 | url?: string | null; 78 | } 79 | 80 | export interface AgentCapabilities { 81 | streaming?: boolean; 82 | pushNotifications?: boolean; 83 | stateTransitionHistory?: boolean; 84 | } 85 | 86 | export interface AgentAuthentication { 87 | schemes: string[]; 88 | credentials?: string | null; 89 | } 90 | 91 | export interface AgentSkill { 92 | id: string; 93 | name: string; 94 | description?: string | null; 95 | tags?: string[] | null; 96 | examples?: string[] | null; 97 | inputModes?: string[] | null; 98 | outputModes?: string[] | null; 99 | } 100 | 101 | export interface AgentCard { 102 | name: string; 103 | description?: string | null; 104 | url: string; 105 | provider?: AgentProvider | null; 106 | version: string; 107 | documentationUrl?: string | null; 108 | capabilities: AgentCapabilities; 109 | authentication?: AgentAuthentication | null; 110 | defaultInputModes?: string[]; 111 | defaultOutputModes?: string[]; 112 | skills: AgentSkill[]; 113 | } 114 | 115 | // JSON-RPC related types 116 | interface JSONRPCRequest { 117 | jsonrpc: "2.0"; 118 | id: string | number; 119 | method: string; 120 | params: unknown; 121 | } 122 | 123 | interface JSONRPCResponse { 124 | jsonrpc: "2.0"; 125 | id: string | number | null; 126 | result?: T; 127 | error?: JSONRPCError; 128 | } 129 | 130 | interface JSONRPCError { 131 | code: number; 132 | message: string; 133 | data?: unknown; 134 | } 135 | 136 | // Client implementation 137 | export class A2AClient { 138 | private baseUrl: string; 139 | 140 | constructor(baseUrl: string) { 141 | // Ensure baseUrl doesn't end with a slash for consistency 142 | this.baseUrl = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl; 143 | } 144 | 145 | /** 146 | * Helper to generate unique request IDs. 147 | */ 148 | private _generateRequestId(): string | number { 149 | if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") { 150 | return crypto.randomUUID(); 151 | } else { 152 | // Fallback for environments without crypto.randomUUID 153 | return Date.now(); 154 | } 155 | } 156 | 157 | /** 158 | * Make a JSON-RPC request to the A2A server. 159 | */ 160 | private async _makeHttpRequest( 161 | method: string, 162 | params: unknown, 163 | acceptHeader: "application/json" | "text/event-stream" = "application/json" 164 | ): Promise { 165 | const requestId = this._generateRequestId(); 166 | const requestBody: JSONRPCRequest = { 167 | jsonrpc: "2.0", 168 | id: requestId, 169 | method, 170 | params, 171 | }; 172 | 173 | try { 174 | const response = await fetch(this.baseUrl, { 175 | method: "POST", 176 | headers: { 177 | "Content-Type": "application/json", 178 | Accept: acceptHeader, 179 | }, 180 | body: JSON.stringify(requestBody), 181 | }); 182 | return response; 183 | } catch (networkError) { 184 | console.error("Network error during RPC call:", networkError); 185 | throw new Error(`Network error: ${ 186 | networkError instanceof Error ? networkError.message : String(networkError) 187 | }`); 188 | } 189 | } 190 | 191 | /** 192 | * Handle standard JSON-RPC responses. 193 | */ 194 | private async _handleJsonResponse( 195 | response: Response, 196 | expectedMethod?: string 197 | ): Promise { 198 | let responseBody: string | null = null; 199 | try { 200 | if (!response.ok) { 201 | responseBody = await response.text(); 202 | try { 203 | const parsedError = JSON.parse(responseBody) as JSONRPCResponse; 204 | if (parsedError.error) { 205 | throw new Error(`${parsedError.error.message} (${parsedError.error.code})`); 206 | } 207 | } catch (parseError) { 208 | // Ignore parsing error, fall through to generic HTTP error 209 | } 210 | throw new Error( 211 | `HTTP error ${response.status}: ${response.statusText}${ 212 | responseBody ? ` - ${responseBody}` : "" 213 | }` 214 | ); 215 | } 216 | 217 | responseBody = await response.text(); 218 | const jsonResponse = JSON.parse(responseBody) as JSONRPCResponse; 219 | 220 | if ( 221 | typeof jsonResponse !== "object" || 222 | jsonResponse === null || 223 | jsonResponse.jsonrpc !== "2.0" 224 | ) { 225 | throw new Error("Invalid JSON-RPC response structure"); 226 | } 227 | 228 | if (jsonResponse.error) { 229 | throw new Error(`${jsonResponse.error.message} (${jsonResponse.error.code})`); 230 | } 231 | 232 | return jsonResponse.result as T; 233 | } catch (error) { 234 | console.error( 235 | `Error processing RPC response for method ${expectedMethod || "unknown"}:`, 236 | error, 237 | responseBody ? `\nResponse Body: ${responseBody}` : "" 238 | ); 239 | throw error; 240 | } 241 | } 242 | 243 | /** 244 | * Handle streaming Server-Sent Events (SSE) responses. 245 | */ 246 | private async *_handleStreamingResponse( 247 | response: Response, 248 | expectedMethod?: string 249 | ): AsyncIterable { 250 | if (!response.ok || !response.body) { 251 | let errorText: string | null = null; 252 | try { 253 | errorText = await response.text(); 254 | } catch (_) { 255 | /* Ignore read error */ 256 | } 257 | console.error( 258 | `HTTP error ${response.status} received for streaming method ${ 259 | expectedMethod || "unknown" 260 | }.`, 261 | errorText ? `Response: ${errorText}` : "" 262 | ); 263 | throw new Error( 264 | `HTTP error ${response.status}: ${response.statusText} - Failed to establish stream.` 265 | ); 266 | } 267 | 268 | const reader = response.body.pipeThrough(new TextDecoderStream()).getReader(); 269 | let buffer = ""; 270 | 271 | try { 272 | while (true) { 273 | const { done, value } = await reader.read(); 274 | 275 | if (done) { 276 | if (buffer.trim()) { 277 | console.warn( 278 | `SSE stream ended with partial data in buffer for method ${expectedMethod}: ${buffer}` 279 | ); 280 | } 281 | break; 282 | } 283 | 284 | buffer += value; 285 | const lines = buffer.replace(/\r/g, "").split("\n\n"); // SSE messages end with \n\n 286 | buffer = lines.pop() || ""; // Keep potential partial message 287 | for (const message of lines) { 288 | if (message.startsWith("data: ")) { 289 | const dataLine = message.substring("data: ".length).trim(); 290 | if (dataLine) { 291 | try { 292 | const parsedData = JSON.parse(dataLine) as JSONRPCResponse; 293 | if ( 294 | typeof parsedData !== "object" || 295 | parsedData === null || 296 | !("jsonrpc" in parsedData && parsedData.jsonrpc === "2.0") 297 | ) { 298 | console.error( 299 | `Invalid SSE data structure received for method ${expectedMethod}:`, 300 | dataLine 301 | ); 302 | continue; // Skip invalid data 303 | } 304 | 305 | if (parsedData.error) { 306 | console.error( 307 | `Error received in SSE stream for method ${expectedMethod}:`, 308 | parsedData.error 309 | ); 310 | throw new Error(`${parsedData.error.message} (${parsedData.error.code})`); 311 | } else if (parsedData.result !== undefined) { 312 | yield parsedData.result as T; 313 | } else { 314 | console.warn( 315 | `SSE data for ${expectedMethod} has neither result nor error:`, 316 | parsedData 317 | ); 318 | } 319 | } catch (e) { 320 | console.error( 321 | `Failed to parse SSE data line for method ${expectedMethod}:`, 322 | dataLine, 323 | e 324 | ); 325 | } 326 | } 327 | } 328 | } 329 | } 330 | } catch (error) { 331 | console.error(`Error reading SSE stream for method ${expectedMethod}:`, error); 332 | throw error; 333 | } finally { 334 | reader.releaseLock(); 335 | console.log(`SSE stream finished for method ${expectedMethod}.`); 336 | } 337 | } 338 | 339 | /** 340 | * Retrieves the AgentCard. 341 | */ 342 | async agentCard(): Promise { 343 | try { 344 | // First try the well-known endpoint 345 | try { 346 | const response = await fetch(`${this.baseUrl}/.well-known/agent.json`); 347 | if (response.ok) { 348 | return response.json(); 349 | } 350 | } catch (e) { 351 | // Ignore and try the next approach 352 | } 353 | 354 | // Then try the traditional endpoint 355 | const cardUrl = `${this.baseUrl}/agent-card`; 356 | const response = await fetch(cardUrl, { 357 | method: "GET", 358 | headers: { 359 | Accept: "application/json", 360 | }, 361 | }); 362 | 363 | if (!response.ok) { 364 | throw new Error( 365 | `HTTP error ${response.status} fetching agent card from ${cardUrl}: ${response.statusText}` 366 | ); 367 | } 368 | 369 | return response.json(); 370 | } catch (error) { 371 | console.error("Failed to fetch or parse agent card:", error); 372 | throw new Error( 373 | `Could not retrieve agent card: ${ 374 | error instanceof Error ? error.message : String(error) 375 | }` 376 | ); 377 | } 378 | } 379 | 380 | /** 381 | * Sends a task request to the agent (non-streaming). 382 | */ 383 | async sendTask(params: TaskSendParams): Promise { 384 | const httpResponse = await this._makeHttpRequest("tasks/send", params); 385 | return this._handleJsonResponse(httpResponse, "tasks/send"); 386 | } 387 | 388 | /** 389 | * Sends a task request and subscribes to streaming updates. 390 | */ 391 | sendTaskSubscribe( 392 | params: TaskSendParams 393 | ): AsyncIterable { 394 | const streamGenerator = async function* ( 395 | this: A2AClient 396 | ): AsyncIterable { 397 | const httpResponse = await this._makeHttpRequest( 398 | "tasks/sendSubscribe", 399 | params, 400 | "text/event-stream" 401 | ); 402 | yield* this._handleStreamingResponse( 403 | httpResponse, 404 | "tasks/sendSubscribe" 405 | ); 406 | }.bind(this)(); 407 | 408 | return streamGenerator; 409 | } 410 | 411 | /** 412 | * Retrieves the current state of a task. 413 | */ 414 | async getTask(params: TaskQueryParams): Promise { 415 | const httpResponse = await this._makeHttpRequest("tasks/get", params); 416 | return this._handleJsonResponse(httpResponse, "tasks/get"); 417 | } 418 | 419 | /** 420 | * Cancels a currently running task. 421 | */ 422 | async cancelTask(params: TaskIdParams): Promise { 423 | const httpResponse = await this._makeHttpRequest("tasks/cancel", params); 424 | return this._handleJsonResponse(httpResponse, "tasks/cancel"); 425 | } 426 | 427 | /** 428 | * Resubscribes to updates for a task after connection interruption. 429 | */ 430 | resubscribeTask( 431 | params: TaskQueryParams 432 | ): AsyncIterable { 433 | const streamGenerator = async function* ( 434 | this: A2AClient 435 | ): AsyncIterable { 436 | const httpResponse = await this._makeHttpRequest( 437 | "tasks/resubscribe", 438 | params, 439 | "text/event-stream" 440 | ); 441 | yield* this._handleStreamingResponse( 442 | httpResponse, 443 | "tasks/resubscribe" 444 | ); 445 | }.bind(this)(); 446 | 447 | return streamGenerator; 448 | } 449 | 450 | /** 451 | * Checks if the server likely supports optional methods based on agent card. 452 | */ 453 | async supports(capability: "streaming" | "pushNotifications"): Promise { 454 | try { 455 | const card = await this.agentCard(); 456 | switch (capability) { 457 | case "streaming": 458 | return !!card.capabilities?.streaming; 459 | case "pushNotifications": 460 | return !!card.capabilities?.pushNotifications; 461 | default: 462 | return false; 463 | } 464 | } catch (error) { 465 | console.error(`Failed to determine support for capability '${capability}':`, error); 466 | return false; 467 | } 468 | } 469 | } 470 | --------------------------------------------------------------------------------