AI & Agentic

Kết nối Google Analytics với Claude Code qua MCP - Hướng dẫn từng bước

Hướng dẫn build MCP server tự viết để kết nối Google Analytics 4 với Claude Code - query data GA4 trực tiếp trong terminal mà không cần mở browser.

🇬🇧 English

Kết nối Google Analytics với Claude Code qua MCP

Thay vì phải mở tab browser để xem GA4 mỗi lần cần số liệu, bạn có thể query pageviews, sessions, top pages trực tiếp trong Claude Code. Bài này hướng dẫn từng bước build MCP server kết nối GA4 API - từ tạo Service Account đến viết server file và config vào Claude.

Khi đang làm việc trong terminal và cần kiểm tra traffic, thông thường bạn phải: switch tab, đợi GA4 load, tìm đúng date range, rồi quay lại. Với MCP server cho GA4, toàn bộ flow đó rút gọn thành một câu hỏi tự nhiên với Claude. Đây là cách setup.

Tổng quan kiến trúc

Trước khi code, cần hiểu flow hoạt động:

Claude Code  →  MCP Server (Node.js)  →  GA4 Data API  →  Google Analytics

MCP Server là một file Node.js chạy local trên máy bạn, nhận lệnh từ Claude qua stdio, rồi gọi GA4 API bằng Service Account credentials. Claude không trực tiếp gọi Google - tất cả đi qua server trung gian này.

Tại sao dùng Service Account thay vì OAuth? Vì OAuth yêu cầu user login qua browser mỗi khi token hết hạn. Service Account hoạt động không cần tương tác, phù hợp cho automated/CLI context.

Bước 1 - Tạo Google Cloud Project và Service Account

1.1. Tạo project trên Google Cloud Console

Truy cập console.cloud.google.com, tạo project mới hoặc chọn project có sẵn. Tên project không quan trọng - chỉ dùng để quản lý credentials.

1.2. Bật Google Analytics Data API

Vào APIs & Services > Library, tìm “Google Analytics Data API” và bật lên. Đây là API v1beta dùng cho GA4 (khác với Universal Analytics đã deprecated).

Google Analytics Data API trong Google Cloud Console Bật Google Analytics Data API - chọn đúng “Data API” cho GA4, không phải “Reporting API” cũ

1.3. Tạo Service Account

Vào IAM & Admin > Service Accounts > Create Service Account:

  • Name: Đặt tên dễ nhận biết, ví dụ analytics-mcp-bot
  • Role: Không cần grant role ở bước này
  • Sau khi tạo xong, click vào Service Account > Keys > Add Key > JSON
  • Download file JSON về máy - đây là credentials file, bảo mật như password

File JSON sẽ có dạng:

{
  "type": "service_account",
  "project_id": "your-project-id",
  "private_key_id": "...",
  "private_key": "-----BEGIN RSA PRIVATE KEY-----\n...",
  "client_email": "analytics-mcp-bot@your-project.iam.gserviceaccount.com",
  ...
}

Lưu file này vào một thư mục an toàn trên máy (không commit lên Git).

Bước 2 - Grant quyền truy cập GA4 Property

Service Account cần được thêm vào GA4 property với quyền Viewer:

  1. Mở Google Analytics > Admin > Property access management
  2. Click + > Add users
  3. Nhập client_email từ file JSON vừa download (dạng ...@...iam.gserviceaccount.com)
  4. Chọn role Viewer
  5. Lưu lại

Quan trọng: Bước 1 (tạo Service Account + credentials) và Bước 2 (grant access trong GA4) phải làm đủ cả hai - thiếu một trong hai sẽ bị lỗi 403 khi query.

Bước 3 - Tìm GA4 Property ID

Property ID là số dạng properties/XXXXXXXXX - không phải Measurement ID dạng G-XXXX.

Tìm tại: GA4 Admin > Property Settings > Property ID (góc trên bên phải). Copy số đó và thêm prefix properties/, ví dụ: properties/123456789.

Bước 4 - Viết MCP Server

Tạo thư mục và file server:

mkdir -p ~/.claude/mcp-servers
cd ~/.claude/mcp-servers
npm init -y
npm install googleapis

Tạo file google-analytics-mcp.js:

#!/usr/bin/env node

const { google } = require('googleapis');
const fs = require('fs');

// --- Config ---
const CREDENTIALS_PATH = '/path/to/your-service-account.json';
const GA4_PROPERTY_ID = process.env.GA4_PROPERTY_ID || 'properties/REPLACE_ME';

// --- Init Auth ---
let auth;
try {
  const credentials = JSON.parse(fs.readFileSync(CREDENTIALS_PATH, 'utf8'));
  auth = new google.auth.GoogleAuth({
    credentials,
    scopes: ['https://www.googleapis.com/auth/analytics.readonly']
  });
} catch (error) {
  process.stderr.write(`Failed to load credentials: ${error.message}\n`);
  process.exit(1);
}

const analyticsdata = google.analyticsdata({ version: 'v1beta', auth });

// --- Tools ---

async function getOverview({ startDate = '30daysAgo', endDate = 'today' } = {}) {
  const res = await analyticsdata.properties.runReport({
    property: GA4_PROPERTY_ID,
    requestBody: {
      dateRanges: [{ startDate, endDate }],
      metrics: [
        { name: 'sessions' },
        { name: 'totalUsers' },
        { name: 'screenPageViews' },
        { name: 'bounceRate' },
        { name: 'averageSessionDuration' },
      ],
    },
  });

  const row = res.data.rows?.[0]?.metricValues || [];
  const [sessions, users, pageviews, bounceRate, avgDuration] = row.map(r => r.value);

  return `## GA4 Overview (${startDate} → ${endDate})

| Metric | Value |
|--------|-------|
| Sessions | ${parseInt(sessions || 0).toLocaleString()} |
| Users | ${parseInt(users || 0).toLocaleString()} |
| Pageviews | ${parseInt(pageviews || 0).toLocaleString()} |
| Bounce Rate | ${(parseFloat(bounceRate || 0) * 100).toFixed(1)}% |
| Avg Session Duration | ${Math.round(parseFloat(avgDuration || 0))}s |`;
}

async function getTopPages({ startDate = '30daysAgo', endDate = 'today', limit = 10 } = {}) {
  const res = await analyticsdata.properties.runReport({
    property: GA4_PROPERTY_ID,
    requestBody: {
      dateRanges: [{ startDate, endDate }],
      dimensions: [{ name: 'pagePath' }],
      metrics: [{ name: 'screenPageViews' }, { name: 'totalUsers' }],
      orderBys: [{ metric: { metricName: 'screenPageViews' }, desc: true }],
      limit,
    },
  });

  const rows = res.data.rows || [];
  const lines = rows.map((row, i) => {
    const path = row.dimensionValues[0].value;
    const [views, users] = row.metricValues.map(m => m.value);
    return `| ${i + 1} | ${path} | ${parseInt(views).toLocaleString()} | ${parseInt(users).toLocaleString()} |`;
  });

  return `## Top ${limit} Pages (${startDate} → ${endDate})

| # | Path | Views | Users |
|---|------|-------|-------|
${lines.join('\n')}`;
}

async function getTrafficSources({ startDate = '30daysAgo', endDate = 'today' } = {}) {
  const res = await analyticsdata.properties.runReport({
    property: GA4_PROPERTY_ID,
    requestBody: {
      dateRanges: [{ startDate, endDate }],
      dimensions: [{ name: 'sessionDefaultChannelGroup' }],
      metrics: [{ name: 'sessions' }, { name: 'totalUsers' }],
      orderBys: [{ metric: { metricName: 'sessions' }, desc: true }],
    },
  });

  const rows = res.data.rows || [];
  const total = rows.reduce((s, r) => s + parseInt(r.metricValues[0].value), 0);
  const lines = rows.map(row => {
    const channel = row.dimensionValues[0].value;
    const [sessions, users] = row.metricValues.map(m => m.value);
    const pct = ((parseInt(sessions) / total) * 100).toFixed(1);
    return `| ${channel} | ${parseInt(sessions).toLocaleString()} (${pct}%) | ${parseInt(users).toLocaleString()} |`;
  });

  return `## Traffic Sources (${startDate} → ${endDate})

| Channel | Sessions | Users |
|---------|----------|-------|
${lines.join('\n')}`;
}

// --- MCP Protocol ---

const TOOLS = [
  {
    name: 'ga4_overview',
    description: 'Lấy tổng quan traffic: sessions, users, pageviews, bounce rate.',
    inputSchema: {
      type: 'object',
      properties: {
        startDate: { type: 'string', description: 'VD: "7daysAgo", "2026-04-01"', default: '30daysAgo' },
        endDate: { type: 'string', default: 'today' },
      },
    },
  },
  {
    name: 'ga4_top_pages',
    description: 'Top pages theo pageviews.',
    inputSchema: {
      type: 'object',
      properties: {
        startDate: { type: 'string', default: '30daysAgo' },
        endDate: { type: 'string', default: 'today' },
        limit: { type: 'number', default: 10 },
      },
    },
  },
  {
    name: 'ga4_traffic_sources',
    description: 'Breakdown traffic theo channel (Organic, Social, Direct...).',
    inputSchema: {
      type: 'object',
      properties: {
        startDate: { type: 'string', default: '30daysAgo' },
        endDate: { type: 'string', default: 'today' },
      },
    },
  },
];

function sendResponse(id, result) {
  const msg = JSON.stringify({ jsonrpc: '2.0', id, result });
  process.stdout.write(`Content-Length: ${Buffer.byteLength(msg)}\r\n\r\n${msg}`);
}

function sendError(id, code, message) {
  const msg = JSON.stringify({ jsonrpc: '2.0', id, error: { code, message } });
  process.stdout.write(`Content-Length: ${Buffer.byteLength(msg)}\r\n\r\n${msg}`);
}

async function handleMessage(msg) {
  const { id, method, params } = msg;

  if (method === 'initialize') {
    return sendResponse(id, {
      protocolVersion: '2024-11-05',
      capabilities: { tools: {} },
      serverInfo: { name: 'google-analytics-mcp', version: '1.0.0' },
    });
  }
  if (method === 'tools/list') return sendResponse(id, { tools: TOOLS });
  if (method === 'notifications/initialized') return;

  if (method === 'tools/call') {
    const { name, arguments: args = {} } = params;
    try {
      let text;
      if (name === 'ga4_overview') text = await getOverview(args);
      else if (name === 'ga4_top_pages') text = await getTopPages(args);
      else if (name === 'ga4_traffic_sources') text = await getTrafficSources(args);
      else return sendError(id, -32601, `Unknown tool: ${name}`);
      return sendResponse(id, { content: [{ type: 'text', text }] });
    } catch (err) {
      return sendError(id, -32000, `GA4 API error: ${err.message}`);
    }
  }

  sendError(id, -32601, `Method not found: ${method}`);
}

// --- Stdio Transport ---
let buffer = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', chunk => {
  buffer += chunk;
  while (true) {
    const headerEnd = buffer.indexOf('\r\n\r\n');
    if (headerEnd === -1) break;
    const lenMatch = buffer.slice(0, headerEnd).match(/Content-Length:\s*(\d+)/i);
    if (!lenMatch) { buffer = buffer.slice(headerEnd + 4); continue; }
    const len = parseInt(lenMatch[1]);
    const bodyStart = headerEnd + 4;
    if (buffer.length < bodyStart + len) break;
    const body = buffer.slice(bodyStart, bodyStart + len);
    buffer = buffer.slice(bodyStart + len);
    try {
      handleMessage(JSON.parse(body)).catch(e => process.stderr.write(`Error: ${e.message}\n`));
    } catch (e) {
      process.stderr.write(`Parse error: ${e.message}\n`);
    }
  }
});

Bước 5 - Config vào Claude Code

Claude Code CLI

Mở file ~/.claude/settings.json và thêm vào block mcpServers:

{
  "mcpServers": {
    "google-analytics": {
      "command": "/usr/local/bin/node",
      "args": ["/Users/yourname/.claude/mcp-servers/google-analytics-mcp.js"],
      "env": {
        "GA4_PROPERTY_ID": "properties/123456789"
      }
    }
  }
}

Claude Desktop App (tùy chọn)

Claude desktop app dùng config riêng tại ~/Library/Application Support/Claude/claude_desktop_config.json - không share với Claude Code CLI. Paste cùng block mcpServers vào file này nếu muốn dùng cả trong desktop app.

Claude Code config file với MCP server Cấu trúc settings.json với mcpServers block - command trỏ đến Node.js executable, args là đường dẫn tới server file

Kiểm tra Node path

Đảm bảo command trỏ đúng đến Node.js trên máy bạn:

which node
# /usr/local/bin/node  hoặc  /opt/homebrew/bin/node

Bước 6 - Restart và test

Restart Claude Code (quit hoàn toàn, mở lại) để load MCP server mới. Sau khi restart, các tools ga4_overview, ga4_top_pages, ga4_traffic_sources sẽ available.

Test bằng cách hỏi Claude:

“Pageviews tháng này bao nhiêu?”

“Top 5 bài viết nhiều view nhất 30 ngày qua?”

“Traffic đến từ kênh nào nhiều nhất?”

Claude sẽ tự động gọi đúng GA4 tool và trả về data trực tiếp trong chat.

Workaround khi MCP chưa load

Nếu session đang mở trước khi config MCP (tools chưa xuất hiện), bạn có thể query GA4 API trực tiếp qua Node.js mà không cần restart:

cd ~/.claude/mcp-servers && node -e "
const { google } = require('googleapis');
const fs = require('fs');
const credentials = JSON.parse(fs.readFileSync('/path/to/credentials.json', 'utf8'));
const auth = new google.auth.GoogleAuth({ credentials, scopes: ['https://www.googleapis.com/auth/analytics.readonly'] });
const api = google.analyticsdata({ version: 'v1beta', auth });

api.properties.runReport({
  property: 'properties/YOUR_ID',
  requestBody: {
    dateRanges: [{ startDate: '2026-04-01', endDate: 'today' }],
    metrics: [{ name: 'screenPageViews' }, { name: 'sessions' }],
  }
}).then(r => console.log(r.data.rows?.[0]?.metricValues));
"

GA4 data trả về trong terminal sau khi MCP connect thành công Output GA4 data trực tiếp trong terminal - không cần mở browser hay tab GA4

Câu hỏi thường gặp (FAQ)

MCP đã config nhưng tools không xuất hiện trong session?

MCP server chỉ load khi Claude Code khởi động. Nếu bạn thêm config trong lúc đang có session mở, cần quit hoàn toàn và mở lại. Dùng workaround Node.js ở trên để query tạm trong khi chưa restart.

Lỗi 403 khi gọi GA4 API?

Có hai nguyên nhân phổ biến: (1) chưa bật Google Analytics Data API trong Cloud Console, hoặc (2) chưa add client_email vào GA4 Property access management. Cả hai bước đều bắt buộc - thiếu một là lỗi.

Dùng Property ID nào - G-XXXX hay số?

Dùng numeric Property ID dạng properties/123456789, tìm tại GA4 Admin > Property Settings. ID dạng G-XXXX là Measurement ID dùng cho tracking code, không phải cho Data API.

Có thể kết nối nhiều GA4 property không?

Có. Tạo nhiều MCP server entries trong settings.json với tên khác nhau (ví dụ google-analytics-blog, google-analytics-app) và trỏ đến cùng server file nhưng khác GA4_PROPERTY_ID trong env.

Claude Desktop và Claude Code CLI có share config không?

Không. Claude Code CLI đọc ~/.claude/settings.json, còn Claude Desktop App đọc ~/Library/Application Support/Claude/claude_desktop_config.json. Cần config MCP server ở cả hai file nếu muốn dùng trên cả hai.

Setup nhanh với Claude Code

Nếu bạn không muốn làm thủ công từng bước, hãy copy prompt dưới đây và paste thẳng vào Claude Code. Claude sẽ tự động tạo server file, hỏi bạn các thông tin cần thiết và config vào settings.json.

Tôi muốn setup một MCP server để kết nối Google Analytics 4 với Claude Code.

Hãy giúp tôi:
1. Tạo file server tại ~/.claude/mcp-servers/google-analytics-mcp.js
   - Dùng googleapis package
   - Hỗ trợ 3 tools: ga4_overview, ga4_top_pages, ga4_traffic_sources
   - Đọc credentials từ file JSON của Service Account
   - Nhận GA4_PROPERTY_ID qua environment variable

2. Thêm config vào ~/.claude/settings.json:
   {
     "google-analytics": {
       "command": "/usr/local/bin/node",
       "args": ["~/.claude/mcp-servers/google-analytics-mcp.js"],
       "env": { "GA4_PROPERTY_ID": "properties/REPLACE_ME" }
     }
   }

3. Hướng dẫn tôi tạo Service Account trên Google Cloud Console
   và grant quyền Viewer vào GA4 property.

Sau khi xong, test thử bằng cách query pageviews 30 ngày gần nhất.

Sau khi chạy xong, bạn chỉ cần cung cấp: đường dẫn đến file credentials JSON và numeric Property ID của GA4 property. Claude Code sẽ xử lý phần còn lại.

Tổng kết

Setup GA4 MCP cho Claude Code mất khoảng 15-20 phút và chỉ cần làm một lần. Sau đó, mọi câu hỏi về traffic website có thể hỏi trực tiếp trong terminal - không cần tab browser, không cần nhớ UI GA4. Khi đã quen pattern này, bạn có thể mở rộng server thêm các tool khác như ga4_monthly_trend, ga4_search_keywords, hoặc kết nối thêm các service khác (Search Console, Ahrefs API…) theo cùng kiến trúc MCP.

Liên kết

✦ Miễn phí

Muốn nhận thêm kiến thức như thế này?

Mình tổng hợp AI, marketing và tech insights mỗi tuần - gọn, có gu.

Không spam. Unsubscribe bất cứ lúc nào.