ruvnet/ruflo · error · Error

gateway tool error

Error message

gateway tool error

What it means

Thrown by `gatewayTool` after a successful `gatewayRpc` round-trip when the remote tool reported failure: either the MCP response has `isError: true` or the parsed content JSON contains an `error` field. Unlike error 26, the JSON-RPC layer succeeded — the tool executed but returned an error payload. Falls back to the literal 'gateway tool error' when the parsed body has no `error` message.

Solutions

  1. Inspect the thrown message — if it is the generic fallback 'gateway tool error', enable verbose logging or call the gateway directly to see the tool's full content payload.
  2. Validate the tool's input against its declared inputSchema before invoking (e.g. required `msgType`/`payload` for federation_publish).
  3. For admin-gated tools, ensure RUFLO_X_ADMIN_TOKEN is set and valid (error 28/29 catch the missing-token case earlier; an invalid token may surface here).
  4. Retry on transient gateway-side failures; check gateway status/logs if errors persist.
  5. Confirm gateway/tool version compatibility if the response shape changed after a gateway upgrade.

Example fix

// before
const out = await gatewayTool('federation_publish', { msgType: 'Status' });
// after: guard required fields first
if (!msgType || !payload) throw new Error('federation_publish requires msgType and payload');
const out = await gatewayTool('federation_publish', { msgType, payload });
Defensive patterns

Strategy: type-guard

Validate before calling

const required = ['msgType', 'payload']; // per tool inputSchema
for (const k of required) {
  if (!(k in args)) throw new Error(`x_federation tool missing required field: ${k}`);
}

Type guard

type ToolResponse = { content?: Array<{ text?: string }>; isError?: boolean };
function toolReportedError(r: ToolResponse, parsed: Record<string, unknown>): boolean {
  return r.isError === true || 'error' in parsed;
}

Try / catch

try {
  const parsed = await gatewayTool(name, args);
} catch (e) {
  if (e instanceof Error && e.message === 'gateway tool error') {
    console.error('tool failed without a message — inspect full gateway content payload / gateway logs');
  } else if (e instanceof Error) {
    console.error('tool error:', e.message);
  } else throw e;
}

Prevention

When it happens

Trigger: Calling any `x_federation_*` gateway tool where the remote handler failed but responded via the MCP `isError` flag or an `{ error: ... }` content body: e.g. `federation_publish` rejecting a msgType/payload, `federation_invite_mint` failing server-side, or a tool returning an error result for invalid input while still returning HTTP 200 and a valid JSON-RPC result.

Common situations: Publishing a message with a payload the gateway's schema rejects; minting invites when the relay-side write fails; a gateway version mismatch where the remote tool's response shape changed (isError set but no parseable message); permissions/auth failures inside the tool that surface as tool-level errors rather than RPC errors.

Related errors


AI-assisted analysis of ruvnet/ruflo@9c61c86f06 (2026-09-22). Data as JSON: /api/errors/d3211683f05d4967. Report an issue: GitHub.

Appendix: source

Thrown at v3/@claude-flow/cli/src/mcp-tools/x-federation-tools.ts:118

 */
export function relayPayload(text: string): Record<string, unknown> {
  const parsed = parseGatewayText(text);
  return (parsed.untrusted === true && parsed.data !== undefined
    ? (parsed.data as Record<string, unknown>)
    : parsed);
}

async function gatewayTool(name: string, args: Record<string, unknown>): Promise<unknown> {
  const { gatewayUrl, ...rest } = args;
  const r = (await gatewayRpc('tools/call', { name, arguments: rest }, gatewayUrl)) as { content?: Array<{ text?: string }>; isError?: boolean };
  const raw = r.content?.[0]?.text ?? '{}';
  // An isError result is the SDK's createToolError, whose message is raw text and
  // not JSON. Parsing first turns "private channels cannot be published…" into
  // "Unexpected token 'p'" — the same class of bug this parser exists to fix.
  if (r.isError) {
    let msg = raw;
    try { msg = String((parseGatewayText(raw) as { error?: unknown }).error ?? raw); } catch { /* raw text: use as-is */ }
    throw new Error(msg || 'gateway tool error');
  }
  const parsed = parseGatewayText(raw);
  if (parsed.error) throw new Error(String(parsed.error));
  return parsed;
}
async function gatewayResource(uri: string, gatewayUrl?: unknown): Promise<unknown> {
  const r = (await gatewayRpc('resources/read', { uri }, gatewayUrl)) as { contents?: Array<{ text?: string }> };
  return parseGatewayText(r.contents?.[0]?.text ?? '{}');
}
// Credential: intentionally env-only (a secret must never be a CLI flag — it would land in
// shell history / process lists). Registered in scripts/audit-env-var-precedence.mjs.
const adminToken = (): string | undefined => process.env.RUFLO_X_ADMIN_TOKEN;

export const xFederationTools: MCPTool[] = [
  {
    name: 'x_federation_sync',
    description:
      'Fetch recent signature-verified coordination messages from the open x.ruv.io swarm federation (Nostr, #t=ruflo-swarm). Use when you need to see what other ruflo nodes across the internet have posted (PeerHello/Status/Task/Result/Claim*). Reading the relay directly is wrong because you would have to do NIP-42 auth yourself; the gateway does it and only returns events whose signatures verify.',

View on GitHub (pinned to 9c61c86f06)