#!/usr/bin/env python3
"""A stdio MCP bridge in front of this platform's HTTP endpoint.

WHY IT EXISTS. The server speaks MCP revision 2026-07-28: no handshake, and
every request carries its protocol version and client capabilities in
`params._meta` with selected fields mirrored into headers. No shipping client
speaks that yet - measured, not assumed: the exact `initialize` a stock client
sends comes back `-32602 Invalid params: _meta must carry
io.modelcontextprotocol/protocolVersion`, because the client has no reason to
send a field its own revision does not define.

So a client cannot be pointed at the endpoint directly today. This process sits
between them: it speaks the handshake revision on stdin/stdout, which is what a
desktop client launches and talks to, and speaks 2026-07-28 over HTTPS to the
platform, adding the `_meta` block and the mirrored headers to every call.

It is a translator and nothing else. It holds no policy: the token, the plan,
the scope and the budget are all decided by the server, and a call this bridge
cannot make is a call that would have been refused anyway.

Usage:
    MWG_MCP_URL=https://example.com/mcp MWG_MCP_TOKEN=mwg_... mcp_bridge.py

Both are required and are read from the environment rather than from argv, so
the token does not end up in a process list.
"""

import json
import os
import sys
import urllib.error
import urllib.request

PROTOCOL_VERSION = "2026-07-28"
META_PROTOCOL_VERSION = "io.modelcontextprotocol/protocolVersion"
META_CLIENT_INFO = "io.modelcontextprotocol/clientInfo"
META_CLIENT_CAPABILITIES = "io.modelcontextprotocol/clientCapabilities"

# What the client told us about itself at initialize, passed on so the server
# logs the real client rather than this bridge.
_client_info = {"name": "mcp-bridge", "version": "1.0.0"}
_client_capabilities = {}

# The revision the CLIENT speaks. Echoed back at initialize rather than
# replaced with ours: telling a client it is talking 2026-07-28 when its own
# stack does not implement that revision is how a working connection turns into
# an unexplainable one three calls later.
_CLIENT_FALLBACK_REVISION = "2025-06-18"

TIMEOUT_S = 60


def _log(message):
    """stderr only. stdout is the protocol channel and anything written there
    that is not a JSON-RPC message desynchronises the client."""
    print("[mcp-bridge] %s" % message, file=sys.stderr, flush=True)


def _remote(method, params, request_id):
    """One call to the platform, in the shape revision 2026-07-28 requires."""
    url = os.environ.get("MWG_MCP_URL", "").strip()
    token = os.environ.get("MWG_MCP_TOKEN", "").strip()
    if not url or not token:
        return {"code": -32603,
                "message": "MWG_MCP_URL and MWG_MCP_TOKEN must both be set"}

    body = {
        "jsonrpc": "2.0",
        "id": request_id,
        "method": method,
        "params": dict(params or {}, _meta={
            META_PROTOCOL_VERSION: PROTOCOL_VERSION,
            META_CLIENT_INFO: _client_info,
            META_CLIENT_CAPABILITIES: _client_capabilities,
        }),
    }
    headers = {
        "Authorization": "Bearer " + token,
        "Content-Type": "application/json",
        # The mirrored headers. They have to AGREE with the body: the server
        # rejects a disagreement outright, because an intermediary may route on
        # the header while the server executes the body, and if the two can
        # differ the policy and the action apply to different requests.
        "MCP-Protocol-Version": PROTOCOL_VERSION,
        "Mcp-Method": method,
    }
    if method == "tools/call":
        headers["Mcp-Name"] = (params or {}).get("name", "")

    request = urllib.request.Request(
        url, data=json.dumps(body).encode("utf-8"), headers=headers, method="POST")
    try:
        with urllib.request.urlopen(request, timeout=TIMEOUT_S) as response:
            payload = json.loads(response.read().decode("utf-8"))
    except urllib.error.HTTPError as exc:
        # The server answers its own refusals as JSON-RPC inside a 200; an HTTP
        # error here is the transport failing, and saying which is the whole
        # point of reporting the status.
        detail = exc.read().decode("utf-8", "replace")[:300]
        return {"code": -32603, "message": "HTTP %s from the server: %s"
                                           % (exc.code, detail)}
    except Exception as exc:  # network, DNS, TLS, timeout
        return {"code": -32603, "message": "%s: %s" % (type(exc).__name__, exc)}

    if "error" in payload:
        return payload["error"]
    return {"result": payload.get("result", {})}


def _handle(message):
    """One client message in, one response out (or None for a notification)."""
    global _client_info, _client_capabilities

    method = message.get("method")
    request_id = message.get("id")
    params = message.get("params") or {}

    # Notifications carry no id and take no answer.
    if request_id is None:
        return None

    if method == "initialize":
        # Answered HERE. The platform has no handshake to forward this to, and
        # inventing one would mean claiming a revision it does not implement.
        _client_info = params.get("clientInfo") or _client_info
        _client_capabilities = params.get("capabilities") or {}
        return {
            "jsonrpc": "2.0", "id": request_id,
            "result": {
                "protocolVersion": params.get("protocolVersion",
                                              _CLIENT_FALLBACK_REVISION),
                "capabilities": {"tools": {}},
                "serverInfo": {"name": "malwagon-bridge", "version": "1.0.0"},
            },
        }

    if method == "ping":
        return {"jsonrpc": "2.0", "id": request_id, "result": {}}

    if method in ("tools/list", "tools/call"):
        outcome = _remote(method, params, request_id)
        if "result" in outcome:
            return {"jsonrpc": "2.0", "id": request_id, "result": outcome["result"]}
        return {"jsonrpc": "2.0", "id": request_id, "error": outcome}

    # resources/* and prompts/* deliberately included: this server exposes
    # neither, and answering "method not found" is the correct, quiet reply.
    return {"jsonrpc": "2.0", "id": request_id,
            "error": {"code": -32601, "message": "Method not found: %s" % method}}


def main():
    _log("started; forwarding to %s" % (os.environ.get("MWG_MCP_URL") or "<unset>"))
    for line in sys.stdin:
        line = line.strip()
        if not line:
            continue
        try:
            message = json.loads(line)
        except ValueError:
            print(json.dumps({"jsonrpc": "2.0", "id": None,
                              "error": {"code": -32700, "message": "Parse error"}}),
                  flush=True)
            continue
        response = _handle(message)
        if response is not None:
            print(json.dumps(response), flush=True)


if __name__ == "__main__":
    main()
