# 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: ```typescript // 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; handler: (args: any, gscClient: GSCClient) => Promise; } export interface ToolResult { content: Array<{ type: 'text' | 'image' | 'resource'; text?: string; data?: string; mimeType?: string; }>; structuredContent?: Record; // For MCP Apps isError?: boolean; } export interface ToolModule { tools: ToolDefinition[]; } ``` ## App Interface Each app in `src/apps/` exports: ```typescript // 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` and a `toolRegistry: Map`. 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: ```typescript export class GSCClient { // Properties listSites(): Promise getSiteDetails(siteUrl: string): Promise addSite(siteUrl: string): Promise removeSite(siteUrl: string): Promise // Analytics searchAnalytics(siteUrl: string, params: SearchAnalyticsParams): Promise // URL Inspection inspectUrl(siteUrl: string, inspectionUrl: string): Promise // Sitemaps listSitemaps(siteUrl: string): Promise getSitemap(siteUrl: string, feedpath: string): Promise submitSitemap(siteUrl: string, feedpath: string): Promise deleteSitemap(siteUrl: string, feedpath: string): Promise // Indexing (separate API) requestIndexing(url: string): Promise } ``` ## 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 ```json { "@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 |