feat: Initial confluence-mcp server for realtime collaboration

Provides 8 MCP tools for Confluence Cloud:
- confluence_list_spaces, confluence_create_space
- confluence_search, confluence_get_page
- confluence_create_page, confluence_update_page
- confluence_get_comments, confluence_add_comment

Uses Confluence REST API v2 with basic auth.
Registered in Claude Code and mcp-proxy.

Refs: CF-935

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Christian Gick
2026-02-10 18:09:30 +02:00
commit 2768650b42
3824 changed files with 859428 additions and 0 deletions

162
dist/client.js vendored Normal file
View File

@@ -0,0 +1,162 @@
/**
* Confluence Cloud REST API v2 client.
* Uses basic auth with Atlassian API token.
*/
const BASE_URL = process.env.CONFLUENCE_URL || 'https://agiliton.atlassian.net/wiki';
const API_V2 = `${BASE_URL}/api/v2`;
const API_V1 = `${BASE_URL}/rest/api`;
function getAuth() {
const user = process.env.CONFLUENCE_USER || process.env.JIRA_USER;
const token = process.env.CONFLUENCE_API_TOKEN || process.env.JIRA_API_TOKEN;
if (!user || !token) {
throw new Error('CONFLUENCE_USER and CONFLUENCE_API_TOKEN (or JIRA_USER/JIRA_API_TOKEN) must be set');
}
return Buffer.from(`${user}:${token}`).toString('base64');
}
async function request(url, options = {}) {
const headers = {
'Authorization': `Basic ${getAuth()}`,
'Accept': 'application/json',
...(options.headers || {}),
};
if (options.body && typeof options.body === 'string') {
headers['Content-Type'] = 'application/json';
}
const response = await fetch(url, { ...options, headers });
if (!response.ok) {
const text = await response.text().catch(() => '');
throw new Error(`Confluence API ${response.status}: ${text.slice(0, 500)}`);
}
if (response.status === 204)
return null;
return response.json();
}
// --- Spaces ---
export async function listSpaces(limit = 25) {
const data = await request(`${API_V2}/spaces?limit=${limit}&sort=name`);
return data.results;
}
export async function getSpace(spaceIdOrKey) {
return request(`${API_V2}/spaces/${spaceIdOrKey}`);
}
export async function createSpace(key, name, description) {
// v2 doesn't have space creation — use v1
const body = {
key,
name,
type: 'global',
};
if (description) {
body.description = {
plain: { value: description, representation: 'plain' },
};
}
return request(`${API_V1}/space`, {
method: 'POST',
body: JSON.stringify(body),
});
}
// --- Pages ---
export async function searchPages(query, spaceId, limit = 10) {
let cql = `type=page AND text ~ "${query.replace(/"/g, '\\"')}"`;
if (spaceId)
cql += ` AND space.id=${spaceId}`;
cql += ' ORDER BY lastmodified DESC';
const params = new URLSearchParams({
cql,
limit: String(limit),
});
// CQL search is v1 only
const data = await request(`${API_V1}/content/search?${params}`);
return data.results.map((r) => ({
id: r.id,
status: r.status,
title: r.title,
spaceId: r.space?.id || '',
authorId: r.history?.createdBy?.accountId || '',
createdAt: r.history?.createdDate || '',
version: {
number: r.version?.number || 1,
createdAt: r.version?.when || '',
},
_links: {
webui: r._links?.webui ? `${BASE_URL}${r._links.webui}` : undefined,
},
}));
}
export async function getPage(pageId, includeBody = true) {
const params = includeBody ? '?body-format=storage' : '';
return request(`${API_V2}/pages/${pageId}${params}`);
}
export async function getPageByTitle(spaceId, title) {
const params = new URLSearchParams({
title,
'space-id': spaceId,
'body-format': 'storage',
limit: '1',
});
const data = await request(`${API_V2}/pages?${params}`);
return data.results?.[0] || null;
}
export async function createPage(spaceId, title, body, parentId) {
const payload = {
spaceId,
status: 'current',
title,
body: {
representation: 'storage',
value: body,
},
};
if (parentId)
payload.parentId = parentId;
return request(`${API_V2}/pages`, {
method: 'POST',
body: JSON.stringify(payload),
});
}
export async function updatePage(pageId, title, body, versionNumber, versionMessage) {
const payload = {
id: pageId,
status: 'current',
title,
body: {
representation: 'storage',
value: body,
},
version: {
number: versionNumber,
message: versionMessage || 'Updated via Claude',
},
};
return request(`${API_V2}/pages/${pageId}`, {
method: 'PUT',
body: JSON.stringify(payload),
});
}
// --- Comments ---
export async function getPageComments(pageId, limit = 25) {
const data = await request(`${API_V2}/pages/${pageId}/footer-comments?body-format=storage&limit=${limit}`);
return data.results;
}
export async function addPageComment(pageId, body) {
return request(`${API_V2}/footer-comments`, {
method: 'POST',
body: JSON.stringify({
pageId,
body: {
representation: 'storage',
value: body,
},
}),
});
}
// --- Utility ---
export function getWebUrl(page) {
if (page._links?.webui) {
return page._links.webui.startsWith('http')
? page._links.webui
: `${BASE_URL}${page._links.webui}`;
}
return `${BASE_URL}/pages/${page.id}`;
}