This commit is contained in:
@@ -15,6 +15,7 @@ export interface AgentRow {
|
||||
status: string;
|
||||
url?: string;
|
||||
description?: string;
|
||||
card?: Record<string, any>;
|
||||
}
|
||||
|
||||
export class ControlPlaneClient {
|
||||
|
||||
41
src/cli.ts
41
src/cli.ts
@@ -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 {
|
||||
|
||||
@@ -12,6 +12,7 @@ import path from "node:path";
|
||||
export interface EnabledAgent {
|
||||
name: string;
|
||||
url: string;
|
||||
mcpPath?: string;
|
||||
description?: string;
|
||||
addedAt: string;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user