app-store-connect-mcp-server

  • src
  • mcp_server_duckdb
import logging from contextlib import closing from typing import Any, List import duckdb import mcp.server.stdio import mcp.types as types from mcp.server import Server from pydantic import AnyUrl from mcp_server_duckdb import Config logger = logging.getLogger("mcp-server-duckdb") logger.info("Starting MCP DuckDB Server") class DuckDBDatabase: def __init__(self, config: Config): self.config = config dir_path = config.db_path.parent if not dir_path.exists(): if config.readonly: raise ValueError(f"Database directory does not exist: {dir_path} in read-only mode") logger.info(f"Creating directory: {dir_path}") dir_path.mkdir(parents=True) if not config.db_path.exists(): if config.readonly: raise ValueError(f"Database file does not exist: {dir_path} in read-only mode") logger.info(f"Creating DuckDB database: {config.db_path}") duckdb.connect(config.db_path).close() self.db_path = config.db_path def connect(self): return duckdb.connect(self.db_path, read_only=self.config.readonly) def execute_query(self, query: object, parameters: object = None) -> List[Any]: with closing(self.connect()) as connection: return connection.execute(query, parameters).fetchall() async def main(config: Config): logger.info(f"Starting DuckDB MCP Server with DB path: {config.db_path}") db = DuckDBDatabase(config) server = Server("mcp-duckdb-server") logger.debug("Registering handlers") @server.list_resources() async def handle_list_resources() -> list[types.Resource]: """ List available duckdb resources. """ return [] @server.read_resource() async def handle_read_resource(uri: AnyUrl) -> str: """ Read a specific note's content by its URI. """ return "No data" @server.list_prompts() async def handle_list_prompts() -> list[types.Prompt]: """ List available prompts. """ return [] @server.get_prompt() async def handle_get_prompt(name: str, arguments: dict[str, str] | None) -> types.GetPromptResult: """ Generate a prompt by combining arguments with server state. """ return types.GetPromptResult( description="No", messages=[], ) @server.list_tools() async def handle_list_tools() -> list[types.Tool]: """List available tools""" tools = [ types.Tool( name="read-query", description="Execute a SELECT query on the DuckDB database", inputSchema={ "type": "object", "properties": { "query": { "type": "string", "description": "SELECT SQL query to execute", }, }, "required": ["query"], }, ), types.Tool( name="list-tables", description="List all tables in the DuckDB database", inputSchema={ "type": "object", "properties": {}, }, ), types.Tool( name="describe-table", description="Get the schema information for a specific table", inputSchema={ "type": "object", "properties": { "table_name": { "type": "string", "description": "Name of the table to describe", }, }, "required": ["table_name"], }, ), ] if not config.readonly: tools.extend( [ types.Tool( name="write-query", description="Execute an INSERT, UPDATE, or DELETE query on the DuckDB database", inputSchema={ "type": "object", "properties": { "query": { "type": "string", "description": "SQL query to execute", }, }, "required": ["query"], }, ), types.Tool( name="create-table", description="Create a new table in the DuckDB database", inputSchema={ "type": "object", "properties": { "query": { "type": "string", "description": "CREATE TABLE SQL statement", }, }, "required": ["query"], }, ), ] ) return tools @server.call_tool() async def handle_call_tool( name: str, arguments: dict[str, Any] | None ) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]: """Handle tool execution requests""" try: if name == "list-tables": results = db.execute_query("SELECT * FROM information_schema.tables;") return [types.TextContent(type="text", text=str(results))] elif name == "describe-table": if not arguments or "table_name" not in arguments: raise ValueError("Missing table_name argument") results = db.execute_query("PRAGMA table_info(?)", [arguments["table_name"]]) return [types.TextContent(type="text", text=str(results))] if not arguments: raise ValueError("Missing arguments") if name == "read-query": if not arguments["query"].strip().upper().startswith("SELECT"): 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": if config.readonly: raise ValueError("Server is running in read-only mode") if arguments["query"].strip().upper().startswith("SELECT"): 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": if config.readonly: raise ValueError("Server is running in read-only mode") if not arguments["query"].strip().upper().startswith("CREATE TABLE"): raise ValueError("Only CREATE TABLE statements are allowed") db.execute_query(arguments["query"]) return [types.TextContent(type="text", text="Table created successfully")] else: raise ValueError(f"Unknown tool: {name}") except duckdb.Error as e: return [types.TextContent(type="text", text=f"Database error: {str(e)}")] except Exception as e: return [types.TextContent(type="text", text=f"Error: {str(e)}")] # Run the server using stdin/stdout streams options = server.create_initialization_options() async with mcp.server.stdio.stdio_server() as (read_stream, write_stream): logger.info("DuckDB MCP Server running with stdio transport") await server.run( read_stream, write_stream, options, )