#
tokens: 18946/50000 2/95 files (page 4/4)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 4 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

--------------------------------------------------------------------------------
/docs/introduction/resources-and-tools.md:
--------------------------------------------------------------------------------

```markdown
  1 | # MCP Atlassian Server: Resources & Tools Reference
  2 | 
  3 | Tài liệu này liệt kê đầy đủ các Resource (truy vấn dữ liệu) và Tool (thao tác) mà MCP Atlassian Server hỗ trợ, kèm endpoint Atlassian API thực tế và thông tin kỹ thuật chi tiết dành cho developers.
  4 | 
  5 | ## Hướng dẫn dành cho Developers
  6 | 
  7 | Tài liệu này cung cấp thông tin chi tiết về implementation, API endpoints, cấu trúc dữ liệu, và các lưu ý kỹ thuật quan trọng để:
  8 | 
  9 | - Hiểu cách Resource và Tool được triển khai
 10 | - Thêm mới hoặc mở rộng Resource/Tool
 11 | - Xử lý các trường hợp đặc biệt (ADF, version conflicts, error handling)
 12 | - Debugging và maintenance
 13 | 
 14 | ## JIRA
 15 | 
 16 | ### 1. Issue
 17 | 
 18 | #### Resource
 19 | | Resource | URI | Mô tả | Atlassian API Endpoint | Dữ liệu trả về |
 20 | |----------|-----|-------|-----------------------|----------------|
 21 | | Issues | `jira://issues` | Danh sách issue | `/rest/api/3/search` | Array của Issue objects |
 22 | | Issue Details | `jira://issues/{issueKey}` | Chi tiết issue | `/rest/api/3/issue/{issueKey}` | Single Issue object |
 23 | | Issue Transitions | `jira://issues/{issueKey}/transitions` | Các transition khả dụng của issue | `/rest/api/3/issue/{issueKey}/transitions` | Array của Transition objects |
 24 | | Issue Comments | `jira://issues/{issueKey}/comments` | Danh sách comment của issue | `/rest/api/3/issue/{issueKey}/comment` | Array của Comment objects |
 25 | 
 26 | #### Tool
 27 | | Tool | Mô tả | Tham số chính | Atlassian API Endpoint | Dữ liệu output |
 28 | |------|-------|---------------|-----------------------|----------------|
 29 | | createIssue | Tạo issue mới | projectKey, summary, ... | `/rest/api/3/issue` | Issue key và ID mới |
 30 | | updateIssue | Cập nhật issue | issueKey, summary, ... | `/rest/api/3/issue/{issueIdOrKey}` | Status của update |
 31 | | transitionIssue | Chuyển trạng thái issue | issueKey, transitionId | `/rest/api/3/issue/{issueIdOrKey}/transitions` | Status của transition |
 32 | | assignIssue | Gán issue cho user | issueKey, accountId | `/rest/api/3/issue/{issueIdOrKey}/assignee` | Status của assignment |
 33 | | addIssuesToBacklog | Đưa issue vào backlog | boardId, issueKeys | `/rest/agile/1.0/backlog/issue` hoặc `/rest/agile/1.0/backlog/{boardId}/issue` | Status của thêm |
 34 | | addIssueToSprint | Đưa issue vào sprint | sprintId, issueKeys | `/rest/agile/1.0/sprint/{sprintId}/issue` | Status của thêm |
 35 | | rankBacklogIssues | Sắp xếp thứ tự issue trong backlog | boardId, issueKeys, rankBeforeIssue, rankAfterIssue | `/rest/agile/1.0/backlog/rank` | Status của sắp xếp |
 36 | 
 37 | ### 2. Project
 38 | 
 39 | #### Resource
 40 | | Resource | URI | Mô tả | Atlassian API Endpoint | Dữ liệu trả về | 
 41 | |----------|-----|-------|-----------------------|----------------|
 42 | | Projects | `jira://projects` | Danh sách project | `/rest/api/3/project` | Array của Project objects |
 43 | | Project Details | `jira://projects/{projectKey}` | Chi tiết project | `/rest/api/3/project/{projectKey}` | Single Project object |
 44 | | Project Roles | `jira://projects/{projectKey}/roles` | Danh sách role của project | `/rest/api/3/project/{projectKey}/role` | Array các role (name, id) |
 45 | 
 46 | ### 3. Board
 47 | 
 48 | #### Resource
 49 | | Resource | URI | Mô tả | Atlassian API Endpoint | Dữ liệu trả về |
 50 | |----------|-----|-------|-----------------------|----------------|
 51 | | Boards | `jira://boards` | Danh sách board | `/rest/agile/1.0/board` | Array của Board objects |
 52 | | Board Details | `jira://boards/{boardId}` | Chi tiết board | `/rest/agile/1.0/board/{boardId}` | Single Board object |
 53 | | Board Issues | `jira://boards/{boardId}/issues` | Danh sách issue trên board | `/rest/agile/1.0/board/{boardId}/issue` | Array của Issue objects |
 54 | | Board Configuration | `jira://boards/{boardId}/configuration` | Cấu hình board | `/rest/agile/1.0/board/{boardId}/configuration` | Board config object |
 55 | | Board Sprints | `jira://boards/{boardId}/sprints` | Danh sách sprint trên board | `/rest/agile/1.0/board/{boardId}/sprint` | Array của Sprint objects |
 56 | 
 57 | ### 4. Sprint
 58 | 
 59 | #### Resource
 60 | | Resource | URI | Mô tả | Atlassian API Endpoint | Dữ liệu trả về |
 61 | |----------|-----|-------|-----------------------|----------------|
 62 | | Sprints | `jira://sprints` | Danh sách tất cả sprints | `/rest/agile/1.0/sprint` | Array của Sprint objects |
 63 | | Sprint Details | `jira://sprints/{sprintId}` | Chi tiết sprint | `/rest/agile/1.0/sprint/{sprintId}` | Single Sprint object |
 64 | | Sprint Issues | `jira://sprints/{sprintId}/issues` | Danh sách issue trong sprint | `/rest/agile/1.0/sprint/{sprintId}/issue` | Array của Issue objects |
 65 | 
 66 | #### Tool
 67 | | Tool | Mô tả | Tham số chính | Atlassian API Endpoint | Dữ liệu output |
 68 | |------|-------|---------------|-----------------------|----------------|
 69 | | createSprint | Tạo sprint mới | boardId, name, ... | `/rest/agile/1.0/sprint` | Sprint ID mới |
 70 | | startSprint | Bắt đầu sprint | sprintId, ... | `/rest/agile/1.0/sprint/{sprintId}/start` | Status của bắt đầu |
 71 | | closeSprint | Đóng sprint | sprintId, ... | `/rest/agile/1.0/sprint/{sprintId}/close` | Status của đóng |
 72 | | addIssueToSprint | Thêm issue vào sprint | sprintId, issueKeys | `/rest/agile/1.0/sprint/{sprintId}/issue` | Status của thêm |
 73 | 
 74 | ### 5. Filter
 75 | 
 76 | #### Resource
 77 | | Resource | URI | Mô tả | Atlassian API Endpoint | Dữ liệu trả về |
 78 | |----------|-----|-------|-----------------------|----------------|
 79 | | Filters | `jira://filters` | Danh sách filter | `/rest/api/3/filter/search` | Array của Filter objects |
 80 | | Filter Details | `jira://filters/{filterId}` | Chi tiết filter | `/rest/api/3/filter/{filterId}` | Single Filter object |
 81 | | My Filters | `jira://filters/my` | Filter của tôi | `/rest/api/3/filter/my` | Array của Filter objects |
 82 | 
 83 | #### Tool
 84 | | Tool | Mô tả | Tham số chính | Atlassian API Endpoint | Dữ liệu output |
 85 | |------|-------|---------------|-----------------------|----------------|
 86 | | createFilter | Tạo filter mới | name, jql, ... | `/rest/api/3/filter` | Filter ID mới |
 87 | | updateFilter | Cập nhật filter | filterId, ... | `/rest/api/3/filter/{filterId}` | Status của update |
 88 | | deleteFilter | Xóa filter | filterId | `/rest/api/3/filter/{filterId}` | Status của xoá |
 89 | 
 90 | ### 6. Dashboard & Gadget
 91 | 
 92 | #### Resource
 93 | | Resource | URI | Mô tả | Atlassian API Endpoint | Dữ liệu trả về |
 94 | |----------|-----|-------|-----------------------|----------------|
 95 | | Dashboards | `jira://dashboards` | Danh sách dashboard | `/rest/api/3/dashboard` | Array của Dashboard objects |
 96 | | My Dashboards | `jira://dashboards/my` | Dashboard của tôi | `/rest/api/3/dashboard?filter=my` | Array của Dashboard objects |
 97 | | Dashboard Details | `jira://dashboards/{dashboardId}` | Chi tiết dashboard | `/rest/api/3/dashboard/{dashboardId}` | Single Dashboard object |
 98 | | Dashboard Gadgets | `jira://dashboards/{dashboardId}/gadgets` | Danh sách gadget trên dashboard | `/rest/api/3/dashboard/{dashboardId}/gadget` | Array của Gadget objects |
 99 | | Gadgets | `jira://gadgets` | Danh sách gadget | `/rest/api/3/dashboard/gadgets` | Array của Gadget objects |
100 | 
101 | #### Tool
102 | | Tool | Mô tả | Tham số chính | Atlassian API Endpoint | Dữ liệu output |
103 | |------|-------|---------------|-----------------------|----------------|
104 | | createDashboard | Tạo dashboard mới | name, ... | `/rest/api/3/dashboard` | Dashboard ID mới |
105 | | updateDashboard | Cập nhật dashboard | dashboardId, ... | `/rest/api/3/dashboard/{dashboardId}` | Status của update |
106 | | addGadgetToDashboard | Thêm gadget vào dashboard | dashboardId, uri, ... | `/rest/api/3/dashboard/{dashboardId}/gadget` | Gadget ID mới |
107 | | removeGadgetFromDashboard | Xóa gadget khỏi dashboard | dashboardId, gadgetId | `/rest/api/3/dashboard/{dashboardId}/gadget/{gadgetId}` | Status của xóa |
108 | 
109 | ### 7. User
110 | 
111 | #### Resource
112 | | Resource | URI | Mô tả | Atlassian API Endpoint | Dữ liệu trả về |
113 | |----------|-----|-------|-----------------------|----------------|
114 | | Users | `jira://users` | Danh sách tất cả user | `/rest/api/3/users/search` | Array của User objects |
115 | | User Details | `jira://users/{accountId}` | Thông tin user | `/rest/api/3/user?accountId=...` | Single User object |
116 | | Assignable Users | `jira://users/assignable/{projectKey}` | User có thể gán cho project | `/rest/api/3/user/assignable/search?project=...` | Array của User objects |
117 | | Users by Role | `jira://users/role/{projectKey}/{roleId}` | User theo role trong project | `/rest/api/3/project/{projectKey}/role/{roleId}` | Array của User objects |
118 | 
119 | ---
120 | 
121 | ## CONFLUENCE
122 | 
123 | ### 1. Space
124 | 
125 | #### Resource
126 | | Resource | URI | Mô tả | Atlassian API Endpoint | Dữ liệu trả về |
127 | |----------|-----|-------|-----------------------|----------------|
128 | | Spaces | `confluence://spaces` | Danh sách space | `/wiki/api/v2/spaces` | Array của Space objects (v2) |
129 | | Space Details | `confluence://spaces/{spaceKey}` | Chi tiết space | `/wiki/api/v2/spaces/{spaceKey}` | Single Space object (v2) |
130 | | Space Pages | `confluence://spaces/{spaceKey}/pages` | Danh sách page trong space | `/wiki/api/v2/pages?space-id=...` | Array của Page objects (v2) |
131 | 
132 | ### 2. Page
133 | 
134 | #### Resource
135 | | Resource | URI | Mô tả | Atlassian API Endpoint | Dữ liệu trả về |
136 | |----------|-----|-------|-----------------------|----------------|
137 | | Pages | `confluence://pages` | Tìm kiếm page theo filter | `/wiki/api/v2/pages` | Array của Page objects (v2) |
138 | | Page Details | `confluence://pages/{pageId}` | Chi tiết page (v2) | `/wiki/api/v2/pages/{pageId}` + `/wiki/api/v2/pages/{pageId}/body` | Single Page object (v2) |
139 | | Page Children | `confluence://pages/{pageId}/children` | Danh sách page con | `/wiki/api/v2/pages/{pageId}/children` | Array của Page objects (v2) |
140 | | Page Ancestors | `confluence://pages/{pageId}/ancestors` | Danh sách ancestor của page | `/wiki/api/v2/pages/{pageId}/ancestors` | Array của Page objects (v2) |
141 | | Page Attachments | `confluence://pages/{pageId}/attachments` | Danh sách file đính kèm | `/wiki/api/v2/pages/{pageId}/attachments` | Array của Attachment objects (v2) |
142 | | Page Versions | `confluence://pages/{pageId}/versions` | Lịch sử version của page | `/wiki/api/v2/pages/{pageId}/versions` | Array của Version objects (v2) |
143 | | Page Labels | `confluence://pages/{pageId}/labels` | Danh sách nhãn của page | `/wiki/api/v2/pages/{pageId}/labels` | Array của Label objects (v2) |
144 | 
145 | #### Tool
146 | | Tool | Mô tả | Tham số chính | Atlassian API Endpoint | Dữ liệu output |
147 | |------|-------|---------------|-----------------------|----------------|
148 | | createPage | Tạo page mới | spaceId, title, content, parentId | `/wiki/api/v2/pages` | Page ID mới |
149 | | updatePage | Cập nhật nội dung page | pageId, title, content, version | `/wiki/api/v2/pages/{pageId}` (PUT) | Status của update |
150 | | updatePageTitle | Đổi tiêu đề page | pageId, title, version | `/wiki/api/v2/pages/{pageId}/title` (PUT) | Status của update |
151 | | deletePage | Xóa page | pageId, draft, purge | `/wiki/api/v2/pages/{pageId}` (DELETE) | Status của xóa |
152 | 
153 | ### 3. Comment
154 | 
155 | #### Resource
156 | | Resource | URI | Mô tả | Atlassian API Endpoint | Dữ liệu trả về |
157 | |----------|-----|-------|-----------------------|----------------|
158 | | Page Comments | `confluence://pages/{pageId}/comments` | Danh sách comment của page | `/wiki/api/v2/pages/{pageId}/footer-comments`, `/wiki/api/v2/pages/{pageId}/inline-comments` | Array của Comment objects (v2) |
159 | 
160 | #### Tool
161 | | Tool | Mô tả | Tham số chính | Atlassian API Endpoint | Dữ liệu output |
162 | |------|-------|---------------|-----------------------|----------------|
163 | | addComment | Thêm comment vào page | pageId, content | `/wiki/api/v2/footer-comments` | Comment mới |
164 | | updateFooterComment | Cập nhật comment ở footer | commentId, version, value, ... | `/wiki/api/v2/footer-comments/{commentId}` (PUT) | Status của update |
165 | | deleteFooterComment | Xóa comment ở footer | commentId | `/wiki/api/v2/footer-comments/{commentId}` (DELETE) | Status của xóa |
166 | 
167 | ---
168 | 
169 | ## Implementation Details: Hướng dẫn mở rộng Resource & Tool cho Developer
170 | 
171 | Khi muốn thêm mới **Resource** (truy vấn dữ liệu) hoặc **Tool** (thao tác/mutation) cho Jira hoặc Confluence, hãy làm theo các bước sau để đảm bảo codebase đồng nhất, dễ bảo trì, mở rộng và tương thích chuẩn MCP SDK:
172 | 
173 | ### 1. Phân biệt Resource và Tool
174 | - **Resource**: Trả về dữ liệu, chỉ đọc (GET), ví dụ: danh sách issue, chi tiết project, các comment, v.v.
175 | - **Tool**: Thực hiện hành động/thao tác (POST/PUT/DELETE), ví dụ: tạo issue, cập nhật filter, thêm comment, v.v.
176 | 
177 | ### 2. Vị trí file
178 | - **Resource**: Thêm/cập nhật file trong:
179 |   - `src/resources/jira/` (cho Jira)
180 |   - `src/resources/confluence/` (cho Confluence)
181 |   - Đăng ký resource mới trong file `index.ts` tương ứng nếu cần.
182 | - **Tool**: Thêm/cập nhật file trong:
183 |   - `src/tools/jira/` (cho Jira)
184 |   - `src/tools/confluence/` (cho Confluence)
185 |   - Đăng ký tool mới trong `src/tools/index.ts`.
186 | 
187 | ### 3. Sử dụng helpers chuẩn hóa
188 | - **Luôn sử dụng helpers mới:**
189 |   - Import `Config` và `Resources` từ `../../utils/mcp-helpers.js`.
190 |   - Không tự gọi fetch/axios trực tiếp trong resource/tool, mà phải dùng các hàm helper trong `src/utils/jira-resource-api.ts`, `src/utils/confluence-resource-api.ts` (resource) hoặc các file tool-api tương ứng.
191 | - **Ví dụ import chuẩn:**
192 |   ```typescript
193 |   import { Config, Resources } from '../../utils/mcp-helpers.js';
194 |   ```
195 | 
196 | ### 4. Đăng ký resource theo chuẩn MCP
197 | - Đăng ký resource qua `server.resource()` với `ResourceTemplate` và callback chuẩn hóa:
198 |   ```typescript
199 |   export function registerYourResource(server: McpServer) {
200 |     server.resource(
201 |       'unique-resource-name',
202 |       new ResourceTemplate('resource://pattern/{param}', {
203 |         list: async (_extra) => ({
204 |           resources: [
205 |             {
206 |               uri: 'resource://pattern/{param}',
207 |               name: 'Resource Name',
208 |               description: 'Resource description',
209 |               mimeType: 'application/json'
210 |             }
211 |           ]
212 |         })
213 |       }),
214 |       async (uri, params, extra) => {
215 |         // Ưu tiên lấy config từ context nếu có, fallback về env
216 |         const config = (extra && typeof extra === 'object' && 'context' in extra && extra.context && (extra.context as any).atlassianConfig)
217 |           ? (extra.context as any).atlassianConfig
218 |           : Config.getAtlassianConfigFromEnv();
219 |         // Xử lý params
220 |         const param = Array.isArray(params.param) ? params.param[0] : params.param;
221 |         // Gọi API helper
222 |         const data = await yourApiFunction(config, param);
223 |         // Chuẩn hóa response
224 |         return Resources.createStandardResource(
225 |           typeof uri === 'string' ? uri : uri.href,
226 |           data.results || [], // hoặc data tuỳ API
227 |           'resourceKey',      // ví dụ: 'issues', 'pages', ...
228 |           yourSchema,
229 |           data.size || (data.results || []).length,
230 |           data.limit || (data.results || []).length,
231 |           0,
232 |           'uiUrl nếu có'
233 |         );
234 |       }
235 |     );
236 |   }
237 |   ```
238 | - **Lưu ý:**
239 |   - Không trả về object tự do, luôn dùng `Resources.createStandardResource` để chuẩn hóa metadata, schema, paging, links.
240 |   - Đảm bảo resource name (tên đầu tiên khi đăng ký) là duy nhất.
241 |   - Không đăng ký trùng URI pattern ở nhiều file.
242 | 
243 | ### 5. Chuẩn hóa schema dữ liệu
244 | - Mỗi resource/tool **bắt buộc phải có schema** validate input/output.
245 | - Thêm/cập nhật schema trong:
246 |   - `src/schemas/jira.ts` (cho Jira)
247 |   - `src/schemas/confluence.ts` (cho Confluence)
248 | - Đảm bảo schema phản ánh đúng dữ liệu thực tế trả về/tạo ra từ Atlassian API.
249 | 
250 | ### 6. Xử lý config và context an toàn
251 | - Luôn ưu tiên lấy config từ context nếu có (khi gọi từ tool hoặc resource lồng nhau), fallback về env:
252 |   ```typescript
253 |   const config = (extra && typeof extra === 'object' && 'context' in extra && extra.context && (extra.context as any).atlassianConfig)
254 |     ? (extra.context as any).atlassianConfig
255 |     : Config.getAtlassianConfigFromEnv();
256 |   ```
257 | - Không hardcode credentials, không truyền config qua params.
258 | 
259 | ### 7. Đăng ký tool theo chuẩn MCP
260 | - Đăng ký tool qua `server.tool()` với schema input rõ ràng, callback chuẩn hóa:
261 |   ```typescript
262 |   export function registerYourTool(server: McpServer) {
263 |     server.tool(
264 |       'tool-name',
265 |       'Tool description',
266 |       {
267 |         type: 'object',
268 |         properties: { param1: { type: 'string' } },
269 |         required: ['param1']
270 |       },
271 |       async (params, context) => {
272 |         const { atlassianConfig } = context;
273 |         const result = await yourToolFunction(atlassianConfig, params);
274 |         return {
275 |           content: [ { type: 'text', text: `Operation completed: ${result}` } ]
276 |         };
277 |       }
278 |     );
279 |   }
280 |   ```
281 | - Đăng ký tool trong `src/tools/index.ts` như resource.
282 | 
283 | ### 7.1 Hướng dẫn chi tiết implement tool Jira (chuẩn mới)
284 | 
285 | Khi cần implement hoặc mở rộng tool Jira (thêm tool mới hoặc sửa tool hiện có), hãy làm theo hướng dẫn chi tiết sau:
286 | 
287 | #### Cấu trúc chung cho tool Jira
288 | ```typescript
289 | // 1. Import helpers mới
290 | import { z } from 'zod';
291 | import { Config, Tools } from '../../utils/mcp-helpers.js';
292 | import { McpResponse, createSuccessResponse, createErrorResponse } from '../../utils/mcp-core.js';
293 | import { Logger } from '../../utils/logger.js';
294 | import { ApiError, ApiErrorType } from '../../utils/error-handler.js';
295 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
296 | // Sử dụng các API helper chuẩn hóa
297 | import { createJiraIssueV3, updateJiraIssueV3 } from '../../utils/jira-tool-api-v3.js';
298 | // hoặc helper API Agile (cho Sprint, Board...)
299 | import { createSprint, updateSprint } from '../../utils/jira-tool-api-agile.js';
300 | 
301 | // 2. Logger chuẩn
302 | const logger = Logger.getLogger('JiraTools:yourTool');
303 | 
304 | // 3. Schema input chuẩn hóa với Zod
305 | export const yourToolSchema = z.object({
306 |   param1: z.string().describe('Parameter 1 description'),
307 |   param2: z.number().optional().describe('Optional parameter description'),
308 |   // các tham số khác...
309 | });
310 | 
311 | // 4. Type cho tham số và kết quả
312 | type YourToolParams = z.infer<typeof yourToolSchema>;
313 | interface YourToolResult {
314 |   id: string;
315 |   key?: string;
316 |   success: boolean;
317 |   message: string;
318 |   // các trường kết quả khác...
319 | }
320 | 
321 | // 5. Hàm handler chính (tách riêng xử lý logic)
322 | export async function yourToolHandler(
323 |   params: YourToolParams,
324 |   config: any
325 | ): Promise<YourToolResult> {
326 |   try {
327 |     logger.info(`Starting yourTool with params: ${params.param1}`);
328 |     
329 |     // Gọi API Jira qua helper chuẩn hóa
330 |     const result = await yourToolApiFunction(config, {
331 |       // Map params sang API params
332 |       param1: params.param1,
333 |       param2: params.param2
334 |     });
335 |     
336 |     // Xử lý kết quả, chuẩn hóa trả về
337 |     return {
338 |       id: result.id,
339 |       key: result.key,
340 |       success: true,
341 |       message: `Operation completed successfully: ${result.id}`
342 |     };
343 |   } catch (error) {
344 |     // Xử lý lỗi chuẩn
345 |     if (error instanceof ApiError) {
346 |       throw error;
347 |     }
348 |     logger.error(`Error in yourTool:`, error);
349 |     throw new ApiError(
350 |       ApiErrorType.SERVER_ERROR,
351 |       `Failed operation: ${error instanceof Error ? error.message : String(error)}`,
352 |       500
353 |     );
354 |   }
355 | }
356 | 
357 | // 6. Hàm đăng ký tool
358 | export const registerYourTool = (server: McpServer) => {
359 |   server.tool(
360 |     'yourTool',
361 |     'Your tool description',
362 |     yourToolSchema.shape,
363 |     async (params: YourToolParams, context: Record<string, any>) => {
364 |       try {
365 |         // Lấy config từ context nếu có, fallback về env
366 |         const config = context?.atlassianConfig ?? Config.getAtlassianConfigFromEnv();
367 |         if (!config) {
368 |           return {
369 |             content: [
370 |               { type: 'text', text: JSON.stringify({
371 |                 success: false,
372 |                 message: 'Invalid or missing Atlassian configuration'
373 |               })}
374 |             ],
375 |             isError: true
376 |           };
377 |         }
378 |         
379 |         // Gọi handler
380 |         const result = await yourToolHandler(params, config);
381 |         
382 |         // Trả về chuẩn JSON trong content[0].text
383 |         return {
384 |           content: [
385 |             {
386 |               type: 'text',
387 |               text: JSON.stringify({
388 |                 success: true,
389 |                 id: result.id,
390 |                 key: result.key,
391 |                 message: result.message
392 |                 // các trường khác...
393 |               })
394 |             }
395 |           ]
396 |         };
397 |       } catch (error) {
398 |         // Xử lý lỗi chuẩn
399 |         if (error instanceof ApiError) {
400 |           return {
401 |             content: [
402 |               {
403 |                 type: 'text',
404 |                 text: JSON.stringify({
405 |                   success: false,
406 |                   message: error.message,
407 |                   code: error.code,
408 |                   statusCode: error.statusCode,
409 |                   type: error.type
410 |                 })
411 |               }
412 |             ],
413 |             isError: true
414 |           };
415 |         }
416 |         return {
417 |           content: [
418 |             {
419 |               type: 'text',
420 |               text: JSON.stringify({
421 |                 success: false,
422 |                 message: `Error: ${error instanceof Error ? error.message : String(error)}`
423 |               })
424 |             }
425 |           ],
426 |           isError: true
427 |         };
428 |       }
429 |     }
430 |   );
431 | };
432 | ```
433 | 
434 | #### Ví dụ cụ thể: Tool createIssue
435 | ```typescript
436 | // src/tools/jira/create-issue.ts
437 | import { z } from 'zod';
438 | import { Config } from '../../utils/mcp-helpers.js';
439 | import { McpResponse, createErrorResponse } from '../../utils/mcp-core.js';
440 | import { createJiraIssueV3 } from '../../utils/jira-tool-api-v3.js';
441 | import { Logger } from '../../utils/logger.js';
442 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
443 | 
444 | const logger = Logger.getLogger('JiraTools:createIssue');
445 | 
446 | // Schema chuẩn hóa
447 | export const createIssueSchema = z.object({
448 |   projectKey: z.string().describe('Jira project key (e.g. DEMO, TES, ...)'),
449 |   summary: z.string().describe('Issue summary'),
450 |   description: z.string().optional().describe('Issue description'),
451 |   issueType: z.string().describe('Issue type (e.g. Task, Bug, Story, ...)'),
452 |   priority: z.string().optional().describe('Issue priority (e.g. Highest, High, Medium, Low, Lowest)'),
453 |   assignee: z.string().optional().describe('Assignee account ID')
454 | });
455 | 
456 | // Main handler
457 | export async function createIssueHandler(params, config) {
458 |   try {
459 |     logger.info(`Creating Jira issue in project: ${params.projectKey}`);
460 |     const issueData = await createJiraIssueV3(config, {
461 |       projectKey: params.projectKey,
462 |       summary: params.summary,
463 |       description: params.description || "",
464 |       issueType: params.issueType,
465 |       priority: params.priority,
466 |       assignee: params.assignee
467 |     });
468 |     
469 |     return {
470 |       id: issueData.id,
471 |       key: issueData.key,
472 |       self: issueData.self,
473 |       success: true
474 |     };
475 |   } catch (error) {
476 |     logger.error(`Error creating Jira issue:`, error);
477 |     throw error;
478 |   }
479 | }
480 | 
481 | // Đăng ký tool
482 | export const registerCreateIssueTool = (server: McpServer) => {
483 |   server.tool(
484 |     'createIssue',
485 |     'Create a new Jira issue',
486 |     createIssueSchema.shape,
487 |     async (params, context) => {
488 |       try {
489 |         const config = context?.atlassianConfig ?? Config.getAtlassianConfigFromEnv();
490 |         const result = await createIssueHandler(params, config);
491 |         return {
492 |           content: [
493 |             {
494 |               type: 'text',
495 |               text: JSON.stringify({
496 |                 id: result.id,
497 |                 key: result.key,
498 |                 self: result.self,
499 |                 success: true
500 |               })
501 |             }
502 |           ]
503 |         };
504 |       } catch (error) {
505 |         return {
506 |           content: [
507 |             {
508 |               type: 'text',
509 |               text: JSON.stringify({
510 |                 success: false,
511 |                 message: error instanceof Error ? error.message : String(error)
512 |               })
513 |             }
514 |           ],
515 |           isError: true
516 |         };
517 |       }
518 |     }
519 |   );
520 | };
521 | ```
522 | 
523 | #### Lưu ý quan trọng khi implement tool Jira
524 | - **Schema chuẩn**: Luôn định nghĩa schema input với Zod, bao gồm mô tả cho mỗi tham số.
525 | - **Response chuẩn**: Trả về object `{ success: true/false, key/id, message, ... }` trong `content[0].text` (JSON string).
526 | - **Error handling**: Xử lý mọi trường hợp lỗi, bao gồm lỗi invalid config, network errors, API errors.
527 | - **Logging**: Sử dụng Logger chuẩn, không log thông tin nhạy cảm.
528 | - **Helper API**: Sử dụng các helper API chuẩn hóa thay vì gọi trực tiếp fetch/axios:
529 |   - `jira-tool-api-v3.js`: Cho các API REST Jira (issue, filter, dashboard, ...)
530 |   - `jira-tool-api-agile.js`: Cho các API Agile (sprint, board, backlog, ...)
531 | - **Không dùng các hàm cũ** từ `tool-helpers.js`, `mcp-response.js`.
532 | 
533 | ### 8. Testing, debugging và backward compatibility
534 | - Luôn test resource/tool mới bằng test client (`dev_mcp-atlassian-test-client`).
535 | - Theo dõi log qua `Logger` để debug dễ dàng.
536 | - Khi refactor, giữ backward compatibility cho client cũ (nếu cần), không đổi format response đột ngột.
537 | 
538 | ### 9. Lưu ý bảo mật
539 | - Không log credentials/API token ra log file.
540 | - Không trả về thông tin nhạy cảm trong response.
541 | - Chỉ expose các endpoint/resource thực sự cần thiết.
542 | 
543 | ### 10. Cập nhật tài liệu
544 | - Sau khi thêm resource/tool mới, cập nhật lại bảng liệt kê resource/tool và schema trong tài liệu này.
545 | - Ghi chú rõ các thay đổi breaking change (nếu có).
546 | 
547 | ---
548 | **Tóm lại:**
549 | - Luôn dùng helpers mới (`Config`, `Resources`), chuẩn hóa response, schema, context.
550 | - Không lặp lại lỗi cũ (trùng resource, trả về object tự do, thiếu schema, hardcode config).
551 | - Ưu tiên bảo mật, dễ mở rộng, dễ bảo trì, tương thích MCP SDK.
```

--------------------------------------------------------------------------------
/docs/dev-guide/confluence-migrate-to-v2.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Hướng Dẫn Migration từ Confluence API v1 sang v2
  2 | 
  3 | Dựa trên thông tin từ kết quả tìm kiếm, API v1 của Confluence sẽ bị loại bỏ trong tương lai gần (nhiều endpoint đã bị đánh dấu deprecated). Vì hiện tại là tháng 5/2025, việc migration là cấp thiết. Dưới đây là hướng dẫn chi tiết để chuyển đổi các resource và tool Confluence trong MCP server của bạn từ API v1 sang API v2.
  4 | 
  5 | ## Nguyên tắc chung khi migration
  6 | 
  7 | 1. Thay đổi base URL từ `/rest/api/` sang `/wiki/api/v2/`
  8 | 2. Sử dụng endpoint chuyên biệt thay vì endpoint chung "content"
  9 | 3. Thay đổi từ phân trang offset-based sang cursor-based
 10 | 4. Thay thế tham số `expand` bằng các API call riêng biệt
 11 | 
 12 | ## Migration cho Resources
 13 | 
 14 | ### 1. Spaces
 15 | 
 16 | **Từ v1:**
 17 | ```typescript
 18 | // Danh sách không gian
 19 | server.resource(
 20 |   "confluence-spaces",
 21 |   new ResourceTemplate("confluence://spaces", { list: undefined }),
 22 |   async (uri) => {
 23 |     const spaces = await confluenceClient.get('/rest/api/space');
 24 |     return {
 25 |       contents: [{
 26 |         uri: uri.href,
 27 |         mimeType: "application/json",
 28 |         text: JSON.stringify({
 29 |           spaces: spaces.results,
 30 |           metadata: createStandardMetadata(spaces.size, spaces.limit, spaces.start, uri.href)
 31 |         }),
 32 |         schema: spaceListSchema
 33 |       }]
 34 |     };
 35 |   }
 36 | );
 37 | ```
 38 | 
 39 | **Sang v2:**
 40 | ```typescript
 41 | // Danh sách không gian
 42 | server.resource(
 43 |   "confluence-spaces",
 44 |   new ResourceTemplate("confluence://spaces", { list: undefined }),
 45 |   async (uri) => {
 46 |     const url = new URL(uri.href);
 47 |     const limit = parseInt(url.searchParams.get("limit") || "25");
 48 |     const cursor = url.searchParams.get("cursor") || undefined;
 49 |     
 50 |     const endpoint = `/wiki/api/v2/spaces${cursor ? `?cursor=${cursor}&limit=${limit}` : `?limit=${limit}`}`;
 51 |     const spaces = await confluenceClient.get(endpoint);
 52 |     
 53 |     return {
 54 |       contents: [{
 55 |         uri: uri.href,
 56 |         mimeType: "application/json",
 57 |         text: JSON.stringify({
 58 |           spaces: spaces.results,
 59 |           metadata: {
 60 |             total: spaces._links.next ? -1 : spaces.results.length, // Total unknown with cursor pagination
 61 |             limit: limit,
 62 |             hasMore: !!spaces._links.next,
 63 |             links: {
 64 |               self: uri.href,
 65 |               next: spaces._links.next ? `${uri.href}?cursor=${new URL(spaces._links.next).searchParams.get("cursor")}&limit=${limit}` : undefined
 66 |             }
 67 |           }
 68 |         }),
 69 |         schema: spaceListSchemaV2
 70 |       }]
 71 |     };
 72 |   }
 73 | );
 74 | ```
 75 | 
 76 | ### 2. Space Details
 77 | 
 78 | **Từ v1:**
 79 | ```typescript
 80 | // Chi tiết không gian
 81 | server.resource(
 82 |   "confluence-space-details",
 83 |   new ResourceTemplate("confluence://spaces/{spaceKey}", { spaceKey: undefined }),
 84 |   async (uri, { spaceKey }) => {
 85 |     const space = await confluenceClient.get(`/rest/api/space/${spaceKey}`);
 86 |     return {
 87 |       contents: [{
 88 |         uri: uri.href,
 89 |         mimeType: "application/json",
 90 |         text: JSON.stringify(space),
 91 |         schema: spaceSchema
 92 |       }]
 93 |     };
 94 |   }
 95 | );
 96 | ```
 97 | 
 98 | **Sang v2:**
 99 | ```typescript
100 | // Chi tiết không gian
101 | server.resource(
102 |   "confluence-space-details",
103 |   new ResourceTemplate("confluence://spaces/{spaceKey}", { spaceKey: undefined }),
104 |   async (uri, { spaceKey }) => {
105 |     const space = await confluenceClient.get(`/wiki/api/v2/spaces/${spaceKey}`);
106 |     return {
107 |       contents: [{
108 |         uri: uri.href,
109 |         mimeType: "application/json",
110 |         text: JSON.stringify(space),
111 |         schema: spaceSchemaV2
112 |       }]
113 |     };
114 |   }
115 | );
116 | ```
117 | 
118 | ### 3. Pages
119 | 
120 | **Từ v1:**
121 | ```typescript
122 | // Danh sách trang
123 | server.resource(
124 |   "confluence-pages",
125 |   new ResourceTemplate("confluence://pages", { list: undefined }),
126 |   async (uri) => {
127 |     const url = new URL(uri.href);
128 |     const limit = parseInt(url.searchParams.get("limit") || "25");
129 |     const start = parseInt(url.searchParams.get("start") || "0");
130 |     
131 |     const pages = await confluenceClient.get(`/rest/api/content/search?type=page&limit=${limit}&start=${start}`);
132 |     
133 |     return {
134 |       contents: [{
135 |         uri: uri.href,
136 |         mimeType: "application/json",
137 |         text: JSON.stringify({
138 |           pages: pages.results,
139 |           metadata: createStandardMetadata(pages.size, pages.limit, pages.start, uri.href)
140 |         }),
141 |         schema: pageListSchema
142 |       }]
143 |     };
144 |   }
145 | );
146 | ```
147 | 
148 | **Sang v2:**
149 | ```typescript
150 | // Danh sách trang
151 | server.resource(
152 |   "confluence-pages",
153 |   new ResourceTemplate("confluence://pages", { list: undefined }),
154 |   async (uri) => {
155 |     const url = new URL(uri.href);
156 |     const limit = parseInt(url.searchParams.get("limit") || "25");
157 |     const cursor = url.searchParams.get("cursor") || undefined;
158 |     
159 |     const endpoint = `/wiki/api/v2/pages${cursor ? `?cursor=${cursor}&limit=${limit}` : `?limit=${limit}`}`;
160 |     const pages = await confluenceClient.get(endpoint);
161 |     
162 |     return {
163 |       contents: [{
164 |         uri: uri.href,
165 |         mimeType: "application/json",
166 |         text: JSON.stringify({
167 |           pages: pages.results,
168 |           metadata: {
169 |             limit: limit,
170 |             hasMore: !!pages._links.next,
171 |             links: {
172 |               self: uri.href,
173 |               next: pages._links.next ? `${uri.href}?cursor=${new URL(pages._links.next).searchParams.get("cursor")}&limit=${limit}` : undefined
174 |             }
175 |           }
176 |         }),
177 |         schema: pageListSchemaV2
178 |       }]
179 |     };
180 |   }
181 | );
182 | ```
183 | 
184 | ### 4. Page Details
185 | 
186 | **Từ v1:**
187 | ```typescript
188 | // Chi tiết trang
189 | server.resource(
190 |   "confluence-page-details",
191 |   new ResourceTemplate("confluence://pages/{pageId}", { pageId: undefined }),
192 |   async (uri, { pageId }) => {
193 |     const page = await confluenceClient.get(`/rest/api/content/${pageId}?expand=body.storage,version`);
194 |     
195 |     return {
196 |       contents: [{
197 |         uri: uri.href,
198 |         mimeType: "application/json",
199 |         text: JSON.stringify({
200 |           ...page,
201 |           body: page.body?.storage?.value || ""
202 |         }),
203 |         schema: pageSchema
204 |       }]
205 |     };
206 |   }
207 | );
208 | ```
209 | 
210 | **Sang v2:**
211 | ```typescript
212 | // Chi tiết trang
213 | server.resource(
214 |   "confluence-page-details",
215 |   new ResourceTemplate("confluence://pages/{pageId}", { pageId: undefined }),
216 |   async (uri, { pageId }) => {
217 |     // Cần 2 API call riêng biệt thay vì dùng expand
218 |     const page = await confluenceClient.get(`/wiki/api/v2/pages/${pageId}`);
219 |     const body = await confluenceClient.get(`/wiki/api/v2/pages/${pageId}/body`);
220 |     
221 |     return {
222 |       contents: [{
223 |         uri: uri.href,
224 |         mimeType: "application/json",
225 |         text: JSON.stringify({
226 |           ...page,
227 |           body: body.value || "",
228 |           bodyType: body.representation || "storage"
229 |         }),
230 |         schema: pageSchemaV2
231 |       }]
232 |     };
233 |   }
234 | );
235 | ```
236 | 
237 | ### 5. Page Children
238 | 
239 | **Từ v1:**
240 | ```typescript
241 | // Danh sách trang con
242 | server.resource(
243 |   "confluence-page-children",
244 |   new ResourceTemplate("confluence://pages/{pageId}/children", { pageId: undefined }),
245 |   async (uri, { pageId }) => {
246 |     const children = await confluenceClient.get(`/rest/api/content/${pageId}/child/page`);
247 |     
248 |     return {
249 |       contents: [{
250 |         uri: uri.href,
251 |         mimeType: "application/json",
252 |         text: JSON.stringify({
253 |           pages: children.results,
254 |           metadata: createStandardMetadata(children.size, children.limit, children.start, uri.href)
255 |         }),
256 |         schema: pageListSchema
257 |       }]
258 |     };
259 |   }
260 | );
261 | ```
262 | 
263 | **Sang v2:**
264 | ```typescript
265 | // Danh sách trang con
266 | server.resource(
267 |   "confluence-page-children",
268 |   new ResourceTemplate("confluence://pages/{pageId}/children", { pageId: undefined }),
269 |   async (uri, { pageId }) => {
270 |     const url = new URL(uri.href);
271 |     const limit = parseInt(url.searchParams.get("limit") || "25");
272 |     const cursor = url.searchParams.get("cursor") || undefined;
273 |     
274 |     const endpoint = `/wiki/api/v2/pages/${pageId}/children${cursor ? `?cursor=${cursor}&limit=${limit}` : `?limit=${limit}`}`;
275 |     const children = await confluenceClient.get(endpoint);
276 |     
277 |     return {
278 |       contents: [{
279 |         uri: uri.href,
280 |         mimeType: "application/json",
281 |         text: JSON.stringify({
282 |           pages: children.results,
283 |           metadata: {
284 |             limit: limit,
285 |             hasMore: !!children._links.next,
286 |             links: {
287 |               self: uri.href,
288 |               next: children._links.next ? `${uri.href}?cursor=${new URL(children._links.next).searchParams.get("cursor")}&limit=${limit}` : undefined
289 |             }
290 |           }
291 |         }),
292 |         schema: pageListSchemaV2
293 |       }]
294 |     };
295 |   }
296 | );
297 | ```
298 | 
299 | ### 6. Page Ancestors
300 | 
301 | **Từ v1:**
302 | ```typescript
303 | // Danh sách tổ tiên
304 | server.resource(
305 |   "confluence-page-ancestors",
306 |   new ResourceTemplate("confluence://pages/{pageId}/ancestors", { pageId: undefined }),
307 |   async (uri, { pageId }) => {
308 |     const page = await confluenceClient.get(`/rest/api/content/${pageId}?expand=ancestors`);
309 |     
310 |     return {
311 |       contents: [{
312 |         uri: uri.href,
313 |         mimeType: "application/json",
314 |         text: JSON.stringify({
315 |           ancestors: page.ancestors || [],
316 |           metadata: createStandardMetadata(page.ancestors?.length || 0, page.ancestors?.length || 0, 0, uri.href)
317 |         }),
318 |         schema: pageListSchema
319 |       }]
320 |     };
321 |   }
322 | );
323 | ```
324 | 
325 | **Sang v2:**
326 | ```typescript
327 | // Danh sách tổ tiên
328 | server.resource(
329 |   "confluence-page-ancestors",
330 |   new ResourceTemplate("confluence://pages/{pageId}/ancestors", { pageId: undefined }),
331 |   async (uri, { pageId }) => {
332 |     const ancestors = await confluenceClient.get(`/wiki/api/v2/pages/${pageId}/ancestors`);
333 |     
334 |     return {
335 |       contents: [{
336 |         uri: uri.href,
337 |         mimeType: "application/json",
338 |         text: JSON.stringify({
339 |           ancestors: ancestors.results,
340 |           metadata: {
341 |             total: ancestors.results.length,
342 |             limit: ancestors.results.length,
343 |             hasMore: false,
344 |             links: {
345 |               self: uri.href
346 |             }
347 |           }
348 |         }),
349 |         schema: pageListSchemaV2
350 |       }]
351 |     };
352 |   }
353 | );
354 | ```
355 | 
356 | ### 7. Page Labels
357 | 
358 | **Từ v1:**
359 | ```typescript
360 | // Nhãn của trang
361 | server.resource(
362 |   "confluence-page-labels",
363 |   new ResourceTemplate("confluence://pages/{pageId}/labels", { pageId: undefined }),
364 |   async (uri, { pageId }) => {
365 |     const labels = await confluenceClient.get(`/rest/api/content/${pageId}/label`);
366 |     
367 |     return {
368 |       contents: [{
369 |         uri: uri.href,
370 |         mimeType: "application/json",
371 |         text: JSON.stringify({
372 |           labels: labels.results,
373 |           metadata: createStandardMetadata(labels.size, labels.limit, labels.start, uri.href)
374 |         }),
375 |         schema: labelListSchema
376 |       }]
377 |     };
378 |   }
379 | );
380 | ```
381 | 
382 | **Sang v2:**
383 | ```typescript
384 | // Nhãn của trang
385 | server.resource(
386 |   "confluence-page-labels",
387 |   new ResourceTemplate("confluence://pages/{pageId}/labels", { pageId: undefined }),
388 |   async (uri, { pageId }) => {
389 |     const url = new URL(uri.href);
390 |     const limit = parseInt(url.searchParams.get("limit") || "25");
391 |     const cursor = url.searchParams.get("cursor") || undefined;
392 |     
393 |     const endpoint = `/wiki/api/v2/pages/${pageId}/labels${cursor ? `?cursor=${cursor}&limit=${limit}` : `?limit=${limit}`}`;
394 |     const labels = await confluenceClient.get(endpoint);
395 |     
396 |     return {
397 |       contents: [{
398 |         uri: uri.href,
399 |         mimeType: "application/json",
400 |         text: JSON.stringify({
401 |           labels: labels.results,
402 |           metadata: {
403 |             limit: limit,
404 |             hasMore: !!labels._links.next,
405 |             links: {
406 |               self: uri.href,
407 |               next: labels._links.next ? `${uri.href}?cursor=${new URL(labels._links.next).searchParams.get("cursor")}&limit=${limit}` : undefined
408 |             }
409 |           }
410 |         }),
411 |         schema: labelListSchemaV2
412 |       }]
413 |     };
414 |   }
415 | );
416 | ```
417 | 
418 | ### 8. Page Attachments
419 | 
420 | **Từ v1:**
421 | ```typescript
422 | // Tập tin đính kèm
423 | server.resource(
424 |   "confluence-page-attachments",
425 |   new ResourceTemplate("confluence://pages/{pageId}/attachments", { pageId: undefined }),
426 |   async (uri, { pageId }) => {
427 |     const attachments = await confluenceClient.get(`/rest/api/content/${pageId}/child/attachment`);
428 |     
429 |     return {
430 |       contents: [{
431 |         uri: uri.href,
432 |         mimeType: "application/json",
433 |         text: JSON.stringify({
434 |           attachments: attachments.results,
435 |           metadata: createStandardMetadata(attachments.size, attachments.limit, attachments.start, uri.href)
436 |         }),
437 |         schema: attachmentListSchema
438 |       }]
439 |     };
440 |   }
441 | );
442 | ```
443 | 
444 | **Sang v2:**
445 | ```typescript
446 | // Tập tin đính kèm
447 | server.resource(
448 |   "confluence-page-attachments",
449 |   new ResourceTemplate("confluence://pages/{pageId}/attachments", { pageId: undefined }),
450 |   async (uri, { pageId }) => {
451 |     const url = new URL(uri.href);
452 |     const limit = parseInt(url.searchParams.get("limit") || "25");
453 |     const cursor = url.searchParams.get("cursor") || undefined;
454 |     
455 |     const endpoint = `/wiki/api/v2/pages/${pageId}/attachments${cursor ? `?cursor=${cursor}&limit=${limit}` : `?limit=${limit}`}`;
456 |     const attachments = await confluenceClient.get(endpoint);
457 |     
458 |     return {
459 |       contents: [{
460 |         uri: uri.href,
461 |         mimeType: "application/json",
462 |         text: JSON.stringify({
463 |           attachments: attachments.results,
464 |           metadata: {
465 |             limit: limit,
466 |             hasMore: !!attachments._links.next,
467 |             links: {
468 |               self: uri.href,
469 |               next: attachments._links.next ? `${uri.href}?cursor=${new URL(attachments._links.next).searchParams.get("cursor")}&limit=${limit}` : undefined
470 |             }
471 |           }
472 |         }),
473 |         schema: attachmentListSchemaV2
474 |       }]
475 |     };
476 |   }
477 | );
478 | ```
479 | 
480 | ### 9. Page Versions
481 | 
482 | **Từ v1:**
483 | ```typescript
484 | // Lịch sử phiên bản
485 | server.resource(
486 |   "confluence-page-versions",
487 |   new ResourceTemplate("confluence://pages/{pageId}/versions", { pageId: undefined }),
488 |   async (uri, { pageId }) => {
489 |     const versions = await confluenceClient.get(`/rest/api/content/${pageId}/version`);
490 |     
491 |     return {
492 |       contents: [{
493 |         uri: uri.href,
494 |         mimeType: "application/json",
495 |         text: JSON.stringify({
496 |           versions: versions.results,
497 |           metadata: createStandardMetadata(versions.size, versions.limit, versions.start, uri.href)
498 |         }),
499 |         schema: versionListSchema
500 |       }]
501 |     };
502 |   }
503 | );
504 | ```
505 | 
506 | **Sang v2:**
507 | ```typescript
508 | // Lịch sử phiên bản
509 | server.resource(
510 |   "confluence-page-versions",
511 |   new ResourceTemplate("confluence://pages/{pageId}/versions", { pageId: undefined }),
512 |   async (uri, { pageId }) => {
513 |     const url = new URL(uri.href);
514 |     const limit = parseInt(url.searchParams.get("limit") || "25");
515 |     const cursor = url.searchParams.get("cursor") || undefined;
516 |     
517 |     const endpoint = `/wiki/api/v2/pages/${pageId}/versions${cursor ? `?cursor=${cursor}&limit=${limit}` : `?limit=${limit}`}`;
518 |     const versions = await confluenceClient.get(endpoint);
519 |     
520 |     return {
521 |       contents: [{
522 |         uri: uri.href,
523 |         mimeType: "application/json",
524 |         text: JSON.stringify({
525 |           versions: versions.results,
526 |           metadata: {
527 |             limit: limit,
528 |             hasMore: !!versions._links.next,
529 |             links: {
530 |               self: uri.href,
531 |               next: versions._links.next ? `${uri.href}?cursor=${new URL(versions._links.next).searchParams.get("cursor")}&limit=${limit}` : undefined
532 |             }
533 |           }
534 |         }),
535 |         schema: versionListSchemaV2
536 |       }]
537 |     };
538 |   }
539 | );
540 | ```
541 | 
542 | ## Migration cho Tools
543 | 
544 | ### 1. createPage
545 | 
546 | **Từ v1:**
547 | ```typescript
548 | // Tạo trang mới
549 | server.tool(
550 |   "createPage",
551 |   z.object({
552 |     spaceKey: z.string().describe("Space key"),
553 |     title: z.string().describe("Page title"),
554 |     content: z.string().describe("Page content (HTML)"),
555 |     parentId: z.string().optional().describe("Parent page ID")
556 |   }),
557 |   async (params) => {
558 |     const payload = {
559 |       type: "page",
560 |       title: params.title,
561 |       space: { key: params.spaceKey },
562 |       body: {
563 |         storage: {
564 |           value: params.content,
565 |           representation: "storage"
566 |         }
567 |       }
568 |     };
569 |     
570 |     if (params.parentId) {
571 |       payload.ancestors = [{ id: params.parentId }];
572 |     }
573 |     
574 |     const response = await confluenceClient.post('/rest/api/content', payload);
575 |     
576 |     return {
577 |       content: [{
578 |         type: "text",
579 |         text: `Page created successfully with ID: ${response.id}`
580 |       }]
581 |     };
582 |   }
583 | );
584 | ```
585 | 
586 | **Sang v2:**
587 | ```typescript
588 | // Tạo trang mới
589 | server.tool(
590 |   "createPage",
591 |   z.object({
592 |     spaceKey: z.string().describe("Space key"),
593 |     title: z.string().describe("Page title"),
594 |     content: z.string().describe("Page content (HTML)"),
595 |     parentId: z.string().optional().describe("Parent page ID")
596 |   }),
597 |   async (params) => {
598 |     const payload = {
599 |       spaceId: params.spaceKey,
600 |       title: params.title,
601 |       body: {
602 |         representation: "storage",
603 |         value: params.content
604 |       }
605 |     };
606 |     
607 |     if (params.parentId) {
608 |       payload.parentId = params.parentId;
609 |     }
610 |     
611 |     const response = await confluenceClient.post('/wiki/api/v2/pages', payload);
612 |     
613 |     return {
614 |       content: [{
615 |         type: "text",
616 |         text: `Page created successfully with ID: ${response.id}`
617 |       }]
618 |     };
619 |   }
620 | );
621 | ```
622 | 
623 | ### 2. updatePage
624 | 
625 | **Từ v1:**
626 | ```typescript
627 | // Cập nhật trang
628 | server.tool(
629 |   "updatePage",
630 |   z.object({
631 |     pageId: z.string().describe("Page ID"),
632 |     title: z.string().optional().describe("New page title"),
633 |     content: z.string().optional().describe("New page content (HTML)"),
634 |     version: z.number().describe("Current page version"),
635 |     addLabels: z.array(z.string()).optional().describe("Labels to add"),
636 |     removeLabels: z.array(z.string()).optional().describe("Labels to remove")
637 |   }),
638 |   async (params) => {
639 |     // Get current page
640 |     const currentPage = await confluenceClient.get(`/rest/api/content/${params.pageId}?expand=version`);
641 |     
642 |     // Update page content
643 |     const payload = {
644 |       type: "page",
645 |       title: params.title || currentPage.title,
646 |       version: {
647 |         number: params.version + 1
648 |       }
649 |     };
650 |     
651 |     if (params.content) {
652 |       payload.body = {
653 |         storage: {
654 |           value: params.content,
655 |           representation: "storage"
656 |         }
657 |       };
658 |     }
659 |     
660 |     const response = await confluenceClient.put(`/rest/api/content/${params.pageId}`, payload);
661 |     
662 |     // Add labels if specified
663 |     if (params.addLabels && params.addLabels.length > 0) {
664 |       const labelObjects = params.addLabels.map(label => ({ name: label }));
665 |       await confluenceClient.post(`/rest/api/content/${params.pageId}/label`, labelObjects);
666 |     }
667 |     
668 |     // Remove labels if specified
669 |     if (params.removeLabels && params.removeLabels.length > 0) {
670 |       for (const label of params.removeLabels) {
671 |         await confluenceClient.delete(`/rest/api/content/${params.pageId}/label?name=${label}`);
672 |       }
673 |     }
674 |     
675 |     return {
676 |       content: [{
677 |         type: "text",
678 |         text: `Page ${params.pageId} updated successfully to version ${response.version.number}`
679 |       }]
680 |     };
681 |   }
682 | );
683 | ```
684 | 
685 | **Sang v2:**
686 | ```typescript
687 | // Cập nhật trang
688 | server.tool(
689 |   "updatePage",
690 |   z.object({
691 |     pageId: z.string().describe("Page ID"),
692 |     title: z.string().optional().describe("New page title"),
693 |     content: z.string().optional().describe("New page content (HTML)"),
694 |     version: z.number().describe("Current page version"),
695 |     addLabels: z.array(z.string()).optional().describe("Labels to add"),
696 |     removeLabels: z.array(z.string()).optional().describe("Labels to remove")
697 |   }),
698 |   async (params) => {
699 |     // Update page title if specified
700 |     if (params.title) {
701 |       await confluenceClient.put(`/wiki/api/v2/pages/${params.pageId}`, {
702 |         id: params.pageId,
703 |         status: "current",
704 |         title: params.title,
705 |         version: {
706 |           number: params.version + 1
707 |         }
708 |       });
709 |     }
710 |     
711 |     // Update page content if specified
712 |     if (params.content) {
713 |       await confluenceClient.put(`/wiki/api/v2/pages/${params.pageId}/body`, {
714 |         representation: "storage",
715 |         value: params.content,
716 |         version: {
717 |           number: params.version + (params.title ? 2 : 1)
718 |         }
719 |       });
720 |     }
721 |     
722 |     // Add labels if specified
723 |     if (params.addLabels && params.addLabels.length > 0) {
724 |       const labelObjects = params.addLabels.map(label => ({ name: label }));
725 |       await confluenceClient.post(`/wiki/api/v2/pages/${params.pageId}/labels`, labelObjects);
726 |     }
727 |     
728 |     // Remove labels if specified
729 |     if (params.removeLabels && params.removeLabels.length > 0) {
730 |       for (const label of params.removeLabels) {
731 |         await confluenceClient.delete(`/wiki/api/v2/pages/${params.pageId}/labels/${label}`);
732 |       }
733 |     }
734 |     
735 |     return {
736 |       content: [{
737 |         type: "text",
738 |         text: `Page ${params.pageId} updated successfully`
739 |       }]
740 |     };
741 |   }
742 | );
743 | ```
744 | 
745 | ### 3. addComment
746 | 
747 | **Từ v1:**
748 | ```typescript
749 | // Thêm comment vào page
750 | server.tool(
751 |   "addComment",
752 |   z.object({
753 |     pageId: z.string().describe("Page ID"),
754 |     content: z.string().describe("Comment content (HTML)")
755 |   }),
756 |   async (params) => {
757 |     const payload = {
758 |       type: "comment",
759 |       container: {
760 |         id: params.pageId,
761 |         type: "page"
762 |       },
763 |       body: {
764 |         storage: {
765 |           value: params.content,
766 |           representation: "storage"
767 |         }
768 |       }
769 |     };
770 |     
771 |     const response = await confluenceClient.post('/rest/api/content', payload);
772 |     
773 |     return {
774 |       content: [{
775 |         type: "text",
776 |         text: `Comment added successfully with ID: ${response.id}`
777 |       }]
778 |     };
779 |   }
780 | );
781 | ```
782 | 
783 | **Sang v2:**
784 | ```typescript
785 | // Thêm comment vào page
786 | server.tool(
787 |   "addComment",
788 |   z.object({
789 |     pageId: z.string().describe("Page ID"),
790 |     content: z.string().describe("Comment content (HTML)")
791 |   }),
792 |   async (params) => {
793 |     const payload = {
794 |       body: {
795 |         representation: "storage",
796 |         value: params.content
797 |       }
798 |     };
799 |     
800 |     const response = await confluenceClient.post(`/wiki/api/v2/pages/${params.pageId}/comments`, payload);
801 |     
802 |     return {
803 |       content: [{
804 |         type: "text",
805 |         text: `Comment added successfully with ID: ${response.id}`
806 |       }]
807 |     };
808 |   }
809 | );
810 | ```
811 | 
812 | ### 4. addLabelsToPage
813 | 
814 | **Từ v1:**
815 | ```typescript
816 | // Thêm nhãn vào trang
817 | server.tool(
818 |   "addLabelsToPage",
819 |   z.object({
820 |     pageId: z.string().describe("Page ID"),
821 |     labels: z.array(z.string()).describe("Labels to add")
822 |   }),
823 |   async (params) => {
824 |     const labelObjects = params.labels.map(label => ({ name: label }));
825 |     
826 |     await confluenceClient.post(`/rest/api/content/${params.pageId}/label`, labelObjects);
827 |     
828 |     return {
829 |       content: [{
830 |         type: "text",
831 |         text: `Labels added to page ${params.pageId} successfully`
832 |       }]
833 |     };
834 |   }
835 | );
836 | ```
837 | 
838 | **Sang v2:**
839 | ```typescript
840 | // Thêm nhãn vào trang
841 | server.tool(
842 |   "addLabelsToPage",
843 |   z.object({
844 |     pageId: z.string().describe("Page ID"),
845 |     labels: z.array(z.string()).describe("Labels to add")
846 |   }),
847 |   async (params) => {
848 |     const labelObjects = params.labels.map(label => ({ name: label }));
849 |     
850 |     await confluenceClient.post(`/wiki/api/v2/pages/${params.pageId}/labels`, labelObjects);
851 |     
852 |     return {
853 |       content: [{
854 |         type: "text",
855 |         text: `Labels added to page ${params.pageId} successfully`
856 |       }]
857 |     };
858 |   }
859 | );
860 | ```
861 | 
862 | ### 5. removeLabelsFromPage
863 | 
864 | **Từ v1:**
865 | ```typescript
866 | // Xóa nhãn khỏi trang
867 | server.tool(
868 |   "removeLabelsFromPage",
869 |   z.object({
870 |     pageId: z.string().describe("Page ID"),
871 |     labels: z.array(z.string()).describe("Labels to remove")
872 |   }),
873 |   async (params) => {
874 |     for (const label of params.labels) {
875 |       await confluenceClient.delete(`/rest/api/content/${params.pageId}/label?name=${label}`);
876 |     }
877 |     
878 |     return {
879 |       content: [{
880 |         type: "text",
881 |         text: `Labels removed from page ${params.pageId} successfully`
882 |       }]
883 |     };
884 |   }
885 | );
886 | ```
887 | 
888 | **Sang v2:**
889 | ```typescript
890 | // Xóa nhãn khỏi trang
891 | server.tool(
892 |   "removeLabelsFromPage",
893 |   z.object({
894 |     pageId: z.string().describe("Page ID"),
895 |     labels: z.array(z.string()).describe("Labels to remove")
896 |   }),
897 |   async (params) => {
898 |     for (const label of params.labels) {
899 |       await confluenceClient.delete(`/wiki/api/v2/pages/${params.pageId}/labels/${label}`);
900 |     }
901 |     
902 |     return {
903 |       content: [{
904 |         type: "text",
905 |         text: `Labels removed from page ${params.pageId} successfully`
906 |       }]
907 |     };
908 |   }
909 | );
910 | ```
911 | 
912 | ## Cập nhật Schema
913 | 
914 | Cần cập nhật các schema để phù hợp với cấu trúc dữ liệu mới từ API v2:
915 | 
916 | ```typescript
917 | // Schema cho Space v2
918 | const spaceSchemaV2 = {
919 |   type: "object",
920 |   properties: {
921 |     id: { type: "string", description: "Space ID" },
922 |     key: { type: "string", description: "Space key" },
923 |     name: { type: "string", description: "Space name" },
924 |     type: { type: "string", description: "Space type" },
925 |     status: { type: "string", description: "Space status" },
926 |     description: { 
927 |       type: "object", 
928 |       properties: {
929 |         plain: { type: "object", properties: { value: { type: "string" } } },
930 |         view: { type: "object", properties: { value: { type: "string" } } }
931 |       }
932 |     },
933 |     _links: { type: "object", description: "Links related to the space" }
934 |   }
935 | };
936 | 
937 | // Schema cho Page v2
938 | const pageSchemaV2 = {
939 |   type: "object",
940 |   properties: {
941 |     id: { type: "string", description: "Page ID" },
942 |     title: { type: "string", description: "Page title" },
943 |     status: { type: "string", description: "Page status" },
944 |     spaceId: { type: "string", description: "Space ID" },
945 |     parentId: { type: "string", description: "Parent page ID" },
946 |     authorId: { type: "string", description: "Author ID" },
947 |     createdAt: { type: "string", description: "Creation date" },
948 |     version: { 
949 |       type: "object", 
950 |       properties: {
951 |         number: { type: "number", description: "Version number" },
952 |         createdAt: { type: "string", description: "Version creation date" }
953 |       }
954 |     },
955 |     body: { type: "string", description: "Page content (converted from body object)" },
956 |     bodyType: { type: "string", description: "Content representation type" },
957 |     _links: { type: "object", description: "Links related to the page" }
958 |   }
959 | };
960 | 
961 | // Các schema khác cũng cần được cập nhật tương tự
962 | ```
963 | 
964 | ## Lưu ý quan trọng
965 | 
966 | 1. **Phân trang cursor-based**: API v2 sử dụng cursor-based pagination thay vì offset-based, nên cần thay đổi cách xử lý phân trang.
967 | 
968 | 2. **Không có tham số expand**: Thay vì dùng `expand`, cần gọi các API riêng biệt (ví dụ: để lấy nội dung trang, cần gọi `/wiki/api/v2/pages/{id}/body`).
969 | 
970 | 3. **Cấu trúc dữ liệu khác biệt**: Cấu trúc JSON trả về từ API v2 khác với v1, cần điều chỉnh schema và xử lý dữ liệu.
971 | 
972 | 4. **Xử lý version**: Trong API v2, việc cập nhật title và body là các hoạt động riêng biệt, mỗi hoạt động tăng version number.
973 | 
974 | 5. **Hiệu năng tốt hơn**: API v2 có hiệu năng tốt hơn đáng kể, đặc biệt với các tập dữ liệu lớn.
975 | 
976 | 6. **Deadline migration**: Dựa vào kết quả tìm kiếm, nhiều endpoint v1 đã bị đánh dấu deprecated và sẽ bị loại bỏ. Vì hiện tại đã là tháng 5/2025, việc migration là cấp thiết.
977 | 
978 | Bằng cách tuân theo hướng dẫn này, bạn có thể chuyển đổi thành công các resource và tool Confluence trong MCP server từ API v1 sang API v2, đảm bảo tính tương thích lâu dài và tận dụng các cải tiến hiệu năng của API mới.
979 | 
980 | > **Lưu ý:** Từ tháng 5/2025, MCP Server chỉ hỗ trợ Confluence API v2. Nếu còn sử dụng API v1, bạn sẽ không thể truy cập resource/tool liên quan Confluence.
```
Page 4/4FirstPrevNextLast