1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

自社のAzure OpenAIから、ServiceNowのMCPサーバー経由でNow Assistに接続する方法(Entra IDでのユーザー引き当て対応)

1
Last updated at Posted at 2026-06-28

社内向けのAIチャットに「最新のインシデントを5件出して」と打つと、もっともらしい一般論が返ってきます。データを見に行く手段を渡されていないからです。この記事は、自社のAzure OpenAIにServiceNowを操作する手段を持たせ、MCPサーバー経由でNow AssistのToolを呼び、会話からインシデントやケースを引けるようにするまでの記録です。つまずいた箇所も省かずに書きます。


今回実現できたこと

  • 自社のAzure OpenAIから、ServiceNowのMCPサーバー(Now AssistのTool)を呼んで実データを引けるようにしました。
  • 認証はEntra ID → ServiceNowの2段OAuth + PKCEです。パスワードはブラウザにしか入れず、アプリはトークンだけを持ちます。Entra IDのログインIDでServiceNowユーザーを引き当てます。
  • 接続できなかった原因は、自作クライアントではなく、ServiceNow側でMCPサービスが有効化されていなかったことでした。

何をしようとしているか

用語集

用語が続くので、最初だけ普通の言葉で説明します。分かっている人は読み飛ばしてください。

  • Azure OpenAI:自社で使っているAIの基盤です。プロンプト(文章)を送ると回答テキストを返します。
  • MCP(Model Context Protocol):AIに道具を持たせるための共通規格です。ServiceNowはNow Assistのスキルをこの規格のToolとして外部に公開できます。
  • Now Assist:ServiceNowのAI機能群です。インシデントやケースの検索・要約などをToolとして提供します。
  • ServiceNowの「Tool」:AIから呼べる機能のことです。ServiceNowの画面では「Tool」と表記されます。例:look_up_incident_records(インシデントを検索する)。
  • OAuth / PKCE:パスワードを直接渡さずに「このアプリにこの権限を許可する」を安全に行う仕組みです。PKCEはその強化版です。

ざっくり構成要素

登場するのはこの子たちです

登場人物 何者か 役どころ
あなた / チャットUI 質問する人と入力画面 「インシデント5件出して」と頼む
Azure OpenAI(自社基盤) 文章を作るAI 依頼を解釈し、どのToolを使うか決める
ServiceNow MCPサーバー 社内データへの窓口 Now AssistのTool(インシデント検索など)を出す
Entra ID 会社のID基盤 「あなたが誰か」を保証し、ServiceNowユーザーに引き当てる

処理の流れ(少し詳しく)

会話1往復の裏側は、次のように動きます。

  1. 利用者が、自社の生成AIチャットアプリに「インシデント5件出して」と日本語で入力します。
  2. アプリがAzure OpenAIに「次にどう動くか」を尋ねます(このとき使えるTool一覧も一緒に渡します)。
  3. Azure OpenAIはServiceNowには触れません。「look_up_incident_records を limit=5 で呼んで」という指示をテキストで返すだけです。
  4. その指示を受け取ったアプリが、ServiceNowのMCPサーバーにアクセスし、Toolを実行します。
  5. ServiceNowは、ログインユーザーのロール(権限)とToolの定義に従ってテーブルを検索し、結果をアプリに返します。
  6. アプリが結果を再びAzure OpenAIに渡し、回答文に整えてもらいます。ここで必要なら、別のToolや社内の他情報も追加で参照させ、回答の幅を広げられます。
  7. アプリが利用者に回答を表示します。

よくある誤解:「Azure OpenAIがServiceNowに直接アクセスして検索する」と思われがちですが、実際にMCPサーバーを叩くのは間に立つアプリです。Azure OpenAIは「どのToolを呼ぶか決める」「返ってきた結果を解釈する」役に徹します。これは手動ツールループでもfunction callingでも変わりません(AIはToolの呼び出しを指示するだけで、実行はクライアント側が行います)。

ポータルを開いてフィルタして、という操作の代わりに、会話だけで社内システムのデータにたどり着けます。これが目的です。


全体構成

図中の色:■利用者 / ■自作アプリ / ■Azure OpenAI / ■ServiceNow / ■Entra ID。

工夫がいるのは2か所です。認証は、Entra IDでログインした本人をServiceNowに引き当て、本人のロール(権限)でトークンを発行します。Tool実行は、補完の口しか使えなかったので、後述の手動ツールループでToolを呼ばせます。


ServiceNow側の準備(設定手順)

ServiceNowには MCP Server Console という管理画面があり、ここでMCPサーバーを作り、Now AssistのToolを足し、OAuthを設定します。画面右に公式の流れ(サーバー選択 → Tool追加 → OAuth設定 → クライアント接続 → テスト)が出ています。

01_console_servers_masked.png

サーバー一覧(社名・URL・作成者)はマスクしています。

Step 1. MCPサーバーを作る(または既存を選ぶ)

「Create server」から作成します。最初は付属の Quickstart Server を使うのが早いです。

02_create_server_masked.png

項目 入力内容
Label 管理用の表示名(例:my-mcp
Name システム内部名(Labelから自動生成)
Application Global(または任意スコープ)
Server URL 自動生成。https://<your-instance>.service-now.com/sncapps/mcp-server/mcp/<server_name> の形
Short description 用途のメモ(必須)

この Server URL が、後でクライアントから叩くMCPエンドポイントになります。控えておきます。

Step 2. Toolを追加する

作成画面の下部「Tools」→「Add tools」で、このサーバーが公開するToolを選びます。ITSM系には、Now Assistの次のToolがあります。

Tool名 役割
look_up_incident_records インシデントの検索
incident_summarization インシデントの要約
look_up_case_records ケースの検索
case_summarization ケースの要約

Toolは接続ユーザーのロールに紐づきます。itil などITSMロールがないと、Toolはあっても結果が返りません。

Step 3. OAuth(インバウンド)を設定する

System OAuth > Application Registry でOAuth client(インバウンド連携)を作ります。外部クライアントが繋がるかどうかは、ここの設定で決まります。

03_oauth_client_masked.png

Name / Client ID / Client Secret / Redirect URL はマスク。右側の設定値が要点です。

認可コード + PKCEのパブリッククライアントとして使う場合の設定値を挙げます。

項目 設定値 補足
Name 任意(例:mcp-client 識別用の一意名
Client ID 自動生成 これをクライアント側に設定します
Client Secret 空でよい PKCEのパブリッククライアントなら不要
Redirect URL http://localhost:<port>/callback アプリ側のループバックURLと完全一致が必要
Public Client true PKCE利用時はON
Token Format JWT
Subject Claim Sys ID(または email 誰のトークンかを示すクレーム
Refresh Token Lifespan 8,640,000(秒・例) リフレッシュ有効期間
Access Token Lifespan 1,800(秒・例) アクセストークン有効期間(30分)
Scope Restriction Broadly scoped
Active true

Redirect URLは、クライアントが使う値と1文字でも違うと弾かれます。ポート番号やパス(/callback)まで揃えます。

Step 4 & 5. クライアントから接続してテストする

クライアント側に、Step1で控えた Server URL と、Step3の Client ID / Redirect URL を設定し、OAuthでログインします。最後にToolを1つ呼ぶだけのテストで疎通を確認します。本記事の自作アプリでは、接続するとTool一覧が自動取得されます。


認証の流れ:2段の OAuth 2.0 + PKCE(Entra IDでユーザー引き当て)

両方とも認可コードフロー + PKCEで、リダイレクトはローカルloopbackで受けます。パスワードの入力はブラウザ側に閉じ、アプリはアクセストークンだけを受け取ります。Entra IDのログインID(メール)をServiceNowの sys_user に照合し、本人として扱います。

認証で詰まった点が2つあります。

ひとつは、SPA登録のEntraアプリは、トークン取得時に Origin ヘッダーがないと AADSTS9002327 系で弾かれることです。

token_headers = {
    "Content-Type": "application/x-www-form-urlencoded",
    "Origin": redirect_uri,   # SPA登録アプリではこれが必須
}

もうひとつは権限の扱いです。ServiceNow側は本人のロールのトークンを出します。アプリに強い権限を持たせるのではなく、ログインした人の権限のままToolが動くので、過剰な権限を渡さずに済みます。


本題①:MCPエンドポイントがJSON-RPCを返さない

ここで一番時間を使いました。OAuthログインもトークン取得も成功しているのに、MCPに投げてもJSON-RPCが返ってきません。生HTTPで叩いて切り分けた結果がこれです。

# リクエスト 実測(問題発生時)
1 POST 未認証 302 Found → Location: /session_timeout.do
2 POST + OAuth Bearer(JWT) 200 OK だが Content-Type: text/html(UIのHTML)
3 GET + OAuth Bearer 302 → /session_timeout.do
4 POST + Basic認証 302(Basicは受理されない)
5 GET .../.well-known/oauth-protected-resource 200 JSON。ただし authorization_servers が404を指す不整合

正常系は、未認証なら 401WWW-Authenticate 付きのOAuthチャレンジ)、認証済みならJSON-RPC(application/jsontext/event-stream)になるはずでした。

切り分けに使ったプローブ関数を載せます(認証情報はログに出しません)。

async def probe(mcp_url: str, token: str | None = None) -> str:
    headers = {
        "Accept": "application/json, text/event-stream",
        "Content-Type": "application/json",
        "MCP-Protocol-Version": "2025-06-18",
    }
    if token:
        headers["Authorization"] = f"Bearer {token}"
    init = {"jsonrpc": "2.0", "id": 1, "method": "initialize",
            "params": {"protocolVersion": "2025-06-18", "capabilities": {},
                       "clientInfo": {"name": "my-mcp-client", "version": "0.1"}}}
    async with httpx.AsyncClient(timeout=30, follow_redirects=False) as c:
        r = await c.post(mcp_url, headers=headers, json=init)
    return f"status={r.status_code} ct={r.headers.get('content-type')} loc={r.headers.get('location','-')}"

効果はありませんでしたが、原因の絞り込みには役立った試行も書いておきます。接続ユーザーに sn_mcp_server.viewer を付与しても#2は200 HTMLのままでした。scopeを変えても変化なし。公式 mcp Python SDKでも#1と同じ302で、401チャレンジが返らないためOAuthディスカバリが始まりませんでした。

原因

原因は、ServiceNowインスタンス側でMCPサーバーのサービスが有効化されていなかったことでした。そのため未認証で302、認証済みでUIのHTMLを返し、JSON-RPCハンドラまで到達していませんでした。

ベンダーサポートに上の実測表(未認証302/認証200 HTML、.well-known の不整合)を添えて起票し、サービスをrefreshしてもらって解消しました。解消後は未認証で401、認証でJSON-RPCを返すようになり、Tool 4件を取得・実行できました。

外部MCPクライアントが繋がらないときは、クライアント実装より先に、インスタンス側のMCPサービス有効化を疑うとよいです。302 /session_timeout.do.well-known の不整合は、その手がかりになります。


本題②:補完の口で動かす手動ツールループ

今回の基盤は補完しか使えなかったので、プロンプトでToolを説明し、Tool呼び出しをテキストで出力させます。クライアントがそれを解析してMCPのToolを実行し、結果をAIに戻します。ReAct風のループになります。

出力フォーマットはプロンプトで固定します。

Toolが必要なときは、次の1行だけを出力する:
  TOOL_CALL {"name":"<Tool名>","arguments":{<引数>}}
結果(TOOL_RESULT)を受け取ったら、十分なら:
  FINAL <ユーザーへの回答>

ループ本体は次のようになります(抜粋・簡略)。

scratch = ""
for _ in range(MAX_STEPS):
    text = await complete(prompt + scratch)        # 補完APIに次の行動を尋ねる
    call = parse_tool_call(text)                    # "TOOL_CALL {...}" を抽出
    if call is None:
        return final_text(text)                     # FINAL → 最終回答
    name, args = call
    result = await mcp_call_tool(mcp_url, token, name, args)   # MCPのToolを実行
    scratch += f'TOOL_CALL {name} {args}\nTOOL_RESULT: {result[:4000]}\n\n'  # 結果を文脈へ

TOOL_CALL の抽出は、波括弧の深さを数えてJSON部分だけを取り出します。前後に余計な文字が混じっても拾えます。基盤側のRAGが混ざると邪魔になるので、Tool駆動時はハイブリッド検索をOFFにしました。

function callingが使える基盤なら、このループはネイティブの tools 定義に置き換えられます。MCPの接続部分(次節)はそのまま使えます。

MCP接続(Streamable HTTP)

公式 mcp SDK の streamablehttp_client に、ServiceNow発行トークンを Bearer で渡します。

from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client

async def call_tool(mcp_url, token, name, arguments):
    headers = {"Authorization": f"Bearer {token}"}
    async with streamablehttp_client(mcp_url, headers=headers) as (read, write, _):
        async with ClientSession(read, write) as session:
            await session.initialize()
            result = await session.call_tool(name, arguments)
            return _result_to_text(result)

動いている画面

左に認証フローの状態(① Entra認証 ② ユーザ引き当て ③ MCP接続)とログ、右にチャットを置きました。

02_app_overview_anon.png

「最新のインシデントを5件教えて」と聞くと、回答の下に ServiceNow MCP Tool実行(どのToolをどの引数で呼んで何が返ったか)が展開表示されます。下は表示イメージで、値はサンプルです。

04_chat_mock_anon.png

Tool実行のトレースを出すと、AIの作文か実データに基づく回答かを、その場で確認できます。


セキュリティで気をつけたこと

  • 資格情報(パスワード/トークン)はログ・画面・AIプロンプトに出しません。
  • トークン類は端末ローカルに保存し(gitignore済)、次回起動で再利用します。
  • ServiceNow操作はログインユーザー本人のロールに従います。アプリに過剰な権限を持たせません。
  • チャット本文とServiceNow応答は、回答生成のためAI基盤に送られます。外部には送りません。

まとめ

補完の口しか使えない構成でも、手動ツールループでMCPのToolを呼ばせれば、Azure OpenAIからServiceNow(Now Assist)の実データに基づいて答えられます。function callingが使えるなら、ループをネイティブのTool機能に置き換えればよいです。

認証はEntra ID → サービス側OAuthの2段 + PKCEにして、パスワードをブラウザに閉じ込め、Entra IDのIDでServiceNowユーザーを引き当て、本人のロールで動かしました。繋がらないときは生HTTPで切り分け、302 /session_timeout.do.well-known の不整合が出たら、サービス側の有効化を疑ってサポートへ回します。

新しい技術は足していません。Azure OpenAI・ServiceNow(Now Assist)・Entra IDという既にあるものを、繋がる順番で繋いだだけです。同じ構成で詰まっている人の手がかりになれば。


参考:使った主なもの

  • ServiceNow MCP Server Console / Application Registry(OAuth インバウンド)/ Now Assist のTool
  • 公式 mcp Python SDK(streamablehttp_client / ClientSession
  • authlib(OAuth2 + PKCE)、httpxrequests
  • NiceGUI(チャットUI)
  • MCPプロトコルバージョン:2025-06-18
1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?