#
tokens: 5853/50000 11/11 files
lines: off (toggle) GitHub
raw markdown copy
# 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.")

```