#
tokens: 12202/50000 19/19 files
lines: on (toggle) GitHub
raw markdown copy reset
# 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 | 
```