feat: Implement Docker deployment with configurable SSE/STDIO modes and add get_current_time tool.
This commit is contained in:
28
Dockerfile
Normal file
28
Dockerfile
Normal 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"]
|
||||
33
README.md
33
README.md
@@ -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
11
debug_mcp.py
Normal 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
6
debug_settings.py
Normal 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
5
debug_sig.py
Normal 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
14
docker-compose.yml
Normal 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
13
entrypoint.sh
Executable 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
1
sig.txt
Normal 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
2
sig_output.txt
Normal 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.
BIN
src/__pycache__/server.cpython-314.pyc
Normal file
BIN
src/__pycache__/server.cpython-314.pyc
Normal file
Binary file not shown.
@@ -1,10 +1,17 @@
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
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
45
test_mcp.py
Normal 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)
|
||||
Reference in New Issue
Block a user