mcp-google-api 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +17 -0
- package/CHANGELOG.md +397 -0
- package/LICENSE +674 -0
- package/README.md +316 -0
- package/dist/chunk-VCFXCLJF.js +1860 -0
- package/dist/chunk-VCFXCLJF.js.map +1 -0
- package/dist/cli.d.ts +162 -0
- package/dist/cli.js +3628 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +276 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/docs/CONFIGURATION.md +116 -0
- package/docs/TOOLS.md +75 -0
- package/package.json +75 -0
- package/src/resources/skills/README.md +45 -0
- package/src/resources/skills/eatspot-discovery/DEBUG.md +71 -0
- package/src/resources/skills/eatspot-discovery/SKILL.md +246 -0
- package/src/resources/skills/manifest.json +15 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,3628 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
Logger,
|
|
4
|
+
PlacesSearcher
|
|
5
|
+
} from "./chunk-VCFXCLJF.js";
|
|
6
|
+
|
|
7
|
+
// src/cli.ts
|
|
8
|
+
import { config as dotenvConfig2 } from "dotenv";
|
|
9
|
+
import { resolve as resolve5 } from "path";
|
|
10
|
+
import yargs from "yargs";
|
|
11
|
+
import { hideBin } from "yargs/helpers";
|
|
12
|
+
|
|
13
|
+
// src/utils/configResolver.ts
|
|
14
|
+
import { config as dotenvConfig } from "dotenv";
|
|
15
|
+
import { existsSync } from "fs";
|
|
16
|
+
var NODE_ENGINE_FLOOR = 24;
|
|
17
|
+
var DEFAULT_HTTP_HOST = "127.0.0.1";
|
|
18
|
+
var DEFAULT_HTTP_PORT = 3e3;
|
|
19
|
+
function resolveRuntimeConfig(env = process.env, overrides = {}) {
|
|
20
|
+
const envPort = env.MCP_SERVER_PORT?.trim() || void 0;
|
|
21
|
+
let port;
|
|
22
|
+
let portRaw;
|
|
23
|
+
let portSource;
|
|
24
|
+
if (overrides.port !== void 0) {
|
|
25
|
+
port = overrides.port;
|
|
26
|
+
portRaw = String(overrides.port);
|
|
27
|
+
portSource = "flag";
|
|
28
|
+
} else if (envPort !== void 0) {
|
|
29
|
+
const parsed = Number(envPort);
|
|
30
|
+
port = Number.isFinite(parsed) ? parsed : void 0;
|
|
31
|
+
portRaw = envPort;
|
|
32
|
+
portSource = "environment";
|
|
33
|
+
} else {
|
|
34
|
+
port = DEFAULT_HTTP_PORT;
|
|
35
|
+
portRaw = void 0;
|
|
36
|
+
portSource = "default";
|
|
37
|
+
}
|
|
38
|
+
const apiKey = overrides.apiKey ?? env.GOOGLE_MAPS_API_KEY;
|
|
39
|
+
const enabledToolsRaw = env.GOOGLE_MAPS_ENABLED_TOOLS?.trim() || void 0;
|
|
40
|
+
return {
|
|
41
|
+
apiKey,
|
|
42
|
+
apiKeySource: overrides.apiKey !== void 0 ? "flag" : apiKey ? "environment" : "none",
|
|
43
|
+
host: overrides.host ?? env.MCP_SERVER_HOST ?? DEFAULT_HTTP_HOST,
|
|
44
|
+
hostSource: overrides.host !== void 0 ? "flag" : env.MCP_SERVER_HOST ? "environment" : "default",
|
|
45
|
+
port,
|
|
46
|
+
portRaw,
|
|
47
|
+
portSource,
|
|
48
|
+
enabledToolsRaw,
|
|
49
|
+
enabledToolsSource: enabledToolsRaw ? "environment" : "default",
|
|
50
|
+
autoUpdateDisabled: overrides.autoUpdateDisabled === true,
|
|
51
|
+
envFile: overrides.envFile
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function maskSecret(value) {
|
|
55
|
+
if (!value) {
|
|
56
|
+
return "unset";
|
|
57
|
+
}
|
|
58
|
+
if (value.length <= 8) {
|
|
59
|
+
return "****";
|
|
60
|
+
}
|
|
61
|
+
return `${value.slice(0, 4)}****${value.slice(-4)}`;
|
|
62
|
+
}
|
|
63
|
+
function describeRuntimeConfig(config) {
|
|
64
|
+
return {
|
|
65
|
+
apiKey: { value: maskSecret(config.apiKey), source: config.apiKeySource },
|
|
66
|
+
httpHost: { value: config.host, source: config.hostSource },
|
|
67
|
+
httpPort: {
|
|
68
|
+
value: config.port ?? null,
|
|
69
|
+
raw: config.portRaw ?? null,
|
|
70
|
+
source: config.portSource
|
|
71
|
+
},
|
|
72
|
+
enabledTools: { value: config.enabledToolsRaw ?? "*", source: config.enabledToolsSource },
|
|
73
|
+
autoUpdateCheck: { enabled: !config.autoUpdateDisabled },
|
|
74
|
+
envFile: { value: config.envFile ?? null }
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function formatConfigDiagnostics(diagnostics) {
|
|
78
|
+
return [
|
|
79
|
+
`host=${diagnostics.httpHost.value} (${diagnostics.httpHost.source})`,
|
|
80
|
+
`port=${diagnostics.httpPort.value ?? "invalid"} (raw: ${diagnostics.httpPort.raw ?? "n/a"}, ${diagnostics.httpPort.source})`,
|
|
81
|
+
`apiKey=${diagnostics.apiKey.value} (${diagnostics.apiKey.source})`,
|
|
82
|
+
`enabledTools=${diagnostics.enabledTools.value} (${diagnostics.enabledTools.source})`,
|
|
83
|
+
`updateCheck=${diagnostics.autoUpdateCheck.enabled ? "enabled" : "disabled"}`
|
|
84
|
+
].join("; ");
|
|
85
|
+
}
|
|
86
|
+
function applyExplicitEnvFile(path) {
|
|
87
|
+
if (!existsSync(path)) {
|
|
88
|
+
return { ok: false, error: `env file not found: ${path}` };
|
|
89
|
+
}
|
|
90
|
+
dotenvConfig({ path });
|
|
91
|
+
return { ok: true };
|
|
92
|
+
}
|
|
93
|
+
function resolveEnabledToolNames(allToolNames, raw) {
|
|
94
|
+
const trimmed = raw?.trim();
|
|
95
|
+
if (!trimmed || trimmed === "*") {
|
|
96
|
+
return { names: [...allToolNames], matched: allToolNames.length, fallback: false, requested: [] };
|
|
97
|
+
}
|
|
98
|
+
const requested = trimmed.split(",").map((item) => item.trim()).filter(Boolean);
|
|
99
|
+
const enabled = new Set(requested);
|
|
100
|
+
const names = allToolNames.filter((name) => enabled.has(name));
|
|
101
|
+
if (names.length === 0) {
|
|
102
|
+
return { names: [...allToolNames], matched: 0, fallback: true, requested };
|
|
103
|
+
}
|
|
104
|
+
return { names, matched: names.length, fallback: false, requested };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// src/tools/maps/searchNearby.ts
|
|
108
|
+
import { z } from "zod";
|
|
109
|
+
|
|
110
|
+
// src/utils/requestContext.ts
|
|
111
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
112
|
+
|
|
113
|
+
// src/utils/apiKeyManager.ts
|
|
114
|
+
var ApiKeyManager = class _ApiKeyManager {
|
|
115
|
+
constructor() {
|
|
116
|
+
this.defaultApiKey = void 0;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* @brief Returns the process-wide singleton instance.
|
|
120
|
+
* @details Lazily constructs the manager; the default key starts unset and
|
|
121
|
+
* MUST be injected by the server-start path before serving requests.
|
|
122
|
+
* Complexity O(1).
|
|
123
|
+
* @return {ApiKeyManager} The singleton instance.
|
|
124
|
+
*/
|
|
125
|
+
static getInstance() {
|
|
126
|
+
if (!_ApiKeyManager.instance) {
|
|
127
|
+
_ApiKeyManager.instance = new _ApiKeyManager();
|
|
128
|
+
}
|
|
129
|
+
return _ApiKeyManager.instance;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* @brief Injects the startup default API key.
|
|
133
|
+
* @details Called by the server-start command with the key resolved from
|
|
134
|
+
* the persisted settings (REQ-142); undefined clears the default so
|
|
135
|
+
* requests without header/session keys resolve no key (REQ-018, DES-006).
|
|
136
|
+
* Pure in-memory mutation; no environment writes.
|
|
137
|
+
* @param key {string | undefined} Default key from the persisted settings; undefined clears it.
|
|
138
|
+
* @return {void}
|
|
139
|
+
* @satisfies DES-006, REQ-142
|
|
140
|
+
*/
|
|
141
|
+
setDefaultApiKey(key) {
|
|
142
|
+
this.defaultApiKey = key;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* @brief Resolves the effective API key for one request.
|
|
146
|
+
* @details Priority: `X-Google-Maps-API-Key` header, then `Authorization:
|
|
147
|
+
* Bearer` token, then the session key, then the injected default key
|
|
148
|
+
* (DES-005, REQ-016, REQ-017, REQ-018). Complexity O(1).
|
|
149
|
+
* @param req {Request | undefined} Incoming HTTP request; undefined skips header resolution.
|
|
150
|
+
* @param sessionApiKey {string | undefined} Session-scoped key; consulted before the default.
|
|
151
|
+
* @return {string | undefined} Effective key; undefined when nothing resolves.
|
|
152
|
+
* @satisfies DES-005, REQ-016, REQ-017, REQ-018
|
|
153
|
+
*/
|
|
154
|
+
getApiKey(req, sessionApiKey) {
|
|
155
|
+
if (req) {
|
|
156
|
+
const headerApiKey = req.headers["x-google-maps-api-key"];
|
|
157
|
+
if (headerApiKey) {
|
|
158
|
+
return headerApiKey;
|
|
159
|
+
}
|
|
160
|
+
const authHeader = req.headers["authorization"];
|
|
161
|
+
if (authHeader && authHeader.startsWith("Bearer ")) {
|
|
162
|
+
return authHeader.substring(7);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (sessionApiKey) {
|
|
166
|
+
return sessionApiKey;
|
|
167
|
+
}
|
|
168
|
+
return this.defaultApiKey;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* @brief Reports whether any API key is resolvable for a request.
|
|
172
|
+
* @details Delegates to `getApiKey` truthiness. Complexity O(1).
|
|
173
|
+
* @param req {Request | undefined} Incoming HTTP request.
|
|
174
|
+
* @param sessionApiKey {string | undefined} Session-scoped key.
|
|
175
|
+
* @return {boolean} True when a key resolves.
|
|
176
|
+
*/
|
|
177
|
+
hasApiKey(req, sessionApiKey) {
|
|
178
|
+
return !!this.getApiKey(req, sessionApiKey);
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* @brief Validates the API key format.
|
|
182
|
+
* @details Accepts 20-50 characters of `[A-Za-z0-9_-]` (REQ-020). Pure;
|
|
183
|
+
* complexity O(n) in key length.
|
|
184
|
+
* @param key {string} Candidate key.
|
|
185
|
+
* @return {boolean} True when the format is valid.
|
|
186
|
+
* @satisfies REQ-020
|
|
187
|
+
*/
|
|
188
|
+
isValidApiKeyFormat(key) {
|
|
189
|
+
return /^[A-Za-z0-9_-]{20,50}$/.test(key);
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
// src/utils/requestContext.ts
|
|
194
|
+
var requestContextStorage = new AsyncLocalStorage();
|
|
195
|
+
function getCurrentApiKey() {
|
|
196
|
+
const context = requestContextStorage.getStore();
|
|
197
|
+
return context?.apiKey || ApiKeyManager.getInstance().getApiKey();
|
|
198
|
+
}
|
|
199
|
+
function runWithContext(context, fn) {
|
|
200
|
+
return requestContextStorage.run(context, fn);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// src/tools/maps/searchNearby.ts
|
|
204
|
+
var NAME = "maps_search_nearby";
|
|
205
|
+
var DESCRIPTION = "Find places near a specific location by type (e.g., restaurants, cafes, hotels). Use when the user wants to discover what's around a given address or coordinates, such as 'find coffee shops near Times Square' or 'what hotels are near the airport'. Supports filtering by place type, search radius, minimum rating, and whether currently open.";
|
|
206
|
+
var SCHEMA = {
|
|
207
|
+
center: z.object({
|
|
208
|
+
value: z.string().describe("Address, landmark name, or coordinates (coordinate format: lat,lng)"),
|
|
209
|
+
isCoordinates: z.boolean().default(false).describe("Whether the value is coordinates")
|
|
210
|
+
}).describe("Search center point (e.g. value: 49.3268778,-123.0585982, isCoordinates: true)"),
|
|
211
|
+
keyword: z.string().optional().describe("Place type to search for (e.g., restaurant, cafe, hotel, gas_station, hospital)"),
|
|
212
|
+
radius: z.number().default(1e3).describe("Search radius in meters"),
|
|
213
|
+
openNow: z.boolean().default(false).describe("Only show places that are currently open"),
|
|
214
|
+
minRating: z.number().min(0).max(5).optional().describe("Minimum rating requirement (0-5)")
|
|
215
|
+
};
|
|
216
|
+
async function ACTION(params) {
|
|
217
|
+
try {
|
|
218
|
+
const apiKey = getCurrentApiKey();
|
|
219
|
+
const placesSearcher = new PlacesSearcher(apiKey);
|
|
220
|
+
const result = await placesSearcher.searchNearby(params);
|
|
221
|
+
if (!result.success) {
|
|
222
|
+
return {
|
|
223
|
+
content: [{ type: "text", text: result.error || "Search failed" }],
|
|
224
|
+
isError: true
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
return {
|
|
228
|
+
content: [
|
|
229
|
+
{
|
|
230
|
+
type: "text",
|
|
231
|
+
text: `location: ${JSON.stringify(result.location, null, 2)}
|
|
232
|
+
` + JSON.stringify(result.data, null, 2)
|
|
233
|
+
}
|
|
234
|
+
],
|
|
235
|
+
isError: false
|
|
236
|
+
};
|
|
237
|
+
} catch (error) {
|
|
238
|
+
const errorMessage = error instanceof Error ? error.message : JSON.stringify(error);
|
|
239
|
+
return {
|
|
240
|
+
isError: true,
|
|
241
|
+
content: [{ type: "text", text: `Error searching nearby places: ${errorMessage}` }]
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
var SearchNearby = {
|
|
246
|
+
NAME,
|
|
247
|
+
DESCRIPTION,
|
|
248
|
+
SCHEMA,
|
|
249
|
+
ACTION
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
// src/tools/maps/placeDetails.ts
|
|
253
|
+
import { z as z2 } from "zod";
|
|
254
|
+
var NAME2 = "maps_place_details";
|
|
255
|
+
var DESCRIPTION2 = "Get comprehensive details for a specific place using its Google Maps place_id. Use after search_nearby or maps_search_places to get reviews, phone, website, and hours. Includes priceRange (raw Money structure: startPrice/endPrice with currencyCode/units/nanos), businessStatus, types and both regular and current opening hours. Reviews, photos, and AI summaries include source/disclosure metadata that must accompany end-user display. Set maxPhotos (1-10) to include photo URLs; omit or set 0 for none. Set includeRaw to attach the original Places API payload (raw_place) for API-verified validation.";
|
|
256
|
+
var SCHEMA2 = {
|
|
257
|
+
placeId: z2.string().describe("Google Maps place ID"),
|
|
258
|
+
maxPhotos: z2.number().int().min(0).max(10).optional().describe("Number of photo URLs to include (0 = none, max 10). Omit to skip photos and save tokens."),
|
|
259
|
+
includeRaw: z2.boolean().optional().describe(
|
|
260
|
+
"Include the original Places API payload (raw_place) alongside the interpreted fields, for API-verified validation"
|
|
261
|
+
)
|
|
262
|
+
};
|
|
263
|
+
async function ACTION2(params) {
|
|
264
|
+
try {
|
|
265
|
+
const apiKey = getCurrentApiKey();
|
|
266
|
+
const placesSearcher = new PlacesSearcher(apiKey);
|
|
267
|
+
const result = await placesSearcher.getPlaceDetails(params.placeId, params.maxPhotos || 0, params.includeRaw || false);
|
|
268
|
+
if (!result.success) {
|
|
269
|
+
return {
|
|
270
|
+
content: [{ type: "text", text: result.error || "Failed to get place details" }],
|
|
271
|
+
isError: true
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
const payload = { ...result.data };
|
|
275
|
+
if (result.fieldMask) payload.field_mask = result.fieldMask;
|
|
276
|
+
return {
|
|
277
|
+
content: [
|
|
278
|
+
{
|
|
279
|
+
type: "text",
|
|
280
|
+
text: JSON.stringify(payload, null, 2)
|
|
281
|
+
}
|
|
282
|
+
],
|
|
283
|
+
isError: false
|
|
284
|
+
};
|
|
285
|
+
} catch (error) {
|
|
286
|
+
const errorMessage = error instanceof Error ? error.message : JSON.stringify(error);
|
|
287
|
+
return {
|
|
288
|
+
isError: true,
|
|
289
|
+
content: [{ type: "text", text: `Error getting place details: ${errorMessage}` }]
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
var PlaceDetails = {
|
|
294
|
+
NAME: NAME2,
|
|
295
|
+
DESCRIPTION: DESCRIPTION2,
|
|
296
|
+
SCHEMA: SCHEMA2,
|
|
297
|
+
ACTION: ACTION2
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
// src/tools/maps/geocode.ts
|
|
301
|
+
import { z as z3 } from "zod";
|
|
302
|
+
var NAME3 = "maps_geocode";
|
|
303
|
+
var DESCRIPTION3 = "Convert an address, city name, or landmark into GPS coordinates (latitude/longitude). Use when you need coordinates for a location described in text \u2014 for example, to provide a center point for search_nearby or a starting point for maps_directions.";
|
|
304
|
+
var SCHEMA3 = {
|
|
305
|
+
address: z3.string().describe("Address or place name to convert to coordinates")
|
|
306
|
+
};
|
|
307
|
+
async function ACTION3(params) {
|
|
308
|
+
try {
|
|
309
|
+
const apiKey = getCurrentApiKey();
|
|
310
|
+
const placesSearcher = new PlacesSearcher(apiKey);
|
|
311
|
+
const result = await placesSearcher.geocode(params.address);
|
|
312
|
+
if (!result.success) {
|
|
313
|
+
return {
|
|
314
|
+
content: [{ type: "text", text: result.error || "Failed to geocode address" }],
|
|
315
|
+
isError: true
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
return {
|
|
319
|
+
content: [
|
|
320
|
+
{
|
|
321
|
+
type: "text",
|
|
322
|
+
text: JSON.stringify(result.data, null, 2)
|
|
323
|
+
}
|
|
324
|
+
],
|
|
325
|
+
isError: false
|
|
326
|
+
};
|
|
327
|
+
} catch (error) {
|
|
328
|
+
const errorMessage = error instanceof Error ? error.message : JSON.stringify(error);
|
|
329
|
+
return {
|
|
330
|
+
isError: true,
|
|
331
|
+
content: [{ type: "text", text: `Error geocoding address: ${errorMessage}` }]
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
var Geocode = {
|
|
336
|
+
NAME: NAME3,
|
|
337
|
+
DESCRIPTION: DESCRIPTION3,
|
|
338
|
+
SCHEMA: SCHEMA3,
|
|
339
|
+
ACTION: ACTION3
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
// src/tools/maps/reverseGeocode.ts
|
|
343
|
+
import { z as z4 } from "zod";
|
|
344
|
+
var NAME4 = "maps_reverse_geocode";
|
|
345
|
+
var DESCRIPTION4 = "Convert GPS coordinates (latitude/longitude) into a human-readable street address. Use when you have coordinates from another tool's output or a user's shared location and need the actual address.";
|
|
346
|
+
var SCHEMA4 = {
|
|
347
|
+
latitude: z4.number().describe("Latitude coordinate"),
|
|
348
|
+
longitude: z4.number().describe("Longitude coordinate")
|
|
349
|
+
};
|
|
350
|
+
async function ACTION4(params) {
|
|
351
|
+
try {
|
|
352
|
+
const apiKey = getCurrentApiKey();
|
|
353
|
+
const placesSearcher = new PlacesSearcher(apiKey);
|
|
354
|
+
const result = await placesSearcher.reverseGeocode(params.latitude, params.longitude);
|
|
355
|
+
if (!result.success) {
|
|
356
|
+
return {
|
|
357
|
+
content: [{ type: "text", text: result.error || "Failed to reverse geocode coordinates" }],
|
|
358
|
+
isError: true
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
return {
|
|
362
|
+
content: [
|
|
363
|
+
{
|
|
364
|
+
type: "text",
|
|
365
|
+
text: JSON.stringify(result.data, null, 2)
|
|
366
|
+
}
|
|
367
|
+
],
|
|
368
|
+
isError: false
|
|
369
|
+
};
|
|
370
|
+
} catch (error) {
|
|
371
|
+
const errorMessage = error instanceof Error ? error.message : JSON.stringify(error);
|
|
372
|
+
return {
|
|
373
|
+
isError: true,
|
|
374
|
+
content: [{ type: "text", text: `Error reverse geocoding: ${errorMessage}` }]
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
var ReverseGeocode = {
|
|
379
|
+
NAME: NAME4,
|
|
380
|
+
DESCRIPTION: DESCRIPTION4,
|
|
381
|
+
SCHEMA: SCHEMA4,
|
|
382
|
+
ACTION: ACTION4
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
// src/tools/maps/distanceMatrix.ts
|
|
386
|
+
import { z as z5 } from "zod";
|
|
387
|
+
var NAME5 = "maps_distance_matrix";
|
|
388
|
+
var DESCRIPTION5 = "Calculate travel distances and durations between multiple origins and destinations in a single request. Use for comparing travel options \u2014 e.g., 'which hotel is closest to the office?' or batch distance calculations. Supports driving, walking, bicycling, and transit modes.";
|
|
389
|
+
var SCHEMA5 = {
|
|
390
|
+
origins: z5.array(z5.string()).describe("List of origin addresses or coordinates"),
|
|
391
|
+
destinations: z5.array(z5.string()).describe("List of destination addresses or coordinates"),
|
|
392
|
+
mode: z5.enum(["driving", "walking", "bicycling", "transit"]).default("driving").describe("Travel mode for calculation"),
|
|
393
|
+
departure_time: z5.string().optional().describe(
|
|
394
|
+
"Departure time in ISO 8601 format (e.g. 2026-03-21T09:00:00Z). Enables traffic-aware duration estimates."
|
|
395
|
+
),
|
|
396
|
+
avoid_tolls: z5.boolean().optional().describe('Avoid toll roads where reasonable. Only supported with mode "driving".'),
|
|
397
|
+
avoid_highways: z5.boolean().optional().describe('Avoid highways where reasonable. Only supported with mode "driving".')
|
|
398
|
+
};
|
|
399
|
+
async function ACTION5(params) {
|
|
400
|
+
try {
|
|
401
|
+
const apiKey = getCurrentApiKey();
|
|
402
|
+
const placesSearcher = new PlacesSearcher(apiKey);
|
|
403
|
+
const result = await placesSearcher.calculateDistanceMatrix(
|
|
404
|
+
params.origins,
|
|
405
|
+
params.destinations,
|
|
406
|
+
params.mode,
|
|
407
|
+
params.departure_time,
|
|
408
|
+
params.avoid_tolls,
|
|
409
|
+
params.avoid_highways
|
|
410
|
+
);
|
|
411
|
+
if (!result.success) {
|
|
412
|
+
return {
|
|
413
|
+
content: [{ type: "text", text: result.error || "Failed to calculate distance matrix" }],
|
|
414
|
+
isError: true
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
return {
|
|
418
|
+
content: [
|
|
419
|
+
{
|
|
420
|
+
type: "text",
|
|
421
|
+
text: JSON.stringify(result.data, null, 2)
|
|
422
|
+
}
|
|
423
|
+
],
|
|
424
|
+
isError: false
|
|
425
|
+
};
|
|
426
|
+
} catch (error) {
|
|
427
|
+
const errorMessage = error instanceof Error ? error.message : JSON.stringify(error);
|
|
428
|
+
return {
|
|
429
|
+
isError: true,
|
|
430
|
+
content: [{ type: "text", text: `Error calculating distance matrix: ${errorMessage}` }]
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
var DistanceMatrix = {
|
|
435
|
+
NAME: NAME5,
|
|
436
|
+
DESCRIPTION: DESCRIPTION5,
|
|
437
|
+
SCHEMA: SCHEMA5,
|
|
438
|
+
ACTION: ACTION5
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
// src/tools/maps/directions.ts
|
|
442
|
+
import { z as z6 } from "zod";
|
|
443
|
+
var NAME6 = "maps_directions";
|
|
444
|
+
var DESCRIPTION6 = "Get step-by-step navigation directions between two points with route details. Use when the user asks 'how do I get from A to B?' and needs the route summary, total distance, estimated travel time, or turn-by-turn instructions. Supports departure/arrival times and multiple travel modes.";
|
|
445
|
+
var SCHEMA6 = {
|
|
446
|
+
origin: z6.string().describe("Starting point address or coordinates"),
|
|
447
|
+
destination: z6.string().describe("Destination address or coordinates"),
|
|
448
|
+
mode: z6.enum(["driving", "walking", "bicycling", "transit"]).default("driving").describe("Travel mode for directions"),
|
|
449
|
+
departure_time: z6.string().optional().describe("Departure time (ISO string format)"),
|
|
450
|
+
arrival_time: z6.string().optional().describe("Arrival time (ISO string format)"),
|
|
451
|
+
avoid_tolls: z6.boolean().optional().describe('Avoid toll roads where reasonable. Only supported with mode "driving".'),
|
|
452
|
+
avoid_highways: z6.boolean().optional().describe('Avoid highways where reasonable. Only supported with mode "driving".')
|
|
453
|
+
};
|
|
454
|
+
async function ACTION6(params) {
|
|
455
|
+
try {
|
|
456
|
+
const apiKey = getCurrentApiKey();
|
|
457
|
+
const placesSearcher = new PlacesSearcher(apiKey);
|
|
458
|
+
const result = await placesSearcher.getDirections(
|
|
459
|
+
params.origin,
|
|
460
|
+
params.destination,
|
|
461
|
+
params.mode,
|
|
462
|
+
params.departure_time,
|
|
463
|
+
params.arrival_time,
|
|
464
|
+
params.avoid_tolls,
|
|
465
|
+
params.avoid_highways
|
|
466
|
+
);
|
|
467
|
+
if (!result.success) {
|
|
468
|
+
return {
|
|
469
|
+
content: [{ type: "text", text: result.error || "Failed to get directions" }],
|
|
470
|
+
isError: true
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
return {
|
|
474
|
+
content: [
|
|
475
|
+
{
|
|
476
|
+
type: "text",
|
|
477
|
+
text: JSON.stringify(result.data, null, 2)
|
|
478
|
+
}
|
|
479
|
+
],
|
|
480
|
+
isError: false
|
|
481
|
+
};
|
|
482
|
+
} catch (error) {
|
|
483
|
+
const errorMessage = error instanceof Error ? error.message : JSON.stringify(error);
|
|
484
|
+
return {
|
|
485
|
+
isError: true,
|
|
486
|
+
content: [{ type: "text", text: `Error getting directions: ${errorMessage}` }]
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
var Directions = {
|
|
491
|
+
NAME: NAME6,
|
|
492
|
+
DESCRIPTION: DESCRIPTION6,
|
|
493
|
+
SCHEMA: SCHEMA6,
|
|
494
|
+
ACTION: ACTION6
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
// src/tools/maps/elevation.ts
|
|
498
|
+
import { z as z7 } from "zod";
|
|
499
|
+
var NAME7 = "maps_elevation";
|
|
500
|
+
var DESCRIPTION7 = "Get elevation (meters above sea level) for geographic coordinates. Use when the user asks 'how high is this place', 'is this area flood-prone', or needs altitude for hiking/cycling route profiles. Also useful for real estate risk assessment \u2014 low elevation near water suggests flood risk.";
|
|
501
|
+
var SCHEMA7 = {
|
|
502
|
+
locations: z7.array(
|
|
503
|
+
z7.object({
|
|
504
|
+
latitude: z7.number().describe("Latitude coordinate"),
|
|
505
|
+
longitude: z7.number().describe("Longitude coordinate")
|
|
506
|
+
})
|
|
507
|
+
).describe("List of locations to get elevation data for")
|
|
508
|
+
};
|
|
509
|
+
async function ACTION7(params) {
|
|
510
|
+
try {
|
|
511
|
+
const apiKey = getCurrentApiKey();
|
|
512
|
+
const placesSearcher = new PlacesSearcher(apiKey);
|
|
513
|
+
const result = await placesSearcher.getElevation(params.locations);
|
|
514
|
+
if (!result.success) {
|
|
515
|
+
return {
|
|
516
|
+
content: [{ type: "text", text: result.error || "Failed to get elevation data" }],
|
|
517
|
+
isError: true
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
return {
|
|
521
|
+
content: [
|
|
522
|
+
{
|
|
523
|
+
type: "text",
|
|
524
|
+
text: JSON.stringify(result.data, null, 2)
|
|
525
|
+
}
|
|
526
|
+
],
|
|
527
|
+
isError: false
|
|
528
|
+
};
|
|
529
|
+
} catch (error) {
|
|
530
|
+
const errorMessage = error instanceof Error ? error.message : JSON.stringify(error);
|
|
531
|
+
return {
|
|
532
|
+
isError: true,
|
|
533
|
+
content: [{ type: "text", text: `Error getting elevation data: ${errorMessage}` }]
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
var Elevation = {
|
|
538
|
+
NAME: NAME7,
|
|
539
|
+
DESCRIPTION: DESCRIPTION7,
|
|
540
|
+
SCHEMA: SCHEMA7,
|
|
541
|
+
ACTION: ACTION7
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
// src/tools/maps/searchPlaces.ts
|
|
545
|
+
import { z as z8 } from "zod";
|
|
546
|
+
var NAME8 = "maps_search_places";
|
|
547
|
+
var DESCRIPTION8 = "Search for places using a free-text query like 'sushi restaurants in Tokyo' or 'best coffee shops near Central Park'. More flexible than search_nearby \u2014 supports natural language queries, optional location bias, server-side rating and cumulative price-level filters (Places API Text Search priceLevels enum), strict type filtering, and open-now filtering. Returns place details including priceRange (raw Money structure), types, businessStatus, rating, userRatingCount and opening hours. Supports pageSize/pageToken pagination (up to 20 per page) and an optional includeRaw mode that attaches the original Places API payload for API-verified validation.";
|
|
548
|
+
var PRICE_LEVELS = [
|
|
549
|
+
"PRICE_LEVEL_FREE",
|
|
550
|
+
"PRICE_LEVEL_INEXPENSIVE",
|
|
551
|
+
"PRICE_LEVEL_MODERATE",
|
|
552
|
+
"PRICE_LEVEL_EXPENSIVE",
|
|
553
|
+
"PRICE_LEVEL_VERY_EXPENSIVE"
|
|
554
|
+
];
|
|
555
|
+
var SCHEMA8 = {
|
|
556
|
+
query: z8.string().describe("Text search query (e.g., 'Italian restaurants in Manhattan', 'hotels near Taipei 101')"),
|
|
557
|
+
locationBias: z8.object({
|
|
558
|
+
latitude: z8.number().describe("Latitude to bias results toward"),
|
|
559
|
+
longitude: z8.number().describe("Longitude to bias results toward"),
|
|
560
|
+
radius: z8.number().optional().describe("Bias radius in meters (default: 5000)")
|
|
561
|
+
}).optional().describe("Optional location to bias results toward"),
|
|
562
|
+
openNow: z8.boolean().optional().describe("Only return places that are currently open (server-side filter)"),
|
|
563
|
+
minRating: z8.number().optional().describe("Minimum rating filter (1.0 - 5.0), applied server-side by the API"),
|
|
564
|
+
includedType: z8.string().optional().describe(
|
|
565
|
+
"Filter by a single place type (e.g., restaurant, cafe, bakery). Text Search supports exactly one includedType per call"
|
|
566
|
+
),
|
|
567
|
+
strictTypeFiltering: z8.boolean().optional().describe(
|
|
568
|
+
"Strict type filtering: if true, only results of the same type as includedType are returned (default: loose interpretation)"
|
|
569
|
+
),
|
|
570
|
+
priceLevels: z8.array(z8.enum(PRICE_LEVELS)).optional().describe(
|
|
571
|
+
'Server-side economic filter. Pass ALL allowed levels cumulatively up to the budget cap, e.g. budget 30 EUR -> ["PRICE_LEVEL_INEXPENSIVE", "PRICE_LEVEL_MODERATE"]. Do not include PRICE_LEVEL_FREE'
|
|
572
|
+
),
|
|
573
|
+
pageSize: z8.number().int().min(1).max(20).optional().describe("Number of results per page (1-20). Use nextPageToken from the response to fetch further pages (up to 60 total)"),
|
|
574
|
+
pageToken: z8.string().optional().describe("nextPageToken returned by a previous maps_search_places call, to retrieve the next page of results"),
|
|
575
|
+
languageCode: z8.string().optional().describe("Language code for results (e.g., 'en', 'it', 'ja')"),
|
|
576
|
+
includeRaw: z8.boolean().optional().describe(
|
|
577
|
+
"Include the original Places API payload: each result gets a raw_place field and the full raw_response is returned (for API-verified validation)"
|
|
578
|
+
)
|
|
579
|
+
};
|
|
580
|
+
async function ACTION8(params) {
|
|
581
|
+
try {
|
|
582
|
+
const apiKey = getCurrentApiKey();
|
|
583
|
+
const placesSearcher = new PlacesSearcher(apiKey);
|
|
584
|
+
const result = await placesSearcher.searchText({
|
|
585
|
+
query: params.query,
|
|
586
|
+
locationBias: params.locationBias,
|
|
587
|
+
openNow: params.openNow,
|
|
588
|
+
minRating: params.minRating,
|
|
589
|
+
includedType: params.includedType,
|
|
590
|
+
strictTypeFiltering: params.strictTypeFiltering,
|
|
591
|
+
priceLevels: params.priceLevels,
|
|
592
|
+
pageSize: params.pageSize,
|
|
593
|
+
pageToken: params.pageToken,
|
|
594
|
+
languageCode: params.languageCode,
|
|
595
|
+
includeRaw: params.includeRaw
|
|
596
|
+
});
|
|
597
|
+
if (!result.success) {
|
|
598
|
+
return {
|
|
599
|
+
content: [{ type: "text", text: result.error || "Failed to search places" }],
|
|
600
|
+
isError: true
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
const payload = {
|
|
604
|
+
results: result.data
|
|
605
|
+
};
|
|
606
|
+
if (result.nextPageToken) payload.nextPageToken = result.nextPageToken;
|
|
607
|
+
if (result.fieldMask) payload.field_mask = result.fieldMask;
|
|
608
|
+
if (params.includeRaw && result.raw_response) payload.raw_response = result.raw_response;
|
|
609
|
+
return {
|
|
610
|
+
content: [
|
|
611
|
+
{
|
|
612
|
+
type: "text",
|
|
613
|
+
text: JSON.stringify(payload, null, 2)
|
|
614
|
+
}
|
|
615
|
+
],
|
|
616
|
+
isError: false
|
|
617
|
+
};
|
|
618
|
+
} catch (error) {
|
|
619
|
+
const errorMessage = error instanceof Error ? error.message : JSON.stringify(error);
|
|
620
|
+
return {
|
|
621
|
+
isError: true,
|
|
622
|
+
content: [{ type: "text", text: `Error searching places: ${errorMessage}` }]
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
var SearchPlaces = {
|
|
627
|
+
NAME: NAME8,
|
|
628
|
+
DESCRIPTION: DESCRIPTION8,
|
|
629
|
+
SCHEMA: SCHEMA8,
|
|
630
|
+
ACTION: ACTION8
|
|
631
|
+
};
|
|
632
|
+
|
|
633
|
+
// src/tools/maps/timezone.ts
|
|
634
|
+
import { z as z9 } from "zod";
|
|
635
|
+
var NAME9 = "maps_timezone";
|
|
636
|
+
var DESCRIPTION9 = "Get the timezone and current local time for a location. Use when the user asks 'what time is it in Tokyo', needs to coordinate a meeting across timezones, or is planning travel across timezone boundaries. Returns timezone ID, UTC/DST offsets, and computed local time.";
|
|
637
|
+
var SCHEMA9 = {
|
|
638
|
+
latitude: z9.number().describe("Latitude coordinate"),
|
|
639
|
+
longitude: z9.number().describe("Longitude coordinate"),
|
|
640
|
+
timestamp: z9.number().optional().describe("Unix timestamp in ms to query timezone at a specific moment (defaults to now)")
|
|
641
|
+
};
|
|
642
|
+
async function ACTION9(params) {
|
|
643
|
+
try {
|
|
644
|
+
const apiKey = getCurrentApiKey();
|
|
645
|
+
const placesSearcher = new PlacesSearcher(apiKey);
|
|
646
|
+
const result = await placesSearcher.getTimezone(params.latitude, params.longitude, params.timestamp);
|
|
647
|
+
if (!result.success) {
|
|
648
|
+
return {
|
|
649
|
+
content: [{ type: "text", text: result.error || "Failed to get timezone data" }],
|
|
650
|
+
isError: true
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
return {
|
|
654
|
+
content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }],
|
|
655
|
+
isError: false
|
|
656
|
+
};
|
|
657
|
+
} catch (error) {
|
|
658
|
+
const errorMessage = error instanceof Error ? error.message : JSON.stringify(error);
|
|
659
|
+
return {
|
|
660
|
+
isError: true,
|
|
661
|
+
content: [{ type: "text", text: `Error getting timezone: ${errorMessage}` }]
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
var Timezone = {
|
|
666
|
+
NAME: NAME9,
|
|
667
|
+
DESCRIPTION: DESCRIPTION9,
|
|
668
|
+
SCHEMA: SCHEMA9,
|
|
669
|
+
ACTION: ACTION9
|
|
670
|
+
};
|
|
671
|
+
|
|
672
|
+
// src/tools/maps/weather.ts
|
|
673
|
+
import { z as z10 } from "zod";
|
|
674
|
+
var NAME10 = "maps_weather";
|
|
675
|
+
var DESCRIPTION10 = "Get weather for a location \u2014 current conditions, daily forecast (10 days), or hourly forecast (240 hours). Use when the user asks 'what's the weather in Paris', is planning outdoor activities, or needs to pack for a trip. Coverage: most regions supported, but China, Japan, South Korea, Cuba, Iran, North Korea, Syria are unavailable.";
|
|
676
|
+
var SCHEMA10 = {
|
|
677
|
+
latitude: z10.number().describe("Latitude coordinate"),
|
|
678
|
+
longitude: z10.number().describe("Longitude coordinate"),
|
|
679
|
+
type: z10.enum(["current", "forecast_daily", "forecast_hourly"]).optional().describe("current = right now, forecast_daily = multi-day outlook, forecast_hourly = hour-by-hour"),
|
|
680
|
+
forecastDays: z10.number().optional().describe("Number of forecast days (1-10, only for forecast_daily, default: 5)"),
|
|
681
|
+
forecastHours: z10.number().optional().describe("Number of forecast hours (1-240, only for forecast_hourly, default: 24)")
|
|
682
|
+
};
|
|
683
|
+
async function ACTION10(params) {
|
|
684
|
+
try {
|
|
685
|
+
const apiKey = getCurrentApiKey();
|
|
686
|
+
const placesSearcher = new PlacesSearcher(apiKey);
|
|
687
|
+
const result = await placesSearcher.getWeather(
|
|
688
|
+
params.latitude,
|
|
689
|
+
params.longitude,
|
|
690
|
+
params.type || "current",
|
|
691
|
+
params.forecastDays,
|
|
692
|
+
params.forecastHours
|
|
693
|
+
);
|
|
694
|
+
if (!result.success) {
|
|
695
|
+
return {
|
|
696
|
+
content: [{ type: "text", text: result.error || "Failed to get weather data" }],
|
|
697
|
+
isError: true
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
return {
|
|
701
|
+
content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }],
|
|
702
|
+
isError: false
|
|
703
|
+
};
|
|
704
|
+
} catch (error) {
|
|
705
|
+
const errorMessage = error instanceof Error ? error.message : JSON.stringify(error);
|
|
706
|
+
return {
|
|
707
|
+
isError: true,
|
|
708
|
+
content: [{ type: "text", text: `Error getting weather: ${errorMessage}` }]
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
var Weather = {
|
|
713
|
+
NAME: NAME10,
|
|
714
|
+
DESCRIPTION: DESCRIPTION10,
|
|
715
|
+
SCHEMA: SCHEMA10,
|
|
716
|
+
ACTION: ACTION10
|
|
717
|
+
};
|
|
718
|
+
|
|
719
|
+
// src/tools/maps/exploreArea.ts
|
|
720
|
+
import { z as z11 } from "zod";
|
|
721
|
+
var NAME11 = "maps_explore_area";
|
|
722
|
+
var DESCRIPTION11 = "Explore what's around a location in one call \u2014 searches multiple place types, gets details for the top results, and returns a categorized summary. Use when the user asks 'what's around here', 'explore the area near my hotel', or needs a quick overview of a neighborhood. Replaces the manual chain of geocode \u2192 search-nearby \u2192 place-details. For trip planning: use search_places first to get geographically spread anchor points, then call this tool around each anchor (e.g. 'Gion, Kyoto') \u2014 never pass just the city name, as it clusters all results in one area. After results, call static_map to visualize.";
|
|
723
|
+
var SCHEMA11 = {
|
|
724
|
+
location: z11.string().describe("Address or landmark to explore around"),
|
|
725
|
+
types: z11.array(z11.string()).optional().describe(
|
|
726
|
+
"Place types to search (default: restaurant, cafe, tourist_attraction). Must be Places API (New) type names. Examples: hotel, bar, park, museum"
|
|
727
|
+
),
|
|
728
|
+
radius: z11.number().optional().describe("Search radius in meters (default: 1000)"),
|
|
729
|
+
topN: z11.number().optional().describe("Number of top results per type to get details for (default: 3)")
|
|
730
|
+
};
|
|
731
|
+
async function ACTION11(params) {
|
|
732
|
+
try {
|
|
733
|
+
const apiKey = getCurrentApiKey();
|
|
734
|
+
const searcher = new PlacesSearcher(apiKey);
|
|
735
|
+
const result = await searcher.exploreArea(params);
|
|
736
|
+
return {
|
|
737
|
+
content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }],
|
|
738
|
+
isError: false
|
|
739
|
+
};
|
|
740
|
+
} catch (error) {
|
|
741
|
+
return {
|
|
742
|
+
isError: true,
|
|
743
|
+
content: [{ type: "text", text: `Error exploring area: ${error.message}` }]
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
var ExploreArea = { NAME: NAME11, DESCRIPTION: DESCRIPTION11, SCHEMA: SCHEMA11, ACTION: ACTION11 };
|
|
748
|
+
|
|
749
|
+
// src/tools/maps/planRoute.ts
|
|
750
|
+
import { z as z12 } from "zod";
|
|
751
|
+
var NAME12 = "maps_plan_route";
|
|
752
|
+
var DESCRIPTION12 = "Plan an optimized multi-stop route in one call \u2014 geocodes all stops, uses Routes API waypoint optimization (2 to 25 intermediate stops) to find the most efficient visit order, and returns directions for each leg. Use when the user says 'visit these 5 places efficiently', 'plan a route through A, B, C', or needs a multi-stop itinerary. Replaces the manual chain of geocode \u2192 distance-matrix \u2192 directions. Waypoint optimization requires at least 4 stops (2 intermediates); with 2 or 3 stops the route is returned in the original order. For multi-day trips: create one plan_route call per day with stops that follow a geographic arc (e.g. east\u2192west) rather than mixing distant areas. After results, call static_map to visualize the route.";
|
|
753
|
+
var SCHEMA12 = {
|
|
754
|
+
stops: z12.array(z12.string()).min(2).describe("List of addresses or landmarks to visit (minimum 2)"),
|
|
755
|
+
mode: z12.enum(["driving", "walking", "bicycling", "transit"]).optional().describe("Travel mode (default: driving)"),
|
|
756
|
+
optimize: z12.boolean().optional().describe(
|
|
757
|
+
"Auto-optimize visit order via Routes API waypoint optimization (default: true). Requires at least 4 stops (2 intermediates) \u2014 ignored for 2-3 stops. Set false to keep original order. Not available for transit mode."
|
|
758
|
+
),
|
|
759
|
+
departure_time: z12.string().optional().describe("Departure time in ISO 8601 format (e.g. 2026-03-21T09:00:00Z). Enables traffic-aware routing."),
|
|
760
|
+
avoid_tolls: z12.boolean().optional().describe('Avoid toll roads where reasonable. Only supported with mode "driving".'),
|
|
761
|
+
avoid_highways: z12.boolean().optional().describe('Avoid highways where reasonable. Only supported with mode "driving".')
|
|
762
|
+
};
|
|
763
|
+
async function ACTION12(params) {
|
|
764
|
+
try {
|
|
765
|
+
const apiKey = getCurrentApiKey();
|
|
766
|
+
const searcher = new PlacesSearcher(apiKey);
|
|
767
|
+
const result = await searcher.planRoute(params);
|
|
768
|
+
return {
|
|
769
|
+
content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }],
|
|
770
|
+
isError: false
|
|
771
|
+
};
|
|
772
|
+
} catch (error) {
|
|
773
|
+
return {
|
|
774
|
+
isError: true,
|
|
775
|
+
content: [{ type: "text", text: `Error planning route: ${error.message}` }]
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
var PlanRoute = { NAME: NAME12, DESCRIPTION: DESCRIPTION12, SCHEMA: SCHEMA12, ACTION: ACTION12 };
|
|
780
|
+
|
|
781
|
+
// src/tools/maps/comparePlaces.ts
|
|
782
|
+
import { z as z13 } from "zod";
|
|
783
|
+
var NAME13 = "maps_compare_places";
|
|
784
|
+
var DESCRIPTION13 = "Compare multiple places side-by-side in one call \u2014 searches by query, gets details for each result, and optionally calculates distance from your location. Use when the user asks 'which restaurant should I pick', 'compare these hotels', or needs a decision table. Replaces the manual chain of search-places \u2192 place-details \u2192 distance-matrix.";
|
|
785
|
+
var SCHEMA13 = {
|
|
786
|
+
query: z13.string().describe("Search query (e.g., 'ramen near Shibuya', 'hotels in Taipei')"),
|
|
787
|
+
userLocation: z13.object({
|
|
788
|
+
latitude: z13.number().describe("Your latitude"),
|
|
789
|
+
longitude: z13.number().describe("Your longitude")
|
|
790
|
+
}).optional().describe("Your current location \u2014 if provided, adds distance and drive time to each result"),
|
|
791
|
+
limit: z13.number().optional().describe("Max places to compare (default: 5)")
|
|
792
|
+
};
|
|
793
|
+
async function ACTION13(params) {
|
|
794
|
+
try {
|
|
795
|
+
const apiKey = getCurrentApiKey();
|
|
796
|
+
const searcher = new PlacesSearcher(apiKey);
|
|
797
|
+
const result = await searcher.comparePlaces(params);
|
|
798
|
+
return {
|
|
799
|
+
content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }],
|
|
800
|
+
isError: false
|
|
801
|
+
};
|
|
802
|
+
} catch (error) {
|
|
803
|
+
return {
|
|
804
|
+
isError: true,
|
|
805
|
+
content: [{ type: "text", text: `Error comparing places: ${error.message}` }]
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
var ComparePlaces = { NAME: NAME13, DESCRIPTION: DESCRIPTION13, SCHEMA: SCHEMA13, ACTION: ACTION13 };
|
|
810
|
+
|
|
811
|
+
// src/tools/maps/airQuality.ts
|
|
812
|
+
import { z as z14 } from "zod";
|
|
813
|
+
var NAME14 = "maps_air_quality";
|
|
814
|
+
var DESCRIPTION14 = "Get air quality for a location \u2014 AQI index, pollutant concentrations, and health recommendations by demographic group (elderly, children, athletes, pregnant women, etc.). Use when the user asks 'is the air safe', 'should I wear a mask', 'good for outdoor exercise', or is planning travel for someone with respiratory/heart conditions. Coverage: global including Japan (unlike weather). Returns both universal AQI and local index (EPA for US, AEROS for Japan, etc.).";
|
|
815
|
+
var SCHEMA14 = {
|
|
816
|
+
latitude: z14.number().describe("Latitude coordinate"),
|
|
817
|
+
longitude: z14.number().describe("Longitude coordinate"),
|
|
818
|
+
includeHealthRecommendations: z14.boolean().optional().describe("Include health advice per demographic group (default: true)"),
|
|
819
|
+
includePollutants: z14.boolean().optional().describe("Include individual pollutant concentrations \u2014 PM2.5, PM10, NO2, O3, CO, SO2 (default: false)")
|
|
820
|
+
};
|
|
821
|
+
async function ACTION14(params) {
|
|
822
|
+
try {
|
|
823
|
+
const apiKey = getCurrentApiKey();
|
|
824
|
+
const placesSearcher = new PlacesSearcher(apiKey);
|
|
825
|
+
const result = await placesSearcher.getAirQuality(
|
|
826
|
+
params.latitude,
|
|
827
|
+
params.longitude,
|
|
828
|
+
params.includeHealthRecommendations,
|
|
829
|
+
params.includePollutants
|
|
830
|
+
);
|
|
831
|
+
if (!result.success) {
|
|
832
|
+
return {
|
|
833
|
+
content: [{ type: "text", text: result.error || "Failed to get air quality data" }],
|
|
834
|
+
isError: true
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
return {
|
|
838
|
+
content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }],
|
|
839
|
+
isError: false
|
|
840
|
+
};
|
|
841
|
+
} catch (error) {
|
|
842
|
+
const errorMessage = error instanceof Error ? error.message : JSON.stringify(error);
|
|
843
|
+
return {
|
|
844
|
+
isError: true,
|
|
845
|
+
content: [{ type: "text", text: `Error getting air quality: ${errorMessage}` }]
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
var AirQuality = {
|
|
850
|
+
NAME: NAME14,
|
|
851
|
+
DESCRIPTION: DESCRIPTION14,
|
|
852
|
+
SCHEMA: SCHEMA14,
|
|
853
|
+
ACTION: ACTION14
|
|
854
|
+
};
|
|
855
|
+
|
|
856
|
+
// src/tools/maps/staticMap.ts
|
|
857
|
+
import { z as z15 } from "zod";
|
|
858
|
+
var NAME15 = "maps_static_map";
|
|
859
|
+
var DESCRIPTION15 = "Generate a map image with markers, paths, or routes \u2014 returned as an inline image the user can see directly in chat. PROACTIVELY call this tool after explore_area, plan_route, search_nearby, or directions to visualize results on a map \u2014 don't wait for the user to ask. Use markers from search results and path from route data. Supports roadmap, satellite, terrain, and hybrid views. Max 640x640 pixels.";
|
|
860
|
+
var SCHEMA15 = {
|
|
861
|
+
center: z15.string().optional().describe('Map center \u2014 "lat,lng" or address. Optional if markers or path are provided.'),
|
|
862
|
+
zoom: z15.number().optional().describe("Zoom level 0-21 (0 = world, 15 = streets, 21 = buildings). Default: auto-fit."),
|
|
863
|
+
size: z15.string().optional().describe('Image size "WxH" in pixels. Default: "600x400". Max: "640x640".'),
|
|
864
|
+
maptype: z15.enum(["roadmap", "satellite", "terrain", "hybrid"]).optional().describe("Map style. Default: roadmap."),
|
|
865
|
+
markers: z15.array(z15.string()).optional().describe(
|
|
866
|
+
'Marker descriptors. Each string: "color:red|label:A|lat,lng" or "color:blue|address". Multiple markers per string separated by |.'
|
|
867
|
+
),
|
|
868
|
+
path: z15.array(z15.string()).optional().describe(
|
|
869
|
+
'Path descriptors. Each string: "color:0x0000ff|weight:3|lat1,lng1|lat2,lng2|..." to draw lines/routes on the map.'
|
|
870
|
+
)
|
|
871
|
+
};
|
|
872
|
+
async function ACTION15(params) {
|
|
873
|
+
try {
|
|
874
|
+
const apiKey = getCurrentApiKey();
|
|
875
|
+
const placesSearcher = new PlacesSearcher(apiKey);
|
|
876
|
+
const result = await placesSearcher.getStaticMap(params);
|
|
877
|
+
if (!result.success) {
|
|
878
|
+
return {
|
|
879
|
+
content: [{ type: "text", text: result.error || "Failed to generate static map" }],
|
|
880
|
+
isError: true
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
return {
|
|
884
|
+
content: [
|
|
885
|
+
{
|
|
886
|
+
type: "image",
|
|
887
|
+
data: result.data.base64,
|
|
888
|
+
mimeType: "image/png"
|
|
889
|
+
},
|
|
890
|
+
{
|
|
891
|
+
type: "text",
|
|
892
|
+
text: `Map generated (${result.data.size} bytes, ${result.data.dimensions})`
|
|
893
|
+
}
|
|
894
|
+
],
|
|
895
|
+
isError: false
|
|
896
|
+
};
|
|
897
|
+
} catch (error) {
|
|
898
|
+
const errorMessage = error instanceof Error ? error.message : JSON.stringify(error);
|
|
899
|
+
return {
|
|
900
|
+
isError: true,
|
|
901
|
+
content: [{ type: "text", text: `Error generating static map: ${errorMessage}` }]
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
var StaticMap = {
|
|
906
|
+
NAME: NAME15,
|
|
907
|
+
DESCRIPTION: DESCRIPTION15,
|
|
908
|
+
SCHEMA: SCHEMA15,
|
|
909
|
+
ACTION: ACTION15
|
|
910
|
+
};
|
|
911
|
+
|
|
912
|
+
// src/tools/maps/batchGeocode.ts
|
|
913
|
+
import { z as z16 } from "zod";
|
|
914
|
+
var NAME16 = "maps_batch_geocode";
|
|
915
|
+
var DESCRIPTION16 = "Geocode multiple addresses in one call \u2014 up to 50 addresses, returns coordinates for each. Use when the user provides a list of addresses and needs all their coordinates, e.g. 'geocode these 10 offices' or 'get coordinates for all these restaurants'. For more than 50, use the CLI batch-geocode command instead.";
|
|
916
|
+
var SCHEMA16 = {
|
|
917
|
+
addresses: z16.array(z16.string()).min(1).max(50).describe("List of addresses or landmark names to geocode (max 50)")
|
|
918
|
+
};
|
|
919
|
+
async function ACTION16(params) {
|
|
920
|
+
try {
|
|
921
|
+
const apiKey = getCurrentApiKey();
|
|
922
|
+
const searcher = new PlacesSearcher(apiKey);
|
|
923
|
+
const addresses = params.addresses;
|
|
924
|
+
const results = await Promise.all(
|
|
925
|
+
addresses.map(async (address) => {
|
|
926
|
+
try {
|
|
927
|
+
const result = await searcher.geocode(address);
|
|
928
|
+
return { address, ...result };
|
|
929
|
+
} catch (error) {
|
|
930
|
+
return { address, success: false, error: error.message };
|
|
931
|
+
}
|
|
932
|
+
})
|
|
933
|
+
);
|
|
934
|
+
const succeeded = results.filter((r) => r.success).length;
|
|
935
|
+
const failed = results.filter((r) => !r.success).length;
|
|
936
|
+
return {
|
|
937
|
+
content: [
|
|
938
|
+
{
|
|
939
|
+
type: "text",
|
|
940
|
+
text: JSON.stringify({ total: addresses.length, succeeded, failed, results }, null, 2)
|
|
941
|
+
}
|
|
942
|
+
],
|
|
943
|
+
isError: false
|
|
944
|
+
};
|
|
945
|
+
} catch (error) {
|
|
946
|
+
const errorMessage = error instanceof Error ? error.message : JSON.stringify(error);
|
|
947
|
+
return {
|
|
948
|
+
isError: true,
|
|
949
|
+
content: [{ type: "text", text: `Error batch geocoding: ${errorMessage}` }]
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
var BatchGeocode = {
|
|
954
|
+
NAME: NAME16,
|
|
955
|
+
DESCRIPTION: DESCRIPTION16,
|
|
956
|
+
SCHEMA: SCHEMA16,
|
|
957
|
+
ACTION: ACTION16
|
|
958
|
+
};
|
|
959
|
+
|
|
960
|
+
// src/tools/maps/searchAlongRoute.ts
|
|
961
|
+
import { z as z17 } from "zod";
|
|
962
|
+
var NAME17 = "maps_search_along_route";
|
|
963
|
+
var DESCRIPTION17 = "Search for places along a route between two points \u2014 restaurants, cafes, gas stations, etc. ranked by minimal detour time. Use for trip planning to find meals, rest stops, or attractions between landmarks without backtracking. Internally computes the route, then searches along it. Essential for building itineraries where stops should feel 'on the way' rather than 'detour to'.";
|
|
964
|
+
var SCHEMA17 = {
|
|
965
|
+
textQuery: z17.string().describe("What to search for along the route (e.g. 'restaurant', 'coffee shop', 'temple')"),
|
|
966
|
+
origin: z17.string().describe("Route start point \u2014 address or landmark name"),
|
|
967
|
+
destination: z17.string().describe("Route end point \u2014 address or landmark name"),
|
|
968
|
+
mode: z17.enum(["driving", "walking", "bicycling", "transit"]).optional().describe("Travel mode for the route (default: walking)"),
|
|
969
|
+
maxResults: z17.number().optional().describe("Max results to return (default: 5, max: 20)")
|
|
970
|
+
};
|
|
971
|
+
async function ACTION17(params) {
|
|
972
|
+
try {
|
|
973
|
+
const apiKey = getCurrentApiKey();
|
|
974
|
+
const searcher = new PlacesSearcher(apiKey);
|
|
975
|
+
const result = await searcher.searchAlongRoute(params);
|
|
976
|
+
if (!result.success) {
|
|
977
|
+
return {
|
|
978
|
+
content: [{ type: "text", text: result.error || "Failed to search along route" }],
|
|
979
|
+
isError: true
|
|
980
|
+
};
|
|
981
|
+
}
|
|
982
|
+
return {
|
|
983
|
+
content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }],
|
|
984
|
+
isError: false
|
|
985
|
+
};
|
|
986
|
+
} catch (error) {
|
|
987
|
+
const errorMessage = error instanceof Error ? error.message : JSON.stringify(error);
|
|
988
|
+
return {
|
|
989
|
+
isError: true,
|
|
990
|
+
content: [{ type: "text", text: `Error searching along route: ${errorMessage}` }]
|
|
991
|
+
};
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
var SearchAlongRoute = {
|
|
995
|
+
NAME: NAME17,
|
|
996
|
+
DESCRIPTION: DESCRIPTION17,
|
|
997
|
+
SCHEMA: SCHEMA17,
|
|
998
|
+
ACTION: ACTION17
|
|
999
|
+
};
|
|
1000
|
+
|
|
1001
|
+
// src/tools/maps/localRankTracker.ts
|
|
1002
|
+
import { z as z18 } from "zod";
|
|
1003
|
+
var NAME18 = "maps_local_rank_tracker";
|
|
1004
|
+
var DESCRIPTION18 = "Track a business's local search ranking across a geographic grid (like LocalFalcon). Searches the same keyword(s) from multiple coordinates around a center point to see how rank varies by location. Supports up to 3 keywords for batch scanning. Returns rank at each grid point, top-3 competitors per point, and summary metrics (ARP, ATRP, SoLV). Useful for local SEO analysis.";
|
|
1005
|
+
var SCHEMA18 = {
|
|
1006
|
+
keyword: z18.string().optional().describe("Single search keyword (e.g., 'dentist'). Use 'keywords' for multi-keyword scanning."),
|
|
1007
|
+
keywords: z18.array(z18.string()).min(1).max(3).optional().describe(
|
|
1008
|
+
"Array of 1-3 keywords to scan (e.g., ['dentist', 'dental clinic', 'teeth cleaning']). Overrides 'keyword'."
|
|
1009
|
+
),
|
|
1010
|
+
placeId: z18.string().describe("Google Maps place_id of the target business to track"),
|
|
1011
|
+
center: z18.object({
|
|
1012
|
+
latitude: z18.number().describe("Center latitude of the grid"),
|
|
1013
|
+
longitude: z18.number().describe("Center longitude of the grid")
|
|
1014
|
+
}).describe("Center coordinate for the grid (typically the business location)"),
|
|
1015
|
+
gridSize: z18.number().int().min(3).max(7).optional().describe("Grid dimension (3 = 3\xD73 = 9 points, 5 = 5\xD75 = 25 points, 7 = 7\xD77 = 49 points). Default: 3"),
|
|
1016
|
+
gridSpacing: z18.number().min(100).max(1e4).optional().describe("Distance between grid points in meters (100-10000). Default: 1000")
|
|
1017
|
+
};
|
|
1018
|
+
async function ACTION18(params) {
|
|
1019
|
+
try {
|
|
1020
|
+
const resolvedKeywords = params.keywords || (params.keyword ? [params.keyword] : []);
|
|
1021
|
+
if (resolvedKeywords.length === 0) {
|
|
1022
|
+
return {
|
|
1023
|
+
content: [{ type: "text", text: "Either 'keyword' or 'keywords' must be provided." }],
|
|
1024
|
+
isError: true
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
const apiKey = getCurrentApiKey();
|
|
1028
|
+
const placesSearcher = new PlacesSearcher(apiKey);
|
|
1029
|
+
const result = await placesSearcher.localRankTracker({ ...params, keywords: resolvedKeywords });
|
|
1030
|
+
if (!result.success) {
|
|
1031
|
+
return {
|
|
1032
|
+
content: [{ type: "text", text: result.error || "Failed to track local rank" }],
|
|
1033
|
+
isError: true
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
return {
|
|
1037
|
+
content: [
|
|
1038
|
+
{
|
|
1039
|
+
type: "text",
|
|
1040
|
+
text: JSON.stringify(result.data, null, 2)
|
|
1041
|
+
}
|
|
1042
|
+
],
|
|
1043
|
+
isError: false
|
|
1044
|
+
};
|
|
1045
|
+
} catch (error) {
|
|
1046
|
+
const errorMessage = error instanceof Error ? error.message : JSON.stringify(error);
|
|
1047
|
+
return {
|
|
1048
|
+
isError: true,
|
|
1049
|
+
content: [{ type: "text", text: `Error tracking local rank: ${errorMessage}` }]
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
var LocalRankTracker = {
|
|
1054
|
+
NAME: NAME18,
|
|
1055
|
+
DESCRIPTION: DESCRIPTION18,
|
|
1056
|
+
SCHEMA: SCHEMA18,
|
|
1057
|
+
ACTION: ACTION18
|
|
1058
|
+
};
|
|
1059
|
+
|
|
1060
|
+
// src/config.ts
|
|
1061
|
+
var MAPS_TOOL_ANNOTATIONS = {
|
|
1062
|
+
readOnlyHint: true,
|
|
1063
|
+
destructiveHint: false,
|
|
1064
|
+
idempotentHint: true,
|
|
1065
|
+
openWorldHint: true
|
|
1066
|
+
};
|
|
1067
|
+
var serverConfigs = [
|
|
1068
|
+
{
|
|
1069
|
+
name: "MCP-Server",
|
|
1070
|
+
portEnvVar: "MCP_SERVER_PORT",
|
|
1071
|
+
tools: [
|
|
1072
|
+
{
|
|
1073
|
+
name: SearchNearby.NAME,
|
|
1074
|
+
description: SearchNearby.DESCRIPTION,
|
|
1075
|
+
schema: SearchNearby.SCHEMA,
|
|
1076
|
+
annotations: MAPS_TOOL_ANNOTATIONS,
|
|
1077
|
+
action: (params) => SearchNearby.ACTION(params)
|
|
1078
|
+
},
|
|
1079
|
+
{
|
|
1080
|
+
name: PlaceDetails.NAME,
|
|
1081
|
+
description: PlaceDetails.DESCRIPTION,
|
|
1082
|
+
schema: PlaceDetails.SCHEMA,
|
|
1083
|
+
annotations: MAPS_TOOL_ANNOTATIONS,
|
|
1084
|
+
action: (params) => PlaceDetails.ACTION(params)
|
|
1085
|
+
},
|
|
1086
|
+
{
|
|
1087
|
+
name: Geocode.NAME,
|
|
1088
|
+
description: Geocode.DESCRIPTION,
|
|
1089
|
+
schema: Geocode.SCHEMA,
|
|
1090
|
+
annotations: MAPS_TOOL_ANNOTATIONS,
|
|
1091
|
+
action: (params) => Geocode.ACTION(params)
|
|
1092
|
+
},
|
|
1093
|
+
{
|
|
1094
|
+
name: ReverseGeocode.NAME,
|
|
1095
|
+
description: ReverseGeocode.DESCRIPTION,
|
|
1096
|
+
schema: ReverseGeocode.SCHEMA,
|
|
1097
|
+
annotations: MAPS_TOOL_ANNOTATIONS,
|
|
1098
|
+
action: (params) => ReverseGeocode.ACTION(params)
|
|
1099
|
+
},
|
|
1100
|
+
{
|
|
1101
|
+
name: DistanceMatrix.NAME,
|
|
1102
|
+
description: DistanceMatrix.DESCRIPTION,
|
|
1103
|
+
schema: DistanceMatrix.SCHEMA,
|
|
1104
|
+
annotations: MAPS_TOOL_ANNOTATIONS,
|
|
1105
|
+
action: (params) => DistanceMatrix.ACTION(params)
|
|
1106
|
+
},
|
|
1107
|
+
{
|
|
1108
|
+
name: Directions.NAME,
|
|
1109
|
+
description: Directions.DESCRIPTION,
|
|
1110
|
+
schema: Directions.SCHEMA,
|
|
1111
|
+
annotations: MAPS_TOOL_ANNOTATIONS,
|
|
1112
|
+
action: (params) => Directions.ACTION(params)
|
|
1113
|
+
},
|
|
1114
|
+
{
|
|
1115
|
+
name: Elevation.NAME,
|
|
1116
|
+
description: Elevation.DESCRIPTION,
|
|
1117
|
+
schema: Elevation.SCHEMA,
|
|
1118
|
+
annotations: MAPS_TOOL_ANNOTATIONS,
|
|
1119
|
+
action: (params) => Elevation.ACTION(params)
|
|
1120
|
+
},
|
|
1121
|
+
{
|
|
1122
|
+
name: SearchPlaces.NAME,
|
|
1123
|
+
description: SearchPlaces.DESCRIPTION,
|
|
1124
|
+
schema: SearchPlaces.SCHEMA,
|
|
1125
|
+
annotations: MAPS_TOOL_ANNOTATIONS,
|
|
1126
|
+
action: (params) => SearchPlaces.ACTION(params)
|
|
1127
|
+
},
|
|
1128
|
+
{
|
|
1129
|
+
name: Timezone.NAME,
|
|
1130
|
+
description: Timezone.DESCRIPTION,
|
|
1131
|
+
schema: Timezone.SCHEMA,
|
|
1132
|
+
annotations: MAPS_TOOL_ANNOTATIONS,
|
|
1133
|
+
action: (params) => Timezone.ACTION(params)
|
|
1134
|
+
},
|
|
1135
|
+
{
|
|
1136
|
+
name: Weather.NAME,
|
|
1137
|
+
description: Weather.DESCRIPTION,
|
|
1138
|
+
schema: Weather.SCHEMA,
|
|
1139
|
+
annotations: MAPS_TOOL_ANNOTATIONS,
|
|
1140
|
+
action: (params) => Weather.ACTION(params)
|
|
1141
|
+
},
|
|
1142
|
+
{
|
|
1143
|
+
name: ExploreArea.NAME,
|
|
1144
|
+
description: ExploreArea.DESCRIPTION,
|
|
1145
|
+
schema: ExploreArea.SCHEMA,
|
|
1146
|
+
annotations: MAPS_TOOL_ANNOTATIONS,
|
|
1147
|
+
action: (params) => ExploreArea.ACTION(params)
|
|
1148
|
+
},
|
|
1149
|
+
{
|
|
1150
|
+
name: PlanRoute.NAME,
|
|
1151
|
+
description: PlanRoute.DESCRIPTION,
|
|
1152
|
+
schema: PlanRoute.SCHEMA,
|
|
1153
|
+
annotations: MAPS_TOOL_ANNOTATIONS,
|
|
1154
|
+
action: (params) => PlanRoute.ACTION(params)
|
|
1155
|
+
},
|
|
1156
|
+
{
|
|
1157
|
+
name: ComparePlaces.NAME,
|
|
1158
|
+
description: ComparePlaces.DESCRIPTION,
|
|
1159
|
+
schema: ComparePlaces.SCHEMA,
|
|
1160
|
+
annotations: MAPS_TOOL_ANNOTATIONS,
|
|
1161
|
+
action: (params) => ComparePlaces.ACTION(params)
|
|
1162
|
+
},
|
|
1163
|
+
{
|
|
1164
|
+
name: AirQuality.NAME,
|
|
1165
|
+
description: AirQuality.DESCRIPTION,
|
|
1166
|
+
schema: AirQuality.SCHEMA,
|
|
1167
|
+
annotations: MAPS_TOOL_ANNOTATIONS,
|
|
1168
|
+
action: (params) => AirQuality.ACTION(params)
|
|
1169
|
+
},
|
|
1170
|
+
{
|
|
1171
|
+
name: StaticMap.NAME,
|
|
1172
|
+
description: StaticMap.DESCRIPTION,
|
|
1173
|
+
schema: StaticMap.SCHEMA,
|
|
1174
|
+
annotations: MAPS_TOOL_ANNOTATIONS,
|
|
1175
|
+
action: (params) => StaticMap.ACTION(params)
|
|
1176
|
+
},
|
|
1177
|
+
{
|
|
1178
|
+
name: BatchGeocode.NAME,
|
|
1179
|
+
description: BatchGeocode.DESCRIPTION,
|
|
1180
|
+
schema: BatchGeocode.SCHEMA,
|
|
1181
|
+
annotations: MAPS_TOOL_ANNOTATIONS,
|
|
1182
|
+
action: (params) => BatchGeocode.ACTION(params)
|
|
1183
|
+
},
|
|
1184
|
+
{
|
|
1185
|
+
name: SearchAlongRoute.NAME,
|
|
1186
|
+
description: SearchAlongRoute.DESCRIPTION,
|
|
1187
|
+
schema: SearchAlongRoute.SCHEMA,
|
|
1188
|
+
annotations: MAPS_TOOL_ANNOTATIONS,
|
|
1189
|
+
action: (params) => SearchAlongRoute.ACTION(params)
|
|
1190
|
+
},
|
|
1191
|
+
{
|
|
1192
|
+
name: LocalRankTracker.NAME,
|
|
1193
|
+
description: LocalRankTracker.DESCRIPTION,
|
|
1194
|
+
schema: LocalRankTracker.SCHEMA,
|
|
1195
|
+
annotations: MAPS_TOOL_ANNOTATIONS,
|
|
1196
|
+
action: (params) => LocalRankTracker.ACTION(params)
|
|
1197
|
+
}
|
|
1198
|
+
]
|
|
1199
|
+
}
|
|
1200
|
+
];
|
|
1201
|
+
function filterTools(tools) {
|
|
1202
|
+
const resolution = resolveEnabledToolNames(
|
|
1203
|
+
tools.map((tool) => tool.name),
|
|
1204
|
+
process.env.GOOGLE_MAPS_ENABLED_TOOLS
|
|
1205
|
+
);
|
|
1206
|
+
if (resolution.fallback) {
|
|
1207
|
+
Logger.error(`GOOGLE_MAPS_ENABLED_TOOLS matched 0 tools. Available: ${tools.map((t) => t.name).join(", ")}`);
|
|
1208
|
+
return tools;
|
|
1209
|
+
}
|
|
1210
|
+
if (resolution.matched !== tools.length) {
|
|
1211
|
+
Logger.log(`GOOGLE_MAPS_ENABLED_TOOLS: ${resolution.matched}/${tools.length} tools active`);
|
|
1212
|
+
}
|
|
1213
|
+
const enabled = new Set(resolution.names);
|
|
1214
|
+
return tools.filter((tool) => enabled.has(tool.name));
|
|
1215
|
+
}
|
|
1216
|
+
var config_default = serverConfigs;
|
|
1217
|
+
|
|
1218
|
+
// src/core/BaseMcpServer.ts
|
|
1219
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
1220
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
1221
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
1222
|
+
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
1223
|
+
import express from "express";
|
|
1224
|
+
import { randomUUID } from "crypto";
|
|
1225
|
+
import { z as z19 } from "zod";
|
|
1226
|
+
var VERSION = "0.0.1";
|
|
1227
|
+
var BaseMcpServer = class {
|
|
1228
|
+
constructor(name, tools) {
|
|
1229
|
+
this.sessions = {};
|
|
1230
|
+
this.httpServer = null;
|
|
1231
|
+
this.serverName = name;
|
|
1232
|
+
this.tools = tools;
|
|
1233
|
+
this.server = this.createMcpServer();
|
|
1234
|
+
}
|
|
1235
|
+
createMcpServer() {
|
|
1236
|
+
const server = new McpServer(
|
|
1237
|
+
{ name: this.serverName, version: VERSION },
|
|
1238
|
+
{ capabilities: { logging: {}, tools: {} } }
|
|
1239
|
+
);
|
|
1240
|
+
this.tools.forEach((tool) => {
|
|
1241
|
+
server.registerTool(
|
|
1242
|
+
tool.name,
|
|
1243
|
+
{
|
|
1244
|
+
description: tool.description,
|
|
1245
|
+
inputSchema: z19.object(tool.schema),
|
|
1246
|
+
annotations: tool.annotations
|
|
1247
|
+
},
|
|
1248
|
+
async (params) => tool.action(params)
|
|
1249
|
+
);
|
|
1250
|
+
});
|
|
1251
|
+
return server;
|
|
1252
|
+
}
|
|
1253
|
+
async connect(transport) {
|
|
1254
|
+
await this.server.connect(transport);
|
|
1255
|
+
const originalStdoutWrite = process.stdout.write.bind(process.stdout);
|
|
1256
|
+
process.stdout.write = (chunk, encoding, callback) => {
|
|
1257
|
+
if (typeof chunk === "string" && !chunk.startsWith("{")) {
|
|
1258
|
+
return true;
|
|
1259
|
+
}
|
|
1260
|
+
return originalStdoutWrite(chunk, encoding, callback);
|
|
1261
|
+
};
|
|
1262
|
+
Logger.log(`${this.serverName} connected and ready to process requests`);
|
|
1263
|
+
}
|
|
1264
|
+
/**
|
|
1265
|
+
* @brief Starts the Streamable HTTP listener and resolves once it is accepting connections.
|
|
1266
|
+
* @details Builds the Express app with the `/mcp` POST/GET/DELETE handlers,
|
|
1267
|
+
* then wraps `app.listen` in a Promise that resolves from the listen callback
|
|
1268
|
+
* and rejects on the server `error` event (e.g. EADDRINUSE), so callers get
|
|
1269
|
+
* a reliable startup outcome instead of an unhandled error. Default host is
|
|
1270
|
+
* loopback `127.0.0.1` (REQ-008); pass `0.0.0.0` to bind all interfaces.
|
|
1271
|
+
* @param port {number} TCP port to listen on (1-65535).
|
|
1272
|
+
* @param host {string} Bind address; defaults to `127.0.0.1`.
|
|
1273
|
+
* @return {Promise<void>} Resolves when listening; rejects with the listen error.
|
|
1274
|
+
* @throws {Error} Rejects when the HTTP listener emits an error (port in use, permission denied).
|
|
1275
|
+
* @satisfies REQ-008, REQ-095
|
|
1276
|
+
*/
|
|
1277
|
+
async startHttpServer(port, host = "127.0.0.1") {
|
|
1278
|
+
const app = express();
|
|
1279
|
+
app.use(express.json());
|
|
1280
|
+
app.post("/mcp", async (req, res) => {
|
|
1281
|
+
const sessionId = req.headers["mcp-session-id"];
|
|
1282
|
+
let context;
|
|
1283
|
+
const apiKeyManager = ApiKeyManager.getInstance();
|
|
1284
|
+
const requestApiKey = apiKeyManager.getApiKey(req, sessionId ? this.sessions[sessionId]?.apiKey : void 0);
|
|
1285
|
+
Logger.log(`${this.serverName} API key received from request context`);
|
|
1286
|
+
if (sessionId && this.sessions[sessionId]) {
|
|
1287
|
+
context = this.sessions[sessionId];
|
|
1288
|
+
if (requestApiKey) {
|
|
1289
|
+
context.apiKey = requestApiKey;
|
|
1290
|
+
}
|
|
1291
|
+
} else if (!sessionId && isInitializeRequest(req.body)) {
|
|
1292
|
+
const transport = new StreamableHTTPServerTransport({
|
|
1293
|
+
sessionIdGenerator: () => randomUUID(),
|
|
1294
|
+
onsessioninitialized: (sessionId2) => {
|
|
1295
|
+
this.sessions[sessionId2] = context;
|
|
1296
|
+
Logger.log(`[${this.serverName}] New session initialized: ${sessionId2}`);
|
|
1297
|
+
}
|
|
1298
|
+
// DNS rebinding protection is disabled by default for backwards compatibility
|
|
1299
|
+
// For production use, enable this:
|
|
1300
|
+
// enableDnsRebindingProtection: true,
|
|
1301
|
+
// allowedHosts: ['127.0.0.1'],
|
|
1302
|
+
});
|
|
1303
|
+
context = {
|
|
1304
|
+
transport,
|
|
1305
|
+
apiKey: requestApiKey
|
|
1306
|
+
};
|
|
1307
|
+
transport.onclose = () => {
|
|
1308
|
+
if (transport.sessionId) {
|
|
1309
|
+
delete this.sessions[transport.sessionId];
|
|
1310
|
+
Logger.log(`[${this.serverName}] Session closed: ${transport.sessionId}`);
|
|
1311
|
+
}
|
|
1312
|
+
};
|
|
1313
|
+
const sessionServer = this.createMcpServer();
|
|
1314
|
+
await sessionServer.connect(transport);
|
|
1315
|
+
} else {
|
|
1316
|
+
res.status(400).json({
|
|
1317
|
+
jsonrpc: "2.0",
|
|
1318
|
+
error: {
|
|
1319
|
+
code: -32e3,
|
|
1320
|
+
message: "Bad Request: No valid session ID provided"
|
|
1321
|
+
},
|
|
1322
|
+
id: null
|
|
1323
|
+
});
|
|
1324
|
+
return;
|
|
1325
|
+
}
|
|
1326
|
+
await runWithContext({ apiKey: context.apiKey, sessionId }, async () => {
|
|
1327
|
+
await context.transport.handleRequest(req, res, req.body);
|
|
1328
|
+
});
|
|
1329
|
+
});
|
|
1330
|
+
const handleSessionRequest = async (req, res) => {
|
|
1331
|
+
const sessionId = req.headers["mcp-session-id"];
|
|
1332
|
+
if (!sessionId || !this.sessions[sessionId]) {
|
|
1333
|
+
res.status(400).send("Invalid or missing session ID");
|
|
1334
|
+
return;
|
|
1335
|
+
}
|
|
1336
|
+
const context = this.sessions[sessionId];
|
|
1337
|
+
const apiKeyManager = ApiKeyManager.getInstance();
|
|
1338
|
+
const requestApiKey = apiKeyManager.getApiKey(req, context.apiKey);
|
|
1339
|
+
if (requestApiKey) {
|
|
1340
|
+
context.apiKey = requestApiKey;
|
|
1341
|
+
}
|
|
1342
|
+
await runWithContext({ apiKey: context.apiKey, sessionId }, async () => {
|
|
1343
|
+
await context.transport.handleRequest(req, res);
|
|
1344
|
+
});
|
|
1345
|
+
};
|
|
1346
|
+
app.get("/mcp", handleSessionRequest);
|
|
1347
|
+
app.delete("/mcp", handleSessionRequest);
|
|
1348
|
+
const displayHost = host === "0.0.0.0" ? "localhost" : host;
|
|
1349
|
+
await new Promise((resolve6, reject) => {
|
|
1350
|
+
this.httpServer = app.listen(port, host, () => {
|
|
1351
|
+
Logger.log(`[${this.serverName}] HTTP server listening on ${host}:${port}`);
|
|
1352
|
+
Logger.log(`[${this.serverName}] MCP endpoint available at http://${displayHost}:${port}/mcp`);
|
|
1353
|
+
resolve6();
|
|
1354
|
+
});
|
|
1355
|
+
this.httpServer.on("error", (error) => reject(error));
|
|
1356
|
+
});
|
|
1357
|
+
}
|
|
1358
|
+
async startStdio() {
|
|
1359
|
+
const transport = new StdioServerTransport();
|
|
1360
|
+
await this.connect(transport);
|
|
1361
|
+
}
|
|
1362
|
+
/**
|
|
1363
|
+
* @brief Stops the HTTP listener and closes every session transport in order.
|
|
1364
|
+
* @details Idempotent: when no listener exists it logs and resolves. Otherwise
|
|
1365
|
+
* it detaches the server reference, clears the session map, awaits
|
|
1366
|
+
* `transport.close()` for every session (errors tolerated via allSettled),
|
|
1367
|
+
* calls `closeAllConnections` when available, and finally closes the socket.
|
|
1368
|
+
* Wired to SIGINT/SIGTERM through `installShutdownHandlers` (REQ-096).
|
|
1369
|
+
* @return {Promise<void>} Resolves after the socket closes; rejects when `server.close` errors.
|
|
1370
|
+
* @throws {Error} Rejects with the `server.close` error.
|
|
1371
|
+
* @satisfies REQ-096
|
|
1372
|
+
*/
|
|
1373
|
+
async stopHttpServer() {
|
|
1374
|
+
const server = this.httpServer;
|
|
1375
|
+
if (!server) {
|
|
1376
|
+
Logger.error(`[${this.serverName}] HTTP server is not running or already stopped.`);
|
|
1377
|
+
return;
|
|
1378
|
+
}
|
|
1379
|
+
this.httpServer = null;
|
|
1380
|
+
const closingSessions = Object.values(this.sessions);
|
|
1381
|
+
this.sessions = {};
|
|
1382
|
+
await Promise.allSettled(closingSessions.map((context) => context.transport.close()));
|
|
1383
|
+
if (closingSessions.length > 0) {
|
|
1384
|
+
Logger.log(`[${this.serverName}] All transports closed.`);
|
|
1385
|
+
}
|
|
1386
|
+
return new Promise((resolve6, reject) => {
|
|
1387
|
+
server.closeAllConnections?.();
|
|
1388
|
+
server.close((err) => {
|
|
1389
|
+
if (err) {
|
|
1390
|
+
Logger.error(`[${this.serverName}] Error stopping HTTP server:`, err);
|
|
1391
|
+
reject(err);
|
|
1392
|
+
return;
|
|
1393
|
+
}
|
|
1394
|
+
Logger.log(`[${this.serverName}] HTTP server stopped.`);
|
|
1395
|
+
resolve6();
|
|
1396
|
+
});
|
|
1397
|
+
});
|
|
1398
|
+
}
|
|
1399
|
+
};
|
|
1400
|
+
|
|
1401
|
+
// src/cli.ts
|
|
1402
|
+
import { fileURLToPath } from "url";
|
|
1403
|
+
import { dirname as dirname3 } from "path";
|
|
1404
|
+
import { readFileSync as readFileSync5, writeFileSync as writeFileSync4, existsSync as existsSync4 } from "fs";
|
|
1405
|
+
import { createInterface } from "readline";
|
|
1406
|
+
|
|
1407
|
+
// src/utils/upgradeManager.ts
|
|
1408
|
+
import { spawnSync } from "child_process";
|
|
1409
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
1410
|
+
import { homedir } from "os";
|
|
1411
|
+
import { dirname, join as join2 } from "path";
|
|
1412
|
+
|
|
1413
|
+
// src/utils/autoUpdate.ts
|
|
1414
|
+
var AUTO_UPDATE_DISABLED_ENV = "MCP_AUTO_UPDATE_DISABLED";
|
|
1415
|
+
var DEFAULT_PACKAGE_NAME = "mcp-google-api";
|
|
1416
|
+
var DEFAULT_REGISTRY_BASE_URL = "https://registry.npmjs.org";
|
|
1417
|
+
var DEFAULT_UPDATE_CHECK_TIMEOUT_MS = 2e3;
|
|
1418
|
+
async function probeLatestVersion(deps = {}) {
|
|
1419
|
+
const env = deps.env;
|
|
1420
|
+
if (env !== void 0 && isAutoUpdateDisabled(env)) {
|
|
1421
|
+
return { status: "disabled" };
|
|
1422
|
+
}
|
|
1423
|
+
const currentVersion = deps.currentVersion;
|
|
1424
|
+
if (!currentVersion) {
|
|
1425
|
+
return { status: "unknown-version" };
|
|
1426
|
+
}
|
|
1427
|
+
const packageName = deps.packageName ?? DEFAULT_PACKAGE_NAME;
|
|
1428
|
+
const registryBaseUrl = deps.registryBaseUrl ?? DEFAULT_REGISTRY_BASE_URL;
|
|
1429
|
+
const timeoutMs = deps.timeoutMs ?? DEFAULT_UPDATE_CHECK_TIMEOUT_MS;
|
|
1430
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
1431
|
+
try {
|
|
1432
|
+
const response = await fetchImpl(`${registryBaseUrl}/${packageName}/latest`, {
|
|
1433
|
+
headers: { accept: "application/json" },
|
|
1434
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
1435
|
+
});
|
|
1436
|
+
if (!response.ok) {
|
|
1437
|
+
return {
|
|
1438
|
+
status: "error",
|
|
1439
|
+
message: `registry responded with HTTP ${response.status}.`
|
|
1440
|
+
};
|
|
1441
|
+
}
|
|
1442
|
+
const payload = await response.json();
|
|
1443
|
+
if (typeof payload?.version !== "string") {
|
|
1444
|
+
return {
|
|
1445
|
+
status: "error",
|
|
1446
|
+
message: "registry response has no version field."
|
|
1447
|
+
};
|
|
1448
|
+
}
|
|
1449
|
+
if (!isNewerVersion(payload.version, currentVersion)) {
|
|
1450
|
+
return { status: "current", currentVersion };
|
|
1451
|
+
}
|
|
1452
|
+
return { status: "newer", currentVersion, latestVersion: payload.version };
|
|
1453
|
+
} catch (error) {
|
|
1454
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1455
|
+
return { status: "error", message };
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
function isAutoUpdateDisabled(env = process.env) {
|
|
1459
|
+
const raw = env[AUTO_UPDATE_DISABLED_ENV];
|
|
1460
|
+
return typeof raw === "string" && /^(1|true|yes)$/i.test(raw.trim());
|
|
1461
|
+
}
|
|
1462
|
+
function isNewerVersion(candidate, current) {
|
|
1463
|
+
const toSegments = (version) => version.trim().split(".").map((segment) => /^\d+$/.test(segment) ? Number(segment) : Number.NaN);
|
|
1464
|
+
const candidateSegments = toSegments(candidate);
|
|
1465
|
+
const currentSegments = toSegments(current);
|
|
1466
|
+
if (candidateSegments.some((segment) => !Number.isInteger(segment)) || currentSegments.some((segment) => !Number.isInteger(segment))) {
|
|
1467
|
+
return false;
|
|
1468
|
+
}
|
|
1469
|
+
const length = Math.max(candidateSegments.length, currentSegments.length);
|
|
1470
|
+
for (let index = 0; index < length; index += 1) {
|
|
1471
|
+
const candidatePart = candidateSegments[index] ?? 0;
|
|
1472
|
+
const currentPart = currentSegments[index] ?? 0;
|
|
1473
|
+
if (candidatePart !== currentPart) {
|
|
1474
|
+
return candidatePart > currentPart;
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
return false;
|
|
1478
|
+
}
|
|
1479
|
+
var DEFAULT_GITHUB_RELEASES_API_URL = "https://api.github.com/repos/Ogekuri/mcp-google-api/releases/latest";
|
|
1480
|
+
function stripReleaseTagPrefix(tag) {
|
|
1481
|
+
return tag.trim().replace(/^v/i, "");
|
|
1482
|
+
}
|
|
1483
|
+
async function probeLatestGitHubVersion(deps = {}) {
|
|
1484
|
+
const releasesUrl = deps.releasesUrl ?? DEFAULT_GITHUB_RELEASES_API_URL;
|
|
1485
|
+
const timeoutMs = deps.timeoutMs ?? DEFAULT_UPDATE_CHECK_TIMEOUT_MS;
|
|
1486
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
1487
|
+
try {
|
|
1488
|
+
const response = await fetchImpl(releasesUrl, {
|
|
1489
|
+
headers: {
|
|
1490
|
+
accept: "application/vnd.github+json",
|
|
1491
|
+
"user-agent": DEFAULT_PACKAGE_NAME
|
|
1492
|
+
},
|
|
1493
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
1494
|
+
});
|
|
1495
|
+
if (!response.ok) {
|
|
1496
|
+
return {
|
|
1497
|
+
status: "error",
|
|
1498
|
+
message: `GitHub releases API responded with HTTP ${response.status}.`
|
|
1499
|
+
};
|
|
1500
|
+
}
|
|
1501
|
+
const payload = await response.json();
|
|
1502
|
+
if (typeof payload?.tag_name !== "string") {
|
|
1503
|
+
return {
|
|
1504
|
+
status: "error",
|
|
1505
|
+
message: "GitHub releases response has no tag_name field."
|
|
1506
|
+
};
|
|
1507
|
+
}
|
|
1508
|
+
const version = stripReleaseTagPrefix(payload.tag_name);
|
|
1509
|
+
if (!version) {
|
|
1510
|
+
return { status: "error", message: "GitHub release tag carries no version." };
|
|
1511
|
+
}
|
|
1512
|
+
return { status: "ok", version };
|
|
1513
|
+
} catch (error) {
|
|
1514
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1515
|
+
return { status: "error", message };
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
// src/utils/cliSettings.ts
|
|
1520
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
|
|
1521
|
+
import { join, resolve } from "path";
|
|
1522
|
+
var SETTINGS_PROGRAM_NAME = "mcp-google-api";
|
|
1523
|
+
var CLI_SETTINGS_RELATIVE = [".config", SETTINGS_PROGRAM_NAME, "config.json"];
|
|
1524
|
+
var MCP_SERVER_NAME = "mcp-google-api";
|
|
1525
|
+
var INSTALL_API_KEY_PLACEHOLDER = "__RUN_mcp-google-api_api-key_TO_CONFIGURE_YOUR_API_KEY__";
|
|
1526
|
+
function resolveCliSettingsPath(home) {
|
|
1527
|
+
const root = home ?? (process.env.HOME ?? process.cwd());
|
|
1528
|
+
return resolve(root, ...CLI_SETTINGS_RELATIVE);
|
|
1529
|
+
}
|
|
1530
|
+
function uniqueStrings(value) {
|
|
1531
|
+
if (!Array.isArray(value)) return [];
|
|
1532
|
+
const result = [];
|
|
1533
|
+
for (const item of value) {
|
|
1534
|
+
if (typeof item === "string" && item.length > 0 && !result.includes(item)) {
|
|
1535
|
+
result.push(item);
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
return result;
|
|
1539
|
+
}
|
|
1540
|
+
function isUnsetApiKey(value) {
|
|
1541
|
+
return value === void 0 || value === null || value.trim().length === 0 || value === INSTALL_API_KEY_PLACEHOLDER;
|
|
1542
|
+
}
|
|
1543
|
+
function loadCliSettings(home) {
|
|
1544
|
+
try {
|
|
1545
|
+
const raw = JSON.parse(readFileSync(resolveCliSettingsPath(home), "utf-8"));
|
|
1546
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
1547
|
+
return { enabled: [], apiKey: "", autoUpdate: true };
|
|
1548
|
+
}
|
|
1549
|
+
const record = raw;
|
|
1550
|
+
return {
|
|
1551
|
+
enabled: uniqueStrings(record.enabled),
|
|
1552
|
+
apiKey: typeof record.apiKey === "string" ? record.apiKey : "",
|
|
1553
|
+
autoUpdate: typeof record.autoUpdate === "boolean" ? record.autoUpdate : true
|
|
1554
|
+
};
|
|
1555
|
+
} catch {
|
|
1556
|
+
return { enabled: [], apiKey: "", autoUpdate: true };
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
function loadConfiguredApiKey(home) {
|
|
1560
|
+
const apiKey = loadCliSettings(home).apiKey;
|
|
1561
|
+
return isUnsetApiKey(apiKey) ? void 0 : apiKey;
|
|
1562
|
+
}
|
|
1563
|
+
function saveCliSettings(settings, home) {
|
|
1564
|
+
const path = resolveCliSettingsPath(home);
|
|
1565
|
+
const temporary = `${path}.tmp`;
|
|
1566
|
+
const payload = {
|
|
1567
|
+
enabled: uniqueStrings(settings.enabled),
|
|
1568
|
+
apiKey: typeof settings.apiKey === "string" ? settings.apiKey : "",
|
|
1569
|
+
autoUpdate: settings.autoUpdate === true
|
|
1570
|
+
};
|
|
1571
|
+
try {
|
|
1572
|
+
mkdirSync(join(path, ".."), { recursive: true });
|
|
1573
|
+
writeFileSync(temporary, JSON.stringify(payload, null, 2) + "\n", "utf-8");
|
|
1574
|
+
renameSync(temporary, path);
|
|
1575
|
+
} catch (error) {
|
|
1576
|
+
throw new Error(`settings write failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1577
|
+
}
|
|
1578
|
+
return path;
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
// src/utils/upgradeManager.ts
|
|
1582
|
+
var ANSI_GREEN = "\x1B[92m";
|
|
1583
|
+
var ANSI_RED = "\x1B[91m";
|
|
1584
|
+
var ANSI_RESET = "\x1B[0m";
|
|
1585
|
+
var green = (text) => `${ANSI_GREEN}${text}${ANSI_RESET}`;
|
|
1586
|
+
var red = (text) => `${ANSI_RED}${text}${ANSI_RESET}`;
|
|
1587
|
+
var IDLE_CACHE_DIR_NAME = "mcp-google-api";
|
|
1588
|
+
var IDLE_CACHE_FILE_NAME = "check_version_idle-time.json";
|
|
1589
|
+
var IDLE_WINDOW_NEWER_MS = 60 * 60 * 1e3;
|
|
1590
|
+
var IDLE_WINDOW_DEFAULT_MS = 24 * 60 * 60 * 1e3;
|
|
1591
|
+
var PACKAGE_UPGRADE_COMMAND = [
|
|
1592
|
+
"npm",
|
|
1593
|
+
"install",
|
|
1594
|
+
"-g",
|
|
1595
|
+
`${DEFAULT_PACKAGE_NAME}@latest`
|
|
1596
|
+
];
|
|
1597
|
+
var PACKAGE_UNINSTALL_COMMAND = [
|
|
1598
|
+
"npm",
|
|
1599
|
+
"uninstall",
|
|
1600
|
+
"-g",
|
|
1601
|
+
DEFAULT_PACKAGE_NAME
|
|
1602
|
+
];
|
|
1603
|
+
var POST_UPGRADE_INSTALL_COMMAND = [
|
|
1604
|
+
DEFAULT_PACKAGE_NAME,
|
|
1605
|
+
"install"
|
|
1606
|
+
];
|
|
1607
|
+
function resolveIdleCachePath(home) {
|
|
1608
|
+
return join2(home ?? homedir(), ".cache", IDLE_CACHE_DIR_NAME, IDLE_CACHE_FILE_NAME);
|
|
1609
|
+
}
|
|
1610
|
+
function loadIdleCache(cachePath, fs = {}) {
|
|
1611
|
+
try {
|
|
1612
|
+
const readFile = fs.readFile ?? ((path) => readFileSync2(path, "utf-8"));
|
|
1613
|
+
const raw = readFile(cachePath);
|
|
1614
|
+
const parsed = JSON.parse(raw);
|
|
1615
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
1616
|
+
return {};
|
|
1617
|
+
}
|
|
1618
|
+
const record = parsed;
|
|
1619
|
+
const isFiniteNumber = (value) => typeof value === "number" && Number.isFinite(value);
|
|
1620
|
+
return {
|
|
1621
|
+
last_success_epoch: isFiniteNumber(record.last_success_epoch) ? record.last_success_epoch : void 0,
|
|
1622
|
+
last_success_human: typeof record.last_success_human === "string" ? record.last_success_human : void 0,
|
|
1623
|
+
idle_time_epoch: isFiniteNumber(record.idle_time_epoch) ? record.idle_time_epoch : void 0,
|
|
1624
|
+
idle_time_human: typeof record.idle_time_human === "string" ? record.idle_time_human : void 0
|
|
1625
|
+
};
|
|
1626
|
+
} catch {
|
|
1627
|
+
return {};
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
function persistIdleCache(cachePath, lastSuccess, idleUntil, fs = {}, logger = Logger) {
|
|
1631
|
+
const toIso = (epoch) => epoch === void 0 ? null : new Date(epoch).toISOString();
|
|
1632
|
+
const data = {
|
|
1633
|
+
last_success_epoch: lastSuccess ?? null,
|
|
1634
|
+
last_success_human: toIso(lastSuccess),
|
|
1635
|
+
idle_time_epoch: idleUntil,
|
|
1636
|
+
idle_time_human: toIso(idleUntil)
|
|
1637
|
+
};
|
|
1638
|
+
try {
|
|
1639
|
+
const mkdir = fs.mkdir ?? ((path) => mkdirSync2(path, { recursive: true }));
|
|
1640
|
+
const writeFile = fs.writeFile ?? ((path, data2) => writeFileSync2(path, data2));
|
|
1641
|
+
mkdir(dirname(cachePath));
|
|
1642
|
+
writeFile(cachePath, JSON.stringify(data, null, 2) + "\n");
|
|
1643
|
+
} catch (error) {
|
|
1644
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1645
|
+
logger.error(red(`Version cache error: ${message}`));
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
async function checkVersionIdle(deps = {}) {
|
|
1649
|
+
const logger = deps.logger ?? Logger;
|
|
1650
|
+
const now = deps.now ?? (() => Date.now());
|
|
1651
|
+
const cachePath = deps.cachePath ?? resolveIdleCachePath();
|
|
1652
|
+
const cache = loadIdleCache(cachePath, deps.fs);
|
|
1653
|
+
if (deps.force !== true) {
|
|
1654
|
+
const idle = cache.idle_time_epoch;
|
|
1655
|
+
if (typeof idle === "number" && idle > now()) {
|
|
1656
|
+
return { checked: false, status: "idle" };
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
const probe = await probeLatestVersion(deps);
|
|
1660
|
+
if (probe.status === "disabled") {
|
|
1661
|
+
return { checked: false, status: "disabled" };
|
|
1662
|
+
}
|
|
1663
|
+
let delayMs = IDLE_WINDOW_DEFAULT_MS;
|
|
1664
|
+
let lastSuccess = typeof cache.last_success_epoch === "number" ? cache.last_success_epoch : void 0;
|
|
1665
|
+
const result = { checked: true, status: probe.status };
|
|
1666
|
+
switch (probe.status) {
|
|
1667
|
+
case "unknown-version":
|
|
1668
|
+
logger.error(red("Version check error: running package version is unknown."));
|
|
1669
|
+
break;
|
|
1670
|
+
case "error":
|
|
1671
|
+
logger.error(red(`Version check error: ${probe.message}`));
|
|
1672
|
+
break;
|
|
1673
|
+
case "current":
|
|
1674
|
+
lastSuccess = now();
|
|
1675
|
+
result.currentVersion = probe.currentVersion;
|
|
1676
|
+
break;
|
|
1677
|
+
case "newer": {
|
|
1678
|
+
lastSuccess = now();
|
|
1679
|
+
delayMs = IDLE_WINDOW_NEWER_MS;
|
|
1680
|
+
result.currentVersion = probe.currentVersion;
|
|
1681
|
+
result.latestVersion = probe.latestVersion;
|
|
1682
|
+
logger.log(
|
|
1683
|
+
green(
|
|
1684
|
+
`Available Version: ${probe.latestVersion} | Installed Version: ${probe.currentVersion}`
|
|
1685
|
+
)
|
|
1686
|
+
);
|
|
1687
|
+
logger.log(
|
|
1688
|
+
green(
|
|
1689
|
+
`Upgrade with: npm install -g ${DEFAULT_PACKAGE_NAME}@latest (or run: ${DEFAULT_PACKAGE_NAME} --upgrade)`
|
|
1690
|
+
)
|
|
1691
|
+
);
|
|
1692
|
+
break;
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
const finishedAt = now();
|
|
1696
|
+
const idleUntil = finishedAt + delayMs;
|
|
1697
|
+
persistIdleCache(cachePath, lastSuccess, idleUntil, deps.fs, logger);
|
|
1698
|
+
result.idleUntilEpoch = idleUntil;
|
|
1699
|
+
result.lastSuccessEpoch = lastSuccess;
|
|
1700
|
+
return result;
|
|
1701
|
+
}
|
|
1702
|
+
function parseManagementCommand(args) {
|
|
1703
|
+
if (args.length !== 1) {
|
|
1704
|
+
return void 0;
|
|
1705
|
+
}
|
|
1706
|
+
const arg = args[0];
|
|
1707
|
+
if (arg === "--ver" || arg === "--version" || arg === "--upgrade" || arg === "--uninstall") {
|
|
1708
|
+
return arg;
|
|
1709
|
+
}
|
|
1710
|
+
return void 0;
|
|
1711
|
+
}
|
|
1712
|
+
function runPackageManagerCommand(command, deps = {}) {
|
|
1713
|
+
const logger = deps.logger ?? Logger;
|
|
1714
|
+
const spawnImpl = deps.spawn ?? spawnSync;
|
|
1715
|
+
const reportSpawnFailure = (message) => {
|
|
1716
|
+
logger.error(`Error executing ${command[0]}: ${message}`);
|
|
1717
|
+
logger.log(`Warning: run this command manually: ${command.join(" ")}`);
|
|
1718
|
+
return 1;
|
|
1719
|
+
};
|
|
1720
|
+
try {
|
|
1721
|
+
const result = spawnImpl(command[0], [...command.slice(1)], {
|
|
1722
|
+
stdio: "inherit",
|
|
1723
|
+
shell: process.platform === "win32"
|
|
1724
|
+
});
|
|
1725
|
+
if (result.error) {
|
|
1726
|
+
return reportSpawnFailure(result.error.message);
|
|
1727
|
+
}
|
|
1728
|
+
return result.status ?? 1;
|
|
1729
|
+
} catch (error) {
|
|
1730
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1731
|
+
return reportSpawnFailure(message);
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
function parsePostUpgradeCliOutcomes(raw) {
|
|
1735
|
+
if (raw === void 0) {
|
|
1736
|
+
return void 0;
|
|
1737
|
+
}
|
|
1738
|
+
const start = raw.indexOf("{");
|
|
1739
|
+
if (start < 0) {
|
|
1740
|
+
return void 0;
|
|
1741
|
+
}
|
|
1742
|
+
let parsed;
|
|
1743
|
+
try {
|
|
1744
|
+
parsed = JSON.parse(raw.slice(start));
|
|
1745
|
+
} catch {
|
|
1746
|
+
return void 0;
|
|
1747
|
+
}
|
|
1748
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
1749
|
+
return void 0;
|
|
1750
|
+
}
|
|
1751
|
+
const summaries = parsed.summaries;
|
|
1752
|
+
const errors = parsed.errors;
|
|
1753
|
+
if (!Array.isArray(summaries)) {
|
|
1754
|
+
return void 0;
|
|
1755
|
+
}
|
|
1756
|
+
const errorTexts = Array.isArray(errors) ? errors.filter((item) => typeof item === "string") : [];
|
|
1757
|
+
const outcomes = [];
|
|
1758
|
+
for (const entry of summaries) {
|
|
1759
|
+
if (typeof entry !== "object" || entry === null) {
|
|
1760
|
+
continue;
|
|
1761
|
+
}
|
|
1762
|
+
const record = entry;
|
|
1763
|
+
const cli = record.cli;
|
|
1764
|
+
if (typeof cli !== "string" || cli.length === 0) {
|
|
1765
|
+
continue;
|
|
1766
|
+
}
|
|
1767
|
+
const lowered = cli.toLowerCase();
|
|
1768
|
+
const failed = errorTexts.some((text) => text.toLowerCase().includes(lowered));
|
|
1769
|
+
outcomes.push({
|
|
1770
|
+
cli,
|
|
1771
|
+
ok: !failed,
|
|
1772
|
+
server: typeof record.server === "number" ? record.server : void 0,
|
|
1773
|
+
skills: typeof record.skills === "number" ? record.skills : void 0
|
|
1774
|
+
});
|
|
1775
|
+
}
|
|
1776
|
+
return outcomes;
|
|
1777
|
+
}
|
|
1778
|
+
function runPostUpgradeSkillInstall(options, deps = {}) {
|
|
1779
|
+
const enabled = loadCliSettings(options.home).enabled;
|
|
1780
|
+
if (enabled.length === 0) {
|
|
1781
|
+
return { exitCode: 0, clis: [] };
|
|
1782
|
+
}
|
|
1783
|
+
const logger = deps.logger ?? Logger;
|
|
1784
|
+
const spawnImpl = deps.spawn ?? spawnSync;
|
|
1785
|
+
const reportSpawnFailure = (message) => {
|
|
1786
|
+
logger.error(`Error executing ${POST_UPGRADE_INSTALL_COMMAND[0]}: ${message}`);
|
|
1787
|
+
logger.log(`Warning: run this command manually: ${POST_UPGRADE_INSTALL_COMMAND.join(" ")}`);
|
|
1788
|
+
return { exitCode: 1, clis: enabled.map((cli) => ({ cli, ok: false })) };
|
|
1789
|
+
};
|
|
1790
|
+
try {
|
|
1791
|
+
const result = spawnImpl(
|
|
1792
|
+
POST_UPGRADE_INSTALL_COMMAND[0],
|
|
1793
|
+
[...POST_UPGRADE_INSTALL_COMMAND.slice(1)],
|
|
1794
|
+
{
|
|
1795
|
+
stdio: ["ignore", "pipe", "inherit"],
|
|
1796
|
+
shell: process.platform === "win32"
|
|
1797
|
+
}
|
|
1798
|
+
);
|
|
1799
|
+
if (result.error) {
|
|
1800
|
+
return reportSpawnFailure(result.error.message);
|
|
1801
|
+
}
|
|
1802
|
+
const exitCode = result.status ?? 1;
|
|
1803
|
+
const stdoutText = result.stdout === void 0 ? void 0 : String(result.stdout);
|
|
1804
|
+
const clis = parsePostUpgradeCliOutcomes(stdoutText) ?? enabled.map((cli) => ({ cli, ok: exitCode === 0 }));
|
|
1805
|
+
return { exitCode, clis };
|
|
1806
|
+
} catch (error) {
|
|
1807
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1808
|
+
return reportSpawnFailure(message);
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
function renderUpgradeSummary(previousVersion, updatedVersion, outcome) {
|
|
1812
|
+
const lines = [
|
|
1813
|
+
`mcp-google-api upgrade completed: ${previousVersion} -> ${updatedVersion}`
|
|
1814
|
+
];
|
|
1815
|
+
if (outcome.clis.length === 0) {
|
|
1816
|
+
lines.push(
|
|
1817
|
+
outcome.exitCode === 0 ? "Post-upgrade install: skipped (no CLI enabled)." : `Post-upgrade install: FAILED (exit code ${outcome.exitCode}).`
|
|
1818
|
+
);
|
|
1819
|
+
return `${lines.join("\n")}
|
|
1820
|
+
`;
|
|
1821
|
+
}
|
|
1822
|
+
lines.push(
|
|
1823
|
+
outcome.exitCode === 0 ? "Post-upgrade install: completed successfully on the following CLI(s):" : `Post-upgrade install: FAILED (exit code ${outcome.exitCode}) for the following CLI(s):`
|
|
1824
|
+
);
|
|
1825
|
+
for (const entry of outcome.clis) {
|
|
1826
|
+
const counts = entry.server !== void 0 || entry.skills !== void 0 ? ` (server: ${entry.server ?? 0}, skills: ${entry.skills ?? 0})` : "";
|
|
1827
|
+
lines.push(` - ${entry.cli}: ${entry.ok ? "OK" : "FAILED"}${counts}`);
|
|
1828
|
+
}
|
|
1829
|
+
return `${lines.join("\n")}
|
|
1830
|
+
`;
|
|
1831
|
+
}
|
|
1832
|
+
function resolveProbeLatestVersion(probe) {
|
|
1833
|
+
if (probe.status === "newer") {
|
|
1834
|
+
return probe.latestVersion;
|
|
1835
|
+
}
|
|
1836
|
+
if (probe.status === "current") {
|
|
1837
|
+
return probe.currentVersion;
|
|
1838
|
+
}
|
|
1839
|
+
return "unknown";
|
|
1840
|
+
}
|
|
1841
|
+
async function resolveUpdatedVersion(options, deps) {
|
|
1842
|
+
const probe = await probeLatestVersion({
|
|
1843
|
+
...deps.idleCheck,
|
|
1844
|
+
currentVersion: options.currentVersion
|
|
1845
|
+
});
|
|
1846
|
+
return resolveProbeLatestVersion(probe);
|
|
1847
|
+
}
|
|
1848
|
+
async function runVersionCommand(options, deps = {}) {
|
|
1849
|
+
const [npmProbe, githubProbe] = await Promise.all([
|
|
1850
|
+
probeLatestVersion({ ...deps.idleCheck, currentVersion: options.currentVersion }),
|
|
1851
|
+
probeLatestGitHubVersion(deps.githubCheck)
|
|
1852
|
+
]);
|
|
1853
|
+
const installedVersion = options.currentVersion || "unknown";
|
|
1854
|
+
const npmLatest = resolveProbeLatestVersion(npmProbe);
|
|
1855
|
+
const githubLatest = githubProbe.status === "ok" ? githubProbe.version : "unknown";
|
|
1856
|
+
const stdout = deps.stdout ?? process.stdout;
|
|
1857
|
+
stdout.write(`${installedVersion}
|
|
1858
|
+
npmjs latest: ${npmLatest}
|
|
1859
|
+
GitHub latest: ${githubLatest}
|
|
1860
|
+
`);
|
|
1861
|
+
return 0;
|
|
1862
|
+
}
|
|
1863
|
+
function isUpgradeNecessary(status) {
|
|
1864
|
+
return status !== "current";
|
|
1865
|
+
}
|
|
1866
|
+
async function runUpgradeCommand(options, deps = {}) {
|
|
1867
|
+
const precheck = await probeLatestVersion({
|
|
1868
|
+
...deps.idleCheck,
|
|
1869
|
+
currentVersion: options.currentVersion
|
|
1870
|
+
});
|
|
1871
|
+
if (!isUpgradeNecessary(precheck.status)) {
|
|
1872
|
+
const stdout2 = deps.stdout ?? process.stdout;
|
|
1873
|
+
stdout2.write(
|
|
1874
|
+
`${DEFAULT_PACKAGE_NAME} ${options.currentVersion} is already the latest available version.
|
|
1875
|
+
`
|
|
1876
|
+
);
|
|
1877
|
+
return 0;
|
|
1878
|
+
}
|
|
1879
|
+
const upgraded = runPackageManagerCommand(PACKAGE_UPGRADE_COMMAND, deps);
|
|
1880
|
+
if (upgraded !== 0) {
|
|
1881
|
+
return upgraded;
|
|
1882
|
+
}
|
|
1883
|
+
const updatedVersion = await resolveUpdatedVersion(options, deps);
|
|
1884
|
+
const outcome = runPostUpgradeSkillInstall(options, deps);
|
|
1885
|
+
const stdout = deps.stdout ?? process.stdout;
|
|
1886
|
+
stdout.write(renderUpgradeSummary(options.currentVersion, updatedVersion, outcome));
|
|
1887
|
+
return outcome.exitCode;
|
|
1888
|
+
}
|
|
1889
|
+
function runUninstallCommand(deps = {}) {
|
|
1890
|
+
return runPackageManagerCommand(PACKAGE_UNINSTALL_COMMAND, deps);
|
|
1891
|
+
}
|
|
1892
|
+
async function runManagementCommand(command, options, deps = {}) {
|
|
1893
|
+
switch (command) {
|
|
1894
|
+
case "--ver":
|
|
1895
|
+
case "--version":
|
|
1896
|
+
return runVersionCommand(options, deps);
|
|
1897
|
+
case "--upgrade":
|
|
1898
|
+
return runUpgradeCommand(options, deps);
|
|
1899
|
+
case "--uninstall":
|
|
1900
|
+
return runUninstallCommand(deps);
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
// src/utils/toolProfiles.ts
|
|
1905
|
+
var TOOL_API_PROFILES = {
|
|
1906
|
+
maps_geocode: { tool: "maps_geocode", apis: ["geocoding"], dependsOnTools: [] },
|
|
1907
|
+
maps_reverse_geocode: { tool: "maps_reverse_geocode", apis: ["geocoding"], dependsOnTools: [] },
|
|
1908
|
+
maps_batch_geocode: { tool: "maps_batch_geocode", apis: ["geocoding"], dependsOnTools: ["maps_geocode"] },
|
|
1909
|
+
maps_search_nearby: {
|
|
1910
|
+
tool: "maps_search_nearby",
|
|
1911
|
+
apis: ["places-new", "geocoding"],
|
|
1912
|
+
dependsOnTools: ["maps_geocode"]
|
|
1913
|
+
},
|
|
1914
|
+
maps_search_places: { tool: "maps_search_places", apis: ["places-new"], dependsOnTools: [] },
|
|
1915
|
+
maps_place_details: { tool: "maps_place_details", apis: ["places-new"], dependsOnTools: [] },
|
|
1916
|
+
maps_directions: { tool: "maps_directions", apis: ["routes"], dependsOnTools: [] },
|
|
1917
|
+
maps_distance_matrix: { tool: "maps_distance_matrix", apis: ["routes"], dependsOnTools: [] },
|
|
1918
|
+
maps_elevation: { tool: "maps_elevation", apis: ["elevation"], dependsOnTools: [] },
|
|
1919
|
+
maps_timezone: { tool: "maps_timezone", apis: ["timezone"], dependsOnTools: [] },
|
|
1920
|
+
maps_weather: { tool: "maps_weather", apis: ["weather"], dependsOnTools: [] },
|
|
1921
|
+
maps_air_quality: { tool: "maps_air_quality", apis: ["air-quality"], dependsOnTools: [] },
|
|
1922
|
+
maps_static_map: { tool: "maps_static_map", apis: ["static-maps"], dependsOnTools: [] },
|
|
1923
|
+
maps_explore_area: {
|
|
1924
|
+
tool: "maps_explore_area",
|
|
1925
|
+
apis: ["places-new", "geocoding"],
|
|
1926
|
+
dependsOnTools: ["maps_geocode", "maps_search_nearby", "maps_place_details"]
|
|
1927
|
+
},
|
|
1928
|
+
maps_plan_route: {
|
|
1929
|
+
tool: "maps_plan_route",
|
|
1930
|
+
apis: ["routes", "geocoding", "places-new"],
|
|
1931
|
+
dependsOnTools: ["maps_geocode", "maps_search_places", "maps_directions"]
|
|
1932
|
+
},
|
|
1933
|
+
maps_compare_places: {
|
|
1934
|
+
tool: "maps_compare_places",
|
|
1935
|
+
apis: ["places-new", "routes"],
|
|
1936
|
+
dependsOnTools: ["maps_search_places", "maps_place_details", "maps_distance_matrix"]
|
|
1937
|
+
},
|
|
1938
|
+
maps_search_along_route: {
|
|
1939
|
+
tool: "maps_search_along_route",
|
|
1940
|
+
apis: ["routes", "places-new"],
|
|
1941
|
+
dependsOnTools: ["maps_directions", "maps_search_places"]
|
|
1942
|
+
},
|
|
1943
|
+
maps_local_rank_tracker: {
|
|
1944
|
+
tool: "maps_local_rank_tracker",
|
|
1945
|
+
apis: ["places-new"],
|
|
1946
|
+
dependsOnTools: ["maps_place_details", "maps_search_places"]
|
|
1947
|
+
}
|
|
1948
|
+
};
|
|
1949
|
+
function describeToolsProfile(toolNames) {
|
|
1950
|
+
const tools = [];
|
|
1951
|
+
const apiOrder = [];
|
|
1952
|
+
const seenApis = /* @__PURE__ */ new Set();
|
|
1953
|
+
const seenTools = /* @__PURE__ */ new Set();
|
|
1954
|
+
for (const name of toolNames) {
|
|
1955
|
+
if (seenTools.has(name)) {
|
|
1956
|
+
continue;
|
|
1957
|
+
}
|
|
1958
|
+
const profile = TOOL_API_PROFILES[name];
|
|
1959
|
+
if (!profile) {
|
|
1960
|
+
continue;
|
|
1961
|
+
}
|
|
1962
|
+
seenTools.add(name);
|
|
1963
|
+
tools.push({
|
|
1964
|
+
tool: profile.tool,
|
|
1965
|
+
apis: [...profile.apis],
|
|
1966
|
+
dependsOnTools: [...profile.dependsOnTools]
|
|
1967
|
+
});
|
|
1968
|
+
for (const api of profile.apis) {
|
|
1969
|
+
if (!seenApis.has(api)) {
|
|
1970
|
+
seenApis.add(api);
|
|
1971
|
+
apiOrder.push(api);
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
return { toolCount: tools.length, apis: apiOrder, tools };
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1978
|
+
// src/utils/clientSnippets.ts
|
|
1979
|
+
var API_KEY_PLACEHOLDER = "<your-google-maps-api-key>";
|
|
1980
|
+
var SNIPPET_DEFAULT_HOST = "127.0.0.1";
|
|
1981
|
+
var SNIPPET_DEFAULT_PORT = 3e3;
|
|
1982
|
+
var SNIPPET_CLIENT_TARGETS = {
|
|
1983
|
+
generic: "your MCP client configuration file",
|
|
1984
|
+
claude: "Claude Desktop: claude_desktop_config.json",
|
|
1985
|
+
cursor: "Cursor: .cursor/mcp.json",
|
|
1986
|
+
pi: "pi CLI: ~/.pi/agent/mcp.json (user) or .pi/mcp.json (project)"
|
|
1987
|
+
};
|
|
1988
|
+
function renderClientSnippet(options = {}) {
|
|
1989
|
+
const client = options.client ?? "generic";
|
|
1990
|
+
const transport = options.transport ?? "stdio";
|
|
1991
|
+
const serverName = options.serverName ?? "mcp-google-api";
|
|
1992
|
+
const notes = [`Paste this snippet into ${SNIPPET_CLIENT_TARGETS[client]}.`];
|
|
1993
|
+
notes.push("This command prints the snippet only; it never writes or edits configuration files.");
|
|
1994
|
+
let entry;
|
|
1995
|
+
if (transport === "http") {
|
|
1996
|
+
const host = options.host ?? SNIPPET_DEFAULT_HOST;
|
|
1997
|
+
const port = options.port ?? SNIPPET_DEFAULT_PORT;
|
|
1998
|
+
entry = { url: `http://${host}:${port}/mcp` };
|
|
1999
|
+
notes.push(`Start the server first: mcp-google-api start --host ${host} --port ${port}.`);
|
|
2000
|
+
} else {
|
|
2001
|
+
const launcher = options.launcher ?? "npx";
|
|
2002
|
+
if (launcher === "node" && !options.commandPath) {
|
|
2003
|
+
throw new Error("--launcher node requires --command <path> pointing at dist/cli.js");
|
|
2004
|
+
}
|
|
2005
|
+
const command = launcher === "npx" ? "npx" : launcher === "bin" ? "mcp-google-api" : "node";
|
|
2006
|
+
const args = launcher === "npx" ? ["-y", "mcp-google-api", "start", "--stdio"] : launcher === "bin" ? ["start", "--stdio"] : [options.commandPath, "start", "--stdio"];
|
|
2007
|
+
if (launcher === "npx") {
|
|
2008
|
+
notes.push("The npx launcher runs the published package; npm must be available on PATH.");
|
|
2009
|
+
}
|
|
2010
|
+
const apiKeyValue = options.envKey ? `\${${options.envKey}}` : API_KEY_PLACEHOLDER;
|
|
2011
|
+
entry = { command, args, env: { GOOGLE_MAPS_API_KEY: apiKeyValue } };
|
|
2012
|
+
if (options.envKey) {
|
|
2013
|
+
notes.push(
|
|
2014
|
+
`Variable references (\${${options.envKey}}) are client-specific: confirm your client expands environment variables before relying on them.`
|
|
2015
|
+
);
|
|
2016
|
+
} else {
|
|
2017
|
+
notes.push(`Replace ${API_KEY_PLACEHOLDER} with your Google Maps API key.`);
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
const json = JSON.stringify({ mcpServers: { [serverName]: entry } }, null, 2);
|
|
2021
|
+
return { json, notes };
|
|
2022
|
+
}
|
|
2023
|
+
|
|
2024
|
+
// src/utils/cliTargets.ts
|
|
2025
|
+
import {
|
|
2026
|
+
appendFileSync,
|
|
2027
|
+
cpSync,
|
|
2028
|
+
existsSync as existsSync2,
|
|
2029
|
+
mkdirSync as mkdirSync3,
|
|
2030
|
+
readFileSync as readFileSync3,
|
|
2031
|
+
readdirSync,
|
|
2032
|
+
renameSync as renameSync2,
|
|
2033
|
+
rmSync,
|
|
2034
|
+
statSync,
|
|
2035
|
+
writeFileSync as writeFileSync3
|
|
2036
|
+
} from "fs";
|
|
2037
|
+
import { join as join3, relative } from "path";
|
|
2038
|
+
function homeRoot(home) {
|
|
2039
|
+
return home ?? (process.env.HOME ?? process.cwd());
|
|
2040
|
+
}
|
|
2041
|
+
var PI_TARGET = {
|
|
2042
|
+
id: "pi",
|
|
2043
|
+
configFormat: "json",
|
|
2044
|
+
mcpConfigPath: (home) => join3(homeRoot(home), ".pi", "agent", "mcp.json"),
|
|
2045
|
+
skillsDir: (home) => join3(homeRoot(home), ".pi", "agent", "skills")
|
|
2046
|
+
};
|
|
2047
|
+
var OPENCLAW_TARGET = {
|
|
2048
|
+
id: "openclaw",
|
|
2049
|
+
configFormat: "json",
|
|
2050
|
+
mcpConfigPath: (home) => join3(homeRoot(home), ".openclaw", "openclaw.json"),
|
|
2051
|
+
skillsDir: (home) => join3(homeRoot(home), ".openclaw", "workspace", "skills")
|
|
2052
|
+
};
|
|
2053
|
+
var CODEX_TARGET = {
|
|
2054
|
+
id: "codex",
|
|
2055
|
+
configFormat: "toml",
|
|
2056
|
+
mcpConfigPath: (home) => join3(homeRoot(home), ".codex", "config.toml"),
|
|
2057
|
+
skillsDir: (home) => join3(homeRoot(home), ".codex", "skills")
|
|
2058
|
+
};
|
|
2059
|
+
var ANTIGRAVITY_TARGET = {
|
|
2060
|
+
id: "antigravity",
|
|
2061
|
+
configFormat: "json",
|
|
2062
|
+
mcpConfigPath: (home) => join3(homeRoot(home), ".gemini", "settings.json"),
|
|
2063
|
+
skillsDir: (home) => join3(homeRoot(home), ".gemini", "config", "skills")
|
|
2064
|
+
};
|
|
2065
|
+
var CLAUDE_TARGET = {
|
|
2066
|
+
id: "claude",
|
|
2067
|
+
configFormat: "json",
|
|
2068
|
+
mcpConfigPath: (home) => join3(homeRoot(home), ".claude.json"),
|
|
2069
|
+
skillsDir: (home) => join3(homeRoot(home), ".claude", "skills")
|
|
2070
|
+
};
|
|
2071
|
+
var CLI_TARGETS = {
|
|
2072
|
+
pi: PI_TARGET,
|
|
2073
|
+
openclaw: OPENCLAW_TARGET,
|
|
2074
|
+
codex: CODEX_TARGET,
|
|
2075
|
+
antigravity: ANTIGRAVITY_TARGET,
|
|
2076
|
+
claude: CLAUDE_TARGET
|
|
2077
|
+
};
|
|
2078
|
+
function resolveCliTarget(name) {
|
|
2079
|
+
return CLI_TARGETS[name.trim().toLowerCase()];
|
|
2080
|
+
}
|
|
2081
|
+
function supportedCliNames() {
|
|
2082
|
+
return Object.keys(CLI_TARGETS).join(", ");
|
|
2083
|
+
}
|
|
2084
|
+
function readJsonConfig(configPath) {
|
|
2085
|
+
if (!existsSync2(configPath)) return {};
|
|
2086
|
+
const parsed = JSON.parse(readFileSync3(configPath, "utf-8"));
|
|
2087
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
2088
|
+
throw new Error(`configuration is not a JSON object: ${configPath}`);
|
|
2089
|
+
}
|
|
2090
|
+
return parsed;
|
|
2091
|
+
}
|
|
2092
|
+
function writeJsonConfig(configPath, value) {
|
|
2093
|
+
const temporary = `${configPath}.tmp`;
|
|
2094
|
+
mkdirSync3(join3(configPath, ".."), { recursive: true });
|
|
2095
|
+
writeFileSync3(temporary, JSON.stringify(value, null, 2) + "\n", "utf-8");
|
|
2096
|
+
renameSync2(temporary, configPath);
|
|
2097
|
+
}
|
|
2098
|
+
function writeJsonMcpServerEntry(configPath, serverName, entry) {
|
|
2099
|
+
const config = readJsonConfig(configPath);
|
|
2100
|
+
const mcpServers = config.mcpServers ?? {};
|
|
2101
|
+
if (mcpServers === null || typeof mcpServers !== "object" || Array.isArray(mcpServers)) {
|
|
2102
|
+
throw new Error(`mcpServers is not an object: ${configPath}`);
|
|
2103
|
+
}
|
|
2104
|
+
mcpServers[serverName] = entry;
|
|
2105
|
+
config.mcpServers = mcpServers;
|
|
2106
|
+
writeJsonConfig(configPath, config);
|
|
2107
|
+
}
|
|
2108
|
+
function removeJsonMcpServerEntry(configPath, serverName) {
|
|
2109
|
+
if (!existsSync2(configPath)) return false;
|
|
2110
|
+
const config = readJsonConfig(configPath);
|
|
2111
|
+
const mcpServers = config.mcpServers;
|
|
2112
|
+
if (mcpServers === void 0) return false;
|
|
2113
|
+
if (mcpServers === null || typeof mcpServers !== "object" || Array.isArray(mcpServers)) {
|
|
2114
|
+
throw new Error(`mcpServers is not an object: ${configPath}`);
|
|
2115
|
+
}
|
|
2116
|
+
const map = mcpServers;
|
|
2117
|
+
if (!(serverName in map)) return false;
|
|
2118
|
+
delete map[serverName];
|
|
2119
|
+
config.mcpServers = map;
|
|
2120
|
+
writeJsonConfig(configPath, config);
|
|
2121
|
+
return true;
|
|
2122
|
+
}
|
|
2123
|
+
function readJsonMcpServerEntry(configPath, serverName) {
|
|
2124
|
+
try {
|
|
2125
|
+
const config = readJsonConfig(configPath);
|
|
2126
|
+
const mcpServers = config.mcpServers;
|
|
2127
|
+
if (mcpServers === null || typeof mcpServers !== "object" || Array.isArray(mcpServers)) {
|
|
2128
|
+
return void 0;
|
|
2129
|
+
}
|
|
2130
|
+
return mcpServers[serverName];
|
|
2131
|
+
} catch {
|
|
2132
|
+
return void 0;
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
function tomlString(value) {
|
|
2136
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
2137
|
+
}
|
|
2138
|
+
function tomlSectionHeader(serverName) {
|
|
2139
|
+
return `[mcp_servers.${serverName}]`;
|
|
2140
|
+
}
|
|
2141
|
+
function removeTomlSection(content, serverName) {
|
|
2142
|
+
const lines = content.split("\n");
|
|
2143
|
+
const header = tomlSectionHeader(serverName);
|
|
2144
|
+
const kept = [];
|
|
2145
|
+
let skipping = false;
|
|
2146
|
+
for (const line of lines) {
|
|
2147
|
+
if (skipping) {
|
|
2148
|
+
if (line.trimStart().startsWith("[")) {
|
|
2149
|
+
skipping = false;
|
|
2150
|
+
} else {
|
|
2151
|
+
continue;
|
|
2152
|
+
}
|
|
2153
|
+
} else if (line.trim() === header) {
|
|
2154
|
+
skipping = true;
|
|
2155
|
+
while (kept.length > 0 && kept[kept.length - 1].trim() === "") {
|
|
2156
|
+
kept.pop();
|
|
2157
|
+
}
|
|
2158
|
+
continue;
|
|
2159
|
+
}
|
|
2160
|
+
kept.push(line);
|
|
2161
|
+
}
|
|
2162
|
+
while (kept.length > 0 && kept[kept.length - 1].trim() === "") {
|
|
2163
|
+
kept.pop();
|
|
2164
|
+
}
|
|
2165
|
+
return kept.length > 0 ? kept.join("\n") + "\n" : "";
|
|
2166
|
+
}
|
|
2167
|
+
function hasTomlSection(content, serverName) {
|
|
2168
|
+
return content.split("\n").some((line) => line.trim() === tomlSectionHeader(serverName));
|
|
2169
|
+
}
|
|
2170
|
+
function registrationToTomlLines(entry) {
|
|
2171
|
+
const lines = [`command = ${tomlString(entry.command)}`];
|
|
2172
|
+
if (entry.args !== void 0) {
|
|
2173
|
+
lines.push(`args = [${entry.args.map((arg) => tomlString(arg)).join(", ")}]`);
|
|
2174
|
+
}
|
|
2175
|
+
if (entry.env !== void 0) {
|
|
2176
|
+
const pairs = Object.entries(entry.env).map(([key, value]) => `${tomlString(key)} = ${tomlString(value)}`);
|
|
2177
|
+
if (pairs.length > 0) lines.push(`env = { ${pairs.join(", ")} }`);
|
|
2178
|
+
}
|
|
2179
|
+
return lines;
|
|
2180
|
+
}
|
|
2181
|
+
function writeTomlMcpServerSection(configPath, serverName, entry) {
|
|
2182
|
+
const previous = existsSync2(configPath) ? readFileSync3(configPath, "utf-8") : "";
|
|
2183
|
+
const base = removeTomlSection(previous, serverName);
|
|
2184
|
+
const table = [tomlSectionHeader(serverName), ...registrationToTomlLines(entry), ""].join("\n");
|
|
2185
|
+
const content = base.length > 0 ? base.replace(/\n*$/, "\n") + table : table;
|
|
2186
|
+
const temporary = `${configPath}.tmp`;
|
|
2187
|
+
mkdirSync3(join3(configPath, ".."), { recursive: true });
|
|
2188
|
+
writeFileSync3(temporary, content, "utf-8");
|
|
2189
|
+
renameSync2(temporary, configPath);
|
|
2190
|
+
}
|
|
2191
|
+
function removeTomlMcpServerSection(configPath, serverName) {
|
|
2192
|
+
if (!existsSync2(configPath)) return false;
|
|
2193
|
+
const previous = readFileSync3(configPath, "utf-8");
|
|
2194
|
+
if (!hasTomlSection(previous, serverName)) return false;
|
|
2195
|
+
const content = removeTomlSection(previous, serverName);
|
|
2196
|
+
const temporary = `${configPath}.tmp`;
|
|
2197
|
+
writeFileSync3(temporary, content, "utf-8");
|
|
2198
|
+
renameSync2(temporary, configPath);
|
|
2199
|
+
return true;
|
|
2200
|
+
}
|
|
2201
|
+
function hasTomlMcpServerSection(configPath, serverName) {
|
|
2202
|
+
if (!existsSync2(configPath)) return false;
|
|
2203
|
+
return hasTomlSection(readFileSync3(configPath, "utf-8"), serverName);
|
|
2204
|
+
}
|
|
2205
|
+
function listFilesRecursive(root) {
|
|
2206
|
+
if (!existsSync2(root)) return [];
|
|
2207
|
+
const files = [];
|
|
2208
|
+
const walk = (dir) => {
|
|
2209
|
+
for (const entry of readdirSync(dir)) {
|
|
2210
|
+
const full = join3(dir, entry);
|
|
2211
|
+
if (statSync(full).isDirectory()) {
|
|
2212
|
+
walk(full);
|
|
2213
|
+
} else {
|
|
2214
|
+
files.push(relative(root, full).split("\\").join("/"));
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
2217
|
+
};
|
|
2218
|
+
walk(root);
|
|
2219
|
+
return files;
|
|
2220
|
+
}
|
|
2221
|
+
var SKILL_DOCUMENT_FILE = "SKILL.md";
|
|
2222
|
+
function syncSkillTree(source, destination, layout = {}) {
|
|
2223
|
+
mkdirSync3(destination, { recursive: true });
|
|
2224
|
+
cpSync(source, destination, {
|
|
2225
|
+
recursive: true,
|
|
2226
|
+
force: true,
|
|
2227
|
+
filter: (entry) => layout.debugPath === void 0 || relative(source, entry).split("\\").join("/") !== layout.debugPath
|
|
2228
|
+
});
|
|
2229
|
+
const expected = new Set(listFilesRecursive(source).filter((rel) => rel !== layout.debugPath));
|
|
2230
|
+
const existing = listFilesRecursive(destination);
|
|
2231
|
+
existing.sort((a, b) => b.length - a.length);
|
|
2232
|
+
for (const rel of existing) {
|
|
2233
|
+
if (expected.has(rel)) continue;
|
|
2234
|
+
rmSync(join3(destination, rel), { recursive: true, force: true });
|
|
2235
|
+
}
|
|
2236
|
+
if (layout.includeDebug === true && layout.debugPath !== void 0) {
|
|
2237
|
+
appendFileSync(
|
|
2238
|
+
join3(destination, ...SKILL_DOCUMENT_FILE.split("/")),
|
|
2239
|
+
readFileSync3(join3(source, ...layout.debugPath.split("/")))
|
|
2240
|
+
);
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
2243
|
+
function removeSkillDir(skillDir) {
|
|
2244
|
+
rmSync(skillDir, { recursive: true, force: true });
|
|
2245
|
+
}
|
|
2246
|
+
function compareSkillTree(source, destination, layout = {}) {
|
|
2247
|
+
const packaged = listFilesRecursive(source).filter((rel) => rel !== layout.debugPath);
|
|
2248
|
+
if (packaged.length === 0) return "MISSING";
|
|
2249
|
+
if (!existsSync2(destination)) return "MISSING";
|
|
2250
|
+
for (const rel of packaged) {
|
|
2251
|
+
const target = join3(destination, ...rel.split("/"));
|
|
2252
|
+
if (!existsSync2(target)) return "MISSING";
|
|
2253
|
+
const packagedText = readFileSync3(join3(source, ...rel.split("/"))).toString("utf-8");
|
|
2254
|
+
const installedText = readFileSync3(target).toString("utf-8");
|
|
2255
|
+
if (installedText === packagedText) continue;
|
|
2256
|
+
if (rel === SKILL_DOCUMENT_FILE && layout.debugPath !== void 0) {
|
|
2257
|
+
const overlayText = readFileSync3(join3(source, ...layout.debugPath.split("/"))).toString("utf-8");
|
|
2258
|
+
if (installedText === packagedText + overlayText) continue;
|
|
2259
|
+
}
|
|
2260
|
+
return "STALE";
|
|
2261
|
+
}
|
|
2262
|
+
return "OK";
|
|
2263
|
+
}
|
|
2264
|
+
function isServerRegistered(target, serverName, home) {
|
|
2265
|
+
const configPath = target.mcpConfigPath(home);
|
|
2266
|
+
if (target.configFormat === "toml") {
|
|
2267
|
+
return hasTomlMcpServerSection(configPath, serverName);
|
|
2268
|
+
}
|
|
2269
|
+
return readJsonMcpServerEntry(configPath, serverName) !== void 0;
|
|
2270
|
+
}
|
|
2271
|
+
function applyServerRegistration(target, serverName, entry, home) {
|
|
2272
|
+
const configPath = target.mcpConfigPath(home);
|
|
2273
|
+
if (target.configFormat === "toml") {
|
|
2274
|
+
if (entry === void 0) {
|
|
2275
|
+
removeTomlMcpServerSection(configPath, serverName);
|
|
2276
|
+
} else {
|
|
2277
|
+
writeTomlMcpServerSection(configPath, serverName, entry);
|
|
2278
|
+
}
|
|
2279
|
+
return;
|
|
2280
|
+
}
|
|
2281
|
+
if (entry === void 0) {
|
|
2282
|
+
removeJsonMcpServerEntry(configPath, serverName);
|
|
2283
|
+
} else {
|
|
2284
|
+
writeJsonMcpServerEntry(configPath, serverName, entry);
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2287
|
+
|
|
2288
|
+
// src/utils/cliInstaller.ts
|
|
2289
|
+
import { existsSync as existsSync3, readFileSync as readFileSync4 } from "fs";
|
|
2290
|
+
import { dirname as dirname2, join as join4, resolve as resolve3 } from "path";
|
|
2291
|
+
function buildServerRegistration() {
|
|
2292
|
+
return { command: "npx", args: ["-y", MCP_SERVER_NAME, "start", "--stdio"] };
|
|
2293
|
+
}
|
|
2294
|
+
function resolveSkillsResourcesRoot(packageRoot) {
|
|
2295
|
+
const candidate = resolve3(packageRoot, "src", "resources", "skills");
|
|
2296
|
+
if (!existsSync3(candidate)) {
|
|
2297
|
+
throw new Error(`skills resources root not found: ${candidate}`);
|
|
2298
|
+
}
|
|
2299
|
+
return candidate;
|
|
2300
|
+
}
|
|
2301
|
+
function loadSkillsManifest(resourcesRoot) {
|
|
2302
|
+
const manifestPath = join4(resourcesRoot, "manifest.json");
|
|
2303
|
+
if (!existsSync3(manifestPath)) {
|
|
2304
|
+
throw new Error(`skills manifest not found: ${manifestPath}`);
|
|
2305
|
+
}
|
|
2306
|
+
let parsed;
|
|
2307
|
+
try {
|
|
2308
|
+
parsed = JSON.parse(readFileSync4(manifestPath, "utf-8"));
|
|
2309
|
+
} catch (error) {
|
|
2310
|
+
throw new Error(`skills manifest is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
2311
|
+
}
|
|
2312
|
+
const skills = parsed?.skills;
|
|
2313
|
+
if (!Array.isArray(skills)) {
|
|
2314
|
+
throw new Error(`skills manifest has no skills array: ${manifestPath}`);
|
|
2315
|
+
}
|
|
2316
|
+
return skills.map((entry) => {
|
|
2317
|
+
const skill = entry;
|
|
2318
|
+
if (typeof skill.id !== "string" || typeof skill.path !== "string") {
|
|
2319
|
+
throw new Error(`skills manifest entry is missing id or path: ${manifestPath}`);
|
|
2320
|
+
}
|
|
2321
|
+
const skillFile = join4(resourcesRoot, skill.path);
|
|
2322
|
+
if (!existsSync3(skillFile)) {
|
|
2323
|
+
throw new Error(`manifest skill file not found: ${skillFile}`);
|
|
2324
|
+
}
|
|
2325
|
+
if (skill.debug !== void 0 && (typeof skill.debug !== "string" || skill.debug.trim() === "")) {
|
|
2326
|
+
throw new Error(`manifest skill debug path is invalid: ${manifestPath}`);
|
|
2327
|
+
}
|
|
2328
|
+
const debugPath = typeof skill.debug === "string" ? skill.debug : void 0;
|
|
2329
|
+
if (debugPath !== void 0 && !existsSync3(join4(dirname2(resolve3(skillFile)), debugPath))) {
|
|
2330
|
+
throw new Error(`manifest skill debug file not found: ${join4(dirname2(resolve3(skillFile)), debugPath)}`);
|
|
2331
|
+
}
|
|
2332
|
+
return {
|
|
2333
|
+
id: skill.id,
|
|
2334
|
+
name: skill.name ?? skill.id,
|
|
2335
|
+
version: skill.version ?? "",
|
|
2336
|
+
path: skill.path,
|
|
2337
|
+
description: skill.description ?? "",
|
|
2338
|
+
...debugPath === void 0 ? {} : { debugPath }
|
|
2339
|
+
};
|
|
2340
|
+
});
|
|
2341
|
+
}
|
|
2342
|
+
function resolveSkillContentLayout(skill, includeDebug) {
|
|
2343
|
+
return {
|
|
2344
|
+
...skill.debugPath === void 0 ? {} : { debugPath: skill.debugPath },
|
|
2345
|
+
includeDebug: includeDebug && skill.debugPath !== void 0
|
|
2346
|
+
};
|
|
2347
|
+
}
|
|
2348
|
+
function installEnabledCliResources(options) {
|
|
2349
|
+
const manifest = loadSkillsManifest(options.resourcesRoot);
|
|
2350
|
+
const entry = buildServerRegistration();
|
|
2351
|
+
const result = { summaries: [], errors: [] };
|
|
2352
|
+
for (const cli of loadCliSettings(options.home).enabled) {
|
|
2353
|
+
const target = CLI_TARGETS[cli];
|
|
2354
|
+
if (target === void 0) {
|
|
2355
|
+
result.errors.push(`Unsupported CLI: ${cli}`);
|
|
2356
|
+
continue;
|
|
2357
|
+
}
|
|
2358
|
+
try {
|
|
2359
|
+
applyServerRegistration(target, MCP_SERVER_NAME, entry, options.home);
|
|
2360
|
+
let skills = 0;
|
|
2361
|
+
for (const skill of manifest) {
|
|
2362
|
+
syncSkillTree(
|
|
2363
|
+
join4(options.resourcesRoot, skill.id),
|
|
2364
|
+
join4(target.skillsDir(options.home), skill.id),
|
|
2365
|
+
resolveSkillContentLayout(skill, options.includeDebugSkills === true)
|
|
2366
|
+
);
|
|
2367
|
+
skills += 1;
|
|
2368
|
+
}
|
|
2369
|
+
result.summaries.push({ cli: target.id, server: 1, skills });
|
|
2370
|
+
} catch (error) {
|
|
2371
|
+
result.errors.push(`${target.id}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2372
|
+
}
|
|
2373
|
+
}
|
|
2374
|
+
return result;
|
|
2375
|
+
}
|
|
2376
|
+
function uninstallEnabledCliResources(options) {
|
|
2377
|
+
const manifest = loadSkillsManifest(options.resourcesRoot);
|
|
2378
|
+
const result = { summaries: [], errors: [] };
|
|
2379
|
+
for (const cli of loadCliSettings(options.home).enabled) {
|
|
2380
|
+
const target = CLI_TARGETS[cli];
|
|
2381
|
+
if (target === void 0) {
|
|
2382
|
+
result.errors.push(`Unsupported CLI: ${cli}`);
|
|
2383
|
+
continue;
|
|
2384
|
+
}
|
|
2385
|
+
try {
|
|
2386
|
+
applyServerRegistration(target, MCP_SERVER_NAME, void 0, options.home);
|
|
2387
|
+
let skills = 0;
|
|
2388
|
+
for (const skill of manifest) {
|
|
2389
|
+
removeSkillDir(join4(target.skillsDir(options.home), skill.id));
|
|
2390
|
+
skills += 1;
|
|
2391
|
+
}
|
|
2392
|
+
result.summaries.push({ cli: target.id, server: 0, skills });
|
|
2393
|
+
} catch (error) {
|
|
2394
|
+
result.errors.push(`${target.id}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2397
|
+
return result;
|
|
2398
|
+
}
|
|
2399
|
+
|
|
2400
|
+
// src/utils/cliShow.ts
|
|
2401
|
+
import { join as join5 } from "path";
|
|
2402
|
+
import { relative as relative2, resolve as resolve4 } from "path";
|
|
2403
|
+
var MAX_CELL_WIDTH = 48;
|
|
2404
|
+
var STATUS_OK = "OK";
|
|
2405
|
+
var STATUS_MISSING = "MISSING";
|
|
2406
|
+
function wrapCell(text, width) {
|
|
2407
|
+
const normalized = String(text).split(/\s+/).filter((word) => word.length > 0);
|
|
2408
|
+
if (normalized.length === 0) return [""];
|
|
2409
|
+
const lines = [];
|
|
2410
|
+
let current = "";
|
|
2411
|
+
for (let word of normalized) {
|
|
2412
|
+
while (word.length > width) {
|
|
2413
|
+
if (current.length > 0) {
|
|
2414
|
+
lines.push(current);
|
|
2415
|
+
current = "";
|
|
2416
|
+
}
|
|
2417
|
+
lines.push(word.slice(0, width));
|
|
2418
|
+
word = word.slice(width);
|
|
2419
|
+
}
|
|
2420
|
+
if (current.length === 0) {
|
|
2421
|
+
current = word;
|
|
2422
|
+
} else if (current.length + 1 + word.length <= width) {
|
|
2423
|
+
current = `${current} ${word}`;
|
|
2424
|
+
} else {
|
|
2425
|
+
lines.push(current);
|
|
2426
|
+
current = word;
|
|
2427
|
+
}
|
|
2428
|
+
}
|
|
2429
|
+
if (current.length > 0) lines.push(current);
|
|
2430
|
+
return lines.length > 0 ? lines : [""];
|
|
2431
|
+
}
|
|
2432
|
+
function renderAsciiTable(headers, rows, maxWidth = MAX_CELL_WIDTH) {
|
|
2433
|
+
const widths = headers.map((header, index) => {
|
|
2434
|
+
const lengths = [header.length, ...rows.map((row) => (row[index] ?? "").length)];
|
|
2435
|
+
return Math.max(1, Math.min(Math.max(...lengths), Math.max(1, maxWidth)));
|
|
2436
|
+
});
|
|
2437
|
+
const border = "+" + widths.map((width) => "-".repeat(width + 2)).join("+") + "+";
|
|
2438
|
+
const renderRow = (cells) => {
|
|
2439
|
+
const wrapped = cells.map((cell, index) => wrapCell(cell ?? "", widths[index]));
|
|
2440
|
+
const height = Math.max(...wrapped.map((part) => part.length));
|
|
2441
|
+
const lines = [];
|
|
2442
|
+
for (let line = 0; line < height; line += 1) {
|
|
2443
|
+
const parts = wrapped.map((part, index) => (part[line] ?? "").padEnd(widths[index]));
|
|
2444
|
+
lines.push("| " + parts.join(" | ") + " |");
|
|
2445
|
+
}
|
|
2446
|
+
return lines;
|
|
2447
|
+
};
|
|
2448
|
+
const output = [border, ...renderRow(headers), border];
|
|
2449
|
+
for (const row of rows) {
|
|
2450
|
+
output.push(...renderRow(row));
|
|
2451
|
+
output.push(border);
|
|
2452
|
+
}
|
|
2453
|
+
return output.join("\n");
|
|
2454
|
+
}
|
|
2455
|
+
function buildCliRows(home) {
|
|
2456
|
+
const root = home ?? (process.env.HOME ?? process.cwd());
|
|
2457
|
+
const enabled = new Set(loadCliSettings(home).enabled);
|
|
2458
|
+
return Object.keys(CLI_TARGETS).map((id) => {
|
|
2459
|
+
const target = CLI_TARGETS[id];
|
|
2460
|
+
return [
|
|
2461
|
+
target.id,
|
|
2462
|
+
enabled.has(id) ? "yes" : "no",
|
|
2463
|
+
relative2(resolve4(root), resolve4(target.mcpConfigPath(home))).split("\\").join("/"),
|
|
2464
|
+
relative2(resolve4(root), resolve4(target.skillsDir(home))).split("\\").join("/")
|
|
2465
|
+
];
|
|
2466
|
+
});
|
|
2467
|
+
}
|
|
2468
|
+
function buildInstallRows(home, resourcesRoot) {
|
|
2469
|
+
const enabled = loadCliSettings(home).enabled;
|
|
2470
|
+
const manifest = loadSkillsManifest(resourcesRoot);
|
|
2471
|
+
const rows = [];
|
|
2472
|
+
for (const cli of enabled) {
|
|
2473
|
+
const target = CLI_TARGETS[cli];
|
|
2474
|
+
if (target === void 0) continue;
|
|
2475
|
+
rows.push([
|
|
2476
|
+
"Server",
|
|
2477
|
+
MCP_SERVER_NAME,
|
|
2478
|
+
target.id,
|
|
2479
|
+
"yes",
|
|
2480
|
+
isServerRegistered(target, MCP_SERVER_NAME, home) ? STATUS_OK : STATUS_MISSING
|
|
2481
|
+
]);
|
|
2482
|
+
for (const skill of manifest) {
|
|
2483
|
+
rows.push([
|
|
2484
|
+
"Skill",
|
|
2485
|
+
skill.id,
|
|
2486
|
+
target.id,
|
|
2487
|
+
"yes",
|
|
2488
|
+
compareSkillTree(join5(resourcesRoot, skill.id), join5(target.skillsDir(home), skill.id), {
|
|
2489
|
+
...skill.debugPath === void 0 ? {} : { debugPath: skill.debugPath }
|
|
2490
|
+
})
|
|
2491
|
+
]);
|
|
2492
|
+
}
|
|
2493
|
+
}
|
|
2494
|
+
return rows;
|
|
2495
|
+
}
|
|
2496
|
+
function buildCliShowReport(options) {
|
|
2497
|
+
return [
|
|
2498
|
+
"CLI Configuration",
|
|
2499
|
+
renderAsciiTable(["CLI", "Enabled", "MCP Config Path", "Skills Config Path"], buildCliRows(options.home)),
|
|
2500
|
+
"",
|
|
2501
|
+
"Installation Status",
|
|
2502
|
+
renderAsciiTable(["Type", "Name", "CLI", "Enabled", "Installed"], buildInstallRows(options.home, options.resourcesRoot))
|
|
2503
|
+
].join("\n");
|
|
2504
|
+
}
|
|
2505
|
+
|
|
2506
|
+
// src/cli.ts
|
|
2507
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
2508
|
+
var __dirname = dirname3(__filename);
|
|
2509
|
+
function extractEnvFileOption(argv) {
|
|
2510
|
+
for (let i = 0; i < argv.length; i++) {
|
|
2511
|
+
const arg = argv[i];
|
|
2512
|
+
if (arg === "--env-file" && i + 1 < argv.length) {
|
|
2513
|
+
return argv[i + 1];
|
|
2514
|
+
}
|
|
2515
|
+
if (arg.startsWith("--env-file=")) {
|
|
2516
|
+
return arg.slice("--env-file=".length);
|
|
2517
|
+
}
|
|
2518
|
+
}
|
|
2519
|
+
return void 0;
|
|
2520
|
+
}
|
|
2521
|
+
function loadEnvironment(envFile) {
|
|
2522
|
+
if (envFile !== void 0) {
|
|
2523
|
+
const result = applyExplicitEnvFile(envFile);
|
|
2524
|
+
if (!result.ok) {
|
|
2525
|
+
process.stderr.write(JSON.stringify({ error: result.error }, null, 2) + "\n");
|
|
2526
|
+
process.exit(1);
|
|
2527
|
+
}
|
|
2528
|
+
return;
|
|
2529
|
+
}
|
|
2530
|
+
dotenvConfig2({ path: resolve5(process.cwd(), ".env") });
|
|
2531
|
+
dotenvConfig2({ path: resolve5(__dirname, "../.env") });
|
|
2532
|
+
}
|
|
2533
|
+
async function startServer(port, apiKey, host) {
|
|
2534
|
+
if (port) {
|
|
2535
|
+
process.env.MCP_SERVER_PORT = port.toString();
|
|
2536
|
+
}
|
|
2537
|
+
ApiKeyManager.getInstance().setDefaultApiKey(apiKey);
|
|
2538
|
+
if (host) {
|
|
2539
|
+
process.env.MCP_SERVER_HOST = host;
|
|
2540
|
+
}
|
|
2541
|
+
Logger.log("\u{1F680} Starting Google Maps MCP Server...");
|
|
2542
|
+
Logger.log("\u{1F4CD} 18 tools registered (set GOOGLE_MAPS_ENABLED_TOOLS to limit)");
|
|
2543
|
+
Logger.log(
|
|
2544
|
+
"\u2139\uFE0F Reminder: enable Places API (New) in https://console.cloud.google.com before using the new Place features."
|
|
2545
|
+
);
|
|
2546
|
+
Logger.log("");
|
|
2547
|
+
const servers = [];
|
|
2548
|
+
let started = 0;
|
|
2549
|
+
let failed = 0;
|
|
2550
|
+
await Promise.all(
|
|
2551
|
+
config_default.map(async (config) => {
|
|
2552
|
+
const portString = process.env[config.portEnvVar];
|
|
2553
|
+
if (!portString) {
|
|
2554
|
+
Logger.error(`\u26A0\uFE0F [${config.name}] Port environment variable ${config.portEnvVar} not set.`);
|
|
2555
|
+
Logger.log(`\u{1F4A1} Please set ${config.portEnvVar} in your .env file or use --port parameter.`);
|
|
2556
|
+
Logger.log(` Example: ${config.portEnvVar}=3000 or --port 3000`);
|
|
2557
|
+
failed++;
|
|
2558
|
+
return;
|
|
2559
|
+
}
|
|
2560
|
+
const serverPort = Number(portString);
|
|
2561
|
+
if (!Number.isInteger(serverPort) || serverPort < 1 || serverPort > 65535) {
|
|
2562
|
+
Logger.error(
|
|
2563
|
+
`\u274C [${config.name}] Invalid port number "${portString}" defined in ${config.portEnvVar} (expected integer 1-65535).`
|
|
2564
|
+
);
|
|
2565
|
+
failed++;
|
|
2566
|
+
return;
|
|
2567
|
+
}
|
|
2568
|
+
try {
|
|
2569
|
+
const activeTools = filterTools(config.tools);
|
|
2570
|
+
const server = new BaseMcpServer(config.name, activeTools);
|
|
2571
|
+
const serverHost = process.env.MCP_SERVER_HOST || "127.0.0.1";
|
|
2572
|
+
Logger.log(`\u{1F527} [${config.name}] Initializing MCP Server in HTTP mode on ${serverHost}:${serverPort}...`);
|
|
2573
|
+
await server.startHttpServer(serverPort, serverHost);
|
|
2574
|
+
servers.push(server);
|
|
2575
|
+
started++;
|
|
2576
|
+
const displayHost = serverHost === "0.0.0.0" ? "localhost" : serverHost;
|
|
2577
|
+
Logger.log(`\u2705 [${config.name}] MCP Server started successfully!`);
|
|
2578
|
+
Logger.log(` \u{1F310} Endpoint: http://${displayHost}:${serverPort}/mcp`);
|
|
2579
|
+
Logger.log(` \u{1F4DA} Tools: ${activeTools.length} available`);
|
|
2580
|
+
} catch (error) {
|
|
2581
|
+
Logger.error(`\u274C [${config.name}] Failed to start MCP Server on port ${serverPort}:`, error);
|
|
2582
|
+
failed++;
|
|
2583
|
+
}
|
|
2584
|
+
})
|
|
2585
|
+
);
|
|
2586
|
+
if (started > 0) {
|
|
2587
|
+
installShutdownHandlers(servers);
|
|
2588
|
+
}
|
|
2589
|
+
Logger.log("");
|
|
2590
|
+
Logger.log(started > 0 ? "\u{1F389} Server initialization completed!" : "\u274C No MCP server instance started.");
|
|
2591
|
+
Logger.log("\u{1F4A1} Need help? Check the README.md for configuration details.");
|
|
2592
|
+
return { started, failed, servers };
|
|
2593
|
+
}
|
|
2594
|
+
function createShutdownHandler(servers, exitHook = (code) => process.exit(code)) {
|
|
2595
|
+
let invoked = false;
|
|
2596
|
+
return async (signal) => {
|
|
2597
|
+
if (invoked) {
|
|
2598
|
+
return;
|
|
2599
|
+
}
|
|
2600
|
+
invoked = true;
|
|
2601
|
+
Logger.log(`\u{1F4F6} Received ${signal}; shutting down MCP HTTP server...`);
|
|
2602
|
+
await Promise.allSettled(servers.map((server) => server.stopHttpServer()));
|
|
2603
|
+
exitHook(0);
|
|
2604
|
+
};
|
|
2605
|
+
}
|
|
2606
|
+
function installShutdownHandlers(servers) {
|
|
2607
|
+
const handler = createShutdownHandler(servers);
|
|
2608
|
+
process.on("SIGINT", () => void handler("SIGINT"));
|
|
2609
|
+
process.on("SIGTERM", () => void handler("SIGTERM"));
|
|
2610
|
+
}
|
|
2611
|
+
var EXEC_TOOLS = [
|
|
2612
|
+
"geocode",
|
|
2613
|
+
"reverse-geocode",
|
|
2614
|
+
"search-nearby",
|
|
2615
|
+
"search-places",
|
|
2616
|
+
"place-details",
|
|
2617
|
+
"directions",
|
|
2618
|
+
"distance-matrix",
|
|
2619
|
+
"elevation",
|
|
2620
|
+
"timezone",
|
|
2621
|
+
"weather",
|
|
2622
|
+
"explore-area",
|
|
2623
|
+
"plan-route",
|
|
2624
|
+
"compare-places",
|
|
2625
|
+
"air-quality",
|
|
2626
|
+
"static-map",
|
|
2627
|
+
"batch-geocode-tool",
|
|
2628
|
+
"search-along-route",
|
|
2629
|
+
"local-rank-tracker"
|
|
2630
|
+
];
|
|
2631
|
+
function isExecFailureResponse(result) {
|
|
2632
|
+
return typeof result === "object" && result !== null && "success" in result && result.success === false;
|
|
2633
|
+
}
|
|
2634
|
+
function getExecFailureMessage(toolName, result) {
|
|
2635
|
+
if (typeof result.error === "string" && result.error.trim().length > 0) {
|
|
2636
|
+
return result.error;
|
|
2637
|
+
}
|
|
2638
|
+
if (result.error !== void 0 && result.error !== null) {
|
|
2639
|
+
return String(result.error);
|
|
2640
|
+
}
|
|
2641
|
+
return `Tool "${toolName}" returned a failure response`;
|
|
2642
|
+
}
|
|
2643
|
+
async function execTool(toolName, params, apiKey) {
|
|
2644
|
+
const searcher = new PlacesSearcher(apiKey);
|
|
2645
|
+
switch (toolName) {
|
|
2646
|
+
case "geocode":
|
|
2647
|
+
case "maps_geocode":
|
|
2648
|
+
return searcher.geocode(params.address);
|
|
2649
|
+
case "reverse-geocode":
|
|
2650
|
+
case "maps_reverse_geocode":
|
|
2651
|
+
return searcher.reverseGeocode(params.latitude, params.longitude);
|
|
2652
|
+
case "search-nearby":
|
|
2653
|
+
case "search_nearby":
|
|
2654
|
+
case "maps_search_nearby":
|
|
2655
|
+
return searcher.searchNearby(params);
|
|
2656
|
+
case "search-places":
|
|
2657
|
+
case "maps_search_places":
|
|
2658
|
+
return searcher.searchText({
|
|
2659
|
+
query: params.query,
|
|
2660
|
+
locationBias: params.locationBias,
|
|
2661
|
+
openNow: params.openNow,
|
|
2662
|
+
minRating: params.minRating,
|
|
2663
|
+
includedType: params.includedType,
|
|
2664
|
+
strictTypeFiltering: params.strictTypeFiltering,
|
|
2665
|
+
priceLevels: params.priceLevels,
|
|
2666
|
+
pageSize: params.pageSize,
|
|
2667
|
+
pageToken: params.pageToken,
|
|
2668
|
+
languageCode: params.languageCode,
|
|
2669
|
+
includeRaw: params.includeRaw
|
|
2670
|
+
});
|
|
2671
|
+
case "place-details":
|
|
2672
|
+
case "get_place_details":
|
|
2673
|
+
case "maps_place_details":
|
|
2674
|
+
return searcher.getPlaceDetails(params.placeId, params.maxPhotos || 0, params.includeRaw || false);
|
|
2675
|
+
case "directions":
|
|
2676
|
+
case "maps_directions":
|
|
2677
|
+
return searcher.getDirections(
|
|
2678
|
+
params.origin,
|
|
2679
|
+
params.destination,
|
|
2680
|
+
params.mode,
|
|
2681
|
+
params.departure_time,
|
|
2682
|
+
params.arrival_time,
|
|
2683
|
+
params.avoid_tolls,
|
|
2684
|
+
params.avoid_highways
|
|
2685
|
+
);
|
|
2686
|
+
case "distance-matrix":
|
|
2687
|
+
case "maps_distance_matrix":
|
|
2688
|
+
return searcher.calculateDistanceMatrix(
|
|
2689
|
+
params.origins,
|
|
2690
|
+
params.destinations,
|
|
2691
|
+
params.mode,
|
|
2692
|
+
params.departure_time,
|
|
2693
|
+
params.avoid_tolls,
|
|
2694
|
+
params.avoid_highways
|
|
2695
|
+
);
|
|
2696
|
+
case "elevation":
|
|
2697
|
+
case "maps_elevation":
|
|
2698
|
+
return searcher.getElevation(params.locations);
|
|
2699
|
+
case "timezone":
|
|
2700
|
+
case "maps_timezone":
|
|
2701
|
+
return searcher.getTimezone(params.latitude, params.longitude, params.timestamp);
|
|
2702
|
+
case "weather":
|
|
2703
|
+
case "maps_weather":
|
|
2704
|
+
return searcher.getWeather(
|
|
2705
|
+
params.latitude,
|
|
2706
|
+
params.longitude,
|
|
2707
|
+
params.type,
|
|
2708
|
+
params.forecastDays,
|
|
2709
|
+
params.forecastHours
|
|
2710
|
+
);
|
|
2711
|
+
case "explore-area":
|
|
2712
|
+
case "maps_explore_area":
|
|
2713
|
+
return searcher.exploreArea(params);
|
|
2714
|
+
case "plan-route":
|
|
2715
|
+
case "maps_plan_route":
|
|
2716
|
+
return searcher.planRoute(params);
|
|
2717
|
+
case "compare-places":
|
|
2718
|
+
case "maps_compare_places":
|
|
2719
|
+
return searcher.comparePlaces(params);
|
|
2720
|
+
case "air-quality":
|
|
2721
|
+
case "maps_air_quality":
|
|
2722
|
+
return searcher.getAirQuality(
|
|
2723
|
+
params.latitude,
|
|
2724
|
+
params.longitude,
|
|
2725
|
+
params.includeHealthRecommendations,
|
|
2726
|
+
params.includePollutants
|
|
2727
|
+
);
|
|
2728
|
+
case "static-map":
|
|
2729
|
+
case "maps_static_map":
|
|
2730
|
+
return searcher.getStaticMap(params);
|
|
2731
|
+
case "batch-geocode-tool":
|
|
2732
|
+
case "maps_batch_geocode": {
|
|
2733
|
+
const results = await Promise.all(
|
|
2734
|
+
params.addresses.map(async (address) => {
|
|
2735
|
+
try {
|
|
2736
|
+
const result = await searcher.geocode(address);
|
|
2737
|
+
return { address, ...result };
|
|
2738
|
+
} catch (error) {
|
|
2739
|
+
return { address, success: false, error: error.message };
|
|
2740
|
+
}
|
|
2741
|
+
})
|
|
2742
|
+
);
|
|
2743
|
+
const succeeded = results.filter((r) => r.success).length;
|
|
2744
|
+
return {
|
|
2745
|
+
success: true,
|
|
2746
|
+
data: { total: params.addresses.length, succeeded, failed: params.addresses.length - succeeded, results }
|
|
2747
|
+
};
|
|
2748
|
+
}
|
|
2749
|
+
case "search-along-route":
|
|
2750
|
+
case "maps_search_along_route":
|
|
2751
|
+
return searcher.searchAlongRoute(params);
|
|
2752
|
+
case "local-rank-tracker":
|
|
2753
|
+
case "maps_local_rank_tracker":
|
|
2754
|
+
return searcher.localRankTracker(params);
|
|
2755
|
+
default:
|
|
2756
|
+
throw new Error(`Unknown tool: ${toolName}. Available: ${EXEC_TOOLS.join(", ")}`);
|
|
2757
|
+
}
|
|
2758
|
+
}
|
|
2759
|
+
async function runExecCommand(toolName, params, apiKey, runner = execTool, streams = { stdout: process.stdout, stderr: process.stderr }) {
|
|
2760
|
+
try {
|
|
2761
|
+
const result = await runner(toolName, params, apiKey);
|
|
2762
|
+
if (isExecFailureResponse(result)) {
|
|
2763
|
+
streams.stderr.write(JSON.stringify({ error: getExecFailureMessage(toolName, result) }, null, 2) + "\n");
|
|
2764
|
+
return 1;
|
|
2765
|
+
}
|
|
2766
|
+
streams.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
2767
|
+
return 0;
|
|
2768
|
+
} catch (error) {
|
|
2769
|
+
streams.stderr.write(
|
|
2770
|
+
JSON.stringify({ error: error instanceof Error ? error.message : String(error) }, null, 2) + "\n"
|
|
2771
|
+
);
|
|
2772
|
+
return 1;
|
|
2773
|
+
}
|
|
2774
|
+
}
|
|
2775
|
+
var DOCTOR_PROBES = [
|
|
2776
|
+
{ id: "geocoding", name: "geocoding-api", tool: "geocode", params: { address: "Tokyo Tower" } },
|
|
2777
|
+
{ id: "places-new", name: "places-api-new", tool: "search-places", params: { query: "Tokyo Tower" } },
|
|
2778
|
+
{
|
|
2779
|
+
id: "routes",
|
|
2780
|
+
name: "routes-api",
|
|
2781
|
+
tool: "directions",
|
|
2782
|
+
params: { origin: "Tokyo Tower", destination: "Tokyo Station", mode: "walking" }
|
|
2783
|
+
}
|
|
2784
|
+
];
|
|
2785
|
+
var DOCTOR_PROBE_ALIASES = {
|
|
2786
|
+
geocoding: "geocoding",
|
|
2787
|
+
"geocoding-api": "geocoding",
|
|
2788
|
+
"places-new": "places-new",
|
|
2789
|
+
places: "places-new",
|
|
2790
|
+
"places-api-new": "places-new",
|
|
2791
|
+
routes: "routes",
|
|
2792
|
+
"routes-api": "routes"
|
|
2793
|
+
};
|
|
2794
|
+
function parseDoctorProbes(raw) {
|
|
2795
|
+
if (raw === void 0) {
|
|
2796
|
+
return { probes: void 0 };
|
|
2797
|
+
}
|
|
2798
|
+
const items = raw.split(",").map((item) => item.trim().toLowerCase()).filter(Boolean);
|
|
2799
|
+
if (items.length === 0) {
|
|
2800
|
+
return { probes: void 0, error: "--probes received no probe names (expected: geocoding, places-new, routes)" };
|
|
2801
|
+
}
|
|
2802
|
+
const probes = [];
|
|
2803
|
+
for (const item of items) {
|
|
2804
|
+
const id = DOCTOR_PROBE_ALIASES[item];
|
|
2805
|
+
if (!id) {
|
|
2806
|
+
return { probes: [], error: `unknown probe "${item}" (expected: geocoding, places-new, routes)` };
|
|
2807
|
+
}
|
|
2808
|
+
if (!probes.includes(id)) {
|
|
2809
|
+
probes.push(id);
|
|
2810
|
+
}
|
|
2811
|
+
}
|
|
2812
|
+
return { probes };
|
|
2813
|
+
}
|
|
2814
|
+
async function runDoctor(packageVersion, apiKey, live = false, runner = execTool, options = {}) {
|
|
2815
|
+
const nodeVersion = options.nodeVersion ?? process.versions.node;
|
|
2816
|
+
const nodeMajor = Number.parseInt(nodeVersion.split(".")[0] ?? "0", 10);
|
|
2817
|
+
const config = options.config ?? resolveRuntimeConfig(process.env);
|
|
2818
|
+
const allToolNames = options.allToolNames ?? config_default[0].tools.map((tool) => tool.name);
|
|
2819
|
+
const toolsResolution = resolveEnabledToolNames(allToolNames, config.enabledToolsRaw);
|
|
2820
|
+
const diagnostics = describeRuntimeConfig(config);
|
|
2821
|
+
const toolsProfile = describeToolsProfile(toolsResolution.names);
|
|
2822
|
+
const checks = [
|
|
2823
|
+
{
|
|
2824
|
+
name: "node",
|
|
2825
|
+
status: nodeMajor >= NODE_ENGINE_FLOOR ? "pass" : "fail",
|
|
2826
|
+
detail: `Node.js ${nodeVersion} detected; version ${NODE_ENGINE_FLOOR} or newer is required (package.json engines).`
|
|
2827
|
+
},
|
|
2828
|
+
{
|
|
2829
|
+
name: "package",
|
|
2830
|
+
status: packageVersion !== "0.0.0" ? "pass" : "fail",
|
|
2831
|
+
detail: packageVersion !== "0.0.0" ? `mcp-google-api ${packageVersion} is available.` : "Could not read the installed package version."
|
|
2832
|
+
},
|
|
2833
|
+
{
|
|
2834
|
+
name: "api-key",
|
|
2835
|
+
status: apiKey ? "pass" : "fail",
|
|
2836
|
+
detail: apiKey ? "GOOGLE_MAPS_API_KEY is configured (value hidden)." : "GOOGLE_MAPS_API_KEY is not configured. Set it in the environment before making API calls."
|
|
2837
|
+
},
|
|
2838
|
+
{
|
|
2839
|
+
name: "config",
|
|
2840
|
+
status: toolsResolution.fallback ? "fail" : "pass",
|
|
2841
|
+
detail: toolsResolution.fallback ? `GOOGLE_MAPS_ENABLED_TOOLS matched 0 tools; the server falls back to all ${allToolNames.length} tools. Effective configuration: ${formatConfigDiagnostics(diagnostics)}` : `Effective configuration: ${formatConfigDiagnostics(diagnostics)}`
|
|
2842
|
+
},
|
|
2843
|
+
{
|
|
2844
|
+
name: "env-file",
|
|
2845
|
+
status: "pass",
|
|
2846
|
+
detail: config.envFile ? `Explicit environment file loaded: ${config.envFile}` : "No explicit --env-file requested; implicit .env discovery (current directory, then package directory) applies."
|
|
2847
|
+
},
|
|
2848
|
+
{
|
|
2849
|
+
name: "tools",
|
|
2850
|
+
status: "pass",
|
|
2851
|
+
detail: `${toolsProfile.toolCount} tool(s) enabled; Google APIs: ${toolsProfile.apis.join(", ") || "none"}; per-tool: ${toolsProfile.tools.map((tool) => `${tool.tool}[${tool.apis.join("+")}]`).join(", ")}`
|
|
2852
|
+
}
|
|
2853
|
+
];
|
|
2854
|
+
if (!live) {
|
|
2855
|
+
checks.push({
|
|
2856
|
+
name: "live-api",
|
|
2857
|
+
status: "skip",
|
|
2858
|
+
detail: "Live API checks were not requested; no Google Maps API calls were made."
|
|
2859
|
+
});
|
|
2860
|
+
} else if (!apiKey) {
|
|
2861
|
+
checks.push({
|
|
2862
|
+
name: "live-api",
|
|
2863
|
+
status: "skip",
|
|
2864
|
+
detail: "Live API checks require GOOGLE_MAPS_API_KEY."
|
|
2865
|
+
});
|
|
2866
|
+
} else {
|
|
2867
|
+
const selected = options.probes ? DOCTOR_PROBES.filter((probe) => options.probes.includes(probe.id)) : DOCTOR_PROBES;
|
|
2868
|
+
for (const check of selected) {
|
|
2869
|
+
try {
|
|
2870
|
+
const result = await runner(check.tool, check.params, apiKey);
|
|
2871
|
+
if (isExecFailureResponse(result)) {
|
|
2872
|
+
checks.push({
|
|
2873
|
+
name: check.name,
|
|
2874
|
+
status: "fail",
|
|
2875
|
+
detail: getExecFailureMessage(check.tool, result)
|
|
2876
|
+
});
|
|
2877
|
+
} else {
|
|
2878
|
+
checks.push({
|
|
2879
|
+
name: check.name,
|
|
2880
|
+
status: "pass",
|
|
2881
|
+
detail: `${check.name} request succeeded.`
|
|
2882
|
+
});
|
|
2883
|
+
}
|
|
2884
|
+
} catch (error) {
|
|
2885
|
+
checks.push({
|
|
2886
|
+
name: check.name,
|
|
2887
|
+
status: "fail",
|
|
2888
|
+
detail: error instanceof Error ? error.message : String(error)
|
|
2889
|
+
});
|
|
2890
|
+
}
|
|
2891
|
+
}
|
|
2892
|
+
}
|
|
2893
|
+
return {
|
|
2894
|
+
success: checks.every((check) => check.status !== "fail"),
|
|
2895
|
+
version: packageVersion,
|
|
2896
|
+
live,
|
|
2897
|
+
checks
|
|
2898
|
+
};
|
|
2899
|
+
}
|
|
2900
|
+
async function runSetup(options, deps = {}) {
|
|
2901
|
+
const fsImpl = deps.fs ?? {
|
|
2902
|
+
existsSync: (path) => existsSync4(path),
|
|
2903
|
+
readFileSync: (path, encoding) => readFileSync5(path, encoding),
|
|
2904
|
+
writeFileSync: (path, data, encoding) => writeFileSync4(path, data, encoding)
|
|
2905
|
+
};
|
|
2906
|
+
const nodeVersion = deps.nodeVersion ?? process.versions.node;
|
|
2907
|
+
const nodeMajor = Number.parseInt(nodeVersion.split(".")[0] ?? "0", 10);
|
|
2908
|
+
const config = deps.config ?? resolveRuntimeConfig(process.env);
|
|
2909
|
+
const examplePath = resolve5(options.packageDir, ".env.example");
|
|
2910
|
+
const envPath = resolve5(options.targetDir, ".env");
|
|
2911
|
+
const checks = [
|
|
2912
|
+
{
|
|
2913
|
+
name: "node",
|
|
2914
|
+
status: nodeMajor >= NODE_ENGINE_FLOOR ? "pass" : "fail",
|
|
2915
|
+
detail: `Node.js ${nodeVersion} detected; version ${NODE_ENGINE_FLOOR} or newer is required (package.json engines).`
|
|
2916
|
+
},
|
|
2917
|
+
{
|
|
2918
|
+
name: "env-template",
|
|
2919
|
+
status: fsImpl.existsSync(examplePath) ? "pass" : "fail",
|
|
2920
|
+
detail: fsImpl.existsSync(examplePath) ? `Template found: ${examplePath}` : `Template missing: ${examplePath} (expected next to the installed package).`
|
|
2921
|
+
}
|
|
2922
|
+
];
|
|
2923
|
+
let created = false;
|
|
2924
|
+
if (options.writeEnv) {
|
|
2925
|
+
if (fsImpl.existsSync(envPath)) {
|
|
2926
|
+
checks.push({
|
|
2927
|
+
name: "env-file",
|
|
2928
|
+
status: "pass",
|
|
2929
|
+
detail: `${envPath} already exists; left untouched (no overwrite).`
|
|
2930
|
+
});
|
|
2931
|
+
} else if (!fsImpl.existsSync(examplePath)) {
|
|
2932
|
+
checks.push({
|
|
2933
|
+
name: "env-file",
|
|
2934
|
+
status: "fail",
|
|
2935
|
+
detail: `Cannot create ${envPath}: template ${examplePath} is missing.`
|
|
2936
|
+
});
|
|
2937
|
+
} else {
|
|
2938
|
+
try {
|
|
2939
|
+
fsImpl.writeFileSync(envPath, fsImpl.readFileSync(examplePath, "utf-8"), "utf-8");
|
|
2940
|
+
created = true;
|
|
2941
|
+
checks.push({
|
|
2942
|
+
name: "env-file",
|
|
2943
|
+
status: "pass",
|
|
2944
|
+
detail: `Created ${envPath} from template; edit it and set GOOGLE_MAPS_API_KEY.`
|
|
2945
|
+
});
|
|
2946
|
+
} catch (error) {
|
|
2947
|
+
checks.push({
|
|
2948
|
+
name: "env-file",
|
|
2949
|
+
status: "fail",
|
|
2950
|
+
detail: `Failed to create ${envPath}: ${error instanceof Error ? error.message : String(error)}`
|
|
2951
|
+
});
|
|
2952
|
+
}
|
|
2953
|
+
}
|
|
2954
|
+
} else {
|
|
2955
|
+
checks.push({
|
|
2956
|
+
name: "env-file",
|
|
2957
|
+
status: fsImpl.existsSync(envPath) ? "pass" : "skip",
|
|
2958
|
+
detail: fsImpl.existsSync(envPath) ? `${envPath} found.` : `${envPath} not found; rerun with --write-env to create it from the template.`
|
|
2959
|
+
});
|
|
2960
|
+
}
|
|
2961
|
+
checks.push({
|
|
2962
|
+
name: "api-key",
|
|
2963
|
+
status: config.apiKey ? "pass" : "fail",
|
|
2964
|
+
detail: config.apiKey ? `GOOGLE_MAPS_API_KEY is configured (source: ${config.apiKeySource}, value hidden).` : "GOOGLE_MAPS_API_KEY is not configured. Set it in the environment or in .env after scaffolding."
|
|
2965
|
+
});
|
|
2966
|
+
return {
|
|
2967
|
+
success: checks.every((check) => check.status !== "fail"),
|
|
2968
|
+
created,
|
|
2969
|
+
checks
|
|
2970
|
+
};
|
|
2971
|
+
}
|
|
2972
|
+
function runSnippetCommand(options, streams = { stdout: process.stdout, stderr: process.stderr }) {
|
|
2973
|
+
try {
|
|
2974
|
+
const snippet = renderClientSnippet(options);
|
|
2975
|
+
streams.stdout.write(snippet.json + "\n");
|
|
2976
|
+
for (const note of snippet.notes) {
|
|
2977
|
+
streams.stderr.write(`[snippet] ${note}
|
|
2978
|
+
`);
|
|
2979
|
+
}
|
|
2980
|
+
return 0;
|
|
2981
|
+
} catch (error) {
|
|
2982
|
+
streams.stderr.write(
|
|
2983
|
+
JSON.stringify({ error: error instanceof Error ? error.message : String(error) }, null, 2) + "\n"
|
|
2984
|
+
);
|
|
2985
|
+
return 1;
|
|
2986
|
+
}
|
|
2987
|
+
}
|
|
2988
|
+
function runEnableDisableCliCommand(name, enable, options = {}) {
|
|
2989
|
+
const streams = options.streams ?? { stdout: process.stdout, stderr: process.stderr };
|
|
2990
|
+
const target = resolveCliTarget(name);
|
|
2991
|
+
if (target === void 0) {
|
|
2992
|
+
streams.stderr.write(
|
|
2993
|
+
JSON.stringify({ error: `Unsupported CLI: ${name}. Supported: ${supportedCliNames()}.` }, null, 2) + "\n"
|
|
2994
|
+
);
|
|
2995
|
+
return 2;
|
|
2996
|
+
}
|
|
2997
|
+
const settings = loadCliSettings(options.home);
|
|
2998
|
+
const already = settings.enabled.includes(target.id);
|
|
2999
|
+
if (enable === already) {
|
|
3000
|
+
streams.stdout.write(
|
|
3001
|
+
JSON.stringify({ cli: target.id, enabled: enable, message: `${target.id} is already ${enable ? "enabled" : "disabled"}.` }, null, 2) + "\n"
|
|
3002
|
+
);
|
|
3003
|
+
return 0;
|
|
3004
|
+
}
|
|
3005
|
+
settings.enabled = enable ? [...settings.enabled, target.id] : settings.enabled.filter((item) => item !== target.id);
|
|
3006
|
+
saveCliSettings(settings, options.home);
|
|
3007
|
+
streams.stdout.write(
|
|
3008
|
+
JSON.stringify({ cli: target.id, enabled: enable, message: `${enable ? "Enabled" : "Disabled"} ${target.id}.` }, null, 2) + "\n"
|
|
3009
|
+
);
|
|
3010
|
+
return 0;
|
|
3011
|
+
}
|
|
3012
|
+
function runApiKeyCommand(key, options) {
|
|
3013
|
+
const streams = options.streams ?? { stdout: process.stdout, stderr: process.stderr };
|
|
3014
|
+
const trimmed = (key ?? "").trim();
|
|
3015
|
+
if (!ApiKeyManager.getInstance().isValidApiKeyFormat(trimmed)) {
|
|
3016
|
+
streams.stderr.write(
|
|
3017
|
+
"api-key: invalid Google Maps API key. Expected 20-50 characters of letters, digits, hyphens, or underscores.\nUsage: mcp-google-api api-key <your-api-key>\n"
|
|
3018
|
+
);
|
|
3019
|
+
return 1;
|
|
3020
|
+
}
|
|
3021
|
+
try {
|
|
3022
|
+
const settings = loadCliSettings(options.home);
|
|
3023
|
+
settings.apiKey = trimmed;
|
|
3024
|
+
saveCliSettings(settings, options.home);
|
|
3025
|
+
} catch (error) {
|
|
3026
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3027
|
+
streams.stderr.write(`api-key: failed to persist the API key: ${message}
|
|
3028
|
+
`);
|
|
3029
|
+
return 1;
|
|
3030
|
+
}
|
|
3031
|
+
streams.stdout.write(`API key configured (${maskSecret(trimmed)}).
|
|
3032
|
+
`);
|
|
3033
|
+
let install;
|
|
3034
|
+
try {
|
|
3035
|
+
install = installEnabledCliResources({ home: options.home, resourcesRoot: options.resourcesRoot });
|
|
3036
|
+
} catch (error) {
|
|
3037
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3038
|
+
streams.stderr.write(`api-key: install failed: ${message}
|
|
3039
|
+
`);
|
|
3040
|
+
return 1;
|
|
3041
|
+
}
|
|
3042
|
+
if (install.summaries.length === 0 && install.errors.length === 0) {
|
|
3043
|
+
streams.stdout.write("No CLI agent enabled. Run 'mcp-google-api enable <cli name>' first.\n");
|
|
3044
|
+
return 0;
|
|
3045
|
+
}
|
|
3046
|
+
for (const summary of install.summaries) {
|
|
3047
|
+
streams.stdout.write(
|
|
3048
|
+
`${summary.cli}: MCP connector installed (server entries: ${summary.server}, skills: ${summary.skills}).
|
|
3049
|
+
`
|
|
3050
|
+
);
|
|
3051
|
+
}
|
|
3052
|
+
for (const error of install.errors) {
|
|
3053
|
+
streams.stdout.write(`${error}
|
|
3054
|
+
`);
|
|
3055
|
+
}
|
|
3056
|
+
return install.errors.length > 0 ? 1 : 0;
|
|
3057
|
+
}
|
|
3058
|
+
function runAutoUpdateCommand(rawState, options = {}) {
|
|
3059
|
+
const streams = options.streams ?? { stdout: process.stdout, stderr: process.stderr };
|
|
3060
|
+
const normalized = (rawState ?? "").trim().toLowerCase();
|
|
3061
|
+
let enabled;
|
|
3062
|
+
if (normalized === "enable" || normalized === "1") {
|
|
3063
|
+
enabled = true;
|
|
3064
|
+
} else if (normalized === "disable" || normalized === "0") {
|
|
3065
|
+
enabled = false;
|
|
3066
|
+
} else {
|
|
3067
|
+
streams.stderr.write(
|
|
3068
|
+
`auto-update: invalid value "${rawState ?? ""}". Accepted values: enable, disable, 1, 0.
|
|
3069
|
+
`
|
|
3070
|
+
);
|
|
3071
|
+
return 1;
|
|
3072
|
+
}
|
|
3073
|
+
try {
|
|
3074
|
+
const settings = loadCliSettings(options.home);
|
|
3075
|
+
settings.autoUpdate = enabled;
|
|
3076
|
+
saveCliSettings(settings, options.home);
|
|
3077
|
+
} catch (error) {
|
|
3078
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3079
|
+
streams.stderr.write(`auto-update: failed to persist the setting: ${message}
|
|
3080
|
+
`);
|
|
3081
|
+
return 1;
|
|
3082
|
+
}
|
|
3083
|
+
streams.stdout.write(enabled ? "Auto-update check enabled.\n" : "Auto-update check disabled.\n");
|
|
3084
|
+
return 0;
|
|
3085
|
+
}
|
|
3086
|
+
function runInstallUninstallCommand(operation, options) {
|
|
3087
|
+
const streams = options.streams ?? { stdout: process.stdout, stderr: process.stderr };
|
|
3088
|
+
if (loadCliSettings(options.home).enabled.length === 0) {
|
|
3089
|
+
streams.stdout.write(
|
|
3090
|
+
JSON.stringify(
|
|
3091
|
+
{
|
|
3092
|
+
success: true,
|
|
3093
|
+
message: "No CLI enabled. Use 'mcp-google-api enable <cli name>' first.",
|
|
3094
|
+
summaries: [],
|
|
3095
|
+
errors: []
|
|
3096
|
+
},
|
|
3097
|
+
null,
|
|
3098
|
+
2
|
|
3099
|
+
) + "\n"
|
|
3100
|
+
);
|
|
3101
|
+
return 0;
|
|
3102
|
+
}
|
|
3103
|
+
let result;
|
|
3104
|
+
try {
|
|
3105
|
+
result = operation === "install" ? installEnabledCliResources({
|
|
3106
|
+
home: options.home,
|
|
3107
|
+
resourcesRoot: options.resourcesRoot,
|
|
3108
|
+
includeDebugSkills: options.includeDebugSkills === true
|
|
3109
|
+
}) : uninstallEnabledCliResources({ home: options.home, resourcesRoot: options.resourcesRoot });
|
|
3110
|
+
} catch (error) {
|
|
3111
|
+
streams.stderr.write(
|
|
3112
|
+
JSON.stringify({ success: false, errors: [error instanceof Error ? error.message : String(error)] }, null, 2) + "\n"
|
|
3113
|
+
);
|
|
3114
|
+
return 1;
|
|
3115
|
+
}
|
|
3116
|
+
streams.stdout.write(
|
|
3117
|
+
JSON.stringify({ success: result.errors.length === 0, operation, summaries: result.summaries, errors: result.errors }, null, 2) + "\n"
|
|
3118
|
+
);
|
|
3119
|
+
return result.errors.length > 0 ? 1 : 0;
|
|
3120
|
+
}
|
|
3121
|
+
function runShowCommand(options) {
|
|
3122
|
+
const streams = options.streams ?? { stdout: process.stdout, stderr: process.stderr };
|
|
3123
|
+
try {
|
|
3124
|
+
streams.stdout.write(buildCliShowReport(options) + "\n");
|
|
3125
|
+
return 0;
|
|
3126
|
+
} catch (error) {
|
|
3127
|
+
streams.stderr.write(
|
|
3128
|
+
JSON.stringify({ error: error instanceof Error ? error.message : String(error) }, null, 2) + "\n"
|
|
3129
|
+
);
|
|
3130
|
+
return 1;
|
|
3131
|
+
}
|
|
3132
|
+
}
|
|
3133
|
+
var isRunDirectly = process.argv[1] && (process.argv[1].endsWith("cli.ts") || process.argv[1].endsWith("cli.js") || process.argv[1].endsWith("mcp-google-api") || process.argv[1].includes("mcp-google-api"));
|
|
3134
|
+
var isMainModule = import.meta.url === `file://${process.argv[1]}`;
|
|
3135
|
+
if (isRunDirectly || isMainModule) {
|
|
3136
|
+
loadEnvironment(extractEnvFileOption(process.argv));
|
|
3137
|
+
let packageVersion = "0.0.0";
|
|
3138
|
+
try {
|
|
3139
|
+
const packageJsonPath = resolve5(__dirname, "../package.json");
|
|
3140
|
+
const packageJson = JSON.parse(readFileSync5(packageJsonPath, "utf-8"));
|
|
3141
|
+
packageVersion = packageJson.version;
|
|
3142
|
+
} catch (e) {
|
|
3143
|
+
packageVersion = "0.0.0";
|
|
3144
|
+
}
|
|
3145
|
+
const managementCommand = parseManagementCommand(hideBin(process.argv));
|
|
3146
|
+
if (managementCommand !== void 0) {
|
|
3147
|
+
void runManagementCommand(managementCommand, { currentVersion: packageVersion }).then((code) => {
|
|
3148
|
+
process.exitCode = code;
|
|
3149
|
+
}).catch((error) => {
|
|
3150
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3151
|
+
process.stderr.write(JSON.stringify({ error: message }, null, 2) + "\n");
|
|
3152
|
+
process.exitCode = 1;
|
|
3153
|
+
});
|
|
3154
|
+
} else {
|
|
3155
|
+
const buildServerCommandOptions = (yargs2) => yargs2.option("port", {
|
|
3156
|
+
alias: "p",
|
|
3157
|
+
type: "number",
|
|
3158
|
+
description: "Port to run the MCP server on",
|
|
3159
|
+
default: process.env.MCP_SERVER_PORT ? parseInt(process.env.MCP_SERVER_PORT) : 3e3
|
|
3160
|
+
}).option("host", {
|
|
3161
|
+
type: "string",
|
|
3162
|
+
description: "Hostname to bind the server to (default 127.0.0.1 loopback; 0.0.0.0 for all interfaces)",
|
|
3163
|
+
default: process.env.MCP_SERVER_HOST || "127.0.0.1"
|
|
3164
|
+
}).option("stdio", {
|
|
3165
|
+
type: "boolean",
|
|
3166
|
+
description: "Use stdio transport instead of HTTP",
|
|
3167
|
+
default: false
|
|
3168
|
+
}).example([
|
|
3169
|
+
["$0 start", "Start HTTP server with default settings"],
|
|
3170
|
+
["$0 start --host 0.0.0.0 --port 3000", "Start HTTP accessible from all interfaces"],
|
|
3171
|
+
["$0 start --stdio", "Start in stdio mode (for MCP clients)"],
|
|
3172
|
+
["$0 run --stdio", "Alias: run the server in stdio mode"]
|
|
3173
|
+
]);
|
|
3174
|
+
const runServerCommand = async (argv) => {
|
|
3175
|
+
const effectiveApiKey = loadConfiguredApiKey();
|
|
3176
|
+
ApiKeyManager.getInstance().setDefaultApiKey(effectiveApiKey);
|
|
3177
|
+
if (loadCliSettings().autoUpdate) {
|
|
3178
|
+
void checkVersionIdle({ currentVersion: packageVersion }).catch(() => void 0);
|
|
3179
|
+
}
|
|
3180
|
+
const tools = filterTools(config_default[0].tools);
|
|
3181
|
+
if (argv.stdio) {
|
|
3182
|
+
if (effectiveApiKey === void 0) {
|
|
3183
|
+
process.stderr.write(
|
|
3184
|
+
JSON.stringify(
|
|
3185
|
+
{
|
|
3186
|
+
error: "Google Maps API key is not configured. Run 'mcp-google-api api-key <your-api-key>' to configure it."
|
|
3187
|
+
},
|
|
3188
|
+
null,
|
|
3189
|
+
2
|
|
3190
|
+
) + "\n"
|
|
3191
|
+
);
|
|
3192
|
+
process.exitCode = 1;
|
|
3193
|
+
return;
|
|
3194
|
+
}
|
|
3195
|
+
const server = new BaseMcpServer(config_default[0].name, tools);
|
|
3196
|
+
await server.startStdio();
|
|
3197
|
+
} else {
|
|
3198
|
+
Logger.log("\u{1F5FA}\uFE0F Google Maps MCP Server");
|
|
3199
|
+
Logger.log(" A Model Context Protocol server for Google Maps services");
|
|
3200
|
+
Logger.log("");
|
|
3201
|
+
if (effectiveApiKey === void 0) {
|
|
3202
|
+
Logger.log("\u26A0\uFE0F Google Maps API Key not found!");
|
|
3203
|
+
Logger.log(" Run 'mcp-google-api api-key <your-api-key>' to configure it.");
|
|
3204
|
+
Logger.log("");
|
|
3205
|
+
}
|
|
3206
|
+
startServer(argv.port, effectiveApiKey, argv.host).then((outcome) => {
|
|
3207
|
+
if (outcome.started === 0) {
|
|
3208
|
+
process.exit(1);
|
|
3209
|
+
}
|
|
3210
|
+
}).catch((error) => {
|
|
3211
|
+
Logger.error("\u274C Failed to start server:", error);
|
|
3212
|
+
process.exit(1);
|
|
3213
|
+
});
|
|
3214
|
+
}
|
|
3215
|
+
};
|
|
3216
|
+
const cli = yargs(hideBin(process.argv)).scriptName("mcp-google-api").usage(
|
|
3217
|
+
`mcp-google-api v${packageVersion}
|
|
3218
|
+
|
|
3219
|
+
Usage: mcp-google-api <command> [options]
|
|
3220
|
+
mcp-google-api start|run [--stdio] [options]`
|
|
3221
|
+
).command(
|
|
3222
|
+
"doctor",
|
|
3223
|
+
"Check standalone CLI readiness without starting an MCP server",
|
|
3224
|
+
(yargs2) => yargs2.option("live", {
|
|
3225
|
+
type: "boolean",
|
|
3226
|
+
default: false,
|
|
3227
|
+
description: "Make billable test requests to Geocoding, Places (New), and Routes APIs"
|
|
3228
|
+
}).option("probes", {
|
|
3229
|
+
type: "string",
|
|
3230
|
+
description: "Comma-separated subset of live probes: geocoding, places-new, routes (default: all three)"
|
|
3231
|
+
}).option("apikey", {
|
|
3232
|
+
alias: "k",
|
|
3233
|
+
type: "string",
|
|
3234
|
+
description: "Google Maps API key (prefer GOOGLE_MAPS_API_KEY to avoid shell history exposure)",
|
|
3235
|
+
default: process.env.GOOGLE_MAPS_API_KEY
|
|
3236
|
+
}).example([
|
|
3237
|
+
["$0 doctor", "Check local runtime and API key configuration without network calls"],
|
|
3238
|
+
["$0 doctor --live", "Also verify three Google Maps APIs with billable requests"],
|
|
3239
|
+
["$0 doctor --live --probes geocoding,routes", "Probe only the selected APIs"]
|
|
3240
|
+
]),
|
|
3241
|
+
async (argv) => {
|
|
3242
|
+
const parsedProbes = parseDoctorProbes(argv.probes);
|
|
3243
|
+
if (parsedProbes.error) {
|
|
3244
|
+
process.stderr.write(JSON.stringify({ error: parsedProbes.error }, null, 2) + "\n");
|
|
3245
|
+
process.exitCode = 1;
|
|
3246
|
+
return;
|
|
3247
|
+
}
|
|
3248
|
+
const report = await runDoctor(packageVersion, argv.apikey, argv.live, void 0, {
|
|
3249
|
+
probes: parsedProbes.probes,
|
|
3250
|
+
config: resolveRuntimeConfig(process.env, { envFile: extractEnvFileOption(process.argv) })
|
|
3251
|
+
});
|
|
3252
|
+
process.stdout.write(JSON.stringify(report, null, 2) + "\n");
|
|
3253
|
+
process.exitCode = report.success ? 0 : 1;
|
|
3254
|
+
}
|
|
3255
|
+
).command(
|
|
3256
|
+
"setup",
|
|
3257
|
+
"Non-interactive onboarding: verify local readiness, optionally scaffold .env",
|
|
3258
|
+
(yargs2) => yargs2.option("write-env", {
|
|
3259
|
+
type: "boolean",
|
|
3260
|
+
default: false,
|
|
3261
|
+
description: "Create .env from .env.example in --env-dir when it does not exist (never overwrites)"
|
|
3262
|
+
}).option("env-dir", {
|
|
3263
|
+
type: "string",
|
|
3264
|
+
default: process.cwd(),
|
|
3265
|
+
description: "Directory where .env is looked up / created"
|
|
3266
|
+
}).example([
|
|
3267
|
+
["$0 setup", "Check readiness without writing anything"],
|
|
3268
|
+
["$0 setup --write-env", "Also scaffold .env from the package template"]
|
|
3269
|
+
]),
|
|
3270
|
+
async (argv) => {
|
|
3271
|
+
const report = await runSetup({
|
|
3272
|
+
packageDir: resolve5(__dirname, ".."),
|
|
3273
|
+
targetDir: argv.envDir,
|
|
3274
|
+
writeEnv: argv.writeEnv
|
|
3275
|
+
});
|
|
3276
|
+
process.stdout.write(JSON.stringify(report, null, 2) + "\n");
|
|
3277
|
+
process.exitCode = report.success ? 0 : 1;
|
|
3278
|
+
}
|
|
3279
|
+
).command(
|
|
3280
|
+
"snippet",
|
|
3281
|
+
"Print an MCP client registration snippet (JSON on stdout; never writes files)",
|
|
3282
|
+
(yargs2) => yargs2.option("client", {
|
|
3283
|
+
type: "string",
|
|
3284
|
+
choices: ["generic", "claude", "cursor", "pi"],
|
|
3285
|
+
default: "generic",
|
|
3286
|
+
description: "Target client (selects the documented target file)"
|
|
3287
|
+
}).option("transport", {
|
|
3288
|
+
type: "string",
|
|
3289
|
+
choices: ["stdio", "http"],
|
|
3290
|
+
default: "stdio",
|
|
3291
|
+
description: "MCP transport of the snippet"
|
|
3292
|
+
}).option("launcher", {
|
|
3293
|
+
type: "string",
|
|
3294
|
+
choices: ["npx", "node", "bin"],
|
|
3295
|
+
default: "npx",
|
|
3296
|
+
description: "stdio launcher command"
|
|
3297
|
+
}).option("command", {
|
|
3298
|
+
type: "string",
|
|
3299
|
+
description: "Absolute path to dist/cli.js (required for --launcher node)"
|
|
3300
|
+
}).option("host", {
|
|
3301
|
+
type: "string",
|
|
3302
|
+
default: "127.0.0.1",
|
|
3303
|
+
description: "Host used in the URL (transport http)"
|
|
3304
|
+
}).option("port", {
|
|
3305
|
+
type: "number",
|
|
3306
|
+
default: 3e3,
|
|
3307
|
+
description: "Port used in the URL (transport http)"
|
|
3308
|
+
}).option("env-key", {
|
|
3309
|
+
type: "string",
|
|
3310
|
+
description: "Emit a ${NAME} variable reference instead of a literal placeholder (expansion is client-specific)"
|
|
3311
|
+
}).option("name", {
|
|
3312
|
+
type: "string",
|
|
3313
|
+
default: "mcp-google-api",
|
|
3314
|
+
description: "Server name inside mcpServers"
|
|
3315
|
+
}).example([
|
|
3316
|
+
["$0 snippet --client claude", "stdio snippet using the npx launcher"],
|
|
3317
|
+
["$0 snippet --client pi --launcher node --command /abs/path/dist/cli.js", "stdio snippet with an explicit artifact"],
|
|
3318
|
+
["$0 snippet --transport http --port 3100", "HTTP URL snippet"]
|
|
3319
|
+
]),
|
|
3320
|
+
(argv) => {
|
|
3321
|
+
process.exitCode = runSnippetCommand({
|
|
3322
|
+
client: argv.client,
|
|
3323
|
+
transport: argv.transport,
|
|
3324
|
+
launcher: argv.launcher,
|
|
3325
|
+
commandPath: argv.command,
|
|
3326
|
+
host: argv.host,
|
|
3327
|
+
port: argv.port,
|
|
3328
|
+
envKey: argv.envKey,
|
|
3329
|
+
serverName: argv.name
|
|
3330
|
+
});
|
|
3331
|
+
}
|
|
3332
|
+
).command(
|
|
3333
|
+
"enable <cli>",
|
|
3334
|
+
"Enable MCP-server and skill installation for a supported CLI",
|
|
3335
|
+
(yargs2) => yargs2.positional("cli", {
|
|
3336
|
+
type: "string",
|
|
3337
|
+
describe: `CLI name: ${supportedCliNames()}`
|
|
3338
|
+
}).option("home", {
|
|
3339
|
+
type: "string",
|
|
3340
|
+
description: "Home-directory override for the settings file (default: user home)"
|
|
3341
|
+
}).example([["$0 enable pi", "Enable pi for install/uninstall/show"]]),
|
|
3342
|
+
(argv) => {
|
|
3343
|
+
process.exitCode = runEnableDisableCliCommand(argv.cli, true, {
|
|
3344
|
+
home: argv.home
|
|
3345
|
+
});
|
|
3346
|
+
}
|
|
3347
|
+
).command(
|
|
3348
|
+
"disable <cli>",
|
|
3349
|
+
"Disable MCP-server and skill installation for a supported CLI",
|
|
3350
|
+
(yargs2) => yargs2.positional("cli", {
|
|
3351
|
+
type: "string",
|
|
3352
|
+
describe: `CLI name: ${supportedCliNames()}`
|
|
3353
|
+
}).option("home", {
|
|
3354
|
+
type: "string",
|
|
3355
|
+
description: "Home-directory override for the settings file (default: user home)"
|
|
3356
|
+
}).example([["$0 disable claude", "Disable claude for install/uninstall/show"]]),
|
|
3357
|
+
(argv) => {
|
|
3358
|
+
process.exitCode = runEnableDisableCliCommand(argv.cli, false, {
|
|
3359
|
+
home: argv.home
|
|
3360
|
+
});
|
|
3361
|
+
}
|
|
3362
|
+
).command(
|
|
3363
|
+
"auto-update <state>",
|
|
3364
|
+
"Enable or disable the automatic update check in the persisted configuration",
|
|
3365
|
+
(yargs2) => yargs2.positional("state", {
|
|
3366
|
+
type: "string",
|
|
3367
|
+
describe: "New state: enable or disable (1 and 0 accepted)"
|
|
3368
|
+
}).option("home", {
|
|
3369
|
+
type: "string",
|
|
3370
|
+
description: "Home-directory override for the settings file (default: user home)"
|
|
3371
|
+
}).example([[
|
|
3372
|
+
"$0 auto-update disable",
|
|
3373
|
+
"Disable the automatic update check in the persisted configuration"
|
|
3374
|
+
]]),
|
|
3375
|
+
(argv) => {
|
|
3376
|
+
process.exitCode = runAutoUpdateCommand(argv.state, {
|
|
3377
|
+
home: argv.home
|
|
3378
|
+
});
|
|
3379
|
+
}
|
|
3380
|
+
).command(
|
|
3381
|
+
"api-key <key>",
|
|
3382
|
+
"Save the Google Maps API key in the user settings and refresh every enabled CLI registration",
|
|
3383
|
+
(yargs2) => yargs2.positional("key", {
|
|
3384
|
+
type: "string",
|
|
3385
|
+
describe: "Google Maps API key (20-50 characters: letters, digits, hyphens, underscores)"
|
|
3386
|
+
}).option("home", {
|
|
3387
|
+
type: "string",
|
|
3388
|
+
description: "Home-directory override for CLI configuration paths (default: user home)"
|
|
3389
|
+
}).example([["$0 api-key AIzaSy...", "Save the key and reinstall every enabled CLI"]]),
|
|
3390
|
+
(argv) => {
|
|
3391
|
+
process.exitCode = runApiKeyCommand(argv.key, {
|
|
3392
|
+
home: argv.home,
|
|
3393
|
+
resourcesRoot: resolveSkillsResourcesRoot(resolve5(__dirname, ".."))
|
|
3394
|
+
});
|
|
3395
|
+
}
|
|
3396
|
+
).command(
|
|
3397
|
+
"install",
|
|
3398
|
+
"Register the mcp-google-api stdio server and install packaged skills for every enabled CLI",
|
|
3399
|
+
(yargs2) => yargs2.option("home", {
|
|
3400
|
+
type: "string",
|
|
3401
|
+
description: "Home-directory override for CLI configuration paths (default: user home)"
|
|
3402
|
+
}).option("debug-skills", {
|
|
3403
|
+
type: "boolean",
|
|
3404
|
+
description: "Append each skill's packaged debug overlay to the installed SKILL.md",
|
|
3405
|
+
default: false
|
|
3406
|
+
}).example([
|
|
3407
|
+
["$0 install", "Register the server and skills in every enabled CLI"],
|
|
3408
|
+
["$0 install --debug-skills", "Also append the packaged debug overlay to every installed skill"]
|
|
3409
|
+
]),
|
|
3410
|
+
(argv) => {
|
|
3411
|
+
process.exitCode = runInstallUninstallCommand("install", {
|
|
3412
|
+
home: argv.home,
|
|
3413
|
+
resourcesRoot: resolveSkillsResourcesRoot(resolve5(__dirname, "..")),
|
|
3414
|
+
includeDebugSkills: argv.debugSkills === true
|
|
3415
|
+
});
|
|
3416
|
+
}
|
|
3417
|
+
).command(
|
|
3418
|
+
"uninstall",
|
|
3419
|
+
"Remove the mcp-google-api server entry and managed skills from every enabled CLI",
|
|
3420
|
+
(yargs2) => yargs2.option("home", {
|
|
3421
|
+
type: "string",
|
|
3422
|
+
description: "Home-directory override for CLI configuration paths (default: user home)"
|
|
3423
|
+
}).example([["$0 uninstall", "Reciprocal removal of install for every enabled CLI"]]),
|
|
3424
|
+
(argv) => {
|
|
3425
|
+
process.exitCode = runInstallUninstallCommand("uninstall", {
|
|
3426
|
+
home: argv.home,
|
|
3427
|
+
resourcesRoot: resolveSkillsResourcesRoot(resolve5(__dirname, ".."))
|
|
3428
|
+
});
|
|
3429
|
+
}
|
|
3430
|
+
).command(
|
|
3431
|
+
"show",
|
|
3432
|
+
"Show CLI configuration and installation status tables (human-readable)",
|
|
3433
|
+
(yargs2) => yargs2.option("home", {
|
|
3434
|
+
type: "string",
|
|
3435
|
+
description: "Home-directory override for CLI configuration paths (default: user home)"
|
|
3436
|
+
}).example([["$0 show", "Print CLI Configuration and Installation Status tables"]]),
|
|
3437
|
+
(argv) => {
|
|
3438
|
+
process.exitCode = runShowCommand({
|
|
3439
|
+
home: argv.home,
|
|
3440
|
+
resourcesRoot: resolveSkillsResourcesRoot(resolve5(__dirname, ".."))
|
|
3441
|
+
});
|
|
3442
|
+
}
|
|
3443
|
+
).command(
|
|
3444
|
+
"exec <tool> [params]",
|
|
3445
|
+
"Execute a tool directly and output JSON",
|
|
3446
|
+
(yargs2) => {
|
|
3447
|
+
return yargs2.positional("tool", {
|
|
3448
|
+
type: "string",
|
|
3449
|
+
describe: `Tool name: ${EXEC_TOOLS.join(", ")}`
|
|
3450
|
+
}).positional("params", {
|
|
3451
|
+
type: "string",
|
|
3452
|
+
describe: "JSON parameters string"
|
|
3453
|
+
}).example([
|
|
3454
|
+
[`$0 exec geocode '{"address":"Tokyo Tower"}'`, "Geocode an address"],
|
|
3455
|
+
[
|
|
3456
|
+
`$0 exec search-nearby '{"center":{"value":"35.68,139.74","isCoordinates":true},"keyword":"restaurant"}'`,
|
|
3457
|
+
"Search nearby"
|
|
3458
|
+
],
|
|
3459
|
+
[`$0 exec search-places '{"query":"ramen in Tokyo"}'`, "Text search"]
|
|
3460
|
+
]);
|
|
3461
|
+
},
|
|
3462
|
+
async (argv) => {
|
|
3463
|
+
const apiKey = loadConfiguredApiKey();
|
|
3464
|
+
if (!apiKey) {
|
|
3465
|
+
process.stderr.write(
|
|
3466
|
+
JSON.stringify(
|
|
3467
|
+
{
|
|
3468
|
+
error: "Google Maps API key is not configured. Run 'mcp-google-api api-key <your-api-key>' to configure it."
|
|
3469
|
+
},
|
|
3470
|
+
null,
|
|
3471
|
+
2
|
|
3472
|
+
) + "\n"
|
|
3473
|
+
);
|
|
3474
|
+
process.exitCode = 1;
|
|
3475
|
+
return;
|
|
3476
|
+
}
|
|
3477
|
+
try {
|
|
3478
|
+
const params = argv.params ? JSON.parse(argv.params) : {};
|
|
3479
|
+
process.exitCode = await runExecCommand(argv.tool, params, apiKey);
|
|
3480
|
+
} catch (error) {
|
|
3481
|
+
process.stderr.write(JSON.stringify({ error: error.message }, null, 2) + "\n");
|
|
3482
|
+
process.exitCode = 1;
|
|
3483
|
+
}
|
|
3484
|
+
}
|
|
3485
|
+
).command(
|
|
3486
|
+
"batch-geocode",
|
|
3487
|
+
"Geocode multiple addresses from a file (one address per line)",
|
|
3488
|
+
(yargs2) => {
|
|
3489
|
+
return yargs2.option("input", {
|
|
3490
|
+
alias: "i",
|
|
3491
|
+
type: "string",
|
|
3492
|
+
describe: "Input file path (one address per line). Use - for stdin.",
|
|
3493
|
+
demandOption: true
|
|
3494
|
+
}).option("output", {
|
|
3495
|
+
alias: "o",
|
|
3496
|
+
type: "string",
|
|
3497
|
+
describe: "Output file path (JSON). Defaults to stdout."
|
|
3498
|
+
}).option("concurrency", {
|
|
3499
|
+
alias: "c",
|
|
3500
|
+
type: "number",
|
|
3501
|
+
describe: "Max parallel requests",
|
|
3502
|
+
default: 20
|
|
3503
|
+
}).example([
|
|
3504
|
+
["$0 batch-geocode -i addresses.txt", "Geocode to stdout"],
|
|
3505
|
+
["$0 batch-geocode -i addresses.txt -o results.json", "Geocode to file"],
|
|
3506
|
+
["cat addresses.txt | $0 batch-geocode -i -", "Geocode from stdin"]
|
|
3507
|
+
]);
|
|
3508
|
+
},
|
|
3509
|
+
async (argv) => {
|
|
3510
|
+
const apiKey = loadConfiguredApiKey();
|
|
3511
|
+
if (!apiKey) {
|
|
3512
|
+
console.error("Error: Google Maps API key is not configured. Run 'mcp-google-api api-key <your-api-key>'.");
|
|
3513
|
+
process.exit(1);
|
|
3514
|
+
}
|
|
3515
|
+
let lines;
|
|
3516
|
+
if (argv.input === "-") {
|
|
3517
|
+
const rl = createInterface({ input: process.stdin });
|
|
3518
|
+
lines = [];
|
|
3519
|
+
for await (const line of rl) {
|
|
3520
|
+
const trimmed = line.trim();
|
|
3521
|
+
if (trimmed) lines.push(trimmed);
|
|
3522
|
+
}
|
|
3523
|
+
} else {
|
|
3524
|
+
if (!existsSync4(argv.input)) {
|
|
3525
|
+
console.error(`Error: File not found: ${argv.input}`);
|
|
3526
|
+
process.exit(1);
|
|
3527
|
+
}
|
|
3528
|
+
lines = readFileSync5(argv.input, "utf-8").split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
|
|
3529
|
+
}
|
|
3530
|
+
if (lines.length === 0) {
|
|
3531
|
+
console.error("Error: No addresses found in input.");
|
|
3532
|
+
process.exit(1);
|
|
3533
|
+
}
|
|
3534
|
+
const searcher = new PlacesSearcher(apiKey);
|
|
3535
|
+
const concurrency = Math.min(Math.max(argv.concurrency, 1), 50);
|
|
3536
|
+
const results = [];
|
|
3537
|
+
let completed = 0;
|
|
3538
|
+
const semaphore = async (tasks2, limit) => {
|
|
3539
|
+
const executing = [];
|
|
3540
|
+
for (const task of tasks2) {
|
|
3541
|
+
const p = task().then(() => {
|
|
3542
|
+
executing.splice(executing.indexOf(p), 1);
|
|
3543
|
+
});
|
|
3544
|
+
executing.push(p);
|
|
3545
|
+
if (executing.length >= limit) {
|
|
3546
|
+
await Promise.race(executing);
|
|
3547
|
+
}
|
|
3548
|
+
}
|
|
3549
|
+
await Promise.all(executing);
|
|
3550
|
+
};
|
|
3551
|
+
const tasks = lines.map((address, index) => async () => {
|
|
3552
|
+
try {
|
|
3553
|
+
const result = await searcher.geocode(address);
|
|
3554
|
+
results[index] = { address, ...result };
|
|
3555
|
+
} catch (error) {
|
|
3556
|
+
results[index] = { address, success: false, error: error.message };
|
|
3557
|
+
}
|
|
3558
|
+
completed++;
|
|
3559
|
+
if (!argv.output) return;
|
|
3560
|
+
process.stderr.write(`\r ${completed}/${lines.length} geocoded`);
|
|
3561
|
+
});
|
|
3562
|
+
await semaphore(tasks, concurrency);
|
|
3563
|
+
if (argv.output) {
|
|
3564
|
+
process.stderr.write("\n");
|
|
3565
|
+
}
|
|
3566
|
+
const succeeded = results.filter((r) => r.success).length;
|
|
3567
|
+
const failed = results.filter((r) => !r.success).length;
|
|
3568
|
+
const summary = { total: lines.length, succeeded, failed, results };
|
|
3569
|
+
const json = JSON.stringify(summary, null, 2);
|
|
3570
|
+
if (argv.output) {
|
|
3571
|
+
writeFileSync4(argv.output, json, "utf-8");
|
|
3572
|
+
console.error(`Done: ${succeeded}/${lines.length} succeeded. Output: ${argv.output}`);
|
|
3573
|
+
} else {
|
|
3574
|
+
console.log(json);
|
|
3575
|
+
}
|
|
3576
|
+
process.exit(failed > 0 ? 1 : 0);
|
|
3577
|
+
}
|
|
3578
|
+
).command(
|
|
3579
|
+
"start",
|
|
3580
|
+
"Start the MCP server (HTTP by default, --stdio for stdio mode)",
|
|
3581
|
+
buildServerCommandOptions,
|
|
3582
|
+
runServerCommand
|
|
3583
|
+
).command(
|
|
3584
|
+
"run",
|
|
3585
|
+
"Alias of start: run the MCP server (HTTP by default, --stdio for stdio mode)",
|
|
3586
|
+
buildServerCommandOptions,
|
|
3587
|
+
runServerCommand
|
|
3588
|
+
).command(
|
|
3589
|
+
"$0",
|
|
3590
|
+
"Print the help text (use start or run to launch the MCP server)",
|
|
3591
|
+
(yargs2) => yargs2,
|
|
3592
|
+
() => {
|
|
3593
|
+
cli.showHelp("log");
|
|
3594
|
+
}
|
|
3595
|
+
).version(packageVersion).alias("version", "v").option("env-file", {
|
|
3596
|
+
type: "string",
|
|
3597
|
+
global: true,
|
|
3598
|
+
description: "Load environment variables from this exact file (error if missing; no implicit .env fallback)"
|
|
3599
|
+
}).epilog(
|
|
3600
|
+
`Management commands (single flags, dispatched before subcommands):
|
|
3601
|
+
--ver Print the installed version and force one update check
|
|
3602
|
+
--version Alias of --ver
|
|
3603
|
+
--upgrade Upgrade the global package to the latest version and refresh installed skills
|
|
3604
|
+
--uninstall Remove the globally installed mcp-google-api package`
|
|
3605
|
+
).help();
|
|
3606
|
+
cli.parse();
|
|
3607
|
+
}
|
|
3608
|
+
}
|
|
3609
|
+
export {
|
|
3610
|
+
DOCTOR_PROBES,
|
|
3611
|
+
EXEC_TOOLS,
|
|
3612
|
+
createShutdownHandler,
|
|
3613
|
+
execTool,
|
|
3614
|
+
extractEnvFileOption,
|
|
3615
|
+
installShutdownHandlers,
|
|
3616
|
+
parseDoctorProbes,
|
|
3617
|
+
runApiKeyCommand,
|
|
3618
|
+
runAutoUpdateCommand,
|
|
3619
|
+
runDoctor,
|
|
3620
|
+
runEnableDisableCliCommand,
|
|
3621
|
+
runExecCommand,
|
|
3622
|
+
runInstallUninstallCommand,
|
|
3623
|
+
runSetup,
|
|
3624
|
+
runShowCommand,
|
|
3625
|
+
runSnippetCommand,
|
|
3626
|
+
startServer
|
|
3627
|
+
};
|
|
3628
|
+
//# sourceMappingURL=cli.js.map
|