UDS MCP server skeleton based on official Python MCP SDK mcp (FastMCP),
with py-uds + python-can as communication foundation.
- Send CAN frame and inspect CAN TX/RX logs.
- Send UDS request and inspect UDS response.
- Load/save shared YAML flow definitions.
- Start/stop/resume flow, inspect async run status.
- Set breakpoints, inject one-off UDS requests, patch flow step send/expect.
- Keep diagnostic session alive on breakpoint pause using
0x3ETesterPresent. - Export logs in BLF format by time window.
uv syncor
pip install -e .uv run uds-mcpThe server uses stdio transport by default (FastMCP.run()).
Direct CLI (without MCP client):
uv run uds-mcp-cli config-show
uv run uds-mcp-cli uds-send 1003 --timeout-ms 1200 --addressing-mode physical
uv run uds-mcp-cli flow-run ./examples/flows/demo_virtual_can_flow.yaml
uv run uds-mcp-cli flow-run ./examples/flows/demo_virtual_can_flow.yaml --config ./uds.toml
uv run uds-mcp-cli flow-suite --glob "./examples/flows/*.yaml" --report-html ./reports/flow-report.htmlUse flow-suite for one-command batch execution and summary reports:
uv run uds-mcp-cli flow-suite --glob "./examples/flows/*.yaml"Default output:
- JSON report:
./flow-report.json - Includes totals, pass/fail/skipped, pass rate, duration, and case details.
Optional outputs:
--report-html ./reports/flow-report.htmlfor lightweight browser viewing.--report-junit ./reports/flow-report.xmlfor CI integration.
Useful options:
--path <file-or-dir>(repeatable)--glob <pattern>(repeatable)--suite ./suite.yaml(suite config file)--timeout-s <seconds>(per-flow timeout)--stop-on-fail(pytest-xstyle)
Example suite file:
name: smoke
include:
- examples/flows/*.yaml
exclude:
- "*draft*"
timeout_s: 3.0
stop_on_fail: falseNotes:
- This built-in report path is intentionally lightweight and dependency-free.
- Allure can be added later through adapters, but the built-in JSON/HTML/JUnit path is faster to adopt and easier to maintain.
- Report payload fields are plain English keys (
passed,failed,pass_rate) and can be localized by post-processing if needed.
Startup config behavior:
- Prefer
./uds.toml(or custom path viaUDS_MCP_CONFIG_PATH). - Fallback to environment variables if the TOML file does not exist.
- For TOML config, relative
flow_repoandextension_whitelistare resolved against the TOML file directory.
Lint:
uv run ruff check .Format:
uv run ruff format .Test:
uv run pytestUsed as fallback only when uds.toml is absent.
UDS_MCP_CAN_INTERFACEdefault:socketcanUDS_MCP_CAN_CHANNELdefault:vcan0UDS_MCP_CAN_BITRATEdefault:500000UDS_MCP_CAN_FDdefault:falseUDS_MCP_CAN_DATA_BITRATEoptional (used when CAN FD is enabled)UDS_MCP_UDS_TX_IDdefault:0x7E0UDS_MCP_UDS_RX_IDdefault:0x7E8UDS_MCP_UDS_TX_FUNCTIONAL_IDdefault:0x7DFUDS_MCP_UDS_RX_FUNCTIONAL_IDdefault:0x7E8UDS_MCP_UDS_USE_DATA_OPTIMIZATIONdefault:falseUDS_MCP_UDS_DLCdefault:8(data bytes, discrete CAN FD lengths only:8/12/16/20/24/32/48/64)UDS_MCP_UDS_MIN_DLCdefault:8(data bytes; used only whenUDS_MCP_UDS_USE_DATA_OPTIMIZATION=true)UDS_MCP_FLOW_REPOdefault:./flowsUDS_MCP_EXTENSION_WHITELISTdefault:./extensionsUDS_MCP_EXTENSION_IMPORT_WHITELISTlegacy compatibility option (no longer enforced)UDS_MCP_TESTER_PRESENT_INTERVALdefault:2.0
can_sendcan_tailcan_restartuds_sendtester_present_starttester_present_stoptester_present_statusflow_loadflow_register_inlineflow_listflow_template_presetsflow_init_templateflow_startflow_statusflow_stopflow_resumeflow_breakpointflow_patch_stepflow_inject_udsflow_savelog_export_blflog_queryconfig_getconfig_updateconfig_loadconfig_export
before_hook now receives a context object with:
request_hex: request hex prepared for current step.response_hex: previous step response hex (Noneon first step).variables: flow variables snapshot.trace: read-only historical step records (step/request_hex/response_hex).
Hook outputs can now include variables for write-back. Example snippet:
seed = context["response_hex"][4:]
result = {
"request_hex": "2712" + seed,
"variables": {"seed": seed},
}before_hook can also return request_sequence_hex (list[str]) to send multiple requests
within one step (for block-level fault injection such as deliberate resend):
req = context["request_hex"]
result = {"request_sequence_hex": [req, req]}Hooks can also return request_items for per-request dispatch controls:
result = {
"request_items": [
{"request_hex": "3601AA", "skipped_response": False},
{"request_hex": "3602BB", "skipped_response": True},
]
}request_items takes priority over request_sequence_hex, which takes priority over request_hex.
For built-in 0x36 TransferData, use standardized segments (address + data_hex).
OEM-specific file parsing should be done by external trusted tools, then flow consumes normalized segments.
Static segments example:
steps:
- name: transfer_payload
transfer_data:
segments:
- address: 0x1000
data_hex: "AABB"
- address: 0x2000
data_hex: "CCDD"
chunk_size: 1
block_counter_start: 1
check_each_response: trueDynamic segments via Python hook (avoids huge YAML payloads):
steps:
- name: transfer_payload
transfer_data:
segments_hook:
snippet: |
# Parse/prepare externally, then return standardized segments
p = context["variables"]["payload_hex"]
result = {"segments": [{"address": 0x1000, "data_hex": p}]}
chunk_size: 256
block_counter_start: 1
message_hook:
snippet: |
if context["message_index"] == 2:
result = {"request_hex": "3603EE"}
else:
result = {}message_hook context includes message_index, message_total, step_name, request_hex,
response_hex, variables, and read-only trace.
can_tx_hook runs for each dispatched request and can send extra raw CAN bursts.
- If
skipped_response=true,can_tx_hookruns immediately after sending that request. - If
skipped_response=false, it runs after response reception for that request.
can_tx_hook context includes message_index, message_total, step_name, request_hex,
response_hex, skipped_response, addressing_mode, variables, and read-only trace.
can_tx_hook output supports:
result = {
"can_frames": [
{"arbitration_id": 0x7D1, "data_hex": "11223344", "is_extended_id": False}
]
}Each step also supports skipped_response (default false). When enabled for a sent request,
the engine transmits it without waiting for a UDS response (fire-and-continue).
transfer_data.check_each_response defaults to true:
true: each 0x36 response is checked againstexpect.response_prefiximmediately (fail-fast).false: only final response is checked byexpect; use hooks to assert negative-path expectations.
Relative script_path values in flow YAML are resolved against the YAML file directory.
When a flow is loaded from YAML, variables keys ending with _path are also resolved against
the YAML file directory before hook execution.
Hook context also provides:
flow_dir: absolute directory of loaded flow YAML.flow_path: absolute file path of loaded flow YAML (ornullif flow was registered inline).
after_hook is also supported per step. It receives current request_hex, current response_hex,
variables, and read-only trace. Hook output can include:
variables: write-back variables for next steps.response_hex: optional response override beforeexpectchecks.
expect now supports native assertions for stronger checks beyond response_prefix.
response_prefix remains fully supported for backward compatibility.
expect response matcher now supports one of:
response_prefix(legacy compatible)response_regexresponse_equals
Use response_on_fail to control behavior when this matcher fails:
recordfailfatal(default)
Supported assertion kinds:
hex_prefixhex_equalsbyte_eqbytes_int_range
Each assertion supports on_fail:
record: only log assertion error event, flow continues.fail: fail the flow immediately.fatal: fail the flow immediately as a hard-stop assertion.
Example (byte0 == 0x62, and byte1-2 in range):
steps:
- name: read_did
send: "22ABCD"
expect:
response_prefix: "62"
assertions:
- name: sid_ok
kind: byte_eq
source: response_hex
index: 0
value: 0x62
on_fail: fatal
- name: did_range
kind: bytes_int_range
source: response_hex
start: 1
length: 2
min_value: 0xAB00
max_value: 0xABFF
on_fail: recordSet expect.apply_each_response: true to apply these assertions on each response in multi-request
steps (for example transfer-data message loops).
Regex response matching example:
steps:
- name: read_did
send: "22ABCD"
expect:
response_regex: "^62ABCD[0-9A-F]{2}$"
response_on_fail: fatalHooks now receive assertions helper in context, so you can run multiple checks in one hook:
assertions.response_byte_eq(0, 0x62, name="sid_ok", on_fail="fatal")
assertions.response_bytes_int_range(
1,
2,
min_value=0xAB00,
max_value=0xABFF,
name="did_range",
on_fail="record",
)
result = {}uds_sendsupportsaddressing_mode:physical(default) orfunctional.tester_present_start(addressing_mode=...)andtester_present_stop()are available for manual control.- Flow breakpoint pause uses an internal TesterPresent owner and no longer conflicts with manual start/stop.
tester_present_status()reports whether periodic sending is running, current addressing mode, and active owners.
Flow YAML supports tester_present_policy with default breakpoint_only:
breakpoint_only: only keepalive while paused on breakpoint (backward-compatible behavior).during_flow: keep keepalive active for full flow run.off: no automatic keepalive at flow level.
Each step can override with tester_present:
inherit(default)on(enable keepalive for this step)off(disable flow-level keepalive for this step)
Each step also supports delay_ms for a non-blocking post-step wait. This is useful for
cases like 1002 -> 5002, where the ECU acknowledges reset immediately but still needs
extra boot time before the next request.
You can also define a native wait-only step by setting only delay_ms (no send, no
transfer_data, no sub_flow). Step-level tester_present still applies during this wait.
Example:
name: security_access_flow
tester_present_policy: during_flow
steps:
- name: request_seed
send: "2711"
tester_present: inherit
- name: send_key_without_tp
send: "2712ABCD"
tester_present: offBoot/reset delay example:
name: bootloader_entry
steps:
- name: ecu_reset_into_boot
send: "1002"
expect:
response_prefix: "5002"
delay_ms: 300
- name: request_seed_after_boot
send: "2701"
expect:
response_prefix: "6701"Wait-only step example:
name: boot_wait_flow
tester_present_policy: off
steps:
- name: wait_boot
delay_ms: 1500
tester_present: on
- name: read_seed
send: "2701"
expect:
response_prefix: "6701"- Recommend using
uvfor environment management and command execution (uv sync,uv run ...). - For SecurityAccess (
0x27) in flow mode, importCrypto(pycryptodome, bundled as a dependency) directly inside hooks to derive keys. - Use
response_hex+ variable write-back inbefore_hookto chain seed-read and key-send steps.
- Hook runtime now executes with unrestricted Python imports and builtins.
- Users are responsible for hook code safety and dependency governance.
extension_import_whitelistis retained for backward compatibility, but no longer enforced.
You can modify config during a session and switch profiles without restarting MCP:
- Use
config_getto inspect current runtime config. - Use
config_updateto patch selected fields (channel, bitrate, IDs, paths, etc.). - Use
config_load(path)to switch to another TOML profile. - Use
config_export(path)to persist current runtime config.
Note: reconfiguration is blocked while any flow run is RUNNING or PAUSED.
UdsClientServicenow uses fullpy-udsclient stack (Client,PyCanTransportInterface,CanAddressingInformation) on top ofpython-canbus.