sequenx-mcp 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,6 @@
1
+ """MCP server exposing the SequenxAI public API as tools for AI hosts."""
2
+
3
+ from ._version import __version__
4
+ from .server import build_server, main
5
+
6
+ __all__ = ["__version__", "build_server", "main"]
@@ -0,0 +1,6 @@
1
+ """``python -m sequenx_mcp``: run the MCP server over stdio."""
2
+
3
+ from .server import main
4
+
5
+ if __name__ == "__main__":
6
+ raise SystemExit(main())
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
sequenx_mcp/errors.py ADDED
@@ -0,0 +1,71 @@
1
+ """Map the SDK's typed errors onto the contract's error shape.
2
+
3
+ The contract answers with ``ErrorResponse`` = ``{"error": {code, message,
4
+ request_id, details?}}`` and does not use RFC 7807 (see
5
+ ``docs/plans/phase_3/06-MCP服务器与宿主Agent工具面.md`` §3.2). A tool therefore
6
+ reports the API's own error body as the tool error message, so the host agent
7
+ sees a structured error instead of a silent empty answer. The MCP framework
8
+ prefixes the message with its own text ("Error executing tool …"), so the
9
+ envelope travels inside the message rather than as the whole message.
10
+
11
+ The body keeps the contract's ``ErrorDetail`` fields — ``code``, ``message``,
12
+ ``request_id``, ``category``, ``retryable`` and ``details`` — and adds the
13
+ retry hints the RFC asks the host to see (``retry_after`` and ``limit_type``)
14
+ inside ``details``. ``category`` and ``retryable`` are only present when the
15
+ upstream body carried them.
16
+
17
+ Three codes are the SDK's own, because no contract code covers them: the
18
+ contract's list (``x-error-codes`` in openapi.yaml) describes server-side
19
+ failures only. ``HTTP_<status>`` is a response without a code, ``REQUEST_FAILED``
20
+ a response the SDK could not read, ``TRANSPORT_ERROR`` a request that never
21
+ reached the server.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import json
27
+ from typing import Any
28
+
29
+ from mcp.server.mcpserver.exceptions import ToolError
30
+ from sequenx import RateLimitedError, SequenxError, TransportError
31
+
32
+
33
+ def error_body(exc: SequenxError) -> dict[str, Any]:
34
+ """The contract's ErrorDetail body for an SDK error."""
35
+ details: dict[str, Any] = {}
36
+ if isinstance(exc.details, dict):
37
+ details.update(exc.details)
38
+ elif exc.details is not None:
39
+ details["details"] = exc.details
40
+ if isinstance(exc, RateLimitedError):
41
+ if exc.retry_after is not None:
42
+ details["retry_after"] = exc.retry_after
43
+ if exc.limit_type is not None:
44
+ details["limit_type"] = exc.limit_type
45
+ if exc.code:
46
+ code = exc.code
47
+ elif exc.status is not None:
48
+ code = f"HTTP_{exc.status}"
49
+ elif isinstance(exc, TransportError):
50
+ # No response reached us at all.
51
+ code = "TRANSPORT_ERROR"
52
+ else:
53
+ # A client-side failure (e.g. a response without a JSON body).
54
+ code = "REQUEST_FAILED"
55
+ body: dict[str, Any] = {
56
+ "code": code,
57
+ "message": str(exc),
58
+ "request_id": exc.request_id,
59
+ }
60
+ if exc.category is not None:
61
+ body["category"] = exc.category
62
+ if exc.retryable is not None:
63
+ body["retryable"] = exc.retryable
64
+ if details:
65
+ body["details"] = details
66
+ return {"error": body}
67
+
68
+
69
+ def tool_error(exc: SequenxError) -> ToolError:
70
+ """A tool error carrying the contract's error body as its message."""
71
+ return ToolError(json.dumps(error_body(exc), ensure_ascii=False))
sequenx_mcp/server.py ADDED
@@ -0,0 +1,115 @@
1
+ """The MCP server: three tools over the SequenxAI public API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import os
7
+ import sys
8
+ from typing import Annotated, Any
9
+
10
+ from mcp.server.mcpserver import MCPServer
11
+ from pydantic import Field
12
+ from sequenx import SequenxClient
13
+
14
+ from . import tools
15
+ from ._version import __version__
16
+ from .tools import Point
17
+
18
+ INSTRUCTIONS = (
19
+ "Time-series forecasting on the SequenxAI platform. The data is passed inline as "
20
+ "long-table rows (unique_id, ds, y). Start with list_models to learn the models and "
21
+ "their limits, clean_data to preview how the data will be cleaned (free), and "
22
+ "forecast to get the prediction and the usage that was billed."
23
+ )
24
+
25
+ LIST_MODELS_DESCRIPTION = (
26
+ "List the public time-series models available to this API key: each model's id, the "
27
+ "default model, its context and horizon limits and its capabilities. Call it before "
28
+ "forecasting to learn the limits; the catalog is owned by the server and changes "
29
+ "without a version bump."
30
+ )
31
+
32
+ CLEAN_DATA_DESCRIPTION = (
33
+ "Run the free cleaning dry run: the same cleaning stage as a forecast, returning the "
34
+ "cleaning report (with every automatic decision) and the effective policy. No "
35
+ "inference, no billing. Input is inline rows (unique_id, ds, y); freq, model, "
36
+ "clean_series and options are the contract's CleanRequest fields."
37
+ )
38
+
39
+ FORECAST_DESCRIPTION = (
40
+ "Forecast one or more series from inline rows (unique_id, ds, y): point forecast plus "
41
+ "intervals, the data-quality report and the billed usage. The tool takes the request "
42
+ "fields h, model, freq, level and options; the remaining contract fields (quantile "
43
+ "policy, covariates, joint groups, ...) are available through REST or the SDK. Two "
44
+ "limits to know: the model reads only the most recent points of a series, so a longer "
45
+ "history is truncated by the API and reported (model_input[].truncated plus a "
46
+ "context_truncated warning) — list_models gives each model's context window; and a "
47
+ "host tool call cannot carry a large table, so for larger data use the REST or SDK "
48
+ "batch path: POST /v1/forecast/batch, then poll GET /v1/forecast/batch/{job_id}."
49
+ )
50
+
51
+ # The contract's horizon bounds (ForecastRequest.h: 1..2048).
52
+ Horizon = Annotated[int, Field(ge=1, le=2048)]
53
+
54
+
55
+ def build_server(client: SequenxClient) -> MCPServer:
56
+ """The MCP server with the three tools, bound to one API client."""
57
+ server = MCPServer(name="sequenx", version=__version__, instructions=INSTRUCTIONS)
58
+
59
+ @server.tool(name="list_models", description=LIST_MODELS_DESCRIPTION)
60
+ def list_models_tool() -> dict[str, Any]:
61
+ return tools.list_models(client)
62
+
63
+ @server.tool(name="clean_data", description=CLEAN_DATA_DESCRIPTION)
64
+ def clean_data_tool(
65
+ series: list[Point],
66
+ options: dict[str, Any] | None = None,
67
+ freq: str | None = None,
68
+ model: str | None = None,
69
+ clean_series: bool | None = None,
70
+ ) -> dict[str, Any]:
71
+ return tools.clean_data(
72
+ client, series, options=options, freq=freq, model=model, clean_series=clean_series
73
+ )
74
+
75
+ @server.tool(name="forecast", description=FORECAST_DESCRIPTION)
76
+ def forecast_tool(
77
+ series: list[Point],
78
+ h: Horizon,
79
+ model: str | None = None,
80
+ freq: str | None = None,
81
+ level: list[int] | None = None,
82
+ options: dict[str, Any] | None = None,
83
+ ) -> dict[str, Any]:
84
+ return tools.forecast(
85
+ client, series, h=h, model=model, freq=freq, level=level, options=options
86
+ )
87
+
88
+ return server
89
+
90
+
91
+ def _quiet_httpx() -> None:
92
+ """The MCP SDK configures the root logger, so httpx's per-request INFO lines would
93
+ reach the host's stderr on every tool call. The protocol lives on stdout; these lines
94
+ are noise there."""
95
+ logging.getLogger("httpx").setLevel(logging.WARNING)
96
+ logging.getLogger("httpcore").setLevel(logging.WARNING)
97
+
98
+
99
+ def main() -> int:
100
+ """Run the server over stdio (the transport a local MCP host uses)."""
101
+ api_key = os.environ.get("SEQUENX_API_KEY")
102
+ if not api_key:
103
+ print(
104
+ "set SEQUENX_API_KEY=<sqx_live_... key> to run the SequenxAI MCP server",
105
+ file=sys.stderr,
106
+ )
107
+ return 2
108
+ base_url = os.environ.get("SEQUENX_API_BASE_URL") or SequenxClient.DEFAULT_BASE_URL
109
+ _quiet_httpx()
110
+ client = SequenxClient(api_key, base_url=base_url)
111
+ try:
112
+ build_server(client).run()
113
+ finally:
114
+ client.close()
115
+ return 0
sequenx_mcp/tools.py ADDED
@@ -0,0 +1,97 @@
1
+ """The three tools: thin adapters over the public API.
2
+
3
+ Each tool maps one-to-one onto an existing operation (RFC
4
+ ``docs/plans/phase_3/06-MCP服务器与宿主Agent工具面.md`` §2) and calls the Python
5
+ SDK — there is no second HTTP client and no business logic here. The server
6
+ adds no limits of its own: the control plane owns the point caps and the model
7
+ context, and it reports what it did (``model_input[].truncated`` plus a
8
+ ``context_truncated`` warning).
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from typing import Any
14
+
15
+ from pydantic import BaseModel, ConfigDict
16
+ from sequenx import SequenxClient, SequenxError
17
+
18
+ from .errors import tool_error
19
+
20
+ # tool name -> (the SDK method it calls, the contract path, the contract method)
21
+ TOOL_OPERATIONS = {
22
+ "list_models": ("models", "/models", "get"),
23
+ "clean_data": ("clean", "/clean", "post"),
24
+ "forecast": ("forecast", "/forecast", "post"),
25
+ }
26
+
27
+
28
+ class Point(BaseModel):
29
+ """One row of the contract's long table (contract: TimeSeriesPoint)."""
30
+
31
+ model_config = ConfigDict(extra="allow")
32
+
33
+ unique_id: str
34
+ ds: str
35
+ y: float | None
36
+
37
+
38
+ def rows_of(series: list[Point]) -> list[dict[str, Any]]:
39
+ """The rows as the API takes them (the contract's long table)."""
40
+ return [point.model_dump() for point in series]
41
+
42
+
43
+ def list_models(client: SequenxClient) -> dict[str, Any]:
44
+ """GET /models: the public model catalog for this key."""
45
+ try:
46
+ return dict(client.models())
47
+ except SequenxError as exc:
48
+ raise tool_error(exc) from exc
49
+
50
+
51
+ def clean_data(
52
+ client: SequenxClient,
53
+ series: list[Point],
54
+ options: dict[str, Any] | None = None,
55
+ freq: str | None = None,
56
+ model: str | None = None,
57
+ clean_series: bool | None = None,
58
+ ) -> dict[str, Any]:
59
+ """POST /clean: the free cleaning dry-run (report + effective policy)."""
60
+ extra: dict[str, Any] = {}
61
+ if options is not None:
62
+ extra["options"] = options
63
+ if freq is not None:
64
+ extra["freq"] = freq
65
+ if model is not None:
66
+ extra["model"] = model
67
+ if clean_series is not None:
68
+ extra["clean_series"] = clean_series
69
+ try:
70
+ return dict(client.clean(rows_of(series), **extra))
71
+ except SequenxError as exc:
72
+ raise tool_error(exc) from exc
73
+
74
+
75
+ def forecast(
76
+ client: SequenxClient,
77
+ series: list[Point],
78
+ h: int,
79
+ model: str | None = None,
80
+ freq: str | None = None,
81
+ level: list[int] | None = None,
82
+ options: dict[str, Any] | None = None,
83
+ ) -> dict[str, Any]:
84
+ """POST /forecast: a synchronous forecast over inline rows."""
85
+ extra: dict[str, Any] = {}
86
+ if model is not None:
87
+ extra["model"] = model
88
+ if freq is not None:
89
+ extra["freq"] = freq
90
+ if level is not None:
91
+ extra["level"] = level
92
+ if options is not None:
93
+ extra["options"] = options
94
+ try:
95
+ return dict(client.forecast(rows_of(series), h=h, **extra))
96
+ except SequenxError as exc:
97
+ raise tool_error(exc) from exc
@@ -0,0 +1,112 @@
1
+ Metadata-Version: 2.4
2
+ Name: sequenx-mcp
3
+ Version: 0.1.0
4
+ Summary: MCP server exposing the SequenxAI public API as tools for AI hosts
5
+ Author-email: SequenxAI <lcw_1497007939@163.com>
6
+ License-Expression: LicenseRef-Proprietary
7
+ Keywords: mcp,time-series,forecasting,sequenxai
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.10
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
14
+ Requires-Python: >=3.10
15
+ Description-Content-Type: text/markdown
16
+ Requires-Dist: mcp<3,>=2.3
17
+ Requires-Dist: pydantic<3,>=2.7
18
+ Requires-Dist: sequenx-ai>=0.1.0
19
+
20
+ # sequenx-mcp (SequenxAI MCP server)
21
+
22
+ MCP server that exposes the SequenxAI public API as tools for AI hosts (Claude
23
+ Desktop, Cursor, …). It is a thin adapter: every tool maps one-to-one onto an
24
+ existing operation and calls the Python SDK — there is no second HTTP client and
25
+ no business logic here (see
26
+ [`docs/plans/phase_3/06-MCP服务器与宿主Agent工具面.md`](../../docs/plans/phase_3/06-MCP服务器与宿主Agent工具面.md)).
27
+
28
+ Not published on PyPI yet; it lives in this repository.
29
+
30
+ ## Install
31
+
32
+ The server depends on the sibling SDK, which is not on PyPI either — install it
33
+ from this repository first:
34
+
35
+ ```bash
36
+ pip install -e packages/sequenx-py
37
+ pip install -e packages/sequenx-mcp
38
+ ```
39
+
40
+ ## Run
41
+
42
+ The server speaks stdio, the transport a local MCP host starts as a child
43
+ process:
44
+
45
+ ```bash
46
+ SEQUENX_API_KEY=sqx_live_... python -m sequenx_mcp
47
+ ```
48
+
49
+ | Variable | Meaning |
50
+ |---|---|
51
+ | `SEQUENX_API_KEY` | The `sqx_live_*` key the tools call with (required). |
52
+ | `SEQUENX_API_BASE_URL` | Base URL override; default `https://api.sequenxai.com/v1`. |
53
+
54
+ Host configuration (any MCP host that starts stdio servers):
55
+
56
+ ```json
57
+ {
58
+ "mcpServers": {
59
+ "sequenx": {
60
+ "command": "python",
61
+ "args": ["-m", "sequenx_mcp"],
62
+ "env": { "SEQUENX_API_KEY": "sqx_live_..." }
63
+ }
64
+ }
65
+ }
66
+ ```
67
+
68
+ ## Tools
69
+
70
+ | Tool | Endpoint | What it does |
71
+ |---|---|---|
72
+ | `list_models` | `GET /v1/models` | The public model catalog for the key: ids, the default model, per-model context / horizon limits and capabilities. |
73
+ | `clean_data` | `POST /v1/clean` | The free cleaning dry-run: cleaning report and effective policy. No inference, no billing. Takes `series`, `options`, `freq`, `model` and `clean_series`. |
74
+ | `forecast` | `POST /v1/forecast` | Synchronous forecast over inline rows: points and intervals, data-quality report, billed usage. Takes `series`, `h`, `model`, `freq`, `level` and `options`. |
75
+
76
+ Input rows are the contract's long table: `unique_id`, `ds`, `y`.
77
+
78
+ **Limits.** `forecast` takes inline rows only — the public API has no dataset
79
+ upload, so the tool is for small-to-medium series. The model reads only the most
80
+ recent points of a series: a longer history is truncated by the API and
81
+ reported (`model_input[].truncated` plus a `context_truncated` warning), and
82
+ `list_models` gives each model's context window. The server adds no limit of
83
+ its own. For larger data use the REST or SDK batch path:
84
+ `POST /v1/forecast/batch`, then poll `GET /v1/forecast/batch/{job_id}`.
85
+
86
+ **Errors.** A tool error carries the contract's error body
87
+ (`{"error": {code, message, request_id, category, retryable, details?}}`), so
88
+ the host agent sees a structured error instead of a silent empty answer. The MCP
89
+ framework prefixes the message with its own text, so the envelope travels
90
+ *inside* the error text. `403` is not retried; `429` carries `retry_after` and
91
+ `limit_type` (`rpm` / `daily`) in `details`.
92
+
93
+ Three codes are the SDK's own, because the contract's code list covers
94
+ server-side failures only: `HTTP_<status>` (a response without a code),
95
+ `REQUEST_FAILED` (a response body the SDK could not read) and `TRANSPORT_ERROR`
96
+ (the request never reached the server).
97
+
98
+ ## Development
99
+
100
+ ```bash
101
+ pip install -e packages/sequenx-py
102
+ pip install -e packages/sequenx-mcp -r packages/sequenx-mcp/requirements-dev.txt
103
+ cd packages/sequenx-mcp
104
+ python -m pytest -q
105
+ python -m ruff check src tests
106
+ ```
107
+
108
+ Gitee Go runs the suite via `scripts/ci/gitee-verify.sh mcp`.
109
+
110
+ Use a separate virtualenv for this package: `mcp` 2.x installs `httpx2`, and a
111
+ control-plane environment that has `httpx2` next to `httpx` breaks Starlette's
112
+ test client (`services/api` needs `httpx` 1.x).
@@ -0,0 +1,10 @@
1
+ sequenx_mcp/__init__.py,sha256=uDWjETMHAREI7GEutCkQUMeSUKYEcNU3jyKbWHI4FD4,199
2
+ sequenx_mcp/__main__.py,sha256=7PUkI_f04uChGSNpjPu-6l7pdCEkK84spTRzKZagAJY,147
3
+ sequenx_mcp/_version.py,sha256=kUR5RAFc7HCeiqdlX36dZOHkUI5wI6V_43RpEcD8b-0,22
4
+ sequenx_mcp/errors.py,sha256=oCUpvdhAVY1-cL36Zxovp2NS7y6V-MOcpWsyn0_RGmU,2854
5
+ sequenx_mcp/server.py,sha256=EFeJvwsrRwHADbtVH_DhLv56mUxu0TmbNK0JacE6KJY,4548
6
+ sequenx_mcp/tools.py,sha256=B7fWJOMgdDGmUzwZX74VHchT9rIZQNDKlHCzB3fMCfs,2974
7
+ sequenx_mcp-0.1.0.dist-info/METADATA,sha256=60fMaPXf_Q8umMeqQQY6Zq3kqW6xADLvAnD_PQcAqe0,4402
8
+ sequenx_mcp-0.1.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
9
+ sequenx_mcp-0.1.0.dist-info/top_level.txt,sha256=jP7ZO66U7u9jSljIRK7_notOIeo1uH_Y5xHIc0P8QLo,12
10
+ sequenx_mcp-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ sequenx_mcp