paperclipai/paperclip · error · Error
Workspace discovery failed
Error message
Workspace discovery failed
What it means
Internal sentinel thrown inside the try block of discoverRailwayWorkspace when the parsed MCP response carries an error payload or result.isError is true. It is immediately caught and rethrown as RailwayError 'railway_workspace_discovery_failed', so callers should never observe it directly.
Solutions
- Retry via 'Refresh actions' to reinitialize the MCP session
- Reconnect the Railway connection to get a new session and token
- Inspect Railway MCP response/logs for the underlying tool error
- Retry after confirming Railway status is healthy
Example fix
// before
let client = createRailwayClient({ authorization }); // MCP session stale
await client.workspaceId();
// after
await refreshRailwayConnectionSession(connection); // force new initializeMcpHttpSession
let client = createRailwayClient({ authorization }); Defensive patterns
Strategy: try-catch
Type guard
function isMcpErrorResult(payload) {
return Boolean(payload?.error || payload?.result?.isError);
} Try / catch
try {
const ws = await client.workspaceId();
} catch (e) {
if (e?.code === "railway_workspace_discovery_failed") {
await reconnectRailwaySession(conn); // stale MCP session is the usual cause
} else throw e;
} Prevention
- Reinitialize MCP sessions proactively instead of reusing long-lived ones
- Log the underlying MCP error/isError result for diagnosis
- Treat any isError MCP result as a session-refresh signal
When it happens
Trigger: MCP list-workspaces tool responds with payload.error set, or result.isError === true, e.g. Railway MCP reports a tool-level failure (invalid session, tool not permitted, upstream Railway API failure).
Common situations: MCP session became invalid between init and call; Railway MCP tool returns an isError result for an account with API problems; malformed structuredContent causing record() to throw.
Related errors
AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18).
Data as JSON: /api/errors/f03d5cfc5d9db9ff.
Report an issue: GitHub.
Appendix: source
Thrown at server/src/services/railway.ts:186
const list = (requestHeaders: Record<string, string>) => send({
method: "POST", headers: mcpHttpRequestHeaders(requestHeaders),
body: JSON.stringify({ jsonrpc: "2.0", id: "paperclip-railway-workspace-probe", method: "tools/call", params: { name: "list-workspaces", arguments: {} } }),
});
let response = await list(headers);
if (response.status === 400) {
await response.body?.cancel();
response = await list(await initializeMcpHttpSession({ send, headers, requestId: "paperclip-railway-workspace-probe" }));
}
if (!response.ok) {
await response.body?.cancel();
throw new RailwayError("railway_workspace_discovery_failed", "Railway's hosted connection is connected, but workspace access could not be checked. Refresh actions to try again.");
}
const body = await boundedResponseText(response, options.signal);
let data: Record<string, any>;
try {
const payload = record(parseMcpHttpResponseBody(body, response.headers.get("content-type")));
const result = record(payload.result);
if (payload.error || result.isError) throw new Error("Workspace discovery failed");
data = record(result.structuredContent ?? JSON.parse(result.content?.find((item: any) => item.type === "text")?.text ?? "{}"));
} catch { throw new RailwayError("railway_workspace_discovery_failed", "Railway could not list authorized workspaces. Refresh actions or reconnect and select a workspace."); }
const workspaceId = Array.isArray(data.workspaces) ? data.workspaces.find((workspace) => id.safeParse(workspace?.id).success)?.id : undefined;
if (!workspaceId) throw new RailwayError("railway_workspace_required", "No authorized Railway workspace was found. Reconnect Railway and select a workspace to enable direct operations.", 403);
return workspaceId;
}
export function createRailwayClient(options: RailwayClientOptions) {
if (!/^Bearer [^\r\n]+$/.test(options.authorization)) throw new RailwayError("railway_authorization_required", "Reconnect Railway to authorize API access.", 401);
const secret = options.authorization.slice(7);
const redact = (value: unknown) => JSON.parse(redactSensitiveText(JSON.stringify(value).split(secret).join("[REDACTED]")));
async function query(document: string, variables: Record<string, unknown>): Promise<Record<string, any>> {
options.signal.throwIfAborted();
let response: Response;
try {
response = await options.request(RAILWAY_API_URL, { method: "POST", redirect: "error", signal: options.signal, headers: { "content-type": "application/json", Authorization: options.authorization }, body: JSON.stringify({ query: document, variables }) });
} catch (error) {View on GitHub (pinned to 3f1d897a7c)