feat(API-11): Route API calls through AgilitonAPI gateway
Add gateway-first pattern: when AGILITON_API_KEY is set, route all external API calls through the gateway with X-API-Key auth. Falls back to direct API access when gateway is unavailable. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
70
dist/client.d.ts
vendored
Normal file
70
dist/client.d.ts
vendored
Normal file
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* Confluence Cloud REST API v2 client — routes through AgilitonAPI gateway.
|
||||
* Falls back to direct Confluence access if AGILITON_API_KEY is not set.
|
||||
*
|
||||
* Gateway: AGILITON_API_KEY + AGILITON_API_URL
|
||||
* Direct: CONFLUENCE_USER/JIRA_USER + CONFLUENCE_API_TOKEN/JIRA_API_TOKEN
|
||||
*/
|
||||
export interface Space {
|
||||
id: string;
|
||||
key: string;
|
||||
name: string;
|
||||
type: string;
|
||||
status: string;
|
||||
description?: string;
|
||||
}
|
||||
export interface Page {
|
||||
id: string;
|
||||
status: string;
|
||||
title: string;
|
||||
spaceId: string;
|
||||
parentId?: string;
|
||||
authorId: string;
|
||||
createdAt: string;
|
||||
version: {
|
||||
number: number;
|
||||
message?: string;
|
||||
createdAt: string;
|
||||
authorId?: string;
|
||||
};
|
||||
body?: {
|
||||
storage?: {
|
||||
representation: string;
|
||||
value: string;
|
||||
};
|
||||
atlas_doc_format?: {
|
||||
representation: string;
|
||||
value: string;
|
||||
};
|
||||
};
|
||||
_links?: {
|
||||
webui?: string;
|
||||
editui?: string;
|
||||
};
|
||||
}
|
||||
export interface Comment {
|
||||
id: string;
|
||||
status: string;
|
||||
title: string;
|
||||
body?: {
|
||||
storage?: {
|
||||
representation: string;
|
||||
value: string;
|
||||
};
|
||||
};
|
||||
version: {
|
||||
number: number;
|
||||
createdAt: string;
|
||||
};
|
||||
}
|
||||
export declare function listSpaces(limit?: number): Promise<Space[]>;
|
||||
export declare function getSpace(spaceIdOrKey: string): Promise<Space>;
|
||||
export declare function createSpace(key: string, name: string, description?: string): Promise<Space>;
|
||||
export declare function searchPages(query: string, spaceId?: string, limit?: number): Promise<Page[]>;
|
||||
export declare function getPage(pageId: string, includeBody?: boolean): Promise<Page>;
|
||||
export declare function getPageByTitle(spaceId: string, title: string): Promise<Page | null>;
|
||||
export declare function createPage(spaceId: string, title: string, body: string, parentId?: string): Promise<Page>;
|
||||
export declare function updatePage(pageId: string, title: string, body: string, versionNumber: number, versionMessage?: string): Promise<Page>;
|
||||
export declare function getPageComments(pageId: string, limit?: number): Promise<Comment[]>;
|
||||
export declare function addPageComment(pageId: string, body: string): Promise<Comment>;
|
||||
export declare function getWebUrl(page: Page): string;
|
||||
209
dist/client.js
vendored
Normal file
209
dist/client.js
vendored
Normal file
@@ -0,0 +1,209 @@
|
||||
/**
|
||||
* Confluence Cloud REST API v2 client — routes through AgilitonAPI gateway.
|
||||
* Falls back to direct Confluence access if AGILITON_API_KEY is not set.
|
||||
*
|
||||
* Gateway: AGILITON_API_KEY + AGILITON_API_URL
|
||||
* Direct: CONFLUENCE_USER/JIRA_USER + CONFLUENCE_API_TOKEN/JIRA_API_TOKEN
|
||||
*/
|
||||
// Gateway config
|
||||
const GATEWAY_URL = (process.env.AGILITON_API_URL || 'https://api.agiliton.cloud').replace(/\/$/, '');
|
||||
const GATEWAY_KEY = process.env.AGILITON_API_KEY || '';
|
||||
// Direct config (fallback)
|
||||
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 useGateway() {
|
||||
return !!GATEWAY_KEY;
|
||||
}
|
||||
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 = {}) {
|
||||
let finalUrl = url;
|
||||
const headers = {
|
||||
'Accept': 'application/json',
|
||||
...(options.headers || {}),
|
||||
};
|
||||
if (useGateway()) {
|
||||
// Route through gateway — translate Confluence URLs to gateway paths
|
||||
finalUrl = toGatewayUrl(url);
|
||||
headers['X-API-Key'] = GATEWAY_KEY;
|
||||
}
|
||||
else {
|
||||
headers['Authorization'] = `Basic ${getAuth()}`;
|
||||
}
|
||||
if (options.body && typeof options.body === 'string') {
|
||||
headers['Content-Type'] = 'application/json';
|
||||
}
|
||||
const response = await fetch(finalUrl, { ...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();
|
||||
}
|
||||
/**
|
||||
* Translate a direct Confluence URL to a gateway URL.
|
||||
* /wiki/api/v2/spaces → /confluence/spaces
|
||||
* /wiki/rest/api/content/search → /confluence/search
|
||||
* /wiki/rest/api/space → /confluence/space
|
||||
*/
|
||||
function toGatewayUrl(url) {
|
||||
// Extract path from full URL
|
||||
let path = url;
|
||||
if (path.startsWith('http')) {
|
||||
const u = new URL(path);
|
||||
path = u.pathname + u.search;
|
||||
}
|
||||
// v2 API: /wiki/api/v2/... → /confluence/...
|
||||
const v2Match = path.match(/\/api\/v2\/(.*)$/);
|
||||
if (v2Match)
|
||||
return `${GATEWAY_URL}/confluence/${v2Match[1]}`;
|
||||
// v1 API: /wiki/rest/api/content/search → /confluence/search
|
||||
const searchMatch = path.match(/\/rest\/api\/content\/search(.*)$/);
|
||||
if (searchMatch)
|
||||
return `${GATEWAY_URL}/confluence/search${searchMatch[1]}`;
|
||||
// v1 API: /wiki/rest/api/space → /confluence/space
|
||||
const spaceMatch = path.match(/\/rest\/api\/space(.*)$/);
|
||||
if (spaceMatch)
|
||||
return `${GATEWAY_URL}/confluence/space${spaceMatch[1]}`;
|
||||
// Fallback: strip /wiki prefix and use as-is
|
||||
const stripped = path.replace(/^\/wiki/, '');
|
||||
return `${GATEWAY_URL}/confluence${stripped}`;
|
||||
}
|
||||
// --- 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}`;
|
||||
}
|
||||
10
dist/index.d.ts
vendored
Normal file
10
dist/index.d.ts
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Confluence MCP Server
|
||||
*
|
||||
* Provides Confluence Cloud page CRUD and collaboration via Model Context Protocol.
|
||||
* Tools: confluence_list_spaces, confluence_create_space, confluence_search,
|
||||
* confluence_get_page, confluence_create_page, confluence_update_page,
|
||||
* confluence_get_comments, confluence_add_comment
|
||||
*/
|
||||
export {};
|
||||
108
dist/index.js
vendored
Normal file
108
dist/index.js
vendored
Normal file
@@ -0,0 +1,108 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Confluence MCP Server
|
||||
*
|
||||
* Provides Confluence Cloud page CRUD and collaboration via Model Context Protocol.
|
||||
* Tools: confluence_list_spaces, confluence_create_space, confluence_search,
|
||||
* confluence_get_page, confluence_create_page, confluence_update_page,
|
||||
* confluence_get_comments, confluence_add_comment
|
||||
*/
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname, join } from 'path';
|
||||
import dotenv from 'dotenv';
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
const envPath = join(__dirname, '..', '.env');
|
||||
dotenv.config({ path: envPath, override: true });
|
||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||
import { ListToolsRequestSchema, CallToolRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
||||
import { toolDefinitions, handleListSpaces, handleCreateSpace, handleSearch, handleGetPage, handleCreatePage, handleUpdatePage, handleGetComments, handleAddComment, } from './tools.js';
|
||||
const server = new Server({ name: 'confluence-mcp', version: '1.0.0' }, { capabilities: { tools: {} } });
|
||||
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
||||
tools: toolDefinitions,
|
||||
}));
|
||||
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
const { name, arguments: args } = request.params;
|
||||
const a = args;
|
||||
let result;
|
||||
try {
|
||||
switch (name) {
|
||||
case 'confluence_list_spaces':
|
||||
result = await handleListSpaces({ limit: a.limit });
|
||||
break;
|
||||
case 'confluence_create_space':
|
||||
result = await handleCreateSpace({
|
||||
key: a.key,
|
||||
name: a.name,
|
||||
description: a.description,
|
||||
});
|
||||
break;
|
||||
case 'confluence_search':
|
||||
result = await handleSearch({
|
||||
query: a.query,
|
||||
space_id: a.space_id,
|
||||
limit: a.limit,
|
||||
});
|
||||
break;
|
||||
case 'confluence_get_page':
|
||||
result = await handleGetPage({ page_id: String(a.page_id) });
|
||||
break;
|
||||
case 'confluence_create_page':
|
||||
result = await handleCreatePage({
|
||||
space_id: a.space_id,
|
||||
title: a.title,
|
||||
body: a.body,
|
||||
parent_id: a.parent_id,
|
||||
});
|
||||
break;
|
||||
case 'confluence_update_page':
|
||||
result = await handleUpdatePage({
|
||||
page_id: String(a.page_id),
|
||||
title: a.title,
|
||||
body: a.body,
|
||||
version_number: a.version_number,
|
||||
version_message: a.version_message,
|
||||
});
|
||||
break;
|
||||
case 'confluence_get_comments':
|
||||
result = await handleGetComments({
|
||||
page_id: String(a.page_id),
|
||||
limit: a.limit,
|
||||
});
|
||||
break;
|
||||
case 'confluence_add_comment':
|
||||
result = await handleAddComment({
|
||||
page_id: String(a.page_id),
|
||||
body: a.body,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
result = `Unknown tool: ${name}`;
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
result = `Error: ${message}`;
|
||||
}
|
||||
return {
|
||||
content: [{ type: 'text', text: result }],
|
||||
};
|
||||
});
|
||||
async function main() {
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
console.error('confluence-mcp: Server started');
|
||||
}
|
||||
main().catch((error) => {
|
||||
console.error('confluence-mcp: Fatal error:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
process.on('SIGINT', async () => {
|
||||
await server.close();
|
||||
process.exit(0);
|
||||
});
|
||||
process.on('SIGTERM', async () => {
|
||||
await server.close();
|
||||
process.exit(0);
|
||||
});
|
||||
272
dist/tools.d.ts
vendored
Normal file
272
dist/tools.d.ts
vendored
Normal file
@@ -0,0 +1,272 @@
|
||||
/**
|
||||
* MCP tool definitions and handlers for Confluence Cloud.
|
||||
*/
|
||||
export declare const toolDefinitions: ({
|
||||
name: string;
|
||||
description: string;
|
||||
inputSchema: {
|
||||
type: "object";
|
||||
properties: {
|
||||
limit: {
|
||||
type: string;
|
||||
description: string;
|
||||
};
|
||||
key?: undefined;
|
||||
name?: undefined;
|
||||
description?: undefined;
|
||||
query?: undefined;
|
||||
space_id?: undefined;
|
||||
page_id?: undefined;
|
||||
title?: undefined;
|
||||
body?: undefined;
|
||||
parent_id?: undefined;
|
||||
version_number?: undefined;
|
||||
version_message?: undefined;
|
||||
};
|
||||
required?: undefined;
|
||||
};
|
||||
} | {
|
||||
name: string;
|
||||
description: string;
|
||||
inputSchema: {
|
||||
type: "object";
|
||||
properties: {
|
||||
key: {
|
||||
type: string;
|
||||
description: string;
|
||||
};
|
||||
name: {
|
||||
type: string;
|
||||
description: string;
|
||||
};
|
||||
description: {
|
||||
type: string;
|
||||
description: string;
|
||||
};
|
||||
limit?: undefined;
|
||||
query?: undefined;
|
||||
space_id?: undefined;
|
||||
page_id?: undefined;
|
||||
title?: undefined;
|
||||
body?: undefined;
|
||||
parent_id?: undefined;
|
||||
version_number?: undefined;
|
||||
version_message?: undefined;
|
||||
};
|
||||
required: string[];
|
||||
};
|
||||
} | {
|
||||
name: string;
|
||||
description: string;
|
||||
inputSchema: {
|
||||
type: "object";
|
||||
properties: {
|
||||
query: {
|
||||
type: string;
|
||||
description: string;
|
||||
};
|
||||
space_id: {
|
||||
type: string;
|
||||
description: string;
|
||||
};
|
||||
limit: {
|
||||
type: string;
|
||||
description: string;
|
||||
};
|
||||
key?: undefined;
|
||||
name?: undefined;
|
||||
description?: undefined;
|
||||
page_id?: undefined;
|
||||
title?: undefined;
|
||||
body?: undefined;
|
||||
parent_id?: undefined;
|
||||
version_number?: undefined;
|
||||
version_message?: undefined;
|
||||
};
|
||||
required: string[];
|
||||
};
|
||||
} | {
|
||||
name: string;
|
||||
description: string;
|
||||
inputSchema: {
|
||||
type: "object";
|
||||
properties: {
|
||||
page_id: {
|
||||
type: string;
|
||||
description: string;
|
||||
};
|
||||
limit?: undefined;
|
||||
key?: undefined;
|
||||
name?: undefined;
|
||||
description?: undefined;
|
||||
query?: undefined;
|
||||
space_id?: undefined;
|
||||
title?: undefined;
|
||||
body?: undefined;
|
||||
parent_id?: undefined;
|
||||
version_number?: undefined;
|
||||
version_message?: undefined;
|
||||
};
|
||||
required: string[];
|
||||
};
|
||||
} | {
|
||||
name: string;
|
||||
description: string;
|
||||
inputSchema: {
|
||||
type: "object";
|
||||
properties: {
|
||||
space_id: {
|
||||
type: string;
|
||||
description: string;
|
||||
};
|
||||
title: {
|
||||
type: string;
|
||||
description: string;
|
||||
};
|
||||
body: {
|
||||
type: string;
|
||||
description: string;
|
||||
};
|
||||
parent_id: {
|
||||
type: string;
|
||||
description: string;
|
||||
};
|
||||
limit?: undefined;
|
||||
key?: undefined;
|
||||
name?: undefined;
|
||||
description?: undefined;
|
||||
query?: undefined;
|
||||
page_id?: undefined;
|
||||
version_number?: undefined;
|
||||
version_message?: undefined;
|
||||
};
|
||||
required: string[];
|
||||
};
|
||||
} | {
|
||||
name: string;
|
||||
description: string;
|
||||
inputSchema: {
|
||||
type: "object";
|
||||
properties: {
|
||||
page_id: {
|
||||
type: string;
|
||||
description: string;
|
||||
};
|
||||
title: {
|
||||
type: string;
|
||||
description: string;
|
||||
};
|
||||
body: {
|
||||
type: string;
|
||||
description: string;
|
||||
};
|
||||
version_number: {
|
||||
type: string;
|
||||
description: string;
|
||||
};
|
||||
version_message: {
|
||||
type: string;
|
||||
description: string;
|
||||
};
|
||||
limit?: undefined;
|
||||
key?: undefined;
|
||||
name?: undefined;
|
||||
description?: undefined;
|
||||
query?: undefined;
|
||||
space_id?: undefined;
|
||||
parent_id?: undefined;
|
||||
};
|
||||
required: string[];
|
||||
};
|
||||
} | {
|
||||
name: string;
|
||||
description: string;
|
||||
inputSchema: {
|
||||
type: "object";
|
||||
properties: {
|
||||
page_id: {
|
||||
type: string;
|
||||
description: string;
|
||||
};
|
||||
limit: {
|
||||
type: string;
|
||||
description: string;
|
||||
};
|
||||
key?: undefined;
|
||||
name?: undefined;
|
||||
description?: undefined;
|
||||
query?: undefined;
|
||||
space_id?: undefined;
|
||||
title?: undefined;
|
||||
body?: undefined;
|
||||
parent_id?: undefined;
|
||||
version_number?: undefined;
|
||||
version_message?: undefined;
|
||||
};
|
||||
required: string[];
|
||||
};
|
||||
} | {
|
||||
name: string;
|
||||
description: string;
|
||||
inputSchema: {
|
||||
type: "object";
|
||||
properties: {
|
||||
page_id: {
|
||||
type: string;
|
||||
description: string;
|
||||
};
|
||||
body: {
|
||||
type: string;
|
||||
description: string;
|
||||
};
|
||||
limit?: undefined;
|
||||
key?: undefined;
|
||||
name?: undefined;
|
||||
description?: undefined;
|
||||
query?: undefined;
|
||||
space_id?: undefined;
|
||||
title?: undefined;
|
||||
parent_id?: undefined;
|
||||
version_number?: undefined;
|
||||
version_message?: undefined;
|
||||
};
|
||||
required: string[];
|
||||
};
|
||||
})[];
|
||||
export declare function handleListSpaces(args: {
|
||||
limit?: number;
|
||||
}): Promise<string>;
|
||||
export declare function handleCreateSpace(args: {
|
||||
key: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
}): Promise<string>;
|
||||
export declare function handleSearch(args: {
|
||||
query: string;
|
||||
space_id?: string;
|
||||
limit?: number;
|
||||
}): Promise<string>;
|
||||
export declare function handleGetPage(args: {
|
||||
page_id: string;
|
||||
}): Promise<string>;
|
||||
export declare function handleCreatePage(args: {
|
||||
space_id: string;
|
||||
title: string;
|
||||
body: string;
|
||||
parent_id?: string;
|
||||
}): Promise<string>;
|
||||
export declare function handleUpdatePage(args: {
|
||||
page_id: string;
|
||||
title: string;
|
||||
body: string;
|
||||
version_number: number;
|
||||
version_message?: string;
|
||||
}): Promise<string>;
|
||||
export declare function handleGetComments(args: {
|
||||
page_id: string;
|
||||
limit?: number;
|
||||
}): Promise<string>;
|
||||
export declare function handleAddComment(args: {
|
||||
page_id: string;
|
||||
body: string;
|
||||
}): Promise<string>;
|
||||
231
dist/tools.js
vendored
Normal file
231
dist/tools.js
vendored
Normal file
@@ -0,0 +1,231 @@
|
||||
/**
|
||||
* MCP tool definitions and handlers for Confluence Cloud.
|
||||
*/
|
||||
import { listSpaces, searchPages, getPage, createPage, updatePage, getPageComments, addPageComment, createSpace, getWebUrl, } from './client.js';
|
||||
// --- Tool Definitions ---
|
||||
export const toolDefinitions = [
|
||||
{
|
||||
name: 'confluence_list_spaces',
|
||||
description: 'List available Confluence spaces.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
limit: {
|
||||
type: 'number',
|
||||
description: 'Max spaces to return (default: 25)',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'confluence_create_space',
|
||||
description: 'Create a new Confluence space.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
key: {
|
||||
type: 'string',
|
||||
description: 'Space key (uppercase, e.g. "AI", "CLAUDE")',
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'Space display name',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
description: 'Space description (optional)',
|
||||
},
|
||||
},
|
||||
required: ['key', 'name'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'confluence_search',
|
||||
description: 'Search Confluence pages by text content. Returns matching pages with titles and links.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
query: {
|
||||
type: 'string',
|
||||
description: 'Search text to find in pages',
|
||||
},
|
||||
space_id: {
|
||||
type: 'string',
|
||||
description: 'Filter by space ID (optional)',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
description: 'Max results (default: 10)',
|
||||
},
|
||||
},
|
||||
required: ['query'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'confluence_get_page',
|
||||
description: 'Get a Confluence page by ID, including its full body content and version number. Use the version number for updates.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
page_id: {
|
||||
type: 'string',
|
||||
description: 'Confluence page ID',
|
||||
},
|
||||
},
|
||||
required: ['page_id'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'confluence_create_page',
|
||||
description: 'Create a new Confluence page in a space. Body is XHTML storage format (supports <p>, <h1>-<h6>, <table>, <ac:structured-macro>, etc).',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
space_id: {
|
||||
type: 'string',
|
||||
description: 'Space ID to create the page in',
|
||||
},
|
||||
title: {
|
||||
type: 'string',
|
||||
description: 'Page title',
|
||||
},
|
||||
body: {
|
||||
type: 'string',
|
||||
description: 'Page body in Confluence storage format (XHTML). Example: "<p>Hello world</p>"',
|
||||
},
|
||||
parent_id: {
|
||||
type: 'string',
|
||||
description: 'Parent page ID (optional, creates at root if omitted)',
|
||||
},
|
||||
},
|
||||
required: ['space_id', 'title', 'body'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'confluence_update_page',
|
||||
description: 'Update an existing Confluence page. MUST provide the current version number (from confluence_get_page) incremented by 1. Replaces the full page body.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
page_id: {
|
||||
type: 'string',
|
||||
description: 'Page ID to update',
|
||||
},
|
||||
title: {
|
||||
type: 'string',
|
||||
description: 'Page title (can be changed)',
|
||||
},
|
||||
body: {
|
||||
type: 'string',
|
||||
description: 'New page body in Confluence storage format (replaces entire body)',
|
||||
},
|
||||
version_number: {
|
||||
type: 'number',
|
||||
description: 'New version number (must be current version + 1)',
|
||||
},
|
||||
version_message: {
|
||||
type: 'string',
|
||||
description: 'Version change message (optional)',
|
||||
},
|
||||
},
|
||||
required: ['page_id', 'title', 'body', 'version_number'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'confluence_get_comments',
|
||||
description: 'Get footer comments on a Confluence page.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
page_id: {
|
||||
type: 'string',
|
||||
description: 'Page ID',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
description: 'Max comments (default: 25)',
|
||||
},
|
||||
},
|
||||
required: ['page_id'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'confluence_add_comment',
|
||||
description: 'Add a footer comment to a Confluence page.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
page_id: {
|
||||
type: 'string',
|
||||
description: 'Page ID to comment on',
|
||||
},
|
||||
body: {
|
||||
type: 'string',
|
||||
description: 'Comment body in storage format (XHTML)',
|
||||
},
|
||||
},
|
||||
required: ['page_id', 'body'],
|
||||
},
|
||||
},
|
||||
];
|
||||
// --- Handlers ---
|
||||
export async function handleListSpaces(args) {
|
||||
const spaces = await listSpaces(args.limit);
|
||||
if (spaces.length === 0)
|
||||
return 'No spaces found.';
|
||||
const lines = spaces.map((s) => `${s.key} | ${s.name} | ID: ${s.id} | ${s.type}`);
|
||||
return `Found ${spaces.length} spaces:\n\n${lines.join('\n')}`;
|
||||
}
|
||||
export async function handleCreateSpace(args) {
|
||||
const space = await createSpace(args.key, args.name, args.description);
|
||||
return `Space created: ${space.key} (${space.name}) — ID: ${space.id}`;
|
||||
}
|
||||
export async function handleSearch(args) {
|
||||
const pages = await searchPages(args.query, args.space_id, args.limit);
|
||||
if (pages.length === 0)
|
||||
return `No pages found for: ${args.query}`;
|
||||
const lines = pages.map((p) => {
|
||||
const url = getWebUrl(p);
|
||||
return `${p.title} | ID: ${p.id} | v${p.version.number} | ${url}`;
|
||||
});
|
||||
return `Found ${pages.length} pages:\n\n${lines.join('\n')}`;
|
||||
}
|
||||
export async function handleGetPage(args) {
|
||||
const page = await getPage(args.page_id, true);
|
||||
const url = getWebUrl(page);
|
||||
const body = page.body?.storage?.value || '(empty)';
|
||||
return [
|
||||
`Title: ${page.title}`,
|
||||
`ID: ${page.id} | Space: ${page.spaceId} | Version: ${page.version.number}`,
|
||||
`URL: ${url}`,
|
||||
`Last updated: ${page.version.createdAt}`,
|
||||
'',
|
||||
'--- Content (storage format) ---',
|
||||
body,
|
||||
].join('\n');
|
||||
}
|
||||
export async function handleCreatePage(args) {
|
||||
const page = await createPage(args.space_id, args.title, args.body, args.parent_id);
|
||||
const url = getWebUrl(page);
|
||||
return `Page created: "${page.title}" | ID: ${page.id} | Version: ${page.version.number}\nURL: ${url}`;
|
||||
}
|
||||
export async function handleUpdatePage(args) {
|
||||
const page = await updatePage(args.page_id, args.title, args.body, args.version_number, args.version_message);
|
||||
const url = getWebUrl(page);
|
||||
return `Page updated: "${page.title}" | Version: ${page.version.number}\nURL: ${url}`;
|
||||
}
|
||||
export async function handleGetComments(args) {
|
||||
const comments = await getPageComments(args.page_id, args.limit);
|
||||
if (comments.length === 0)
|
||||
return 'No comments on this page.';
|
||||
const lines = comments.map((c) => {
|
||||
const body = c.body?.storage?.value || '(empty)';
|
||||
const date = c.version?.createdAt || 'unknown';
|
||||
return `[${date}] ${body}`;
|
||||
});
|
||||
return `${comments.length} comments:\n\n${lines.join('\n\n')}`;
|
||||
}
|
||||
export async function handleAddComment(args) {
|
||||
const comment = await addPageComment(args.page_id, args.body);
|
||||
return `Comment added (ID: ${comment.id})`;
|
||||
}
|
||||
Reference in New Issue
Block a user