Skip to main content
Glama
Aditya-Khadye

mcp-clinical-doc-agent

mcp-clinical-doc-agent

CI Python 3.11 License: MIT

MCP-Orchestrated Clinical Document Agent — May 2026 Python · FastAPI · LangGraph · Anthropic MCP · Claude Code · AWS · Pandas

  • Built a FastAPI backend exposing custom MCP server tools to a Claude Code agent for end-to-end document analysis on synthetic FDA-style clinical trial protocols.

  • Orchestrated a multi-step agentic workflow with LangGraph: ingestion, entity extraction, adverse-event clustering, and structured summary generation with evaluation guardrails.

  • Containerized inference on AWS with Pandas-based ETL for reproducible, analysis-ready outputs; published as an open-source reference implementation on GitHub.


What this project demonstrates

  • Model Context Protocol (MCP) server built on the official Anthropic mcp Python SDK — 4 tools, stdio transport, registerable as a native tool provider in Claude Code and Claude Desktop.

  • LangGraph state machine that orchestrates the same tools as a deterministic 4-node pipeline with typed state, error capture, and a final eval gate.

  • Pydantic v2 schemas for every cross-boundary object (DocumentRef, ClinicalEntity, AdverseEvent, AdverseEventCluster, ProtocolSummary, WorkflowReport, EvalResult).

  • FastAPI HTTP surface mirroring the MCP tools so the same business logic runs over HTTP for non-MCP clients.

  • Pandas-based ETL that flattens the workflow report into five tidy, join-ready CSVs (documents, entities, adverse_events, cluster_summary, summaries).

  • Container-ready for AWS — multi-stage Dockerfile, healthcheck, docker-compose for local parity.

  • Offline-first — works with zero external API calls. Set ANTHROPIC_API_KEY to upgrade the summarizer to Claude Haiku 4.5.


Related MCP server: ClinicalTrials.gov Intelligence MCP

Architecture

┌──────────────────────────────────────────────────────────────────────┐
│  data/*.md   ── 10 synthetic FDA-style clinical trial protocols      │
└──────────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌──────────────────────────────────────────────────────────────────────┐
│  src/mcp_clinical_doc_agent/tools.py                                 │
│  list_documents · extract_entities · cluster_adverse_events ·        │
│  summarize_protocol  (shared business logic)                         │
└──────────────────────────────────────────────────────────────────────┘
       │                       │                       │
       ▼                       ▼                       ▼
┌─────────────┐         ┌─────────────┐         ┌───────────────────┐
│ MCP server  │         │  FastAPI    │         │  LangGraph        │
│ (stdio)     │         │  HTTP       │         │  workflow         │
│ Claude Code │         │  /docs, /…  │         │  → JSON report    │
│ Claude Desk │         │  Docker     │         │  → eval gate      │
└─────────────┘         └─────────────┘         └───────────────────┘

Quick start

Requires Python 3.11 and uv.

# 1. Clone and install
git clone https://github.com/Aditya-Khadye/mcp-clinical-doc-agent
cd mcp-clinical-doc-agent
uv sync --extra dev

# 2. Run the LangGraph workflow end-to-end (writes reports/run.json)
uv run mcp-clinical-doc-workflow

# 3. Start the MCP server (stdio) for local testing
uv run mcp-clinical-doc-server

# 4. Or start the FastAPI HTTP surface
uv run mcp-clinical-doc-agent   # http://localhost:8000/docs

# 5. Tests
uv run pytest -v

How to use this with Claude Code

This repo ships a project-scoped .mcp.json at the root:

{
  "mcpServers": {
    "clinical-doc-agent": {
      "command": "uv",
      "args": ["--directory", ".", "run", "mcp-clinical-doc-server"]
    }
  }
}

To register:

  1. Open this directory in Claude Code: cd mcp-clinical-doc-agent && claude.

  2. Run /mcp inside Claude Code — you should see clinical-doc-agent as a connected server with 4 tools.

  3. Try prompts like:

    • "List the clinical trial protocols you can see."

    • "Cluster the adverse events across all protocols and tell me which body system has the most reported events."

    • "Summarize the NSCLC protocol and flag any immune-related adverse events."

Claude Code will call the MCP tools directly — you'll see tool-use blocks render inline.

Claude Desktop

Edit ~/Library/Application Support/Claude/claude_desktop_config.json and merge the contents of claude_desktop_config.example.json, replacing /ABSOLUTE/PATH/TO/... with your local checkout path. Restart Claude Desktop and the server will appear in the tool tray.


The four MCP tools

Tool

Input

Output

list_documents

[{id, title, path, indication, phase}, …] for every protocol in data/.

extract_entities

document_id? (omit to scan all)

List of ClinicalEntity — drugs, conditions, interventions, endpoints, populations, phase.

cluster_adverse_events

document_ids? (omit to scan all)

List of AdverseEventCluster bucketed by body system (gastrointestinal, cardiovascular, neurological, dermatological, hematological, hepatic, respiratory, infections, metabolic, other).

summarize_protocol

document_id

ProtocolSummary with phase, indication, intervention, primary endpoint, planned N, AE count, and a 3-4 sentence narrative.

The summarizer uses Claude Haiku 4.5 when ANTHROPIC_API_KEY is set; otherwise it falls back to a deterministic template so the demo runs offline.


The LangGraph workflow

A 4-node StateGraph over AgentState:

START → ingest → extract → cluster → summarize → END
                                                  ↓
                                          evaluate(report)

Each node calls one MCP tool and updates the shared state. evaluate() runs after the graph and applies six pass/fail checks on the assembled report:

  • documents >= 5

  • each_doc_has_>=3_entities

  • adverse_events >= 25

  • distinct_clusters >= 4

  • summary_text >= 80 chars

  • summary_mentions_AE

Output is a Pydantic-validated WorkflowReport written to reports/run.json. The CLI exits non-zero on eval failure.

uv run mcp-clinical-doc-workflow --output reports/run.json --etl-dir reports/etl

Pandas ETL — analysis-ready outputs

After the eval gate passes, the report is flattened into five tidy CSVs under reports/etl/:

File

Granularity

Joins on

documents.csv

one row per protocol

id

entities.csv

one row per extracted entity

document_iddocuments.id

adverse_events.csv

one row per AE mention (with cluster_label)

document_iddocuments.id

cluster_summary.csv

one row per body-system cluster, with event_count and top 3 events

cluster_label

summaries.csv

one row per protocol summary

document_iddocuments.id

Example cluster_summary.csv:

cluster_label,event_count,distinct_terms,top_events
gastrointestinal,19,6,nausea(7); diarrhea(5); constipation(3)
neurological,15,4,headache(7); fatigue(4); dizziness(3)
dermatological,11,4,injection site reaction(4); rash(3); pruritus(3)
hepatic,11,4,elevated ast(4); elevated alt(4); transaminitis(2)

Drop these directly into a notebook with pd.read_csv(...), or load into Athena / Snowflake / DuckDB. A small demo script is included:

uv run python scripts/analyze.py

…which prints (1) per-protocol entity counts by category, (2) top adverse events overall, and (3) AE burden per protocol joined against documents.csv.

Example tail of a run:

[workflow] eval: PASS
  ✓ documents>=5
  ✓ each_doc_has_>=3_entities
  ✓ adverse_events>=25
  ✓ distinct_clusters>=4
  ✓ summary_text>=`80`_chars
  ✓ summary_mentions_AE
  · Processed 10 documents.
  · Total adverse events across clusters: 79.
  · Distinct AE body-system clusters: 9.

Docker / AWS deployment

Build and run locally with docker-compose:

docker-compose up --build
curl http://localhost:8000/health
curl http://localhost:8000/documents | jq .
curl -X POST http://localhost:8000/adverse-events/clusters \
     -H 'content-type: application/json' -d '{}' | jq '.[].cluster_label'

The Dockerfile is a multi-stage build (python:3.11-slim-bookworm + uv) with a built-in healthcheck. To push to ECR:

aws ecr create-repository --repository-name mcp-clinical-doc-agent
aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin <acct>.dkr.ecr.us-east-1.amazonaws.com
docker build -t mcp-clinical-doc-agent .
docker tag mcp-clinical-doc-agent:latest <acct>.dkr.ecr.us-east-1.amazonaws.com/mcp-clinical-doc-agent:latest
docker push <acct>.dkr.ecr.us-east-1.amazonaws.com/mcp-clinical-doc-agent:latest

From there, deploy to ECS Fargate, App Runner, or EKS. The container exposes port 8000, serves a /health endpoint, and reads ANTHROPIC_API_KEY from env.


Project layout

mcp-clinical-doc-agent/
├── .mcp.json                          # Claude Code project config
├── claude_desktop_config.example.json # Claude Desktop snippet
├── Dockerfile                         # Multi-stage build for AWS
├── docker-compose.yml
├── pyproject.toml                     # uv-managed
├── data/
│   └── protocol_*.md                  # 10 synthetic FDA-style protocols
├── src/mcp_clinical_doc_agent/
│   ├── tools.py                       # 4 tool implementations
│   ├── schema.py                      # Pydantic v2 models
│   ├── server.py                      # MCP server (stdio)
│   ├── api.py                         # FastAPI HTTP surface
│   ├── etl.py                         # Pandas ETL: report -> 5 CSVs
│   └── graph/
│       ├── workflow.py                # LangGraph state machine
│       ├── nodes.py                   # ingest / extract / cluster / summarize
│       └── eval.py                    # Pass/fail report gate
├── tests/                             # pytest — tools + workflow + etl
├── scripts/
│   ├── run_workflow.py
│   └── analyze.py                     # Pandas demo over the ETL CSVs
├── .github/workflows/ci.yml           # tests + lint + smoke-test on push/PR
└── reports/                           # JSON + CSV output (gitignored)

Data

The data/ directory contains 10 synthetic clinical trial protocols (~1-2 pages each in markdown) covering oncology (NSCLC, pembrolizumab), cardiology (HFrEF, SGLT2 inhibitor), endocrinology (T2D, dual GIP/GLP-1), rheumatology (RA, JAK1 inhibitor), psychiatry (treatment-resistant MDD, psilocybin analogue), dermatology (atopic dermatitis, IL-31R biologic), gastroenterology (Crohn's, anti-TL1A), neurology (early AD, anti-amyloid mAb), neurogenetics (SOD1 ALS, antisense oligonucleotide), and infectious disease (cUTI, novel cephalosporin).

Every protocol has the same canonical sections (Phase, Indication, Intervention, Primary Endpoint, N, Adverse Events) so the heuristic extractor can locate fields reliably.


Demo

Claude Code connecting to the MCP server, calling all four tools in sequence, and producing a natural-language answer:

Claude Code calling the clinical-doc-agent MCP tools


License

MIT — see LICENSE.

Available Tools

4 tools
cluster_adverse_eventsA

Identify and group adverse-event mentions across one or more protocols.

Returns clusters bucketed by body system (gastrointestinal, cardiovascular,
neurological, dermatological, hematological, hepatic, respiratory, infections,
metabolic, other). If ``document_ids`` is omitted, clusters across all protocols.
ParametersJSON Schema
NameRequiredDescriptionDefault
document_idsNo

Output Schema

ParametersJSON Schema
NameRequiredDescription
resultYes

TDQS

A4.2/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description implies a read-only operation through 'Identify and group' and describes the output structure. It lacks explicit statements on safety (e.g., non-destructive) or side effects, which would raise the score. The parameter behavior (omission => all protocols) is well explained.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is two concise sentences with no unnecessary words. The first sentence states the primary action, the second details output and parameter behavior, making it efficient and front-loaded.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given one optional parameter and an existing output schema (not shown but referenced), the description is nearly complete. It explains output and parameter behavior. A minor gap is the lack of explicit mention that it operates on existing mentions without modification.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Despite 0% schema description coverage, the description adds significant meaning to the only parameter (`document_ids`) by explaining that omission clusters across all protocols. This compensates for the schema's lack of detail, though it could clarify what constitutes a document ID.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool groups adverse-event mentions by body system, with a specific verb ('Identify and group') and resource ('adverse-event mentions'). It distinguishes from siblings like `extract_entities` and `list_documents` by focusing on clustering adverse events across protocols.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It provides clear context for when to use the tool (across one or more protocols) and behavior for the optional parameter. However, it does not explicitly state when not to use it or mention alternatives like `summarize_protocol`, though the sibling names make the distinction inferable.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

extract_entitiesA

Extract clinical entities (drugs, conditions, interventions, endpoints, populations).

If ``document_id`` is omitted, runs across every document and returns the combined set.
Each entity carries its source ``document_id`` so callers can group results.
ParametersJSON Schema
NameRequiredDescriptionDefault
document_idNo

Output Schema

ParametersJSON Schema
NameRequiredDescription
resultYes

TDQS

A4.2/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description fully discloses behavior: extraction, scope control via document_id, and result grouping capability. No destructive implications mentioned, but it is a read operation.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Three concise sentences front-loading purpose, then usage detail. Every sentence adds value with no redundancy.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the single optional parameter and existence of output schema, the description covers main behavior and result structure. Minor omissions like pagination or limits do not detract significantly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Adds context about omitting document_id leading to all-document search, but does not explain its type or format. Schema coverage is 0%, so description partially compensates but leaves gaps.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the verb 'Extract' and the resource 'clinical entities' with explicit types (drugs, conditions, interventions, endpoints, populations). It distinguishes from sibling tools like cluster, list, and summarize.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Provides clear context on when to omit document_id (run across all documents) and that each entity carries source document_id. Lacks explicit when-not or alternatives but offers sufficient guidance for typical use.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

list_documentsA

List all clinical trial protocol documents available in the data directory.

Returns one entry per protocol with id, title, path, indication, and phase.
Use this tool first to discover what documents are available, then pass an
``id`` from the result to ``extract_entities`` or ``summarize_protocol``.
ParametersJSON Schema
NameRequiredDescriptionDefault

No parameters

Output Schema

ParametersJSON Schema
NameRequiredDescription
resultYes

TDQS

A4.6/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description carries the burden. It discloses the output structure (id, title, path, indication, phase) and that it lists all documents. It does not mention read-only nature or potential side effects, but for a list tool this is generally implicit. Slight gap but acceptable.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is three sentences, each serving a clear purpose: define the action, detail the output, and provide usage context. No unnecessary words.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given no parameters and an output schema, the description sufficiently explains what the tool does and what it returns. It could mention edge cases or error conditions, but for a straightforward list tool, it is largely complete.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The input schema has 0 parameters, so the baseline is 4. The description does not need to add parameter information, and it correctly omits any.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool lists all clinical trial protocol documents, specifies the return fields (id, title, path, indication, phase), and explicitly distinguishes from siblings by suggesting subsequent use of extract_entities or summarize_protocol.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description directly advises to use this tool first to discover available documents, then pass the id from its result to the sibling tools extract_entities or summarize_protocol, providing clear workflow guidance.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

summarize_protocolA

Generate a structured summary of a single protocol.

Returns phase, indication, intervention, primary endpoint, planned enrollment,
adverse-event count, and a 3-4 sentence narrative. Uses Claude (Haiku) when
``ANTHROPIC_API_KEY`` is set; otherwise falls back to a deterministic template.
ParametersJSON Schema
NameRequiredDescriptionDefault
document_idYes

TDQS

A3.9/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description discloses model selection behavior (uses Claude Haiku if key is set, else deterministic template), which is helpful. However, it lacks information on error handling for invalid document IDs.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Three sentences, front-loaded with purpose, no extraneous information. Every sentence adds value.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a simple one-parameter tool, the description adequately covers output structure and model behavior. Minor gap: no discussion of edge cases or required dependencies.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters2/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The schema has 0% description coverage on 'document_id', and the description does not elaborate on this parameter, leaving its type or format unclear.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states 'Generate a structured summary of a single protocol' and lists specific outputs (phase, indication, etc.), distinguishing it from siblings like cluster_adverse_events or list_documents.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description implies usage for summarizing a protocol but does not explicitly state when to use this tool versus siblings or provide any exclusion criteria.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

  1. 4 tool updatesv0.1.0
    • First observedcluster_adverse_events
    • First observedextract_entities
    • First observedlist_documents
    • First observedsummarize_protocol

TDQS

A4.3/5.0

Scored across 4 tools

Disambiguation5/5

Each tool has a clearly distinct purpose: listing documents, extracting entities, clustering adverse events, and summarizing protocols. No overlap or ambiguity.

Naming Consistency5/5

All tool names follow a consistent verb_noun pattern with snake_case (e.g., cluster_adverse_events, extract_entities), making them predictable and easy to distinguish.

Tool Count5/5

With 4 tools, the server is well-scoped for its purpose of clinical document analysis. Each tool fills a necessary role without redundancy or excessive granularity.

Completeness4/5

The set covers key functions: discovery, entity extraction, AE clustering, and summarization. Minor gaps exist (e.g., no search or cross-protocol comparison), but core workflows are supported.

Maintenance

ActivityInactive
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers