社内向けの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往復の裏側は、次のように動きます。
- 利用者が、自社の生成AIチャットアプリに「インシデント5件出して」と日本語で入力します。
- アプリがAzure OpenAIに「次にどう動くか」を尋ねます(このとき使えるTool一覧も一緒に渡します)。
- Azure OpenAIはServiceNowには触れません。「
look_up_incident_recordsを limit=5 で呼んで」という指示をテキストで返すだけです。 - その指示を受け取ったアプリが、ServiceNowのMCPサーバーにアクセスし、Toolを実行します。
- ServiceNowは、ログインユーザーのロール(権限)とToolの定義に従ってテーブルを検索し、結果をアプリに返します。
- アプリが結果を再びAzure OpenAIに渡し、回答文に整えてもらいます。ここで必要なら、別のToolや社内の他情報も追加で参照させ、回答の幅を広げられます。
- アプリが利用者に回答を表示します。
よくある誤解:「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設定 → クライアント接続 → テスト)が出ています。
サーバー一覧(社名・URL・作成者)はマスクしています。
Step 1. MCPサーバーを作る(または既存を選ぶ)
「Create server」から作成します。最初は付属の Quickstart Server を使うのが早いです。
| 項目 | 入力内容 |
|---|---|
| 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(インバウンド連携)を作ります。外部クライアントが繋がるかどうかは、ここの設定で決まります。
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を指す不整合 |
正常系は、未認証なら 401(WWW-Authenticate 付きのOAuthチャレンジ)、認証済みならJSON-RPC(application/json か text/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接続)とログ、右にチャットを置きました。
「最新のインシデントを5件教えて」と聞くと、回答の下に ServiceNow MCP Tool実行(どのToolをどの引数で呼んで何が返ったか)が展開表示されます。下は表示イメージで、値はサンプルです。
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
- 公式
mcpPython SDK(streamablehttp_client/ClientSession) -
authlib(OAuth2 + PKCE)、httpx、requests - NiceGUI(チャットUI)
- MCPプロトコルバージョン:
2025-06-18




