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).
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:
- Mở Google Analytics > Admin > Property access management
- Click + > Add users
- Nhập
client_emailtừ file JSON vừa download (dạng...@...iam.gserviceaccount.com) - Chọn role Viewer
- 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.
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));
"
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.