はじめに
こんにちは!Algomaticネオセールスカンパニーでソフトウェアエンジニアをしている越川と申します。
先日、Azure FunctionsがMCPに対応したとのニュースがありました。 techcommunity.microsoft.com
弊カンパニーではインフラ基盤にAzureを利用しており、MCP対応は非常にいいニュースだと思っています。
早速試してみたんですが、toolの定義自体は数十行のコードで完結するのでその記述量の少なさに感動しました。
app.mcpTool('getCompanyInfo', { toolName: TOOL_NAME, description: TOOL_DESCRIPTION, toolProperties: { corporateNumber: z.string().describe(CORPORATE_NUMBER_PROPERTY_DESCRIPTION) }, handler: getCompanyInfo });
Cursor上の設定もラクで、自然言語でデータを引っ張ってきてくれる体験がとてもよかったです。
※ 自然言語でCursorにチャットを送って、DBからデータを引いてきている様子
今回の記事では、実際にAzureFunctionsをMCPサーバーとしてデプロイするところまで試してみましたので、得られた知見をシェアしたいと思います。
ポイント
- MCPツールの定義はたった数行のコードで可能
- MCPサーバーをcallするための設定もjsonで数行でできる
何をつくるか
公式のサンプルレポジトリをベースに実装をすすめます。
このレポジトリには予めいくつかの機能が定義済みなのですが、折角なので自分で機能を追加してみます。
実装
法人番号を受け取り、該当する企業情報をCosmosDBから検索して返す機能を実装してみます。
MCPサーバーのエントリポイントの作成
remote-mcp-functions-typescript/src/functions/getCompanyInfo.ts
を作成し、以下のコードを書きます。
import { app, InvocationContext } from "@azure/functions"; import { CosmosClient } from "@azure/cosmos"; import { z } from "zod"; const cosmosEndpoint = process.env.COSMOS_DB_ENDPOINT const cosmosKey = process.env.COSMOS_DB_KEY const databaseId = process.env.COSMOS_DB_DATABASE_ID const containerId = process.env.COSMOS_DB_CONTAINER_ID // MCP Tool実装 export async function getCompanyInfo(_message: unknown, context: InvocationContext): Promise<any> { console.info('企業情報取得を開始します'); // MCPツールの引数を取得 const mcptoolargs = context.triggerMetadata.mcptoolargs as { corporateNumber?: string }; const corporateNumber = mcptoolargs?.corporateNumber; console.info(`法人番号: ${corporateNumber}`); // 空文字列や undefined も弾く if (!corporateNumber || corporateNumber === "undefined" || corporateNumber === '') { return { error: "法人番号(corporateNumber)が指定されていません。" }; } try { const cosmosClient = new CosmosClient({ endpoint: cosmosEndpoint, key: cosmosKey }); const container = cosmosClient.database(databaseId).container(containerId); if (!container) { return { error: "CosmosDB接続が設定されていません。環境変数 COSMOS_ENDPOINT と COSMOS_KEY を確認してください。" }; } const querySpec = { query: "SELECT * FROM c WHERE c.corporateNumber = @corporateNumber", parameters: [{ name: "@corporateNumber", value: corporateNumber.toString() }] }; const { resources: results } = await container.items.query(querySpec).fetchAll(); if (results.length === 0) { return { error: `法人番号「${corporateNumber}」に該当するデータが見つかりませんでした。` }; } return results[0]; } catch (error) { console.error(`Error getting company info: ${error}`); return { error: "サーバーエラーが発生しました。" }; } } app.mcpTool('getCompanyInfo', { toolName: 'getCompanyInfo', description: '法人番号を指定し、自社DBから企業情報を取得します。', toolProperties: { corporateNumber: z.string().describe('取得したい企業の法人番号') }, handler: getCompanyInfo });
app.mcpTool()
で、MCPトリガーとして動作するエンドポイントを登録しています。
toolProperties
で定義した情報は、CursorなどのMCPクライアントがツールを認識し、ユーザーが引数を入力する際の補助情報として利用されます。
ここでは、getCompanyInfo
というツール名で法人番号を渡すと企業情報を返す処理を持つMCPサーバーを作成しています。
CursorのChatから「 6010401173850 の企業情報を教えて」といったやり取りを受けた際、Cursorは登録された getCompanyInfo
ツールを呼び出します。
リクエストに含まれる corporateNumber
の値は context.triggerMetadata.mcptoolargs
を介してAzure Functionsに渡され、これを用いてCosmos DBから該当企業の情報を検索して返します。
ローカル環境で動かす
docker desktopを立ち上げて、remote-mcp-functions-typescriptのディレクトリに移動し、以下のコマンドを実行します。
docker run -p 10000:10000 -p 10001:10001 -p 10002:10002 mcr.microsoft.com/azure-storage/azurite
次に、別のターミナルタブを開いて以下のコマンドを順番に実行します
npm install npm run build func start
これでMCPサーバーが立ち上がります。ターミナルのログに、
Functions: getCompanyInfo: mcpToolTrigger getSnippet: mcpToolTrigger hello: mcpToolTrigger mcpToolTriggerWithArguments: mcpToolTrigger mcpToolTriggerWithZod: mcpToolTrigger saveSnippet: mcpToolTrigger For detailed output, run func with --verbose flag.
のように表示されていればエラーなく起動できています。
MCPホスト/クライアント(Cursor)の設定
1. .cursor/mcp.jsonの作成
MCPサーバーとの接続設定をするために、mcp.jsonファイルを作成します。
{ "mcpServers": { "my-local-mcp-server": { "url": "http://0.0.0.0:7071/runtime/webhooks/mcp/sse" } } }
参考: docs.cursor.com
2. settingの確認
画面左上の Cursor をクリックし、 Preferences → Cursor Settings を開きます。
Cursor Settingsの画面に MCP という項目があるのでクリックすると、以下の表示になります。
ローカル環境を立ち上げた上で、Disabledのところをクリックすると、Enabledに変わり、以下の表示になります。
緑の丸印が出ていれば、ローカルのMCPサーバーと疎通できる状態になっています。
なんらか不具合があると、赤の丸印が表示されます。その際はエラーメッセージを頼りにトラブルシュートする必要があります。
緑の丸印が表示されている状態で、「6010401173850 の企業情報を教えて」のように Chatを送ると、toolを実行するか聞かれるので、 Run tool を押しましょう。
Run toolを押した後、以下のように企業情報が自然言語で返ってくれば成功です。
デプロイ
通常のAzureFunctionsと同じく、azdコマンドでデプロイが可能です。
デプロイに成功すると、Azure Portal の functionsの画面に デプロイに成功した関数が表示されます。
デプロイ時の注意
- 対応リージョン: デプロイ対象のAzure Functionsのリージョンは、MCPサーバー機能が利用可能なリージョンを指定する必要があります。本記事執筆時点では、
eastus
,eastus2
,westus2
,northeurope
,uksouth
,australiaeast
,eastasia
,southeastasia
,southcentralus
,swedencentral
,eastus2euap
などが対応しています。
参考: github.com
認証キー: Azure FunctionsのHTTPトリガーに認証キー (Function KeyやHost Key) を設定している場合、Cursorなどのクライアント側でもリクエストヘッダーに
x-functions-key
を含める必要があります。Cursorの場合、mcp.json
のMCPサーバー設定にheaders
フィールドを追加します。{ "mcpServers": { "mcp-server-prd": { "url": "https://<your-function-app-name>.azurewebsites.net/runtime/webhooks/mcp/sse", "headers": { "x-functions-key": "<your-function-key>" } } } }
まとめ
Azure Functionsを使ってMCPサーバーを簡単に構築できるようになったことで、開発者はMCPサーバーのビジネスロジック実装に集中できることがメリットかと思います。
公式のレポジトリをcloneしてローカル環境で動かす分にはAzureアカウントも不要なので、ぜひ試してみてください。
2025/4/9時点で本機能はbeta版ですので本番投入はさすがに腰が重たいですが、GAした際にはガンガン利用しようと思います。
補足
authLevel
設定について (HTTPトリガーとの違い)
通常のAzure FunctionsのHTTPトリガー (app.http
) では、以下のように authLevel
オプションを指定して認証レベル(例: function
, anonymous
, admin
)を設定できます。
app.http("myHttpFunction", { methods: ["GET", "POST"], authLevel: "function", // 関数キーが必要 handler: myHandler });
しかし、本記事で利用しているMCPトリガー (app.mcpTool
) の定義には、現時点でこの authLevel
オプションは存在しません。
MCPエンドポイントの認証は、app.mcpTool
の定義内ではなく、Function App全体の認証設定(ホストキーや関数キー) によって制御されます。
したがって、MCPサーバーへのアクセスに認証(関数キーなど)を要求したい場合は、app.mcpTool
に設定を記述するのではなく、Function Appのキーを取得し、クライアント(Cursorなど)からのリクエスト時に x-functions-key
ヘッダーでそのキーを送信する必要があります。これは本文中の「デプロイ時の注意」で説明している方法です。
エンジニアのみなさん、カジュアル面談させてください
LLM活用に興味があるエンジニアの方、ぜひ弊カンパニーCTOの菊池とカジュアル面談しましょう!