Update mcp_time_server.py
This commit is contained in:
+59
-16
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user