UNPKG

@bdmarvin/mcp-server-gbp

Version:

MCP server for Google Business Profile Performance API.

322 lines (321 loc) 19 kB
#!/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); });