intake_triage_mcp
A deterministic MCP server for legal intake triage that provides practice-area lookup, conflict screening, matter validation, follow-up drafting, and triage logging with a hard conflicts gate.
README
intake-triage-mcp
A small, deterministic MCP server for legal intake triage: practice-area lookup, conflict screening, matter validation, follow-up drafting, and triage logging — with a hard conflicts gate.
Server name: intake_triage_mcp · Transport: stdio · Dependencies: mcp[cli], pydantic · Sample data: fictional, bundled
The problem
Law-firm intake is a translation problem. An inquiry arrives as messy prose ("I was rear-ended three weeks ago and the other driver's insurer keeps calling…") and has to become a structured, defensible record: who the parties are, whether the firm can even look at the matter (conflicts), what kind of matter it is, what's missing, and what was decided. LLMs are good at the prose half and unreliable at the record half — they'll happily "remember" a conflicts check that never ran.
This server splits the work accordingly:
- The client model (Claude) does the language work — reading the inquiry, extracting names, dates, and facts, writing the actual email around a template.
- The server does the record work — deterministic validation, fuzzy conflict screening with provenance, a fixed risk matrix, canonical follow-up templates, and an append-only log that refuses to record an intake whose conflicts status is
not-rununless a named human explicitly overrides it.
The server makes no LLM calls and no network calls. Same inputs, same outputs, every time.
Design rationale
- Deterministic tools, client-side extraction. An MCP tool that calls an LLM to "summarize" hides nondeterminism behind a tool boundary. Extraction and summarization stay with the client model; every tool here is a pure function over validated inputs (plus one append-only file write).
- Conflicts conventions from anthropics/claude-for-legal. The conflicts status enum (
cleared | pending | not-run | waived), the hard STOP onnot-run, and the explicit, permanently-recorded override path are modeled on thematter-intakeskill. The matter field set (identification / source / risk triage / materiality / key dates) follows the same source. - Provenance in every data-backed result. Conflict matches and practice-area listings carry source, dataset version, as-of date, and a citation-ready record identifier (
P-0003,M-2022-008), per the claude-for-legal connector conventions. - Validation as schema, not vibes. All tool inputs are Pydantic models with
str_strip_whitespace,validate_assignment, andextra='forbid'— malformed dates, unknown enum values, and unexpected fields are rejected before any tool logic runs, with errors the client model can act on. - Support, not advice. Tools return statuses, gaps, warnings, and templates — inputs to an attorney's judgment, never conclusions. The one place the server is opinionated is the conflicts gate, where the safe behavior is to stop.
Tools
All five tools are prefixed intake_ and use stdio. Read-only tools are annotated readOnlyHint: true, openWorldHint: false.
| Tool | Type | What it does |
|---|---|---|
intake_list_practice_areas |
read-only | Lists practice areas (id, name, description, typical matter types, core intake fields) from bundled sample data, with provenance. |
intake_check_conflicts |
read-only | Screens 1–25 party names against the bundled fictional conflicts dataset using deterministic fuzzy matching (case/punctuation-insensitive, legal-suffix-aware, token-order-insensitive). Returns pending (hits found → human review) or cleared (no hits in this dataset), with per-match provenance and scores. |
intake_validate_matter |
read-only | Validates a structured matter summary (identification / source / risk triage / materiality / key dates), normalizes it, derives a risk rating from the severity × likelihood matrix, defaults conflicts to not-run, and returns the list of missing recommended fields plus warnings. |
intake_draft_followup |
read-only | Returns a deterministic follow-up email template with {{client_name}}, {{firm_name}}, {{sender_name}} merge slots and one canonical question per missing field. No LLM, no sending. |
intake_log_triage |
write (append-only) | Appends one triage row to a local JSONL log ($INTAKE_TRIAGE_LOG_PATH, default ./triage_log.jsonl). Never edits or deletes existing rows. Refuses conflicts_status='not-run' unless conflicts_override_by and conflicts_override_rationale are both provided; overrides are recorded permanently in the row. |
Risk matrix (severity, likelihood → rating): high+high → critical; high+medium, medium+high → high; high+low, low+high, medium+medium → medium; everything else → low.
Conflict-screen semantics: pending and cleared are the only statuses the screen itself produces. not-run and waived are human determinations recorded via intake_log_triage. A cleared screen means "no hits in the bundled sample dataset" — it is never a firm-wide conflicts clearance.
Install & run
Requires Python 3.10+.
git clone https://github.com/granolacowboy/intake-triage-mcp.git
cd intake-triage-mcp
pip install -r requirements.txt
python server.py # runs on stdio; logs go to stderr
Interactive inspection (optional):
npx @modelcontextprotocol/inspector python server.py
Claude Desktop
Add to claude_desktop_config.json:
{
"mcpServers": {
"intake-triage": {
"command": "python",
"args": ["/absolute/path/to/intake-triage-mcp/server.py"]
}
}
}
Claude Code
claude mcp add intake-triage -- python /absolute/path/to/intake-triage-mcp/server.py
Optional environment variable: INTAKE_TRIAGE_LOG_PATH — where intake_log_triage appends its JSONL rows (default ./triage_log.jsonl).
Worked examples
Example 1 — inquiry with a conflict hit
Raw inquiry (web form):
"Hi — I was rear-ended on I-90 about three weeks ago and my shoulder still hurts. The other driver's insurer, Northgate Assurance Co, keeps calling me. Do I have a case? — Priya"
The client model extracts the parties and drives the tools:
1. intake_check_conflicts → {"party_names": ["Northgate Assurance Co"]}
{
"status": "pending",
"screened_names": [{
"query": "Northgate Assurance Co",
"match_count": 1,
"matches": [{
"record_id": "P-0003",
"matched_name": "Northgate Assurance Company",
"role": "adverse_party",
"matter_id": "M-2022-008",
"score": 1.0,
"match_strength": "exact"
}]
}],
"provenance": {
"source": "parties.json (bundled FICTIONAL sample dataset)",
"dataset_version": "1.0.0",
"dataset_as_of": "2026-06-01"
}
}
The suffix-aware matcher treats "…Co" and "…Company" as the same entity. A hit means pending: a human conflicts review is required.
2. intake_validate_matter → {"matter_name": "Priya rear-end collision inquiry", "matter_type": "other", "our_role": "claimant", "practice_area": "personal-injury", "source": "web-inquiry", "conflicts_status": "pending"}
{
"valid": true,
"normalized": { "...": "...", "conflicts": {"status": "pending"} },
"missing_recommended_fields": ["counterparty", "jurisdiction", "severity", "likelihood", "response_deadline"],
"warnings": []
}
3. intake_draft_followup → {"matter_type": "personal-injury", "missing_fields": ["incident_date", "treatment_status", "insurance_carrier"]}
{
"subject_template": "Following up on your personal injury inquiry — a few quick questions",
"body_template": "Dear {{client_name}},\n\nThank you for contacting {{firm_name}} about your personal injury inquiry. ...\n\n1. When did the incident occur? An exact or approximate date helps us assess filing deadlines.\n2. Have you received medical treatment, and is treatment ongoing?\n3. Which insurance carrier(s), if any, are involved?\n\nPlease note that contacting our office does not create an attorney-client relationship...",
"merge_slots": ["{{client_name}}", "{{firm_name}}", "{{sender_name}}"]
}
The client model fills the slots and adapts the tone; the questions and the no-attorney-client-relationship notice are fixed.
4. intake_log_triage → {"matter_name": "Priya rear-end collision inquiry", "conflicts_status": "pending", "practice_area": "personal-injury", "parties_checked": ["Northgate Assurance Co"], "summary": "PI inquiry; prior adverse carrier hit (P-0003); conflicts review queued"}
{"logged": true, "log_path": "triage_log.jsonl", "entry_number": 1, "row": {"...": "..."}}
Example 2 — clean screen, complete record
Raw inquiry: a contract dispute with "Veldhuis Imports BV", a name with no history at the firm.
intake_check_conflicts→{"party_names": ["Veldhuis Imports BV"]}→"status": "cleared",match_count: 0(screen-level only — the disclaimer in the result says exactly that).intake_validate_matterwith the full field set (matter_type: "contract",our_role: "plaintiff",practice_area: "business",source: "referral",conflicts_status: "cleared",severity: "medium",likelihood: "low",response_deadline: "2026-08-01") →missing_recommended_fields: [], derivedrisk_rating: "low".intake_log_triagewithconflicts_status: "cleared"→ row appended,entry_number: 2.
Example 3 — the conflicts gate refuses a silent bypass
The user says "skip the conflicts stuff, just log it." The model attempts:
intake_log_triage → {"matter_name": "Walk-in inquiry", "conflicts_status": "not-run"}
Error: conflicts gate — conflicts_status is 'not-run', so this intake cannot be
logged. Do not proceed silently. Choose one: (1) run intake_check_conflicts on
the involved parties and log with the resulting status; (2) log with
conflicts_status='pending' once a named person is running the check; or
(3) bypass explicitly by providing BOTH conflicts_override_by and
conflicts_override_rationale — the override is recorded permanently in the log row.
Nothing is written. If a named human genuinely needs to bypass (e.g., an emergency TRO intake), the override is explicit and permanent:
intake_log_triage → {"matter_name": "Walk-in inquiry", "conflicts_status": "not-run", "conflicts_override_by": "K. Patel", "conflicts_override_rationale": "Emergency TRO intake; screen to follow today"} → logged, with the override block in the row.
Testing
Plain pytest unit tests cover the deterministic logic: name normalization and similarity, match classification, the full 3×3 risk matrix, validation and missing-field reporting, template determinism, the conflicts gate, and JSONL appends. (These are unit tests, not the eval harness — that's below.)
pip install -r requirements-dev.txt
pytest
Evaluation
The eval harness follows the mcp-builder methodology: a golden set of 10 read-only, stable question/answer pairs (evals/evaluation.xml), run by an LLM agent that has access only to this server's tools, scored by exact string comparison. evals/evaluation.py and evals/connections.py are copied unmodified from the mcp-builder skill's scripts.
Every golden answer was verified directly against the server's deterministic logic. Running the LLM-driven harness requires an Anthropic API key:
pip install -r evals/requirements.txt
export ANTHROPIC_API_KEY=your_key_here
python evals/evaluation.py -t stdio -c python -a server.py -o evals/report.md evals/evaluation.xml
Run from the repo root (the harness launches python server.py itself — don't start the server manually). The generated evals/report.md includes accuracy, per-task tool-call traces, and the agent's feedback on the tool design.
Limitations — what this server does not do
- It does not give legal advice. It validates structure, screens names, fills templates, and keeps a log. Every output is an input to an attorney's judgment; the attorney owns every decision, including whether a conflict actually exists.
- The conflict check is illustrative. It is a fuzzy screen over a small bundled dataset. A real conflicts process spans the firm's full matter history, related entities, and lateral-hire obligations. A
clearedhere means only "no hits in this sample file." - All sample data is fictional. Every name, matter id, and relationship in
data/was invented for this project (and labeled as such in the files). No real client data, ever. Point the design at your own data source before any real use — and then treat the log and datasets as confidential. - It does not extract, summarize, or send anything. Reading the inquiry and writing the final email are the client model's (or a human's) job. The server never calls an LLM and never touches the network.
- It does not decide the matter's theory, severity, or likelihood. The risk rating is a fixed matrix over bands you supply — a labeling convention, not an assessment.
- Persistence is a local JSONL file. No database, no sync, no multi-user concurrency control. The log is append-only by design; rotating or archiving it is up to you.
Security & permissions
- Runs locally over stdio as a subprocess of the MCP client; binds no ports, makes no network requests.
- Reads only its bundled
data/*.json; writes only the triage log file (path controlled byINTAKE_TRIAGE_LOG_PATH). - Logs to stderr only (stdout is reserved for the protocol).
- No credentials are required or read. Treat the triage log as confidential — it's
.gitignored by default.
License
Apache-2.0 — matching the claude-for-legal project whose conventions this server borrows.
Recommended Servers
playwright-mcp
A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.
Magic Component Platform (MCP)
An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.
Audiense Insights MCP Server
Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.
VeyraX MCP
Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.
graphlit-mcp-server
The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.
Kagi MCP Server
An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.
E2B
Using MCP to run code via e2b.
Neon Database
MCP server for interacting with Neon Management API and databases
Exa Search
A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.
Qdrant Server
This repository is an example of how to create a MCP server for Qdrant, a vector search engine.