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.
- tilla_mcp-2.0.0/.gitignore +16 -0
- tilla_mcp-2.0.0/PKG-INFO +114 -0
- tilla_mcp-2.0.0/README.md +94 -0
- tilla_mcp-2.0.0/pyproject.toml +40 -0
- tilla_mcp-2.0.0/server.json +36 -0
- tilla_mcp-2.0.0/tilla_mcp.py +315 -0
tilla_mcp-2.0.0/PKG-INFO
ADDED
|
@@ -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()
|