MCP Workspace Exposure Plan

Goal

Expose workspace read/write and widget schema discovery through the MCP server, so AI agents can construct, inspect, and modify Nagelfluh layouts programmatically.


MCP Tools to Add

Tool Description
list_workspaces List all saved workspaces (id, title, timestamps). Use to discover what layouts exist before reading one.
get_workspace Get the full layout tree for a workspace. Returns a recursive JSON tree of nodes with id, widget, children, and widget-specific layoutConfig. Call get_workspace_schema first to understand valid structures.
create_workspace Create a new workspace with a title and layout tree. The layout field must conform to the schema returned by get_workspace_schema.
get_workspace_schema Get the JSON Schema for the entire workspace layout format — the recursive node tree, all valid widget types as a discriminated union, and per-widget layoutConfig schemas including Nagelfluh-specific PlotView layer types. Always call this before constructing a layout.

Architecture

Why widget schemas can't be extracted at build time automatically

PlotView's get_schema() returns a schema assembled by gladly at import time by merging schemas from all registered layer types — including Nagelfluh-specific ones (ResistivityCurtain, FlightlinePlot, MagLinePlot, etc.). This requires:

Node.js stubs cannot satisfy these constraints without producing wrong/empty schemas.

Solution: committed generated artifact

backend/widget_schemas.json is generated by a Puppeteer script that loads the running dev server, extracts all widget schemas from window.__nagelfluh_widgets, and writes the result. The file is committed to git and treated as a checked-in generated artifact — like a lockfile or generated protobuf stubs.

Docker builds and the production backend simply read the committed file. No schema generation happens at Docker build time or at server startup.


Implementation Steps

Phase 1 — Expose workspace endpoints in MCP

1.1 Add a "Workspaces" tag to all endpoints in backend/routers/workspaces.py.

1.2 Add "Workspaces" to the include_tags list in the FastApiMCP setup in backend/main.py.

1.3 Remove or do not expose the update and delete endpoints — workspaces are create-and-read only via MCP. Review and improve docstrings on the remaining endpoints; these become the MCP tool descriptions seen by the agent.


Phase 2 — Widget schema endpoint

2.1 Add GET /workspace-schema endpoint to backend/routers/workspaces.py. It reads backend/widget_schemas.json and assembles a single JSON Schema for the entire workspace layout format: a recursive $defs-based schema with a discriminated union over all widget types (container widgets and leaf widgets), each with their layoutConfig inlined. Tag it "Workspaces".

Return 503 with a clear message if the file is missing, so the developer knows to run the export script.

2.2 Add backend/widget_schemas.json as an empty {} placeholder and commit it, so the backend starts cleanly before the first real export.


Phase 3 — Frontend schema export

3.1 In frontend/src/App.js, after the widgets object is defined, expose it on window:

window.__nagelfluh_widgets = widgets;

This must be set before the app renders so Puppeteer can read it after a simple page load without waiting for user interaction.

3.2 Write frontend/scripts/export-widget-schemas.mjs:

import puppeteer from 'puppeteer'
import { writeFileSync } from 'fs'
import { resolve, dirname } from 'path'
import { fileURLToPath } from 'url'

const __dirname = dirname(fileURLToPath(import.meta.url))

const browser = await puppeteer.launch()
const page = await browser.newPage()

// Suppress console noise from the app
page.on('pageerror', () => {})

await page.goto('http://localhost:3000', { waitUntil: 'networkidle0' })

const schemas = await page.evaluate(() => {
  const widgets = window.__nagelfluh_widgets
  if (!widgets) throw new Error('__nagelfluh_widgets not found — is the dev server running?')
  const result = {}
  for (const [name, Widget] of Object.entries(widgets)) {
    result[name] = {
      title: Widget.title ?? name,
      schema: Widget.get_schema ? Widget.get_schema({}) : null,
      default: Widget.get_default ? Widget.get_default({}) : null,
    }
  }
  return result
})

await browser.close()

const outPath = resolve(__dirname, '../../backend/widget_schemas.json')
writeFileSync(outPath, JSON.stringify(schemas, null, 2))
console.log(`Written to ${outPath}`)

3.3 Add puppeteer as a dev dependency:

npm install --save-dev puppeteer

3.4 Add script to frontend/package.json:

"export-schemas": "node scripts/export-widget-schemas.mjs"

3.5 Run npm run export-schemas with the dev server running, verify backend/widget_schemas.json looks correct, and commit it.


Phase 4 — AGENTS.md discipline note

Add or update AGENTS.md at the repo root with the following section:

## Widget Schema Discipline

`backend/widget_schemas.json` is a **committed generated file**. It contains the JSON
Schemas for all widget types (including PlotView layer types assembled by gladly at
import time) and is read by the backend to assemble the full workspace JSON Schema
served by the `get_workspace_schema` MCP tool.

**You must regenerate and commit this file whenever you:**
- Add a new widget to `frontend/src/App.js`
- Add or modify a layer type schema in `frontend/src/widgets/PlotView/elements/`
- Change `get_schema()` or `get_default()` on any widget

To regenerate:
1. Ensure the dev server is running (`./runall.sh`)
2. `cd frontend && npm run export-schemas`
3. Commit `backend/widget_schemas.json` alongside your code changes

File Checklist

File Change
backend/routers/workspaces.py Add "Workspaces" tag (create/list/get only), add /workspace-schema endpoint, improve docstrings
backend/main.py Add "Workspaces" to include_tags
backend/widget_schemas.json New committed generated file
frontend/src/App.js Expose window.__nagelfluh_widgets
frontend/scripts/export-widget-schemas.mjs New Puppeteer export script
frontend/package.json Add puppeteer devDep, add export-schemas script
AGENTS.md New or updated — discipline note for schema regeneration