Skip to main content
Glama
index.ts23 kB
#!/usr/bin/env node import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, GetPromptRequestSchema, ListPromptsRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, } from '@modelcontextprotocol/sdk/types.js'; import { TestRailClient } from './testrail-client.js'; import { TestRailConfig } from './types.js'; import { z } from 'zod'; import dotenv from 'dotenv'; // Load environment variables dotenv.config(); // Configuration schema const ConfigSchema = z.object({ url: z.string().url(), username: z.string().email(), apiKey: z.string().min(1), defaultProjectId: z.number().optional(), }); // Initialize configuration function getConfig(): TestRailConfig { const config = { url: process.env.TESTRAIL_URL || '', username: process.env.TESTRAIL_USERNAME || '', apiKey: process.env.TESTRAIL_API_KEY || '', defaultProjectId: process.env.DEFAULT_PROJECT_ID ? parseInt(process.env.DEFAULT_PROJECT_ID) : undefined, }; try { return ConfigSchema.parse(config); } catch (error) { console.error('Invalid configuration:', error); console.error('Please set TESTRAIL_URL, TESTRAIL_USERNAME, and TESTRAIL_API_KEY environment variables'); process.exit(1); } } const config = getConfig(); const testRailClient = new TestRailClient(config); // Create MCP server const server = new Server( { name: 'testrail-server', version: '1.0.0', }, { capabilities: { tools: {}, resources: {}, prompts: {}, }, } ); // List available tools server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: 'get_projects', description: 'Get all TestRail projects', inputSchema: { type: 'object', properties: {}, }, }, { name: 'get_project', description: 'Get a specific TestRail project by ID', inputSchema: { type: 'object', properties: { projectId: { type: 'number', description: 'The ID of the project (optional: uses DEFAULT_PROJECT_ID if not provided)', }, }, required: [], }, }, { name: 'get_test_cases', description: 'Get test cases from a project, optionally filtered by suite or section', inputSchema: { type: 'object', properties: { projectId: { type: 'number', description: 'The ID of the project (optional: uses DEFAULT_PROJECT_ID if not provided)', }, suiteId: { type: 'number', description: 'Optional: Filter by suite ID', }, sectionId: { type: 'number', description: 'Optional: Filter by section ID', }, }, required: [], }, }, { name: 'create_test_case', description: 'Create a new test case', inputSchema: { type: 'object', properties: { sectionId: { type: 'number', description: 'The ID of the section to create the test case in', }, title: { type: 'string', description: 'The title of the test case', }, type_id: { type: 'number', description: 'Optional: The type ID (default: 1)', }, priority_id: { type: 'number', description: 'Optional: The priority ID (1=Low, 2=Medium, 3=High, 4=Critical)', }, estimate: { type: 'string', description: 'Optional: Time estimate', }, refs: { type: 'string', description: 'Optional: References (e.g., requirements)', }, custom_steps_separated: { type: 'array', description: 'Optional: Test steps', }, }, required: ['sectionId', 'title'], }, }, { name: 'get_test_runs', description: 'Get test runs for a project', inputSchema: { type: 'object', properties: { projectId: { type: 'number', description: 'The ID of the project (optional: uses DEFAULT_PROJECT_ID if not provided)', }, }, required: [], }, }, { name: 'create_test_run', description: 'Create a new test run', inputSchema: { type: 'object', properties: { projectId: { type: 'number', description: 'The ID of the project (optional: uses DEFAULT_PROJECT_ID if not provided)', }, name: { type: 'string', description: 'The name of the test run', }, description: { type: 'string', description: 'Optional: Description of the test run', }, suiteId: { type: 'number', description: 'Optional: The suite ID', }, milestoneId: { type: 'number', description: 'Optional: The milestone ID', }, assignedtoId: { type: 'number', description: 'Optional: User ID to assign the run to', }, include_all: { type: 'boolean', description: 'Whether to include all test cases (default: true)', }, }, required: ['name'], }, }, { name: 'add_test_result', description: 'Add a test result', inputSchema: { type: 'object', properties: { runId: { type: 'number', description: 'The ID of the test run', }, caseId: { type: 'number', description: 'The ID of the test case', }, status_id: { type: 'number', description: 'Status ID (1=Passed, 2=Blocked, 3=Untested, 4=Retest, 5=Failed)', }, comment: { type: 'string', description: 'Optional: Comment about the result', }, version: { type: 'string', description: 'Optional: Version tested', }, elapsed: { type: 'string', description: 'Optional: Time elapsed (e.g., "5m" or "1h 30m")', }, defects: { type: 'string', description: 'Optional: Bug/defect references', }, }, required: ['runId', 'caseId', 'status_id'], }, }, { name: 'get_users', description: 'Get all TestRail users', inputSchema: { type: 'object', properties: {}, }, }, { name: 'test_connection', description: 'Test the connection to TestRail', inputSchema: { type: 'object', properties: {}, }, }, { name: 'parse_testrail_url', description: 'Parse a TestRail URL and automatically call the appropriate tool', inputSchema: { type: 'object', properties: { url: { type: 'string', description: 'The TestRail URL to parse (e.g., test case, test run, project URLs)', }, }, required: ['url'], }, }, ], }; }); // Handle tool calls server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { switch (name) { case 'get_projects': { const projects = await testRailClient.getProjects(); return { content: [ { type: 'text', text: JSON.stringify(projects, null, 2), }, ], }; } case 'get_project': { const { projectId } = args as { projectId?: number }; const finalProjectId = projectId || testRailClient.getDefaultProjectId(); if (!finalProjectId) { throw new Error('No projectId provided and no DEFAULT_PROJECT_ID configured'); } const project = await testRailClient.getProject(finalProjectId); return { content: [ { type: 'text', text: JSON.stringify(project, null, 2), }, ], }; } case 'get_test_cases': { const { projectId, suiteId, sectionId } = args as { projectId?: number; suiteId?: number; sectionId?: number; }; const finalProjectId = projectId || testRailClient.getDefaultProjectId(); if (!finalProjectId) { throw new Error('No projectId provided and no DEFAULT_PROJECT_ID configured'); } const testCases = await testRailClient.getTestCases(finalProjectId, suiteId, sectionId); return { content: [ { type: 'text', text: JSON.stringify(testCases, null, 2), }, ], }; } case 'create_test_case': { const { sectionId, ...caseData } = args as any; const testCase = await testRailClient.createTestCase(sectionId, caseData); return { content: [ { type: 'text', text: JSON.stringify(testCase, null, 2), }, ], }; } case 'get_test_runs': { const { projectId } = args as { projectId?: number }; const finalProjectId = projectId || testRailClient.getDefaultProjectId(); if (!finalProjectId) { throw new Error('No projectId provided and no DEFAULT_PROJECT_ID configured'); } const testRuns = await testRailClient.getTestRuns(finalProjectId); return { content: [ { type: 'text', text: JSON.stringify(testRuns, null, 2), }, ], }; } case 'create_test_run': { const { projectId, ...runData } = args as any; const finalProjectId = projectId || testRailClient.getDefaultProjectId(); if (!finalProjectId) { throw new Error('No projectId provided and no DEFAULT_PROJECT_ID configured'); } const testRun = await testRailClient.createTestRun(finalProjectId, { include_all: true, ...runData, }); return { content: [ { type: 'text', text: JSON.stringify(testRun, null, 2), }, ], }; } case 'add_test_result': { const { runId, caseId, ...resultData } = args as any; const result = await testRailClient.addTestResultForCase(runId, caseId, resultData); return { content: [ { type: 'text', text: JSON.stringify(result, null, 2), }, ], }; } case 'get_users': { const users = await testRailClient.getUsers(); return { content: [ { type: 'text', text: JSON.stringify(users, null, 2), }, ], }; } case 'test_connection': { const isConnected = await testRailClient.testConnection(); const defaultProjectId = testRailClient.getDefaultProjectId(); let message = isConnected ? 'Connection to TestRail successful!' : 'Failed to connect to TestRail. Please check your configuration.'; if (isConnected && defaultProjectId) { message += `\nDefault Project ID: ${defaultProjectId}`; } else if (isConnected) { message += '\nNo default project ID configured.'; } return { content: [ { type: 'text', text: message, }, ], }; } case 'parse_testrail_url': { const { url } = args as { url: string }; try { const urlObj = new URL(url); const path = urlObj.pathname + urlObj.search; // Parse different TestRail URL patterns // Test Case View: /cases/view/{case_id} const caseViewMatch = path.match(/\/cases\/view\/(\d+)/); if (caseViewMatch) { const caseId = parseInt(caseViewMatch[1]); const testCase = await testRailClient.getTestCase(caseId); return { content: [ { type: 'text', text: `Detected TestRail test case URL. Retrieved case ID ${caseId}:\n\n${JSON.stringify(testCase, null, 2)}`, }, ], }; } // Test Run View: /runs/view/{run_id} const runViewMatch = path.match(/\/runs\/view\/(\d+)/); if (runViewMatch) { const runId = parseInt(runViewMatch[1]); const testRun = await testRailClient.getTestRun(runId); return { content: [ { type: 'text', text: `Detected TestRail test run URL. Retrieved run ID ${runId}:\n\n${JSON.stringify(testRun, null, 2)}`, }, ], }; } // Project View: /projects/overview/{project_id} const projectViewMatch = path.match(/\/projects\/overview\/(\d+)/); if (projectViewMatch) { const projectId = parseInt(projectViewMatch[1]); const project = await testRailClient.getProject(projectId); return { content: [ { type: 'text', text: `Detected TestRail project URL. Retrieved project ID ${projectId}:\n\n${JSON.stringify(project, null, 2)}`, }, ], }; } // Test Cases List: /cases/{project_id} const casesListMatch = path.match(/\/cases\/(\d+)/); if (casesListMatch) { const projectId = parseInt(casesListMatch[1]); // Extract suite_id and section_id from query parameters if present const suiteId = urlObj.searchParams.get('suite_id') ? parseInt(urlObj.searchParams.get('suite_id')!) : undefined; const sectionId = urlObj.searchParams.get('section_id') ? parseInt(urlObj.searchParams.get('section_id')!) : undefined; const testCases = await testRailClient.getTestCases(projectId, suiteId, sectionId); let message = `Detected TestRail test cases list URL. Retrieved test cases for project ID ${projectId}`; if (suiteId) message += `, suite ID ${suiteId}`; if (sectionId) message += `, section ID ${sectionId}`; return { content: [ { type: 'text', text: `${message}:\n\n${JSON.stringify(testCases, null, 2)}`, }, ], }; } // Test Runs List: /runs/{project_id} const runsListMatch = path.match(/\/runs\/(\d+)/); if (runsListMatch) { const projectId = parseInt(runsListMatch[1]); const testRuns = await testRailClient.getTestRuns(projectId); return { content: [ { type: 'text', text: `Detected TestRail test runs list URL. Retrieved test runs for project ID ${projectId}:\n\n${JSON.stringify(testRuns, null, 2)}`, }, ], }; } // If no pattern matches, provide helpful message return { content: [ { type: 'text', text: `URL parsed but no matching TestRail pattern found. Supported patterns: - Test Case: /cases/view/{case_id} - Test Run: /runs/view/{run_id} - Project: /projects/overview/{project_id} - Test Cases List: /cases/{project_id} - Test Runs List: /runs/{project_id} Provided URL path: ${path}`, }, ], }; } catch (error) { throw new Error(`Failed to parse URL: ${error instanceof Error ? error.message : 'Invalid URL format'}`); } } default: throw new Error(`Unknown tool: ${name}`); } } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; return { content: [ { type: 'text', text: `Error: ${errorMessage}`, }, ], isError: true, }; } }); // List available resources server.setRequestHandler(ListResourcesRequestSchema, async () => { return { resources: [ { uri: 'testrail://projects', name: 'TestRail Projects', description: 'List of all TestRail projects', mimeType: 'application/json', }, { uri: 'testrail://users', name: 'TestRail Users', description: 'List of all TestRail users', mimeType: 'application/json', }, ], }; }); // Handle resource requests server.setRequestHandler(ReadResourceRequestSchema, async (request) => { const { uri } = request.params; try { switch (uri) { case 'testrail://projects': { const projects = await testRailClient.getProjects(); return { contents: [ { uri, mimeType: 'application/json', text: JSON.stringify(projects, null, 2), }, ], }; } case 'testrail://users': { const users = await testRailClient.getUsers(); return { contents: [ { uri, mimeType: 'application/json', text: JSON.stringify(users, null, 2), }, ], }; } default: throw new Error(`Unknown resource: ${uri}`); } } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; throw new Error(`Failed to read resource ${uri}: ${errorMessage}`); } }); // List available prompts server.setRequestHandler(ListPromptsRequestSchema, async () => { return { prompts: [ { name: 'test_case_template', description: 'Generate a test case template', arguments: [ { name: 'feature', description: 'The feature being tested', required: true, }, { name: 'priority', description: 'Priority level (Low, Medium, High, Critical)', required: false, }, ], }, { name: 'test_run_summary', description: 'Generate a test run summary report', arguments: [ { name: 'projectId', description: 'The project ID', required: true, }, { name: 'runId', description: 'The test run ID', required: true, }, ], }, ], }; }); // Handle prompt requests server.setRequestHandler(GetPromptRequestSchema, async (request) => { const { name, arguments: args } = request.params; switch (name) { case 'test_case_template': { const feature = args?.feature || 'Feature Name'; const priority = args?.priority || 'Medium'; return { messages: [ { role: 'user', content: { type: 'text', text: `Create a comprehensive test case for the following feature: ${feature} Priority: ${priority} Please include: 1. Test Case Title 2. Description 3. Preconditions 4. Test Steps (numbered) 5. Expected Results 6. Post-conditions (if any) Format the test case in a clear, structured way that follows testing best practices.`, }, }, ], }; } case 'test_run_summary': { const projectId = args?.projectId; const runId = args?.runId; if (!projectId || !runId) { throw new Error('Both projectId and runId are required'); } try { const testRun = await testRailClient.getTestRun(Number(runId)); const project = await testRailClient.getProject(Number(projectId)); const totalTests = testRun.passed_count + testRun.failed_count + testRun.blocked_count + testRun.untested_count + testRun.retest_count; const passRate = totalTests > 0 ? ((testRun.passed_count / totalTests) * 100).toFixed(1) : '0'; return { messages: [ { role: 'user', content: { type: 'text', text: `Generate a comprehensive test run summary report for: Project: ${project.name} Test Run: ${testRun.name} Status: ${testRun.is_completed ? 'Completed' : 'In Progress'} Test Results: - Total Tests: ${totalTests} - Passed: ${testRun.passed_count} - Failed: ${testRun.failed_count} - Blocked: ${testRun.blocked_count} - Untested: ${testRun.untested_count} - Retest: ${testRun.retest_count} - Pass Rate: ${passRate}% Please create a detailed summary report that includes: 1. Executive Summary 2. Test Execution Overview 3. Results Analysis 4. Key Findings 5. Recommendations 6. Next Steps Make the report professional and suitable for stakeholders.`, }, }, ], }; } catch (error) { throw new Error(`Failed to generate test run summary: ${error instanceof Error ? error.message : 'Unknown error'}`); } } default: throw new Error(`Unknown prompt: ${name}`); } }); // Start the server async function main() { // Test connection on startup console.error('Testing connection to TestRail...'); const isConnected = await testRailClient.testConnection(); if (!isConnected) { console.error('Failed to connect to TestRail. Please check your configuration.'); process.exit(1); } console.error('Connected to TestRail successfully!'); const transport = new StdioServerTransport(); await server.connect(transport); console.error('TestRail MCP server running on stdio'); } main().catch((error) => { console.error('Server error:', error); process.exit(1); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/moneyforward/mcp-testrail'

If you have feedback or need assistance with the MCP directory API, please join our Discord server