3 Commits

Author SHA1 Message Date
024f14000e release: 0.1.4 — forward cp_jwt + bucket via params._meta
All checks were successful
build / test (push) Successful in 29s
publish / npm (push) Successful in 30s
On login the gateway now captures user.id from /v1/auth/login, derives
the user's MinIO bucket (user-<id>-files), and persists both in
~/.a2a/credentials.json. UpstreamAgent.callTool injects
``params._meta = { cp_jwt, cp_url, bucket }`` on every tools/call so
upstream agents see the same caller context the platform orchestrator
provides. Unauthenticated clients omit _meta — back-compat preserved.

Requires a2a-pack with the matching _meta handler in
``a2a_pack/mcp/server.py`` (companion change in apps/a2a).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 20:53:07 -03:00
633ca5dad6 release: 0.1.3 — add a2amcp bin, sync server/cli version from package.json
All checks were successful
build / test (push) Successful in 29s
publish / npm (push) Successful in 31s
Adds an "a2amcp" bin (alongside the existing "a2a-mcp" for back-compat)
so that npx -y a2amcp resolves the binary by package name. Without it
npx fell back unreliably when no bin matched.

Also rips the hardcoded "0.1.0" out of cli.ts and gateway.ts and reads
the version from package.json — that was why serverInfo lagged behind
the published tag. Renames the user-facing command in help / error
strings / README from a2a-mcp to a2amcp; the legacy bin stays for
existing scripts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 20:39:23 -03:00
fd71a528e9 release: rename to unscoped a2amcp, bump 0.1.2
All checks were successful
build / test (push) Successful in 29s
publish / npm (push) Successful in 33s
Drops the @a2amcp/mcp scope so the npx invocation collapses to
`npx -y a2amcp` — one token instead of two, no scope to register or
remember.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 20:26:09 -03:00
8 changed files with 107 additions and 59 deletions

View File

@@ -1,4 +1,4 @@
# @a2amcp/mcp
# a2amcp
MCP gateway for a2acloud agents. It runs locally as a stdio MCP server and
exposes deployed A2A agents as tools to Claude Code, Cursor, and other MCP
@@ -7,9 +7,9 @@ clients.
## Quickstart
```bash
npx -y @a2amcp/mcp login
npx -y @a2amcp/mcp add <agent-name>
npx -y @a2amcp/mcp doctor
npx -y a2amcp login
npx -y a2amcp add <agent-name>
npx -y a2amcp doctor
```
Add it to your MCP client config:
@@ -19,7 +19,7 @@ Add it to your MCP client config:
"mcpServers": {
"a2a": {
"command": "npx",
"args": ["-y", "@a2amcp/mcp"]
"args": ["-y", "a2amcp"]
}
}
}
@@ -30,14 +30,14 @@ Restart the client after adding or removing agents.
## Commands
```bash
a2a-mcp login # save control-plane credentials
a2a-mcp whoami # show current account
a2a-mcp agents # list agents visible to your account
a2a-mcp add <name> # expose one agent through this gateway
a2a-mcp list # list locally enabled agents
a2a-mcp remove <name> # stop exposing an agent
a2a-mcp doctor # probe enabled agents and count tools
a2a-mcp logout # clear local credentials
a2amcp login # save control-plane credentials
a2amcp whoami # show current account
a2amcp agents # list agents visible to your account
a2amcp add <name> # expose one agent through this gateway
a2amcp list # list locally enabled agents
a2amcp remove <name> # stop exposing an agent
a2amcp doctor # probe enabled agents and count tools
a2amcp logout # clear local credentials
```
Running `a2a-mcp` with no command starts the MCP gateway over stdio.
Running `a2amcp` with no command starts the MCP gateway over stdio.

11
package-lock.json generated
View File

@@ -1,19 +1,20 @@
{
"name": "@a2amcp/mcp",
"version": "0.1.1",
"name": "a2amcp",
"version": "0.1.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@a2amcp/mcp",
"version": "0.1.1",
"name": "a2amcp",
"version": "0.1.4",
"license": "Apache-2.0",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.0.4",
"commander": "^12.1.0"
},
"bin": {
"a2a-mcp": "dist/cli.js"
"a2a-mcp": "dist/cli.js",
"a2amcp": "dist/cli.js"
},
"devDependencies": {
"@types/node": "^20.12.0",

View File

@@ -1,9 +1,10 @@
{
"name": "@a2amcp/mcp",
"version": "0.1.1",
"name": "a2amcp",
"version": "0.1.4",
"description": "MCP gateway for a2acloud agents. Run as a local MCP server; expose any number of deployed A2A agents to Claude Code, Cursor, and other MCP clients.",
"type": "module",
"bin": {
"a2amcp": "dist/cli.js",
"a2a-mcp": "dist/cli.js"
},
"main": "dist/index.js",

View File

@@ -52,23 +52,21 @@ export class ControlPlaneClient {
}
login(email: string, password: string) {
return this.request<{ access_token: string; user: { email: string } }>(
"POST",
"/v1/auth/login",
{ email, password },
);
return this.request<{
access_token: string;
user: { id: number; email: string };
}>("POST", "/v1/auth/login", { email, password });
}
signup(email: string, password: string) {
return this.request<{ access_token: string; user: { email: string } }>(
"POST",
"/v1/auth/signup",
{ email, password },
);
return this.request<{
access_token: string;
user: { id: number; email: string };
}>("POST", "/v1/auth/signup", { email, password });
}
me() {
return this.request<{ email: string }>("GET", "/v1/me");
return this.request<{ id: number; email: string }>("GET", "/v1/me");
}
listAgents() {

View File

@@ -1,15 +1,19 @@
#!/usr/bin/env node
/**
* `a2a-mcp` — install once, expose any number of a2acloud agents to your MCP
* `a2amcp` — install once, expose any number of a2acloud agents to your MCP
* client (Claude Code, Cursor, etc.).
*
* No-argument invocation runs the gateway over stdio. That's the form an MCP
* client launches. Subcommands manage the local agent list, auth, etc.
*/
import { createRequire } from "node:module";
import { Command } from "commander";
import readline from "node:readline/promises";
import { stdin as input, stdout as output, stderr } from "node:process";
const require = createRequire(import.meta.url);
const pkg = require("../package.json") as { version: string };
import { ApiError, ControlPlaneClient } from "./api.js";
import { addAgent, loadConfig, removeAgent } from "./config.js";
import {
@@ -24,9 +28,9 @@ import { runStdio } from "./gateway.js";
const program = new Command();
program
.name("a2a-mcp")
.name("a2amcp")
.description("MCP gateway for a2acloud agents.")
.version("0.1.0");
.version(pkg.version);
async function client(apiOverride?: string): Promise<ControlPlaneClient> {
const creds = await loadCredentials();
@@ -69,7 +73,7 @@ program
try {
await runStdio();
} catch (err) {
stderr.write(`a2a-mcp: ${(err as Error).message}\n`);
stderr.write(`a2amcp: ${(err as Error).message}\n`);
process.exit(1);
}
});
@@ -88,7 +92,14 @@ program
const api = resolveApiUrl(opts.api);
try {
const out = await new ControlPlaneClient(api, null).login(email, password);
await saveCredentials({ apiUrl: api, token: out.access_token, email: out.user.email });
const bucket = `user-${out.user.id}-files`;
await saveCredentials({
apiUrl: api,
token: out.access_token,
email: out.user.email,
userId: out.user.id,
bucket,
});
output.write(`logged in as ${out.user.email} @ ${api}\n`);
} catch (err) {
fail(err instanceof ApiError ? err.message : (err as Error).message);
@@ -108,7 +119,7 @@ program
.description("Show the currently logged-in user.")
.action(async () => {
const creds = await loadCredentials();
if (!creds) fail("not logged in (run `a2a-mcp login`)");
if (!creds) fail("not logged in (run `a2amcp login`)");
try {
const me = await (await client()).me();
output.write(`${me.email} (${creds.apiUrl})\n`);
@@ -146,7 +157,7 @@ program
.action(async () => {
const cfg = await loadConfig();
if (cfg.agents.length === 0) {
output.write("(no agents added; try `a2a-mcp add <name>`)\n");
output.write("(no agents added; try `a2amcp add <name>`)\n");
return;
}
for (const a of cfg.agents) output.write(` ${a.name} ${a.url}\n`);
@@ -229,7 +240,7 @@ program
mcpServers: {
a2a: {
command: "npx",
args: ["-y", "@a2amcp/mcp"],
args: ["-y", "a2amcp"],
},
},
};
@@ -237,6 +248,6 @@ program
});
program.parseAsync(process.argv).catch((err) => {
stderr.write(`a2a-mcp: ${(err as Error).message}\n`);
stderr.write(`a2amcp: ${(err as Error).message}\n`);
process.exit(1);
});

View File

@@ -1,7 +1,7 @@
/**
* Reader for ~/.a2a/credentials.json — same file the Python `a2a` CLI writes.
*
* We deliberately *only read* this file (and overwrite it on `a2a-mcp login`)
* We deliberately *only read* this file (and overwrite it on `a2amcp login`)
* so a single login flow is shared between the Python and Node tooling.
*/
import { promises as fs } from "node:fs";
@@ -14,6 +14,8 @@ export interface Credentials {
apiUrl: string;
token: string;
email: string;
userId?: number;
bucket?: string;
}
const credsDir = () => path.join(os.homedir(), ".a2a");
@@ -28,6 +30,8 @@ export async function loadCredentials(): Promise<Credentials | null> {
apiUrl: data.api_url ?? DEFAULT_API_URL,
token: data.token,
email: data.email ?? "",
userId: typeof data.user_id === "number" ? data.user_id : undefined,
bucket: typeof data.bucket === "string" ? data.bucket : undefined,
};
} catch (err: any) {
if (err?.code === "ENOENT") return null;
@@ -37,11 +41,14 @@ export async function loadCredentials(): Promise<Credentials | null> {
export async function saveCredentials(c: Credentials): Promise<void> {
await fs.mkdir(credsDir(), { recursive: true });
await fs.writeFile(
credsFile(),
JSON.stringify({ api_url: c.apiUrl, token: c.token, email: c.email }),
{ mode: 0o600 },
);
const out: Record<string, unknown> = {
api_url: c.apiUrl,
token: c.token,
email: c.email,
};
if (c.userId !== undefined) out.user_id = c.userId;
if (c.bucket !== undefined) out.bucket = c.bucket;
await fs.writeFile(credsFile(), JSON.stringify(out), { mode: 0o600 });
}
export async function clearCredentials(): Promise<boolean> {

View File

@@ -6,6 +6,7 @@
* Naming: ``{agent}__{skill}``. Double underscore separator. Skill names
* containing ``__`` are not supported.
*/
import { createRequire } from "node:module";
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
@@ -17,6 +18,9 @@ import { loadConfig, type EnabledAgent } from "./config.js";
import { loadCredentials } from "./credentials.js";
import { UpstreamAgent, UpstreamError, type UpstreamTool } from "./upstream.js";
const require = createRequire(import.meta.url);
const pkg = require("../package.json") as { version: string };
export const SEP = "__";
export interface GatewayOptions {
@@ -35,20 +39,31 @@ export interface GatewayHandle {
export async function buildGateway(opts: GatewayOptions = {}): Promise<GatewayHandle> {
const cfg = opts.agents ?? (await loadConfig()).agents;
const token =
opts.token !== undefined
? opts.token
: (await loadCredentials())?.token ?? null;
const creds = opts.token !== undefined ? null : await loadCredentials();
const token = opts.token !== undefined ? opts.token : creds?.token ?? null;
// Forward CP credentials + bucket as params._meta so upstream agents
// can act on behalf of the caller (mirrors the platform-orchestrator
// call shape). When the user isn't logged in, _meta is omitted and
// upstream sees the same unauthenticated context as before.
const cpJwt = creds?.token ?? null;
const cpUrl = creds?.apiUrl ?? null;
const bucket = creds?.bucket ?? null;
const upstreams = new Map<string, UpstreamAgent>();
for (const a of cfg) {
upstreams.set(a.name, new UpstreamAgent({ name: a.name, url: a.url, token }));
upstreams.set(
a.name,
new UpstreamAgent({
name: a.name, url: a.url, token,
cpJwt, cpUrl, bucket,
}),
);
}
const server =
opts.server ??
new Server(
{ name: "a2a-mcp", version: "0.1.0" },
{ name: "a2amcp", version: pkg.version },
{ capabilities: { tools: { listChanged: false } } },
);
@@ -70,9 +85,9 @@ export async function buildGateway(opts: GatewayOptions = {}): Promise<GatewayHa
} catch (err) {
// Silently skip an upstream that's down — surfacing an error here
// would break the client's whole tool list. The user can debug via
// `a2a-mcp doctor`.
// `a2amcp doctor`.
process.stderr.write(
`a2a-mcp: skipping ${u.name}: ${(err as Error).message}\n`,
`a2amcp: skipping ${u.name}: ${(err as Error).message}\n`,
);
}
}),

View File

@@ -12,6 +12,18 @@ export interface UpstreamConfig {
name: string;
url: string;
token: string | null;
/** Forwarded to the upstream as ``params._meta.cp_jwt`` on tools/call.
* Lets the upstream agent act on the caller's behalf (file/agent CRUD
* on /v1/me/*) — same path the platform orchestrator uses. */
cpJwt?: string | null;
/** Paired with ``cpJwt``. The upstream agent uses this as the base URL
* when calling back into /v1/me/*. */
cpUrl?: string | null;
/** User's MinIO bucket (``user-<id>-files``). Forwarded as
* ``params._meta.bucket`` so agents that touch the workspace
* (agent-builder, file tools) get a context whose
* ``ctx.workspace.bucket`` resolves to the right bucket. */
bucket?: string | null;
}
export interface UpstreamTool {
@@ -88,9 +100,12 @@ export class UpstreamAgent {
name: string,
arguments_: Record<string, unknown>,
): Promise<UpstreamCallResult> {
return this.rpc<UpstreamCallResult>("tools/call", {
name,
arguments: arguments_,
});
const meta: Record<string, unknown> = {};
if (this.cfg.cpJwt) meta.cp_jwt = this.cfg.cpJwt;
if (this.cfg.cpUrl) meta.cp_url = this.cfg.cpUrl;
if (this.cfg.bucket) meta.bucket = this.cfg.bucket;
const params: Record<string, unknown> = { name, arguments: arguments_ };
if (Object.keys(meta).length > 0) params._meta = meta;
return this.rpc<UpstreamCallResult>("tools/call", params);
}
}