added mcp server code
This commit is contained in:
@@ -192,6 +192,40 @@ def build(mkdocs_yml: Path) -> None:
|
|||||||
click.echo("MkDocs build completed")
|
click.echo("MkDocs build completed")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------
|
||||||
|
# serve-mcp
|
||||||
|
# ---------------------------------------------------------------------
|
||||||
|
|
||||||
|
@cli.command(name="serve-mcp")
|
||||||
|
def serve_mcp() -> None:
|
||||||
|
"""
|
||||||
|
Serve MCP documentation from the local mcp_docs directory.
|
||||||
|
"""
|
||||||
|
from docforge.servers import MCPServer
|
||||||
|
|
||||||
|
mcp_root = Path.cwd() / "mcp_docs"
|
||||||
|
|
||||||
|
if not mcp_root.exists():
|
||||||
|
raise click.ClickException("mcp_docs directory not found")
|
||||||
|
|
||||||
|
required = [
|
||||||
|
mcp_root / "index.json",
|
||||||
|
mcp_root / "nav.json",
|
||||||
|
mcp_root / "modules",
|
||||||
|
]
|
||||||
|
|
||||||
|
for path in required:
|
||||||
|
if not path.exists():
|
||||||
|
raise click.ClickException(f"Invalid MCP bundle, missing: {path.name}")
|
||||||
|
|
||||||
|
server = MCPServer(
|
||||||
|
mcp_root=mcp_root,
|
||||||
|
name="doc-forge-mcp",
|
||||||
|
)
|
||||||
|
|
||||||
|
server.run()
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------
|
# ---------------------------------------------------------------------
|
||||||
# serve
|
# serve
|
||||||
# ---------------------------------------------------------------------
|
# ---------------------------------------------------------------------
|
||||||
|
|||||||
5
docforge/servers/__init__.py
Normal file
5
docforge/servers/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
from .mcp_server import MCPServer
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"MCPServer",
|
||||||
|
]
|
||||||
73
docforge/servers/mcp_server.py
Normal file
73
docforge/servers/mcp_server.py
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Literal
|
||||||
|
|
||||||
|
from mcp.server.fastmcp import FastMCP
|
||||||
|
|
||||||
|
|
||||||
|
class MCPServer:
|
||||||
|
"""
|
||||||
|
MCP server for serving a pre-built MCP documentation bundle.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, mcp_root: Path, name: str) -> None:
|
||||||
|
self.mcp_root = mcp_root
|
||||||
|
self.app = FastMCP(name)
|
||||||
|
|
||||||
|
self._register_resources()
|
||||||
|
self._register_tools()
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Internal helpers
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _read_json(self, path: Path) -> Any:
|
||||||
|
if not path.exists():
|
||||||
|
return {
|
||||||
|
"error": "not_found",
|
||||||
|
"path": str(path),
|
||||||
|
}
|
||||||
|
return json.loads(path.read_text(encoding="utf-8"))
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# MCP resources
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _register_resources(self) -> None:
|
||||||
|
@self.app.resource("docs://index")
|
||||||
|
def index():
|
||||||
|
return self._read_json(self.mcp_root / "index.json")
|
||||||
|
|
||||||
|
@self.app.resource("docs://nav")
|
||||||
|
def nav():
|
||||||
|
return self._read_json(self.mcp_root / "nav.json")
|
||||||
|
|
||||||
|
@self.app.resource("docs://module/{module}")
|
||||||
|
def module(module: str):
|
||||||
|
return self._read_json(
|
||||||
|
self.mcp_root / "modules" / f"{module}.json"
|
||||||
|
)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# MCP tools (optional / diagnostic)
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _register_tools(self) -> None:
|
||||||
|
@self.app.tool()
|
||||||
|
def ping() -> str:
|
||||||
|
return "pong"
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Server lifecycle
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def run(self, transport: Literal["stdio", "sse", "streamable-http"] = "streamable-http") -> None:
|
||||||
|
"""
|
||||||
|
Start the MCP server.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
transport: MCP transport (default: streamable-http)
|
||||||
|
"""
|
||||||
|
self.app.run(transport=transport)
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
from pathlib import Path
|
import json
|
||||||
from typing import Callable
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from click.testing import CliRunner
|
from click.testing import CliRunner
|
||||||
|
|
||||||
|
|
||||||
@@ -65,3 +65,47 @@ def mock_mkdocs_serve(monkeypatch):
|
|||||||
fake_serve,
|
fake_serve,
|
||||||
)
|
)
|
||||||
return lambda: called["value"]
|
return lambda: called["value"]
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def fake_mcp_docs(tmp_path: Path) -> Path:
|
||||||
|
"""
|
||||||
|
Create a minimal valid MCP bundle in mcp_docs/.
|
||||||
|
"""
|
||||||
|
mcp_root = tmp_path / "mcp_docs"
|
||||||
|
modules_dir = mcp_root / "modules"
|
||||||
|
modules_dir.mkdir(parents=True)
|
||||||
|
|
||||||
|
(mcp_root / "index.json").write_text(
|
||||||
|
json.dumps({"project": "test", "type": "docforge-model"}),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
(mcp_root / "nav.json").write_text(
|
||||||
|
json.dumps([]),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
(modules_dir / "test.mod.json").write_text(
|
||||||
|
json.dumps({"module": "test.mod", "content": {}}),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
return mcp_root
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_mcp_server_run(monkeypatch):
|
||||||
|
"""
|
||||||
|
Mock MCPServer.run so no real server is started.
|
||||||
|
"""
|
||||||
|
called = {"value": False}
|
||||||
|
|
||||||
|
def fake_run(self, transport="streamable-http"):
|
||||||
|
called["value"] = True
|
||||||
|
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"docforge.servers.MCPServer.run",
|
||||||
|
fake_run,
|
||||||
|
)
|
||||||
|
|
||||||
|
return lambda: called["value"]
|
||||||
|
|||||||
18
tests/cli/test_mcp_serve.py
Normal file
18
tests/cli/test_mcp_serve.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
from docforge.cli.main import cli
|
||||||
|
|
||||||
|
|
||||||
|
def test_serve_mcp(
|
||||||
|
cli_runner,
|
||||||
|
fake_mcp_docs,
|
||||||
|
mock_mcp_server_run,
|
||||||
|
monkeypatch,
|
||||||
|
):
|
||||||
|
monkeypatch.chdir(fake_mcp_docs.parent)
|
||||||
|
|
||||||
|
result = cli_runner.invoke(
|
||||||
|
cli,
|
||||||
|
["serve-mcp"],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert mock_mcp_server_run()
|
||||||
Reference in New Issue
Block a user