# 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; } } ```