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による
queryとlimitの検証 - ローカルクライアント向けの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が追加した dependencies と devDependencies も残します。
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: "入力検証、最小権限、実行ログが重要です。",
},
];
name と version はクライアントがサーバーを識別する情報です。サービスのバージョン管理方針に合わせます。
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の絶対パスを使うと問題を切り分けやすくなります。
接続できないときの確認順
npm run buildが成功するかnode dist/index.jsが終了せず待機するか- クライアント設定のcommandとargsが正しいか
- 標準出力へ通常ログを書いていないか
- クライアントを再起動または設定再読込したか
- クライアントの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ドキュメントと利用クライアントの対応を確認して進めます。