mirror of
https://github.com/latinogino/dolibarr-mcp.git
synced 2026-04-20 01:02:40 +02:00
Add HTTP transport option for MCP server
This commit is contained in:
22
README.md
22
README.md
@@ -90,6 +90,9 @@ The server reads configuration from the environment or a `.env` file. Both
|
||||
| `DOLIBARR_URL` / `DOLIBARR_SHOP_URL` | Base Dolibarr API endpoint, e.g. `https://example.com/api/index.php`. Trailing slashes are handled automatically. |
|
||||
| `DOLIBARR_API_KEY` | Personal Dolibarr API token. |
|
||||
| `LOG_LEVEL` | Optional logging verbosity (`INFO`, `DEBUG`, `WARNING`, …). |
|
||||
| `MCP_TRANSPORT` | Transport to use: `stdio` (default) or `http` for streamable HTTP. |
|
||||
| `MCP_HTTP_HOST` | Host/interface to bind when using HTTP transport (default `0.0.0.0`). |
|
||||
| `MCP_HTTP_PORT` | Port to bind when using HTTP transport (default `8080`). |
|
||||
|
||||
Example `.env`:
|
||||
|
||||
@@ -127,8 +130,8 @@ same environment variables when launched from Linux or macOS hosts.
|
||||
|
||||
### Start the MCP server
|
||||
|
||||
The server communicates over STDIO, so run it in the foreground from the virtual
|
||||
environment:
|
||||
The server communicates over STDIO by default, so run it in the foreground from
|
||||
the virtual environment:
|
||||
|
||||
```bash
|
||||
python -m dolibarr_mcp.dolibarr_mcp_server
|
||||
@@ -137,6 +140,20 @@ python -m dolibarr_mcp.dolibarr_mcp_server
|
||||
Logs are written to stderr to avoid interfering with the MCP protocol. Keep the
|
||||
process running while Claude Desktop is active.
|
||||
|
||||
### HTTP streaming mode (for Open WebUI or remote MCP clients)
|
||||
|
||||
Enable the HTTP transport by setting `MCP_TRANSPORT=http` (and optionally
|
||||
`MCP_HTTP_HOST` / `MCP_HTTP_PORT`). This keeps the server running without STDIO
|
||||
and exposes the Streamable HTTP transport compatible with Open WebUI:
|
||||
|
||||
```bash
|
||||
MCP_TRANSPORT=http MCP_HTTP_PORT=8080 python -m dolibarr_mcp.dolibarr_mcp_server
|
||||
```
|
||||
|
||||
Then point Open WebUI’s MCP configuration at `http://<host>:8080/`. The MCP
|
||||
protocol headers (including `mcp-protocol-version`) are handled automatically by
|
||||
Open WebUI’s MCP client.
|
||||
|
||||
### Test the Dolibarr credentials
|
||||
|
||||
Use the standalone connectivity check before wiring the server into an MCP host:
|
||||
@@ -161,4 +178,3 @@ When the environment variables are already set, omit the overrides and run
|
||||
## 📄 License
|
||||
|
||||
Released under the [MIT License](LICENSE).
|
||||
|
||||
|
||||
@@ -16,6 +16,9 @@ services:
|
||||
- PYTHONUNBUFFERED=1
|
||||
|
||||
# MCP Server Configuration
|
||||
- MCP_TRANSPORT=${MCP_TRANSPORT:-http}
|
||||
- MCP_HTTP_PORT=${MCP_HTTP_PORT:-8080}
|
||||
- MCP_HTTP_HOST=${MCP_HTTP_HOST:-0.0.0.0}
|
||||
- MCP_SERVER_NAME=dolibarr-mcp
|
||||
- MCP_SERVER_VERSION=1.0.0
|
||||
|
||||
@@ -29,7 +32,7 @@ services:
|
||||
|
||||
# Expose port for future HTTP interface
|
||||
ports:
|
||||
- "8080:8080"
|
||||
- "18004:8080"
|
||||
|
||||
# Health check
|
||||
healthcheck:
|
||||
|
||||
@@ -39,6 +39,7 @@ dependencies = [
|
||||
"pydantic-settings>=2.0.0",
|
||||
"click>=8.1.0",
|
||||
"python-dotenv>=1.0.0",
|
||||
"uvicorn>=0.25.0",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
|
||||
@@ -12,6 +12,9 @@ python-dotenv>=1.0.0
|
||||
# CLI support
|
||||
click>=8.1.0
|
||||
|
||||
# HTTP server runtime for Streamable HTTP transport
|
||||
uvicorn>=0.25.0
|
||||
|
||||
# Development and testing
|
||||
pytest>=7.4.0
|
||||
pytest-asyncio>=0.21.0
|
||||
|
||||
@@ -38,6 +38,21 @@ class Config(BaseSettings):
|
||||
default="INFO",
|
||||
)
|
||||
|
||||
mcp_transport: str = Field(
|
||||
description="Transport for MCP server (stdio or http)",
|
||||
default="stdio",
|
||||
)
|
||||
|
||||
mcp_http_host: str = Field(
|
||||
description="HTTP host/interface for MCP server",
|
||||
default="0.0.0.0",
|
||||
)
|
||||
|
||||
mcp_http_port: int = Field(
|
||||
description="HTTP port for MCP server",
|
||||
default=8080,
|
||||
)
|
||||
|
||||
@field_validator("dolibarr_url")
|
||||
@classmethod
|
||||
def validate_dolibarr_url(cls, v: str) -> str:
|
||||
@@ -114,6 +129,36 @@ class Config(BaseSettings):
|
||||
return "INFO"
|
||||
return v.upper()
|
||||
|
||||
@field_validator("mcp_transport")
|
||||
@classmethod
|
||||
def validate_transport(cls, v: str) -> str:
|
||||
"""Validate MCP transport selection."""
|
||||
if not v:
|
||||
v = os.getenv("MCP_TRANSPORT", "stdio")
|
||||
normalized = v.lower()
|
||||
if normalized not in {"stdio", "http"}:
|
||||
print(f"⚠️ Invalid MCP_TRANSPORT '{v}', defaulting to stdio", file=sys.stderr)
|
||||
return "stdio"
|
||||
return normalized
|
||||
|
||||
@field_validator("mcp_http_host")
|
||||
@classmethod
|
||||
def validate_http_host(cls, v: str) -> str:
|
||||
"""Validate HTTP host."""
|
||||
return v or os.getenv("MCP_HTTP_HOST", "0.0.0.0")
|
||||
|
||||
@field_validator("mcp_http_port")
|
||||
@classmethod
|
||||
def validate_http_port(cls, v: int) -> int:
|
||||
"""Validate HTTP port."""
|
||||
try:
|
||||
port = int(v)
|
||||
except Exception as exc:
|
||||
raise ValueError(f"Invalid MCP_HTTP_PORT '{v}': {exc}") from exc
|
||||
if not 1 <= port <= 65535:
|
||||
raise ValueError("MCP_HTTP_PORT must be between 1 and 65535")
|
||||
return port
|
||||
|
||||
@classmethod
|
||||
def from_env(cls) -> "Config":
|
||||
"""Create configuration from environment variables with validation."""
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import asyncio
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
import logging
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
@@ -11,12 +10,19 @@ from contextlib import asynccontextmanager
|
||||
from mcp.server.models import InitializationOptions
|
||||
from mcp.server import NotificationOptions, Server
|
||||
from mcp.server.stdio import stdio_server
|
||||
from mcp.server.streamable_http_manager import StreamableHTTPSessionManager
|
||||
from mcp.types import Tool, TextContent
|
||||
|
||||
# Import our Dolibarr components
|
||||
from .config import Config
|
||||
from .dolibarr_client import DolibarrClient, DolibarrAPIError
|
||||
|
||||
# HTTP transport imports
|
||||
from starlette.applications import Starlette
|
||||
from starlette.responses import Response
|
||||
from starlette.routing import Route
|
||||
import uvicorn
|
||||
|
||||
|
||||
# Configure logging to stderr so it doesn't interfere with MCP protocol
|
||||
logging.basicConfig(
|
||||
@@ -1379,11 +1385,13 @@ async def handle_call_tool(name: str, arguments: dict):
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def test_api_connection():
|
||||
async def test_api_connection(config: Config | None = None):
|
||||
"""Test API connection and yield client if successful."""
|
||||
config = None
|
||||
created_config = False
|
||||
try:
|
||||
config = Config()
|
||||
if config is None:
|
||||
config = Config()
|
||||
created_config = True
|
||||
|
||||
# Check if environment variables are set
|
||||
if not config.dolibarr_url or config.dolibarr_url == "https://your-dolibarr-instance.com/api/index.php":
|
||||
@@ -1413,17 +1421,77 @@ async def test_api_connection():
|
||||
yield True # Allow server to start anyway
|
||||
except Exception as e:
|
||||
print(f"⚠️ API test error: {e}", file=sys.stderr)
|
||||
if config is None:
|
||||
if config is None or created_config:
|
||||
print("💡 Check your .env file configuration", file=sys.stderr)
|
||||
print("⚠️ Server will start but API calls may fail", file=sys.stderr)
|
||||
yield True # Allow server to start anyway
|
||||
|
||||
|
||||
async def _run_stdio_server(_config: Config) -> None:
|
||||
"""Run the MCP server over STDIO (default)."""
|
||||
async with stdio_server() as (read_stream, write_stream):
|
||||
await server.run(
|
||||
read_stream,
|
||||
write_stream,
|
||||
InitializationOptions(
|
||||
server_name="dolibarr-mcp",
|
||||
server_version="1.0.1",
|
||||
capabilities=server.get_capabilities(
|
||||
notification_options=NotificationOptions(),
|
||||
experimental_capabilities={},
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _build_http_app(session_manager: StreamableHTTPSessionManager) -> Starlette:
|
||||
"""Create Starlette app that forwards to the StreamableHTTP session manager."""
|
||||
|
||||
async def options_handler(request):
|
||||
"""Lightweight CORS-friendly response for preflight requests."""
|
||||
return Response(status_code=204)
|
||||
|
||||
async def lifespan(app):
|
||||
async with session_manager.run():
|
||||
yield
|
||||
|
||||
return Starlette(
|
||||
routes=[
|
||||
Route("/", session_manager.handle_request, methods=["GET", "POST", "DELETE"]),
|
||||
Route("/{path:path}", session_manager.handle_request, methods=["GET", "POST", "DELETE"]),
|
||||
Route("/", options_handler, methods=["OPTIONS"]),
|
||||
Route("/{path:path}", options_handler, methods=["OPTIONS"]),
|
||||
],
|
||||
lifespan=lifespan,
|
||||
)
|
||||
|
||||
|
||||
async def _run_http_server(config: Config) -> None:
|
||||
"""Run the MCP server over HTTP (StreamableHTTP)."""
|
||||
session_manager = StreamableHTTPSessionManager(server, json_response=False, stateless=False)
|
||||
app = _build_http_app(session_manager)
|
||||
print(
|
||||
f"🌐 Starting MCP HTTP server on {config.mcp_http_host}:{config.mcp_http_port}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
uvicorn_config = uvicorn.Config(
|
||||
app,
|
||||
host=config.mcp_http_host,
|
||||
port=config.mcp_http_port,
|
||||
log_level=config.log_level.lower(),
|
||||
loop="asyncio",
|
||||
access_log=False,
|
||||
)
|
||||
uvicorn_server = uvicorn.Server(uvicorn_config)
|
||||
await uvicorn_server.serve()
|
||||
|
||||
|
||||
async def main():
|
||||
"""Run the Dolibarr MCP server."""
|
||||
|
||||
config = Config()
|
||||
|
||||
# Test API connection but don't fail if it's not working
|
||||
async with test_api_connection() as api_ok:
|
||||
async with test_api_connection(config) as api_ok:
|
||||
if not api_ok:
|
||||
print("⚠️ Starting server without valid API connection", file=sys.stderr)
|
||||
print("📝 Configure your .env file to enable API functionality", file=sys.stderr)
|
||||
@@ -1434,21 +1502,12 @@ async def main():
|
||||
print("🚀 Starting Professional Dolibarr MCP server...", file=sys.stderr)
|
||||
print("✅ Server ready with comprehensive ERP management capabilities", file=sys.stderr)
|
||||
print("📝 Tools will attempt to connect when called", file=sys.stderr)
|
||||
|
||||
|
||||
try:
|
||||
async with stdio_server() as (read_stream, write_stream):
|
||||
await server.run(
|
||||
read_stream,
|
||||
write_stream,
|
||||
InitializationOptions(
|
||||
server_name="dolibarr-mcp",
|
||||
server_version="1.0.1",
|
||||
capabilities=server.get_capabilities(
|
||||
notification_options=NotificationOptions(),
|
||||
experimental_capabilities={},
|
||||
),
|
||||
),
|
||||
)
|
||||
if config.mcp_transport == "http":
|
||||
await _run_http_server(config)
|
||||
else:
|
||||
await _run_stdio_server(config)
|
||||
except Exception as e:
|
||||
print(f"💥 Server error: {e}", file=sys.stderr)
|
||||
raise
|
||||
|
||||
Reference in New Issue
Block a user