APS を使ったカスタム MCP サーバーの構築

Model Context Protocol (MCP) は、Claude Desktop、VS Code、カスタムチャット UI などの AI クライアントが外部データやサービスにアクセスする方法を定義するオープンスタンダードです。これにより、大規模言語モデルがツールの発見・呼び出し、リソースの読み取り、プロンプトテンプレートの使用を、明確に定義されたトランスポートに依存しないプロトコルを通じておこなえます。

この記事では、JavaScript、Python、.NET を用いた実際の参照実装で使用されているパターンを参考に、Autodesk Platform Services (APS) と統合する MCP サーバーを構築するためのベストプラクティスを解説します。


セットアップ

公式 MCP SDK

公式 MCP SDK は、すべての言語において推奨される出発点です。:

言語パッケージ
JavaScript/TypeScript@modelcontextprotocol/sdkaps-mcp-app-example
C# / .NETModelContextProtocolaps-aecdm-mcp-dotnet
Pythonfastmcp(公式 SDK をベースに構築))aps-mcp-server-python

公式 SDK は、メッセージフレーミング、トランスポート抽象化、ツール/リソース登録フック、セッション管理といったコアプロトコル実装を提供します。プロトコルの基盤となる処理は SDK が担うため、開発者はドメインロジックに集中できます。

JavaScript

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
const server = new McpServer({
name: "My APS MCP Server",
version: "1.0.0",
});

.NET

var builder = Host.CreateEmptyApplicationBuilder(settings: null);
builder.Services.AddMcpServer()
.WithStdioServerTransport()
.WithToolsFromAssembly();
var app = builder.Build();
await app.RunAsync();

Python

Python の場合、FastMCP が公式 SDK 上に高レベルの便利なレイヤーを提供します。これにより、定型コードが大幅に削減されます。デコレータベースの APIを 使用すると、わずか数行でツール、リソース、プロンプトを登録できます。:

from fastmcp import FastMCP
mcp = FastMCP(
"My APS MCP Server",
)

FastMCP は、サーバーの初期化、型ヒントからのツール スキーマ生成、トランスポートの設定などを処理するため、迅速な反復開発が求められる場合に有力な選択肢となります。


ステートレス サーバー vs. ステートフル サーバー

MCP サーバーはステートレスstateless)、またはステートフルstateful)のいずれかであり、その選択はアーキテクチャの設計方法に影響を与えます。

ステートレス

ステートレスサーバーは、受信リクエストごとに新しい McpServer インスタンスを作成します。リクエスト間のセッションの継続性はなく、共有メモリも蓄積されたコンテキストもありません。これは最もシンプルなモデルであり、次のような場合に適しています。:

  • 各リクエストが独立しているリモート環境におけるマルチテナント デプロイ
  • ユーザーセッションを追跡する必要のないサーバー
  • ロードバランサーの背後で水平方向に拡張可能なサービス

aps-mcp-app-example (JavaScript)はこのパターンを示しています。受信したHTTP リクエストごとに新しい McpServer インスタンスが作成され、トランスポートに接続され、リクエストが処理され、その後すべてが破棄されます。:

app.all("/mcp", async (req, res) => {
const server = createMcpServer(options);
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined, // No sessions
});
res.on("close", () => {
transport.close();
server.close();
});
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
});

sessionIdGenerator: undefinedを設定すると、サーバーがステートレスであることを明示的に示します。

ステートフル

ステートフル サーバーは、リクエスト間でセッション状態を維持します。これは、次のような場合に必要となります。 :

  • ユーザーごとの認証トークン(例:3-legged OAuth)を追跡する必要がある。
  • サーバーは会話を通じてコン​​テキストを蓄積する。
  • 単一のクライアントに接続されたローカル STDIO サーバーを実行する。

aps-mcp-server-python 3-legged サンプルは、ステートフルなセッション管理を示しています。このサンプルは、MCP セッションコンテキスト (ctx) を使用して、ツール呼び出し間で OAuth トークンを保存および取得します。 :

@mcp.tool()
async def list_hubs(ctx: Context) -> list[dict] | dict:
token = await _get_valid_token(ctx)
if not token:
return {
"auth_required": True,
"auth_url": _build_auth_url(ctx.session_id),
"message": "Open auth_url in a browser to authenticate, then call list_hubs again."
}
return await _list_hubs(token)

aps-aecdm-mcp-dotnet (.NET) のサンプルもステートフルです。トークンをグローバル ステートに保存し、WebSocket を介してローカルで起動された Viewer と通信します。


トランスポート

MCP は、トランスポート層をプロトコル自体とは別に定義します。主なトランスポート方式は、STDIOStreamable HTTP の2つです。

STDIO

STDIO トランスポートは、標準入出力ストリームを介して通信します。MCP クライアントは、サーバーを子プロセスとして起動し、stdinstdout を介して JSON-RPC メッセージを送受信します。

// .NET example: STDIO transport
builder.Services.AddMcpServer()
.WithStdioServerTransport()
.WithToolsFromAssembly();

STDIO は次のような用途に最適です。:

  • ローカル開発およびテスト – ネットワーク接続不要
  • デスクトップ クライアント – Claude Desktop などのデスクトップ クライアントがサーバーをサブプロセスとして起動
  • シングル ユーザー シナリオ – サーバープロセス毎に 1 クライアント

Claude Desktop の設定は通常、次のようになります。:

{
"mcpServers": {
"my-server": {
"command": "dotnet",
"args": ["run", "--project", "/path/to/mcp-server.csproj"]
}
}
}

Streamable HTTP

その他のすべての利用例、特にリモート デプロイマルチユーザー サーバーWeb ベースのクライアントでは、Streamable HTTP の利用が推奨されます。Streamable HTTP は、MCP サーバーを HTTP エンドポイントとして実行し、通常は /mcp パスで動作します。

// JavaScript example: Streamable HTTP with Express
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
# Python (FastMCP): Streamable HTTP
mcp.run(transport="streamable-http", host="0.0.0.0", port=5000)
# Or via CLI:
# uv run fastmcp run server.py --transport streamable-http --port 5000

Streamable HTTP の利点:

  • ネットワーク境界を越えて動作(異なるマシン上のクライアントとサーバーに対応)。
  • 複数クライアントの同時接続をサポート
  • 標準的な Web インフラストラクチャ(ロードバランサー、リバースプロキシ、CORS)と互換性があり
  • HTTP ヘッダーによるセッション管理が可能

認証

APS と連携する MCP サーバーの認証には、2 つの異なるレイヤーが関わっています。それぞれのレイヤーをいつ、どのように使用するかを理解することが重要です。

レイヤー1:MCP クライアントと MCP サーバー間の認証

このレイヤーは、MCP サーバーに接続できるユーザーを制御します。STDIO ベースのローカルサーバーの場合、これは暗黙的です。プロセスを実行しているローカルユーザーのみが接続できます。HTTP ベースのサーバーの場合は、次の実装が必要になる場合があります。:

  • 受信リクエストにおける API キーまたはトークンの検証
  • OAuth ベースのクライアント認証

レイヤー2:MCP サーバーと APS 間の認証

ここでは、サーバーが Autodesk Platform Services で認証を行い、アプリまたはユーザーに代わって APS API を呼び出します。APS は主に 3 つの認証方式をサポートしており、それぞれ異なるシナリオに適しています。:

2-Legged OAuth

最もシンプルな方法です。サーバーは自身の Client ID と Client Secret を使用して、アプリ(識別子)に紐づいたトークンを取得します。ユーザー コンテキストは一切関与しません。

async with httpx.AsyncClient() as client:
response = await client.post(
APS_TOKEN_URL,
data={
"grant_type": "client_credentials",
"scope": "bucket:read data:read",
},
auth=(APS_CLIENT_ID, APS_CLIENT_SECRET),
)
data = response.json()

使用場面:アプリが所有するリソース(ユーザーコンテキストは不要)。例えば、アプリンが所有する OSS Bucket へのアクセスなど。

Secure Service Accounts

SSA(Secure Service Accounts) は、ユーザーのサインイン操作なしに 3-legged トークンを取得できる Autodesk ID(オートデスク アカウント)であり、ユーザーコンテキスト API(Data Management API など)を必要とする自動化されたワークフローに最適です。サーバーは 署名付き JWT アサーションを作成してアクセス トークンと交換します。:

payload = {
"iss": APS_CLIENT_ID,
"sub": APS_SSA_ID,
"aud": APS_TOKEN_URL,
"exp": now + 300,
"scope": ["data:read"],
}
assertion = jwt.encode(payload, SSA_PRIVATE_KEY, algorithm="RS256",
headers={"kid": SSA_KEY_ID})
// JavaScript equivalent
const assertion = jwt.sign(payload, serviceAccountPrivateKey, {
algorithm: "RS256",
header: { alg: "RS256", kid: serviceAccountKeyId },
});

使用場面:自動化されたサーバー間ワークフロー。サービス アカウントは、対象リソースへのアクセス権限を事前に取得しておく必要があります。

3-Legged OAuth

OAuth 2.0 の認証コードフロー全体。ユーザーは明示的にサインイン、アクセスに同意します。サーバーはコールバックを介してコードを受け取り、アクセストークンとリフレッシュトークンと交換します。

@mcp.custom_route("/callback", methods=["GET"])
async def oauth_callback(request: Request) -> HTMLResponse:
code = request.query_params.get("code")
session_id = request.query_params.get("state")
tokens = await _exchange_code(code)
_pending_tokens[session_id] = {
"access_token": tokens["access_token"],
"refresh_token": tokens.get("refresh_token"),
"expires_at": time.time() + tokens.get("expires_in", 3600),
}
return HTMLResponse("Authentication successful! You can close this window.")

使用場面:明示的な同意を得た上で実際のユーザーに代わって操作する場合。ユーザー毎のトークンを追跡するには、セッションはステートフルである必要があります。

どれを使うべきか

アプローチユーザー操作トークン タイプ最適な用途
2-Legged (Client Credentials)なしアプリケーション トークンアプリ所有のリソース、ユーザーコンテキストなし
Secure Service Accountsなしユーザーコンテキスト トークンユーザーコンテキスト API を必要とする自動化ワークフロー
3-Legged (Authorization Code)サインイン操作ユーザー トークン同意を得た実際のユーザーに代わってアクション

ベスト プラクティス

  • トークンを常にキャッシュし、有効期限が切れるまで再利用してください。3 つのリファレンス実装はすべて、有効期限チェック付きのインメモリキャッシュを使用しています。:
if _token_cache["access_token"] and time.time() < _token_cache["expires_at"] - 60:
return _token_cache["access_token"]
  • トークンは積極的に更新する。API 呼び出しを行う前に、有効期限が切れていないか(例えば 60 秒の猶予期間を設けて)確認してください。
  • 認証情報はコードから分離する。Client ID、Client Secret、SSA キーは環境変数(.envファイル)を使用してください。
  • 要求するスコープを最小限に抑える。ツールが実際に必要とする OAuthス コープ(例:data:read、bucket:read)のみを要求してください。

ツール

MCP ツールとは何か?

ツールとは、LLM(Large Language Model、大規模言語モデル)がアクションを実行したりデータを取得したりするために呼び出すことができる機能です。これは、MCP サーバーが機能を公開する主要な方法です。各ツールには次の機能があります。:

  • ツールの名前説明(LLM が呼び出しタイミングを決定するために使用)
  • パラメータを定義する入力スキーマ
  • ロジックを実行するコールバック
  • オプションの注釈(例:readOnlyHint はツールが状態を変更しないことを示す)

ツールの実装

Python (FastMCP) — デコレータベース

FastMCP は、関数シグネチャとドキュメント文字列からツールスキーマを推論します。:

@mcp.tool()
async def list_buckets() -> list[dict]:
"""List all OSS buckets owned by the configured APS application."""
token = await _get_access_token()
return await _list_oss_buckets(token)
@mcp.tool()
async def list_objects(bucket_key: str) -> list[dict]:
"""List objects stored in a specific OSS bucket.
Args:
bucket_key: The unique key identifying the OSS bucket.
"""
token = await _get_access_token()
return await _list_oss_objects(token, bucket_key)

JavaScript — Zod スキーマを使用したファクトリー パターン

aps-mcp-app-example はファクトリーパターンを使用しており、各ツールはファクトリー関数をエクスポートするモジュールです。入力スキーマは Zod を使用して定義されています。:

import z from "zod";
export const getProjectContentsToolFactory = ({}) => ({
name: "get-project-contents",
config: {
title: "Get project contents",
description: "Retrieves top-level folders in a project or contents of a specified folder.",
inputSchema: {
accountId: z.string().nonempty().describe("The ID of the account."),
projectId: z.string().nonempty().describe("The ID of the project."),
folderId: z.string().optional().describe("The ID of the folder."),
},
annotations: { readOnlyHint: true },
},
callback: async ({ accountId, projectId, folderId }) => {
// ... tool implementation
},
});

ツールはサーバー設定時に動的に登録されます。:

for (const toolFactory of Object.values(tools)) {
const { name, config, callback } = toolFactory(options);
registerAppTool(server, name, config, callback);
}

.NET — 属性ベースの登録

.NET SDK は属性と自動検出を使用します。静的クラスに [McpServerToolType] を、各ツールメソッドに [McpServerTool] を注釈として付加します。:

[McpServerToolType]
public static class AECDMTools
{
[McpServerTool, Description("Get the ACC hubs from the user")]
public static async Task<string> GetHubs()
{
// ... tool implementation
}
[McpServerTool, Description("Get the ACC projects from one hub")]
public static async Task<string> GetProjects(
[Description("Hub id to query the projects from")] string hubId)
{
// ... tool implementation
}
}

サーバーは WithToolsFromAssembly() を使用して、アセンブリ内のすべてのツールを自動的に検出します。

content vs. structuredContent の返却

MCP ツールは、content(人間が可読なテキスト、または LLM 用のイメージ)とstructuredContent(プログラム処理用のマシンが機械可読 JSON)の両方を返すことができます。aps-mcp-app-example は、一貫して両方を使用しています。:

callback: async ({ projectId, designId, region }) => {
// ... fetch data
return {
structuredContent: props.data, // Full JSON for MCP Apps / UI rendering
content: [{
type: "text",
text: `Found properties containing ${props.data.collection.length} elements`,
}],
};
}
  • content: LLM が読み込み、推論するコンテンツ ブロック(テキスト、画像など)の配列。簡潔かつ分かりやすく記述してください。
  • structuredContent: プログラムから利用できる JSONオ ブジェクト。例えば、3D Viewerやデータ テーブルを表示する MCP アプリの UI などで使用出来ます。

ツールがリッチデータを UI コンポーネントに渡す必要がある場合、またはクライアントがレスポンスをプログラムで処理する必要がある場合は、structuredContentを使用してください。レスポンスが主に LLM によって解釈され、ユーザーに伝達される場合は、contentを使用してください。


追加リソース

※ 本記事は Building Custom MCP Servers with Autodesk Platform Services | Autodesk Platform Services から転写・意訳・補足したものです。

Discover more from Autodesk Developer Blog

Subscribe now to keep reading and get access to the full archive.

Continue reading