diff --git a/README.md b/README.md index 886719d..1f07a70 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ A Model Context Protocol (MCP) server that lets you manipulate Excel files witho - 📈 Create charts and visualizations - 📊 Generate pivot tables - 🔄 Manage worksheets and ranges +- 🔌 Dual transport support: stdio and SSE ## Quick Start @@ -34,36 +35,59 @@ uv pip install -e . ### Running the Server -Start the server (default port 8000): +The server supports two transport modes: stdio and SSE. + +#### Using stdio transport (default) + +Stdio transport is ideal for direct integration with tools like Cursor Desktop or local development, which can manipulate local files: + ```bash -uv run excel-mcp-server +excel-mcp-server stdio ``` -Custom port (e.g., 8080): +#### Using SSE transport + +SSE transport is perfect for web-based applications and remote connections, which manipulate remote files: ```bash -# Bash/Linux/macOS -export FASTMCP_PORT=8080 && uv run excel-mcp-server +excel-mcp sse +``` -# Windows PowerShell -$env:FASTMCP_PORT = "8080"; uv run excel-mcp-server +You can specify host and port for the SSE server: + +```bash +excel-mcp sse --host 127.0.0.1 --port 8080 ``` ## Using with AI Tools ### Cursor IDE -1. Add this configuration to Cursor: +1. Add this configuration to Cursor, choosing the appropriate transport method for your needs: + +**Stdio transport connection** (for local integration): ```json { - "mcpServers": { - "excel": { - "url": "http://localhost:8000/sse", - "env": { - "EXCEL_FILES_PATH": "/path/to/excel/files" + "mcpServers": { + "excel-stdio": { + "command": "uv", + "args": ["run", "excel-mcp-server", "stdio"] } - } - } + } +} +``` + +**SSE transport connection** (for web-based applications): +```json +{ + "mcpServers": { + "excel": { + "url": "http://localhost:8000/sse", + "env": { + "EXCEL_FILES_PATH": "/path/to/excel/files" + } + } + } } ``` @@ -71,17 +95,17 @@ $env:FASTMCP_PORT = "8080"; uv run excel-mcp-server ### Remote Hosting & Transport Protocols -This server uses Server-Sent Events (SSE) transport protocol. For different use cases: +This server supports both stdio and SSE transport protocols for maximum flexibility: -1. **Using with Claude Desktop (requires stdio):** - - Use [Supergateway](https://github.com/supercorp-ai/supergateway) to convert SSE to stdio: +1. **Using with Claude Desktop:** + - Use Stdio transport -2. **Hosting Your MCP Server:** +2. **Hosting Your MCP Server (SSE):** - [Remote MCP Server Guide](https://developers.cloudflare.com/agents/guides/remote-mcp-server/) -## Environment Variables +## Environment Variables (for SSE transport) -- `FASTMCP_PORT`: Server port (default: 8000) +- `FASTMCP_PORT`: Server port for SSE transport (default: 8000) - `EXCEL_FILES_PATH`: Directory for Excel files (default: `./excel_files`) ## Available Tools diff --git a/pyproject.toml b/pyproject.toml index 3d14e9f..94a66cc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,26 +1,27 @@ [project] name = "excel-mcp-server" version = "0.1.1" -description = "MCP server for Excel file manipulation" +description = "Excel MCP Server for manipulating Excel files" readme = "README.md" requires-python = ">=3.10" dependencies = [ - "mcp[cli]>=1.2.0", - "openpyxl>=3.1.2" + "mcp[cli]>=1.6.0", + "openpyxl>=3.1.2", + "typer>=0.15.1" ] [[project.authors]] name = "haris" email = "haris.musa@outlook.com" +[project.scripts] +excel-mcp-server = "excel_mcp.__main__:app" + [build-system] requires = ["hatchling"] build-backend = "hatchling.build" -[project.scripts] -excel-mcp-server = "excel_mcp.__main__:main" - [tool.hatch.build.targets.wheel] packages = ["src/excel_mcp"] [tool.hatch.build] -packages = ["src/excel_mcp"] \ No newline at end of file +packages = ["src/excel_mcp"] diff --git a/src/excel_mcp/__main__.py b/src/excel_mcp/__main__.py index 6eb0e10..61125cf 100644 --- a/src/excel_mcp/__main__.py +++ b/src/excel_mcp/__main__.py @@ -1,13 +1,19 @@ import asyncio -from .server import run_server +import typer +from typing import Optional -def main(): - """Start the Excel MCP server.""" +from .server import run_sse, run_stdio + +app = typer.Typer(help="Excel MCP Server") + +@app.command() +def sse(): + """Start Excel MCP Server in SSE mode""" + print("Excel MCP Server - SSE mode") + print("----------------------") + print("Press Ctrl+C to exit") try: - print("Excel MCP Server") - print("---------------") - print("Starting server... Press Ctrl+C to exit") - asyncio.run(run_server()) + asyncio.run(run_sse()) except KeyboardInterrupt: print("\nShutting down server...") except Exception as e: @@ -15,7 +21,21 @@ def main(): import traceback traceback.print_exc() finally: - print("Server stopped.") + print("Service stopped.") + +@app.command() +def stdio(): + """Start Excel MCP Server in stdio mode""" + try: + run_stdio() + except KeyboardInterrupt: + print("\nShutting down server...") + except Exception as e: + print(f"\nError: {e}") + import traceback + traceback.print_exc() + finally: + print("Service stopped.") if __name__ == "__main__": - main() \ No newline at end of file + app() \ No newline at end of file diff --git a/src/excel_mcp/server.py b/src/excel_mcp/server.py index d186529..79895e7 100644 --- a/src/excel_mcp/server.py +++ b/src/excel_mcp/server.py @@ -34,25 +34,31 @@ from excel_mcp.sheet import ( unmerge_range, ) +# Get project root directory path for log file path. +# When using the stdio transmission method, +# relative paths may cause log files to fail to create +# due to the client's running location and permission issues, +# resulting in the program not being able to run. +# Thus using os.path.join(ROOT_DIR, "excel-mcp.log") instead. + +ROOT_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +LOG_FILE = os.path.join(ROOT_DIR, "excel-mcp.log") + + +# Initialize EXCEL_FILES_PATH variable without assigning a value +EXCEL_FILES_PATH = None + # Configure logging logging.basicConfig( level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", handlers=[ - logging.StreamHandler(sys.stdout), - logging.FileHandler("excel-mcp.log") + # Referring to https://github.com/modelcontextprotocol/python-sdk/issues/409#issuecomment-2816831318 + # The stdio mode server MUST NOT write anything to its stdout that is not a valid MCP message. + logging.FileHandler(LOG_FILE) ], - force=True ) - logger = logging.getLogger("excel-mcp") - -# Get Excel files path from environment or use default -EXCEL_FILES_PATH = os.environ.get("EXCEL_FILES_PATH", "./excel_files") - -# Create the directory if it doesn't exist -os.makedirs(EXCEL_FILES_PATH, exist_ok=True) - # Initialize FastMCP server mcp = FastMCP( "excel-mcp", @@ -80,8 +86,13 @@ def get_excel_path(filename: str) -> str: # If filename is already an absolute path, return it if os.path.isabs(filename): return filename - - # Use the configured Excel files path + + # Check if in SSE mode (EXCEL_FILES_PATH is not None) + if EXCEL_FILES_PATH is None: + # Must use absolute path + raise ValueError(f"Invalid filename: {filename}, must be an absolute path when not in SSE mode") + + # In SSE mode, if it's a relative path, resolve it based on EXCEL_FILES_PATH return os.path.join(EXCEL_FILES_PATH, filename) @mcp.tool() @@ -491,10 +502,16 @@ def validate_excel_range( logger.error(f"Error validating range: {e}") raise -async def run_server(): - """Run the Excel MCP server.""" +async def run_sse(): + """Run Excel MCP server in SSE mode.""" + # Assign value to EXCEL_FILES_PATH in SSE mode + global EXCEL_FILES_PATH + EXCEL_FILES_PATH = os.environ.get("EXCEL_FILES_PATH", "./excel_files") + # Create directory if it doesn't exist + os.makedirs(EXCEL_FILES_PATH, exist_ok=True) + try: - logger.info(f"Starting Excel MCP server (files directory: {EXCEL_FILES_PATH})") + logger.info(f"Starting Excel MCP server with SSE transport (files directory: {EXCEL_FILES_PATH})") await mcp.run_sse_async() except KeyboardInterrupt: logger.info("Server stopped by user") @@ -503,4 +520,19 @@ async def run_server(): logger.error(f"Server failed: {e}") raise finally: - logger.info("Server shutdown complete") \ No newline at end of file + logger.info("Server shutdown complete") + +def run_stdio(): + """Run Excel MCP server in stdio mode.""" + # No need to assign EXCEL_FILES_PATH in stdio mode + + try: + logger.info("Starting Excel MCP server with stdio transport") + mcp.run(transport="stdio") + except KeyboardInterrupt: + logger.info("Server stopped by user") + except Exception as e: + logger.error(f"Server failed: {e}") + raise + finally: + logger.info("Server shutdown complete") \ No newline at end of file