From db08853c6b80f7311438ae06cca9780d9c523b5d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 29 Jan 2026 14:05:14 +0000 Subject: [PATCH 1/4] Initial plan From edddd27e32127e7d9abe3b59d8e346b203c332a0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 29 Jan 2026 14:13:16 +0000 Subject: [PATCH 2/4] Add MCP server project with core functionality Co-authored-by: Lakritzator <708125+Lakritzator@users.noreply.github.com> --- .../ConfluenceSettings.cs | 32 ++ .../Dapplo.Confluence.McpServer.csproj | 31 ++ .../McpServerHandler.cs | 114 +++++ .../Models/McpRequest.cs | 21 + .../Models/McpResponse.cs | 36 ++ src/Dapplo.Confluence.McpServer/Program.cs | 128 ++++++ src/Dapplo.Confluence.McpServer/README.md | 411 +++++++++++++++++ .../Tools/ToolDefinition.cs | 18 + .../Tools/ToolRegistry.cs | 427 ++++++++++++++++++ .../appsettings.json | 7 + src/Dapplo.Confluence.sln | 50 +- 11 files changed, 1271 insertions(+), 4 deletions(-) create mode 100644 src/Dapplo.Confluence.McpServer/ConfluenceSettings.cs create mode 100644 src/Dapplo.Confluence.McpServer/Dapplo.Confluence.McpServer.csproj create mode 100644 src/Dapplo.Confluence.McpServer/McpServerHandler.cs create mode 100644 src/Dapplo.Confluence.McpServer/Models/McpRequest.cs create mode 100644 src/Dapplo.Confluence.McpServer/Models/McpResponse.cs create mode 100644 src/Dapplo.Confluence.McpServer/Program.cs create mode 100644 src/Dapplo.Confluence.McpServer/README.md create mode 100644 src/Dapplo.Confluence.McpServer/Tools/ToolDefinition.cs create mode 100644 src/Dapplo.Confluence.McpServer/Tools/ToolRegistry.cs create mode 100644 src/Dapplo.Confluence.McpServer/appsettings.json diff --git a/src/Dapplo.Confluence.McpServer/ConfluenceSettings.cs b/src/Dapplo.Confluence.McpServer/ConfluenceSettings.cs new file mode 100644 index 0000000..f829532 --- /dev/null +++ b/src/Dapplo.Confluence.McpServer/ConfluenceSettings.cs @@ -0,0 +1,32 @@ +namespace Dapplo.Confluence.McpServer; + +/// +/// Configuration settings for the Confluence MCP server +/// +public class ConfluenceSettings +{ + /// + /// The Confluence server URL (e.g., https://yourcompany.atlassian.net) + /// + public string ConfluenceUrl { get; set; } = string.Empty; + + /// + /// Authentication method: "basic" or "bearer" + /// + public string AuthType { get; set; } = "bearer"; + + /// + /// Username for basic authentication + /// + public string? Username { get; set; } + + /// + /// Password or API token for basic authentication + /// + public string? Password { get; set; } + + /// + /// Bearer token for token-based authentication + /// + public string? BearerToken { get; set; } +} diff --git a/src/Dapplo.Confluence.McpServer/Dapplo.Confluence.McpServer.csproj b/src/Dapplo.Confluence.McpServer/Dapplo.Confluence.McpServer.csproj new file mode 100644 index 0000000..997ce6a --- /dev/null +++ b/src/Dapplo.Confluence.McpServer/Dapplo.Confluence.McpServer.csproj @@ -0,0 +1,31 @@ + + + + Exe + net10.0 + enable + enable + MCP (Model Context Protocol) server for Confluence API integration + mcp;confluence;atlassian;dapplo;copilot + false + false + + + + + + + + + + + + + + + + PreserveNewest + + + + diff --git a/src/Dapplo.Confluence.McpServer/McpServerHandler.cs b/src/Dapplo.Confluence.McpServer/McpServerHandler.cs new file mode 100644 index 0000000..ae4a546 --- /dev/null +++ b/src/Dapplo.Confluence.McpServer/McpServerHandler.cs @@ -0,0 +1,114 @@ +using Dapplo.Confluence.McpServer.Models; +using Dapplo.Confluence.McpServer.Tools; +using System.Text.Json; +using System.Text.Json.Nodes; + +namespace Dapplo.Confluence.McpServer; + +/// +/// MCP server handler that processes JSON-RPC requests +/// +public class McpServerHandler +{ + private readonly ToolRegistry _toolRegistry; + private const string McpVersion = "2024-11-05"; + + public McpServerHandler(IConfluenceClient confluenceClient) + { + _toolRegistry = new ToolRegistry(confluenceClient); + } + + /// + /// Process an MCP request and return a response + /// + public async Task ProcessRequestAsync(McpRequest request) + { + try + { + var result = request.Method switch + { + "initialize" => HandleInitialize(request.Params), + "tools/list" => HandleToolsList(), + "tools/call" => await HandleToolsCallAsync(request.Params), + "ping" => new { }, + _ => throw new InvalidOperationException($"Unknown method: {request.Method}") + }; + + return new McpResponse + { + Id = request.Id, + Result = result + }; + } + catch (Exception ex) + { + return new McpResponse + { + Id = request.Id, + Error = new McpError + { + Code = -32603, + Message = ex.Message, + Data = new { exception = ex.GetType().Name } + } + }; + } + } + + private object HandleInitialize(object? parameters) + { + return new + { + protocolVersion = McpVersion, + capabilities = new + { + tools = new { } + }, + serverInfo = new + { + name = "confluence-mcp-server", + version = "1.0.0" + } + }; + } + + private object HandleToolsList() + { + var tools = _toolRegistry.GetToolDefinitions(); + return new { tools }; + } + + private async Task HandleToolsCallAsync(object? parameters) + { + if (parameters == null) + throw new ArgumentException("Parameters required for tools/call"); + + var jsonElement = JsonSerializer.SerializeToElement(parameters); + + var toolName = jsonElement.GetProperty("name").GetString() + ?? throw new ArgumentException("Tool name is required"); + + JsonElement? toolParams = null; + if (jsonElement.TryGetProperty("arguments", out var argsElement)) + { + toolParams = argsElement; + } + + var result = await _toolRegistry.ExecuteToolAsync(toolName, toolParams); + + return new + { + content = new[] + { + new + { + type = "text", + text = JsonSerializer.Serialize(result, new JsonSerializerOptions + { + WriteIndented = true + }) + } + } + }; + } +} diff --git a/src/Dapplo.Confluence.McpServer/Models/McpRequest.cs b/src/Dapplo.Confluence.McpServer/Models/McpRequest.cs new file mode 100644 index 0000000..6eae6f4 --- /dev/null +++ b/src/Dapplo.Confluence.McpServer/Models/McpRequest.cs @@ -0,0 +1,21 @@ +using System.Text.Json.Serialization; + +namespace Dapplo.Confluence.McpServer.Models; + +/// +/// Represents a JSON-RPC 2.0 request for MCP +/// +public class McpRequest +{ + [JsonPropertyName("jsonrpc")] + public string JsonRpc { get; set; } = "2.0"; + + [JsonPropertyName("id")] + public object? Id { get; set; } + + [JsonPropertyName("method")] + public string Method { get; set; } = string.Empty; + + [JsonPropertyName("params")] + public object? Params { get; set; } +} diff --git a/src/Dapplo.Confluence.McpServer/Models/McpResponse.cs b/src/Dapplo.Confluence.McpServer/Models/McpResponse.cs new file mode 100644 index 0000000..41b2ec0 --- /dev/null +++ b/src/Dapplo.Confluence.McpServer/Models/McpResponse.cs @@ -0,0 +1,36 @@ +using System.Text.Json.Serialization; + +namespace Dapplo.Confluence.McpServer.Models; + +/// +/// Represents a JSON-RPC 2.0 response for MCP +/// +public class McpResponse +{ + [JsonPropertyName("jsonrpc")] + public string JsonRpc { get; set; } = "2.0"; + + [JsonPropertyName("id")] + public object? Id { get; set; } + + [JsonPropertyName("result")] + public object? Result { get; set; } + + [JsonPropertyName("error")] + public McpError? Error { get; set; } +} + +/// +/// Represents a JSON-RPC 2.0 error +/// +public class McpError +{ + [JsonPropertyName("code")] + public int Code { get; set; } + + [JsonPropertyName("message")] + public string Message { get; set; } = string.Empty; + + [JsonPropertyName("data")] + public object? Data { get; set; } +} diff --git a/src/Dapplo.Confluence.McpServer/Program.cs b/src/Dapplo.Confluence.McpServer/Program.cs new file mode 100644 index 0000000..d8b94cc --- /dev/null +++ b/src/Dapplo.Confluence.McpServer/Program.cs @@ -0,0 +1,128 @@ +using Dapplo.Confluence; +using Dapplo.Confluence.McpServer; +using Dapplo.Confluence.McpServer.Models; +using Microsoft.Extensions.Configuration; +using System.Text; +using System.Text.Json; + +// Load configuration from appsettings.json and environment variables +var configuration = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json", optional: true) + .AddEnvironmentVariables(prefix: "CONFLUENCE_") + .Build(); + +var settings = new ConfluenceSettings(); +configuration.Bind(settings); + +// Validate configuration +if (string.IsNullOrEmpty(settings.ConfluenceUrl)) +{ + await Console.Error.WriteLineAsync("Error: ConfluenceUrl is not configured. Set via appsettings.json or environment variable CONFLUENCE_ConfluenceUrl"); + return 1; +} + +// Create Confluence client based on auth type +IConfluenceClient confluenceClient; +try +{ + var confluenceUri = new Uri(settings.ConfluenceUrl); + + if (settings.AuthType.Equals("basic", StringComparison.OrdinalIgnoreCase)) + { + if (string.IsNullOrEmpty(settings.Username) || string.IsNullOrEmpty(settings.Password)) + { + await Console.Error.WriteLineAsync("Error: Username and Password are required for basic authentication"); + return 1; + } + + confluenceClient = ConfluenceClient.Create(confluenceUri); + confluenceClient.SetBasicAuthentication(settings.Username, settings.Password); + } + else if (settings.AuthType.Equals("bearer", StringComparison.OrdinalIgnoreCase)) + { + if (string.IsNullOrEmpty(settings.BearerToken)) + { + await Console.Error.WriteLineAsync("Error: BearerToken is required for bearer authentication"); + return 1; + } + + confluenceClient = ConfluenceClient.Create(confluenceUri); + confluenceClient.SetBearerAuthentication(settings.BearerToken); + } + else + { + await Console.Error.WriteLineAsync($"Error: Invalid AuthType '{settings.AuthType}'. Use 'basic' or 'bearer'"); + return 1; + } +} +catch (Exception ex) +{ + await Console.Error.WriteLineAsync($"Error initializing Confluence client: {ex.Message}"); + return 1; +} + +// Create MCP server handler +var serverHandler = new McpServerHandler(confluenceClient); + +// Write startup message to stderr (stdout is reserved for MCP protocol) +await Console.Error.WriteLineAsync($"Confluence MCP Server started"); +await Console.Error.WriteLineAsync($"Server URL: {settings.ConfluenceUrl}"); +await Console.Error.WriteLineAsync($"Auth Type: {settings.AuthType}"); +await Console.Error.WriteLineAsync("Waiting for MCP requests on stdin..."); + +var jsonOptions = new JsonSerializerOptions +{ + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + WriteIndented = false +}; + +// Process JSON-RPC requests from stdin +using var stdin = Console.OpenStandardInput(); +using var reader = new StreamReader(stdin, Encoding.UTF8); + +while (!reader.EndOfStream) +{ + try + { + var line = await reader.ReadLineAsync(); + if (string.IsNullOrWhiteSpace(line)) + continue; + + // Parse the JSON-RPC request + var request = JsonSerializer.Deserialize(line, jsonOptions); + if (request == null) + { + await Console.Error.WriteLineAsync("Error: Failed to parse request"); + continue; + } + + // Process the request + var response = await serverHandler.ProcessRequestAsync(request); + + // Write response to stdout + var responseJson = JsonSerializer.Serialize(response, jsonOptions); + await Console.Out.WriteLineAsync(responseJson); + await Console.Out.FlushAsync(); + } + catch (Exception ex) + { + await Console.Error.WriteLineAsync($"Error processing request: {ex.Message}"); + + // Send error response + var errorResponse = new McpResponse + { + Error = new McpError + { + Code = -32603, + Message = ex.Message + } + }; + + var errorJson = JsonSerializer.Serialize(errorResponse, jsonOptions); + await Console.Out.WriteLineAsync(errorJson); + await Console.Out.FlushAsync(); + } +} + +return 0; diff --git a/src/Dapplo.Confluence.McpServer/README.md b/src/Dapplo.Confluence.McpServer/README.md new file mode 100644 index 0000000..0930436 --- /dev/null +++ b/src/Dapplo.Confluence.McpServer/README.md @@ -0,0 +1,411 @@ +# Confluence MCP Server + +A Model Context Protocol (MCP) server that provides Confluence integration for AI assistants like Claude Desktop and Microsoft 365 Copilot. + +## Overview + +This MCP server exposes Confluence functionality through a standardized protocol, allowing AI assistants to: +- Search for Confluence content using CQL (Confluence Query Language) +- Retrieve page content and metadata +- Create new pages +- Update existing pages +- List spaces +- Get user information + +## Requirements + +- .NET 10.0 or later +- Confluence Cloud or Server instance +- API token or credentials for authentication + +## Installation + +### Build from Source + +```bash +cd src/Dapplo.Confluence.McpServer +dotnet build -c Release +``` + +### Publish Self-Contained + +For cross-platform deployment: + +```bash +# Windows +dotnet publish -c Release -r win-x64 --self-contained + +# macOS (Intel) +dotnet publish -c Release -r osx-x64 --self-contained + +# macOS (Apple Silicon) +dotnet publish -c Release -r osx-arm64 --self-contained + +# Linux +dotnet publish -c Release -r linux-x64 --self-contained +``` + +## Configuration + +The server supports two authentication methods: + +### Option 1: Bearer Token (Recommended) + +Configure via `appsettings.json`: + +```json +{ + "ConfluenceUrl": "https://yourcompany.atlassian.net", + "AuthType": "bearer", + "BearerToken": "your-api-token-here" +} +``` + +### Option 2: Basic Authentication + +```json +{ + "ConfluenceUrl": "https://yourcompany.atlassian.net", + "AuthType": "basic", + "Username": "your-email@example.com", + "Password": "your-api-token" +} +``` + +### Environment Variables + +You can also configure using environment variables (prefixed with `CONFLUENCE_`): + +```bash +export CONFLUENCE_ConfluenceUrl="https://yourcompany.atlassian.net" +export CONFLUENCE_AuthType="bearer" +export CONFLUENCE_BearerToken="your-api-token" +``` + +## Getting a Confluence API Token + +1. Log in to [https://id.atlassian.com/manage-profile/security/api-tokens](https://id.atlassian.com/manage-profile/security/api-tokens) +2. Click "Create API token" +3. Give it a name (e.g., "MCP Server") +4. Copy the token and use it as your `BearerToken` + +## Available Tools + +### confluence_search +Search for content using CQL. + +**Parameters:** +- `query` (required): CQL query string (e.g., "type=page and space=DEV") +- `limit` (optional): Maximum results (default: 25) + +**Example:** +```json +{ + "query": "type=page and title~'API' and space=DEV", + "limit": 10 +} +``` + +### confluence_get_page +Get a specific page by ID or title. + +**Parameters:** +- `pageId`: Page ID (use either this or title+spaceKey) +- `title`: Page title +- `spaceKey`: Space key (required with title) +- `expand`: Properties to expand (default: "body.storage,version") + +### confluence_create_page +Create a new page. + +**Parameters:** +- `spaceKey` (required): Space key +- `title` (required): Page title +- `content` (required): HTML content in Confluence storage format +- `parentId` (optional): Parent page ID + +### confluence_update_page +Update an existing page. + +**Parameters:** +- `pageId` (required): Page ID +- `title` (required): New title +- `content` (required): New HTML content +- `version` (required): Current version number + +### confluence_list_spaces +List accessible spaces. + +**Parameters:** +- `limit` (optional): Maximum results (default: 25) + +### confluence_get_user_info +Get current user information. + +**Parameters:** None + +## Integration with Claude Desktop + +Add to your Claude Desktop configuration (`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS): + +```json +{ + "mcpServers": { + "confluence": { + "command": "dotnet", + "args": [ + "run", + "--project", + "/path/to/Dapplo.Confluence.McpServer/Dapplo.Confluence.McpServer.csproj" + ], + "env": { + "CONFLUENCE_ConfluenceUrl": "https://yourcompany.atlassian.net", + "CONFLUENCE_AuthType": "bearer", + "CONFLUENCE_BearerToken": "your-api-token" + } + } + } +} +``` + +Or using the published binary: + +```json +{ + "mcpServers": { + "confluence": { + "command": "/path/to/published/Dapplo.Confluence.McpServer", + "env": { + "CONFLUENCE_ConfluenceUrl": "https://yourcompany.atlassian.net", + "CONFLUENCE_AuthType": "bearer", + "CONFLUENCE_BearerToken": "your-api-token" + } + } + } +} +``` + +## Integration with Microsoft 365 Copilot + +Microsoft 365 Copilot can integrate with MCP servers through the following approaches: + +### Option 1: Through Teams AI Library + +Use the Teams AI Library to create a bot that bridges MCP servers: + +1. **Create a Teams Bot Application:** + ```bash + npm install @microsoft/teams-ai + ``` + +2. **Configure Bot to Use MCP Server:** + ```typescript + import { Application } from '@microsoft/teams-ai'; + import { spawn } from 'child_process'; + + // Start MCP server as child process + const mcpServer = spawn('dotnet', [ + 'run', + '--project', + '/path/to/Dapplo.Confluence.McpServer/Dapplo.Confluence.McpServer.csproj' + ], { + env: { + CONFLUENCE_ConfluenceUrl: 'https://yourcompany.atlassian.net', + CONFLUENCE_AuthType: 'bearer', + CONFLUENCE_BearerToken: process.env.CONFLUENCE_TOKEN + } + }); + + // Create Teams app with MCP integration + const app = new Application({ + storage: new MemoryStorage() + }); + + // Register actions that call MCP tools + app.ai.action('searchConfluence', async (context, state, parameters) => { + // Send JSON-RPC request to MCP server + const request = { + jsonrpc: '2.0', + id: 1, + method: 'tools/call', + params: { + name: 'confluence_search', + arguments: parameters + } + }; + + // Write to MCP server stdin + mcpServer.stdin.write(JSON.stringify(request) + '\n'); + + // Read response from stdout + // ... implementation details + }); + ``` + +3. **Deploy to Azure:** + Deploy your Teams bot to Azure App Service or Azure Functions. + +4. **Configure in Teams Admin Center:** + - Go to Teams Admin Center + - Add your bot to the organization + - Enable Copilot integration in the bot settings + +### Option 2: Through Power Platform Connector + +Create a custom connector in Power Platform: + +1. **Wrap MCP Server in REST API:** + Create an ASP.NET Core API that hosts the MCP server and exposes REST endpoints. + +2. **Create Custom Connector:** + - Go to Power Platform Admin Center + - Create a new custom connector + - Define OpenAPI spec for your Confluence operations + - Point to your REST API wrapper + +3. **Enable in Copilot Studio:** + - Open Copilot Studio + - Add your custom connector as a plugin + - Configure authentication + - Enable for M365 Copilot + +### Option 3: Direct Plugin Integration (Future) + +Microsoft is working on direct MCP support for M365 Copilot. When available: + +1. **Register MCP Server:** + ```json + { + "plugins": [ + { + "type": "mcp", + "name": "Confluence", + "command": "dotnet", + "args": ["run", "--project", "/path/to/Dapplo.Confluence.McpServer/..."], + "env": { ... } + } + ] + } + ``` + +2. **Deploy to Organization:** + Upload plugin configuration through M365 Admin Center. + +### Recommended Approach for Production + +For production M365 Copilot integration, we recommend: + +1. **Host MCP Server as Azure Function or Container:** + ```bash + # Create Docker image + docker build -t confluence-mcp-server . + docker run -p 8080:8080 \ + -e CONFLUENCE_ConfluenceUrl="..." \ + -e CONFLUENCE_BearerToken="..." \ + confluence-mcp-server + ``` + +2. **Create REST API Wrapper:** + Build a thin REST API layer that translates HTTP requests to MCP JSON-RPC calls. + +3. **Secure with Azure AD:** + Use Azure Active Directory for authentication between M365 Copilot and your service. + +4. **Monitor and Scale:** + Use Azure Application Insights for monitoring and set up auto-scaling based on demand. + +## Security Considerations + +- **Never commit** API tokens or credentials to source control +- Use environment variables or Azure Key Vault for secrets in production +- Restrict API tokens to minimum required permissions +- Enable audit logging in Confluence to track API usage +- Consider implementing rate limiting for production deployments +- Use HTTPS for all Confluence connections + +## Architecture + +The MCP server follows a scalable, modular design: + +``` +┌─────────────────┐ +│ AI Assistant │ (Claude, M365 Copilot, etc.) +└────────┬────────┘ + │ JSON-RPC over stdio + │ +┌────────▼────────────┐ +│ McpServerHandler │ Processes MCP protocol requests +└────────┬────────────┘ + │ +┌────────▼────────────┐ +│ ToolRegistry │ Manages available Confluence tools +└────────┬────────────┘ + │ +┌────────▼────────────┐ +│ IConfluenceClient │ Dapplo.Confluence library +└────────┬────────────┘ + │ +┌────────▼────────────┐ +│ Confluence API │ (REST API) +└─────────────────────┘ +``` + +## Troubleshooting + +### Server won't start +- Check that `ConfluenceUrl` is configured correctly +- Verify your API token is valid +- Ensure .NET 10.0 is installed + +### Authentication errors +- Verify API token hasn't expired +- Check that you're using the correct authentication method +- For Confluence Cloud, use email address as username with API token as password + +### No results from searches +- Verify CQL syntax (use Confluence's built-in CQL editor to test) +- Check that your user has permissions to view the content +- Try simpler queries first (e.g., just "type=page") + +## Development + +### Project Structure +``` +Dapplo.Confluence.McpServer/ +├── Models/ # MCP protocol models (Request, Response, Error) +├── Tools/ # Tool definitions and implementations +├── McpServerHandler.cs # Main request handler +├── ConfluenceSettings.cs # Configuration model +└── Program.cs # Entry point and stdio handling +``` + +### Adding New Tools + +1. Add tool definition to `ToolRegistry.GetToolDefinitions()` +2. Implement handler in `ToolRegistry.ExecuteToolAsync()` +3. Update this README with tool documentation + +### Testing Manually + +```bash +# Run the server +dotnet run + +# Send test request (in another terminal) +echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' | dotnet run + +# Test search +echo '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"confluence_search","arguments":{"query":"type=page","limit":5}}}' | dotnet run +``` + +## License + +This project follows the same license as Dapplo.Confluence (MIT License). + +## Resources + +- [Model Context Protocol Specification](https://modelcontextprotocol.io/) +- [Confluence REST API Documentation](https://developer.atlassian.com/cloud/confluence/rest/v2/intro/) +- [Dapplo.Confluence Library](https://github.com/dapplo/Dapplo.Confluence) +- [Microsoft Teams AI Library](https://github.com/microsoft/teams-ai) +- [Power Platform Custom Connectors](https://learn.microsoft.com/en-us/connectors/custom-connectors/) diff --git a/src/Dapplo.Confluence.McpServer/Tools/ToolDefinition.cs b/src/Dapplo.Confluence.McpServer/Tools/ToolDefinition.cs new file mode 100644 index 0000000..af57c60 --- /dev/null +++ b/src/Dapplo.Confluence.McpServer/Tools/ToolDefinition.cs @@ -0,0 +1,18 @@ +using System.Text.Json.Serialization; + +namespace Dapplo.Confluence.McpServer.Tools; + +/// +/// Defines an MCP tool +/// +public class ToolDefinition +{ + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + [JsonPropertyName("description")] + public string Description { get; set; } = string.Empty; + + [JsonPropertyName("inputSchema")] + public object InputSchema { get; set; } = new { }; +} diff --git a/src/Dapplo.Confluence.McpServer/Tools/ToolRegistry.cs b/src/Dapplo.Confluence.McpServer/Tools/ToolRegistry.cs new file mode 100644 index 0000000..3f64923 --- /dev/null +++ b/src/Dapplo.Confluence.McpServer/Tools/ToolRegistry.cs @@ -0,0 +1,427 @@ +using Dapplo.Confluence.Query; +using Dapplo.Confluence.Entities; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Reflection; + +namespace Dapplo.Confluence.McpServer.Tools; + +/// +/// Registry of available Confluence tools for MCP +/// +public class ToolRegistry +{ + private readonly IConfluenceClient _confluenceClient; + + public ToolRegistry(IConfluenceClient confluenceClient) + { + _confluenceClient = confluenceClient; + } + + /// + /// Helper to create a Clause from a CQL string using reflection (since Clause is internal) + /// + private IFinalClause CreateClauseFromCql(string cql) + { + // Get the internal Clause type + var clauseType = typeof(IConfluenceClient).Assembly.GetType("Dapplo.Confluence.Query.Clause"); + if (clauseType == null) + throw new InvalidOperationException("Could not find Clause type"); + + // Create instance using the string constructor + var clause = Activator.CreateInstance(clauseType, new object[] { cql }); + return clause as IFinalClause ?? throw new InvalidOperationException("Failed to create Clause"); + } + + /// + /// Get all available tool definitions + /// + public List GetToolDefinitions() + { + return new List + { + new() + { + Name = "confluence_search", + Description = "Search for Confluence content using CQL (Confluence Query Language). Returns pages and blog posts matching the query.", + InputSchema = new + { + type = "object", + properties = new + { + query = new + { + type = "string", + description = "CQL query string (e.g., 'type=page and space=DEV')" + }, + limit = new + { + type = "integer", + description = "Maximum number of results to return (default: 25)", + @default = 25 + } + }, + required = new[] { "query" } + } + }, + new() + { + Name = "confluence_get_page", + Description = "Get a specific Confluence page by ID or title. Returns the page content and metadata.", + InputSchema = new + { + type = "object", + properties = new + { + pageId = new + { + type = "string", + description = "The page ID (use either pageId or title)" + }, + title = new + { + type = "string", + description = "The page title (use either pageId or title)" + }, + spaceKey = new + { + type = "string", + description = "The space key (required when using title)" + }, + expand = new + { + type = "string", + description = "Comma-separated list of properties to expand (e.g., 'body.storage,version,space')", + @default = "body.storage,version" + } + } + } + }, + new() + { + Name = "confluence_create_page", + Description = "Create a new Confluence page in a specific space.", + InputSchema = new + { + type = "object", + properties = new + { + spaceKey = new + { + type = "string", + description = "The space key where the page will be created" + }, + title = new + { + type = "string", + description = "The page title" + }, + content = new + { + type = "string", + description = "The page content in Confluence storage format (HTML)" + }, + parentId = new + { + type = "string", + description = "Optional parent page ID" + } + }, + required = new[] { "spaceKey", "title", "content" } + } + }, + new() + { + Name = "confluence_update_page", + Description = "Update an existing Confluence page.", + InputSchema = new + { + type = "object", + properties = new + { + pageId = new + { + type = "string", + description = "The ID of the page to update" + }, + title = new + { + type = "string", + description = "The new page title" + }, + content = new + { + type = "string", + description = "The new page content in Confluence storage format (HTML)" + }, + version = new + { + type = "integer", + description = "Current version number (required for update)" + } + }, + required = new[] { "pageId", "title", "content", "version" } + } + }, + new() + { + Name = "confluence_list_spaces", + Description = "List all Confluence spaces accessible to the user.", + InputSchema = new + { + type = "object", + properties = new + { + limit = new + { + type = "integer", + description = "Maximum number of spaces to return (default: 25)", + @default = 25 + } + } + } + }, + new() + { + Name = "confluence_get_user_info", + Description = "Get information about the current authenticated user.", + InputSchema = new + { + type = "object", + properties = new { } + } + } + }; + } + + /// + /// Execute a tool with the given parameters + /// + public async Task ExecuteToolAsync(string toolName, JsonElement? parameters) + { + return toolName switch + { + "confluence_search" => await SearchContentAsync(parameters), + "confluence_get_page" => await GetPageAsync(parameters), + "confluence_create_page" => await CreatePageAsync(parameters), + "confluence_update_page" => await UpdatePageAsync(parameters), + "confluence_list_spaces" => await ListSpacesAsync(parameters), + "confluence_get_user_info" => await GetUserInfoAsync(), + _ => throw new InvalidOperationException($"Unknown tool: {toolName}") + }; + } + + private async Task SearchContentAsync(JsonElement? parameters) + { + if (parameters == null) + throw new ArgumentException("Parameters required for search"); + + var query = parameters.Value.GetProperty("query").GetString() + ?? throw new ArgumentException("Query is required"); + var limit = parameters.Value.TryGetProperty("limit", out var limitProp) + ? limitProp.GetInt32() : 25; + + // Create clause from CQL string + var clause = CreateClauseFromCql(query); + var pagingInfo = new PagingInformation { Limit = limit }; + var searchResult = await _confluenceClient.Content.SearchAsync(clause, pagingInformation: pagingInfo); + + return new + { + totalSize = searchResult.Size, + results = searchResult.Results.Select(c => new + { + id = c.Id, + type = c.Type.ToString(), + title = c.Title, + spaceKey = c.Space?.Key, + spaceName = c.Space?.Name, + url = c.Links?.Self, + lastModified = c.Version?.When + }).ToList() + }; + } + + private async Task GetPageAsync(JsonElement? parameters) + { + if (parameters == null) + throw new ArgumentException("Parameters required"); + + var expand = parameters.Value.TryGetProperty("expand", out var expandProp) + ? expandProp.GetString()?.Split(',') : new[] { "body.storage", "version" }; + + Content? page = null; + + if (parameters.Value.TryGetProperty("pageId", out var pageIdProp)) + { + var pageIdStr = pageIdProp.GetString() + ?? throw new ArgumentException("Page ID cannot be null"); + if (!long.TryParse(pageIdStr, out var pageId)) + throw new ArgumentException("Page ID must be a valid number"); + + page = await _confluenceClient.Content.GetAsync(pageId, expand); + } + else if (parameters.Value.TryGetProperty("title", out var titleProp) && + parameters.Value.TryGetProperty("spaceKey", out var spaceKeyProp)) + { + var title = titleProp.GetString() + ?? throw new ArgumentException("Title cannot be null"); + var spaceKey = spaceKeyProp.GetString() + ?? throw new ArgumentException("Space key cannot be null"); + + var clause = Where.And(Where.Type.IsPage, Where.Title.Is(title), Where.Space.Is(spaceKey)); + var pagingInfo = new PagingInformation { Limit = 1 }; + var results = await _confluenceClient.Content.SearchAsync(clause, pagingInformation: pagingInfo); + + if (results.Results.Any()) + { + page = await _confluenceClient.Content.GetAsync(results.Results.First().Id, expand); + } + } + else + { + throw new ArgumentException("Either pageId or (title and spaceKey) must be provided"); + } + + if (page == null) + throw new InvalidOperationException("Page not found"); + + return new + { + id = page.Id, + type = page.Type.ToString(), + title = page.Title, + spaceKey = page.Space?.Key, + spaceName = page.Space?.Name, + content = page.Body?.Storage?.Value, + version = page.Version?.Number, + lastModified = page.Version?.When, + url = page.Links?.Self + }; + } + + private async Task CreatePageAsync(JsonElement? parameters) + { + if (parameters == null) + throw new ArgumentException("Parameters required"); + + var spaceKey = parameters.Value.GetProperty("spaceKey").GetString() + ?? throw new ArgumentException("Space key is required"); + var title = parameters.Value.GetProperty("title").GetString() + ?? throw new ArgumentException("Title is required"); + var content = parameters.Value.GetProperty("content").GetString() + ?? throw new ArgumentException("Content is required"); + + var newContent = new Content + { + Type = ContentTypes.Page, + Title = title, + Space = new Space { Key = spaceKey }, + Body = new Body + { + Storage = new BodyContent + { + Value = content, + Representation = "storage" + } + } + }; + + if (parameters.Value.TryGetProperty("parentId", out var parentIdProp)) + { + var parentIdStr = parentIdProp.GetString(); + if (!string.IsNullOrEmpty(parentIdStr) && long.TryParse(parentIdStr, out var parentId)) + { + newContent.Ancestors = new List { new() { Id = parentId } }; + } + } + + var createdPage = await _confluenceClient.Content.CreateAsync(newContent); + + return new + { + id = createdPage.Id, + title = createdPage.Title, + spaceKey = createdPage.Space?.Key, + url = createdPage.Links?.Self, + version = createdPage.Version?.Number + }; + } + + private async Task UpdatePageAsync(JsonElement? parameters) + { + if (parameters == null) + throw new ArgumentException("Parameters required"); + + var pageIdStr = parameters.Value.GetProperty("pageId").GetString() + ?? throw new ArgumentException("Page ID is required"); + if (!long.TryParse(pageIdStr, out var pageId)) + throw new ArgumentException("Page ID must be a valid number"); + + var title = parameters.Value.GetProperty("title").GetString() + ?? throw new ArgumentException("Title is required"); + var content = parameters.Value.GetProperty("content").GetString() + ?? throw new ArgumentException("Content is required"); + var version = parameters.Value.GetProperty("version").GetInt32(); + + var updateContent = new Content + { + Id = pageId, + Type = ContentTypes.Page, + Title = title, + Version = new Entities.Version { Number = version + 1 }, + Body = new Body + { + Storage = new BodyContent + { + Value = content, + Representation = "storage" + } + } + }; + + var updatedPage = await _confluenceClient.Content.UpdateAsync(updateContent); + + return new + { + id = updatedPage.Id, + title = updatedPage.Title, + version = updatedPage.Version?.Number, + url = updatedPage.Links?.Self + }; + } + + private async Task ListSpacesAsync(JsonElement? parameters) + { + var limit = parameters?.TryGetProperty("limit", out var limitProp) == true + ? limitProp.GetInt32() : 25; + + var pagingInfo = new PagingInformation { Limit = limit }; + var spaces = await _confluenceClient.Space.GetAllWithParametersAsync(pagingInformation: pagingInfo); + + return new + { + totalSize = spaces.Count, + spaces = spaces.Select(s => new + { + key = s.Key, + name = s.Name, + type = s.Type, + url = s.Links?.Self + }).ToList() + }; + } + + private async Task GetUserInfoAsync() + { + var user = await _confluenceClient.User.GetCurrentUserAsync(); + + return new + { + username = user.Username, + displayName = user.DisplayName, + email = user.Email, + profilePicture = user.ProfilePicture?.Path + }; + } +} diff --git a/src/Dapplo.Confluence.McpServer/appsettings.json b/src/Dapplo.Confluence.McpServer/appsettings.json new file mode 100644 index 0000000..980d080 --- /dev/null +++ b/src/Dapplo.Confluence.McpServer/appsettings.json @@ -0,0 +1,7 @@ +{ + "ConfluenceUrl": "", + "AuthType": "bearer", + "BearerToken": "", + "Username": "", + "Password": "" +} diff --git a/src/Dapplo.Confluence.sln b/src/Dapplo.Confluence.sln index a854fb7..ff34bc8 100644 --- a/src/Dapplo.Confluence.sln +++ b/src/Dapplo.Confluence.sln @@ -9,24 +9,66 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapplo.Confluence.OAuth", " EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapplo.Confluence.Tests", "Dapplo.Confluence.Tests\Dapplo.Confluence.Tests.csproj", "{2119C12B-5874-4E60-BD36-45CA1D29FDDC}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapplo.Confluence.McpServer", "Dapplo.Confluence.McpServer\Dapplo.Confluence.McpServer.csproj", "{15573160-C799-4B22-A33C-C370C5F61F21}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {9E5111D5-7572-4643-937E-80D59B62B9F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {9E5111D5-7572-4643-937E-80D59B62B9F7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9E5111D5-7572-4643-937E-80D59B62B9F7}.Debug|x64.ActiveCfg = Debug|Any CPU + {9E5111D5-7572-4643-937E-80D59B62B9F7}.Debug|x64.Build.0 = Debug|Any CPU + {9E5111D5-7572-4643-937E-80D59B62B9F7}.Debug|x86.ActiveCfg = Debug|Any CPU + {9E5111D5-7572-4643-937E-80D59B62B9F7}.Debug|x86.Build.0 = Debug|Any CPU {9E5111D5-7572-4643-937E-80D59B62B9F7}.Release|Any CPU.ActiveCfg = Release|Any CPU {9E5111D5-7572-4643-937E-80D59B62B9F7}.Release|Any CPU.Build.0 = Release|Any CPU - {2119C12B-5874-4E60-BD36-45CA1D29FDDC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2119C12B-5874-4E60-BD36-45CA1D29FDDC}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2119C12B-5874-4E60-BD36-45CA1D29FDDC}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2119C12B-5874-4E60-BD36-45CA1D29FDDC}.Release|Any CPU.Build.0 = Release|Any CPU + {9E5111D5-7572-4643-937E-80D59B62B9F7}.Release|x64.ActiveCfg = Release|Any CPU + {9E5111D5-7572-4643-937E-80D59B62B9F7}.Release|x64.Build.0 = Release|Any CPU + {9E5111D5-7572-4643-937E-80D59B62B9F7}.Release|x86.ActiveCfg = Release|Any CPU + {9E5111D5-7572-4643-937E-80D59B62B9F7}.Release|x86.Build.0 = Release|Any CPU {02980BD0-F615-498C-8478-7E033394005F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {02980BD0-F615-498C-8478-7E033394005F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {02980BD0-F615-498C-8478-7E033394005F}.Debug|x64.ActiveCfg = Debug|Any CPU + {02980BD0-F615-498C-8478-7E033394005F}.Debug|x64.Build.0 = Debug|Any CPU + {02980BD0-F615-498C-8478-7E033394005F}.Debug|x86.ActiveCfg = Debug|Any CPU + {02980BD0-F615-498C-8478-7E033394005F}.Debug|x86.Build.0 = Debug|Any CPU {02980BD0-F615-498C-8478-7E033394005F}.Release|Any CPU.ActiveCfg = Release|Any CPU {02980BD0-F615-498C-8478-7E033394005F}.Release|Any CPU.Build.0 = Release|Any CPU + {02980BD0-F615-498C-8478-7E033394005F}.Release|x64.ActiveCfg = Release|Any CPU + {02980BD0-F615-498C-8478-7E033394005F}.Release|x64.Build.0 = Release|Any CPU + {02980BD0-F615-498C-8478-7E033394005F}.Release|x86.ActiveCfg = Release|Any CPU + {02980BD0-F615-498C-8478-7E033394005F}.Release|x86.Build.0 = Release|Any CPU + {2119C12B-5874-4E60-BD36-45CA1D29FDDC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2119C12B-5874-4E60-BD36-45CA1D29FDDC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2119C12B-5874-4E60-BD36-45CA1D29FDDC}.Debug|x64.ActiveCfg = Debug|Any CPU + {2119C12B-5874-4E60-BD36-45CA1D29FDDC}.Debug|x64.Build.0 = Debug|Any CPU + {2119C12B-5874-4E60-BD36-45CA1D29FDDC}.Debug|x86.ActiveCfg = Debug|Any CPU + {2119C12B-5874-4E60-BD36-45CA1D29FDDC}.Debug|x86.Build.0 = Debug|Any CPU + {2119C12B-5874-4E60-BD36-45CA1D29FDDC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2119C12B-5874-4E60-BD36-45CA1D29FDDC}.Release|Any CPU.Build.0 = Release|Any CPU + {2119C12B-5874-4E60-BD36-45CA1D29FDDC}.Release|x64.ActiveCfg = Release|Any CPU + {2119C12B-5874-4E60-BD36-45CA1D29FDDC}.Release|x64.Build.0 = Release|Any CPU + {2119C12B-5874-4E60-BD36-45CA1D29FDDC}.Release|x86.ActiveCfg = Release|Any CPU + {2119C12B-5874-4E60-BD36-45CA1D29FDDC}.Release|x86.Build.0 = Release|Any CPU + {15573160-C799-4B22-A33C-C370C5F61F21}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {15573160-C799-4B22-A33C-C370C5F61F21}.Debug|Any CPU.Build.0 = Debug|Any CPU + {15573160-C799-4B22-A33C-C370C5F61F21}.Debug|x64.ActiveCfg = Debug|Any CPU + {15573160-C799-4B22-A33C-C370C5F61F21}.Debug|x64.Build.0 = Debug|Any CPU + {15573160-C799-4B22-A33C-C370C5F61F21}.Debug|x86.ActiveCfg = Debug|Any CPU + {15573160-C799-4B22-A33C-C370C5F61F21}.Debug|x86.Build.0 = Debug|Any CPU + {15573160-C799-4B22-A33C-C370C5F61F21}.Release|Any CPU.ActiveCfg = Release|Any CPU + {15573160-C799-4B22-A33C-C370C5F61F21}.Release|Any CPU.Build.0 = Release|Any CPU + {15573160-C799-4B22-A33C-C370C5F61F21}.Release|x64.ActiveCfg = Release|Any CPU + {15573160-C799-4B22-A33C-C370C5F61F21}.Release|x64.Build.0 = Release|Any CPU + {15573160-C799-4B22-A33C-C370C5F61F21}.Release|x86.ActiveCfg = Release|Any CPU + {15573160-C799-4B22-A33C-C370C5F61F21}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 6916c7b6b5f00deb8193fd6819d6bba856da4644 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 29 Jan 2026 14:19:38 +0000 Subject: [PATCH 3/4] Add security improvements and example configuration Co-authored-by: Lakritzator <708125+Lakritzator@users.noreply.github.com> --- .gitignore | 5 ++++- src/Dapplo.Confluence.McpServer/README.md | 3 ++- src/Dapplo.Confluence.McpServer/appsettings.example.json | 7 +++++++ src/Dapplo.Confluence.McpServer/appsettings.json | 4 +--- 4 files changed, 14 insertions(+), 5 deletions(-) create mode 100644 src/Dapplo.Confluence.McpServer/appsettings.example.json diff --git a/.gitignore b/.gitignore index 0934e18..658aac9 100644 --- a/.gitignore +++ b/.gitignore @@ -225,4 +225,7 @@ doc/api/.manifest # Coverage CoverageReport coverage.xml -xunit.xml \ No newline at end of file +xunit.xml + +# MCP Server configuration with secrets +src/Dapplo.Confluence.McpServer/appsettings.json \ No newline at end of file diff --git a/src/Dapplo.Confluence.McpServer/README.md b/src/Dapplo.Confluence.McpServer/README.md index 0930436..8722d96 100644 --- a/src/Dapplo.Confluence.McpServer/README.md +++ b/src/Dapplo.Confluence.McpServer/README.md @@ -51,7 +51,8 @@ The server supports two authentication methods: ### Option 1: Bearer Token (Recommended) -Configure via `appsettings.json`: +1. Copy `appsettings.example.json` to `appsettings.json` +2. Configure via `appsettings.json`: ```json { diff --git a/src/Dapplo.Confluence.McpServer/appsettings.example.json b/src/Dapplo.Confluence.McpServer/appsettings.example.json new file mode 100644 index 0000000..beced29 --- /dev/null +++ b/src/Dapplo.Confluence.McpServer/appsettings.example.json @@ -0,0 +1,7 @@ +{ + "ConfluenceUrl": "https://yourcompany.atlassian.net", + "AuthType": "bearer", + "BearerToken": "YOUR_API_TOKEN_HERE", + "Username": "", + "Password": "" +} diff --git a/src/Dapplo.Confluence.McpServer/appsettings.json b/src/Dapplo.Confluence.McpServer/appsettings.json index 980d080..e40c2a7 100644 --- a/src/Dapplo.Confluence.McpServer/appsettings.json +++ b/src/Dapplo.Confluence.McpServer/appsettings.json @@ -1,7 +1,5 @@ { "ConfluenceUrl": "", "AuthType": "bearer", - "BearerToken": "", - "Username": "", - "Password": "" + "BearerToken": "" } From 4de870a50bc3b1a789e08b474e33a45268768803 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 29 Jan 2026 14:20:43 +0000 Subject: [PATCH 4/4] Add quick start guide and update main README Co-authored-by: Lakritzator <708125+Lakritzator@users.noreply.github.com> --- README.md | 18 +++ src/Dapplo.Confluence.McpServer/QUICKSTART.md | 113 ++++++++++++++++++ 2 files changed, 131 insertions(+) create mode 100644 src/Dapplo.Confluence.McpServer/QUICKSTART.md diff --git a/README.md b/README.md index d594709..54c6c23 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,24 @@ This is a simple REST based Confluence client, written for Greenshot, by using D - Coverage Status: [![Coverage Status](https://coveralls.io/repos/github/dapplo/Dapplo.Confluence/badge.svg?branch=master)](https://coveralls.io/github/dapplo/Dapplo.Confluence?branch=master) - NuGet package: [![NuGet package](https://badge.fury.io/nu/Dapplo.Confluence.svg)](https://badge.fury.io/nu/Dapplo.Confluence) +## 🆕 MCP Server for AI Integration + +**NEW**: Integrate Confluence with AI assistants like Claude Desktop and Microsoft 365 Copilot! + +The [Dapplo.Confluence.McpServer](src/Dapplo.Confluence.McpServer) provides a Model Context Protocol (MCP) server that exposes Confluence functionality to AI assistants. + +**Quick start:** +```bash +cd src/Dapplo.Confluence.McpServer +cp appsettings.example.json appsettings.json +# Edit appsettings.json with your Confluence URL and API token +dotnet run +``` + +See the [Quick Start Guide](src/Dapplo.Confluence.McpServer/QUICKSTART.md) and [full documentation](src/Dapplo.Confluence.McpServer/README.md) for details. + +## Confluence Client Library + The Confluence client supports most REST methods, and has a fluent API for building a CQL (Confluence Query Language) string to search with. An example on how to use this Confluence client: diff --git a/src/Dapplo.Confluence.McpServer/QUICKSTART.md b/src/Dapplo.Confluence.McpServer/QUICKSTART.md new file mode 100644 index 0000000..610d207 --- /dev/null +++ b/src/Dapplo.Confluence.McpServer/QUICKSTART.md @@ -0,0 +1,113 @@ +# Quick Start Guide - Confluence MCP Server + +Get started with the Confluence MCP Server in 3 simple steps! + +## Step 1: Get Your Confluence API Token + +1. Go to [https://id.atlassian.com/manage-profile/security/api-tokens](https://id.atlassian.com/manage-profile/security/api-tokens) +2. Click **Create API token** +3. Give it a name like "MCP Server" +4. Copy the token (you won't be able to see it again!) + +## Step 2: Configure the Server + +1. Navigate to the MCP Server directory: + ```bash + cd src/Dapplo.Confluence.McpServer + ``` + +2. Copy the example configuration: + ```bash + cp appsettings.example.json appsettings.json + ``` + +3. Edit `appsettings.json` and add your details: + ```json + { + "ConfluenceUrl": "https://yourcompany.atlassian.net", + "AuthType": "bearer", + "BearerToken": "YOUR_API_TOKEN_HERE" + } + ``` + +## Step 3: Run the Server + +### Test it directly: +```bash +dotnet run +``` + +The server will start and wait for MCP requests on stdin. You should see: +``` +Confluence MCP Server started +Server URL: https://yourcompany.atlassian.net +Auth Type: bearer +Waiting for MCP requests on stdin... +``` + +### Test with a sample request: +```bash +echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' | dotnet run +``` + +You should get a JSON response with server information and capabilities. + +## Using with Claude Desktop + +Add this to your Claude Desktop config file: + +**macOS/Linux:** `~/Library/Application Support/Claude/claude_desktop_config.json` +**Windows:** `%APPDATA%\Claude\claude_desktop_config.json` + +```json +{ + "mcpServers": { + "confluence": { + "command": "dotnet", + "args": [ + "run", + "--project", + "/full/path/to/Dapplo.Confluence.McpServer/Dapplo.Confluence.McpServer.csproj" + ], + "env": { + "CONFLUENCE_ConfluenceUrl": "https://yourcompany.atlassian.net", + "CONFLUENCE_AuthType": "bearer", + "CONFLUENCE_BearerToken": "your-api-token" + } + } + } +} +``` + +Restart Claude Desktop, and you'll now have access to Confluence tools! + +## What Can You Do? + +Ask Claude to: +- "Search for pages about API documentation in our Confluence" +- "Show me the content of the page titled 'Team Guidelines'" +- "Create a new page in the DEV space with a summary of our discussion" +- "List all our Confluence spaces" +- "Update the Release Notes page with new information" + +## Troubleshooting + +### "ConfluenceUrl is not configured" +Make sure your `appsettings.json` file exists and has the correct URL. + +### "Authentication errors" +- Verify your API token is correct +- For Confluence Cloud, use your email as username if using basic auth +- Make sure the token hasn't expired + +### "Page not found" +- Check that your user has permission to view/edit the content +- Verify the space key and page title are correct + +## Next Steps + +- Read the [full README](README.md) for detailed documentation +- Learn about [M365 Copilot integration](README.md#integration-with-microsoft-365-copilot) +- Understand [security best practices](README.md#security-considerations) + +Need help? Check the main README or open an issue on GitHub!