All skills
jeffallan avatar

/atlassian-mcp

@efebc44
by jeffallanjeffallan/claude-skills12k stars
1,124

Integrates with Atlassian products to manage project tracking and documentation via MCP protocol. Use when querying Jira issues with JQL filters, creating and updating tickets with custom fields, searching or editing Confluence pages with CQL, managing sprints and backlogs, setting up MCP server authentication, syncing documentation, or debugging Atlassian API integrations.

Use this Skill: https://skilld.dev/gh/jeffallan/claude-skills/atlassian-mcp

This session only. Nothing lands on disk.

referencescommon-workflows.md

≈4.7k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Common Workflows


Issue Triage Workflow

Automatically categorize, prioritize, and route incoming issues.

Triage Bot Implementation

interface TriageResult {
  issueKey: string;
  category: string;
  priority: string;
  assignee: string | null;
  labels: string[];
  actions: string[];
}

async function triageNewIssue(
  client: MCPClient,
  issueKey: string
): Promise<TriageResult> {
  // Get issue details
  const issueResult = await client.callTool({
    name: "jira_get_issue",
    arguments: {
      issue_key: issueKey,
      expand: ["changelog"]
    }
  });

  const issue = JSON.parse(issueResult.content[0].text);
  const result: TriageResult = {
    issueKey,
    category: "uncategorized",
    priority: issue.fields.priority?.name || "Medium",
    assignee: null,
    labels: [],
    actions: []
  };

  // Categorize based on content
  const summary = issue.fields.summary.toLowerCase();
  const description = (issue.fields.description?.content?.[0]?.content?.[0]?.text || "").toLowerCase();
  const text = `${summary} ${description}`;

  // Category detection
  if (text.match(/\b(crash|down|outage|critical|emergency)\b/)) {
    result.category = "incident";
    result.priority = "Highest";
    result.labels.push("incident", "urgent");
  } else if (text.match(/\b(bug|error|fail|broken|doesn't work)\b/)) {
    result.category = "bug";
    result.labels.push("bug", "needs-investigation");
  } else if (text.match(/\b(feature|enhancement|request|add|improve)\b/)) {
    result.category = "feature";
    result.labels.push("feature-request");
  } else if (text.match(/\b(docs|documentation|readme|guide)\b/)) {
    result.category = "documentation";
    result.labels.push("documentation");
  }

  // Component detection for routing
  const componentMap: Record<string, { component: string; team: string }> = {
    "api|rest|endpoint": { component: "API", team: "backend-team" },
    "ui|button|page|screen|css": { component: "Frontend", team: "frontend-team" },
    "database|sql|query|migration": { component: "Database", team: "data-team" },
    "auth|login|password|sso": { component: "Authentication", team: "security-team" },
    "deploy|ci|cd|pipeline": { component: "DevOps", team: "platform-team" }
  };

  for (const [pattern, { component, team }] of Object.entries(componentMap)) {
    if (text.match(new RegExp(`\\b(${pattern})\\b`))) {
      result.labels.push(component.toLowerCase());
      result.actions.push(`Route to ${team}`);
      break;
    }
  }

  // Apply triage results
  await client.callTool({
    name: "jira_update_issue",
    arguments: {
      issue_key: issueKey,
      fields: {
        priority: { name: result.priority },
        labels: [...new Set([...issue.fields.labels || [], ...result.labels])]
      }
    }
  });

  // Add triage comment
  await client.callTool({
    name: "jira_add_comment",
    arguments: {
      issue_key: issueKey,
      body: `*Automated Triage Results*

Category: ${result.category}
Priority: ${result.priority}
Labels added: ${result.labels.join(", ")}

${result.actions.length > 0 ? `Recommended actions:\n${result.actions.map(a => `- ${a}`).join("\n")}` : ""}`
    }
  });

  return result;
}

Bulk Triage New Issues

async function triageBacklog(client: MCPClient, projectKey: string): Promise<void> {
  // Find untriaged issues
  const searchResult = await client.callTool({
    name: "jira_search",
    arguments: {
      jql: `project = ${projectKey} AND labels IS EMPTY AND created >= -7d AND resolution IS EMPTY`,
      max_results: 50,
      fields: ["summary", "description", "priority", "labels"]
    }
  });

  const response = JSON.parse(searchResult.content[0].text);
  console.log(`Found ${response.total} issues to triage`);

  for (const issue of response.issues) {
    const result = await triageNewIssue(client, issue.key);
    console.log(`Triaged ${issue.key}: ${result.category} (${result.priority})`);

    // Rate limiting
    await delay(500);
  }
}

Documentation Sync Workflow

Keep Confluence documentation in sync with Jira issues.

Generate Release Notes

async function generateReleaseNotes(
  client: MCPClient,
  projectKey: string,
  version: string,
  confluenceSpaceKey: string
): Promise<string> {
  // Get all issues in the release
  const searchResult = await client.callTool({
    name: "jira_search",
    arguments: {
      jql: `project = ${projectKey} AND fixVersion = "${version}" AND resolution = Done ORDER BY issuetype, priority DESC`,
      max_results: 200,
      fields: ["summary", "issuetype", "priority", "assignee", "labels"]
    }
  });

  const response = JSON.parse(searchResult.content[0].text);

  // Group by issue type
  const grouped: Record<string, any[]> = {};
  for (const issue of response.issues) {
    const type = issue.fields.issuetype.name;
    if (!grouped[type]) grouped[type] = [];
    grouped[type].push(issue);
  }

  // Build Confluence page content
  let content = `
<h2>Release ${version}</h2>
<p>Released on ${new Date().toISOString().split('T')[0]}</p>

<ac:structured-macro ac:name="toc">
  <ac:parameter ac:name="maxLevel">2</ac:parameter>
</ac:structured-macro>

<h2>Summary</h2>
<p>This release includes ${response.total} changes.</p>
<table>
  <tr><th>Type</th><th>Count</th></tr>
  ${Object.entries(grouped).map(([type, issues]) =>
    `<tr><td>${type}</td><td>${issues.length}</td></tr>`
  ).join('')}
</table>
`;

  // Add sections for each issue type
  const typeOrder = ["New Feature", "Improvement", "Bug", "Task"];
  const orderedTypes = [...new Set([...typeOrder, ...Object.keys(grouped)])];

  for (const type of orderedTypes) {
    if (!grouped[type]) continue;

    content += `
<h2>${type}s</h2>
<table>
  <tr><th>Key</th><th>Summary</th><th>Assignee</th></tr>
  ${grouped[type].map(issue => `
    <tr>
      <td><ac:structured-macro ac:name="jira"><ac:parameter ac:name="key">${issue.key}</ac:parameter></ac:structured-macro></td>
      <td>${escapeHtml(issue.fields.summary)}</td>
      <td>${issue.fields.assignee?.displayName || 'Unassigned'}</td>
    </tr>
  `).join('')}
</table>
`;
  }

  // Create or update Confluence page
  const pageTitle = `Release Notes - ${version}`;

  // Check if page exists
  const existingSearch = await client.callTool({
    name: "confluence_search",
    arguments: {
      cql: `space = "${confluenceSpaceKey}" AND title = "${pageTitle}"`,
      limit: 1
    }
  });

  const existingResults = JSON.parse(existingSearch.content[0].text);

  if (existingResults.results.length > 0) {
    // Update existing page
    const pageId = existingResults.results[0].id;
    const currentVersion = existingResults.results[0].version.number;

    await client.callTool({
      name: "confluence_update_page",
      arguments: {
        page_id: pageId,
        title: pageTitle,
        body: content,
        version_number: currentVersion + 1,
        version_message: `Updated release notes for ${version}`
      }
    });

    return pageId;
  } else {
    // Create new page
    const newPage = await client.callTool({
      name: "confluence_create_page",
      arguments: {
        space_key: confluenceSpaceKey,
        title: pageTitle,
        body: content,
        labels: ["release-notes", `version-${version.replace(/\./g, '-')}`]
      }
    });

    return JSON.parse(newPage.content[0].text).id;
  }
}

Sync Meeting Notes to Jira

interface ActionItem {
  description: string;
  assignee: string;
  dueDate?: string;
}

async function syncMeetingNotes(
  client: MCPClient,
  confluencePageId: string,
  jiraProjectKey: string
): Promise<string[]> {
  // Get meeting notes content
  const pageResult = await client.callTool({
    name: "confluence_get_page",
    arguments: {
      page_id: confluencePageId,
      expand: ["body.storage"]
    }
  });

  const page = JSON.parse(pageResult.content[0].text);
  const content = page.body.storage.value;

  // Extract action items (look for checkbox patterns)
  const actionItems = extractActionItems(content);
  const createdIssues: string[] = [];

  for (const item of actionItems) {
    // Create Jira task
    const newIssue = await client.callTool({
      name: "jira_create_issue",
      arguments: {
        project_key: jiraProjectKey,
        issue_type: "Task",
        summary: item.description,
        description: {
          type: "doc",
          version: 1,
          content: [{
            type: "paragraph",
            content: [{
              type: "text",
              text: `Action item from meeting: ${page.title}\n\nSource: ${page._links.webui}`
            }]
          }]
        },
        assignee: item.assignee,
        due_date: item.dueDate,
        labels: ["meeting-action", "auto-created"]
      }
    });

    const issueKey = JSON.parse(newIssue.content[0].text).key;
    createdIssues.push(issueKey);
  }

  // Update Confluence page with Jira links
  if (createdIssues.length > 0) {
    const jiraLinksSection = `
<h2>Linked Jira Issues</h2>
<ac:structured-macro ac:name="jira">
  <ac:parameter ac:name="jqlQuery">key IN (${createdIssues.join(',')})</ac:parameter>
  <ac:parameter ac:name="columns">key,summary,status,assignee</ac:parameter>
</ac:structured-macro>
`;

    await client.callTool({
      name: "confluence_update_page",
      arguments: {
        page_id: confluencePageId,
        title: page.title,
        body: content + jiraLinksSection,
        version_number: page.version.number + 1
      }
    });
  }

  return createdIssues;
}

function extractActionItems(html: string): ActionItem[] {
  // Parse action items from common patterns
  const items: ActionItem[] = [];

  // Pattern: [x] or [ ] followed by @mention and task description
  const taskPattern = /\[[ x]\]\s*@([a-zA-Z.]+)\s*[-:]\s*(.+?)(?=<|$)/gi;
  let match;

  while ((match = taskPattern.exec(html)) !== null) {
    items.push({
      assignee: match[1],
      description: match[2].trim()
    });
  }

  return items;
}

Sprint Automation Workflow

Automate sprint ceremonies and reporting.

Sprint Planning Assistant

interface SprintPlanningReport {
  totalPoints: number;
  byAssignee: Record<string, number>;
  byComponent: Record<string, number>;
  riskyItems: string[];
  recommendations: string[];
}

async function analyzeSprintPlanning(
  client: MCPClient,
  boardId: number,
  sprintId: number
): Promise<SprintPlanningReport> {
  // Get sprint issues
  const searchResult = await client.callTool({
    name: "jira_search",
    arguments: {
      jql: `sprint = ${sprintId}`,
      max_results: 100,
      fields: ["summary", "assignee", "customfield_10001", "components", "labels", "priority"]
    }
  });

  const response = JSON.parse(searchResult.content[0].text);
  const issues = response.issues;

  const report: SprintPlanningReport = {
    totalPoints: 0,
    byAssignee: {},
    byComponent: {},
    riskyItems: [],
    recommendations: []
  };

  for (const issue of issues) {
    const points = issue.fields.customfield_10001 || 0; // Story points
    const assignee = issue.fields.assignee?.displayName || "Unassigned";
    const components = issue.fields.components?.map((c: any) => c.name) || ["No Component"];

    report.totalPoints += points;
    report.byAssignee[assignee] = (report.byAssignee[assignee] || 0) + points;

    for (const component of components) {
      report.byComponent[component] = (report.byComponent[component] || 0) + points;
    }

    // Identify risks
    if (points === 0) {
      report.riskyItems.push(`${issue.key}: No story points estimated`);
    }
    if (points >= 8) {
      report.riskyItems.push(`${issue.key}: Large story (${points} points) - consider breaking down`);
    }
    if (assignee === "Unassigned") {
      report.riskyItems.push(`${issue.key}: No assignee`);
    }
  }

  // Generate recommendations
  const avgPointsPerPerson = report.totalPoints / Object.keys(report.byAssignee).length;

  for (const [assignee, points] of Object.entries(report.byAssignee)) {
    if (points > avgPointsPerPerson * 1.5) {
      report.recommendations.push(`${assignee} has ${points} points (${Math.round((points / avgPointsPerPerson - 1) * 100)}% above average) - consider rebalancing`);
    }
  }

  return report;
}

Sprint Retrospective Generator

async function generateRetroBoard(
  client: MCPClient,
  boardId: number,
  sprintId: number,
  confluenceSpaceKey: string
): Promise<string> {
  // Get completed sprint data
  const sprintResult = await client.callTool({
    name: "jira_get_sprint_report",
    arguments: {
      board_id: boardId,
      sprint_id: sprintId
    }
  });

  const sprintReport = JSON.parse(sprintResult.content[0].text);

  // Get sprint issues with details
  const issuesResult = await client.callTool({
    name: "jira_search",
    arguments: {
      jql: `sprint = ${sprintId}`,
      max_results: 100,
      fields: ["summary", "status", "resolution", "customfield_10001", "assignee"]
    }
  });

  const issues = JSON.parse(issuesResult.content[0].text).issues;

  // Calculate metrics
  const completed = issues.filter((i: any) => i.fields.resolution);
  const incomplete = issues.filter((i: any) => !i.fields.resolution);
  const completedPoints = completed.reduce((sum: number, i: any) => sum + (i.fields.customfield_10001 || 0), 0);
  const plannedPoints = issues.reduce((sum: number, i: any) => sum + (i.fields.customfield_10001 || 0), 0);

  // Create retrospective page
  const content = `
<h2>Sprint Retrospective: ${sprintReport.sprint.name}</h2>
<p><em>Generated on ${new Date().toISOString().split('T')[0]}</em></p>

<h3>Sprint Metrics</h3>
<table>
  <tr><th>Metric</th><th>Value</th></tr>
  <tr><td>Start Date</td><td>${sprintReport.sprint.startDate}</td></tr>
  <tr><td>End Date</td><td>${sprintReport.sprint.endDate}</td></tr>
  <tr><td>Issues Planned</td><td>${issues.length}</td></tr>
  <tr><td>Issues Completed</td><td>${completed.length}</td></tr>
  <tr><td>Completion Rate</td><td>${Math.round(completed.length / issues.length * 100)}%</td></tr>
  <tr><td>Points Planned</td><td>${plannedPoints}</td></tr>
  <tr><td>Points Completed</td><td>${completedPoints}</td></tr>
  <tr><td>Velocity</td><td>${completedPoints} points</td></tr>
</table>

<h3>Completed Items</h3>
<ac:structured-macro ac:name="jira">
  <ac:parameter ac:name="jqlQuery">sprint = ${sprintId} AND resolution IS NOT EMPTY</ac:parameter>
  <ac:parameter ac:name="columns">key,summary,assignee</ac:parameter>
</ac:structured-macro>

${incomplete.length > 0 ? `
<h3>Incomplete Items (Spillover)</h3>
<ac:structured-macro ac:name="warning">
  <ac:rich-text-body>
    <p>${incomplete.length} items were not completed and will spill over to the next sprint.</p>
  </ac:rich-text-body>
</ac:structured-macro>
<ac:structured-macro ac:name="jira">
  <ac:parameter ac:name="jqlQuery">sprint = ${sprintId} AND resolution IS EMPTY</ac:parameter>
  <ac:parameter ac:name="columns">key,summary,assignee,status</ac:parameter>
</ac:structured-macro>
` : ''}

<h3>Discussion Points</h3>
<ac:structured-macro ac:name="expand">
  <ac:parameter ac:name="title">What went well?</ac:parameter>
  <ac:rich-text-body>
    <ul>
      <li><em>Add team input here...</em></li>
    </ul>
  </ac:rich-text-body>
</ac:structured-macro>

<ac:structured-macro ac:name="expand">
  <ac:parameter ac:name="title">What could be improved?</ac:parameter>
  <ac:rich-text-body>
    <ul>
      <li><em>Add team input here...</em></li>
    </ul>
  </ac:rich-text-body>
</ac:structured-macro>

<ac:structured-macro ac:name="expand">
  <ac:parameter ac:name="title">Action Items</ac:parameter>
  <ac:rich-text-body>
    <ac:task-list>
      <ac:task><ac:task-body><em>Add action items here...</em></ac:task-body></ac:task>
    </ac:task-list>
  </ac:rich-text-body>
</ac:structured-macro>
`;

  const newPage = await client.callTool({
    name: "confluence_create_page",
    arguments: {
      space_key: confluenceSpaceKey,
      title: `Sprint Retro - ${sprintReport.sprint.name}`,
      body: content,
      labels: ["sprint-retro", "team-meeting"]
    }
  });

  return JSON.parse(newPage.content[0].text).id;
}

Issue-Documentation Link Workflow

Bidirectional linking between code changes and documentation.

Link PR to Documentation

async function linkPRToDocumentation(
  client: MCPClient,
  issueKey: string,
  docPageId: string
): Promise<void> {
  // Add remote link to Jira issue
  await client.callTool({
    name: "jira_add_remote_link",
    arguments: {
      issue_key: issueKey,
      url: `https://your-site.atlassian.net/wiki/spaces/DEV/pages/${docPageId}`,
      title: "Related Documentation",
      icon_url: "https://your-site.atlassian.net/wiki/favicon.ico"
    }
  });

  // Add Jira macro to Confluence page
  const pageResult = await client.callTool({
    name: "confluence_get_page",
    arguments: {
      page_id: docPageId,
      expand: ["body.storage", "version"]
    }
  });

  const page = JSON.parse(pageResult.content[0].text);

  // Check if link already exists
  if (page.body.storage.value.includes(issueKey)) {
    return; // Already linked
  }

  const jiraLink = `
<ac:structured-macro ac:name="info">
  <ac:parameter ac:name="title">Related Issue</ac:parameter>
  <ac:rich-text-body>
    <p><ac:structured-macro ac:name="jira"><ac:parameter ac:name="key">${issueKey}</ac:parameter></ac:structured-macro></p>
  </ac:rich-text-body>
</ac:structured-macro>
`;

  await client.callTool({
    name: "confluence_update_page",
    arguments: {
      page_id: docPageId,
      title: page.title,
      body: jiraLink + page.body.storage.value,
      version_number: page.version.number + 1
    }
  });
}

Utility Functions

function escapeHtml(text: string): string {
  return text
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#039;');
}

function delay(ms: number): Promise<void> {
  return new Promise(resolve => setTimeout(resolve, ms));
}

async function withRetry<T>(
  operation: () => Promise<T>,
  maxRetries = 3,
  baseDelay = 1000
): Promise<T> {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      return await operation();
    } catch (error: any) {
      if (attempt === maxRetries) throw error;

      const delay = baseDelay * Math.pow(2, attempt - 1);
      console.log(`Attempt ${attempt} failed, retrying in ${delay}ms...`);
      await new Promise(r => setTimeout(r, delay));
    }
  }
  throw new Error('Unreachable');
}

Related References

  • jira-queries.md - JQL syntax for workflow queries
  • confluence-operations.md - Page creation and formatting
  • authentication-patterns.md - Secure API access for automated workflows

Source: SKILL.md on GitHub

2 alerts16d5 checks · Risk CRITICAL
  • Gen Agent Trust Hub16d

    This skill provides developer guidance for integrating Atlassian products with the Model Context Protocol (MCP). While the content is primarily educational, automated scanners have flagged the documentation URL and the skill's main entry point. The skill also recommends the use of community-maintained packages from non-trusted sources and provides workflows vulnerable to indirect prompt injection.

  • Socket16d

    1 alert: gptAnomaly

  • Snyk16d

    Risk: MEDIUM · 1 issue

  • Runlayer6mo

    4/6 files flagged

  • ZeroLeaks5mo

    Score: 93/100 · 2 sections analyzed

Signed by skilld at efebc44. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 2 months ago.

Steadyupdated 5 months ago
Other metadata
metadata
{
  "author": "https://github.com/Jeffallan",
  "version": "1.1.0",
  "domain": "platform",
  "triggers": "Jira, Confluence, Atlassian, MCP, tickets, issues, wiki, JQL, CQL, sprint, backlog, project management",
  "role": "expert",
  "scope": "implementation",
  "output-format": "code",
  "related-skills": "mcp-developer, api-designer, security-reviewer"
}
  • MCP
  • jira
  • confluence
  • atlassian
  • jql
  • cql
  • project-management
  • api-integration
  • authentication

README badge

README badge for jeffallan/claude-skills/atlassian-mcp

Integrates Claude with Jira and Confluence via MCP protocol to query issues with JQL, create and update tickets, search pages with CQL, and manage sprints and backlogs. Handles OAuth and API token authentication, pagination, rate limits, and permission validation for Atlassian cloud and self-hosted instances.

Generated from the current SKILL.md.

Does this skill work with self-hosted Jira and Confluence instances?
Yes. The skill supports official cloud, open-source, and self-hosted MCP servers. You configure the server URL and authentication method (OAuth 2.0, API tokens, or PAT) in the MCP configuration.
What authentication methods are supported?
The skill supports OAuth 2.0, API tokens, and personal access tokens (PAT). Credentials must be loaded from environment variables or a secrets manager, never hardcoded.
Can I automate sprint and backlog management with this skill?
Yes. The skill covers JQL queries for sprints and backlogs, issue linking, and bulk operations. It includes patterns for sprint workflows, issue triage, and board updates with required permission validation.
How do I avoid hitting Atlassian API rate limits?
The skill requires implementing exponential backoff, pagination (50-100 items per page), and monitoring rate limit headers from Atlassian APIs. Always probe with `maxResults=1` before executing large queries.
Does this skill handle Confluence page creation and editing?
Yes. The skill covers CQL search syntax, page creation, space management, and comments. Reference documentation includes Confluence-specific operations and workflow examples.

Generated from the current SKILL.md. These answers refresh after source changes.