# 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<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:

```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<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:
```typescript
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
```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 |
