恐竜本舗

エンジニアをしている恐竜の徒然日記です。

PostgreSQL MCP サーバを作った

はじめに

仕事データ分析等で、データベースのスキーマを取得して各種のテーブルを調べたり、売上分析やKPI分析など色々な点でデータベースを参照することが多くある。

しばらくは、下記の公式なPostgreSQL MCP があったのでこちらを利用していた。

github.com

しかし、気づいたらこちらがアーカイブされていて、利用しているsdk も古くて今後メンテもされなそうなので、いっそ勉強がてら自分で作ることにした。

github.com

www.npmjs.com

MCP の基本的な部分の振り返りも兼ねて書いてみる。

Model Context Protocol(MCP) の概観

Model Context Protocol(MCP)とは、Anthropic社が公開しているオープンソースのプロトコルのこと。

アプリケーションがLLMに対して何かしらのコンテキストを提供する際のプロトコルを標準化したものになる。

MCP のアーキテクチャ

flowchart LR
    subgraph "Host"
        client1[MCP Client]
        client2[MCP Client]
    end
    subgraph "Server Process"
        server1[MCP Server]
    end
    subgraph "Server Process"
        server2[MCP Server]
    end

    client1 <-->|Transport Layer| server1
    client2 <-->|Transport Layer| server2

ref: Core architecture - Model Context Protocol

MCP は大きく3つのコンポーネントから成り立っている。

  • host
    • host は接続を開始するLLMアプリケーション(Claude DesktopやIDEなど)
  • client
    • client はホストアプリケーション内でサーバーと1:1の接続を維持するもの
  • server
    • server はクライアントに context、tool、prompt を提供する
flowchart LR
    subgraph "Your Computer"
        Host["Host with MCP Client<br/>(Claude, IDEs, Tools)"]
        S1["MCP Server A"]
        S2["MCP Server B"]
        D1[("Local<br/>Data Source A")]
        Host <-->|"MCP Protocol"| S1
        Host <-->|"MCP Protocol"| S2
        S1 <--> D1
    end
    subgraph "Internet"
        S3["MCP Server C"]
        D2[("Remote<br/>Service B")]
        D3[("Remote<br/>Service C")]
        S2 <-->|"Web APIs"| D2
        S3 <-->|"Web APIs"| D3
    end
    Host <-->|"MCP Protocol"| S3

ref: Introduction - Model Context Protocol

MCP の基本的な形としては上記のようになる。

  • MCP クライアントから、複数のMCPサーバにアクセスすることができる
  • MCPサーバは、ローカルのデータソースやリモート上の各アプリケーションに接続できる
  • アプリケーションが返却する情報は、特定のスキーマに沿って取得される

これが基本的なMCPの仕組み。

Typescript SDK を用いて、PostgreSQL MCPツールを作ってみる

MCP サーバの作成には、 各種言語のSDKが提供されているため、そちらを使って作成することができる。

github.com

仕組みは割とシンプルで、下記のようにしてMCP サーバインスタンスを立て、メタデータの設定を行う。

  const server = new McpServer({
    name: SERVER_NAME,
    version: SERVER_VERSION,
  });

あとはこれに対し、MCP tool などの処理を載せていくだけ。

今回の場合、 MCP toolで 渡した query が実行されるようにしたいので、下記のような設定になる。

  const pool = new pg.Pool({
    connectionString: databaseUrl,
  });

  const resourceHandlers = createResourceHandlers(pool);
  const toolHandlers = createToolHandlers(pool);

  server.registerTool(
    'query',
    {
      description: 'Execute read-only SQL queries',
      inputSchema: {
        sql: z.string().describe('SQL query to execute'),
      },
    },
    async (input: { sql: string }) => {
      const result = await toolHandlers.executeQuery(input);

      return result.match(
        (rows) => ({
          content: [
            {
              type: 'text',
              text: JSON.stringify(rows, null, 2),
            },
          ],
        }),
        (error) => {
          throw new Error(
            `Query error: ${error.message}${error.detail ? ` (${error.detail})` : ''}`,
          );
        },
      );
    },
  );

server.registerTool で tool の登録ができる。

第一引数がツール名、第二引数が descriptioninputSchema などの各種設定。 inputSchema によって、LLM がMCPサーバに対してどんな値を渡せばよいかを定義している。

これにより、MCP Client は下記のように MCP サーバの説明や必要な引数を理解することができる。

最後にコールバックとして、inputSchema の情報を引数として受け取り、ここに実際に行いたい処理を書く。

今回の場合、inputSchema で 発火させたい sql を受け取り、 このcallback で query の実行を行っている。

諸々の tool の登録が終わったら、最後に connect する。

  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error(`${SERVER_NAME} MCP server running on stdio`);

内容自体は至ってシンプルで、思っていたよりもサクッとMCPサーバを作成することができた。

使い方

README のとおりだが、 npx 経由でそのままも使えるし、 ローカルに落としてbuild すれば 直接 node コマンドで動くようにしている。

docker で動かすかは迷ったが、 npx で動けば個人的には困らなそうなのでやってない。

{
  "mcpServers": {
    "postgres": {
      "command": "npx",
      "args": ["@daitasu/postgres-mcp", "postgresql://user:password@localhost:5432/database"]
    }
  }
}

色々作って繋いでいくと面白そうなので、今後は monorepo にしていろんな MCP を作っていきたいな。

github.com