はじめに
最近色々なSaaS製品が自社のMCPを公開しているため、自分が開発しているプロダクトでも(自分用のお試しで)MCP(Model Context Protocol)サーバー**として公開し、Claude Codeから直接CRUD操作できるようにしてみたので、そのメモを残します。
ちなみに私は普段、自社のプロダクトの従業員情報を共通的で管理する基盤の開発を行っています。
今回はこの中から従業員を登録する機能をClaude Codeから実行できることをゴールとします。
MCPとは
MCP(Model Context Protocol)は、AIアシスタントが外部ツールやデータソースにアクセスするための標準プロトコルです。Claude Code や Claude Desktop などのクライアントから、MCPサーバーが提供する「ツール」を呼び出すことで、AIがAPIを直接操作できるようになります。
[Claude Code] --stdio--> [MCPサーバー] --HTTP--> [既存のRails API]
構成
技術スタック
| レイヤー | 技術 |
|---|---|
| MCPサーバー | Python + FastMCP |
| 認証 | Auth0 OAuth 2.0 PKCE |
| HTTP通信 | httpx(非同期) |
| パッケージ管理 | uv |
ディレクトリ構成
my_app_demo_mcp_tools/
├── server.py # MCPサーバー本体(ツール定義)
├── api_client.py # REST APIクライアント
├── auth.py # 対象のプロダクトがOAuth PKCE認証フローを使っているので、そのフローを使用してトークンを取得
├── config.py # トークン・設定の永続化
├── pyproject.toml
└── requirements.txt
実装のポイント
1. FastMCPでツールを定義する
PythonのMCP SDKが提供する FastMCP を使うと、デコレータで簡単にツールを定義できます。
今回はこの関数でユーザの一覧表示、登録、編集、削除機能を定義しました。
from mcp.server.fastmcp import FastMCP
mcp = FastMCP(
"my-api",
instructions="MCS組織管理APIツール。ユーザー・属性・属性項目のCRUD操作が可能です。",
)
@mcp.tool()
async def list_users(
company_code: str = "",
service_id: int | None = None,
page: int = 1,
) -> str:
"""
ユーザー一覧を取得する。
Args:
company_code: 企業コード (省略時はset_contextの値を使用)
service_id: サービスID (省略時はset_contextの値を使用)
page: ページ番号 (1ページ50件)
"""
# ... API呼び出し
result = await client.list_users(company_code, service_id, page)
return json.dumps(result, ensure_ascii=False, indent=2)
@mcp.tool()
async def create_user(
email: str,
last_name: str,
first_name: str,
display_name: str,
role_id: int,
...
) -> str:
"""
新しいユーザーを登録する。
...
"""
# ...以下略
ポイント:
- 関数名がそのままツール名になる
- docstringがツールの説明としてAIに表示される
- 型ヒント付き引数がツールのパラメータスキーマになる
- 戻り値は文字列(JSONを整形して返す)
2. Auth0 PKCE認証をMCPツールとして組み込む
私が開発する機能ではAuth0のJWT認証を使っており、当然、MCPからAPIを使う時も必要です。
このJWTはAuth 2.0 Authorization Code + PKCE フローという仕組みで、取得することができます。
MCPサーバーで上記の仕組みを使うために今回は一時的にフロー中のcallbackをローカルホスト通るように、authorization サーバー側に設定します。
その上で、MCPを動かすローカルないにコールバックを受け付けるサーバーを作ります。
async def login_via_pkce(
auth0_domain: str,
client_id: str,
audience: str,
callback_port: int = 18234,
) -> dict:
# 1. PKCE code_verifier / code_challenge を生成
code_verifier = secrets.token_urlsafe(64)[:128]
digest = hashlib.sha256(code_verifier.encode("ascii")).digest()
code_challenge = base64.urlsafe_b64encode(digest).rstrip(b"=").decode("ascii")
# 2. ローカルにコールバックサーバーを起動
server = HTTPServer(("localhost", callback_port), CallbackHandler)
# 3. ブラウザでAuth0認可URLを開く
authorize_url = f"https://{auth0_domain}/authorize?..."
webbrowser.open(authorize_url)
# 4. ユーザーがログイン → コールバックで認可コード受信
# 5. 認可コードをアクセストークンに交換
async with httpx.AsyncClient() as http_client:
resp = await http_client.post(token_url, json={
"grant_type": "authorization_code",
"code": authorization_code,
"code_verifier": code_verifier,
...
})
return resp.json()
このコードを実行するとブラウザが開き、指定のURLでログインを済ませると、JWTが取得できている状態になります。
フロー図:
Claude Code MCPサーバー ブラウザ Auth0
| | | |
|-- login ツール --> | | |
| |-- localhost:18234 起動 |
| |-- webbrowser.open -->| |
| | |-- /authorize ->|
| | |<- ログイン画面 -|
| | |-- 認証情報入力 ->|
| |<-- /callback?code= --|<- リダイレクト -|
| |-- POST /oauth/token ---------------->|
| |<-- access_token ----------------------|
|<-- "ログイン成功" --| | |
2. トークン永続化と自動リフレッシュ
一旦サンプルということでトークンをファイルに永続化することで、毎回ログインする手間を省きました。
# ~/.config/my-app-demo-mcp/tokens.json に保存
CONFIG_DIR = Path.home() / ".config" / "my-app-demo-mcp"
TOKEN_FILE = CONFIG_DIR / "tokens.json"
def save_token(access_token: str, refresh_token: str = "", expires_in: int = 86400):
token_data = {
"access_token": access_token,
"refresh_token": refresh_token,
"expires_at": int(time.time() * 1000) + (expires_in * 1000),
}
# パーミッション 0o600 でセキュアに保存
path.write_text(json.dumps(token_data))
os.chmod(path, stat.S_IRUSR | stat.S_IWUSR)
3. 構造化エラーハンドリング
APIからのエラーをそのまま返すと、AIが適切な対処を提案しにくくなります。ステータスコード別にヒントを付与しました。
_ERROR_HINTS = {
401: "アクセストークンが無効または期限切れです。`login` で再認証してください。",
403: "アクセス権限がありません。company_code / ロールを確認してください。",
404: "リソースが見つかりません。ID や company_code が正しいか確認してください。",
422: "バリデーションエラーです。`list_*` や `get_*` で既存データの構造を確認してみてください。",
}
def _build_error_response(self, status, body, method, url):
error_response = {
"error": True,
"status": status,
"request": f"{method} {url}",
}
# レスポンスからエラーメッセージを抽出
if isinstance(body, dict) and "errors" in body:
error_response["errors"] = body["errors"]
# ヒントを追加
if status in _ERROR_HINTS:
error_response["hint"] = _ERROR_HINTS[status]
return error_response
こうすることで、AIが「login で再認証してください」「list_attributes で既存データを確認してみてください」といった具体的な次のアクションを提案できるようになります。
4. ドメイン固有のバリデーション
MCPツールはAIが呼び出すため、APIに送信する前にツール側でバリデーションできると、より親切なエラーメッセージを返せます。
例えば、今回はユーザー登録時に「権限」などが必須です。
@mcp.tool()
async def create_user(
email: str,
last_name: str,
first_name: str,
display_name: str,
role_id: int,
...
) -> str:
"""
新しいユーザーを登録する。
Args:
...
role_id: 権限/ロールID (必須)
サービスAの場合: 1=マネジメント責任者, 2=オペレーション担当者,
3=アクションプラン担当者, 4=サーベイ回答者
survey_take_type: サーベイ回答方法 (必須)
0=回答しない, 1=メールで回答, 4=IDで回答
"""
ctx = _get_context(company_code or None, service_id)
# 固有バリデーション
if role_id not in (1, 2, 3, 4):
return "エラー: role_idの値が不正です。有効値: 1, 2, 3, 4"
...
# API呼び出し
result = await client.create_user(ctx[0], ctx[1], user_data)
return _fmt(result)
...
ドメイン知識を docstring の Args に詳しく書く ことで、AIが適切なパラメータを選択できるようになります。
Claude Codeへの接続設定
.mcp.json
リポジトリルートの .mcp.json に追加します。
{
"mcpServers": {
"my-app-demo-api": {
"command": "/Users/you/.local/bin/uv",
"args": [
"run",
"--directory", "/path/to/my-app-demo-api/mcp_tools",
"mcp", "run", "server.py"
],
"env": {
"MCS_API_BASE_URL": "https://your-api.example.com",
"AUTH0_DOMAIN": "your-tenant.auth0.com",
"AUTH0_CLIENT_ID": "your-client-id",
"AUTH0_AUDIENCE": "https://your-api.example.com/"
},
"alwaysAllow": [
"login", "auth_status", "set_context", "get_context",
"get_current_user",
"list_users", "get_user",
"list_attributes", "get_attribute",
"list_attribute_items", "get_attribute_item"
]
}
}
}
ハマりポイント: uv + setuptools の flat-layout エラー
uv run mcp run server.py 実行時に以下のエラーが出ることがあります:
error: Multiple top-level modules discovered in a flat-layout: ['auth', 'server', 'config', 'api_client'].
pyproject.toml で明示的にモジュールを指定して解決します:
[tool.setuptools]
py-modules = ["server", "api_client", "auth", "config"]
動作確認
適当にユーザを作成するように指示してみます。
実際の画面を見ても作成するように指示したユーザが存在します!

まとめ
既存のREST APIをMCPツール化するための主要なステップは以下の通りです:
- FastMCPでツールを定義 - デコレータ + docstring + 型ヒントで自然にスキーマが生成される
-
認証フローを組み込む - Auth0 PKCEなら
webbrowser.open+ ローカルコールバックサーバーで実現可能 - トークンを永続化する - 毎回ログインは面倒なので、ファイル保存 + 自動リフレッシュ
- エラーにヒントを付与 - AIが次のアクションを提案しやすくなる
- ドメイン知識をdocstringに書く - AIが正しいパラメータを選択する助けになる
MCPの良いところは、既存APIを一切変更せずにAIからアクセス可能にできる点です。今回対象のAPIには一切手を加えず、薄いPython層を追加するだけで実現できました。簡単!


