├── .editorconfig ├── .env.example ├── .eslintrc.json ├── .github └── workflows │ └── publish.yml ├── .gitignore ├── .prettierrc ├── .vscode ├── extensions.json └── settings.json ├── DEVELOPMENT.md ├── Dockerfile ├── LICENSE.md ├── README.md ├── TOOLS.md ├── docs └── linear-app-icon.png ├── glama.json ├── jest.config.js ├── nodemon.json ├── package-lock.json ├── package.json ├── smithery.yaml ├── src ├── index.ts ├── mcp-server.ts ├── services │ └── linear-service.ts ├── tools │ ├── definitions │ │ ├── cycle-tools.ts │ │ ├── index.ts │ │ ├── issue-tools.ts │ │ ├── project-tools.ts │ │ ├── team-tools.ts │ │ └── user-tools.ts │ ├── handlers │ │ ├── cycle-handlers.ts │ │ ├── index.ts │ │ ├── issue-handlers.ts │ │ ├── project-handlers.ts │ │ ├── team-handlers.ts │ │ └── user-handlers.ts │ └── type-guards.ts ├── types.ts └── utils │ └── config.ts ├── tsconfig.json └── tsconfig.node.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | 9 | [*.md] 10 | trim_trailing_whitespace = false 11 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Server Configuration 2 | PORT=3000 3 | NODE_ENV=development 4 | 5 | # Linear API 6 | LINEAR_API_URL=https://api.linear.app/graphql 7 | LINEAR_API_TOKEN= 8 | 9 | # Security 10 | CORS_ORIGINS=http://localhost:3000,https://your-deployed-domain.com 11 | RATE_LIMIT_WINDOW_MS=60000 12 | RATE_LIMIT_MAX=100 13 | 14 | # Logging 15 | LOG_LEVEL=info 16 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true, 4 | "es2022": true, 5 | "jest": true 6 | }, 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:@typescript-eslint/recommended", 10 | "prettier" 11 | ], 12 | "parser": "@typescript-eslint/parser", 13 | "parserOptions": { 14 | "ecmaVersion": "latest", 15 | "sourceType": "module" 16 | }, 17 | "plugins": ["@typescript-eslint"], 18 | "rules": { 19 | "no-console": "warn", 20 | "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }], 21 | "@typescript-eslint/explicit-function-return-type": "off", 22 | "@typescript-eslint/explicit-module-boundary-types": "off" 23 | } 24 | } -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to npm 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | version: 7 | description: 'Version bump type (patch, minor, major)' 8 | required: true 9 | default: 'patch' 10 | type: choice 11 | options: 12 | - patch 13 | - minor 14 | - major 15 | 16 | jobs: 17 | publish: 18 | runs-on: ubuntu-latest 19 | permissions: 20 | contents: write 21 | steps: 22 | - name: Checkout repository 23 | uses: actions/checkout@v3 24 | with: 25 | fetch-depth: 0 26 | 27 | - name: Setup Node.js 28 | uses: actions/setup-node@v3 29 | with: 30 | node-version: '20.x' 31 | registry-url: 'https://registry.npmjs.org/' 32 | 33 | - name: Install dependencies 34 | run: npm ci 35 | 36 | - name: Run tests 37 | run: npm test 38 | 39 | - name: Build 40 | run: npm run build 41 | 42 | - name: Version bump 43 | id: version 44 | run: | 45 | git config --global user.name 'GitHub Action' 46 | git config --global user.email 'action@github.com' 47 | npm version ${{ github.event.inputs.version }} -m "Bump version to %s [skip ci]" 48 | echo "VERSION=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT 49 | 50 | - name: Push changes 51 | run: git push --follow-tags 52 | 53 | - name: Create GitHub Release 54 | uses: softprops/action-gh-release@v1 55 | with: 56 | tag_name: v${{ steps.version.outputs.VERSION }} 57 | generate_release_notes: true 58 | 59 | - name: Publish to npm 60 | run: npm publish --access public 61 | env: 62 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules/ 3 | 4 | # Build 5 | /dist 6 | /build 7 | 8 | # Environment variables 9 | .env 10 | .env.local 11 | .env.development.local 12 | .env.test.local 13 | .env.production.local 14 | 15 | # Logs 16 | logs 17 | *.log 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | 22 | # Editor directories and files 23 | .idea 24 | *.suo 25 | *.ntvs* 26 | *.njsproj 27 | *.sln 28 | *.sw? 29 | 30 | # OS files 31 | .DS_Store 32 | 33 | # Testing 34 | /coverage 35 | 36 | # Temporary files 37 | /tmp 38 | ____* -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "trailingComma": "all", 4 | "singleQuote": true, 5 | "printWidth": 100, 6 | "tabWidth": 2 7 | } -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "esbenp.prettier-vscode", 4 | "dbaeumer.vscode-eslint", 5 | "editorconfig.editorconfig" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "editor.defaultFormatter": "esbenp.prettier-vscode", 4 | "editor.codeActionsOnSave": { 5 | "source.fixAll.eslint": "explicit" 6 | }, 7 | "[typescript]": { 8 | "editor.defaultFormatter": "esbenp.prettier-vscode" 9 | }, 10 | "[javascript]": { 11 | "editor.defaultFormatter": "esbenp.prettier-vscode" 12 | }, 13 | "[json]": { 14 | "editor.defaultFormatter": "esbenp.prettier-vscode" 15 | }, 16 | "[jsonc]": { 17 | "editor.defaultFormatter": "esbenp.prettier-vscode" 18 | }, 19 | "prettier.useEditorConfig": true, 20 | "prettier.requireConfig": true, 21 | "typescript.tsdk": "node_modules/typescript/lib", 22 | "typescript.enablePromptUseWorkspaceTsdk": true, 23 | "javascript.preferences.importModuleSpecifier": "non-relative", 24 | "typescript.preferences.importModuleSpecifier": "non-relative" 25 | } 26 | -------------------------------------------------------------------------------- /DEVELOPMENT.md: -------------------------------------------------------------------------------- 1 | ## Development 2 | 3 | To develop locally: 4 | 5 | ```bash 6 | # Clone the repository 7 | git clone https://github.com/tacticlaunch/mcp-linear.git 8 | cd mcp-linear 9 | 10 | # Install dependencies 11 | npm install 12 | 13 | # Run in development mode 14 | npm run dev -- --token YOUR_LINEAR_API_TOKEN 15 | ``` 16 | 17 | ### Inspecting the server 18 | 19 | To inspect the server by @modelcontextprotocol/inspector: 20 | 21 | ```bash 22 | npm run inspect -- -e LINEAR_API_TOKEN=YOUR_LINEAR_API_TOKEN 23 | ``` 24 | 25 | ### Extending the Server 26 | 27 | To add new tools to the server: 28 | 29 | 1. Follow the implementation guide in the [TOOLS.md](./TOOLS.md) document 30 | 2. Make sure to follow the established code structure in the `src/` directory 31 | 3. Update the documentation to reflect your changes 32 | 33 | ### Publishing to npm 34 | 35 | To publish this package to npm: 36 | 37 | 1. Update the version in package.json 38 | 39 | ```bash 40 | npm version patch # or minor, or major 41 | ``` 42 | 43 | 2. Build the project 44 | 45 | ```bash 46 | npm run build 47 | ``` 48 | 49 | 3. Make sure you've already logged in to npm 50 | 51 | ```bash 52 | npm login 53 | ``` 54 | 55 | 4. Publish to npm 56 | 57 | ```bash 58 | npm publish --access public 59 | ``` 60 | 61 | 5. For Smithery registry, you'll need to work with the Smithery team to get your server listed in their catalog. 62 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile 2 | FROM node:lts-alpine AS builder 3 | 4 | WORKDIR /app 5 | 6 | # Copy package.json and package-lock.json if exists 7 | COPY package.json ./ 8 | 9 | # Install dependencies without running scripts to avoid issues 10 | RUN npm install --ignore-scripts 11 | 12 | # Copy the rest of the files 13 | COPY . ./ 14 | 15 | # Build the project 16 | RUN npm run build 17 | 18 | 19 | FROM node:lts-alpine 20 | 21 | WORKDIR /app 22 | 23 | # Copy only necessary files for runtime 24 | COPY package.json ./ 25 | COPY --from=builder /app/dist ./dist 26 | 27 | # Install only production dependencies 28 | RUN npm install --production --ignore-scripts 29 | 30 | EXPOSE 3000 31 | 32 | # Run the server 33 | CMD ["npm", "start"] -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Alexey Elizarov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Linear App Icon 3 |

4 | 5 | # MCP Linear 6 | 7 | A Model Context Protocol (MCP) server implementation for the Linear GraphQL API that enables AI assistants to interact with Linear project management systems. 8 | 9 | ![MCP Linear](https://img.shields.io/badge/MCP-Linear-blue) 10 | [![npm version](https://img.shields.io/npm/v/@tacticlaunch/mcp-linear.svg)](https://www.npmjs.com/package/@tacticlaunch/mcp-linear) 11 | [![smithery badge](https://smithery.ai/badge/@tacticlaunch/mcp-linear)](https://smithery.ai/server/@tacticlaunch/mcp-linear) 12 | 13 | 14 | 15 | 16 | 17 | ## Features 18 | 19 | MCP Linear bridges the gap between AI assistant and Linear (project management tool) by implementing the MCP protocol. This allows to: 20 | 21 | - Retrieve issues, projects, teams, and other data from Linear 22 | - Create and update issues 23 | - Change issue status 24 | - Assign issues to team members 25 | - Add comments 26 | - Create projects and teams 27 | 28 | ## Example prompts 29 | 30 | Once connected, you can use prompts like: 31 | 32 | - "Show me all my Linear issues" 33 | - "Create a new issue titled 'Fix login bug' in the Frontend team" 34 | - "Change the status of issue FE-123 to 'In Progress'" 35 | - "Assign issue BE-456 to John Smith" 36 | - "Add a comment to issue UI-789: 'This needs to be fixed by Friday'" 37 | 38 | ## Installation 39 | 40 | ### Getting Your Linear API Token 41 | 42 | To use MCP Linear, you'll need a Linear API token. Here's how to get one: 43 | 44 | 1. Log in to your Linear account at [linear.app](https://linear.app) 45 | 2. Click on organization avatar (in the top-left corner) 46 | 3. Select **Settings** 47 | 4. Navigate to **Security & access** in the left sidebar 48 | 5. Under **Personal API Keys** click **New API Key** 49 | 6. Give your key a name (e.g., `MCP Linear Integration`) 50 | 7. Copy the generated API token and store it securely - you won't be able to see it again! 51 | 52 | ### Installing via [Smithery](https://smithery.ai/server/@tacticlaunch/mcp-linear) (Recommended) 53 | 54 | - To install MCP Linear for Cursor: 55 | 56 | ```bash 57 | npx -y @smithery/cli install @tacticlaunch/mcp-linear --client cursor 58 | ``` 59 | 60 | - To install MCP Linear for Claude Desktop: 61 | 62 | ```bash 63 | npx -y @smithery/cli install @tacticlaunch/mcp-linear --client claude 64 | ``` 65 | 66 | ### Manual Configuration 67 | 68 | Add the following to your MCP settings file: 69 | 70 | ```json 71 | { 72 | "mcpServers": { 73 | "linear": { 74 | "command": "npx", 75 | "args": ["-y", "@tacticlaunch/mcp-linear"], 76 | "env": { 77 | "LINEAR_API_TOKEN": "" 78 | } 79 | } 80 | } 81 | } 82 | ``` 83 | 84 | #### Client-Specific Configuration Locations 85 | 86 | - Cursor: `~/.cursor/mcp.json` 87 | - Claude Desktop: `~/Library/Application Support/Claude/claude_desktop_config.json` 88 | - Claude VSCode Extension: `~/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json` 89 | - GoMCP: `~/.config/gomcp/config.yaml` 90 | 91 | ### Manual run 92 | 93 | Prerequisites 94 | 95 | - Node.js (v18+) 96 | - NPM or Yarn 97 | - Linear API token 98 | 99 | ```bash 100 | # Install globally 101 | npm install -g @tacticlaunch/mcp-linear 102 | 103 | # Or clone and install locally 104 | git clone https://github.com/tacticlaunch/mcp-linear.git 105 | cd mcp-linear 106 | npm install 107 | npm link # Makes the package available globally 108 | ``` 109 | 110 | #### Running the Server 111 | 112 | Run the server with your Linear API token: 113 | 114 | ```bash 115 | mcp-linear --token YOUR_LINEAR_API_TOKEN 116 | ``` 117 | 118 | Or set the token in your environment and run without arguments: 119 | 120 | ```bash 121 | export LINEAR_API_TOKEN=YOUR_LINEAR_API_TOKEN 122 | mcp-linear 123 | ``` 124 | 125 | ## Available Tools 126 | 127 | See [TOOLS.md](https://github.com/tacticlaunch/mcp-linear/blob/main/TOOLS.md) for a complete list of available tools and planned features. 128 | 129 | ## Development 130 | 131 | See [DEVELOPMENT.md](https://github.com/tacticlaunch/mcp-linear/blob/main/DEVELOPMENT.md) for more information on how to develop locally. 132 | 133 | ## Links 134 | 135 | [tacticlaunch/cursor-memory-bank](https://github.com/tacticlaunch/cursor-memory-bank) - If you are a developer seeking to enhance your workflow with Cursor, consider giving it a try. 136 | 137 | 138 | ## License 139 | 140 | This project is licensed under the MIT License - see the LICENSE file for details. 141 | -------------------------------------------------------------------------------- /TOOLS.md: -------------------------------------------------------------------------------- 1 | # MCP Linear Tools 2 | 3 | This document provides an overview of all tools implemented in the MCP Linear, as well as recommendations for future development. 4 | 5 | ## Implemented Tools 6 | 7 | The following tools are currently implemented and available in the MCP Linear: 8 | 9 | ### User & Organization Tools 10 | 11 | | Tool Name | Description | Status | 12 | | ------------------------ | ------------------------------------------------------ | -------------- | 13 | | `linear_getViewer` | Get information about the currently authenticated user | ✅ Implemented | 14 | | `linear_getOrganization` | Get information about the current Linear organization | ✅ Implemented | 15 | | `linear_getUsers` | Get a list of users in the Linear organization | ✅ Implemented | 16 | | `linear_getLabels` | Get a list of issue labels from Linear | ✅ Implemented | 17 | 18 | ### Team Tools 19 | 20 | | Tool Name | Description | Status | 21 | | ----------------- | ------------------------------- | -------------- | 22 | | `linear_getTeams` | Get a list of teams from Linear | ✅ Implemented | 23 | 24 | ### Project Tools 25 | 26 | | Tool Name | Description | Status | 27 | | ---------------------- | ---------------------------------- | -------------- | 28 | | `linear_getProjects` | Get a list of projects from Linear | ✅ Implemented | 29 | | `linear_createProject` | Create a new project in Linear | ✅ Implemented | 30 | 31 | ### Issue Tools 32 | 33 | | Tool Name | Description | Status | 34 | | ------------------------- | -------------------------------------------------------- | -------------- | 35 | | `linear_getIssues` | Get a list of recent issues from Linear | ✅ Implemented | 36 | | `linear_getIssueById` | Get a specific issue by ID or identifier (e.g., ABC-123) | ✅ Implemented | 37 | | `linear_searchIssues` | Search for issues with various filters | ✅ Implemented | 38 | | `linear_createIssue` | Create a new issue in Linear | ✅ Implemented | 39 | | `linear_updateIssue` | Update an existing issue in Linear | ✅ Implemented | 40 | | `linear_createComment` | Add a comment to an issue in Linear | ✅ Implemented | 41 | | `linear_addIssueLabel` | Add a label to an issue | ✅ Implemented | 42 | | `linear_removeIssueLabel` | Remove a label from an issue | ✅ Implemented | 43 | 44 | ### Issue Management Tools 45 | 46 | | Tool Name | Description | Status | 47 | | ------------------------------ | ------------------------------------------------------------- | -------------- | 48 | | `linear_assignIssue` | Assign an issue to a user | ✅ Implemented | 49 | | `linear_subscribeToIssue` | Subscribe to issue updates | 🔄 In Progress | 50 | | `linear_convertIssueToSubtask` | Convert an issue to a subtask | ✅ Implemented | 51 | | `linear_createIssueRelation` | Create relations between issues (blocks, is blocked by, etc.) | 🔄 In Progress | 52 | | `linear_archiveIssue` | Archive an issue | ✅ Implemented | 53 | | `linear_setIssuePriority` | Set the priority of an issue | ✅ Implemented | 54 | | `linear_transferIssue` | Transfer an issue to another team | ✅ Implemented | 55 | | `linear_duplicateIssue` | Duplicate an issue | ✅ Implemented | 56 | | `linear_getIssueHistory` | Get the history of changes made to an issue | ✅ Implemented | 57 | 58 | ### Comment Management Tools 59 | 60 | | Tool Name | Description | Status | 61 | | -------------------- | ----------------------------- | -------------- | 62 | | `linear_getComments` | Get all comments for an issue | ✅ Implemented | 63 | 64 | ### Project Management Tools 65 | 66 | | Tool Name | Description | Status | 67 | | -------------------------- | ---------------------------------------- | -------------- | 68 | | `linear_updateProject` | Update an existing project | ✅ Implemented | 69 | | `linear_addIssueToProject` | Add an existing issue to a project | ✅ Implemented | 70 | | `linear_getProjectIssues` | Get all issues associated with a project | ✅ Implemented | 71 | 72 | ### Cycle Management Tools 73 | 74 | | Tool Name | Description | Status | 75 | | ------------------------ | ----------------------------------------- | -------------- | 76 | | `linear_getCycles` | Get a list of all cycles | ✅ Implemented | 77 | | `linear_getActiveCycle` | Get the currently active cycle for a team | ✅ Implemented | 78 | | `linear_addIssueToCycle` | Add an issue to a cycle | ✅ Implemented | 79 | 80 | ## Recommended Future Tools 81 | 82 | The following tools are recommended for future implementation to enhance the capabilities of the MCP Linear: 83 | 84 | ### Comment Management 85 | 86 | | Tool Name | Description | Priority | Status | 87 | | ---------------------- | -------------------------- | -------- | ---------- | 88 | | `linear_updateComment` | Update an existing comment | Medium | 📝 Planned | 89 | | `linear_deleteComment` | Delete a comment | Low | 📝 Planned | 90 | 91 | ### Project Management 92 | 93 | | Tool Name | Description | Priority | Status | 94 | | ------------------------------- | --------------------------------- | -------- | ---------- | 95 | | `linear_archiveProject` | Archive a project | Medium | 📝 Planned | 96 | | `linear_getProjectUpdates` | Get updates for a project | Medium | 📝 Planned | 97 | | `linear_removeIssueFromProject` | Remove an issue from a project | Medium | 📝 Planned | 98 | | `linear_getProjectMembers` | Get members assigned to a project | Medium | 📝 Planned | 99 | | `linear_addProjectMember` | Add a member to a project | Medium | 📝 Planned | 100 | | `linear_removeProjectMember` | Remove a member from a project | Medium | 📝 Planned | 101 | 102 | ### Initiative Management 103 | 104 | | Tool Name | Description | Priority | Status | 105 | | ------------------------------------ | ------------------------------------- | -------- | ---------- | 106 | | `linear_getInitiatives` | Get a list of initiatives from Linear | High | 📝 Planned | 107 | | `linear_getInitiativeById` | Get details of a specific initiative | High | 📝 Planned | 108 | | `linear_createInitiative` | Create a new initiative | High | 📝 Planned | 109 | | `linear_updateInitiative` | Update an existing initiative | Medium | 📝 Planned | 110 | | `linear_archiveInitiative` | Archive an initiative | Medium | 📝 Planned | 111 | | `linear_addProjectToInitiative` | Add a project to an initiative | High | 📝 Planned | 112 | | `linear_removeProjectFromInitiative` | Remove a project from an initiative | Medium | 📝 Planned | 113 | | `linear_getInitiativeProjects` | Get all projects in an initiative | High | 📝 Planned | 114 | 115 | ### Cycle Management 116 | 117 | | Tool Name | Description | Priority | Status | 118 | | ----------------------------- | ------------------------------- | -------- | ---------- | 119 | | `linear_getCycleById` | Get details of a specific cycle | Medium | 📝 Planned | 120 | | `linear_createCycle` | Create a new cycle | Medium | 📝 Planned | 121 | | `linear_updateCycle` | Update an existing cycle | Medium | 📝 Planned | 122 | | `linear_removeIssueFromCycle` | Remove an issue from a cycle | Medium | 📝 Planned | 123 | | `linear_completeCycle` | Mark a cycle as complete | Medium | 📝 Planned | 124 | | `linear_getCycleStats` | Get statistics for a cycle | Medium | 📝 Planned | 125 | 126 | ### Milestone Management 127 | 128 | | Tool Name | Description | Priority | Status | 129 | | ----------------------------------- | ---------------------------------------- | -------- | ---------- | 130 | | `linear_getMilestones` | Get a list of milestones | Medium | 📝 Planned | 131 | | `linear_getMilestoneById` | Get details of a specific milestone | Medium | 📝 Planned | 132 | | `linear_createMilestone` | Create a new milestone | Medium | 📝 Planned | 133 | | `linear_updateMilestone` | Update an existing milestone | Low | 📝 Planned | 134 | | `linear_archiveMilestone` | Archive a milestone | Low | 📝 Planned | 135 | | `linear_getMilestoneProjects` | Get projects associated with a milestone | Medium | 📝 Planned | 136 | | `linear_addProjectToMilestone` | Add a project to a milestone | Medium | 📝 Planned | 137 | | `linear_removeProjectFromMilestone` | Remove a project from a milestone | Low | 📝 Planned | 138 | 139 | ### Roadmap Tools 140 | 141 | | Tool Name | Description | Priority | Status | 142 | | ------------------------------ | --------------------------------- | -------- | ---------- | 143 | | `linear_getRoadmaps` | Get a list of roadmaps | Medium | 📝 Planned | 144 | | `linear_getRoadmapById` | Get details of a specific roadmap | Medium | 📝 Planned | 145 | | `linear_createRoadmap` | Create a new roadmap | Medium | 📝 Planned | 146 | | `linear_updateRoadmap` | Update an existing roadmap | Low | 📝 Planned | 147 | | `linear_archiveRoadmap` | Archive a roadmap | Low | 📝 Planned | 148 | | `linear_getRoadmapItems` | Get items in a roadmap | Medium | 📝 Planned | 149 | | `linear_addItemToRoadmap` | Add an item to a roadmap | Medium | 📝 Planned | 150 | | `linear_removeItemFromRoadmap` | Remove an item from a roadmap | Low | 📝 Planned | 151 | 152 | ### Workflow Management 153 | 154 | | Tool Name | Description | Priority | Status | 155 | | ------------------------------ | --------------------------------------- | -------- | -------------- | 156 | | `linear_getWorkflowStates` | Get all workflow states | Medium | ✅ Implemented | 157 | | `linear_createWorkflowState` | Create a new workflow state | Low | 📝 Planned | 158 | | `linear_updateWorkflowState` | Update a workflow state | Low | 📝 Planned | 159 | | `linear_getTeamStates` | Get workflow states for a specific team | Medium | 📝 Planned | 160 | | `linear_reorderWorkflowStates` | Change the order of workflow states | Low | 📝 Planned | 161 | 162 | ### Team Management 163 | 164 | | Tool Name | Description | Priority | Status | 165 | | ----------------------------- | ------------------------------ | -------- | ---------- | 166 | | `linear_updateTeam` | Update team settings | Medium | 📝 Planned | 167 | | `linear_getTeamMemberships` | Get team memberships | Medium | 📝 Planned | 168 | | `linear_createTeam` | Create a new team | Medium | 📝 Planned | 169 | | `linear_archiveTeam` | Archive a team | Low | 📝 Planned | 170 | | `linear_addUserToTeam` | Add a user to a team | Medium | 📝 Planned | 171 | | `linear_removeUserFromTeam` | Remove a user from a team | Medium | 📝 Planned | 172 | | `linear_updateTeamMembership` | Update a user's role in a team | Medium | 📝 Planned | 173 | | `linear_getTeamLabels` | Get labels for a specific team | Medium | 📝 Planned | 174 | | `linear_createTeamLabel` | Create a new label for a team | Medium | 📝 Planned | 175 | 176 | ### Custom Fields 177 | 178 | | Tool Name | Description | Priority | Status | 179 | | ------------------------------- | ---------------------------------------- | -------- | ---------- | 180 | | `linear_getCustomFields` | Get a list of custom fields | Medium | 📝 Planned | 181 | | `linear_createCustomField` | Create a new custom field | Low | 📝 Planned | 182 | | `linear_updateCustomField` | Update a custom field | Low | 📝 Planned | 183 | | `linear_getIssueCustomFields` | Get custom field values for an issue | Medium | 📝 Planned | 184 | | `linear_updateIssueCustomField` | Update a custom field value for an issue | Medium | 📝 Planned | 185 | | `linear_getTeamCustomFields` | Get custom fields for a specific team | Medium | 📝 Planned | 186 | | `linear_deleteCustomField` | Delete a custom field | Low | 📝 Planned | 187 | 188 | ### Issue Templates 189 | 190 | | Tool Name | Description | Priority | Status | 191 | | -------------------------------- | ---------------------------------------- | -------- | ---------- | 192 | | `linear_getIssueTemplates` | Get a list of issue templates | Medium | 📝 Planned | 193 | | `linear_getIssueTemplateById` | Get details of a specific issue template | Medium | 📝 Planned | 194 | | `linear_createIssueTemplate` | Create a new issue template | Medium | 📝 Planned | 195 | | `linear_updateIssueTemplate` | Update an existing issue template | Low | 📝 Planned | 196 | | `linear_createIssueFromTemplate` | Create a new issue from a template | High | 📝 Planned | 197 | | `linear_getTeamTemplates` | Get templates for a specific team | Medium | 📝 Planned | 198 | | `linear_archiveTemplate` | Archive an issue template | Low | 📝 Planned | 199 | 200 | ### Import and Export 201 | 202 | | Tool Name | Description | Priority | Status | 203 | | ------------------------- | -------------------------------------- | -------- | ---------- | 204 | | `linear_bulkCreateIssues` | Create multiple issues at once | Medium | 📝 Planned | 205 | | `linear_exportIssues` | Export issues to a structured format | Low | 📝 Planned | 206 | | `linear_importIssues` | Import issues from a structured format | Low | 📝 Planned | 207 | | `linear_importCsvData` | Import data from CSV | Low | 📝 Planned | 208 | 209 | ### Integration Tools 210 | 211 | | Tool Name | Description | Priority | Status | 212 | | -------------------------- | --------------------------------------- | -------- | ---------- | 213 | | `linear_createWebhook` | Create a webhook for integration events | Low | 📝 Planned | 214 | | `linear_getAttachments` | Get attachments for an issue | Medium | 📝 Planned | 215 | | `linear_addAttachment` | Add an attachment to an issue | Medium | 📝 Planned | 216 | | `linear_getIntegrations` | Get a list of active integrations | Low | 📝 Planned | 217 | | `linear_createIntegration` | Create a new integration | Low | 📝 Planned | 218 | | `linear_removeIntegration` | Remove an integration | Low | 📝 Planned | 219 | | `linear_getWebhooks` | Get a list of webhooks | Low | 📝 Planned | 220 | | `linear_deleteWebhook` | Delete a webhook | Low | 📝 Planned | 221 | 222 | ### Notifications and Subscriptions 223 | 224 | | Tool Name | Description | Priority | Status | 225 | | ----------------------------------- | --------------------------------------- | -------- | ---------- | 226 | | `linear_getNotifications` | Get notifications for the current user | Medium | 📝 Planned | 227 | | `linear_markNotificationAsRead` | Mark a notification as read | Medium | 📝 Planned | 228 | | `linear_getSubscriptions` | Get subscriptions for the current user | Low | 📝 Planned | 229 | | `linear_manageSubscription` | Subscribe or unsubscribe from an entity | Low | 📝 Planned | 230 | | `linear_markAllNotificationsAsRead` | Mark all notifications as read | Medium | 📝 Planned | 231 | | `linear_getUnreadNotificationCount` | Get count of unread notifications | Medium | 📝 Planned | 232 | 233 | ### Favorites and Pinning 234 | 235 | | Tool Name | Description | Priority | Status | 236 | | ---------------------------- | ----------------------------------- | -------- | ---------- | 237 | | `linear_getFavorites` | Get a list of user's favorite items | Medium | 📝 Planned | 238 | | `linear_addToFavorites` | Add an item to favorites | Medium | 📝 Planned | 239 | | `linear_removeFromFavorites` | Remove an item from favorites | Medium | 📝 Planned | 240 | | `linear_pinIssue` | Pin an issue to the top of a list | Medium | 📝 Planned | 241 | | `linear_unpinIssue` | Unpin an issue | Medium | 📝 Planned | 242 | 243 | ### User Preferences 244 | 245 | | Tool Name | Description | Priority | Status | 246 | | ------------------------------ | -------------------------------- | -------- | ---------- | 247 | | `linear_getUserPreferences` | Get user preferences | Low | 📝 Planned | 248 | | `linear_updateUserPreferences` | Update user preferences | Low | 📝 Planned | 249 | | `linear_getUserSettings` | Get user application settings | Low | 📝 Planned | 250 | | `linear_updateUserSettings` | Update user application settings | Low | 📝 Planned | 251 | 252 | ### Views and Filters 253 | 254 | | Tool Name | Description | Priority | Status | 255 | | ------------------------- | ------------------------- | -------- | ---------- | 256 | | `linear_getSavedViews` | Get user's saved views | Medium | 📝 Planned | 257 | | `linear_createSavedView` | Create a new saved view | Medium | 📝 Planned | 258 | | `linear_updateSavedView` | Update a saved view | Low | 📝 Planned | 259 | | `linear_deleteSavedView` | Delete a saved view | Low | 📝 Planned | 260 | | `linear_getFavoriteViews` | Get user's favorite views | Medium | 📝 Planned | 261 | 262 | ### Metrics and Reporting 263 | 264 | | Tool Name | Description | Priority | Status | 265 | | ----------------------------------- | ------------------------------------------- | -------- | ---------- | 266 | | `linear_getTeamCycles` | Get information about team cycles | Medium | 📝 Planned | 267 | | `linear_getCycleIssues` | Get issues for a specific cycle | Medium | 📝 Planned | 268 | | `linear_getTeamMetrics` | Get performance metrics for a team | Low | 📝 Planned | 269 | | `linear_getIssueAnalytics` | Get analytics for issues (cycle time, etc.) | Medium | 📝 Planned | 270 | | `linear_generateReport` | Generate a custom report | Low | 📝 Planned | 271 | | `linear_getVelocityMetrics` | Get team velocity metrics | Medium | 📝 Planned | 272 | | `linear_getCompletionRateMetrics` | Get completion rate metrics | Medium | 📝 Planned | 273 | | `linear_getTimeToResolutionMetrics` | Get time-to-resolution metrics | Medium | 📝 Planned | 274 | 275 | ### Audit and History 276 | 277 | | Tool Name | Description | Priority | Status | 278 | | ----------------------------------- | ---------------------------------------- | -------- | ---------- | 279 | | `linear_getOrganizationAuditEvents` | Get audit events for the organization | Medium | 📝 Planned | 280 | | `linear_getUserAuditEvents` | Get audit events for a specific user | Medium | 📝 Planned | 281 | | `linear_getTeamAuditEvents` | Get audit events for a specific team | Medium | 📝 Planned | 282 | | `linear_getEntityHistory` | Get the history of changes for an entity | Medium | 📝 Planned | 283 | 284 | ### API and Authentication 285 | 286 | | Tool Name | Description | Priority | Status | 287 | | --------------------------- | ----------------------------- | -------- | ---------- | 288 | | `linear_createApiKey` | Create a new API key | Low | 📝 Planned | 289 | | `linear_revokeApiKey` | Revoke an API key | Low | 📝 Planned | 290 | | `linear_getApiKeys` | Get all API keys for the user | Low | 📝 Planned | 291 | | `linear_revokeUserSessions` | Revoke all user sessions | Low | 📝 Planned | 292 | 293 | ## Implementation Guide 294 | 295 | When implementing new tools, follow these steps: 296 | 297 | 1. Add a new tool definition in the appropriate file under `src/tools/definitions/` 298 | 2. Implement the handler function in `src/tools/handlers/` 299 | 3. Add any necessary type guards in `src/tools/type-guards.ts` 300 | 4. Add any required methods to the `LinearService` class in `src/services/linear-service.ts` 301 | 5. Register the tool in `src/tools/definitions/index.ts` 302 | 6. Register the handler in `src/tools/handlers/index.ts` 303 | 7. Update this document to mark the tool as implemented 304 | 305 | ## Status Legend 306 | 307 | - ✅ Implemented: Tool is fully implemented and tested 308 | - 🔄 In Progress: Tool is currently being implemented 309 | - 📝 Planned: Tool is planned for future implementation 310 | - ❓ Under Consideration: Tool is being considered, but not yet planned 311 | -------------------------------------------------------------------------------- /docs/linear-app-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tacticlaunch/mcp-linear/ec0975efcee5ecb6653c125045ed735a828fa6ff/docs/linear-app-icon.png -------------------------------------------------------------------------------- /glama.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://glama.ai/mcp/schemas/server.json", 3 | "maintainers": ["beautyfree"] 4 | } 5 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 2 | export default { 3 | preset: 'ts-jest/presets/default-esm', 4 | testEnvironment: 'node', 5 | roots: ['/src'], 6 | testMatch: ['**/__tests__/**/*.ts?(x)', '**/?(*.)+(spec|test).ts?(x)'], 7 | collectCoverageFrom: [ 8 | 'src/**/*.{ts,tsx}', 9 | '!src/**/*.d.ts', 10 | '!src/index.ts', 11 | ], 12 | coverageDirectory: 'coverage', 13 | transform: { 14 | '^.+\\.tsx?$': ['ts-jest', { 15 | tsconfig: 'tsconfig.json', 16 | useESM: true, 17 | }], 18 | }, 19 | extensionsToTreatAsEsm: ['.ts'], 20 | moduleNameMapper: { 21 | '^(\\.{1,2}/.*)\\.js$': '$1', 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["src"], 3 | "ext": "ts,json", 4 | "ignore": ["src/**/*.spec.ts"], 5 | "exec": "node --loader ts-node/esm src/index.ts" 6 | } 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tacticlaunch/mcp-linear", 3 | "version": "1.0.9", 4 | "description": "A Model Context Protocol (MCP) server implementation for the Linear GraphQL API that enables AI assistants to interact with Linear project management systems.", 5 | "main": "dist/index.js", 6 | "type": "module", 7 | "bin": { 8 | "mcp-linear": "./dist/index.js" 9 | }, 10 | "directories": { 11 | "doc": "docs" 12 | }, 13 | "scripts": { 14 | "start": "node dist/index.js", 15 | "inspect": "npx @modelcontextprotocol/inspector node dist/index.js", 16 | "dev": "nodemon --exec node --loader ts-node/esm src/index.ts", 17 | "build": "tsc", 18 | "lint": "eslint . --ext .ts", 19 | "format": "prettier --write \"src/**/*.ts\"", 20 | "test": "jest --passWithNoTests", 21 | "test:watch": "jest --watch", 22 | "prepare": "npm run build", 23 | "postinstall": "node -e \"try { require('fs').chmodSync('./dist/index.js', '755') } catch (e) {}\"" 24 | }, 25 | "smithery": { 26 | "name": "linear", 27 | "displayName": "Linear", 28 | "description": "Interact with Linear project management through AI assistants", 29 | "tools": [ 30 | "linear_getViewer", 31 | "linear_getOrganization", 32 | "linear_getUsers", 33 | "linear_getLabels", 34 | "linear_getTeams", 35 | "linear_getProjects", 36 | "linear_createProject", 37 | "linear_getIssues", 38 | "linear_getIssueById", 39 | "linear_searchIssues", 40 | "linear_createIssue", 41 | "linear_updateIssue", 42 | "linear_createComment", 43 | "linear_addIssueLabel", 44 | "linear_removeIssueLabel" 45 | ] 46 | }, 47 | "keywords": [ 48 | "mcp", 49 | "linear", 50 | "cursor", 51 | "claude", 52 | "ai", 53 | "model-context-protocol", 54 | "project-management", 55 | "smithery" 56 | ], 57 | "author": "Alexey Elizarov ", 58 | "license": "MIT", 59 | "repository": { 60 | "type": "git", 61 | "url": "https://github.com/tacticlaunch/mcp-linear" 62 | }, 63 | "dependencies": { 64 | "@linear/sdk": "^38.0.0", 65 | "@modelcontextprotocol/sdk": "^1.6.0", 66 | "@types/cors": "^2.8.17", 67 | "@types/express": "^5.0.0", 68 | "cors": "^2.8.5", 69 | "dotenv": "^16.4.7", 70 | "express": "^4.21.2", 71 | "graphql": "^16.10.0", 72 | "graphql-request": "^7.1.2", 73 | "helmet": "^8.0.0", 74 | "yargs": "^17.7.2", 75 | "zod": "^3.24.2" 76 | }, 77 | "devDependencies": { 78 | "@types/jest": "^29.5.14", 79 | "@types/node": "^22.13.5", 80 | "@types/yargs": "^17.0.33", 81 | "eslint": "^9.21.0", 82 | "eslint-config-prettier": "^10.0.2", 83 | "jest": "^29.7.0", 84 | "nodemon": "^3.1.9", 85 | "prettier": "^3.5.2", 86 | "ts-jest": "^29.2.6", 87 | "ts-node": "^10.9.2", 88 | "typescript": "^5.7.3" 89 | }, 90 | "engines": { 91 | "node": ">=20.0.0" 92 | }, 93 | "publishConfig": { 94 | "access": "public" 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /smithery.yaml: -------------------------------------------------------------------------------- 1 | # Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml 2 | 3 | startCommand: 4 | type: stdio 5 | configSchema: 6 | # JSON Schema defining the configuration options for the MCP 7 | type: object 8 | required: 9 | - token 10 | properties: 11 | token: 12 | type: string 13 | description: Your Linear API key. Required for Linear API access. 14 | commandFunction: | 15 | (config) => ({ 16 | command: 'node', 17 | args: ['dist/index.js', '--token', config.token], 18 | env: { 19 | LINEAR_API_TOKEN: config.token 20 | } 21 | }) -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { LinearClient } from '@linear/sdk'; 4 | import { runMCPServer } from './mcp-server.js'; 5 | 6 | import { LinearService } from './services/linear-service.js'; 7 | import { allToolDefinitions } from './tools/definitions/index.js'; 8 | import { registerToolHandlers } from './tools/handlers/index.js'; 9 | import { getLinearApiToken, logInfo, logError } from './utils/config.js'; 10 | import pkg from '../package.json' with { type: 'json' }; // Import package.json to access version 11 | 12 | /** 13 | * Main function to run the MCP Linear 14 | */ 15 | async function runServer() { 16 | try { 17 | // Log package version 18 | logInfo(`MCP Linear version: ${pkg.version}`); 19 | 20 | // Get Linear API token 21 | const linearApiToken = getLinearApiToken(); 22 | 23 | if (!linearApiToken) { 24 | throw new Error( 25 | 'Linear API token not found. Please provide it via --token command line argument or LINEAR_API_TOKEN environment variable.', 26 | ); 27 | } 28 | 29 | logInfo(`Starting MCP Linear...`); 30 | 31 | // Initialize Linear client and service 32 | const linearClient = new LinearClient({ apiKey: linearApiToken }); 33 | const linearService = new LinearService(linearClient); 34 | 35 | // Start the MCP server 36 | const server = await runMCPServer({ 37 | tools: allToolDefinitions, 38 | handleInitialize: async () => { 39 | logInfo('MCP Linear initialized successfully.'); 40 | return { 41 | tools: allToolDefinitions, 42 | }; 43 | }, 44 | handleRequest: async (req: { name: string; args: unknown }) => { 45 | const handlers = registerToolHandlers(linearService); 46 | const toolName = req.name; 47 | 48 | if (toolName in handlers) { 49 | // Use a type assertion here since we know the tool name is valid 50 | const handler = handlers[toolName as keyof typeof handlers]; 51 | return await handler(req.args); 52 | } else { 53 | throw new Error(`Unknown tool: ${toolName}`); 54 | } 55 | }, 56 | }); 57 | 58 | // Set up heartbeat to keep server alive 59 | setInterval(() => { 60 | logInfo('MCP Linear is running...'); 61 | }, 60000); 62 | 63 | return server; 64 | } catch (error) { 65 | logError('Error starting MCP Linear', error); 66 | process.exit(1); 67 | } 68 | } 69 | 70 | // Start the server 71 | runServer().catch((error) => { 72 | logError('Fatal error in MCP Linear', error); 73 | process.exit(1); 74 | }); 75 | -------------------------------------------------------------------------------- /src/mcp-server.ts: -------------------------------------------------------------------------------- 1 | import { Server } from '@modelcontextprotocol/sdk/server/index.js'; 2 | import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; 3 | import { 4 | CallToolRequestSchema, 5 | ListToolsRequestSchema, 6 | Tool, 7 | } from '@modelcontextprotocol/sdk/types.js'; 8 | import { MCPToolDefinition } from './types.js'; 9 | import pkg from '../package.json' with { type: 'json' }; 10 | 11 | /** 12 | * Interface for the MCP server handler functions 13 | */ 14 | export interface MCPServerHandlers { 15 | handleInitialize: () => Promise<{ tools: MCPToolDefinition[] }>; 16 | handleRequest: (req: { name: string; args: unknown }) => Promise; 17 | } 18 | 19 | /** 20 | * Interface for the MCP server configuration 21 | */ 22 | export interface MCPServerConfig { 23 | tools: MCPToolDefinition[]; 24 | handleInitialize: () => Promise<{ tools: MCPToolDefinition[] }>; 25 | handleRequest: (req: { name: string; args: unknown }) => Promise; 26 | } 27 | 28 | /** 29 | * Convert MCPToolDefinition to the MCP SDK Tool format 30 | */ 31 | function convertToolDefinition(toolDef: MCPToolDefinition): Tool { 32 | return { 33 | name: toolDef.name, 34 | description: toolDef.description, 35 | inputSchema: { 36 | type: 'object', 37 | properties: toolDef.input_schema.properties, 38 | ...(toolDef.input_schema.required ? { required: toolDef.input_schema.required } : {}), 39 | }, 40 | }; 41 | } 42 | 43 | /** 44 | * Runs an MCP server with the given configuration 45 | */ 46 | export async function runMCPServer(config: MCPServerConfig) { 47 | const server = new Server( 48 | { 49 | name: 'linear', 50 | version: pkg.version, 51 | }, 52 | { 53 | capabilities: { 54 | tools: {}, 55 | }, 56 | }, 57 | ); 58 | 59 | // Convert our tool definitions to the SDK format 60 | const sdkTools = config.tools.map(convertToolDefinition); 61 | 62 | // Handle list tools request 63 | server.setRequestHandler(ListToolsRequestSchema, async () => ({ 64 | tools: sdkTools, 65 | })); 66 | 67 | // Handle call tool request 68 | server.setRequestHandler(CallToolRequestSchema, async (request) => { 69 | try { 70 | const { name, arguments: args = null } = request.params; 71 | 72 | // Call the handler 73 | const result = await config.handleRequest({ name, args }); 74 | 75 | return { 76 | content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], 77 | isError: false, 78 | }; 79 | } catch (error) { 80 | console.error('Error in tool handler:', error); 81 | return { 82 | content: [ 83 | { 84 | type: 'text', 85 | text: `Error: ${error instanceof Error ? error.message : String(error)}`, 86 | }, 87 | ], 88 | isError: true, 89 | }; 90 | } 91 | }); 92 | 93 | // Connect the server to stdio 94 | const transport = new StdioServerTransport(); 95 | await server.connect(transport); 96 | 97 | return server; 98 | } 99 | -------------------------------------------------------------------------------- /src/services/linear-service.ts: -------------------------------------------------------------------------------- 1 | import { LinearClient } from '@linear/sdk'; 2 | 3 | // Define Linear API service 4 | export class LinearService { 5 | private client: LinearClient; 6 | 7 | constructor(client: LinearClient) { 8 | this.client = client; 9 | } 10 | 11 | async getUserInfo() { 12 | const viewer = await this.client.viewer; 13 | return { 14 | id: viewer.id, 15 | name: viewer.name, 16 | email: viewer.email, 17 | displayName: viewer.displayName, 18 | active: viewer.active, 19 | }; 20 | } 21 | 22 | async getOrganizationInfo() { 23 | const organization = await this.client.organization; 24 | return { 25 | id: organization.id, 26 | name: organization.name, 27 | urlKey: organization.urlKey, 28 | logoUrl: organization.logoUrl, 29 | createdAt: organization.createdAt, 30 | // Include subscription details if available 31 | subscription: organization.subscription || null, 32 | }; 33 | } 34 | 35 | async getAllUsers() { 36 | const users = await this.client.users(); 37 | return users.nodes.map((user) => ({ 38 | id: user.id, 39 | name: user.name, 40 | email: user.email, 41 | displayName: user.displayName, 42 | active: user.active, 43 | })); 44 | } 45 | 46 | async getLabels() { 47 | const labels = await this.client.issueLabels(); 48 | return Promise.all( 49 | labels.nodes.map(async (label) => { 50 | const teamData = label.team ? await label.team : null; 51 | 52 | return { 53 | id: label.id, 54 | name: label.name, 55 | color: label.color, 56 | description: label.description, 57 | team: teamData 58 | ? { 59 | id: teamData.id, 60 | name: teamData.name, 61 | } 62 | : null, 63 | }; 64 | }), 65 | ); 66 | } 67 | 68 | async getTeams() { 69 | const teams = await this.client.teams(); 70 | return teams.nodes.map((team) => ({ 71 | id: team.id, 72 | name: team.name, 73 | key: team.key, 74 | description: team.description, 75 | })); 76 | } 77 | 78 | async getProjects() { 79 | const projects = await this.client.projects(); 80 | return Promise.all( 81 | projects.nodes.map(async (project) => { 82 | // We need to fetch teams using the relationship 83 | const teams = await project.teams(); 84 | 85 | return { 86 | id: project.id, 87 | name: project.name, 88 | description: project.description, 89 | content: project.content, 90 | state: project.state, 91 | teams: teams.nodes.map((team) => ({ 92 | id: team.id, 93 | name: team.name, 94 | })), 95 | }; 96 | }), 97 | ); 98 | } 99 | 100 | async getIssues(limit = 25) { 101 | const issues = await this.client.issues({ first: limit }); 102 | return Promise.all( 103 | issues.nodes.map(async (issue) => { 104 | // For relations, we need to fetch the objects 105 | const teamData = issue.team ? await issue.team : null; 106 | const assigneeData = issue.assignee ? await issue.assignee : null; 107 | const projectData = issue.project ? await issue.project : null; 108 | const cycleData = issue.cycle ? await issue.cycle : null; 109 | const parentData = issue.parent ? await issue.parent : null; 110 | 111 | // Get labels 112 | const labels = await issue.labels(); 113 | const labelsList = labels.nodes.map((label) => ({ 114 | id: label.id, 115 | name: label.name, 116 | color: label.color, 117 | })); 118 | 119 | return { 120 | id: issue.id, 121 | title: issue.title, 122 | description: issue.description, 123 | state: issue.state, 124 | priority: issue.priority, 125 | estimate: issue.estimate, 126 | dueDate: issue.dueDate, 127 | team: teamData 128 | ? { 129 | id: teamData.id, 130 | name: teamData.name, 131 | } 132 | : null, 133 | assignee: assigneeData 134 | ? { 135 | id: assigneeData.id, 136 | name: assigneeData.name, 137 | } 138 | : null, 139 | project: projectData 140 | ? { 141 | id: projectData.id, 142 | name: projectData.name, 143 | } 144 | : null, 145 | cycle: cycleData 146 | ? { 147 | id: cycleData.id, 148 | name: cycleData.name, 149 | } 150 | : null, 151 | parent: parentData 152 | ? { 153 | id: parentData.id, 154 | title: parentData.title, 155 | } 156 | : null, 157 | labels: labelsList, 158 | sortOrder: issue.sortOrder, 159 | createdAt: issue.createdAt, 160 | updatedAt: issue.updatedAt, 161 | url: issue.url, 162 | }; 163 | }), 164 | ); 165 | } 166 | 167 | async getIssueById(id: string) { 168 | const issue = await this.client.issue(id); 169 | 170 | if (!issue) { 171 | throw new Error(`Issue with ID ${id} not found`); 172 | } 173 | 174 | // For relations, we need to fetch the objects 175 | const teamData = issue.team ? await issue.team : null; 176 | const assigneeData = issue.assignee ? await issue.assignee : null; 177 | const projectData = issue.project ? await issue.project : null; 178 | const cycleData = issue.cycle ? await issue.cycle : null; 179 | const parentData = issue.parent ? await issue.parent : null; 180 | 181 | // Get comments 182 | const comments = await issue.comments(); 183 | const commentsList = await Promise.all( 184 | comments.nodes.map(async (comment) => { 185 | const userData = comment.user ? await comment.user : null; 186 | 187 | return { 188 | id: comment.id, 189 | body: comment.body, 190 | createdAt: comment.createdAt, 191 | user: userData 192 | ? { 193 | id: userData.id, 194 | name: userData.name, 195 | } 196 | : null, 197 | }; 198 | }), 199 | ); 200 | 201 | // Get labels 202 | const labels = await issue.labels(); 203 | const labelsList = labels.nodes.map((label) => ({ 204 | id: label.id, 205 | name: label.name, 206 | color: label.color, 207 | })); 208 | 209 | return { 210 | id: issue.id, 211 | title: issue.title, 212 | description: issue.description, 213 | state: issue.state, 214 | priority: issue.priority, 215 | estimate: issue.estimate, 216 | dueDate: issue.dueDate, 217 | team: teamData 218 | ? { 219 | id: teamData.id, 220 | name: teamData.name, 221 | } 222 | : null, 223 | assignee: assigneeData 224 | ? { 225 | id: assigneeData.id, 226 | name: assigneeData.name, 227 | } 228 | : null, 229 | project: projectData 230 | ? { 231 | id: projectData.id, 232 | name: projectData.name, 233 | } 234 | : null, 235 | cycle: cycleData 236 | ? { 237 | id: cycleData.id, 238 | name: cycleData.name, 239 | } 240 | : null, 241 | parent: parentData 242 | ? { 243 | id: parentData.id, 244 | title: parentData.title, 245 | } 246 | : null, 247 | labels: labelsList, 248 | sortOrder: issue.sortOrder, 249 | createdAt: issue.createdAt, 250 | updatedAt: issue.updatedAt, 251 | url: issue.url, 252 | comments: commentsList, 253 | }; 254 | } 255 | 256 | async searchIssues(args: { 257 | query?: string; 258 | teamId?: string; 259 | assigneeId?: string; 260 | projectId?: string; 261 | states?: string[]; 262 | limit?: number; 263 | }) { 264 | try { 265 | // Build filter object 266 | const filter: any = {}; 267 | 268 | if (args.teamId) { 269 | filter.team = { id: { eq: args.teamId } }; 270 | } 271 | 272 | if (args.assigneeId) { 273 | filter.assignee = { id: { eq: args.assigneeId } }; 274 | } 275 | 276 | if (args.projectId) { 277 | filter.project = { id: { eq: args.projectId } }; 278 | } 279 | 280 | // Handle state filtering 281 | if (args.states && args.states.length > 0) { 282 | // First, get all workflow states to map names to IDs if needed 283 | let stateIds: string[] = []; 284 | 285 | if (args.teamId) { 286 | // If we have a teamId, get workflow states for that team 287 | const workflowStates = await this.getWorkflowStates(args.teamId); 288 | 289 | // Map state names to IDs 290 | for (const stateName of args.states) { 291 | const matchingState = workflowStates.find( 292 | (state) => state.name.toLowerCase() === stateName.toLowerCase(), 293 | ); 294 | 295 | if (matchingState) { 296 | stateIds.push(matchingState.id); 297 | } 298 | } 299 | } else { 300 | // If no teamId, we need to get all teams and their workflow states 301 | const teams = await this.getTeams(); 302 | 303 | for (const team of teams) { 304 | const workflowStates = await this.getWorkflowStates(team.id); 305 | 306 | // Map state names to IDs 307 | for (const stateName of args.states) { 308 | const matchingState = workflowStates.find( 309 | (state) => state.name.toLowerCase() === stateName.toLowerCase(), 310 | ); 311 | 312 | if (matchingState) { 313 | stateIds.push(matchingState.id); 314 | } 315 | } 316 | } 317 | } 318 | 319 | // If we found matching state IDs, filter by them 320 | if (stateIds.length > 0) { 321 | filter.state = { id: { in: stateIds } }; 322 | } 323 | } 324 | 325 | // Handle text search 326 | let searchFilter = filter; 327 | if (args.query) { 328 | searchFilter = { 329 | ...filter, 330 | or: [{ title: { contains: args.query } }, { description: { contains: args.query } }], 331 | }; 332 | } 333 | 334 | // Execute the search 335 | const issues = await this.client.issues({ 336 | first: args.limit || 10, 337 | filter: searchFilter, 338 | }); 339 | 340 | // Process the results 341 | return Promise.all( 342 | issues.nodes.map(async (issue) => { 343 | // For relations, we need to fetch the objects 344 | const teamData = issue.team ? await issue.team : null; 345 | const assigneeData = issue.assignee ? await issue.assignee : null; 346 | const projectData = issue.project ? await issue.project : null; 347 | const cycleData = issue.cycle ? await issue.cycle : null; 348 | const parentData = issue.parent ? await issue.parent : null; 349 | 350 | // Get labels 351 | const labels = await issue.labels(); 352 | const labelsList = labels.nodes.map((label) => ({ 353 | id: label.id, 354 | name: label.name, 355 | color: label.color, 356 | })); 357 | 358 | // Get state data 359 | const stateData = issue.state ? await issue.state : null; 360 | 361 | return { 362 | id: issue.id, 363 | title: issue.title, 364 | description: issue.description, 365 | state: stateData 366 | ? { 367 | id: stateData.id, 368 | name: stateData.name, 369 | color: stateData.color, 370 | type: stateData.type, 371 | } 372 | : null, 373 | priority: issue.priority, 374 | estimate: issue.estimate, 375 | dueDate: issue.dueDate, 376 | team: teamData 377 | ? { 378 | id: teamData.id, 379 | name: teamData.name, 380 | } 381 | : null, 382 | assignee: assigneeData 383 | ? { 384 | id: assigneeData.id, 385 | name: assigneeData.name, 386 | } 387 | : null, 388 | project: projectData 389 | ? { 390 | id: projectData.id, 391 | name: projectData.name, 392 | } 393 | : null, 394 | cycle: cycleData 395 | ? { 396 | id: cycleData.id, 397 | name: cycleData.name, 398 | } 399 | : null, 400 | parent: parentData 401 | ? { 402 | id: parentData.id, 403 | title: parentData.title, 404 | } 405 | : null, 406 | labels: labelsList, 407 | sortOrder: issue.sortOrder, 408 | createdAt: issue.createdAt, 409 | updatedAt: issue.updatedAt, 410 | url: issue.url, 411 | }; 412 | }), 413 | ); 414 | } catch (error) { 415 | console.error('Error searching issues:', error); 416 | throw error; 417 | } 418 | } 419 | 420 | async createIssue(args: { 421 | title: string; 422 | description?: string; 423 | teamId: string; 424 | assigneeId?: string; 425 | priority?: number; 426 | projectId?: string; 427 | cycleId?: string; 428 | estimate?: number; 429 | dueDate?: string; 430 | labelIds?: string[]; 431 | parentId?: string; 432 | subscriberIds?: string[]; 433 | stateId?: string; 434 | templateId?: string; 435 | sortOrder?: number; 436 | }) { 437 | const createdIssue = await this.client.createIssue({ 438 | title: args.title, 439 | description: args.description, 440 | teamId: args.teamId, 441 | assigneeId: args.assigneeId, 442 | priority: args.priority, 443 | projectId: args.projectId, 444 | cycleId: args.cycleId, 445 | estimate: args.estimate, 446 | dueDate: args.dueDate, 447 | labelIds: args.labelIds, 448 | parentId: args.parentId, 449 | subscriberIds: args.subscriberIds, 450 | stateId: args.stateId, 451 | templateId: args.templateId, 452 | sortOrder: args.sortOrder, 453 | }); 454 | 455 | // Access the issue from the payload 456 | if (createdIssue.success && createdIssue.issue) { 457 | const issueData = await createdIssue.issue; 458 | return { 459 | id: issueData.id, 460 | title: issueData.title, 461 | description: issueData.description, 462 | url: issueData.url, 463 | }; 464 | } else { 465 | throw new Error('Failed to create issue'); 466 | } 467 | } 468 | 469 | async updateIssue(args: { 470 | id: string; 471 | title?: string; 472 | description?: string; 473 | stateId?: string; 474 | priority?: number; 475 | projectId?: string; 476 | assigneeId?: string; 477 | cycleId?: string; 478 | estimate?: number; 479 | dueDate?: string; 480 | labelIds?: string[]; 481 | addedLabelIds?: string[]; 482 | removedLabelIds?: string[]; 483 | parentId?: string; 484 | subscriberIds?: string[]; 485 | teamId?: string; 486 | sortOrder?: number; 487 | }) { 488 | const updatedIssue = await this.client.updateIssue(args.id, { 489 | title: args.title, 490 | description: args.description, 491 | stateId: args.stateId, 492 | priority: args.priority, 493 | projectId: args.projectId, 494 | assigneeId: args.assigneeId, 495 | cycleId: args.cycleId, 496 | estimate: args.estimate, 497 | dueDate: args.dueDate, 498 | labelIds: args.labelIds, 499 | addedLabelIds: args.addedLabelIds, 500 | removedLabelIds: args.removedLabelIds, 501 | parentId: args.parentId, 502 | subscriberIds: args.subscriberIds, 503 | teamId: args.teamId, 504 | sortOrder: args.sortOrder, 505 | }); 506 | 507 | if (updatedIssue.success && updatedIssue.issue) { 508 | const issueData = await updatedIssue.issue; 509 | return { 510 | id: issueData.id, 511 | title: issueData.title, 512 | description: issueData.description, 513 | url: issueData.url, 514 | }; 515 | } else { 516 | throw new Error('Failed to update issue'); 517 | } 518 | } 519 | 520 | async createComment(args: { issueId: string; body: string }) { 521 | const createdComment = await this.client.createComment({ 522 | issueId: args.issueId, 523 | body: args.body, 524 | }); 525 | 526 | if (createdComment.success && createdComment.comment) { 527 | const commentData = await createdComment.comment; 528 | return { 529 | id: commentData.id, 530 | body: commentData.body, 531 | url: commentData.url, 532 | }; 533 | } else { 534 | throw new Error('Failed to create comment'); 535 | } 536 | } 537 | 538 | async createProject(args: { 539 | name: string; 540 | description?: string; 541 | content?: string; 542 | teamIds: string[] | string; 543 | state?: string; 544 | startDate?: string; 545 | targetDate?: string; 546 | leadId?: string; 547 | memberIds?: string[] | string; 548 | sortOrder?: number; 549 | icon?: string; 550 | color?: string; 551 | }) { 552 | const teamIds = Array.isArray(args.teamIds) ? args.teamIds : [args.teamIds]; 553 | const memberIds = args.memberIds 554 | ? Array.isArray(args.memberIds) 555 | ? args.memberIds 556 | : [args.memberIds] 557 | : undefined; 558 | 559 | const createdProject = await this.client.createProject({ 560 | name: args.name, 561 | description: args.description, 562 | content: args.content, 563 | teamIds: teamIds, 564 | state: args.state, 565 | startDate: args.startDate ? new Date(args.startDate) : undefined, 566 | targetDate: args.targetDate ? new Date(args.targetDate) : undefined, 567 | leadId: args.leadId, 568 | memberIds: memberIds, 569 | sortOrder: args.sortOrder, 570 | icon: args.icon, 571 | color: args.color, 572 | }); 573 | 574 | if (createdProject.success && createdProject.project) { 575 | const projectData = await createdProject.project; 576 | const leadData = projectData.lead ? await projectData.lead : null; 577 | 578 | return { 579 | id: projectData.id, 580 | name: projectData.name, 581 | description: projectData.description, 582 | content: projectData.content, 583 | state: projectData.state, 584 | startDate: projectData.startDate, 585 | targetDate: projectData.targetDate, 586 | lead: leadData 587 | ? { 588 | id: leadData.id, 589 | name: leadData.name, 590 | } 591 | : null, 592 | icon: projectData.icon, 593 | color: projectData.color, 594 | url: projectData.url, 595 | }; 596 | } else { 597 | throw new Error('Failed to create project'); 598 | } 599 | } 600 | 601 | /** 602 | * Adds a label to an issue 603 | * @param issueId The ID or identifier of the issue 604 | * @param labelId The ID of the label to add 605 | * @returns Success status and IDs 606 | */ 607 | async addIssueLabel(issueId: string, labelId: string) { 608 | // Get the issue 609 | const issue = await this.client.issue(issueId); 610 | 611 | if (!issue) { 612 | throw new Error(`Issue not found: ${issueId}`); 613 | } 614 | 615 | // Get the current labels 616 | const currentLabels = await issue.labels(); 617 | const currentLabelIds = currentLabels.nodes.map((label) => label.id); 618 | 619 | // Add the new label ID if it's not already present 620 | if (!currentLabelIds.includes(labelId)) { 621 | await issue.update({ 622 | labelIds: [...currentLabelIds, labelId], 623 | }); 624 | } 625 | 626 | return { 627 | success: true, 628 | issueId: issue.id, 629 | labelId, 630 | }; 631 | } 632 | 633 | /** 634 | * Removes a label from an issue 635 | * @param issueId The ID or identifier of the issue 636 | * @param labelId The ID of the label to remove 637 | * @returns Success status and IDs 638 | */ 639 | async removeIssueLabel(issueId: string, labelId: string) { 640 | // Get the issue 641 | const issue = await this.client.issue(issueId); 642 | 643 | if (!issue) { 644 | throw new Error(`Issue not found: ${issueId}`); 645 | } 646 | 647 | // Get the current labels 648 | const currentLabels = await issue.labels(); 649 | const currentLabelIds = currentLabels.nodes.map((label) => label.id); 650 | 651 | // Filter out the label ID to remove 652 | const updatedLabelIds = currentLabelIds.filter((id) => id !== labelId); 653 | 654 | // Only update if the label was actually present 655 | if (currentLabelIds.length !== updatedLabelIds.length) { 656 | await issue.update({ 657 | labelIds: updatedLabelIds, 658 | }); 659 | } 660 | 661 | return { 662 | success: true, 663 | issueId: issue.id, 664 | labelId, 665 | }; 666 | } 667 | 668 | /** 669 | * Assigns an issue to a user 670 | */ 671 | async assignIssue(issueId: string, assigneeId: string) { 672 | try { 673 | // Get the issue 674 | const issue = await this.client.issue(issueId); 675 | if (!issue) { 676 | throw new Error(`Issue with ID ${issueId} not found`); 677 | } 678 | 679 | // Get the user to assign 680 | const user = assigneeId ? await this.client.user(assigneeId) : null; 681 | 682 | // Update the issue with the new assignee 683 | const updatedIssue = await issue.update({ 684 | assigneeId: assigneeId, 685 | }); 686 | 687 | // Get the updated assignee data 688 | // We need to get the full issue record and its relationships 689 | const issueData = await this.client.issue(issue.id); 690 | const assigneeData = issueData && issueData.assignee ? await issueData.assignee : null; 691 | 692 | return { 693 | success: true, 694 | issue: { 695 | id: issue.id, 696 | identifier: issue.identifier, 697 | title: issue.title, 698 | assignee: assigneeData 699 | ? { 700 | id: assigneeData.id, 701 | name: assigneeData.name, 702 | displayName: assigneeData.displayName, 703 | } 704 | : null, 705 | url: issue.url, 706 | }, 707 | }; 708 | } catch (error) { 709 | console.error('Error assigning issue:', error); 710 | throw error; 711 | } 712 | } 713 | 714 | /** 715 | * Subscribes to issue updates 716 | */ 717 | async subscribeToIssue(issueId: string) { 718 | try { 719 | // Get the issue 720 | const issue = await this.client.issue(issueId); 721 | if (!issue) { 722 | throw new Error(`Issue with ID ${issueId} not found`); 723 | } 724 | 725 | // Get current user info 726 | const viewer = await this.client.viewer; 727 | 728 | // For now, we'll just acknowledge the request with a success message 729 | // The actual subscription logic would need to be implemented based on the Linear SDK specifics 730 | // In a production environment, we should check the SDK documentation for the correct method 731 | 732 | return { 733 | success: true, 734 | message: `User ${viewer.name} (${viewer.id}) would be subscribed to issue ${issue.identifier}. (Note: Actual subscription API call implementation needed)`, 735 | }; 736 | } catch (error) { 737 | console.error('Error subscribing to issue:', error); 738 | throw error; 739 | } 740 | } 741 | 742 | /** 743 | * Converts an issue to a subtask of another issue 744 | */ 745 | async convertIssueToSubtask(issueId: string, parentIssueId: string) { 746 | try { 747 | // Get both issues 748 | const issue = await this.client.issue(issueId); 749 | if (!issue) { 750 | throw new Error(`Issue with ID ${issueId} not found`); 751 | } 752 | 753 | const parentIssue = await this.client.issue(parentIssueId); 754 | if (!parentIssue) { 755 | throw new Error(`Parent issue with ID ${parentIssueId} not found`); 756 | } 757 | 758 | // Convert the issue to a subtask 759 | const updatedIssue = await issue.update({ 760 | parentId: parentIssueId, 761 | }); 762 | 763 | // Get parent data - we need to fetch the updated issue to get relationships 764 | const updatedIssueData = await this.client.issue(issue.id); 765 | const parentData = 766 | updatedIssueData && updatedIssueData.parent ? await updatedIssueData.parent : null; 767 | 768 | return { 769 | success: true, 770 | issue: { 771 | id: issue.id, 772 | identifier: issue.identifier, 773 | title: issue.title, 774 | parent: parentData 775 | ? { 776 | id: parentData.id, 777 | identifier: parentData.identifier, 778 | title: parentData.title, 779 | } 780 | : null, 781 | url: issue.url, 782 | }, 783 | }; 784 | } catch (error) { 785 | console.error('Error converting issue to subtask:', error); 786 | throw error; 787 | } 788 | } 789 | 790 | /** 791 | * Creates a relation between two issues 792 | */ 793 | async createIssueRelation(issueId: string, relatedIssueId: string, type: string) { 794 | try { 795 | // Get both issues 796 | const issue = await this.client.issue(issueId); 797 | if (!issue) { 798 | throw new Error(`Issue with ID ${issueId} not found`); 799 | } 800 | 801 | const relatedIssue = await this.client.issue(relatedIssueId); 802 | if (!relatedIssue) { 803 | throw new Error(`Related issue with ID ${relatedIssueId} not found`); 804 | } 805 | 806 | // For now, we'll just acknowledge the request with a success message 807 | // The actual relation creation logic would need to be implemented based on the Linear SDK specifics 808 | // In a production environment, we should check the SDK documentation for the correct method 809 | 810 | return { 811 | success: true, 812 | relation: { 813 | id: 'relation-id-would-go-here', 814 | type: type, 815 | issueIdentifier: issue.identifier, 816 | relatedIssueIdentifier: relatedIssue.identifier, 817 | }, 818 | }; 819 | } catch (error) { 820 | console.error('Error creating issue relation:', error); 821 | throw error; 822 | } 823 | } 824 | 825 | /** 826 | * Archives an issue 827 | */ 828 | async archiveIssue(issueId: string) { 829 | try { 830 | // Get the issue 831 | const issue = await this.client.issue(issueId); 832 | if (!issue) { 833 | throw new Error(`Issue with ID ${issueId} not found`); 834 | } 835 | 836 | // Archive the issue 837 | await issue.archive(); 838 | 839 | return { 840 | success: true, 841 | message: `Issue ${issue.identifier} has been archived`, 842 | }; 843 | } catch (error) { 844 | console.error('Error archiving issue:', error); 845 | throw error; 846 | } 847 | } 848 | 849 | /** 850 | * Sets the priority of an issue 851 | */ 852 | async setIssuePriority(issueId: string, priority: number) { 853 | try { 854 | // Get the issue 855 | const issue = await this.client.issue(issueId); 856 | if (!issue) { 857 | throw new Error(`Issue with ID ${issueId} not found`); 858 | } 859 | 860 | // Update the issue priority 861 | await issue.update({ 862 | priority: priority, 863 | }); 864 | 865 | // Get the updated issue 866 | const updatedIssue = await this.client.issue(issue.id); 867 | 868 | return { 869 | success: true, 870 | issue: { 871 | id: updatedIssue.id, 872 | identifier: updatedIssue.identifier, 873 | title: updatedIssue.title, 874 | priority: updatedIssue.priority, 875 | url: updatedIssue.url, 876 | }, 877 | }; 878 | } catch (error) { 879 | console.error('Error setting issue priority:', error); 880 | throw error; 881 | } 882 | } 883 | 884 | /** 885 | * Transfers an issue to another team 886 | */ 887 | async transferIssue(issueId: string, teamId: string) { 888 | try { 889 | // Get the issue 890 | const issue = await this.client.issue(issueId); 891 | if (!issue) { 892 | throw new Error(`Issue with ID ${issueId} not found`); 893 | } 894 | 895 | // Get the team 896 | const team = await this.client.team(teamId); 897 | if (!team) { 898 | throw new Error(`Team with ID ${teamId} not found`); 899 | } 900 | 901 | // Transfer the issue 902 | await issue.update({ 903 | teamId: teamId, 904 | }); 905 | 906 | // Get the updated issue 907 | const updatedIssue = await this.client.issue(issue.id); 908 | const teamData = updatedIssue.team ? await updatedIssue.team : null; 909 | 910 | return { 911 | success: true, 912 | issue: { 913 | id: updatedIssue.id, 914 | identifier: updatedIssue.identifier, 915 | title: updatedIssue.title, 916 | team: teamData 917 | ? { 918 | id: teamData.id, 919 | name: teamData.name, 920 | key: teamData.key, 921 | } 922 | : null, 923 | url: updatedIssue.url, 924 | }, 925 | }; 926 | } catch (error) { 927 | console.error('Error transferring issue:', error); 928 | throw error; 929 | } 930 | } 931 | 932 | /** 933 | * Duplicates an issue 934 | */ 935 | async duplicateIssue(issueId: string) { 936 | try { 937 | // Get the issue 938 | const issue = await this.client.issue(issueId); 939 | if (!issue) { 940 | throw new Error(`Issue with ID ${issueId} not found`); 941 | } 942 | 943 | // Get all the relevant issue data 944 | const teamData = await issue.team; 945 | if (!teamData) { 946 | throw new Error('Could not retrieve team data for the issue'); 947 | } 948 | 949 | // Create a new issue using the createIssue method of this service 950 | const newIssueData = await this.createIssue({ 951 | title: `${issue.title} (Copy)`, 952 | description: issue.description, 953 | teamId: teamData.id, 954 | // We'll have to implement getting these properties in a production environment 955 | // For now, we'll just create a basic copy with title and description 956 | }); 957 | 958 | // Get the full issue details with identifier 959 | const newIssue = await this.client.issue(newIssueData.id); 960 | 961 | return { 962 | success: true, 963 | originalIssue: { 964 | id: issue.id, 965 | identifier: issue.identifier, 966 | title: issue.title, 967 | }, 968 | duplicatedIssue: { 969 | id: newIssue.id, 970 | identifier: newIssue.identifier, 971 | title: newIssue.title, 972 | url: newIssue.url, 973 | }, 974 | }; 975 | } catch (error) { 976 | console.error('Error duplicating issue:', error); 977 | throw error; 978 | } 979 | } 980 | 981 | /** 982 | * Gets the history of changes made to an issue 983 | */ 984 | async getIssueHistory(issueId: string, limit = 10) { 985 | try { 986 | // Get the issue 987 | const issue = await this.client.issue(issueId); 988 | if (!issue) { 989 | throw new Error(`Issue with ID ${issueId} not found`); 990 | } 991 | 992 | // Get the issue history 993 | const history = await issue.history({ first: limit }); 994 | 995 | // Process and format each history event 996 | const historyEvents = await Promise.all( 997 | history.nodes.map(async (event) => { 998 | // Get the actor data if available 999 | const actorData = event.actor ? await event.actor : null; 1000 | 1001 | return { 1002 | id: event.id, 1003 | createdAt: event.createdAt, 1004 | actor: actorData 1005 | ? { 1006 | id: actorData.id, 1007 | name: actorData.name, 1008 | displayName: actorData.displayName, 1009 | } 1010 | : null, 1011 | // Use optional chaining to safely access properties that may not exist 1012 | type: (event as any).type || 'unknown', 1013 | from: (event as any).from || null, 1014 | to: (event as any).to || null, 1015 | }; 1016 | }), 1017 | ); 1018 | 1019 | return { 1020 | issueId: issue.id, 1021 | identifier: issue.identifier, 1022 | history: historyEvents, 1023 | }; 1024 | } catch (error) { 1025 | console.error('Error getting issue history:', error); 1026 | throw error; 1027 | } 1028 | } 1029 | 1030 | /** 1031 | * Get all comments for an issue 1032 | * @param issueId The ID or identifier of the issue 1033 | * @param limit Maximum number of comments to return 1034 | * @returns List of comments 1035 | */ 1036 | async getComments(issueId: string, limit = 25) { 1037 | try { 1038 | // Get the issue 1039 | const issue = await this.client.issue(issueId); 1040 | if (!issue) { 1041 | throw new Error(`Issue with ID ${issueId} not found`); 1042 | } 1043 | 1044 | // Get comments 1045 | const comments = await issue.comments({ first: limit }); 1046 | 1047 | // Process comments 1048 | return Promise.all( 1049 | comments.nodes.map(async (comment) => { 1050 | const userData = comment.user ? await comment.user : null; 1051 | 1052 | return { 1053 | id: comment.id, 1054 | body: comment.body, 1055 | createdAt: comment.createdAt, 1056 | user: userData 1057 | ? { 1058 | id: userData.id, 1059 | name: userData.name, 1060 | displayName: userData.displayName, 1061 | } 1062 | : null, 1063 | url: comment.url, 1064 | }; 1065 | }), 1066 | ); 1067 | } catch (error) { 1068 | console.error('Error getting comments:', error); 1069 | throw error; 1070 | } 1071 | } 1072 | 1073 | /** 1074 | * Update an existing project 1075 | * @param args Project update data 1076 | * @returns Updated project 1077 | */ 1078 | async updateProject(args: { 1079 | id: string; 1080 | name?: string; 1081 | description?: string; 1082 | content?: string; 1083 | state?: string; 1084 | startDate?: string; 1085 | targetDate?: string; 1086 | leadId?: string; 1087 | memberIds?: string[] | string; 1088 | sortOrder?: number; 1089 | icon?: string; 1090 | color?: string; 1091 | }) { 1092 | try { 1093 | // Get the project 1094 | const project = await this.client.project(args.id); 1095 | if (!project) { 1096 | throw new Error(`Project with ID ${args.id} not found`); 1097 | } 1098 | 1099 | // Process member IDs if provided 1100 | const memberIds = args.memberIds 1101 | ? Array.isArray(args.memberIds) 1102 | ? args.memberIds 1103 | : [args.memberIds] 1104 | : undefined; 1105 | 1106 | // Update the project using client.updateProject 1107 | const updatePayload = await this.client.updateProject(args.id, { 1108 | name: args.name, 1109 | description: args.description, 1110 | content: args.content, 1111 | state: args.state as any, 1112 | startDate: args.startDate ? new Date(args.startDate) : undefined, 1113 | targetDate: args.targetDate ? new Date(args.targetDate) : undefined, 1114 | leadId: args.leadId, 1115 | memberIds: memberIds, 1116 | sortOrder: args.sortOrder, 1117 | icon: args.icon, 1118 | color: args.color, 1119 | }); 1120 | 1121 | if (updatePayload.success) { 1122 | // Get the updated project data 1123 | const updatedProject = await this.client.project(args.id); 1124 | const leadData = updatedProject.lead ? await updatedProject.lead : null; 1125 | 1126 | // Return the updated project info 1127 | return { 1128 | id: updatedProject.id, 1129 | name: updatedProject.name, 1130 | description: updatedProject.description, 1131 | content: updatedProject.content, 1132 | state: updatedProject.state, 1133 | startDate: updatedProject.startDate, 1134 | targetDate: updatedProject.targetDate, 1135 | lead: leadData 1136 | ? { 1137 | id: leadData.id, 1138 | name: leadData.name, 1139 | } 1140 | : null, 1141 | icon: updatedProject.icon, 1142 | color: updatedProject.color, 1143 | url: updatedProject.url, 1144 | }; 1145 | } else { 1146 | throw new Error('Failed to update project'); 1147 | } 1148 | } catch (error) { 1149 | console.error('Error updating project:', error); 1150 | throw error; 1151 | } 1152 | } 1153 | 1154 | /** 1155 | * Add an issue to a project 1156 | * @param issueId ID of the issue to add 1157 | * @param projectId ID of the project 1158 | * @returns Success status and issue details 1159 | */ 1160 | async addIssueToProject(issueId: string, projectId: string) { 1161 | try { 1162 | // Get the issue 1163 | const issue = await this.client.issue(issueId); 1164 | if (!issue) { 1165 | throw new Error(`Issue with ID ${issueId} not found`); 1166 | } 1167 | 1168 | // Get the project 1169 | const project = await this.client.project(projectId); 1170 | if (!project) { 1171 | throw new Error(`Project with ID ${projectId} not found`); 1172 | } 1173 | 1174 | // Update the issue with the project ID 1175 | await issue.update({ 1176 | projectId: projectId, 1177 | }); 1178 | 1179 | // Get the updated issue data with project 1180 | const updatedIssue = await this.client.issue(issueId); 1181 | const projectData = updatedIssue.project ? await updatedIssue.project : null; 1182 | 1183 | return { 1184 | success: true, 1185 | issue: { 1186 | id: updatedIssue.id, 1187 | identifier: updatedIssue.identifier, 1188 | title: updatedIssue.title, 1189 | project: projectData 1190 | ? { 1191 | id: projectData.id, 1192 | name: projectData.name, 1193 | } 1194 | : null, 1195 | }, 1196 | }; 1197 | } catch (error) { 1198 | console.error('Error adding issue to project:', error); 1199 | throw error; 1200 | } 1201 | } 1202 | 1203 | /** 1204 | * Get all issues associated with a project 1205 | * @param projectId ID of the project 1206 | * @param limit Maximum number of issues to return 1207 | * @returns List of issues in the project 1208 | */ 1209 | async getProjectIssues(projectId: string, limit = 25) { 1210 | try { 1211 | // Get the project 1212 | const project = await this.client.project(projectId); 1213 | if (!project) { 1214 | throw new Error(`Project with ID ${projectId} not found`); 1215 | } 1216 | 1217 | // Get issues for the project 1218 | const issues = await this.client.issues({ 1219 | first: limit, 1220 | filter: { 1221 | project: { 1222 | id: { eq: projectId }, 1223 | }, 1224 | }, 1225 | }); 1226 | 1227 | // Process the issues 1228 | return Promise.all( 1229 | issues.nodes.map(async (issue) => { 1230 | const teamData = issue.team ? await issue.team : null; 1231 | const assigneeData = issue.assignee ? await issue.assignee : null; 1232 | 1233 | return { 1234 | id: issue.id, 1235 | identifier: issue.identifier, 1236 | title: issue.title, 1237 | description: issue.description, 1238 | state: issue.state, 1239 | priority: issue.priority, 1240 | team: teamData 1241 | ? { 1242 | id: teamData.id, 1243 | name: teamData.name, 1244 | } 1245 | : null, 1246 | assignee: assigneeData 1247 | ? { 1248 | id: assigneeData.id, 1249 | name: assigneeData.name, 1250 | } 1251 | : null, 1252 | url: issue.url, 1253 | }; 1254 | }), 1255 | ); 1256 | } catch (error) { 1257 | console.error('Error getting project issues:', error); 1258 | throw error; 1259 | } 1260 | } 1261 | 1262 | /** 1263 | * Gets a list of all cycles 1264 | * @param teamId Optional team ID to filter cycles by team 1265 | * @param limit Maximum number of cycles to return 1266 | * @returns List of cycles 1267 | */ 1268 | async getCycles(teamId?: string, limit = 25) { 1269 | try { 1270 | const filters: Record = {}; 1271 | 1272 | if (teamId) { 1273 | filters.team = { id: { eq: teamId } }; 1274 | } 1275 | 1276 | const cycles = await this.client.cycles({ 1277 | filter: filters, 1278 | first: limit, 1279 | }); 1280 | 1281 | const cyclesData = await cycles.nodes; 1282 | 1283 | return Promise.all( 1284 | cyclesData.map(async (cycle) => { 1285 | // Get team information 1286 | const team = cycle.team ? await cycle.team : null; 1287 | 1288 | return { 1289 | id: cycle.id, 1290 | number: cycle.number, 1291 | name: cycle.name, 1292 | description: cycle.description, 1293 | startsAt: cycle.startsAt, 1294 | endsAt: cycle.endsAt, 1295 | completedAt: cycle.completedAt, 1296 | team: team 1297 | ? { 1298 | id: team.id, 1299 | name: team.name, 1300 | key: team.key, 1301 | } 1302 | : null, 1303 | }; 1304 | }), 1305 | ); 1306 | } catch (error) { 1307 | console.error('Error getting cycles:', error); 1308 | throw error; 1309 | } 1310 | } 1311 | 1312 | /** 1313 | * Gets the currently active cycle for a team 1314 | * @param teamId ID of the team 1315 | * @returns Active cycle information with progress stats 1316 | */ 1317 | async getActiveCycle(teamId: string) { 1318 | try { 1319 | // Get the team 1320 | const team = await this.client.team(teamId); 1321 | if (!team) { 1322 | throw new Error(`Team with ID ${teamId} not found`); 1323 | } 1324 | 1325 | // Get the active cycle for the team 1326 | const activeCycle = await team.activeCycle; 1327 | if (!activeCycle) { 1328 | throw new Error(`No active cycle found for team ${team.name}`); 1329 | } 1330 | 1331 | // Get cycle issues for count and progress 1332 | const cycleIssues = await this.client.issues({ 1333 | filter: { 1334 | cycle: { id: { eq: activeCycle.id } }, 1335 | }, 1336 | }); 1337 | const issueNodes = await cycleIssues.nodes; 1338 | 1339 | // Calculate progress 1340 | const totalIssues = issueNodes.length; 1341 | const completedIssues = issueNodes.filter((issue) => issue.completedAt).length; 1342 | const progress = totalIssues > 0 ? (completedIssues / totalIssues) * 100 : 0; 1343 | 1344 | return { 1345 | id: activeCycle.id, 1346 | number: activeCycle.number, 1347 | name: activeCycle.name, 1348 | description: activeCycle.description, 1349 | startsAt: activeCycle.startsAt, 1350 | endsAt: activeCycle.endsAt, 1351 | team: { 1352 | id: team.id, 1353 | name: team.name, 1354 | key: team.key, 1355 | }, 1356 | progress: Math.round(progress * 100) / 100, // Round to 2 decimal places 1357 | issueCount: totalIssues, 1358 | completedIssueCount: completedIssues, 1359 | }; 1360 | } catch (error) { 1361 | console.error('Error getting active cycle:', error); 1362 | throw error; 1363 | } 1364 | } 1365 | 1366 | /** 1367 | * Adds an issue to a cycle 1368 | * @param issueId ID or identifier of the issue 1369 | * @param cycleId ID of the cycle 1370 | * @returns Success status and updated issue information 1371 | */ 1372 | async addIssueToCycle(issueId: string, cycleId: string) { 1373 | try { 1374 | // Get the issue 1375 | const issueResult = await this.client.issue(issueId); 1376 | if (!issueResult) { 1377 | throw new Error(`Issue with ID ${issueId} not found`); 1378 | } 1379 | 1380 | // Get the cycle 1381 | const cycleResult = await this.client.cycle(cycleId); 1382 | if (!cycleResult) { 1383 | throw new Error(`Cycle with ID ${cycleId} not found`); 1384 | } 1385 | 1386 | // Update the issue with the cycle ID 1387 | await this.client.updateIssue(issueResult.id, { cycleId: cycleId }); 1388 | 1389 | // Get the updated issue data 1390 | const updatedIssue = await this.client.issue(issueId); 1391 | const cycleData = await this.client.cycle(cycleId); 1392 | 1393 | return { 1394 | success: true, 1395 | issue: { 1396 | id: updatedIssue.id, 1397 | identifier: updatedIssue.identifier, 1398 | title: updatedIssue.title, 1399 | cycle: cycleData 1400 | ? { 1401 | id: cycleData.id, 1402 | number: cycleData.number, 1403 | name: cycleData.name, 1404 | } 1405 | : null, 1406 | }, 1407 | }; 1408 | } catch (error) { 1409 | console.error('Error adding issue to cycle:', error); 1410 | throw error; 1411 | } 1412 | } 1413 | 1414 | /** 1415 | * Get workflow states for a team 1416 | * @param teamId ID of the team to get workflow states for 1417 | * @param includeArchived Whether to include archived states (default: false) 1418 | * @returns Array of workflow states with their details 1419 | */ 1420 | async getWorkflowStates(teamId: string, includeArchived = false) { 1421 | try { 1422 | // Use GraphQL to query workflow states for the team 1423 | const response = await this.client.workflowStates({ 1424 | filter: { 1425 | team: { id: { eq: teamId } }, 1426 | }, 1427 | }); 1428 | 1429 | if (!response.nodes || response.nodes.length === 0) { 1430 | return []; 1431 | } 1432 | 1433 | // Filter out archived states if includeArchived is false 1434 | let states = response.nodes; 1435 | if (!includeArchived) { 1436 | states = states.filter((state) => !state.archivedAt); 1437 | } 1438 | 1439 | // Map the response to match our output schema 1440 | return states.map((state) => ({ 1441 | id: state.id, 1442 | name: state.name, 1443 | type: state.type, 1444 | position: state.position, 1445 | color: state.color, 1446 | description: state.description || '', 1447 | })); 1448 | } catch (error: unknown) { 1449 | // Properly handle the unknown error type 1450 | const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; 1451 | throw new Error(`Failed to get workflow states: ${errorMessage}`); 1452 | } 1453 | } 1454 | 1455 | /** 1456 | * Creates a project update 1457 | * @param args Project update parameters 1458 | * @returns Created project update details 1459 | */ 1460 | async createProjectUpdate(args: { 1461 | projectId: string; 1462 | body: string; 1463 | health?: 'onTrack' | 'atRisk' | 'offTrack' | string; 1464 | userId?: string; 1465 | attachments?: string[]; 1466 | }) { 1467 | try { 1468 | // Get the project 1469 | const project = await this.client.project(args.projectId); 1470 | if (!project) { 1471 | throw new Error(`Project with ID ${args.projectId} not found`); 1472 | } 1473 | 1474 | // Create the project update 1475 | const createPayload = await this.client.createProjectUpdate({ 1476 | projectId: args.projectId, 1477 | body: args.body, 1478 | health: args.health as any, 1479 | // Note: userId and attachmentIds are not supported in the direct API input 1480 | // The SDK uses the authenticated user by default 1481 | }); 1482 | 1483 | if (createPayload.success && createPayload.projectUpdate) { 1484 | const updateData = await createPayload.projectUpdate; 1485 | const userData = updateData.user ? await updateData.user : null; 1486 | 1487 | return { 1488 | id: updateData.id, 1489 | body: updateData.body, 1490 | health: updateData.health, 1491 | createdAt: updateData.createdAt, 1492 | updatedAt: updateData.updatedAt, 1493 | user: userData 1494 | ? { 1495 | id: userData.id, 1496 | name: userData.name, 1497 | } 1498 | : null, 1499 | project: { 1500 | id: project.id, 1501 | name: project.name, 1502 | }, 1503 | }; 1504 | } else { 1505 | throw new Error('Failed to create project update'); 1506 | } 1507 | } catch (error) { 1508 | console.error('Error creating project update:', error); 1509 | throw error; 1510 | } 1511 | } 1512 | 1513 | /** 1514 | * Updates an existing project update 1515 | * @param args Update parameters 1516 | * @returns Updated project update details 1517 | */ 1518 | async updateProjectUpdate(args: { 1519 | id: string; 1520 | body?: string; 1521 | health?: 'onTrack' | 'atRisk' | 'offTrack' | string; 1522 | }) { 1523 | try { 1524 | // Get the project update 1525 | const projectUpdate = await this.client.projectUpdate(args.id); 1526 | if (!projectUpdate) { 1527 | throw new Error(`Project update with ID ${args.id} not found`); 1528 | } 1529 | 1530 | // Get project info for the response 1531 | const projectData = await projectUpdate.project; 1532 | if (!projectData) { 1533 | throw new Error(`Project not found for update with ID ${args.id}`); 1534 | } 1535 | 1536 | // Update the project update 1537 | const updatePayload = await this.client.updateProjectUpdate(args.id, { 1538 | body: args.body, 1539 | health: args.health as any, 1540 | }); 1541 | 1542 | if (updatePayload.success) { 1543 | // Get the updated project update data 1544 | const updatedProjectUpdate = await this.client.projectUpdate(args.id); 1545 | const userData = updatedProjectUpdate.user ? await updatedProjectUpdate.user : null; 1546 | 1547 | // Return the updated project update info 1548 | return { 1549 | id: updatedProjectUpdate.id, 1550 | body: updatedProjectUpdate.body, 1551 | health: updatedProjectUpdate.health, 1552 | createdAt: updatedProjectUpdate.createdAt, 1553 | updatedAt: updatedProjectUpdate.updatedAt, 1554 | user: userData 1555 | ? { 1556 | id: userData.id, 1557 | name: userData.name, 1558 | } 1559 | : null, 1560 | project: { 1561 | id: projectData.id, 1562 | name: projectData.name, 1563 | }, 1564 | }; 1565 | } else { 1566 | throw new Error('Failed to update project update'); 1567 | } 1568 | } catch (error) { 1569 | console.error('Error updating project update:', error); 1570 | throw error; 1571 | } 1572 | } 1573 | 1574 | /** 1575 | * Gets updates for a project 1576 | * @param projectId ID of the project 1577 | * @param limit Maximum number of updates to return 1578 | * @returns List of project updates 1579 | */ 1580 | async getProjectUpdates(projectId: string, limit = 25) { 1581 | try { 1582 | // Get the project 1583 | const project = await this.client.project(projectId); 1584 | if (!project) { 1585 | throw new Error(`Project with ID ${projectId} not found`); 1586 | } 1587 | 1588 | // Get project updates 1589 | const updates = await this.client.projectUpdates({ 1590 | first: limit, 1591 | filter: { 1592 | project: { 1593 | id: { eq: projectId }, 1594 | }, 1595 | }, 1596 | }); 1597 | 1598 | // Process and return the updates 1599 | return Promise.all( 1600 | updates.nodes.map(async (update) => { 1601 | const userData = update.user ? await update.user : null; 1602 | 1603 | return { 1604 | id: update.id, 1605 | body: update.body, 1606 | health: update.health, 1607 | createdAt: update.createdAt, 1608 | updatedAt: update.updatedAt, 1609 | user: userData 1610 | ? { 1611 | id: userData.id, 1612 | name: userData.name, 1613 | } 1614 | : null, 1615 | project: { 1616 | id: project.id, 1617 | name: project.name, 1618 | }, 1619 | }; 1620 | }), 1621 | ); 1622 | } catch (error) { 1623 | console.error('Error getting project updates:', error); 1624 | throw error; 1625 | } 1626 | } 1627 | 1628 | /** 1629 | * Archives a project 1630 | * @param projectId ID of the project to archive 1631 | * @returns Success status and archived project info 1632 | */ 1633 | async archiveProject(projectId: string) { 1634 | try { 1635 | // Get the project 1636 | const project = await this.client.project(projectId); 1637 | if (!project) { 1638 | throw new Error(`Project with ID ${projectId} not found`); 1639 | } 1640 | 1641 | // Archive the project 1642 | const archivePayload = await project.archive(); 1643 | 1644 | if (archivePayload.success) { 1645 | // Get the archived project data 1646 | const archivedProject = await this.client.project(projectId); 1647 | 1648 | return { 1649 | success: true, 1650 | project: { 1651 | id: archivedProject.id, 1652 | name: archivedProject.name, 1653 | state: archivedProject.state, 1654 | archivedAt: archivedProject.archivedAt, 1655 | }, 1656 | }; 1657 | } else { 1658 | throw new Error('Failed to archive project'); 1659 | } 1660 | } catch (error) { 1661 | console.error('Error archiving project:', error); 1662 | throw error; 1663 | } 1664 | } 1665 | } 1666 | -------------------------------------------------------------------------------- /src/tools/definitions/cycle-tools.ts: -------------------------------------------------------------------------------- 1 | import { MCPToolDefinition } from '../../types.js'; 2 | 3 | /** 4 | * Tool definition for getting all cycles 5 | */ 6 | export const getCyclesToolDefinition: MCPToolDefinition = { 7 | name: 'linear_getCycles', 8 | description: 'Get a list of all cycles', 9 | input_schema: { 10 | type: 'object', 11 | properties: { 12 | teamId: { 13 | type: 'string', 14 | description: 'ID of the team to get cycles for (optional)', 15 | }, 16 | limit: { 17 | type: 'number', 18 | description: 'Maximum number of cycles to return (default: 25)', 19 | }, 20 | }, 21 | }, 22 | output_schema: { 23 | type: 'array', 24 | items: { 25 | type: 'object', 26 | properties: { 27 | id: { type: 'string' }, 28 | number: { type: 'number' }, 29 | name: { type: 'string' }, 30 | description: { type: 'string' }, 31 | startsAt: { type: 'string' }, 32 | endsAt: { type: 'string' }, 33 | completedAt: { type: 'string' }, 34 | team: { 35 | type: 'object', 36 | properties: { 37 | id: { type: 'string' }, 38 | name: { type: 'string' }, 39 | key: { type: 'string' }, 40 | }, 41 | }, 42 | }, 43 | }, 44 | }, 45 | }; 46 | 47 | /** 48 | * Tool definition for getting the active cycle for a team 49 | */ 50 | export const getActiveCycleToolDefinition: MCPToolDefinition = { 51 | name: 'linear_getActiveCycle', 52 | description: 'Get the currently active cycle for a team', 53 | input_schema: { 54 | type: 'object', 55 | properties: { 56 | teamId: { 57 | type: 'string', 58 | description: 'ID of the team to get the active cycle for', 59 | }, 60 | }, 61 | required: ['teamId'], 62 | }, 63 | output_schema: { 64 | type: 'object', 65 | properties: { 66 | id: { type: 'string' }, 67 | number: { type: 'number' }, 68 | name: { type: 'string' }, 69 | description: { type: 'string' }, 70 | startsAt: { type: 'string' }, 71 | endsAt: { type: 'string' }, 72 | team: { 73 | type: 'object', 74 | properties: { 75 | id: { type: 'string' }, 76 | name: { type: 'string' }, 77 | key: { type: 'string' }, 78 | }, 79 | }, 80 | progress: { type: 'number' }, 81 | issueCount: { type: 'number' }, 82 | completedIssueCount: { type: 'number' }, 83 | }, 84 | }, 85 | }; 86 | 87 | /** 88 | * Tool definition for adding an issue to a cycle 89 | */ 90 | export const addIssueToCycleToolDefinition: MCPToolDefinition = { 91 | name: 'linear_addIssueToCycle', 92 | description: 'Add an issue to a cycle', 93 | input_schema: { 94 | type: 'object', 95 | properties: { 96 | issueId: { 97 | type: 'string', 98 | description: 'ID or identifier of the issue to add to the cycle', 99 | }, 100 | cycleId: { 101 | type: 'string', 102 | description: 'ID of the cycle to add the issue to', 103 | }, 104 | }, 105 | required: ['issueId', 'cycleId'], 106 | }, 107 | output_schema: { 108 | type: 'object', 109 | properties: { 110 | success: { type: 'boolean' }, 111 | issue: { 112 | type: 'object', 113 | properties: { 114 | id: { type: 'string' }, 115 | identifier: { type: 'string' }, 116 | title: { type: 'string' }, 117 | cycle: { 118 | type: 'object', 119 | properties: { 120 | id: { type: 'string' }, 121 | number: { type: 'number' }, 122 | name: { type: 'string' }, 123 | }, 124 | }, 125 | }, 126 | }, 127 | }, 128 | }, 129 | }; 130 | -------------------------------------------------------------------------------- /src/tools/definitions/index.ts: -------------------------------------------------------------------------------- 1 | import { MCPToolDefinition } from '../../types.js'; 2 | import { 3 | getIssuesToolDefinition, 4 | getIssueByIdToolDefinition, 5 | searchIssuesToolDefinition, 6 | createIssueToolDefinition, 7 | updateIssueToolDefinition, 8 | createCommentToolDefinition, 9 | addIssueLabelToolDefinition, 10 | removeIssueLabelToolDefinition, 11 | // New Issue Management tools 12 | assignIssueToolDefinition, 13 | subscribeToIssueToolDefinition, 14 | convertIssueToSubtaskToolDefinition, 15 | createIssueRelationToolDefinition, 16 | archiveIssueToolDefinition, 17 | setIssuePriorityToolDefinition, 18 | transferIssueToolDefinition, 19 | duplicateIssueToolDefinition, 20 | getIssueHistoryToolDefinition, 21 | // Comment Management tools 22 | getCommentsToolDefinition, 23 | } from './issue-tools.js'; 24 | import { 25 | getProjectsToolDefinition, 26 | createProjectToolDefinition, 27 | // Project Management tools 28 | updateProjectToolDefinition, 29 | addIssueToProjectToolDefinition, 30 | getProjectIssuesToolDefinition, 31 | } from './project-tools.js'; 32 | import { getTeamsToolDefinition, getWorkflowStatesToolDefinition } from './team-tools.js'; 33 | import { 34 | getViewerToolDefinition, 35 | getOrganizationToolDefinition, 36 | getUsersToolDefinition, 37 | getLabelsToolDefinition, 38 | } from './user-tools.js'; 39 | import { 40 | // Cycle Management tools 41 | getCyclesToolDefinition, 42 | getActiveCycleToolDefinition, 43 | addIssueToCycleToolDefinition, 44 | } from './cycle-tools.js'; 45 | 46 | // All tool definitions 47 | export const allToolDefinitions: MCPToolDefinition[] = [ 48 | // User tools 49 | getViewerToolDefinition, 50 | getOrganizationToolDefinition, 51 | getUsersToolDefinition, 52 | getLabelsToolDefinition, 53 | 54 | // Team tools 55 | getTeamsToolDefinition, 56 | getWorkflowStatesToolDefinition, 57 | 58 | // Project tools 59 | getProjectsToolDefinition, 60 | createProjectToolDefinition, 61 | 62 | // Project Management tools 63 | updateProjectToolDefinition, 64 | addIssueToProjectToolDefinition, 65 | getProjectIssuesToolDefinition, 66 | 67 | // Cycle Management tools 68 | getCyclesToolDefinition, 69 | getActiveCycleToolDefinition, 70 | addIssueToCycleToolDefinition, 71 | 72 | // Issue tools 73 | getIssuesToolDefinition, 74 | getIssueByIdToolDefinition, 75 | searchIssuesToolDefinition, 76 | createIssueToolDefinition, 77 | updateIssueToolDefinition, 78 | createCommentToolDefinition, 79 | addIssueLabelToolDefinition, 80 | removeIssueLabelToolDefinition, 81 | 82 | // New Issue Management tools 83 | assignIssueToolDefinition, 84 | subscribeToIssueToolDefinition, 85 | convertIssueToSubtaskToolDefinition, 86 | createIssueRelationToolDefinition, 87 | archiveIssueToolDefinition, 88 | setIssuePriorityToolDefinition, 89 | transferIssueToolDefinition, 90 | duplicateIssueToolDefinition, 91 | getIssueHistoryToolDefinition, 92 | 93 | // Comment Management tools 94 | getCommentsToolDefinition, 95 | ]; 96 | 97 | // Export all tool definitions individually 98 | export { 99 | getIssuesToolDefinition, 100 | getIssueByIdToolDefinition, 101 | searchIssuesToolDefinition, 102 | createIssueToolDefinition, 103 | updateIssueToolDefinition, 104 | createCommentToolDefinition, 105 | addIssueLabelToolDefinition, 106 | removeIssueLabelToolDefinition, 107 | getProjectsToolDefinition, 108 | createProjectToolDefinition, 109 | getTeamsToolDefinition, 110 | getWorkflowStatesToolDefinition, 111 | getViewerToolDefinition, 112 | getOrganizationToolDefinition, 113 | getUsersToolDefinition, 114 | getLabelsToolDefinition, 115 | 116 | // New Issue Management tools 117 | assignIssueToolDefinition, 118 | subscribeToIssueToolDefinition, 119 | convertIssueToSubtaskToolDefinition, 120 | createIssueRelationToolDefinition, 121 | archiveIssueToolDefinition, 122 | setIssuePriorityToolDefinition, 123 | transferIssueToolDefinition, 124 | duplicateIssueToolDefinition, 125 | getIssueHistoryToolDefinition, 126 | 127 | // Comment Management tools 128 | getCommentsToolDefinition, 129 | 130 | // Project Management tools 131 | updateProjectToolDefinition, 132 | addIssueToProjectToolDefinition, 133 | getProjectIssuesToolDefinition, 134 | 135 | // Cycle Management tools 136 | getCyclesToolDefinition, 137 | getActiveCycleToolDefinition, 138 | addIssueToCycleToolDefinition, 139 | }; 140 | -------------------------------------------------------------------------------- /src/tools/definitions/issue-tools.ts: -------------------------------------------------------------------------------- 1 | import { MCPToolDefinition } from '../../types.js'; 2 | 3 | /** 4 | * Tool definition for getting issues 5 | */ 6 | export const getIssuesToolDefinition: MCPToolDefinition = { 7 | name: 'linear_getIssues', 8 | description: 'Get a list of recent issues from Linear', 9 | input_schema: { 10 | type: 'object', 11 | properties: { 12 | limit: { 13 | type: 'number', 14 | description: 'Maximum number of issues to return (default: 10)', 15 | }, 16 | }, 17 | }, 18 | output_schema: { 19 | type: 'array', 20 | items: { 21 | type: 'object', 22 | properties: { 23 | id: { type: 'string' }, 24 | identifier: { type: 'string' }, 25 | title: { type: 'string' }, 26 | description: { type: 'string' }, 27 | state: { type: 'string' }, 28 | priority: { type: 'number' }, 29 | estimate: { type: 'number' }, 30 | dueDate: { type: 'string' }, 31 | team: { type: 'object' }, 32 | assignee: { type: 'object' }, 33 | project: { type: 'object' }, 34 | cycle: { type: 'object' }, 35 | parent: { type: 'object' }, 36 | labels: { 37 | type: 'array', 38 | items: { 39 | type: 'object', 40 | properties: { 41 | id: { type: 'string' }, 42 | name: { type: 'string' }, 43 | color: { type: 'string' }, 44 | }, 45 | }, 46 | }, 47 | sortOrder: { type: 'number' }, 48 | createdAt: { type: 'string' }, 49 | updatedAt: { type: 'string' }, 50 | url: { type: 'string' }, 51 | }, 52 | }, 53 | }, 54 | }; 55 | 56 | /** 57 | * Tool definition for getting issue by ID 58 | */ 59 | export const getIssueByIdToolDefinition: MCPToolDefinition = { 60 | name: 'linear_getIssueById', 61 | description: 'Get a specific issue by ID or identifier (e.g., ABC-123)', 62 | input_schema: { 63 | type: 'object', 64 | properties: { 65 | id: { 66 | type: 'string', 67 | description: 'The ID or identifier of the issue (e.g., ABC-123)', 68 | }, 69 | }, 70 | required: ['id'], 71 | }, 72 | output_schema: { 73 | type: 'object', 74 | properties: { 75 | id: { type: 'string' }, 76 | identifier: { type: 'string' }, 77 | title: { type: 'string' }, 78 | description: { type: 'string' }, 79 | state: { type: 'string' }, 80 | priority: { type: 'number' }, 81 | estimate: { type: 'number' }, 82 | dueDate: { type: 'string' }, 83 | team: { type: 'object' }, 84 | assignee: { type: 'object' }, 85 | project: { type: 'object' }, 86 | cycle: { type: 'object' }, 87 | parent: { type: 'object' }, 88 | labels: { 89 | type: 'array', 90 | items: { 91 | type: 'object', 92 | properties: { 93 | id: { type: 'string' }, 94 | name: { type: 'string' }, 95 | color: { type: 'string' }, 96 | }, 97 | }, 98 | }, 99 | sortOrder: { type: 'number' }, 100 | createdAt: { type: 'string' }, 101 | updatedAt: { type: 'string' }, 102 | url: { type: 'string' }, 103 | comments: { type: 'array' }, 104 | }, 105 | }, 106 | }; 107 | 108 | /** 109 | * Tool definition for searching issues 110 | */ 111 | export const searchIssuesToolDefinition: MCPToolDefinition = { 112 | name: 'linear_searchIssues', 113 | description: 'Search for issues with various filters', 114 | input_schema: { 115 | type: 'object', 116 | properties: { 117 | query: { 118 | type: 'string', 119 | description: 'Text to search for in issue title or description', 120 | }, 121 | teamId: { 122 | type: 'string', 123 | description: 'Filter issues by team ID', 124 | }, 125 | assigneeId: { 126 | type: 'string', 127 | description: 'Filter issues by assignee ID', 128 | }, 129 | projectId: { 130 | type: 'string', 131 | description: 'Filter issues by project ID', 132 | }, 133 | states: { 134 | type: 'array', 135 | items: { type: 'string' }, 136 | description: "Filter issues by state name (e.g., 'Todo', 'In Progress', 'Done')", 137 | }, 138 | limit: { 139 | type: 'number', 140 | description: 'Maximum number of issues to return (default: 10)', 141 | }, 142 | }, 143 | required: [], 144 | }, 145 | output_schema: { 146 | type: 'array', 147 | items: { 148 | type: 'object', 149 | properties: { 150 | id: { type: 'string' }, 151 | identifier: { type: 'string' }, 152 | title: { type: 'string' }, 153 | description: { type: 'string' }, 154 | state: { type: 'string' }, 155 | priority: { type: 'number' }, 156 | estimate: { type: 'number' }, 157 | dueDate: { type: 'string' }, 158 | team: { type: 'object' }, 159 | assignee: { type: 'object' }, 160 | project: { type: 'object' }, 161 | cycle: { type: 'object' }, 162 | parent: { type: 'object' }, 163 | labels: { 164 | type: 'array', 165 | items: { 166 | type: 'object', 167 | properties: { 168 | id: { type: 'string' }, 169 | name: { type: 'string' }, 170 | color: { type: 'string' }, 171 | }, 172 | }, 173 | }, 174 | sortOrder: { type: 'number' }, 175 | createdAt: { type: 'string' }, 176 | updatedAt: { type: 'string' }, 177 | url: { type: 'string' }, 178 | }, 179 | }, 180 | }, 181 | }; 182 | 183 | /** 184 | * Tool definition for creating an issue 185 | */ 186 | export const createIssueToolDefinition: MCPToolDefinition = { 187 | name: 'linear_createIssue', 188 | description: 'Create a new issue in Linear', 189 | input_schema: { 190 | type: 'object', 191 | properties: { 192 | title: { 193 | type: 'string', 194 | description: 'Title of the issue', 195 | }, 196 | description: { 197 | type: 'string', 198 | description: 'Description of the issue (Markdown supported)', 199 | }, 200 | teamId: { 201 | type: 'string', 202 | description: 'ID of the team the issue belongs to', 203 | }, 204 | assigneeId: { 205 | type: 'string', 206 | description: 'ID of the user to assign the issue to', 207 | }, 208 | priority: { 209 | type: 'number', 210 | description: 211 | 'Priority of the issue (0 = No priority, 1 = Urgent, 2 = High, 3 = Normal, 4 = Low)', 212 | }, 213 | projectId: { 214 | type: 'string', 215 | description: 'ID of the project the issue belongs to', 216 | }, 217 | cycleId: { 218 | type: 'string', 219 | description: 'ID of the cycle to add the issue to', 220 | }, 221 | estimate: { 222 | type: 'number', 223 | description: 'The estimated complexity/points for the issue', 224 | }, 225 | dueDate: { 226 | type: 'string', 227 | description: 'The date at which the issue is due (YYYY-MM-DD format)', 228 | }, 229 | labelIds: { 230 | type: 'array', 231 | items: { type: 'string' }, 232 | description: 'IDs of the labels to attach to the issue', 233 | }, 234 | parentId: { 235 | type: 'string', 236 | description: 'ID of the parent issue (to create as a sub-task)', 237 | }, 238 | subscriberIds: { 239 | type: 'array', 240 | items: { type: 'string' }, 241 | description: 'IDs of the users to subscribe to the issue', 242 | }, 243 | stateId: { 244 | type: 'string', 245 | description: 'ID of the workflow state for the issue', 246 | }, 247 | templateId: { 248 | type: 'string', 249 | description: 'ID of a template to use for creating the issue', 250 | }, 251 | sortOrder: { 252 | type: 'number', 253 | description: 'The position of the issue in relation to other issues', 254 | }, 255 | }, 256 | required: ['title', 'teamId'], 257 | }, 258 | output_schema: { 259 | type: 'object', 260 | properties: { 261 | id: { type: 'string' }, 262 | identifier: { type: 'string' }, 263 | title: { type: 'string' }, 264 | url: { type: 'string' }, 265 | }, 266 | }, 267 | }; 268 | 269 | /** 270 | * Tool definition for updating an issue 271 | */ 272 | export const updateIssueToolDefinition: MCPToolDefinition = { 273 | name: 'linear_updateIssue', 274 | description: 'Update an existing issue in Linear', 275 | input_schema: { 276 | type: 'object', 277 | properties: { 278 | id: { 279 | type: 'string', 280 | description: 'ID or identifier of the issue to update (e.g., ABC-123)', 281 | }, 282 | title: { 283 | type: 'string', 284 | description: 'New title for the issue', 285 | }, 286 | description: { 287 | type: 'string', 288 | description: 'New description for the issue (Markdown supported)', 289 | }, 290 | stateId: { 291 | type: 'string', 292 | description: 'ID of the new state for the issue', 293 | }, 294 | priority: { 295 | type: 'number', 296 | description: 297 | 'New priority for the issue (0 = No priority, 1 = Urgent, 2 = High, 3 = Normal, 4 = Low)', 298 | }, 299 | projectId: { 300 | type: 'string', 301 | description: 'ID of the project to move the issue to', 302 | }, 303 | assigneeId: { 304 | type: 'string', 305 | description: 'ID of the user to assign the issue to, or null to unassign', 306 | }, 307 | cycleId: { 308 | type: 'string', 309 | description: 'ID of the cycle to move the issue to, or null to remove from current cycle', 310 | }, 311 | estimate: { 312 | type: 'number', 313 | description: 'The estimated complexity/points for the issue', 314 | }, 315 | dueDate: { 316 | type: 'string', 317 | description: 'The new due date for the issue (YYYY-MM-DD format), or null to remove', 318 | }, 319 | labelIds: { 320 | type: 'array', 321 | items: { type: 'string' }, 322 | description: 'IDs of the labels to set on the issue (replacing existing labels)', 323 | }, 324 | addedLabelIds: { 325 | type: 'array', 326 | items: { type: 'string' }, 327 | description: 'IDs of labels to add to the issue (without removing existing ones)', 328 | }, 329 | removedLabelIds: { 330 | type: 'array', 331 | items: { type: 'string' }, 332 | description: 'IDs of labels to remove from the issue', 333 | }, 334 | parentId: { 335 | type: 'string', 336 | description: 'ID of the parent issue, or null to convert to a regular issue', 337 | }, 338 | subscriberIds: { 339 | type: 'array', 340 | items: { type: 'string' }, 341 | description: 'IDs of the users to subscribe to the issue (replacing existing subscribers)', 342 | }, 343 | teamId: { 344 | type: 'string', 345 | description: 'ID of the team to move the issue to', 346 | }, 347 | sortOrder: { 348 | type: 'number', 349 | description: 'The position of the issue in relation to other issues', 350 | }, 351 | }, 352 | required: ['id'], 353 | }, 354 | output_schema: { 355 | type: 'object', 356 | properties: { 357 | id: { type: 'string' }, 358 | identifier: { type: 'string' }, 359 | title: { type: 'string' }, 360 | url: { type: 'string' }, 361 | }, 362 | }, 363 | }; 364 | 365 | /** 366 | * Tool definition for creating a comment 367 | */ 368 | export const createCommentToolDefinition: MCPToolDefinition = { 369 | name: 'linear_createComment', 370 | description: 'Add a comment to an issue in Linear', 371 | input_schema: { 372 | type: 'object', 373 | properties: { 374 | issueId: { 375 | type: 'string', 376 | description: 'ID or identifier of the issue to comment on (e.g., ABC-123)', 377 | }, 378 | body: { 379 | type: 'string', 380 | description: 'Text of the comment (Markdown supported)', 381 | }, 382 | }, 383 | required: ['issueId', 'body'], 384 | }, 385 | output_schema: { 386 | type: 'object', 387 | properties: { 388 | id: { type: 'string' }, 389 | body: { type: 'string' }, 390 | url: { type: 'string' }, 391 | }, 392 | }, 393 | }; 394 | 395 | /** 396 | * Tool definition for adding a label to an issue 397 | */ 398 | export const addIssueLabelToolDefinition: MCPToolDefinition = { 399 | name: 'linear_addIssueLabel', 400 | description: 'Add a label to an issue in Linear', 401 | input_schema: { 402 | type: 'object', 403 | properties: { 404 | issueId: { 405 | type: 'string', 406 | description: 'ID or identifier of the issue to add the label to (e.g., ABC-123)', 407 | }, 408 | labelId: { 409 | type: 'string', 410 | description: 'ID of the label to add to the issue', 411 | }, 412 | }, 413 | required: ['issueId', 'labelId'], 414 | }, 415 | output_schema: { 416 | type: 'object', 417 | properties: { 418 | success: { type: 'boolean' }, 419 | issueId: { type: 'string' }, 420 | labelId: { type: 'string' }, 421 | }, 422 | }, 423 | }; 424 | 425 | /** 426 | * Tool definition for removing a label from an issue 427 | */ 428 | export const removeIssueLabelToolDefinition: MCPToolDefinition = { 429 | name: 'linear_removeIssueLabel', 430 | description: 'Remove a label from an issue in Linear', 431 | input_schema: { 432 | type: 'object', 433 | properties: { 434 | issueId: { 435 | type: 'string', 436 | description: 'ID or identifier of the issue to remove the label from (e.g., ABC-123)', 437 | }, 438 | labelId: { 439 | type: 'string', 440 | description: 'ID of the label to remove from the issue', 441 | }, 442 | }, 443 | required: ['issueId', 'labelId'], 444 | }, 445 | output_schema: { 446 | type: 'object', 447 | properties: { 448 | success: { type: 'boolean' }, 449 | issueId: { type: 'string' }, 450 | labelId: { type: 'string' }, 451 | }, 452 | }, 453 | }; 454 | 455 | /** 456 | * Tool definition for assigning an issue to a user 457 | */ 458 | export const assignIssueToolDefinition: MCPToolDefinition = { 459 | name: 'linear_assignIssue', 460 | description: 'Assign an issue to a user', 461 | input_schema: { 462 | type: 'object', 463 | properties: { 464 | issueId: { 465 | type: 'string', 466 | description: 'ID or identifier of the issue to assign (e.g., ABC-123)', 467 | }, 468 | assigneeId: { 469 | type: 'string', 470 | description: 'ID of the user to assign the issue to, or null to unassign', 471 | }, 472 | }, 473 | required: ['issueId', 'assigneeId'], 474 | }, 475 | output_schema: { 476 | type: 'object', 477 | properties: { 478 | success: { type: 'boolean' }, 479 | issue: { 480 | type: 'object', 481 | properties: { 482 | id: { type: 'string' }, 483 | identifier: { type: 'string' }, 484 | title: { type: 'string' }, 485 | assignee: { type: 'object' }, 486 | url: { type: 'string' }, 487 | }, 488 | }, 489 | }, 490 | }, 491 | }; 492 | 493 | /** 494 | * Tool definition for subscribing to issue updates 495 | */ 496 | export const subscribeToIssueToolDefinition: MCPToolDefinition = { 497 | name: 'linear_subscribeToIssue', 498 | description: 'Subscribe to issue updates', 499 | input_schema: { 500 | type: 'object', 501 | properties: { 502 | issueId: { 503 | type: 'string', 504 | description: 'ID or identifier of the issue to subscribe to (e.g., ABC-123)', 505 | }, 506 | }, 507 | required: ['issueId'], 508 | }, 509 | output_schema: { 510 | type: 'object', 511 | properties: { 512 | success: { type: 'boolean' }, 513 | message: { type: 'string' }, 514 | }, 515 | }, 516 | }; 517 | 518 | /** 519 | * Tool definition for converting an issue to a subtask 520 | */ 521 | export const convertIssueToSubtaskToolDefinition: MCPToolDefinition = { 522 | name: 'linear_convertIssueToSubtask', 523 | description: 'Convert an issue to a subtask', 524 | input_schema: { 525 | type: 'object', 526 | properties: { 527 | issueId: { 528 | type: 'string', 529 | description: 'ID or identifier of the issue to convert (e.g., ABC-123)', 530 | }, 531 | parentIssueId: { 532 | type: 'string', 533 | description: 'ID or identifier of the parent issue (e.g., ABC-456)', 534 | }, 535 | }, 536 | required: ['issueId', 'parentIssueId'], 537 | }, 538 | output_schema: { 539 | type: 'object', 540 | properties: { 541 | success: { type: 'boolean' }, 542 | issue: { 543 | type: 'object', 544 | properties: { 545 | id: { type: 'string' }, 546 | identifier: { type: 'string' }, 547 | title: { type: 'string' }, 548 | parent: { type: 'object' }, 549 | url: { type: 'string' }, 550 | }, 551 | }, 552 | }, 553 | }, 554 | }; 555 | 556 | /** 557 | * Tool definition for creating issue relations 558 | */ 559 | export const createIssueRelationToolDefinition: MCPToolDefinition = { 560 | name: 'linear_createIssueRelation', 561 | description: 'Create relations between issues (blocks, is blocked by, etc.)', 562 | input_schema: { 563 | type: 'object', 564 | properties: { 565 | issueId: { 566 | type: 'string', 567 | description: 'ID or identifier of the first issue (e.g., ABC-123)', 568 | }, 569 | relatedIssueId: { 570 | type: 'string', 571 | description: 'ID or identifier of the second issue (e.g., ABC-456)', 572 | }, 573 | type: { 574 | type: 'string', 575 | description: 576 | "Type of relation: 'blocks', 'blocked_by', 'related', 'duplicate', 'duplicate_of'", 577 | enum: ['blocks', 'blocked_by', 'related', 'duplicate', 'duplicate_of'], 578 | }, 579 | }, 580 | required: ['issueId', 'relatedIssueId', 'type'], 581 | }, 582 | output_schema: { 583 | type: 'object', 584 | properties: { 585 | success: { type: 'boolean' }, 586 | relation: { 587 | type: 'object', 588 | properties: { 589 | id: { type: 'string' }, 590 | type: { type: 'string' }, 591 | issueIdentifier: { type: 'string' }, 592 | relatedIssueIdentifier: { type: 'string' }, 593 | }, 594 | }, 595 | }, 596 | }, 597 | }; 598 | 599 | /** 600 | * Tool definition for archiving an issue 601 | */ 602 | export const archiveIssueToolDefinition: MCPToolDefinition = { 603 | name: 'linear_archiveIssue', 604 | description: 'Archive an issue', 605 | input_schema: { 606 | type: 'object', 607 | properties: { 608 | issueId: { 609 | type: 'string', 610 | description: 'ID or identifier of the issue to archive (e.g., ABC-123)', 611 | }, 612 | }, 613 | required: ['issueId'], 614 | }, 615 | output_schema: { 616 | type: 'object', 617 | properties: { 618 | success: { type: 'boolean' }, 619 | message: { type: 'string' }, 620 | }, 621 | }, 622 | }; 623 | 624 | /** 625 | * Tool definition for setting issue priority 626 | */ 627 | export const setIssuePriorityToolDefinition: MCPToolDefinition = { 628 | name: 'linear_setIssuePriority', 629 | description: 'Set the priority of an issue', 630 | input_schema: { 631 | type: 'object', 632 | properties: { 633 | issueId: { 634 | type: 'string', 635 | description: 'ID or identifier of the issue (e.g., ABC-123)', 636 | }, 637 | priority: { 638 | type: 'number', 639 | description: 'Priority level (0 = No priority, 1 = Urgent, 2 = High, 3 = Normal, 4 = Low)', 640 | enum: [0, 1, 2, 3, 4], 641 | }, 642 | }, 643 | required: ['issueId', 'priority'], 644 | }, 645 | output_schema: { 646 | type: 'object', 647 | properties: { 648 | success: { type: 'boolean' }, 649 | issue: { 650 | type: 'object', 651 | properties: { 652 | id: { type: 'string' }, 653 | identifier: { type: 'string' }, 654 | title: { type: 'string' }, 655 | priority: { type: 'number' }, 656 | url: { type: 'string' }, 657 | }, 658 | }, 659 | }, 660 | }, 661 | }; 662 | 663 | /** 664 | * Tool definition for transferring an issue to another team 665 | */ 666 | export const transferIssueToolDefinition: MCPToolDefinition = { 667 | name: 'linear_transferIssue', 668 | description: 'Transfer an issue to another team', 669 | input_schema: { 670 | type: 'object', 671 | properties: { 672 | issueId: { 673 | type: 'string', 674 | description: 'ID or identifier of the issue to transfer (e.g., ABC-123)', 675 | }, 676 | teamId: { 677 | type: 'string', 678 | description: 'ID of the team to transfer the issue to', 679 | }, 680 | }, 681 | required: ['issueId', 'teamId'], 682 | }, 683 | output_schema: { 684 | type: 'object', 685 | properties: { 686 | success: { type: 'boolean' }, 687 | issue: { 688 | type: 'object', 689 | properties: { 690 | id: { type: 'string' }, 691 | identifier: { type: 'string' }, 692 | title: { type: 'string' }, 693 | team: { type: 'object' }, 694 | url: { type: 'string' }, 695 | }, 696 | }, 697 | }, 698 | }, 699 | }; 700 | 701 | /** 702 | * Tool definition for duplicating an issue 703 | */ 704 | export const duplicateIssueToolDefinition: MCPToolDefinition = { 705 | name: 'linear_duplicateIssue', 706 | description: 'Duplicate an issue', 707 | input_schema: { 708 | type: 'object', 709 | properties: { 710 | issueId: { 711 | type: 'string', 712 | description: 'ID or identifier of the issue to duplicate (e.g., ABC-123)', 713 | }, 714 | }, 715 | required: ['issueId'], 716 | }, 717 | output_schema: { 718 | type: 'object', 719 | properties: { 720 | success: { type: 'boolean' }, 721 | originalIssue: { 722 | type: 'object', 723 | properties: { 724 | id: { type: 'string' }, 725 | identifier: { type: 'string' }, 726 | title: { type: 'string' }, 727 | }, 728 | }, 729 | duplicatedIssue: { 730 | type: 'object', 731 | properties: { 732 | id: { type: 'string' }, 733 | identifier: { type: 'string' }, 734 | title: { type: 'string' }, 735 | url: { type: 'string' }, 736 | }, 737 | }, 738 | }, 739 | }, 740 | }; 741 | 742 | /** 743 | * Tool definition for getting issue history 744 | */ 745 | export const getIssueHistoryToolDefinition: MCPToolDefinition = { 746 | name: 'linear_getIssueHistory', 747 | description: 'Get the history of changes made to an issue', 748 | input_schema: { 749 | type: 'object', 750 | properties: { 751 | issueId: { 752 | type: 'string', 753 | description: 'ID or identifier of the issue (e.g., ABC-123)', 754 | }, 755 | limit: { 756 | type: 'number', 757 | description: 'Maximum number of history events to return (default: 10)', 758 | }, 759 | }, 760 | required: ['issueId'], 761 | }, 762 | output_schema: { 763 | type: 'object', 764 | properties: { 765 | issueId: { type: 'string' }, 766 | identifier: { type: 'string' }, 767 | history: { 768 | type: 'array', 769 | items: { 770 | type: 'object', 771 | properties: { 772 | id: { type: 'string' }, 773 | createdAt: { type: 'string' }, 774 | actor: { type: 'object' }, 775 | type: { type: 'string' }, 776 | from: { type: 'string' }, 777 | to: { type: 'string' }, 778 | }, 779 | }, 780 | }, 781 | }, 782 | }, 783 | }; 784 | 785 | /** 786 | * Tool definition for getting comments for an issue 787 | */ 788 | export const getCommentsToolDefinition: MCPToolDefinition = { 789 | name: 'linear_getComments', 790 | description: 'Get all comments for an issue', 791 | input_schema: { 792 | type: 'object', 793 | properties: { 794 | issueId: { 795 | type: 'string', 796 | description: 'ID or identifier of the issue to get comments from (e.g., ABC-123)', 797 | }, 798 | limit: { 799 | type: 'number', 800 | description: 'Maximum number of comments to return (default: 25)', 801 | }, 802 | }, 803 | required: ['issueId'], 804 | }, 805 | output_schema: { 806 | type: 'array', 807 | items: { 808 | type: 'object', 809 | properties: { 810 | id: { type: 'string' }, 811 | body: { type: 'string' }, 812 | createdAt: { type: 'string' }, 813 | user: { 814 | type: 'object', 815 | properties: { 816 | id: { type: 'string' }, 817 | name: { type: 'string' }, 818 | }, 819 | }, 820 | url: { type: 'string' }, 821 | }, 822 | }, 823 | }, 824 | }; 825 | -------------------------------------------------------------------------------- /src/tools/definitions/project-tools.ts: -------------------------------------------------------------------------------- 1 | import { MCPToolDefinition } from '../../types.js'; 2 | 3 | /** 4 | * Tool definition for getting projects 5 | */ 6 | export const getProjectsToolDefinition: MCPToolDefinition = { 7 | name: 'linear_getProjects', 8 | description: 'Get a list of projects from Linear', 9 | input_schema: { 10 | type: 'object', 11 | properties: {}, 12 | }, 13 | output_schema: { 14 | type: 'array', 15 | items: { 16 | type: 'object', 17 | properties: { 18 | id: { type: 'string' }, 19 | name: { type: 'string' }, 20 | description: { type: 'string' }, 21 | content: { type: 'string' }, 22 | state: { type: 'string' }, 23 | teams: { 24 | type: 'array', 25 | items: { 26 | type: 'object', 27 | properties: { 28 | id: { type: 'string' }, 29 | name: { type: 'string' }, 30 | }, 31 | }, 32 | }, 33 | url: { type: 'string' }, 34 | }, 35 | }, 36 | }, 37 | }; 38 | 39 | /** 40 | * Tool definition for creating a project 41 | */ 42 | export const createProjectToolDefinition: MCPToolDefinition = { 43 | name: 'linear_createProject', 44 | description: 'Create a new project in Linear', 45 | input_schema: { 46 | type: 'object', 47 | properties: { 48 | name: { 49 | type: 'string', 50 | description: 'Name of the project', 51 | }, 52 | description: { 53 | type: 'string', 54 | description: 'Short summary of the project', 55 | }, 56 | content: { 57 | type: 'string', 58 | description: 'Content of the project (Markdown supported)', 59 | }, 60 | teamIds: { 61 | type: 'array', 62 | items: { type: 'string' }, 63 | description: 'IDs of the teams this project belongs to', 64 | }, 65 | state: { 66 | type: 'string', 67 | description: 68 | "Initial state of the project (e.g., 'planned', 'started', 'paused', 'completed', 'canceled')", 69 | }, 70 | }, 71 | required: ['name', 'teamIds'], 72 | }, 73 | output_schema: { 74 | type: 'object', 75 | properties: { 76 | id: { type: 'string' }, 77 | name: { type: 'string' }, 78 | url: { type: 'string' }, 79 | }, 80 | }, 81 | }; 82 | 83 | /** 84 | * Tool definition for updating a project 85 | */ 86 | export const updateProjectToolDefinition: MCPToolDefinition = { 87 | name: 'linear_updateProject', 88 | description: 'Update an existing project in Linear', 89 | input_schema: { 90 | type: 'object', 91 | properties: { 92 | id: { 93 | type: 'string', 94 | description: 'ID of the project to update', 95 | }, 96 | name: { 97 | type: 'string', 98 | description: 'New name of the project', 99 | }, 100 | description: { 101 | type: 'string', 102 | description: 'New short summary of the project', 103 | }, 104 | content: { 105 | type: 'string', 106 | description: 'New content of the project (Markdown supported)', 107 | }, 108 | state: { 109 | type: 'string', 110 | description: 111 | "New state of the project (e.g., 'planned', 'started', 'paused', 'completed', 'canceled')", 112 | }, 113 | }, 114 | required: ['id'], 115 | }, 116 | output_schema: { 117 | type: 'object', 118 | properties: { 119 | id: { type: 'string' }, 120 | name: { type: 'string' }, 121 | description: { type: 'string' }, 122 | state: { type: 'string' }, 123 | url: { type: 'string' }, 124 | }, 125 | }, 126 | }; 127 | 128 | /** 129 | * Tool definition for adding an issue to a project 130 | */ 131 | export const addIssueToProjectToolDefinition: MCPToolDefinition = { 132 | name: 'linear_addIssueToProject', 133 | description: 'Add an existing issue to a project', 134 | input_schema: { 135 | type: 'object', 136 | properties: { 137 | issueId: { 138 | type: 'string', 139 | description: 'ID or identifier of the issue to add to the project', 140 | }, 141 | projectId: { 142 | type: 'string', 143 | description: 'ID of the project to add the issue to', 144 | }, 145 | }, 146 | required: ['issueId', 'projectId'], 147 | }, 148 | output_schema: { 149 | type: 'object', 150 | properties: { 151 | success: { type: 'boolean' }, 152 | issue: { 153 | type: 'object', 154 | properties: { 155 | id: { type: 'string' }, 156 | identifier: { type: 'string' }, 157 | title: { type: 'string' }, 158 | project: { 159 | type: 'object', 160 | properties: { 161 | id: { type: 'string' }, 162 | name: { type: 'string' }, 163 | }, 164 | }, 165 | }, 166 | }, 167 | }, 168 | }, 169 | }; 170 | 171 | /** 172 | * Tool definition for getting issues in a project 173 | */ 174 | export const getProjectIssuesToolDefinition: MCPToolDefinition = { 175 | name: 'linear_getProjectIssues', 176 | description: 'Get all issues associated with a project', 177 | input_schema: { 178 | type: 'object', 179 | properties: { 180 | projectId: { 181 | type: 'string', 182 | description: 'ID of the project to get issues for', 183 | }, 184 | limit: { 185 | type: 'number', 186 | description: 'Maximum number of issues to return (default: 25)', 187 | }, 188 | }, 189 | required: ['projectId'], 190 | }, 191 | output_schema: { 192 | type: 'array', 193 | items: { 194 | type: 'object', 195 | properties: { 196 | id: { type: 'string' }, 197 | identifier: { type: 'string' }, 198 | title: { type: 'string' }, 199 | description: { type: 'string' }, 200 | state: { type: 'string' }, 201 | priority: { type: 'number' }, 202 | team: { type: 'object' }, 203 | assignee: { type: 'object' }, 204 | url: { type: 'string' }, 205 | }, 206 | }, 207 | }, 208 | }; 209 | -------------------------------------------------------------------------------- /src/tools/definitions/team-tools.ts: -------------------------------------------------------------------------------- 1 | import { MCPToolDefinition } from '../../types.js'; 2 | 3 | /** 4 | * Tool definition for getting teams 5 | */ 6 | export const getTeamsToolDefinition: MCPToolDefinition = { 7 | name: 'linear_getTeams', 8 | description: 'Get a list of teams from Linear', 9 | input_schema: { 10 | type: 'object', 11 | properties: {}, 12 | }, 13 | output_schema: { 14 | type: 'array', 15 | items: { 16 | type: 'object', 17 | properties: { 18 | id: { type: 'string' }, 19 | name: { type: 'string' }, 20 | key: { type: 'string' }, 21 | description: { type: 'string' }, 22 | states: { 23 | type: 'array', 24 | items: { 25 | type: 'object', 26 | properties: { 27 | id: { type: 'string' }, 28 | name: { type: 'string' }, 29 | }, 30 | }, 31 | }, 32 | }, 33 | }, 34 | }, 35 | }; 36 | 37 | /** 38 | * Tool definition for getting workflow states for a team 39 | */ 40 | export const getWorkflowStatesToolDefinition: MCPToolDefinition = { 41 | name: 'linear_getWorkflowStates', 42 | description: 'Get workflow states for a team', 43 | input_schema: { 44 | type: 'object', 45 | properties: { 46 | teamId: { 47 | type: 'string', 48 | description: 'ID of the team to get workflow states for', 49 | }, 50 | includeArchived: { 51 | type: 'boolean', 52 | description: 'Whether to include archived states (default: false)', 53 | }, 54 | }, 55 | required: ['teamId'], 56 | }, 57 | output_schema: { 58 | type: 'array', 59 | items: { 60 | type: 'object', 61 | properties: { 62 | id: { type: 'string' }, 63 | name: { type: 'string' }, 64 | type: { type: 'string' }, 65 | position: { type: 'number' }, 66 | color: { type: 'string' }, 67 | description: { type: 'string' }, 68 | }, 69 | }, 70 | }, 71 | }; 72 | -------------------------------------------------------------------------------- /src/tools/definitions/user-tools.ts: -------------------------------------------------------------------------------- 1 | import { MCPToolDefinition } from '../../types.js'; 2 | 3 | /** 4 | * Tool definition for getting the current viewer's information 5 | */ 6 | export const getViewerToolDefinition: MCPToolDefinition = { 7 | name: 'linear_getViewer', 8 | description: 'Get information about the currently authenticated user', 9 | input_schema: { 10 | type: 'object', 11 | properties: {}, 12 | }, 13 | output_schema: { 14 | type: 'object', 15 | properties: { 16 | id: { type: 'string' }, 17 | name: { type: 'string' }, 18 | email: { type: 'string' }, 19 | active: { type: 'boolean' }, 20 | displayName: { type: 'string' }, 21 | organization: { 22 | type: 'object', 23 | properties: { 24 | id: { type: 'string' }, 25 | name: { type: 'string' }, 26 | }, 27 | }, 28 | }, 29 | }, 30 | }; 31 | 32 | /** 33 | * Tool definition for getting organization information 34 | */ 35 | export const getOrganizationToolDefinition: MCPToolDefinition = { 36 | name: 'linear_getOrganization', 37 | description: 'Get information about the current Linear organization', 38 | input_schema: { 39 | type: 'object', 40 | properties: {}, 41 | }, 42 | output_schema: { 43 | type: 'object', 44 | properties: { 45 | id: { type: 'string' }, 46 | name: { type: 'string' }, 47 | urlKey: { type: 'string' }, 48 | logoUrl: { type: 'string' }, 49 | }, 50 | }, 51 | }; 52 | 53 | /** 54 | * Tool definition for getting users 55 | */ 56 | export const getUsersToolDefinition: MCPToolDefinition = { 57 | name: 'linear_getUsers', 58 | description: 'Get a list of users in the Linear organization', 59 | input_schema: { 60 | type: 'object', 61 | properties: {}, 62 | }, 63 | output_schema: { 64 | type: 'array', 65 | items: { 66 | type: 'object', 67 | properties: { 68 | id: { type: 'string' }, 69 | name: { type: 'string' }, 70 | email: { type: 'string' }, 71 | displayName: { type: 'string' }, 72 | active: { type: 'boolean' }, 73 | }, 74 | }, 75 | }, 76 | }; 77 | 78 | /** 79 | * Tool definition for getting labels 80 | */ 81 | export const getLabelsToolDefinition: MCPToolDefinition = { 82 | name: 'linear_getLabels', 83 | description: 'Get a list of issue labels from Linear', 84 | input_schema: { 85 | type: 'object', 86 | properties: {}, 87 | }, 88 | output_schema: { 89 | type: 'array', 90 | items: { 91 | type: 'object', 92 | properties: { 93 | id: { type: 'string' }, 94 | name: { type: 'string' }, 95 | description: { type: 'string' }, 96 | color: { type: 'string' }, 97 | team: { 98 | type: 'object', 99 | properties: { 100 | id: { type: 'string' }, 101 | name: { type: 'string' }, 102 | }, 103 | }, 104 | }, 105 | }, 106 | }, 107 | }; 108 | -------------------------------------------------------------------------------- /src/tools/handlers/cycle-handlers.ts: -------------------------------------------------------------------------------- 1 | import { isAddIssueToCycleArgs, isGetActiveCycleArgs, isGetCyclesArgs } from '../type-guards.js'; 2 | import { LinearService } from '../../services/linear-service.js'; 3 | import { logError } from '../../utils/config.js'; 4 | 5 | /** 6 | * Handler for getting all cycles 7 | */ 8 | export function handleGetCycles(linearService: LinearService) { 9 | return async (args: unknown) => { 10 | try { 11 | if (!isGetCyclesArgs(args)) { 12 | throw new Error('Invalid arguments for getCycles'); 13 | } 14 | 15 | return await linearService.getCycles(args.teamId, args.limit); 16 | } catch (error) { 17 | logError('Error getting cycles', error); 18 | throw error; 19 | } 20 | }; 21 | } 22 | 23 | /** 24 | * Handler for getting the active cycle for a team 25 | */ 26 | export function handleGetActiveCycle(linearService: LinearService) { 27 | return async (args: unknown) => { 28 | try { 29 | if (!isGetActiveCycleArgs(args)) { 30 | throw new Error('Invalid arguments for getActiveCycle'); 31 | } 32 | 33 | return await linearService.getActiveCycle(args.teamId); 34 | } catch (error) { 35 | logError('Error getting active cycle', error); 36 | throw error; 37 | } 38 | }; 39 | } 40 | 41 | /** 42 | * Handler for adding an issue to a cycle 43 | */ 44 | export function handleAddIssueToCycle(linearService: LinearService) { 45 | return async (args: unknown) => { 46 | try { 47 | if (!isAddIssueToCycleArgs(args)) { 48 | throw new Error('Invalid arguments for addIssueToCycle'); 49 | } 50 | 51 | return await linearService.addIssueToCycle(args.issueId, args.cycleId); 52 | } catch (error) { 53 | logError('Error adding issue to cycle', error); 54 | throw error; 55 | } 56 | }; 57 | } 58 | -------------------------------------------------------------------------------- /src/tools/handlers/index.ts: -------------------------------------------------------------------------------- 1 | import { LinearService } from '../../services/linear-service.js'; 2 | import { 3 | handleGetIssues, 4 | handleGetIssueById, 5 | handleSearchIssues, 6 | handleCreateIssue, 7 | handleUpdateIssue, 8 | handleCreateComment, 9 | handleAddIssueLabel, 10 | handleRemoveIssueLabel, 11 | // New Issue Management handlers 12 | handleAssignIssue, 13 | handleSubscribeToIssue, 14 | handleConvertIssueToSubtask, 15 | handleCreateIssueRelation, 16 | handleArchiveIssue, 17 | handleSetIssuePriority, 18 | handleTransferIssue, 19 | handleDuplicateIssue, 20 | handleGetIssueHistory, 21 | // Comment Management handlers 22 | handleGetComments, 23 | } from './issue-handlers.js'; 24 | import { 25 | handleGetProjects, 26 | handleCreateProject, 27 | // Project Management handlers 28 | handleUpdateProject, 29 | handleAddIssueToProject, 30 | handleGetProjectIssues, 31 | } from './project-handlers.js'; 32 | import { handleGetTeams, handleGetWorkflowStates } from './team-handlers.js'; 33 | import { 34 | handleGetViewer, 35 | handleGetOrganization, 36 | handleGetUsers, 37 | handleGetLabels, 38 | } from './user-handlers.js'; 39 | import { 40 | // Cycle Management handlers 41 | handleGetCycles, 42 | handleGetActiveCycle, 43 | handleAddIssueToCycle, 44 | } from './cycle-handlers.js'; 45 | 46 | /** 47 | * Registers all tool handlers for the MCP Linear 48 | * @param linearService The Linear service instance 49 | * @returns A map of tool name to handler function 50 | */ 51 | export function registerToolHandlers(linearService: LinearService) { 52 | return { 53 | // User tools 54 | linear_getViewer: handleGetViewer(linearService), 55 | linear_getOrganization: handleGetOrganization(linearService), 56 | linear_getUsers: handleGetUsers(linearService), 57 | linear_getLabels: handleGetLabels(linearService), 58 | 59 | // Team tools 60 | linear_getTeams: handleGetTeams(linearService), 61 | linear_getWorkflowStates: handleGetWorkflowStates(linearService), 62 | 63 | // Project tools 64 | linear_getProjects: handleGetProjects(linearService), 65 | linear_createProject: handleCreateProject(linearService), 66 | 67 | // Project Management tools 68 | linear_updateProject: handleUpdateProject(linearService), 69 | linear_addIssueToProject: handleAddIssueToProject(linearService), 70 | linear_getProjectIssues: handleGetProjectIssues(linearService), 71 | 72 | // Cycle Management tools 73 | linear_getCycles: handleGetCycles(linearService), 74 | linear_getActiveCycle: handleGetActiveCycle(linearService), 75 | linear_addIssueToCycle: handleAddIssueToCycle(linearService), 76 | 77 | // Issue tools 78 | linear_getIssues: handleGetIssues(linearService), 79 | linear_getIssueById: handleGetIssueById(linearService), 80 | linear_searchIssues: handleSearchIssues(linearService), 81 | linear_createIssue: handleCreateIssue(linearService), 82 | linear_updateIssue: handleUpdateIssue(linearService), 83 | linear_createComment: handleCreateComment(linearService), 84 | linear_addIssueLabel: handleAddIssueLabel(linearService), 85 | linear_removeIssueLabel: handleRemoveIssueLabel(linearService), 86 | 87 | // New Issue Management tools 88 | linear_assignIssue: handleAssignIssue(linearService), 89 | linear_subscribeToIssue: handleSubscribeToIssue(linearService), 90 | linear_convertIssueToSubtask: handleConvertIssueToSubtask(linearService), 91 | linear_createIssueRelation: handleCreateIssueRelation(linearService), 92 | linear_archiveIssue: handleArchiveIssue(linearService), 93 | linear_setIssuePriority: handleSetIssuePriority(linearService), 94 | linear_transferIssue: handleTransferIssue(linearService), 95 | linear_duplicateIssue: handleDuplicateIssue(linearService), 96 | linear_getIssueHistory: handleGetIssueHistory(linearService), 97 | 98 | // Comment Management tools 99 | linear_getComments: handleGetComments(linearService), 100 | }; 101 | } 102 | 103 | // Export all handlers individually 104 | export { 105 | handleGetIssues, 106 | handleGetIssueById, 107 | handleSearchIssues, 108 | handleCreateIssue, 109 | handleUpdateIssue, 110 | handleCreateComment, 111 | handleAddIssueLabel, 112 | handleRemoveIssueLabel, 113 | handleGetProjects, 114 | handleCreateProject, 115 | handleGetTeams, 116 | handleGetWorkflowStates, 117 | handleGetViewer, 118 | handleGetOrganization, 119 | handleGetUsers, 120 | handleGetLabels, 121 | 122 | // New Issue Management handlers 123 | handleAssignIssue, 124 | handleSubscribeToIssue, 125 | handleConvertIssueToSubtask, 126 | handleCreateIssueRelation, 127 | handleArchiveIssue, 128 | handleSetIssuePriority, 129 | handleTransferIssue, 130 | handleDuplicateIssue, 131 | handleGetIssueHistory, 132 | 133 | // Comment Management handlers 134 | handleGetComments, 135 | 136 | // Project Management handlers 137 | handleUpdateProject, 138 | handleAddIssueToProject, 139 | handleGetProjectIssues, 140 | 141 | // Cycle Management handlers 142 | handleGetCycles, 143 | handleGetActiveCycle, 144 | handleAddIssueToCycle, 145 | }; 146 | -------------------------------------------------------------------------------- /src/tools/handlers/issue-handlers.ts: -------------------------------------------------------------------------------- 1 | import { 2 | isAddIssueLabelArgs, 3 | isArchiveIssueArgs, 4 | isAssignIssueArgs, 5 | isConvertIssueToSubtaskArgs, 6 | isCreateCommentArgs, 7 | isCreateIssueArgs, 8 | isCreateIssueRelationArgs, 9 | isDuplicateIssueArgs, 10 | isGetCommentsArgs, 11 | isGetIssueByIdArgs, 12 | isGetIssueHistoryArgs, 13 | isGetIssuesArgs, 14 | isRemoveIssueLabelArgs, 15 | isSearchIssuesArgs, 16 | isSetIssuePriorityArgs, 17 | isSubscribeToIssueArgs, 18 | isTransferIssueArgs, 19 | isUpdateIssueArgs, 20 | } from '../type-guards.js'; 21 | import { LinearService } from '../../services/linear-service.js'; 22 | import { logError } from '../../utils/config.js'; 23 | 24 | /** 25 | * Handler for getting issues 26 | */ 27 | export function handleGetIssues(linearService: LinearService) { 28 | return async (args: unknown) => { 29 | try { 30 | if (!isGetIssuesArgs(args)) { 31 | throw new Error('Invalid arguments for getIssues'); 32 | } 33 | 34 | return await linearService.getIssues(args.limit); 35 | } catch (error) { 36 | logError('Error getting issues', error); 37 | throw error; 38 | } 39 | }; 40 | } 41 | 42 | /** 43 | * Handler for getting issue by ID 44 | */ 45 | export function handleGetIssueById(linearService: LinearService) { 46 | return async (args: unknown) => { 47 | try { 48 | if (!isGetIssueByIdArgs(args)) { 49 | throw new Error('Invalid arguments for getIssueById'); 50 | } 51 | 52 | return await linearService.getIssueById(args.id); 53 | } catch (error) { 54 | logError('Error getting issue by ID', error); 55 | throw error; 56 | } 57 | }; 58 | } 59 | 60 | /** 61 | * Handler for searching issues 62 | */ 63 | export function handleSearchIssues(linearService: LinearService) { 64 | return async (args: unknown) => { 65 | try { 66 | console.log('searchIssues args:', JSON.stringify(args, null, 2)); 67 | 68 | if (!isSearchIssuesArgs(args)) { 69 | console.error('Invalid arguments for searchIssues'); 70 | throw new Error('Invalid arguments for searchIssues'); 71 | } 72 | 73 | console.log('Arguments validated successfully'); 74 | return await linearService.searchIssues(args); 75 | } catch (error) { 76 | logError('Error searching issues', error); 77 | throw error; 78 | } 79 | }; 80 | } 81 | 82 | /** 83 | * Handler for creating an issue 84 | */ 85 | export function handleCreateIssue(linearService: LinearService) { 86 | return async (args: unknown) => { 87 | try { 88 | if (!isCreateIssueArgs(args)) { 89 | throw new Error('Invalid arguments for createIssue'); 90 | } 91 | 92 | return await linearService.createIssue(args); 93 | } catch (error) { 94 | logError('Error creating issue', error); 95 | throw error; 96 | } 97 | }; 98 | } 99 | 100 | /** 101 | * Handler for updating an issue 102 | */ 103 | export function handleUpdateIssue(linearService: LinearService) { 104 | return async (args: unknown) => { 105 | try { 106 | if (!isUpdateIssueArgs(args)) { 107 | throw new Error('Invalid arguments for updateIssue'); 108 | } 109 | 110 | return await linearService.updateIssue(args); 111 | } catch (error) { 112 | logError('Error updating issue', error); 113 | throw error; 114 | } 115 | }; 116 | } 117 | 118 | /** 119 | * Handler for creating a comment 120 | */ 121 | export function handleCreateComment(linearService: LinearService) { 122 | return async (args: unknown) => { 123 | try { 124 | if (!isCreateCommentArgs(args)) { 125 | throw new Error('Invalid arguments for createComment'); 126 | } 127 | 128 | return await linearService.createComment(args); 129 | } catch (error) { 130 | logError('Error creating comment', error); 131 | throw error; 132 | } 133 | }; 134 | } 135 | 136 | /** 137 | * Handler for adding a label to an issue 138 | */ 139 | export function handleAddIssueLabel(linearService: LinearService) { 140 | return async (args: unknown) => { 141 | try { 142 | if (!isAddIssueLabelArgs(args)) { 143 | throw new Error('Invalid arguments for addIssueLabel'); 144 | } 145 | 146 | return await linearService.addIssueLabel(args.issueId, args.labelId); 147 | } catch (error) { 148 | logError('Error adding label to issue', error); 149 | throw error; 150 | } 151 | }; 152 | } 153 | 154 | /** 155 | * Handler for removing a label from an issue 156 | */ 157 | export function handleRemoveIssueLabel(linearService: LinearService) { 158 | return async (args: unknown) => { 159 | try { 160 | if (!isRemoveIssueLabelArgs(args)) { 161 | throw new Error('Invalid arguments for removeIssueLabel'); 162 | } 163 | 164 | return await linearService.removeIssueLabel(args.issueId, args.labelId); 165 | } catch (error) { 166 | logError('Error removing label from issue', error); 167 | throw error; 168 | } 169 | }; 170 | } 171 | 172 | /** 173 | * Handler for assigning an issue to a user 174 | */ 175 | export function handleAssignIssue(linearService: LinearService) { 176 | return async (args: unknown) => { 177 | try { 178 | if (!isAssignIssueArgs(args)) { 179 | throw new Error('Invalid arguments for assignIssue'); 180 | } 181 | 182 | return await linearService.assignIssue(args.issueId, args.assigneeId); 183 | } catch (error) { 184 | logError('Error assigning issue', error); 185 | throw error; 186 | } 187 | }; 188 | } 189 | 190 | /** 191 | * Handler for subscribing to issue updates 192 | */ 193 | export function handleSubscribeToIssue(linearService: LinearService) { 194 | return async (args: unknown) => { 195 | try { 196 | if (!isSubscribeToIssueArgs(args)) { 197 | throw new Error('Invalid arguments for subscribeToIssue'); 198 | } 199 | 200 | return await linearService.subscribeToIssue(args.issueId); 201 | } catch (error) { 202 | logError('Error subscribing to issue', error); 203 | throw error; 204 | } 205 | }; 206 | } 207 | 208 | /** 209 | * Handler for converting an issue to a subtask 210 | */ 211 | export function handleConvertIssueToSubtask(linearService: LinearService) { 212 | return async (args: unknown) => { 213 | try { 214 | if (!isConvertIssueToSubtaskArgs(args)) { 215 | throw new Error('Invalid arguments for convertIssueToSubtask'); 216 | } 217 | 218 | return await linearService.convertIssueToSubtask(args.issueId, args.parentIssueId); 219 | } catch (error) { 220 | logError('Error converting issue to subtask', error); 221 | throw error; 222 | } 223 | }; 224 | } 225 | 226 | /** 227 | * Handler for creating an issue relation 228 | */ 229 | export function handleCreateIssueRelation(linearService: LinearService) { 230 | return async (args: unknown) => { 231 | try { 232 | if (!isCreateIssueRelationArgs(args)) { 233 | throw new Error('Invalid arguments for createIssueRelation'); 234 | } 235 | 236 | return await linearService.createIssueRelation(args.issueId, args.relatedIssueId, args.type); 237 | } catch (error) { 238 | logError('Error creating issue relation', error); 239 | throw error; 240 | } 241 | }; 242 | } 243 | 244 | /** 245 | * Handler for archiving an issue 246 | */ 247 | export function handleArchiveIssue(linearService: LinearService) { 248 | return async (args: unknown) => { 249 | try { 250 | if (!isArchiveIssueArgs(args)) { 251 | throw new Error('Invalid arguments for archiveIssue'); 252 | } 253 | 254 | return await linearService.archiveIssue(args.issueId); 255 | } catch (error) { 256 | logError('Error archiving issue', error); 257 | throw error; 258 | } 259 | }; 260 | } 261 | 262 | /** 263 | * Handler for setting issue priority 264 | */ 265 | export function handleSetIssuePriority(linearService: LinearService) { 266 | return async (args: unknown) => { 267 | try { 268 | if (!isSetIssuePriorityArgs(args)) { 269 | throw new Error('Invalid arguments for setIssuePriority'); 270 | } 271 | 272 | return await linearService.setIssuePriority(args.issueId, args.priority); 273 | } catch (error) { 274 | logError('Error setting issue priority', error); 275 | throw error; 276 | } 277 | }; 278 | } 279 | 280 | /** 281 | * Handler for transferring an issue 282 | */ 283 | export function handleTransferIssue(linearService: LinearService) { 284 | return async (args: unknown) => { 285 | try { 286 | if (!isTransferIssueArgs(args)) { 287 | throw new Error('Invalid arguments for transferIssue'); 288 | } 289 | 290 | return await linearService.transferIssue(args.issueId, args.teamId); 291 | } catch (error) { 292 | logError('Error transferring issue', error); 293 | throw error; 294 | } 295 | }; 296 | } 297 | 298 | /** 299 | * Handler for duplicating an issue 300 | */ 301 | export function handleDuplicateIssue(linearService: LinearService) { 302 | return async (args: unknown) => { 303 | try { 304 | if (!isDuplicateIssueArgs(args)) { 305 | throw new Error('Invalid arguments for duplicateIssue'); 306 | } 307 | 308 | return await linearService.duplicateIssue(args.issueId); 309 | } catch (error) { 310 | logError('Error duplicating issue', error); 311 | throw error; 312 | } 313 | }; 314 | } 315 | 316 | /** 317 | * Handler for getting issue history 318 | */ 319 | export function handleGetIssueHistory(linearService: LinearService) { 320 | return async (args: unknown) => { 321 | try { 322 | if (!isGetIssueHistoryArgs(args)) { 323 | throw new Error('Invalid arguments for getIssueHistory'); 324 | } 325 | 326 | return await linearService.getIssueHistory(args.issueId, args.limit); 327 | } catch (error) { 328 | logError('Error getting issue history', error); 329 | throw error; 330 | } 331 | }; 332 | } 333 | 334 | /** 335 | * Handler for getting comments for an issue 336 | */ 337 | export function handleGetComments(linearService: LinearService) { 338 | return async (args: unknown) => { 339 | try { 340 | if (!isGetCommentsArgs(args)) { 341 | throw new Error('Invalid arguments for getComments'); 342 | } 343 | 344 | return await linearService.getComments(args.issueId, args.limit); 345 | } catch (error) { 346 | logError('Error getting comments for issue', error); 347 | throw error; 348 | } 349 | }; 350 | } 351 | -------------------------------------------------------------------------------- /src/tools/handlers/project-handlers.ts: -------------------------------------------------------------------------------- 1 | import { 2 | isAddIssueToProjectArgs, 3 | isCreateProjectArgs, 4 | isGetProjectIssuesArgs, 5 | isUpdateProjectArgs, 6 | } from '../type-guards.js'; 7 | import { LinearService } from '../../services/linear-service.js'; 8 | import { logError } from '../../utils/config.js'; 9 | 10 | /** 11 | * Handler for getting projects 12 | */ 13 | export function handleGetProjects(linearService: LinearService) { 14 | return async (args: unknown) => { 15 | try { 16 | return await linearService.getProjects(); 17 | } catch (error) { 18 | logError('Error getting projects', error); 19 | throw error; 20 | } 21 | }; 22 | } 23 | 24 | /** 25 | * Handler for creating a project 26 | */ 27 | export function handleCreateProject(linearService: LinearService) { 28 | return async (args: unknown) => { 29 | try { 30 | if (!isCreateProjectArgs(args)) { 31 | throw new Error('Invalid arguments for createProject'); 32 | } 33 | 34 | return await linearService.createProject(args); 35 | } catch (error) { 36 | logError('Error creating project', error); 37 | throw error; 38 | } 39 | }; 40 | } 41 | 42 | /** 43 | * Handler for updating a project 44 | */ 45 | export function handleUpdateProject(linearService: LinearService) { 46 | return async (args: unknown) => { 47 | try { 48 | if (!isUpdateProjectArgs(args)) { 49 | throw new Error('Invalid arguments for updateProject'); 50 | } 51 | 52 | return await linearService.updateProject(args); 53 | } catch (error) { 54 | logError('Error updating project', error); 55 | throw error; 56 | } 57 | }; 58 | } 59 | 60 | /** 61 | * Handler for adding an issue to a project 62 | */ 63 | export function handleAddIssueToProject(linearService: LinearService) { 64 | return async (args: unknown) => { 65 | try { 66 | if (!isAddIssueToProjectArgs(args)) { 67 | throw new Error('Invalid arguments for addIssueToProject'); 68 | } 69 | 70 | return await linearService.addIssueToProject(args.issueId, args.projectId); 71 | } catch (error) { 72 | logError('Error adding issue to project', error); 73 | throw error; 74 | } 75 | }; 76 | } 77 | 78 | /** 79 | * Handler for getting issues in a project 80 | */ 81 | export function handleGetProjectIssues(linearService: LinearService) { 82 | return async (args: unknown) => { 83 | try { 84 | if (!isGetProjectIssuesArgs(args)) { 85 | throw new Error('Invalid arguments for getProjectIssues'); 86 | } 87 | 88 | return await linearService.getProjectIssues(args.projectId, args.limit); 89 | } catch (error) { 90 | logError('Error getting project issues', error); 91 | throw error; 92 | } 93 | }; 94 | } 95 | -------------------------------------------------------------------------------- /src/tools/handlers/team-handlers.ts: -------------------------------------------------------------------------------- 1 | import { LinearService } from '../../services/linear-service.js'; 2 | import { logError } from '../../utils/config.js'; 3 | import { isGetWorkflowStatesArgs } from '../type-guards.js'; 4 | 5 | /** 6 | * Handler for getting teams 7 | */ 8 | export function handleGetTeams(linearService: LinearService) { 9 | return async (args: unknown) => { 10 | try { 11 | return await linearService.getTeams(); 12 | } catch (error) { 13 | logError('Error getting teams', error); 14 | throw error; 15 | } 16 | }; 17 | } 18 | 19 | /** 20 | * Handler for getting workflow states for a team 21 | */ 22 | export function handleGetWorkflowStates(linearService: LinearService) { 23 | return async (args: unknown) => { 24 | try { 25 | if (!isGetWorkflowStatesArgs(args)) { 26 | throw new Error('Invalid arguments for getWorkflowStates'); 27 | } 28 | 29 | return await linearService.getWorkflowStates(args.teamId, args.includeArchived || false); 30 | } catch (error) { 31 | logError('Error getting workflow states', error); 32 | throw error; 33 | } 34 | }; 35 | } 36 | -------------------------------------------------------------------------------- /src/tools/handlers/user-handlers.ts: -------------------------------------------------------------------------------- 1 | import { LinearService } from '../../services/linear-service.js'; 2 | import { logError } from '../../utils/config.js'; 3 | 4 | /** 5 | * Handler for getting viewer information 6 | */ 7 | export function handleGetViewer(linearService: LinearService) { 8 | return async (args: unknown) => { 9 | try { 10 | return await linearService.getUserInfo(); 11 | } catch (error) { 12 | logError('Error getting viewer information', error); 13 | throw error; 14 | } 15 | }; 16 | } 17 | 18 | /** 19 | * Handler for getting organization information 20 | */ 21 | export function handleGetOrganization(linearService: LinearService) { 22 | return async (args: unknown) => { 23 | try { 24 | return await linearService.getOrganizationInfo(); 25 | } catch (error) { 26 | logError('Error getting organization information', error); 27 | throw error; 28 | } 29 | }; 30 | } 31 | 32 | /** 33 | * Handler for getting users 34 | */ 35 | export function handleGetUsers(linearService: LinearService) { 36 | return async (args: unknown) => { 37 | try { 38 | return await linearService.getAllUsers(); 39 | } catch (error) { 40 | logError('Error getting users', error); 41 | throw error; 42 | } 43 | }; 44 | } 45 | 46 | /** 47 | * Handler for getting labels 48 | */ 49 | export function handleGetLabels(linearService: LinearService) { 50 | return async (args: unknown) => { 51 | try { 52 | return await linearService.getLabels(); 53 | } catch (error) { 54 | logError('Error getting labels', error); 55 | throw error; 56 | } 57 | }; 58 | } 59 | -------------------------------------------------------------------------------- /src/tools/type-guards.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Type guard for linear_getIssues tool arguments 3 | */ 4 | export function isGetIssuesArgs(args: unknown): args is { limit?: number } { 5 | return ( 6 | typeof args === 'object' && 7 | args !== null && 8 | (!('limit' in args) || typeof (args as { limit: number }).limit === 'number') 9 | ); 10 | } 11 | 12 | /** 13 | * Type guard for linear_getIssueById tool arguments 14 | */ 15 | export function isGetIssueByIdArgs(args: unknown): args is { id: string } { 16 | return ( 17 | typeof args === 'object' && 18 | args !== null && 19 | 'id' in args && 20 | typeof (args as { id: string }).id === 'string' 21 | ); 22 | } 23 | 24 | /** 25 | * Type guard for linear_searchIssues tool arguments 26 | */ 27 | export function isSearchIssuesArgs(args: unknown): args is { 28 | query?: string; 29 | teamId?: string; 30 | assigneeId?: string; 31 | projectId?: string; 32 | states?: string[]; 33 | limit?: number; 34 | } { 35 | // Check if args is an object 36 | if (typeof args !== 'object' || args === null) { 37 | console.error('searchIssues args is not an object or is null'); 38 | return false; 39 | } 40 | 41 | // Check query 42 | if ('query' in args && typeof (args as { query: unknown }).query !== 'string') { 43 | console.error('searchIssues query is not a string'); 44 | return false; 45 | } 46 | 47 | // Check teamId 48 | if ('teamId' in args && typeof (args as { teamId: unknown }).teamId !== 'string') { 49 | console.error('searchIssues teamId is not a string'); 50 | return false; 51 | } 52 | 53 | // Check assigneeId 54 | if ('assigneeId' in args && typeof (args as { assigneeId: unknown }).assigneeId !== 'string') { 55 | console.error('searchIssues assigneeId is not a string'); 56 | return false; 57 | } 58 | 59 | // Check projectId 60 | if ('projectId' in args && typeof (args as { projectId: unknown }).projectId !== 'string') { 61 | console.error('searchIssues projectId is not a string'); 62 | return false; 63 | } 64 | 65 | // Check states 66 | if ('states' in args) { 67 | const states = (args as { states: unknown }).states; 68 | if (!Array.isArray(states)) { 69 | console.error('searchIssues states is not an array'); 70 | return false; 71 | } 72 | 73 | // Check that all elements in the array are strings 74 | for (let i = 0; i < states.length; i++) { 75 | if (typeof states[i] !== 'string') { 76 | console.error(`searchIssues states[${i}] is not a string`); 77 | return false; 78 | } 79 | } 80 | } 81 | 82 | // Check limit 83 | if ('limit' in args && typeof (args as { limit: unknown }).limit !== 'number') { 84 | console.error('searchIssues limit is not a number'); 85 | return false; 86 | } 87 | 88 | return true; 89 | } 90 | 91 | /** 92 | * Type guard for linear_createIssue tool arguments 93 | */ 94 | export function isCreateIssueArgs(args: unknown): args is { 95 | title: string; 96 | description?: string; 97 | teamId: string; 98 | assigneeId?: string; 99 | priority?: number; 100 | projectId?: string; 101 | cycleId?: string; 102 | estimate?: number; 103 | dueDate?: string; 104 | labelIds?: string[]; 105 | parentId?: string; 106 | subscriberIds?: string[]; 107 | stateId?: string; 108 | templateId?: string; 109 | sortOrder?: number; 110 | } { 111 | return ( 112 | typeof args === 'object' && 113 | args !== null && 114 | 'title' in args && 115 | typeof (args as { title: string }).title === 'string' && 116 | 'teamId' in args && 117 | typeof (args as { teamId: string }).teamId === 'string' && 118 | (!('assigneeId' in args) || typeof (args as { assigneeId: string }).assigneeId === 'string') && 119 | (!('priority' in args) || typeof (args as { priority: number }).priority === 'number') && 120 | (!('projectId' in args) || typeof (args as { projectId: string }).projectId === 'string') && 121 | (!('cycleId' in args) || typeof (args as { cycleId: string }).cycleId === 'string') && 122 | (!('estimate' in args) || typeof (args as { estimate: number }).estimate === 'number') && 123 | (!('dueDate' in args) || typeof (args as { dueDate: string }).dueDate === 'string') && 124 | (!('labelIds' in args) || Array.isArray((args as { labelIds: string[] }).labelIds)) && 125 | (!('parentId' in args) || typeof (args as { parentId: string }).parentId === 'string') && 126 | (!('subscriberIds' in args) || 127 | Array.isArray((args as { subscriberIds: string[] }).subscriberIds)) && 128 | (!('stateId' in args) || typeof (args as { stateId: string }).stateId === 'string') && 129 | (!('templateId' in args) || typeof (args as { templateId: string }).templateId === 'string') && 130 | (!('sortOrder' in args) || typeof (args as { sortOrder: number }).sortOrder === 'number') 131 | ); 132 | } 133 | 134 | /** 135 | * Type guard for linear_updateIssue tool arguments 136 | */ 137 | export function isUpdateIssueArgs(args: unknown): args is { 138 | id: string; 139 | title?: string; 140 | description?: string; 141 | stateId?: string; 142 | priority?: number; 143 | projectId?: string; 144 | assigneeId?: string; 145 | cycleId?: string; 146 | estimate?: number; 147 | dueDate?: string; 148 | labelIds?: string[]; 149 | addedLabelIds?: string[]; 150 | removedLabelIds?: string[]; 151 | parentId?: string; 152 | subscriberIds?: string[]; 153 | teamId?: string; 154 | sortOrder?: number; 155 | } { 156 | return ( 157 | typeof args === 'object' && 158 | args !== null && 159 | 'id' in args && 160 | typeof (args as { id: string }).id === 'string' && 161 | (!('title' in args) || typeof (args as { title: string }).title === 'string') && 162 | (!('description' in args) || 163 | typeof (args as { description: string }).description === 'string') && 164 | (!('stateId' in args) || typeof (args as { stateId: string }).stateId === 'string') && 165 | (!('priority' in args) || typeof (args as { priority: number }).priority === 'number') && 166 | (!('projectId' in args) || typeof (args as { projectId: string }).projectId === 'string') && 167 | (!('assigneeId' in args) || typeof (args as { assigneeId: string }).assigneeId === 'string') && 168 | (!('cycleId' in args) || typeof (args as { cycleId: string }).cycleId === 'string') && 169 | (!('estimate' in args) || typeof (args as { estimate: number }).estimate === 'number') && 170 | (!('dueDate' in args) || typeof (args as { dueDate: string }).dueDate === 'string') && 171 | (!('labelIds' in args) || Array.isArray((args as { labelIds: string[] }).labelIds)) && 172 | (!('addedLabelIds' in args) || 173 | Array.isArray((args as { addedLabelIds: string[] }).addedLabelIds)) && 174 | (!('removedLabelIds' in args) || 175 | Array.isArray((args as { removedLabelIds: string[] }).removedLabelIds)) && 176 | (!('parentId' in args) || typeof (args as { parentId: string }).parentId === 'string') && 177 | (!('subscriberIds' in args) || 178 | Array.isArray((args as { subscriberIds: string[] }).subscriberIds)) && 179 | (!('teamId' in args) || typeof (args as { teamId: string }).teamId === 'string') && 180 | (!('sortOrder' in args) || typeof (args as { sortOrder: number }).sortOrder === 'number') 181 | ); 182 | } 183 | 184 | /** 185 | * Type guard for linear_createComment tool arguments 186 | */ 187 | export function isCreateCommentArgs(args: unknown): args is { 188 | issueId: string; 189 | body: string; 190 | } { 191 | return ( 192 | typeof args === 'object' && 193 | args !== null && 194 | 'issueId' in args && 195 | typeof (args as { issueId: string }).issueId === 'string' && 196 | 'body' in args && 197 | typeof (args as { body: string }).body === 'string' 198 | ); 199 | } 200 | 201 | /** 202 | * Type guard for linear_createProject tool arguments 203 | */ 204 | export function isCreateProjectArgs(args: unknown): args is { 205 | name: string; 206 | description?: string; 207 | content?: string; 208 | teamIds: string[]; 209 | state?: string; 210 | } { 211 | return ( 212 | typeof args === 'object' && 213 | args !== null && 214 | 'name' in args && 215 | typeof (args as { name: string }).name === 'string' && 216 | 'teamIds' in args && 217 | Array.isArray((args as { teamIds: string[] }).teamIds) 218 | ); 219 | } 220 | 221 | /** 222 | * Type guard for linear_addIssueLabel tool arguments 223 | */ 224 | export function isAddIssueLabelArgs(args: unknown): args is { 225 | issueId: string; 226 | labelId: string; 227 | } { 228 | return ( 229 | typeof args === 'object' && 230 | args !== null && 231 | 'issueId' in args && 232 | typeof (args as { issueId: string }).issueId === 'string' && 233 | 'labelId' in args && 234 | typeof (args as { labelId: string }).labelId === 'string' 235 | ); 236 | } 237 | 238 | /** 239 | * Type guard for linear_removeIssueLabel tool arguments 240 | */ 241 | export function isRemoveIssueLabelArgs(args: unknown): args is { 242 | issueId: string; 243 | labelId: string; 244 | } { 245 | return ( 246 | typeof args === 'object' && 247 | args !== null && 248 | 'issueId' in args && 249 | typeof (args as { issueId: string }).issueId === 'string' && 250 | 'labelId' in args && 251 | typeof (args as { labelId: string }).labelId === 'string' 252 | ); 253 | } 254 | 255 | /** 256 | * Type guard for linear_assignIssue tool arguments 257 | */ 258 | export function isAssignIssueArgs(args: unknown): args is { 259 | issueId: string; 260 | assigneeId: string; 261 | } { 262 | return ( 263 | typeof args === 'object' && 264 | args !== null && 265 | 'issueId' in args && 266 | typeof (args as { issueId: string }).issueId === 'string' && 267 | 'assigneeId' in args && 268 | typeof (args as { assigneeId: string }).assigneeId === 'string' 269 | ); 270 | } 271 | 272 | /** 273 | * Type guard for linear_subscribeToIssue tool arguments 274 | */ 275 | export function isSubscribeToIssueArgs(args: unknown): args is { 276 | issueId: string; 277 | } { 278 | return ( 279 | typeof args === 'object' && 280 | args !== null && 281 | 'issueId' in args && 282 | typeof (args as { issueId: string }).issueId === 'string' 283 | ); 284 | } 285 | 286 | /** 287 | * Type guard for linear_convertIssueToSubtask tool arguments 288 | */ 289 | export function isConvertIssueToSubtaskArgs(args: unknown): args is { 290 | issueId: string; 291 | parentIssueId: string; 292 | } { 293 | return ( 294 | typeof args === 'object' && 295 | args !== null && 296 | 'issueId' in args && 297 | typeof (args as { issueId: string }).issueId === 'string' && 298 | 'parentIssueId' in args && 299 | typeof (args as { parentIssueId: string }).parentIssueId === 'string' 300 | ); 301 | } 302 | 303 | /** 304 | * Type guard for linear_createIssueRelation tool arguments 305 | */ 306 | export function isCreateIssueRelationArgs(args: unknown): args is { 307 | issueId: string; 308 | relatedIssueId: string; 309 | type: 'blocks' | 'blocked_by' | 'related' | 'duplicate' | 'duplicate_of'; 310 | } { 311 | return ( 312 | typeof args === 'object' && 313 | args !== null && 314 | 'issueId' in args && 315 | typeof (args as { issueId: string }).issueId === 'string' && 316 | 'relatedIssueId' in args && 317 | typeof (args as { relatedIssueId: string }).relatedIssueId === 'string' && 318 | 'type' in args && 319 | typeof (args as { type: string }).type === 'string' && 320 | ['blocks', 'blocked_by', 'related', 'duplicate', 'duplicate_of'].includes( 321 | (args as { type: string }).type, 322 | ) 323 | ); 324 | } 325 | 326 | /** 327 | * Type guard for linear_archiveIssue tool arguments 328 | */ 329 | export function isArchiveIssueArgs(args: unknown): args is { 330 | issueId: string; 331 | } { 332 | return ( 333 | typeof args === 'object' && 334 | args !== null && 335 | 'issueId' in args && 336 | typeof (args as { issueId: string }).issueId === 'string' 337 | ); 338 | } 339 | 340 | /** 341 | * Type guard for linear_setIssuePriority tool arguments 342 | */ 343 | export function isSetIssuePriorityArgs(args: unknown): args is { 344 | issueId: string; 345 | priority: number; 346 | } { 347 | return ( 348 | typeof args === 'object' && 349 | args !== null && 350 | 'issueId' in args && 351 | typeof (args as { issueId: string }).issueId === 'string' && 352 | 'priority' in args && 353 | typeof (args as { priority: number }).priority === 'number' && 354 | [0, 1, 2, 3, 4].includes((args as { priority: number }).priority) 355 | ); 356 | } 357 | 358 | /** 359 | * Type guard for linear_transferIssue tool arguments 360 | */ 361 | export function isTransferIssueArgs(args: unknown): args is { 362 | issueId: string; 363 | teamId: string; 364 | } { 365 | return ( 366 | typeof args === 'object' && 367 | args !== null && 368 | 'issueId' in args && 369 | typeof (args as { issueId: string }).issueId === 'string' && 370 | 'teamId' in args && 371 | typeof (args as { teamId: string }).teamId === 'string' 372 | ); 373 | } 374 | 375 | /** 376 | * Type guard for linear_duplicateIssue tool arguments 377 | */ 378 | export function isDuplicateIssueArgs(args: unknown): args is { 379 | issueId: string; 380 | } { 381 | return ( 382 | typeof args === 'object' && 383 | args !== null && 384 | 'issueId' in args && 385 | typeof (args as { issueId: string }).issueId === 'string' 386 | ); 387 | } 388 | 389 | /** 390 | * Type guard for linear_getIssueHistory tool arguments 391 | */ 392 | export function isGetIssueHistoryArgs(args: unknown): args is { 393 | issueId: string; 394 | limit?: number; 395 | } { 396 | return ( 397 | typeof args === 'object' && 398 | args !== null && 399 | 'issueId' in args && 400 | typeof (args as { issueId: string }).issueId === 'string' && 401 | (!('limit' in args) || typeof (args as { limit: number }).limit === 'number') 402 | ); 403 | } 404 | 405 | /** 406 | * Type guard for linear_getComments tool arguments 407 | */ 408 | export function isGetCommentsArgs(args: unknown): args is { 409 | issueId: string; 410 | limit?: number; 411 | } { 412 | return ( 413 | typeof args === 'object' && 414 | args !== null && 415 | 'issueId' in args && 416 | typeof (args as { issueId: string }).issueId === 'string' && 417 | (!('limit' in args) || typeof (args as { limit: number }).limit === 'number') 418 | ); 419 | } 420 | 421 | /** 422 | * Type guard for linear_updateProject tool arguments 423 | */ 424 | export function isUpdateProjectArgs(args: unknown): args is { 425 | id: string; 426 | name?: string; 427 | description?: string; 428 | content?: string; 429 | state?: string; 430 | } { 431 | return ( 432 | typeof args === 'object' && 433 | args !== null && 434 | 'id' in args && 435 | typeof (args as { id: string }).id === 'string' && 436 | (!('name' in args) || typeof (args as { name: string }).name === 'string') && 437 | (!('description' in args) || 438 | typeof (args as { description: string }).description === 'string') && 439 | (!('content' in args) || typeof (args as { content: string }).content === 'string') && 440 | (!('state' in args) || typeof (args as { state: string }).state === 'string') 441 | ); 442 | } 443 | 444 | /** 445 | * Type guard for linear_addIssueToProject tool arguments 446 | */ 447 | export function isAddIssueToProjectArgs(args: unknown): args is { 448 | issueId: string; 449 | projectId: string; 450 | } { 451 | return ( 452 | typeof args === 'object' && 453 | args !== null && 454 | 'issueId' in args && 455 | typeof (args as { issueId: string }).issueId === 'string' && 456 | 'projectId' in args && 457 | typeof (args as { projectId: string }).projectId === 'string' 458 | ); 459 | } 460 | 461 | /** 462 | * Type guard for linear_getProjectIssues tool arguments 463 | */ 464 | export function isGetProjectIssuesArgs(args: unknown): args is { 465 | projectId: string; 466 | limit?: number; 467 | } { 468 | return ( 469 | typeof args === 'object' && 470 | args !== null && 471 | 'projectId' in args && 472 | typeof (args as { projectId: string }).projectId === 'string' && 473 | (!('limit' in args) || typeof (args as { limit: number }).limit === 'number') 474 | ); 475 | } 476 | 477 | /** 478 | * Type guard for linear_getCycles tool arguments 479 | */ 480 | export function isGetCyclesArgs(args: unknown): args is { 481 | teamId?: string; 482 | limit?: number; 483 | } { 484 | return ( 485 | typeof args === 'object' && 486 | args !== null && 487 | (!('teamId' in args) || typeof (args as { teamId: string }).teamId === 'string') && 488 | (!('limit' in args) || typeof (args as { limit: number }).limit === 'number') 489 | ); 490 | } 491 | 492 | /** 493 | * Type guard for linear_getActiveCycle tool arguments 494 | */ 495 | export function isGetActiveCycleArgs(args: unknown): args is { 496 | teamId: string; 497 | } { 498 | return ( 499 | typeof args === 'object' && 500 | args !== null && 501 | 'teamId' in args && 502 | typeof (args as { teamId: string }).teamId === 'string' 503 | ); 504 | } 505 | 506 | /** 507 | * Type guard for linear_addIssueToCycle tool arguments 508 | */ 509 | export function isAddIssueToCycleArgs(args: unknown): args is { 510 | issueId: string; 511 | cycleId: string; 512 | } { 513 | return ( 514 | typeof args === 'object' && 515 | args !== null && 516 | 'issueId' in args && 517 | typeof (args as { issueId: string }).issueId === 'string' && 518 | 'cycleId' in args && 519 | typeof (args as { cycleId: string }).cycleId === 'string' 520 | ); 521 | } 522 | 523 | /** 524 | * Type guard for linear_getWorkflowStates tool arguments 525 | */ 526 | export function isGetWorkflowStatesArgs(args: unknown): args is { 527 | teamId: string; 528 | includeArchived?: boolean; 529 | } { 530 | if ( 531 | typeof args !== 'object' || 532 | args === null || 533 | !('teamId' in args) || 534 | typeof (args as { teamId: string }).teamId !== 'string' 535 | ) { 536 | return false; 537 | } 538 | 539 | if ( 540 | 'includeArchived' in args && 541 | typeof (args as { includeArchived: boolean }).includeArchived !== 'boolean' 542 | ) { 543 | return false; 544 | } 545 | 546 | return true; 547 | } 548 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Interface for MCP Tool Definition in our format 3 | */ 4 | export interface MCPToolDefinition { 5 | name: string; 6 | description: string; 7 | input_schema: { 8 | type: string; 9 | properties: Record; 10 | required?: string[]; 11 | }; 12 | output_schema: { 13 | type: string; 14 | properties?: Record; 15 | items?: any; 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /src/utils/config.ts: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv'; 2 | 3 | // Load environment variables from .env file 4 | dotenv.config(); 5 | 6 | /** 7 | * Parse command line arguments to find a specific flag and its value 8 | * @param flag The flag to search for (e.g., '--token') 9 | * @returns The value of the flag or undefined if not found 10 | */ 11 | export function getCommandLineArg(flag: string): string | undefined { 12 | const args = process.argv.slice(2); 13 | 14 | for (let i = 0; i < args.length; i++) { 15 | if (args[i] === flag && i + 1 < args.length) { 16 | return args[i + 1]; 17 | } 18 | } 19 | 20 | return undefined; 21 | } 22 | 23 | /** 24 | * Get the Linear API token from command-line arguments or environment variable 25 | * @returns The API token or undefined if not found 26 | */ 27 | export function getLinearApiToken(): string | undefined { 28 | // First try to get the token from command-line arguments 29 | const tokenFromArgs = getCommandLineArg('--token'); 30 | 31 | // If not found, try to get it from environment variables 32 | // Check both LINEAR_API_TOKEN and LINEAR_API_KEY for compatibility with Smithery 33 | const tokenFromEnv = process.env.LINEAR_API_TOKEN || process.env.LINEAR_API_KEY; 34 | 35 | // Log for debugging 36 | if (!tokenFromArgs && !tokenFromEnv) { 37 | console.error('API token not found in command line args or environment variables'); 38 | console.error( 39 | 'Environment variables:', 40 | Object.keys(process.env).filter((key) => key.includes('LINEAR')), 41 | ); 42 | } 43 | 44 | return tokenFromArgs || tokenFromEnv; 45 | } 46 | 47 | /** 48 | * Log initialization information 49 | * @param message The message to log 50 | */ 51 | export function logInfo(message: string): void { 52 | console.error(message); 53 | } 54 | 55 | /** 56 | * Log error information 57 | * @param message The error message 58 | * @param error The error object (optional) 59 | */ 60 | export function logError(message: string, error?: unknown): void { 61 | if (error) { 62 | console.error(message, error); 63 | } else { 64 | console.error(message); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "NodeNext", 5 | "moduleResolution": "NodeNext", 6 | "lib": [ 7 | "ES2022" 8 | ], 9 | "allowJs": true, 10 | "outDir": "dist", 11 | "rootDir": "src", 12 | "strict": true, 13 | "esModuleInterop": true, 14 | "skipLibCheck": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "resolveJsonModule": true, 17 | "baseUrl": ".", 18 | "paths": { 19 | "@/*": [ 20 | "src/*" 21 | ] 22 | } 23 | }, 24 | "include": [ 25 | "src/**/*" 26 | ], 27 | "exclude": [ 28 | "node_modules", 29 | "**/*.test.ts" 30 | ] 31 | } -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "ESNext", 5 | "moduleResolution": "NodeNext", 6 | "target": "ESNext", 7 | "sourceMap": true, 8 | "outDir": "dist" 9 | }, 10 | "ts-node": { 11 | "esm": true, 12 | "experimentalSpecifierResolution": "node" 13 | }, 14 | "include": ["src/**/*"], 15 | "exclude": ["node_modules", "**/*.spec.ts"] 16 | } 17 | --------------------------------------------------------------------------------