testlink-mcp-server
v1.2.0
Published
Production-ready MCP server for TestLink XML-RPC integration.
Maintainers
Readme
TestLink MCP Server
An MCP server that exposes TestLink over its XML-RPC API, so an AI client (Claude Desktop, Claude Code, Cursor) can read and write test cases, suites and execution results by name — no test case IDs, no XML-RPC calls by hand.
Twelve tools cover the day-to-day TestLink loop: browse projects and suites, create and update test cases, read and set custom fields, and report a pass/fail/blocked result against a test plan and build.
How it works
AI client (Claude / Cursor)
→ stdio ──► TestLink MCP server
registerTools tool definitions + input schemas
toolController zod validation, error → MCP result
services one per tool, name → ID resolution
gateway XML-RPC calls, response normalization
│
▼
TestLink xmlrpc.php (XML-RPC v1)Everything is addressed by name. TestCaseResolutionService turns a project, suite or test case
name into the internal TestLink ID, walking the full suite tree breadth-first and caching projects
and suites for 60 seconds so a multi-step conversation doesn't re-fetch the tree on every tool
call. Name matching is case-insensitive.
TestLink's XML-RPC responses are inconsistent — arrays in some versions, objects keyed by ID in
others, and field names that vary (name / testcase_name, tc_external_id /
full_tc_external_id). The gateway normalizes all of that, so services and tool output see one
stable shape.
Requirements
- Node.js 18+ (ES2022 / NodeNext modules)
- A TestLink instance with the XML-RPC API enabled and a developer key for the account the server acts as
Setup
npm install
cp .env.example .env # then fill in your values
npm run buildEnvironment Variables
| Variable | Required | Default | Description |
|---|---|---|---|
| TESTLINK_API_ENDPOINT | Yes | — | TestLink XML-RPC endpoint URL, e.g. https://host/lib/api/xmlrpc/v1/xmlrpc.php. Must parse as a URL. |
| TESTLINK_DEV_KEY | Yes | — | TestLink API dev key (My Settings → API interface) |
| TESTLINK_AUTHOR_LOGIN | No | admin | Author username for creating/updating test cases |
| MCP_SERVER_NAME | No | testlink-mcp-server | MCP server name, also tagged on every log line |
| MCP_SERVER_VERSION | No | version from package.json | MCP server version reported in the handshake |
| LOG_LEVEL | No | info | fatal error warn info debug trace |
Config is validated with zod at startup — a missing or malformed variable exits immediately with the offending field named, rather than failing on the first tool call.
Keep
.envout of git. The developer key grants full API access under that TestLink account.
Run
npm run dev # tsx, no build step — development
npm run build # tsc → dist/, then chmod +x dist/index.js
npm start # node dist/index.js — production
npm run typecheck # tsc --noEmitThe server speaks stdio, so it is normally launched by the MCP client rather than run by hand.
MCP Client Configuration
- Command:
node - Args:
dist/index.js
Ensure environment variables are available to the MCP process. Add to your client's MCP config —
claude_desktop_config.json for Claude Desktop, .mcp.json for Claude Code, or the equivalent in
Cursor:
{
"mcpServers": {
"testlink": {
"command": "node",
"args": ["/absolute/path/to/TZ-Taazaa-Testlink-MCP-Server/dist/index.js"],
"env": {
"TESTLINK_API_ENDPOINT": "https://your-testlink-host/lib/api/xmlrpc/v1/xmlrpc.php",
"TESTLINK_DEV_KEY": "your-testlink-dev-key",
"TESTLINK_AUTHOR_LOGIN": "admin"
}
}
}
}Use an absolute path — the client does not run from this repo's directory. Run npm run build
first; dist/index.js is what the config points at. Variables set in env here take precedence
over .env.
Available Tools
| # | Tool | Description |
|---|---|---|
| 0 | get_projects | List all TestLink projects |
| 1 | get_test_cases | Fetch test cases with optional filters (suite, importance, automated) |
| 2 | create_test_case | Create a test case in a project/suite |
| 3 | update_test_case | Update an existing test case (including custom fields) |
| 4 | create_test_suite | Create a test suite, optionally nested under a parent |
| 5 | get_test_suites | List test suites, optionally filtered by parent |
| 6 | get_custom_field | Get a custom field value on a test case |
| 7 | update_custom_field | Update custom field values on a test case |
| 8 | report_execution | Report test execution result (passed/failed/blocked) for a test case under a test plan and build |
| 9 | get_test_plans | List all test plans for a project |
| 10 | get_builds | List all builds for a test plan |
| 11 | get_platforms | List all platforms for a test plan |
Inputs
Every tool that needs a project takes project_name. If the user hasn't named one, the tool
descriptions instruct the model to call get_projects first and ask.
| Tool | Required | Optional |
| --- | --- | --- |
| get_projects | — | — |
| get_test_suites | project_name | parent_suite_name |
| create_test_suite | project_name, suite_name | details, parent_suite_name |
| get_test_cases | project_name | test_suite_name, importance, automated |
| create_test_case | project_name, test_suite_name, test_case_name, summary, steps, expected_results, importance, execution_type | preconditions |
| update_test_case | project_name, test_suite_name, test_case_name | new_test_case_name, summary, preconditions, steps, importance, execution_type, custom_fields |
| get_custom_field | project_name, test_suite_name, test_case_name, custom_field_name | — |
| update_custom_field | project_name, test_suite_name, test_case_name, custom_fields | — |
| get_test_plans | project_name | — |
| get_builds | project_name, test_plan_name | — |
| get_platforms | project_name, test_plan_name | — |
| report_execution | project_name, test_plan_name, build_name, test_suite_name, test_case_name, status | platform_name, notes |
Field values and limits
importance—low|medium|highexecution_type—manual|automatedstatus(report_execution) —passed|failed|blockedsteps— array of{ actions, expected_results }, 1–100 entriescustom_fields—{ "FieldName": "value" }string pairs- Lengths — names 255 chars, free text 4000, custom field names 64
Notes on specific tools
get_test_caseswithouttest_suite_namewalks every suite in the project, which on a large project is a lot of XML-RPC round-trips. Pass a suite when you can.importanceandautomatedfilter the normalized results after fetching, not in the query.update_test_caseis a partial update — send only the fields you want changed. It resolves the test case by name and edits that version in place; it does not create a new version.report_executionrequiresplatform_namewhen the test plan has platforms assigned; callget_platformsfirst to find out. Without it TestLink rejects the execution.create_test_suitewithparent_suite_namenests the new suite; without it the suite is created at the project root. The suite tree is cached for 60s and is not invalidated on create, so a suite you just made can be reported asNOT_FOUNDby the next call until the TTL expires. (TestCaseResolutionService.invalidateSuites()exists for this but is currently never called.)
Response format
Every tool returns a single JSON text block, so results are readable in a transcript and parseable by the client.
Success
{
"ok": true,
"data": {
"count": 1,
"items": [
{
"test_case_id": "1421",
"external_id": "KIN-42",
"name": "Validate user can log in",
"summary": "...",
"steps": ["Open the login page", "Submit valid credentials"],
"expected_results": ["Login page renders", "Dashboard loads"],
"importance": "high",
"execution_type": "automated",
"suite_name": "Authentication"
}
]
}
}Failure — also flagged with MCP's isError, so the client shows it as a failed call:
{
"ok": false,
"error": { "code": "NOT_FOUND", "message": "Test suite not found: Authentcation" }
}| Code | Means |
| --- | --- |
| VALIDATION_ERROR | Input failed the tool's zod schema — wrong type, missing field, value outside the allowed set. |
| NOT_FOUND | A named project, suite, test case, plan, build or platform doesn't exist. |
| CONFLICT | Raised by create_test_case when the suite already holds a test case with that name. |
| INTEGRATION_ERROR | TestLink returned an error, or a response the gateway couldn't make sense of. |
| INTERNAL_ERROR | Anything else — an unexpected exception. |
Errors never throw out of a tool: executeTool catches everything, logs it with the tool name, and
returns the structured error above. A bad call fails that call, not the server.
Architecture
Controller (validation + response) → Service (business logic) → Gateway (XML-RPC adapter)Folder Structure
src/
config/ env.ts, logger.ts
controllers/ toolController.ts
integration/ testlink/ (mappers, gateway, xmlrpc client)
services/
projects/ getProjectsService
testCases/ create, get, update, customField, resolution, schemas
testSuites/ create, get
executions/ reportExecution, getTestPlans, getBuilds, getPlatforms
shared/ cache, errors, mcpResponse
tools/ registerTools.ts
types/ testlink.ts
index.ts
specs/
testcase.md sample BDD test cases, used as input when exercising the serverAdding a tool means: a zod shape in services/testCases/schemas.ts, a service class, a line in
index.ts to construct it, and a server.registerTool block in registerTools.ts.
Logs go to stderr via pino — stdout carries the MCP protocol, so writing to it would corrupt the
stream. Set LOG_LEVEL=debug to see each suite fetch and the raw TestLink payloads.
Troubleshooting
| Symptom | Cause |
| --- | --- |
| Exits at startup naming a variable | .env missing or a variable malformed — TESTLINK_API_ENDPOINT must parse as a URL. |
| Every call returns INTEGRATION_ERROR | Wrong endpoint path, XML-RPC disabled in TestLink, or an invalid dev key. |
| NOT_FOUND on a name you can see in TestLink | Matching is case-insensitive but exact otherwise — no trimming of inner whitespace, no partial match. |
| A just-created suite isn't found | Suite and project lookups are cached for 60s and nothing invalidates them on write. Wait out the TTL or restart the server. |
| report_execution rejected | The test plan has platforms — pass platform_name (get_platforms lists them). |
| Client shows the server as failed to start | The config points at dist/index.js without a build, or at a relative path. |
License
MIT
