This commit is contained in:
@@ -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 {
|
||||||
|
|||||||
41
src/cli.ts
41
src/cli.ts
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
Reference in New Issue
Block a user