QA How-To
Building an MCP server for test automation (2026)
Learn building an MCP server for test automation with typed tools, safe runner allowlists, stdio transport, protocol tests, authorization, and observability.
22 min read | 3,199 words
TL;DR
Building an MCP server for test automation means wrapping approved test discovery, execution, and result lookup behind small schema-validated MCP tools. Start with a local stdio server, use an allowlisted manifest and shell-free process execution, return structured results, and add remote transport only with proper authentication, authorization, rate limits, and audit controls.
Key Takeaways
- Expose narrow testing capabilities, never a generic shell command tool.
- Use typed input and output schemas and return machine-readable results.
- Prefer stdio for a local client-spawned server and Streamable HTTP for remote deployments.
- Authorize every side effect and keep test IDs mapped to an allowlisted manifest.
- Separate read-only resources from action-oriented tools.
- Test protocol behavior, tool contracts, runner failures, cancellation, and concurrency.
- Log to stderr for stdio and keep secrets and huge artifacts out of tool responses.
Building an MCP server for test automation gives an AI client a standard way to discover and call approved testing capabilities. A well-designed server can list test cases, run one allowlisted case, read a sanitized result, or retrieve framework guidance. It should never become a conversationally controlled terminal.
The engineering challenge is not registering a tool. It is defining a safe capability boundary, validating every argument, preserving the test runner's truth, enforcing authorization, and returning evidence the client can interpret without guessing. This guide uses TypeScript and the current MCP SDK patterns to build and test that boundary.
TL;DR
| Decision | Recommended starting point | Why |
|---|---|---|
| Local client integration | stdio transport | Simple process lifecycle and no listening port |
| Remote shared service | Streamable HTTP | Current remote transport with HTTP security controls |
| First capability | List approved tests | Read-only and easy to validate |
| First side effect | Run one manifest-listed test | Narrow scope and auditable intent |
| Input validation | Zod object schemas | Rejects invalid values before handler logic |
| Output | Text plus structured content | Useful to humans and programmatic clients |
| Dangerous capability | Arbitrary command or path | Do not expose it |
Start with a server that is useful even when the model is wrong: invalid IDs are rejected, arguments cannot alter the command, timeouts are bounded, output is truncated safely, and every run receives an audit ID.
1. What Building an MCP Server for Test Automation Means
Model Context Protocol, or MCP, defines how clients and servers exchange capabilities such as tools, resources, and prompts. In a test automation server, tools perform bounded operations, resources expose read-only reference data, and prompts can provide reusable interaction templates. The AI client chooses when to request a capability, but the server remains responsible for validation, authorization, execution, and results.
Examples of useful tools are list_test_cases, run_test_case, get_test_run, create_test_data_lease, and release_test_data_lease. Useful resources include test environment status, approved project configuration, framework conventions, and immutable run reports. A reusable prompt might guide defect triage, but prompts should not carry credentials or bypass server policy.
MCP does not make an unsafe test runner safe. If a tool accepts command: string and passes it to a shell, a model can accidentally or maliciously execute anything the process identity permits. The correct abstraction accepts domain arguments such as testId, environment, and browser, then maps them to a fixed server-side action.
Before exposing automation, review test automation architecture patterns so the MCP surface reflects stable testing capabilities rather than framework internals that change every sprint.
2. Choose Tools, Resources, and Prompts Deliberately
Use a tool when the operation computes a result, changes state, talks to another service, or may fail based on inputs. Running a test is a tool. Creating data or filing a defect is also a tool and needs stronger confirmation. Tool descriptions should be concrete enough that a client knows when not to call them.
Use a resource for data the client can read without triggering a costly or mutating operation. A URI such as qa-run://results/abc123 can expose a sanitized completed report. Framework policy at qa-doc://playwright/locators is another resource candidate. Resources still require authorization when their data is not public.
Use prompts sparingly. They are user-selectable templates, not hidden security instructions. A "triage failed run" prompt can request a run resource and compare evidence, but the server must enforce permissions independently.
Keep the first server small. Two or three coherent tools are easier for a model to select and easier for QA to evaluate than twenty overlapping tools. Avoid pairs such as run_test, execute_test, and start_automation with unclear distinctions. Prefer names and descriptions that reveal scope, cost, side effects, and prerequisites.
Version behavior through the server implementation and output schema. Do not change the meaning of an existing tool argument silently. Add a new field with a compatible default or introduce a clearly named replacement and notify clients when the tool list changes.
3. Threat-Model the Test Execution Boundary
List assets and abuse cases before writing handlers. Assets include source code, CI credentials, test accounts, customer-like data, network access, browsers, reports, and issue-tracker permissions. Abuse cases include arbitrary command execution, path traversal, environment escalation, secret exfiltration through logs, denial of service through parallel runs, and forged results.
The server process should run with the minimum filesystem, network, and credential access needed. Use a dedicated workspace or container, read-only source when possible, ephemeral output directories, and separate identities by environment. Production should not be an accepted environment for a general-purpose test tool unless the organization has an explicit, tightly controlled policy.
Never interpolate model-provided values into a shell string. Use process APIs that pass an executable and argument array without a shell. Map testId to a server-owned manifest entry. Validate browser and project names against enums. Enforce timeouts, output limits, concurrency limits, and cancellation. Sanitize report content before returning it.
For remote MCP, authentication proves identity, while authorization decides whether that identity may call a particular tool with a particular environment. Check authorization inside every handler or shared guard. Do not assume that successful connection authorization grants every future side effect.
4. Define Stable Tool Contracts and Error Semantics
Design the contract from client needs, not from the runner's command line. run_test_case might accept a stable testId, an allowed project, and an optional correlationId. It should not expose reporter flags, output paths, workers, arbitrary environment variables, or raw grep patterns unless each has a controlled business reason.
Return structured content with a run ID, status, exit code, duration, test ID, and truncated diagnostic. Also return a text content block because clients commonly display it directly. If a tool operation fails in its domain, return a tool result with isError: true and a safe description. Protocol, schema, or authentication errors are different and should use the SDK or transport's error handling.
| Error | Tool result | Retry guidance |
|---|---|---|
| Unknown test ID | isError: true |
Do not retry unchanged |
| Test assertion failure | Normal completed result with failed status | Investigate evidence |
| Runner timeout | Completed tool error or timed-out status | Retry only under policy |
| Busy concurrency pool | Controlled error with retry hint | Retry after backoff |
| Invalid schema input | Rejected before handler | Client must correct input |
| Unauthorized environment | Authorization error | Do not retry without new authority |
A failed test is not necessarily a failed tool call. The tool successfully ran the test and returned status: failed. This distinction lets the client reason about test evidence without treating every assertion as infrastructure failure.
5. Implement Building an MCP Server for Test Automation in TypeScript
The following local server uses the MCP TypeScript SDK v2 package layout, Zod 4.2 or later, and stdio. It reads an allowlisted manifest and executes Playwright through execFile, which does not invoke a shell. The test file and project come only from the manifest and schema enum.
Create a project and install dependencies:
npm init -y
npm install @modelcontextprotocol/server zod
npm install --save-dev typescript tsx @types/node @playwright/test
npx playwright install chromium
Create qa-tests.json with entries such as [{"id":"checkout-smoke","file":"tests/checkout.spec.ts","title":"Checkout smoke"}], then add server.ts:
import { readFile } from 'node:fs/promises';
import { execFile } from 'node:child_process';
import { promisify } from 'node:util';
import { McpServer } from '@modelcontextprotocol/server';
import { StdioServerTransport } from '@modelcontextprotocol/server/stdio';
import { z } from 'zod';
const execFileAsync = promisify(execFile);
type ManifestTest = { id: string; file: string; title: string };
const manifest = JSON.parse(
await readFile(new URL('./qa-tests.json', import.meta.url), 'utf8'),
) as ManifestTest[];
const byId = new Map(manifest.map((test) => [test.id, test]));
const server = new McpServer({
name: 'qa-test-automation',
version: '1.0.0',
});
server.registerTool(
'list_test_cases',
{
description: 'List test cases approved for MCP execution.',
inputSchema: z.object({ query: z.string().max(100).default('') }),
outputSchema: z.object({
tests: z.array(z.object({ id: z.string(), title: z.string() })),
}),
},
async ({ query }) => {
const needle = query.trim().toLowerCase();
const tests = manifest
.filter((test) => !needle || `${test.id} ${test.title}`.toLowerCase().includes(needle))
.map(({ id, title }) => ({ id, title }));
const output = { tests };
return {
content: [{ type: 'text', text: JSON.stringify(output) }],
structuredContent: output,
};
},
);
server.registerTool(
'run_test_case',
{
description: 'Run one manifest-approved Playwright test in the local test workspace.',
inputSchema: z.object({
testId: z.string().min(1).max(100),
project: z.enum(['chromium']),
}),
outputSchema: z.object({
testId: z.string(),
status: z.enum(['passed', 'failed', 'timed_out']),
diagnostic: z.string(),
}),
},
async ({ testId, project }) => {
const selected = byId.get(testId);
if (!selected) {
return {
content: [{ type: 'text', text: `Unknown test ID: ${testId}` }],
isError: true,
};
}
try {
const { stdout, stderr } = await execFileAsync(
process.platform === 'win32' ? 'npx.cmd' : 'npx',
['playwright', 'test', selected.file, '--project', project, '--reporter=line'],
{ cwd: process.cwd(), timeout: 120_000, maxBuffer: 512_000 },
);
const output = {
testId,
status: 'passed' as const,
diagnostic: `${stdout}\n${stderr}`.trim().slice(-8000),
};
return { content: [{ type: 'text', text: JSON.stringify(output) }], structuredContent: output };
} catch (error: any) {
const timedOut = error?.killed === true || error?.code === 'ETIMEDOUT';
const output = {
testId,
status: timedOut ? ('timed_out' as const) : ('failed' as const),
diagnostic: `${error?.stdout ?? ''}\n${error?.stderr ?? error?.message ?? ''}`.trim().slice(-8000),
};
return { content: [{ type: 'text', text: JSON.stringify(output) }], structuredContent: output };
}
},
);
const transport = new StdioServerTransport();
await server.connect(transport);
Start it with npx tsx server.ts. For stdio, write diagnostics to stderr, never stdout, because standard output carries protocol messages.
6. Connect a Client and Configure Process Lifecycle
A local MCP client launches the server process with a command and arguments. Configure the client to run npx, tsx, and the absolute path to server.ts, or compile to JavaScript and run Node directly for a more predictable production setup. Pass only required environment variables and avoid inheriting a full developer shell environment.
The stdio transport is ideal when one trusted desktop or agent client owns the server lifecycle. There is no port, TLS termination, or multi-user session to manage. It is not automatically secure, because the child process still inherits operating system permissions. Treat client configuration as code and review any changed executable path or arguments.
Test initialization, tools/list, each tools/call, process termination, and behavior when the child crashes. The client should show tool descriptions before calls and request confirmation according to its own policy, especially for execution. The server must remain safe even if the client confirms a poor request.
For distribution, pin dependency ranges, create a lockfile, compile TypeScript, include the manifest, and verify checksums or package provenance. Avoid a startup script that downloads changing code on every invocation. Log the server version and manifest checksum to stderr at startup, without logging credentials.
7. Test the MCP Server at Protocol and Domain Levels
Unit-test pure functions such as manifest loading, authorization decisions, result mapping, truncation, and redaction. Integration-test handlers with a temporary workspace and a fake executable or tiny deterministic test suite. End-to-end tests should use an MCP client transport to initialize, list tools, call them, and close cleanly.
Contract cases should cover default query handling, invalid types, maximum lengths, unknown IDs, runner pass, assertion failure, timeout, excessive output, missing manifest, malformed manifest, process launch failure, and concurrent calls. Assert both text content and structured content. Confirm that a failed assertion produces a completed failed test result, not an invented success or opaque protocol crash.
Security tests should try ../ paths, shell metacharacters, flag injection, environment overrides, enormous strings, Unicode confusables, repeated calls, symlink escapes, and malicious report text. Because the public schema accepts a test ID rather than a file path, most path attacks should fail before process creation. Still validate manifest paths at startup and keep them inside the workspace.
Use Playwright test automation best practices for the underlying suite. MCP cannot compensate for unstable locators, shared state, or tests that leave environments dirty.
8. Move to Streamable HTTP Only When Remote Access Is Needed
Remote servers use Streamable HTTP, the modern MCP transport for HTTP request and optional server-to-client streaming behavior. HTTP plus SSE is a legacy compatibility path, not the default for new deployments. Moving remote introduces identity, network, session, proxy, and browser-origin risks that stdio avoids.
Use the SDK transport that matches the runtime. The Node integration uses the Node Streamable HTTP server transport, while web-standard runtimes use the web-standard transport. Follow the current SDK example for request routing and session management rather than copying an old SSE tutorial. Validate the Origin header where applicable and protect localhost servers from DNS rebinding. Bind intentionally, not to every interface by default.
For authorization, follow MCP's OAuth guidance and use an established identity provider. Validate token audience so a token issued for another service cannot be replayed to the MCP server. Never pass through a client's bearer token to an upstream API as if the server were that client. Use separate upstream credentials or a correctly designed delegated flow.
Rate-limit by user and tool, cap concurrent browsers, isolate workspaces, and associate every run with the verified principal. For shared servers, one user's artifacts, caches, progress messages, and session identifiers must never be visible to another user.
9. Add Safe Test Data and External Integrations
Test execution often needs data setup, issue lookup, or environment status. Expose each as a separate narrow capability. create_customer_fixture should accept a schema of safe synthetic attributes, return a lease ID, and expire automatically. It should not accept arbitrary SQL. get_issue_requirements should fetch a permitted issue, not accept a full URL to any internal host.
Classify tools by risk: read-only, reversible write, irreversible write, and privileged administration. The client can use that metadata for confirmation, but the server must enforce policy. Require stronger authorization and explicit user context for defect creation or environment mutation. Prefer dry-run output when a tool can show what it would change.
Keep credentials in the server's secret store and scope them per integration. Do not return raw upstream error bodies that may contain tokens. Map errors into safe codes and store full diagnostics only in an access-controlled system. Protect against server-side request forgery by allowlisting hosts and constructing URLs from server configuration.
Use compensating actions for leased data and make cleanup idempotent. A test run that times out should not leave permanent accounts or locks. Return cleanup status with the run result and expose a controlled recovery tool only when necessary.
10. Observe Runs Without Polluting Protocol Output
For stdio servers, all operational logs go to stderr or a separate sink. For remote servers, use structured logs and traces. Record request ID, verified principal, tool name, validated argument summary, server version, manifest version, start and finish time, result status, duration, output bytes, and safe error code. Avoid raw test data and secrets.
Metrics should include calls by tool, schema rejection, authorization denial, queue time, runner time, timeout, cancellation, pass and fail result, truncation, and cleanup failure. Alert on unusual call volume, repeated unknown IDs, high denial rates, stuck browser processes, and output-size spikes.
Large artifacts should be stored outside tool responses. Return a permission-protected resource URI or short-lived artifact reference, plus a small structured summary. Check authorization again when the resource is read. Do not place a megabyte trace into the conversation context.
Provide correlation across client request, MCP tool call, test run, CI job, and artifact. This lets an SDET reconstruct what occurred without relying on the model's narration. If the client retries after a timeout, an idempotency or correlation key should find the existing run rather than start an uncontrolled duplicate.
11. Evaluate Tool Selection and Result Interpretation
Server tests prove the implementation, but an AI-facing capability also needs behavior evaluation. Create prompts where the correct behavior is to list tests, run exactly one test, refuse an unavailable environment, ask for missing test identity, read an existing result instead of rerunning, or perform no call because the user asked a conceptual question.
Measure tool selection precision, argument validity, unnecessary calls, duplicate execution, confirmation compliance, result interpretation, and recovery from tool errors. Run the same cases against supported clients and model configurations, because tool descriptions interact with model behavior. The server should still contain the blast radius when selection is wrong.
Test adversarial instructions embedded in test titles, report output, issue descriptions, and resources. The client must treat tool results as data, while the server must never interpret model text as executable configuration. Add evaluation for confusing similar IDs and for a malicious result that claims a different tool should be called.
Review descriptions when the client chooses poorly. Make them more specific about prerequisites and side effects, but do not hide safety only in prose. Schemas, allowlists, authorization, rate limits, and isolation are enforceable controls.
12. Scale Building an MCP Server for Test Automation Responsibly
Scale capabilities by domain. A discovery server, execution server, and test-data server may have different identities and risk profiles. Splitting them can reduce credentials and simplify ownership. Avoid one central server with every QA and deployment permission simply because clients prefer one connection.
Add queue-backed asynchronous execution when tests outlive a normal request. Return a run ID promptly, expose status and result retrieval, support cancellation where the runner can honor it, and define retention. Do not simulate completion while a detached process continues invisibly. Remote state must survive process restarts or report a clear terminal failure.
Use compatibility tests before SDK or protocol upgrades. Confirm tool advertisement, schema conversion, transport behavior, sessions, authentication challenges, and clients in use. Keep legacy transport support isolated and time-bound if older clients require it.
Finally, publish an ownership and incident model. Someone must be able to disable a dangerous tool, revoke credentials, drain execution, and audit calls. A production MCP server is an API and execution system, not only a plugin for an AI demo.
Interview Questions and Answers
Q: What is the main security principle for an MCP test server?
Expose domain capabilities, not ambient machine power. Inputs should be typed and mapped to allowlisted server actions, the process should have least privilege, and every side effect should be authorized. A generic shell tool violates that boundary even if its description asks the model to be careful.
Q: When would you choose stdio over Streamable HTTP?
I choose stdio for a local server spawned and owned by one client because it has a simple lifecycle and no listening network service. I choose Streamable HTTP when multiple or remote clients genuinely need access. The remote option requires authentication, authorization, origin and host defenses, session isolation, rate limits, and stronger operations.
Q: Is a failed test an MCP tool error?
Usually no. If the runner executed correctly and an assertion failed, the tool succeeded in producing a failed test result. Infrastructure failure, invalid input, or an unknown test ID may be tool errors. Keeping these states distinct helps clients interpret evidence correctly.
Q: How do you prevent command injection?
The public tool accepts a stable test ID and enums, not a command, flags, or path. The server maps that ID to an owned manifest and uses a process API with an argument array and no shell. It also validates manifest paths, limits the process, and runs in an isolated workspace.
Q: How would you test the MCP protocol integration?
I use an actual MCP client transport to initialize, list tools, call tools with valid and invalid inputs, inspect structured and text results, and close. Tests cover handler errors, process crashes, timeouts, cancellation, concurrency, and version compatibility. Pure domain logic remains unit-tested separately.
Q: What belongs in a resource instead of a tool?
Read-only reference data such as a sanitized run report, environment status snapshot, or test framework policy is a resource candidate. Operations that compute, mutate, or invoke external systems belong in tools. Both still require authorization if the data is restricted.
Q: How do you observe a stdio MCP server?
I send structured diagnostics to stderr or a separate telemetry sink and reserve stdout for protocol traffic. Logs include safe correlation, tool, version, timing, status, and error-code fields. Large and sensitive artifacts go to protected storage rather than logs or tool content.
Common Mistakes
- Exposing
command,script,url, or arbitrary filesystem paths as model-controlled strings. - Assuming tool descriptions are security controls.
- Writing debug logs to standard output in a stdio server.
- Treating assertion failures as transport failures or hiding their exit status.
- Returning complete traces, videos, logs, and secrets inside tool content.
- Using one powerful service account for every environment and integration.
- Deploying an old HTTP plus SSE tutorial for a new remote server.
- Authenticating a connection once but skipping per-tool authorization.
- Forgetting timeouts, concurrency limits, cleanup, cancellation, and idempotency.
- Testing handlers directly without exercising an MCP client and transport.
Conclusion
Building an MCP server for test automation is successful when the protocol surface is smaller and safer than the underlying test system. Register typed tools, map stable IDs to approved actions, execute without a shell, return structured evidence, and preserve authorization and audit boundaries under every client request.
Begin with local stdio discovery and one allowlisted runner tool. Prove contract, security, and model-behavior tests, then add remote access or mutating integrations only when their operational need justifies the additional risk.
Interview Questions and Answers
Design a secure MCP tool for running tests.
The tool accepts a stable test ID and small enums, validates them with schemas, maps the ID to an owned manifest, authorizes the user and environment, and invokes a fixed runner with an argument array and no shell. It runs in an isolated workspace with time, output, and concurrency limits. The structured result includes a run ID, exact status, and protected artifact reference.
What is the difference between tools and resources in MCP?
Tools perform computations or side effects and can fail based on arguments. Resources expose read-only content through URIs. Both are advertised through MCP and both need authorization when the capability or data is restricted.
Why is stdio useful for a local MCP server?
The client owns the child process and communicates over standard streams, so no port or remote session layer is required. It is simple to configure and shut down. The child still inherits operating system permissions, so least privilege and safe executable configuration remain essential.
What extra controls does a remote MCP server need?
It needs authenticated identity, token audience validation, per-tool authorization, TLS at the deployment boundary, origin and host defenses, session and tenant isolation, rate and concurrency limits, secure artifact access, and complete audit trails. Streamable HTTP should follow current SDK and specification guidance.
How do you avoid duplicate test runs?
Accept or generate a correlation key, record run state durably, and return the existing run when the same authorized request is retried. The key must include behavior-changing inputs. Queue and result stores should use atomic creation so concurrent calls cannot both launch work.
What should be logged for an MCP tool call?
Log a safe request ID, verified principal, tool name, validated argument summary, server and manifest versions, timing, status, result size, and safe error code. Do not log tokens, raw test data, or huge report content. For stdio, operational logs must not go to standard output.
How would you evaluate whether a model uses MCP tools correctly?
I build scenarios for correct calls, missing-information clarification, no-call conceptual answers, authorization denial, existing-run lookup, and recovery from errors. Metrics include selection precision, valid arguments, unnecessary calls, duplicate actions, and correct interpretation. Server enforcement remains necessary even with strong evaluation results.
Frequently Asked Questions
What is an MCP server for test automation?
It is a server that exposes bounded testing capabilities, such as listing or running approved tests, through the Model Context Protocol. MCP-compatible clients can discover the tools and call them with schema-validated arguments.
Should an MCP test server expose a shell command tool?
No. A generic shell gives model-controlled input ambient machine power. Expose domain arguments such as an allowlisted test ID and map them to fixed server-side executables and argument arrays.
Which MCP transport should I use?
Use stdio for a local server spawned by one client. Use Streamable HTTP for remote or shared access, then add robust authentication, per-tool authorization, origin and host protections, rate limits, and session isolation.
Can an MCP server run Playwright tests?
Yes. The server can map an approved test ID to a Playwright file and invoke the runner without a shell. Limit projects, environments, time, output, concurrency, and filesystem access.
How should an MCP tool return test failures?
Return a normal structured test result with a failed status when the runner completed and an assertion failed. Reserve `isError` for tool-level problems such as unknown input, unavailable infrastructure, or authorization failure.
How do I test an MCP server?
Unit-test domain logic, integration-test runner boundaries, and use an MCP client for initialization, tool listing, calls, errors, and shutdown. Add security and model-behavior evaluations for unsafe arguments and unnecessary calls.
Where should large test artifacts go?
Store them in protected artifact storage and return an authorized resource URI or short-lived reference. Keep tool results compact and verify access again when the artifact is read.