#!/usr/bin/env python3
"""One of the Part 24 agent's tools, exposed over the Model Context Protocol.

Purpose: take the document-search tool the agent already has and publish it as an MCP
    server, so the same tool serves your own loop, an editor and a desktop application
    without being written three times. The guard rails stay where they were, in
    toolbox.py: this file is a protocol wrapper and nothing else, which is the shape
    a server should have. Read the docstrings below as prompts, because that is what
    they become: the SDK turns each one into the tool description the model reads.
Platform: all (pure Python). The command tool inherits toolbox.py's POSIX resource
    limits, so on Windows run this inside WSL2.
Minimum memory: 8 GB on the machine serving the model; this server needs almost none
Assumes: Python 3.9 or later, the MCP Python SDK installed (`uv add "mcp[cli]"` or
    `pip install "mcp[cli]"`, which needs Python 3.10 or later), and toolbox.py beside
    this file. The workspace comes from the AGENT_WORKSPACE environment variable and an
    optional Part 10 index from AGENT_INDEX, because `mcp run` and desktop clients start
    a server without command-line arguments.

Usage: export AGENT_WORKSPACE=./agent-workspace
       python3 mcp-server.py                       # stdio, the default transport
       python3 mcp-server.py --http --port 8000    # Streamable HTTP on 127.0.0.1
       uv run mcp dev mcp-server.py                # in the MCP Inspector
       uv run mcp run mcp-server.py --transport streamable-http
"""

from __future__ import annotations

import os
import sys
from pathlib import Path

from mcp.server import MCPServer
from mcp.server.mcpserver.exceptions import ToolError as McpToolError

from toolbox import Toolbox, ToolError

WORKSPACE = Path(os.environ.get("AGENT_WORKSPACE", "./agent-workspace")).expanduser()
INDEX = os.environ.get("AGENT_INDEX")

# Built once, at import, so that a bad workspace fails loudly at startup rather than on
# the first call. Diagnostics go to stderr: the specification allows any logging there
# and forbids anything but protocol messages on stdout.
try:
    BOX = Toolbox(workspace=WORKSPACE, index=Path(INDEX).expanduser() if INDEX else None)
except ToolError as exc:
    sys.exit("mcp-server: %s (set AGENT_WORKSPACE to a directory that exists)" % exc)

print("mcp-server: workspace %s, index %s" % (BOX.workspace, BOX.index or "none"), file=sys.stderr)

mcp = MCPServer("part24-documents")


@mcp.tool()
def search_documents(query: str, limit: int = 5) -> str:
    """Search the document collection for passages matching keywords.

    Returns the best matching passages with the file each came from. Use this when the
    answer might be written down in the collection rather than known in general. The
    query should be search terms rather than a question.
    """
    try:
        return BOX.search_documents(query, limit)
    except ToolError as exc:
        # A refusal the model can act on: the SDK reports this with is_error set and
        # the message in the content, which is what lets the model correct itself.
        raise McpToolError(str(exc)) from None


@mcp.tool()
def read_document(path: str) -> str:
    """Read one document from the collection and return its text.

    The path must be relative to the collection root and must not contain '..'. Paths
    that resolve outside the collection are refused, including through symbolic links.
    """
    try:
        return BOX.read_file(path)
    except ToolError as exc:
        raise McpToolError(str(exc)) from None


@mcp.resource("workspace://index")
def collection_index() -> str:
    """The list of documents in the collection, as a resource the application may read.

    This is a resource rather than a tool because the application chooses to read it,
    not the model. It is the difference the specification draws between something the
    model decides to invoke and something the host decides to include.
    """
    names = sorted(p.relative_to(BOX.workspace).as_posix()
                   for p in BOX.workspace.rglob("*") if p.is_file())
    return "\n".join(names) or "(the collection is empty)"


if __name__ == "__main__":
    # `mcp run` imports this module and starts the server itself, so this block is only
    # for running the file directly. With no argument the transport is stdio.
    if "--http" in sys.argv:
        port = 8000
        if "--port" in sys.argv:
            port = int(sys.argv[sys.argv.index("--port") + 1])
        mcp.run(transport="streamable-http", port=port)
    else:
        mcp.run()
