#!/usr/bin/env node
/**
* Confluence MCP Server
*
* A Model Context Protocol server for Atlassian Confluence Cloud.
* Provides tools to create, read, update, delete, search, and list pages.
*/
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import { marked } from 'marked';
// Configuration from environment variables
const CONFLUENCE_URL = process.env.CONFLUENCE_URL;
const CONFLUENCE_EMAIL = process.env.CONFLUENCE_EMAIL;
const CONFLUENCE_API_TOKEN = process.env.CONFLUENCE_API_TOKEN;
const DEFAULT_SPACE_KEY = process.env.CONFLUENCE_SPACE_KEY || null;
// Validate required environment variables
if (!CONFLUENCE_URL || !CONFLUENCE_EMAIL || !CONFLUENCE_API_TOKEN) {
console.error('Missing required environment variables:');
console.error('- CONFLUENCE_URL: Your Confluence instance URL (e.g., https://yourname.atlassian.net)');
console.error('- CONFLUENCE_EMAIL: Your Atlassian account email');
console.error('- CONFLUENCE_API_TOKEN: Your Confluence API token');
console.error('- CONFLUENCE_SPACE_KEY: (Optional) Default space key');
process.exit(1);
}
// Base64 encode credentials for Basic Auth
const authHeader = 'Basic ' + Buffer.from(`${CONFLUENCE_EMAIL}:${CONFLUENCE_API_TOKEN}`).toString('base64');
// API base URL
const API_BASE = `${CONFLUENCE_URL}/wiki/api/v2`;
/**
* Convert Markdown to Confluence Storage Format (XHTML)
*/
function markdownToConfluence(markdown) {
const html = marked.parse(markdown, { async: false });
return html;
}
/**
* Make authenticated request to Confluence API
*/
async function confluenceRequest(endpoint, options = {}) {
const url = `${API_BASE}${endpoint}`;
const response = await fetch(url, {
...options,
headers: {
'Authorization': authHeader,
'Content-Type': 'application/json',
'Accept': 'application/json',
...options.headers,
},
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Confluence API error (${response.status}): ${errorText}`);
}
// Handle DELETE which returns no content
if (response.status === 204) {
return { success: true };
}
return response.json();
}
/**
* Get space ID from space key
*/
async function getSpaceId(spaceKey) {
const result = await confluenceRequest(`/spaces?keys=${spaceKey}`);
if (result.results && result.results.length > 0) {
return result.results[0].id;
}
throw new Error(`Space with key "${spaceKey}" not found`);
}
/**
* Create a new page in Confluence
*/
async function createPage(spaceKey, title, content, parentId = null) {
const spaceId = await getSpaceId(spaceKey);
const body = {
spaceId: spaceId,
status: 'current',
title: title,
body: {
representation: 'storage',
value: markdownToConfluence(content),
},
};
if (parentId) {
body.parentId = parentId;
}
return confluenceRequest('/pages', {
method: 'POST',
body: JSON.stringify(body),
});
}
/**
* Update an existing page
*/
async function updatePage(pageId, title, content) {
// First get the current page to get the version number
const currentPage = await confluenceRequest(`/pages/${pageId}`);
const currentVersion = currentPage.version.number;
const body = {
id: pageId,
status: 'current',
title: title,
body: {
representation: 'storage',
value: markdownToConfluence(content),
},
version: {
number: currentVersion + 1,
},
};
return confluenceRequest(`/pages/${pageId}`, {
method: 'PUT',
body: JSON.stringify(body),
});
}
/**
* Get page content by ID
*/
async function getPage(pageId) {
return confluenceRequest(`/pages/${pageId}?body-format=storage`);
}
/**
* Delete a page by ID
*/
async function deletePage(pageId) {
return confluenceRequest(`/pages/${pageId}`, {
method: 'DELETE',
});
}
/**
* Search for pages in a space
*/
async function searchPages(spaceKey, query) {
const spaceId = await getSpaceId(spaceKey);
return confluenceRequest(`/spaces/${spaceId}/pages?title=${encodeURIComponent(query)}`);
}
/**
* List all pages in a space
*/
async function listPages(spaceKey) {
const spaceId = await getSpaceId(spaceKey);
return confluenceRequest(`/spaces/${spaceId}/pages?limit=50`);
}
// Create MCP Server
const server = new Server(
{
name: 'confluence-mcp-server',
version: '1.0.0',
},
{
capabilities: {
tools: {},
},
}
);
// Define available tools
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: 'create_confluence_page',
description: 'Create a new page in Confluence. Supports Markdown content which will be converted to Confluence format.',
inputSchema: {
type: 'object',
properties: {
spaceKey: {
type: 'string',
description: 'The space key where the page will be created (e.g., "DOCS", "TEAM")',
},
title: {
type: 'string',
description: 'The title of the page',
},
content: {
type: 'string',
description: 'The page content in Markdown format',
},
parentId: {
type: 'string',
description: 'Optional parent page ID to create this page under',
},
},
required: ['spaceKey', 'title', 'content'],
},
},
{
name: 'update_confluence_page',
description: 'Update an existing page in Confluence',
inputSchema: {
type: 'object',
properties: {
pageId: {
type: 'string',
description: 'The ID of the page to update',
},
title: {
type: 'string',
description: 'The new title of the page',
},
content: {
type: 'string',
description: 'The new page content in Markdown format',
},
},
required: ['pageId', 'title', 'content'],
},
},
{
name: 'get_confluence_page',
description: 'Get the content of a Confluence page by ID',
inputSchema: {
type: 'object',
properties: {
pageId: {
type: 'string',
description: 'The ID of the page to retrieve',
},
},
required: ['pageId'],
},
},
{
name: 'delete_confluence_page',
description: 'Delete a Confluence page by ID. Use with caution!',
inputSchema: {
type: 'object',
properties: {
pageId: {
type: 'string',
description: 'The ID of the page to delete',
},
},
required: ['pageId'],
},
},
{
name: 'search_confluence_pages',
description: 'Search for pages in a Confluence space by title',
inputSchema: {
type: 'object',
properties: {
spaceKey: {
type: 'string',
description: 'The space key to search in',
},
query: {
type: 'string',
description: 'The search query (matches page titles)',
},
},
required: ['spaceKey', 'query'],
},
},
{
name: 'list_confluence_pages',
description: 'List all pages in a Confluence space',
inputSchema: {
type: 'object',
properties: {
spaceKey: {
type: 'string',
description: 'The space key to list pages from',
},
},
required: ['spaceKey'],
},
},
],
};
});
// Handle tool calls
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
// Use default space key if not provided and available
const spaceKey = args.spaceKey || DEFAULT_SPACE_KEY;
try {
switch (name) {
case 'create_confluence_page': {
if (!spaceKey) throw new Error('spaceKey is required');
const result = await createPage(spaceKey, args.title, args.content, args.parentId);
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: true,
pageId: result.id,
title: result.title,
url: `${CONFLUENCE_URL}/wiki${result._links?.webui || ''}`,
}, null, 2),
},
],
};
}
case 'update_confluence_page': {
const result = await updatePage(args.pageId, args.title, args.content);
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: true,
pageId: result.id,
title: result.title,
version: result.version.number,
}, null, 2),
},
],
};
}
case 'get_confluence_page': {
const result = await getPage(args.pageId);
return {
content: [
{
type: 'text',
text: JSON.stringify({
id: result.id,
title: result.title,
content: result.body?.storage?.value || '',
version: result.version?.number,
}, null, 2),
},
],
};
}
case 'delete_confluence_page': {
await deletePage(args.pageId);
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: true,
message: `Page ${args.pageId} deleted successfully`,
}, null, 2),
},
],
};
}
case 'search_confluence_pages': {
if (!spaceKey) throw new Error('spaceKey is required');
const result = await searchPages(spaceKey, args.query);
return {
content: [
{
type: 'text',
text: JSON.stringify({
results: result.results?.map(p => ({
id: p.id,
title: p.title,
})) || [],
}, null, 2),
},
],
};
}
case 'list_confluence_pages': {
if (!spaceKey) throw new Error('spaceKey is required');
const result = await listPages(spaceKey);
return {
content: [
{
type: 'text',
text: JSON.stringify({
pages: result.results?.map(p => ({
id: p.id,
title: p.title,
})) || [],
}, null, 2),
},
],
};
}
default:
throw new Error(`Unknown tool: ${name}`);
}
} catch (error) {
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: false,
error: error.message,
}, null, 2),
},
],
isError: true,
};
}
});
// Start the server
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('Confluence MCP Server running on stdio');
}
main().catch(console.error);