@bdmarvin/mcp-server-gbp
Version:
MCP server for Google Business Profile Performance API.
322 lines (321 loc) • 19 kB
JavaScript
#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
import { zodToJsonSchema } from 'zod-to-json-schema';
import { z } from 'zod';
import { GetDailyPerformanceMetricsInputSchema, GetMonthlySearchKeywordsInputSchema, ListBusinessesInputSchema, GetBusinessInformationInputSchema, GetMultipleBusinessInformationInputSchema, UpdateBusinessInformationInputSchema, ListManagedAccountsInputSchema, ListQuestionsInputSchema, CreateQuestionInputSchema, AnswerQuestionInputSchema, ListServicesInputSchema, UpdateServicesInputSchema, CreateLocalPostInputSchemaV4, ListLocalPostsInputSchemaV4, GetLocalPostInputSchemaV4, UpdateLocalPostInputSchemaV4, DeleteLocalPostInputSchemaV4, } from './schemas.js';
import { GbpPerformanceService } from './gbp-performance-service.js';
import { GbpBusinessInformationService } from './gbp-business-information-service.js';
import { GbpQaService } from './gbp-qa-service.js';
import { GbpLocalPostsServiceV4 } from './gbp-local-posts-service.js';
const packageVersion = '0.3.16'; // Incremented version for media removal
// Manually define the JSON schema for CreateLocalPost to avoid provider rejection
const CreateLocalPostManualSchema = {
type: 'object',
properties: {
accountId: { type: 'string' },
locationId: { type: 'string' },
post: {
type: 'object',
properties: {
languageCode: { type: 'string', default: 'en-US' },
summary: { type: 'string' },
topicType: { type: 'string', enum: ['STANDARD', 'EVENT', 'OFFER'] },
callToAction: {
type: 'object',
properties: {
actionType: { type: 'string', enum: ['BOOK', 'ORDER', 'SHOP', 'LEARN_MORE', 'SIGN_UP', 'CALL'] },
url: { type: 'string', format: 'uri' },
},
required: ['actionType', 'url'],
},
media: {
type: 'array',
items: {
type: 'object',
properties: {
mediaFormat: { type: 'string', enum: ['PHOTO', 'VIDEO'] },
sourceUrl: { type: 'string', format: 'uri' },
},
required: ['mediaFormat', 'sourceUrl'],
},
},
event: {
type: 'object',
properties: {
title: { type: 'string' },
schedule: {
type: 'object',
properties: {
startDate: {
type: 'object',
properties: { year: { type: 'integer' }, month: { type: 'integer' }, day: { type: 'integer' } },
required: ['year', 'month', 'day'],
},
startTime: {
type: 'object',
properties: { hours: { type: 'integer' }, minutes: { type: 'integer' }, seconds: { type: 'integer' } },
},
endDate: {
type: 'object',
properties: { year: { type: 'integer' }, month: { type: 'integer' }, day: { type: 'integer' } },
required: ['year', 'month', 'day'],
},
endTime: {
type: 'object',
properties: { hours: { type: 'integer' }, minutes: { type: 'integer' }, seconds: { type: 'integer' } }
},
},
required: ['startDate', 'endDate'],
},
},
required: ['title', 'schedule'],
},
offer: {
type: 'object',
properties: {
couponCode: { type: 'string' },
redeemOnlineUrl: { type: 'string', format: 'uri' },
termsConditions: { type: 'string' },
},
},
},
required: ['summary', 'topicType'],
},
},
required: ['accountId', 'locationId', 'post'],
};
// Manually define the JSON schema for UpdateLocalPost
const UpdateLocalPostManualSchema = {
type: 'object',
properties: {
accountId: { type: 'string' },
locationId: { type: 'string' },
localPostId: { type: 'string' },
updateMask: { type: 'string' },
post: {
type: 'object',
properties: {
summary: { type: 'string' },
callToAction: {
type: 'object',
properties: {
actionType: { type: 'string', enum: ['BOOK', 'ORDER', 'SHOP', 'LEARN_MORE', 'SIGN_UP', 'CALL'] },
url: { type: 'string', format: 'uri' },
},
required: ['actionType', 'url'],
},
media: {
type: 'array',
items: {
type: 'object',
properties: {
mediaFormat: { type: 'string', enum: ['PHOTO', 'VIDEO'] },
sourceUrl: { type: 'string', format: 'uri' },
},
required: ['mediaFormat', 'sourceUrl'],
},
},
event: {
type: 'object',
properties: {
title: { type: 'string' },
schedule: {
type: 'object',
properties: {
startDate: {
type: 'object',
properties: { year: { type: 'integer' }, month: { type: 'integer' }, day: { type: 'integer' } },
required: ['year', 'month', 'day'],
},
startTime: {
type: 'object',
properties: { hours: { type: 'integer' }, minutes: { type: 'integer' }, seconds: { type: 'integer' } }
},
endDate: {
type: 'object',
properties: { year: { type: 'integer' }, month: { type: 'integer' }, day: { type: 'integer' } },
required: ['year', 'month', 'day'],
},
endTime: {
type: 'object',
properties: { hours: { type: 'integer' }, minutes: { type: 'integer' }, seconds: { type: 'integer' } }
},
},
required: ['startDate', 'endDate'],
},
},
required: ['title', 'schedule'],
},
offer: {
type: 'object',
properties: {
couponCode: { type: 'string' },
redeemOnlineUrl: { type: 'string', format: 'uri' },
termsConditions: { type: 'string' },
},
},
},
},
},
required: ['accountId', 'locationId', 'localPostId', 'updateMask', 'post'],
};
const server = new Server({ name: 'gbp-mcp-server', version: packageVersion }, { capabilities: { tools: {} } });
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{ name: 'list_managed_accounts', description: 'Lists all Google Business Profile accounts accessible by the authenticated user.', inputSchema: zodToJsonSchema(ListManagedAccountsInputSchema) },
{ name: 'get_daily_performance_metrics', description: 'Fetches daily performance metrics for a Google Business Profile location.', inputSchema: zodToJsonSchema(GetDailyPerformanceMetricsInputSchema) },
{ name: 'get_monthly_search_keywords', description: 'Fetches monthly search keyword impressions for a Google Business Profile location.', inputSchema: zodToJsonSchema(GetMonthlySearchKeywordsInputSchema) },
{ name: 'list_businesses', description: 'Lists businesses associated with a Google Business Profile account.', inputSchema: zodToJsonSchema(ListBusinessesInputSchema) },
{ name: 'get_business_information', description: 'Fetches detailed information for a specific Google Business Profile location.', inputSchema: zodToJsonSchema(GetBusinessInformationInputSchema) },
{ name: 'get_multiple_business_information', description: 'Fetches detailed information for multiple Google Business Profile locations.', inputSchema: zodToJsonSchema(GetMultipleBusinessInformationInputSchema) },
{ name: 'update_business_information', description: 'Updates specific fields of a Google Business Profile location.', inputSchema: zodToJsonSchema(UpdateBusinessInformationInputSchema) },
{ name: 'list_services', description: 'Lists the services for a Google Business Profile location.', inputSchema: zodToJsonSchema(ListServicesInputSchema) },
{ name: 'update_services', description: 'Updates the services for a Google Business Profile location.', inputSchema: zodToJsonSchema(UpdateServicesInputSchema) },
{ name: 'list_questions', description: 'Lists questions for a Google Business Profile location.', inputSchema: zodToJsonSchema(ListQuestionsInputSchema) },
{ name: 'create_question', description: 'Creates a new question for a Google Business Profile location.', inputSchema: zodToJsonSchema(CreateQuestionInputSchema) },
{ name: 'answer_question', description: 'Answers a question for a Google Business Profile location.', inputSchema: zodToJsonSchema(AnswerQuestionInputSchema) },
{ name: 'create_local_post', description: 'Creates a new local post for a location using the v4 API.', inputSchema: CreateLocalPostManualSchema },
{ name: 'list_local_posts', description: 'Lists all local posts for a location using the v4 API.', inputSchema: zodToJsonSchema(ListLocalPostsInputSchemaV4) },
{ name: 'get_local_post', description: 'Gets a specific local post by its name using the v4 API.', inputSchema: zodToJsonSchema(GetLocalPostInputSchemaV4) },
{ name: 'update_local_post', description: 'Updates a specific local post using the v4 API.', inputSchema: UpdateLocalPostManualSchema },
{ name: 'delete_local_post', description: 'Deletes a specific local post using the v4 API.', inputSchema: zodToJsonSchema(DeleteLocalPostInputSchemaV4) }
]
};
});
server.setRequestHandler(CallToolRequestSchema, async (request) => {
try {
const allArgsFromController = request.params.arguments;
const googleAccessToken = allArgsFromController.__google_access_token__;
const googleUserEmail = allArgsFromController.__google_user_email__;
if (typeof googleAccessToken !== 'string' || !googleAccessToken) {
throw new Error('__google_access_token__ not provided or invalid.');
}
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(`GBP Server: Tool: ${request.params.name}, User: ${googleUserEmail || 'unknown'}`);
const gbpPerformanceService = new GbpPerformanceService();
const gbpBusinessInformationService = new GbpBusinessInformationService();
const gbpQaService = new GbpQaService();
const gbpLocalPostsService = new GbpLocalPostsServiceV4();
let responseData;
switch (request.params.name) {
case 'list_managed_accounts': {
const parsedArgs = ListManagedAccountsInputSchema.parse(originalToolArgs);
responseData = await gbpBusinessInformationService.listManagedAccounts(googleAccessToken, parsedArgs);
break;
}
case 'get_daily_performance_metrics': {
const parsedArgs = GetDailyPerformanceMetricsInputSchema.parse(originalToolArgs);
responseData = await gbpPerformanceService.getDailyPerformanceMetrics(googleAccessToken, parsedArgs);
break;
}
case 'get_monthly_search_keywords': {
const parsedArgs = GetMonthlySearchKeywordsInputSchema.parse(originalToolArgs);
responseData = await gbpPerformanceService.getMonthlySearchKeywords(googleAccessToken, parsedArgs);
break;
}
case 'list_businesses': {
const parsedArgs = ListBusinessesInputSchema.parse(originalToolArgs);
// Explicitly include accountId in parsedArgs for listBusinesses
parsedArgs.accountId = allArgsFromController.accountId;
responseData = await gbpBusinessInformationService.listBusinesses(googleAccessToken, parsedArgs);
break;
}
case 'get_business_information': {
const parsedArgs = GetBusinessInformationInputSchema.parse(originalToolArgs);
responseData = await gbpBusinessInformationService.getBusinessInformation(googleAccessToken, parsedArgs);
break;
}
case 'get_multiple_business_information': {
const parsedArgs = GetMultipleBusinessInformationInputSchema.parse(originalToolArgs);
responseData = await gbpBusinessInformationService.getMultipleBusinessInformation(googleAccessToken, parsedArgs);
return { content: [{ type: 'text', text: JSON.stringify(responseData, null, 2) }] };
}
case 'update_business_information': {
const parsedArgs = UpdateBusinessInformationInputSchema.parse(originalToolArgs);
responseData = await gbpBusinessInformationService.updateBusinessInformation(googleAccessToken, parsedArgs);
break;
}
case 'list_services': {
const parsedArgs = ListServicesInputSchema.parse(originalToolArgs);
responseData = await gbpBusinessInformationService.listServices(googleAccessToken, parsedArgs);
break;
}
case 'update_services': {
const parsedArgs = UpdateServicesInputSchema.parse(originalToolArgs);
responseData = await gbpBusinessInformationService.updateServices(googleAccessToken, parsedArgs);
break;
}
case 'list_questions': {
const parsedArgs = ListQuestionsInputSchema.parse(originalToolArgs);
responseData = await gbpQaService.listQuestions(googleAccessToken, parsedArgs);
break;
}
case 'create_question': {
const parsedArgs = CreateQuestionInputSchema.parse(originalToolArgs);
responseData = await gbpQaService.createQuestion(googleAccessToken, parsedArgs);
break;
}
case 'answer_question': {
const parsedArgs = AnswerQuestionInputSchema.parse(originalToolArgs);
responseData = await gbpQaService.answerQuestion(googleAccessToken, parsedArgs);
break;
}
case 'create_local_post': {
const parsedArgs = CreateLocalPostInputSchemaV4.parse(originalToolArgs);
responseData = await gbpLocalPostsService.createLocalPost(googleAccessToken, parsedArgs);
break;
}
case 'list_local_posts': {
const parsedArgs = ListLocalPostsInputSchemaV4.parse(originalToolArgs);
responseData = await gbpLocalPostsService.listLocalPosts(googleAccessToken, parsedArgs);
break;
}
case 'get_local_post': {
const parsedArgs = GetLocalPostInputSchemaV4.parse(originalToolArgs);
responseData = await gbpLocalPostsService.getLocalPost(googleAccessToken, parsedArgs);
break;
}
case 'update_local_post': {
const parsedArgs = UpdateLocalPostInputSchemaV4.parse(originalToolArgs);
responseData = await gbpLocalPostsService.updateLocalPost(googleAccessToken, parsedArgs);
break;
}
case 'delete_local_post': {
const parsedArgs = DeleteLocalPostInputSchemaV4.parse(originalToolArgs);
responseData = await gbpLocalPostsService.deleteLocalPost(googleAccessToken, parsedArgs);
break;
}
default:
throw new Error(`Unknown tool: ${request.params.name}`);
}
return { content: [{ type: 'text', text: JSON.stringify(responseData?.data || responseData, null, 2) }] };
}
catch (error) {
console.error(`Error in CallToolRequest for '${request.params.name}':`, error.message, error.stack);
if (error.response?.data) {
console.error('Google API Details:', JSON.stringify(error.response.data, null, 2));
}
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('; ')}`);
}
throw new Error(error.message || `An unexpected error occurred in tool '${request.params.name}'`);
}
});
async function runServer() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error(`Google Business Profile MCP Server (v${packageVersion}) running on stdio, expecting __google_access_token__`);
}
runServer().catch((error) => {
console.error('Fatal error running GBP MCP Server:', error);
process.exit(1);
});