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.
@@ -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"]