#
tokens: 9631/50000 10/10 files
lines: off (toggle) GitHub
raw markdown copy
# 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;
  }
}

```