Backlog MCP Server

by fleagne
Verified
import { BACKLOG_API_KEY, BACKLOG_BASE_URL } from "../config/config.js"; import type { AddIssueParams, AddWikiParams, DeleteIssueParams, DeleteWikiParams, IssueParams, IssuesParams, ProjectParams, ProjectsParams, UpdateIssueParams, UpdateWikiParams, WikiParams, WikisParams, } from "../core/schema.js"; import type { BacklogIssue, BacklogProject, BacklogWiki, } from "../core/types.js"; import { APIError } from "../error/errors.js"; class BacklogAPI { private baseUrl: string; private apiKey: string; constructor(baseUrl: string, apiKey: string) { this.baseUrl = baseUrl; this.apiKey = apiKey; } private async request<T>( path: string, // biome-ignore lint/suspicious/noExplicitAny: Because the interface changes according to the purpose params: Record<string, any> = {}, method: "GET" | "POST" | "PATCH" | "DELETE" = "GET", headers: Record<string, string> = {}, ): Promise<T> { const url = new URL(`${this.baseUrl}${path}`); url.searchParams.set("apiKey", this.apiKey); for (const [key, value] of Object.entries(params)) { if (value === undefined || value === null) continue; if (Array.isArray(value)) { for (const item of value) { url.searchParams.append(`${key}[]`, String(item)); } } else { url.searchParams.set(key, String(value)); } } let bodyData: URLSearchParams | string | undefined; if (method === "POST" || method === "PATCH") { if (headers["Content-Type"] === "application/x-www-form-urlencoded") { const formData = new URLSearchParams(); for (const [key, value] of Object.entries(params)) { if (value === undefined || value === null) continue; if (Array.isArray(value)) { for (const item of value) { formData.append(`${key}[]`, String(item)); } } else { formData.append(key, String(value)); } } bodyData = formData; } else { bodyData = JSON.stringify(params); } } try { const response = await fetch(url, { method: method, headers: headers, body: bodyData, }); if (!response.ok) { const errorText = await response.text(); throw new APIError( `Backlog API responded with status: ${response.status} ${response.statusText}\n${errorText}`, response.status, ); } return response.json() as Promise<T>; } catch (error) { if (error instanceof APIError) { throw error; } throw new APIError( `Failed to communicate with Backlog API: ${error instanceof Error ? error.message : String(error)}`, ); } } async getProjects(params: ProjectsParams): Promise<string> { const data = await this.request<BacklogProject[]>( "/projects", params, "GET", { Accept: "application/json", }, ); return JSON.stringify(data, null, 2); } async getProject(params: ProjectParams): Promise<string> { const data = await this.request<BacklogProject>( `/projects/${params.projectIdOrKey}`, {}, "GET", { Accept: "application/json", }, ); return JSON.stringify(data, null, 2); } async getIssues(params: IssuesParams): Promise<string> { const data = await this.request<BacklogIssue[]>("/issues", params, "GET", { Accept: "application/json", }); return JSON.stringify(data, null, 2); } async getIssue(params: IssueParams): Promise<string> { const data = await this.request<BacklogIssue>( `/issues/${params.issueIdOrKey}`, {}, "GET", { Accept: "application/json", }, ); return JSON.stringify(data, null, 2); } async addIssue(params: AddIssueParams): Promise<string> { const data = await this.request<BacklogIssue>("/issues", params, "POST", { "Content-Type": "application/x-www-form-urlencoded", }); return JSON.stringify(data, null, 2); } async updateIssue(params: UpdateIssueParams): Promise<string> { const data = await this.request<BacklogIssue>( `/issues/${params.issueIdOrKey}`, { ...params, issueIdOrKey: undefined }, "PATCH", { "Content-Type": "application/x-www-form-urlencoded", }, ); return JSON.stringify(data, null, 2); } async deleteIssue(params: DeleteIssueParams): Promise<string> { const data = await this.request<BacklogIssue>( `/issues/${params.issueIdOrKey}`, {}, "DELETE", { "Content-Type": "application/x-www-form-urlencoded", }, ); return JSON.stringify(data, null, 2); } async getWikis(params: WikisParams): Promise<string> { const data = await this.request<BacklogWiki[]>("/wikis", params, "GET", { Accept: "application/json", }); return JSON.stringify(data, null, 2); } async getWiki(params: WikiParams): Promise<string> { const data = await this.request<BacklogWiki>( `/wikis/${params.wikiId}`, {}, "GET", { Accept: "application/json", }, ); return JSON.stringify(data, null, 2); } async addWiki(params: AddWikiParams): Promise<string> { const data = await this.request<BacklogWiki>("/wikis", params, "POST", { "Content-Type": "application/x-www-form-urlencoded", }); return JSON.stringify(data, null, 2); } async updateWiki(params: UpdateWikiParams): Promise<string> { const data = await this.request<BacklogWiki>( `/wikis/${params.wikiId}`, { ...params, wikiId: undefined }, "PATCH", { "Content-Type": "application/x-www-form-urlencoded", }, ); return JSON.stringify(data, null, 2); } async deleteWiki(params: DeleteWikiParams): Promise<string> { const data = await this.request<BacklogWiki>( `/wikis/${params.wikiId}`, {}, "DELETE", { "Content-Type": "application/x-www-form-urlencoded", }, ); return JSON.stringify(data, null, 2); } } export const backlogAPI = new BacklogAPI(BACKLOG_BASE_URL, BACKLOG_API_KEY);