rebase
This commit is contained in:
commit
796e5122d9
7 changed files with 884 additions and 49 deletions
|
|
@ -2,15 +2,37 @@
|
||||||
|
|
||||||
This MCP server integrates with Google Drive to allow listing, reading, and searching over files.
|
This MCP server integrates with Google Drive to allow listing, reading, and searching over files.
|
||||||
|
|
||||||
|
## Components
|
||||||
|
|
||||||
|
### Tools
|
||||||
|
|
||||||
|
- **search**
|
||||||
|
- Search for files in Google Drive
|
||||||
|
- Input: `query` (string): Search query
|
||||||
|
- Returns file names and MIME types of matching files
|
||||||
|
|
||||||
|
### Resources
|
||||||
|
|
||||||
|
The server provides access to Google Drive files:
|
||||||
|
|
||||||
|
- **Files** (`gdrive:///<file_id>`)
|
||||||
|
- Supports all file types
|
||||||
|
- Google Workspace files are automatically exported:
|
||||||
|
- Docs → Markdown
|
||||||
|
- Sheets → CSV
|
||||||
|
- Presentations → Plain text
|
||||||
|
- Drawings → PNG
|
||||||
|
- Other files are provided in their native format
|
||||||
|
|
||||||
## Getting started
|
## Getting started
|
||||||
|
|
||||||
1. Create a new Google Cloud project
|
1. [Create a new Google Cloud project](https://console.cloud.google.com/projectcreate)
|
||||||
2. Enable the Google Drive API
|
2. [Enable the Google Drive API](https://console.cloud.google.com/workspace-api/products)
|
||||||
3. Configure an OAuth consent screen ("internal" is fine for testing)
|
3. [Configure an OAuth consent screen](https://console.cloud.google.com/apis/credentials/consent) ("internal" is fine for testing)
|
||||||
4. Add OAuth scope `https://www.googleapis.com/auth/drive.readonly`
|
4. Add OAuth scope `https://www.googleapis.com/auth/drive.readonly`
|
||||||
5. Create an OAuth Client ID for application type "Desktop App"
|
5. [Create an OAuth Client ID](https://console.cloud.google.com/apis/credentials/oauthclient) for application type "Desktop App"
|
||||||
6. Download the JSON file of your client's OAuth keys
|
6. Download the JSON file of your client's OAuth keys
|
||||||
7. Rename the key file to `gcp-oauth.keys.json` and place into the root of this repo
|
7. Rename the key file to `gcp-oauth.keys.json` and place into the root of this repo (i.e. `servers/gcp-oauth.keys.json`)
|
||||||
|
|
||||||
Make sure to build the server with either `npm run build` or `npm run watch`.
|
Make sure to build the server with either `npm run build` or `npm run watch`.
|
||||||
|
|
||||||
|
|
@ -18,16 +40,18 @@ Make sure to build the server with either `npm run build` or `npm run watch`.
|
||||||
|
|
||||||
To authenticate and save credentials:
|
To authenticate and save credentials:
|
||||||
|
|
||||||
1. Run the server with the `auth` argument: `node build/gdrive auth`
|
1. Run the server with the `auth` argument: `node ./dist auth`
|
||||||
2. This will open an authentication flow in your system browser
|
2. This will open an authentication flow in your system browser
|
||||||
3. Complete the authentication process
|
3. Complete the authentication process
|
||||||
4. Credentials will be saved for future use
|
4. Credentials will be saved in the root of this repo (i.e. `servers/.gdrive-server-credentials.json`)
|
||||||
|
|
||||||
### Running the server
|
### Usage with Desktop App
|
||||||
|
|
||||||
After authenticating:
|
To integrate this server with the desktop app, add the following to your app's server configuration:
|
||||||
|
|
||||||
1. Run the server normally: `node build/gdrive`
|
```json
|
||||||
2. The server will load the saved credentials and start
|
{
|
||||||
|
"mcp-server-gdrive": {
|
||||||
Note: If you haven't authenticated yet, the server will prompt you to run with the `auth` argument first.
|
"command": "mcp-server-gdrive"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -44,7 +44,7 @@ server.setRequestHandler(ListResourcesRequestSchema, async (request) => {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
resources: files.map((file) => ({
|
resources: files.map((file) => ({
|
||||||
uri: `gdrive://${file.id}`,
|
uri: `gdrive:///${file.id}`,
|
||||||
mimeType: file.mimeType,
|
mimeType: file.mimeType,
|
||||||
name: file.name,
|
name: file.name,
|
||||||
})),
|
})),
|
||||||
|
|
@ -53,7 +53,7 @@ server.setRequestHandler(ListResourcesRequestSchema, async (request) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
||||||
const fileId = request.params.uri.replace("gdrive://", "");
|
const fileId = request.params.uri.replace("gdrive:///", "");
|
||||||
|
|
||||||
// First get file metadata to check mime type
|
// First get file metadata to check mime type
|
||||||
const file = await drive.files.get({
|
const file = await drive.files.get({
|
||||||
|
|
@ -149,14 +149,16 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||||
|
|
||||||
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||||
if (request.params.name === "search") {
|
if (request.params.name === "search") {
|
||||||
const query = request.params.arguments?.query as string;
|
const userQuery = request.params.arguments?.query as string;
|
||||||
|
const escapedQuery = userQuery.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
||||||
|
const formattedQuery = `fullText contains '${escapedQuery}'`;
|
||||||
|
|
||||||
const res = await drive.files.list({
|
const res = await drive.files.list({
|
||||||
q: query,
|
q: formattedQuery,
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
fields: "files(id, name, mimeType, modifiedTime, size)",
|
fields: "files(id, name, mimeType, modifiedTime, size)",
|
||||||
});
|
});
|
||||||
|
|
||||||
const fileList = res.data.files
|
const fileList = res.data.files
|
||||||
?.map((file: any) => `${file.name} (${file.mimeType})`)
|
?.map((file: any) => `${file.name} (${file.mimeType})`)
|
||||||
.join("\n");
|
.join("\n");
|
||||||
|
|
@ -175,7 +177,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||||
|
|
||||||
const credentialsPath = path.join(
|
const credentialsPath = path.join(
|
||||||
path.dirname(new URL(import.meta.url).pathname),
|
path.dirname(new URL(import.meta.url).pathname),
|
||||||
"../../.gdrive-server-credentials.json",
|
"../../../.gdrive-server-credentials.json",
|
||||||
);
|
);
|
||||||
|
|
||||||
async function authenticateAndSaveCredentials() {
|
async function authenticateAndSaveCredentials() {
|
||||||
|
|
@ -183,7 +185,7 @@ async function authenticateAndSaveCredentials() {
|
||||||
const auth = await authenticate({
|
const auth = await authenticate({
|
||||||
keyfilePath: path.join(
|
keyfilePath: path.join(
|
||||||
path.dirname(new URL(import.meta.url).pathname),
|
path.dirname(new URL(import.meta.url).pathname),
|
||||||
"../../gcp-oauth.keys.json",
|
"../../../gcp-oauth.keys.json",
|
||||||
),
|
),
|
||||||
scopes: ["https://www.googleapis.com/auth/drive.readonly"],
|
scopes: ["https://www.googleapis.com/auth/drive.readonly"],
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -35,36 +35,6 @@ python -m mcp_git
|
||||||
```
|
```
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
### Configure for Zed
|
|
||||||
|
|
||||||
Add to your Zed settings.json:
|
|
||||||
|
|
||||||
```json
|
|
||||||
"experimental.context_servers": {
|
|
||||||
"servers": [
|
|
||||||
{
|
|
||||||
"id": "mcp-git",
|
|
||||||
"executable": "uvx",
|
|
||||||
"args": ["mcp-git"]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
```
|
|
||||||
|
|
||||||
Alternatively, if using pip installation:
|
|
||||||
|
|
||||||
```json
|
|
||||||
"experimental.context_servers": {
|
|
||||||
"servers": [
|
|
||||||
{
|
|
||||||
"id": "mcp-git",
|
|
||||||
"executable": "python",
|
|
||||||
"args": ["-m", "mcp_git"]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
```
|
|
||||||
|
|
||||||
### Configure for Claude.app
|
### Configure for Claude.app
|
||||||
|
|
||||||
Add to your Claude settings:
|
Add to your Claude settings:
|
||||||
|
|
@ -73,7 +43,7 @@ Add to your Claude settings:
|
||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
"mcp-git": {
|
"mcp-git": {
|
||||||
"command": "uvx",
|
"command": "uvx",
|
||||||
"args": ["mcp-git"]
|
"args": ["mcp-git", "--repository", "path/to/git/repo"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
@ -84,14 +54,39 @@ Alternatively, if using pip installation:
|
||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
"mcp-git": {
|
"mcp-git": {
|
||||||
"command": "python",
|
"command": "python",
|
||||||
"args": ["-m", "mcp_git"]
|
"args": ["-m", "mcp_git", "--repository", "path/to/git/repo"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Configure for Zed
|
||||||
|
|
||||||
|
Add to your Zed settings.json:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"context_servers": [
|
||||||
|
"mcp-git": {
|
||||||
|
"command": "uvx",
|
||||||
|
"args": ["mcp-git"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternatively, if using pip installation:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"context_servers": {
|
||||||
|
"mcp-git": {
|
||||||
|
"command": "python",
|
||||||
|
"args": ["-m", "mcp-git"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
For examples of other MCP servers and implementation patterns, see:
|
For examples of other MCP servers and implementation patterns, see:
|
||||||
https://github.com/modelcontextprotocol/example-servers/
|
https://github.com/modelcontextprotocol/servers
|
||||||
|
|
||||||
Pull requests welcome!
|
Pull requests welcome!
|
||||||
|
|
|
||||||
65
src/google-maps/README.md
Normal file
65
src/google-maps/README.md
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
# Google Maps MCP Server
|
||||||
|
|
||||||
|
MCP Server for the Google Maps API.
|
||||||
|
|
||||||
|
## Tools
|
||||||
|
|
||||||
|
1. `geocode`
|
||||||
|
- Convert address to coordinates
|
||||||
|
- Input: `address` (string)
|
||||||
|
- Returns: location, formatted_address, place_id
|
||||||
|
|
||||||
|
2. `reverse_geocode`
|
||||||
|
- Convert coordinates to address
|
||||||
|
- Inputs:
|
||||||
|
- `latitude` (number)
|
||||||
|
- `longitude` (number)
|
||||||
|
- Returns: formatted_address, place_id, address_components
|
||||||
|
|
||||||
|
3. `search_places`
|
||||||
|
- Search for places using text query
|
||||||
|
- Inputs:
|
||||||
|
- `query` (string)
|
||||||
|
- `location` (optional): { latitude: number, longitude: number }
|
||||||
|
- `radius` (optional): number (meters, max 50000)
|
||||||
|
- Returns: array of places with names, addresses, locations
|
||||||
|
|
||||||
|
4. `get_place_details`
|
||||||
|
- Get detailed information about a place
|
||||||
|
- Input: `place_id` (string)
|
||||||
|
- Returns: name, address, contact info, ratings, reviews, opening hours
|
||||||
|
|
||||||
|
5. `get_distance_matrix`
|
||||||
|
- Calculate distances and times between points
|
||||||
|
- Inputs:
|
||||||
|
- `origins` (string[])
|
||||||
|
- `destinations` (string[])
|
||||||
|
- `mode` (optional): "driving" | "walking" | "bicycling" | "transit"
|
||||||
|
- Returns: distances and durations matrix
|
||||||
|
|
||||||
|
6. `get_elevation`
|
||||||
|
- Get elevation data for locations
|
||||||
|
- Input: `locations` (array of {latitude, longitude})
|
||||||
|
- Returns: elevation data for each point
|
||||||
|
|
||||||
|
7. `get_directions`
|
||||||
|
- Get directions between points
|
||||||
|
- Inputs:
|
||||||
|
- `origin` (string)
|
||||||
|
- `destination` (string)
|
||||||
|
- `mode` (optional): "driving" | "walking" | "bicycling" | "transit"
|
||||||
|
- Returns: route details with steps, distance, duration
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
1. Get a Google Maps API key by following the instructions [here](https://developers.google.com/maps/documentation/javascript/get-api-key#create-api-keys).
|
||||||
|
|
||||||
|
2. To use this with Claude Desktop, add the following to your `claude_desktop_config.json`:
|
||||||
|
```json
|
||||||
|
"mcp-server-google-maps": {
|
||||||
|
"command": "mcp-server-google-maps",
|
||||||
|
"env": {
|
||||||
|
"GOOGLE_MAPS_API_KEY": "<YOUR_API_KEY>"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
710
src/google-maps/index.ts
Normal file
710
src/google-maps/index.ts
Normal file
|
|
@ -0,0 +1,710 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
||||||
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||||
|
import {
|
||||||
|
CallToolRequestSchema,
|
||||||
|
ListToolsRequestSchema,
|
||||||
|
Tool,
|
||||||
|
} from "@modelcontextprotocol/sdk/types.js";
|
||||||
|
import fetch from "node-fetch";
|
||||||
|
|
||||||
|
// Response interfaces
|
||||||
|
interface GoogleMapsResponse {
|
||||||
|
status: string;
|
||||||
|
error_message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GeocodeResponse extends GoogleMapsResponse {
|
||||||
|
results: Array<{
|
||||||
|
place_id: string;
|
||||||
|
formatted_address: string;
|
||||||
|
geometry: {
|
||||||
|
location: {
|
||||||
|
lat: number;
|
||||||
|
lng: number;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
address_components: Array<{
|
||||||
|
long_name: string;
|
||||||
|
short_name: string;
|
||||||
|
types: string[];
|
||||||
|
}>;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PlacesSearchResponse extends GoogleMapsResponse {
|
||||||
|
results: Array<{
|
||||||
|
name: string;
|
||||||
|
place_id: string;
|
||||||
|
formatted_address: string;
|
||||||
|
geometry: {
|
||||||
|
location: {
|
||||||
|
lat: number;
|
||||||
|
lng: number;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
rating?: number;
|
||||||
|
types: string[];
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PlaceDetailsResponse extends GoogleMapsResponse {
|
||||||
|
result: {
|
||||||
|
name: string;
|
||||||
|
place_id: string;
|
||||||
|
formatted_address: string;
|
||||||
|
formatted_phone_number?: string;
|
||||||
|
website?: string;
|
||||||
|
rating?: number;
|
||||||
|
reviews?: Array<{
|
||||||
|
author_name: string;
|
||||||
|
rating: number;
|
||||||
|
text: string;
|
||||||
|
time: number;
|
||||||
|
}>;
|
||||||
|
opening_hours?: {
|
||||||
|
weekday_text: string[];
|
||||||
|
open_now: boolean;
|
||||||
|
};
|
||||||
|
geometry: {
|
||||||
|
location: {
|
||||||
|
lat: number;
|
||||||
|
lng: number;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DistanceMatrixResponse extends GoogleMapsResponse {
|
||||||
|
origin_addresses: string[];
|
||||||
|
destination_addresses: string[];
|
||||||
|
rows: Array<{
|
||||||
|
elements: Array<{
|
||||||
|
status: string;
|
||||||
|
duration: {
|
||||||
|
text: string;
|
||||||
|
value: number;
|
||||||
|
};
|
||||||
|
distance: {
|
||||||
|
text: string;
|
||||||
|
value: number;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ElevationResponse extends GoogleMapsResponse {
|
||||||
|
results: Array<{
|
||||||
|
elevation: number;
|
||||||
|
location: {
|
||||||
|
lat: number;
|
||||||
|
lng: number;
|
||||||
|
};
|
||||||
|
resolution: number;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DirectionsResponse extends GoogleMapsResponse {
|
||||||
|
routes: Array<{
|
||||||
|
summary: string;
|
||||||
|
legs: Array<{
|
||||||
|
distance: {
|
||||||
|
text: string;
|
||||||
|
value: number;
|
||||||
|
};
|
||||||
|
duration: {
|
||||||
|
text: string;
|
||||||
|
value: number;
|
||||||
|
};
|
||||||
|
steps: Array<{
|
||||||
|
html_instructions: string;
|
||||||
|
distance: {
|
||||||
|
text: string;
|
||||||
|
value: number;
|
||||||
|
};
|
||||||
|
duration: {
|
||||||
|
text: string;
|
||||||
|
value: number;
|
||||||
|
};
|
||||||
|
travel_mode: string;
|
||||||
|
}>;
|
||||||
|
}>;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getApiKey(): string {
|
||||||
|
const apiKey = process.env.GOOGLE_MAPS_API_KEY;
|
||||||
|
if (!apiKey) {
|
||||||
|
console.error("GOOGLE_MAPS_API_KEY environment variable is not set");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
return apiKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
const GOOGLE_MAPS_API_KEY = getApiKey();
|
||||||
|
|
||||||
|
// Tool definitions
|
||||||
|
const GEOCODE_TOOL: Tool = {
|
||||||
|
name: "maps_geocode",
|
||||||
|
description: "Convert an address into geographic coordinates",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
address: {
|
||||||
|
type: "string",
|
||||||
|
description: "The address to geocode"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ["address"]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const REVERSE_GEOCODE_TOOL: Tool = {
|
||||||
|
name: "maps_reverse_geocode",
|
||||||
|
description: "Convert coordinates into an address",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
latitude: {
|
||||||
|
type: "number",
|
||||||
|
description: "Latitude coordinate"
|
||||||
|
},
|
||||||
|
longitude: {
|
||||||
|
type: "number",
|
||||||
|
description: "Longitude coordinate"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ["latitude", "longitude"]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const SEARCH_PLACES_TOOL: Tool = {
|
||||||
|
name: "maps_search_places",
|
||||||
|
description: "Search for places using Google Places API",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
query: {
|
||||||
|
type: "string",
|
||||||
|
description: "Search query"
|
||||||
|
},
|
||||||
|
location: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
latitude: { type: "number" },
|
||||||
|
longitude: { type: "number" }
|
||||||
|
},
|
||||||
|
description: "Optional center point for the search"
|
||||||
|
},
|
||||||
|
radius: {
|
||||||
|
type: "number",
|
||||||
|
description: "Search radius in meters (max 50000)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ["query"]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const PLACE_DETAILS_TOOL: Tool = {
|
||||||
|
name: "maps_place_details",
|
||||||
|
description: "Get detailed information about a specific place",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
place_id: {
|
||||||
|
type: "string",
|
||||||
|
description: "The place ID to get details for"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ["place_id"]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const DISTANCE_MATRIX_TOOL: Tool = {
|
||||||
|
name: "maps_distance_matrix",
|
||||||
|
description: "Calculate travel distance and time for multiple origins and destinations",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
origins: {
|
||||||
|
type: "array",
|
||||||
|
items: { type: "string" },
|
||||||
|
description: "Array of origin addresses or coordinates"
|
||||||
|
},
|
||||||
|
destinations: {
|
||||||
|
type: "array",
|
||||||
|
items: { type: "string" },
|
||||||
|
description: "Array of destination addresses or coordinates"
|
||||||
|
},
|
||||||
|
mode: {
|
||||||
|
type: "string",
|
||||||
|
description: "Travel mode (driving, walking, bicycling, transit)",
|
||||||
|
enum: ["driving", "walking", "bicycling", "transit"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ["origins", "destinations"]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const ELEVATION_TOOL: Tool = {
|
||||||
|
name: "maps_elevation",
|
||||||
|
description: "Get elevation data for locations on the earth",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
locations: {
|
||||||
|
type: "array",
|
||||||
|
items: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
latitude: { type: "number" },
|
||||||
|
longitude: { type: "number" }
|
||||||
|
},
|
||||||
|
required: ["latitude", "longitude"]
|
||||||
|
},
|
||||||
|
description: "Array of locations to get elevation for"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ["locations"]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const DIRECTIONS_TOOL: Tool = {
|
||||||
|
name: "maps_directions",
|
||||||
|
description: "Get directions between two points",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
origin: {
|
||||||
|
type: "string",
|
||||||
|
description: "Starting point address or coordinates"
|
||||||
|
},
|
||||||
|
destination: {
|
||||||
|
type: "string",
|
||||||
|
description: "Ending point address or coordinates"
|
||||||
|
},
|
||||||
|
mode: {
|
||||||
|
type: "string",
|
||||||
|
description: "Travel mode (driving, walking, bicycling, transit)",
|
||||||
|
enum: ["driving", "walking", "bicycling", "transit"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ["origin", "destination"]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const MAPS_TOOLS = [
|
||||||
|
GEOCODE_TOOL,
|
||||||
|
REVERSE_GEOCODE_TOOL,
|
||||||
|
SEARCH_PLACES_TOOL,
|
||||||
|
PLACE_DETAILS_TOOL,
|
||||||
|
DISTANCE_MATRIX_TOOL,
|
||||||
|
ELEVATION_TOOL,
|
||||||
|
DIRECTIONS_TOOL,
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
// API handlers
|
||||||
|
async function handleGeocode(address: string) {
|
||||||
|
const url = new URL("https://maps.googleapis.com/maps/api/geocode/json");
|
||||||
|
url.searchParams.append("address", address);
|
||||||
|
url.searchParams.append("key", GOOGLE_MAPS_API_KEY);
|
||||||
|
|
||||||
|
const response = await fetch(url.toString());
|
||||||
|
const data = await response.json() as GeocodeResponse;
|
||||||
|
|
||||||
|
if (data.status !== "OK") {
|
||||||
|
return {
|
||||||
|
toolResult: {
|
||||||
|
content: [{
|
||||||
|
type: "text",
|
||||||
|
text: `Geocoding failed: ${data.error_message || data.status}`
|
||||||
|
}],
|
||||||
|
isError: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
toolResult: {
|
||||||
|
content: [{
|
||||||
|
type: "text",
|
||||||
|
text: JSON.stringify({
|
||||||
|
location: data.results[0].geometry.location,
|
||||||
|
formatted_address: data.results[0].formatted_address,
|
||||||
|
place_id: data.results[0].place_id
|
||||||
|
}, null, 2)
|
||||||
|
}],
|
||||||
|
isError: false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleReverseGeocode(latitude: number, longitude: number) {
|
||||||
|
const url = new URL("https://maps.googleapis.com/maps/api/geocode/json");
|
||||||
|
url.searchParams.append("latlng", `${latitude},${longitude}`);
|
||||||
|
url.searchParams.append("key", GOOGLE_MAPS_API_KEY);
|
||||||
|
|
||||||
|
const response = await fetch(url.toString());
|
||||||
|
const data = await response.json() as GeocodeResponse;
|
||||||
|
|
||||||
|
if (data.status !== "OK") {
|
||||||
|
return {
|
||||||
|
toolResult: {
|
||||||
|
content: [{
|
||||||
|
type: "text",
|
||||||
|
text: `Reverse geocoding failed: ${data.error_message || data.status}`
|
||||||
|
}],
|
||||||
|
isError: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
toolResult: {
|
||||||
|
content: [{
|
||||||
|
type: "text",
|
||||||
|
text: JSON.stringify({
|
||||||
|
formatted_address: data.results[0].formatted_address,
|
||||||
|
place_id: data.results[0].place_id,
|
||||||
|
address_components: data.results[0].address_components
|
||||||
|
}, null, 2)
|
||||||
|
}],
|
||||||
|
isError: false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handlePlaceSearch(
|
||||||
|
query: string,
|
||||||
|
location?: { latitude: number; longitude: number },
|
||||||
|
radius?: number
|
||||||
|
) {
|
||||||
|
const url = new URL("https://maps.googleapis.com/maps/api/place/textsearch/json");
|
||||||
|
url.searchParams.append("query", query);
|
||||||
|
url.searchParams.append("key", GOOGLE_MAPS_API_KEY);
|
||||||
|
|
||||||
|
if (location) {
|
||||||
|
url.searchParams.append("location", `${location.latitude},${location.longitude}`);
|
||||||
|
}
|
||||||
|
if (radius) {
|
||||||
|
url.searchParams.append("radius", radius.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(url.toString());
|
||||||
|
const data = await response.json() as PlacesSearchResponse;
|
||||||
|
|
||||||
|
if (data.status !== "OK") {
|
||||||
|
return {
|
||||||
|
toolResult: {
|
||||||
|
content: [{
|
||||||
|
type: "text",
|
||||||
|
text: `Place search failed: ${data.error_message || data.status}`
|
||||||
|
}],
|
||||||
|
isError: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
toolResult: {
|
||||||
|
content: [{
|
||||||
|
type: "text",
|
||||||
|
text: JSON.stringify({
|
||||||
|
places: data.results.map((place) => ({
|
||||||
|
name: place.name,
|
||||||
|
formatted_address: place.formatted_address,
|
||||||
|
location: place.geometry.location,
|
||||||
|
place_id: place.place_id,
|
||||||
|
rating: place.rating,
|
||||||
|
types: place.types
|
||||||
|
}))
|
||||||
|
}, null, 2)
|
||||||
|
}],
|
||||||
|
isError: false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handlePlaceDetails(place_id: string) {
|
||||||
|
const url = new URL("https://maps.googleapis.com/maps/api/place/details/json");
|
||||||
|
url.searchParams.append("place_id", place_id);
|
||||||
|
url.searchParams.append("key", GOOGLE_MAPS_API_KEY);
|
||||||
|
|
||||||
|
const response = await fetch(url.toString());
|
||||||
|
const data = await response.json() as PlaceDetailsResponse;
|
||||||
|
|
||||||
|
if (data.status !== "OK") {
|
||||||
|
return {
|
||||||
|
toolResult: {
|
||||||
|
content: [{
|
||||||
|
type: "text",
|
||||||
|
text: `Place details request failed: ${data.error_message || data.status}`
|
||||||
|
}],
|
||||||
|
isError: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
toolResult: {
|
||||||
|
content: [{
|
||||||
|
type: "text",
|
||||||
|
text: JSON.stringify({
|
||||||
|
name: data.result.name,
|
||||||
|
formatted_address: data.result.formatted_address,
|
||||||
|
location: data.result.geometry.location,
|
||||||
|
formatted_phone_number: data.result.formatted_phone_number,
|
||||||
|
website: data.result.website,
|
||||||
|
rating: data.result.rating,
|
||||||
|
reviews: data.result.reviews,
|
||||||
|
opening_hours: data.result.opening_hours
|
||||||
|
}, null, 2)
|
||||||
|
}],
|
||||||
|
isError: false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
async function handleDistanceMatrix(
|
||||||
|
origins: string[],
|
||||||
|
destinations: string[],
|
||||||
|
mode: "driving" | "walking" | "bicycling" | "transit" = "driving"
|
||||||
|
) {
|
||||||
|
const url = new URL("https://maps.googleapis.com/maps/api/distancematrix/json");
|
||||||
|
url.searchParams.append("origins", origins.join("|"));
|
||||||
|
url.searchParams.append("destinations", destinations.join("|"));
|
||||||
|
url.searchParams.append("mode", mode);
|
||||||
|
url.searchParams.append("key", GOOGLE_MAPS_API_KEY);
|
||||||
|
|
||||||
|
const response = await fetch(url.toString());
|
||||||
|
const data = await response.json() as DistanceMatrixResponse;
|
||||||
|
|
||||||
|
if (data.status !== "OK") {
|
||||||
|
return {
|
||||||
|
toolResult: {
|
||||||
|
content: [{
|
||||||
|
type: "text",
|
||||||
|
text: `Distance matrix request failed: ${data.error_message || data.status}`
|
||||||
|
}],
|
||||||
|
isError: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
toolResult: {
|
||||||
|
content: [{
|
||||||
|
type: "text",
|
||||||
|
text: JSON.stringify({
|
||||||
|
origin_addresses: data.origin_addresses,
|
||||||
|
destination_addresses: data.destination_addresses,
|
||||||
|
results: data.rows.map((row) => ({
|
||||||
|
elements: row.elements.map((element) => ({
|
||||||
|
status: element.status,
|
||||||
|
duration: element.duration,
|
||||||
|
distance: element.distance
|
||||||
|
}))
|
||||||
|
}))
|
||||||
|
}, null, 2)
|
||||||
|
}],
|
||||||
|
isError: false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleElevation(locations: Array<{ latitude: number; longitude: number }>) {
|
||||||
|
const url = new URL("https://maps.googleapis.com/maps/api/elevation/json");
|
||||||
|
const locationString = locations
|
||||||
|
.map((loc) => `${loc.latitude},${loc.longitude}`)
|
||||||
|
.join("|");
|
||||||
|
url.searchParams.append("locations", locationString);
|
||||||
|
url.searchParams.append("key", GOOGLE_MAPS_API_KEY);
|
||||||
|
|
||||||
|
const response = await fetch(url.toString());
|
||||||
|
const data = await response.json() as ElevationResponse;
|
||||||
|
|
||||||
|
if (data.status !== "OK") {
|
||||||
|
return {
|
||||||
|
toolResult: {
|
||||||
|
content: [{
|
||||||
|
type: "text",
|
||||||
|
text: `Elevation request failed: ${data.error_message || data.status}`
|
||||||
|
}],
|
||||||
|
isError: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
toolResult: {
|
||||||
|
content: [{
|
||||||
|
type: "text",
|
||||||
|
text: JSON.stringify({
|
||||||
|
results: data.results.map((result) => ({
|
||||||
|
elevation: result.elevation,
|
||||||
|
location: result.location,
|
||||||
|
resolution: result.resolution
|
||||||
|
}))
|
||||||
|
}, null, 2)
|
||||||
|
}],
|
||||||
|
isError: false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDirections(
|
||||||
|
origin: string,
|
||||||
|
destination: string,
|
||||||
|
mode: "driving" | "walking" | "bicycling" | "transit" = "driving"
|
||||||
|
) {
|
||||||
|
const url = new URL("https://maps.googleapis.com/maps/api/directions/json");
|
||||||
|
url.searchParams.append("origin", origin);
|
||||||
|
url.searchParams.append("destination", destination);
|
||||||
|
url.searchParams.append("mode", mode);
|
||||||
|
url.searchParams.append("key", GOOGLE_MAPS_API_KEY);
|
||||||
|
|
||||||
|
const response = await fetch(url.toString());
|
||||||
|
const data = await response.json() as DirectionsResponse;
|
||||||
|
|
||||||
|
if (data.status !== "OK") {
|
||||||
|
return {
|
||||||
|
toolResult: {
|
||||||
|
content: [{
|
||||||
|
type: "text",
|
||||||
|
text: `Directions request failed: ${data.error_message || data.status}`
|
||||||
|
}],
|
||||||
|
isError: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
toolResult: {
|
||||||
|
content: [{
|
||||||
|
type: "text",
|
||||||
|
text: JSON.stringify({
|
||||||
|
routes: data.routes.map((route) => ({
|
||||||
|
summary: route.summary,
|
||||||
|
distance: route.legs[0].distance,
|
||||||
|
duration: route.legs[0].duration,
|
||||||
|
steps: route.legs[0].steps.map((step) => ({
|
||||||
|
instructions: step.html_instructions,
|
||||||
|
distance: step.distance,
|
||||||
|
duration: step.duration,
|
||||||
|
travel_mode: step.travel_mode
|
||||||
|
}))
|
||||||
|
}))
|
||||||
|
}, null, 2)
|
||||||
|
}],
|
||||||
|
isError: false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Server setup
|
||||||
|
const server = new Server(
|
||||||
|
{
|
||||||
|
name: "mcp-server/google-maps",
|
||||||
|
version: "0.1.0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
capabilities: {
|
||||||
|
tools: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Set up request handlers
|
||||||
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
||||||
|
tools: MAPS_TOOLS,
|
||||||
|
}));
|
||||||
|
|
||||||
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||||
|
try {
|
||||||
|
switch (request.params.name) {
|
||||||
|
case "maps_geocode": {
|
||||||
|
const { address } = request.params.arguments as { address: string };
|
||||||
|
return await handleGeocode(address);
|
||||||
|
}
|
||||||
|
|
||||||
|
case "maps_reverse_geocode": {
|
||||||
|
const { latitude, longitude } = request.params.arguments as {
|
||||||
|
latitude: number;
|
||||||
|
longitude: number;
|
||||||
|
};
|
||||||
|
return await handleReverseGeocode(latitude, longitude);
|
||||||
|
}
|
||||||
|
|
||||||
|
case "maps_search_places": {
|
||||||
|
const { query, location, radius } = request.params.arguments as {
|
||||||
|
query: string;
|
||||||
|
location?: { latitude: number; longitude: number };
|
||||||
|
radius?: number;
|
||||||
|
};
|
||||||
|
return await handlePlaceSearch(query, location, radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
case "maps_place_details": {
|
||||||
|
const { place_id } = request.params.arguments as { place_id: string };
|
||||||
|
return await handlePlaceDetails(place_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
case "maps_distance_matrix": {
|
||||||
|
const { origins, destinations, mode } = request.params.arguments as {
|
||||||
|
origins: string[];
|
||||||
|
destinations: string[];
|
||||||
|
mode?: "driving" | "walking" | "bicycling" | "transit";
|
||||||
|
};
|
||||||
|
return await handleDistanceMatrix(origins, destinations, mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
case "maps_elevation": {
|
||||||
|
const { locations } = request.params.arguments as {
|
||||||
|
locations: Array<{ latitude: number; longitude: number }>;
|
||||||
|
};
|
||||||
|
return await handleElevation(locations);
|
||||||
|
}
|
||||||
|
|
||||||
|
case "maps_directions": {
|
||||||
|
const { origin, destination, mode } = request.params.arguments as {
|
||||||
|
origin: string;
|
||||||
|
destination: string;
|
||||||
|
mode?: "driving" | "walking" | "bicycling" | "transit";
|
||||||
|
};
|
||||||
|
return await handleDirections(origin, destination, mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
toolResult: {
|
||||||
|
content: [{
|
||||||
|
type: "text",
|
||||||
|
text: `Unknown tool: ${request.params.name}`
|
||||||
|
}],
|
||||||
|
isError: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
toolResult: {
|
||||||
|
content: [{
|
||||||
|
type: "text",
|
||||||
|
text: `Error: ${error instanceof Error ? error.message : String(error)}`
|
||||||
|
}],
|
||||||
|
isError: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function runServer() {
|
||||||
|
const transport = new StdioServerTransport();
|
||||||
|
await server.connect(transport);
|
||||||
|
console.error("Google Maps MCP Server running on stdio");
|
||||||
|
}
|
||||||
|
|
||||||
|
runServer().catch((error) => {
|
||||||
|
console.error("Fatal error running server:", error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
29
src/google-maps/package.json
Normal file
29
src/google-maps/package.json
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
{
|
||||||
|
"name": "@modelcontextprotocol/server-google-maps",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "MCP server for using the Google Maps API",
|
||||||
|
"license": "MIT",
|
||||||
|
"author": "Anthropic, PBC (https://anthropic.com)",
|
||||||
|
"homepage": "https://modelcontextprotocol.io",
|
||||||
|
"bugs": "https://github.com/modelcontextprotocol/servers/issues",
|
||||||
|
"type": "module",
|
||||||
|
"bin": {
|
||||||
|
"mcp-server-google-maps": "dist/index.js"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc && shx chmod +x dist/*.js",
|
||||||
|
"prepare": "npm run build",
|
||||||
|
"watch": "tsc --watch"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@modelcontextprotocol/sdk": "0.6.0",
|
||||||
|
"@types/node-fetch": "^2.6.12"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"shx": "^0.3.4",
|
||||||
|
"typescript": "^5.6.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/google-maps/tsconfig.json
Normal file
10
src/google-maps/tsconfig.json
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "."
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"./**/*.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue