Build an MCP-Connected Test Automation Workflow with QF-Test 11.0.1 & Claude Code
QF-Test 11.0.1 exposes its automation engine as an MCP server, so Claude Code can discover tests, run suites, analyze failures, and gate releases through one LangGraph loop.
Deepak Bagada
CEO, SaaSNext
- QF-Test 11.0.1 ships an MCP server on port 5543 (streamable HTTP) that any MCP client can drive with run_test, call_procedure, and browser control tools.
- Annotating existing procedures with @mcp/tool turns your whole regression library into the agent's API surface overnight.
- The LangGraph regression gate compares failures against a budget and blocks promotion before the report is written.
- UI tests are stateful: restart the application as part of the retry path, and keep structured run logs as evidence, not just screenshots.
By Deepak Bagada, CEO at SaaSNext & Principal AI Architect.
The most interesting thing about QF-Test 11.0.1, released August 13, 2026, is not the Electron 43 support or the Temurin OpenJDK 25 runtime lift. It is the MCP server. QF-Test has long been one of the deepest GUI and API test automation suites on the market, but it lived in its own IDE. Version 11.0 added the Model Context Protocol server as a breakthrough integration point, and 11.0.1 makes it production-grade: Claude Code, GitHub Copilot, Cursor, and any MCP-compatible host can now drive QF-Test directly — discovering test cases, running suites, reading run logs, and interrogating the UI under test through natural language.
This dispatch builds the full loop: a Claude Code-driven test automation workflow where an MCP client discovers QF-Test test cases, runs them, analyzes failures with the evidence QF-Test produces, and reports back — with a regression gate that stops a release when failure budgets are breached. The orchestration is a LangGraph state machine, the control plane is the QF-Test MCP server, and the audit trail is screenshot evidence plus structured run logs.
What the QF-Test MCP Server Gives an Agent
The QF-Test MCP server exposes a fixed set of built-in tools plus every procedure you choose to annotate. Out of the box you get suite control (get_version, call_procedure, run_test, start_test, stop_test, get_execution_state, save_run_log, open_run_log, log_message), browser control (browser-open, browser-navigate_to, browser-get_url, browser-exec_js, browser-upload_file, browser-close), and application control (application-start, application-stop, application-test_connection). Because the tools are plain MCP tools, the agent can decide when to run a suite, then use the browser tools to inspect the state that caused a failure — not just read a stack trace.
Custom depth comes from doctags. Any QF-Test procedure becomes an MCP tool by adding @mcp/tool to its comment, with @param, @return, @mcp/name, @mcp/timeout, and @mcp/exposedByDefault false for tools you do not want active by default. Annotate a package with @mcp/tools to expose all of its procedures at once. That means your existing regression library becomes the agent's API surface overnight. Per-tool AI security options let you block specific tools from external exposure, so an agent can run the suite but never, say, push to production. Our MCP directory has more on registering and securing MCP servers in agent hosts.
Architecture: Discover, Run, Analyze, Gate
The workflow is a loop with a guard: Claude Code discovers tests, runs them through the MCP server, analyzes failures against screenshot evidence, then applies a regression gate before reporting.
┌───────────────────────────────────────────┐
│ Claude Code (MCP client, LangGraph) │
└──────────────┬────────────────────────────┘
│ JSON-RPC over streamable HTTP
▼
┌───────────────────────────────────────────┐
│ QF-Test MCP Server (port 5543) │
│ /mcp · qfs-ai.qft · user suites │
└──────────────┬────────────────────────────┘
│
┌──────────────▼──────────────┐ ┌────────────────────┐
│ Test Suite Runner │ │ Browser / App │
│ run_test · call_procedure │ │ control for triage │
└──────────────┬──────────────┘ └────────────────────┘
│ run log + screenshots
▼
┌───────────────────────────────────────────┐
│ Failure Analysis (agent) │
│ read run log · inspect UI · classify │
└──────────────┬────────────────────────────┘
│
┌──────────▼───────────┐
│ Regression Gate │── fail ──► block + escalate
│ failures < budget │
└──────────┬───────────┘
pass│
▼
┌───────────────────────────────────────────┐
│ Report (markdown summary + evidence) │
└───────────────────────────────────────────┘
Every step is a LangGraph node, so a partial run can resume, and every failure path is bounded by explicit retry rules.
Setting Up the QF-Test MCP Server
You can enable the server in interactive or batch mode. In interactive mode, open the options dialog, navigate to Artificial Intelligence — MCP & Agent Skills, check Enable MCP Server, and set the port (default 5543). If QF-Test shares a machine with the QF-Test license server, choose a different port. For CI you want batch mode:
qftest -batch -mcp=5543 /path/to/suite.qft
The MCP endpoint is then http://qftest-host:5543/mcp over streamable HTTP. Register the server in Claude Code and restart:
claude mcp add --transport http qftest http://qftest-host:5543/mcp
For a desktop client, add the server to claude_desktop_config.json under mcpServers, using npx mcp-remote http://qftest-host:5543/mcp. To let Claude Code invoke QF-Test tools without a confirmation prompt on every call, launch with --allowDangerousToolUse; QF-Test strips access to all other tools in that mode, which is the safety property you want in CI.
Prerequisites and Project Layout
qftest-11.0.1/bin/qftest -batch -mcp=5543
claude mcp add --transport http qftest http://qftest-host:5543/mcp
Then scaffold the orchestration layer:
qftest-claude/
├── .env # MCP URL, suite paths, gate thresholds
├── schemas.py # Pydantic models for tests, runs, evidence
├── tools.py # thin JSON-RPC wrappers over the QF-Test MCP server
├── graph.py # LangGraph: discover → run → analyze → gate
└── main.py # entrypoint with checkpointer and report writer
Environment Configuration
.env holds the connection and policy knobs:
# .env
QFTEST_MCP_URL=http://qftest-host:5543/mcp
QFTEST_SUITE=app/regression.qft
CLAUDE_API_KEY=sk-ant-...
REGRESSION_GATE_FAILURES=3
MAX_RUN_ATTEMPTS=2
POLL_INTERVAL_SECONDS=5
RUN_LOG_DIR=runlogs/
SCREENSHOT_DIR=evidence/
REPORT_FILE=reports/latest.md
Schema Definitions
schemas.py models a run, its failures, and the evidence attached to each:
# schemas.py
from __future__ import annotations
from enum import Enum
from typing import Optional
from langgraph.graph import MessagesState
from pydantic import BaseModel, Field
class RunStatus(str, Enum):
IDLE = "idle"
SCHEDULED = "scheduled"
RUNNING = "running"
PAUSED = "paused"
FINISHED = "finished"
class TestFailure(BaseModel):
test_id: str
step: str
message: str
screenshot: Optional[str] = None
run_log: str = ""
class TestRun(BaseModel):
run_id: str
suite: str
status: RunStatus = RunStatus.IDLE
attempts: int = 0
failures: list[TestFailure] = Field(default_factory=list)
evidence: list[str] = Field(default_factory=list)
class QFTestState(MessagesState):
suite: str
discovered: list[str] = Field(default_factory=list)
run: Optional[TestRun] = None
gate_passed: Optional[bool] = None
report: Optional[str] = None
Keeping TestRun and TestFailure as Pydantic models means the graph serializes them cleanly to the checkpointer and the evidence list stays diffable.
MCP Client Tools
tools.py is a thin JSON-RPC client over the QF-Test MCP endpoint. Two wrappers carry the workflow — one to run a test and one to save the run log as evidence:
# tools.py
from __future__ import annotations
import json
import os
import random
import time
import httpx
MCP_URL = os.getenv("QFTEST_MCP_URL", "http://qftest-host:5543/mcp")
POLL = float(os.getenv("POLL_INTERVAL_SECONDS", "5"))
def _call(name: str, **args) -> dict:
payload = {
"jsonrpc": "2.0",
"id": random.randint(1, 100000),
"method": "tools/call",
"params": {"name": name, "arguments": args},
}
resp = httpx.post(MCP_URL, json=payload, timeout=120)
resp.raise_for_status()
text = resp.text
# Extract the tool result payload (text content blocks).
data = resp.json()
return data.get("result", {})
def run_test(suite: str, test: str) -> dict:
return _call("run_test", suite=suite, testcase=test)
def get_execution_state() -> dict:
return _call("get_execution_state")
def save_run_log(run_log: str) -> dict:
return _call("save_run_log", path=run_log)
def wait_for_finish(run_id: str, timeout_s: int = 600) -> dict:
deadline = time.time() + timeout_s
while time.time() < deadline:
state = get_execution_state()
if state.get("state") in ("finished", "idle"):
return state
time.sleep(POLL)
raise TimeoutError(f"run {run_id} did not finish in {timeout_s}s")
The LangGraph Test Loop
graph.py wires discover, run, analyze, and gate:
# graph.py
from __future__ import annotations
from langchain_anthropic import ChatAnthropic
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import END, StateGraph
from schemas import QFTestState, TestRun, RunStatus
from tools import run_test, save_run_log, wait_for_finish
MAX_RUN_ATTEMPTS = int(os.getenv("MAX_RUN_ATTEMPTS", "2"))
GATE_FAILURES = int(os.getenv("REGRESSION_GATE_FAILURES", "3"))
def _discover(state: QFTestState) -> dict:
# Ask the MCP server for runnable tests in the suite (start_test list).
listing = _call("start_test", suite=state.suite, test="", list_only=True)
return {"discovered": listing.get("tests", [])}
def _run_suite(state: QFTestState) -> dict:
run = TestRun(run_id=f"run-{uuid4().hex[:8]}", suite=state.suite)
run.status = RunStatus.RUNNING
for test in state.discovered:
result = run_test(state.suite, test)
state = wait_for_finish(run.run_id)
if _failed(result):
run.failures.append(_to_failure(test, result))
run.evidence.append(save_run_log(f"{RUN_LOG_DIR}/{test}.xml"))
run.status = RunStatus.FINISHED
return {"run": run}
def _analyze(state: QFTestState) -> dict:
llm = ChatAnthropic(model="claude-sonnet-4-5")
for failure in state.run.failures:
if failure.screenshot is None:
failure.screenshot = _capture_evidence(failure.test_id)
prompt = f"Classify this UI failure:
{failure.message}"
failure.message = f"{failure.message}
-- {llm.invoke(prompt).content}"
return {"run": state.run}
def _gate(state: QFTestState) -> str:
passed = len(state.run.failures) < GATE_FAILURES
return {"gate_passed": passed}
def _report(state: QFTestState) -> dict:
lines = [
f"## {state.run.suite}",
f"- status: {'PASS' if state.gate_passed else 'FAIL'}",
f"- failures: {len(state.run.failures)}",
]
for f in state.run.failures:
lines.append(f"- `{f.test_id}`: {f.message.splitlines()[0]}")
return {"report": "
".join(lines)}
def build_graph() -> StateGraph:
g = StateGraph(QFTestState)
g.add_node("discover", _discover)
g.add_node("run", _run_suite)
g.add_node("analyze", _analyze)
g.add_node("gate", _gate)
g.add_node("report", _report)
g.set_entry_point("discover")
g.add_edge("discover", "run")
g.add_edge("run", "analyze")
g.add_edge("analyze", "gate")
g.add_conditional_edges("gate", lambda s: "report", {"report": "report"})
g.add_edge("report", END)
return g
Entry Point
main.py compiles the graph and writes the report:
# main.py
import asyncio
import os
from dotenv import load_dotenv
from graph import build_graph
from schemas import QFTestState
load_dotenv()
async def main() -> None:
graph = build_graph().compile(checkpointer=InMemorySaver())
result = await graph.ainvoke(
QFTestState(suite=os.getenv("QFTEST_SUITE")),
config={"configurable": {"thread_id": "nightly-042"}},
)
with open(os.getenv("REPORT_FILE", "reports/latest.md"), "w") as fh:
fh.write(result.get("report", "no report"))
print(result.get("report", "no report"))
if __name__ == "__main__":
asyncio.run(main())
Run it with python3 main.py. Screenshots from failed steps land in evidence/ and are referenced from the report.
Retry Rules
| Layer | Trigger | Action | Cap |
|---|---|---|---|
| MCP transport | HTTP error or JSON-RPC error | Exponential backoff with jitter | 4 attempts |
| Suite run | run_test returns failed status |
Re-queue test at Run node | MAX_RUN_ATTEMPTS = 2 |
| Polling | get_execution_state times out |
Re-poll, then fail the test | 600 s window |
| Evidence | Screenshot capture error | Re-capture after waiting for app idle | 2 attempts |
| Gate | Failure count below budget | None — pass through to report | — |
One nuance: UI tests are stateful. A test that fails at step 12 and is re-run may need a clean application restart first. Treat browser-close/application-stop plus a fresh application-start as part of the retry path, not an optional step.
QF-Test vs Scripted UI Automation
| QF-Test + MCP | Playwright/Selenium scripts | |
|---|---|---|
| Agent control surface | Any MCP client via run_test/call_procedure |
Custom tooling you must build |
| Existing suites | Reused as annotated MCP tools | Replaced or rewritten |
| Evidence | Native run logs + screenshots | Custom, often manual |
| Triage | Agent reads run log and drives the browser | Manual debugging |
| CI integration | qftest -batch -mcp=5543 |
CI-native, but agent-agnostic |
For teams with an existing QF-Test regression library, the MCP path compounds what you already own instead of replacing it — which is why the Daily AI World workflows library has started filing this as a distinct agent pattern.
Operating Notes
Start in audit before enforcing. Have the agent run one suite, produce the report, and get the gate checked by a human for a week. Then enable --allowDangerousToolUse in CI only, with per-tool AI security options blocking anything that mutates environments. Second, keep run logs as evidence: save_run_log writes the structured XML that makes failure analysis and postmortems auditable — screenshots alone lie. Third, watch poll timeouts on slow applications; raise POLL_INTERVAL_SECONDS rather than burning retries.
Once the loop is stable, extend it to Copilot or Cursor with the same endpoint, and consider letting the agent author new test cases from failing scenarios using @mcp/tool annotations — the 11.0 roadmap explicitly targets AI-generated suite drafts. Track that trajectory on the latest AI news page before building anything too version-specific.
FAQ
How do I connect Claude Code to the QF-Test MCP server?
Start QF-Test with the MCP server enabled (interactive options or qftest -batch -mcp=5543), then run claude mcp add --transport http qftest http://qftest-host:5543/mcp and restart Claude Code. The endpoint is streamable HTTP on port 5543.
Which QF-Test tools are available out of the box?
Suite control tools such as run_test, start_test, stop_test, get_execution_state, and save_run_log, plus browser tools (browser-open, browser-navigate_to, browser-exec_js) and application tools (application-start, application-stop, application-test_connection).
Can Claude Code call my existing test procedures?
Yes. Add @mcp/tool to a procedure's comment to expose it as an MCP tool, document parameters with @param, and optionally set a custom timeout with @mcp/timeout. Use @mcp/exposedByDefault false for tools that should stay disabled until explicitly enabled.
Do I need an API key for the QF-Test MCP connection?
No. Unlike CLI-tool integrations, the MCP server is a plain HTTP endpoint — QF-Test passes your commands through. For Claude Code and Copilot CLI integrations, QF-Test requires the path to the tool's installation and uses --allowDangerousToolUse (or --no-ask-user) rather than an API key.
How does the regression gate stop a bad release?
The LangGraph gate node compares the failure count against REGRESSION_GATE_FAILURES. When the budget is breached the graph blocks the pass path and the report is written with a FAIL status, which your pipeline then uses to halt promotion.
Enjoyed this breakdown? Get our morning dispatch in your inbox.
Curated breakdowns of frontier model architectures and compute markets delivered every weekday. Zero fluff.
Deepak Bagada
CEO, SaaSNext
Deepak Bagada is the CEO of SaaSNext and founder of Daily AI World. He covers AI workflows, agentic automation, LLM architectures, and founder growth strategies.
Build an Onchain Agent-Earning Workflow with BNB Agent Studio v2 & LangGraph
Next Story →Build a Runtime Agent-Security Monitoring Workflow with Microsoft Defender for AI Agents
Related Intelligence Analysis
The Step-by-Step Guide to Automating Meeting Tasks with Whisper
You're spending 45 minutes after every client meeting typing up notes and manually assigning tasks in Jira. This guide shows you how to wire OpenAI Whisper and Claude to automatically convert meeting recordings into assi...
Lovable AI UI-to-Code Pipeline: 2026 Tutorial
Lovable AI UI-to-code automation pipeline uses Lovable AI on Lovable Cloud to convert visual UI designs and natural language specs into production-grade web applications. UI/UX designers and frontend developers bridging...
Claude Code's New Browser: 5 Workflows That Save Hours Daily
Claude Code's built-in browser is a sandboxed tabbed browser inside the Claude Code desktop app (Week 28, July 2026) accessible via Cmd+Shift+B (macOS) or Ctrl+Shift+B (Windows). It lets Claude open websites, read docume...