はじめに
Model Context Protocol(MCP)は、ClaudeなどのAIアシスタントが外部サービスを直接操作するための標準規格です。CRM(顧客・案件管理)をMCP対応にすると何が起きるのか、実装側では何を決める必要があるのか——この記事では、受注型のひとり社長向けに作った国産CRM「Knottle」のMCPサーバー実装を題材に、接続方式の選択とツール設計で効いた判断を具体的に書きます。
対象読者は、自分のサービスをMCP対応させようとしている開発者です。
接続は2方式。どちらを主にするかで導入体験が変わる
MCP対応SaaSにClaudeから繋ぐ経路は2つあります。
1. OAuth(claude.aiのカスタムコネクタ)
claude.aiの「設定 > コネクタ」にサーバーのURLを1つ登録し、ブラウザでOAuth認可するだけで繋がります。実装側の要点は次のとおりです。
-
動的クライアント登録(DCR):
POST /api/oauth/registerは認証不要で誰でも登録できる。ただしredirect_urisはclaude.aiとlocalhostのみ許可する -
クライアントシークレットを発行しない。公開クライアント(
token_endpoint_auth_method: "none")として動かし、認証はPKCEのみに寄せる。接続案内で「クライアントIDとシークレットは空欄で」と書けるのはこのため - PKCEはS256必須。平文チャレンジは受け付けない
- アクセストークン1時間 / リフレッシュトークン60日(スライディング)
- リフレッシュトークンの再利用を検知したら、その一族(family)ごと失効させる。トークンが漏れたときに被害を止める
発見用エンドポイントは /.well-known/oauth-protected-resource と /.well-known/oauth-authorization-server の2つ。認証失敗時の 401 に WWW-Authenticate ヘッダを付けて返せば、claude.ai側がここから認可サーバーを自動で見つけてくれます。
2. APIキー(Claude Desktopなど)
ローカルの設定ファイルにサーバー起動コマンドとキーを書く従来型です。knottle_sk_ + ランダム64文字、保存はbcryptハッシュ、平文は発行時に1回だけ表示。
ひとつ設計上のトレードオフがあります。APIキーにはユーザーの紐付けがないため、この経路の書き込みは「誰が」の記録が残りません(監査ログにテナント・ツール名・対象は残る)。個人利用なら問題になりませんが、複数人で使う想定ならOAuth経路に寄せた方がいいです。
Knottleは非エンジニアが主な利用者なので、OAuthコネクタを主経路にしています。APIキーの発行画面を開かせない、設定ファイルを触らせない、という判断です。
ツールは35個。内訳は業務フローの形になる
| カテゴリ | ツール数 |
|---|---|
| 見積・請求 | 13 |
| 顧客・取引先 | 5 |
| 案件 | 5 |
| 担当者 | 3 |
| タスク | 5 |
| 活動履歴 | 2 |
| 書類 | 2 |
見積・請求が突出しているのは、ここが状態機械そのものだからです。create_quote → accept_quote(受注)→ issue_quote(見積書発行)→ issue_invoice(請求書発行)→ record_payment(入金記録)と進み、それぞれに逆操作(unaccept_quote / unissue_quote / unissue_invoice / unrecord_payment)が別ツールとして存在します。
この「不可逆操作と取り消し操作を別ツールに割る」設計の意図は別記事(Zenn)に書きました。 ここでは接続とツール設計の話に絞ります。
実装して効いた判断4つ
1. AIに生のブール値を渡さない
見積系ツールはDBの行をそのまま返しません。日本語のキーと結論文に変換してから返します。
状態.見積書: 「発行済み(2026年7月12日)」/「未発行」
状態.請求書: 「発行済み(2026年7月14日)」/「未発行」
状態.削除: 「削除できます」/「削除できません。先に ①…②… の順で解除してください。」
状態.明細の編集: 「編集できます」/「発行済みのため編集できません」
can_delete: false を渡してAIに理由を考えさせると、平気で創作します。判定はサーバーが済ませ、「できない理由と次の一手」まで含めて、そのまま読み上げられる文で渡す。 これが誤情報を一番減らしました。
2. 作る前に探す(冪等性)
create_account / create_deal / create_person / create_quote は、作成前に既存を探します。
| ツール | 同一とみなす条件 |
|---|---|
create_account |
同一テナントで名前が完全一致 |
create_deal |
同じ顧客で案件名が完全一致 |
create_person |
メール完全一致。無ければ指定顧客内で氏名一致 |
create_quote |
同じ案件の、未受注・最新・下書き |
見つかれば reused: true と「既存の〜を再利用しました。」を返します。AIはツール呼び出しを平気でリトライするので、これが無いと顧客や見積が二重に増えます。 再利用時は採番もプラン上限も消費しません。
3. ツールは例外を投げない
MCPツール内で例外を投げると、セッションごと落ちます。必ず { isError: true, content: [...] } で返します。
エラーの出し分けは2種類だけにしました。
- 業務ルール由来のエラー → メッセージをそのまま返す(最初から顧客に見せられる日本語で書いてある)
-
それ以外すべて(DBエラー・想定外の例外)→ 一文に潰す:
「操作できませんでした。恐れ入りますが Knottle サポート(support@amjt.jp)へお問い合わせください。」
内部のエラー文やSQLエラーコードが顧客の目に触れることはありません。
4. 一覧は最大20件
LLMのコンテキストを食い潰さないためです。search_all(顧客・案件・担当者の横断検索)は、3種の合計が20件を超えないよう配分します。件数を絞ることは機能の劣化ではなく、AI経由で使うことの前提条件でした。
ハマったところ
instructions は接続時に1回しか渡されない。 MCPサーバーがAIへ渡す行動規約は、文言を変えても既存セッションや進行中の会話には反映されません。検証するにはコネクタを繋ぎ直して新しいチャットを始める必要があります。「直したのに直っていない」の大半はこれでした。
RLSが効かない。 MCPツールはservice_roleで動かすため、行レベルセキュリティは適用されません。テナント分離は各ツールが自分で .eq('tenant_id', tenantId) を付けることで成立しています。トークンからテナントIDを最初に1回だけ解決してクロージャに閉じ込め、MCPハンドラをリクエストごとに作り直す——これが同時実行でテナントが混ざらない仕組みそのものです。
タイムゾーンを持たないツールがある。 list_tasks は日時範囲で絞るだけで、タイムゾーンを解釈しません。「今日のタスク」を出すのはAI側の責任、という切り分けをツール説明文に明記しています。曖昧に両方で処理すると、ズレたときに原因が追えなくなります。
まとめ
MCP対応SaaSを実装する側の判断は、「ツールを何個作るか」よりも次の3点に集約されました。
- 接続方式: 非エンジニアが使うならOAuthコネクタを主経路にする。APIキー方式は「誰が」が残らないトレードオフを理解して選ぶ
- 返し方: 生の値ではなく結論文を返す。判定をAIにさせない
- 壊れ方: 例外を投げない、リトライで二重にしない、コンテキストを溢れさせない
他のMCP対応SaaSを評価するときも、この3点を見ると実装の練度が分かると思います。
実際にKnottleのMCP接続を試してみたい方は、フリープラン(無料・カード不要)で確認できます: https://knottle.amjt.jp/signup