8.9 KiB

Google Search Console MCP Server — Architecture Spec

Overview

A TypeScript MCP server for Google Search Console with:

  • 22 tools across 4 categories
  • 5 interactive MCP Apps (structured content UI)
  • Full tool annotations (readOnlyHint, destructiveHint, idempotentHint, openWorldHint)
  • Lazy loading via gateway tools + discover_tools meta-tool
  • OAuth 2.0 + Service Account auth
  • Response caching, rate limiting, server-side date handling

Project Structure

google-console-mcp/
├── src/
│   ├── index.ts              # Entry point — creates server, connects transport
│   ├── server.ts             # MCP server setup, lazy loading registry, tool/app registration
│   ├── auth/
│   │   ├── index.ts          # Auth manager — picks OAuth or Service Account
│   │   ├── oauth.ts          # OAuth 2.0 flow (personal accounts)
│   │   └── service-account.ts # Service account auth (automated/CI)
│   ├── tools/
│   │   ├── types.ts          # Shared tool module interface
│   │   ├── discovery.ts      # list_properties, get_property_details, verify_ownership, discover_tools
│   │   ├── analytics.ts      # search_analytics, compare_periods, top_movers, mobile_vs_desktop, country_breakdown
│   │   ├── intelligence.ts   # detect_quick_wins, keyword_cannibalization, content_decay_detection, query_clustering
│   │   ├── indexing.ts       # inspect_url, batch_inspect_urls, request_indexing
│   │   ├── sitemaps.ts       # list_sitemaps, get_sitemap, submit_sitemap, delete_sitemap
│   │   └── management.ts     # add_property, remove_property
│   ├── apps/
│   │   ├── types.ts          # App registration interface
│   │   ├── dashboard.ts      # Performance Dashboard app
│   │   ├── quick-wins.ts     # Quick Wins Board app
│   │   ├── url-health.ts     # URL Health Inspector app
│   │   ├── cannibalization.ts # Keyword Cannibalization Map app
│   │   └── content-decay.ts  # Content Decay Tracker app
│   ├── lib/
│   │   ├── gsc-client.ts     # Google Search Console API wrapper (googleapis)
│   │   ├── cache.ts          # In-memory response cache with TTL
│   │   ├── rate-limit.ts     # API quota management (1200 queries/min default)
│   │   └── date-utils.ts     # Server-side date calculations (prevents LLM hallucination)
│   └── schemas/
│       ├── analytics.ts      # Zod schemas for analytics tools
│       ├── indexing.ts       # Zod schemas for indexing tools
│       ├── sitemaps.ts       # Zod schemas for sitemap tools
│       ├── intelligence.ts   # Zod schemas for intelligence tools
│       └── management.ts     # Zod schemas for management tools
├── package.json
├── tsconfig.json
└── README.md

Tool Module Interface

Every tool file in src/tools/ MUST export this interface:

// src/tools/types.ts
import { z } from 'zod';

export interface ToolAnnotations {
  readOnlyHint?: boolean;
  destructiveHint?: boolean;
  idempotentHint?: boolean;
  openWorldHint?: boolean;
}

export interface ToolDefinition {
  name: string;
  description: string;
  category: 'discovery' | 'analytics' | 'intelligence' | 'indexing' | 'sitemaps' | 'management';
  annotations: ToolAnnotations;
  inputSchema: z.ZodType<any>;
  handler: (args: any, gscClient: GSCClient) => Promise<ToolResult>;
}

export interface ToolResult {
  content: Array<{
    type: 'text' | 'image' | 'resource';
    text?: string;
    data?: string;
    mimeType?: string;
  }>;
  structuredContent?: Record<string, any>; // For MCP Apps
  isError?: boolean;
}

export interface ToolModule {
  tools: ToolDefinition[];
}

App Interface

Each app in src/apps/ exports:

// src/apps/types.ts
export interface AppDefinition {
  /** Tool name that triggers this app */
  toolName: string;
  /** Resource URI for the app HTML */
  resourceUri: string;
  /** The bundled HTML string (self-contained, no external deps) */
  html: string;
  /** Description shown in tool listing */
  description: string;
}

Apps are self-contained HTML strings with inline CSS and JS. They receive data via the tool result's structuredContent and render it client-side. Use the MCP Apps SDK pattern: app.callServerTool() for any server communication.

Lazy Loading

The server starts with only 4 tools registered:

  1. list_properties (discovery)
  2. search_analytics (analytics)
  3. inspect_url (indexing)
  4. discover_tools (meta — returns available categories + tool descriptions)

When the AI calls discover_tools({ category: "intelligence" }), the server dynamically registers all intelligence tools and returns their descriptions.

Implementation: server.ts maintains a registeredCategories: Set<string> and a toolRegistry: Map<string, ToolDefinition>. On discover_tools, it adds the requested category's tools to the active set and sends a tools/list_changed notification.

Auth

OAuth 2.0 (for personal use)

  • Uses googleapis OAuth2Client
  • Reads client ID/secret from env or JSON file
  • Opens browser for consent on first run
  • Stores refresh token in ~/.gsc-mcp/oauth-token.json
  • Auto-refreshes access token

Service Account (for CI/automation)

  • Reads from GOOGLE_APPLICATION_CREDENTIALS env var or GSC_SERVICE_ACCOUNT_KEY (base64)
  • Uses google-auth-library JWT client
  • No interactive flow needed

Auth Selection

  • If GSC_OAUTH_CLIENT_FILE or GSC_OAUTH_CLIENT_ID is set → OAuth
  • If GOOGLE_APPLICATION_CREDENTIALS or GSC_SERVICE_ACCOUNT_KEY is set → Service Account
  • Both can coexist; OAuth takes priority

GSC Client (lib/gsc-client.ts)

Wraps the Google Search Console API:

export class GSCClient {
  // Properties
  listSites(): Promise<Site[]>
  getSiteDetails(siteUrl: string): Promise<SiteDetails>
  addSite(siteUrl: string): Promise<void>
  removeSite(siteUrl: string): Promise<void>
  
  // Analytics
  searchAnalytics(siteUrl: string, params: SearchAnalyticsParams): Promise<SearchAnalyticsResponse>
  
  // URL Inspection
  inspectUrl(siteUrl: string, inspectionUrl: string): Promise<InspectionResult>
  
  // Sitemaps
  listSitemaps(siteUrl: string): Promise<Sitemap[]>
  getSitemap(siteUrl: string, feedpath: string): Promise<Sitemap>
  submitSitemap(siteUrl: string, feedpath: string): Promise<void>
  deleteSitemap(siteUrl: string, feedpath: string): Promise<void>
  
  // Indexing (separate API)
  requestIndexing(url: string): Promise<void>
}

Cache Strategy

  • Analytics responses: 15 min TTL (data is 2-3 days stale anyway)
  • Sitemap list: 5 min TTL
  • URL inspection: 30 min TTL
  • Properties list: 60 min TTL
  • Cache key = tool name + JSON.stringify(sorted args)

Rate Limiting

  • Google Search Console API: 1,200 queries/minute
  • URL Inspection API: 600 inspections/day per property, 2,000/day total
  • Indexing API: 200 requests/day
  • Built-in token bucket that queues requests when approaching limits

Environment Variables

# Auth (pick one or both)
GSC_OAUTH_CLIENT_FILE=/path/to/client_secrets.json
GSC_OAUTH_CLIENT_ID=xxx
GSC_OAUTH_CLIENT_SECRET=xxx
GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account.json
GSC_SERVICE_ACCOUNT_KEY=base64encodedkey

# Optional
GSC_CACHE_TTL=900          # Default cache TTL in seconds
GSC_DATA_STATE=all         # 'all' for fresh data, 'final' for confirmed only
GSC_DEFAULT_ROW_LIMIT=1000 # Default row limit for analytics
GSC_MAX_ROW_LIMIT=25000    # Max row limit

Dependencies

{
  "@modelcontextprotocol/sdk": "latest",
  "googleapis": "latest",
  "google-auth-library": "latest",
  "zod": "^3.22",
  "zod-to-json-schema": "^3.22",
  "open": "^10.0.0"
}

Tool Annotations Reference

Tool readOnly destructive idempotent openWorld
list_properties true false true false
get_property_details true false true false
verify_ownership true false true false
discover_tools true false true false
search_analytics true false true false
compare_periods true false true false
keyword_cannibalization true false true false
content_decay_detection true false true false
detect_quick_wins true false true false
top_movers true false true false
mobile_vs_desktop true false true false
country_breakdown true false true false
query_clustering true false true false
inspect_url true false true false
batch_inspect_urls true false true false
list_sitemaps true false true false
get_sitemap true false true false
submit_sitemap false false true false
delete_sitemap false true false false
request_indexing false false true true
add_property false false true false
remove_property false true false false