plaask
fdtd-mcp
MCP server for Lumerical FDTD
- Stars
- 2
- Language
- Python
- Created
- Jul 3, 2026
- Updated
- Aug 14, 2026
Introduction
FDTD MCP
MCP server for Lumerical FDTD automation. Let AI assistants read, edit, run, and analyze FDTD simulations through the Model Context Protocol.
Architecture
AI Assistant --MCP stdio--> server.py (system Python ≥3.10)
│ subprocess stdin/stdout
bridge.py (Lumerical embed Python 3.6.8)
│ lumapi
Lumerical FDTD engine
The dual-process design isolates the MCP protocol (which needs modern Python) from the Lumerical API (which only runs on the bundled Python 3.6.8). The bridge communicates via line-delimited JSON over stdin/stdout.
Installation
Prerequisites: Python ≥ 3.10, Lumerical FDTD
1. Install the package
git clone https://github.com/plaask/fdtd-mcp.git && cd fdtd-mcp
pip install .
2. Register with Claude Code
python install.py
This auto-detects your Lumerical installation and prints the registration command (which includes the detected --lumerical-home). Run the printed command, restart Claude Code, done.
If fdtd-mcp is not on your PATH, replace it with python -m fdtd_mcp.server in the printed command.
If auto-detection fails
If Lumerical is installed at a non-standard location, specify the path manually:
Option A — pass it in the registration command:
claude mcp add fdtd -- python -m fdtd_mcp.server --lumerical-home "C:/Program Files/Lumerical/v241"
Option B — set the env var once:
[Environment]::SetEnvironmentVariable("LUMERICAL_HOME", "C:/Program Files/Lumerical/v241", "User")
# Restart the terminal, then just:
claude mcp add fdtd -- python -m fdtd_mcp.server
Other MCP clients (Cursor, VS Code, etc.)
The JSON equivalent of claude mcp add. Auto-detection works here too:
{
"mcpServers": {
"fdtd": {
"command": "python",
"args": ["-m", "fdtd_mcp.server"]
}
}
}
If auto-detection fails, add the --lumerical-home argument:
{
"mcpServers": {
"fdtd": {
"command": "python",
"args": ["-m", "fdtd_mcp.server", "--lumerical-home", "C:/Program Files/Lumerical/v241"]
}
}
}
DeepSeek Harness (DSH)
The MCP server is bridged into DSH through its official plugin
@deepseek-ai/dsh-mcp-client (ships with every dsh install). Tools are exposed
to DSH as mcp__fdtd__* (e.g. mcp__fdtd__execute, mcp__fdtd__run,
mcp__fdtd__model_add; 30 tools total).
Registration steps:
- Open the web profile's user patch layer
$DSH_HOME/profiles/web/cordis.patch.yml($DSH_HOMEdefaults toC:\Users\<you>\.dsh) and append to the top-level array:
- insert:
- id: mcp-fdtd
name: '@deepseek-ai/dsh-mcp-client'
config:
serverName: fdtd
transport: stdio
command: D:/coding/anaconda3/python.exe # ← your Python ≥3.10 (with the mcp package)
args: ['-m', 'fdtd_mcp.server']
env:
PYTHONPATH: D:/project/fdtd-mcp # ← your checkout; can be removed after pip install .
cwd: D:/project/fdtd-mcp # ← same
failOnStartupError: false # skip tool registration, never block GUI boot
- No Lumerical path configuration needed: the server auto-detects it
(
--lumerical-homearg >LUMERICAL_HOMEenv var > scanning common install directories, newest version wins). For non-standard installs, changeargsto['-m', 'fdtd_mcp.server', '--lumerical-home', '<your-path>']or set theLUMERICAL_HOMEenvironment variable. Do NOT hardcode a nonexistentLUMERICAL_HOMEin the plugin config — the server trusts it without validation and auto-detection would be short-circuited. - The profile patch layer is watched live by DSH, so a restart is usually not
needed; if the tools do not appear, restart
dsh web. - Verify with
dsh web --dump-config— the output should contain themcp-fdtdrow.
For other profiles (e.g. headless), add the same snippet to
$DSH_HOME/profiles/<name>/cordis.patch.yml, or to the home-level
$DSH_HOME/cordis.patch.yml which applies to every profile. This integration does
not affect the Claude Code .mcp.json registration.
Tools (30 tools, 6 modules)
session (5) session_open, session_new, session_close,
session_save, session_save_as
model (6) model_info, model_add, model_get, model_set,
model_delete, model_script
material (5) material_add, material_get, material_set,
material_delete, material_exists
sweep (6) sweep_add, sweep_get, sweep_set, sweep_delete,
sweep_run, sweep_result
result (4) result_list, result_get, result_save, result_has
engine (4) run, execute, execute_file, reference_lookup
run / sweep_run auto-save an unsaved project to a temp path before solving,
so they never block on Lumerical's invisible "Save As" dialog when the engine is
run hidden. Note: model variables (addvar) are not available in Lumerical
v202 — create them in the GUI, or sweep_add parameter paths like
::model>gap will not resolve.
Module overview
| Module | Purpose | Tools |
|---|---|---|
| session | Project file lifecycle | session_open, session_new, session_close, session_save, session_save_as |
| model | Object tree unified CRUD | model_info, model_add, model_get, model_set, model_delete, model_script |
| material | Material database | material_add, material_get, material_set, material_delete, material_exists |
| sweep | Parameter sweep lifecycle | sweep_add, sweep_get, sweep_set, sweep_delete, sweep_run, sweep_result |
| result | Simulation data | result_list, result_get, result_save, result_has |
| engine | Direct engine interaction | run, execute, execute_file, reference_lookup |
model_add type table
| type | Lumerical command | Category |
|---|---|---|
rectangle, circle, ring, polygon, sphere, pyramid, triangle, waveguide | addrect, addcircle, ... | Geometry |
fdtd | addfdtd | Solver |
mesh | addmesh | Mesh |
dipole, tfsf, plane, gaussian, mode_source | adddipole, addtfsf, ... | Source |
power_monitor, dft_monitor, index_monitor, field_monitor, movie_monitor | addpower, adddftmonitor, ... | Monitor |
structure_group, analysis_group | addstructuregroup, addanalysisgroup | Group |
Usage examples
Open and inspect
session_open("D:/project/my_sim.fsp")
model_info() → objects, materials, variables, FDTD summary (one call)
model_get("FDTD") → full properties of the FDTD region
model_script("::model", action="get") → setup + analysis scripts
Build from scratch
session_new(dimension="3D", x_span=2e-6, y_span=2e-6, mesh_accuracy=4)
# Add objects
model_add(type="fdtd")
model_add(type="rectangle", name="substrate",
properties={"x span": 2e-6, "y span": 2e-6, "z span": 200e-9})
model_add(type="dipole", name="source_1")
# Set properties
model_set("substrate", {"material": "Si (Silicon) - Palik"})
model_set("source_1", {"x": 0, "y": 0, "z": 100e-9, "wavelength start": 500e-9})
session_save("new_sim.fsp")
Custom materials
material_add(type="Sampled 3D data") → {name: "material_1"}
material_set("material_1", "name", "PA_RCP")
material_set("material_1", "sampled 3d data",
[[300e-9, 1.5+0.001i], [800e-9, 1.5+0.001i]]) # Nx2 [wl, n+ik]
material_set("material_1", "mesh order", 2)
# Assign to an object
model_set("substrate", {"material": "PA_RCP"})
# Or import from file
execute('importnk("D:/data/nk_data.txt")')
model_set("substrate", {"material": "nk_data"})
Structure groups and analysis groups
# Create a structure group with script
model_add(type="structure_group", name="dbr_stack")
model_set("dbr_stack", {"x": 0, "y": 0})
model_script("dbr_stack", action="set", script_type="script",
content="addrect(); set('name', 'layer'); set('x span', 2e-6);")
# Create an analysis group
model_add(type="analysis_group", name="transmission_calc")
model_script("transmission_calc", action="set", script_type="setup",
content="addpower(); set('name', 'monitor');")
model_script("transmission_calc", action="set", script_type="analysis",
content="T = transmission('monitor');")
Run and get data
run()
result_has("monitor") → check before fetching
result_list("monitor") → discover available datasets
result_get("monitor", data="E",
fields=["Ex", "Ey", "f"]) → get specific fields (fields is REQUIRED)
result_save("monitor", data="E",
output="C:/data/fields.mat") → export to .mat file
Parameter sweeps
# Create and run
sweep_add(type=0, name="thickness_sweep",
parameters=[{"name": "t", "parameter": "::model::substrate::z span",
"start": 50e-9, "stop": 300e-9, "points": 6}],
results=[{"name": "T", "result": "::model::monitor::T"}])
sweep_run(name="thickness_sweep")
sweep_result(name="thickness_sweep", result="T") → get sweep data
Anti-hallucination
reference_lookup(list_only=true) → verify function names exist
reference_lookup(name="addrect") → get signature + pitfalls
execute("?getnamed('FDTD', 'dimension')") → ?expr captures return value
Key design principles
- Unified CRUD — every module uses consistent
add/get/set/deletenaming - Single-source dispatch —
dispatch.jsonmaps every tool to its bridge handler; both processes read the same table, so the two sides can't drift (and a test enforces it) - Execute is transparent —
execute(code)passes LSF directly to the engine with no parsing - model_set handles variables automatically — uses
addvar/addanalysisprop/adduserpropbased on object type - result_get requires fields — call
result_listfirst to discover available fields, then request only what you need - Short names resolve —
model_get("FDTD")works without the::model::prefix
Files
fdtd-mcp/
├── README.md
├── README_zh.md
├── LICENSE
├── pyproject.toml
├── install.py
├── fdtd_mcp/
│ ├── __init__.py
│ ├── discovery.py # auto-detect Lumerical installation
│ ├── dispatch.json # single source of truth for tool -> bridge handler
│ ├── bridge.py # JSON-RPC bridge (Lumerical Python 3.6.8)
│ ├── server.py # MCP server (system Python)
│ └── cheatsheet/
│ └── lumapi_ref.json # Lumerical API reference
└── tests/ # pytest suite (no Lumerical needed)
Requirements
- Python ≥ 3.10
- Lumerical FDTD (v202 or later)
mcp
Timeouts for long-running calls
run, sweep_run, execute, execute_file block until the engine finishes.
By default there is no timeout, so legitimate long simulations are never cut
short. If you want a bound (e.g. to stop a hung engine freezing the session),
set the FDTD_MCP_CALL_TIMEOUT env var in seconds, or pass timeout=<seconds>
to a single call. On expiry the bridge is killed and auto-restarted on the next
call — note this discards unsaved in-memory engine state, so save your project
before running long simulations.