import { connectServer, createMcpServer, formatToolResponse, initializeRuntimeConfig, resolveAttachmentGateway } from '@atlassian-dc-mcp/common'; import { ConfluenceService, ConfluenceContent, confluenceToolSchemas } from './confluence-service.js'; import { shapeConfluenceMutationAck } from './confluence-response-mapper.js'; import { CONFLUENCE_PRODUCT, getConfluenceRuntimeConfig, getDefaultPageSize } from './config.js'; import { createRequire } from 'node:module'; const require = createRequire(import.meta.url); const { version } = require('../package.json'); initializeRuntimeConfig(); // Validate required environment variables const missingEnvVars = ConfluenceService.validateConfig(); if (missingEnvVars.length > 0) { throw new Error(`Missing required environment variables: ${missingEnvVars.join(', ')}`); } // Initialize Confluence service const confluenceConfig = getConfluenceRuntimeConfig(); const confluenceService = new ConfluenceService( confluenceConfig.host, () => getConfluenceRuntimeConfig().token, confluenceConfig.apiBasePath, getDefaultPageSize ); // Define Confluence instance type const confluenceInstanceType = "Confluence Data Center edition instance"; // Initialize MCP server const server = createMcpServer({ name: "atlassian-confluence-mcp", version }); // Add Confluence content tools server.tool( "confluence_getContent", `Get Confluence content by ID from the ${confluenceInstanceType}`, confluenceToolSchemas.getContent, async ({ contentId, expand, bodyMode, maxBodyChars, bodyStart }) => { const result = await confluenceService.getContent(contentId, expand, bodyMode, maxBodyChars, bodyStart); return formatToolResponse(result); } ); server.tool( "confluence_searchContent", `Search for content in ${confluenceInstanceType} using CQL`, confluenceToolSchemas.searchContent, async ({ cql, limit, start, expand, excerpt }) => { const result = await confluenceService.searchContent(cql, limit, start, expand, excerpt); return formatToolResponse(result); } ); server.tool( "confluence_createContent", `Create new content in ${confluenceInstanceType}`, confluenceToolSchemas.createContent, async ({ title, spaceKey, type, content, parentId, output }) => { const contentObj: ConfluenceContent = { type: type || 'page', title, space: { key: spaceKey }, body: { storage: { value: content, representation: 'storage' } } }; // Add parent page as ancestor if specified if (parentId) { contentObj.ancestors = [{ id: parentId }]; } const result = await confluenceService.createContent(contentObj); if (result.success && result.data && output !== 'full') { return formatToolResponse({ ...result, data: shapeConfluenceMutationAck(result.data), }); } return formatToolResponse(result); } ); server.tool( "confluence_updateContent", `Update existing content in ${confluenceInstanceType}`, confluenceToolSchemas.updateContent, async ({ contentId, title, content, version, versionComment, output }) => { // First get the current content to build upon const currentContent = await confluenceService.getContentRaw(contentId); if (!currentContent.success || !currentContent.data) { return formatToolResponse({ success: false, error: `Failed to retrieve content with ID ${contentId}: ${currentContent.error || 'Unknown error'}` }); } // Type assertion to help TypeScript understand the structure const contentData = currentContent.data as { type: string; title: string; space: { key: string }; }; const updateObj: ConfluenceContent = { id: contentId, type: contentData.type, title: title || contentData.title, space: contentData.space, version: { number: version, message: versionComment } }; // Only update body if content is provided if (content) { updateObj.body = { storage: { value: content, representation: 'storage' } }; } const result = await confluenceService.updateContent(contentId, updateObj); if (result.success && result.data && output !== 'full') { return formatToolResponse({ ...result, data: shapeConfluenceMutationAck(result.data), }); } return formatToolResponse(result); } ); const attachmentGateway = resolveAttachmentGateway(CONFLUENCE_PRODUCT); // Filesystem-reading upload is only registered when the operator explicitly enables it. if (attachmentGateway.upload.enabled) { server.tool( "confluence_uploadAttachment", `Upload a local file as an attachment to a Confluence content (page) in the ${confluenceInstanceType}. The file must live under the server-configured upload directory; the path is given relative to that directory.`, confluenceToolSchemas.uploadAttachment, async ({ contentId, sourcePath, filename, comment, minorEdit, hidden, allowDuplicated, versionIfExists }) => { const result = await confluenceService.uploadAttachment( contentId, sourcePath, attachmentGateway.upload, filename, comment, minorEdit, hidden, allowDuplicated, versionIfExists, ); return formatToolResponse(result); } ); } // Download always returns content inline (no filesystem access). Saving to disk is // only offered when the operator enables it, and is confined to the configured directory. const downloadSaveEnabled = attachmentGateway.download.enabled; server.tool( "confluence_downloadAttachment", `Download one or more attachments from a Confluence content (page) in the ${confluenceInstanceType}. Returns the file content inline (base64 or text). Useful for inspecting a file or moving it elsewhere (e.g. re-uploading to a Jira issue).${downloadSaveEnabled ? ' Can also save into the server-configured download directory; existing files are never overwritten.' : ' Saving to local disk is disabled on this server.'}`, { ...confluenceToolSchemas.downloadAttachment, ...(downloadSaveEnabled ? confluenceToolSchemas.downloadAttachmentSaveFields : {}) }, async ({ contentId, filename, returnContent, maxInlineBytes, save, saveName }: { contentId: string; filename?: string; returnContent?: 'none' | 'base64' | 'text'; maxInlineBytes?: number; save?: boolean; saveName?: string; }) => { const result = await confluenceService.downloadAttachmentFromContent({ contentId, filename, save, saveName, returnContent, maxInlineBytes, downloadSide: attachmentGateway.download, }); return formatToolResponse(result); } ); server.tool('confluence_searchSpace', `Search for spaces in ${confluenceInstanceType}`, confluenceToolSchemas.searchSpaces, async ({ searchText, limit, start, expand, excerpt }) => { const result = await confluenceService.searchSpaces(searchText, limit, start, expand, excerpt); return formatToolResponse(result); }); await connectServer(server);