はじめに
本記事で紹介する Model Context Protocol (MCP) サーバーは、クラウドや社内ネットワーク上のリモートサーバーではなく、ローカル PC 上で動作するローカルサーバーです。
最終的に Claude Desktop との通信も標準入出力(stdio)経由で行います。
この記事でやること(先に概要)
- MCP サーバー(TypeScript)を用意
- MCP サーバーを作成
- dotenv 経由で環境変数を読み込む
- Tools として SELECT クエリ専用の読み取り API を公開
- SELECT 専用の安全実行
- エラーや実行 SQL をログファイルに出力
- MCP Inspector を使って動作確認
- Claude Desktop に mcp 設定を追加し、「自然言語 → SQL」の対話フローを実現
ℹ️ 補足:本記事では DB (Azure SQL Database) への接続設定方法については扱いません。接続情報はご自身の環境に合わせて設定してください。
🧪 成果:Claude に「直近のアクセスログを教えて」と話しかけると、MCP サーバーが DB に安全にアクセスし、サマリーを返してくれる。
背景とねらい
- ユーザーは自然言語で、DB に保管されている社内情報へアクセスしたい。
- 従来は、 LangChain などのフレームワークを用いて 自然言語 → SQL 変換処理を自前で組み上げる必要があった。
- MCP を利用することで、ツール連携や入出力のやり取りを標準化でき、自然言語から SQL への変換を LLM 側に委ねる設計を取りやすくなる。
その結果、専用のオーケストレーション層をゼロから構築する工数を大幅に削減できる。 - まずはローカル環境で MCP サーバーを構築し、期待通り動作するか検証する。
全体アーキテクチャ
Claude Desktop ──(MCP stdio)──> Node.js MCP Server ──> Azure SQL Database
▲ ▲
│ │
自然言語プロンプト Entra ID 認証
- 通信は stdio を使用
- DB 接続には Entra ID 認証を使用
事前準備
1) 環境変数(例)
ルートディレクトリに .env を作成し、接続先の DB 情報などを記載します。
# DB 接続
DB_SERVER=your-sql-server.database.windows.net
DB_PORT=1433
DB_NAME=your_db
# 認証
AUTH_TYPE=azure-active-directory-default
AZURE_TENANT_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
2) パッケージ
必要な npm パッケージをインストールします。
npm i @modelcontextprotocol/sdk mssql dotenv @azure/identity
実装ハイライト
MCP サーバーを作成
以下のコードは、ローカルで MCP サーバーを実行するための最小構成です。
ここでは src/index.ts に記載している想定です。
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import dotenv from "dotenv";
import { dirname, resolve } from "path";
import { fileURLToPath } from "url";
const server = new Server(
{
name: "xxx-server",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
}
);
// ツール一覧
server.setRequestHandler(ListToolsRequestSchema, async () => {
// 後述
});
// ツール実行
server.setRequestHandler(CallToolRequestSchema, async (request) => {
// 後述
});
// サーバー起動
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
}
main().catch(() => process.exit(1));
dotenv 経由で環境変数を読み込む
筆者がハマったポイントとして、dotenv.config() を main 関数の外に書くと、server.connect() の前に実行されてしまい、stdio の通信に混入してテスト時にエラーになりました。
dotenv の呼び出し位置には注意が必要です。
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
// 現在のファイルディレクトリを取得
const __dirname = dirname(fileURLToPath(import.meta.url));
// src / dist どちらから実行してもルートを指すよう調整
const rootDir = resolve(__dirname, '..', '..');
const envPath = resolve(rootDir, '.env');
dotenv.config({ path: envPath });
}
Tools として SELECT クエリ専用 API を公開
SELECT クエリを安全に実行できるツールを公開します。
スキーマやテーブル一覧の取得も可能です。
(セキュリティ上の制約など、改善案があればコメントでぜひ教えてください 🙏)
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "query_database",
description: "SQL Server データベースに対して SELECT クエリを実行します(読み取り専用)",
inputSchema: {
type: "object",
properties: {
query: {
type: "string",
description: "実行する T-SQL クエリ(SELECT のみ)",
},
},
required: ["query"],
},
},
{
name: "get_table_schema",
description: "テーブルのスキーマ情報を取得します",
inputSchema: {
type: "object",
properties: {
table_name: {
type: "string",
description: "テーブル名(スキーマ付き可: dbo.Customers)",
},
},
required: ["table_name"],
},
},
{
name: "list_tables",
description: "データベース内のテーブル一覧を取得します",
inputSchema: {
type: "object",
properties: {
schema: {
type: "string",
description: "スキーマ名(デフォルト: dbo)",
default: "dbo",
},
},
},
}
],
};
});
SELECT 専用の安全実行
CallToolRequestSchema の詳細な実装(DB 接続処理をふくむ部分)は本記事では割愛しますが、ここではツール query_database について補足します。
このツールでは自由に SQL クエリを入力できる仕様にしています。
ただし、誤って更新系の SQL を実行してしまうリスクを防ぐために、 DB の権限を読み取り専用に設定することに加えて、以下のような簡易的なクエリ制限を追加しています。
function isSafeQuery(query: string): boolean {
const normalized = query.trim().toUpperCase();
// SELECT または WITH で始まるかチェック
const isSelectQuery = /^(SELECT|WITH)\b/.test(normalized);
if (!isSelectQuery) return false;
// 危険なキーワード一覧
const dangerous = [
"DROP", "DELETE", "INSERT", "UPDATE", "ALTER",
"CREATE", "TRUNCATE", "EXEC", "EXECUTE", "SP_", "XP_"
];
// 危険キーワードが含まれていれば false
const containsDangerous = dangerous.some(keyword =>
new RegExp(`\\b${keyword}\\b`, "i").test(normalized)
);
return !containsDangerous;
}
エラーや実行 SQL をログファイルに出力
stderr とログファイルの両方に出力することで、通信を壊さずに記録を残せます(MCP クライアントは stderr をログ扱いで無視するため安全)。
function log(level: LogLevel, message: string, data?: any) {
const timestamp = new Date().toISOString();
const logMessage = data
? `[${timestamp}] [${level}] ${message} ${JSON.stringify(data)}`
: `[${timestamp}] [${level}] ${message}`;
// stderr 出力(MCP 要件)
console.error(logMessage);
// ファイルにも追記
try {
appendFileSync(LOG_FILE, logMessage + '\n', 'utf8');
} catch (error) {
console.error(`Failed to write log to file: ${error}`);
}
}
MCP Inspector で動作確認
以下のコマンドから MCP Inspector を起動します。
npx @modelcontextprotocol/inspector tsx src/index.ts
実行するとブラウザが起動し、MCP サーバーのテスト UI が表示されます。
Connect → List Tools をクリックすると、サーバーが公開しているツール一覧を確認できます。
get_table_schema を選んで実行すれば、指定テーブルのスキーマ情報を取得可能です。
Claude Desktop
MCP 設定
ここまで動作確認ができたら、Claude Desktop に MCP 設定を追加します。
{
"mcpServers": {
"qiita-mcp": {
"command": "node",
"args": ["/absolute/path/to/dist/index.js"]
}
}
}
再起動後、「設定 → コネクタ」に MCP サーバーを確認できます。正しく設定されていれば、ツール一覧を開いて接続できます。
いざ実演 — Claude に話しかける
以下のスクショのように Claude に話しかけると、MCP サーバーが SQL を生成し、DB から結果を取得して回答してくれます。
ここまでやってみて(所感)
LLM(Claude)を通じて自然言語で社内 DB の情報を取得できることを確認しました。
SQL 変換処理を一切書かず実現できた点に、非常に可能性を感じます。
MCP は “AI の USB-C” とも言われており、Claude Desktop 以外のクライアントからも同様に接続可能になると考えています。
今後は、特定の売上ビュー専用ツールなどを実装し、より実務的な応用を進めたいと考えています。
また、今回はローカル実行でしたが、次は Streamable HTTP サーバー としてリモートデプロイにも挑戦したいです。
改善点や提案などがあれば、ぜひコメントで教えてください!