lumerical-mcp 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lumerical_mcp-0.1.0/PKG-INFO +162 -0
- lumerical_mcp-0.1.0/README.md +136 -0
- lumerical_mcp-0.1.0/lumerical_mcp/__init__.py +1 -0
- lumerical_mcp-0.1.0/lumerical_mcp/config.py +56 -0
- lumerical_mcp-0.1.0/lumerical_mcp/server.py +311 -0
- lumerical_mcp-0.1.0/lumerical_mcp.egg-info/PKG-INFO +162 -0
- lumerical_mcp-0.1.0/lumerical_mcp.egg-info/SOURCES.txt +11 -0
- lumerical_mcp-0.1.0/lumerical_mcp.egg-info/dependency_links.txt +1 -0
- lumerical_mcp-0.1.0/lumerical_mcp.egg-info/entry_points.txt +2 -0
- lumerical_mcp-0.1.0/lumerical_mcp.egg-info/requires.txt +6 -0
- lumerical_mcp-0.1.0/lumerical_mcp.egg-info/top_level.txt +1 -0
- lumerical_mcp-0.1.0/pyproject.toml +47 -0
- lumerical_mcp-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lumerical-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MCP server for Lumerical FDTD automation
|
|
5
|
+
Author-email: Saku1234 <lxh875410020@gmail.com>
|
|
6
|
+
Maintainer-email: Saku1234 <lxh875410020@gmail.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Project-URL: Homepage, https://github.com/saku123hh/lumerical-mcp
|
|
9
|
+
Project-URL: Repository, https://github.com/saku123hh/lumerical-mcp
|
|
10
|
+
Project-URL: Issues, https://github.com/saku123hh/lumerical-mcp/issues
|
|
11
|
+
Keywords: mcp,lumerical,fdtd,photonics,simulation
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering :: Physics
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
Requires-Dist: mcp>=1.0.0
|
|
22
|
+
Requires-Dist: numpy
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: pytest; extra == "dev"
|
|
25
|
+
Requires-Dist: ruff; extra == "dev"
|
|
26
|
+
|
|
27
|
+
# Lumerical MCP Server
|
|
28
|
+
|
|
29
|
+
MCP (Model Context Protocol) server for controlling Lumerical FDTD simulations from LLMs.
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
cd lumerical-mcp
|
|
35
|
+
pip install -e .
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Configuration
|
|
39
|
+
|
|
40
|
+
### Required Environment Variable
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
# Windows (PowerShell)
|
|
44
|
+
$env:LUMERICAL_ROOT = "D:\Program Files\ANSYS Inc\v251\Lumerical"
|
|
45
|
+
|
|
46
|
+
# Windows (cmd)
|
|
47
|
+
set LUMERICAL_ROOT=D:\Program Files\ANSYS Inc\v251\Lumerical
|
|
48
|
+
|
|
49
|
+
# Linux/macOS
|
|
50
|
+
export LUMERICAL_ROOT=/opt/lumerical/v251
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Optional Environment Variables
|
|
54
|
+
|
|
55
|
+
| Variable | Default | Description |
|
|
56
|
+
|----------|---------|-------------|
|
|
57
|
+
| `MCP_HOST` | `127.0.0.1` | Server bind address |
|
|
58
|
+
| `MCP_PORT` | `8000` | Server port |
|
|
59
|
+
| `MCP_TRANSPORT` | `streamable-http` | Transport protocol (`streamable-http` or `stdio`) |
|
|
60
|
+
| `MCP_WORK_DIR` | Current directory | Working directory for relative paths |
|
|
61
|
+
| `MCP_LOG_LEVEL` | `INFO` | Log level (`DEBUG`, `INFO`, `WARNING`, `ERROR`) |
|
|
62
|
+
|
|
63
|
+
### Config File (Optional)
|
|
64
|
+
|
|
65
|
+
Create `~/.config/lumerical-mcp/config.json`:
|
|
66
|
+
|
|
67
|
+
```json
|
|
68
|
+
{
|
|
69
|
+
"MCP_HOST": "127.0.0.1",
|
|
70
|
+
"MCP_PORT": 8000,
|
|
71
|
+
"MCP_TRANSPORT": "streamable-http",
|
|
72
|
+
"MCP_WORK_DIR": "C:/Users/name/projects/lumerical",
|
|
73
|
+
"MCP_LOG_LEVEL": "INFO"
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Environment variables take precedence over config file.
|
|
78
|
+
|
|
79
|
+
## Running the Server
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
lumerical-mcp
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Or with custom settings:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
MCP_PORT=9000 MCP_TRANSPORT=stdio lumerical-mcp
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Available Tools
|
|
92
|
+
|
|
93
|
+
| Tool | Description |
|
|
94
|
+
|------|-------------|
|
|
95
|
+
| `hello` | Connectivity check |
|
|
96
|
+
| `start_fdtd` / `end_fdtd` | Start/stop FDTD API session |
|
|
97
|
+
| `open_projects` / `fdtd_load_project_tool` | Load `.fsp` project |
|
|
98
|
+
| `save_project` / `fdtd_save_project_tool` | Save project |
|
|
99
|
+
| `set_parameter` | Set object property |
|
|
100
|
+
| `run_simulation` / `fdtd_run_simulation_tool` | Run simulation |
|
|
101
|
+
| `get_transmission` | Get transmission spectrum from monitor |
|
|
102
|
+
| `fdtd_eval_script_tool` | Execute Lumerical script |
|
|
103
|
+
| `fdtd_eval_script_get_tool` | Execute script and return variable |
|
|
104
|
+
| `fdtd_run_script_file_tool` | Run `.lsf`/`.lsfx` script file |
|
|
105
|
+
| `fdtd_get_variable_tool` / `fdtd_put_variable_tool` | Read/write script variables |
|
|
106
|
+
| `fdtd_launch_gui_tool` | Launch Lumerical GUI |
|
|
107
|
+
| `fdtd_command_docs_tool` | Search command documentation |
|
|
108
|
+
|
|
109
|
+
## Client Usage
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
# Using the test client
|
|
113
|
+
python client_test.py
|
|
114
|
+
|
|
115
|
+
# Or with custom server address
|
|
116
|
+
MCP_HOST=192.168.1.100 MCP_PORT=9000 python client_test.py
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## LLM Client Configuration
|
|
120
|
+
|
|
121
|
+
### Claude Desktop (`claude_desktop_config.json`)
|
|
122
|
+
|
|
123
|
+
```json
|
|
124
|
+
{
|
|
125
|
+
"mcpServers": {
|
|
126
|
+
"lumerical": {
|
|
127
|
+
"command": "lumerical-mcp",
|
|
128
|
+
"env": {
|
|
129
|
+
"LUMERICAL_ROOT": "D:\\Program Files\\ANSYS Inc\\v251\\Lumerical"
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### VS Code / Continue / Other MCP Clients
|
|
137
|
+
|
|
138
|
+
```json
|
|
139
|
+
{
|
|
140
|
+
"mcpServers": {
|
|
141
|
+
"lumerical": {
|
|
142
|
+
"command": "lumerical-mcp",
|
|
143
|
+
"args": [],
|
|
144
|
+
"env": {
|
|
145
|
+
"LUMERICAL_ROOT": "/opt/lumerical/v251"
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Development
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
pip install -e .[dev]
|
|
156
|
+
ruff check .
|
|
157
|
+
pytest
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## License
|
|
161
|
+
|
|
162
|
+
MIT
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# Lumerical MCP Server
|
|
2
|
+
|
|
3
|
+
MCP (Model Context Protocol) server for controlling Lumerical FDTD simulations from LLMs.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
cd lumerical-mcp
|
|
9
|
+
pip install -e .
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Configuration
|
|
13
|
+
|
|
14
|
+
### Required Environment Variable
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
# Windows (PowerShell)
|
|
18
|
+
$env:LUMERICAL_ROOT = "D:\Program Files\ANSYS Inc\v251\Lumerical"
|
|
19
|
+
|
|
20
|
+
# Windows (cmd)
|
|
21
|
+
set LUMERICAL_ROOT=D:\Program Files\ANSYS Inc\v251\Lumerical
|
|
22
|
+
|
|
23
|
+
# Linux/macOS
|
|
24
|
+
export LUMERICAL_ROOT=/opt/lumerical/v251
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Optional Environment Variables
|
|
28
|
+
|
|
29
|
+
| Variable | Default | Description |
|
|
30
|
+
|----------|---------|-------------|
|
|
31
|
+
| `MCP_HOST` | `127.0.0.1` | Server bind address |
|
|
32
|
+
| `MCP_PORT` | `8000` | Server port |
|
|
33
|
+
| `MCP_TRANSPORT` | `streamable-http` | Transport protocol (`streamable-http` or `stdio`) |
|
|
34
|
+
| `MCP_WORK_DIR` | Current directory | Working directory for relative paths |
|
|
35
|
+
| `MCP_LOG_LEVEL` | `INFO` | Log level (`DEBUG`, `INFO`, `WARNING`, `ERROR`) |
|
|
36
|
+
|
|
37
|
+
### Config File (Optional)
|
|
38
|
+
|
|
39
|
+
Create `~/.config/lumerical-mcp/config.json`:
|
|
40
|
+
|
|
41
|
+
```json
|
|
42
|
+
{
|
|
43
|
+
"MCP_HOST": "127.0.0.1",
|
|
44
|
+
"MCP_PORT": 8000,
|
|
45
|
+
"MCP_TRANSPORT": "streamable-http",
|
|
46
|
+
"MCP_WORK_DIR": "C:/Users/name/projects/lumerical",
|
|
47
|
+
"MCP_LOG_LEVEL": "INFO"
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Environment variables take precedence over config file.
|
|
52
|
+
|
|
53
|
+
## Running the Server
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
lumerical-mcp
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Or with custom settings:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
MCP_PORT=9000 MCP_TRANSPORT=stdio lumerical-mcp
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Available Tools
|
|
66
|
+
|
|
67
|
+
| Tool | Description |
|
|
68
|
+
|------|-------------|
|
|
69
|
+
| `hello` | Connectivity check |
|
|
70
|
+
| `start_fdtd` / `end_fdtd` | Start/stop FDTD API session |
|
|
71
|
+
| `open_projects` / `fdtd_load_project_tool` | Load `.fsp` project |
|
|
72
|
+
| `save_project` / `fdtd_save_project_tool` | Save project |
|
|
73
|
+
| `set_parameter` | Set object property |
|
|
74
|
+
| `run_simulation` / `fdtd_run_simulation_tool` | Run simulation |
|
|
75
|
+
| `get_transmission` | Get transmission spectrum from monitor |
|
|
76
|
+
| `fdtd_eval_script_tool` | Execute Lumerical script |
|
|
77
|
+
| `fdtd_eval_script_get_tool` | Execute script and return variable |
|
|
78
|
+
| `fdtd_run_script_file_tool` | Run `.lsf`/`.lsfx` script file |
|
|
79
|
+
| `fdtd_get_variable_tool` / `fdtd_put_variable_tool` | Read/write script variables |
|
|
80
|
+
| `fdtd_launch_gui_tool` | Launch Lumerical GUI |
|
|
81
|
+
| `fdtd_command_docs_tool` | Search command documentation |
|
|
82
|
+
|
|
83
|
+
## Client Usage
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
# Using the test client
|
|
87
|
+
python client_test.py
|
|
88
|
+
|
|
89
|
+
# Or with custom server address
|
|
90
|
+
MCP_HOST=192.168.1.100 MCP_PORT=9000 python client_test.py
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## LLM Client Configuration
|
|
94
|
+
|
|
95
|
+
### Claude Desktop (`claude_desktop_config.json`)
|
|
96
|
+
|
|
97
|
+
```json
|
|
98
|
+
{
|
|
99
|
+
"mcpServers": {
|
|
100
|
+
"lumerical": {
|
|
101
|
+
"command": "lumerical-mcp",
|
|
102
|
+
"env": {
|
|
103
|
+
"LUMERICAL_ROOT": "D:\\Program Files\\ANSYS Inc\\v251\\Lumerical"
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### VS Code / Continue / Other MCP Clients
|
|
111
|
+
|
|
112
|
+
```json
|
|
113
|
+
{
|
|
114
|
+
"mcpServers": {
|
|
115
|
+
"lumerical": {
|
|
116
|
+
"command": "lumerical-mcp",
|
|
117
|
+
"args": [],
|
|
118
|
+
"env": {
|
|
119
|
+
"LUMERICAL_ROOT": "/opt/lumerical/v251"
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Development
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
pip install -e .[dev]
|
|
130
|
+
ruff check .
|
|
131
|
+
pytest
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## License
|
|
135
|
+
|
|
136
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class Config:
|
|
9
|
+
lumerical_root: Path
|
|
10
|
+
host: str = "127.0.0.1"
|
|
11
|
+
port: int = 8000
|
|
12
|
+
transport: str = "streamable-http"
|
|
13
|
+
work_dir: Path = field(default_factory=Path.cwd)
|
|
14
|
+
log_level: str = "INFO"
|
|
15
|
+
|
|
16
|
+
@property
|
|
17
|
+
def lumerical_api_python(self) -> Path:
|
|
18
|
+
return self.lumerical_root / "api" / "python"
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def lumerical_bin(self) -> Path:
|
|
22
|
+
return self.lumerical_root / "bin"
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def load(cls) -> "Config":
|
|
26
|
+
lumerical_root = os.environ.get("LUMERICAL_ROOT")
|
|
27
|
+
if not lumerical_root:
|
|
28
|
+
raise RuntimeError(
|
|
29
|
+
"LUMERICAL_ROOT environment variable not set. "
|
|
30
|
+
"Point it to your Lumerical installation directory, e.g.: "
|
|
31
|
+
" Windows: D:\\Program Files\\ANSYS Inc\\v251\\Lumerical "
|
|
32
|
+
" Linux: /opt/lumerical/v251"
|
|
33
|
+
)
|
|
34
|
+
root = Path(lumerical_root).expanduser().resolve()
|
|
35
|
+
if not root.exists():
|
|
36
|
+
raise RuntimeError(f"LUMERICAL_ROOT path does not exist: {root}")
|
|
37
|
+
|
|
38
|
+
config_file = Path.home() / ".config" / "lumerical-mcp" / "config.json"
|
|
39
|
+
file_cfg = {}
|
|
40
|
+
if config_file.exists():
|
|
41
|
+
try:
|
|
42
|
+
file_cfg = json.loads(config_file.read_text(encoding="utf-8"))
|
|
43
|
+
except Exception:
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
def get(key: str, default):
|
|
47
|
+
return os.environ.get(key) or file_cfg.get(key) or default
|
|
48
|
+
|
|
49
|
+
return cls(
|
|
50
|
+
lumerical_root=root,
|
|
51
|
+
host=get("MCP_HOST", "127.0.0.1"),
|
|
52
|
+
port=int(get("MCP_PORT", "8000")),
|
|
53
|
+
transport=get("MCP_TRANSPORT", "streamable-http"),
|
|
54
|
+
work_dir=Path(get("MCP_WORK_DIR", str(Path.cwd()))).expanduser().resolve(),
|
|
55
|
+
log_level=get("MCP_LOG_LEVEL", "INFO"),
|
|
56
|
+
)
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import time
|
|
4
|
+
import json
|
|
5
|
+
import subprocess
|
|
6
|
+
import logging
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Optional
|
|
9
|
+
|
|
10
|
+
from .config import Config
|
|
11
|
+
|
|
12
|
+
cfg = Config.load()
|
|
13
|
+
|
|
14
|
+
logging.basicConfig(level=cfg.log_level, format="%(asctime)s %(levelname)s %(name)s: %(message)s")
|
|
15
|
+
logger = logging.getLogger("lumerical-mcp")
|
|
16
|
+
|
|
17
|
+
for _directory in (cfg.lumerical_api_python, cfg.lumerical_bin):
|
|
18
|
+
_directory_str = str(_directory)
|
|
19
|
+
if _directory.exists():
|
|
20
|
+
if _directory_str not in sys.path:
|
|
21
|
+
sys.path.append(_directory_str)
|
|
22
|
+
if hasattr(os, "add_dll_directory"):
|
|
23
|
+
try:
|
|
24
|
+
os.add_dll_directory(_directory_str)
|
|
25
|
+
except OSError:
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
import lumapi
|
|
29
|
+
|
|
30
|
+
from mcp.server.mcpserver import MCPServer
|
|
31
|
+
|
|
32
|
+
mcp = MCPServer("lumerical")
|
|
33
|
+
|
|
34
|
+
fdtd = None
|
|
35
|
+
_gui_jobs: dict[str, dict[str, Any]] = {}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _ensure_fdtd(hide: bool = False):
|
|
39
|
+
global fdtd
|
|
40
|
+
if fdtd is None:
|
|
41
|
+
fdtd = lumapi.FDTD(hide=hide)
|
|
42
|
+
return fdtd
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _jsonable(value: Any, max_array_items: int = 2000) -> Any:
|
|
46
|
+
try:
|
|
47
|
+
import numpy as np
|
|
48
|
+
except Exception:
|
|
49
|
+
np = None
|
|
50
|
+
|
|
51
|
+
if value is None or isinstance(value, (bool, int, float, str)):
|
|
52
|
+
return value
|
|
53
|
+
|
|
54
|
+
if isinstance(value, bytes):
|
|
55
|
+
return value.decode("utf-8", errors="replace")
|
|
56
|
+
|
|
57
|
+
if isinstance(value, complex):
|
|
58
|
+
return {"real": value.real, "imag": value.imag}
|
|
59
|
+
|
|
60
|
+
if np is not None:
|
|
61
|
+
if isinstance(value, np.generic):
|
|
62
|
+
return _jsonable(value.item(), max_array_items)
|
|
63
|
+
if isinstance(value, np.ndarray):
|
|
64
|
+
size = int(value.size)
|
|
65
|
+
if size <= max_array_items:
|
|
66
|
+
return {
|
|
67
|
+
"type": "ndarray",
|
|
68
|
+
"shape": list(value.shape),
|
|
69
|
+
"dtype": str(value.dtype),
|
|
70
|
+
"data": _jsonable(value.tolist(), max_array_items),
|
|
71
|
+
}
|
|
72
|
+
flat = value.ravel()
|
|
73
|
+
return {
|
|
74
|
+
"type": "ndarray",
|
|
75
|
+
"shape": list(value.shape),
|
|
76
|
+
"dtype": str(value.dtype),
|
|
77
|
+
"size": size,
|
|
78
|
+
"sample": _jsonable(flat[:20].tolist(), max_array_items),
|
|
79
|
+
"truncated": True,
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if isinstance(value, dict):
|
|
83
|
+
return {str(k): _jsonable(v, max_array_items) for k, v in value.items()}
|
|
84
|
+
|
|
85
|
+
if isinstance(value, (list, tuple)):
|
|
86
|
+
return [_jsonable(item, max_array_items) for item in value]
|
|
87
|
+
|
|
88
|
+
return repr(value)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@mcp.tool()
|
|
92
|
+
def hello(name: str) -> str:
|
|
93
|
+
return f"Hello {name}, MCP is working."
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@mcp.tool()
|
|
97
|
+
def start_fdtd() -> str:
|
|
98
|
+
global fdtd
|
|
99
|
+
if fdtd is not None:
|
|
100
|
+
return "FDTD is already running."
|
|
101
|
+
fdtd = lumapi.FDTD()
|
|
102
|
+
return "Lumerical FDTD started successfully."
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@mcp.tool()
|
|
106
|
+
def end_fdtd() -> str:
|
|
107
|
+
global fdtd
|
|
108
|
+
if fdtd is None:
|
|
109
|
+
return "No FDTD session is running."
|
|
110
|
+
fdtd.close()
|
|
111
|
+
fdtd = None
|
|
112
|
+
return "Lumerical FDTD closed."
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@mcp.tool()
|
|
116
|
+
def open_projects(path: str) -> str:
|
|
117
|
+
session = _ensure_fdtd()
|
|
118
|
+
session.load(str(cfg.work_dir / path) if not Path(path).is_absolute() else path)
|
|
119
|
+
return f"Project opened: {path}"
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@mcp.tool()
|
|
123
|
+
def set_parameter(object_name: str, property_name: str, value: float) -> str:
|
|
124
|
+
if fdtd is None:
|
|
125
|
+
return "FDTD is not running."
|
|
126
|
+
fdtd.switchtolayout()
|
|
127
|
+
fdtd.setnamed(object_name, property_name, value)
|
|
128
|
+
return f"{object_name}: {property_name} = {value}"
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@mcp.tool()
|
|
132
|
+
def run_simulation() -> str:
|
|
133
|
+
if fdtd is None:
|
|
134
|
+
return "FDTD is not running."
|
|
135
|
+
fdtd.run()
|
|
136
|
+
return "Simulation finished."
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@mcp.tool()
|
|
140
|
+
def save_project(path: str) -> str:
|
|
141
|
+
if fdtd is None:
|
|
142
|
+
return "FDTD is not running."
|
|
143
|
+
fdtd.save(str(cfg.work_dir / path) if not Path(path).is_absolute() else path)
|
|
144
|
+
return f"Project saved: {path}"
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@mcp.tool()
|
|
148
|
+
def get_transmission(monitor_name: str) -> dict:
|
|
149
|
+
if fdtd is None:
|
|
150
|
+
return {"error": "FDTD is not running."}
|
|
151
|
+
result = fdtd.getresult(monitor_name, "T")
|
|
152
|
+
wavelength = result["lambda"].flatten().flip()
|
|
153
|
+
transmission = result["T"].flatten().flip()
|
|
154
|
+
return {
|
|
155
|
+
"wavelength_nm": (wavelength * 1e9).tolist(),
|
|
156
|
+
"T": transmission.tolist(),
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@mcp.tool()
|
|
161
|
+
def fdtd_eval_script_tool(code: str) -> dict:
|
|
162
|
+
session = _ensure_fdtd()
|
|
163
|
+
session.eval(code)
|
|
164
|
+
return {"ok": True, "executed": True}
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@mcp.tool()
|
|
168
|
+
def fdtd_eval_script_get_tool(code: str, return_var: str, max_array_items: int = 2000) -> dict:
|
|
169
|
+
session = _ensure_fdtd()
|
|
170
|
+
session.eval(code)
|
|
171
|
+
value = session.getv(return_var)
|
|
172
|
+
return {
|
|
173
|
+
"ok": True,
|
|
174
|
+
"variable": return_var,
|
|
175
|
+
"value": _jsonable(value, max_array_items),
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@mcp.tool()
|
|
180
|
+
def fdtd_run_script_file_tool(script_path: str) -> dict:
|
|
181
|
+
session = _ensure_fdtd()
|
|
182
|
+
path = str(cfg.work_dir / script_path) if not Path(script_path).is_absolute() else script_path
|
|
183
|
+
if path.lower().endswith(".lsfx"):
|
|
184
|
+
session.eval(path[:-5] + ";")
|
|
185
|
+
else:
|
|
186
|
+
session.feval(path)
|
|
187
|
+
return {"ok": True, "script_path": path}
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
@mcp.tool()
|
|
191
|
+
def fdtd_get_variable_tool(name: str, max_array_items: int = 2000) -> dict:
|
|
192
|
+
session = _ensure_fdtd()
|
|
193
|
+
value = session.getv(name)
|
|
194
|
+
return {
|
|
195
|
+
"ok": True,
|
|
196
|
+
"variable": name,
|
|
197
|
+
"value": _jsonable(value, max_array_items),
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
@mcp.tool()
|
|
202
|
+
def fdtd_put_variable_tool(name: str, value: Any) -> dict:
|
|
203
|
+
session = _ensure_fdtd()
|
|
204
|
+
session.putv(name, value)
|
|
205
|
+
return {"ok": True, "variable": name}
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
@mcp.tool()
|
|
209
|
+
def fdtd_load_project_tool(project_path: str) -> dict:
|
|
210
|
+
session = _ensure_fdtd()
|
|
211
|
+
path = str(cfg.work_dir / project_path) if not Path(project_path).is_absolute() else project_path
|
|
212
|
+
session.load(path)
|
|
213
|
+
return {"ok": True, "project_path": path}
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
@mcp.tool()
|
|
217
|
+
def fdtd_save_project_tool(project_path: Optional[str] = None) -> dict:
|
|
218
|
+
session = _ensure_fdtd()
|
|
219
|
+
if project_path:
|
|
220
|
+
path = str(cfg.work_dir / project_path) if not Path(project_path).is_absolute() else project_path
|
|
221
|
+
session.save(path)
|
|
222
|
+
else:
|
|
223
|
+
session.save()
|
|
224
|
+
path = None
|
|
225
|
+
return {"ok": True, "project_path": path}
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
@mcp.tool()
|
|
229
|
+
def fdtd_run_simulation_tool() -> dict:
|
|
230
|
+
session = _ensure_fdtd()
|
|
231
|
+
started_at = time.time()
|
|
232
|
+
session.eval("run;")
|
|
233
|
+
return {"ok": True, "elapsed_seconds": time.time() - started_at}
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
@mcp.tool()
|
|
237
|
+
def fdtd_launch_gui_tool(target: str = "launcher", project_path: Optional[str] = None) -> dict:
|
|
238
|
+
if target.lower() in {"fdtd", "fdtd-solutions", "solutions"}:
|
|
239
|
+
exe = cfg.lumerical_bin / "fdtd-solutions.exe"
|
|
240
|
+
else:
|
|
241
|
+
exe = cfg.lumerical_bin / "launcher.exe"
|
|
242
|
+
|
|
243
|
+
if not exe.exists():
|
|
244
|
+
return {"ok": False, "error": f"Executable not found: {exe}"}
|
|
245
|
+
|
|
246
|
+
args = [str(exe)]
|
|
247
|
+
if project_path:
|
|
248
|
+
path = str(cfg.work_dir / project_path) if not Path(project_path).is_absolute() else project_path
|
|
249
|
+
args.append(path)
|
|
250
|
+
|
|
251
|
+
try:
|
|
252
|
+
process = subprocess.Popen(
|
|
253
|
+
args,
|
|
254
|
+
cwd=str(cfg.lumerical_bin) if cfg.lumerical_bin.exists() else None,
|
|
255
|
+
stdout=subprocess.DEVNULL,
|
|
256
|
+
stderr=subprocess.DEVNULL,
|
|
257
|
+
stdin=subprocess.DEVNULL,
|
|
258
|
+
)
|
|
259
|
+
except OSError as exc:
|
|
260
|
+
return {"ok": False, "error": f"{type(exc).__name__}: {exc}"}
|
|
261
|
+
|
|
262
|
+
job_id = f"fdtd-gui-{int(time.time())}-{process.pid}"
|
|
263
|
+
_gui_jobs[job_id] = {
|
|
264
|
+
"job_id": job_id,
|
|
265
|
+
"pid": process.pid,
|
|
266
|
+
"target": target,
|
|
267
|
+
"args": args,
|
|
268
|
+
"started_at": time.time(),
|
|
269
|
+
}
|
|
270
|
+
return {"ok": True, **_gui_jobs[job_id]}
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
@mcp.tool()
|
|
274
|
+
def fdtd_command_docs_tool(query: str = "", limit: int = 50) -> dict:
|
|
275
|
+
docs_path = cfg.lumerical_api_python / "docs.json"
|
|
276
|
+
if not docs_path.exists():
|
|
277
|
+
return {"ok": False, "error": f"docs.json not found: {docs_path}"}
|
|
278
|
+
|
|
279
|
+
docs = json.loads(docs_path.read_text(encoding="utf-8", errors="replace"))
|
|
280
|
+
needle = query.lower() if query else None
|
|
281
|
+
items = []
|
|
282
|
+
|
|
283
|
+
for name, entry in docs.items():
|
|
284
|
+
if isinstance(entry, dict):
|
|
285
|
+
text = entry.get("text", "")
|
|
286
|
+
link = entry.get("link", "")
|
|
287
|
+
else:
|
|
288
|
+
text = str(entry)
|
|
289
|
+
link = ""
|
|
290
|
+
|
|
291
|
+
if needle:
|
|
292
|
+
haystack = f"{name}\n{text}".lower()
|
|
293
|
+
if needle not in haystack:
|
|
294
|
+
continue
|
|
295
|
+
|
|
296
|
+
items.append({"name": name, "text": text, "link": link})
|
|
297
|
+
if len(items) >= limit:
|
|
298
|
+
break
|
|
299
|
+
|
|
300
|
+
return {"ok": True, "count": len(items), "commands": items}
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def main():
|
|
304
|
+
logger.info(f"Starting Lumerical MCP server on {cfg.host}:{cfg.port} ({cfg.transport})")
|
|
305
|
+
logger.info(f"Lumerical root: {cfg.lumerical_root}")
|
|
306
|
+
logger.info(f"Work directory: {cfg.work_dir}")
|
|
307
|
+
mcp.run(transport=cfg.transport, host=cfg.host, port=cfg.port)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
if __name__ == "__main__":
|
|
311
|
+
main()
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lumerical-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MCP server for Lumerical FDTD automation
|
|
5
|
+
Author-email: Saku1234 <lxh875410020@gmail.com>
|
|
6
|
+
Maintainer-email: Saku1234 <lxh875410020@gmail.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Project-URL: Homepage, https://github.com/saku123hh/lumerical-mcp
|
|
9
|
+
Project-URL: Repository, https://github.com/saku123hh/lumerical-mcp
|
|
10
|
+
Project-URL: Issues, https://github.com/saku123hh/lumerical-mcp/issues
|
|
11
|
+
Keywords: mcp,lumerical,fdtd,photonics,simulation
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering :: Physics
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
Requires-Dist: mcp>=1.0.0
|
|
22
|
+
Requires-Dist: numpy
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: pytest; extra == "dev"
|
|
25
|
+
Requires-Dist: ruff; extra == "dev"
|
|
26
|
+
|
|
27
|
+
# Lumerical MCP Server
|
|
28
|
+
|
|
29
|
+
MCP (Model Context Protocol) server for controlling Lumerical FDTD simulations from LLMs.
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
cd lumerical-mcp
|
|
35
|
+
pip install -e .
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Configuration
|
|
39
|
+
|
|
40
|
+
### Required Environment Variable
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
# Windows (PowerShell)
|
|
44
|
+
$env:LUMERICAL_ROOT = "D:\Program Files\ANSYS Inc\v251\Lumerical"
|
|
45
|
+
|
|
46
|
+
# Windows (cmd)
|
|
47
|
+
set LUMERICAL_ROOT=D:\Program Files\ANSYS Inc\v251\Lumerical
|
|
48
|
+
|
|
49
|
+
# Linux/macOS
|
|
50
|
+
export LUMERICAL_ROOT=/opt/lumerical/v251
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Optional Environment Variables
|
|
54
|
+
|
|
55
|
+
| Variable | Default | Description |
|
|
56
|
+
|----------|---------|-------------|
|
|
57
|
+
| `MCP_HOST` | `127.0.0.1` | Server bind address |
|
|
58
|
+
| `MCP_PORT` | `8000` | Server port |
|
|
59
|
+
| `MCP_TRANSPORT` | `streamable-http` | Transport protocol (`streamable-http` or `stdio`) |
|
|
60
|
+
| `MCP_WORK_DIR` | Current directory | Working directory for relative paths |
|
|
61
|
+
| `MCP_LOG_LEVEL` | `INFO` | Log level (`DEBUG`, `INFO`, `WARNING`, `ERROR`) |
|
|
62
|
+
|
|
63
|
+
### Config File (Optional)
|
|
64
|
+
|
|
65
|
+
Create `~/.config/lumerical-mcp/config.json`:
|
|
66
|
+
|
|
67
|
+
```json
|
|
68
|
+
{
|
|
69
|
+
"MCP_HOST": "127.0.0.1",
|
|
70
|
+
"MCP_PORT": 8000,
|
|
71
|
+
"MCP_TRANSPORT": "streamable-http",
|
|
72
|
+
"MCP_WORK_DIR": "C:/Users/name/projects/lumerical",
|
|
73
|
+
"MCP_LOG_LEVEL": "INFO"
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Environment variables take precedence over config file.
|
|
78
|
+
|
|
79
|
+
## Running the Server
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
lumerical-mcp
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Or with custom settings:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
MCP_PORT=9000 MCP_TRANSPORT=stdio lumerical-mcp
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Available Tools
|
|
92
|
+
|
|
93
|
+
| Tool | Description |
|
|
94
|
+
|------|-------------|
|
|
95
|
+
| `hello` | Connectivity check |
|
|
96
|
+
| `start_fdtd` / `end_fdtd` | Start/stop FDTD API session |
|
|
97
|
+
| `open_projects` / `fdtd_load_project_tool` | Load `.fsp` project |
|
|
98
|
+
| `save_project` / `fdtd_save_project_tool` | Save project |
|
|
99
|
+
| `set_parameter` | Set object property |
|
|
100
|
+
| `run_simulation` / `fdtd_run_simulation_tool` | Run simulation |
|
|
101
|
+
| `get_transmission` | Get transmission spectrum from monitor |
|
|
102
|
+
| `fdtd_eval_script_tool` | Execute Lumerical script |
|
|
103
|
+
| `fdtd_eval_script_get_tool` | Execute script and return variable |
|
|
104
|
+
| `fdtd_run_script_file_tool` | Run `.lsf`/`.lsfx` script file |
|
|
105
|
+
| `fdtd_get_variable_tool` / `fdtd_put_variable_tool` | Read/write script variables |
|
|
106
|
+
| `fdtd_launch_gui_tool` | Launch Lumerical GUI |
|
|
107
|
+
| `fdtd_command_docs_tool` | Search command documentation |
|
|
108
|
+
|
|
109
|
+
## Client Usage
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
# Using the test client
|
|
113
|
+
python client_test.py
|
|
114
|
+
|
|
115
|
+
# Or with custom server address
|
|
116
|
+
MCP_HOST=192.168.1.100 MCP_PORT=9000 python client_test.py
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## LLM Client Configuration
|
|
120
|
+
|
|
121
|
+
### Claude Desktop (`claude_desktop_config.json`)
|
|
122
|
+
|
|
123
|
+
```json
|
|
124
|
+
{
|
|
125
|
+
"mcpServers": {
|
|
126
|
+
"lumerical": {
|
|
127
|
+
"command": "lumerical-mcp",
|
|
128
|
+
"env": {
|
|
129
|
+
"LUMERICAL_ROOT": "D:\\Program Files\\ANSYS Inc\\v251\\Lumerical"
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### VS Code / Continue / Other MCP Clients
|
|
137
|
+
|
|
138
|
+
```json
|
|
139
|
+
{
|
|
140
|
+
"mcpServers": {
|
|
141
|
+
"lumerical": {
|
|
142
|
+
"command": "lumerical-mcp",
|
|
143
|
+
"args": [],
|
|
144
|
+
"env": {
|
|
145
|
+
"LUMERICAL_ROOT": "/opt/lumerical/v251"
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Development
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
pip install -e .[dev]
|
|
156
|
+
ruff check .
|
|
157
|
+
pytest
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## License
|
|
161
|
+
|
|
162
|
+
MIT
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
lumerical_mcp/__init__.py
|
|
4
|
+
lumerical_mcp/config.py
|
|
5
|
+
lumerical_mcp/server.py
|
|
6
|
+
lumerical_mcp.egg-info/PKG-INFO
|
|
7
|
+
lumerical_mcp.egg-info/SOURCES.txt
|
|
8
|
+
lumerical_mcp.egg-info/dependency_links.txt
|
|
9
|
+
lumerical_mcp.egg-info/entry_points.txt
|
|
10
|
+
lumerical_mcp.egg-info/requires.txt
|
|
11
|
+
lumerical_mcp.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
lumerical_mcp
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# D:\Files\gitProjects\lumerical-mcp\pyproject.toml
|
|
2
|
+
[build-system]
|
|
3
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
4
|
+
build-backend = "setuptools.build_meta"
|
|
5
|
+
|
|
6
|
+
[project]
|
|
7
|
+
name = "lumerical-mcp"
|
|
8
|
+
version = "0.1.0"
|
|
9
|
+
description = "MCP server for Lumerical FDTD automation"
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [{name = "Saku1234", email = "lxh875410020@gmail.com"}]
|
|
13
|
+
maintainers = [{name = "Saku1234", email = "lxh875410020@gmail.com"}]
|
|
14
|
+
keywords = ["mcp", "lumerical", "fdtd", "photonics", "simulation"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Intended Audience :: Science/Research",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.10",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Topic :: Scientific/Engineering :: Physics",
|
|
23
|
+
]
|
|
24
|
+
requires-python = ">=3.10"
|
|
25
|
+
dependencies = [
|
|
26
|
+
"mcp>=1.0.0",
|
|
27
|
+
"numpy",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Homepage = "https://github.com/saku123hh/lumerical-mcp"
|
|
32
|
+
Repository = "https://github.com/saku123hh/lumerical-mcp"
|
|
33
|
+
Issues = "https://github.com/saku123hh/lumerical-mcp/issues"
|
|
34
|
+
|
|
35
|
+
[project.optional-dependencies]
|
|
36
|
+
dev = ["pytest", "ruff"]
|
|
37
|
+
|
|
38
|
+
[project.scripts]
|
|
39
|
+
"lumerical-mcp" = "lumerical_mcp.server:main"
|
|
40
|
+
|
|
41
|
+
[tool.setuptools.packages.find]
|
|
42
|
+
where = ["."]
|
|
43
|
+
include = ["lumerical_mcp*"]
|
|
44
|
+
|
|
45
|
+
[tool.ruff]
|
|
46
|
+
line-length = 100
|
|
47
|
+
target-version = "py310"
|