#!/usr/bin/env python3 """Ten31 CRM MCP server (Workstream C). Exposes CRM reads, retrieval modes, and logged writes to the Claude Agent SDK over MCP (stdio). All logic lives in crm_tools.py (tested independently); this file is the thin transport wrapper. Run: pip install mcp # one-time (MCP Python SDK) CRM_DB_PATH=/data/crm.db python3 backend/mcp/server.py Register with the Agent SDK / Claude Code as an stdio MCP server pointing at this script. NO outbound/contact tools are exposed — that capability is gated to Phase 3 behind the compliance review (CLAUDE.md guardrails #4, #6). """ import os import sys sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) import crm_tools as t # noqa: E402 from mcp.server.fastmcp import FastMCP # noqa: E402 mcp = FastMCP("ten31-crm") # ── reads ── @mcp.tool() def get_entity(lp_id: str) -> dict: """Fetch a canonical LP/organization/person entity by id, with its linked source records and interaction count.""" return t.get_entity(lp_id) @mcp.tool() def search_records(query: str = "", entity_kind: str = "", limit: int = 20) -> dict: """Structured search over canonical entities by name substring and kind ('lp' | 'organization' | 'person').""" return t.search_records(query=query or None, entity_kind=entity_kind or None, limit=limit) @mcp.tool() def get_interaction_history(lp_id: str, limit: int = 20) -> dict: """Merged, dated interaction history (communications + fundraising grid notes) for a canonical entity.""" return t.get_interaction_history(lp_id, limit=limit) # ── retrieval modes ── @mcp.tool() def hybrid_search(query: str, top_k: int = 8, lp_id: str = "", doc_type: str = "", date_from: int = 0, date_to: int = 0) -> dict: """Dense + BM25 + rerank retrieval (default; best for entity-heavy queries). Optional filters: lp_id, doc_type, date_from/date_to (epoch seconds).""" return t.hybrid_search(query, top_k=top_k, lp_id=lp_id or None, doc_type=doc_type or None, date_from=date_from or None, date_to=date_to or None) @mcp.tool() def semantic_search(query: str, top_k: int = 8, lp_id: str = "", doc_type: str = "") -> dict: """Dense-only retrieval (high recall).""" return t.semantic_search(query, top_k=top_k, lp_id=lp_id or None, doc_type=doc_type or None) @mcp.tool() def keyword_search(query: str, top_k: int = 8, lp_id: str = "", doc_type: str = "") -> dict: """High-precision lexical retrieval (sparse leg + rerank).""" return t.keyword_search(query, top_k=top_k, lp_id=lp_id or None, doc_type=doc_type or None) # ── writes (logged) ── @mcp.tool() def log_interaction(action: str, actor_type: str = "agent", actor_id: str = "", target_id: str = "", payload: dict = None, source: str = "mcp") -> dict: """Append an entry to the append-only interaction log (guardrail #5).""" return t.log_interaction(action, actor_type=actor_type, actor_id=actor_id or None, target_id=target_id or None, payload=payload, source=source) @mcp.tool() def set_entity_enrichment(lp_id: str, fields: dict, actor_id: str = "analyst") -> dict: """One-way enrichment write into a canonical entity (thesis_fit, segment, warmth_score, accreditation_status, etc.). Logged automatically.""" return t.set_entity_enrichment(lp_id, fields, actor_id=actor_id) if __name__ == "__main__": mcp.run()