/**
 * Transparent stdio→HTTP MCP proxy (Increment 1 of the unified multi-peer
 * editing model — see ~/.claude/plans/indexed-stirring-turtle.md).
 *
 * `studiograph mcp` no longer writes entities directly. Instead it presents an
 * MCP server to Claude Desktop over stdio and forwards every `tools/list` and
 * `tools/call` to an upstream MCP `Client`:
 *
 *   - REMOTE: a `StreamableHTTPClientTransport` to the running local server's
 *     `/mcp` endpoint, so the write goes through BodyWriteCoordinator and merges
 *     into any open live editor (this is the data-loss fix).
 *   - LOCAL: an in-process server (InMemoryTransport) built lazily on demand,
 *     used only when no live server is reachable — in which case there is no
 *     open editor to clobber, so a direct write is safe.
 *
 * The upstream is re-selected on EVERY forwarded request (`probeServer`), which
 * closes the "server started after Claude Desktop" window.
 *
 * FAIL-LOUD: if a request is routed to the remote and the transport throws, the
 * error is surfaced to Claude Desktop and the remote client is marked stale so
 * the next call re-probes. We NEVER silently retry the failed call against the
 * local in-process server — that silent fallback is exactly the data-loss path
 * this increment removes. A `probeServer` `fail` (the server rejected our token)
 * is likewise surfaced, never downgraded to a local write.
 *
 * NOTE: stdout is owned by the JSON-RPC protocol — never console.log here. Use
 * log.error (writes to stderr).
 */
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
import { CallToolRequestSchema, ListToolsRequestSchema, McpError, ErrorCode, } from '@modelcontextprotocol/sdk/types.js';
import { probeServer } from './server-discovery.js';
import { MCP_SERVER_INFO, MCP_INSTRUCTIONS } from './tools.js';
import { log } from '../utils/log.js';
/** Connect an MCP Client to a running server's /mcp over Streamable HTTP. */
export async function createRemoteClient(url, token) {
    const client = new Client({ name: 'studiograph-stdio-proxy', version: '1.0.0' });
    const transport = new StreamableHTTPClientTransport(new URL(`${url}/mcp`), {
        requestInit: { headers: { Authorization: `Bearer ${token}` } },
    });
    await client.connect(transport);
    return client;
}
export class StdioProxy {
    opts;
    probeFn;
    buildRemoteClient;
    remote = null;
    localClient = null;
    constructor(opts) {
        this.opts = opts;
        this.probeFn = opts.probeFn ?? probeServer;
        this.buildRemoteClient = opts.buildRemoteClient ?? createRemoteClient;
    }
    /**
     * Probe + return the upstream client for this request. Throws an McpError on
     * a `fail` probe (token rejected by a live server) — never falls back local.
     */
    async selectUpstream() {
        const result = await this.probeFn(this.opts.workspacePath, this.opts.token);
        if (result.mode === 'fail') {
            this.markRemoteStale();
            throw new McpError(ErrorCode.InternalError, `Studiograph server is running but rejected the token: ${result.reason}. ` +
                `Check STUDIOGRAPH_MCP_TOKEN. (Not falling back to a local write — a live ` +
                `editor may be open and a local write could overwrite unsaved edits.)`);
        }
        if (result.mode === 'remote') {
            if (this.remote && this.remote.url === result.url)
                return this.remote.client;
            // URL changed or no cached remote — (re)connect.
            this.markRemoteStale();
            const client = await this.buildRemoteClient(result.url, this.opts.token);
            this.remote = { client, url: result.url };
            return client;
        }
        // local
        if (!this.localClient) {
            this.localClient = await this.opts.buildLocalClient();
        }
        return this.localClient;
    }
    markRemoteStale() {
        if (this.remote) {
            void this.remote.client.close().catch(() => { });
            this.remote = null;
        }
    }
    /** Forward a tools/list to the selected upstream. */
    async forwardListTools(params) {
        const client = await this.selectUpstream();
        try {
            return await client.listTools(params);
        }
        catch (err) {
            this.onUpstreamError(err);
            throw toMcpError(err);
        }
    }
    /** Forward a tools/call to the selected upstream. */
    async forwardCallTool(params) {
        const client = await this.selectUpstream();
        try {
            // A tool that fails returns a normal result with `isError: true`; the SDK
            // does NOT throw for that, so it forwards verbatim. A throw here is a
            // transport/protocol failure — fail loud + re-probe next call.
            return await client.callTool(params);
        }
        catch (err) {
            this.onUpstreamError(err);
            throw toMcpError(err);
        }
    }
    /** On a transport error against the remote, drop the cached client. */
    onUpstreamError(err) {
        log.error(`[stdio-proxy] upstream call failed: ${err instanceof Error ? err.message : String(err)}`);
        this.markRemoteStale();
    }
    /** Wire the two forwarders onto a low-level MCP Server. */
    registerHandlers(server) {
        server.setRequestHandler(ListToolsRequestSchema, async (req) => this.forwardListTools(req.params));
        server.setRequestHandler(CallToolRequestSchema, async (req) => this.forwardCallTool(req.params));
    }
    /** Build the stdio-facing Server and connect it to Claude Desktop. */
    async start() {
        const server = new Server(MCP_SERVER_INFO, {
            instructions: MCP_INSTRUCTIONS,
            capabilities: { tools: {} },
        });
        this.registerHandlers(server);
        const transport = new StdioServerTransport();
        await server.connect(transport);
        log.error('Studiograph MCP stdio proxy ready');
        process.on('SIGINT', async () => {
            this.markRemoteStale();
            await this.localClient?.close().catch(() => { });
            await server.close();
            process.exit(0);
        });
    }
}
/** Normalize an arbitrary upstream error into an McpError for the client. */
function toMcpError(err) {
    if (err instanceof McpError)
        return err;
    return new McpError(ErrorCode.InternalError, err instanceof Error ? err.message : String(err));
}
//# sourceMappingURL=stdio-proxy.js.map