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>
114 lines
3.9 KiB
Python
114 lines
3.9 KiB
Python
"""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() |