@bdmarvin/mcp-server-gbp
Version:
MCP server for Google Business Profile Performance API.
201 lines (200 loc) • 9.17 kB
JavaScript
import { z } from 'zod';
// Common Date schema (as defined from discovery doc)
export const GbpDateSchema = z.object({
year: z.number().int().min(0).max(9999),
month: z.number().int().min(0).max(12),
day: z.number().int().min(0).max(31),
}).refine(data => !(data.month === 0 && data.day !== 0), {
message: "If month is 0, day must also be 0",
path: ["day"],
});
// Create a structurally identical but distinct schema for EndMonth (used in GetMonthlySearchKeywordsInputSchema)
export const GbpEndMonthDateSchema = z.object({
year: z.number().int().min(0).max(9999),
month: z.number().int().min(0).max(12),
day: z.number().int().min(0).max(31),
}).refine(data => !(data.month === 0 && data.day !== 0), {
message: "If month is 0, day must also be 0",
path: ["day"],
});
// Create a structurally identical but distinct schema for EndDate (for GetDailyPerformanceMetricsInputSchema)
export const GbpEndDateSchema = z.object({
year: z.number().int().min(0).max(9999),
month: z.number().int().min(0).max(12),
day: z.number().int().min(0).max(31),
}).refine(data => !(data.month === 0 && data.day !== 0), {
message: "If month is 0, day must also be 0",
path: ["day"],
});
// Enum for Daily Metrics (from discovery doc)
export const DailyMetricEnum = z.enum([
"DAILY_METRIC_UNKNOWN",
"BUSINESS_IMPRESSIONS_DESKTOP_MAPS",
"BUSINESS_IMPRESSIONS_DESKTOP_SEARCH",
"BUSINESS_IMPRESSIONS_MOBILE_MAPS",
"BUSINESS_IMPRESSIONS_MOBILE_SEARCH",
"BUSINESS_CONVERSATIONS",
"BUSINESS_DIRECTION_REQUESTS",
"CALL_CLICKS",
"WEBSITE_CLICKS",
"BUSINESS_BOOKINGS",
"BUSINESS_FOOD_ORDERS",
"BUSINESS_FOOD_MENU_CLICKS"
]);
// Input schema for 'get_daily_performance_metrics' tool
export const GetDailyPerformanceMetricsInputSchema = z.object({
locationName: z.string().regex(/^locations\/[^/]+$/, "locationName must be in the format locations/{location_id}"),
dailyMetrics: z.array(DailyMetricEnum).min(1, "At least one metric must be requested in dailyMetrics"),
startDate: GbpDateSchema,
endDate: GbpEndDateSchema, // Use the new distinct schema for endDate
});
// Input schema for 'get_monthly_search_keywords' tool
export const GetMonthlySearchKeywordsInputSchema = z.object({
parentLocationName: z.string().regex(/^locations\/[^/]+$/, "parentLocationName must be in the format locations/{location_id}"),
startMonth: GbpDateSchema.refine(d => d.year !== 0 && d.month !== 0, { message: "Year and month are required for startMonth", path: ["startMonth"] }),
endMonth: GbpEndMonthDateSchema.refine(d => d.year !== 0 && d.month !== 0, { message: "Year and month are required for endMonth", path: ["endMonth"] }),
pageSize: z.number().int().min(1).max(100).optional(),
pageToken: z.string().optional(),
});
// --- Schemas for Business Information API ---
export const ListBusinessesInputSchema = z.object({
accountId: z.string().min(1, "accountId is required"),
readMask: z.string().optional().describe("Field mask, e.g., name,title,storeCode,storefrontAddress.addressLines"),
pageSize: z.number().int().min(1).max(100).optional(),
pageToken: z.string().optional(),
filter: z.string().optional().describe('Filter string, e.g., title = "My Store"'),
orderBy: z.string().optional().describe("Order by string, e.g., title, store_code desc"),
});
export const GetBusinessInformationInputSchema = z.object({
locationName: z.string().regex(/^locations\/[^/]+$/, "locationName must be in the format locations/{locationId}"),
readMask: z.string().optional().describe("Field mask to specify returned fields"),
});
export const GetMultipleBusinessInformationInputSchema = z.object({
locationNames: z.array(z.string().regex(/^locations\/[^/]+$/, "Each locationName must be in the format locations/{locationId}")).min(1, "At least one locationName is required"),
readMask: z.string().optional().describe("Field mask to specify returned fields for each location"),
});
export const UpdateBusinessInformationInputSchema = z.object({
locationName: z.string().regex(/^locations\/[^/]+$/, "locationName must be in the format locations/{locationId}"),
updateMask: z.string().min(1, "updateMask is required to specify which fields to update."),
data: z.any().describe("An object containing the fields to update, corresponding to the updateMask."),
});
// --- Schemas for Business Services API ---
const MoneySchema = z.object({
currencyCode: z.string().optional(),
units: z.string().optional(),
nanos: z.number().optional(),
});
const ServiceItemSchema = z.object({
serviceTypeId: z.string(),
displayName: z.string(),
description: z.string().optional(),
price: MoneySchema.optional(),
});
export const ListServicesInputSchema = z.object({
locationName: z.string().regex(/^locations\/[^/]+$/, "locationName must be in the format locations/{locationId}"),
});
export const UpdateServicesInputSchema = z.object({
locationName: z.string().regex(/^locations\/[^/]+$/, "locationName must be in the format locations/{locationId}"),
services: z.array(ServiceItemSchema),
});
// --- Schemas for v4 Local Posts API ---
const V4TimeOfDaySchema = z.object({
hours: z.number().int().optional(),
minutes: z.number().int().optional(),
seconds: z.number().int().optional(),
nanos: z.number().int().optional(),
});
const V4DateSchema = z.object({
year: z.number().int(),
month: z.number().int(),
day: z.number().int(),
});
const V4CallToActionSchema = z.object({
actionType: z.enum(["BOOK", "ORDER", "SHOP", "LEARN_MORE", "SIGN_UP", "CALL"]),
url: z.string().url(),
});
const V4MediaItemSchema = z.object({
mediaFormat: z.enum(["PHOTO", "VIDEO"]),
sourceUrl: z.string().url(),
});
const V4EventSchema = z.object({
title: z.string(),
schedule: z.object({
startDate: V4DateSchema,
startTime: V4TimeOfDaySchema.optional(),
endDate: V4DateSchema,
endTime: V4TimeOfDaySchema.optional(),
}),
});
const V4OfferSchema = z.object({
couponCode: z.string().optional(),
redeemOnlineUrl: z.string().url().optional(),
termsConditions: z.string().optional(),
});
const V4LocalPostSchema = z.object({
languageCode: z.string().default('en-US'),
summary: z.string(),
topicType: z.enum(["STANDARD", "EVENT", "OFFER"]),
callToAction: V4CallToActionSchema.optional(),
media: z.array(V4MediaItemSchema).optional(),
event: V4EventSchema.optional(),
offer: V4OfferSchema.optional(),
});
export const CreateLocalPostInputSchemaV4 = z.object({
accountId: z.string(),
locationId: z.string(),
post: V4LocalPostSchema,
});
export const ListLocalPostsInputSchemaV4 = z.object({
accountId: z.string(),
locationId: z.string(),
pageSize: z.number().int().optional(),
pageToken: z.string().optional(),
});
export const GetLocalPostInputSchemaV4 = z.object({
accountId: z.string(),
locationId: z.string(),
localPostId: z.string(),
});
export const UpdateLocalPostInputSchemaV4 = z.object({
accountId: z.string(),
locationId: z.string(),
localPostId: z.string(),
updateMask: z.string(),
post: V4LocalPostSchema.omit({ languageCode: true, topicType: true }), // These fields are immutable
});
export const DeleteLocalPostInputSchemaV4 = z.object({
accountId: z.string(),
locationId: z.string(),
localPostId: z.string(),
});
// --- New Schema for Account Management API ---
export const AccountTypeEnum = z.enum([
"PERSONAL",
"LOCATION_GROUP",
"USER_GROUP",
"ORGANIZATION"
]).describe("Filter by account type");
export const ListManagedAccountsInputSchema = z.object({
pageSize: z.number().int().min(1).max(20).optional().describe("How many accounts to fetch per page. Default and maximum is 20."),
pageToken: z.string().optional().describe("Token for fetching the next page of accounts."),
filterAccountType: AccountTypeEnum.optional().describe("Optional: Filter accounts by type (e.g., LOCATION_GROUP)"),
});
// --- Schemas for Q&A API ---
export const ListQuestionsInputSchema = z.object({
locationName: z.string().regex(/^locations\/[^/]+$/, "locationName must be in the format locations/{location_id}"),
pageSize: z.number().int().min(1).max(100).optional(),
pageToken: z.string().optional(),
});
export const CreateQuestionInputSchema = z.object({
locationName: z.string().regex(/^locations\/[^/]+$/, "locationName must be in the format locations/{location_id}"),
text: z.string().min(1, "Question text cannot be empty"),
});
export const AnswerQuestionInputSchema = z.object({
questionName: z.string().regex(/^locations\/[^/]+\/questions\/[^/]+$/, "questionName must be in the format locations/{location_id}/questions/{question_id}"),
text: z.string().min(1, "Answer text cannot be empty"),
});
// New Schema for Get Question (if needed, but not in current tools)
export const GetQuestionInputSchema = z.object({
questionName: z.string().regex(/^locations\/[^/]+\/questions\/[^/]+$/, "questionName must be in the format locations/{location_id}/questions/{question_id}"),
});