curator-mcp 0.2.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.
- curator_mcp-0.2.0/.gitignore +46 -0
- curator_mcp-0.2.0/PKG-INFO +164 -0
- curator_mcp-0.2.0/README.md +145 -0
- curator_mcp-0.2.0/SKILL.md +114 -0
- curator_mcp-0.2.0/curator_mcp/__init__.py +16 -0
- curator_mcp-0.2.0/curator_mcp/server.py +215 -0
- curator_mcp-0.2.0/pyproject.toml +55 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# ── Secrets ───────────────────────────────────────────────────────────────
|
|
2
|
+
.env
|
|
3
|
+
.env.*
|
|
4
|
+
!.env.example
|
|
5
|
+
|
|
6
|
+
# ── Python ────────────────────────────────────────────────────────────────
|
|
7
|
+
__pycache__/
|
|
8
|
+
*.py[cod]
|
|
9
|
+
.venv/
|
|
10
|
+
venv/
|
|
11
|
+
.pytest_cache/
|
|
12
|
+
.ruff_cache/
|
|
13
|
+
.mypy_cache/
|
|
14
|
+
*.egg-info/
|
|
15
|
+
dist/
|
|
16
|
+
build/
|
|
17
|
+
|
|
18
|
+
# ── Node / Next.js ────────────────────────────────────────────────────────
|
|
19
|
+
node_modules/
|
|
20
|
+
.next/
|
|
21
|
+
out/
|
|
22
|
+
.turbo/
|
|
23
|
+
*.tsbuildinfo
|
|
24
|
+
|
|
25
|
+
# ── Foundry ───────────────────────────────────────────────────────────────
|
|
26
|
+
cache/
|
|
27
|
+
broadcast/
|
|
28
|
+
# NOTE: contracts/out is intentionally NOT ignored — Lane A publishes ABIs from
|
|
29
|
+
# there and every other lane consumes them (master plan §10 Lane A).
|
|
30
|
+
|
|
31
|
+
# ── Local runtime state ───────────────────────────────────────────────────
|
|
32
|
+
*.log
|
|
33
|
+
.anvil-state.json
|
|
34
|
+
scratch/
|
|
35
|
+
# Lane B: persisted mandates and AgentAction journals (agent/config.py state_dir)
|
|
36
|
+
.agent-state/
|
|
37
|
+
# Wave 0: e2e runs against an isolated AGENT_STATE_DIR so they never share a
|
|
38
|
+
# journal with a running agent — two writers corrupt line-count-derived ids.
|
|
39
|
+
.agent-state-*/
|
|
40
|
+
|
|
41
|
+
# ── OS / editor ───────────────────────────────────────────────────────────
|
|
42
|
+
.DS_Store
|
|
43
|
+
Thumbs.db
|
|
44
|
+
.idea/
|
|
45
|
+
.vscode/*
|
|
46
|
+
!.vscode/extensions.json
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: curator-mcp
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: MCP server exposing DeFi lending yields, TVL, utilization and token prices from The Graph
|
|
5
|
+
License: MIT
|
|
6
|
+
Keywords: agent-tools,defi,mcp,subgraph,thegraph,yields
|
|
7
|
+
Classifier: Development Status :: 4 - Beta
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
11
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
12
|
+
Requires-Python: >=3.10
|
|
13
|
+
Requires-Dist: curator-data
|
|
14
|
+
Requires-Dist: mcp>=1.2
|
|
15
|
+
Provides-Extra: test
|
|
16
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'test'
|
|
17
|
+
Requires-Dist: pytest>=8; extra == 'test'
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# curator-mcp
|
|
21
|
+
|
|
22
|
+
**Live DeFi lending yields, TVL, utilization and token prices on Base — as MCP tools.**
|
|
23
|
+
|
|
24
|
+
An MCP server that answers the questions an agent actually asks about DeFi markets: *where should
|
|
25
|
+
this asset sit, what does it earn, and how deep is the liquidity?* Data comes from
|
|
26
|
+
[The Graph](https://thegraph.com) — Messari standardized subgraphs for lending and DEX markets, and
|
|
27
|
+
the Token API for spot prices.
|
|
28
|
+
|
|
29
|
+
Standalone and dependency-light. It knows nothing about any particular agent; it was extracted from
|
|
30
|
+
one, which is the only relationship it has to the vault curator that first used it.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
Requires Python ≥ 3.10 and a free Graph API key from
|
|
37
|
+
[thegraph.com/studio](https://thegraph.com/studio) → API Keys.
|
|
38
|
+
|
|
39
|
+
**From a clone** — works today:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
git clone <this-repo> && cd <this-repo>
|
|
43
|
+
uv pip install ./data/curator_mcp
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The two sibling packages (`curator-data`, `curator-schema`) resolve from the repo automatically.
|
|
47
|
+
|
|
48
|
+
**From PyPI** — once published (see [PUBLISHING.md](../PUBLISHING.md); the packages are built and
|
|
49
|
+
verified, the upload needs a token):
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
uvx curator-mcp # run without installing
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Configure your MCP client
|
|
56
|
+
|
|
57
|
+
```jsonc
|
|
58
|
+
{
|
|
59
|
+
"mcpServers": {
|
|
60
|
+
"curator": {
|
|
61
|
+
"command": "uv",
|
|
62
|
+
"args": ["run", "--directory", "/abs/path/to/repo", "curator-mcp"],
|
|
63
|
+
"env": { "GRAPH_API_KEY": "your-key-here" }
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Once published, that becomes simply `"command": "uvx", "args": ["curator-mcp"]`.
|
|
70
|
+
|
|
71
|
+
Either way, Claude Desktop, Claude Code, Cursor, Zed and anything else speaking MCP over stdio will
|
|
72
|
+
pick the tools up.
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Tools
|
|
77
|
+
|
|
78
|
+
| Tool | Arguments | Answers |
|
|
79
|
+
|---|---|---|
|
|
80
|
+
| `compare_protocols` | `asset` | Where should this asset sit? Ranks every protocol, and names the best-APY and deepest-TVL ones separately because they are usually not the same. |
|
|
81
|
+
| `get_market_yields` | `asset` | What does this asset earn, and where? Supply-side APYs across every protocol listing it. |
|
|
82
|
+
| `list_markets` | `assets?` | Broad survey of lending markets and DEX pools. |
|
|
83
|
+
| `get_token_price` | `symbol` | Spot USD price. |
|
|
84
|
+
|
|
85
|
+
Plus a `curator://protocols` resource listing which protocols are configured.
|
|
86
|
+
|
|
87
|
+
### Response shape
|
|
88
|
+
|
|
89
|
+
Every tool returns the same three guarantees:
|
|
90
|
+
|
|
91
|
+
```jsonc
|
|
92
|
+
{
|
|
93
|
+
"asset": "USDC",
|
|
94
|
+
"protocols": [
|
|
95
|
+
{
|
|
96
|
+
"protocol": "aave-v3",
|
|
97
|
+
"supply_apy": 0.0432, // fraction — use this for arithmetic
|
|
98
|
+
"supply_apy_pct": 4.32, // percent — use this for display
|
|
99
|
+
"tvl_usd": 84200000.0,
|
|
100
|
+
"utilization": 0.91,
|
|
101
|
+
"fact_ids": ["messari:yield:aave-v3/usdc", "messari:tvl:aave-v3/usdc"],
|
|
102
|
+
"sources": ["messari"]
|
|
103
|
+
}
|
|
104
|
+
],
|
|
105
|
+
"best_apy": "aave-v3",
|
|
106
|
+
"deepest_tvl": "moonwell",
|
|
107
|
+
"taken_at": "2026-07-25T14:05:00+00:00",
|
|
108
|
+
"errors": [] // non-empty means your view is PARTIAL
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
1. **`fact_ids` and `sources` on every number.** Provenance travels with the data, so a model can
|
|
113
|
+
cite what it saw and a human can check it.
|
|
114
|
+
2. **`errors` on every response.** If a protocol could not be reached it is named here. A non-empty
|
|
115
|
+
`errors` means the answer is partial and should be described that way — never silently treated as
|
|
116
|
+
the whole market.
|
|
117
|
+
3. **APY twice, in both units.** `supply_apy` is a fraction (`0.0432`); `supply_apy_pct` is a
|
|
118
|
+
percentage (`4.32`). Mixing them up is a 100× error, so neither has to be inferred.
|
|
119
|
+
|
|
120
|
+
See [SKILL.md](SKILL.md) for how an agent should use these tools well.
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Configuration
|
|
125
|
+
|
|
126
|
+
| Variable | Required | Default | Purpose |
|
|
127
|
+
|---|---|---|---|
|
|
128
|
+
| `GRAPH_API_KEY` | **yes** | — | Subgraph gateway credential ([thegraph.com/studio](https://thegraph.com/studio)) |
|
|
129
|
+
| `TOKEN_API_KEY` | no | `GRAPH_API_KEY` | Token API bearer token, if different |
|
|
130
|
+
| `TOKEN_API_URL` | no | `https://api.pinax.network/v1` | Token API host. Pinax operates it — `token-api.thegraph.com` does not resolve |
|
|
131
|
+
| `DATA_CHAIN` | no | `base` | Chain to query |
|
|
132
|
+
| `DATA_REQUEST_TIMEOUT_S` | no | `15` | Per-request timeout |
|
|
133
|
+
| `CURATOR_MCP_LOG_LEVEL` | no | `WARNING` | Logs go to stderr; stdout is the MCP transport |
|
|
134
|
+
|
|
135
|
+
Values are read from the environment, or from a `.env` found by walking up from the package.
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Extending it
|
|
140
|
+
|
|
141
|
+
Protocols are configuration, not code. Because Messari publishes one standardized schema per
|
|
142
|
+
protocol *type*, every lending market answers the same GraphQL query — so adding one is a single
|
|
143
|
+
line in [`curator_data/sources/protocols.py`](../curator_data/sources/protocols.py):
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
Protocol(key="moonwell", subgraph_id="33ex…sBrg", family="lending", label="Moonwell (Base)"),
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Adding a whole new *provider* (Chainlink, Pyth, DefiLlama) is one file plus one registration line —
|
|
150
|
+
see the [curator-data README](../README.md). New sources appear in these tools automatically,
|
|
151
|
+
because tools resolve sources by capability (`provides = ("price",)`) rather than by name.
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## Verify it works
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
GRAPH_API_KEY=… curator-data verify-live # live gateway check, names any failing protocol
|
|
159
|
+
uv run pytest data/tests -q # offline test suite
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## License
|
|
163
|
+
|
|
164
|
+
MIT.
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# curator-mcp
|
|
2
|
+
|
|
3
|
+
**Live DeFi lending yields, TVL, utilization and token prices on Base — as MCP tools.**
|
|
4
|
+
|
|
5
|
+
An MCP server that answers the questions an agent actually asks about DeFi markets: *where should
|
|
6
|
+
this asset sit, what does it earn, and how deep is the liquidity?* Data comes from
|
|
7
|
+
[The Graph](https://thegraph.com) — Messari standardized subgraphs for lending and DEX markets, and
|
|
8
|
+
the Token API for spot prices.
|
|
9
|
+
|
|
10
|
+
Standalone and dependency-light. It knows nothing about any particular agent; it was extracted from
|
|
11
|
+
one, which is the only relationship it has to the vault curator that first used it.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
Requires Python ≥ 3.10 and a free Graph API key from
|
|
18
|
+
[thegraph.com/studio](https://thegraph.com/studio) → API Keys.
|
|
19
|
+
|
|
20
|
+
**From a clone** — works today:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
git clone <this-repo> && cd <this-repo>
|
|
24
|
+
uv pip install ./data/curator_mcp
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The two sibling packages (`curator-data`, `curator-schema`) resolve from the repo automatically.
|
|
28
|
+
|
|
29
|
+
**From PyPI** — once published (see [PUBLISHING.md](../PUBLISHING.md); the packages are built and
|
|
30
|
+
verified, the upload needs a token):
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
uvx curator-mcp # run without installing
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Configure your MCP client
|
|
37
|
+
|
|
38
|
+
```jsonc
|
|
39
|
+
{
|
|
40
|
+
"mcpServers": {
|
|
41
|
+
"curator": {
|
|
42
|
+
"command": "uv",
|
|
43
|
+
"args": ["run", "--directory", "/abs/path/to/repo", "curator-mcp"],
|
|
44
|
+
"env": { "GRAPH_API_KEY": "your-key-here" }
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Once published, that becomes simply `"command": "uvx", "args": ["curator-mcp"]`.
|
|
51
|
+
|
|
52
|
+
Either way, Claude Desktop, Claude Code, Cursor, Zed and anything else speaking MCP over stdio will
|
|
53
|
+
pick the tools up.
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Tools
|
|
58
|
+
|
|
59
|
+
| Tool | Arguments | Answers |
|
|
60
|
+
|---|---|---|
|
|
61
|
+
| `compare_protocols` | `asset` | Where should this asset sit? Ranks every protocol, and names the best-APY and deepest-TVL ones separately because they are usually not the same. |
|
|
62
|
+
| `get_market_yields` | `asset` | What does this asset earn, and where? Supply-side APYs across every protocol listing it. |
|
|
63
|
+
| `list_markets` | `assets?` | Broad survey of lending markets and DEX pools. |
|
|
64
|
+
| `get_token_price` | `symbol` | Spot USD price. |
|
|
65
|
+
|
|
66
|
+
Plus a `curator://protocols` resource listing which protocols are configured.
|
|
67
|
+
|
|
68
|
+
### Response shape
|
|
69
|
+
|
|
70
|
+
Every tool returns the same three guarantees:
|
|
71
|
+
|
|
72
|
+
```jsonc
|
|
73
|
+
{
|
|
74
|
+
"asset": "USDC",
|
|
75
|
+
"protocols": [
|
|
76
|
+
{
|
|
77
|
+
"protocol": "aave-v3",
|
|
78
|
+
"supply_apy": 0.0432, // fraction — use this for arithmetic
|
|
79
|
+
"supply_apy_pct": 4.32, // percent — use this for display
|
|
80
|
+
"tvl_usd": 84200000.0,
|
|
81
|
+
"utilization": 0.91,
|
|
82
|
+
"fact_ids": ["messari:yield:aave-v3/usdc", "messari:tvl:aave-v3/usdc"],
|
|
83
|
+
"sources": ["messari"]
|
|
84
|
+
}
|
|
85
|
+
],
|
|
86
|
+
"best_apy": "aave-v3",
|
|
87
|
+
"deepest_tvl": "moonwell",
|
|
88
|
+
"taken_at": "2026-07-25T14:05:00+00:00",
|
|
89
|
+
"errors": [] // non-empty means your view is PARTIAL
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
1. **`fact_ids` and `sources` on every number.** Provenance travels with the data, so a model can
|
|
94
|
+
cite what it saw and a human can check it.
|
|
95
|
+
2. **`errors` on every response.** If a protocol could not be reached it is named here. A non-empty
|
|
96
|
+
`errors` means the answer is partial and should be described that way — never silently treated as
|
|
97
|
+
the whole market.
|
|
98
|
+
3. **APY twice, in both units.** `supply_apy` is a fraction (`0.0432`); `supply_apy_pct` is a
|
|
99
|
+
percentage (`4.32`). Mixing them up is a 100× error, so neither has to be inferred.
|
|
100
|
+
|
|
101
|
+
See [SKILL.md](SKILL.md) for how an agent should use these tools well.
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Configuration
|
|
106
|
+
|
|
107
|
+
| Variable | Required | Default | Purpose |
|
|
108
|
+
|---|---|---|---|
|
|
109
|
+
| `GRAPH_API_KEY` | **yes** | — | Subgraph gateway credential ([thegraph.com/studio](https://thegraph.com/studio)) |
|
|
110
|
+
| `TOKEN_API_KEY` | no | `GRAPH_API_KEY` | Token API bearer token, if different |
|
|
111
|
+
| `TOKEN_API_URL` | no | `https://api.pinax.network/v1` | Token API host. Pinax operates it — `token-api.thegraph.com` does not resolve |
|
|
112
|
+
| `DATA_CHAIN` | no | `base` | Chain to query |
|
|
113
|
+
| `DATA_REQUEST_TIMEOUT_S` | no | `15` | Per-request timeout |
|
|
114
|
+
| `CURATOR_MCP_LOG_LEVEL` | no | `WARNING` | Logs go to stderr; stdout is the MCP transport |
|
|
115
|
+
|
|
116
|
+
Values are read from the environment, or from a `.env` found by walking up from the package.
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## Extending it
|
|
121
|
+
|
|
122
|
+
Protocols are configuration, not code. Because Messari publishes one standardized schema per
|
|
123
|
+
protocol *type*, every lending market answers the same GraphQL query — so adding one is a single
|
|
124
|
+
line in [`curator_data/sources/protocols.py`](../curator_data/sources/protocols.py):
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
Protocol(key="moonwell", subgraph_id="33ex…sBrg", family="lending", label="Moonwell (Base)"),
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Adding a whole new *provider* (Chainlink, Pyth, DefiLlama) is one file plus one registration line —
|
|
131
|
+
see the [curator-data README](../README.md). New sources appear in these tools automatically,
|
|
132
|
+
because tools resolve sources by capability (`provides = ("price",)`) rather than by name.
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## Verify it works
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
GRAPH_API_KEY=… curator-data verify-live # live gateway check, names any failing protocol
|
|
140
|
+
uv run pytest data/tests -q # offline test suite
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## License
|
|
144
|
+
|
|
145
|
+
MIT.
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: defi-market-analysis
|
|
3
|
+
description: Analyse DeFi lending markets and token prices on Base using the curator-mcp tools. Use when deciding where to allocate an asset for yield, comparing lending protocols, checking whether a headline APY is trustworthy, or pricing tokens. Covers reading yields correctly, weighing APY against liquidity depth and utilization, and citing data provenance.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# DeFi market analysis
|
|
7
|
+
|
|
8
|
+
How to use the `curator-mcp` tools to reach a defensible allocation decision instead of a plausible
|
|
9
|
+
one. The tools give you live market data; this document is about not misreading it.
|
|
10
|
+
|
|
11
|
+
## The tools
|
|
12
|
+
|
|
13
|
+
| Tool | Use it when |
|
|
14
|
+
|---|---|
|
|
15
|
+
| `compare_protocols(asset)` | You know the asset and must choose where it goes. **Start here.** |
|
|
16
|
+
| `get_market_yields(asset)` | You only need the rates, without the ranking commentary. |
|
|
17
|
+
| `list_markets(assets)` | You are surveying, not yet deciding. |
|
|
18
|
+
| `get_token_price(symbol)` | You need to value a holding in USD. |
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Three ways to misread this data
|
|
23
|
+
|
|
24
|
+
### 1. APY comes in two units. Do not mix them.
|
|
25
|
+
|
|
26
|
+
Every response carries both:
|
|
27
|
+
|
|
28
|
+
- `supply_apy` — a **fraction**. `0.0432` means 4.32%. Use this for arithmetic.
|
|
29
|
+
- `supply_apy_pct` — a **percentage**. `4.32` means 4.32%. Use this for display.
|
|
30
|
+
|
|
31
|
+
Multiplying a balance by `supply_apy_pct` overstates the return 100×. If you are computing, use
|
|
32
|
+
`supply_apy`. If you are writing a sentence for a human, use `supply_apy_pct` and add the `%`.
|
|
33
|
+
|
|
34
|
+
### 2. A non-empty `errors` array means you are looking at a partial market.
|
|
35
|
+
|
|
36
|
+
Every response has `errors`. When it is non-empty, one or more protocols could not be reached — so
|
|
37
|
+
the protocol you are about to call "the best available" may simply be the best of what *responded*.
|
|
38
|
+
|
|
39
|
+
**Say so.** "Aave has the highest APY of the two protocols that responded; Moonwell timed out" is a
|
|
40
|
+
useful sentence. "Aave has the highest APY" — when Moonwell was silently missing — is a false one.
|
|
41
|
+
|
|
42
|
+
### 3. The highest APY is frequently the wrong answer.
|
|
43
|
+
|
|
44
|
+
`compare_protocols` deliberately reports `best_apy` and `deepest_tvl` as separate fields because
|
|
45
|
+
they usually name different protocols. That gap *is* the decision. Reasoning that stops at the
|
|
46
|
+
highest number has not done the work.
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Reading a market properly
|
|
51
|
+
|
|
52
|
+
For each candidate, three numbers matter together:
|
|
53
|
+
|
|
54
|
+
**`supply_apy`** — what you earn. Necessary, not sufficient.
|
|
55
|
+
|
|
56
|
+
**`tvl_usd`** — how deep the market is. This is your exit. A position that is a large fraction of a
|
|
57
|
+
market's TVL cannot be unwound without moving the rate against you. As a rough discipline: a
|
|
58
|
+
position above ~1% of TVL deserves an explicit note; above ~5% treat depth as the binding
|
|
59
|
+
constraint, not the yield.
|
|
60
|
+
|
|
61
|
+
**`utilization`** — borrowed ÷ supplied, between 0 and 1. The most informative and most ignored:
|
|
62
|
+
|
|
63
|
+
| Utilization | What it means |
|
|
64
|
+
|---|---|
|
|
65
|
+
| < 0.5 | Rate is stable but low. Plenty of headroom to withdraw. |
|
|
66
|
+
| 0.5 – 0.8 | Healthy. The rate reflects real demand. |
|
|
67
|
+
| 0.8 – 0.9 | Rate is sensitive; small changes in demand move it materially. |
|
|
68
|
+
| **> 0.9** | **Withdrawals may be constrained.** The headline APY is volatile and often unrepresentative of what you'll actually average. Treat a high APY here as a warning, not a reward. |
|
|
69
|
+
|
|
70
|
+
A 5.9% APY at 0.95 utilization and a 4.3% APY at 0.6 utilization are not 1.6 percentage points
|
|
71
|
+
apart in any meaningful sense. The first can trap the position; the second is liquid.
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Citing your sources
|
|
76
|
+
|
|
77
|
+
Every number carries `fact_ids` and `sources`. Cite them when you explain a decision:
|
|
78
|
+
|
|
79
|
+
> Allocating to Aave V3 (4.32% APY, $84.2M TVL, 0.91 utilization) over Moonwell (5.87%, $12.1M,
|
|
80
|
+
> 0.96). The 1.55pp of extra yield does not compensate for a market one-seventh the depth at higher
|
|
81
|
+
> utilization — an exit would be constrained exactly when it mattered.
|
|
82
|
+
> *Facts: `messari:yield:aave-v3/usdc`, `messari:tvl:aave-v3/usdc`, `messari:utilization:aave-v3/usdc`.*
|
|
83
|
+
|
|
84
|
+
Fact ids are stable across snapshots for the same market, so decisions can be diffed over time.
|
|
85
|
+
|
|
86
|
+
**Never state a number these tools did not return.** If you need a figure that isn't in a response,
|
|
87
|
+
say it is unavailable. A fabricated yield is indistinguishable from a real one to the reader, and
|
|
88
|
+
acted on identically.
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## A worked pattern
|
|
93
|
+
|
|
94
|
+
```
|
|
95
|
+
1. compare_protocols("USDC")
|
|
96
|
+
2. Check `errors`. Non-empty → note which protocols are missing and qualify everything below.
|
|
97
|
+
3. Read best_apy and deepest_tvl. Different? That is the tradeoff to resolve, explicitly.
|
|
98
|
+
4. For each candidate, check utilization. Above 0.9 → the APY is not what it appears.
|
|
99
|
+
5. Size the position against tvl_usd, not just against your own balance.
|
|
100
|
+
6. State the decision, the runner-up, and why the runner-up lost — citing fact ids.
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
If every candidate is unattractive, **say so and recommend holding.** "No market currently justifies
|
|
104
|
+
the move" is a complete answer, and a better one than allocating into a 0.97-utilization market
|
|
105
|
+
because it had the biggest number.
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Scope
|
|
110
|
+
|
|
111
|
+
Base mainnet by default. Lending and DEX data comes from Messari standardized subgraphs, prices from
|
|
112
|
+
the Token API — both via The Graph. The `curator://protocols` resource lists exactly what is
|
|
113
|
+
configured; if a protocol you expect is missing it is not being queried, and any "best available"
|
|
114
|
+
claim should be scoped to the list that is.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""curator-mcp — DeFi market data from The Graph, as MCP tools.
|
|
2
|
+
|
|
3
|
+
A standalone MCP server. It has no dependency on the vault-curation agent that
|
|
4
|
+
happens to be its first consumer; any MCP client can install and run it.
|
|
5
|
+
|
|
6
|
+
uvx curator-mcp
|
|
7
|
+
|
|
8
|
+
See README.md for client configuration and SKILL.md for how an agent should
|
|
9
|
+
actually use these tools.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from .server import build_server, main
|
|
13
|
+
|
|
14
|
+
__all__ = ["build_server", "main"]
|
|
15
|
+
|
|
16
|
+
__version__ = "0.2.0"
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""The MCP server: four tools over live DeFi market data from The Graph.
|
|
2
|
+
|
|
3
|
+
Design rules this server follows, because they are what separate reusable
|
|
4
|
+
tooling from a demo endpoint:
|
|
5
|
+
|
|
6
|
+
* **Every tool answers a question an agent actually asks.** Not "run this
|
|
7
|
+
GraphQL" — an agent that could write the GraphQL would not need the tool.
|
|
8
|
+
`compare_protocols("USDC")` is the unit of thought.
|
|
9
|
+
* **Provenance travels with every number.** Each response carries `fact_ids`
|
|
10
|
+
and `sources`, so a caller can cite what it saw and a reviewer can check it.
|
|
11
|
+
* **Degradation is reported, never hidden.** `errors` is present on every
|
|
12
|
+
response. A tool that silently returns 2 protocols when 3 were configured
|
|
13
|
+
teaches the model that the market is smaller than it is.
|
|
14
|
+
* **No knowledge of any particular consumer.** This server does not import,
|
|
15
|
+
reference or assume the vault-curation agent that happens to be its first
|
|
16
|
+
user.
|
|
17
|
+
|
|
18
|
+
Transport is stdio, which is what MCP clients launch by default.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import logging
|
|
24
|
+
import os
|
|
25
|
+
|
|
26
|
+
from mcp.server.fastmcp import FastMCP
|
|
27
|
+
|
|
28
|
+
from curator_data.config import Settings
|
|
29
|
+
from curator_data.queries import (
|
|
30
|
+
MARKET_KINDS,
|
|
31
|
+
PRICE_KINDS,
|
|
32
|
+
errors_as_dicts,
|
|
33
|
+
pivot_markets,
|
|
34
|
+
pivot_pools,
|
|
35
|
+
prices,
|
|
36
|
+
snapshot_for,
|
|
37
|
+
)
|
|
38
|
+
from curator_data.sources.feeds import known_symbols as feed_symbols
|
|
39
|
+
from curator_data.sources.protocols import ALL as ALL_PROTOCOLS
|
|
40
|
+
from curator_data.sources.tokens import known_symbols
|
|
41
|
+
|
|
42
|
+
logger = logging.getLogger(__name__)
|
|
43
|
+
|
|
44
|
+
#: Used when a caller names no assets. Broad enough to be useful on Base,
|
|
45
|
+
#: small enough to keep a response readable.
|
|
46
|
+
DEFAULT_ASSETS = ["USDC", "WETH"]
|
|
47
|
+
|
|
48
|
+
INSTRUCTIONS = """\
|
|
49
|
+
Live DeFi market data on Base from The Graph.
|
|
50
|
+
|
|
51
|
+
Use `compare_protocols` to pick where to put an asset, `get_market_yields` for one
|
|
52
|
+
asset's rates across every protocol, `list_markets` for a broad survey, and
|
|
53
|
+
`get_token_price` for spot USD prices.
|
|
54
|
+
|
|
55
|
+
Every response includes `fact_ids` and `sources`. Cite them when you explain a
|
|
56
|
+
decision. Every response also includes `errors`: if it is non-empty, some data
|
|
57
|
+
could not be fetched and your view is partial — say so rather than treating the
|
|
58
|
+
result as the whole market.
|
|
59
|
+
|
|
60
|
+
APYs are returned twice: `supply_apy` as a fraction (0.0432) for arithmetic, and
|
|
61
|
+
`supply_apy_pct` (4.32) for display. Do not mix them up.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def build_server(settings: Settings | None = None) -> FastMCP:
|
|
66
|
+
"""Construct the server. Separated from `main` so tests can drive it."""
|
|
67
|
+
resolved = settings or Settings.from_env()
|
|
68
|
+
mcp = FastMCP("curator", instructions=INSTRUCTIONS)
|
|
69
|
+
|
|
70
|
+
@mcp.tool()
|
|
71
|
+
async def list_markets(assets: list[str] | None = None) -> dict:
|
|
72
|
+
"""Survey lending markets across every configured protocol.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
assets: Token symbols to include, e.g. ["USDC", "WETH"]. Omit for a
|
|
76
|
+
default set. Filtering happens by symbol, case-insensitive.
|
|
77
|
+
|
|
78
|
+
Returns markets sorted by supply APY (highest first), each with TVL,
|
|
79
|
+
utilization, and the fact ids backing it.
|
|
80
|
+
"""
|
|
81
|
+
symbols = [a.upper() for a in (assets or DEFAULT_ASSETS)]
|
|
82
|
+
snapshot = await snapshot_for(symbols, kinds=MARKET_KINDS, settings=resolved)
|
|
83
|
+
return {
|
|
84
|
+
"markets": [row.to_dict() for row in pivot_markets(snapshot)],
|
|
85
|
+
"pools": [row.to_dict() for row in pivot_pools(snapshot)],
|
|
86
|
+
"assets": symbols,
|
|
87
|
+
"taken_at": snapshot.taken_at.isoformat(),
|
|
88
|
+
"errors": errors_as_dicts(snapshot),
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
@mcp.tool()
|
|
92
|
+
async def get_market_yields(asset: str) -> dict:
|
|
93
|
+
"""Supply APYs for one asset across every protocol that lists it.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
asset: Token symbol, e.g. "USDC".
|
|
97
|
+
|
|
98
|
+
Use this when you know the asset and want to know what it earns and
|
|
99
|
+
where. Rates are current supply-side (lender) APYs.
|
|
100
|
+
"""
|
|
101
|
+
symbol = asset.strip().upper()
|
|
102
|
+
snapshot = await snapshot_for([symbol], kinds=MARKET_KINDS, settings=resolved)
|
|
103
|
+
rows = [r for r in pivot_markets(snapshot) if r.market.upper() == symbol]
|
|
104
|
+
return {
|
|
105
|
+
"asset": symbol,
|
|
106
|
+
"yields": [
|
|
107
|
+
{
|
|
108
|
+
"protocol": r.protocol,
|
|
109
|
+
"supply_apy": r.supply_apy,
|
|
110
|
+
"supply_apy_pct": r.supply_apy_pct,
|
|
111
|
+
"tvl_usd": r.tvl_usd,
|
|
112
|
+
"utilization": r.utilization,
|
|
113
|
+
"fact_ids": r.fact_ids,
|
|
114
|
+
"sources": r.sources,
|
|
115
|
+
}
|
|
116
|
+
for r in rows
|
|
117
|
+
],
|
|
118
|
+
"taken_at": snapshot.taken_at.isoformat(),
|
|
119
|
+
"errors": errors_as_dicts(snapshot),
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
@mcp.tool()
|
|
123
|
+
async def compare_protocols(asset: str) -> dict:
|
|
124
|
+
"""Rank protocols for supplying one asset, with the tradeoffs stated.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
asset: Token symbol, e.g. "USDC".
|
|
128
|
+
|
|
129
|
+
Returns the same rows as `get_market_yields` plus `best_apy` and
|
|
130
|
+
`deepest_tvl`, which are frequently different protocols — that gap is
|
|
131
|
+
the actual decision, so it is surfaced rather than left to be inferred.
|
|
132
|
+
High utilization means a headline APY is less stable and withdrawals
|
|
133
|
+
may be constrained.
|
|
134
|
+
"""
|
|
135
|
+
symbol = asset.strip().upper()
|
|
136
|
+
snapshot = await snapshot_for([symbol], kinds=MARKET_KINDS, settings=resolved)
|
|
137
|
+
rows = [r for r in pivot_markets(snapshot) if r.market.upper() == symbol]
|
|
138
|
+
|
|
139
|
+
with_apy = [r for r in rows if r.supply_apy is not None]
|
|
140
|
+
with_tvl = [r for r in rows if r.tvl_usd is not None]
|
|
141
|
+
best = max(with_apy, key=lambda r: r.supply_apy or 0.0, default=None)
|
|
142
|
+
deepest = max(with_tvl, key=lambda r: r.tvl_usd or 0.0, default=None)
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
"asset": symbol,
|
|
146
|
+
"protocols": [r.to_dict() for r in rows],
|
|
147
|
+
"best_apy": None if best is None else best.protocol,
|
|
148
|
+
"deepest_tvl": None if deepest is None else deepest.protocol,
|
|
149
|
+
"note": (
|
|
150
|
+
"Highest APY and deepest liquidity are often different protocols. "
|
|
151
|
+
"Utilization above ~0.9 means the rate is volatile and exits can be "
|
|
152
|
+
"constrained."
|
|
153
|
+
),
|
|
154
|
+
"taken_at": snapshot.taken_at.isoformat(),
|
|
155
|
+
"errors": errors_as_dicts(snapshot),
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
@mcp.tool()
|
|
159
|
+
async def get_token_price(symbol: str) -> dict:
|
|
160
|
+
"""Spot USD price for a token, cross-checked across independent sources.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
symbol: Token symbol, e.g. "WETH".
|
|
164
|
+
|
|
165
|
+
`price.price_usd` is the consensus (median). `price.observations` lists
|
|
166
|
+
each source separately — a Chainlink oracle read and a price derived
|
|
167
|
+
from executed DEX swaps measure the same number by unrelated means, so
|
|
168
|
+
when they agree the figure is well corroborated. `price.disagreement`
|
|
169
|
+
is true when they diverge enough to be worth acting on: a stale oracle,
|
|
170
|
+
a manipulated pool, or a genuinely dislocated market.
|
|
171
|
+
"""
|
|
172
|
+
token = symbol.strip().upper()
|
|
173
|
+
snapshot = await snapshot_for([token], kinds=PRICE_KINDS, settings=resolved)
|
|
174
|
+
found = prices(snapshot).get(token)
|
|
175
|
+
return {
|
|
176
|
+
"symbol": token,
|
|
177
|
+
"price": found,
|
|
178
|
+
"known_symbols": sorted(
|
|
179
|
+
set(known_symbols(resolved.chain)) | set(feed_symbols(resolved.chain))
|
|
180
|
+
),
|
|
181
|
+
"taken_at": snapshot.taken_at.isoformat(),
|
|
182
|
+
"errors": errors_as_dicts(snapshot),
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
@mcp.resource("curator://protocols")
|
|
186
|
+
def protocols() -> str:
|
|
187
|
+
"""The configured protocol set — what this server can currently see."""
|
|
188
|
+
lines = [
|
|
189
|
+
"# Configured protocols",
|
|
190
|
+
"",
|
|
191
|
+
"| key | label | family | chain | enabled |",
|
|
192
|
+
"|---|---|---|---|---|",
|
|
193
|
+
]
|
|
194
|
+
lines += [
|
|
195
|
+
f"| {p.key} | {p.label} | {p.family} | {p.chain} | {'yes' if p.enabled else 'no'} |"
|
|
196
|
+
for p in ALL_PROTOCOLS
|
|
197
|
+
]
|
|
198
|
+
return "\n".join(lines)
|
|
199
|
+
|
|
200
|
+
return mcp
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def main() -> None:
|
|
204
|
+
"""Console-script entry point: `curator-mcp` / `uvx curator-mcp`."""
|
|
205
|
+
logging.basicConfig(
|
|
206
|
+
level=os.getenv("CURATOR_MCP_LOG_LEVEL", "WARNING").upper(),
|
|
207
|
+
# stderr: stdout is the MCP transport and anything else on it is a
|
|
208
|
+
# protocol violation that shows up as a client-side parse error.
|
|
209
|
+
format="%(levelname)s %(name)s: %(message)s",
|
|
210
|
+
)
|
|
211
|
+
build_server().run()
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
if __name__ == "__main__": # pragma: no cover
|
|
215
|
+
main()
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "curator-mcp"
|
|
3
|
+
#: See the note in ../pyproject.toml — a version bump is what invalidates uv's
|
|
4
|
+
#: wheel cache, not just changelog hygiene.
|
|
5
|
+
version = "0.2.0"
|
|
6
|
+
description = "MCP server exposing DeFi lending yields, TVL, utilization and token prices from The Graph"
|
|
7
|
+
readme = "README.md"
|
|
8
|
+
classifiers = [
|
|
9
|
+
"Development Status :: 4 - Beta",
|
|
10
|
+
"Intended Audience :: Developers",
|
|
11
|
+
"License :: OSI Approved :: MIT License",
|
|
12
|
+
"Programming Language :: Python :: 3.10",
|
|
13
|
+
"Topic :: Software Development :: Libraries",
|
|
14
|
+
]
|
|
15
|
+
# 3.10 is the MCP SDK's floor. Kept as low as the SDK allows so this server is
|
|
16
|
+
# installable next to whatever agent wants it, not just inside our stack.
|
|
17
|
+
requires-python = ">=3.10"
|
|
18
|
+
license = { text = "MIT" }
|
|
19
|
+
keywords = ["mcp", "defi", "thegraph", "subgraph", "yields", "agent-tools"]
|
|
20
|
+
dependencies = [
|
|
21
|
+
"mcp>=1.2",
|
|
22
|
+
"curator-data",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[project.optional-dependencies]
|
|
26
|
+
test = ["pytest>=8", "pytest-asyncio>=0.23"]
|
|
27
|
+
|
|
28
|
+
[project.scripts]
|
|
29
|
+
# `uvx curator-mcp` — the invocation an MCP client config uses.
|
|
30
|
+
curator-mcp = "curator_mcp.server:main"
|
|
31
|
+
|
|
32
|
+
[tool.uv.sources]
|
|
33
|
+
# Resolve the sibling packages from the repo until all three are on PyPI.
|
|
34
|
+
# Without this, `uv pip install ./data/curator_mcp` fails outright:
|
|
35
|
+
# "Because curator-data was not found in the package registry ... unsatisfiable"
|
|
36
|
+
# which is exactly what a judge following SKILL.md hits. Relative paths, so it
|
|
37
|
+
# works from any clone location and on any OS.
|
|
38
|
+
#
|
|
39
|
+
# These entries are INERT once the dependencies exist on PyPI, and the root
|
|
40
|
+
# workspace overrides them for our own development.
|
|
41
|
+
#
|
|
42
|
+
# `editable = true` matters: the root workspace declares these as editable
|
|
43
|
+
# members, and uv treats the same path with a different editable flag as a
|
|
44
|
+
# CONFLICTING url ("Requirements contain conflicting URLs for package
|
|
45
|
+
# curator-data"). Matching the root's flag keeps `uv sync` working here while
|
|
46
|
+
# still making a standalone `uv pip install ./data/curator_mcp` resolve.
|
|
47
|
+
curator-data = { path = "../", editable = true }
|
|
48
|
+
curator-schema = { path = "../../packages/schema/python", editable = true }
|
|
49
|
+
|
|
50
|
+
[build-system]
|
|
51
|
+
requires = ["hatchling"]
|
|
52
|
+
build-backend = "hatchling.build"
|
|
53
|
+
|
|
54
|
+
[tool.hatch.build.targets.wheel]
|
|
55
|
+
packages = ["curator_mcp"]
|