#
tokens: 47559/50000 23/95 files (page 2/4)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 2 of 4. Use http://codebase.md/phuc-nt/mcp-atlassian-server?lines=true&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/jira/filters.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Jira Filter Resources
  3 |  * 
  4 |  * These resources provide access to Jira filters through MCP.
  5 |  */
  6 | 
  7 | import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
  8 | import { filterListSchema, filterSchema } from '../../schemas/jira.js';
  9 | import { createStandardMetadata } from '../../schemas/common.js';
 10 | import { getFilters, getFilterById, getMyFilters } from '../../utils/jira-resource-api.js';
 11 | import { Logger } from '../../utils/logger.js';
 12 | import { Config, Resources } from '../../utils/mcp-helpers.js';
 13 | 
 14 | const logger = Logger.getLogger('JiraFilterResources');
 15 | 
 16 | /**
 17 |  * Register all Jira filter resources with MCP Server
 18 |  * @param server MCP Server instance
 19 |  */
 20 | export function registerFilterResources(server: McpServer) {
 21 |   logger.info('Registering Jira filter resources...');
 22 |   
 23 |   // Chỉ đăng ký mỗi template một lần kèm handler
 24 |   
 25 |   // Resource: Filter list
 26 |   const filtersTemplate = new ResourceTemplate('jira://filters', {
 27 |     list: async (_extra) => ({
 28 |       resources: [
 29 |         {
 30 |           uri: 'jira://filters',
 31 |           name: 'Jira Filters',
 32 |           description: 'List and search all Jira filters',
 33 |           mimeType: 'application/json'
 34 |         }
 35 |       ]
 36 |     })
 37 |   });
 38 |   
 39 |   // Resource: Filter details
 40 |   const filterDetailsTemplate = new ResourceTemplate('jira://filters/{filterId}', {
 41 |     list: async (_extra) => ({
 42 |       resources: [
 43 |         {
 44 |           uri: 'jira://filters/{filterId}',
 45 |           name: 'Jira Filter Details',
 46 |           description: 'Get details for a specific Jira filter by ID. Replace {filterId} with the filter ID.',
 47 |           mimeType: 'application/json'
 48 |         }
 49 |       ]
 50 |     })
 51 |   });
 52 |   
 53 |   // Resource: My filters
 54 |   const myFiltersTemplate = new ResourceTemplate('jira://filters/my', {
 55 |     list: async (_extra) => ({
 56 |       resources: [
 57 |         {
 58 |           uri: 'jira://filters/my',
 59 |           name: 'Jira My Filters',
 60 |           description: 'List filters owned by or shared with the current user.',
 61 |           mimeType: 'application/json'
 62 |         }
 63 |       ]
 64 |     })
 65 |   });
 66 |   
 67 |   // Đăng ký template kèm handler thực thi - chỉ đăng ký một lần mỗi URI
 68 |   server.resource('jira-filters-list', filtersTemplate, 
 69 |     async (uri: string | URL, params: Record<string, any>, _extra: any) => {
 70 |       try {
 71 |         // Get config from environment
 72 |         const config = Config.getAtlassianConfigFromEnv();
 73 |         
 74 |         const { limit, offset } = Resources.extractPagingParams(params);
 75 |         const response = await getFilters(config, offset, limit);
 76 |         return Resources.createStandardResource(
 77 |           typeof uri === 'string' ? uri : uri.href,
 78 |           response.values,
 79 |           'filters',
 80 |           filterListSchema,
 81 |           response.total || response.values.length,
 82 |           limit,
 83 |           offset,
 84 |           `${config.baseUrl}/secure/ManageFilters.jspa`
 85 |         );
 86 |       } catch (error) {
 87 |         logger.error('Error getting filter list:', error);
 88 |         throw error;
 89 |       }
 90 |     }
 91 |   );
 92 | 
 93 |   server.resource('jira-filter-details', filterDetailsTemplate, 
 94 |     async (uri: string | URL, params: Record<string, any>, _extra: any) => {
 95 |       try {
 96 |         // Get config from environment
 97 |         const config = Config.getAtlassianConfigFromEnv();
 98 |         
 99 |         const filterId = Array.isArray(params.filterId) ? params.filterId[0] : params.filterId;
100 |         const filter = await getFilterById(config, filterId);
101 |         return Resources.createStandardResource(
102 |           typeof uri === 'string' ? uri : uri.href,
103 |           [filter],
104 |           'filter',
105 |           filterSchema,
106 |           1,
107 |           1,
108 |           0,
109 |           `${config.baseUrl}/secure/ManageFilters.jspa?filterId=${filterId}`
110 |         );
111 |       } catch (error) {
112 |         logger.error(`Error getting filter details for filter ${params.filterId}:`, error);
113 |         throw error;
114 |       }
115 |     }
116 |   );
117 | 
118 |   server.resource('jira-my-filters', myFiltersTemplate, 
119 |     async (uri: string | URL, _params: Record<string, any>, _extra: any) => {
120 |       try {
121 |         // Get config from environment
122 |         const config = Config.getAtlassianConfigFromEnv();
123 |         
124 |         const filters = await getMyFilters(config);
125 |         return Resources.createStandardResource(
126 |           typeof uri === 'string' ? uri : uri.href,
127 |           filters,
128 |           'filters',
129 |           filterListSchema,
130 |           filters.length,
131 |           filters.length,
132 |           0,
133 |           `${config.baseUrl}/secure/ManageFilters.jspa?filterView=my`
134 |         );
135 |       } catch (error) {
136 |         logger.error('Error getting my filters:', error);
137 |         throw error;
138 |       }
139 |     }
140 |   );
141 |   
142 |   logger.info('Jira filter resources registered successfully');
143 | }
```

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

```typescript
  1 | import { z } from 'zod';
  2 | import { callConfluenceApi } from '../../utils/atlassian-api-base.js';
  3 | import { AtlassianConfig } from '../../utils/atlassian-api-base.js';
  4 | import { ApiError, ApiErrorType } from '../../utils/error-handler.js';
  5 | import { Logger } from '../../utils/logger.js';
  6 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
  7 | import { McpResponse, createSuccessResponse, createErrorResponse } from '../../utils/mcp-core.js';
  8 | import { createConfluencePageV2 } from '../../utils/confluence-tool-api.js';
  9 | import { Config } from '../../utils/mcp-helpers.js';
 10 | 
 11 | // Initialize logger
 12 | const logger = Logger.getLogger('ConfluenceTools:createPage');
 13 | 
 14 | // Input parameter schema
 15 | export const createPageSchema = z.object({
 16 |   spaceId: z.string().describe('Space ID (required, must be the numeric ID from API v2, NOT the key like TX, DEV, ...)'),
 17 |   title: z.string().describe('Title of the page (required)'),
 18 |   content: z.string().describe(`Content of the page (required, must be in Confluence storage format - XML-like HTML).
 19 | 
 20 | - Plain text or markdown is NOT supported (will throw error).
 21 | - 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.
 22 | - Content MUST strictly follow Confluence storage format.
 23 | 
 24 | Valid examples:
 25 | - <p>This is a paragraph</p>
 26 | - <ac:structured-macro ac:name="info"><ac:rich-text-body>Information</ac:rich-text-body></ac:structured-macro>
 27 | `),
 28 |   parentId: z.string().describe('Parent page ID (required, must specify the parent page to create a child page)')
 29 | });
 30 | 
 31 | type CreatePageParams = z.infer<typeof createPageSchema>;
 32 | 
 33 | interface CreatePageResult {
 34 |   id: string;
 35 |   key: string;
 36 |   title: string;
 37 |   self: string;
 38 |   webui: string;
 39 |   success: boolean;
 40 |   spaceId?: string;
 41 | }
 42 | 
 43 | // Main handler to create a new page (API v2)
 44 | export async function createPageHandler(
 45 |   params: CreatePageParams,
 46 |   config: AtlassianConfig
 47 | ): Promise<CreatePageResult> {
 48 |   try {
 49 |     logger.info(`Creating new page (v2) "${params.title}" in spaceId ${params.spaceId}`);
 50 |     const data = await createConfluencePageV2(config, {
 51 |       spaceId: params.spaceId,
 52 |       title: params.title,
 53 |       content: params.content,
 54 |       parentId: params.parentId
 55 |     });
 56 |     return {
 57 |       id: data.id,
 58 |       key: data.key || '',
 59 |       title: data.title,
 60 |       self: data._links.self,
 61 |       webui: data._links.webui,
 62 |       success: true,
 63 |       spaceId: params.spaceId
 64 |     };
 65 |   } catch (error) {
 66 |     if (error instanceof ApiError) {
 67 |       throw error;
 68 |     }
 69 |     logger.error(`Error creating page (v2) in spaceId ${params.spaceId}:`, error);
 70 |     let message = `Failed to create page: ${error instanceof Error ? error.message : String(error)}`;
 71 |     throw new ApiError(
 72 |       ApiErrorType.SERVER_ERROR,
 73 |       message,
 74 |       500
 75 |     );
 76 |   }
 77 | }
 78 | 
 79 | // Register the tool with MCP Server
 80 | export const registerCreatePageTool = (server: McpServer) => {
 81 |   server.tool(
 82 |     'createPage',
 83 |     'Create a new page in Confluence (API v2, chỉ hỗ trợ spaceId)',
 84 |     createPageSchema.shape,
 85 |     async (params: CreatePageParams, context: Record<string, any>) => {
 86 |       try {
 87 |         const config = context?.atlassianConfig ?? Config.getAtlassianConfigFromEnv();
 88 |         if (!config) {
 89 |           return {
 90 |             content: [
 91 |               { type: 'text', text: 'Invalid or missing Atlassian configuration' }
 92 |             ],
 93 |             isError: true
 94 |           };
 95 |         }
 96 |         const result = await createPageHandler(params, config);
 97 |         return {
 98 |           content: [
 99 |             {
100 |               type: 'text',
101 |               text: JSON.stringify({
102 |                 success: true,
103 |                 message: `Page created successfully!`,
104 |                 id: result.id,
105 |                 title: result.title,
106 |                 spaceId: result.spaceId
107 |               })
108 |             }
109 |           ]
110 |         };
111 |       } catch (error) {
112 |         if (error instanceof ApiError) {
113 |           return {
114 |             content: [
115 |               {
116 |                 type: 'text',
117 |                 text: JSON.stringify({
118 |                   success: false,
119 |                   message: error.message,
120 |                   code: error.code,
121 |                   statusCode: error.statusCode,
122 |                   type: error.type
123 |                 })
124 |               }
125 |             ],
126 |             isError: true
127 |           };
128 |         }
129 |         return {
130 |           content: [
131 |             {
132 |               type: 'text',
133 |               text: JSON.stringify({
134 |                 success: false,
135 |                 message: `Error while creating page: ${error instanceof Error ? error.message : String(error)}`
136 |               })
137 |             }
138 |           ],
139 |           isError: true
140 |         };
141 |       }
142 |     }
143 |   );
144 | }; 
```

--------------------------------------------------------------------------------
/dev_mcp-atlassian-test-client/src/list-mcp-inventory.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { Client } from "@modelcontextprotocol/sdk/client/index.js";
  2 | import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
  3 | import path from "path";
  4 | import { fileURLToPath } from "url";
  5 | 
  6 | // Get current file path
  7 | const __filename = fileURLToPath(import.meta.url);
  8 | const __dirname = path.dirname(__filename);
  9 | 
 10 | async function main() {
 11 |   const client = new Client({ name: "mcp-atlassian-inventory-list", version: "1.0.0" });
 12 |   const serverPath = "/Users/phucnt/Workspace/mcp-atlassian-server/dist/index.js";
 13 |   const transport = new StdioClientTransport({
 14 |     command: "node",
 15 |     args: [serverPath],
 16 |     env: process.env as Record<string, string>
 17 |   });
 18 | 
 19 |   console.log("Connecting to MCP server...");
 20 |   await client.connect(transport);
 21 | 
 22 |   // List available tools with details
 23 |   console.log("\n=== Available Tools ===");
 24 |   const toolsResult = await client.listTools();
 25 |   console.log(`Total tools: ${toolsResult.tools.length}`);
 26 |   toolsResult.tools.forEach((tool, index) => {
 27 |     console.log(`${index + 1}. ${tool.name}: ${tool.description || 'No description'}`);
 28 |   });
 29 | 
 30 |   // List available resources
 31 |   console.log("\n=== Available Resources ===");
 32 |   let resourcesResult: any = { resources: [] };
 33 |   try {
 34 |     resourcesResult = await client.listResources();
 35 |     console.log(`Total resources: ${resourcesResult.resources.length}`);
 36 |     resourcesResult.resources.forEach((resource: any, index: number) => {
 37 |       console.log(`${index + 1}. ${resource.uriPattern || resource.uri}: ${resource.description || 'No description'}`);
 38 |     });
 39 |     if (resourcesResult.resources.length === 0) {
 40 |       console.warn("WARNING: No resources returned by listResources. This may indicate missing list callbacks in the MCP server resource registration.");
 41 |       console.warn("Try these common resource URIs manually:");
 42 |       [
 43 |         'jira://issues',
 44 |         'jira://projects',
 45 |         'jira://boards',
 46 |         'confluence://pages',
 47 |         'confluence://spaces'
 48 |       ].forEach((uri, idx) => {
 49 |         console.log(`  ${idx + 1}. ${uri}`);
 50 |       });
 51 |     }
 52 |   } catch (error) {
 53 |     console.log("Error listing resources:", error instanceof Error ? error.message : String(error));
 54 |   }
 55 | 
 56 |   // Group tools by category
 57 |   console.log("\n=== Tools by Category ===");
 58 |   const toolsByCategory: Record<string, any[]> = {};
 59 |   toolsResult.tools.forEach(tool => {
 60 |     let category = "Other";
 61 |     if (tool.name.startsWith("create") || tool.name.startsWith("update") || 
 62 |         tool.name.startsWith("delete") || tool.name.startsWith("get")) {
 63 |       if (tool.name.toLowerCase().includes("issue") || tool.name.toLowerCase().includes("sprint") || 
 64 |           tool.name.toLowerCase().includes("board") || tool.name.toLowerCase().includes("filter")) {
 65 |         category = "Jira";
 66 |       } else if (tool.name.toLowerCase().includes("page") || tool.name.toLowerCase().includes("comment") || 
 67 |                 tool.name.toLowerCase().includes("space")) {
 68 |         category = "Confluence";
 69 |       }
 70 |     }
 71 |     if (!toolsByCategory[category]) toolsByCategory[category] = [];
 72 |     toolsByCategory[category].push(tool);
 73 |   });
 74 |   Object.entries(toolsByCategory).forEach(([category, tools]) => {
 75 |     console.log(`\n${category} Tools (${tools.length}):`);
 76 |     tools.forEach((tool, index) => {
 77 |       console.log(`  ${index + 1}. ${tool.name}`);
 78 |     });
 79 |   });
 80 | 
 81 |   // Group resources by category
 82 |   console.log("\n=== Resources by Category ===");
 83 |   const resourcesByCategory: Record<string, any[]> = {};
 84 |   resourcesResult.resources.forEach((resource: any) => {
 85 |     let category = "Other";
 86 |     const uri = resource.uriPattern || resource.uri || "";
 87 |     if (uri.startsWith("jira://")) {
 88 |       category = "Jira";
 89 |     } else if (uri.startsWith("confluence://")) {
 90 |       category = "Confluence";
 91 |     }
 92 |     if (!resourcesByCategory[category]) resourcesByCategory[category] = [];
 93 |     resourcesByCategory[category].push(resource);
 94 |   });
 95 |   Object.entries(resourcesByCategory).forEach(([category, resources]) => {
 96 |     console.log(`\n${category} Resources (${resources.length}):`);
 97 |     resources.forEach((resource: any, index: number) => {
 98 |       const uri = resource.uriPattern || resource.uri || "";
 99 |       console.log(`  ${index + 1}. ${uri}`);
100 |     });
101 |   });
102 | 
103 |   // Show details for some important tools
104 |   console.log("\n=== Tool Details ===");
105 |   const toolsToInspect = ["createIssue", "updatePage", "addComment"];
106 |   for (const toolName of toolsToInspect) {
107 |     const tool = toolsResult.tools.find(t => t.name === toolName);
108 |     if (tool) {
109 |       console.log(`\nTool: ${tool.name}`);
110 |       console.log(`Description: ${tool.description || 'No description'}`);
111 |       console.log("Input Schema:", JSON.stringify(tool.inputSchema, null, 2));
112 |     }
113 |   }
114 | 
115 |   await client.close();
116 |   console.log("\nDone.");
117 | }
118 | 
119 | main();
120 | 
```

--------------------------------------------------------------------------------
/dev_mcp-atlassian-test-client/src/test-confluence-pages.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { Client } from "@modelcontextprotocol/sdk/client/index.js";
  2 | import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
  3 | import path from 'path';
  4 | import { fileURLToPath } from "url";
  5 | import fs from "fs";
  6 | 
  7 | // Get current file path
  8 | const __filename = fileURLToPath(import.meta.url);
  9 | const __dirname = path.dirname(__filename);
 10 | 
 11 | // Load environment variables from .env
 12 | function loadEnv(): Record<string, string> {
 13 |   try {
 14 |     const envFile = path.resolve(process.cwd(), '.env');
 15 |     const envContent = fs.readFileSync(envFile, 'utf8');
 16 |     const envVars: Record<string, string> = {};
 17 |     envContent.split('\n').forEach(line => {
 18 |       if (line.trim().startsWith('#') || !line.trim()) return;
 19 |       const [key, ...valueParts] = line.split('=');
 20 |       if (key && valueParts.length > 0) {
 21 |         const value = valueParts.join('=');
 22 |         envVars[key.trim()] = value.trim();
 23 |       }
 24 |     });
 25 |     return envVars;
 26 |   } catch (error) {
 27 |     console.error("Error loading .env file:", error);
 28 |     return {};
 29 |   }
 30 | }
 31 | 
 32 | // Print only response data
 33 | function printResourceMetaAndSchema(res: any) {
 34 |   if (res.contents && res.contents.length > 0) {
 35 |     const content = res.contents[0];
 36 |     
 37 |     // COMMENTED OUT: Metadata and schema printing
 38 |     // // Print metadata if exists
 39 |     // if (content.metadata) {
 40 |     //   console.log("Metadata:", content.metadata);
 41 |     // }
 42 |     // // Print schema if exists
 43 |     // if (content.schema) {
 44 |     //   console.log("Schema:", JSON.stringify(content.schema, null, 2));
 45 |     // }
 46 |     
 47 |     // Try to parse text if exists
 48 |     if (content.text) {
 49 |       try {
 50 |         const data = JSON.parse(String(content.text));
 51 |         console.log("Response Data:", JSON.stringify(data, null, 2));
 52 |       } catch (e) {
 53 |         console.log("Raw Response:", content.text);
 54 |       }
 55 |     }
 56 |   }
 57 | }
 58 | 
 59 | async function main() {
 60 |   const client = new Client({
 61 |     name: "mcp-atlassian-test-client-confluence-pages",
 62 |     version: "1.0.0"
 63 |   });
 64 | 
 65 |   // Path to MCP server
 66 |   const serverPath = "/Users/phucnt/Workspace/mcp-atlassian-server/dist/index.js";
 67 | 
 68 |   // Load environment variables
 69 |   const envVars = loadEnv();
 70 |   const processEnv: Record<string, string> = {};
 71 |   Object.keys(process.env).forEach(key => {
 72 |     if (process.env[key] !== undefined) {
 73 |       processEnv[key] = process.env[key] as string;
 74 |     }
 75 |   });
 76 | 
 77 |   // Initialize transport
 78 |   const transport = new StdioClientTransport({
 79 |     command: "node",
 80 |     args: [serverPath],
 81 |     env: {
 82 |       ...processEnv,
 83 |       ...envVars
 84 |     }
 85 |   });
 86 | 
 87 |   // Connect to server
 88 |   console.log("Connecting to MCP server...");
 89 |   await client.connect(transport);
 90 | 
 91 |   console.log("\n=== Test Confluence Pages Resource ===");
 92 | 
 93 |   // Change these values to match your environment if needed
 94 |   const pageId = "19431426"; // Home page id mới cho space AWA1
 95 |   const spaceKey = "AWA1"; // Space key mới
 96 |   const resourceUris = [
 97 |     `confluence://pages/${pageId}`,
 98 |     `confluence://spaces/${spaceKey}/pages`,
 99 |     `confluence://pages/${pageId}/children`,
100 |     `confluence://pages/${pageId}/comments`,
101 |     `confluence://pages/${pageId}/versions`,
102 |     `confluence://pages/${pageId}/ancestors`,
103 |     `confluence://pages/${pageId}/attachments`
104 |   ];
105 | 
106 |   for (const uri of resourceUris) {
107 |     try {
108 |       console.log(`\nResource: ${uri}`);
109 |       const res = await client.readResource({ uri });
110 |       if (uri.includes("?cql=")) {
111 |         const pagesData = JSON.parse(String(res.contents[0].text));
112 |         console.log("Number of pages from CQL:", pagesData.pages?.length || 0);
113 |       } else if (uri.includes("/children")) {
114 |         const childrenData = JSON.parse(String(res.contents[0].text));
115 |         console.log("Number of children:", childrenData.children?.length || 0);
116 |       } else if (uri.includes("/comments")) {
117 |         const commentsData = JSON.parse(String(res.contents[0].text));
118 |         console.log("Number of comments:", commentsData.comments?.length || 0);
119 |       } else if (uri.includes("/versions")) {
120 |         const versionsData = JSON.parse(String(res.contents[0].text));
121 |         console.log("Number of versions:", versionsData.versions?.length || 0);
122 |       } else if (uri.includes("/ancestors")) {
123 |         const ancestorsData = JSON.parse(String(res.contents[0].text));
124 |         console.log("Ancestors:", JSON.stringify(ancestorsData.ancestors, null, 2));
125 |       } else if (uri.includes("/attachments")) {
126 |         const attachmentsData = JSON.parse(String(res.contents[0].text));
127 |         console.log("Number of attachments:", attachmentsData.attachments?.length || 0);
128 |       }
129 |       printResourceMetaAndSchema(res);
130 |     } catch (e) {
131 |       console.error(`Resource ${uri} error:`, e instanceof Error ? e.message : e);
132 |     }
133 |   }
134 | 
135 |   console.log("\n=== Finished testing Confluence Pages Resource! ===");
136 |   await client.close();
137 | }
138 | 
139 | main();
```

--------------------------------------------------------------------------------
/src/utils/error-handler.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { Logger } from './logger.js';
  2 | 
  3 | // Initialize logger
  4 | const logger = Logger.getLogger('ErrorHandler');
  5 | 
  6 | /**
  7 |  * API Error Type
  8 |  */
  9 | export enum ApiErrorType {
 10 |   AUTHENTICATION_ERROR = 'AUTHENTICATION_ERROR',
 11 |   AUTHORIZATION_ERROR = 'AUTHORIZATION_ERROR',
 12 |   VALIDATION_ERROR = 'VALIDATION_ERROR',
 13 |   NOT_FOUND_ERROR = 'NOT_FOUND_ERROR',
 14 |   RATE_LIMIT_ERROR = 'RATE_LIMIT_ERROR',
 15 |   SERVER_ERROR = 'SERVER_ERROR',
 16 |   NETWORK_ERROR = 'NETWORK_ERROR',
 17 |   UNKNOWN_ERROR = 'UNKNOWN_ERROR',
 18 |   RESOURCE_ERROR = 'RESOURCE_ERROR'
 19 | }
 20 | 
 21 | /**
 22 |  * API Error
 23 |  */
 24 | export class ApiError extends Error {
 25 |   readonly type: ApiErrorType;
 26 |   readonly statusCode: number;
 27 |   readonly code: string;
 28 |   readonly originalError?: Error;
 29 | 
 30 |   /**
 31 |    * Initialize ApiError
 32 |    * @param type Error type
 33 |    * @param message Error message
 34 |    * @param statusCode HTTP status code
 35 |    * @param originalError Original error (optional)
 36 |    */
 37 |   constructor(
 38 |     type: ApiErrorType,
 39 |     message: string,
 40 |     statusCode: number = 500,
 41 |     originalError?: Error
 42 |   ) {
 43 |     super(message);
 44 |     this.name = 'ApiError';
 45 |     this.type = type;
 46 |     this.statusCode = statusCode;
 47 |     this.code = type; // Use ApiErrorType as code
 48 |     this.originalError = originalError;
 49 | 
 50 |     // Log error
 51 |     logger.error(`${type}: ${message}`, {
 52 |       statusCode,
 53 |       code: this.code,
 54 |       originalError: originalError?.message
 55 |     });
 56 |   }
 57 | 
 58 |   /**
 59 |    * Convert ApiError to JSON string
 60 |    * @returns JSON representation of the error
 61 |    */
 62 |   toJSON(): Record<string, any> {
 63 |     return {
 64 |       error: true,
 65 |       type: this.type,
 66 |       code: this.code,
 67 |       message: this.message,
 68 |       statusCode: this.statusCode
 69 |     };
 70 |   }
 71 | }
 72 | 
 73 | /**
 74 |  * Handle error from Atlassian API
 75 |  * @param error Error to handle
 76 |  * @returns Normalized ApiError
 77 |  */
 78 | export function handleAtlassianError(error: any): ApiError {
 79 |   // If already an ApiError, return it
 80 |   if (error instanceof ApiError) {
 81 |     return error;
 82 |   }
 83 |   
 84 |   // Handle HTTP error from Atlassian API
 85 |   if (error.response) {
 86 |     const { status, data } = error.response;
 87 |     
 88 |     switch (status) {
 89 |       case 400:
 90 |         return new ApiError(
 91 |           ApiErrorType.VALIDATION_ERROR,
 92 |           data.message || 'Invalid data',
 93 |           400,
 94 |           error
 95 |         );
 96 |       case 401:
 97 |         return new ApiError(
 98 |           ApiErrorType.AUTHENTICATION_ERROR,
 99 |           'Authentication failed. Please check your API token.',
100 |           401,
101 |           error
102 |         );
103 |       case 403:
104 |         return new ApiError(
105 |           ApiErrorType.AUTHORIZATION_ERROR,
106 |           'You do not have permission to access this resource.',
107 |           403,
108 |           error
109 |         );
110 |       case 404:
111 |         return new ApiError(
112 |           ApiErrorType.NOT_FOUND_ERROR,
113 |           'Requested resource not found.',
114 |           404,
115 |           error
116 |         );
117 |       case 429:
118 |         return new ApiError(
119 |           ApiErrorType.RATE_LIMIT_ERROR,
120 |           'Rate limit exceeded. Please try again later.',
121 |           429,
122 |           error
123 |         );
124 |       case 500:
125 |       case 502:
126 |       case 503:
127 |       case 504:
128 |         return new ApiError(
129 |           ApiErrorType.SERVER_ERROR,
130 |           'Atlassian server error.',
131 |           status,
132 |           error
133 |         );
134 |       default:
135 |         return new ApiError(
136 |           ApiErrorType.UNKNOWN_ERROR,
137 |           `Unknown error (${status})`,
138 |           status,
139 |           error
140 |         );
141 |     }
142 |   }
143 | 
144 |   // Handle network error
145 |   if (error.request) {
146 |     return new ApiError(
147 |       ApiErrorType.NETWORK_ERROR,
148 |       'Cannot connect to Atlassian API.',
149 |       0,
150 |       error
151 |     );
152 |   }
153 | 
154 |   // Other errors
155 |   return new ApiError(
156 |     ApiErrorType.UNKNOWN_ERROR,
157 |     error.message || 'Unknown error',
158 |     500,
159 |     error
160 |   );
161 | }
162 | 
163 | /**
164 |  * Utility function to handle errors when calling API
165 |  * @param fn Function to handle errors for
166 |  * @returns Function with error handling
167 |  */
168 | export function withErrorHandling<T>(fn: () => Promise<T>): Promise<T> {
169 |   return fn().catch(error => {
170 |     throw handleAtlassianError(error);
171 |   });
172 | }
173 | 
174 | /**
175 |  * Higher-order function that wraps a resource handler with error handling
176 |  * @param resourceName Name of the resource for logging purposes
177 |  * @param handler Resource handler function to wrap
178 |  * @returns Wrapped handler function with error handling
179 |  */
180 | export function wrapResourceWithErrorHandling<T, P>(
181 |   resourceName: string,
182 |   handler: (params: P) => Promise<T>
183 | ): (params: P) => Promise<T> {
184 |   return async (params: P): Promise<T> => {
185 |     try {
186 |       return await handler(params);
187 |     } catch (error) {
188 |       logger.error(`Error in resource ${resourceName}:`, error);
189 |       
190 |       // Convert to ApiError if not already
191 |       const apiError = error instanceof ApiError 
192 |         ? error 
193 |         : new ApiError(
194 |             ApiErrorType.RESOURCE_ERROR,
195 |             `Error processing resource ${resourceName}: ${error instanceof Error ? error.message : String(error)}`,
196 |             500,
197 |             error instanceof Error ? error : new Error(String(error))
198 |           );
199 |           
200 |       throw apiError;
201 |     }
202 |   };
203 | } 
```

--------------------------------------------------------------------------------
/src/utils/confluence-interfaces.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Confluence API Interface
  3 |  * Define data structures for Confluence API
  4 |  */
  5 | 
  6 | /**
  7 |  * Confluence user information
  8 |  */
  9 | export interface ConfluenceUser {
 10 |   accountId: string;
 11 |   email?: string;
 12 |   displayName: string;
 13 |   publicName?: string;
 14 |   profilePicture: {
 15 |     path: string;
 16 |     width: number;
 17 |     height: number;
 18 |     isDefault: boolean;
 19 |   };
 20 | }
 21 | 
 22 | /**
 23 |  * Space information
 24 |  */
 25 | export interface ConfluenceSpace {
 26 |   id: string;
 27 |   key: string;
 28 |   name: string;
 29 |   type: 'global' | 'personal';
 30 |   status: 'current';
 31 |   _expandable?: Record<string, string>;
 32 |   _links?: Record<string, string>;
 33 | }
 34 | 
 35 | /**
 36 |  * Version information
 37 |  */
 38 | export interface ConfluenceVersion {
 39 |   by: ConfluenceUser;
 40 |   when: string;
 41 |   number: number;
 42 |   message?: string;
 43 |   minorEdit: boolean;
 44 |   hidden: boolean;
 45 | }
 46 | 
 47 | /**
 48 |  * Confluence content type
 49 |  */
 50 | export type ConfluenceContentType = 'page' | 'blogpost' | 'comment' | 'attachment';
 51 | 
 52 | /**
 53 |  * Content body
 54 |  */
 55 | export interface ConfluenceBody {
 56 |   storage: {
 57 |     value: string;
 58 |     representation: 'storage' | 'view' | 'export_view' | 'styled_view' | 'anonymous_export_view';
 59 |   };
 60 |   _expandable?: Record<string, string>;
 61 | }
 62 | 
 63 | /**
 64 |  * Content information in Confluence
 65 |  */
 66 | export interface ConfluenceContent {
 67 |   id: string;
 68 |   type: ConfluenceContentType;
 69 |   status: 'current' | 'trashed' | 'historical' | 'draft';
 70 |   title: string;
 71 |   space?: ConfluenceSpace;
 72 |   version?: ConfluenceVersion;
 73 |   body?: ConfluenceBody;
 74 |   ancestors?: ConfluenceContent[];
 75 |   children?: {
 76 |     page?: {
 77 |       results: ConfluenceContent[];
 78 |       size: number;
 79 |     };
 80 |     comment?: {
 81 |       results: ConfluenceContent[];
 82 |       size: number;
 83 |     };
 84 |     attachment?: {
 85 |       results: ConfluenceContent[];
 86 |       size: number;
 87 |     };
 88 |   };
 89 |   descendants?: {
 90 |     page?: {
 91 |       results: ConfluenceContent[];
 92 |       size: number;
 93 |     };
 94 |     comment?: {
 95 |       results: ConfluenceContent[];
 96 |       size: number;
 97 |     };
 98 |     attachment?: {
 99 |       results: ConfluenceContent[];
100 |       size: number;
101 |     };
102 |   };
103 |   container?: {
104 |     id: string;
105 |     type: ConfluenceContentType;
106 |     _links?: Record<string, string>;
107 |   };
108 |   metadata?: {
109 |     labels?: {
110 |       results: {
111 |         prefix: string;
112 |         name: string;
113 |         id: string;
114 |       }[];
115 |       size: number;
116 |     };
117 |     currentuser?: Record<string, any>;
118 |     properties?: Record<string, any>;
119 |   };
120 |   restrictions?: {
121 |     read?: {
122 |       restrictions: {
123 |         group?: {
124 |           name: string;
125 |           type: string;
126 |         };
127 |         user?: ConfluenceUser;
128 |       }[];
129 |       operation: 'read';
130 |     };
131 |     update?: {
132 |       restrictions: {
133 |         group?: {
134 |           name: string;
135 |           type: string;
136 |         };
137 |         user?: ConfluenceUser;
138 |       }[];
139 |       operation: 'update';
140 |     };
141 |   };
142 |   _expandable?: Record<string, string>;
143 |   _links?: Record<string, string>;
144 | }
145 | 
146 | /**
147 |  * Parameters for creating new content
148 |  */
149 | export interface CreateContentParams {
150 |   type: ConfluenceContentType;
151 |   space: {
152 |     key: string;
153 |   };
154 |   title: string;
155 |   body: {
156 |     storage: {
157 |       value: string;
158 |       representation: 'storage';
159 |     };
160 |   };
161 |   ancestors?: {
162 |     id: string;
163 |   }[];
164 |   status?: 'current' | 'draft';
165 | }
166 | 
167 | /**
168 |  * Parameters for updating content
169 |  */
170 | export interface UpdateContentParams {
171 |   type?: ConfluenceContentType;
172 |   title?: string;
173 |   body?: {
174 |     storage: {
175 |       value: string;
176 |       representation: 'storage';
177 |     };
178 |   };
179 |   version: {
180 |     number: number;
181 |   };
182 |   status?: 'current' | 'draft';
183 | }
184 | 
185 | /**
186 |  * Parameters for searching spaces
187 |  */
188 | export interface SearchSpacesParams {
189 |   keys?: string[];
190 |   type?: 'global' | 'personal';
191 |   status?: 'current' | 'archived';
192 |   label?: string;
193 |   expand?: string[];
194 |   start?: number;
195 |   limit?: number;
196 | }
197 | 
198 | /**
199 |  * Search result for spaces
200 |  */
201 | export interface SearchSpacesResult {
202 |   results: ConfluenceSpace[];
203 |   start: number;
204 |   limit: number;
205 |   size: number;
206 |   _links?: Record<string, string>;
207 | }
208 | 
209 | /**
210 |  * Parameters for searching content
211 |  */
212 | export interface SearchContentParams {
213 |   cql: string;
214 |   cqlcontext?: Record<string, string>;
215 |   expand?: string[];
216 |   start?: number;
217 |   limit?: number;
218 | }
219 | 
220 | /**
221 |  * Search result for content
222 |  */
223 | export interface SearchContentResult {
224 |   results: ConfluenceContent[];
225 |   start: number;
226 |   limit: number;
227 |   size: number;
228 |   totalSize?: number;
229 |   cqlQuery?: string;
230 |   searchDuration?: number;
231 |   _links?: Record<string, string>;
232 | }
233 | 
234 | /**
235 |  * Information about a comment
236 |  */
237 | export interface ConfluenceComment {
238 |   id: string;
239 |   type: 'comment';
240 |   status: 'current' | 'trashed' | 'historical' | 'draft';
241 |   title: string;
242 |   body: ConfluenceBody;
243 |   version: ConfluenceVersion;
244 |   container: {
245 |     id: string;
246 |     type: ConfluenceContentType;
247 |     _links?: Record<string, string>;
248 |   };
249 |   _expandable?: Record<string, string>;
250 |   _links?: Record<string, string>;
251 | }
252 | 
253 | /**
254 |  * Parameters for creating a comment
255 |  */
256 | export interface CreateCommentParams {
257 |   body: {
258 |     storage: {
259 |       value: string;
260 |       representation: 'storage';
261 |     };
262 |   };
263 |   container: {
264 |     id: string;
265 |     type: ConfluenceContentType;
266 |   };
267 |   status?: 'current' | 'draft';
268 | } 
```

--------------------------------------------------------------------------------
/src/utils/mcp-helpers.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Helper functions for MCP resources and tools
  3 |  */
  4 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
  5 | import { McpResponse, createJsonResponse, createErrorResponse, createSuccessResponse } from './mcp-core.js';
  6 | import { ApiError, ApiErrorType } from './error-handler.js';
  7 | import { AtlassianConfig } from './atlassian-api-base.js';
  8 | import { Logger } from './logger.js';
  9 | import { StandardMetadata, createStandardMetadata } from '../schemas/common.js';
 10 | 
 11 | const logger = Logger.getLogger('MCPHelpers');
 12 | 
 13 | /**
 14 |  * Environment and configuration utilities
 15 |  */
 16 | export namespace Config {
 17 |   /**
 18 |    * Get Atlassian configuration from environment variables
 19 |    */
 20 |   export function getAtlassianConfigFromEnv(): AtlassianConfig {
 21 |     const ATLASSIAN_SITE_NAME = process.env.ATLASSIAN_SITE_NAME || '';
 22 |     const ATLASSIAN_USER_EMAIL = process.env.ATLASSIAN_USER_EMAIL || '';
 23 |     const ATLASSIAN_API_TOKEN = process.env.ATLASSIAN_API_TOKEN || '';
 24 | 
 25 |     if (!ATLASSIAN_SITE_NAME || !ATLASSIAN_USER_EMAIL || !ATLASSIAN_API_TOKEN) {
 26 |       logger.error('Missing Atlassian credentials in environment variables');
 27 |       throw new Error('Missing Atlassian credentials in environment variables');
 28 |     }
 29 | 
 30 |     return {
 31 |       baseUrl: ATLASSIAN_SITE_NAME.includes('.atlassian.net') 
 32 |         ? `https://${ATLASSIAN_SITE_NAME}` 
 33 |         : ATLASSIAN_SITE_NAME,
 34 |       email: ATLASSIAN_USER_EMAIL,
 35 |       apiToken: ATLASSIAN_API_TOKEN
 36 |     };
 37 |   }
 38 | 
 39 |   /**
 40 |    * Helper to get Atlassian config from context or environment
 41 |    */
 42 |   export function getConfigFromContextOrEnv(context: any): AtlassianConfig {
 43 |     if (context?.atlassianConfig) {
 44 |       return context.atlassianConfig;
 45 |     }
 46 |     return getAtlassianConfigFromEnv();
 47 |   }
 48 | }
 49 | 
 50 | /**
 51 |  * Resource helper functions
 52 |  */
 53 | export namespace Resources {
 54 |   /**
 55 |    * Create a standardized resource response with metadata and schema
 56 |    */
 57 |   export function createStandardResource(
 58 |     uri: string,
 59 |     data: any[],
 60 |     dataKey: string,
 61 |     schema: any,
 62 |     totalCount: number,
 63 |     limit: number,
 64 |     offset: number,
 65 |     uiUrl?: string
 66 |   ): McpResponse {
 67 |     // Create standard metadata
 68 |     const metadata = createStandardMetadata(totalCount, limit, offset, uri, uiUrl);
 69 |     
 70 |     // Create response data object
 71 |     const responseData: Record<string, any> = {
 72 |       metadata: metadata
 73 |     };
 74 |     
 75 |     // Add the data with the specified key
 76 |     responseData[dataKey] = data;
 77 |     
 78 |     // Return formatted resource
 79 |     return createJsonResponse(uri, responseData);
 80 |   }
 81 | 
 82 |   /**
 83 |    * Extract paging parameters from resource URI or request
 84 |    */
 85 |   export function extractPagingParams(
 86 |     params: any,
 87 |     defaultLimit: number = 20,
 88 |     defaultOffset: number = 0
 89 |   ): { limit: number, offset: number } {
 90 |     let limit = defaultLimit;
 91 |     let offset = defaultOffset;
 92 |     
 93 |     if (params) {
 94 |       // Extract limit
 95 |       if (params.limit) {
 96 |         const limitParam = Array.isArray(params.limit) ? params.limit[0] : params.limit;
 97 |         const parsedLimit = parseInt(limitParam, 10);
 98 |         if (!isNaN(parsedLimit) && parsedLimit > 0) {
 99 |           limit = parsedLimit;
100 |         }
101 |       }
102 |       // Extract offset
103 |       if (params.offset) {
104 |         const offsetParam = Array.isArray(params.offset) ? params.offset[0] : params.offset;
105 |         const parsedOffset = parseInt(offsetParam, 10);
106 |         if (!isNaN(parsedOffset) && parsedOffset >= 0) {
107 |           offset = parsedOffset;
108 |         }
109 |       }
110 |     }
111 |     return { limit, offset };
112 |   }
113 | }
114 | 
115 | /**
116 |  * Tool helper functions
117 |  */
118 | export namespace Tools {
119 |   /**
120 |    * Standardized response structure for MCP tools
121 |    */
122 |   export interface ToolResponse<T = any> {
123 |     contents: Array<{
124 |       mimeType: string;
125 |       text: string;
126 |     }>;
127 |     isError?: boolean;
128 |   }
129 | 
130 |   /**
131 |    * Create a standardized response for MCP tools
132 |    */
133 |   export function createToolResponse<T = any>(success: boolean, message?: string, data?: T): ToolResponse<T> {
134 |     const response = {
135 |       success,
136 |       ...(message && { message }),
137 |       ...(data && { data })
138 |     };
139 |     return {
140 |       contents: [
141 |         {
142 |           mimeType: 'application/json',
143 |           text: JSON.stringify(response)
144 |         }
145 |       ]
146 |     };
147 |   }
148 | 
149 |   /**
150 |    * Higher-order function to wrap a tool implementation with standardized error handling
151 |    */
152 |   export function wrapWithErrorHandling<T, P>(
153 |     toolName: string,
154 |     handler: (params: P) => Promise<T>
155 |   ): (params: P) => Promise<ToolResponse<T>> {
156 |     return async (params: P): Promise<ToolResponse<T>> => {
157 |       try {
158 |         // Execute the handler
159 |         const result = await handler(params);
160 |         // Return successful response with data
161 |         return createToolResponse<T>(true, `${toolName} executed successfully`, result);
162 |       } catch (error) {
163 |         // Log the error
164 |         logger.error(`Error executing tool ${toolName}:`, error);
165 |         // Create appropriate error message
166 |         let errorMessage: string;
167 |         if (error instanceof ApiError) {
168 |           errorMessage = error.message;
169 |         } else {
170 |           errorMessage = error instanceof Error ? error.message : String(error);
171 |         }
172 |         // Return standardized error response
173 |         return createToolResponse(false, errorMessage);
174 |       }
175 |     };
176 |   }
177 | } 
```

--------------------------------------------------------------------------------
/src/utils/jira-interfaces.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Jira API Interface
  3 |  * Define data structures for Jira API
  4 |  */
  5 | 
  6 | /**
  7 |  * Interface defining data types for Jira API
  8 |  */
  9 | 
 10 | /**
 11 |  * Define Jira user information
 12 |  */
 13 | export interface JiraUser {
 14 |   accountId: string;
 15 |   emailAddress?: string;
 16 |   displayName: string;
 17 |   active: boolean;
 18 |   timeZone?: string;
 19 |   accountType: string;
 20 |   avatarUrls?: {
 21 |     '48x48'?: string;
 22 |     '24x24'?: string;
 23 |     '16x16'?: string;
 24 |     '32x32'?: string;
 25 |   };
 26 |   self: string;
 27 | }
 28 | 
 29 | /**
 30 |  * Define Jira project information
 31 |  */
 32 | export interface JiraProject {
 33 |   id: string;
 34 |   key: string;
 35 |   name: string;
 36 |   self: string;
 37 |   avatarUrls?: Record<string, string>;
 38 |   projectCategory?: {
 39 |     id: string;
 40 |     name: string;
 41 |     description?: string;
 42 |   };
 43 |   simplified?: boolean;
 44 |   style?: string;
 45 |   isPrivate?: boolean;
 46 | }
 47 | 
 48 | /**
 49 |  * Define issue type
 50 |  */
 51 | export interface JiraIssueType {
 52 |   id: string;
 53 |   name: string;
 54 |   description?: string;
 55 |   iconUrl: string;
 56 |   subtask: boolean;
 57 |   avatarId?: number;
 58 |   entityId?: string;
 59 |   hierarchyLevel?: number;
 60 |   self: string;
 61 | }
 62 | 
 63 | /**
 64 |  * Define issue status
 65 |  */
 66 | export interface JiraStatus {
 67 |   id: string;
 68 |   name: string;
 69 |   description?: string;
 70 |   statusCategory: {
 71 |     id: number;
 72 |     key: string;
 73 |     name: string;
 74 |     colorName: string;
 75 |     self: string;
 76 |   };
 77 |   self: string;
 78 | }
 79 | 
 80 | /**
 81 |  * Define custom field
 82 |  */
 83 | export interface JiraCustomField {
 84 |   id: string;
 85 |   key?: string;
 86 |   name?: string;
 87 |   custom: boolean;
 88 |   orderable: boolean;
 89 |   navigable: boolean;
 90 |   searchable: boolean;
 91 |   clauseNames?: string[];
 92 |   schema?: {
 93 |     type: string;
 94 |     custom?: string;
 95 |     customId?: number;
 96 |     items?: string;
 97 |   };
 98 | }
 99 | 
100 | /**
101 |  * Define issue priority
102 |  */
103 | export interface JiraPriority {
104 |   id: string;
105 |   name: string;
106 |   iconUrl: string;
107 |   self: string;
108 | }
109 | 
110 | /**
111 |  * Define creator/updater
112 |  */
113 | export interface JiraUserDetails {
114 |   self: string;
115 |   accountId: string;
116 |   displayName: string;
117 |   active: boolean;
118 | }
119 | 
120 | /**
121 |  * Define version/update information
122 |  */
123 | export interface JiraVersionInfo {
124 |   by: JiraUserDetails;
125 |   when: string;
126 | }
127 | 
128 | /**
129 |  * Define rich text content
130 |  */
131 | export interface JiraContent {
132 |   type: string;
133 |   content?: JiraContent[];
134 |   text?: string;
135 |   attrs?: Record<string, any>;
136 | }
137 | 
138 | /**
139 |  * Define content format
140 |  */
141 | export interface JiraBody {
142 |   type: string;
143 |   version: number;
144 |   content: JiraContent[];
145 | }
146 | 
147 | /**
148 |  * Define comment
149 |  */
150 | export interface JiraComment {
151 |   id: string;
152 |   self: string;
153 |   body: any;
154 |   author: {
155 |     accountId: string;
156 |     displayName: string;
157 |     emailAddress?: string;
158 |     avatarUrls?: Record<string, string>;
159 |   };
160 |   created: string;
161 |   updated: string;
162 | }
163 | 
164 | /**
165 |  * Define comment list
166 |  */
167 | export interface JiraComments {
168 |   comments: JiraComment[];
169 |   maxResults: number;
170 |   total: number;
171 |   startAt: number;
172 | }
173 | 
174 | /**
175 |  * Define transition status
176 |  */
177 | export interface JiraTransition {
178 |   id: string;
179 |   name: string;
180 |   to: JiraStatus;
181 |   hasScreen: boolean;
182 |   isGlobal: boolean;
183 |   isInitial: boolean;
184 |   isConditional: boolean;
185 |   isAvailable: boolean;
186 | }
187 | 
188 | /**
189 |  * Define transition result
190 |  */
191 | export interface JiraTransitionsResult {
192 |   transitions: {
193 |     id: string;
194 |     name: string;
195 |     to: {
196 |       id: string;
197 |       name: string;
198 |       statusCategory?: {
199 |         id: number;
200 |         key: string;
201 |         name: string;
202 |       };
203 |     };
204 |   }[];
205 | }
206 | 
207 | /**
208 |  * Định nghĩa thông tin tệp đính kèm
209 |  */
210 | export interface JiraAttachment {
211 |   id: string;
212 |   filename: string;
213 |   author: JiraUserDetails;
214 |   created: string;
215 |   size: number;
216 |   mimeType: string;
217 |   content: string;
218 |   thumbnail?: string;
219 |   self: string;
220 | }
221 | 
222 | /**
223 |  * Định nghĩa issue trong Jira
224 |  */
225 | export interface JiraIssue {
226 |   id: string;
227 |   key: string;
228 |   self: string;
229 |   fields: {
230 |     summary: string;
231 |     description?: any;
232 |     issuetype: {
233 |       id: string;
234 |       name: string;
235 |       iconUrl?: string;
236 |     };
237 |     project: {
238 |       id: string;
239 |       key: string;
240 |       name: string;
241 |     };
242 |     status?: {
243 |       id: string;
244 |       name: string;
245 |       statusCategory?: {
246 |         id: number;
247 |         key: string;
248 |         name: string;
249 |       };
250 |     };
251 |     priority?: {
252 |       id: string;
253 |       name: string;
254 |     };
255 |     labels?: string[];
256 |     assignee?: {
257 |       accountId: string;
258 |       displayName: string;
259 |       emailAddress?: string;
260 |       avatarUrls?: Record<string, string>;
261 |     };
262 |     reporter?: {
263 |       accountId: string;
264 |       displayName: string;
265 |       emailAddress?: string;
266 |       avatarUrls?: Record<string, string>;
267 |     };
268 |     created?: string;
269 |     updated?: string;
270 |     [key: string]: any;
271 |   };
272 |   changelog?: {
273 |     histories: {
274 |       id: string;
275 |       author: JiraUserDetails;
276 |       created: string;
277 |       items: {
278 |         field: string;
279 |         fieldtype: string;
280 |         from?: string;
281 |         fromString?: string;
282 |         to?: string;
283 |         toString?: string;
284 |       }[];
285 |     }[];
286 |   };
287 | }
288 | 
289 | /**
290 |  * Định nghĩa kết quả tìm kiếm
291 |  */
292 | export interface JiraSearchResult {
293 |   startAt: number;
294 |   maxResults: number;
295 |   total: number;
296 |   issues: JiraIssue[];
297 | }
298 | 
299 | /**
300 |  * Định nghĩa tham số tìm kiếm
301 |  */
302 | export interface JiraSearchParams {
303 |   jql: string;
304 |   startAt?: number;
305 |   maxResults?: number;
306 |   fields?: string[];
307 |   validateQuery?: boolean;
308 |   expand?: string[];
309 | }
310 | 
311 | /**
312 |  * Định nghĩa tham số tạo issue
313 |  */
314 | export interface JiraCreateIssueParams {
315 |   fields: {
316 |     summary: string;
317 |     issuetype: {
318 |       id: string;
319 |     };
320 |     project: {
321 |       id: string;
322 |     };
323 |     description?: any;
324 |     [key: string]: any;
325 |   };
326 |   update?: any;
327 | } 
```

--------------------------------------------------------------------------------
/docs/dev-guide/resource-metadata-schema-guideline.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Hướng Dẫn Chuẩn Hóa Metadata và Bổ Sung Schema cho MCP Server
  2 | 
  3 | ## 1. Chuẩn Hóa Metadata Trả Về
  4 | 
  5 | ### Tạo Cấu Trúc Metadata Nhất Quán
  6 | 
  7 | ```typescript
  8 | // Định nghĩa interface chuẩn cho metadata
  9 | interface StandardMetadata {
 10 |   total: number;          // Tổng số bản ghi
 11 |   limit: number;          // Số bản ghi tối đa trả về
 12 |   offset: number;         // Vị trí bắt đầu
 13 |   hasMore: boolean;       // Còn dữ liệu không
 14 |   links?: {               // Các liên kết hữu ích
 15 |     self: string;         // Link đến resource hiện tại
 16 |     ui?: string;          // Link đến UI Atlassian
 17 |     next?: string;        // Link đến trang tiếp theo
 18 |   }
 19 | }
 20 | 
 21 | // Hàm helper để tạo metadata chuẩn
 22 | function createStandardMetadata(
 23 |   total: number,
 24 |   limit: number,
 25 |   offset: number,
 26 |   baseUrl: string,
 27 |   uiUrl?: string
 28 | ): StandardMetadata {
 29 |   const hasMore = offset + limit  {
 30 |     // Xử lý query parameters
 31 |     const url = new URL(uri.href);
 32 |     const limit = parseInt(url.searchParams.get("limit") || "20");
 33 |     const offset = parseInt(url.searchParams.get("offset") || "0");
 34 |     
 35 |     // Lấy dữ liệu từ Jira API
 36 |     const issues = await jiraClient.getIssues(limit, offset);
 37 |     const total = issues.total;
 38 |     
 39 |     // Tạo metadata chuẩn
 40 |     const metadata = createStandardMetadata(
 41 |       total,
 42 |       limit,
 43 |       offset,
 44 |       uri.href,
 45 |       `https://${process.env.ATLASSIAN_SITE_NAME}/jira/issues`
 46 |     );
 47 |     
 48 |     // Trả về kết quả với metadata chuẩn
 49 |     return {
 50 |       contents: [{
 51 |         uri: uri.href,
 52 |         mimeType: "application/json",
 53 |         text: JSON.stringify({
 54 |           metadata,
 55 |           issues: issues.issues
 56 |         })
 57 |       }]
 58 |     };
 59 |   }
 60 | );
 61 | ```
 62 | 
 63 | ## 2. Bổ Sung Schema Cho Resource MCP
 64 | 
 65 | ### Định Nghĩa Schema Cho Resource
 66 | 
 67 | ```typescript
 68 | // Định nghĩa schema cho issue
 69 | const issueSchema = {
 70 |   type: "object",
 71 |   properties: {
 72 |     key: { type: "string", description: "Issue key (e.g., PROJ-123)" },
 73 |     summary: { type: "string", description: "Issue title/summary" },
 74 |     status: { 
 75 |       type: "object", 
 76 |       properties: {
 77 |         name: { type: "string", description: "Status name" },
 78 |         id: { type: "string", description: "Status ID" }
 79 |       }
 80 |     },
 81 |     assignee: {
 82 |       type: "object",
 83 |       properties: {
 84 |         displayName: { type: "string", description: "Assignee's display name" },
 85 |         accountId: { type: "string", description: "Assignee's account ID" }
 86 |       },
 87 |       nullable: true
 88 |     }
 89 |   },
 90 |   required: ["key", "summary", "status"]
 91 | };
 92 | 
 93 | // Schema cho danh sách issues
 94 | const issuesListSchema = {
 95 |   type: "object",
 96 |   properties: {
 97 |     metadata: {
 98 |       type: "object",
 99 |       properties: {
100 |         total: { type: "number", description: "Total number of issues" },
101 |         limit: { type: "number", description: "Maximum number of issues returned" },
102 |         offset: { type: "number", description: "Starting position" },
103 |         hasMore: { type: "boolean", description: "Whether there are more issues" },
104 |         links: {
105 |           type: "object",
106 |           properties: {
107 |             self: { type: "string", description: "Link to this resource" },
108 |             ui: { type: "string", description: "Link to Atlassian UI" },
109 |             next: { type: "string", description: "Link to next page" }
110 |           }
111 |         }
112 |       },
113 |       required: ["total", "limit", "offset", "hasMore"]
114 |     },
115 |     issues: {
116 |       type: "array",
117 |       items: issueSchema
118 |     }
119 |   },
120 |   required: ["metadata", "issues"]
121 | };
122 | ```
123 | 
124 | ### Đăng Ký Resource Với Schema
125 | 
126 | ```typescript
127 | // Khi đăng ký resource với server
128 | server.setRequestHandler(ListResourcesRequestSchema, async () => ({
129 |   resources: [
130 |     {
131 |       uri: "jira://issues",
132 |       name: "Jira Issues",
133 |       description: "List of Jira issues with pagination",
134 |       mimeType: "application/json",
135 |       schema: issuesListSchema  // Thêm schema vào metadata resource
136 |     },
137 |     // Các resource khác...
138 |   ]
139 | }));
140 | ```
141 | 
142 | ### Trả Về Schema Trong Response
143 | 
144 | ```typescript
145 | // Trong handler của resource
146 | server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
147 |   if (request.params.uri === "jira://issues") {
148 |     // Xử lý logic lấy dữ liệu...
149 |     
150 |     return {
151 |       contents: [{
152 |         uri: request.params.uri,
153 |         mimeType: "application/json",
154 |         text: JSON.stringify(responseData),
155 |         schema: issuesListSchema  // Thêm schema vào response
156 |       }]
157 |     };
158 |   }
159 |   // Xử lý các resource khác...
160 | });
161 | ```
162 | 
163 | ## 3. Áp Dụng Cho Tất Cả Resource
164 | 
165 | Để áp dụng nhất quán cho tất cả resource, bạn nên:
166 | 
167 | 1. **Tạo thư viện schema**: Tạo file riêng chứa tất cả schema (ví dụ: `schemas/jira.ts`, `schemas/confluence.ts`)
168 | 2. **Tạo helper function**: Viết các hàm helper để tạo metadata chuẩn và response chuẩn
169 | 3. **Áp dụng cho tất cả resource handler**: Đảm bảo mọi resource đều sử dụng cấu trúc và helper giống nhau
170 | 
171 | ```typescript
172 | // Ví dụ helper function
173 | function createResourceResponse(uri: string, data: any, schema: any) {
174 |   return {
175 |     contents: [{
176 |       uri,
177 |       mimeType: "application/json",
178 |       text: JSON.stringify(data),
179 |       schema
180 |     }]
181 |   };
182 | }
183 | ```
184 | 
185 | ## 4. Kiểm Tra Với Cline
186 | 
187 | Sau khi triển khai, hãy kiểm tra với Cline để đảm bảo:
188 | - Cline hiển thị đúng kiểu dữ liệu (không còn "Returns Unknown")
189 | - Cline có thể render UI thông minh dựa trên schema
190 | - Metadata được hiển thị và sử dụng đúng (phân trang, liên kết, v.v.)
191 | 
192 | Việc chuẩn hóa này sẽ giúp MCP server của bạn chuyên nghiệp hơn, dễ sử dụng với AI agent, và tương thích tốt hơn với hệ sinh thái MCP.
```

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

```markdown
  1 | https://modelcontextprotocol.io/introduction
  2 | 
  3 | # Introduction
  4 | 
  5 | > Get started with the Model Context Protocol (MCP)
  6 | 
  7 | <Note>C# SDK released! Check out [what else is new.](/development/updates)</Note>
  8 | 
  9 | MCP is an open protocol that standardizes how applications provide context to LLMs. Think of MCP like a USB-C port for AI applications. Just as USB-C provides a standardized way to connect your devices to various peripherals and accessories, MCP provides a standardized way to connect AI models to different data sources and tools.
 10 | 
 11 | ## Why MCP?
 12 | 
 13 | MCP helps you build agents and complex workflows on top of LLMs. LLMs frequently need to integrate with data and tools, and MCP provides:
 14 | 
 15 | * A growing list of pre-built integrations that your LLM can directly plug into
 16 | * The flexibility to switch between LLM providers and vendors
 17 | * Best practices for securing your data within your infrastructure
 18 | 
 19 | ### General architecture
 20 | 
 21 | At its core, MCP follows a client-server architecture where a host application can connect to multiple servers:
 22 | 
 23 | ```mermaid
 24 | flowchart LR
 25 |     subgraph "Your Computer"
 26 |         Host["Host with MCP Client\n(Claude, IDEs, Tools)"]
 27 |         S1["MCP Server A"]
 28 |         S2["MCP Server B"]
 29 |         S3["MCP Server C"]
 30 |         Host <-->|"MCP Protocol"| S1
 31 |         Host <-->|"MCP Protocol"| S2
 32 |         Host <-->|"MCP Protocol"| S3
 33 |         S1 <--> D1[("Local\nData Source A")]
 34 |         S2 <--> D2[("Local\nData Source B")]
 35 |     end
 36 |     subgraph "Internet"
 37 |         S3 <-->|"Web APIs"| D3[("Remote\nService C")]
 38 |     end
 39 | ```
 40 | 
 41 | * **MCP Hosts**: Programs like Claude Desktop, IDEs, or AI tools that want to access data through MCP
 42 | * **MCP Clients**: Protocol clients that maintain 1:1 connections with servers
 43 | * **MCP Servers**: Lightweight programs that each expose specific capabilities through the standardized Model Context Protocol
 44 | * **Local Data Sources**: Your computer's files, databases, and services that MCP servers can securely access
 45 | * **Remote Services**: External systems available over the internet (e.g., through APIs) that MCP servers can connect to
 46 | 
 47 | ## Get started
 48 | 
 49 | Choose the path that best fits your needs:
 50 | 
 51 | #### Quick Starts
 52 | 
 53 | <CardGroup cols={2}>
 54 |   <Card title="For Server Developers" icon="bolt" href="/quickstart/server">
 55 |     Get started building your own server to use in Claude for Desktop and other clients
 56 |   </Card>
 57 | 
 58 |   <Card title="For Client Developers" icon="bolt" href="/quickstart/client">
 59 |     Get started building your own client that can integrate with all MCP servers
 60 |   </Card>
 61 | 
 62 |   <Card title="For Claude Desktop Users" icon="bolt" href="/quickstart/user">
 63 |     Get started using pre-built servers in Claude for Desktop
 64 |   </Card>
 65 | </CardGroup>
 66 | 
 67 | #### Examples
 68 | 
 69 | <CardGroup cols={2}>
 70 |   <Card title="Example Servers" icon="grid" href="/examples">
 71 |     Check out our gallery of official MCP servers and implementations
 72 |   </Card>
 73 | 
 74 |   <Card title="Example Clients" icon="cubes" href="/clients">
 75 |     View the list of clients that support MCP integrations
 76 |   </Card>
 77 | </CardGroup>
 78 | 
 79 | ## Tutorials
 80 | 
 81 | <CardGroup cols={2}>
 82 |   <Card title="Building MCP with LLMs" icon="comments" href="/tutorials/building-mcp-with-llms">
 83 |     Learn how to use LLMs like Claude to speed up your MCP development
 84 |   </Card>
 85 | 
 86 |   <Card title="Debugging Guide" icon="bug" href="/docs/tools/debugging">
 87 |     Learn how to effectively debug MCP servers and integrations
 88 |   </Card>
 89 | 
 90 |   <Card title="MCP Inspector" icon="magnifying-glass" href="/docs/tools/inspector">
 91 |     Test and inspect your MCP servers with our interactive debugging tool
 92 |   </Card>
 93 | 
 94 |   <Card title="MCP Workshop (Video, 2hr)" icon="person-chalkboard" href="https://www.youtube.com/watch?v=kQmXtrmQ5Zg">
 95 |     <iframe src="https://www.youtube.com/embed/kQmXtrmQ5Zg" />
 96 |   </Card>
 97 | </CardGroup>
 98 | 
 99 | ## Explore MCP
100 | 
101 | Dive deeper into MCP's core concepts and capabilities:
102 | 
103 | <CardGroup cols={2}>
104 |   <Card title="Core architecture" icon="sitemap" href="/docs/concepts/architecture">
105 |     Understand how MCP connects clients, servers, and LLMs
106 |   </Card>
107 | 
108 |   <Card title="Resources" icon="database" href="/docs/concepts/resources">
109 |     Expose data and content from your servers to LLMs
110 |   </Card>
111 | 
112 |   <Card title="Prompts" icon="message" href="/docs/concepts/prompts">
113 |     Create reusable prompt templates and workflows
114 |   </Card>
115 | 
116 |   <Card title="Tools" icon="wrench" href="/docs/concepts/tools">
117 |     Enable LLMs to perform actions through your server
118 |   </Card>
119 | 
120 |   <Card title="Sampling" icon="robot" href="/docs/concepts/sampling">
121 |     Let your servers request completions from LLMs
122 |   </Card>
123 | 
124 |   <Card title="Transports" icon="network-wired" href="/docs/concepts/transports">
125 |     Learn about MCP's communication mechanism
126 |   </Card>
127 | </CardGroup>
128 | 
129 | ## Contributing
130 | 
131 | Want to contribute? Check out our [Contributing Guide](/development/contributing) to learn how you can help improve MCP.
132 | 
133 | ## Support and Feedback
134 | 
135 | Here's how to get help or provide feedback:
136 | 
137 | * For bug reports and feature requests related to the MCP specification, SDKs, or documentation (open source), please [create a GitHub issue](https://github.com/modelcontextprotocol)
138 | * For discussions or Q\&A about the MCP specification, use the [specification discussions](https://github.com/modelcontextprotocol/specification/discussions)
139 | * For discussions or Q\&A about other MCP open source components, use the [organization discussions](https://github.com/orgs/modelcontextprotocol/discussions)
140 | * For bug reports, feature requests, and questions related to Claude.app and claude.ai's MCP integration, please see Anthropic's guide on [How to Get Support](https://support.anthropic.com/en/articles/9015913-how-to-get-support)
141 | 
```

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

```typescript
  1 | import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
  2 | import { Logger } from '../../utils/logger.js';
  3 | import { getConfluenceSpacesV2, getConfluenceSpaceV2, getConfluencePagesWithFilters } from '../../utils/confluence-resource-api.js';
  4 | import { spacesListSchema, spaceSchema, pagesListSchema } from '../../schemas/confluence.js';
  5 | import { Config, Resources } from '../../utils/mcp-helpers.js';
  6 | 
  7 | const logger = Logger.getLogger('ConfluenceResource:Spaces');
  8 | 
  9 | /**
 10 |  * Register Confluence space-related resources
 11 |  * @param server MCP Server instance
 12 |  */
 13 | export function registerSpaceResources(server: McpServer) {
 14 |   logger.info('Registering Confluence space resources...');
 15 | 
 16 |   // Resource: List of spaces (API v2, cursor-based)
 17 |   server.resource(
 18 |     'confluence-spaces-list',
 19 |     new ResourceTemplate('confluence://spaces', {
 20 |       list: async (_extra) => {
 21 |         return {
 22 |           resources: [
 23 |             {
 24 |               uri: 'confluence://spaces',
 25 |               name: 'Confluence Spaces',
 26 |               description: 'List and search all Confluence spaces',
 27 |               mimeType: 'application/json'
 28 |             }
 29 |           ]
 30 |         };
 31 |       }
 32 |     }),
 33 |     async (uri, params, _extra) => {
 34 |       const config = Config.getAtlassianConfigFromEnv();
 35 |       const limit = params?.limit ? parseInt(Array.isArray(params.limit) ? params.limit[0] : params.limit, 10) : 25;
 36 |       const cursor = params?.cursor ? (Array.isArray(params.cursor) ? params.cursor[0] : params.cursor) : undefined;
 37 |       logger.info(`Getting Confluence spaces list (v2): cursor=${cursor}, limit=${limit}`);
 38 |       const data = await getConfluenceSpacesV2(config, cursor, limit);
 39 |       const uriString = typeof uri === 'string' ? uri : uri.href;
 40 |       // Chuẩn hóa metadata cho cursor-based
 41 |       const total = data.size ?? (data.results?.length || 0);
 42 |       const hasMore = !!(data._links && data._links.next);
 43 |       const nextCursor = hasMore ? (new URL(data._links.next, 'http://dummy').searchParams.get('cursor') || '') : undefined;
 44 |       const metadata = {
 45 |         total,
 46 |         limit,
 47 |         hasMore,
 48 |         links: {
 49 |           self: uriString,
 50 |           next: hasMore && nextCursor ? `${uriString}?cursor=${encodeURIComponent(nextCursor)}&limit=${limit}` : undefined
 51 |         }
 52 |       };
 53 |       // Chuẩn hóa trả về
 54 |       return Resources.createStandardResource(
 55 |         uriString,
 56 |         data.results,
 57 |         'spaces',
 58 |         spacesListSchema,
 59 |         total,
 60 |         limit,
 61 |         0,
 62 |         undefined
 63 |       );
 64 |     }
 65 |   );
 66 | 
 67 |   // Resource: Space details (API v2, mapping id)
 68 |   server.resource(
 69 |     'confluence-space-details',
 70 |     new ResourceTemplate('confluence://spaces/{spaceId}', {
 71 |       list: async (_extra) => ({
 72 |         resources: [
 73 |           {
 74 |             uri: 'confluence://spaces/{spaceId}',
 75 |             name: 'Confluence Space Details',
 76 |             description: 'Get details for a specific Confluence space by id. Replace {spaceId} với id số của space (ví dụ: 19464200).',
 77 |             mimeType: 'application/json'
 78 |           }
 79 |         ]
 80 |       })
 81 |     }),
 82 |     async (uri, params, _extra) => {
 83 |       const config = Config.getAtlassianConfigFromEnv();
 84 |       let normalizedSpaceId = Array.isArray(params.spaceId) ? params.spaceId[0] : params.spaceId;
 85 |       if (!normalizedSpaceId) throw new Error('Missing spaceId in URI');
 86 |       if (!/^\d+$/.test(normalizedSpaceId)) throw new Error('spaceId must be a number');
 87 |       logger.info(`Getting details for Confluence space (v2) by id: ${normalizedSpaceId}`);
 88 |       // Lấy thông tin space qua API helper (giả sử getConfluenceSpaceV2 hỗ trợ lookup theo id)
 89 |       const space = await getConfluenceSpaceV2(config, normalizedSpaceId);
 90 |       const uriString = typeof uri === 'string' ? uri : uri.href;
 91 |       return Resources.createStandardResource(
 92 |         uriString,
 93 |         [space],
 94 |         'space',
 95 |         spaceSchema,
 96 |         1,
 97 |         1,
 98 |         0,
 99 |         undefined
100 |       );
101 |     }
102 |   );
103 | 
104 |   // Resource: List of pages in a space
105 |   server.resource(
106 |     'confluence-space-pages',
107 |     new ResourceTemplate('confluence://spaces/{spaceId}/pages', {
108 |       list: async (_extra) => ({
109 |         resources: [
110 |           {
111 |             uri: 'confluence://spaces/{spaceId}/pages',
112 |             name: 'Confluence Space Pages',
113 |             description: 'List all pages in a specific Confluence space. Replace {spaceId} với id số của space.',
114 |             mimeType: 'application/json'
115 |           }
116 |         ]
117 |       })
118 |     }),
119 |     async (uri, params, _extra) => {
120 |       const config = Config.getAtlassianConfigFromEnv();
121 |       let normalizedSpaceId = Array.isArray(params.spaceId) ? params.spaceId[0] : params.spaceId;
122 |       if (!normalizedSpaceId) throw new Error('Missing spaceId in URI');
123 |       if (!/^\d+$/.test(normalizedSpaceId)) throw new Error('spaceId must be a number');
124 |       // Không lookup theo key nữa, dùng trực tiếp id
125 |       const filterParams = {
126 |         'space-id': normalizedSpaceId,
127 |         limit: params.limit ? parseInt(Array.isArray(params.limit) ? params.limit[0] : params.limit, 10) : 25
128 |       };
129 |       const data = await getConfluencePagesWithFilters(config, filterParams);
130 |       const formattedPages = (data.results || []).map((page: any) => ({
131 |         id: page.id,
132 |         title: page.title,
133 |         status: page.status,
134 |         url: `${config.baseUrl}/wiki/pages/${page.id}`
135 |       }));
136 |       const uriString = typeof uri === 'string' ? uri : uri.href;
137 |       return Resources.createStandardResource(
138 |         uriString,
139 |         formattedPages,
140 |         'pages',
141 |         pagesListSchema,
142 |         data.size || formattedPages.length,
143 |         filterParams.limit,
144 |         0,
145 |         undefined
146 |       );
147 |     }
148 |   );
149 | }
150 | 
```

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

```typescript
  1 | /**
  2 |  * Jira Sprint Resources
  3 |  * 
  4 |  * These resources provide access to Jira sprints through MCP.
  5 |  */
  6 | 
  7 | import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
  8 | import { sprintListSchema, sprintSchema, issuesListSchema } from '../../schemas/jira.js';
  9 | import { getSprintsByBoard, getSprintById, getSprintIssues } from '../../utils/jira-resource-api.js';
 10 | import { Logger } from '../../utils/logger.js';
 11 | import { Config, Resources } from '../../utils/mcp-helpers.js';
 12 | 
 13 | const logger = Logger.getLogger('JiraSprintResources');
 14 | 
 15 | /**
 16 |  * Register all Jira sprint resources with MCP Server
 17 |  * @param server MCP Server instance
 18 |  */
 19 | export function registerSprintResources(server: McpServer) {
 20 |   logger.info('Registering Jira sprint resources...');
 21 | 
 22 |   // Resource: Board sprints
 23 |   server.resource(
 24 |     'jira-board-sprints',
 25 |     new ResourceTemplate('jira://boards/{boardId}/sprints', { 
 26 |       list: async (_extra) => ({
 27 |         resources: [
 28 |           {
 29 |             uri: 'jira://boards/{boardId}/sprints',
 30 |             name: 'Jira Board Sprints',
 31 |             description: 'List all sprints in a Jira board. Replace {boardId} with the board ID.',
 32 |             mimeType: 'application/json'
 33 |           }
 34 |         ]
 35 |       }) 
 36 |     }),
 37 |     async (uri, params, _extra) => {
 38 |       try {
 39 |         const config = Config.getAtlassianConfigFromEnv();
 40 |         const boardId = Array.isArray(params.boardId) ? params.boardId[0] : params.boardId;
 41 |         const { limit, offset } = Resources.extractPagingParams(params);
 42 |         const response = await getSprintsByBoard(config, boardId, offset, limit);
 43 |         
 44 |         const uriString = typeof uri === 'string' ? uri : uri.href;
 45 |         return Resources.createStandardResource(
 46 |           uriString,
 47 |           response.values,
 48 |           'sprints',
 49 |           sprintListSchema,
 50 |           response.total || response.values.length,
 51 |           limit,
 52 |           offset,
 53 |           `${config.baseUrl}/jira/software/projects/browse/boards/${boardId}`
 54 |         );
 55 |       } catch (error) {
 56 |         logger.error(`Error getting sprints for board ${params.boardId}:`, error);
 57 |         throw error;
 58 |       }
 59 |     }
 60 |   );
 61 | 
 62 |   // Resource: Sprint details
 63 |   server.resource(
 64 |     'jira-sprint-details',
 65 |     new ResourceTemplate('jira://sprints/{sprintId}', {
 66 |       list: async (_extra) => ({
 67 |         resources: [
 68 |           {
 69 |             uri: 'jira://sprints/{sprintId}',
 70 |             name: 'Jira Sprint Details',
 71 |             description: 'Get details for a specific Jira sprint by ID. Replace {sprintId} with the sprint ID.',
 72 |             mimeType: 'application/json'
 73 |           }
 74 |         ]
 75 |       })
 76 |     }),
 77 |     async (uri, params, _extra) => {
 78 |       try {
 79 |         const config = Config.getAtlassianConfigFromEnv();
 80 |         const sprintId = Array.isArray(params.sprintId) ? params.sprintId[0] : params.sprintId;
 81 |         const sprint = await getSprintById(config, sprintId);
 82 |         
 83 |         const uriString = typeof uri === 'string' ? uri : uri.href;
 84 |         return Resources.createStandardResource(
 85 |           uriString,
 86 |           [sprint],
 87 |           'sprint',
 88 |           sprintSchema,
 89 |           1,
 90 |           1,
 91 |           0,
 92 |           `${config.baseUrl}/jira/software/projects/browse/boards/${sprint.originBoardId}/sprint/${sprintId}`
 93 |         );
 94 |       } catch (error) {
 95 |         logger.error(`Error getting sprint details for sprint ${params.sprintId}:`, error);
 96 |         throw error;
 97 |       }
 98 |     }
 99 |   );
100 | 
101 |   // Resource: Sprint issues
102 |   server.resource(
103 |     'jira-sprint-issues',
104 |     new ResourceTemplate('jira://sprints/{sprintId}/issues', {
105 |       list: async (_extra) => ({
106 |         resources: [
107 |           {
108 |             uri: 'jira://sprints/{sprintId}/issues',
109 |             name: 'Jira Sprint Issues',
110 |             description: 'List issues in a Jira sprint. Replace {sprintId} with the sprint ID.',
111 |             mimeType: 'application/json'
112 |           }
113 |         ]
114 |       })
115 |     }),
116 |     async (uri, params, _extra) => {
117 |       try {
118 |         const config = Config.getAtlassianConfigFromEnv();
119 |         const sprintId = Array.isArray(params.sprintId) ? params.sprintId[0] : params.sprintId;
120 |         const { limit, offset } = Resources.extractPagingParams(params);
121 |         const response = await getSprintIssues(config, sprintId, offset, limit);
122 |         
123 |         const uriString = typeof uri === 'string' ? uri : uri.href;
124 |         return Resources.createStandardResource(
125 |           uriString,
126 |           response.issues,
127 |           'issues',
128 |           issuesListSchema,
129 |           response.total || response.issues.length,
130 |           limit,
131 |           offset,
132 |           `${config.baseUrl}/jira/software/projects/browse/issues/sprint/${sprintId}`
133 |         );
134 |       } catch (error) {
135 |         logger.error(`Error getting issues for sprint ${params.sprintId}:`, error);
136 |         throw error;
137 |       }
138 |     }
139 |   );
140 |   
141 |   // Resource: All sprints
142 |   server.resource(
143 |     'jira-sprints',
144 |     new ResourceTemplate('jira://sprints', {
145 |       list: async (_extra) => ({
146 |         resources: [
147 |           {
148 |             uri: 'jira://sprints',
149 |             name: 'Jira Sprints',
150 |             description: 'List and search all Jira sprints',
151 |             mimeType: 'application/json'
152 |           }
153 |         ]
154 |       })
155 |     }),
156 |     async (uri, _params, _extra) => {
157 |       const uriString = typeof uri === 'string' ? uri : uri.href;
158 |       return {
159 |         contents: [{
160 |           uri: uriString,
161 |           mimeType: 'application/json',
162 |           text: JSON.stringify({
163 |             message: "Please use specific board sprints URI: jira://boards/{boardId}/sprints",
164 |             suggestion: "To view sprints, first select a board using jira://boards, then access the board's sprints with jira://boards/{boardId}/sprints"
165 |           })
166 |         }]
167 |       };
168 |     }
169 |   );
170 | 
171 |   logger.info('Jira sprint resources registered successfully');
172 | }
```

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

```markdown
  1 | # Mini-Plan Để Refactoring Nhóm Tools
  2 | 
  3 | 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.
  4 | 
  5 | ## Giai Đoạn 1: Chuẩn Bị và Phân Tích
  6 | 
  7 | 1. **Kiểm tra và phân loại tools hiện tại**
  8 |    - Phân loại tools theo chức năng (Jira/Confluence)
  9 |    - 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)
 10 |    - Lập danh sách tools cần refactor
 11 | 
 12 | 2. **Thiết lập unit tests**
 13 |    - Viết tests cho các tools hiện tại trước khi refactor
 14 |    - Đảm bảo coverage cho các use cases quan trọng
 15 | 
 16 | ## Giai Đoạn 2: Thiết Kế Cấu Trúc Mới
 17 | 
 18 | 3. **Chuẩn hóa cách đặt tên và tổ chức**
 19 |    ```
 20 |    /src
 21 |      /tools
 22 |        /jira
 23 |          /issue
 24 |            index.ts          # Tổng hợp đăng ký
 25 |            create.ts         # createIssue
 26 |            transition.ts     # transitionIssue
 27 |            assign.ts         # assignIssue
 28 |          /comment
 29 |            index.ts
 30 |            add.ts            # addComment
 31 |        /confluence
 32 |          /page
 33 |            index.ts
 34 |            create.ts         # createPage
 35 |            update.ts         # updatePage
 36 |        index.ts              # Đăng ký tập trung tất cả tools
 37 |      /utils
 38 |        tool-helpers.ts       # Các utility functions
 39 |    ```
 40 | 
 41 | 4. **Tạo các helper functions chuẩn hóa**
 42 |    ```typescript
 43 |    // src/utils/tool-helpers.ts
 44 |    import { z } from 'zod';
 45 |    import { Logger } from './logger.js';
 46 | 
 47 |    const logger = Logger.getLogger('MCPTool');
 48 | 
 49 |    export function createToolResponse(text: string) {
 50 |      return {
 51 |        content: [{ type: 'text', text }]
 52 |      };
 53 |    }
 54 | 
 55 |    export function createErrorResponse(error: Error | string) {
 56 |      const message = error instanceof Error ? error.message : error;
 57 |      return {
 58 |        content: [{ type: 'text', text: `Error: ${message}` }],
 59 |        isError: true
 60 |      };
 61 |    }
 62 | 
 63 |    export function registerTool(server, name, description, schema, handler) {
 64 |      logger.info(`Registering tool: ${name}`);
 65 |      server.tool(name, schema, async (params, context) => {
 66 |        try {
 67 |          logger.debug(`Executing tool ${name} with params:`, params);
 68 |          const result = await handler(params, context);
 69 |          logger.debug(`Tool ${name} executed successfully`);
 70 |          return result;
 71 |        } catch (error) {
 72 |          logger.error(`Error in tool ${name}:`, error);
 73 |          return createErrorResponse(error);
 74 |        }
 75 |      });
 76 |    }
 77 |    ```
 78 | 
 79 | ## Giai Đoạn 3: Triển Khai Refactoring
 80 | 
 81 | 5. **Thực hiện refactoring theo từng nhóm nhỏ**
 82 |    - Bắt đầu với một nhóm tools (ví dụ: Jira issue tools)
 83 |    - Refactor từng tool một, chạy tests sau mỗi lần thay đổi
 84 | 
 85 | 6. **Mẫu triển khai cho một tool cụ thể**
 86 |    ```typescript
 87 |    // src/tools/jira/issue/create.ts
 88 |    import { z } from 'zod';
 89 |    import { createToolResponse, createErrorResponse } from '../../../utils/tool-helpers.js';
 90 | 
 91 |    export const createIssueSchema = z.object({
 92 |      projectKey: z.string().describe('Project key (e.g., PROJ)'),
 93 |      summary: z.string().describe('Issue title/summary'),
 94 |      description: z.string().optional().describe('Detailed description'),
 95 |      issueType: z.string().default('Task').describe('Type of issue')
 96 |    });
 97 | 
 98 |    export async function createIssueHandler(params, context) {
 99 |      try {
100 |        const config = context.get('atlassianConfig');
101 |        if (!config) {
102 |          throw new Error('Atlassian configuration not found');
103 |        }
104 | 
105 |        // Logic tạo issue...
106 |        
107 |        return createToolResponse(`Issue ${newIssue.key} created successfully`);
108 |      } catch (error) {
109 |        return createErrorResponse(error);
110 |      }
111 |    }
112 | 
113 |    export function registerCreateIssueTool(server) {
114 |      server.tool(
115 |        'createIssue',
116 |        createIssueSchema,
117 |        createIssueHandler
118 |      );
119 |    }
120 |    ```
121 | 
122 | 7. **Tạo index.ts để đăng ký tập trung**
123 |    ```typescript
124 |    // src/tools/jira/issue/index.ts
125 |    import { registerCreateIssueTool } from './create.js';
126 |    import { registerTransitionIssueTool } from './transition.js';
127 |    import { registerAssignIssueTool } from './assign.js';
128 | 
129 |    export function registerJiraIssueTools(server) {
130 |      registerCreateIssueTool(server);
131 |      registerTransitionIssueTool(server);
132 |      registerAssignIssueTool(server);
133 |    }
134 |    ```
135 | 
136 |    ```typescript
137 |    // src/tools/index.ts
138 |    import { registerJiraIssueTools } from './jira/issue/index.js';
139 |    import { registerJiraCommentTools } from './jira/comment/index.js';
140 |    import { registerConfluencePageTools } from './confluence/page/index.js';
141 | 
142 |    export function registerAllTools(server) {
143 |      registerJiraIssueTools(server);
144 |      registerJiraCommentTools(server);
145 |      registerConfluencePageTools(server);
146 |    }
147 |    ```
148 | 
149 | ## Giai Đoạn 4: Đảm Bảo Chất Lượng và Hoàn Thiện
150 | 
151 | 8. **Kiểm tra và tài liệu hóa**
152 |    - Chạy tất cả unit tests
153 |    - Cập nhật tài liệu phát triển
154 |    - Thêm ví dụ cho từng tool
155 | 
156 | 9. **Đồng bộ hóa với cấu trúc Resource**
157 |    - Đảm bảo sự nhất quán giữa naming và pattern giữa Tools và Resources
158 |    - Xác minh không có trùng lặp chức năng giữa hai nhóm
159 | 
160 | 10. **Đánh giá và optimizing**
161 |     - Xác định các patterns chung và cơ hội trừu tượng hóa
162 |     - Kiểm tra hiệu suất nếu cần
163 | 
164 | ## Best Practices Khi Refactoring Tools
165 | 
166 | - **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")
167 | - **Input validation**: Sử dụng Zod schemas chi tiết và rõ ràng cho tất cả tham số
168 | - **Error handling**: Xử lý lỗi nhất quán trong tất cả các tools
169 | - **Logging**: Log đầy đủ thông tin hoạt động của tools cho debug
170 | - **Documentation**: Tài liệu hóa rõ ràng mục đích, tham số và kết quả mong đợi
171 | - **Testing**: Unit tests đầy đủ cho mỗi tool
172 | 
173 | 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
  1 | import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
  2 | import { getDashboards, getMyDashboards, getDashboardById, getDashboardGadgets } from '../../utils/jira-resource-api.js';
  3 | import { Logger } from '../../utils/logger.js';
  4 | import { dashboardSchema, dashboardListSchema, gadgetListSchema } from '../../schemas/jira.js';
  5 | import { Config, Resources } from '../../utils/mcp-helpers.js';
  6 | 
  7 | const logger = Logger.getLogger('JiraDashboardResources');
  8 | 
  9 | // (Có thể bổ sung schema dashboardSchema, gadgetsSchema nếu cần)
 10 | 
 11 | export function registerDashboardResources(server: McpServer) {
 12 |   logger.info('Registering Jira dashboard resources...');
 13 | 
 14 |   // List all dashboards
 15 |   server.resource(
 16 |     'jira-dashboards',
 17 |     new ResourceTemplate('jira://dashboards', {
 18 |       list: async (_extra) => {
 19 |         return {
 20 |           resources: [
 21 |             {
 22 |               uri: 'jira://dashboards',
 23 |               name: 'Jira Dashboards',
 24 |               description: 'List and search all Jira dashboards',
 25 |               mimeType: 'application/json'
 26 |             }
 27 |           ]
 28 |         };
 29 |       }
 30 |     }),
 31 |     async (uri: string | URL, params: Record<string, any>, extra: any) => {
 32 |       try {
 33 |         // Get config from context or environment
 34 |         const config = Config.getAtlassianConfigFromEnv();
 35 |         const uriStr = typeof uri === 'string' ? uri : uri.href;
 36 |         
 37 |         const { limit, offset } = Resources.extractPagingParams(params);
 38 |         const data = await getDashboards(config, offset, limit);
 39 |         return Resources.createStandardResource(
 40 |           uriStr,
 41 |           data.dashboards || [],
 42 |           'dashboards',
 43 |           dashboardListSchema,
 44 |           data.total || (data.dashboards ? data.dashboards.length : 0),
 45 |           limit,
 46 |           offset,
 47 |           `${config.baseUrl}/jira/dashboards` // UI URL
 48 |         );
 49 |       } catch (error) {
 50 |         logger.error(`Error handling resource request for jira-dashboards:`, error);
 51 |         throw error;
 52 |       }
 53 |     }
 54 |   );
 55 | 
 56 |   // List my dashboards
 57 |   server.resource(
 58 |     'jira-my-dashboards',
 59 |     new ResourceTemplate('jira://dashboards/my', {
 60 |       list: async (_extra) => ({
 61 |         resources: [
 62 |           {
 63 |             uri: 'jira://dashboards/my',
 64 |             name: 'Jira My Dashboards',
 65 |             description: 'List dashboards owned by or shared with the current user.',
 66 |             mimeType: 'application/json'
 67 |           }
 68 |         ]
 69 |       })
 70 |     }),
 71 |     async (uri: string | URL, params: Record<string, any>, extra: any) => {
 72 |       try {
 73 |         // Get config from context or environment
 74 |         const config = Config.getAtlassianConfigFromEnv();
 75 |         const uriStr = typeof uri === 'string' ? uri : uri.href;
 76 |         
 77 |         const { limit, offset } = Resources.extractPagingParams(params);
 78 |         const data = await getMyDashboards(config, offset, limit);
 79 |         return Resources.createStandardResource(
 80 |           uriStr,
 81 |           data.dashboards || [],
 82 |           'dashboards',
 83 |           dashboardListSchema,
 84 |           data.total || (data.dashboards ? data.dashboards.length : 0),
 85 |           limit,
 86 |           offset,
 87 |           `${config.baseUrl}/jira/dashboards?filter=my`
 88 |         );
 89 |       } catch (error) {
 90 |         logger.error(`Error handling resource request for jira-my-dashboards:`, error);
 91 |         throw error;
 92 |       }
 93 |     }
 94 |   );
 95 | 
 96 |   // Dashboard details
 97 |   server.resource(
 98 |     'jira-dashboard-details',
 99 |     new ResourceTemplate('jira://dashboards/{dashboardId}', {
100 |       list: async (_extra) => ({
101 |         resources: [
102 |           {
103 |             uri: 'jira://dashboards/{dashboardId}',
104 |             name: 'Jira Dashboard Details',
105 |             description: 'Get details of a specific Jira dashboard.',
106 |             mimeType: 'application/json'
107 |           }
108 |         ]
109 |       })
110 |     }),
111 |     async (uri: string | URL, params: Record<string, any>, extra: any) => {
112 |       try {
113 |         // Get config from context or environment
114 |         const config = Config.getAtlassianConfigFromEnv();
115 |         const uriStr = typeof uri === 'string' ? uri : uri.href;
116 |         
117 |         const dashboardId = params.dashboardId || (uriStr.split('/').pop());
118 |         const dashboard = await getDashboardById(config, dashboardId);
119 |         return Resources.createStandardResource(
120 |           uriStr,
121 |           [dashboard],
122 |           'dashboard',
123 |           dashboardSchema,
124 |           1,
125 |           1,
126 |           0,
127 |           `${config.baseUrl}/jira/dashboards/${dashboardId}`
128 |         );
129 |       } catch (error) {
130 |         logger.error(`Error handling resource request for jira-dashboard-details:`, error);
131 |         throw error;
132 |       }
133 |     }
134 |   );
135 | 
136 |   // Dashboard gadgets
137 |   server.resource(
138 |     'jira-dashboard-gadgets',
139 |     new ResourceTemplate('jira://dashboards/{dashboardId}/gadgets', {
140 |       list: async (_extra) => ({
141 |         resources: [
142 |           {
143 |             uri: 'jira://dashboards/{dashboardId}/gadgets',
144 |             name: 'Jira Dashboard Gadgets',
145 |             description: 'List gadgets of a specific Jira dashboard.',
146 |             mimeType: 'application/json'
147 |           }
148 |         ]
149 |       })
150 |     }),
151 |     async (uri: string | URL, params: Record<string, any>, extra: any) => {
152 |       try {
153 |         // Get config from context or environment
154 |         const config = Config.getAtlassianConfigFromEnv();
155 |         const uriStr = typeof uri === 'string' ? uri : uri.href;
156 |         
157 |         const dashboardId = params.dashboardId || (uriStr.split('/')[uriStr.split('/').length - 2]);
158 |         const gadgets = await getDashboardGadgets(config, dashboardId);
159 |         return Resources.createStandardResource(
160 |           uriStr,
161 |           gadgets,
162 |           'gadgets',
163 |           gadgetListSchema,
164 |           gadgets.length,
165 |           gadgets.length,
166 |           0,
167 |           `${config.baseUrl}/jira/dashboards/${dashboardId}`
168 |         );
169 |       } catch (error) {
170 |         logger.error(`Error handling resource request for jira-dashboard-gadgets:`, error);
171 |         throw error;
172 |       }
173 |     }
174 |   );
175 | 
176 |   logger.info('Jira dashboard resources registered successfully');
177 | }
```

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

```typescript
  1 | import { z } from 'zod';
  2 | import { callConfluenceApi } from '../../utils/atlassian-api-base.js';
  3 | import { AtlassianConfig } from '../../utils/atlassian-api-base.js';
  4 | import { ApiError, ApiErrorType } from '../../utils/error-handler.js';
  5 | import { Logger } from '../../utils/logger.js';
  6 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
  7 | import { McpResponse, createSuccessResponse, createErrorResponse } from '../../utils/mcp-core.js';
  8 | import { updateConfluencePageV2 } from '../../utils/confluence-tool-api.js';
  9 | import { Config } from '../../utils/mcp-helpers.js';
 10 | 
 11 | // Initialize logger
 12 | const logger = Logger.getLogger('ConfluenceTools:updatePage');
 13 | 
 14 | // Input parameter schema
 15 | export const updatePageSchema = z.object({
 16 |   pageId: z.string().describe('ID of the page to update'),
 17 |   title: z.string().optional().describe('New title of the page'),
 18 |   content: z.string().optional().describe(`New content of the page (Confluence storage format only, XML-like HTML).
 19 | 
 20 | - Plain text or markdown is NOT supported (will throw error).
 21 | - 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.
 22 | - Content MUST strictly follow Confluence storage format.
 23 | 
 24 | Valid examples:
 25 | - <p>This is a paragraph</p>
 26 | - <ac:structured-macro ac:name="info"><ac:rich-text-body>Information</ac:rich-text-body></ac:structured-macro>
 27 | `),
 28 |   version: z.number().describe('Current version number of the page (required to avoid conflicts)')
 29 | });
 30 | 
 31 | type UpdatePageParams = z.infer<typeof updatePageSchema>;
 32 | 
 33 | interface UpdatePageResult {
 34 |   id: string;
 35 |   title: string;
 36 |   version: number;
 37 |   self: string;
 38 |   webui: string;
 39 |   success: boolean;
 40 |   message: string;
 41 | }
 42 | 
 43 | // Main handler to update a page (API v2)
 44 | export async function updatePageHandler(
 45 |   params: UpdatePageParams,
 46 |   config: AtlassianConfig
 47 | ): Promise<UpdatePageResult> {
 48 |   try {
 49 |     logger.info(`Updating page (v2) with ID: ${params.pageId}`);
 50 |     // Lấy version, title, content hiện tại nếu thiếu
 51 |     const baseUrl = config.baseUrl.endsWith('/wiki') ? config.baseUrl : `${config.baseUrl}/wiki`;
 52 |     const auth = Buffer.from(`${config.email}:${config.apiToken}`).toString('base64');
 53 |     const headers = {
 54 |       'Authorization': `Basic ${auth}`,
 55 |       'Content-Type': 'application/json',
 56 |       'Accept': 'application/json',
 57 |       'User-Agent': 'MCP-Atlassian-Server/1.0.0'
 58 |     };
 59 |     const url = `${baseUrl}/api/v2/pages/${encodeURIComponent(params.pageId)}`;
 60 |     const res = await fetch(url, { method: 'GET', headers, credentials: 'omit' });
 61 |     if (!res.ok) throw new Error(`Failed to get page info: ${params.pageId}`);
 62 |     const pageData = await res.json();
 63 |     let version = pageData.version.number + 1;
 64 |     let title = params.title ?? pageData.title;
 65 |     let content = params.content;
 66 |     if (!title) throw new Error('Missing title for page update');
 67 |     if (!content) {
 68 |       // Lấy body hiện tại nếu không truyền content
 69 |       const bodyRes = await fetch(`${url}/body`, { method: 'GET', headers, credentials: 'omit' });
 70 |       if (!bodyRes.ok) throw new Error(`Failed to get page body: ${params.pageId}`);
 71 |       const bodyData = await bodyRes.json();
 72 |       content = bodyData.value;
 73 |       if (!content) throw new Error('Missing content for page update');
 74 |     }
 75 |     // Gọi helper updateConfluencePageV2 với đủ trường
 76 |     const data = await updateConfluencePageV2(config, {
 77 |       pageId: params.pageId,
 78 |       title,
 79 |       content,
 80 |       version
 81 |     });
 82 |     return {
 83 |       id: data.id,
 84 |       title: data.title,
 85 |       version: data.version.number,
 86 |       self: data._links?.self || '',
 87 |       webui: data._links?.webui || '',
 88 |       success: true,
 89 |       message: 'Successfully updated page'
 90 |     };
 91 |   } catch (error) {
 92 |     if (error instanceof ApiError) {
 93 |       throw error;
 94 |     }
 95 |     logger.error(`Error updating page (v2) with ID ${params.pageId}:`, error);
 96 |     let message = `Failed to update page: ${error instanceof Error ? error.message : String(error)}`;
 97 |     throw new ApiError(
 98 |       ApiErrorType.SERVER_ERROR,
 99 |       message,
100 |       500
101 |     );
102 |   }
103 | }
104 | 
105 | // Register the tool with MCP Server
106 | export const registerUpdatePageTool = (server: McpServer) => {
107 |   server.tool(
108 |     'updatePage',
109 |     'Update the content and information of a Confluence page',
110 |     updatePageSchema.shape,
111 |     async (params: UpdatePageParams, context: Record<string, any>) => {
112 |       try {
113 |         const config = context?.atlassianConfig ?? Config.getAtlassianConfigFromEnv();
114 |         if (!config) {
115 |           return {
116 |             content: [
117 |               { type: 'text', text: 'Invalid or missing Atlassian configuration' }
118 |             ],
119 |             isError: true
120 |           };
121 |         }
122 |         const result = await updatePageHandler(params, config);
123 |         return {
124 |           content: [
125 |             {
126 |               type: 'text',
127 |               text: JSON.stringify({
128 |                 success: true,
129 |                 message: result.message,
130 |                 id: result.id,
131 |                 title: result.title,
132 |                 version: result.version,
133 |                 url: `${config.baseUrl}/wiki${result.webui}`
134 |               })
135 |             }
136 |           ]
137 |         };
138 |       } catch (error) {
139 |         if (error instanceof ApiError) {
140 |           return {
141 |             content: [
142 |               {
143 |                 type: 'text',
144 |                 text: JSON.stringify({
145 |                   success: false,
146 |                   message: error.message,
147 |                   code: error.code,
148 |                   statusCode: error.statusCode,
149 |                   type: error.type
150 |                 })
151 |               }
152 |             ],
153 |             isError: true
154 |           };
155 |         }
156 |         return {
157 |           content: [
158 |             {
159 |               type: 'text',
160 |               text: JSON.stringify({
161 |                 success: false,
162 |                 message: `Error while updating page: ${error instanceof Error ? error.message : String(error)}`
163 |               })
164 |             }
165 |           ],
166 |           isError: true
167 |         };
168 |       }
169 |     }
170 |   );
171 | }; 
```

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

```typescript
  1 | /**
  2 |  * Jira Board Resources
  3 |  * 
  4 |  * These resources provide access to Jira boards through MCP.
  5 |  */
  6 | 
  7 | import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
  8 | import { boardListSchema, boardSchema, issuesListSchema } from '../../schemas/jira.js';
  9 | import { getBoards, getBoardById, getBoardIssues } from '../../utils/jira-resource-api.js';
 10 | import { Logger } from '../../utils/logger.js';
 11 | import { Config, Resources } from '../../utils/mcp-helpers.js';
 12 | 
 13 | const logger = Logger.getLogger('JiraBoardResources');
 14 | 
 15 | /**
 16 |  * Register all Jira board resources with MCP Server
 17 |  * @param server MCP Server instance
 18 |  */
 19 | export function registerBoardResources(server: McpServer) {
 20 |   logger.info('Registering Jira board resources...');
 21 |   
 22 |   // Resource: Board list
 23 |   server.resource(
 24 |     'jira-boards',
 25 |     new ResourceTemplate('jira://boards', {
 26 |       list: async (_extra) => {
 27 |         return {
 28 |           resources: [
 29 |             {
 30 |               uri: 'jira://boards',
 31 |               name: 'Jira Boards',
 32 |               description: 'List and search all Jira boards',
 33 |               mimeType: 'application/json'
 34 |             }
 35 |           ]
 36 |         };
 37 |       }
 38 |     }),
 39 |     async (uri, params, _extra) => {
 40 |       try {
 41 |         const config = Config.getAtlassianConfigFromEnv();
 42 |         const { limit, offset } = Resources.extractPagingParams(params);
 43 |         const response = await getBoards(config, offset, limit);
 44 |         
 45 |         const uriString = typeof uri === 'string' ? uri : uri.href;
 46 |         return Resources.createStandardResource(
 47 |           uriString,
 48 |           response.values,
 49 |           'boards',
 50 |           boardListSchema,
 51 |           response.total || response.values.length,
 52 |           limit,
 53 |           offset,
 54 |           `${config.baseUrl}/jira/boards`
 55 |         );
 56 |       } catch (error) {
 57 |         logger.error('Error getting board list:', error);
 58 |         throw error;
 59 |       }
 60 |     }
 61 |   );
 62 | 
 63 |   // Resource: Board details
 64 |   server.resource(
 65 |     'jira-board-details',
 66 |     new ResourceTemplate('jira://boards/{boardId}', {
 67 |       list: async (_extra) => ({
 68 |         resources: [
 69 |           {
 70 |             uri: 'jira://boards/{boardId}',
 71 |             name: 'Jira Board Details',
 72 |             description: 'Get details for a specific Jira board by ID. Replace {boardId} with the board ID.',
 73 |             mimeType: 'application/json'
 74 |           }
 75 |         ]
 76 |       })
 77 |     }),
 78 |     async (uri, params, _extra) => {
 79 |       try {
 80 |         const config = Config.getAtlassianConfigFromEnv();
 81 |         const boardId = Array.isArray(params.boardId) ? params.boardId[0] : params.boardId;
 82 |         const board = await getBoardById(config, boardId);
 83 |         
 84 |         const uriString = typeof uri === 'string' ? uri : uri.href;
 85 |         return Resources.createStandardResource(
 86 |           uriString,
 87 |           [board],
 88 |           'board',
 89 |           boardSchema,
 90 |           1,
 91 |           1,
 92 |           0,
 93 |           `${config.baseUrl}/jira/software/projects/${board.location?.projectKey || 'browse'}/boards/${boardId}`
 94 |         );
 95 |       } catch (error) {
 96 |         logger.error(`Error getting board details for board ${params.boardId}:`, error);
 97 |         throw error;
 98 |       }
 99 |     }
100 |   );
101 | 
102 |   // Resource: Issues in board
103 |   server.resource(
104 |     'jira-board-issues',
105 |     new ResourceTemplate('jira://boards/{boardId}/issues', {
106 |       list: async (_extra) => ({
107 |         resources: [
108 |           {
109 |             uri: 'jira://boards/{boardId}/issues',
110 |             name: 'Jira Board Issues',
111 |             description: 'List issues in a Jira board. Replace {boardId} with the board ID.',
112 |             mimeType: 'application/json'
113 |           }
114 |         ]
115 |       })
116 |     }),
117 |     async (uri, params, _extra) => {
118 |       try {
119 |         const config = Config.getAtlassianConfigFromEnv();
120 |         const boardId = Array.isArray(params.boardId) ? params.boardId[0] : params.boardId;
121 |         const { limit, offset } = Resources.extractPagingParams(params);
122 |         const response = await getBoardIssues(config, boardId, offset, limit);
123 |         
124 |         const uriString = typeof uri === 'string' ? uri : uri.href;
125 |         return Resources.createStandardResource(
126 |           uriString,
127 |           response.issues,
128 |           'issues',
129 |           issuesListSchema,
130 |           response.total || response.issues.length,
131 |           limit,
132 |           offset,
133 |           `${config.baseUrl}/jira/software/projects/browse/boards/${boardId}`
134 |         );
135 |       } catch (error) {
136 |         logger.error(`Error getting issues for board ${params.boardId}:`, error);
137 |         throw error;
138 |       }
139 |     }
140 |   );
141 | 
142 |   // Resource: Board configuration
143 |   server.resource(
144 |     'jira-board-configuration',
145 |     new ResourceTemplate('jira://boards/{boardId}/configuration', {
146 |       list: async (_extra) => ({
147 |         resources: [
148 |           {
149 |             uri: 'jira://boards/{boardId}/configuration',
150 |             name: 'Jira Board Configuration',
151 |             description: 'Get configuration of a specific Jira board. Replace {boardId} with the board ID.',
152 |             mimeType: 'application/json'
153 |           }
154 |         ]
155 |       })
156 |     }),
157 |     async (uri, params, _extra) => {
158 |       try {
159 |         const config = Config.getAtlassianConfigFromEnv();
160 |         const boardId = Array.isArray(params.boardId) ? params.boardId[0] : params.boardId;
161 |         // Gọi API lấy cấu hình board
162 |         const response = await fetch(`${config.baseUrl}/rest/agile/1.0/board/${boardId}/configuration`, {
163 |           method: 'GET',
164 |           headers: {
165 |             'Authorization': `Basic ${Buffer.from(`${config.email}:${config.apiToken}`).toString('base64')}`,
166 |             'Accept': 'application/json',
167 |             'Content-Type': 'application/json',
168 |           },
169 |         });
170 |         if (!response.ok) throw new Error(`Jira API error: ${response.status} ${await response.text()}`);
171 |         const configData = await response.json();
172 |         
173 |         const uriString = typeof uri === 'string' ? uri : uri.href;
174 |         // Inline schema (mô tả cơ bản, không validate sâu)
175 |         const boardConfigurationSchema = {
176 |           type: 'object',
177 |           properties: {
178 |             id: { type: 'number' },
179 |             name: { type: 'string' },
180 |             type: { type: 'string' },
181 |             self: { type: 'string' },
182 |             location: { type: 'object' },
183 |             filter: { type: 'object' },
184 |             subQuery: { type: 'object' },
185 |             columnConfig: { type: 'object' },
186 |             estimation: { type: 'object' },
187 |             ranking: { type: 'object' }
188 |           },
189 |           required: ['id', 'name', 'type', 'self', 'columnConfig']
190 |         };
191 |         return {
192 |           contents: [{
193 |             uri: uriString,
194 |             mimeType: 'application/json',
195 |             text: JSON.stringify(configData),
196 |             schema: boardConfigurationSchema
197 |           }]
198 |         };
199 |       } catch (error) {
200 |         logger.error(`Error getting board configuration for board ${params.boardId}:`, error);
201 |         throw error;
202 |       }
203 |     }
204 |   );
205 |   
206 |   logger.info('Jira board resources registered successfully');
207 | }
```

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

```typescript
  1 | import dotenv from 'dotenv';
  2 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
  3 | import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
  4 | import { registerAllTools } from './tools/index.js';
  5 | import { registerAllResources } from './resources/index.js';
  6 | import { Logger } from './utils/logger.js';
  7 | import { AtlassianConfig } from './utils/atlassian-api-base.js';
  8 | 
  9 | // Load environment variables
 10 | dotenv.config();
 11 | 
 12 | // Initialize logger
 13 | const logger = Logger.getLogger('MCP:Server');
 14 | 
 15 | // Get Atlassian config from environment variables
 16 | const ATLASSIAN_SITE_NAME = process.env.ATLASSIAN_SITE_NAME;
 17 | const ATLASSIAN_USER_EMAIL = process.env.ATLASSIAN_USER_EMAIL;
 18 | const ATLASSIAN_API_TOKEN = process.env.ATLASSIAN_API_TOKEN;
 19 | 
 20 | if (!ATLASSIAN_SITE_NAME || !ATLASSIAN_USER_EMAIL || !ATLASSIAN_API_TOKEN) {
 21 |   logger.error('Missing Atlassian credentials in environment variables');
 22 |   process.exit(1);
 23 | }
 24 | 
 25 | // Create Atlassian config
 26 | const atlassianConfig: AtlassianConfig = {
 27 |   baseUrl: ATLASSIAN_SITE_NAME.includes('.atlassian.net') 
 28 |     ? `https://${ATLASSIAN_SITE_NAME}` 
 29 |     : ATLASSIAN_SITE_NAME,
 30 |   email: ATLASSIAN_USER_EMAIL,
 31 |   apiToken: ATLASSIAN_API_TOKEN
 32 | };
 33 | 
 34 | logger.info('Initializing MCP Atlassian Server...');
 35 | 
 36 | // Track registered resources for logging
 37 | const registeredResources: Array<{ name: string; pattern: string }> = [];
 38 | 
 39 | // Initialize MCP server with capabilities
 40 | const server = new McpServer({
 41 |   name: process.env.MCP_SERVER_NAME || 'phuc-nt/mcp-atlassian-server',
 42 |   version: process.env.MCP_SERVER_VERSION || '1.0.0',
 43 |   capabilities: {
 44 |     resources: {},  // Declare support for resources capability
 45 |     tools: {}
 46 |   }
 47 | });
 48 | 
 49 | // Create a context-aware server proxy for resources
 50 | const serverProxy = new Proxy(server, {
 51 |   get(target, prop) {
 52 |     if (prop === 'resource') {
 53 |       // Override the resource method to inject context
 54 |       return (name: string, pattern: any, handler: any) => {
 55 |         try {
 56 |           // Extract pattern for logging
 57 |           let patternStr = 'unknown-pattern';
 58 |           
 59 |           if (typeof pattern === 'string') {
 60 |             patternStr = pattern;
 61 |           } else if (pattern && typeof pattern === 'object') {
 62 |             if ('pattern' in pattern) {
 63 |               patternStr = pattern.pattern;
 64 |             }
 65 |           }
 66 |           
 67 |           // Track registered resources for logging purposes only
 68 |           registeredResources.push({ name, pattern: patternStr });
 69 |           
 70 |           // Create a context-aware handler wrapper
 71 |           const contextAwareHandler = async (uri: any, params: any, extra: any) => {
 72 |             try {
 73 |               // Ensure extra has context
 74 |               if (!extra) extra = {};
 75 |               if (!extra.context) extra.context = {};
 76 |               
 77 |               // Add Atlassian config to context
 78 |               extra.context.atlassianConfig = atlassianConfig;
 79 |               
 80 |               // Call the original handler with the enriched context
 81 |               return await handler(uri, params, extra);
 82 |             } catch (error) {
 83 |               logger.error(`Error in resource handler for ${name}:`, error);
 84 |               throw error;
 85 |             }
 86 |           };
 87 |           
 88 |           // Register the resource with the context-aware handler
 89 |           return target.resource(name, pattern, contextAwareHandler);
 90 |         } catch (error) {
 91 |           logger.error(`Error registering resource: ${error}`);
 92 |           throw error;
 93 |         }
 94 |       };
 95 |     }
 96 |     return Reflect.get(target, prop);
 97 |   }
 98 | });
 99 | 
100 | // Log config info for debugging
101 | logger.info(`Atlassian config available: ${JSON.stringify(atlassianConfig, null, 2)}`);
102 | 
103 | // Tool server proxy for consistent handling
104 | const toolServerProxy: any = {
105 |   tool: (name: string, description: string, schema: any, handler: any) => {
106 |     // Register tool with a context-aware handler wrapper
107 |     server.tool(name, description, schema, async (params: any, context: any) => {
108 |       // Add Atlassian config to context
109 |       context.atlassianConfig = atlassianConfig;
110 |       
111 |       logger.debug(`Tool ${name} called with context keys: [${Object.keys(context)}]`);
112 |       
113 |       try {
114 |         return await handler(params, context);
115 |       } catch (error) {
116 |         logger.error(`Error in tool handler for ${name}:`, error);
117 |         return {
118 |           content: [{ type: 'text', text: `Error in tool handler: ${error instanceof Error ? error.message : String(error)}` }],
119 |           isError: true
120 |         };
121 |       }
122 |     });
123 |   }
124 | };
125 | 
126 | // Register all tools
127 | logger.info('Registering all MCP Tools...');
128 | registerAllTools(toolServerProxy);
129 | 
130 | // Register all resources
131 | logger.info('Registering MCP Resources...');
132 | registerAllResources(serverProxy);
133 | 
134 | // Start the server based on configured transport type
135 | async function startServer() {
136 |   try {
137 |     // Always use STDIO transport for highest reliability
138 |     const stdioTransport = new StdioServerTransport();
139 |     await server.connect(stdioTransport);
140 |     logger.info('MCP Atlassian Server started with STDIO transport');
141 |     
142 |     // Print startup info
143 |     logger.info(`MCP Server Name: ${process.env.MCP_SERVER_NAME || 'phuc-nt/mcp-atlassian-server'}`);
144 |     logger.info(`MCP Server Version: ${process.env.MCP_SERVER_VERSION || '1.0.0'}`);
145 |     logger.info(`Connected to Atlassian site: ${ATLASSIAN_SITE_NAME}`);
146 |     
147 |     logger.info('Registered tools:');
148 |     // Liệt kê tất cả các tool đã đăng ký
149 |     logger.info('- Jira issue tools: createIssue, updateIssue, transitionIssue, assignIssue');
150 |     logger.info('- Jira filter tools: createFilter, updateFilter, deleteFilter');
151 |     logger.info('- Jira sprint tools: createSprint, startSprint, closeSprint, addIssueToSprint');
152 |     logger.info('- Jira board tools: addIssueToBoard, configureBoardColumns');
153 |     logger.info('- Jira backlog tools: addIssuesToBacklog, rankBacklogIssues');
154 |     logger.info('- Jira dashboard tools: createDashboard, updateDashboard, addGadgetToDashboard, removeGadgetFromDashboard');
155 |     logger.info('- Confluence tools: createPage, updatePage, addComment, addLabelsToPage, removeLabelsFromPage');
156 |     
157 |     // Resources - dynamically list all registered resources
158 |     logger.info('Registered resources:');
159 |     
160 |     if (registeredResources.length === 0) {
161 |       logger.info('No resources registered');
162 |     } else {
163 |       // Group by pattern and name to improve readability
164 |       const resourcesByPattern = new Map<string, string[]>();
165 |       
166 |       registeredResources.forEach(res => {
167 |         if (!resourcesByPattern.has(res.pattern)) {
168 |           resourcesByPattern.set(res.pattern, []);
169 |         }
170 |         resourcesByPattern.get(res.pattern)!.push(res.name);
171 |       });
172 |       
173 |       // Log each pattern with its resource names
174 |       Array.from(resourcesByPattern.entries())
175 |         .sort((a, b) => a[0].localeCompare(b[0]))
176 |         .forEach(([pattern, names]) => {
177 |           logger.info(`- ${pattern} (${names.join(', ')})`);
178 |         });
179 |     }
180 |   } catch (error) {
181 |     logger.error('Failed to start MCP Server:', error);
182 |     process.exit(1);
183 |   }
184 | }
185 | 
186 | // Start server
187 | startServer(); 
```

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

```typescript
  1 | /**
  2 |  * Schema definitions for Confluence resources
  3 |  */
  4 | import { standardMetadataSchema } from './common.js';
  5 | import { z } from 'zod';
  6 | 
  7 | /**
  8 |  * Schema for Confluence space
  9 |  */
 10 | export const spaceSchema = {
 11 |   type: "object",
 12 |   properties: {
 13 |     key: { type: "string", description: "Space key" },
 14 |     name: { type: "string", description: "Space name" },
 15 |     type: { type: "string", description: "Space type (global, personal, etc.)" },
 16 |     status: { type: "string", description: "Space status" },
 17 |     url: { type: "string", description: "Space URL" }
 18 |   },
 19 |   required: ["key", "name", "type"]
 20 | };
 21 | 
 22 | /**
 23 |  * Schema for Confluence spaces list
 24 |  */
 25 | export const spacesListSchema = {
 26 |   type: "object",
 27 |   properties: {
 28 |     metadata: standardMetadataSchema,
 29 |     spaces: {
 30 |       type: "array",
 31 |       items: spaceSchema
 32 |     }
 33 |   },
 34 |   required: ["metadata", "spaces"]
 35 | };
 36 | 
 37 | /**
 38 |  * Schema for Confluence page
 39 |  */
 40 | export const pageSchema = {
 41 |   type: "object",
 42 |   properties: {
 43 |     id: { type: "string", description: "Page ID" },
 44 |     title: { type: "string", description: "Page title" },
 45 |     status: { type: "string", description: "Page status" },
 46 |     spaceId: { type: "string", description: "Space ID" },
 47 |     parentId: { type: "string", description: "Parent page ID", nullable: true },
 48 |     authorId: { type: "string", description: "Author ID", nullable: true },
 49 |     createdAt: { type: "string", description: "Creation date" },
 50 |     version: {
 51 |       type: "object",
 52 |       properties: {
 53 |         number: { type: "number", description: "Version number" },
 54 |         createdAt: { type: "string", description: "Version creation date" }
 55 |       }
 56 |     },
 57 |     body: { type: "string", description: "Page content (Confluence storage format)" },
 58 |     bodyType: { type: "string", description: "Content representation type" },
 59 |     _links: { type: "object", description: "Links related to the page" }
 60 |   },
 61 |   required: ["id", "title", "status", "spaceId", "createdAt", "version", "body", "bodyType", "_links"]
 62 | };
 63 | 
 64 | /**
 65 |  * Schema for Confluence pages list
 66 |  */
 67 | export const pagesListSchema = {
 68 |   type: "object",
 69 |   properties: {
 70 |     metadata: standardMetadataSchema,
 71 |     pages: {
 72 |       type: "array",
 73 |       items: pageSchema
 74 |     },
 75 |     spaceKey: { type: "string", description: "Space key", nullable: true }
 76 |   },
 77 |   required: ["metadata", "pages"]
 78 | };
 79 | 
 80 | /**
 81 |  * Schema for Confluence comment
 82 |  */
 83 | export const commentSchema = {
 84 |   type: "object",
 85 |   properties: {
 86 |     id: { type: "string", description: "Comment ID" },
 87 |     pageId: { type: "string", description: "Page ID" },
 88 |     body: { type: "string", description: "Comment content (HTML)" },
 89 |     bodyType: { type: "string", description: "Content representation type" },
 90 |     createdAt: { type: "string", format: "date-time", description: "Creation date" },
 91 |     createdBy: {
 92 |       type: "object",
 93 |       properties: {
 94 |         accountId: { type: "string", description: "Author's account ID" },
 95 |         displayName: { type: "string", description: "Author's display name" }
 96 |       }
 97 |     },
 98 |     _links: { type: "object", description: "Links related to the comment" }
 99 |   },
100 |   required: ["id", "pageId", "body", "createdAt", "createdBy"]
101 | };
102 | 
103 | /**
104 |  * Schema for Confluence comments list
105 |  */
106 | export const commentsListSchema = {
107 |   type: "object",
108 |   properties: {
109 |     metadata: standardMetadataSchema,
110 |     comments: {
111 |       type: "array",
112 |       items: commentSchema
113 |     },
114 |     pageId: { type: "string", description: "Page ID" }
115 |   },
116 |   required: ["metadata", "comments", "pageId"]
117 | };
118 | 
119 | /**
120 |  * Schema for Confluence search results
121 |  */
122 | export const searchResultSchema = {
123 |   type: "object",
124 |   properties: {
125 |     id: { type: "string", description: "Content ID" },
126 |     title: { type: "string", description: "Content title" },
127 |     type: { type: "string", description: "Content type (page, blogpost, etc.)" },
128 |     spaceKey: { type: "string", description: "Space key" },
129 |     url: { type: "string", description: "Content URL" },
130 |     excerpt: { type: "string", description: "Content excerpt with highlights" }
131 |   },
132 |   required: ["id", "title", "type", "spaceKey"]
133 | };
134 | 
135 | /**
136 |  * Schema for Confluence search results list
137 |  */
138 | export const searchResultsListSchema = {
139 |   type: "object",
140 |   properties: {
141 |     metadata: standardMetadataSchema,
142 |     results: {
143 |       type: "array",
144 |       items: searchResultSchema
145 |     },
146 |     cql: { type: "string", description: "CQL query used for the search" }
147 |   },
148 |   required: ["metadata", "results"]
149 | };
150 | 
151 | // Label schemas
152 | export const labelSchema = {
153 |   type: "object",
154 |   properties: {
155 |     id: { type: "string", description: "Label ID" },
156 |     name: { type: "string", description: "Label name" },
157 |     prefix: { type: "string", description: "Label prefix" }
158 |   }
159 | };
160 | 
161 | export const labelListSchema = {
162 |   type: "object",
163 |   properties: {
164 |     labels: {
165 |       type: "array",
166 |       items: labelSchema
167 |     },
168 |     metadata: standardMetadataSchema
169 |   }
170 | };
171 | 
172 | // Attachment schemas
173 | export const attachmentSchema = {
174 |   type: "object",
175 |   properties: {
176 |     id: { type: "string", description: "Attachment ID" },
177 |     title: { type: "string", description: "Attachment title" },
178 |     filename: { type: "string", description: "File name" },
179 |     mediaType: { type: "string", description: "Media type" },
180 |     fileSize: { type: "number", description: "File size in bytes" },
181 |     downloadUrl: { type: "string", description: "Download URL" }
182 |   }
183 | };
184 | 
185 | export const attachmentListSchema = {
186 |   type: "object",
187 |   properties: {
188 |     attachments: {
189 |       type: "array",
190 |       items: attachmentSchema
191 |     },
192 |     metadata: standardMetadataSchema
193 |   }
194 | };
195 | 
196 | // Version schemas
197 | export const versionSchema = {
198 |   type: "object",
199 |   properties: {
200 |     number: { type: "number", description: "Version number" },
201 |     by: { 
202 |       type: "object", 
203 |       properties: {
204 |         displayName: { type: "string" },
205 |         accountId: { type: "string" }
206 |       }
207 |     },
208 |     when: { type: "string", description: "Creation date" },
209 |     message: { type: "string", description: "Version message" }
210 |   }
211 | };
212 | 
213 | export const versionListSchema = {
214 |   type: "object",
215 |   properties: {
216 |     versions: {
217 |       type: "array",
218 |       items: versionSchema
219 |     },
220 |     metadata: standardMetadataSchema
221 |   }
222 | };
223 | 
224 | export const createPageSchema = z.object({
225 |   spaceId: z.string().describe('Space ID (required, must be the numeric ID from API v2, NOT the key like TX, DEV, ...)'),
226 |   title: z.string().describe('Title of the page (required)'),
227 |   content: z.string().describe(`Content of the page (required, must be in Confluence storage format - XML-like HTML).
228 | 
229 | - Plain text or markdown is NOT supported (will throw error).
230 | - 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.
231 | - Content MUST strictly follow Confluence storage format.
232 | 
233 | Valid examples:
234 | - <p>This is a paragraph</p>
235 | - <ac:structured-macro ac:name="info"><ac:rich-text-body>Information</ac:rich-text-body></ac:structured-macro>
236 | `),
237 |   parentId: z.string().describe('Parent page ID (required, must specify the parent page to create a child page)')
238 | }); 
```

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

```markdown
  1 | # Hướng Dẫn Bổ Sung Resource và Tool cho Confluence API v2
  2 | 
  3 | 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.
  4 | 
  5 | ## Bổ Sung Resource
  6 | 
  7 | ### 1. Blog Posts
  8 | 
  9 | | Resource | URI | Mô tả | Atlassian API Endpoint | Dữ liệu trả về |
 10 | |----------|-----|-------|-----------------------|----------------|
 11 | | Blog Posts | `confluence://blogposts` | Danh sách bài viết blog | `/wiki/api/v2/blogposts` | Array của BlogPost objects |
 12 | | Blog Post Details | `confluence://blogposts/{blogpostId}` | Chi tiết bài viết blog | `/wiki/api/v2/blogposts/{blogpostId}` | Single BlogPost object |
 13 | | 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 |
 14 | | 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 |
 15 | 
 16 | ### 2. Comments
 17 | 
 18 | | Resource | URI | Mô tả | Atlassian API Endpoint | Dữ liệu trả về |
 19 | |----------|-----|-------|-----------------------|----------------|
 20 | | 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 |
 21 | | 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 |
 22 | | Comment Details | `confluence://comments/{commentId}` | Chi tiết bình luận | `/wiki/api/v2/comments/{commentId}` | Single Comment object |
 23 | 
 24 | ### 3. Watchers
 25 | 
 26 | | Resource | URI | Mô tả | Atlassian API Endpoint | Dữ liệu trả về |
 27 | |----------|-----|-------|-----------------------|----------------|
 28 | | Page Watchers | `confluence://pages/{pageId}/watchers` | Người theo dõi trang | `/wiki/api/v2/pages/{pageId}/watchers` | Array của Watcher objects |
 29 | | 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 |
 30 | | Blog Watchers | `confluence://blogposts/{blogpostId}/watchers` | Người theo dõi blog | `/wiki/api/v2/blogposts/{blogpostId}/watchers` | Array của Watcher objects |
 31 | 
 32 | ### 4. Custom Content
 33 | 
 34 | | Resource | URI | Mô tả | Atlassian API Endpoint | Dữ liệu trả về |
 35 | |----------|-----|-------|-----------------------|----------------|
 36 | | 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 |
 37 | | 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 |
 38 | 
 39 | ## Bổ Sung Tool
 40 | 
 41 | ### 1. Quản lý trang
 42 | 
 43 | | Tool | Mô tả | Tham số chính | Atlassian API Endpoint | Dữ liệu output |
 44 | |------|-------|---------------|-----------------------|----------------|
 45 | | deletePage | Xóa trang | pageId | `/wiki/api/v2/pages/{id}` (DELETE) | Status của xóa |
 46 | | 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 |
 47 | | watchPage | Theo dõi trang | pageId, userId | `/wiki/api/v2/pages/{id}/watchers` (POST) | Status của theo dõi |
 48 | | 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 |
 49 | 
 50 | ### 2. Quản lý Blog Post
 51 | 
 52 | | Tool | Mô tả | Tham số chính | Atlassian API Endpoint | Dữ liệu output |
 53 | |------|-------|---------------|-----------------------|----------------|
 54 | | createBlogPost | Tạo bài viết blog | spaceId, title, content | `/wiki/api/v2/blogposts` (POST) | BlogPost ID mới |
 55 | | updateBlogPost | Cập nhật bài viết blog | blogpostId, title, content, version | `/wiki/api/v2/blogposts/{id}` (PUT) | Status của update |
 56 | | deleteBlogPost | Xóa bài viết blog | blogpostId | `/wiki/api/v2/blogposts/{id}` (DELETE) | Status của xóa |
 57 | | watchBlogPost | Theo dõi bài viết blog | blogpostId, userId | `/wiki/api/v2/blogposts/{id}/watchers` (POST) | Status của theo dõi |
 58 | 
 59 | ### 3. Quản lý Comment
 60 | 
 61 | | Tool | Mô tả | Tham số chính | Atlassian API Endpoint | Dữ liệu output |
 62 | |------|-------|---------------|-----------------------|----------------|
 63 | | updateComment | Cập nhật bình luận | commentId, content, version | `/wiki/api/v2/comments/{id}` (PUT) | Status của update |
 64 | | deleteComment | Xóa bình luận | commentId | `/wiki/api/v2/comments/{id}` (DELETE) | Status của xóa |
 65 | 
 66 | ### 4. Quản lý Attachment
 67 | 
 68 | | Tool | Mô tả | Tham số chính | Atlassian API Endpoint | Dữ liệu output |
 69 | |------|-------|---------------|-----------------------|----------------|
 70 | | uploadAttachment | Tải lên tệp đính kèm | pageId, file, comment | `/wiki/api/v2/pages/{id}/attachments` (POST) | Attachment ID mới |
 71 | | updateAttachment | Cập nhật tệp đính kèm | pageId, attachmentId, file, comment | `/wiki/api/v2/attachments/{id}` (PUT) | Status của update |
 72 | | deleteAttachment | Xóa tệp đính kèm | attachmentId | `/wiki/api/v2/attachments/{id}` (DELETE) | Status của xóa |
 73 | 
 74 | ### 5. Quản lý Space
 75 | 
 76 | | Tool | Mô tả | Tham số chính | Atlassian API Endpoint | Dữ liệu output |
 77 | |------|-------|---------------|-----------------------|----------------|
 78 | | createSpace | Tạo không gian | name, key, description | `/wiki/api/v2/spaces` (POST) | Space ID mới |
 79 | | updateSpace | Cập nhật không gian | spaceId, name, description | `/wiki/api/v2/spaces/{id}` (PUT) | Status của update |
 80 | | watchSpace | Theo dõi không gian | spaceId, userId | `/wiki/api/v2/spaces/{id}/watchers` (POST) | Status của theo dõi |
 81 | | 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 |
 82 | 
 83 | ## Lưu ý quan trọng về API v2
 84 | 
 85 | 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`.
 86 | 
 87 | 2. **Cấu trúc request/response**:
 88 |    - 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
 89 |    - Ví dụ tạo trang:
 90 |      ```
 91 |      {
 92 |        "spaceId": "string",
 93 |        "status": "current",
 94 |        "title": "string",
 95 |        "parentId": "string",
 96 |        "body": {
 97 |          "representation": "storage",
 98 |          "value": "string"
 99 |        }
100 |      }
101 |      ```
102 | 
103 | 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.
104 | 
105 | 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`.
106 | 
107 | 5. **Quyền hạn**: Mỗi endpoint yêu cầu quyền hạn cụ thể, ví dụ:
108 |    - Tạo trang: Quyền xem không gian tương ứng và quyền tạo trang trong không gian đó
109 |    - Xóa trang: Quyền xem trang, không gian tương ứng và quyền xóa trang
110 | 
111 | 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.
112 | 
113 | 7. **Scope cho Connect app**: Các endpoint yêu cầu scope cụ thể, ví dụ:
114 |    - Đọc trang: `READ`
115 |    - Ghi trang: `WRITE`
116 | 
117 | 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
  1 | https://modelcontextprotocol.io/docs/concepts/resources
  2 | 
  3 | # Resources
  4 | 
  5 | > Expose data and content from your servers to LLMs
  6 | 
  7 | 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.
  8 | 
  9 | <Note>
 10 |   Resources are designed to be **application-controlled**, meaning that the client application can decide how and when they should be used.
 11 |   Different MCP clients may handle resources differently. For example:
 12 | 
 13 |   * Claude Desktop currently requires users to explicitly select resources before they can be used
 14 |   * Other clients might automatically select resources based on heuristics
 15 |   * Some implementations may even allow the AI model itself to determine which resources to use
 16 | 
 17 |   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).
 18 | </Note>
 19 | 
 20 | ## Overview
 21 | 
 22 | Resources represent any kind of data that an MCP server wants to make available to clients. This can include:
 23 | 
 24 | * File contents
 25 | * Database records
 26 | * API responses
 27 | * Live system data
 28 | * Screenshots and images
 29 | * Log files
 30 | * And more
 31 | 
 32 | Each resource is identified by a unique URI and can contain either text or binary data.
 33 | 
 34 | ## Resource URIs
 35 | 
 36 | Resources are identified using URIs that follow this format:
 37 | 
 38 | ```
 39 | [protocol]://[host]/[path]
 40 | ```
 41 | 
 42 | For example:
 43 | 
 44 | * `file:///home/user/documents/report.pdf`
 45 | * `postgres://database/customers/schema`
 46 | * `screen://localhost/display1`
 47 | 
 48 | The protocol and path structure is defined by the MCP server implementation. Servers can define their own custom URI schemes.
 49 | 
 50 | ## Resource types
 51 | 
 52 | Resources can contain two types of content:
 53 | 
 54 | ### Text resources
 55 | 
 56 | Text resources contain UTF-8 encoded text data. These are suitable for:
 57 | 
 58 | * Source code
 59 | * Configuration files
 60 | * Log files
 61 | * JSON/XML data
 62 | * Plain text
 63 | 
 64 | ### Binary resources
 65 | 
 66 | Binary resources contain raw binary data encoded in base64. These are suitable for:
 67 | 
 68 | * Images
 69 | * PDFs
 70 | * Audio files
 71 | * Video files
 72 | * Other non-text formats
 73 | 
 74 | ## Resource discovery
 75 | 
 76 | Clients can discover available resources through two main methods:
 77 | 
 78 | ### Direct resources
 79 | 
 80 | Servers expose a list of concrete resources via the `resources/list` endpoint. Each resource includes:
 81 | 
 82 | ```typescript
 83 | {
 84 |   uri: string;           // Unique identifier for the resource
 85 |   name: string;          // Human-readable name
 86 |   description?: string;  // Optional description
 87 |   mimeType?: string;     // Optional MIME type
 88 | }
 89 | ```
 90 | 
 91 | ### Resource templates
 92 | 
 93 | For dynamic resources, servers can expose [URI templates](https://datatracker.ietf.org/doc/html/rfc6570) that clients can use to construct valid resource URIs:
 94 | 
 95 | ```typescript
 96 | {
 97 |   uriTemplate: string;   // URI template following RFC 6570
 98 |   name: string;          // Human-readable name for this type
 99 |   description?: string;  // Optional description
100 |   mimeType?: string;     // Optional MIME type for all matching resources
101 | }
102 | ```
103 | 
104 | ## Reading resources
105 | 
106 | To read a resource, clients make a `resources/read` request with the resource URI.
107 | 
108 | The server responds with a list of resource contents:
109 | 
110 | ```typescript
111 | {
112 |   contents: [
113 |     {
114 |       uri: string;        // The URI of the resource
115 |       mimeType?: string;  // Optional MIME type
116 | 
117 |       // One of:
118 |       text?: string;      // For text resources
119 |       blob?: string;      // For binary resources (base64 encoded)
120 |     }
121 |   ]
122 | }
123 | ```
124 | 
125 | <Tip>
126 |   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.
127 | </Tip>
128 | 
129 | ## Resource updates
130 | 
131 | MCP supports real-time updates for resources through two mechanisms:
132 | 
133 | ### List changes
134 | 
135 | Servers can notify clients when their list of available resources changes via the `notifications/resources/list_changed` notification.
136 | 
137 | ### Content changes
138 | 
139 | Clients can subscribe to updates for specific resources:
140 | 
141 | 1. Client sends `resources/subscribe` with resource URI
142 | 2. Server sends `notifications/resources/updated` when the resource changes
143 | 3. Client can fetch latest content with `resources/read`
144 | 4. Client can unsubscribe with `resources/unsubscribe`
145 | 
146 | ## Example implementation
147 | 
148 | Here's a simple example of implementing resource support in an MCP server:
149 | 
150 | <Tabs>
151 |   <Tab title="TypeScript">
152 |     ```typescript
153 |     const server = new Server({
154 |       name: "example-server",
155 |       version: "1.0.0"
156 |     }, {
157 |       capabilities: {
158 |         resources: {}
159 |       }
160 |     });
161 | 
162 |     // List available resources
163 |     server.setRequestHandler(ListResourcesRequestSchema, async () => {
164 |       return {
165 |         resources: [
166 |           {
167 |             uri: "file:///logs/app.log",
168 |             name: "Application Logs",
169 |             mimeType: "text/plain"
170 |           }
171 |         ]
172 |       };
173 |     });
174 | 
175 |     // Read resource contents
176 |     server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
177 |       const uri = request.params.uri;
178 | 
179 |       if (uri === "file:///logs/app.log") {
180 |         const logContents = await readLogFile();
181 |         return {
182 |           contents: [
183 |             {
184 |               uri,
185 |               mimeType: "text/plain",
186 |               text: logContents
187 |             }
188 |           ]
189 |         };
190 |       }
191 | 
192 |       throw new Error("Resource not found");
193 |     });
194 |     ```
195 |   </Tab>
196 | 
197 |   <Tab title="Python">
198 |     ```python
199 |     app = Server("example-server")
200 | 
201 |     @app.list_resources()
202 |     async def list_resources() -> list[types.Resource]:
203 |         return [
204 |             types.Resource(
205 |                 uri="file:///logs/app.log",
206 |                 name="Application Logs",
207 |                 mimeType="text/plain"
208 |             )
209 |         ]
210 | 
211 |     @app.read_resource()
212 |     async def read_resource(uri: AnyUrl) -> str:
213 |         if str(uri) == "file:///logs/app.log":
214 |             log_contents = await read_log_file()
215 |             return log_contents
216 | 
217 |         raise ValueError("Resource not found")
218 | 
219 |     # Start server
220 |     async with stdio_server() as streams:
221 |         await app.run(
222 |             streams[0],
223 |             streams[1],
224 |             app.create_initialization_options()
225 |         )
226 |     ```
227 |   </Tab>
228 | </Tabs>
229 | 
230 | ## Best practices
231 | 
232 | When implementing resource support:
233 | 
234 | 1. Use clear, descriptive resource names and URIs
235 | 2. Include helpful descriptions to guide LLM understanding
236 | 3. Set appropriate MIME types when known
237 | 4. Implement resource templates for dynamic content
238 | 5. Use subscriptions for frequently changing resources
239 | 6. Handle errors gracefully with clear error messages
240 | 7. Consider pagination for large resource lists
241 | 8. Cache resource contents when appropriate
242 | 9. Validate URIs before processing
243 | 10. Document your custom URI schemes
244 | 
245 | ## Security considerations
246 | 
247 | When exposing resources:
248 | 
249 | * Validate all resource URIs
250 | * Implement appropriate access controls
251 | * Sanitize file paths to prevent directory traversal
252 | * Be cautious with binary data handling
253 | * Consider rate limiting for resource reads
254 | * Audit resource access
255 | * Encrypt sensitive data in transit
256 | * Validate MIME types
257 | * Implement timeouts for long-running reads
258 | * Handle resource cleanup appropriately
259 | 
```

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

```markdown
  1 | # MCP Atlassian Server (by phuc-nt) - Bundle Installation Guide for AI
  2 | 
  3 | > **Important Note:** This is a pre-built bundle version of MCP Atlassian Server. No compilation or dependency installation required - just extract and run!
  4 | 
  5 | ## System Requirements
  6 | - macOS 10.15+ or Windows 10+
  7 | - Node.js v16+ (only for running the server, not for building)
  8 | - Atlassian Cloud account and API token
  9 | - Cline AI assistant (main supported client)
 10 | 
 11 | ## Step 1: Extract the Bundle
 12 | ```bash
 13 | # Extract the downloaded bundle
 14 | unzip mcp-atlassian-server-bundle.zip
 15 | 
 16 | # Navigate to the extracted directory
 17 | cd mcp-atlassian-server-bundle
 18 | ```
 19 | 
 20 | ## Step 2: Configure Cline
 21 | 
 22 | MCP Atlassian Server is specifically designed for seamless integration with Cline. Below is the guide to configure Cline to connect to the server:
 23 | 
 24 | ### Determine the Full Path
 25 | 
 26 | First, determine the full path to your extracted bundle directory:
 27 | 
 28 | ```bash
 29 | # macOS/Linux
 30 | pwd
 31 | 
 32 | # Windows (PowerShell)
 33 | (Get-Location).Path
 34 | 
 35 | # Windows (Command Prompt)
 36 | cd
 37 | ```
 38 | 
 39 | Then, add the following configuration to your `cline_mcp_settings.json` file:
 40 | 
 41 | ```json
 42 | {
 43 |   "mcpServers": {
 44 |     "phuc-nt/mcp-atlassian-server": {
 45 |       "disabled": false,
 46 |       "timeout": 60,
 47 |       "command": "node",
 48 |       "args": [
 49 |         "/full/path/to/mcp-atlassian-server-bundle/dist/index.js"
 50 |       ],
 51 |       "env": {
 52 |         "ATLASSIAN_SITE_NAME": "your-site.atlassian.net",
 53 |         "ATLASSIAN_USER_EMAIL": "[email protected]",
 54 |         "ATLASSIAN_API_TOKEN": "your-api-token"
 55 |       },
 56 |       "transportType": "stdio"
 57 |     }
 58 |   }
 59 | }
 60 | ```
 61 | 
 62 | Replace:
 63 | - `/full/path/to/` with the path you just obtained
 64 | - `your-site.atlassian.net` with your Atlassian site name
 65 | - `[email protected]` with your Atlassian email
 66 | - `your-api-token` with your Atlassian API token
 67 | 
 68 | > **Note for Windows**: The path on Windows may look like `C:\\Users\\YourName\\mcp-atlassian-server-bundle\\dist\\index.js` (use `\\` instead of `/`).
 69 | 
 70 | ## Step 3: Get Atlassian API Token
 71 | 1. Go to https://id.atlassian.com/manage-profile/security/api-tokens
 72 | 2. Click "Create API token", name it (e.g., "MCP Server")
 73 | 3. Copy the token immediately (it will not be shown again)
 74 | 
 75 | ### Note on API Token Permissions
 76 | 
 77 | - **The API token inherits all permissions of the account that created it** – there is no separate permission mechanism for the token itself.
 78 | - **To use all features of MCP Server**, the account creating the token must have appropriate permissions:
 79 |   - **Jira**: Needs Browse Projects, Edit Issues, Assign Issues, Transition Issues, Create Issues, etc.
 80 |   - **Confluence**: Needs View Spaces, Add Pages, Add Comments, Edit Pages, etc.
 81 | - **If the token is read-only**, you can only use read resources (view issues, projects) but cannot create/update.
 82 | - **Recommendations**:
 83 |   - For personal use: You can use your main account's token
 84 |   - For team/long-term use: Create a dedicated service account with appropriate permissions
 85 |   - Do not share your token; if you suspect it is compromised, revoke and create a new one
 86 | - **If you get a "permission denied" error**, check the permissions of the account that created the token on the relevant projects/spaces
 87 | 
 88 | > **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.
 89 | 
 90 | ### Security Warning When Using LLMs
 91 | 
 92 | - **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.).
 93 | - **How it works**:
 94 |   - Cline does **NOT** automatically send config files to the cloud
 95 |   - 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
 96 | - **Safety recommendations**:
 97 |   - Do not ask the LLM to read/check config files containing tokens
 98 |   - If you need support, remove sensitive information before sending to the LLM
 99 |   - Treat your API token like a password – never share it in LLM prompts
100 | 
101 | > **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.
102 | 
103 | ## Step 4: Run the Server (Optional Testing)
104 | 
105 | You can test the server locally before configuring Cline by running:
106 | 
107 | ```bash
108 | node dist/index.js
109 | ```
110 | 
111 | You should see output confirming the server is running. Press Ctrl+C to stop.
112 | 
113 | > **Note**: You don't need to manually run the server when using with Cline - Cline will automatically start and manage the server process.
114 | 
115 | ## Verify Installation
116 | After configuration, test the connection by asking Cline a question related to Jira or Confluence, for example:
117 | - "List all projects in Jira"
118 | - "Search for Confluence pages about [topic]"
119 | 
120 | 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.
121 | 
122 | ## Introduction & Usage Scenarios
123 | 
124 | ### Capabilities of MCP Atlassian Server (by phuc-nt)
125 | 
126 | This MCP Server connects AI to Atlassian systems (Jira and Confluence), enabling:
127 | 
128 | #### Jira Information Access
129 | - View details of issues, projects, and users
130 | - Search issues with simple JQL
131 | - View possible transitions
132 | - View issue comments
133 | - Find users assignable to tasks or by role
134 | 
135 | #### Jira Actions
136 | - Create new issues
137 | - Update issue content
138 | - Transition issue status
139 | - Assign issues to users
140 | 
141 | #### Confluence Information Access
142 | - View spaces
143 | - View pages and child pages
144 | - View page details (title, content, version, labels)
145 | - View comments on pages
146 | - View labels on pages
147 | 
148 | #### Confluence Actions
149 | - Create new pages with simple HTML content
150 | - Update existing pages (title, content, version, labels)
151 | - Add and remove labels on pages
152 | - Add comments to pages
153 | 
154 | ### Example Usage Scenarios
155 | 
156 | 1. **Create and Manage Tasks**
157 |    ```
158 |    "Create a new issue in project XDEMO2 about login error"
159 |    "Find issues that are 'In Progress' and assign them to me"
160 |    "Transition issue XDEMO2-43 to Done"
161 |    ```
162 | 
163 | 2. **Project Information Summary**
164 |    ```
165 |    "Summarize all issues in project XDEMO2"
166 |    "Who is assigned issues in project XDEMO2?"
167 |    "List unassigned issues in the current sprint"
168 |    ```
169 | 
170 | 3. **Documentation with Confluence**
171 |    ```
172 |    "Create a new Confluence page named 'Meeting Notes 2025-05-03'"
173 |    "Update the Confluence page about API Documentation with new examples and add label 'documentation'"
174 |    "Add the label 'documentation' to the page about architecture"
175 |    "Remove the label 'draft' from the page 'Meeting Notes'"
176 |    "Add a comment to the Confluence page about API Documentation"
177 |    ```
178 | 
179 | 4. **Analysis and Reporting**
180 |    ```
181 |    "Compare the number of completed issues between the current and previous sprint"
182 |    "Who has the most issues in 'To Do' status?"
183 |    ```
184 | 
185 | ### Usage Notes
186 | 
187 | 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`).
188 | 
189 | 2. **Create Confluence Page**: When creating a Confluence page, use simple HTML content and do not specify parentId to avoid errors.
190 | 
191 | 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.
192 | 
193 | 4. **Create Issue**: When creating new issues, only provide the minimum required fields (projectKey, summary) for best success.
194 | 
195 | 5. **Access Rights**: Ensure the configured Atlassian account has access to the projects and spaces you want to interact with.
196 | 
197 | 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
  1 | import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
  2 | import { Config, Resources } from '../../utils/mcp-helpers.js';
  3 | import { AtlassianConfig } from '../../utils/atlassian-api-base.js';
  4 | import { ApiError, ApiErrorType } from '../../utils/error-handler.js';
  5 | import { Logger } from '../../utils/logger.js';
  6 | import fetch from 'cross-fetch';
  7 | import { projectsListSchema, projectSchema } from '../../schemas/jira.js';
  8 | import { getProjects as getProjectsApi, getProject as getProjectApi } from '../../utils/jira-resource-api.js';
  9 | 
 10 | const logger = Logger.getLogger('JiraResource:Projects');
 11 | 
 12 | /**
 13 |  * Create basic headers for Atlassian API with Basic Authentication
 14 |  */
 15 | function createBasicHeaders(email: string, apiToken: string) {
 16 |   const auth = Buffer.from(`${email}:${apiToken}`).toString('base64');
 17 |   return {
 18 |     'Authorization': `Basic ${auth}`,
 19 |     'Content-Type': 'application/json',
 20 |     'Accept': 'application/json',
 21 |     'User-Agent': 'MCP-Atlassian-Server/1.0.0'
 22 |   };
 23 | }
 24 | 
 25 | /**
 26 |  * Helper function to get the list of projects
 27 |  */
 28 | async function getProjects(config: AtlassianConfig): Promise<any[]> {
 29 |   return await getProjectsApi(config);
 30 | }
 31 | 
 32 | /**
 33 |  * Helper function to get project details
 34 |  */
 35 | async function getProject(config: AtlassianConfig, projectKey: string): Promise<any> {
 36 |   return await getProjectApi(config, projectKey);
 37 | }
 38 | 
 39 | /**
 40 |  * Register resources related to Jira projects
 41 |  * @param server MCP Server instance
 42 |  */
 43 | export function registerProjectResources(server: McpServer) {
 44 |   // Resource: List all projects
 45 |   server.resource(
 46 |     'jira-projects-list',
 47 |     new ResourceTemplate('jira://projects', {
 48 |       list: async (_extra) => {
 49 |         return {
 50 |           resources: [
 51 |             {
 52 |               uri: 'jira://projects',
 53 |               name: 'Jira Projects',
 54 |               description: 'List and search all Jira projects',
 55 |               mimeType: 'application/json'
 56 |             }
 57 |           ]
 58 |         };
 59 |       }
 60 |     }),
 61 |     async (uri, _params, _extra) => {
 62 |       logger.info('Getting list of Jira projects');
 63 |       try {
 64 |         // Get config from environment
 65 |         const config = Config.getAtlassianConfigFromEnv();
 66 |         
 67 |         // Get the list of projects from Jira API
 68 |         const projects = await getProjects(config);
 69 |         // Convert response to a more friendly format
 70 |         const formattedProjects = projects.map((project: any) => ({
 71 |           id: project.id,
 72 |           key: project.key,
 73 |           name: project.name,
 74 |           projectType: project.projectTypeKey,
 75 |           url: `${config.baseUrl}/browse/${project.key}`,
 76 |           lead: project.lead?.displayName || 'Unknown'
 77 |         }));
 78 |         
 79 |         const uriString = typeof uri === 'string' ? uri : uri.href;
 80 |         // Return standardized resource with metadata and schema
 81 |         return Resources.createStandardResource(
 82 |           uriString,
 83 |           formattedProjects,
 84 |           'projects',
 85 |           projectsListSchema,
 86 |           formattedProjects.length,
 87 |           formattedProjects.length,
 88 |           0,
 89 |           `${config.baseUrl}/jira/projects`
 90 |         );
 91 |       } catch (error) {
 92 |         logger.error('Error getting Jira projects:', error);
 93 |         throw error;
 94 |       }
 95 |     }
 96 |   );
 97 |   
 98 |   // Resource: Project details
 99 |   server.resource(
100 |     'jira-project-details',
101 |     new ResourceTemplate('jira://projects/{projectKey}', {
102 |       list: async (_extra) => ({
103 |         resources: [
104 |           {
105 |             uri: 'jira://projects/{projectKey}',
106 |             name: 'Jira Project Details',
107 |             description: 'Get details for a specific Jira project by key. Replace {projectKey} with the project key.',
108 |             mimeType: 'application/json'
109 |           }
110 |         ]
111 |       })
112 |     }),
113 |     async (uri, params, _extra) => {
114 |       try {
115 |         // Get config from environment
116 |         const config = Config.getAtlassianConfigFromEnv();
117 |         
118 |         // Get projectKey from URI pattern
119 |         let normalizedProjectKey = '';
120 |         if (params && 'projectKey' in params) {
121 |           normalizedProjectKey = Array.isArray(params.projectKey) ? params.projectKey[0] : params.projectKey;
122 |         }
123 |         
124 |         if (!normalizedProjectKey) {
125 |           throw new ApiError(
126 |             ApiErrorType.VALIDATION_ERROR,
127 |             'Project key not provided',
128 |             400,
129 |             new Error('Missing project key parameter')
130 |           );
131 |         }
132 |         logger.info(`Getting details for Jira project: ${normalizedProjectKey}`);
133 |         
134 |         // Get project info from Jira API
135 |         const project = await getProject(config, normalizedProjectKey);
136 |         // Convert response to a more friendly format
137 |         const formattedProject = {
138 |           id: project.id,
139 |           key: project.key,
140 |           name: project.name,
141 |           description: project.description || 'No description',
142 |           lead: project.lead?.displayName || 'Unknown',
143 |           url: `${config.baseUrl}/browse/${project.key}`,
144 |           projectCategory: project.projectCategory?.name || 'Uncategorized',
145 |           projectType: project.projectTypeKey
146 |         };
147 |         
148 |         const uriString = typeof uri === 'string' ? uri : uri.href;
149 |         // Chuẩn hóa metadata/schema
150 |         return Resources.createStandardResource(
151 |           uriString,
152 |           [formattedProject],
153 |           'project',
154 |           projectSchema,
155 |           1,
156 |           1,
157 |           0,
158 |           `${config.baseUrl}/browse/${project.key}`
159 |         );
160 |       } catch (error) {
161 |         logger.error(`Error getting Jira project details:`, error);
162 |         throw error;
163 |       }
164 |     }
165 |   );
166 | 
167 |   // Resource: List roles of a project
168 |   server.resource(
169 |     'jira-project-roles',
170 |     new ResourceTemplate('jira://projects/{projectKey}/roles', {
171 |       list: async (_extra) => ({
172 |         resources: [
173 |           {
174 |             uri: 'jira://projects/{projectKey}/roles',
175 |             name: 'Jira Project Roles',
176 |             description: 'List roles for a Jira project. Replace {projectKey} with the project key.',
177 |             mimeType: 'application/json'
178 |           }
179 |         ]
180 |       })
181 |     }),
182 |     async (uri, params, _extra) => {
183 |       try {
184 |         // Get config from environment
185 |         const config = Config.getAtlassianConfigFromEnv();
186 |         
187 |         let normalizedProjectKey = '';
188 |         if (params && 'projectKey' in params) {
189 |           normalizedProjectKey = Array.isArray(params.projectKey) ? params.projectKey[0] : params.projectKey;
190 |         }
191 |         
192 |         if (!normalizedProjectKey) {
193 |           throw new Error('Missing projectKey');
194 |         }
195 |         logger.info(`Getting roles for Jira project: ${normalizedProjectKey}`);
196 |         
197 |         const auth = Buffer.from(`${config.email}:${config.apiToken}`).toString('base64');
198 |         const headers = {
199 |           'Authorization': `Basic ${auth}`,
200 |           'Content-Type': 'application/json',
201 |           'Accept': 'application/json',
202 |           'User-Agent': 'MCP-Atlassian-Server/1.0.0'
203 |         };
204 |         let baseUrl = config.baseUrl;
205 |         if (!baseUrl.startsWith('https://')) baseUrl = `https://${baseUrl}`;
206 |         const url = `${baseUrl}/rest/api/3/project/${encodeURIComponent(normalizedProjectKey)}/role`;
207 |         logger.debug(`Calling Jira API: ${url}`);
208 |         const response = await fetch(url, { method: 'GET', headers, credentials: 'omit' });
209 |         if (!response.ok) {
210 |           const statusCode = response.status;
211 |           const responseText = await response.text();
212 |           logger.error(`Jira API error (${statusCode}):`, responseText);
213 |           throw new Error(`Jira API error: ${responseText}`);
214 |         }
215 |         const data = await response.json();
216 |         // data is an object: key is roleName, value is URL containing roleId
217 |         const roles = Object.entries(data).map(([roleName, url]) => {
218 |           const urlStr = String(url);
219 |           const match = urlStr.match(/\/role\/(\d+)$/);
220 |           return {
221 |             roleName,
222 |             roleId: match ? match[1] : '',
223 |             url: urlStr
224 |           };
225 |         });
226 |         
227 |         const uriString = typeof uri === 'string' ? uri : uri.href;
228 |         // Chuẩn hóa metadata/schema (dùng array of role object, schema tự tạo inline)
229 |         const rolesListSchema = {
230 |           type: "array",
231 |           items: {
232 |             type: "object",
233 |             properties: {
234 |               roleName: { type: "string" },
235 |               roleId: { type: "string" },
236 |               url: { type: "string" }
237 |             },
238 |             required: ["roleName", "roleId", "url"]
239 |           }
240 |         };
241 |         return Resources.createStandardResource(
242 |           uriString,
243 |           roles,
244 |           'roles',
245 |           rolesListSchema,
246 |           roles.length,
247 |           roles.length,
248 |           0,
249 |           `${config.baseUrl}/browse/${normalizedProjectKey}/project-roles`
250 |         );
251 |       } catch (error) {
252 |         logger.error(`Error getting roles for Jira project:`, error);
253 |         throw error;
254 |       }
255 |     }
256 |   );
257 |   
258 |   logger.info('Jira project resources registered successfully');
259 | }
260 | 
```

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

```markdown
  1 | https://modelcontextprotocol.io/docs/concepts/architecture
  2 | 
  3 | # Core architecture
  4 | 
  5 | > Understand how MCP connects clients, servers, and LLMs
  6 | 
  7 | 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.
  8 | 
  9 | ## Overview
 10 | 
 11 | MCP follows a client-server architecture where:
 12 | 
 13 | * **Hosts** are LLM applications (like Claude Desktop or IDEs) that initiate connections
 14 | * **Clients** maintain 1:1 connections with servers, inside the host application
 15 | * **Servers** provide context, tools, and prompts to clients
 16 | 
 17 | ```mermaid
 18 | flowchart LR
 19 |     subgraph "Host"
 20 |         client1[MCP Client]
 21 |         client2[MCP Client]
 22 |     end
 23 |     subgraph "Server Process"
 24 |         server1[MCP Server]
 25 |     end
 26 |     subgraph "Server Process"
 27 |         server2[MCP Server]
 28 |     end
 29 | 
 30 |     client1 <-->|Transport Layer| server1
 31 |     client2 <-->|Transport Layer| server2
 32 | ```
 33 | 
 34 | ## Core components
 35 | 
 36 | ### Protocol layer
 37 | 
 38 | The protocol layer handles message framing, request/response linking, and high-level communication patterns.
 39 | 
 40 | <Tabs>
 41 |   <Tab title="TypeScript">
 42 |     ```typescript
 43 |     class Protocol<Request, Notification, Result> {
 44 |         // Handle incoming requests
 45 |         setRequestHandler<T>(schema: T, handler: (request: T, extra: RequestHandlerExtra) => Promise<Result>): void
 46 | 
 47 |         // Handle incoming notifications
 48 |         setNotificationHandler<T>(schema: T, handler: (notification: T) => Promise<void>): void
 49 | 
 50 |         // Send requests and await responses
 51 |         request<T>(request: Request, schema: T, options?: RequestOptions): Promise<T>
 52 | 
 53 |         // Send one-way notifications
 54 |         notification(notification: Notification): Promise<void>
 55 |     }
 56 |     ```
 57 |   </Tab>
 58 | 
 59 |   <Tab title="Python">
 60 |     ```python
 61 |     class Session(BaseSession[RequestT, NotificationT, ResultT]):
 62 |         async def send_request(
 63 |             self,
 64 |             request: RequestT,
 65 |             result_type: type[Result]
 66 |         ) -> Result:
 67 |             """Send request and wait for response. Raises McpError if response contains error."""
 68 |             # Request handling implementation
 69 | 
 70 |         async def send_notification(
 71 |             self,
 72 |             notification: NotificationT
 73 |         ) -> None:
 74 |             """Send one-way notification that doesn't expect response."""
 75 |             # Notification handling implementation
 76 | 
 77 |         async def _received_request(
 78 |             self,
 79 |             responder: RequestResponder[ReceiveRequestT, ResultT]
 80 |         ) -> None:
 81 |             """Handle incoming request from other side."""
 82 |             # Request handling implementation
 83 | 
 84 |         async def _received_notification(
 85 |             self,
 86 |             notification: ReceiveNotificationT
 87 |         ) -> None:
 88 |             """Handle incoming notification from other side."""
 89 |             # Notification handling implementation
 90 |     ```
 91 |   </Tab>
 92 | </Tabs>
 93 | 
 94 | Key classes include:
 95 | 
 96 | * `Protocol`
 97 | * `Client`
 98 | * `Server`
 99 | 
100 | ### Transport layer
101 | 
102 | The transport layer handles the actual communication between clients and servers. MCP supports multiple transport mechanisms:
103 | 
104 | 1. **Stdio transport**
105 |    * Uses standard input/output for communication
106 |    * Ideal for local processes
107 | 
108 | 2. **HTTP with SSE transport**
109 |    * Uses Server-Sent Events for server-to-client messages
110 |    * HTTP POST for client-to-server messages
111 | 
112 | 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.
113 | 
114 | ### Message types
115 | 
116 | MCP has these main types of messages:
117 | 
118 | 1. **Requests** expect a response from the other side:
119 |    ```typescript
120 |    interface Request {
121 |      method: string;
122 |      params?: { ... };
123 |    }
124 |    ```
125 | 
126 | 2. **Results** are successful responses to requests:
127 |    ```typescript
128 |    interface Result {
129 |      [key: string]: unknown;
130 |    }
131 |    ```
132 | 
133 | 3. **Errors** indicate that a request failed:
134 |    ```typescript
135 |    interface Error {
136 |      code: number;
137 |      message: string;
138 |      data?: unknown;
139 |    }
140 |    ```
141 | 
142 | 4. **Notifications** are one-way messages that don't expect a response:
143 |    ```typescript
144 |    interface Notification {
145 |      method: string;
146 |      params?: { ... };
147 |    }
148 |    ```
149 | 
150 | ## Connection lifecycle
151 | 
152 | ### 1. Initialization
153 | 
154 | ```mermaid
155 | sequenceDiagram
156 |     participant Client
157 |     participant Server
158 | 
159 |     Client->>Server: initialize request
160 |     Server->>Client: initialize response
161 |     Client->>Server: initialized notification
162 | 
163 |     Note over Client,Server: Connection ready for use
164 | ```
165 | 
166 | 1. Client sends `initialize` request with protocol version and capabilities
167 | 2. Server responds with its protocol version and capabilities
168 | 3. Client sends `initialized` notification as acknowledgment
169 | 4. Normal message exchange begins
170 | 
171 | ### 2. Message exchange
172 | 
173 | After initialization, the following patterns are supported:
174 | 
175 | * **Request-Response**: Client or server sends requests, the other responds
176 | * **Notifications**: Either party sends one-way messages
177 | 
178 | ### 3. Termination
179 | 
180 | Either party can terminate the connection:
181 | 
182 | * Clean shutdown via `close()`
183 | * Transport disconnection
184 | * Error conditions
185 | 
186 | ## Error handling
187 | 
188 | MCP defines these standard error codes:
189 | 
190 | ```typescript
191 | enum ErrorCode {
192 |   // Standard JSON-RPC error codes
193 |   ParseError = -32700,
194 |   InvalidRequest = -32600,
195 |   MethodNotFound = -32601,
196 |   InvalidParams = -32602,
197 |   InternalError = -32603
198 | }
199 | ```
200 | 
201 | SDKs and applications can define their own error codes above -32000.
202 | 
203 | Errors are propagated through:
204 | 
205 | * Error responses to requests
206 | * Error events on transports
207 | * Protocol-level error handlers
208 | 
209 | ## Implementation example
210 | 
211 | Here's a basic example of implementing an MCP server:
212 | 
213 | <Tabs>
214 |   <Tab title="TypeScript">
215 |     ```typescript
216 |     import { Server } from "@modelcontextprotocol/sdk/server/index.js";
217 |     import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
218 | 
219 |     const server = new Server({
220 |       name: "example-server",
221 |       version: "1.0.0"
222 |     }, {
223 |       capabilities: {
224 |         resources: {}
225 |       }
226 |     });
227 | 
228 |     // Handle requests
229 |     server.setRequestHandler(ListResourcesRequestSchema, async () => {
230 |       return {
231 |         resources: [
232 |           {
233 |             uri: "example://resource",
234 |             name: "Example Resource"
235 |           }
236 |         ]
237 |       };
238 |     });
239 | 
240 |     // Connect transport
241 |     const transport = new StdioServerTransport();
242 |     await server.connect(transport);
243 |     ```
244 |   </Tab>
245 | 
246 |   <Tab title="Python">
247 |     ```python
248 |     import asyncio
249 |     import mcp.types as types
250 |     from mcp.server import Server
251 |     from mcp.server.stdio import stdio_server
252 | 
253 |     app = Server("example-server")
254 | 
255 |     @app.list_resources()
256 |     async def list_resources() -> list[types.Resource]:
257 |         return [
258 |             types.Resource(
259 |                 uri="example://resource",
260 |                 name="Example Resource"
261 |             )
262 |         ]
263 | 
264 |     async def main():
265 |         async with stdio_server() as streams:
266 |             await app.run(
267 |                 streams[0],
268 |                 streams[1],
269 |                 app.create_initialization_options()
270 |             )
271 | 
272 |     if __name__ == "__main__":
273 |         asyncio.run(main())
274 |     ```
275 |   </Tab>
276 | </Tabs>
277 | 
278 | ## Best practices
279 | 
280 | ### Transport selection
281 | 
282 | 1. **Local communication**
283 |    * Use stdio transport for local processes
284 |    * Efficient for same-machine communication
285 |    * Simple process management
286 | 
287 | 2. **Remote communication**
288 |    * Use SSE for scenarios requiring HTTP compatibility
289 |    * Consider security implications including authentication and authorization
290 | 
291 | ### Message handling
292 | 
293 | 1. **Request processing**
294 |    * Validate inputs thoroughly
295 |    * Use type-safe schemas
296 |    * Handle errors gracefully
297 |    * Implement timeouts
298 | 
299 | 2. **Progress reporting**
300 |    * Use progress tokens for long operations
301 |    * Report progress incrementally
302 |    * Include total progress when known
303 | 
304 | 3. **Error management**
305 |    * Use appropriate error codes
306 |    * Include helpful error messages
307 |    * Clean up resources on errors
308 | 
309 | ## Security considerations
310 | 
311 | 1. **Transport security**
312 |    * Use TLS for remote connections
313 |    * Validate connection origins
314 |    * Implement authentication when needed
315 | 
316 | 2. **Message validation**
317 |    * Validate all incoming messages
318 |    * Sanitize inputs
319 |    * Check message size limits
320 |    * Verify JSON-RPC format
321 | 
322 | 3. **Resource protection**
323 |    * Implement access controls
324 |    * Validate resource paths
325 |    * Monitor resource usage
326 |    * Rate limit requests
327 | 
328 | 4. **Error handling**
329 |    * Don't leak sensitive information
330 |    * Log security-relevant errors
331 |    * Implement proper cleanup
332 |    * Handle DoS scenarios
333 | 
334 | ## Debugging and monitoring
335 | 
336 | 1. **Logging**
337 |    * Log protocol events
338 |    * Track message flow
339 |    * Monitor performance
340 |    * Record errors
341 | 
342 | 2. **Diagnostics**
343 |    * Implement health checks
344 |    * Monitor connection state
345 |    * Track resource usage
346 |    * Profile performance
347 | 
348 | 3. **Testing**
349 |    * Test different transports
350 |    * Verify error handling
351 |    * Check edge cases
352 |    * Load test servers
353 | 
```
Page 2/4FirstPrevNextLast