前回はMPCサーバをAzure Functionsで作ることを試したけど、今度はMCPサーバを使う側を試してみる。
試すのは、Azure AI FoundryのAgent ServiceからのMCPサーバの呼び出し。
ちなみにまだプレビュー。
learn.microsoft.com
サンプルを試す。
サンプルは、SDKを使ったPythonコードと、curlコマンドを使ったREST API実行の2つ。
learn.microsoft.com
今まで雰囲気でPython使っていたので、このページだけだと何から始めたらいいかピンとこないけど、少なくとも from ... import に書かれている azure.identity azure.ai.projects azure.ai.agents くらいは入れないといけないはず。
入れてみた。
$ pip install azure.identity azure.ai.projects azure.ai.agents
あとはコードをマルっとコピーして、環境変数をいくつか設定して実行。
。。。
動かない。
肝心の McpTool がインポートできない、と言われた。
Python詳しくないので、早々に諦める。
bash上でcurlでエンドポイントにアクセスしているだけなので、こっちの方がわかりやすい。
ただ、SDKだと良しなにしてくれる認証に必要なアクセストークンは、ここに書いてある👇のコマンドで取得する。
$ az account get-access-token --resource 'https://ai.azure.com' | jq -r .accessToken | tr -d '"'
実際は、環境変数 AGENT_TOKEN に入れて使うけど、長ーいトークンをコピーするのが面倒なので、
$ AGENT_TOKEN=$(az account get-access-token --resource 'https://ai.azure.com' | jq -r .accessToken | tr -d '"')
としてる。
あと API_VERSION という環境変数も設定。
$ API_VERSION=2025-05-15-preview
ちゃんとプレビューのバージョンを指定。
あとは書かれているcurlコマンドを、ただただ実行。
。。。
初っ端のエージェント作成から失敗。
まず、リクエストボディの指定でJSON文字列全体を " (ダブルクォーテーション)で囲っているのに、JSON内の " をエスケープしていない。
JSON内をエスケープするのが面倒だったので、全体を囲む " を ' (シングルクォーテーション) に変更。
まだある。
付けちゃいけない末尾カンマが付いているのでこれも削除。

まったく、、、そういうとこだぞ。
この修正*1をしたらエージェントの作成は成功。
その後は、
- スレッドの作成
- スレッドへのメッセージ(質問文)の追加
- エージェントを指定してスレッドを実行
- 実行の状態を取得
というステップでcurlでREST APIを実行していき、無事成功。
すると、実行の状態がMCPツールの実行承認待ち、ということになっているのがわかるので、MCPツールの実行を承認するREST APIを実行と、スレッドメッセージ一覧にエージェントから回答が含まれている。
なるほど、実行の流れはわかった。
ただ、各REST APIのレスポンスに含まれるID(エージェント、スレッド、実行、MCPツールのコール、等々)をコピーして、次のREST APIのパラメータに使うのがややだるい。
やっぱりプログラムで何とかしたい。
どうせ壁に当たるので、Pythonサンプルを何とかして動かすよりも、Javascriptの方が理解しやすいので、Javascriptでサンプルと同じ流れを作ってみた。
そして早速一番デカい壁に当たる。
最新の @azure/ai-agentsパッケージでも MCPツールに対応していない。
MCPツールの登録はエージェント作成の時に行うが、そのインターフェースが元々プレビューの機能なので、このチャレンジを始めた時点でSDKが対応していなかった。
しょうがないので、以下のSDKが対応していない処理はREST APIで代用。
- エージェント作成時のMCPツール登録
- MCPツール実行の承認
- 承認後のメッセージ取得
結果、SDKとREST APIが入り混じる、人様に見せられるものではないけど、自分の中ではかなり理解が深まった。
サンプルコード
作成したコードがこちらに、、、というタイミングで、@azure/ai-agentsのベータ版が公開され、なんとMCPツールに対応してしまった。
間が悪い。
なので、頑張ってREST APIとSDKがごちゃ混ぜになったコードはお蔵入りにして、改めてSDKのみで書き直し。
処理の基本の流れは、REST APIだろうがSDKだろうが特に変わりなし。
超重要な@azure/ai-agentsのバージョンは1.2.0-beta.1 。
package.json だとこんな感じ👇。
{
:
"dependencies": {
"@azure/ai-agents": "^1.2.0-beta.1",
"@azure/identity": "^4.12.0",
"comment-json": "^4.2.5",
"dotenv": "^17.2.2"
}
:
}
全くの好みだけど*2、エージェントを作るスクリプト create_agent.tsと、作ったエージェントに質問をするスクリプト ask_agent.ts の二つに分けた。
二つのスクリプトで使う環境変数は以下の三つ。
PROJECT_ENDPOINT:
Azure AI Foundry プロジェクトのエンドポイント
👇のような書式。
https://.services.ai.azure.com/api/projects/
MODEL_DEPLOYMENT_NAME:
エージェントが使うLLMモデルのデプロイ名。事前にデプロイされてる前提。
MCP_CONFIG_PATH:
MCPサーバの名前とURLを書いておくJSONファイル。
今回は.vscode/mcp.jsonの書式を読めるようにした。
けど . をMCPサーバ名に含められないので注意*3。
最初は、MCPツールを設定したエージェントを作るスクリプト。
といっても、半分は mcp.json をパースためのコードで、実質 AgentClient#createAgent するだけ。
一点だけ気になる点があって、ドキュメント上はAgentClient#createAgent の引数の中に toolResources が設定できることになっているけど、これは効果がなかったこと。
これが効くならMCPツールの事前承認ができるので、もう1つの質問用のスクリプトの実装がだいぶ楽になったんだけど、
REST APIでのエージェントの新規作成/更新時に toolResources の設定を追加してもダメだったので、どの言語のSDKでやってもダメだと予想。
SRあげるしかないのかな?
create_agent.ts
import { AgentsClient } from '@azure/ai-agents';
import { DefaultAzureCredential } from '@azure/identity';
import { parse, stringify } from 'comment-json';
import { readFile } from 'fs/promises';
import dotenv from 'dotenv'
dotenv.config();
const loadMCPToolFromConfigPath = async (mcpConfigPath: any) => {
const mcpConfigContent = await readFile(mcpConfigPath, 'utf8');
const mcpConfig = JSON.parse(stringify(parse(mcpConfigContent, null, true)));
return {
tools: Object.keys(mcpConfig.servers).map((serverLabel) => {
const server = mcpConfig['servers'][serverLabel];
return {
type: 'mcp',
serverLabel,
serverUrl: server.url
};
}),
toolResources:
{
'mcp': Object.keys(mcpConfig.servers).map((serverLabel) => {
return {
serverLabel,
requireApproval: 'never',
headers: {}
};
})
}
};
};
const main = async (options: { agent?: { name?: string; instructions?: string }, model: { name: string } }) => {
if (!process.env.PROJECT_ENDPOINT) {
throw new Error("Missing PROJECT_ENDPOINT environment variable");
}
if (!process.env.MODEL_DEPLOYMENT_NAME) {
throw new Error("Missing MODEL_DEPLOYMENT_NAME environment variable");
}
if (!process.env.API_VERSION) {
throw new Error("Missing API_VERSION environment variable");
}
if (!process.env.MCP_CONFIG_PATH) {
throw new Error("Missing MCP_CONFIG_PATH environment variable");
}
const modelDeploymentName = options.model.name;
const agentName = options?.agent?.name || `my-agent-${Date.now()}`;
const agentInstructions = options?.agent?.instructions || "You are a helpful assistant";
const { tools, toolResources } = await loadMCPToolFromConfigPath(process.env.MCP_CONFIG_PATH);
const client = new AgentsClient(process.env.PROJECT_ENDPOINT, new DefaultAzureCredential());
const agent = await client.createAgent(
modelDeploymentName,
{
name: agentName,
instructions: agentInstructions,
toolResources,
tools
}
);
return agent
};
main({
model: {
name: process.env.MODEL_DEPLOYMENT_NAME || "gpt-4o"
},
}).then((data: any) => {
console.log("Agent created:", data.id);
}).catch((err) => {
console.error("Error creating agent:", err);
});
もう1つが、👆で作成したエージェントに対してプロンプトを送信して、レスポンスを表示するスクリプト。
このSDKの使い方としては、プロンプトをエージェントに送信すると、エージェントからの応答はストリームを通してツラツラと流れてくるのでそれを取得する、というのが主流らしい。
(追記)
MCPツールを事前承認する方法がわかったので、ストリームの中断と再取得が不要なように質問用スクリプトを更新。
uncaughtexception.hatenablog.com
一方で、MCPツールを使う場合、プロンプトに回答する上でエージェントがMCPツールが必要と判断すると、MCPツール利用をユーザに承認してもらうために一度ストリームに流れる応答が止まって終了する。
ここで承認用のメソッド submitToolOutputs を実行すると、再度レスポンスのストリームがとれて、そこに応答の続きが流れてくるので、そこからMCPツールを使った回答を取得、というのが大まかな流れ。
ask_agent.ts
import {
Agent,
AgentsClient,
DoneEvent,
ErrorEvent,
MessageDeltaChunk,
MessageDeltaTextContent,
MessageStreamEvent,
RequiredToolCall,
RunsSubmitToolOutputsToRunOptionalParams,
RunStreamEvent,
SubmitToolApprovalAction,
ThreadRun
} from '@azure/ai-agents';
import { DefaultAzureCredential } from '@azure/identity';
import dotenv from 'dotenv'
dotenv.config();
const isThreadRun = (x: any): x is ThreadRun => {
return x && typeof x.id === "string";
}
const responseHandler = async (client: AgentsClient, threadId: string, agentOrThreadRun: Agent | ThreadRun) => {
const streamEventMessages = await (async () => {
return await (('requiredAction' in agentOrThreadRun)
? client.runs.submitToolOutputs(
threadId,
agentOrThreadRun.id,
[],
{
toolApprovals: (agentOrThreadRun.requiredAction as SubmitToolApprovalAction)?.submitToolApproval.
toolCalls.map((tc: RequiredToolCall) => ({ toolCallId: tc.id, approve: true })) || []
} as RunsSubmitToolOutputsToRunOptionalParams
)
: client.runs.create(
threadId,
agentOrThreadRun.id
)
).stream();
})();
let threadRun: ThreadRun | undefined = undefined;
for await (const eventMessage of streamEventMessages) {
switch (eventMessage.event) {
case RunStreamEvent.ThreadRunCreated:
console.debug(`ThreadRun status: ${(eventMessage.data as ThreadRun).status}`);
break;
case MessageStreamEvent.ThreadMessageDelta:
{
const messageDelta = eventMessage.data;
(messageDelta as MessageDeltaChunk).delta.content.forEach((contentPart) => {
if (contentPart.type === "text") {
const textContent = contentPart as MessageDeltaTextContent;
const textValue = textContent.text?.value || "";
process.stdout.write(textValue);
}
});
}
break;
case RunStreamEvent.ThreadRunCompleted:
process.stdout.write("\n");
console.debug("Thread Run Completed");
break;
case ErrorEvent.Error:
console.debug(`An error occurred. Data ${eventMessage.data}`);
break;
case RunStreamEvent.ThreadRunRequiresAction:
console.debug("Tool approval required.");
if (!isThreadRun(eventMessage.data)) {
console.warn("Received ThreadRunRequiresAction but event data is not a ThreadRun:", eventMessage.data);
continue;
}
threadRun = eventMessage.data;
break;
case DoneEvent.Done:
console.debug("Stream completed.");
break;
}
}
return threadRun;
}
const main = async () => {
if (!process.env.PROJECT_ENDPOINT) {
throw new Error("Missing PROJECT_ENDPOINT environment variable");
}
const client = new AgentsClient(process.env.PROJECT_ENDPOINT, new DefaultAzureCredential());
const agentId = process.argv[2];
if (!agentId) {
throw new Error("Please provide the agent ID as a command line argument");
}
const ask = process.argv[3] || "Please explain the spec of Azure AI Services.";
const agent = await client.getAgent(agentId);
const thread = await client.threads.create();
const message = await client.messages.create(
thread.id,
"user",
ask,
);
let threadRun: ThreadRun | undefined;
do {
threadRun = await responseHandler(client, thread.id, threadRun ? threadRun : agent);
} while (threadRun);
};
main().catch((err) => {
console.error("Error in conversation:", err);
});
エージェント作成のスクリプトのところに書いた、ツールの事前承認ができれば、応答途中の承認を考えないで済むので、もっと楽な実装になるんだけど、プレビューのせいか、そうもいかない様子。
さて、Pythonのサンプルコードが動かなかった件、Javascriptでもプレビュー対応したライブアリが必要だったので、「もしや」と思ったら、ドンピシャだった。
Previewに対応した
azure.ai.projects==1.1.0b4
azure.ai.agents==1.2.0b4
の二つをインストールしたらいいだけ。
そういうのは書いとけよ*4。
まったく、、、そういう(ry