Merge branch 'main' into patch-1
This commit is contained in:
commit
eaf92b0f74
18 changed files with 422 additions and 72 deletions
41
README.md
41
README.md
|
|
@ -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.
|
||||
|
||||
- **[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
|
||||
- **[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
|
||||
- **[Filesystem](src/filesystem)** - Secure file operations with configurable access controls
|
||||
- **[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
|
||||
- **[Slack](src/slack)** - Channel management and messaging capabilities
|
||||
- **[Sqlite](src/sqlite)** - Database interaction and business intelligence capabilities
|
||||
- **[Time](src/time)** - Time and timezone conversion capabilities
|
||||
|
||||
## 🤝 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.
|
||||
|
||||
- <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://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
|
||||
- **[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)
|
||||
- **[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://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://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
|
||||
|
||||
|
|
@ -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.
|
||||
|
||||
- **[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
|
||||
- **[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/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.
|
||||
- **[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.
|
||||
- **[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
|
||||
- **[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.
|
||||
- **[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).
|
||||
- **[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
|
||||
|
||||
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 wong2](https://github.com/wong2/awesome-mcp-servers)** - A curated list of MCP servers by **[wong2](https://github.com/wong2)**
|
||||
- **[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)** (**[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)**
|
||||
- **[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-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
|
||||
|
||||
|
|
|
|||
26
package-lock.json
generated
26
package-lock.json
generated
|
|
@ -1565,6 +1565,13 @@
|
|||
"integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==",
|
||||
"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": {
|
||||
"version": "22.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.0.tgz",
|
||||
|
|
@ -5185,6 +5192,7 @@
|
|||
"@modelcontextprotocol/sdk": "0.5.0",
|
||||
"diff": "^5.1.0",
|
||||
"glob": "^10.3.10",
|
||||
"minimatch": "^10.0.1",
|
||||
"zod-to-json-schema": "^3.23.5"
|
||||
},
|
||||
"bin": {
|
||||
|
|
@ -5192,6 +5200,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@types/diff": "^5.0.9",
|
||||
"@types/minimatch": "^5.1.2",
|
||||
"@types/node": "^20.11.0",
|
||||
"shx": "^0.3.4",
|
||||
"typescript": "^5.3.3"
|
||||
|
|
@ -5246,7 +5255,7 @@
|
|||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"src/filesystem/node_modules/minimatch": {
|
||||
"src/filesystem/node_modules/glob/node_modules/minimatch": {
|
||||
"version": "9.0.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
||||
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
|
||||
|
|
@ -5261,6 +5270,21 @@
|
|||
"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": {
|
||||
"name": "@modelcontextprotocol/server-gdrive",
|
||||
"version": "0.6.2",
|
||||
|
|
|
|||
|
|
@ -30,4 +30,4 @@
|
|||
"@modelcontextprotocol/server-everart": "*",
|
||||
"@modelcontextprotocol/server-sequential-thinking": "*"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -82,6 +82,7 @@ Node.js server implementing Model Context Protocol (MCP) for filesystem operatio
|
|||
- Inputs:
|
||||
- `path` (string): Starting directory
|
||||
- `pattern` (string): Search pattern
|
||||
- `excludePatterns` (string[]): Exclude any patterns. Glob formats are supported.
|
||||
- Case-insensitive matching
|
||||
- Returns full paths to matches
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import os from 'os';
|
|||
import { z } from "zod";
|
||||
import { zodToJsonSchema } from "zod-to-json-schema";
|
||||
import { diffLines, createTwoFilesPatch } from 'diff';
|
||||
import { minimatch } from 'minimatch';
|
||||
|
||||
// Command line argument parsing
|
||||
const args = process.argv.slice(2);
|
||||
|
|
@ -23,7 +24,7 @@ if (args.length === 0) {
|
|||
|
||||
// Normalize all paths consistently
|
||||
function normalizePath(p: string): string {
|
||||
return path.normalize(p).toLowerCase();
|
||||
return path.normalize(p);
|
||||
}
|
||||
|
||||
function expandHome(filepath: string): string {
|
||||
|
|
@ -34,7 +35,7 @@ function expandHome(filepath: string): string {
|
|||
}
|
||||
|
||||
// Store allowed directories in normalized form
|
||||
const allowedDirectories = args.map(dir =>
|
||||
const allowedDirectories = args.map(dir =>
|
||||
normalizePath(path.resolve(expandHome(dir)))
|
||||
);
|
||||
|
||||
|
|
@ -58,7 +59,7 @@ async function validatePath(requestedPath: string): Promise<string> {
|
|||
const absolute = path.isAbsolute(expandedPath)
|
||||
? path.resolve(expandedPath)
|
||||
: path.resolve(process.cwd(), expandedPath);
|
||||
|
||||
|
||||
const normalizedRequested = normalizePath(absolute);
|
||||
|
||||
// Check if path is within allowed directories
|
||||
|
|
@ -126,6 +127,10 @@ const ListDirectoryArgsSchema = z.object({
|
|||
path: z.string(),
|
||||
});
|
||||
|
||||
const DirectoryTreeArgsSchema = z.object({
|
||||
path: z.string(),
|
||||
});
|
||||
|
||||
const MoveFileArgsSchema = z.object({
|
||||
source: z.string(),
|
||||
destination: z.string(),
|
||||
|
|
@ -134,6 +139,7 @@ const MoveFileArgsSchema = z.object({
|
|||
const SearchFilesArgsSchema = z.object({
|
||||
path: z.string(),
|
||||
pattern: z.string(),
|
||||
excludePatterns: z.array(z.string()).optional().default([])
|
||||
});
|
||||
|
||||
const GetFileInfoArgsSchema = z.object({
|
||||
|
|
@ -183,6 +189,7 @@ async function getFileStats(filePath: string): Promise<FileInfo> {
|
|||
async function searchFiles(
|
||||
rootPath: string,
|
||||
pattern: string,
|
||||
excludePatterns: string[] = []
|
||||
): Promise<string[]> {
|
||||
const results: string[] = [];
|
||||
|
||||
|
|
@ -191,11 +198,22 @@ async function searchFiles(
|
|||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(currentPath, entry.name);
|
||||
|
||||
|
||||
try {
|
||||
// Validate each path before processing
|
||||
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())) {
|
||||
results.push(fullPath);
|
||||
}
|
||||
|
|
@ -223,7 +241,7 @@ function createUnifiedDiff(originalContent: string, newContent: string, filepath
|
|||
// Ensure consistent line endings for diff
|
||||
const normalizedOriginal = normalizeLineEndings(originalContent);
|
||||
const normalizedNew = normalizeLineEndings(newContent);
|
||||
|
||||
|
||||
return createTwoFilesPatch(
|
||||
filepath,
|
||||
filepath,
|
||||
|
|
@ -241,33 +259,33 @@ async function applyFileEdits(
|
|||
): Promise<string> {
|
||||
// Read file content and normalize line endings
|
||||
const content = normalizeLineEndings(await fs.readFile(filePath, 'utf-8'));
|
||||
|
||||
|
||||
// Apply edits sequentially
|
||||
let modifiedContent = content;
|
||||
for (const edit of edits) {
|
||||
const normalizedOld = normalizeLineEndings(edit.oldText);
|
||||
const normalizedNew = normalizeLineEndings(edit.newText);
|
||||
|
||||
|
||||
// If exact match exists, use it
|
||||
if (modifiedContent.includes(normalizedOld)) {
|
||||
modifiedContent = modifiedContent.replace(normalizedOld, normalizedNew);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
// Otherwise, try line-by-line matching with flexibility for whitespace
|
||||
const oldLines = normalizedOld.split('\n');
|
||||
const contentLines = modifiedContent.split('\n');
|
||||
let matchFound = false;
|
||||
|
||||
|
||||
for (let i = 0; i <= contentLines.length - oldLines.length; i++) {
|
||||
const potentialMatch = contentLines.slice(i, i + oldLines.length);
|
||||
|
||||
|
||||
// Compare lines with normalized whitespace
|
||||
const isMatch = oldLines.every((oldLine, j) => {
|
||||
const contentLine = potentialMatch[j];
|
||||
return oldLine.trim() === contentLine.trim();
|
||||
});
|
||||
|
||||
|
||||
if (isMatch) {
|
||||
// Preserve original indentation of first line
|
||||
const originalIndent = contentLines[i].match(/^\s*/)?.[0] || '';
|
||||
|
|
@ -282,33 +300,33 @@ async function applyFileEdits(
|
|||
}
|
||||
return line;
|
||||
});
|
||||
|
||||
|
||||
contentLines.splice(i, oldLines.length, ...newLines);
|
||||
modifiedContent = contentLines.join('\n');
|
||||
matchFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (!matchFound) {
|
||||
throw new Error(`Could not find exact match for edit:\n${edit.oldText}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Create unified diff
|
||||
const diff = createUnifiedDiff(content, modifiedContent, filePath);
|
||||
|
||||
|
||||
// Format diff with appropriate number of backticks
|
||||
let numBackticks = 3;
|
||||
while (diff.includes('`'.repeat(numBackticks))) {
|
||||
numBackticks++;
|
||||
}
|
||||
const formattedDiff = `${'`'.repeat(numBackticks)}diff\n${diff}${'`'.repeat(numBackticks)}\n\n`;
|
||||
|
||||
|
||||
if (!dryRun) {
|
||||
await fs.writeFile(filePath, modifiedContent, 'utf-8');
|
||||
}
|
||||
|
||||
|
||||
return formattedDiff;
|
||||
}
|
||||
|
||||
|
|
@ -369,6 +387,15 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|||
"finding specific files within a directory. Only works within allowed directories.",
|
||||
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",
|
||||
description:
|
||||
|
|
@ -399,7 +426,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|||
},
|
||||
{
|
||||
name: "list_allowed_directories",
|
||||
description:
|
||||
description:
|
||||
"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.",
|
||||
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": {
|
||||
const parsed = MoveFileArgsSchema.safeParse(args);
|
||||
if (!parsed.success) {
|
||||
|
|
@ -522,7 +592,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|||
throw new Error(`Invalid arguments for search_files: ${parsed.error}`);
|
||||
}
|
||||
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 {
|
||||
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": {
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: `Allowed directories:\n${allowedDirectories.join('\n')}`
|
||||
content: [{
|
||||
type: "text",
|
||||
text: `Allowed directories:\n${allowedDirectories.join('\n')}`
|
||||
}],
|
||||
};
|
||||
}
|
||||
|
|
@ -574,4 +644,4 @@ async function runServer() {
|
|||
runServer().catch((error) => {
|
||||
console.error("Fatal error running server:", error);
|
||||
process.exit(1);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -22,12 +22,14 @@
|
|||
"@modelcontextprotocol/sdk": "0.5.0",
|
||||
"diff": "^5.1.0",
|
||||
"glob": "^10.3.10",
|
||||
"minimatch": "^10.0.1",
|
||||
"zod-to-json-schema": "^3.23.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/diff": "^5.0.9",
|
||||
"@types/minimatch": "^5.1.2",
|
||||
"@types/node": "^20.11.0",
|
||||
"shx": "^0.3.4",
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,40 +26,53 @@ Please note that mcp-server-git is currently in early development. The functiona
|
|||
- `repo_path` (string): Path to Git repository
|
||||
- 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
|
||||
- Inputs:
|
||||
- `repo_path` (string): Path to Git repository
|
||||
- `message` (string): Commit message
|
||||
- Returns: Confirmation with new commit hash
|
||||
|
||||
5. `git_add`
|
||||
6. `git_add`
|
||||
- Adds file contents to the staging area
|
||||
- Inputs:
|
||||
- `repo_path` (string): Path to Git repository
|
||||
- `files` (string[]): Array of file paths to stage
|
||||
- Returns: Confirmation of staged files
|
||||
|
||||
6. `git_reset`
|
||||
7. `git_reset`
|
||||
- Unstages all staged changes
|
||||
- Input:
|
||||
- `repo_path` (string): Path to Git repository
|
||||
- Returns: Confirmation of reset operation
|
||||
|
||||
7. `git_log`
|
||||
8. `git_log`
|
||||
- Shows the commit logs
|
||||
- Inputs:
|
||||
- `repo_path` (string): Path to Git repository
|
||||
- `max_count` (number, optional): Maximum number of commits to show (default: 10)
|
||||
- Returns: Array of commit entries with hash, author, date, and message
|
||||
|
||||
8. `git_create_branch`
|
||||
9. `git_create_branch`
|
||||
- Creates a new branch
|
||||
- Inputs:
|
||||
- `repo_path` (string): Path to Git repository
|
||||
- `branch_name` (string): Name of the new branch
|
||||
- `start_point` (string, optional): Starting point for the new branch
|
||||
- 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
|
||||
|
||||
|
|
|
|||
|
|
@ -30,4 +30,10 @@ requires = ["hatchling"]
|
|||
build-backend = "hatchling.build"
|
||||
|
||||
[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_*"
|
||||
|
|
@ -24,6 +24,10 @@ class GitDiffUnstaged(BaseModel):
|
|||
class GitDiffStaged(BaseModel):
|
||||
repo_path: str
|
||||
|
||||
class GitDiff(BaseModel):
|
||||
repo_path: str
|
||||
target: str
|
||||
|
||||
class GitCommit(BaseModel):
|
||||
repo_path: str
|
||||
message: str
|
||||
|
|
@ -44,15 +48,21 @@ class GitCreateBranch(BaseModel):
|
|||
branch_name: str
|
||||
base_branch: str | None = None
|
||||
|
||||
class GitCheckout(BaseModel):
|
||||
repo_path: str
|
||||
branch_name: str
|
||||
|
||||
class GitTools(str, Enum):
|
||||
STATUS = "git_status"
|
||||
DIFF_UNSTAGED = "git_diff_unstaged"
|
||||
DIFF_STAGED = "git_diff_staged"
|
||||
DIFF = "git_diff"
|
||||
COMMIT = "git_commit"
|
||||
ADD = "git_add"
|
||||
RESET = "git_reset"
|
||||
LOG = "git_log"
|
||||
CREATE_BRANCH = "git_create_branch"
|
||||
CHECKOUT = "git_checkout"
|
||||
|
||||
def git_status(repo: git.Repo) -> str:
|
||||
return repo.git.status()
|
||||
|
|
@ -63,6 +73,9 @@ def git_diff_unstaged(repo: git.Repo) -> str:
|
|||
def git_diff_staged(repo: git.Repo) -> str:
|
||||
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:
|
||||
commit = repo.index.commit(message)
|
||||
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)
|
||||
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:
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -127,6 +144,11 @@ async def serve(repository: Path | None) -> None:
|
|||
description="Shows changes that are staged for commit",
|
||||
inputSchema=GitDiffStaged.schema(),
|
||||
),
|
||||
Tool(
|
||||
name=GitTools.DIFF,
|
||||
description="Shows differences between branches or commits",
|
||||
inputSchema=GitDiff.schema(),
|
||||
),
|
||||
Tool(
|
||||
name=GitTools.COMMIT,
|
||||
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",
|
||||
inputSchema=GitCreateBranch.schema(),
|
||||
),
|
||||
Tool(
|
||||
name=GitTools.CHECKOUT,
|
||||
description="Switches branches",
|
||||
inputSchema=GitCheckout.schema(),
|
||||
),
|
||||
]
|
||||
|
||||
async def list_repos() -> Sequence[str]:
|
||||
|
|
@ -210,6 +237,13 @@ async def serve(repository: Path | None) -> None:
|
|||
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:
|
||||
result = git_commit(repo, arguments["message"])
|
||||
return [TextContent(
|
||||
|
|
@ -249,6 +283,13 @@ async def serve(repository: Path | None) -> None:
|
|||
text=result
|
||||
)]
|
||||
|
||||
case GitTools.CHECKOUT:
|
||||
result = git_checkout(repo, arguments["branch_name"])
|
||||
return [TextContent(
|
||||
type="text",
|
||||
text=result
|
||||
)]
|
||||
|
||||
case _:
|
||||
raise ValueError(f"Unknown tool: {name}")
|
||||
|
||||
|
|
|
|||
30
src/git/tests/test_server.py
Normal file
30
src/git/tests/test_server.py
Normal 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
89
src/git/uv.lock
generated
|
|
@ -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 },
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "mcp"
|
||||
version = "1.1.0"
|
||||
|
|
@ -156,9 +165,9 @@ dependencies = [
|
|||
{ name = "sse-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 = [
|
||||
{ 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]]
|
||||
|
|
@ -175,6 +184,7 @@ dependencies = [
|
|||
[package.dev-dependencies]
|
||||
dev = [
|
||||
{ name = "pyright" },
|
||||
{ name = "pytest" },
|
||||
{ name = "ruff" },
|
||||
]
|
||||
|
||||
|
|
@ -189,6 +199,7 @@ requires-dist = [
|
|||
[package.metadata.requires-dev]
|
||||
dev = [
|
||||
{ name = "pyright", specifier = ">=1.1.389" },
|
||||
{ name = "pytest", specifier = ">=8.0.0" },
|
||||
{ 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 },
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "pydantic"
|
||||
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 },
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "ruff"
|
||||
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 },
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "typing-extensions"
|
||||
version = "4.12.2"
|
||||
|
|
|
|||
|
|
@ -180,6 +180,14 @@ MCP Server for the GitHub API, enabling file operations, repository management,
|
|||
- `sha` (optional string): branch name
|
||||
- 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
|
||||
|
||||
### Code Search
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import {
|
|||
CreateRepositorySchema,
|
||||
ForkRepositorySchema,
|
||||
GetFileContentsSchema,
|
||||
GetIssueSchema,
|
||||
GitHubCommitSchema,
|
||||
GitHubContentSchema,
|
||||
GitHubCreateUpdateFileResponseSchema,
|
||||
|
|
@ -691,6 +692,29 @@ async function searchUsers(
|
|||
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 () => {
|
||||
return {
|
||||
tools: [
|
||||
|
|
@ -778,6 +802,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|||
description: "Search for users on GitHub",
|
||||
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) }] };
|
||||
}
|
||||
|
||||
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:
|
||||
throw new Error(`Unknown tool: ${request.params.name}`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -274,7 +274,7 @@ export const GitHubIssueSchema = z.object({
|
|||
created_at: z.string(),
|
||||
updated_at: z.string(),
|
||||
closed_at: z.string().nullable(),
|
||||
body: z.string(),
|
||||
body: z.string().nullable(),
|
||||
});
|
||||
|
||||
// Pull Request related schemas
|
||||
|
|
@ -677,6 +677,12 @@ export const IssueCommentSchema = z.object({
|
|||
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 type GitHubAuthor = z.infer<typeof GitHubAuthorSchema>;
|
||||
export type GitHubFork = z.infer<typeof GitHubForkSchema>;
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ A Model Context Protocol server for retrieving and analyzing issues from Sentry.
|
|||
|
||||
### Tools
|
||||
|
||||
1. `get-sentry-issue`
|
||||
1. `get_sentry_issue`
|
||||
- Retrieve and analyze a Sentry issue by ID or URL
|
||||
- Input:
|
||||
- `issue_id_or_url` (string): Sentry issue ID or URL to analyze
|
||||
|
|
|
|||
|
|
@ -223,7 +223,7 @@ async def serve(auth_token: str) -> Server:
|
|||
async def handle_list_tools() -> list[types.Tool]:
|
||||
return [
|
||||
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:
|
||||
- Investigate production errors and crashes
|
||||
- Access detailed stacktraces from Sentry
|
||||
|
|
@ -247,7 +247,7 @@ async def serve(auth_token: str) -> Server:
|
|||
async def handle_call_tool(
|
||||
name: str, arguments: dict | None
|
||||
) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
|
||||
if name != "get-sentry-issue":
|
||||
if name != "get_sentry_issue":
|
||||
raise ValueError(f"Unknown tool: {name}")
|
||||
|
||||
if not arguments or "issue_id_or_url" not in arguments:
|
||||
|
|
|
|||
|
|
@ -22,26 +22,26 @@ The server provides a demonstration prompt:
|
|||
The server offers six core tools:
|
||||
|
||||
#### Query Tools
|
||||
- `read-query`
|
||||
- `read_query`
|
||||
- Execute SELECT queries to read data from the database
|
||||
- Input:
|
||||
- `query` (string): The SELECT SQL query to execute
|
||||
- Returns: Query results as array of objects
|
||||
|
||||
- `write-query`
|
||||
- `write_query`
|
||||
- Execute INSERT, UPDATE, or DELETE queries
|
||||
- Input:
|
||||
- `query` (string): The SQL modification query
|
||||
- Returns: `{ affected_rows: number }`
|
||||
|
||||
- `create-table`
|
||||
- `create_table`
|
||||
- Create new tables in the database
|
||||
- Input:
|
||||
- `query` (string): CREATE TABLE SQL statement
|
||||
- Returns: Confirmation of table creation
|
||||
|
||||
#### Schema Tools
|
||||
- `list-tables`
|
||||
- `list_tables`
|
||||
- Get a list of all tables in the database
|
||||
- No input required
|
||||
- Returns: Array of table names
|
||||
|
|
@ -53,7 +53,7 @@ The server offers six core tools:
|
|||
- Returns: Array of column definitions with names and types
|
||||
|
||||
#### Analysis Tools
|
||||
- `append-insight`
|
||||
- `append_insight`
|
||||
- Add new business insights to the memo resource
|
||||
- Input:
|
||||
- `insight` (string): Business insight discovered from data analysis
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
Tools:
|
||||
This server provides several SQL-related tools:
|
||||
"read-query": Executes SELECT queries to read data from the database
|
||||
"write-query": Executes INSERT, UPDATE, or DELETE queries to modify data
|
||||
"create-table": Creates new tables in the database
|
||||
"list-tables": Shows all existing tables
|
||||
"describe-table": Shows the schema for a specific table
|
||||
"append-insight": Adds a new business insight to the memo resource
|
||||
"read_query": Executes SELECT queries to read data from the database
|
||||
"write_query": Executes INSERT, UPDATE, or DELETE queries to modify data
|
||||
"create_table": Creates new tables in the database
|
||||
"list_tables": Shows all existing tables
|
||||
"describe_table": Shows the schema for a specific table
|
||||
"append_insight": Adds a new business insight to the memo resource
|
||||
</mcp>
|
||||
<demo-instructions>
|
||||
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.
|
||||
c. Wait for the user to select one of the query options.
|
||||
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:
|
||||
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"""
|
||||
return [
|
||||
types.Tool(
|
||||
name="read-query",
|
||||
name="read_query",
|
||||
description="Execute a SELECT query on the SQLite database",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
|
|
@ -244,7 +244,7 @@ async def main(db_path: str):
|
|||
},
|
||||
),
|
||||
types.Tool(
|
||||
name="write-query",
|
||||
name="write_query",
|
||||
description="Execute an INSERT, UPDATE, or DELETE query on the SQLite database",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
|
|
@ -255,7 +255,7 @@ async def main(db_path: str):
|
|||
},
|
||||
),
|
||||
types.Tool(
|
||||
name="create-table",
|
||||
name="create_table",
|
||||
description="Create a new table in the SQLite database",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
|
|
@ -266,7 +266,7 @@ async def main(db_path: str):
|
|||
},
|
||||
),
|
||||
types.Tool(
|
||||
name="list-tables",
|
||||
name="list_tables",
|
||||
description="List all tables in the SQLite database",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
|
|
@ -274,7 +274,7 @@ async def main(db_path: str):
|
|||
},
|
||||
),
|
||||
types.Tool(
|
||||
name="describe-table",
|
||||
name="describe_table",
|
||||
description="Get the schema information for a specific table",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
|
|
@ -285,7 +285,7 @@ async def main(db_path: str):
|
|||
},
|
||||
),
|
||||
types.Tool(
|
||||
name="append-insight",
|
||||
name="append_insight",
|
||||
description="Add a business insight to the memo",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
|
|
@ -303,13 +303,13 @@ async def main(db_path: str):
|
|||
) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
|
||||
"""Handle tool execution requests"""
|
||||
try:
|
||||
if name == "list-tables":
|
||||
if name == "list_tables":
|
||||
results = db._execute_query(
|
||||
"SELECT name FROM sqlite_master WHERE type='table'"
|
||||
)
|
||||
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:
|
||||
raise ValueError("Missing table_name argument")
|
||||
results = db._execute_query(
|
||||
|
|
@ -317,7 +317,7 @@ async def main(db_path: str):
|
|||
)
|
||||
return [types.TextContent(type="text", text=str(results))]
|
||||
|
||||
elif name == "append-insight":
|
||||
elif name == "append_insight":
|
||||
if not arguments or "insight" not in arguments:
|
||||
raise ValueError("Missing insight argument")
|
||||
|
||||
|
|
@ -332,19 +332,19 @@ async def main(db_path: str):
|
|||
if not arguments:
|
||||
raise ValueError("Missing arguments")
|
||||
|
||||
if name == "read-query":
|
||||
if name == "read_query":
|
||||
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"])
|
||||
return [types.TextContent(type="text", text=str(results))]
|
||||
|
||||
elif name == "write-query":
|
||||
elif name == "write_query":
|
||||
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"])
|
||||
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"):
|
||||
raise ValueError("Only CREATE TABLE statements are allowed")
|
||||
db._execute_query(arguments["query"])
|
||||
|
|
|
|||
Loading…
Reference in a new issue