tilla-mcp 2.0.0__tar.gz

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,16 @@
1
+ # never commit a key
2
+ .env
3
+ *.key
4
+ # your local copies of the data
5
+ *.db
6
+ *.db-wal
7
+ *.db-shm
8
+ *.parquet
9
+ *.csv
10
+ # python
11
+ __pycache__/
12
+ *.pyc
13
+ dist/
14
+ build/
15
+ *.egg-info/
16
+ .venv/
@@ -0,0 +1,114 @@
1
+ Metadata-Version: 2.5
2
+ Name: tilla-mcp
3
+ Version: 2.0.0
4
+ Summary: MCP server for TILLA - raw venue data, derived indicators and Bitcoin node data on one UTC clock. Let any AI agent pull model-ready market data directly.
5
+ Project-URL: Homepage, https://tillacrypto.com
6
+ Project-URL: API, https://api.tillacrypto.com
7
+ Project-URL: Documentation, https://tillacrypto.com/docs
8
+ Project-URL: Repository, https://github.com/TillaCrypto/tilla
9
+ Author-email: TILLA <support@tillacrypto.com>
10
+ License: MIT
11
+ Keywords: ai-agent,bitcoin,crypto,dataset,machine-learning,macro,market-data,mcp,model-context-protocol,quant,trading
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Intended Audience :: Financial and Insurance Industry
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Requires-Python: >=3.10
17
+ Requires-Dist: httpx>=0.27
18
+ Requires-Dist: mcp<2,>=1.2.0
19
+ Description-Content-Type: text/markdown
20
+
21
+ # TILLA MCP server
22
+
23
+ **Let your AI agent pull TILLA's market data directly.**
24
+
25
+ [TILLA](https://tillacrypto.com) serves raw venue data, derived indicators computed from it bar by bar
26
+ with no look-ahead, and blocks and mempool data from our own Bitcoin node - crypto spot and derivatives,
27
+ equities and ETFs, FX, rates and macro - every value on one UTC clock, every bar hashed as it lands.
28
+
29
+ This is the official [Model Context Protocol](https://modelcontextprotocol.io) server. Add it to any MCP
30
+ client and your agent can query TILLA on its own.
31
+
32
+ ## Connect in 30 seconds
33
+
34
+ 1. **Get a free API key** at **https://tillacrypto.com** (no card), or `POST https://api.tillacrypto.com/v1/demo_key`
35
+ for one with no account.
36
+ 2. Add the server to your MCP client.
37
+
38
+ ### Claude Desktop / Claude Code / Cursor (local, stdio)
39
+
40
+ First `pip install "mcp>=1.2,<2" httpx`, then point your client at `tilla_mcp.py` in this folder:
41
+
42
+ ```jsonc
43
+ {
44
+ "mcpServers": {
45
+ "tilla": {
46
+ "command": "python",
47
+ "args": ["/path/to/tilla_mcp.py"],
48
+ "env": { "TILLA_API_KEY": "tk_live_your_key_here" }
49
+ }
50
+ }
51
+ }
52
+ ```
53
+
54
+ Once `tilla-mcp` is published on PyPI, `"command": "uvx", "args": ["tilla-mcp"]` will work too, with nothing to download.
55
+ ### Remote / hosted (streamable HTTP)
56
+
57
+ ```bash
58
+ TILLA_MCP_TRANSPORT=http TILLA_API_KEY=tk_live_... python tilla_mcp.py
59
+ # serves MCP over http://127.0.0.1:8971/mcp (front with TLS for a public connector)
60
+ ```
61
+
62
+ ## Tools
63
+
64
+ Every tool calls a real route of the API (`TOOL_ROUTES` in `tilla_mcp.py` names it).
65
+
66
+ | Tool | Key? | What it does | API call |
67
+ |---|---|---|---|
68
+ | `tilla_about` | no | What TILLA is and how to start | - |
69
+ | `tilla_status` | no | The newest 1-minute bar and the tiers served | `GET /v1/status` |
70
+ | `tilla_tiers` | no | The public plans: price, history cap, limits, size | `GET /v1/tiers` |
71
+ | `tilla_inventory` | yes | Exactly which series the key holds, per layer and lane (call this first) | `GET /v1/inventory` |
72
+ | `tilla_latest` | yes | The newest bar of named series on one lane | `GET /v1/<tier>/<layer>/<lane>` |
73
+ | `tilla_window` | yes | A range of bars; page with `next_start` while `more` is true | `GET /v1/<tier>/<layer>/<lane>` |
74
+ | `tilla_usage` | yes | The key's plan and budget right now | `GET /v1/usage` |
75
+ | `tilla_markets` | no | Every market, its category and related markets | `GET /v1/markets` |
76
+ | `tilla_market` | yes | One market (and optionally its related ones) on one clock | `GET /v1/<tier>/market/<id>` |
77
+ | `tilla_sample` | no | The free sample of a plan: series per lane + one real row | `GET /v1/sample/<tier>` |
78
+ | `tilla_health` | no | The newest bar of every lane - is the data current | `GET /v1/health` |
79
+ | `tilla_docs` | no | The whole API manual in one call | `GET /llms-full.txt` + `GET /v1` |
80
+ | `tilla_tick` | yes | Every series the key holds for the bar that just closed, one call | `GET /v1/<tier>/tick` |
81
+ | `tilla_changes` | yes | Every value repaired since a time, so a kept copy stays correct | `GET /v1/<tier>/changes` |
82
+
83
+ Resources: `tilla://about`, `tilla://tiers`.
84
+
85
+ ## Config (env)
86
+
87
+ | Var | Default | Meaning |
88
+ |---|---|---|
89
+ | `TILLA_API_KEY` | - | Your `tk_live_...` key (needed for keyed tools) |
90
+ | `TILLA_API_BASE` | `https://api.tillacrypto.com` | API base (dev: `http://127.0.0.1:8898`) |
91
+ | `TILLA_MCP_TRANSPORT` | `stdio` | `stdio` (local) or `http` (remote) |
92
+ | `TILLA_MCP_HOST` / `TILLA_MCP_PORT` | `127.0.0.1` / `8971` | HTTP bind |
93
+
94
+ ## Safe by design
95
+
96
+ - **Read-only:** every tool is an HTTPS `GET` to `api.tillacrypto.com` (or `tillacrypto.com` for the docs). No tool
97
+ writes a file, runs a command, places a trade or touches a wallet.
98
+ - **Your key** is read from `TILLA_API_KEY` and sent only to the TILLA API, never logged or returned to the agent.
99
+ - **HTTP mode binds to `127.0.0.1`** unless you set `TILLA_MCP_HOST` yourself.
100
+ - **Two dependencies,** both standard: the official `mcp` SDK and `httpx`.
101
+
102
+ ## Notes for the agent
103
+
104
+ - Call `tilla_inventory` before naming a series - **never guess a series name**.
105
+ - A window with no `start` returns the newest bars; with a `start`, page forward with `next_start` until
106
+ `more` is false.
107
+ - `403` means the key or the data is outside the plan; `429` means a minute or day budget was hit, and
108
+ `retry_after_seconds` says when to try again.
109
+ - An empty value is real: a closed market carries its last print, but an open market with no print (and a
110
+ derived value with no mathematical answer) is left empty, never invented.
111
+
112
+ MIT licensed. Data access governed by the TILLA plan terms.
113
+
114
+ <!-- mcp-name: io.github.TillaCrypto/tilla-mcp -->
@@ -0,0 +1,94 @@
1
+ # TILLA MCP server
2
+
3
+ **Let your AI agent pull TILLA's market data directly.**
4
+
5
+ [TILLA](https://tillacrypto.com) serves raw venue data, derived indicators computed from it bar by bar
6
+ with no look-ahead, and blocks and mempool data from our own Bitcoin node - crypto spot and derivatives,
7
+ equities and ETFs, FX, rates and macro - every value on one UTC clock, every bar hashed as it lands.
8
+
9
+ This is the official [Model Context Protocol](https://modelcontextprotocol.io) server. Add it to any MCP
10
+ client and your agent can query TILLA on its own.
11
+
12
+ ## Connect in 30 seconds
13
+
14
+ 1. **Get a free API key** at **https://tillacrypto.com** (no card), or `POST https://api.tillacrypto.com/v1/demo_key`
15
+ for one with no account.
16
+ 2. Add the server to your MCP client.
17
+
18
+ ### Claude Desktop / Claude Code / Cursor (local, stdio)
19
+
20
+ First `pip install "mcp>=1.2,<2" httpx`, then point your client at `tilla_mcp.py` in this folder:
21
+
22
+ ```jsonc
23
+ {
24
+ "mcpServers": {
25
+ "tilla": {
26
+ "command": "python",
27
+ "args": ["/path/to/tilla_mcp.py"],
28
+ "env": { "TILLA_API_KEY": "tk_live_your_key_here" }
29
+ }
30
+ }
31
+ }
32
+ ```
33
+
34
+ Once `tilla-mcp` is published on PyPI, `"command": "uvx", "args": ["tilla-mcp"]` will work too, with nothing to download.
35
+ ### Remote / hosted (streamable HTTP)
36
+
37
+ ```bash
38
+ TILLA_MCP_TRANSPORT=http TILLA_API_KEY=tk_live_... python tilla_mcp.py
39
+ # serves MCP over http://127.0.0.1:8971/mcp (front with TLS for a public connector)
40
+ ```
41
+
42
+ ## Tools
43
+
44
+ Every tool calls a real route of the API (`TOOL_ROUTES` in `tilla_mcp.py` names it).
45
+
46
+ | Tool | Key? | What it does | API call |
47
+ |---|---|---|---|
48
+ | `tilla_about` | no | What TILLA is and how to start | - |
49
+ | `tilla_status` | no | The newest 1-minute bar and the tiers served | `GET /v1/status` |
50
+ | `tilla_tiers` | no | The public plans: price, history cap, limits, size | `GET /v1/tiers` |
51
+ | `tilla_inventory` | yes | Exactly which series the key holds, per layer and lane (call this first) | `GET /v1/inventory` |
52
+ | `tilla_latest` | yes | The newest bar of named series on one lane | `GET /v1/<tier>/<layer>/<lane>` |
53
+ | `tilla_window` | yes | A range of bars; page with `next_start` while `more` is true | `GET /v1/<tier>/<layer>/<lane>` |
54
+ | `tilla_usage` | yes | The key's plan and budget right now | `GET /v1/usage` |
55
+ | `tilla_markets` | no | Every market, its category and related markets | `GET /v1/markets` |
56
+ | `tilla_market` | yes | One market (and optionally its related ones) on one clock | `GET /v1/<tier>/market/<id>` |
57
+ | `tilla_sample` | no | The free sample of a plan: series per lane + one real row | `GET /v1/sample/<tier>` |
58
+ | `tilla_health` | no | The newest bar of every lane - is the data current | `GET /v1/health` |
59
+ | `tilla_docs` | no | The whole API manual in one call | `GET /llms-full.txt` + `GET /v1` |
60
+ | `tilla_tick` | yes | Every series the key holds for the bar that just closed, one call | `GET /v1/<tier>/tick` |
61
+ | `tilla_changes` | yes | Every value repaired since a time, so a kept copy stays correct | `GET /v1/<tier>/changes` |
62
+
63
+ Resources: `tilla://about`, `tilla://tiers`.
64
+
65
+ ## Config (env)
66
+
67
+ | Var | Default | Meaning |
68
+ |---|---|---|
69
+ | `TILLA_API_KEY` | - | Your `tk_live_...` key (needed for keyed tools) |
70
+ | `TILLA_API_BASE` | `https://api.tillacrypto.com` | API base (dev: `http://127.0.0.1:8898`) |
71
+ | `TILLA_MCP_TRANSPORT` | `stdio` | `stdio` (local) or `http` (remote) |
72
+ | `TILLA_MCP_HOST` / `TILLA_MCP_PORT` | `127.0.0.1` / `8971` | HTTP bind |
73
+
74
+ ## Safe by design
75
+
76
+ - **Read-only:** every tool is an HTTPS `GET` to `api.tillacrypto.com` (or `tillacrypto.com` for the docs). No tool
77
+ writes a file, runs a command, places a trade or touches a wallet.
78
+ - **Your key** is read from `TILLA_API_KEY` and sent only to the TILLA API, never logged or returned to the agent.
79
+ - **HTTP mode binds to `127.0.0.1`** unless you set `TILLA_MCP_HOST` yourself.
80
+ - **Two dependencies,** both standard: the official `mcp` SDK and `httpx`.
81
+
82
+ ## Notes for the agent
83
+
84
+ - Call `tilla_inventory` before naming a series - **never guess a series name**.
85
+ - A window with no `start` returns the newest bars; with a `start`, page forward with `next_start` until
86
+ `more` is false.
87
+ - `403` means the key or the data is outside the plan; `429` means a minute or day budget was hit, and
88
+ `retry_after_seconds` says when to try again.
89
+ - An empty value is real: a closed market carries its last print, but an open market with no print (and a
90
+ derived value with no mathematical answer) is left empty, never invented.
91
+
92
+ MIT licensed. Data access governed by the TILLA plan terms.
93
+
94
+ <!-- mcp-name: io.github.TillaCrypto/tilla-mcp -->
@@ -0,0 +1,40 @@
1
+ [project]
2
+ name = "tilla-mcp"
3
+ version = "2.0.0"
4
+ description = "MCP server for TILLA - raw venue data, derived indicators and Bitcoin node data on one UTC clock. Let any AI agent pull model-ready market data directly."
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ license = { text = "MIT" }
8
+ authors = [{ name = "TILLA", email = "support@tillacrypto.com" }]
9
+ keywords = ["mcp", "model-context-protocol", "crypto", "market-data", "macro",
10
+ "quant", "machine-learning", "bitcoin", "trading", "dataset", "ai-agent"]
11
+ classifiers = [
12
+ "Programming Language :: Python :: 3",
13
+ "License :: OSI Approved :: MIT License",
14
+ "Intended Audience :: Developers",
15
+ "Intended Audience :: Financial and Insurance Industry",
16
+ ]
17
+ dependencies = [
18
+ "mcp>=1.2.0,<2", # official Model Context Protocol SDK (FastMCP). Pin below the v2 pre-release.
19
+ "httpx>=0.27",
20
+ ]
21
+
22
+ [project.urls]
23
+ Homepage = "https://tillacrypto.com"
24
+ API = "https://api.tillacrypto.com"
25
+ Documentation = "https://tillacrypto.com/docs"
26
+ Repository = "https://github.com/TillaCrypto/tilla"
27
+
28
+ [project.scripts]
29
+ tilla-mcp = "tilla_mcp:main"
30
+
31
+ [build-system]
32
+ requires = ["hatchling"]
33
+ build-backend = "hatchling.build"
34
+
35
+ [tool.hatch.build.targets.wheel]
36
+ # single-module package
37
+ only-include = ["tilla_mcp.py"]
38
+
39
+ [tool.hatch.build.targets.wheel.sources]
40
+ "tilla_mcp.py" = "tilla_mcp.py"
@@ -0,0 +1,36 @@
1
+ {
2
+ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-07-09/server.schema.json",
3
+ "name": "io.github.TillaCrypto/tilla-mcp",
4
+ "title": "TILLA - market data on one clock",
5
+ "description": "Crypto, US stocks, FX, rates, macro and Bitcoin node data on one clock, for any AI agent.",
6
+ "version": "2.0.0",
7
+ "repository": {
8
+ "url": "https://github.com/TillaCrypto/tilla",
9
+ "source": "github",
10
+ "subfolder": "mcp"
11
+ },
12
+ "websiteUrl": "https://tillacrypto.com",
13
+ "packages": [
14
+ {
15
+ "registryType": "pypi",
16
+ "identifier": "tilla-mcp",
17
+ "version": "2.0.0",
18
+ "transport": {
19
+ "type": "stdio"
20
+ },
21
+ "environmentVariables": [
22
+ {
23
+ "name": "TILLA_API_KEY",
24
+ "description": "Your TILLA API key (tk_live_...). Get a free one at https://tillacrypto.com",
25
+ "isRequired": true,
26
+ "isSecret": true
27
+ },
28
+ {
29
+ "name": "TILLA_API_BASE",
30
+ "description": "API base URL. Defaults to https://api.tillacrypto.com",
31
+ "isRequired": false
32
+ }
33
+ ]
34
+ }
35
+ ]
36
+ }
@@ -0,0 +1,315 @@
1
+ """
2
+ TILLA MCP server — let any AI agent pull the TILLA data set directly.
3
+ =====================================================================
4
+ Wraps the TILLA data API (https://api.tillacrypto.com, /v1/*) as Model Context
5
+ Protocol tools. Once a user adds this server to their agent (Claude Desktop /
6
+ Claude Code / Cursor / any MCP client), the agent can query TILLA on its own —
7
+ no browser, no copy-paste.
8
+
9
+ Every tool maps to one route of the API; `TOOL_ROUTES` names it.
10
+
11
+ What it serves:
12
+ Raw venue data, derived indicators and our own Bitcoin node - crypto spot and
13
+ derivatives, equities and ETFs, FX, rates and macro - every value on one UTC
14
+ clock. Plans are nested; the plan is in the path.
15
+
16
+ Auth:
17
+ Set TILLA_API_KEY to a key (tk_live_...). Get a free one at
18
+ https://tillacrypto.com, or POST /v1/demo_key for one with no account.
19
+ The keyless tools (about, status, tiers, markets, sample, health, docs) work
20
+ without a key.
21
+
22
+ Config (env):
23
+ TILLA_API_KEY the caller's key. Optional for the keyless tools.
24
+ TILLA_API_BASE default https://api.tillacrypto.com (dev: http://127.0.0.1:8898)
25
+ TILLA_MCP_TRANSPORT "stdio" (default, for local agents) or "http" (remote/hosted)
26
+ TILLA_MCP_HOST http transport bind host (default 127.0.0.1)
27
+ TILLA_MCP_PORT http transport port (default 8971)
28
+
29
+ Run:
30
+ Local (stdio): python tilla_mcp.py
31
+ Remote (http): TILLA_MCP_TRANSPORT=http python tilla_mcp.py
32
+ """
33
+ from __future__ import annotations
34
+
35
+ import json
36
+ import os
37
+
38
+ import httpx
39
+
40
+ API_BASE = os.environ.get("TILLA_API_BASE", "https://api.tillacrypto.com").rstrip("/")
41
+ API_KEY = os.environ.get("TILLA_API_KEY", "").strip()
42
+ SIGNUP_URL = "https://tillacrypto.com"
43
+ TIMEOUT = httpx.Timeout(30.0, connect=8.0)
44
+ LAYERS = ("raw", "derived", "node", "deep") # deep: the hourly history before 2023-08-10
45
+
46
+ # The API route each tool calls.
47
+ TOOL_ROUTES = {
48
+ "tilla_status": ("GET", "/v1/status"),
49
+ "tilla_tiers": ("GET", "/v1/tiers"),
50
+ "tilla_inventory": ("GET", "/v1/inventory"),
51
+ "tilla_window": ("GET", "/v1/{tier}/raw/{lane}"), # and /derived/ and /node/
52
+ "tilla_latest": ("GET", "/v1/{tier}/raw/{lane}"),
53
+ "tilla_usage": ("GET", "/v1/usage"),
54
+ }
55
+
56
+ INSTRUCTIONS = (
57
+ "TILLA serves raw market data, derived indicators and its own Bitcoin node on one UTC "
58
+ "clock. Call `tilla_docs` to read the whole manual (every call and plan) in one go. `tilla_tick` returns every series the key holds for the minute just closed in one call, and `tilla_changes` every repaired cell. Use `tilla_status` to check the feed is live and `tilla_tiers` to see the plans. "
59
+ "With a key: call `tilla_inventory` FIRST to get the exact series names the key holds "
60
+ "(never invent a name), then `tilla_latest` for the newest bar or `tilla_window` for a "
61
+ "range (page with the returned `next_start` while `more` is true). For a whole market at once - "
62
+ "every BTC series plus its ETFs and miners, or one stock, pair or commodity - call `tilla_markets` "
63
+ "for the ids and `tilla_market` with one. `tilla_usage` shows "
64
+ "the key's budget. If TILLA_API_KEY is missing, tell the user to get a free key at "
65
+ + SIGNUP_URL + " ."
66
+ )
67
+
68
+
69
+ # ── http helpers ──────────────────────────────────────────────────────────────
70
+ def _headers(auth: bool) -> dict:
71
+ h = {"Accept": "application/json", "User-Agent": "tilla-mcp/2.0"}
72
+ if auth and API_KEY:
73
+ h["X-API-Key"] = API_KEY
74
+ return h
75
+
76
+
77
+ def _need_key() -> dict | None:
78
+ if not API_KEY:
79
+ return {"error": "no_api_key",
80
+ "message": "This tool needs a TILLA API key. Set the TILLA_API_KEY "
81
+ "environment variable to a tk_live_... key.",
82
+ "get_a_free_key": SIGNUP_URL}
83
+ return None
84
+
85
+
86
+ def _safe_json(r: httpx.Response) -> dict:
87
+ try:
88
+ return r.json()
89
+ except Exception: # noqa: BLE001
90
+ return {"raw": r.text[:500]}
91
+
92
+
93
+ def _get(path: str, auth: bool, params: dict | None = None) -> dict:
94
+ """GET {API_BASE}{path}. Parsed JSON, or a structured {error,...} an agent can act on."""
95
+ try:
96
+ with httpx.Client(timeout=TIMEOUT) as c:
97
+ r = c.get(API_BASE + path, headers=_headers(auth),
98
+ params={k: v for k, v in (params or {}).items() if v is not None})
99
+ except Exception as e: # noqa: BLE001 network / DNS / TLS
100
+ return {"error": "unreachable",
101
+ "message": "Could not reach the TILLA API at %s (%s)." % (API_BASE, type(e).__name__)}
102
+ if r.status_code == 401:
103
+ return {"error": "unauthorized", "status": 401,
104
+ "message": "No API key was sent. Get a free key at " + SIGNUP_URL}
105
+ if r.status_code == 403:
106
+ return {"error": "forbidden", "status": 403,
107
+ "message": _safe_json(r).get("error") or "That key or that data is outside the plan. "
108
+ "See tilla_tiers and tilla_inventory."}
109
+ if r.status_code == 429:
110
+ return {"error": "rate_limited", "status": 429,
111
+ "retry_after_seconds": r.headers.get("Retry-After"),
112
+ "remaining_this_minute": r.headers.get("X-RateLimit-Remaining"),
113
+ "message": _safe_json(r).get("error") or "A rate limit or daily budget was hit."}
114
+ if r.status_code >= 400:
115
+ return {"error": "http_error", "status": r.status_code,
116
+ "message": _safe_json(r).get("error", r.text[:200])}
117
+ return _safe_json(r)
118
+
119
+
120
+ def _names(series) -> str | None:
121
+ """Accept a list or a comma string; return the comma string the API expects."""
122
+ if not series:
123
+ return None
124
+ if isinstance(series, (list, tuple)):
125
+ return ",".join(str(c).strip() for c in series if str(c).strip())
126
+ return ",".join(p.strip() for p in str(series).split(",") if p.strip())
127
+
128
+
129
+ _TIER = {"t": None}
130
+
131
+
132
+ def _my_tier() -> str | dict:
133
+ """The key's own tier, asked once of /v1/usage - the tier goes in the data path."""
134
+ if _TIER["t"]:
135
+ return _TIER["t"]
136
+ u = _get("/v1/usage", auth=True)
137
+ if "tier" not in u:
138
+ return u
139
+ _TIER["t"] = u["tier"]
140
+ return _TIER["t"]
141
+
142
+
143
+ # ── the tools, as plain functions (registered with MCP below) ────────────────
144
+ def tilla_about() -> dict:
145
+ """What TILLA is, who it's for, and how to start. No API key needed."""
146
+ return {
147
+ "product": "TILLA - market data on one clock",
148
+ "what": "Raw venue data, derived indicators computed from it bar by bar with no look-ahead, "
149
+ "and blocks/mempool from our own Bitcoin node. Every value sits on a UTC bar labelled "
150
+ "by the end of the period it covers; every bar is hashed as it lands.",
151
+ "covers": ["crypto spot and derivatives (funding, open interest, long/short, options)",
152
+ "equities and ETFs", "FX", "commodity and index proxies", "rates and macro",
153
+ "sentiment and flows", "the Bitcoin node (Max and above)"],
154
+ "empty_values": "A closed market carries its last real print; an open market with no print, "
155
+ "or a derived value with no mathematical answer, is empty - never invented.",
156
+ "get_a_free_key": SIGNUP_URL,
157
+ "docs": "https://tillacrypto.com/#docs",
158
+ "openapi": API_BASE + "/openapi.json",
159
+ "tools": sorted(TOOL_ROUTES) + ["tilla_about"],
160
+ }
161
+
162
+
163
+ def tilla_status() -> dict:
164
+ """The newest 1-minute bar and the tiers served. No API key needed."""
165
+ return _get("/v1/status", auth=False)
166
+
167
+
168
+ def tilla_tiers() -> dict:
169
+ """The public plans: price, history cap, request limits and size. No API key needed."""
170
+ return _get("/v1/tiers", auth=False)
171
+
172
+
173
+ def tilla_inventory(layer: str | None = None, lane: str | None = None) -> dict:
174
+ """Exactly what the caller's key holds. No arguments: every lane per layer with its series
175
+ count. layer=raw|derived|node and lane=1min|hour|daily|...: every series name there. Call
176
+ this before tilla_latest / tilla_window and use the names it returns. Needs TILLA_API_KEY."""
177
+ return _need_key() or _get("/v1/inventory", auth=True, params={"layer": layer, "lane": lane})
178
+
179
+
180
+ def tilla_window(series=None, layer: str = "raw", lane: str = "1min", start: str | None = None,
181
+ end: str | None = None, limit: int = 1000, period: str | None = None, markets=None,
182
+ fields=None) -> dict:
183
+ """A window of bars. Name exactly what you want, one of: `series` = comma-separated names (at most 200) from
184
+ tilla_inventory, or a pattern like 'btc_*' / '*_close'; or `markets` + `fields` ('btc,eth' + 'close,volume' ->
185
+ btc_close, btc_volume, eth_close, eth_volume). layer = raw | derived | node | deep; lane = 1min, 5min, hour,
186
+ daily, ... start/end = 'YYYY-MM-DD[ HH:MM[:SS]]' UTC inclusive, or period = 30d 2m 6m 1y all; no start = the
187
+ newest bars. limit <= 50000. When `more` is true, call again with start = `next_start`. Needs TILLA_API_KEY."""
188
+ miss = _need_key()
189
+ if miss:
190
+ return miss
191
+ if layer not in LAYERS:
192
+ return {"error": "bad_layer", "message": "layer is raw, derived, node or deep"}
193
+ t = _my_tier()
194
+ if isinstance(t, dict):
195
+ return t
196
+ return _get("/v1/%s/%s/%s" % (t, layer, lane), auth=True,
197
+ params={"series": _names(series), "markets": _names(markets), "fields": _names(fields),
198
+ "start": start, "end": end, "period": period, "limit": int(limit)})
199
+
200
+
201
+ def tilla_latest(series, layer: str = "raw", lane: str = "1min") -> dict:
202
+ """The newest bar of the named series on one lane. Needs TILLA_API_KEY."""
203
+ return tilla_window(series, layer=layer, lane=lane, limit=1)
204
+
205
+
206
+ def tilla_usage() -> dict:
207
+ """The caller's plan and budget right now: tier, history days, limits, used today. Needs TILLA_API_KEY."""
208
+ return _need_key() or _get("/v1/usage", auth=True)
209
+
210
+
211
+ def tilla_markets() -> dict:
212
+ """Every market TILLA carries - crypto, US stocks and ETFs, FX, macro - with its category, its related markets,
213
+ how many series each plan holds of it, and its lanes. No API key needed. Use the `id` with tilla_market."""
214
+ return _get("/v1/markets", auth=False)
215
+
216
+
217
+ def tilla_market(market: str, lane: str = "1min", layer: str = "raw", related: bool = False,
218
+ period: str | None = None, limit: int = 500) -> dict:
219
+ """One market in one call: every series of `market` (an id from tilla_markets, e.g. btc, nvda, eurusd, gold) the
220
+ key holds on `lane`, on one clock. layer = raw | derived | node | deep | all. related=True adds its connected
221
+ markets (btc: its ETFs and miners). period = 1d 7d 30d 2m 3m 6m 1y 2y 3y all. At most 200 columns an answer:
222
+ `series_next_offset` names the next page. Needs TILLA_API_KEY."""
223
+ miss = _need_key()
224
+ if miss:
225
+ return miss
226
+ t = _my_tier()
227
+ if isinstance(t, dict):
228
+ return t
229
+ return _get("/v1/%s/market/%s" % (t, market), auth=True,
230
+ params={"lane": lane, "layer": layer, "related": "1" if related else None, "period": period,
231
+ "limit": int(limit)})
232
+
233
+
234
+ def tilla_sample(tier: str = "free") -> dict:
235
+ """The free sample of one plan (free, builder, pro or max), no key needed: how many series it holds on every
236
+ lane, and one aligned 1-minute row of real values. The full one-tick files are at /v1/sample/<tier>.csv and
237
+ .db (named in the answer's `files`)."""
238
+ return _get("/v1/sample/%s" % tier, auth=False)
239
+
240
+
241
+ def tilla_health() -> dict:
242
+ """The newest bar of every lane on every layer - is the data current? No API key needed."""
243
+ return _get("/v1/health", auth=False)
244
+
245
+
246
+ def tilla_docs() -> dict:
247
+ """The whole API manual in one call, no key needed: every call, every parameter, every plan, how paging, tick,
248
+ resume, changes and bulk work. Read this first when you need to know how to do anything."""
249
+ try:
250
+ with httpx.Client(timeout=TIMEOUT) as c:
251
+ r = c.get(SIGNUP_URL + "/llms-full.txt", headers={"User-Agent": "tilla-mcp/2.0"})
252
+ return {"docs": r.text, "calls": _get("/v1", auth=False)}
253
+ except Exception as e: # noqa: BLE001
254
+ return {"error": "unreachable", "message": "Could not read the docs (%s)." % type(e).__name__}
255
+
256
+
257
+ def tilla_tick(at: str | None = None, lanes: str | None = None) -> dict:
258
+ """Your whole plan, live, in one call: every series the key holds on every lane that closed at a boundary (`at` =
259
+ 'YYYY-MM-DD HH:MM', default the minute just closed). Per lane: bar, complete, series, values. Needs a key."""
260
+ t = _my_tier()
261
+ if isinstance(t, dict):
262
+ return t
263
+ return _get("/v1/%s/tick" % t, auth=True, params={"at": at, "lanes": lanes})
264
+
265
+
266
+ def tilla_changes(since: str) -> dict:
267
+ """Every value our checks repaired since `since` ('YYYY-MM-DD HH:MM:SS') that the key holds, with its value now -
268
+ apply them and a kept copy stays correct. Pass the answer's `until` as the next since. Needs a key."""
269
+ t = _my_tier()
270
+ if isinstance(t, dict):
271
+ return t
272
+ return _get("/v1/%s/changes" % t, auth=True, params={"since": since})
273
+
274
+
275
+ TOOL_ROUTES.update({"tilla_tick": ("GET", "/v1/{tier}/tick"), "tilla_changes": ("GET", "/v1/{tier}/changes")})
276
+ TOOL_ROUTES.update({"tilla_markets": ("GET", "/v1/markets"),
277
+ "tilla_market": ("GET", "/v1/{tier}/market/{subject}"),
278
+ "tilla_sample": ("GET", "/v1/sample/{name}"),
279
+ "tilla_health": ("GET", "/v1/health")})
280
+ TOOLS = (tilla_about, tilla_status, tilla_tiers, tilla_inventory, tilla_window, tilla_latest, tilla_usage,
281
+ tilla_markets, tilla_market, tilla_sample, tilla_health, tilla_docs, tilla_tick,
282
+ tilla_changes)
283
+
284
+
285
+ # ── MCP registration - only when the server actually runs ─────────────────────
286
+ def build():
287
+ from mcp.server.fastmcp import FastMCP
288
+ mcp = FastMCP("tilla", host=os.environ.get("TILLA_MCP_HOST", "127.0.0.1"),
289
+ port=int(os.environ.get("TILLA_MCP_PORT", "8971")), instructions=INSTRUCTIONS)
290
+ for fn in TOOLS:
291
+ mcp.tool()(fn)
292
+
293
+ @mcp.resource("tilla://about")
294
+ def about_resource() -> str:
295
+ """What TILLA is, for an agent's context."""
296
+ return json.dumps(tilla_about(), indent=2)
297
+
298
+ @mcp.resource("tilla://tiers")
299
+ def tiers_resource() -> str:
300
+ """The live plan catalog as JSON."""
301
+ return json.dumps(tilla_tiers(), indent=2)
302
+ return mcp
303
+
304
+
305
+ def main() -> None:
306
+ mcp = build()
307
+ transport = os.environ.get("TILLA_MCP_TRANSPORT", "stdio").lower()
308
+ if transport in ("http", "streamable-http", "streamable_http"):
309
+ mcp.run(transport="streamable-http")
310
+ else:
311
+ mcp.run() # stdio - the default for local agents
312
+
313
+
314
+ if __name__ == "__main__":
315
+ main()