initial: @a2a/mcp gateway for a2acloud agents

Local MCP server that exposes any number of deployed A2A agents to
Claude Code, Cursor, Windsurf, and other MCP clients. Reuses the
~/.a2a/credentials.json token written by the Python a2a CLI. Adding
or removing agents is a single ~/.a2a/mcp.json edit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-11 20:33:54 -03:00
commit 9c5d8172dc
13 changed files with 2738 additions and 0 deletions

96
src/upstream.ts Normal file
View File

@@ -0,0 +1,96 @@
/**
* Per-agent JSON-RPC client. Talks to the deployed agent's POST /mcp endpoint.
*
* We hand-roll the wire calls (instead of using @modelcontextprotocol/sdk's
* Client) because (a) the surface we need is tiny — tools/list, tools/call —
* and (b) we want to send the user's bearer token as a plain Authorization
* header without coupling to a transport class.
*/
const JSON_RPC = "2.0";
export interface UpstreamConfig {
name: string;
url: string;
token: string | null;
}
export interface UpstreamTool {
name: string;
description?: string;
inputSchema: unknown;
outputSchema?: unknown;
}
export interface UpstreamCallResult {
content: Array<{ type: string; text?: string; [k: string]: unknown }>;
structuredContent?: unknown;
isError?: boolean;
}
export class UpstreamError extends Error {
constructor(
public agent: string,
public status: number,
message: string,
) {
super(`upstream ${agent}: ${message}`);
this.name = "UpstreamError";
}
}
let _nextId = 1;
export class UpstreamAgent {
constructor(private cfg: UpstreamConfig) {
this.cfg.url = cfg.url.replace(/\/+$/, "");
}
get name(): string {
return this.cfg.name;
}
private async rpc<T>(method: string, params: unknown): Promise<T> {
const id = _nextId++;
const headers: Record<string, string> = {
"content-type": "application/json",
accept: "application/json",
};
if (this.cfg.token) headers["authorization"] = `Bearer ${this.cfg.token}`;
const resp = await fetch(`${this.cfg.url}/mcp`, {
method: "POST",
headers,
body: JSON.stringify({ jsonrpc: JSON_RPC, id, method, params }),
});
if (!resp.ok) {
const text = await resp.text().catch(() => "");
throw new UpstreamError(this.cfg.name, resp.status, text || resp.statusText);
}
const env: any = await resp.json();
if (env?.error) {
throw new UpstreamError(
this.cfg.name,
500,
`${env.error.code}: ${env.error.message}`,
);
}
return env?.result as T;
}
async listTools(): Promise<UpstreamTool[]> {
const out = await this.rpc<{ tools: UpstreamTool[] }>("tools/list", {});
return out?.tools ?? [];
}
async callTool(
name: string,
arguments_: Record<string, unknown>,
): Promise<UpstreamCallResult> {
return this.rpc<UpstreamCallResult>("tools/call", {
name,
arguments: arguments_,
});
}
}