"use strict";
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2024-2026 Gracker (Chris)
// This file is part of SmartPerfetto. See LICENSE for details.
Object.defineProperty(exports, "__esModule", { value: true });
exports.LineDelimitedJsonSink = exports.LineDelimitedJsonSource = exports.RPC_ERROR_CODES = void 0;
exports.dispatch = dispatch;
exports.runStdioLoop = runStdioLoop;
/**
 * StandaloneMcpServer — Plan 41 M1 stdio server.
 *
 * Implements the minimum subset of the Model Context Protocol that
 * Claude Code, Cursor, and Codex CLI clients use:
 *   - `initialize` handshake with capabilities advertisement
 *   - `tools/list` — returns the public-exposure tools from
 *     `McpToolRegistry`
 *   - `tools/call` — invokes the requested tool's handler with
 *     args, returns the structured content
 *
 * Wire protocol is JSON-RPC 2.0 over line-delimited JSON
 * (one request per stdin line, one response per stdout line).
 *
 * The dispatcher is factored out from the actual stdio wiring so
 * unit tests can drive request/response pairs without spawning a
 * subprocess. `bin/smartperfetto-mcp.ts` is the thin entry that
 * pipes process.stdin/stdout into `runStdioLoop`.
 *
 * Out of scope:
 * - Capability negotiation beyond `tools` (no resources / prompts).
 * - Auth handshake (deferred to Plan 41 M2 A2A).
 * - Streaming tool output (the agent SDK handlers we wrap are
 *   request/response only).
 *
 * @module standaloneMcpServer
 */
const mcpToolRegistry_1 = require("./mcpToolRegistry");
const PROTOCOL_VERSION = '2024-11-05';
const SERVER_NAME = 'smartperfetto';
const SERVER_VERSION = '1.0.0';
/** Standard JSON-RPC error codes plus MCP-specific tool errors. */
exports.RPC_ERROR_CODES = {
    PARSE_ERROR: -32700,
    INVALID_REQUEST: -32600,
    METHOD_NOT_FOUND: -32601,
    INVALID_PARAMS: -32602,
    INTERNAL_ERROR: -32603,
    TOOL_EXECUTION_FAILED: -32000,
};
/** Best-effort handler extraction across SDK versions. */
function getToolHandler(tool) {
    const t = tool;
    if (typeof t.handler === 'function')
        return t.handler.bind(t);
    if (typeof t.callback === 'function')
        return t.callback.bind(t);
    if (typeof t.fn === 'function')
        return t.fn.bind(t);
    return null;
}
/** Dispatch one JSON-RPC request against the registry. Pure
 * function — no I/O. The stdio loop wraps this and writes the
 * response back. */
async function dispatch(registry, req) {
    // Notifications (no id) — execute side effects but never reply.
    // Our protocol surface has none, but per JSON-RPC we silently drop.
    if (req.id === undefined)
        return null;
    const id = req.id;
    // Codex round E P2#5: parse-error sentinel surfaces with a real
    // PARSE_ERROR response instead of falling through to METHOD_NOT_FOUND.
    if (req.method === '__parse_error__') {
        return rpcError(null, exports.RPC_ERROR_CODES.PARSE_ERROR, 'Invalid JSON');
    }
    if (req.method === 'initialize') {
        return {
            jsonrpc: '2.0',
            id,
            result: {
                protocolVersion: PROTOCOL_VERSION,
                serverInfo: { name: SERVER_NAME, version: SERVER_VERSION },
                capabilities: { tools: {} },
            },
        };
    }
    if (req.method === 'tools/list') {
        // External hosts only see public tools. Internal session-protocol
        // tools (submit_plan, write_analysis_note, etc.) stay hidden so a
        // remote Claude Code instance cannot corrupt our session state.
        const publicDefs = (0, mcpToolRegistry_1.filterByExposure)(registry.list(), ['public']);
        return {
            jsonrpc: '2.0',
            id,
            result: {
                tools: publicDefs.map(d => sdkToolToMcpDescriptor(d)),
            },
        };
    }
    if (req.method === 'tools/call') {
        const params = (req.params ?? {});
        if (!params.name || typeof params.name !== 'string') {
            return rpcError(id, exports.RPC_ERROR_CODES.INVALID_PARAMS, '`name` is required');
        }
        const def = registry.list().find(d => d.name === params.name);
        if (!def) {
            return rpcError(id, exports.RPC_ERROR_CODES.METHOD_NOT_FOUND, `Unknown tool '${params.name}'`);
        }
        if (def.exposure !== 'public') {
            // Internal / deprecated tools stay invisible to external hosts
            // both in the listing and at call time.
            return rpcError(id, exports.RPC_ERROR_CODES.METHOD_NOT_FOUND, `Tool '${params.name}' is not exposed to external hosts`);
        }
        const handler = getToolHandler(def.tool);
        if (!handler) {
            return rpcError(id, exports.RPC_ERROR_CODES.INTERNAL_ERROR, `Tool '${params.name}' has no callable handler`);
        }
        try {
            const out = await handler(params.arguments ?? {});
            return { jsonrpc: '2.0', id, result: out };
        }
        catch (err) {
            return rpcError(id, exports.RPC_ERROR_CODES.TOOL_EXECUTION_FAILED, err instanceof Error ? err.message : String(err));
        }
    }
    return rpcError(id, exports.RPC_ERROR_CODES.METHOD_NOT_FOUND, `Unknown method '${req.method}'`);
}
function rpcError(id, code, message) {
    return { jsonrpc: '2.0', id, error: { code, message } };
}
function sdkToolToMcpDescriptor(def) {
    const t = def.tool;
    return {
        name: def.name,
        description: def.summary || t.description || '',
        // Codex round E P1#1: SDK's `inputSchema` is Zod raw shape, not
        // JSON Schema. Emit a safe permissive descriptor so external MCP
        // clients see valid JSON Schema; the handler still validates
        // against the real Zod shape on `tools/call`.
        inputSchema: { type: 'object', additionalProperties: true },
    };
}
/** Run the dispatch loop until the source is exhausted. Each
 * request is dispatched independently; failures in one tool call
 * never bring down the loop. */
async function runStdioLoop(registry, source, sink) {
    while (true) {
        const req = await source.next();
        if (req === null)
            return;
        try {
            const resp = await dispatch(registry, req);
            if (resp)
                sink.send(resp);
        }
        catch (err) {
            // Defensive: dispatch itself shouldn't throw, but if a tool
            // handler throws synchronously past the inner catch, log it
            // back as an internal error rather than crashing the server.
            const id = req.id ?? null;
            sink.send(rpcError(id, exports.RPC_ERROR_CODES.INTERNAL_ERROR, err instanceof Error ? err.message : String(err)));
        }
    }
}
/** stdin-backed request source: reads line-delimited JSON from a
 * Node Readable. Returns null when the stream ends. Malformed JSON
 * lines are surfaced as a special MALFORMED_PARSE request so the
 * dispatch layer can emit a proper PARSE_ERROR. */
class LineDelimitedJsonSource {
    constructor(stream) {
        this.buffer = '';
        this.queue = [];
        this.closed = false;
        this.wakeup = null;
        stream.setEncoding?.('utf-8');
        stream.on('data', (chunk) => {
            this.buffer += chunk;
            this.drainBuffer();
            this.wake();
        });
        stream.on('end', () => {
            // Flush any partial line.
            if (this.buffer.trim().length > 0) {
                this.tryParseAndQueue(this.buffer);
                this.buffer = '';
            }
            this.closed = true;
            this.wake();
        });
        stream.on('close', () => {
            this.closed = true;
            this.wake();
        });
    }
    drainBuffer() {
        let nl;
        while ((nl = this.buffer.indexOf('\n')) >= 0) {
            const line = this.buffer.slice(0, nl).trim();
            this.buffer = this.buffer.slice(nl + 1);
            if (line.length > 0)
                this.tryParseAndQueue(line);
        }
    }
    tryParseAndQueue(line) {
        try {
            const obj = JSON.parse(line);
            this.queue.push(obj);
        }
        catch {
            this.queue.push(PARSE_ERROR_SENTINEL);
        }
    }
    wake() {
        const w = this.wakeup;
        this.wakeup = null;
        w?.();
    }
    async next() {
        while (this.queue.length === 0 && !this.closed) {
            await new Promise(resolve => (this.wakeup = resolve));
        }
        const head = this.queue.shift();
        if (!head)
            return null;
        if (head === PARSE_ERROR_SENTINEL) {
            // Synthesize a parse-error request — the dispatcher emits the
            // proper PARSE_ERROR response. id=null since we have no real id.
            return { jsonrpc: '2.0', id: null, method: '__parse_error__' };
        }
        return head;
    }
}
exports.LineDelimitedJsonSource = LineDelimitedJsonSource;
const PARSE_ERROR_SENTINEL = Symbol('parse-error');
/** stdout-backed response sink. Writes one JSON-encoded response
 * per line. */
class LineDelimitedJsonSink {
    constructor(stream) {
        this.stream = stream;
    }
    send(response) {
        this.stream.write(JSON.stringify(response) + '\n');
    }
}
exports.LineDelimitedJsonSink = LineDelimitedJsonSink;
//# sourceMappingURL=standaloneMcpServer.js.map