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
|
## Usage
|
||||||
|
|
||||||
### Running Manually
|
### 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.
|
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
|
```bash
|
||||||
uv run src/server.py
|
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
|
### 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`):
|
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
|
## Available Tools
|
||||||
|
|
||||||
|
- **`get_current_time()`**
|
||||||
|
- Returns the current local machine time in ISO format.
|
||||||
|
|
||||||
- **`list_calendars()`**
|
- **`list_calendars()`**
|
||||||
- Returns a list of all calendars the user has access to.
|
- 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
|
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 datetime import datetime
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
# Initialize FastMCP server
|
# Initialize FastMCP server
|
||||||
mcp = FastMCP("CalDAV 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
|
# Initialize CalDAV client
|
||||||
# We initialize it lazily or here if credentials are guaranteed to be present
|
# We initialize it lazily or here if credentials are guaranteed to be present
|
||||||
@@ -14,6 +21,11 @@ except Exception as e:
|
|||||||
import sys
|
import sys
|
||||||
sys.stderr.write(f"Error initializing CalDAV client: {e}\n")
|
sys.stderr.write(f"Error initializing CalDAV client: {e}\n")
|
||||||
client = None
|
client = None
|
||||||
|
@mcp.tool()
|
||||||
|
def get_current_time() -> str:
|
||||||
|
"""Returns the current local machine time in ISO format."""
|
||||||
|
return datetime.now().isoformat()
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
def list_calendars() -> str:
|
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)}"
|
return f"Error deleting event: {str(e)}"
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
import sys
|
||||||
|
sys.stderr.write("CalDAV MCP Server running on stdio... (Press Ctrl+C to stop)\n")
|
||||||
mcp.run()
|
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