@cablate/mcp-google-map 0.0.22 → 0.0.24

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.
Files changed (3) hide show
  1. package/README.md +29 -2
  2. package/dist/cli.js +2 -2
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -16,7 +16,18 @@
16
16
 
17
17
  ---
18
18
 
19
- A Model Context Protocol (MCP) server providing comprehensive Google Maps API integration with streamable HTTP transport support and multi-session capabilities.
19
+ Google Maps tools for AI agents use as an **MCP server** or as a standalone **Agent Skill** via CLI.
20
+
21
+ ```bash
22
+ # Agent Skill — no server needed
23
+ npx @cablate/mcp-google-map exec geocode '{"address":"Tokyo Tower"}'
24
+ npx @cablate/mcp-google-map exec search-places '{"query":"ramen in Tokyo"}'
25
+
26
+ # MCP Server
27
+ npx @cablate/mcp-google-map --port 3000 --apikey "YOUR_API_KEY"
28
+ ```
29
+
30
+ All 8 tools available in both modes. See [`skills/google-maps/`](./skills/google-maps/) for the full agent skill definition.
20
31
 
21
32
  ## Special Thanks
22
33
 
@@ -108,6 +119,17 @@ GOOGLE_MAPS_API_KEY=YOUR_API_KEY npx @cablate/mcp-google-map
108
119
  - **Transport**: Streamable HTTP (not stdio)
109
120
  - **Tools**: 8 Google Maps tools
110
121
 
122
+ ### CLI Exec Mode (Agent Skill)
123
+
124
+ Use tools directly without running the MCP server:
125
+
126
+ ```bash
127
+ npx @cablate/mcp-google-map exec geocode '{"address":"Tokyo Tower"}'
128
+ npx @cablate/mcp-google-map exec search-places '{"query":"ramen in Tokyo"}'
129
+ ```
130
+
131
+ All 8 tools available: `geocode`, `reverse-geocode`, `search-nearby`, `search-places`, `place-details`, `directions`, `distance-matrix`, `elevation`. See [`skills/google-maps/`](./skills/google-maps/) for the agent skill definition and full parameter docs.
132
+
111
133
  ### API Key Configuration
112
134
 
113
135
  API keys can be provided in three ways (priority order):
@@ -202,6 +224,11 @@ src/
202
224
  └── requestContext.ts # Per-request context (API key isolation)
203
225
  tests/
204
226
  └── smoke.test.ts # Smoke + E2E test suite
227
+ skills/
228
+ └── google-maps/
229
+ ├── SKILL.md # Agent skill definition
230
+ └── references/
231
+ └── tools-api.md # Tool parameter reference
205
232
  ```
206
233
 
207
234
  ## Tech Stack
@@ -221,7 +248,7 @@ tests/
221
248
  - DNS rebinding protection available for production
222
249
  - Input validation using Zod schemas
223
250
 
224
- For enterprise security reviews, see [Security Assessment Clarifications](./SECURITY_ASSESSMENT.md).
251
+ For enterprise security reviews, see [Security Assessment Clarifications](./SECURITY_ASSESSMENT.md) — a 23-item checklist covering licensing, data protection, credential management, tool contamination, and AI agent execution environment verification.
225
252
 
226
253
  ## Changelog
227
254
 
package/dist/cli.js CHANGED
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env node
2
- import{b as s,c as i}from"./chunk-RIT3FLYG.js";import{config as $}from"dotenv";import{resolve as T}from"path";import Ne from"yargs";import{hideBin as Ie}from"yargs/helpers";import{z as u}from"zod";import{AsyncLocalStorage as k}from"async_hooks";var w=new k;function c(){return w.getStore()?.apiKey||process.env.GOOGLE_MAPS_API_KEY}function O(t,e){return w.run(t,e)}var z="search_nearby",J="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.",L={center:u.object({value:u.string().describe("Address, landmark name, or coordinates (coordinate format: lat,lng)"),isCoordinates:u.boolean().default(!1).describe("Whether the value is coordinates")}).describe("Search center point (e.g. value: 49.3268778,-123.0585982, isCoordinates: true)"),keyword:u.string().optional().describe("Place type to search for (e.g., restaurant, cafe, hotel, gas_station, hospital)"),radius:u.number().default(1e3).describe("Search radius in meters"),openNow:u.boolean().default(!1).describe("Only show places that are currently open"),minRating:u.number().min(0).max(5).optional().describe("Minimum rating requirement (0-5)")};async function q(t){try{let e=c(),r=await new i(e).searchNearby(t);return r.success?{content:[{type:"text",text:`location: ${JSON.stringify(r.location,null,2)}
3
- `+JSON.stringify(r.data,null,2)}],isError:!1}:{content:[{type:"text",text:r.error||"Search failed"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error searching nearby places: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var f={NAME:z,DESCRIPTION:J,SCHEMA:L,ACTION:q};import{z as j}from"zod";var V="get_place_details",F="Get comprehensive details for a specific place using its Google Maps place_id. Use after search_nearby or maps_search_places to get full information including reviews, phone number, website, opening hours, and photos. Returns everything needed to evaluate or contact a business.",U={placeId:j.string().describe("Google Maps place ID")};async function W(t){try{let e=c(),r=await new i(e).getPlaceDetails(t.placeId);return r.success?{content:[{type:"text",text:JSON.stringify(r.data,null,2)}],isError:!1}:{content:[{type:"text",text:r.error||"Failed to get place details"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error getting place details: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var h={NAME:V,DESCRIPTION:F,SCHEMA:U,ACTION:W};import{z as Z}from"zod";var B="maps_geocode",Y="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.",Q={address:Z.string().describe("Address or place name to convert to coordinates")};async function X(t){try{let e=c(),r=await new i(e).geocode(t.address);return r.success?{content:[{type:"text",text:JSON.stringify(r.data,null,2)}],isError:!1}:{content:[{type:"text",text:r.error||"Failed to geocode address"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error geocoding address: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var v={NAME:B,DESCRIPTION:Y,SCHEMA:Q,ACTION:X};import{z as R}from"zod";var ee="maps_reverse_geocode",re="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.",te={latitude:R.number().describe("Latitude coordinate"),longitude:R.number().describe("Longitude coordinate")};async function oe(t){try{let e=c(),r=await new i(e).reverseGeocode(t.latitude,t.longitude);return r.success?{content:[{type:"text",text:JSON.stringify(r.data,null,2)}],isError:!1}:{content:[{type:"text",text:r.error||"Failed to reverse geocode coordinates"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error reverse geocoding: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var S={NAME:ee,DESCRIPTION:re,SCHEMA:te,ACTION:oe};import{z as E}from"zod";var se="maps_distance_matrix",ne="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.",ae={origins:E.array(E.string()).describe("List of origin addresses or coordinates"),destinations:E.array(E.string()).describe("List of destination addresses or coordinates"),mode:E.enum(["driving","walking","bicycling","transit"]).default("driving").describe("Travel mode for calculation")};async function ie(t){try{let e=c(),r=await new i(e).calculateDistanceMatrix(t.origins,t.destinations,t.mode);return r.success?{content:[{type:"text",text:JSON.stringify(r.data,null,2)}],isError:!1}:{content:[{type:"text",text:r.error||"Failed to calculate distance matrix"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error calculating distance matrix: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var P={NAME:se,DESCRIPTION:ne,SCHEMA:ae,ACTION:ie};import{z as b}from"zod";var ce="maps_directions",pe="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.",le={origin:b.string().describe("Starting point address or coordinates"),destination:b.string().describe("Destination address or coordinates"),mode:b.enum(["driving","walking","bicycling","transit"]).default("driving").describe("Travel mode for directions"),departure_time:b.string().optional().describe("Departure time (ISO string format)"),arrival_time:b.string().optional().describe("Arrival time (ISO string format)")};async function de(t){try{let e=c(),r=await new i(e).getDirections(t.origin,t.destination,t.mode,t.departure_time,t.arrival_time);return r.success?{content:[{type:"text",text:JSON.stringify(r.data,null,2)}],isError:!1}:{content:[{type:"text",text:r.error||"Failed to get directions"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error getting directions: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var A={NAME:ce,DESCRIPTION:pe,SCHEMA:le,ACTION:de};import{z as N}from"zod";var me="maps_elevation",ue="Get elevation (height above sea level in meters) for one or more geographic coordinates. Use for terrain analysis, hiking/cycling route planning, or when the user asks about altitude at specific locations.",ge={locations:N.array(N.object({latitude:N.number().describe("Latitude coordinate"),longitude:N.number().describe("Longitude coordinate")})).describe("List of locations to get elevation data for")};async function ye(t){try{let e=c(),r=await new i(e).getElevation(t.locations);return r.success?{content:[{type:"text",text:JSON.stringify(r.data,null,2)}],isError:!1}:{content:[{type:"text",text:r.error||"Failed to get elevation data"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error getting elevation data: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var x={NAME:me,DESCRIPTION:ue,SCHEMA:ge,ACTION:ye};import{z as d}from"zod";var fe="maps_search_places",he="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, rating filters, and open-now filtering. Use when the user describes what they're looking for in words rather than by type and coordinates.",ve={query:d.string().describe("Text search query (e.g., 'Italian restaurants in Manhattan', 'hotels near Taipei 101')"),locationBias:d.object({latitude:d.number().describe("Latitude to bias results toward"),longitude:d.number().describe("Longitude to bias results toward"),radius:d.number().optional().describe("Bias radius in meters (default: 5000)")}).optional().describe("Optional location to bias results toward"),openNow:d.boolean().optional().describe("Only return places that are currently open"),minRating:d.number().optional().describe("Minimum rating filter (1.0 - 5.0)"),includedType:d.string().optional().describe("Filter by place type (e.g., restaurant, cafe, hotel)")};async function Se(t){try{let e=c(),r=await new i(e).searchText({query:t.query,locationBias:t.locationBias,openNow:t.openNow,minRating:t.minRating,includedType:t.includedType});return r.success?{content:[{type:"text",text:JSON.stringify(r.data,null,2)}],isError:!1}:{content:[{type:"text",text:r.error||"Failed to search places"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error searching places: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var M={NAME:fe,DESCRIPTION:he,SCHEMA:ve,ACTION:Se};var m={readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},Ee=[{name:"MCP-Server",portEnvVar:"MCP_SERVER_PORT",tools:[{name:f.NAME,description:f.DESCRIPTION,schema:f.SCHEMA,annotations:m,action:t=>f.ACTION(t)},{name:h.NAME,description:h.DESCRIPTION,schema:h.SCHEMA,annotations:m,action:t=>h.ACTION(t)},{name:v.NAME,description:v.DESCRIPTION,schema:v.SCHEMA,annotations:m,action:t=>v.ACTION(t)},{name:S.NAME,description:S.DESCRIPTION,schema:S.SCHEMA,annotations:m,action:t=>S.ACTION(t)},{name:P.NAME,description:P.DESCRIPTION,schema:P.SCHEMA,annotations:m,action:t=>P.ACTION(t)},{name:A.NAME,description:A.DESCRIPTION,schema:A.SCHEMA,annotations:m,action:t=>A.ACTION(t)},{name:x.NAME,description:x.DESCRIPTION,schema:x.SCHEMA,annotations:m,action:t=>x.ACTION(t)},{name:M.NAME,description:M.DESCRIPTION,schema:M.SCHEMA,annotations:m,action:t=>M.ACTION(t)}]}],K=Ee;import{McpServer as Pe}from"@modelcontextprotocol/sdk/server/mcp.js";import{StreamableHTTPServerTransport as be}from"@modelcontextprotocol/sdk/server/streamableHttp.js";import{isInitializeRequest as Ae}from"@modelcontextprotocol/sdk/types.js";import D from"express";import{randomUUID as xe}from"crypto";import{z as Me}from"zod";var C=class t{constructor(){this.defaultApiKey=process.env.GOOGLE_MAPS_API_KEY}static getInstance(){return t.instance||(t.instance=new t),t.instance}setDefaultApiKey(e){this.defaultApiKey=e,process.env.GOOGLE_MAPS_API_KEY=e}getApiKey(e,o){if(e){let r=e.headers["x-google-maps-api-key"];if(r)return r;let n=e.headers.authorization;if(n&&n.startsWith("Bearer "))return n.substring(7)}return o||this.defaultApiKey}hasApiKey(e,o){return!!this.getApiKey(e,o)}isValidApiKeyFormat(e){return/^[A-Za-z0-9_-]{20,50}$/.test(e)}};var Ce="0.0.1",I=class{constructor(e,o){this.sessions={};this.httpServer=null;this.serverName=e,this.tools=o,this.server=this.createMcpServer()}createMcpServer(){let e=new Pe({name:this.serverName,version:Ce},{capabilities:{logging:{},tools:{}}});return this.tools.forEach(o=>{e.registerTool(o.name,{description:o.description,inputSchema:Me.object(o.schema),annotations:o.annotations},async r=>o.action(r))}),e}async connect(e){await this.server.connect(e);let o=process.stdout.write.bind(process.stdout);process.stdout.write=(r,n,a)=>typeof r=="string"&&!r.startsWith("{")?!0:o(r,n,a),s.log(`${this.serverName} connected and ready to process requests`)}async startHttpServer(e){let o=D();o.use(D.json()),o.post("/mcp",async(n,a)=>{let p=n.headers["mcp-session-id"],l,g=C.getInstance().getApiKey(n);if(s.log(`${this.serverName} API key received from request context`),p&&this.sessions[p])l=this.sessions[p],g&&(l.apiKey=g);else if(!p&&Ae(n.body)){let y=new be({sessionIdGenerator:()=>xe(),onsessioninitialized:_=>{this.sessions[_]=l,s.log(`[${this.serverName}] New session initialized: ${_}`)}});l={transport:y,apiKey:g},y.onclose=()=>{y.sessionId&&(delete this.sessions[y.sessionId],s.log(`[${this.serverName}] Session closed: ${y.sessionId}`))},await this.createMcpServer().connect(y)}else{a.status(400).json({jsonrpc:"2.0",error:{code:-32e3,message:"Bad Request: No valid session ID provided"},id:null});return}await O({apiKey:l.apiKey,sessionId:p},async()=>{await l.transport.handleRequest(n,a,n.body)})});let r=async(n,a)=>{let p=n.headers["mcp-session-id"];if(!p||!this.sessions[p]){a.status(400).send("Invalid or missing session ID");return}let l=this.sessions[p],g=C.getInstance().getApiKey(n);g&&(l.apiKey=g),await O({apiKey:l.apiKey,sessionId:p},async()=>{await l.transport.handleRequest(n,a)})};o.get("/mcp",r),o.delete("/mcp",r),this.httpServer=o.listen(e,()=>{s.log(`[${this.serverName}] HTTP server listening on port ${e}`),s.log(`[${this.serverName}] MCP endpoint available at http://localhost:${e}/mcp`)})}async stopHttpServer(){if(!this.httpServer){s.error(`[${this.serverName}] HTTP server is not running or already stopped.`);return}return new Promise((e,o)=>{this.httpServer.close(r=>{if(r){s.error(`[${this.serverName}] Error stopping HTTP server:`,r),o(r);return}s.log(`[${this.serverName}] HTTP server stopped.`),this.httpServer=null;let n=Object.values(this.sessions).map(a=>(a.transport.sessionId&&delete this.sessions[a.transport.sessionId],Promise.resolve()));Promise.all(n).then(()=>{s.log(`[${this.serverName}] All transports closed.`),e()}).catch(a=>{s.error(`[${this.serverName}] Error during bulk transport closing:`,a),o(a)})})})}};import{fileURLToPath as Oe}from"url";import{dirname as Te}from"path";import{readFileSync as _e}from"fs";var we=Oe(import.meta.url),H=Te(we);$({path:T(process.cwd(),".env")});$({path:T(H,"../.env")});async function Re(t,e){t&&(process.env.MCP_SERVER_PORT=t.toString()),e&&(process.env.GOOGLE_MAPS_API_KEY=e),s.log("\u{1F680} Starting Google Maps MCP Server..."),s.log("\u{1F4CD} Available tools: search_nearby, get_place_details, maps_geocode, maps_reverse_geocode, maps_distance_matrix, maps_directions, maps_elevation, echo"),s.log("\u2139\uFE0F Reminder: enable Places API (New) in https://console.cloud.google.com before using the new Place features."),s.log("");let o=K.map(async r=>{let n=process.env[r.portEnvVar];if(!n){s.error(`\u26A0\uFE0F [${r.name}] Port environment variable ${r.portEnvVar} not set.`),s.log(`\u{1F4A1} Please set ${r.portEnvVar} in your .env file or use --port parameter.`),s.log(` Example: ${r.portEnvVar}=3000 or --port 3000`);return}let a=Number(n);if(isNaN(a)||a<=0){s.error(`\u274C [${r.name}] Invalid port number "${n}" defined in ${r.portEnvVar}.`);return}try{let p=new I(r.name,r.tools);s.log(`\u{1F527} [${r.name}] Initializing MCP Server in HTTP mode on port ${a}...`),await p.startHttpServer(a),s.log(`\u2705 [${r.name}] MCP Server started successfully!`),s.log(` \u{1F310} Endpoint: http://localhost:${a}/mcp`),s.log(` \u{1F4DA} Tools: ${r.tools.length} available`)}catch(p){s.error(`\u274C [${r.name}] Failed to start MCP Server on port ${a}:`,p)}});await Promise.allSettled(o),s.log(""),s.log("\u{1F389} Server initialization completed!"),s.log("\u{1F4A1} Need help? Check the README.md for configuration details.")}var Ke=process.argv[1]&&(process.argv[1].endsWith("cli.ts")||process.argv[1].endsWith("cli.js")||process.argv[1].endsWith("mcp-google-map")||process.argv[1].includes("mcp-google-map")),De=import.meta.url===`file://${process.argv[1]}`;if(Ke||De){let t="0.0.0";try{let o=T(H,"../package.json");t=JSON.parse(_e(o,"utf-8")).version}catch{t="0.0.0"}let e=Ne(Ie(process.argv)).option("port",{alias:"p",type:"number",description:"Port to run the MCP server on",default:process.env.MCP_SERVER_PORT?parseInt(process.env.MCP_SERVER_PORT):3e3}).option("apikey",{alias:"k",type:"string",description:"Google Maps API key",default:process.env.GOOGLE_MAPS_API_KEY}).option("help",{alias:"h",type:"boolean",description:"Show help"}).version(t).alias("version","v").example([["$0","Start server with default settings"],['$0 --port 3000 --apikey "your_api_key"',"Start server with custom port and API key"],['$0 -p 3001 -k "your_api_key"',"Start server with short options"]]).help().parseSync();s.log("\u{1F5FA}\uFE0F Google Maps MCP Server"),s.log(" A Model Context Protocol server for Google Maps services"),s.log(""),e.apikey||(s.log("\u26A0\uFE0F Google Maps API Key not found!"),s.log(" Please provide --apikey parameter or set GOOGLE_MAPS_API_KEY in your .env file"),s.log(" Example: mcp-google-map --apikey your_api_key_here"),s.log(" Or: GOOGLE_MAPS_API_KEY=your_api_key_here"),s.log("")),Re(e.port,e.apikey).catch(o=>{s.error("\u274C Failed to start server:",o),process.exit(1)})}export{Re as startServer};
2
+ import{b as s,c as i}from"./chunk-RIT3FLYG.js";import{config as D}from"dotenv";import{resolve as T}from"path";import Oe from"yargs";import{hideBin as Ie}from"yargs/helpers";import{z as m}from"zod";import{AsyncLocalStorage as z}from"async_hooks";var w=new z;function c(){return w.getStore()?.apiKey||process.env.GOOGLE_MAPS_API_KEY}function I(t,e){return w.run(t,e)}var J="search_nearby",L="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.",q={center:m.object({value:m.string().describe("Address, landmark name, or coordinates (coordinate format: lat,lng)"),isCoordinates:m.boolean().default(!1).describe("Whether the value is coordinates")}).describe("Search center point (e.g. value: 49.3268778,-123.0585982, isCoordinates: true)"),keyword:m.string().optional().describe("Place type to search for (e.g., restaurant, cafe, hotel, gas_station, hospital)"),radius:m.number().default(1e3).describe("Search radius in meters"),openNow:m.boolean().default(!1).describe("Only show places that are currently open"),minRating:m.number().min(0).max(5).optional().describe("Minimum rating requirement (0-5)")};async function j(t){try{let e=c(),r=await new i(e).searchNearby(t);return r.success?{content:[{type:"text",text:`location: ${JSON.stringify(r.location,null,2)}
3
+ `+JSON.stringify(r.data,null,2)}],isError:!1}:{content:[{type:"text",text:r.error||"Search failed"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error searching nearby places: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var f={NAME:J,DESCRIPTION:L,SCHEMA:q,ACTION:j};import{z as V}from"zod";var F="get_place_details",U="Get comprehensive details for a specific place using its Google Maps place_id. Use after search_nearby or maps_search_places to get full information including reviews, phone number, website, opening hours, and photos. Returns everything needed to evaluate or contact a business.",B={placeId:V.string().describe("Google Maps place ID")};async function W(t){try{let e=c(),r=await new i(e).getPlaceDetails(t.placeId);return r.success?{content:[{type:"text",text:JSON.stringify(r.data,null,2)}],isError:!1}:{content:[{type:"text",text:r.error||"Failed to get place details"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error getting place details: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var h={NAME:F,DESCRIPTION:U,SCHEMA:B,ACTION:W};import{z as Z}from"zod";var Y="maps_geocode",X="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.",Q={address:Z.string().describe("Address or place name to convert to coordinates")};async function ee(t){try{let e=c(),r=await new i(e).geocode(t.address);return r.success?{content:[{type:"text",text:JSON.stringify(r.data,null,2)}],isError:!1}:{content:[{type:"text",text:r.error||"Failed to geocode address"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error geocoding address: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var v={NAME:Y,DESCRIPTION:X,SCHEMA:Q,ACTION:ee};import{z as R}from"zod";var re="maps_reverse_geocode",te="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.",oe={latitude:R.number().describe("Latitude coordinate"),longitude:R.number().describe("Longitude coordinate")};async function se(t){try{let e=c(),r=await new i(e).reverseGeocode(t.latitude,t.longitude);return r.success?{content:[{type:"text",text:JSON.stringify(r.data,null,2)}],isError:!1}:{content:[{type:"text",text:r.error||"Failed to reverse geocode coordinates"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error reverse geocoding: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var S={NAME:re,DESCRIPTION:te,SCHEMA:oe,ACTION:se};import{z as E}from"zod";var ne="maps_distance_matrix",ae="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.",ie={origins:E.array(E.string()).describe("List of origin addresses or coordinates"),destinations:E.array(E.string()).describe("List of destination addresses or coordinates"),mode:E.enum(["driving","walking","bicycling","transit"]).default("driving").describe("Travel mode for calculation")};async function ce(t){try{let e=c(),r=await new i(e).calculateDistanceMatrix(t.origins,t.destinations,t.mode);return r.success?{content:[{type:"text",text:JSON.stringify(r.data,null,2)}],isError:!1}:{content:[{type:"text",text:r.error||"Failed to calculate distance matrix"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error calculating distance matrix: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var P={NAME:ne,DESCRIPTION:ae,SCHEMA:ie,ACTION:ce};import{z as b}from"zod";var le="maps_directions",pe="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.",de={origin:b.string().describe("Starting point address or coordinates"),destination:b.string().describe("Destination address or coordinates"),mode:b.enum(["driving","walking","bicycling","transit"]).default("driving").describe("Travel mode for directions"),departure_time:b.string().optional().describe("Departure time (ISO string format)"),arrival_time:b.string().optional().describe("Arrival time (ISO string format)")};async function ue(t){try{let e=c(),r=await new i(e).getDirections(t.origin,t.destination,t.mode,t.departure_time,t.arrival_time);return r.success?{content:[{type:"text",text:JSON.stringify(r.data,null,2)}],isError:!1}:{content:[{type:"text",text:r.error||"Failed to get directions"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error getting directions: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var x={NAME:le,DESCRIPTION:pe,SCHEMA:de,ACTION:ue};import{z as C}from"zod";var me="maps_elevation",ge="Get elevation (height above sea level in meters) for one or more geographic coordinates. Use for terrain analysis, hiking/cycling route planning, or when the user asks about altitude at specific locations.",ye={locations:C.array(C.object({latitude:C.number().describe("Latitude coordinate"),longitude:C.number().describe("Longitude coordinate")})).describe("List of locations to get elevation data for")};async function fe(t){try{let e=c(),r=await new i(e).getElevation(t.locations);return r.success?{content:[{type:"text",text:JSON.stringify(r.data,null,2)}],isError:!1}:{content:[{type:"text",text:r.error||"Failed to get elevation data"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error getting elevation data: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var A={NAME:me,DESCRIPTION:ge,SCHEMA:ye,ACTION:fe};import{z as d}from"zod";var he="maps_search_places",ve="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, rating filters, and open-now filtering. Use when the user describes what they're looking for in words rather than by type and coordinates.",Se={query:d.string().describe("Text search query (e.g., 'Italian restaurants in Manhattan', 'hotels near Taipei 101')"),locationBias:d.object({latitude:d.number().describe("Latitude to bias results toward"),longitude:d.number().describe("Longitude to bias results toward"),radius:d.number().optional().describe("Bias radius in meters (default: 5000)")}).optional().describe("Optional location to bias results toward"),openNow:d.boolean().optional().describe("Only return places that are currently open"),minRating:d.number().optional().describe("Minimum rating filter (1.0 - 5.0)"),includedType:d.string().optional().describe("Filter by place type (e.g., restaurant, cafe, hotel)")};async function Ee(t){try{let e=c(),r=await new i(e).searchText({query:t.query,locationBias:t.locationBias,openNow:t.openNow,minRating:t.minRating,includedType:t.includedType});return r.success?{content:[{type:"text",text:JSON.stringify(r.data,null,2)}],isError:!1}:{content:[{type:"text",text:r.error||"Failed to search places"}],isError:!0}}catch(e){return{isError:!0,content:[{type:"text",text:`Error searching places: ${e instanceof Error?e.message:JSON.stringify(e)}`}]}}}var M={NAME:he,DESCRIPTION:ve,SCHEMA:Se,ACTION:Ee};var u={readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!0},Pe=[{name:"MCP-Server",portEnvVar:"MCP_SERVER_PORT",tools:[{name:f.NAME,description:f.DESCRIPTION,schema:f.SCHEMA,annotations:u,action:t=>f.ACTION(t)},{name:h.NAME,description:h.DESCRIPTION,schema:h.SCHEMA,annotations:u,action:t=>h.ACTION(t)},{name:v.NAME,description:v.DESCRIPTION,schema:v.SCHEMA,annotations:u,action:t=>v.ACTION(t)},{name:S.NAME,description:S.DESCRIPTION,schema:S.SCHEMA,annotations:u,action:t=>S.ACTION(t)},{name:P.NAME,description:P.DESCRIPTION,schema:P.SCHEMA,annotations:u,action:t=>P.ACTION(t)},{name:x.NAME,description:x.DESCRIPTION,schema:x.SCHEMA,annotations:u,action:t=>x.ACTION(t)},{name:A.NAME,description:A.DESCRIPTION,schema:A.SCHEMA,annotations:u,action:t=>A.ACTION(t)},{name:M.NAME,description:M.DESCRIPTION,schema:M.SCHEMA,annotations:u,action:t=>M.ACTION(t)}]}],K=Pe;import{McpServer as be}from"@modelcontextprotocol/sdk/server/mcp.js";import{StreamableHTTPServerTransport as xe}from"@modelcontextprotocol/sdk/server/streamableHttp.js";import{isInitializeRequest as Ae}from"@modelcontextprotocol/sdk/types.js";import $ from"express";import{randomUUID as Me}from"crypto";import{z as Ne}from"zod";var N=class t{constructor(){this.defaultApiKey=process.env.GOOGLE_MAPS_API_KEY}static getInstance(){return t.instance||(t.instance=new t),t.instance}setDefaultApiKey(e){this.defaultApiKey=e,process.env.GOOGLE_MAPS_API_KEY=e}getApiKey(e,o){if(e){let r=e.headers["x-google-maps-api-key"];if(r)return r;let n=e.headers.authorization;if(n&&n.startsWith("Bearer "))return n.substring(7)}return o||this.defaultApiKey}hasApiKey(e,o){return!!this.getApiKey(e,o)}isValidApiKeyFormat(e){return/^[A-Za-z0-9_-]{20,50}$/.test(e)}};var Ce="0.0.1",O=class{constructor(e,o){this.sessions={};this.httpServer=null;this.serverName=e,this.tools=o,this.server=this.createMcpServer()}createMcpServer(){let e=new be({name:this.serverName,version:Ce},{capabilities:{logging:{},tools:{}}});return this.tools.forEach(o=>{e.registerTool(o.name,{description:o.description,inputSchema:Ne.object(o.schema),annotations:o.annotations},async r=>o.action(r))}),e}async connect(e){await this.server.connect(e);let o=process.stdout.write.bind(process.stdout);process.stdout.write=(r,n,a)=>typeof r=="string"&&!r.startsWith("{")?!0:o(r,n,a),s.log(`${this.serverName} connected and ready to process requests`)}async startHttpServer(e){let o=$();o.use($.json()),o.post("/mcp",async(n,a)=>{let l=n.headers["mcp-session-id"],p,g=N.getInstance().getApiKey(n);if(s.log(`${this.serverName} API key received from request context`),l&&this.sessions[l])p=this.sessions[l],g&&(p.apiKey=g);else if(!l&&Ae(n.body)){let y=new xe({sessionIdGenerator:()=>Me(),onsessioninitialized:_=>{this.sessions[_]=p,s.log(`[${this.serverName}] New session initialized: ${_}`)}});p={transport:y,apiKey:g},y.onclose=()=>{y.sessionId&&(delete this.sessions[y.sessionId],s.log(`[${this.serverName}] Session closed: ${y.sessionId}`))},await this.createMcpServer().connect(y)}else{a.status(400).json({jsonrpc:"2.0",error:{code:-32e3,message:"Bad Request: No valid session ID provided"},id:null});return}await I({apiKey:p.apiKey,sessionId:l},async()=>{await p.transport.handleRequest(n,a,n.body)})});let r=async(n,a)=>{let l=n.headers["mcp-session-id"];if(!l||!this.sessions[l]){a.status(400).send("Invalid or missing session ID");return}let p=this.sessions[l],g=N.getInstance().getApiKey(n);g&&(p.apiKey=g),await I({apiKey:p.apiKey,sessionId:l},async()=>{await p.transport.handleRequest(n,a)})};o.get("/mcp",r),o.delete("/mcp",r),this.httpServer=o.listen(e,()=>{s.log(`[${this.serverName}] HTTP server listening on port ${e}`),s.log(`[${this.serverName}] MCP endpoint available at http://localhost:${e}/mcp`)})}async stopHttpServer(){if(!this.httpServer){s.error(`[${this.serverName}] HTTP server is not running or already stopped.`);return}return new Promise((e,o)=>{this.httpServer.close(r=>{if(r){s.error(`[${this.serverName}] Error stopping HTTP server:`,r),o(r);return}s.log(`[${this.serverName}] HTTP server stopped.`),this.httpServer=null;let n=Object.values(this.sessions).map(a=>(a.transport.sessionId&&delete this.sessions[a.transport.sessionId],Promise.resolve()));Promise.all(n).then(()=>{s.log(`[${this.serverName}] All transports closed.`),e()}).catch(a=>{s.error(`[${this.serverName}] Error during bulk transport closing:`,a),o(a)})})})}};import{fileURLToPath as Te}from"url";import{dirname as _e}from"path";import{readFileSync as we}from"fs";var Re=Te(import.meta.url),G=_e(Re);D({path:T(process.cwd(),".env")});D({path:T(G,"../.env")});async function Ke(t,e){t&&(process.env.MCP_SERVER_PORT=t.toString()),e&&(process.env.GOOGLE_MAPS_API_KEY=e),s.log("\u{1F680} Starting Google Maps MCP Server..."),s.log("\u{1F4CD} Available tools: search_nearby, get_place_details, maps_geocode, maps_reverse_geocode, maps_distance_matrix, maps_directions, maps_elevation, echo"),s.log("\u2139\uFE0F Reminder: enable Places API (New) in https://console.cloud.google.com before using the new Place features."),s.log("");let o=K.map(async r=>{let n=process.env[r.portEnvVar];if(!n){s.error(`\u26A0\uFE0F [${r.name}] Port environment variable ${r.portEnvVar} not set.`),s.log(`\u{1F4A1} Please set ${r.portEnvVar} in your .env file or use --port parameter.`),s.log(` Example: ${r.portEnvVar}=3000 or --port 3000`);return}let a=Number(n);if(isNaN(a)||a<=0){s.error(`\u274C [${r.name}] Invalid port number "${n}" defined in ${r.portEnvVar}.`);return}try{let l=new O(r.name,r.tools);s.log(`\u{1F527} [${r.name}] Initializing MCP Server in HTTP mode on port ${a}...`),await l.startHttpServer(a),s.log(`\u2705 [${r.name}] MCP Server started successfully!`),s.log(` \u{1F310} Endpoint: http://localhost:${a}/mcp`),s.log(` \u{1F4DA} Tools: ${r.tools.length} available`)}catch(l){s.error(`\u274C [${r.name}] Failed to start MCP Server on port ${a}:`,l)}});await Promise.allSettled(o),s.log(""),s.log("\u{1F389} Server initialization completed!"),s.log("\u{1F4A1} Need help? Check the README.md for configuration details.")}var H=["geocode","reverse-geocode","search-nearby","search-places","place-details","directions","distance-matrix","elevation"];async function $e(t,e,o){let r=new i(o);switch(t){case"geocode":case"maps_geocode":return r.geocode(e.address);case"reverse-geocode":case"maps_reverse_geocode":return r.reverseGeocode(e.latitude,e.longitude);case"search-nearby":case"search_nearby":return r.searchNearby(e);case"search-places":case"maps_search_places":return r.searchText({query:e.query,locationBias:e.locationBias,openNow:e.openNow,minRating:e.minRating,includedType:e.includedType});case"place-details":case"get_place_details":return r.getPlaceDetails(e.placeId);case"directions":case"maps_directions":return r.getDirections(e.origin,e.destination,e.mode,e.departure_time,e.arrival_time);case"distance-matrix":case"maps_distance_matrix":return r.calculateDistanceMatrix(e.origins,e.destinations,e.mode);case"elevation":case"maps_elevation":return r.getElevation(e.locations);default:throw new Error(`Unknown tool: ${t}. Available: ${H.join(", ")}`)}}var De=process.argv[1]&&(process.argv[1].endsWith("cli.ts")||process.argv[1].endsWith("cli.js")||process.argv[1].endsWith("mcp-google-map")||process.argv[1].includes("mcp-google-map")),Ge=import.meta.url===`file://${process.argv[1]}`;if(De||Ge){let t="0.0.0";try{let e=T(G,"../package.json");t=JSON.parse(we(e,"utf-8")).version}catch{t="0.0.0"}Oe(Ie(process.argv)).command("exec <tool> [params]","Execute a tool directly and output JSON",e=>e.positional("tool",{type:"string",describe:`Tool name: ${H.join(", ")}`}).positional("params",{type:"string",describe:"JSON parameters string"}).option("apikey",{alias:"k",type:"string",description:"Google Maps API key",default:process.env.GOOGLE_MAPS_API_KEY}).example([[`$0 exec geocode '{"address":"Tokyo Tower"}'`,"Geocode an address"],[`$0 exec search-nearby '{"center":{"value":"35.68,139.74","isCoordinates":true},"keyword":"restaurant"}'`,"Search nearby"],[`$0 exec search-places '{"query":"ramen in Tokyo"}'`,"Text search"]]),async e=>{e.apikey||(console.error(JSON.stringify({error:"GOOGLE_MAPS_API_KEY not set. Use --apikey or set GOOGLE_MAPS_API_KEY environment variable."},null,2)),process.exit(1));try{let o=e.params?JSON.parse(e.params):{},r=await $e(e.tool,o,e.apikey);console.log(JSON.stringify(r,null,2)),process.exit(0)}catch(o){console.error(JSON.stringify({error:o.message},null,2)),process.exit(1)}}).command("$0","Start the MCP server",e=>e.option("port",{alias:"p",type:"number",description:"Port to run the MCP server on",default:process.env.MCP_SERVER_PORT?parseInt(process.env.MCP_SERVER_PORT):3e3}).option("apikey",{alias:"k",type:"string",description:"Google Maps API key",default:process.env.GOOGLE_MAPS_API_KEY}).example([["$0","Start server with default settings"],['$0 --port 3000 --apikey "your_api_key"',"Start with custom port and API key"]]),async e=>{s.log("\u{1F5FA}\uFE0F Google Maps MCP Server"),s.log(" A Model Context Protocol server for Google Maps services"),s.log(""),e.apikey||(s.log("\u26A0\uFE0F Google Maps API Key not found!"),s.log(" Please provide --apikey parameter or set GOOGLE_MAPS_API_KEY in your .env file"),s.log("")),Ke(e.port,e.apikey).catch(o=>{s.error("\u274C Failed to start server:",o),process.exit(1)})}).version(t).alias("version","v").help().parse()}export{Ke as startServer};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cablate/mcp-google-map",
3
- "version": "0.0.22",
3
+ "version": "0.0.24",
4
4
  "description": "Google Maps MCP server with streamable HTTP transport support for location services, geocoding, and navigation",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",