アプリケーションサービス部の山本です。
中途社員の研修を担当しています。
が、毎日こちらが学ぶことばかりです。
はじめに
Claude Desktop で MCP(Model Context Protocol)サーバーを使って Backlog などの外部サービスと連携する際、API キーの管理は避けて通れない課題です。
設定ファイルに API キーをベタ書きしていませんか? その設定ファイルを Claude 自身が読めてしまう状態になっていませんか?
この記事では、設定ファイルに API キーを一切残さず、AWS Secrets Manager と Python の venv を使って安全に MCP サーバーへキーを渡す方法を、実際に動作確認しながら解説します。きっかけは「これ AWS Secrets Manager で出来ないかな?」「Windows でも Mac でも同じ手順でやりたい」という素朴な疑問でした。Python で書けば OS を問わず同じコードで動きます。
なお、シークレットの保管先としては macOS Keychain や Windows Credential Manager も有力な選択肢です。個人利用であればそちらの方がシンプルで十分安全なケースも多いでしょう。本記事では、チームでの共有・監査ログ・キーの自動ローテーションといった運用面のメリットから AWS Secrets Manager を採用しています。両者の詳しい比較は記事の後半にまとめています。
SSM Parameter Store を使用すると無料なので、追加で記事を書きました。よろしければ、こちらもどうぞ。
MCP サーバーの API キーを AWS Systems Manager Parameter Store で管理してみた - サーバーワークスエンジニアブログ
この記事で実現すること
flowchart LR
Claude["Claude Desktop\n( MCP 設定ファイルの command に\nrun_mcp.py を指定)"]
Wrapper["run_mcp.py"]
Claude -- "ツール呼び出し・結果受信\n(stdin/stdout)" --> Wrapper
flowchart LR
Wrapper["run_mcp.py"]
Secrets["AWS Secrets Manager"]
Wrapper -- "① API キーを取得" --> Secrets
flowchart LR
Wrapper["run_mcp.py"]
MCP["Backlog MCP サーバー"]
Wrapper -- "② API キーを環境変数にセット\n③ MCP サーバーを子プロセスとして起動" --> MCP
flowchart LR
MCP["Backlog MCP サーバー"]
Backlog["Backlog API"]
MCP -- "環境変数の API キーで\n認証・リクエスト" --> Backlog
ポイント:
- MCP 設定ファイルに API キーを書かない
- AWS の一時トークン(
aws sso login/aws login)で認証するため、静的なキーがファイルに残らない - Claude が認識するのは「ツール名」と「実行結果」だけ
- Python の venv でグローバル環境を汚さない
- macOS / Windows 共通のコードで動く
なぜ API キーを Claude に読ませてはいけないのか
MCP サーバーの設定方法として、以下のような args に直接キーを書くパターンを見かけることがあります。
{ "mcpServers": { "backlog-server": { "command": "npx", "args": [ "-y", "backlog-mcp-server", "--api-key", "ここにAPIキーを直書き" ] } } }
これには複数のリスクがあります。
| リスク | 説明 |
|---|---|
| プロセス一覧からの漏洩 | ps コマンドやタスクマネージャーで引数が丸見えになる |
| エラーログへの出力 | MCP サーバーがクラッシュした際、Node.js のエラーハンドラや OS のクラッシュレポートが process.argv(起動引数)をログファイルやコンソールにそのまま出力することがある。args に API キーが含まれていると、ログ収集ツール(CloudWatch Logs、Datadog 等)やクラッシュレポートサービスに平文で送信されてしまう |
| Filesystem MCP 経由の読み取り | Claude に Filesystem ツールを使わせている場合、設定ファイルを読み取って API キーを出力してしまう可能性がある |
env ブロックに書く方法はこれらのリスクを軽減しますが、設定ファイル自体にキーが残ることに変わりはありません。AWS Secrets Manager を使えば、設定ファイルにすらキーを書かない状態を実現できます。
前提条件
- Python 3.9 以上
- Node.js(MCP サーバーの実行に必要)
- AWS CLI が設定済み(
aws loginまたはaws sso loginで一時トークンを利用) - 上記で認証したプロファイル名を、後述の設定ファイルで
AWS_PROFILEに指定します
"env": { "AWS_PROFILE": "aws login / aws sso login で使用したプロファイル名" }
- AWS Secrets Manager へのアクセス権限(
secretsmanager:GetSecretValue)
手順 1: AWS Secrets Manager にシークレットを登録する
AWS マネジメントコンソール、または AWS CLI でシークレットを作成します。
aws secretsmanager create-secret \
--name "mcp/backlog-keys" \
--secret-string '{"BACKLOG_API_KEY":"あなたのBacklog APIキー"}' \
--region ap-northeast-1
実行結果:
{ "ARN": "arn:aws:secretsmanager:ap-northeast-1:123456789012:secret:mcp/backlog-keys-AbCdEf", "Name": "mcp/backlog-keys", "VersionId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" }
手順 2: Python venv のセットアップ
任意のディレクトリにプロジェクトを作成します。
mkdir backlog-mcp-wrapper cd backlog-mcp-wrapper # venv 作成 python3 -m venv venv # boto3 と aws login 用の依存パッケージをインストール venv/bin/pip install boto3 "botocore[crt]" # macOS / Linux # venv\Scripts\pip install boto3 "botocore[crt]" # Windows
手順 3: ラッパースクリプトの作成
run_mcp.py を作成します。このスクリプトが全体の要です。
import os import sys import json import subprocess import boto3 from botocore.exceptions import ClientError, BotoCoreError # ============================================================ # 設定値(ご自身の環境に合わせて変更してください) # ============================================================ SECRET_NAME = "mcp/backlog-keys" AWS_REGION = "ap-northeast-1" BACKLOG_DOMAIN = "yourspace.backlog.com" # MCP サーバーの起動コマンド # npx 経由で backlog-mcp-server を起動する MCP_COMMAND = os.environ.get("MCP_COMMAND", "npx") MCP_ARGS = os.environ.get( "MCP_ARGS", "-y,backlog-mcp-server,--enable-toolsets,project,wiki", ).split(",") def get_aws_secret() -> dict: """AWS Secrets Manager からシークレットを取得して dict で返す。""" aws_profile = os.environ.get("AWS_PROFILE", "default") try: session = boto3.session.Session( profile_name=aws_profile, region_name=AWS_REGION, ) client = session.client(service_name="secretsmanager") response = client.get_secret_value(SecretId=SECRET_NAME) return json.loads(response["SecretString"]) except (ClientError, BotoCoreError) as e: # 【重要】ログは必ず stderr へ。 # stdout に出すと Claude ⇔ MCP の JSON-RPC 通信が壊れる。 print( f"[run_mcp] AWS シークレットの取得に失敗しました (Profile: {aws_profile})", file=sys.stderr, ) print(f"[run_mcp] エラー詳細: {e}", file=sys.stderr) sys.exit(1) def main() -> None: # 1. AWS Secrets Manager からキーを取得 secrets = get_aws_secret() # 2. 子プロセス用の環境変数を構築 env = os.environ.copy() env["BACKLOG_API_KEY"] = secrets.get("BACKLOG_API_KEY", "") env["BACKLOG_DOMAIN"] = os.environ.get("BACKLOG_DOMAIN", BACKLOG_DOMAIN) # 3. MCP サーバーを起動 cmd = [MCP_COMMAND] + MCP_ARGS print(f"[run_mcp] 起動コマンド: {' '.join(cmd)}", file=sys.stderr) try: result = subprocess.run(cmd, env=env) sys.exit(result.returncode) except FileNotFoundError: print( f"[run_mcp] '{MCP_COMMAND}' コマンドが見つかりません。", file=sys.stderr, ) sys.exit(1) except KeyboardInterrupt: pass if __name__ == "__main__": main()
ラッパースクリプトの各ステップと設定ファイルの関係
このスクリプトが何をしているのか、通常の MCP 設定と比較しながらステップごとに解説します。
通常の設定(API キーが設定ファイルに残る):
{ "mcpServers": { "backlog-server": { "command": "npx", "args": ["-y", "backlog-mcp-server", "--enable-toolsets", "project", "wiki"], "env": { "BACKLOG_DOMAIN": "yourspace.backlog.com", "BACKLOG_API_KEY": "ここにAPIキーが丸見え" } } } }
ラッパーを使った設定(API キーが設定ファイルに存在しない):
{ "mcpServers": { "backlog-server": { "command": "/path/to/venv/bin/python", "args": ["/path/to/run_mcp.py"], "env": { "AWS_PROFILE": "my-sso-profile" } } } }
違いは明確です。env に BACKLOG_API_KEY が存在しません。では API キーはどこから来るのか? それがラッパースクリプトの3つのステップです。
ステップ ①: AWS Secrets Manager から API キーを取得
secrets = get_aws_secret()
Claude Desktop が MCP サーバーを起動する際、設定ファイルの command に書かれたプログラムを実行します。通常は npx が直接呼ばれますが、ラッパー方式では代わりに venv/bin/python run_mcp.py が起動します。
起動直後、スクリプトは env で渡された AWS_PROFILE を使って AWS Secrets Manager に接続し、API キーを取得します。この時点で API キーはプロセスのメモリ上にだけ存在し、ファイルには一切書かれていません。
ステップ ②: 環境変数を構築
env = os.environ.copy() env["BACKLOG_API_KEY"] = secrets.get("BACKLOG_API_KEY", "") env["BACKLOG_DOMAIN"] = os.environ.get("BACKLOG_DOMAIN", BACKLOG_DOMAIN)
取得した API キーを、これから起動する MCP サーバーに渡すための環境変数としてセットします。os.environ.copy() で現在の環境変数をコピーし、そこに BACKLOG_API_KEY を追加しています。
通常の設定では MCP 設定ファイルの env ブロックがこの役割を担いますが、ラッパー方式ではスクリプトが動的に組み立てます。
ステップ ③: MCP サーバーを起動
cmd = [MCP_COMMAND] + MCP_ARGS # → ["npx", "-y", "backlog-mcp-server", ...]
result = subprocess.run(cmd, env=env)
最後に、通常の設定で command + args に書かれていたコマンド(npx -y backlog-mcp-server ...)を、ステップ②で構築した環境変数付きで実行します。
subprocess.run はデフォルトで親プロセスの stdin/stdout を子プロセスに引き継ぎます。Claude Desktop は MCP サーバーと stdin/stdout 経由で JSON-RPC 通信を行うため、この引き継ぎが正しく動くことが必須です。
つまり、Claude Desktop から見ると通信相手は変わりません。間に Python が挟まっていることすら意識しない構造です。
通常:
flowchart LR
A1["Claude Desktop"] -- "stdin/stdout" --> B1["npx\nbacklog-mcp-server"]
ラッパー方式:
flowchart LR
A2["Claude Desktop"] -- "stdin/stdout" --> B2["python\nrun_mcp.py"] -- "stdin/stdout" --> C2["npx\nbacklog-mcp-server"]
D2["AWS\nSecrets Manager"] -. "API キーを取得\nenv に注入" .-> B2
その他の重要ポイント
ログは必ず stderr へ
print("メッセージ", file=sys.stderr) # ✅ 正しい print("メッセージ") # ❌ JSON-RPC 通信が壊れる
Claude と MCP サーバーは stdout で JSON-RPC 通信を行います。ラッパースクリプト内で stdout にログを出すと、通信データが破損して MCP サーバーの起動エラーになります。
環境変数 MCP_COMMAND / MCP_ARGS での上書き
MCP_COMMAND = os.environ.get("MCP_COMMAND", "npx") MCP_ARGS = os.environ.get( "MCP_ARGS", "-y,backlog-mcp-server,--enable-toolsets,project,wiki", ).split(",")
テスト時にモックサーバーに差し替えられるよう、環境変数でコマンドを上書きできるようにしています。MCP_ARGS はカンマ区切りで指定します。
手順 4: MCP 設定ファイルの更新
MCP サーバーの設定ファイルは、使用するクライアントによって場所が異なります。
| クライアント | 設定ファイルの場所 |
|---|---|
| Claude Desktop | ~/Library/Application Support/Claude/claude_desktop_config.json(macOS)%APPDATA%\Claude\claude_desktop_config.json(Windows) |
| Kiro | ~/.kiro/settings/mcp.json(ユーザーレベル).kiro/settings/mcp.json(ワークスペースレベル) |
| Claude Code(VS Code 拡張) | ~/.claude.json の .projects 内 |
いずれの場合も、mcpServers に追加する内容は同じです。
macOS / Linux
{ "mcpServers": { "backlog-server": { "command": "/path/to/backlog-mcp-wrapper/venv/bin/python", "args": ["/path/to/backlog-mcp-wrapper/run_mcp.py"], "env": { "AWS_PROFILE": "my-sso-profile" } } } }
Windows
{ "mcpServers": { "backlog-server": { "command": "C:\\path\\to\\backlog-mcp-wrapper\\venv\\Scripts\\python.exe", "args": ["C:\\path\\to\\backlog-mcp-wrapper\\run_mcp.py"], "env": { "AWS_PROFILE": "my-sso-profile" } } } }
command に venv 内の Python 実行ファイルを直接指定するのがポイントです。source activate のような手順は不要で、venv の Python バイナリを直接呼べば自動的にその仮想環境が使われます。
動作確認
シークレット取得テスト
まず、AWS Secrets Manager からシークレットを正しく取得できるか確認します。
# test_secret.py import os, sys, json, boto3 from botocore.exceptions import ClientError, BotoCoreError def main(): aws_profile = os.environ.get("AWS_PROFILE", "default") print(f"[テスト] 使用プロファイル: {aws_profile}") session = boto3.session.Session( profile_name=aws_profile, region_name="ap-northeast-1" ) client = session.client(service_name="secretsmanager") response = client.get_secret_value(SecretId="mcp/backlog-keys") secrets = json.loads(response["SecretString"]) print(f"[成功] キー一覧: {list(secrets.keys())}") api_key = secrets.get("BACKLOG_API_KEY", "") print(f" BACKLOG_API_KEY: {'*' * len(api_key)} ({len(api_key)}文字)") if __name__ == "__main__": main()
venv/bin/python test_secret.py
実行結果:
[テスト] 使用プロファイル: default [成功] キー一覧: ['BACKLOG_API_KEY'] BACKLOG_API_KEY: **************************** (28文字)
E2E テスト(モック MCP サーバーを使用)
実際の Backlog MCP サーバーの代わりに、環境変数を受け取って JSON-RPC レスポンスを返すモックサーバー(mock_mcp_server.js)と、それを使った E2E テストスクリプト(test_e2e.py)を作成してテストします。
まず、モックサーバー mock_mcp_server.js です。実際の backlog-mcp-server が Node.js 製のため、同じ Node.js で stdin/stdout の挙動を再現しています。
// mock_mcp_server.js const readline = require('readline'); const apiKey = process.env.BACKLOG_API_KEY || ''; const domain = process.env.BACKLOG_DOMAIN || ''; // ログは stderr に(stdout は JSON-RPC 専用) process.stderr.write(`[mock-mcp] BACKLOG_API_KEY: ${'*'.repeat(apiKey.length)}\n`); const rl = readline.createInterface({ input: process.stdin }); rl.on('line', (line) => { const request = JSON.parse(line); const response = { jsonrpc: '2.0', id: request.id, result: { status: 'ok', has_api_key: apiKey.length > 0, // API キーの値自体は絶対にレスポンスに含めない } }; process.stdout.write(JSON.stringify(response) + '\n'); });
次に、テストスクリプト test_e2e.py です。MCP_COMMAND と MCP_ARGS を上書きしてモックサーバーに差し替え、run_mcp.py 経由で通信が正常に動くかを検証します。
# test_e2e.py import os, sys, json, subprocess SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) MOCK_SERVER = os.path.join(SCRIPT_DIR, "mock_mcp_server.js") RUN_MCP = os.path.join(SCRIPT_DIR, "run_mcp.py") VENV_PYTHON = os.path.join(SCRIPT_DIR, "venv", "bin", "python") def test_via_wrapper(): env = os.environ.copy() env["AWS_PROFILE"] = os.environ.get("AWS_PROFILE", "default") env["MCP_COMMAND"] = "node" env["MCP_ARGS"] = MOCK_SERVER request = json.dumps({"jsonrpc": "2.0", "id": 1, "method": "test"}) + "\n" proc = subprocess.run( [VENV_PYTHON, RUN_MCP], input=request, capture_output=True, text=True, timeout=15, env=env, ) response = json.loads(proc.stdout.strip()) assert response["result"]["has_api_key"] is True print("✅ 成功: AWS Secrets Manager → 環境変数 → MCP サーバーの連携が正常") if __name__ == "__main__": test_via_wrapper()
実行します。
MCP_COMMAND=node MCP_ARGS=./mock_mcp_server.js venv/bin/python test_e2e.py
実行結果:
✅ 成功: AWS Secrets Manager → 環境変数 → MCP サーバーの連携が正常
テスト2 の結果から、以下が確認できました:
- AWS Secrets Manager からシークレットが正しく取得された(
has_api_key: true) - API キーの値自体はレスポンスに含まれていない(漏洩チェック通過)
- stdin/stdout の JSON-RPC 通信が Python ラッパーを経由しても正常に動作する
SSO プロファイル利用時の注意点
aws sso login や aws login で認証している場合、トークンには有効期限(通常数時間〜1日)があります。
トークンが切れた状態で Claude Desktop を起動すると、Python スクリプトが認証エラーを起こし、MCP サーバーが起動に失敗します。その場合は:
# ターミナルで再ログイン aws sso login --profile my-sso-profile # または aws login --profile my-sso-profile # Claude Desktop を再起動(または Cmd+R / Ctrl+R でリロード)
補足: OS の Keychain / Credential Manager ではダメなのか
macOS Keychain や Windows Credential Manager もシークレットの保管先として有力な選択肢です。AWS Secrets Manager との比較を整理しておきます。
| 観点 | macOS Keychain / Windows Credential Manager | AWS Secrets Manager |
|---|---|---|
| 保管場所のセキュリティ | ◎ OS レベルで暗号化、生体認証やログインパスワードで保護 | ○ AWS KMS で暗号化、IAM で制御 |
| 個人利用 | ◎ 追加コストゼロ、セットアップ簡単 | △ AWS アカウント必要、月額コストあり |
| チーム共有 | ❌ 端末ごとに手動設定が必要 | ◎ IAM ポリシーで複数人に配布可能 |
| キーのローテーション | ❌ 手動 | ◎ 自動ローテーション機能あり |
| 監査ログ | ❌ なし | ◎ CloudTrail で誰がいつ取得したか記録 |
| オフライン動作 | ◎ ネットワーク不要 | ❌ AWS への通信が必要 |
「どちらが安全か」というよりも、用途が違うというのが正確な整理です。
- 個人で1台のマシンだけで使う → Keychain / Credential Manager の方がシンプルで十分安全
- チームで共有したい、監査ログが必要、キーの自動ローテーションをしたい → AWS Secrets Manager
本記事では AWS Secrets Manager を採用していますが、これは筆者の環境が AWS を前提としていることや、チームでの運用上こうせざるを得ない環境もありそう、という背景からです。また、AWS の各種サービスと連携する MCP サーバーを使う場合は、同じ AWS の認証基盤に乗せられる Secrets Manager の方が相性が良いでしょう。個人利用であれば OS のキーチェーンで十分なケースも多いと思います。
この方式の限界
正直に書いておきます。この方式は万能ではありません。
そもそも Claude は env に書いた API キーを読めない
MCP の仕様上、Claude の LLM が受け取るのはツールの定義と実行結果だけです。MCP 設定ファイル(claude_desktop_config.json や ~/.claude.json 等)の env ブロックに書いた API キーを Claude が読み取ることは、通常ありません。つまり「Claude に API キーを認識させない」という目的だけなら、env に書くだけで達成できています。
この記事の本質的な価値はそこではなく、設定ファイルに平文でキーを残さないこと、そして Filesystem MCP 等で設定ファイルを読まれても安全な状態を作ることにあります。
環境変数自体のリスクは残る
ラッパースクリプトは最終的に API キーを環境変数にセットして子プロセスに渡しています。Linux であれば /proc/<pid>/environ、macOS でも ps eww 等で環境変数を読み取れる可能性があります。これは env に直書きした場合と同じ攻撃面です。ただし、設定ファイルという永続的なファイルにキーが残るのと、プロセスが動いている間だけメモリ上に存在するのとでは、漏洩の機会が大きく異なります。
守るべきものが移動しただけでは?
Backlog API キーを守るために AWS 認証情報を使っているので、その AWS 認証情報が漏洩したら同じことです。ただし、ここが重要なポイントで、AWS 認証情報は aws sso login や aws login を使えば一時的なトークンになります。有効期限があり、MFA で保護でき、CloudTrail で利用履歴を追跡できます。静的な API キーをファイルに置き続けるのとは、リスクの性質が根本的に異なります。
本記事で aws configure による静的なアクセスキーではなく aws sso login / aws login の利用を前提としているのは、この理由からです。
まとめ
この記事では、Claude Desktop × MCP サーバーの構成で API キーを安全に管理する方法を紹介しました。
| 方法 | セキュリティレベル | 手軽さ |
|---|---|---|
args に直書き |
❌ 危険 | ◎ |
env に直書き |
△ 設定ファイルにキーが残る | ○ |
| AWS Secrets Manager + Python ラッパー | ◎ 設定ファイルにもキーなし | △(初回セットアップが必要) |
初回のセットアップは少し手間がかかりますが、一度構築してしまえば運用は簡単です。Python の venv で環境を分離しているため、OS を問わず同じコードで動作します。
API キーの管理は「動けばいい」で済ませがちですが、MCP サーバーは Claude に強力な外部操作能力を与える仕組みです。その入口となるキー管理こそ、しっかりと守っていきましょう。