❄️

Snowflake Managed MCP Serverが気になるので調べてみた

に公開

はじめに

少し前、Tableau MCPを触ってみて面白い!Snowflakeでもやってみよう!
と思ったのに難しすぎて心が折れた私。
このたびSnowflake Managed MCPがパブリックプレビューになったということで、クイックスタートになぞってチャレンジしてみようと思います。

Tableau MCP楽しいぜのつぶやきがこれ。
https://x.com/mfujita2023/status/1971817065859498454

ちなみに心折れたつぶやきはこれ↓
https://x.com/mfujita2023/status/1971827421356941723

前提

参考にするクイックスタート

このたび、こちらのクイックスタートを参考にしてSnowflake Managed MCPを作ってみたいと思います。
https://quickstarts.snowflake.com/guide/getting-started-with-snowflake-mcp-server/index.html#0

MCP(Managed Containerized Process)ってなに?っていうのをこちらのクイックスタートの絵が分かりやすく示してくださっています。
「AIモデルと外部データソースやツール間のシームレスな統合」とのこと。

クイックスタートではCursorで問い合わせをするようなイメージですが、今回はClaude Desktopから問い合わせできるよう作ってみようと思います。

環境設定

Githubに載っているSetup.sqlをSQLワークシートに貼り付けて実行します。

https://github.com/Snowflake-Labs/sfguide-getting-started-with-snowflake-mcp-server/blob/main/setup.sql

個人アクセストークンの作成

こちらの個人ページの認証画面からPersonal Access Token (PAT)を作成します。


個人アクセストークンはコピーし、メモ帳などに貼り付けて画面を閉じます。

Cortex検索サービスを作成


右上の「作成」をクリックします


Snowflake Managed MCP サーバの作成

以下のSQLをワークスペースに貼り付けて実行します。

create or replace mcp server dash_mcp_db.data.dash_mcp_server from specification
$$
tools:
  - name: "Support_Tickets_Search_Service"
    identifier: "dash_mcp_db.data.support_tickets"
    type: "CORTEX_SEARCH_SERVICE_QUERY"
    description: "A tool that performs keyword and vector search over support tickets and call transcripts."
    title: "Support Tickets"
$$;

Node.jsのインストール

https://nodejs.org/en/download
こちらからNode.jsをインストールしてください。

Claude Desktopのインストール

https://claude.ai/download

Claude Desktopのインストーラーをダウンロードおよび実行し、Claude Desktopをインストールしてください。

Claude Desktopの接続設定ファイルを作成する

c:\mcpというフォルダを作り、snowflake-mcp-proxy.jsという名前でプロキシファイルを作ります。
Claude DesktopからのリクエストをSnowflake MCP Serverに転送する設定となります。

snowflake-mcp-proxy.js
#!/usr/bin/env node

const https = require('https');
const readline = require('readline');
const fs = require('fs');

const SNOWFLAKE_HOST = '<アカウントロケーター>.snowflakecomputing.com';
const MCP_PATH = '/api/v2/databases/DASH_MCP_DB/schemas/DATA/mcp-servers/dash_mcp_server';
const AUTH_TOKEN = 'Bearer <PATを登録したときにコピーしたトークン>';

const logFile = 'C:\\mcp\\debug.log';

function log(message) {
  const timestamp = new Date().toISOString();
  const logMessage = `${timestamp} - ${message}\n`;
  fs.appendFileSync(logFile, logMessage);
  console.error(logMessage);
}

// ツール名のマッピング(MCP名 -> Snowflake名)
const toolNameMap = {};
const reverseToolNameMap = {};

function sanitizeToolName(name) {
  return name.replace(/\s+/g, '_').replace(/[^a-zA-Z0-9_-]/g, '');
}

const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
  terminal: false
});

rl.on('line', (line) => {
  log(`Received: ${line}`);
  
  try {
    const request = JSON.parse(line);
    log(`Parsed request: ${JSON.stringify(request)}`);
    
    const isNotification = request.method && request.method.startsWith('notifications/');
    
    // tools/call の場合、ツール名を元に戻す
    if (request.method === 'tools/call' && request.params && request.params.name) {
      const sanitizedName = request.params.name;
      const originalName = reverseToolNameMap[sanitizedName];
      if (originalName) {
        log(`Converting tool name: ${sanitizedName} -> ${originalName}`);
        request.params.name = originalName;
      }
    }
    
    const postData = JSON.stringify(request);
    
    const options = {
      hostname: SNOWFLAKE_HOST,
      port: 443,
      path: MCP_PATH,
      method: 'POST',
      headers: {
        'Authorization': AUTH_TOKEN,
        'Content-Type': 'application/json',
        'Accept': 'application/json',
        'User-Agent': 'Claude-MCP-Client/1.0',
        'Content-Length': Buffer.byteLength(postData)
      }
    };

    const req = https.request(options, (res) => {
      log(`Response status: ${res.statusCode}`);
      
      let data = '';
      res.on('data', (chunk) => {
        data += chunk;
      });
      
      res.on('end', () => {
        log(`Response body (raw): ${data}`);
        
        if (res.statusCode === 202 || isNotification) {
          log(`Notification acknowledged (status ${res.statusCode})`);
          return;
        }
        
        if (res.statusCode !== 200) {
          log(`Error: Non-200 status code: ${res.statusCode}`);
          const errorResponse = {
            jsonrpc: '2.0',
            id: request.id,
            error: { 
              code: -32603, 
              message: `Snowflake returned status ${res.statusCode}: ${data}`,
            }
          };
          console.log(JSON.stringify(errorResponse));
          return;
        }
        
        if (!data || data.trim() === '') {
          const errorResponse = {
            jsonrpc: '2.0',
            id: request.id,
            error: { code: -32603, message: 'Empty response from Snowflake' }
          };
          console.log(JSON.stringify(errorResponse));
          return;
        }
        
        try {
          const parsed = JSON.parse(data);
          
          // tools/list レスポンスの場合、ツール名を修正してマッピングを保存
          if (request.method === 'tools/list' && parsed.result && parsed.result.tools) {
            parsed.result.tools = parsed.result.tools.map(tool => {
              const originalName = tool.name;
              const sanitizedName = sanitizeToolName(tool.name);
              
              // マッピングを保存
              toolNameMap[originalName] = sanitizedName;
              reverseToolNameMap[sanitizedName] = originalName;
              
              log(`Tool name mapping: "${originalName}" -> "${sanitizedName}"`);
              
              return {
                ...tool,
                name: sanitizedName
              };
            });
          }
          
          const compactJson = JSON.stringify(parsed);
          log(`Sending to Claude: ${compactJson}`);
          console.log(compactJson);
        } catch (e) {
          log(`JSON parse error: ${e.message}`);
          const errorResponse = {
            jsonrpc: '2.0',
            id: request.id,
            error: { 
              code: -32700, 
              message: `Invalid JSON response: ${e.message}`,
              data: data.substring(0, 200)
            }
          };
          console.log(JSON.stringify(errorResponse));
        }
      });
    });

    req.on('error', (error) => {
      log(`Request error: ${error.message}`);
      if (!isNotification) {
        console.log(JSON.stringify({
          jsonrpc: '2.0',
          id: request.id,
          error: { code: -32603, message: error.message }
        }));
      }
    });

    req.write(postData);
    req.end();
    
  } catch (error) {
    log(`Parse error: ${error.message}`);
    console.log(JSON.stringify({
      jsonrpc: '2.0',
      error: { code: -32700, message: `Parse error: ${error.message}` }
    }));
  }
});

log('Snowflake MCP Proxy started');

ファイル―設定-開発者

設定の編集を押すとエクスプローラ画面が開くので、
claude_desktop_config.json を開き下記のように設定します。

{
  "mcpServers": {
    "snowflake": {
      "command": "node",
      "args": ["C:\\mcp\\snowflake-mcp-proxy.js"]
    }
  }
}

ちなみに、Tableau MCPと併用する場合はこのような形で設定します。
これで完成です!

{
  "mcpServers": {
    "tableau": {
      "command": "node",
      "args": [jsファイルのパス],
      "env": {
        "TRANSPORT": "stdio",
        "SERVER": "https://10ax.online.tableau.com/",
        "SITE_NAME": "サイト名",
        "PAT_NAME": "PAT名",
        "PAT_VALUE": "PATシークレット"
      }
    },
    "snowflake": {
      "command": "node",
      "args": ["C:\\mcp\\snowflake-mcp-proxy.js"]
    }
  }
}

Claude Desktopで問い合わせてみる

「サポートチケットを検索して、最近の問い合わせ内容を教えて」と問い合わせてみて、そのあと「グラフにして」とお願いしたらこのように表示されました!!

まとめ

Claude DesktopでSnowflake Managed MCPに接続して問い合わせができました!
ただ、接続部分でエラーが出て数時間ハマってしまいました。
こちらのブログの設定でうまくいくと思うのですが、エラーで動作しない等ありましたらご指摘ください。
ありがとうございました。

truestarテックブログ
設定によりコメント欄が無効化されています