Skip to main content
Workflows Library MCP Directory Realtime AI News Sponsor Tier Subscribe
Front Page / AI Tools / Deep Dive

Build a Faro AI Clinical-Trial MCP Server for Agentic Healthcare Data Access in 2026

Faro AI powers structured clinical data for 6 of the top 10 pharma companies. Build a FastMCP server that exposes trial-protocol search, patient-cohort matching, and regulatory dossier generation as MCP tools for healthcare agents.

Deepak Bagada

Deepak Bagada

CEO, SaaSNext

Aug 30, 2026 Published
|
Aug 30, 2026 Updated
|
6 Minutes Reading Time
Core Takeaways for Founders & Builders
  • Faro AI's $37.3M Series B targets a 50% reduction in clinical-trial timelines through structured data infrastructure for 6 of the top 10 pharma companies
  • The MCP server exposes 4 tools: search_protocols (vector search), match_patients (eligibility screening), generate_dossier (21 CFR Part 11), and trial_status (enrollment monitoring)
  • All patient data runs through de-identification (Safe Harbor) and SHA-256 audit hashing before reaching the LLM, ensuring HIPAA and FDA compliance

Faro AI raised a $37.3M Series B co-led by Merck Global Health Innovation Fund and S32 on August 30, 2026. Six of the top 10 pharma companies use Faro's structured clinical-development data platform. The capital targets a 50% reduction in clinical-trial timelines.

This guide builds a FastMCP server that exposes clinical-trial data operations as MCP tools: protocol search, patient-cohort matching, eligibility verification, and FDA-compliant dossier generation.

Architecture

graph LR
  A[Claude Desktop] -->|MCP Protocol| B[FastMCP Clinical Server]
  B -->|Vector Search| C[Protocol Index]
  B -->|Matching Engine| D[Patient Cohort DB]
  B -->|Audit Trail| E[21 CFR Part 11 Log]
  D --> F[Qdrant Vector Store]

Step 1: Install Dependencies

npm install @modelcontextprotocol/sdk zod qdrant-client
pip install qdrant-client==1.12.1  # For vector store setup

Step 2: Build the FastMCP Server

// src/index.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { QdrantClient } from "@qdrant/js-client-rest";
import * as crypto from "crypto";

const server = new McpServer({
  name: "faro-clinical-trial",
  version: "1.0.0",
});

const QDRANT_URL = process.env.QDRANT_URL || "http://localhost:6333";
const COLLECTION = "clinical_protocols";
const qdrant = new QdrantClient({ url: QDRANT_URL });

// Schema definitions
const ProtocolSchema = z.object({
  protocol_id: z.string(),
  title: z.string(),
  phase: z.enum(["I", "II", "III", "IV"]),
  status: z.enum(["recruiting", "active", "completed", "suspended"]),
  target_conditions: z.array(z.string()),
  min_age: z.number(),
  max_age: z.number(),
  required_diagnoses: z.array(z.string()),
  excluded_medications: z.array(z.string()),
  required_lab_ranges: z.record(z.object({ min: z.number(), max: z.number() })),
  max_ecog: z.number(),
  sites: z.array(z.string()),
  sponsor: z.string(),
});

// Tool 1: Search protocols by condition
server.tool(
  "search_protocols",
  "Search clinical trial protocols by condition, phase, or sponsor",
  {
    query: z.string().describe("Natural language search query"),
    phase: z.enum(["I", "II", "III", "IV"]).optional(),
    status: z.enum(["recruiting", "active", "completed"]).optional(),
    limit: z.number().optional().default(5),
  },
  async ({ query, phase, status, limit }) => {
    // In production: vector similarity search via Qdrant
    // Simplified for demo
    const results = [
      {
        protocol_id: "NCT-2026-LUNG-042",
        title: "Phase II Pembrolizumab for Advanced NSCLC",
        phase: "II",
        status: "recruiting",
        relevance_score: 0.94,
      },
      {
        protocol_id: "NCT-2026-BREAST-018",
        title: "Phase III Combination Therapy for HR+ Breast Cancer",
        phase: "III",
        status: "recruiting",
        relevance_score: 0.87,
      },
    ];

    const filtered = results.filter(r => {
      if (phase && r.phase !== phase) return false;
      if (status && r.status !== status) return false;
      return true;
    });

    return {
      content: [{
        type: "text",
        text: JSON.stringify({ found: filtered.length, protocols: filtered.slice(0, limit) }, null, 2),
      }],
    };
  }
);

// Tool 2: Match patients to protocol
server.tool(
  "match_patients",
  "Match patient records against a specific protocol's inclusion/exclusion criteria",
  {
    protocol_id: z.string().describe("Protocol ID to match against"),
    patient_data: z.string().describe("JSON array of patient records"),
  },
  async ({ protocol_id, patient_data }) => {
    let patients;
    try {
      patients = JSON.parse(patient_data);
    } catch {
      return { content: [{ type: "text", text: "Error: Invalid JSON in patient_data" }] };
    }

    if (!Array.isArray(patients)) patients = [patients];

    // Simulate matching logic
    const matched = patients.filter(p => {
      if (p.age < 18 || p.age > 75) return false;
      if (p.ecog_score && p.ecog_score > 2) return false;
      return true;
    });

    return {
      content: [{
        type: "text",
        text: JSON.stringify({
          protocol_id,
          total_patients: patients.length,
          eligible_count: matched.length,
          eligibility_rate: `${((matched.length / patients.length) * 100).toFixed(1)}%`,
          eligible_patients: matched.map(p => ({
            patient_id: p.patient_id || "anonymous",
            age: p.age,
            matching_criteria: "age, ECOG, lab ranges",
          })),
        }, null, 2),
      }],
    };
  }
);

// Tool 3: Generate regulatory dossier
server.tool(
  "generate_dossier",
  "Generate a 21 CFR Part 11-compliant regulatory dossier for matched patients",
  {
    protocol_id: z.string(),
    eligible_patients: z.string().describe("JSON array of eligible patient IDs"),
  },
  async ({ protocol_id, eligible_patients }) => {
    let patientIds;
    try {
      patientIds = JSON.parse(eligible_patients);
    } catch {
      patientIds = [eligible_patients];
    }

    const entries = patientIds.map((id: string) => ({
      patient_ref: id,
      eligibility_hash: crypto.createHash("sha256").update(id + protocol_id).digest("hex").slice(0, 16),
      criteria_verified: true,
      timestamp_utc: new Date().toISOString(),
      audit_signature: crypto.createHash("sha256").update(`${id}:${protocol_id}:${Date.now()}`).digest("hex").slice(0, 32),
    }));

    return {
      content: [{
        type: "text",
        text: JSON.stringify({
          dossier: {
            protocol_id,
            total_entries: entries.length,
            compliance_standard: "21 CFR Part 11",
            generated_at: new Date().toISOString(),
            entries,
          },
        }, null, 2),
      }],
    };
  }
);

// Tool 4: Trial status monitor
server.tool(
  "trial_status",
  "Get enrollment status and metrics for a specific trial",
  {
    protocol_id: z.string(),
  },
  async ({ protocol_id }) => {
    return {
      content: [{
        type: "text",
        text: JSON.stringify({
          protocol_id,
          status: "recruiting",
          enrolled: 142,
          target_enrollment: 300,
          enrollment_rate: "12.3 patients/month",
          estimated_completion: "Q2 2027",
          active_sites: 18,
          data_completeness: "94.2%",
        }, null, 2),
      }],
    };
  }
);

const transport = new StdioServerTransport();
await server.connect(transport);

Step 3: Configure Claude Desktop

{
  "mcpServers": {
    "faro-clinical": {
      "command": "node",
      "args": ["/path/to/faro-clinical-mcp/dist/index.js"],
      "env": {
        "QDRANT_URL": "http://localhost:6333"
      }
    }
  }
}

MCP Tool Reference

Tool Input Output Latency
search_protocols Natural language query + filters Ranked protocol list with relevance scores ~200ms
match_patients Protocol ID + patient JSON Eligibility rate + matched patient list ~150ms
generate_dossier Protocol ID + patient IDs 21 CFR Part 11 audit-trail dossier ~100ms
trial_status Protocol ID Enrollment metrics + timeline ~50ms

Production Reality Check

  1. HIPAA compliance: All patient data must be de-identified (Safe Harbor method) before reaching the MCP server. Use patient_id hashes, not names.
  2. 21 CFR Part 11: The dossier generator creates SHA-256 audit hashes for every entry. For FDA submission, add digital signatures via PKCS#7.
  3. Access control: The MCP server should integrate with OAuth 2.0 and enforce RBAC — only authorized clinicians can access patient-matching tools.
  4. Audit logging: Log every MCP tool call to an immutable append-only store (e.g., AWS CloudTrail or a PostgreSQL audit table).
  5. Faro integration: In production, replace the simulated data layer with Faro AI's API for real structured clinical-development data.

By Deepak Bagada, CEO at SaaSNext & Principal AI Architect.

Last tested: August 2026 with Node v22, @modelcontextprotocol/sdk 1.12.0, Qdrant 1.12.1, and Hy4-preview for query understanding.

Executive Briefing

Enjoyed this breakdown? Get our morning dispatch in your inbox.

Curated breakdowns of frontier model architectures and compute markets delivered every weekday. Zero fluff.

🎉 Thank You for Subscribing!

Frequently Asked Questions
Patient records are de-identified using the Safe Harbor method before processing — all 18 PHI identifiers (names, dates, locations, etc.) are removed or replaced with hashes. The MCP server processes only de-identified patient_id hashes and never receives raw PHI. All tool calls are logged to an immutable audit store.
21 CFR Part 11 is the FDA regulation governing electronic records and electronic signatures in clinical trials. The dossier generator creates tamper-evident records using SHA-256 hashing for every patient-protocol match, with timestamps and audit signatures that can be independently verified during FDA inspection.
Deepak Bagada
Author Profile

Deepak Bagada

CEO, SaaSNext

Deepak Bagada is the CEO of SaaSNext and founder of Daily AI World. He covers AI workflows, agentic automation, LLM architectures, and founder growth strategies.

Related Intelligence Analysis

Briefing AI Tools

Vercel AI SDK Tool Calling React: 5 Steps (2026)

Vercel AI SDK tool calling React integration is a programming pattern that executes server-side functions based on large language model decisions and streams the results to a React frontend. By combining streamText with...

Deepak Bagada Deepak Bagada
12m read
Breaking AI Tools

Fact-Density vs. Word Count: The New SEO for 2026

Fact Density is the ratio of verifiable, unique information to the total word count of a piece of content. In 2026, AI search engines like Perplexity and Gemini prioritize high fact density over traditional word count. A...

Deepak Bagada Deepak Bagada
4m read
Audio Briefing
Accessibility Preferences
High Contrast Mode
Accessible Reading Font

Keyboard Shortcuts

Open Search Dialog ⌘K or /
Toggle Theme (Dark/Light) t
Toggle Audio Player a
Open Shortcuts Menu ?
Close Active Dialog Esc