Feat/add stdio transport (#25)

* feat: Add stdio transport method.
Update relevant documentation and code to support both SSE and stdio connection methods, initialize the EXCEL_FILES_PATH variable and perform the corresponding path handling.

* Remove PyPI mirror configuration.
This commit is contained in:
allenZhang
2025-05-17 22:39:06 +08:00
committed by GitHub
parent 46adf0b173
commit bbab5bf184
4 changed files with 131 additions and 54 deletions

View File

@ -12,6 +12,7 @@ A Model Context Protocol (MCP) server that lets you manipulate Excel files witho
- 📈 Create charts and visualizations - 📈 Create charts and visualizations
- 📊 Generate pivot tables - 📊 Generate pivot tables
- 🔄 Manage worksheets and ranges - 🔄 Manage worksheets and ranges
- 🔌 Dual transport support: stdio and SSE
## Quick Start ## Quick Start
@ -34,26 +35,49 @@ uv pip install -e .
### Running the Server ### 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 ```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
# Bash/Linux/macOS excel-mcp sse
export FASTMCP_PORT=8080 && uv run excel-mcp-server ```
# Windows PowerShell You can specify host and port for the SSE server:
$env:FASTMCP_PORT = "8080"; uv run excel-mcp-server
```bash
excel-mcp sse --host 127.0.0.1 --port 8080
``` ```
## Using with AI Tools ## Using with AI Tools
### Cursor IDE ### 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-stdio": {
"command": "uv",
"args": ["run", "excel-mcp-server", "stdio"]
}
}
}
```
**SSE transport connection** (for web-based applications):
```json ```json
{ {
"mcpServers": { "mcpServers": {
@ -71,17 +95,17 @@ $env:FASTMCP_PORT = "8080"; uv run excel-mcp-server
### Remote Hosting & Transport Protocols ### 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):** 1. **Using with Claude Desktop:**
- Use [Supergateway](https://github.com/supercorp-ai/supergateway) to convert SSE to stdio: - 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/) - [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`) - `EXCEL_FILES_PATH`: Directory for Excel files (default: `./excel_files`)
## Available Tools ## Available Tools

View File

@ -1,24 +1,25 @@
[project] [project]
name = "excel-mcp-server" name = "excel-mcp-server"
version = "0.1.1" version = "0.1.1"
description = "MCP server for Excel file manipulation" description = "Excel MCP Server for manipulating Excel files"
readme = "README.md" readme = "README.md"
requires-python = ">=3.10" requires-python = ">=3.10"
dependencies = [ dependencies = [
"mcp[cli]>=1.2.0", "mcp[cli]>=1.6.0",
"openpyxl>=3.1.2" "openpyxl>=3.1.2",
"typer>=0.15.1"
] ]
[[project.authors]] [[project.authors]]
name = "haris" name = "haris"
email = "haris.musa@outlook.com" email = "haris.musa@outlook.com"
[project.scripts]
excel-mcp-server = "excel_mcp.__main__:app"
[build-system] [build-system]
requires = ["hatchling"] requires = ["hatchling"]
build-backend = "hatchling.build" build-backend = "hatchling.build"
[project.scripts]
excel-mcp-server = "excel_mcp.__main__:main"
[tool.hatch.build.targets.wheel] [tool.hatch.build.targets.wheel]
packages = ["src/excel_mcp"] packages = ["src/excel_mcp"]

View File

@ -1,13 +1,19 @@
import asyncio import asyncio
from .server import run_server import typer
from typing import Optional
def main(): from .server import run_sse, run_stdio
"""Start the Excel MCP server."""
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: try:
print("Excel MCP Server") asyncio.run(run_sse())
print("---------------")
print("Starting server... Press Ctrl+C to exit")
asyncio.run(run_server())
except KeyboardInterrupt: except KeyboardInterrupt:
print("\nShutting down server...") print("\nShutting down server...")
except Exception as e: except Exception as e:
@ -15,7 +21,21 @@ def main():
import traceback import traceback
traceback.print_exc() traceback.print_exc()
finally: 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__": if __name__ == "__main__":
main() app()

View File

@ -34,25 +34,31 @@ from excel_mcp.sheet import (
unmerge_range, 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 # Configure logging
logging.basicConfig( logging.basicConfig(
level=logging.INFO, level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
handlers=[ handlers=[
logging.StreamHandler(sys.stdout), # Referring to https://github.com/modelcontextprotocol/python-sdk/issues/409#issuecomment-2816831318
logging.FileHandler("excel-mcp.log") # 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") 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 # Initialize FastMCP server
mcp = FastMCP( mcp = FastMCP(
"excel-mcp", "excel-mcp",
@ -81,7 +87,12 @@ def get_excel_path(filename: str) -> str:
if os.path.isabs(filename): if os.path.isabs(filename):
return 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) return os.path.join(EXCEL_FILES_PATH, filename)
@mcp.tool() @mcp.tool()
@ -491,10 +502,16 @@ def validate_excel_range(
logger.error(f"Error validating range: {e}") logger.error(f"Error validating range: {e}")
raise raise
async def run_server(): async def run_sse():
"""Run the Excel MCP server.""" """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: 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() await mcp.run_sse_async()
except KeyboardInterrupt: except KeyboardInterrupt:
logger.info("Server stopped by user") logger.info("Server stopped by user")
@ -504,3 +521,18 @@ async def run_server():
raise raise
finally: finally:
logger.info("Server shutdown complete") 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")