paperclipai/paperclip · error

Plugin does not expose scoped API routes

Error message

Plugin does not expose scoped API routes

What it means

Returned as HTTP 404 by the scoped API gateway when the plugin is found and ready but its manifest capabilities array does not include "api.routes.register". The gateway refuses to serve /api/plugins/:pluginId/api/* because the plugin never declared that it exposes scoped HTTP routes. The 404 is deliberate: to callers without knowledge of the plugin, the surface does not exist.

Source

Thrown at server/src/routes/plugins.ts:1857

    const { pluginId } = req.params;
    const plugin = await resolvePlugin(registry, pluginId);
    if (!plugin) {
      res.status(404).json({ error: "Plugin not found" });
      return;
    }
    if (plugin.status !== "ready") {
      res.status(503).json({ error: `Plugin is not ready (current status: ${plugin.status})` });
      return;
    }
    const isWorkerRunning = typeof bridgeDeps.workerManager.isRunning === "function"
      ? bridgeDeps.workerManager.isRunning(plugin.id)
      : true;
    if (!isWorkerRunning) {
      res.status(503).json({ error: "Plugin worker is not running" });
      return;
    }
    if (!plugin.manifestJson.capabilities.includes("api.routes.register")) {
      res.status(404).json({ error: "Plugin does not expose scoped API routes" });
      return;
    }

    const requestPath = req.path || "/";
    const routes = plugin.manifestJson.apiRoutes ?? [];
    const match = routes
      .map((route) => ({ route, params: matchScopedApiRoute(route, req.method, requestPath) }))
      .find((candidate) => candidate.params !== null);
    if (!match || !match.params) {
      res.status(404).json({ error: "Plugin API route not found" });
      return;
    }

    try {
      assertScopedApiAuth(req, match.route);
      const companyId = await resolveScopedApiCompanyId(match.route, match.params, req);
      if (!companyId) {
        res.status(400).json({ error: "Unable to resolve company for plugin API route" });

View on GitHub (pinned to a7e689b3c3)

Solutions

  1. GET /api/plugins/:pluginId and inspect manifestJson.capabilities to confirm "api.routes.register" is absent.
  2. If the plugin should expose routes, add "api.routes.register" plus an apiRoutes array to its manifest and reinstall or upgrade the plugin so the stored manifest is refreshed.
  3. Otherwise use the plugin's actual surface: tools via /api/plugins/tools/execute, or the UI bridge routes (/bridge/data, /bridge/action).

Example fix

// before — plugin manifest.json
{ "name": "acme-linear", "capabilities": ["tools.register"] }

// after
{
  "name": "acme-linear",
  "capabilities": ["tools.register", "api.routes.register"],
  "apiRoutes": [
    { "routeKey": "list-issues", "method": "GET", "path": "/issues", "auth": "agent" }
  ]
}
Defensive patterns

Strategy: validation

Validate before calling

async function pluginHasApiRoutes(apiBase: string, pluginId: string): Promise<boolean> {
  const res = await fetch(`${apiBase}/api/plugins/${encodeURIComponent(pluginId)}`);
  const plugin = await res.json();
  return Array.isArray(plugin.manifestJson?.capabilities)
    && plugin.manifestJson.capabilities.includes("api.routes.register");
}

Type guard

function exposesScopedApi(plugin: { manifestJson?: { capabilities?: string[] } }): boolean {
  return plugin.manifestJson?.capabilities?.includes("api.routes.register") === true;
}

Prevention

When it happens

Trigger: Calling /api/plugins/:pluginId/api/<anything> on a plugin whose manifest.json capabilities only list e.g. ["tools.register"] or ["ui.contributions"] — a plugin that exposes tools or UI slots but not HTTP routes.

Common situations: Developer assumes every plugin serves HTTP routes; manifest capability string typo (exact match required: "api.routes.register"); an old plugin version predating scoped API support; manifest edited on disk but the plugin never reinstalled/upgraded so the DB copy still lacks the capability.

Related errors


AI-assisted analysis of paperclipai/paperclip@a7e689b3c3 (2026-08-18). Data as JSON: /api/errors/db274b9d4f8f16d3. Report an issue: GitHub.