Update mcp_time_server.py

This commit is contained in:
2026-06-12 03:49:03 +00:00
parent 5b2f4bfeb2
commit a80132998e
+59 -16
View File
@@ -10,16 +10,24 @@ Tools:
- parse_natural_language - parse_natural_language
- compare_times - compare_times
- format_time - format_time
API Key Auth:
- Set API_KEY environment variable to enable auth.
- Send as:
- Authorization: Bearer <API_KEY>
- or X-API-Key: <API_KEY>
- If API_KEY is not set, the server runs without auth (for dev).
""" """
import json import json
import os
import sys import sys
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
from http.server import HTTPServer, BaseHTTPRequestHandler from http.server import HTTPServer, BaseHTTPRequestHandler
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
# ---------- MCP helpers ---------- API_KEY = os.environ.get("API_KEY", "").strip()
TOOL_SCHEMA: Dict[str, Any] = { TOOL_SCHEMA: Dict[str, Any] = {
"tools": [ "tools": [
@@ -160,9 +168,6 @@ TOOL_SCHEMA: Dict[str, Any] = {
def parse_iso_datetime(value: str) -> datetime: def parse_iso_datetime(value: str) -> datetime:
"""
Parse ISO 8601-ish datetime strings.
"""
value = value.strip() value = value.strip()
# If no timezone info, treat as UTC # If no timezone info, treat as UTC
if not any(ch in value for ch in ("+", "-", "Z")) or value.endswith("Z"): if not any(ch in value for ch in ("+", "-", "Z")) or value.endswith("Z"):
@@ -173,11 +178,6 @@ def parse_iso_datetime(value: str) -> datetime:
def parse_duration_to_timedelta(dur: str) -> timedelta: def parse_duration_to_timedelta(dur: str) -> timedelta:
"""
Parse duration in:
- ISO 8601: P1DT2H30M, PT1H, P3D, etc.
- Simple tokens: "2 hours", "3 days", "1 week", "5 minutes"
"""
s = dur.strip() s = dur.strip()
# ISO 8601 duration (starts with P) # ISO 8601 duration (starts with P)
@@ -230,9 +230,6 @@ def parse_natural_language_time(
ref: datetime, ref: datetime,
tz: timezone tz: timezone
) -> datetime: ) -> datetime:
"""
Interpret common natural language time expressions.
"""
expr = expression.strip().lower() expr = expression.strip().lower()
now = ref.astimezone(tz) now = ref.astimezone(tz)
@@ -335,6 +332,38 @@ def format_datetime(dt: datetime, fmt: str) -> str:
return dt.strftime(fmt) return dt.strftime(fmt)
def get_api_key_from_request(headers: Any) -> Optional[str]:
"""
Extract API key from:
- Authorization: Bearer <key>
- X-API-Key: <key>
"""
auth = (headers.get("Authorization") or "").strip()
if auth.startswith("Bearer "):
return auth[len("Bearer "):].strip()
x_key = (headers.get("X-API-Key") or "").strip()
if x_key:
return x_key
return None
def require_auth(headers: Any) -> bool:
"""
If API_KEY is set, the request must provide a matching key.
Returns True if allowed, raises HTTPError if not.
"""
if not API_KEY:
return True
key = get_api_key_from_request(headers)
if not key or key != API_KEY:
raise PermissionError("Missing or invalid API key")
return True
# ---------- MCP HTTP handler ---------- # ---------- MCP HTTP handler ----------
class MCPTimeToolsHandler(BaseHTTPRequestHandler): class MCPTimeToolsHandler(BaseHTTPRequestHandler):
@@ -350,18 +379,28 @@ class MCPTimeToolsHandler(BaseHTTPRequestHandler):
self.end_headers() self.end_headers()
self.wfile.write(body) self.wfile.write(body)
def _auth_or_401(self):
try:
return require_auth(self.headers)
except PermissionError:
self._send_json(401, {"error": "Missing or invalid API key"})
return False
def do_GET(self): def do_GET(self):
# Root: simple info # Root: simple info (no auth required)
if self.path == "/": if self.path == "/":
self._send_json(200, { self._send_json(200, {
"service": "mcp-time-tools", "service": "mcp-time-tools",
"transport": "http", "transport": "http",
"auth": "API key required if API_KEY is set (Authorization: Bearer <key> or X-API-Key: <key>)",
"docs": "Use POST /mcp with MCP JSON-RPC requests." "docs": "Use POST /mcp with MCP JSON-RPC requests."
}) })
return return
# MCP discover tools via GET /mcp # MCP discover tools via GET /mcp (auth-gated if API_KEY is set)
if self.path == "/mcp": if self.path == "/mcp":
if not self._auth_or_401():
return
self._send_json(200, { self._send_json(200, {
"jsonrpc": "2.0", "jsonrpc": "2.0",
"tools": TOOL_SCHEMA["tools"] "tools": TOOL_SCHEMA["tools"]
@@ -375,6 +414,9 @@ class MCPTimeToolsHandler(BaseHTTPRequestHandler):
self.send_error(404, "Not Found") self.send_error(404, "Not Found")
return return
if not self._auth_or_401():
return
length = int(self.headers.get("Content-Length", 0)) length = int(self.headers.get("Content-Length", 0))
if length == 0: if length == 0:
self._send_json(400, {"error": "Empty body"}) self._send_json(400, {"error": "Empty body"})
@@ -539,9 +581,10 @@ class MCPTimeToolsHandler(BaseHTTPRequestHandler):
def main(): def main():
# Allow optional port override via environment or argument # Allow optional port override via environment or argument
port = int(sys.argv[1]) if len(sys.argv) > 1 else int(__import__("os").environ.get("PORT", "8080")) port = int(sys.argv[1]) if len(sys.argv) > 1 else int(os.environ.get("PORT", "8080"))
server = HTTPServer(("0.0.0.0", port), MCPTimeToolsHandler) server = HTTPServer(("0.0.0.0", port), MCPTimeToolsHandler)
print(f"MCP Time Tools HTTP server listening on 0.0.0.0:{port}") mode = "auth enabled" if API_KEY else "no auth (API_KEY not set)"
print(f"MCP Time Tools HTTP server listening on 0.0.0.0:{port} [{mode}]")
try: try:
server.serve_forever() server.serve_forever()
except KeyboardInterrupt: except KeyboardInterrupt: