Model Context Protocol(MCP)は、AIアシスタントを外部リソースに接続するための革新的なプロトコルです。本稿では、Claude DesktopとMCPを組み合わせ、データベース、ファイルシステム、Webリソースへの安全な接続方法を詳細に解説します。HolySheep AI🔥の高パフォーマンスAPIを活用し、本番環境に対応したアーキテクチャを構築していきます。
MCP(Model Context Protocol)のアーキテクチャ概要
MCPは、ホストアプリケーション(Claude Desktop)と外部リソース間の通信を標準化するプロトコルです。以下の三層アーキテクチャで構成されます:
- MCP Host:Claude Desktop等のAIクライアント
- MCP Client:ホストとサーバー間の通信を管理
- MCP Server:データベース、ファイルシステム等のリソースへのアクセスを提供
HolySheep AIの<50msレイテンシを活用することで、MCP経由でのリソースアクセスも低遅延で実行可能です。レートは¥1=$1という競合 대비85%節約的成本で運用できます。
プロジェクト構成と環境構築
まず、MCPサーバーを含むプロジェクト構造を作成します。HolySheep AIへの接続부터 실제 리소스 연동까지 순차적으로 설명하겠습니다。
// project-structure/
// ├── src/
// │ ├── servers/
// │ │ ├── database-mcp-server.ts
// │ │ ├── filesystem-mcp-server.ts
// │ │ └── web-mcp-server.ts
// │ ├── clients/
// │ │ └── holysheep-client.ts
// │ └── index.ts
// ├── config/
// │ └── mcp-config.json
// └── package.json
// 必要なパッケージのインストール
{
"dependencies": {
"@anthropic-ai/claude-code": "^0.2.0",
"@modelcontextprotocol/sdk": "^0.5.0",
"pg": "^8.11.0",
"better-sqlite3": "^9.4.0",
"playwright": "^1.40.0"
}
}
HolySheep AIクライアントの実装
MCPサーバーから受け取ったリクエストをHolySheep AIで処理するクライアントを実装します。base_urlには必ずhttps://api.holysheep.ai/v1を使用してください。
// src/clients/holysheep-client.ts
import OpenAI from 'openai';
const HOLYSHEEP_API_KEY = process.env.HOLYSHEEP_API_KEY || 'YOUR_HOLYSHEEP_API_KEY';
const HOLYSHEEP_BASE_URL = 'https://api.holysheep.ai/v1';
const client = new OpenAI({
apiKey: HOLYSHEEP_API_KEY,
baseURL: HOLYSHEEP_BASE_URL,
timeout: 30000,
maxRetries: 3,
});
interface MCPRequest {
tool: 'database' | 'filesystem' | 'web';
action: string;
params: Record;
}
export async function processMCPRequest(request: MCPRequest): Promise {
const systemPrompt = buildSystemPrompt(request.tool);
const userPrompt = buildUserPrompt(request);
try {
const response = await client.chat.completions.create({
model: 'claude-sonnet-4-20250514', // HolySheepでClaude Sonnet 4.5が利用可能
messages: [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: userPrompt }
],
temperature: 0.3,
max_tokens: 4096,
});
return response.choices[0]?.message?.content || 'No response generated';
} catch (error) {
console.error('HolySheep API Error:', error);
throw new Error(API request failed: ${error.message});
}
}
function buildSystemPrompt(tool: string): string {
const prompts = {
database: `あなたはPostgreSQL、MySQL、SQLiteに精通したデータベースExpertです。
安全で最適化されたSQLクエリを生成してください。SQLインジェクションを防ぐため、
パラメータ化されたクエリを常に使用してください。`,
filesystem: `あなたはファイルシステム操作のExpertです。
/path/to/file 形式のパスを正しく解釈し、安全なファイル操作を実行します。
絶対パスを使用し、相対パスによるディレクトリトラバーサル攻撃を防ぎます。`,
web: `あなたはWebスクレイピングとAPI呼び出しのExpertです。
robots.txtを尊重し、Webサイトへの負荷を最小限に抑えます。
WebFetchやBrowse的工具を適切に選択します。`
};
return prompts[tool] || 'General purpose AI assistant';
}
function buildUserPrompt(request: MCPRequest): string {
return `Action: ${request.action}
Parameters: ${JSON.stringify(request.params, null, 2)}
Result: 上記のアクションを実行し、結果を詳細に説明してください。`;
}
export default client;
データベースMCPサーバーの実装
PostgreSQLとSQLiteの両方に対応した汎用的なデータベースMCPサーバーを作成します。接続プール管理和トランザクション制御を含みます。
// src/servers/database-mcp-server.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 { Pool, PoolConfig } from 'pg';
import Database from 'better-sqlite3';
interface DatabaseConfig {
type: 'postgresql' | 'sqlite';
connection: PoolConfig | string;
}
class DatabaseMCPServer {
private server: Server;
private pgPool: Pool | null = null;
private sqliteDb: Database.Database | null = null;
private dbConfig: DatabaseConfig;
constructor(config: DatabaseConfig) {
this.dbConfig = config;
this.server = new Server(
{ name: 'database-mcp-server', version: '1.0.0' },
{ capabilities: { tools: {} } }
);
this.setupDatabase();
this.setupToolHandlers();
}
private setupDatabase(): void {
if (this.dbConfig.type === 'postgresql') {
const poolConfig: PoolConfig = {
...this.dbConfig.connection as PoolConfig,
max: 20, // 接続プール最大サイズ
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 5000,
};
this.pgPool = new Pool(poolConfig);
this.pgPool.on('error', (err) => {
console.error('Unexpected PostgreSQL error:', err);
});
} else {
this.sqliteDb = new Database(this.dbConfig.connection as string);
this.sqliteDb.pragma('journal_mode = WAL');
this.sqliteDb.pragma('synchronous = NORMAL');
}
}
private setupToolHandlers(): void {
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: 'query',
description: 'SQLクエリを実行して結果を取得します(SELECT専用)',
inputSchema: {
type: 'object',
properties: {
sql: { type: 'string', description: '実行するSQLクエリ' },
params: { type: 'array', description: 'パラメータ化された値' }
},
required: ['sql']
}
},
{
name: 'execute',
description: 'INSERT/UPDATE/DELETEなどのデータ操作を実行します',
inputSchema: {
type: 'object',
properties: {
sql: { type: 'string', description: '実行するSQLクエリ' },
params: { type: 'array', description: 'パラメータ化された値' }
},
required: ['sql']
}
},
{
name: 'describe_table',
description: 'テーブル構造を取得します',
inputSchema: {
type: 'object',
properties: {
table: { type: 'string', description: 'テーブル名' }
},
required: ['table']
}
}
]
}));
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case 'query':
return await this.handleQuery(args.sql, args.params);
case 'execute':
return await this.handleExecute(args.sql, args.params);
case 'describe_table':
return await this.handleDescribe(args.table);
default:
throw new Error(Unknown tool: ${name});
}
} catch (error) {
return {
content: [{
type: 'text',
text: Error: ${error.message}
}],
isError: true
};
}
});
}
private async handleQuery(sql: string, params: unknown[] = []): Promise {
// SQLインジェクション防止:SELECT文のみ許可
const trimmedSql = sql.trim().toUpperCase();
if (!trimmedSql.startsWith('SELECT')) {
throw new Error('クエリツールではSELECT文のみ許可されています');
}
if (this.pgPool) {
const result = await this.pgPool.query(sql, params);
return {
content: [{
type: 'text',
text: JSON.stringify({ rows: result.rows, rowCount: result.rowCount }, null, 2)
}]
};
} else if (this.sqliteDb) {
const stmt = this.sqliteDb.prepare(sql);
const rows = stmt.all(...params);
return {
content: [{
type: 'text',
text: JSON.stringify({ rows, rowCount: rows.length }, null, 2)
}]
};
}
throw new Error('データベース接続が確立されていません');
}
private async handleExecute(sql: string, params: unknown[] = []): Promise {
if (this.pgPool) {
const client = await this.pgPool.connect();
try {
await client.query('BEGIN');
const result = await client.query(sql, params);
await client.query('COMMIT');
return {
content: [{
type: 'text',
text: JSON.stringify({ affectedRows: result.rowCount, lastInsertId: null }, null, 2)
}]
};
} catch (error) {
await client.query('ROLLBACK');
throw error;
} finally {
client.release();
}
} else if (this.sqliteDb) {
const result = this.sqliteDb.prepare(sql).run(...params);
return {
content: [{
type: 'text',
text: JSON.stringify({ affectedRows: result.changes, lastInsertId: result.lastInsertRowid }, null, 2)
}]
};
}
throw new Error('データベース接続が確立されていません');
}
private async handleDescribe(table: string): Promise {
// テーブル名のバリデーション(SQLインジェクション防止)
const tableName = table.replace(/[^a-zA-Z0-9_]/g, '');
if (this.pgPool) {
const result = await this.pgPool.query(
`SELECT column_name, data_type, is_nullable, column_default
FROM information_schema.columns
WHERE table_name = $1
ORDER BY ordinal_position`,
[tableName]
);
return { content: [{ type: 'text', text: JSON.stringify(result.rows, null, 2) }] };
} else if (this.sqliteDb) {
const stmt = this.sqliteDb.prepare(PRAGMA table_info("${tableName}"));
const rows = stmt.all();
return { content: [{ type: 'text', text: JSON.stringify(rows, null, 2) }] };
}
throw new Error('データベース接続が確立されていません');
}
async start(): Promise {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error('Database MCP Server running on stdio');
}
async stop(): Promise {
if (this.pgPool) await this.pgPool.end();
if (this.sqliteDb) this.sqliteDb.close();
await this.server.close();
}
}
// 使用例
const server = new DatabaseMCPServer({
type: 'postgresql',
connection: {
host: process.env.DB_HOST || 'localhost',
port: parseInt(process.env.DB_PORT || '5432'),
database: process.env.DB_NAME || 'mydb',
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
}
});
server.start().catch(console.error);
ファイルシステムMCPサーバーの実装
セキュアなファイル操作を提供するMCPサーバーを実装します。ディレクトリトラバーサル攻撃を防ぎつつ、Claudeがファイルシステムを安全に操作できるようにします。
// src/servers/filesystem-mcp-server.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 * as fs from 'fs/promises';
import * as path from 'path';
class FilesystemMCPServer {
private server: Server;
private allowedBasePath: string;
constructor(basePath: string = process.cwd()) {
this.allowedBasePath = path.resolve(basePath);
this.server = new Server(
{ name: 'filesystem-mcp-server', version: '1.0.0' },
{ capabilities: { tools: {} } }
);
this.setupToolHandlers();
}
private async validatePath(requestedPath: string): Promise {
const resolved = path.resolve(this.allowedBasePath, requestedPath);
if (!resolved.startsWith(this.allowedBasePath)) {
throw new Error('許可されたディレクトリ外へのアクセスは拒否されました');
}
return resolved;
}
private setupToolHandlers(): void {
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: 'read_file',
description: 'ファイルを読み取ります',
inputSchema: {
type: 'object',
properties: {
path: { type: 'string', description: 'ファイルパス' },
encoding: { type: 'string', default: 'utf-8' }
},
required: ['path']
}
},
{
name: 'write_file',
description: 'ファイルに書き込みます(ディレクトリ作成含む)',
inputSchema: {
type: 'object',
properties: {
path: { type: 'string', description: 'ファイルパス' },
content: { type: 'string', description: '書き込む内容' }
},
required: ['path', 'content']
}
},
{
name: 'list_directory',
description: 'ディレクトリの内容を一覧表示します',
inputSchema: {
type: 'object',
properties: {
path: { type: 'string', default: '.' }
}
}
},
{
name: 'file_stats',
description: 'ファイルまたはディレクトリの統計情報を取得します',
inputSchema: {
type: 'object',
properties: {
path: { type: 'string', description: 'ファイルパス' }
},
required: ['path']
}
}
]
}));
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
let result: string;
switch (name) {
case 'read_file':
result = await this.readFile(args.path, args.encoding);
break;
case 'write_file':
result = await this.writeFile(args.path, args.content);
break;
case 'list_directory':
result = await this.listDirectory(args.path);
break;
case 'file_stats':
result = await this.fileStats(args.path);
break;
default:
throw new Error(Unknown tool: ${name});
}
return { content: [{ type: 'text', text: result }] };
} catch (error) {
return {
content: [{ type: 'text', text: Error: ${error.message} }],
isError: true
};
}
});
}
private async readFile(filePath: string, encoding: string = 'utf-8'): Promise {
const resolved = await this.validatePath(filePath);
const stats = await fs.stat(resolved);
if (stats.isDirectory()) {
throw new Error('指定されたパスはディレクトリです');
}
// 10MB以上のファイルは分割読み込み
if (stats.size > 10 * 1024 * 1024) {
const content = await fs.readFile(resolved);
return `File is too large (${stats.size} bytes). First 10KB preview:\n${