# Directory Structure ``` ├── .env_example ├── .gitignore ├── .python-version ├── claude-integration-guide.md ├── confluence-mcp-config.json ├── confluence.py ├── example_usage.py ├── install-claude-integration.ps1 ├── pyproject.toml ├── README.md ├── requirements.txt └── uv.lock ``` # Files -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- ``` 3.12 ``` -------------------------------------------------------------------------------- /.env_example: -------------------------------------------------------------------------------- ``` CONFLUENCE_API_BASE=http://localhost:8090/rest/api CONFLUENCE_USERNAME=admin CONFLUENCE_PASSWORD=admin ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` # Python-generated files __pycache__/ *.py[oc] build/ dist/ wheels/ *.egg-info # Virtual environments .venv .env ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown # Confluence MCP Server A Model Context Protocol (MCP) server for interacting with Confluence Data Center via REST API. This server provides a set of tools that allow AI models to interact with Confluence content. ## Features This MCP server provides the following operations for Confluence: - Execute CQL (Confluence Query Language) searches - Get page content by ID - Get page content with body - Find pages by space key - Find page by title and space key - Create new pages (with optional parent page) - Update existing pages - Delete pages ## Installation 1. Clone this repository 2. Install dependencies: ```bash pip install -r requirements.txt ``` ## Configuration Create a `.env` file in the project root with the following variables: ``` CONFLUENCE_API_BASE=http://localhost:8090/rest/api CONFLUENCE_USERNAME=your_username CONFLUENCE_PASSWORD=your_password ``` Adjust the values to match your Confluence instance. ## Running the Server ### Development Mode (Recommended) The proper way to run an MCP server is using the MCP CLI tool with the development mode. This will start the MCP Inspector UI which allows you to test and debug the server: ```bash mcp dev confluence.py ``` This will start the MCP Inspector at http://127.0.0.1:6274 by default. ### Direct Execution (Not Recommended) MCP servers are designed to be run with the MCP CLI tool or integrated with Claude Desktop. Direct execution with Python is not the standard way to run an MCP server, but the script includes a fallback mode for testing: ```bash python confluence.py ``` However, this mode has limited functionality and is only intended for basic testing. ### Installing in Claude Desktop To install the server in Claude Desktop: ```bash mcp install confluence.py ``` ## API Reference ### execute_cql_search Execute a CQL query on Confluence to search pages. **Parameters:** - `cql`: CQL query string - `limit`: Number of results to return (default: 10) ### get_page_content Get the content of a Confluence page. **Parameters:** - `pageId`: Confluence Page ID ### get_page_with_body Get a page with its body content. **Parameters:** - `pageId`: Confluence Page ID ### find_pages_by_space Find pages by space key. **Parameters:** - `spaceKey`: Confluence Space Key - `limit`: Maximum number of results to return (default: 10) - `expand`: Optional comma-separated list of properties to expand ### find_page_by_title Find a page by title and space key. **Parameters:** - `title`: Page title - `spaceKey`: Confluence Space Key ### create_page Create a new page in Confluence. **Parameters:** - `title`: Page title - `spaceKey`: Confluence Space Key - `content`: Page content in storage format (HTML) - `parentId`: Optional parent page ID ### update_page Update an existing page in Confluence. **Parameters:** - `pageId`: Confluence Page ID - `content`: New page content in storage format (HTML) - `title`: Optional new title for the page - `spaceKey`: Optional space key (only needed if changing space) ### delete_page Delete a page by ID. **Parameters:** - `pageId`: Confluence Page ID ## Example Usage Once the server is running and connected to an AI model, you can interact with Confluence using natural language. For example: - "Find all pages in the DOCS space" - "Get the content of page with ID 123456" - "Create a new page titled 'Meeting Notes' in the TEAM space with content '<p>Notes from our meeting</p>'" - "Update page with ID 123456 to have the content '<p>Updated meeting notes</p>'" - "Update the title of page 123456 to 'Revised Meeting Notes'" ## License MIT ``` -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- ``` httpx>=0.24.0 mcp>=0.4.0 uvicorn>=0.23.0 python-dotenv>=1.0.0 ``` -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- ```toml [project] name = "mcp-confluence-server" version = "0.1.0" description = "Add your description here" readme = "README.md" requires-python = ">=3.12" dependencies = [ "httpx>=0.28.1", "mcp[cli]>=1.6.0", "python-dotenv>=1.1.0", "uvicorn>=0.34.2", ] ``` -------------------------------------------------------------------------------- /install-claude-integration.ps1: -------------------------------------------------------------------------------- ``` # PowerShell script to install the Confluence MCP server in Claude Desktop # Run this script from the project directory # Check if MCP is installed try { $mcpVersion = (uv run mcp --version) 2>&1 Write-Host "Found MCP: $mcpVersion" } catch { Write-Host "MCP CLI not found. Installing required packages..." uv pip install "mcp[cli]" } # Ensure we're in the right directory $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path Set-Location $scriptDir # Check if config file exists $configFile = "confluence-mcp-config.json" if (-not (Test-Path $configFile)) { Write-Host "Configuration file not found: $configFile" exit 1 } # Install the MCP server in Claude Desktop Write-Host "Installing Confluence MCP server in Claude Desktop..." uv run mcp install --config $configFile Write-Host "`nInstallation complete!" Write-Host "To verify the installation, open Claude Desktop and check the MCP Servers section in Settings." Write-Host "You can now use Claude to interact with your Confluence instance." ``` -------------------------------------------------------------------------------- /confluence-mcp-config.json: -------------------------------------------------------------------------------- ```json { "name": "confluence-server", "displayName": "Confluence Data Center", "description": "MCP server for interacting with Confluence Data Center via REST API", "version": "1.0.0", "command": "uv", "args": ["run", "--with", "mcp", "mcp", "run", "server.py"], "env": { "CONFLUENCE_API_BASE": "http://localhost:8090/rest/api", "CONFLUENCE_USERNAME": "admin", "CONFLUENCE_PASSWORD": "@Qi85yhn", "PYTHONUNBUFFERED": "1", "PYTHONIOENCODING": "utf-8" }, "workingDirectory": "${configDir}", "transportType": "stdio", "icon": "https://wac-cdn.atlassian.com/assets/img/favicons/confluence/favicon-32x32.png", "capabilities": { "tools": { "listChanged": true }, "resources": { "subscribe": true, "listChanged": true }, "prompts": { "listChanged": true } }, "metadata": { "author": "Your Name", "homepage": "https://github.com/yourusername/mcp-confluence-server", "category": "Knowledge Management", "tags": ["confluence", "documentation", "knowledge-base"] } } ``` -------------------------------------------------------------------------------- /claude-integration-guide.md: -------------------------------------------------------------------------------- ```markdown # Integrating Confluence MCP Server with Claude Desktop This guide explains how to integrate your Confluence MCP server with Claude Desktop, allowing Claude to interact with your Confluence instance through the MCP protocol. ## Prerequisites - Claude Desktop installed - Python 3.9+ installed - `uv` and `mcp` packages installed - Confluence server accessible ## Integration Methods There are two ways to integrate your MCP server with Claude Desktop: 1. **Simple Installation** - Quick but with limited configuration 2. **JSON Configuration Installation** - More control and customization ## Method 1: Simple Installation For a quick installation with default settings: ```bash # Navigate to your project directory cd path/to/mcp-confluence-server # Install the server in Claude Desktop mcp install server.py ``` ## Method 2: JSON Configuration Installation (Recommended) This method gives you more control over how your server is configured in Claude Desktop. ### Step 1: Edit the Configuration File The `confluence-mcp-config.json` file contains all the settings for your MCP server. Customize it as needed: - Update the `CONFLUENCE_API_BASE` to point to your Confluence instance - Set the correct `CONFLUENCE_USERNAME` and `CONFLUENCE_PASSWORD` - Modify the `metadata` section with your information ### Step 2: Install Using the Configuration File ```bash # Navigate to your project directory cd path/to/mcp-confluence-server # Install with the configuration file mcp install --config confluence-mcp-config.json ``` ### Step 3: Verify Installation 1. Open Claude Desktop 2. Click on the "..." menu in the top-right corner 3. Select "Settings" 4. Go to the "MCP Servers" tab 5. Confirm that "Confluence Data Center" appears in the list of installed servers ## Using Custom Commands If you need to customize how the server is run, you can modify the `command` and `args` fields in the configuration file: ```json { "command": "python", "args": ["-m", "mcp", "run", "server.py"] } ``` Or for using a virtual environment: ```json { "command": ".venv/Scripts/python", "args": ["-m", "mcp", "run", "server.py"] } ``` ## Troubleshooting If you encounter issues with the integration: 1. **Check Logs**: Claude Desktop logs can help identify issues ```bash mcp logs ``` 2. **Test the Server**: Make sure the server runs correctly in development mode ```bash mcp dev server.py ``` 3. **Reinstall**: If needed, uninstall and reinstall the server ```bash mcp uninstall confluence-server mcp install --config confluence-mcp-config.json ``` ## Advanced Configuration Options The JSON configuration supports additional options: - `icon`: URL or path to an icon for the server in Claude Desktop - `capabilities`: Specify which MCP capabilities your server supports - `metadata`: Additional information about your server - `env`: Environment variables for the server process ## Example: Complete Installation Command Here's a complete example command that installs the server with all options: ```bash mcp install --config confluence-mcp-config.json --name "Confluence Server" --description "Access Confluence content from Claude" --icon https://path/to/icon.png ``` However, when using a config file, the command-line options are usually not necessary as they're defined in the JSON file. ``` -------------------------------------------------------------------------------- /example_usage.py: -------------------------------------------------------------------------------- ```python """ Example script demonstrating how to use the Confluence MCP server. This script uses the MCP client to connect to the server and execute operations. """ import asyncio import json from mcp import ClientSession, StdioServerParameters, types from mcp.client.stdio import stdio_client # Create server parameters for stdio connection server_params = StdioServerParameters( command="python", # Executable args=["server.py"], # Server script env=None, # Use default environment ) async def print_json(data): """Print data as formatted JSON.""" print(json.dumps(data, indent=2)) async def run_examples(): """Run example operations against the Confluence server.""" print("Connecting to Confluence MCP server...") async with stdio_client(server_params) as (read, write): async with ClientSession(read, write) as session: # Initialize the connection await session.initialize() # List available tools print("\n=== Available Tools ===") tools = await session.list_tools() for tool in tools: print(f"- {tool.name}: {tool.description}") # Example 1: Find pages by space print("\n=== Finding pages in space 'COD' ===") try: result = await session.call_tool( "find_pages_by_space", arguments={"spaceKey": "COD", "limit": 5} ) await print_json(result) except Exception as e: print(f"Error finding pages: {str(e)}") # Example 2: Execute CQL search print("\n=== Executing CQL search ===") try: result = await session.call_tool( "execute_cql_search", arguments={"cql": "space = COD", "limit": 3} ) await print_json(result) except Exception as e: print(f"Error executing search: {str(e)}") # Example 3: Find page by title print("\n=== Finding page by title ===") try: result = await session.call_tool( "find_page_by_title", arguments={"title": "demo page", "spaceKey": "COD"} ) await print_json(result) # If we found a page, get its content if result.get("results") and len(result["results"]) > 0: page_id = result["results"][0]["id"] print(f"\n=== Getting content for page {page_id} ===") content_result = await session.call_tool( "get_page_with_body", arguments={"pageId": page_id} ) # Just print the title and a snippet of the body to avoid too much output print(f"Title: {content_result.get('title', 'Unknown')}") body = content_result.get("body", {}).get("storage", {}).get("value", "") print(f"Body snippet: {body[:100]}...") except Exception as e: print(f"Error finding page: {str(e)}") # Example 4: Create a new page (commented out to avoid creating pages accidentally) """ print("\n=== Creating a new page ===") try: result = await session.call_tool( "create_page", arguments={ "title": "Test Page from MCP Client", "spaceKey": "COD", "content": "<p>This is a test page created by the MCP client.</p>" } ) await print_json(result) # If we created a page, delete it to clean up if "id" in result: page_id = result["id"] print(f"\n=== Deleting page {page_id} ===") delete_result = await session.call_tool( "delete_page", arguments={"pageId": page_id} ) await print_json(delete_result) except Exception as e: print(f"Error creating/deleting page: {str(e)}") """ if __name__ == "__main__": asyncio.run(run_examples()) ``` -------------------------------------------------------------------------------- /confluence.py: -------------------------------------------------------------------------------- ```python from typing import Any, Dict, List, Optional, Union import os import base64 import json import logging from contextlib import asynccontextmanager from dataclasses import dataclass from collections.abc import AsyncIterator import httpx from dotenv import load_dotenv from mcp.server.fastmcp import Context, FastMCP # Load environment variables from .env file load_dotenv() # Configure logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) # Constants CONFLUENCE_API_BASE = os.environ.get("CONFLUENCE_API_BASE", "http://localhost:8090/rest/api") CONFLUENCE_USERNAME = os.environ.get("CONFLUENCE_USERNAME", "admin") CONFLUENCE_PASSWORD = os.environ.get("CONFLUENCE_PASSWORD", "admin") # Initialize FastMCP server mcp = FastMCP("confluence") @dataclass class ConfluenceContext: """Context for Confluence API client.""" client: httpx.AsyncClient @asynccontextmanager async def confluence_lifespan(server: FastMCP) -> AsyncIterator[ConfluenceContext]: """Manage Confluence API client lifecycle.""" # Create auth header auth_str = f"{CONFLUENCE_USERNAME}:{CONFLUENCE_PASSWORD}" auth_bytes = auth_str.encode('ascii') auth_b64 = base64.b64encode(auth_bytes).decode('ascii') # Initialize HTTP client with authentication async with httpx.AsyncClient( base_url=CONFLUENCE_API_BASE, headers={ "Authorization": f"Basic {auth_b64}", "Content-Type": "application/json", "Accept": "application/json" }, timeout=30.0 ) as client: logger.info(f"Initialized Confluence API client for {CONFLUENCE_API_BASE}") yield ConfluenceContext(client=client) # Set up lifespan context mcp = FastMCP("confluence", lifespan=confluence_lifespan) # Helper functions async def handle_response(response: httpx.Response) -> Dict[str, Any]: """Handle API response, raising appropriate exceptions for errors.""" try: response.raise_for_status() return response.json() except httpx.HTTPStatusError as e: error_msg = f"HTTP error: {e.response.status_code}" try: error_data = e.response.json() error_msg = f"{error_msg} - {error_data.get('message', 'Unknown error')}" except Exception: error_msg = f"{error_msg} - {e.response.text or 'No error details'}" logger.error(error_msg) raise ValueError(error_msg) except Exception as e: logger.error(f"Error processing response: {str(e)}") raise ValueError(f"Failed to process response: {str(e)}") # MCP Tools for Confluence API @mcp.tool() async def update_page(ctx: Context, pageId: str, content: str, title: str = None, spaceKey: str = None) -> Dict[str, Any]: """ Update an existing page in Confluence. Args: pageId: Confluence Page ID content: New page content in storage format (HTML) title: Optional new title for the page spaceKey: Optional space key (only needed if changing space) Returns: Updated page details """ client = ctx.request_context.lifespan_context.client # First, get the current page to obtain version number and other details logger.info(f"Getting current page details for page ID: {pageId}") response = await client.get(f"/content/{pageId}") current_page = await handle_response(response) # Prepare update data update_data = { "id": pageId, "type": "page", "title": title or current_page.get("title", ""), "body": { "storage": { "value": content, "representation": "storage" } }, "version": { "number": current_page.get("version", {}).get("number", 0) + 1 } } # Add space if specified if spaceKey: update_data["space"] = {"key": spaceKey} elif "space" in current_page and "key" in current_page["space"]: update_data["space"] = {"key": current_page["space"]["key"]} logger.info(f"Updating page with ID: {pageId}") response = await client.put(f"/content/{pageId}", json=update_data) return await handle_response(response) @mcp.tool() async def execute_cql_search(ctx: Context, cql: str, limit: int = 10) -> Dict[str, Any]: """ Execute a CQL query on Confluence to search pages. Args: cql: CQL query string limit: Number of results to return Returns: Search results """ client = ctx.request_context.lifespan_context.client params = {"cql": cql, "limit": limit} logger.info(f"Executing CQL search: {cql} with limit {limit}") response = await client.get("/content/search", params=params) return await handle_response(response) @mcp.tool() async def get_page_content(ctx: Context, pageId: str) -> Dict[str, Any]: """ Get the content of a Confluence page. Args: pageId: Confluence Page ID Returns: Page content """ client = ctx.request_context.lifespan_context.client logger.info(f"Getting page content for page ID: {pageId}") response = await client.get(f"/content/{pageId}") return await handle_response(response) @mcp.tool() async def get_page_with_body(ctx: Context, pageId: str) -> Dict[str, Any]: """ Get a page with its body content. Args: pageId: Confluence Page ID Returns: Page with body content """ client = ctx.request_context.lifespan_context.client logger.info(f"Getting page with body for page ID: {pageId}") response = await client.get(f"/content/{pageId}?expand=body.storage") return await handle_response(response) @mcp.tool() async def find_pages_by_space(ctx: Context, spaceKey: str, limit: int = 10, expand: Optional[str] = None) -> Dict[str, Any]: """ Find pages by space key. Args: spaceKey: Confluence Space Key limit: Maximum number of results to return expand: Optional comma-separated list of properties to expand Returns: List of pages in the space """ client = ctx.request_context.lifespan_context.client params = {"spaceKey": spaceKey, "limit": limit} if expand: params["expand"] = expand logger.info(f"Finding pages in space: {spaceKey}") # Use scan endpoint for better performance in Confluence 7.18+ try: response = await client.get("/content/scan", params=params) return await handle_response(response) except Exception as e: logger.warning(f"Scan endpoint failed, falling back to standard endpoint: {str(e)}") response = await client.get("/content", params=params) return await handle_response(response) @mcp.tool() async def find_page_by_title(ctx: Context, title: str, spaceKey: str) -> Dict[str, Any]: """ Find a page by title and space key. Args: title: Page title spaceKey: Confluence Space Key Returns: Page details if found """ client = ctx.request_context.lifespan_context.client params = {"title": title, "spaceKey": spaceKey} logger.info(f"Finding page by title: {title} in space: {spaceKey}") response = await client.get("/content", params=params) return await handle_response(response) @mcp.tool() async def create_page( ctx: Context, title: str, spaceKey: str, content: str, parentId: Optional[str] = None ) -> Dict[str, Any]: """ Create a new page in Confluence. Args: title: Page title spaceKey: Confluence Space Key content: Page content in storage format (HTML) parentId: Optional parent page ID Returns: Created page details """ client = ctx.request_context.lifespan_context.client # Prepare page data page_data = { "type": "page", "title": title, "space": {"key": spaceKey}, "body": { "storage": { "value": content, "representation": "storage" } } } # Add parent if specified if parentId: page_data["ancestors"] = [{"id": parentId}] logger.info(f"Creating page: {title} in space: {spaceKey}") response = await client.post("/content", json=page_data) return await handle_response(response) @mcp.tool() async def delete_page(ctx: Context, pageId: str) -> Dict[str, Any]: """ Delete a page by ID. Args: pageId: Confluence Page ID Returns: Deletion status """ client = ctx.request_context.lifespan_context.client logger.info(f"Deleting page with ID: {pageId}") response = await client.delete(f"/content/{pageId}") if response.status_code == 204: return {"status": "success", "message": f"Page {pageId} deleted successfully"} return await handle_response(response) # This server is designed to be run with the MCP CLI tool # To run it, use: mcp dev server.py # If you want to run it directly (not recommended), you can use this code if __name__ == "__main__": print("=== Confluence MCP Server ===") print("This server is designed to be run with the MCP CLI tool.") print("To run it properly, use: mcp dev server.py") print("\nAttempting to start in direct mode (for testing only)...") try: import asyncio from mcp.server.stdio import stdio_server async def run(): # This is a simplified version that works for direct execution async with stdio_server() as (read, write): await mcp.start(read, write) # Run the server asyncio.run(run()) except Exception as e: print(f"\nError starting server: {e}") print("\nPlease use 'mcp dev server.py' instead.") ```