@bdmarvin/mcp-server-gsc
Version:
A Model Context Protocol (MCP) server providing access to Google Search Console using OAuth2 tokens.
158 lines (157 loc) • 7.54 kB
JavaScript
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
// @ts-ignore
import { zodToJsonSchema } from 'zod-to-json-schema';
import { GetSitemapSchema, IndexInspectSchema, ListSitemapsSchema, SearchAnalyticsSchema, SubmitSitemapSchema, } from './schemas.js';
import { z } from 'zod';
import { SearchConsoleService } from './search-console.js';
const server = new Server({
name: 'gsc-mcp-server',
version: '0.2.1', // Updated version
}, {
capabilities: {
resources: {},
tools: {},
prompts: {},
},
});
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: 'list_sites',
description: 'List all sites in Google Search Console',
inputSchema: zodToJsonSchema(z.object({})),
},
{
name: 'search_analytics',
description: 'Get search performance data from Google Search Console',
inputSchema: zodToJsonSchema(SearchAnalyticsSchema),
},
{
name: 'index_inspect',
description: 'Inspect a URL to see if it is indexed or can be indexed',
inputSchema: zodToJsonSchema(IndexInspectSchema),
},
{
name: 'list_sitemaps',
description: 'List sitemaps for a site in Google Search Console',
inputSchema: zodToJsonSchema(ListSitemapsSchema),
},
{
name: 'get_sitemap',
description: 'Get a sitemap for a site in Google Search Console',
inputSchema: zodToJsonSchema(GetSitemapSchema),
},
{
name: 'submit_sitemap',
description: 'Submit a sitemap for a site in Google Search Console',
inputSchema: zodToJsonSchema(SubmitSitemapSchema),
},
],
};
});
server.setRequestHandler(CallToolRequestSchema, async (request) => {
try {
const allArgsFromController = request.params.arguments;
if (!allArgsFromController) {
throw new Error('Arguments object is required from the controller');
}
const googleAccessToken = allArgsFromController.__google_access_token__;
const googleUserId = allArgsFromController.__google_user_id__;
const googleUserEmail = allArgsFromController.__google_user_email__;
if (typeof googleAccessToken !== 'string' || !googleAccessToken) {
throw new Error('__google_access_token__ not provided or invalid in arguments by the MCP OAuth Controller.');
}
const originalToolArgs = {};
for (const key in allArgsFromController) {
if (key !== '__google_access_token__' && key !== '__google_user_id__' && key !== '__google_user_email__') {
originalToolArgs[key] = allArgsFromController[key];
}
}
console.error(`GSC Server: Received call for tool: ${request.params.name}`);
console.error(`GSC Server: Google User Context: ID=${googleUserId}, Email=${googleUserEmail}`);
const searchConsole = new SearchConsoleService();
switch (request.params.name) {
case 'search_analytics': {
const parsedArgs = SearchAnalyticsSchema.parse(originalToolArgs);
const siteUrl = parsedArgs.siteUrl;
const requestBody = {
startDate: parsedArgs.startDate,
endDate: parsedArgs.endDate,
dimensions: parsedArgs.dimensions,
searchType: parsedArgs.type,
aggregationType: parsedArgs.aggregationType,
rowLimit: parsedArgs.rowLimit,
};
const response = await searchConsole.searchAnalytics(googleAccessToken, siteUrl, requestBody);
return { content: [{ type: 'text', text: JSON.stringify(response.data, null, 2) }] };
}
case 'list_sites': {
const response = await searchConsole.listSites(googleAccessToken);
return { content: [{ type: 'text', text: JSON.stringify(response.data, null, 2) }] };
}
case 'index_inspect': {
const parsedArgs = IndexInspectSchema.parse(originalToolArgs);
const requestBody = {
siteUrl: parsedArgs.siteUrl,
inspectionUrl: parsedArgs.inspectionUrl,
languageCode: parsedArgs.languageCode,
};
const response = await searchConsole.indexInspect(googleAccessToken, requestBody);
return { content: [{ type: 'text', text: JSON.stringify(response.data, null, 2) }] };
}
case 'list_sitemaps': {
const parsedArgs = ListSitemapsSchema.parse(originalToolArgs);
const requestBody = {
siteUrl: parsedArgs.siteUrl,
sitemapIndex: parsedArgs.sitemapIndex,
};
const response = await searchConsole.listSitemaps(googleAccessToken, requestBody);
return { content: [{ type: 'text', text: JSON.stringify(response.data, null, 2) }] };
}
case 'get_sitemap': {
const parsedArgs = GetSitemapSchema.parse(originalToolArgs);
const requestBody = {
siteUrl: parsedArgs.siteUrl,
feedpath: parsedArgs.feedpath,
};
const response = await searchConsole.getSitemap(googleAccessToken, requestBody);
return { content: [{ type: 'text', text: JSON.stringify(response.data, null, 2) }] };
}
case 'submit_sitemap': {
const parsedArgs = SubmitSitemapSchema.parse(originalToolArgs);
const requestBody = {
siteUrl: parsedArgs.siteUrl,
feedpath: parsedArgs.feedpath,
};
const response = await searchConsole.submitSitemap(googleAccessToken, requestBody);
return { content: [{ type: 'text', text: JSON.stringify(response.data, null, 2) }] };
}
default:
throw new Error(`Unknown tool: ${request.params.name}`);
}
}
catch (error) {
console.error(`Error in CallToolRequest handler for tool '${request.params.name}':`, error);
if (error instanceof z.ZodError) {
const messages = error.errors.map((e) => `${e.path.join('.') || 'argument'}: ${e.message}`);
throw new Error(`Invalid arguments for tool '${request.params.name}': ${messages.join('; ')}`);
}
if (error instanceof Error) {
throw error;
}
throw new Error(`An unexpected error occurred in tool '${request.params.name}': ${String(error)}`);
}
});
async function runServer() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('Google Search Console MCP Server (OAuth2 Ready - v0.2.1) running on stdio'); // Updated version in log
}
runServer().catch((error) => {
console.error('Fatal error running GSC MCP Server:', error);
process.exit(1);
});