Backlog MCP Server
- src
/**
* Backlog API client for the MCP server
*/
import { AuthConfig, RecentlyViewedProject, BacklogProject, BacklogError, BacklogIssue, BacklogIssueDetail, BacklogComment, BacklogCommentDetail, BacklogCommentCount, BacklogWikiPage } from './types.js';
/**
* Backlog API client for making API calls
*/
export class BacklogClient {
private config: AuthConfig;
constructor(config: AuthConfig) {
this.config = config;
}
/**
* Get the full API URL with API key parameter
*/
private getUrl(path: string, queryParams: Record<string, string> = {}): string {
const url = new URL(`${this.config.spaceUrl}/api/v2${path}`);
// Add API key
url.searchParams.append('apiKey', this.config.apiKey);
// Add any additional query parameters
Object.entries(queryParams).forEach(([key, value]) => {
url.searchParams.append(key, value);
});
return url.toString();
}
/**
* Make an API request to Backlog
*/
private async request<T>(path: string, options: RequestInit = {}, queryParams: Record<string, string> = {}): Promise<T> {
const url = this.getUrl(path, queryParams);
try {
const response = await fetch(url, {
...options,
headers: {
'Content-Type': 'application/json',
...options.headers,
},
});
const data = await response.json();
if (!response.ok) {
const error = data as BacklogError;
throw new Error(`Backlog API Error: ${error.errors?.[0]?.message || 'Unknown error'} (Code: ${error.errors?.[0]?.code})`);
}
return data as T;
} catch (error) {
console.error(`Error in Backlog API request to ${path}:`, error);
throw error;
}
}
/**
* Make a POST request with form data to Backlog
*/
private async postFormData<T>(path: string, formData: Record<string, string | number | boolean>): Promise<T> {
const url = this.getUrl(path);
const formBody = new URLSearchParams();
// Add form parameters
Object.entries(formData).forEach(([key, value]) => {
formBody.append(key, value.toString());
});
try {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: formBody,
});
const data = await response.json();
if (!response.ok) {
const error = data as BacklogError;
throw new Error(`Backlog API Error: ${error.errors?.[0]?.message || 'Unknown error'} (Code: ${error.errors?.[0]?.code})`);
}
return data as T;
} catch (error) {
console.error(`Error in Backlog API POST request to ${path}:`, error);
throw error;
}
}
/**
* Get recently viewed projects for the current user
*/
async getRecentlyViewedProjects(params: { order?: 'asc' | 'desc', offset?: number, count?: number } = {}): Promise<RecentlyViewedProject[]> {
const queryParams: Record<string, string> = {};
if (params.order) queryParams.order = params.order;
if (params.offset !== undefined) queryParams.offset = params.offset.toString();
if (params.count !== undefined) queryParams.count = params.count.toString();
return this.request<RecentlyViewedProject[]>('/users/myself/recentlyViewedProjects', {}, queryParams);
}
/**
* Get information about a specific project
*/
async getProject(projectId: string): Promise<BacklogProject> {
return this.request<BacklogProject>(`/projects/${projectId}`);
}
/**
* Get information about the current user
*/
async getMyself() {
return this.request('/users/myself');
}
/**
* Get space information
*/
async getSpace() {
return this.request('/space');
}
/**
* Get issues from a project
* @param projectIdOrKey Project ID or project key
* @param params Query parameters for filtering issues
*/
async getIssues(projectIdOrKey: string, params: {
statusId?: number[] | number;
assigneeId?: number[] | number;
categoryId?: number[] | number;
priorityId?: number[] | number;
offset?: number;
count?: number;
sort?: string;
order?: 'asc' | 'desc';
} = {}): Promise<BacklogIssue[]> {
const queryParams: Record<string, string> = {};
// Convert parameters to the format expected by the Backlog API
Object.entries(params).forEach(([key, value]) => {
if (Array.isArray(value)) {
value.forEach(v => {
queryParams[`${key}[]`] = v.toString();
});
} else if (value !== undefined) {
queryParams[key] = value.toString();
}
});
return this.request<BacklogIssue[]>(`/projects/${projectIdOrKey}/issues`, {}, queryParams);
}
/**
* Get detailed information about a specific issue
* @param issueIdOrKey Issue ID or issue key
*/
async getIssue(issueIdOrKey: string): Promise<BacklogIssueDetail> {
return this.request<BacklogIssueDetail>(`/issues/${issueIdOrKey}`);
}
/**
* Get comments from an issue
* @param issueIdOrKey Issue ID or issue key
* @param params Query parameters for filtering comments
*/
async getComments(issueIdOrKey: string, params: {
minId?: number;
maxId?: number;
count?: number;
order?: 'asc' | 'desc';
} = {}): Promise<BacklogComment[]> {
const queryParams: Record<string, string> = {};
if (params.minId !== undefined) queryParams.minId = params.minId.toString();
if (params.maxId !== undefined) queryParams.maxId = params.maxId.toString();
if (params.count !== undefined) queryParams.count = params.count.toString();
if (params.order) queryParams.order = params.order;
return this.request<BacklogComment[]>(`/issues/${issueIdOrKey}/comments`, {}, queryParams);
}
/**
* Add a comment to an issue
* @param issueIdOrKey Issue ID or issue key
* @param content Comment content
*/
async addComment(issueIdOrKey: string, content: string): Promise<BacklogComment> {
return this.postFormData<BacklogComment>(`/issues/${issueIdOrKey}/comments`, {
content
});
}
/**
* Get the count of comments in an issue
* @param issueIdOrKey Issue ID or issue key
*/
async getCommentCount(issueIdOrKey: string): Promise<BacklogCommentCount> {
return this.request<BacklogCommentCount>(`/issues/${issueIdOrKey}/comments/count`);
}
/**
* Get detailed information about a specific comment
* @param issueIdOrKey Issue ID or issue key
* @param commentId Comment ID
*/
async getComment(issueIdOrKey: string, commentId: number): Promise<BacklogCommentDetail> {
return this.request<BacklogCommentDetail>(`/issues/${issueIdOrKey}/comments/${commentId}`);
}
/**
* Get Wiki page list
*/
async getWikiPageList(projectIdOrKey?: string, keyword?: string): Promise<BacklogWikiPage[]> {
const queryParams: Record<string, string> = {};
if (projectIdOrKey) {
queryParams.projectIdOrKey = projectIdOrKey;
}
if (keyword) {
queryParams.keyword = keyword;
}
return this.request<BacklogWikiPage[]>('/wikis', {}, queryParams);
}
/**
* Get Wiki page detail
*/
async getWikiPage(wikiId: string): Promise<BacklogWikiPage> {
return this.request<BacklogWikiPage>(`/wikis/${wikiId}`);
}
/**
* Update Wiki page
*/
async updateWikiPage(
wikiId: string,
params: {
name?: string;
content?: string;
mailNotify?: boolean;
}
): Promise<BacklogWikiPage> {
const formData: Record<string, string | number | boolean> = {};
if (params.name !== undefined) {
formData.name = params.name;
}
if (params.content !== undefined) {
formData.content = params.content;
}
if (params.mailNotify !== undefined) {
formData.mailNotify = params.mailNotify;
}
return this.postFormData<BacklogWikiPage>(`/wikis/${wikiId}`, formData);
}
}