added mcp

This commit is contained in:
2026-01-19 22:44:54 +05:30
parent 32c2c07aa2
commit 9d1635c043
2 changed files with 177 additions and 1 deletions

176
generate_mcp.py Normal file
View File

@@ -0,0 +1,176 @@
"""
Generate a fully-contained MCP bundle by reading Python docstrings directly.
Uses Griffe (the same library mkdocstrings-python uses internally),
but without MkDocs, Markdown, or HTML.
Output:
mcp/
├── index.json
├── nav.json
└── modules/
└── package.module.json
"""
from __future__ import annotations
import json
import argparse
from pathlib import Path
from typing import Iterable
from griffe import GriffeLoader, ModulesCollection, LinesCollection, AliasResolutionError
PROJECT_ROOT = Path(__file__).resolve().parent
DEFAULT_PACKAGE_ROOT = "mail_intake"
DEFAULT_MCP_DIR = PROJECT_ROOT / "mcp"
# -------------------------
# Utilities
# -------------------------
def iter_modules(package_root: Path, package_name: str) -> Iterable[str]:
for py in package_root.rglob("*.py"):
rel = py.relative_to(package_root.parent)
if py.name == "__init__.py":
yield ".".join(rel.parent.parts)
else:
yield ".".join(rel.with_suffix("").parts)
def serialize_object(obj) -> dict:
"""Convert a Griffe object into MCP-friendly JSON (alias-safe)."""
try:
docstring = obj.docstring.value if obj.docstring else None
except AliasResolutionError:
docstring = None
data = {
"name": obj.name,
"kind": obj.kind.value,
"path": obj.path,
"docstring": docstring,
}
# Signature may also trigger alias resolution
try:
if hasattr(obj, "signature") and obj.signature:
data["signature"] = str(obj.signature)
except AliasResolutionError:
pass
# Recurse into members, but never allow alias failure to bubble up
members = {}
if hasattr(obj, "members"):
for name, member in obj.members.items():
if name.startswith("_"):
continue
try:
members[name] = serialize_object(member)
except AliasResolutionError:
continue
if members:
data["members"] = members
return data
# -------------------------
# MCP generation
# -------------------------
def build_mcp(package_root: Path, package_name: str, mcp_root: Path) -> None:
modules_dir = mcp_root / "modules"
modules_dir.mkdir(parents=True, exist_ok=True)
loader = GriffeLoader(
search_paths=[str(package_root.parent)],
modules_collection=ModulesCollection(),
lines_collection=LinesCollection(),
)
nav = []
count = 0
for module in sorted(iter_modules(package_root, package_name)):
loader.load(module)
mod = loader.modules_collection[module]
payload = {
"module": module,
"content": serialize_object(mod),
}
out = modules_dir / f"{module}.json"
out.parent.mkdir(parents=True, exist_ok=True)
out.write_text(json.dumps(payload, indent=2), encoding="utf-8")
nav.append({
"module": module,
"resource": f"docs://module/{module}",
})
count += 1
(mcp_root / "nav.json").write_text(
json.dumps(nav, indent=2),
encoding="utf-8",
)
(mcp_root / "index.json").write_text(
json.dumps(
{
"project": package_name,
"type": "docstrings-direct",
"modules_count": count,
"source": "griffe",
},
indent=2,
),
encoding="utf-8",
)
print(f"MCP generated at: {mcp_root}")
# -------------------------
# CLI
# -------------------------
def main() -> None:
parser = argparse.ArgumentParser(
description="Generate MCP directly from Python docstrings (Griffe-based)",
)
parser.add_argument(
"--package-root",
default=DEFAULT_PACKAGE_ROOT,
help="Root Python package name",
)
parser.add_argument(
"--mcp-dir",
type=Path,
default=DEFAULT_MCP_DIR,
help="Output MCP directory",
)
args = parser.parse_args()
package_root = PROJECT_ROOT / args.package_root
if not package_root.exists():
raise FileNotFoundError(package_root)
build_mcp(
package_root=package_root,
package_name=args.package_root,
mcp_root=args.mcp_dir,
)
if __name__ == "__main__":
main()

View File

@@ -19,7 +19,7 @@ def index():
def nav():
return read_json(MCP_ROOT / "nav.json")
@mcp.resource("docs://{module}")
@mcp.resource("docs://module/{module}")
def module(module: str):
return read_json(MCP_ROOT / "modules" / f"{module}.json")