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;
url?: string;
description?: string;
card?: Record<string, any>;
}
export class ControlPlaneClient {

View File

@@ -43,6 +43,30 @@ function fail(msg: string, code = 1): never {
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> {
const rl = readline.createInterface({ input, output, terminal: true });
try {
@@ -160,7 +184,9 @@ program
output.write("(no agents added; try `a2amcp add <name>`)\n");
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
@@ -171,11 +197,16 @@ program
.action(
async (name: string, opts: { url?: string; api?: string }) => {
let url = opts.url;
let mcpPath = "/mcp";
let description: string | undefined;
if (!url) {
try {
const row = await (await client(opts.api)).getAgent(name);
url = row.url;
const creds = await loadCredentials();
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;
} catch (err) {
fail(err instanceof ApiError ? err.message : (err as Error).message);
@@ -185,10 +216,11 @@ program
await addAgent({
name,
url,
mcpPath,
description,
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");
},
);
@@ -219,6 +251,7 @@ program
const u = new UpstreamAgent({
name: a.name,
url: a.url,
mcpPath: a.mcpPath,
token: creds?.token ?? null,
});
try {

View File

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

View File

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

View File

@@ -13,6 +13,7 @@ const DEFAULT_STREAM_IDLE_TIMEOUT_MS = 120_000;
export interface UpstreamConfig {
name: string;
url: string;
mcpPath?: string;
token: string | null;
requestTimeoutMs?: number;
streamIdleTimeoutMs?: number;
@@ -62,15 +63,27 @@ export class UpstreamError extends Error {
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 {
constructor(private cfg: UpstreamConfig) {
this.cfg.url = cfg.url.replace(/\/+$/, "");
this.cfg.mcpPath = normalizeMcpPath(cfg.mcpPath);
}
get name(): string {
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> {
const id = _nextId++;
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}`;
try {
const resp = await fetch(`${this.cfg.url}/mcp`, {
const resp = await fetch(this.mcpUrl(), {
method: "POST",
headers,
body: JSON.stringify({ jsonrpc: JSON_RPC, id, method, params }),
@@ -193,7 +206,7 @@ export class UpstreamAgent {
let resp: Response;
try {
resp = await fetch(`${this.cfg.url}/mcp`, {
resp = await fetch(this.mcpUrl(), {
method: "POST",
headers,
body: JSON.stringify({
@@ -356,7 +369,7 @@ export class UpstreamAgent {
"mcp-session-id": upstreamSessionId,
};
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",
headers,
body: JSON.stringify(envelope),