Test Scripting
The mcp-tester scripting engine enables automated test workflows for MCP servers. Scripts are saved in files with the .mcp extension.
General Syntax
- Commands: One command per line.
- Comments: Lines starting with
#or//are ignored. Trailing comments (#,//) are also supported, but only outside quotes:"## Heading"stays intact. - Variables: Referenced with a
$prefix (e.g.,$name). They are substituted per argument after the line is split, so a value with spaces stays one argument and an empty value stays an (empty) argument:assert_equals $body "". Substitution uses regex matching (\$([A-Za-z_][A-Za-z0-9_]*)). Referencing an unknown variable aborts execution with a clear line-referenced error. - Strings: Can be enclosed in double quotes (
"...") or single quotes ('...') if they contain spaces or special characters. Inside double quotes\"and\\are escapes ("missing: [\"code\"]"); every other backslash stays literal. Single quotes are taken verbatim. An unterminated quote fails the line. - Lists / Objects: JSON arrays (
'["a.png", "b.png"]') or JSON objects ('{"key": "value"}') can be passed directly as arguments.
Command Overview
1. echo
Prints a message to the console. Useful for structuring test output and adding section headers in longer scripts.
echo "--- Testing user creation workflow ---"
echo "Current ID: $user_id"
2. call_tool
Invokes an MCP tool.
call_tool <tool_name> [arg1] [arg2] ...
- Arguments: Can be passed positionally or as named arguments (
key:value).- Positional: Arguments are automatically converted to the correct type based on the tool's JSON schema. The order corresponds to the alphabetical sorting of the property names in the schema.
- Named: Arguments follow the
key:valuesyntax (e.g.paths:'["a.png", "b.png"]'). This is recommended to avoid confusion with alphabetical sorting. A name the tool's schema does not know is a script error that lists the valid names, so a typo cannot land in another field; so are more positional values than fields. A value starting with//(http://…) counts as positional. - Unchecked:
call_tool_raw <tool> '<json object>'sends the arguments exactly as written, e.g. to test that a server rejects unknown fields. - Arrays and Objects: Supports schemas with
type: "array",type: "object"as well as nullable definitions (type: ["null", "array"]). - Mixed: You can mix both; positional arguments will fill the remaining properties in alphabetical order.
- Result check: If the tool declares an
outputSchema, the call fails unless the result carriesstructuredContentthat matches it — the check strict clients make (the official TypeScript SDK, used by OpenCode, rejects such a call with-32600). Results withisError: trueare exempt. The Go SDK this tester is built on does not check this itself, which is why the tester does.
Heredoc Support: For multiline arguments (e.g. JSON or code blocks), heredoc syntax can be used:
call_tool execute_script <<EOF
console.log("Hello from Heredoc!");
console.log(1 + 2);
EOF
3. expect_error
Prepended to a command when an error (tool error or protocol error) is expected.
expect_error call_tool add a:"not_a_number"
assert_tool_error
assert_contains "type"
expect_error call_tool unknown_tool
assert_error_code -32602
4. assert_tool_error and assert_error_code
The MCP specification strictly distinguishes between application-level tool errors (isError: true in the tool result) and JSON-RPC protocol errors (such as invalid request or unknown tool).
assert_tool_error: Verifies that the tool returned a result withisError: true(alternativelyassert_error_code tool).assert_error_code <code>: Verifies the numeric JSON-RPC protocol error code of the last failed request. A tool error (isError: true) fails on numeric codes.
# Tool execution error
expect_error call_tool validate_script script:"invalid"
assert_tool_error
# Protocol error (-32602 = Invalid params / unknown tool)
expect_error call_tool non_existent_tool
assert_error_code -32602
Common JSON-RPC Error Codes
-32700: Parse error (invalid JSON)-32600: Invalid Request-32601: Method not found-32602: Invalid params (schema validation on RPC level)-32603: Internal error
5. set_var
Extracts a value from the last tool response and stores it in a variable.
set_var <variable_name> <path>
- Paths:
rawResponse: Stores the complete JSON response from the server.structuredContent.<path>: Navigates through the JSON structure (dot notation).$.<path>: Short form forstructuredContent.<path>; if the field is not there, the top level of the result is tried.
6. input_var
Prompts the user for input during the test.
input_var <variable_name> ["Interactive Prompt"]
7. assert_contains
Checks if the last response (text or JSON) or a specific value contains the expected string.
assert_contains "Execution finished"
assert_contains $var "expected"
8. assert_equals
Checks for an exact match against the last response or between two values.
assert_equals "Result: 30"
assert_equals $var "123"
9. assert_number
Checks if a value (or a variable) is a valid number.
assert_number $variable
10. assert_gt
Checks if the first value is greater than the second.
assert_gt $value1 $value2
11. assert_string_length
Checks if the length of a string (or variable) is within a specific range.
assert_string_length $variable <min> <max>
12. complete
Asks the server for argument completions (completion/complete).
complete <prompt:name|resource:uri> <argument> [value]
The result is stored as {values, total, hasMore}: assert_contains sees the values one per line, set_var addresses values.0 or total.
complete prompt:persona_developer language g
assert_contains "golang"
set_var first values.0
13. Input requests: elicit_response, sample_response, add_root
Since spec 2026-07-28 a server asks the client for input via multi round-trip requests: tools/call, prompts/get or resources/read returns inputRequests, the client answers and retries. The tester does this automatically; the script prepares the answers before the call. A request without a prepared answer fails the call.
elicit_response accept '{"confirm": true}' # also: decline, cancel
sample_response "The model's reply" # answer to sampling/createMessage
add_root file:///home/user/project project # offered via roots/list
Answers are used in order (queue). assert_elicited <text> checks the message of the last elicitation request, assert_sampled <text> the prompt of the last sampling request.
elicit_response accept '{"confirm": true}'
call_tool confirm_delete item:"report.pdf"
assert_elicited "report.pdf"
On the command line the same answers are given with --elicit 'accept:{"confirm":true}', --sample "text" and --root file:///path (all repeatable); there an unanswered elicitation is declined.
14. Notifications: subscribe, wait_notification
The tester opens a subscriptions/listen stream for every list the server marks as listChanged, and records notifications/tools|prompts|resources/list_changed and notifications/resources/updated.
subscribe mcp://time # resource updates for this URI
wait_notification tools/list_changed # waits up to 5s (default)
wait_notification resources/updated mcp://time 2s
wait_notification succeeds with a notification received since the last wait, also one that arrived before the command, and consumes it. With expect_error, it checks that nothing arrives.
On the command line, mcp-tester listen [--subscribe <uri>] [--duration 30s] prints notifications as they arrive.
15. Tasks: call_task, start_task, wait_task, get_task, cancel_task
With the Tasks extension (io.modelcontextprotocol/tasks) a server may answer a tool call with a task handle and run it in the background. The tester declares the extension only for these commands; call_tool stays synchronous.
call_task long_job seconds:2 # start, poll to the end, result as with call_tool
assert_task_status completed
start_task long_job seconds:30 # only the handle
set_var id taskId
get_task $id # one tasks/get
cancel_task $id
wait_task $id 5s # poll until completed, failed or cancelled
assert_task_status cancelled
If a task needs input (status input_required), the answers prepared with elicit_response / sample_response / add_root are sent via tasks/update. A failed task (failed) is an RPC error for expect_error / assert_error_code. On the command line: mcp-tester call <tool> --task.
16. Skills: list_skills, verify_skills
For servers publishing Agent Skills via the Skills extension (io.modelcontextprotocol/skills).
list_skills # stored as {skills: [...], count}; text: one name per line
assert_contains "image-prompting"
set_var n count
verify_skills # all checks incl. reading every file; fails on any violation
On the command line, mcp-tester skills [--verify] lists name, description, license, allowed-tools and files of every skill and prints the checks.
17. MCP Apps: verify_apps
For servers delivering interactive UIs via MCP Apps (io.modelcontextprotocol/ui). Scripts announce the tester as a UI-capable host, so such servers expose their UI tools.
verify_apps # ui:// resources, MIME type, HTML, visibility, CSP; fails on any violation
assert_contains "ui://clock"
set_var n count
On the command line, mcp-tester apps lists every app with its tools, tools hidden from the model, external domains (CSP) and requested permissions.
Example Script
echo "--- Starting user workflow ---"
# 1. Call tool and store ID
call_tool create_user "John Doe"
set_var user_id $.id
# 2. Use variable in next call
call_tool get_user $user_id
assert_contains "Doe"
# 3. Pass array argument
call_tool assign_roles user_id:$user_id roles:'["admin", "tester"]'
assert_contains "Roles assigned"
# 4. Mathematical check
set_var score $.profile.score
assert_gt $score 0
Execution
A script is started via the test menu item or directly via the CLI:
mcp-tester test --script my_test.mcp --profile my_server
The command exits with status 1 as soon as one script command fails, so it can gate CI or a Taskfile. With --format json, stdout carries only the summary (including a failures list with line and error); per-command output goes to stderr.