Merge branch 'main' into patch-1

This commit is contained in:
Justin Spahr-Summers 2024-12-13 13:48:23 +00:00 • committed by GitHub
commit eaf92b0f74
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 422 additions and 72 deletions

View file

@ -10,9 +10,10 @@ Each MCP server is implemented with either the [Typescript MCP SDK](https://gith
These servers aim to demonstrate MCP features and the Typescript and Python SDK. These servers aim to demonstrate MCP features and the Typescript and Python SDK.
- **[AWS KB Retrieval](src/aws-kb-retrieval)** - Retrieval from AWS Knowledge Base using Bedrock Agent Runtime - **[AWS KB Retrieval](src/aws-kb-retrieval-server)** - Retrieval from AWS Knowledge Base using Bedrock Agent Runtime
- **[Brave Search](src/brave-search)** - Web and local search using Brave's Search API - **[Brave Search](src/brave-search)** - Web and local search using Brave's Search API
- **[EverArt](src/everart)** - AI image generation using various models - **[EverArt](src/everart)** - AI image generation using various models
- **[Everything](src/everything)** - Reference / test server with prompts, resources, and tools
- **[Fetch](src/fetch)** - Web content fetching and conversion for efficient LLM usage - **[Fetch](src/fetch)** - Web content fetching and conversion for efficient LLM usage
- **[Filesystem](src/filesystem)** - Secure file operations with configurable access controls - **[Filesystem](src/filesystem)** - Secure file operations with configurable access controls
- **[Git](src/git)** - Tools to read, search, and manipulate Git repositories - **[Git](src/git)** - Tools to read, search, and manipulate Git repositories
@ -27,6 +28,7 @@ These servers aim to demonstrate MCP features and the Typescript and Python SDK.
- **[Sequential Thinking](src/sequentialthinking)** - Dynamic and reflective problem-solving through thought sequences - **[Sequential Thinking](src/sequentialthinking)** - Dynamic and reflective problem-solving through thought sequences
- **[Slack](src/slack)** - Channel management and messaging capabilities - **[Slack](src/slack)** - Channel management and messaging capabilities
- **[Sqlite](src/sqlite)** - Database interaction and business intelligence capabilities - **[Sqlite](src/sqlite)** - Database interaction and business intelligence capabilities
- **[Time](src/time)** - Time and timezone conversion capabilities
## 🤝 Third-Party Servers ## 🤝 Third-Party Servers
@ -34,14 +36,17 @@ These servers aim to demonstrate MCP features and the Typescript and Python SDK.
Official integrations are maintained by companies building production ready MCP servers for their platforms. Official integrations are maintained by companies building production ready MCP servers for their platforms.
- <img height="12" width="12" src="https://axiom.co/favicon.ico" alt="Axiom Logo" /> **[Axiom](https://github.com/axiomhq/mcp-server-axiom)** - Query and analyze your Axiom logs, traces, and all other event data in natural language
- <img height="12" width="12" src="https://browserbase.com/favicon.ico" alt="Browserbase Logo" /> **[Browserbase](https://github.com/browserbase/mcp-server-browserbase)** - Automate browser interactions in the cloud (e.g. web navigation, data extraction, form filling, and more) - <img height="12" width="12" src="https://browserbase.com/favicon.ico" alt="Browserbase Logo" /> **[Browserbase](https://github.com/browserbase/mcp-server-browserbase)** - Automate browser interactions in the cloud (e.g. web navigation, data extraction, form filling, and more)
- <img height="12" width="12" src="https://cdn.simpleicons.org/cloudflare" /> **[Cloudflare](https://github.com/cloudflare/mcp-server-cloudflare)** - Deploy, configure & interrogate your resources on the Cloudflare developer platform (e.g. Workers/KV/R2/D1) - <img height="12" width="12" src="https://cdn.simpleicons.org/cloudflare" /> **[Cloudflare](https://github.com/cloudflare/mcp-server-cloudflare)** - Deploy, configure & interrogate your resources on the Cloudflare developer platform (e.g. Workers/KV/R2/D1)
- **[Raygun](https://github.com/MindscapeHQ/mcp-server-raygun)** - Interact with your crash reporting and real using monitoring data on your Raygun account - **[Raygun](https://github.com/MindscapeHQ/mcp-server-raygun)** - Interact with your crash reporting and real using monitoring data on your Raygun account
- **[Obsidian Markdown Notes](https://github.com/calclavia/mcp-obsidian)** - Read and search through your Obsidian vault or any directory containing Markdown notes
- <img height="12" width="12" src="https://e2b.dev/favicon.ico" alt="E2B Logo" /> **[E2B](https://github.com/e2b-dev/mcp-server)** - Run code in secure sandboxes hosted by [E2B](https://e2b.dev) - <img height="12" width="12" src="https://e2b.dev/favicon.ico" alt="E2B Logo" /> **[E2B](https://github.com/e2b-dev/mcp-server)** - Run code in secure sandboxes hosted by [E2B](https://e2b.dev)
- **[Neon](https://github.com/neondatabase/mcp-server-neon)** - Interact with the Neon serverless Postgres platform - **[Neon](https://github.com/neondatabase/mcp-server-neon)** - Interact with the Neon serverless Postgres platform
- <img height="12" width="12" src="https://www.tinybird.co/favicon.ico" alt="Tinybird Logo" /> **[Tinybird](https://github.com/tinybirdco/mcp-tinybird)** - Interact with Tinybird serverless ClickHouse platform - <img height="12" width="12" src="https://www.tinybird.co/favicon.ico" alt="Tinybird Logo" /> **[Tinybird](https://github.com/tinybirdco/mcp-tinybird)** - Interact with Tinybird serverless ClickHouse platform
- <img height="12" width="12" src="https://pics.fatwang2.com/56912e614b35093426c515860f9f2234.svg" /> [Search1API](https://github.com/fatwang2/search1api-mcp) - One API for Search, Crawling, and Sitemaps - <img height="12" width="12" src="https://pics.fatwang2.com/56912e614b35093426c515860f9f2234.svg" /> [Search1API](https://github.com/fatwang2/search1api-mcp) - One API for Search, Crawling, and Sitemaps
- <img height="12" width="12" src="https://qdrant.tech/img/brand-resources-logos/logomark.svg" /> **[Qdrant](https://github.com/qdrant/mcp-server-qdrant/)** - Implement semantic memory layer on top of the Qdrant vector search engine - <img height="12" width="12" src="https://qdrant.tech/img/brand-resources-logos/logomark.svg" /> **[Qdrant](https://github.com/qdrant/mcp-server-qdrant/)** - Implement semantic memory layer on top of the Qdrant vector search engine
- <img height="12" width="12" src="https://metoro.io/static/images/logos/Metoro.svg" /> **[Metoro](https://github.com/metoro-io/metoro-mcp-server)** - Query and interact with kubernetes environments monitored by Metoro
### 🌎 Community Servers ### 🌎 Community Servers
@ -50,30 +55,50 @@ A growing set of community-developed and maintained servers demonstrates various
> **Note:** Community servers are **untested** and should be used at **your own risk**. They are not affiliated with or endorsed by Anthropic. > **Note:** Community servers are **untested** and should be used at **your own risk**. They are not affiliated with or endorsed by Anthropic.
- **[MCP Installer](https://github.com/anaisbetts/mcp-installer)** - This server is a server that installs other MCP servers for you. - **[MCP Installer](https://github.com/anaisbetts/mcp-installer)** - This server is a server that installs other MCP servers for you.
- **[Spotify MCP](https://github.com/varunneal/spotify-mcp)** - This MCP allows an LLM to play and use Spotify. - **[NS Travel Information](https://github.com/r-huijts/ns-mcp-server)** - Access Dutch Railways (NS) real-time train travel information and disruptions through the official NS API.
- **[Spotify](https://github.com/varunneal/spotify-mcp)** - This MCP allows an LLM to play and use Spotify.
- **[Inoyu](https://github.com/sergehuber/inoyu-mcp-unomi-server)** - Interact with an Apache Unomi CDP customer data platform to retrieve and update customer profiles - **[Inoyu](https://github.com/sergehuber/inoyu-mcp-unomi-server)** - Interact with an Apache Unomi CDP customer data platform to retrieve and update customer profiles
- **[MySQL](https://github.com/designcomputer/mysql_mcp_server)** - MySQL database integration with configurable access controls and schema inspection - **[Snowflake](https://github.com/datawiz168/mcp-snowflake-service)** - This MCP server enables LLMs to interact with Snowflake databases, allowing for secure and controlled data operations.
- **[MySQL](https://github.com/designcomputer/mysql_mcp_server)** (by DesignComputer) - MySQL database integration in Python with configurable access controls and schema inspection
- **[MySQL](https://github.com/benborla/mcp-server-mysql)** (by benborla) - MySQL database integration in NodeJS with configurable access controls and schema inspection
- **[MSSQL](https://github.com/aekanun2020/mcp-server/)** - MSSQL database integration with configurable access controls and schema inspection
- **[BigQuery](https://github.com/LucasHild/mcp-server-bigquery)** (by LucasHild) - This server enables LLMs to inspect database schemas and execute queries on BigQuery. - **[BigQuery](https://github.com/LucasHild/mcp-server-bigquery)** (by LucasHild) - This server enables LLMs to inspect database schemas and execute queries on BigQuery.
- **[BigQuery](https://github.com/ergut/mcp-bigquery-server)** (by ergut) - Server implementation for Google BigQuery integration that enables direct BigQuery database access and querying capabilities - **[BigQuery](https://github.com/ergut/mcp-bigquery-server)** (by ergut) - Server implementation for Google BigQuery integration that enables direct BigQuery database access and querying capabilities
- **[Todoist](https://github.com/abhiz123/todoist-mcp-server)** - Interact with Todoist to manage your tasks. - **[Todoist](https://github.com/abhiz123/todoist-mcp-server)** - Interact with Todoist to manage your tasks.
- **[Tavily search](https://github.com/RamXX/mcp-tavily")** - An MCP server for Tavily's search & news API, with explicit site inclusions/exclusions - **[Tavily search](https://github.com/RamXX/mcp-tavily)** - An MCP server for Tavily's search & news API, with explicit site inclusions/exclusions
- **[Linear](https://github.com/jerhadf/linear-mcp-server)** - Allows LLM to interact with Linear's API for project management, including searching, creating, and updating issues. - **[Linear](https://github.com/jerhadf/linear-mcp-server)** - Allows LLM to interact with Linear's API for project management, including searching, creating, and updating issues.
- **[Playwright MCP](https://github.com/executeautomation/mcp-playwright)** - This MCP Server will help you run browser automation and webscraping using Playwright - **[Playwright](https://github.com/executeautomation/mcp-playwright)** - This MCP Server will help you run browser automation and webscraping using Playwright
- **[AWS](https://github.com/rishikavikondala/mcp-server-aws)** - Perform operations on your AWS resources using an LLM - **[AWS](https://github.com/rishikavikondala/mcp-server-aws)** - Perform operations on your AWS resources using an LLM
- **[LlamaCloud](https://github.com/run-llama/mcp-server-llamacloud)** (by marcusschiesser) - Integrate the data stored in a managed index on [LlamaCloud](https://cloud.llamaindex.ai/) - **[LlamaCloud](https://github.com/run-llama/mcp-server-llamacloud)** (by marcusschiesser) - Integrate the data stored in a managed index on [LlamaCloud](https://cloud.llamaindex.ai/)
- **[Any Chat Completions](https://github.com/pyroprompts/any-chat-completions-mcp)** - Interact with any OpenAI SDK Compatible Chat Completions API like OpenAI, Perplexity, Groq, xAI and many more. - **[Any Chat Completions](https://github.com/pyroprompts/any-chat-completions-mcp)** - Interact with any OpenAI SDK Compatible Chat Completions API like OpenAI, Perplexity, Groq, xAI and many more.
- **[Windows CLI](https://github.com/SimonB97/win-cli-mcp-server)** - MCP server for secure command-line interactions on Windows systems, enabling controlled access to PowerShell, CMD, and Git Bash shells. - **[Windows CLI](https://github.com/SimonB97/win-cli-mcp-server)** - MCP server for secure command-line interactions on Windows systems, enabling controlled access to PowerShell, CMD, and Git Bash shells.
- **[OpenRPC](https://github.com/shanejonas/openrpc-mpc-server)** - Interact with and discover JSON-RPC APIs via [OpenRPC](https://open-rpc.org). - **[OpenRPC](https://github.com/shanejonas/openrpc-mpc-server)** - Interact with and discover JSON-RPC APIs via [OpenRPC](https://open-rpc.org).
- **[X (Twitter)](https://github.com/vidhupv/x-mcp)** - Create, manage and publish X/Twitter posts directly through Claude chat. - **[FireCrawl](https://github.com/vrknetha/mcp-server-firecrawl)** - Advanced web scraping with JavaScript rendering, PDF support, and smart rate limiting
- **[AlphaVantage](https://github.com/calvernaz/alphavantage)** - MCP server for stock market data API [AlphaVantage](https://www.alphavantage.co)
- **[Docker](https://github.com/ckreiling/mcp-server-docker)** - Integrate with Docker to manage containers, images, volumes, and networks.
- **[Kubernetes](https://github.com/Flux159/mcp-server-kubernetes)** - Connect to Kubernetes cluster and manage pods, deployments, and services.
- **[OpenAPI](https://github.com/snaggle-ai/openapi-mcp-server)** - Interact with [OpenAPI](https://www.openapis.org/) APIs.
- **[Pandoc](https://github.com/vivekVells/mcp-pandoc)** - MCP server for seamless document format conversion using Pandoc, supporting Markdown, HTML, and plain text, with other formats like PDF, csv and docx in development.
- **[HuggingFace Spaces](https://github.com/evalstate/mcp-hfspace)** - Server for using HuggingFace Spaces, supporting Open Source Image, Audio, Text Models and more. Claude Desktop mode for easy integration.
- **[ChatSum](https://github.com/mcpso/mcp-server-chatsum)** - Query and Summarize chat messages with LLM. by [mcpso](https://mcp.so)
- **[Rememberizer AI](https://github.com/skydeckai/mcp-server-rememberizer)** - An MCP server designed for interacting with the Rememberizer data source, facilitating enhanced knowledge retrieval.
- **[X (Twitter)](https://github.com/vidhupv/x-mcp)** (by vidhupv) - Create, manage and publish X/Twitter posts directly through Claude chat.
- **[X (Twitter)](https://github.com/EnesCinr/twitter-mcp)** (by EnesCinr) - Interact with twitter API. Post tweets and search for tweets by query.
- **[RAG Web Browser](https://github.com/apify/mcp-server-rag-web-browser)** An MCP server for Apify's RAG Web Browser Actor to perform web searches, scrape URLs, and return content in Markdown.
## 📚 Resources ## 📚 Resources
Additional resources on MCP. Additional resources on MCP.
- **[Awesome MCP Servers by punkpeye](https://github.com/punkpeye/awesome-mcp-servers)** - A curated list of MCP servers by **[Frank Fiegel](https://github.com/punkpeye)** - **[Awesome MCP Servers by punkpeye](https://github.com/punkpeye/awesome-mcp-servers)** (**[website](https://glama.ai/mcp/servers)**) - A curated list of MCP servers by **[Frank Fiegel](https://github.com/punkpeye)**
- **[Awesome MCP Servers by wong2](https://github.com/wong2/awesome-mcp-servers)** - A curated list of MCP servers by **[wong2](https://github.com/wong2)** - **[Awesome MCP Servers by wong2](https://github.com/wong2/awesome-mcp-servers)** (**[website](https://mcpservers.org)**) - A curated list of MCP servers by **[wong2](https://github.com/wong2)**
- **[Awesome MCP Servers by appcypher](https://github.com/appcypher/awesome-mcp-servers)** - A curated list of MCP servers by **[Stephen Akinyemi](https://github.com/appcypher)** - **[Awesome MCP Servers by appcypher](https://github.com/appcypher/awesome-mcp-servers)** - A curated list of MCP servers by **[Stephen Akinyemi](https://github.com/appcypher)**
- **[Discord Server](https://glama.ai/mcp/discord)** – A community discord server dedicated to MCP by **[Frank Fiegel](https://github.com/punkpeye)**
- **[mcp-get](https://mcp-get.com)** - Command line tool for installing and managing MCP servers by **[Michael Latman](https://github.com/michaellatman)** - **[mcp-get](https://mcp-get.com)** - Command line tool for installing and managing MCP servers by **[Michael Latman](https://github.com/michaellatman)**
- **[mcp-cli](https://github.com/wong2/mcp-cli)** - A CLI inspector for the Model Context Protocol by **[wong2](https://github.com/wong2)** - **[mcp-cli](https://github.com/wong2/mcp-cli)** - A CLI inspector for the Model Context Protocol by **[wong2](https://github.com/wong2)**
- **[r/mcp](https://www.reddit.com/r/mcp)** – A Reddit community dedicated to MCP by **[Frank Fiegel](https://github.com/punkpeye)**
- **[MCP X Community](https://x.com/i/communities/1861891349609603310)** – A X community for MCP by **[Xiaoyi](https://x.com/chxy)**
- **[mcp-manager](https://github.com/zueai/mcp-manager)** - Simple Web UI to install and manage MCP servers for Claude Desktop by **[Zue](https://github.com/zueai)**
## 🚀 Getting Started ## 🚀 Getting Started

26
package-lock.json generated
View file

@ -1565,6 +1565,13 @@
"integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==",
"dev": true "dev": true
}, },
"node_modules/@types/minimatch": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz",
"integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "22.9.0", "version": "22.9.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.0.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.0.tgz",
@ -5185,6 +5192,7 @@
"@modelcontextprotocol/sdk": "0.5.0", "@modelcontextprotocol/sdk": "0.5.0",
"diff": "^5.1.0", "diff": "^5.1.0",
"glob": "^10.3.10", "glob": "^10.3.10",
"minimatch": "^10.0.1",
"zod-to-json-schema": "^3.23.5" "zod-to-json-schema": "^3.23.5"
}, },
"bin": { "bin": {
@ -5192,6 +5200,7 @@
}, },
"devDependencies": { "devDependencies": {
"@types/diff": "^5.0.9", "@types/diff": "^5.0.9",
"@types/minimatch": "^5.1.2",
"@types/node": "^20.11.0", "@types/node": "^20.11.0",
"shx": "^0.3.4", "shx": "^0.3.4",
"typescript": "^5.3.3" "typescript": "^5.3.3"
@ -5246,7 +5255,7 @@
"url": "https://github.com/sponsors/isaacs" "url": "https://github.com/sponsors/isaacs"
} }
}, },
"src/filesystem/node_modules/minimatch": { "src/filesystem/node_modules/glob/node_modules/minimatch": {
"version": "9.0.5", "version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
@ -5261,6 +5270,21 @@
"url": "https://github.com/sponsors/isaacs" "url": "https://github.com/sponsors/isaacs"
} }
}, },
"src/filesystem/node_modules/minimatch": {
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz",
"integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==",
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": "20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"src/gdrive": { "src/gdrive": {
"name": "@modelcontextprotocol/server-gdrive", "name": "@modelcontextprotocol/server-gdrive",
"version": "0.6.2", "version": "0.6.2",

View file

@ -30,4 +30,4 @@
"@modelcontextprotocol/server-everart": "*", "@modelcontextprotocol/server-everart": "*",
"@modelcontextprotocol/server-sequential-thinking": "*" "@modelcontextprotocol/server-sequential-thinking": "*"
} }
} }

View file

@ -82,6 +82,7 @@ Node.js server implementing Model Context Protocol (MCP) for filesystem operatio
- Inputs: - Inputs:
- `path` (string): Starting directory - `path` (string): Starting directory
- `pattern` (string): Search pattern - `pattern` (string): Search pattern
- `excludePatterns` (string[]): Exclude any patterns. Glob formats are supported.
- Case-insensitive matching - Case-insensitive matching
- Returns full paths to matches - Returns full paths to matches

View file

@ -13,6 +13,7 @@ import os from 'os';
import { z } from "zod"; import { z } from "zod";
import { zodToJsonSchema } from "zod-to-json-schema"; import { zodToJsonSchema } from "zod-to-json-schema";
import { diffLines, createTwoFilesPatch } from 'diff'; import { diffLines, createTwoFilesPatch } from 'diff';
import { minimatch } from 'minimatch';
// Command line argument parsing // Command line argument parsing
const args = process.argv.slice(2); const args = process.argv.slice(2);
@ -23,7 +24,7 @@ if (args.length === 0) {
// Normalize all paths consistently // Normalize all paths consistently
function normalizePath(p: string): string { function normalizePath(p: string): string {
return path.normalize(p).toLowerCase(); return path.normalize(p);
} }
function expandHome(filepath: string): string { function expandHome(filepath: string): string {
@ -34,7 +35,7 @@ function expandHome(filepath: string): string {
} }
// Store allowed directories in normalized form // Store allowed directories in normalized form
const allowedDirectories = args.map(dir => const allowedDirectories = args.map(dir =>
normalizePath(path.resolve(expandHome(dir))) normalizePath(path.resolve(expandHome(dir)))
); );
@ -58,7 +59,7 @@ async function validatePath(requestedPath: string): Promise<string> {
const absolute = path.isAbsolute(expandedPath) const absolute = path.isAbsolute(expandedPath)
? path.resolve(expandedPath) ? path.resolve(expandedPath)
: path.resolve(process.cwd(), expandedPath); : path.resolve(process.cwd(), expandedPath);
const normalizedRequested = normalizePath(absolute); const normalizedRequested = normalizePath(absolute);
// Check if path is within allowed directories // Check if path is within allowed directories
@ -126,6 +127,10 @@ const ListDirectoryArgsSchema = z.object({
path: z.string(), path: z.string(),
}); });
const DirectoryTreeArgsSchema = z.object({
path: z.string(),
});
const MoveFileArgsSchema = z.object({ const MoveFileArgsSchema = z.object({
source: z.string(), source: z.string(),
destination: z.string(), destination: z.string(),
@ -134,6 +139,7 @@ const MoveFileArgsSchema = z.object({
const SearchFilesArgsSchema = z.object({ const SearchFilesArgsSchema = z.object({
path: z.string(), path: z.string(),
pattern: z.string(), pattern: z.string(),
excludePatterns: z.array(z.string()).optional().default([])
}); });
const GetFileInfoArgsSchema = z.object({ const GetFileInfoArgsSchema = z.object({
@ -183,6 +189,7 @@ async function getFileStats(filePath: string): Promise<FileInfo> {
async function searchFiles( async function searchFiles(
rootPath: string, rootPath: string,
pattern: string, pattern: string,
excludePatterns: string[] = []
): Promise<string[]> { ): Promise<string[]> {
const results: string[] = []; const results: string[] = [];
@ -191,11 +198,22 @@ async function searchFiles(
for (const entry of entries) { for (const entry of entries) {
const fullPath = path.join(currentPath, entry.name); const fullPath = path.join(currentPath, entry.name);
try { try {
// Validate each path before processing // Validate each path before processing
await validatePath(fullPath); await validatePath(fullPath);
// Check if path matches any exclude pattern
const relativePath = path.relative(rootPath, fullPath);
const shouldExclude = excludePatterns.some(pattern => {
const globPattern = pattern.includes('*') ? pattern : `**/${pattern}/**`;
return minimatch(relativePath, globPattern, { dot: true });
});
if (shouldExclude) {
continue;
}
if (entry.name.toLowerCase().includes(pattern.toLowerCase())) { if (entry.name.toLowerCase().includes(pattern.toLowerCase())) {
results.push(fullPath); results.push(fullPath);
} }
@ -223,7 +241,7 @@ function createUnifiedDiff(originalContent: string, newContent: string, filepath
// Ensure consistent line endings for diff // Ensure consistent line endings for diff
const normalizedOriginal = normalizeLineEndings(originalContent); const normalizedOriginal = normalizeLineEndings(originalContent);
const normalizedNew = normalizeLineEndings(newContent); const normalizedNew = normalizeLineEndings(newContent);
return createTwoFilesPatch( return createTwoFilesPatch(
filepath, filepath,
filepath, filepath,
@ -241,33 +259,33 @@ async function applyFileEdits(
): Promise<string> { ): Promise<string> {
// Read file content and normalize line endings // Read file content and normalize line endings
const content = normalizeLineEndings(await fs.readFile(filePath, 'utf-8')); const content = normalizeLineEndings(await fs.readFile(filePath, 'utf-8'));
// Apply edits sequentially // Apply edits sequentially
let modifiedContent = content; let modifiedContent = content;
for (const edit of edits) { for (const edit of edits) {
const normalizedOld = normalizeLineEndings(edit.oldText); const normalizedOld = normalizeLineEndings(edit.oldText);
const normalizedNew = normalizeLineEndings(edit.newText); const normalizedNew = normalizeLineEndings(edit.newText);
// If exact match exists, use it // If exact match exists, use it
if (modifiedContent.includes(normalizedOld)) { if (modifiedContent.includes(normalizedOld)) {
modifiedContent = modifiedContent.replace(normalizedOld, normalizedNew); modifiedContent = modifiedContent.replace(normalizedOld, normalizedNew);
continue; continue;
} }
// Otherwise, try line-by-line matching with flexibility for whitespace // Otherwise, try line-by-line matching with flexibility for whitespace
const oldLines = normalizedOld.split('\n'); const oldLines = normalizedOld.split('\n');
const contentLines = modifiedContent.split('\n'); const contentLines = modifiedContent.split('\n');
let matchFound = false; let matchFound = false;
for (let i = 0; i <= contentLines.length - oldLines.length; i++) { for (let i = 0; i <= contentLines.length - oldLines.length; i++) {
const potentialMatch = contentLines.slice(i, i + oldLines.length); const potentialMatch = contentLines.slice(i, i + oldLines.length);
// Compare lines with normalized whitespace // Compare lines with normalized whitespace
const isMatch = oldLines.every((oldLine, j) => { const isMatch = oldLines.every((oldLine, j) => {
const contentLine = potentialMatch[j]; const contentLine = potentialMatch[j];
return oldLine.trim() === contentLine.trim(); return oldLine.trim() === contentLine.trim();
}); });
if (isMatch) { if (isMatch) {
// Preserve original indentation of first line // Preserve original indentation of first line
const originalIndent = contentLines[i].match(/^\s*/)?.[0] || ''; const originalIndent = contentLines[i].match(/^\s*/)?.[0] || '';
@ -282,33 +300,33 @@ async function applyFileEdits(
} }
return line; return line;
}); });
contentLines.splice(i, oldLines.length, ...newLines); contentLines.splice(i, oldLines.length, ...newLines);
modifiedContent = contentLines.join('\n'); modifiedContent = contentLines.join('\n');
matchFound = true; matchFound = true;
break; break;
} }
} }
if (!matchFound) { if (!matchFound) {
throw new Error(`Could not find exact match for edit:\n${edit.oldText}`); throw new Error(`Could not find exact match for edit:\n${edit.oldText}`);
} }
} }
// Create unified diff // Create unified diff
const diff = createUnifiedDiff(content, modifiedContent, filePath); const diff = createUnifiedDiff(content, modifiedContent, filePath);
// Format diff with appropriate number of backticks // Format diff with appropriate number of backticks
let numBackticks = 3; let numBackticks = 3;
while (diff.includes('`'.repeat(numBackticks))) { while (diff.includes('`'.repeat(numBackticks))) {
numBackticks++; numBackticks++;
} }
const formattedDiff = `${'`'.repeat(numBackticks)}diff\n${diff}${'`'.repeat(numBackticks)}\n\n`; const formattedDiff = `${'`'.repeat(numBackticks)}diff\n${diff}${'`'.repeat(numBackticks)}\n\n`;
if (!dryRun) { if (!dryRun) {
await fs.writeFile(filePath, modifiedContent, 'utf-8'); await fs.writeFile(filePath, modifiedContent, 'utf-8');
} }
return formattedDiff; return formattedDiff;
} }
@ -369,6 +387,15 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
"finding specific files within a directory. Only works within allowed directories.", "finding specific files within a directory. Only works within allowed directories.",
inputSchema: zodToJsonSchema(ListDirectoryArgsSchema) as ToolInput, inputSchema: zodToJsonSchema(ListDirectoryArgsSchema) as ToolInput,
}, },
{
name: "directory_tree",
description:
"Get a recursive tree view of files and directories as a JSON structure. " +
"Each entry includes 'name', 'type' (file/directory), and 'children' for directories. " +
"Files have no children array, while directories always have a children array (which may be empty). " +
"The output is formatted with 2-space indentation for readability. Only works within allowed directories.",
inputSchema: zodToJsonSchema(DirectoryTreeArgsSchema) as ToolInput,
},
{ {
name: "move_file", name: "move_file",
description: description:
@ -399,7 +426,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
}, },
{ {
name: "list_allowed_directories", name: "list_allowed_directories",
description: description:
"Returns the list of directories that this server is allowed to access. " + "Returns the list of directories that this server is allowed to access. " +
"Use this to understand which directories are available before trying to access files.", "Use this to understand which directories are available before trying to access files.",
inputSchema: { inputSchema: {
@ -503,6 +530,49 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
}; };
} }
case "directory_tree": {
const parsed = DirectoryTreeArgsSchema.safeParse(args);
if (!parsed.success) {
throw new Error(`Invalid arguments for directory_tree: ${parsed.error}`);
}
interface TreeEntry {
name: string;
type: 'file' | 'directory';
children?: TreeEntry[];
}
async function buildTree(currentPath: string): Promise<TreeEntry[]> {
const validPath = await validatePath(currentPath);
const entries = await fs.readdir(validPath, {withFileTypes: true});
const result: TreeEntry[] = [];
for (const entry of entries) {
const entryData: TreeEntry = {
name: entry.name,
type: entry.isDirectory() ? 'directory' : 'file'
};
if (entry.isDirectory()) {
const subPath = path.join(currentPath, entry.name);
entryData.children = await buildTree(subPath);
}
result.push(entryData);
}
return result;
}
const treeData = await buildTree(parsed.data.path);
return {
content: [{
type: "text",
text: JSON.stringify(treeData, null, 2)
}],
};
}
case "move_file": { case "move_file": {
const parsed = MoveFileArgsSchema.safeParse(args); const parsed = MoveFileArgsSchema.safeParse(args);
if (!parsed.success) { if (!parsed.success) {
@ -522,7 +592,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
throw new Error(`Invalid arguments for search_files: ${parsed.error}`); throw new Error(`Invalid arguments for search_files: ${parsed.error}`);
} }
const validPath = await validatePath(parsed.data.path); const validPath = await validatePath(parsed.data.path);
const results = await searchFiles(validPath, parsed.data.pattern); const results = await searchFiles(validPath, parsed.data.pattern, parsed.data.excludePatterns);
return { return {
content: [{ type: "text", text: results.length > 0 ? results.join("\n") : "No matches found" }], content: [{ type: "text", text: results.length > 0 ? results.join("\n") : "No matches found" }],
}; };
@ -544,9 +614,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
case "list_allowed_directories": { case "list_allowed_directories": {
return { return {
content: [{ content: [{
type: "text", type: "text",
text: `Allowed directories:\n${allowedDirectories.join('\n')}` text: `Allowed directories:\n${allowedDirectories.join('\n')}`
}], }],
}; };
} }
@ -574,4 +644,4 @@ async function runServer() {
runServer().catch((error) => { runServer().catch((error) => {
console.error("Fatal error running server:", error); console.error("Fatal error running server:", error);
process.exit(1); process.exit(1);
}); });

View file

@ -22,12 +22,14 @@
"@modelcontextprotocol/sdk": "0.5.0", "@modelcontextprotocol/sdk": "0.5.0",
"diff": "^5.1.0", "diff": "^5.1.0",
"glob": "^10.3.10", "glob": "^10.3.10",
"minimatch": "^10.0.1",
"zod-to-json-schema": "^3.23.5" "zod-to-json-schema": "^3.23.5"
}, },
"devDependencies": { "devDependencies": {
"@types/diff": "^5.0.9", "@types/diff": "^5.0.9",
"@types/minimatch": "^5.1.2",
"@types/node": "^20.11.0", "@types/node": "^20.11.0",
"shx": "^0.3.4", "shx": "^0.3.4",
"typescript": "^5.3.3" "typescript": "^5.3.3"
} }
} }

View file

@ -26,40 +26,53 @@ Please note that mcp-server-git is currently in early development. The functiona
- `repo_path` (string): Path to Git repository - `repo_path` (string): Path to Git repository
- Returns: Diff output of staged changes - Returns: Diff output of staged changes
4. `git_commit` 4. `git_diff`
- Shows differences between branches or commits
- Inputs:
- `repo_path` (string): Path to Git repository
- `target` (string): Target branch or commit to compare with
- Returns: Diff output comparing current state with target
5. `git_commit`
- Records changes to the repository - Records changes to the repository
- Inputs: - Inputs:
- `repo_path` (string): Path to Git repository - `repo_path` (string): Path to Git repository
- `message` (string): Commit message - `message` (string): Commit message
- Returns: Confirmation with new commit hash - Returns: Confirmation with new commit hash
5. `git_add` 6. `git_add`
- Adds file contents to the staging area - Adds file contents to the staging area
- Inputs: - Inputs:
- `repo_path` (string): Path to Git repository - `repo_path` (string): Path to Git repository
- `files` (string[]): Array of file paths to stage - `files` (string[]): Array of file paths to stage
- Returns: Confirmation of staged files - Returns: Confirmation of staged files
6. `git_reset` 7. `git_reset`
- Unstages all staged changes - Unstages all staged changes
- Input: - Input:
- `repo_path` (string): Path to Git repository - `repo_path` (string): Path to Git repository
- Returns: Confirmation of reset operation - Returns: Confirmation of reset operation
7. `git_log` 8. `git_log`
- Shows the commit logs - Shows the commit logs
- Inputs: - Inputs:
- `repo_path` (string): Path to Git repository - `repo_path` (string): Path to Git repository
- `max_count` (number, optional): Maximum number of commits to show (default: 10) - `max_count` (number, optional): Maximum number of commits to show (default: 10)
- Returns: Array of commit entries with hash, author, date, and message - Returns: Array of commit entries with hash, author, date, and message
8. `git_create_branch` 9. `git_create_branch`
- Creates a new branch - Creates a new branch
- Inputs: - Inputs:
- `repo_path` (string): Path to Git repository - `repo_path` (string): Path to Git repository
- `branch_name` (string): Name of the new branch - `branch_name` (string): Name of the new branch
- `start_point` (string, optional): Starting point for the new branch - `start_point` (string, optional): Starting point for the new branch
- Returns: Confirmation of branch creation - Returns: Confirmation of branch creation
8. `git_checkout`
- Switches branches
- Inputs:
- `repo_path` (string): Path to Git repository
- `branch_name` (string): Name of branch to checkout
- Returns: Confirmation of branch switch
## Installation ## Installation

View file

@ -30,4 +30,10 @@ requires = ["hatchling"]
build-backend = "hatchling.build" build-backend = "hatchling.build"
[tool.uv] [tool.uv]
dev-dependencies = ["pyright>=1.1.389", "ruff>=0.7.3"] dev-dependencies = ["pyright>=1.1.389", "ruff>=0.7.3", "pytest>=8.0.0"]
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = "test_*.py"
python_classes = "Test*"
python_functions = "test_*"

View file

@ -24,6 +24,10 @@ class GitDiffUnstaged(BaseModel):
class GitDiffStaged(BaseModel): class GitDiffStaged(BaseModel):
repo_path: str repo_path: str
class GitDiff(BaseModel):
repo_path: str
target: str
class GitCommit(BaseModel): class GitCommit(BaseModel):
repo_path: str repo_path: str
message: str message: str
@ -44,15 +48,21 @@ class GitCreateBranch(BaseModel):
branch_name: str branch_name: str
base_branch: str | None = None base_branch: str | None = None
class GitCheckout(BaseModel):
repo_path: str
branch_name: str
class GitTools(str, Enum): class GitTools(str, Enum):
STATUS = "git_status" STATUS = "git_status"
DIFF_UNSTAGED = "git_diff_unstaged" DIFF_UNSTAGED = "git_diff_unstaged"
DIFF_STAGED = "git_diff_staged" DIFF_STAGED = "git_diff_staged"
DIFF = "git_diff"
COMMIT = "git_commit" COMMIT = "git_commit"
ADD = "git_add" ADD = "git_add"
RESET = "git_reset" RESET = "git_reset"
LOG = "git_log" LOG = "git_log"
CREATE_BRANCH = "git_create_branch" CREATE_BRANCH = "git_create_branch"
CHECKOUT = "git_checkout"
def git_status(repo: git.Repo) -> str: def git_status(repo: git.Repo) -> str:
return repo.git.status() return repo.git.status()
@ -63,6 +73,9 @@ def git_diff_unstaged(repo: git.Repo) -> str:
def git_diff_staged(repo: git.Repo) -> str: def git_diff_staged(repo: git.Repo) -> str:
return repo.git.diff("--cached") return repo.git.diff("--cached")
def git_diff(repo: git.Repo, target: str) -> str:
return repo.git.diff(target)
def git_commit(repo: git.Repo, message: str) -> str: def git_commit(repo: git.Repo, message: str) -> str:
commit = repo.index.commit(message) commit = repo.index.commit(message)
return f"Changes committed successfully with hash {commit.hexsha}" return f"Changes committed successfully with hash {commit.hexsha}"
@ -96,6 +109,10 @@ def git_create_branch(repo: git.Repo, branch_name: str, base_branch: str | None
repo.create_head(branch_name, base) repo.create_head(branch_name, base)
return f"Created branch '{branch_name}' from '{base.name}'" return f"Created branch '{branch_name}' from '{base.name}'"
def git_checkout(repo: git.Repo, branch_name: str) -> str:
repo.git.checkout(branch_name)
return f"Switched to branch '{branch_name}'"
async def serve(repository: Path | None) -> None: async def serve(repository: Path | None) -> None:
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -127,6 +144,11 @@ async def serve(repository: Path | None) -> None:
description="Shows changes that are staged for commit", description="Shows changes that are staged for commit",
inputSchema=GitDiffStaged.schema(), inputSchema=GitDiffStaged.schema(),
), ),
Tool(
name=GitTools.DIFF,
description="Shows differences between branches or commits",
inputSchema=GitDiff.schema(),
),
Tool( Tool(
name=GitTools.COMMIT, name=GitTools.COMMIT,
description="Records changes to the repository", description="Records changes to the repository",
@ -152,6 +174,11 @@ async def serve(repository: Path | None) -> None:
description="Creates a new branch from an optional base branch", description="Creates a new branch from an optional base branch",
inputSchema=GitCreateBranch.schema(), inputSchema=GitCreateBranch.schema(),
), ),
Tool(
name=GitTools.CHECKOUT,
description="Switches branches",
inputSchema=GitCheckout.schema(),
),
] ]
async def list_repos() -> Sequence[str]: async def list_repos() -> Sequence[str]:
@ -210,6 +237,13 @@ async def serve(repository: Path | None) -> None:
text=f"Staged changes:\n{diff}" text=f"Staged changes:\n{diff}"
)] )]
case GitTools.DIFF:
diff = git_diff(repo, arguments["target"])
return [TextContent(
type="text",
text=f"Diff with {arguments['target']}:\n{diff}"
)]
case GitTools.COMMIT: case GitTools.COMMIT:
result = git_commit(repo, arguments["message"]) result = git_commit(repo, arguments["message"])
return [TextContent( return [TextContent(
@ -249,6 +283,13 @@ async def serve(repository: Path | None) -> None:
text=result text=result
)] )]
case GitTools.CHECKOUT:
result = git_checkout(repo, arguments["branch_name"])
return [TextContent(
type="text",
text=result
)]
case _: case _:
raise ValueError(f"Unknown tool: {name}") raise ValueError(f"Unknown tool: {name}")

View file

@ -0,0 +1,30 @@
import pytest
from pathlib import Path
import git
from mcp_server_git.server import git_checkout
import shutil
@pytest.fixture
def test_repository(tmp_path: Path):
repo_path = tmp_path / "temp_test_repo"
test_repo = git.Repo.init(repo_path)
Path(repo_path / "test.txt").write_text("test")
test_repo.index.add(["test.txt"])
test_repo.index.commit("initial commit")
yield test_repo
shutil.rmtree(repo_path)
def test_git_checkout_existing_branch(test_repository):
test_repository.git.branch("test-branch")
result = git_checkout(test_repository, "test-branch")
assert "Switched to branch 'test-branch'" in result
assert test_repository.active_branch.name == "test-branch"
def test_git_checkout_nonexistent_branch(test_repository):
with pytest.raises(git.GitCommandError):
git_checkout(test_repository, "nonexistent-branch")

89
src/git/uv.lock generated
View file

@ -144,6 +144,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 },
] ]
[[package]]
name = "iniconfig"
version = "2.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 },
]
[[package]] [[package]]
name = "mcp" name = "mcp"
version = "1.1.0" version = "1.1.0"
@ -156,9 +165,9 @@ dependencies = [
{ name = "sse-starlette" }, { name = "sse-starlette" },
{ name = "starlette" }, { name = "starlette" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/77/f2/067b1fc114e8d3ae4af02fc4f4ed8971a2c4900362d976fabe0f4e9a3418/mcp-1.1.0.tar.gz", hash = "sha256:e3c8d6df93a4de90230ea944dd667730744a3cd91a4cc0ee66a5acd53419e100", size = 83802 } sdist = { url = "https://files.pythonhosted.org/packages/97/de/a9ec0a1b6439f90ea59f89004bb2e7ec6890dfaeef809751d9e6577dca7e/mcp-1.0.0.tar.gz", hash = "sha256:dba51ce0b5c6a80e25576f606760c49a91ee90210fed805b530ca165d3bbc9b7", size = 82891 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/b9/3e/aef19ac08a6f9a347c086c4e628c2f7329659828cbe92ffd524ec2aac833/mcp-1.1.0-py3-none-any.whl", hash = "sha256:44aa4d2e541f0924d6c344aa7f96b427a6ee1df2fab70b5f9ae2f8777b3f05f2", size = 36576 }, { url = "https://files.pythonhosted.org/packages/56/89/900c0c8445ec001d3725e475fc553b0feb2e8a51be018f3bb7de51e683db/mcp-1.0.0-py3-none-any.whl", hash = "sha256:bbe70ffa3341cd4da78b5eb504958355c68381fb29971471cea1e642a2af5b8a", size = 36361 },
] ]
[[package]] [[package]]
@ -175,6 +184,7 @@ dependencies = [
[package.dev-dependencies] [package.dev-dependencies]
dev = [ dev = [
{ name = "pyright" }, { name = "pyright" },
{ name = "pytest" },
{ name = "ruff" }, { name = "ruff" },
] ]
@ -189,6 +199,7 @@ requires-dist = [
[package.metadata.requires-dev] [package.metadata.requires-dev]
dev = [ dev = [
{ name = "pyright", specifier = ">=1.1.389" }, { name = "pyright", specifier = ">=1.1.389" },
{ name = "pytest", specifier = ">=8.0.0" },
{ name = "ruff", specifier = ">=0.7.3" }, { name = "ruff", specifier = ">=0.7.3" },
] ]
@ -201,6 +212,24 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 }, { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 },
] ]
[[package]]
name = "packaging"
version = "24.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 },
]
[[package]]
name = "pluggy"
version = "1.5.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 },
]
[[package]] [[package]]
name = "pydantic" name = "pydantic"
version = "2.10.1" version = "2.10.1"
@ -303,6 +332,23 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/1b/26/c288cabf8cfc5a27e1aa9e5029b7682c0f920b8074f45d22bf844314d66a/pyright-1.1.389-py3-none-any.whl", hash = "sha256:41e9620bba9254406dc1f621a88ceab5a88af4c826feb4f614d95691ed243a60", size = 18581 }, { url = "https://files.pythonhosted.org/packages/1b/26/c288cabf8cfc5a27e1aa9e5029b7682c0f920b8074f45d22bf844314d66a/pyright-1.1.389-py3-none-any.whl", hash = "sha256:41e9620bba9254406dc1f621a88ceab5a88af4c826feb4f614d95691ed243a60", size = 18581 },
] ]
[[package]]
name = "pytest"
version = "8.3.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
{ name = "iniconfig" },
{ name = "packaging" },
{ name = "pluggy" },
{ name = "tomli", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/05/35/30e0d83068951d90a01852cb1cef56e5d8a09d20c7f511634cc2f7e0372a/pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761", size = 1445919 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/11/92/76a1c94d3afee238333bc0a42b82935dd8f9cf8ce9e336ff87ee14d9e1cf/pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6", size = 343083 },
]
[[package]] [[package]]
name = "ruff" name = "ruff"
version = "0.8.0" version = "0.8.0"
@ -372,6 +418,45 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/96/00/2b325970b3060c7cecebab6d295afe763365822b1306a12eeab198f74323/starlette-0.41.3-py3-none-any.whl", hash = "sha256:44cedb2b7c77a9de33a8b74b2b90e9f50d11fcf25d8270ea525ad71a25374ff7", size = 73225 }, { url = "https://files.pythonhosted.org/packages/96/00/2b325970b3060c7cecebab6d295afe763365822b1306a12eeab198f74323/starlette-0.41.3-py3-none-any.whl", hash = "sha256:44cedb2b7c77a9de33a8b74b2b90e9f50d11fcf25d8270ea525ad71a25374ff7", size = 73225 },
] ]
[[package]]
name = "tomli"
version = "2.2.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077 },
{ url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429 },
{ url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067 },
{ url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030 },
{ url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898 },
{ url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894 },
{ url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319 },
{ url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273 },
{ url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310 },
{ url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309 },
{ url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762 },
{ url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453 },
{ url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486 },
{ url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349 },
{ url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159 },
{ url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243 },
{ url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645 },
{ url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584 },
{ url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875 },
{ url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418 },
{ url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708 },
{ url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582 },
{ url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543 },
{ url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691 },
{ url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170 },
{ url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530 },
{ url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666 },
{ url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954 },
{ url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724 },
{ url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383 },
{ url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 },
]
[[package]] [[package]]
name = "typing-extensions" name = "typing-extensions"
version = "4.12.2" version = "4.12.2"

View file

@ -180,6 +180,14 @@ MCP Server for the GitHub API, enabling file operations, repository management,
- `sha` (optional string): branch name - `sha` (optional string): branch name
- Returns: List of commits - Returns: List of commits
17. `get_issue`
- Gets the contents of an issue within a repository
- Inputs:
- `owner` (string): Repository owner
- `repo` (string): Repository name
- `issue_number` (number): Issue number to retrieve
- Returns: Github Issue object & details
## Search Query Syntax ## Search Query Syntax
### Code Search ### Code Search

View file

@ -20,6 +20,7 @@ import {
CreateRepositorySchema, CreateRepositorySchema,
ForkRepositorySchema, ForkRepositorySchema,
GetFileContentsSchema, GetFileContentsSchema,
GetIssueSchema,
GitHubCommitSchema, GitHubCommitSchema,
GitHubContentSchema, GitHubContentSchema,
GitHubCreateUpdateFileResponseSchema, GitHubCreateUpdateFileResponseSchema,
@ -691,6 +692,29 @@ async function searchUsers(
return SearchUsersResponseSchema.parse(await response.json()); return SearchUsersResponseSchema.parse(await response.json());
} }
async function getIssue(
owner: string,
repo: string,
issueNumber: number
): Promise<GitHubIssue> {
const response = await fetch(
`https://api.github.com/repos/${owner}/${repo}/issues/${issueNumber}`,
{
headers: {
Authorization: `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`,
Accept: "application/vnd.github.v3+json",
"User-Agent": "github-mcp-server",
},
}
);
if (!response.ok) {
throw new Error(`Github API error: ${response.statusText}`);
}
return GitHubIssueSchema.parse(await response.json());
}
server.setRequestHandler(ListToolsRequestSchema, async () => { server.setRequestHandler(ListToolsRequestSchema, async () => {
return { return {
tools: [ tools: [
@ -778,6 +802,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
description: "Search for users on GitHub", description: "Search for users on GitHub",
inputSchema: zodToJsonSchema(SearchUsersSchema), inputSchema: zodToJsonSchema(SearchUsersSchema),
}, },
{
name: "get_issue",
description: "Get details of a specific issue in a GitHub repository.",
inputSchema: zodToJsonSchema(GetIssueSchema)
}
], ],
}; };
}); });
@ -972,6 +1001,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }] }; return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }] };
} }
case "get_issue": {
const args = z.object({
owner: z.string(),
repo: z.string(),
issue_number: z.number()
}).parse(request.params.arguments);
const issue = await getIssue(args.owner, args.repo, args.issue_number);
return { toolResult: issue };
}
default: default:
throw new Error(`Unknown tool: ${request.params.name}`); throw new Error(`Unknown tool: ${request.params.name}`);
} }

View file

@ -274,7 +274,7 @@ export const GitHubIssueSchema = z.object({
created_at: z.string(), created_at: z.string(),
updated_at: z.string(), updated_at: z.string(),
closed_at: z.string().nullable(), closed_at: z.string().nullable(),
body: z.string(), body: z.string().nullable(),
}); });
// Pull Request related schemas // Pull Request related schemas
@ -677,6 +677,12 @@ export const IssueCommentSchema = z.object({
body: z.string() body: z.string()
}); });
export const GetIssueSchema = z.object({
owner: z.string().describe("Repository owner (username or organization)"),
repo: z.string().describe("Repository name"),
issue_number: z.number().describe("Issue number")
});
// Export types // Export types
export type GitHubAuthor = z.infer<typeof GitHubAuthorSchema>; export type GitHubAuthor = z.infer<typeof GitHubAuthorSchema>;
export type GitHubFork = z.infer<typeof GitHubForkSchema>; export type GitHubFork = z.infer<typeof GitHubForkSchema>;

View file

@ -6,7 +6,7 @@ A Model Context Protocol server for retrieving and analyzing issues from Sentry.
### Tools ### Tools
1. `get-sentry-issue` 1. `get_sentry_issue`
- Retrieve and analyze a Sentry issue by ID or URL - Retrieve and analyze a Sentry issue by ID or URL
- Input: - Input:
- `issue_id_or_url` (string): Sentry issue ID or URL to analyze - `issue_id_or_url` (string): Sentry issue ID or URL to analyze

View file

@ -223,7 +223,7 @@ async def serve(auth_token: str) -> Server:
async def handle_list_tools() -> list[types.Tool]: async def handle_list_tools() -> list[types.Tool]:
return [ return [
types.Tool( types.Tool(
name="get-sentry-issue", name="get_sentry_issue",
description="""Retrieve and analyze a Sentry issue by ID or URL. Use this tool when you need to: description="""Retrieve and analyze a Sentry issue by ID or URL. Use this tool when you need to:
- Investigate production errors and crashes - Investigate production errors and crashes
- Access detailed stacktraces from Sentry - Access detailed stacktraces from Sentry
@ -247,7 +247,7 @@ async def serve(auth_token: str) -> Server:
async def handle_call_tool( async def handle_call_tool(
name: str, arguments: dict | None name: str, arguments: dict | None
) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]: ) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
if name != "get-sentry-issue": if name != "get_sentry_issue":
raise ValueError(f"Unknown tool: {name}") raise ValueError(f"Unknown tool: {name}")
if not arguments or "issue_id_or_url" not in arguments: if not arguments or "issue_id_or_url" not in arguments:

View file

@ -22,26 +22,26 @@ The server provides a demonstration prompt:
The server offers six core tools: The server offers six core tools:
#### Query Tools #### Query Tools
- `read-query` - `read_query`
- Execute SELECT queries to read data from the database - Execute SELECT queries to read data from the database
- Input: - Input:
- `query` (string): The SELECT SQL query to execute - `query` (string): The SELECT SQL query to execute
- Returns: Query results as array of objects - Returns: Query results as array of objects
- `write-query` - `write_query`
- Execute INSERT, UPDATE, or DELETE queries - Execute INSERT, UPDATE, or DELETE queries
- Input: - Input:
- `query` (string): The SQL modification query - `query` (string): The SQL modification query
- Returns: `{ affected_rows: number }` - Returns: `{ affected_rows: number }`
- `create-table` - `create_table`
- Create new tables in the database - Create new tables in the database
- Input: - Input:
- `query` (string): CREATE TABLE SQL statement - `query` (string): CREATE TABLE SQL statement
- Returns: Confirmation of table creation - Returns: Confirmation of table creation
#### Schema Tools #### Schema Tools
- `list-tables` - `list_tables`
- Get a list of all tables in the database - Get a list of all tables in the database
- No input required - No input required
- Returns: Array of table names - Returns: Array of table names
@ -53,7 +53,7 @@ The server offers six core tools:
- Returns: Array of column definitions with names and types - Returns: Array of column definitions with names and types
#### Analysis Tools #### Analysis Tools
- `append-insight` - `append_insight`
- Add new business insights to the memo resource - Add new business insights to the memo resource
- Input: - Input:
- `insight` (string): Business insight discovered from data analysis - `insight` (string): Business insight discovered from data analysis

View file

@ -27,12 +27,12 @@ Resources:
This server exposes one key resource: "memo://insights", which is a business insights memo that gets automatically updated throughout the analysis process. As users analyze the database and discover insights, the memo resource gets updated in real-time to reflect new findings. Resources act as living documents that provide context to the conversation. This server exposes one key resource: "memo://insights", which is a business insights memo that gets automatically updated throughout the analysis process. As users analyze the database and discover insights, the memo resource gets updated in real-time to reflect new findings. Resources act as living documents that provide context to the conversation.
Tools: Tools:
This server provides several SQL-related tools: This server provides several SQL-related tools:
"read-query": Executes SELECT queries to read data from the database "read_query": Executes SELECT queries to read data from the database
"write-query": Executes INSERT, UPDATE, or DELETE queries to modify data "write_query": Executes INSERT, UPDATE, or DELETE queries to modify data
"create-table": Creates new tables in the database "create_table": Creates new tables in the database
"list-tables": Shows all existing tables "list_tables": Shows all existing tables
"describe-table": Shows the schema for a specific table "describe_table": Shows the schema for a specific table
"append-insight": Adds a new business insight to the memo resource "append_insight": Adds a new business insight to the memo resource
</mcp> </mcp>
<demo-instructions> <demo-instructions>
You are an AI assistant tasked with generating a comprehensive business scenario based on a given topic. You are an AI assistant tasked with generating a comprehensive business scenario based on a given topic.
@ -68,7 +68,7 @@ a. Present 1 additional multiple-choice query options to the user. Its important
b. Explain the purpose of each query option. b. Explain the purpose of each query option.
c. Wait for the user to select one of the query options. c. Wait for the user to select one of the query options.
d. After each query be sure to opine on the results. d. After each query be sure to opine on the results.
e. Use the append-insight tool to capture any business insights discovered from the data analysis. e. Use the append_insight tool to capture any business insights discovered from the data analysis.
7. Generate a dashboard: 7. Generate a dashboard:
a. Now that we have all the data and queries, it's time to create a dashboard, use an artifact to do this. a. Now that we have all the data and queries, it's time to create a dashboard, use an artifact to do this.
@ -233,7 +233,7 @@ async def main(db_path: str):
"""List available tools""" """List available tools"""
return [ return [
types.Tool( types.Tool(
name="read-query", name="read_query",
description="Execute a SELECT query on the SQLite database", description="Execute a SELECT query on the SQLite database",
inputSchema={ inputSchema={
"type": "object", "type": "object",
@ -244,7 +244,7 @@ async def main(db_path: str):
}, },
), ),
types.Tool( types.Tool(
name="write-query", name="write_query",
description="Execute an INSERT, UPDATE, or DELETE query on the SQLite database", description="Execute an INSERT, UPDATE, or DELETE query on the SQLite database",
inputSchema={ inputSchema={
"type": "object", "type": "object",
@ -255,7 +255,7 @@ async def main(db_path: str):
}, },
), ),
types.Tool( types.Tool(
name="create-table", name="create_table",
description="Create a new table in the SQLite database", description="Create a new table in the SQLite database",
inputSchema={ inputSchema={
"type": "object", "type": "object",
@ -266,7 +266,7 @@ async def main(db_path: str):
}, },
), ),
types.Tool( types.Tool(
name="list-tables", name="list_tables",
description="List all tables in the SQLite database", description="List all tables in the SQLite database",
inputSchema={ inputSchema={
"type": "object", "type": "object",
@ -274,7 +274,7 @@ async def main(db_path: str):
}, },
), ),
types.Tool( types.Tool(
name="describe-table", name="describe_table",
description="Get the schema information for a specific table", description="Get the schema information for a specific table",
inputSchema={ inputSchema={
"type": "object", "type": "object",
@ -285,7 +285,7 @@ async def main(db_path: str):
}, },
), ),
types.Tool( types.Tool(
name="append-insight", name="append_insight",
description="Add a business insight to the memo", description="Add a business insight to the memo",
inputSchema={ inputSchema={
"type": "object", "type": "object",
@ -303,13 +303,13 @@ async def main(db_path: str):
) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]: ) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
"""Handle tool execution requests""" """Handle tool execution requests"""
try: try:
if name == "list-tables": if name == "list_tables":
results = db._execute_query( results = db._execute_query(
"SELECT name FROM sqlite_master WHERE type='table'" "SELECT name FROM sqlite_master WHERE type='table'"
) )
return [types.TextContent(type="text", text=str(results))] return [types.TextContent(type="text", text=str(results))]
elif name == "describe-table": elif name == "describe_table":
if not arguments or "table_name" not in arguments: if not arguments or "table_name" not in arguments:
raise ValueError("Missing table_name argument") raise ValueError("Missing table_name argument")
results = db._execute_query( results = db._execute_query(
@ -317,7 +317,7 @@ async def main(db_path: str):
) )
return [types.TextContent(type="text", text=str(results))] return [types.TextContent(type="text", text=str(results))]
elif name == "append-insight": elif name == "append_insight":
if not arguments or "insight" not in arguments: if not arguments or "insight" not in arguments:
raise ValueError("Missing insight argument") raise ValueError("Missing insight argument")
@ -332,19 +332,19 @@ async def main(db_path: str):
if not arguments: if not arguments:
raise ValueError("Missing arguments") raise ValueError("Missing arguments")
if name == "read-query": if name == "read_query":
if not arguments["query"].strip().upper().startswith("SELECT"): if not arguments["query"].strip().upper().startswith("SELECT"):
raise ValueError("Only SELECT queries are allowed for read-query") raise ValueError("Only SELECT queries are allowed for read_query")
results = db._execute_query(arguments["query"]) results = db._execute_query(arguments["query"])
return [types.TextContent(type="text", text=str(results))] return [types.TextContent(type="text", text=str(results))]
elif name == "write-query": elif name == "write_query":
if arguments["query"].strip().upper().startswith("SELECT"): if arguments["query"].strip().upper().startswith("SELECT"):
raise ValueError("SELECT queries are not allowed for write-query") raise ValueError("SELECT queries are not allowed for write_query")
results = db._execute_query(arguments["query"]) results = db._execute_query(arguments["query"])
return [types.TextContent(type="text", text=str(results))] return [types.TextContent(type="text", text=str(results))]
elif name == "create-table": elif name == "create_table":
if not arguments["query"].strip().upper().startswith("CREATE TABLE"): if not arguments["query"].strip().upper().startswith("CREATE TABLE"):
raise ValueError("Only CREATE TABLE statements are allowed") raise ValueError("Only CREATE TABLE statements are allowed")
db._execute_query(arguments["query"]) db._execute_query(arguments["query"])