#
tokens: 25037/50000 4/95 files (page 3/3)
lines: off (toggle) GitHub
raw markdown copy
This is page 3 of 3. Use http://codebase.md/phuc-nt/mcp-atlassian-server?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

--------------------------------------------------------------------------------
/dev_mcp-atlassian-test-client/src/tool-test.ts:
--------------------------------------------------------------------------------

```typescript
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import path from "path";
import { fileURLToPath } from "url";
import fs from "fs";

// Get current file path
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

// Load environment variables from .env
function loadEnv(): Record<string, string> {
  try {
    const envFile = path.resolve(process.cwd(), '.env');
    const envContent = fs.readFileSync(envFile, 'utf8');
    const envVars: Record<string, string> = {};
    envContent.split('\n').forEach(line => {
      if (line.trim().startsWith('#') || !line.trim()) return;
      const [key, ...valueParts] = line.split('=');
      if (key && valueParts.length > 0) {
        const value = valueParts.join('=');
        envVars[key.trim()] = value.trim();
      }
    });
    return envVars;
  } catch (error) {
    console.error("Error loading .env file:", error);
    return {};
  }
}

async function main() {
  try {
    console.log("=== MCP Atlassian Tool Test (Refactored) ===");
    const envVars = loadEnv();
    const client = new Client({ name: "mcp-atlassian-test-client", version: "1.0.0" });
    const serverPath = "/Users/phucnt/Workspace/mcp-atlassian-server/dist/index.js";
    const processEnv: Record<string, string> = {};
    Object.keys(process.env).forEach(key => {
      if (process.env[key] !== undefined) {
        processEnv[key] = process.env[key] as string;
      }
    });
    const transport = new StdioClientTransport({
      command: "node",
      args: [serverPath],
      env: {
        ...processEnv,
        ...envVars
      }
    });
    await client.connect(transport);
    console.log("Connected to MCP server\n");

    // === Jira Tools ===
    console.log("--- Jira Tool Tests ---");
    const jiraProjectKey = "XDEMO2";
    // 1. createIssue
    const newIssueSummary = `Test Issue ${new Date().toLocaleString()}`;
    const createIssueResult = await client.callTool({
      name: "createIssue",
      arguments: {
        projectKey: jiraProjectKey,
        summary: newIssueSummary,
        description: "Test issue created by MCP tool-test",
        issueType: "Task"
      }
    });
    console.log("createIssueResult (raw):", createIssueResult);
    let createIssueObj = createIssueResult;
    if (
      createIssueObj.content &&
      Array.isArray(createIssueObj.content) &&
      typeof createIssueObj.content[0]?.text === 'string'
    ) {
      createIssueObj = JSON.parse(createIssueObj.content[0].text);
      console.log("createIssueResult (parsed):", createIssueObj);
    }
    console.log("createIssue:", createIssueObj.key ? "✅" : "❌", createIssueObj.key || "Unknown");
    const newIssueKey = createIssueObj.key;

    // 2. updateIssue
    if (newIssueKey) {
      const updateIssueResult = await client.callTool({
        name: "updateIssue",
        arguments: {
          issueIdOrKey: newIssueKey,
          summary: `${newIssueSummary} (Updated)`
        }
      });
      console.log("updateIssueResult (raw):", updateIssueResult);
      let updateIssueObj = updateIssueResult;
      if (
        updateIssueObj.content &&
        Array.isArray(updateIssueObj.content) &&
        typeof updateIssueObj.content[0]?.text === 'string'
      ) {
        updateIssueObj = JSON.parse(updateIssueObj.content[0].text);
        console.log("updateIssueResult (parsed):", updateIssueObj);
      }
      console.log("updateIssue:", updateIssueObj.success ? "✅" : "❌");
    }

    // 3. assignIssue
    if (newIssueKey) {
      const assignIssueResult = await client.callTool({
        name: "assignIssue",
        arguments: {
          issueIdOrKey: newIssueKey,
          accountId: ""
        }
      });
      console.log("assignIssueResult (raw):", assignIssueResult);
      let assignIssueObj = assignIssueResult;
      if (
        assignIssueObj.content &&
        Array.isArray(assignIssueObj.content) &&
        typeof assignIssueObj.content[0]?.text === 'string'
      ) {
        assignIssueObj = JSON.parse(assignIssueObj.content[0].text);
        console.log("assignIssueResult (parsed):", assignIssueObj);
      }
      console.log("assignIssue:", assignIssueObj.success ? "✅" : "❌");
    }

    // 4. transitionIssue
    if (newIssueKey) {
      const transitionIssueResult = await client.callTool({
        name: "transitionIssue",
        arguments: {
          issueIdOrKey: newIssueKey,
          transitionId: "11",
          comment: "Test transition"
        }
      });
      console.log("transitionIssueResult (raw):", transitionIssueResult);
      let transitionIssueObj = transitionIssueResult;
      if (
        transitionIssueObj.content &&
        Array.isArray(transitionIssueObj.content) &&
        typeof transitionIssueObj.content[0]?.text === 'string'
      ) {
        transitionIssueObj = JSON.parse(transitionIssueObj.content[0].text);
        console.log("transitionIssueResult (parsed):", transitionIssueObj);
      }
      console.log("transitionIssue:", transitionIssueObj.success ? "✅" : "❌");
    }

    // 5. createSprint (nếu có boardId)
    let boardId = null;
    try {
      const boardsResult = await client.readResource({ uri: `jira://boards` });
      if (boardsResult.contents && boardsResult.contents[0].text) {
        const boardsData = JSON.parse(String(boardsResult.contents[0].text));
        if (boardsData && boardsData.boards && boardsData.boards.length > 0) {
          for (const board of boardsData.boards) {
            if (board.type === "scrum") {
              boardId = board.id;
              break;
            }
          }
        }
      }
    } catch {}
    let newSprintId = null;
    if (boardId) {
      try {
        const createSprintResult = await client.callTool({
          name: "createSprint",
          arguments: {
            boardId: String(boardId),
            name: `Sprint-${Date.now()}`.substring(0, 25),
            goal: "Test sprint created by MCP tool-test"
          }
        });
        console.log("createSprintResult (raw):", createSprintResult);
        let createSprintObj = createSprintResult;
        if (
          createSprintObj.content &&
          Array.isArray(createSprintObj.content) &&
          typeof createSprintObj.content[0]?.text === 'string'
        ) {
          createSprintObj = JSON.parse(createSprintObj.content[0].text);
          console.log("createSprintResult (parsed):", createSprintObj);
        }
        console.log("createSprint:", createSprintObj.id ? "✅" : "❌", createSprintObj.id || "Unknown");
        newSprintId = createSprintObj.id;
      } catch (e) {
        console.log("createSprint: ❌", e instanceof Error ? e.message : String(e));
      }
    }

    // 6. createFilter
    const createFilterResult = await client.callTool({
      name: "createFilter",
      arguments: {
        name: `Test Filter ${Date.now()}`,
        jql: "project = XDEMO2 ORDER BY created DESC",
        description: "Test filter created by MCP tool-test",
        favourite: false
      }
    });
    console.log("createFilterResult (raw):", createFilterResult);
    let createFilterObj = createFilterResult;
    if (
      createFilterObj.content &&
      Array.isArray(createFilterObj.content) &&
      typeof createFilterObj.content[0]?.text === 'string'
    ) {
      createFilterObj = JSON.parse(createFilterObj.content[0].text);
      console.log("createFilterResult (parsed):", createFilterObj);
    }
    console.log("createFilter:", createFilterObj.id ? "✅" : "❌", createFilterObj.id || "Unknown");

    // 7. createDashboard
    const createDashboardResult = await client.callTool({
      name: "createDashboard",
      arguments: {
        name: `Dashboard-${Date.now()}`,
        description: "Test dashboard created by MCP tool-test"
      }
    });
    console.log("createDashboardResult (raw):", createDashboardResult);
    let createDashboardObj = createDashboardResult;
    if (
      createDashboardObj.content &&
      Array.isArray(createDashboardObj.content) &&
      typeof createDashboardObj.content[0]?.text === 'string'
    ) {
      createDashboardObj = JSON.parse(createDashboardObj.content[0].text);
      console.log("createDashboardResult (parsed):", createDashboardObj);
    }
    console.log("createDashboard:", createDashboardObj.id ? "✅" : "❌", createDashboardObj.id || "Unknown");

    // === Confluence Tools ===
    console.log("\n--- Confluence Tool Tests ---");
    // const confluenceSpaceKey = "AWA1";
    // let spaceId: string | null = null;
    // let parentId: string | null = null;
    // Lấy đúng spaceId (số) từ resource confluence://spaces/AWA1
    // try {
    //   const spaceResult = await client.readResource({ uri: `confluence://spaces/${confluenceSpaceKey}` });
    //   if (spaceResult.contents && spaceResult.contents[0].text) {
    //     const data = JSON.parse(String(spaceResult.contents[0].text));
    //     console.log("spaceResult data:", data);
    //     spaceId = data.id || data.spaceId || (data.space && data.space.id) || null;
    //     console.log(`Using spaceId for createPage: ${spaceId}`);
    //   }
    // } catch (e) {
    //   console.log("Error fetching spaceId:", e instanceof Error ? e.message : String(e));
    // }
    // Sử dụng trực tiếp spaceId số
    const confluenceSpaceId = "19464200";
    let spaceId: string | null = confluenceSpaceId;
    let parentId: string | null = null;
    // Lấy parentId là page đầu tiên trong resource confluence://spaces/19464200/pages
    try {
      const pagesResult = await client.readResource({ uri: `confluence://spaces/${confluenceSpaceId}/pages` });
      if (pagesResult.contents && pagesResult.contents[0].text) {
        const data = JSON.parse(String(pagesResult.contents[0].text));
        if (data.pages && data.pages.length > 0) {
          parentId = data.pages[0].id;
          console.log(`Using parentId for createPage: ${parentId}`);
        }
      }
    } catch (e) {
      console.log("Error fetching parentId:", e instanceof Error ? e.message : String(e));
    }
    const newPageTitle = `Test Page ${new Date().toLocaleString()}`;
    let newPageId: string | null = null;
    if (spaceId && parentId) {
      try {
          const createPageResult = await client.callTool({
            name: "createPage",
            arguments: {
            spaceId: spaceId,
            parentId: parentId,
              title: newPageTitle,
              content: "<p>This is a test page created by MCP tool-test</p>"
            }
          });
        console.log("createPageResult (raw):", createPageResult);
        let createPageObj = createPageResult;
        if (
          createPageObj.content &&
          Array.isArray(createPageObj.content) &&
          typeof createPageObj.content[0]?.text === 'string'
        ) {
          createPageObj = JSON.parse(createPageObj.content[0].text);
          console.log("createPageResult (parsed):", createPageObj);
        }
        console.log("createPage:", createPageObj.id ? "✅" : "❌", createPageObj.id || "Unknown");
        if (createPageObj && createPageObj.id) newPageId = String(createPageObj.id);
      } catch (e) {
        console.log("createPage: ❌", e instanceof Error ? e.message : String(e));
      }
    } else {
      console.log("Skip createPage: No spaceId or parentId available");
    }
    // 2. updatePage
    if (newPageId) {
      try {
        const updatePageResult = await client.callTool({
          name: "updatePage",
          arguments: {
            pageId: newPageId,
            title: `${newPageTitle} (Updated)`,
            content: "<p>This page has been updated by MCP tool-test</p>",
            version: 1
          }
        });
        console.log("updatePageResult (raw):", updatePageResult);
        let updatePageObj = updatePageResult;
        if (
          updatePageObj.content &&
          Array.isArray(updatePageObj.content) &&
          typeof updatePageObj.content[0]?.text === 'string'
        ) {
          updatePageObj = JSON.parse(updatePageObj.content[0].text);
          console.log("updatePageResult (parsed):", updatePageObj);
        }
        console.log("updatePage:", updatePageObj.success ? "✅" : "❌");
      } catch (e) {
        console.log("updatePage: ❌", e instanceof Error ? e.message : String(e));
      }
    }
    // 3. addComment
    if (newPageId) {
      try {
        const addCommentResult = await client.callTool({
          name: "addComment",
          arguments: {
            pageId: newPageId,
            content: "<p>This is a test comment added by MCP tool-test</p>"
          }
        });
        console.log("addCommentResult (raw):", addCommentResult);
        let addCommentObj = addCommentResult;
        if (
          addCommentObj.content &&
          Array.isArray(addCommentObj.content) &&
          typeof addCommentObj.content[0]?.text === 'string'
        ) {
          addCommentObj = JSON.parse(addCommentObj.content[0].text);
          console.log("addCommentResult (parsed):", addCommentObj);
        }
        console.log("addComment:", addCommentObj.id ? "✅" : "❌");
      } catch (e) {
        console.log("addComment: ❌", e instanceof Error ? e.message : String(e));
      }
    }
    // 4. updatePageTitle
    if (newPageId) {
      try {
        const updatePageTitleResult = await client.callTool({
          name: "updatePageTitle",
          arguments: {
            pageId: newPageId,
            title: `${newPageTitle} (Title Updated)`,
            version: 2
          }
        });
        console.log("updatePageTitleResult (raw):", updatePageTitleResult);
        let updatePageTitleObj = updatePageTitleResult;
        if (
          updatePageTitleObj.content &&
          Array.isArray(updatePageTitleObj.content) &&
          typeof updatePageTitleObj.content[0]?.text === 'string'
        ) {
          updatePageTitleObj = JSON.parse(updatePageTitleObj.content[0].text);
          console.log("updatePageTitleResult (parsed):", updatePageTitleObj);
        }
        console.log("updatePageTitle:", updatePageTitleObj.success ? "✅" : "❌");
      } catch (e) {
        console.log("updatePageTitle: ❌", e instanceof Error ? e.message : String(e));
      }
    }
    // 5. deletePage
    if (newPageId) {
      try {
        const deletePageResult = await client.callTool({
          name: "deletePage",
          arguments: {
            pageId: newPageId
          }
        });
        console.log("deletePageResult (raw):", deletePageResult);
        let deletePageObj = deletePageResult;
        if (
          deletePageObj.content &&
          Array.isArray(deletePageObj.content) &&
          typeof deletePageObj.content[0]?.text === 'string'
        ) {
          deletePageObj = JSON.parse(deletePageObj.content[0].text);
          console.log("deletePageResult (parsed):", deletePageObj);
        }
        console.log("deletePage:", deletePageObj.success ? "✅" : "❌");
      } catch (e) {
        console.log("deletePage: ❌", e instanceof Error ? e.message : String(e));
      }
    }

    // === Resource Test ===
    console.log("\n--- Resource Test ---");
    // Jira resource
    try {
      const issuesResult = await client.readResource({ uri: "jira://issues" });
      if (issuesResult.contents && issuesResult.contents[0].text) {
        const data = JSON.parse(String(issuesResult.contents[0].text));
        console.log("jira://issues response: total issues:", data.metadata?.total ?? data.issues?.length ?? "?");
      } else {
        console.log("No content returned for jira://issues");
      }
    } catch (e) {
      console.log("Error reading jira://issues:", e instanceof Error ? e.message : String(e));
    }
    // Confluence resource
    try {
      const pagesResult = await client.readResource({ uri: `confluence://spaces/${confluenceSpaceId}/pages` });
      if (pagesResult.contents && pagesResult.contents[0].text) {
        const data = JSON.parse(String(pagesResult.contents[0].text));
        console.log("confluence://spaces/19464200/pages response: total pages:", data.metadata?.total ?? data.pages?.length ?? "?");
    } else {
        console.log("No content returned for confluence://spaces/19464200/pages");
      }
    } catch (e) {
      console.log("Error reading confluence://spaces/19464200/pages:", e instanceof Error ? e.message : String(e));
    }

    // Summary
    console.log("\n=== Tool Test Summary ===");
    console.log("All important tools and resources have been tested!");
    await client.close();
    console.log("Connection closed successfully");
  } catch (error) {
    console.error("Error:", error);
  }
}

main();
```

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

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

// Helper: Fetch Jira create metadata for a project/issueType
export async function fetchJiraCreateMeta(
  config: AtlassianConfig,
  projectKey: string,
  issueType: string
): Promise<Record<string, any>> {
  const headers = createBasicHeaders(config.email, config.apiToken);
  const baseUrl = normalizeAtlassianBaseUrl(config.baseUrl);
  // Lấy metadata cho project và issueType (dùng name hoặc id)
  const url = `${baseUrl}/rest/api/3/issue/createmeta?projectKeys=${encodeURIComponent(projectKey)}&issuetypeNames=${encodeURIComponent(issueType)}&expand=projects.issuetypes.fields`;
  const response = await fetch(url, { headers, credentials: 'omit' });
  if (!response.ok) {
    const responseText = await response.text();
    logger.error(`Jira API error (createmeta, ${response.status}):`, responseText);
    throw new Error(`Jira API error (createmeta): ${response.status} ${responseText}`);
  }
  const meta = await response.json();
  // Trả về object các trường hợp lệ
  try {
    const fields = meta.projects?.[0]?.issuetypes?.[0]?.fields || {};
    return fields;
  } catch (e) {
    logger.error('Cannot parse createmeta fields', e);
    return {};
  }
}

// Create a new Jira issue (fix: chỉ gửi các trường có trong createmeta)
export async function createIssue(
  config: AtlassianConfig,
  projectKey: string,
  summary: string,
  description?: string,
  issueType: string = "Task",
  additionalFields: Record<string, any> = {}
): Promise<any> {
  try {
    const headers = createBasicHeaders(config.email, config.apiToken);
    const baseUrl = normalizeAtlassianBaseUrl(config.baseUrl);
    const url = `${baseUrl}/rest/api/3/issue`;

    // Lấy metadata các trường hợp lệ
    const createmetaFields = await fetchJiraCreateMeta(config, projectKey, issueType);
    
    // Chỉ map các trường có trong createmeta
    const safeFields: Record<string, any> = {};
    let labelsToUpdate: string[] | undefined = undefined;
    for (const key of Object.keys(additionalFields)) {
      if (createmetaFields[key]) {
        safeFields[key] = additionalFields[key];
      } else {
        logger.warn(`[createIssue] Field '${key}' is not available on create screen for project ${projectKey} / issueType ${issueType}, will be ignored.`);
        // Nếu là labels thì lưu lại để update sau
        if (key === 'labels') {
          labelsToUpdate = additionalFields[key];
        }
      }
    }

    const data: {
      fields: {
        project: { key: string };
        summary: string;
        issuetype: { name: string };
        description?: any;
        [key: string]: any;
      };
    } = {
      fields: {
        project: { key: projectKey },
        summary: summary,
        issuetype: { name: issueType },
        ...safeFields,
      },
    };

    if (description && createmetaFields['description']) {
      data.fields.description = {
        type: "doc",
        version: 1,
        content: [
          {
            type: "paragraph",
            content: [
              {
                type: "text",
                text: description,
              },
            ],
          },
        ],
      };
    }

    logger.debug(`Creating issue in project ${projectKey}`);
    const curlCmd = `curl -X POST -H \"Content-Type: application/json\" -H \"Accept: application/json\" -H \"User-Agent: MCP-Atlassian-Server/1.0.0\" -u \"${config.email}:${config.apiToken.substring(0, 5)}...\" \"${url}\" -d '${JSON.stringify(data)}'`;
    logger.info(`Debug with curl: ${curlCmd}`);

    const response = await fetch(url, {
      method: "POST",
      headers,
      body: JSON.stringify(data),
      credentials: "omit",
    });

    if (!response.ok) {
      const statusCode = response.status;
      const responseText = await response.text();
      logger.error(`Jira API error (${statusCode}):`, responseText);
      if (statusCode === 400) {
        throw new ApiError(
          ApiErrorType.VALIDATION_ERROR,
          "Invalid issue data",
          statusCode,
          new Error(responseText)
        );
      } else if (statusCode === 401) {
        throw new ApiError(
          ApiErrorType.AUTHENTICATION_ERROR,
          "Unauthorized. Check your credentials.",
          statusCode,
          new Error(responseText)
        );
      } else if (statusCode === 403) {
        throw new ApiError(
          ApiErrorType.AUTHORIZATION_ERROR,
          "No permission to create issue",
          statusCode,
          new Error(responseText)
        );
      } else if (statusCode === 429) {
        throw new ApiError(
          ApiErrorType.RATE_LIMIT_ERROR,
          "API rate limit exceeded",
          statusCode,
          new Error(responseText)
        );
      } else {
        throw new ApiError(
          ApiErrorType.SERVER_ERROR,
          `Jira API error: ${responseText}`,
          statusCode,
          new Error(responseText)
        );
      }
    }

    const newIssue = await response.json();

    // Nếu không tạo được labels khi tạo issue, update lại ngay sau khi tạo
    if (labelsToUpdate && newIssue && newIssue.key) {
      logger.info(`[createIssue] Updating labels for issue ${newIssue.key} ngay sau khi tạo (do không khả dụng trên màn hình tạo issue)`);
      const updateUrl = `${baseUrl}/rest/api/3/issue/${newIssue.key}`;
      const updateData = { fields: { labels: labelsToUpdate } };
      const updateResponse = await fetch(updateUrl, {
        method: "PUT",
        headers,
        body: JSON.stringify(updateData),
        credentials: "omit",
      });
      if (!updateResponse.ok) {
        const updateText = await updateResponse.text();
        logger.error(`[createIssue] Failed to update labels for issue ${newIssue.key}:`, updateText);
      } else {
        logger.info(`[createIssue] Labels updated for issue ${newIssue.key}`);
      }
    }

    return newIssue;
  } catch (error) {
    logger.error(`Error creating issue:`, error);
    if (error instanceof ApiError) {
      throw error;
    }
    throw new ApiError(
      ApiErrorType.UNKNOWN_ERROR,
      `Error creating issue: ${error instanceof Error ? error.message : String(error)}`,
      500,
      error instanceof Error ? error : new Error(String(error))
    );
  }
}

// Update a Jira issue
export async function updateIssue(
  config: AtlassianConfig,
  issueIdOrKey: string,
  fields: Record<string, any>
): Promise<any> {
  try {
    const headers = createBasicHeaders(config.email, config.apiToken);
    const baseUrl = normalizeAtlassianBaseUrl(config.baseUrl);
    const url = `${baseUrl}/rest/api/3/issue/${issueIdOrKey}`;
    const data = { fields };
    logger.debug(`Updating issue ${issueIdOrKey}`);
    const curlCmd = `curl -X PUT -H "Content-Type: application/json" -H "Accept: application/json" -H "User-Agent: MCP-Atlassian-Server/1.0.0" -u "${config.email}:${config.apiToken.substring(0, 5)}..." "${url}" -d '${JSON.stringify(data)}'`;
    logger.info(`Debug with curl: ${curlCmd}`);
    const response = await fetch(url, {
      method: "PUT",
      headers,
      body: JSON.stringify(data),
      credentials: "omit",
    });
    if (!response.ok) {
      const statusCode = response.status;
      const responseText = await response.text();
      logger.error(`Jira API error (${statusCode}):`, responseText);
      if (statusCode === 400) {
        throw new ApiError(
          ApiErrorType.VALIDATION_ERROR,
          "Invalid update data",
          statusCode,
          new Error(responseText)
        );
      } else if (statusCode === 401) {
        throw new ApiError(
          ApiErrorType.AUTHENTICATION_ERROR,
          "Unauthorized. Check your credentials.",
          statusCode,
          new Error(responseText)
        );
      } else if (statusCode === 403) {
        throw new ApiError(
          ApiErrorType.AUTHORIZATION_ERROR,
          "No permission to update issue",
          statusCode,
          new Error(responseText)
        );
      } else if (statusCode === 404) {
        throw new ApiError(
          ApiErrorType.NOT_FOUND_ERROR,
          `Issue ${issueIdOrKey} does not exist`,
          statusCode,
          new Error(responseText)
        );
      } else if (statusCode === 429) {
        throw new ApiError(
          ApiErrorType.RATE_LIMIT_ERROR,
          "API rate limit exceeded",
          statusCode,
          new Error(responseText)
        );
      } else {
        throw new ApiError(
          ApiErrorType.SERVER_ERROR,
          `Jira API error: ${responseText}`,
          statusCode,
          new Error(responseText)
        );
      }
    }
    return {
      success: true,
      message: `Issue ${issueIdOrKey} updated successfully`,
    };
  } catch (error: any) {
    logger.error(`Error updating issue ${issueIdOrKey}:`, error);
    if (error instanceof ApiError) {
      throw error;
    }
    throw new ApiError(
      ApiErrorType.UNKNOWN_ERROR,
      `Error updating issue: ${error instanceof Error ? error.message : String(error)}`,
      500,
      error instanceof Error ? error : new Error(String(error))
    );
  }
}

// Change issue status
export async function transitionIssue(
  config: AtlassianConfig,
  issueIdOrKey: string,
  transitionId: string,
  comment?: string
): Promise<any> {
  try {
    const headers = createBasicHeaders(config.email, config.apiToken);
    const baseUrl = normalizeAtlassianBaseUrl(config.baseUrl);
    const url = `${baseUrl}/rest/api/3/issue/${issueIdOrKey}/transitions`;
    const data: any = {
      transition: {
        id: transitionId,
      },
    };
    if (comment) {
      data.update = {
        comment: [
          {
            add: {
              body: {
                type: "doc",
                version: 1,
                content: [
                  {
                    type: "paragraph",
                    content: [
                      {
                        type: "text",
                        text: comment,
                      },
                    ],
                  },
                ],
              },
            },
          },
        ],
      };
    }
    logger.debug(`Transitioning issue ${issueIdOrKey} to status ID ${transitionId}`);
    const curlCmd = `curl -X POST -H "Content-Type: application/json" -H "Accept: application/json" -H "User-Agent: MCP-Atlassian-Server/1.0.0" -u "${config.email}:${config.apiToken.substring(0, 5)}..." "${url}" -d '${JSON.stringify(data)}'`;
    logger.info(`Debug with curl: ${curlCmd}`);
    const response = await fetch(url, {
      method: "POST",
      headers,
      body: JSON.stringify(data),
      credentials: "omit",
    });
    if (!response.ok) {
      const statusCode = response.status;
      const responseText = await response.text();
      logger.error(`Jira API error (${statusCode}):`, responseText);
      if (statusCode === 400) {
        throw new ApiError(
          ApiErrorType.VALIDATION_ERROR,
          "Invalid transition ID or not applicable",
          statusCode,
          new Error(responseText)
        );
      } else if (statusCode === 401) {
        throw new ApiError(
          ApiErrorType.AUTHENTICATION_ERROR,
          "Unauthorized. Check your credentials.",
          statusCode,
          new Error(responseText)
        );
      } else if (statusCode === 403) {
        throw new ApiError(
          ApiErrorType.AUTHORIZATION_ERROR,
          "No permission to transition issue",
          statusCode,
          new Error(responseText)
        );
      } else if (statusCode === 404) {
        throw new ApiError(
          ApiErrorType.NOT_FOUND_ERROR,
          `Issue ${issueIdOrKey} does not exist`,
          statusCode,
          new Error(responseText)
        );
      } else if (statusCode === 429) {
        throw new ApiError(
          ApiErrorType.RATE_LIMIT_ERROR,
          "API rate limit exceeded",
          statusCode,
          new Error(responseText)
        );
      } else {
        throw new ApiError(
          ApiErrorType.SERVER_ERROR,
          `Jira API error: ${responseText}`,
          statusCode,
          new Error(responseText)
        );
      }
    }
    return {
      success: true,
      message: `Issue ${issueIdOrKey} transitioned successfully`,
      transitionId,
    };
  } catch (error: any) {
    logger.error(`Error transitioning issue ${issueIdOrKey}:`, error);
    if (error instanceof ApiError) {
      throw error;
    }
    throw new ApiError(
      ApiErrorType.UNKNOWN_ERROR,
      `Error transitioning issue: ${error instanceof Error ? error.message : String(error)}`,
      500,
      error instanceof Error ? error : new Error(String(error))
    );
  }
}

// Assign issue to a user
export async function assignIssue(
  config: AtlassianConfig,
  issueIdOrKey: string,
  accountId: string | null
): Promise<any> {
  try {
    const headers = createBasicHeaders(config.email, config.apiToken);
    const baseUrl = normalizeAtlassianBaseUrl(config.baseUrl);
    const url = `${baseUrl}/rest/api/3/issue/${issueIdOrKey}/assignee`;
    const data = { accountId: accountId };
    logger.debug(`Assigning issue ${issueIdOrKey} to account ID ${accountId || "UNASSIGNED"}`);
    const curlCmd = `curl -X PUT -H "Content-Type: application/json" -H "Accept: application/json" -H "User-Agent: MCP-Atlassian-Server/1.0.0" -u "${config.email}:${config.apiToken.substring(0, 5)}..." "${url}" -d '${JSON.stringify(data)}'`;
    logger.info(`Debug with curl: ${curlCmd}`);
    const response = await fetch(url, {
      method: "PUT",
      headers,
      body: JSON.stringify(data),
      credentials: "omit",
    });
    if (!response.ok) {
      const statusCode = response.status;
      const responseText = await response.text();
      logger.error(`Jira API error (${statusCode}):`, responseText);
      if (statusCode === 400) {
        throw new ApiError(
          ApiErrorType.VALIDATION_ERROR,
          "Invalid data",
          statusCode,
          new Error(responseText)
        );
      } else if (statusCode === 401) {
        throw new ApiError(
          ApiErrorType.AUTHENTICATION_ERROR,
          "Unauthorized. Check your credentials.",
          statusCode,
          new Error(responseText)
        );
      } else if (statusCode === 403) {
        throw new ApiError(
          ApiErrorType.AUTHORIZATION_ERROR,
          "No permission to assign issue",
          statusCode,
          new Error(responseText)
        );
      } else if (statusCode === 404) {
        throw new ApiError(
          ApiErrorType.NOT_FOUND_ERROR,
          `Issue ${issueIdOrKey} does not exist`,
          statusCode,
          new Error(responseText)
        );
      } else if (statusCode === 429) {
        throw new ApiError(
          ApiErrorType.RATE_LIMIT_ERROR,
          "API rate limit exceeded",
          statusCode,
          new Error(responseText)
        );
      } else {
        throw new ApiError(
          ApiErrorType.SERVER_ERROR,
          `Jira API error: ${responseText}`,
          statusCode,
          new Error(responseText)
        );
      }
    }
    return {
      success: true,
      message: accountId
        ? `Issue ${issueIdOrKey} assigned successfully`
        : `Issue ${issueIdOrKey} unassigned successfully`,
    };
  } catch (error: any) {
    logger.error(`Error assigning issue ${issueIdOrKey}:`, error);
    if (error instanceof ApiError) {
      throw error;
    }
    throw new ApiError(
      ApiErrorType.UNKNOWN_ERROR,
      `Error assigning issue: ${error instanceof Error ? error.message : String(error)}`,
      500,
      error instanceof Error ? error : new Error(String(error))
    );
  }
}

// Create a new dashboard
export async function createDashboard(config: AtlassianConfig, data: { name: string, description?: string, sharePermissions?: any[] }): Promise<any> {
  try {
    const headers = createBasicHeaders(config.email, config.apiToken);
    const baseUrl = normalizeAtlassianBaseUrl(config.baseUrl);
    const url = `${baseUrl}/rest/api/3/dashboard`;
    logger.debug(`Creating dashboard: ${data.name}`);
    const response = await fetch(url, {
      method: 'POST',
      headers,
      body: JSON.stringify(data),
      credentials: 'omit',
    });
    if (!response.ok) {
      const responseText = await response.text();
      logger.error(`Jira API error (${response.status}):`, responseText);
      throw new Error(`Jira API error: ${response.status} ${responseText}`);
    }
    return await response.json();
  } catch (error) {
    logger.error(`Error creating dashboard:`, error);
    throw error;
  }
}

// Update a dashboard
export async function updateDashboard(config: AtlassianConfig, dashboardId: string, data: { name?: string, description?: string, sharePermissions?: any[] }): Promise<any> {
  try {
    const headers = createBasicHeaders(config.email, config.apiToken);
    const baseUrl = normalizeAtlassianBaseUrl(config.baseUrl);
    const url = `${baseUrl}/rest/api/3/dashboard/${dashboardId}`;
    logger.debug(`Updating dashboard ${dashboardId}`);
    const response = await fetch(url, {
      method: 'PUT',
      headers,
      body: JSON.stringify(data),
      credentials: 'omit',
    });
    if (!response.ok) {
      const responseText = await response.text();
      logger.error(`Jira API error (${response.status}):`, responseText);
      throw new Error(`Jira API error: ${response.status} ${responseText}`);
    }
    return await response.json();
  } catch (error) {
    logger.error(`Error updating dashboard ${dashboardId}:`, error);
    throw error;
  }
}

// Add a gadget to a dashboard
export async function addGadgetToDashboard(config: AtlassianConfig, dashboardId: string, data: { uri: string, color?: string, position?: any, title?: string, properties?: any }): Promise<any> {
  try {
    const headers = createBasicHeaders(config.email, config.apiToken);
    const baseUrl = normalizeAtlassianBaseUrl(config.baseUrl);
    const url = `${baseUrl}/rest/api/3/dashboard/${dashboardId}/gadget`;
    logger.debug(`Adding gadget to dashboard ${dashboardId}`);
    const response = await fetch(url, {
      method: 'POST',
      headers,
      body: JSON.stringify(data),
      credentials: 'omit',
    });
    if (!response.ok) {
      const responseText = await response.text();
      logger.error(`Jira API error (${response.status}):`, responseText);
      throw new Error(`Jira API error: ${response.status} ${responseText}`);
    }
    return await response.json();
  } catch (error) {
    logger.error(`Error adding gadget to dashboard ${dashboardId}:`, error);
    throw error;
  }
}

// Remove a gadget from a dashboard
export async function removeGadgetFromDashboard(config: AtlassianConfig, dashboardId: string, gadgetId: string): Promise<any> {
  try {
    const headers = createBasicHeaders(config.email, config.apiToken);
    const baseUrl = normalizeAtlassianBaseUrl(config.baseUrl);
    const url = `${baseUrl}/rest/api/3/dashboard/${dashboardId}/gadget/${gadgetId}`;
    logger.debug(`Removing gadget ${gadgetId} from dashboard ${dashboardId}`);
    const response = await fetch(url, {
      method: 'DELETE',
      headers,
      credentials: 'omit',
    });
    if (!response.ok) {
      const responseText = await response.text();
      logger.error(`Jira API error (${response.status}):`, responseText);
      throw new Error(`Jira API error: ${response.status} ${responseText}`);
    }
    return { success: true };
  } catch (error) {
    logger.error(`Error removing gadget ${gadgetId} from dashboard ${dashboardId}:`, error);
    throw error;
  }
}

// Create a new filter
export async function createFilter(
  config: AtlassianConfig,
  name: string,
  jql: string,
  description?: string,
  favourite?: boolean
): Promise<any> {
  try {
    const headers = createBasicHeaders(config.email, config.apiToken);
    const baseUrl = normalizeAtlassianBaseUrl(config.baseUrl);
    const url = `${baseUrl}/rest/api/3/filter`;
    const data: any = {
      name,
      jql,
      description: description || '',
      favourite: favourite !== undefined ? favourite : false
    };
    logger.debug(`Creating Jira filter: ${name}`);
    const response = await fetch(url, {
      method: 'POST',
      headers,
      body: JSON.stringify(data),
      credentials: 'omit',
    });
    if (!response.ok) {
      const responseText = await response.text();
      logger.error(`Jira API error (${response.status}):`, responseText);
      throw new Error(`Jira API error: ${response.status} ${responseText}`);
    }
    return await response.json();
  } catch (error) {
    logger.error(`Error creating Jira filter:`, error);
    throw error;
  }
}

// Update a filter
export async function updateFilter(
  config: AtlassianConfig,
  filterId: string,
  updateData: { name?: string; jql?: string; description?: string; favourite?: boolean; sharePermissions?: any[] }
): Promise<any> {
  try {
    const headers = createBasicHeaders(config.email, config.apiToken);
    const baseUrl = normalizeAtlassianBaseUrl(config.baseUrl);
    const url = `${baseUrl}/rest/api/3/filter/${filterId}`;
    logger.debug(`Updating Jira filter ${filterId}`);
    // Chỉ build payload với các trường hợp lệ
    const allowedFields = ['name', 'jql', 'description', 'favourite', 'sharePermissions'] as const;
    type AllowedField = typeof allowedFields[number];
    const data: any = {};
    for (const key of allowedFields) {
      if (updateData[key as AllowedField] !== undefined) {
        data[key] = updateData[key as AllowedField];
      }
    }
    logger.debug('Payload for updateFilter:', JSON.stringify(data));
    const response = await fetch(url, {
      method: 'PUT',
      headers,
      body: JSON.stringify(data),
      credentials: 'omit',
    });
    if (!response.ok) {
      const responseText = await response.text();
      logger.error(`Jira API error (${response.status}):`, responseText);
      throw new Error(`Jira API error: ${response.status} ${responseText}`);
    }
    return await response.json();
  } catch (error) {
    logger.error(`Error updating Jira filter ${filterId}:`, error);
    throw error;
  }
}

// Delete a filter
export async function deleteFilter(
  config: AtlassianConfig,
  filterId: string
): Promise<void> {
  try {
    const headers = createBasicHeaders(config.email, config.apiToken);
    const baseUrl = normalizeAtlassianBaseUrl(config.baseUrl);
    const url = `${baseUrl}/rest/api/3/filter/${filterId}`;
    logger.debug(`Deleting Jira filter ${filterId}`);
    const response = await fetch(url, {
      method: 'DELETE',
      headers,
      credentials: 'omit',
    });
    if (!response.ok) {
      const responseText = await response.text();
      logger.error(`Jira API error (${response.status}):`, responseText);
      throw new Error(`Jira API error: ${response.status} ${responseText}`);
    }
  } catch (error) {
    logger.error(`Error deleting Jira filter ${filterId}:`, error);
    throw error;
  }
}

// Lấy danh sách tất cả gadget có sẵn để thêm vào dashboard
export async function getJiraAvailableGadgets(config: AtlassianConfig): Promise<any> {
  const headers = createBasicHeaders(config.email, config.apiToken);
  const baseUrl = normalizeAtlassianBaseUrl(config.baseUrl);
  const url = `${baseUrl}/rest/api/3/dashboard/gadgets`;
  const response = await fetch(url, { headers, credentials: 'omit' });
  if (!response.ok) {
    const responseText = await response.text();
    logger.error(`Jira API error (gadgets, ${response.status}):`, responseText);
    throw new Error(`Jira API error (gadgets): ${response.status} ${responseText}`);
  }
  return await response.json();
} 
```

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

```markdown
# MCP Atlassian Server: Resources & Tools Reference

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.

## Hướng dẫn dành cho Developers

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 để:

- Hiểu cách Resource và Tool được triển khai
- Thêm mới hoặc mở rộng Resource/Tool
- Xử lý các trường hợp đặc biệt (ADF, version conflicts, error handling)
- Debugging và maintenance

## JIRA

### 1. Issue

#### Resource
| Resource | URI | Mô tả | Atlassian API Endpoint | Dữ liệu trả về |
|----------|-----|-------|-----------------------|----------------|
| Issues | `jira://issues` | Danh sách issue | `/rest/api/3/search` | Array của Issue objects |
| Issue Details | `jira://issues/{issueKey}` | Chi tiết issue | `/rest/api/3/issue/{issueKey}` | Single Issue object |
| 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 |
| Issue Comments | `jira://issues/{issueKey}/comments` | Danh sách comment của issue | `/rest/api/3/issue/{issueKey}/comment` | Array của Comment objects |

#### Tool
| Tool | Mô tả | Tham số chính | Atlassian API Endpoint | Dữ liệu output |
|------|-------|---------------|-----------------------|----------------|
| createIssue | Tạo issue mới | projectKey, summary, ... | `/rest/api/3/issue` | Issue key và ID mới |
| updateIssue | Cập nhật issue | issueKey, summary, ... | `/rest/api/3/issue/{issueIdOrKey}` | Status của update |
| transitionIssue | Chuyển trạng thái issue | issueKey, transitionId | `/rest/api/3/issue/{issueIdOrKey}/transitions` | Status của transition |
| assignIssue | Gán issue cho user | issueKey, accountId | `/rest/api/3/issue/{issueIdOrKey}/assignee` | Status của assignment |
| 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 |
| addIssueToSprint | Đưa issue vào sprint | sprintId, issueKeys | `/rest/agile/1.0/sprint/{sprintId}/issue` | Status của thêm |
| 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 |

### 2. Project

#### Resource
| Resource | URI | Mô tả | Atlassian API Endpoint | Dữ liệu trả về | 
|----------|-----|-------|-----------------------|----------------|
| Projects | `jira://projects` | Danh sách project | `/rest/api/3/project` | Array của Project objects |
| Project Details | `jira://projects/{projectKey}` | Chi tiết project | `/rest/api/3/project/{projectKey}` | Single Project object |
| 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) |

### 3. Board

#### Resource
| Resource | URI | Mô tả | Atlassian API Endpoint | Dữ liệu trả về |
|----------|-----|-------|-----------------------|----------------|
| Boards | `jira://boards` | Danh sách board | `/rest/agile/1.0/board` | Array của Board objects |
| Board Details | `jira://boards/{boardId}` | Chi tiết board | `/rest/agile/1.0/board/{boardId}` | Single Board object |
| 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 |
| Board Configuration | `jira://boards/{boardId}/configuration` | Cấu hình board | `/rest/agile/1.0/board/{boardId}/configuration` | Board config object |
| 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 |

### 4. Sprint

#### Resource
| Resource | URI | Mô tả | Atlassian API Endpoint | Dữ liệu trả về |
|----------|-----|-------|-----------------------|----------------|
| Sprints | `jira://sprints` | Danh sách tất cả sprints | `/rest/agile/1.0/sprint` | Array của Sprint objects |
| Sprint Details | `jira://sprints/{sprintId}` | Chi tiết sprint | `/rest/agile/1.0/sprint/{sprintId}` | Single Sprint object |
| Sprint Issues | `jira://sprints/{sprintId}/issues` | Danh sách issue trong sprint | `/rest/agile/1.0/sprint/{sprintId}/issue` | Array của Issue objects |

#### Tool
| Tool | Mô tả | Tham số chính | Atlassian API Endpoint | Dữ liệu output |
|------|-------|---------------|-----------------------|----------------|
| createSprint | Tạo sprint mới | boardId, name, ... | `/rest/agile/1.0/sprint` | Sprint ID mới |
| startSprint | Bắt đầu sprint | sprintId, ... | `/rest/agile/1.0/sprint/{sprintId}/start` | Status của bắt đầu |
| closeSprint | Đóng sprint | sprintId, ... | `/rest/agile/1.0/sprint/{sprintId}/close` | Status của đóng |
| addIssueToSprint | Thêm issue vào sprint | sprintId, issueKeys | `/rest/agile/1.0/sprint/{sprintId}/issue` | Status của thêm |

### 5. Filter

#### Resource
| Resource | URI | Mô tả | Atlassian API Endpoint | Dữ liệu trả về |
|----------|-----|-------|-----------------------|----------------|
| Filters | `jira://filters` | Danh sách filter | `/rest/api/3/filter/search` | Array của Filter objects |
| Filter Details | `jira://filters/{filterId}` | Chi tiết filter | `/rest/api/3/filter/{filterId}` | Single Filter object |
| My Filters | `jira://filters/my` | Filter của tôi | `/rest/api/3/filter/my` | Array của Filter objects |

#### Tool
| Tool | Mô tả | Tham số chính | Atlassian API Endpoint | Dữ liệu output |
|------|-------|---------------|-----------------------|----------------|
| createFilter | Tạo filter mới | name, jql, ... | `/rest/api/3/filter` | Filter ID mới |
| updateFilter | Cập nhật filter | filterId, ... | `/rest/api/3/filter/{filterId}` | Status của update |
| deleteFilter | Xóa filter | filterId | `/rest/api/3/filter/{filterId}` | Status của xoá |

### 6. Dashboard & Gadget

#### Resource
| Resource | URI | Mô tả | Atlassian API Endpoint | Dữ liệu trả về |
|----------|-----|-------|-----------------------|----------------|
| Dashboards | `jira://dashboards` | Danh sách dashboard | `/rest/api/3/dashboard` | Array của Dashboard objects |
| My Dashboards | `jira://dashboards/my` | Dashboard của tôi | `/rest/api/3/dashboard?filter=my` | Array của Dashboard objects |
| Dashboard Details | `jira://dashboards/{dashboardId}` | Chi tiết dashboard | `/rest/api/3/dashboard/{dashboardId}` | Single Dashboard object |
| Dashboard Gadgets | `jira://dashboards/{dashboardId}/gadgets` | Danh sách gadget trên dashboard | `/rest/api/3/dashboard/{dashboardId}/gadget` | Array của Gadget objects |
| Gadgets | `jira://gadgets` | Danh sách gadget | `/rest/api/3/dashboard/gadgets` | Array của Gadget objects |

#### Tool
| Tool | Mô tả | Tham số chính | Atlassian API Endpoint | Dữ liệu output |
|------|-------|---------------|-----------------------|----------------|
| createDashboard | Tạo dashboard mới | name, ... | `/rest/api/3/dashboard` | Dashboard ID mới |
| updateDashboard | Cập nhật dashboard | dashboardId, ... | `/rest/api/3/dashboard/{dashboardId}` | Status của update |
| addGadgetToDashboard | Thêm gadget vào dashboard | dashboardId, uri, ... | `/rest/api/3/dashboard/{dashboardId}/gadget` | Gadget ID mới |
| removeGadgetFromDashboard | Xóa gadget khỏi dashboard | dashboardId, gadgetId | `/rest/api/3/dashboard/{dashboardId}/gadget/{gadgetId}` | Status của xóa |

### 7. User

#### Resource
| Resource | URI | Mô tả | Atlassian API Endpoint | Dữ liệu trả về |
|----------|-----|-------|-----------------------|----------------|
| Users | `jira://users` | Danh sách tất cả user | `/rest/api/3/users/search` | Array của User objects |
| User Details | `jira://users/{accountId}` | Thông tin user | `/rest/api/3/user?accountId=...` | Single User object |
| 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 |
| 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 |

---

## CONFLUENCE

### 1. Space

#### Resource
| Resource | URI | Mô tả | Atlassian API Endpoint | Dữ liệu trả về |
|----------|-----|-------|-----------------------|----------------|
| Spaces | `confluence://spaces` | Danh sách space | `/wiki/api/v2/spaces` | Array của Space objects (v2) |
| Space Details | `confluence://spaces/{spaceKey}` | Chi tiết space | `/wiki/api/v2/spaces/{spaceKey}` | Single Space object (v2) |
| Space Pages | `confluence://spaces/{spaceKey}/pages` | Danh sách page trong space | `/wiki/api/v2/pages?space-id=...` | Array của Page objects (v2) |

### 2. Page

#### Resource
| Resource | URI | Mô tả | Atlassian API Endpoint | Dữ liệu trả về |
|----------|-----|-------|-----------------------|----------------|
| Pages | `confluence://pages` | Tìm kiếm page theo filter | `/wiki/api/v2/pages` | Array của Page objects (v2) |
| 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) |
| Page Children | `confluence://pages/{pageId}/children` | Danh sách page con | `/wiki/api/v2/pages/{pageId}/children` | Array của Page objects (v2) |
| 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) |
| 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) |
| 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) |
| 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) |

#### Tool
| Tool | Mô tả | Tham số chính | Atlassian API Endpoint | Dữ liệu output |
|------|-------|---------------|-----------------------|----------------|
| createPage | Tạo page mới | spaceId, title, content, parentId | `/wiki/api/v2/pages` | Page ID mới |
| updatePage | Cập nhật nội dung page | pageId, title, content, version | `/wiki/api/v2/pages/{pageId}` (PUT) | Status của update |
| updatePageTitle | Đổi tiêu đề page | pageId, title, version | `/wiki/api/v2/pages/{pageId}/title` (PUT) | Status của update |
| deletePage | Xóa page | pageId, draft, purge | `/wiki/api/v2/pages/{pageId}` (DELETE) | Status của xóa |

### 3. Comment

#### Resource
| Resource | URI | Mô tả | Atlassian API Endpoint | Dữ liệu trả về |
|----------|-----|-------|-----------------------|----------------|
| 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) |

#### Tool
| Tool | Mô tả | Tham số chính | Atlassian API Endpoint | Dữ liệu output |
|------|-------|---------------|-----------------------|----------------|
| addComment | Thêm comment vào page | pageId, content | `/wiki/api/v2/footer-comments` | Comment mới |
| updateFooterComment | Cập nhật comment ở footer | commentId, version, value, ... | `/wiki/api/v2/footer-comments/{commentId}` (PUT) | Status của update |
| deleteFooterComment | Xóa comment ở footer | commentId | `/wiki/api/v2/footer-comments/{commentId}` (DELETE) | Status của xóa |

---

## Implementation Details: Hướng dẫn mở rộng Resource & Tool cho Developer

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:

### 1. Phân biệt Resource và Tool
- **Resource**: Trả về dữ liệu, chỉ đọc (GET), ví dụ: danh sách issue, chi tiết project, các comment, v.v.
- **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.

### 2. Vị trí file
- **Resource**: Thêm/cập nhật file trong:
  - `src/resources/jira/` (cho Jira)
  - `src/resources/confluence/` (cho Confluence)
  - Đăng ký resource mới trong file `index.ts` tương ứng nếu cần.
- **Tool**: Thêm/cập nhật file trong:
  - `src/tools/jira/` (cho Jira)
  - `src/tools/confluence/` (cho Confluence)
  - Đăng ký tool mới trong `src/tools/index.ts`.

### 3. Sử dụng helpers chuẩn hóa
- **Luôn sử dụng helpers mới:**
  - Import `Config` và `Resources` từ `../../utils/mcp-helpers.js`.
  - 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.
- **Ví dụ import chuẩn:**
  ```typescript
  import { Config, Resources } from '../../utils/mcp-helpers.js';
  ```

### 4. Đăng ký resource theo chuẩn MCP
- Đăng ký resource qua `server.resource()` với `ResourceTemplate` và callback chuẩn hóa:
  ```typescript
  export function registerYourResource(server: McpServer) {
    server.resource(
      'unique-resource-name',
      new ResourceTemplate('resource://pattern/{param}', {
        list: async (_extra) => ({
          resources: [
            {
              uri: 'resource://pattern/{param}',
              name: 'Resource Name',
              description: 'Resource description',
              mimeType: 'application/json'
            }
          ]
        })
      }),
      async (uri, params, extra) => {
        // Ưu tiên lấy config từ context nếu có, fallback về env
        const config = (extra && typeof extra === 'object' && 'context' in extra && extra.context && (extra.context as any).atlassianConfig)
          ? (extra.context as any).atlassianConfig
          : Config.getAtlassianConfigFromEnv();
        // Xử lý params
        const param = Array.isArray(params.param) ? params.param[0] : params.param;
        // Gọi API helper
        const data = await yourApiFunction(config, param);
        // Chuẩn hóa response
        return Resources.createStandardResource(
          typeof uri === 'string' ? uri : uri.href,
          data.results || [], // hoặc data tuỳ API
          'resourceKey',      // ví dụ: 'issues', 'pages', ...
          yourSchema,
          data.size || (data.results || []).length,
          data.limit || (data.results || []).length,
          0,
          'uiUrl nếu có'
        );
      }
    );
  }
  ```
- **Lưu ý:**
  - Không trả về object tự do, luôn dùng `Resources.createStandardResource` để chuẩn hóa metadata, schema, paging, links.
  - Đảm bảo resource name (tên đầu tiên khi đăng ký) là duy nhất.
  - Không đăng ký trùng URI pattern ở nhiều file.

### 5. Chuẩn hóa schema dữ liệu
- Mỗi resource/tool **bắt buộc phải có schema** validate input/output.
- Thêm/cập nhật schema trong:
  - `src/schemas/jira.ts` (cho Jira)
  - `src/schemas/confluence.ts` (cho Confluence)
- Đảm bảo schema phản ánh đúng dữ liệu thực tế trả về/tạo ra từ Atlassian API.

### 6. Xử lý config và context an toàn
- 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:
  ```typescript
  const config = (extra && typeof extra === 'object' && 'context' in extra && extra.context && (extra.context as any).atlassianConfig)
    ? (extra.context as any).atlassianConfig
    : Config.getAtlassianConfigFromEnv();
  ```
- Không hardcode credentials, không truyền config qua params.

### 7. Đăng ký tool theo chuẩn MCP
- Đăng ký tool qua `server.tool()` với schema input rõ ràng, callback chuẩn hóa:
  ```typescript
  export function registerYourTool(server: McpServer) {
    server.tool(
      'tool-name',
      'Tool description',
      {
        type: 'object',
        properties: { param1: { type: 'string' } },
        required: ['param1']
      },
      async (params, context) => {
        const { atlassianConfig } = context;
        const result = await yourToolFunction(atlassianConfig, params);
        return {
          content: [ { type: 'text', text: `Operation completed: ${result}` } ]
        };
      }
    );
  }
  ```
- Đăng ký tool trong `src/tools/index.ts` như resource.

### 7.1 Hướng dẫn chi tiết implement tool Jira (chuẩn mới)

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:

#### Cấu trúc chung cho tool Jira
```typescript
// 1. Import helpers mới
import { z } from 'zod';
import { Config, Tools } from '../../utils/mcp-helpers.js';
import { McpResponse, createSuccessResponse, createErrorResponse } from '../../utils/mcp-core.js';
import { Logger } from '../../utils/logger.js';
import { ApiError, ApiErrorType } from '../../utils/error-handler.js';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
// Sử dụng các API helper chuẩn hóa
import { createJiraIssueV3, updateJiraIssueV3 } from '../../utils/jira-tool-api-v3.js';
// hoặc helper API Agile (cho Sprint, Board...)
import { createSprint, updateSprint } from '../../utils/jira-tool-api-agile.js';

// 2. Logger chuẩn
const logger = Logger.getLogger('JiraTools:yourTool');

// 3. Schema input chuẩn hóa với Zod
export const yourToolSchema = z.object({
  param1: z.string().describe('Parameter 1 description'),
  param2: z.number().optional().describe('Optional parameter description'),
  // các tham số khác...
});

// 4. Type cho tham số và kết quả
type YourToolParams = z.infer<typeof yourToolSchema>;
interface YourToolResult {
  id: string;
  key?: string;
  success: boolean;
  message: string;
  // các trường kết quả khác...
}

// 5. Hàm handler chính (tách riêng xử lý logic)
export async function yourToolHandler(
  params: YourToolParams,
  config: any
): Promise<YourToolResult> {
  try {
    logger.info(`Starting yourTool with params: ${params.param1}`);
    
    // Gọi API Jira qua helper chuẩn hóa
    const result = await yourToolApiFunction(config, {
      // Map params sang API params
      param1: params.param1,
      param2: params.param2
    });
    
    // Xử lý kết quả, chuẩn hóa trả về
    return {
      id: result.id,
      key: result.key,
      success: true,
      message: `Operation completed successfully: ${result.id}`
    };
  } catch (error) {
    // Xử lý lỗi chuẩn
    if (error instanceof ApiError) {
      throw error;
    }
    logger.error(`Error in yourTool:`, error);
    throw new ApiError(
      ApiErrorType.SERVER_ERROR,
      `Failed operation: ${error instanceof Error ? error.message : String(error)}`,
      500
    );
  }
}

// 6. Hàm đăng ký tool
export const registerYourTool = (server: McpServer) => {
  server.tool(
    'yourTool',
    'Your tool description',
    yourToolSchema.shape,
    async (params: YourToolParams, context: Record<string, any>) => {
      try {
        // Lấy config từ context nếu có, fallback về env
        const config = context?.atlassianConfig ?? Config.getAtlassianConfigFromEnv();
        if (!config) {
          return {
            content: [
              { type: 'text', text: JSON.stringify({
                success: false,
                message: 'Invalid or missing Atlassian configuration'
              })}
            ],
            isError: true
          };
        }
        
        // Gọi handler
        const result = await yourToolHandler(params, config);
        
        // Trả về chuẩn JSON trong content[0].text
        return {
          content: [
            {
              type: 'text',
              text: JSON.stringify({
                success: true,
                id: result.id,
                key: result.key,
                message: result.message
                // các trường khác...
              })
            }
          ]
        };
      } catch (error) {
        // Xử lý lỗi chuẩn
        if (error instanceof ApiError) {
          return {
            content: [
              {
                type: 'text',
                text: JSON.stringify({
                  success: false,
                  message: error.message,
                  code: error.code,
                  statusCode: error.statusCode,
                  type: error.type
                })
              }
            ],
            isError: true
          };
        }
        return {
          content: [
            {
              type: 'text',
              text: JSON.stringify({
                success: false,
                message: `Error: ${error instanceof Error ? error.message : String(error)}`
              })
            }
          ],
          isError: true
        };
      }
    }
  );
};
```

#### Ví dụ cụ thể: Tool createIssue
```typescript
// src/tools/jira/create-issue.ts
import { z } from 'zod';
import { Config } from '../../utils/mcp-helpers.js';
import { McpResponse, createErrorResponse } from '../../utils/mcp-core.js';
import { createJiraIssueV3 } from '../../utils/jira-tool-api-v3.js';
import { Logger } from '../../utils/logger.js';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';

const logger = Logger.getLogger('JiraTools:createIssue');

// Schema chuẩn hóa
export const createIssueSchema = z.object({
  projectKey: z.string().describe('Jira project key (e.g. DEMO, TES, ...)'),
  summary: z.string().describe('Issue summary'),
  description: z.string().optional().describe('Issue description'),
  issueType: z.string().describe('Issue type (e.g. Task, Bug, Story, ...)'),
  priority: z.string().optional().describe('Issue priority (e.g. Highest, High, Medium, Low, Lowest)'),
  assignee: z.string().optional().describe('Assignee account ID')
});

// Main handler
export async function createIssueHandler(params, config) {
  try {
    logger.info(`Creating Jira issue in project: ${params.projectKey}`);
    const issueData = await createJiraIssueV3(config, {
      projectKey: params.projectKey,
      summary: params.summary,
      description: params.description || "",
      issueType: params.issueType,
      priority: params.priority,
      assignee: params.assignee
    });
    
    return {
      id: issueData.id,
      key: issueData.key,
      self: issueData.self,
      success: true
    };
  } catch (error) {
    logger.error(`Error creating Jira issue:`, error);
    throw error;
  }
}

// Đăng ký tool
export const registerCreateIssueTool = (server: McpServer) => {
  server.tool(
    'createIssue',
    'Create a new Jira issue',
    createIssueSchema.shape,
    async (params, context) => {
      try {
        const config = context?.atlassianConfig ?? Config.getAtlassianConfigFromEnv();
        const result = await createIssueHandler(params, config);
        return {
          content: [
            {
              type: 'text',
              text: JSON.stringify({
                id: result.id,
                key: result.key,
                self: result.self,
                success: true
              })
            }
          ]
        };
      } catch (error) {
        return {
          content: [
            {
              type: 'text',
              text: JSON.stringify({
                success: false,
                message: error instanceof Error ? error.message : String(error)
              })
            }
          ],
          isError: true
        };
      }
    }
  );
};
```

#### Lưu ý quan trọng khi implement tool Jira
- **Schema chuẩn**: Luôn định nghĩa schema input với Zod, bao gồm mô tả cho mỗi tham số.
- **Response chuẩn**: Trả về object `{ success: true/false, key/id, message, ... }` trong `content[0].text` (JSON string).
- **Error handling**: Xử lý mọi trường hợp lỗi, bao gồm lỗi invalid config, network errors, API errors.
- **Logging**: Sử dụng Logger chuẩn, không log thông tin nhạy cảm.
- **Helper API**: Sử dụng các helper API chuẩn hóa thay vì gọi trực tiếp fetch/axios:
  - `jira-tool-api-v3.js`: Cho các API REST Jira (issue, filter, dashboard, ...)
  - `jira-tool-api-agile.js`: Cho các API Agile (sprint, board, backlog, ...)
- **Không dùng các hàm cũ** từ `tool-helpers.js`, `mcp-response.js`.

### 8. Testing, debugging và backward compatibility
- Luôn test resource/tool mới bằng test client (`dev_mcp-atlassian-test-client`).
- Theo dõi log qua `Logger` để debug dễ dàng.
- Khi refactor, giữ backward compatibility cho client cũ (nếu cần), không đổi format response đột ngột.

### 9. Lưu ý bảo mật
- Không log credentials/API token ra log file.
- Không trả về thông tin nhạy cảm trong response.
- Chỉ expose các endpoint/resource thực sự cần thiết.

### 10. Cập nhật tài liệu
- 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.
- Ghi chú rõ các thay đổi breaking change (nếu có).

---
**Tóm lại:**
- Luôn dùng helpers mới (`Config`, `Resources`), chuẩn hóa response, schema, context.
- Không lặp lại lỗi cũ (trùng resource, trả về object tự do, thiếu schema, hardcode config).
- Ư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
# Hướng Dẫn Migration từ Confluence API v1 sang v2

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.

## Nguyên tắc chung khi migration

1. Thay đổi base URL từ `/rest/api/` sang `/wiki/api/v2/`
2. Sử dụng endpoint chuyên biệt thay vì endpoint chung "content"
3. Thay đổi từ phân trang offset-based sang cursor-based
4. Thay thế tham số `expand` bằng các API call riêng biệt

## Migration cho Resources

### 1. Spaces

**Từ v1:**
```typescript
// Danh sách không gian
server.resource(
  "confluence-spaces",
  new ResourceTemplate("confluence://spaces", { list: undefined }),
  async (uri) => {
    const spaces = await confluenceClient.get('/rest/api/space');
    return {
      contents: [{
        uri: uri.href,
        mimeType: "application/json",
        text: JSON.stringify({
          spaces: spaces.results,
          metadata: createStandardMetadata(spaces.size, spaces.limit, spaces.start, uri.href)
        }),
        schema: spaceListSchema
      }]
    };
  }
);
```

**Sang v2:**
```typescript
// Danh sách không gian
server.resource(
  "confluence-spaces",
  new ResourceTemplate("confluence://spaces", { list: undefined }),
  async (uri) => {
    const url = new URL(uri.href);
    const limit = parseInt(url.searchParams.get("limit") || "25");
    const cursor = url.searchParams.get("cursor") || undefined;
    
    const endpoint = `/wiki/api/v2/spaces${cursor ? `?cursor=${cursor}&limit=${limit}` : `?limit=${limit}`}`;
    const spaces = await confluenceClient.get(endpoint);
    
    return {
      contents: [{
        uri: uri.href,
        mimeType: "application/json",
        text: JSON.stringify({
          spaces: spaces.results,
          metadata: {
            total: spaces._links.next ? -1 : spaces.results.length, // Total unknown with cursor pagination
            limit: limit,
            hasMore: !!spaces._links.next,
            links: {
              self: uri.href,
              next: spaces._links.next ? `${uri.href}?cursor=${new URL(spaces._links.next).searchParams.get("cursor")}&limit=${limit}` : undefined
            }
          }
        }),
        schema: spaceListSchemaV2
      }]
    };
  }
);
```

### 2. Space Details

**Từ v1:**
```typescript
// Chi tiết không gian
server.resource(
  "confluence-space-details",
  new ResourceTemplate("confluence://spaces/{spaceKey}", { spaceKey: undefined }),
  async (uri, { spaceKey }) => {
    const space = await confluenceClient.get(`/rest/api/space/${spaceKey}`);
    return {
      contents: [{
        uri: uri.href,
        mimeType: "application/json",
        text: JSON.stringify(space),
        schema: spaceSchema
      }]
    };
  }
);
```

**Sang v2:**
```typescript
// Chi tiết không gian
server.resource(
  "confluence-space-details",
  new ResourceTemplate("confluence://spaces/{spaceKey}", { spaceKey: undefined }),
  async (uri, { spaceKey }) => {
    const space = await confluenceClient.get(`/wiki/api/v2/spaces/${spaceKey}`);
    return {
      contents: [{
        uri: uri.href,
        mimeType: "application/json",
        text: JSON.stringify(space),
        schema: spaceSchemaV2
      }]
    };
  }
);
```

### 3. Pages

**Từ v1:**
```typescript
// Danh sách trang
server.resource(
  "confluence-pages",
  new ResourceTemplate("confluence://pages", { list: undefined }),
  async (uri) => {
    const url = new URL(uri.href);
    const limit = parseInt(url.searchParams.get("limit") || "25");
    const start = parseInt(url.searchParams.get("start") || "0");
    
    const pages = await confluenceClient.get(`/rest/api/content/search?type=page&limit=${limit}&start=${start}`);
    
    return {
      contents: [{
        uri: uri.href,
        mimeType: "application/json",
        text: JSON.stringify({
          pages: pages.results,
          metadata: createStandardMetadata(pages.size, pages.limit, pages.start, uri.href)
        }),
        schema: pageListSchema
      }]
    };
  }
);
```

**Sang v2:**
```typescript
// Danh sách trang
server.resource(
  "confluence-pages",
  new ResourceTemplate("confluence://pages", { list: undefined }),
  async (uri) => {
    const url = new URL(uri.href);
    const limit = parseInt(url.searchParams.get("limit") || "25");
    const cursor = url.searchParams.get("cursor") || undefined;
    
    const endpoint = `/wiki/api/v2/pages${cursor ? `?cursor=${cursor}&limit=${limit}` : `?limit=${limit}`}`;
    const pages = await confluenceClient.get(endpoint);
    
    return {
      contents: [{
        uri: uri.href,
        mimeType: "application/json",
        text: JSON.stringify({
          pages: pages.results,
          metadata: {
            limit: limit,
            hasMore: !!pages._links.next,
            links: {
              self: uri.href,
              next: pages._links.next ? `${uri.href}?cursor=${new URL(pages._links.next).searchParams.get("cursor")}&limit=${limit}` : undefined
            }
          }
        }),
        schema: pageListSchemaV2
      }]
    };
  }
);
```

### 4. Page Details

**Từ v1:**
```typescript
// Chi tiết trang
server.resource(
  "confluence-page-details",
  new ResourceTemplate("confluence://pages/{pageId}", { pageId: undefined }),
  async (uri, { pageId }) => {
    const page = await confluenceClient.get(`/rest/api/content/${pageId}?expand=body.storage,version`);
    
    return {
      contents: [{
        uri: uri.href,
        mimeType: "application/json",
        text: JSON.stringify({
          ...page,
          body: page.body?.storage?.value || ""
        }),
        schema: pageSchema
      }]
    };
  }
);
```

**Sang v2:**
```typescript
// Chi tiết trang
server.resource(
  "confluence-page-details",
  new ResourceTemplate("confluence://pages/{pageId}", { pageId: undefined }),
  async (uri, { pageId }) => {
    // Cần 2 API call riêng biệt thay vì dùng expand
    const page = await confluenceClient.get(`/wiki/api/v2/pages/${pageId}`);
    const body = await confluenceClient.get(`/wiki/api/v2/pages/${pageId}/body`);
    
    return {
      contents: [{
        uri: uri.href,
        mimeType: "application/json",
        text: JSON.stringify({
          ...page,
          body: body.value || "",
          bodyType: body.representation || "storage"
        }),
        schema: pageSchemaV2
      }]
    };
  }
);
```

### 5. Page Children

**Từ v1:**
```typescript
// Danh sách trang con
server.resource(
  "confluence-page-children",
  new ResourceTemplate("confluence://pages/{pageId}/children", { pageId: undefined }),
  async (uri, { pageId }) => {
    const children = await confluenceClient.get(`/rest/api/content/${pageId}/child/page`);
    
    return {
      contents: [{
        uri: uri.href,
        mimeType: "application/json",
        text: JSON.stringify({
          pages: children.results,
          metadata: createStandardMetadata(children.size, children.limit, children.start, uri.href)
        }),
        schema: pageListSchema
      }]
    };
  }
);
```

**Sang v2:**
```typescript
// Danh sách trang con
server.resource(
  "confluence-page-children",
  new ResourceTemplate("confluence://pages/{pageId}/children", { pageId: undefined }),
  async (uri, { pageId }) => {
    const url = new URL(uri.href);
    const limit = parseInt(url.searchParams.get("limit") || "25");
    const cursor = url.searchParams.get("cursor") || undefined;
    
    const endpoint = `/wiki/api/v2/pages/${pageId}/children${cursor ? `?cursor=${cursor}&limit=${limit}` : `?limit=${limit}`}`;
    const children = await confluenceClient.get(endpoint);
    
    return {
      contents: [{
        uri: uri.href,
        mimeType: "application/json",
        text: JSON.stringify({
          pages: children.results,
          metadata: {
            limit: limit,
            hasMore: !!children._links.next,
            links: {
              self: uri.href,
              next: children._links.next ? `${uri.href}?cursor=${new URL(children._links.next).searchParams.get("cursor")}&limit=${limit}` : undefined
            }
          }
        }),
        schema: pageListSchemaV2
      }]
    };
  }
);
```

### 6. Page Ancestors

**Từ v1:**
```typescript
// Danh sách tổ tiên
server.resource(
  "confluence-page-ancestors",
  new ResourceTemplate("confluence://pages/{pageId}/ancestors", { pageId: undefined }),
  async (uri, { pageId }) => {
    const page = await confluenceClient.get(`/rest/api/content/${pageId}?expand=ancestors`);
    
    return {
      contents: [{
        uri: uri.href,
        mimeType: "application/json",
        text: JSON.stringify({
          ancestors: page.ancestors || [],
          metadata: createStandardMetadata(page.ancestors?.length || 0, page.ancestors?.length || 0, 0, uri.href)
        }),
        schema: pageListSchema
      }]
    };
  }
);
```

**Sang v2:**
```typescript
// Danh sách tổ tiên
server.resource(
  "confluence-page-ancestors",
  new ResourceTemplate("confluence://pages/{pageId}/ancestors", { pageId: undefined }),
  async (uri, { pageId }) => {
    const ancestors = await confluenceClient.get(`/wiki/api/v2/pages/${pageId}/ancestors`);
    
    return {
      contents: [{
        uri: uri.href,
        mimeType: "application/json",
        text: JSON.stringify({
          ancestors: ancestors.results,
          metadata: {
            total: ancestors.results.length,
            limit: ancestors.results.length,
            hasMore: false,
            links: {
              self: uri.href
            }
          }
        }),
        schema: pageListSchemaV2
      }]
    };
  }
);
```

### 7. Page Labels

**Từ v1:**
```typescript
// Nhãn của trang
server.resource(
  "confluence-page-labels",
  new ResourceTemplate("confluence://pages/{pageId}/labels", { pageId: undefined }),
  async (uri, { pageId }) => {
    const labels = await confluenceClient.get(`/rest/api/content/${pageId}/label`);
    
    return {
      contents: [{
        uri: uri.href,
        mimeType: "application/json",
        text: JSON.stringify({
          labels: labels.results,
          metadata: createStandardMetadata(labels.size, labels.limit, labels.start, uri.href)
        }),
        schema: labelListSchema
      }]
    };
  }
);
```

**Sang v2:**
```typescript
// Nhãn của trang
server.resource(
  "confluence-page-labels",
  new ResourceTemplate("confluence://pages/{pageId}/labels", { pageId: undefined }),
  async (uri, { pageId }) => {
    const url = new URL(uri.href);
    const limit = parseInt(url.searchParams.get("limit") || "25");
    const cursor = url.searchParams.get("cursor") || undefined;
    
    const endpoint = `/wiki/api/v2/pages/${pageId}/labels${cursor ? `?cursor=${cursor}&limit=${limit}` : `?limit=${limit}`}`;
    const labels = await confluenceClient.get(endpoint);
    
    return {
      contents: [{
        uri: uri.href,
        mimeType: "application/json",
        text: JSON.stringify({
          labels: labels.results,
          metadata: {
            limit: limit,
            hasMore: !!labels._links.next,
            links: {
              self: uri.href,
              next: labels._links.next ? `${uri.href}?cursor=${new URL(labels._links.next).searchParams.get("cursor")}&limit=${limit}` : undefined
            }
          }
        }),
        schema: labelListSchemaV2
      }]
    };
  }
);
```

### 8. Page Attachments

**Từ v1:**
```typescript
// Tập tin đính kèm
server.resource(
  "confluence-page-attachments",
  new ResourceTemplate("confluence://pages/{pageId}/attachments", { pageId: undefined }),
  async (uri, { pageId }) => {
    const attachments = await confluenceClient.get(`/rest/api/content/${pageId}/child/attachment`);
    
    return {
      contents: [{
        uri: uri.href,
        mimeType: "application/json",
        text: JSON.stringify({
          attachments: attachments.results,
          metadata: createStandardMetadata(attachments.size, attachments.limit, attachments.start, uri.href)
        }),
        schema: attachmentListSchema
      }]
    };
  }
);
```

**Sang v2:**
```typescript
// Tập tin đính kèm
server.resource(
  "confluence-page-attachments",
  new ResourceTemplate("confluence://pages/{pageId}/attachments", { pageId: undefined }),
  async (uri, { pageId }) => {
    const url = new URL(uri.href);
    const limit = parseInt(url.searchParams.get("limit") || "25");
    const cursor = url.searchParams.get("cursor") || undefined;
    
    const endpoint = `/wiki/api/v2/pages/${pageId}/attachments${cursor ? `?cursor=${cursor}&limit=${limit}` : `?limit=${limit}`}`;
    const attachments = await confluenceClient.get(endpoint);
    
    return {
      contents: [{
        uri: uri.href,
        mimeType: "application/json",
        text: JSON.stringify({
          attachments: attachments.results,
          metadata: {
            limit: limit,
            hasMore: !!attachments._links.next,
            links: {
              self: uri.href,
              next: attachments._links.next ? `${uri.href}?cursor=${new URL(attachments._links.next).searchParams.get("cursor")}&limit=${limit}` : undefined
            }
          }
        }),
        schema: attachmentListSchemaV2
      }]
    };
  }
);
```

### 9. Page Versions

**Từ v1:**
```typescript
// Lịch sử phiên bản
server.resource(
  "confluence-page-versions",
  new ResourceTemplate("confluence://pages/{pageId}/versions", { pageId: undefined }),
  async (uri, { pageId }) => {
    const versions = await confluenceClient.get(`/rest/api/content/${pageId}/version`);
    
    return {
      contents: [{
        uri: uri.href,
        mimeType: "application/json",
        text: JSON.stringify({
          versions: versions.results,
          metadata: createStandardMetadata(versions.size, versions.limit, versions.start, uri.href)
        }),
        schema: versionListSchema
      }]
    };
  }
);
```

**Sang v2:**
```typescript
// Lịch sử phiên bản
server.resource(
  "confluence-page-versions",
  new ResourceTemplate("confluence://pages/{pageId}/versions", { pageId: undefined }),
  async (uri, { pageId }) => {
    const url = new URL(uri.href);
    const limit = parseInt(url.searchParams.get("limit") || "25");
    const cursor = url.searchParams.get("cursor") || undefined;
    
    const endpoint = `/wiki/api/v2/pages/${pageId}/versions${cursor ? `?cursor=${cursor}&limit=${limit}` : `?limit=${limit}`}`;
    const versions = await confluenceClient.get(endpoint);
    
    return {
      contents: [{
        uri: uri.href,
        mimeType: "application/json",
        text: JSON.stringify({
          versions: versions.results,
          metadata: {
            limit: limit,
            hasMore: !!versions._links.next,
            links: {
              self: uri.href,
              next: versions._links.next ? `${uri.href}?cursor=${new URL(versions._links.next).searchParams.get("cursor")}&limit=${limit}` : undefined
            }
          }
        }),
        schema: versionListSchemaV2
      }]
    };
  }
);
```

## Migration cho Tools

### 1. createPage

**Từ v1:**
```typescript
// Tạo trang mới
server.tool(
  "createPage",
  z.object({
    spaceKey: z.string().describe("Space key"),
    title: z.string().describe("Page title"),
    content: z.string().describe("Page content (HTML)"),
    parentId: z.string().optional().describe("Parent page ID")
  }),
  async (params) => {
    const payload = {
      type: "page",
      title: params.title,
      space: { key: params.spaceKey },
      body: {
        storage: {
          value: params.content,
          representation: "storage"
        }
      }
    };
    
    if (params.parentId) {
      payload.ancestors = [{ id: params.parentId }];
    }
    
    const response = await confluenceClient.post('/rest/api/content', payload);
    
    return {
      content: [{
        type: "text",
        text: `Page created successfully with ID: ${response.id}`
      }]
    };
  }
);
```

**Sang v2:**
```typescript
// Tạo trang mới
server.tool(
  "createPage",
  z.object({
    spaceKey: z.string().describe("Space key"),
    title: z.string().describe("Page title"),
    content: z.string().describe("Page content (HTML)"),
    parentId: z.string().optional().describe("Parent page ID")
  }),
  async (params) => {
    const payload = {
      spaceId: params.spaceKey,
      title: params.title,
      body: {
        representation: "storage",
        value: params.content
      }
    };
    
    if (params.parentId) {
      payload.parentId = params.parentId;
    }
    
    const response = await confluenceClient.post('/wiki/api/v2/pages', payload);
    
    return {
      content: [{
        type: "text",
        text: `Page created successfully with ID: ${response.id}`
      }]
    };
  }
);
```

### 2. updatePage

**Từ v1:**
```typescript
// Cập nhật trang
server.tool(
  "updatePage",
  z.object({
    pageId: z.string().describe("Page ID"),
    title: z.string().optional().describe("New page title"),
    content: z.string().optional().describe("New page content (HTML)"),
    version: z.number().describe("Current page version"),
    addLabels: z.array(z.string()).optional().describe("Labels to add"),
    removeLabels: z.array(z.string()).optional().describe("Labels to remove")
  }),
  async (params) => {
    // Get current page
    const currentPage = await confluenceClient.get(`/rest/api/content/${params.pageId}?expand=version`);
    
    // Update page content
    const payload = {
      type: "page",
      title: params.title || currentPage.title,
      version: {
        number: params.version + 1
      }
    };
    
    if (params.content) {
      payload.body = {
        storage: {
          value: params.content,
          representation: "storage"
        }
      };
    }
    
    const response = await confluenceClient.put(`/rest/api/content/${params.pageId}`, payload);
    
    // Add labels if specified
    if (params.addLabels && params.addLabels.length > 0) {
      const labelObjects = params.addLabels.map(label => ({ name: label }));
      await confluenceClient.post(`/rest/api/content/${params.pageId}/label`, labelObjects);
    }
    
    // Remove labels if specified
    if (params.removeLabels && params.removeLabels.length > 0) {
      for (const label of params.removeLabels) {
        await confluenceClient.delete(`/rest/api/content/${params.pageId}/label?name=${label}`);
      }
    }
    
    return {
      content: [{
        type: "text",
        text: `Page ${params.pageId} updated successfully to version ${response.version.number}`
      }]
    };
  }
);
```

**Sang v2:**
```typescript
// Cập nhật trang
server.tool(
  "updatePage",
  z.object({
    pageId: z.string().describe("Page ID"),
    title: z.string().optional().describe("New page title"),
    content: z.string().optional().describe("New page content (HTML)"),
    version: z.number().describe("Current page version"),
    addLabels: z.array(z.string()).optional().describe("Labels to add"),
    removeLabels: z.array(z.string()).optional().describe("Labels to remove")
  }),
  async (params) => {
    // Update page title if specified
    if (params.title) {
      await confluenceClient.put(`/wiki/api/v2/pages/${params.pageId}`, {
        id: params.pageId,
        status: "current",
        title: params.title,
        version: {
          number: params.version + 1
        }
      });
    }
    
    // Update page content if specified
    if (params.content) {
      await confluenceClient.put(`/wiki/api/v2/pages/${params.pageId}/body`, {
        representation: "storage",
        value: params.content,
        version: {
          number: params.version + (params.title ? 2 : 1)
        }
      });
    }
    
    // Add labels if specified
    if (params.addLabels && params.addLabels.length > 0) {
      const labelObjects = params.addLabels.map(label => ({ name: label }));
      await confluenceClient.post(`/wiki/api/v2/pages/${params.pageId}/labels`, labelObjects);
    }
    
    // Remove labels if specified
    if (params.removeLabels && params.removeLabels.length > 0) {
      for (const label of params.removeLabels) {
        await confluenceClient.delete(`/wiki/api/v2/pages/${params.pageId}/labels/${label}`);
      }
    }
    
    return {
      content: [{
        type: "text",
        text: `Page ${params.pageId} updated successfully`
      }]
    };
  }
);
```

### 3. addComment

**Từ v1:**
```typescript
// Thêm comment vào page
server.tool(
  "addComment",
  z.object({
    pageId: z.string().describe("Page ID"),
    content: z.string().describe("Comment content (HTML)")
  }),
  async (params) => {
    const payload = {
      type: "comment",
      container: {
        id: params.pageId,
        type: "page"
      },
      body: {
        storage: {
          value: params.content,
          representation: "storage"
        }
      }
    };
    
    const response = await confluenceClient.post('/rest/api/content', payload);
    
    return {
      content: [{
        type: "text",
        text: `Comment added successfully with ID: ${response.id}`
      }]
    };
  }
);
```

**Sang v2:**
```typescript
// Thêm comment vào page
server.tool(
  "addComment",
  z.object({
    pageId: z.string().describe("Page ID"),
    content: z.string().describe("Comment content (HTML)")
  }),
  async (params) => {
    const payload = {
      body: {
        representation: "storage",
        value: params.content
      }
    };
    
    const response = await confluenceClient.post(`/wiki/api/v2/pages/${params.pageId}/comments`, payload);
    
    return {
      content: [{
        type: "text",
        text: `Comment added successfully with ID: ${response.id}`
      }]
    };
  }
);
```

### 4. addLabelsToPage

**Từ v1:**
```typescript
// Thêm nhãn vào trang
server.tool(
  "addLabelsToPage",
  z.object({
    pageId: z.string().describe("Page ID"),
    labels: z.array(z.string()).describe("Labels to add")
  }),
  async (params) => {
    const labelObjects = params.labels.map(label => ({ name: label }));
    
    await confluenceClient.post(`/rest/api/content/${params.pageId}/label`, labelObjects);
    
    return {
      content: [{
        type: "text",
        text: `Labels added to page ${params.pageId} successfully`
      }]
    };
  }
);
```

**Sang v2:**
```typescript
// Thêm nhãn vào trang
server.tool(
  "addLabelsToPage",
  z.object({
    pageId: z.string().describe("Page ID"),
    labels: z.array(z.string()).describe("Labels to add")
  }),
  async (params) => {
    const labelObjects = params.labels.map(label => ({ name: label }));
    
    await confluenceClient.post(`/wiki/api/v2/pages/${params.pageId}/labels`, labelObjects);
    
    return {
      content: [{
        type: "text",
        text: `Labels added to page ${params.pageId} successfully`
      }]
    };
  }
);
```

### 5. removeLabelsFromPage

**Từ v1:**
```typescript
// Xóa nhãn khỏi trang
server.tool(
  "removeLabelsFromPage",
  z.object({
    pageId: z.string().describe("Page ID"),
    labels: z.array(z.string()).describe("Labels to remove")
  }),
  async (params) => {
    for (const label of params.labels) {
      await confluenceClient.delete(`/rest/api/content/${params.pageId}/label?name=${label}`);
    }
    
    return {
      content: [{
        type: "text",
        text: `Labels removed from page ${params.pageId} successfully`
      }]
    };
  }
);
```

**Sang v2:**
```typescript
// Xóa nhãn khỏi trang
server.tool(
  "removeLabelsFromPage",
  z.object({
    pageId: z.string().describe("Page ID"),
    labels: z.array(z.string()).describe("Labels to remove")
  }),
  async (params) => {
    for (const label of params.labels) {
      await confluenceClient.delete(`/wiki/api/v2/pages/${params.pageId}/labels/${label}`);
    }
    
    return {
      content: [{
        type: "text",
        text: `Labels removed from page ${params.pageId} successfully`
      }]
    };
  }
);
```

## Cập nhật Schema

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:

```typescript
// Schema cho Space v2
const spaceSchemaV2 = {
  type: "object",
  properties: {
    id: { type: "string", description: "Space ID" },
    key: { type: "string", description: "Space key" },
    name: { type: "string", description: "Space name" },
    type: { type: "string", description: "Space type" },
    status: { type: "string", description: "Space status" },
    description: { 
      type: "object", 
      properties: {
        plain: { type: "object", properties: { value: { type: "string" } } },
        view: { type: "object", properties: { value: { type: "string" } } }
      }
    },
    _links: { type: "object", description: "Links related to the space" }
  }
};

// Schema cho Page v2
const pageSchemaV2 = {
  type: "object",
  properties: {
    id: { type: "string", description: "Page ID" },
    title: { type: "string", description: "Page title" },
    status: { type: "string", description: "Page status" },
    spaceId: { type: "string", description: "Space ID" },
    parentId: { type: "string", description: "Parent page ID" },
    authorId: { type: "string", description: "Author ID" },
    createdAt: { type: "string", description: "Creation date" },
    version: { 
      type: "object", 
      properties: {
        number: { type: "number", description: "Version number" },
        createdAt: { type: "string", description: "Version creation date" }
      }
    },
    body: { type: "string", description: "Page content (converted from body object)" },
    bodyType: { type: "string", description: "Content representation type" },
    _links: { type: "object", description: "Links related to the page" }
  }
};

// Các schema khác cũng cần được cập nhật tương tự
```

## Lưu ý quan trọng

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.

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

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.

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.

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.

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.

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.

> **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 3/3FirstPrevNextLast