{"record":{"id":"db274b9d4f8f16d3","repo":"paperclipai/paperclip","slug":"plugin-does-not-expose-scoped-api-routes","errorCode":null,"errorMessage":"Plugin does not expose scoped API routes","messagePattern":"Plugin does not expose scoped API routes","errorType":"http","errorClass":null,"httpStatus":404,"severity":"error","filePath":"server/src/routes/plugins.ts","lineNumber":1857,"sourceCode":"    const { pluginId } = req.params;\n    const plugin = await resolvePlugin(registry, pluginId);\n    if (!plugin) {\n      res.status(404).json({ error: \"Plugin not found\" });\n      return;\n    }\n    if (plugin.status !== \"ready\") {\n      res.status(503).json({ error: `Plugin is not ready (current status: ${plugin.status})` });\n      return;\n    }\n    const isWorkerRunning = typeof bridgeDeps.workerManager.isRunning === \"function\"\n      ? bridgeDeps.workerManager.isRunning(plugin.id)\n      : true;\n    if (!isWorkerRunning) {\n      res.status(503).json({ error: \"Plugin worker is not running\" });\n      return;\n    }\n    if (!plugin.manifestJson.capabilities.includes(\"api.routes.register\")) {\n      res.status(404).json({ error: \"Plugin does not expose scoped API routes\" });\n      return;\n    }\n\n    const requestPath = req.path || \"/\";\n    const routes = plugin.manifestJson.apiRoutes ?? [];\n    const match = routes\n      .map((route) => ({ route, params: matchScopedApiRoute(route, req.method, requestPath) }))\n      .find((candidate) => candidate.params !== null);\n    if (!match || !match.params) {\n      res.status(404).json({ error: \"Plugin API route not found\" });\n      return;\n    }\n\n    try {\n      assertScopedApiAuth(req, match.route);\n      const companyId = await resolveScopedApiCompanyId(match.route, match.params, req);\n      if (!companyId) {\n        res.status(400).json({ error: \"Unable to resolve company for plugin API route\" });","sourceCodeStart":1839,"sourceCodeEnd":1875,"githubUrl":"https://github.com/paperclipai/paperclip/blob/a7e689b3c35347b529cb9f54c9b9a8575a3dcab6/server/src/routes/plugins.ts#L1839-L1875","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["GET /api/plugins/:pluginId and inspect manifestJson.capabilities to confirm \"api.routes.register\" is absent.","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.","Otherwise use the plugin's actual surface: tools via /api/plugins/tools/execute, or the UI bridge routes (/bridge/data, /bridge/action)."],"exampleFix":"// before — plugin manifest.json\n{ \"name\": \"acme-linear\", \"capabilities\": [\"tools.register\"] }\n\n// after\n{\n  \"name\": \"acme-linear\",\n  \"capabilities\": [\"tools.register\", \"api.routes.register\"],\n  \"apiRoutes\": [\n    { \"routeKey\": \"list-issues\", \"method\": \"GET\", \"path\": \"/issues\", \"auth\": \"agent\" }\n  ]\n}","handlingStrategy":"validation","validationCode":"async function pluginHasApiRoutes(apiBase: string, pluginId: string): Promise<boolean> {\n  const res = await fetch(`${apiBase}/api/plugins/${encodeURIComponent(pluginId)}`);\n  const plugin = await res.json();\n  return Array.isArray(plugin.manifestJson?.capabilities)\n    && plugin.manifestJson.capabilities.includes(\"api.routes.register\");\n}","typeGuard":"function exposesScopedApi(plugin: { manifestJson?: { capabilities?: string[] } }): boolean {\n  return plugin.manifestJson?.capabilities?.includes(\"api.routes.register\") === true;\n}","tryCatchPattern":null,"preventionTips":["Declare \"api.routes.register\" plus an apiRoutes array in the plugin manifest at authoring time, not after clients need it.","Feature-detect: only show/call plugin HTTP surfaces when the capability is present.","Reinstall or upgrade after manifest edits so the stored manifestJson matches what ships."],"tags":["plugin","manifest","capabilities","http-404","scoped-api"],"backgroundTag":"missing-capability","analyzedSha":"a7e689b3c35347b529cb9f54c9b9a8575a3dcab6","analyzedAt":"2026-08-18T22:49:45.177Z","schemaVersion":2},"datasetVersion":"2026-08-22T04:17:13.399Z"}