# Directory Structure ``` ├── .eslintrc.json ├── .github │ └── workflows │ └── test.yml ├── .gitignore ├── .node-version ├── .prettierrc ├── Dockerfile ├── eslint.config.js ├── LICENSE ├── package-lock.json ├── package.json ├── README.md ├── src │ ├── index.ts │ ├── operations │ │ ├── cases.ts │ │ ├── plans.ts │ │ ├── projects.ts │ │ ├── results.ts │ │ ├── runs.ts │ │ ├── shared-steps.ts │ │ └── suites.ts │ └── utils.ts └── tsconfig.json ``` # Files -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- ``` 1 | 22.13.1 2 | ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` 1 | node_modules/ 2 | build/ 3 | *.log 4 | .env* ``` -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- ``` 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "printWidth": 80 5 | } 6 | ``` -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:@typescript-eslint/recommended", 9 | "prettier" 10 | ], 11 | "parser": "@typescript-eslint/parser", 12 | "parserOptions": { 13 | "ecmaVersion": 12, 14 | "sourceType": "module" 15 | }, 16 | "plugins": [ 17 | "@typescript-eslint", 18 | "prettier" 19 | ], 20 | "rules": { 21 | "prettier/prettier": "error" 22 | } 23 | } 24 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | # QASE MCP Server 2 | 3 | MCP server implementation for Qase API 4 | 5 | This is a TypeScript-based MCP server that provides integration with the Qase test management platform. It implements core MCP concepts by providing tools for interacting with various Qase entities. 6 | 7 | ## Features 8 | 9 | ### Tools 10 | The server provides tools for interacting with the Qase API, allowing you to manage the following entities: 11 | 12 | #### Projects 13 | - `list_projects` - Get all projects 14 | - `get_project` - Get project by code 15 | - `create_project` - Create new project 16 | - `delete_project` - Delete project by code 17 | 18 | #### Test Cases 19 | - `get_cases` - Get all test cases in a project 20 | - `get_case` - Get a specific test case 21 | - `create_case` - Create a new test case 22 | - `update_case` - Update an existing test case 23 | 24 | #### Test Runs 25 | - `get_runs` - Get all test runs in a project 26 | - `get_run` - Get a specific test run 27 | 28 | #### Test Results 29 | - `get_results` - Get all test run results for a project 30 | - `get_result` - Get test run result by code and hash 31 | - `create_result` - Create test run result 32 | - `create_result_bulk` - Create multiple test run results in bulk 33 | - `update_result` - Update an existing test run result 34 | 35 | #### Test Plans 36 | - `get_plans` - Get all test plans in a project 37 | - `get_plan` - Get a specific test plan 38 | - `create_plan` - Create a new test plan 39 | - `update_plan` - Update an existing test plan 40 | - `delete_plan` - Delete a test plan 41 | 42 | #### Test Suites 43 | - `get_suites` - Get all test suites in a project 44 | - `get_suite` - Get a specific test suite 45 | - `create_suite` - Create a new test suite 46 | - `update_suite` - Update an existing test suite 47 | - `delete_suite` - Delete a test suite 48 | 49 | #### Shared Steps 50 | - `get_shared_steps` - Get all shared steps in a project 51 | - `get_shared_step` - Get a specific shared step 52 | - `create_shared_step` - Create a new shared step 53 | - `update_shared_step` - Update an existing shared step 54 | - `delete_shared_step` - Delete a shared step 55 | 56 | ## Development 57 | 58 | Install dependencies: 59 | ```bash 60 | npm install 61 | ``` 62 | 63 | Build the server: 64 | ```bash 65 | npm run build 66 | ``` 67 | 68 | For development with auto-rebuild: 69 | ```bash 70 | npm run watch 71 | ``` 72 | 73 | ## Installation 74 | 75 | ### Claude Desktop 76 | 77 | To use with Claude Desktop, add the server config: 78 | 79 | - On MacOS: `~/Library/Application Support/Claude/claude_desktop_config.json` 80 | - On Windows: `%APPDATA%/Claude/claude_desktop_config.json` 81 | 82 | ```json 83 | { 84 | "mcpServers": { 85 | "mcp-qase": { 86 | "command": "/path/to/mcp-qase/build/index.js", 87 | "env": { 88 | "QASE_API_TOKEN": "<YOUR_TOKEN>" 89 | } 90 | } 91 | } 92 | } 93 | ``` 94 | 95 | ### Cursor 96 | 97 | To use with Cursor, register the command as follows: 98 | 99 | ``` 100 | env QASE_API_TOKEN=<YOUR_TOKEN> /path/to/mcp-qase/build/index.js 101 | ``` 102 | 103 | ## Debugging 104 | 105 | Since MCP servers communicate over stdio, debugging can be challenging. We recommend using the [MCP Inspector](https://github.com/modelcontextprotocol/inspector): 106 | 107 | ```bash 108 | npx -y @modelcontextprotocol/inspector -e QASE_API_TOKEN=<YOUR_TOKEN> ./build/index.js 109 | ``` ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "Node16", 5 | "moduleResolution": "Node16", 6 | "outDir": "./build", 7 | "rootDir": "./src", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true 12 | }, 13 | "include": ["src/**/*"], 14 | "exclude": ["node_modules"] 15 | } 16 | ``` -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- ```yaml 1 | name: Test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | lint: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - name: Checkout repository 11 | uses: actions/checkout@v2 12 | 13 | - name: Set up Node.js 14 | uses: actions/setup-node@v4 15 | with: 16 | node-version: 22 17 | 18 | - name: Install dependencies 19 | run: npm install 20 | 21 | - name: Run ESLint 22 | run: npm run lint 23 | ``` -------------------------------------------------------------------------------- /src/operations/runs.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | import { toResult } from '../utils.js'; 3 | import { apply, pipe } from 'ramda'; 4 | import { client } from '../utils.js'; 5 | 6 | export const GetRunsSchema = z.object({ 7 | code: z.string(), 8 | search: z.string().optional(), 9 | status: z.string().optional(), 10 | milestone: z.number().optional(), 11 | environment: z.number().optional(), 12 | fromStartTime: z.number().optional(), 13 | toStartTime: z.number().optional(), 14 | limit: z.number().optional(), 15 | offset: z.number().optional(), 16 | include: z.string().optional(), 17 | }); 18 | 19 | export const GetRunSchema = z.object({ 20 | code: z.string(), 21 | id: z.number(), 22 | include: z.enum(['cases']).optional(), 23 | }); 24 | 25 | export const getRuns = pipe( 26 | apply(client.runs.getRuns.bind(client.runs)), 27 | toResult, 28 | ); 29 | 30 | export const getRun = pipe(client.runs.getRun.bind(client.runs), toResult); 31 | ``` -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- ```javascript 1 | export default { 2 | languageOptions: { 3 | ecmaVersion: 12, 4 | sourceType: "module", 5 | globals: { 6 | browser: true, 7 | es2021: true, 8 | }, 9 | }, 10 | files: ["**/*.ts", "**/*.tsx"], 11 | plugins: { 12 | "@typescript-eslint": (await import("@typescript-eslint/eslint-plugin")).default, 13 | "prettier": (await import("eslint-plugin-prettier")).default, 14 | }, 15 | rules: { 16 | ...(await import("eslint-config-prettier")).default.rules, 17 | ...(await import("eslint-plugin-prettier")).default.configs.recommended.rules, 18 | ...(await import("@typescript-eslint/eslint-plugin")).default.configs.recommended.rules, 19 | }, 20 | languageOptions: { 21 | parser: (await import("@typescript-eslint/parser")).default, 22 | ecmaVersion: 12, 23 | sourceType: "module", 24 | globals: { 25 | browser: true, 26 | es2021: true, 27 | }, 28 | }, 29 | }; 30 | ``` -------------------------------------------------------------------------------- /src/operations/projects.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { ProjectCreateAccessEnum } from 'qaseio'; 2 | import { z } from 'zod'; 3 | import { client, toResult } from '../utils.js'; 4 | import { pipe } from 'ramda'; 5 | 6 | export const ListProjectsSchema = z.object({ 7 | limit: z.number().optional(), 8 | offset: z.number().optional(), 9 | }); 10 | 11 | export const GetProjectSchema = z.object({ 12 | code: z.string(), 13 | }); 14 | 15 | export const CreateProjectSchema = z.object({ 16 | code: z.string(), 17 | title: z.string(), 18 | description: z.string().optional(), 19 | access: z.nativeEnum(ProjectCreateAccessEnum).optional(), 20 | group: z.string().optional(), 21 | }); 22 | 23 | export const listProjects = pipe( 24 | client.projects.getProjects.bind(client.projects), 25 | toResult, 26 | ); 27 | 28 | export const getProject = pipe( 29 | client.projects.getProject.bind(client.projects), 30 | toResult, 31 | ); 32 | 33 | export const createProject = pipe( 34 | client.projects.createProject.bind(client.projects), 35 | toResult, 36 | ); 37 | ``` -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { AxiosResponse } from 'axios'; 2 | import { ResultAsync } from 'neverthrow'; 3 | import { QaseApi } from 'qaseio'; 4 | 5 | export type ApiError = { 6 | response?: { data?: { message?: string } }; 7 | message: string; 8 | }; 9 | 10 | export type ApiResponse<T> = { 11 | data: { 12 | result?: T; 13 | status: boolean; 14 | errorMessage?: string; 15 | }; 16 | }; 17 | 18 | export const formatApiError = (error: unknown) => { 19 | const apiError = error as ApiError; 20 | return apiError.response?.data?.message || apiError.message; 21 | }; 22 | 23 | export const toResult = (promise: Promise<AxiosResponse>) => 24 | ResultAsync.fromPromise(promise, formatApiError); 25 | 26 | export const client = (({ QASE_API_TOKEN }) => { 27 | if (!QASE_API_TOKEN) { 28 | throw new Error( 29 | 'QASE_API_TOKEN environment variable is required. Please set it before running the server.', 30 | ); 31 | } 32 | return new QaseApi({ 33 | token: QASE_API_TOKEN, 34 | }); 35 | })(process.env); 36 | ``` -------------------------------------------------------------------------------- /src/operations/plans.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | import { client, toResult } from '../utils.js'; 3 | import { pipe } from 'ramda'; 4 | 5 | export const GetPlansSchema = z.object({ 6 | code: z.string(), 7 | limit: z.number().optional(), 8 | offset: z.number().optional(), 9 | }); 10 | 11 | export const GetPlanSchema = z.object({ 12 | code: z.string(), 13 | id: z.number(), 14 | }); 15 | 16 | export const CreatePlanSchema = z.object({ 17 | code: z.string(), 18 | title: z.string(), 19 | description: z.string().optional(), 20 | cases: z.array(z.number()), 21 | }); 22 | 23 | export const UpdatePlanSchema = z.object({ 24 | code: z.string(), 25 | id: z.number(), 26 | title: z.string().optional(), 27 | description: z.string().optional(), 28 | cases: z.array(z.number()).optional(), 29 | }); 30 | 31 | export const getPlans = pipe( 32 | client.plans.getPlans.bind(client.plans), 33 | toResult, 34 | ); 35 | 36 | export const getPlan = pipe(client.plans.getPlan.bind(client.plans), toResult); 37 | 38 | export const createPlan = pipe( 39 | client.plans.createPlan.bind(client.plans), 40 | toResult, 41 | ); 42 | 43 | export const updatePlan = pipe( 44 | client.plans.updatePlan.bind(client.plans), 45 | toResult, 46 | ); 47 | ``` -------------------------------------------------------------------------------- /src/operations/suites.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | import { client, toResult } from '../utils.js'; 3 | import { pipe } from 'ramda'; 4 | 5 | export const GetSuitesSchema = z.object({ 6 | code: z.string(), 7 | search: z.string().optional(), 8 | limit: z.number().optional(), 9 | offset: z.number().optional(), 10 | }); 11 | 12 | export const GetSuiteSchema = z.object({ 13 | code: z.string(), 14 | id: z.number(), 15 | }); 16 | 17 | export const CreateSuiteSchema = z.object({ 18 | code: z.string(), 19 | title: z.string(), 20 | description: z.string().optional(), 21 | preconditions: z.string().optional(), 22 | parent_id: z.number().optional(), 23 | }); 24 | 25 | export const UpdateSuiteSchema = z.object({ 26 | code: z.string(), 27 | id: z.number(), 28 | title: z.string().optional(), 29 | description: z.string().optional(), 30 | preconditions: z.string().optional(), 31 | parent_id: z.number().optional(), 32 | }); 33 | 34 | export const getSuites = pipe( 35 | client.suites.getSuites.bind(client.suites), 36 | toResult, 37 | ); 38 | 39 | export const getSuite = pipe( 40 | client.suites.getSuite.bind(client.suites), 41 | toResult, 42 | ); 43 | 44 | export const createSuite = pipe( 45 | client.suites.createSuite.bind(client.suites), 46 | toResult, 47 | ); 48 | 49 | export const updateSuite = pipe( 50 | client.suites.updateSuite.bind(client.suites), 51 | toResult, 52 | ); 53 | ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "mcp-qase", 3 | "version": "0.1.0", 4 | "description": "MCP server implementation for Qase API", 5 | "private": true, 6 | "type": "module", 7 | "bin": { 8 | "mcp-qase": "./build/index.js" 9 | }, 10 | "files": [ 11 | "build" 12 | ], 13 | "scripts": { 14 | "build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"", 15 | "prepare": "npm run build", 16 | "watch": "tsc --watch", 17 | "inspector": "npx @modelcontextprotocol/inspector build/index.js", 18 | "lint": "eslint src --ext .ts", 19 | "lint:fix": "eslint . --ext .ts --fix" 20 | }, 21 | "dependencies": { 22 | "@modelcontextprotocol/sdk": "0.6.0", 23 | "@types/ramda": "^0.30.2", 24 | "neverthrow": "^8.2.0", 25 | "qaseio": "^2.4.1", 26 | "ramda": "^0.30.1", 27 | "ts-pattern": "^5.6.2", 28 | "zod": "^3.24.2", 29 | "zod-to-json-schema": "^3.24.3" 30 | }, 31 | "devDependencies": { 32 | "@types/node": "^20.11.24", 33 | "@typescript-eslint/eslint-plugin": "^8.26.1", 34 | "eslint": "^9.22.0", 35 | "eslint-config-prettier": "^10.1.1", 36 | "eslint-plugin-prettier": "^5.2.3", 37 | "lint-staged": "^12.0.0", 38 | "typescript": "^5.3.3" 39 | }, 40 | "lint-staged": { 41 | "*.{js,ts,tsx}": [ 42 | "eslint --fix", 43 | "prettier --write" 44 | ] 45 | } 46 | } 47 | ``` -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- ```dockerfile 1 | FROM debian:bullseye-slim 2 | 3 | ENV DEBIAN_FRONTEND=noninteractive \ 4 | GLAMA_VERSION="0.2.0" \ 5 | PATH="/home/service-user/.local/bin:${PATH}" 6 | 7 | RUN (groupadd -r service-user) && (useradd -u 1987 -r -m -g service-user service-user) && (mkdir -p /home/service-user/.local/bin /app) && (chown -R service-user:service-user /home/service-user /app) && (apt-get update) && (apt-get install -y --no-install-recommends build-essential curl wget software-properties-common libssl-dev zlib1g-dev git) && (rm -rf /var/lib/apt/lists/*) && (curl -fsSL https://deb.nodesource.com/setup_22.x | bash -) && (apt-get install -y nodejs) && (apt-get clean) && (npm install -g [email protected]) && (npm install -g [email protected]) && (npm install -g [email protected]) && (node --version) && (curl -LsSf https://astral.sh/uv/install.sh | UV_INSTALL_DIR="/usr/local/bin" sh) && (uv python install 3.13 --default --preview) && (ln -s $(uv python find) /usr/local/bin/python) && (python --version) && (apt-get clean) && (rm -rf /var/lib/apt/lists/*) && (rm -rf /tmp/*) && (rm -rf /var/tmp/*) && (su - service-user -c "uv python install 3.13 --default --preview && python --version") 8 | 9 | USER service-user 10 | 11 | WORKDIR /app 12 | 13 | RUN git clone https://github.com/rikuson/mcp-qase . 14 | 15 | RUN (npm install) && (npm run build) 16 | 17 | CMD ["mcp-proxy","node","./build/index.js"] 18 | ``` -------------------------------------------------------------------------------- /src/operations/results.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { 2 | TestStepResultCreateStatusEnum, 3 | ResultCreate, 4 | ResultCreateBulk, 5 | ResultUpdate, 6 | } from 'qaseio'; 7 | import { z } from 'zod'; 8 | import { toResult } from '../utils.js'; 9 | import { apply, pipe } from 'ramda'; 10 | import { client } from '../utils.js'; 11 | 12 | export const GetResultsSchema = z.object({ 13 | code: z.string(), 14 | limit: z.string().optional(), 15 | offset: z.string().optional(), 16 | status: z.nativeEnum(TestStepResultCreateStatusEnum).optional(), 17 | from: z.string().optional(), 18 | to: z.string().optional(), 19 | }); 20 | 21 | export const GetResultSchema = z.object({ 22 | code: z.string(), 23 | hash: z.string(), 24 | }); 25 | 26 | export const CreateResultSchema = z.object({ 27 | code: z.string(), 28 | id: z.number(), 29 | result: z.record(z.any()).transform((v) => v as ResultCreate), 30 | }); 31 | 32 | export const CreateResultBulkSchema = z.object({ 33 | code: z.string(), 34 | id: z.number(), 35 | results: z.record(z.any()).transform((v) => v as ResultCreateBulk), 36 | }); 37 | 38 | export const UpdateResultSchema = z.object({ 39 | code: z.string(), 40 | id: z.number(), 41 | hash: z.string(), 42 | result: z.record(z.any()).transform((v) => v as ResultUpdate), 43 | }); 44 | 45 | export const getResults = pipe( 46 | apply(client.results.getResults.bind(client.results)), 47 | toResult, 48 | ); 49 | 50 | export const getResult = pipe( 51 | client.results.getResult.bind(client.results), 52 | toResult, 53 | ); 54 | 55 | export const createResult = pipe( 56 | client.results.createResult.bind(client.results), 57 | toResult, 58 | ); 59 | 60 | export const createResultBulk = pipe( 61 | client.results.createResultBulk.bind(client.results), 62 | toResult, 63 | ); 64 | 65 | export const updateResult = pipe( 66 | client.results.updateResult.bind(client.results), 67 | toResult, 68 | ); 69 | ``` -------------------------------------------------------------------------------- /src/operations/shared-steps.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { SharedStepUpdate } from 'qaseio'; 2 | import { z } from 'zod'; 3 | import { client, toResult } from '../utils.js'; 4 | import { pipe } from 'ramda'; 5 | 6 | export const GetSharedStepsSchema = z.object({ 7 | code: z.string(), 8 | search: z.string().optional(), 9 | limit: z.number().optional(), 10 | offset: z.number().optional(), 11 | }); 12 | 13 | export const GetSharedStepSchema = z.object({ 14 | code: z.string(), 15 | hash: z.string(), 16 | }); 17 | 18 | export const CreateSharedStepSchema = z.object({ 19 | code: z.string(), 20 | title: z.string(), 21 | action: z.string(), 22 | expected_result: z.string().optional(), 23 | data: z.string().optional(), 24 | steps: z 25 | .array( 26 | z.object({ 27 | action: z.string(), 28 | expected_result: z.string().optional(), 29 | data: z.string().optional(), 30 | position: z.number().optional(), 31 | }), 32 | ) 33 | .optional(), 34 | }); 35 | 36 | export const UpdateSharedStepSchema = z 37 | .object({ 38 | code: z.string(), 39 | hash: z.string(), 40 | title: z.string(), 41 | action: z.string(), 42 | expected_result: z.string().optional(), 43 | data: z.string().optional(), 44 | steps: z 45 | .array( 46 | z.object({ 47 | action: z.string(), 48 | expected_result: z.string().optional(), 49 | data: z.string().optional(), 50 | position: z.number().optional(), 51 | }), 52 | ) 53 | .optional(), 54 | }) 55 | .transform((data) => ({ 56 | code: data.code, 57 | hash: data.hash, 58 | stepData: { 59 | title: data.title, 60 | action: data.action, 61 | expected_result: data.expected_result, 62 | data: data.data, 63 | steps: data.steps, 64 | } as SharedStepUpdate, 65 | })); 66 | 67 | export const getSharedSteps = pipe( 68 | client.sharedSteps.getSharedSteps.bind(client.sharedSteps), 69 | toResult, 70 | ); 71 | 72 | export const getSharedStep = pipe( 73 | client.sharedSteps.getSharedStep.bind(client.sharedSteps), 74 | toResult, 75 | ); 76 | 77 | export const createSharedStep = pipe( 78 | client.sharedSteps.createSharedStep.bind(client.sharedSteps), 79 | toResult, 80 | ); 81 | 82 | export const updateSharedStep = pipe( 83 | client.sharedSteps.updateSharedStep.bind(client.sharedSteps), 84 | toResult, 85 | ); 86 | ``` -------------------------------------------------------------------------------- /src/operations/cases.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { TestCaseCreate } from 'qaseio'; 2 | import { z } from 'zod'; 3 | import { client, toResult } from '../utils.js'; 4 | import { apply, pipe } from 'ramda'; 5 | 6 | export const GetCasesSchema = z.object({ 7 | code: z.string(), 8 | search: z.string().optional(), 9 | milestoneId: z.number().optional(), 10 | suiteId: z.number().optional(), 11 | severity: z.string().optional(), 12 | priority: z.string().optional(), 13 | type: z.string().optional(), 14 | behavior: z.string().optional(), 15 | automation: z.string().optional(), 16 | status: z.string().optional(), 17 | externalIssuesType: z 18 | .enum([ 19 | 'asana', 20 | 'azure-devops', 21 | 'clickup-app', 22 | 'github-app', 23 | 'gitlab-app', 24 | 'jira-cloud', 25 | 'jira-server', 26 | 'linear', 27 | 'monday', 28 | 'redmine-app', 29 | 'trello-app', 30 | 'youtrack-app', 31 | ]) 32 | .optional(), 33 | externalIssuesIds: z.array(z.string()).optional(), 34 | include: z.string().optional(), 35 | limit: z.number().optional(), 36 | offset: z.number().optional(), 37 | }); 38 | 39 | export const GetCaseSchema = z.object({ 40 | code: z.string(), 41 | id: z.number(), 42 | }); 43 | 44 | export const CreateCaseSchema = z.object({ 45 | code: z.string(), 46 | testCase: z.record(z.any()).transform((v) => v as TestCaseCreate), 47 | }); 48 | 49 | export const UpdateCaseSchema = z.object({ 50 | code: z.string(), 51 | id: z.number(), 52 | title: z.string().optional(), 53 | description: z.string().optional(), 54 | preconditions: z.string().optional(), 55 | postconditions: z.string().optional(), 56 | severity: z.number().optional(), 57 | priority: z.number().optional(), 58 | type: z.number().optional(), 59 | behavior: z.number().optional(), 60 | automation: z.number().optional(), 61 | status: z.number().optional(), 62 | suite_id: z.number().optional(), 63 | milestone_id: z.number().optional(), 64 | layer: z.number().optional(), 65 | is_flaky: z.boolean().optional(), 66 | params: z 67 | .array( 68 | z.object({ 69 | title: z.string(), 70 | value: z.string(), 71 | }), 72 | ) 73 | .optional(), 74 | tags: z.array(z.string()).optional(), 75 | steps: z 76 | .array( 77 | z.object({ 78 | action: z.string(), 79 | expected_result: z.string().optional(), 80 | data: z.string().optional(), 81 | position: z.number().optional(), 82 | }), 83 | ) 84 | .optional(), 85 | custom_fields: z 86 | .array( 87 | z.object({ 88 | id: z.number(), 89 | value: z.string(), 90 | }), 91 | ) 92 | .optional(), 93 | }); 94 | 95 | export const CreateCaseBulkSchema = z.object({ 96 | code: z.string(), 97 | cases: z.array( 98 | z.object({ 99 | title: z.string(), 100 | description: z.string().optional(), 101 | preconditions: z.string().optional(), 102 | postconditions: z.string().optional(), 103 | severity: z.number().optional(), 104 | priority: z.number().optional(), 105 | type: z.number().optional(), 106 | behavior: z.number().optional(), 107 | automation: z.number().optional(), 108 | status: z.number().optional(), 109 | suite_id: z.number().optional(), 110 | milestone_id: z.number().optional(), 111 | layer: z.number().optional(), 112 | is_flaky: z.boolean().optional(), 113 | params: z 114 | .array( 115 | z.object({ 116 | title: z.string(), 117 | value: z.string(), 118 | }), 119 | ) 120 | .optional(), 121 | tags: z.array(z.string()).optional(), 122 | steps: z 123 | .array( 124 | z.object({ 125 | action: z.string(), 126 | expected_result: z.string().optional(), 127 | data: z.string().optional(), 128 | position: z.number().optional(), 129 | }), 130 | ) 131 | .optional(), 132 | custom_fields: z 133 | .array( 134 | z.object({ 135 | id: z.number(), 136 | value: z.string(), 137 | }), 138 | ) 139 | .optional(), 140 | }), 141 | ), 142 | }); 143 | 144 | export const getCases = pipe( 145 | apply(client.cases.getCases.bind(client.cases)), 146 | toResult, 147 | ); 148 | 149 | export const getCase = pipe(client.cases.getCase.bind(client.cases), toResult); 150 | 151 | export const createCase = pipe( 152 | client.cases.createCase.bind(client.cases), 153 | toResult, 154 | ); 155 | 156 | const convertCaseData = ( 157 | data: Omit<z.infer<typeof UpdateCaseSchema>, 'code' | 'id'>, 158 | ) => ({ 159 | ...data, 160 | is_flaky: data.is_flaky === undefined ? undefined : data.is_flaky ? 1 : 0, 161 | params: data.params 162 | ? data.params.reduce( 163 | (acc, param) => ({ 164 | ...acc, 165 | [param.title]: [param.value], 166 | }), 167 | {}, 168 | ) 169 | : undefined, 170 | }); 171 | 172 | export const updateCase = pipe( 173 | ( 174 | code: string, 175 | id: number, 176 | data: Omit<z.infer<typeof UpdateCaseSchema>, 'code' | 'id'>, 177 | ) => client.cases.updateCase(code, id, convertCaseData(data)), 178 | toResult, 179 | ); 180 | ``` -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * This is a template MCP server that implements a simple notes system. 5 | * It demonstrates core MCP concepts like resources and tools by allowing: 6 | * - Listing notes as resources 7 | * - Reading individual notes 8 | * - Creating new notes via a tool 9 | * - Summarizing all notes via a prompt 10 | */ 11 | 12 | import { Server } from '@modelcontextprotocol/sdk/server/index.js'; 13 | import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; 14 | import { 15 | CallToolRequestSchema, 16 | ListResourcesRequestSchema, 17 | ListToolsRequestSchema, 18 | ReadResourceRequestSchema, 19 | ListPromptsRequestSchema, 20 | GetPromptRequestSchema, 21 | } from '@modelcontextprotocol/sdk/types.js'; 22 | import { zodToJsonSchema } from 'zod-to-json-schema'; 23 | import { 24 | listProjects, 25 | getProject, 26 | createProject, 27 | CreateProjectSchema, 28 | GetProjectSchema, 29 | ListProjectsSchema, 30 | } from './operations/projects.js'; 31 | import { 32 | getResults, 33 | getResult, 34 | createResult, 35 | CreateResultSchema, 36 | GetResultSchema, 37 | GetResultsSchema, 38 | CreateResultBulkSchema, 39 | createResultBulk, 40 | UpdateResultSchema, 41 | updateResult, 42 | } from './operations/results.js'; 43 | import { 44 | getCases, 45 | getCase, 46 | createCase, 47 | updateCase, 48 | GetCasesSchema, 49 | GetCaseSchema, 50 | CreateCaseSchema, 51 | UpdateCaseSchema, 52 | } from './operations/cases.js'; 53 | import { 54 | getRuns, 55 | getRun, 56 | GetRunsSchema, 57 | GetRunSchema, 58 | } from './operations/runs.js'; 59 | import { 60 | getPlans, 61 | getPlan, 62 | createPlan, 63 | updatePlan, 64 | GetPlansSchema, 65 | GetPlanSchema, 66 | CreatePlanSchema, 67 | UpdatePlanSchema, 68 | } from './operations/plans.js'; 69 | import { 70 | GetSuitesSchema, 71 | GetSuiteSchema, 72 | CreateSuiteSchema, 73 | UpdateSuiteSchema, 74 | getSuites, 75 | getSuite, 76 | createSuite, 77 | updateSuite, 78 | } from './operations/suites.js'; 79 | import { 80 | GetSharedStepsSchema, 81 | GetSharedStepSchema, 82 | CreateSharedStepSchema, 83 | UpdateSharedStepSchema, 84 | getSharedSteps, 85 | getSharedStep, 86 | createSharedStep, 87 | updateSharedStep, 88 | } from './operations/shared-steps.js'; 89 | import { match } from 'ts-pattern'; 90 | import { errAsync } from 'neverthrow'; 91 | 92 | /** 93 | * Create an MCP server with capabilities for resources (to list/read notes), 94 | * tools (to create new notes), and prompts (to summarize notes). 95 | */ 96 | const server = new Server( 97 | { 98 | name: 'mcp-qase', 99 | version: '0.1.0', 100 | }, 101 | { 102 | capabilities: { 103 | resources: {}, 104 | tools: {}, 105 | prompts: {}, 106 | }, 107 | }, 108 | ); 109 | 110 | /** 111 | * Handler for listing available notes as resources. 112 | */ 113 | server.setRequestHandler(ListResourcesRequestSchema, () => ({ 114 | resources: [], 115 | })); 116 | 117 | /** 118 | * Handler for reading the contents 119 | */ 120 | server.setRequestHandler(ReadResourceRequestSchema, () => ({ 121 | contents: [], 122 | })); 123 | 124 | /** 125 | * Handler that lists available tools. 126 | * Exposes a single "create_note" tool that lets clients create new notes. 127 | */ 128 | server.setRequestHandler(ListToolsRequestSchema, () => ({ 129 | tools: [ 130 | { 131 | name: 'list_projects', 132 | description: 'Get All Projects', 133 | inputSchema: zodToJsonSchema(ListProjectsSchema), 134 | }, 135 | { 136 | name: 'get_project', 137 | description: 'Get project by code', 138 | inputSchema: zodToJsonSchema(GetProjectSchema), 139 | }, 140 | { 141 | name: 'create_project', 142 | description: 'Create new project', 143 | inputSchema: zodToJsonSchema(CreateProjectSchema), 144 | }, 145 | { 146 | name: 'get_results', 147 | description: 'Get all test run results for a project', 148 | inputSchema: zodToJsonSchema(GetResultsSchema), 149 | }, 150 | { 151 | name: 'get_result', 152 | description: 'Get test run result by code and hash', 153 | inputSchema: zodToJsonSchema(GetResultSchema), 154 | }, 155 | { 156 | name: 'create_result', 157 | description: 'Create test run result', 158 | inputSchema: zodToJsonSchema(CreateResultSchema), 159 | }, 160 | { 161 | name: 'create_result_bulk', 162 | description: 'Create multiple test run results in bulk', 163 | inputSchema: zodToJsonSchema(CreateResultBulkSchema), 164 | }, 165 | { 166 | name: 'update_result', 167 | description: 'Update an existing test run result', 168 | inputSchema: zodToJsonSchema(UpdateResultSchema), 169 | }, 170 | { 171 | name: 'get_cases', 172 | description: 'Get all test cases in a project', 173 | inputSchema: zodToJsonSchema(GetCasesSchema), 174 | }, 175 | { 176 | name: 'get_case', 177 | description: 'Get a specific test case', 178 | inputSchema: zodToJsonSchema(GetCaseSchema), 179 | }, 180 | { 181 | name: 'create_case', 182 | description: 'Create a new test case', 183 | inputSchema: zodToJsonSchema(CreateCaseSchema), 184 | }, 185 | { 186 | name: 'update_case', 187 | description: 'Update an existing test case', 188 | inputSchema: zodToJsonSchema(UpdateCaseSchema), 189 | }, 190 | { 191 | name: 'get_runs', 192 | description: 'Get all test runs in a project', 193 | inputSchema: zodToJsonSchema(GetRunsSchema), 194 | }, 195 | { 196 | name: 'get_run', 197 | description: 'Get a specific test run', 198 | inputSchema: zodToJsonSchema(GetRunSchema), 199 | }, 200 | { 201 | name: 'get_plans', 202 | description: 'Get all test plans in a project', 203 | inputSchema: zodToJsonSchema(GetPlansSchema), 204 | }, 205 | { 206 | name: 'get_plan', 207 | description: 'Get a specific test plan', 208 | inputSchema: zodToJsonSchema(GetPlanSchema), 209 | }, 210 | { 211 | name: 'create_plan', 212 | description: 'Create a new test plan', 213 | inputSchema: zodToJsonSchema(CreatePlanSchema), 214 | }, 215 | { 216 | name: 'update_plan', 217 | description: 'Update an existing test plan', 218 | inputSchema: zodToJsonSchema(UpdatePlanSchema), 219 | }, 220 | { 221 | name: 'get_suites', 222 | description: 'Get all test suites in a project', 223 | inputSchema: zodToJsonSchema(GetSuitesSchema), 224 | }, 225 | { 226 | name: 'get_suite', 227 | description: 'Get a specific test suite', 228 | inputSchema: zodToJsonSchema(GetSuiteSchema), 229 | }, 230 | { 231 | name: 'create_suite', 232 | description: 'Create a new test suite', 233 | inputSchema: zodToJsonSchema(CreateSuiteSchema), 234 | }, 235 | { 236 | name: 'update_suite', 237 | description: 'Update an existing test suite', 238 | inputSchema: zodToJsonSchema(UpdateSuiteSchema), 239 | }, 240 | { 241 | name: 'get_shared_steps', 242 | description: 'Get all shared steps in a project', 243 | inputSchema: zodToJsonSchema(GetSharedStepsSchema), 244 | }, 245 | { 246 | name: 'get_shared_step', 247 | description: 'Get a specific shared step', 248 | inputSchema: zodToJsonSchema(GetSharedStepSchema), 249 | }, 250 | { 251 | name: 'create_shared_step', 252 | description: 'Create a new shared step', 253 | inputSchema: zodToJsonSchema(CreateSharedStepSchema), 254 | }, 255 | { 256 | name: 'update_shared_step', 257 | description: 'Update an existing shared step', 258 | inputSchema: zodToJsonSchema(UpdateSharedStepSchema), 259 | }, 260 | ], 261 | })); 262 | 263 | /** 264 | * Handler for the create_note tool. 265 | * Creates a new note with the provided title and content, and returns success message. 266 | */ 267 | server.setRequestHandler(CallToolRequestSchema, (request) => 268 | match(request.params) 269 | .with({ name: 'list_projects' }, ({ arguments: args }) => { 270 | const { limit, offset } = ListProjectsSchema.parse(args); 271 | return listProjects(limit, offset); 272 | }) 273 | .with({ name: 'get_project' }, ({ arguments: args }) => { 274 | const { code } = GetProjectSchema.parse(args); 275 | return getProject(code); 276 | }) 277 | .with({ name: 'create_project' }, ({ arguments: args }) => { 278 | const parsedArgs = CreateProjectSchema.parse(args); 279 | return createProject(parsedArgs); 280 | }) 281 | .with({ name: 'get_results' }, ({ arguments: args }) => { 282 | const parsedArgs = GetResultsSchema.parse(args); 283 | const filters = 284 | parsedArgs.status || parsedArgs.from || parsedArgs.to 285 | ? `status=${parsedArgs.status || ''}&from=${parsedArgs.from || ''}&to=${parsedArgs.to || ''}` 286 | : undefined; 287 | return getResults([ 288 | parsedArgs.code, 289 | parsedArgs.limit, 290 | parsedArgs.offset, 291 | filters, 292 | ]); 293 | }) 294 | .with({ name: 'get_result' }, ({ arguments: args }) => { 295 | const { code, hash } = GetResultSchema.parse(args); 296 | return getResult(code, hash); 297 | }) 298 | .with({ name: 'create_result' }, ({ arguments: args }) => { 299 | const { code, id, result } = CreateResultSchema.parse(args); 300 | return createResult(code, id, result); 301 | }) 302 | .with({ name: 'create_result_bulk' }, ({ arguments: args }) => { 303 | const { code, id, results } = CreateResultBulkSchema.parse(args); 304 | return createResultBulk(code, id, results); 305 | }) 306 | .with({ name: 'update_result' }, ({ arguments: args }) => { 307 | const { code, id, hash, result } = UpdateResultSchema.parse(args); 308 | return updateResult(code, id, hash, result); 309 | }) 310 | .with({ name: 'get_cases' }, ({ arguments: args }) => { 311 | const { 312 | code, 313 | search, 314 | milestoneId, 315 | suiteId, 316 | severity, 317 | priority, 318 | type, 319 | behavior, 320 | automation, 321 | status, 322 | externalIssuesType, 323 | externalIssuesIds, 324 | include, 325 | limit, 326 | offset, 327 | } = GetCasesSchema.parse(args); 328 | return getCases([ 329 | code, 330 | search, 331 | milestoneId, 332 | suiteId, 333 | severity, 334 | priority, 335 | type, 336 | behavior, 337 | automation, 338 | status, 339 | externalIssuesType, 340 | externalIssuesIds, 341 | include, 342 | limit, 343 | offset, 344 | ]); 345 | }) 346 | .with({ name: 'get_case' }, ({ arguments: args }) => { 347 | const { code, id } = GetCaseSchema.parse(args); 348 | return getCase(code, id); 349 | }) 350 | .with({ name: 'create_case' }, ({ arguments: args }) => { 351 | const { code, testCase } = CreateCaseSchema.parse(args); 352 | return createCase(code, testCase); 353 | }) 354 | .with({ name: 'update_case' }, ({ arguments: args }) => { 355 | const { code, id, ...caseData } = UpdateCaseSchema.parse(args); 356 | return updateCase(code, id, caseData); 357 | }) 358 | .with({ name: 'get_runs' }, ({ arguments: args }) => { 359 | const { 360 | code, 361 | search, 362 | status, 363 | milestone, 364 | environment, 365 | fromStartTime, 366 | toStartTime, 367 | limit, 368 | offset, 369 | include, 370 | } = GetRunsSchema.parse(args); 371 | return getRuns([ 372 | code, 373 | search, 374 | status, 375 | milestone, 376 | environment, 377 | fromStartTime, 378 | toStartTime, 379 | limit, 380 | offset, 381 | include, 382 | ]); 383 | }) 384 | .with({ name: 'get_run' }, ({ arguments: args }) => { 385 | const { code, id, include } = GetRunSchema.parse(args); 386 | return getRun(code, id, include); 387 | }) 388 | .with({ name: 'get_plans' }, ({ arguments: args }) => { 389 | const { code, limit, offset } = GetPlansSchema.parse(args); 390 | return getPlans(code, limit, offset); 391 | }) 392 | .with({ name: 'get_plan' }, ({ arguments: args }) => { 393 | const { code, id } = GetPlanSchema.parse(args); 394 | return getPlan(code, id); 395 | }) 396 | .with({ name: 'create_plan' }, ({ arguments: args }) => { 397 | const { code, ...planData } = CreatePlanSchema.parse(args); 398 | return createPlan(code, planData); 399 | }) 400 | .with({ name: 'update_plan' }, ({ arguments: args }) => { 401 | const { code, id, ...planData } = UpdatePlanSchema.parse(args); 402 | return updatePlan(code, id, planData); 403 | }) 404 | .with({ name: 'get_suites' }, ({ arguments: args }) => { 405 | const { code, search, limit, offset } = GetSuitesSchema.parse(args); 406 | return getSuites(code, search, limit, offset); 407 | }) 408 | .with({ name: 'get_suite' }, ({ arguments: args }) => { 409 | const { code, id } = GetSuiteSchema.parse(args); 410 | return getSuite(code, id); 411 | }) 412 | .with({ name: 'create_suite' }, ({ arguments: args }) => { 413 | const { code, ...suiteData } = CreateSuiteSchema.parse(args); 414 | return createSuite(code, suiteData); 415 | }) 416 | .with({ name: 'update_suite' }, ({ arguments: args }) => { 417 | const { code, id, ...suiteData } = UpdateSuiteSchema.parse(args); 418 | return updateSuite(code, id, suiteData); 419 | }) 420 | .with({ name: 'get_shared_steps' }, ({ arguments: args }) => { 421 | const { code, search, limit, offset } = GetSharedStepsSchema.parse(args); 422 | return getSharedSteps(code, search, limit, offset); 423 | }) 424 | .with({ name: 'get_shared_step' }, ({ arguments: args }) => { 425 | const { code, hash } = GetSharedStepSchema.parse(args); 426 | return getSharedStep(code, hash); 427 | }) 428 | .with({ name: 'create_shared_step' }, ({ arguments: args }) => { 429 | const { code, ...stepData } = CreateSharedStepSchema.parse(args); 430 | return createSharedStep(code, stepData); 431 | }) 432 | .with({ name: 'update_shared_step' }, ({ arguments: args }) => { 433 | const { code, hash, stepData } = UpdateSharedStepSchema.parse(args); 434 | return updateSharedStep(code, hash, stepData); 435 | }) 436 | .otherwise(() => errAsync('Unknown tool')) 437 | .map((response) => response.data.result) 438 | .map((data) => ({ 439 | content: [{ type: 'text', text: JSON.stringify(data, null, 2) }], 440 | })) 441 | .match( 442 | (data) => data, 443 | (error) => { 444 | throw new Error(error); 445 | }, 446 | ), 447 | ); 448 | 449 | /** 450 | * Handler that lists available prompts. 451 | * Exposes a single "summarize_notes" prompt that summarizes all notes. 452 | */ 453 | server.setRequestHandler(ListPromptsRequestSchema, async () => ({ 454 | prompts: [], 455 | })); 456 | 457 | /** 458 | * Handler for the summarize_notes prompt. 459 | * Returns a prompt that requests summarization of all notes, with the notes' contents embedded as resources. 460 | */ 461 | server.setRequestHandler(GetPromptRequestSchema, async () => ({ 462 | messages: [], 463 | })); 464 | 465 | /** 466 | * Start the server using stdio transport. 467 | * This allows the server to communicate via standard input/output streams. 468 | */ 469 | async function main() { 470 | const transport = new StdioServerTransport(); 471 | await server.connect(transport); 472 | } 473 | 474 | main().catch((error) => { 475 | console.error('Server error:', error); 476 | process.exit(1); 477 | }); 478 | ```