# Building a Confluence MCP Server for Claude Code: From Setup to Skills

> Source: <https://dev.to/tariqulislam/-building-a-confluence-mcp-server-for-claude-code-from-setup-to-skills-k95>
> Published: 2026-06-16 14:32:23+00:00

Automate your Confluence workflow directly from Claude Code — publish pages, sync content, and upload images without leaving your terminal.

**GitHub:** [https://github.com/tariqulislam/confluence-mcp-server](https://github.com/tariqulislam/confluence-mcp-server)

**MCP** stands for **Model Context Protocol** — an open standard that lets AI assistants like Claude Code connect to external tools and services. An MCP server acts as a bridge: it exposes a set of *tools* that Claude Code can call, allowing it to interact with third-party systems (like Confluence) on your behalf.

In this guide you will learn:

```
confluence-mcp-mock/
├── .env                     ← your credentials (never committed)
├── .env.example             ← template
├── .mcp.json                ← registers MCP servers with Claude Code
├── confluence-mcp/
│   ├── src/
│   │   ├── index.ts         ← entry point
│   │   ├── server.ts        ← tool registration
│   │   ├── config.ts        ← env validation (Zod)
│   │   ├── confluence/
│   │   │   ├── client.ts    ← Axios HTTP client
│   │   │   ├── api.ts       ← REST API calls
│   │   │   ├── parser.ts    ← XHTML to structured data
│   │   │   └── markdown-converter.ts
│   │   └── tools/           ← one file per MCP tool
│   │       ├── connection-check.ts
│   │       ├── list-pages.ts
│   │       ├── publish-page.ts
│   │       ├── read-save-page.ts
│   │       ├── upload-images.ts
│   │       └── sync-pages.ts
│   └── build/               ← compiled output
└── .claude/skills/          ← slash command definitions
    ├── confluence-conn-check/
    ├── confluence-page-list/
    ├── publish-to-confluence/
    ├── confluence-upload-images/
    └── confluence-sync-pages/
```

The server reads all credentials from a single `.env`

file at the project root. Copy the example template:

```
cp .env.example .env
```

Then fill in the four Confluence variables:

```
# .env  (never commit this file)
CONFLUENCE_URL=https://your-confluence-instance.com/confluence/
CONFLUENCE_USER=your.email@company.com
CONFLUENCE_PERSONAL_ACCESS_TOKEN=your_token_here
CONFLUENCE_SPACE_KEY=MYSPACE
```

The four variables you need:

**CONFLUENCE_URL** — Base URL of your Confluence instance. Must end with a trailing slash.

Example: `https://confluence.company.com/confluence/`

**CONFLUENCE_USER** — The email address tied to your Confluence account.

**CONFLUENCE_PERSONAL_ACCESS_TOKEN** — A Bearer token generated in Confluence settings.

**CONFLUENCE_SPACE_KEY** — The space key where new pages will be created. Example: `ENGINEERING`

When the server starts, `config.ts`

reads the `.env`

file and validates every variable using **Zod** — a TypeScript schema library. If any variable is missing or malformed, the server exits with a clear error instead of failing silently later.

``` python
// confluence-mcp/src/config.ts  (simplified)
import dotenv from 'dotenv';
import { z } from 'zod';

dotenv.config({ path: join(__dirname, '..', '..', '.env') });

const envSchema = z.object({
  CONFLUENCE_URL: z.string().url(),
  CONFLUENCE_USER: z.string().email(),
  CONFLUENCE_PERSONAL_ACCESS_TOKEN: z.string().min(1),
  CONFLUENCE_SPACE_KEY: z.string().min(1),
});

export const config = envSchema.parse(process.env);
```

The validated config is imported by `client.ts`

, which creates a single Axios instance used by every tool:

``` js
// confluence-mcp/src/confluence/client.ts  (simplified)
const client = axios.create({
  baseURL: config.CONFLUENCE_URL,
  headers: {
    Authorization: `Bearer ${config.CONFLUENCE_PERSONAL_ACCESS_TOKEN}`,
    Accept: 'application/json',
    'Content-Type': 'application/json',
  },
  timeout: 30000,
});
```

Credentials are set once here and never repeated anywhere else in the codebase.

Claude Code discovers MCP servers through the `.mcp.json`

file in the project root:

```
{
  "mcpServers": {
    "confluence": {
      "command": "node",
      "args": ["./confluence-mcp/build/index.js"]
    }
  }
}
```

This tells Claude Code: *"When this project is open, launch the node process at that path and communicate with it over stdio."*

```
# Install dependencies
cd confluence-mcp && npm install

# Compile TypeScript to JavaScript
npm run build

# Restart Claude Code to load the new server
```

During development, use watch mode so the server recompiles on every save:

```
npm run watch
```

The server follows a clean three-layer pattern:

```
Claude Code
    │  (MCP protocol over stdio)
    ▼
server.ts         ← registers tools, routes calls
    │
    ▼
tools/*.ts        ← one file per tool, handles input/output
    │
    ▼
confluence/api.ts ← REST API calls (Confluence v1 endpoints)
```

`server.ts`

declares every tool's name, description, and input schema in the `ListToolsRequestSchema`

handler. Claude Code reads this to understand what tools exist. Calls are then routed in the `CallToolRequestSchema`

handler:

```
// server.ts  (simplified)
server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [
    {
      name: 'confluence_connection_check',
      description: 'Verify connection to Confluence and check authentication.',
      inputSchema: { type: 'object', properties: {}, required: [] },
    },
    {
      name: 'confluence_list_pages',
      description: 'List all child pages under a parent page.',
      inputSchema: {
        type: 'object',
        properties: {
          parent_page_id: { type: 'string', description: 'ID of the parent page' },
        },
        required: ['parent_page_id'],
      },
    },
  ],
}));

server.setRequestHandler(CallToolRequestSchema, async (request) => {
  switch (request.params.name) {
    case 'confluence_connection_check':
      return { content: [{ type: 'text', text: await handleConnectionCheck() }] };
    case 'confluence_list_pages':
      return { content: [{ type: 'text', text: await handleListPages(args) }] };
  }
});
```

**confluence_connection_check** — Verifies auth and returns server/user info

**confluence_list_pages** — Lists all child pages under a parent

**confluence_read_save_page** — Downloads a page (sections, tables, images, comments) to local JSON

**confluence_publish_page** — Converts markdown to Confluence XHTML and creates/updates a page

**confluence_upload_images** — Bulk-uploads images from a local directory as page attachments

**confluence_sync_pages** — Syncs all child pages to local markdown with change tracking

Skills are the user-friendly layer on top of raw MCP tools. A skill is a directory under `.claude/skills/`

containing a `SKILL.md`

file.

```
.claude/skills/confluence-conn-check/
└── SKILL.md
---
name: confluence-conn-check
description: Verify Confluence server connection and authentication.
argument-hint: (no arguments required)
---

# Confluence Connection Check

Test the connection to Confluence and verify your credentials.

## Usage

/confluence-conn-check

## MCP Tool

confluence_connection_check
```

The three frontmatter fields that matter:

**name** — becomes the slash command (e.g. `/confluence-conn-check`

)

**description** — shown in the `/`

autocomplete menu and used for auto-discovery by Claude

**argument-hint** — hint shown in autocomplete, e.g. `<parent_page_id>`

or `(no arguments required)`

When you type `/confluence-conn-check`

in Claude Code, it reads `SKILL.md`

, knows which MCP tool to call, and formats the response back to you.

Verify your credentials before doing anything else.

```
/confluence-conn-check
```

Expected response:

```
{
  "status": "success",
  "user": {
    "displayName": "Jane Doe",
    "email": "jane.doe@company.com"
  },
  "server": {
    "version": "8.5.0",
    "baseUrl": "https://confluence.company.com/confluence"
  }
}
```

List all child pages under a parent so you can find page IDs.

```
/confluence-page-list 123456789
```

Response:

```
{
  "status": "success",
  "message": "Found 3 child page(s)",
  "pages": [
    { "id": "987654321", "title": "Getting Started" },
    { "id": "987654322", "title": "API Reference" }
  ]
}
```

Download a full page — sections, tables, images, and comments — and save it locally as structured JSON.

```
/confluence-page-read-save-content 987654321
```

Saves to:

```
confluence-docs/con_<parentId>_<pageId>/
├── sections_and_sub_sections_contents.json
├── comments_contents.json
└── images/
    ├── screenshot.png
    └── diagram.jpg
```

Convert a markdown file to Confluence storage format and publish it. Images referenced in the markdown are automatically uploaded as attachments.

Create a new page:

```
/publish-to-confluence 123456789 docs/architecture.md
```

Update an existing page:

```
/publish-to-confluence 123456789 docs/architecture.md 987654321
```

The markdown converter handles headings, bold/italic, fenced code blocks with syntax highlighting, ordered and unordered lists (nested), pipe tables, local and external images, blockquotes, and horizontal rules.

Bulk-upload images from a local directory with configurable rate-limit control.

```
# Default (3 parallel, 2 s delay between batches)
/confluence-upload-images 987654321 ./screenshots

# Conservative — slow instance or large files
/confluence-upload-images 987654321 ./screenshots 1 3000
```

Rate-limit tuning guide:

`batch_size=1, delay_ms=3000`

— safest, use on aggressive instances`batch_size=3, delay_ms=2000`

— default, works for most instances`batch_size=5, delay_ms=1000`

— faster, for lightly loaded on-premise serversDownload all child pages under a parent as local markdown files. On repeat runs, only pages that actually changed (version bump or new attachment) are re-downloaded.

```
/confluence-sync-pages 6656101928
```

Output structure:

```
confluence_doc_md/
└── parent_6656101928/
    ├── manifest.json      ← change-tracking index
    ├── page_987654321/
    │   ├── page.md
    │   ├── comments.json
    │   └── images/
    └── page_987654322/
        └── page.md
```

Each `page.md`

starts with a metadata header:

```
# Page Title

> Page ID: 987654321
> Version: 5
> Last modified: 2026-05-20T10:30:00.000Z

[page body]

---

## Comments

### Comment by jane.doe (2026-05-19T09:00:00.000Z)

Comment text here.
```

Change detection logic:

```
# 1. Verify credentials
/confluence-conn-check

# 2. Find the parent page ID
/confluence-page-list 100000001

# 3. Write docs locally in your editor
# (create docs/my-feature.md)

# 4. Publish
/publish-to-confluence 100000002 docs/my-feature.md

# 5. Update after edits
/publish-to-confluence 100000002 docs/my-feature.md 987654321

# 6. Back up all pages under a parent
/confluence-sync-pages 100000001
```

To extend the server with a custom tool, follow four steps.

**1. Create the tool file** at `confluence-mcp/src/tools/your-tool.ts`

:

``` js
import { z } from 'zod';

export const yourToolSchema = z.object({
  page_id: z.string().describe('The Confluence page ID'),
});

export async function handleYourTool(input: z.infer<typeof yourToolSchema>) {
  return JSON.stringify({ status: 'success', data: {} }, null, 2);
}
```

**2. Register in server.ts** — add to the tool list and the switch statement:

``` js
import { handleYourTool } from './tools/your-tool.js';

// in ListToolsRequestSchema handler:
{ name: 'confluence_your_tool', description: '...', inputSchema: { ... } }

// in CallToolRequestSchema handler:
case 'confluence_your_tool':
  return { content: [{ type: 'text', text: await handleYourTool(args) }] };
```

**3. Create the skill** at `.claude/skills/your-tool/SKILL.md`

following the format in Step 6.

**4. Rebuild and restart:**

```
cd confluence-mcp && npm run build
# Restart Claude Code
```

**MCP server not loading**

Run `npm run build`

inside `confluence-mcp/`

and check for TypeScript errors. Verify `.mcp.json`

points to `./confluence-mcp/build/index.js`

. Fully restart Claude Code — the server process is launched at startup.

**401 Authentication Error**

Regenerate your Personal Access Token in Confluence settings. Confirm `CONFLUENCE_USER`

is the email associated with the token.

**URL errors**

`CONFLUENCE_URL`

must end with a trailing slash. If behind a VPN, connect before running.

**Publishing formatting issues**

Tables need a header separator row (`|---|---|`

). Avoid emoji and raw angle bracket characters (`<`

, `>`

) directly inside table cells — Confluence's XHTML parser rejects them.

**Rate limit exceeded during image upload**

Slow down: `/confluence-upload-images <page_id> <dir> 1 4000`

The full stack in one view:

`.env`

— credentials, never committed`config.ts`

— Zod validates env at startup`client.ts`

— single Axios instance with Bearer auth`api.ts`

— Confluence REST v1 endpoint calls`tools/*.ts`

— one handler per MCP tool`server.ts`

— declares tools to Claude Code via MCP protocol`.mcp.json`

— tells Claude Code where to find the server`.claude/skills/*/SKILL.md`

— slash commands that wrap MCP toolsWith this architecture you can publish markdown documentation, sync entire Confluence spaces, and upload images — all from a single Claude Code session.

**Source code:** [github.com/tariqulislam/confluence-mcp-server](https://github.com/tariqulislam/confluence-mcp-server)
