paperclipai/paperclip · error · ToolGatewayHttpError

input.reasonCode

input.reasonCode

Error message

Gateway bearer token is expired or invalid

What it means

This 401 ToolGatewayHttpError is thrown inside recordNamedGatewayAuthFailure when the gateway bearer token presented to the named MCP gateway cannot be resolved to any existing gateway record (findGatewayForProtocolLocator returns null). The gateway routes MCP protocol requests by hashing the bearer token and looking up tool_mcp_gateway_tokens joined with tool_mcp_gateways; when no row matches, there is no gateway to authenticate against, so the request is rejected with the caller's reasonCode (typically 'gateway_token_invalid'). Repeated failures against the same gateway/token are additionally throttled with a 429 'gateway_auth_throttled'.

Solutions

  1. Regenerate the gateway bearer token from the board UI (gateway settings) and update the client's Authorization header / MCP config with the new value.
  2. Verify the token belongs to the same Paperclip instance and company as the gateway you are calling; cross-instance tokens can never match the hash lookup.
  3. Check the token is the named gateway token (pcgw_ prefixed) and not an agent_api_key or other bearer credential.
  4. If failures repeat, wait for the auth-failure throttle window to reset before retrying, or the request will be rejected with 429 gateway_auth_throttled.

Example fix

// before: stale token cached in MCP client config
"headers": { "Authorization": "Bearer pcgw_old_deleted_token" }
// after: freshly minted gateway token from board gateway settings
"headers": { "Authorization": "Bearer pcgw_<newly-generated-token>" }
Defensive patterns

Strategy: try-catch

Validate before calling

// before calling the gateway, check the token shape and non-emptiness
const token = process.env.PAPERCLIP_GATEWAY_TOKEN?.trim();
if (!token || !token.startsWith("pcgw_")) {
  throw new Error("PAPERCLIP_GATEWAY_TOKEN is missing or not a named gateway token");
}

Type guard

function isNamedGatewayToken(v: unknown): v is string {
  return typeof v === "string" && v.trim().startsWith("pcgw_") && v.trim().length > 10;
}

Try / catch

try {
  await mcpGatewayCall(token);
} catch (err) {
  if (err?.status === 401 && err?.code === "gateway_token_invalid") {
    // token unknown to this instance: re-mint from board settings and reload config
    token = await issueNewGatewayToken();
  } else if (err?.status === 429 && err?.code === "gateway_auth_throttled") {
    await sleep(err.details?.retryAfterMs ?? 60000);
  } else throw err;
}

Prevention

When it happens

Trigger: Calling any MCP gateway protocol method (e.g. initialize, tools/call) with an Authorization: Bearer token whose hash does not match any row in tool_mcp_gateway_tokens — a token that was never issued for this instance, a token from a different environment (staging vs prod), or a token with a typo/whitespace-only difference that fails the hash lookup.

Common situations: Copying a pcgw_ token from a different Paperclip instance or company; an old token deleted from the board while a client (agent adapter, mcp client config) still caches it; rotating gateway tokens without updating the client's MCP server config; passing a regular agent API key instead of a gateway bearer token.

Understand the failure class

Related errors


AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18). Data as JSON: /api/errors/b2e5c1b0c4d10a31. Report an issue: GitHub.

Appendix: source

Thrown at server/src/services/tool-gateway.ts:6671

  async function recordNamedGatewayAuthFailure(input: {
    gatewayId?: string | null;
    gatewayPublicId?: string | null;
    bearerToken: string;
    reasonCode: string;
    clientMetadata: ReturnType<typeof safeClientMetadata>;
  }): Promise<never> {
    const token = input.bearerToken.trim();
    const tokenId = namedGatewayTokenId(token);
    const gatewayKey = input.gatewayId
      ? `id:${input.gatewayId}`
      : `public:${input.gatewayPublicId ?? "unknown"}`;
    const tokenKey = tokenId
      ? `id:${tokenId}`
      : `hash:${hashGatewayToken(token).slice(0, 24)}`;
    const gateway = await findGatewayForProtocolLocator(input);
    if (!gateway) {
      throw new ToolGatewayHttpError(
        401,
        "Gateway bearer token is expired or invalid",
        input.reasonCode,
      );
    }
    const gatewayState = await consumeProtocolRateLimit({
      companyId: gateway.companyId,
      counterKey: `mcp_gateway_auth_failure:gateway:${gatewayKey}`,
      config: protocolLimits.authFailures,
    });
    const tokenState = await consumeProtocolRateLimit({
      companyId: gateway.companyId,
      counterKey: `mcp_gateway_auth_failure:token:${gatewayKey}:${tokenKey}`,
      config: protocolLimits.authFailures,
    });
    const limited = gatewayState.limited || tokenState.limited;
    if (limited) {
      const limiterKeyClass = gatewayState.limited

View on GitHub (pinned to 3f1d897a7c)