VibecoderMcSwaggins commited on
Commit
dd587c9
Β·
1 Parent(s): 7b20f5d

docs: add Phase 12 MCP Server Integration specification

Browse files

- Introduced detailed documentation for the MCP server integration, outlining goals, implementation options, and technical specifications.
- Created MCP tool wrappers for PubMed, ClinicalTrials, and bioRxiv, enabling compliance with Track 2 requirements.
- Updated Gradio app to support MCP server functionality, including new tool interfaces.
- Added unit and integration tests to ensure functionality and compliance with MCP standards.

Files added:
- docs/implementation/12_phase_mcp_server.md
- src/mcp_tools.py
- src/app.py
- tests/unit/test_mcp_tools.py
- tests/integration/test_mcp_server.py
- Updated pyproject.toml for MCP dependencies

docs/implementation/12_phase_mcp_server.md ADDED
@@ -0,0 +1,832 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Phase 12 Implementation Spec: MCP Server Integration
2
+
3
+ **Goal**: Expose DeepCritical search tools as MCP servers for Track 2 compliance.
4
+ **Philosophy**: "MCP is the bridge between tools and LLMs."
5
+ **Prerequisite**: Phase 11 complete (all search tools working)
6
+ **Priority**: P0 - REQUIRED FOR HACKATHON TRACK 2
7
+ **Estimated Time**: 2-3 hours
8
+
9
+ ---
10
+
11
+ ## 1. Why MCP Server?
12
+
13
+ ### Hackathon Requirement
14
+
15
+ | Requirement | Status Before | Status After |
16
+ |-------------|---------------|--------------|
17
+ | Must use MCP servers as tools | **MISSING** | **COMPLIANT** |
18
+ | Autonomous Agent behavior | **Have it** | Have it |
19
+ | Must be Gradio app | **Have it** | Have it |
20
+ | Planning/reasoning/execution | **Have it** | Have it |
21
+
22
+ **Bottom Line**: Without MCP server, we're disqualified from Track 2.
23
+
24
+ ### What MCP Enables
25
+
26
+ ```
27
+ Current State:
28
+ Our Tools β†’ Called directly by Python code β†’ Only our app can use them
29
+
30
+ After MCP:
31
+ Our Tools β†’ Exposed via MCP protocol β†’ Claude Desktop, Cursor, ANY MCP client
32
+ ```
33
+
34
+ ---
35
+
36
+ ## 2. Implementation Options Analysis
37
+
38
+ ### Option A: Gradio MCP (Recommended)
39
+
40
+ **Pros:**
41
+ - Single parameter: `demo.launch(mcp_server=True)`
42
+ - Already have Gradio app
43
+ - Automatic tool schema generation from docstrings
44
+ - Built into Gradio 5.0+
45
+
46
+ **Cons:**
47
+ - Requires Gradio 5.0+ with MCP extras
48
+ - Must follow strict docstring format
49
+
50
+ ### Option B: Native MCP SDK (FastMCP)
51
+
52
+ **Pros:**
53
+ - More control over tool definitions
54
+ - Explicit server configuration
55
+ - Separate from UI concerns
56
+
57
+ **Cons:**
58
+ - Separate server process
59
+ - More code to maintain
60
+ - Additional dependency
61
+
62
+ ### Decision: **Gradio MCP (Option A)**
63
+
64
+ Rationale:
65
+ 1. Already have Gradio app (`src/app.py`)
66
+ 2. Minimal code changes
67
+ 3. Judges will appreciate simplicity
68
+ 4. Follows hackathon's official Gradio guide
69
+
70
+ ---
71
+
72
+ ## 3. Technical Specification
73
+
74
+ ### 3.1 Dependencies
75
+
76
+ ```toml
77
+ # pyproject.toml - add MCP extras
78
+ dependencies = [
79
+ "gradio[mcp]>=5.0.0", # Updated from gradio>=4.0
80
+ # ... existing deps
81
+ ]
82
+ ```
83
+
84
+ ### 3.2 MCP Tool Functions
85
+
86
+ Each tool needs:
87
+ 1. **Type hints** on all parameters
88
+ 2. **Docstring** with Args section (Google style)
89
+ 3. **Return type** annotation
90
+ 4. **`api_name`** parameter for explicit endpoint naming
91
+
92
+ ```python
93
+ async def search_pubmed(query: str, max_results: int = 10) -> str:
94
+ """Search PubMed for biomedical literature.
95
+
96
+ Args:
97
+ query: Search query for PubMed (e.g., "metformin alzheimer")
98
+ max_results: Maximum number of results to return (1-50)
99
+
100
+ Returns:
101
+ Formatted search results with titles, citations, and abstracts
102
+ """
103
+ ```
104
+
105
+ ### 3.3 MCP Server URL
106
+
107
+ Once launched:
108
+ ```
109
+ http://localhost:7860/gradio_api/mcp/
110
+ ```
111
+
112
+ Or on HuggingFace Spaces:
113
+ ```
114
+ https://[space-id].hf.space/gradio_api/mcp/
115
+ ```
116
+
117
+ ---
118
+
119
+ ## 4. Implementation
120
+
121
+ ### 4.1 MCP Tool Wrappers (`src/mcp_tools.py`)
122
+
123
+ ```python
124
+ """MCP tool wrappers for DeepCritical search tools.
125
+
126
+ These functions expose our search tools via MCP protocol.
127
+ Each function follows the MCP tool contract:
128
+ - Full type hints
129
+ - Google-style docstrings with Args section
130
+ - Formatted string returns
131
+ """
132
+
133
+ from src.tools.biorxiv import BioRxivTool
134
+ from src.tools.clinicaltrials import ClinicalTrialsTool
135
+ from src.tools.pubmed import PubMedTool
136
+
137
+
138
+ # Singleton instances (avoid recreating on each call)
139
+ _pubmed = PubMedTool()
140
+ _trials = ClinicalTrialsTool()
141
+ _biorxiv = BioRxivTool()
142
+
143
+
144
+ async def search_pubmed(query: str, max_results: int = 10) -> str:
145
+ """Search PubMed for peer-reviewed biomedical literature.
146
+
147
+ Searches NCBI PubMed database for scientific papers matching your query.
148
+ Returns titles, authors, abstracts, and citation information.
149
+
150
+ Args:
151
+ query: Search query (e.g., "metformin alzheimer", "drug repurposing cancer")
152
+ max_results: Maximum results to return (1-50, default 10)
153
+
154
+ Returns:
155
+ Formatted search results with paper titles, authors, dates, and abstracts
156
+ """
157
+ max_results = max(1, min(50, max_results)) # Clamp to valid range
158
+
159
+ results = await _pubmed.search(query, max_results)
160
+
161
+ if not results:
162
+ return f"No PubMed results found for: {query}"
163
+
164
+ formatted = [f"## PubMed Results for: {query}\n"]
165
+ for i, evidence in enumerate(results, 1):
166
+ formatted.append(f"### {i}. {evidence.citation.title}")
167
+ formatted.append(f"**Authors**: {', '.join(evidence.citation.authors[:3])}")
168
+ formatted.append(f"**Date**: {evidence.citation.date}")
169
+ formatted.append(f"**URL**: {evidence.citation.url}")
170
+ formatted.append(f"\n{evidence.content}\n")
171
+
172
+ return "\n".join(formatted)
173
+
174
+
175
+ async def search_clinical_trials(query: str, max_results: int = 10) -> str:
176
+ """Search ClinicalTrials.gov for clinical trial data.
177
+
178
+ Searches the ClinicalTrials.gov database for trials matching your query.
179
+ Returns trial titles, phases, status, conditions, and interventions.
180
+
181
+ Args:
182
+ query: Search query (e.g., "metformin alzheimer", "diabetes phase 3")
183
+ max_results: Maximum results to return (1-50, default 10)
184
+
185
+ Returns:
186
+ Formatted clinical trial information with NCT IDs, phases, and status
187
+ """
188
+ max_results = max(1, min(50, max_results))
189
+
190
+ results = await _trials.search(query, max_results)
191
+
192
+ if not results:
193
+ return f"No clinical trials found for: {query}"
194
+
195
+ formatted = [f"## Clinical Trials for: {query}\n"]
196
+ for i, evidence in enumerate(results, 1):
197
+ formatted.append(f"### {i}. {evidence.citation.title}")
198
+ formatted.append(f"**URL**: {evidence.citation.url}")
199
+ formatted.append(f"**Date**: {evidence.citation.date}")
200
+ formatted.append(f"\n{evidence.content}\n")
201
+
202
+ return "\n".join(formatted)
203
+
204
+
205
+ async def search_biorxiv(query: str, max_results: int = 10) -> str:
206
+ """Search bioRxiv/medRxiv for preprint research.
207
+
208
+ Searches bioRxiv and medRxiv preprint servers for cutting-edge research.
209
+ Note: Preprints are NOT peer-reviewed but contain the latest findings.
210
+
211
+ Args:
212
+ query: Search query (e.g., "metformin neuroprotection", "long covid treatment")
213
+ max_results: Maximum results to return (1-50, default 10)
214
+
215
+ Returns:
216
+ Formatted preprint results with titles, authors, and abstracts
217
+ """
218
+ max_results = max(1, min(50, max_results))
219
+
220
+ results = await _biorxiv.search(query, max_results)
221
+
222
+ if not results:
223
+ return f"No bioRxiv/medRxiv preprints found for: {query}"
224
+
225
+ formatted = [f"## Preprint Results for: {query}\n"]
226
+ for i, evidence in enumerate(results, 1):
227
+ formatted.append(f"### {i}. {evidence.citation.title}")
228
+ formatted.append(f"**Authors**: {', '.join(evidence.citation.authors[:3])}")
229
+ formatted.append(f"**Date**: {evidence.citation.date}")
230
+ formatted.append(f"**URL**: {evidence.citation.url}")
231
+ formatted.append(f"\n{evidence.content}\n")
232
+
233
+ return "\n".join(formatted)
234
+
235
+
236
+ async def search_all_sources(query: str, max_per_source: int = 5) -> str:
237
+ """Search all biomedical sources simultaneously.
238
+
239
+ Performs parallel search across PubMed, ClinicalTrials.gov, and bioRxiv.
240
+ This is the most comprehensive search option for drug repurposing research.
241
+
242
+ Args:
243
+ query: Search query (e.g., "metformin alzheimer", "aspirin cancer prevention")
244
+ max_per_source: Maximum results per source (1-20, default 5)
245
+
246
+ Returns:
247
+ Combined results from all sources with source labels
248
+ """
249
+ import asyncio
250
+
251
+ max_per_source = max(1, min(20, max_per_source))
252
+
253
+ # Run all searches in parallel
254
+ pubmed_task = search_pubmed(query, max_per_source)
255
+ trials_task = search_clinical_trials(query, max_per_source)
256
+ biorxiv_task = search_biorxiv(query, max_per_source)
257
+
258
+ pubmed_results, trials_results, biorxiv_results = await asyncio.gather(
259
+ pubmed_task, trials_task, biorxiv_task, return_exceptions=True
260
+ )
261
+
262
+ formatted = [f"# Comprehensive Search: {query}\n"]
263
+
264
+ # Add each result section (handle exceptions gracefully)
265
+ if isinstance(pubmed_results, str):
266
+ formatted.append(pubmed_results)
267
+ else:
268
+ formatted.append(f"## PubMed\n*Error: {pubmed_results}*\n")
269
+
270
+ if isinstance(trials_results, str):
271
+ formatted.append(trials_results)
272
+ else:
273
+ formatted.append(f"## Clinical Trials\n*Error: {trials_results}*\n")
274
+
275
+ if isinstance(biorxiv_results, str):
276
+ formatted.append(biorxiv_results)
277
+ else:
278
+ formatted.append(f"## Preprints\n*Error: {biorxiv_results}*\n")
279
+
280
+ return "\n---\n".join(formatted)
281
+ ```
282
+
283
+ ### 4.2 Update Gradio App (`src/app.py`)
284
+
285
+ ```python
286
+ """Gradio UI for DeepCritical agent with MCP server support."""
287
+
288
+ import os
289
+ from collections.abc import AsyncGenerator
290
+ from typing import Any
291
+
292
+ import gradio as gr
293
+
294
+ from src.agent_factory.judges import JudgeHandler, MockJudgeHandler
295
+ from src.mcp_tools import (
296
+ search_all_sources,
297
+ search_biorxiv,
298
+ search_clinical_trials,
299
+ search_pubmed,
300
+ )
301
+ from src.orchestrator_factory import create_orchestrator
302
+ from src.tools.biorxiv import BioRxivTool
303
+ from src.tools.clinicaltrials import ClinicalTrialsTool
304
+ from src.tools.pubmed import PubMedTool
305
+ from src.tools.search_handler import SearchHandler
306
+ from src.utils.models import OrchestratorConfig
307
+
308
+
309
+ # ... (existing configure_orchestrator and research_agent functions unchanged)
310
+
311
+
312
+ def create_demo() -> Any:
313
+ """
314
+ Create the Gradio demo interface with MCP support.
315
+
316
+ Returns:
317
+ Configured Gradio Blocks interface with MCP server enabled
318
+ """
319
+ with gr.Blocks(
320
+ title="DeepCritical - Drug Repurposing Research Agent",
321
+ theme=gr.themes.Soft(),
322
+ ) as demo:
323
+ gr.Markdown("""
324
+ # DeepCritical
325
+ ## AI-Powered Drug Repurposing Research Agent
326
+
327
+ Ask questions about potential drug repurposing opportunities.
328
+ The agent searches PubMed, ClinicalTrials.gov, and bioRxiv/medRxiv preprints.
329
+
330
+ **Example questions:**
331
+ - "What drugs could be repurposed for Alzheimer's disease?"
332
+ - "Is metformin effective for cancer treatment?"
333
+ - "What existing medications show promise for Long COVID?"
334
+ """)
335
+
336
+ # Main chat interface (existing)
337
+ gr.ChatInterface(
338
+ fn=research_agent,
339
+ type="messages",
340
+ title="",
341
+ examples=[
342
+ "What drugs could be repurposed for Alzheimer's disease?",
343
+ "Is metformin effective for treating cancer?",
344
+ "What medications show promise for Long COVID treatment?",
345
+ "Can statins be repurposed for neurological conditions?",
346
+ ],
347
+ additional_inputs=[
348
+ gr.Radio(
349
+ choices=["simple", "magentic"],
350
+ value="simple",
351
+ label="Orchestrator Mode",
352
+ info="Simple: Linear (OpenAI/Anthropic) | Magentic: Multi-Agent (OpenAI)",
353
+ )
354
+ ],
355
+ )
356
+
357
+ # MCP Tool Interfaces (exposed via MCP protocol)
358
+ gr.Markdown("---\n## MCP Tools (Also Available via Claude Desktop)")
359
+
360
+ with gr.Tab("PubMed Search"):
361
+ gr.Interface(
362
+ fn=search_pubmed,
363
+ inputs=[
364
+ gr.Textbox(label="Query", placeholder="metformin alzheimer"),
365
+ gr.Slider(1, 50, value=10, step=1, label="Max Results"),
366
+ ],
367
+ outputs=gr.Markdown(label="Results"),
368
+ api_name="search_pubmed",
369
+ )
370
+
371
+ with gr.Tab("Clinical Trials"):
372
+ gr.Interface(
373
+ fn=search_clinical_trials,
374
+ inputs=[
375
+ gr.Textbox(label="Query", placeholder="diabetes phase 3"),
376
+ gr.Slider(1, 50, value=10, step=1, label="Max Results"),
377
+ ],
378
+ outputs=gr.Markdown(label="Results"),
379
+ api_name="search_clinical_trials",
380
+ )
381
+
382
+ with gr.Tab("Preprints"):
383
+ gr.Interface(
384
+ fn=search_biorxiv,
385
+ inputs=[
386
+ gr.Textbox(label="Query", placeholder="long covid treatment"),
387
+ gr.Slider(1, 50, value=10, step=1, label="Max Results"),
388
+ ],
389
+ outputs=gr.Markdown(label="Results"),
390
+ api_name="search_biorxiv",
391
+ )
392
+
393
+ with gr.Tab("Search All"):
394
+ gr.Interface(
395
+ fn=search_all_sources,
396
+ inputs=[
397
+ gr.Textbox(label="Query", placeholder="metformin cancer"),
398
+ gr.Slider(1, 20, value=5, step=1, label="Max Per Source"),
399
+ ],
400
+ outputs=gr.Markdown(label="Results"),
401
+ api_name="search_all",
402
+ )
403
+
404
+ gr.Markdown("""
405
+ ---
406
+ **Note**: This is a research tool and should not be used for medical decisions.
407
+ Always consult healthcare professionals for medical advice.
408
+
409
+ Built with PydanticAI + PubMed, ClinicalTrials.gov & bioRxiv
410
+
411
+ **MCP Server**: Available at `/gradio_api/mcp/` for Claude Desktop integration
412
+ """)
413
+
414
+ return demo
415
+
416
+
417
+ def main() -> None:
418
+ """Run the Gradio app with MCP server enabled."""
419
+ demo = create_demo()
420
+ demo.launch(
421
+ server_name="0.0.0.0",
422
+ server_port=7860,
423
+ share=False,
424
+ mcp_server=True, # Enable MCP server
425
+ )
426
+
427
+
428
+ if __name__ == "__main__":
429
+ main()
430
+ ```
431
+
432
+ ---
433
+
434
+ ## 5. TDD Test Suite
435
+
436
+ ### 5.1 Unit Tests (`tests/unit/test_mcp_tools.py`)
437
+
438
+ ```python
439
+ """Unit tests for MCP tool wrappers."""
440
+
441
+ from unittest.mock import AsyncMock, patch
442
+
443
+ import pytest
444
+
445
+ from src.mcp_tools import (
446
+ search_all_sources,
447
+ search_biorxiv,
448
+ search_clinical_trials,
449
+ search_pubmed,
450
+ )
451
+ from src.utils.models import Citation, Evidence
452
+
453
+
454
+ @pytest.fixture
455
+ def mock_evidence() -> Evidence:
456
+ """Sample evidence for testing."""
457
+ return Evidence(
458
+ content="Metformin shows neuroprotective effects in preclinical models.",
459
+ citation=Citation(
460
+ source="pubmed",
461
+ title="Metformin and Alzheimer's Disease",
462
+ url="https://pubmed.ncbi.nlm.nih.gov/12345678/",
463
+ date="2024-01-15",
464
+ authors=["Smith J", "Jones M", "Brown K"],
465
+ ),
466
+ relevance=0.85,
467
+ )
468
+
469
+
470
+ class TestSearchPubMed:
471
+ """Tests for search_pubmed MCP tool."""
472
+
473
+ @pytest.mark.asyncio
474
+ async def test_returns_formatted_string(self, mock_evidence: Evidence) -> None:
475
+ """Should return formatted markdown string."""
476
+ with patch("src.mcp_tools._pubmed") as mock_tool:
477
+ mock_tool.search = AsyncMock(return_value=[mock_evidence])
478
+
479
+ result = await search_pubmed("metformin alzheimer", 10)
480
+
481
+ assert isinstance(result, str)
482
+ assert "PubMed Results" in result
483
+ assert "Metformin and Alzheimer's Disease" in result
484
+ assert "Smith J" in result
485
+
486
+ @pytest.mark.asyncio
487
+ async def test_clamps_max_results(self) -> None:
488
+ """Should clamp max_results to valid range (1-50)."""
489
+ with patch("src.mcp_tools._pubmed") as mock_tool:
490
+ mock_tool.search = AsyncMock(return_value=[])
491
+
492
+ # Test lower bound
493
+ await search_pubmed("test", 0)
494
+ mock_tool.search.assert_called_with("test", 1)
495
+
496
+ # Test upper bound
497
+ await search_pubmed("test", 100)
498
+ mock_tool.search.assert_called_with("test", 50)
499
+
500
+ @pytest.mark.asyncio
501
+ async def test_handles_no_results(self) -> None:
502
+ """Should return appropriate message when no results."""
503
+ with patch("src.mcp_tools._pubmed") as mock_tool:
504
+ mock_tool.search = AsyncMock(return_value=[])
505
+
506
+ result = await search_pubmed("xyznonexistent", 10)
507
+
508
+ assert "No PubMed results found" in result
509
+
510
+
511
+ class TestSearchClinicalTrials:
512
+ """Tests for search_clinical_trials MCP tool."""
513
+
514
+ @pytest.mark.asyncio
515
+ async def test_returns_formatted_string(self, mock_evidence: Evidence) -> None:
516
+ """Should return formatted markdown string."""
517
+ mock_evidence.citation.source = "clinicaltrials" # type: ignore
518
+
519
+ with patch("src.mcp_tools._trials") as mock_tool:
520
+ mock_tool.search = AsyncMock(return_value=[mock_evidence])
521
+
522
+ result = await search_clinical_trials("diabetes", 10)
523
+
524
+ assert isinstance(result, str)
525
+ assert "Clinical Trials" in result
526
+
527
+
528
+ class TestSearchBiorxiv:
529
+ """Tests for search_biorxiv MCP tool."""
530
+
531
+ @pytest.mark.asyncio
532
+ async def test_returns_formatted_string(self, mock_evidence: Evidence) -> None:
533
+ """Should return formatted markdown string."""
534
+ mock_evidence.citation.source = "biorxiv" # type: ignore
535
+
536
+ with patch("src.mcp_tools._biorxiv") as mock_tool:
537
+ mock_tool.search = AsyncMock(return_value=[mock_evidence])
538
+
539
+ result = await search_biorxiv("preprint search", 10)
540
+
541
+ assert isinstance(result, str)
542
+ assert "Preprint Results" in result
543
+
544
+
545
+ class TestSearchAllSources:
546
+ """Tests for search_all_sources MCP tool."""
547
+
548
+ @pytest.mark.asyncio
549
+ async def test_combines_all_sources(self, mock_evidence: Evidence) -> None:
550
+ """Should combine results from all sources."""
551
+ with patch("src.mcp_tools.search_pubmed", new_callable=AsyncMock) as mock_pubmed, \
552
+ patch("src.mcp_tools.search_clinical_trials", new_callable=AsyncMock) as mock_trials, \
553
+ patch("src.mcp_tools.search_biorxiv", new_callable=AsyncMock) as mock_biorxiv:
554
+
555
+ mock_pubmed.return_value = "## PubMed Results"
556
+ mock_trials.return_value = "## Clinical Trials"
557
+ mock_biorxiv.return_value = "## Preprints"
558
+
559
+ result = await search_all_sources("metformin", 5)
560
+
561
+ assert "Comprehensive Search" in result
562
+ assert "PubMed" in result
563
+ assert "Clinical Trials" in result
564
+ assert "Preprints" in result
565
+
566
+ @pytest.mark.asyncio
567
+ async def test_handles_partial_failures(self) -> None:
568
+ """Should handle partial failures gracefully."""
569
+ with patch("src.mcp_tools.search_pubmed", new_callable=AsyncMock) as mock_pubmed, \
570
+ patch("src.mcp_tools.search_clinical_trials", new_callable=AsyncMock) as mock_trials, \
571
+ patch("src.mcp_tools.search_biorxiv", new_callable=AsyncMock) as mock_biorxiv:
572
+
573
+ mock_pubmed.return_value = "## PubMed Results"
574
+ mock_trials.side_effect = Exception("API Error")
575
+ mock_biorxiv.return_value = "## Preprints"
576
+
577
+ result = await search_all_sources("metformin", 5)
578
+
579
+ # Should still contain working sources
580
+ assert "PubMed" in result
581
+ assert "Preprints" in result
582
+ # Should show error for failed source
583
+ assert "Error" in result
584
+
585
+
586
+ class TestMCPDocstrings:
587
+ """Tests that docstrings follow MCP format."""
588
+
589
+ def test_search_pubmed_has_args_section(self) -> None:
590
+ """Docstring must have Args section for MCP schema generation."""
591
+ assert search_pubmed.__doc__ is not None
592
+ assert "Args:" in search_pubmed.__doc__
593
+ assert "query:" in search_pubmed.__doc__
594
+ assert "max_results:" in search_pubmed.__doc__
595
+ assert "Returns:" in search_pubmed.__doc__
596
+
597
+ def test_search_clinical_trials_has_args_section(self) -> None:
598
+ """Docstring must have Args section for MCP schema generation."""
599
+ assert search_clinical_trials.__doc__ is not None
600
+ assert "Args:" in search_clinical_trials.__doc__
601
+
602
+ def test_search_biorxiv_has_args_section(self) -> None:
603
+ """Docstring must have Args section for MCP schema generation."""
604
+ assert search_biorxiv.__doc__ is not None
605
+ assert "Args:" in search_biorxiv.__doc__
606
+
607
+ def test_search_all_sources_has_args_section(self) -> None:
608
+ """Docstring must have Args section for MCP schema generation."""
609
+ assert search_all_sources.__doc__ is not None
610
+ assert "Args:" in search_all_sources.__doc__
611
+
612
+
613
+ class TestMCPTypeHints:
614
+ """Tests that type hints are complete for MCP."""
615
+
616
+ def test_search_pubmed_type_hints(self) -> None:
617
+ """All parameters and return must have type hints."""
618
+ import inspect
619
+
620
+ sig = inspect.signature(search_pubmed)
621
+
622
+ # Check parameter hints
623
+ assert sig.parameters["query"].annotation == str
624
+ assert sig.parameters["max_results"].annotation == int
625
+
626
+ # Check return hint
627
+ assert sig.return_annotation == str
628
+
629
+ def test_search_clinical_trials_type_hints(self) -> None:
630
+ """All parameters and return must have type hints."""
631
+ import inspect
632
+
633
+ sig = inspect.signature(search_clinical_trials)
634
+ assert sig.parameters["query"].annotation == str
635
+ assert sig.parameters["max_results"].annotation == int
636
+ assert sig.return_annotation == str
637
+ ```
638
+
639
+ ### 5.2 Integration Test (`tests/integration/test_mcp_server.py`)
640
+
641
+ ```python
642
+ """Integration tests for MCP server functionality."""
643
+
644
+ import pytest
645
+
646
+
647
+ class TestMCPServerIntegration:
648
+ """Integration tests for MCP server (requires running app)."""
649
+
650
+ @pytest.mark.integration
651
+ @pytest.mark.asyncio
652
+ async def test_mcp_tools_work_end_to_end(self) -> None:
653
+ """Test that MCP tools execute real searches."""
654
+ from src.mcp_tools import search_pubmed
655
+
656
+ result = await search_pubmed("metformin diabetes", 3)
657
+
658
+ assert isinstance(result, str)
659
+ assert "PubMed Results" in result
660
+ # Should have actual content (not just "no results")
661
+ assert len(result) > 100
662
+ ```
663
+
664
+ ---
665
+
666
+ ## 6. Claude Desktop Configuration
667
+
668
+ ### 6.1 Local Development
669
+
670
+ ```json
671
+ // ~/.config/claude/claude_desktop_config.json (Linux/Mac)
672
+ // %APPDATA%\Claude\claude_desktop_config.json (Windows)
673
+ {
674
+ "mcpServers": {
675
+ "deepcritical": {
676
+ "url": "http://localhost:7860/gradio_api/mcp/"
677
+ }
678
+ }
679
+ }
680
+ ```
681
+
682
+ ### 6.2 HuggingFace Spaces
683
+
684
+ ```json
685
+ {
686
+ "mcpServers": {
687
+ "deepcritical": {
688
+ "url": "https://MCP-1st-Birthday-deepcritical.hf.space/gradio_api/mcp/"
689
+ }
690
+ }
691
+ }
692
+ ```
693
+
694
+ ### 6.3 Private Spaces (with auth)
695
+
696
+ ```json
697
+ {
698
+ "mcpServers": {
699
+ "deepcritical": {
700
+ "url": "https://your-space.hf.space/gradio_api/mcp/",
701
+ "headers": {
702
+ "Authorization": "Bearer hf_xxxxxxxxxxxxx"
703
+ }
704
+ }
705
+ }
706
+ }
707
+ ```
708
+
709
+ ---
710
+
711
+ ## 7. Verification Commands
712
+
713
+ ```bash
714
+ # 1. Install MCP extras
715
+ uv add "gradio[mcp]>=5.0.0"
716
+
717
+ # 2. Run unit tests
718
+ uv run pytest tests/unit/test_mcp_tools.py -v
719
+
720
+ # 3. Run full test suite
721
+ make check
722
+
723
+ # 4. Start server with MCP
724
+ uv run python src/app.py
725
+
726
+ # 5. Verify MCP schema (in another terminal)
727
+ curl http://localhost:7860/gradio_api/mcp/schema
728
+
729
+ # 6. Test with MCP Inspector
730
+ npx @anthropic/mcp-inspector http://localhost:7860/gradio_api/mcp/
731
+
732
+ # 7. Integration test (requires running server)
733
+ uv run pytest tests/integration/test_mcp_server.py -v -m integration
734
+ ```
735
+
736
+ ---
737
+
738
+ ## 8. Definition of Done
739
+
740
+ Phase 12 is **COMPLETE** when:
741
+
742
+ - [ ] `src/mcp_tools.py` created with all 4 MCP tools
743
+ - [ ] `src/app.py` updated with `mcp_server=True`
744
+ - [ ] Unit tests in `tests/unit/test_mcp_tools.py`
745
+ - [ ] Integration test in `tests/integration/test_mcp_server.py`
746
+ - [ ] `pyproject.toml` updated with `gradio[mcp]`
747
+ - [ ] MCP schema accessible at `/gradio_api/mcp/schema`
748
+ - [ ] Claude Desktop can connect and use tools
749
+ - [ ] All unit tests pass
750
+ - [ ] Lints pass
751
+
752
+ ---
753
+
754
+ ## 9. Demo Script for Judges
755
+
756
+ ### Show MCP Integration Works
757
+
758
+ 1. **Start the server**:
759
+ ```bash
760
+ uv run python src/app.py
761
+ ```
762
+
763
+ 2. **Show Claude Desktop using our tools**:
764
+ - Open Claude Desktop with DeepCritical MCP configured
765
+ - Ask: "Search PubMed for metformin Alzheimer's"
766
+ - Show real results appearing
767
+ - Ask: "Now search clinical trials for the same"
768
+ - Show combined analysis
769
+
770
+ 3. **Show MCP Inspector**:
771
+ ```bash
772
+ npx @anthropic/mcp-inspector http://localhost:7860/gradio_api/mcp/
773
+ ```
774
+ - Show all 4 tools listed
775
+ - Execute `search_pubmed` from inspector
776
+ - Show results
777
+
778
+ ---
779
+
780
+ ## 10. Value Delivered
781
+
782
+ | Before | After |
783
+ |--------|-------|
784
+ | Tools only usable in our app | Tools usable by ANY MCP client |
785
+ | Not Track 2 compliant | **FULLY TRACK 2 COMPLIANT** |
786
+ | Can't use with Claude Desktop | Full Claude Desktop integration |
787
+
788
+ **Prize Impact**:
789
+ - Without MCP: **Disqualified from Track 2**
790
+ - With MCP: **Eligible for $2,500 1st place**
791
+
792
+ ---
793
+
794
+ ## 11. Files to Create/Modify
795
+
796
+ | File | Action | Purpose |
797
+ |------|--------|---------|
798
+ | `src/mcp_tools.py` | CREATE | MCP tool wrapper functions |
799
+ | `src/app.py` | MODIFY | Add `mcp_server=True`, add tool tabs |
800
+ | `pyproject.toml` | MODIFY | Add `gradio[mcp]>=5.0.0` |
801
+ | `tests/unit/test_mcp_tools.py` | CREATE | Unit tests for MCP tools |
802
+ | `tests/integration/test_mcp_server.py` | CREATE | Integration tests |
803
+ | `README.md` | MODIFY | Add MCP usage instructions |
804
+
805
+ ---
806
+
807
+ ## 12. Architecture After Phase 12
808
+
809
+ ```
810
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
811
+ β”‚ Claude Desktop / Cursor β”‚
812
+ β”‚ (MCP Client) β”‚
813
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
814
+ β”‚ MCP Protocol
815
+ β–Ό
816
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
817
+ β”‚ Gradio MCP Server β”‚
818
+ β”‚ /gradio_api/mcp/ β”‚
819
+ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
820
+ β”‚ β”‚search_pubmed β”‚ β”‚search_trials β”‚ β”‚search_biorxivβ”‚ β”‚search_ β”‚ β”‚
821
+ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚all β”‚ β”‚
822
+ β”‚ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜ β”‚
823
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”˜
824
+ β”‚ β”‚ β”‚ β”‚
825
+ β–Ό β–Ό β–Ό β–Ό
826
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” (calls all)
827
+ β”‚PubMedToolβ”‚ β”‚Trials β”‚ β”‚BioRxiv β”‚
828
+ β”‚ β”‚ β”‚Tool β”‚ β”‚Tool β”‚
829
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
830
+ ```
831
+
832
+ **This is the MCP compliance stack.**