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

  1. Retry via 'Refresh actions' to reinitialize the MCP session
  2. Reconnect the Railway connection to get a new session and token
  3. Inspect Railway MCP response/logs for the underlying tool error
  4. 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

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)