diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index dbb414f..0000000 --- a/.eslintrc.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "env": { - "node": true, - "es2022": true - }, - "extends": [ - "eslint:recommended", - "plugin:@typescript-eslint/recommended", - "plugin:prettier/recommended" - ], - "parser": "@typescript-eslint/parser", - "parserOptions": { - "ecmaVersion": "latest", - "sourceType": "module" - }, - "plugins": [ - "@typescript-eslint", - "prettier" - ], - "rules": { - "prettier/prettier": "error", - "indent": ["error", 2], - "linebreak-style": ["error", "unix"], - "quotes": ["error", "single"], - "semi": ["error", "always"], - "@typescript-eslint/explicit-function-return-type": "warn", - "@typescript-eslint/no-explicit-any": "warn" - } -} \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..8e94013 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,30 @@ +# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs + +name: Node.js CI + +on: + push: + branches: [ $default-branch ] + pull_request: + branches: [ $default-branch ] + +jobs: + build: + + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [18.x, 20.x, 22.x] + # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ + + steps: + - uses: actions/checkout@v4 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + - run: npm ci + - run: npm test diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 5c624cb..dec1d58 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -50,4 +50,4 @@ jobs: name: v${{ github.event.inputs.version }} draft: false prerelease: false - generate_notes: true + generate_release_notes: true diff --git a/README.md b/README.md index 533b07e..b8ccb59 100644 --- a/README.md +++ b/README.md @@ -13,9 +13,9 @@ The Plane MCP Server is a Model Context Protocol (MCP) server that provides seam ## Configuration Parameters -1. `PLANE_API_HOST_URL` - The host URL of the Plane API Server. Defaults to https://api.plane.so/ -2. `PLANE_API_KEY` - The user's API token. This can be obtained from the `/settings/api-tokens/` page in the UI. -3. `PLANE_WORKSPACE_SLUG` - The workspace slug for your Plane instance. +1. `PLANE_API_KEY` - The user's API token. This can be obtained from the `/settings/api-tokens/` page in the UI. +2. `PLANE_WORKSPACE_SLUG` - The workspace slug for your Plane instance. +3. `PLANE_API_HOST_URL` (optional) - The host URL of the Plane API Server. Defaults to https://api.plane.so/ ## Tools @@ -270,9 +270,11 @@ The Plane MCP Server is a Model Context Protocol (MCP) server that provides seam - `issue_id`: UUID of the issue (string, required) - `worklog_id`: UUID of the worklog (string, required) -## Usage with Claude Desktop +## Usage -Adding plane mcp server like below to your `claude_desktop_config.json` +### Claude Desktop + +Add Plane to [Claude Desktop](https://modelcontextprotocol.io/quickstart/user) by editing your `claude_desktop_config.json`. ```json { @@ -293,6 +295,30 @@ Adding plane mcp server like below to your `claude_desktop_config.json` } ``` +### VSCode + +Add Plane to [VSCode](https://code.visualstudio.com/docs/copilot/chat/mcp-servers#_add-an-mcp-server) by editing your `.vscode.json/mcp.json`. + +```json +{ + "servers": { + "plane": { + "command": "npx", + "args": [ + "-y", + "@makeplane/plane-mcp-server" + ], + "env": { + "PLANE_API_KEY": "", + "PLANE_API_HOST_URL": "" + } + } + } +} + +``` + ## License This project is licensed under the terms of the MIT open source license. diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..679cc31 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,53 @@ +import { FlatCompat } from "@eslint/eslintrc"; +import js from "@eslint/js"; +import typescriptEslint from "@typescript-eslint/eslint-plugin"; +import tsParser from "@typescript-eslint/parser"; +import prettier from "eslint-plugin-prettier"; +import { defineConfig, globalIgnores } from "eslint/config"; +import globals from "globals"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + allConfig: js.configs.all, +}); + +export default defineConfig([ + globalIgnores(["build/"]), + { + extends: compat.extends( + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "plugin:prettier/recommended", + "prettier" + ), + + plugins: { + "@typescript-eslint": typescriptEslint, + prettier, + }, + + languageOptions: { + globals: { + ...globals.node, + }, + + parser: tsParser, + ecmaVersion: "latest", + sourceType: "module", + }, + + rules: { + "prettier/prettier": "error", + indent: ["error", 2], + semi: ["error", "always"], + "linebreak-style": ["error", "unix"], + "@typescript-eslint/explicit-function-return-type": "warn", + "@typescript-eslint/no-explicit-any": "warn", + }, + }, +]); diff --git a/package-lock.json b/package-lock.json index 271ca81..0ba1bdb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@makeplane/plane-mcp-server", - "version": "0.1.1", + "version": "0.1.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@makeplane/plane-mcp-server", - "version": "0.1.1", + "version": "0.1.2", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.9.0", @@ -17,6 +17,8 @@ "plane-mcp-server": "build/index.js" }, "devDependencies": { + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "^9.25.0", "@trivago/prettier-plugin-sort-imports": "^5.2.2", "@types/node": "^22.14.0", "@typescript-eslint/eslint-plugin": "^8.29.1", @@ -24,6 +26,7 @@ "eslint": "^9.24.0", "eslint-config-prettier": "^10.1.1", "eslint-plugin-prettier": "^5.2.6", + "globals": "^16.0.0", "json-schema-to-zod": "^2.6.1", "prettier": "^3.5.3", "typescript": "^5.8.3", @@ -132,6 +135,16 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/traverse/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/@babel/types": { "version": "7.27.0", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz", @@ -264,9 +277,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.24.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.24.0.tgz", - "integrity": "sha512-uIY/y3z0uvOGX8cp1C2fiC4+ZmBhp6yZWkojtHL1YEMnRt1Y63HB9TM17proGEmeG7HeUY+UP36F0aknKYTpYA==", + "version": "9.25.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.25.0.tgz", + "integrity": "sha512-iWhsUS8Wgxz9AXNfvfOPFSW4VfMXdVhp1hjkZVhXCrpgh/aLcc45rX6MPu+tIVUWDw0HfNwth7O28M1xDxNf9w==", "dev": true, "license": "MIT", "engines": { @@ -1350,6 +1363,16 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/@eslint/js": { + "version": "9.24.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.24.0.tgz", + "integrity": "sha512-uIY/y3z0uvOGX8cp1C2fiC4+ZmBhp6yZWkojtHL1YEMnRt1Y63HB9TM17proGEmeG7HeUY+UP36F0aknKYTpYA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/espree": { "version": "10.3.0", "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", @@ -1784,13 +1807,16 @@ } }, "node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.0.0.tgz", + "integrity": "sha512-iInW14XItCXET01CQFqudPOWP2jYMl7T+QRQT+UNcR/iQncN/F0UNpgd76iFkBPgNQb4+X3LV9tLJYzwh+Gl3A==", "dev": true, "license": "MIT", "engines": { - "node": ">=4" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/gopd": { diff --git a/package.json b/package.json index 83c963d..fdfb047 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@makeplane/plane-mcp-server", - "version": "0.1.1", + "version": "0.1.2", "description": "The Plane MCP Server is a Model Context Protocol (MCP) server that provides seamless integration with Plane APIs, enabling projects, work items, and automations capabilities for develops and AI interfaces.", "bin": { "plane-mcp-server": "./build/index.js" @@ -13,6 +13,8 @@ "lint:fix": "eslint . --ext .ts --fix", "format": "prettier --write \"src/**/*.ts\"", "format:check": "prettier --check \"src/**/*.ts\"", + "test-startup": "node build/server.js", + "test": "npm run lint && npm run format:check && npm run build && npm run test-startup", "prepublishOnly": "npm run build" }, "files": [ @@ -34,6 +36,8 @@ "zod-to-json-schema": "^3.24.5" }, "devDependencies": { + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "^9.25.0", "@trivago/prettier-plugin-sort-imports": "^5.2.2", "@types/node": "^22.14.0", "@typescript-eslint/eslint-plugin": "^8.29.1", @@ -41,6 +45,7 @@ "eslint": "^9.24.0", "eslint-config-prettier": "^10.1.1", "eslint-plugin-prettier": "^5.2.6", + "globals": "^16.0.0", "json-schema-to-zod": "^2.6.1", "prettier": "^3.5.3", "typescript": "^5.8.3", diff --git a/src/common/request-helper.ts b/src/common/request-helper.ts index 4efc5a7..d4f07e4 100644 --- a/src/common/request-helper.ts +++ b/src/common/request-helper.ts @@ -1,7 +1,9 @@ import axios, { AxiosRequestConfig } from "axios"; export async function makePlaneRequest(method: string, path: string, body: any = null): Promise { - const url = `${process.env.PLANE_API_HOST_URL}api/v1/${path}`; + const hostUrl = process.env.PLANE_API_HOST_URL || "https://api.plane.so/"; + const host = hostUrl.endsWith("/") ? hostUrl : `${hostUrl}/`; + const url = `${host}api/v1/${path}`; const headers: Record = { "Content-Type": "application/json", "X-API-Key": process.env.PLANE_API_KEY || "", diff --git a/src/common/version.ts b/src/common/version.ts index 6d57f7a..bddb991 100644 --- a/src/common/version.ts +++ b/src/common/version.ts @@ -1,4 +1,7 @@ -import pkg from "../../package.json" with { type: "json" }; +import { createRequire } from "node:module"; + +const require = createRequire(import.meta.url); +const pkg = require("../../package.json"); export function getVersion() { return pkg.version; diff --git a/src/index.ts b/src/index.ts index bca7431..e82bc2b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,21 +1,10 @@ #!/usr/bin/env node - -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { registerTools } from "./tools/index.js"; -import { getVersion } from "./common/version.js"; - -async function main() { - const version = getVersion(); - const server = new McpServer({ - name: "plane-mcp-server", - version, - capabilities: {}, - }); - - registerTools(server); +import { createServer } from "./server.js"; +async function main() { + const { server, version } = createServer(); const transport = new StdioServerTransport(); await server.connect(transport); console.error(`Plane MCP Server running on stdio: ${version}`); diff --git a/src/server.ts b/src/server.ts new file mode 100644 index 0000000..d75254c --- /dev/null +++ b/src/server.ts @@ -0,0 +1,18 @@ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; + +import { getVersion } from "./common/version.js"; +import { registerTools } from "./tools/index.js"; + +export function createServer() { + const version = getVersion(); + + const server = new McpServer({ + name: "plane-mcp-server", + version, + capabilities: {}, + }); + + registerTools(server); + + return { server, version }; +} diff --git a/src/tools/cycle-issues.ts b/src/tools/cycle-issues.ts index 602849a..2049cec 100644 --- a/src/tools/cycle-issues.ts +++ b/src/tools/cycle-issues.ts @@ -1,5 +1,5 @@ -import { z } from "zod"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; import { makePlaneRequest } from "../common/request-helper.js"; diff --git a/src/tools/projects.ts b/src/tools/projects.ts index 4fe2146..e25be9f 100644 --- a/src/tools/projects.ts +++ b/src/tools/projects.ts @@ -49,7 +49,12 @@ export const registerProjectTools = (server: McpServer) => { "Create a new project", { name: z.string().describe("The name of the project"), - identifier: z.string().max(7).describe("The identifier of the project. This is typically a word of around 5 characters derived from the name of the project in uppercase."), + identifier: z + .string() + .max(7) + .describe( + "The identifier of the project. This is typically a word of around 5 characters derived from the name of the project in uppercase." + ), }, async ({ name, identifier }) => { const project = await makePlaneRequest("POST", `workspaces/${process.env.PLANE_WORKSPACE_SLUG}/projects/`, { diff --git a/src/tools/user.ts b/src/tools/user.ts index 7567b81..0c99ccd 100644 --- a/src/tools/user.ts +++ b/src/tools/user.ts @@ -4,7 +4,7 @@ import { makePlaneRequest } from "../common/request-helper.js"; export const registerUserTools = (server: McpServer) => { server.tool("get_user", "Get the current user's information", {}, async () => { - const user = await makePlaneRequest("GET", `users/me/`); + const user = await makePlaneRequest("GET", "users/me/"); return { content: [{ type: "text", text: JSON.stringify(user, null, 2) }], };