こんにちは、株式会社TIMEWELLの濱本です。
AIエージェントを業務に組み込もうとすると、ほぼ必ず壁にぶつかる場所があります。社内のデータベース、チーム固有のAPI、古い業務システムに、AIからどうアクセスさせるかという問題です。ChatGPTのプラグインやFunction Callingで一つずつ個別に繋いでいた時代は終わりつつあります。2024年11月にAnthropicが発表したModel Context Protocol(以下MCP)は、このつなぎ方そのものを標準化する試みでした。そして2026年4月現在、MCP v2.1まで仕様が進み、FastMCPはPythonエコシステムの実質的なデファクトになっています。
この記事では、既存の社内ツールをMCPサーバーとして公開し、Claude CodeやClaude Desktopから呼べるようにする実装手順を、PythonとTypeScriptの両面からまとめます。単なるHello Worldで終わらせず、本番投入で引っかかりがちなトランスポートやスコープ設計まで踏み込みます。AIエージェント周りの実装に着手する開発者を想定して書きました。
なぜ今、MCPサーバーを自作する必要があるのか
MCPはJSON-RPC 2.0をベースにした、AIアプリケーションと外部ツールを繋ぐためのオープンプロトコルです。基盤の発想はマイクロソフトのLanguage Server Protocol(LSP)に近い。LSPがエディタと言語処理系を分離したように、MCPはLLMクライアントと外部ツールを分離します。LLMホストが何であれ、MCPサーバーを1個書けば多くのクライアントから使えるという設計です。
公式のリファレンス実装リポジトリには、Git、Filesystem、Fetch、Memory、Sequential Thinkingといった基本的なサーバーが揃っています[^1]。GitHub、Slack、Google Drive、Postgresなど主要SaaS向けのサーバーも公式・コミュニティ含めて数百個は流通していて、Claude Desktop 3.2.1やCursor 2.5.0はこれらをほぼ素のまま取り込めます。ここまでなら「使うだけ」でよいのですが、問題は社内固有のシステムです。
受発注管理、案件管理、在庫、社内Wiki、独自認証基盤。これらは当然ながら既製MCPサーバーに存在しません。汎用のDB用MCPサーバーを無理やり当てはめると、テーブル設計がそのままLLMに露出してしまい、スキーマレベルの脆弱性を抱えたまま運用する羽目になる。ここで必要になるのが、業務の粒度に合わせた自作MCPサーバーです。ツールの名前、引数、戻り値を業務の言葉で設計し、アクセス制御もそこに寄せる。Anthropicが2026年3月に公開した「Code execution with MCP」のエンジニアリングブログでも、粗い汎用ツールを積み重ねるより、業務特化の粒度でMCPを切るほうがエージェントのトークン消費と精度の両面で効く、と明言されています[^2]。
ここで実際に手を動かす前に、登場人物を整理しておきます。MCPクライアントはClaude DesktopやClaude Codeなど、LLMを呼び出すアプリ側のことです。MCPサーバーは、その外側でツールや資料(Resources)や定型プロンプト(Prompts)を公開する側。JSON-RPC 2.0のリクエストとレスポンスが両者の間を行き来します。リクエストにはjsonrpc、id、method、paramsが入り、通知なら同じくjsonrpcとmethodが入る。このあたりは仕様書に厳密に書かれていて、自前で実装するなら一度目を通す価値があります[^3]。
トランスポートとプロトコルの基礎を押さえる
実装に入る前に、トランスポート層の選択肢を理解しておきます。MCPには大きく2つのトランスポートがあります。stdio transportは、クライアントが子プロセスとしてサーバーを起動し、標準入出力でJSON-RPCメッセージを流す方式です。ローカル専用で、認証の話を持ち込まずに済むのが強みで、開発中はほぼこれで困りません。もう1つがStreamable HTTP transportです。こちらはHTTPでリクエストを受け、長い結果はストリーミングで返す。2026年の公式仕様ではこれが標準トランスポートになり、旧SSE transport(Server-Sent Events)は置き換えられる流れになっています。公式ベンチマークではv2.1のStreamable HTTPが旧方式比で95%のレイテンシ削減を達成した、という数値が出ています[^3]。
どちらを選ぶかは、どこから呼ぶかで決まります。自分のPCで動くClaude Codeから社内PCのツールを使うだけならstdioで十分です。社内の共有サーバーに置いて複数人で叩くならStreamable HTTP一択になります。筆者はまずstdioで動かして、社内共有が必要になってからHTTP化する順番を推しています。stdio版が動けば、HTTP化はロジックをそのままにTransportクラスを差し替えるだけで済むことが多いからです。
メッセージの流れは、初期化、ケーパビリティ交換、ツール一覧取得、ツール実行という順番で進みます。クライアントが最初にinitializeを送り、サーバーは自分がサポートする機能、たとえばtoolsやresourcesやpromptsの有無を返します。クライアントはtools/listでツール一覧を受け取り、LLMが「このツールを使いたい」と判断した瞬間にtools/callが飛んでくる。ここでサーバーが実処理を行って結果を返します。FastMCPや公式TypeScript SDKを使うと、この初期化やスキーマ生成は全部フレームワーク側が面倒を見てくれるため、アプリ開発者が書くのは実質的に「ツールとして公開する関数」だけです。
細かい話をすると、ツール定義(tool definitions)には必ず入力スキーマが必要です。JSON Schemaで記述され、LLMはこれを読んで引数を組み立てます。つまり、スキーマの書き方がエージェントの精度に直結する。引数の説明文を手抜きすると、LLMが引数を埋められずにツール呼び出しが失敗します。ここはAPI設計と同じ緊張感で書く必要があります。余談ですが、私はツールを1つ追加するたびに、自分のプロンプトからそのツールを呼べるかClaude Desktopで小さくテストしています。スキーマ文字列の一言の違いで呼ばれたり呼ばれなかったりするので、侮れません。
PythonのFastMCPで最小サーバーを書く
ここから実装に入ります。まずはPython側、FastMCPで書きます。FastMCPはPrefectHQが開発している高水準フレームワークで、2026年1月19日にFastMCP 3.0がリリースされ、現在はあらゆる言語のMCPサーバーの7割程度がFastMCP系を使っているとされます[^4]。Python 3.11以上推奨、依存はfastmcpと必要に応じてuvicornだけです。
uv init my-mcp-server
cd my-mcp-server
uv add fastmcp
最小のサーバーはこれだけで動きます。架空の社内案件管理APIにアクセスして、担当者名で案件一覧を返すツールを1つ定義してみます。
# server.py
from fastmcp import FastMCP
import httpx
mcp = FastMCP("project-tracker")
API_BASE = "https://internal.example.com/api/v1"
@mcp.tool()
def list_projects_by_owner(owner_email: str, status: str = "active") -> list[dict]:
"""指定した担当者のメールアドレスに紐づく案件一覧を取得する。
Args:
owner_email: 担当者のメールアドレス(会社ドメイン)
status: 案件ステータス。active、closed、allのいずれか
"""
response = httpx.get(
f"{API_BASE}/projects",
params={"owner": owner_email, "status": status},
timeout=10.0,
)
response.raise_for_status()
return response.json()["projects"]
if __name__ == "__main__":
mcp.run()
ポイントは3つあります。第一に、関数のドキュメント文字列と型注釈がそのままLLMへのツール説明になります。ここを雑に書くとエージェントがツールを使いこなせません。owner_emailが必須か任意か、statusの取りうる値は何か、このレベルで書き切るのが重要です。第二に、戻り値はJSON化できる構造ならなんでもよく、FastMCPが内部で整形してくれます。第三に、mcp.run()を引数なしで呼べばstdioで動きます。Streamable HTTPにしたければmcp.run(transport="http", port=8080)と指定するだけで、コードの主要部分は一切変わりません。
もう少し実用的な例として、社内の顧客マスターから類似企業を検索するツールも書いてみます。社内DBはPostgresを想定します。
# search_customers.py
from fastmcp import FastMCP
import asyncpg
import os
mcp = FastMCP("customer-search")
_pool: asyncpg.Pool | None = None
async def get_pool() -> asyncpg.Pool:
global _pool
if _pool is None:
_pool = await asyncpg.create_pool(os.environ["DATABASE_URL"], max_size=5)
return _pool
@mcp.tool()
async def search_customers(query: str, limit: int = 10) -> list[dict]:
"""社名の部分一致で顧客マスターを検索する。
Args:
query: 社名の検索語(2文字以上)
limit: 返す件数の上限。既定は10、最大は50
"""
if len(query) < 2:
raise ValueError("queryは2文字以上で指定してください")
limit = min(max(limit, 1), 50)
pool = await get_pool()
async with pool.acquire() as conn:
rows = await conn.fetch(
"SELECT id, name, industry, updated_at FROM customers "
"WHERE name ILIKE $1 ORDER BY updated_at DESC LIMIT $2",
f"%{query}%", limit,
)
return [dict(r) for r in rows]
if __name__ == "__main__":
mcp.run()
async関数もそのままツール化できるのがFastMCPの強みです。DBコネクションプールをモジュールスコープで持てば、リクエストごとに接続が走る事故を避けられます。サーバー内の副作用、たとえばSQLを書き換える系のツールでは、必ず「何を更新したか」を戻り値に含めて返す設計にしておくと、後からログを追いやすくなります。ここは個人的にかなり気を遣うところで、エージェントが何をやったか再構築できる状態を維持することが、運用の肝だと感じています。
TypeScript SDKで書く場合の型安全なパターン
Node.jsエコシステムで動かす場合は公式の@modelcontextprotocol/sdkを使います。npmでは週次ダウンロードが数十万に達しており、TypeScript側の事実上の標準です[^5]。こちらはFastMCPほど魔法は使わず、Zodでスキーマを明示する設計になっています。明示的な分、型の通り方が気持ちよく、大規模に書くとむしろこちらの方が保守しやすい、というのが筆者の感触です。
// src/index.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const server = new McpServer({
name: "notion-internal",
version: "0.1.0",
});
server.tool(
"create_meeting_note",
"社内ミーティングの議事録ページをNotionに作成する",
{
title: z.string().min(1).describe("議事録のタイトル"),
attendees: z.array(z.string()).describe("参加者のメールアドレス配列"),
agenda: z.string().describe("議題のマークダウン本文"),
},
async ({ title, attendees, agenda }) => {
const res = await fetch("https://api.notion.com/v1/pages", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.NOTION_TOKEN}`,
"Notion-Version": "2022-06-28",
"Content-Type": "application/json",
},
body: JSON.stringify({
parent: { database_id: process.env.NOTION_DB_ID },
properties: {
Name: { title: [{ text: { content: title } }] },
Attendees: { multi_select: attendees.map((a) => ({ name: a })) },
},
children: [
{
object: "block",
type: "paragraph",
paragraph: { rich_text: [{ text: { content: agenda } }] },
},
],
}),
});
if (!res.ok) throw new Error(`Notion API ${res.status}`);
const data = await res.json();
return {
content: [{ type: "text", text: `created: ${data.id}` }],
};
}
);
await server.connect(new StdioServerTransport());
Zodで定義したスキーマは、MCPプロトコルに流す際に自動でJSON Schemaに変換されます。.describe()に書いた説明が、そのままLLMに渡るツール引数の説明文になる。ここをサボらないことが、TypeScriptでも同じく重要です。もう1点、server.toolの返り値はcontent配列形式で、type: "text"以外にもtype: "image"やtype: "resource"が指定できます。画像やファイルを返せるので、たとえばグラフ生成ツールや、社内PDF検索を実装するときに便利です。
運用視点で欠かせないのがエラーハンドリングです。MCPクライアントはツール実行が例外で落ちたとき、エラー内容をそのままLLMに渡します。つまり、エラーメッセージを雑に書くとエージェントが正しいリトライを組み立てられない。私は原則として、ユーザー原因のエラー(引数不備など)と、サーバー側の障害(API 500など)を文言レベルで区別し、前者は「こう直すべき」を含めるようにしています。Anthropicの公式ドキュメントでも、ツール側からのヒント返却は推奨されています[^6]。
リモート配布を前提にするなら、Streamable HTTPに切り替えるのも一行レベルの差です。
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import express from "express";
const app = express();
app.use(express.json());
app.post("/mcp", async (req, res) => {
const transport = new StreamableHTTPServerTransport();
res.on("close", () => transport.close());
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
});
app.listen(8787);
このサーバーをECSでもCloud Runでも社内のKubernetesでもよいですが、HTTPSエンドポイントとして公開すれば、複数のClaude Codeから同時に参照できます。TIMEWELLではこの構成で社内ナレッジを取り出す共有MCPサーバーを立てていて、毎週少しずつツールを足しています。ちなみに設計の勘所は、以前の記事Google Cloud Next 2025:エンタープライズAIエージェントで触れたA2AやADKの議論とも重なります。
Claude CodeとClaude Desktopに登録して実際に使う
サーバーが書けたら、クライアントから呼べるように登録します。Claude Codeならclaude mcp addコマンドでほぼ完結します[^7]。
# Pythonの自作サーバーをstdioで登録
claude mcp add --transport stdio customer-search -- uv run python /path/to/search_customers.py
# TypeScriptの自作サーバーを登録
claude mcp add --transport stdio notion-internal -- node /path/to/dist/index.js
# リモートHTTPサーバーを登録
claude mcp add --transport http internal-kb https://mcp.internal.example.com/mcp
# 一覧確認
claude mcp list
# 個別詳細
claude mcp get customer-search
# 削除
claude mcp remove customer-search
スコープの概念が効いてくるのはチーム開発のときです。--scope projectでプロジェクトスコープに登録すると、リポジトリ直下に.mcp.jsonが生成され、これをGitに入れておけばチーム全員が同じMCPサーバー群を使えます。社内で「この業務にはこのMCPを使う」という標準を決めたいなら、プロジェクトスコープが現実解です。個人でだけ使いたいユーティリティは--scope userにして、環境を汚さない。この切り分けは、VS Codeの拡張機能のworkspace settingsとuser settingsの感覚に近いです。
Claude Desktopの場合は設定ファイルclaude_desktop_config.jsonに直接書きます。
{
"mcpServers": {
"customer-search": {
"command": "uv",
"args": ["run", "python", "/Users/me/work/search_customers.py"],
"env": {
"DATABASE_URL": "postgres://..."
}
},
"internal-kb": {
"transport": {
"type": "http",
"url": "https://mcp.internal.example.com/mcp"
}
}
}
}
登録したあとにやるべきはデバッグです。MCP Inspectorという公式ツールが便利で、npx @modelcontextprotocol/inspectorでブラウザUIが立ち上がり、手元のMCPサーバーに対してtools/listやtools/callを叩けます。LLMを介さずにプロトコルレベルで疎通確認ができるので、ツールの説明文を微修正しながら追い込むのに使えます。以前紹介したAntigravity × Google Workspaceの連携記事や、Superpowers Claude Codeプラグイン解説で触れたような、複数エージェントを束ねる構成でも、結局はこのMCPのレイヤーが土台になります。
運用上の最後の注意点を3つだけ。1つ目は認証です。Streamable HTTPで公開するならOAuthかAPIキーをサーバー側で必ず差してください。社内に向けて開けたつもりのエンドポイントが、設定ミスでインターネット全公開になるのは、よくある事故です。2つ目はリクエストのスコープ制限。LLMは指示されれば貪欲に大きなデータを取りに行くので、1リクエストの結果サイズに上限を設け、ページネーションを強制する設計にしておくとトークン爆発を防げます。3つ目はログです。誰がどのツールをどんな引数で呼んだかを、サーバー側で必ず残す。後から「AIが勝手に何かした」と騒ぎになったとき、ログがなければ弁明すらできません。
まとめ:MCPの自作は、AIエージェント運用の土台工事
ここまでで、MCPサーバーをPythonのFastMCPとTypeScriptの公式SDKで書き、Claude CodeとClaude Desktopに登録するところまで一通り眺めました。プロトコル自体はJSON-RPC 2.0で素直に読めるし、フレームワークが初期化や通知の面倒を見てくれるので、実装の難しさは意外なほど低い。本当に難しいのは、どの業務をどの粒度のツールとして切るかという設計判断のほうです。
筆者としては、社内でAIエージェントを育てたいチームは、既製MCPサーバーを並べるだけでなく、自社のための小さなMCPサーバーを10個ほど自作する段階を必ず通るべきだと思っています。そのプロセスで初めて、自社のデータと業務がAI向けにどう整っていないかが見えてくる。AIに仕事をさせる前提で業務を再設計する作業そのものが、いちばん本質的なAI導入です。
TIMEWELLではエンタープライズ向けAIとしてZEROCKを提供していて、社内ナレッジを安全に引き出すMCPサーバーを含めて、要件定義から実装まで一緒に進めることが多くなっています。経営レイヤーでAIエージェント戦略を整えるところは、AIコンサルティングのWARPでお手伝いしています。自社のデータと業務に向き合いながらMCPを設計する作業は、一度外部と並走しておくと早く回ります。自作の一歩目をどこから踏むか迷ったら、気軽にご相談ください。
参考文献
[^1]: Model Context Protocol Servers(reference implementations). https://github.com/modelcontextprotocol/servers [^2]: Anthropic. Code execution with MCP: building more efficient AI agents. https://www.anthropic.com/engineering/code-execution-with-mcp [^3]: Model Context Protocol Specification 2025-11-25. https://modelcontextprotocol.io/specification/2025-11-25 [^4]: PrefectHQ. FastMCP 3.0 GitHub Repository. https://github.com/prefecthq/fastmcp [^5]: modelcontextprotocol. TypeScript SDK. https://github.com/modelcontextprotocol/typescript-sdk [^6]: Model Context Protocol. Build an MCP server. https://modelcontextprotocol.io/docs/develop/build-server [^7]: Claude Code Docs. Connect Claude Code to tools via MCP. https://code.claude.com/docs/en/mcp
