Skip to main content
Glama

Atlassian MCP Server

by kompallik
index.ts15.7 kB
#!/usr/bin/env node /** * Atlassian MCP server for JIRA and Confluence integration. * This server provides tools to interact with JIRA tickets and Confluence pages. */ import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, ErrorCode, McpError, } from "@modelcontextprotocol/sdk/types.js"; import axios from "axios"; import fs from "fs"; import path from "path"; // Configuration handling let config: { atlassian: { baseUrl: string; email: string; token: string; }; server: { name: string; version: string; }; }; // Try to load config from file const configPath = process.env.ATLASSIAN_CONFIG_PATH || path.join(process.cwd(), "config", "config.json"); try { if (fs.existsSync(configPath)) { console.error(`Loading config from ${configPath}`); config = JSON.parse(fs.readFileSync(configPath, "utf8")); } else { // Fallback to environment variables console.error(`Config file not found at ${configPath}, using environment variables`); config = { atlassian: { baseUrl: process.env.ATLASSIAN_BASE_URL || "", email: process.env.ATLASSIAN_EMAIL || "", token: process.env.ATLASSIAN_TOKEN || "", }, server: { name: process.env.SERVER_NAME || "atlassian-server", version: process.env.SERVER_VERSION || "0.1.0", }, }; } } catch (error) { console.error(`Error loading config: ${error}`); process.exit(1); } // Validate required configuration if (!config.atlassian.baseUrl) { throw new Error("Atlassian base URL is required in config or ATLASSIAN_BASE_URL environment variable"); } if (!config.atlassian.email) { throw new Error("Atlassian email is required in config or ATLASSIAN_EMAIL environment variable"); } if (!config.atlassian.token) { throw new Error("Atlassian token is required in config or ATLASSIAN_TOKEN environment variable"); } /** * Create an Axios instance with authentication headers * Using Basic authentication with the token as password and email as username */ const atlassianApi = axios.create({ baseURL: config.atlassian.baseUrl, headers: { // Using API token as password with Basic auth (email:token) Authorization: `Basic ${Buffer.from(`${config.atlassian.email}:${config.atlassian.token}`).toString('base64')}`, Accept: "application/json", "Content-Type": "application/json", }, }); // Add request/response logging for debugging atlassianApi.interceptors.request.use(request => { console.error('Request:', { method: request.method, url: request.url, headers: { ...request.headers, Authorization: 'Basic [REDACTED]' // Don't log the actual token }, data: request.data }); return request; }); atlassianApi.interceptors.response.use( response => { console.error('Response:', { status: response.status, statusText: response.statusText, headers: response.headers, data: response.data }); return response; }, error => { console.error('Error:', { message: error.message, response: error.response ? { status: error.response.status, statusText: error.response.statusText, data: error.response.data } : 'No response' }); return Promise.reject(error); } ); /** * Create an MCP server with capabilities for resources and tools */ const server = new Server( { name: config.server.name, version: config.server.version, }, { capabilities: { resources: {}, tools: {}, }, } ); /** * Handler for listing available JIRA tickets as resources */ server.setRequestHandler(ListResourcesRequestSchema, async () => { try { // Get recent JIRA tickets const response = await atlassianApi.get("/rest/api/3/search", { params: { maxResults: 10, fields: "summary,status,created,updated", }, }); const tickets = response.data.issues || []; return { resources: [ ...tickets.map((ticket: any) => ({ uri: `jira://ticket/${ticket.key}`, mimeType: "application/json", name: `JIRA Ticket: ${ticket.key}`, description: `${ticket.fields.summary} (${ticket.fields.status.name})`, })), { uri: "confluence://spaces", mimeType: "application/json", name: "Confluence Spaces", description: "List of available Confluence spaces", }, ], }; } catch (error) { console.error("Error listing resources:", error); return { resources: [] }; } }); /** * Handler for reading JIRA tickets and Confluence resources */ server.setRequestHandler(ReadResourceRequestSchema, async (request) => { try { const uri = request.params.uri; // Handle JIRA ticket resources if (uri.startsWith("jira://ticket/")) { const ticketKey = uri.replace("jira://ticket/", ""); const response = await atlassianApi.get(`/rest/api/3/issue/${ticketKey}`, { params: { fields: "summary,description,status,created,updated,assignee,reporter,priority,issuetype", }, }); return { contents: [{ uri: request.params.uri, mimeType: "application/json", text: JSON.stringify(response.data, null, 2), }], }; } // Handle Confluence spaces resource if (uri === "confluence://spaces") { const response = await atlassianApi.get("/wiki/rest/api/space", { params: { limit: 25, }, }); return { contents: [{ uri: request.params.uri, mimeType: "application/json", text: JSON.stringify(response.data, null, 2), }], }; } // Handle Confluence page resources if (uri.startsWith("confluence://page/")) { const pageId = uri.replace("confluence://page/", ""); const response = await atlassianApi.get(`/wiki/rest/api/content/${pageId}`, { params: { expand: "body.storage,version,space", }, }); return { contents: [{ uri: request.params.uri, mimeType: "application/json", text: JSON.stringify(response.data, null, 2), }], }; } throw new McpError( ErrorCode.InvalidRequest, `Unsupported resource URI: ${uri}` ); } catch (error) { console.error("Error reading resource:", error); if (axios.isAxiosError(error)) { throw new McpError( ErrorCode.InternalError, `Atlassian API error: ${error.response?.data?.message || error.message}` ); } throw error; } }); /** * Handler that lists available tools for JIRA and Confluence */ server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: "get_jira_ticket", description: "Get details of a JIRA ticket by key", inputSchema: { type: "object", properties: { ticket_key: { type: "string", description: "JIRA ticket key (e.g., CPDEV-3371)" } }, required: ["ticket_key"] } }, { name: "search_jira_tickets", description: "Search for JIRA tickets using JQL", inputSchema: { type: "object", properties: { jql: { type: "string", description: "JQL query string" }, max_results: { type: "number", description: "Maximum number of results to return", default: 10 } }, required: ["jql"] } }, { name: "create_jira_ticket", description: "Create a new JIRA ticket", inputSchema: { type: "object", properties: { project_key: { type: "string", description: "Project key (e.g., CPDEV)" }, summary: { type: "string", description: "Ticket summary/title" }, description: { type: "string", description: "Ticket description" }, issue_type: { type: "string", description: "Issue type (e.g., Bug, Task, Story)", default: "Task" } }, required: ["project_key", "summary", "description"] } }, { name: "add_comment_to_jira_ticket", description: "Add a comment to a JIRA ticket", inputSchema: { type: "object", properties: { ticket_key: { type: "string", description: "JIRA ticket key (e.g., CPDEV-3371)" }, comment: { type: "string", description: "Comment text" } }, required: ["ticket_key", "comment"] } }, { name: "get_confluence_page", description: "Get a Confluence page by ID", inputSchema: { type: "object", properties: { page_id: { type: "string", description: "Confluence page ID" } }, required: ["page_id"] } }, { name: "search_confluence", description: "Search for content in Confluence", inputSchema: { type: "object", properties: { query: { type: "string", description: "Search query" }, limit: { type: "number", description: "Maximum number of results", default: 10 } }, required: ["query"] } } ] }; }); /** * Handler for executing JIRA and Confluence tools */ server.setRequestHandler(CallToolRequestSchema, async (request) => { try { switch (request.params.name) { case "get_jira_ticket": { const ticketKey = String(request.params.arguments?.ticket_key); if (!ticketKey) { throw new McpError(ErrorCode.InvalidParams, "Ticket key is required"); } const response = await atlassianApi.get(`/rest/api/3/issue/${ticketKey}`, { params: { fields: "summary,description,status,created,updated,assignee,reporter,priority,issuetype", }, }); return { content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }] }; } case "search_jira_tickets": { const jql = String(request.params.arguments?.jql); const maxResults = Number(request.params.arguments?.max_results || 10); if (!jql) { throw new McpError(ErrorCode.InvalidParams, "JQL query is required"); } const response = await atlassianApi.get("/rest/api/3/search", { params: { jql, maxResults, fields: "summary,status,created,updated", }, }); return { content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }] }; } case "create_jira_ticket": { const projectKey = String(request.params.arguments?.project_key); const summary = String(request.params.arguments?.summary); const description = String(request.params.arguments?.description); const issueType = String(request.params.arguments?.issue_type || "Task"); if (!projectKey || !summary || !description) { throw new McpError(ErrorCode.InvalidParams, "Project key, summary, and description are required"); } const response = await atlassianApi.post("/rest/api/3/issue", { fields: { project: { key: projectKey }, summary, description: { type: "doc", version: 1, content: [ { type: "paragraph", content: [ { type: "text", text: description } ] } ] }, issuetype: { name: issueType } } }); return { content: [{ type: "text", text: `Created JIRA ticket: ${response.data.key}` }] }; } case "add_comment_to_jira_ticket": { const ticketKey = String(request.params.arguments?.ticket_key); const comment = String(request.params.arguments?.comment); if (!ticketKey || !comment) { throw new McpError(ErrorCode.InvalidParams, "Ticket key and comment are required"); } const response = await atlassianApi.post(`/rest/api/3/issue/${ticketKey}/comment`, { body: { type: "doc", version: 1, content: [ { type: "paragraph", content: [ { type: "text", text: comment } ] } ] } }); return { content: [{ type: "text", text: `Added comment to ${ticketKey}` }] }; } case "get_confluence_page": { const pageId = String(request.params.arguments?.page_id); if (!pageId) { throw new McpError(ErrorCode.InvalidParams, "Page ID is required"); } const response = await atlassianApi.get(`/wiki/rest/api/content/${pageId}`, { params: { expand: "body.storage,version,space", }, }); return { content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }] }; } case "search_confluence": { const query = String(request.params.arguments?.query); const limit = Number(request.params.arguments?.limit || 10); if (!query) { throw new McpError(ErrorCode.InvalidParams, "Search query is required"); } const response = await atlassianApi.get("/wiki/rest/api/content/search", { params: { cql: `text ~ "${query}"`, limit, expand: "space", }, }); return { content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }] }; } default: throw new McpError(ErrorCode.MethodNotFound, "Unknown tool"); } } catch (error) { console.error("Error executing tool:", error); if (axios.isAxiosError(error)) { return { content: [{ type: "text", text: `Atlassian API error: ${error.response?.data?.message || error.message}` }], isError: true }; } throw error; } }); /** * Start the server using stdio transport. */ async function main() { const transport = new StdioServerTransport(); await server.connect(transport); console.error(`Atlassian MCP server running on stdio (connected to ${config.atlassian.baseUrl})`); } main().catch((error) => { console.error("Server error:", error); process.exit(1); });

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/kompallik/ATLASSIAN-MCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server