自社システムやデータベースを安全にLLM(大規模言語モデル)と連携させたいと考えたとき、どのようなアーキテクチャを選択すべきでしょうか。
多くの開発現場では、LLMごとに独自のAPIコネクタを開発し、Function Callingを用いて個別に連携させる手法が採用されています。しかし、このアプローチは将来の技術的負債を意図的に抱え込むリスクを含んでいます。APIの仕様変更や新しいLLMの登場のたびに、膨大なメンテナンスコストが発生することは珍しくありません。
最新のLLMは、複雑なコーディングタスクや自律的な推論において非常に高い性能を示しています。このような高度な推論能力を持つAIに対して、自社の構造化データやツールをいかに正確かつ安全に渡すかが、システム開発における新たな焦点となっています。
本稿では、API連携の壁を突破する標準規格「MCP(Model Context Protocol)」のサーバ構築手法を、TypeScriptを用いたハンズオン形式で解説します。B2Bの実務で即座に応用できる「顧客データベース連携」をユースケースとし、コードレベルで具体的な実装手順と実務上の判断基準を解き明かしていきます。
なぜ今「MCP(Model Context Protocol)」なのか?基本概念と構築のメリット
MCP(Model Context Protocol)は、LLMと外部データソースやツールを接続するための標準化されたオープンプロトコルです。なぜ今、このプロトコルが重要視されているのでしょうか。その答えは「開発のスケール可能性」と「セキュリティの担保」にあります。
プロトコル統一がもたらす開発効率の向上
従来の独自連携とMCPを用いた連携の違いを、以下の比較図で確認してみてください。
【従来のFunction Calling連携】
LLM Client (A社モデル) ↔ 独自API変換層A ↔ 社内データベース
LLM Client (B社モデル) ↔ 独自API変換層B ↔ 社内データベース
※ LLMごとに個別のインターフェース開発・保守が必要
【MCP(Model Context Protocol)連携】
LLM Client (各種モデル) ↔ [標準化プロトコル] ↔ MCP Server ↔ 社内データベース
※ 一度のMCP Server構築で、プロトコル対応クライアントと共通連携が可能
このように、MCPサーバを構築することで、プロトコルに対応したAIクライアントから共通のインターフェースでアクセス可能になります。モデルごとにコネクタを作り直す手間が省け、開発リソースをビジネスロジックの改善に集中させることができます。
セキュリティと柔軟性の両立
公式ドキュメントに記載されている通り、MCPの通信方式として標準入出力(stdio)やHTTP通信などがサポートされています(※最新の仕様や推奨される通信方式については、Anthropic公式ドキュメント等で最新情報をご確認ください)。
社内の機密データを扱うB2Bシステムにおいては、外部のネットワークにデータベースを直接公開することなく、ローカル環境のstdio通信を通じてLLMに必要なデータだけを安全に提供するアーキテクチャが構築可能です。このセキュリティ上の優位性は、エンタープライズ環境でのAI導入において非常に重要な判断基準となります。
【準備】MCPサーバ構築に必要な環境セットアップ
実際に手を動かしてMCPサーバを構築していくための準備を進めます。今回は、型安全で保守性の高い開発が可能なTypeScriptを使用します。
Node.jsとTypeScriptの環境構築
まずは、開発の土台となるプロジェクトディレクトリを作成し、必要なパッケージをインストールします。Node.js環境は、現行のLTS(Long Term Support)バージョン以上を使用することを推奨します。
# プロジェクトディレクトリの作成と移動
mkdir customer-db-mcp
cd customer-db-mcp
# package.jsonの初期化
npm init -y
# TypeScriptと必要な開発ツールのインストール
npm install -D typescript @types/node tsx tsup
次に、TypeScriptの設定ファイル(tsconfig.json)を生成し、モダンなNode.js環境向けに設定を調整します。
npx tsc --init
生成された tsconfig.json を開き、以下の設定が有効になっていることを確認してください。現場での失敗例として、モジュール解決の挙動を安定させるために NodeNext を指定し忘れると、ES ModulesとCommonJSの混在によって予期せぬ実行時エラーが発生しやすくなります。
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "./build",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"]
}
MCP SDKのインストールと初期設定
続いて、MCPサーバを構築するための公式SDKをインストールします。※SDKのAPI名やメソッド仕様はアップデートされる可能性があるため、実装時は公式リポジトリやドキュメントを参照してください。
npm install @modelcontextprotocol/sdk
開発をスムーズに進めるため、package.json にビルドと実行用のスクリプトを追加しておきます。
"scripts": {
"build": "tsc",
"start": "node build/index.js",
"dev": "tsx src/index.ts"
}
『期待される実行結果』
エラーなくパッケージのインストールが完了し、package.json と tsconfig.json が正しく設定されていれば、開発環境の土台作りは完了です。
Step 1:サーバの基本構造とトランスポート層の実装
環境が整ったところで、MCPサーバの骨格となるメインファイルを実装していきます。src ディレクトリを作成し、その中に index.ts を配置します。
StdioTransportの基本実装とServerクラスのインスタンス化
MCPサーバは、クライアント(Claude Desktopなどのアプリ)と通信するために「トランスポート層」を必要とします。今回は最も基本的でセキュアな標準入出力(stdio)を用いた通信を実装します。
// src/index.ts
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
// 1. サーバのインスタンス化と基本情報の定義
const server = new Server(
{
name: "customer-db-mcp-server", // サーバの識別名
version: "1.0.0", // バージョン情報
},
{
capabilities: {
resources: {}, // 静的データ提供機能の有効化
tools: {}, // 動的ツール実行機能の有効化
},
}
);
// 2. 起動ロジックの実装
async function main() {
try {
// 標準入出力を用いたトランスポート層の初期化
const transport = new StdioServerTransport();
// サーバをトランスポートに接続
await server.connect(transport);
// 接続成功時のログ(※重要:標準エラー出力を使用すること)
console.error("Customer DB MCP Server is running securely on stdio");
} catch (error) {
console.error("Failed to start server:", error);
process.exit(1);
}
}
// 起動
main();
このコードにおいて、実務上非常に重要なポイントがあります。それは、ログ出力に console.error を使用している点です。標準入出力(stdio)通信では、console.log(標準出力)はMCPプロトコルのデータ送受信そのものに使用されます。
そのため、デバッグ目的で無意識に console.log を記述してしまうと、JSON-RPCの通信フォーマットが破壊され、原因不明の通信エラーを引き起こします。これはMCP開発において多くの開発者が直面する典型的なつまずきポイントです。
『期待される実行結果』
ターミナルで npm run dev を実行すると、Customer DB MCP Server is running securely on stdio というメッセージがエラー出力として表示され、プロセスが待機状態になります。これで通信の受け入れ準備が整いました。
Step 2:リソース(Resources)の定義:静的データの提供
MCPにおける「リソース」とは、LLMが読み取ることができる静的なデータ(ファイル内容、データベースの特定レコード、APIのレスポンスなど)を指します。ここでは、社内の顧客対応ガイドラインをLLMに提供する実装を行います。
【独自フレームワーク】ResourcesとToolsの使い分け基準
実装に入る前に、実務において「Resources」と後述する「Tools」をどう使い分けるべきか、明確な判断基準を持っておくことが重要です。
Resources(リソース)を使用すべきケース
- 事前にLLMに読み込ませたいコンテキスト情報
- 状態が変わらない、または変化が緩やかな静的データ
- 例:社内マニュアル、コーディング規約、製品カタログ
Tools(ツール)を使用すべきケース
- LLMからの引数に基づいて動的に結果が変わる処理
- 外部システムへの副作用(書き込み・更新)を伴うアクション
- 例:データベースの条件検索、チケットの起票、メール送信
この基準を誤ると、LLMが不必要なタイミングで巨大なデータを読み込もうとしたり、逆に静的なデータを取得するために無駄なツール呼び出しを繰り返したりといった非効率が生じます。
URIスキームを用いたリソース設計
リソースは一意のURI(例:custom://docs/guidelines)によって識別されます。リソースの一覧を提供するハンドラと、特定のリソースの内容を返すハンドラを実装します。
// src/index.ts に追記
import {
ListResourcesRequestSchema,
ReadResourceRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
// モックデータ:社内ガイドライン
const INTERNAL_GUIDELINES = `
# 顧客対応ガイドライン
1. 顧客からの問い合わせには24時間以内に一次返答を行うこと。
2. プレミアムプランの顧客には専任のサポート担当をアサインすること。
3. 解約リスクの高い顧客(Churned予備軍)にはマネージャー層が介入すること。
`;
// 1. 利用可能なリソースの一覧を提供するハンドラ
server.setRequestHandler(ListResourcesRequestSchema, async () => {
return {
resources: [
{
uri: "guidelines://internal/customer-support",
name: "社内顧客対応ガイドライン",
mimeType: "text/markdown",
description: "カスタマーサポート部門向けの基本対応ガイドライン",
},
],
};
});
// 2. リソースの内容を読み取るハンドラ
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
if (request.params.uri === "guidelines://internal/customer-support") {
return {
contents: [
{
uri: request.params.uri,
mimeType: "text/markdown",
text: INTERNAL_GUIDELINES,
},
],
};
}
// 存在しないURIが要求された場合のエラーハンドリング
throw new Error(`Resource not found: ${request.params.uri}`);
});
これにより、AIクライアントは「どのようなデータが存在するか」を一覧で取得し、プロンプトの文脈に応じて「具体的なデータの中身」を読み取ることができるようになります。
『期待される実行結果』
クライアントから ListResourcesRequest が送信されると、ガイドラインのメタデータが返却され、ReadResourceRequest が送信されるとマークダウン形式のテキストが返却される振る舞いが定義されます。
Step 3:ツール(Tools)の実装:AIにアクションを実行させる
リソースが「静的な読み取り」であるのに対し、「ツール」はLLMが自律的に引数を指定して実行できる「動的なアクション」です。B2Bシーンを想定し、「顧客データベースから特定の条件でリード情報を検索する」ツールを実装します。
リード情報の検索ツールの作成
ツールを定義する際は、LLMがどのような引数を渡すべきかを「JSON Schema」形式で正確に定義する必要があります。このスキーマ定義が曖昧だと、LLMが誤った引数を生成し、いわゆるハルシネーション(幻覚)に似た誤動作を引き起こす原因となります。
// src/index.ts に追記
import {
ListToolsRequestSchema,
CallToolRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
// モックデータ:顧客データベース
const CUSTOMER_DB = [
{ id: "C001", name: "株式会社アルファ", plan: "Premium", status: "Active" },
{ id: "C002", name: "ベータ工業", plan: "Standard", status: "Churned" },
{ id: "C003", name: "ガンマ商事", plan: "Premium", status: "Active" },
{ id: "C004", name: "デルタシステムズ", plan: "Enterprise", status: "Active" },
];
// 1. 利用可能なツールの一覧を提供するハンドラ
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "search_customers",
description: "プランやステータスに基づいて顧客データベースを検索します。",
inputSchema: {
type: "object",
properties: {
plan: {
type: "string",
description: "検索するプラン名(例: Premium, Standard, Enterprise)",
},
status: {
type: "string",
description: "顧客のステータス(例: Active, Churned)",
},
},
// 柔軟な検索を許可するため、必須プロパティ(required)は指定しない
},
},
],
};
});
// 2. ツールの実行を処理するハンドラ
server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === "search_customers") {
// 引数の取得と型キャスト
const args = request.params.arguments as { plan?: string; status?: string } | undefined;
// 検索ロジックの実装
let results = CUSTOMER_DB;
if (args?.plan) {
results = results.filter((c) => c.plan === args.plan);
}
if (args?.status) {
results = results.filter((c) => c.status === args.status);
}
// 結果を文字列としてフォーマットして返却
return {
content: [
{
type: "text",
text: JSON.stringify(results, null, 2),
},
],
};
}
throw new Error(`Tool not found: ${request.params.name}`);
});
この実装により、LLMはユーザーからの曖昧な指示(例:「今アクティブなプレミアム顧客を教えて」)を解釈し、自律的に search_customers ツールを適切な引数({"plan": "Premium", "status": "Active"})で呼び出すことが可能になります。
『期待される実行結果』
ツール呼び出しに対して、条件に合致する顧客のJSONデータがテキスト形式で返却されます。
Step 4:デバッグとClaude Desktopへの統合テスト
構築したMCPサーバが正しく動作するかを検証します。まずは単体テストを行い、その後実際のAIクライアントに統合します。
mcp-inspectorによる単体テスト
公式のインスペクターツールを使用すると、ブラウザ上で簡単にMCPサーバの動作確認が可能です。
# プロジェクトをビルド
npm run build
# インスペクターを使用してサーバを起動
npx @modelcontextprotocol/inspector node build/index.js
コマンドを実行するとローカルサーバーが立ち上がり、ブラウザでUIが開きます。ここで「Resources」タブや「Tools」タブから、先ほど実装した機能が正しく認識され、期待通りのレスポンスが返ってくるかを確認してください。
config.jsonの編集と実機検証
単体テストで問題がなければ、Claude Desktopなどのクライアントにサーバを統合します。設定ファイル(claude_desktop_config.json)を編集します。
※設定ファイルの配置場所はOSやアプリケーションのバージョンによって異なる場合があります。最新の正確なパスは公式ドキュメントをご確認ください。以下は一般的な配置場所の例です。
- macOS:
~/Library/Application Support/Claude/claude_desktop_config.json - Windows:
%APPDATA%\Claude\claude_desktop_config.json
{
"mcpServers": {
"customer-db": {
"command": "node",
"args": ["/絶対パス/customer-db-mcp/build/index.js"]
}
}
}
設定を保存し、クライアントアプリを再起動します。UI上で外部ツールとして認識されていれば統合は完了です。チャットで「Premiumプランの顧客を検索して、社内ガイドラインに照らし合わせた対応方針を提案して」と入力してみてください。AIが自律的にデータベース検索ツールを実行し、リソースからガイドラインを読み取り、総合的な回答を生成すれば、統合テストは成功です。
実運用に向けたベストプラクティスとセキュリティ対策
ここまでの手順で、基本的なMCPサーバの構築と連携が完了しました。しかし、概念実証(PoC)の段階から本番環境への導入へと進めるにあたり、システムを堅牢に保つためのチェックリストを整理しておく必要があります。
【独自フレームワーク】本番導入判断の4つのチェックポイント
MCPサーバを実業務に適用する際は、以下の4つの観点からアーキテクチャを評価してください。
クレデンシャルの分離(認証・認可)
今回のチュートリアルではモックデータを使用しましたが、実際には外部のデータベースやSaaSのAPIと連携することになります。APIキーやデータベースの接続情報をコード内に直接記述することは厳禁です。dotenvなどのパッケージを使用し、環境変数として外部から注入する設計を徹底してください。入力値の厳密なバリデーション
LLMは確率的なモデルであるため、常にスキーマ通りの完璧な引数を生成するとは限りません。予期せぬデータ型や不正なパラメータでツールが呼び出されるケースを想定し、Zodなどのスキーマ検証ライブラリを活用して入力値のバリデーションを厳密に行うことが推奨されます。エラーハンドリングとリカバリ
システムがクラッシュしないよう、適切なエラーメッセージをLLMに返す堅牢なエラーハンドリングを実装してください。「検索結果が多すぎます。条件を絞ってください」といった人間向けのメッセージを返すことで、LLMは自律的に引数を修正して再試行してくれます。レート制限(Rate Limiting)と負荷対策
ツール内で高コストなAPI呼び出しや重いデータベースクエリを実行する場合、LLMの自律的なループ処理によってバックエンドリソースが枯渇するリスクがあります。実運用においては、MCPサーバ側でレート制限を設け、予期せぬ負荷スパイクを防ぐ設計を取り入れることが不可欠です。
次のステップへ
MCPを活用することで、社内のあらゆるデータやシステムを、安全かつシームレスにAIのコンテキストに組み込むことができます。この技術は、単なる業務効率化を超えて、企業のデータ活用戦略そのものを根本から変革するポテンシャルを持っています。
自社システムへの適用を検討する際は、まずは小規模なデータセットでの検証から始めるのが効果的です。個別のセキュリティ要件や、既存システムとの具体的な連携アプローチについて確認したい場合は、専門家のアドバイスを受けながら実際の動作環境を体験できる無料デモや14日間トライアルを活用し、自社における有用性を確かめてみることをおすすめします。
コメント