Skip to main content
Glama
Derrbal
by Derrbal
http-server.ts50.4 kB
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { z } from 'zod'; import { getCase, updateCase, getProjects, getProject, getSuites, getSuite, getCases, addAttachmentToCase, getSections, getRuns, getRun, getTests, getTest, updateTest, updateRun, addResult } from './services/testrailService'; import * as http from 'http'; import * as url from 'url'; async function main(): Promise<void> { console.log('Starting TestRail MCP HTTP server...'); const server = new McpServer({ name: 'testrail-mcp', version: '0.1.0', }); // Register all tools (same as original server.ts) console.log('Registering get_case tool...'); server.registerTool( 'get_case', { title: 'Get TestRail Case', description: 'Fetch a TestRail test case by ID.', inputSchema: { case_id: z.number().int().positive().describe('TestRail case ID'), }, }, async ({ case_id }) => { console.log(`Tool called with case_id: ${case_id}`); try { const result = await getCase(case_id); console.log(`Tool completed successfully for case_id: ${case_id}`); return { content: [ { type: 'text', text: JSON.stringify(result, null, 2), }, ], }; } catch (err) { console.log(`Tool failed for case_id: ${case_id}`, err); const e = err as { type?: string; status?: number; message?: string }; let message = 'Unexpected error'; if (e?.type === 'auth') message = 'Authentication failed: check TESTRAIL_USER/API_KEY'; else if (e?.type === 'not_found') message = `Case ${case_id} not found`; else if (e?.type === 'rate_limited') message = 'Rate limited by TestRail; try again later'; else if (e?.type === 'server') message = 'TestRail server error'; else if (e?.type === 'network') message = 'Network error contacting TestRail'; else if (e?.message) message = e.message; return { content: [ { type: 'text', text: message }, ], isError: true, }; } }, ); console.log('Registering update_case tool...'); server.registerTool( 'update_case', { title: 'Update TestRail Case', description: 'Update a TestRail test case by ID with new field values.', inputSchema: { case_id: z.number().int().positive().describe('TestRail case ID'), title: z.string().min(1).optional().describe('Test case title'), section_id: z.number().int().positive().optional().describe('Section ID'), type_id: z.number().int().positive().optional().describe('Test case type ID'), priority_id: z.number().int().positive().optional().describe('Priority ID'), refs: z.string().nullable().optional().describe('References (e.g., requirement IDs)'), custom: z.record(z.string(), z.unknown()).optional().describe('Custom fields (key-value pairs)'), }, }, async ({ case_id, title, section_id, type_id, priority_id, refs, custom }) => { console.log(`Update case tool called with case_id: ${case_id}`); try { const updates = { title, section_id, type_id, priority_id, refs, custom, }; // Remove undefined values to avoid sending empty fields const cleanUpdates = Object.fromEntries( Object.entries(updates).filter(([, value]) => value !== undefined) ); const result = await updateCase(case_id, cleanUpdates); console.log(`Update case tool completed successfully for case_id: ${case_id}`); return { content: [ { type: 'text', text: JSON.stringify(result, null, 2), }, ], }; } catch (err) { console.log(`Update case tool failed for case_id: ${case_id}`, err); const e = err as { type?: string; status?: number; message?: string }; let message = 'Unexpected error'; if (e?.type === 'auth') message = 'Authentication failed: check TESTRAIL_USER/API_KEY'; else if (e?.type === 'not_found') message = `Case ${case_id} not found`; else if (e?.type === 'rate_limited') message = 'Rate limited by TestRail; try again later'; else if (e?.type === 'server') message = 'TestRail server error'; else if (e?.type === 'network') message = 'Network error contacting TestRail'; else if (e?.message) message = e.message; return { content: [ { type: 'text', text: message }, ], isError: true, }; } }, ); console.log('Registering get_projects tool...'); server.registerTool( 'get_projects', { title: 'Get TestRail Projects', description: 'List all TestRail projects.', inputSchema: {}, // No parameters required }, async () => { console.log('Get projects tool called'); try { const result = await getProjects(); console.log(`Get projects tool completed successfully. Found ${result.length} projects`); return { content: [ { type: 'text', text: JSON.stringify(result, null, 2), }, ], }; } catch (err) { console.log('Get projects tool failed', err); const e = err as { type?: string; status?: number; message?: string }; let message = 'Unexpected error'; if (e?.type === 'auth') message = 'Authentication failed: check TESTRAIL_USER/API_KEY'; else if (e?.type === 'rate_limited') message = 'Rate limited by TestRail; try again later'; else if (e?.type === 'server') message = 'TestRail server error'; else if (e?.type === 'network') message = 'Network error contacting TestRail'; else if (e?.message) message = e.message; return { content: [ { type: 'text', text: message }, ], isError: true, }; } }, ); console.log('Registering get_project tool...'); server.registerTool( 'get_project', { title: 'Get TestRail Project', description: 'Get details for a specific TestRail project by ID.', inputSchema: { project_id: z.number().int().positive().describe('TestRail project ID'), }, }, async ({ project_id }) => { console.log(`Get project tool called with project_id: ${project_id}`); try { const result = await getProject(project_id); console.log(`Get project tool completed successfully for project_id: ${project_id}`); return { content: [ { type: 'text', text: JSON.stringify(result, null, 2), }, ], }; } catch (err) { console.log(`Get project tool failed for project_id: ${project_id}`, err); const e = err as { type?: string; status?: number; message?: string }; let message = 'Unexpected error'; if (e?.type === 'auth') message = 'Authentication failed: check TESTRAIL_USER/API_KEY'; else if (e?.type === 'not_found') message = `Project ${project_id} not found`; else if (e?.type === 'rate_limited') message = 'Rate limited by TestRail; try again later'; else if (e?.type === 'server') message = 'TestRail server error'; else if (e?.type === 'network') message = 'Network error contacting TestRail'; else if (e?.message) message = e.message; return { content: [ { type: 'text', text: message }, ], isError: true, }; } }, ); console.log('Registering get_suites tool...'); server.registerTool( 'get_suites', { title: 'Get TestRail Suites', description: 'Get all test suites for a specific TestRail project by ID.', inputSchema: { project_id: z.number().int().positive().describe('TestRail project ID'), }, }, async ({ project_id }) => { console.log(`Get suites tool called with project_id: ${project_id}`); try { const result = await getSuites(project_id); console.log(`Get suites tool completed successfully for project_id: ${project_id}. Found ${result.length} suites`); return { content: [ { type: 'text', text: JSON.stringify(result, null, 2), }, ], }; } catch (err) { console.log(`Get suites tool failed for project_id: ${project_id}`, err); const e = err as { type?: string; status?: number; message?: string }; let message = 'Unexpected error'; if (e?.type === 'auth') message = 'Authentication failed: check TESTRAIL_USER/API_KEY'; else if (e?.type === 'not_found') message = `Project ${project_id} not found`; else if (e?.type === 'rate_limited') message = 'Rate limited by TestRail; try again later'; else if (e?.type === 'server') message = 'TestRail server error'; else if (e?.type === 'network') message = 'Network error contacting TestRail'; else if (e?.message) message = e.message; return { content: [ { type: 'text', text: message }, ], isError: true, }; } }, ); console.log('Registering get_suite tool...'); server.registerTool( 'get_suite', { title: 'Get TestRail Suite', description: 'Get details for a specific TestRail test suite by ID.', inputSchema: { suite_id: z.number().int().positive().describe('TestRail suite ID'), }, }, async ({ suite_id }) => { console.log(`Get suite tool called with suite_id: ${suite_id}`); try { const result = await getSuite(suite_id); console.log(`Get suite tool completed successfully for suite_id: ${suite_id}`); return { content: [ { type: 'text', text: JSON.stringify(result, null, 2), }, ], }; } catch (err) { console.log(`Get suite tool failed for suite_id: ${suite_id}`, err); const e = err as { type?: string; status?: number; message?: string }; let message = 'Unexpected error'; if (e?.type === 'auth') message = 'Authentication failed: check TESTRAIL_USER/API_KEY'; else if (e?.type === 'not_found') message = `Suite ${suite_id} not found`; else if (e?.type === 'rate_limited') message = 'Rate limited by TestRail; try again later'; else if (e?.type === 'server') message = 'TestRail server error'; else if (e?.type === 'network') message = 'Network error contacting TestRail'; else if (e?.message) message = e.message; return { content: [ { type: 'text', text: message }, ], isError: true, }; } }, ); console.log('Registering get_cases tool...'); server.registerTool( 'get_cases', { title: 'Get TestRail Cases', description: 'Get a list of test cases for a project or specific test suite with optional filtering and pagination.', inputSchema: { project_id: z.number().int().positive().describe('TestRail project ID'), suite_id: z.number().int().positive().optional().describe('TestRail suite ID (optional if project is in single suite mode)'), created_after: z.number().int().optional().describe('Only return test cases created after this date (as UNIX timestamp)'), created_before: z.number().int().optional().describe('Only return test cases created before this date (as UNIX timestamp)'), created_by: z.array(z.number().int().positive()).optional().describe('A list of creator user IDs to filter by'), filter: z.string().optional().describe('Only return cases with matching filter string in the case title'), limit: z.number().int().positive().max(250).optional().describe('The number of test cases to return (max 250, default 250)'), milestone_id: z.array(z.number().int().positive()).optional().describe('A list of milestone IDs to filter by'), offset: z.number().int().min(0).optional().describe('Where to start counting the test cases from (pagination offset)'), priority_id: z.array(z.number().int().positive()).optional().describe('A list of priority IDs to filter by'), refs: z.string().optional().describe('A single Reference ID (e.g. TR-1, 4291, etc.)'), section_id: z.number().int().positive().optional().describe('The ID of a test case section'), template_id: z.array(z.number().int().positive()).optional().describe('A list of template IDs to filter by'), type_id: z.array(z.number().int().positive()).optional().describe('A list of case type IDs to filter by'), updated_after: z.number().int().optional().describe('Only return test cases updated after this date (as UNIX timestamp)'), updated_before: z.number().int().optional().describe('Only return test cases updated before this date (as UNIX timestamp)'), updated_by: z.number().int().positive().optional().describe('A user ID who updated test cases to filter by'), label_id: z.array(z.number().int().positive()).optional().describe('A list of label IDs to filter by'), }, }, async ({ project_id, suite_id, created_after, created_before, created_by, filter, limit, milestone_id, offset, priority_id, refs, section_id, template_id, type_id, updated_after, updated_before, updated_by, label_id }) => { console.log(`Get cases tool called with project_id: ${project_id}, suite_id: ${suite_id}`); try { const filters = { project_id, ...(suite_id !== undefined && { suite_id }), ...(created_after !== undefined && { created_after }), ...(created_before !== undefined && { created_before }), ...(created_by !== undefined && { created_by }), ...(filter !== undefined && { filter }), ...(limit !== undefined && { limit }), ...(milestone_id !== undefined && { milestone_id }), ...(offset !== undefined && { offset }), ...(priority_id !== undefined && { priority_id }), ...(refs !== undefined && { refs }), ...(section_id !== undefined && { section_id }), ...(template_id !== undefined && { template_id }), ...(type_id !== undefined && { type_id }), ...(updated_after !== undefined && { updated_after }), ...(updated_before !== undefined && { updated_before }), ...(updated_by !== undefined && { updated_by }), ...(label_id !== undefined && { label_id }), }; const result = await getCases(filters); console.log(`Get cases tool completed successfully for project_id: ${project_id}. Found ${result.cases.length} cases (total: ${result.size})`); return { content: [ { type: 'text', text: JSON.stringify(result, null, 2), }, ], }; } catch (err) { console.log(`Get cases tool failed for project_id: ${project_id}`, err); const e = err as { type?: string; status?: number; message?: string }; let message = 'Unexpected error'; if (e?.type === 'auth') message = 'Authentication failed: check TESTRAIL_USER/API_KEY'; else if (e?.type === 'not_found') message = `Project ${project_id} or suite ${suite_id} not found`; else if (e?.type === 'rate_limited') message = 'Rate limited by TestRail; try again later'; else if (e?.type === 'server') message = 'TestRail server error'; else if (e?.type === 'network') message = 'Network error contacting TestRail'; else if (e?.message) message = e.message; return { content: [ { type: 'text', text: message }, ], isError: true, }; } }, ); console.log('Registering add_attachment_to_case tool...'); server.registerTool( 'add_attachment_to_case', { title: 'Add Attachment to TestRail Case', description: 'Upload a file attachment to a TestRail test case.', inputSchema: { case_id: z.number().int().positive().describe('TestRail case ID'), file_path: z.string().min(1).describe('Path to the file to upload as attachment'), }, }, async ({ case_id, file_path }) => { console.log(`Add attachment tool called with case_id: ${case_id}, file_path: ${file_path}`); try { const result = await addAttachmentToCase(case_id, file_path); console.log(`Add attachment tool completed successfully for case_id: ${case_id}, attachment_id: ${result.attachment_id}`); return { content: [ { type: 'text', text: JSON.stringify(result, null, 2), }, ], }; } catch (err) { console.log(`Add attachment tool failed for case_id: ${case_id}, file_path: ${file_path}`, err); const e = err as { type?: string; status?: number; message?: string }; let message = 'Unexpected error'; if (e?.type === 'auth') message = 'Authentication failed: check TESTRAIL_USER/API_KEY'; else if (e?.type === 'not_found') message = `Case ${case_id} not found`; else if (e?.type === 'rate_limited') message = 'Rate limited by TestRail; try again later'; else if (e?.type === 'server') message = 'TestRail server error'; else if (e?.type === 'network') message = 'Network error contacting TestRail'; else if (e?.message) message = e.message; return { content: [ { type: 'text', text: message }, ], isError: true, }; } }, ); console.log('Registering get_sections tool...'); server.registerTool( 'get_sections', { title: 'Get TestRail Sections', description: 'Get a list of sections for a project and test suite with optional pagination.', inputSchema: { project_id: z.number().int().positive().describe('TestRail project ID'), suite_id: z.number().int().positive().optional().describe('TestRail suite ID (optional if project is in single suite mode)'), limit: z.number().int().positive().optional().describe('The number of sections to return (max 250, default 250)'), offset: z.number().int().min(0).optional().describe('Where to start counting the sections from (pagination offset)'), }, }, async ({ project_id, suite_id, limit, offset }) => { console.log(`Get sections tool called with project_id: ${project_id}, suite_id: ${suite_id}`); try { const filters = { project_id, ...(suite_id !== undefined && { suite_id }), ...(limit !== undefined && { limit }), ...(offset !== undefined && { offset }), }; const result = await getSections(filters); console.log(`Get sections tool completed successfully for project_id: ${project_id}. Found ${result.sections.length} sections (total: ${result.size})`); return { content: [ { type: 'text', text: JSON.stringify(result, null, 2), }, ], }; } catch (err) { console.log(`Get sections tool failed for project_id: ${project_id}`, err); const e = err as { type?: string; status?: number; message?: string }; let message = 'Unexpected error'; if (e?.type === 'auth') message = 'Authentication failed: check TESTRAIL_USER/API_KEY'; else if (e?.type === 'not_found') message = `Project ${project_id} or suite ${suite_id} not found`; else if (e?.type === 'rate_limited') message = 'Rate limited by TestRail; try again later'; else if (e?.type === 'server') message = 'TestRail server error'; else if (e?.type === 'network') message = 'Network error contacting TestRail'; else if (e?.message) message = e.message; return { content: [ { type: 'text', text: message }, ], isError: true, }; } }, ); console.log('Registering get_runs tool...'); server.registerTool( 'get_runs', { title: 'Get TestRail Runs', description: 'Get a list of test runs for a project with optional filtering and pagination. Only returns test runs that are not part of a test plan.', inputSchema: { project_id: z.number().int().positive().describe('TestRail project ID'), created_after: z.number().int().optional().describe('Only return test runs created after this date (as UNIX timestamp)'), created_before: z.number().int().optional().describe('Only return test runs created before this date (as UNIX timestamp)'), created_by: z.array(z.number().int().positive()).optional().describe('A comma-separated list of creators (user IDs) to filter by'), is_completed: z.boolean().optional().describe('1 to return completed test runs only. 0 to return active test runs only'), limit: z.number().int().positive().optional().describe('The number of test runs to return (max 250, default 250)'), milestone_id: z.array(z.number().int().positive()).optional().describe('A comma-separated list of milestone IDs to filter by'), offset: z.number().int().min(0).optional().describe('Where to start counting the test runs from (pagination offset)'), refs_filter: z.string().optional().describe('A single Reference ID (e.g. TR-a, 4291, etc.)'), suite_id: z.array(z.number().int().positive()).optional().describe('A comma-separated list of test suite IDs to filter by'), }, }, async ({ project_id, created_after, created_before, created_by, is_completed, limit, milestone_id, offset, refs_filter, suite_id }) => { console.log(`Get runs tool called with project_id: ${project_id}`); const filters = { project_id, created_after, created_before, created_by, is_completed, limit, milestone_id, offset, refs_filter, suite_id, }; const result = await getRuns(filters); console.log(`Get runs tool completed. Found ${result.runs.length} runs.`); return { content: [ { type: 'text', text: JSON.stringify(result, null, 2), }, ], }; }, ); console.log('Registering get_run tool...'); server.registerTool( 'get_run', { title: 'Get TestRail Run', description: 'Returns an existing test run. Please see get tests for the list of included tests in this run.', inputSchema: { run_id: z.number().int().positive().describe('The ID of the test run'), }, }, async ({ run_id }) => { console.log(`Get run tool called with run_id: ${run_id}`); const result = await getRun(run_id); console.log(`Get run tool completed. Retrieved run: ${result.name}`); return { content: [ { type: 'text', text: JSON.stringify(result, null, 2), }, ], }; }, ); console.log('Registering update_run tool...'); server.registerTool( 'update_run', { title: 'Update TestRail Run', description: 'Updates an existing test run. Partial updates are supported.', inputSchema: { run_id: z.number().int().positive().describe('The ID of the test run to be updated'), name: z.string().min(1).optional().describe('The name of the test run'), description: z.string().optional().describe('The description of the test run'), milestone_id: z.number().int().positive().optional().describe('The ID of the milestone'), include_all: z.boolean().optional().describe('True for including all test cases and false for a custom case selection'), case_ids: z.array(z.number().int().positive()).optional().describe('An array of case IDs for the custom case selection'), config: z.string().optional().describe('A comma-separated list of configuration IDs'), config_ids: z.array(z.number().int().positive()).optional().describe('An array of configuration IDs'), refs: z.string().optional().describe('A string of external requirements'), start_on: z.number().int().optional().describe('The start date (Unix timestamp)'), due_on: z.number().int().optional().describe('The due date (Unix timestamp)'), custom: z.record(z.string(), z.unknown()).optional().describe('Custom fields (key-value pairs)'), }, }, async ({ run_id, name, description, milestone_id, include_all, case_ids, config, config_ids, refs, start_on, due_on, custom }) => { console.log(`Update run tool called with run_id: ${run_id}`); try { const updates = { name, description, milestone_id, include_all, case_ids, config, config_ids, refs, start_on, due_on, custom, }; // Remove undefined values to avoid sending empty fields const cleanUpdates = Object.fromEntries( Object.entries(updates).filter(([, value]) => value !== undefined) ); const result = await updateRun(run_id, cleanUpdates); console.log(`Update run tool completed successfully for run_id: ${run_id}`); return { content: [ { type: 'text', text: JSON.stringify(result, null, 2), }, ], }; } catch (err) { console.log(`Update run tool failed for run_id: ${run_id}`, err); const e = err as { type?: string; status?: number; message?: string }; let message = 'Unexpected error'; if (e?.type === 'auth') message = 'Authentication failed: check TESTRAIL_USER/API_KEY'; else if (e?.type === 'not_found') message = `Run ${run_id} not found`; else if (e?.type === 'rate_limited') message = 'Rate limited by TestRail; try again later'; else if (e?.type === 'server') message = 'TestRail server error'; else if (e?.type === 'network') message = 'Network error contacting TestRail'; else if (e?.message) message = e.message; return { content: [ { type: 'text', text: message }, ], isError: true, }; } }, ); console.log('Registering get_tests tool...'); server.registerTool( 'get_tests', { title: 'Get TestRail Tests', description: 'Returns a list of tests for a test run.', inputSchema: { run_id: z.number().int().positive().describe('The ID of the test run'), status_id: z.array(z.number().int().positive()).optional().describe('A comma-separated list of status IDs to filter by'), limit: z.number().int().positive().optional().describe('The number that sets the limit of tests to be shown on the response (max 250, default 250)'), offset: z.number().int().min(0).optional().describe('The number that sets the position where the response should start from (pagination offset)'), label_id: z.array(z.number().int().positive()).optional().describe('IDs of labels as comma separated values to filter by'), }, }, async ({ run_id, status_id, limit, offset, label_id }) => { console.log(`Get tests tool called with run_id: ${run_id}`); const filters = { run_id, status_id, limit, offset, label_id, }; const result = await getTests(filters); console.log(`Get tests tool completed. Found ${result.tests.length} tests.`); return { content: [ { type: 'text', text: JSON.stringify(result, null, 2), }, ], }; }, ); console.log('Registering get_test tool...'); server.registerTool( 'get_test', { title: 'Get TestRail Test', description: 'Returns an existing test.', inputSchema: { test_id: z.number().int().positive().describe('The ID of the test'), with_data: z.string().optional().describe('The parameter to get data'), }, }, async ({ test_id, with_data }) => { console.log(`Get test tool called with test_id: ${test_id}`); const filters = { test_id, with_data, }; const result = await getTest(filters); console.log(`Get test tool completed. Retrieved test: ${result.title}`); return { content: [ { type: 'text', text: JSON.stringify(result, null, 2), }, ], }; }, ); console.log('Registering update_test tool...'); server.registerTool( 'update_test', { title: 'Update TestRail Test', description: 'Updates the labels assigned to an existing test.', inputSchema: { test_id: z.number().int().positive().describe('The ID of the test to be updated'), labels: z.array(z.union([z.number().int().positive(), z.string()])).optional().describe('The ID of a label, the title of a label or both, in array form'), custom: z.record(z.string(), z.unknown()).optional().describe('Custom fields (key-value pairs)'), }, }, async ({ test_id, labels, custom }) => { console.log(`Update test tool called with test_id: ${test_id}`); try { const updates = { labels, custom, }; // Remove undefined values to avoid sending empty fields const cleanUpdates = Object.fromEntries( Object.entries(updates).filter(([, value]) => value !== undefined) ); const result = await updateTest(test_id, cleanUpdates); console.log(`Update test tool completed successfully for test_id: ${test_id}`); return { content: [ { type: 'text', text: JSON.stringify(result, null, 2), }, ], }; } catch (err) { console.log(`Update test tool failed for test_id: ${test_id}`, err); const e = err as { type?: string; status?: number; message?: string }; let message = 'Unexpected error'; if (e?.type === 'auth') message = 'Authentication failed: check TESTRAIL_USER/API_KEY'; else if (e?.type === 'not_found') message = `Test ${test_id} not found`; else if (e?.type === 'rate_limited') message = 'Rate limited by TestRail; try again later'; else if (e?.type === 'server') message = 'TestRail server error'; else if (e?.type === 'network') message = 'Network error contacting TestRail'; else if (e?.message) message = e.message; return { content: [ { type: 'text', text: message }, ], isError: true, }; } }, ); console.log('Registering add_result tool...'); server.registerTool( 'add_result', { title: 'Add TestRail Result', description: 'Adds a new test result, comment, or assigns a test.', inputSchema: { test_id: z.number().int().positive().describe('The ID of the test to which the result should be added'), status_id: z.number().int().positive().describe('The ID of the test status (1=Passed, 2=Blocked, 4=Retest, 5=Failed)'), comment: z.string().optional().describe('The comment or description for the test result'), version: z.string().optional().describe('The version or build against which the test was executed'), elapsed: z.string().optional().describe('The time it took to execute the test (e.g., "30s" or "1m 45s")'), defects: z.string().optional().describe('A comma-separated list of defects to link to the test result'), assignedto_id: z.number().int().positive().optional().describe('The ID of a user to whom the test should be assigned'), custom_step_results: z.array(z.object({ content: z.string().describe('The test step content'), expected: z.string().describe('The expected result for the step'), actual: z.string().describe('The actual result for the step'), status_id: z.number().int().positive().describe('The status ID for the step (1=Passed, 2=Blocked, 4=Retest, 5=Failed)') })).optional().describe('Array of step results for structured testing'), custom: z.record(z.unknown()).optional().describe('Custom fields with custom_ prefix'), }, }, async ({ test_id, status_id, comment, version, elapsed, defects, assignedto_id, custom_step_results, custom }) => { console.log(`Add result tool called with test_id: ${test_id}, status_id: ${status_id}`); const filters = { test_id, status_id, comment, version, elapsed, defects, assignedto_id, custom_step_results, custom, }; const result = await addResult(filters); console.log(`Add result tool completed. Result ID: ${result.id}`); return { content: [ { type: 'text', text: JSON.stringify(result, null, 2), }, ], }; }, ); // Create HTTP server wrapper for MCP-over-HTTP with WebSocket support const httpServer = http.createServer(async (req, res) => { const parsedUrl = url.parse(req.url || '', true); const path = parsedUrl.pathname; res.setHeader('Content-Type', 'application/json'); res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization'); res.setHeader('Access-Control-Allow-Credentials', 'true'); if (req.method === 'OPTIONS') { res.writeHead(200); res.end(); return; } if (path === '/health') { res.writeHead(200); res.end(JSON.stringify({ status: 'ok', timestamp: new Date().toISOString() })); return; } // Handle MCP JSON-RPC calls if (req.method === 'POST' && path === '/mcp') { let body = ''; req.on('data', chunk => { body += chunk.toString(); }); req.on('end', async () => { try { const request = JSON.parse(body); console.log('Received MCP request:', request.method); if (request.method === 'initialize') { res.writeHead(200); res.end(JSON.stringify({ jsonrpc: '2.0', id: request.id, result: { protocolVersion: '2024-11-05', capabilities: { tools: {}, logging: {} }, serverInfo: { name: 'testrail-mcp', version: '0.1.0' } } })); return; } if (request.method === 'notifications/initialized') { // N8n sends this after initialize - just acknowledge it console.log('Received initialized notification'); res.writeHead(200); res.end(JSON.stringify({ jsonrpc: '2.0', id: request.id || null, result: {} })); return; } if (request.method === 'ping') { res.writeHead(200); res.end(JSON.stringify({ jsonrpc: '2.0', id: request.id, result: {} })); return; } if (request.method === 'tools/list') { const tools = [ { name: 'get_case', description: 'Fetch a TestRail test case by ID.', inputSchema: { type: 'object', properties: { case_id: { type: 'integer', description: 'TestRail case ID' } }, required: ['case_id'] } }, { name: 'update_case', description: 'Update a TestRail test case by ID with new field values.', inputSchema: { type: 'object', properties: { case_id: { type: 'integer', description: 'TestRail case ID' }, title: { type: 'string', description: 'Test case title' }, priority_id: { type: 'integer', description: 'Priority ID' } }, required: ['case_id'] } }, { name: 'get_projects', description: 'Get all available TestRail projects. Use this when asked to list, get, or retrieve projects.', inputSchema: { type: 'object', properties: {}, required: [] } }, { name: 'get_project', description: 'Get details for a specific TestRail project by ID.', inputSchema: { type: 'object', properties: { project_id: { type: 'integer', description: 'TestRail project ID' } }, required: ['project_id'] } }, { name: 'get_suites', description: 'Get all test suites for a specific TestRail project by ID.', inputSchema: { type: 'object', properties: { project_id: { type: 'integer', description: 'TestRail project ID' } }, required: ['project_id'] } }, { name: 'get_suite', description: 'Get details for a specific TestRail test suite by ID.', inputSchema: { type: 'object', properties: { suite_id: { type: 'integer', description: 'TestRail suite ID' } }, required: ['suite_id'] } }, { name: 'get_cases', description: 'Get a list of test cases for a project or specific test suite with optional filtering and pagination.', inputSchema: { type: 'object', properties: { project_id: { type: 'integer', description: 'TestRail project ID' }, suite_id: { type: 'integer', description: 'TestRail suite ID' }, limit: { type: 'integer', description: 'Number of cases to return (max 250)' } }, required: ['project_id'] } }, { name: 'add_attachment_to_case', description: 'Upload a file attachment to a TestRail test case.', inputSchema: { type: 'object', properties: { case_id: { type: 'integer', description: 'TestRail case ID' }, file_path: { type: 'string', description: 'Path to file to upload' } }, required: ['case_id', 'file_path'] } }, { name: 'get_sections', description: 'Get a list of sections for a project and test suite with optional pagination.', inputSchema: { type: 'object', properties: { project_id: { type: 'integer', description: 'TestRail project ID' }, suite_id: { type: 'integer', description: 'TestRail suite ID' } }, required: ['project_id'] } }, { name: 'get_runs', description: 'Get a list of test runs for a project with optional filtering and pagination.', inputSchema: { type: 'object', properties: { project_id: { type: 'integer', description: 'TestRail project ID' }, limit: { type: 'integer', description: 'Number of runs to return (max 250)' } }, required: ['project_id'] } }, { name: 'get_run', description: 'Returns an existing test run.', inputSchema: { type: 'object', properties: { run_id: { type: 'integer', description: 'The ID of the test run' } }, required: ['run_id'] } }, { name: 'update_run', description: 'Updates an existing test run.', inputSchema: { type: 'object', properties: { run_id: { type: 'integer', description: 'The ID of the test run' }, name: { type: 'string', description: 'The name of the test run' } }, required: ['run_id'] } }, { name: 'get_tests', description: 'Returns a list of tests for a test run.', inputSchema: { type: 'object', properties: { run_id: { type: 'integer', description: 'The ID of the test run' }, limit: { type: 'integer', description: 'Number of tests to return (max 250)' } }, required: ['run_id'] } }, { name: 'get_test', description: 'Returns an existing test.', inputSchema: { type: 'object', properties: { test_id: { type: 'integer', description: 'The ID of the test' } }, required: ['test_id'] } }, { name: 'update_test', description: 'Updates the labels assigned to an existing test.', inputSchema: { type: 'object', properties: { test_id: { type: 'integer', description: 'The ID of the test' }, labels: { type: 'array', description: 'Array of label IDs or titles', items: { oneOf: [ { type: 'integer', description: 'Label ID' }, { type: 'string', description: 'Label title' } ] } } }, required: ['test_id'] } }, { name: 'add_result', description: 'Adds a new test result, comment, or assigns a test.', inputSchema: { type: 'object', properties: { test_id: { type: 'integer', description: 'The ID of the test' }, status_id: { type: 'integer', description: 'The ID of the test status (1=Passed, 5=Failed)' }, comment: { type: 'string', description: 'Comment for the test result' }, version: { type: 'string', description: 'The version or build tested' }, elapsed: { type: 'string', description: 'Time to execute (e.g., "30s")' }, defects: { type: 'string', description: 'Comma-separated list of defects' }, assignedto_id: { type: 'integer', description: 'User ID to assign test to' }, custom_step_results: { type: 'array', description: 'Array of step results for structured testing', items: { type: 'object', properties: { content: { type: 'string', description: 'The test step content' }, expected: { type: 'string', description: 'The expected result for the step' }, actual: { type: 'string', description: 'The actual result for the step' }, status_id: { type: 'integer', description: 'The status ID for the step (1=Passed, 2=Blocked, 4=Retest, 5=Failed)' } }, required: ['content', 'expected', 'actual', 'status_id'] } } }, required: ['test_id', 'status_id'] } } ]; res.writeHead(200); res.end(JSON.stringify({ jsonrpc: '2.0', id: request.id, result: { tools } })); return; } if (request.method === 'tools/call') { const { name, arguments: args } = request.params; // Route tool calls to appropriate service functions try { let result; switch (name) { case 'get_case': result = await getCase(args.case_id); break; case 'update_case': result = await updateCase(args.case_id, args); break; case 'get_projects': result = await getProjects(); break; case 'get_project': result = await getProject(args.project_id); break; case 'get_suites': result = await getSuites(args.project_id); break; case 'get_suite': result = await getSuite(args.suite_id); break; case 'get_cases': result = await getCases(args); break; case 'add_attachment_to_case': result = await addAttachmentToCase(args.case_id, args.file_path); break; case 'get_sections': result = await getSections(args); break; case 'get_runs': result = await getRuns(args); break; case 'get_run': result = await getRun(args.run_id); break; case 'update_run': result = await updateRun(args.run_id, args); break; case 'get_tests': result = await getTests(args); break; case 'get_test': result = await getTest(args); break; case 'update_test': result = await updateTest(args.test_id, args); break; case 'add_result': result = await addResult(args); break; default: res.writeHead(404); res.end(JSON.stringify({ jsonrpc: '2.0', id: request.id, error: { code: -32601, message: `Tool not found: ${name}` } })); return; } res.writeHead(200); res.end(JSON.stringify({ jsonrpc: '2.0', id: request.id, result: { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } })); } catch (error: any) { res.writeHead(500); res.end(JSON.stringify({ jsonrpc: '2.0', id: request.id, error: { code: -32603, message: error.message || 'Internal error' } })); } return; } res.writeHead(501); res.end(JSON.stringify({ jsonrpc: '2.0', id: request.id, error: { code: -32601, message: `Method not implemented: ${request.method}` } })); } catch (error: any) { console.log('MCP request error:', error); res.writeHead(400); res.end(JSON.stringify({ jsonrpc: '2.0', id: null, error: { code: -32700, message: 'Parse error', data: error?.message || 'Unknown error' } })); } }); return; } res.writeHead(404); res.end(JSON.stringify({ error: 'Not found' })); }); httpServer.listen(1823, () => { console.log('MCP HTTP server listening on port 1823'); }); console.log('MCP HTTP server ready on port 1823!'); } main().catch((error: unknown) => { const err = error as Error; console.error('Failed to start MCP HTTP server:', err.message); console.error('Stack trace:', err.stack); process.exit(1); }); process.on('exit', (code) => { console.log(`MCP HTTP server exiting with code: ${code}`); }); process.on('SIGINT', () => { console.log('MCP HTTP server received SIGINT, shutting down...'); process.exit(0); }); process.on('SIGTERM', () => { console.log('MCP HTTP server received SIGTERM, shutting down...'); process.exit(0); });

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/Derrbal/testrail-mcp'

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