1 Commits

Author SHA1 Message Date
45e12324df release: 0.1.6 — relay notifications/progress to MCP client
All checks were successful
build / test (push) Successful in 30s
publish / npm (push) Successful in 33s
Long-running upstream skills emit MCP notifications/progress on the SSE
stream. Without forwarding, the gateway swallows them and the MCP
client (Claude Code, Cursor) aborts the tools/call on its idle timer
after ~60s — even though the HTTP stream stays warm.

Gateway now passes the SDK's extra.sendNotification through to
UpstreamAgent.callTool. Any non-response frame with a method but no id
becomes a notification on the client side. Each tools/call carries a
progressToken (client-supplied or request-id fallback) so the client
can correlate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 21:37:58 -03:00
4 changed files with 52 additions and 7 deletions

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "a2amcp", "name": "a2amcp",
"version": "0.1.5", "version": "0.1.6",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "a2amcp", "name": "a2amcp",
"version": "0.1.5", "version": "0.1.6",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@modelcontextprotocol/sdk": "^1.0.4", "@modelcontextprotocol/sdk": "^1.0.4",

View File

@@ -1,6 +1,6 @@
{ {
"name": "a2amcp", "name": "a2amcp",
"version": "0.1.5", "version": "0.1.6",
"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.", "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", "type": "module",
"bin": { "bin": {

View File

@@ -95,7 +95,7 @@ export async function buildGateway(opts: GatewayOptions = {}): Promise<GatewayHa
return { tools }; return { tools };
}); });
server.setRequestHandler(CallToolRequestSchema, async (req) => { server.setRequestHandler(CallToolRequestSchema, async (req, extra) => {
const fullName = req.params.name; const fullName = req.params.name;
const idx = fullName.indexOf(SEP); const idx = fullName.indexOf(SEP);
if (idx < 0) { if (idx < 0) {
@@ -107,10 +107,30 @@ export async function buildGateway(opts: GatewayOptions = {}): Promise<GatewayHa
if (!upstream) { if (!upstream) {
return _softError(`unknown agent: ${agentName}`) as any; return _softError(`unknown agent: ${agentName}`) as any;
} }
// Honor the client's progressToken if it supplied one; else fall
// back to the request id. Either way, when upstream emits
// notifications/progress we forward them via the SDK's
// sendNotification so clients like Claude Code reset their idle
// tool-call timer and don't abort long builds.
const clientToken = (req.params._meta as any)?.progressToken as
| string
| number
| undefined;
const progressToken = clientToken ?? (extra.requestId as string | number);
try { try {
const result = await upstream.callTool( const result = await upstream.callTool(
toolName, toolName,
(req.params.arguments as Record<string, unknown>) ?? {}, (req.params.arguments as Record<string, unknown>) ?? {},
{
progressToken,
onNotification: async (notif) => {
try {
await extra.sendNotification(notif as any);
} catch {
// best effort
}
},
},
); );
// Pass through the upstream's CallToolResult as-is. Upstreams already // Pass through the upstream's CallToolResult as-is. Upstreams already
// populate content / structuredContent / isError per the MCP spec. // populate content / structuredContent / isError per the MCP spec.

View File

@@ -99,14 +99,22 @@ export class UpstreamAgent {
async callTool( async callTool(
name: string, name: string,
arguments_: Record<string, unknown>, arguments_: Record<string, unknown>,
opts: {
onNotification?: (notif: {
method: string;
params?: Record<string, unknown>;
}) => void | Promise<void>;
progressToken?: string | number;
} = {},
): Promise<UpstreamCallResult> { ): Promise<UpstreamCallResult> {
const meta: Record<string, unknown> = {}; const meta: Record<string, unknown> = {};
if (this.cfg.cpJwt) meta.cp_jwt = this.cfg.cpJwt; if (this.cfg.cpJwt) meta.cp_jwt = this.cfg.cpJwt;
if (this.cfg.cpUrl) meta.cp_url = this.cfg.cpUrl; if (this.cfg.cpUrl) meta.cp_url = this.cfg.cpUrl;
if (this.cfg.bucket) meta.bucket = this.cfg.bucket; if (this.cfg.bucket) meta.bucket = this.cfg.bucket;
if (opts.progressToken !== undefined) meta.progressToken = opts.progressToken;
const params: Record<string, unknown> = { name, arguments: arguments_ }; const params: Record<string, unknown> = { name, arguments: arguments_ };
if (Object.keys(meta).length > 0) params._meta = meta; if (Object.keys(meta).length > 0) params._meta = meta;
return this.streamingCallTool(params); return this.streamingCallTool(params, opts.onNotification);
} }
/** /**
@@ -122,6 +130,10 @@ export class UpstreamAgent {
*/ */
private async streamingCallTool( private async streamingCallTool(
params: Record<string, unknown>, params: Record<string, unknown>,
onNotification?: (notif: {
method: string;
params?: Record<string, unknown>;
}) => void | Promise<void>,
): Promise<UpstreamCallResult> { ): Promise<UpstreamCallResult> {
const id = _nextId++; const id = _nextId++;
const headers: Record<string, string> = { const headers: Record<string, string> = {
@@ -188,8 +200,21 @@ export class UpstreamAgent {
} }
return msg.result as UpstreamCallResult; return msg.result as UpstreamCallResult;
} }
// else: server→client request (elicitation/create) or another // Otherwise it's a server→client message. Notifications
// frame; ignore — elicit relaying not wired yet. // (no id) → forward to the MCP client so long-running
// skills keep the client's tool-call timer alive. Requests
// (has id, has method) — e.g. elicitation/create — are
// not relayed yet, so they will time out upstream.
if (msg && typeof msg.method === "string" && !("id" in msg) && onNotification) {
try {
await onNotification({
method: msg.method,
params: msg.params as Record<string, unknown> | undefined,
});
} catch {
// Best-effort; never break the stream on a notify error.
}
}
} }
sep = buf.indexOf("\n\n"); sep = buf.indexOf("\n\n");
} }