Skip to content

Commit 1cdd774

Browse files
committed
feat: improve new place API work
1 parent 5dd3c1b commit 1cdd774

5 files changed

Lines changed: 180 additions & 56 deletions

File tree

src/cli.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export async function startServer(port?: number, apiKey?: string): Promise<void>
3232

3333
Logger.log("🚀 Starting Google Maps MCP Server...");
3434
Logger.log("📍 Available tools: search_nearby, get_place_details, maps_geocode, maps_reverse_geocode, maps_distance_matrix, maps_directions, maps_elevation, echo");
35+
Logger.log("ℹ️ Reminder: enable Places API (New) in https://console.cloud.google.com before using the new Place features.");
3536
Logger.log("");
3637

3738
const startPromises = serverConfigs.map(async (config) => {

src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,6 @@ export const Logger = {
66
console.error("[ERROR]", ...args);
77
},
88
};
9+
10+
export { PlacesSearcher } from "./services/PlacesSearcher.js";
11+
export { NewPlacesService } from "./services/NewPlacesService.js";

src/services/NewPlacesService.ts

Lines changed: 150 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@ import { Logger } from "../index.js";
44
export class NewPlacesService {
55
private client: PlacesClient;
66
private readonly defaultLanguage: string = "en";
7-
7+
private readonly placeFieldMask: string = ["displayName", "name", "id", "formattedAddress", "location", "utcOffsetMinutes", "regularOpeningHours.periods", "regularOpeningHours.weekdayDescriptions", "currentOpeningHours.openNow", "nationalPhoneNumber", "websiteUri", "priceLevel", "rating", "userRatingCount", "reviews.rating", "reviews.text", "reviews.publishTime", "reviews.authorAttribution.displayName", "photos.heightPx", "photos.widthPx", "photos.name"].join(",");
88
constructor(apiKey?: string) {
99
this.client = new PlacesClient({
1010
apiKey: apiKey || process.env.GOOGLE_MAPS_API_KEY || "",
1111
});
12-
12+
1313
if (!apiKey && !process.env.GOOGLE_MAPS_API_KEY) {
1414
throw new Error("Google Maps API Key is required");
1515
}
@@ -18,11 +18,20 @@ export class NewPlacesService {
1818
async getPlaceDetails(placeId: string) {
1919
try {
2020
const placeName = `places/${placeId}`;
21-
22-
const [place] = await this.client.getPlace({
23-
name: placeName,
24-
languageCode: this.defaultLanguage,
25-
});
21+
22+
const [place] = await this.client.getPlace(
23+
{
24+
name: placeName,
25+
languageCode: this.defaultLanguage,
26+
},
27+
{
28+
otherArgs: {
29+
headers: {
30+
"X-Goog-FieldMask": this.placeFieldMask,
31+
},
32+
},
33+
}
34+
);
2635

2736
return this.transformPlaceResponse(place);
2837
} catch (error: any) {
@@ -34,7 +43,7 @@ export class NewPlacesService {
3443
private transformPlaceResponse(place: any) {
3544
return {
3645
name: place.displayName?.text || place.name || "",
37-
place_id: place.id || "",
46+
place_id: this.extractLegacyPlaceId(place),
3847
formatted_address: place.formattedAddress || "",
3948
geometry: {
4049
location: {
@@ -44,65 +53,155 @@ export class NewPlacesService {
4453
},
4554
rating: place.rating || 0,
4655
user_ratings_total: place.userRatingCount || 0,
47-
opening_hours: place.regularOpeningHours ? {
48-
open_now: this.isCurrentlyOpen(place.regularOpeningHours),
49-
weekday_text: this.formatOpeningHours(place.regularOpeningHours),
50-
} : undefined,
56+
opening_hours: place.regularOpeningHours
57+
? {
58+
open_now: this.isCurrentlyOpen(place.regularOpeningHours, place.utcOffsetMinutes, place.currentOpeningHours),
59+
weekday_text: this.formatOpeningHours(place.regularOpeningHours),
60+
}
61+
: undefined,
5162
formatted_phone_number: place.nationalPhoneNumber || "",
5263
website: place.websiteUri || "",
5364
price_level: place.priceLevel || 0,
54-
reviews: place.reviews?.map((review: any) => ({
55-
rating: review.rating || 0,
56-
text: review.text?.text || "",
57-
time: review.publishTime?.seconds || 0,
58-
author_name: review.authorAttribution?.displayName || "",
59-
})) || [],
60-
photos: place.photos?.map((photo: any) => ({
61-
photo_reference: photo.name || "",
62-
height: photo.heightPx || 0,
63-
width: photo.widthPx || 0,
64-
})) || [],
65+
reviews:
66+
place.reviews?.map((review: any) => ({
67+
rating: review.rating || 0,
68+
text: review.text?.text || "",
69+
time: review.publishTime?.seconds || 0,
70+
author_name: review.authorAttribution?.displayName || "",
71+
})) || [],
72+
photos:
73+
place.photos?.map((photo: any) => ({
74+
photo_reference: photo.name || "",
75+
height: photo.heightPx || 0,
76+
width: photo.widthPx || 0,
77+
})) || [],
6578
};
6679
}
6780

68-
private isCurrentlyOpen(openingHours: any): boolean {
69-
if (!openingHours?.weekdayDescriptions) {
81+
private extractLegacyPlaceId(place: any): string {
82+
const resourceName = place?.name;
83+
84+
if (typeof resourceName === "string" && resourceName.startsWith("places/")) {
85+
const legacyId = resourceName.substring("places/".length);
86+
if (legacyId) {
87+
return legacyId;
88+
}
89+
}
90+
91+
return place?.id || "";
92+
}
93+
94+
private isCurrentlyOpen(openingHours: any, utcOffsetMinutes?: number, currentOpeningHours?: any): boolean {
95+
if (typeof currentOpeningHours?.openNow === "boolean") {
96+
return currentOpeningHours.openNow;
97+
}
98+
99+
if (typeof openingHours?.openNow === "boolean") {
100+
return openingHours.openNow;
101+
}
102+
103+
const periods = openingHours?.periods;
104+
105+
if (!Array.isArray(periods) || periods.length === 0) {
70106
return false;
71107
}
72108

73-
const now = new Date();
74-
const currentDay = now.getDay();
75-
const currentTime = now.getHours() * 60 + now.getMinutes();
109+
const minutesInDay = 24 * 60;
110+
const minutesInWeek = minutesInDay * 7;
111+
112+
const { day: localDay, minutes: localMinutes } = this.getLocalTimeComponents(utcOffsetMinutes);
113+
const localTimeValue = localDay * minutesInDay + localMinutes;
76114

77115
const dayMapping = {
78-
"SUNDAY": 0,
79-
"MONDAY": 1,
80-
"TUESDAY": 2,
81-
"WEDNESDAY": 3,
82-
"THURSDAY": 4,
83-
"FRIDAY": 5,
84-
"SATURDAY": 6,
116+
SUNDAY: 0,
117+
MONDAY: 1,
118+
TUESDAY: 2,
119+
WEDNESDAY: 3,
120+
THURSDAY: 4,
121+
FRIDAY: 5,
122+
SATURDAY: 6,
85123
};
86124

87-
const todayHours = openingHours.weekdayDescriptions.find((desc: string) => {
88-
const dayName = Object.keys(dayMapping).find(day =>
89-
desc.toUpperCase().includes(day)
90-
);
91-
return dayName && dayMapping[dayName as keyof typeof dayMapping] === currentDay;
92-
});
125+
const toDayNumber = (value: any): number | undefined => {
126+
if (typeof value === "number" && value >= 0 && value <= 6) {
127+
return value;
128+
}
129+
if (typeof value === "string") {
130+
const normalized = value.toUpperCase();
131+
if (normalized in dayMapping) {
132+
return dayMapping[normalized as keyof typeof dayMapping];
133+
}
134+
}
135+
return undefined;
136+
};
93137

94-
if (!todayHours) {
95-
return false;
96-
}
138+
const toMinutes = (time: any): number | undefined => {
139+
if (!time) {
140+
return undefined;
141+
}
97142

98-
if (todayHours.toLowerCase().includes("closed")) {
99-
return false;
143+
const hours = typeof time.hours === "number" ? time.hours : Number(time.hours ?? NaN);
144+
const minutes = typeof time.minutes === "number" ? time.minutes : Number(time.minutes ?? NaN);
145+
146+
if (!Number.isFinite(hours) || !Number.isFinite(minutes)) {
147+
return undefined;
148+
}
149+
150+
return hours * 60 + minutes;
151+
};
152+
153+
for (const period of periods) {
154+
const openDay = toDayNumber(period?.openDay);
155+
const closeDay = toDayNumber(period?.closeDay ?? period?.openDay);
156+
const openMinutes = toMinutes(period?.openTime);
157+
const closeMinutes = toMinutes(period?.closeTime);
158+
159+
if (openDay === undefined || openMinutes === undefined) {
160+
continue;
161+
}
162+
163+
let start = openDay * minutesInDay + openMinutes;
164+
let end: number;
165+
166+
if (closeDay === undefined || closeMinutes === undefined) {
167+
end = start + minutesInDay;
168+
} else {
169+
end = closeDay * minutesInDay + closeMinutes;
170+
}
171+
172+
if (end <= start) {
173+
end += minutesInWeek;
174+
}
175+
176+
let comparableLocalTime = localTimeValue;
177+
while (comparableLocalTime < start) {
178+
comparableLocalTime += minutesInWeek;
179+
}
180+
181+
if (comparableLocalTime >= start && comparableLocalTime < end) {
182+
return true;
183+
}
100184
}
101-
if (todayHours.toLowerCase().includes("24 hours")) {
102-
return true;
185+
186+
return false;
187+
}
188+
189+
private getLocalTimeComponents(utcOffsetMinutes?: number): { day: number; minutes: number } {
190+
const now = new Date();
191+
192+
if (typeof utcOffsetMinutes === "number" && Number.isFinite(utcOffsetMinutes)) {
193+
const localTime = new Date(now.getTime() + utcOffsetMinutes * 60000);
194+
195+
return {
196+
day: localTime.getUTCDay(),
197+
minutes: localTime.getUTCHours() * 60 + localTime.getUTCMinutes(),
198+
};
103199
}
104200

105-
return true;
201+
return {
202+
day: now.getDay(),
203+
minutes: now.getHours() * 60 + now.getMinutes(),
204+
};
106205
}
107206

108207
private formatOpeningHours(openingHours: any): string[] {
@@ -111,11 +210,11 @@ export class NewPlacesService {
111210

112211
private extractErrorMessage(error: any): string {
113212
const apiError = error?.message || error?.details || error?.status;
114-
213+
115214
if (apiError) {
116215
return `${apiError}`;
117216
}
118217

119218
return error instanceof Error ? error.message : String(error);
120219
}
121-
}
220+
}

src/services/PlacesSearcher.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ export class PlacesSearcher {
122122
phone: details.formatted_phone_number,
123123
website: details.website,
124124
price_level: details.price_level,
125-
reviews: details.reviews?.map((review) => ({
125+
reviews: details.reviews?.map((review: any) => ({
126126
rating: review.rating,
127127
text: review.text,
128128
time: review.time,

test-new-api.js

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#!/usr/bin/env node
22

3-
import { PlacesSearcher } from './dist/services/PlacesSearcher.js';
3+
import { PlacesSearcher, NewPlacesService } from './dist/index.js';
44

55
async function testNewPlacesAPI() {
66
console.log('Testing new Places API implementation...');
@@ -15,25 +15,46 @@ async function testNewPlacesAPI() {
1515

1616
try {
1717
const placesSearcher = new PlacesSearcher(apiKey);
18+
const placesService = new NewPlacesService(apiKey);
1819

1920
const testPlaceId = 'ChIJQ2BmJhVwhlQRPkt6FWiet90';
2021
console.log(`Testing with place ID: ${testPlaceId}`);
2122

23+
const rawDetails = await placesService.getPlaceDetails(testPlaceId);
24+
if (rawDetails.place_id !== testPlaceId) {
25+
console.error('❌ Legacy place_id mismatch');
26+
console.error(`Expected: ${testPlaceId}`);
27+
console.error(`Received: ${rawDetails.place_id}`);
28+
process.exit(1);
29+
} else {
30+
console.log('✅ Legacy place_id preserved');
31+
}
32+
33+
if (rawDetails.opening_hours?.open_now !== undefined) {
34+
console.log(`ℹ️ open_now flag from NewPlacesService: ${rawDetails.opening_hours.open_now}`);
35+
} else {
36+
console.log('ℹ️ open_now flag unavailable for this place (no opening hours data).');
37+
}
38+
2239
const result = await placesSearcher.getPlaceDetails(testPlaceId);
2340

2441
if (result.success) {
25-
console.log('✅ Successfully retrieved place details using new Places API!');
42+
console.log('✅ Successfully retrieved place details using PlacesSearcher wrapper!');
2643
console.log('Place details:');
2744
console.log(JSON.stringify(result.data, null, 2));
2845
} else {
29-
console.error('❌ Failed to retrieve place details:');
46+
console.error('❌ Failed to retrieve place details via PlacesSearcher:');
3047
console.error(result.error);
3148
}
3249

3350
} catch (error) {
3451
console.error('❌ Error during test:');
3552
console.error(error.message);
53+
process.exit(1);
3654
}
3755
}
3856

39-
testNewPlacesAPI().catch(console.error);
57+
testNewPlacesAPI().catch((error) => {
58+
console.error('❌ Unhandled error during test:', error);
59+
process.exit(1);
60+
});

0 commit comments

Comments
 (0)