paperclipai/paperclip · error

Paperclip API origin is unavailable

Error message

Paperclip API origin is unavailable

What it means

The project-tools MCP endpoint proxies tool calls to the Paperclip API via callProjectTool, and it requires the origin of that API from the PAPERCLIP_API_URL environment variable. When the variable is unset (empty string is falsy), the server cannot construct the callback URL and throws this error instead of making a doomed request. It is a startup/deployment configuration guard for the tool-call path only.

Solutions

  1. Set PAPERCLIP_API_URL to the externally reachable origin of the Paperclip API (e.g. https://paperclip.example.com) and restart the server.
  2. In dev, use the standard `pnpm dev` flow which provisions the API origin automatically.
  3. Add the variable to your deployment manifest/Dockerfile env and verify with `curl $PAPERCLIP_API_URL/api/health`.
  4. If the server should default to localhost, export PAPERCLIP_API_URL=http://localhost:3100 in the shell profile.

Example fix

// before (docker-compose.yml)
// environment:
//   - NODE_ENV=production
// after
environment:
  - NODE_ENV=production
  - PAPERCLIP_API_URL=https://paperclip.internal.example.com
Defensive patterns

Strategy: validation

Validate before calling

if (!process.env.PAPERCLIP_API_URL) {
  throw new Error("PAPERCLIP_API_URL must be set for project tool calls");
}

Type guard

null

Try / catch

try { await callMcpTool(name, args); }
catch (e) {
  if (e.message === "Paperclip API origin is unavailable") {
    process.exitCode = 1; // fail fast at boot, not at call time
  }
  throw e;
}

Prevention

When it happens

Trigger: A tools/call request reaches /api project-tools route in a deployment where process.env.PAPERCLIP_API_URL is not set; the tools/list path works because it never needs the origin, so the misconfiguration only surfaces on actual tool invocation.

Common situations: Self-hosted deployments behind a reverse proxy where only PORT/HOST were configured; running the server outside the standard dev script which sets PAPERCLIP_API_URL; Docker/K8s manifests missing the env entry; new env-var naming after a refactor.

Understand the failure class

Background: "environment variable is not set" and "Missing keys in environment" errors: what missing required env var messages mean and how to fix them — this error's family across 28 libraries.

Related errors


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

Appendix: source

Thrown at server/src/routes/project-tools.ts:24

import { forbidden } from "../errors.js";

/** Mounted after actor middleware; connection-scoped tokens cannot authenticate here. */
export function projectToolRoutes(db: Db) {
  const router = Router();
  router.post("/mcp/project-tools", async (req, res) => {
    const context = await projectToolContext(db, req.actor);
    assertCompanyAccess(req, context.run.companyId);
    const { id = null, method, params } = req.body;
    const send = (result: unknown) => res.json({ jsonrpc: "2.0", id, result });
    if (method === "initialize") return send({ protocolVersion: "2025-03-26", capabilities: { tools: { listChanged: false } }, serverInfo: { name: "paperclip-project-tools", version: "1" } });
    if (method === "notifications/initialized") return res.status(202).end();
    const definitions = projectToolDefinitions(context.issue.workMode, true);
    if (method === "tools/list") return send({ tools: definitions });
    if (method !== "tools/call") return res.json({ jsonrpc: "2.0", id, error: { code: -32601, message: "Method not found" } });
    try {
      if (!definitions.some(tool => tool.name === params?.name)) throw forbidden("Tool is unavailable in this mode");
      const apiUrl = process.env.PAPERCLIP_API_URL;
      if (!apiUrl) throw new Error("Paperclip API origin is unavailable");
      const result = await callProjectTool({
        name: params.name, arguments: params.arguments ?? {}, apiUrl,
        token: req.header("authorization")!.replace(/^Bearer\s+/i, ""),
        companyId: context.run.companyId, issueId: context.issue.id, agentId: context.run.agentId,
        conversation: Boolean(context.issue.conversationAgentId),
      });
      return send({ content: [{ type: "text", text: JSON.stringify(result) }], structuredContent: result });
    } catch (error) {
      return send({ isError: true, content: [{ type: "text", text: error instanceof Error ? error.message : "Project tool failed" }] });
    }
  });
  return router;
}

View on GitHub (pinned to 3f1d897a7c)