UNPKG

mcp-planes-server

Version:

Plane API client and server

255 lines (254 loc) 10.5 kB
import axios from 'axios'; import fetch from 'node-fetch'; import { McpError } from '@modelcontextprotocol/sdk/types.js'; export class PlanesClient { workspaceSlug; client; constructor(baseURL, apiKey, workspaceSlug) { if (!baseURL) { throw new Error('Base URL is required'); } if (!apiKey) { throw new Error('API key is required'); } if (!workspaceSlug) { throw new Error('Workspace slug is required'); } this.workspaceSlug = workspaceSlug; this.client = axios.create({ baseURL, headers: { 'X-API-Key': apiKey, 'Content-Type': 'application/json', }, }); } async request(path, method = "GET", body) { try { const url = `${this.client.defaults.baseURL}/api/v1/workspaces/${this.workspaceSlug}${path}`; const response = await fetch(url, { method, headers: { "Content-Type": "application/json", "X-API-Key": this.client.defaults.headers['X-API-Key'], }, body: body ? JSON.stringify(body) : undefined, }); if (!response.ok) { const errorText = await response.text(); throw new Error(`HTTP error! status: ${response.status}, message: ${errorText}`); } return await response.json(); } catch (error) { console.error('Request failed:', error); throw error; } } // Project operations async getProjects() { return this.request("/projects"); } // 可根据语义相似度查询项目,即关键字,需要查出来所有的项目后,然后根据关键字,查出来相似度最高的项目 // identifier 是项目唯一标识,可以用来查询项目 async getProject(input) { // 如果有projectId,直接查询 if (input.projectId) { return this.request(`/projects/${input.projectId}/`, "GET"); } // 如果有identifier,使用identifier查询 if (input.identifier) { const projects = await this.getProjects(); const project = projects.results.find((p) => p.identifier === input.identifier); if (project) { return project; } throw new Error(`Project with identifier ${input.identifier} not found`); } // 如果有keyword,进行语义相似度搜索 if (input.keyword) { const projects = await this.getProjects(); if (projects.results.length === 0) { throw new Error('No projects found'); } // 计算每个项目名称和描述与关键字的相似度 const projectsWithScore = projects.results.map((project) => { const nameScore = this.calculateSimilarity(project.name.toLowerCase(), input.keyword.toLowerCase()); const descScore = project.description ? this.calculateSimilarity(project.description.toLowerCase(), input.keyword.toLowerCase()) : 0; // 名称相似度权重更高 return { project, score: nameScore * 0.7 + descScore * 0.3 }; }).filter((p) => p.score > 0.5); if (projectsWithScore.length === 0) { return null; } // 按相似度降序排序 projectsWithScore.sort((a, b) => b.score - a.score); // 返回最相似的项目 return projectsWithScore[0].project; } throw new Error('Must provide either projectId, identifier, or keyword'); } // 计算两个字符串的相似度(使用 Levenshtein 距离的简化版本) calculateSimilarity(str1, str2) { const len1 = str1.length; const len2 = str2.length; // 如果字符串完全相同,返回1 if (str1 === str2) return 1; // 如果其中一个字符串为空,返回0 if (len1 === 0 || len2 === 0) return 0; // 计算包含关系的分数 const contains = str1.includes(str2) || str2.includes(str1); if (contains) return 0.8; // 计算公共子串的长度 let commonChars = 0; const str2Chars = new Set(str2); for (const char of str1) { if (str2Chars.has(char)) { commonChars++; } } // 返回基于公共字符的相似度分数 return commonChars / Math.max(len1, len2); } // 创建项目,如果存在,则更新 // keyword 是关键字,通过关键字查询项目,如果存在,则更新,否则创建 async createProject(input) { try { // 如果keyword和name都存在,则使用keyword查询 const project = await this.getProject({ keyword: input.keyword || input.name }); if (project) { throw new Error(`Project with keyword ${input.keyword} already exists`); } return this.request("/projects/", "POST", input); } catch (error) { if (error instanceof McpError) { throw error; } throw error; } } async updateProject(input) { return this.request(`/projects/${input.projectId}/`, "PATCH", input); } async deleteProject(input) { return this.request(`/projects/${input.projectId}/`, "DELETE"); } // Issue operations async getIssues(input) { return this.request(`/projects/${input.projectId}/issues/`); } async getIssue(input) { if (input.issueId) { return this.request(`/projects/${input.projectId}/issues/${input.issueId}/`); } if (input.sequenceId) { return this.request(`/projects/${input.projectId}/issues/${input.sequenceId}/`); } throw new Error('Must provide either issueId or sequenceId'); } async createIssue(input) { return this.request(`/projects/${input.projectId}/issues/`, "POST", input); } async updateIssue(input) { return this.request(`/projects/${input.projectId}/issues/${input.issueId}/`, "PATCH", input); } async deleteIssue(input) { return this.request(`/projects/${input.projectId}/issues/${input.issueId}/`, "DELETE"); } // Label operations async getLabels(input) { return this.request(`/projects/${input.projectId}/labels/`); } async createLabel(input) { return this.request(`/projects/${input.projectId}/labels/`, "POST", input); } async updateLabel(input) { return this.request(`/projects/${input.projectId}/labels/${input.labelId}/`, "PATCH", input); } async deleteLabel(input) { return this.request(`/projects/${input.projectId}/labels/${input.labelId}/`, "DELETE"); } // Comment operations async getComments(input) { return this.request(`/projects/${input.projectId}/issues/${input.issueId}/comments/`); } async createComment(input) { return this.request(`/projects/${input.projectId}/issues/${input.issueId}/comments/`, "POST", input); } async updateComment(input) { return this.request(`/projects/${input.projectId}/issues/${input.issueId}/comments/${input.commentId}/`, "PATCH", input); } async deleteComment(input) { return this.request(`/projects/${input.projectId}/issues/${input.issueId}/comments/${input.commentId}/`, "DELETE"); } // member operations async getMembers(input) { return this.request(`/projects/${input.projectId}/members/`); } async getMember(input) { return this.request(`/projects/${input.projectId}/members/${input.memberId}/`); } async createMember(input) { return this.request(`/projects/${input.projectId}/members/`, "POST", input); } async updateMember(input) { return this.request(`/projects/${input.projectId}/members/${input.memberId}/`, "PATCH", input); } async deleteMember(input) { return this.request(`/projects/${input.projectId}/members/${input.memberId}/`, "DELETE"); } // States async getStates(input) { return this.request(`/projects/${input.projectId}/states/`); } async createState(input) { return this.request(`/projects/${input.projectId}/states/`, "POST", input); } async updateState(input) { return this.request(`/projects/${input.projectId}/states/${input.stateId}/`, "PATCH", input); } async deleteState(input) { return this.request(`/projects/${input.projectId}/states/${input.stateId}/`, "DELETE"); } // Cycle operations async getCycles(input) { return this.request(`/projects/${input.projectId}/cycles/`); } // 可根据语义相似度查询项目,即关键字,需要查出来所有的项目后,然后根据关键字,查出来相似度最高的项目 // identifier 是项目唯一标识,可以用来查询项目 async getCycle(input) { if (input.cycleId) { return this.request(`/projects/${input.projectId}/cycles/${input.cycleId}/`); } if (input.keyword) { const cycles = await this.getCycles(input); if (cycles.results.length === 0) { throw new Error('No cycles found'); } const cycle = cycles.results.find((cycle) => this.calculateSimilarity(cycle.name.toLowerCase(), input.keyword.toLowerCase()) > 0.5); if (cycle) { return cycle; } throw new Error(`Cycle with keyword ${input.keyword} not found`); } throw new Error('Must provide either cycleId or keyword'); } async createCycle(input) { return this.request(`/projects/${input.projectId}/cycles/`, "POST", input); } async updateCycle(input) { return this.request(`/projects/${input.projectId}/cycles/${input.cycleId}/`, "PATCH", input); } async deleteCycle(input) { return this.request(`/projects/${input.projectId}/cycles/${input.cycleId}/`, "DELETE"); } } export const client = new PlanesClient(process.env.PLANE_API_URL || "https://app.plane.so", process.env.PLANE_API_KEY || "", process.env.PLANE_WORKSPACE_SLUG || "");