/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ /** * Static data shared by all three landing-page variants: * * • CATALOG — the auto-generated tool list (typed import of the JSON * emitted by `node packages/mcp/dist/cli.js --dump-tools`). * Until that script lands, `mcp-catalog.json` is hand- * seeded with the real shape of the v0.1 surface so the * UI is real. * • CATEGORIES — display order + short blurb per category. * • CLIENTS — the install-grid targets (Claude Desktop, Cursor, * Windsurf, VS Code, Goose). * • RECIPES — copy-pasteable starter prompts. * • makeConfig… — builds the JSON config snippet & deep-link URL each * client expects, so the install Dialog is one source * of truth instead of three. */ import catalogJson from '@/generated/mcp-catalog.json'; import type { CatalogTool, McpCatalog, McpClient, McpClientId, McpRecipe, ToolCategory, } from './types'; export const CATALOG: McpCatalog = catalogJson as McpCatalog; /** * The version we advertise on the page. Read from the catalog if present, * else fall back to a sensible default. (Kept here so the three landings * agree without re-reading the JSON.) */ export const MCP_VERSION = CATALOG.version ?? '0.1.0'; /** * Display order. Maps to the "package.json scripts" mental model so users * scanning the catalog land on Discovery → Query first (most common entry * points), then escalating capability tiers. */ export const CATEGORY_ORDER: ToolCategory[] = [ 'Discovery', 'Query', 'Geometry', 'Validation', 'Mutation', 'BCF', 'bSDD', 'Diff', 'Export', 'Viewer', 'Clash', ]; export const CATEGORY_BLURBS: Record = { Discovery: 'Models, schema, what’s loaded.', Query: 'Find entities, properties, materials, classifications.', Geometry: 'Bounding boxes, volumes, areas — read from quantity sets.', Validation: 'IDS specs and a built-in model audit.', Mutation: 'Queue property/attribute writes; persist on save.', BCF: 'Author and export buildingSMART issues + viewpoints.', bSDD: 'Look up canonical class/property metadata.', Diff: 'Compare two loaded models — added/removed/changed.', Export: 'Dump to .ifc, CSV, JSON, glTF, IFCx, PDF.', Viewer: 'Drive the live WebGL viewer; subscribe to user picks.', Clash: 'Mesh in-browser and detect hard/clearance interferences.', }; export const CATEGORY_GLYPHS: Record = { Discovery: '01', Query: '02', Geometry: '03', Validation: '04', Mutation: '05', BCF: '06', bSDD: '07', Diff: '08', Export: '09', Viewer: '10', Clash: '11', }; export function toolsByCategory(): Map { const map = new Map(); for (const cat of CATEGORY_ORDER) map.set(cat, []); for (const tool of CATALOG.tools) { const list = map.get(tool.category) ?? []; list.push(tool); map.set(tool.category, list); } for (const [cat, list] of map) { list.sort((a, b) => a.name.localeCompare(b.name)); map.set(cat, list); } return map; } // ── install grid ──────────────────────────────────────────────────────────── export const CLIENTS: McpClient[] = [ { id: 'claude-desktop', name: 'Claude Desktop', blurb: 'Drop into the desktop config. Restart, ask Claude.', configHint: '~/Library/Application Support/Claude/claude_desktop_config.json', }, { id: 'cursor', name: 'Cursor', blurb: 'One-click install via deep link, or paste into mcp.json.', deepLinkPrefix: 'cursor://anysphere.cursor-deeplink/mcp/install', configHint: '~/.cursor/mcp.json', }, { id: 'windsurf', name: 'Windsurf', blurb: 'Same shape as Cursor; deep link supported in 1.4+.', deepLinkPrefix: 'windsurf://mcp/install', configHint: '~/.codeium/windsurf/mcp_config.json', }, { id: 'vscode', name: 'VS Code', blurb: 'GitHub Copilot Chat, agent mode. Adds an MCP entry.', deepLinkPrefix: 'vscode:mcp/install', configHint: '.vscode/mcp.json (workspace) or settings.json', }, { id: 'goose', name: 'Goose', blurb: 'Block’s open-source agent. CLI install.', configHint: '~/.config/goose/profiles.yaml', }, ]; /** Build the canonical JSON snippet for each client. */ export function makeConfigSnippet(client: McpClientId): string { const sample = '/abs/path/to/your/model.ifc'; switch (client) { case 'claude-desktop': return JSON.stringify( { mcpServers: { 'ifc-lite': { command: 'npx', args: ['-y', '@ifc-lite/mcp', sample], }, }, }, null, 2, ); case 'cursor': return JSON.stringify( { mcpServers: { 'ifc-lite': { command: 'npx', args: ['-y', '@ifc-lite/mcp', sample], }, }, }, null, 2, ); case 'windsurf': return JSON.stringify( { mcpServers: { 'ifc-lite': { command: 'npx', args: ['-y', '@ifc-lite/mcp', sample], }, }, }, null, 2, ); case 'vscode': return JSON.stringify( { servers: { 'ifc-lite': { type: 'stdio', command: 'npx', args: ['-y', '@ifc-lite/mcp', sample], }, }, }, null, 2, ); case 'goose': return [ '# In ~/.config/goose/profiles.yaml', 'extensions:', ' ifc-lite:', ' cmd: npx', ' args: ["-y", "@ifc-lite/mcp", "/abs/path/to/your/model.ifc"]', ].join('\n'); } } /** * Build the deep-link URL for clients that support one-click install. * Returns null for clients that need manual config. */ export function makeDeepLink(client: McpClientId): string | null { const c = CLIENTS.find((x) => x.id === client); if (!c?.deepLinkPrefix) return null; const config = { name: 'ifc-lite', command: 'npx', args: ['-y', '@ifc-lite/mcp'], }; const encoded = btoa(unescape(encodeURIComponent(JSON.stringify(config)))); return `${c.deepLinkPrefix}?name=ifc-lite&config=${encoded}`; } // ── recipes ───────────────────────────────────────────────────────────────── export const RECIPES: McpRecipe[] = [ { id: 'audit-fire-ratings', title: 'Audit fire-rating coverage', family: 'audit', prompt: 'Run model_audit, then for every IfcWall and IfcDoor with no FireRating in Pset_WallCommon / Pset_DoorCommon, list the GlobalId and storey. Group by storey.', uses: ['model_audit', 'query_entities', 'properties_unique'], }, { id: 'visualize-external-walls', title: 'Visualize all external walls', family: 'visualize', prompt: 'Open the viewer, isolate IfcWall where Pset_WallCommon.IsExternal=true, color them red, frame the camera on the bounds.', uses: ['viewer_open', 'viewer_isolate', 'viewer_colorize', 'viewer_fly_to'], }, { id: 'validate-ids', title: 'Validate against this IDS spec', family: 'validate', prompt: 'Validate the model against ./specs/lod350-walls.ids. Summarize the failing specifications and color the offending entities red in the viewer.', uses: ['ids_validate', 'ids_explain', 'viewer_colorize'], }, { id: 'narrow-doors', title: 'BCF every door under 80 cm', family: 'author', prompt: 'Find every IfcDoor whose OverallWidth < 0.80m. For each, create a BCF topic with priority=Major and a viewpoint that selects only that door. Export the .bcfzip when done.', uses: ['query_entities', 'bcf_topic_create', 'bcf_viewpoint_create', 'bcf_export'], }, { id: 'diff-versions', title: 'Diff two model revisions', family: 'compare', prompt: 'Load arch.v3.ifc as model "v3" and arch.v4.ifc as model "v4". Run model_diff and quantity_diff(IfcWall, Volume). Tell me what walls were added or modified.', uses: ['model_load', 'model_diff', 'quantity_diff'], }, { id: 'bsdd-properties', title: 'Lookup bSDD properties for IfcWall', family: 'discover', prompt: 'Use bsdd_property_sets for IfcWall. List every Pset and the canonical property names + datatypes. Highlight any in our model that aren’t in the bSDD spec.', uses: ['bsdd_property_sets', 'properties_unique', 'schema_describe'], }, { id: 'click-to-inspect', title: 'Click to inspect', family: 'visualize', prompt: 'Open the viewer. When I click an entity, run viewer_describe_selection and tell me everything: attributes, properties, quantities, classifications, materials.', uses: ['viewer_open', 'viewer_wait_for_selection', 'viewer_describe_selection'], }, { id: 'space-program', title: 'Check the space program', family: 'audit', prompt: 'Group IfcSpace by storey, sum the area, and tell me which storeys are over/under their target gross area from Pset_SpaceProgram.', uses: ['query_entities', 'count_entities', 'geometry_area'], }, ]; export const FAMILY_ACCENT: Record = { audit: '#d6ff3f', visualize: '#7aa2f7', validate: '#73daca', author: '#ff9e64', compare: '#bb9af7', discover: '#9ece6a', }; // ── stats helpers ─────────────────────────────────────────────────────────── export function catalogStats() { const tools = CATALOG.tools; const byScope = { read: 0, mutate: 0, export: 0 }; for (const t of tools) byScope[t.scope]++; return { total: tools.length, categories: new Set(tools.map((t) => t.category)).size, read: byScope.read, mutate: byScope.mutate, export: byScope.export, }; } // ── parameter introspection ───────────────────────────────────────────────── // // The hand-seeded catalog only carries `type` + `required` for many tools // (a richer schema dump will land once the CLI flag is wired). The catalog // page still has to *show* something useful per tool, so we fall back to a // curated map of parameter descriptions per tool — the agent-facing docs. // When the live catalog gains real `properties` entries, the page prefers // those and the fallback only fills in the gaps. export interface ParamRow { name: string; type: string; required: boolean; description?: string; } const PARAM_FALLBACKS: Record> & { required?: string[] }> = (() => { type Row = Omit; function pset(includeName = true): Row[] { const r: Row[] = [ { name: 'pset', type: 'string', description: 'Property set name (e.g. "Pset_WallCommon").' }, ]; if (includeName) r.push({ name: 'name', type: 'string', description: 'Property name within the pset.' }); return r; } function refLocators(): Row[] { return [ { name: 'global_id', type: 'string', description: 'IFC GlobalId. Either this or express_id is required.' }, { name: 'express_id', type: 'integer', description: 'STEP express id. Either this or global_id is required.' }, { name: 'model_id', type: 'string', description: 'Optional when only one model is loaded.' }, ]; } return { model_info: [{ name: 'model_id', type: 'string', description: 'Optional; defaults to the only loaded model.' }], model_load: [ { name: 'file_path', type: 'string', description: 'Absolute path to the .ifc file.' }, { name: 'model_id', type: 'string', description: 'Optional explicit ID; defaults to a slug of the file name.' }, ], model_unload: [{ name: 'model_id', type: 'string', description: 'Model to drop from the registry.' }], schema_describe: [ { name: 'type', type: 'string', description: 'IFC entity name, e.g. "IfcWall".' }, { name: 'include_inherited', type: 'boolean', description: 'Include attributes from parent classes (default true).' }, ], query_entities: [ { name: 'type', type: 'string', description: 'IFC entity type filter (e.g. "IfcWall").' }, { name: 'limit', type: 'integer', description: 'Cap the result count. Default 100.' }, { name: 'offset', type: 'integer', description: 'Skip this many matches before returning.' }, { name: 'fields', type: 'string[]', description: 'Whitelist of fields per result (default returns the full shape).' }, { name: 'in_storey', type: 'string', description: 'GlobalId of an IfcBuildingStorey to scope the query.' }, { name: 'model_id', type: 'string' }, ], count_entities: [ { name: 'group_by', type: '"type" | "storey" | "material"' }, { name: 'type', type: 'string', description: 'Restrict the count to one IFC type.' }, { name: 'model_id', type: 'string' }, ], get_entity: [...refLocators()], get_entities_bulk: [ { name: 'global_ids', type: 'string[]', description: 'Up to 200 GlobalIds to resolve.' }, { name: 'model_id', type: 'string' }, ], properties_unique: [ { name: 'type', type: 'string', description: 'IFC type to scan, e.g. "IfcWall".' }, ...pset(), { name: 'property', type: 'string', description: 'Property name within the pset.' }, { name: 'model_id', type: 'string' }, ], relationships: refLocators(), containment_chain: refLocators(), geometry_bbox: refLocators(), geometry_volume: refLocators(), geometry_area: refLocators(), ids_validate: [ { name: 'ids_path', type: 'string', description: 'Path to the .ids XML file to validate against.' }, { name: 'model_id', type: 'string' }, ], ids_explain: [{ name: 'ids_path', type: 'string', description: 'Path to the .ids XML to summarise in plain language.' }], entity_set_property: [ ...refLocators(), ...pset(), { name: 'value', type: 'string | number | boolean', description: 'New value to write.' }, ], entity_delete_property: [...refLocators(), ...pset()], entity_set_attribute: [ ...refLocators(), { name: 'attribute', type: '"Name" | "Description" | "ObjectType" | "Tag"' }, { name: 'value', type: 'string' }, ], entity_create: [ { name: 'type', type: 'string', description: 'IFC entity to create, e.g. "IfcBuildingElementProxy".' }, { name: 'attributes', type: 'unknown[]', description: 'Positional STEP attributes (strings, numbers, refs as "#42").' }, { name: 'model_id', type: 'string' }, ], entity_delete: refLocators(), mutation_batch: [ { name: 'operations', type: '{ tool: string; args: object }[]', description: 'List of sub-tool ops to apply in order.' }, { name: 'model_id', type: 'string' }, ], mutation_undo: [{ name: 'n', type: 'integer', description: 'Pop the last N pending mutations (default 1).' }], model_save: [ { name: 'file_path', type: 'string', description: 'Output .ifc path.' }, { name: 'schema', type: '"IFC2X3" | "IFC4" | "IFC4X3"' }, { name: 'model_id', type: 'string' }, ], bcf_topic_list: [{ name: 'status', type: 'string', description: 'Optional status filter (e.g. "Open").' }], bcf_topic_create: [ { name: 'title', type: 'string' }, { name: 'description', type: 'string' }, { name: 'priority', type: 'string' }, { name: 'assigned_to', type: 'string' }, { name: 'labels', type: 'string[]' }, ], bcf_topic_update: [ { name: 'guid', type: 'string', description: 'BCF topic GUID.' }, { name: 'status', type: 'string' }, { name: 'priority', type: 'string' }, { name: 'comment', type: 'string', description: 'Append a comment.' }, ], bcf_topic_close: [{ name: 'guid', type: 'string' }], bcf_viewpoint_create: [ { name: 'guid', type: 'string', description: 'Topic to attach the viewpoint to.' }, { name: 'selection_global_ids', type: 'string[]' }, ], bcf_export: [{ name: 'file_path', type: 'string', description: 'Output .bcfzip path.' }], bsdd_search: [{ name: 'query', type: 'string', description: 'Free-text search across all bSDD dictionaries.' }], bsdd_class: [{ name: 'ifc_type', type: 'string', description: 'IFC entity name, e.g. "IfcWall".' }], bsdd_property_sets: [{ name: 'ifc_type', type: 'string' }], bsdd_match: refLocators(), model_diff: [ { name: 'a', type: 'string', description: 'model_id of the base.' }, { name: 'b', type: 'string', description: 'model_id of the head.' }, { name: 'by_entity', type: 'boolean', description: 'Include per-entity GlobalId additions/removals (default true).' }, ], quantity_diff: [ { name: 'a', type: 'string' }, { name: 'b', type: 'string' }, { name: 'type', type: 'string', description: 'Default "IfcWall".' }, { name: 'quantity', type: 'string', description: 'Default "Volume".' }, { name: 'group_by', type: '"storey" | "type"' }, ], export_ifc: [ { name: 'file_path', type: 'string' }, { name: 'schema', type: '"IFC2X3" | "IFC4" | "IFC4X3"' }, { name: 'global_ids', type: 'string[]', description: 'Optional GlobalId allowlist; defaults to the whole model.' }, ], export_csv: [ { name: 'file_path', type: 'string' }, { name: 'type', type: 'string', description: 'Filter by IFC type. Default: all products.' }, { name: 'columns', type: 'string[]', description: 'Plain attributes or "Pset_X.Property" / "Qto_X.Quantity" paths.' }, { name: 'separator', type: 'string', description: 'Default ",".' }, ], export_json: [ { name: 'file_path', type: 'string' }, { name: 'type', type: 'string' }, { name: 'columns', type: 'string[]' }, ], viewer_open: [{ name: 'model_id', type: 'string' }], viewer_colorize: [ { name: 'global_ids', type: 'string[]' }, { name: 'express_ids', type: 'integer[]' }, { name: 'type', type: 'string' }, { name: 'color', type: 'string | [r,g,b] | [r,g,b,a]', description: 'Hex (#ff8800), [0–1] tuple, or named colour.' }, { name: 'model_id', type: 'string' }, ], viewer_isolate: [ { name: 'global_ids', type: 'string[]' }, { name: 'express_ids', type: 'integer[]' }, { name: 'type', type: 'string' }, { name: 'model_id', type: 'string' }, ], viewer_hide: [ { name: 'global_ids', type: 'string[]' }, { name: 'express_ids', type: 'integer[]' }, { name: 'type', type: 'string' }, ], viewer_show: [ { name: 'global_ids', type: 'string[]' }, { name: 'express_ids', type: 'integer[]' }, { name: 'type', type: 'string' }, ], viewer_fly_to: [ { name: 'global_ids', type: 'string[]' }, { name: 'express_ids', type: 'integer[]' }, ], viewer_set_section: [ { name: 'axis', type: '"x" | "y" | "z"' }, { name: 'position', type: 'number', description: 'Section plane offset along the chosen axis.' }, { name: 'flipped', type: 'boolean' }, { name: 'enabled', type: 'boolean' }, ], viewer_color_by_property: [ { name: 'type', type: 'string' }, { name: 'pset', type: 'string' }, { name: 'property', type: 'string' }, { name: 'missing_color', type: 'string', description: 'Colour for entities lacking the property. Default "gray".' }, ], viewer_get_selection: [ { name: 'include', type: '("attributes"|"properties"|"quantities"|"classifications"|"materials")[]', description: 'Default ["attributes","classifications","materials"].' }, ], viewer_describe_selection: [], viewer_wait_for_selection: [ { name: 'timeout_ms', type: 'integer', description: 'Default 60000.' }, { name: 'include', type: 'string[]' }, ], viewer_ask: [ { name: 'reason', type: 'string', description: 'Why the agent wants the viewer open (used in the suggested wording).' }, { name: 'model_id', type: 'string' }, ], }; })(); /** * Build a parameter table for a tool. Merges fields declared on the live * `inputSchema.properties` with the curated `PARAM_FALLBACKS` map, so a * partially-enriched schema (a few typed fields, no descriptions) still * picks up the curated descriptions instead of rendering blank cells. * * Schema is the source of truth for `type`, `enum`, and `required`; the * fallback only fills gaps in `description` and supplies any rows the * schema didn't declare at all. */ export function paramsFor(tool: CatalogTool): ParamRow[] { const schema = tool.inputSchema as { properties?: Record; required?: string[] }; const required = new Set(schema?.required ?? []); const fallback = PARAM_FALLBACKS[tool.name] ?? []; const fallbackByName = new Map(fallback.map((row) => [row.name, row])); const seen = new Set(); const rows: ParamRow[] = []; if (schema?.properties) { for (const [name, def] of Object.entries(schema.properties)) { const fb = fallbackByName.get(name); const type = def?.enum ? def.enum.map((v) => JSON.stringify(v)).join(' | ') : (def?.type ?? fb?.type ?? 'unknown'); rows.push({ name, type, required: required.has(name), description: def?.description ?? fb?.description, }); seen.add(name); } } // Append fallback-only rows (schemas that haven't enumerated every // parameter yet — mostly the v0.5 stubs). for (const row of fallback) { if (seen.has(row.name)) continue; rows.push({ ...row, required: required.has(row.name) }); } return rows; } // ── example invocations ───────────────────────────────────────────────────── // // One realistic example arg payload per tool. Used by the catalog page to // show "this is what an agent would actually send" — far more useful than // dumping the JSON Schema verbatim. export const EXAMPLES: Record> = { model_info: {}, model_list: {}, model_load: { file_path: '/abs/path/to/arch.ifc' }, model_unload: { model_id: 'arch' }, schema_describe: { type: 'IfcWall' }, query_entities: { type: 'IfcWall', limit: 25 }, count_entities: { group_by: 'type' }, get_entity: { global_id: '1AQAupaRP1txwK1AGiN61V' }, get_entities_bulk: { global_ids: ['1AQAupaRP1txwK1AGiN61V', '0u4wgLe6n0ABVaiXyikbkA'] }, spatial_hierarchy: {}, containment_chain: { global_id: '1AQAupaRP1txwK1AGiN61V' }, relationships: { global_id: '1AQAupaRP1txwK1AGiN61V' }, properties_unique: { type: 'IfcWall', pset: 'Pset_WallCommon', property: 'IsExternal' }, materials_list: {}, classifications_list: {}, georeferencing: {}, units: {}, geometry_bbox: { global_id: '1AQAupaRP1txwK1AGiN61V' }, geometry_volume: { global_id: '1AQAupaRP1txwK1AGiN61V' }, geometry_area: { global_id: '1AQAupaRP1txwK1AGiN61V' }, clash_check: { a: 'IfcDuct*|IfcPipe*', b: 'IfcWall*|IfcSlab*' }, clash_matrix: { mode: 'hard' }, clash_bcf_export: { group_by: 'cluster' }, model_audit: {}, ids_validate: { ids_path: './specs/lod350-walls.ids' }, ids_explain: { ids_path: './specs/lod350-walls.ids' }, entity_set_property: { global_id: '1AQAupaRP1txwK1AGiN61V', pset: 'Pset_WallCommon', name: 'FireRating', value: 'EI60' }, entity_delete_property: { global_id: '1AQAupaRP1txwK1AGiN61V', pset: 'Pset_WallCommon', name: 'FireRating' }, entity_set_attribute: { global_id: '1AQAupaRP1txwK1AGiN61V', attribute: 'Description', value: 'Touched by MCP suite' }, entity_create: { type: 'IfcBuildingElementProxy', attributes: [] }, entity_delete: { express_id: 981 }, mutation_batch: { operations: [{ tool: 'entity_set_property', args: { global_id: '...', pset: 'Pset_WallCommon', name: 'FireRating', value: 'EI60' } }] }, mutation_diff: {}, mutation_undo: { n: 1 }, model_save: { file_path: '/tmp/audited.ifc' }, bcf_topic_list: {}, bcf_topic_create: { title: 'Missing fire rating', priority: 'Major' }, bcf_topic_update: { guid: '7e87d7f4-...', comment: 'Confirmed with the structural team.' }, bcf_topic_close: { guid: '7e87d7f4-...' }, bcf_viewpoint_create: { guid: '7e87d7f4-...', selection_global_ids: ['1AQAupaRP1txwK1AGiN61V'] }, bcf_export: { file_path: '/tmp/issues.bcfzip' }, bsdd_search: { query: 'wall' }, bsdd_class: { ifc_type: 'IfcWall' }, bsdd_property_sets: { ifc_type: 'IfcWall' }, bsdd_match: { global_id: '1AQAupaRP1txwK1AGiN61V' }, model_diff: { a: 'arch_v3', b: 'arch_v4' }, quantity_diff: { a: 'arch_v3', b: 'arch_v4', type: 'IfcWall', quantity: 'Volume' }, export_ifc: { file_path: '/tmp/audited.ifc' }, export_csv: { file_path: '/tmp/walls.csv', type: 'IfcWall', columns: ['GlobalId', 'Name', 'Pset_WallCommon.IsExternal'] }, export_json: { file_path: '/tmp/walls.json', type: 'IfcWall' }, export_glb: { file_path: '/tmp/scene.glb' }, export_ifcx: { file_path: '/tmp/scene.ifcx' }, export_pdf_report: { file_path: '/tmp/audit.pdf' }, viewer_ask: { reason: 'to highlight failing fire-rated doors' }, viewer_open: {}, viewer_close: {}, viewer_status: {}, viewer_colorize: { type: 'IfcWall', color: '#d6ff3f' }, viewer_isolate: { type: 'IfcWall' }, viewer_hide: { global_ids: ['1AQAupaRP1txwK1AGiN61V'] }, viewer_show: { global_ids: ['1AQAupaRP1txwK1AGiN61V'] }, viewer_reset: {}, viewer_fly_to: { global_ids: ['1AQAupaRP1txwK1AGiN61V'] }, viewer_set_section: { axis: 'z', position: 1.5 }, viewer_clear_section: {}, viewer_color_by_storey: {}, viewer_color_by_property: { type: 'IfcWall', pset: 'Pset_WallCommon', property: 'IsExternal' }, viewer_get_selection: { include: ['attributes', 'properties', 'materials'] }, viewer_describe_selection: {}, viewer_wait_for_selection: { timeout_ms: 60000 }, }; /** Build the JSON-RPC tools/call envelope for a given tool example. */ export function exampleCall(tool: CatalogTool): string { return JSON.stringify( { jsonrpc: '2.0', id: 1, method: 'tools/call', params: { name: tool.name, arguments: EXAMPLES[tool.name] ?? {} }, }, null, 2, ); }