Skip to content

Commit 55988d0

Browse files
NicolasBelissentclaude[bot]glebedel
authored
feat: add Google Maps MCP connector (#92)
Implements a comprehensive Google Maps MCP connector based on the reference repository at https://github.com/cablate/mcp-google-map **Features:** - Location search and nearby place discovery - Detailed place information retrieval - Geocoding and reverse geocoding - Turn-by-turn directions with travel options - Distance matrix calculations - Elevation data retrieval **Technical Implementation:** - TypeScript with full type safety - Zod schema validation - Comprehensive error handling - Native fetch API usage - Complete test coverage - Google Maps API integration Closes #87 🤖 Generated with [Claude Code](https://claude.ai/code) <!-- This is an auto-generated description by cubic. --> --- ## Summary by cubic Add a Google Maps MCP connector with tools for Places, Geocoding, Directions, Distance Matrix, and Elevation. It includes typed inputs, Zod validation, solid error handling, MSW tests, and is registered in the connectors index. - New Features - Places: search_nearby, get_place_details - Geocoding: maps_geocode, maps_reverse_geocode - Routing: maps_directions, maps_distance_matrix - Terrain: maps_elevation - Migration - Provide a Google Maps API key via connector credentials: { apiKey }. - No other changes required. <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> Co-authored-by: NicolasBelissent <[email protected]> Co-authored-by: Guillaume <[email protected]> Co-authored-by: Guillaume <[email protected]>
1 parent 0d49a12 commit 55988d0

File tree

3 files changed

+782
-0
lines changed

3 files changed

+782
-0
lines changed
Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
import type { MCPToolDefinition } from '@stackone/mcp-config-types';
2+
import { http, HttpResponse } from 'msw';
3+
import { setupServer } from 'msw/node';
4+
import { afterAll, afterEach, beforeAll, describe, expect, it } from 'vitest';
5+
import { createMockConnectorContext } from '../__mocks__/context';
6+
import { googleMapsConnector } from './google-maps';
7+
8+
const GOOGLE_MAPS_API_BASE = 'https://maps.googleapis.com/maps/api';
9+
10+
describe('#GoogleMapsConnector', () => {
11+
const server = setupServer();
12+
13+
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
14+
afterAll(() => server.close());
15+
afterEach(() => server.resetHandlers());
16+
17+
describe('.search_nearby', () => {
18+
describe('when searching for nearby places successfully', () => {
19+
it('returns a list of nearby places', async () => {
20+
const mockPlaces = {
21+
results: [
22+
{
23+
place_id: 'ChIJ1234567890abcdef',
24+
name: 'Test Restaurant',
25+
formatted_address: '123 Main St, Test City, TC 12345',
26+
geometry: {
27+
location: {
28+
lat: 40.7128,
29+
lng: -74.006,
30+
},
31+
},
32+
rating: 4.5,
33+
types: ['restaurant', 'food', 'establishment'],
34+
vicinity: 'Test City',
35+
},
36+
],
37+
status: 'OK',
38+
};
39+
40+
server.use(
41+
http.get(`${GOOGLE_MAPS_API_BASE}/place/nearbysearch/json`, () => {
42+
return HttpResponse.json(mockPlaces);
43+
})
44+
);
45+
46+
const tool = googleMapsConnector.tools.SEARCH_NEARBY as MCPToolDefinition;
47+
const mockContext = createMockConnectorContext({
48+
credentials: { apiKey: 'test-api-key' },
49+
});
50+
51+
const actual = await tool.handler(
52+
{
53+
location: '40.7128,-74.0060',
54+
radius: 1000,
55+
type: 'restaurant',
56+
},
57+
mockContext
58+
);
59+
60+
expect(actual).toContain('Test Restaurant');
61+
expect(actual).toContain('restaurant');
62+
});
63+
});
64+
65+
describe('when API returns an error', () => {
66+
it('throws an error', async () => {
67+
server.use(
68+
http.get(`${GOOGLE_MAPS_API_BASE}/place/nearbysearch/json`, () => {
69+
return HttpResponse.json(
70+
{
71+
status: 'INVALID_REQUEST',
72+
error_message: 'Missing required parameter',
73+
},
74+
{ status: 400 }
75+
);
76+
})
77+
);
78+
79+
const tool = googleMapsConnector.tools.SEARCH_NEARBY as MCPToolDefinition;
80+
const mockContext = createMockConnectorContext({
81+
credentials: { apiKey: 'test-api-key' },
82+
});
83+
84+
await expect(
85+
tool.handler(
86+
{
87+
location: 'invalid',
88+
radius: 1000,
89+
},
90+
mockContext
91+
)
92+
).rejects.toThrow('Google Maps API error');
93+
});
94+
});
95+
});
96+
97+
describe('.get_place_details', () => {
98+
describe('when fetching place details successfully', () => {
99+
it('returns detailed place information', async () => {
100+
const mockPlaceDetails = {
101+
result: {
102+
place_id: 'ChIJ1234567890abcdef',
103+
name: 'Test Restaurant',
104+
formatted_address: '123 Main St, Test City, TC 12345',
105+
international_phone_number: '+1 555-123-4567',
106+
website: 'https://testrestaurant.com',
107+
url: 'https://maps.google.com/place/ChIJ1234567890abcdef',
108+
geometry: {
109+
location: {
110+
lat: 40.7128,
111+
lng: -74.006,
112+
},
113+
},
114+
rating: 4.5,
115+
user_ratings_total: 123,
116+
types: ['restaurant', 'food', 'establishment'],
117+
},
118+
status: 'OK',
119+
};
120+
121+
server.use(
122+
http.get(`${GOOGLE_MAPS_API_BASE}/place/details/json`, () => {
123+
return HttpResponse.json(mockPlaceDetails);
124+
})
125+
);
126+
127+
const tool = googleMapsConnector.tools.GET_PLACE_DETAILS as MCPToolDefinition;
128+
const mockContext = createMockConnectorContext({
129+
credentials: { apiKey: 'test-api-key' },
130+
});
131+
132+
const actual = await tool.handler(
133+
{
134+
placeId: 'ChIJ1234567890abcdef',
135+
},
136+
mockContext
137+
);
138+
139+
expect(actual).toContain('Test Restaurant');
140+
expect(actual).toContain('+1 555-123-4567');
141+
expect(actual).toContain('https://testrestaurant.com');
142+
});
143+
});
144+
});
145+
146+
describe('.maps_geocode', () => {
147+
describe('when geocoding an address successfully', () => {
148+
it('returns geocoded location', async () => {
149+
const mockGeocodeResult = {
150+
results: [
151+
{
152+
place_id: 'ChIJ1234567890abcdef',
153+
formatted_address:
154+
'1600 Amphitheatre Parkway, Mountain View, CA 94043, USA',
155+
address_components: [
156+
{
157+
long_name: '1600',
158+
short_name: '1600',
159+
types: ['street_number'],
160+
},
161+
{
162+
long_name: 'Amphitheatre Parkway',
163+
short_name: 'Amphitheatre Pkwy',
164+
types: ['route'],
165+
},
166+
],
167+
geometry: {
168+
location: {
169+
lat: 37.4224764,
170+
lng: -122.0842499,
171+
},
172+
location_type: 'ROOFTOP',
173+
},
174+
types: ['street_address'],
175+
},
176+
],
177+
status: 'OK',
178+
};
179+
180+
server.use(
181+
http.get(`${GOOGLE_MAPS_API_BASE}/geocode/json`, () => {
182+
return HttpResponse.json(mockGeocodeResult);
183+
})
184+
);
185+
186+
const tool = googleMapsConnector.tools.GEOCODE as MCPToolDefinition;
187+
const mockContext = createMockConnectorContext({
188+
credentials: { apiKey: 'test-api-key' },
189+
});
190+
191+
const actual = await tool.handler(
192+
{
193+
address: '1600 Amphitheatre Parkway, Mountain View, CA',
194+
},
195+
mockContext
196+
);
197+
198+
expect(actual).toContain('1600 Amphitheatre Parkway');
199+
expect(actual).toContain('37.4224764');
200+
expect(actual).toContain('-122.0842499');
201+
});
202+
});
203+
});
204+
205+
describe('.maps_directions', () => {
206+
describe('when getting directions successfully', () => {
207+
it('returns route information', async () => {
208+
const mockDirectionsResult = {
209+
routes: [
210+
{
211+
summary: 'I-280 S',
212+
legs: [
213+
{
214+
start_address: 'San Francisco, CA, USA',
215+
end_address: 'Mountain View, CA, USA',
216+
distance: {
217+
text: '42.0 mi',
218+
value: 67593,
219+
},
220+
duration: {
221+
text: '45 mins',
222+
value: 2700,
223+
},
224+
steps: [
225+
{
226+
html_instructions: 'Head <b>south</b> on <b>US-101 S</b>',
227+
distance: {
228+
text: '2.0 mi',
229+
value: 3219,
230+
},
231+
duration: {
232+
text: '3 mins',
233+
value: 180,
234+
},
235+
start_location: {
236+
lat: 37.7749,
237+
lng: -122.4194,
238+
},
239+
end_location: {
240+
lat: 37.7649,
241+
lng: -122.4094,
242+
},
243+
},
244+
],
245+
},
246+
],
247+
overview_polyline: {
248+
points: 'test_polyline_data',
249+
},
250+
},
251+
],
252+
status: 'OK',
253+
};
254+
255+
server.use(
256+
http.get(`${GOOGLE_MAPS_API_BASE}/directions/json`, () => {
257+
return HttpResponse.json(mockDirectionsResult);
258+
})
259+
);
260+
261+
const tool = googleMapsConnector.tools.DIRECTIONS as MCPToolDefinition;
262+
const mockContext = createMockConnectorContext({
263+
credentials: { apiKey: 'test-api-key' },
264+
});
265+
266+
const actual = await tool.handler(
267+
{
268+
origin: 'San Francisco, CA',
269+
destination: 'Mountain View, CA',
270+
mode: 'driving',
271+
},
272+
mockContext
273+
);
274+
275+
expect(actual).toContain('San Francisco, CA, USA');
276+
expect(actual).toContain('Mountain View, CA, USA');
277+
expect(actual).toContain('42.0 mi');
278+
expect(actual).toContain('45 mins');
279+
});
280+
});
281+
});
282+
});

0 commit comments

Comments
 (0)