Model Context Protocol (MCP) を使うと、外部サービスやツールを Claude Code の「手足」として接続できます。GA4、Figma、Backlog のような有名サービスには公式・サードパーティの MCP サーバーがすでに揃っていて、設定ファイルに数行書くだけで使い始められます。

一方で、社内でしか動いていない管理ツールや、独自の在庫システム、社内 API といった「自社固有のツール」には、当然ながら既製の MCP は存在しません。ここで選択肢になるのが、自分で MCP サーバーを書くことです。

この記事では、TypeScript を使って MCP サーバーを最小構成から自作し、Claude Code に接続して動かすところまでを、実務で必要になる認証・権限・配布の設計とあわせて解説します。MCP サーバーの自作は身構えるほど大げさなものではなく、関数をいくつか公開するだけの小さな実装から始められます。

なぜ MCP サーバーを自作するのか — 既製品との境目

まず判断したいのは、本当に自作が必要かどうかです。MCP サーバーの自作には実装と運用のコストがかかるので、既製品で済むなら既製品を使うのが鉄則です。

既製の MCP サーバーで足りるのは、対象が GitHub・Slack・Google 系サービスのような広く使われている SaaS で、公式またはコミュニティ製のサーバーが公開されているケースです。導入記事の実務で効く MCP の活用例で紹介しているような連携は、ほぼ既製品でまかなえます。

逆に、自作を検討すべきなのは次のような場面です。

社内独自のツールや API を Claude Code から触りたいとき。これは既製品が存在しないので、自作が唯一の道になります。たとえば「社内の案件管理 DB から特定条件のレコードを引く」「自社の見積もり計算ロジックを呼ぶ」といったものです。

もうひとつは、既製の MCP では権限や公開範囲が要件に合わないとき。汎用サーバーは多機能な反面、不要な操作まで公開してしまうことがあります。「読み取りだけ、それも特定テーブルだけ」のように絞り込んだサーバーを自作したほうが、安全に運用できる場合があります。

複数の社内 API を束ねて、エージェントにとって扱いやすい 1 つの「業務ドメインの道具箱」として提供したい、という動機で自作することもあります。生の REST API をそのまま叩かせるより、業務の語彙に合わせたツールを用意したほうが、エージェントは正確に動きます。

最小構成の MCP サーバーを TypeScript で実装する

MCP サーバーは、公式の SDK @modelcontextprotocol/sdk を使えば数十行で書けます。ここでは標準入出力 (stdio) でやり取りする、ローカル実行向けの最小サーバーを作ります。Claude Code から起動する用途では stdio が扱いやすく、最初の一歩に向いています。

まずプロジェクトを用意して、SDK とスキーマ定義用の Zod を入れます。

mkdir my-mcp-server && cd my-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node tsx

package.json に ES Modules の指定を入れておきます (SDK が ESM のため)。

{
  "type": "module",
}

サーバー本体です。ここでは「2 つの数値を足す」だけの最小ツールを 1 つ公開します。中身は後で社内 API 呼び出しに差し替えますが、まずは配線が通ることを確認します。

// 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: "my-mcp-server",
  version: "0.1.0",
});
 
// ツール (= エージェントが呼べる関数) を 1 つ登録する
server.registerTool(
  "add",
  {
    title: "数値の加算",
    description: "2 つの数値を足し合わせて結果を返す",
    inputSchema: {
      a: z.number().describe("足される数"),
      b: z.number().describe("足す数"),
    },
  },
  async ({ a, b }) => {
    return {
      content: [{ type: "text", text: `${a + b}` }],
    };
  },
);
 
// stdio でホスト (Claude Code) とやり取りする
const transport = new StdioServerTransport();
await server.connect(transport);

この時点で構造は出来上がっています。McpServer を作り、registerTool でツールを足し、StdioServerTransport でホストにつなぐ。MCP サーバーの骨格はこれだけです。tsx src/index.ts で起動エラーが出ないことだけ先に確認しておくと安心です (stdio を待ち受けて入力待ちになります。Ctrl+C で抜けてかまいません)。

ツール (関数) の設計とスキーマ定義

最小サーバーが動いたら、次はツールを実用的に設計します。MCP のツールは、エージェントから見れば「説明文と引数スキーマを持った呼び出し可能な関数」です。エージェントがツールを正しく選び、正しい引数で呼べるかどうかは、この descriptioninputSchema の質でほぼ決まります。

ツール設計で意識したいのは、まず説明文を「何をするか」だけでなく「いつ使うか」まで書くことです。エージェントは説明文を読んでツールを選ぶので、用途が明確なほど誤用が減ります。

次に、入力スキーマの各フィールドに .describe() で意味を添えることです。引数名だけでは曖昧な値も、説明があれば適切に埋めてくれます。

実務に寄せて、社内 API を叩くツールの例を示します。ここでは架空の案件管理 API から、ステータスで絞った案件一覧を取得するツールを想定します。

server.registerTool(
  "search_projects",
  {
    title: "案件検索",
    description:
      "ステータスを指定して社内の案件一覧を取得する。" +
      "進行中の案件を確認したい、特定ステータスの件数を数えたいときに使う。",
    inputSchema: {
      status: z
        .enum(["draft", "in_progress", "done"])
        .describe("絞り込むステータス。指定しなければ全件"),
      limit: z
        .number()
        .int()
        .max(100)
        .default(20)
        .describe("取得件数の上限 (最大 100)"),
    },
  },
  async ({ status, limit }) => {
    const res = await fetch(
      `${process.env.INTERNAL_API_BASE}/projects?status=${status}&limit=${limit}`,
      {
        headers: { Authorization: `Bearer ${process.env.INTERNAL_API_TOKEN}` },
      },
    );
 
    if (!res.ok) {
      // エラーは throw せず、エージェントが読める形で返すと対処しやすい
      return {
        content: [{ type: "text", text: `API エラー: ${res.status}` }],
        isError: true,
      };
    }
 
    const data = await res.json();
    return {
      content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
    };
  },
);

ここでのポイントは 3 つあります。enummax で入力を制約しておくと、エージェントが無効な値を渡しても SDK 側で弾けます。返り値はテキストで返すのが基本で、構造化データは JSON 文字列にして渡すとエージェントが解釈しやすくなります。そしてエラーは例外で落とすのではなく、isError: true を付けたテキストとして返すと、エージェントが状況を読んで次の行動を選べます。

ツールは「業務の単位」で切ると使いやすくなります。生の CRUD をそのまま並べるより、「進行中の案件を探す」「案件にコメントを残す」のように、人がやりたいことの粒度で用意したほうが、エージェントの呼び出しが安定します。

Claude Code への登録と動作確認

サーバーができたら Claude Code に登録します。登録は CLI からでも設定ファイルへの直書きでもできます。チームで共有する前提なら、リポジトリ直下の .mcp.json (project スコープ) に書くのがおすすめです。

// .mcp.json (リポジトリ直下 = project スコープ。チームで共有できる)
{
  "mcpServers": {
    "my-tools": {
      "type": "stdio",
      "command": "npx",
      "args": ["tsx", "/absolute/path/to/my-mcp-server/src/index.ts"],
      "env": {
        // 値ではなく環境変数からの参照にする (後述)
        "INTERNAL_API_BASE": "${INTERNAL_API_BASE}",
        "INTERNAL_API_TOKEN": "${INTERNAL_API_TOKEN}",
      },
    },
  },
}

CLI から登録する場合は次のように追加できます。

claude mcp add my-tools --scope project -- npx tsx /absolute/path/to/my-mcp-server/src/index.ts

登録できたら Claude Code を起動し、/mcp コマンドでサーバーが接続済みか、ツールが認識されているかを確認します。ここで「接続できない」「ツールが出てこない」となったときは、まずサーバー単体を tsx src/index.ts で起動してエラーが出ないか、command のパスが絶対パスになっているかを見ます。stdio サーバーは標準出力に余計なログを混ぜると壊れるので、デバッグ出力は console.error (標準エラー) 側に出すのが鉄則です。

接続が確認できたら、実際に「進行中の案件を 5 件見せて」のように自然言語で頼んでみます。エージェントが search_projects を選び、status=in_progresslimit=5 のような引数で呼べていれば成功です。期待と違うツールを選ぶ場合は、description を見直すと改善することが多いです。

認証・権限・公開範囲の設計

ここからが、自作 MCP サーバーを実務で安全に使うために最も重要な部分です。自作サーバーは社内データに直接触れるので、設計を誤ると情報漏洩や事故につながります。

認証情報は設定ファイルに直書きしないのが大原則です。トークンや鍵は環境変数や外部の認証情報ストアから読み込み、.mcp.json には参照だけを書きます。.mcp.json をリポジトリにコミットして共有する以上、ここに値が入っていると履歴に残り続けてしまいます。

権限は最小から始めます。社内 API を叩くトークンは、まず読み取り専用のものを発行し、書き込みが本当に必要になってから段階的に広げます。多くの用途は読み取りだけで足りるので、いきなりフルアクセスのトークンを渡す必要はありません。

サーバー側でも、公開するツールを意図的に絞ります。「便利だから」と削除や更新まで一度に公開せず、まずは検索・取得系から提供して、運用に慣れてから書き込み系を足していくと安全です。書き込み系のツールには、影響範囲がひと目で分かるよう説明文に注意書きを入れておくと、エージェントの暴走を抑えられます。

スコープの使い分けも公開範囲の設計の一部です。チーム全体に配りたいサーバーは project スコープ (.mcp.json)、自分の検証用は user スコープ (個人設定) に置きます。誰が触れるツールなのかを、スコープで物理的に分けておくと管理がしやすくなります。権限とスコープの考え方は実務の MCP 活用記事でも触れているので、あわせて参照すると全体像がつかめます。

社内ツール連携の実用例とユースケース

自作 MCP サーバーが効くのは、社内固有の業務を Claude Code から一気通貫で扱いたい場面です。いくつか具体的なユースケースを挙げます。

社内ナレッジ検索の例では、独自のドキュメント DB や社内 Wiki を検索するツールを公開すると、「この仕様について書かれた社内ドキュメントを探して」と頼むだけで、エージェントが該当箇所を引いて要約してくれます。全文検索 API を 1 本ラップするだけで実装できます。

社内マスタ参照の例では、商品マスタや顧客マスタのような参照系データを読み取り専用ツールとして公開すると、コードを書く途中で「この商品コードの正式名称は何か」をその場で確認できます。仕様の確認のためにブラウザで管理画面を開く往復がなくなります。

運用オペレーションの自動化の例では、デプロイ状況の確認やジョブの実行状況の取得といった、社内の運用 API をツール化しておくと、エージェントに状況確認を任せられます。ここは影響が大きいので、まず参照系から始めて、実行系は慎重に足すのが安全です。

いずれの例も、共通するのは「社内 API を業務の語彙に翻訳して、エージェントが扱える道具にする」という発想です。生の API を直接叩かせるより、用途を絞ったツールとして包むことで、エージェントの動きが安定し、事故も減らせます。

運用と配布 — チームで使うための工夫

個人で動かす段階を越えてチームに配るときは、いくつか整えておくと運用が楽になります。

配布の方法としては、ソースをリポジトリに置いて tsx で直接起動する形が手軽です。安定してきたら、tsc でビルドした成果物を npm パッケージや社内レジストリに公開し、npx パッケージ名 で起動させると、各自の環境に依存しにくくなります。.mcp.json をリポジトリにコミットしておけば、チームメンバーはクローンするだけで同じ MCP を使い始められます。

依存の管理では、SDK のバージョンを固定し、サーバーのバージョン番号 (McpServerversion) を変更のたびに上げておくと、どの版が配られているか追いやすくなります。

ドキュメントは README に「どのツールが何をするか」「必要な環境変数は何か」を書いておけば十分です。ツールの説明文 (description) を丁寧に書いておけば、それ自体がエージェント向けと人間向けの両方のドキュメントを兼ねます。

運用に乗せた後は、想定外のツール呼び出しがないか、エラー応答が増えていないかを時々見ます。stdio サーバーなら標準エラーにログを出しておき、障害の調査や監査が必要になったときにファイルへ残すようにすると、後から挙動を追えます。

自作 MCP サーバーは、一度作ってしまえばチーム全員の Claude Code が同じ社内ツールを共有できる資産になります。最初は小さく、検索系のツールを 1 つ公開するところから始め、手応えのあるものを足していくのがおすすめです。

関連: MCP 活用パターン

社内ツールの MCP 化や、AI 駆動開発をプロダクトに組み込む取り組みでお困りの際は、AI 駆動開発のサービス紹介をご覧ください。要件の整理から実装まで、AI 駆動開発の無料相談で具体的な進め方をご提案します。