Inspiration

Every year, thousands of cancer patients miss clinical trials they could qualify for. The matching process is manual, slow, and requires domain expertise that most patients simply do not have access to. Meanwhile, trials struggle to recruit because finding eligible patients is equally hard on the other side.

I wanted to build something that could close that gap. Not a search engine, not a chatbot, but an agent that actually reasons about a patient's clinical profile and matches it against real recruiting trials with explainable confidence score.

MongoDB Atlas was chosen as the partner technology because clinical trial matching is fundamentally a historical pattern-matching problem; the agent needs to know how similar patients have fared before it can calibrate its confidence scores. MongoDB MCP provides the ideal interface for an AI agent to query that historical context at reasoning time, without hardcoding database logic into the agent itself.

What it does

Trialgent is a clinical trial matchmaker built on Google Cloud's Agent Development Kit. A patient uploads their medical record, and the system:

  1. De-identifies it using Google Cloud Data Loss Prevention (DLP), removing all 18 HIPAA identifiers before any AI model sees the data.
  2. Extracts a structured clinical profile, including conditions, stage, biomarkers, medications, prior treatments, and lab values.
  3. Queries ClinicalTrials.gov for actively recruiting Phase 2 and Phase 3 studies.
  4. Uses a structured 6-dimension scoring system to assess eligibility across biomarker match, stage, prior therapy, exclusion risk, organ function, and data completeness.
  5. Drafts a ready-to-send outreach email to the trial coordinator.
  6. Produces a plain English summary written for the patient, not the doctor.

A voice Q&A feature lets the patient asks follow-up questions about their results in real time using Gemini Live API.

How I built it

The agent pipeline is built entirely on Google Cloud ADK with Vertex AI as the model backend. The core components are:

Agent Pipeline

  • Profile extractor: LLM Agent that compresses a 250KB Electronic Health Record (EHR) export to a structured 7KB profile.
  • Trial querier: A deterministic Python tool that queries ClinicalTrials.gov v2 API with biomarker-aware search terms.
  • Eligibility reasoner: LLM Agent with structured 6-dimension scoring and a hard veto mechanism for critical exclusions like severe renal failure.
  • Email drafter and patient summarizer: Two downstream LLM Agents that generate a ready-to-send outreach email and a plain English summary tailored for the patient.

MongoDB MCP Integration The eligibility reasoner uses the official MongoDB MCP server (mongodb-mcp-server v1.11.0) via an HTTP sidecar architecture. When the application starts, a mongodb-mcp-server subprocess is launched as a persistent sidecar on port 3099. During each eligibility assessment, the agent calls query_eligibility_history, which is a plain Python function registered as an ADK tool that connects to the sidecar via two sequential HTTP POST requests to execute a find operation on the eligibility cache. The retrieved historical match data is returned to the eligibility reasoner as plain text, calibrating its confidence scores based on how similar patients have fared on the same trials.

A permanent seed_cache collection pre-populates the database with synthetic historical patients, ensuring meaningful context is available from the first query. If the eligibility cache is empty, the agent falls back to the seed cache before falling back to direct pymongo.

The final architecture runs mongodb-mcp-server as a persistent HTTP sidecar on port 3099, started at application boot via FastAPI's lifespan context manager. This eliminates the 2-3 second subprocess startup overhead per query and keeps the JSON-RPC channel clean.

Caching Layer Five MongoDB Atlas collections serve as a deterministic caching layer: profile cache, trials cache, eligibility cache, seed_cache (permanent historical baseline) and session store. Cache hits reduce end-to-end latency from 90 seconds to under 5 seconds on repeat queries.

Voice Q&A Built on Gemini Live API with Voice Activity Detection (VAD) based turn detection, audio resampling, and graceful interruption handling. The patient can ask natural language questions about their results, including historical context about similar patients, in real time

Infrastructure Deployed on Google Cloud Run with Server-Sent Events (SSE) streaming. Google Cloud DLP runs before any LLM call, ensuring no PII reaches Gemini.

Challenges I ran into

SSE streaming on Cloud Run was the most persistent issue. Python's default block buffering inside Docker containers silently held back SSE events until the buffer filled causing the frontend to appear frozen. The fix required PYTHONUNBUFFERED=1 in the Dockerfile, X-Accel-Buffering: no headers, and a flush comment after each SSE event.

Frontend SSE transition on Cloud Run The results screen failed to transition after the pipeline completed on Cloud Run cache misses, while working perfectly locally and on cache hits. Two weeks of debugging ruled out buffering, timeouts, animation queue races, and TCP chunking. Each fix worked temporarily before the issue resurfaced.

The root cause turned out to be subtle. Cloud Run closes the HTTP connection immediately after sending the last byte. When done=true fires in the fetch + ReadableStream reader, the buffer still contains the unprocessed result event. The trailing buffer flush that should catch it was silently throwing an error first, dropping the event entirely.

The fix was straightforward once the cause was clear: replace fetch + ReadableStream with the browser's native EventSource API, which is designed specifically for SSE and handles all edge cases around connection close, buffering, and event parsing natively. One line of change resolved two weeks of intermittent failures.

ADK timeout caused silent failures after cache expiry. The eligibility_reasoner with structured 6-dimension scoring over 5 trials takes 45 to 90 seconds. A 15-second asyncio.wait_for timeout was silently cutting if off and returning empty results. Increasing it to 120 seconds fixed the issue.

MongoDB MCP schema incompatibility with Vertex AI. The MongoDB MCP server exposes tools with JSON Schema features (const, exclusiveMinimum, propertyNames) that ADK's Gemini schema parser rejects. The fix was to bypass ADK's automatic tool discovery entirely and use the raw MCP Python SDK inside a plain Python function, which ADK registers without schema validation.

MongoDB MCP transport compatibility proved to be the most architecturally challenging integration. The initial implementation used the Python MCP SDK's stdio_client to spawn mongodb-mcp-server as a subprocess and communicate via standard input/output. This failed with a BrokenResourceError because the MongoDB MCP server sends notification messages(notifications/resources/list_changed, notifications/tools/list_changed) over the same stdout channel as JSON-RPC responses, corrupting the stream reader.

Switching to HTTP transport resolved the stream corruption, but introduced two new discoveries. First, the HTTP endpoint requires a two-step session handshake. An initial POST to /mcp returns a mcp-session-id header, which must be included in all subsequent tool calls. Without this session ID, every tool call returns a 400 error. Second, the server wraps all returned documents in an <untrusted-user-data> security fence with a unique UUID delimiter, designed to protect downstream LLMs from prompt injection. The raw response text had to be parsed by extracting the JSON array between the first [ and last ] characters, discarding the surrounding warning prose.

ClinicalTrials.gov API instability meant that the same query returned different trials on different days, breaking deterministic matching. Solved this with a 24-hour trials cache keyed on condition, biomarkers, and phase.

Accomplishments That I am Proud Of

When I sat down to decide what to build, I thought about my late mother, who lost her life to complications from Retroperitoneal Fibrosis. Because the condition had wrapped around her aorta, surgery wasn't an option. The only treatment was steroids, which ultimately worsened her health and led to Non-Hodgkin's lymphoma. At the time, I wasn't aware that clinical trials existed as an alternative path. Even if I had known, navigating that process as someone living outside the US would have been enormously difficult. That experience stayed with me. The complexity a patient faces in finding trials that match their condition is real, and most people simply don't have the time, knowledge, or resources to navigate it alone. That's what I set out to solve.

Generating realistic test data was harder than expected. Synthea, the open-source synthetic patient generator, produces medically accurate EHR exports. However, getting it to generate patients with specific cancer profiles, targeted biomarkers, and realistic edge cases like severe renal failure or incomplete molecular profiling required significant configuration and iteration. Eight carefully crafted patient profiles cover a range of scenarios from strong matches to deliberate exclusions, forming the backbone of the test suite. An additional ten synthetic patients, generated using the same Synthea, pipeline with varied biomarker profiles, are pre-loaded into the seed cache to ensure meaningful historical context is available from the first query; this eliminates the cold start problem for the MongoDB MCP integration.

The pipeline is backed by 434 unit tests covering the scoring logic, cache key generation, JSON parsing, guardrail validation, and SSE event formatting. Writing these tests caught several subtle bugs, including a cache key mismatch between the pre-check and the actual query function that was silently returning zero trials.

I'm proud of what I built in two and a half weeks – ideating, designing, building, testing, debugging, deploying, and recording a demo. Trialgent isn't just a hackathon project. It's something I genuinely wish had existed a decade ago.

What I learned

Building a production-grade agentic pipeline is fundamentally different from building a demo for a hackathon. The hard parts are not the LLM calls, but are the caching strategy, the timeout handling, the graceful degradation when APIs return unexpected results, and the SSE streaming reliability across cloud infrastructure.

The MongoDB MCP integration taught me how the MCP protocol actually works at the transport layer. The MongoDB MCP HTTP transport, using a persistent sidecar process and a session ID handshake, is cleaner and more reliable than the stdio approach when the target SDK sends notification messages over the same channel as JSON-RPC responses. Separating the transport layer from the agent framework entirely, by wrapping the MCP call inside a plain Python function, avoids schema validation conflicts while keeping the integration fully compliant with the MCP protocol.

What's next for Trialgent

Trialgent is designed to generalize. Adding support for a new cancer type requires updating the biomarker vocabulary and synonyms, not the agent code. The next step is connecting to additional trial registries beyond ClinicalTrials.gov and adding a clinician-facing dashboard alongside the patient-facing interface.

To test the app, you can download the test patient file and upload it to the app

Built With

Share this project:

Updates