Skip to content

03. 技術仕様

niro-mcp-servers/
├── packages/
│ ├── shared/
│ │ └── confluence-cleaner/ # 共有ロジック
│ │ ├── src/
│ │ │ ├── cleaner.ts # コアロジック
│ │ │ ├── html-parser.ts
│ │ │ ├── macro-expander.ts
│ │ │ ├── markdown-converter.ts
│ │ │ └── types.ts
│ │ ├── package.json
│ │ └── README.md
│ │
│ └── confluence-md/ # MCP サーバー
│ ├── src/
│ │ └── index.ts
│ ├── package.json
│ └── README.md
├── package.json
├── bunfig.toml
└── tsconfig.json

1. @niro/shared-confluence-cleaner(共有ロジック)

Section titled “1. @niro/shared-confluence-cleaner(共有ロジック)”

コアのクリーニングロジックを提供。両方の MCP サーバーから利用可能。

export class ConfluenceCleaner {
clean(html: string, options?: CleanOptions): CleanedContent {
// HTMLパース → マクロ展開 → Markdown変換
}
}

2. @niro/mcp-confluence-md(MCP サーバー)

Section titled “2. @niro/mcp-confluence-md(MCP サーバー)”

Confluence クリーニング専用 MCP サーバー。

提供ツール:

  • clean_confluence_html
  • clean_confluence_page
  • batch_clean_pages
graph TB
    A[Input: HTML] --> B[1. HTML Parser
JSDOM] B --> C[2. Remove Unwanted
script, style, icons] C --> D[3. Expand Macros
info, warning, code] D --> E[4. Convert to Markdown
Turndown] E --> F[5. Post Process
trim, limit] F --> G[Output: Clean Markdown]

使用ライブラリ: JSDOM

const dom = new JSDOM(html);
const document = dom.window.document;

削除対象のセレクタ:

const selectorsToRemove = [
'script', // スクリプト
'style', // スタイル
'.confluence-information-macro-icon', // アイコン画像
'.expand-control', // 展開コントロール
'.ia-button', // ボタン
'ac\\:structured-macro[ac\\:name="toc"]', // 目次マクロ
];

入力:

<ac:structured-macro ac:name="info">
<ac:rich-text-body>
<p>重要な情報です</p>
</ac:rich-text-body>
</ac:structured-macro>

出力:

> ℹ️ Info: 重要な情報です

入力:

<ac:structured-macro ac:name="code" ac:language="typescript">
<ac:plain-text-body>
function hello() {
console.log("Hello");
}
</ac:plain-text-body>
</ac:structured-macro>

出力:

```typescript
function hello() {
console.log("Hello");
}
```

使用ライブラリ: Turndown

this.turndownService = new TurndownService({
headingStyle: 'atx', // # 見出し形式
codeBlockStyle: 'fenced', // ``` コードブロック
bulletListMarker: '-', // - リスト
});
  • 余分な空行削除: \n{3,}\n\n
  • 前後の空白削除: trim()
  • 文字数制限: substring(0, maxLength)
import { JSDOM } from "jsdom";
import TurndownService from "turndown";
export interface CleanOptions {
format?: 'markdown' | 'plaintext';
removeUnwanted?: boolean;
expandMacros?: boolean;
maxLength?: number;
}
export interface CleanedContent {
markdown: string;
plaintext: string;
metadata: {
wordCount: number;
processedAt: string;
macrosExpanded: number;
};
}
export class ConfluenceCleaner {
private turndownService: TurndownService;
constructor() {
this.turndownService = new TurndownService({
headingStyle: 'atx',
codeBlockStyle: 'fenced',
bulletListMarker: '-',
});
this.setupCustomRules();
}
clean(html: string, options: CleanOptions = {}): CleanedContent {
const dom = new JSDOM(html);
const document = dom.window.document;
// 1. 不要要素削除
if (options.removeUnwanted !== false) {
this.removeUnwantedElements(document);
}
// 2. マクロ展開
let macrosExpanded = 0;
if (options.expandMacros !== false) {
macrosExpanded = this.expandConfluenceMacros(document);
}
// 3. Markdown変換
let markdown = this.turndownService.turndown(document.body.innerHTML);
// 4. プレーンテキスト生成
let plaintext = document.body.textContent || '';
plaintext = plaintext.replace(/\s+/g, ' ').trim();
// 5. 長さ制限
if (options.maxLength) {
markdown = markdown.substring(0, options.maxLength);
plaintext = plaintext.substring(0, options.maxLength);
}
// 6. 後処理
markdown = this.postProcessMarkdown(markdown);
return {
markdown,
plaintext,
metadata: {
wordCount: plaintext.split(/\s+/).length,
processedAt: new Date().toISOString(),
macrosExpanded,
},
};
}
private removeUnwantedElements(document: Document): void {
const selectors = [
'script',
'style',
'.confluence-information-macro-icon',
'.expand-control',
'ac\\:structured-macro[ac\\:name="toc"]',
];
selectors.forEach(selector => {
document.querySelectorAll(selector).forEach(el => el.remove());
});
}
private expandConfluenceMacros(document: Document): number {
let count = 0;
// infoマクロ
document.querySelectorAll('ac\\:structured-macro[ac\\:name="info"]')
.forEach(macro => {
const body = macro.querySelector('ac\\:rich-text-body');
if (body) {
const blockquote = document.createElement('blockquote');
blockquote.innerHTML = `<strong>ℹ️ Info:</strong> ${body.innerHTML}`;
macro.replaceWith(blockquote);
count++;
}
});
// 他のマクロも同様に実装...
return count;
}
private postProcessMarkdown(markdown: string): string {
return markdown
.replace(/\n{3,}/g, '\n\n')
.trim();
}
private setupCustomRules(): void {
// カスタムルール追加
}
}
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { ConfluenceCleaner } from "@niro/shared-confluence-cleaner";
const server = new Server(
{
name: "confluence-md",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
}
);
const cleaner = new ConfluenceCleaner();
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: "clean_confluence_html",
description: "ConfluenceのHTMLをクリーンなMarkdown/テキストに変換",
inputSchema: {
type: "object",
properties: {
html: { type: "string", description: "変換対象のHTML" },
format: { type: "string", enum: ["markdown", "plaintext"] },
remove_unwanted: { type: "boolean" },
expand_macros: { type: "boolean" },
max_length: { type: "number" },
},
required: ["html"],
},
},
// 他のツール定義...
],
}));
server.setRequestHandler(CallToolRequestSchema, async (request) => {
switch (request.params.name) {
case "clean_confluence_html": {
const result = cleaner.clean(request.params.arguments.html, {
format: request.params.arguments.format || 'markdown',
removeUnwanted: request.params.arguments.remove_unwanted,
expandMacros: request.params.arguments.expand_macros,
maxLength: request.params.arguments.max_length,
});
return {
content: [{
type: "text",
text: JSON.stringify(result, null, 2)
}]
};
}
// 他のツール実装...
}
});
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
}
main().catch(console.error);
{
"name": "@niro/shared-confluence-cleaner",
"version": "1.0.0",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "bun build src/index.ts --outdir dist --target node",
"test": "bun test"
},
"dependencies": {
"jsdom": "^24.0.0",
"turndown": "^7.1.2"
},
"devDependencies": {
"@types/jsdom": "^21.1.6",
"@types/turndown": "^5.0.4",
"typescript": "^5.6.2"
}
}
{
"name": "@niro/mcp-confluence-md",
"version": "1.0.0",
"type": "module",
"bin": {
"mcp-confluence-md": "dist/index.js"
},
"scripts": {
"build": "tsc && chmod +x dist/*.js",
"prepare": "bun run build"
},
"dependencies": {
"@modelcontextprotocol/sdk": "1.0.1",
"@niro/shared-confluence-cleaner": "workspace:*"
}
}

Phase 1: 共有ロジック実装(2-3日)

Section titled “Phase 1: 共有ロジック実装(2-3日)”

Day 1-2: コアロジック

  • ConfluenceCleaner クラス
  • HTML → Markdown 変換
  • 基本的なマクロ対応(info, warning, code)
  • ユニットテスト

Day 3: 完成度向上

  • すべてのマクロ対応
  • エッジケース対応
  • ドキュメント作成

Day 4: MCP サーバー構築

  • 3つのツール実装
    • clean_confluence_html
    • clean_confluence_page
    • batch_clean_pages

Day 5: 仕上げ

  • 統合テスト
  • README作成
  • 使用例の追加
describe('ConfluenceCleaner', () => {
it('should convert info macro to blockquote', () => {
const html = `
<ac:structured-macro ac:name="info">
<ac:rich-text-body><p>Test</p></ac:rich-text-body>
</ac:structured-macro>
`;
const result = cleaner.clean(html);
expect(result.markdown).toContain('> ℹ️ Info: Test');
});
// 他のテストケース...
});
  • 実際の Confluence ページを使ったテスト
  • 複雑なマクロのネストをテスト
  • 大量ページでのパフォーマンステスト

セキュリティ制約とローカル実行

Section titled “セキュリティ制約とローカル実行”

このプロジェクトはセキュリティ要件により、ローカル環境(Docker コンテナ内)での実行を前提としています。

  • Confluence への接続は社内ネットワーク内のみ
  • 外部サービスへのデータ送信は禁止
  • Docker コンテナ内で完結する構成
# Dockerfile
FROM oven/bun:1.1-alpine
WORKDIR /app
# 依存関係のインストール
COPY package.json bun.lockb ./
COPY packages/shared/confluence-cleaner/package.json ./packages/shared/confluence-cleaner/
COPY packages/confluence-md/package.json ./packages/confluence-md/
RUN bun install --frozen-lockfile
# ソースコードのコピー
COPY . .
# ビルド
RUN bun run build
# MCP サーバーのエントリーポイント
WORKDIR /app/packages/confluence-md
CMD ["bun", "run", "dist/index.js"]
docker-compose.yml
version: '3.8'
services:
confluence-md:
build:
context: .
dockerfile: Dockerfile
container_name: confluence-md
volumes:
# ソースコードのホットリロード(開発時)
- ./packages:/app/packages
# ビルド成果物の永続化
- confluence-md-dist:/app/packages/confluence-md/dist
environment:
- NODE_ENV=production
# MCP サーバーは stdio を使用するため、ポート公開不要
stdin_open: true
tty: true
networks:
- confluence-network
networks:
confluence-network:
driver: bridge
volumes:
confluence-md-dist:
docker-compose.dev.yml
version: '3.8'
services:
confluence-md-dev:
build:
context: .
dockerfile: Dockerfile.dev
container_name: confluence-md-dev
volumes:
# 開発時はソースコード全体をマウント
- .:/app
- /app/node_modules
- /app/packages/shared/confluence-cleaner/node_modules
- /app/packages/confluence-md/node_modules
environment:
- NODE_ENV=development
stdin_open: true
tty: true
command: bun run dev
networks:
- confluence-network
networks:
confluence-network:
driver: bridge
Dockerfile.dev
FROM oven/bun:1.1-alpine
# 開発ツールのインストール
RUN apk add --no-cache git
WORKDIR /app
# 依存関係のインストール
COPY package.json bun.lockb ./
RUN bun install
# 開発サーバー起動
CMD ["bun", "run", "dev"]
Terminal window
# モノレポルートで
bun install
# すべてのパッケージをビルド
bun run build
# MCP サーバーを起動
bun run --filter @niro/mcp-confluence-md start
Terminal window
# 開発環境のビルドと起動
docker-compose -f docker-compose.dev.yml up -d
# ログの確認
docker-compose -f docker-compose.dev.yml logs -f
# コンテナに入る
docker-compose -f docker-compose.dev.yml exec confluence-md-dev sh
# 停止
docker-compose -f docker-compose.dev.yml down
Terminal window
# 本番イメージのビルド
docker-compose build
# コンテナの起動
docker-compose up -d
# MCP サーバーとの通信(stdio 経由)
docker-compose exec confluence-md bun run dist/index.js
# 停止
docker-compose down

Claude Desktop での設定(Docker 経由)

Section titled “Claude Desktop での設定(Docker 経由)”
{
"mcpServers": {
"confluence-md": {
"command": "docker",
"args": [
"compose",
"exec",
"-T",
"confluence-md",
"bun",
"run",
"dist/index.js"
],
"cwd": "/path/to/niro-mcp-servers"
}
}
}

ポイント:

  • -T: 疑似 TTY を割り当てない(Claude Desktop との stdio 通信に必要)
  • cwd: docker-compose.yml があるディレクトリを指定