File size: 6,328 Bytes
7e5bd92
2e00e7a
 
7e5bd92
30c74cf
 
2e00e7a
 
30c74cf
 
 
2e00e7a
 
30c74cf
 
2e00e7a
 
 
 
30c74cf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2e00e7a
 
 
 
30c74cf
 
 
 
 
 
 
2e00e7a
30c74cf
 
 
 
 
 
2e00e7a
30c74cf
 
2e00e7a
30c74cf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2e00e7a
30c74cf
 
2e00e7a
 
7e5bd92
635b668
30c74cf
 
635b668
30c74cf
 
 
 
574d348
30c74cf
 
 
635b668
 
 
30c74cf
635b668
30c74cf
635b668
d478b93
30c74cf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7e5bd92
30c74cf
 
 
 
 
 
71b023d
30c74cf
33da02e
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
# app.py
import json
import logging
from datetime import datetime, timezone
from typing import Dict

import gradio as gr

# --------------------------
# Logging setup (quiet + precise)
# --------------------------
class JsonFormatter(logging.Formatter):
    def format(self, record: logging.LogRecord) -> str:
        payload = {
            "ts": datetime.now(timezone.utc).isoformat(timespec="milliseconds"),
            "level": record.levelname,
            "logger": record.name,
            "message": record.getMessage(),
        }
        # Attach any extra dict passed via logger.extra
        for k, v in getattr(record, "__dict__", {}).items():
            if k not in payload and k not in (
                "args", "asctime", "created", "exc_info", "exc_text", "filename",
                "funcName", "levelno", "lineno", "module", "msecs", "msg",
                "name", "pathname", "process", "processName", "relativeCreated",
                "stack_info", "thread", "threadName"
            ):
                payload[k] = v
        return json.dumps(payload, ensure_ascii=False)

def setup_logging() -> logging.Logger:
    root = logging.getLogger()
    root.setLevel(logging.WARNING)  # default higher; opt-in with our logger

    # Silence noisy libs
    logging.getLogger("httpx").setLevel(logging.ERROR)          # suppress asset GETs
    logging.getLogger("uvicorn").setLevel(logging.WARNING)
    logging.getLogger("uvicorn.access").setLevel(logging.ERROR)  # access logs off/noisy

    handler = logging.StreamHandler()
    handler.setFormatter(JsonFormatter())

    app_logger = logging.getLogger("app")
    app_logger.setLevel(logging.INFO)
    # Avoid duplicate handlers if reloaded
    if not any(isinstance(h, logging.StreamHandler) for h in app_logger.handlers):
        app_logger.addHandler(handler)
        app_logger.propagate = False
    return app_logger

log = setup_logging()

# --------------------------
# Time function (LLM-friendly JSON)
# --------------------------
def _time_payload_utc() -> Dict[str, object]:
    """
    Return multiple canonical time representations for LLMs/tools.
    All times are UTC.
    """
    now = datetime.now(timezone.utc)
    iso8601 = now.isoformat(timespec="milliseconds").replace("+00:00", "Z")

    # ISO week/date parts
    iso_year, iso_week, iso_weekday = now.isocalendar()
    yday = int(now.strftime("%j"))

    return {
        # Canonical machine formats
        "utc_iso8601": iso8601,                  # e.g., 2025-11-11T04:15:30.123Z
        "unix_seconds": int(now.timestamp()),    # e.g., 1762824930
        "unix_milliseconds": int(now.timestamp() * 1000),

        # Human-friendly hints (still machine-parseable)
        "date_utc": now.strftime("%Y-%m-%d"),    # 2025-11-11
        "time_utc": now.strftime("%H:%M:%S.%f")[:-3],  # 04:15:30.123
        "weekday_utc": now.strftime("%A"),       # Tuesday
        "rfc_1123_utc": now.strftime("%a, %d %b %Y %H:%M:%S GMT"),

        # Components
        "year": now.year,
        "month": now.month,
        "day": now.day,
        "hour": now.hour,
        "minute": now.minute,
        "second": now.second,
        "millisecond": int(now.microsecond / 1000),

        # ISO calendar context
        "iso_year": iso_year,
        "iso_week": iso_week,
        "iso_weekday": iso_weekday,  # 1=Mon ... 7=Sun
        "yearday": yday,

        # Provenance
        "timezone": "UTC",
        "source": "Datetime MCP Server",
        "note": "All values are UTC. Prefer 'utc_iso8601' for portability."
    }

def now_utc():
    """
    Return current time (UTC) as a rich JSON object.
    Designed to be easily consumed by LLMs/tools.
    """
    payload = _time_payload_utc()
    # Application-level audit log (one line per query)
    log.info("time_requested", extra={"route": "predict", "out_keys": list(payload.keys())})
    return payload

# --------------------------
# Gradio Interface
# --------------------------
demo = gr.Interface(
    fn=now_utc,
    inputs=None,
    outputs=gr.JSON(label="UTC time (LLM-friendly JSON)"),
    title="Datetime MCP Server",
    description="Returns the current UTC datetime in multiple canonical formats."
)

# --------------------------
# Middleware to log only meaningful hits
# --------------------------
def attach_selective_logging_middleware(app):
    """
    Add a lightweight middleware that logs only when an API/predict-like
    endpoint is hit; skips UI asset routes (/_app, /assets, /favicon, etc).
    """
    from fastapi import Request
    from starlette.responses import Response

    ASSET_PREFIXES = ("/_app", "/assets", "/static", "/favicon", "/logo", "/file=")
    INTERESTING_PREFIXES = ("/run", "/queue", "/predict", "/api", "/chat", "/submit")

    @app.middleware("http")
    async def selective_access_log(request: Request, call_next):
        path = request.url.path or "/"
        # Skip common asset paths
        if path.startswith(ASSET_PREFIXES):
            return await call_next(request)

        # Log only interesting interactive endpoints
        if path.startswith(INTERESTING_PREFIXES) or path == "/":
            ua = request.headers.get("user-agent", "")
            client_ip = getattr(request.client, "host", None)
            log.info(
                "incoming_request",
                extra={
                    "method": request.method,
                    "path": path,
                    "client_ip": client_ip,
                    "user_agent": ua[:300],
                },
            )
        response: Response = await call_next(request)
        return response

# Attach middleware to the FastAPI app Gradio runs under
# Gradio 4.x exposes the FastAPI app at demo.server.app
try:
    attach_selective_logging_middleware(demo.server.app)  # type: ignore[attr-defined]
except Exception:
    # Fallback for older/newer Gradio internals; safe no-op if unavailable.
    pass

if __name__ == "__main__":
    # quiet=True reduces banner/noise; mcp_server=True exposes the MCP SSE endpoint.
    demo.launch(
        mcp_server=True,
        quiet=True,            # suppresses gradio banner/extra logs
        show_error=True,       # still useful while developing
        server_name="0.0.0.0",
        server_port=None,
    )

    # https://nimo007-mcp-datetime-server.hf.space/gradio_api/mcp/