{"record":{"id":"ccda2650a9facede","repo":"paperclipai/paperclip","slug":"railway-unknown-tool","errorCode":"railway_unknown_tool","errorMessage":"Unknown Railway operation.","messagePattern":"Unknown Railway operation\\.","errorType":"error_code","errorClass":"RailwayError","httpStatus":400,"severity":"error","filePath":"server/src/services/railway.ts","lineNumber":254,"sourceCode":"    return data.serviceInstance;\n  }\n\n  async function validateDeployment(args: Record<string, any>) {\n    const data = await query(RAILWAY_QUERIES.deployment, { deploymentId: args.deploymentId });\n    const d = record(data.deployment);\n    if (d.id !== args.deploymentId || d.projectId !== args.projectId || d.environmentId !== args.environmentId || d.serviceId !== args.serviceId) throw new RailwayError(\"railway_target_mismatch\", \"The deployment does not belong to the selected Railway target.\", 403);\n    return d;\n  }\n\n  return {\n    async probe(workspaceId: string) {\n      if (!id.safeParse(workspaceId).success) throw new RailwayError(\"railway_workspace_required\", \"Choose an authorized Railway workspace before checking API access.\", 400);\n      await query(RAILWAY_QUERIES.projects, { workspaceId, first: 1 });\n    },\n    async call(name: string, parameters: unknown): Promise<unknown> {\n      if (isRailwayToolBlocked(name)) throw new RailwayError(\"railway_action_blocked\", \"This Railway action cannot bind its effects to an approved target. Use redeploy, restart, or rollback for an existing deployment.\", 403);\n      const operation = name.slice(RAILWAY_TOOL_PREFIX.length) as Operation;\n      if (!name.startsWith(RAILWAY_TOOL_PREFIX) || !Object.hasOwn(schema, operation)) throw new RailwayError(\"railway_unknown_tool\", \"Unknown Railway operation.\", 400);\n      const parsed = schema[operation].safeParse(parameters);\n      if (!parsed.success) throw new RailwayError(\"railway_invalid_arguments\", \"Invalid Railway operation arguments. Use the exact IDs and limits in the action schema.\", 400);\n      const args = parsed.data as Record<string, any>;\n      let result: unknown;\n      if (operation === \"list-projects\") result = await query(RAILWAY_QUERIES.projects, args);\n      else if (operation === \"list-services\" || operation === \"list-environments\") result = await query(operation === \"list-services\" ? RAILWAY_QUERIES.services : RAILWAY_QUERIES.environments, args);\n      else {\n        const instance = await validateTarget(args);\n        const deployment = args.deploymentId ? await validateDeployment(args) : null;\n        switch (operation) {\n          case \"service-status\": result = instance; break;\n          case \"deployment-status\": result = deployment; break;\n          case \"list-deployments\": result = await query(RAILWAY_QUERIES.deployments, { input: { projectId: args.projectId, environmentId: args.environmentId, serviceId: args.serviceId }, first: args.first, after: args.after }); break;\n          case \"read-logs\": {\n            if (args.startDate && args.endDate && Date.parse(args.startDate) > Date.parse(args.endDate)) throw new RailwayError(\"railway_invalid_arguments\", \"Log start time must precede end time.\", 400);\n            const data = await query(args.kind === \"build\" ? RAILWAY_QUERIES.buildLogs : RAILWAY_QUERIES.runtimeLogs, { deploymentId: args.deploymentId, limit: args.limit, startDate: args.startDate, endDate: args.endDate, filter: args.filter });\n            const lines = data[args.kind === \"build\" ? \"buildLogs\" : \"deploymentLogs\"];\n            if (!Array.isArray(lines)) throw new RailwayError(\"railway_invalid_response\", \"Railway returned invalid log data.\");","sourceCodeStart":236,"sourceCodeEnd":272,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/server/src/services/railway.ts#L236-L272","documentation":"`call()` strips the `paperclip-railway-` prefix and checks the remainder against the known operation schema map (list-projects, list-services, list-environments, service-status, list-deployments, deployment-status, read-logs, redeploy, restart, rollback, run-command). Calls with a name that either doesn't carry the prefix or doesn't map to a known operation throw `railway_unknown_tool` with HTTP 400.","triggerScenarios":"Calling a tool name without the `paperclip-railway-` prefix; a typo like `paperclip-railway-list-projects ` or `paperclip-railway-service-status-v2`; a camelCase variant not covered by the normalizer; a tool from Railway's hosted MCP catalog that isn't part of the direct bridge.","commonSituations":"Hardcoded tool names that drifted from the current catalog; agents inventing operations like `create-service` or `delete-project` (not in the direct schema); name casing mismatch when calling directly instead of via the advertised RAILWAY_TOOLS list.","solutions":["Call only names taken verbatim from the exported RAILWAY_TOOLS array (name field)","Check the name starts with `paperclip-railway-` and matches a kebab-case operation exactly","If the desired action isn't in the direct bridge (e.g. create-project), use Railway's hosted MCP connection or dashboard instead"],"exampleFix":"// before\nawait client.call(\"railway-list-projects\", args); // missing prefix\n// after\nawait client.call(\"paperclip-railway-list-projects\", args);","handlingStrategy":"validation","validationCode":"import { RAILWAY_TOOL_PREFIX, RAILWAY_TOOLS } from \"../services/railway.js\";\nconst known = new Set(RAILWAY_TOOLS.map((t) => t.name));\nif (!known.has(toolName)) throw new Error(`unknown tool: ${toolName}`);","typeGuard":"function isRailwayToolName(v: unknown): v is `${typeof RAILWAY_TOOL_PREFIX}${string}` {\n  return typeof v === \"string\" && v.startsWith(RAILWAY_TOOL_PREFIX) && knownOperations.has(v.slice(RAILWAY_TOOL_PREFIX.length));\n}","tryCatchPattern":"try {\n  return await client.call(name, args);\n} catch (e) {\n  if (isRailwayError(e) && e.code === \"railway_unknown_tool\") {\n    const resolved = RAILWAY_TOOLS.find((t) => normalizeRailwayToolName(t.name) === normalizeRailwayToolName(name));\n    if (resolved) return client.call(resolved.name, args);\n  }\n  throw e;\n}","preventionTips":["Copy tool names from RAILWAY_TOOLS rather than hardcoding strings","Remember the `paperclip-railway-` prefix is required","Use the normalizer (normalizeRailwayToolName) when accepting user/agent-supplied names","Only the 11 direct-bridge operations exist; anything else must go through the hosted MCP connection"],"tags":["railway","tool-name","validation"],"backgroundTag":"unsupported-operation","analyzedSha":"3f1d897a7c018d76563a21c6e39c3c9b03933622","analyzedAt":"2026-09-18T08:03:59.046Z","contentChangedAt":"2026-09-18T08:03:59.046Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}