STORES Product Blog

こだわりを持ったお商売を支える「STORES」のテクノロジー部門のメンバーによるブログです。

BigQuery MCPサーバーを作ってみる

はじめに

はじめまして。 データ本部でデータエンジニアをやっている@takaHALです。

最近、MCP(Model Context Protocol)を活用した様々なツールが登場し、ClaudeやCursorなどのAIツールでできることが急速に拡大していますよね。

今回は、MCPへの理解を深めるため、実際にMCPサーバーを作ってみることにしました。弊社STORESではデータウェアハウスとしてBigQueryを使用しているので、BigQueryにアクセスできるMCPサーバーの実装に挑戦します。

今回作るもの

作成するMCPサーバーでは、最終的にClaudeDesktopから以下の3つの操作ができることを目指します

  1. データセット一覧の取得 - プロジェクト内の全データセットを確認
  2. テーブル一覧の取得 - 特定データセット内のテーブルとスキーマ情報を取得
  3. SQLクエリの実行 - 任意のSQLを実行して結果を取得

SQL作成はClaude自身が行うか、ユーザーが直接SQLを渡すことを想定しています。

実装の仕組み

実現したい機能をそれぞれ「tool」として実装し、MCPサーバーに登録します。

// Nodeの例
server.tool(
  "ツール名",
  "ツールの機能説明",
  async () => {
    // toolの実装
    const result = 'hoge'
    return {
      content: [
        {
          type: "text",
          text: JSON.stringify(result)
        }
      ]
    };
  }
)

登録されたtoolは、MCPクライアント(Claude等)は、ユーザーの指示に応じて適切なtoolを選択・実行してくれます。

開発環境のセットアップ

公式のクイックスタートをみながら進めていきます。

必要な環境

  • Node.js
  • Google Cloud Platform のセットアップ
  • BigQuery APIの有効化
  • MCPにアクセス可能なツール(Claude/Cursor)

事前準備

BigQueryに検証用データを登録する

今回はTableauが公開しているサンプルデータスーパーストアの日本語化データを使用します。

以下の2つのテーブルを作成しました

  • orders - 注文データ
  • returns - 返品データ

GoogleCloud CLIで認証

Google Cloud CLIをインストール後、セットアップ済みのBigQueryにアクセスできるアカウントで認証します。

gcloud auth application-default login

認証方式について

今回作成するMCPサーバーは以下の2つの認証方式に対応しています

  • デフォルト認証情報(本記事で使用)
    • 上記コマンドで設定される認証情報を自動的に使用
  • サービスアカウント
    • 以下の3つのロールが必要
      • BigQuery ジョブユーザー
      • BigQuery データ閲覧者
      • BigQuery メタデータ閲覧者

実運用時は、アクセスを許可するデータセットに対してのみ「データ閲覧者」と「メタデータ閲覧者」のロールを付与するようにしましょう。

プロジェクトの初期化

mkdir bigquery-mcp
cd bigquery-mcp
npm init -y
npm install @modelcontextprotocol/sdk zod @google-cloud/bigquery
npm install -D @types/node typescript
mkdir src
touch src/index.ts
touch tsconfig.json

package.jsonにbuildコマンドの追加

{
  "name": "bigquery-mcp",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
+   "build": "tsc && chmod 755 build/index.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "description": "",
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.13.0",
    "zod": "^3.25.67"
  },
  "devDependencies": {
    "@types/node": "^24.0.3",
    "typescript": "^5.8.3"
  }
}

tsconfig.jsonの作成

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "./build",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

最終的な構成はこちら

bigquery-mcp/
├── src/
│    └── index.ts
├── package.json
└── tsconfig.json

BigQuery MCP サーバーの実装

1. MCPサーバーを起動してみる

まずは最小構成のMCPサーバーを作成し、動作確認を行います。

基本的なサーバーの実装

https://modelcontextprotocol.io/quickstart/server#building-your-server-2

src/index.tsに以下のコードを記述します

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";

const server = new McpServer({
  name: "bigquery-mcp",
  version: "1.0.0"
});

async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("BigQuery MCP Server running on stdio");
}

main().catch((error) => {
  console.error("Fatal error in main():", error);
  process.exit(1);
});

Inspectorを使った動作確認

MCP Inspectorを使用して、MCPサーバーの動作を確認します。Inspectorを使うことで、ClaudeやCursorなどのMCPクライアントがなくても、接続テストやtoolの実行が可能です。

# ビルド
npm run build 
# Inspectorを起動
npx @modelcontextprotocol/inspector node build/index.js

実行すると以下のような出力が表示されます

npx @modelcontextprotocol/inspector node build/index.js
Starting MCP inspector...
⚙️ Proxy server listening on 127.0.0.1:6277
🔑 Session token: 8c8df79ee9918efb3fda7f9cf4e0b7bb2bdbbfa8d5d7af1df49ae8c0bb3ec0e4
Use this token to authenticate requests or set DANGEROUSLY_OMIT_AUTH=true to disable auth

🔗 Open inspector with token pre-filled:
   http://localhost:6274/?MCP_PROXY_AUTH_TOKEN=8c8df79ee9918efb3fda7f9cf4e0b7bb2bdbbfa8d5d7af1df49ae8c0bb3ec0e4
   (Auto-open is disabled when authentication is enabled)

🔍 MCP Inspector is up and running at http://127.0.0.1:6274 🚀

出力されたURLにアクセスし、ConnectボタンをクリックしてConnectedと表示されれば接続成功です。

Connectを押して Connectedになれば接続確認OK

これでMCPサーバーの基本的な動作確認ができました。次はBigQuery操作のためのtoolを実装していきます。

2. データセット一覧取得

最初のtoolとして、プロジェクト内の全データセットを取得する機能を実装します。

実装

src/index.tsに以下のコードを追加します

import { BigQuery } from "@google-cloud/bigquery";
import { z } from "zod";
const bigquery = new BigQuery();

// ... 既存のコード ...

server.tool(
  "list_datasets",
  "List all datasets in the project",
  async () => {
    try {
      const [datasets] = await bigquery.getDatasets();
      const datasetNames = datasets.map((dataset) => dataset.id || '').filter((name) => name !== '');
      return {
        content: [
          {
            type: "text",
            text: JSON.stringify(datasetNames)
          }
        ]
      };
    }
    catch (error) {
      return {
        content: [
          {
            type: "text",
            text: `Error: ${error}`
          }
        ],
        isError: true
      };
    };
  }
)

Inspectorを使った動作確認

今回はBigQueryへのアクセスが必要なため、環境変数でGoogleCloudのプロジェクトIDを指定します

# ビルド
npm run build 
# プロジェクトIDを指定してInspectorを起動
npx @modelcontextprotocol/inspector -e GOOGLE_CLOUD_PROJECT={GoogleCloudのプロジェクトID} node build/index.js

Inspectorでlist_datasetsを実行すると、以下のようにデータセット一覧が表示されます

BigQueryコンソールで確認すると、同じデータセットが存在していることが分かります。最初のtool実装が成功しました!

3. テーブル一覧取得

次に、特定のデータセット内のテーブル一覧とスキーマ情報を取得する機能を実装します。

実装

src/index.tsに以下のコードを追加します

import { BigQuery } from "@google-cloud/bigquery";
import { z } from "zod";
const bigquery = new BigQuery();

// ... 既存のコード ...

server.tool(
  "list_tables",
  "List all tables in a dataset",
  {
    datasetId: z.string().min(1, "Dataset ID is required")
  },
  async (args) => {
    try {
      const dataset = bigquery.dataset(args.datasetId);
      const [tables] = await dataset.getTables();
      const results = await Promise.all(tables.map(async (table) => {
        if (!table.id) {
          return null;
        }
        const [metadata] = await table.getMetadata();
        return {
          tableId: table.id,
          name: table.id,
          description: metadata.description || null,
          schema: metadata.schema || null,
        }
      }));
      return {
        content: [
          {
            type: "text",
            text: JSON.stringify(results)
          }
        ]
      };
    }
    catch (error) {
      return {
        content: [
          {
            type: "text",
            text: `Error: ${error}`
          }
        ],
        isError: true
      };
    };
  }
)

パラメータの仕組み

このtoolでは第3引数にパラメータ定義を追加しています

これにより以下のことができるようになります

  • MCPクライアントに「datasetIdが必要」という情報を伝える
  • クライアントから受け取ったdatasetIdを使って処理を実行
  • zodによる入力値の検証が自動的に行われる
server.tool(
  "list_tables",
  "List all tables in a dataset",
  {
    datasetId: z.string().min(1, "Dataset ID is required")
  },

Inspectorを使った動作確認

# ビルド
npm run build 
# プロジェクトIDを指定してInspectorを起動
npx @modelcontextprotocol/inspector -e GOOGLE_CLOUD_PROJECT={GoogleCloudのプロジェクトID} node build/index.js

起動すると、datasetIdの入力フィールドが表示されます

未入力で実行すると、datasetId: z.string().min(1, "Dataset ID is required")で設定したエラーメッセージが表示されます

sampleデータセットを指定して実行すると、事前に準備しておいたテーブルの情報が取得できます

スキーマ情報も含まれているので、この情報を使ってMCPクライアントがSQLを生成してくれそうですね。

4. SQL実行

最後に、実際にSQLクエリを実行する機能を実装します。

実装

src/index.tsに以下のコードを追加します

import { BigQuery } from "@google-cloud/bigquery";
import { z } from "zod";
const bigquery = new BigQuery();

// ... 既存のコード ...

server.tool(
  "execute_query",
  "Execute a query on BigQuery",
  {
    query: z.string().min(1)
  },
  async (args) => {
    try {
      const job_id = 'mcp-job-' + Date.now();
      const [job] = await bigquery.createQueryJob({
        query: args.query,
        jobId: job_id,
      });
      const [rows] = await job.getQueryResults();
      const [metadata] = await job.getMetadata();

      const resultMetadata = {
        totalRows: rows.length,
        bytesProcessed: metadata.statistics?.query?.totalBytesProcessed || 0,
        jobId: metadata.jobReference?.jobId || 'unknown',
      };
      return {
        content: [
          {
            type: "text",
            text: JSON.stringify(
              {
                results: rows,
                metadata: resultMetadata
              }
            )
          }
        ]
      };
    }
    catch (error) {
      return {
        content: [
          {
            type: "text",
            text: `Error: ${error}`
          }
        ],
        isError: true
      };
    };
  }
)

ジョブIDの管理

MCP経由で実行されたクエリを後から追跡できるよう、ジョブIDに識別子を付与しています

    const job_id = 'mcp-job-' + Date.now();
    const [job] = await bigquery.createQueryJob({
      query: args.query,
      jobId: job_id,
    });

これにより以下のことができるようになります - ジョブ履歴から、MCP経由で実行されたクエリを簡単に識別できる - 実行履歴を分析して、よく使われるクエリパターンを把握できる - 頻繁に実行されるクエリを元に、データマートの設計や改修の参考にできる

Inspectorを使った動作確認

# ビルド
npm run build 
# プロジェクトIDを指定してInspectorを起動
npx @modelcontextprotocol/inspector -e GOOGLE_CLOUD_PROJECT={GoogleCloudのプロジェクトID} node build/index.js

クエリを実行してみます

クエリ結果と共に、処理情報も取得できました

コード全体

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { BigQuery } from "@google-cloud/bigquery";
import { z } from "zod";

const bigquery = new BigQuery();

const server = new McpServer({
  name: "bigquery-mcp",
  version: "1.0.0"
});

server.tool(
  "list_datasets",
  "List all datasets in the project",
  async () => {
    try {
      const [datasets] = await bigquery.getDatasets();
      const datasetNames = datasets.map((dataset) => dataset.id || '').filter((name) => name !== '');
      return {
        content: [
          {
            type: "text",
            text: JSON.stringify(datasetNames)
          }
        ]
      };
    }
    catch (error) {
      return {
        content: [
          {
            type: "text",
            text: `Error: ${error}`
          }
        ],
        isError: true
      };
    };
  }
)

server.tool(
  "list_tables",
  "List all tables in a dataset",
  {
    datasetId: z.string().min(1, "Dataset ID is required")
  },
  async (args) => {
    try {
      const dataset = bigquery.dataset(args.datasetId);
      const [tables] = await dataset.getTables();
      const results = await Promise.all(tables.map(async (table) => {
        if (!table.id) {
          return null;
        }
        const [metadata] = await table.getMetadata();
        return {
          tableId: table.id,
          name: table.id,
          description: metadata.description || null,
          schema: metadata.schema || null,
        }
      }));
      return {
        content: [
          {
            type: "text",
            text: JSON.stringify(results)
          }
        ]
      };
    }
    catch (error) {
      return {
        content: [
          {
            type: "text",
            text: `Error: ${error}`
          }
        ],
        isError: true
      };
    };
  }
)

server.tool(
  "execute_query",
  "Execute a query on BigQuery",
  {
    query: z.string().min(1)
  },
  async (args) => {
    try {
      const job_id = 'mcp-job-' + Date.now();
      const [job] = await bigquery.createQueryJob({
        query: args.query,
        jobId: job_id,
      });
      const [rows] = await job.getQueryResults();
      const [metadata] = await job.getMetadata();

      const resultMetadata = {
        totalRows: rows.length,
        bytesProcessed: metadata.statistics?.query?.totalBytesProcessed || 0,
        jobId: metadata.jobReference?.jobId || 'unknown',
      };
      return {
        content: [
          {
            type: "text",
            text: JSON.stringify(
              {
                results: rows,
                metadata: resultMetadata
              }
            )
          }
        ]
      };
    }
    catch (error) {
      return {
        content: [
          {
            type: "text",
            text: `Error: ${error}`
          }
        ],
        isError: true
      };
    };
  }
)

async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("BigQuery MCP Server running on stdio");
}

main().catch((error) => {
  console.error("Fatal error in main():", error);
  process.exit(1);
});

Claude Desktop で使ってみる

Inspectorでの検証が完了したので、実際にClaude Desktopで使ってみましょう。

設定ファイルの編集

1. 設定ファイルを開く

Claude Desktopの設定から、MCPサーバーの設定を行います

  1. Claude Desktopの設定を開く
  2. 「開発者」タブを選択
  3. 「構成を編集」をクリック

「構成を編集」をクリックすると、claude_desktop_config.jsonが保存されているフォルダが開きます。

2. claude_desktop_config.jsonの編集

以下の内容でclaude_desktop_config.jsonを更新します

  • args: npm run buildで生成されたファイルへのパスを指定
  • GOOGLE_CLOUD_PROJECT: プロジェクトIDを指定
  • GOOGLE_APPLICATION_CREDENTIALS:認証情報ファイルへのパスを指定
    • デフォルト認証: ~/.config/gcloud/application_default_credentials.json
    • サービスアカウント: ダウンロードしたJSONファイルのパス
{
  "mcpServers": {
    "bigquery": {
      "command": "node",
      "args": ["path/to/bigquery-mcp/build/index.js"],
      "env": {
         "GOOGLE_CLOUD_PROJECT": "プロジェクトID",
         "GOOGLE_APPLICATION_CREDENTIALS": "path/to/.config/gcloud/application_default_credentials.json"
      }
    }
  }
}

動作確認

Claude Desktopを再起動し、MCPサーバーが正しく接続されているか確認します。

  1. Claude Desktopを再起動
  2. 設定を開き、「開発者」タブを選択
  3. MCPサーバーの一覧に「bigquery」が表示され、ステータスがrunningになっていることを確認

接続が成功すると、BigQueryへのアクセスが可能になります。もし接続に失敗している場合は、以下を確認してください - args,GOOGLE_APPLICATION_CREDENTIALSに設定したパスが正しいか - Google Cloud CLIの認証が完了しているか

実際の使用例

データ分析の依頼

Claude Desktopで以下のような依頼をしてみます

「sample」データセットの注文データから、地域別の売上を集計して見せて

Claudeは以下の手順で処理を実行します

  • データセット確認 - list_datasetsでデータセットを確認
  • テーブル構造の把握 - list_tablesでスキーマ情報を取得
  • SQL生成・実行 - スキーマ情報を基にSQLを生成し、execute_queryで実行
  • 結果の表示 - 取得したデータを整形して表示

可視化の依頼

この結果をグラフで可視化して

HTMLで作成されるため、グラフの種類変更や色やレイアウトの調整が柔軟に指示するだけで対応してくれます。

活用のポイント

  • SQLが書ける人: 直接SQLを指定して実行し、結果の分析や可視化をClaudeに依頼
  • SQLが書けない人: データの要件を自然言語で伝えるだけで、Claudeが適切なSQLを生成・実行

どちらのケースでも、BigQueryのデータに簡単にアクセスし、分析結果を得ることができるようになりました。

今後の拡張案

  • OAuth対応
  • MCPクライアントが効率的にSQLを生成するためのドキュメント群の追加
  • 特定のデータセットにのみアクセス可能にする制御
  • クエリ実行前に実行コストが多くかかりそうな時の制御
  • 実行するクエリをSELECTのみに限定

まとめ

BigQuery MCPサーバーを作成することで、Claude DesktopからシームレスにBigQueryにアクセスできるようになりました。

実際に作ってみて改めて実感したのは、メタデータの重要性です。

テーブルやカラムに適切な説明があれば、LLMはより正確なSQLを生成し、的確な分析を行えます。

例えば

  • カラム名が「売上」だけでなく「売上金額(税抜)」という説明があれば、税込/税抜の混同を防げる
  • 「顧客ID」に「B2B顧客の場合は企業コード、B2C顧客の場合は個人ID」という説明があれば、より適切な集計が可能

データ分析とLLMの連携が進むにつれ、このようなメタデータの整備は「あると便利」から「ないと困る」へと変化していくでしょう。

できるようになったこと

SQLが書ける人 - 直接SQLを実行し、結果の可視化や分析をClaudeに依頼 - 複雑なクエリ結果の解釈や、追加の分析をAIがサポート

SQLが書けない人 - 自然言語でデータの要件を伝えるだけで分析が可能 - 「目的のテーブルを探す → ClaudeがSQL作成 → 結果を可視化・分析」の流れを実現

活用シーン

以下のような時にLLMでアドホックにデータ分析するのは有効だと思いました。

  • アドホックな分析 - 急ぎでデータを確認したい
  • 探索的分析 - どんなデータがあるか探りながら分析
  • 簡易レポート作成 - すぐに示唆や洞察が欲しい

一方、定期的なモニタリングが必要になった場合はクエリコスト最適化全社員共通の正しいデータを見て意思決定するということを達成するためにデータマート化したりBIでダッシュボードを作成することを検討するべきだと思います。

データ基盤の進化サイクル

LLMによる分析で素早く仮説検証を行い、価値が確認できたものから順次、本格的なデータ基盤に組み込んでいく。このようなサイクルを回すことで、組織のデータ基盤をより効率的に成長させることができます。

データマート化した後に MCP Toolbox for Databasesを使ってユーザーに見せたいデータをtoolとして絞り込むのも面白そうです。

参考資料