File size: 27,086 Bytes
dd587c9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f9cb2b7
dd587c9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f9cb2b7
dd587c9
 
 
 
f9cb2b7
dd587c9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f9cb2b7
dd587c9
f5b2917
 
dd587c9
 
 
f5b2917
dd587c9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
# Phase 12 Implementation Spec: MCP Server Integration

**Goal**: Expose DeepCritical search tools as MCP servers for Track 2 compliance.
**Philosophy**: "MCP is the bridge between tools and LLMs."
**Prerequisite**: Phase 11 complete (all search tools working)
**Priority**: P0 - REQUIRED FOR HACKATHON TRACK 2
**Estimated Time**: 2-3 hours

---

## 1. Why MCP Server?

### Hackathon Requirement

| Requirement | Status Before | Status After |
|-------------|---------------|--------------|
| Must use MCP servers as tools | **MISSING** | **COMPLIANT** |
| Autonomous Agent behavior | **Have it** | Have it |
| Must be Gradio app | **Have it** | Have it |
| Planning/reasoning/execution | **Have it** | Have it |

**Bottom Line**: Without MCP server, we're disqualified from Track 2.

### What MCP Enables

```text
Current State:
  Our Tools β†’ Called directly by Python code β†’ Only our app can use them

After MCP:
  Our Tools β†’ Exposed via MCP protocol β†’ Claude Desktop, Cursor, ANY MCP client
```

---

## 2. Implementation Options Analysis

### Option A: Gradio MCP (Recommended)

**Pros:**
- Single parameter: `demo.launch(mcp_server=True)`
- Already have Gradio app
- Automatic tool schema generation from docstrings
- Built into Gradio 5.0+

**Cons:**
- Requires Gradio 5.0+ with MCP extras
- Must follow strict docstring format

### Option B: Native MCP SDK (FastMCP)

**Pros:**
- More control over tool definitions
- Explicit server configuration
- Separate from UI concerns

**Cons:**
- Separate server process
- More code to maintain
- Additional dependency

### Decision: **Gradio MCP (Option A)**

Rationale:
1. Already have Gradio app (`src/app.py`)
2. Minimal code changes
3. Judges will appreciate simplicity
4. Follows hackathon's official Gradio guide

---

## 3. Technical Specification

### 3.1 Dependencies

```toml
# pyproject.toml - add MCP extras
dependencies = [
    "gradio[mcp]>=5.0.0",  # Updated from gradio>=4.0
    # ... existing deps
]
```

### 3.2 MCP Tool Functions

Each tool needs:
1. **Type hints** on all parameters
2. **Docstring** with Args section (Google style)
3. **Return type** annotation
4. **`api_name`** parameter for explicit endpoint naming

```python
async def search_pubmed(query: str, max_results: int = 10) -> str:
    """Search PubMed for biomedical literature.

    Args:
        query: Search query for PubMed (e.g., "metformin alzheimer")
        max_results: Maximum number of results to return (1-50)

    Returns:
        Formatted search results with titles, citations, and abstracts
    """
```

### 3.3 MCP Server URL

Once launched:
```text
http://localhost:7860/gradio_api/mcp/
```

Or on HuggingFace Spaces:
```text
https://[space-id].hf.space/gradio_api/mcp/
```

---

## 4. Implementation

### 4.1 MCP Tool Wrappers (`src/mcp_tools.py`)

```python
"""MCP tool wrappers for DeepCritical search tools.

These functions expose our search tools via MCP protocol.
Each function follows the MCP tool contract:
- Full type hints
- Google-style docstrings with Args section
- Formatted string returns
"""

from src.tools.biorxiv import BioRxivTool
from src.tools.clinicaltrials import ClinicalTrialsTool
from src.tools.pubmed import PubMedTool


# Singleton instances (avoid recreating on each call)
_pubmed = PubMedTool()
_trials = ClinicalTrialsTool()
_biorxiv = BioRxivTool()


async def search_pubmed(query: str, max_results: int = 10) -> str:
    """Search PubMed for peer-reviewed biomedical literature.

    Searches NCBI PubMed database for scientific papers matching your query.
    Returns titles, authors, abstracts, and citation information.

    Args:
        query: Search query (e.g., "metformin alzheimer", "drug repurposing cancer")
        max_results: Maximum results to return (1-50, default 10)

    Returns:
        Formatted search results with paper titles, authors, dates, and abstracts
    """
    max_results = max(1, min(50, max_results))  # Clamp to valid range

    results = await _pubmed.search(query, max_results)

    if not results:
        return f"No PubMed results found for: {query}"

    formatted = [f"## PubMed Results for: {query}\n"]
    for i, evidence in enumerate(results, 1):
        formatted.append(f"### {i}. {evidence.citation.title}")
        formatted.append(f"**Authors**: {', '.join(evidence.citation.authors[:3])}")
        formatted.append(f"**Date**: {evidence.citation.date}")
        formatted.append(f"**URL**: {evidence.citation.url}")
        formatted.append(f"\n{evidence.content}\n")

    return "\n".join(formatted)


async def search_clinical_trials(query: str, max_results: int = 10) -> str:
    """Search ClinicalTrials.gov for clinical trial data.

    Searches the ClinicalTrials.gov database for trials matching your query.
    Returns trial titles, phases, status, conditions, and interventions.

    Args:
        query: Search query (e.g., "metformin alzheimer", "diabetes phase 3")
        max_results: Maximum results to return (1-50, default 10)

    Returns:
        Formatted clinical trial information with NCT IDs, phases, and status
    """
    max_results = max(1, min(50, max_results))

    results = await _trials.search(query, max_results)

    if not results:
        return f"No clinical trials found for: {query}"

    formatted = [f"## Clinical Trials for: {query}\n"]
    for i, evidence in enumerate(results, 1):
        formatted.append(f"### {i}. {evidence.citation.title}")
        formatted.append(f"**URL**: {evidence.citation.url}")
        formatted.append(f"**Date**: {evidence.citation.date}")
        formatted.append(f"\n{evidence.content}\n")

    return "\n".join(formatted)


async def search_biorxiv(query: str, max_results: int = 10) -> str:
    """Search bioRxiv/medRxiv for preprint research.

    Searches bioRxiv and medRxiv preprint servers for cutting-edge research.
    Note: Preprints are NOT peer-reviewed but contain the latest findings.

    Args:
        query: Search query (e.g., "metformin neuroprotection", "long covid treatment")
        max_results: Maximum results to return (1-50, default 10)

    Returns:
        Formatted preprint results with titles, authors, and abstracts
    """
    max_results = max(1, min(50, max_results))

    results = await _biorxiv.search(query, max_results)

    if not results:
        return f"No bioRxiv/medRxiv preprints found for: {query}"

    formatted = [f"## Preprint Results for: {query}\n"]
    for i, evidence in enumerate(results, 1):
        formatted.append(f"### {i}. {evidence.citation.title}")
        formatted.append(f"**Authors**: {', '.join(evidence.citation.authors[:3])}")
        formatted.append(f"**Date**: {evidence.citation.date}")
        formatted.append(f"**URL**: {evidence.citation.url}")
        formatted.append(f"\n{evidence.content}\n")

    return "\n".join(formatted)


async def search_all_sources(query: str, max_per_source: int = 5) -> str:
    """Search all biomedical sources simultaneously.

    Performs parallel search across PubMed, ClinicalTrials.gov, and bioRxiv.
    This is the most comprehensive search option for drug repurposing research.

    Args:
        query: Search query (e.g., "metformin alzheimer", "aspirin cancer prevention")
        max_per_source: Maximum results per source (1-20, default 5)

    Returns:
        Combined results from all sources with source labels
    """
    import asyncio

    max_per_source = max(1, min(20, max_per_source))

    # Run all searches in parallel
    pubmed_task = search_pubmed(query, max_per_source)
    trials_task = search_clinical_trials(query, max_per_source)
    biorxiv_task = search_biorxiv(query, max_per_source)

    pubmed_results, trials_results, biorxiv_results = await asyncio.gather(
        pubmed_task, trials_task, biorxiv_task, return_exceptions=True
    )

    formatted = [f"# Comprehensive Search: {query}\n"]

    # Add each result section (handle exceptions gracefully)
    if isinstance(pubmed_results, str):
        formatted.append(pubmed_results)
    else:
        formatted.append(f"## PubMed\n*Error: {pubmed_results}*\n")

    if isinstance(trials_results, str):
        formatted.append(trials_results)
    else:
        formatted.append(f"## Clinical Trials\n*Error: {trials_results}*\n")

    if isinstance(biorxiv_results, str):
        formatted.append(biorxiv_results)
    else:
        formatted.append(f"## Preprints\n*Error: {biorxiv_results}*\n")

    return "\n---\n".join(formatted)
```

### 4.2 Update Gradio App (`src/app.py`)

```python
"""Gradio UI for DeepCritical agent with MCP server support."""

import os
from collections.abc import AsyncGenerator
from typing import Any

import gradio as gr

from src.agent_factory.judges import JudgeHandler, MockJudgeHandler
from src.mcp_tools import (
    search_all_sources,
    search_biorxiv,
    search_clinical_trials,
    search_pubmed,
)
from src.orchestrator_factory import create_orchestrator
from src.tools.biorxiv import BioRxivTool
from src.tools.clinicaltrials import ClinicalTrialsTool
from src.tools.pubmed import PubMedTool
from src.tools.search_handler import SearchHandler
from src.utils.models import OrchestratorConfig


# ... (existing configure_orchestrator and research_agent functions unchanged)


def create_demo() -> Any:
    """
    Create the Gradio demo interface with MCP support.

    Returns:
        Configured Gradio Blocks interface with MCP server enabled
    """
    with gr.Blocks(
        title="DeepCritical - Drug Repurposing Research Agent",
        theme=gr.themes.Soft(),
    ) as demo:
        gr.Markdown("""
        # DeepCritical
        ## AI-Powered Drug Repurposing Research Agent

        Ask questions about potential drug repurposing opportunities.
        The agent searches PubMed, ClinicalTrials.gov, and bioRxiv/medRxiv preprints.

        **Example questions:**
        - "What drugs could be repurposed for Alzheimer's disease?"
        - "Is metformin effective for cancer treatment?"
        - "What existing medications show promise for Long COVID?"
        """)

        # Main chat interface (existing)
        gr.ChatInterface(
            fn=research_agent,
            type="messages",
            title="",
            examples=[
                "What drugs could be repurposed for Alzheimer's disease?",
                "Is metformin effective for treating cancer?",
                "What medications show promise for Long COVID treatment?",
                "Can statins be repurposed for neurological conditions?",
            ],
            additional_inputs=[
                gr.Radio(
                    choices=["simple", "magentic"],
                    value="simple",
                    label="Orchestrator Mode",
                    info="Simple: Linear (OpenAI/Anthropic) | Magentic: Multi-Agent (OpenAI)",
                )
            ],
        )

        # MCP Tool Interfaces (exposed via MCP protocol)
        gr.Markdown("---\n## MCP Tools (Also Available via Claude Desktop)")

        with gr.Tab("PubMed Search"):
            gr.Interface(
                fn=search_pubmed,
                inputs=[
                    gr.Textbox(label="Query", placeholder="metformin alzheimer"),
                    gr.Slider(1, 50, value=10, step=1, label="Max Results"),
                ],
                outputs=gr.Markdown(label="Results"),
                api_name="search_pubmed",
            )

        with gr.Tab("Clinical Trials"):
            gr.Interface(
                fn=search_clinical_trials,
                inputs=[
                    gr.Textbox(label="Query", placeholder="diabetes phase 3"),
                    gr.Slider(1, 50, value=10, step=1, label="Max Results"),
                ],
                outputs=gr.Markdown(label="Results"),
                api_name="search_clinical_trials",
            )

        with gr.Tab("Preprints"):
            gr.Interface(
                fn=search_biorxiv,
                inputs=[
                    gr.Textbox(label="Query", placeholder="long covid treatment"),
                    gr.Slider(1, 50, value=10, step=1, label="Max Results"),
                ],
                outputs=gr.Markdown(label="Results"),
                api_name="search_biorxiv",
            )

        with gr.Tab("Search All"):
            gr.Interface(
                fn=search_all_sources,
                inputs=[
                    gr.Textbox(label="Query", placeholder="metformin cancer"),
                    gr.Slider(1, 20, value=5, step=1, label="Max Per Source"),
                ],
                outputs=gr.Markdown(label="Results"),
                api_name="search_all",
            )

        gr.Markdown("""
        ---
        **Note**: This is a research tool and should not be used for medical decisions.
        Always consult healthcare professionals for medical advice.

        Built with PydanticAI + PubMed, ClinicalTrials.gov & bioRxiv

        **MCP Server**: Available at `/gradio_api/mcp/` for Claude Desktop integration
        """)

    return demo


def main() -> None:
    """Run the Gradio app with MCP server enabled."""
    demo = create_demo()
    demo.launch(
        server_name="0.0.0.0",
        server_port=7860,
        share=False,
        mcp_server=True,  # Enable MCP server
    )


if __name__ == "__main__":
    main()
```

---

## 5. TDD Test Suite

### 5.1 Unit Tests (`tests/unit/test_mcp_tools.py`)

```python
"""Unit tests for MCP tool wrappers."""

from unittest.mock import AsyncMock, patch

import pytest

from src.mcp_tools import (
    search_all_sources,
    search_biorxiv,
    search_clinical_trials,
    search_pubmed,
)
from src.utils.models import Citation, Evidence


@pytest.fixture
def mock_evidence() -> Evidence:
    """Sample evidence for testing."""
    return Evidence(
        content="Metformin shows neuroprotective effects in preclinical models.",
        citation=Citation(
            source="pubmed",
            title="Metformin and Alzheimer's Disease",
            url="https://pubmed.ncbi.nlm.nih.gov/12345678/",
            date="2024-01-15",
            authors=["Smith J", "Jones M", "Brown K"],
        ),
        relevance=0.85,
    )


class TestSearchPubMed:
    """Tests for search_pubmed MCP tool."""

    @pytest.mark.asyncio
    async def test_returns_formatted_string(self, mock_evidence: Evidence) -> None:
        """Should return formatted markdown string."""
        with patch("src.mcp_tools._pubmed") as mock_tool:
            mock_tool.search = AsyncMock(return_value=[mock_evidence])

            result = await search_pubmed("metformin alzheimer", 10)

            assert isinstance(result, str)
            assert "PubMed Results" in result
            assert "Metformin and Alzheimer's Disease" in result
            assert "Smith J" in result

    @pytest.mark.asyncio
    async def test_clamps_max_results(self) -> None:
        """Should clamp max_results to valid range (1-50)."""
        with patch("src.mcp_tools._pubmed") as mock_tool:
            mock_tool.search = AsyncMock(return_value=[])

            # Test lower bound
            await search_pubmed("test", 0)
            mock_tool.search.assert_called_with("test", 1)

            # Test upper bound
            await search_pubmed("test", 100)
            mock_tool.search.assert_called_with("test", 50)

    @pytest.mark.asyncio
    async def test_handles_no_results(self) -> None:
        """Should return appropriate message when no results."""
        with patch("src.mcp_tools._pubmed") as mock_tool:
            mock_tool.search = AsyncMock(return_value=[])

            result = await search_pubmed("xyznonexistent", 10)

            assert "No PubMed results found" in result


class TestSearchClinicalTrials:
    """Tests for search_clinical_trials MCP tool."""

    @pytest.mark.asyncio
    async def test_returns_formatted_string(self, mock_evidence: Evidence) -> None:
        """Should return formatted markdown string."""
        mock_evidence.citation.source = "clinicaltrials"  # type: ignore

        with patch("src.mcp_tools._trials") as mock_tool:
            mock_tool.search = AsyncMock(return_value=[mock_evidence])

            result = await search_clinical_trials("diabetes", 10)

            assert isinstance(result, str)
            assert "Clinical Trials" in result


class TestSearchBiorxiv:
    """Tests for search_biorxiv MCP tool."""

    @pytest.mark.asyncio
    async def test_returns_formatted_string(self, mock_evidence: Evidence) -> None:
        """Should return formatted markdown string."""
        mock_evidence.citation.source = "biorxiv"  # type: ignore

        with patch("src.mcp_tools._biorxiv") as mock_tool:
            mock_tool.search = AsyncMock(return_value=[mock_evidence])

            result = await search_biorxiv("preprint search", 10)

            assert isinstance(result, str)
            assert "Preprint Results" in result


class TestSearchAllSources:
    """Tests for search_all_sources MCP tool."""

    @pytest.mark.asyncio
    async def test_combines_all_sources(self, mock_evidence: Evidence) -> None:
        """Should combine results from all sources."""
        with patch("src.mcp_tools.search_pubmed", new_callable=AsyncMock) as mock_pubmed, \
             patch("src.mcp_tools.search_clinical_trials", new_callable=AsyncMock) as mock_trials, \
             patch("src.mcp_tools.search_biorxiv", new_callable=AsyncMock) as mock_biorxiv:

            mock_pubmed.return_value = "## PubMed Results"
            mock_trials.return_value = "## Clinical Trials"
            mock_biorxiv.return_value = "## Preprints"

            result = await search_all_sources("metformin", 5)

            assert "Comprehensive Search" in result
            assert "PubMed" in result
            assert "Clinical Trials" in result
            assert "Preprints" in result

    @pytest.mark.asyncio
    async def test_handles_partial_failures(self) -> None:
        """Should handle partial failures gracefully."""
        with patch("src.mcp_tools.search_pubmed", new_callable=AsyncMock) as mock_pubmed, \
             patch("src.mcp_tools.search_clinical_trials", new_callable=AsyncMock) as mock_trials, \
             patch("src.mcp_tools.search_biorxiv", new_callable=AsyncMock) as mock_biorxiv:

            mock_pubmed.return_value = "## PubMed Results"
            mock_trials.side_effect = Exception("API Error")
            mock_biorxiv.return_value = "## Preprints"

            result = await search_all_sources("metformin", 5)

            # Should still contain working sources
            assert "PubMed" in result
            assert "Preprints" in result
            # Should show error for failed source
            assert "Error" in result


class TestMCPDocstrings:
    """Tests that docstrings follow MCP format."""

    def test_search_pubmed_has_args_section(self) -> None:
        """Docstring must have Args section for MCP schema generation."""
        assert search_pubmed.__doc__ is not None
        assert "Args:" in search_pubmed.__doc__
        assert "query:" in search_pubmed.__doc__
        assert "max_results:" in search_pubmed.__doc__
        assert "Returns:" in search_pubmed.__doc__

    def test_search_clinical_trials_has_args_section(self) -> None:
        """Docstring must have Args section for MCP schema generation."""
        assert search_clinical_trials.__doc__ is not None
        assert "Args:" in search_clinical_trials.__doc__

    def test_search_biorxiv_has_args_section(self) -> None:
        """Docstring must have Args section for MCP schema generation."""
        assert search_biorxiv.__doc__ is not None
        assert "Args:" in search_biorxiv.__doc__

    def test_search_all_sources_has_args_section(self) -> None:
        """Docstring must have Args section for MCP schema generation."""
        assert search_all_sources.__doc__ is not None
        assert "Args:" in search_all_sources.__doc__


class TestMCPTypeHints:
    """Tests that type hints are complete for MCP."""

    def test_search_pubmed_type_hints(self) -> None:
        """All parameters and return must have type hints."""
        import inspect

        sig = inspect.signature(search_pubmed)

        # Check parameter hints
        assert sig.parameters["query"].annotation == str
        assert sig.parameters["max_results"].annotation == int

        # Check return hint
        assert sig.return_annotation == str

    def test_search_clinical_trials_type_hints(self) -> None:
        """All parameters and return must have type hints."""
        import inspect

        sig = inspect.signature(search_clinical_trials)
        assert sig.parameters["query"].annotation == str
        assert sig.parameters["max_results"].annotation == int
        assert sig.return_annotation == str
```

### 5.2 Integration Test (`tests/integration/test_mcp_server.py`)

```python
"""Integration tests for MCP server functionality."""

import pytest


class TestMCPServerIntegration:
    """Integration tests for MCP server (requires running app)."""

    @pytest.mark.integration
    @pytest.mark.asyncio
    async def test_mcp_tools_work_end_to_end(self) -> None:
        """Test that MCP tools execute real searches."""
        from src.mcp_tools import search_pubmed

        result = await search_pubmed("metformin diabetes", 3)

        assert isinstance(result, str)
        assert "PubMed Results" in result
        # Should have actual content (not just "no results")
        assert len(result) > 100
```

---

## 6. Claude Desktop Configuration

### 6.1 Local Development

```json
// ~/.config/claude/claude_desktop_config.json (Linux/Mac)
// %APPDATA%\Claude\claude_desktop_config.json (Windows)
{
  "mcpServers": {
    "deepcritical": {
      "url": "http://localhost:7860/gradio_api/mcp/"
    }
  }
}
```

### 6.2 HuggingFace Spaces

```json
{
  "mcpServers": {
    "deepcritical": {
      "url": "https://MCP-1st-Birthday-deepcritical.hf.space/gradio_api/mcp/"
    }
  }
}
```

### 6.3 Private Spaces (with auth)

```json
{
  "mcpServers": {
    "deepcritical": {
      "url": "https://your-space.hf.space/gradio_api/mcp/",
      "headers": {
        "Authorization": "Bearer hf_xxxxxxxxxxxxx"
      }
    }
  }
}
```

---

## 7. Verification Commands

```bash
# 1. Install MCP extras
uv add "gradio[mcp]>=5.0.0"

# 2. Run unit tests
uv run pytest tests/unit/test_mcp_tools.py -v

# 3. Run full test suite
make check

# 4. Start server with MCP
uv run python src/app.py

# 5. Verify MCP schema (in another terminal)
curl http://localhost:7860/gradio_api/mcp/schema

# 6. Test with MCP Inspector
npx @anthropic/mcp-inspector http://localhost:7860/gradio_api/mcp/

# 7. Integration test (requires running server)
uv run pytest tests/integration/test_mcp_server.py -v -m integration
```

---

## 8. Definition of Done

Phase 12 is **COMPLETE** when:

- [ ] `src/mcp_tools.py` created with all 4 MCP tools
- [ ] `src/app.py` updated with `mcp_server=True`
- [ ] Unit tests in `tests/unit/test_mcp_tools.py`
- [ ] Integration test in `tests/integration/test_mcp_server.py`
- [ ] `pyproject.toml` updated with `gradio[mcp]`
- [ ] MCP schema accessible at `/gradio_api/mcp/schema`
- [ ] Claude Desktop can connect and use tools
- [ ] All unit tests pass
- [ ] Lints pass

---

## 9. Demo Script for Judges

### Show MCP Integration Works

1. **Start the server**:
   ```bash
   uv run python src/app.py
   ```

2. **Show Claude Desktop using our tools**:
   - Open Claude Desktop with DeepCritical MCP configured
   - Ask: "Search PubMed for metformin Alzheimer's"
   - Show real results appearing
   - Ask: "Now search clinical trials for the same"
   - Show combined analysis

3. **Show MCP Inspector**:
   ```bash
   npx @anthropic/mcp-inspector http://localhost:7860/gradio_api/mcp/
   ```
   - Show all 4 tools listed
   - Execute `search_pubmed` from inspector
   - Show results

---

## 10. Value Delivered

| Before | After |
|--------|-------|
| Tools only usable in our app | Tools usable by ANY MCP client |
| Not Track 2 compliant | **FULLY TRACK 2 COMPLIANT** |
| Can't use with Claude Desktop | Full Claude Desktop integration |

**Prize Impact**:
- Without MCP: **Disqualified from Track 2**
- With MCP: **Eligible for $2,500 1st place**

---

## 11. Files to Create/Modify

| File | Action | Purpose |
|------|--------|---------|
| `src/mcp_tools.py` | CREATE | MCP tool wrapper functions |
| `src/app.py` | MODIFY | Add `mcp_server=True`, add tool tabs |
| `pyproject.toml` | MODIFY | Add `gradio[mcp]>=5.0.0` |
| `tests/unit/test_mcp_tools.py` | CREATE | Unit tests for MCP tools |
| `tests/integration/test_mcp_server.py` | CREATE | Integration tests |
| `README.md` | MODIFY | Add MCP usage instructions |

---

## 12. Architecture After Phase 12

```text
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                      Claude Desktop / Cursor                   β”‚
β”‚                           (MCP Client)                         β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                              β”‚ MCP Protocol
                              β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                        Gradio MCP Server                        β”‚
β”‚                  /gradio_api/mcp/                               β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚  β”‚search_pubmed β”‚ β”‚search_trials β”‚ β”‚search_biorxivβ”‚ β”‚search_  β”‚ β”‚
β”‚  β”‚              β”‚ β”‚              β”‚ β”‚              β”‚ β”‚all      β”‚ β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”˜
          β”‚                β”‚                β”‚              β”‚
          β–Ό                β–Ό                β–Ό              β–Ό
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    (calls all)
   β”‚PubMedToolβ”‚     β”‚Trials    β”‚     β”‚BioRxiv   β”‚
   β”‚          β”‚     β”‚Tool      β”‚     β”‚Tool      β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
```

**This is the MCP compliance stack.**