AI活用

MCPサーバーの作り方|Node.js・TypeScriptで最初のToolを実装する

MCPサーバーをNode.jsとTypeScriptで作り、公式SDK、Zod、stdioを使ってノート検索Toolを登録し、接続設定と安全対策まで解説します。

この記事の目次
  1. 結論:最初は読み取り専用のstdio Toolから作る
  2. MCPサーバーで何を作るのか
  3. TypeScriptプロジェクトを準備する
  4. package.jsonへスクリプトを追加する
  5. tsconfig.jsonを作る
  6. ファイル構成
  7. McpServerを作成する
  8. search_notes Toolを登録する
  9. Tool名と説明を具体的にする
  10. stdioトランスポートへ接続する
  11. ビルドして単体起動を確認する
  12. MCPクライアントへ登録する考え方
  13. 接続できないときの確認順
  14. データベース検索へ置き換える
  15. stdioとStreamable HTTPの違い
  16. 安全なMCP Toolにする
  17. モデルの指示と権限を分ける
  18. Toolから始めた後の拡張
  19. よくある質問
  20. MCPサーバーにLLMのAPIキーは必要ですか?
  21. stdioサーバーをインターネットへ公開できますか?
  22. Toolと普通のREST APIは何が違いますか?
  23. まとめ

MCPサーバーは、AIクライアントへ自分のデータや処理をTool、Resource、Promptとして提供するプログラムです。Node.jsでは公式TypeScript SDKの McpServer へToolを登録し、ローカル接続なら StdioServerTransport へ接続するのが最小構成です。

この記事では、検索語を受け取ってサンプルノートを返す search_notes Toolを作ります。データベースや外部APIを使わないため、MCPの登録、入力検証、結果返却、stdio接続の流れに集中できます。

MCP SDKと仕様は更新が続いています。2026年7月18日時点で、この記事は公式TypeScript SDKの安定版v1ドキュメントを基準にしています。導入時は利用するクライアントとSDKの最新版を確認してください。

情報確認日:2026年7月18日(日本時間)

結論:最初は読み取り専用のstdio Toolから作る

この記事で作るもの

  • Node.jsとTypeScriptのMCPサーバー
  • search_notes という読み取り専用Tool
  • Zodによる querylimit の検証
  • ローカルクライアント向けのstdio接続
  • 秘密情報を出さないstderrログ

MCPのTool、Resource、Promptの違いが分からない場合は、MCPとは何かMCPのTools・Resources・Promptsの違いを先に読むと理解しやすくなります。

スポンサーリンク

MCPサーバーで何を作るのか

AIクライアント
  ↓ tools/list:利用できるToolを確認
MCPサーバー
  ↓ search_notes(query, limit)
入力検証
  ↓
ノートを検索
  ↓
MCP形式で結果を返す
  ↓
AIクライアントが結果を利用者へ説明

モデルがToolの利用を提案しても、実際の処理はMCPサーバーで動きます。したがって、MCPサーバーは通常のAPIと同じように入力検証、認証・認可、タイムアウト、監査、エラー処理が必要です。

TypeScriptプロジェクトを準備する

mkdir note-search-mcp
cd note-search-mcp
npm init -y
npm pkg set type=module
npm install @modelcontextprotocol/sdk zod
npm install -D typescript tsx @types/node

公式v1ドキュメントでは、サーバーSDKとZodを組み合わせる構成が案内されています。SDKのメジャーバージョンが変わるとimportや登録方法が変わる可能性があるため、lockfileをコミットしてバージョンを固定します。

package.jsonへスクリプトを追加する

{
  "type": "module",
  "scripts": {
    "dev": "tsx src/index.ts",
    "build": "tsc",
    "start": "node dist/index.js"
  }
}

実際の package.json には、npmが追加した dependenciesdevDependencies も残します。

tsconfig.jsonを作る

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "rootDir": "src",
    "outDir": "dist",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  },
  "include": ["src/**/*.ts"]
}

ファイル構成

note-search-mcp/
├── src/
│   └── index.ts
├── package-lock.json
├── package.json
└── tsconfig.json

McpServerを作成する

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: "note-search",
  version: "1.0.0",
});

const notes = [
  {
    id: "note-1",
    title: "MCPの概要",
    body: "MCPはAIアプリと外部ツールを接続するための仕様です。",
  },
  {
    id: "note-2",
    title: "stdio接続",
    body: "ローカルではクライアントがサーバーを子プロセスとして起動します。",
  },
  {
    id: "note-3",
    title: "安全なTool",
    body: "入力検証、最小権限、実行ログが重要です。",
  },
];

nameversion はクライアントがサーバーを識別する情報です。サービスのバージョン管理方針に合わせます。

search_notes Toolを登録する

server.registerTool(
  "search_notes",
  {
    title: "ノート検索",
    description:
      "ノートのタイトルと本文からキーワード検索する",
    inputSchema: {
      query: z.string().trim().min(1).max(100),
      limit: z.number().int().min(1).max(10).default(5),
    },
  },
  async ({ query, limit }) => {
    const normalizedQuery = query.toLocaleLowerCase("ja");

    const items = notes
      .filter((note) => {
        const text = `${note.title} ${note.body}`
          .toLocaleLowerCase("ja");
        return text.includes(normalizedQuery);
      })
      .slice(0, limit);

    return {
      content: [
        {
          type: "text",
          text: JSON.stringify(
            {
              query,
              count: items.length,
              items,
            },
            null,
            2
          ),
        },
      ],
    };
  }
);

inputSchema にZodスキーマを渡すと、空文字、長すぎる検索語、不正な件数をTool実行前に拒否できます。検索結果はMCPの content 配列へテキストとして返します。

Tool名と説明を具体的にする

項目 悪い例 改善例
名前 run search_notes
説明 データを処理する ノートのタイトルと本文からキーワード検索する
引数 data: string query と上限付き limit
戻り値 自由文だけ 件数と項目を持つJSON

名前が曖昧なToolを大量に公開すると、モデルが正しいものを選びにくくなります。1つのToolへ読み取り、更新、削除を詰め込まず、目的とリスクで分けます。

stdioトランスポートへ接続する

src/index.ts の末尾へ次の処理を追加します。

async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("note-search MCP server started");
}

main().catch((error: unknown) => {
  console.error("MCP server failed", error);
  process.exit(1);
});

stdioでは標準入力と標準出力をJSON-RPC通信に使います。通常ログを console.log() で標準出力へ書くとプロトコルを壊す可能性があるため、診断ログは標準エラーへ出します。

ログの注意:console.error() であっても、APIキー、アクセストークン、個人情報、ノート本文を無制限に出力しないでください。

ビルドして単体起動を確認する

npm run build
npm start

stdioサーバーは起動後にクライアントからのJSON-RPC入力を待つため、そのままでは画面に結果が出ません。標準エラーに起動メッセージが表示され、プロセスが待機すれば基本起動はできています。終了は Ctrl+C です。

実際のTool一覧取得と実行は、MCP対応クライアントまたは公式の検査ツールで確認します。クライアントごとに設定ファイルの場所と形式が異なるため、利用中クライアントの公式手順を参照してください。

MCPクライアントへ登録する考え方

{
  "mcpServers": {
    "note-search": {
      "command": "node",
      "args": [
        "/absolute/path/to/note-search-mcp/dist/index.js"
      ]
    }
  }
}

上は代表的な概念例です。設定キーや形式はクライアントによって異なります。相対パスやシェルエイリアスに頼らず、実行ファイルとビルド済みJavaScriptの絶対パスを使うと問題を切り分けやすくなります。

接続できないときの確認順

  1. npm run build が成功するか
  2. node dist/index.js が終了せず待機するか
  3. クライアント設定のcommandとargsが正しいか
  4. 標準出力へ通常ログを書いていないか
  5. クライアントを再起動または設定再読込したか
  6. クライアントのMCPログにimport・権限エラーがないか

データベース検索へ置き換える

サンプル配列を実データへ置き換える場合でも、Toolの入力と出力形式は維持できます。データベース接続文字列は環境変数やシークレット管理へ置き、クライアント設定やソースコードへ直接書きません。

async function searchNotes(
  query: string,
  limit: number,
  userId: string
) {
  return database.note.findMany({
    where: {
      ownerId: userId,
      searchableText: {
        contains: query,
      },
    },
    take: limit,
    select: {
      id: true,
      title: true,
      excerpt: true,
    },
  });
}

検索語をSQL文字列へ連結せず、パラメータ化されたクエリやORMを使います。さらに、利用者のIDや組織IDで必ず範囲を絞り、検索語だけで他人の文書へアクセスできないようにします。

stdioとStreamable HTTPの違い

接続 主な用途 追加で必要なもの
stdio ローカルでクライアントが子プロセス起動 実行パス、ローカル権限、標準入出力管理
Streamable HTTP リモート・共有サーバー TLS、認証・認可、セッション、Host検証、運用監視

公式v1ドキュメントでは、リモート向けにStreamable HTTP、ローカルのプロセス起動向けにstdioが案内されています。古いHTTP+SSEは後方互換用とされているため、新規実装は現行トランスポートを確認します。

安全なMCP Toolにする

  • 読み取りと書き込みを別Toolに分ける
  • 入力の型、長さ、件数、形式を検証する
  • 利用者・組織・対象データの認可を毎回行う
  • ファイルパス、URL、コマンドを許可リスト化する
  • タイムアウト、同時実行数、結果件数を制限する
  • 送信、削除、購入、権限変更は人の承認を求める
  • Tool名、実行者、対象、結果を監査ログへ残す
  • APIキーを最小権限で分離し、定期的に更新する

APIキーの保管とローテーションはAI APIキーの安全な管理方法も参考になります。

モデルの指示と権限を分ける

モデルが「すべてのノートを返して」と要求しても、MCPサーバーは利用者の権限と上限を優先します。モデルのツール引数は信頼できない外部入力として扱います。

モデルの要求
  ↓
Tool入力スキーマ
  ↓
アプリの認証・認可・業務ルール
  ↓
許可された範囲だけ実行
  ↓
必要最小限の結果を返す

Toolから始めた後の拡張

  • 定型文書を読むResourceを追加する
  • 業務用の指示テンプレートをPromptとして追加する
  • 検索結果へ構造化データを追加する
  • エラー結果に isError: true を付ける
  • リモート化が必要になった段階でStreamable HTTPを検討する

最初から認証付きリモートサーバー、複数Tool、データベース更新を同時に作ると、接続と業務ロジックの問題を切り分けにくくなります。読み取り専用の1 Toolで疎通を確認してから広げます。

よくある質問

MCPサーバーにLLMのAPIキーは必要ですか?

ノート検索のように外部処理だけを提供するサーバーなら、LLM APIキーは不要です。MCPクライアント側がモデルと接続します。Tool内から別APIを呼ぶ場合だけ、そのAPIの認証情報が必要です。

stdioサーバーをインターネットへ公開できますか?

stdioはローカルでクライアントがプロセスを起動する用途です。リモート提供にはStreamable HTTPを検討し、TLS、認証・認可、Host検証、セッション管理を追加します。

Toolと普通のREST APIは何が違いますか?

MCP Toolは、クライアントが一覧・スキーマを共通方式で取得し、モデルのツール利用へ接続できる点が特徴です。内部では既存REST APIを呼んでも構いません。認可や業務ロジックは既存API側にも残します。

まとめ

Node.jsのMCPサーバーは、公式TypeScript SDKの McpServer へToolを登録し、ローカル用途なら StdioServerTransport へ接続して作れます。入力はZodで制約し、結果をMCPのcontentとして返します。

動作確認後に実データへ置き換えるときは、認証・認可、最小権限、件数制限、監査ログを追加してください。SDKと仕様は更新されるため、バージョンを固定し、公式v1ドキュメントと利用クライアントの対応を確認して進めます。

スポンサーリンク