Content
# Tool List
Let AI (MCP client) programmatically control circuits in [DS-VLAB v2.0 multi-thought experimental group principle virtual laboratory](../../index.html): load circuits, power on, apply input, single-step/continuous operation, read pins and storage, and run truth tables in batches to achieve automated testing.
## Architecture
```
AI Client ─stdio─► dsvlab-mcp(This service) ─Playwright─► Edge/Chromium(Load index.html)
│ page.evaluate()
▼
window.mycircuit / cDispatch
/ recovercircuit()(Original system global)
```
- **Do not modify any source code of the original project**(18 components, dispatch, circuitdiagram, fileoperation remain unchanged).
- All new files are concentrated in `mcp/dsvlab-mcp/`.
- Reuse the original project `tools/circuit-validator-core.js`(UMD) for loading verification.
- Default to use the system's built-in **Edge**(`channel: 'msedge'`), **no need to download Chromium**.
## Installation
```bash
cd mcp/dsvlab-mcp
npm install
npm run build # tsc compile to dist/
# Optional: run integration test (will load example_bus_demo.txt with Edge headless)
npm test
```
> No need for `npx playwright install` — default to use system Edge. If you want to use Playwright's built-in Chromium, set the environment variable `DSVLAB_CHANNEL=chromium` and then execute `npx playwright install chromium`.
## Configuration (MCP Client)
### ZCode / Claude Desktop
Merge the contents of `files/config.sample.json` into the client's MCP configuration (adjust according to the actual path):
```json
{
"mcpServers": {
"dsvlab": {
"command": "node",
"args": ["C:/.../dsvlab2.0/mcp/dsvlab-mcp/dist/index.js"],
"env": {
"DSVLAB_INDEX": "C:/.../dsvlab2.0/index.html",
"DSVLAB_CHANNEL": "msedge",
"DSVLAB_HEADLESS": "0"
}
}
}
}
```
### Environment Variables
| Variable | Default | Description |
|---|---|---|
| `DSVLAB_INDEX` | `<project root>/index.html` | Absolute path of index.html |
| `DSVLAB_CHANNEL` | `msedge` | Browser channel: `msedge` / `chrome` / `chromium` |
| `DSVLAB_HEADLESS` | `0`(visible) | `1` = headless (no popup, suitable for CI) |
> The browser is **lazy started**: the MCP service does not pop up immediately after connecting, but only starts when the tool is called for the first time.
## Tool List (25 in total)
### Circuit
| Tool | Description |
|---|---|
| `load_circuit` | Load circuit txt (default to verify first, reject loading if there are errors) |
| `clear_circuit` | Clear the canvas |
### Draw Circuit (auxiliary generation, cooperate with docs/AI draw circuit manual.md process)
| Tool | Description |
|---|---|
| `list_component_catalog` | List all 23 components: name/type/pin count |
| `get_component_pins` | Check component pin table (must be checked when drawing circuit, pin number prohibited from guessing) |
| `build_circuit` | Generate legitimate txt from structured description (with verification), can be directly loaded by load_circuit |
### Power
| Tool | Description |
|---|---|
| `power_on` / `power_off` / `reset` | Turn on/off/reset power |
### Simulation
| Tool | Description |
|---|---|
| `step` | Single step: run an empty ready queue once (one stable propagation) |
| `run` | Continuous operation: start ContinuousPulse to run for N milliseconds and then stop |
| `stop` | Stop all continuous pulses (clear timer) |
| `press_switch` | Toggle switch |
| `set_switch` | Set switch to 0/1 (handle initial high resistance, automatically determine if flipping is needed) |
| `trigger_pulse` | Trigger single pulse (must be powered on first) |
| `apply_input` | Apply 0/1 to specified pin and propagate |
| `set_pulse_width` | Set clock cycle (ms, default 800, test suggestion 50) |
### Read
| Tool | Description |
|---|---|
| `list_components` | Overview of all components |
| `get_component` | All pins of the component (pinName/pinFunction/pinValue) |
| `read_pin` / `read_all_pins` | Read pin value (0 low / 1 high / 2 high resistance) |
| `read_memory` / `write_memory` | Read/write RAM6116/EPROM memory (runtime, not saved with txt) |
| `snapshot` | Full circuit snapshot of all pins |
### Batch Test
| Tool | Description |
|---|---|
| `run_truth_table` | Run truth table in batches (automatically traverse 2^n or custom cases) |
| `assert_pins` | Assert multiple pin values at once, return pass/fail |
## Usage Example (AI perspective)
```
1. load_circuit(text=<circuit txt full text>) → Load and verify
2. power_on()
3. set_switch(id="CP8", value=0) → Enable 74LS245
4. for each input combination: set_switch + step + read_pin
or directly: run_truth_table(switch_ids=["CP0",...], outputs=[{id,pin}])
5. assert_pins(expected=[{id,pin,value},...])
```
## Key Technical Points
1. **Bridge layer**(`src/inject.ts`): inject a `window.__bridge` script into the page, encapsulate and centralize calls to the original system's `cDispatch`/`mycircuit`/`recovercircuit`, and return serializable JSON through `page.evaluate`. Reduce cross-process round trips.
2. **Call mapping**(all confirmed by checking the source code):
- Load: `recovercircuit('demo', mycircuit, txt)`(fileoperation.js:136)
- Power on/off: `cDispatch.powerOn()/powerOff()`(dispatch.js:93/99)
- Single step: `cDispatch.runCircuit()`(dispatch.js:75)
- Trigger source device: `cDispatch.sourceTrigger(c|id)`(dispatch.js:86)
- Read pin: `comp.pinValue[pinNo]`(0 low / 1 high / 2 high resistance)
3. **pinValue three states**: 0 low / 1 high / 2 high resistance undrive, assertion needs to distinguish.
4. **Switch initial value pit**: the original system's `new Array([0])` produces `[[0]]`, `pinValue[0]` is actually an array `[0]`, `[0]==0` holds but `[0]===0` does not hold. Therefore, `setSwitch` uses loose comparison `!=`, consistent with the original system's semantics of `==`.
5. **Memory limitation**: RAM6116/EPROM's memory is hard-coded in the source code (`js/component/ram6116.js`, etc.), not saved with circuit txt; runtime `component.memory` can be read and written, but **refreshing the page will reset** to the source code value.
6. **Asynchronous pulse**: `ContinuousPulse`/`SinglePulse` uses `setTimeout` self-driving (default 800ms); `run(duration)` uses real waiting, `stop()` clears the timer. Test with `set_pulse_width(50)` acceleration.
7. **MCP uses stdio**: disable `console.log`(destroy protocol flow), use `console.error`.
8. **TS + zod + MCP SDK's TS2589**: zod `ZodRawShape` and SDK generic combination will trigger "Type instantiation excessively deep". This project uniformly uses `server.registerTool<any, any>` + handler `(args:any, _extra:any): Promise<any>` to avoid.
## Debugging
```bash
# MCP Inspector: view all tools and manually call in the browser
npx @modelcontextprotocol/inspector node dist/index.js
# Run integration test
npm test
```
## Known Limitations
- Do not modify the original project source code, so RAM/EPROM memory initial value still needs to be modified in `js/component/*.js`(this tool only modifies `component.memory` at runtime, refreshing will lose).
- `run(duration)` is based on real clock waiting, large-scale timing test is slower; use `step`/`run_truth_table` for combinational logic.
- Single step without GUI button, `step` = `cDispatch.runCircuit()`(run empty ready queue).
## Integration Test Results
`tests/integration.test.ts` uses `files/examples/example_bus_demo.txt`(8 switches → 74LS245 → 8 LEDs) for end-to-end verification:
- ✅ Load 18 components
- ✅ `-E=0` enables A→B transmission, 8 LEDs all light up
- ✅ `-E=1` disables B side high resistance, LEDs do not light up