Skip to main content
Glama
nht-x
by nht-x
README.md
# knowledge-mcp

社内ナレッジをGitHubの保管庫に貯め、目次から辿って読むためのMCPサーバー。

ChatGPT や Claude などのMCP対応クライアントから、社内の記録を検索・保存できます。

## できること

| ツール | 種別 | 内容 |
|---|---|---|
| `get_knowledge_root` | 読み取り | 目次の入口を読む |
| `get_knowledge_index` | 読み取り | 分類ごとの一覧を読む |
| `get_knowledge_record` | 読み取り | 記録を1件読む |
| `save_knowledge` | 書き込み | 記録を1件保存する |

**この4つだけです。** ファイル削除・Issue作成・リポジトリ操作などの権限は持ちません。

## 探し方の設計

いきなり全文検索せず、**目次から辿ります**。

```
get_knowledge_root        目次の入口(ROOT.md)
   ↓ 分類を選ぶ
get_knowledge_index       分類ごとの一覧(knowledge/{分類}/INDEX.md)
   ↓ 記録を選ぶ
get_knowledge_record      記録の本文
```

目次は記録の位置を `path: knowledge/policy/abc.md` の形で載せています。
`get_knowledge_record` にはこのうち**最後の「/」より後ろだけ**を渡してください
(分類は `category` へ分けて渡します)。

```
目次の表記    path: `knowledge/policy/20260805T060019Z-abc.md`
                     └─ category ─┘ └────── filename ──────┘
```

ディレクトリを含む文字列は拒否されます。`.github/workflows/` へ書き込めると
任意のコードが実行できてしまうため、パス区切りは受け付けない設計です。
取り違えたときは、エラーメッセージが渡すべき形を示します。

分類は5つです。

| 分類 | ディレクトリ | 内容 |
|---|---|---|
| Policy | `policy` | 社内ルール・方針 |
| How-to | `how-to` | 作業手順・やり方 |
| Q&A | `qa` | よくある質問と答え |
| Reference | `reference` | 参照表・データ |
| Decision | `decision` | 決定事項とその理由 |

## 保存のしくみ

ファイルを直接コミットせず、**保管庫のワークフローを起動します**。

```
save_knowledge
  → POST /repos/{owner}/{repo}/dispatches
     (event_type: save_knowledge・日本語のまま送る)
        ↓
  保管庫の GitHub Actions が起動
        ↓
  形式変換 → コミット → 目次の再生成 → 個人情報チェック
```

**AIやサーバー側でBase64変換を行いません。** 変換はワークフロー側の責務です。

保存は非同期のため、ツールの成功は「受け付けた」ことを意味します。入力に不備があった場合は、保管庫に「保存失敗」として記録されます。

## セットアップ

### 1. 依存のインストール

```bash
poetry install
```

### 2. 環境変数

`.env.example` をコピーして `.env` を作成します。

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

| 変数 | 必須 | 内容 |
|---|---|---|
| `KNOWLEDGE_REPOSITORY` | ✅ | 保管庫のリポジトリ(`owner/repo`) |
| `GITHUB_TOKEN` | ✅ | アクセストークン |
| `KNOWLEDGE_SAVE_RATE_LIMIT_PER_MINUTE` | | 1分あたりの保存上限(既定 10) |
| `MCP_TRANSPORT` | | `stdio`(既定)または `streamable-http` |

### 3. トークンの権限

**Fine-grained PAT を推奨します。** 権限は必要最小限にしてください。

```
対象リポジトリ : 保管庫の1つだけ
権限          : Contents — Read and write
付与しないもの : Actions / Issues / Administration / Workflows
```

### 4. 起動

```bash
# stdio(ローカルのMCPクライアント向け)
poetry run knowledge-mcp

# Streamable HTTP(リモート接続向け)
MCP_TRANSPORT=streamable-http poetry run knowledge-mcp
```

## セキュリティ

AIに書き込み権限を渡すため、**AIの判断に依存しない防御**を実装しています。

| 対策 | 内容 |
|---|---|
| **パス検証** | 分類はホワイトリスト方式。ファイル名は `.md` のみ許可し、`..` `/` `\` と隠しファイルを拒否 |
| **最小の権限** | ツールは4つのみ。削除・Issue作成・ワークフロー実行の手段を持たない |
| **レート制限** | 1分あたりの保存回数に上限。プロンプトインジェクションによる大量保存への歯止め |
| **入力の上限** | タイトル120字/概要500字/本文10,000字/タグ10個 |
| **秘密情報** | 暗号化せず環境変数から直接読む(鍵を同じ場所に置く二重管理はしない) |
| **タイムアウト** | 全HTTPリクエストに設定(接続5秒/読み取り30秒) |
| **リダイレクト** | 追わない。意図しない転送先へトークンが送られることを防ぐ |
| **ログ** | 本文・トークンを記録しない。エラーの詳細は標準エラーのみに出す |
| **ログ偽装対策** | エラーメッセージに含める入力値から制御文字を除去し、長さを切り詰める |

### パス検証について

`.github/workflows/` に書き込めると**任意のコードが実行できてしまう**ため、
分類名はホワイトリスト方式で固定しています。相対参照や絶対パスは、
ホワイトリストに存在しない時点で弾かれます。

ファイル名の正規表現には `\A` と `\Z` を使っています。
Python の `$` は「文字列末尾の直前の改行」にもマッチするため、
`^...$` だと `"file.md\n"` のような入力が通ってしまいます。

## ログ

操作の記録と、障害を追うための情報を1行1JSONで出します。
出力先は常に標準エラーです(stdio 通信では標準出力が MCP プロトコル専用のため)。

### 何が残るか

| 種類 | レベル | 内容 | 本番(INFO)で残るか |
|---|---|---|---|
| `tool.started` | DEBUG | 呼び出しの開始 | ✗ |
| `tool.completed` | INFO | **操作ログ**。何が実行されたか | ✓ |
| `tool.denied` | WARNING | **弾いた入力**。検証エラー・レート制限 | ✓ |
| `tool.failed` | ERROR | 失敗。原因の種別つき | ✓ |

```json
{"timestamp":"2026-09-01T05:12:33.123Z","level":"WARNING","logger":"knowledge_mcp.audit",
 "message":"tool.denied",
 "extra":{"tool":"get_knowledge_index","request_id":"71346ae6020f",
          "category":"../.github/workflows","outcome":"denied",
          "reason":"validation_error","duration_ms":0.1}}
```

### 調べ方

1回の呼び出しに `request_id` が振られ、その間に出た全てのログに同じ値が付きます。

```bash
grep '71346ae6020f' server.log
```

```bash
jq 'select(.extra.tool=="save_knowledge")' server.log
jq 'select(.message=="tool.denied")' server.log
jq 'select(.extra.duration_ms > 3000)' server.log
```

**`tool.denied` が増えていたら中身を見てください。** 検証で弾いた記録であり、
AIが仕様を取り違えているか、指示文が誘導に乗せられている可能性があります。
上の例では、保管庫の外(`.github/workflows`)を読もうとしたことが残っています。

### 保存の追跡

保存は非同期です。このサーバーのログに残るのは**受け付けたところまで**で、
実際にコミットされたかは保管庫側の Actions の記録を見ます。
突き合わせられるよう、保存時の `request_id` を控えておいてください。

### 残さないもの

記録の本文・タイトル・トークンは出しません。本文は長さ(`body_chars`)だけを残します。
何が保存されたかは保管庫の履歴で確認してください。ログに業務内容を写すと、
ログの保管場所にも同じ機密性が必要になるためです。

トークンは形式によらず伏せられます(`GITHUB_TOKEN=` / `GITHUB_PAT=` のような
環境変数名の形も含む)。

## 他のMCPサーバーとの統合

`register(server)` を公開しているため、1台のサーバーに複数のMCPをまとめられます。

```python
from mcp.server import MCPServer
from chatwork_mcp.interfaces.mcp_tools import register as register_chatwork
from knowledge_mcp.interfaces.mcp_tools import register as register_knowledge

server = MCPServer("integration", version="1.0.0")
register_chatwork(server)
register_knowledge(server)
server.run(transport="stdio")
```

クライアントからは1つのアプリとして見え、サーバーの費用も1台分で済みます。

## 構成

```
src/knowledge_mcp/
├── domain/              外部に依存しない。保管庫の構造と検証ルール
│   ├── models.py
│   └── validators.py
├── application/         ユースケース
│   └── usecases.py
├── infrastructure/      外部との接続
│   ├── config.py
│   ├── github_client.py
│   └── rate_limiter.py
├── interfaces/          MCPとの接続
│   └── mcp_tools.py
├── observability/       運用のための記録
│   ├── audit.py         操作の記録と相関ID
│   └── log_config.py    出力形式・秘密情報の伏せ字・保存先
└── mcp_server.py        エントリポイント
```

依存の向きは常に**外側から内側**です。MCP SDK の仕様が変わっても
`domain` と `application` は影響を受けません。

## テスト

```bash
poetry run pytest
```

## ライセンス

MIT