# Directory Structure
```
├── .gitignore
├── package-lock.json
├── package.json
├── README.md
├── src
│ ├── backlog-client.ts
│ ├── config.ts
│ ├── handlers
│ │ ├── prompt-handlers.ts
│ │ ├── resource-handlers.ts
│ │ └── tool-handlers.ts
│ ├── index.ts
│ └── types.ts
└── tsconfig.json
```
# Files
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
node_modules/
build/
*.log
.env*
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./build",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
```
--------------------------------------------------------------------------------
/src/config.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Configuration for the Backlog MCP server
*/
import { AuthConfig } from './types.js';
/**
* Load configuration from environment variables
*/
export function loadConfig(): AuthConfig {
const apiKey = process.env.BACKLOG_API_KEY;
const spaceUrl = process.env.BACKLOG_SPACE_URL;
if (!apiKey) {
throw new Error('BACKLOG_API_KEY environment variable is required');
}
if (!spaceUrl) {
throw new Error('BACKLOG_SPACE_URL environment variable is required');
}
return { apiKey, spaceUrl };
}
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
{
"name": "mcp-backlog-server",
"version": "0.1.0",
"description": "Backlog MCP Server",
"private": true,
"type": "module",
"bin": {
"mcp-backlog-server": "./build/index.js"
},
"files": [
"build"
],
"scripts": {
"build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"",
"prepare": "npm run build",
"watch": "tsc --watch",
"inspector": "npx @modelcontextprotocol/inspector build/index.js"
},
"dependencies": {
"@modelcontextprotocol/sdk": "0.6.0"
},
"devDependencies": {
"@types/node": "^20.11.24",
"typescript": "^5.3.3"
}
}
```
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
```typescript
#!/usr/bin/env node
/**
* Backlog MCP server
*
* This server implements a Backlog integration with Model Context Protocol.
* It provides resources for viewing recent projects and tools for interactions.
*/
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListResourcesRequestSchema,
ListToolsRequestSchema,
ReadResourceRequestSchema,
ListPromptsRequestSchema,
GetPromptRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { loadConfig } from './config.js';
import { BacklogClient } from './backlog-client.js';
import { listRecentProjects, readProject } from './handlers/resource-handlers.js';
import { listTools, executeTools } from './handlers/tool-handlers.js';
import { listPrompts, getPrompt } from './handlers/prompt-handlers.js';
/**
* Create an MCP server with capabilities for resources, tools, and prompts.
*/
const server = new Server(
{
name: "mcp-backlog-server",
version: "0.1.0",
},
{
capabilities: {
resources: {},
tools: {},
prompts: {},
},
}
);
/**
* Initialize the Backlog client
*/
const config = loadConfig();
const backlogClient = new BacklogClient(config);
/**
* Handler for listing available Backlog resources (recently viewed projects)
*/
server.setRequestHandler(ListResourcesRequestSchema, async () => {
return await listRecentProjects(backlogClient);
});
/**
* Handler for reading the contents of a specific Backlog resource
*/
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
return await readProject(backlogClient, request.params.uri);
});
/**
* Handler that lists available tools
*/
server.setRequestHandler(ListToolsRequestSchema, async () => {
return listTools();
});
/**
* Handler for executing tools
*/
server.setRequestHandler(CallToolRequestSchema, async (request) => {
return await executeTools(
backlogClient,
request.params.name,
request.params.arguments
);
});
/**
* Handler that lists available prompts
*/
server.setRequestHandler(ListPromptsRequestSchema, async () => {
return listPrompts();
});
/**
* Handler for generating prompts
*/
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
return await getPrompt(backlogClient, request.params.name);
});
/**
* Start the server using stdio transport
*/
async function main() {
try {
console.error("Starting Backlog MCP server...");
const transport = new StdioServerTransport();
await server.connect(transport);
} catch (error) {
console.error("Server initialization error:", error);
process.exit(1);
}
}
main().catch((error) => {
console.error("Server error:", error);
process.exit(1);
});
```
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Types for the Backlog MCP server
*/
// Auth configuration
export interface AuthConfig {
apiKey: string;
spaceUrl: string;
}
// Backlog Project type
export interface BacklogProject {
id: number;
projectKey: string;
name: string;
chartEnabled: boolean;
useResolvedForChart: boolean;
subtaskingEnabled: boolean;
projectLeaderCanEditProjectLeader: boolean;
useWiki: boolean;
useFileSharing: boolean;
useWikiTreeView: boolean;
useSubversion: boolean;
useGit: boolean;
useOriginalImageSizeAtWiki: boolean;
textFormattingRule: string;
archived: boolean;
displayOrder: number;
useDevAttributes: boolean;
}
// Recently viewed project response
export interface RecentlyViewedProject {
project: BacklogProject;
updated: string;
}
// Backlog Error response
export interface BacklogError {
errors: Array<{
message: string;
code: number;
moreInfo: string;
}>;
}
// Backlog user information
export interface BacklogUser {
id: number;
userId: string;
name: string;
roleType: number;
lang: string;
mailAddress: string;
nulabAccount: {
nulabId: string;
name: string;
uniqueId: string;
};
}
// Backlog space information
export interface BacklogSpace {
spaceKey: string;
name: string;
ownerId: number;
lang: string;
timezone: string;
reportSendTime: string;
textFormattingRule: string;
created: string;
updated: string;
}
// Backlog issue information
export interface BacklogIssue {
id: number;
projectId: number;
issueKey: string;
keyId: number;
issueType: {
id: number;
projectId: number;
name: string;
color: string;
displayOrder: number;
};
summary: string;
description: string;
priority: {
id: number;
name: string;
};
status: {
id: number;
name: string;
};
assignee: {
id: number;
name: string;
roleType: number;
userId: string;
} | null;
category: {
id: number;
name: string;
}[];
versions: {
id: number;
name: string;
}[];
milestone: {
id: number;
name: string;
}[];
startDate: string | null;
dueDate: string | null;
estimatedHours: number | null;
actualHours: number | null;
parentIssueId: number | null;
createdUser: {
id: number;
userId: string;
name: string;
};
created: string;
updatedUser: {
id: number;
userId: string;
name: string;
};
updated: string;
customFields: any[];
attachments: any[];
sharedFiles: any[];
stars: any[];
}
// Backlog comment information
export interface BacklogComment {
id: number;
projectId: number;
issueId: number;
content: string;
changeLog: any[] | null;
createdUser: {
id: number;
userId: string;
name: string;
roleType: number;
lang: string;
nulabAccount?: {
nulabId: string;
name: string;
uniqueId: string;
};
mailAddress?: string;
lastLoginTime?: string;
};
created: string;
updated: string;
stars: any[];
notifications: any[];
}
// Backlog comment detail information
export interface BacklogCommentDetail extends BacklogComment {
// 追加のフィールドがある場合はここに定義
}
// Backlog comment count response
export interface BacklogCommentCount {
count: number;
}
// Backlog issue detail with comments
export interface BacklogIssueDetail extends BacklogIssue {
comments: BacklogComment[];
}
// Backlog Wiki page
export interface BacklogWikiPage {
id: number;
projectId: number;
name: string;
content?: string;
tags: BacklogWikiTag[];
attachments?: any[];
sharedFiles?: any[];
stars?: any[];
createdUser: {
id: number;
userId: string;
name: string;
roleType: number;
lang: string;
nulabAccount: {
nulabId: string;
name: string;
uniqueId: string;
};
mailAddress: string;
lastLoginTime: string;
};
created: string;
updatedUser: {
id: number;
userId: string;
name: string;
roleType: number;
lang: string;
nulabAccount: {
nulabId: string;
name: string;
uniqueId: string;
};
mailAddress: string;
lastLoginTime: string;
};
updated: string;
}
// Backlog Wiki tag
export interface BacklogWikiTag {
id: number;
name: string;
}
```
--------------------------------------------------------------------------------
/src/handlers/resource-handlers.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Resource handlers for the Backlog MCP server
*/
import { BacklogClient } from '../backlog-client.js';
import { RecentlyViewedProject, BacklogIssue, BacklogWikiPage } from '../types.js';
/**
* Extract the project ID from a backlog URI
*/
function extractProjectId(uri: string): string {
const url = new URL(uri);
return url.pathname.replace(/^\/project\//, '');
}
/**
* Extract the issue ID from a backlog issue URI
*/
function extractIssueId(uri: string): string {
const url = new URL(uri);
return url.pathname.replace(/^\/issue\//, '');
}
/**
* Extract project key from issue key (e.g., "PROJECT-123" -> "PROJECT")
*/
function extractProjectKeyFromIssueKey(issueKey: string): string {
const match = issueKey.match(/^([A-Z0-9_]+)-\d+$/);
return match ? match[1] : '';
}
/**
* Extract the wiki ID from a backlog wiki URI
*/
function extractWikiId(uri: string): string {
const url = new URL(uri);
return url.pathname.replace(/^\/wiki\//, '');
}
/**
* Handler for listing recent projects
*/
export async function listRecentProjects(client: BacklogClient) {
try {
const projects = await client.getRecentlyViewedProjects({ count: 20 });
// Create resources for projects
const projectResources = projects.map(item => ({
uri: `backlog://project/${item.project.id}`,
mimeType: "application/json",
name: item.project.name,
description: `Backlog project: ${item.project.name} (${item.project.projectKey})`
}));
// For the first project, also list its issues and wikis
if (projects.length > 0) {
try {
const firstProject = projects[0].project;
const issues = await client.getIssues(firstProject.id.toString(), { count: 10 });
// Create resources for issues
const issueResources = issues.map(issue => ({
uri: `backlog://issue/${issue.id}`,
mimeType: "application/json",
name: issue.summary,
description: `Issue: ${issue.issueKey} - ${issue.summary}`
}));
// Try to get wiki pages for the first project
try {
const wikiPages = await client.getWikiPageList(firstProject.projectKey);
// Create resources for wiki pages (limit to 10)
const wikiResources = wikiPages.slice(0, 10).map(wiki => ({
uri: `backlog://wiki/${wiki.id}`,
mimeType: "application/json",
name: wiki.name,
description: `Wiki: ${wiki.name}`
}));
return {
resources: [...projectResources, ...issueResources, ...wikiResources]
};
} catch (wikiError) {
console.error('Error fetching wikis for first project:', wikiError);
// Fall back to just returning projects and issues if wiki fetch fails
return { resources: [...projectResources, ...issueResources] };
}
} catch (error) {
console.error('Error fetching issues for first project:', error);
// Fall back to just returning projects if issues fetch fails
return { resources: projectResources };
}
}
return { resources: projectResources };
} catch (error) {
console.error('Error listing recent projects:', error);
throw error;
}
}
/**
* Handler for reading a project, issue, or wiki resource
*/
export async function readProject(client: BacklogClient, uri: string) {
try {
if (uri.startsWith('backlog://project/')) {
// Handle project resource
const projectId = extractProjectId(uri);
try {
const project = await client.getProject(projectId);
// Return the project data as a JSON resource
return {
contents: [{
uri,
mimeType: "application/json",
text: JSON.stringify(project, null, 2)
}]
};
} catch (e) {
// Fallback: if direct project fetch fails, try to find it in recent projects
const recentProjects = await client.getRecentlyViewedProjects({ count: 100 });
const projectData = recentProjects.find(item => item.project.id.toString() === projectId);
if (!projectData) {
throw new Error(`Project ${projectId} not found`);
}
return {
contents: [{
uri,
mimeType: "application/json",
text: JSON.stringify(projectData.project, null, 2)
}]
};
}
} else if (uri.startsWith('backlog://issue/')) {
// Handle issue resource
const issueId = extractIssueId(uri);
try {
const issue = await client.getIssue(issueId);
// Return the issue data as a JSON resource
return {
contents: [{
uri,
mimeType: "application/json",
text: JSON.stringify(issue, null, 2)
}]
};
} catch (error) {
console.error('Error fetching issue:', error);
throw new Error(`Issue ${issueId} not found`);
}
} else if (uri.startsWith('backlog://wiki/')) {
// Handle wiki resource
const wikiId = extractWikiId(uri);
try {
const wiki = await client.getWikiPage(wikiId);
// Return the wiki data as a JSON resource
return {
contents: [{
uri,
mimeType: "application/json",
text: JSON.stringify(wiki, null, 2)
}]
};
} catch (e) {
console.error(`Error fetching wiki ${wikiId}:`, e);
throw new Error(`Wiki not found: ${wikiId}`);
}
} else {
throw new Error(`Unsupported resource URI: ${uri}`);
}
} catch (error) {
console.error(`Error reading resource ${uri}:`, error);
throw error;
}
}
```
--------------------------------------------------------------------------------
/src/handlers/prompt-handlers.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Prompt handlers for the Backlog MCP server
*/
import { BacklogClient } from '../backlog-client.js';
/**
* List the available prompts
*/
export function listPrompts() {
return {
prompts: [
{
name: "summarize_projects",
description: "Summarize recently viewed Backlog projects",
},
{
name: "analyze_backlog_usage",
description: "Analyze your Backlog usage patterns",
},
{
name: "summarize_wiki_pages",
description: "Summarize Wiki pages from a Backlog project",
}
]
};
}
/**
* Handle prompt generation
*/
export async function getPrompt(client: BacklogClient, promptName: string) {
try {
switch (promptName) {
case "summarize_projects": {
// Get recent projects
const recentProjects = await client.getRecentlyViewedProjects({ count: 10 });
// Create embedded resources for each project
const embeddedProjects = recentProjects.map(item => ({
type: "resource" as const,
resource: {
uri: `backlog://project/${item.project.id}`,
mimeType: "application/json",
text: JSON.stringify(item.project, null, 2)
}
}));
// Construct the prompt
return {
messages: [
{
role: "user",
content: {
type: "text",
text: "Please review the following recent Backlog projects:"
}
},
...embeddedProjects.map(project => ({
role: "user" as const,
content: project
})),
{
role: "user",
content: {
type: "text",
text: "Provide a concise summary of these recent projects, highlighting any patterns or important activities."
}
}
]
};
}
case "analyze_backlog_usage": {
// Get user data and space data
const userData = await client.getMyself();
const spaceData = await client.getSpace();
const recentProjects = await client.getRecentlyViewedProjects({ count: 20 });
// User data as resource
const userResource = {
type: "resource" as const,
resource: {
uri: "backlog://user/myself",
mimeType: "application/json",
text: JSON.stringify(userData, null, 2)
}
};
// Space data as resource
const spaceResource = {
type: "resource" as const,
resource: {
uri: "backlog://space",
mimeType: "application/json",
text: JSON.stringify(spaceData, null, 2)
}
};
// Projects summary as resource
const projectsResource = {
type: "resource" as const,
resource: {
uri: "backlog://projects/summary",
mimeType: "application/json",
text: JSON.stringify({
totalProjects: recentProjects.length,
projectNames: recentProjects.map(p => p.project.name),
lastUpdated: recentProjects.map(p => p.updated)
}, null, 2)
}
};
// Construct the prompt
return {
messages: [
{
role: "user",
content: {
type: "text",
text: "I'd like to understand my Backlog usage patterns. Please analyze the following information about my Backlog account, space, and recent projects:"
}
},
{
role: "user",
content: userResource
},
{
role: "user",
content: spaceResource
},
{
role: "user",
content: projectsResource
},
{
role: "user",
content: {
type: "text",
text: "Based on this data, please provide insights about how I'm using Backlog, which projects I'm focusing on recently, and any suggestions for improving my workflow."
}
}
]
};
}
case "summarize_wiki_pages": {
// Get recent projects to select one
const recentProjects = await client.getRecentlyViewedProjects({ count: 5 });
if (recentProjects.length === 0) {
throw new Error("No recent projects found");
}
// Use the first project
const firstProject = recentProjects[0].project;
// Get wiki pages for the project
const wikiPages = await client.getWikiPageList(firstProject.projectKey);
// Limit to 10 wiki pages
const limitedWikiPages = wikiPages.slice(0, 10);
// Create embedded resources for each wiki page
const embeddedWikiPages = await Promise.all(
limitedWikiPages.map(async (wiki) => {
// Get full wiki content
const fullWiki = await client.getWikiPage(wiki.id.toString());
return {
type: "resource" as const,
resource: {
uri: `backlog://wiki/${wiki.id}`,
mimeType: "application/json",
text: JSON.stringify(fullWiki, null, 2)
}
};
})
);
// Construct the prompt
return {
messages: [
{
role: "user",
content: {
type: "text",
text: `Please review the following Wiki pages from the "${firstProject.name}" project:`
}
},
...embeddedWikiPages.map(wiki => ({
role: "user" as const,
content: wiki
})),
{
role: "user",
content: {
type: "text",
text: "Provide a concise summary of these Wiki pages, highlighting the key information and how they relate to each other."
}
}
]
};
}
default:
throw new Error(`Unknown prompt: ${promptName}`);
}
} catch (error) {
console.error(`Error generating prompt ${promptName}:`, error);
throw error;
}
}
```
--------------------------------------------------------------------------------
/src/backlog-client.ts:
--------------------------------------------------------------------------------
```typescript
/**
* 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);
}
}
```
--------------------------------------------------------------------------------
/src/handlers/tool-handlers.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Tool handlers for the Backlog MCP server
*/
import { BacklogClient } from '../backlog-client.js';
/**
* List the available tools for Backlog operations
*/
export function listTools() {
return {
tools: [
{
name: "get_backlog_user",
description: "Get current Backlog user information",
inputSchema: {
type: "object",
properties: {},
required: []
}
},
{
name: "get_backlog_space",
description: "Get Backlog space information",
inputSchema: {
type: "object",
properties: {},
required: []
}
},
{
name: "list_recent_projects",
description: "List recently viewed Backlog projects",
inputSchema: {
type: "object",
properties: {
count: {
type: "number",
description: "Number of projects to retrieve (1-100, default 20)"
},
order: {
type: "string",
description: "Sorting order (asc or desc, default desc)",
enum: ["asc", "desc"]
}
},
required: []
}
},
{
name: "get_project_issues",
description: "Get issues from a Backlog project",
inputSchema: {
type: "object",
properties: {
projectIdOrKey: {
type: "string",
description: "Project ID or project key"
},
statusId: {
type: "array",
items: {
type: "number"
},
description: "Filter by status IDs"
},
assigneeId: {
type: "array",
items: {
type: "number"
},
description: "Filter by assignee IDs"
},
count: {
type: "number",
description: "Number of issues to retrieve (1-100, default 20)"
},
offset: {
type: "number",
description: "Offset for pagination"
},
sort: {
type: "string",
description: "Sort field (e.g., 'created', 'updated')"
},
order: {
type: "string",
description: "Sorting order (asc or desc, default desc)",
enum: ["asc", "desc"]
}
},
required: ["projectIdOrKey"]
}
},
{
name: "get_issue_detail",
description: "Get detailed information about a specific Backlog issue",
inputSchema: {
type: "object",
properties: {
issueIdOrKey: {
type: "string",
description: "Issue ID or issue key"
}
},
required: ["issueIdOrKey"]
}
},
{
name: "get_issue_comments",
description: "Get comments from a specific Backlog issue",
inputSchema: {
type: "object",
properties: {
issueIdOrKey: {
type: "string",
description: "Issue ID or issue key"
},
minId: {
type: "number",
description: "Minimum comment ID"
},
maxId: {
type: "number",
description: "Maximum comment ID"
},
count: {
type: "number",
description: "Number of comments to retrieve (1-100, default 20)"
},
order: {
type: "string",
description: "Sorting order (asc or desc, default desc)",
enum: ["asc", "desc"]
}
},
required: ["issueIdOrKey"]
}
},
{
name: "add_issue_comment",
description: "Add a comment to a specific Backlog issue",
inputSchema: {
type: "object",
properties: {
issueIdOrKey: {
type: "string",
description: "Issue ID or issue key"
},
content: {
type: "string",
description: "Comment content"
}
},
required: ["issueIdOrKey", "content"]
}
},
{
name: "get_issue_comment_count",
description: "Get the count of comments in a specific Backlog issue",
inputSchema: {
type: "object",
properties: {
issueIdOrKey: {
type: "string",
description: "Issue ID or issue key"
}
},
required: ["issueIdOrKey"]
}
},
{
name: "get_issue_comment",
description: "Get detailed information about a specific comment in a Backlog issue",
inputSchema: {
type: "object",
properties: {
issueIdOrKey: {
type: "string",
description: "Issue ID or issue key"
},
commentId: {
type: "number",
description: "Comment ID"
}
},
required: ["issueIdOrKey", "commentId"]
}
},
{
name: "get_wiki_page_list",
description: "Get a list of Wiki pages from Backlog",
inputSchema: {
type: "object",
properties: {
projectIdOrKey: {
type: "string",
description: "Project ID or project key (optional)"
},
keyword: {
type: "string",
description: "Keyword to search for in Wiki pages (optional)"
}
},
required: []
}
},
{
name: "get_wiki_page",
description: "Get detailed information about a specific Wiki page",
inputSchema: {
type: "object",
properties: {
wikiId: {
type: "string",
description: "Wiki page ID"
}
},
required: ["wikiId"]
}
},
{
name: "update_wiki_page",
description: "Update a Wiki page in Backlog",
inputSchema: {
type: "object",
properties: {
wikiId: {
type: "string",
description: "Wiki page ID"
},
name: {
type: "string",
description: "New name for the Wiki page (optional)"
},
content: {
type: "string",
description: "New content for the Wiki page (optional)"
},
mailNotify: {
type: "boolean",
description: "Whether to send notification emails (optional)"
}
},
required: ["wikiId"]
}
}
]
};
}
/**
* Format data for display in tool response
*/
function formatToolResponse(title: string, data: any): any {
return {
content: [
{
type: "text",
text: `# ${title}\n\n${JSON.stringify(data, null, 2)}`
}
]
};
}
/**
* Handle tool execution
*/
export async function executeTools(client: BacklogClient, toolName: string, args: any) {
try {
switch (toolName) {
case "get_backlog_user": {
const userData = await client.getMyself();
return formatToolResponse("Backlog User Information", userData);
}
case "get_backlog_space": {
const spaceData = await client.getSpace();
return formatToolResponse("Backlog Space Information", spaceData);
}
case "list_recent_projects": {
const count = args?.count && Number(args.count) > 0 && Number(args.count) <= 100
? Number(args.count)
: 20;
const order = args?.order === 'asc' ? 'asc' : 'desc';
const projects = await client.getRecentlyViewedProjects({
count,
order
});
return formatToolResponse("Recently Viewed Projects", projects);
}
case "get_project_issues": {
if (!args?.projectIdOrKey) {
throw new Error("Project ID or key is required");
}
const projectIdOrKey = args.projectIdOrKey;
const count = args?.count && Number(args.count) > 0 && Number(args.count) <= 100
? Number(args.count)
: 20;
const offset = args?.offset && Number(args.offset) >= 0
? Number(args.offset)
: 0;
const sort = args?.sort || 'created';
const order = args?.order === 'asc' ? 'asc' : 'desc';
// Handle array parameters
const statusId = args?.statusId;
const assigneeId = args?.assigneeId;
const issues = await client.getIssues(projectIdOrKey, {
statusId,
assigneeId,
count,
offset,
sort,
order
});
return formatToolResponse("Project Issues", issues);
}
case "get_issue_detail": {
if (!args?.issueIdOrKey) {
throw new Error("Issue ID or key is required");
}
const issueIdOrKey = args.issueIdOrKey;
const issueDetail = await client.getIssue(issueIdOrKey);
return formatToolResponse("Issue Detail", issueDetail);
}
case "get_issue_comments": {
if (!args?.issueIdOrKey) {
throw new Error("Issue ID or key is required");
}
const issueIdOrKey = args.issueIdOrKey;
const minId = args?.minId !== undefined ? Number(args.minId) : undefined;
const maxId = args?.maxId !== undefined ? Number(args.maxId) : undefined;
const count = args?.count && Number(args.count) > 0 && Number(args.count) <= 100
? Number(args.count)
: 20;
const order = args?.order === 'asc' ? 'asc' : 'desc';
const comments = await client.getComments(issueIdOrKey, {
minId,
maxId,
count,
order
});
return formatToolResponse("Issue Comments", comments);
}
case "add_issue_comment": {
if (!args?.issueIdOrKey) {
throw new Error("Issue ID or key is required");
}
if (!args?.content) {
throw new Error("Comment content is required");
}
const issueIdOrKey = args.issueIdOrKey;
const content = args.content;
const comment = await client.addComment(issueIdOrKey, content);
return formatToolResponse("Added Comment", comment);
}
case "get_issue_comment_count": {
if (!args?.issueIdOrKey) {
throw new Error("Issue ID or key is required");
}
const issueIdOrKey = args.issueIdOrKey;
const commentCount = await client.getCommentCount(issueIdOrKey);
return formatToolResponse("Issue Comment Count", commentCount);
}
case "get_issue_comment": {
if (!args?.issueIdOrKey) {
throw new Error("Issue ID or key is required");
}
if (!args?.commentId) {
throw new Error("Comment ID is required");
}
const issueIdOrKey = args.issueIdOrKey;
const commentId = Number(args.commentId);
const comment = await client.getComment(issueIdOrKey, commentId);
return formatToolResponse("Issue Comment", comment);
}
case "get_wiki_page_list": {
const { projectIdOrKey, keyword } = args;
const wikiPages = await client.getWikiPageList(projectIdOrKey, keyword);
return formatToolResponse("Wiki Pages", wikiPages);
}
case "get_wiki_page": {
const { wikiId } = args;
if (!wikiId) {
throw new Error("Wiki ID is required");
}
const wikiPage = await client.getWikiPage(wikiId);
return formatToolResponse("Wiki Page", wikiPage);
}
case "update_wiki_page": {
const { wikiId, name, content, mailNotify } = args;
if (!wikiId) {
throw new Error("Wiki ID is required");
}
const updatedWikiPage = await client.updateWikiPage(wikiId, {
name,
content,
mailNotify
});
return formatToolResponse("Updated Wiki Page", updatedWikiPage);
}
default:
throw new Error("Unknown tool");
}
} catch (error) {
console.error(`Error executing tool ${toolName}:`, error);
throw error;
}
}
```