以下の内容はhttps://uncaughtexception.hatenablog.com/entry/2025/09/24/082834より取得しました。


Azure AI Foundry Agent ServiceのPythonサンプルが動かせなかったのでNode.jsでMCPツールを組み込んでみた

前回はMPCサーバをAzure Functionsで作ることを試したけど、今度はMCPサーバを使う側を試してみる。

試すのは、Azure AI FoundryのAgent ServiceからのMCPサーバの呼び出し。
ちなみにまだプレビュー。

learn.microsoft.com

サンプルを試す。

サンプルは、SDKを使ったPythonコードと、curlコマンドを使ったREST API実行の2つ。

learn.microsoft.com

Pythonのサンプルを試す。

今まで雰囲気でPython使っていたので、このページだけだと何から始めたらいいかピンとこないけど、少なくとも from ... import に書かれている azure.identity azure.ai.projects azure.ai.agents くらいは入れないといけないはず。

入れてみた。

$ pip install azure.identity azure.ai.projects azure.ai.agents

あとはコードをマルっとコピーして、環境変数をいくつか設定して実行。

。。。

動かない。
肝心の McpTool がインポートできない、と言われた。

Python詳しくないので、早々に諦める。

REST APIのサンプルを試す

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をしたらエージェントの作成は成功。

その後は、

  1. スレッドの作成
  2. スレッドへのメッセージ(質問文)の追加
  3. エージェントを指定してスレッドを実行
  4. 実行の状態を取得

というステップでcurlREST APIを実行していき、無事成功。

すると、実行の状態がMCPツールの実行承認待ち、ということになっているのがわかるので、MCPツールの実行を承認するREST APIを実行と、スレッドメッセージ一覧にエージェントから回答が含まれている。

なるほど、実行の流れはわかった。

ただ、各REST APIのレスポンスに含まれるID(エージェント、スレッド、実行、MCPツールのコール、等々)をコピーして、次のREST APIのパラメータに使うのがややだるい。

やっぱりプログラムで何とかしたい。

Javascriptで試す

どうせ壁に当たるので、Pythonサンプルを何とかして動かすよりも、Javascriptの方が理解しやすいので、Javascriptでサンプルと同じ流れを作ってみた。

そして早速一番デカい壁に当たる。

最新の @azure/ai-agentsパッケージでも MCPツールに対応していない。
MCPツールの登録はエージェント作成の時に行うが、そのインターフェースが元々プレビューの機能なので、このチャレンジを始めた時点でSDKが対応していなかった。

しょうがないので、以下のSDKが対応していない処理はREST APIで代用。

  • エージェント作成時のMCPツール登録
  • MCPツール実行の承認
  • 承認後のメッセージ取得

結果、SDKREST APIが入り混じる、人様に見せられるものではないけど、自分の中ではかなり理解が深まった。

サンプルコード

作成したコードがこちらに、、、というタイミングで、@azure/ai-agentsのベータ版が公開され、なんとMCPツールに対応してしまった。
間が悪い。

なので、頑張ってREST APISDKがごちゃ混ぜになったコードはお蔵入りにして、改めて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 の二つに分けた。

二つのスクリプトで使う環境変数は以下の三つ。

  1. PROJECT_ENDPOINT:
    Azure AI Foundry プロジェクトのエンドポイント
    👇のような書式。

    https://.services.ai.azure.com/api/projects/

  2. MODEL_DEPLOYMENT_NAME:
    エージェントが使うLLMモデルのデプロイ名。事前にデプロイされてる前提。

  3. 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();

// load the mcp.json as MCP tool array
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();

// check if the object is ThreadRun
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, // threadId
        agentOrThreadRun.id, // runId
        [], // toolOutputs
        {
          toolApprovals: (agentOrThreadRun.requiredAction as SubmitToolApprovalAction)?.submitToolApproval.
            toolCalls.map((tc: RequiredToolCall) => ({ toolCallId: tc.id, approve: true })) || []
        } as RunsSubmitToolOutputsToRunOptionalParams // options
      )
      : client.runs.create(
          threadId,
          agentOrThreadRun.id  // agentId
      )
    ).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.";

  // retrieve the agent
  const agent = await client.getAgent(agentId);

  // create a new thread
  const thread = await client.threads.create();

  // send a user message to the agent
  const message = await client.messages.create(
    thread.id,
    "user",
    ask,
  );

  let threadRun: ThreadRun | undefined;
  do {
    // start a new run and stream events
     threadRun = await responseHandler(client, thread.id, threadRun ? threadRun : agent);
  } while (threadRun);
};

main().catch((err) => {
  console.error("Error in conversation:", err);
});

エージェント作成のスクリプトのところに書いた、ツールの事前承認ができれば、応答途中の承認を考えないで済むので、もっと楽な実装になるんだけど、プレビューのせいか、そうもいかない様子。

Pythonのサンプル、その後。

さて、Pythonのサンプルコードが動かなかった件、Javascriptでもプレビュー対応したライブアリが必要だったので、「もしや」と思ったら、ドンピシャだった。
Previewに対応した

  • azure.ai.projects==1.1.0b4
  • azure.ai.agents==1.2.0b4

の二つをインストールしたらいいだけ。

そういうのは書いとけよ*4

まったく、、、そういう(ry


*1:まとめて https://github.com/MicrosoftDocs/azure-ai-docs/pull/546 でプルリクエスト提出済み。

*2:毎回エージェントを作って消す、をするのが嫌だった

*3:Azure AI Foundry Agent Serviceの仕様?

*4:*1👆のプルリクエストに混ぜた




以上の内容はhttps://uncaughtexception.hatenablog.com/entry/2025/09/24/082834より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14