feat(lib): bundle MCP servers from .common/lib into factory/lib/
Copies source (no node_modules, dist, .tasks, .wiki, __pycache__) for: - projects-meta-mcp v2.25.0 (TypeScript/Node) - wiki-graph v0.3.1 (TypeScript/Node) - interns-mcp v0.3.3 (Python/FastMCP) .gitignore: exclude lib build artefacts (node_modules, dist, .venv, __pycache__, *.pyc) bootstrap.ps1: add MCP build step — npm install+build for TS servers, venv+pip for Python Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
114
lib/interns-mcp/interns_mcp/server.py
Normal file
114
lib/interns-mcp/interns_mcp/server.py
Normal file
@@ -0,0 +1,114 @@
|
||||
"""FastMCP stdio server — MCP entry point for interns."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from fastmcp import FastMCP
|
||||
|
||||
from .client import ClientPool
|
||||
from .registry import load_config, load_interns
|
||||
from .safety import check_paths
|
||||
|
||||
# Resolve config path relative to .common root; secrets live outside the git tree.
|
||||
_COMMON_ROOT = Path(__file__).resolve().parents[3] # lib/interns-mcp/interns_mcp/ → .common/
|
||||
CONFIG_PATH = _COMMON_ROOT / "config" / "interns" / "config.yaml"
|
||||
SECRETS_PATH = Path(
|
||||
os.environ.get("INTERNS_SECRETS_PATH")
|
||||
or (Path.home() / ".config" / "projects-secrets" / "interns.env")
|
||||
)
|
||||
|
||||
mcp = FastMCP("interns")
|
||||
|
||||
|
||||
def _get_api_key(endpoint_name: str, endpoints: dict[str, Any]) -> str:
|
||||
env_var = endpoints[endpoint_name]["api_key_env"]
|
||||
key = os.environ.get(env_var, "")
|
||||
if not key:
|
||||
raise RuntimeError(f"Missing API key: set {env_var} in {SECRETS_PATH}")
|
||||
return key
|
||||
|
||||
|
||||
def _build_extra_body(endpoint_cfg: dict[str, Any]) -> dict[str, Any]:
|
||||
return endpoint_cfg.get("request_defaults", {}).get("extra_body", {})
|
||||
|
||||
|
||||
def register_tool(intern: Any, client_pool: ClientPool, endpoints: dict[str, Any]) -> None:
|
||||
"""Register a single intern as an MCP tool."""
|
||||
|
||||
@mcp.tool(name=intern.id, description=intern.description)
|
||||
def intern_tool(paths: list[str], question: str, max_tokens: int | None = None) -> dict:
|
||||
if intern.safety:
|
||||
blocked = check_paths(paths)
|
||||
if blocked:
|
||||
return {"blocked": True, "path": blocked[0]["path"], "pattern": blocked[0]["pattern"], "reason": blocked[0]["reason"]}
|
||||
|
||||
# Wire endpoint client if intern needs one.
|
||||
if intern.endpoint and intern.client is None:
|
||||
ep_cfg = endpoints[intern.endpoint]
|
||||
api_key = _get_api_key(intern.endpoint, endpoints)
|
||||
intern.client = client_pool.get(intern.endpoint, ep_cfg, api_key)
|
||||
|
||||
kwargs: dict[str, Any] = {}
|
||||
if intern.endpoint and intern.endpoint in endpoints:
|
||||
kwargs["extra_body"] = _build_extra_body(endpoints[intern.endpoint])
|
||||
if max_tokens is not None:
|
||||
kwargs["max_tokens"] = max_tokens
|
||||
|
||||
result = intern.run(paths, question, **kwargs)
|
||||
if isinstance(result, dict) and result.get("blocked"):
|
||||
return result
|
||||
return {"text": result.text, "usage": result.usage}
|
||||
|
||||
|
||||
def register_grep_audit_tool(intern: Any) -> None:
|
||||
"""Register grep_audit with its distinct (paths, patterns, output, case_sensitive) shape.
|
||||
|
||||
Deterministic: no LLM client wiring path. Safety gate still applies — server is
|
||||
the single trust boundary regardless of whether the intern hits an endpoint.
|
||||
"""
|
||||
|
||||
@mcp.tool(name=intern.id, description=intern.description)
|
||||
def grep_audit_tool(
|
||||
paths: list[str],
|
||||
patterns: list[Any],
|
||||
output: str = "table",
|
||||
case_sensitive: bool = True,
|
||||
) -> dict:
|
||||
if intern.safety:
|
||||
blocked = check_paths(paths)
|
||||
if blocked:
|
||||
return {"blocked": True, "path": blocked[0]["path"], "pattern": blocked[0]["pattern"], "reason": blocked[0]["reason"]}
|
||||
|
||||
result = intern.run(
|
||||
paths=paths,
|
||||
patterns=patterns,
|
||||
output=output,
|
||||
case_sensitive=case_sensitive,
|
||||
)
|
||||
return {"text": result.text, "usage": result.usage}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
load_dotenv(SECRETS_PATH)
|
||||
config = load_config(CONFIG_PATH)
|
||||
endpoints = config.get("endpoints", {})
|
||||
|
||||
pool = ClientPool()
|
||||
from . import safety as _safety_mod
|
||||
|
||||
interns = load_interns(config, safety=_safety_mod, client=None)
|
||||
for intern in interns:
|
||||
if intern.id == "grep_audit":
|
||||
register_grep_audit_tool(intern)
|
||||
else:
|
||||
register_tool(intern, pool, endpoints)
|
||||
|
||||
mcp.run(transport="stdio")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user