Merge branch 'main' into mahesh/update-postgres

This commit is contained in:
Mahesh Murag
2024-11-20 17:06:30 +01:00
committed by GitHub
19 changed files with 1861 additions and 160 deletions

106
package-lock.json generated
View File

@@ -14,6 +14,7 @@
"dependencies": {
"@modelcontextprotocol/server-everything": "*",
"@modelcontextprotocol/server-gdrive": "*",
"@modelcontextprotocol/server-memory": "*",
"@modelcontextprotocol/server-postgres": "*",
"@modelcontextprotocol/server-puppeteer": "*",
"@modelcontextprotocol/server-slack": "*"
@@ -72,6 +73,14 @@
"resolved": "src/gdrive",
"link": true
},
"node_modules/@modelcontextprotocol/server-google-maps": {
"resolved": "src/google-maps",
"link": true
},
"node_modules/@modelcontextprotocol/server-memory": {
"resolved": "src/memory",
"link": true
},
"node_modules/@modelcontextprotocol/server-postgres": {
"resolved": "src/postgres",
"link": true
@@ -190,11 +199,20 @@
"version": "22.9.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.0.tgz",
"integrity": "sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ==",
"devOptional": true,
"dependencies": {
"undici-types": "~6.19.8"
}
},
"node_modules/@types/node-fetch": {
"version": "2.6.12",
"resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.12.tgz",
"integrity": "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==",
"license": "MIT",
"dependencies": {
"@types/node": "*",
"form-data": "^4.0.0"
}
},
"node_modules/@types/pg": {
"version": "8.11.10",
"resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.11.10.tgz",
@@ -343,6 +361,12 @@
"node": ">=4"
}
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/b4a": {
"version": "1.6.7",
"resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz",
@@ -589,6 +613,18 @@
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -697,6 +733,15 @@
"node": ">= 14"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
@@ -973,6 +1018,20 @@
"node": ">= 0.8"
}
},
"node_modules/form-data": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz",
"integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@@ -2659,8 +2718,7 @@
"node_modules/undici-types": {
"version": "6.19.8",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
"devOptional": true
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="
},
"node_modules/universalify": {
"version": "2.0.1",
@@ -2873,6 +2931,48 @@
"typescript": "^5.6.2"
}
},
"src/google-maps": {
"name": "@modelcontextprotocol/server-google-maps",
"version": "0.1.0",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/sdk": "0.6.0",
"@types/node-fetch": "^2.6.12"
},
"bin": {
"mcp-server-google-maps": "dist/index.js"
},
"devDependencies": {
"shx": "^0.3.4",
"typescript": "^5.6.2"
}
},
"src/google-maps/node_modules/@modelcontextprotocol/sdk": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-0.6.0.tgz",
"integrity": "sha512-9rsDudGhDtMbvxohPoMMyAUOmEzQsOK+XFchh6gZGqo8sx9sBuZQs+CUttXqa8RZXKDaJRCN2tUtgGof7jRkkw==",
"license": "MIT",
"dependencies": {
"content-type": "^1.0.5",
"raw-body": "^3.0.0",
"zod": "^3.23.8"
}
},
"src/memory": {
"name": "@modelcontextprotocol/server-memory",
"version": "0.1.0",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/sdk": "0.5.0"
},
"bin": {
"mcp-server-memory": "dist/index.js"
},
"devDependencies": {
"shx": "^0.3.4",
"typescript": "^5.6.2"
}
},
"src/postgres": {
"name": "@modelcontextprotocol/server-postgres",
"version": "0.1.0",

View File

@@ -21,6 +21,7 @@
"@modelcontextprotocol/server-gdrive": "*",
"@modelcontextprotocol/server-postgres": "*",
"@modelcontextprotocol/server-puppeteer": "*",
"@modelcontextprotocol/server-slack": "*"
"@modelcontextprotocol/server-slack": "*",
"@modelcontextprotocol/server-memory": "*"
}
}

View File

@@ -2,15 +2,37 @@
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
1. Create a new Google Cloud project
2. Enable the Google Drive API
3. Configure an OAuth consent screen ("internal" is fine for testing)
1. [Create a new Google Cloud project](https://console.cloud.google.com/projectcreate)
2. [Enable the Google Drive API](https://console.cloud.google.com/workspace-api/products)
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`
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
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`.
@@ -18,16 +40,18 @@ Make sure to build the server with either `npm run build` or `npm run watch`.
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
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`
2. The server will load the saved credentials and start
Note: If you haven't authenticated yet, the server will prompt you to run with the `auth` argument first.
```json
{
"mcp-server-gdrive": {
"command": "mcp-server-gdrive"
}
}

View File

@@ -44,7 +44,7 @@ server.setRequestHandler(ListResourcesRequestSchema, async (request) => {
return {
resources: files.map((file) => ({
uri: `gdrive://${file.id}`,
uri: `gdrive:///${file.id}`,
mimeType: file.mimeType,
name: file.name,
})),
@@ -53,7 +53,7 @@ server.setRequestHandler(ListResourcesRequestSchema, 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
const file = await drive.files.get({
@@ -149,14 +149,16 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
server.setRequestHandler(CallToolRequestSchema, async (request) => {
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({
q: query,
q: formattedQuery,
pageSize: 10,
fields: "files(id, name, mimeType, modifiedTime, size)",
});
const fileList = res.data.files
?.map((file: any) => `${file.name} (${file.mimeType})`)
.join("\n");
@@ -175,7 +177,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
const credentialsPath = path.join(
path.dirname(new URL(import.meta.url).pathname),
"../../.gdrive-server-credentials.json",
"../../../.gdrive-server-credentials.json",
);
async function authenticateAndSaveCredentials() {
@@ -183,7 +185,7 @@ async function authenticateAndSaveCredentials() {
const auth = await authenticate({
keyfilePath: path.join(
path.dirname(new URL(import.meta.url).pathname),
"../../gcp-oauth.keys.json",
"../../../gcp-oauth.keys.json",
),
scopes: ["https://www.googleapis.com/auth/drive.readonly"],
});

View File

@@ -1 +1 @@
3.11
3.10

View File

@@ -1,9 +1,13 @@
# mcp-git
# mcp-git: A git MCP server
A Model Context Protocol server for Git repository interaction and automation. This server provides tools to read, search, and manipulate Git repositories via Large Language Models.
Please note that mcp-git is currently in early development. The functionality and available tools are subject to change and expansion as we continue to develop and improve the server.
## Available Tools
The current list of tools includes:
- `git_read_file`: Read contents of a file at a specific Git reference
- `git_list_files`: List all files in a repository or subdirectory
- `git_file_history`: Get commit history for a specific file
@@ -12,10 +16,15 @@ A Model Context Protocol server for Git repository interaction and automation. T
- `git_get_diff`: View diffs between Git references
- `git_get_repo_structure`: View repository file structure
- `git_list_repos`: List available Git repositories
- `git_log`: Retrieve commit log for the repository
- `git_list_branches`: List all branches in the repository
- `git_list_tags`: List all tags in the repository
This list is expected to grow as we add more functionality to the server. We welcome contributions from the community to expand and enhance the available tools.
## Installation
### Using uv
### Using uv (recommended)
When using [`uv`](https://docs.astral.sh/uv/) no specific installation is needed. We will
use [`uvx`](https://docs.astral.sh/uv/guides/tools/) to directly run *mcp-git*.
@@ -35,36 +44,6 @@ python -m mcp_git
```
## 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
Add to your Claude settings:
@@ -73,7 +52,7 @@ Add to your Claude settings:
"mcpServers": {
"mcp-git": {
"command": "uvx",
"args": ["mcp-git"]
"args": ["mcp-git", "--repository", "path/to/git/repo"]
}
}
```
@@ -84,14 +63,41 @@ Alternatively, if using pip installation:
"mcpServers": {
"mcp-git": {
"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
For examples of other MCP servers and implementation patterns, see:
https://github.com/modelcontextprotocol/example-servers/
We encourage contributions to help expand and improve mcp-git. Whether you want to add new tools, enhance existing functionality, or improve documentation, your input is valuable.
Pull requests welcome!
For examples of other MCP servers and implementation patterns, see:
https://github.com/modelcontextprotocol/servers
Pull requests are welcome! Feel free to contribute new ideas, bug fixes, or enhancements to make mcp-git even more powerful and useful.

View File

@@ -1,9 +1,9 @@
[project]
name = "mcp-git"
version = "0.1.0"
version = "0.2.0"
description = "A Model Context Protocol server providing tools to read, search, and manipulate Git repositories programmatically via LLMs"
readme = "README.md"
requires-python = ">=3.11"
requires-python = ">=3.10"
authors = [{ name = "Anthropic, PBC." }]
maintainers = [{ name = "David Soria Parra", email = "davidsp@anthropic.com" }]
keywords = ["git", "mcp", "llm", "automation"]
@@ -13,12 +13,12 @@ classifiers = [
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.10",
]
dependencies = [
"click>=8.1.7",
"gitpython>=3.1.43",
"mcp-python~=0.6.0",
"mcp>=0.6.0",
"pydantic>=2.0.0",
]
@@ -30,5 +30,4 @@ requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.uv]
index-strategy = "unsafe-best-match"
dev-dependencies = ["ruff>=0.7.3"]

View File

@@ -1,17 +1,26 @@
import logging
import json
import sys
import click
import anyio
import anyio.lowlevel
from pathlib import Path
from git.types import Sequence
from mcp_python.server import Server
from mcp_python.server.stdio import stdio_server
from mcp_python.types import Tool
from mcp_python.server.types import EmbeddedResource, ImageContent
from enum import StrEnum
from mcp.server import Server
from mcp.server.session import ServerSession
from mcp.server.stdio import stdio_server
from mcp.types import (
ClientCapabilities,
TextContent,
Tool,
EmbeddedResource,
ImageContent,
ListRootsResult,
RootsCapability,
)
from enum import Enum
import git
from git.objects import Blob, Tree
from mcp_python import ServerSession
from pydantic import BaseModel, Field
from typing import List, Optional
@@ -67,7 +76,21 @@ class ListReposInput(BaseModel):
pass
class GitTools(StrEnum):
class GitLogInput(BaseModel):
repo_path: str
max_count: int = 10
ref: str = "HEAD"
class ListBranchesInput(BaseModel):
repo_path: str
class ListTagsInput(BaseModel):
repo_path: str
class GitTools(str, Enum):
READ_FILE = "git_read_file"
LIST_FILES = "git_list_files"
FILE_HISTORY = "git_file_history"
@@ -76,12 +99,19 @@ class GitTools(StrEnum):
GET_DIFF = "git_get_diff"
GET_REPO_STRUCTURE = "git_get_repo_structure"
LIST_REPOS = "git_list_repos"
GIT_LOG = "git_log"
LIST_BRANCHES = "git_list_branches"
LIST_TAGS = "git_list_tags"
def git_read_file(repo: git.Repo, file_path: str, ref: str = "HEAD") -> str:
tree = repo.commit(ref).tree
blob = tree / file_path
return blob.data_stream.read().decode("utf-8", errors="replace")
try:
return blob.data_stream.read().decode("utf-8", errors="replace")
except UnicodeDecodeError:
# If it's a binary file, return a message indicating that
return "[Binary file content not shown]"
def git_list_files(repo: git.Repo, path: str = "", ref: str = "HEAD") -> Sequence[str]:
@@ -123,10 +153,14 @@ def git_search_code(
tree = repo.commit(ref).tree
for blob in tree.traverse():
if isinstance(blob, Blob) and Path(blob.path).match(file_pattern):
content = blob.data_stream.read().decode("utf-8")
for i, line in enumerate(content.splitlines()):
if query in line:
results.append(f"{blob.path}:{i+1}: {line}")
try:
content = blob.data_stream.read().decode("utf-8", errors="replace")
for i, line in enumerate(content.splitlines()):
if query in line:
results.append(f"{blob.path}:{i+1}: {line}")
except UnicodeDecodeError:
# Skip binary files
continue
return results
@@ -154,20 +188,41 @@ def git_get_repo_structure(repo: git.Repo, ref: str = "HEAD") -> str:
return str(structure)
def git_log(repo: git.Repo, max_count: int = 10, ref: str = "HEAD") -> list[str]:
commits = list(repo.iter_commits(ref, max_count=max_count))
log = []
for commit in commits:
log.append(
f"Commit: {commit.hexsha}\n"
f"Author: {commit.author}\n"
f"Date: {commit.authored_datetime}\n"
f"Message: {commit.message}\n"
)
return log
def git_list_branches(repo: git.Repo) -> list[str]:
return [str(branch) for branch in repo.branches]
def git_list_tags(repo: git.Repo) -> list[str]:
return [str(tag) for tag in repo.tags]
async def serve(repository: Path | None) -> None:
# Set up logging
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
if repository is not None:
try:
git.Repo(repository)
logger.info(f"Using repository at {repository}")
except git.InvalidGitRepositoryError:
logger.error(f"{repository} is not a valid Git repository")
return
# Create server
server = Server("git-mcp")
server = Server("mcp-git")
@server.list_tools()
async def list_tools() -> list[Tool]:
@@ -235,6 +290,28 @@ async def serve(repository: Path | None) -> None:
"accessible to the current session.",
inputSchema=ListReposInput.schema(),
),
Tool(
name=GitTools.GIT_LOG,
description="Retrieves the commit log for the repository, showing the "
"history of commits including commit hashes, authors, dates, and "
"commit messages. This tool provides an overview of the project's "
"development history.",
inputSchema=GitLogInput.schema(),
),
Tool(
name=GitTools.LIST_BRANCHES,
description="Lists all branches in the Git repository. This tool "
"provides an overview of the different lines of development in the "
"project.",
inputSchema=ListBranchesInput.schema(),
),
Tool(
name=GitTools.LIST_TAGS,
description="Lists all tags in the Git repository. This tool "
"provides an overview of the tagged versions or releases in the "
"project.",
inputSchema=ListTagsInput.schema(),
),
]
async def list_repos() -> Sequence[str]:
@@ -244,7 +321,14 @@ async def serve(repository: Path | None) -> None:
"server.request_context.session must be a ServerSession"
)
roots_result = await server.request_context.session.list_roots()
if not server.request_context.session.check_client_capability(
ClientCapabilities(roots=RootsCapability())
):
return []
roots_result: ListRootsResult = (
await server.request_context.session.list_roots()
)
logger.debug(f"Roots result: {roots_result}")
repo_paths = []
for root in roots_result.roots:
@@ -267,58 +351,128 @@ async def serve(repository: Path | None) -> None:
@server.call_tool()
async def call_tool(
name: str, arguments: dict
) -> Sequence[str | ImageContent | EmbeddedResource]:
) -> list[TextContent | ImageContent | EmbeddedResource]:
if name == GitTools.LIST_REPOS:
return await list_repos()
result = await list_repos()
logging.debug(f"repos={result}")
return [
TextContent(
type="text",
text=f"Here is some JSON that contains a list of git repositories: {json.dumps(result)}",
)
]
repo_path = Path(arguments["repo_path"])
repo = git.Repo(repo_path)
match name:
case GitTools.READ_FILE:
content = git_read_file(
repo, arguments["file_path"], arguments.get("ref", "HEAD")
)
return [
git_read_file(
repo, arguments["file_path"], arguments.get("ref", "HEAD")
TextContent(
type="text",
text=f"Here is some JSON that contains the contents of a file: {json.dumps({'content': content})}",
)
]
case GitTools.LIST_FILES:
files = git_list_files(
repo, arguments.get("path", ""), arguments.get("ref", "HEAD")
)
return [
str(f)
for f in git_list_files(
repo, arguments.get("path", ""), arguments.get("ref", "HEAD")
TextContent(
type="text",
text=f"Here is some JSON that contains a list of files: {json.dumps({'files': list(files)})}",
)
]
case GitTools.FILE_HISTORY:
return git_file_history(
history = git_file_history(
repo, arguments["file_path"], arguments.get("max_entries", 10)
)
return [
TextContent(
type="text",
text=f"Here is some JSON that contains a file's history: {json.dumps({'history': list(history)})}",
)
]
case GitTools.COMMIT:
result = git_commit(repo, arguments["message"], arguments.get("files"))
return [result]
return [
TextContent(
type="text",
text=f"Here is some JSON that contains the commit result: {json.dumps({'result': result})}",
)
]
case GitTools.SEARCH_CODE:
return git_search_code(
results = git_search_code(
repo,
arguments["query"],
arguments.get("file_pattern", "*"),
arguments.get("ref", "HEAD"),
)
return [
TextContent(
type="text",
text=f"Here is some JSON that contains code search matches: {json.dumps({'matches': results})}",
)
]
case GitTools.GET_DIFF:
diff = git_get_diff(
repo,
arguments["ref1"],
arguments["ref2"],
arguments.get("file_path"),
)
return [
git_get_diff(
repo,
arguments["ref1"],
arguments["ref2"],
arguments.get("file_path"),
TextContent(
type="text",
text=f"Here is some JSON that contains a diff: {json.dumps({'diff': diff})}",
)
]
case GitTools.GET_REPO_STRUCTURE:
return [git_get_repo_structure(repo, arguments.get("ref", "HEAD"))]
structure = git_get_repo_structure(repo, arguments.get("ref", "HEAD"))
return [
TextContent(
type="text",
text=f"Here is some JSON that contains the repository structure: {json.dumps({'structure': structure})}",
)
]
case GitTools.GIT_LOG:
log = git_log(
repo, arguments.get("max_count", 10), arguments.get("ref", "HEAD")
)
return [
TextContent(
type="text",
text=f"Here is some JSON that contains the git log: {json.dumps({'log': log})}",
)
]
case GitTools.LIST_BRANCHES:
branches = git_list_branches(repo)
return [
TextContent(
type="text",
text=f"Here is some JSON that contains a list of branches: {json.dumps({'branches': branches})}",
)
]
case GitTools.LIST_TAGS:
tags = git_list_tags(repo)
return [
TextContent(
type="text",
text=f"Here is some JSON that contains a list of tags: {json.dumps({'tags': tags})}",
)
]
case _:
raise ValueError(f"Unknown tool: {name}")
@@ -326,12 +480,19 @@ async def serve(repository: Path | None) -> None:
# Run the server
options = server.create_initialization_options()
async with stdio_server() as (read_stream, write_stream):
await server.run(read_stream, write_stream, options)
await server.run(read_stream, write_stream, options, raise_exceptions=True)
@click.command()
@click.option("-r", "--repository", type=click.Path(path_type=Path, dir_okay=True))
def main(repository: Path | None):
@click.option("-v", "--verbose", count=True)
def main(repository: Path | None, verbose: int):
logging_level = logging.WARN
if verbose == 1:
logging_level = logging.INFO
elif verbose >= 2:
logging_level = logging.DEBUG
logging.basicConfig(level=logging_level, stream=sys.stderr)
anyio.run(serve, repository)

134
src/git/uv.lock generated
View File

@@ -1,5 +1,5 @@
version = 1
requires-python = ">=3.11"
requires-python = ">=3.10"
resolution-markers = [
"python_full_version < '3.13'",
"python_full_version >= '3.13'",
@@ -19,8 +19,10 @@ name = "anyio"
version = "4.6.2.post1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
{ name = "idna" },
{ name = "sniffio" },
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/9f/09/45b9b7a6d4e45c6bcb5bf61d19e3ab87df68e0601fa8c5293de3542546cc/anyio-4.6.2.post1.tar.gz", hash = "sha256:4c8bc31ccdb51c7f7bd251f51c609e038d63e34219b44aa86e47576389880b4c", size = 173422 }
wheels = [
@@ -51,9 +53,19 @@ wheels = [
[[package]]
name = "colorama"
version = "0.4.6"
source = { registry = "https://artifactory.infra.ant.dev/artifactory/api/pypi/pypi-internal/simple" }
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 }
wheels = [
{ url = "https://artifactory.infra.ant.dev/artifactory/api/pypi/pypi-internal/colorama/0.4.6/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6" },
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
]
[[package]]
name = "exceptiongroup"
version = "1.2.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 },
]
[[package]]
@@ -83,22 +95,23 @@ wheels = [
[[package]]
name = "h11"
version = "0.14.0"
source = { registry = "https://artifactory.infra.ant.dev/artifactory/api/pypi/pypi-internal/simple" }
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 }
wheels = [
{ url = "https://artifactory.infra.ant.dev/artifactory/api/pypi/pypi-internal/h11/0.14.0/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761" },
{ url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 },
]
[[package]]
name = "httpcore"
version = "1.0.6"
version = "1.0.7"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "h11" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b6/44/ed0fa6a17845fb033bd885c03e842f08c1b9406c86a2e60ac1ae1b9206a6/httpcore-1.0.6.tar.gz", hash = "sha256:73f6dbd6eb8c21bbf7ef8efad555481853f5f6acdeaff1edb0694289269ee17f", size = 85180 }
sdist = { url = "https://files.pythonhosted.org/packages/6a/41/d7d0a89eb493922c37d343b607bc1b5da7f5be7e383740b4753ad8943e90/httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c", size = 85196 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/06/89/b161908e2f51be56568184aeb4a880fd287178d176fd1c860d2217f41106/httpcore-1.0.6-py3-none-any.whl", hash = "sha256:27b59625743b85577a8c0e10e55b50b5368a4f2cfe8cc7bcfa9cf00829c2682f", size = 78011 },
{ url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551 },
]
[[package]]
@@ -135,14 +148,31 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 },
]
[[package]]
name = "mcp"
version = "0.9.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "httpx" },
{ name = "httpx-sse" },
{ name = "pydantic" },
{ name = "sse-starlette" },
{ name = "starlette" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e7/1c/932818470ffd49c33509110c835101a8dc4c9cdd06028b9f647fb3dde237/mcp-0.9.1.tar.gz", hash = "sha256:e8509a37c2ab546095788ed170e0fb4d7ce0cf5a3ee56b6449c78af27321a425", size = 78218 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b3/a0/2ee813d456b57a726d583868417d1ad900fbe12ee3c8cd866e3e804ca486/mcp-0.9.1-py3-none-any.whl", hash = "sha256:7f640fcfb0be486aa510594df309920ae1d375cdca1f8aff21db3a96d837f303", size = 31562 },
]
[[package]]
name = "mcp-git"
version = "0.1.0"
version = "0.2.0"
source = { editable = "." }
dependencies = [
{ name = "click" },
{ name = "gitpython" },
{ name = "mcp-python" },
{ name = "mcp" },
{ name = "pydantic" },
]
@@ -155,30 +185,13 @@ dev = [
requires-dist = [
{ name = "click", specifier = ">=8.1.7" },
{ name = "gitpython", specifier = ">=3.1.43" },
{ name = "mcp-python", specifier = "~=0.6.0" },
{ name = "mcp", specifier = ">=0.6.0" },
{ name = "pydantic", specifier = ">=2.0.0" },
]
[package.metadata.requires-dev]
dev = [{ name = "ruff", specifier = ">=0.7.3" }]
[[package]]
name = "mcp-python"
version = "0.6.1"
source = { registry = "https://artifactory.infra.ant.dev/artifactory/api/pypi/pypi-internal/simple" }
dependencies = [
{ name = "anyio" },
{ name = "httpx" },
{ name = "httpx-sse" },
{ name = "pydantic" },
{ name = "sse-starlette" },
{ name = "starlette" },
]
sdist = { url = "https://artifactory.infra.ant.dev/artifactory/api/pypi/pypi-internal/mcp-python/0.6.1/mcp_python-0.6.1.tar.gz", hash = "sha256:e8a2a6067e36b5790397d678686ffe1642ca1d2249f667d3b9f44bcf3b506b1c" }
wheels = [
{ url = "https://artifactory.infra.ant.dev/artifactory/api/pypi/pypi-internal/mcp-python/0.6.1/mcp_python-0.6.1-py3-none-any.whl", hash = "sha256:812cf7e7da61b6ca5a2498150d152417bdd8519a0ee24a6964442f473599aa8c" },
]
[[package]]
name = "pydantic"
version = "2.9.2"
@@ -202,6 +215,18 @@ dependencies = [
]
sdist = { url = "https://files.pythonhosted.org/packages/e2/aa/6b6a9b9f8537b872f552ddd46dd3da230367754b6f707b8e1e963f515ea3/pydantic_core-2.23.4.tar.gz", hash = "sha256:2584f7cf844ac4d970fba483a717dbe10c1c1c96a969bf65d61ffe94df1b2863", size = 402156 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5c/8b/d3ae387f66277bd8104096d6ec0a145f4baa2966ebb2cad746c0920c9526/pydantic_core-2.23.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:b10bd51f823d891193d4717448fab065733958bdb6a6b351967bd349d48d5c9b", size = 1867835 },
{ url = "https://files.pythonhosted.org/packages/46/76/f68272e4c3a7df8777798282c5e47d508274917f29992d84e1898f8908c7/pydantic_core-2.23.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4fc714bdbfb534f94034efaa6eadd74e5b93c8fa6315565a222f7b6f42ca1166", size = 1776689 },
{ url = "https://files.pythonhosted.org/packages/cc/69/5f945b4416f42ea3f3bc9d2aaec66c76084a6ff4ff27555bf9415ab43189/pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63e46b3169866bd62849936de036f901a9356e36376079b05efa83caeaa02ceb", size = 1800748 },
{ url = "https://files.pythonhosted.org/packages/50/ab/891a7b0054bcc297fb02d44d05c50e68154e31788f2d9d41d0b72c89fdf7/pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed1a53de42fbe34853ba90513cea21673481cd81ed1be739f7f2efb931b24916", size = 1806469 },
{ url = "https://files.pythonhosted.org/packages/31/7c/6e3fa122075d78f277a8431c4c608f061881b76c2b7faca01d317ee39b5d/pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cfdd16ab5e59fc31b5e906d1a3f666571abc367598e3e02c83403acabc092e07", size = 2002246 },
{ url = "https://files.pythonhosted.org/packages/ad/6f/22d5692b7ab63fc4acbc74de6ff61d185804a83160adba5e6cc6068e1128/pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255a8ef062cbf6674450e668482456abac99a5583bbafb73f9ad469540a3a232", size = 2659404 },
{ url = "https://files.pythonhosted.org/packages/11/ac/1e647dc1121c028b691028fa61a4e7477e6aeb5132628fde41dd34c1671f/pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a7cd62e831afe623fbb7aabbb4fe583212115b3ef38a9f6b71869ba644624a2", size = 2053940 },
{ url = "https://files.pythonhosted.org/packages/91/75/984740c17f12c3ce18b5a2fcc4bdceb785cce7df1511a4ce89bca17c7e2d/pydantic_core-2.23.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f09e2ff1f17c2b51f2bc76d1cc33da96298f0a036a137f5440ab3ec5360b624f", size = 1921437 },
{ url = "https://files.pythonhosted.org/packages/a0/74/13c5f606b64d93f0721e7768cd3e8b2102164866c207b8cd6f90bb15d24f/pydantic_core-2.23.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e38e63e6f3d1cec5a27e0afe90a085af8b6806ee208b33030e65b6516353f1a3", size = 1966129 },
{ url = "https://files.pythonhosted.org/packages/18/03/9c4aa5919457c7b57a016c1ab513b1a926ed9b2bb7915bf8e506bf65c34b/pydantic_core-2.23.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0dbd8dbed2085ed23b5c04afa29d8fd2771674223135dc9bc937f3c09284d071", size = 2110908 },
{ url = "https://files.pythonhosted.org/packages/92/2c/053d33f029c5dc65e5cf44ff03ceeefb7cce908f8f3cca9265e7f9b540c8/pydantic_core-2.23.4-cp310-none-win32.whl", hash = "sha256:6531b7ca5f951d663c339002e91aaebda765ec7d61b7d1e3991051906ddde119", size = 1735278 },
{ url = "https://files.pythonhosted.org/packages/de/81/7dfe464eca78d76d31dd661b04b5f2036ec72ea8848dd87ab7375e185c23/pydantic_core-2.23.4-cp310-none-win_amd64.whl", hash = "sha256:7c9129eb40958b3d4500fa2467e6a83356b3b61bfff1b414c7361d9220f9ae8f", size = 1917453 },
{ url = "https://files.pythonhosted.org/packages/5d/30/890a583cd3f2be27ecf32b479d5d615710bb926d92da03e3f7838ff3e58b/pydantic_core-2.23.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:77733e3892bb0a7fa797826361ce8a9184d25c8dffaec60b7ffe928153680ba8", size = 1865160 },
{ url = "https://files.pythonhosted.org/packages/1d/9a/b634442e1253bc6889c87afe8bb59447f106ee042140bd57680b3b113ec7/pydantic_core-2.23.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b84d168f6c48fabd1f2027a3d1bdfe62f92cade1fb273a5d68e621da0e44e6d", size = 1776777 },
{ url = "https://files.pythonhosted.org/packages/75/9a/7816295124a6b08c24c96f9ce73085032d8bcbaf7e5a781cd41aa910c891/pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df49e7a0861a8c36d089c1ed57d308623d60416dab2647a4a17fe050ba85de0e", size = 1799244 },
@@ -238,31 +263,39 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/16/16/b805c74b35607d24d37103007f899abc4880923b04929547ae68d478b7f4/pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed541d70698978a20eb63d8c5d72f2cc6d7079d9d90f6b50bad07826f1320f5f", size = 2116814 },
{ url = "https://files.pythonhosted.org/packages/d1/58/5305e723d9fcdf1c5a655e6a4cc2a07128bf644ff4b1d98daf7a9dbf57da/pydantic_core-2.23.4-cp313-none-win32.whl", hash = "sha256:3d5639516376dce1940ea36edf408c554475369f5da2abd45d44621cb616f769", size = 1738360 },
{ url = "https://files.pythonhosted.org/packages/a5/ae/e14b0ff8b3f48e02394d8acd911376b7b66e164535687ef7dc24ea03072f/pydantic_core-2.23.4-cp313-none-win_amd64.whl", hash = "sha256:5a1504ad17ba4210df3a045132a7baeeba5a200e930f57512ee02909fc5c4cb5", size = 1919411 },
{ url = "https://files.pythonhosted.org/packages/13/a9/5d582eb3204464284611f636b55c0a7410d748ff338756323cb1ce721b96/pydantic_core-2.23.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f455ee30a9d61d3e1a15abd5068827773d6e4dc513e795f380cdd59932c782d5", size = 1857135 },
{ url = "https://files.pythonhosted.org/packages/2c/57/faf36290933fe16717f97829eabfb1868182ac495f99cf0eda9f59687c9d/pydantic_core-2.23.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1e90d2e3bd2c3863d48525d297cd143fe541be8bbf6f579504b9712cb6b643ec", size = 1740583 },
{ url = "https://files.pythonhosted.org/packages/91/7c/d99e3513dc191c4fec363aef1bf4c8af9125d8fa53af7cb97e8babef4e40/pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e203fdf807ac7e12ab59ca2bfcabb38c7cf0b33c41efeb00f8e5da1d86af480", size = 1793637 },
{ url = "https://files.pythonhosted.org/packages/29/18/812222b6d18c2d13eebbb0f7cdc170a408d9ced65794fdb86147c77e1982/pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e08277a400de01bc72436a0ccd02bdf596631411f592ad985dcee21445bd0068", size = 1941963 },
{ url = "https://files.pythonhosted.org/packages/0f/36/c1f3642ac3f05e6bb4aec3ffc399fa3f84895d259cf5f0ce3054b7735c29/pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f220b0eea5965dec25480b6333c788fb72ce5f9129e8759ef876a1d805d00801", size = 1915332 },
{ url = "https://files.pythonhosted.org/packages/f7/ca/9c0854829311fb446020ebb540ee22509731abad886d2859c855dd29b904/pydantic_core-2.23.4-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d06b0c8da4f16d1d1e352134427cb194a0a6e19ad5db9161bf32b2113409e728", size = 1957926 },
{ url = "https://files.pythonhosted.org/packages/c0/1c/7836b67c42d0cd4441fcd9fafbf6a027ad4b79b6559f80cf11f89fd83648/pydantic_core-2.23.4-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ba1a0996f6c2773bd83e63f18914c1de3c9dd26d55f4ac302a7efe93fb8e7433", size = 2100342 },
{ url = "https://files.pythonhosted.org/packages/a9/f9/b6bcaf874f410564a78908739c80861a171788ef4d4f76f5009656672dfe/pydantic_core-2.23.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:9a5bce9d23aac8f0cf0836ecfc033896aa8443b501c58d0602dbfd5bd5b37753", size = 1920344 },
]
[[package]]
name = "ruff"
version = "0.7.3"
version = "0.7.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/4b/06/09d1276df977eece383d0ed66052fc24ec4550a61f8fbc0a11200e690496/ruff-0.7.3.tar.gz", hash = "sha256:e1d1ba2e40b6e71a61b063354d04be669ab0d39c352461f3d789cac68b54a313", size = 3243664 }
sdist = { url = "https://files.pythonhosted.org/packages/0b/8b/bc4e0dfa1245b07cf14300e10319b98e958a53ff074c1dd86b35253a8c2a/ruff-0.7.4.tar.gz", hash = "sha256:cd12e35031f5af6b9b93715d8c4f40360070b2041f81273d0527683d5708fce2", size = 3275547 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c0/56/933d433c2489e4642487b835f53dd9ff015fb3d8fa459b09bb2ce42d7c4b/ruff-0.7.3-py3-none-linux_armv6l.whl", hash = "sha256:34f2339dc22687ec7e7002792d1f50712bf84a13d5152e75712ac08be565d344", size = 10372090 },
{ url = "https://files.pythonhosted.org/packages/20/ea/1f0a22a6bcdd3fc26c73f63a025d05bd565901b729d56bcb093c722a6c4c/ruff-0.7.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:fb397332a1879b9764a3455a0bb1087bda876c2db8aca3a3cbb67b3dbce8cda0", size = 10190037 },
{ url = "https://files.pythonhosted.org/packages/16/74/aca75666e0d481fe394e76a8647c44ea919087748024924baa1a17371e3e/ruff-0.7.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:37d0b619546103274e7f62643d14e1adcbccb242efda4e4bdb9544d7764782e9", size = 9811998 },
{ url = "https://files.pythonhosted.org/packages/20/a1/cf446a0d7f78ea1f0bd2b9171c11dfe746585c0c4a734b25966121eb4f5d/ruff-0.7.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d59f0c3ee4d1a6787614e7135b72e21024875266101142a09a61439cb6e38a5", size = 10620626 },
{ url = "https://files.pythonhosted.org/packages/cd/c1/82b27d09286ae855f5d03b1ad37cf243f21eb0081732d4d7b0d658d439cb/ruff-0.7.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:44eb93c2499a169d49fafd07bc62ac89b1bc800b197e50ff4633aed212569299", size = 10177598 },
{ url = "https://files.pythonhosted.org/packages/b9/42/c0acac22753bf74013d035a5ef6c5c4c40ad4d6686bfb3fda7c6f37d9b37/ruff-0.7.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6d0242ce53f3a576c35ee32d907475a8d569944c0407f91d207c8af5be5dae4e", size = 11171963 },
{ url = "https://files.pythonhosted.org/packages/43/18/bb0befb7fb9121dd9009e6a72eb98e24f1bacb07c6f3ecb55f032ba98aed/ruff-0.7.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:6b6224af8b5e09772c2ecb8dc9f3f344c1aa48201c7f07e7315367f6dd90ac29", size = 11856157 },
{ url = "https://files.pythonhosted.org/packages/5e/91/04e98d7d6e32eca9d1372be595f9abc7b7f048795e32eb2edbd8794d50bd/ruff-0.7.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c50f95a82b94421c964fae4c27c0242890a20fe67d203d127e84fbb8013855f5", size = 11440331 },
{ url = "https://files.pythonhosted.org/packages/f5/dc/3fe99f2ce10b76d389041a1b9f99e7066332e479435d4bebcceea16caff5/ruff-0.7.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7f3eff9961b5d2644bcf1616c606e93baa2d6b349e8aa8b035f654df252c8c67", size = 12725354 },
{ url = "https://files.pythonhosted.org/packages/43/7b/1daa712de1c5bc6cbbf9fa60e9c41cc48cda962dc6d2c4f2a224d2c3007e/ruff-0.7.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8963cab06d130c4df2fd52c84e9f10d297826d2e8169ae0c798b6221be1d1d2", size = 11010091 },
{ url = "https://files.pythonhosted.org/packages/b6/db/1227a903587432eb569e57a95b15a4f191a71fe315cde4c0312df7bc85da/ruff-0.7.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:61b46049d6edc0e4317fb14b33bd693245281a3007288b68a3f5b74a22a0746d", size = 10610687 },
{ url = "https://files.pythonhosted.org/packages/db/e2/dc41ee90c3085aadad4da614d310d834f641aaafddf3dfbba08210c616ce/ruff-0.7.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:10ebce7696afe4644e8c1a23b3cf8c0f2193a310c18387c06e583ae9ef284de2", size = 10254843 },
{ url = "https://files.pythonhosted.org/packages/6f/09/5f6cac1c91542bc5bd33d40b4c13b637bf64d7bb29e091dadb01b62527fe/ruff-0.7.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:3f36d56326b3aef8eeee150b700e519880d1aab92f471eefdef656fd57492aa2", size = 10730962 },
{ url = "https://files.pythonhosted.org/packages/d3/42/89a4b9a24ef7d00269e24086c417a006f9a3ffeac2c80f2629eb5ce140ee/ruff-0.7.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5d024301109a0007b78d57ab0ba190087b43dce852e552734ebf0b0b85e4fb16", size = 11101907 },
{ url = "https://files.pythonhosted.org/packages/b0/5c/efdb4777686683a8edce94ffd812783bddcd3d2454d38c5ac193fef7c500/ruff-0.7.3-py3-none-win32.whl", hash = "sha256:4ba81a5f0c5478aa61674c5a2194de8b02652f17addf8dfc40c8937e6e7d79fc", size = 8611095 },
{ url = "https://files.pythonhosted.org/packages/bb/b8/28fbc6a4efa50178f973972d1c84b2d0a33cdc731588522ab751ac3da2f5/ruff-0.7.3-py3-none-win_amd64.whl", hash = "sha256:588a9ff2fecf01025ed065fe28809cd5a53b43505f48b69a1ac7707b1b7e4088", size = 9418283 },
{ url = "https://files.pythonhosted.org/packages/3f/77/b587cba6febd5e2003374f37eb89633f79f161e71084f94057c8653b7fb3/ruff-0.7.3-py3-none-win_arm64.whl", hash = "sha256:1713e2c5545863cdbfe2cbce21f69ffaf37b813bfd1fb3b90dc9a6f1963f5a8c", size = 8725228 },
{ url = "https://files.pythonhosted.org/packages/e6/4b/f5094719e254829766b807dadb766841124daba75a37da83e292ae5ad12f/ruff-0.7.4-py3-none-linux_armv6l.whl", hash = "sha256:a4919925e7684a3f18e18243cd6bea7cfb8e968a6eaa8437971f681b7ec51478", size = 10447512 },
{ url = "https://files.pythonhosted.org/packages/9e/1d/3d2d2c9f601cf6044799c5349ff5267467224cefed9b35edf5f1f36486e9/ruff-0.7.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:cfb365c135b830778dda8c04fb7d4280ed0b984e1aec27f574445231e20d6c63", size = 10235436 },
{ url = "https://files.pythonhosted.org/packages/62/83/42a6ec6216ded30b354b13e0e9327ef75a3c147751aaf10443756cb690e9/ruff-0.7.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:63a569b36bc66fbadec5beaa539dd81e0527cb258b94e29e0531ce41bacc1f20", size = 9888936 },
{ url = "https://files.pythonhosted.org/packages/4d/26/e1e54893b13046a6ad05ee9b89ee6f71542ba250f72b4c7a7d17c3dbf73d/ruff-0.7.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d06218747d361d06fd2fdac734e7fa92df36df93035db3dc2ad7aa9852cb109", size = 10697353 },
{ url = "https://files.pythonhosted.org/packages/21/24/98d2e109c4efc02bfef144ec6ea2c3e1217e7ce0cfddda8361d268dfd499/ruff-0.7.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e0cea28d0944f74ebc33e9f934238f15c758841f9f5edd180b5315c203293452", size = 10228078 },
{ url = "https://files.pythonhosted.org/packages/ad/b7/964c75be9bc2945fc3172241b371197bb6d948cc69e28bc4518448c368f3/ruff-0.7.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:80094ecd4793c68b2571b128f91754d60f692d64bc0d7272ec9197fdd09bf9ea", size = 11264823 },
{ url = "https://files.pythonhosted.org/packages/12/8d/20abdbf705969914ce40988fe71a554a918deaab62c38ec07483e77866f6/ruff-0.7.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:997512325c6620d1c4c2b15db49ef59543ef9cd0f4aa8065ec2ae5103cedc7e7", size = 11951855 },
{ url = "https://files.pythonhosted.org/packages/b8/fc/6519ce58c57b4edafcdf40920b7273dfbba64fc6ebcaae7b88e4dc1bf0a8/ruff-0.7.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00b4cf3a6b5fad6d1a66e7574d78956bbd09abfd6c8a997798f01f5da3d46a05", size = 11516580 },
{ url = "https://files.pythonhosted.org/packages/37/1a/5ec1844e993e376a86eb2456496831ed91b4398c434d8244f89094758940/ruff-0.7.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7dbdc7d8274e1422722933d1edddfdc65b4336abf0b16dfcb9dedd6e6a517d06", size = 12692057 },
{ url = "https://files.pythonhosted.org/packages/50/90/76867152b0d3c05df29a74bb028413e90f704f0f6701c4801174ba47f959/ruff-0.7.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e92dfb5f00eaedb1501b2f906ccabfd67b2355bdf117fea9719fc99ac2145bc", size = 11085137 },
{ url = "https://files.pythonhosted.org/packages/c8/eb/0a7cb6059ac3555243bd026bb21785bbc812f7bbfa95a36c101bd72b47ae/ruff-0.7.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3bd726099f277d735dc38900b6a8d6cf070f80828877941983a57bca1cd92172", size = 10681243 },
{ url = "https://files.pythonhosted.org/packages/5e/76/2270719dbee0fd35780b05c08a07b7a726c3da9f67d9ae89ef21fc18e2e5/ruff-0.7.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2e32829c429dd081ee5ba39aef436603e5b22335c3d3fff013cd585806a6486a", size = 10319187 },
{ url = "https://files.pythonhosted.org/packages/9f/e5/39100f72f8ba70bec1bd329efc880dea8b6c1765ea1cb9d0c1c5f18b8d7f/ruff-0.7.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:662a63b4971807623f6f90c1fb664613f67cc182dc4d991471c23c541fee62dd", size = 10803715 },
{ url = "https://files.pythonhosted.org/packages/a5/89/40e904784f305fb56850063f70a998a64ebba68796d823dde67e89a24691/ruff-0.7.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:876f5e09eaae3eb76814c1d3b68879891d6fde4824c015d48e7a7da4cf066a3a", size = 11162912 },
{ url = "https://files.pythonhosted.org/packages/8d/1b/dd77503b3875c51e3dbc053fd8367b845ab8b01c9ca6d0c237082732856c/ruff-0.7.4-py3-none-win32.whl", hash = "sha256:75c53f54904be42dd52a548728a5b572344b50d9b2873d13a3f8c5e3b91f5cac", size = 8702767 },
{ url = "https://files.pythonhosted.org/packages/63/76/253ddc3e89e70165bba952ecca424b980b8d3c2598ceb4fc47904f424953/ruff-0.7.4-py3-none-win_amd64.whl", hash = "sha256:745775c7b39f914238ed1f1b0bebed0b9155a17cd8bc0b08d3c87e4703b990d6", size = 9497534 },
{ url = "https://files.pythonhosted.org/packages/aa/70/f8724f31abc0b329ca98b33d73c14020168babcf71b0cba3cded5d9d0e66/ruff-0.7.4-py3-none-win_arm64.whl", hash = "sha256:11bff065102c3ae9d3ea4dc9ecdfe5a5171349cdd0787c1fc64761212fc9cf1f", size = 8851590 },
]
[[package]]
@@ -299,14 +332,14 @@ wheels = [
[[package]]
name = "starlette"
version = "0.41.2"
version = "0.41.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
]
sdist = { url = "https://files.pythonhosted.org/packages/3e/da/1fb4bdb72ae12b834becd7e1e7e47001d32f91ec0ce8d7bc1b618d9f0bd9/starlette-0.41.2.tar.gz", hash = "sha256:9834fd799d1a87fd346deb76158668cfa0b0d56f85caefe8268e2d97c3468b62", size = 2573867 }
sdist = { url = "https://files.pythonhosted.org/packages/1a/4c/9b5764bd22eec91c4039ef4c55334e9187085da2d8a2df7bd570869aae18/starlette-0.41.3.tar.gz", hash = "sha256:0e4ab3d16522a255be6b28260b938eae2482f98ce5cc934cb08dce8dc3ba5835", size = 2574159 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/54/43/f185bfd0ca1d213beb4293bed51d92254df23d8ceaf6c0e17146d508a776/starlette-0.41.2-py3-none-any.whl", hash = "sha256:fbc189474b4731cf30fcef52f18a8d070e3f3b46c6a04c97579e85e6ffca942d", size = 73259 },
{ url = "https://files.pythonhosted.org/packages/96/00/2b325970b3060c7cecebab6d295afe763365822b1306a12eeab198f74323/starlette-0.41.3-py3-none-any.whl", hash = "sha256:44cedb2b7c77a9de33a8b74b2b90e9f50d11fcf25d8270ea525ad71a25374ff7", size = 73225 },
]
[[package]]
@@ -325,6 +358,7 @@ source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "h11" },
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e0/fc/1d785078eefd6945f3e5bab5c076e4230698046231eb0f3747bc5c8fa992/uvicorn-0.32.0.tar.gz", hash = "sha256:f78b36b143c16f54ccdb8190d0a26b5f1901fe5a3c777e1ab29f26391af8551e", size = 77564 }
wheels = [

65
src/google-maps/README.md Normal file
View 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
View 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);
});

View 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"
}
}

View File

@@ -0,0 +1,10 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "."
},
"include": [
"./**/*.ts"
]
}

101
src/memory/README.md Normal file
View File

@@ -0,0 +1,101 @@
# Knowledge Graph Memory Server
A basic MCP server implementation that provides persistent memory using a knowledge-graph. The server manages entities, their observations, and the relationships between them using a JSON-based file system.
This lets Claude remember information about the user across chats and projects, and lets them bypass the issues of having super long chats
# Core Concepts
## Entities
Entities are the primary nodes in the knowledge graph. Each entity has:
- A unique name (identifier)
- An entity type (e.g., "person", "organization", "event")
- A list of observations
Example:
```json
{
"name": "John_Smith",
"entityType": "person",
"observations": ["Lives in New York", "Works as a software engineer"]
}
```
## Relations
Relations define directed connections between entities. They are always stored in active voice and describe how entities interact or relate to each other.
Example:
```jsonCopy{
"from": "John_Smith",
"to": "TechCorp",
"relationType": "works_at"
}
```
## Observations
Observations are discrete pieces of information about an entity. They are:
- Stored as strings
- Attached to specific entities
- Can be added or removed independently
- Should be atomic (one fact per observation)
Example:
```jsonCopy{
"entityName": "John_Smith",
"observations": [
"Speaks fluent Spanish",
"Graduated in 2019",
"Prefers morning meetings"
]
}
```
# Tools
## Entity Management
- create_entities: Create new entities in the knowledge graph with names, types, and observations
- delete_entities: Remove entities and their associated relations from the graph
- add_observations: Add new observations to existing entities
- delete_observations: Remove specific observations from entities
## Relation Management
- create_relations: Establish relationships between entities in active voice
- delete_relations: Remove specific relationships between entities
## Query Tools
- read_graph: Retrieve the entire knowledge graph
- search_nodes: Search for nodes based on names, types, and observation content
- open_nodes: Access specific nodes by their names
# Prompts
The prompt for utilizing memory depends on the use case, but here is an example prompt for chat personalization. You could use this prompt in the "Custom Instructions" field of a Project
```
Follow these steps for each interaction:
1. User Identification:
- You should assume that you are interacting with default_user
- If you have not identified default_user, proactively try to do so.
2. Memory Retrieval:
- Always begin your chat by saying only "Remembering..." and retrieve all relevant information from your knowledge graph
- Always refer to your knowledge as your "memory"
3. Memory
- While conversing with the user, be attentive to any new information that falls into these categories:
a) Basic Identity (Age, gender, location, Job title, education level, etc.)
b) Behaviors (interests, habits, etc.)
c) Preferences (communication style, preferred language, etc.)
d) Goals/Psychology (Goals, targets, aspirations, etc.)
e) Relationships (personal and professional relationships up to 3 degrees of separation)
4. Memory Update:
- If any new information was gathered during the interaction, update your memory as follows:
a) Create nodes for recurring organizations, people, and significant events, connecting them to the current node.
b) Store most facts as observations within these nodes
- Try to perform all updates in one operation using the create and delete functions.
```

414
src/memory/index.ts Normal file
View File

@@ -0,0 +1,414 @@
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { promises as fs } from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
// Define the path to the JSONL file, you can change this to your desired local path
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const MEMORY_FILE_PATH = path.join(__dirname, 'memory.json');
// We are storing our memory using entities, relations, and observations in a graph structure
interface Entity {
name: string;
entityType: string;
observations: string[];
}
interface Relation {
from: string;
to: string;
relationType: string;
}
interface KnowledgeGraph {
entities: Entity[];
relations: Relation[];
}
// The KnowledgeGraphManager class contains all operations to interact with the knowledge graph
class KnowledgeGraphManager {
private async loadGraph(): Promise<KnowledgeGraph> {
try {
const data = await fs.readFile(MEMORY_FILE_PATH, "utf-8");
const lines = data.split("\n").filter(line => line.trim() !== "");
return lines.reduce((graph: KnowledgeGraph, line) => {
const item = JSON.parse(line);
if (item.type === "entity") graph.entities.push(item as Entity);
if (item.type === "relation") graph.relations.push(item as Relation);
return graph;
}, { entities: [], relations: [] });
} catch (error) {
if (error instanceof Error && 'code' in error && (error as any).code === "ENOENT") {
return { entities: [], relations: [] };
}
throw error;
}
}
private async saveGraph(graph: KnowledgeGraph): Promise<void> {
const lines = [
...graph.entities.map(e => JSON.stringify({ type: "entity", ...e })),
...graph.relations.map(r => JSON.stringify({ type: "relation", ...r })),
];
await fs.writeFile(MEMORY_FILE_PATH, lines.join("\n"));
}
async createEntities(entities: Entity[]): Promise<Entity[]> {
const graph = await this.loadGraph();
const newEntities = entities.filter(e => !graph.entities.some(existingEntity => existingEntity.name === e.name));
graph.entities.push(...newEntities);
await this.saveGraph(graph);
return newEntities;
}
async createRelations(relations: Relation[]): Promise<Relation[]> {
const graph = await this.loadGraph();
const newRelations = relations.filter(r => !graph.relations.some(existingRelation =>
existingRelation.from === r.from &&
existingRelation.to === r.to &&
existingRelation.relationType === r.relationType
));
graph.relations.push(...newRelations);
await this.saveGraph(graph);
return newRelations;
}
async addObservations(observations: { entityName: string; contents: string[] }[]): Promise<{ entityName: string; addedObservations: string[] }[]> {
const graph = await this.loadGraph();
const results = observations.map(o => {
const entity = graph.entities.find(e => e.name === o.entityName);
if (!entity) {
throw new Error(`Entity with name ${o.entityName} not found`);
}
const newObservations = o.contents.filter(content => !entity.observations.includes(content));
entity.observations.push(...newObservations);
return { entityName: o.entityName, addedObservations: newObservations };
});
await this.saveGraph(graph);
return results;
}
async deleteEntities(entityNames: string[]): Promise<void> {
const graph = await this.loadGraph();
graph.entities = graph.entities.filter(e => !entityNames.includes(e.name));
graph.relations = graph.relations.filter(r => !entityNames.includes(r.from) && !entityNames.includes(r.to));
await this.saveGraph(graph);
}
async deleteObservations(deletions: { entityName: string; observations: string[] }[]): Promise<void> {
const graph = await this.loadGraph();
deletions.forEach(d => {
const entity = graph.entities.find(e => e.name === d.entityName);
if (entity) {
entity.observations = entity.observations.filter(o => !d.observations.includes(o));
}
});
await this.saveGraph(graph);
}
async deleteRelations(relations: Relation[]): Promise<void> {
const graph = await this.loadGraph();
graph.relations = graph.relations.filter(r => !relations.some(delRelation =>
r.from === delRelation.from &&
r.to === delRelation.to &&
r.relationType === delRelation.relationType
));
await this.saveGraph(graph);
}
async readGraph(): Promise<KnowledgeGraph> {
return this.loadGraph();
}
// Very basic search function
async searchNodes(query: string): Promise<KnowledgeGraph> {
const graph = await this.loadGraph();
// Filter entities
const filteredEntities = graph.entities.filter(e =>
e.name.toLowerCase().includes(query.toLowerCase()) ||
e.entityType.toLowerCase().includes(query.toLowerCase()) ||
e.observations.some(o => o.toLowerCase().includes(query.toLowerCase()))
);
// Create a Set of filtered entity names for quick lookup
const filteredEntityNames = new Set(filteredEntities.map(e => e.name));
// Filter relations to only include those between filtered entities
const filteredRelations = graph.relations.filter(r =>
filteredEntityNames.has(r.from) && filteredEntityNames.has(r.to)
);
const filteredGraph: KnowledgeGraph = {
entities: filteredEntities,
relations: filteredRelations,
};
return filteredGraph;
}
async openNodes(names: string[]): Promise<KnowledgeGraph> {
const graph = await this.loadGraph();
// Filter entities
const filteredEntities = graph.entities.filter(e => names.includes(e.name));
// Create a Set of filtered entity names for quick lookup
const filteredEntityNames = new Set(filteredEntities.map(e => e.name));
// Filter relations to only include those between filtered entities
const filteredRelations = graph.relations.filter(r =>
filteredEntityNames.has(r.from) && filteredEntityNames.has(r.to)
);
const filteredGraph: KnowledgeGraph = {
entities: filteredEntities,
relations: filteredRelations,
};
return filteredGraph;
}
}
const knowledgeGraphManager = new KnowledgeGraphManager();
// The server instance and tools exposed to Claude
const server = new Server({
name: "memory-server",
version: "1.0.0",
}, {
capabilities: {
tools: {},
},
},);
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "create_entities",
description: "Create multiple new entities in the knowledge graph",
inputSchema: {
type: "object",
properties: {
entities: {
type: "array",
items: {
type: "object",
properties: {
name: { type: "string", description: "The name of the entity" },
entityType: { type: "string", description: "The type of the entity" },
observations: {
type: "array",
items: { type: "string" },
description: "An array of observation contents associated with the entity"
},
},
required: ["name", "entityType", "observations"],
},
},
},
required: ["entities"],
},
},
{
name: "create_relations",
description: "Create multiple new relations between entities in the knowledge graph. Relations should be in active voice",
inputSchema: {
type: "object",
properties: {
relations: {
type: "array",
items: {
type: "object",
properties: {
from: { type: "string", description: "The name of the entity where the relation starts" },
to: { type: "string", description: "The name of the entity where the relation ends" },
relationType: { type: "string", description: "The type of the relation" },
},
required: ["from", "to", "relationType"],
},
},
},
required: ["relations"],
},
},
{
name: "add_observations",
description: "Add new observations to existing entities in the knowledge graph",
inputSchema: {
type: "object",
properties: {
observations: {
type: "array",
items: {
type: "object",
properties: {
entityName: { type: "string", description: "The name of the entity to add the observations to" },
contents: {
type: "array",
items: { type: "string" },
description: "An array of observation contents to add"
},
},
required: ["entityName", "contents"],
},
},
},
required: ["observations"],
},
},
{
name: "delete_entities",
description: "Delete multiple entities and their associated relations from the knowledge graph",
inputSchema: {
type: "object",
properties: {
entityNames: {
type: "array",
items: { type: "string" },
description: "An array of entity names to delete"
},
},
required: ["entityNames"],
},
},
{
name: "delete_observations",
description: "Delete specific observations from entities in the knowledge graph",
inputSchema: {
type: "object",
properties: {
deletions: {
type: "array",
items: {
type: "object",
properties: {
entityName: { type: "string", description: "The name of the entity containing the observations" },
observations: {
type: "array",
items: { type: "string" },
description: "An array of observations to delete"
},
},
required: ["entityName", "observations"],
},
},
},
required: ["deletions"],
},
},
{
name: "delete_relations",
description: "Delete multiple relations from the knowledge graph",
inputSchema: {
type: "object",
properties: {
relations: {
type: "array",
items: {
type: "object",
properties: {
from: { type: "string", description: "The name of the entity where the relation starts" },
to: { type: "string", description: "The name of the entity where the relation ends" },
relationType: { type: "string", description: "The type of the relation" },
},
required: ["from", "to", "relationType"],
},
description: "An array of relations to delete"
},
},
required: ["relations"],
},
},
{
name: "read_graph",
description: "Read the entire knowledge graph",
inputSchema: {
type: "object",
properties: {},
},
},
{
name: "search_nodes",
description: "Search for nodes in the knowledge graph based on a query",
inputSchema: {
type: "object",
properties: {
query: { type: "string", description: "The search query to match against entity names, types, and observation content" },
},
required: ["query"],
},
},
{
name: "open_nodes",
description: "Open specific nodes in the knowledge graph by their names",
inputSchema: {
type: "object",
properties: {
names: {
type: "array",
items: { type: "string" },
description: "An array of entity names to retrieve",
},
},
required: ["names"],
},
},
],
};
});
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
if (!args) {
throw new Error(`No arguments provided for tool: ${name}`);
}
switch (name) {
case "create_entities":
return { toolResult: await knowledgeGraphManager.createEntities(args.entities as Entity[]) };
case "create_relations":
return { toolResult: await knowledgeGraphManager.createRelations(args.relations as Relation[]) };
case "add_observations":
return { toolResult: await knowledgeGraphManager.addObservations(args.observations as { entityName: string; contents: string[] }[]) };
case "delete_entities":
await knowledgeGraphManager.deleteEntities(args.entityNames as string[]);
return { toolResult: "Entities deleted successfully" };
case "delete_observations":
await knowledgeGraphManager.deleteObservations(args.deletions as { entityName: string; observations: string[] }[]);
return { toolResult: "Observations deleted successfully" };
case "delete_relations":
await knowledgeGraphManager.deleteRelations(args.relations as Relation[]);
return { toolResult: "Relations deleted successfully" };
case "read_graph":
return { toolResult: await knowledgeGraphManager.readGraph() };
case "search_nodes":
return { toolResult: await knowledgeGraphManager.searchNodes(args.query as string) };
case "open_nodes":
return { toolResult: await knowledgeGraphManager.openNodes(args.names as string[]) };
default:
throw new Error(`Unknown tool: ${name}`);
}
});
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Knowledge Graph MCP Server running on stdio");
}
main().catch((error) => {
console.error("Fatal error in main():", error);
process.exit(1);
});

28
src/memory/package.json Normal file
View File

@@ -0,0 +1,28 @@
{
"name": "@modelcontextprotocol/server-memory",
"version": "0.1.0",
"description": "MCP server for enabling memory for Claude through a knowledge graph",
"license": "MIT",
"author": "Anthropic, PBC (https://anthropic.com)",
"homepage": "https://modelcontextprotocol.io",
"bugs": "https://github.com/modelcontextprotocol/servers/issues",
"type": "module",
"bin": {
"mcp-server-memory": "dist/index.js"
},
"files": [
"dist"
],
"scripts": {
"build": "tsc && shx chmod +x dist/*.js",
"prepare": "npm run build",
"watch": "tsc --watch"
},
"dependencies": {
"@modelcontextprotocol/sdk": "0.5.0"
},
"devDependencies": {
"shx": "^0.3.4",
"typescript": "^5.6.2"
}
}

11
src/memory/tsconfig.json Normal file
View File

@@ -0,0 +1,11 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "."
},
"include": [
"./**/*.ts"
]
}

View File

@@ -1,9 +1,11 @@
from . import server
import asyncio
def main():
"""Main entry point for the package."""
asyncio.run(server.main())
# Optionally expose other important items at package level
__all__ = ['main', 'server']
__all__ = ["main", "server"]

View File

@@ -28,7 +28,7 @@ class McpServer(Server):
if cursor.fetchone()[0] == 0:
cursor.execute(
"INSERT INTO notes (name, content) VALUES (?, ?)",
("example", "This is an example note.")
("example", "This is an example note."),
)
conn.commit()
@@ -55,13 +55,13 @@ class McpServer(Server):
with closing(conn.cursor()) as cursor:
cursor.execute(
"INSERT OR REPLACE INTO notes (name, content) VALUES (?, ?)",
(name, content)
(name, content),
)
conn.commit()
def __init__(self):
super().__init__("sqlite")
# Initialize SQLite database
self.db_path = "notes.db"
self._init_database()
@@ -118,10 +118,14 @@ class McpServer(Server):
"""Generate a prompt using notes from the database"""
if name != "summarize-notes":
raise ValueError(f"Unknown prompt: {name}")
notes = "<notes>\n" + "\n".join(
f"<note name='{name}'>\n{content}\n</note>"
for name, content in self._get_notes().items()
) + "\n</notes>"
notes = (
"<notes>\n"
+ "\n".join(
f"<note name='{name}'>\n{content}\n</note>"
for name, content in self._get_notes().items()
)
+ "\n</notes>"
)
style = (arguments or {}).get("style", "simple")
prompt = """
Your task is to provide a summary of the notes provided below.
@@ -207,4 +211,4 @@ async def main():
experimental_capabilities={},
),
),
)
)