mirror of
https://github.com/haris-musa/excel-mcp-server.git
synced 2025-12-08 17:12:41 +08:00
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:
52
README.md
52
README.md
@ -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
|
||||||
|
|||||||
@ -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"]
|
||||||
|
|
||||||
|
|||||||
@ -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()
|
||||||
@ -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")
|
||||||
Reference in New Issue
Block a user