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 |
```