File size: 28,373 Bytes
e418df7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
# HFU Multimodal Anatomy Chatbot — System Design, Evolution, and MCP Architecture

**Document type:** Thesis technical design record  
**Scope:** Planning rationale, failed/iterative approaches, final architecture, and Model Context Protocol (MCP) integration  
**Repository:** `ThesisBackend` (FastAPI + Next.js + `anatomy_mcp/`)

---

## 1. Executive summary

This project delivers a **multimodal anatomy learning assistant** with two deliberately separated capabilities:

| Mode | User intent | Knowledge source | Output |
|------|-------------|------------------|--------|
| **RAG Chatbot** | Answer textbook questions from uploaded PDFs | Chroma + BM25 + optional CLIP figure index | Grounded text, ≤3 citations, optional figures |
| **MCP 3D Anatomy** | Export and inspect a named body structure | Z-Anatomy `Startup.blend` + geometry-proven catalog | GLB + annotation JSON + Three.js viewer |

The thesis contribution is not “one model that does everything,” but a **dual-pipeline architecture** where document grounding and 3D geometry export remain **evaluated independently**. The final 3D path implements the **official Model Context Protocol** (`stdio` transport, `ClientSession.call_tool`, FastMCP tool server) rather than ad-hoc Python function calls or prompt-only LLM hallucination.

---

## 2. Original research goals

When the project started, the goals were:

1. **Grounded Q&A** — Students ask anatomy questions; answers must cite uploaded course PDFs, not unconstrained LLM knowledge.
2. **Multimodal support** — Textbook figures (diagrams, histology, radiology stills) should appear when relevant, not only text chunks.
3. **3D visualization** — When a structure is named, the system should produce inspectable 3D geometry with labels, not a static screenshot alone.
4. **Reproducible evaluation** — RAG quality (citation accuracy, abstention) and 3D export success (GLB URL, viewer load) must be measurable separately.
5. **Local-first deployment** — Run on a Windows developer machine: LM Studio, Blender 5.x, Z-Anatomy, optional MinIO.

These goals drove every architectural decision below.

---

## 3. Initial approach (v0) and its flaws

### 3.1 What we built first

The first iteration was a **monolithic FastAPI + single LLM** stack:

```
User question → Chroma similarity search → top-k chunks → LLM prompt → answer
```

Supporting pieces:

- PDF upload → text chunking → MiniLM embeddings → Chroma persist directory
- Optional local `/outputs` folder for extracted images
- A separate experimental path: **prompt Blender to procedurally generate geometry** (`src/mcp/blender_generator.py`, `BlenderMCP.generate_3d_brain`)

### 3.2 Flaws discovered in practice

| Flaw | Symptom | Root cause |
|------|---------|------------|
| **Weak retrieval** | Correct answer in PDF but wrong passage retrieved | Dense-only search; no BM25 for exact terms (e.g. “mitral valve”, “NMJ”) |
| **Citation sprawl** | 10+ reference cards; duplicates from same page | No deduplication, no max-source cap, no evidence grading |
| **Ungrounded synthesis** | Plausible anatomy not in corpus | LLM answered from parametric knowledge when retrieval was empty |
| **Figure blindness** | Questions about labeled diagrams missed images | Text-only index; figures stored but not retrieved cross-modally |
| **Fragile image URLs** | Broken images after restart or HF deploy | Local paths only; no object storage abstraction |
| **Fake 3D success** | UI showed “Exported liver” with no GLB | LLM text claimed export; no tool verification |
| **Wrong 3D source** | Procedural “brain” mesh unrelated to Z-Anatomy | `blender_generator.py` creates synthetic geometry, not curriculum-accurate labels |
| **Name-only matching** | “kidney” export failed or picked wrong collection | Raw label index lists scene names, not exportable geometry |
| **Parallel Blender corruption** | Random export failures on Windows | Multiple Blender subprocesses writing concurrently |
| **Coupled evaluation** | Could not tell if RAG or 3D failed | Single chat endpoint mixed document answers and export URLs |

These flaws motivated the phased redesign documented in Sections 4–7.

---

## 4. Approaches we tried (chronological evolution)

### 4.1 Overview timeline

```mermaid
timeline
    title Architecture evolution
    section Retrieval
        v0 Dense Chroma only : Weak on exact terms
        v1 Hybrid BM25 + dense + RRF : Better keyword + semantic balance
        v2 Evidence grading + max 3 sources : Thesis-grade grounding
    section Multimodal
        v0 Text chunks only : Missed figures
        v1 CLIP multimodal Chroma : Figure retrieval added
        v2 PDF pipeline + MinIO/Supabase : Durable public image URLs
    section 3D visualization
        v0 Procedural Blender script : Not Z-Anatomy accurate
        v1 Remote render worker (asset PNG) : RAG sidebar preview only
        v2 Direct Python anatomy calls : Worked but not MCP-standard
        v3 True MCP stdio + catalog fast path : Final production design
    section LLM
        v0 Single local model : LM Studio only
        v1 Split providers : Groq for RAG speed, LM Studio as MCP tool host
```

---

### 4.2 Approach A — Dense vector RAG only

**Design:** `VectorStoreFactory` + `similarity_search(k=...)`.

**Why we tried it:** Fastest path to a demo; LangChain/Chroma defaults.

**Result:** Failed on anatomy terminology where embedding similarity diverges from exact nomenclature (Latin names, hyphenation, “left vs right”).

**Replacement:** `HybridRetriever` in `src/retrieval/hybrid_retriever.py` — BM25 + dense + phrase matching fused with **Reciprocal Rank Fusion (RRF)**.

---

### 4.3 Approach B — Multimodal RAG (CLIP + text)

**Design:** `MultimodalRAG` combines:

- Text corpus (MiniLM Chroma, same as ingestion)
- Multimodal retriever (`MultimodalRetriever` + `CLIPEmbedding`)
- Up to 3 unique text passages (`_MAX_UNIQUE_TEXT_PASSAGES = 3`)

**Why we tried it:** Anatomy learning is inherently visual; students ask about figures explicitly.

**Improvements over A:**

- Retrieves figure metadata and serves images from `/outputs` or presigned storage
- Dedupes by PDF stem + page + text fingerprint

**Remaining gap:** Still no 3D export; images are 2D textbook figures only.

---

### 4.4 Approach C — Strict grounding & thesis evaluation pipeline

**Design:** `src/multimodal/thesis_rag_eval.py` — parallel experiment track:

- **Baseline** — LLM without retrieval (measures hallucination rate)
- **Strict RAG** — answer only from numbered passages
- **Coherent synthesis** — stricter prompt with citation IDs
- **Blind judge** — compares baseline vs strict

**Why we tried it:** Quantify *how much* retrieval helps for thesis evaluation (`POST /rag/experiment/ask`).

**Lesson:** Production `POST /rag/ask` and experiment pipeline stay separate so tuning does not break live UI.

---

### 4.5 Approach D — Procedural Blender generation (`BlenderMCP`)

**Design:** `src/mcp/blender_mcp.py` + `blender_generator.py`

```
LLM prompt → blender --background --python blender_generator.py → synthetic GLB
```

**Why we tried it:** Quick 3D “something” without licensing a full anatomy asset library.

**Flaws:**

- Geometry is **not** Z-Anatomy; labels do not match course material
- No annotation JSON sidecar
- `POST /blender/generate-brain-3d` is a research stub, not curriculum export

**Status:** Kept for optional brain demo; **not** the MCP catalog path.

---

### 4.6 Approach E — Remote Blender render worker (RAG adjunct)

**Design:** `app/services/blender_service.py``render_related_anatomy()`

```
POST /rag/ask → detect keyword (brain, heart, spine…) → POST {BLENDER_SERVER_URL}/render-asset → PNG URL
```

**Why we tried it:** Attach a **related 3D preview** to RAG answers without blocking on full GLB export latency.

**Characteristics:**

- Fixed asset map (`ASSET_MAPPING`: brain, heart, hand, lung, spine)
- Worker uploads to object storage; backend stores **no local render files**
- Returns `render_3d_url` in RAG response (optional, best-effort)

**Flaws:**

- Only ~5 pre-authored assets; no arbitrary “left femur” from user query
- Docker `blender` service in `docker-compose.yml` is a **different** worker from Z-Anatomy MCP export
- Must not be confused with MCP panel exports in evaluation

**Status:** Optional RAG enhancement; orthogonal to MCP.

---

### 4.7 Approach F — Label index without exportable catalog

**Design:** Scan Z-Anatomy once → `z_anatomy_index.json` (all collection/object names in scene).

**Why we tried it:** First automated bridge from natural language to Blender object names.

**Flaws:**

- Index proves **name exists**, not that mesh is exportable
- Broad queries (`kidney`, `iris`) hit ambiguous or empty collections
- Exports sometimes succeeded with **huge** selections (entire limb regions)

**Replacement:** `build_exportable_catalog.py``exportable_catalog.json` — entries validated by geometry probe in Blender.

---

### 4.8 Approach G — Direct in-process Python calls (pre-MCP)

**Design:** Import `anatomy_mcp/server.py` functions directly from FastAPI handlers.

**Why we tried it:** Fastest integration before MCP SDK maturity on Windows.

**Flaws:**

- Not interoperable with LM Studio, Claude Desktop, or MCP Inspector
- No standard tool discovery / schema contract
- LLM could not participate in multi-step tool loops using a portable protocol
- Harder to sandbox Blender side effects

**Replacement:** True MCP stdio server + `MCPBridge` client (Approach H).

---

### 4.9 Approach H — True MCP with dual execution paths (FINAL)

**Design:** See Section 6–7. This is the production 3D architecture.

---

### 4.10 Storage approaches tried

| Approach | Mechanism | When used | Flaw / fix |
|----------|-----------|-----------|------------|
| Local `outputs/` | FastAPI static mount | Earliest dev | URLs break on multi-replica / HF Spaces |
| MinIO | S3-compatible Docker (`docker-compose.yml`) | Local full stack | Requires Docker; good for dev |
| Supabase Storage | `STORAGE_PROVIDER=supabase` | Production / HF | Needs service role key; auto-selected when env set |

Image pipeline steps are observable via `GET /debug/storage` and upload response `pipeline` array.

---

### 4.11 LLM provider approaches

| Role | Initial | Final |
|------|---------|-------|
| RAG synthesis | LM Studio local | **Groq** (`llama-3.1-8b-instant`) for latency |
| MCP tool host | N/A | **LM Studio** OpenAI-compatible API at `:1234/v1` |
| Evidence grading / judge | Groq | Groq (thesis eval) |

**Rationale for split:** RAG needs fast repeated calls; MCP tool loop needs a model with reliable **function calling** locally without cloud cost for Blender waits.

---

## 5. Final architecture (what we reached)

### 5.1 System context

```mermaid
flowchart TB
    subgraph User["User / Evaluator"]
        U1[Browser]
    end

    subgraph Frontend["Next.js Frontend :3000"]
        Chat[Main Chat UI]
        Upload[Upload Panel]
        MCPPanel[MCP 3D Panel + iframe viewer]
    end

    subgraph Backend["FastAPI Backend :8000"]
        RAGRoute["/rag/ask"]
        UploadRoute["/upload-pdf/"]
        MCPRoute["/anatomy/ask"]
        RAGSvc[rag_service]
        RAGEngine[MultimodalRAG]
        MCPChat[anatomy_mcp_chat]
        MCPBridge[MCPBridge stdio client]
    end

    subgraph RAGData["Document knowledge"]
        Chroma[(Chroma text index)]
        BM25[(BM25 pickle)]
        MMChroma[(Multimodal Chroma)]
        MinIO[(MinIO / Supabase / local outputs)]
    end

    subgraph MCPData["3D anatomy knowledge"]
        Catalog[(exportable_catalog.json)]
        Blend[Z-Anatomy Startup.blend]
        Exports[/anatomy-exports GLB + JSON/]
        Viewer[/anatomy-viewer Three.js/]
    end

    subgraph External["External runtimes"]
        Groq[Groq API — RAG LLM]
        LMStudio[LM Studio — MCP host LLM]
        Blender[Blender 5.x headless]
    end

    subgraph MCPServer["anatomy_mcp/server.py — stdio MCP"]
        SearchTool[search_anatomy_catalog]
        ExportPart[export_anatomy_part]
        ExportPkg[export_anatomy_package]
    end

    U1 --> Chat & Upload & MCPPanel
    Chat -->|POST question| RAGRoute
    Upload -->|POST PDF| UploadRoute
    MCPPanel -->|POST message| MCPRoute

    RAGRoute --> RAGSvc --> RAGEngine
    RAGEngine --> Chroma & BM25 & MMChroma
    RAGEngine --> Groq
    UploadRoute --> Chroma & MMChroma & MinIO
    RAGEngine --> MinIO
    RAGSvc -->|optional| RenderWorker[Remote render worker]

    MCPRoute --> MCPChat --> MCPBridge
    MCPChat -->|fast path or tool loop| LMStudio
    MCPBridge <-->|stdio JSON-RPC| MCPServer
    SearchTool --> Catalog
    ExportPart & ExportPkg --> Blender
    Blender --> Blend
    ExportPart & ExportPkg --> Exports
    MCPPanel -->|iframe src=viewer_url| Viewer
    Viewer --> Exports
```

### 5.2 Separation of concerns (evaluation rule)

```mermaid
flowchart LR
    subgraph RAGPath["Mode A — RAG Chat"]
        Q1[Textbook question]
        A1[Grounded answer + PDF citations]
    end

    subgraph MCPPath["Mode B — MCP 3D"]
        Q2[Structure name e.g. left femur]
        A2[GLB + annotations + viewer]
    end

    Q1 -.->|must NOT| A2
    Q2 -.->|must NOT| A1

    style RAGPath fill:#e8f4fc,stroke:#2563eb
    style MCPPath fill:#ecfdf5,stroke:#059669
```

**Thesis rule:** Never score a RAG answer on GLB export success; never score an MCP export on PDF citation quality.

---

## 6. MCP deep dive — protocol, components, and guarantees

### 6.1 What “proper MCP” means in this project

The implementation satisfies the MCP contract at three layers:

| Layer | Component | Responsibility |
|-------|-----------|----------------|
| **Tool server** | `anatomy_mcp/server.py` | FastMCP registers tools; runs Blender; returns structured JSON |
| **Transport** | `stdio` via `mcp.client.stdio` | Parent process spawns server; no HTTP port on MCP server |
| **Host orchestration** | `app/services/anatomy_mcp_chat.py` | LM Studio chooses tools; backend executes via `ClientSession.call_tool` |

**Non-MCP patterns explicitly rejected:**

- LLM printing `http://.../anatomy.glb` without calling a tool
- Direct Python import of export functions from FastAPI routes
- Hard-coded export in frontend

### 6.2 MCP layer diagram

```mermaid
flowchart TB
    subgraph Presentation["Presentation layer"]
        UI[AnatomyMcpPanel.js]
        Viewer[Three.js viewer iframe]
    end

    subgraph API["FastAPI REST layer"]
        Ask["POST /anatomy/ask"]
        Health["GET /anatomy/health"]
        Static["/anatomy-exports /anatomy-viewer"]
    end

    subgraph Orchestration["MCP host orchestration"]
        Agent[run_lmstudio_mcp_agent]
        FastPath[_try_catalog_fast_path]
        ToolLoop[LM Studio tool loop max 6 rounds]
    end

    subgraph Client["MCP client — MCPBridge"]
        Session[ClientSession]
        ListTools[list_tools → OpenAI schema]
        CallTool[call_tool]
        StdioTransport[stdio_client]
    end

    subgraph Server["MCP server — FastMCP"]
        T1[search_anatomy_catalog]
        T2[export_anatomy_part]
        T3[export_anatomy_package]
        Lock[_BLENDER_EXPORT_LOCK]
        Resolver[exportable_catalog resolver]
    end

    subgraph Execution["Execution layer"]
        BlenderCLI[Blender subprocess]
        ZAnatomy[Startup.blend]
        Cache[exports/cache v4 schema]
    end

    UI --> Ask
    Ask --> Agent
    Agent --> FastPath
    Agent --> ToolLoop
    FastPath --> CallTool
    ToolLoop --> ListTools
    ToolLoop --> CallTool
    CallTool --> StdioTransport --> Session
    Session --> T1 & T2 & T3
    T2 & T3 --> Lock --> BlenderCLI --> ZAnatomy
    T2 & T3 --> Cache
    Static --> Viewer
    UI --> Viewer
```

### 6.3 MCP tool catalog

| Tool | Input | Output | When used |
|------|-------|--------|-----------|
| `search_anatomy_catalog` | `query`, `limit` | Ranked catalog entries, suggestions, ambiguity errors | Every export path starts here or equivalent resolution |
| `export_anatomy_part` | `part_query`, `include_preview` | `model_url`, `annotations_url`, `viewer_url`, metadata | Single structure (e.g. `Femur.l`, `Liver`) |
| `export_anatomy_package` | `part_query`, options | Study package with subparts for large regions | Brain, thalamus; fallback after part export timeout |

**Safety boundaries** (from `anatomy_mcp/README.md`):

- No arbitrary `.blend` paths from user input
- No arbitrary Python injection
- No full-scene export by default
- Serialized Blender lock — one export at a time on Windows

### 6.4 Catalog resolution pipeline

```mermaid
flowchart TD
    Input[User message e.g. left femur]
    Validate{Query valid?}
    Vague{Vague query?}
    Normalize[catalog_query_from_user_message]
    Search[search_anatomy_catalog]
    Match{Exact exportable match?}
    Ambiguous{Multiple matches?}
    Export[export_anatomy_part]
    Package[export_anatomy_package]
    Success[anatomy_export status ok]
    Clarify[Clarification message]
    Suggest[Return suggestion labels]

    Input --> Validate
    Validate -->|invalid chars / too long| Clarify
    Validate --> Vague
    Vague -->|show me the organ| Clarify
    Vague --> Normalize --> Search
    Search --> Match
    Match -->|yes Femur.l| Export
    Match -->|no| Ambiguous
    Ambiguous -->|kidney| Suggest
    Export -->|timeout| Package
    Export --> Success
    Package --> Success
```

**Key data artifact:** `exportable_catalog.json` — only entries with proven mesh geometry. This replaced naive `z_anatomy_index.json` matching.

### 6.5 MCP sequence — fast path (plain queries)

Used when input matches `looks_like_plain_anatomy_query` (≤80 chars, alphanumeric + space/dot/dash).

```mermaid
sequenceDiagram
    autonumber
    actor User
    participant UI as AnatomyMcpPanel
    participant API as POST /anatomy/ask
    participant Chat as anatomy_mcp_chat
    participant Bridge as MCPBridge
    participant Srv as anatomy_mcp/server.py
    participant Cat as exportable_catalog.json
    participant B as Blender
    participant V as anatomy-viewer iframe

    User->>UI: "left femur"
    UI->>API: { message, language }
    API->>Chat: run_lmstudio_mcp_agent()

    Note over Chat: Fast path — no LM Studio round-trip

    Chat->>Bridge: call_tool search_anatomy_catalog
    Bridge->>Srv: MCP CallTool
    Srv->>Cat: token + lateral resolution
    Srv-->>Chat: Femur.l match

    Chat->>Bridge: call_tool export_anatomy_part
    Bridge->>Srv: MCP CallTool
    Srv->>B: subprocess export_part.py
    B-->>Srv: anatomy.glb + annotations.json
    Srv-->>Chat: model_url, viewer_url

    Chat->>Chat: structured_to_anatomy_export
    Note over Chat: Fail if model_url missing — no fake success

    Chat-->>API: answer + anatomy_export + mcp_tools_used
    API-->>UI: JSON 200
    UI->>V: iframe src = viewer_url
    V-->>User: labeled 3D mesh
```

**Why fast path exists:** Blender exports take 30–120s. Skipping LLM orchestration for `"Femur.l"`-style queries reduces failure modes and token cost.

### 6.6 MCP sequence — LLM tool loop (complex queries)

```mermaid
sequenceDiagram
    autonumber
    participant Chat as anatomy_mcp_chat
    participant LM as LM Studio LLM
    participant Bridge as MCPBridge
    participant Srv as MCP server

    Chat->>Bridge: get_openai_tools()
    Bridge->>Srv: list_tools
    Srv-->>Bridge: tool schemas
    Bridge-->>Chat: OpenAI function definitions

    loop Up to MAX_TOOL_ROUNDS = 6
        Chat->>LM: messages + tools + tool_choice auto
        LM-->>Chat: assistant message + tool_calls
        alt tool_calls present
            Chat->>Bridge: call_tool(name, args)
            Bridge->>Srv: MCP CallTool
            Srv-->>Bridge: structured JSON result
            Bridge-->>Chat: compact_tool_result_for_llm
            Chat->>Chat: append tool result to messages
        else no tool_calls
            Chat->>Chat: verify export or require tools
        end
    end

    Chat->>Chat: structured_to_anatomy_export
    alt model_url present
        Chat-->>Chat: status ok + mcp_tools_used logged
    else export error
        Chat-->>Chat: status error — UI shows failure, no empty iframe
    end
```

**Host prompt constraints** (`build_mcp_system_prompt` in `app/i18n/locale.py`):

- Must call `export_anatomy_part` or `export_anatomy_package` before claiming success
- Must not echo raw package manifest URLs to user
- Must ask clarification on vague queries
- Retry package export after part export timeout

### 6.7 How the final approach **ensures** MCP integrity

| Guarantee | Mechanism | Code reference |
|-----------|-----------|----------------|
| **Real tool discovery** | `session.list_tools()` → OpenAI schemas | `MCPBridge.get_openai_tools()` |
| **Real tool execution** | `session.call_tool(name, arguments)` | `MCPBridge.call_tool()` |
| **Stdio isolation** | Separate Python process for `server.py` | `StdioServerParameters` |
| **Structured tool results** | `structuredContent` + text fallback parsing | `extract_structured_from_tool_payload()` |
| **No hallucinated exports** | `structured_to_anatomy_export` returns `null` without `model_url` | `anatomy_mcp_client.py` |
| **UI honesty** | Frontend checks `anatomy_export.status === "ok"` before iframe | `AnatomyMcpPanel.js` |
| **Audit trail** | Response includes `mcp_tools_used`, `mcp_tool_steps` | `_agent_result()` |
| **URL rewriting** | Dev server `:8123` → public API base | `rewrite_local_urls()` |
| **Concurrency safety** | `threading.RLock` around Blender | `anatomy_mcp/server.py` |
| **Health probe** | `GET /anatomy/health` checks Blender, catalog, MCP import | `anatomy_mcp_health()` |

```mermaid
flowchart TD
    Claim[LLM says Exported liver]
    Tools{mcp_tools_used includes export tool?}
    URL{model_url HTTP 200?}
    OK[Show viewer + GLB links]
    Fail[Show error panel]

    Claim --> Tools
    Tools -->|no| Fail
    Tools -->|yes| URL
    URL -->|no| Fail
    URL -->|yes| OK
```

---

## 7. Final approach — what we implemented in the last iteration

### 7.1 Backend wiring (`app/main.py`)

Routers and static mounts required for MCP UI:

- `app.include_router(anatomy_mcp.router)``/anatomy/*`
- `app.mount("/anatomy-exports", ...)` → GLB, packages, annotations
- `app.mount("/anatomy-viewer", ...)` → Three.js viewer assets

### 7.2 Settings contract (`src/config/settings.py`)

Environment-driven configuration for MCP:

- `PUBLIC_API_BASE` — URL prefix for exported assets
- `BLENDER_BIN`, `Z_ANATOMY_BLEND` — passed into MCP server env
- `LLM_API_BASE` — LM Studio for tool host
- `ANATOMY_MCP_ENABLED` — feature gate

### 7.3 Frontend MCP panel

`frontend/app/components/AnatomyMcpPanel.js`:

- Calls `POST /anatomy/ask` (not `/rag/ask`)
- Renders `AnatomyExportPanel` with viewer link, GLB download
- Salvage path parses URLs from verbose LLM dumps (legacy compatibility)
- i18n EN/DE strings for errors including “restart backend if 404”

### 7.4 Caching & performance

- Export cache schema **v4** under `anatomy_mcp/exports/cache/`
- Repeated `Femur.l` queries hit cache → near-instant URLs
- Package exports for large structures avoid timeout on single-part mesh

### 7.5 RAG path (unchanged responsibility)

`POST /rag/ask` still uses `MultimodalRAG` + optional `render_3d_url` from remote worker — **does not** invoke MCP tools by default. This preserves evaluation separation.

---

## 8. RAG pipeline reference (Mode A detail)

```mermaid
sequenceDiagram
    autonumber
    actor User
    participant UI as Next.js Chat
    participant API as POST /rag/ask
    participant RAG as MultimodalRAG
    participant IDX as Chroma + BM25 + Hybrid
    participant LLM as Groq

    User->>UI: Question + optional PDF context
    UI->>API: { question, allow_world_knowledge, language }
    API->>RAG: ask()

    RAG->>RAG: classify + typo correct + rewrite
    RAG->>IDX: hybrid retrieve
    RAG->>RAG: rerank + dedupe passages
    RAG->>LLM: evidence grading A/B/C
    RAG->>LLM: grounded synthesis
    RAG->>RAG: normalize citations, max 3 sources
    RAG-->>API: answer, sources, images, grounding
    API-->>UI: AskResponse
```

---

## 9. Comparison matrix (for thesis discussion section)

| Dimension | Initial v0 | Final system |
|-----------|------------|--------------|
| Retrieval | Dense only | Hybrid BM25 + dense + RRF + phrases |
| Grounding | Prompt-only | Evidence grading + abstention + max 3 sources |
| Figures | Ignored | CLIP multimodal index + storage URLs |
| 3D source | Procedural script | Z-Anatomy catalog + Blender export |
| 3D protocol | Direct Python / fake LLM text | MCP stdio + tool audit trail |
| LLM | Single local | Groq (RAG) + LM Studio (MCP host) |
| Storage | Local folder | MinIO / Supabase abstraction |
| Evaluation | Mixed | Separated RAG vs MCP metrics |
| Export correctness | Name in index | Geometry-proven catalog |
| Parallelism | Unsafe | Blender export lock |

---

## 10. Known limitations & future work

1. **LM Studio dependency for complex MCP queries** — Fast path avoids it; natural-language disambiguation still needs tool-capable local model.
2. **Windows-only Blender path** — Linux/Mac require different Blender binary paths.
3. **Export latency** — Large packages (brain) may exceed user patience; cache mitigates repeat queries.
4. **RAG render worker** — Only five asset keys; not a general anatomy renderer.
5. **MCP + RAG fusion** — Deliberately not merged; future work could *link* citation text to MCP viewer via shared entity IDs without mixing retrieval corpora.

---

## 11. Verification checklist

### RAG (Mode A)

```powershell
curl -X POST http://127.0.0.1:8000/rag/ask `
  -H "Content-Type: application/json" `
  -d '{"question":"What is the function of the mitral valve?","allow_world_knowledge":false,"language":"en"}'
```

Pass: answer cites PDF sources; no GLB URLs.

### MCP (Mode B)

```powershell
curl -X POST http://127.0.0.1:8000/anatomy/ask `
  -H "Content-Type: application/json" `
  -d '{"message":"left femur","language":"en"}'
```

Pass: `anatomy_export.status == "ok"`, `mcp_tools_used` includes `export_anatomy_part`, viewer URL loads.

### MCP health

```powershell
Invoke-RestMethod http://127.0.0.1:8000/anatomy/health
```

Pass: `ready: true`, `mcp_stdio_ok: true`, `blender_exists: true`, `exportable_catalog_exists: true`.

---

## 12. Key file index

| Path | Role |
|------|------|
| `app/main.py` | FastAPI entry, routers, static mounts |
| `app/services/rag_service.py` | RAG + optional render worker |
| `app/services/anatomy_mcp_chat.py` | MCP host agent (fast path + tool loop) |
| `app/services/anatomy_mcp_client.py` | MCPBridge stdio client |
| `app/services/anatomy_mcp_service.py` | Health checks, env configuration |
| `anatomy_mcp/server.py` | FastMCP tool server |
| `anatomy_mcp/label_index/exportable_catalog.json` | Geometry-validated catalog |
| `anatomy_mcp/viewer/` | Three.js annotation viewer |
| `src/multimodal/multimodal_rag_chain.py` | MultimodalRAG core |
| `src/retrieval/hybrid_retriever.py` | BM25 + dense hybrid |
| `frontend/app/components/AnatomyMcpPanel.js` | MCP UI |
| `docs/ARCHITECTURE_FOR_DIAGRAMS.md` | Diagram source pack |

---

## 13. Conclusion

The project evolved from a **single-path RAG demo** into a **dual-mode architecture** grounded in evaluation needs:

- **Documents** are handled by retrieval-augmented generation with strict citation limits.
- **Geometry** is handled by a **standards-based MCP toolchain** connecting an LLM host, a stdio tool server, Blender, and a web viewer.

The final MCP design is not “MCP-themed naming” — it implements discoverable tools, stdio transport, structured tool results, and verifiable export URLs. The **fast path** optimizes common classroom queries without bypassing MCP (`call_tool` is still used); the **LLM tool loop** handles ambiguous natural language while respecting the same server contract.

This document should be cited alongside `docs/ARCHITECTURE_FOR_DIAGRAMS.md` when generating thesis figures: use **blue** styling for RAG paths and **green** styling for MCP paths in all diagrams.

---

*Generated from repository state and architecture records. Update when new approaches are tried or MCP tools change.*