#
tokens: 48937/50000 22/95 files (page 2/3)
lines: off (toggle) GitHub
raw markdown copy
This is page 2 of 3. Use http://codebase.md/phuc-nt/mcp-atlassian-server?lines=false&page={x} to view the full context.

# Directory Structure

```
├── .env.example
├── .gitignore
├── .npmignore
├── assets
│   ├── atlassian_logo_icon.png
│   └── atlassian_logo_icon.webp
├── CHANGELOG.md
├── dev_mcp-atlassian-test-client
│   ├── package-lock.json
│   ├── package.json
│   ├── src
│   │   ├── list-mcp-inventory.ts
│   │   ├── test-confluence-pages.ts
│   │   ├── test-confluence-spaces.ts
│   │   ├── test-jira-issues.ts
│   │   ├── test-jira-projects.ts
│   │   ├── test-jira-users.ts
│   │   └── tool-test.ts
│   └── tsconfig.json
├── docker-compose.yml
├── Dockerfile
├── docs
│   ├── dev-guide
│   │   ├── advance-resource-tool-2.md
│   │   ├── advance-resource-tool-3.md
│   │   ├── advance-resource-tool.md
│   │   ├── confluence-migrate-to-v2.md
│   │   ├── github-community-exchange.md
│   │   ├── marketplace-publish-application-template.md
│   │   ├── marketplace-publish-guideline.md
│   │   ├── mcp-client-for-testing.md
│   │   ├── mcp-overview.md
│   │   ├── migrate-api-v2-to-v3.md
│   │   ├── mini-plan-refactor-tools.md
│   │   ├── modelcontextprotocol-architecture.md
│   │   ├── modelcontextprotocol-introduction.md
│   │   ├── modelcontextprotocol-resources.md
│   │   ├── modelcontextprotocol-tools.md
│   │   ├── one-click-setup.md
│   │   ├── prompts.md
│   │   ├── release-with-prebuild-bundle.md
│   │   ├── resource-metadata-schema-guideline.md
│   │   ├── resources.md
│   │   ├── sampling.md
│   │   ├── schema-metadata.md
│   │   ├── stdio-transport.md
│   │   ├── tool-vs-resource.md
│   │   ├── tools.md
│   │   └── workflow-examples.md
│   ├── introduction
│   │   ├── marketplace-submission.md
│   │   └── resources-and-tools.md
│   ├── knowledge
│   │   ├── 01-mcp-overview-architecture.md
│   │   ├── 02-mcp-tools-resources.md
│   │   ├── 03-mcp-prompts-sampling.md
│   │   ├── building-mcp-server.md
│   │   └── client-development-guide.md
│   ├── plan
│   │   ├── history.md
│   │   ├── roadmap.md
│   │   └── todo.md
│   └── test-reports
│       ├── cline-installation-test-2025-05-04.md
│       └── cline-test-2025-04-20.md
├── jest.config.js
├── LICENSE
├── llms-install-bundle.md
├── llms-install.md
├── package-lock.json
├── package.json
├── README.md
├── RELEASE_NOTES.md
├── smithery.yaml
├── src
│   ├── index.ts
│   ├── resources
│   │   ├── confluence
│   │   │   ├── index.ts
│   │   │   ├── pages.ts
│   │   │   └── spaces.ts
│   │   ├── index.ts
│   │   └── jira
│   │       ├── boards.ts
│   │       ├── dashboards.ts
│   │       ├── filters.ts
│   │       ├── index.ts
│   │       ├── issues.ts
│   │       ├── projects.ts
│   │       ├── sprints.ts
│   │       └── users.ts
│   ├── schemas
│   │   ├── common.ts
│   │   ├── confluence.ts
│   │   └── jira.ts
│   ├── tests
│   │   ├── confluence
│   │   │   └── create-page.test.ts
│   │   └── e2e
│   │       └── mcp-server.test.ts
│   ├── tools
│   │   ├── confluence
│   │   │   ├── add-comment.ts
│   │   │   ├── create-page.ts
│   │   │   ├── delete-footer-comment.ts
│   │   │   ├── delete-page.ts
│   │   │   ├── update-footer-comment.ts
│   │   │   ├── update-page-title.ts
│   │   │   └── update-page.ts
│   │   ├── index.ts
│   │   └── jira
│   │       ├── add-gadget-to-dashboard.ts
│   │       ├── add-issue-to-sprint.ts
│   │       ├── add-issues-to-backlog.ts
│   │       ├── assign-issue.ts
│   │       ├── close-sprint.ts
│   │       ├── create-dashboard.ts
│   │       ├── create-filter.ts
│   │       ├── create-issue.ts
│   │       ├── create-sprint.ts
│   │       ├── delete-filter.ts
│   │       ├── get-gadgets.ts
│   │       ├── rank-backlog-issues.ts
│   │       ├── remove-gadget-from-dashboard.ts
│   │       ├── start-sprint.ts
│   │       ├── transition-issue.ts
│   │       ├── update-dashboard.ts
│   │       ├── update-filter.ts
│   │       └── update-issue.ts
│   └── utils
│       ├── atlassian-api-base.ts
│       ├── confluence-interfaces.ts
│       ├── confluence-resource-api.ts
│       ├── confluence-tool-api.ts
│       ├── error-handler.ts
│       ├── jira-interfaces.ts
│       ├── jira-resource-api.ts
│       ├── jira-tool-api-agile.ts
│       ├── jira-tool-api-v3.ts
│       ├── jira-tool-api.ts
│       ├── logger.ts
│       ├── mcp-core.ts
│       └── mcp-helpers.ts
├── start-docker.sh
└── tsconfig.json
```

# Files

--------------------------------------------------------------------------------
/src/resources/confluence/spaces.ts:
--------------------------------------------------------------------------------

```typescript
import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
import { Logger } from '../../utils/logger.js';
import { getConfluenceSpacesV2, getConfluenceSpaceV2, getConfluencePagesWithFilters } from '../../utils/confluence-resource-api.js';
import { spacesListSchema, spaceSchema, pagesListSchema } from '../../schemas/confluence.js';
import { Config, Resources } from '../../utils/mcp-helpers.js';

const logger = Logger.getLogger('ConfluenceResource:Spaces');

/**
 * Register Confluence space-related resources
 * @param server MCP Server instance
 */
export function registerSpaceResources(server: McpServer) {
  logger.info('Registering Confluence space resources...');

  // Resource: List of spaces (API v2, cursor-based)
  server.resource(
    'confluence-spaces-list',
    new ResourceTemplate('confluence://spaces', {
      list: async (_extra) => {
        return {
          resources: [
            {
              uri: 'confluence://spaces',
              name: 'Confluence Spaces',
              description: 'List and search all Confluence spaces',
              mimeType: 'application/json'
            }
          ]
        };
      }
    }),
    async (uri, params, _extra) => {
      const config = Config.getAtlassianConfigFromEnv();
      const limit = params?.limit ? parseInt(Array.isArray(params.limit) ? params.limit[0] : params.limit, 10) : 25;
      const cursor = params?.cursor ? (Array.isArray(params.cursor) ? params.cursor[0] : params.cursor) : undefined;
      logger.info(`Getting Confluence spaces list (v2): cursor=${cursor}, limit=${limit}`);
      const data = await getConfluenceSpacesV2(config, cursor, limit);
      const uriString = typeof uri === 'string' ? uri : uri.href;
      // Chuẩn hóa metadata cho cursor-based
      const total = data.size ?? (data.results?.length || 0);
      const hasMore = !!(data._links && data._links.next);
      const nextCursor = hasMore ? (new URL(data._links.next, 'http://dummy').searchParams.get('cursor') || '') : undefined;
      const metadata = {
        total,
        limit,
        hasMore,
        links: {
          self: uriString,
          next: hasMore && nextCursor ? `${uriString}?cursor=${encodeURIComponent(nextCursor)}&limit=${limit}` : undefined
        }
      };
      // Chuẩn hóa trả về
      return Resources.createStandardResource(
        uriString,
        data.results,
        'spaces',
        spacesListSchema,
        total,
        limit,
        0,
        undefined
      );
    }
  );

  // Resource: Space details (API v2, mapping id)
  server.resource(
    'confluence-space-details',
    new ResourceTemplate('confluence://spaces/{spaceId}', {
      list: async (_extra) => ({
        resources: [
          {
            uri: 'confluence://spaces/{spaceId}',
            name: 'Confluence Space Details',
            description: 'Get details for a specific Confluence space by id. Replace {spaceId} với id số của space (ví dụ: 19464200).',
            mimeType: 'application/json'
          }
        ]
      })
    }),
    async (uri, params, _extra) => {
      const config = Config.getAtlassianConfigFromEnv();
      let normalizedSpaceId = Array.isArray(params.spaceId) ? params.spaceId[0] : params.spaceId;
      if (!normalizedSpaceId) throw new Error('Missing spaceId in URI');
      if (!/^\d+$/.test(normalizedSpaceId)) throw new Error('spaceId must be a number');
      logger.info(`Getting details for Confluence space (v2) by id: ${normalizedSpaceId}`);
      // Lấy thông tin space qua API helper (giả sử getConfluenceSpaceV2 hỗ trợ lookup theo id)
      const space = await getConfluenceSpaceV2(config, normalizedSpaceId);
      const uriString = typeof uri === 'string' ? uri : uri.href;
      return Resources.createStandardResource(
        uriString,
        [space],
        'space',
        spaceSchema,
        1,
        1,
        0,
        undefined
      );
    }
  );

  // Resource: List of pages in a space
  server.resource(
    'confluence-space-pages',
    new ResourceTemplate('confluence://spaces/{spaceId}/pages', {
      list: async (_extra) => ({
        resources: [
          {
            uri: 'confluence://spaces/{spaceId}/pages',
            name: 'Confluence Space Pages',
            description: 'List all pages in a specific Confluence space. Replace {spaceId} với id số của space.',
            mimeType: 'application/json'
          }
        ]
      })
    }),
    async (uri, params, _extra) => {
      const config = Config.getAtlassianConfigFromEnv();
      let normalizedSpaceId = Array.isArray(params.spaceId) ? params.spaceId[0] : params.spaceId;
      if (!normalizedSpaceId) throw new Error('Missing spaceId in URI');
      if (!/^\d+$/.test(normalizedSpaceId)) throw new Error('spaceId must be a number');
      // Không lookup theo key nữa, dùng trực tiếp id
      const filterParams = {
        'space-id': normalizedSpaceId,
        limit: params.limit ? parseInt(Array.isArray(params.limit) ? params.limit[0] : params.limit, 10) : 25
      };
      const data = await getConfluencePagesWithFilters(config, filterParams);
      const formattedPages = (data.results || []).map((page: any) => ({
        id: page.id,
        title: page.title,
        status: page.status,
        url: `${config.baseUrl}/wiki/pages/${page.id}`
      }));
      const uriString = typeof uri === 'string' ? uri : uri.href;
      return Resources.createStandardResource(
        uriString,
        formattedPages,
        'pages',
        pagesListSchema,
        data.size || formattedPages.length,
        filterParams.limit,
        0,
        undefined
      );
    }
  );
}

```

--------------------------------------------------------------------------------
/src/resources/jira/sprints.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Jira Sprint Resources
 * 
 * These resources provide access to Jira sprints through MCP.
 */

import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
import { sprintListSchema, sprintSchema, issuesListSchema } from '../../schemas/jira.js';
import { getSprintsByBoard, getSprintById, getSprintIssues } from '../../utils/jira-resource-api.js';
import { Logger } from '../../utils/logger.js';
import { Config, Resources } from '../../utils/mcp-helpers.js';

const logger = Logger.getLogger('JiraSprintResources');

/**
 * Register all Jira sprint resources with MCP Server
 * @param server MCP Server instance
 */
export function registerSprintResources(server: McpServer) {
  logger.info('Registering Jira sprint resources...');

  // Resource: Board sprints
  server.resource(
    'jira-board-sprints',
    new ResourceTemplate('jira://boards/{boardId}/sprints', { 
      list: async (_extra) => ({
        resources: [
          {
            uri: 'jira://boards/{boardId}/sprints',
            name: 'Jira Board Sprints',
            description: 'List all sprints in a Jira board. Replace {boardId} with the board ID.',
            mimeType: 'application/json'
          }
        ]
      }) 
    }),
    async (uri, params, _extra) => {
      try {
        const config = Config.getAtlassianConfigFromEnv();
        const boardId = Array.isArray(params.boardId) ? params.boardId[0] : params.boardId;
        const { limit, offset } = Resources.extractPagingParams(params);
        const response = await getSprintsByBoard(config, boardId, offset, limit);
        
        const uriString = typeof uri === 'string' ? uri : uri.href;
        return Resources.createStandardResource(
          uriString,
          response.values,
          'sprints',
          sprintListSchema,
          response.total || response.values.length,
          limit,
          offset,
          `${config.baseUrl}/jira/software/projects/browse/boards/${boardId}`
        );
      } catch (error) {
        logger.error(`Error getting sprints for board ${params.boardId}:`, error);
        throw error;
      }
    }
  );

  // Resource: Sprint details
  server.resource(
    'jira-sprint-details',
    new ResourceTemplate('jira://sprints/{sprintId}', {
      list: async (_extra) => ({
        resources: [
          {
            uri: 'jira://sprints/{sprintId}',
            name: 'Jira Sprint Details',
            description: 'Get details for a specific Jira sprint by ID. Replace {sprintId} with the sprint ID.',
            mimeType: 'application/json'
          }
        ]
      })
    }),
    async (uri, params, _extra) => {
      try {
        const config = Config.getAtlassianConfigFromEnv();
        const sprintId = Array.isArray(params.sprintId) ? params.sprintId[0] : params.sprintId;
        const sprint = await getSprintById(config, sprintId);
        
        const uriString = typeof uri === 'string' ? uri : uri.href;
        return Resources.createStandardResource(
          uriString,
          [sprint],
          'sprint',
          sprintSchema,
          1,
          1,
          0,
          `${config.baseUrl}/jira/software/projects/browse/boards/${sprint.originBoardId}/sprint/${sprintId}`
        );
      } catch (error) {
        logger.error(`Error getting sprint details for sprint ${params.sprintId}:`, error);
        throw error;
      }
    }
  );

  // Resource: Sprint issues
  server.resource(
    'jira-sprint-issues',
    new ResourceTemplate('jira://sprints/{sprintId}/issues', {
      list: async (_extra) => ({
        resources: [
          {
            uri: 'jira://sprints/{sprintId}/issues',
            name: 'Jira Sprint Issues',
            description: 'List issues in a Jira sprint. Replace {sprintId} with the sprint ID.',
            mimeType: 'application/json'
          }
        ]
      })
    }),
    async (uri, params, _extra) => {
      try {
        const config = Config.getAtlassianConfigFromEnv();
        const sprintId = Array.isArray(params.sprintId) ? params.sprintId[0] : params.sprintId;
        const { limit, offset } = Resources.extractPagingParams(params);
        const response = await getSprintIssues(config, sprintId, offset, limit);
        
        const uriString = typeof uri === 'string' ? uri : uri.href;
        return Resources.createStandardResource(
          uriString,
          response.issues,
          'issues',
          issuesListSchema,
          response.total || response.issues.length,
          limit,
          offset,
          `${config.baseUrl}/jira/software/projects/browse/issues/sprint/${sprintId}`
        );
      } catch (error) {
        logger.error(`Error getting issues for sprint ${params.sprintId}:`, error);
        throw error;
      }
    }
  );
  
  // Resource: All sprints
  server.resource(
    'jira-sprints',
    new ResourceTemplate('jira://sprints', {
      list: async (_extra) => ({
        resources: [
          {
            uri: 'jira://sprints',
            name: 'Jira Sprints',
            description: 'List and search all Jira sprints',
            mimeType: 'application/json'
          }
        ]
      })
    }),
    async (uri, _params, _extra) => {
      const uriString = typeof uri === 'string' ? uri : uri.href;
      return {
        contents: [{
          uri: uriString,
          mimeType: 'application/json',
          text: JSON.stringify({
            message: "Please use specific board sprints URI: jira://boards/{boardId}/sprints",
            suggestion: "To view sprints, first select a board using jira://boards, then access the board's sprints with jira://boards/{boardId}/sprints"
          })
        }]
      };
    }
  );

  logger.info('Jira sprint resources registered successfully');
}
```

--------------------------------------------------------------------------------
/docs/dev-guide/mini-plan-refactor-tools.md:
--------------------------------------------------------------------------------

```markdown
# Mini-Plan Để Refactoring Nhóm Tools

Dựa trên phân tích về sự khác biệt giữa cách tổ chức Resource và Tool trong codebase hiện tại, dưới đây là kế hoạch refactoring nhóm Tools để cải thiện tính nhất quán, khả năng bảo trì và tuân thủ guidelines.

## Giai Đoạn 1: Chuẩn Bị và Phân Tích

1. **Kiểm tra và phân loại tools hiện tại**
   - Phân loại tools theo chức năng (Jira/Confluence)
   - Xác định các tools thực sự cần thay đổi trạng thái (mutations) vs chỉ đọc (đã chuyển sang Resources)
   - Lập danh sách tools cần refactor

2. **Thiết lập unit tests**
   - Viết tests cho các tools hiện tại trước khi refactor
   - Đảm bảo coverage cho các use cases quan trọng

## Giai Đoạn 2: Thiết Kế Cấu Trúc Mới

3. **Chuẩn hóa cách đặt tên và tổ chức**
   ```
   /src
     /tools
       /jira
         /issue
           index.ts          # Tổng hợp đăng ký
           create.ts         # createIssue
           transition.ts     # transitionIssue
           assign.ts         # assignIssue
         /comment
           index.ts
           add.ts            # addComment
       /confluence
         /page
           index.ts
           create.ts         # createPage
           update.ts         # updatePage
       index.ts              # Đăng ký tập trung tất cả tools
     /utils
       tool-helpers.ts       # Các utility functions
   ```

4. **Tạo các helper functions chuẩn hóa**
   ```typescript
   // src/utils/tool-helpers.ts
   import { z } from 'zod';
   import { Logger } from './logger.js';

   const logger = Logger.getLogger('MCPTool');

   export function createToolResponse(text: string) {
     return {
       content: [{ type: 'text', text }]
     };
   }

   export function createErrorResponse(error: Error | string) {
     const message = error instanceof Error ? error.message : error;
     return {
       content: [{ type: 'text', text: `Error: ${message}` }],
       isError: true
     };
   }

   export function registerTool(server, name, description, schema, handler) {
     logger.info(`Registering tool: ${name}`);
     server.tool(name, schema, async (params, context) => {
       try {
         logger.debug(`Executing tool ${name} with params:`, params);
         const result = await handler(params, context);
         logger.debug(`Tool ${name} executed successfully`);
         return result;
       } catch (error) {
         logger.error(`Error in tool ${name}:`, error);
         return createErrorResponse(error);
       }
     });
   }
   ```

## Giai Đoạn 3: Triển Khai Refactoring

5. **Thực hiện refactoring theo từng nhóm nhỏ**
   - Bắt đầu với một nhóm tools (ví dụ: Jira issue tools)
   - Refactor từng tool một, chạy tests sau mỗi lần thay đổi

6. **Mẫu triển khai cho một tool cụ thể**
   ```typescript
   // src/tools/jira/issue/create.ts
   import { z } from 'zod';
   import { createToolResponse, createErrorResponse } from '../../../utils/tool-helpers.js';

   export const createIssueSchema = z.object({
     projectKey: z.string().describe('Project key (e.g., PROJ)'),
     summary: z.string().describe('Issue title/summary'),
     description: z.string().optional().describe('Detailed description'),
     issueType: z.string().default('Task').describe('Type of issue')
   });

   export async function createIssueHandler(params, context) {
     try {
       const config = context.get('atlassianConfig');
       if (!config) {
         throw new Error('Atlassian configuration not found');
       }

       // Logic tạo issue...
       
       return createToolResponse(`Issue ${newIssue.key} created successfully`);
     } catch (error) {
       return createErrorResponse(error);
     }
   }

   export function registerCreateIssueTool(server) {
     server.tool(
       'createIssue',
       createIssueSchema,
       createIssueHandler
     );
   }
   ```

7. **Tạo index.ts để đăng ký tập trung**
   ```typescript
   // src/tools/jira/issue/index.ts
   import { registerCreateIssueTool } from './create.js';
   import { registerTransitionIssueTool } from './transition.js';
   import { registerAssignIssueTool } from './assign.js';

   export function registerJiraIssueTools(server) {
     registerCreateIssueTool(server);
     registerTransitionIssueTool(server);
     registerAssignIssueTool(server);
   }
   ```

   ```typescript
   // src/tools/index.ts
   import { registerJiraIssueTools } from './jira/issue/index.js';
   import { registerJiraCommentTools } from './jira/comment/index.js';
   import { registerConfluencePageTools } from './confluence/page/index.js';

   export function registerAllTools(server) {
     registerJiraIssueTools(server);
     registerJiraCommentTools(server);
     registerConfluencePageTools(server);
   }
   ```

## Giai Đoạn 4: Đảm Bảo Chất Lượng và Hoàn Thiện

8. **Kiểm tra và tài liệu hóa**
   - Chạy tất cả unit tests
   - Cập nhật tài liệu phát triển
   - Thêm ví dụ cho từng tool

9. **Đồng bộ hóa với cấu trúc Resource**
   - Đảm bảo sự nhất quán giữa naming và pattern giữa Tools và Resources
   - Xác minh không có trùng lặp chức năng giữa hai nhóm

10. **Đánh giá và optimizing**
    - Xác định các patterns chung và cơ hội trừu tượng hóa
    - Kiểm tra hiệu suất nếu cần

## Best Practices Khi Refactoring Tools

- **Duy trì tính atom**: Mỗi tool chỉ thực hiện một nhiệm vụ cụ thể (nguyên tắc "do one thing well")
- **Input validation**: Sử dụng Zod schemas chi tiết và rõ ràng cho tất cả tham số
- **Error handling**: Xử lý lỗi nhất quán trong tất cả các tools
- **Logging**: Log đầy đủ thông tin hoạt động của tools cho debug
- **Documentation**: Tài liệu hóa rõ ràng mục đích, tham số và kết quả mong đợi
- **Testing**: Unit tests đầy đủ cho mỗi tool

Approach này sẽ giúp cải thiện tính nhất quán, khả năng bảo trì và mở rộng của nhóm Tools, đồng thời duy trì sự rõ ràng về phân chia trách nhiệm giữa Tools và Resources theo đúng guidelines của MCP. 
```

--------------------------------------------------------------------------------
/src/resources/jira/dashboards.ts:
--------------------------------------------------------------------------------

```typescript
import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
import { getDashboards, getMyDashboards, getDashboardById, getDashboardGadgets } from '../../utils/jira-resource-api.js';
import { Logger } from '../../utils/logger.js';
import { dashboardSchema, dashboardListSchema, gadgetListSchema } from '../../schemas/jira.js';
import { Config, Resources } from '../../utils/mcp-helpers.js';

const logger = Logger.getLogger('JiraDashboardResources');

// (Có thể bổ sung schema dashboardSchema, gadgetsSchema nếu cần)

export function registerDashboardResources(server: McpServer) {
  logger.info('Registering Jira dashboard resources...');

  // List all dashboards
  server.resource(
    'jira-dashboards',
    new ResourceTemplate('jira://dashboards', {
      list: async (_extra) => {
        return {
          resources: [
            {
              uri: 'jira://dashboards',
              name: 'Jira Dashboards',
              description: 'List and search all Jira dashboards',
              mimeType: 'application/json'
            }
          ]
        };
      }
    }),
    async (uri: string | URL, params: Record<string, any>, extra: any) => {
      try {
        // Get config from context or environment
        const config = Config.getAtlassianConfigFromEnv();
        const uriStr = typeof uri === 'string' ? uri : uri.href;
        
        const { limit, offset } = Resources.extractPagingParams(params);
        const data = await getDashboards(config, offset, limit);
        return Resources.createStandardResource(
          uriStr,
          data.dashboards || [],
          'dashboards',
          dashboardListSchema,
          data.total || (data.dashboards ? data.dashboards.length : 0),
          limit,
          offset,
          `${config.baseUrl}/jira/dashboards` // UI URL
        );
      } catch (error) {
        logger.error(`Error handling resource request for jira-dashboards:`, error);
        throw error;
      }
    }
  );

  // List my dashboards
  server.resource(
    'jira-my-dashboards',
    new ResourceTemplate('jira://dashboards/my', {
      list: async (_extra) => ({
        resources: [
          {
            uri: 'jira://dashboards/my',
            name: 'Jira My Dashboards',
            description: 'List dashboards owned by or shared with the current user.',
            mimeType: 'application/json'
          }
        ]
      })
    }),
    async (uri: string | URL, params: Record<string, any>, extra: any) => {
      try {
        // Get config from context or environment
        const config = Config.getAtlassianConfigFromEnv();
        const uriStr = typeof uri === 'string' ? uri : uri.href;
        
        const { limit, offset } = Resources.extractPagingParams(params);
        const data = await getMyDashboards(config, offset, limit);
        return Resources.createStandardResource(
          uriStr,
          data.dashboards || [],
          'dashboards',
          dashboardListSchema,
          data.total || (data.dashboards ? data.dashboards.length : 0),
          limit,
          offset,
          `${config.baseUrl}/jira/dashboards?filter=my`
        );
      } catch (error) {
        logger.error(`Error handling resource request for jira-my-dashboards:`, error);
        throw error;
      }
    }
  );

  // Dashboard details
  server.resource(
    'jira-dashboard-details',
    new ResourceTemplate('jira://dashboards/{dashboardId}', {
      list: async (_extra) => ({
        resources: [
          {
            uri: 'jira://dashboards/{dashboardId}',
            name: 'Jira Dashboard Details',
            description: 'Get details of a specific Jira dashboard.',
            mimeType: 'application/json'
          }
        ]
      })
    }),
    async (uri: string | URL, params: Record<string, any>, extra: any) => {
      try {
        // Get config from context or environment
        const config = Config.getAtlassianConfigFromEnv();
        const uriStr = typeof uri === 'string' ? uri : uri.href;
        
        const dashboardId = params.dashboardId || (uriStr.split('/').pop());
        const dashboard = await getDashboardById(config, dashboardId);
        return Resources.createStandardResource(
          uriStr,
          [dashboard],
          'dashboard',
          dashboardSchema,
          1,
          1,
          0,
          `${config.baseUrl}/jira/dashboards/${dashboardId}`
        );
      } catch (error) {
        logger.error(`Error handling resource request for jira-dashboard-details:`, error);
        throw error;
      }
    }
  );

  // Dashboard gadgets
  server.resource(
    'jira-dashboard-gadgets',
    new ResourceTemplate('jira://dashboards/{dashboardId}/gadgets', {
      list: async (_extra) => ({
        resources: [
          {
            uri: 'jira://dashboards/{dashboardId}/gadgets',
            name: 'Jira Dashboard Gadgets',
            description: 'List gadgets of a specific Jira dashboard.',
            mimeType: 'application/json'
          }
        ]
      })
    }),
    async (uri: string | URL, params: Record<string, any>, extra: any) => {
      try {
        // Get config from context or environment
        const config = Config.getAtlassianConfigFromEnv();
        const uriStr = typeof uri === 'string' ? uri : uri.href;
        
        const dashboardId = params.dashboardId || (uriStr.split('/')[uriStr.split('/').length - 2]);
        const gadgets = await getDashboardGadgets(config, dashboardId);
        return Resources.createStandardResource(
          uriStr,
          gadgets,
          'gadgets',
          gadgetListSchema,
          gadgets.length,
          gadgets.length,
          0,
          `${config.baseUrl}/jira/dashboards/${dashboardId}`
        );
      } catch (error) {
        logger.error(`Error handling resource request for jira-dashboard-gadgets:`, error);
        throw error;
      }
    }
  );

  logger.info('Jira dashboard resources registered successfully');
}
```

--------------------------------------------------------------------------------
/src/tools/confluence/update-page.ts:
--------------------------------------------------------------------------------

```typescript
import { z } from 'zod';
import { callConfluenceApi } from '../../utils/atlassian-api-base.js';
import { AtlassianConfig } from '../../utils/atlassian-api-base.js';
import { ApiError, ApiErrorType } from '../../utils/error-handler.js';
import { Logger } from '../../utils/logger.js';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { McpResponse, createSuccessResponse, createErrorResponse } from '../../utils/mcp-core.js';
import { updateConfluencePageV2 } from '../../utils/confluence-tool-api.js';
import { Config } from '../../utils/mcp-helpers.js';

// Initialize logger
const logger = Logger.getLogger('ConfluenceTools:updatePage');

// Input parameter schema
export const updatePageSchema = z.object({
  pageId: z.string().describe('ID of the page to update'),
  title: z.string().optional().describe('New title of the page'),
  content: z.string().optional().describe(`New content of the page (Confluence storage format only, XML-like HTML).

- Plain text or markdown is NOT supported (will throw error).
- Only XML-like HTML tags, Confluence macros (<ac:structured-macro>, <ac:rich-text-body>, ...), tables, panels, info, warning, etc. are supported if valid storage format.
- Content MUST strictly follow Confluence storage format.

Valid examples:
- <p>This is a paragraph</p>
- <ac:structured-macro ac:name="info"><ac:rich-text-body>Information</ac:rich-text-body></ac:structured-macro>
`),
  version: z.number().describe('Current version number of the page (required to avoid conflicts)')
});

type UpdatePageParams = z.infer<typeof updatePageSchema>;

interface UpdatePageResult {
  id: string;
  title: string;
  version: number;
  self: string;
  webui: string;
  success: boolean;
  message: string;
}

// Main handler to update a page (API v2)
export async function updatePageHandler(
  params: UpdatePageParams,
  config: AtlassianConfig
): Promise<UpdatePageResult> {
  try {
    logger.info(`Updating page (v2) with ID: ${params.pageId}`);
    // Lấy version, title, content hiện tại nếu thiếu
    const baseUrl = config.baseUrl.endsWith('/wiki') ? config.baseUrl : `${config.baseUrl}/wiki`;
    const auth = Buffer.from(`${config.email}:${config.apiToken}`).toString('base64');
    const headers = {
      'Authorization': `Basic ${auth}`,
      'Content-Type': 'application/json',
      'Accept': 'application/json',
      'User-Agent': 'MCP-Atlassian-Server/1.0.0'
    };
    const url = `${baseUrl}/api/v2/pages/${encodeURIComponent(params.pageId)}`;
    const res = await fetch(url, { method: 'GET', headers, credentials: 'omit' });
    if (!res.ok) throw new Error(`Failed to get page info: ${params.pageId}`);
    const pageData = await res.json();
    let version = pageData.version.number + 1;
    let title = params.title ?? pageData.title;
    let content = params.content;
    if (!title) throw new Error('Missing title for page update');
    if (!content) {
      // Lấy body hiện tại nếu không truyền content
      const bodyRes = await fetch(`${url}/body`, { method: 'GET', headers, credentials: 'omit' });
      if (!bodyRes.ok) throw new Error(`Failed to get page body: ${params.pageId}`);
      const bodyData = await bodyRes.json();
      content = bodyData.value;
      if (!content) throw new Error('Missing content for page update');
    }
    // Gọi helper updateConfluencePageV2 với đủ trường
    const data = await updateConfluencePageV2(config, {
      pageId: params.pageId,
      title,
      content,
      version
    });
    return {
      id: data.id,
      title: data.title,
      version: data.version.number,
      self: data._links?.self || '',
      webui: data._links?.webui || '',
      success: true,
      message: 'Successfully updated page'
    };
  } catch (error) {
    if (error instanceof ApiError) {
      throw error;
    }
    logger.error(`Error updating page (v2) with ID ${params.pageId}:`, error);
    let message = `Failed to update page: ${error instanceof Error ? error.message : String(error)}`;
    throw new ApiError(
      ApiErrorType.SERVER_ERROR,
      message,
      500
    );
  }
}

// Register the tool with MCP Server
export const registerUpdatePageTool = (server: McpServer) => {
  server.tool(
    'updatePage',
    'Update the content and information of a Confluence page',
    updatePageSchema.shape,
    async (params: UpdatePageParams, context: Record<string, any>) => {
      try {
        const config = context?.atlassianConfig ?? Config.getAtlassianConfigFromEnv();
        if (!config) {
          return {
            content: [
              { type: 'text', text: 'Invalid or missing Atlassian configuration' }
            ],
            isError: true
          };
        }
        const result = await updatePageHandler(params, config);
        return {
          content: [
            {
              type: 'text',
              text: JSON.stringify({
                success: true,
                message: result.message,
                id: result.id,
                title: result.title,
                version: result.version,
                url: `${config.baseUrl}/wiki${result.webui}`
              })
            }
          ]
        };
      } catch (error) {
        if (error instanceof ApiError) {
          return {
            content: [
              {
                type: 'text',
                text: JSON.stringify({
                  success: false,
                  message: error.message,
                  code: error.code,
                  statusCode: error.statusCode,
                  type: error.type
                })
              }
            ],
            isError: true
          };
        }
        return {
          content: [
            {
              type: 'text',
              text: JSON.stringify({
                success: false,
                message: `Error while updating page: ${error instanceof Error ? error.message : String(error)}`
              })
            }
          ],
          isError: true
        };
      }
    }
  );
}; 
```

--------------------------------------------------------------------------------
/src/resources/jira/boards.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Jira Board Resources
 * 
 * These resources provide access to Jira boards through MCP.
 */

import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
import { boardListSchema, boardSchema, issuesListSchema } from '../../schemas/jira.js';
import { getBoards, getBoardById, getBoardIssues } from '../../utils/jira-resource-api.js';
import { Logger } from '../../utils/logger.js';
import { Config, Resources } from '../../utils/mcp-helpers.js';

const logger = Logger.getLogger('JiraBoardResources');

/**
 * Register all Jira board resources with MCP Server
 * @param server MCP Server instance
 */
export function registerBoardResources(server: McpServer) {
  logger.info('Registering Jira board resources...');
  
  // Resource: Board list
  server.resource(
    'jira-boards',
    new ResourceTemplate('jira://boards', {
      list: async (_extra) => {
        return {
          resources: [
            {
              uri: 'jira://boards',
              name: 'Jira Boards',
              description: 'List and search all Jira boards',
              mimeType: 'application/json'
            }
          ]
        };
      }
    }),
    async (uri, params, _extra) => {
      try {
        const config = Config.getAtlassianConfigFromEnv();
        const { limit, offset } = Resources.extractPagingParams(params);
        const response = await getBoards(config, offset, limit);
        
        const uriString = typeof uri === 'string' ? uri : uri.href;
        return Resources.createStandardResource(
          uriString,
          response.values,
          'boards',
          boardListSchema,
          response.total || response.values.length,
          limit,
          offset,
          `${config.baseUrl}/jira/boards`
        );
      } catch (error) {
        logger.error('Error getting board list:', error);
        throw error;
      }
    }
  );

  // Resource: Board details
  server.resource(
    'jira-board-details',
    new ResourceTemplate('jira://boards/{boardId}', {
      list: async (_extra) => ({
        resources: [
          {
            uri: 'jira://boards/{boardId}',
            name: 'Jira Board Details',
            description: 'Get details for a specific Jira board by ID. Replace {boardId} with the board ID.',
            mimeType: 'application/json'
          }
        ]
      })
    }),
    async (uri, params, _extra) => {
      try {
        const config = Config.getAtlassianConfigFromEnv();
        const boardId = Array.isArray(params.boardId) ? params.boardId[0] : params.boardId;
        const board = await getBoardById(config, boardId);
        
        const uriString = typeof uri === 'string' ? uri : uri.href;
        return Resources.createStandardResource(
          uriString,
          [board],
          'board',
          boardSchema,
          1,
          1,
          0,
          `${config.baseUrl}/jira/software/projects/${board.location?.projectKey || 'browse'}/boards/${boardId}`
        );
      } catch (error) {
        logger.error(`Error getting board details for board ${params.boardId}:`, error);
        throw error;
      }
    }
  );

  // Resource: Issues in board
  server.resource(
    'jira-board-issues',
    new ResourceTemplate('jira://boards/{boardId}/issues', {
      list: async (_extra) => ({
        resources: [
          {
            uri: 'jira://boards/{boardId}/issues',
            name: 'Jira Board Issues',
            description: 'List issues in a Jira board. Replace {boardId} with the board ID.',
            mimeType: 'application/json'
          }
        ]
      })
    }),
    async (uri, params, _extra) => {
      try {
        const config = Config.getAtlassianConfigFromEnv();
        const boardId = Array.isArray(params.boardId) ? params.boardId[0] : params.boardId;
        const { limit, offset } = Resources.extractPagingParams(params);
        const response = await getBoardIssues(config, boardId, offset, limit);
        
        const uriString = typeof uri === 'string' ? uri : uri.href;
        return Resources.createStandardResource(
          uriString,
          response.issues,
          'issues',
          issuesListSchema,
          response.total || response.issues.length,
          limit,
          offset,
          `${config.baseUrl}/jira/software/projects/browse/boards/${boardId}`
        );
      } catch (error) {
        logger.error(`Error getting issues for board ${params.boardId}:`, error);
        throw error;
      }
    }
  );

  // Resource: Board configuration
  server.resource(
    'jira-board-configuration',
    new ResourceTemplate('jira://boards/{boardId}/configuration', {
      list: async (_extra) => ({
        resources: [
          {
            uri: 'jira://boards/{boardId}/configuration',
            name: 'Jira Board Configuration',
            description: 'Get configuration of a specific Jira board. Replace {boardId} with the board ID.',
            mimeType: 'application/json'
          }
        ]
      })
    }),
    async (uri, params, _extra) => {
      try {
        const config = Config.getAtlassianConfigFromEnv();
        const boardId = Array.isArray(params.boardId) ? params.boardId[0] : params.boardId;
        // Gọi API lấy cấu hình board
        const response = await fetch(`${config.baseUrl}/rest/agile/1.0/board/${boardId}/configuration`, {
          method: 'GET',
          headers: {
            'Authorization': `Basic ${Buffer.from(`${config.email}:${config.apiToken}`).toString('base64')}`,
            'Accept': 'application/json',
            'Content-Type': 'application/json',
          },
        });
        if (!response.ok) throw new Error(`Jira API error: ${response.status} ${await response.text()}`);
        const configData = await response.json();
        
        const uriString = typeof uri === 'string' ? uri : uri.href;
        // Inline schema (mô tả cơ bản, không validate sâu)
        const boardConfigurationSchema = {
          type: 'object',
          properties: {
            id: { type: 'number' },
            name: { type: 'string' },
            type: { type: 'string' },
            self: { type: 'string' },
            location: { type: 'object' },
            filter: { type: 'object' },
            subQuery: { type: 'object' },
            columnConfig: { type: 'object' },
            estimation: { type: 'object' },
            ranking: { type: 'object' }
          },
          required: ['id', 'name', 'type', 'self', 'columnConfig']
        };
        return {
          contents: [{
            uri: uriString,
            mimeType: 'application/json',
            text: JSON.stringify(configData),
            schema: boardConfigurationSchema
          }]
        };
      } catch (error) {
        logger.error(`Error getting board configuration for board ${params.boardId}:`, error);
        throw error;
      }
    }
  );
  
  logger.info('Jira board resources registered successfully');
}
```

--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------

```typescript
import dotenv from 'dotenv';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { registerAllTools } from './tools/index.js';
import { registerAllResources } from './resources/index.js';
import { Logger } from './utils/logger.js';
import { AtlassianConfig } from './utils/atlassian-api-base.js';

// Load environment variables
dotenv.config();

// Initialize logger
const logger = Logger.getLogger('MCP:Server');

// Get Atlassian config from environment variables
const ATLASSIAN_SITE_NAME = process.env.ATLASSIAN_SITE_NAME;
const ATLASSIAN_USER_EMAIL = process.env.ATLASSIAN_USER_EMAIL;
const ATLASSIAN_API_TOKEN = process.env.ATLASSIAN_API_TOKEN;

if (!ATLASSIAN_SITE_NAME || !ATLASSIAN_USER_EMAIL || !ATLASSIAN_API_TOKEN) {
  logger.error('Missing Atlassian credentials in environment variables');
  process.exit(1);
}

// Create Atlassian config
const atlassianConfig: AtlassianConfig = {
  baseUrl: ATLASSIAN_SITE_NAME.includes('.atlassian.net') 
    ? `https://${ATLASSIAN_SITE_NAME}` 
    : ATLASSIAN_SITE_NAME,
  email: ATLASSIAN_USER_EMAIL,
  apiToken: ATLASSIAN_API_TOKEN
};

logger.info('Initializing MCP Atlassian Server...');

// Track registered resources for logging
const registeredResources: Array<{ name: string; pattern: string }> = [];

// Initialize MCP server with capabilities
const server = new McpServer({
  name: process.env.MCP_SERVER_NAME || 'phuc-nt/mcp-atlassian-server',
  version: process.env.MCP_SERVER_VERSION || '1.0.0',
  capabilities: {
    resources: {},  // Declare support for resources capability
    tools: {}
  }
});

// Create a context-aware server proxy for resources
const serverProxy = new Proxy(server, {
  get(target, prop) {
    if (prop === 'resource') {
      // Override the resource method to inject context
      return (name: string, pattern: any, handler: any) => {
        try {
          // Extract pattern for logging
          let patternStr = 'unknown-pattern';
          
          if (typeof pattern === 'string') {
            patternStr = pattern;
          } else if (pattern && typeof pattern === 'object') {
            if ('pattern' in pattern) {
              patternStr = pattern.pattern;
            }
          }
          
          // Track registered resources for logging purposes only
          registeredResources.push({ name, pattern: patternStr });
          
          // Create a context-aware handler wrapper
          const contextAwareHandler = async (uri: any, params: any, extra: any) => {
            try {
              // Ensure extra has context
              if (!extra) extra = {};
              if (!extra.context) extra.context = {};
              
              // Add Atlassian config to context
              extra.context.atlassianConfig = atlassianConfig;
              
              // Call the original handler with the enriched context
              return await handler(uri, params, extra);
            } catch (error) {
              logger.error(`Error in resource handler for ${name}:`, error);
              throw error;
            }
          };
          
          // Register the resource with the context-aware handler
          return target.resource(name, pattern, contextAwareHandler);
        } catch (error) {
          logger.error(`Error registering resource: ${error}`);
          throw error;
        }
      };
    }
    return Reflect.get(target, prop);
  }
});

// Log config info for debugging
logger.info(`Atlassian config available: ${JSON.stringify(atlassianConfig, null, 2)}`);

// Tool server proxy for consistent handling
const toolServerProxy: any = {
  tool: (name: string, description: string, schema: any, handler: any) => {
    // Register tool with a context-aware handler wrapper
    server.tool(name, description, schema, async (params: any, context: any) => {
      // Add Atlassian config to context
      context.atlassianConfig = atlassianConfig;
      
      logger.debug(`Tool ${name} called with context keys: [${Object.keys(context)}]`);
      
      try {
        return await handler(params, context);
      } catch (error) {
        logger.error(`Error in tool handler for ${name}:`, error);
        return {
          content: [{ type: 'text', text: `Error in tool handler: ${error instanceof Error ? error.message : String(error)}` }],
          isError: true
        };
      }
    });
  }
};

// Register all tools
logger.info('Registering all MCP Tools...');
registerAllTools(toolServerProxy);

// Register all resources
logger.info('Registering MCP Resources...');
registerAllResources(serverProxy);

// Start the server based on configured transport type
async function startServer() {
  try {
    // Always use STDIO transport for highest reliability
    const stdioTransport = new StdioServerTransport();
    await server.connect(stdioTransport);
    logger.info('MCP Atlassian Server started with STDIO transport');
    
    // Print startup info
    logger.info(`MCP Server Name: ${process.env.MCP_SERVER_NAME || 'phuc-nt/mcp-atlassian-server'}`);
    logger.info(`MCP Server Version: ${process.env.MCP_SERVER_VERSION || '1.0.0'}`);
    logger.info(`Connected to Atlassian site: ${ATLASSIAN_SITE_NAME}`);
    
    logger.info('Registered tools:');
    // Liệt kê tất cả các tool đã đăng ký
    logger.info('- Jira issue tools: createIssue, updateIssue, transitionIssue, assignIssue');
    logger.info('- Jira filter tools: createFilter, updateFilter, deleteFilter');
    logger.info('- Jira sprint tools: createSprint, startSprint, closeSprint, addIssueToSprint');
    logger.info('- Jira board tools: addIssueToBoard, configureBoardColumns');
    logger.info('- Jira backlog tools: addIssuesToBacklog, rankBacklogIssues');
    logger.info('- Jira dashboard tools: createDashboard, updateDashboard, addGadgetToDashboard, removeGadgetFromDashboard');
    logger.info('- Confluence tools: createPage, updatePage, addComment, addLabelsToPage, removeLabelsFromPage');
    
    // Resources - dynamically list all registered resources
    logger.info('Registered resources:');
    
    if (registeredResources.length === 0) {
      logger.info('No resources registered');
    } else {
      // Group by pattern and name to improve readability
      const resourcesByPattern = new Map<string, string[]>();
      
      registeredResources.forEach(res => {
        if (!resourcesByPattern.has(res.pattern)) {
          resourcesByPattern.set(res.pattern, []);
        }
        resourcesByPattern.get(res.pattern)!.push(res.name);
      });
      
      // Log each pattern with its resource names
      Array.from(resourcesByPattern.entries())
        .sort((a, b) => a[0].localeCompare(b[0]))
        .forEach(([pattern, names]) => {
          logger.info(`- ${pattern} (${names.join(', ')})`);
        });
    }
  } catch (error) {
    logger.error('Failed to start MCP Server:', error);
    process.exit(1);
  }
}

// Start server
startServer(); 
```

--------------------------------------------------------------------------------
/src/schemas/confluence.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Schema definitions for Confluence resources
 */
import { standardMetadataSchema } from './common.js';
import { z } from 'zod';

/**
 * Schema for Confluence space
 */
export const spaceSchema = {
  type: "object",
  properties: {
    key: { type: "string", description: "Space key" },
    name: { type: "string", description: "Space name" },
    type: { type: "string", description: "Space type (global, personal, etc.)" },
    status: { type: "string", description: "Space status" },
    url: { type: "string", description: "Space URL" }
  },
  required: ["key", "name", "type"]
};

/**
 * Schema for Confluence spaces list
 */
export const spacesListSchema = {
  type: "object",
  properties: {
    metadata: standardMetadataSchema,
    spaces: {
      type: "array",
      items: spaceSchema
    }
  },
  required: ["metadata", "spaces"]
};

/**
 * Schema for Confluence page
 */
export const pageSchema = {
  type: "object",
  properties: {
    id: { type: "string", description: "Page ID" },
    title: { type: "string", description: "Page title" },
    status: { type: "string", description: "Page status" },
    spaceId: { type: "string", description: "Space ID" },
    parentId: { type: "string", description: "Parent page ID", nullable: true },
    authorId: { type: "string", description: "Author ID", nullable: true },
    createdAt: { type: "string", description: "Creation date" },
    version: {
      type: "object",
      properties: {
        number: { type: "number", description: "Version number" },
        createdAt: { type: "string", description: "Version creation date" }
      }
    },
    body: { type: "string", description: "Page content (Confluence storage format)" },
    bodyType: { type: "string", description: "Content representation type" },
    _links: { type: "object", description: "Links related to the page" }
  },
  required: ["id", "title", "status", "spaceId", "createdAt", "version", "body", "bodyType", "_links"]
};

/**
 * Schema for Confluence pages list
 */
export const pagesListSchema = {
  type: "object",
  properties: {
    metadata: standardMetadataSchema,
    pages: {
      type: "array",
      items: pageSchema
    },
    spaceKey: { type: "string", description: "Space key", nullable: true }
  },
  required: ["metadata", "pages"]
};

/**
 * Schema for Confluence comment
 */
export const commentSchema = {
  type: "object",
  properties: {
    id: { type: "string", description: "Comment ID" },
    pageId: { type: "string", description: "Page ID" },
    body: { type: "string", description: "Comment content (HTML)" },
    bodyType: { type: "string", description: "Content representation type" },
    createdAt: { type: "string", format: "date-time", description: "Creation date" },
    createdBy: {
      type: "object",
      properties: {
        accountId: { type: "string", description: "Author's account ID" },
        displayName: { type: "string", description: "Author's display name" }
      }
    },
    _links: { type: "object", description: "Links related to the comment" }
  },
  required: ["id", "pageId", "body", "createdAt", "createdBy"]
};

/**
 * Schema for Confluence comments list
 */
export const commentsListSchema = {
  type: "object",
  properties: {
    metadata: standardMetadataSchema,
    comments: {
      type: "array",
      items: commentSchema
    },
    pageId: { type: "string", description: "Page ID" }
  },
  required: ["metadata", "comments", "pageId"]
};

/**
 * Schema for Confluence search results
 */
export const searchResultSchema = {
  type: "object",
  properties: {
    id: { type: "string", description: "Content ID" },
    title: { type: "string", description: "Content title" },
    type: { type: "string", description: "Content type (page, blogpost, etc.)" },
    spaceKey: { type: "string", description: "Space key" },
    url: { type: "string", description: "Content URL" },
    excerpt: { type: "string", description: "Content excerpt with highlights" }
  },
  required: ["id", "title", "type", "spaceKey"]
};

/**
 * Schema for Confluence search results list
 */
export const searchResultsListSchema = {
  type: "object",
  properties: {
    metadata: standardMetadataSchema,
    results: {
      type: "array",
      items: searchResultSchema
    },
    cql: { type: "string", description: "CQL query used for the search" }
  },
  required: ["metadata", "results"]
};

// Label schemas
export const labelSchema = {
  type: "object",
  properties: {
    id: { type: "string", description: "Label ID" },
    name: { type: "string", description: "Label name" },
    prefix: { type: "string", description: "Label prefix" }
  }
};

export const labelListSchema = {
  type: "object",
  properties: {
    labels: {
      type: "array",
      items: labelSchema
    },
    metadata: standardMetadataSchema
  }
};

// Attachment schemas
export const attachmentSchema = {
  type: "object",
  properties: {
    id: { type: "string", description: "Attachment ID" },
    title: { type: "string", description: "Attachment title" },
    filename: { type: "string", description: "File name" },
    mediaType: { type: "string", description: "Media type" },
    fileSize: { type: "number", description: "File size in bytes" },
    downloadUrl: { type: "string", description: "Download URL" }
  }
};

export const attachmentListSchema = {
  type: "object",
  properties: {
    attachments: {
      type: "array",
      items: attachmentSchema
    },
    metadata: standardMetadataSchema
  }
};

// Version schemas
export const versionSchema = {
  type: "object",
  properties: {
    number: { type: "number", description: "Version number" },
    by: { 
      type: "object", 
      properties: {
        displayName: { type: "string" },
        accountId: { type: "string" }
      }
    },
    when: { type: "string", description: "Creation date" },
    message: { type: "string", description: "Version message" }
  }
};

export const versionListSchema = {
  type: "object",
  properties: {
    versions: {
      type: "array",
      items: versionSchema
    },
    metadata: standardMetadataSchema
  }
};

export const createPageSchema = z.object({
  spaceId: z.string().describe('Space ID (required, must be the numeric ID from API v2, NOT the key like TX, DEV, ...)'),
  title: z.string().describe('Title of the page (required)'),
  content: z.string().describe(`Content of the page (required, must be in Confluence storage format - XML-like HTML).

- Plain text or markdown is NOT supported (will throw error).
- Only XML-like HTML tags, Confluence macros (<ac:structured-macro>, <ac:rich-text-body>, ...), tables, panels, info, warning, etc. are supported if valid storage format.
- Content MUST strictly follow Confluence storage format.

Valid examples:
- <p>This is a paragraph</p>
- <ac:structured-macro ac:name="info"><ac:rich-text-body>Information</ac:rich-text-body></ac:structured-macro>
`),
  parentId: z.string().describe('Parent page ID (required, must specify the parent page to create a child page)')
}); 
```

--------------------------------------------------------------------------------
/docs/dev-guide/advance-resource-tool-3.md:
--------------------------------------------------------------------------------

```markdown
# Hướng Dẫn Bổ Sung Resource và Tool cho Confluence API v2

Dựa trên tài liệu API v2 của Confluence, dưới đây là hướng dẫn bổ sung các resource và tool mới cho MCP server của bạn.

## Bổ Sung Resource

### 1. Blog Posts

| Resource | URI | Mô tả | Atlassian API Endpoint | Dữ liệu trả về |
|----------|-----|-------|-----------------------|----------------|
| Blog Posts | `confluence://blogposts` | Danh sách bài viết blog | `/wiki/api/v2/blogposts` | Array của BlogPost objects |
| Blog Post Details | `confluence://blogposts/{blogpostId}` | Chi tiết bài viết blog | `/wiki/api/v2/blogposts/{blogpostId}` | Single BlogPost object |
| Blog Post Labels | `confluence://blogposts/{blogpostId}/labels` | Nhãn của bài viết blog | `/wiki/api/v2/blogposts/{blogpostId}/labels` | Array của Label objects |
| Blog Post Versions | `confluence://blogposts/{blogpostId}/versions` | Lịch sử phiên bản | `/wiki/api/v2/blogposts/{blogpostId}/versions` | Array của Version objects |

### 2. Comments

| Resource | URI | Mô tả | Atlassian API Endpoint | Dữ liệu trả về |
|----------|-----|-------|-----------------------|----------------|
| Page Comments | `confluence://pages/{pageId}/comments` | Danh sách bình luận của trang | `/wiki/api/v2/pages/{pageId}/comments` | Array của Comment objects |
| Blog Comments | `confluence://blogposts/{blogpostId}/comments` | Danh sách bình luận của blog | `/wiki/api/v2/blogposts/{blogpostId}/comments` | Array của Comment objects |
| Comment Details | `confluence://comments/{commentId}` | Chi tiết bình luận | `/wiki/api/v2/comments/{commentId}` | Single Comment object |

### 3. Watchers

| Resource | URI | Mô tả | Atlassian API Endpoint | Dữ liệu trả về |
|----------|-----|-------|-----------------------|----------------|
| Page Watchers | `confluence://pages/{pageId}/watchers` | Người theo dõi trang | `/wiki/api/v2/pages/{pageId}/watchers` | Array của Watcher objects |
| Space Watchers | `confluence://spaces/{spaceId}/watchers` | Người theo dõi không gian | `/wiki/api/v2/spaces/{spaceId}/watchers` | Array của Watcher objects |
| Blog Watchers | `confluence://blogposts/{blogpostId}/watchers` | Người theo dõi blog | `/wiki/api/v2/blogposts/{blogpostId}/watchers` | Array của Watcher objects |

### 4. Custom Content

| Resource | URI | Mô tả | Atlassian API Endpoint | Dữ liệu trả về |
|----------|-----|-------|-----------------------|----------------|
| Custom Content | `confluence://custom-content` | Danh sách nội dung tùy chỉnh | `/wiki/api/v2/custom-content` | Array của CustomContent objects |
| Custom Content Details | `confluence://custom-content/{customContentId}` | Chi tiết nội dung tùy chỉnh | `/wiki/api/v2/custom-content/{customContentId}` | Single CustomContent object |

## Bổ Sung Tool

### 1. Quản lý trang

| Tool | Mô tả | Tham số chính | Atlassian API Endpoint | Dữ liệu output |
|------|-------|---------------|-----------------------|----------------|
| deletePage | Xóa trang | pageId | `/wiki/api/v2/pages/{id}` (DELETE) | Status của xóa |
| publishDraft | Xuất bản bản nháp | pageId | `/wiki/api/v2/pages/{id}` (PUT với status=current) | Page đã xuất bản |
| watchPage | Theo dõi trang | pageId, userId | `/wiki/api/v2/pages/{id}/watchers` (POST) | Status của theo dõi |
| unwatchPage | Hủy theo dõi trang | pageId, userId | `/wiki/api/v2/pages/{id}/watchers/{userId}` (DELETE) | Status của hủy theo dõi |

### 2. Quản lý Blog Post

| Tool | Mô tả | Tham số chính | Atlassian API Endpoint | Dữ liệu output |
|------|-------|---------------|-----------------------|----------------|
| createBlogPost | Tạo bài viết blog | spaceId, title, content | `/wiki/api/v2/blogposts` (POST) | BlogPost ID mới |
| updateBlogPost | Cập nhật bài viết blog | blogpostId, title, content, version | `/wiki/api/v2/blogposts/{id}` (PUT) | Status của update |
| deleteBlogPost | Xóa bài viết blog | blogpostId | `/wiki/api/v2/blogposts/{id}` (DELETE) | Status của xóa |
| watchBlogPost | Theo dõi bài viết blog | blogpostId, userId | `/wiki/api/v2/blogposts/{id}/watchers` (POST) | Status của theo dõi |

### 3. Quản lý Comment

| Tool | Mô tả | Tham số chính | Atlassian API Endpoint | Dữ liệu output |
|------|-------|---------------|-----------------------|----------------|
| updateComment | Cập nhật bình luận | commentId, content, version | `/wiki/api/v2/comments/{id}` (PUT) | Status của update |
| deleteComment | Xóa bình luận | commentId | `/wiki/api/v2/comments/{id}` (DELETE) | Status của xóa |

### 4. Quản lý Attachment

| Tool | Mô tả | Tham số chính | Atlassian API Endpoint | Dữ liệu output |
|------|-------|---------------|-----------------------|----------------|
| uploadAttachment | Tải lên tệp đính kèm | pageId, file, comment | `/wiki/api/v2/pages/{id}/attachments` (POST) | Attachment ID mới |
| updateAttachment | Cập nhật tệp đính kèm | pageId, attachmentId, file, comment | `/wiki/api/v2/attachments/{id}` (PUT) | Status của update |
| deleteAttachment | Xóa tệp đính kèm | attachmentId | `/wiki/api/v2/attachments/{id}` (DELETE) | Status của xóa |

### 5. Quản lý Space

| Tool | Mô tả | Tham số chính | Atlassian API Endpoint | Dữ liệu output |
|------|-------|---------------|-----------------------|----------------|
| createSpace | Tạo không gian | name, key, description | `/wiki/api/v2/spaces` (POST) | Space ID mới |
| updateSpace | Cập nhật không gian | spaceId, name, description | `/wiki/api/v2/spaces/{id}` (PUT) | Status của update |
| watchSpace | Theo dõi không gian | spaceId, userId | `/wiki/api/v2/spaces/{id}/watchers` (POST) | Status của theo dõi |
| unwatchSpace | Hủy theo dõi không gian | spaceId, userId | `/wiki/api/v2/spaces/{id}/watchers/{userId}` (DELETE) | Status của hủy theo dõi |

## Lưu ý quan trọng về API v2

1. **Phân trang cursor-based**: API v2 sử dụng cursor-based pagination thay vì offset-based. Các tham số phân trang là `limit` và `cursor` thay vì `limit` và `start`.

2. **Cấu trúc request/response**:
   - Request body cho các thao tác tạo/cập nhật trang có cấu trúc khác với API v1
   - Ví dụ tạo trang:
     ```
     {
       "spaceId": "string",
       "status": "current",
       "title": "string",
       "parentId": "string",
       "body": {
         "representation": "storage",
         "value": "string"
       }
     }
     ```

3. **Định dạng nội dung**: API v2 hỗ trợ nhiều định dạng nội dung (representation) như `storage`, `atlas_doc_format`, v.v. Cần chỉ định rõ trong request.

4. **Quản lý phiên bản**: Khi cập nhật trang, cần cung cấp số phiên bản hiện tại trong trường `version.number`.

5. **Quyền hạn**: Mỗi endpoint yêu cầu quyền hạn cụ thể, ví dụ:
   - Tạo trang: Quyền xem không gian tương ứng và quyền tạo trang trong không gian đó
   - Xóa trang: Quyền xem trang, không gian tương ứng và quyền xóa trang

6. **Thời gian hết hạn**: API v1 sẽ bị loại bỏ, nên việc chuyển đổi sang API v2 là cần thiết.

7. **Scope cho Connect app**: Các endpoint yêu cầu scope cụ thể, ví dụ:
   - Đọc trang: `READ`
   - Ghi trang: `WRITE`

Với các thông tin này, bạn có thể mở rộng MCP server để hỗ trợ đầy đủ các tính năng của Confluence API v2, giúp người dùng tương tác hiệu quả hơn với Confluence thông qua AI.
```

--------------------------------------------------------------------------------
/docs/dev-guide/modelcontextprotocol-resources.md:
--------------------------------------------------------------------------------

```markdown
https://modelcontextprotocol.io/docs/concepts/resources

# Resources

> Expose data and content from your servers to LLMs

Resources are a core primitive in the Model Context Protocol (MCP) that allow servers to expose data and content that can be read by clients and used as context for LLM interactions.

<Note>
  Resources are designed to be **application-controlled**, meaning that the client application can decide how and when they should be used.
  Different MCP clients may handle resources differently. For example:

  * Claude Desktop currently requires users to explicitly select resources before they can be used
  * Other clients might automatically select resources based on heuristics
  * Some implementations may even allow the AI model itself to determine which resources to use

  Server authors should be prepared to handle any of these interaction patterns when implementing resource support. In order to expose data to models automatically, server authors should use a **model-controlled** primitive such as [Tools](./tools).
</Note>

## Overview

Resources represent any kind of data that an MCP server wants to make available to clients. This can include:

* File contents
* Database records
* API responses
* Live system data
* Screenshots and images
* Log files
* And more

Each resource is identified by a unique URI and can contain either text or binary data.

## Resource URIs

Resources are identified using URIs that follow this format:

```
[protocol]://[host]/[path]
```

For example:

* `file:///home/user/documents/report.pdf`
* `postgres://database/customers/schema`
* `screen://localhost/display1`

The protocol and path structure is defined by the MCP server implementation. Servers can define their own custom URI schemes.

## Resource types

Resources can contain two types of content:

### Text resources

Text resources contain UTF-8 encoded text data. These are suitable for:

* Source code
* Configuration files
* Log files
* JSON/XML data
* Plain text

### Binary resources

Binary resources contain raw binary data encoded in base64. These are suitable for:

* Images
* PDFs
* Audio files
* Video files
* Other non-text formats

## Resource discovery

Clients can discover available resources through two main methods:

### Direct resources

Servers expose a list of concrete resources via the `resources/list` endpoint. Each resource includes:

```typescript
{
  uri: string;           // Unique identifier for the resource
  name: string;          // Human-readable name
  description?: string;  // Optional description
  mimeType?: string;     // Optional MIME type
}
```

### Resource templates

For dynamic resources, servers can expose [URI templates](https://datatracker.ietf.org/doc/html/rfc6570) that clients can use to construct valid resource URIs:

```typescript
{
  uriTemplate: string;   // URI template following RFC 6570
  name: string;          // Human-readable name for this type
  description?: string;  // Optional description
  mimeType?: string;     // Optional MIME type for all matching resources
}
```

## Reading resources

To read a resource, clients make a `resources/read` request with the resource URI.

The server responds with a list of resource contents:

```typescript
{
  contents: [
    {
      uri: string;        // The URI of the resource
      mimeType?: string;  // Optional MIME type

      // One of:
      text?: string;      // For text resources
      blob?: string;      // For binary resources (base64 encoded)
    }
  ]
}
```

<Tip>
  Servers may return multiple resources in response to one `resources/read` request. This could be used, for example, to return a list of files inside a directory when the directory is read.
</Tip>

## Resource updates

MCP supports real-time updates for resources through two mechanisms:

### List changes

Servers can notify clients when their list of available resources changes via the `notifications/resources/list_changed` notification.

### Content changes

Clients can subscribe to updates for specific resources:

1. Client sends `resources/subscribe` with resource URI
2. Server sends `notifications/resources/updated` when the resource changes
3. Client can fetch latest content with `resources/read`
4. Client can unsubscribe with `resources/unsubscribe`

## Example implementation

Here's a simple example of implementing resource support in an MCP server:

<Tabs>
  <Tab title="TypeScript">
    ```typescript
    const server = new Server({
      name: "example-server",
      version: "1.0.0"
    }, {
      capabilities: {
        resources: {}
      }
    });

    // List available resources
    server.setRequestHandler(ListResourcesRequestSchema, async () => {
      return {
        resources: [
          {
            uri: "file:///logs/app.log",
            name: "Application Logs",
            mimeType: "text/plain"
          }
        ]
      };
    });

    // Read resource contents
    server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
      const uri = request.params.uri;

      if (uri === "file:///logs/app.log") {
        const logContents = await readLogFile();
        return {
          contents: [
            {
              uri,
              mimeType: "text/plain",
              text: logContents
            }
          ]
        };
      }

      throw new Error("Resource not found");
    });
    ```
  </Tab>

  <Tab title="Python">
    ```python
    app = Server("example-server")

    @app.list_resources()
    async def list_resources() -> list[types.Resource]:
        return [
            types.Resource(
                uri="file:///logs/app.log",
                name="Application Logs",
                mimeType="text/plain"
            )
        ]

    @app.read_resource()
    async def read_resource(uri: AnyUrl) -> str:
        if str(uri) == "file:///logs/app.log":
            log_contents = await read_log_file()
            return log_contents

        raise ValueError("Resource not found")

    # Start server
    async with stdio_server() as streams:
        await app.run(
            streams[0],
            streams[1],
            app.create_initialization_options()
        )
    ```
  </Tab>
</Tabs>

## Best practices

When implementing resource support:

1. Use clear, descriptive resource names and URIs
2. Include helpful descriptions to guide LLM understanding
3. Set appropriate MIME types when known
4. Implement resource templates for dynamic content
5. Use subscriptions for frequently changing resources
6. Handle errors gracefully with clear error messages
7. Consider pagination for large resource lists
8. Cache resource contents when appropriate
9. Validate URIs before processing
10. Document your custom URI schemes

## Security considerations

When exposing resources:

* Validate all resource URIs
* Implement appropriate access controls
* Sanitize file paths to prevent directory traversal
* Be cautious with binary data handling
* Consider rate limiting for resource reads
* Audit resource access
* Encrypt sensitive data in transit
* Validate MIME types
* Implement timeouts for long-running reads
* Handle resource cleanup appropriately

```

--------------------------------------------------------------------------------
/llms-install-bundle.md:
--------------------------------------------------------------------------------

```markdown
# MCP Atlassian Server (by phuc-nt) - Bundle Installation Guide for AI

> **Important Note:** This is a pre-built bundle version of MCP Atlassian Server. No compilation or dependency installation required - just extract and run!

## System Requirements
- macOS 10.15+ or Windows 10+
- Node.js v16+ (only for running the server, not for building)
- Atlassian Cloud account and API token
- Cline AI assistant (main supported client)

## Step 1: Extract the Bundle
```bash
# Extract the downloaded bundle
unzip mcp-atlassian-server-bundle.zip

# Navigate to the extracted directory
cd mcp-atlassian-server-bundle
```

## Step 2: Configure Cline

MCP Atlassian Server is specifically designed for seamless integration with Cline. Below is the guide to configure Cline to connect to the server:

### Determine the Full Path

First, determine the full path to your extracted bundle directory:

```bash
# macOS/Linux
pwd

# Windows (PowerShell)
(Get-Location).Path

# Windows (Command Prompt)
cd
```

Then, add the following configuration to your `cline_mcp_settings.json` file:

```json
{
  "mcpServers": {
    "phuc-nt/mcp-atlassian-server": {
      "disabled": false,
      "timeout": 60,
      "command": "node",
      "args": [
        "/full/path/to/mcp-atlassian-server-bundle/dist/index.js"
      ],
      "env": {
        "ATLASSIAN_SITE_NAME": "your-site.atlassian.net",
        "ATLASSIAN_USER_EMAIL": "[email protected]",
        "ATLASSIAN_API_TOKEN": "your-api-token"
      },
      "transportType": "stdio"
    }
  }
}
```

Replace:
- `/full/path/to/` with the path you just obtained
- `your-site.atlassian.net` with your Atlassian site name
- `[email protected]` with your Atlassian email
- `your-api-token` with your Atlassian API token

> **Note for Windows**: The path on Windows may look like `C:\\Users\\YourName\\mcp-atlassian-server-bundle\\dist\\index.js` (use `\\` instead of `/`).

## Step 3: Get Atlassian API Token
1. Go to https://id.atlassian.com/manage-profile/security/api-tokens
2. Click "Create API token", name it (e.g., "MCP Server")
3. Copy the token immediately (it will not be shown again)

### Note on API Token Permissions

- **The API token inherits all permissions of the account that created it** – there is no separate permission mechanism for the token itself.
- **To use all features of MCP Server**, the account creating the token must have appropriate permissions:
  - **Jira**: Needs Browse Projects, Edit Issues, Assign Issues, Transition Issues, Create Issues, etc.
  - **Confluence**: Needs View Spaces, Add Pages, Add Comments, Edit Pages, etc.
- **If the token is read-only**, you can only use read resources (view issues, projects) but cannot create/update.
- **Recommendations**:
  - For personal use: You can use your main account's token
  - For team/long-term use: Create a dedicated service account with appropriate permissions
  - Do not share your token; if you suspect it is compromised, revoke and create a new one
- **If you get a "permission denied" error**, check the permissions of the account that created the token on the relevant projects/spaces

> **Summary**: MCP Atlassian Server works best when using an API token from an account with all the permissions needed for the actions you want the AI to perform on Jira/Confluence.

### Security Warning When Using LLMs

- **Security risk**: If you or the AI in Cline ask an LLM to read/analyze the `cline_mcp_settings.json` file, **your Atlassian token will be sent to a third-party server** (OpenAI, Anthropic, etc.).
- **How it works**:
  - Cline does **NOT** automatically send config files to the cloud
  - However, if you ask to "check the config file" or similar, the file content (including API token) will be sent to the LLM endpoint for processing
- **Safety recommendations**:
  - Do not ask the LLM to read/check config files containing tokens
  - If you need support, remove sensitive information before sending to the LLM
  - Treat your API token like a password – never share it in LLM prompts

> **Important**: If you do not ask the LLM to read the config file, your API token will only be used locally and will not be sent anywhere.

## Step 4: Run the Server (Optional Testing)

You can test the server locally before configuring Cline by running:

```bash
node dist/index.js
```

You should see output confirming the server is running. Press Ctrl+C to stop.

> **Note**: You don't need to manually run the server when using with Cline - Cline will automatically start and manage the server process.

## Verify Installation
After configuration, test the connection by asking Cline a question related to Jira or Confluence, for example:
- "List all projects in Jira"
- "Search for Confluence pages about [topic]"

Cline is optimized to work with this MCP Atlassian Server (by phuc-nt) and will automatically use the most appropriate resources and tools for your queries.

## Introduction & Usage Scenarios

### Capabilities of MCP Atlassian Server (by phuc-nt)

This MCP Server connects AI to Atlassian systems (Jira and Confluence), enabling:

#### Jira Information Access
- View details of issues, projects, and users
- Search issues with simple JQL
- View possible transitions
- View issue comments
- Find users assignable to tasks or by role

#### Jira Actions
- Create new issues
- Update issue content
- Transition issue status
- Assign issues to users

#### Confluence Information Access
- View spaces
- View pages and child pages
- View page details (title, content, version, labels)
- View comments on pages
- View labels on pages

#### Confluence Actions
- Create new pages with simple HTML content
- Update existing pages (title, content, version, labels)
- Add and remove labels on pages
- Add comments to pages

### Example Usage Scenarios

1. **Create and Manage Tasks**
   ```
   "Create a new issue in project XDEMO2 about login error"
   "Find issues that are 'In Progress' and assign them to me"
   "Transition issue XDEMO2-43 to Done"
   ```

2. **Project Information Summary**
   ```
   "Summarize all issues in project XDEMO2"
   "Who is assigned issues in project XDEMO2?"
   "List unassigned issues in the current sprint"
   ```

3. **Documentation with Confluence**
   ```
   "Create a new Confluence page named 'Meeting Notes 2025-05-03'"
   "Update the Confluence page about API Documentation with new examples and add label 'documentation'"
   "Add the label 'documentation' to the page about architecture"
   "Remove the label 'draft' from the page 'Meeting Notes'"
   "Add a comment to the Confluence page about API Documentation"
   ```

4. **Analysis and Reporting**
   ```
   "Compare the number of completed issues between the current and previous sprint"
   "Who has the most issues in 'To Do' status?"
   ```

### Usage Notes

1. **Simple JQL**: When searching for issues, use simple JQL without spaces or special characters (e.g., `project=XDEMO2` instead of `project = XDEMO2 AND key = XDEMO2-43`).

2. **Create Confluence Page**: When creating a Confluence page, use simple HTML content and do not specify parentId to avoid errors.

3. **Update Confluence Page**: When updating a page, always include the current version number to avoid conflicts. You can also update labels (add/remove) and must use valid storage format for content.

4. **Create Issue**: When creating new issues, only provide the minimum required fields (projectKey, summary) for best success.

5. **Access Rights**: Ensure the configured Atlassian account has access to the projects and spaces you want to interact with.

After installation, you can use Cline to interact with Jira and Confluence naturally, making project management and documentation more efficient. 
```

--------------------------------------------------------------------------------
/src/resources/jira/projects.ts:
--------------------------------------------------------------------------------

```typescript
import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
import { Config, Resources } from '../../utils/mcp-helpers.js';
import { AtlassianConfig } from '../../utils/atlassian-api-base.js';
import { ApiError, ApiErrorType } from '../../utils/error-handler.js';
import { Logger } from '../../utils/logger.js';
import fetch from 'cross-fetch';
import { projectsListSchema, projectSchema } from '../../schemas/jira.js';
import { getProjects as getProjectsApi, getProject as getProjectApi } from '../../utils/jira-resource-api.js';

const logger = Logger.getLogger('JiraResource:Projects');

/**
 * Create basic headers for Atlassian API with Basic Authentication
 */
function createBasicHeaders(email: string, apiToken: string) {
  const auth = Buffer.from(`${email}:${apiToken}`).toString('base64');
  return {
    'Authorization': `Basic ${auth}`,
    'Content-Type': 'application/json',
    'Accept': 'application/json',
    'User-Agent': 'MCP-Atlassian-Server/1.0.0'
  };
}

/**
 * Helper function to get the list of projects
 */
async function getProjects(config: AtlassianConfig): Promise<any[]> {
  return await getProjectsApi(config);
}

/**
 * Helper function to get project details
 */
async function getProject(config: AtlassianConfig, projectKey: string): Promise<any> {
  return await getProjectApi(config, projectKey);
}

/**
 * Register resources related to Jira projects
 * @param server MCP Server instance
 */
export function registerProjectResources(server: McpServer) {
  // Resource: List all projects
  server.resource(
    'jira-projects-list',
    new ResourceTemplate('jira://projects', {
      list: async (_extra) => {
        return {
          resources: [
            {
              uri: 'jira://projects',
              name: 'Jira Projects',
              description: 'List and search all Jira projects',
              mimeType: 'application/json'
            }
          ]
        };
      }
    }),
    async (uri, _params, _extra) => {
      logger.info('Getting list of Jira projects');
      try {
        // Get config from environment
        const config = Config.getAtlassianConfigFromEnv();
        
        // Get the list of projects from Jira API
        const projects = await getProjects(config);
        // Convert response to a more friendly format
        const formattedProjects = projects.map((project: any) => ({
          id: project.id,
          key: project.key,
          name: project.name,
          projectType: project.projectTypeKey,
          url: `${config.baseUrl}/browse/${project.key}`,
          lead: project.lead?.displayName || 'Unknown'
        }));
        
        const uriString = typeof uri === 'string' ? uri : uri.href;
        // Return standardized resource with metadata and schema
        return Resources.createStandardResource(
          uriString,
          formattedProjects,
          'projects',
          projectsListSchema,
          formattedProjects.length,
          formattedProjects.length,
          0,
          `${config.baseUrl}/jira/projects`
        );
      } catch (error) {
        logger.error('Error getting Jira projects:', error);
        throw error;
      }
    }
  );
  
  // Resource: Project details
  server.resource(
    'jira-project-details',
    new ResourceTemplate('jira://projects/{projectKey}', {
      list: async (_extra) => ({
        resources: [
          {
            uri: 'jira://projects/{projectKey}',
            name: 'Jira Project Details',
            description: 'Get details for a specific Jira project by key. Replace {projectKey} with the project key.',
            mimeType: 'application/json'
          }
        ]
      })
    }),
    async (uri, params, _extra) => {
      try {
        // Get config from environment
        const config = Config.getAtlassianConfigFromEnv();
        
        // Get projectKey from URI pattern
        let normalizedProjectKey = '';
        if (params && 'projectKey' in params) {
          normalizedProjectKey = Array.isArray(params.projectKey) ? params.projectKey[0] : params.projectKey;
        }
        
        if (!normalizedProjectKey) {
          throw new ApiError(
            ApiErrorType.VALIDATION_ERROR,
            'Project key not provided',
            400,
            new Error('Missing project key parameter')
          );
        }
        logger.info(`Getting details for Jira project: ${normalizedProjectKey}`);
        
        // Get project info from Jira API
        const project = await getProject(config, normalizedProjectKey);
        // Convert response to a more friendly format
        const formattedProject = {
          id: project.id,
          key: project.key,
          name: project.name,
          description: project.description || 'No description',
          lead: project.lead?.displayName || 'Unknown',
          url: `${config.baseUrl}/browse/${project.key}`,
          projectCategory: project.projectCategory?.name || 'Uncategorized',
          projectType: project.projectTypeKey
        };
        
        const uriString = typeof uri === 'string' ? uri : uri.href;
        // Chuẩn hóa metadata/schema
        return Resources.createStandardResource(
          uriString,
          [formattedProject],
          'project',
          projectSchema,
          1,
          1,
          0,
          `${config.baseUrl}/browse/${project.key}`
        );
      } catch (error) {
        logger.error(`Error getting Jira project details:`, error);
        throw error;
      }
    }
  );

  // Resource: List roles of a project
  server.resource(
    'jira-project-roles',
    new ResourceTemplate('jira://projects/{projectKey}/roles', {
      list: async (_extra) => ({
        resources: [
          {
            uri: 'jira://projects/{projectKey}/roles',
            name: 'Jira Project Roles',
            description: 'List roles for a Jira project. Replace {projectKey} with the project key.',
            mimeType: 'application/json'
          }
        ]
      })
    }),
    async (uri, params, _extra) => {
      try {
        // Get config from environment
        const config = Config.getAtlassianConfigFromEnv();
        
        let normalizedProjectKey = '';
        if (params && 'projectKey' in params) {
          normalizedProjectKey = Array.isArray(params.projectKey) ? params.projectKey[0] : params.projectKey;
        }
        
        if (!normalizedProjectKey) {
          throw new Error('Missing projectKey');
        }
        logger.info(`Getting roles for Jira project: ${normalizedProjectKey}`);
        
        const auth = Buffer.from(`${config.email}:${config.apiToken}`).toString('base64');
        const headers = {
          'Authorization': `Basic ${auth}`,
          'Content-Type': 'application/json',
          'Accept': 'application/json',
          'User-Agent': 'MCP-Atlassian-Server/1.0.0'
        };
        let baseUrl = config.baseUrl;
        if (!baseUrl.startsWith('https://')) baseUrl = `https://${baseUrl}`;
        const url = `${baseUrl}/rest/api/3/project/${encodeURIComponent(normalizedProjectKey)}/role`;
        logger.debug(`Calling Jira API: ${url}`);
        const response = await fetch(url, { method: 'GET', headers, credentials: 'omit' });
        if (!response.ok) {
          const statusCode = response.status;
          const responseText = await response.text();
          logger.error(`Jira API error (${statusCode}):`, responseText);
          throw new Error(`Jira API error: ${responseText}`);
        }
        const data = await response.json();
        // data is an object: key is roleName, value is URL containing roleId
        const roles = Object.entries(data).map(([roleName, url]) => {
          const urlStr = String(url);
          const match = urlStr.match(/\/role\/(\d+)$/);
          return {
            roleName,
            roleId: match ? match[1] : '',
            url: urlStr
          };
        });
        
        const uriString = typeof uri === 'string' ? uri : uri.href;
        // Chuẩn hóa metadata/schema (dùng array of role object, schema tự tạo inline)
        const rolesListSchema = {
          type: "array",
          items: {
            type: "object",
            properties: {
              roleName: { type: "string" },
              roleId: { type: "string" },
              url: { type: "string" }
            },
            required: ["roleName", "roleId", "url"]
          }
        };
        return Resources.createStandardResource(
          uriString,
          roles,
          'roles',
          rolesListSchema,
          roles.length,
          roles.length,
          0,
          `${config.baseUrl}/browse/${normalizedProjectKey}/project-roles`
        );
      } catch (error) {
        logger.error(`Error getting roles for Jira project:`, error);
        throw error;
      }
    }
  );
  
  logger.info('Jira project resources registered successfully');
}

```

--------------------------------------------------------------------------------
/docs/dev-guide/modelcontextprotocol-architecture.md:
--------------------------------------------------------------------------------

```markdown
https://modelcontextprotocol.io/docs/concepts/architecture

# Core architecture

> Understand how MCP connects clients, servers, and LLMs

The Model Context Protocol (MCP) is built on a flexible, extensible architecture that enables seamless communication between LLM applications and integrations. This document covers the core architectural components and concepts.

## Overview

MCP follows a client-server architecture where:

* **Hosts** are LLM applications (like Claude Desktop or IDEs) that initiate connections
* **Clients** maintain 1:1 connections with servers, inside the host application
* **Servers** provide context, tools, and prompts to clients

```mermaid
flowchart LR
    subgraph "Host"
        client1[MCP Client]
        client2[MCP Client]
    end
    subgraph "Server Process"
        server1[MCP Server]
    end
    subgraph "Server Process"
        server2[MCP Server]
    end

    client1 <-->|Transport Layer| server1
    client2 <-->|Transport Layer| server2
```

## Core components

### Protocol layer

The protocol layer handles message framing, request/response linking, and high-level communication patterns.

<Tabs>
  <Tab title="TypeScript">
    ```typescript
    class Protocol<Request, Notification, Result> {
        // Handle incoming requests
        setRequestHandler<T>(schema: T, handler: (request: T, extra: RequestHandlerExtra) => Promise<Result>): void

        // Handle incoming notifications
        setNotificationHandler<T>(schema: T, handler: (notification: T) => Promise<void>): void

        // Send requests and await responses
        request<T>(request: Request, schema: T, options?: RequestOptions): Promise<T>

        // Send one-way notifications
        notification(notification: Notification): Promise<void>
    }
    ```
  </Tab>

  <Tab title="Python">
    ```python
    class Session(BaseSession[RequestT, NotificationT, ResultT]):
        async def send_request(
            self,
            request: RequestT,
            result_type: type[Result]
        ) -> Result:
            """Send request and wait for response. Raises McpError if response contains error."""
            # Request handling implementation

        async def send_notification(
            self,
            notification: NotificationT
        ) -> None:
            """Send one-way notification that doesn't expect response."""
            # Notification handling implementation

        async def _received_request(
            self,
            responder: RequestResponder[ReceiveRequestT, ResultT]
        ) -> None:
            """Handle incoming request from other side."""
            # Request handling implementation

        async def _received_notification(
            self,
            notification: ReceiveNotificationT
        ) -> None:
            """Handle incoming notification from other side."""
            # Notification handling implementation
    ```
  </Tab>
</Tabs>

Key classes include:

* `Protocol`
* `Client`
* `Server`

### Transport layer

The transport layer handles the actual communication between clients and servers. MCP supports multiple transport mechanisms:

1. **Stdio transport**
   * Uses standard input/output for communication
   * Ideal for local processes

2. **HTTP with SSE transport**
   * Uses Server-Sent Events for server-to-client messages
   * HTTP POST for client-to-server messages

All transports use [JSON-RPC](https://www.jsonrpc.org/) 2.0 to exchange messages. See the [specification](/specification/) for detailed information about the Model Context Protocol message format.

### Message types

MCP has these main types of messages:

1. **Requests** expect a response from the other side:
   ```typescript
   interface Request {
     method: string;
     params?: { ... };
   }
   ```

2. **Results** are successful responses to requests:
   ```typescript
   interface Result {
     [key: string]: unknown;
   }
   ```

3. **Errors** indicate that a request failed:
   ```typescript
   interface Error {
     code: number;
     message: string;
     data?: unknown;
   }
   ```

4. **Notifications** are one-way messages that don't expect a response:
   ```typescript
   interface Notification {
     method: string;
     params?: { ... };
   }
   ```

## Connection lifecycle

### 1. Initialization

```mermaid
sequenceDiagram
    participant Client
    participant Server

    Client->>Server: initialize request
    Server->>Client: initialize response
    Client->>Server: initialized notification

    Note over Client,Server: Connection ready for use
```

1. Client sends `initialize` request with protocol version and capabilities
2. Server responds with its protocol version and capabilities
3. Client sends `initialized` notification as acknowledgment
4. Normal message exchange begins

### 2. Message exchange

After initialization, the following patterns are supported:

* **Request-Response**: Client or server sends requests, the other responds
* **Notifications**: Either party sends one-way messages

### 3. Termination

Either party can terminate the connection:

* Clean shutdown via `close()`
* Transport disconnection
* Error conditions

## Error handling

MCP defines these standard error codes:

```typescript
enum ErrorCode {
  // Standard JSON-RPC error codes
  ParseError = -32700,
  InvalidRequest = -32600,
  MethodNotFound = -32601,
  InvalidParams = -32602,
  InternalError = -32603
}
```

SDKs and applications can define their own error codes above -32000.

Errors are propagated through:

* Error responses to requests
* Error events on transports
* Protocol-level error handlers

## Implementation example

Here's a basic example of implementing an MCP server:

<Tabs>
  <Tab title="TypeScript">
    ```typescript
    import { Server } from "@modelcontextprotocol/sdk/server/index.js";
    import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";

    const server = new Server({
      name: "example-server",
      version: "1.0.0"
    }, {
      capabilities: {
        resources: {}
      }
    });

    // Handle requests
    server.setRequestHandler(ListResourcesRequestSchema, async () => {
      return {
        resources: [
          {
            uri: "example://resource",
            name: "Example Resource"
          }
        ]
      };
    });

    // Connect transport
    const transport = new StdioServerTransport();
    await server.connect(transport);
    ```
  </Tab>

  <Tab title="Python">
    ```python
    import asyncio
    import mcp.types as types
    from mcp.server import Server
    from mcp.server.stdio import stdio_server

    app = Server("example-server")

    @app.list_resources()
    async def list_resources() -> list[types.Resource]:
        return [
            types.Resource(
                uri="example://resource",
                name="Example Resource"
            )
        ]

    async def main():
        async with stdio_server() as streams:
            await app.run(
                streams[0],
                streams[1],
                app.create_initialization_options()
            )

    if __name__ == "__main__":
        asyncio.run(main())
    ```
  </Tab>
</Tabs>

## Best practices

### Transport selection

1. **Local communication**
   * Use stdio transport for local processes
   * Efficient for same-machine communication
   * Simple process management

2. **Remote communication**
   * Use SSE for scenarios requiring HTTP compatibility
   * Consider security implications including authentication and authorization

### Message handling

1. **Request processing**
   * Validate inputs thoroughly
   * Use type-safe schemas
   * Handle errors gracefully
   * Implement timeouts

2. **Progress reporting**
   * Use progress tokens for long operations
   * Report progress incrementally
   * Include total progress when known

3. **Error management**
   * Use appropriate error codes
   * Include helpful error messages
   * Clean up resources on errors

## Security considerations

1. **Transport security**
   * Use TLS for remote connections
   * Validate connection origins
   * Implement authentication when needed

2. **Message validation**
   * Validate all incoming messages
   * Sanitize inputs
   * Check message size limits
   * Verify JSON-RPC format

3. **Resource protection**
   * Implement access controls
   * Validate resource paths
   * Monitor resource usage
   * Rate limit requests

4. **Error handling**
   * Don't leak sensitive information
   * Log security-relevant errors
   * Implement proper cleanup
   * Handle DoS scenarios

## Debugging and monitoring

1. **Logging**
   * Log protocol events
   * Track message flow
   * Monitor performance
   * Record errors

2. **Diagnostics**
   * Implement health checks
   * Monitor connection state
   * Track resource usage
   * Profile performance

3. **Testing**
   * Test different transports
   * Verify error handling
   * Check edge cases
   * Load test servers

```

--------------------------------------------------------------------------------
/llms-install.md:
--------------------------------------------------------------------------------

```markdown
# MCP Atlassian Server (by phuc-nt) Installation Guide for AI

> **Important Note:** MCP Atlassian Server (by phuc-nt) is primarily developed and optimized for use with the Cline AI assistant. While it follows the MCP standard and can work with other compatible MCP clients, the best performance and experience are achieved with Cline.

> **Version Note:** This guide is for MCP Atlassian Server v2.0.1. For detailed documentation on architecture, development, and usage, refer to the new documentation series in the `/docs/knowledge/` directory.

## System Requirements
- macOS 10.15+ or Windows 10+
- Atlassian Cloud account and API token
- Cline AI assistant (main supported client)

## Installation Options

You have two ways to install MCP Atlassian Server:

1. **[Install from npm](#option-1-install-from-npm)** (recommended, easier) - Install directly from npm registry
2. **[Clone & Build from source](#option-2-clone-and-build-from-source)** - Clone the GitHub repository and build locally

## Option 1: Install from npm

This is the recommended method as it's simpler and lets you easily update to new versions.

### Install the package globally

```bash
npm install -g @phuc-nt/mcp-atlassian-server
```

Or install in your project:

```bash
npm install @phuc-nt/mcp-atlassian-server
```

### Find the installation path

After installation, you'll need to know the path to the package for Cline configuration:

```bash
# For global installation, find the global node_modules directory
npm root -g
# Output will be something like: /usr/local/lib/node_modules

# For local installation, the path will be in your project directory
# e.g., /your/project/node_modules/@phuc-nt/mcp-atlassian-server
```

The full path to the executable will be: `<npm_modules_path>/@phuc-nt/mcp-atlassian-server/dist/index.js`

Skip to [Configure Cline section](#configure-cline) after installing from npm.

## Option 2: Clone and Build from Source

### Prerequisite Tools Check & Installation

### Check Installed Tools

Verify that Git, Node.js, and npm are installed:

```bash
git --version
node --version
npm --version
```

If the above commands show version numbers, you have the required tools. If not, follow the steps below:

### Install Git

#### macOS
**Method 1**: Using Homebrew (recommended)
```bash
# Install Homebrew if not available
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

# Install Git
brew install git
```

**Method 2**: Install Xcode Command Line Tools
```bash
xcode-select --install
```

#### Windows
1. Download the Git installer from [git-scm.com](https://git-scm.com/download/win)
2. Run the installer with default options
3. After installation, open Command Prompt or PowerShell and check: `git --version`

### Install Node.js and npm

#### macOS
**Method 1**: Using Homebrew (recommended)
```bash
brew install node
```

**Method 2**: Using nvm (Node Version Manager)
```bash
# Install nvm
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash

# Install Node.js LTS
nvm install --lts
```

#### Windows
1. Download Node.js installer from [nodejs.org](https://nodejs.org/) (choose LTS version)
2. Run the installer with default options
3. After installation, open Command Prompt or PowerShell and check:
   ```
   node --version
   npm --version
   ```

### Step 1: Clone the Repository
```bash
# macOS/Linux
git clone https://github.com/phuc-nt/mcp-atlassian-server.git
cd mcp-atlassian-server

# Windows
git clone https://github.com/phuc-nt/mcp-atlassian-server.git
cd mcp-atlassian-server
```

### Step 2: Install Dependencies
```bash
npm install
```

### Step 3: Build the Project
```bash
npm run build
```

## Configure Cline

MCP Atlassian Server is specifically designed for seamless integration with Cline. Below is the guide to configure Cline to connect to the server:

### Determine the Full Path

#### For npm installation
If you installed the package via npm, you need the path to the installed package:

```bash
# For global npm installation
echo "$(npm root -g)/@phuc-nt/mcp-atlassian-server/dist/index.js"

# For local npm installation (run from your project directory)
echo "$(pwd)/node_modules/@phuc-nt/mcp-atlassian-server/dist/index.js"
```

#### For source code installation

First, determine the full path to your project directory:

```bash
# macOS/Linux
pwd

# Windows (PowerShell)
(Get-Location).Path

# Windows (Command Prompt)
cd
```

Then, add the following configuration to your `cline_mcp_settings.json` file:

```json
{
  "mcpServers": {
    "phuc-nt/mcp-atlassian-server": {
      "disabled": false,
      "timeout": 60,
      "command": "node",
      "args": [
        "/path/to/mcp-atlassian-server/dist/index.js"
      ],
      "env": {
        "ATLASSIAN_SITE_NAME": "your-site.atlassian.net",
        "ATLASSIAN_USER_EMAIL": "[email protected]",
        "ATLASSIAN_API_TOKEN": "your-api-token"
      },
      "transportType": "stdio"
    }
  }
}
```

Replace:
- For **npm installation**: Use the path to the npm package: 
  - Global install: `/path/to/global/node_modules/@phuc-nt/mcp-atlassian-server/dist/index.js` 
  - Local install: `/path/to/your/project/node_modules/@phuc-nt/mcp-atlassian-server/dist/index.js`
- For **source installation**: Use the path you just obtained with `pwd` command
- `your-site.atlassian.net` with your Atlassian site name
- `[email protected]` with your Atlassian email
- `your-api-token` with your Atlassian API token

> **Note for global npm installs**: You can find the global node_modules path by running: `npm root -g`

> **Note for Windows**: The path on Windows may look like `C:\\Users\\YourName\\AppData\\Roaming\\npm\\node_modules\\@phuc-nt\\mcp-atlassian-server\\dist\\index.js` (use `\\` instead of `/`).

## Step 5: Get Atlassian API Token
1. Go to https://id.atlassian.com/manage-profile/security/api-tokens
2. Click "Create API token", name it (e.g., "MCP Server")
3. Copy the token immediately (it will not be shown again)

### Note on API Token Permissions

- **The API token inherits all permissions of the account that created it** – there is no separate permission mechanism for the token itself.
- **To use all features of MCP Server**, the account creating the token must have appropriate permissions:
  - **Jira**: Needs Browse Projects, Edit Issues, Assign Issues, Transition Issues, Create Issues, etc.
  - **Confluence**: Needs View Spaces, Add Pages, Add Comments, Edit Pages, etc.
- **If the token is read-only**, you can only use read resources (view issues, projects) but cannot create/update.
- **Recommendations**:
  - For personal use: You can use your main account's token
  - For team/long-term use: Create a dedicated service account with appropriate permissions
  - Do not share your token; if you suspect it is compromised, revoke and create a new one
- **If you get a "permission denied" error**, check the permissions of the account that created the token on the relevant projects/spaces

> **Summary**: MCP Atlassian Server works best when using an API token from an account with all the permissions needed for the actions you want the AI to perform on Jira/Confluence.

### Security Warning When Using LLMs

- **Security risk**: If you or the AI in Cline ask an LLM to read/analyze the `cline_mcp_settings.json` file, **your Atlassian token will be sent to a third-party server** (OpenAI, Anthropic, etc.).
- **How it works**:
  - Cline does **NOT** automatically send config files to the cloud
  - However, if you ask to "check the config file" or similar, the file content (including API token) will be sent to the LLM endpoint for processing
- **Safety recommendations**:
  - Do not ask the LLM to read/check config files containing tokens
  - If you need support, remove sensitive information before sending to the LLM
  - Treat your API token like a password – never share it in LLM prompts

> **Important**: If you do not ask the LLM to read the config file, your API token will only be used locally and will not be sent anywhere.

## Documentation Resources

MCP Atlassian Server (by phuc-nt) now includes a comprehensive documentation series:

1. [MCP Overview & Architecture](./docs/knowledge/01-mcp-overview-architecture.md): Core concepts, architecture, and design principles
2. [MCP Tools & Resources Development](./docs/knowledge/02-mcp-tools-resources.md): How to develop and extend MCP resources and tools
3. [MCP Prompts & Sampling](./docs/knowledge/03-mcp-prompts-sampling.md): Guide for prompt engineering and sampling with MCP

These documents provide deeper insights into the server's functionality and are valuable for both users and developers.

## Verify Installation

### Test the MCP Server directly

You can test that the MCP Server runs correctly by executing it directly:

```bash
# For npm global install
node $(npm root -g)/@phuc-nt/mcp-atlassian-server/dist/index.js

# For npm local install
node ./node_modules/@phuc-nt/mcp-atlassian-server/dist/index.js

# For source code install
node ./dist/index.js
```

You should see output indicating that the server has started and registered resources and tools.

### Test with Cline

After configuration, test the connection by asking Cline a question related to Jira or Confluence, for example:
- "List all projects in Jira"
- "Search for Confluence pages about [topic]"
- "Create a new issue in project DEMO"

Cline is optimized to work with this MCP Atlassian Server (by phuc-nt) and will automatically use the most appropriate resources and tools for your queries.
```

--------------------------------------------------------------------------------
/src/utils/atlassian-api-base.ts:
--------------------------------------------------------------------------------

```typescript
import { Version3Client } from "jira.js";
import { Logger } from "./logger.js";
import { ApiError, ApiErrorType } from "./error-handler.js";
import fetch from "cross-fetch";

export interface AtlassianConfig {
  baseUrl: string;
  apiToken: string;
  email: string;
}

// Initialize logger
export const logger = Logger.getLogger("AtlassianAPI");

// Cache for Atlassian clients to reuse
export const clientCache = new Map<string, Version3Client>();

/**
 * Create basic headers for API request
 * @param email User email
 * @param apiToken User API token
 * @returns Object containing basic headers
 */
export const createBasicHeaders = (email: string, apiToken: string) => {
  // Remove whitespace and newlines from API token
  const cleanedToken = apiToken.replace(/\s+/g, "");
  // Always use Basic Authentication as per API docs
  const auth = Buffer.from(`${email}:${cleanedToken}`).toString("base64");
  // Log headers for debugging
  logger.debug(
    "Creating headers with User-Agent:",
    "MCP-Atlassian-Server/1.0.0"
  );
  return {
    Authorization: `Basic ${auth}`,
    "Content-Type": "application/json",
    Accept: "application/json",
    // Add User-Agent to help Cloudfront identify the request
    "User-Agent": "MCP-Atlassian-Server/1.0.0",
  };
};

// Helper: Create or get Jira client from cache
export function getJiraClient(config: AtlassianConfig): Version3Client {
  const cacheKey = `jira:${config.baseUrl}:${config.email}`;
  if (clientCache.has(cacheKey)) {
    return clientCache.get(cacheKey) as Version3Client;
  }
  logger.debug(`Creating new Jira client for ${config.baseUrl}`);
  // Normalize baseUrl
  let baseUrl = config.baseUrl;
  if (baseUrl.startsWith("http://")) {
    baseUrl = baseUrl.replace("http://", "https://");
  } else if (!baseUrl.startsWith("https://")) {
    baseUrl = `https://${baseUrl}`;
  }
  if (!baseUrl.includes(".atlassian.net")) {
    baseUrl = `${baseUrl}.atlassian.net`;
  }
  if (baseUrl.match(/\.atlassian\.net\.atlassian\.net/)) {
    baseUrl = baseUrl.replace(".atlassian.net.atlassian.net", ".atlassian.net");
  }
  const client = new Version3Client({
    host: baseUrl,
    authentication: {
      basic: {
        email: config.email,
        apiToken: config.apiToken,
      },
    },
    baseRequestConfig: {
      headers: {
        "User-Agent": "MCP-Atlassian-Server/1.0.0",
      },
    },
  });
  clientCache.set(cacheKey, client);
  return client;
}

// Helper: Normalize baseUrl for Atlassian API
export function normalizeAtlassianBaseUrl(baseUrl: string): string {
  let normalizedUrl = baseUrl;
  if (normalizedUrl.startsWith("http://")) {
    normalizedUrl = normalizedUrl.replace("http://", "https://");
  } else if (!normalizedUrl.startsWith("https://")) {
    normalizedUrl = `https://${normalizedUrl}`;
  }
  if (!normalizedUrl.includes(".atlassian.net")) {
    normalizedUrl = `${normalizedUrl}.atlassian.net`;
  }
  if (normalizedUrl.match(/\.atlassian\.net\.atlassian\.net/)) {
    normalizedUrl = normalizedUrl.replace(
      ".atlassian.net.atlassian.net",
      ".atlassian.net"
    );
  }
  return normalizedUrl;
}

// Helper: Call Jira API using jira.js (throw by default)
export async function callJiraApi<T>(
  config: AtlassianConfig,
  endpoint: string,
  method: "GET" | "POST" | "PUT" | "DELETE" = "GET",
  data: any = null,
  params: Record<string, any> = {}
): Promise<T> {
  try {
    const client = getJiraClient(config);
    logger.debug(`Calling Jira API with jira.js: ${method} ${endpoint}`);
    throw new ApiError(
      ApiErrorType.UNKNOWN_ERROR,
      "This API call method is not implemented with jira.js. Please use specific methods.",
      501
    );
  } catch (error: any) {
    logger.error(`Jira API error with jira.js:`, error);
    if (error instanceof ApiError) {
      throw error;
    }
    const statusCode = error.response?.status || 500;
    const errorMessage = error.message || "Unknown error";
    throw new ApiError(
      ApiErrorType.SERVER_ERROR,
      `Jira API error: ${errorMessage}`,
      statusCode,
      error
    );
  }
}

// Helper: Call Confluence API using fetch
export async function callConfluenceApi<T>(
  config: AtlassianConfig,
  endpoint: string,
  method: "GET" | "POST" | "PUT" | "DELETE" = "GET",
  data: any = null,
  params: Record<string, any> = {}
): Promise<T> {
  try {
    const headers = createBasicHeaders(config.email, config.apiToken);
    let baseUrl = config.baseUrl;
    if (baseUrl.startsWith("http://")) {
      baseUrl = baseUrl.replace("http://", "https://");
    } else if (!baseUrl.startsWith("https://")) {
      baseUrl = `https://${baseUrl}`;
    }
    if (!baseUrl.includes(".atlassian.net")) {
      baseUrl = `${baseUrl}.atlassian.net`;
    }
    if (baseUrl.match(/\.atlassian\.net\.atlassian\.net/)) {
      baseUrl = baseUrl.replace(
        ".atlassian.net.atlassian.net",
        ".atlassian.net"
      );
    }
    let url = `${baseUrl}/wiki${endpoint}`;
    if (params && Object.keys(params).length > 0) {
      const queryParams = new URLSearchParams();
      Object.entries(params).forEach(([key, value]) => {
        queryParams.append(key, String(value));
      });
      url += `?${queryParams.toString()}`;
    }
    logger.debug(`Calling Confluence API: ${method} ${url}`);
    logger.debug(`With Auth: ${config.email}:*****`);
    logger.debug(`Token length: ${config.apiToken?.length || 0} characters`);
    logger.debug(
      "Full request headers:",
      JSON.stringify(
        headers,
        (key, value) => (key === "Authorization" ? "Basic ***" : value),
        2
      )
    );
    const curlCmd = `curl -X ${method} -H "Content-Type: application/json" -H "Accept: application/json" -H "User-Agent: MCP-Atlassian-Server/1.0.0" -u "${
      config.email
    }:${config.apiToken.substring(0, 5)}..." "${url}"$${
      data && (method === "POST" || method === "PUT")
        ? ` -d '${JSON.stringify(data)}'`
        : ""
    }`;
    logger.info(`Debug with curl: ${curlCmd}`);
    const fetchOptions: RequestInit = {
      method,
      headers,
      credentials: "omit",
    };
    if (data && (method === "POST" || method === "PUT")) {
      fetchOptions.body = JSON.stringify(data);
    }
    logger.debug("Fetch options:", {
      ...fetchOptions,
      headers: { ...headers, Authorization: "Basic ***" },
    });
    const response = await fetch(url, fetchOptions);
    if (!response.ok) {
      const statusCode = response.status;
      const responseText = await response.text();
      logger.error(`Confluence API error (${statusCode}):`, responseText);
      throw new ApiError(
        ApiErrorType.SERVER_ERROR,
        `Confluence API error: ${responseText}`,
        statusCode,
        new Error(responseText)
      );
    }
    if (method === 'DELETE') {
      const text = await response.text();
      if (!text) return true as any;
      try {
        return JSON.parse(text) as T;
      } catch {
        return true as any;
      }
    }
    const responseData = await response.json();
    return responseData as T;
  } catch (error: any) {
    if (error instanceof ApiError) {
      throw error;
    }
    logger.error("Unhandled error in Confluence API call:", error);
    throw new ApiError(
      ApiErrorType.UNKNOWN_ERROR,
      `Unknown error: ${
        error instanceof Error ? error.message : String(error)
      }`,
      500
    );
  }
}

// Helper: Convert Atlassian Document Format to simple Markdown
export function adfToMarkdown(content: any): string {
  if (!content || !content.content) return "";
  let markdown = "";
  const processNode = (node: any): string => {
    if (!node) return "";
    switch (node.type) {
      case "paragraph":
        return node.content
          ? node.content.map(processNode).join("") + "\n\n"
          : "\n\n";
      case "text":
        let text = node.text || "";
        if (node.marks) {
          node.marks.forEach((mark: any) => {
            switch (mark.type) {
              case "strong":
                text = `**${text}**`;
                break;
              case "em":
                text = `*${text}*`;
                break;
              case "code":
                text = `\`${text}\``;
                break;
              case "link":
                text = `[${text}](${mark.attrs.href})`;
                break;
            }
          });
        }
        return text;
      case "heading":
        const level = node.attrs.level;
        const headingContent = node.content
          ? node.content.map(processNode).join("")
          : "";
        return "#".repeat(level) + " " + headingContent + "\n\n";
      case "bulletList":
        return node.content ? node.content.map(processNode).join("") : "";
      case "listItem":
        return (
          "- " +
          (node.content ? node.content.map(processNode).join("") : "") +
          "\n"
        );
      case "orderedList":
        return node.content
          ? node.content
              .map((item: any, index: number) => {
                return `${index + 1}. ${processNode(item)}`;
              })
              .join("")
          : "";
      case "codeBlock":
        const code = node.content ? node.content.map(processNode).join("") : "";
        const language =
          node.attrs && node.attrs.language ? node.attrs.language : "";
        return `
                
                
                ${language}\n${code}\n
                
                `;
      default:
        return node.content ? node.content.map(processNode).join("") : "";
    }
  };
  content.content.forEach((node: any) => {
    markdown += processNode(node);
  });
  return markdown;
} 
```

--------------------------------------------------------------------------------
/src/schemas/jira.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Schema definitions for Jira resources
 */
import { standardMetadataSchema } from './common.js';

/**
 * Schema for Jira issue
 */
export const issueSchema = {
  type: "object",
  properties: {
    id: { type: "string", description: "Issue ID" },
    key: { type: "string", description: "Issue key (e.g., PROJ-123)" },
    summary: { type: "string", description: "Issue title/summary" },
    description: { anyOf: [
      { type: "string", description: "Issue description as plain text" },
      { type: "object", description: "Issue description in ADF format" }
    ], nullable: true },
    rawDescription: { anyOf: [
      { type: "string", description: "Issue description as plain text" },
      { type: "object", description: "Issue description in ADF format" }
    ], nullable: true },
    status: { 
      type: "object", 
      properties: {
        name: { type: "string", description: "Status name" },
        id: { type: "string", description: "Status ID" }
      }
    },
    assignee: {
      type: "object",
      properties: {
        displayName: { type: "string", description: "Assignee's display name" },
        accountId: { type: "string", description: "Assignee's account ID" }
      },
      nullable: true
    },
    reporter: {
      type: "object",
      properties: {
        displayName: { type: "string", description: "Reporter's display name" },
        accountId: { type: "string", description: "Reporter's account ID" }
      },
      nullable: true
    },
    priority: {
      type: "object",
      properties: {
        name: { type: "string", description: "Priority name" },
        id: { type: "string", description: "Priority ID" }
      },
      nullable: true
    },
    labels: {
      type: "array",
      items: { type: "string" },
      description: "List of labels attached to the issue"
    },
    created: { type: "string", format: "date-time", description: "Creation date" },
    updated: { type: "string", format: "date-time", description: "Last update date" },
    issueType: {
      type: "object",
      properties: {
        name: { type: "string", description: "Issue type name" },
        id: { type: "string", description: "Issue type ID" }
      }
    },
    projectKey: { type: "string", description: "Project key" },
    projectName: { type: "string", description: "Project name" }
  },
  required: ["id", "key", "summary", "status", "issueType", "projectKey"]
};

/**
 * Schema for Jira issues list
 */
export const issuesListSchema = {
  type: "object",
  properties: {
    metadata: standardMetadataSchema,
    issues: {
      type: "array",
      items: issueSchema
    }
  },
  required: ["metadata", "issues"]
};

/**
 * Schema for Jira transitions
 */
export const transitionSchema = {
  type: "object",
  properties: {
    id: { type: "string", description: "Transition ID" },
    name: { type: "string", description: "Transition name" },
    to: {
      type: "object",
      properties: {
        id: { type: "string", description: "Status ID after transition" },
        name: { type: "string", description: "Status name after transition" }
      }
    }
  },
  required: ["id", "name"]
};

/**
 * Schema for Jira transitions list
 */
export const transitionsListSchema = {
  type: "object",
  properties: {
    issueKey: { type: "string", description: "Issue key" },
    transitions: {
      type: "array",
      items: transitionSchema
    }
  },
  required: ["issueKey", "transitions"]
};

/**
 * Schema for Jira project
 */
export const projectSchema = {
  type: "object",
  properties: {
    id: { type: "string", description: "Project ID" },
    key: { type: "string", description: "Project key" },
    name: { type: "string", description: "Project name" },
    projectTypeKey: { type: "string", description: "Project type" },
    url: { type: "string", description: "Project URL" },
    lead: {
      type: "object",
      properties: {
        displayName: { type: "string", description: "Project lead's display name" },
        accountId: { type: "string", description: "Project lead's account ID" }
      },
      nullable: true
    }
  },
  required: ["id", "key", "name"]
};

/**
 * Schema for Jira projects list
 */
export const projectsListSchema = {
  type: "object",
  properties: {
    metadata: standardMetadataSchema,
    projects: {
      type: "array",
      items: projectSchema
    }
  },
  required: ["metadata", "projects"]
};

/**
 * Schema for Jira user
 */
export const userSchema = {
  type: "object",
  properties: {
    accountId: { type: "string", description: "User account ID" },
    displayName: { type: "string", description: "User display name" },
    emailAddress: { type: "string", description: "User email address", nullable: true },
    active: { type: "boolean", description: "Whether the user is active" },
    avatarUrl: { type: "string", description: "URL to user avatar" },
    timeZone: { type: "string", description: "User timezone", nullable: true },
    locale: { type: "string", description: "User locale", nullable: true }
  },
  required: ["accountId", "displayName", "active"]
};

/**
 * Schema for Jira users list
 */
export const usersListSchema = {
  type: "object",
  properties: {
    metadata: standardMetadataSchema,
    users: {
      type: "array",
      items: userSchema
    }
  },
  required: ["metadata", "users"]
};

/**
 * Schema for Jira comment
 */
export const commentSchema = {
  type: "object",
  properties: {
    id: { type: "string", description: "Comment ID" },
    body: { anyOf: [
      { type: "string", description: "Comment body as plain text" },
      { type: "object", description: "Comment body in ADF format" }
    ] },
    rawBody: { anyOf: [
      { type: "string", description: "Comment body as plain text" },
      { type: "object", description: "Comment body in ADF format" }
    ] },
    author: {
      type: "object",
      properties: {
        displayName: { type: "string", description: "Author's display name" },
        accountId: { type: "string", description: "Author's account ID" }
      }
    },
    created: { type: "string", format: "date-time", description: "Creation date" },
    updated: { type: "string", format: "date-time", description: "Last update date" }
  },
  required: ["id", "body", "author", "created"]
};

/**
 * Schema for Jira comments list
 */
export const commentsListSchema = {
  type: "object",
  properties: {
    metadata: standardMetadataSchema,
    comments: {
      type: "array",
      items: commentSchema
    },
    issueKey: { type: "string", description: "Issue key" }
  },
  required: ["metadata", "comments", "issueKey"]
};

// Filter schemas
export const filterSchema = {
  type: "object",
  properties: {
    id: { type: "string", description: "Filter ID" },
    name: { type: "string", description: "Filter name" },
    jql: { type: "string", description: "JQL query" },
    description: { type: "string", description: "Filter description" },
    owner: { 
      type: "object", 
      properties: {
        displayName: { type: "string" },
        accountId: { type: "string" }
      }
    },
    favourite: { type: "boolean", description: "Whether the filter is favorited" },
    sharePermissions: { type: "array", description: "Share permissions" }
  }
};

export const filterListSchema = {
  type: "object",
  properties: {
    filters: {
      type: "array",
      items: filterSchema
    },
    metadata: standardMetadataSchema
  }
};

// Board schemas
export const boardSchema = {
  type: "object",
  properties: {
    id: { type: "number", description: "Board ID" },
    name: { type: "string", description: "Board name" },
    type: { type: "string", description: "Board type (scrum, kanban)" },
    location: { 
      type: "object", 
      properties: {
        projectId: { type: "string" },
        displayName: { type: "string" },
        projectKey: { type: "string" },
        projectName: { type: "string" }
      }
    }
  }
};

export const boardListSchema = {
  type: "object",
  properties: {
    boards: {
      type: "array",
      items: boardSchema
    },
    metadata: standardMetadataSchema
  }
};

// Sprint schemas
export const sprintSchema = {
  type: "object",
  properties: {
    id: { type: "number", description: "Sprint ID" },
    name: { type: "string", description: "Sprint name" },
    state: { type: "string", description: "Sprint state (future, active, closed)" },
    startDate: { type: "string", description: "Start date" },
    endDate: { type: "string", description: "End date" },
    goal: { type: "string", description: "Sprint goal" },
    originBoardId: { type: "number", description: "Board ID" }
  }
};

export const sprintListSchema = {
  type: "object",
  properties: {
    sprints: {
      type: "array",
      items: sprintSchema
    },
    metadata: standardMetadataSchema
  }
};

// Dashboard schemas
export const dashboardSchema = {
  type: "object",
  properties: {
    id: { type: "string", description: "Dashboard ID" },
    name: { type: "string", description: "Dashboard name" },
    description: { type: "string", description: "Dashboard description", nullable: true },
    owner: {
      type: "object",
      properties: {
        displayName: { type: "string" },
        accountId: { type: "string" }
      },
      nullable: true
    },
    sharePermissions: { type: "array", description: "Share permissions", items: { type: "object" }, nullable: true },
    gadgets: { type: "array", items: { type: "object" }, nullable: true },
    isFavourite: { type: "boolean", description: "Is favourite", nullable: true },
    view: { type: "string", description: "View type", nullable: true },
    url: { type: "string", description: "Dashboard URL", nullable: true }
  },
  required: ["id", "name"]
};

export const dashboardListSchema = {
  type: "object",
  properties: {
    dashboards: { type: "array", items: dashboardSchema },
    total: { type: "number", description: "Total dashboards" },
    maxResults: { type: "number", description: "Max results per page" },
    startAt: { type: "number", description: "Start offset" },
    metadata: standardMetadataSchema
  },
  required: ["dashboards", "total"]
};

// Gadget schemas
export const gadgetSchema = {
  type: "object",
  properties: {
    id: { type: "string", description: "Gadget ID" },
    title: { type: "string", description: "Gadget title" },
    color: { type: "string", description: "Gadget color", nullable: true },
    position: { type: "object", description: "Gadget position", nullable: true },
    uri: { type: "string", description: "Gadget URI", nullable: true },
    properties: { type: "object", description: "Gadget properties", nullable: true }
  },
  required: ["id", "title"]
};

export const gadgetListSchema = {
  type: "object",
  properties: {
    gadgets: { type: "array", items: gadgetSchema },
    metadata: standardMetadataSchema
  },
  required: ["gadgets"]
}; 
```

--------------------------------------------------------------------------------
/src/resources/jira/users.ts:
--------------------------------------------------------------------------------

```typescript
import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
import { Logger } from '../../utils/logger.js';
import { AtlassianConfig } from '../../utils/atlassian-api-base.js';
import fetch from 'cross-fetch';
import { usersListSchema, userSchema } from '../../schemas/jira.js';
import { Config, Resources } from '../../utils/mcp-helpers.js';

const logger = Logger.getLogger('JiraResource:Users');

/**
 * Helper function to get the list of users from Jira (supports pagination)
 */
async function getUsers(config: AtlassianConfig, startAt = 0, maxResults = 20, accountId?: string, username?: string): Promise<any[]> {
  try {
    const auth = Buffer.from(`${config.email}:${config.apiToken}`).toString('base64');
    const headers = {
      'Authorization': `Basic ${auth}`,
      'Content-Type': 'application/json',
      'Accept': 'application/json',
      'User-Agent': 'MCP-Atlassian-Server/1.0.0'
    };
    let baseUrl = config.baseUrl;
    if (!baseUrl.startsWith('https://')) {
      baseUrl = `https://${baseUrl}`;
    }
    // Only filter by accountId or username
    let url = `${baseUrl}/rest/api/2/user/search?startAt=${startAt}&maxResults=${maxResults}`;
    if (accountId && accountId.trim()) {
      url += `&accountId=${encodeURIComponent(accountId.trim())}`;
    } else if (username && username.trim()) {
      url += `&username=${encodeURIComponent(username.trim())}`;
    }
    logger.debug(`Getting Jira users: ${url}`);
    const response = await fetch(url, { method: 'GET', headers, credentials: 'omit' });
    if (!response.ok) {
      const statusCode = response.status;
      const responseText = await response.text();
      logger.error(`Jira API error (${statusCode}):`, responseText);
      throw new Error(`Jira API error: ${responseText}`);
    }
    const users = await response.json();
    return users;
  } catch (error) {
    logger.error(`Error getting Jira users:`, error);
    throw error;
  }
}

/**
 * Helper function to get user details from Jira
 */
async function getUser(config: AtlassianConfig, accountId: string): Promise<any> {
  try {
    const auth = Buffer.from(`${config.email}:${config.apiToken}`).toString('base64');
    const headers = {
      'Authorization': `Basic ${auth}`,
      'Content-Type': 'application/json',
      'Accept': 'application/json',
      'User-Agent': 'MCP-Atlassian-Server/1.0.0'
    };
    let baseUrl = config.baseUrl;
    if (!baseUrl.startsWith('https://')) {
      baseUrl = `https://${baseUrl}`;
    }
    // API get user: /rest/api/3/user?accountId=...
    const url = `${baseUrl}/rest/api/3/user?accountId=${encodeURIComponent(accountId)}`;
    logger.debug(`Getting Jira user: ${url}`);
    const response = await fetch(url, { method: 'GET', headers, credentials: 'omit' });
    if (!response.ok) {
      const statusCode = response.status;
      const responseText = await response.text();
      logger.error(`Jira API error (${statusCode}):`, responseText);
      throw new Error(`Jira API error: ${responseText}`);
    }
    const user = await response.json();
    return user;
  } catch (error) {
    logger.error(`Error getting Jira user:`, error);
    throw error;
  }
}

/**
 * Register Jira user-related resources
 * @param server MCP Server instance
 */
export function registerUserResources(server: McpServer) {
  logger.info('Registering Jira user resources...');

  // Resource: Root users resource
  server.resource(
    'jira-users-root',
    new ResourceTemplate('jira://users', {
      list: async (_extra) => ({
        resources: [
          {
            uri: 'jira://users',
            name: 'Jira Users',
            description: 'List and search all Jira users (use filters)',
            mimeType: 'application/json'
          }
        ]
      })
    }),
    async (uri, _params, _extra) => {
      const uriString = typeof uri === 'string' ? uri : uri.href;
      return {
        contents: [{
          uri: uriString,
          mimeType: 'application/json',
          text: JSON.stringify({
            message: "Please use a more specific user resource. The Jira API requires parameters to search users.",
            suggestedResources: [
              "jira://users/{accountId} - Get details for a specific user",
              "jira://users/assignable/{projectKey} - Get users who can be assigned in a project",
              "jira://users/role/{projectKey}/{roleId} - Get users with specific role in a project"
            ]
          })
        }]
      };
    }
  );

  // Resource: User details
  server.resource(
    'jira-user-details',
    new ResourceTemplate('jira://users/{accountId}', {
      list: async (_extra) => ({
        resources: [
          {
            uri: 'jira://users/{accountId}',
            name: 'Jira User Details',
            description: 'Get details for a specific Jira user by accountId. Replace {accountId} with the user accountId.',
            mimeType: 'application/json'
          }
        ]
      })
    }),
    async (uri, params, _extra) => {
      let normalizedAccountId = '';
      try {
        const config = Config.getAtlassianConfigFromEnv();
        if (!params.accountId) {
          throw new Error('Missing accountId in URI');
        }
        normalizedAccountId = Array.isArray(params.accountId) ? params.accountId[0] : params.accountId;
        logger.info(`Getting details for Jira user: ${normalizedAccountId}`);
        const user = await getUser(config, normalizedAccountId);
        // Format returned data
        const formattedUser = {
          accountId: user.accountId,
          displayName: user.displayName,
          emailAddress: user.emailAddress,
          active: user.active,
          avatarUrl: user.avatarUrls?.['48x48'] || '',
          timeZone: user.timeZone,
          locale: user.locale
        };
        
        const uriString = typeof uri === 'string' ? uri : uri.href;
        // Chuẩn hóa metadata/schema cho resource chi tiết user
        return Resources.createStandardResource(
          uriString,
          [formattedUser],
          'user',
          userSchema,
          1,
          1,
          0,
          user.self || ''
        );
      } catch (error) {
        logger.error(`Error getting Jira user ${normalizedAccountId}:`, error);
        throw error;
      }
    }
  );

  // Resource: List of assignable users for a project
  server.resource(
    'jira-users-assignable',
    new ResourceTemplate('jira://users/assignable/{projectKey}', {
      list: async (_extra) => ({
        resources: [
          {
            uri: 'jira://users/assignable/{projectKey}',
            name: 'Jira Assignable Users',
            description: 'List assignable users for a Jira project. Replace {projectKey} with the project key.',
            mimeType: 'application/json'
          }
        ]
      })
    }),
    async (uri, params, _extra) => {
      try {
        const config = Config.getAtlassianConfigFromEnv();
        const projectKey = Array.isArray(params.projectKey) ? params.projectKey[0] : params.projectKey;
        if (!projectKey) throw new Error('Missing projectKey');
        const auth = Buffer.from(`${config.email}:${config.apiToken}`).toString('base64');
        const headers = {
          'Authorization': `Basic ${auth}`,
          'Content-Type': 'application/json',
          'Accept': 'application/json',
          'User-Agent': 'MCP-Atlassian-Server/1.0.0'
        };
        let baseUrl = config.baseUrl;
        if (!baseUrl.startsWith('https://')) baseUrl = `https://${baseUrl}`;
        const url = `${baseUrl}/rest/api/3/user/assignable/search?project=${encodeURIComponent(projectKey)}`;
        logger.info(`Getting assignable users for project ${projectKey}: ${url}`);
        const response = await fetch(url, { method: 'GET', headers, credentials: 'omit' });
        if (!response.ok) {
          const statusCode = response.status;
          const responseText = await response.text();
          logger.error(`Jira API error (${statusCode}):`, responseText);
          throw new Error(`Jira API error: ${responseText}`);
        }
        const users = await response.json();
        const formattedUsers = (users || []).map((user: any) => ({
          accountId: user.accountId,
          displayName: user.displayName,
          emailAddress: user.emailAddress,
          active: user.active,
          avatarUrl: user.avatarUrls?.['48x48'] || '',
        }));
        
        const uriString = typeof uri === 'string' ? uri : uri.href;
        // Chuẩn hóa metadata/schema
        return Resources.createStandardResource(
          uriString,
          formattedUsers,
          'users',
          usersListSchema,
          formattedUsers.length,
          formattedUsers.length,
          0,
          `${config.baseUrl}/jira/people`
        );
      } catch (error) {
        logger.error(`Error getting assignable users for project:`, error);
        throw error;
      }
    }
  );

  // Resource: List of users by role in a project
  server.resource(
    'jira-users-role',
    new ResourceTemplate('jira://users/role/{projectKey}/{roleId}', {
      list: async (_extra) => ({
        resources: [
          {
            uri: 'jira://users/role/{projectKey}/{roleId}',
            name: 'Jira Users by Role',
            description: 'List users by role in a Jira project. Replace {projectKey} and {roleId} with the project key and role ID.',
            mimeType: 'application/json'
          }
        ]
      })
    }),
    async (uri, params, _extra) => {
      try {
        const config = Config.getAtlassianConfigFromEnv();
        const projectKey = Array.isArray(params.projectKey) ? params.projectKey[0] : params.projectKey;
        const roleId = Array.isArray(params.roleId) ? params.roleId[0] : params.roleId;
        if (!projectKey || !roleId) throw new Error('Missing projectKey or roleId');
        const auth = Buffer.from(`${config.email}:${config.apiToken}`).toString('base64');
        const headers = {
          'Authorization': `Basic ${auth}`,
          'Content-Type': 'application/json',
          'Accept': 'application/json',
          'User-Agent': 'MCP-Atlassian-Server/1.0.0'
        };
        let baseUrl = config.baseUrl;
        if (!baseUrl.startsWith('https://')) baseUrl = `https://${baseUrl}`;
        const url = `${baseUrl}/rest/api/3/project/${encodeURIComponent(projectKey)}/role/${encodeURIComponent(roleId)}`;
        logger.info(`Getting users in role for project ${projectKey}, role ${roleId}: ${url}`);
        const response = await fetch(url, { method: 'GET', headers, credentials: 'omit' });
        if (!response.ok) {
          const statusCode = response.status;
          const responseText = await response.text();
          logger.error(`Jira API error (${statusCode}):`, responseText);
          throw new Error(`Jira API error: ${responseText}`);
        }
        const roleData = await response.json();
        const formattedUsers = (roleData.actors || [])
          .filter((actor: any) => actor.actorUser && actor.actorUser.accountId)
          .map((actor: any) => ({
            accountId: actor.actorUser.accountId,
            displayName: actor.displayName,
            type: 'atlassian-user-role-actor',
            roleId: roleId
          }));
        
        const uriString = typeof uri === 'string' ? uri : uri.href;
        return Resources.createStandardResource(
          uriString,
          formattedUsers,
          'users',
          usersListSchema,
          formattedUsers.length,
          formattedUsers.length,
          0,
          `${config.baseUrl}/jira/projects/${projectKey}/people`
        );
      } catch (error) {
        logger.error(`Error getting users by role:`, error);
        throw error;
      }
    }
  );

  logger.info('Jira user resources registered successfully');
}
```

--------------------------------------------------------------------------------
/src/utils/jira-tool-api-agile.ts:
--------------------------------------------------------------------------------

```typescript
import { AtlassianConfig, logger, createBasicHeaders } from './atlassian-api-base.js';
import { normalizeAtlassianBaseUrl } from './atlassian-api-base.js';
import { ApiError, ApiErrorType } from './error-handler.js';

// Add issues to backlog (support both /backlog/issue and /backlog/{boardId}/issue)
export async function addIssuesToBacklog(config: AtlassianConfig, issueKeys: string[], boardId?: string): Promise<any> {
  try {
    const headers = createBasicHeaders(config.email, config.apiToken);
    const baseUrl = normalizeAtlassianBaseUrl(config.baseUrl);
    const url = boardId
      ? `${baseUrl}/rest/agile/1.0/backlog/${boardId}/issue`
      : `${baseUrl}/rest/agile/1.0/backlog/issue`;
    const data = { issues: issueKeys };
    logger.debug(`Adding issues to backlog${boardId ? ` for board ${boardId}` : ''}: ${issueKeys.join(', ')}`);
    const response = await fetch(url, {
      method: 'POST',
      headers,
      body: JSON.stringify(data),
      credentials: 'omit',
    });
    if (!response.ok) {
      const responseText = await response.text();
      logger.error(`Jira API error (${response.status}):`, responseText);
      throw new Error(`Jira API error: ${response.status} ${responseText}`);
    }
    // Xử lý response rỗng
    const contentLength = response.headers.get('content-length');
    if (contentLength === '0' || response.status === 204) {
      return { success: true };
    }
    const text = await response.text();
    if (!text) return { success: true };
    try {
      return JSON.parse(text);
    } catch (e) {
      return { success: true };
    }
  } catch (error) {
    logger.error(`Error adding issues to backlog:`, error);
    throw error;
  }
}

/**
 * Di chuyển issues vào sprint (POST /rest/agile/1.0/sprint/{sprintId}/issue)
 * Sprint đích phải là future hoặc active. API trả về response rỗng khi thành công.
 * @param config cấu hình Atlassian
 * @param sprintId ID của sprint đích
 * @param issueKeys mảng issue key cần di chuyển
 */
export async function addIssueToSprint(config: AtlassianConfig, sprintId: string, issueKeys: string[]): Promise<any> {
  try {
    const headers = createBasicHeaders(config.email, config.apiToken);
    const baseUrl = normalizeAtlassianBaseUrl(config.baseUrl);
    const url = `${baseUrl}/rest/agile/1.0/sprint/${sprintId}/issue`;
    const data = { issues: issueKeys };
    logger.debug(`Adding issues to sprint ${sprintId}: ${issueKeys.join(', ')}`);
    const response = await fetch(url, {
      method: 'POST',
      headers,
      body: JSON.stringify(data),
      credentials: 'omit',
    });
    if (!response.ok) {
      const responseText = await response.text();
      logger.error(`Jira API error (${response.status}):`, responseText);
      throw new Error(`Jira API error: ${response.status} ${responseText}`);
    }
    // Xử lý response rỗng
    const contentLength = response.headers.get('content-length');
    if (contentLength === '0' || response.status === 204) {
      return { success: true };
    }
    const text = await response.text();
    if (!text) return { success: true };
    try {
      return JSON.parse(text);
    } catch (e) {
      return { success: true };
    }
  } catch (error) {
    logger.error(`Error adding issues to sprint:`, error);
    throw error;
  }
}

// Sắp xếp thứ tự backlog
export async function rankBacklogIssues(config: AtlassianConfig, boardId: string, issueKeys: string[], options: { rankBeforeIssue?: string, rankAfterIssue?: string } = {}): Promise<any> {
  try {
    const headers = createBasicHeaders(config.email, config.apiToken);
    const baseUrl = normalizeAtlassianBaseUrl(config.baseUrl);
    const url = `${baseUrl}/rest/agile/1.0/issue/rank`;
    const data: any = { issues: issueKeys };
    if (options.rankBeforeIssue) data.rankBeforeIssue = options.rankBeforeIssue;
    if (options.rankAfterIssue) data.rankAfterIssue = options.rankAfterIssue;
    logger.debug(`Ranking issues in backlog: ${issueKeys.join(', ')}`);
    const response = await fetch(url, {
      method: 'PUT',
      headers,
      body: JSON.stringify(data),
      credentials: 'omit',
    });
    if (!response.ok) {
      const responseText = await response.text();
      logger.error(`Jira API error (${response.status}):`, responseText);
      throw new Error(`Jira API error: ${response.status} ${responseText}`);
    }
    // Xử lý response rỗng
    const contentLength = response.headers.get('content-length');
    if (contentLength === '0' || response.status === 204) {
      return { success: true };
    }
    const text = await response.text();
    if (!text) return { success: true };
    try {
      return JSON.parse(text);
    } catch (e) {
      return { success: true };
    }
  } catch (error) {
    logger.error(`Error ranking backlog issues:`, error);
    throw error;
  }
}

// Bắt đầu sprint
export async function startSprint(config: AtlassianConfig, sprintId: string, startDate: string, endDate: string, goal?: string): Promise<any> {
  try {
    const headers = createBasicHeaders(config.email, config.apiToken);
    const baseUrl = normalizeAtlassianBaseUrl(config.baseUrl);
    const url = `${baseUrl}/rest/agile/1.0/sprint/${sprintId}`;
    const data: any = {
      state: 'active',
      startDate,
      endDate
    };
    if (goal) data.goal = goal;
    logger.debug(`Starting sprint ${sprintId}`);
    const response = await fetch(url, {
      method: 'POST',
      headers,
      body: JSON.stringify(data),
      credentials: 'omit',
    });
    if (!response.ok) {
      const responseText = await response.text();
      logger.error(`Jira API error (${response.status}):`, responseText);
      throw new Error(`Jira API error: ${response.status} ${responseText}`);
    }
    return await response.json();
  } catch (error) {
    logger.error(`Error starting sprint ${sprintId}:`, error);
    throw error;
  }
}

// Đóng sprint
export async function closeSprint(config: AtlassianConfig, sprintId: string, options: { completeDate?: string } = {}): Promise<any> {
  try {
    const headers = createBasicHeaders(config.email, config.apiToken);
    const baseUrl = normalizeAtlassianBaseUrl(config.baseUrl);
    const url = `${baseUrl}/rest/agile/1.0/sprint/${sprintId}`;
    // Chỉ build payload với các trường hợp lệ
    const data: any = { state: 'closed' };
    if (options.completeDate) data.completeDate = options.completeDate;
    // (Không gửi moveToSprintId, createNewSprint vì API không hỗ trợ)
    logger.debug(`Closing sprint ${sprintId} with payload:`, JSON.stringify(data));
    const response = await fetch(url, {
      method: 'POST',
      headers,
      body: JSON.stringify(data),
      credentials: 'omit',
    });
    if (!response.ok) {
      const responseText = await response.text();
      logger.error(`Jira API error (${response.status}):`, responseText);
      throw new Error(`Jira API error: ${response.status} ${responseText}`);
    }
    return await response.json();
  } catch (error) {
    logger.error(`Error closing sprint ${sprintId}:`, error);
    throw error;
  }
}

// Di chuyển issues giữa các sprint
export async function moveIssuesBetweenSprints(config: AtlassianConfig, fromSprintId: string, toSprintId: string, issueKeys: string[]): Promise<any> {
  try {
    const headers = createBasicHeaders(config.email, config.apiToken);
    const baseUrl = normalizeAtlassianBaseUrl(config.baseUrl);
    logger.debug(`Moving issues from sprint ${fromSprintId} to sprint ${toSprintId}: ${issueKeys.join(', ')}`);
    // Remove from old sprint
    const removeUrl = `${baseUrl}/rest/agile/1.0/sprint/${fromSprintId}/issue`;
    const removeResponse = await fetch(removeUrl, {
      method: 'POST',
      headers,
      body: JSON.stringify({ issues: issueKeys, remove: true }),
      credentials: 'omit',
    });
    if (!removeResponse.ok) {
      const responseText = await removeResponse.text();
      logger.error(`Jira API error (${removeResponse.status}):`, responseText);
      throw new Error(`Jira API error: ${removeResponse.status} ${responseText}`);
    }
    // Add to new sprint
    const addUrl = `${baseUrl}/rest/agile/1.0/sprint/${toSprintId}/issue`;
    const addResponse = await fetch(addUrl, {
      method: 'POST',
      headers,
      body: JSON.stringify({ issues: issueKeys }),
      credentials: 'omit',
    });
    if (!addResponse.ok) {
      const responseText = await addResponse.text();
      logger.error(`Jira API error (${addResponse.status}):`, responseText);
      throw new Error(`Jira API error: ${addResponse.status} ${responseText}`);
    }
    return await addResponse.json();
  } catch (error) {
    logger.error(`Error moving issues between sprints:`, error);
    throw error;
  }
}

// Thêm issue vào board
export async function addIssueToBoard(config: AtlassianConfig, boardId: string, issueKey: string | string[]): Promise<any> {
  try {
    const headers = createBasicHeaders(config.email, config.apiToken);
    const baseUrl = normalizeAtlassianBaseUrl(config.baseUrl);
    const url = `${baseUrl}/rest/agile/1.0/backlog/issue`;
    const issues = Array.isArray(issueKey) ? issueKey : [issueKey];
    const data = { issues };
    logger.debug(`Adding issue(s) to board ${boardId}: ${Array.isArray(issueKey) ? issueKey.join(', ') : issueKey}`);
    const response = await fetch(url, {
      method: 'POST',
      headers,
      body: JSON.stringify(data),
      credentials: 'omit',
    });
    if (!response.ok) {
      const responseText = await response.text();
      logger.error(`Jira API error (${response.status}):`, responseText);
      throw new Error(`Jira API error: ${response.status} ${responseText}`);
    }
    // Xử lý response rỗng
    const contentLength = response.headers.get('content-length');
    if (contentLength === '0' || response.status === 204) {
      return { success: true };
    }
    const text = await response.text();
    if (!text) return { success: true };
    try {
      return JSON.parse(text);
    } catch (e) {
      return { success: true };
    }
  } catch (error) {
    logger.error(`Error adding issue to board:`, error);
    throw error;
  }
}

// Cấu hình cột board
export async function configureBoardColumns(config: AtlassianConfig, boardId: string, columns: any[]): Promise<any> {
  try {
    const headers = createBasicHeaders(config.email, config.apiToken);
    const baseUrl = normalizeAtlassianBaseUrl(config.baseUrl);
    const url = `${baseUrl}/rest/agile/1.0/board/${boardId}/configuration`;
    logger.debug(`Configuring columns for board ${boardId}`);
    // Get current config to merge
    const currentRes = await fetch(url, { method: 'GET', headers, credentials: 'omit' });
    if (!currentRes.ok) {
      const responseText = await currentRes.text();
      logger.error(`Jira API error (${currentRes.status}):`, responseText);
      throw new Error(`Jira API error: ${currentRes.status} ${responseText}`);
    }
    const currentConfig = await currentRes.json();
    const data = { ...currentConfig, columnConfig: { columns } };
    const response = await fetch(url, {
      method: 'PUT',
      headers,
      body: JSON.stringify(data),
      credentials: 'omit',
    });
    if (!response.ok) {
      const responseText = await response.text();
      logger.error(`Jira API error (${response.status}):`, responseText);
      throw new Error(`Jira API error: ${response.status} ${responseText}`);
    }
    return await response.json();
  } catch (error) {
    logger.error(`Error configuring board columns:`, error);
    throw error;
  }
}

// Tạo sprint mới
export async function createSprint(
  config: AtlassianConfig,
  boardId: string,
  name: string,
  startDate?: string,
  endDate?: string,
  goal?: string
): Promise<any> {
  try {
    const headers = createBasicHeaders(config.email, config.apiToken);
    const baseUrl = normalizeAtlassianBaseUrl(config.baseUrl);
    const url = `${baseUrl}/rest/agile/1.0/sprint`;
    const data: any = {
      name,
      originBoardId: boardId
    };
    if (startDate) data.startDate = startDate;
    if (endDate) data.endDate = endDate;
    if (goal) data.goal = goal;
    logger.debug(`Creating new sprint "${name}" for board ${boardId}`);
    const response = await fetch(url, {
      method: 'POST',
      headers,
      body: JSON.stringify(data),
      credentials: 'omit',
    });
    if (!response.ok) {
      const responseText = await response.text();
      logger.error(`Jira API error (${response.status}):`, responseText);
      throw new Error(`Jira API error: ${response.status} ${responseText}`);
    }
    return await response.json();
  } catch (error) {
    logger.error(`Error creating sprint:`, error);
    throw error;
  }
}

// ... existing code ...
// (To be filled with the full code of the above functions, keeping their implementation unchanged) 
```

--------------------------------------------------------------------------------
/src/utils/jira-resource-api.ts:
--------------------------------------------------------------------------------

```typescript
import { AtlassianConfig, logger, createBasicHeaders } from './atlassian-api-base.js';
import { normalizeAtlassianBaseUrl } from './atlassian-api-base.js';

// Get list of Jira dashboards (all)
export async function getDashboards(config: AtlassianConfig, startAt = 0, maxResults = 50): Promise<any> {
  const headers = createBasicHeaders(config.email, config.apiToken);
  let baseUrl = normalizeAtlassianBaseUrl(config.baseUrl);
  const url = `${baseUrl}/rest/api/3/dashboard?startAt=${startAt}&maxResults=${maxResults}`;
  const response = await fetch(url, { method: 'GET', headers, credentials: 'omit' });
  if (!response.ok) throw new Error(`Jira API error: ${response.status} ${await response.text()}`);
  return await response.json();
}

// Get list of Jira dashboards owned by current user (my dashboards)
export async function getMyDashboards(config: AtlassianConfig, startAt = 0, maxResults = 50): Promise<any> {
  const headers = createBasicHeaders(config.email, config.apiToken);
  let baseUrl = normalizeAtlassianBaseUrl(config.baseUrl);
  // Atlassian API: filter=my
  const url = `${baseUrl}/rest/api/3/dashboard/search?filter=my&startAt=${startAt}&maxResults=${maxResults}`;
  const response = await fetch(url, { method: 'GET', headers, credentials: 'omit' });
  if (!response.ok) throw new Error(`Jira API error: ${response.status} ${await response.text()}`);
  return await response.json();
}

// Get Jira dashboard by ID
export async function getDashboardById(config: AtlassianConfig, dashboardId: string): Promise<any> {
  const headers = createBasicHeaders(config.email, config.apiToken);
  let baseUrl = normalizeAtlassianBaseUrl(config.baseUrl);
  const url = `${baseUrl}/rest/api/3/dashboard/${dashboardId}`;
  const response = await fetch(url, { method: 'GET', headers, credentials: 'omit' });
  if (!response.ok) throw new Error(`Jira API error: ${response.status} ${await response.text()}`);
  return await response.json();
}

// Get gadgets (widgets) of a Jira dashboard
export async function getDashboardGadgets(config: AtlassianConfig, dashboardId: string): Promise<any> {
  const headers = createBasicHeaders(config.email, config.apiToken);
  let baseUrl = normalizeAtlassianBaseUrl(config.baseUrl);
  const url = `${baseUrl}/rest/api/3/dashboard/${dashboardId}/gadget`;
  const response = await fetch(url, { method: 'GET', headers, credentials: 'omit' });
  if (!response.ok) throw new Error(`Jira API error: ${response.status} ${await response.text()}`);
  const data = await response.json();
  return data.gadgets || [];
}

// Get Jira issue by key or id
export async function getIssue(config: AtlassianConfig, issueIdOrKey: string): Promise<any> {
  try {
    const headers = createBasicHeaders(config.email, config.apiToken);
    let baseUrl = config.baseUrl;
    if (baseUrl.startsWith("http://")) {
      baseUrl = baseUrl.replace("http://", "https://");
    } else if (!baseUrl.startsWith("https://")) {
      baseUrl = `https://${baseUrl}`;
    }
    if (!baseUrl.includes(".atlassian.net")) {
      baseUrl = `${baseUrl}.atlassian.net`;
    }
    if (baseUrl.match(/\.atlassian\.net\.atlassian\.net/)) {
      baseUrl = baseUrl.replace(
        ".atlassian.net.atlassian.net",
        ".atlassian.net"
      );
    }
    const url = `${baseUrl}/rest/api/3/issue/${issueIdOrKey}?expand=renderedFields,names,schema,operations`;
    logger.debug(`Getting issue with direct fetch: ${url}`);
    logger.debug(`With Auth: ${config.email}:*****`);
    const curlCmd = `curl -X GET -H "Content-Type: application/json" -H "Accept: application/json" -H "User-Agent: MCP-Atlassian-Server/1.0.0" -u "${
      config.email
    }:${config.apiToken.substring(0, 5)}..." "${url}"`;
    logger.info(`Debug with curl: ${curlCmd}`);
    const response = await fetch(url, {
      method: "GET",
      headers,
      credentials: "omit",
    });
    if (!response.ok) {
      const statusCode = response.status;
      const responseText = await response.text();
      logger.error(`Jira API error (${statusCode}):`, responseText);
      throw new Error(`Jira API error (${statusCode}): ${responseText}`);
    }
    const issue = await response.json();
    return issue;
  } catch (error: any) {
    logger.error(`Error getting issue ${issueIdOrKey}:`, error);
    throw error;
  }
}

// Search issues by JQL
export async function searchIssues(config: AtlassianConfig, jql: string, maxResults: number = 50): Promise<any> {
  try {
    const headers = createBasicHeaders(config.email, config.apiToken);
    let baseUrl = config.baseUrl;
    if (baseUrl.startsWith("http://")) {
      baseUrl = baseUrl.replace("http://", "https://");
    } else if (!baseUrl.startsWith("https://")) {
      baseUrl = `https://${baseUrl}`;
    }
    if (!baseUrl.includes(".atlassian.net")) {
      baseUrl = `${baseUrl}.atlassian.net`;
    }
    if (baseUrl.match(/\.atlassian\.net\.atlassian\.net/)) {
      baseUrl = baseUrl.replace(
        ".atlassian.net.atlassian.net",
        ".atlassian.net"
      );
    }
    const url = `${baseUrl}/rest/api/3/search`;
    logger.debug(`Searching issues with JQL: ${jql}`);
    logger.debug(`With Auth: ${config.email}:*****`);
    const data = {
      jql,
      maxResults,
      expand: ["names", "schema", "operations"],
    };
    const curlCmd = `curl -X POST -H "Content-Type: application/json" -H "Accept: application/json" -H "User-Agent: MCP-Atlassian-Server/1.0.0" -u "${
      config.email
    }:${config.apiToken.substring(0, 5)}..." "${url}" -d '${JSON.stringify(
      data
    )}'`;
    logger.info(`Debug with curl: ${curlCmd}`);
    const response = await fetch(url, {
      method: "POST",
      headers,
      body: JSON.stringify(data),
      credentials: "omit",
    });
    if (!response.ok) {
      const statusCode = response.status;
      const responseText = await response.text();
      logger.error(`Jira API error (${statusCode}):`, responseText);
      throw new Error(`Jira API error (${statusCode}): ${responseText}`);
    }
    const searchResults = await response.json();
    return searchResults;
  } catch (error: any) {
    logger.error(`Error searching issues:`, error);
    throw error;
  }
}

// Get list of Jira filters
export async function getFilters(config: AtlassianConfig, startAt = 0, maxResults = 50): Promise<any> {
  const headers = createBasicHeaders(config.email, config.apiToken);
  let baseUrl = normalizeAtlassianBaseUrl(config.baseUrl);
  const url = `${baseUrl}/rest/api/3/filter/search?startAt=${startAt}&maxResults=${maxResults}`;
  logger.debug(`GET Jira filters: ${url}`);
  const response = await fetch(url, { method: 'GET', headers, credentials: 'omit' });
  if (!response.ok) throw new Error(`Jira API error: ${response.status} ${await response.text()}`);
  return await response.json();
}

// Get Jira filter by ID
export async function getFilterById(config: AtlassianConfig, filterId: string): Promise<any> {
  const headers = createBasicHeaders(config.email, config.apiToken);
  let baseUrl = normalizeAtlassianBaseUrl(config.baseUrl);
  const url = `${baseUrl}/rest/api/3/filter/${filterId}`;
  logger.debug(`GET Jira filter by ID: ${url}`);
  const response = await fetch(url, { method: 'GET', headers, credentials: 'omit' });
  if (!response.ok) throw new Error(`Jira API error: ${response.status} ${await response.text()}`);
  return await response.json();
}

// Get filters owned by or shared with the current user
export async function getMyFilters(config: AtlassianConfig): Promise<any[]> {
  const headers = createBasicHeaders(config.email, config.apiToken);
  let baseUrl = normalizeAtlassianBaseUrl(config.baseUrl);
  const url = `${baseUrl}/rest/api/3/filter/my`;
  logger.debug(`GET Jira my filters: ${url}`);
  const response = await fetch(url, { method: 'GET', headers, credentials: 'omit' });
  if (!response.ok) throw new Error(`Jira API error: ${response.status} ${await response.text()}`);
  return await response.json();
}

// Get list of Jira boards (Agile)
export async function getBoards(config: AtlassianConfig, startAt = 0, maxResults = 50): Promise<any> {
  const headers = createBasicHeaders(config.email, config.apiToken);
  let baseUrl = normalizeAtlassianBaseUrl(config.baseUrl);
  const url = `${baseUrl}/rest/agile/1.0/board?startAt=${startAt}&maxResults=${maxResults}`;
  logger.debug(`GET Jira boards: ${url}`);
  const response = await fetch(url, { method: 'GET', headers, credentials: 'omit' });
  if (!response.ok) throw new Error(`Jira API error: ${response.status} ${await response.text()}`);
  return await response.json();
}

// Get Jira board by ID (Agile)
export async function getBoardById(config: AtlassianConfig, boardId: string): Promise<any> {
  const headers = createBasicHeaders(config.email, config.apiToken);
  let baseUrl = normalizeAtlassianBaseUrl(config.baseUrl);
  const url = `${baseUrl}/rest/agile/1.0/board/${boardId}`;
  logger.debug(`GET Jira board by ID: ${url}`);
  const response = await fetch(url, { method: 'GET', headers, credentials: 'omit' });
  if (!response.ok) throw new Error(`Jira API error: ${response.status} ${await response.text()}`);
  return await response.json();
}

// Get issues in a Jira board (Agile)
export async function getBoardIssues(config: AtlassianConfig, boardId: string, startAt = 0, maxResults = 50): Promise<any> {
  const headers = createBasicHeaders(config.email, config.apiToken);
  let baseUrl = normalizeAtlassianBaseUrl(config.baseUrl);
  const url = `${baseUrl}/rest/agile/1.0/board/${boardId}/issue?startAt=${startAt}&maxResults=${maxResults}`;
  logger.debug(`GET Jira board issues: ${url}`);
  const response = await fetch(url, { method: 'GET', headers, credentials: 'omit' });
  if (!response.ok) throw new Error(`Jira API error: ${response.status} ${await response.text()}`);
  return await response.json();
}

// Get list of sprints in a Jira board (Agile)
export async function getSprintsByBoard(config: AtlassianConfig, boardId: string, startAt = 0, maxResults = 50): Promise<any> {
  const headers = createBasicHeaders(config.email, config.apiToken);
  let baseUrl = normalizeAtlassianBaseUrl(config.baseUrl);
  const url = `${baseUrl}/rest/agile/1.0/board/${boardId}/sprint?startAt=${startAt}&maxResults=${maxResults}`;
  logger.debug(`GET Jira sprints by board: ${url}`);
  const response = await fetch(url, { method: 'GET', headers, credentials: 'omit' });
  if (!response.ok) throw new Error(`Jira API error: ${response.status} ${await response.text()}`);
  return await response.json();
}

// Get Jira sprint by ID (Agile)
export async function getSprintById(config: AtlassianConfig, sprintId: string): Promise<any> {
  const headers = createBasicHeaders(config.email, config.apiToken);
  let baseUrl = normalizeAtlassianBaseUrl(config.baseUrl);
  const url = `${baseUrl}/rest/agile/1.0/sprint/${sprintId}`;
  logger.debug(`GET Jira sprint by ID: ${url}`);
  const response = await fetch(url, { method: 'GET', headers, credentials: 'omit' });
  if (!response.ok) throw new Error(`Jira API error: ${response.status} ${await response.text()}`);
  return await response.json();
}

// Get issues in a Jira sprint (Agile)
export async function getSprintIssues(config: AtlassianConfig, sprintId: string, startAt = 0, maxResults = 50): Promise<any> {
  const headers = createBasicHeaders(config.email, config.apiToken);
  let baseUrl = normalizeAtlassianBaseUrl(config.baseUrl);
  const url = `${baseUrl}/rest/agile/1.0/sprint/${sprintId}/issue?startAt=${startAt}&maxResults=${maxResults}`;
  logger.debug(`GET Jira sprint issues: ${url}`);
  const response = await fetch(url, { method: 'GET', headers, credentials: 'omit' });
  if (!response.ok) throw new Error(`Jira API error: ${response.status} ${await response.text()}`);
  return await response.json();
}

// Get list of Jira projects (API v3)
export async function getProjects(config: AtlassianConfig): Promise<any[]> {
  const headers = createBasicHeaders(config.email, config.apiToken);
  const baseUrl = normalizeAtlassianBaseUrl(config.baseUrl);
  const url = `${baseUrl}/rest/api/3/project`;
  const response = await fetch(url, { method: 'GET', headers, credentials: 'omit' });
  if (!response.ok) throw new Error(`Jira API error: ${response.status} ${await response.text()}`);
  return await response.json();
}

// Get Jira project details (API v3)
export async function getProject(config: AtlassianConfig, projectKey: string): Promise<any> {
  const headers = createBasicHeaders(config.email, config.apiToken);
  const baseUrl = normalizeAtlassianBaseUrl(config.baseUrl);
  const url = `${baseUrl}/rest/api/3/project/${projectKey}`;
  const response = await fetch(url, { method: 'GET', headers, credentials: 'omit' });
  if (!response.ok) throw new Error(`Jira API error: ${response.status} ${await response.text()}`);
  return await response.json();
} 
```

--------------------------------------------------------------------------------
/src/resources/jira/issues.ts:
--------------------------------------------------------------------------------

```typescript
import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
import { Logger } from '../../utils/logger.js';
import { getIssue as getIssueApi, searchIssues as searchIssuesApi } from '../../utils/jira-resource-api.js';
import { issueSchema, issuesListSchema, transitionsListSchema, commentsListSchema } from '../../schemas/jira.js';
import { Config, Resources } from '../../utils/mcp-helpers.js';

const logger = Logger.getLogger('JiraResource:Issues');

/**
 * Helper function to get issue details from Jira
 */
async function getIssue(config: any, issueKey: string): Promise<any> {
  return await getIssueApi(config, issueKey);
}

/**
 * Helper function to get a list of issues from Jira (supports pagination)
 */
async function getIssues(config: any, startAt = 0, maxResults = 20, jql = ''): Promise<any> {
  const jqlQuery = jql && jql.trim() ? jql.trim() : '';
  return await searchIssuesApi(config, jqlQuery, maxResults);
}

/**
 * Helper function to search issues by JQL from Jira (supports pagination)
 */
async function searchIssuesByJql(config: any, jql: string, startAt = 0, maxResults = 20): Promise<any> {
  return await searchIssuesApi(config, jql, maxResults);
}

/**
 * Helper function to get a list of transitions for an issue from Jira
 */
async function getIssueTransitions(config: any, issueKey: string): Promise<any> {
  try {
    const auth = Buffer.from(`${config.email}:${config.apiToken}`).toString('base64');
    const headers = {
      'Authorization': `Basic ${auth}`,
      'Content-Type': 'application/json',
      'Accept': 'application/json',
      'User-Agent': 'MCP-Atlassian-Server/1.0.0'
    };
    let baseUrl = config.baseUrl;
    if (!baseUrl.startsWith('https://')) {
      baseUrl = `https://${baseUrl}`;
    }
    const url = `${baseUrl}/rest/api/3/issue/${issueKey}/transitions`;
    logger.debug(`Getting Jira issue transitions: ${url}`);
    const response = await fetch(url, { method: 'GET', headers, credentials: 'omit' });
    if (!response.ok) {
      const statusCode = response.status;
      const responseText = await response.text();
      logger.error(`Jira API error (${statusCode}):`, responseText);
      throw new Error(`Jira API error: ${responseText}`);
    }
    const data = await response.json();
    return data.transitions || [];
  } catch (error) {
    logger.error(`Error getting Jira issue transitions:`, error);
    throw error;
  }
}

/**
 * Helper function to get a list of comments for an issue from Jira
 */
async function getIssueComments(config: any, issueKey: string, startAt = 0, maxResults = 20): Promise<any> {
  try {
    const auth = Buffer.from(`${config.email}:${config.apiToken}`).toString('base64');
    const headers = {
      'Authorization': `Basic ${auth}`,
      'Content-Type': 'application/json',
      'Accept': 'application/json',
      'User-Agent': 'MCP-Atlassian-Server/1.0.0'
    };
    let baseUrl = config.baseUrl;
    if (!baseUrl.startsWith('https://')) {
      baseUrl = `https://${baseUrl}`;
    }
    const url = `${baseUrl}/rest/api/3/issue/${issueKey}/comment?startAt=${startAt}&maxResults=${maxResults}`;
    logger.debug(`Getting Jira issue comments: ${url}`);
    const response = await fetch(url, { method: 'GET', headers, credentials: 'omit' });
    if (!response.ok) {
      const statusCode = response.status;
      const responseText = await response.text();
      logger.error(`Jira API error (${statusCode}):`, responseText);
      throw new Error(`Jira API error: ${responseText}`);
    }
    const data = await response.json();
    return data;
  } catch (error) {
    logger.error(`Error getting Jira issue comments:`, error);
    throw error;
  }
}

/**
 * Hàm chuyển ADF sang text thuần
 */
function extractTextFromADF(adf: any): string {
  if (!adf || typeof adf === 'string') return adf || '';
  let text = '';
  if (adf.content) {
    adf.content.forEach((node: any) => {
      if (node.type === 'paragraph' && node.content) {
        node.content.forEach((inline: any) => {
          if (inline.type === 'text') {
            text += inline.text;
          }
        });
        text += '\n';
      }
    });
  }
  return text.trim();
}

/**
 * Format Jira issue data to standardized format
 */
function formatIssueData(issue: any, baseUrl: string): any {
  return {
    id: issue.id,
    key: issue.key,
    summary: issue.fields?.summary || '',
    description: extractTextFromADF(issue.fields?.description),
    rawDescription: issue.fields?.description || null,
    status: {
      name: issue.fields?.status?.name || 'Unknown',
      id: issue.fields?.status?.id || '0'
    },
    assignee: issue.fields?.assignee ? {
      displayName: issue.fields.assignee.displayName,
      accountId: issue.fields.assignee.accountId
    } : null,
    reporter: issue.fields?.reporter ? {
      displayName: issue.fields.reporter.displayName,
      accountId: issue.fields.reporter.accountId
    } : null,
    priority: issue.fields?.priority ? {
      name: issue.fields.priority.name,
      id: issue.fields.priority.id
    } : null,
    labels: issue.fields?.labels || [],
    created: issue.fields?.created || null,
    updated: issue.fields?.updated || null,
    issueType: {
      name: issue.fields?.issuetype?.name || 'Unknown',
      id: issue.fields?.issuetype?.id || '0'
    },
    projectKey: issue.fields?.project?.key || '',
    projectName: issue.fields?.project?.name || '',
    url: `${baseUrl}/browse/${issue.key}`
  };
}

/**
 * Format Jira comment data to standardized format
 */
function formatCommentData(comment: any): any {
  return {
    id: comment.id,
    body: extractTextFromADF(comment.body),
    rawBody: comment.body || '',
    author: comment.author ? {
      displayName: comment.author.displayName,
      accountId: comment.author.accountId
    } : null,
    created: comment.created || null,
    updated: comment.updated || null
  };
}

/**
 * Register resources related to Jira issues
 * @param server MCP Server instance
 */
export function registerIssueResources(server: McpServer) {
  logger.info('Registering Jira issue resources...');

  // Resource: Issues list (with pagination and JQL support)
  server.resource(
    'jira-issues-list',
    new ResourceTemplate('jira://issues', {
      list: async (_extra) => {
        return {
          resources: [
            {
              uri: 'jira://issues',
              name: 'Jira Issues',
              description: 'List and search all Jira issues',
              mimeType: 'application/json'
            }
          ]
        };
      }
    }),
    async (uri, params, _extra) => {
      try {
        const config = Config.getAtlassianConfigFromEnv();
        const { limit, offset } = Resources.extractPagingParams(params);
        const jql = params.jql ? Array.isArray(params.jql) ? params.jql[0] : params.jql : '';
        const project = params.project ? Array.isArray(params.project) ? params.project[0] : params.project : '';
        const status = params.status ? Array.isArray(params.status) ? params.status[0] : params.status : '';
        
        // Build JQL query based on parameters
        let jqlQuery = jql;
        if (project && !jqlQuery.includes('project=')) {
          jqlQuery = jqlQuery ? `${jqlQuery} AND project = ${project}` : `project = ${project}`;
        }
        if (status && !jqlQuery.includes('status=')) {
          jqlQuery = jqlQuery ? `${jqlQuery} AND status = "${status}"` : `status = "${status}"`;
        }
        
        logger.info(`Searching Jira issues with JQL: ${jqlQuery || 'All issues'}`);
        const response = await searchIssuesApi(config, jqlQuery, limit);
        
        // Format issues data
        const formattedIssues = response.issues.map((issue: any) => formatIssueData(issue, config.baseUrl));
        
        const uriString = typeof uri === 'string' ? uri : uri.href;
        return Resources.createStandardResource(
          uriString,
          formattedIssues,
          'issues',
          issuesListSchema,
          response.total,
          limit,
          offset,
          `${config.baseUrl}/issues/?jql=${encodeURIComponent(jqlQuery)}`
        );
      } catch (error) {
        logger.error('Error getting Jira issues:', error);
        throw error;
      }
    }
  );

  // Resource: Issue details
  server.resource(
    'jira-issue-details',
    new ResourceTemplate('jira://issues/{issueKey}', {
      list: async (_extra) => {
        return {
          resources: [
            {
              uri: 'jira://issues/{issueKey}',
              name: 'Jira Issue Details',
              description: 'Get details for a specific Jira issue by key. Replace {issueKey} with the issue key.',
              mimeType: 'application/json'
            }
          ]
        };
      }
    }),
    async (uri, params, _extra) => {
      try {
        const config = Config.getAtlassianConfigFromEnv();
        let normalizedIssueKey = Array.isArray(params.issueKey) ? params.issueKey[0] : params.issueKey;
        
        if (!normalizedIssueKey) {
          throw new Error('Missing issueKey in URI');
        }
        
        logger.info(`Getting details for Jira issue: ${normalizedIssueKey}`);
        const issue = await getIssue(config, normalizedIssueKey);
        const formattedIssue = formatIssueData(issue, config.baseUrl);
        
        const uriString = typeof uri === 'string' ? uri : uri.href;
        return Resources.createStandardResource(
          uriString,
          [formattedIssue],
          'issue',
          issueSchema,
          1,
          1,
          0,
          `${config.baseUrl}/browse/${normalizedIssueKey}`
        );
      } catch (error) {
        logger.error(`Error getting Jira issue details:`, error);
        throw error;
      }
    }
  );

  // Resource: Issue transitions (available actions/status changes)
  server.resource(
    'jira-issue-transitions',
    new ResourceTemplate('jira://issues/{issueKey}/transitions', {
      list: async (_extra) => {
        return {
          resources: [
            {
              uri: 'jira://issues/{issueKey}/transitions',
              name: 'Jira Issue Transitions',
              description: 'List available transitions for a Jira issue. Replace {issueKey} with the issue key.',
              mimeType: 'application/json'
            }
          ]
        };
      }
    }),
    async (uri, params, _extra) => {
      try {
        const config = Config.getAtlassianConfigFromEnv();
        let normalizedIssueKey = Array.isArray(params.issueKey) ? params.issueKey[0] : params.issueKey;
        
        if (!normalizedIssueKey) {
          throw new Error('Missing issueKey in URI');
        }
        
        logger.info(`Getting transitions for Jira issue: ${normalizedIssueKey}`);
        const transitions = await getIssueTransitions(config, normalizedIssueKey);
        
        // Format transitions data
        const formattedTransitions = transitions.map((t: any) => ({
          id: t.id,
          name: t.name,
          to: {
            id: t.to.id,
            name: t.to.name
          }
        }));
        
        const uriString = typeof uri === 'string' ? uri : uri.href;
        return Resources.createStandardResource(
          uriString,
          formattedTransitions,
          'transitions',
          transitionsListSchema,
          formattedTransitions.length,
          formattedTransitions.length,
          0,
          `${config.baseUrl}/browse/${normalizedIssueKey}`
        );
      } catch (error) {
        logger.error(`Error getting Jira issue transitions:`, error);
        throw error;
      }
    }
  );

  // Resource: Issue comments
  server.resource(
    'jira-issue-comments',
    new ResourceTemplate('jira://issues/{issueKey}/comments', {
      list: async (_extra) => {
        return {
          resources: [
            {
              uri: 'jira://issues/{issueKey}/comments',
              name: 'Jira Issue Comments',
              description: 'List comments for a Jira issue. Replace {issueKey} with the issue key.',
              mimeType: 'application/json'
            }
          ]
        };
      }
    }),
    async (uri, params, _extra) => {
      try {
        const config = Config.getAtlassianConfigFromEnv();
        let normalizedIssueKey = Array.isArray(params.issueKey) ? params.issueKey[0] : params.issueKey;
        
        if (!normalizedIssueKey) {
          throw new Error('Missing issueKey in URI');
        }
        
        const { limit, offset } = Resources.extractPagingParams(params);
        logger.info(`Getting comments for Jira issue: ${normalizedIssueKey}`);
        const commentData = await getIssueComments(config, normalizedIssueKey, offset, limit);
        
        // Format comments data
        const formattedComments = (commentData.comments || []).map((c: any) => formatCommentData(c));
        
        const uriString = typeof uri === 'string' ? uri : uri.href;
        return Resources.createStandardResource(
          uriString,
          formattedComments,
          'comments',
          commentsListSchema,
          commentData.total || formattedComments.length,
          limit,
          offset,
          `${config.baseUrl}/browse/${normalizedIssueKey}`
        );
      } catch (error) {
        logger.error(`Error getting Jira issue comments:`, error);
        throw error;
      }
    }
  );

  logger.info('Jira issue resources registered successfully');
}

```

--------------------------------------------------------------------------------
/docs/dev-guide/modelcontextprotocol-tools.md:
--------------------------------------------------------------------------------

```markdown
https://modelcontextprotocol.io/docs/concepts/tools

# Tools

> Enable LLMs to perform actions through your server

Tools are a powerful primitive in the Model Context Protocol (MCP) that enable servers to expose executable functionality to clients. Through tools, LLMs can interact with external systems, perform computations, and take actions in the real world.

<Note>
  Tools are designed to be **model-controlled**, meaning that tools are exposed from servers to clients with the intention of the AI model being able to automatically invoke them (with a human in the loop to grant approval).
</Note>

## Overview

Tools in MCP allow servers to expose executable functions that can be invoked by clients and used by LLMs to perform actions. Key aspects of tools include:

* **Discovery**: Clients can list available tools through the `tools/list` endpoint
* **Invocation**: Tools are called using the `tools/call` endpoint, where servers perform the requested operation and return results
* **Flexibility**: Tools can range from simple calculations to complex API interactions

Like [resources](/docs/concepts/resources), tools are identified by unique names and can include descriptions to guide their usage. However, unlike resources, tools represent dynamic operations that can modify state or interact with external systems.

## Tool definition structure

Each tool is defined with the following structure:

```typescript
{
  name: string;          // Unique identifier for the tool
  description?: string;  // Human-readable description
  inputSchema: {         // JSON Schema for the tool's parameters
    type: "object",
    properties: { ... }  // Tool-specific parameters
  },
  annotations?: {        // Optional hints about tool behavior
    title?: string;      // Human-readable title for the tool
    readOnlyHint?: boolean;    // If true, the tool does not modify its environment
    destructiveHint?: boolean; // If true, the tool may perform destructive updates
    idempotentHint?: boolean;  // If true, repeated calls with same args have no additional effect
    openWorldHint?: boolean;   // If true, tool interacts with external entities
  }
}
```

## Implementing tools

Here's an example of implementing a basic tool in an MCP server:

<Tabs>
  <Tab title="TypeScript">
    ```typescript
    const server = new Server({
      name: "example-server",
      version: "1.0.0"
    }, {
      capabilities: {
        tools: {}
      }
    });

    // Define available tools
    server.setRequestHandler(ListToolsRequestSchema, async () => {
      return {
        tools: [{
          name: "calculate_sum",
          description: "Add two numbers together",
          inputSchema: {
            type: "object",
            properties: {
              a: { type: "number" },
              b: { type: "number" }
            },
            required: ["a", "b"]
          }
        }]
      };
    });

    // Handle tool execution
    server.setRequestHandler(CallToolRequestSchema, async (request) => {
      if (request.params.name === "calculate_sum") {
        const { a, b } = request.params.arguments;
        return {
          content: [
            {
              type: "text",
              text: String(a + b)
            }
          ]
        };
      }
      throw new Error("Tool not found");
    });
    ```
  </Tab>

  <Tab title="Python">
    ```python
    app = Server("example-server")

    @app.list_tools()
    async def list_tools() -> list[types.Tool]:
        return [
            types.Tool(
                name="calculate_sum",
                description="Add two numbers together",
                inputSchema={
                    "type": "object",
                    "properties": {
                        "a": {"type": "number"},
                        "b": {"type": "number"}
                    },
                    "required": ["a", "b"]
                }
            )
        ]

    @app.call_tool()
    async def call_tool(
        name: str,
        arguments: dict
    ) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
        if name == "calculate_sum":
            a = arguments["a"]
            b = arguments["b"]
            result = a + b
            return [types.TextContent(type="text", text=str(result))]
        raise ValueError(f"Tool not found: {name}")
    ```
  </Tab>
</Tabs>

## Example tool patterns

Here are some examples of types of tools that a server could provide:

### System operations

Tools that interact with the local system:

```typescript
{
  name: "execute_command",
  description: "Run a shell command",
  inputSchema: {
    type: "object",
    properties: {
      command: { type: "string" },
      args: { type: "array", items: { type: "string" } }
    }
  }
}
```

### API integrations

Tools that wrap external APIs:

```typescript
{
  name: "github_create_issue",
  description: "Create a GitHub issue",
  inputSchema: {
    type: "object",
    properties: {
      title: { type: "string" },
      body: { type: "string" },
      labels: { type: "array", items: { type: "string" } }
    }
  }
}
```

### Data processing

Tools that transform or analyze data:

```typescript
{
  name: "analyze_csv",
  description: "Analyze a CSV file",
  inputSchema: {
    type: "object",
    properties: {
      filepath: { type: "string" },
      operations: {
        type: "array",
        items: {
          enum: ["sum", "average", "count"]
        }
      }
    }
  }
}
```

## Best practices

When implementing tools:

1. Provide clear, descriptive names and descriptions
2. Use detailed JSON Schema definitions for parameters
3. Include examples in tool descriptions to demonstrate how the model should use them
4. Implement proper error handling and validation
5. Use progress reporting for long operations
6. Keep tool operations focused and atomic
7. Document expected return value structures
8. Implement proper timeouts
9. Consider rate limiting for resource-intensive operations
10. Log tool usage for debugging and monitoring

## Security considerations

When exposing tools:

### Input validation

* Validate all parameters against the schema
* Sanitize file paths and system commands
* Validate URLs and external identifiers
* Check parameter sizes and ranges
* Prevent command injection

### Access control

* Implement authentication where needed
* Use appropriate authorization checks
* Audit tool usage
* Rate limit requests
* Monitor for abuse

### Error handling

* Don't expose internal errors to clients
* Log security-relevant errors
* Handle timeouts appropriately
* Clean up resources after errors
* Validate return values

## Tool discovery and updates

MCP supports dynamic tool discovery:

1. Clients can list available tools at any time
2. Servers can notify clients when tools change using `notifications/tools/list_changed`
3. Tools can be added or removed during runtime
4. Tool definitions can be updated (though this should be done carefully)

## Error handling

Tool errors should be reported within the result object, not as MCP protocol-level errors. This allows the LLM to see and potentially handle the error. When a tool encounters an error:

1. Set `isError` to `true` in the result
2. Include error details in the `content` array

Here's an example of proper error handling for tools:

<Tabs>
  <Tab title="TypeScript">
    ```typescript
    try {
      // Tool operation
      const result = performOperation();
      return {
        content: [
          {
            type: "text",
            text: `Operation successful: ${result}`
          }
        ]
      };
    } catch (error) {
      return {
        isError: true,
        content: [
          {
            type: "text",
            text: `Error: ${error.message}`
          }
        ]
      };
    }
    ```
  </Tab>

  <Tab title="Python">
    ```python
    try:
        # Tool operation
        result = perform_operation()
        return types.CallToolResult(
            content=[
                types.TextContent(
                    type="text",
                    text=f"Operation successful: {result}"
                )
            ]
        )
    except Exception as error:
        return types.CallToolResult(
            isError=True,
            content=[
                types.TextContent(
                    type="text",
                    text=f"Error: {str(error)}"
                )
            ]
        )
    ```
  </Tab>
</Tabs>

This approach allows the LLM to see that an error occurred and potentially take corrective action or request human intervention.

## Tool annotations

Tool annotations provide additional metadata about a tool's behavior, helping clients understand how to present and manage tools. These annotations are hints that describe the nature and impact of a tool, but should not be relied upon for security decisions.

### Purpose of tool annotations

Tool annotations serve several key purposes:

1. Provide UX-specific information without affecting model context
2. Help clients categorize and present tools appropriately
3. Convey information about a tool's potential side effects
4. Assist in developing intuitive interfaces for tool approval

### Available tool annotations

The MCP specification defines the following annotations for tools:

| Annotation        | Type    | Default | Description                                                                                                                          |
| ----------------- | ------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `title`           | string  | -       | A human-readable title for the tool, useful for UI display                                                                           |
| `readOnlyHint`    | boolean | false   | If true, indicates the tool does not modify its environment                                                                          |
| `destructiveHint` | boolean | true    | If true, the tool may perform destructive updates (only meaningful when `readOnlyHint` is false)                                     |
| `idempotentHint`  | boolean | false   | If true, calling the tool repeatedly with the same arguments has no additional effect (only meaningful when `readOnlyHint` is false) |
| `openWorldHint`   | boolean | true    | If true, the tool may interact with an "open world" of external entities                                                             |

### Example usage

Here's how to define tools with annotations for different scenarios:

```typescript
// A read-only search tool
{
  name: "web_search",
  description: "Search the web for information",
  inputSchema: {
    type: "object",
    properties: {
      query: { type: "string" }
    },
    required: ["query"]
  },
  annotations: {
    title: "Web Search",
    readOnlyHint: true,
    openWorldHint: true
  }
}

// A destructive file deletion tool
{
  name: "delete_file",
  description: "Delete a file from the filesystem",
  inputSchema: {
    type: "object",
    properties: {
      path: { type: "string" }
    },
    required: ["path"]
  },
  annotations: {
    title: "Delete File",
    readOnlyHint: false,
    destructiveHint: true,
    idempotentHint: true,
    openWorldHint: false
  }
}

// A non-destructive database record creation tool
{
  name: "create_record",
  description: "Create a new record in the database",
  inputSchema: {
    type: "object",
    properties: {
      table: { type: "string" },
      data: { type: "object" }
    },
    required: ["table", "data"]
  },
  annotations: {
    title: "Create Database Record",
    readOnlyHint: false,
    destructiveHint: false,
    idempotentHint: false,
    openWorldHint: false
  }
}
```

### Integrating annotations in server implementation

<Tabs>
  <Tab title="TypeScript">
    ```typescript
    server.setRequestHandler(ListToolsRequestSchema, async () => {
      return {
        tools: [{
          name: "calculate_sum",
          description: "Add two numbers together",
          inputSchema: {
            type: "object",
            properties: {
              a: { type: "number" },
              b: { type: "number" }
            },
            required: ["a", "b"]
          },
          annotations: {
            title: "Calculate Sum",
            readOnlyHint: true,
            openWorldHint: false
          }
        }]
      };
    });
    ```
  </Tab>

  <Tab title="Python">
    ```python
    from mcp.server.fastmcp import FastMCP

    mcp = FastMCP("example-server")

    @mcp.tool(
        annotations={
            "title": "Calculate Sum",
            "readOnlyHint": True,
            "openWorldHint": False
        }
    )
    async def calculate_sum(a: float, b: float) -> str:
        """Add two numbers together.
        
        Args:
            a: First number to add
            b: Second number to add
        """
        result = a + b
        return str(result)
    ```
  </Tab>
</Tabs>

### Best practices for tool annotations

1. **Be accurate about side effects**: Clearly indicate whether a tool modifies its environment and whether those modifications are destructive.

2. **Use descriptive titles**: Provide human-friendly titles that clearly describe the tool's purpose.

3. **Indicate idempotency properly**: Mark tools as idempotent only if repeated calls with the same arguments truly have no additional effect.

4. **Set appropriate open/closed world hints**: Indicate whether a tool interacts with a closed system (like a database) or an open system (like the web).

5. **Remember annotations are hints**: All properties in ToolAnnotations are hints and not guaranteed to provide a faithful description of tool behavior. Clients should never make security-critical decisions based solely on annotations.

## Testing tools

A comprehensive testing strategy for MCP tools should cover:

* **Functional testing**: Verify tools execute correctly with valid inputs and handle invalid inputs appropriately
* **Integration testing**: Test tool interaction with external systems using both real and mocked dependencies
* **Security testing**: Validate authentication, authorization, input sanitization, and rate limiting
* **Performance testing**: Check behavior under load, timeout handling, and resource cleanup
* **Error handling**: Ensure tools properly report errors through the MCP protocol and clean up resources

```

--------------------------------------------------------------------------------
/src/resources/confluence/pages.ts:
--------------------------------------------------------------------------------

```typescript
import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
import { Logger } from '../../utils/logger.js';
import { pagesListSchema, pageSchema, commentsListSchema, attachmentListSchema, versionListSchema, labelListSchema } from '../../schemas/confluence.js';
import { getConfluencePagesV2, getConfluencePageV2, getConfluencePageBodyV2, getConfluencePageAncestorsV2, getConfluencePageChildrenV2, getConfluencePageLabelsV2, getConfluencePageAttachmentsV2, getConfluencePageVersionsV2, getConfluencePagesWithFilters } from '../../utils/confluence-resource-api.js';
import { getConfluencePageFooterCommentsV2, getConfluencePageInlineCommentsV2 } from '../../utils/confluence-resource-api.js';
import { Config, Resources } from '../../utils/mcp-helpers.js';

const logger = Logger.getLogger('ConfluenceResource:Pages');

export function registerPageResources(server: McpServer) {
  logger.info('Registering Confluence page resources...');

  // Resource: Page details (API v2, tách call metadata và body)
  server.resource(
    'confluence-page-details-v2',
    new ResourceTemplate('confluence://pages/{pageId}', {
      list: async (_extra) => ({
        resources: [
          {
            uri: 'confluence://pages/{pageId}',
            name: 'Confluence Page Details',
            description: 'Get details for a specific Confluence page by ID. Replace {pageId} with the page ID.',
            mimeType: 'application/json'
          }
        ]
      })
    }),
    async (uri, { pageId }, extra) => {
      let normalizedPageId = Array.isArray(pageId) ? pageId[0] : pageId;
      try {
        let config = (extra && typeof extra === 'object' && 'context' in extra && extra.context && (extra.context as any).atlassianConfig)
          ? (extra.context as any).atlassianConfig
          : Config.getAtlassianConfigFromEnv();
        if (!normalizedPageId) {
          throw new Error('Missing pageId in URI');
        }
        logger.info(`Getting details for Confluence page (v2): ${normalizedPageId}`);
        const page = await getConfluencePageV2(config, normalizedPageId);
        let body = {};
        try {
          body = await getConfluencePageBodyV2(config, normalizedPageId);
        } catch (e) {
          body = {};
        }
        const formattedPage = {
          ...page,
          body: (body && typeof body === 'object' && 'value' in body) ? body.value : '',
          bodyType: (body && typeof body === 'object' && 'representation' in body) ? body.representation : 'storage',
        };
        const uriString = typeof uri === 'string' ? uri : uri.href;
        return Resources.createStandardResource(
          uriString,
          [formattedPage],
          'page',
          pageSchema,
          1,
          1,
          0,
          `${config.baseUrl}/wiki/pages/${normalizedPageId}`
        );
      } catch (error) {
        logger.error(`Error getting Confluence page details (v2) for ${normalizedPageId}:`, error);
        throw error;
      }
    }
  );

  // Resource: List of children pages
  server.resource(
    'confluence-page-children',
    new ResourceTemplate('confluence://pages/{pageId}/children', {
      list: async (_extra) => ({
        resources: [
          {
            uri: 'confluence://pages/{pageId}/children',
            name: 'Confluence Page Children',
            description: 'List all children for a Confluence page. Replace {pageId} với ID trang.',
            mimeType: 'application/json'
          }
        ]
      })
    }),
    async (uri, { pageId }, extra) => {
      let normalizedPageId = Array.isArray(pageId) ? pageId[0] : pageId;
      try {
        let config = (extra && typeof extra === 'object' && 'context' in extra && extra.context && (extra.context as any).atlassianConfig)
          ? (extra.context as any).atlassianConfig
          : Config.getAtlassianConfigFromEnv();
        if (!normalizedPageId) {
          throw new Error('Missing pageId in URI');
        }
        logger.info(`Getting children for Confluence page (v2): ${normalizedPageId}`);
        const data = await getConfluencePageChildrenV2(config, normalizedPageId);
        const formattedChildren = (data.results || []).map((child: any) => ({
          id: child.id,
          title: child.title,
          status: child.status,
          url: `${config.baseUrl}/wiki/pages/${child.id}`
        }));
        const childrenSchema = { type: 'array', items: pageSchema };
        return Resources.createStandardResource(
          typeof uri === 'string' ? uri : uri.href,
          formattedChildren,
          'children',
          childrenSchema,
          formattedChildren.length,
          formattedChildren.length,
          0,
          `${config.baseUrl}/wiki/pages/${normalizedPageId}`
        );
      } catch (error) {
        logger.error(`Error getting Confluence page children for ${normalizedPageId}:`, error);
        throw error;
      }
    }
  );

  // Resource: List of comments for a page (API v2, gộp cả footer và inline)
  server.resource(
    'confluence-page-comments',
    new ResourceTemplate('confluence://pages/{pageId}/comments', {
      list: async (_extra) => ({
        resources: [
          {
            uri: 'confluence://pages/{pageId}/comments',
            name: 'Confluence Page Comments',
            description: 'List comments for a Confluence page. Replace {pageId} with the page ID.',
            mimeType: 'application/json'
          }
        ]
      })
    }),
    async (uri, { pageId }, extra) => {
      let normalizedPageId = Array.isArray(pageId) ? pageId[0] : pageId;
      try {
        let config = (extra && typeof extra === 'object' && 'context' in extra && extra.context && (extra.context as any).atlassianConfig)
          ? (extra.context as any).atlassianConfig
          : Config.getAtlassianConfigFromEnv();
        if (!normalizedPageId) {
          throw new Error('Missing pageId in URI');
        }
        logger.info(`Getting comments for Confluence page (v2): ${normalizedPageId}`);
        const footerComments = await getConfluencePageFooterCommentsV2(config, normalizedPageId);
        const inlineComments = await getConfluencePageInlineCommentsV2(config, normalizedPageId);
        const allComments = [...(footerComments.results || []), ...(inlineComments.results || [])];
        return Resources.createStandardResource(
          typeof uri === 'string' ? uri : uri.href,
          allComments,
          'comments',
          commentsListSchema,
          allComments.length,
          allComments.length,
          0,
          `${config.baseUrl}/wiki/pages/${normalizedPageId}`
        );
      } catch (error) {
        logger.error(`Error getting Confluence page comments for ${normalizedPageId}:`, error);
        throw error;
      }
    }
  );

  // Resource: List of ancestors for a page
  server.resource(
    'confluence-page-ancestors',
    new ResourceTemplate('confluence://pages/{pageId}/ancestors', {
      list: async (_extra) => ({
        resources: [
          {
            uri: 'confluence://pages/{pageId}/ancestors',
            name: 'Confluence Page Ancestors',
            description: 'List all ancestors for a Confluence page. Replace {pageId} with the page ID.',
            mimeType: 'application/json'
          }
        ]
      })
    }),
    async (uri, { pageId }, extra) => {
      let normalizedPageId = Array.isArray(pageId) ? pageId[0] : pageId;
      try {
        let config = (extra && typeof extra === 'object' && 'context' in extra && extra.context && (extra.context as any).atlassianConfig)
          ? (extra.context as any).atlassianConfig
          : Config.getAtlassianConfigFromEnv();
        if (!normalizedPageId) {
          throw new Error('Missing pageId in URI');
        }
        logger.info(`Getting ancestors for Confluence page (v2): ${normalizedPageId}`);
        const data = await getConfluencePageAncestorsV2(config, normalizedPageId);
        const ancestors = Array.isArray(data?.results) ? data.results : [];
        return Resources.createStandardResource(
          typeof uri === 'string' ? uri : uri.href,
          ancestors,
          'ancestors',
          { type: 'array', items: pageSchema },
          ancestors.length,
          ancestors.length,
          0,
          `${config.baseUrl}/wiki/pages/${normalizedPageId}`
        );
      } catch (error) {
        logger.error(`Error getting Confluence page ancestors for ${normalizedPageId}:`, error);
        throw error;
      }
    }
  );

  // Resource: List of attachments for a page
  server.resource(
    'confluence-page-attachments',
    new ResourceTemplate('confluence://pages/{pageId}/attachments', {
      list: async (_extra) => ({
        resources: [
          {
            uri: 'confluence://pages/{pageId}/attachments',
            name: 'Confluence Page Attachments',
            description: 'List all attachments for a Confluence page. Replace {pageId} with the page ID.',
            mimeType: 'application/json'
          }
        ]
      })
    }),
    async (uri, params, extra) => {
      let normalizedPageId = Array.isArray(params.pageId) ? params.pageId[0] : params.pageId;
      try {
        let config = (extra && typeof extra === 'object' && 'context' in extra && extra.context && (extra.context as any).atlassianConfig)
          ? (extra.context as any).atlassianConfig
          : Config.getAtlassianConfigFromEnv();
        if (!normalizedPageId) {
          throw new Error('Missing pageId in URI');
        }
        logger.info(`Getting attachments for Confluence page (v2): ${normalizedPageId}`);
        const data = await getConfluencePageAttachmentsV2(config, normalizedPageId);
        return Resources.createStandardResource(
          typeof uri === 'string' ? uri : uri.href,
          data.results || [],
          'attachments',
          attachmentListSchema,
          data.size || (data.results || []).length,
          data.limit || (data.results || []).length,
          0,
          undefined
        );
      } catch (error) {
        logger.error(`Error getting Confluence page attachments for ${normalizedPageId}:`, error);
        throw error;
      }
    }
  );

  // Resource: List of versions for a page
  server.resource(
    'confluence-page-versions',
    new ResourceTemplate('confluence://pages/{pageId}/versions', {
      list: async (_extra) => ({
        resources: [
          {
            uri: 'confluence://pages/{pageId}/versions',
            name: 'Confluence Page Versions',
            description: 'List all versions for a Confluence page. Replace {pageId} with the page ID.',
            mimeType: 'application/json'
          }
        ]
      })
    }),
    async (uri, params, extra) => {
      let normalizedPageId = Array.isArray(params.pageId) ? params.pageId[0] : params.pageId;
      try {
        let config = (extra && typeof extra === 'object' && 'context' in extra && extra.context && (extra.context as any).atlassianConfig)
          ? (extra.context as any).atlassianConfig
          : Config.getAtlassianConfigFromEnv();
        if (!normalizedPageId) {
          throw new Error('Missing pageId in URI');
        }
        logger.info(`Getting versions for Confluence page (v2): ${normalizedPageId}`);
        const data = await getConfluencePageVersionsV2(config, normalizedPageId);
        return Resources.createStandardResource(
          typeof uri === 'string' ? uri : uri.href,
          data.results || [],
          'versions',
          versionListSchema,
          data.size || (data.results || []).length,
          data.limit || (data.results || []).length,
          0,
          undefined
        );
      } catch (error) {
        logger.error(`Error getting Confluence page versions for ${normalizedPageId}:`, error);
        throw error;
      }
    }
  );

  // Resource: List of pages (search/filter)
  server.resource(
    'confluence-pages-list',
    new ResourceTemplate('confluence://pages', {
      list: async (_extra) => ({
        resources: [
          {
            uri: 'confluence://pages',
            name: 'Confluence Pages',
            description: 'List and search all Confluence pages',
            mimeType: 'application/json'
          }
        ]
      })
    }),
    async (uri, params, extra) => {
      let config = (extra && typeof extra === 'object' && 'context' in extra && extra.context && (extra.context as any).atlassianConfig)
        ? (extra.context as any).atlassianConfig
        : Config.getAtlassianConfigFromEnv();
      const filterParams = { ...params };
      const data = await getConfluencePagesWithFilters(config, filterParams);
      const formattedPages = (data.results || []).map((page: any) => ({
        id: page.id,
        title: page.title,
        status: page.status,
        url: `${config.baseUrl}/wiki/pages/${page.id}`
      }));
      const uriString = typeof uri === 'string' ? uri : uri.href;
      return Resources.createStandardResource(
        uriString,
        formattedPages,
        'pages',
        pagesListSchema,
        data.size || formattedPages.length,
        filterParams.limit || formattedPages.length,
        0,
        undefined
      );
    }
  );

  // Resource: List of labels for a page
  server.resource(
    'confluence-page-labels',
    new ResourceTemplate('confluence://pages/{pageId}/labels', {
      list: async (_extra) => ({
        resources: [
          {
            uri: 'confluence://pages/{pageId}/labels',
            name: 'Confluence Page Labels',
            description: 'List all labels for a Confluence page. Replace {pageId} with the page ID.',
            mimeType: 'application/json'
          }
        ]
      })
    }),
    async (uri, { pageId }, extra) => {
      let normalizedPageId = Array.isArray(pageId) ? pageId[0] : pageId;
      try {
        let config = (extra && typeof extra === 'object' && 'context' in extra && extra.context && (extra.context as any).atlassianConfig)
          ? (extra.context as any).atlassianConfig
          : Config.getAtlassianConfigFromEnv();
        if (!normalizedPageId) {
          throw new Error('Missing pageId in URI');
        }
        logger.info(`Getting labels for Confluence page (v2): ${normalizedPageId}`);
        const data = await getConfluencePageLabelsV2(config, normalizedPageId);
        const formattedLabels = (data.results || []).map((label: any) => ({
          id: label.id,
          name: label.name,
          prefix: label.prefix
        }));
        return Resources.createStandardResource(
          typeof uri === 'string' ? uri : uri.href,
          formattedLabels,
          'labels',
          labelListSchema,
          data.size || formattedLabels.length,
          data.limit || formattedLabels.length,
          0,
          undefined
        );
      } catch (error) {
        logger.error(`Error getting Confluence page labels for ${normalizedPageId}:`, error);
        throw error;
      }
    }
  );
}

```
Page 2/3FirstPrevNextLast