paperclipai/paperclip · error · RailwayError
railway_unknown_tool
railway_unknown_tool
Error message
Unknown Railway operation.
What it means
`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.
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
Example fix
// before
await client.call("railway-list-projects", args); // missing prefix
// after
await client.call("paperclip-railway-list-projects", args); Defensive patterns
Strategy: validation
Validate before calling
import { RAILWAY_TOOL_PREFIX, RAILWAY_TOOLS } from "../services/railway.js";
const known = new Set(RAILWAY_TOOLS.map((t) => t.name));
if (!known.has(toolName)) throw new Error(`unknown tool: ${toolName}`); Type guard
function isRailwayToolName(v: unknown): v is `${typeof RAILWAY_TOOL_PREFIX}${string}` {
return typeof v === "string" && v.startsWith(RAILWAY_TOOL_PREFIX) && knownOperations.has(v.slice(RAILWAY_TOOL_PREFIX.length));
} Try / catch
try {
return await client.call(name, args);
} catch (e) {
if (isRailwayError(e) && e.code === "railway_unknown_tool") {
const resolved = RAILWAY_TOOLS.find((t) => normalizeRailwayToolName(t.name) === normalizeRailwayToolName(name));
if (resolved) return client.call(resolved.name, args);
}
throw e;
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
Background: UnsupportedOperationException and "is not supported" errors: when a library deliberately refuses a call — this error's family across 30 libraries.
Related errors
- railway_authorization_required
- railway_invalid_arguments
- railway_ssh_host_key_invalid
- railway_target_mismatch
- railway_target_mismatch
AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18).
Data as JSON: /api/errors/ccda2650a9facede.
Report an issue: GitHub.
Appendix: source
Thrown at server/src/services/railway.ts:254
return data.serviceInstance;
}
async function validateDeployment(args: Record<string, any>) {
const data = await query(RAILWAY_QUERIES.deployment, { deploymentId: args.deploymentId });
const d = record(data.deployment);
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);
return d;
}
return {
async probe(workspaceId: string) {
if (!id.safeParse(workspaceId).success) throw new RailwayError("railway_workspace_required", "Choose an authorized Railway workspace before checking API access.", 400);
await query(RAILWAY_QUERIES.projects, { workspaceId, first: 1 });
},
async call(name: string, parameters: unknown): Promise<unknown> {
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);
const operation = name.slice(RAILWAY_TOOL_PREFIX.length) as Operation;
if (!name.startsWith(RAILWAY_TOOL_PREFIX) || !Object.hasOwn(schema, operation)) throw new RailwayError("railway_unknown_tool", "Unknown Railway operation.", 400);
const parsed = schema[operation].safeParse(parameters);
if (!parsed.success) throw new RailwayError("railway_invalid_arguments", "Invalid Railway operation arguments. Use the exact IDs and limits in the action schema.", 400);
const args = parsed.data as Record<string, any>;
let result: unknown;
if (operation === "list-projects") result = await query(RAILWAY_QUERIES.projects, args);
else if (operation === "list-services" || operation === "list-environments") result = await query(operation === "list-services" ? RAILWAY_QUERIES.services : RAILWAY_QUERIES.environments, args);
else {
const instance = await validateTarget(args);
const deployment = args.deploymentId ? await validateDeployment(args) : null;
switch (operation) {
case "service-status": result = instance; break;
case "deployment-status": result = deployment; break;
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;
case "read-logs": {
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);
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 });
const lines = data[args.kind === "build" ? "buildLogs" : "deploymentLogs"];
if (!Array.isArray(lines)) throw new RailwayError("railway_invalid_response", "Railway returned invalid log data.");View on GitHub (pinned to 3f1d897a7c)