feat: Implement Docker deployment with configurable SSE/STDIO modes and add get_current_time tool.

This commit is contained in:
2026-01-25 15:56:42 +01:00
parent d8cb818040
commit 32dd756885
13 changed files with 173 additions and 1 deletions

28
Dockerfile Normal file
View File

@@ -0,0 +1,28 @@
# Use Python 3.14 as requested (using rc-slim as 3.14 is likely in pre-release)
# If 3.14-slim-bookworm is not available, this might need adjustment to python:3.14-rc-slim or similar.
FROM python:3.14-rc-slim-bookworm
# Install uv
COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv
# Set working directory
WORKDIR /app
# Copy dependency files
COPY pyproject.toml uv.lock ./
# Sync dependencies (frozen to ensure reproducibility)
RUN uv sync --frozen
# Copy source code
COPY src/ src/
COPY entrypoint.sh .
# Make entrypoint executable
RUN chmod +x entrypoint.sh
# Expose port 8000 for SSE mode
EXPOSE 8000
# Set entrypoint
ENTRYPOINT ["/app/entrypoint.sh"]

View File

@@ -44,12 +44,42 @@ CALDAV_PASSWORD=YourPassword
## Usage
### Running Manually
#### Standard I/O (Default)
You can run the server directly using `uv`. It communicates via standard input/output (stdio), so running it directly in a terminal will just wait for input.
```bash
uv run src/server.py
```
#### HTTP (SSE)
To run the server over HTTP (Server-Sent Events) - useful for testing with Postman/Inspector or remote access:
```bash
uv run uvicorn src.server:mcp.sse_app --host 0.0.0.0 --port 8000
```
**Endpoint:** `http://localhost:8000/sse`
### Docker Deployment
You can run the server using Docker and Docker Compose. Environment variables are loaded from the `.env` file.
**Build and Run (SSE Mode by default):**
```bash
docker compose up --build
```
**Switching Modes:**
The container supports two modes via the `MCP_MODE` environment variable:
- `SSE` (default): Runs the HTTP server on port 8000.
- `STDIO`: Runs the script via standard I/O.
To run in STDIO mode (e.g., for piping):
```bash
docker compose run -e MCP_MODE=STDIO -T caldev_mcp
```
### Configuring an MCP Client
To use this with an MCP client (like Claude Desktop or another MCP-compatible app), add the following configuration to your client's settings (e.g., `claude_desktop_config.json`):
@@ -73,6 +103,9 @@ To use this with an MCP client (like Claude Desktop or another MCP-compatible ap
## Available Tools
- **`get_current_time()`**
- Returns the current local machine time in ISO format.
- **`list_calendars()`**
- Returns a list of all calendars the user has access to.

11
debug_mcp.py Normal file
View File

@@ -0,0 +1,11 @@
try:
from src.server import mcp
print(f"DIR: {dir(mcp)}")
try:
print(f"DICT_KEYS: {list(mcp.__dict__.keys())}")
except:
pass
except ImportError:
print("ImportError")
except Exception as e:
print(f"Error: {e}")

6
debug_settings.py Normal file
View File

@@ -0,0 +1,6 @@
from src.server import mcp
try:
print(f"SETTINGS DIR: {dir(mcp.settings)}")
print(f"SETTINGS DICT: {mcp.settings.__dict__}")
except Exception as e:
print(f"Error: {e}")

5
debug_sig.py Normal file
View File

@@ -0,0 +1,5 @@
from mcp.server.fastmcp import FastMCP
import inspect
print("Init Signature:")
print(inspect.signature(FastMCP.__init__))

14
docker-compose.yml Normal file
View File

@@ -0,0 +1,14 @@
services:
caldev_mcp:
build: .
image: caldev_mcp
environment:
- MCP_MODE=${MCP_MODE:-SSE}
env_file:
- .env
ports:
- "8000:8000"
volumes:
- .:/app
stdin_open: true # Open stdin
tty: true # Allocate pseudo-TTY

13
entrypoint.sh Executable file
View File

@@ -0,0 +1,13 @@
#!/bin/bash
set -e
# Default to STDIO if not set
MCP_MODE="${MCP_MODE:-STDIO}"
if [ "$MCP_MODE" = "SSE" ]; then
echo "Starting in SSE mode..." >&2
exec uv run uvicorn src.server:mcp.sse_app --host 0.0.0.0 --port 8000
else
echo "Starting in STDIO mode..." >&2
exec uv run src/server.py
fi

1
sig.txt Normal file
View File

@@ -0,0 +1 @@
(self, name: 'str | None' = None, instructions: 'str | None' = None, website_url: 'str | None' = None, icons: 'list[Icon] | None' = None, auth_server_provider: 'OAuthAuthorizationServerProvider[Any, Any, Any] | None' = None, token_verifier: 'TokenVerifier | None' = None, event_store: 'EventStore | None' = None, retry_interval: 'int | None' = None, *, tools: 'list[Tool] | None' = None, debug: 'bool' = False, log_level: "Literal['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']" = 'INFO', host: 'str' = '127.0.0.1', port: 'int' = 8000, mount_path: 'str' = '/', sse_path: 'str' = '/sse', message_path: 'str' = '/messages/', streamable_http_path: 'str' = '/mcp', json_response: 'bool' = False, stateless_http: 'bool' = False, warn_on_duplicate_resources: 'bool' = True, warn_on_duplicate_tools: 'bool' = True, warn_on_duplicate_prompts: 'bool' = True, dependencies: 'Collection[str]' = (), lifespan: 'Callable[[FastMCP[LifespanResultT]], AbstractAsyncContextManager[LifespanResultT]] | None' = None, auth: 'AuthSettings | None' = None, transport_security: 'TransportSecuritySettings | None' = None)

2
sig_output.txt Normal file
View File

@@ -0,0 +1,2 @@
Init Signature:
(self, name: 'str | None' = None, instructions: 'str | None' = None, website_url: 'str | None' = None, icons: 'list[Icon] | None' = None, auth_server_provider: 'OAuthAuthorizationServerProvider[Any, Any, Any] | None' = None, token_verifier: 'TokenVerifier | None' = None, event_store: 'EventStore | None' = None, retry_interval: 'int | None' = None, *, tools: 'list[Tool] | None' = None, debug: 'bool' = False, log_level: "Literal['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']" = 'INFO', host: 'str' = '127.0.0.1', port: 'int' = 8000, mount_path: 'str' = '/', sse_path: 'str' = '/sse', message_path: 'str' = '/messages/', streamable_http_path: 'str' = '/mcp', json_response: 'bool' = False, stateless_http: 'bool' = False, warn_on_duplicate_resources: 'bool' = True, warn_on_duplicate_tools: 'bool' = True, warn_on_duplicate_prompts: 'bool' = True, dependencies: 'Collection[str]' = (), lifespan: 'Callable[[FastMCP[LifespanResultT]], AbstractAsyncContextManager[LifespanResultT]] | None' = None, auth: 'AuthSettings | None' = None, transport_security: 'TransportSecuritySettings | None' = None)

Binary file not shown.

View File

@@ -1,10 +1,17 @@
from mcp.server.fastmcp import FastMCP
from src.caldav_client import CalDAVClient
try:
from src.caldav_client import CalDAVClient
except ImportError:
from caldav_client import CalDAVClient
from datetime import datetime
from typing import Optional
# Initialize FastMCP server
mcp = FastMCP("CalDAV Server")
# Allow external connections
# Allow external connections
mcp.settings.transport_security.allowed_hosts = ["*"]
mcp.settings.transport_security.enable_dns_rebinding_protection = False
# Initialize CalDAV client
# We initialize it lazily or here if credentials are guaranteed to be present
@@ -14,6 +21,11 @@ except Exception as e:
import sys
sys.stderr.write(f"Error initializing CalDAV client: {e}\n")
client = None
@mcp.tool()
def get_current_time() -> str:
"""Returns the current local machine time in ISO format."""
return datetime.now().isoformat()
@mcp.tool()
def list_calendars() -> str:
@@ -116,4 +128,6 @@ def delete_event(calendar_name: str, event_uid: str) -> str:
return f"Error deleting event: {str(e)}"
if __name__ == "__main__":
import sys
sys.stderr.write("CalDAV MCP Server running on stdio... (Press Ctrl+C to stop)\n")
mcp.run()

45
test_mcp.py Normal file
View File

@@ -0,0 +1,45 @@
import asyncio
import sys
import os
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
async def main():
# Get absolute path to the project root
project_dir = os.path.dirname(os.path.abspath(__file__))
# Define the server parameters
server_params = StdioServerParameters(
command="uv",
args=["run", "src/server.py"],
env=os.environ.copy() # Pass current environment including PATH and .env vars
)
print("Connecting to server...")
async with stdio_client(server_params) as (read, write):
async with ClientSession(read, write) as session:
# Initialize
await session.initialize()
print("Initialized!")
# List tools
print("\n--- Available Tools ---")
tools = await session.list_tools()
for tool in tools.tools:
print(f"- {tool.name}: {tool.description}")
# Call list_calendars
print("\n--- Calling list_calendars ---")
try:
result = await session.call_tool("list_calendars", {})
print("Result:")
for content in result.content:
print(content.text)
except Exception as e:
print(f"Error calling tool: {e}")
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
sys.exit(0)