ClaudeとOutlook連携|ローカルMCPのクラウドMCP化とつまずき10問

対象読者:Claude で Outlook を操作したい情シス担当・開発者の方/ローカルMCPをクラウドに移したい方
※Azure・Entra ID の管理画面は図解で説明しています(他社製品の画面キャプチャは使用していません)

クラウドMCP(リモートMCP)とは、AIが使うツールをインターネット上のサーバーとして公開し、手元のPCの起動状態に関係なく呼び出せるようにした構成です。 MCP には2種類あります。PC 上でプロセスとして動く「ローカルMCP」と、HTTPS で公開される「リモートMCP」です。後者はクラウドの自動実行から呼び出せます。

わたしたち FROM CENTER では、Outlook のカレンダー操作を自動化するために outlook-mcp というローカルMCPを使っていました。しかし「PCが起動していないと動かない」という制約があり、定期実行が空振りする日が続きました。そこで Microsoft Graph を直接呼ぶ MCP サーバーを自作し、Azure App Service に載せてクラウドMCP化しました。

構築手順を9つに分け、実際にぶつかった10個のつまずきを後半のFAQ編に並べます。特に claude.ai のカスタムコネクタと Entra ID の OAuth が噛み合わない問題は、公式ドキュメントを読んでも書いていない落とし穴でした。

この記事でわかること

  • ローカルMCPをクラウド化すべきか、しなくてよいかの判断基準
  • Microsoft Graph を無人実行で叩くための Entra ID 設定(と、権限を絞る必須作業)
  • Azure App Service に MCP サーバーを載せてデプロイする手順
  • claude.ai のカスタムコネクタが Entra ID の OAuth で繋がらないときの回避策

目次

用語ミニ辞典

本文は正式な用語で書いています。IT に馴染みのない方は先にこちらをご覧ください。

用語意味
MCPAI が外部のツールを呼び出すための共通規格。Model Context Protocol の略
ローカルMCP手元のPC上でプロセスとして動く MCP。標準入出力で通信するため、そのPCからしか使えない
リモートMCPHTTPS で公開される MCP。インターネット経由でどこからでも呼び出せる
委任権限サインインした本人の権限の範囲で API を使う方式。人がログインする前提
アプリケーション権限人のサインインなしに、アプリ自身の権限で API を使う方式。無人実行に使う
ルーティンclaude.ai が決めた時刻に自動実行する仕組み。cron で指定する

その前に:クラウド化は本当に必要か

先に正直なことを書きます。やりたいことが1つの自動処理だけなら、クラウドMCP化は過剰です。

わたしたちは最初、「スクリプトを書いてクラウドの自動実行から叩く」という案を検討していました。必要な作業は Entra ID の設定だけで、サーバーもデプロイも要りません。

スクリプト方式クラウドMCP方式
月額0円0円〜(無料プランなら0円。有料は約1,900円/月)
必要な作業Entra ID の設定のみEntra ID + サーバー構築 + デプロイ + コネクタ登録
障害点自動実行の仕組みだけ自動実行 + サーバー + 公開エンドポイント
対話中に使えるか使えない使える(「来週の予定を見せて」と頼める)
他の業務への転用しにくいしやすい(メール送信なども足せる)

クラウドMCP化が見合うのは、「AIとの対話中にも使いたい」または「他の業務も同じ基盤に載せる予定がある」場合です。わたしたちは後者の理由で選びました。

⚠️ MCPサーバーは「待ち受け続けるプログラム」です。
ローカルMCPが無料なのは、あなたのPCがその置き場所を負担しているからです。クラウドに移すと、その負担がクラウド事業者に移り、料金として見えるようになります。スクリプト方式は常駐しないので置き場所が要りません。ここが両者の分かれ目です。

全体像と、先に知らないと後悔する3つのこと

構成はこうなります。

ローカルMCPはPCの起動が前提だが、クラウドMCPではルーティンからApp Service上のMCPサーバーを呼び、PCの状態に依存せずOutlookを操作できることを比べた図

作り終えたいま、「最初に知っていれば時間を使わずに済んだ」と思うことが3つあります。

  1. Microsoft 365 の公式コネクタは読み取り専用です。 予定やメールの検索はできますが、作成・削除のツールがありません。書き込みが必要なら自分でMCPサーバーを用意することになります。
  2. アプリケーション権限は、既定でテナント全員のメールボックスに届きます。 「アプリ登録に権限を付けた」だけで終わらせると、そのシークレットを持つ人が全社員のカレンダーを操作できる状態になります。ApplicationAccessPolicy で範囲を絞る作業まで含めて1セットです。
  3. claude.ai のカスタムコネクタと Entra ID の OAuth は、そのままでは繋がりません。 わたしたちは3回試して断念しました(→FAQ 2)。回避策はありますが、保護は弱くなります。ここを知らずに始めると、最後の工程で手が止まります。

Entra IDの準備・MCPサーバーの実装・App Serviceへのデプロイ・claude.aiへの接続という4パートに9手順を割り振った構築の流れの図


Part 1|Entra ID の準備(手順1〜3)

手順1. 専用のアプリ登録を新規作成する

  1. Azure Portal → Microsoft Entra ID → 「アプリの登録」→ 新規登録
  2. 名前を入力(例:outlook-calendar-mcp
  3. アカウントの種類は「この組織ディレクトリのみ」(シングルテナント)
  4. リダイレクト URI は設定しない
  5. 「概要」画面でアプリケーション(クライアント)IDディレクトリ(テナント)ID を控える
  6. 「証明書とシークレット」→ 新しいクライアントシークレット → を控える(一度しか表示されません)
⚠️ 既存の共用アプリに権限を追加してはいけません。
わたしたちは最初、社内で共用しているアプリ登録に権限を足してしまい、同日中に権限を削除して個人用アプリに作り直しました。共用のアプリ登録は、シークレットを複数人が持つ構成になりがちです。そこにアプリケーション権限を付けると、シークレットを持つ全員が全社員のカレンダーを読み書きできる状態になります。用途ごとにアプリ登録を分けてください(→FAQ 8)。

手順2. アプリケーション権限を付与する

  1. 作成したアプリ → 「API のアクセス許可」→ アクセス許可の追加
  2. Microsoft Graph → アプリケーションの許可 を選ぶ
  3. 必要な権限にチェック(カレンダー操作なら Calendars.ReadWrite
  4. 「管理者の同意を与えます」 を実行

「種類」列が 「アプリケーション」 になっていることを確認します。ここが「委任済み」だと、人のサインインが必要になり無人実行できません。

委任済みアプリケーション
誰の権限で動くかサインインした本人アプリ自身
サインインが必要か必要不要
アクセスできる範囲本人がアクセスできる範囲テナント内の任意のユーザー
無人実行できないできる

手順3. アクセス範囲をメールボックス1つに絞る【必須】

この手順を飛ばすと、シークレットが漏れたときの影響が全社員に及びます。 Azure Portal では設定できず、Exchange Online の PowerShell からのみ操作できます。

  1. PowerShell を開く(管理者権限は不要)
  2. モジュールを入れる(初回のみ)
Install-Module ExchangeOnlineManagement -Scope CurrentUser
  1. Exchange に接続する(ブラウザでサインインを求められます)
Connect-ExchangeOnline -UserPrincipalName you@example.com
  1. アクセス範囲を絞る
New-ApplicationAccessPolicy -AppId "<クライアントID>" -PolicyScopeGroupId "you@example.com" -AccessRight RestrictAccess -Description "outlook-mcp: 対象メールボックスのみ"
  1. 効いているか確認する(反映に最大1時間かかります)
Test-ApplicationAccessPolicy -Identity "you@example.com" -AppId "<クライアントID>"

AccessCheckResult : Granted(日本語環境では「許可」)になり、他の人のアドレスで試すと Denied(拒否) になれば成功です。

⚠️ 「許可」が出ただけでは設定できた証拠になりません。
ポリシーが1つも存在しない状態でも Test-ApplicationAccessPolicy は「許可」を返します(制限がなければ全部アクセスできるため)。Get-ApplicationAccessPolicy でポリシーの存在を確認し、他の人のアドレスで「拒否」になることまで見てください。わたしたちはここで一度勘違いしました。

Part 2|MCPサーバーの実装(手順4)

手順4. MCPサーバーを書く

Python の MCP SDK を使えば、依存は2つで足ります。

mcp>=2.0.0
PyJWT[crypto]>=2.8.0

実装の要点は4つです。

  1. ツールを関数として定義する(docstring がそのまま AI 向けの説明になります)
  2. Streamable HTTP のアプリを生成する
  3. Graph のトークンを client credentials で取得する(受け取ったトークンを Graph に転送してはいけません)
  4. me/ は使えないので、すべて users/{メールアドレス}/ 形式で呼ぶ

ツールの定義はこれだけです。

import asyncio

from mcp.server import MCPServer

mcp = MCPServer("outlook-calendar")

@mcp.tool()
async def list_calendars() -> dict:
    """自分のメールボックスにあるカレンダーの一覧を返す。"""
    calendars = await asyncio.to_thread(graph.list_calendars)
    return {"calendars": [{"name": c["name"], "id": c["id"]} for c in calendars]}
⚠️ MCP SDK 2.0 で API 名が変わっています。
1.x の mcp.server.fastmcp.FastMCP は 2.0 では存在せず、mcp.server.MCPServer になりました。stateless_http はコンストラクタ引数ではなく streamable_http_app() の引数に移動し、独自ルートの追加は @mcp.custom_route デコレータになっています(→FAQ 10)。

判定ロジックはサーバー側に寄せてください。 わたしたちは「同期処理」をツール1個にまとめ、除外条件や削除の可否をすべてサーバー側で決めるようにしました。AI 側に判断を残すと、実行のたびに結果が揺れる可能性があります。決定論的に動かしたい処理は、AI に渡す前に閉じておくのが安全です。


Part 3|Azure App Service へのデプロイ(手順5〜7)

手順5. App Service を作成する

  1. Azure Portal → 「App Service」→ 作成 → Web アプリ
  2. 設定値は次のとおり
項目
公開コード(コンテナーではない)
ランタイム スタックPython 3.12
オペレーティング システムLinux
価格プランFree F1(無料で試す場合。20分アクセスが無いと停止します)
  1. 「デプロイ」タブの継続的デプロイは「無効化」のままにする

継続的デプロイを有効にすると、Azure が独自のワークフローファイルをリポジトリに作成します。自分でワークフローを用意する場合は衝突するため、ここは触りません。

⚠️ Free プランはリージョンによって作成できません。
わたしたちは Japan East で Current Limit (Total VMs): 0 というエラーに当たりました。Free の枠が割り当てられていないリージョンがあります。East Asia に変更したら通りました(→FAQ 1)。

手順6. 環境変数とスタートアップコマンドを設定する

「設定」→「構成」→「全般設定」 で2箇所を設定します。

  1. スタートアップ コマンド(「スタック設定」の中にあります)
python -m uvicorn server:app --host 0.0.0.0 --port 8000
  1. 「SCM 基本認証の発行資格情報」を「オン」 にする

「設定」→「環境変数」 で資格情報と動作設定を登録します。

名前内容
MS_TENANT_IDディレクトリ(テナント)ID
MS_CLIENT_IDアプリケーション(クライアント)ID
MS_CLIENT_SECRETクライアントシークレットの値
WEBSITES_PORT8000
SCM_DO_BUILD_DURING_DEPLOYMENTtrue
⚠️ 既定の起動方法では動きません。
App Service の Python は既定で gunicorn を使い、app.pyapplication.py を探します。ファイル名が違えば見つからず、さらに非同期(ASGI)アプリは gunicorn 単体では起動できません。スタートアップコマンドの設定は省略できない手順です。
⚠️ SCM 基本認証が無効だとデプロイが認証に失敗します。
Azure はセキュリティ強化のため、この設定を既定で無効にしています。発行プロファイルを使うデプロイ方式では有効化が必要です(→FAQ 4)。

手順7. デプロイする

GitHub Actions を使う場合は、発行プロファイルをリポジトリのシークレットに登録します。

  1. App Service の「概要」→ 「発行プロファイルの取得」 でファイルをダウンロード
  2. GitHub のリポジトリ → Settings → Secrets and variables → Actions
  3. シークレット AZURE_WEBAPP_PUBLISH_PROFILE にファイルの中身(XML全文)を登録
  4. 変数 AZURE_WEBAPP_NAME に App Service 名(リソース名)を登録

ワークフローの要点は3つです。

  • app-name には App Service のリソース名を渡す(公開URLではない)
  • publish-profile には発行プロファイルのXML全文を渡す
  • package にはサーバーのディレクトリを指定する
- uses: azure/webapps-deploy@v3
  with:
    app-name: ${{ vars.AZURE_WEBAPP_NAME }}
    publish-profile: ${{ secrets.AZURE_WEBAPP_PUBLISH_PROFILE }}
    package: <サーバーのディレクトリ>

デプロイ後は /healthz などの死活確認エンドポイントを叩いて、起動を確認します。

⚠️ 公開URLはリソース名と一致しません。
作成時に「安全な一意の既定のホスト名」が有効だと、URL にランダムな文字列とリージョン名が入ります(例:myapp-abc123def456ghi7.eastasia-01.azurewebsites.net)。myapp.azurewebsites.net では届きません。デプロイ設定の app-name に渡すのはリソース名のほうです(→FAQ 3)。

Part 4|claude.ai への接続とルーティン(手順8〜9)

手順8. カスタムコネクタとして登録する

claude.ai → 設定 → コネクタ → カスタムコネクタを追加

項目
名前任意
URLhttps://<公開URL>/mcp
詳細設定OAuth Client ID / Secret(使う場合のみ)

接続に成功すると、実装したツールが一覧に表示されます。

⚠️ この工程で3回つまずき、最後まで解決できませんでした。
Entra ID を認可サーバーにした OAuth は、わたしたちの環境では繋がりませんでした。3回試して原因を特定できず、URL を秘密にする方式に切り替えました。詳しくはFAQ 2 に書きました。

手順9. ルーティンで定期実行する

コネクタを登録したら、claude.ai のルーティン(cron)で自動実行できます。

ツールの権限を「常に許可」に変えてください。 既定は「承認が必要」で、無人実行では承認待ちのまま止まります。定期実行で呼ぶツールだけを許可し、他は「承認が必要」のまま残すのが安全です。

ルーティンに渡す指示は、できるだけ短くします

  • 「このツールを呼んで、結果を報告する」だけにする
  • ファイルの読み書き・コミットはさせない
  • 判定はサーバー側で完結しているので、AI に条件判断をさせない

わたしたちは当初「実行ログをリポジトリに追記してコミットする」まで指示していましたが、組織のフックに阻まれて停止しました(→FAQ 9)。やることを1つに絞ったら40秒で完走しました。

公式コネクタが読み取り専用、アプリ権限がテナント全員に届く、Freeプランのクォータ不足、OAuthのスコープ解決失敗など6つの落とし穴と、それぞれの症状と回避策を並べた図


つまずきとFAQ編(10問)

先に一覧を置きます。症状から探してください。

#症状・疑問分類
1Current Limit (Total VMs): 0 で App Service が作れないエラー
2コネクタ接続で Entra エラー 9010010 / 650053エラー
3デプロイは成功したのに URL にアクセスできないエラー
4GitHub Actions のデプロイが認証エラーで失敗するエラー
5Graph API の応答が空になる(一覧が0件で返る)エラー
6WWW-Authenticate を付けたら 500 になるエラー
7スタートアップコマンドの設定画面が見つからない迷子
8委任権限とアプリケーション権限、どちらを選ぶか仕様
9ルーティンが「承認待ち」で止まる仕様
10MCP SDK のサンプルコードが動かない仕様

エラー編

FAQ 1. App Service の作成で「Operation cannot be completed without additional quota. Current Limit (Total VMs): 0」と出る

そのリージョンで、選んだプランの枠が割り当てられていません。リージョンを変えると通ることがあります。 わたしたちは Japan East の Free プランで発生し、East Asia に変更して解決しました。

有料プランは別のクォータで管理されているため、Japan East のままでも通る場合があります。どのリージョンでも通らない場合は、エラー画面からクォータの引き上げを申請できますが、承認に時間がかかります。

FAQ 2. claude.ai のコネクタ接続で Entra エラー「9010010」「650053」が出る

わたしたちが解決できなかった問題です。 経緯を残しておきます。

試行エラー内容
1回目9010010The resource parameter provided in the request doesn't match with the requested scopes.
2回目650053The application asked for scope that doesn't exist on the resource 'Microsoft Graph'.
3回目650053同上(クライアント側の User.Read を削除、コネクタも作り直したが変化なし)

サインインログを見ると、リソースが Microsoft Graph に解決されているのが共通していました。

Entra ID の v2.0 エンドポイントは、要求するリソースを scope の値だけで決めます。カスタムAPIのスコープは api://<クライアントID>/<スコープ名> という完全修飾で渡す必要があります。短い名前(calendar.access)だと既定リソースの Microsoft Graph と解釈され、そこに該当スコープがないため失敗します。

MCP 仕様側のメタデータ(scopes_supported)を完全修飾に直しても改善しませんでした。Entra ID 側に「既定リソースを Graph 以外にする」設定はありません。HTTPS URL をアプリケーション ID URI にするには検証済みドメインが必要で、azurewebsites.net は使えません。カスタムドメインは無料プランでは使えないため、有料プランへの移行が前提になります。

回避策:URL を秘密にする方式

わたしたちは MCP エンドポイントのパスにランダムな文字列を入れ、URL 自体を資格情報として扱う方式に切り替えました。

https://<公開URL>/mcp/<43文字のランダム文字列>
  • 正しいパスなら 200、それ以外は 404 を返す
  • OAuth のメタデータは公開しない(404 にして認証フローの開始を抑止する)
  • claude.ai には OAuth の欄を空にして登録する

保護は OAuth より弱くなります。 URL を知っている人は誰でもツールを実行できます。だからこそ、手順3のアクセス範囲の絞り込みが重要になります。被害範囲を「対象メールボックスのカレンダーのみ」に限定しておけば、URL が漏れてもメールやファイルには届きません。

FAQ 3. デプロイは成功したのに URL にアクセスできない

公開URLがリソース名と一致していない可能性があります。作成時に「安全な一意の既定のホスト名」が有効だと、次のような形になります。

myapp-abc123def456ghi7.eastasia-01.azurewebsites.net

サブドメイン乗っ取りを防ぐための Azure の機能です。App Service の「概要」画面の「既定のドメイン」で正確な値を確認してください。デプロイ設定の app-name に渡すのはリソース名(myapp)で、アクセスに使うのは上記の完全な URL です。ここを混同すると、デプロイは成功するのに繋がらない状態になります。

FAQ 4. GitHub Actions のデプロイが認証エラーで失敗する

SCM 基本認証が無効になっています。 App Service → 設定 → 構成 → 全般設定 →「SCM 基本認証の発行資格情報」を「オン」にしてください。

Azure はセキュリティ強化のためこの設定を既定で無効にしました。発行プロファイル方式は基本認証を使うため、無効のままでは通りません。作成画面にも「このアプリの基本認証は現在無効になっており、デプロイに影響する場合があります」という案内が出ます。作成時には変更できないので、作成後に設定します。

FAQ 5. Graph API の応答が空になる(一覧が0件で返る)

レスポンスの空判定に Content-Length を使っていると起こります。Graph は chunked transfer encoding で返すことがあり、その場合 Content-Length が存在しません。

# 誤り: chunked 応答では length が None になり、中身があるのに空扱いになる
if res.status == 204 or not res.length:
    return {}

# 正しい: ステータスコードだけで判定し、本文は読んでから判断する
if res.status == 204:
    return {}
raw = res.read()
return json.loads(raw) if raw else {}

わたしたちはこれで「カレンダーが0件」という結果に一度騙されました。エラーにならず正常終了するので気づきにくい不具合です。

FAQ 6. WWW-Authenticate ヘッダを付けたら 500 エラーになる

HTTP ヘッダには latin-1 の文字しか入れられません。 日本語のエラー説明をヘッダに入れると UnicodeEncodeError で 500 になります。

# 日本語を含む説明はヘッダに載せず、レスポンスボディ(JSON)側に入れる
if description and description.isascii():
    parts.append(f'error_description="{description}"')

ボディは UTF-8 なので日本語で問題ありません。エラーメッセージを日本語で書く方針の場合、ヘッダとボディで扱いを分ける必要があります。

迷子編

FAQ 7. スタートアップコマンドの設定画面が見つからない

App Service の左メニュー → 「設定」「構成」「全般設定」 タブ → 「スタック設定」 の中にあります。

環境変数は最近のポータルでは別画面に分離されており、「設定」→「環境変数」 です。同じ「構成」画面にあった時期もあるため、古い手順書と食い違うことがあります。

仕様編

FAQ 8. 委任権限とアプリケーション権限、どちらを選ぶべきか

無人実行が目的ならアプリケーション権限です。委任権限は人のサインインが前提なので、誰もいない時間に動く処理では使えません。

ただし2点注意があります。

  1. 既存の共用アプリに追加しないこと。 アプリケーション権限はテナント全員のメールボックスに届きます。共用のアプリ登録は、シークレットを複数人が持つ構成になりがちです。そこに付与すると、シークレットを持つ全員が全社員のデータにアクセスできる状態になります。用途ごとにアプリ登録を分けてください
  2. ApplicationAccessPolicy で範囲を絞ること。 手順3の作業まで含めて1セットです

なお、対話用のツールとして使うだけなら委任権限(On-Behalf-Of)のほうが権限は最小になります。用途で選び分けてください。

FAQ 9. ルーティンが「承認待ち」で止まる

原因は2つ考えられます。

1つめ:ツールの権限が「承認が必要」のまま

claude.ai のコネクタ設定で、ツールごとに権限を設定できます。既定は「承認が必要」なので、無人実行では承認を待って止まります。定期実行で呼ぶツールだけ「常に許可」に変えてください。

2つめ:組織のフックやガードに阻まれている

わたしたちのリポジトリには「自分のフォルダ以外への書き込みを禁止する」フックが入っています。ルーティンの実行環境には作業者の判定に必要な設定ファイル(gitignore 対象)が無いため、書き込みがブロックされました。

PreToolUse:Edit hook error: 作業者(WORKSPACE_MEMBER)が未設定のため、書き込み先を検証できません。

対処は「ルーティンにファイル操作をさせない」ことでした。 実行記録はルーティンの履歴に残るので、リポジトリへの追記をやめれば衝突しません。指示を「ツールを1回呼んで報告する」だけに絞ったところ、40秒で完走しました。

無人実行の指示は、できるだけ短く、副作用を少なくが原則です。

FAQ 10. MCP SDK のサンプルコードが動かない

2.0 で API 名が変わっています。 1.x 向けの記事やサンプルは、そのままでは動きません。

項目1.x2.0
クラスmcp.server.fastmcp.FastMCPmcp.server.MCPServer
stateless_httpコンストラクタ引数streamable_http_app() の引数
独自ルート追加Starlette の routes を直接操作@mcp.custom_route(path, methods=[...])
クライアントstreamablehttp_clientmcp.Client(URL文字列を渡せる)
属性名inputSchemainput_schema(snake_case)

もう1点、DNS リバインディング保護が既定で有効です。公開ドメインを許可リストに入れないと、リバースプロキシ経由のリクエストが全て拒否されます。

security = TransportSecuritySettings(allowed_hosts=["myapp.example.net", "localhost:8000"])
app = mcp.streamable_http_app(transport_security=security)

まとめ

  • まずクラウド化が必要か見極める。 自動処理を1つ動かすだけならスクリプト方式のほうが軽いです。対話中にも使いたい、他の業務にも広げる、という場合にクラウドMCPが見合います
  • Microsoft 365 の公式コネクタは読み取り専用。 書き込みが必要なら自作のMCPサーバーを用意することになります
  • アプリケーション権限は ApplicationAccessPolicy で範囲を絞るまでが1セット。 絞らないとテナント全員のメールボックスに届きます。共用のアプリ登録に付与するのは特に危険です
  • claude.ai と Entra ID の OAuth は素直に繋がらない。 URL を秘密にする方式で回避できますが、保護は弱くなります。だからこそアクセス範囲の絞り込みが効きます
  • 無人実行の指示は短く、副作用を少なく。 ファイル操作やコミットを含めると、フックや権限で止まります。判定はサーバー側に寄せて、AI には「呼んで報告する」だけを任せます

わたしたちの環境では、これで PC の起動状態に関係なく3時間ごとの同期が回るようになりました。なお Free プランは20分アクセスが無いと停止するため、久しぶりの実行では起動待ちで時間がかかります。作業時間は調査を含めて半日ほど、月額費用は0円です(無料プランのため。安定性を求めるなら有料プランを検討してください)。

参考リンク

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です