probnode-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,3 @@
1
+ """MCP server for ProbNode. Run `probnode-mcp`."""
2
+
3
+ __version__ = "0.1.0"
probnode_mcp/server.py ADDED
@@ -0,0 +1,146 @@
1
+ """ProbNode as MCP tools, for an AI agent working with Polymarket history.
2
+
3
+ {"mcpServers": {"probnode": {"command": "probnode-mcp",
4
+ "env": {"PROBNODE_API_KEY": "pn_..."}}}}
5
+
6
+ Every tool is one ProbNode API call through `probnode.Client`, so an agent sees exactly what a
7
+ customer's script sees: the same entitlements, the same quotas, the same refusals in words. Tools
8
+ return small JSON; bulk data is handed over as signed URLs (`sign_files`) or a capped range query
9
+ (`timeseries`), never streamed through the model's context.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from typing import Any
15
+
16
+ from mcp.server.fastmcp import FastMCP
17
+ from probnode import Client, ProbNodeError
18
+
19
+ #: Rows a `timeseries` call returns to the model at most. The API caps a window at 200,000 rows;
20
+ #: that is a file, not context. More than this, the tool says so and suggests files instead.
21
+ MAX_ROWS_TO_MODEL = 500
22
+
23
+ mcp = FastMCP("probnode")
24
+ _client: Client | None = None
25
+
26
+
27
+ def client() -> Client:
28
+ global _client
29
+ if _client is None:
30
+ _client = Client()
31
+ return _client
32
+
33
+
34
+ def _refusal(exc: ProbNodeError) -> dict[str, Any]:
35
+ return {"error": exc.detail, "status": exc.status}
36
+
37
+
38
+ @mcp.tool()
39
+ def datasets() -> Any:
40
+ """Datasets ProbNode holds (books, trades, gaps, markets, quotes, ohlcv_1m), their formats,
41
+ whether a paid plan is needed (tier "tape") and the first and last UTC hour held."""
42
+ try:
43
+ return client().datasets()
44
+ except ProbNodeError as exc:
45
+ return _refusal(exc)
46
+
47
+
48
+ @mcp.tool()
49
+ def search_markets(query: str, limit: int = 20) -> Any:
50
+ """Find Polymarket markets by words in their slug or title. Returns slugs, series, start and
51
+ end times, resolution and outcome token ids."""
52
+ try:
53
+ return client().markets(q=query, limit=min(max(limit, 1), 100))
54
+ except ProbNodeError as exc:
55
+ return _refusal(exc)
56
+
57
+
58
+ @mcp.tool()
59
+ def coverage(token_id: str, start: str | None = None, end: str | None = None) -> Any:
60
+ """How completely one outcome token was recorded, hour by hour: recorded ms, gaps and uptime.
61
+ Times are ISO UTC. Hours marked implied were recorded whole (no gap touched them)."""
62
+ try:
63
+ return client().coverage(token_id, start, end)
64
+ except ProbNodeError as exc:
65
+ return _refusal(exc)
66
+
67
+
68
+ @mcp.tool()
69
+ def list_files(
70
+ dataset: str,
71
+ format: str,
72
+ start: str,
73
+ end: str,
74
+ granularity: str = "hour",
75
+ market: list[str] | None = None,
76
+ ) -> Any:
77
+ """Files for a dataset and format over [start, end) (ISO UTC, on the hour, at most 31 days):
78
+ object path, rows, bytes and sha256 each, plus totals. granularity "hour" (all markets per
79
+ file) or "market_day" (one market, one UTC day). Needs an API key."""
80
+ try:
81
+ files = client().files(
82
+ dataset, format, start, end, granularity=granularity, market=market or ()
83
+ )
84
+ except ProbNodeError as exc:
85
+ return _refusal(exc)
86
+ return {
87
+ "total_files": len(files),
88
+ "total_bytes": sum(f.bytes for f in files),
89
+ "total_rows": sum(f.rows for f in files),
90
+ "files": [
91
+ {
92
+ "object_path": f.object_path,
93
+ "rows": f.rows,
94
+ "bytes": f.bytes,
95
+ "sha256": f.sha256,
96
+ "market": f.market,
97
+ }
98
+ for f in files[:200]
99
+ ],
100
+ "truncated": len(files) > 200,
101
+ }
102
+
103
+
104
+ @mcp.tool()
105
+ def sign_files(object_paths: list[str]) -> Any:
106
+ """Short-lived download URLs (about 15 minutes) for object paths from list_files. Each counts
107
+ against the account's daily signed-URL limit; tape data needs a paid plan."""
108
+ try:
109
+ return client().sign(object_paths)
110
+ except ProbNodeError as exc:
111
+ return _refusal(exc)
112
+
113
+
114
+ @mcp.tool()
115
+ def timeseries(dataset: str, market: str, start: str, end: str) -> Any:
116
+ """Rows of one dataset (e.g. quotes, trades, ohlcv_1m) for one market over a short window,
117
+ as JSON objects. For more than a few hundred rows use list_files + sign_files instead."""
118
+ try:
119
+ rows = client().timeseries(dataset, market, start, end, format="ndjson")
120
+ except ProbNodeError as exc:
121
+ return _refusal(exc)
122
+ if len(rows) > MAX_ROWS_TO_MODEL:
123
+ return {
124
+ "rows": rows[:MAX_ROWS_TO_MODEL],
125
+ "returned": MAX_ROWS_TO_MODEL,
126
+ "total": len(rows),
127
+ "note": "more rows than fit here; narrow the window or download files",
128
+ }
129
+ return {"rows": rows, "returned": len(rows), "total": len(rows)}
130
+
131
+
132
+ @mcp.tool()
133
+ def samples() -> Any:
134
+ """Free sample files - one market-day per dataset and format - with URLs that need no key."""
135
+ try:
136
+ return client().samples()
137
+ except ProbNodeError as exc:
138
+ return _refusal(exc)
139
+
140
+
141
+ def main() -> None:
142
+ mcp.run()
143
+
144
+
145
+ if __name__ == "__main__":
146
+ main()
@@ -0,0 +1,31 @@
1
+ Metadata-Version: 2.5
2
+ Name: probnode-mcp
3
+ Version: 0.1.0
4
+ Summary: MCP server for ProbNode: Polymarket order books, trades and gaps for AI agents
5
+ License: MIT
6
+ Requires-Python: >=3.10
7
+ Requires-Dist: mcp<2,>=1.2
8
+ Requires-Dist: probnode-sdk>=0.1.0
9
+ Provides-Extra: dev
10
+ Requires-Dist: pytest>=8.3; extra == 'dev'
11
+ Requires-Dist: ruff>=0.6; extra == 'dev'
12
+ Description-Content-Type: text/markdown
13
+
14
+ # probnode-mcp
15
+
16
+ An [MCP](https://modelcontextprotocol.io) server that gives an AI agent ProbNode's Polymarket
17
+ history: find markets, check recording coverage, list files with sizes and sha256, sign download
18
+ links, and pull small windows of rows.
19
+
20
+ ```sh
21
+ pip install ./packages/probnode ./packages/probnode-mcp # from a checkout; on PyPI soon
22
+ ```
23
+
24
+ ```json
25
+ {"mcpServers": {"probnode": {"command": "probnode-mcp", "env": {"PROBNODE_API_KEY": "pmd_..."}}}}
26
+ ```
27
+
28
+ Tools: `datasets`, `search_markets`, `coverage`, `list_files`, `sign_files`, `timeseries`,
29
+ `samples`. Every call goes through the same API, plan and limits as the `probnode` SDK; refusals
30
+ come back as `{"error": ..., "status": ...}`. Bulk data travels as signed URLs, not through the
31
+ model's context - `timeseries` returns at most 500 rows.
@@ -0,0 +1,6 @@
1
+ probnode_mcp/__init__.py,sha256=D-nj4UIMAHgV6wXhm9hIo8YPmwbQvJlzNzzMkCm8ZRk,74
2
+ probnode_mcp/server.py,sha256=FxowPq_NyM2TC4lSJBh85CbOIhdH_WcVx2hquNQggG0,4854
3
+ probnode_mcp-0.1.0.dist-info/METADATA,sha256=fxvLebKsx47JyhHd4IaAjP8cee3aBkMKYg8noM0QAt0,1188
4
+ probnode_mcp-0.1.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
5
+ probnode_mcp-0.1.0.dist-info/entry_points.txt,sha256=LJNUXpTSoQT3X7_ttyp1Ph4BnrVWPlwnqf1zdVBSe2w,58
6
+ probnode_mcp-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ probnode-mcp = probnode_mcp.server:main