ghh1125 commited on
Commit
e2bb7fc
·
verified ·
1 Parent(s): 086b1ba

Upload 14 files

Browse files
Dockerfile CHANGED
@@ -1,18 +1,23 @@
1
- FROM python:3.10
2
 
3
- RUN useradd -m -u 1000 user && python -m pip install --upgrade pip
4
- USER user
5
- ENV PATH="/home/user/.local/bin:$PATH"
6
 
7
  WORKDIR /app
8
 
9
- COPY --chown=user ./requirements.txt requirements.txt
10
- RUN pip install --no-cache-dir --upgrade -r requirements.txt
 
 
 
 
 
11
 
12
- COPY --chown=user . /app
13
  ENV MCP_TRANSPORT=http
14
  ENV MCP_PORT=7860
15
 
16
  EXPOSE 7860
17
 
18
- CMD ["python", "dateutil/mcp_output/start_mcp.py"]
 
 
 
1
+ FROM python:3.11-slim
2
 
3
+ ENV PYTHONDONTWRITEBYTECODE=1 \
4
+ PYTHONUNBUFFERED=1
 
5
 
6
  WORKDIR /app
7
 
8
+ RUN groupadd -g 1000 appuser && useradd -m -u 1000 -g 1000 appuser
9
+
10
+ COPY requirements.txt /app/requirements.txt
11
+ RUN pip install --no-cache-dir -r /app/requirements.txt
12
+
13
+ COPY dateutil /app/dateutil
14
+ COPY app.py /app/app.py
15
 
 
16
  ENV MCP_TRANSPORT=http
17
  ENV MCP_PORT=7860
18
 
19
  EXPOSE 7860
20
 
21
+ USER appuser
22
+
23
+ ENTRYPOINT ["python", "dateutil/mcp_output/start_mcp.py"]
README.md CHANGED
@@ -1,10 +1,72 @@
1
  ---
2
- title: Dateutil
3
- emoji: 🔥
4
- colorFrom: pink
5
- colorTo: yellow
6
  sdk: docker
7
  pinned: false
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: dateutil MCP Service
3
+ emoji: 🔧
4
+ colorFrom: blue
5
+ colorTo: indigo
6
  sdk: docker
7
  pinned: false
8
+ license: mit
9
  ---
10
 
11
+ # dateutil MCP Service
12
+
13
+ This deployment packages `python-dateutil` as an MCP service using FastMCP.
14
+ It supports local `stdio` mode and Docker/HTTP mode for HuggingFace Spaces.
15
+
16
+ ## Available MCP Tools
17
+
18
+ - `health_check`
19
+ - `parse_datetime`
20
+ - `parse_iso_datetime`
21
+ - `compute_relativedelta`
22
+ - `generate_rrule_occurrences`
23
+ - `resolve_timezone`
24
+ - `calculate_easter`
25
+ - `list_loaded_modules`
26
+
27
+ ## Local stdio usage
28
+
29
+ ```bash
30
+ cd dateutil/mcp_output
31
+ python start_mcp.py
32
+ ```
33
+
34
+ Or explicitly:
35
+
36
+ ```bash
37
+ MCP_TRANSPORT=stdio MCP_PORT=8000 python start_mcp.py
38
+ ```
39
+
40
+ For local CLI / Claude Desktop style clients, use stdio transport.
41
+
42
+ ## HTTP client usage
43
+
44
+ Run in HTTP mode:
45
+
46
+ ```bash
47
+ MCP_TRANSPORT=http MCP_PORT=7860 python dateutil/mcp_output/start_mcp.py
48
+ ```
49
+
50
+ Then connect MCP clients to:
51
+
52
+ - `http://localhost:7860/mcp`
53
+
54
+ For HuggingFace Spaces, the public endpoint is:
55
+
56
+ - `https://<your-space-host>/mcp`
57
+
58
+ ## Docker
59
+
60
+ Build and run:
61
+
62
+ ```bash
63
+ bash run_docker.sh
64
+ ```
65
+
66
+ Or on PowerShell:
67
+
68
+ ```powershell
69
+ ./run_docker.ps1
70
+ ```
71
+
72
+ The Docker entrypoint is `python dateutil/mcp_output/start_mcp.py`.
app.py CHANGED
@@ -1,45 +1,63 @@
1
- from fastapi import FastAPI
 
2
  import os
3
  import sys
 
 
 
 
 
 
 
 
4
 
5
- mcp_plugin_path = os.path.join(os.path.dirname(__file__), "dateutil", "mcp_output", "mcp_plugin")
6
- sys.path.insert(0, mcp_plugin_path)
7
 
8
- app = FastAPI(
9
- title="Dateutil MCP Service",
10
- description="Auto-generated MCP service for dateutil",
11
- version="1.0.0"
12
- )
13
 
14
  @app.get("/")
15
- def root():
16
  return {
17
- "service": "Dateutil MCP Service",
18
- "version": "1.0.0",
19
- "status": "running",
20
- "transport": os.environ.get("MCP_TRANSPORT", "http")
 
21
  }
22
 
 
23
  @app.get("/health")
24
- def health_check():
25
- return {"status": "healthy", "service": "dateutil MCP"}
 
26
 
27
  @app.get("/tools")
28
- def list_tools():
29
- try:
30
- from mcp_service import create_app
31
- mcp_app = create_app()
32
- tools = []
33
- for tool_name, tool_func in mcp_app.tools.items():
34
- tools.append({
35
- "name": tool_name,
36
- "description": tool_func.__doc__ or "No description available"
37
- })
38
- return {"tools": tools}
39
- except Exception as e:
40
- return {"error": f"Failed to load tools: {str(e)}"}
41
-
42
- if __name__ == "__main__":
43
- import uvicorn
44
- port = int(os.environ.get("PORT", 7860))
45
- uvicorn.run(app, host="0.0.0.0", port=port)
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
  import os
4
  import sys
5
+ from pathlib import Path
6
+
7
+ from fastapi import FastAPI # type: ignore[import-not-found]
8
+
9
+ ROOT_DIR = Path(__file__).resolve().parent
10
+ PLUGIN_DIR = (ROOT_DIR / "dateutil" / "mcp_output" / "mcp_plugin").resolve()
11
+ if str(PLUGIN_DIR) not in sys.path:
12
+ sys.path.insert(0, str(PLUGIN_DIR))
13
 
14
+ app = FastAPI(title="dateutil MCP info app", version="1.0.0")
 
15
 
 
 
 
 
 
16
 
17
  @app.get("/")
18
+ def service_info() -> dict:
19
  return {
20
+ "service": "dateutil-mcp",
21
+ "description": "Supplementary info API for dateutil MCP deployment",
22
+ "mcp_entrypoint": "dateutil/mcp_output/start_mcp.py",
23
+ "transport_default": "stdio",
24
+ "http_endpoint": "/mcp when MCP_TRANSPORT=http",
25
  }
26
 
27
+
28
  @app.get("/health")
29
+ def health() -> dict:
30
+ return {"status": "healthy"}
31
+
32
 
33
  @app.get("/tools")
34
+ def tools() -> dict:
35
+ import importlib
36
+
37
+ service_module = importlib.import_module("mcp_service")
38
+ create_app = getattr(service_module, "create_app")
39
+ mcp = create_app()
40
+ raw_tools = getattr(mcp, "tools", None)
41
+
42
+ items: list[dict] = []
43
+ if isinstance(raw_tools, dict):
44
+ for key, tool_obj in raw_tools.items():
45
+ items.append(
46
+ {
47
+ "name": getattr(tool_obj, "name", str(key)),
48
+ "description": getattr(tool_obj, "description", ""),
49
+ }
50
+ )
51
+ elif isinstance(raw_tools, list):
52
+ for tool_obj in raw_tools:
53
+ items.append(
54
+ {
55
+ "name": getattr(tool_obj, "name", "unknown"),
56
+ "description": getattr(tool_obj, "description", ""),
57
+ }
58
+ )
59
+
60
+ return {"count": len(items), "tools": items}
61
+
62
+
63
+ PORT = int(os.getenv("PORT", "7860"))
dateutil/mcp_output/README_MCP.md CHANGED
@@ -1,129 +1,89 @@
1
- # dateutil MCP (Model Context Protocol) Service README
2
-
3
- ## 1) Project Introduction
4
-
5
- This service wraps the `python-dateutil` library as an MCP (Model Context Protocol) service for robust date/time handling in LLM workflows.
6
-
7
- Main capabilities:
8
- - Parse flexible human-readable datetime strings
9
- - Parse strict ISO-8601 datetime strings
10
- - Perform calendar-aware arithmetic (`relativedelta`)
11
- - Generate recurring schedules (`rrule` / `rruleset`)
12
- - Handle time zones, DST ambiguity, and imaginary times
13
- - Provide utility helpers (today, default tz assignment, delta comparisons)
14
- - Compute Easter dates
15
-
16
- Repository analyzed: `https://github.com/dateutil/dateutil`
17
-
18
- ---
19
-
20
- ## 2) Installation Method
21
-
22
- ### Requirements
23
- - Python 3.x
24
- - `python-dateutil`
25
- - `six` (runtime dependency used by dateutil)
26
-
27
- ### Install
28
- pip install python-dateutil
29
-
30
- If you are implementing this as an MCP (Model Context Protocol) server, also install your MCP runtime/framework (depends on your stack), then add `python-dateutil` to the same environment.
31
-
32
- ---
33
-
34
- ## 3) Quick Start
35
-
36
- Typical service actions your MCP (Model Context Protocol) server can expose:
37
-
38
- - Parse free-form datetime:
39
- - Input: `"next Friday 5pm"`
40
- - Backend: `dateutil.parser.parse(...)`
41
-
42
- - Parse ISO datetime:
43
- - Input: `"2026-03-12T10:30:00+00:00"`
44
- - Backend: `dateutil.parser.isoparser`
45
-
46
- - Date arithmetic:
47
- - Add one month, set weekday rules, etc.
48
- - Backend: `dateutil.relativedelta.relativedelta`
49
-
50
- - Recurrence generation:
51
- - Build schedules like “every Tuesday at 09:00”
52
- - Backend: `dateutil.rrule`
53
-
54
- - Time zone normalization:
55
- - Detect ambiguous/non-existent local times around DST transitions
56
- - Backend: `dateutil.tz.datetime_ambiguous`, `datetime_exists`, `resolve_imaginary`
57
-
58
- - Utility checks:
59
- - `within_delta`, `today`, `default_tzinfo`
60
-
61
- ---
62
-
63
- ## 4) Available Tools and Endpoints List
64
-
65
- Recommended MCP (Model Context Protocol) service endpoint design:
66
-
67
- 1. `datetime.parse`
68
- - Purpose: Parse natural-language or mixed-format datetime strings
69
- - Core module: `dateutil.parser._parser.parse`
70
- - Notes: Powerful but permissive; validate inputs for production workflows
71
-
72
- 2. `datetime.parse_iso`
73
- - Purpose: Strict ISO-8601 parsing
74
- - Core module: `dateutil.parser.isoparser`
75
- - Notes: Prefer when deterministic format validation is required
76
-
77
- 3. `datetime.add_relativedelta`
78
- - Purpose: Calendar-aware arithmetic (months/years/weekday semantics)
79
- - Core module: `dateutil.relativedelta.relativedelta`
80
-
81
- 4. `datetime.recurrence.generate`
82
- - Purpose: Generate repeated datetime sequences
83
- - Core module: `dateutil.rrule.rrule`, `rruleset`, `rrulestr`
84
- - Notes: Supports RFC 5545-style recurrence logic
85
-
86
- 5. `timezone.convert_or_resolve`
87
- - Purpose: Apply/convert time zones and handle DST edge cases
88
- - Core module: `dateutil.tz.tz*`, `datetime_exists`, `datetime_ambiguous`, `resolve_imaginary`, `enfold`
89
-
90
- 6. `calendar.easter`
91
- - Purpose: Compute Easter date by year
92
- - Core module: `dateutil.easter.easter`
93
-
94
- 7. `datetime.utils`
95
- - Purpose: Small helpers (`today`, `default_tzinfo`, `within_delta`)
96
- - Core module: `dateutil.utils`
97
-
98
- ---
99
-
100
- ## 5) Common Issues and Notes
101
-
102
- - Ambiguous or non-existent local times:
103
- - DST transitions can produce duplicate or invalid local timestamps.
104
- - Use `datetime_ambiguous`, `datetime_exists`, and `resolve_imaginary`.
105
-
106
- - Parsing ambiguity:
107
- - Free-form parsing may infer unintended day/month ordering.
108
- - Prefer strict ISO endpoint for critical pipelines.
109
-
110
- - Time zone data differences:
111
- - Platform behavior may vary (especially Windows-specific timezone handling via `tzwin`).
112
-
113
- - Performance:
114
- - Large recurrence expansions can be expensive.
115
- - Require range limits (`start`, `end`, `count`) in endpoint contracts.
116
-
117
- - Testing/dev dependencies:
118
- - `pytest`, `hypothesis`, and Sphinx tooling are for tests/docs, not required at runtime.
119
-
120
- ---
121
-
122
- ## 6) Reference Links / Documentation
123
-
124
- - Upstream repository: https://github.com/dateutil/dateutil
125
- - Official docs: https://dateutil.readthedocs.io/
126
- - PyPI package: https://pypi.org/project/python-dateutil/
127
- - RFC 5545 recurrence background: https://datatracker.ietf.org/doc/html/rfc5545
128
-
129
- If needed, I can also generate a production-ready `mcp_server.py` endpoint skeleton aligned to the tool list above.
 
1
+ # dateutil MCP Plugin
2
+
3
+ This MCP layer exposes key capabilities from `python-dateutil` for parsing, recurrence handling, timezone resolution, and calendar calculations.
4
+
5
+ ## Exposed Tools
6
+
7
+ ### 1) `health_check`
8
+ - Parameters: none
9
+ - Returns dependency health and adapter load state.
10
+ - Example:
11
+ - Call: `health_check()`
12
+
13
+ ### 2) `parse_datetime`
14
+ - Parameters:
15
+ - `value: str`
16
+ - `dayfirst: bool = False`
17
+ - `yearfirst: bool = False`
18
+ - Parses free-form datetime strings.
19
+ - Example:
20
+ - Call: `parse_datetime(value="2026-03-23 10:20", dayfirst=False, yearfirst=True)`
21
+
22
+ ### 3) `parse_iso_datetime`
23
+ - Parameters:
24
+ - `value: str`
25
+ - Parses strict ISO-8601 datetime text.
26
+ - Example:
27
+ - Call: `parse_iso_datetime(value="2026-03-23T10:20:00+08:00")`
28
+
29
+ ### 4) `compute_relativedelta`
30
+ - Parameters:
31
+ - `start: str`
32
+ - `end: str`
33
+ - Computes calendar-aware delta between two date/time values.
34
+ - Example:
35
+ - Call: `compute_relativedelta(start="2025-01-01", end="2026-03-23")`
36
+
37
+ ### 5) `generate_rrule_occurrences`
38
+ - Parameters:
39
+ - `frequency: str` (`YEARLY|MONTHLY|WEEKLY|DAILY`)
40
+ - `dtstart: str`
41
+ - `count: int = 5`
42
+ - `interval: int = 1`
43
+ - Generates recurrence instances using `dateutil.rrule`.
44
+ - Example:
45
+ - Call: `generate_rrule_occurrences(frequency="WEEKLY", dtstart="2026-01-01", count=4, interval=1)`
46
+
47
+ ### 6) `resolve_timezone`
48
+ - Parameters:
49
+ - `tz_name: str`
50
+ - Resolves IANA timezone and reports current offset.
51
+ - Example:
52
+ - Call: `resolve_timezone(tz_name="Asia/Shanghai")`
53
+
54
+ ### 7) `calculate_easter`
55
+ - Parameters:
56
+ - `year: int`
57
+ - Returns Easter Sunday date for a given year.
58
+ - Example:
59
+ - Call: `calculate_easter(year=2027)`
60
+
61
+ ### 8) `list_loaded_modules`
62
+ - Parameters: none
63
+ - Lists adapter-loaded and failed modules.
64
+ - Example:
65
+ - Call: `list_loaded_modules()`
66
+
67
+ ## Run Locally (stdio)
68
+
69
+ From `dateutil/mcp_output/`:
70
+
71
+ ```bash
72
+ python start_mcp.py
73
+ ```
74
+
75
+ Or explicitly:
76
+
77
+ ```bash
78
+ MCP_TRANSPORT=stdio MCP_PORT=8000 python start_mcp.py
79
+ ```
80
+
81
+ ## Run via HTTP Transport
82
+
83
+ ```bash
84
+ MCP_TRANSPORT=http MCP_PORT=7860 python start_mcp.py
85
+ ```
86
+
87
+ The MCP endpoint is served by FastMCP at:
88
+
89
+ - `http://localhost:7860/mcp`
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
dateutil/mcp_output/mcp_plugin/adapter.py CHANGED
@@ -1,432 +1,208 @@
1
- import os
2
- import sys
3
  import importlib
4
- from typing import Any, Dict, Optional, List
 
 
 
 
5
 
6
- source_path = os.path.join(
7
- os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))),
8
- "source",
9
- )
10
- if source_path not in sys.path:
11
- sys.path.insert(0, source_path)
12
 
13
 
14
  class Adapter:
15
- """
16
- Import-mode adapter for the dateutil repository implementation under source/src/dateutil.
17
-
18
- This adapter attempts to import repository modules directly from the local source tree.
19
- If import fails, methods provide graceful fallback responses with actionable guidance.
20
- """
21
-
22
- def __init__(self) -> None:
23
- self.mode = "import"
24
- self._modules: Dict[str, Any] = {}
25
- self._import_errors: Dict[str, str] = {}
26
- self._initialize_imports()
27
-
28
- # -------------------------------------------------------------------------
29
- # Internal helpers
30
- # -------------------------------------------------------------------------
31
- def _result(
32
- self,
33
- status: str,
34
- data: Optional[Any] = None,
35
- message: str = "",
36
- error: str = "",
37
- meta: Optional[Dict[str, Any]] = None,
38
- ) -> Dict[str, Any]:
39
- payload = {"status": status}
40
- if message:
41
- payload["message"] = message
42
- if error:
43
- payload["error"] = error
44
- if data is not None:
45
- payload["data"] = data
46
- if meta is not None:
47
- payload["meta"] = meta
48
- return payload
49
-
50
- def _initialize_imports(self) -> None:
51
- module_names = [
52
- "src.dateutil",
53
- "src.dateutil.easter",
54
- "src.dateutil.parser",
55
- "src.dateutil.parser._parser",
56
- "src.dateutil.parser.isoparser",
57
- "src.dateutil.relativedelta",
58
- "src.dateutil.rrule",
59
- "src.dateutil.tz",
60
- "src.dateutil.tz.tz",
61
- "src.dateutil.tz.win",
62
- "src.dateutil.tzwin",
63
- "src.dateutil.utils",
64
- "src.dateutil.zoneinfo",
65
- "src.dateutil.zoneinfo.rebuild",
66
  ]
67
- for mod_name in module_names:
 
 
 
 
 
 
68
  try:
69
- self._modules[mod_name] = importlib.import_module(mod_name)
 
70
  except Exception as exc:
71
- self._import_errors[mod_name] = str(exc)
72
-
73
- def _get_module(self, module_name: str) -> Dict[str, Any]:
74
- mod = self._modules.get(module_name)
75
- if mod is not None:
76
- return self._result("success", data=mod)
77
- err = self._import_errors.get(module_name, "Unknown import error.")
78
- return self._result(
79
- "fallback",
80
- message=(
81
- f"Module '{module_name}' is unavailable in import mode. "
82
- "Verify repository source path and dependencies (e.g., six)."
83
- ),
84
- error=err,
85
- )
86
-
87
- def health_check(self) -> Dict[str, Any]:
88
- """
89
- Report adapter initialization and import state.
90
-
91
- Returns:
92
- dict: Unified status payload with loaded modules and import errors.
93
- """
94
- return self._result(
95
- "success" if not self._import_errors else "partial",
96
- data={
97
- "mode": self.mode,
98
- "loaded_modules": sorted(self._modules.keys()),
99
- "failed_modules": self._import_errors,
100
- "source_path": source_path,
101
- },
102
- )
103
-
104
- # -------------------------------------------------------------------------
105
- # dateutil.easter
106
- # -------------------------------------------------------------------------
107
- def call_easter(self, year: int, method: int = 3) -> Dict[str, Any]:
108
- """
109
- Compute Easter date for a given year.
110
-
111
- Parameters:
112
- year (int): Year for Easter calculation.
113
- method (int): Calculation method from dateutil.easter constants.
114
-
115
- Returns:
116
- dict: status + computed date or fallback/error information.
117
- """
118
- mod_res = self._get_module("src.dateutil.easter")
119
- if mod_res["status"] != "success":
120
- return mod_res
121
- try:
122
- fn = getattr(mod_res["data"], "easter")
123
- return self._result("success", data=fn(year, method))
124
- except Exception as exc:
125
- return self._result(
126
- "error",
127
- error=f"Failed to compute Easter: {exc}",
128
- message="Validate year and method inputs.",
129
- )
130
-
131
- # -------------------------------------------------------------------------
132
- # dateutil.parser
133
- # -------------------------------------------------------------------------
134
- def call_parse(self, timestr: str, **kwargs: Any) -> Dict[str, Any]:
135
- """
136
- Parse a date/time string using dateutil parser.
137
-
138
- Parameters:
139
- timestr (str): Input date/time text.
140
- **kwargs: Additional parser keyword arguments.
141
-
142
- Returns:
143
- dict: status + parsed datetime or error.
144
- """
145
- mod_res = self._get_module("src.dateutil.parser")
146
- if mod_res["status"] != "success":
147
- return mod_res
148
- try:
149
- fn = getattr(mod_res["data"], "parse")
150
- return self._result("success", data=fn(timestr, **kwargs))
151
- except Exception as exc:
152
- return self._result(
153
- "error",
154
- error=f"Failed to parse datetime string: {exc}",
155
- message="Check input format and parser keyword arguments.",
156
- )
157
-
158
- def create_parserinfo(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
159
- """
160
- Create parserinfo instance from parser internals.
161
-
162
- Parameters:
163
- *args: Positional arguments for parserinfo.
164
- **kwargs: Keyword arguments for parserinfo.
165
-
166
- Returns:
167
- dict: status + parserinfo instance or error.
168
- """
169
- mod_res = self._get_module("src.dateutil.parser._parser")
170
- if mod_res["status"] != "success":
171
- return mod_res
172
- try:
173
- cls = getattr(mod_res["data"], "parserinfo")
174
- return self._result("success", data=cls(*args, **kwargs))
175
- except Exception as exc:
176
- return self._result(
177
- "error",
178
- error=f"Failed to create parserinfo: {exc}",
179
- message="Review parserinfo constructor arguments.",
180
- )
181
-
182
- def create_isoparser(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
183
- """
184
- Create an ISO parser instance.
185
-
186
- Parameters:
187
- *args: Positional constructor args.
188
- **kwargs: Keyword constructor args.
189
-
190
- Returns:
191
- dict: status + isoparser instance or error.
192
- """
193
- mod_res = self._get_module("src.dateutil.parser.isoparser")
194
- if mod_res["status"] != "success":
195
- return mod_res
196
- try:
197
- cls = getattr(mod_res["data"], "isoparser")
198
- return self._result("success", data=cls(*args, **kwargs))
199
- except Exception as exc:
200
- return self._result(
201
- "error",
202
- error=f"Failed to create isoparser: {exc}",
203
- message="Check isoparser constructor parameters.",
204
- )
205
-
206
- def call_isoparse(self, dt_str: str) -> Dict[str, Any]:
207
- """
208
- Parse ISO-8601 datetime string.
209
-
210
- Parameters:
211
- dt_str (str): ISO datetime text.
212
-
213
- Returns:
214
- dict: status + parsed datetime or error.
215
- """
216
- mod_res = self._get_module("src.dateutil.parser")
217
- if mod_res["status"] != "success":
218
- return mod_res
219
- try:
220
- fn = getattr(mod_res["data"], "isoparse")
221
- return self._result("success", data=fn(dt_str))
222
- except Exception as exc:
223
- return self._result(
224
- "error",
225
- error=f"Failed to parse ISO datetime: {exc}",
226
- message="Ensure the input follows ISO-8601 format.",
227
- )
228
-
229
- # -------------------------------------------------------------------------
230
- # dateutil.relativedelta
231
- # -------------------------------------------------------------------------
232
- def create_relativedelta(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
233
- """
234
- Create relativedelta instance.
235
-
236
- Parameters:
237
- *args: Positional arguments.
238
- **kwargs: Keyword arguments.
239
-
240
- Returns:
241
- dict: status + relativedelta instance or error.
242
- """
243
- mod_res = self._get_module("src.dateutil.relativedelta")
244
- if mod_res["status"] != "success":
245
- return mod_res
246
- try:
247
- cls = getattr(mod_res["data"], "relativedelta")
248
- return self._result("success", data=cls(*args, **kwargs))
249
- except Exception as exc:
250
- return self._result(
251
- "error",
252
- error=f"Failed to create relativedelta: {exc}",
253
- message="Check relativedelta arguments.",
254
- )
255
-
256
- # -------------------------------------------------------------------------
257
- # dateutil.rrule
258
- # -------------------------------------------------------------------------
259
- def create_rrule(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
260
- mod_res = self._get_module("src.dateutil.rrule")
261
- if mod_res["status"] != "success":
262
- return mod_res
263
- try:
264
- cls = getattr(mod_res["data"], "rrule")
265
- return self._result("success", data=cls(*args, **kwargs))
266
- except Exception as exc:
267
- return self._result("error", error=f"Failed to create rrule: {exc}")
268
-
269
- def create_rruleset(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
270
- mod_res = self._get_module("src.dateutil.rrule")
271
- if mod_res["status"] != "success":
272
- return mod_res
273
- try:
274
- cls = getattr(mod_res["data"], "rruleset")
275
- return self._result("success", data=cls(*args, **kwargs))
276
- except Exception as exc:
277
- return self._result("error", error=f"Failed to create rruleset: {exc}")
278
-
279
- def call_rrulestr(self, s: str, **kwargs: Any) -> Dict[str, Any]:
280
- mod_res = self._get_module("src.dateutil.rrule")
281
- if mod_res["status"] != "success":
282
- return mod_res
283
- try:
284
- fn = getattr(mod_res["data"], "rrulestr")
285
- return self._result("success", data=fn(s, **kwargs))
286
- except Exception as exc:
287
- return self._result("error", error=f"Failed to parse RRULE string: {exc}")
288
-
289
- # -------------------------------------------------------------------------
290
- # dateutil.tz
291
- # -------------------------------------------------------------------------
292
- def call_gettz(self, name: Optional[str] = None) -> Dict[str, Any]:
293
- mod_res = self._get_module("src.dateutil.tz")
294
- if mod_res["status"] != "success":
295
- return mod_res
296
- try:
297
- fn = getattr(mod_res["data"], "gettz")
298
- return self._result("success", data=fn(name))
299
- except Exception as exc:
300
- return self._result("error", error=f"Failed to resolve timezone: {exc}")
301
-
302
- def call_datetime_exists(self, dt: Any, tz: Optional[Any] = None) -> Dict[str, Any]:
303
- mod_res = self._get_module("src.dateutil.tz")
304
- if mod_res["status"] != "success":
305
- return mod_res
306
- try:
307
- fn = getattr(mod_res["data"], "datetime_exists")
308
- return self._result("success", data=fn(dt, tz))
309
- except Exception as exc:
310
- return self._result("error", error=f"Failed to check datetime existence: {exc}")
311
-
312
- def call_datetime_ambiguous(self, dt: Any, tz: Optional[Any] = None) -> Dict[str, Any]:
313
- mod_res = self._get_module("src.dateutil.tz")
314
- if mod_res["status"] != "success":
315
- return mod_res
316
- try:
317
- fn = getattr(mod_res["data"], "datetime_ambiguous")
318
- return self._result("success", data=fn(dt, tz))
319
- except Exception as exc:
320
- return self._result("error", error=f"Failed to check datetime ambiguity: {exc}")
321
-
322
- def call_resolve_imaginary(self, dt: Any) -> Dict[str, Any]:
323
- mod_res = self._get_module("src.dateutil.tz")
324
- if mod_res["status"] != "success":
325
- return mod_res
326
- try:
327
- fn = getattr(mod_res["data"], "resolve_imaginary")
328
- return self._result("success", data=fn(dt))
329
- except Exception as exc:
330
- return self._result("error", error=f"Failed to resolve imaginary time: {exc}")
331
-
332
- # -------------------------------------------------------------------------
333
- # dateutil.utils
334
- # -------------------------------------------------------------------------
335
- def call_today(self, tzinfo: Optional[Any] = None) -> Dict[str, Any]:
336
- mod_res = self._get_module("src.dateutil.utils")
337
- if mod_res["status"] != "success":
338
- return mod_res
339
- try:
340
- fn = getattr(mod_res["data"], "today")
341
- return self._result("success", data=fn(tzinfo))
342
- except Exception as exc:
343
- return self._result("error", error=f"Failed to compute today(): {exc}")
344
-
345
- def call_within_delta(self, dt1: Any, dt2: Any, delta: Any) -> Dict[str, Any]:
346
- mod_res = self._get_module("src.dateutil.utils")
347
- if mod_res["status"] != "success":
348
- return mod_res
349
- try:
350
- fn = getattr(mod_res["data"], "within_delta")
351
- return self._result("success", data=fn(dt1, dt2, delta))
352
- except Exception as exc:
353
- return self._result("error", error=f"Failed to evaluate within_delta: {exc}")
354
-
355
- # -------------------------------------------------------------------------
356
- # dateutil.zoneinfo
357
- # -------------------------------------------------------------------------
358
- def call_get_zonefile_instance(self) -> Dict[str, Any]:
359
- mod_res = self._get_module("src.dateutil.zoneinfo")
360
- if mod_res["status"] != "success":
361
- return mod_res
362
- try:
363
- fn = getattr(mod_res["data"], "get_zonefile_instance")
364
- return self._result("success", data=fn())
365
- except Exception as exc:
366
- return self._result("error", error=f"Failed to get zonefile instance: {exc}")
367
-
368
- def call_gettz_zoneinfo(self, name: str) -> Dict[str, Any]:
369
- mod_res = self._get_module("src.dateutil.zoneinfo")
370
- if mod_res["status"] != "success":
371
- return mod_res
372
- try:
373
- fn = getattr(mod_res["data"], "gettz")
374
- return self._result("success", data=fn(name))
375
- except Exception as exc:
376
- return self._result("error", error=f"Failed to load zoneinfo timezone: {exc}")
377
-
378
- def call_rebuild_zoneinfo(self, filename: Optional[str] = None) -> Dict[str, Any]:
379
- mod_res = self._get_module("src.dateutil.zoneinfo.rebuild")
380
- if mod_res["status"] != "success":
381
- return mod_res
382
- try:
383
- fn = getattr(mod_res["data"], "rebuild")
384
- if filename is None:
385
- return self._result("success", data=fn())
386
- return self._result("success", data=fn(filename))
387
- except Exception as exc:
388
- return self._result(
389
- "error",
390
- error=f"Failed to rebuild zoneinfo data: {exc}",
391
- message="Provide a valid zoneinfo tarball path or run with default configuration.",
392
- )
393
-
394
- # -------------------------------------------------------------------------
395
- # Discovery helpers
396
- # -------------------------------------------------------------------------
397
- def list_available_modules(self) -> Dict[str, Any]:
398
- """
399
- List imported and failed modules for quick adapter introspection.
400
-
401
- Returns:
402
- dict: status with module availability details.
403
- """
404
- return self._result(
405
- "success",
406
- data={
407
- "available": sorted(self._modules.keys()),
408
- "failed": self._import_errors,
409
- },
410
- )
411
-
412
- def list_module_attributes(self, module_name: str, public_only: bool = True) -> Dict[str, Any]:
413
- """
414
- List attributes for an imported module.
415
-
416
- Parameters:
417
- module_name (str): Fully-qualified module name.
418
- public_only (bool): If True, omit private names starting with underscore.
419
-
420
- Returns:
421
- dict: status with attribute names or fallback/error details.
422
- """
423
- mod_res = self._get_module(module_name)
424
- if mod_res["status"] != "success":
425
- return mod_res
426
- try:
427
- names: List[str] = dir(mod_res["data"])
428
- if public_only:
429
- names = [n for n in names if not n.startswith("_")]
430
- return self._result("success", data=sorted(names))
431
- except Exception as exc:
432
- return self._result("error", error=f"Failed to inspect module attributes: {exc}")
 
1
+ from __future__ import annotations
2
+
3
  import importlib
4
+ import inspect
5
+ import sys
6
+ from pathlib import Path
7
+ from types import ModuleType
8
+ from typing import Any
9
 
10
+ CURRENT_DIR = Path(__file__).resolve().parent
11
+ SOURCE_DIR = (CURRENT_DIR / ".." / ".." / "source").resolve()
12
+ if str(SOURCE_DIR) not in sys.path:
13
+ sys.path.insert(0, str(SOURCE_DIR))
 
 
14
 
15
 
16
  class Adapter:
17
+ def __init__(self, module_names: list[str] | None = None) -> None:
18
+ self.module_names = module_names or [
19
+ "dateutil",
20
+ "dateutil.parser",
21
+ "dateutil.relativedelta",
22
+ "dateutil.rrule",
23
+ "dateutil.tz",
24
+ "dateutil.easter",
25
+ "dateutil.zoneinfo",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  ]
27
+ self.loaded_modules: dict[str, ModuleType] = {}
28
+ self.failed_modules: dict[str, str] = {}
29
+ self.mode = "blackbox"
30
+ self._load_modules()
31
+
32
+ def _load_modules(self) -> None:
33
+ for module_name in self.module_names:
34
  try:
35
+ module = importlib.import_module(module_name)
36
+ self.loaded_modules[module_name] = module
37
  except Exception as exc:
38
+ self.failed_modules[module_name] = str(exc)
39
+
40
+ self.mode = "active" if self.loaded_modules else "blackbox"
41
+
42
+ def health(self) -> dict[str, Any]:
43
+ status = "ok" if self.loaded_modules else "fallback"
44
+ return {
45
+ "status": status,
46
+ "mode": self.mode,
47
+ "loaded_count": len(self.loaded_modules),
48
+ "failed_count": len(self.failed_modules),
49
+ "loaded_modules": sorted(self.loaded_modules.keys()),
50
+ "failed_modules": self.failed_modules,
51
+ }
52
+
53
+ def list_modules(self) -> dict[str, Any]:
54
+ status = "ok" if self.loaded_modules else "fallback"
55
+ return {
56
+ "status": status,
57
+ "mode": self.mode,
58
+ "loaded": sorted(self.loaded_modules.keys()),
59
+ "failed": self.failed_modules,
60
+ }
61
+
62
+ def list_symbols(self, module_name: str, public_only: bool = True) -> dict[str, Any]:
63
+ module = self.loaded_modules.get(module_name)
64
+ if module is None:
65
+ if module_name in self.failed_modules:
66
+ return {
67
+ "status": "error",
68
+ "module": module_name,
69
+ "error": self.failed_modules[module_name],
70
+ }
71
+ return {
72
+ "status": "error",
73
+ "module": module_name,
74
+ "error": "Module not loaded",
75
+ }
76
+
77
+ try:
78
+ symbols = []
79
+ for name in dir(module):
80
+ if public_only and name.startswith("_"):
81
+ continue
82
+ value = getattr(module, name)
83
+ if inspect.ismodule(value):
84
+ kind = "module"
85
+ elif inspect.isclass(value):
86
+ kind = "class"
87
+ elif inspect.isfunction(value) or inspect.isbuiltin(value):
88
+ kind = "function"
89
+ else:
90
+ kind = type(value).__name__
91
+ symbols.append({"name": name, "kind": kind})
92
+
93
+ return {
94
+ "status": "ok",
95
+ "module": module_name,
96
+ "count": len(symbols),
97
+ "symbols": symbols,
98
+ }
99
+ except Exception as exc:
100
+ return {
101
+ "status": "error",
102
+ "module": module_name,
103
+ "error": str(exc),
104
+ }
105
+
106
+ def call_function(
107
+ self,
108
+ module_name: str,
109
+ function_name: str,
110
+ args: list[Any] | None = None,
111
+ kwargs: dict[str, Any] | None = None,
112
+ ) -> dict[str, Any]:
113
+ module = self.loaded_modules.get(module_name)
114
+ if module is None:
115
+ return {
116
+ "status": "error",
117
+ "module": module_name,
118
+ "function": function_name,
119
+ "error": "Module not loaded",
120
+ }
121
+
122
+ try:
123
+ function_obj = getattr(module, function_name)
124
+ except AttributeError:
125
+ return {
126
+ "status": "error",
127
+ "module": module_name,
128
+ "function": function_name,
129
+ "error": "Function not found",
130
+ }
131
+
132
+ if not callable(function_obj):
133
+ return {
134
+ "status": "error",
135
+ "module": module_name,
136
+ "function": function_name,
137
+ "error": "Target is not callable",
138
+ }
139
+
140
+ try:
141
+ final_args = args or []
142
+ final_kwargs = kwargs or {}
143
+ result = function_obj(*final_args, **final_kwargs)
144
+ return {
145
+ "status": "ok",
146
+ "module": module_name,
147
+ "function": function_name,
148
+ "result": result,
149
+ }
150
+ except Exception as exc:
151
+ return {
152
+ "status": "error",
153
+ "module": module_name,
154
+ "function": function_name,
155
+ "error": str(exc),
156
+ }
157
+
158
+ def create_instance(
159
+ self,
160
+ module_name: str,
161
+ class_name: str,
162
+ args: list[Any] | None = None,
163
+ kwargs: dict[str, Any] | None = None,
164
+ ) -> dict[str, Any]:
165
+ module = self.loaded_modules.get(module_name)
166
+ if module is None:
167
+ return {
168
+ "status": "error",
169
+ "module": module_name,
170
+ "class": class_name,
171
+ "error": "Module not loaded",
172
+ }
173
+
174
+ try:
175
+ class_obj = getattr(module, class_name)
176
+ except AttributeError:
177
+ return {
178
+ "status": "error",
179
+ "module": module_name,
180
+ "class": class_name,
181
+ "error": "Class not found",
182
+ }
183
+
184
+ if not inspect.isclass(class_obj):
185
+ return {
186
+ "status": "error",
187
+ "module": module_name,
188
+ "class": class_name,
189
+ "error": "Target is not a class",
190
+ }
191
+
192
+ try:
193
+ final_args = args or []
194
+ final_kwargs = kwargs or {}
195
+ instance = class_obj(*final_args, **final_kwargs)
196
+ return {
197
+ "status": "ok",
198
+ "module": module_name,
199
+ "class": class_name,
200
+ "instance_type": type(instance).__name__,
201
+ }
202
+ except Exception as exc:
203
+ return {
204
+ "status": "error",
205
+ "module": module_name,
206
+ "class": class_name,
207
+ "error": str(exc),
208
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
dateutil/mcp_output/mcp_plugin/main.py CHANGED
@@ -1,13 +1,6 @@
1
- """
2
- MCP Service Auto-Wrapper - Auto-generated
3
- """
4
  from mcp_service import create_app
5
 
6
- def main():
7
- """Main entry point"""
8
- app = create_app()
9
- return app
10
-
11
  if __name__ == "__main__":
12
- app = main()
13
- app.run()
 
 
 
 
1
  from mcp_service import create_app
2
 
3
+ # Local stdio entry point only (Claude Desktop / CLI), not for web or Docker deployment.
 
 
 
 
4
  if __name__ == "__main__":
5
+ app = create_app()
6
+ app.run()
dateutil/mcp_output/mcp_plugin/mcp_service.py CHANGED
@@ -1,242 +1,278 @@
1
- import os
2
- import sys
3
- from datetime import datetime, date
4
- from typing import List, Optional, Dict, Any
5
 
6
- source_path = os.path.join(
7
- os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))),
8
- "source",
9
- )
10
- if source_path not in sys.path:
11
- sys.path.insert(0, source_path)
12
 
13
  from fastmcp import FastMCP
14
- from dateutil import easter
15
- from dateutil.parser import parse as dt_parse
16
- from dateutil.parser import isoparse
17
- from dateutil.relativedelta import relativedelta
18
- from dateutil.rrule import rrule, rrulestr, YEARLY, MONTHLY, WEEKLY, DAILY, HOURLY, MINUTELY, SECONDLY
19
- from dateutil.tz import gettz, UTC
20
-
21
-
22
- mcp = FastMCP("dateutil_service")
23
-
24
-
25
- _FREQ_MAP = {
26
- "YEARLY": YEARLY,
27
- "MONTHLY": MONTHLY,
28
- "WEEKLY": WEEKLY,
29
- "DAILY": DAILY,
30
- "HOURLY": HOURLY,
31
- "MINUTELY": MINUTELY,
32
- "SECONDLY": SECONDLY,
33
- }
34
-
35
-
36
- def _safe_result(value: Any) -> Dict[str, Any]:
37
- return {"success": True, "result": value, "error": None}
38
-
39
 
40
- def _safe_error(exc: Exception) -> Dict[str, Any]:
41
- return {"success": False, "result": None, "error": str(exc)}
42
-
43
-
44
- @mcp.tool(name="parse_datetime", description="Parse a general datetime string into ISO format.")
45
- def parse_datetime(
46
- text: str,
47
- dayfirst: bool = False,
48
- yearfirst: bool = False,
49
- default_iso: Optional[str] = None
50
- ) -> Dict[str, Any]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  """
52
- Parse a datetime string using dateutil.parser.parse.
53
 
54
  Parameters:
55
- - text: Input datetime text to parse.
56
- - dayfirst: Whether to interpret ambiguous dates as day-first.
57
- - yearfirst: Whether to interpret ambiguous dates as year-first.
58
- - default_iso: Optional ISO datetime used as default values for missing parts.
59
-
60
- Returns:
61
- - Dictionary with success/result/error.
62
  """
 
 
 
63
  try:
64
- default_dt = None
65
- if default_iso:
66
- default_dt = datetime.fromisoformat(default_iso)
67
- parsed = dt_parse(text, dayfirst=dayfirst, yearfirst=yearfirst, default=default_dt)
68
- return _safe_result(parsed.isoformat())
69
  except Exception as exc:
70
- return _safe_error(exc)
71
 
72
 
73
- @mcp.tool(name="parse_iso_datetime", description="Parse an ISO-8601 datetime string.")
74
- def parse_iso_datetime(text: str) -> Dict[str, Any]:
75
  """
76
- Parse an ISO-8601 datetime string using dateutil.parser.isoparse.
77
 
78
  Parameters:
79
- - text: ISO datetime text.
80
-
81
- Returns:
82
- - Dictionary with success/result/error.
83
  """
 
 
 
84
  try:
85
- parsed = isoparse(text)
86
- return _safe_result(parsed.isoformat())
87
  except Exception as exc:
88
- return _safe_error(exc)
89
 
90
 
91
- @mcp.tool(name="compute_easter", description="Compute Easter date for a given year.")
92
- def compute_easter(year: int, method: int = 3) -> Dict[str, Any]:
93
  """
94
- Compute Easter date using dateutil.easter.easter.
95
 
96
  Parameters:
97
- - year: Target year.
98
- - method: Easter algorithm (1=Julian, 2=Orthodox, 3=Western).
99
-
100
- Returns:
101
- - Dictionary with success/result/error.
102
  """
103
- try:
104
- easter_date = easter.easter(year, method=method)
105
- return _safe_result(easter_date.isoformat())
106
- except Exception as exc:
107
- return _safe_error(exc)
108
-
109
-
110
- @mcp.tool(name="add_relativedelta", description="Apply relative date/time offsets to a datetime.")
111
- def add_relativedelta(
112
- base_iso: str,
113
- years: int = 0,
114
- months: int = 0,
115
- weeks: int = 0,
116
- days: int = 0,
117
- hours: int = 0,
118
- minutes: int = 0,
119
- seconds: int = 0
120
- ) -> Dict[str, Any]:
121
- """
122
- Apply relativedelta offsets to a base datetime.
123
-
124
- Parameters:
125
- - base_iso: Base datetime in ISO format.
126
- - years, months, weeks, days, hours, minutes, seconds: Relative offsets.
127
 
128
- Returns:
129
- - Dictionary with success/result/error.
130
- """
131
  try:
132
- base_dt = datetime.fromisoformat(base_iso)
133
- delta = relativedelta(
134
- years=years,
135
- months=months,
136
- weeks=weeks,
137
- days=days,
138
- hours=hours,
139
- minutes=minutes,
140
- seconds=seconds,
 
 
 
 
 
 
141
  )
142
- out = base_dt + delta
143
- return _safe_result(out.isoformat())
144
  except Exception as exc:
145
- return _safe_error(exc)
146
 
147
 
148
- @mcp.tool(name="generate_rrule", description="Generate recurrence instances from rrule parameters.")
149
- def generate_rrule(
150
- dtstart_iso: str,
151
- freq: str,
152
- count: int = 10,
153
- interval: int = 1
154
- ) -> Dict[str, Any]:
155
  """
156
- Generate recurrence datetimes using dateutil.rrule.rrule.
157
 
158
  Parameters:
159
- - dtstart_iso: Start datetime in ISO format.
160
- - freq: Frequency name: YEARLY, MONTHLY, WEEKLY, DAILY, HOURLY, MINUTELY, SECONDLY.
161
- - count: Number of occurrences to produce.
162
- - interval: Frequency interval.
163
-
164
- Returns:
165
- - Dictionary with success/result/error.
166
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
167
  try:
168
- freq_key = freq.upper().strip()
169
- if freq_key not in _FREQ_MAP:
170
- raise ValueError("Invalid freq value.")
171
- dtstart = datetime.fromisoformat(dtstart_iso)
172
- rule = rrule(_FREQ_MAP[freq_key], dtstart=dtstart, count=count, interval=interval)
173
- items = [item.isoformat() for item in rule]
174
- return _safe_result(items)
 
 
 
 
 
175
  except Exception as exc:
176
- return _safe_error(exc)
177
 
178
 
179
- @mcp.tool(name="expand_rrule_string", description="Expand an iCalendar RRULE string into occurrences.")
180
- def expand_rrule_string(
181
- rule_text: str,
182
- dtstart_iso: Optional[str] = None,
183
- limit: int = 20
184
- ) -> Dict[str, Any]:
185
  """
186
- Expand occurrences from an RRULE string using dateutil.rrule.rrulestr.
187
 
188
  Parameters:
189
- - rule_text: RRULE text (e.g., 'FREQ=DAILY;COUNT=5').
190
- - dtstart_iso: Optional ISO datetime start.
191
- - limit: Maximum number of occurrences returned.
192
-
193
- Returns:
194
- - Dictionary with success/result/error.
195
  """
 
 
 
196
  try:
197
- dtstart = datetime.fromisoformat(dtstart_iso) if dtstart_iso else None
198
- rule = rrulestr(rule_text, dtstart=dtstart)
199
- items: List[str] = []
200
- for i, item in enumerate(rule):
201
- if i >= limit:
202
- break
203
- items.append(item.isoformat())
204
- return _safe_result(items)
 
 
 
 
 
 
 
205
  except Exception as exc:
206
- return _safe_error(exc)
207
 
208
 
209
- @mcp.tool(name="convert_timezone", description="Convert datetime from one timezone to another.")
210
- def convert_timezone(
211
- dt_iso: str,
212
- from_tz_name: str,
213
- to_tz_name: str
214
- ) -> Dict[str, Any]:
215
  """
216
- Convert datetime between timezones using dateutil.tz.gettz.
217
 
218
  Parameters:
219
- - dt_iso: Input datetime in ISO format.
220
- - from_tz_name: Source timezone name (e.g., 'UTC', 'America/New_York').
221
- - to_tz_name: Target timezone name.
222
-
223
- Returns:
224
- - Dictionary with success/result/error.
225
  """
 
 
 
226
  try:
227
- dt = datetime.fromisoformat(dt_iso)
228
- from_tz = UTC if from_tz_name.upper() == "UTC" else gettz(from_tz_name)
229
- to_tz = UTC if to_tz_name.upper() == "UTC" else gettz(to_tz_name)
230
- if from_tz is None or to_tz is None:
231
- raise ValueError("Invalid timezone name.")
232
- if dt.tzinfo is None:
233
- dt = dt.replace(tzinfo=from_tz)
234
- else:
235
- dt = dt.astimezone(from_tz)
236
- converted = dt.astimezone(to_tz)
237
- return _safe_result(converted.isoformat())
238
  except Exception as exc:
239
- return _safe_error(exc)
 
 
 
 
 
 
 
 
240
 
241
 
242
  def create_app() -> FastMCP:
@@ -244,4 +280,4 @@ def create_app() -> FastMCP:
244
 
245
 
246
  if __name__ == "__main__":
247
- mcp.run()
 
1
+ from __future__ import annotations
 
 
 
2
 
3
+ import datetime as dt
4
+ import sys
5
+ from pathlib import Path
6
+ from typing import Any
 
 
7
 
8
  from fastmcp import FastMCP
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
 
10
+ CURRENT_DIR = Path(__file__).resolve().parent
11
+ SOURCE_DIR = (CURRENT_DIR / ".." / ".." / "source").resolve()
12
+ if str(SOURCE_DIR) not in sys.path:
13
+ sys.path.insert(0, str(SOURCE_DIR))
14
+
15
+ try:
16
+ import dateutil # type: ignore
17
+ except Exception:
18
+ dateutil = None
19
+
20
+ try:
21
+ from dateutil.parser import parse as dateutil_parse # type: ignore
22
+ except Exception:
23
+ dateutil_parse = None
24
+
25
+ try:
26
+ from dateutil.parser import isoparse as dateutil_isoparse # type: ignore
27
+ except Exception:
28
+ dateutil_isoparse = None
29
+
30
+ try:
31
+ from dateutil.relativedelta import relativedelta # type: ignore
32
+ except Exception:
33
+ relativedelta = None
34
+
35
+ try:
36
+ from dateutil.rrule import DAILY, MONTHLY, WEEKLY, YEARLY, rrule # type: ignore
37
+ except Exception:
38
+ DAILY = None
39
+ MONTHLY = None
40
+ WEEKLY = None
41
+ YEARLY = None
42
+ rrule = None
43
+
44
+ try:
45
+ from dateutil.tz import gettz # type: ignore
46
+ except Exception:
47
+ gettz = None
48
+
49
+ try:
50
+ from dateutil.easter import easter # type: ignore
51
+ except Exception:
52
+ easter = None
53
+
54
+ try:
55
+ from adapter import Adapter
56
+ except Exception:
57
+ Adapter = None
58
+
59
+ mcp = FastMCP("dateutil-mcp-service")
60
+ adapter = Adapter() if Adapter is not None else None
61
+
62
+
63
+ def _ok(result: Any) -> dict[str, Any]:
64
+ return {"success": True, "result": result, "error": None}
65
+
66
+
67
+ def _err(message: str) -> dict[str, Any]:
68
+ return {"success": False, "result": None, "error": message}
69
+
70
+
71
+ def _parse_datetime_value(value: str) -> dt.datetime:
72
+ if dateutil_parse is None:
73
+ raise RuntimeError("dateutil.parser.parse is unavailable")
74
+ return dateutil_parse(value)
75
+
76
+
77
+ @mcp.tool(name="health_check", description="Report availability of dateutil and MCP dependencies")
78
+ def health_check() -> dict[str, Any]:
79
+ """Return dependency and adapter health state."""
80
+ deps = {
81
+ "dateutil": dateutil is not None,
82
+ "dateutil_parse": dateutil_parse is not None,
83
+ "dateutil_isoparse": dateutil_isoparse is not None,
84
+ "relativedelta": relativedelta is not None,
85
+ "rrule": rrule is not None,
86
+ "gettz": gettz is not None,
87
+ "easter": easter is not None,
88
+ "adapter": adapter is not None,
89
+ }
90
+
91
+ result = {
92
+ "dependencies": deps,
93
+ "adapter": adapter.health() if adapter is not None else {"status": "fallback", "mode": "blackbox"},
94
+ }
95
+ return _ok(result)
96
+
97
+
98
+ @mcp.tool(name="parse_datetime", description="Parse free-form datetime text with dateutil.parser")
99
+ def parse_datetime(value: str, dayfirst: bool = False, yearfirst: bool = False) -> dict[str, Any]:
100
  """
101
+ Parse a free-form date/time string.
102
 
103
  Parameters:
104
+ - value: Input datetime string.
105
+ - dayfirst: Interpret ambiguous dates as day-first.
106
+ - yearfirst: Interpret ambiguous dates as year-first.
 
 
 
 
107
  """
108
+ if dateutil_parse is None:
109
+ return _err("dateutil.parser.parse is unavailable")
110
+
111
  try:
112
+ parsed = dateutil_parse(value, dayfirst=dayfirst, yearfirst=yearfirst)
113
+ return _ok({"input": value, "iso": parsed.isoformat()})
 
 
 
114
  except Exception as exc:
115
+ return _err(str(exc))
116
 
117
 
118
+ @mcp.tool(name="parse_iso_datetime", description="Parse ISO-8601 datetime string")
119
+ def parse_iso_datetime(value: str) -> dict[str, Any]:
120
  """
121
+ Parse an ISO-8601 datetime string.
122
 
123
  Parameters:
124
+ - value: ISO-8601 datetime text.
 
 
 
125
  """
126
+ if dateutil_isoparse is None:
127
+ return _err("dateutil.parser.isoparse is unavailable")
128
+
129
  try:
130
+ parsed = dateutil_isoparse(value)
131
+ return _ok({"input": value, "iso": parsed.isoformat()})
132
  except Exception as exc:
133
+ return _err(str(exc))
134
 
135
 
136
+ @mcp.tool(name="compute_relativedelta", description="Compute calendar-aware delta between two datetimes")
137
+ def compute_relativedelta(start: str, end: str) -> dict[str, Any]:
138
  """
139
+ Compute relativedelta between two datetime strings.
140
 
141
  Parameters:
142
+ - start: Start datetime string.
143
+ - end: End datetime string.
 
 
 
144
  """
145
+ if relativedelta is None:
146
+ return _err("dateutil.relativedelta.relativedelta is unavailable")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
 
 
 
 
148
  try:
149
+ start_dt = _parse_datetime_value(start)
150
+ end_dt = _parse_datetime_value(end)
151
+ delta = relativedelta(end_dt, start_dt)
152
+ return _ok(
153
+ {
154
+ "start": start_dt.isoformat(),
155
+ "end": end_dt.isoformat(),
156
+ "years": delta.years,
157
+ "months": delta.months,
158
+ "days": delta.days,
159
+ "hours": delta.hours,
160
+ "minutes": delta.minutes,
161
+ "seconds": delta.seconds,
162
+ "microseconds": delta.microseconds,
163
+ }
164
  )
 
 
165
  except Exception as exc:
166
+ return _err(str(exc))
167
 
168
 
169
+ @mcp.tool(name="generate_rrule_occurrences", description="Generate recurring datetimes from an rrule")
170
+ def generate_rrule_occurrences(
171
+ frequency: str,
172
+ dtstart: str,
173
+ count: int = 5,
174
+ interval: int = 1,
175
+ ) -> dict[str, Any]:
176
  """
177
+ Generate recurring occurrences.
178
 
179
  Parameters:
180
+ - frequency: One of YEARLY, MONTHLY, WEEKLY, DAILY.
181
+ - dtstart: Start datetime string.
182
+ - count: Number of occurrences to generate.
183
+ - interval: Interval for recurrence frequency.
 
 
 
184
  """
185
+ if rrule is None:
186
+ return _err("dateutil.rrule.rrule is unavailable")
187
+
188
+ freq_lookup = {
189
+ "YEARLY": YEARLY,
190
+ "MONTHLY": MONTHLY,
191
+ "WEEKLY": WEEKLY,
192
+ "DAILY": DAILY,
193
+ }
194
+
195
+ normalized = frequency.upper().strip()
196
+ if normalized not in freq_lookup or freq_lookup[normalized] is None:
197
+ return _err("Invalid frequency. Use YEARLY, MONTHLY, WEEKLY, or DAILY")
198
+
199
+ if count < 1:
200
+ return _err("count must be >= 1")
201
+ if interval < 1:
202
+ return _err("interval must be >= 1")
203
+
204
  try:
205
+ start_dt = _parse_datetime_value(dtstart)
206
+ rule = rrule(freq_lookup[normalized], dtstart=start_dt, count=count, interval=interval)
207
+ occurrences = [item.isoformat() for item in rule]
208
+ return _ok(
209
+ {
210
+ "frequency": normalized,
211
+ "dtstart": start_dt.isoformat(),
212
+ "count": count,
213
+ "interval": interval,
214
+ "occurrences": occurrences,
215
+ }
216
+ )
217
  except Exception as exc:
218
+ return _err(str(exc))
219
 
220
 
221
+ @mcp.tool(name="resolve_timezone", description="Resolve timezone by IANA name using dateutil.tz.gettz")
222
+ def resolve_timezone(tz_name: str) -> dict[str, Any]:
 
 
 
 
223
  """
224
+ Resolve timezone information.
225
 
226
  Parameters:
227
+ - tz_name: IANA timezone name, e.g. 'Asia/Shanghai'.
 
 
 
 
 
228
  """
229
+ if gettz is None:
230
+ return _err("dateutil.tz.gettz is unavailable")
231
+
232
  try:
233
+ tzinfo = gettz(tz_name)
234
+ if tzinfo is None:
235
+ return _err(f"Timezone not found: {tz_name}")
236
+
237
+ now_utc = dt.datetime.now(dt.timezone.utc)
238
+ local_time = now_utc.astimezone(tzinfo)
239
+ offset = local_time.utcoffset()
240
+ return _ok(
241
+ {
242
+ "timezone": tz_name,
243
+ "resolved": True,
244
+ "current_time": local_time.isoformat(),
245
+ "utc_offset_seconds": int(offset.total_seconds()) if offset is not None else None,
246
+ }
247
+ )
248
  except Exception as exc:
249
+ return _err(str(exc))
250
 
251
 
252
+ @mcp.tool(name="calculate_easter", description="Calculate Easter Sunday date for a given year")
253
+ def calculate_easter(year: int) -> dict[str, Any]:
 
 
 
 
254
  """
255
+ Calculate Easter Sunday date.
256
 
257
  Parameters:
258
+ - year: Gregorian year.
 
 
 
 
 
259
  """
260
+ if easter is None:
261
+ return _err("dateutil.easter.easter is unavailable")
262
+
263
  try:
264
+ result_date = easter(year)
265
+ return _ok({"year": year, "easter": result_date.isoformat()})
 
 
 
 
 
 
 
 
 
266
  except Exception as exc:
267
+ return _err(str(exc))
268
+
269
+
270
+ @mcp.tool(name="list_loaded_modules", description="List adapter module loading status")
271
+ def list_loaded_modules() -> dict[str, Any]:
272
+ """List loaded/failed modules reported by adapter."""
273
+ if adapter is None:
274
+ return _err("Adapter unavailable")
275
+ return _ok(adapter.list_modules())
276
 
277
 
278
  def create_app() -> FastMCP:
 
280
 
281
 
282
  if __name__ == "__main__":
283
+ mcp.run()
dateutil/mcp_output/requirements.txt CHANGED
@@ -1,8 +1,3 @@
1
  fastmcp
2
- fastapi
3
- uvicorn[standard]
4
- pydantic>=2.0.0
5
- six >= 1.6
6
- package_dir=
7
- =src
8
- packages = find:
 
1
  fastmcp
2
+ python-dateutil
3
+ six
 
 
 
 
 
dateutil/mcp_output/start_mcp.py CHANGED
@@ -1,30 +1,33 @@
 
1
 
2
- """
3
- MCP Service Startup Entry
4
- """
5
- import sys
6
  import os
 
 
7
 
8
- project_root = os.path.dirname(os.path.abspath(__file__))
9
- mcp_plugin_dir = os.path.join(project_root, "mcp_plugin")
10
- if mcp_plugin_dir not in sys.path:
11
- sys.path.insert(0, mcp_plugin_dir)
12
 
13
  from mcp_service import create_app
14
 
15
- def main():
16
- """Start FastMCP service"""
 
 
 
17
  app = create_app()
18
- # Use environment variable to configure port, default 8000
19
- port = int(os.environ.get("MCP_PORT", "8000"))
20
-
21
- # Choose transport mode based on environment variable
22
- transport = os.environ.get("MCP_TRANSPORT", "stdio")
23
  if transport == "http":
24
  app.run(transport="http", host="0.0.0.0", port=port)
25
- else:
26
- # Default to STDIO mode
27
- app.run()
 
28
 
29
  if __name__ == "__main__":
30
  main()
 
1
+ from __future__ import annotations
2
 
 
 
 
 
3
  import os
4
+ import sys
5
+ from pathlib import Path
6
 
7
+ CURRENT_DIR = Path(__file__).resolve().parent
8
+ PLUGIN_DIR = (CURRENT_DIR / "mcp_plugin").resolve()
9
+ if str(PLUGIN_DIR) not in sys.path:
10
+ sys.path.insert(0, str(PLUGIN_DIR))
11
 
12
  from mcp_service import create_app
13
 
14
+
15
+ def main() -> None:
16
+ transport = os.getenv("MCP_TRANSPORT", "stdio").strip().lower()
17
+ port = int(os.getenv("MCP_PORT", "8000"))
18
+
19
  app = create_app()
20
+
21
+ if transport == "stdio":
22
+ app.run(transport="stdio")
23
+ return
24
+
25
  if transport == "http":
26
  app.run(transport="http", host="0.0.0.0", port=port)
27
+ return
28
+
29
+ raise ValueError("Unsupported MCP_TRANSPORT. Use 'stdio' or 'http'.")
30
+
31
 
32
  if __name__ == "__main__":
33
  main()
port.json CHANGED
@@ -1,5 +1 @@
1
- {
2
- "repo": "dateutil",
3
- "port": 7951,
4
- "timestamp": 1773283013
5
- }
 
1
+ {"port": 7860}
 
 
 
 
requirements.txt CHANGED
@@ -1,6 +1,5 @@
1
  fastmcp
 
 
2
  fastapi
3
- uvicorn[standard]
4
- pydantic>=2.0.0
5
- six >= 1.6
6
- dateutil
 
1
  fastmcp
2
+ python-dateutil
3
+ six
4
  fastapi
5
+ uvicorn
 
 
 
run_docker.ps1 CHANGED
@@ -1,26 +1,8 @@
1
- cd $PSScriptRoot
2
  $ErrorActionPreference = "Stop"
3
- $entryName = if ($env:MCP_ENTRY_NAME) { $env:MCP_ENTRY_NAME } else { "dateutil" }
4
- $entryUrl = if ($env:MCP_ENTRY_URL) { $env:MCP_ENTRY_URL } else { "http://localhost:7951/mcp" }
5
- $imageName = if ($env:MCP_IMAGE_NAME) { $env:MCP_IMAGE_NAME } else { "dateutil-mcp" }
6
- $mcpDir = Join-Path $env:USERPROFILE ".cursor"
7
- $mcpPath = Join-Path $mcpDir "mcp.json"
8
- if (!(Test-Path $mcpDir)) { New-Item -ItemType Directory -Path $mcpDir | Out-Null }
9
- $config = @{}
10
- if (Test-Path $mcpPath) {
11
- try { $config = Get-Content $mcpPath -Raw | ConvertFrom-Json } catch { $config = @{} }
12
- }
13
- $serversOrdered = [ordered]@{}
14
- if ($config -and ($config.PSObject.Properties.Name -contains "mcpServers") -and $config.mcpServers) {
15
- $existing = $config.mcpServers
16
- if ($existing -is [pscustomobject]) {
17
- foreach ($p in $existing.PSObject.Properties) { if ($p.Name -ne $entryName) { $serversOrdered[$p.Name] = $p.Value } }
18
- } elseif ($existing -is [System.Collections.IDictionary]) {
19
- foreach ($k in $existing.Keys) { if ($k -ne $entryName) { $serversOrdered[$k] = $existing[$k] } }
20
- }
21
- }
22
- $serversOrdered[$entryName] = @{ url = $entryUrl }
23
- $config = @{ mcpServers = $serversOrdered }
24
- $config | ConvertTo-Json -Depth 10 | Set-Content -Path $mcpPath -Encoding UTF8
25
  docker build -t $imageName .
26
- docker run --rm -p 7951:7860 $imageName
 
 
1
  $ErrorActionPreference = "Stop"
2
+
3
+ $portConfig = Get-Content -Raw -Path "port.json" | ConvertFrom-Json
4
+ $port = $portConfig.port
5
+ $imageName = "dateutil-mcp"
6
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  docker build -t $imageName .
8
+ docker run --rm -p "${port}:${port}" -e MCP_TRANSPORT=http -e MCP_PORT=$port $imageName
run_docker.sh CHANGED
@@ -1,75 +1,8 @@
1
  #!/usr/bin/env bash
2
  set -euo pipefail
3
- cd "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
4
- mcp_entry_name="${MCP_ENTRY_NAME:-dateutil}"
5
- mcp_entry_url="${MCP_ENTRY_URL:-http://localhost:7951/mcp}"
6
- mcp_dir="${HOME}/.cursor"
7
- mcp_path="${mcp_dir}/mcp.json"
8
- mkdir -p "${mcp_dir}"
9
- if command -v python3 >/dev/null 2>&1; then
10
- python3 - "${mcp_path}" "${mcp_entry_name}" "${mcp_entry_url}" <<'PY'
11
- import json, os, sys
12
- path, name, url = sys.argv[1:4]
13
- cfg = {"mcpServers": {}}
14
- if os.path.exists(path):
15
- try:
16
- with open(path, "r", encoding="utf-8") as f:
17
- cfg = json.load(f)
18
- except Exception:
19
- cfg = {"mcpServers": {}}
20
- if not isinstance(cfg, dict):
21
- cfg = {"mcpServers": {}}
22
- servers = cfg.get("mcpServers")
23
- if not isinstance(servers, dict):
24
- servers = {}
25
- ordered = {}
26
- for k, v in servers.items():
27
- if k != name:
28
- ordered[k] = v
29
- ordered[name] = {"url": url}
30
- cfg = {"mcpServers": ordered}
31
- with open(path, "w", encoding="utf-8") as f:
32
- json.dump(cfg, f, indent=2, ensure_ascii=False)
33
- PY
34
- elif command -v python >/dev/null 2>&1; then
35
- python - "${mcp_path}" "${mcp_entry_name}" "${mcp_entry_url}" <<'PY'
36
- import json, os, sys
37
- path, name, url = sys.argv[1:4]
38
- cfg = {"mcpServers": {}}
39
- if os.path.exists(path):
40
- try:
41
- with open(path, "r", encoding="utf-8") as f:
42
- cfg = json.load(f)
43
- except Exception:
44
- cfg = {"mcpServers": {}}
45
- if not isinstance(cfg, dict):
46
- cfg = {"mcpServers": {}}
47
- servers = cfg.get("mcpServers")
48
- if not isinstance(servers, dict):
49
- servers = {}
50
- ordered = {}
51
- for k, v in servers.items():
52
- if k != name:
53
- ordered[k] = v
54
- ordered[name] = {"url": url}
55
- cfg = {"mcpServers": ordered}
56
- with open(path, "w", encoding="utf-8") as f:
57
- json.dump(cfg, f, indent=2, ensure_ascii=False)
58
- PY
59
- elif command -v jq >/dev/null 2>&1; then
60
- name="${mcp_entry_name}"; url="${mcp_entry_url}"
61
- if [ -f "${mcp_path}" ]; then
62
- tmp="$(mktemp)"
63
- jq --arg name "$name" --arg url "$url" '
64
- .mcpServers = (.mcpServers // {})
65
- | .mcpServers as $s
66
- | ($s | with_entries(select(.key != $name))) as $base
67
- | .mcpServers = ($base + {($name): {"url": $url}})
68
- ' "${mcp_path}" > "${tmp}" && mv "${tmp}" "${mcp_path}"
69
- else
70
- printf '{ "mcpServers": { "%s": { "url": "%s" } } }
71
- ' "$name" "$url" > "${mcp_path}"
72
- fi
73
- fi
74
- docker build -t dateutil-mcp .
75
- docker run --rm -p 7951:7860 dateutil-mcp
 
1
  #!/usr/bin/env bash
2
  set -euo pipefail
3
+
4
+ PORT=$(python3 -c "import json; print(json.load(open('port.json'))['port'])")
5
+ IMAGE_NAME="dateutil-mcp"
6
+
7
+ docker build -t "${IMAGE_NAME}" .
8
+ docker run --rm -p "${PORT}:${PORT}" -e MCP_TRANSPORT=http -e MCP_PORT="${PORT}" "${IMAGE_NAME}"