#
tokens: 9019/50000 19/19 files
lines: off (toggle) GitHub
raw markdown copy
# 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:
--------------------------------------------------------------------------------

```
22.13.1

```

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
node_modules/
build/
*.log
.env*
```

--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------

```
{
  "singleQuote": true,
  "trailingComma": "all",
  "printWidth": 80
}

```

--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------

```json
{
  "env": {
    "browser": true,
    "es2021": true
  },
  "extends": [
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended",
    "prettier"
  ],
  "parser": "@typescript-eslint/parser",
  "parserOptions": {
    "ecmaVersion": 12,
    "sourceType": "module"
  },
  "plugins": [
    "@typescript-eslint",
    "prettier"
  ],
  "rules": {
    "prettier/prettier": "error"
  }
}

```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
# QASE MCP Server

MCP server implementation for Qase API

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.

## Features

### Tools
The server provides tools for interacting with the Qase API, allowing you to manage the following entities:

#### Projects
- `list_projects` - Get all projects
- `get_project` - Get project by code
- `create_project` - Create new project
- `delete_project` - Delete project by code

#### Test Cases
- `get_cases` - Get all test cases in a project
- `get_case` - Get a specific test case
- `create_case` - Create a new test case
- `update_case` - Update an existing test case

#### Test Runs
- `get_runs` - Get all test runs in a project
- `get_run` - Get a specific test run

#### Test Results
- `get_results` - Get all test run results for a project
- `get_result` - Get test run result by code and hash
- `create_result` - Create test run result
- `create_result_bulk` - Create multiple test run results in bulk
- `update_result` - Update an existing test run result

#### Test Plans
- `get_plans` - Get all test plans in a project
- `get_plan` - Get a specific test plan
- `create_plan` - Create a new test plan
- `update_plan` - Update an existing test plan
- `delete_plan` - Delete a test plan

#### Test Suites
- `get_suites` - Get all test suites in a project
- `get_suite` - Get a specific test suite
- `create_suite` - Create a new test suite
- `update_suite` - Update an existing test suite
- `delete_suite` - Delete a test suite

#### Shared Steps
- `get_shared_steps` - Get all shared steps in a project
- `get_shared_step` - Get a specific shared step
- `create_shared_step` - Create a new shared step
- `update_shared_step` - Update an existing shared step
- `delete_shared_step` - Delete a shared step

## Development

Install dependencies:
```bash
npm install
```

Build the server:
```bash
npm run build
```

For development with auto-rebuild:
```bash
npm run watch
```

## Installation

### Claude Desktop

To use with Claude Desktop, add the server config:

- On MacOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
- On Windows: `%APPDATA%/Claude/claude_desktop_config.json`

```json
{
  "mcpServers": {
    "mcp-qase": {
      "command": "/path/to/mcp-qase/build/index.js",
      "env": {
        "QASE_API_TOKEN": "<YOUR_TOKEN>"
      }
    }
  }
}
```

### Cursor

To use with Cursor, register the command as follows:

```
env QASE_API_TOKEN=<YOUR_TOKEN> /path/to/mcp-qase/build/index.js
```

## Debugging

Since MCP servers communicate over stdio, debugging can be challenging. We recommend using the [MCP Inspector](https://github.com/modelcontextprotocol/inspector):

```bash
npx -y @modelcontextprotocol/inspector -e QASE_API_TOKEN=<YOUR_TOKEN> ./build/index.js
```
```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "./build",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

```

--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------

```yaml
name: Test

on: [push, pull_request]

jobs:
  lint:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout repository
        uses: actions/checkout@v2

      - name: Set up Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 22

      - name: Install dependencies
        run: npm install

      - name: Run ESLint
        run: npm run lint

```

--------------------------------------------------------------------------------
/src/operations/runs.ts:
--------------------------------------------------------------------------------

```typescript
import { z } from 'zod';
import { toResult } from '../utils.js';
import { apply, pipe } from 'ramda';
import { client } from '../utils.js';

export const GetRunsSchema = z.object({
  code: z.string(),
  search: z.string().optional(),
  status: z.string().optional(),
  milestone: z.number().optional(),
  environment: z.number().optional(),
  fromStartTime: z.number().optional(),
  toStartTime: z.number().optional(),
  limit: z.number().optional(),
  offset: z.number().optional(),
  include: z.string().optional(),
});

export const GetRunSchema = z.object({
  code: z.string(),
  id: z.number(),
  include: z.enum(['cases']).optional(),
});

export const getRuns = pipe(
  apply(client.runs.getRuns.bind(client.runs)),
  toResult,
);

export const getRun = pipe(client.runs.getRun.bind(client.runs), toResult);

```

--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------

```javascript
export default {
  languageOptions: {
    ecmaVersion: 12,
    sourceType: "module",
    globals: {
      browser: true,
      es2021: true,
    },
  },
  files: ["**/*.ts", "**/*.tsx"],
  plugins: {
    "@typescript-eslint": (await import("@typescript-eslint/eslint-plugin")).default,
    "prettier": (await import("eslint-plugin-prettier")).default,
  },
  rules: {
    ...(await import("eslint-config-prettier")).default.rules,
    ...(await import("eslint-plugin-prettier")).default.configs.recommended.rules,
    ...(await import("@typescript-eslint/eslint-plugin")).default.configs.recommended.rules,
  },
  languageOptions: {
    parser: (await import("@typescript-eslint/parser")).default,
    ecmaVersion: 12,
    sourceType: "module",
    globals: {
      browser: true,
      es2021: true,
    },
  },
};

```

--------------------------------------------------------------------------------
/src/operations/projects.ts:
--------------------------------------------------------------------------------

```typescript
import { ProjectCreateAccessEnum } from 'qaseio';
import { z } from 'zod';
import { client, toResult } from '../utils.js';
import { pipe } from 'ramda';

export const ListProjectsSchema = z.object({
  limit: z.number().optional(),
  offset: z.number().optional(),
});

export const GetProjectSchema = z.object({
  code: z.string(),
});

export const CreateProjectSchema = z.object({
  code: z.string(),
  title: z.string(),
  description: z.string().optional(),
  access: z.nativeEnum(ProjectCreateAccessEnum).optional(),
  group: z.string().optional(),
});

export const listProjects = pipe(
  client.projects.getProjects.bind(client.projects),
  toResult,
);

export const getProject = pipe(
  client.projects.getProject.bind(client.projects),
  toResult,
);

export const createProject = pipe(
  client.projects.createProject.bind(client.projects),
  toResult,
);

```

--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------

```typescript
import { AxiosResponse } from 'axios';
import { ResultAsync } from 'neverthrow';
import { QaseApi } from 'qaseio';

export type ApiError = {
  response?: { data?: { message?: string } };
  message: string;
};

export type ApiResponse<T> = {
  data: {
    result?: T;
    status: boolean;
    errorMessage?: string;
  };
};

export const formatApiError = (error: unknown) => {
  const apiError = error as ApiError;
  return apiError.response?.data?.message || apiError.message;
};

export const toResult = (promise: Promise<AxiosResponse>) =>
  ResultAsync.fromPromise(promise, formatApiError);

export const client = (({ QASE_API_TOKEN }) => {
  if (!QASE_API_TOKEN) {
    throw new Error(
      'QASE_API_TOKEN environment variable is required. Please set it before running the server.',
    );
  }
  return new QaseApi({
    token: QASE_API_TOKEN,
  });
})(process.env);

```

--------------------------------------------------------------------------------
/src/operations/plans.ts:
--------------------------------------------------------------------------------

```typescript
import { z } from 'zod';
import { client, toResult } from '../utils.js';
import { pipe } from 'ramda';

export const GetPlansSchema = z.object({
  code: z.string(),
  limit: z.number().optional(),
  offset: z.number().optional(),
});

export const GetPlanSchema = z.object({
  code: z.string(),
  id: z.number(),
});

export const CreatePlanSchema = z.object({
  code: z.string(),
  title: z.string(),
  description: z.string().optional(),
  cases: z.array(z.number()),
});

export const UpdatePlanSchema = z.object({
  code: z.string(),
  id: z.number(),
  title: z.string().optional(),
  description: z.string().optional(),
  cases: z.array(z.number()).optional(),
});

export const getPlans = pipe(
  client.plans.getPlans.bind(client.plans),
  toResult,
);

export const getPlan = pipe(client.plans.getPlan.bind(client.plans), toResult);

export const createPlan = pipe(
  client.plans.createPlan.bind(client.plans),
  toResult,
);

export const updatePlan = pipe(
  client.plans.updatePlan.bind(client.plans),
  toResult,
);

```

--------------------------------------------------------------------------------
/src/operations/suites.ts:
--------------------------------------------------------------------------------

```typescript
import { z } from 'zod';
import { client, toResult } from '../utils.js';
import { pipe } from 'ramda';

export const GetSuitesSchema = z.object({
  code: z.string(),
  search: z.string().optional(),
  limit: z.number().optional(),
  offset: z.number().optional(),
});

export const GetSuiteSchema = z.object({
  code: z.string(),
  id: z.number(),
});

export const CreateSuiteSchema = z.object({
  code: z.string(),
  title: z.string(),
  description: z.string().optional(),
  preconditions: z.string().optional(),
  parent_id: z.number().optional(),
});

export const UpdateSuiteSchema = z.object({
  code: z.string(),
  id: z.number(),
  title: z.string().optional(),
  description: z.string().optional(),
  preconditions: z.string().optional(),
  parent_id: z.number().optional(),
});

export const getSuites = pipe(
  client.suites.getSuites.bind(client.suites),
  toResult,
);

export const getSuite = pipe(
  client.suites.getSuite.bind(client.suites),
  toResult,
);

export const createSuite = pipe(
  client.suites.createSuite.bind(client.suites),
  toResult,
);

export const updateSuite = pipe(
  client.suites.updateSuite.bind(client.suites),
  toResult,
);

```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
{
  "name": "mcp-qase",
  "version": "0.1.0",
  "description": "MCP server implementation for Qase API",
  "private": true,
  "type": "module",
  "bin": {
    "mcp-qase": "./build/index.js"
  },
  "files": [
    "build"
  ],
  "scripts": {
    "build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"",
    "prepare": "npm run build",
    "watch": "tsc --watch",
    "inspector": "npx @modelcontextprotocol/inspector build/index.js",
    "lint": "eslint src --ext .ts",
    "lint:fix": "eslint . --ext .ts --fix"
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "0.6.0",
    "@types/ramda": "^0.30.2",
    "neverthrow": "^8.2.0",
    "qaseio": "^2.4.1",
    "ramda": "^0.30.1",
    "ts-pattern": "^5.6.2",
    "zod": "^3.24.2",
    "zod-to-json-schema": "^3.24.3"
  },
  "devDependencies": {
    "@types/node": "^20.11.24",
    "@typescript-eslint/eslint-plugin": "^8.26.1",
    "eslint": "^9.22.0",
    "eslint-config-prettier": "^10.1.1",
    "eslint-plugin-prettier": "^5.2.3",
    "lint-staged": "^12.0.0",
    "typescript": "^5.3.3"
  },
  "lint-staged": {
    "*.{js,ts,tsx}": [
      "eslint --fix",
      "prettier --write"
    ]
  }
}

```

--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile
FROM debian:bullseye-slim

ENV DEBIAN_FRONTEND=noninteractive \
    GLAMA_VERSION="0.2.0" \
    PATH="/home/service-user/.local/bin:${PATH}"

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")

USER service-user

WORKDIR /app

RUN git clone https://github.com/rikuson/mcp-qase .

RUN (npm install) && (npm run build)

CMD ["mcp-proxy","node","./build/index.js"]

```

--------------------------------------------------------------------------------
/src/operations/results.ts:
--------------------------------------------------------------------------------

```typescript
import {
  TestStepResultCreateStatusEnum,
  ResultCreate,
  ResultCreateBulk,
  ResultUpdate,
} from 'qaseio';
import { z } from 'zod';
import { toResult } from '../utils.js';
import { apply, pipe } from 'ramda';
import { client } from '../utils.js';

export const GetResultsSchema = z.object({
  code: z.string(),
  limit: z.string().optional(),
  offset: z.string().optional(),
  status: z.nativeEnum(TestStepResultCreateStatusEnum).optional(),
  from: z.string().optional(),
  to: z.string().optional(),
});

export const GetResultSchema = z.object({
  code: z.string(),
  hash: z.string(),
});

export const CreateResultSchema = z.object({
  code: z.string(),
  id: z.number(),
  result: z.record(z.any()).transform((v) => v as ResultCreate),
});

export const CreateResultBulkSchema = z.object({
  code: z.string(),
  id: z.number(),
  results: z.record(z.any()).transform((v) => v as ResultCreateBulk),
});

export const UpdateResultSchema = z.object({
  code: z.string(),
  id: z.number(),
  hash: z.string(),
  result: z.record(z.any()).transform((v) => v as ResultUpdate),
});

export const getResults = pipe(
  apply(client.results.getResults.bind(client.results)),
  toResult,
);

export const getResult = pipe(
  client.results.getResult.bind(client.results),
  toResult,
);

export const createResult = pipe(
  client.results.createResult.bind(client.results),
  toResult,
);

export const createResultBulk = pipe(
  client.results.createResultBulk.bind(client.results),
  toResult,
);

export const updateResult = pipe(
  client.results.updateResult.bind(client.results),
  toResult,
);

```

--------------------------------------------------------------------------------
/src/operations/shared-steps.ts:
--------------------------------------------------------------------------------

```typescript
import { SharedStepUpdate } from 'qaseio';
import { z } from 'zod';
import { client, toResult } from '../utils.js';
import { pipe } from 'ramda';

export const GetSharedStepsSchema = z.object({
  code: z.string(),
  search: z.string().optional(),
  limit: z.number().optional(),
  offset: z.number().optional(),
});

export const GetSharedStepSchema = z.object({
  code: z.string(),
  hash: z.string(),
});

export const CreateSharedStepSchema = z.object({
  code: z.string(),
  title: z.string(),
  action: z.string(),
  expected_result: z.string().optional(),
  data: z.string().optional(),
  steps: z
    .array(
      z.object({
        action: z.string(),
        expected_result: z.string().optional(),
        data: z.string().optional(),
        position: z.number().optional(),
      }),
    )
    .optional(),
});

export const UpdateSharedStepSchema = z
  .object({
    code: z.string(),
    hash: z.string(),
    title: z.string(),
    action: z.string(),
    expected_result: z.string().optional(),
    data: z.string().optional(),
    steps: z
      .array(
        z.object({
          action: z.string(),
          expected_result: z.string().optional(),
          data: z.string().optional(),
          position: z.number().optional(),
        }),
      )
      .optional(),
  })
  .transform((data) => ({
    code: data.code,
    hash: data.hash,
    stepData: {
      title: data.title,
      action: data.action,
      expected_result: data.expected_result,
      data: data.data,
      steps: data.steps,
    } as SharedStepUpdate,
  }));

export const getSharedSteps = pipe(
  client.sharedSteps.getSharedSteps.bind(client.sharedSteps),
  toResult,
);

export const getSharedStep = pipe(
  client.sharedSteps.getSharedStep.bind(client.sharedSteps),
  toResult,
);

export const createSharedStep = pipe(
  client.sharedSteps.createSharedStep.bind(client.sharedSteps),
  toResult,
);

export const updateSharedStep = pipe(
  client.sharedSteps.updateSharedStep.bind(client.sharedSteps),
  toResult,
);

```

--------------------------------------------------------------------------------
/src/operations/cases.ts:
--------------------------------------------------------------------------------

```typescript
import { TestCaseCreate } from 'qaseio';
import { z } from 'zod';
import { client, toResult } from '../utils.js';
import { apply, pipe } from 'ramda';

export const GetCasesSchema = z.object({
  code: z.string(),
  search: z.string().optional(),
  milestoneId: z.number().optional(),
  suiteId: z.number().optional(),
  severity: z.string().optional(),
  priority: z.string().optional(),
  type: z.string().optional(),
  behavior: z.string().optional(),
  automation: z.string().optional(),
  status: z.string().optional(),
  externalIssuesType: z
    .enum([
      'asana',
      'azure-devops',
      'clickup-app',
      'github-app',
      'gitlab-app',
      'jira-cloud',
      'jira-server',
      'linear',
      'monday',
      'redmine-app',
      'trello-app',
      'youtrack-app',
    ])
    .optional(),
  externalIssuesIds: z.array(z.string()).optional(),
  include: z.string().optional(),
  limit: z.number().optional(),
  offset: z.number().optional(),
});

export const GetCaseSchema = z.object({
  code: z.string(),
  id: z.number(),
});

export const CreateCaseSchema = z.object({
  code: z.string(),
  testCase: z.record(z.any()).transform((v) => v as TestCaseCreate),
});

export const UpdateCaseSchema = z.object({
  code: z.string(),
  id: z.number(),
  title: z.string().optional(),
  description: z.string().optional(),
  preconditions: z.string().optional(),
  postconditions: z.string().optional(),
  severity: z.number().optional(),
  priority: z.number().optional(),
  type: z.number().optional(),
  behavior: z.number().optional(),
  automation: z.number().optional(),
  status: z.number().optional(),
  suite_id: z.number().optional(),
  milestone_id: z.number().optional(),
  layer: z.number().optional(),
  is_flaky: z.boolean().optional(),
  params: z
    .array(
      z.object({
        title: z.string(),
        value: z.string(),
      }),
    )
    .optional(),
  tags: z.array(z.string()).optional(),
  steps: z
    .array(
      z.object({
        action: z.string(),
        expected_result: z.string().optional(),
        data: z.string().optional(),
        position: z.number().optional(),
      }),
    )
    .optional(),
  custom_fields: z
    .array(
      z.object({
        id: z.number(),
        value: z.string(),
      }),
    )
    .optional(),
});

export const CreateCaseBulkSchema = z.object({
  code: z.string(),
  cases: z.array(
    z.object({
      title: z.string(),
      description: z.string().optional(),
      preconditions: z.string().optional(),
      postconditions: z.string().optional(),
      severity: z.number().optional(),
      priority: z.number().optional(),
      type: z.number().optional(),
      behavior: z.number().optional(),
      automation: z.number().optional(),
      status: z.number().optional(),
      suite_id: z.number().optional(),
      milestone_id: z.number().optional(),
      layer: z.number().optional(),
      is_flaky: z.boolean().optional(),
      params: z
        .array(
          z.object({
            title: z.string(),
            value: z.string(),
          }),
        )
        .optional(),
      tags: z.array(z.string()).optional(),
      steps: z
        .array(
          z.object({
            action: z.string(),
            expected_result: z.string().optional(),
            data: z.string().optional(),
            position: z.number().optional(),
          }),
        )
        .optional(),
      custom_fields: z
        .array(
          z.object({
            id: z.number(),
            value: z.string(),
          }),
        )
        .optional(),
    }),
  ),
});

export const getCases = pipe(
  apply(client.cases.getCases.bind(client.cases)),
  toResult,
);

export const getCase = pipe(client.cases.getCase.bind(client.cases), toResult);

export const createCase = pipe(
  client.cases.createCase.bind(client.cases),
  toResult,
);

const convertCaseData = (
  data: Omit<z.infer<typeof UpdateCaseSchema>, 'code' | 'id'>,
) => ({
  ...data,
  is_flaky: data.is_flaky === undefined ? undefined : data.is_flaky ? 1 : 0,
  params: data.params
    ? data.params.reduce(
        (acc, param) => ({
          ...acc,
          [param.title]: [param.value],
        }),
        {},
      )
    : undefined,
});

export const updateCase = pipe(
  (
    code: string,
    id: number,
    data: Omit<z.infer<typeof UpdateCaseSchema>, 'code' | 'id'>,
  ) => client.cases.updateCase(code, id, convertCaseData(data)),
  toResult,
);

```

--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------

```typescript
#!/usr/bin/env node

/**
 * This is a template MCP server that implements a simple notes system.
 * It demonstrates core MCP concepts like resources and tools by allowing:
 * - Listing notes as resources
 * - Reading individual notes
 * - Creating new notes via a tool
 * - Summarizing all notes via a prompt
 */

import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
  CallToolRequestSchema,
  ListResourcesRequestSchema,
  ListToolsRequestSchema,
  ReadResourceRequestSchema,
  ListPromptsRequestSchema,
  GetPromptRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import { zodToJsonSchema } from 'zod-to-json-schema';
import {
  listProjects,
  getProject,
  createProject,
  CreateProjectSchema,
  GetProjectSchema,
  ListProjectsSchema,
} from './operations/projects.js';
import {
  getResults,
  getResult,
  createResult,
  CreateResultSchema,
  GetResultSchema,
  GetResultsSchema,
  CreateResultBulkSchema,
  createResultBulk,
  UpdateResultSchema,
  updateResult,
} from './operations/results.js';
import {
  getCases,
  getCase,
  createCase,
  updateCase,
  GetCasesSchema,
  GetCaseSchema,
  CreateCaseSchema,
  UpdateCaseSchema,
} from './operations/cases.js';
import {
  getRuns,
  getRun,
  GetRunsSchema,
  GetRunSchema,
} from './operations/runs.js';
import {
  getPlans,
  getPlan,
  createPlan,
  updatePlan,
  GetPlansSchema,
  GetPlanSchema,
  CreatePlanSchema,
  UpdatePlanSchema,
} from './operations/plans.js';
import {
  GetSuitesSchema,
  GetSuiteSchema,
  CreateSuiteSchema,
  UpdateSuiteSchema,
  getSuites,
  getSuite,
  createSuite,
  updateSuite,
} from './operations/suites.js';
import {
  GetSharedStepsSchema,
  GetSharedStepSchema,
  CreateSharedStepSchema,
  UpdateSharedStepSchema,
  getSharedSteps,
  getSharedStep,
  createSharedStep,
  updateSharedStep,
} from './operations/shared-steps.js';
import { match } from 'ts-pattern';
import { errAsync } from 'neverthrow';

/**
 * Create an MCP server with capabilities for resources (to list/read notes),
 * tools (to create new notes), and prompts (to summarize notes).
 */
const server = new Server(
  {
    name: 'mcp-qase',
    version: '0.1.0',
  },
  {
    capabilities: {
      resources: {},
      tools: {},
      prompts: {},
    },
  },
);

/**
 * Handler for listing available notes as resources.
 */
server.setRequestHandler(ListResourcesRequestSchema, () => ({
  resources: [],
}));

/**
 * Handler for reading the contents
 */
server.setRequestHandler(ReadResourceRequestSchema, () => ({
  contents: [],
}));

/**
 * Handler that lists available tools.
 * Exposes a single "create_note" tool that lets clients create new notes.
 */
server.setRequestHandler(ListToolsRequestSchema, () => ({
  tools: [
    {
      name: 'list_projects',
      description: 'Get All Projects',
      inputSchema: zodToJsonSchema(ListProjectsSchema),
    },
    {
      name: 'get_project',
      description: 'Get project by code',
      inputSchema: zodToJsonSchema(GetProjectSchema),
    },
    {
      name: 'create_project',
      description: 'Create new project',
      inputSchema: zodToJsonSchema(CreateProjectSchema),
    },
    {
      name: 'get_results',
      description: 'Get all test run results for a project',
      inputSchema: zodToJsonSchema(GetResultsSchema),
    },
    {
      name: 'get_result',
      description: 'Get test run result by code and hash',
      inputSchema: zodToJsonSchema(GetResultSchema),
    },
    {
      name: 'create_result',
      description: 'Create test run result',
      inputSchema: zodToJsonSchema(CreateResultSchema),
    },
    {
      name: 'create_result_bulk',
      description: 'Create multiple test run results in bulk',
      inputSchema: zodToJsonSchema(CreateResultBulkSchema),
    },
    {
      name: 'update_result',
      description: 'Update an existing test run result',
      inputSchema: zodToJsonSchema(UpdateResultSchema),
    },
    {
      name: 'get_cases',
      description: 'Get all test cases in a project',
      inputSchema: zodToJsonSchema(GetCasesSchema),
    },
    {
      name: 'get_case',
      description: 'Get a specific test case',
      inputSchema: zodToJsonSchema(GetCaseSchema),
    },
    {
      name: 'create_case',
      description: 'Create a new test case',
      inputSchema: zodToJsonSchema(CreateCaseSchema),
    },
    {
      name: 'update_case',
      description: 'Update an existing test case',
      inputSchema: zodToJsonSchema(UpdateCaseSchema),
    },
    {
      name: 'get_runs',
      description: 'Get all test runs in a project',
      inputSchema: zodToJsonSchema(GetRunsSchema),
    },
    {
      name: 'get_run',
      description: 'Get a specific test run',
      inputSchema: zodToJsonSchema(GetRunSchema),
    },
    {
      name: 'get_plans',
      description: 'Get all test plans in a project',
      inputSchema: zodToJsonSchema(GetPlansSchema),
    },
    {
      name: 'get_plan',
      description: 'Get a specific test plan',
      inputSchema: zodToJsonSchema(GetPlanSchema),
    },
    {
      name: 'create_plan',
      description: 'Create a new test plan',
      inputSchema: zodToJsonSchema(CreatePlanSchema),
    },
    {
      name: 'update_plan',
      description: 'Update an existing test plan',
      inputSchema: zodToJsonSchema(UpdatePlanSchema),
    },
    {
      name: 'get_suites',
      description: 'Get all test suites in a project',
      inputSchema: zodToJsonSchema(GetSuitesSchema),
    },
    {
      name: 'get_suite',
      description: 'Get a specific test suite',
      inputSchema: zodToJsonSchema(GetSuiteSchema),
    },
    {
      name: 'create_suite',
      description: 'Create a new test suite',
      inputSchema: zodToJsonSchema(CreateSuiteSchema),
    },
    {
      name: 'update_suite',
      description: 'Update an existing test suite',
      inputSchema: zodToJsonSchema(UpdateSuiteSchema),
    },
    {
      name: 'get_shared_steps',
      description: 'Get all shared steps in a project',
      inputSchema: zodToJsonSchema(GetSharedStepsSchema),
    },
    {
      name: 'get_shared_step',
      description: 'Get a specific shared step',
      inputSchema: zodToJsonSchema(GetSharedStepSchema),
    },
    {
      name: 'create_shared_step',
      description: 'Create a new shared step',
      inputSchema: zodToJsonSchema(CreateSharedStepSchema),
    },
    {
      name: 'update_shared_step',
      description: 'Update an existing shared step',
      inputSchema: zodToJsonSchema(UpdateSharedStepSchema),
    },
  ],
}));

/**
 * Handler for the create_note tool.
 * Creates a new note with the provided title and content, and returns success message.
 */
server.setRequestHandler(CallToolRequestSchema, (request) =>
  match(request.params)
    .with({ name: 'list_projects' }, ({ arguments: args }) => {
      const { limit, offset } = ListProjectsSchema.parse(args);
      return listProjects(limit, offset);
    })
    .with({ name: 'get_project' }, ({ arguments: args }) => {
      const { code } = GetProjectSchema.parse(args);
      return getProject(code);
    })
    .with({ name: 'create_project' }, ({ arguments: args }) => {
      const parsedArgs = CreateProjectSchema.parse(args);
      return createProject(parsedArgs);
    })
    .with({ name: 'get_results' }, ({ arguments: args }) => {
      const parsedArgs = GetResultsSchema.parse(args);
      const filters =
        parsedArgs.status || parsedArgs.from || parsedArgs.to
          ? `status=${parsedArgs.status || ''}&from=${parsedArgs.from || ''}&to=${parsedArgs.to || ''}`
          : undefined;
      return getResults([
        parsedArgs.code,
        parsedArgs.limit,
        parsedArgs.offset,
        filters,
      ]);
    })
    .with({ name: 'get_result' }, ({ arguments: args }) => {
      const { code, hash } = GetResultSchema.parse(args);
      return getResult(code, hash);
    })
    .with({ name: 'create_result' }, ({ arguments: args }) => {
      const { code, id, result } = CreateResultSchema.parse(args);
      return createResult(code, id, result);
    })
    .with({ name: 'create_result_bulk' }, ({ arguments: args }) => {
      const { code, id, results } = CreateResultBulkSchema.parse(args);
      return createResultBulk(code, id, results);
    })
    .with({ name: 'update_result' }, ({ arguments: args }) => {
      const { code, id, hash, result } = UpdateResultSchema.parse(args);
      return updateResult(code, id, hash, result);
    })
    .with({ name: 'get_cases' }, ({ arguments: args }) => {
      const {
        code,
        search,
        milestoneId,
        suiteId,
        severity,
        priority,
        type,
        behavior,
        automation,
        status,
        externalIssuesType,
        externalIssuesIds,
        include,
        limit,
        offset,
      } = GetCasesSchema.parse(args);
      return getCases([
        code,
        search,
        milestoneId,
        suiteId,
        severity,
        priority,
        type,
        behavior,
        automation,
        status,
        externalIssuesType,
        externalIssuesIds,
        include,
        limit,
        offset,
      ]);
    })
    .with({ name: 'get_case' }, ({ arguments: args }) => {
      const { code, id } = GetCaseSchema.parse(args);
      return getCase(code, id);
    })
    .with({ name: 'create_case' }, ({ arguments: args }) => {
      const { code, testCase } = CreateCaseSchema.parse(args);
      return createCase(code, testCase);
    })
    .with({ name: 'update_case' }, ({ arguments: args }) => {
      const { code, id, ...caseData } = UpdateCaseSchema.parse(args);
      return updateCase(code, id, caseData);
    })
    .with({ name: 'get_runs' }, ({ arguments: args }) => {
      const {
        code,
        search,
        status,
        milestone,
        environment,
        fromStartTime,
        toStartTime,
        limit,
        offset,
        include,
      } = GetRunsSchema.parse(args);
      return getRuns([
        code,
        search,
        status,
        milestone,
        environment,
        fromStartTime,
        toStartTime,
        limit,
        offset,
        include,
      ]);
    })
    .with({ name: 'get_run' }, ({ arguments: args }) => {
      const { code, id, include } = GetRunSchema.parse(args);
      return getRun(code, id, include);
    })
    .with({ name: 'get_plans' }, ({ arguments: args }) => {
      const { code, limit, offset } = GetPlansSchema.parse(args);
      return getPlans(code, limit, offset);
    })
    .with({ name: 'get_plan' }, ({ arguments: args }) => {
      const { code, id } = GetPlanSchema.parse(args);
      return getPlan(code, id);
    })
    .with({ name: 'create_plan' }, ({ arguments: args }) => {
      const { code, ...planData } = CreatePlanSchema.parse(args);
      return createPlan(code, planData);
    })
    .with({ name: 'update_plan' }, ({ arguments: args }) => {
      const { code, id, ...planData } = UpdatePlanSchema.parse(args);
      return updatePlan(code, id, planData);
    })
    .with({ name: 'get_suites' }, ({ arguments: args }) => {
      const { code, search, limit, offset } = GetSuitesSchema.parse(args);
      return getSuites(code, search, limit, offset);
    })
    .with({ name: 'get_suite' }, ({ arguments: args }) => {
      const { code, id } = GetSuiteSchema.parse(args);
      return getSuite(code, id);
    })
    .with({ name: 'create_suite' }, ({ arguments: args }) => {
      const { code, ...suiteData } = CreateSuiteSchema.parse(args);
      return createSuite(code, suiteData);
    })
    .with({ name: 'update_suite' }, ({ arguments: args }) => {
      const { code, id, ...suiteData } = UpdateSuiteSchema.parse(args);
      return updateSuite(code, id, suiteData);
    })
    .with({ name: 'get_shared_steps' }, ({ arguments: args }) => {
      const { code, search, limit, offset } = GetSharedStepsSchema.parse(args);
      return getSharedSteps(code, search, limit, offset);
    })
    .with({ name: 'get_shared_step' }, ({ arguments: args }) => {
      const { code, hash } = GetSharedStepSchema.parse(args);
      return getSharedStep(code, hash);
    })
    .with({ name: 'create_shared_step' }, ({ arguments: args }) => {
      const { code, ...stepData } = CreateSharedStepSchema.parse(args);
      return createSharedStep(code, stepData);
    })
    .with({ name: 'update_shared_step' }, ({ arguments: args }) => {
      const { code, hash, stepData } = UpdateSharedStepSchema.parse(args);
      return updateSharedStep(code, hash, stepData);
    })
    .otherwise(() => errAsync('Unknown tool'))
    .map((response) => response.data.result)
    .map((data) => ({
      content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
    }))
    .match(
      (data) => data,
      (error) => {
        throw new Error(error);
      },
    ),
);

/**
 * Handler that lists available prompts.
 * Exposes a single "summarize_notes" prompt that summarizes all notes.
 */
server.setRequestHandler(ListPromptsRequestSchema, async () => ({
  prompts: [],
}));

/**
 * Handler for the summarize_notes prompt.
 * Returns a prompt that requests summarization of all notes, with the notes' contents embedded as resources.
 */
server.setRequestHandler(GetPromptRequestSchema, async () => ({
  messages: [],
}));

/**
 * Start the server using stdio transport.
 * This allows the server to communicate via standard input/output streams.
 */
async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
}

main().catch((error) => {
  console.error('Server error:', error);
  process.exit(1);
});

```