Skip to content
Start free evaluation

Simulator MCP server

The SECS/GEM simulator can run as an MCP (Model Context Protocol) server. AI tools such as Claude Code or Cursor, or your own scripts, call its MCP tools to create simulator sessions, establish communication and exercise GEM capabilities such as reports, alarms, commands and process programs.

How you start it What it controls Tools
pdcomm-simulator-mcp --stdio Sessions owned by this process. No window, no Qt. 169
pdcomm-simulator --mcp-stdio The sessions and window of this GUI process 175
pdcomm-simulator --mcp-local <descriptor> plus pdcomm-simulator-mcp-attach --descriptor <descriptor> The same GUI, already running 175

The two GUI modes add six simulator.ui.* tools that drive the window. Separate processes never share sessions: if you start pdcomm-simulator-mcp on its own, it can’t see the sessions of a GUI you already opened.

An MCP client starts the server as a child process and talks to it over standard input and output. Most clients use a JSON configuration like this one; replace the path with your installation folder.

{
"mcpServers": {
"pdcomm-simulator": {
"command": "C:\\pdcomm-tutorial\\simulator\\pdcomm-simulator-mcp.exe",
"args": ["--stdio"]
}
}
}

To let the client drive the GUI instead, set command to the GUI executable (pdcomm-simulator, or pdcomm-simulator.app/Contents/MacOS/pdcomm-simulator on macOS) and args to ["--mcp-stdio"]. The client then opens the GUI window, so you need a desktop session.

Option Description
--stdio (--mcp-stdio for the GUI) Serve MCP over standard input and output. Pass it exactly once.
--allow-endpoint <IPv4:port> Allow a non-loopback address as a session address. Repeat up to 32 times. 0.0.0.0 isn’t accepted.
--request-deadline-ms <1-600000> The time limit for one request. Defaults to 10000.

By default, sessions can only use loopback addresses such as 127.0.0.1. To connect to equipment or a host on another computer, allow its address with --allow-endpoint.

Code Meaning
0 The client closed standard input.
1 An I/O error, such as a failed write to standard output
2 Invalid command-line arguments
130 Stopped by SIGINT or SIGTERM (Ctrl+C, Ctrl+Break or closing the console on Windows)

The server supports MCP protocol version 2025-11-25 (the usual initialize handshake) and 2026-07-28 (_meta on every request). Most clients use initialize:

{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"docs-walkthrough","version":"1.0"}}}
{"id":1,"jsonrpc":"2.0","result":{"capabilities":{"resources":{"listChanged":false,"subscribe":false},"tools":{"listChanged":false}},"instructions":"Discover available simulator operations before calling tools.","protocolVersion":"2025-11-25","serverInfo":{"name":"pdcomm-simulator-mcp","version":"0.1.0"}}}

Then send the notifications/initialized notification and call tools/list. Read tool names and input schemas from that list; don’t hard-code the number of tools in your client.

Group Tools What they do
simulator.capabilities 1 Supported capabilities and limits
simulator.session 4 Create, list, get and destroy sessions
simulator.lifecycle 6 Start, stop, Select, Deselect, establish communication, poll
simulator.status_data 5 List variables, change equipment values, query names and values from the host
simulator.reports_events 9 Report and event catalogs, emit events, inspect and acknowledge received events, remote report configuration
simulator.alarms 9 Alarm catalogs, observe, publish, enable, list and acknowledge
simulator.commands 8 Remote command catalogs, requests, equipment decisions and results
simulator.programs 53 Unformatted and formatted process programs, verification, selection, execution and capability catalogs
simulator.spooling 17 Spooling configuration, capture, host controls and delivery proofs
simulator.constants 8 Equipment constant catalogs, queries, changes and decisions
simulator.data_variables 5 Data variable catalogs and caches, events with samples
simulator.directories 5 Static event directories, data and event directory requests
simulator.report_queries 5 Report queries and replies
simulator.profiles 5 Equipment profiles
simulator.scenarios 7 Scenario definitions and runs
simulator.trace 4 Read, export, inspect and clear the trace
simulator.project 2 Export and replace project definitions
simulator.diagnostics 1 Session diagnostics
simulator.transport 3 Arm and release the write backpressure fault
simulator.secs1 10 The SECS-I virtual pair
simulator.gem300 2 The GEM300 capability catalog
simulator.ui (GUI modes only) 6 Window state, workspace navigation, theme, session binding, open and close a trace

The same information is also available as resources. resources/list returns pdcomm://capabilities, pdcomm://sessions, pdcomm://profiles and pdcomm://scenarios, and resources/templates/list returns per-session resources such as pdcomm://sessions/{session_id}/trace.

Each MCP session is identified by:

  • session_id: the server’s name for the session, such as sim-1. It’s not the HSMS session ID (protocol_session_id).
  • generation: a new number every time the session starts. It increases across the whole server, so it isn’t consecutive per session.
  • revision: increases by one whenever the session changes.

Every tool that changes a session takes the current revision in expected_revision. If it doesn’t match, the call fails with REVISION_CONFLICT and nothing changes. Always pass the latest generation and revision from the previous response.

Tools whose effects persist, such as catalog configuration, event and alarm publishing and commands, also take an operation_id of 1 to 64 bytes. If you repeat a call with the same operation_id and the same input, you get the earlier result with replayed: true, and nothing runs twice. After a timeout or a dropped connection, call again with the same ID to learn the outcome instead of sending a new ID.

Walkthrough: create an equipment and a host, then read values

Section titled “Walkthrough: create an equipment and a host, then read values”

These requests and responses come from a real run against the standalone server (pdcomm-simulator-mcp --stdio). Only the structuredContent of each response is shown, shortened.

{"name":"simulator.session.create","arguments":{"role":"equipment","mode":"passive","address":"127.0.0.1:0","protocol_session_id":7}}
{"session_id":"sim-1","generation":1,"revision":0,"role":"equipment","mode":"passive","lifecycle":"created","tcp_state":"stopped","local_address":""}
{"name":"simulator.lifecycle.start","arguments":{"session_id":"sim-1","generation":1,"expected_revision":0}}
{"session_id":"sim-1","generation":2,"revision":1,"lifecycle":"running","tcp_state":"listening","local_address":"127.0.0.1:49677","control_state":"host_offline"}

Starting the session assigns a new generation and resets revision to 1. Because you asked for port 0, read the real port from local_address.

{"name":"simulator.session.create","arguments":{"role":"host","mode":"active","address":"127.0.0.1:49677","protocol_session_id":7}}
{"name":"simulator.lifecycle.start","arguments":{"session_id":"sim-2","generation":3,"expected_revision":0}}
{"session_id":"sim-2","generation":4,"revision":1,"lifecycle":"running","tcp_state":"connecting"}

Sessions of the standalone server only move bytes when you call simulator.lifecycle.poll. Poll both sessions in turn. When the host reports tcp_state: "selected" and communication_state: "not_communicating", call simulator.lifecycle.begin_communication. Keep polling until both sessions report "communicating"; in this run that took five rounds.

{"name":"simulator.lifecycle.begin_communication","arguments":{"session_id":"sim-2","generation":4,"expected_revision":3}}
{"session_id":"sim-2","generation":4,"revision":4,"tcp_state":"selected","communication_state":"waiting_ack"}

The host queries names first (simulator.status_data.query_names) and only then values (simulator.status_data.query_values). After polling, the results appear with remote: true in the host session’s simulator.status_data.list.

{"name":"simulator.status_data.query_names","arguments":{"session_id":"sim-2","generation":4,"expected_revision":6,"ids":[1001,1002]}}

After the values arrive, simulator.status_data.list returns:

{"variables":[
{"kind":"status","id":1001,"name":"EquipmentState","units":"state","format":16,"count":4,"value":"IDLE","value_present":true,"remote":true,"editable":false},
{"kind":"status","id":1002,"name":"WaferCount","units":"wafers","format":44,"count":1,"value":"0","value_present":true,"remote":true,"editable":false}
]}

format is the SECS-II format code: 16 (octal 20) is ASCII and 44 (octal 54) is U4.

simulator.trace.read returns the session’s most recent records, up to 256. Each record’s category is activity, received or error, and its source is service, tcp or gem. Message bodies aren’t recorded.

Finally, call simulator.lifecycle.stop and simulator.session.destroy for each session. When the client closes standard input, the server exits with code 0.

Errors come in two layers:

  • JSON-RPC errors: malformed requests, unknown methods (-32601), unknown tools or invalid parameters (-32602), request deadline exceeded (-32603) and so on.
  • Tool errors: a normal result with isError: true and the details in structuredContent.error.

Here’s the real response to a wrong expected_revision:

{"error":{"code":"REVISION_CONFLICT","field":"expected_revision","message":"session revision does not match"}}
Tool error code Meaning
INVALID_ARGUMENT The input doesn’t match the schema or a value is out of range.
SESSION_NOT_FOUND The session_id doesn’t exist.
STALE_GENERATION The generation belongs to an earlier start. Get the session again.
REVISION_CONFLICT expected_revision doesn’t match the current value.
INVALID_STATE Not allowed in the current state, for example a query before communication is established, or a catalog change after start.
ROLE_FORBIDDEN Wrong role, for example begin_communication on an equipment session.
LIMIT_EXCEEDED A limit was reached, such as the maximum of eight sessions, or an event report (S6F11) doesn’t fit the message limits. Nothing is sent in that case; see Event report size limits.
DEADLINE_EXCEEDED, CANCELLED The request timed out or was cancelled before it changed anything.
UNSUPPORTED_CAPABILITY, CORE_FAILURE, SHUTTING_DOWN Unsupported capability, internal failure, or the server is shutting down

A rejected call never changes the session. Treat both isError and a JSON-RPC error as failures.

Equipment and host sessions use the same message limits: a message body of up to 8,514 bytes, with up to 4,096 bytes of value in any single item.

  • An equipment session sends only event reports (S6F11) that fit these limits. For a larger event, simulator.reports_events.emit and simulator.data_variables.emit_with_samples return LIMIT_EXCEEDED and send nothing. Both sessions keep communicating.
  • A host session accepts every event report within the limits. It keeps the encoded values of each received event until you acknowledge the event. limits.max_received_value_bytes in the simulator.capabilities.get result is the value budget for one event.
  • A host session keeps at most 16 reports per event, 16 values per report and 64 values in total. It refuses a valid event report beyond that, for example from another vendor’s equipment: it replies S6F12 with ACKC6 1 (error, not accepted), adds a reports_events.event_refused error record with code LIMIT_EXCEEDED to the trace, and keeps communicating. The reason appears in last_error_message of simulator.trace.read and simulator.diagnostics.get, and the refused event isn’t listed as received. Only an S6F11 body that can’t be parsed is aborted with S6F0.
  • Other messages that a session keeps for you follow the same rule. When a valid message holds content the simulator can’t represent, such as a PPID with control characters that the simulator doesn’t accept, the session answers it instead of keeping it. A host answers S7F27 with S7F28, which carries no acknowledgement code. An equipment answers S7F23 with S7F24 ACKC7 1 (permission not granted), S7F31 with S7F32 ACKC7 1, and S7F25 with S7F26 from its program store, which is an empty list for such a PPID. Each refusal adds an error record to the trace (programs.verification_report_refused, programs.formatted_transfer_refused, programs.verification_request_refused or programs.formatted_query_refused) with the reason in last_error_message, and both sessions keep communicating.
  • A message that exceeds the limits configured in the PDComm library, for example more than 16 parameters in one command or more than 16 issues in S7F27, is still aborted with SxF0; an equipment can also send S9F7 (illegal data) first.
  • When 16 events are pending, or their values add up to 65,536 bytes (limits.received_value_backpressure_bytes), the host stops reading new messages, and simulator.reports_events.list_received returns backpressured: true. Acknowledge an event with simulator.reports_events.ack_received to resume. This budget keeps the list of pending events within the 1 MiB MCP response limit.

To drive a GUI that is already open, start it with --mcp-local and let your client run pdcomm-simulator-mcp-attach.

  1. Start the GUI with a descriptor file path. If a file already exists at that path, the GUI exits with code 3.

    터미널 창
    "$PDCOMM_SIMULATOR/pdcomm-simulator" --mcp-local "$HOME/pdcomm-tutorial/results/gui-descriptor.json"

    The GUI writes a descriptor that only its owner can read (mode 0600). It holds the local connection name and an authentication token (capability token).

  2. In your MCP client configuration, set command to pdcomm-simulator-mcp-attach and args to ["--descriptor", "<descriptor path>"]. On macOS, the program is pdcomm-simulator.app/Contents/MacOS/pdcomm-simulator-mcp-attach.

The attach program relays standard input and output to the GUI unchanged. From then on, the MCP conversation is the same as before, with 175 tools. For example, simulator.ui.navigate with {"workspace_id":"trace_diagnostics"} switches the GUI to the Message Trace & Diagnostics workspace. When you close the GUI, it removes the descriptor file.

Attach exit code Meaning
0 Standard input was closed.
2 Invalid arguments
3 The descriptor is missing, malformed, or has the wrong owner or permissions.
4 Connection or authentication failed.
5 The GUI disconnected, or writing standard output failed.
6 Reading standard input failed, or an input line exceeded 1 MiB.