WIP a2a-mcp updates
All checks were successful
build / test (push) Successful in 30s

This commit is contained in:
2026-05-23 14:33:03 -04:00
parent 4ff51157c0
commit 91c7bf9c57
5 changed files with 56 additions and 8 deletions

View File

@@ -15,6 +15,7 @@ export interface AgentRow {
status: string; status: string;
url?: string; url?: string;
description?: string; description?: string;
card?: Record<string, any>;
} }
export class ControlPlaneClient { export class ControlPlaneClient {

View File

@@ -43,6 +43,30 @@ function fail(msg: string, code = 1): never {
process.exit(code); process.exit(code);
} }
function normalizeMcpPath(path: unknown): string {
if (typeof path !== "string" || !path.trim()) return "/mcp";
const trimmed = path.trim();
return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
}
function mcpTargetForAgent(
row: { name: string; url?: string; card?: Record<string, any> },
apiUrl: string,
): { url?: string; mcpPath: string } {
const imported = row.card?.capabilities?.a2a_cloud_import;
const mcp = imported?.mcp;
if (mcp?.mode === "generated") {
return {
url: `${apiUrl.replace(/\/+$/, "")}/v1/agents/${encodeURIComponent(row.name)}`,
mcpPath: normalizeMcpPath(mcp.path),
};
}
return {
url: mcp?.base_url || row.url,
mcpPath: normalizeMcpPath(mcp?.path || row.card?.mcp_endpoint),
};
}
async function prompt(label: string, opts: { hidden?: boolean } = {}): Promise<string> { async function prompt(label: string, opts: { hidden?: boolean } = {}): Promise<string> {
const rl = readline.createInterface({ input, output, terminal: true }); const rl = readline.createInterface({ input, output, terminal: true });
try { try {
@@ -160,7 +184,9 @@ program
output.write("(no agents added; try `a2amcp add <name>`)\n"); output.write("(no agents added; try `a2amcp add <name>`)\n");
return; return;
} }
for (const a of cfg.agents) output.write(` ${a.name} ${a.url}\n`); for (const a of cfg.agents) {
output.write(` ${a.name} ${a.url}${a.mcpPath ?? "/mcp"}\n`);
}
}); });
program program
@@ -171,11 +197,16 @@ program
.action( .action(
async (name: string, opts: { url?: string; api?: string }) => { async (name: string, opts: { url?: string; api?: string }) => {
let url = opts.url; let url = opts.url;
let mcpPath = "/mcp";
let description: string | undefined; let description: string | undefined;
if (!url) { if (!url) {
try { try {
const row = await (await client(opts.api)).getAgent(name); const creds = await loadCredentials();
url = row.url; const apiUrl = resolveApiUrl(opts.api ?? creds?.apiUrl);
const row = await new ControlPlaneClient(apiUrl, creds?.token ?? null).getAgent(name);
const target = mcpTargetForAgent(row, apiUrl);
url = target.url;
mcpPath = target.mcpPath;
description = row.description; description = row.description;
} catch (err) { } catch (err) {
fail(err instanceof ApiError ? err.message : (err as Error).message); fail(err instanceof ApiError ? err.message : (err as Error).message);
@@ -185,10 +216,11 @@ program
await addAgent({ await addAgent({
name, name,
url, url,
mcpPath,
description, description,
addedAt: new Date().toISOString(), addedAt: new Date().toISOString(),
}); });
output.write(`added ${name} -> ${url}\n`); output.write(`added ${name} -> ${url.replace(/\/+$/, "")}${mcpPath}\n`);
output.write("restart your MCP client to pick up the new tools.\n"); output.write("restart your MCP client to pick up the new tools.\n");
}, },
); );
@@ -219,6 +251,7 @@ program
const u = new UpstreamAgent({ const u = new UpstreamAgent({
name: a.name, name: a.name,
url: a.url, url: a.url,
mcpPath: a.mcpPath,
token: creds?.token ?? null, token: creds?.token ?? null,
}); });
try { try {

View File

@@ -12,6 +12,7 @@ import path from "node:path";
export interface EnabledAgent { export interface EnabledAgent {
name: string; name: string;
url: string; url: string;
mcpPath?: string;
description?: string; description?: string;
addedAt: string; addedAt: string;
} }

View File

@@ -60,7 +60,7 @@ export async function buildGateway(opts: GatewayOptions = {}): Promise<GatewayHa
upstreams.set( upstreams.set(
a.name, a.name,
new UpstreamAgent({ new UpstreamAgent({
name: a.name, url: a.url, token, name: a.name, url: a.url, mcpPath: a.mcpPath, token,
cpJwt, cpUrl, bucket, cpJwt, cpUrl, bucket,
requestTimeoutMs: opts.upstreamRequestTimeoutMs, requestTimeoutMs: opts.upstreamRequestTimeoutMs,
streamIdleTimeoutMs: opts.upstreamStreamIdleTimeoutMs, streamIdleTimeoutMs: opts.upstreamStreamIdleTimeoutMs,

View File

@@ -13,6 +13,7 @@ const DEFAULT_STREAM_IDLE_TIMEOUT_MS = 120_000;
export interface UpstreamConfig { export interface UpstreamConfig {
name: string; name: string;
url: string; url: string;
mcpPath?: string;
token: string | null; token: string | null;
requestTimeoutMs?: number; requestTimeoutMs?: number;
streamIdleTimeoutMs?: number; streamIdleTimeoutMs?: number;
@@ -62,15 +63,27 @@ export class UpstreamError extends Error {
let _nextId = 1; let _nextId = 1;
function normalizeMcpPath(path: string | undefined): string {
if (!path) return "/mcp";
const trimmed = path.trim();
if (!trimmed) return "/mcp";
return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
}
export class UpstreamAgent { export class UpstreamAgent {
constructor(private cfg: UpstreamConfig) { constructor(private cfg: UpstreamConfig) {
this.cfg.url = cfg.url.replace(/\/+$/, ""); this.cfg.url = cfg.url.replace(/\/+$/, "");
this.cfg.mcpPath = normalizeMcpPath(cfg.mcpPath);
} }
get name(): string { get name(): string {
return this.cfg.name; return this.cfg.name;
} }
private mcpUrl(): string {
return `${this.cfg.url}${this.cfg.mcpPath ?? "/mcp"}`;
}
private async rpc<T>(method: string, params: unknown): Promise<T> { private async rpc<T>(method: string, params: unknown): Promise<T> {
const id = _nextId++; const id = _nextId++;
const timeoutMs = this.cfg.requestTimeoutMs ?? DEFAULT_RPC_TIMEOUT_MS; const timeoutMs = this.cfg.requestTimeoutMs ?? DEFAULT_RPC_TIMEOUT_MS;
@@ -87,7 +100,7 @@ export class UpstreamAgent {
if (this.cfg.token) headers["authorization"] = `Bearer ${this.cfg.token}`; if (this.cfg.token) headers["authorization"] = `Bearer ${this.cfg.token}`;
try { try {
const resp = await fetch(`${this.cfg.url}/mcp`, { const resp = await fetch(this.mcpUrl(), {
method: "POST", method: "POST",
headers, headers,
body: JSON.stringify({ jsonrpc: JSON_RPC, id, method, params }), body: JSON.stringify({ jsonrpc: JSON_RPC, id, method, params }),
@@ -193,7 +206,7 @@ export class UpstreamAgent {
let resp: Response; let resp: Response;
try { try {
resp = await fetch(`${this.cfg.url}/mcp`, { resp = await fetch(this.mcpUrl(), {
method: "POST", method: "POST",
headers, headers,
body: JSON.stringify({ body: JSON.stringify({
@@ -356,7 +369,7 @@ export class UpstreamAgent {
"mcp-session-id": upstreamSessionId, "mcp-session-id": upstreamSessionId,
}; };
if (this.cfg.token) headers.authorization = `Bearer ${this.cfg.token}`; if (this.cfg.token) headers.authorization = `Bearer ${this.cfg.token}`;
const resp = await fetch(`${this.cfg.url}/mcp`, { const resp = await fetch(this.mcpUrl(), {
method: "POST", method: "POST",
headers, headers,
body: JSON.stringify(envelope), body: JSON.stringify(envelope),