テックトレンド

MCPサーバーを自作する完全ガイド|Python・TypeScriptで既存ツールをAIエージェントに接続する実装手順【2026年最新】

2026-04-24濱本 隆太

Model Context Protocol(MCP)のサーバーをPythonのFastMCPとTypeScriptの公式SDKで自作する手順を、JSON-RPC仕様・stdio/HTTPトランスポート・Claude Codeへの登録まで含めて解説します。社内ツールをAIエージェントから呼び出したい開発者向けの実装ガイド。

MCPサーバーを自作する完全ガイド|Python・TypeScriptで既存ツールをAIエージェントに接続する実装手順【2026年最新】
シェア

こんにちは、株式会社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]。

AI活用に関心をお持ちですか?

TIMEWELLのサービス資料をご用意しています。まずはお気軽にご相談ください。

トランスポートとプロトコルの基礎を押さえる

実装に入る前に、トランスポート層の選択肢を理解しておきます。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を送り、サーバーは自分がサポートする機能、たとえばtoolsresourcespromptsの有無を返します。クライアントは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 settingsuser 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/listtools/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

あなたのAIリテラシーを測ってみませんか?

5分の無料診断で、AIの理解度からセキュリティ意識まで7つの観点で評価します。

この記事が参考になったらシェア

シェア

メルマガ登録

AI活用やDXの最新情報を毎週お届けします

ご登録いただいたメールアドレスは、メルマガ配信のみに使用します。

無料診断ツール

あなたのAIリテラシー、診断してみませんか?

5分で分かるAIリテラシー診断。活用レベルからセキュリティ意識まで、7つの観点で評価します。

テックトレンドについてもっと詳しく

テックトレンドの機能や導入事例について、詳しくご紹介しています。