UNPKG

google-search-console-mcp-js

Version:

Google Search Console MCP Server for Node.js - connect GSC data to Claude, Cursor and other MCP clients

439 lines (397 loc) 16.2 kB
#!/usr/bin/env node const { Server } = require('@modelcontextprotocol/sdk/server/index.js'); const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js'); const { CallToolRequestSchema, ListToolsRequestSchema } = require('@modelcontextprotocol/sdk/types.js'); const { google } = require('googleapis'); const fs = require('fs'); const path = require('path'); // Configuration from environment variables const CREDENTIALS_PATH = process.env.GOOGLE_APPLICATION_CREDENTIALS; const GSC_SITE_URL = process.env.GSC_SITE_URL; // Validate required environment variables if (!CREDENTIALS_PATH) { console.error("ERROR: GOOGLE_APPLICATION_CREDENTIALS environment variable not set"); console.error("Please set it to the path of your service account JSON file"); process.exit(1); } if (!GSC_SITE_URL) { console.error("ERROR: GSC_SITE_URL environment variable not set"); console.error("Please set it to your verified site URL (e.g., https://example.com/)"); process.exit(1); } // Validate credentials file exists if (!fs.existsSync(CREDENTIALS_PATH)) { console.error(`ERROR: Credentials file not found: ${CREDENTIALS_PATH}`); console.error("Please check the GOOGLE_APPLICATION_CREDENTIALS path"); process.exit(1); } // Load JSON data files const gscDimensions = JSON.parse(fs.readFileSync(path.join(__dirname, 'gsc_dimensions.json'), 'utf8')); const gscMetrics = JSON.parse(fs.readFileSync(path.join(__dirname, 'gsc_metrics.json'), 'utf8')); const gscFilters = JSON.parse(fs.readFileSync(path.join(__dirname, 'gsc_filters.json'), 'utf8')); // Initialize Google Search Console API client async function getGscService() { try { const auth = new google.auth.GoogleAuth({ keyFile: CREDENTIALS_PATH, scopes: ['https://www.googleapis.com/auth/webmasters.readonly'] }); const authClient = await auth.getClient(); return google.searchconsole({ version: 'v1', auth: authClient }); } catch (error) { console.error(`Error initializing GSC service: ${error.message}`); throw error; } } // Helper function to format dates function formatDate(daysAgo) { const date = new Date(); date.setDate(date.getDate() - daysAgo); return date.toISOString().split('T')[0]; } // Create the server const server = new Server( { name: "google-search-console-mcp", version: "1.0.0", }, { capabilities: { tools: {}, }, } ); // List tools handler server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: "list_gsc_sites", description: "List all sites verified in Google Search Console", inputSchema: { type: "object", properties: {}, }, }, { name: "list_available_dimensions", description: "List all available GSC dimensions with their descriptions", inputSchema: { type: "object", properties: {}, }, }, { name: "list_available_metrics", description: "List all available GSC metrics with their descriptions", inputSchema: { type: "object", properties: {}, }, }, { name: "get_search_analytics", description: "Retrieve Google Search Console search analytics data", inputSchema: { type: "object", properties: { dimensions: { type: "array", items: { type: "string" }, description: "List of dimensions: country, device, page, query, searchAppearance, date", default: ["query"] }, start_date: { type: "string", description: "Start date in YYYY-MM-DD format (defaults to 30 days ago)" }, end_date: { type: "string", description: "End date in YYYY-MM-DD format (defaults to 3 days ago)" }, filters: { type: "array", description: "List of filter objects", items: { type: "object", properties: { dimension: { type: "string" }, operator: { type: "string" }, expression: { type: "string" } } } }, search_type: { type: "string", description: "Type of search: web, image, video, news, discover, googleNews", default: "web" }, row_limit: { type: "number", description: "Maximum number of rows to return (max 25000)", default: 1000 }, start_row: { type: "number", description: "Starting row for pagination (0-based)", default: 0 } }, }, }, { name: "get_sitemaps", description: "Get all sitemaps for the configured site", inputSchema: { type: "object", properties: {}, }, }, { name: "submit_sitemap", description: "Submit a sitemap to Google Search Console", inputSchema: { type: "object", properties: { sitemap_url: { type: "string", description: "Full URL of the sitemap to submit" } }, required: ["sitemap_url"] }, }, { name: "delete_sitemap", description: "Delete a sitemap from Google Search Console", inputSchema: { type: "object", properties: { sitemap_url: { type: "string", description: "Full URL of the sitemap to delete" } }, required: ["sitemap_url"] }, } ], }; }); // Call tool handler server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { switch (name) { case "list_gsc_sites": { const service = await getGscService(); const response = await service.sites.list(); const result = response.data.siteEntry?.map(site => ({ siteUrl: site.siteUrl, permissionLevel: site.permissionLevel })) || []; return { content: [ { type: "text", text: JSON.stringify(result, null, 2), }, ], }; } case "list_available_dimensions": { return { content: [ { type: "text", text: JSON.stringify(gscDimensions.dimensions, null, 2), }, ], }; } case "list_available_metrics": { return { content: [ { type: "text", text: JSON.stringify(gscMetrics.metrics, null, 2), }, ], }; } case "get_search_analytics": { const { dimensions = ["query"], start_date, end_date, filters, search_type = "web", row_limit = 1000, start_row = 0 } = args; // Validate dimensions const validDimensions = ["country", "device", "page", "query", "searchAppearance", "date"]; for (const dim of dimensions) { if (!validDimensions.includes(dim)) { throw new Error(`Invalid dimension '${dim}'. Valid dimensions: ${validDimensions.join(', ')}`); } } // Set default dates const startDate = start_date || formatDate(30); const endDate = end_date || formatDate(3); // Build request const requestBody = { startDate, endDate, dimensions, searchType: search_type, rowLimit: Math.min(row_limit, 25000), startRow: start_row }; // Handle filters if (filters && filters.length > 0) { requestBody.dimensionFilterGroups = [{ filters: filters.map(filter => ({ dimension: filter.dimension, operator: filter.operator || 'equals', expression: filter.expression })) }]; } const service = await getGscService(); const response = await service.searchanalytics.query({ siteUrl: GSC_SITE_URL, requestBody }); // Format response const result = { metadata: { site_url: GSC_SITE_URL, start_date: startDate, end_date: endDate, dimensions, search_type, total_rows: response.data.rows?.length || 0, row_limit, start_row }, data: [] }; if (response.data.rows) { result.data = response.data.rows.map(row => { const dataRow = {}; // Add dimension values if (row.keys) { dimensions.forEach((dimension, i) => { if (i < row.keys.length) { dataRow[dimension] = row.keys[i]; } }); } // Add metrics dataRow.clicks = row.clicks || 0; dataRow.impressions = row.impressions || 0; dataRow.ctr = Math.round((row.ctr || 0) * 100 * 100) / 100; // Convert to percentage dataRow.position = Math.round((row.position || 0) * 10) / 10; return dataRow; }); } return { content: [ { type: "text", text: JSON.stringify(result, null, 2), }, ], }; } case "get_sitemaps": { const service = await getGscService(); const response = await service.sitemaps.list({ siteUrl: GSC_SITE_URL }); const result = response.data.sitemap?.map(sitemap => ({ path: sitemap.path, lastSubmitted: sitemap.lastSubmitted, isPending: sitemap.isPending || false, isSitemapsIndex: sitemap.isSitemapsIndex || false, type: sitemap.type, lastDownloaded: sitemap.lastDownloaded, warnings: sitemap.warnings || 0, errors: sitemap.errors || 0 })) || []; return { content: [ { type: "text", text: JSON.stringify(result, null, 2), }, ], }; } case "submit_sitemap": { const { sitemap_url } = args; if (!sitemap_url) { throw new Error("sitemap_url is required"); } const service = await getGscService(); await service.sitemaps.submit({ siteUrl: GSC_SITE_URL, feedpath: sitemap_url }); return { content: [ { type: "text", text: JSON.stringify({ success: `Sitemap submitted successfully: ${sitemap_url}` }, null, 2), }, ], }; } case "delete_sitemap": { const { sitemap_url } = args; if (!sitemap_url) { throw new Error("sitemap_url is required"); } const service = await getGscService(); await service.sitemaps.delete({ siteUrl: GSC_SITE_URL, feedpath: sitemap_url }); return { content: [ { type: "text", text: JSON.stringify({ success: `Sitemap deleted successfully: ${sitemap_url}` }, null, 2), }, ], }; } default: throw new Error(`Unknown tool: ${name}`); } } catch (error) { return { content: [ { type: "text", text: JSON.stringify({ error: error.message }, null, 2), }, ], isError: true, }; } }); // Start the server async function main() { const transport = new StdioServerTransport(); await server.connect(transport); console.error("🚀 Google Search Console MCP server running on stdio"); } if (require.main === module) { main().catch((error) => { console.error("Fatal error in main():", error); process.exit(1); }); } module.exports = { server };