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:
96
src/upstream.ts
Normal file
96
src/upstream.ts
Normal 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_,
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user