MCPサーバーをPythonで自作する:注文照会から認証・公開まで
PythonでMCPサーバーを自作し、読み取り専用の注文照会をMCP Inspectorで検証。Claude CodeとCursorへの接続から、OAuth認証付きHTTPサービスの構築、RenderやCloudflare Workersへの公開までを、コードと料金例で解説します。権限設計とログ管理も確認できます。
公開日

MCPサーバーを使えば、社内のレコードをチャットに貼り付けなくても、Claude CodeやCursorに注文状況を確認させられます。まずは読み取り専用のMCPツールを一つ作り、Inspectorで動作を確かめましょう。そのうえで、ローカルプロセスとして使うか、認証付きのHTTPサービスとしてチームで共有するかを選びます。
最初に作るサーバーの役割は、注文IDを受け取り、その注文の状況を返すだけで十分です。ここから始めましょう。汎用のデータベースツールを渡すと、モデルが判断しなければならないことが増え、このワークフローに必要な範囲を超えるアクセス権まで与えてしまいます。
この手順は、現行の公式サーバーチュートリアルに沿っています。2026年10月7日時点で、ドキュメントが採用しているのはMCP仕様2026-07-28と、MCPのメッセージ処理を担う公式Python SDKのMCPServer APIです。この記事ではSDK 2.3.0にバージョンを固定します。古いチュートリアルのインポート文に合わせた結果、意図しないバージョンがインストールされるのを防ぐためです。
MCPサーバーで公開する機能を整理する
MCPサーバーは、AIアプリケーションと自社システムの間に置く、対応範囲を限定した窓口と考えるとわかりやすいです。アプリケーションは利用できる機能を確認し、処理を依頼して、結果を受け取ります。その窓口で何を受け付けるかは、サーバー側のコードで決めます。
MCPはModel Context Protocolの略で、このやり取りに共通の形式を与えます。ホストは、Claude CodeやCursorなど、利用者が操作するアプリケーションです。ホストのクライアントが、サーバーとのプロトコル上の通信を担当します。これらは役割を表す言葉であり、別のアプリケーションを三つ追加でインストールするという意味ではありません。
ツールは読み取り専用にもできます。「リソース」と名付ければアクセスチェックが不要になるわけではありません。プロンプトが与えるのは指示であり、権限ではありません。対応機能や表示方法はクライアントによって異なるため、実際に公開する機能を確認してください。以上がサーバーの三つの基本機能です。このあとのサーバーで使うのは、ツールだけです。

開発に入る前に、保守されている既存のコネクターで目的を満たせないか確認しましょう。2026年のおすすめMCPサーバーが、その出発点になります。独自のサーバーを作る価値があるのは、社内データ、権限のルール、ワークフローが既存のコネクターと合わない場合です。
Pythonで読み取り専用の注文照会を作る
プロトコルの処理は公式SDKに任せ、自分のコードは注文照会に集中させます。必要なのはPython 3.10以上、公式チュートリアルで使われているPythonプロジェクト管理ツールのuv、そしてInspector用のNode.jsです。現行のInspectorにはNode 22.19.0以上が必要です。
ターミナルで、次のコマンドを順に実行します。
uv init orders-mcpcd orders-mcpuv venvuv add "mcp[cli]==2.3.0"
そのフォルダーにorders.pyを作り、次のコードを貼り付けてください。これだけでサーバー全体が動きます。レコードは学習用の架空データで、顧客名、支払い情報、APIの認証情報は含みません。
import json
from mcp.server import MCPServer
mcp = MCPServer("orders")
# Fictional training data. No customer records or credentials.
ORDERS = {
"A100": {"status": "shipped", "carrier": "Demo Courier"},
"A101": {"status": "packing", "carrier": "not assigned"},
}
@mcp.tool()
def lookup_order(order_id: str) -> str:
"""Look up a fictional order by ID, such as A100. Read-only.
Args:
order_id: Exact order ID, for example A100 or A101.
"""
key = order_id.strip().upper()
order = ORDERS.get(key)
if order is None:
return json.dumps({"found": False, "order_id": key})
return json.dumps({"found": True, "order_id": key, **order})
if __name__ == "__main__":
mcp.run(transport="stdio")公式チュートリアルに記載されたMCPServer、@mcp.tool()、mcp.run(transport="stdio")の構成を使い、天気のツールを注文照会に置き換えています。文字列の型ヒントによって、SDKはorder_idが必須のテキスト入力だと判断します。docstringは、どのような場面でこのツールを使うかをクライアントに伝えます。ツールの定義とプロトコルのメッセージ処理はSDKが担当します。
uv run orders.pyで起動します。何も表示されず、入力を待っている状態なら正常です。stdioは標準入力と標準出力のことで、クライアントはこの経路を通じてプロセスと通信します。クライアントにサーバーを起動させる前に、この手動で起動したプロセスは停止してください。
アプリケーションのログを標準出力に流してはいけません。デフォルトで標準エラー出力に書き込むPythonのloggingモジュールを使いましょう。不用意なprint()がプロトコルの通信を壊すことがあります。これはログの見た目の好みではなく、ドキュメントに明記されたstdioの制約です。
辞書をデータベースに置き換えるときも、入力と機能の範囲は狭く保ちます。パラメーター化したクエリで照会し、必要なフィールドだけを読み取れるデータベースアカウントを使い、レコードを返す前に呼び出し元の権限を確認してください。この用途で、モデルから任意のSQL文を受け取ってはいけません。
MCP Inspectorで動作を検証する
モデルに使わせる前に、ツール自体が動くことを確かめます。プロジェクトのフォルダーでuv run mcp dev orders.pyを実行してください。このSDKの開発用コマンドでMCP Inspectorが起動します。コマンドが表示したブラウザー用URLを開き、まだ接続されていなければサーバーに接続します。
Toolsでlookup_orderを選びます。フォームには、必須のorder_idフィールドが表示されるはずです。A100で呼び出すと、結果にfound: true、status: shipped、carrier: Demo Courierが含まれます。A101ではpackingが返ります。DOES-NOT-EXISTなら、結果にfound: falseが含まれることを確認してください。
order_idを指定しないリクエストも試します。注文を照会する前に、入力の検証で失敗するのが正しい動作です。InspectorのProtocolビューとConsoleビューを使うと、リクエストの形式が不正なのか、サーバープロセスに問題があるのかを切り分けられます。
ターミナルから繰り返し確認するには、npx @modelcontextprotocol/inspector --cli uv run orders.py --method tools/listを使います。ツールを呼び出すコマンドは、npx @modelcontextprotocol/inspector --cli uv run orders.py --method tools/call --tool-name lookup_order --tool-arg order_id=A100です。どちらも公式に記載されたInspector CLIの使い方です。
合格条件は明確です。ツールが一つ見つかり、既知の注文には想定した状況が返り、存在しない注文には見つからなかったことが明示されること。モデルがもっともらしい説明文を生成しただけでは、照会が実行された証拠にはなりません。
同じサーバーをClaude CodeとCursorにつなぐ
ローカルの各クライアントは、それぞれ自分のサーバープロセスを起動します。Inspectorを起動したままにする必要はありません。クライアントがどのフォルダーを開いていても起動できるよう、絶対パスを指定してください。
Claude Code: claude mcp add --transport stdio --scope local orders -- /ABSOLUTE/PATH/orders-mcp/.venv/bin/python /ABSOLUTE/PATH/orders-mcp/orders.pyを実行します。Windowsのインタープリターは.venv\Scripts\python.exeです。起動コマンドの前に置く--区切りも含め、Claude Codeのローカルサーバー用の構文に沿っています。
claude mcp get ordersで接続を確認します。Claude Codeのセッションで/mcpを開き、「lookup_orderでA100を確認し、返された状況と配送業者だけを報告してください」と依頼します。ツールの呼び出しと、その引数を確認してください。
Cursor: プロジェクトに.cursor/mcp.jsonを作ります。次のJSONを追加し、二か所の絶対パスを置き換えてください。{"mcpServers":{"orders":{"type":"stdio","command":"/ABSOLUTE/PATH/orders-mcp/.venv/bin/python","args":["/ABSOLUTE/PATH/orders-mcp/orders.py"]}}}。
Customizeを開いてサーバーを有効にし、Agentで同じ質問をします。承認設定に従って呼び出しを確認してください。ファイルの場所、起動用のフィールド、操作方法はCursorのMCP設定に沿っています。接続に失敗したら、ツールを書き換える前に、実行ファイルのパスとサーバーの標準エラー出力を確認しましょう。
ローカルのstdioとリモートのHTTPを使い分ける
個人のワークフローなら、ローカルで使う構成から始めましょう。複数人やホスト上で動くクライアントが、一元管理されたサービスを使う必要があるなら、リモートHTTPを選びます。
リモートサービスからも、社内データへのネットワーク接続が必要です。エンドポイントを公開しただけで、非公開のデータベースに届くようになったり、権限設定が正しくなったりするわけではありません。
2026-07-28のHTTPプロトコルでは、各リクエストが自己完結します。現行のPython SDKは古いクライアントにも対応できますが、レプリカを増やすと、そのセッションにスティッキールーティングが必要になることがあります。拡張する前に、ドキュメントに記載された旧仕様向けの設定を意図して適用してください。接続するすべてのクライアントが最新の仕様に対応しているとは限りません。

HTTPで公開し、認証を付ける
HTTPアクセスには、このサービス向けに発行されたトークンを使います。MCP仕様の認可フレームワークはOAuth 2.1です。IDプロバイダーが利用者をログインさせてトークンを発行し、MCPサーバーがそれを検証します。スコープはorders:readのような、名前を付けた権限です。オーディエンスは、そのトークンを受け入れられるサービスを示します。
SDKが提供するのはリソースサーバーとの統合機能であり、自社のログインシステムではありません。この例では、OAuthのディスカバリー、利用するクライアントの登録、利用者ログイン用のPKCE、トークンイントロスペクションのエンドポイントを備えたIDプロバイダーを設定します。PKCEは、ログインを完了するアプリケーションが、ログインを開始したアプリケーションと同じであることを確認する仕組みです。イントロスペクションでは、トークンが有効か、何を許可しているかを発行元に問い合わせます。
このアダプターは、HTTPSのイントロスペクション、HTTP Basicによるクライアント認証、そしてactive、aud、exp、client_id、scopeを含むレスポンスを前提にしています。発行元には、このエンドポイントの正確な公開URLをaudに含め、orders:readを発行するよう設定してください。プロバイダーが別のイントロスペクション認証方式を使う場合は、そのドキュメントに合わせてリクエストを調整します。代わりにJWTという署名付きトークンを提供する場合は、同じTokenVerifierインターフェースで、署名、発行元、有効期限、オーディエンスの検証を実装してください。
uv add uvicornでuvicornを追加し、orders.pyと同じ場所にremote.pyを作ります。これはSDKの公式イントロスペクションのサンプルと、ドキュメントに記載されたHTTP・認証のインターフェースに沿った実装です。先ほど検証した注文照会を再利用します。
import os
import time
from urllib.parse import urlsplit
import httpx2
from pydantic import AnyHttpUrl
from mcp.server import MCPServer
from mcp.server.auth.provider import AccessToken, TokenVerifier
from mcp.server.auth.settings import AuthSettings
from mcp.server.transport_security import TransportSecuritySettings
from orders import lookup_order as local_lookup
RESOURCE = os.environ["MCP_RESOURCE_URL"]
ISSUER = os.environ["MCP_ISSUER_URL"]
INTROSPECT = os.environ["MCP_INTROSPECTION_URL"]
if any(urlsplit(url).scheme != "https" for url in (RESOURCE, ISSUER, INTROSPECT)):
raise ValueError("Public auth and resource URLs must use HTTPS")
class OrderTokenVerifier(TokenVerifier):
async def verify_token(self, token: str) -> AccessToken | None:
try:
async with httpx2.AsyncClient(timeout=5.0) as client:
response = await client.post(
INTROSPECT,
data={"token": token},
auth=(os.environ["MCP_INTROSPECTION_CLIENT_ID"],
os.environ["MCP_INTROSPECTION_CLIENT_SECRET"]),
)
response.raise_for_status()
data = response.json()
audiences = data.get("aud", [])
if isinstance(audiences, str):
audiences = [audiences]
expiry = data.get("exp")
if (data.get("active") is not True or RESOURCE not in audiences
or not isinstance(expiry, int) or expiry <= time.time()):
return None
if data.get("iss", ISSUER) != ISSUER:
return None
return AccessToken(
token=token, client_id=data["client_id"],
scopes=data.get("scope", "").split(), expires_at=expiry,
resource=RESOURCE, subject=data.get("sub"),
)
except Exception:
return None
mcp = MCPServer(
"orders",
token_verifier=OrderTokenVerifier(),
auth=AuthSettings(
issuer_url=AnyHttpUrl(ISSUER),
resource_server_url=AnyHttpUrl(RESOURCE),
required_scopes=["orders:read"], validate_token_resource=True,
),
)
@mcp.tool()
def lookup_order(order_id: str) -> str:
"""Look up a fictional order by ID, such as A100. Read-only."""
return local_lookup(order_id)
hostname = urlsplit(RESOURCE).hostname
security = TransportSecuritySettings(
allowed_hosts=[hostname, f"{hostname}:*"],
allowed_origins=[os.environ["MCP_ALLOWED_ORIGIN"]],
)
app = mcp.streamable_http_app(transport_security=security)デプロイ先の環境変数、またはシークレットストアに次の値を設定します。
ホストの許可リストを明示しているのは、SDKがデフォルトではlocalhostを受け入れる一方、公開ホスト名には421 Misdirected Requestを返して拒否するためです。ブラウザーのオリジンは別に検証します。実際に使うオリジンだけを列挙してください。返されるアプリケーションには、起動と終了のライフサイクルも組み込まれています。これらはSDKのデプロイとASGIアプリケーションのドキュメントに沿った設定です。ASGIは、PythonのWebサーバーがこのアプリケーションを動かすためのインターフェースです。
ホスティング先のHTTPSプロキシの背後で、uv run uvicorn remote:app --host 0.0.0.0 --port 8000を使って起動します。プロキシからプロセスへの通信がHTTPでも、公開するリソースURLはHTTPSのままです。転送ヘッダーを信頼する範囲は、そのホスティング先の実際のプロキシ構成に合わせて設定してください。
実際のレコードを使う前に、HTTP経由で次を確認します。
- トークンがない、有効期限が切れている、または別のオーディエンス向けのトークンである場合は、アクセスが拒否される。
- 有効なトークンでも
orders:readがなければ、アクセスが拒否される。 - 正しいオーディエンスとスコープを持つ有効なトークンなら、
lookup_orderがデモの注文状況を返す。 /.well-known/oauth-protected-resource/mcpのメタデータが、正しいリソースと発行元を示す。
npx @modelcontextprotocol/inspector --server-url https://orders.example.com/mcp --transport httpで、デプロイしたエンドポイントを検証し、認証フローを完了させます。メモリー内でのツールテストはHTTPの認可を通らないため、この境界が機能している証明にはなりません。
Claude Codeでは、claude mcp add --transport http orders-remote https://orders.example.com/mcpで別の接続を追加し、/mcpから認証します。Cursorでは、mcpServersの下に"url":"https://orders.example.com/mcp"を持つリモート接続のエントリーを追加し、OAuthを完了させます。事前登録済みのクライアント向けに、CursorはCLIENT_IDとscopesを指定できるauthオブジェクトもドキュメントに記載しています。利用するクライアントに適したコールバックを発行元に登録してください。Claude Codeの認証とCursorのリモートOAuth設定を参照できます。
ここまででできるのは、認証付きの小さなアダプターです。本番システム全体が完成したわけではありません。デモ用の辞書を置き換える前に、検証済みのIDを使ってテナントとレコードの権限を適用し、呼び出しの前後に監査イベントを記録し、HTTP接続を再利用し、リクエストに上限を設けてください。スコープが許可するのは操作であり、すべての注文の所有権を証明するものではありません。
RenderかCloudflare Workersにデプロイする
このPythonサーバーなら、私ならRenderから始めます。PythonのWebサービスを選べば、すでに作ったアプリケーションをそのまま活用できます。同じ範囲のツールを、ドキュメントに記載されたWorkerハンドラーで実装したいなら、Cloudflare Workersも有力な選択肢です。
次の料金は、2026年10月7日に確認した、各社が公開している価格です。
出典はCloudflare Workersの料金とRenderの料金です。RenderのProワークスペースでチーム向け機能を選ぶ場合は、コンピュート料金に加えて$25/月がかかります。ストレージ、IDサービス、モデルの利用料などは別に予算を組む必要があります。ここで挙げているのはホスティング料金であり、AIを使ったワークフロー全体の費用ではありません。
Renderへのデプロイ: リポジトリにorders.py、remote.py、requirements.txtを置きます。requirementsファイルにはmcp[cli]==2.3.0とuvicornを、それぞれ別の行に記載してください。Python Web Serviceを作り、ビルドコマンドをpip install -r requirements.txt、起動コマンドをuvicorn remote:app --host 0.0.0.0 --port $PORTにします。上記の環境変数を追加し、割り当てられたホスト名か独自ドメインをMCP_RESOURCE_URLに設定します。Renderの公式Python Webサービスのデプロイ手順を、SDKのASGIアプリケーションに合わせた構成です。
Renderの無料サービスはデモに便利ですが、15分間アクセスがないとスリープし、復帰には約1分かかります。対話的に使う共有ツールには、私なら有料のコンピュートを選びます。
Cloudflareへのデプロイ: 現行のMCPハンドラーのドキュメントとリモートサーバーのガイドを使います。現行のTypeScriptによる実装では、agents/mcp/serverのcreateMcpHandlerと@modelcontextprotocol/serverを使います。同じ注文照会を実装し、URLを共有する前に認証を設定してください。Pythonのuvicorn起動コマンドはPythonのホスティング先向けであり、Workerのデプロイ手順にはなりません。
セキュリティの境界を必要最小限に保つ
サーバーには、ツールに必要なアクセスだけを与えます。注文状況なら、バックエンドの読み取り専用の認証情報、必要なフィールド、レコード単位の権限チェックです。返金、キャンセル、住所変更には、別のツールと権限を用意してください。モデルが引数を選んだことを、認可の根拠にしてはいけません。
HTTPのトークンは、発行元、有効期限、オーディエンス、スコープを検証します。HTTPSを使い、サーバーから接続先のAPIを呼ぶ際には別の認証情報を使ってください。MCPのセキュリティガイドはトークンのパススルーを禁止しています。MCPエンドポイントに提示されたトークンが、そのまま注文システムの認証情報として使えるわけではありません。ローカルのstdioでは、起動するプロセス、その環境、ファイルシステムへのアクセスを制限します。
検証済みの呼び出し元、ツール名、適切に機密情報を伏せたレコードの参照、結果、処理時間、リクエストIDをログに残します。トークンや顧客レコード全体は記録しないでください。stdioなら標準エラー出力、HTTPならホスティング先のログシステムに記録します。レコードから取得したテキストはデータとして扱ってください。注文に含まれるメモが、別の操作の権限を与えることはありません。
複数のサーバーやチームで、IDのポリシー、レート制限、監査ログの収集、アクセスの取り消しを共有する必要があるなら、前段にゲートウェイを置きます。こうした制御はゲートウェイで一元化できますが、各バックエンドにも正しいレコード権限が必要です。MCPゲートウェイのガイドで、その判断を詳しく説明しています。

効果を見込みやすい六つのワークフロー
同じ構成は、次の用途にも広げられます。人が繰り返し限られた範囲の事実を調べていて、答えが正しいか判断できる業務から始めましょう。
採算の計算は、自社のワークフローから始めます。あくまで計算例です。 一日に80回、各2分の照会があれば、合計160分を使っています。その後の計測で、連携したワークフローが照会あたり1分を削減できるとわかれば、一日80分を取り戻せる計算です。これは算数であり、性能のベンチマークではありません。ホスティング費用に見合う効果を主張する前に、回答の正確さと削減時間を測ってください。
製品化を検討できる二つのアイデア
最も有望なのは、サポートチーム向けの注文コンテキストを提供するアダプターです。 チームがすでに使っているアシスタントの中で、適切な配送情報を取り出せる、範囲を絞った連携を販売する余地があります。2026年10月7日に確認したDataForSEOの推定では、“customer support automation”の米国Google検索数は月間260件です。これは業務全体への関心を表すもので、MCPを購入する人数ではありません。Intercomが公開しているFinの価格は、成果あたり$0.99です。サポートの自動化に既存の予算があることを示しています。この小さなアダプターは、その製品を置き換えるのではなく、必要な情報を提供します。
販売できる最小構成としては、一つの注文バックエンド、lookup_order、スコープ付きのログイン、監査証跡、返されたフィールドを引用する返信の下書きが考えられます。特定の会社のデータとアクセスルールに適合することが、強みになります。ただし、既存ベンダーがすでにコネクターを持っている可能性があります。チームのモデル利用ライセンス、データの整備、サポートにも費用がかかります。ツールを増やす前に、サポート責任者と、その不足が本当にあるかを確認しましょう。
もう一つは、権限を考慮した社内ポリシーの照会です。 リソースか、範囲を限定した検索ツールを通じて、一社の承認済みポリシー集にアクセスできる仕組みを作れます。同じ日に確認したDataForSEOの推定では、“enterprise search”の米国検索数は月間390件です。MVPには、一つの文書集、出典の引用、情報の鮮度チェック、ログインした人の権限による絞り込みを含められます。難しい点は、検索の品質とアクセス制御そのものが製品の価値になることです。フォルダーをMCPで包むだけなら、簡単にまねされます。この検索数は、より広い業務への需要の兆候であって、この実装に利用者がお金を払う証拠ではありません。
MCPで解決できないことを押さえる
MCPが標準化するのは、アクセスの方法です。データの品質、認可、バックエンドの信頼性、ツールに何を許可するかは、自分で責任を持つ必要があります。モデルは正しい結果を読み違えることがあります。また、接続するクライアントによって、対応機能や承認ポリシーは異なります。
計測できるワークフローを、共通のツールインターフェースで改善できる場合に作りましょう。アシスタントがツールを選ぶ必要のない固定のバッチ処理なら、通常のAPI呼び出しやスクリプトのほうが適していることもあります。
週明けに着手するなら、サポート担当者と一緒に、繰り返し行う照会を一つ選びます。架空のレコードで両方のクライアントを接続できるようにし、その後、読み取り専用アカウントを使って実データに置き換え、レコード権限を検証してください。最初の導入には書き込みを含めません。ワークフローとIDによるアクセスの境界を確認できたら、共有用のHTTPサービスに移行します。
MCPサーバーを作るのは難しいですか?
小さな読み取り専用サーバーなら、公式SDKで比較的簡単に作れます。関数を定義し、入力を説明し、通信方式を選ぶだけです。ただし、社内データを安全に使えるようにするには、権限の適用、認証情報の管理、サービスの運用が必要になるため、作業は増えます。
テストに使える無料のMCPサーバーはありますか?
この手順の架空の注文サーバーは、ローカルならホスティング料金なしで動かせます。MCP Inspectorを使えば、モデルのサブスクリプションなしで呼び出せます。クラウドのホスティングと、あとで選ぶAIクライアントには、それぞれ独自の料金があります。
MCPサーバーの費用はどのくらいですか?
ローカルのプロセスに、別途ホスティングプランは必要ありません。Cloudflare Workersは無料枠と最低$5/月の有料プランを公開しています。Renderの小規模な有料Webサービスは、コンピュート料金が$7/月です。これらには、モデル利用料、IDサービス、ストレージ、開発費は含まれません。
MCPサーバーをインストールする必要がありますか?
stdioでは、サーバーがクライアントのマシン上で動くため、そのマシンにコードと実行環境が必要です。リモートHTTPでは、エンドポイントを設定して認証します。サーバーはホスティング先で動きます。選んだクライアントが対応している構成を使ってください。
社内データを扱うMCPサービスをチーム向けに構築・運用したい場合は、AI本番システムの構築サービスで、連携とアクセス制御を含めて対応しています。
- 公開日
- カテゴリー
- Build
- 言語







