From c4fe62910cbf94caf0a5891591287d85eb413d7c Mon Sep 17 00:00:00 2001 From: Carlos Cruz Date: Fri, 10 Jul 2026 10:40:21 +0100 Subject: [PATCH 1/6] feat: add mcp-widgets-server with widget tools and data prefetching Adds a new MCP (Model Context Protocol) server that exposes Open Targets data visualisation widgets as MCP App tools for use in Claude Desktop. - Express HTTP server with Streamable HTTP MCP transport - Widget tools auto-generated from the sections registry (target, disease, drug, evidence, credible-set, variant, study entities) - Server-side GraphQL prefetch via ot_fetch_widget_data tool, working around Claude Desktop bug #696 that strips structuredContent from tool-result notifications - IIFE widget bundles built with Vite, inlined into HTML shells served as MCP App resources - Molecular structure widget with AlphaFold 3D viewer (manual entry) - Dockerfile for Cloud Run deployment (max-instances=1 for session stickiness); stdio transport for local Claude Desktop use via mcp-remote --- .gitignore | 2 + apps/mcp-widgets-server/.dockerignore | 33 + apps/mcp-widgets-server/.env.example | 2 + apps/mcp-widgets-server/.gitignore | 2 + apps/mcp-widgets-server/Dockerfile | 48 + apps/mcp-widgets-server/WIDGETS.md | 183 +++ apps/mcp-widgets-server/package.json | 47 + .../scripts/build-sections.ts | 49 + apps/mcp-widgets-server/src/index.ts | 94 ++ apps/mcp-widgets-server/src/mcp-server.ts | 260 ++++ .../src/sections/registry.ts | 727 ++++++++++++ apps/mcp-widgets-server/src/stdio.ts | 15 + apps/mcp-widgets-server/src/widgets/index.ts | 100 ++ .../src/widgets/molecular-structure.ts | 49 + apps/mcp-widgets-server/src/widgets/types.ts | 65 + apps/mcp-widgets-server/tsconfig.json | 14 + apps/mcp-widgets-server/turbo.json | 9 + .../vite/section-widget.plugin.ts | 99 ++ .../vite/widget.config.base.ts | 241 ++++ .../vite/widget.config.ms.ts | 10 + .../MolecularStructureWidget.tsx | 5 + .../widget-src/molecular-structure/main.tsx | 12 + .../widget-src/shared/createWidgetEntry.tsx | 285 +++++ .../widget-src/shared/stubs/ui-index.tsx | 290 +++++ .../widget-src/shared/stubs/ui-ms-index.tsx | 61 + .../widget-src/shared/theme.ts | 27 + package.json | 3 + yarn.lock | 1045 +++++++++++++++-- 28 files changed, 3650 insertions(+), 127 deletions(-) create mode 100644 apps/mcp-widgets-server/.dockerignore create mode 100644 apps/mcp-widgets-server/.env.example create mode 100644 apps/mcp-widgets-server/.gitignore create mode 100644 apps/mcp-widgets-server/Dockerfile create mode 100644 apps/mcp-widgets-server/WIDGETS.md create mode 100644 apps/mcp-widgets-server/package.json create mode 100644 apps/mcp-widgets-server/scripts/build-sections.ts create mode 100644 apps/mcp-widgets-server/src/index.ts create mode 100644 apps/mcp-widgets-server/src/mcp-server.ts create mode 100644 apps/mcp-widgets-server/src/sections/registry.ts create mode 100644 apps/mcp-widgets-server/src/stdio.ts create mode 100644 apps/mcp-widgets-server/src/widgets/index.ts create mode 100644 apps/mcp-widgets-server/src/widgets/molecular-structure.ts create mode 100644 apps/mcp-widgets-server/src/widgets/types.ts create mode 100644 apps/mcp-widgets-server/tsconfig.json create mode 100644 apps/mcp-widgets-server/turbo.json create mode 100644 apps/mcp-widgets-server/vite/section-widget.plugin.ts create mode 100644 apps/mcp-widgets-server/vite/widget.config.base.ts create mode 100644 apps/mcp-widgets-server/vite/widget.config.ms.ts create mode 100644 apps/mcp-widgets-server/widget-src/molecular-structure/MolecularStructureWidget.tsx create mode 100644 apps/mcp-widgets-server/widget-src/molecular-structure/main.tsx create mode 100644 apps/mcp-widgets-server/widget-src/shared/createWidgetEntry.tsx create mode 100644 apps/mcp-widgets-server/widget-src/shared/stubs/ui-index.tsx create mode 100644 apps/mcp-widgets-server/widget-src/shared/stubs/ui-ms-index.tsx create mode 100644 apps/mcp-widgets-server/widget-src/shared/theme.ts diff --git a/.gitignore b/.gitignore index 2a63f423c..8afd903a0 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/apps/mcp-widgets-server/.dockerignore b/apps/mcp-widgets-server/.dockerignore new file mode 100644 index 000000000..62faadcc1 --- /dev/null +++ b/apps/mcp-widgets-server/.dockerignore @@ -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/ diff --git a/apps/mcp-widgets-server/.env.example b/apps/mcp-widgets-server/.env.example new file mode 100644 index 000000000..d15bcf501 --- /dev/null +++ b/apps/mcp-widgets-server/.env.example @@ -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 diff --git a/apps/mcp-widgets-server/.gitignore b/apps/mcp-widgets-server/.gitignore new file mode 100644 index 000000000..c6a0d566b --- /dev/null +++ b/apps/mcp-widgets-server/.gitignore @@ -0,0 +1,2 @@ +.env +dist/ diff --git a/apps/mcp-widgets-server/Dockerfile b/apps/mcp-widgets-server/Dockerfile new file mode 100644 index 000000000..1ba0e8bc7 --- /dev/null +++ b/apps/mcp-widgets-server/Dockerfile @@ -0,0 +1,48 @@ +# ── 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 + +# 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"] diff --git a/apps/mcp-widgets-server/WIDGETS.md b/apps/mcp-widgets-server/WIDGETS.md new file mode 100644 index 000000000..96f033faa --- /dev/null +++ b/apps/mcp-widgets-server/WIDGETS.md @@ -0,0 +1,183 @@ +# 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, makeWidgetShell, toAnthropicTool +│ ├── mcp-server.ts # MCP tool + resource registration, GraphQL prefetch +│ ├── chat.ts # Chat workspace HTTP handler +│ ├── index.ts # Server entrypoint +│ ├── otp-client.ts # OT GraphQL client helpers +│ └── 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, fetches GraphQL data server-side, serves widget HTML | 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` runs the GraphQL prefetch and returns data +via `_meta.prefetchedData`. The widget HTML shell (with the bundle inlined) renders in an iframe, +and a `postMessage` interceptor delivers the prefetched data to Apollo without any live network +requests from the widget. + +--- + +## How to add a new section widget + +1. Add an entry to `src/sections/registry.ts` +2. Run `yarn build:widgets:` (or `yarn build:widgets:sections` for all) +3. Restart Claude Desktop + +For sections where the GQL query variable does not match the input param name (or that need a +two-step prefetch), use `primaryPrefetch` and/or `extraPrefetches` in the `SectionDef`. + +--- + +## Included section widgets (61 total) + +### Target (14 sections) + +| Tool name | Section | Input | +|-----------|---------|-------| +| `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_expression_widget` | `target/Expression` | `ensemblId` | +| `get_target_gene_ontology_widget` | `target/GeneOntology` | `ensemblId` | +| `get_target_genetic_constraint_widget` | `target/GeneticConstraint` | `ensemblId` | +| `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_tractability_widget` | `target/Tractability` | `ensemblId` | + +### Disease (4 sections) + +| Tool name | Section | Input | +|-----------|---------|-------| +| `get_disease_drugs_widget` | `disease/Drugs` | `efoId` | +| `get_disease_ot_projects_widget` | `disease/OTProjects` | `efoId` | +| `get_disease_ontology_widget` | `disease/Ontology` | `efoId` | +| `get_disease_phenotypes_widget` | `disease/Phenotypes` | `efoId` | + +### Drug (5 sections) + +| Tool name | Section | Input | Notes | +|-----------|---------|-------|-------| +| `get_drug_adverse_events_widget` | `drug/AdverseEvents` | `chemblId` | | +| `get_drug_indications_widget` | `drug/ClinicalIndications` | `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 (20 sections) + +| Tool name | Section | Input | +|-----------|---------|-------| +| `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_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; prefetches CIF file server-side | + +--- + +## Excluded sections + +These sections exist in `packages/sections/src` but are not in the registry. + +| Section | Entity | Reason excluded | +|---------|--------|-----------------| +| `target/BaselineExpression` | Target | Requires symbol lookup first (two-query with TargetSymbol); `target/Expression` covers expression data | +| `target/Bibliography` | Target | Uses SimilarEntities API with cursor pagination; publication rendering complexity | +| `target/MolecularInteractions` | Target | Four separate databases (IntAct, Reactome, SIGNOR, STRING), each with its own query | +| `target/MolecularStructure` | Target | 3D viewer — covered by `get_molecular_structure_widget` (manual) | +| `target/OverlappingVariants` | Target | Interactive genome browser with complex stateful viewer | +| `target/SubcellularLocation` | Target | Custom SVG-based subcellular location visualisation with no standard table | +| `disease/Bibliography` | Disease | SimilarEntities cursor pagination; publication rendering complexity | +| `disease/GWASStudies` | Disease | Requires `diseaseIds: [String!]!` array input, not a single EFO ID | +| `drug/Bibliography` | Drug | SimilarEntities cursor pagination | +| `evidence/EuropePmc` | Evidence | SentenceMatch + Publication components with cursor pagination | +| `variant/MolecularStructure` | Variant | Covered by `get_molecular_structure_widget` (manual) | diff --git a/apps/mcp-widgets-server/package.json b/apps/mcp-widgets-server/package.json new file mode 100644 index 000000000..729b508d0 --- /dev/null +++ b/apps/mcp-widgets-server/package.json @@ -0,0 +1,47 @@ +{ + "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", + "react": "^18.0.0", + "react-dom": "^18.0.0", + "react-router-dom": "6.28.0", + "typescript": "^5.2.2", + "vite": "^6.0.0" + } +} diff --git a/apps/mcp-widgets-server/scripts/build-sections.ts b/apps/mcp-widgets-server/scripts/build-sections.ts new file mode 100644 index 000000000..9be9a1dd6 --- /dev/null +++ b/apps/mcp-widgets-server/scripts/build-sections.ts @@ -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 [--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."); diff --git a/apps/mcp-widgets-server/src/index.ts b/apps/mcp-widgets-server/src/index.ts new file mode 100644 index 000000000..b9931f961 --- /dev/null +++ b/apps/mcp-widgets-server/src/index.ts @@ -0,0 +1,94 @@ +import express from "express"; +import cors from "cors"; +import { randomUUID } from "node:crypto"; +import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import { createMcpServer } from "./mcp-server.js"; +import { WIDGET_REGISTRY } from "./widgets/index.js"; + +const PORT = parseInt(process.env.PORT ?? "3001", 10); + +const app = express(); +app.use(express.json()); +app.use(cors({ origin: "*", exposedHeaders: ["mcp-session-id"] })); + +// ----- MCP transport (stateful sessions over HTTP) ----- + +const transports = new Map(); + +app.post("/mcp", async (req, res) => { + const sessionId = req.headers["mcp-session-id"] as string | undefined; + let transport = sessionId ? transports.get(sessionId) : undefined; + + if (!transport) { + transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: () => randomUUID(), + onsessioninitialized: sid => { + transports.set(sid, transport!); + }, + }); + transport.onclose = () => { + if (transport?.sessionId) transports.delete(transport.sessionId); + }; + await createMcpServer().connect(transport); + } + + await transport.handleRequest(req, res, req.body); +}); + +app.get("/mcp", async (req, res) => { + const sessionId = req.headers["mcp-session-id"] as string | undefined; + const transport = sessionId ? transports.get(sessionId) : undefined; + if (!transport) { res.status(404).json({ error: "Session not found" }); return; } + await transport.handleRequest(req, res); +}); + +app.delete("/mcp", async (req, res) => { + const sessionId = req.headers["mcp-session-id"] as string | undefined; + const transport = sessionId ? transports.get(sessionId) : undefined; + if (!transport) { res.status(404).json({ error: "Session not found" }); return; } + await transport.handleRequest(req, res); +}); + +// ----- Static assets ----- + +// Serve the sandbox proxy (only in local dev — the platform app must be present). +const sandboxProxyRoot = new URL("../../../apps/platform/public", import.meta.url).pathname; +app.get("/sandbox_proxy.html", (_req, res) => { + res.setHeader("Content-Type", "text/html"); + res.sendFile("sandbox_proxy.html", { root: sandboxProxyRoot }, err => { + if (err) res.status(404).send("Not available in this deployment"); + }); +}); + +// Serve the built widget IIFE bundles. +app.use( + "/widgets", + express.static(new URL("../dist/widgets", import.meta.url).pathname, { + setHeaders: res => { + res.setHeader("Access-Control-Allow-Origin", "*"); + }, + }) +); + +// ----- Utility endpoints ----- + +app.get("/health", (_req, res) => + res.json({ status: "ok", server: "ot-widgets-server", sessions: transports.size }) +); + +app.get("/status", (_req, res) => + res.json({ + server: "ot-widgets-server", + widgets: WIDGET_REGISTRY.length, + }) +); + +// ----- Start ----- + +app.listen(PORT, () => { + console.log("\n Open Targets Widgets MCP Server"); + console.log(` Running on http://localhost:${PORT}`); + console.log(` MCP endpoint: http://localhost:${PORT}/mcp`); + console.log(` Widgets: http://localhost:${PORT}/widgets/`); + console.log(` Status: http://localhost:${PORT}/status\n`); +}); diff --git a/apps/mcp-widgets-server/src/mcp-server.ts b/apps/mcp-widgets-server/src/mcp-server.ts new file mode 100644 index 000000000..bdd5b5185 --- /dev/null +++ b/apps/mcp-widgets-server/src/mcp-server.ts @@ -0,0 +1,260 @@ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { + registerAppTool, + registerAppResource, + RESOURCE_MIME_TYPE, +} from "@modelcontextprotocol/ext-apps/server"; +import { z } from "zod"; +import { readFile } from "node:fs/promises"; +import { WIDGET_REGISTRY } from "./widgets/index.js"; + +const PUBLIC_API_URL = "https://api.platform.opentargets.org/api/v4/graphql"; + +/** + * Builds a self-contained HTML string with the widget IIFE bundle inlined. + * + * Data delivery follows the standard MCP Apps pattern: + * widget's ontoolinput → app.callServerTool("ot_fetch_widget_data") → + * result.structuredContent → fetch interceptor cache → Apollo resolves. + * + * window.__OT_WIDGET_TOOL__ is set here so createWidgetEntry.tsx knows which + * tool to call via callServerTool without needing to parse hostContext. + */ +async function makeWidgetShell(bundleFile: string, title: string, toolName: string): Promise { + const bundlePath = new URL(`../dist/widgets/${bundleFile}`, import.meta.url).pathname; + const bundleJs = await readFile(bundlePath, "utf-8"); + const apiUrl = process.env.OT_API_URL ?? PUBLIC_API_URL; + + return ` + + + + + ${title} + + + + + + + +
+ + +`; +} + +type PrefetchResult = { + operations: Array<{ operationName: string; data: unknown }>; + /** Variable-filtered operations: interceptor filters allItems by request variables at call time */ + filteredOps?: Array<{ + operationName: string; + allItems: unknown[]; + itemIdField: string; + requestVarName: string; + responseKey: string; + }>; + urlData?: Array<{ url: string; text: string; contentType: string }>; +}; + +/** Runs a single GraphQL request against the OT API. */ +async function gqlFetch( + apiUrl: string, + operationName: string, + query: string, + variables: Record +): Promise { + const response = await fetch(apiUrl, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ operationName, query, variables }), + }); + const json = (await response.json()) as { data?: unknown }; + return json.data; +} + +/** Fetches GraphQL data server-side for widgets that define prefetch config. */ +async function fetchPrefetchData( + def: { + prefetch: NonNullable<(typeof import("./widgets/index.js").WIDGET_REGISTRY)[number]["prefetch"]>; + inputParam: { name: string }; + inputParams?: Array<{ name: string }>; + }, + inputValues: Record +): Promise { + const apiUrl = process.env.GRAPHQL_API_URL ?? process.env.OT_API_URL ?? PUBLIC_API_URL; + try { + const primaryVariables = { + ...inputValues, + ...(def.prefetch.extraVariables ?? {}), + }; + const primaryData = await gqlFetch( + apiUrl, + def.prefetch.operationName, + def.prefetch.query, + primaryVariables + ); + + const operations: Array<{ operationName: string; data: unknown }> = [ + { operationName: def.prefetch.operationName, data: primaryData }, + ]; + + const filteredOps: PrefetchResult["filteredOps"] = []; + + // Run extra queries in parallel (all depend only on primaryData, not each other) + if (def.prefetch.extraPrefetches) { + const firstInputValue = inputValues[def.inputParam.name] ?? Object.values(inputValues)[0] ?? ""; + await Promise.all( + def.prefetch.extraPrefetches.map(async extra => { + try { + const extraData = await gqlFetch( + apiUrl, + extra.operationName, + extra.query, + extra.variables(firstInputValue, primaryData) + ); + if (extra.filteredBy) { + const fb = extra.filteredBy; + const allItems = (extraData as any)?.[fb.responseKey] ?? []; + filteredOps.push({ + operationName: extra.operationName, + allItems, + itemIdField: fb.itemIdField, + requestVarName: fb.requestVarName, + responseKey: fb.responseKey, + }); + } else { + operations.push({ operationName: extra.operationName, data: extraData }); + } + } catch (err) { + console.error(`[mcp] extra prefetch failed for ${extra.operationName}:`, err); + } + }) + ); + } + + // Fetch extra URL assets (e.g. AlphaFold CIF) derived from primary data + let urlData: Array<{ url: string; text: string; contentType: string }> | undefined; + if (def.prefetch.extractExtraFetches) { + const extraFetches = def.prefetch.extractExtraFetches(primaryData); + if (extraFetches.length > 0) { + urlData = await Promise.all( + extraFetches.map(async ({ url, contentType }) => { + try { + const r = await fetch(url); + const text = await r.text(); + return { url, text, contentType }; + } catch (err) { + console.error(`[mcp] extra fetch failed for ${url}:`, err); + return { url, text: "", contentType }; + } + }) + ); + } + } + + return { operations, filteredOps: filteredOps.length > 0 ? filteredOps : undefined, urlData }; + } catch (err) { + console.error(`[mcp] prefetch failed for ${def.prefetch.operationName}:`, err); + return null; + } +} + +export function createMcpServer(): McpServer { + const server = new McpServer({ name: "ot-widgets-server", version: "0.1.0" }); + + for (const def of WIDGET_REGISTRY) { + const resourceUri = `ui://ot-widgets/${def.toolName}`; + + const params = def.inputParams ?? [def.inputParam]; + const inputSchema = Object.fromEntries( + params.map(p => [p.name, z.string().describe(p.description)]) + ) as Parameters[2]["inputSchema"]; + + registerAppTool( + server, + def.toolName, + { + description: def.description, + inputSchema, + _meta: { ui: { resourceUri } }, + }, + async input => { + console.error(`[mcp] tool called: ${def.toolName}`, input); + const inputValues = Object.fromEntries( + params.map(p => [p.name, String(input[p.name as keyof typeof input] ?? "")]) + ); + + let prefetched = null; + if (def.prefetch) { + prefetched = await fetchPrefetchData( + def as Parameters[0], + inputValues + ); + } + + console.error(`[mcp] prefetch done for ${def.toolName}`); + + return { + content: [{ type: "text" as const, text: def.successMessage }], + // structuredContent is the spec-defined field for UI data; the host forwards it + // to the widget via ui/notifications/tool-result without adding it to model context. + structuredContent: prefetched ?? undefined, + _meta: { ui: { resourceUri } }, + }; + } + ); + + // Resource handler — serves the HTML shell immediately (no blocking). + // Data is not embedded here; it arrives via AppBridge tool-result postMessage. + registerAppResource(server, def.title, resourceUri, { mimeType: RESOURCE_MIME_TYPE }, async () => { + console.error(`[mcp] resource READ: ${resourceUri}`); + const html = await makeWidgetShell(def.bundleFile, def.title, def.toolName); + return { contents: [{ uri: resourceUri, mimeType: RESOURCE_MIME_TYPE, text: html }] }; + }); + } + + // Internal tool called by widgets via app.callServerTool() in ontoolinput. + // This bypasses the notification path (where Claude Desktop strips structuredContent) + // and returns data through the request/response path instead. + server.tool( + "ot_fetch_widget_data", + { + tool: z.string().describe("The widget tool name to fetch prefetched data for"), + inputJson: z.string().describe("JSON-encoded tool input arguments"), + }, + async ({ tool, inputJson }) => { + const def = WIDGET_REGISTRY.find(w => w.toolName === tool); + if (!def?.prefetch) { + return { content: [{ type: "text" as const, text: "no prefetch config" }] }; + } + + const args = JSON.parse(inputJson) as Record; + const params = def.inputParams ?? [def.inputParam]; + const inputValues = Object.fromEntries( + params.map(p => [p.name, String(args[p.name] ?? "")]) + ); + + console.error(`[mcp] ot_fetch_widget_data: tool=${tool}`, inputValues); + const prefetched = await fetchPrefetchData( + def as Parameters[0], + inputValues + ); + + return { + content: [{ type: "text" as const, text: "ok" }], + structuredContent: prefetched ?? undefined, + }; + } + ); + + return server; +} diff --git a/apps/mcp-widgets-server/src/sections/registry.ts b/apps/mcp-widgets-server/src/sections/registry.ts new file mode 100644 index 000000000..792687e53 --- /dev/null +++ b/apps/mcp-widgets-server/src/sections/registry.ts @@ -0,0 +1,727 @@ +/** + * Registry of all section-based MCP widget tools. + * + * Each entry defines one MCP tool that renders a section Body component + * from packages/sections. The bundle file, GQL query, and operation name + * are auto-derived at build/server-startup time from the sectionPath. + * + * Naming convention for toolName: get_{entity}_{section_snake_case}_widget + * Bundle files: {entity}-{section-kebab-case}.js (e.g. target-tractability.js) + */ +export type SectionDef = { + /** Entity type — determines Body prop shape and URL variable convention */ + entity: "target" | "disease" | "drug" | "evidence" | "variant" | "credibleSet" | "study"; + /** Path relative to packages/sections/src (e.g. "target/Tractability") */ + sectionPath: string; + /** MCP tool name (e.g. "get_target_tractability_widget") */ + toolName: string; + /** Human-readable description shown to the LLM when selecting tools */ + description: string; + /** Input parameters. One for single-entity sections, two for evidence. */ + inputParams: ReadonlyArray<{ name: string; description: string }>; + /** + * Extra static variables merged into the prefetch GraphQL request alongside + * the primary input(s). Used for pagination defaults (size, index, cursor). + */ + prefetchExtraVariables?: Record; + /** + * Override the auto-detected primary GQL query. Use when the section's query + * variable doesn't match the tool's input param (e.g. SharedTraitStudies needs + * diseaseIds, but the tool input is studyId). + */ + primaryPrefetch?: { query: string; operationName: string }; + /** + * Additional queries run after the primary, same shape as WidgetDef.prefetch.extraPrefetches. + */ + extraPrefetches?: Array<{ + query: string; + operationName: string; + variables: (inputValue: string, primaryData: unknown) => Record; + filteredBy?: { + requestVarName: string; + itemIdField: string; + responseKey: string; + }; + }>; +}; + +const TARGET_INPUT = [ + { name: "ensemblId", description: "Ensembl gene ID (e.g. ENSG00000157764 for BRAF)" }, +] as const; + +const DISEASE_INPUT = [ + { name: "efoId", description: "EFO disease ID (e.g. EFO_0000249 for Alzheimer disease)" }, +] as const; + +const DRUG_INPUT = [ + { name: "chemblId", description: "ChEMBL drug ID (e.g. CHEMBL192 for ibuprofen)" }, +] as const; + +const EVIDENCE_INPUT = [ + { name: "ensemblId", description: "Ensembl gene ID (e.g. ENSG00000157764 for BRAF)" }, + { name: "efoId", description: "EFO disease ID (e.g. EFO_0000249)" }, +] as const; + +const CREDIBLE_SET_INPUT = [ + { name: "studyLocusId", description: "Study locus ID of the credible set (e.g. 28a6eae8368c995192905821ee578ae8)" }, +] as const; + +const VARIANT_INPUT = [ + { name: "variantId", description: "Variant ID in chromosome_position_ref_alt format (e.g. 19_44908822_C_T)" }, +] as const; + +const STUDY_INPUT = [ + { name: "studyId", description: "GWAS study ID (e.g. GCST90002357)" }, +] as const; + +export const SECTION_REGISTRY: SectionDef[] = [ + // ── TARGET ────────────────────────────────────────────────────────────────── + { + entity: "target", + sectionPath: "target/CancerHallmarks", + toolName: "get_target_cancer_hallmarks_widget", + description: + "Shows cancer hallmark annotations for a target gene — which hallmarks it promotes or " + + "suppresses, with supporting publication evidence.", + inputParams: TARGET_INPUT, + }, + { + entity: "target", + sectionPath: "target/ChemicalProbes", + toolName: "get_target_chemical_probes_widget", + description: + "Shows chemical probes available for a target gene — highly selective tool compounds " + + "used in pharmacological research, with probe quality scores and sources.", + inputParams: TARGET_INPUT, + }, + { + entity: "target", + sectionPath: "target/ComparativeGenomics", + toolName: "get_target_comparative_genomics_widget", + description: + "Shows comparative genomics data for a target gene — orthologues across species " + + "(human, mouse, rat, zebrafish, etc.) with homology scores.", + inputParams: TARGET_INPUT, + }, + { + entity: "target", + sectionPath: "target/DepMap", + toolName: "get_target_depmap_widget", + description: + "Shows DepMap CRISPR gene essentiality data for a target — cancer cell line " + + "dependency scores across tissue types from the Cancer Dependency Map.", + inputParams: TARGET_INPUT, + }, + { + entity: "target", + sectionPath: "target/Drugs", + toolName: "get_target_drugs_widget", + description: + "Shows drugs and clinical candidates associated with a target gene — approved drugs, " + + "clinical-stage compounds, mechanisms of action, and disease indications.", + inputParams: TARGET_INPUT, + }, + { + entity: "target", + sectionPath: "target/BaselineExpression", + toolName: "get_target_baseline_expression_widget", + description: + "Shows baseline RNA expression levels for a target gene across tissues and cell types — " + + "median TPM values, tissue specificity scores, and distribution scores from GTEx, HPA, " + + "and other sources.", + inputParams: TARGET_INPUT, + }, + { + entity: "target", + sectionPath: "target/GeneOntology", + toolName: "get_target_gene_ontology_widget", + description: + "Shows Gene Ontology (GO) annotations for a target gene — molecular functions, " + + "biological processes, and cellular components.", + inputParams: TARGET_INPUT, + }, + { + entity: "target", + sectionPath: "target/GeneticConstraint", + toolName: "get_target_genetic_constraint_widget", + description: + "Shows genetic constraint metrics for a target gene (pLI, LOEUF, Z-scores) from " + + "gnomAD — indicating intolerance to loss-of-function variants.", + inputParams: TARGET_INPUT, + }, + { + entity: "target", + sectionPath: "target/MousePhenotypes", + toolName: "get_target_mouse_phenotypes_widget", + description: + "Shows mouse phenotype data for a target gene from IMPC — phenotypic categories " + + "observed in knockout mice with significance scores.", + inputParams: TARGET_INPUT, + }, + { + entity: "target", + sectionPath: "target/Pathways", + toolName: "get_target_pathways_widget", + description: + "Shows Reactome pathway memberships for a target gene — biological pathways and " + + "hierarchical pathway categories.", + inputParams: TARGET_INPUT, + }, + { + entity: "target", + sectionPath: "target/Pharmacogenomics", + toolName: "get_target_pharmacogenomics_widget", + description: + "Shows pharmacogenomics data for a target gene — genetic variants that affect drug " + + "response and their clinical annotations from PharmGKB.", + inputParams: TARGET_INPUT, + }, + { + entity: "target", + sectionPath: "target/QTLCredibleSets", + toolName: "get_target_qtl_credible_sets_widget", + description: + "Shows QTL credible sets linked to a target gene — eQTL and sQTL fine-mapped " + + "credible sets from GTEx and other QTL datasets.", + inputParams: TARGET_INPUT, + prefetchExtraVariables: { size: 500, index: 0 }, + }, + { + entity: "target", + sectionPath: "target/Safety", + toolName: "get_target_safety_widget", + description: + "Shows target safety information — adverse effects, safety risk information, " + + "and experimental toxicity data from curated sources.", + inputParams: TARGET_INPUT, + }, + { + entity: "target", + sectionPath: "target/Tractability", + toolName: "get_target_tractability_widget", + description: + "Shows tractability assessment for a target gene — small molecule, antibody, PROTAC, " + + "and other drug modality predictions with supporting evidence buckets.", + inputParams: TARGET_INPUT, + }, + + // ── DISEASE ───────────────────────────────────────────────────────────────── + { + entity: "disease", + sectionPath: "disease/Drugs", + toolName: "get_disease_drugs_widget", + description: + "Shows drugs approved or in clinical trials for a disease — drug names, " + + "clinical phases, mechanisms of action, and linked targets.", + inputParams: DISEASE_INPUT, + }, + { + entity: "disease", + sectionPath: "disease/OTProjects", + toolName: "get_disease_ot_projects_widget", + description: + "Shows Open Targets experimental projects related to a disease — OTAR-funded " + + "functional genomics and validation studies.", + inputParams: DISEASE_INPUT, + }, + { + entity: "disease", + sectionPath: "disease/Ontology", + toolName: "get_disease_ontology_widget", + description: + "Shows the disease ontology subgraph for a disease — its position in the EFO " + + "hierarchy with parent and child disease terms.", + inputParams: DISEASE_INPUT, + }, + { + entity: "disease", + sectionPath: "disease/Phenotypes", + toolName: "get_disease_phenotypes_widget", + description: + "Shows phenotype annotations for a disease — HPO phenotype terms linked to " + + "the disease with evidence from curated sources.", + inputParams: DISEASE_INPUT, + prefetchExtraVariables: { size: 500, index: 0 }, + }, + + // ── DRUG ──────────────────────────────────────────────────────────────────── + { + entity: "drug", + sectionPath: "drug/AdverseEvents", + toolName: "get_drug_adverse_events_widget", + description: + "Shows adverse event data for a drug from FDA FAERS — significant adverse events " + + "by MedDRA term with likelihood ratio scores.", + inputParams: DRUG_INPUT, + prefetchExtraVariables: { size: 25, index: 0 }, + }, + { + entity: "drug", + sectionPath: "drug/Indications", + toolName: "get_drug_indications_widget", + description: + "Shows drug clinical indications with investigational and approved indications from clinical trial records, including disease names, maximum clinical stage, and individual trial records in a detail panel.", + inputParams: DRUG_INPUT, + extraPrefetches: [ + { + operationName: "ClinicalRecordsQuery", + query: ` + query ClinicalRecordsQuery($clinicalReportsIds: [String!]!) { + clinicalReports(clinicalReportsIds: $clinicalReportsIds) { + clinicalStage + id + source + trialLiterature + title + trialOverallStatus + trialStartDate + type + year + } + } + `, + variables: (_inputValue: string, primaryData: unknown) => { + const rows = (primaryData as any)?.drug?.indications?.rows ?? []; + const allIds: string[] = []; + for (const row of rows) { + for (const report of (row.clinicalReports ?? [])) { + allIds.push(report.id); + } + } + return { clinicalReportsIds: allIds }; + }, + filteredBy: { + requestVarName: "clinicalReportsIds", + itemIdField: "id", + responseKey: "clinicalReports", + }, + }, + ], + }, + { + entity: "drug", + sectionPath: "drug/DrugWarnings", + toolName: "get_drug_warnings_widget", + description: + "Shows drug safety warnings for a drug — black box warnings, withdrawn status, " + + "and year of withdrawal with regulatory authority details.", + inputParams: DRUG_INPUT, + }, + { + entity: "drug", + sectionPath: "drug/MechanismsOfAction", + toolName: "get_drug_mechanisms_of_action_widget", + description: + "Shows mechanisms of action for a drug — target proteins, action types " + + "(agonist, antagonist, inhibitor, etc.), and source references.", + inputParams: DRUG_INPUT, + }, + { + entity: "drug", + sectionPath: "drug/Pharmacogenomics", + toolName: "get_drug_pharmacogenomics_widget", + description: + "Shows pharmacogenomics data for a drug — genetic variants that affect drug " + + "response, phenotype categories, and clinical significance.", + inputParams: DRUG_INPUT, + }, + + // ── EVIDENCE ───────────────────────────────────────────────────────────────── + { + entity: "evidence", + sectionPath: "evidence/CRISPR", + toolName: "get_evidence_crispr_widget", + description: + "Shows CRISPR screen evidence linking a target to a disease — cancer cell " + + "line screens showing gene essentiality in disease-relevant models.", + inputParams: EVIDENCE_INPUT, + prefetchExtraVariables: { size: 500 }, + }, + { + entity: "evidence", + sectionPath: "evidence/CRISPRScreen", + toolName: "get_evidence_crispr_screen_widget", + description: + "Shows CRISPR modifier screen evidence — genetic interaction screens " + + "identifying target-disease associations through functional genomics.", + inputParams: EVIDENCE_INPUT, + prefetchExtraVariables: { size: 500 }, + }, + { + entity: "evidence", + sectionPath: "evidence/CancerBiomarkers", + toolName: "get_evidence_cancer_biomarkers_widget", + description: + "Shows cancer biomarker evidence — genomic alterations in the target gene " + + "associated with drug response in specific cancer types.", + inputParams: EVIDENCE_INPUT, + }, + { + entity: "evidence", + sectionPath: "evidence/CancerGeneCensus", + toolName: "get_evidence_cancer_gene_census_widget", + description: + "Shows Cancer Gene Census evidence — curated cancer driver gene annotations " + + "from COSMIC linking the target to the disease.", + inputParams: EVIDENCE_INPUT, + prefetchExtraVariables: { size: 500 }, + }, + { + entity: "evidence", + sectionPath: "evidence/ClinicalPrecedence", + toolName: "get_evidence_clinical_precedence_widget", + description: + "Shows clinical precedence evidence linking a target gene to a disease — approved and " + + "investigational drugs, clinical phases, and trial outcomes.", + inputParams: EVIDENCE_INPUT, + prefetchExtraVariables: { size: 10, cursor: null }, + }, + { + entity: "evidence", + sectionPath: "evidence/ClinGen", + toolName: "get_evidence_clingen_widget", + description: + "Shows ClinGen curated evidence — expert-curated gene-disease validity " + + "classifications with supporting evidence summaries.", + inputParams: EVIDENCE_INPUT, + prefetchExtraVariables: { size: 500 }, + }, + { + entity: "evidence", + sectionPath: "evidence/EVA", + toolName: "get_evidence_eva_widget", + description: + "Shows ClinVar/EVA genetic evidence — clinically interpreted variants in the " + + "target gene associated with the disease, with pathogenicity classifications.", + inputParams: EVIDENCE_INPUT, + prefetchExtraVariables: { size: 10, cursor: null }, + }, + { + entity: "evidence", + sectionPath: "evidence/EVASomatic", + toolName: "get_evidence_eva_somatic_widget", + description: + "Shows ClinVar somatic evidence — somatic variants in the target gene " + + "associated with the disease from clinical submissions.", + inputParams: EVIDENCE_INPUT, + prefetchExtraVariables: { size: 10, cursor: null }, + }, + { + entity: "evidence", + sectionPath: "evidence/ExpressionAtlas", + toolName: "get_evidence_expression_atlas_widget", + description: + "Shows Expression Atlas evidence — differential expression studies showing " + + "the target gene is up- or down-regulated in the disease context.", + inputParams: EVIDENCE_INPUT, + prefetchExtraVariables: { size: 500 }, + }, + { + entity: "evidence", + sectionPath: "evidence/GWASCredibleSets", + toolName: "get_evidence_gwas_credible_sets_widget", + description: + "Shows GWAS credible set evidence — fine-mapped GWAS loci where the target " + + "gene is the top L2G candidate for the disease.", + inputParams: EVIDENCE_INPUT, + prefetchExtraVariables: { size: 500 }, + }, + { + entity: "evidence", + sectionPath: "evidence/Gene2Phenotype", + toolName: "get_evidence_gene2phenotype_widget", + description: + "Shows Gene2Phenotype curated evidence — expert-curated gene-disease " + + "associations from the G2P database with confidence levels.", + inputParams: EVIDENCE_INPUT, + prefetchExtraVariables: { size: 500 }, + }, + { + entity: "evidence", + sectionPath: "evidence/GeneBurden", + toolName: "get_evidence_gene_burden_widget", + description: + "Shows gene burden evidence — rare variant collapsing analyses associating " + + "the target gene with the disease from biobank studies.", + inputParams: EVIDENCE_INPUT, + prefetchExtraVariables: { size: 500 }, + }, + { + entity: "evidence", + sectionPath: "evidence/GenomicsEngland", + toolName: "get_evidence_genomics_england_widget", + description: + "Shows Genomics England PanelApp evidence — curated gene-disease associations " + + "from the GEL rare disease gene panels.", + inputParams: EVIDENCE_INPUT, + prefetchExtraVariables: { size: 500 }, + }, + { + entity: "evidence", + sectionPath: "evidence/Impc", + toolName: "get_evidence_impc_widget", + description: + "Shows IMPC mouse model evidence — phenotypes observed in knockout mice " + + "that map to the human disease.", + inputParams: EVIDENCE_INPUT, + prefetchExtraVariables: { size: 10, cursor: null }, + }, + { + entity: "evidence", + sectionPath: "evidence/IntOgen", + toolName: "get_evidence_intogen_widget", + description: + "Shows IntOGen cancer driver evidence — somatic mutation enrichment analysis " + + "identifying the target gene as a cancer driver in the disease.", + inputParams: EVIDENCE_INPUT, + prefetchExtraVariables: { size: 500 }, + }, + { + entity: "evidence", + sectionPath: "evidence/OTCRISPR", + toolName: "get_evidence_ot_crispr_widget", + description: + "Shows Open Targets CRISPR evidence — OT-funded CRISPR validation screens " + + "supporting the target-disease association.", + inputParams: EVIDENCE_INPUT, + prefetchExtraVariables: { size: 500 }, + }, + { + entity: "evidence", + sectionPath: "evidence/OTEncore", + toolName: "get_evidence_ot_encore_widget", + description: + "Shows Open Targets ENCORE combinatorial CRISPR screen evidence — genetic " + + "interaction data from OT's double-KO screens.", + inputParams: EVIDENCE_INPUT, + prefetchExtraVariables: { size: 500 }, + }, + { + entity: "evidence", + sectionPath: "evidence/OTValidation", + toolName: "get_evidence_ot_validation_widget", + description: + "Shows Open Targets validation evidence — OT-funded experimental validation " + + "studies supporting the target-disease hypothesis.", + inputParams: EVIDENCE_INPUT, + prefetchExtraVariables: { size: 500 }, + }, + { + entity: "evidence", + sectionPath: "evidence/Orphanet", + toolName: "get_evidence_orphanet_widget", + description: + "Shows Orphanet rare disease evidence — curated gene-disease associations " + + "from the Orphanet rare disease database.", + inputParams: EVIDENCE_INPUT, + prefetchExtraVariables: { size: 500 }, + }, + { + entity: "evidence", + sectionPath: "evidence/Reactome", + toolName: "get_evidence_reactome_widget", + description: + "Shows Reactome pathway evidence — pathway disruption events linking the " + + "target gene to the disease through biological pathways.", + inputParams: EVIDENCE_INPUT, + prefetchExtraVariables: { size: 500 }, + }, + { + entity: "evidence", + sectionPath: "evidence/UniProtLiterature", + toolName: "get_evidence_uniprot_literature_widget", + description: + "Shows UniProt literature evidence — manually curated disease annotations " + + "from UniProtKB with supporting PubMed references.", + inputParams: EVIDENCE_INPUT, + prefetchExtraVariables: { size: 500 }, + }, + { + entity: "evidence", + sectionPath: "evidence/UniProtVariants", + toolName: "get_evidence_uniprot_variants_widget", + description: + "Shows UniProt variant evidence — manually curated disease-causing variants " + + "in the target gene from UniProtKB.", + inputParams: EVIDENCE_INPUT, + prefetchExtraVariables: { size: 500 }, + }, + + // ── CREDIBLE SET ───────────────────────────────────────────────────────────── + { + entity: "credibleSet", + sectionPath: "credibleSet/Locus2Gene", + toolName: "get_l2g_widget", + description: + "Shows the Locus-to-Gene (L2G) heatmap for a credible set — gene prioritisation scores and SHAP feature contributions used to identify causal genes at GWAS loci.", + inputParams: CREDIBLE_SET_INPUT, + }, + { + entity: "credibleSet", + sectionPath: "credibleSet/GWASColoc", + toolName: "get_credible_set_gwas_coloc_widget", + description: + "Shows GWAS colocalisation results for a credible set — other GWAS traits and studies whose signals co-localise at this locus, with posterior probabilities.", + inputParams: CREDIBLE_SET_INPUT, + prefetchExtraVariables: { size: 50, index: 0 }, + }, + { + entity: "credibleSet", + sectionPath: "credibleSet/MolQTLColoc", + toolName: "get_credible_set_mol_qtl_coloc_widget", + description: + "Shows molecular QTL colocalisation for a credible set — eQTL, pQTL and sQTL signals from GTEx and other resources that co-localise at this locus.", + inputParams: CREDIBLE_SET_INPUT, + prefetchExtraVariables: { size: 50, index: 0 }, + }, + { + entity: "credibleSet", + sectionPath: "credibleSet/Variants", + toolName: "get_credible_set_variants_widget", + description: + "Shows the variants within a credible set — posterior inclusion probabilities, allele frequencies, and functional annotations for all tagged variants.", + inputParams: CREDIBLE_SET_INPUT, + prefetchExtraVariables: { size: 500, index: 0 }, + }, + { + entity: "credibleSet", + sectionPath: "credibleSet/EnhancerToGenePredictions", + toolName: "get_credible_set_e2g_widget", + description: + "Shows enhancer-to-gene (E2G) predictions for a credible set — regulatory links between non-coding variants and target genes from Activity-by-Contact and other models.", + inputParams: CREDIBLE_SET_INPUT, + }, + + // ── STUDY ──────────────────────────────────────────────────────────────────── + { + entity: "study", + sectionPath: "study/GWASCredibleSets", + toolName: "get_gwas_credible_sets_widget", + description: + "Shows GWAS credible sets for a study — Manhattan plot of fine-mapped loci with lead variants, p-values, fine-mapping confidence, and top L2G gene scores.", + inputParams: STUDY_INPUT, + prefetchExtraVariables: { size: 500, index: 0 }, + }, + { + entity: "study", + sectionPath: "study/QTLCredibleSets", + toolName: "get_study_qtl_credible_sets_widget", + description: + "Shows QTL credible sets for a molecular QTL study — fine-mapped loci with lead variants and associated gene targets.", + inputParams: STUDY_INPUT, + }, + { + entity: "study", + sectionPath: "study/SharedTraitStudies", + toolName: "get_shared_trait_studies_widget", + description: + "Shows other GWAS studies that share the same disease or phenotype associations as a given study, with sample sizes, cohorts, and publications.", + inputParams: STUDY_INPUT, + primaryPrefetch: { + operationName: "StudyDiseasesForSharedTraits", + query: `query StudyDiseasesForSharedTraits($studyId: String!) { study(studyId: $studyId) { diseases { id } } }`, + }, + extraPrefetches: [ + { + operationName: "SharedTraitStudiesQuery", + query: `query SharedTraitStudiesQuery($diseaseIds: [String!]!, $size: Int!, $index: Int!) { + sharedTraitStudies: studies(diseaseIds: $diseaseIds, page: { size: $size, index: $index }) { + count + rows { + id + traitFromSource + projectId + diseases { + id + name + } + publicationFirstAuthor + publicationDate + publicationJournal + nSamples + cohorts + ldPopulationStructure { + ldPopulation + relativeSampleSize + } + pubmedId + } + } +}`, + variables: (_inputValue: string, primaryData: unknown) => ({ + diseaseIds: (primaryData as any)?.study?.diseases?.map((d: { id: string }) => d.id) ?? [], + size: 500, + index: 0, + }), + }, + ], + }, + + // ── VARIANT ────────────────────────────────────────────────────────────────── + { + entity: "variant", + sectionPath: "variant/VariantEffect", + toolName: "get_variant_effect_widget", + description: + "Shows in-silico predictor scores for a variant — AlphaMissense, SIFT, LOFTEE, FoldX, GERP, VEP, and LoF curation scores as a normalised dot plot from likely benign to likely deleterious.", + inputParams: VARIANT_INPUT, + }, + { + entity: "variant", + sectionPath: "variant/EVA", + toolName: "get_variant_eva_widget", + description: + "Shows ClinVar/EVA clinical evidence for a variant — variant classifications, conditions, review status, and supporting evidence from clinical databases.", + inputParams: VARIANT_INPUT, + }, + { + entity: "variant", + sectionPath: "variant/EnhancerToGenePredictions", + toolName: "get_variant_e2g_widget", + description: + "Shows enhancer-to-gene (E2G) predictions for a variant — regulatory links to target genes from Activity-by-Contact and other functional genomics models.", + inputParams: VARIANT_INPUT, + }, + { + entity: "variant", + sectionPath: "variant/GWASCredibleSets", + toolName: "get_variant_gwas_credible_sets_widget", + description: + "Shows GWAS credible sets containing a variant — fine-mapped loci across studies where this variant is included, with study traits and posterior probabilities.", + inputParams: VARIANT_INPUT, + prefetchExtraVariables: { size: 500, index: 0 }, + }, + { + entity: "variant", + sectionPath: "variant/Pharmacogenomics", + toolName: "get_variant_pharmacogenomics_widget", + description: + "Shows pharmacogenomics annotations for a variant — drug-gene interactions and phenotype associations from PharmGKB and other resources.", + inputParams: VARIANT_INPUT, + }, + { + entity: "variant", + sectionPath: "variant/QTLCredibleSets", + toolName: "get_variant_qtl_credible_sets_widget", + description: + "Shows molecular QTL credible sets for a variant — eQTL, pQTL, and sQTL fine-mapped loci containing this variant across tissues and cell types.", + inputParams: VARIANT_INPUT, + prefetchExtraVariables: { size: 500, index: 0 }, + }, + { + entity: "variant", + sectionPath: "variant/UniProtVariants", + toolName: "get_variant_uniprot_widget", + description: + "Shows UniProt protein variant annotations for a variant — functional consequence, pathogenicity classification, and protein domain context.", + inputParams: VARIANT_INPUT, + }, + { + entity: "variant", + sectionPath: "variant/VariantEffectPredictor", + toolName: "get_variant_vep_widget", + description: + "Shows Ensembl Variant Effect Predictor (VEP) annotations for a variant — transcript consequences, amino acid changes, and regulatory feature impacts across all overlapping genes.", + inputParams: VARIANT_INPUT, + }, +]; diff --git a/apps/mcp-widgets-server/src/stdio.ts b/apps/mcp-widgets-server/src/stdio.ts new file mode 100644 index 000000000..d9efda37e --- /dev/null +++ b/apps/mcp-widgets-server/src/stdio.ts @@ -0,0 +1,15 @@ +/** + * Stdio transport entry point — exposes all widget tools over stdin/stdout. + * Used to connect Claude Desktop directly (claude_desktop_config.json). + * + * Widget bundles are read from dist/widgets/ and inlined into the HTML resource. + * Run "yarn build:widgets" once before starting this entry point. + * + * GraphQL data is fetched server-side when the tool is called and injected into + * the widget HTML via a fetch interceptor — the iframe makes no external requests. + */ +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { createMcpServer } from "./mcp-server.js"; + +const transport = new StdioServerTransport(); +await createMcpServer().connect(transport); diff --git a/apps/mcp-widgets-server/src/widgets/index.ts b/apps/mcp-widgets-server/src/widgets/index.ts new file mode 100644 index 000000000..cd71da918 --- /dev/null +++ b/apps/mcp-widgets-server/src/widgets/index.ts @@ -0,0 +1,100 @@ +import { readdirSync, readFileSync, existsSync } from "node:fs"; +import { resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import type { WidgetDef } from "./types.js"; +import { SECTION_REGISTRY, type SectionDef } from "../sections/registry.js"; + +/** Converts a section path to a kebab-case ID used for bundle filenames and URIs. */ +export function sectionPathToId(sectionPath: string): string { + return sectionPath.replaceAll("/", "-").toLowerCase(); +} + +// Re-export for consumers +export type { WidgetDef } from "./types.js"; +export { molecularStructureWidget } from "./molecular-structure.js"; + +import { molecularStructureWidget } from "./molecular-structure.js"; + +/** Manually-configured widgets with custom prefetch logic */ +const MANUAL_WIDGETS: WidgetDef[] = [molecularStructureWidget]; + +// Resolve sections source root relative to this file (4 levels up from src/widgets/) +const __dirname = fileURLToPath(new URL(".", import.meta.url)); +const SECTIONS_SRC = resolve(__dirname, "../../../../packages/sections/src"); + +/** Auto-detects and reads the primary GQL query file for a section. */ +function loadSectionQuery(sectionPath: string): { query: string; operationName: string } { + const sectionName = sectionPath.split("/").pop()!; + const dir = resolve(SECTIONS_SRC, sectionPath); + const files = readdirSync(dir); + + // Preference order: {Name}Query.gql → {Name}.gql → sectionQuery.gql → any non-summary .gql + const candidates = [ + `${sectionName}Query.gql`, + `${sectionName}.gql`, + "sectionQuery.gql", + ...files.filter( + f => + f.endsWith(".gql") && + !f.toLowerCase().includes("summary") && + !f.toLowerCase().includes("fragment") + ), + ]; + + for (const candidate of candidates) { + const filePath = resolve(dir, candidate); + if (existsSync(filePath)) { + const query = readFileSync(filePath, "utf-8"); + const match = query.match(/query\s+(\w+)/); + const operationName = match?.[1] ?? sectionName + "Query"; + return { query, operationName }; + } + } + + throw new Error(`[mcp] No query file found for section: ${sectionPath}`); +} + +/** Derives a WidgetDef from a SectionDef registry entry. */ +function deriveSectionWidgetDef(def: SectionDef): WidgetDef { + // If primaryPrefetch is provided, use it; otherwise auto-detect from .gql file + const prefetchBase = def.primaryPrefetch ?? (() => { + const { query, operationName } = loadSectionQuery(def.sectionPath); + return { query, operationName }; + })(); + + const sectionId = sectionPathToId(def.sectionPath); + const sectionName = def.sectionPath.split("/").pop()!; + + // Convert PascalCase to readable: "CancerHallmarks" → "Cancer Hallmarks" + const readableName = sectionName.replace(/([A-Z])/g, " $1").trim(); + + return { + toolName: def.toolName, + description: def.description, + inputParam: def.inputParams[0] as { name: string; description: string }, + inputParams: def.inputParams as Array<{ name: string; description: string }>, + uriPrefix: `ui://ot-mcp/${sectionId}`, + bundleFile: `${sectionId}.js`, + title: `${readableName} Widget`, + successMessage: `${readableName} widget rendered successfully in the chat interface.`, + prefetch: { + operationName: prefetchBase.operationName, + query: prefetchBase.query, + extraVariables: def.prefetchExtraVariables, + extraPrefetches: def.extraPrefetches, + }, + }; +} + +/** Derived widgets from SECTION_REGISTRY (auto-detected GQL, standard Body delegation) */ +const SECTION_WIDGETS: WidgetDef[] = SECTION_REGISTRY.flatMap(def => { + try { + return [deriveSectionWidgetDef(def)]; + } catch (err) { + console.warn(`[mcp] Skipping section widget ${def.toolName}:`, (err as Error).message); + return []; + } +}); + +/** All registered widget tools — MCP server, chat handler, and /status all read from this. */ +export const WIDGET_REGISTRY: WidgetDef[] = [...MANUAL_WIDGETS, ...SECTION_WIDGETS]; diff --git a/apps/mcp-widgets-server/src/widgets/molecular-structure.ts b/apps/mcp-widgets-server/src/widgets/molecular-structure.ts new file mode 100644 index 000000000..82b815f71 --- /dev/null +++ b/apps/mcp-widgets-server/src/widgets/molecular-structure.ts @@ -0,0 +1,49 @@ +import type { WidgetDef } from "./types.js"; + +export const molecularStructureWidget: WidgetDef = { + toolName: "get_molecular_structure_widget", + description: + "Get an interactive 3D molecular structure widget for a variant. " + + "Shows the AlphaFold predicted protein structure with the variant residue highlighted, " + + "coloured by pLDDT confidence score.", + inputParam: { + name: "variantId", + description: "The variant ID (e.g. 19_44908822_C_T)", + }, + uriPrefix: "ui://ot-mcp/molecular-structure", + bundleFile: "molecular-structure.js", + title: "Molecular Structure Widget", + successMessage: "Molecular structure widget rendered successfully in the chat interface.", + prefetch: { + operationName: "MolecularStructureQuery", + query: ` + query MolecularStructureQuery($variantId: String!) { + variant(variantId: $variantId) { + id + referenceAllele + alternateAllele + proteinCodingCoordinates { + count + rows { + uniprotAccessions + variant { id } + target { id approvedSymbol } + referenceAminoAcid + alternateAminoAcid + aminoAcidPosition + } + } + } + } + `, + extractExtraFetches: (data: unknown) => { + const rows = (data as any)?.variant?.proteinCodingCoordinates?.rows; + const uniprotId = rows?.[0]?.uniprotAccessions?.[0]; + if (!uniprotId) return []; + return [{ + url: `https://alphafold.ebi.ac.uk/files/AF-${uniprotId}-F1-model_v6.cif`, + contentType: "text/plain", + }]; + }, + }, +}; diff --git a/apps/mcp-widgets-server/src/widgets/types.ts b/apps/mcp-widgets-server/src/widgets/types.ts new file mode 100644 index 000000000..c32c7e8ec --- /dev/null +++ b/apps/mcp-widgets-server/src/widgets/types.ts @@ -0,0 +1,65 @@ +/** Descriptor for a single MCP widget tool — single source of truth for registration. */ +export type WidgetDef = { + /** MCP tool name, e.g. "get_l2g_widget" */ + toolName: string; + /** Human-readable description shown to the LLM */ + description: string; + /** The single string input this widget accepts */ + inputParam: { name: string; description: string }; + /** + * Multi-param variant of inputParam — canonical for section widgets and evidence tools. + * When present, all entries are registered as MCP tool input schema fields. + */ + inputParams?: Array<{ name: string; description: string }>; + /** URI prefix for the MCP resource, e.g. "ui://ot-mcp/l2g" */ + uriPrefix: string; + /** Filename of the IIFE bundle served from /widgets/, e.g. "l2g.js" */ + bundleFile: string; + /** text shown in the iframe document */ + title: string; + /** Message returned to Claude after the widget renders */ + successMessage: string; + /** + * When defined, the tool handler fetches this GraphQL query server-side and + * injects the result via a window.fetch interceptor in the widget HTML. + * This is required for Claude Desktop where the sandboxed iframe cannot make + * external network requests. + */ + prefetch?: { + /** Full GraphQL query string (must include the operationName) */ + query: string; + /** Apollo operationName — must match the name in the query string */ + operationName: string; + /** Extra static variables merged alongside the main inputParam (e.g. pagination) */ + extraVariables?: Record<string, unknown>; + /** + * Additional queries to run after the primary query. + * Variables can depend on the primary query's result (e.g. diseaseIds from studyId). + */ + extraPrefetches?: Array<{ + query: string; + operationName: string; + /** Compute variables from the raw input value and the primary query's data */ + variables: (inputValue: string, primaryData: unknown) => Record<string, unknown>; + /** + * When set, the cached data for this operation is an array of items. + * The interceptor filters by matching item[itemIdField] against the + * widget's actual request variable[requestVarName], then returns + * { [responseKey]: filteredItems }. + * Used for on-demand detail queries (e.g. ClinicalRecordsQuery). + */ + filteredBy?: { + requestVarName: string; + itemIdField: string; + responseKey: string; + }; + }>; + /** + * Optional: derive additional URLs to fetch server-side from the GraphQL data. + * Results are injected into the HTML fetch interceptor by exact URL match. + * Useful for binary/text assets (e.g. AlphaFold CIF files) that would be + * blocked from the sandboxed iframe. + */ + extractExtraFetches?: (data: unknown) => Array<{ url: string; contentType: string }>; + }; +}; diff --git a/apps/mcp-widgets-server/tsconfig.json b/apps/mcp-widgets-server/tsconfig.json new file mode 100644 index 000000000..7cde0f64e --- /dev/null +++ b/apps/mcp-widgets-server/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "jsx": "react-jsx", + "outDir": "dist", + "rootDir": "." + }, + "include": ["src", "widget-src", "vite"] +} diff --git a/apps/mcp-widgets-server/turbo.json b/apps/mcp-widgets-server/turbo.json new file mode 100644 index 000000000..a74bbf269 --- /dev/null +++ b/apps/mcp-widgets-server/turbo.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://turborepo.org/schema.json", + "extends": ["//"], + "tasks": { + "build": { + "outputs": ["dist/widgets/**"] + } + } +} diff --git a/apps/mcp-widgets-server/vite/section-widget.plugin.ts b/apps/mcp-widgets-server/vite/section-widget.plugin.ts new file mode 100644 index 000000000..3adf0c3d0 --- /dev/null +++ b/apps/mcp-widgets-server/vite/section-widget.plugin.ts @@ -0,0 +1,99 @@ +/** + * Vite plugin and config factory for section-based MCP widgets. + * + * Instead of per-widget main.tsx files, this generates the widget entry point + * and writes it to a temp file that Vite can use as a lib entry. + * + * Usage in the build script (iterates all sections for an entity): + * await build(createSectionWidgetConfig(def, { emptyOutDir: i === 0 })) + */ +import { resolve } from "path"; +import { writeFileSync, mkdirSync } from "node:fs"; +import { tmpdir } from "node:os"; +import type { UserConfig } from "vite"; +import { + ROOT, + createWidgetBuildConfig, + createUiBarrelStub, + createPlatformStubsPlugin, +} from "./widget.config.base"; +import type { SectionDef } from "../src/sections/registry"; +import { sectionPathToId } from "../src/widgets/index.js"; + +/** Generates the IIFE entry code for a section widget. */ +function generateEntryCode(def: SectionDef): string { + const paramNames = def.inputParams.map(p => p.name); + const isEvidence = def.entity === "evidence"; + + const extractChecks = paramNames + .map(n => `typeof args[${JSON.stringify(n)}] === "string"`) + .join(" && "); + const extractReturn = + "{ " + paramNames.map(n => `${JSON.stringify(n)}: args[${JSON.stringify(n)}]`).join(", ") + " }"; + + const bodyId = isEvidence + ? `{ ensgId: props[${JSON.stringify(paramNames[0])}], efoId: props[${JSON.stringify(paramNames[1])}] }` + : `props[${JSON.stringify(paramNames[0])}]`; + const bodyLabel = `props[${JSON.stringify(paramNames[0])}]`; + + const sectionId = sectionPathToId(def.sectionPath); + + return ` +import React from "react"; +import Body from "@ot/sections/${def.sectionPath}/Body"; +import { mountWidget } from "@widget-shared/createWidgetEntry"; + +mountWidget({ + appName: ${JSON.stringify("ot-section-" + sectionId)}, + cacheKey: ${JSON.stringify("ot-" + sectionId)}, + extractArgs: function(args) { + if (!args) return null; + if (!(${extractChecks})) return null; + return ${extractReturn}; + }, + component: function WidgetBody(props) { + return React.createElement(Body, { + id: ${bodyId}, + label: ${bodyLabel}, + entity: ${JSON.stringify(def.entity === "evidence" ? "disease" : def.entity)} + }); + } +}); +`.trim(); +} + +/** Writes entry code to a temp .tsx file and returns the path. */ +function writeTempEntry(def: SectionDef): string { + const sectionId = sectionPathToId(def.sectionPath); + const dir = resolve(tmpdir(), "ot-mcp-section-entries"); + mkdirSync(dir, { recursive: true }); + const filePath = resolve(dir, `${sectionId}.tsx`); + writeFileSync(filePath, generateEntryCode(def), "utf-8"); + return filePath; +} + +/** Creates a complete Vite build config for one section widget. */ +export function createSectionWidgetConfig( + def: SectionDef, + opts?: { emptyOutDir?: boolean } +): UserConfig { + const sectionId = sectionPathToId(def.sectionPath); + const entryPath = writeTempEntry(def); + + // PascalCase IIFE global name: "target-tractability" -> "OtTargetTractabilityWidget" + const iifeName = + "Ot" + + sectionId + .split("-") + .map(s => s.charAt(0).toUpperCase() + s.slice(1)) + .join("") + + "Widget"; + + return createWidgetBuildConfig({ + entry: entryPath, + outputName: iifeName, + outputFile: `${sectionId}.js`, + emptyOutDir: opts?.emptyOutDir ?? false, + plugins: [createUiBarrelStub(), createPlatformStubsPlugin()], + }); +} diff --git a/apps/mcp-widgets-server/vite/widget.config.base.ts b/apps/mcp-widgets-server/vite/widget.config.base.ts new file mode 100644 index 000000000..0d996c828 --- /dev/null +++ b/apps/mcp-widgets-server/vite/widget.config.base.ts @@ -0,0 +1,241 @@ +import { defineConfig, loadEnv } from "vite"; +import type { UserConfig, Plugin } from "vite"; +import react from "@vitejs/plugin-react"; +import { resolve } from "path"; +import { fileURLToPath } from "node:url"; + +const __dirname = fileURLToPath(new URL(".", import.meta.url)); + +/** Absolute path to the package root (apps/mcp-widgets-server). */ +export const ROOT = resolve(__dirname, ".."); + +/** Monorepo root (two levels up from apps/mcp-widgets-server). */ +const MONO_ROOT = resolve(ROOT, "../.."); + +const PUBLIC_API_URL = "https://api.platform.opentargets.org/api/v4/graphql"; + +/** + * Load OT_API_URL from .env at build time (empty prefix = load all vars). + * Falls back to the public production endpoint. + */ +const { OT_API_URL = PUBLIC_API_URL } = loadEnv("production", ROOT, ""); + +const BASE_DEDUPE = [ + "react", + "react-dom", + "@emotion/react", + "@emotion/styled", + "@emotion/cache", + "@emotion/sheet", + "@emotion/utils", + "@emotion/serialize", + "@mui/material", + "@mui/system", + "@apollo/client", + "graphql", + "react-router-dom", + "lodash", +]; + +export interface WidgetBuildOptions { + /** Absolute path to the widget entry point (main.tsx). */ + entry: string; + /** IIFE global variable name, e.g. "L2GWidget". */ + outputName: string; + /** Output filename under dist/widgets/, e.g. "l2g.js". */ + outputFile: string; + /** + * Set to true for the first widget in the build chain so dist/widgets/ is + * cleaned before the fresh build. All subsequent widgets use false to avoid + * wiping previously built bundles. + */ + emptyOutDir?: boolean; + /** Widget-specific Vite plugins (stub plugins go here). */ + plugins: Plugin[]; + /** Extra entries added to the dedupe list, e.g. ["3dmol"]. */ + extraDedupe?: string[]; +} + +/** + * Shared Vite plugin that stubs platform-specific paths which would otherwise + * pull in heavy dependencies (graphiql CSS, OT-specific Apollo context). + * + * - DataDownloader.jsx → null component (prevents graphiql CSS from landing in bundle) + * - OTApolloProvider.tsx → re-exports useApolloClient from standard @apollo/client + * so that useBatchQuery works under the standard ApolloProvider we provide in + * createWidgetEntry.tsx + */ +/** + * Creates the ui barrel stub plugin for a widget build. + * + * Uses both `resolveId` (intercepts bare "ui" imports) and `load` (intercepts + * the real resolved package path via symlinks) so the stub is applied in all + * cases. Without the `load` hook the real SectionItem with full section chrome + * (title, description, download controls) leaks into the bundle. + */ +export function createUiBarrelStub(stubFile = "widget-src/shared/stubs/ui-index.tsx"): Plugin { + const stubPath = resolve(ROOT, stubFile); + const uiIndexPath = resolve(ROOT, "../../packages/ui/src/index.tsx"); + const nodeModulesUiIndex = resolve(ROOT, "node_modules/ui/src/index.tsx"); + return { + name: `stub-ui-barrel`, + resolveId(id: string) { + if (id === "ui") return stubPath; + }, + load(id: string) { + if (id === uiIndexPath || id === nodeModulesUiIndex) { + return `export * from ${JSON.stringify(stubPath)};`; + } + }, + }; +} + +export function createPlatformStubsPlugin(): Plugin { + const dataDownloaderPath = resolve( + MONO_ROOT, + "packages/ui/src/components/DataDownloader.tsx" + ); + const otApolloProviderPath = resolve( + MONO_ROOT, + "packages/ui/src/providers/OTApolloProvider/OTApolloProvider.tsx" + ); + // OtAsyncTooltip imports OtGenomicLocation from the ui barrel, but that + // component does not exist yet in the codebase. Stub the whole component + // so widgets that transitively import it (via OtTable etc.) don't break. + const otAsyncTooltipPath = resolve( + MONO_ROOT, + "packages/ui/src/components/OtAsyncTooltip/OtAsyncTooltip.tsx" + ); + // ApiPlaygroundDrawer dynamically imports "graphiql" which is not available + // in the widget build environment. Stub it out as a no-op component. + const apiPlaygroundDrawerPath = resolve( + MONO_ROOT, + "packages/ui/src/components/ApiPlaygroundDrawer.tsx" + ); + // @ot/config's theme.ts calls lighten/darken (polished) on getConfig() colors at + // module-load time. In the widget sandbox window.configProfile is absent so + // primaryColor/secondaryColor are undefined → polished throws error #3. + // Stub both files by absolute path (same pattern as OTApolloProvider/DataDownloader) + // so polished is never invoked regardless of how the module is resolved. + const otConfigThemePath = resolve(MONO_ROOT, "packages/ot-config/src/theme.ts"); + const otConfigEnvPath = resolve(MONO_ROOT, "packages/ot-config/src/environment.ts"); + + return { + name: "platform-stubs", + load(id: string) { + if (id === otConfigThemePath) { + return ` +import { createTheme } from "@mui/material"; +const PRIMARY = "#3489ca"; +const SECONDARY = "#ff6350"; +export const theme = createTheme({ + palette: { primary: { main: PRIMARY }, secondary: { main: SECONDARY } }, +}); +`; + } + if (id === otConfigEnvPath) { + const PRIMARY = "#3489ca"; + const SECONDARY = "#ff6350"; + return ` +const PRIMARY = "${PRIMARY}"; +const SECONDARY = "${SECONDARY}"; +export function getConfig() { + return { + urlApi: ${JSON.stringify(OT_API_URL)}, + urlAiApi: "", + profile: { primaryColor: PRIMARY, secondaryColor: SECONDARY, isPartnerPreview: false, partnerTargetSectionIds: [], partnerDiseaseSectionIds: [], partnerDrugSectionIds: [], partnerEvidenceSectionIds: [], partnerDataTypes: [], partnerDataSources: [] }, + googleTagManagerID: null, + geneticsPortalUrl: "https://genetics.opentargets.org", + gitVersion: "", + }; +} +export function getEnvironmentConfig() { return getConfig(); } +`; + } + if (id === otAsyncTooltipPath) { + return ` +import React from "react"; +export default function OtAsyncTooltip({ children }) { return React.createElement(React.Fragment, null, children); } +`; + } + if (id === apiPlaygroundDrawerPath) { + return "export default function ApiPlaygroundDrawer() { return null; }"; + } + if (id === dataDownloaderPath) { + return "export default function DataDownloader() { return null; }"; + } + if (id === otApolloProviderPath) { + // useBatchQuery calls useApolloClient() from this module. + // Re-export from @apollo/client so standard ApolloProvider satisfies it. + // OTApolloProvider itself is a no-op here (we provide ApolloProvider in createWidgetEntry). + return ` +export { useApolloClient } from "@apollo/client"; +export function OTApolloProvider({ children }) { return children; } +`; + } + }, + }; +} + +/** + * Transforms .gql/.graphql files into importable DocumentNode objects. + * Replicates vite-plugin-simple-gql inline to avoid CJS/ESM interop issues. + * The emitted code uses graphql-tag (re-exported by @apollo/client) so the + * parsed document is cached and de-duplicated across imports. + */ +function gqlPlugin(): Plugin { + return { + name: "gql-transform", + transform(src: string, id: string) { + if (id.endsWith(".graphql") || id.endsWith(".gql")) { + return { + code: `import { gql } from "@apollo/client"; export default gql(${JSON.stringify(src)});`, + map: null, + }; + } + }, + }; +} + +export function createWidgetBuildConfig(opts: WidgetBuildOptions): UserConfig { + return defineConfig({ + plugins: [...opts.plugins, gqlPlugin(), react()], + build: { + lib: { + entry: opts.entry, + formats: ["iife"], + name: opts.outputName, + }, + outDir: resolve(ROOT, "dist/widgets"), + emptyOutDir: opts.emptyOutDir ?? false, + rollupOptions: { + output: { + entryFileNames: opts.outputFile, + // Inline all assets and dynamic imports into the single IIFE file. + inlineDynamicImports: true, + }, + }, + }, + define: { + // Replace Node.js global so the IIFE bundle works in the browser. + "process.env.NODE_ENV": '"production"', + // Bake the GraphQL endpoint into the bundle so it's used even when the + // inline window.__OT_API_URL__ script is blocked by CSP. + __OT_API_URL__: JSON.stringify(OT_API_URL), + }, + resolve: { + // Deduplicate across the monorepo so each package has only one instance + // in the bundle. Emotion packages must be deduplicated to ensure + // CacheContext is shared between CacheProvider and @emotion/styled consumers. + dedupe: [...BASE_DEDUPE, ...(opts.extraDedupe ?? [])], + alias: { + // Allow widget-src to import directly from monorepo packages. + "@ot/ui": resolve(MONO_ROOT, "packages/ui/src"), + "@ot/sections": resolve(MONO_ROOT, "packages/sections/src"), + "@ot/constants": resolve(MONO_ROOT, "packages/ot-constants/src"), + "@ot/utils": resolve(MONO_ROOT, "packages/ot-utils/src"), + "@widget-shared": resolve(ROOT, "widget-src/shared"), + }, + }, + }); +} diff --git a/apps/mcp-widgets-server/vite/widget.config.ms.ts b/apps/mcp-widgets-server/vite/widget.config.ms.ts new file mode 100644 index 000000000..6f5502b08 --- /dev/null +++ b/apps/mcp-widgets-server/vite/widget.config.ms.ts @@ -0,0 +1,10 @@ +import { resolve } from "path"; +import { createWidgetBuildConfig, createPlatformStubsPlugin, createUiBarrelStub, ROOT } from "./widget.config.base"; + +export default createWidgetBuildConfig({ + entry: resolve(ROOT, "widget-src/molecular-structure/main.tsx"), + outputName: "MolecularStructureWidget", + outputFile: "molecular-structure.js", + extraDedupe: ["3dmol"], + plugins: [createUiBarrelStub("widget-src/shared/stubs/ui-ms-index.tsx"), createPlatformStubsPlugin()], +}); diff --git a/apps/mcp-widgets-server/widget-src/molecular-structure/MolecularStructureWidget.tsx b/apps/mcp-widgets-server/widget-src/molecular-structure/MolecularStructureWidget.tsx new file mode 100644 index 000000000..c28738301 --- /dev/null +++ b/apps/mcp-widgets-server/widget-src/molecular-structure/MolecularStructureWidget.tsx @@ -0,0 +1,5 @@ +import Body from "@ot/sections/variant/MolecularStructure/Body"; + +export default function MolecularStructureWidget({ variantId }: { variantId: string }) { + return <Body id={variantId} entity="variant" />; +} diff --git a/apps/mcp-widgets-server/widget-src/molecular-structure/main.tsx b/apps/mcp-widgets-server/widget-src/molecular-structure/main.tsx new file mode 100644 index 000000000..c5c14a404 --- /dev/null +++ b/apps/mcp-widgets-server/widget-src/molecular-structure/main.tsx @@ -0,0 +1,12 @@ +import { mountWidget } from "../shared/createWidgetEntry"; +import MolecularStructureWidget from "./MolecularStructureWidget"; + +mountWidget({ + appName: "ot-molecular-structure-widget", + cacheKey: "ot-ms", + extractArgs: args => { + const variantId = args?.variantId; + return typeof variantId === "string" ? { variantId } : null; + }, + component: MolecularStructureWidget, +}); diff --git a/apps/mcp-widgets-server/widget-src/shared/createWidgetEntry.tsx b/apps/mcp-widgets-server/widget-src/shared/createWidgetEntry.tsx new file mode 100644 index 000000000..636ffb094 --- /dev/null +++ b/apps/mcp-widgets-server/widget-src/shared/createWidgetEntry.tsx @@ -0,0 +1,285 @@ +/** + * Factory that wires up the App class, fetch interceptor, ResizeObserver + * height reporting, Emotion cache, and MUI theme for a widget IIFE bundle. + * + * Data delivery pattern (per MCP Apps spec examples): + * ontoolinput fires → app.callServerTool("ot_fetch_widget_data") → + * read result.structuredContent → populate fetch interceptor cache → + * Apollo queries resolve from cache. + * + * app.ontoolresult is kept as a primary path for when Claude Desktop fixes + * the structuredContent stripping bug (#696 in ext-apps). + * + * autoResize: false — we report only HEIGHT manually so AppFrame never sets a + * pixel width on the proxy iframe (which would collapse the layout to the + * document's current pixel width and trigger a feedback loop). + */ +import React, { useState } from "react"; +import { createRoot } from "react-dom/client"; +import { App } from "@modelcontextprotocol/ext-apps"; +import { ThemeProvider, CssBaseline } from "@mui/material"; +import createCache from "@emotion/cache"; +import { CacheProvider } from "@emotion/react"; +import { ApolloClient, InMemoryCache, ApolloProvider } from "@apollo/client"; +import { MemoryRouter } from "react-router-dom"; +import { theme } from "./theme"; + +// __OT_API_URL__ is injected at build time by Vite's define (from OT_API_URL in .env). +// window.__OT_API_URL__ can override it at runtime (set by the HTML shell script). +declare const __OT_API_URL__: string; + +const apolloClient = new ApolloClient({ + uri: (window as { __OT_API_URL__?: string }).__OT_API_URL__ ?? __OT_API_URL__, + cache: new InMemoryCache(), +}); + +// ── Prefetch data types ─────────────────────────────────────────────────────── + +interface FilteredOp { + operationName: string; + allItems: unknown[]; + itemIdField: string; + requestVarName: string; + responseKey: string; +} + +interface PrefetchResult { + operations?: Array<{ operationName: string; data: unknown }>; + filteredOps?: FilteredOp[]; + urlData?: Array<{ url: string; text: string; contentType: string }>; +} + +// ── Module-scope data store ─────────────────────────────────────────────────── +// Shared between the fetch interceptor and applyData. Module scope guarantees +// the interceptor is in place before Apollo Client makes its first request. + +const _gql: Record<string, unknown> = {}; +const _gqlPending: Record<string, Array<(data: unknown) => void>> = {}; +const _filteredOps: Record<string, FilteredOp> = {}; +let _urls: Array<{ url: string; text: string; contentType: string }> = []; + +function clearDataStore() { + for (const k of Object.keys(_gql)) delete _gql[k]; + for (const k of Object.keys(_gqlPending)) delete _gqlPending[k]; + for (const k of Object.keys(_filteredOps)) delete _filteredOps[k]; + _urls = []; +} + +function applyData(pf: PrefetchResult) { + _urls = pf.urlData ?? []; + + for (const fo of pf.filteredOps ?? []) { + _filteredOps[fo.operationName] = fo; + } + + for (const op of pf.operations ?? []) { + if (op.operationName === undefined) continue; + _gql[op.operationName] = op.data; + const waiting = _gqlPending[op.operationName]; + if (waiting) { + for (const resolve of waiting) resolve(op.data); + delete _gqlPending[op.operationName]; + } + } +} + +// ── window.fetch interceptor ────────────────────────────────────────────────── +// Must be set up at module scope (before any Apollo query runs) so every +// GraphQL request is routed through the prefetch cache. + +const _originalFetch = window.fetch.bind(window); + +window.fetch = function otFetchInterceptor( + url: RequestInfo | URL, + opts?: RequestInit +): Promise<Response> { + try { + // Cached URL assets (e.g. AlphaFold CIF files fetched by the 3D widget) + const urlStr = + typeof url === "string" ? url : url instanceof URL ? url.href : (url as Request).url; + const urlEntry = _urls.find(e => e.url === urlStr); + if (urlEntry) { + return Promise.resolve( + new Response(urlEntry.text, { + status: 200, + headers: { "Content-Type": urlEntry.contentType }, + }) + ); + } + + // GraphQL POST requests + if (opts?.method === "POST" && typeof opts.body === "string") { + const body = JSON.parse(opts.body) as { operationName?: string; variables?: Record<string, unknown> }; + const op = body.operationName; + if (op) { + // Filtered operation — server prefetches all items; interceptor filters by request vars + const fo = _filteredOps[op]; + if (fo) { + const reqIds = ((body.variables ?? {})[fo.requestVarName] ?? []) as unknown[]; + const idSet = new Set(reqIds); + const filtered = (fo.allItems as Record<string, unknown>[]).filter( + item => idSet.has(item[fo.itemIdField]) + ); + const data: Record<string, unknown> = {}; + data[fo.responseKey] = filtered; + return Promise.resolve( + new Response(JSON.stringify({ data }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }) + ); + } + + // Already cached + if (_gql[op] !== undefined) { + return Promise.resolve( + new Response(JSON.stringify({ data: _gql[op] }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }) + ); + } + + // Pend until applyData populates this operation + return new Promise((resolve, reject) => { + if (!_gqlPending[op]) _gqlPending[op] = []; + _gqlPending[op].push(data => + resolve( + new Response(JSON.stringify({ data }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }) + ) + ); + setTimeout(() => reject(new Error(`OT data timeout for ${op}`)), 30000); + }); + } + } + } catch (_) {} + + return _originalFetch(url as RequestInfo, opts); +}; + +// ── Widget entry factory ────────────────────────────────────────────────────── + +export interface WidgetEntryConfig<TArgs extends Record<string, unknown>> { + /** MCP app name reported to the host, e.g. "ot-l2g-widget" */ + appName: string; + /** Emotion cache key — must be unique per widget to avoid style conflicts */ + cacheKey: string; + /** Extract typed args from the raw ontoolinput arguments object, or null if incomplete */ + extractArgs: (args: Record<string, unknown>) => TArgs | null; + /** The widget React component to render once args are received */ + component: React.ComponentType<TArgs>; +} + +export function mountWidget<TArgs extends Record<string, unknown>>( + config: WidgetEntryConfig<TArgs> +): void { + const emotionCache = createCache({ + key: config.cacheKey, + container: document.head, + speedy: false, + }); + + const app = new App({ name: config.appName, version: "0.1.0" }, {}, { autoResize: false }); + + function Root() { + const [args, setArgs] = useState<TArgs | null>(null); + + React.useEffect(() => { + let observer: ResizeObserver | null = null; + + async function connect() { + try { + // ontoolresult: primary path for when Claude Desktop fixes #696 + app.ontoolresult = result => { + if (result.structuredContent) { + applyData(result.structuredContent as PrefetchResult); + } + }; + + // ontoolinput: set BEFORE connect() so we never miss the initial event. + // Calls callServerTool to fetch prefetched data (request/response path, + // not a notification — avoids the structuredContent stripping bug #696). + app.ontoolinput = async ({ arguments: rawArgs }) => { + clearDataStore(); + + const extracted = config.extractArgs( + (rawArgs ?? {}) as Record<string, unknown> + ); + if (extracted) setArgs(extracted); + + const toolName = ( + window as { __OT_WIDGET_TOOL__?: string } + ).__OT_WIDGET_TOOL__; + + if (toolName) { + try { + const result = await app.callServerTool({ + name: "ot_fetch_widget_data", + arguments: { + tool: toolName, + inputJson: JSON.stringify(rawArgs ?? {}), + }, + }); + if (result.structuredContent) { + applyData(result.structuredContent as PrefetchResult); + } + } catch (e) { + console.error(`[${config.appName}] callServerTool failed:`, e); + } + } + }; + + await app.connect(); + + const sendHeight = () => { + const h = Math.max(document.documentElement.scrollHeight, 50); + app.sendSizeChanged({ height: h }).catch(() => {}); + }; + + observer = new ResizeObserver(sendHeight); + observer.observe(document.documentElement); + sendHeight(); + } catch (err) { + console.error(`[${config.appName}] AppBridge connect failed:`, err); + } + } + + connect(); + return () => { + observer?.disconnect(); + }; + }, []); + + if (!args) { + return ( + <div style={{ padding: "24px", color: "#718096", fontFamily: "sans-serif" }}> + Connecting… + </div> + ); + } + + const Widget = config.component; + return ( + <ApolloProvider client={apolloClient}> + <MemoryRouter> + <Widget {...args} /> + </MemoryRouter> + </ApolloProvider> + ); + } + + const rootEl = document.getElementById("root"); + if (rootEl) { + createRoot(rootEl).render( + <CacheProvider value={emotionCache}> + <ThemeProvider theme={theme}> + <CssBaseline /> + <Root /> + </ThemeProvider> + </CacheProvider> + ); + } +} diff --git a/apps/mcp-widgets-server/widget-src/shared/stubs/ui-index.tsx b/apps/mcp-widgets-server/widget-src/shared/stubs/ui-index.tsx new file mode 100644 index 000000000..80566917f --- /dev/null +++ b/apps/mcp-widgets-server/widget-src/shared/stubs/ui-index.tsx @@ -0,0 +1,290 @@ +/** + * Stubs for platform-specific ui package exports. + * + * HeatmapTable (and HeatmapLegend) import from the "ui" barrel index + * which pulls in React Router, OtAsyncTooltip, and other platform deps + * that don't make sense in a standalone iframe widget. + * + * This stub file replaces those imports at build time (via the stubUiBarrel + * Vite plugin in vite.widget.config.ts). + */ +import type { ReactNode } from "react"; + +/** Link: opens OT platform pages in a new tab, or follows external URLs */ +export function Link({ + to, + children, +}: { + to?: string; + children?: ReactNode; + external?: boolean; + asyncTooltip?: boolean; + newTab?: boolean; + footer?: boolean; + tooltip?: unknown; + className?: string; + ariaLabel?: string; + onClick?: () => void; +}) { + const href = + !to ? "#" : to.startsWith("/") ? `https://platform.opentargets.org${to}` : to; + return ( + <a href={href} target="_blank" rel="noreferrer"> + {children} + </a> + ); +} + +/** DataDownloader: disabled in the widget (no export button) */ +export function DataDownloader() { + return null; +} + +// Real ObsPlot / ObsChart / ObsTooltip — needed for waterfall charts in cell +// popovers and detail modals. Imported directly (bypass barrel). +export { default as ObsPlot } from "@ot/ui/components/ObsPlot/ObsPlot"; +export { default as ObsChart } from "@ot/ui/components/ObsPlot/ObsChart"; +export { default as ObsTooltip } from "@ot/ui/components/ObsPlot/ObsTooltip"; + +/** Tooltip: renders children only (no help icon / tooltip popup) */ +export function Tooltip({ children }: { children?: ReactNode; [key: string]: unknown }) { + return <>{children}</>; +} + +/** OtAsyncTooltip: renders children only */ +export function OtAsyncTooltip({ children }: { children?: ReactNode; [key: string]: unknown }) { + return <>{children}</>; +} + +/** TooltipTable: renders a table container for tooltip content */ +export function TooltipTable({ children }: { children?: ReactNode }) { + return ( + <table style={{ borderSpacing: "0 0.4rem", width: "100%" }}> + <tbody>{children}</tbody> + </table> + ); +} + +/** TooltipRow: renders a labeled row for tooltip content */ +export function TooltipRow({ + label, + children, +}: { + label?: string; + children?: ReactNode; + valueWidth?: number; + truncateValue?: boolean; +}) { + return ( + <tr> + {label !== undefined && ( + <td + style={{ + fontSize: 13, + fontWeight: 600, + paddingRight: 8, + verticalAlign: "top", + whiteSpace: "nowrap", + color: "#718096", + }} + > + {label}: + </td> + )} + <td>{children}</td> + </tr> + ); +} + +/** ScientificNotation: renders a number in scientific notation */ +export function ScientificNotation({ + number, + dp = 2, +}: { + number?: number | [number, number]; + dp?: number; + [key: string]: unknown; +}) { + if (number == null) return null; + if (Array.isArray(number)) { + const [mantissa, exponent] = number; + return ( + <span> + {mantissa.toFixed(dp)} × 10<sup>{exponent}</sup> + </span> + ); + } + if (number === 0) return <span>0</span>; + const exp = Math.floor(Math.log10(Math.abs(number))); + const mant = (number / Math.pow(10, exp)).toFixed(dp); + return ( + <span> + {mant} × 10<sup>{exp}</sup> + </span> + ); +} + +/** DisplayVariantId: shows a short variant ID label */ +export function DisplayVariantId({ + variantId, + referenceAllele, + alternateAllele, + maxChars = 6, +}: { + variantId?: string; + referenceAllele?: string; + alternateAllele?: string; + expand?: boolean; + maxChars?: number; +}) { + if (!variantId) return null; + const ref = referenceAllele && referenceAllele.length > maxChars ? "DEL" : referenceAllele; + const alt = alternateAllele && alternateAllele.length > maxChars ? "INS" : alternateAllele; + const display = ref && alt ? `${ref}/${alt}` : variantId; + return <span>{display}</span>; +} + +/** Navigate: renders a "View →" link to an OT platform page */ +export function Navigate({ to }: { to?: string }) { + const href = !to ? "#" : to.startsWith("/") ? `https://platform.opentargets.org${to}` : to; + return ( + <a + href={href} + target="_blank" + rel="noreferrer" + style={{ + display: "inline-flex", + alignItems: "center", + gap: 4, + textDecoration: "none", + color: "inherit", + }} + > + View → + </a> + ); +} + +/** ClinvarStars: renders a star rating (★ filled, ☆ empty) */ +export function ClinvarStars({ num = 0, length = 4 }: { num?: number; length?: number }) { + const stars = Array.from({ length }, (_, i) => (i < num ? "★" : "☆")).join(""); + return <span style={{ color: "#FFC107", letterSpacing: 1 }}>{stars}</span>; +} + +/** L2GScoreIndicator: renders the L2G score as tabular text */ +export function L2GScoreIndicator({ + score, +}: { + score?: number; + studyLocusId?: string; + targetId?: string; +}) { + if (score == null) return null; + return <span style={{ fontVariantNumeric: "tabular-nums" }}>{score.toFixed(3)}</span>; +} + +/** SummaryItem: not used in widget context — no-op stub */ +export function SummaryItem() { + return null; +} + +/** SectionItem: real component — shows full section chrome (title, description, body) */ +export { default as SectionItem } from "@ot/ui/components/Section/SectionItem"; + +/** HeatmapTable: real component from @ot/ui (direct path, bypassing barrel) */ +export { default as HeatmapTable } from "@ot/ui/components/HeatmapTable/HeatmapTable"; + +/** OtTable: real component from @ot/ui (direct path, bypassing barrel) */ +export { default as OtTable } from "@ot/ui/components/OtTable/OtTable"; + +/** useBatchQuery: real hook from @ot/ui (direct path, bypassing barrel) */ +export { default as useBatchQuery } from "@ot/ui/hooks/useBatchQuery"; + +/** Clinical Indications components — direct paths bypass the ui barrel */ +export { default as useClinicalReportsMasterDetail } from "@ot/ui/hooks/useClinicalReportsMasterDetail"; +export { default as RecordsCards } from "@ot/ui/components/ClinicalReports/RecordsCards"; +export { default as ClinicalReportsMasterDetailFrame } from "@ot/ui/components/ClinicalReports/ClinicalReportsMasterDetailFrame"; +export { default as ClinicalRecordDrawer } from "@ot/ui/components/ClinicalReports/ClinicalRecordDrawer"; + +/** usePlatformApi: returns null — fragments are not needed inside the widget */ +export function usePlatformApi() { + return null; +} + +/** useApolloClient: standard Apollo client from the widget's ApolloProvider */ +export { useApolloClient } from "@apollo/client"; + +// ── Real components (direct paths, bypass barrel) ──────────────────────────── +export { default as ChipList } from "@ot/ui/components/ChipList"; +export { default as SectionLoader } from "@ot/ui/components/Section/SectionLoader"; +export { default as DirectionOfEffectIcon } from "@ot/ui/components/DirectionOfEffectIcon"; +export { default as DirectionOfEffectTooltip } from "@ot/ui/components/DirectionOfEffectTooltip"; +export { default as OtTableSSP } from "@ot/ui/components/OtTable/OtTableSSP"; +export { default as DataTable } from "@ot/ui/components/Table/DataTable"; +export { default as Table } from "@ot/ui/components/Table/Table"; +export { default as TableDrawer } from "@ot/ui/components/Table/TableDrawer"; +export { default as DirectionalityDrawer } from "@ot/ui/components/DirectionalityDrawer"; +export { default as EllsWrapper } from "@ot/ui/components/EllsWrapper"; +export { default as ErrorBoundary } from "@ot/ui/components/ErrorBoundary"; +/** FacetsSelect: no-op stub (only used in excluded sections) */ +export function FacetsSelect() { + return null; +} +export { default as useDebounce } from "@ot/ui/hooks/useDebounce"; +export { default as LabelChip } from "@ot/ui/components/LabelChip"; +export { default as Legend } from "@ot/ui/components/Legend"; +export { default as LongText } from "@ot/ui/components/LongText"; +export { default as MouseModelAllelicComposition } from "@ot/ui/components/MouseModelAllelicComposition"; +export { default as TooltipStyledLabel } from "@ot/ui/components/TooltipStyledLabel"; +export { PaginationActionsComplete } from "@ot/ui/components/Table/TablePaginationActions"; +export { getPage } from "@ot/ui/components/Table/utils"; +export { default as PublicationSummaryLabel } from "@ot/ui/components/PublicationsDrawer/PublicationSummaryLabel"; +export { default as PublicationWrapper } from "@ot/ui/components/PublicationsDrawer/PublicationWrapper"; +export { default as SummaryLoader } from "@ot/ui/components/PublicationsDrawer/SummaryLoader"; +export { default as useBatchDownloader } from "@ot/ui/hooks/useBatchDownloader"; +export { useConfigContext } from "@ot/ui/providers/ConfigurationProvider"; + +// ── Viewer stubs (3D viewer sections are excluded; these prevent missing-export errors) ─ +export function ViewerProvider({ children }: { children: React.ReactNode }) { return <>{children}</>; } +export function ViewerInteractionProvider({ children }: { children: React.ReactNode }) { return <>{children}</>; } +export function useViewerState() { return {}; } +export function useViewerDispatch() { return () => {}; } +export function useViewerInteractionState() { return {}; } +export function useViewerInteractionDispatch() { return () => {}; } +export function ViewerLegend() { return null; } +export function DetailPopover({ children }: { children?: React.ReactNode }) { return <>{children}</>; } + +/** DownloadSvgPlot: renders the plot content, omits download controls */ +export function DownloadSvgPlot({ + children, + center, +}: { + children?: React.ReactNode; + center?: React.ReactNode; + [key: string]: unknown; +}) { + return ( + <> + {center} + {children} + </> + ); +} + +/** PublicationsDrawer: renders a simple link to the first publication entry */ +export function PublicationsDrawer({ + entries, + customLabel, +}: { + entries?: { name?: string; url?: string }[]; + customLabel?: string; + [key: string]: unknown; +}) { + const first = entries?.[0]; + if (!first?.url) return null; + return ( + <a href={first.url} target="_blank" rel="noreferrer"> + {customLabel ?? first.name ?? first.url} + </a> + ); +} diff --git a/apps/mcp-widgets-server/widget-src/shared/stubs/ui-ms-index.tsx b/apps/mcp-widgets-server/widget-src/shared/stubs/ui-ms-index.tsx new file mode 100644 index 000000000..0d6bf81d4 --- /dev/null +++ b/apps/mcp-widgets-server/widget-src/shared/stubs/ui-ms-index.tsx @@ -0,0 +1,61 @@ +import React from "react"; + +// ---- Viewer providers and hooks (real — context must be shared across all components) ---- +export { + ViewerProvider, + useViewerState, + useViewerDispatch, +} from "@ot/ui/providers/ViewerProvider"; +export { + ViewerInteractionProvider, + useViewerInteractionState, + useViewerInteractionDispatch, +} from "@ot/ui/providers/ViewerInteractionProvider"; + +// ---- Real viewer components (direct path imports) ---- +export { default as Viewer } from "@ot/ui/components/Viewer/Viewer"; +export { default as ViewerRadios } from "@ot/ui/components/Viewer/ViewerRadios"; +export { default as ViewerDropdown } from "@ot/ui/components/Viewer/ViewerDropdown"; +export { default as ViewerLegend } from "@ot/ui/components/Viewer/ViewerLegend"; +export { default as DetailPopover } from "@ot/ui/components/DetailPopover"; +export { default as CompactAlphaFoldLegend } from "@ot/ui/components/Viewer/CompactAlphaFoldLegend"; +export { default as CompactAlphaFoldPathogenicityLegend } from "@ot/ui/components/Viewer/CompactAlphaFoldPathogenicityLegend"; +export { default as CompactAlphaFoldDomainLegend } from "@ot/ui/components/Viewer/CompactAlphaFoldDomainLegend"; +export { default as CompactAlphaFoldHydrophobicityLegend } from "@ot/ui/components/Viewer/CompactAlphaFoldHydrophobicityLegend"; + +// ---- Shared widget stubs (reused from the main ui stub) ---- +export { SectionItem, SummaryItem, DisplayVariantId, usePlatformApi, useBatchQuery } from "./ui-index"; + +// ---- Stubs ---- + +export function Link({ children, to }: { children: React.ReactNode; to: string }) { + return ( + <a href={to} target="_blank" rel="noopener noreferrer"> + {children} + </a> + ); +} + +export function DataDownloader() { + return null; +} +export function ObsPlot() { + return null; +} +export function ObsChart() { + return null; +} +export function ObsTooltip() { + return null; +} +export function Tooltip({ children }: { children: React.ReactNode }) { + return <>{children}</>; +} +export function OtAsyncTooltip({ children }: { children: React.ReactNode }) { + return <>{children}</>; +} + +// Sequence track — not needed in the standalone widget +export function ViewerTrack() { + return null; +} diff --git a/apps/mcp-widgets-server/widget-src/shared/theme.ts b/apps/mcp-widgets-server/widget-src/shared/theme.ts new file mode 100644 index 000000000..0fe7c191c --- /dev/null +++ b/apps/mcp-widgets-server/widget-src/shared/theme.ts @@ -0,0 +1,27 @@ +import { createTheme } from "@mui/material"; + +/** + * Shared MUI theme used by all widget IIFE bundles. + * + * Mirrors the platform's custom theme extensions (boxShadow, typography variants) + * so platform section components render without crashes when used as widgets. + */ +export const theme = createTheme({ + shape: { borderRadius: 2 }, + typography: { fontFamily: '"Inter", sans-serif' }, + palette: { + primary: { main: "#3489ca" }, + secondary: { main: "#ff6350" }, + text: { primary: "#5A5F5F" }, + }, + // Custom platform theme extensions — match packages/ot-config/src/theme.ts + // so any section component accessing theme.boxShadow etc. works correctly. + // @ts-ignore: not in MUI's default ThemeOptions type + boxShadow: { + value: "0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)", + sm: "0 1px 2px 0 rgb(0 0 0 / 0.05)", + default: "0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)", + md: "0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)", + lg: "0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)", + }, +}); diff --git a/package.json b/package.json index 52ffe2bbc..f5e4cd185 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,9 @@ "dev": "turbo run dev --parallel", "dev:platform": "turbo run dev --filter=platform", "build:platform": "turbo run build --filter=platform", + "dev:mcp-widgets-server": "turbo run dev --filter=mcp-widgets-server", + "dev:mcp-widgets-server:ppp": "turbo run dev-ppp --filter=mcp-widgets-server", + "build:mcp-widgets-server": "turbo run build --filter=mcp-widgets-server", "dev:docs": "turbo run dev --filter=docs", "build:docs": "turbo run build --filter=docs", "dev:platform:ppp": "turbo run dev-ppp --filter=platform", diff --git a/yarn.lock b/yarn.lock index 6e7cc4140..070a379e4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1682,19 +1682,19 @@ unixify "^1.0.0" "@graphql-tools/load@^8.1.0", "@graphql-tools/load@^8.1.8": - version "8.1.12" - resolved "https://registry.yarnpkg.com/@graphql-tools/load/-/load-8.1.12.tgz#d32907b0505a7f97704e3d62459d0a4d153bc988" - integrity sha512-3xDm8vzTb8DPXzN04K4AbWAIFrSwkDpISv+/YX8cd2xRbifFNVOu5tuhFK0chC1Wj6fJ8fThry+IV72hC41BiQ== + version "8.1.13" + resolved "https://registry.yarnpkg.com/@graphql-tools/load/-/load-8.1.13.tgz#c6a9a1369d4986af7fcc173ff22691ff5afbd807" + integrity sha512-XstLIkH2gDVk3xlzbfNyR+MFGTaCop2rJSEyShXTnIy4Sh+DoNM6re8eXzZPIOs+N33kuOWm6e0EGE9tGpHUNQ== dependencies: - "@graphql-tools/schema" "^10.0.35" + "@graphql-tools/schema" "^10.0.36" "@graphql-tools/utils" "^11.2.0" p-limit "3.1.0" tslib "^2.4.0" -"@graphql-tools/merge@^9.0.0", "@graphql-tools/merge@^9.0.6", "@graphql-tools/merge@^9.1.11": - version "9.1.11" - resolved "https://registry.yarnpkg.com/@graphql-tools/merge/-/merge-9.1.11.tgz#a39e4f1e7db7bc81d51f35f13ffe3084aac2335e" - integrity sha512-APuOIPBUm9NQQCl1qfKC39zclR1p8UVsEhCg9t9nYF3kSe859F9/wq4VmBqiz/ETuABS7Bm7WA+yDYWC6HoD8A== +"@graphql-tools/merge@^9.0.0", "@graphql-tools/merge@^9.0.6", "@graphql-tools/merge@^9.2.0": + version "9.2.0" + resolved "https://registry.yarnpkg.com/@graphql-tools/merge/-/merge-9.2.0.tgz#42a210c1f8ec0ab86b868b3c6da383cd214188ce" + integrity sha512-kChDH/sOxm3TCICup5NgTiz/aJBk+5GAuC0lfJS8FcRfsqZvlCkNwpsYTGAATBCJMrgZ9+csRugCjS97jPcNMw== dependencies: "@graphql-tools/utils" "^11.2.0" tslib "^2.4.0" @@ -1731,12 +1731,12 @@ "@graphql-tools/utils" "^11.2.0" tslib "^2.4.0" -"@graphql-tools/schema@^10.0.0", "@graphql-tools/schema@^10.0.29", "@graphql-tools/schema@^10.0.35": - version "10.0.35" - resolved "https://registry.yarnpkg.com/@graphql-tools/schema/-/schema-10.0.35.tgz#22c5f7556e0200a5f215556ce9dbbe83dabd3abf" - integrity sha512-g/7IUmuLr3y4xoK1PDUm3B7arLBEMZatcbCIMf2690CU66xncY+GlMH37G0Sbm2lJY1/ELFpXa/S50rHKmoEkQ== +"@graphql-tools/schema@^10.0.0", "@graphql-tools/schema@^10.0.29", "@graphql-tools/schema@^10.0.36": + version "10.0.36" + resolved "https://registry.yarnpkg.com/@graphql-tools/schema/-/schema-10.0.36.tgz#56f4b4d21ee68f94a2a393af0cc9b52a0266d450" + integrity sha512-g8S5aLirZInoi3NojzmBxwsZfVawOE0UBlVWYe8kDAR0FxS0riBDiyW7JnlAKayooHMRAMwGaze4RQU3VTTyig== dependencies: - "@graphql-tools/merge" "^9.1.11" + "@graphql-tools/merge" "^9.2.0" "@graphql-tools/utils" "^11.2.0" tslib "^2.4.0" @@ -1836,6 +1836,11 @@ dependencies: "@hapi/hoek" "^11.0.2" +"@hono/node-server@^1.19.9": + version "1.19.14" + resolved "https://registry.yarnpkg.com/@hono/node-server/-/node-server-1.19.14.tgz#e30f844bc77e3ce7be442aac3b1f73ad8b58d181" + integrity sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw== + "@iconify/types@^2.0.0": version "2.0.0" resolved "https://registry.yarnpkg.com/@iconify/types/-/types-2.0.0.tgz#ab0e9ea681d6c8a1214f30cd741fe3a20cc57f57" @@ -2051,6 +2056,36 @@ dependencies: "@chevrotain/types" "~11.1.2" +"@modelcontextprotocol/ext-apps@*": + version "1.7.4" + resolved "https://registry.yarnpkg.com/@modelcontextprotocol/ext-apps/-/ext-apps-1.7.4.tgz#8b97005a134a6da90ce088bfda2097464bed9b46" + integrity sha512-QQqysE549cf/Y0VabBmAACXhj92EhB3t8yVct2BHbkWiPTFA1S91EqTVjYXXcZEefXU0pmHcdObhsNMcomJIOQ== + dependencies: + "@standard-schema/spec" "^1.1.0" + +"@modelcontextprotocol/sdk@^1.26.0": + version "1.29.0" + resolved "https://registry.yarnpkg.com/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz#79786d8b525e269de850ac82b1f1f757f3915f44" + integrity sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ== + dependencies: + "@hono/node-server" "^1.19.9" + ajv "^8.17.1" + ajv-formats "^3.0.1" + content-type "^1.0.5" + cors "^2.8.5" + cross-spawn "^7.0.5" + eventsource "^3.0.2" + eventsource-parser "^3.0.0" + express "^5.2.1" + express-rate-limit "^8.2.1" + hono "^4.11.4" + jose "^6.1.3" + json-schema-typed "^8.0.2" + pkce-challenge "^5.0.0" + raw-body "^3.0.0" + zod "^3.25 || ^4.0" + zod-to-json-schema "^3.25.1" + "@mui/base@5.0.0-beta.40-1": version "5.0.0-beta.40-1" resolved "https://registry.yarnpkg.com/@mui/base/-/base-5.0.0-beta.40-1.tgz#6da6229e5e675e811f319149f6e29d7a77522851" @@ -2893,35 +2928,35 @@ resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-14.6.1.tgz#13e09a32d7a8b7060fe38304788ebf4197cd2149" integrity sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw== -"@turbo/darwin-64@2.10.3": - version "2.10.3" - resolved "https://registry.yarnpkg.com/@turbo/darwin-64/-/darwin-64-2.10.3.tgz#e2ac067f34f64554619fee7dcb2c93972d94b717" - integrity sha512-guuiO2kKc7yUQFU2jhWyM/BrYGD/brqb0+JvJIXTQ/QI1NlWfHZ1id50kkkOWqxmBiDc8DgW2orfY85pQIc7gw== +"@turbo/darwin-64@2.10.4": + version "2.10.4" + resolved "https://registry.yarnpkg.com/@turbo/darwin-64/-/darwin-64-2.10.4.tgz#8ba4c3da35315203af32055c3f8c2081311a100c" + integrity sha512-m1MUEI4MJ69r5CwfMYxmHi0H0rrgiYCBOp0tgBZ9x/YVvOb5uu/lRIDyDwdtH054R2yWeQaIigUGu6aCX9f8cA== -"@turbo/darwin-arm64@2.10.3": - version "2.10.3" - resolved "https://registry.yarnpkg.com/@turbo/darwin-arm64/-/darwin-arm64-2.10.3.tgz#04cc3151f4dd68ba57c2a0b6b549c7cad44659a9" - integrity sha512-UglEDl/r1/h5Vw6oS6cEE29Jugz2sDLxHCSupNznKatj1fDMRXFqYKFcbvm8RTFhtYPI45NxkrPNo9BqNychBg== +"@turbo/darwin-arm64@2.10.4": + version "2.10.4" + resolved "https://registry.yarnpkg.com/@turbo/darwin-arm64/-/darwin-arm64-2.10.4.tgz#0b2acafd439cc8045f0caea86418cafa451a45b0" + integrity sha512-VQ1Yxs5zkPT+2z7t1P4mvn6JmcKLkOCAsPuK9XbOvuVj0DlTlETfIXNisX0771v/vTWHOQqiwoGi+TtAUq8efw== -"@turbo/linux-64@2.10.3": - version "2.10.3" - resolved "https://registry.yarnpkg.com/@turbo/linux-64/-/linux-64-2.10.3.tgz#234addb660d9bf705d58e28cc7c0fa2101c35a61" - integrity sha512-KuQxaPWD7OBmwEZqO0sgijwcuXR5eMWbo7BEt2m1D2Q3RylJDj7WMcJ0Btv2VJA0QC+khzAaTAm1yWowJ0CWAw== +"@turbo/linux-64@2.10.4": + version "2.10.4" + resolved "https://registry.yarnpkg.com/@turbo/linux-64/-/linux-64-2.10.4.tgz#37183deb4c35c13d929af6fefc7c78fd0b11d499" + integrity sha512-IzV1QovmwX7mfGnVinmE++2IB8tbeo38weltiuH5zNqwCTBjLs/DytyRKx+bmnhHdXIq9SheR8p0Nip/LBUPHg== -"@turbo/linux-arm64@2.10.3": - version "2.10.3" - resolved "https://registry.yarnpkg.com/@turbo/linux-arm64/-/linux-arm64-2.10.3.tgz#4265da47fdafa408e8fd5cf922ee0651c9aace49" - integrity sha512-DPkIKQ+6p0sho3Cx/e/A3F+AKgXQJA+z//cHsTcyyM1YWRGSYA0UTP8NUpb4iS2tVBSniGwmjRUr73hpq8kcDg== +"@turbo/linux-arm64@2.10.4": + version "2.10.4" + resolved "https://registry.yarnpkg.com/@turbo/linux-arm64/-/linux-arm64-2.10.4.tgz#76c5871a6f5023d513bcc42e5c2df9f4a9339cde" + integrity sha512-rfujSQkP5aYiRn0PgTM7F00WkJCP/bKDVZbOx3WmrZwa/vHA0bplhCl328kpX7VI9HH2vI90ISGwuSVgJgoqTw== -"@turbo/windows-64@2.10.3": - version "2.10.3" - resolved "https://registry.yarnpkg.com/@turbo/windows-64/-/windows-64-2.10.3.tgz#fc685cbd24a539233dab6697e64f878a65db903a" - integrity sha512-8NvFAze9D4PsosZAnK3aGqHz2vLzREz9lNIxLgfE0jRchq2sZQE190cwFm7WX17yg3ftPvwCvWH3rhLbG1UHCQ== +"@turbo/windows-64@2.10.4": + version "2.10.4" + resolved "https://registry.yarnpkg.com/@turbo/windows-64/-/windows-64-2.10.4.tgz#bab445734a769a099dc2f1305c236c80de27c972" + integrity sha512-NnspP7Wd5fa3Wwnqv9bKfhegqZzuHBgbPxdZU/idTLQcazx/vgKu95JlCx2YHY0hdvKCnPcARrDwM+KEUmaO7A== -"@turbo/windows-arm64@2.10.3": - version "2.10.3" - resolved "https://registry.yarnpkg.com/@turbo/windows-arm64/-/windows-arm64-2.10.3.tgz#7f1fb49edd2bcd85b761ca58cf8b757c15d8d156" - integrity sha512-AqjqV5cHbFa0YRaQ+Dyw6lSm6as4h/Uf8LcZ7VN8i+Odr23ShFD5SUvVAR1d3rZqibFVs9c0VdtXdfQfkdcs3A== +"@turbo/windows-arm64@2.10.4": + version "2.10.4" + resolved "https://registry.yarnpkg.com/@turbo/windows-arm64/-/windows-arm64-2.10.4.tgz#ab5f0c53266bbe18a52d1f5a0a15f718ba2378f3" + integrity sha512-Iv02YgOpaEShc2OkG7mgCJ2pEw1RUKiKbs0h8W5wAf4jZ5vpmraTEjuGTgHRuOORQnC1GN3KHo5WB+hu1abRMA== "@tybys/wasm-util@^0.10.3": version "0.10.3" @@ -2963,6 +2998,14 @@ dependencies: "@babel/types" "^7.28.2" +"@types/body-parser@*": + version "1.19.6" + resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.6.tgz#1859bebb8fd7dac9918a45d54c1971ab8b5af474" + integrity sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g== + dependencies: + "@types/connect" "*" + "@types/node" "*" + "@types/chai@^5.2.2": version "5.2.3" resolved "https://registry.yarnpkg.com/@types/chai/-/chai-5.2.3.tgz#8e9cd9e1c3581fa6b341a5aed5588eb285be0b4a" @@ -2971,6 +3014,20 @@ "@types/deep-eql" "*" assertion-error "^2.0.1" +"@types/connect@*": + version "3.4.38" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.38.tgz#5ba7f3bc4fbbdeaff8dded952e5ff2cc53f8d858" + integrity sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug== + dependencies: + "@types/node" "*" + +"@types/cors@^2.8.17": + version "2.8.19" + resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.19.tgz#d93ea2673fd8c9f697367f5eeefc2bbfa94f0342" + integrity sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg== + dependencies: + "@types/node" "*" + "@types/d3-array@*": version "3.2.2" resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-3.2.2.tgz#e02151464d02d4a1b44646d0fcdb93faf88fde8c" @@ -3203,6 +3260,26 @@ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.9.tgz#cf3f0e876d7bee15a93ab925b82bf570a3904a24" integrity sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg== +"@types/express-serve-static-core@^4.17.33": + version "4.19.9" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.19.9.tgz#b746a8bb6c389af7a31141397bb539f775b0ae84" + integrity sha512-QP2ESEe/ImWY0HDwNAnK9PvEffUyhLTnWkk7KXzHfyeWAnlrDe1fN77bXl6ia8KT3wPlmA7t9/VPRpnf4Ex9sg== + dependencies: + "@types/node" "*" + "@types/qs" "*" + "@types/range-parser" "*" + "@types/send" "*" + +"@types/express@^4.17.21": + version "4.17.25" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.25.tgz#070c8c73a6fee6936d65c195dbbfb7da5026649b" + integrity sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "^4.17.33" + "@types/qs" "*" + "@types/serve-static" "^1" + "@types/geojson@*": version "7946.0.16" resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.16.tgz#8ebe53d69efada7044454e3305c19017d97ced2a" @@ -3213,6 +3290,11 @@ resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.11.tgz#56588b17ae8f50c53983a524fc3cc47437969d64" integrity sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA== +"@types/http-errors@*": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.5.tgz#5b749ab2b16ba113423feb1a64a95dcd30398472" + integrity sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg== + "@types/mdast@^4.0.0": version "4.0.4" resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-4.0.4.tgz#7ccf72edd2f1aa7dd3437e180c64373585804dd6" @@ -3225,15 +3307,20 @@ resolved "https://registry.yarnpkg.com/@types/mdx/-/mdx-2.0.14.tgz#c1e54113265b152021ab0afe0434e3cadd90bfe3" integrity sha512-T48PeuJtvLosNTPVhfnIp3i/n3a4g4Bad7YCq5k64D4u7NwDrAotikQ+5+sjtUvBmxCMlbo3dVL+C2dP0rWHzg== +"@types/mime@^1": + version "1.3.5" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.5.tgz#1ef302e01cf7d2b5a0fa526790c9123bf1d06690" + integrity sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w== + "@types/ms@*": version "2.1.0" resolved "https://registry.yarnpkg.com/@types/ms/-/ms-2.1.0.tgz#052aa67a48eccc4309d7f0191b7e41434b90bb78" integrity sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA== "@types/node@*": - version "26.1.0" - resolved "https://registry.yarnpkg.com/@types/node/-/node-26.1.0.tgz#aa85f0727fc5611347091c478341c63650903439" - integrity sha512-O0A1G3xPGy4w7AgQdAQYUlQ+BKk2Oovw8eRpofyp5KdBZULnbe+WqaOVNrm705SHphCiG4XHsACrSmPu1f+Kgw== + version "26.1.1" + resolved "https://registry.yarnpkg.com/@types/node/-/node-26.1.1.tgz#bad758d601e97d6cf457d204ee76a35fce7bd119" + integrity sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw== dependencies: undici-types "~8.3.0" @@ -3252,9 +3339,9 @@ undici-types "~6.21.0" "@types/node@^24.6.0": - version "24.13.2" - resolved "https://registry.yarnpkg.com/@types/node/-/node-24.13.2.tgz#3b9b280a7055128359f125eb1067d9a190f39854" - integrity sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA== + version "24.13.3" + resolved "https://registry.yarnpkg.com/@types/node/-/node-24.13.3.tgz#49f18bd3c647866dcda51a0756c145e14590ce16" + integrity sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q== dependencies: undici-types "~7.18.0" @@ -3275,7 +3362,17 @@ resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.15.tgz#e6e5a86d602beaca71ce5163fadf5f95d70931c7" integrity sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw== -"@types/react-dom@^18.0.11": +"@types/qs@*": + version "6.15.1" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.15.1.tgz#8606884272c63f0db96986bd3548650d8a9388bf" + integrity sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw== + +"@types/range-parser@*": + version "1.2.7" + resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.7.tgz#50ae4353eaaddc04044279812f52c8c65857dbcb" + integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ== + +"@types/react-dom@^18.0.0", "@types/react-dom@^18.0.11": version "18.3.7" resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.3.7.tgz#b89ddf2cd83b4feafcc4e2ea41afdfb95a0d194f" integrity sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ== @@ -3323,7 +3420,7 @@ dependencies: csstype "^3.2.2" -"@types/react@^18.0.28": +"@types/react@^18.0.0", "@types/react@^18.0.28": version "18.3.31" resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.31.tgz#b5e95e28ffcceab8d982f33f2eb076e17653c2a4" integrity sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw== @@ -3336,6 +3433,30 @@ resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.20.6.tgz#e6e60dad29c2c8c206c026e6dd8d6d1bdda850b8" integrity sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ== +"@types/send@*": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@types/send/-/send-1.2.1.tgz#6a784e45543c18c774c049bff6d3dbaf045c9c74" + integrity sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ== + dependencies: + "@types/node" "*" + +"@types/send@<1": + version "0.17.6" + resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.6.tgz#aeb5385be62ff58a52cd5459daa509ae91651d25" + integrity sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og== + dependencies: + "@types/mime" "^1" + "@types/node" "*" + +"@types/serve-static@^1": + version "1.15.10" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.10.tgz#768169145a778f8f5dfcb6360aead414a3994fee" + integrity sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw== + dependencies: + "@types/http-errors" "*" + "@types/node" "*" + "@types/send" "<1" + "@types/trusted-types@^2.0.7": version "2.0.7" resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.7.tgz#baccb07a970b91707df3a3e8ba6896c57ead2d11" @@ -3371,7 +3492,7 @@ d3-selection "^3.0.0" d3-transition "^3.0.1" -"@vitejs/plugin-react@^4.0.4": +"@vitejs/plugin-react@^4.0.0", "@vitejs/plugin-react@^4.0.4": version "4.7.0" resolved "https://registry.yarnpkg.com/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz#647af4e7bb75ad3add578e762ad984b90f4a24b9" integrity sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA== @@ -3502,6 +3623,22 @@ dependencies: tslib "^2.3.0" +accepts@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-2.0.0.tgz#bbcf4ba5075467f3f2131eab3cffc73c2f5d7895" + integrity sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng== + dependencies: + mime-types "^3.0.0" + negotiator "^1.0.0" + +accepts@~1.3.8: + version "1.3.8" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" + integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== + dependencies: + mime-types "~2.1.34" + negotiator "0.6.3" + acorn@^8.15.0: version "8.17.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.17.0.tgz#1785adb84faf8d8add10369b93826fc2bd08f1fe" @@ -3526,6 +3663,23 @@ agent-base@6: dependencies: debug "4" +ajv-formats@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-3.0.1.tgz#3d5dc762bca17679c3c2ea7e90ad6b7532309578" + integrity sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ== + dependencies: + ajv "^8.0.0" + +ajv@^8.0.0, ajv@^8.17.1: + version "8.20.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.20.0.tgz#304b3636add88ba7d936760dd50ece006dea95f9" + integrity sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA== + dependencies: + fast-deep-equal "^3.1.3" + fast-uri "^3.0.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + ansi-escapes@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-5.0.0.tgz#b6a0caf0eef0c41af190e9a749e0c00ec04bb2a6" @@ -3587,6 +3741,11 @@ aria-query@^5.0.0: resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.2.tgz#93f81a43480e33a338f19163a3d10a50c01dcd59" integrity sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw== +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== + array-union@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" @@ -3734,18 +3893,51 @@ bluebird@3.7.2: resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== +body-parser@^2.2.1: + version "2.3.0" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-2.3.0.tgz#6d8662f4d8c336028b8ac9aa24251b0ca64ba437" + integrity sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw== + dependencies: + bytes "^3.1.2" + content-type "^2.0.0" + debug "^4.4.3" + http-errors "^2.0.1" + iconv-lite "^0.7.2" + on-finished "^2.4.1" + qs "^6.15.2" + raw-body "^3.0.2" + type-is "^2.1.0" + +body-parser@~1.20.5: + version "1.20.6" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.6.tgz#60c789c78e0992d906da0a29d71ae01d15c1ed76" + integrity sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g== + dependencies: + bytes "~3.1.2" + content-type "~1.0.5" + debug "2.6.9" + depd "2.0.0" + destroy "~1.2.0" + http-errors "~2.0.1" + iconv-lite "~0.4.24" + on-finished "~2.4.1" + qs "~6.15.1" + raw-body "~2.5.3" + type-is "~1.6.18" + unpipe "~1.0.0" + brace-expansion@^1.1.7: - version "1.1.15" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.15.tgz#a6d90d54067236e5f42570a3b7378d594d9b7738" - integrity sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg== + version "1.1.16" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.16.tgz#723d3a30c0558c225abc9fc479a73e14e26c3c2f" + integrity sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw== dependencies: balanced-match "^1.0.0" concat-map "0.0.1" brace-expansion@^2.0.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.1.1.tgz#c68b1c4111c76aae3a6fba55d496cee10c39dad8" - integrity sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA== + version "2.1.2" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.1.2.tgz#0bba2271feb7d458b0d31ad13625aaa4754431e2" + integrity sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA== dependencies: balanced-match "^1.0.0" @@ -3793,6 +3985,11 @@ bundle-name@^4.1.0: dependencies: run-applescript "^7.0.0" +bytes@^3.1.2, bytes@~3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6" @@ -3801,6 +3998,14 @@ call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: es-errors "^1.3.0" function-bind "^1.1.2" +call-bound@^1.0.2: + version "1.0.4" + resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.4.tgz#238de935d2a2a692928c538c7ccfa91067fd062a" + integrity sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg== + dependencies: + call-bind-apply-helpers "^1.0.2" + get-intrinsic "^1.3.0" + callsites@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" @@ -3830,9 +4035,9 @@ camelize@^1.0.0: integrity sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ== caniuse-lite@^1.0.30001800: - version "1.0.30001802" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001802.tgz#2671fb13d468930586c56ffa80feb1c51e18ec69" - integrity sha512-vmv8ub2xwTNmljSKf82mtCk5JH7hC+YgzLj3P5zotvA0tPQ9016tdNNOG8WRca1IxOnhSsivB+J0z5FeE5LOUw== + version "1.0.30001803" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001803.tgz#b2a5d696e042bc8304dcd4942c39fe330fbbcb24" + integrity sha512-g/uHREV2ZpK9qMalCsWaxmA6ol+DX8GYhuf3T40RKoP+oL7vhRJh8LNt73PCjpnR6l14FzfPrB5Yux4PKm2meg== capital-case@^1.0.4: version "1.0.4" @@ -4138,6 +4343,28 @@ constant-case@^3.0.4: tslib "^2.0.3" upper-case "^2.0.2" +content-disposition@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-1.1.0.tgz#f3db789c752d45564cc7e9e1e0b31790d4a38e17" + integrity sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g== + +content-disposition@~0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" + integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== + dependencies: + safe-buffer "5.2.1" + +content-type@^1.0.5, content-type@~1.0.4, content-type@~1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" + integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== + +content-type@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-2.0.0.tgz#2fb3ede69dffa0af78ca7c4ce7589680638b56df" + integrity sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ== + convert-source-map@^1.5.0, convert-source-map@^1.7.0: version "1.9.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f" @@ -4148,6 +4375,21 @@ convert-source-map@^2.0.0: resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== +cookie-signature@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.2.2.tgz#57c7fc3cc293acab9fec54d73e15690ebe4a1793" + integrity sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg== + +cookie-signature@~1.0.6: + version "1.0.7" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.7.tgz#ab5dd7ab757c54e60f37ef6550f481c426d10454" + integrity sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA== + +cookie@^0.7.1, cookie@~0.7.1: + version "0.7.2" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.2.tgz#556369c472a2ba910f2979891b526b3436237ed7" + integrity sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w== + copy-to-clipboard@^3.2.0: version "3.3.3" resolved "https://registry.yarnpkg.com/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz#55ac43a1db8ae639a4bd99511c148cdd1b83a1b0" @@ -4165,6 +4407,14 @@ core-js@^2.4.0, core-js@^2.6.12: resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec" integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ== +cors@^2.8.5: + version "2.8.6" + resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.6.tgz#ff5dd69bd95e547503820d29aba4f8faf8dfec96" + integrity sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw== + dependencies: + object-assign "^4" + vary "^1" + cose-base@^1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/cose-base/-/cose-base-1.0.3.tgz#650334b41b869578a543358b80cda7e0abe0a60a" @@ -4229,7 +4479,7 @@ cross-inspect@1.0.1: dependencies: tslib "^2.4.0" -cross-spawn@^7.0.3: +cross-spawn@^7.0.3, cross-spawn@^7.0.5: version "7.0.6" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== @@ -4748,6 +4998,42 @@ d3-zoom@3: d3-selection "2 - 3" d3-transition "2 - 3" +d3@*, d3@^7.9.0: + version "7.9.0" + resolved "https://registry.yarnpkg.com/d3/-/d3-7.9.0.tgz#579e7acb3d749caf8860bd1741ae8d371070cd5d" + integrity sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA== + dependencies: + d3-array "3" + d3-axis "3" + d3-brush "3" + d3-chord "3" + d3-color "3" + d3-contour "4" + d3-delaunay "6" + d3-dispatch "3" + d3-drag "3" + d3-dsv "3" + d3-ease "3" + d3-fetch "3" + d3-force "3" + d3-format "3" + d3-geo "3" + d3-hierarchy "3" + d3-interpolate "3" + d3-path "3" + d3-polygon "3" + d3-quadtree "3" + d3-random "3" + d3-scale "4" + d3-scale-chromatic "3" + d3-selection "3" + d3-shape "3" + d3-time "3" + d3-time-format "4" + d3-timer "3" + d3-transition "3" + d3-zoom "3" + d3@^5.9.2: version "5.16.0" resolved "https://registry.yarnpkg.com/d3/-/d3-5.16.0.tgz#9c5e8d3b56403c79d4ed42fbd62f6113f199c877" @@ -4785,42 +5071,6 @@ d3@^5.9.2: d3-voronoi "1" d3-zoom "1" -d3@^7.9.0: - version "7.9.0" - resolved "https://registry.yarnpkg.com/d3/-/d3-7.9.0.tgz#579e7acb3d749caf8860bd1741ae8d371070cd5d" - integrity sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA== - dependencies: - d3-array "3" - d3-axis "3" - d3-brush "3" - d3-chord "3" - d3-color "3" - d3-contour "4" - d3-delaunay "6" - d3-dispatch "3" - d3-drag "3" - d3-dsv "3" - d3-ease "3" - d3-fetch "3" - d3-force "3" - d3-format "3" - d3-geo "3" - d3-hierarchy "3" - d3-interpolate "3" - d3-path "3" - d3-polygon "3" - d3-quadtree "3" - d3-random "3" - d3-scale "4" - d3-scale-chromatic "3" - d3-selection "3" - d3-shape "3" - d3-time "3" - d3-time-format "4" - d3-timer "3" - d3-transition "3" - d3-zoom "3" - dagre-d3-es@7.0.14: version "7.0.14" resolved "https://registry.yarnpkg.com/dagre-d3-es/-/dagre-d3-es-7.0.14.tgz#1272276e26457cf3b97dac569f8f0531ec33c377" @@ -4862,7 +5112,14 @@ debounce@^2.0.0: resolved "https://registry.yarnpkg.com/debounce/-/debounce-2.2.0.tgz#f895fa2fbdb579a0f0d3dcf5dde19657e50eaad5" integrity sha512-Xks6RUDLZFdz8LIdR6q0MTH44k7FikOmnh5xkSjMig6ch45afc8sjTjRQf3P6ax8dMgcQrYO/AR2RGWURrruqw== -debug@4, debug@4.4.3, debug@^4.0.0, debug@^4.1.0, debug@^4.3.1, debug@^4.3.3: +debug@2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@4, debug@4.4.3, debug@^4.0.0, debug@^4.1.0, debug@^4.3.1, debug@^4.3.3, debug@^4.4.0, debug@^4.4.3: version "4.4.3" resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== @@ -4933,6 +5190,11 @@ delayed-stream@~1.0.0: resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== +depd@2.0.0, depd@^2.0.0, depd@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + dependency-graph@^0.11.0: version "0.11.0" resolved "https://registry.yarnpkg.com/dependency-graph/-/dependency-graph-0.11.0.tgz#ac0ce7ed68a54da22165a85e97a01d53f5eb2e27" @@ -4948,6 +5210,11 @@ dequal@^2.0.0: resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== +destroy@1.2.0, destroy@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" + integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== + detect-indent@^6.0.0: version "6.1.0" resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-6.1.0.tgz#592485ebbbf6b3b1ab2be175c8393d04ca0d57e6" @@ -5090,6 +5357,11 @@ eastasianwidth@^0.2.0: resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== + ejs@^3.1.5: version "3.1.10" resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.10.tgz#69ab8358b14e896f80cc39e62087b88500c3ac3b" @@ -5098,9 +5370,9 @@ ejs@^3.1.5: jake "^10.8.5" electron-to-chromium@^1.5.387: - version "1.5.387" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.387.tgz#d3ce821b0a37af5a46e2d2a33379ecfa16303636" - integrity sha512-TaxwufTFDufvPEoXdhwVrA3UdFWBeWGkYoJ1K8ldF1xe6gKfth6iRNS5lTQ5JPNOHdGQm8PT1QYKUqFLCiUefQ== + version "1.5.389" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.389.tgz#538be9ebec78026d4daba6be321ab854dfac2a8f" + integrity sha512-cEto7aeOqBfU1D+c5py5pE+ooscKE75JifxLBdFUZsqAxRS6y7kebtxAZvICszSl05gPjYHDTjY+lXpyGvpJbg== emoji-regex@^10.3.0: version "10.6.0" @@ -5122,6 +5394,11 @@ empathic@^2.0.0: resolved "https://registry.yarnpkg.com/empathic/-/empathic-2.0.1.tgz#37b1bede31093e04a03d2abce95ea323fee8ef49" integrity sha512-YGRs8knHhKHVShLkFET/rWAU8kmHbOV5LwN938RHI0pljAJ1Gf6SzXsSmRaEzcXTtOOmVqJ5+WtQPL5uigY50Q== +encodeurl@^2.0.0, encodeurl@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58" + integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg== + encoding@^0.1.11: version "0.1.13" resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.13.tgz#56574afdd791f54a8e9b2785c0582a2d26210fa9" @@ -5193,7 +5470,7 @@ es-toolkit@^1.45.1: resolved "https://registry.yarnpkg.com/es-toolkit/-/es-toolkit-1.49.0.tgz#93c5b031865792fc03cbf5bd20c132a4f976a52a" integrity sha512-G5iZ6Pc/FNRY/soKZHC+TxGDD83rHUDXxzaWhGCX44vAv/tMs56WMusnm/KMNK+luUPsgA9U28cGr4RDlSzL2g== -"esbuild@^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.26.0 || ^0.27.0 || ^0.28.0", "esbuild@^0.27.0 || ^0.28.0": +"esbuild@^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.26.0 || ^0.27.0 || ^0.28.0", "esbuild@^0.27.0 || ^0.28.0", esbuild@~0.28.0: version "0.28.1" resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.28.1.tgz#ef45b4634c9c9d97a296aea4114a5f9840f95578" integrity sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw== @@ -5262,7 +5539,7 @@ escalade@^3.1.1, escalade@^3.2.0: resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== -escape-html@^1.0.3: +escape-html@^1.0.3, escape-html@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== @@ -5292,6 +5569,11 @@ esutils@^2.0.2: resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== +etag@^1.8.1, etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== + event-stream@=3.3.4: version "3.3.4" resolved "https://registry.yarnpkg.com/event-stream/-/event-stream-3.3.4.tgz#4ab4c9a0f5a54db9338b4c34d86bfce8f4b35571" @@ -5310,6 +5592,18 @@ eventemitter3@^5.0.1: resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-5.0.4.tgz#a86d66170433712dde814707ac52b5271ceb1feb" integrity sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw== +eventsource-parser@^3.0.0, eventsource-parser@^3.0.1: + version "3.1.0" + resolved "https://registry.yarnpkg.com/eventsource-parser/-/eventsource-parser-3.1.0.tgz#4e198eb91cd333d0a8ddcc036502b3618a25f449" + integrity sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg== + +eventsource@^3.0.2: + version "3.0.7" + resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-3.0.7.tgz#1157622e2f5377bb6aef2114372728ba0c156989" + integrity sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA== + dependencies: + eventsource-parser "^3.0.1" + execa@5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" @@ -5340,11 +5634,94 @@ execa@7.2.0: signal-exit "^3.0.7" strip-final-newline "^3.0.0" +express-rate-limit@^8.2.1: + version "8.5.2" + resolved "https://registry.yarnpkg.com/express-rate-limit/-/express-rate-limit-8.5.2.tgz#5922dbf76df2124611cea955d93432b37514b2f3" + integrity sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A== + dependencies: + ip-address "^10.2.0" + +express@^4.21.2: + version "4.22.2" + resolved "https://registry.yarnpkg.com/express/-/express-4.22.2.tgz#c17ae0981e5efc24b22272f0e041c4662503b700" + integrity sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q== + dependencies: + accepts "~1.3.8" + array-flatten "1.1.1" + body-parser "~1.20.5" + content-disposition "~0.5.4" + content-type "~1.0.4" + cookie "~0.7.1" + cookie-signature "~1.0.6" + debug "2.6.9" + depd "2.0.0" + encodeurl "~2.0.0" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "~1.3.1" + fresh "~0.5.2" + http-errors "~2.0.0" + merge-descriptors "1.0.3" + methods "~1.1.2" + on-finished "~2.4.1" + parseurl "~1.3.3" + path-to-regexp "~0.1.12" + proxy-addr "~2.0.7" + qs "~6.15.1" + range-parser "~1.2.1" + safe-buffer "5.2.1" + send "~0.19.0" + serve-static "~1.16.2" + setprototypeof "1.2.0" + statuses "~2.0.1" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + +express@^5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/express/-/express-5.2.1.tgz#8f21d15b6d327f92b4794ecf8cb08a72f956ac04" + integrity sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw== + dependencies: + accepts "^2.0.0" + body-parser "^2.2.1" + content-disposition "^1.0.0" + content-type "^1.0.5" + cookie "^0.7.1" + cookie-signature "^1.2.1" + debug "^4.4.0" + depd "^2.0.0" + encodeurl "^2.0.0" + escape-html "^1.0.3" + etag "^1.8.1" + finalhandler "^2.1.0" + fresh "^2.0.0" + http-errors "^2.0.0" + merge-descriptors "^2.0.0" + mime-types "^3.0.0" + on-finished "^2.4.1" + once "^1.4.0" + parseurl "^1.3.3" + proxy-addr "^2.0.7" + qs "^6.14.0" + range-parser "^1.2.1" + router "^2.2.0" + send "^1.1.0" + serve-static "^2.2.0" + statuses "^2.0.1" + type-is "^2.0.1" + vary "^1.1.2" + extend@^3.0.0: version "3.0.2" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== +fast-deep-equal@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + fast-glob@^3.2.9: version "3.3.3" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.3.tgz#d06d585ce8dba90a16b0505c543c3ccfb3aeb818" @@ -5361,6 +5738,11 @@ fast-json-stable-stringify@^2.0.0: resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== +fast-uri@^3.0.1: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.1.3.tgz#f695a40f006aba505631573a0021ddb21194ad11" + integrity sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg== + fastpriorityqueue@^0.6.3: version "0.6.4" resolved "https://registry.yarnpkg.com/fastpriorityqueue/-/fastpriorityqueue-0.6.4.tgz#7b9e315b47df6badcc2480e5a2d1c2f975971f80" @@ -5462,6 +5844,31 @@ filter-obj@^1.1.0: resolved "https://registry.yarnpkg.com/filter-obj/-/filter-obj-1.1.0.tgz#9b311112bc6c6127a16e016c6c5d7f19e0805c5b" integrity sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ== +finalhandler@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-2.1.1.tgz#a2c517a6559852bcdb06d1f8bd7f51b68fad8099" + integrity sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA== + dependencies: + debug "^4.4.0" + encodeurl "^2.0.0" + escape-html "^1.0.3" + on-finished "^2.4.1" + parseurl "^1.3.3" + statuses "^2.0.1" + +finalhandler@~1.3.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.3.2.tgz#1ebc2228fc7673aac4a472c310cc05b77d852b88" + integrity sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg== + dependencies: + debug "2.6.9" + encodeurl "~2.0.0" + escape-html "~1.0.3" + on-finished "~2.4.1" + parseurl "~1.3.3" + statuses "~2.0.2" + unpipe "~1.0.0" + find-root@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/find-root/-/find-root-1.1.0.tgz#abcfc8ba76f708c42a97b3d685b7e9450bfb9ce4" @@ -5498,11 +5905,26 @@ formdata-polyfill@^4.0.10: dependencies: fetch-blob "^3.1.2" +forwarded@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" + integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== + frac@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/frac/-/frac-1.1.2.tgz#3d74f7f6478c88a1b5020306d747dc6313c74d0b" integrity sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA== +fresh@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-2.0.0.tgz#8dd7df6a1b3a1b3a5cf186c05a5dd267622635a4" + integrity sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A== + +fresh@~0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== + from@~0: version "0.1.7" resolved "https://registry.yarnpkg.com/from/-/from-0.1.7.tgz#83c60afc58b9c56997007ed1a768b3ab303a44fe" @@ -5552,7 +5974,7 @@ get-east-asian-width@^1.0.0, get-east-asian-width@^1.3.1, get-east-asian-width@^ resolved "https://registry.yarnpkg.com/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz#216900f91df11a8b2c198c3e1d93d6c035a776b9" integrity sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA== -get-intrinsic@^1.2.6: +get-intrinsic@^1.2.5, get-intrinsic@^1.2.6, get-intrinsic@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01" integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== @@ -5794,6 +6216,11 @@ hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1, hoist-non-react- dependencies: react-is "^16.7.0" +hono@^4.11.4: + version "4.12.28" + resolved "https://registry.yarnpkg.com/hono/-/hono-4.12.28.tgz#c2e12c32027f0e9fb8b0cef2ecfb73050c2ca62e" + integrity sha512-YwUvVpSF7m1yOblFPrU3Hbo8XhPheBoiyfGuII6z19LnOr6JpDnyyp7LFNrfV56wS8tpvtBFGRISHN02pDdLOA== + htmlparser2@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-4.1.0.tgz#9a4ef161f2e4625ebf7dfbe6c0a2f52d18a59e78" @@ -5804,6 +6231,17 @@ htmlparser2@^4.1.0: domutils "^2.0.0" entities "^2.0.0" +http-errors@^2.0.0, http-errors@^2.0.1, http-errors@~2.0.0, http-errors@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.1.tgz#36d2f65bc909c8790018dd36fb4d93da6caae06b" + integrity sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ== + dependencies: + depd "~2.0.0" + inherits "~2.0.4" + setprototypeof "~1.2.0" + statuses "~2.0.2" + toidentifier "~1.0.1" + https-proxy-agent@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" @@ -5832,7 +6270,7 @@ hyphenate-style-name@^1.0.3: resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.1.0.tgz#1797bf50369588b47b72ca6d5e65374607cf4436" integrity sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw== -iconv-lite@0.4: +iconv-lite@0.4, iconv-lite@~0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== @@ -5846,7 +6284,7 @@ iconv-lite@0.6, iconv-lite@^0.6.2: dependencies: safer-buffer ">= 2.1.2 < 3.0.0" -iconv-lite@^0.7.0: +iconv-lite@^0.7.0, iconv-lite@^0.7.2, iconv-lite@~0.7.0: version "0.7.3" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.7.3.tgz#84ee12f963e7de50bc01a13e160a078b3b0f415f" integrity sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ== @@ -5899,7 +6337,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2: +inherits@2, inherits@~2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -5933,6 +6371,16 @@ iobuffer@^5.0.0, iobuffer@^5.3.2: resolved "https://registry.yarnpkg.com/iobuffer/-/iobuffer-5.4.0.tgz#f85dff957fd0579257472f0a4cfe5ed3430e63e1" integrity sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA== +ip-address@^10.2.0: + version "10.2.0" + resolved "https://registry.yarnpkg.com/ip-address/-/ip-address-10.2.0.tgz#805fc178b20c518bd4c8548b24fe30892d7f3206" + integrity sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA== + +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + is-absolute@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-absolute/-/is-absolute-1.0.0.tgz#395e1ae84b11f26ad1795e73c17378e48a301576" @@ -6038,6 +6486,11 @@ is-primitive@^3.0.1: resolved "https://registry.yarnpkg.com/is-primitive/-/is-primitive-3.0.1.tgz#98c4db1abff185485a657fc2905052b940524d05" integrity sha512-GljRxhWvlCNRfZyORiH77FwdFwGcMO620o37EOYC0ORWdq+WYNVqW0w2Juzew4M+L81l6/QS3t5gkkihyRqv9w== +is-promise@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-4.0.0.tgz#42ff9f84206c1991d26debf520dd5c01042dd2f3" + integrity sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ== + is-relative@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-relative/-/is-relative-1.0.0.tgz#a1bb6935ce8c5dba1e8b9754b9b2dcc020e2260d" @@ -6168,6 +6621,11 @@ joi@^18.0.2: "@hapi/topo" "^6.0.2" "@standard-schema/spec" "^1.1.0" +jose@^6.1.3: + version "6.2.3" + resolved "https://registry.yarnpkg.com/jose/-/jose-6.2.3.tgz#0975197ad973251221c658a3cddc4b951a250c2d" + integrity sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw== + "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" @@ -6190,6 +6648,16 @@ json-parse-even-better-errors@^2.3.0: resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== +json-schema-traverse@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" + integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== + +json-schema-typed@^8.0.2: + version "8.0.2" + resolved "https://registry.yarnpkg.com/json-schema-typed/-/json-schema-typed-8.0.2.tgz#e98ee7b1899ff4a184534d1f167c288c66bbeff4" + integrity sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA== + json-to-pretty-yaml@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/json-to-pretty-yaml/-/json-to-pretty-yaml-1.2.2.tgz#f4cd0bd0a5e8fe1df25aaf5ba118b099fd992d5b" @@ -6475,9 +6943,9 @@ lower-case@^2.0.2: tslib "^2.0.3" lru-cache@^11.0.0: - version "11.5.1" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-11.5.1.tgz#f3daa3540847b9737ebc02499ddb36765e54db4a" - integrity sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A== + version "11.5.2" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-11.5.2.tgz#00e16665c90c620fba14a3c368732a976493f760" + integrity sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g== lru-cache@^5.1.1: version "5.1.1" @@ -6657,6 +7125,16 @@ mdurl@^1.0.1: resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" integrity sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g== +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== + +media-typer@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-1.1.0.tgz#6ab74b8f2d3320f2064b2a87a38e7931ff3a5561" + integrity sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw== + memoize-one@^5.0.0: version "5.2.1" resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e" @@ -6669,6 +7147,16 @@ merge-anything@^2.2.4: dependencies: is-what "^3.3.1" +merge-descriptors@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz#d80319a65f3c7935351e5cfdac8f9318504dbed5" + integrity sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ== + +merge-descriptors@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-2.0.0.tgz#ea922f660635a2249ee565e0449f951e6b603808" + integrity sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g== + merge-stream@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" @@ -6711,6 +7199,11 @@ meros@^1.1.4, meros@^1.3.2: resolved "https://registry.yarnpkg.com/meros/-/meros-1.3.2.tgz#4cb0d7f3d22074c6e7ae1d66f72c080bca2e414b" integrity sha512-Q3mobPbvEx7XbwhnC1J1r60+5H6EZyNccdzSz0eGexJRwouUtTZxPVRGdqKtxlpD84ScK4+tIGldkqDtCKdI0A== +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== + micromark-core-commonmark@^2.0.0: version "2.0.3" resolved "https://registry.yarnpkg.com/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz#c691630e485021a68cf28dbc2b2ca27ebf678cd4" @@ -7005,13 +7498,30 @@ mime-db@1.52.0: resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== -mime-types@^2.1.35: +mime-db@^1.54.0: + version "1.54.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.54.0.tgz#cddb3ee4f9c64530dff640236661d42cb6a314f5" + integrity sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ== + +mime-types@^2.1.35, mime-types@~2.1.24, mime-types@~2.1.34: version "2.1.35" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== dependencies: mime-db "1.52.0" +mime-types@^3.0.0, mime-types@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-3.0.2.tgz#39002d4182575d5af036ffa118100f2524b2e2ab" + integrity sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A== + dependencies: + mime-db "^1.54.0" + +mime@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + mimic-fn@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" @@ -7070,12 +7580,17 @@ mkdirp@^0.5.1: dependencies: minimist "^1.2.6" +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== + ms@2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -ms@^2.1.3: +ms@2.1.3, ms@^2.1.3: version "2.1.3" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== @@ -7090,6 +7605,16 @@ nanoid@^3.3.12: resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.15.tgz#36c490fad8c6e86c824c940dfdde999b69ed4316" integrity sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA== +negotiator@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== + +negotiator@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-1.0.0.tgz#b6c91bb47172d69f93cfd7c357bbb529019b5f6a" + integrity sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg== + netcdfjs@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/netcdfjs/-/netcdfjs-3.0.0.tgz#de14b6ea2135fe141b254e0d1600872f625f55ca" @@ -7145,9 +7670,9 @@ node-int64@^0.4.0: integrity sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw== node-releases@^2.0.50: - version "2.0.50" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.50.tgz#597197a852071ce42fc2550e58e223242bcba969" - integrity sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg== + version "2.0.51" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.51.tgz#cdc08433577f5b32ad01694481726e22eeb54aef" + integrity sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ== normalize-path@^2.1.1: version "2.1.1" @@ -7175,7 +7700,7 @@ nullthrows@^1.0.0, nullthrows@^1.1.1: resolved "https://registry.yarnpkg.com/nullthrows/-/nullthrows-1.1.1.tgz#7818258843856ae971eae4208ad7d7eb19a431b1" integrity sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw== -object-assign@4.x, object-assign@^4.1.0, object-assign@^4.1.1: +object-assign@4.x, object-assign@^4, object-assign@^4.1.0, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== @@ -7185,7 +7710,19 @@ object-hash@^2.0.1: resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-2.2.0.tgz#5ad518581eefc443bd763472b8ff2e9c2c0d54a5" integrity sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw== -once@^1.3.0: +object-inspect@^1.13.3, object-inspect@^1.13.4: + version "1.13.4" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.4.tgz#8375265e21bc20d0fa582c22e1b13485d6e00213" + integrity sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew== + +on-finished@^2.4.1, on-finished@~2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== + dependencies: + ee-first "1.1.1" + +once@^1.3.0, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== @@ -7388,6 +7925,11 @@ parse-srcset@^1.0.2: resolved "https://registry.yarnpkg.com/parse-srcset/-/parse-srcset-1.0.2.tgz#f2bd221f6cc970a938d88556abc589caaaa2bde1" integrity sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q== +parseurl@^1.3.3, parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + pascal-case@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/pascal-case/-/pascal-case-3.1.2.tgz#b48e0ef2b98e205e7c1dae747d0b1508237660eb" @@ -7454,6 +7996,16 @@ path-scurry@^2.0.2: lru-cache "^11.0.0" minipass "^7.1.2" +path-to-regexp@^8.0.0: + version "8.4.2" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-8.4.2.tgz#795c420c4f7ca45c5b887366f622ee0c9852cccd" + integrity sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA== + +path-to-regexp@~0.1.12: + version "0.1.13" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.13.tgz#9b22ec16bc3ab88d05a0c7e369869421401ab17d" + integrity sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA== + path-type@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" @@ -7501,6 +8053,11 @@ pidtree@0.6.0: resolved "https://registry.yarnpkg.com/pidtree/-/pidtree-0.6.0.tgz#90ad7b6d42d5841e69e0a2419ef38f8883aa057c" integrity sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g== +pkce-challenge@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/pkce-challenge/-/pkce-challenge-5.0.1.tgz#3b4446865b17b1745e9ace2016a31f48ddf6230d" + integrity sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ== + playwright-core@1.61.1: version "1.61.1" resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.61.1.tgz#3c99841307efbbabc9d724c41a88c914705d15fc" @@ -7593,6 +8150,14 @@ prop-types@15.x, prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.8, pr object-assign "^4.1.1" react-is "^16.13.1" +proxy-addr@^2.0.7, proxy-addr@~2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" + integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== + dependencies: + forwarded "0.2.0" + ipaddr.js "1.9.1" + proxy-from-env@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-2.1.0.tgz#a7487568adad577cfaaa7e88c49cab3ab3081aba" @@ -7605,6 +8170,14 @@ ps-tree@1.2.0: dependencies: event-stream "=3.3.4" +qs@^6.14.0, qs@^6.15.2, qs@~6.15.1: + version "6.15.3" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.15.3.tgz#76852132a58ed5c7c0ef67e4441b9bb5d6061b3b" + integrity sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A== + dependencies: + es-define-property "^1.0.1" + side-channel "^1.1.1" + quadprog@^1.6.1: version "1.6.1" resolved "https://registry.yarnpkg.com/quadprog/-/quadprog-1.6.1.tgz#1cd3b13700de9553ef939a6fa73d0d55ddb2f082" @@ -7632,6 +8205,36 @@ raf@^3.4.0: dependencies: performance-now "^2.1.0" +range-parser@^1.2.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.3.0.tgz#d7f19be812bb62721472b45d3be219ef09572b47" + integrity sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw== + +range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@^3.0.0, raw-body@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-3.0.2.tgz#3e3ada5ae5568f9095d84376fd3a49b8fb000a51" + integrity sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA== + dependencies: + bytes "~3.1.2" + http-errors "~2.0.1" + iconv-lite "~0.7.0" + unpipe "~1.0.0" + +raw-body@~2.5.3: + version "2.5.3" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.3.tgz#11c6650ee770a7de1b494f197927de0c923822e2" + integrity sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA== + dependencies: + bytes "~3.1.2" + http-errors "~2.0.1" + iconv-lite "~0.4.24" + unpipe "~1.0.0" + rc-align@^2.4.0: version "2.4.5" resolved "https://registry.yarnpkg.com/rc-align/-/rc-align-2.4.5.tgz#c941a586f59d1017f23a428f0b468663fb7102ab" @@ -7765,7 +8368,7 @@ react-dom@^16.10.1, react-dom@^16.12.0: dependencies: scheduler "^0.27.0" -react-dom@^18.2.0: +react-dom@^18.0.0, react-dom@^18.2.0: version "18.3.1" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.3.1.tgz#c2265d79511b57d479b3dd3fdfa51536494c5cb4" integrity sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw== @@ -7962,7 +8565,7 @@ react@^16.10.1, react@^16.12.0: resolved "https://registry.yarnpkg.com/react/-/react-19.2.7.tgz#1f47a1bfc06f8ec885752c6f4af14369a9f8260b" integrity sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ== -react@^18.2.0: +react@^18.0.0, react@^18.2.0: version "18.3.1" resolved "https://registry.yarnpkg.com/react/-/react-18.3.1.tgz#49ab892009c53933625bd16b2533fc754cab2891" integrity sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ== @@ -8080,6 +8683,11 @@ require-directory@^2.1.1: resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== +require-from-string@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" + integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== + require-main-filename@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" @@ -8207,6 +8815,17 @@ roughjs@^4.6.6: points-on-curve "^0.2.0" points-on-path "^0.2.1" +router@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/router/-/router-2.2.0.tgz#019be620b711c87641167cc79b99090f00b146ef" + integrity sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ== + dependencies: + debug "^4.4.0" + depd "^2.0.0" + is-promise "^4.0.0" + parseurl "^1.3.3" + path-to-regexp "^8.0.0" + run-applescript@^7.0.0: version "7.1.0" resolved "https://registry.yarnpkg.com/run-applescript/-/run-applescript-7.1.0.tgz#2e9e54c4664ec3106c5b5630e249d3d6595c4911" @@ -8231,6 +8850,11 @@ rxjs@^7.8.2: dependencies: tslib "^2.1.0" +safe-buffer@5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + "safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0": version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" @@ -8287,6 +8911,42 @@ semver@^7.7.3: resolved "https://registry.yarnpkg.com/semver/-/semver-7.8.5.tgz#39b646037dd50c14fb451e7e4cac58ed8b863f69" integrity sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA== +send@^1.1.0, send@^1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/send/-/send-1.2.1.tgz#9eab743b874f3550f40a26867bf286ad60d3f3ed" + integrity sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ== + dependencies: + debug "^4.4.3" + encodeurl "^2.0.0" + escape-html "^1.0.3" + etag "^1.8.1" + fresh "^2.0.0" + http-errors "^2.0.1" + mime-types "^3.0.2" + ms "^2.1.3" + on-finished "^2.4.1" + range-parser "^1.2.1" + statuses "^2.0.2" + +send@~0.19.0, send@~0.19.1: + version "0.19.2" + resolved "https://registry.yarnpkg.com/send/-/send-0.19.2.tgz#59bc0da1b4ea7ad42736fd642b1c4294e114ff29" + integrity sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg== + dependencies: + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + encodeurl "~2.0.0" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "~0.5.2" + http-errors "~2.0.1" + mime "1.6.0" + ms "2.1.3" + on-finished "~2.4.1" + range-parser "~1.2.1" + statuses "~2.0.2" + sentence-case@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/sentence-case/-/sentence-case-3.0.4.tgz#3645a7b8c117c787fde8702056225bb62a45131f" @@ -8296,6 +8956,26 @@ sentence-case@^3.0.4: tslib "^2.0.3" upper-case-first "^2.0.2" +serve-static@^2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-2.2.1.tgz#7f186a4a4e5f5b663ad7a4294ff1bf37cf0e98a9" + integrity sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw== + dependencies: + encodeurl "^2.0.0" + escape-html "^1.0.3" + parseurl "^1.3.3" + send "^1.2.0" + +serve-static@~1.16.2: + version "1.16.3" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.16.3.tgz#a97b74d955778583f3862a4f0b841eb4d5d78cf9" + integrity sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA== + dependencies: + encodeurl "~2.0.0" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "~0.19.1" + set-blocking@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" @@ -8314,6 +8994,11 @@ setimmediate@^1.0.5: resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" integrity sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA== +setprototypeof@1.2.0, setprototypeof@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + shallowequal@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-1.1.0.tgz#188d521de95b9087404fd4dcb68b13df0ae4e7f8" @@ -8336,6 +9021,46 @@ shell-quote@^1.7.3: resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.9.0.tgz#e108b1a136586d5964edb3300016d4bedba0fe57" integrity sha512-Iov+JwFv/2HcTpcwNMKd8+IWNb8tboQJNQTkAY/LLVK7gGH9jy+LGkVqPxfekHl+yMmiqXszdGWXgkfml7hjqA== +side-channel-list@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/side-channel-list/-/side-channel-list-1.0.1.tgz#c2e0b5a14a540aebee3bbc6c3f8666cc9b509127" + integrity sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w== + dependencies: + es-errors "^1.3.0" + object-inspect "^1.13.4" + +side-channel-map@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/side-channel-map/-/side-channel-map-1.0.1.tgz#d6bb6b37902c6fef5174e5f533fab4c732a26f42" + integrity sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + +side-channel-weakmap@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz#11dda19d5368e40ce9ec2bdc1fb0ecbc0790ecea" + integrity sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + side-channel-map "^1.0.1" + +side-channel@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.1.1.tgz#ea02c62e05dc4bea67d4442f0fb71ee192f8e0ab" + integrity sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ== + dependencies: + es-errors "^1.3.0" + object-inspect "^1.13.4" + side-channel-list "^1.0.1" + side-channel-map "^1.0.1" + side-channel-weakmap "^1.0.2" + signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7: version "3.0.7" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" @@ -8473,6 +9198,11 @@ start-server-and-test@^2.0.4: ps-tree "1.2.0" wait-on "9.0.4" +statuses@^2.0.1, statuses@^2.0.2, statuses@~2.0.1, statuses@~2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.2.tgz#8f75eecef765b5e1cfcdc080da59409ed424e382" + integrity sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw== + storybook@^10.0.6: version "10.4.6" resolved "https://registry.yarnpkg.com/storybook/-/storybook-10.4.6.tgz#c28a2edb9bd01388d567f0c48f56c0386811016b" @@ -8544,9 +9274,9 @@ string-width@^7.0.0: strip-ansi "^7.1.0" string-width@^8.2.0: - version "8.2.1" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-8.2.1.tgz#165089cfa527cc88fbc23dd73313f5e334af1ea1" - integrity sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA== + version "8.2.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-8.2.2.tgz#7310516493df575742fe98af6fae87d85d5ed0ac" + integrity sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg== dependencies: get-east-asian-width "^1.5.0" strip-ansi "^7.1.2" @@ -8751,6 +9481,11 @@ toggle-selection@^1.0.6: resolved "https://registry.yarnpkg.com/toggle-selection/-/toggle-selection-1.0.6.tgz#6e45b1263f2017fa0acc7d89d78b15b8bf77da32" integrity sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ== +toidentifier@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + tr46@~0.0.3: version "0.0.3" resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" @@ -9167,23 +9902,49 @@ tsparticles@^2.0.6: tsparticles-updater-twinkle "^2.12.0" tsparticles-updater-wobble "^2.12.0" +tsx@^4.7.0: + version "4.23.0" + resolved "https://registry.yarnpkg.com/tsx/-/tsx-4.23.0.tgz#5393ae8bfce5a9c34db9c00d1353e3d8c3fd3de6" + integrity sha512-eUdUIaCr963q2h5u3+QwvYp0+eqPvn+egeqZUm0hwERCqqx1E3kK5ehbGCvqSE5MQAULr67ww0cA3jKc3YkM1w== + dependencies: + esbuild "~0.28.0" + optionalDependencies: + fsevents "~2.3.3" + turbo@^2.4.2: - version "2.10.3" - resolved "https://registry.yarnpkg.com/turbo/-/turbo-2.10.3.tgz#faabfcda7978c3383a37e34dc63747cc0a642a47" - integrity sha512-uZIqzfgtWbyqu1Tqwdd0giRnPGgL0ejbcdF8eZLJhtTTVSrOgArZGzYeXTD6XNcc7ANgdhqex11Y9bHBeyiQLQ== + version "2.10.4" + resolved "https://registry.yarnpkg.com/turbo/-/turbo-2.10.4.tgz#e406af232678433949987b190efe7d99f47412e2" + integrity sha512-GQpduILaKjoaGljw097ScsSyKTtZSY7cZ3bJktzfTkPMyCf3ShKLuXK2IaOEN2Plziml+ArR7WJ1m+V4VbnaKQ== optionalDependencies: - "@turbo/darwin-64" "2.10.3" - "@turbo/darwin-arm64" "2.10.3" - "@turbo/linux-64" "2.10.3" - "@turbo/linux-arm64" "2.10.3" - "@turbo/windows-64" "2.10.3" - "@turbo/windows-arm64" "2.10.3" + "@turbo/darwin-64" "2.10.4" + "@turbo/darwin-arm64" "2.10.4" + "@turbo/linux-64" "2.10.4" + "@turbo/linux-arm64" "2.10.4" + "@turbo/windows-64" "2.10.4" + "@turbo/windows-arm64" "2.10.4" type-fest@^1.0.2: version "1.4.0" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-1.4.0.tgz#e9fb813fe3bf1744ec359d55d1affefa76f14be1" integrity sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA== +type-is@^2.0.1, type-is@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-2.1.0.tgz#71d1a7053293582e16ac9f3ebaf1ab9aa49e5570" + integrity sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA== + dependencies: + content-type "^2.0.0" + media-typer "^1.1.0" + mime-types "^3.0.0" + +type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + typeface-inter@^3.3.0: version "3.18.1" resolved "https://registry.yarnpkg.com/typeface-inter/-/typeface-inter-3.18.1.tgz#24cccdf29923f318589783997be20a662cd3ab9c" @@ -9305,6 +10066,11 @@ unixify@^1.0.0: dependencies: normalize-path "^2.1.1" +unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== + unplugin@^2.3.5: version "2.3.11" resolved "https://registry.yarnpkg.com/unplugin/-/unplugin-2.3.11.tgz#411e020dd2ba90e2fbe1e7bd63a5a399e6ee3b54" @@ -9359,6 +10125,11 @@ use-sync-external-store@^1.5.0: resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz#b174bfa65cb2b526732d9f2ac0a408027876f32d" integrity sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w== +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== + "uuid@^11.1.0 || ^12 || ^13 || ^14.0.0": version "14.0.1" resolved "https://registry.yarnpkg.com/uuid/-/uuid-14.0.1.tgz#8a5975b3e038902bfd169a10b5202f5ec0cf3faf" @@ -9369,6 +10140,11 @@ uuid@^9.0.0, uuid@^9.0.1: resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== +vary@^1, vary@^1.1.2, vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== + vfile-message@^4.0.0: version "4.0.3" resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-4.0.3.tgz#87b44dddd7b70f0641c2e3ed0864ba73e2ea8df4" @@ -9407,7 +10183,7 @@ vite-plugin-svgr@^2.4.0: "@rollup/pluginutils" "^5.0.2" "@svgr/core" "^6.5.1" -vite@^6.1.1: +vite@^6.0.0, vite@^6.1.1: version "6.4.3" resolved "https://registry.yarnpkg.com/vite/-/vite-6.4.3.tgz#85a164db7ce706f2a776812efa2b340f1721858e" integrity sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A== @@ -9699,6 +10475,21 @@ zen-observable@0.8.15: resolved "https://registry.yarnpkg.com/zen-observable/-/zen-observable-0.8.15.tgz#96415c512d8e3ffd920afd3889604e30b9eaac15" integrity sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ== +zod-to-json-schema@^3.25.1: + version "3.25.2" + resolved "https://registry.yarnpkg.com/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz#3fa799a7badd554541472fb65843fdc460b2e5aa" + integrity sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA== + +zod@^3.22.0: + version "3.25.76" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.25.76.tgz#26841c3f6fd22a6a2760e7ccb719179768471e34" + integrity sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ== + +"zod@^3.25 || ^4.0": + version "4.4.3" + resolved "https://registry.yarnpkg.com/zod/-/zod-4.4.3.tgz#b680f172885d18bbebf21a834ea25e55a1bbf356" + integrity sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ== + zwitch@^2.0.0: version "2.0.4" resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-2.0.4.tgz#c827d4b0acb76fc3e685a4c6ec2902d51070e9d7" From 79163dabee362e07e03f2077eef2e0006787a5c5 Mon Sep 17 00:00:00 2001 From: Carlos Cruz <me@carcruzcast.com> Date: Sat, 11 Jul 2026 15:24:33 +0100 Subject: [PATCH 2/6] feat: implement directFetch option for widgets to bypass server-side prefetch --- apps/mcp-widgets-server/src/mcp-server.ts | 27 ++- .../src/sections/registry.ts | 174 +++++++----------- apps/mcp-widgets-server/src/widgets/index.ts | 28 +-- apps/mcp-widgets-server/src/widgets/types.ts | 5 + .../widget-src/shared/createWidgetEntry.tsx | 10 + 5 files changed, 124 insertions(+), 120 deletions(-) diff --git a/apps/mcp-widgets-server/src/mcp-server.ts b/apps/mcp-widgets-server/src/mcp-server.ts index bdd5b5185..850206bb6 100644 --- a/apps/mcp-widgets-server/src/mcp-server.ts +++ b/apps/mcp-widgets-server/src/mcp-server.ts @@ -20,7 +20,12 @@ const PUBLIC_API_URL = "https://api.platform.opentargets.org/api/v4/graphql"; * window.__OT_WIDGET_TOOL__ is set here so createWidgetEntry.tsx knows which * tool to call via callServerTool without needing to parse hostContext. */ -async function makeWidgetShell(bundleFile: string, title: string, toolName: string): Promise<string> { +async function makeWidgetShell( + bundleFile: string, + title: string, + toolName: string, + directFetch?: boolean +): Promise<string> { const bundlePath = new URL(`../dist/widgets/${bundleFile}`, import.meta.url).pathname; const bundleJs = await readFile(bundlePath, "utf-8"); const apiUrl = process.env.OT_API_URL ?? PUBLIC_API_URL; @@ -42,6 +47,7 @@ async function makeWidgetShell(bundleFile: string, title: string, toolName: stri <script> window.__OT_API_URL__ = "${apiUrl}"; window.__OT_WIDGET_TOOL__ = "${toolName}"; + window.__OT_DIRECT_FETCH__ = ${directFetch ? "true" : "false"}; window.configProfile = { isPartnerPreview: false, partnerTargetSectionIds: [], partnerDiseaseSectionIds: [], partnerDrugSectionIds: [], partnerEvidenceSectionIds: [], partnerDataTypes: [], partnerDataSources: [] }; </script> </head> @@ -214,11 +220,24 @@ export function createMcpServer(): McpServer { ); // Resource handler — serves the HTML shell immediately (no blocking). - // Data is not embedded here; it arrives via AppBridge tool-result postMessage. + // Data is not embedded here; it arrives via AppBridge tool-result postMessage, + // except for directFetch widgets, which fetch the GraphQL API directly (see CSP below). registerAppResource(server, def.title, resourceUri, { mimeType: RESOURCE_MIME_TYPE }, async () => { console.error(`[mcp] resource READ: ${resourceUri}`); - const html = await makeWidgetShell(def.bundleFile, def.title, def.toolName); - return { contents: [{ uri: resourceUri, mimeType: RESOURCE_MIME_TYPE, text: html }] }; + const html = await makeWidgetShell(def.bundleFile, def.title, def.toolName, def.directFetch); + const apiUrl = process.env.OT_API_URL ?? PUBLIC_API_URL; + return { + contents: [ + { + uri: resourceUri, + mimeType: RESOURCE_MIME_TYPE, + text: html, + ...(def.directFetch + ? { _meta: { ui: { csp: { connectDomains: [apiUrl] } } } } + : {}), + }, + ], + }; }); } diff --git a/apps/mcp-widgets-server/src/sections/registry.ts b/apps/mcp-widgets-server/src/sections/registry.ts index 792687e53..eea17dfe2 100644 --- a/apps/mcp-widgets-server/src/sections/registry.ts +++ b/apps/mcp-widgets-server/src/sections/registry.ts @@ -26,8 +26,8 @@ export type SectionDef = { prefetchExtraVariables?: Record<string, unknown>; /** * Override the auto-detected primary GQL query. Use when the section's query - * variable doesn't match the tool's input param (e.g. SharedTraitStudies needs - * diseaseIds, but the tool input is studyId). + * variable doesn't match the tool's input param. Only applies when directFetch + * is unset — every current entry is directFetch, so this is currently unused. */ primaryPrefetch?: { query: string; operationName: string }; /** @@ -43,6 +43,12 @@ export type SectionDef = { responseKey: string; }; }>; + /** + * Experimental: skip server-side prefetch entirely and let the widget iframe fetch + * the GraphQL API directly, allowlisted via CSP connectDomains. When set, no + * `prefetch` config is derived and the fetch interceptor is bypassed client-side. + */ + directFetch?: boolean; }; const TARGET_INPUT = [ @@ -84,6 +90,7 @@ export const SECTION_REGISTRY: SectionDef[] = [ "Shows cancer hallmark annotations for a target gene — which hallmarks it promotes or " + "suppresses, with supporting publication evidence.", inputParams: TARGET_INPUT, + directFetch: true, }, { entity: "target", @@ -93,6 +100,7 @@ export const SECTION_REGISTRY: SectionDef[] = [ "Shows chemical probes available for a target gene — highly selective tool compounds " + "used in pharmacological research, with probe quality scores and sources.", inputParams: TARGET_INPUT, + directFetch: true, }, { entity: "target", @@ -102,6 +110,7 @@ export const SECTION_REGISTRY: SectionDef[] = [ "Shows comparative genomics data for a target gene — orthologues across species " + "(human, mouse, rat, zebrafish, etc.) with homology scores.", inputParams: TARGET_INPUT, + directFetch: true, }, { entity: "target", @@ -111,6 +120,7 @@ export const SECTION_REGISTRY: SectionDef[] = [ "Shows DepMap CRISPR gene essentiality data for a target — cancer cell line " + "dependency scores across tissue types from the Cancer Dependency Map.", inputParams: TARGET_INPUT, + directFetch: true, }, { entity: "target", @@ -120,6 +130,7 @@ export const SECTION_REGISTRY: SectionDef[] = [ "Shows drugs and clinical candidates associated with a target gene — approved drugs, " + "clinical-stage compounds, mechanisms of action, and disease indications.", inputParams: TARGET_INPUT, + directFetch: true, }, { entity: "target", @@ -130,6 +141,7 @@ export const SECTION_REGISTRY: SectionDef[] = [ "median TPM values, tissue specificity scores, and distribution scores from GTEx, HPA, " + "and other sources.", inputParams: TARGET_INPUT, + directFetch: true, }, { entity: "target", @@ -139,6 +151,7 @@ export const SECTION_REGISTRY: SectionDef[] = [ "Shows Gene Ontology (GO) annotations for a target gene — molecular functions, " + "biological processes, and cellular components.", inputParams: TARGET_INPUT, + directFetch: true, }, { entity: "target", @@ -148,6 +161,7 @@ export const SECTION_REGISTRY: SectionDef[] = [ "Shows genetic constraint metrics for a target gene (pLI, LOEUF, Z-scores) from " + "gnomAD — indicating intolerance to loss-of-function variants.", inputParams: TARGET_INPUT, + directFetch: true, }, { entity: "target", @@ -157,6 +171,7 @@ export const SECTION_REGISTRY: SectionDef[] = [ "Shows mouse phenotype data for a target gene from IMPC — phenotypic categories " + "observed in knockout mice with significance scores.", inputParams: TARGET_INPUT, + directFetch: true, }, { entity: "target", @@ -166,6 +181,7 @@ export const SECTION_REGISTRY: SectionDef[] = [ "Shows Reactome pathway memberships for a target gene — biological pathways and " + "hierarchical pathway categories.", inputParams: TARGET_INPUT, + directFetch: true, }, { entity: "target", @@ -175,6 +191,7 @@ export const SECTION_REGISTRY: SectionDef[] = [ "Shows pharmacogenomics data for a target gene — genetic variants that affect drug " + "response and their clinical annotations from PharmGKB.", inputParams: TARGET_INPUT, + directFetch: true, }, { entity: "target", @@ -184,7 +201,7 @@ export const SECTION_REGISTRY: SectionDef[] = [ "Shows QTL credible sets linked to a target gene — eQTL and sQTL fine-mapped " + "credible sets from GTEx and other QTL datasets.", inputParams: TARGET_INPUT, - prefetchExtraVariables: { size: 500, index: 0 }, + directFetch: true, }, { entity: "target", @@ -194,6 +211,7 @@ export const SECTION_REGISTRY: SectionDef[] = [ "Shows target safety information — adverse effects, safety risk information, " + "and experimental toxicity data from curated sources.", inputParams: TARGET_INPUT, + directFetch: true, }, { entity: "target", @@ -203,6 +221,7 @@ export const SECTION_REGISTRY: SectionDef[] = [ "Shows tractability assessment for a target gene — small molecule, antibody, PROTAC, " + "and other drug modality predictions with supporting evidence buckets.", inputParams: TARGET_INPUT, + directFetch: true, }, // ── DISEASE ───────────────────────────────────────────────────────────────── @@ -214,6 +233,7 @@ export const SECTION_REGISTRY: SectionDef[] = [ "Shows drugs approved or in clinical trials for a disease — drug names, " + "clinical phases, mechanisms of action, and linked targets.", inputParams: DISEASE_INPUT, + directFetch: true, }, { entity: "disease", @@ -223,6 +243,7 @@ export const SECTION_REGISTRY: SectionDef[] = [ "Shows Open Targets experimental projects related to a disease — OTAR-funded " + "functional genomics and validation studies.", inputParams: DISEASE_INPUT, + directFetch: true, }, { entity: "disease", @@ -232,6 +253,7 @@ export const SECTION_REGISTRY: SectionDef[] = [ "Shows the disease ontology subgraph for a disease — its position in the EFO " + "hierarchy with parent and child disease terms.", inputParams: DISEASE_INPUT, + directFetch: true, }, { entity: "disease", @@ -241,7 +263,7 @@ export const SECTION_REGISTRY: SectionDef[] = [ "Shows phenotype annotations for a disease — HPO phenotype terms linked to " + "the disease with evidence from curated sources.", inputParams: DISEASE_INPUT, - prefetchExtraVariables: { size: 500, index: 0 }, + directFetch: true, }, // ── DRUG ──────────────────────────────────────────────────────────────────── @@ -253,7 +275,7 @@ export const SECTION_REGISTRY: SectionDef[] = [ "Shows adverse event data for a drug from FDA FAERS — significant adverse events " + "by MedDRA term with likelihood ratio scores.", inputParams: DRUG_INPUT, - prefetchExtraVariables: { size: 25, index: 0 }, + directFetch: true, }, { entity: "drug", @@ -262,41 +284,7 @@ export const SECTION_REGISTRY: SectionDef[] = [ description: "Shows drug clinical indications with investigational and approved indications from clinical trial records, including disease names, maximum clinical stage, and individual trial records in a detail panel.", inputParams: DRUG_INPUT, - extraPrefetches: [ - { - operationName: "ClinicalRecordsQuery", - query: ` - query ClinicalRecordsQuery($clinicalReportsIds: [String!]!) { - clinicalReports(clinicalReportsIds: $clinicalReportsIds) { - clinicalStage - id - source - trialLiterature - title - trialOverallStatus - trialStartDate - type - year - } - } - `, - variables: (_inputValue: string, primaryData: unknown) => { - const rows = (primaryData as any)?.drug?.indications?.rows ?? []; - const allIds: string[] = []; - for (const row of rows) { - for (const report of (row.clinicalReports ?? [])) { - allIds.push(report.id); - } - } - return { clinicalReportsIds: allIds }; - }, - filteredBy: { - requestVarName: "clinicalReportsIds", - itemIdField: "id", - responseKey: "clinicalReports", - }, - }, - ], + directFetch: true, }, { entity: "drug", @@ -306,6 +294,7 @@ export const SECTION_REGISTRY: SectionDef[] = [ "Shows drug safety warnings for a drug — black box warnings, withdrawn status, " + "and year of withdrawal with regulatory authority details.", inputParams: DRUG_INPUT, + directFetch: true, }, { entity: "drug", @@ -315,6 +304,7 @@ export const SECTION_REGISTRY: SectionDef[] = [ "Shows mechanisms of action for a drug — target proteins, action types " + "(agonist, antagonist, inhibitor, etc.), and source references.", inputParams: DRUG_INPUT, + directFetch: true, }, { entity: "drug", @@ -324,6 +314,7 @@ export const SECTION_REGISTRY: SectionDef[] = [ "Shows pharmacogenomics data for a drug — genetic variants that affect drug " + "response, phenotype categories, and clinical significance.", inputParams: DRUG_INPUT, + directFetch: true, }, // ── EVIDENCE ───────────────────────────────────────────────────────────────── @@ -335,7 +326,7 @@ export const SECTION_REGISTRY: SectionDef[] = [ "Shows CRISPR screen evidence linking a target to a disease — cancer cell " + "line screens showing gene essentiality in disease-relevant models.", inputParams: EVIDENCE_INPUT, - prefetchExtraVariables: { size: 500 }, + directFetch: true, }, { entity: "evidence", @@ -345,7 +336,7 @@ export const SECTION_REGISTRY: SectionDef[] = [ "Shows CRISPR modifier screen evidence — genetic interaction screens " + "identifying target-disease associations through functional genomics.", inputParams: EVIDENCE_INPUT, - prefetchExtraVariables: { size: 500 }, + directFetch: true, }, { entity: "evidence", @@ -355,6 +346,7 @@ export const SECTION_REGISTRY: SectionDef[] = [ "Shows cancer biomarker evidence — genomic alterations in the target gene " + "associated with drug response in specific cancer types.", inputParams: EVIDENCE_INPUT, + directFetch: true, }, { entity: "evidence", @@ -364,7 +356,7 @@ export const SECTION_REGISTRY: SectionDef[] = [ "Shows Cancer Gene Census evidence — curated cancer driver gene annotations " + "from COSMIC linking the target to the disease.", inputParams: EVIDENCE_INPUT, - prefetchExtraVariables: { size: 500 }, + directFetch: true, }, { entity: "evidence", @@ -374,7 +366,7 @@ export const SECTION_REGISTRY: SectionDef[] = [ "Shows clinical precedence evidence linking a target gene to a disease — approved and " + "investigational drugs, clinical phases, and trial outcomes.", inputParams: EVIDENCE_INPUT, - prefetchExtraVariables: { size: 10, cursor: null }, + directFetch: true, }, { entity: "evidence", @@ -384,7 +376,7 @@ export const SECTION_REGISTRY: SectionDef[] = [ "Shows ClinGen curated evidence — expert-curated gene-disease validity " + "classifications with supporting evidence summaries.", inputParams: EVIDENCE_INPUT, - prefetchExtraVariables: { size: 500 }, + directFetch: true, }, { entity: "evidence", @@ -394,7 +386,7 @@ export const SECTION_REGISTRY: SectionDef[] = [ "Shows ClinVar/EVA genetic evidence — clinically interpreted variants in the " + "target gene associated with the disease, with pathogenicity classifications.", inputParams: EVIDENCE_INPUT, - prefetchExtraVariables: { size: 10, cursor: null }, + directFetch: true, }, { entity: "evidence", @@ -404,7 +396,7 @@ export const SECTION_REGISTRY: SectionDef[] = [ "Shows ClinVar somatic evidence — somatic variants in the target gene " + "associated with the disease from clinical submissions.", inputParams: EVIDENCE_INPUT, - prefetchExtraVariables: { size: 10, cursor: null }, + directFetch: true, }, { entity: "evidence", @@ -414,7 +406,7 @@ export const SECTION_REGISTRY: SectionDef[] = [ "Shows Expression Atlas evidence — differential expression studies showing " + "the target gene is up- or down-regulated in the disease context.", inputParams: EVIDENCE_INPUT, - prefetchExtraVariables: { size: 500 }, + directFetch: true, }, { entity: "evidence", @@ -424,7 +416,7 @@ export const SECTION_REGISTRY: SectionDef[] = [ "Shows GWAS credible set evidence — fine-mapped GWAS loci where the target " + "gene is the top L2G candidate for the disease.", inputParams: EVIDENCE_INPUT, - prefetchExtraVariables: { size: 500 }, + directFetch: true, }, { entity: "evidence", @@ -434,7 +426,7 @@ export const SECTION_REGISTRY: SectionDef[] = [ "Shows Gene2Phenotype curated evidence — expert-curated gene-disease " + "associations from the G2P database with confidence levels.", inputParams: EVIDENCE_INPUT, - prefetchExtraVariables: { size: 500 }, + directFetch: true, }, { entity: "evidence", @@ -444,7 +436,7 @@ export const SECTION_REGISTRY: SectionDef[] = [ "Shows gene burden evidence — rare variant collapsing analyses associating " + "the target gene with the disease from biobank studies.", inputParams: EVIDENCE_INPUT, - prefetchExtraVariables: { size: 500 }, + directFetch: true, }, { entity: "evidence", @@ -454,7 +446,7 @@ export const SECTION_REGISTRY: SectionDef[] = [ "Shows Genomics England PanelApp evidence — curated gene-disease associations " + "from the GEL rare disease gene panels.", inputParams: EVIDENCE_INPUT, - prefetchExtraVariables: { size: 500 }, + directFetch: true, }, { entity: "evidence", @@ -464,7 +456,7 @@ export const SECTION_REGISTRY: SectionDef[] = [ "Shows IMPC mouse model evidence — phenotypes observed in knockout mice " + "that map to the human disease.", inputParams: EVIDENCE_INPUT, - prefetchExtraVariables: { size: 10, cursor: null }, + directFetch: true, }, { entity: "evidence", @@ -474,7 +466,7 @@ export const SECTION_REGISTRY: SectionDef[] = [ "Shows IntOGen cancer driver evidence — somatic mutation enrichment analysis " + "identifying the target gene as a cancer driver in the disease.", inputParams: EVIDENCE_INPUT, - prefetchExtraVariables: { size: 500 }, + directFetch: true, }, { entity: "evidence", @@ -484,7 +476,7 @@ export const SECTION_REGISTRY: SectionDef[] = [ "Shows Open Targets CRISPR evidence — OT-funded CRISPR validation screens " + "supporting the target-disease association.", inputParams: EVIDENCE_INPUT, - prefetchExtraVariables: { size: 500 }, + directFetch: true, }, { entity: "evidence", @@ -494,7 +486,7 @@ export const SECTION_REGISTRY: SectionDef[] = [ "Shows Open Targets ENCORE combinatorial CRISPR screen evidence — genetic " + "interaction data from OT's double-KO screens.", inputParams: EVIDENCE_INPUT, - prefetchExtraVariables: { size: 500 }, + directFetch: true, }, { entity: "evidence", @@ -504,7 +496,7 @@ export const SECTION_REGISTRY: SectionDef[] = [ "Shows Open Targets validation evidence — OT-funded experimental validation " + "studies supporting the target-disease hypothesis.", inputParams: EVIDENCE_INPUT, - prefetchExtraVariables: { size: 500 }, + directFetch: true, }, { entity: "evidence", @@ -514,7 +506,7 @@ export const SECTION_REGISTRY: SectionDef[] = [ "Shows Orphanet rare disease evidence — curated gene-disease associations " + "from the Orphanet rare disease database.", inputParams: EVIDENCE_INPUT, - prefetchExtraVariables: { size: 500 }, + directFetch: true, }, { entity: "evidence", @@ -524,7 +516,7 @@ export const SECTION_REGISTRY: SectionDef[] = [ "Shows Reactome pathway evidence — pathway disruption events linking the " + "target gene to the disease through biological pathways.", inputParams: EVIDENCE_INPUT, - prefetchExtraVariables: { size: 500 }, + directFetch: true, }, { entity: "evidence", @@ -534,7 +526,7 @@ export const SECTION_REGISTRY: SectionDef[] = [ "Shows UniProt literature evidence — manually curated disease annotations " + "from UniProtKB with supporting PubMed references.", inputParams: EVIDENCE_INPUT, - prefetchExtraVariables: { size: 500 }, + directFetch: true, }, { entity: "evidence", @@ -544,7 +536,7 @@ export const SECTION_REGISTRY: SectionDef[] = [ "Shows UniProt variant evidence — manually curated disease-causing variants " + "in the target gene from UniProtKB.", inputParams: EVIDENCE_INPUT, - prefetchExtraVariables: { size: 500 }, + directFetch: true, }, // ── CREDIBLE SET ───────────────────────────────────────────────────────────── @@ -555,6 +547,7 @@ export const SECTION_REGISTRY: SectionDef[] = [ description: "Shows the Locus-to-Gene (L2G) heatmap for a credible set — gene prioritisation scores and SHAP feature contributions used to identify causal genes at GWAS loci.", inputParams: CREDIBLE_SET_INPUT, + directFetch: true, }, { entity: "credibleSet", @@ -563,7 +556,7 @@ export const SECTION_REGISTRY: SectionDef[] = [ description: "Shows GWAS colocalisation results for a credible set — other GWAS traits and studies whose signals co-localise at this locus, with posterior probabilities.", inputParams: CREDIBLE_SET_INPUT, - prefetchExtraVariables: { size: 50, index: 0 }, + directFetch: true, }, { entity: "credibleSet", @@ -572,7 +565,7 @@ export const SECTION_REGISTRY: SectionDef[] = [ description: "Shows molecular QTL colocalisation for a credible set — eQTL, pQTL and sQTL signals from GTEx and other resources that co-localise at this locus.", inputParams: CREDIBLE_SET_INPUT, - prefetchExtraVariables: { size: 50, index: 0 }, + directFetch: true, }, { entity: "credibleSet", @@ -581,7 +574,7 @@ export const SECTION_REGISTRY: SectionDef[] = [ description: "Shows the variants within a credible set — posterior inclusion probabilities, allele frequencies, and functional annotations for all tagged variants.", inputParams: CREDIBLE_SET_INPUT, - prefetchExtraVariables: { size: 500, index: 0 }, + directFetch: true, }, { entity: "credibleSet", @@ -590,6 +583,7 @@ export const SECTION_REGISTRY: SectionDef[] = [ description: "Shows enhancer-to-gene (E2G) predictions for a credible set — regulatory links between non-coding variants and target genes from Activity-by-Contact and other models.", inputParams: CREDIBLE_SET_INPUT, + directFetch: true, }, // ── STUDY ──────────────────────────────────────────────────────────────────── @@ -600,7 +594,7 @@ export const SECTION_REGISTRY: SectionDef[] = [ description: "Shows GWAS credible sets for a study — Manhattan plot of fine-mapped loci with lead variants, p-values, fine-mapping confidence, and top L2G gene scores.", inputParams: STUDY_INPUT, - prefetchExtraVariables: { size: 500, index: 0 }, + directFetch: true, }, { entity: "study", @@ -609,6 +603,7 @@ export const SECTION_REGISTRY: SectionDef[] = [ description: "Shows QTL credible sets for a molecular QTL study — fine-mapped loci with lead variants and associated gene targets.", inputParams: STUDY_INPUT, + directFetch: true, }, { entity: "study", @@ -617,44 +612,7 @@ export const SECTION_REGISTRY: SectionDef[] = [ description: "Shows other GWAS studies that share the same disease or phenotype associations as a given study, with sample sizes, cohorts, and publications.", inputParams: STUDY_INPUT, - primaryPrefetch: { - operationName: "StudyDiseasesForSharedTraits", - query: `query StudyDiseasesForSharedTraits($studyId: String!) { study(studyId: $studyId) { diseases { id } } }`, - }, - extraPrefetches: [ - { - operationName: "SharedTraitStudiesQuery", - query: `query SharedTraitStudiesQuery($diseaseIds: [String!]!, $size: Int!, $index: Int!) { - sharedTraitStudies: studies(diseaseIds: $diseaseIds, page: { size: $size, index: $index }) { - count - rows { - id - traitFromSource - projectId - diseases { - id - name - } - publicationFirstAuthor - publicationDate - publicationJournal - nSamples - cohorts - ldPopulationStructure { - ldPopulation - relativeSampleSize - } - pubmedId - } - } -}`, - variables: (_inputValue: string, primaryData: unknown) => ({ - diseaseIds: (primaryData as any)?.study?.diseases?.map((d: { id: string }) => d.id) ?? [], - size: 500, - index: 0, - }), - }, - ], + directFetch: true, }, // ── VARIANT ────────────────────────────────────────────────────────────────── @@ -665,6 +623,7 @@ export const SECTION_REGISTRY: SectionDef[] = [ description: "Shows in-silico predictor scores for a variant — AlphaMissense, SIFT, LOFTEE, FoldX, GERP, VEP, and LoF curation scores as a normalised dot plot from likely benign to likely deleterious.", inputParams: VARIANT_INPUT, + directFetch: true, }, { entity: "variant", @@ -673,6 +632,7 @@ export const SECTION_REGISTRY: SectionDef[] = [ description: "Shows ClinVar/EVA clinical evidence for a variant — variant classifications, conditions, review status, and supporting evidence from clinical databases.", inputParams: VARIANT_INPUT, + directFetch: true, }, { entity: "variant", @@ -681,6 +641,7 @@ export const SECTION_REGISTRY: SectionDef[] = [ description: "Shows enhancer-to-gene (E2G) predictions for a variant — regulatory links to target genes from Activity-by-Contact and other functional genomics models.", inputParams: VARIANT_INPUT, + directFetch: true, }, { entity: "variant", @@ -689,7 +650,7 @@ export const SECTION_REGISTRY: SectionDef[] = [ description: "Shows GWAS credible sets containing a variant — fine-mapped loci across studies where this variant is included, with study traits and posterior probabilities.", inputParams: VARIANT_INPUT, - prefetchExtraVariables: { size: 500, index: 0 }, + directFetch: true, }, { entity: "variant", @@ -698,6 +659,7 @@ export const SECTION_REGISTRY: SectionDef[] = [ description: "Shows pharmacogenomics annotations for a variant — drug-gene interactions and phenotype associations from PharmGKB and other resources.", inputParams: VARIANT_INPUT, + directFetch: true, }, { entity: "variant", @@ -706,7 +668,7 @@ export const SECTION_REGISTRY: SectionDef[] = [ description: "Shows molecular QTL credible sets for a variant — eQTL, pQTL, and sQTL fine-mapped loci containing this variant across tissues and cell types.", inputParams: VARIANT_INPUT, - prefetchExtraVariables: { size: 500, index: 0 }, + directFetch: true, }, { entity: "variant", @@ -715,6 +677,7 @@ export const SECTION_REGISTRY: SectionDef[] = [ description: "Shows UniProt protein variant annotations for a variant — functional consequence, pathogenicity classification, and protein domain context.", inputParams: VARIANT_INPUT, + directFetch: true, }, { entity: "variant", @@ -723,5 +686,6 @@ export const SECTION_REGISTRY: SectionDef[] = [ description: "Shows Ensembl Variant Effect Predictor (VEP) annotations for a variant — transcript consequences, amino acid changes, and regulatory feature impacts across all overlapping genes.", inputParams: VARIANT_INPUT, + directFetch: true, }, ]; diff --git a/apps/mcp-widgets-server/src/widgets/index.ts b/apps/mcp-widgets-server/src/widgets/index.ts index cd71da918..e9b420c00 100644 --- a/apps/mcp-widgets-server/src/widgets/index.ts +++ b/apps/mcp-widgets-server/src/widgets/index.ts @@ -56,11 +56,14 @@ function loadSectionQuery(sectionPath: string): { query: string; operationName: /** Derives a WidgetDef from a SectionDef registry entry. */ function deriveSectionWidgetDef(def: SectionDef): WidgetDef { - // If primaryPrefetch is provided, use it; otherwise auto-detect from .gql file - const prefetchBase = def.primaryPrefetch ?? (() => { - const { query, operationName } = loadSectionQuery(def.sectionPath); - return { query, operationName }; - })(); + // directFetch widgets skip server-side prefetch entirely — no query file needed. + const prefetchBase = def.directFetch + ? null + : (def.primaryPrefetch ?? + (() => { + const { query, operationName } = loadSectionQuery(def.sectionPath); + return { query, operationName }; + })()); const sectionId = sectionPathToId(def.sectionPath); const sectionName = def.sectionPath.split("/").pop()!; @@ -77,12 +80,15 @@ function deriveSectionWidgetDef(def: SectionDef): WidgetDef { bundleFile: `${sectionId}.js`, title: `${readableName} Widget`, successMessage: `${readableName} widget rendered successfully in the chat interface.`, - prefetch: { - operationName: prefetchBase.operationName, - query: prefetchBase.query, - extraVariables: def.prefetchExtraVariables, - extraPrefetches: def.extraPrefetches, - }, + prefetch: prefetchBase + ? { + operationName: prefetchBase.operationName, + query: prefetchBase.query, + extraVariables: def.prefetchExtraVariables, + extraPrefetches: def.extraPrefetches, + } + : undefined, + directFetch: def.directFetch, }; } diff --git a/apps/mcp-widgets-server/src/widgets/types.ts b/apps/mcp-widgets-server/src/widgets/types.ts index c32c7e8ec..1ef4ae757 100644 --- a/apps/mcp-widgets-server/src/widgets/types.ts +++ b/apps/mcp-widgets-server/src/widgets/types.ts @@ -62,4 +62,9 @@ export type WidgetDef = { */ extractExtraFetches?: (data: unknown) => Array<{ url: string; contentType: string }>; }; + /** + * Experimental: when true, no server-side prefetch runs. The widget iframe fetches + * the GraphQL API directly instead, allowlisted via the resource's CSP connectDomains. + */ + directFetch?: boolean; }; diff --git a/apps/mcp-widgets-server/widget-src/shared/createWidgetEntry.tsx b/apps/mcp-widgets-server/widget-src/shared/createWidgetEntry.tsx index 636ffb094..8510fa9e8 100644 --- a/apps/mcp-widgets-server/widget-src/shared/createWidgetEntry.tsx +++ b/apps/mcp-widgets-server/widget-src/shared/createWidgetEntry.tsx @@ -89,10 +89,16 @@ function applyData(pf: PrefetchResult) { const _originalFetch = window.fetch.bind(window); +// Set by the HTML shell before this bundle runs. When true, this widget skips +// server-side prefetch entirely and fetches the GraphQL API directly — the +// interceptor must get out of the way and let requests through unmolested. +const _isDirectFetch = Boolean((window as { __OT_DIRECT_FETCH__?: boolean }).__OT_DIRECT_FETCH__); + window.fetch = function otFetchInterceptor( url: RequestInfo | URL, opts?: RequestInit ): Promise<Response> { + if (_isDirectFetch) return _originalFetch(url as RequestInfo, opts); try { // Cached URL assets (e.g. AlphaFold CIF files fetched by the 3D widget) const urlStr = @@ -210,6 +216,10 @@ export function mountWidget<TArgs extends Record<string, unknown>>( ); if (extracted) setArgs(extracted); + // directFetch widgets have no server-side prefetch to fetch — the + // widget's own Apollo client hits the GraphQL API directly instead. + if (_isDirectFetch) return; + const toolName = ( window as { __OT_WIDGET_TOOL__?: string } ).__OT_WIDGET_TOOL__; From 030fbeae45f4f35fbffe3ddf846be9dc387c8e05 Mon Sep 17 00:00:00 2001 From: Carlos Cruz <me@carcruzcast.com> Date: Sat, 11 Jul 2026 17:22:29 +0100 Subject: [PATCH 3/6] Refactor widget data fetching to eliminate server-side prefetching - Updated SectionDef to remove prefetch-related properties and directFetch flag. - Modified SECTION_REGISTRY to reflect changes in widget definitions. - Simplified widget derivation logic in index.ts, removing prefetch handling. - Adjusted molecularStructureWidget to fetch data directly from the iframe. - Cleaned up types related to prefetching in types.ts. - Removed unused prefetch data handling and fetch interceptor logic in createWidgetEntry.tsx. --- apps/mcp-widgets-server/WIDGETS.md | 23 +- apps/mcp-widgets-server/src/mcp-server.ts | 202 ++---------------- .../src/sections/registry.ts | 98 +-------- apps/mcp-widgets-server/src/widgets/index.ts | 70 +----- .../src/widgets/molecular-structure.ts | 35 +-- apps/mcp-widgets-server/src/widgets/types.ts | 50 +---- .../widget-src/shared/createWidgetEntry.tsx | 185 +--------------- 7 files changed, 44 insertions(+), 619 deletions(-) diff --git a/apps/mcp-widgets-server/WIDGETS.md b/apps/mcp-widgets-server/WIDGETS.md index 96f033faa..2e89907f4 100644 --- a/apps/mcp-widgets-server/WIDGETS.md +++ b/apps/mcp-widgets-server/WIDGETS.md @@ -10,11 +10,9 @@ apps/mcp-widgets-server/ │ ├── widgets/ │ │ ├── index.ts # Widget registry builder + sectionPathToId utility │ │ ├── molecular-structure.ts # Manual widget def (AlphaFold 3D viewer) -│ │ └── types.ts # WidgetDef type, makeWidgetShell, toAnthropicTool -│ ├── mcp-server.ts # MCP tool + resource registration, GraphQL prefetch -│ ├── chat.ts # Chat workspace HTTP handler +│ │ └── types.ts # WidgetDef type +│ ├── mcp-server.ts # MCP tool + resource registration, widget HTML shell, CSP │ ├── index.ts # Server entrypoint -│ ├── otp-client.ts # OT GraphQL client helpers │ └── stdio.ts # stdio MCP transport │ ├── widget-src/ # Browser-side React code (compiled to IIFE bundles) @@ -36,14 +34,14 @@ apps/mcp-widgets-server/ | | `src/` | `widget-src/` | |---|---|---| | **Runs in** | Node.js (server / MCP host) | Browser (iframe inside Claude Desktop) | -| **Purpose** | Registers MCP tools, fetches GraphQL data server-side, serves widget HTML | React components compiled into self-contained IIFE bundles | +| **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` runs the GraphQL prefetch and returns data -via `_meta.prefetchedData`. The widget HTML shell (with the bundle inlined) renders in an iframe, -and a `postMessage` interceptor delivers the prefetched data to Apollo without any live network -requests from the widget. +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. --- @@ -53,8 +51,9 @@ requests from the widget. 2. Run `yarn build:widgets:<entity>` (or `yarn build:widgets:sections` for all) 3. Restart Claude Desktop -For sections where the GQL query variable does not match the input param name (or that need a -two-step prefetch), use `primaryPrefetch` and/or `extraPrefetches` in the `SectionDef`. +If the section fetches anything beyond the OT GraphQL API (e.g. a third-party CDN), add its +origin to `extraConnectDomains` in the derived `WidgetDef` (see `molecular-structure.ts` for +an example with AlphaFold and UniProt). --- @@ -160,7 +159,7 @@ two-step prefetch), use `primaryPrefetch` and/or `extraPrefetches` in the `Secti | Tool name | Section | Notes | |-----------|---------|-------| -| `get_molecular_structure_widget` | `variant/MolecularStructure` | Custom 3D AlphaFold viewer; prefetches CIF file server-side | +| `get_molecular_structure_widget` | `variant/MolecularStructure` | Custom 3D AlphaFold viewer; fetches CIF/pathogenicity/domain data directly from AlphaFold + UniProt (extra CSP `connectDomains`) | --- diff --git a/apps/mcp-widgets-server/src/mcp-server.ts b/apps/mcp-widgets-server/src/mcp-server.ts index 850206bb6..aadc8dc60 100644 --- a/apps/mcp-widgets-server/src/mcp-server.ts +++ b/apps/mcp-widgets-server/src/mcp-server.ts @@ -12,20 +12,11 @@ const PUBLIC_API_URL = "https://api.platform.opentargets.org/api/v4/graphql"; /** * Builds a self-contained HTML string with the widget IIFE bundle inlined. - * - * Data delivery follows the standard MCP Apps pattern: - * widget's ontoolinput → app.callServerTool("ot_fetch_widget_data") → - * result.structuredContent → fetch interceptor cache → Apollo resolves. - * - * window.__OT_WIDGET_TOOL__ is set here so createWidgetEntry.tsx knows which - * tool to call via callServerTool without needing to parse hostContext. + * The widget's own Apollo client fetches the GraphQL API directly from the + * iframe — no server-side prefetch. Allowed origins are declared via CSP + * connectDomains on the resource (see registerAppResource below). */ -async function makeWidgetShell( - bundleFile: string, - title: string, - toolName: string, - directFetch?: boolean -): Promise<string> { +async function makeWidgetShell(bundleFile: string, title: string): Promise<string> { const bundlePath = new URL(`../dist/widgets/${bundleFile}`, import.meta.url).pathname; const bundleJs = await readFile(bundlePath, "utf-8"); const apiUrl = process.env.OT_API_URL ?? PUBLIC_API_URL; @@ -46,8 +37,6 @@ async function makeWidgetShell( </style> <script> window.__OT_API_URL__ = "${apiUrl}"; - window.__OT_WIDGET_TOOL__ = "${toolName}"; - window.__OT_DIRECT_FETCH__ = ${directFetch ? "true" : "false"}; window.configProfile = { isPartnerPreview: false, partnerTargetSectionIds: [], partnerDiseaseSectionIds: [], partnerDrugSectionIds: [], partnerEvidenceSectionIds: [], partnerDataTypes: [], partnerDataSources: [] }; </script> </head> @@ -58,122 +47,6 @@ async function makeWidgetShell( </html>`; } -type PrefetchResult = { - operations: Array<{ operationName: string; data: unknown }>; - /** Variable-filtered operations: interceptor filters allItems by request variables at call time */ - filteredOps?: Array<{ - operationName: string; - allItems: unknown[]; - itemIdField: string; - requestVarName: string; - responseKey: string; - }>; - urlData?: Array<{ url: string; text: string; contentType: string }>; -}; - -/** Runs a single GraphQL request against the OT API. */ -async function gqlFetch( - apiUrl: string, - operationName: string, - query: string, - variables: Record<string, unknown> -): Promise<unknown> { - const response = await fetch(apiUrl, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ operationName, query, variables }), - }); - const json = (await response.json()) as { data?: unknown }; - return json.data; -} - -/** Fetches GraphQL data server-side for widgets that define prefetch config. */ -async function fetchPrefetchData( - def: { - prefetch: NonNullable<(typeof import("./widgets/index.js").WIDGET_REGISTRY)[number]["prefetch"]>; - inputParam: { name: string }; - inputParams?: Array<{ name: string }>; - }, - inputValues: Record<string, string> -): Promise<PrefetchResult | null> { - const apiUrl = process.env.GRAPHQL_API_URL ?? process.env.OT_API_URL ?? PUBLIC_API_URL; - try { - const primaryVariables = { - ...inputValues, - ...(def.prefetch.extraVariables ?? {}), - }; - const primaryData = await gqlFetch( - apiUrl, - def.prefetch.operationName, - def.prefetch.query, - primaryVariables - ); - - const operations: Array<{ operationName: string; data: unknown }> = [ - { operationName: def.prefetch.operationName, data: primaryData }, - ]; - - const filteredOps: PrefetchResult["filteredOps"] = []; - - // Run extra queries in parallel (all depend only on primaryData, not each other) - if (def.prefetch.extraPrefetches) { - const firstInputValue = inputValues[def.inputParam.name] ?? Object.values(inputValues)[0] ?? ""; - await Promise.all( - def.prefetch.extraPrefetches.map(async extra => { - try { - const extraData = await gqlFetch( - apiUrl, - extra.operationName, - extra.query, - extra.variables(firstInputValue, primaryData) - ); - if (extra.filteredBy) { - const fb = extra.filteredBy; - const allItems = (extraData as any)?.[fb.responseKey] ?? []; - filteredOps.push({ - operationName: extra.operationName, - allItems, - itemIdField: fb.itemIdField, - requestVarName: fb.requestVarName, - responseKey: fb.responseKey, - }); - } else { - operations.push({ operationName: extra.operationName, data: extraData }); - } - } catch (err) { - console.error(`[mcp] extra prefetch failed for ${extra.operationName}:`, err); - } - }) - ); - } - - // Fetch extra URL assets (e.g. AlphaFold CIF) derived from primary data - let urlData: Array<{ url: string; text: string; contentType: string }> | undefined; - if (def.prefetch.extractExtraFetches) { - const extraFetches = def.prefetch.extractExtraFetches(primaryData); - if (extraFetches.length > 0) { - urlData = await Promise.all( - extraFetches.map(async ({ url, contentType }) => { - try { - const r = await fetch(url); - const text = await r.text(); - return { url, text, contentType }; - } catch (err) { - console.error(`[mcp] extra fetch failed for ${url}:`, err); - return { url, text: "", contentType }; - } - }) - ); - } - } - - return { operations, filteredOps: filteredOps.length > 0 ? filteredOps : undefined, urlData }; - } catch (err) { - console.error(`[mcp] prefetch failed for ${def.prefetch.operationName}:`, err); - return null; - } -} - export function createMcpServer(): McpServer { const server = new McpServer({ name: "ot-widgets-server", version: "0.1.0" }); @@ -195,36 +68,19 @@ export function createMcpServer(): McpServer { }, async input => { console.error(`[mcp] tool called: ${def.toolName}`, input); - const inputValues = Object.fromEntries( - params.map(p => [p.name, String(input[p.name as keyof typeof input] ?? "")]) - ); - - let prefetched = null; - if (def.prefetch) { - prefetched = await fetchPrefetchData( - def as Parameters<typeof fetchPrefetchData>[0], - inputValues - ); - } - - console.error(`[mcp] prefetch done for ${def.toolName}`); - return { content: [{ type: "text" as const, text: def.successMessage }], - // structuredContent is the spec-defined field for UI data; the host forwards it - // to the widget via ui/notifications/tool-result without adding it to model context. - structuredContent: prefetched ?? undefined, _meta: { ui: { resourceUri } }, }; } ); - // Resource handler — serves the HTML shell immediately (no blocking). - // Data is not embedded here; it arrives via AppBridge tool-result postMessage, - // except for directFetch widgets, which fetch the GraphQL API directly (see CSP below). + // Resource handler — serves the HTML shell. The widget's Apollo client fetches + // data directly from the iframe once mounted; CSP connectDomains allowlists the + // GraphQL API plus any extra origins the widget needs (e.g. AlphaFold, UniProt). registerAppResource(server, def.title, resourceUri, { mimeType: RESOURCE_MIME_TYPE }, async () => { console.error(`[mcp] resource READ: ${resourceUri}`); - const html = await makeWidgetShell(def.bundleFile, def.title, def.toolName, def.directFetch); + const html = await makeWidgetShell(def.bundleFile, def.title); const apiUrl = process.env.OT_API_URL ?? PUBLIC_API_URL; return { contents: [ @@ -232,48 +88,16 @@ export function createMcpServer(): McpServer { uri: resourceUri, mimeType: RESOURCE_MIME_TYPE, text: html, - ...(def.directFetch - ? { _meta: { ui: { csp: { connectDomains: [apiUrl] } } } } - : {}), + _meta: { + ui: { + csp: { connectDomains: [apiUrl, ...(def.extraConnectDomains ?? [])] }, + }, + }, }, ], }; }); } - // Internal tool called by widgets via app.callServerTool() in ontoolinput. - // This bypasses the notification path (where Claude Desktop strips structuredContent) - // and returns data through the request/response path instead. - server.tool( - "ot_fetch_widget_data", - { - tool: z.string().describe("The widget tool name to fetch prefetched data for"), - inputJson: z.string().describe("JSON-encoded tool input arguments"), - }, - async ({ tool, inputJson }) => { - const def = WIDGET_REGISTRY.find(w => w.toolName === tool); - if (!def?.prefetch) { - return { content: [{ type: "text" as const, text: "no prefetch config" }] }; - } - - const args = JSON.parse(inputJson) as Record<string, unknown>; - const params = def.inputParams ?? [def.inputParam]; - const inputValues = Object.fromEntries( - params.map(p => [p.name, String(args[p.name] ?? "")]) - ); - - console.error(`[mcp] ot_fetch_widget_data: tool=${tool}`, inputValues); - const prefetched = await fetchPrefetchData( - def as Parameters<typeof fetchPrefetchData>[0], - inputValues - ); - - return { - content: [{ type: "text" as const, text: "ok" }], - structuredContent: prefetched ?? undefined, - }; - } - ); - return server; } diff --git a/apps/mcp-widgets-server/src/sections/registry.ts b/apps/mcp-widgets-server/src/sections/registry.ts index eea17dfe2..21425bfc8 100644 --- a/apps/mcp-widgets-server/src/sections/registry.ts +++ b/apps/mcp-widgets-server/src/sections/registry.ts @@ -1,9 +1,10 @@ /** * Registry of all section-based MCP widget tools. * - * Each entry defines one MCP tool that renders a section Body component - * from packages/sections. The bundle file, GQL query, and operation name - * are auto-derived at build/server-startup time from the sectionPath. + * Each entry defines one MCP tool that renders a section Body component from + * packages/sections. The widget's own Apollo client fetches GraphQL data directly + * from the iframe (no server-side prefetch) — see src/mcp-server.ts for the CSP + * connectDomains allowlist that makes this possible. * * Naming convention for toolName: get_{entity}_{section_snake_case}_widget * Bundle files: {entity}-{section-kebab-case}.js (e.g. target-tractability.js) @@ -19,36 +20,6 @@ export type SectionDef = { description: string; /** Input parameters. One for single-entity sections, two for evidence. */ inputParams: ReadonlyArray<{ name: string; description: string }>; - /** - * Extra static variables merged into the prefetch GraphQL request alongside - * the primary input(s). Used for pagination defaults (size, index, cursor). - */ - prefetchExtraVariables?: Record<string, unknown>; - /** - * Override the auto-detected primary GQL query. Use when the section's query - * variable doesn't match the tool's input param. Only applies when directFetch - * is unset — every current entry is directFetch, so this is currently unused. - */ - primaryPrefetch?: { query: string; operationName: string }; - /** - * Additional queries run after the primary, same shape as WidgetDef.prefetch.extraPrefetches. - */ - extraPrefetches?: Array<{ - query: string; - operationName: string; - variables: (inputValue: string, primaryData: unknown) => Record<string, unknown>; - filteredBy?: { - requestVarName: string; - itemIdField: string; - responseKey: string; - }; - }>; - /** - * Experimental: skip server-side prefetch entirely and let the widget iframe fetch - * the GraphQL API directly, allowlisted via CSP connectDomains. When set, no - * `prefetch` config is derived and the fetch interceptor is bypassed client-side. - */ - directFetch?: boolean; }; const TARGET_INPUT = [ @@ -90,7 +61,6 @@ export const SECTION_REGISTRY: SectionDef[] = [ "Shows cancer hallmark annotations for a target gene — which hallmarks it promotes or " + "suppresses, with supporting publication evidence.", inputParams: TARGET_INPUT, - directFetch: true, }, { entity: "target", @@ -100,7 +70,6 @@ export const SECTION_REGISTRY: SectionDef[] = [ "Shows chemical probes available for a target gene — highly selective tool compounds " + "used in pharmacological research, with probe quality scores and sources.", inputParams: TARGET_INPUT, - directFetch: true, }, { entity: "target", @@ -110,7 +79,6 @@ export const SECTION_REGISTRY: SectionDef[] = [ "Shows comparative genomics data for a target gene — orthologues across species " + "(human, mouse, rat, zebrafish, etc.) with homology scores.", inputParams: TARGET_INPUT, - directFetch: true, }, { entity: "target", @@ -120,7 +88,6 @@ export const SECTION_REGISTRY: SectionDef[] = [ "Shows DepMap CRISPR gene essentiality data for a target — cancer cell line " + "dependency scores across tissue types from the Cancer Dependency Map.", inputParams: TARGET_INPUT, - directFetch: true, }, { entity: "target", @@ -130,7 +97,6 @@ export const SECTION_REGISTRY: SectionDef[] = [ "Shows drugs and clinical candidates associated with a target gene — approved drugs, " + "clinical-stage compounds, mechanisms of action, and disease indications.", inputParams: TARGET_INPUT, - directFetch: true, }, { entity: "target", @@ -141,7 +107,6 @@ export const SECTION_REGISTRY: SectionDef[] = [ "median TPM values, tissue specificity scores, and distribution scores from GTEx, HPA, " + "and other sources.", inputParams: TARGET_INPUT, - directFetch: true, }, { entity: "target", @@ -151,7 +116,6 @@ export const SECTION_REGISTRY: SectionDef[] = [ "Shows Gene Ontology (GO) annotations for a target gene — molecular functions, " + "biological processes, and cellular components.", inputParams: TARGET_INPUT, - directFetch: true, }, { entity: "target", @@ -161,7 +125,6 @@ export const SECTION_REGISTRY: SectionDef[] = [ "Shows genetic constraint metrics for a target gene (pLI, LOEUF, Z-scores) from " + "gnomAD — indicating intolerance to loss-of-function variants.", inputParams: TARGET_INPUT, - directFetch: true, }, { entity: "target", @@ -171,7 +134,6 @@ export const SECTION_REGISTRY: SectionDef[] = [ "Shows mouse phenotype data for a target gene from IMPC — phenotypic categories " + "observed in knockout mice with significance scores.", inputParams: TARGET_INPUT, - directFetch: true, }, { entity: "target", @@ -181,7 +143,6 @@ export const SECTION_REGISTRY: SectionDef[] = [ "Shows Reactome pathway memberships for a target gene — biological pathways and " + "hierarchical pathway categories.", inputParams: TARGET_INPUT, - directFetch: true, }, { entity: "target", @@ -191,7 +152,6 @@ export const SECTION_REGISTRY: SectionDef[] = [ "Shows pharmacogenomics data for a target gene — genetic variants that affect drug " + "response and their clinical annotations from PharmGKB.", inputParams: TARGET_INPUT, - directFetch: true, }, { entity: "target", @@ -201,7 +161,6 @@ export const SECTION_REGISTRY: SectionDef[] = [ "Shows QTL credible sets linked to a target gene — eQTL and sQTL fine-mapped " + "credible sets from GTEx and other QTL datasets.", inputParams: TARGET_INPUT, - directFetch: true, }, { entity: "target", @@ -211,7 +170,6 @@ export const SECTION_REGISTRY: SectionDef[] = [ "Shows target safety information — adverse effects, safety risk information, " + "and experimental toxicity data from curated sources.", inputParams: TARGET_INPUT, - directFetch: true, }, { entity: "target", @@ -221,7 +179,6 @@ export const SECTION_REGISTRY: SectionDef[] = [ "Shows tractability assessment for a target gene — small molecule, antibody, PROTAC, " + "and other drug modality predictions with supporting evidence buckets.", inputParams: TARGET_INPUT, - directFetch: true, }, // ── DISEASE ───────────────────────────────────────────────────────────────── @@ -233,7 +190,6 @@ export const SECTION_REGISTRY: SectionDef[] = [ "Shows drugs approved or in clinical trials for a disease — drug names, " + "clinical phases, mechanisms of action, and linked targets.", inputParams: DISEASE_INPUT, - directFetch: true, }, { entity: "disease", @@ -243,7 +199,6 @@ export const SECTION_REGISTRY: SectionDef[] = [ "Shows Open Targets experimental projects related to a disease — OTAR-funded " + "functional genomics and validation studies.", inputParams: DISEASE_INPUT, - directFetch: true, }, { entity: "disease", @@ -253,7 +208,6 @@ export const SECTION_REGISTRY: SectionDef[] = [ "Shows the disease ontology subgraph for a disease — its position in the EFO " + "hierarchy with parent and child disease terms.", inputParams: DISEASE_INPUT, - directFetch: true, }, { entity: "disease", @@ -263,7 +217,6 @@ export const SECTION_REGISTRY: SectionDef[] = [ "Shows phenotype annotations for a disease — HPO phenotype terms linked to " + "the disease with evidence from curated sources.", inputParams: DISEASE_INPUT, - directFetch: true, }, // ── DRUG ──────────────────────────────────────────────────────────────────── @@ -275,7 +228,6 @@ export const SECTION_REGISTRY: SectionDef[] = [ "Shows adverse event data for a drug from FDA FAERS — significant adverse events " + "by MedDRA term with likelihood ratio scores.", inputParams: DRUG_INPUT, - directFetch: true, }, { entity: "drug", @@ -284,7 +236,6 @@ export const SECTION_REGISTRY: SectionDef[] = [ description: "Shows drug clinical indications with investigational and approved indications from clinical trial records, including disease names, maximum clinical stage, and individual trial records in a detail panel.", inputParams: DRUG_INPUT, - directFetch: true, }, { entity: "drug", @@ -294,7 +245,6 @@ export const SECTION_REGISTRY: SectionDef[] = [ "Shows drug safety warnings for a drug — black box warnings, withdrawn status, " + "and year of withdrawal with regulatory authority details.", inputParams: DRUG_INPUT, - directFetch: true, }, { entity: "drug", @@ -304,7 +254,6 @@ export const SECTION_REGISTRY: SectionDef[] = [ "Shows mechanisms of action for a drug — target proteins, action types " + "(agonist, antagonist, inhibitor, etc.), and source references.", inputParams: DRUG_INPUT, - directFetch: true, }, { entity: "drug", @@ -314,7 +263,6 @@ export const SECTION_REGISTRY: SectionDef[] = [ "Shows pharmacogenomics data for a drug — genetic variants that affect drug " + "response, phenotype categories, and clinical significance.", inputParams: DRUG_INPUT, - directFetch: true, }, // ── EVIDENCE ───────────────────────────────────────────────────────────────── @@ -326,7 +274,6 @@ export const SECTION_REGISTRY: SectionDef[] = [ "Shows CRISPR screen evidence linking a target to a disease — cancer cell " + "line screens showing gene essentiality in disease-relevant models.", inputParams: EVIDENCE_INPUT, - directFetch: true, }, { entity: "evidence", @@ -336,7 +283,6 @@ export const SECTION_REGISTRY: SectionDef[] = [ "Shows CRISPR modifier screen evidence — genetic interaction screens " + "identifying target-disease associations through functional genomics.", inputParams: EVIDENCE_INPUT, - directFetch: true, }, { entity: "evidence", @@ -346,7 +292,6 @@ export const SECTION_REGISTRY: SectionDef[] = [ "Shows cancer biomarker evidence — genomic alterations in the target gene " + "associated with drug response in specific cancer types.", inputParams: EVIDENCE_INPUT, - directFetch: true, }, { entity: "evidence", @@ -356,7 +301,6 @@ export const SECTION_REGISTRY: SectionDef[] = [ "Shows Cancer Gene Census evidence — curated cancer driver gene annotations " + "from COSMIC linking the target to the disease.", inputParams: EVIDENCE_INPUT, - directFetch: true, }, { entity: "evidence", @@ -366,7 +310,6 @@ export const SECTION_REGISTRY: SectionDef[] = [ "Shows clinical precedence evidence linking a target gene to a disease — approved and " + "investigational drugs, clinical phases, and trial outcomes.", inputParams: EVIDENCE_INPUT, - directFetch: true, }, { entity: "evidence", @@ -376,7 +319,6 @@ export const SECTION_REGISTRY: SectionDef[] = [ "Shows ClinGen curated evidence — expert-curated gene-disease validity " + "classifications with supporting evidence summaries.", inputParams: EVIDENCE_INPUT, - directFetch: true, }, { entity: "evidence", @@ -386,7 +328,6 @@ export const SECTION_REGISTRY: SectionDef[] = [ "Shows ClinVar/EVA genetic evidence — clinically interpreted variants in the " + "target gene associated with the disease, with pathogenicity classifications.", inputParams: EVIDENCE_INPUT, - directFetch: true, }, { entity: "evidence", @@ -396,7 +337,6 @@ export const SECTION_REGISTRY: SectionDef[] = [ "Shows ClinVar somatic evidence — somatic variants in the target gene " + "associated with the disease from clinical submissions.", inputParams: EVIDENCE_INPUT, - directFetch: true, }, { entity: "evidence", @@ -406,7 +346,6 @@ export const SECTION_REGISTRY: SectionDef[] = [ "Shows Expression Atlas evidence — differential expression studies showing " + "the target gene is up- or down-regulated in the disease context.", inputParams: EVIDENCE_INPUT, - directFetch: true, }, { entity: "evidence", @@ -416,7 +355,6 @@ export const SECTION_REGISTRY: SectionDef[] = [ "Shows GWAS credible set evidence — fine-mapped GWAS loci where the target " + "gene is the top L2G candidate for the disease.", inputParams: EVIDENCE_INPUT, - directFetch: true, }, { entity: "evidence", @@ -426,7 +364,6 @@ export const SECTION_REGISTRY: SectionDef[] = [ "Shows Gene2Phenotype curated evidence — expert-curated gene-disease " + "associations from the G2P database with confidence levels.", inputParams: EVIDENCE_INPUT, - directFetch: true, }, { entity: "evidence", @@ -436,7 +373,6 @@ export const SECTION_REGISTRY: SectionDef[] = [ "Shows gene burden evidence — rare variant collapsing analyses associating " + "the target gene with the disease from biobank studies.", inputParams: EVIDENCE_INPUT, - directFetch: true, }, { entity: "evidence", @@ -446,7 +382,6 @@ export const SECTION_REGISTRY: SectionDef[] = [ "Shows Genomics England PanelApp evidence — curated gene-disease associations " + "from the GEL rare disease gene panels.", inputParams: EVIDENCE_INPUT, - directFetch: true, }, { entity: "evidence", @@ -456,7 +391,6 @@ export const SECTION_REGISTRY: SectionDef[] = [ "Shows IMPC mouse model evidence — phenotypes observed in knockout mice " + "that map to the human disease.", inputParams: EVIDENCE_INPUT, - directFetch: true, }, { entity: "evidence", @@ -466,7 +400,6 @@ export const SECTION_REGISTRY: SectionDef[] = [ "Shows IntOGen cancer driver evidence — somatic mutation enrichment analysis " + "identifying the target gene as a cancer driver in the disease.", inputParams: EVIDENCE_INPUT, - directFetch: true, }, { entity: "evidence", @@ -476,7 +409,6 @@ export const SECTION_REGISTRY: SectionDef[] = [ "Shows Open Targets CRISPR evidence — OT-funded CRISPR validation screens " + "supporting the target-disease association.", inputParams: EVIDENCE_INPUT, - directFetch: true, }, { entity: "evidence", @@ -486,7 +418,6 @@ export const SECTION_REGISTRY: SectionDef[] = [ "Shows Open Targets ENCORE combinatorial CRISPR screen evidence — genetic " + "interaction data from OT's double-KO screens.", inputParams: EVIDENCE_INPUT, - directFetch: true, }, { entity: "evidence", @@ -496,7 +427,6 @@ export const SECTION_REGISTRY: SectionDef[] = [ "Shows Open Targets validation evidence — OT-funded experimental validation " + "studies supporting the target-disease hypothesis.", inputParams: EVIDENCE_INPUT, - directFetch: true, }, { entity: "evidence", @@ -506,7 +436,6 @@ export const SECTION_REGISTRY: SectionDef[] = [ "Shows Orphanet rare disease evidence — curated gene-disease associations " + "from the Orphanet rare disease database.", inputParams: EVIDENCE_INPUT, - directFetch: true, }, { entity: "evidence", @@ -516,7 +445,6 @@ export const SECTION_REGISTRY: SectionDef[] = [ "Shows Reactome pathway evidence — pathway disruption events linking the " + "target gene to the disease through biological pathways.", inputParams: EVIDENCE_INPUT, - directFetch: true, }, { entity: "evidence", @@ -526,7 +454,6 @@ export const SECTION_REGISTRY: SectionDef[] = [ "Shows UniProt literature evidence — manually curated disease annotations " + "from UniProtKB with supporting PubMed references.", inputParams: EVIDENCE_INPUT, - directFetch: true, }, { entity: "evidence", @@ -536,7 +463,6 @@ export const SECTION_REGISTRY: SectionDef[] = [ "Shows UniProt variant evidence — manually curated disease-causing variants " + "in the target gene from UniProtKB.", inputParams: EVIDENCE_INPUT, - directFetch: true, }, // ── CREDIBLE SET ───────────────────────────────────────────────────────────── @@ -547,7 +473,6 @@ export const SECTION_REGISTRY: SectionDef[] = [ description: "Shows the Locus-to-Gene (L2G) heatmap for a credible set — gene prioritisation scores and SHAP feature contributions used to identify causal genes at GWAS loci.", inputParams: CREDIBLE_SET_INPUT, - directFetch: true, }, { entity: "credibleSet", @@ -556,7 +481,6 @@ export const SECTION_REGISTRY: SectionDef[] = [ description: "Shows GWAS colocalisation results for a credible set — other GWAS traits and studies whose signals co-localise at this locus, with posterior probabilities.", inputParams: CREDIBLE_SET_INPUT, - directFetch: true, }, { entity: "credibleSet", @@ -565,7 +489,6 @@ export const SECTION_REGISTRY: SectionDef[] = [ description: "Shows molecular QTL colocalisation for a credible set — eQTL, pQTL and sQTL signals from GTEx and other resources that co-localise at this locus.", inputParams: CREDIBLE_SET_INPUT, - directFetch: true, }, { entity: "credibleSet", @@ -574,7 +497,6 @@ export const SECTION_REGISTRY: SectionDef[] = [ description: "Shows the variants within a credible set — posterior inclusion probabilities, allele frequencies, and functional annotations for all tagged variants.", inputParams: CREDIBLE_SET_INPUT, - directFetch: true, }, { entity: "credibleSet", @@ -583,7 +505,6 @@ export const SECTION_REGISTRY: SectionDef[] = [ description: "Shows enhancer-to-gene (E2G) predictions for a credible set — regulatory links between non-coding variants and target genes from Activity-by-Contact and other models.", inputParams: CREDIBLE_SET_INPUT, - directFetch: true, }, // ── STUDY ──────────────────────────────────────────────────────────────────── @@ -594,7 +515,6 @@ export const SECTION_REGISTRY: SectionDef[] = [ description: "Shows GWAS credible sets for a study — Manhattan plot of fine-mapped loci with lead variants, p-values, fine-mapping confidence, and top L2G gene scores.", inputParams: STUDY_INPUT, - directFetch: true, }, { entity: "study", @@ -603,7 +523,6 @@ export const SECTION_REGISTRY: SectionDef[] = [ description: "Shows QTL credible sets for a molecular QTL study — fine-mapped loci with lead variants and associated gene targets.", inputParams: STUDY_INPUT, - directFetch: true, }, { entity: "study", @@ -612,7 +531,6 @@ export const SECTION_REGISTRY: SectionDef[] = [ description: "Shows other GWAS studies that share the same disease or phenotype associations as a given study, with sample sizes, cohorts, and publications.", inputParams: STUDY_INPUT, - directFetch: true, }, // ── VARIANT ────────────────────────────────────────────────────────────────── @@ -623,7 +541,6 @@ export const SECTION_REGISTRY: SectionDef[] = [ description: "Shows in-silico predictor scores for a variant — AlphaMissense, SIFT, LOFTEE, FoldX, GERP, VEP, and LoF curation scores as a normalised dot plot from likely benign to likely deleterious.", inputParams: VARIANT_INPUT, - directFetch: true, }, { entity: "variant", @@ -632,7 +549,6 @@ export const SECTION_REGISTRY: SectionDef[] = [ description: "Shows ClinVar/EVA clinical evidence for a variant — variant classifications, conditions, review status, and supporting evidence from clinical databases.", inputParams: VARIANT_INPUT, - directFetch: true, }, { entity: "variant", @@ -641,7 +557,6 @@ export const SECTION_REGISTRY: SectionDef[] = [ description: "Shows enhancer-to-gene (E2G) predictions for a variant — regulatory links to target genes from Activity-by-Contact and other functional genomics models.", inputParams: VARIANT_INPUT, - directFetch: true, }, { entity: "variant", @@ -650,7 +565,6 @@ export const SECTION_REGISTRY: SectionDef[] = [ description: "Shows GWAS credible sets containing a variant — fine-mapped loci across studies where this variant is included, with study traits and posterior probabilities.", inputParams: VARIANT_INPUT, - directFetch: true, }, { entity: "variant", @@ -659,7 +573,6 @@ export const SECTION_REGISTRY: SectionDef[] = [ description: "Shows pharmacogenomics annotations for a variant — drug-gene interactions and phenotype associations from PharmGKB and other resources.", inputParams: VARIANT_INPUT, - directFetch: true, }, { entity: "variant", @@ -668,7 +581,6 @@ export const SECTION_REGISTRY: SectionDef[] = [ description: "Shows molecular QTL credible sets for a variant — eQTL, pQTL, and sQTL fine-mapped loci containing this variant across tissues and cell types.", inputParams: VARIANT_INPUT, - directFetch: true, }, { entity: "variant", @@ -677,7 +589,6 @@ export const SECTION_REGISTRY: SectionDef[] = [ description: "Shows UniProt protein variant annotations for a variant — functional consequence, pathogenicity classification, and protein domain context.", inputParams: VARIANT_INPUT, - directFetch: true, }, { entity: "variant", @@ -686,6 +597,5 @@ export const SECTION_REGISTRY: SectionDef[] = [ description: "Shows Ensembl Variant Effect Predictor (VEP) annotations for a variant — transcript consequences, amino acid changes, and regulatory feature impacts across all overlapping genes.", inputParams: VARIANT_INPUT, - directFetch: true, }, ]; diff --git a/apps/mcp-widgets-server/src/widgets/index.ts b/apps/mcp-widgets-server/src/widgets/index.ts index e9b420c00..ff6e52be3 100644 --- a/apps/mcp-widgets-server/src/widgets/index.ts +++ b/apps/mcp-widgets-server/src/widgets/index.ts @@ -1,6 +1,3 @@ -import { readdirSync, readFileSync, existsSync } from "node:fs"; -import { resolve } from "node:path"; -import { fileURLToPath } from "node:url"; import type { WidgetDef } from "./types.js"; import { SECTION_REGISTRY, type SectionDef } from "../sections/registry.js"; @@ -15,56 +12,11 @@ export { molecularStructureWidget } from "./molecular-structure.js"; import { molecularStructureWidget } from "./molecular-structure.js"; -/** Manually-configured widgets with custom prefetch logic */ +/** Manually-configured widgets */ const MANUAL_WIDGETS: WidgetDef[] = [molecularStructureWidget]; -// Resolve sections source root relative to this file (4 levels up from src/widgets/) -const __dirname = fileURLToPath(new URL(".", import.meta.url)); -const SECTIONS_SRC = resolve(__dirname, "../../../../packages/sections/src"); - -/** Auto-detects and reads the primary GQL query file for a section. */ -function loadSectionQuery(sectionPath: string): { query: string; operationName: string } { - const sectionName = sectionPath.split("/").pop()!; - const dir = resolve(SECTIONS_SRC, sectionPath); - const files = readdirSync(dir); - - // Preference order: {Name}Query.gql → {Name}.gql → sectionQuery.gql → any non-summary .gql - const candidates = [ - `${sectionName}Query.gql`, - `${sectionName}.gql`, - "sectionQuery.gql", - ...files.filter( - f => - f.endsWith(".gql") && - !f.toLowerCase().includes("summary") && - !f.toLowerCase().includes("fragment") - ), - ]; - - for (const candidate of candidates) { - const filePath = resolve(dir, candidate); - if (existsSync(filePath)) { - const query = readFileSync(filePath, "utf-8"); - const match = query.match(/query\s+(\w+)/); - const operationName = match?.[1] ?? sectionName + "Query"; - return { query, operationName }; - } - } - - throw new Error(`[mcp] No query file found for section: ${sectionPath}`); -} - /** Derives a WidgetDef from a SectionDef registry entry. */ function deriveSectionWidgetDef(def: SectionDef): WidgetDef { - // directFetch widgets skip server-side prefetch entirely — no query file needed. - const prefetchBase = def.directFetch - ? null - : (def.primaryPrefetch ?? - (() => { - const { query, operationName } = loadSectionQuery(def.sectionPath); - return { query, operationName }; - })()); - const sectionId = sectionPathToId(def.sectionPath); const sectionName = def.sectionPath.split("/").pop()!; @@ -80,27 +32,11 @@ function deriveSectionWidgetDef(def: SectionDef): WidgetDef { bundleFile: `${sectionId}.js`, title: `${readableName} Widget`, successMessage: `${readableName} widget rendered successfully in the chat interface.`, - prefetch: prefetchBase - ? { - operationName: prefetchBase.operationName, - query: prefetchBase.query, - extraVariables: def.prefetchExtraVariables, - extraPrefetches: def.extraPrefetches, - } - : undefined, - directFetch: def.directFetch, }; } -/** Derived widgets from SECTION_REGISTRY (auto-detected GQL, standard Body delegation) */ -const SECTION_WIDGETS: WidgetDef[] = SECTION_REGISTRY.flatMap(def => { - try { - return [deriveSectionWidgetDef(def)]; - } catch (err) { - console.warn(`[mcp] Skipping section widget ${def.toolName}:`, (err as Error).message); - return []; - } -}); +/** Derived widgets from SECTION_REGISTRY (standard Body delegation) */ +const SECTION_WIDGETS: WidgetDef[] = SECTION_REGISTRY.map(deriveSectionWidgetDef); /** All registered widget tools — MCP server, chat handler, and /status all read from this. */ export const WIDGET_REGISTRY: WidgetDef[] = [...MANUAL_WIDGETS, ...SECTION_WIDGETS]; diff --git a/apps/mcp-widgets-server/src/widgets/molecular-structure.ts b/apps/mcp-widgets-server/src/widgets/molecular-structure.ts index 82b815f71..75f9cf23b 100644 --- a/apps/mcp-widgets-server/src/widgets/molecular-structure.ts +++ b/apps/mcp-widgets-server/src/widgets/molecular-structure.ts @@ -14,36 +14,7 @@ export const molecularStructureWidget: WidgetDef = { bundleFile: "molecular-structure.js", title: "Molecular Structure Widget", successMessage: "Molecular structure widget rendered successfully in the chat interface.", - prefetch: { - operationName: "MolecularStructureQuery", - query: ` - query MolecularStructureQuery($variantId: String!) { - variant(variantId: $variantId) { - id - referenceAllele - alternateAllele - proteinCodingCoordinates { - count - rows { - uniprotAccessions - variant { id } - target { id approvedSymbol } - referenceAminoAcid - alternateAminoAcid - aminoAcidPosition - } - } - } - } - `, - extractExtraFetches: (data: unknown) => { - const rows = (data as any)?.variant?.proteinCodingCoordinates?.rows; - const uniprotId = rows?.[0]?.uniprotAccessions?.[0]; - if (!uniprotId) return []; - return [{ - url: `https://alphafold.ebi.ac.uk/files/AF-${uniprotId}-F1-model_v6.cif`, - contentType: "text/plain", - }]; - }, - }, + // StructureViewer.tsx fetches AlphaFold CIF/pathogenicity files and UniProt domain + // annotations directly from the iframe — these origins must be CSP-allowlisted. + extraConnectDomains: ["https://alphafold.ebi.ac.uk", "https://rest.uniprot.org"], }; diff --git a/apps/mcp-widgets-server/src/widgets/types.ts b/apps/mcp-widgets-server/src/widgets/types.ts index 1ef4ae757..cc267b7c3 100644 --- a/apps/mcp-widgets-server/src/widgets/types.ts +++ b/apps/mcp-widgets-server/src/widgets/types.ts @@ -20,51 +20,9 @@ export type WidgetDef = { /** Message returned to Claude after the widget renders */ successMessage: string; /** - * When defined, the tool handler fetches this GraphQL query server-side and - * injects the result via a window.fetch interceptor in the widget HTML. - * This is required for Claude Desktop where the sandboxed iframe cannot make - * external network requests. + * Extra origins to allowlist in CSP connectDomains beyond the GraphQL API, for widgets + * that fetch other external resources directly from the iframe (e.g. molecular-structure + * fetching AlphaFold CIF/CSV files and UniProt domain annotations). */ - prefetch?: { - /** Full GraphQL query string (must include the operationName) */ - query: string; - /** Apollo operationName — must match the name in the query string */ - operationName: string; - /** Extra static variables merged alongside the main inputParam (e.g. pagination) */ - extraVariables?: Record<string, unknown>; - /** - * Additional queries to run after the primary query. - * Variables can depend on the primary query's result (e.g. diseaseIds from studyId). - */ - extraPrefetches?: Array<{ - query: string; - operationName: string; - /** Compute variables from the raw input value and the primary query's data */ - variables: (inputValue: string, primaryData: unknown) => Record<string, unknown>; - /** - * When set, the cached data for this operation is an array of items. - * The interceptor filters by matching item[itemIdField] against the - * widget's actual request variable[requestVarName], then returns - * { [responseKey]: filteredItems }. - * Used for on-demand detail queries (e.g. ClinicalRecordsQuery). - */ - filteredBy?: { - requestVarName: string; - itemIdField: string; - responseKey: string; - }; - }>; - /** - * Optional: derive additional URLs to fetch server-side from the GraphQL data. - * Results are injected into the HTML fetch interceptor by exact URL match. - * Useful for binary/text assets (e.g. AlphaFold CIF files) that would be - * blocked from the sandboxed iframe. - */ - extractExtraFetches?: (data: unknown) => Array<{ url: string; contentType: string }>; - }; - /** - * Experimental: when true, no server-side prefetch runs. The widget iframe fetches - * the GraphQL API directly instead, allowlisted via the resource's CSP connectDomains. - */ - directFetch?: boolean; + extraConnectDomains?: string[]; }; diff --git a/apps/mcp-widgets-server/widget-src/shared/createWidgetEntry.tsx b/apps/mcp-widgets-server/widget-src/shared/createWidgetEntry.tsx index 8510fa9e8..95913f9a2 100644 --- a/apps/mcp-widgets-server/widget-src/shared/createWidgetEntry.tsx +++ b/apps/mcp-widgets-server/widget-src/shared/createWidgetEntry.tsx @@ -1,14 +1,10 @@ /** - * Factory that wires up the App class, fetch interceptor, ResizeObserver - * height reporting, Emotion cache, and MUI theme for a widget IIFE bundle. + * Factory that wires up the App class, ResizeObserver height reporting, + * Emotion cache, and MUI theme for a widget IIFE bundle. * - * Data delivery pattern (per MCP Apps spec examples): - * ontoolinput fires → app.callServerTool("ot_fetch_widget_data") → - * read result.structuredContent → populate fetch interceptor cache → - * Apollo queries resolve from cache. - * - * app.ontoolresult is kept as a primary path for when Claude Desktop fixes - * the structuredContent stripping bug (#696 in ext-apps). + * The widget's Apollo client fetches GraphQL data directly from the iframe — + * the MCP server allowlists the API origin (and any extra origins the widget + * needs) via CSP connectDomains on the resource. No server-side prefetch. * * autoResize: false — we report only HEIGHT manually so AppFrame never sets a * pixel width on the proxy iframe (which would collapse the layout to the @@ -33,139 +29,6 @@ const apolloClient = new ApolloClient({ cache: new InMemoryCache(), }); -// ── Prefetch data types ─────────────────────────────────────────────────────── - -interface FilteredOp { - operationName: string; - allItems: unknown[]; - itemIdField: string; - requestVarName: string; - responseKey: string; -} - -interface PrefetchResult { - operations?: Array<{ operationName: string; data: unknown }>; - filteredOps?: FilteredOp[]; - urlData?: Array<{ url: string; text: string; contentType: string }>; -} - -// ── Module-scope data store ─────────────────────────────────────────────────── -// Shared between the fetch interceptor and applyData. Module scope guarantees -// the interceptor is in place before Apollo Client makes its first request. - -const _gql: Record<string, unknown> = {}; -const _gqlPending: Record<string, Array<(data: unknown) => void>> = {}; -const _filteredOps: Record<string, FilteredOp> = {}; -let _urls: Array<{ url: string; text: string; contentType: string }> = []; - -function clearDataStore() { - for (const k of Object.keys(_gql)) delete _gql[k]; - for (const k of Object.keys(_gqlPending)) delete _gqlPending[k]; - for (const k of Object.keys(_filteredOps)) delete _filteredOps[k]; - _urls = []; -} - -function applyData(pf: PrefetchResult) { - _urls = pf.urlData ?? []; - - for (const fo of pf.filteredOps ?? []) { - _filteredOps[fo.operationName] = fo; - } - - for (const op of pf.operations ?? []) { - if (op.operationName === undefined) continue; - _gql[op.operationName] = op.data; - const waiting = _gqlPending[op.operationName]; - if (waiting) { - for (const resolve of waiting) resolve(op.data); - delete _gqlPending[op.operationName]; - } - } -} - -// ── window.fetch interceptor ────────────────────────────────────────────────── -// Must be set up at module scope (before any Apollo query runs) so every -// GraphQL request is routed through the prefetch cache. - -const _originalFetch = window.fetch.bind(window); - -// Set by the HTML shell before this bundle runs. When true, this widget skips -// server-side prefetch entirely and fetches the GraphQL API directly — the -// interceptor must get out of the way and let requests through unmolested. -const _isDirectFetch = Boolean((window as { __OT_DIRECT_FETCH__?: boolean }).__OT_DIRECT_FETCH__); - -window.fetch = function otFetchInterceptor( - url: RequestInfo | URL, - opts?: RequestInit -): Promise<Response> { - if (_isDirectFetch) return _originalFetch(url as RequestInfo, opts); - try { - // Cached URL assets (e.g. AlphaFold CIF files fetched by the 3D widget) - const urlStr = - typeof url === "string" ? url : url instanceof URL ? url.href : (url as Request).url; - const urlEntry = _urls.find(e => e.url === urlStr); - if (urlEntry) { - return Promise.resolve( - new Response(urlEntry.text, { - status: 200, - headers: { "Content-Type": urlEntry.contentType }, - }) - ); - } - - // GraphQL POST requests - if (opts?.method === "POST" && typeof opts.body === "string") { - const body = JSON.parse(opts.body) as { operationName?: string; variables?: Record<string, unknown> }; - const op = body.operationName; - if (op) { - // Filtered operation — server prefetches all items; interceptor filters by request vars - const fo = _filteredOps[op]; - if (fo) { - const reqIds = ((body.variables ?? {})[fo.requestVarName] ?? []) as unknown[]; - const idSet = new Set(reqIds); - const filtered = (fo.allItems as Record<string, unknown>[]).filter( - item => idSet.has(item[fo.itemIdField]) - ); - const data: Record<string, unknown> = {}; - data[fo.responseKey] = filtered; - return Promise.resolve( - new Response(JSON.stringify({ data }), { - status: 200, - headers: { "Content-Type": "application/json" }, - }) - ); - } - - // Already cached - if (_gql[op] !== undefined) { - return Promise.resolve( - new Response(JSON.stringify({ data: _gql[op] }), { - status: 200, - headers: { "Content-Type": "application/json" }, - }) - ); - } - - // Pend until applyData populates this operation - return new Promise((resolve, reject) => { - if (!_gqlPending[op]) _gqlPending[op] = []; - _gqlPending[op].push(data => - resolve( - new Response(JSON.stringify({ data }), { - status: 200, - headers: { "Content-Type": "application/json" }, - }) - ) - ); - setTimeout(() => reject(new Error(`OT data timeout for ${op}`)), 30000); - }); - } - } - } catch (_) {} - - return _originalFetch(url as RequestInfo, opts); -}; - // ── Widget entry factory ────────────────────────────────────────────────────── export interface WidgetEntryConfig<TArgs extends Record<string, unknown>> { @@ -198,48 +61,12 @@ export function mountWidget<TArgs extends Record<string, unknown>>( async function connect() { try { - // ontoolresult: primary path for when Claude Desktop fixes #696 - app.ontoolresult = result => { - if (result.structuredContent) { - applyData(result.structuredContent as PrefetchResult); - } - }; - // ontoolinput: set BEFORE connect() so we never miss the initial event. - // Calls callServerTool to fetch prefetched data (request/response path, - // not a notification — avoids the structuredContent stripping bug #696). - app.ontoolinput = async ({ arguments: rawArgs }) => { - clearDataStore(); - + app.ontoolinput = ({ arguments: rawArgs }) => { const extracted = config.extractArgs( (rawArgs ?? {}) as Record<string, unknown> ); if (extracted) setArgs(extracted); - - // directFetch widgets have no server-side prefetch to fetch — the - // widget's own Apollo client hits the GraphQL API directly instead. - if (_isDirectFetch) return; - - const toolName = ( - window as { __OT_WIDGET_TOOL__?: string } - ).__OT_WIDGET_TOOL__; - - if (toolName) { - try { - const result = await app.callServerTool({ - name: "ot_fetch_widget_data", - arguments: { - tool: toolName, - inputJson: JSON.stringify(rawArgs ?? {}), - }, - }); - if (result.structuredContent) { - applyData(result.structuredContent as PrefetchResult); - } - } catch (e) { - console.error(`[${config.appName}] callServerTool failed:`, e); - } - } }; await app.connect(); From 877aeca6e99f1e3f371890b9e6a7b4ce7fbb3f76 Mon Sep 17 00:00:00 2001 From: Carlos Cruz <me@carcruzcast.com> Date: Sat, 11 Jul 2026 20:27:53 +0100 Subject: [PATCH 4/6] feat: enhance widget functionality with profile-based configuration and improved link handling --- apps/mcp-widgets-server/package.json | 1 + apps/mcp-widgets-server/src/mcp-server.ts | 9 +- .../vite/widget.config.base.ts | 42 ++------ .../widget-src/shared/createWidgetEntry.tsx | 13 ++- .../widget-src/shared/stubs/ui-index.tsx | 96 +++++++++---------- .../widget-src/shared/stubs/ui-ms-index.tsx | 17 ++-- .../widget-src/shared/theme.ts | 27 ------ 7 files changed, 83 insertions(+), 122 deletions(-) delete mode 100644 apps/mcp-widgets-server/widget-src/shared/theme.ts diff --git a/apps/mcp-widgets-server/package.json b/apps/mcp-widgets-server/package.json index 729b508d0..b3ad68515 100644 --- a/apps/mcp-widgets-server/package.json +++ b/apps/mcp-widgets-server/package.json @@ -38,6 +38,7 @@ "@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", diff --git a/apps/mcp-widgets-server/src/mcp-server.ts b/apps/mcp-widgets-server/src/mcp-server.ts index aadc8dc60..3f1be3060 100644 --- a/apps/mcp-widgets-server/src/mcp-server.ts +++ b/apps/mcp-widgets-server/src/mcp-server.ts @@ -10,6 +10,11 @@ import { WIDGET_REGISTRY } from "./widgets/index.js"; const PUBLIC_API_URL = "https://api.platform.opentargets.org/api/v4/graphql"; +// Same profile mechanism the real platform app uses: apps/platform/index.html loads +// <script src="/profiles/{name}.js"> before its app bundle, setting window.configProfile. +// OT_PROFILE picks which file — "platform" (public) or "ppp" (partner preview). +const PROFILES_ROOT = new URL("../../../apps/platform/public/profiles", import.meta.url).pathname; + /** * Builds a self-contained HTML string with the widget IIFE bundle inlined. * The widget's own Apollo client fetches the GraphQL API directly from the @@ -20,6 +25,8 @@ async function makeWidgetShell(bundleFile: string, title: string): Promise<strin const bundlePath = new URL(`../dist/widgets/${bundleFile}`, import.meta.url).pathname; const bundleJs = await readFile(bundlePath, "utf-8"); const apiUrl = process.env.OT_API_URL ?? PUBLIC_API_URL; + const profileName = process.env.OT_PROFILE ?? "platform"; + const profileJs = await readFile(`${PROFILES_ROOT}/${profileName}.js`, "utf-8"); return `<!doctype html> <html> @@ -37,8 +44,8 @@ async function makeWidgetShell(bundleFile: string, title: string): Promise<strin </style> <script> window.__OT_API_URL__ = "${apiUrl}"; - window.configProfile = { isPartnerPreview: false, partnerTargetSectionIds: [], partnerDiseaseSectionIds: [], partnerDrugSectionIds: [], partnerEvidenceSectionIds: [], partnerDataTypes: [], partnerDataSources: [] }; </script> + <script>${profileJs}</script> </head> <body> <div id="root"></div> diff --git a/apps/mcp-widgets-server/vite/widget.config.base.ts b/apps/mcp-widgets-server/vite/widget.config.base.ts index 0d996c828..595e37259 100644 --- a/apps/mcp-widgets-server/vite/widget.config.base.ts +++ b/apps/mcp-widgets-server/vite/widget.config.base.ts @@ -112,46 +112,15 @@ export function createPlatformStubsPlugin(): Plugin { MONO_ROOT, "packages/ui/src/components/ApiPlaygroundDrawer.tsx" ); - // @ot/config's theme.ts calls lighten/darken (polished) on getConfig() colors at - // module-load time. In the widget sandbox window.configProfile is absent so - // primaryColor/secondaryColor are undefined → polished throws error #3. - // Stub both files by absolute path (same pattern as OTApolloProvider/DataDownloader) - // so polished is never invoked regardless of how the module is resolved. - const otConfigThemePath = resolve(MONO_ROOT, "packages/ot-config/src/theme.ts"); - const otConfigEnvPath = resolve(MONO_ROOT, "packages/ot-config/src/environment.ts"); + // @ot/config's theme.ts/environment.ts are NOT stubbed — the widget HTML shell + // (src/mcp-server.ts) sets window.configProfile from the real profile file before + // the bundle script runs, so getConfig() and theme.ts's lighten/darken calls resolve + // real colors safely. See widget-src/shared/createWidgetEntry.tsx, which imports the + // real theme from "@ot/config" directly (aliased below). return { name: "platform-stubs", load(id: string) { - if (id === otConfigThemePath) { - return ` -import { createTheme } from "@mui/material"; -const PRIMARY = "#3489ca"; -const SECONDARY = "#ff6350"; -export const theme = createTheme({ - palette: { primary: { main: PRIMARY }, secondary: { main: SECONDARY } }, -}); -`; - } - if (id === otConfigEnvPath) { - const PRIMARY = "#3489ca"; - const SECONDARY = "#ff6350"; - return ` -const PRIMARY = "${PRIMARY}"; -const SECONDARY = "${SECONDARY}"; -export function getConfig() { - return { - urlApi: ${JSON.stringify(OT_API_URL)}, - urlAiApi: "", - profile: { primaryColor: PRIMARY, secondaryColor: SECONDARY, isPartnerPreview: false, partnerTargetSectionIds: [], partnerDiseaseSectionIds: [], partnerDrugSectionIds: [], partnerEvidenceSectionIds: [], partnerDataTypes: [], partnerDataSources: [] }, - googleTagManagerID: null, - geneticsPortalUrl: "https://genetics.opentargets.org", - gitVersion: "", - }; -} -export function getEnvironmentConfig() { return getConfig(); } -`; - } if (id === otAsyncTooltipPath) { return ` import React from "react"; @@ -234,6 +203,7 @@ export function createWidgetBuildConfig(opts: WidgetBuildOptions): UserConfig { "@ot/sections": resolve(MONO_ROOT, "packages/sections/src"), "@ot/constants": resolve(MONO_ROOT, "packages/ot-constants/src"), "@ot/utils": resolve(MONO_ROOT, "packages/ot-utils/src"), + "@ot/config": resolve(MONO_ROOT, "packages/ot-config/src"), "@widget-shared": resolve(ROOT, "widget-src/shared"), }, }, diff --git a/apps/mcp-widgets-server/widget-src/shared/createWidgetEntry.tsx b/apps/mcp-widgets-server/widget-src/shared/createWidgetEntry.tsx index 95913f9a2..1f493f24e 100644 --- a/apps/mcp-widgets-server/widget-src/shared/createWidgetEntry.tsx +++ b/apps/mcp-widgets-server/widget-src/shared/createWidgetEntry.tsx @@ -18,7 +18,7 @@ import createCache from "@emotion/cache"; import { CacheProvider } from "@emotion/react"; import { ApolloClient, InMemoryCache, ApolloProvider } from "@apollo/client"; import { MemoryRouter } from "react-router-dom"; -import { theme } from "./theme"; +import { theme } from "@ot/config"; // __OT_API_URL__ is injected at build time by Vite's define (from OT_API_URL in .env). // window.__OT_API_URL__ can override it at runtime (set by the HTML shell script). @@ -29,6 +29,16 @@ const apolloClient = new ApolloClient({ cache: new InMemoryCache(), }); +// ── Shared App instance accessor ────────────────────────────────────────────── +// Each widget bundle mounts exactly one widget (one mountWidget() call), so a +// single module-scope reference is enough. Used by the Link stub (ui-index.tsx) +// to route external navigation through app.openLink() — raw <a target="_blank"> +// clicks are unreliable inside the sandboxed MCP App iframe. +let currentApp: App | null = null; +export function getApp(): App | null { + return currentApp; +} + // ── Widget entry factory ────────────────────────────────────────────────────── export interface WidgetEntryConfig<TArgs extends Record<string, unknown>> { @@ -52,6 +62,7 @@ export function mountWidget<TArgs extends Record<string, unknown>>( }); const app = new App({ name: config.appName, version: "0.1.0" }, {}, { autoResize: false }); + currentApp = app; function Root() { const [args, setArgs] = useState<TArgs | null>(null); diff --git a/apps/mcp-widgets-server/widget-src/shared/stubs/ui-index.tsx b/apps/mcp-widgets-server/widget-src/shared/stubs/ui-index.tsx index 80566917f..ec6fea4a7 100644 --- a/apps/mcp-widgets-server/widget-src/shared/stubs/ui-index.tsx +++ b/apps/mcp-widgets-server/widget-src/shared/stubs/ui-index.tsx @@ -8,30 +8,53 @@ * This stub file replaces those imports at build time (via the stubUiBarrel * Vite plugin in vite.widget.config.ts). */ -import type { ReactNode } from "react"; +import type { ComponentProps, MouseEvent, ReactNode } from "react"; +import { Box } from "@mui/material"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faArrowRightToBracket } from "@fortawesome/free-solid-svg-icons"; +import RealLink from "@ot/ui/components/Link/Link"; +import { getApp } from "@widget-shared/createWidgetEntry"; + +/** + * Link: reuses the real platform Link (packages/ui/src/components/Link/Link.tsx) + * for its actual styling/hover behavior, but always forces `external` — the widget + * has no real <Routes> for RouterLink to navigate to (it's a standalone component + * in a bare <MemoryRouter>), so internal-style paths ("/target/ENSG...") are + * resolved to absolute platform URLs. + * + * Navigation goes through app.openLink() rather than the native <a target="_blank"> + * click — MCP App iframes are sandboxed by the host, which may not grant popup + * permissions, so raw anchor-tag navigation is unreliable. openLink() is the + * spec-sanctioned way to ask the host to open a URL in the real browser. + */ +export function Link({ to, onClick, ...rest }: ComponentProps<typeof RealLink>) { + const resolvedTo = to?.startsWith("/") ? `https://platform.opentargets.org${to}` : to; + + const handleClick = (e: MouseEvent<HTMLAnchorElement>) => { + e.preventDefault(); + onClick?.(); + if (resolvedTo) getApp()?.openLink({ url: resolvedTo }).catch(() => {}); + }; -/** Link: opens OT platform pages in a new tab, or follows external URLs */ -export function Link({ - to, - children, -}: { - to?: string; - children?: ReactNode; - external?: boolean; - asyncTooltip?: boolean; - newTab?: boolean; - footer?: boolean; - tooltip?: unknown; - className?: string; - ariaLabel?: string; - onClick?: () => void; -}) { - const href = - !to ? "#" : to.startsWith("/") ? `https://platform.opentargets.org${to}` : to; return ( - <a href={href} target="_blank" rel="noreferrer"> - {children} - </a> + <RealLink {...rest} to={resolvedTo} external newTab onClick={handleClick as () => void} /> + ); +} + +/** + * Navigate: mirrors packages/ui/src/components/Navigate.tsx exactly, but composed + * with the Link above instead of importing the real component directly — the real + * Navigate.tsx imports Link via a relative path ("./Link"), which bypasses this + * stub file entirely and would reintroduce the broken-click issue Link fixes. + */ +export function Navigate({ to }: { to?: string }) { + return ( + <Link asyncTooltip to={to}> + <Box display="flex" justifyContent="center" alignItems="center" gap={1}> + View + <FontAwesomeIcon size="sm" icon={faArrowRightToBracket} /> + </Box> + </Link> ); } @@ -144,32 +167,9 @@ export function DisplayVariantId({ return <span>{display}</span>; } -/** Navigate: renders a "View →" link to an OT platform page */ -export function Navigate({ to }: { to?: string }) { - const href = !to ? "#" : to.startsWith("/") ? `https://platform.opentargets.org${to}` : to; - return ( - <a - href={href} - target="_blank" - rel="noreferrer" - style={{ - display: "inline-flex", - alignItems: "center", - gap: 4, - textDecoration: "none", - color: "inherit", - }} - > - View → - </a> - ); -} - -/** ClinvarStars: renders a star rating (★ filled, ☆ empty) */ -export function ClinvarStars({ num = 0, length = 4 }: { num?: number; length?: number }) { - const stars = Array.from({ length }, (_, i) => (i < num ? "★" : "☆")).join(""); - return <span style={{ color: "#FFC107", letterSpacing: 1 }}>{stars}</span>; -} +// ClinvarStars is a pure presentational component (no navigation/routing +// dependency), so it's reused directly — no adapter needed. +export { default as ClinvarStars } from "@ot/ui/components/ClinvarStars"; /** L2GScoreIndicator: renders the L2G score as tabular text */ export function L2GScoreIndicator({ diff --git a/apps/mcp-widgets-server/widget-src/shared/stubs/ui-ms-index.tsx b/apps/mcp-widgets-server/widget-src/shared/stubs/ui-ms-index.tsx index 0d6bf81d4..6febc8de1 100644 --- a/apps/mcp-widgets-server/widget-src/shared/stubs/ui-ms-index.tsx +++ b/apps/mcp-widgets-server/widget-src/shared/stubs/ui-ms-index.tsx @@ -24,18 +24,17 @@ export { default as CompactAlphaFoldDomainLegend } from "@ot/ui/components/Viewe export { default as CompactAlphaFoldHydrophobicityLegend } from "@ot/ui/components/Viewer/CompactAlphaFoldHydrophobicityLegend"; // ---- Shared widget stubs (reused from the main ui stub) ---- -export { SectionItem, SummaryItem, DisplayVariantId, usePlatformApi, useBatchQuery } from "./ui-index"; +export { + Link, + SectionItem, + SummaryItem, + DisplayVariantId, + usePlatformApi, + useBatchQuery, +} from "./ui-index"; // ---- Stubs ---- -export function Link({ children, to }: { children: React.ReactNode; to: string }) { - return ( - <a href={to} target="_blank" rel="noopener noreferrer"> - {children} - </a> - ); -} - export function DataDownloader() { return null; } diff --git a/apps/mcp-widgets-server/widget-src/shared/theme.ts b/apps/mcp-widgets-server/widget-src/shared/theme.ts deleted file mode 100644 index 0fe7c191c..000000000 --- a/apps/mcp-widgets-server/widget-src/shared/theme.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { createTheme } from "@mui/material"; - -/** - * Shared MUI theme used by all widget IIFE bundles. - * - * Mirrors the platform's custom theme extensions (boxShadow, typography variants) - * so platform section components render without crashes when used as widgets. - */ -export const theme = createTheme({ - shape: { borderRadius: 2 }, - typography: { fontFamily: '"Inter", sans-serif' }, - palette: { - primary: { main: "#3489ca" }, - secondary: { main: "#ff6350" }, - text: { primary: "#5A5F5F" }, - }, - // Custom platform theme extensions — match packages/ot-config/src/theme.ts - // so any section component accessing theme.boxShadow etc. works correctly. - // @ts-ignore: not in MUI's default ThemeOptions type - boxShadow: { - value: "0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)", - sm: "0 1px 2px 0 rgb(0 0 0 / 0.05)", - default: "0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)", - md: "0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)", - lg: "0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)", - }, -}); From c8c44382da1adaa7bac1d9fb4018fb4d6432be76 Mon Sep 17 00:00:00 2001 From: Carlos Cruz <me@carcruzcast.com> Date: Sun, 12 Jul 2026 03:01:34 +0100 Subject: [PATCH 5/6] feat: enhance section widget registry with new entries and CSP configurations --- apps/mcp-widgets-server/WIDGETS.md | 133 ++++++++++-------- .../src/sections/registry.ts | 99 +++++++++++++ apps/mcp-widgets-server/src/widgets/index.ts | 1 + .../vite/section-widget.plugin.ts | 1 + .../vite/widget.config.base.ts | 128 +++++++++++++++-- .../widget-src/shared/stubs/ui-index.tsx | 90 +++++++----- .../widget-src/shared/stubs/ui-ms-index.tsx | 4 +- 7 files changed, 349 insertions(+), 107 deletions(-) diff --git a/apps/mcp-widgets-server/WIDGETS.md b/apps/mcp-widgets-server/WIDGETS.md index 2e89907f4..1cb0ce8a6 100644 --- a/apps/mcp-widgets-server/WIDGETS.md +++ b/apps/mcp-widgets-server/WIDGETS.md @@ -48,81 +48,98 @@ Apollo client then fetches data directly from the iframe — there is no server- ## 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:sections` for all) +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), add its -origin to `extraConnectDomains` in the derived `WidgetDef` (see `molecular-structure.ts` for -an example with AlphaFold and UniProt). +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 (61 total) +## Included section widgets (69 total) -### Target (14 sections) +### Target (18 sections) -| Tool name | Section | Input | -|-----------|---------|-------| -| `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_expression_widget` | `target/Expression` | `ensemblId` | -| `get_target_gene_ontology_widget` | `target/GeneOntology` | `ensemblId` | -| `get_target_genetic_constraint_widget` | `target/GeneticConstraint` | `ensemblId` | -| `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_tractability_widget` | `target/Tractability` | `ensemblId` | - -### Disease (4 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 (5 sections) +### Drug (6 sections) | Tool name | Section | Input | Notes | |-----------|---------|-------|-------| | `get_drug_adverse_events_widget` | `drug/AdverseEvents` | `chemblId` | | -| `get_drug_indications_widget` | `drug/ClinicalIndications` | `chemblId` | Two-query: indications + `ClinicalRecordsQuery` (filtered on row click) | +| `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 (20 sections) +### Evidence (23 sections) -| Tool name | Section | Input | -|-----------|---------|-------| -| `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_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` | +| 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) @@ -169,14 +186,8 @@ These sections exist in `packages/sections/src` but are not in the registry. | Section | Entity | Reason excluded | |---------|--------|-----------------| -| `target/BaselineExpression` | Target | Requires symbol lookup first (two-query with TargetSymbol); `target/Expression` covers expression data | -| `target/Bibliography` | Target | Uses SimilarEntities API with cursor pagination; publication rendering complexity | -| `target/MolecularInteractions` | Target | Four separate databases (IntAct, Reactome, SIGNOR, STRING), each with its own query | -| `target/MolecularStructure` | Target | 3D viewer — covered by `get_molecular_structure_widget` (manual) | -| `target/OverlappingVariants` | Target | Interactive genome browser with complex stateful viewer | -| `target/SubcellularLocation` | Target | Custom SVG-based subcellular location visualisation with no standard table | -| `disease/Bibliography` | Disease | SimilarEntities cursor pagination; publication rendering complexity | -| `disease/GWASStudies` | Disease | Requires `diseaseIds: [String!]!` array input, not a single EFO ID | -| `drug/Bibliography` | Drug | SimilarEntities cursor pagination | -| `evidence/EuropePmc` | Evidence | SentenceMatch + Publication components with cursor pagination | | `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. diff --git a/apps/mcp-widgets-server/src/sections/registry.ts b/apps/mcp-widgets-server/src/sections/registry.ts index 21425bfc8..886e1f00f 100644 --- a/apps/mcp-widgets-server/src/sections/registry.ts +++ b/apps/mcp-widgets-server/src/sections/registry.ts @@ -20,8 +20,28 @@ export type SectionDef = { description: string; /** Input parameters. One for single-entity sections, two for evidence. */ inputParams: ReadonlyArray<{ name: string; description: string }>; + /** + * Extra origins to allowlist in CSP connectDomains beyond the GraphQL API, for sections + * whose Body fetches other external resources directly from the iframe (e.g. Bibliography + * sections fetching publication details from EuropePMC). + */ + extraConnectDomains?: string[]; + /** + * Extra packages to force-dedupe in the Vite build, for sections that import a package + * directly (not through a shared ui wrapper) where multiple bundled copies could conflict + * (e.g. "3dmol", which the manual molecular-structure widget already requires this for). + */ + extraDedupe?: string[]; }; +const EUROPE_PMC_DOMAIN = ["https://www.ebi.ac.uk"]; +const SWISSBIOPICS_DOMAIN = ["https://www.swissbiopics.org"]; +const MOLECULAR_STRUCTURE_DOMAINS = [ + "https://rest.uniprot.org", + "https://alphafold.ebi.ac.uk", + "https://www.ebi.ac.uk", +]; + const TARGET_INPUT = [ { name: "ensemblId", description: "Ensembl gene ID (e.g. ENSG00000157764 for BRAF)" }, ] as const; @@ -108,6 +128,16 @@ export const SECTION_REGISTRY: SectionDef[] = [ "and other sources.", inputParams: TARGET_INPUT, }, + { + entity: "target", + sectionPath: "target/Bibliography", + toolName: "get_target_bibliography_widget", + description: + "Shows scientific literature mentioning a target gene and other co-occurring entities " + + "recognised via NLP — publication list with PMIDs and publication dates.", + inputParams: TARGET_INPUT, + extraConnectDomains: EUROPE_PMC_DOMAIN, + }, { entity: "target", sectionPath: "target/GeneOntology", @@ -162,6 +192,26 @@ export const SECTION_REGISTRY: SectionDef[] = [ "credible sets from GTEx and other QTL datasets.", inputParams: TARGET_INPUT, }, + { + entity: "target", + sectionPath: "target/MolecularInteractions", + toolName: "get_target_molecular_interactions_widget", + description: + "Shows molecular interaction partners for a target gene — protein-protein and " + + "functional interactions from IntAct, Signor, Reactome, and STRING.", + inputParams: TARGET_INPUT, + }, + { + entity: "target", + sectionPath: "target/MolecularStructure", + toolName: "get_target_molecular_structure_widget", + description: + "Shows experimental and predicted 3D protein structures for a target gene — PDB " + + "structures and the AlphaFold model, in an interactive 3D viewer.", + inputParams: TARGET_INPUT, + extraConnectDomains: MOLECULAR_STRUCTURE_DOMAINS, + extraDedupe: ["3dmol"], + }, { entity: "target", sectionPath: "target/Safety", @@ -171,6 +221,16 @@ export const SECTION_REGISTRY: SectionDef[] = [ "and experimental toxicity data from curated sources.", inputParams: TARGET_INPUT, }, + { + entity: "target", + sectionPath: "target/SubcellularLocation", + toolName: "get_target_subcellular_location_widget", + description: + "Shows subcellular location annotations for a target gene's protein — cellular " + + "compartments from the Human Protein Atlas and UniProt, with a diagram of the cell.", + inputParams: TARGET_INPUT, + extraConnectDomains: SWISSBIOPICS_DOMAIN, + }, { entity: "target", sectionPath: "target/Tractability", @@ -191,6 +251,25 @@ export const SECTION_REGISTRY: SectionDef[] = [ "clinical phases, mechanisms of action, and linked targets.", inputParams: DISEASE_INPUT, }, + { + entity: "disease", + sectionPath: "disease/Bibliography", + toolName: "get_disease_bibliography_widget", + description: + "Shows scientific literature mentioning a disease and other co-occurring entities " + + "recognised via NLP — publication list with PMIDs and publication dates.", + inputParams: DISEASE_INPUT, + extraConnectDomains: EUROPE_PMC_DOMAIN, + }, + { + entity: "disease", + sectionPath: "disease/GWASStudies", + toolName: "get_disease_gwas_studies_widget", + description: + "Shows GWAS studies associated with a disease from the GWAS Catalog and FinnGen — " + + "reported trait, sample size, cohorts, publication, and credible set counts.", + inputParams: DISEASE_INPUT, + }, { entity: "disease", sectionPath: "disease/OTProjects", @@ -229,6 +308,16 @@ export const SECTION_REGISTRY: SectionDef[] = [ "by MedDRA term with likelihood ratio scores.", inputParams: DRUG_INPUT, }, + { + entity: "drug", + sectionPath: "drug/Bibliography", + toolName: "get_drug_bibliography_widget", + description: + "Shows scientific literature mentioning a drug and other co-occurring entities " + + "recognised via NLP — publication list with PMIDs and publication dates.", + inputParams: DRUG_INPUT, + extraConnectDomains: EUROPE_PMC_DOMAIN, + }, { entity: "drug", sectionPath: "drug/Indications", @@ -338,6 +427,16 @@ export const SECTION_REGISTRY: SectionDef[] = [ "associated with the disease from clinical submissions.", inputParams: EVIDENCE_INPUT, }, + { + entity: "evidence", + sectionPath: "evidence/EuropePmc", + toolName: "get_evidence_europe_pmc_widget", + description: + "Shows text-mined literature evidence linking a target gene to a disease — " + + "publications from Europe PMC with matched sentences and disease/phenotype context.", + inputParams: EVIDENCE_INPUT, + extraConnectDomains: EUROPE_PMC_DOMAIN, + }, { entity: "evidence", sectionPath: "evidence/ExpressionAtlas", diff --git a/apps/mcp-widgets-server/src/widgets/index.ts b/apps/mcp-widgets-server/src/widgets/index.ts index ff6e52be3..746c7c014 100644 --- a/apps/mcp-widgets-server/src/widgets/index.ts +++ b/apps/mcp-widgets-server/src/widgets/index.ts @@ -32,6 +32,7 @@ function deriveSectionWidgetDef(def: SectionDef): WidgetDef { bundleFile: `${sectionId}.js`, title: `${readableName} Widget`, successMessage: `${readableName} widget rendered successfully in the chat interface.`, + extraConnectDomains: def.extraConnectDomains, }; } diff --git a/apps/mcp-widgets-server/vite/section-widget.plugin.ts b/apps/mcp-widgets-server/vite/section-widget.plugin.ts index 3adf0c3d0..aa46aeaf3 100644 --- a/apps/mcp-widgets-server/vite/section-widget.plugin.ts +++ b/apps/mcp-widgets-server/vite/section-widget.plugin.ts @@ -95,5 +95,6 @@ export function createSectionWidgetConfig( outputFile: `${sectionId}.js`, emptyOutDir: opts?.emptyOutDir ?? false, plugins: [createUiBarrelStub(), createPlatformStubsPlugin()], + extraDedupe: def.extraDedupe, }); } diff --git a/apps/mcp-widgets-server/vite/widget.config.base.ts b/apps/mcp-widgets-server/vite/widget.config.base.ts index 595e37259..56e13f39c 100644 --- a/apps/mcp-widgets-server/vite/widget.config.base.ts +++ b/apps/mcp-widgets-server/vite/widget.config.base.ts @@ -99,13 +99,28 @@ export function createPlatformStubsPlugin(): Plugin { MONO_ROOT, "packages/ui/src/providers/OTApolloProvider/OTApolloProvider.tsx" ); - // OtAsyncTooltip imports OtGenomicLocation from the ui barrel, but that - // component does not exist yet in the codebase. Stub the whole component - // so widgets that transitively import it (via OtTable etc.) don't break. - const otAsyncTooltipPath = resolve( + // useConfigContext() is imported both via the "ui" barrel (stubbed in ui-index.tsx) + // AND by several components via a relative path straight to this module + // (PublicationWrapper.tsx, PartnerLockIcon.tsx, Footer.tsx) — the relative imports + // bypass the "ui" barrel stub entirely. Stub the module itself so every import + // style gets the same safe, non-null config instead of crashing on the real + // provider's default context value ({ config: null }, since there's no + // <OTConfigurationProvider> ancestor in the widget). + const configurationProviderPath = resolve( MONO_ROOT, - "packages/ui/src/components/OtAsyncTooltip/OtAsyncTooltip.tsx" + "packages/ui/src/providers/ConfigurationProvider.tsx" ); + // Link.tsx is imported both via the "ui" barrel (wrapped in ui-index.tsx to force + // external navigation through app.openLink()) AND by several real components via a + // relative path straight to this module (PublicationWrapper.tsx, Footer.tsx) — those + // relative imports get the unwrapped real component, which renders a plain <a> with + // no target="_blank" for "external" links and native RouterLink navigation otherwise. + // Clicking it inside the sandboxed MCP App iframe navigates the iframe itself away + // from the widget (no popup permission, no real <Routes> to land on) — the widget + // "disappearing" the user saw. Stub the module itself, mirroring the real component's + // styling classes exactly, but unconditionally routing through openLink() — there is + // no legitimate internal-navigation case inside a standalone widget iframe. + const linkComponentPath = resolve(MONO_ROOT, "packages/ui/src/components/Link/Link.tsx"); // ApiPlaygroundDrawer dynamically imports "graphiql" which is not available // in the widget build environment. Stub it out as a no-op component. const apiPlaygroundDrawerPath = resolve( @@ -121,12 +136,6 @@ export function createPlatformStubsPlugin(): Plugin { return { name: "platform-stubs", load(id: string) { - if (id === otAsyncTooltipPath) { - return ` -import React from "react"; -export default function OtAsyncTooltip({ children }) { return React.createElement(React.Fragment, null, children); } -`; - } if (id === apiPlaygroundDrawerPath) { return "export default function ApiPlaygroundDrawer() { return null; }"; } @@ -140,6 +149,103 @@ export default function OtAsyncTooltip({ children }) { return React.createElemen return ` export { useApolloClient } from "@apollo/client"; export function OTApolloProvider({ children }) { return children; } +`; + } + if (id === configurationProviderPath) { + return ` +import { getConfig } from "@ot/config"; +export function useConfigContext() { return { config: getConfig() }; } +export function OTConfigurationProvider({ children }) { return children; } +export const OTConfigurationContext = undefined; +`; + } + if (id === linkComponentPath) { + return ` +import React from "react"; +import { makeStyles } from "@mui/styles"; +import classNames from "classnames"; +import { getApp } from "@widget-shared/createWidgetEntry"; +import OtAsyncTooltip from "@ot/ui/components/OtAsyncTooltip/OtAsyncTooltip"; + +const useStyles = makeStyles((theme) => ({ + base: { + fontSize: "inherit", + "text-decoration-color": "transparent", + "-webkit-text-decoration-color": "transparent", + }, + baseDefault: { + color: theme.palette.primary.main, + "&:hover": { + color: theme.palette.primary.dark, + "text-decoration-color": theme.palette.primary.dark, + "-webkit-text-decoration-color": theme.palette.primary.dark, + }, + }, + baseTooltip: { + color: theme.palette.primary.main, + "&:hover": { color: theme.palette.primary.dark }, + textDecoration: "none", + }, + baseFooter: { + color: "white", + "text-decoration-color": "transparent", + "-webkit-text-decoration-color": "transparent", + "&:hover": { + color: theme.palette.primary.light, + "text-decoration-color": theme.palette.primary.light, + "-webkit-text-decoration-color": theme.palette.primary.light, + }, + display: "flex", + alignItems: "center", + }, +})); + +function Link({ children, to, onClick, footer, tooltip, asyncTooltip, className, ariaLabel }) { + const classes = useStyles(); + const ariaLabelProp = ariaLabel ? { "aria-label": ariaLabel } : {}; + const resolvedTo = to && to.startsWith("/") ? "https://platform.opentargets.org" + to : to; + + const handleClick = (e) => { + e.preventDefault(); + if (onClick) onClick(); + if (resolvedTo) { + const app = getApp(); + if (app) app.openLink({ url: resolvedTo }).catch(() => {}); + } + }; + + const anchor = React.createElement( + "a", + { + className: classNames( + classes.base, + { + [classes.baseDefault]: !footer && !tooltip, + [classes.baseFooter]: footer, + [classes.baseTooltip]: tooltip, + }, + className + ), + href: resolvedTo, + onClick: handleClick, + ...ariaLabelProp, + }, + children + ); + + // Real Link.tsx only shows the OtAsyncTooltip hover-preview for internal + // (non-external) RouterLink navigation. This widget always renders a plain + // <a> (see above), so asyncTooltip is keyed off the prop alone, same "/entity/id" + // path-parsing the real component uses. + if (asyncTooltip && to) { + const args = to.split("/"); + return React.createElement(OtAsyncTooltip, { entity: args[1], id: args[2] }, anchor); + } + + return anchor; +} + +export default Link; `; } }, diff --git a/apps/mcp-widgets-server/widget-src/shared/stubs/ui-index.tsx b/apps/mcp-widgets-server/widget-src/shared/stubs/ui-index.tsx index ec6fea4a7..99381871d 100644 --- a/apps/mcp-widgets-server/widget-src/shared/stubs/ui-index.tsx +++ b/apps/mcp-widgets-server/widget-src/shared/stubs/ui-index.tsx @@ -8,44 +8,28 @@ * This stub file replaces those imports at build time (via the stubUiBarrel * Vite plugin in vite.widget.config.ts). */ -import type { ComponentProps, MouseEvent, ReactNode } from "react"; +import type { ReactNode } from "react"; import { Box } from "@mui/material"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faArrowRightToBracket } from "@fortawesome/free-solid-svg-icons"; -import RealLink from "@ot/ui/components/Link/Link"; -import { getApp } from "@widget-shared/createWidgetEntry"; +import { getConfig } from "@ot/config"; /** - * Link: reuses the real platform Link (packages/ui/src/components/Link/Link.tsx) - * for its actual styling/hover behavior, but always forces `external` — the widget - * has no real <Routes> for RouterLink to navigate to (it's a standalone component - * in a bare <MemoryRouter>), so internal-style paths ("/target/ENSG...") are - * resolved to absolute platform URLs. - * - * Navigation goes through app.openLink() rather than the native <a target="_blank"> - * click — MCP App iframes are sandboxed by the host, which may not grant popup - * permissions, so raw anchor-tag navigation is unreliable. openLink() is the - * spec-sanctioned way to ask the host to open a URL in the real browser. + * Link: `@ot/ui/components/Link/Link` (the real component's own module path) is + * intercepted at the Vite-plugin level (createPlatformStubsPlugin, widget.config.base.ts) + * to force external-only, openLink()-routed navigation — that covers this barrel import + * AND the several real components that import Link via a relative path instead + * (PublicationWrapper.tsx, Footer.tsx), which would otherwise bypass a stub living only + * in this file. Re-exported here just to satisfy the "ui" barrel surface. */ -export function Link({ to, onClick, ...rest }: ComponentProps<typeof RealLink>) { - const resolvedTo = to?.startsWith("/") ? `https://platform.opentargets.org${to}` : to; - - const handleClick = (e: MouseEvent<HTMLAnchorElement>) => { - e.preventDefault(); - onClick?.(); - if (resolvedTo) getApp()?.openLink({ url: resolvedTo }).catch(() => {}); - }; - - return ( - <RealLink {...rest} to={resolvedTo} external newTab onClick={handleClick as () => void} /> - ); -} +import Link from "@ot/ui/components/Link/Link"; +export { Link }; /** * Navigate: mirrors packages/ui/src/components/Navigate.tsx exactly, but composed * with the Link above instead of importing the real component directly — the real - * Navigate.tsx imports Link via a relative path ("./Link"), which bypasses this - * stub file entirely and would reintroduce the broken-click issue Link fixes. + * Navigate.tsx imports Link via a relative path ("./Link"), which (before the + * plugin-level interception above) would have bypassed a stub living only in this file. */ export function Navigate({ to }: { to?: string }) { return ( @@ -206,9 +190,33 @@ export { default as RecordsCards } from "@ot/ui/components/ClinicalReports/Recor export { default as ClinicalReportsMasterDetailFrame } from "@ot/ui/components/ClinicalReports/ClinicalReportsMasterDetailFrame"; export { default as ClinicalRecordDrawer } from "@ot/ui/components/ClinicalReports/ClinicalRecordDrawer"; -/** usePlatformApi: returns null — fragments are not needed inside the widget */ +/** + * usePlatformApi: in the real app, calling this with no fragment (as MolecularInteractions + * Body.tsx does — the only live caller across all migrated sections, confirmed via + * repo-wide grep) returns the raw PlatformApiContext: the *entire* already-fetched target + * profile page query, cached by a page-level provider the widget has no equivalent of + * (that whole-page prefetch architecture was removed — see [[project-mcp-server-review]]). + * + * MolecularInteractions passes this straight through as SectionItem's `request` prop. + * SectionItem gates ALL rendering on `definition.hasData(data[entity])`, which for this + * section checks `data.interactions?.count > 0` — an empty/null data object makes + * SectionItem decide there's "no data" and return null, so the section would never + * render for any target. Returning a truthy `interactions.count` here just lets the + * *real* per-tab gating take over: each of the 4 tabs (Intact/Signor/Reactome/String) + * independently re-fetches and checks its own real count via client.query. The only + * cost is a target with truly zero interactions across all 4 sources shows an + * all-disabled-tabs section instead of no section at all — a minor degradation, not + * a correctness bug for the vast majority of targets that do have some data. + * + * If usePlatformApi() ever gets a second caller with a different `hasData` shape, + * this hardcoded shape will need revisiting — it's tailored to this one call site. + */ export function usePlatformApi() { - return null; + return { + loading: false, + error: undefined, + data: { target: { interactions: { count: 1 } } }, + }; } /** useApolloClient: standard Apollo client from the widget's ApolloProvider */ @@ -217,6 +225,7 @@ export { useApolloClient } from "@apollo/client"; // ── Real components (direct paths, bypass barrel) ──────────────────────────── export { default as ChipList } from "@ot/ui/components/ChipList"; export { default as SectionLoader } from "@ot/ui/components/Section/SectionLoader"; +export { default as OtGenomicLocation } from "@ot/ui/components/GenomicLocation"; export { default as DirectionOfEffectIcon } from "@ot/ui/components/DirectionOfEffectIcon"; export { default as DirectionOfEffectTooltip } from "@ot/ui/components/DirectionOfEffectTooltip"; export { default as OtTableSSP } from "@ot/ui/components/OtTable/OtTableSSP"; @@ -242,9 +251,26 @@ export { default as PublicationSummaryLabel } from "@ot/ui/components/Publicatio export { default as PublicationWrapper } from "@ot/ui/components/PublicationsDrawer/PublicationWrapper"; export { default as SummaryLoader } from "@ot/ui/components/PublicationsDrawer/SummaryLoader"; export { default as useBatchDownloader } from "@ot/ui/hooks/useBatchDownloader"; -export { useConfigContext } from "@ot/ui/providers/ConfigurationProvider"; +/** + * useConfigContext: the widget has no <OTConfigurationProvider> ancestor (that provider + * throws without a real Config object, and also wires up a second Apollo client/theme/API + * metadata provider the widget doesn't need). Real config is still available via getConfig() + * from @ot/config — the same window.configProfile-based resolution createWidgetEntry.tsx + * already uses for `theme` — so components reading config.profile.* (PartnerLockIcon, + * SwissbioViz) get real values. urlAiApi resolves empty (no AI backend wired for the widget), + * which keeps AI-summary features (PublicationWrapper/Publication) off rather than crashing. + */ +export function useConfigContext() { + return { config: getConfig() }; +} -// ── Viewer stubs (3D viewer sections are excluded; these prevent missing-export errors) ─ +// ── Viewer stubs ─────────────────────────────────────────────────────────────── +// ViewerProvider/useViewerState etc. only carry real shared state in the manual +// molecular-structure widget, which uses its own ui-ms-index.tsx stub with the real +// versions instead of these. Sections that reference these hooks without that +// provider (e.g. target/MolecularStructure's Viewer.tsx, which manages its own local +// state and doesn't need shared viewer context) get inert values here — their own +// code already handles the "no provider" case gracefully (see ViewerLegend.tsx). export function ViewerProvider({ children }: { children: React.ReactNode }) { return <>{children}</>; } export function ViewerInteractionProvider({ children }: { children: React.ReactNode }) { return <>{children}</>; } export function useViewerState() { return {}; } diff --git a/apps/mcp-widgets-server/widget-src/shared/stubs/ui-ms-index.tsx b/apps/mcp-widgets-server/widget-src/shared/stubs/ui-ms-index.tsx index 6febc8de1..88d96d462 100644 --- a/apps/mcp-widgets-server/widget-src/shared/stubs/ui-ms-index.tsx +++ b/apps/mcp-widgets-server/widget-src/shared/stubs/ui-ms-index.tsx @@ -31,6 +31,7 @@ export { DisplayVariantId, usePlatformApi, useBatchQuery, + OtGenomicLocation, } from "./ui-index"; // ---- Stubs ---- @@ -50,9 +51,6 @@ export function ObsTooltip() { export function Tooltip({ children }: { children: React.ReactNode }) { return <>{children}</>; } -export function OtAsyncTooltip({ children }: { children: React.ReactNode }) { - return <>{children}</>; -} // Sequence track — not needed in the standalone widget export function ViewerTrack() { From 3601b45ad8065a470d015a67dbf84c9e390d8e90 Mon Sep 17 00:00:00 2001 From: Carlos Cruz <me@carcruzcast.com> Date: Mon, 13 Jul 2026 19:21:13 +0100 Subject: [PATCH 6/6] feat: add profile config scripts to Dockerfile for widget HTML shell inlining --- apps/mcp-widgets-server/Dockerfile | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/mcp-widgets-server/Dockerfile b/apps/mcp-widgets-server/Dockerfile index 1ba0e8bc7..9c431de0c 100644 --- a/apps/mcp-widgets-server/Dockerfile +++ b/apps/mcp-widgets-server/Dockerfile @@ -39,6 +39,10 @@ COPY apps/mcp-widgets-server/dist apps/mcp-widgets-server/dist # 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