245 lines
8.9 KiB
Markdown
245 lines
8.9 KiB
Markdown
# 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 |
|