Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

apps/mcp-widgets-server/.env
.claude/
# dependencies
node_modules
.pnp
Expand Down
33 changes: 33 additions & 0 deletions apps/mcp-widgets-server/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Build context is the monorepo root.
# Paths here are relative to the repo root.

# Dependencies (reinstalled inside the image)
node_modules
**/node_modules

# Other apps — only mcp-widgets-server is needed
apps/platform/
apps/platform-test/

# Package sources not needed at server runtime
packages/ui/src
packages/ot-config/src
packages/ot-constants/src
packages/ot-utils/src
packages/tsconfig/

# Widget build artifacts that are not the pre-built bundles
apps/mcp-widgets-server/widget-src/
apps/mcp-widgets-server/vite/
apps/mcp-widgets-server/scripts/
apps/mcp-widgets-server/certs/

# Misc
**/.DS_Store
**/*.swp
**/.env
**/.env.local
**/npm-debug.log*
**/yarn-debug.log*
**/yarn-error.log*
.git/
2 changes: 2 additions & 0 deletions apps/mcp-widgets-server/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# GraphQL endpoint for data prefetch (server-side) and widget runtime (browser-side)
OT_API_URL=https://dev.opentargets.xyz/api/v4/graphql
2 changes: 2 additions & 0 deletions apps/mcp-widgets-server/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.env
dist/
52 changes: 52 additions & 0 deletions apps/mcp-widgets-server/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# ── Stage 1: Install dependencies ─────────────────────────────────────────────
# Yarn v1 workspaces require all workspace package.json files to be present
# so the lockfile resolves correctly. We copy only manifests here so that
# dependency installation is cached independently of source changes.
FROM node:22-alpine AS deps

WORKDIR /app

COPY package.json yarn.lock ./

# Workspace package manifests (source not needed — server doesn't import them at runtime)
COPY apps/mcp-widgets-server/package.json apps/mcp-widgets-server/
COPY packages/ui/package.json packages/ui/
COPY packages/sections/package.json packages/sections/
COPY packages/ot-config/package.json packages/ot-config/
COPY packages/ot-constants/package.json packages/ot-constants/
COPY packages/ot-utils/package.json packages/ot-utils/
COPY packages/tsconfig/package.json packages/tsconfig/
COPY packages/platform-test/package.json packages/platform-test/

# Install all deps (including devDeps — tsx is needed at runtime)
RUN yarn install --frozen-lockfile --ignore-scripts

# ── Stage 2: Runtime ──────────────────────────────────────────────────────────
FROM node:22-alpine

WORKDIR /app

# Bring in installed node_modules from the deps stage
COPY --from=deps /app/node_modules ./node_modules

# Server TypeScript source
COPY apps/mcp-widgets-server/src apps/mcp-widgets-server/src

# Pre-built widget IIFE bundles (built locally, committed to the repo)
COPY apps/mcp-widgets-server/dist apps/mcp-widgets-server/dist

# GQL query files — read at startup by loadSectionQuery to auto-detect operation names.
# Only .gql files are needed; .tsx source is not used at server runtime.
COPY packages/sections/src packages/sections/src

# Profile config scripts — read per-request by makeWidgetShell (OT_PROFILE env var
# picks "platform" or "ppp") and inlined into every widget's HTML shell.
COPY apps/platform/public/profiles apps/platform/public/profiles

# Cloud Run injects PORT (default 8080). OT_API_URL can be overridden at deploy time.
ENV PORT=8080
ENV NODE_ENV=production

EXPOSE 8080

CMD ["node_modules/.bin/tsx", "apps/mcp-widgets-server/src/index.ts"]
193 changes: 193 additions & 0 deletions apps/mcp-widgets-server/WIDGETS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
# MCP Widgets Server — Architecture & Widget Registry

## Directory Structure

```
apps/mcp-widgets-server/
├── src/ # Server-side Node.js code (runs in Claude Desktop / MCP host)
│ ├── sections/
│ │ └── registry.ts # Single source of truth: all section widget definitions
│ ├── widgets/
│ │ ├── index.ts # Widget registry builder + sectionPathToId utility
│ │ ├── molecular-structure.ts # Manual widget def (AlphaFold 3D viewer)
│ │ └── types.ts # WidgetDef type
│ ├── mcp-server.ts # MCP tool + resource registration, widget HTML shell, CSP
│ ├── index.ts # Server entrypoint
│ └── stdio.ts # stdio MCP transport
├── widget-src/ # Browser-side React code (compiled to IIFE bundles)
│ ├── molecular-structure/ # Custom 3D viewer React app (only custom widget entry)
│ └── shared/
│ ├── createWidgetEntry.tsx # mountWidget() — React root bootstrapper for all widgets
│ └── stubs/
│ ├── ui-index.tsx # Stub replacements for @ot/ui barrel (used by all section widgets)
│ └── ui-ms-index.tsx # Stub/real mix for molecular-structure widget (includes real Viewer providers)
└── vite/ # Build tooling
├── widget.config.base.ts # Shared Vite config factory + Vite plugins
├── section-widget.plugin.ts # Auto-generates entry code + Vite config for section widgets
└── widget.config.ms.ts # Custom Vite config for molecular-structure widget
```

### `src/` vs `widget-src/` — the key distinction

| | `src/` | `widget-src/` |
|---|---|---|
| **Runs in** | Node.js (server / MCP host) | Browser (iframe inside Claude Desktop) |
| **Purpose** | Registers MCP tools, serves the widget HTML shell + CSP allowlist | React components compiled into self-contained IIFE bundles |
| **Output** | Running MCP server process | `dist/widgets/*.js` IIFE bundle files |
| **Build tool** | `tsx` (TypeScript execution) | Vite (browser bundle) |

When Claude calls an MCP tool, `src/mcp-server.ts` serves the widget HTML shell (bundle inlined)
as an MCP App resource, with `_meta.ui.csp.connectDomains` allowlisting the GraphQL API origin
(and any extra origins the widget needs, via `WidgetDef.extraConnectDomains`). The widget's own
Apollo client then fetches data directly from the iframe — there is no server-side prefetch.

---

## How to add a new section widget

1. Add an entry to `src/sections/registry.ts`
2. Run `yarn build:widgets:<entity>` (or `yarn build:widgets` for all)
3. Restart Claude Desktop

If the section fetches anything beyond the OT GraphQL API (e.g. a third-party CDN), set
`extraConnectDomains` on the `SectionDef` entry itself (see `target/Bibliography`,
`evidence/EuropePmc`, `target/SubcellularLocation`, or `target/MolecularStructure` for examples).

If the section imports a package directly rather than through a shared `ui` wrapper (e.g. `3dmol`),
set `extraDedupe` too — see `target/MolecularStructure`. Before adding a new section, check it for
components that import platform modules (`Link`, `useConfigContext`, etc.) via a **relative path**
rather than the `"ui"` barrel — those bypass the widget stubs in `widget-src/shared/stubs/` and need
handling at the Vite-plugin level (`createPlatformStubsPlugin` in `vite/widget.config.base.ts`), not
just a barrel re-export. This bit `PublicationWrapper`/`ConfigurationProvider`/`Link` and
`OtAsyncTooltip`/`OtGenomicLocation` during the initial migration — grep the section's files for
`^import` and check anything not going through `"ui"`.

---

## Included section widgets (69 total)

### Target (18 sections)

| Tool name | Section | Input | Notes |
|-----------|---------|-------|-------|
| `get_target_cancer_hallmarks_widget` | `target/CancerHallmarks` | `ensemblId` | |
| `get_target_chemical_probes_widget` | `target/ChemicalProbes` | `ensemblId` | |
| `get_target_comparative_genomics_widget` | `target/ComparativeGenomics` | `ensemblId` | |
| `get_target_depmap_widget` | `target/DepMap` | `ensemblId` | |
| `get_target_drugs_widget` | `target/Drugs` | `ensemblId` | |
| `get_target_baseline_expression_widget` | `target/BaselineExpression` | `ensemblId` | |
| `get_target_bibliography_widget` | `target/Bibliography` | `ensemblId` | |
| `get_target_gene_ontology_widget` | `target/GeneOntology` | `ensemblId` | |
| `get_target_genetic_constraint_widget` | `target/GeneticConstraint` | `ensemblId` | |
| `get_target_molecular_interactions_widget` | `target/MolecularInteractions` | `ensemblId` | |
| `get_target_molecular_structure_widget` | `target/MolecularStructure` | `ensemblId` | Experimental PDB + AlphaFold viewer; raw `3dmol` usage (`extraDedupe`) and 3 extra CSP domains (UniProt, AlphaFold, PDBe) — distinct from the manual `variant/MolecularStructure` widget |
| `get_target_mouse_phenotypes_widget` | `target/MousePhenotypes` | `ensemblId` | |
| `get_target_pathways_widget` | `target/Pathways` | `ensemblId` | |
| `get_target_pharmacogenomics_widget` | `target/Pharmacogenomics` | `ensemblId` | |
| `get_target_qtl_credible_sets_widget` | `target/QTLCredibleSets` | `ensemblId` | |
| `get_target_safety_widget` | `target/Safety` | `ensemblId` | |
| `get_target_subcellular_location_widget` | `target/SubcellularLocation` | `ensemblId` | |
| `get_target_tractability_widget` | `target/Tractability` | `ensemblId` | |

### Disease (6 sections)

| Tool name | Section | Input |
|-----------|---------|-------|
| `get_disease_drugs_widget` | `disease/Drugs` | `efoId` |
| `get_disease_bibliography_widget` | `disease/Bibliography` | `efoId` |
| `get_disease_gwas_studies_widget` | `disease/GWASStudies` | `efoId` |
| `get_disease_ot_projects_widget` | `disease/OTProjects` | `efoId` |
| `get_disease_ontology_widget` | `disease/Ontology` | `efoId` |
| `get_disease_phenotypes_widget` | `disease/Phenotypes` | `efoId` |

### Drug (6 sections)

| Tool name | Section | Input | Notes |
|-----------|---------|-------|-------|
| `get_drug_adverse_events_widget` | `drug/AdverseEvents` | `chemblId` | |
| `get_drug_bibliography_widget` | `drug/Bibliography` | `chemblId` | |
| `get_drug_indications_widget` | `drug/Indications` | `chemblId` | Two-query: indications + `ClinicalRecordsQuery` (filtered on row click) |
| `get_drug_warnings_widget` | `drug/DrugWarnings` | `chemblId` | |
| `get_drug_mechanisms_of_action_widget` | `drug/MechanismsOfAction` | `chemblId` | |
| `get_drug_pharmacogenomics_widget` | `drug/Pharmacogenomics` | `chemblId` | |

### Evidence (23 sections)

| Tool name | Section | Input | Notes |
|-----------|---------|-------|-------|
| `get_evidence_crispr_widget` | `evidence/CRISPR` | `ensemblId` + `efoId` | |
| `get_evidence_crispr_screen_widget` | `evidence/CRISPRScreen` | `ensemblId` + `efoId` | |
| `get_evidence_cancer_biomarkers_widget` | `evidence/CancerBiomarkers` | `ensemblId` + `efoId` | |
| `get_evidence_cancer_gene_census_widget` | `evidence/CancerGeneCensus` | `ensemblId` + `efoId` | |
| `get_evidence_chembl_widget` | `evidence/Chembl` | `ensemblId` + `efoId` | |
| `get_evidence_clingen_widget` | `evidence/ClinGen` | `ensemblId` + `efoId` | |
| `get_evidence_eva_widget` | `evidence/EVA` | `ensemblId` + `efoId` | |
| `get_evidence_eva_somatic_widget` | `evidence/EVASomatic` | `ensemblId` + `efoId` | |
| `get_evidence_europe_pmc_widget` | `evidence/EuropePmc` | `ensemblId` + `efoId` | Fetches publication details directly from EuropePMC (extra CSP `connectDomains`) |
| `get_evidence_expression_atlas_widget` | `evidence/ExpressionAtlas` | `ensemblId` + `efoId` | |
| `get_evidence_gwas_credible_sets_widget` | `evidence/GWASCredibleSets` | `ensemblId` + `efoId` | |
| `get_evidence_gene2phenotype_widget` | `evidence/Gene2Phenotype` | `ensemblId` + `efoId` | |
| `get_evidence_gene_burden_widget` | `evidence/GeneBurden` | `ensemblId` + `efoId` | |
| `get_evidence_genomics_england_widget` | `evidence/GenomicsEngland` | `ensemblId` + `efoId` | |
| `get_evidence_impc_widget` | `evidence/Impc` | `ensemblId` + `efoId` | |
| `get_evidence_intogen_widget` | `evidence/IntOgen` | `ensemblId` + `efoId` | |
| `get_evidence_ot_crispr_widget` | `evidence/OTCRISPR` | `ensemblId` + `efoId` | |
| `get_evidence_ot_encore_widget` | `evidence/OTEncore` | `ensemblId` + `efoId` | |
| `get_evidence_ot_validation_widget` | `evidence/OTValidation` | `ensemblId` + `efoId` | |
| `get_evidence_orphanet_widget` | `evidence/Orphanet` | `ensemblId` + `efoId` | |
| `get_evidence_reactome_widget` | `evidence/Reactome` | `ensemblId` + `efoId` | |
| `get_evidence_uniprot_literature_widget` | `evidence/UniProtLiterature` | `ensemblId` + `efoId` | |
| `get_evidence_uniprot_variants_widget` | `evidence/UniProtVariants` | `ensemblId` + `efoId` | |

### Credible Set (5 sections)

| Tool name | Section | Input |
|-----------|---------|-------|
| `get_l2g_widget` | `credibleSet/Locus2Gene` | `studyLocusId` |
| `get_credible_set_gwas_coloc_widget` | `credibleSet/GWASColoc` | `studyLocusId` |
| `get_credible_set_mol_qtl_coloc_widget` | `credibleSet/MolQTLColoc` | `studyLocusId` |
| `get_credible_set_variants_widget` | `credibleSet/Variants` | `studyLocusId` |
| `get_credible_set_e2g_widget` | `credibleSet/EnhancerToGenePredictions` | `studyLocusId` |

### Study (3 sections)

| Tool name | Section | Input | Notes |
|-----------|---------|-------|-------|
| `get_gwas_credible_sets_widget` | `study/GWASCredibleSets` | `studyId` | |
| `get_study_qtl_credible_sets_widget` | `study/QTLCredibleSets` | `studyId` | |
| `get_shared_trait_studies_widget` | `study/SharedTraitStudies` | `studyId` | Two-query: fetches disease IDs first, then queries studies by `diseaseIds` |

### Variant (8 sections)

| Tool name | Section | Input |
|-----------|---------|-------|
| `get_variant_effect_widget` | `variant/VariantEffect` | `variantId` |
| `get_variant_eva_widget` | `variant/EVA` | `variantId` |
| `get_variant_e2g_widget` | `variant/EnhancerToGenePredictions` | `variantId` |
| `get_variant_gwas_credible_sets_widget` | `variant/GWASCredibleSets` | `variantId` |
| `get_variant_pharmacogenomics_widget` | `variant/Pharmacogenomics` | `variantId` |
| `get_variant_qtl_credible_sets_widget` | `variant/QTLCredibleSets` | `variantId` |
| `get_variant_uniprot_widget` | `variant/UniProtVariants` | `variantId` |
| `get_variant_vep_widget` | `variant/VariantEffectPredictor` | `variantId` |

### Manual widgets (custom entry points)

| Tool name | Section | Notes |
|-----------|---------|-------|
| `get_molecular_structure_widget` | `variant/MolecularStructure` | Custom 3D AlphaFold viewer; fetches CIF/pathogenicity/domain data directly from AlphaFold + UniProt (extra CSP `connectDomains`) |

---

## Excluded sections

These sections exist in `packages/sections/src` but are not in the registry.

| Section | Entity | Reason excluded |
|---------|--------|-----------------|
| `variant/MolecularStructure` | Variant | Covered by `get_molecular_structure_widget` (manual) |

Note: `target/Bibliography`, `disease/Bibliography`, `drug/Bibliography`, `disease/GWASStudies`, `target/MolecularInteractions`, `target/SubcellularLocation`, `evidence/EuropePmc`, and `target/MolecularStructure` were previously listed here but turned out not to be real blockers. The first four were mostly documentation drift — the widget entry codegen just renders `Body` directly and lets it fetch its own data, so cursor pagination and array-shaped GraphQL variables inside `Body` don't matter. `MolecularInteractions` needed a one-line `usePlatformApi()` stub fix; `SubcellularLocation` and `EuropePmc` needed one `extraConnectDomains` entry each. `target/MolecularStructure` was a genuine miscategorization — it was claimed to be "covered by the manual widget," but it's a distinct component (its own experimental-PDB + AlphaFold viewer, own CIF parser) live on the platform (unlike `target/OverlappingVariants` below); it needed new per-section `extraDedupe` support (for its direct `3dmol` usage) plus 3 CSP domains. All eight are now in the registry above — every excluded section that's actually live on the platform has been migrated.

Note: `target/OverlappingVariants` was previously listed here but is out of scope entirely — it's not rendered on the real Open Targets Platform (disabled/commented out in `apps/platform/src/pages/TargetPage/Profile.tsx`), so there's no live platform widget to migrate.
48 changes: 48 additions & 0 deletions apps/mcp-widgets-server/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
{
"name": "mcp-widgets-server",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"dev": "tsx watch --env-file=.env src/index.ts",
"dev-ppp": "OT_API_URL=https://api.partner-platform.opentargets.org/api/v4/graphql OTP_MCP_URL=https://mcp.partner-platform.opentargets.org/mcp tsx watch --env-file=.env src/index.ts",
"start": "tsx src/index.ts",
"start:stdio": "tsx src/stdio.ts",
"build:widget:ms": "vite build --config vite/widget.config.ms.ts",
"build:widgets": "yarn build:widget:ms && yarn build:widgets:target && yarn build:widgets:disease && yarn build:widgets:drug && yarn build:widgets:evidence && yarn build:widgets:credibleset && yarn build:widgets:variant && yarn build:widgets:study",
"build:widgets:target": "tsx scripts/build-sections.ts target --no-clean",
"build:widgets:disease": "tsx scripts/build-sections.ts disease --no-clean",
"build:widgets:drug": "tsx scripts/build-sections.ts drug --no-clean",
"build:widgets:evidence": "tsx scripts/build-sections.ts evidence --no-clean",
"build:widgets:credibleset": "tsx scripts/build-sections.ts credibleSet --no-clean",
"build:widgets:variant": "tsx scripts/build-sections.ts variant --no-clean",
"build:widgets:study": "tsx scripts/build-sections.ts study --no-clean",
"build:widgets:sections": "tsx scripts/build-sections.ts all --no-clean",
"build": "yarn build:widgets"
},
"dependencies": {
"@modelcontextprotocol/ext-apps": "*",
"@modelcontextprotocol/sdk": "^1.26.0",
"cors": "^2.8.5",
"express": "^4.21.2",
"tsx": "^4.7.0",
"zod": "^3.22.0"
},
"devDependencies": {
"@apollo/client": "3.10.8",
"@types/cors": "^2.8.17",
"@types/express": "^4.17.21",
"@types/node": "^20.0.0",
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0",
"@vitejs/plugin-react": "^4.0.0",
"d3": "*",
"graphql": "^15.0.0",
"polished": "^4.3.1",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"react-router-dom": "6.28.0",
"typescript": "^5.2.2",
"vite": "^6.0.0"
}
}
49 changes: 49 additions & 0 deletions apps/mcp-widgets-server/scripts/build-sections.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/**
* Build all section-based MCP widget bundles for a given entity.
*
* Usage:
* yarn build:widgets:target # builds all target section widgets
* yarn build:widgets:drug # builds all drug section widgets
* yarn build:widgets:evidence # builds all evidence section widgets
* yarn build:widgets:all # builds everything (existing + sections)
*
* Bundles are emitted to dist/widgets/ (same directory as the manual widgets).
* The first section widget of each run clears the directory; subsequent ones append.
* Pass --no-clean to skip cleaning (useful when building multiple entities sequentially).
*/
import { build } from "vite";
import { SECTION_REGISTRY } from "../src/sections/registry.js";
import { createSectionWidgetConfig } from "../vite/section-widget.plugin.js";

const entity = process.argv[2];
const noClean = process.argv.includes("--no-clean");

if (!entity || entity === "--help") {
console.log("Usage: tsx scripts/build-sections.ts <entity|all> [--no-clean]");
console.log(" entity: target | disease | drug | evidence | credibleSet | variant | study | all");
process.exit(entity ? 0 : 1);
}

const sections =
entity === "all"
? SECTION_REGISTRY
: SECTION_REGISTRY.filter(s => s.entity === entity);

if (sections.length === 0) {
console.error(`No sections found for entity: ${entity}`);
process.exit(1);
}

console.log(`Building ${sections.length} ${entity} widget${sections.length > 1 ? "s" : ""}…`);

for (let i = 0; i < sections.length; i++) {
const def = sections[i];
const emptyOutDir = i === 0 && !noClean;
console.log(` [${i + 1}/${sections.length}] ${def.toolName}`);
await build({
...createSectionWidgetConfig(def, { emptyOutDir }),
logLevel: "warn",
});
}

console.log("Done.");
Loading
Loading