03. 技術仕様
アーキテクチャ概要
Section titled “アーキテクチャ概要”モノレポ構成
Section titled “モノレポ構成”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各パッケージの役割
Section titled “各パッケージの役割”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_htmlclean_confluence_pagebatch_clean_pages
処理フロー詳細
Section titled “処理フロー詳細”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]
1. HTML パース
Section titled “1. HTML パース”使用ライブラリ: JSDOM
const dom = new JSDOM(html);const document = dom.window.document;2. 不要要素の削除
Section titled “2. 不要要素の削除”削除対象のセレクタ:
const selectorsToRemove = [ 'script', // スクリプト 'style', // スタイル '.confluence-information-macro-icon', // アイコン画像 '.expand-control', // 展開コントロール '.ia-button', // ボタン 'ac\\:structured-macro[ac\\:name="toc"]', // 目次マクロ];3. Confluence マクロの展開
Section titled “3. Confluence マクロの展開”info マクロ
Section titled “info マクロ”入力:
<ac:structured-macro ac:name="info"> <ac:rich-text-body> <p>重要な情報です</p> </ac:rich-text-body></ac:structured-macro>出力:
> ℹ️ Info: 重要な情報ですcode マクロ
Section titled “code マクロ”入力:
<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>出力:
```typescriptfunction hello() { console.log("Hello");}```4. Markdown 変換
Section titled “4. Markdown 変換”使用ライブラリ: Turndown
this.turndownService = new TurndownService({ headingStyle: 'atx', // # 見出し形式 codeBlockStyle: 'fenced', // ``` コードブロック bulletListMarker: '-', // - リスト});5. 後処理
Section titled “5. 後処理”- 余分な空行削除:
\n{3,}→\n\n - 前後の空白削除:
trim() - 文字数制限:
substring(0, maxLength)
共有ロジック
Section titled “共有ロジック”cleaner.ts
Section titled “cleaner.ts”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 { // カスタムルール追加 }}MCP サーバー
Section titled “MCP サーバー”index.ts
Section titled “index.ts”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);package.json 構成
Section titled “package.json 構成”共有ロジック
Section titled “共有ロジック”{ "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" }}MCP サーバー
Section titled “MCP サーバー”{ "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:*" }}開発フェーズ
Section titled “開発フェーズ”Phase 1: 共有ロジック実装(2-3日)
Section titled “Phase 1: 共有ロジック実装(2-3日)”Day 1-2: コアロジック
- ConfluenceCleaner クラス
- HTML → Markdown 変換
- 基本的なマクロ対応(info, warning, code)
- ユニットテスト
Day 3: 完成度向上
- すべてのマクロ対応
- エッジケース対応
- ドキュメント作成
Phase 2: MCP サーバー実装(2日)
Section titled “Phase 2: MCP サーバー実装(2日)”Day 4: MCP サーバー構築
- 3つのツール実装
- clean_confluence_html
- clean_confluence_page
- batch_clean_pages
Day 5: 仕上げ
- 統合テスト
- README作成
- 使用例の追加
ユニットテスト
Section titled “ユニットテスト”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 ページを使ったテスト
- 複雑なマクロのネストをテスト
- 大量ページでのパフォーマンステスト
Docker 環境
Section titled “Docker 環境”セキュリティ制約とローカル実行
Section titled “セキュリティ制約とローカル実行”このプロジェクトはセキュリティ要件により、ローカル環境(Docker コンテナ内)での実行を前提としています。
- Confluence への接続は社内ネットワーク内のみ
- 外部サービスへのデータ送信は禁止
- Docker コンテナ内で完結する構成
Dockerfile
Section titled “Dockerfile”# DockerfileFROM 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-mdCMD ["bun", "run", "dist/index.js"]docker-compose.yml
Section titled “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
Section titled “開発用 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: bridgeDockerfile.dev(開発用)
Section titled “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"]ローカル開発(Docker なし)
Section titled “ローカル開発(Docker なし)”# モノレポルートでbun install
# すべてのパッケージをビルドbun run build
# MCP サーバーを起動bun run --filter @niro/mcp-confluence-md startDocker での開発
Section titled “Docker での開発”# 開発環境のビルドと起動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 downDocker での本番実行
Section titled “Docker での本番実行”# 本番イメージのビルドdocker-compose build
# コンテナの起動docker-compose up -d
# MCP サーバーとの通信(stdio 経由)docker-compose exec confluence-md bun run dist/index.js
# 停止docker-compose downClaude 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 があるディレクトリを指定