Model Context Protocol(MCP)は、AIアシスタントを外部リソースに接続するための革新的なプロトコルです。本稿では、Claude DesktopとMCPを組み合わせ、データベース、ファイルシステム、Webリソースへの安全な接続方法を詳細に解説します。HolySheep AI🔥の高パフォーマンスAPIを活用し、本番環境に対応したアーキテクチャを構築していきます。

MCP(Model Context Protocol)のアーキテクチャ概要

MCPは、ホストアプリケーション(Claude Desktop)と外部リソース間の通信を標準化するプロトコルです。以下の三層アーキテクチャで構成されます:

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${