sbor-mcp 1.1.3 → 1.2.0

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.
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- <!-- mcp-name: io.github.sborxyz/sbor -->
1
+ <!-- mcp-name: xyz.sbor/sbor -->
2
2
 
3
3
  # SBOR MCP server
4
4
 
@@ -32,17 +32,37 @@ It starts and waits silently. That is success. Ctrl+C to exit.
32
32
 
33
33
  Claude Desktop: add that to `claude_desktop_config.json` and restart.
34
34
 
35
+ ## Or connect by address, with nothing to install
36
+
37
+ ```
38
+ https://mcp.sbor.xyz/mcp
39
+ ```
40
+
41
+ Streamable HTTP, no key, no sign-in. Works as a custom connector in Claude on
42
+ the web and in any MCP client that accepts a URL. The remote server is generated
43
+ from this one, so both give the same answers.
44
+
35
45
  ## Tools
36
46
 
37
47
  | Tool | What it answers |
38
48
  |---|---|
39
- | `get_rate` | What does capital cost on Stacks right now |
49
+ | `get_rate` | What does capital cost on Stacks right now, and what does borrowing USDC against bitcoin cost |
40
50
  | `compare_rate` | Is this offer above or below the market, and by how much |
41
51
  | `list_markets` | Which venues make up the rate, with utilization and depth |
42
52
  | `get_history` | How has the rate moved |
43
53
  | `compare_chains` | How Stacks compares with Ethereum, Base, Hyperliquid, Solana and SOFR |
44
54
  | `get_methodology` | How the number is built, and what it excludes |
45
55
 
56
+ All six tools are read-only.
57
+
58
+ ## Benchmarks
59
+
60
+ `SBOR-USD`, `SBOR-BTC` and `SBOR-STX` are the Stacks indices. `BTC-COLLATERAL-USDC`
61
+ is a reference, not an SBOR index: what it costs to borrow USDC against bitcoin
62
+ wrapped by a custodian (cbBTC, WBTC), from the Morpho markets on Base and
63
+ Ethereum whose only collateral is that bitcoin. Use it to check a USDC loan
64
+ against bitcoin on those chains.
65
+
46
66
  ## Recommended use
47
67
 
48
68
  **Use SBOR to stop, never to start.** Before borrowing, call `compare_rate`. If
@@ -81,3 +101,5 @@ writes, no telemetry. Set `SBOR_BASE` to point at a different host.
81
101
 
82
102
  MIT. The published fixing is free to read. See
83
103
  [llms.txt](https://sbor.xyz/llms.txt) for the full integration policy.
104
+
105
+ *Last updated 25 September 2026.*
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sbor-mcp",
3
- "version": "1.1.3",
3
+ "version": "1.2.0",
4
4
  "description": "Check a lending rate before your agent borrows. The benchmark rate for Stacks, read from contract state. Free, no key, no rate limit.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -36,5 +36,10 @@
36
36
  "engines": {
37
37
  "node": ">=20"
38
38
  },
39
- "mcpName": "io.github.sborxyz/sbor"
39
+ "mcpName": "xyz.sbor/sbor",
40
+ "files": [
41
+ "server.mjs",
42
+ "server.json",
43
+ "README.md"
44
+ ]
40
45
  }
package/server.json CHANGED
@@ -1,23 +1,29 @@
1
1
  {
2
2
  "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
- "name": "io.github.sborxyz/sbor",
3
+ "name": "xyz.sbor/sbor",
4
4
  "description": "Check a lending rate before your agent borrows. Stacks benchmark, read from contract state. No key.",
5
5
  "repository": {
6
6
  "url": "https://github.com/sborxyz/sbor",
7
7
  "source": "github",
8
8
  "subfolder": "mcp"
9
9
  },
10
- "version": "1.1.3",
10
+ "version": "1.2.0",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "npm",
14
14
  "identifier": "sbor-mcp",
15
- "version": "1.1.3",
15
+ "version": "1.2.0",
16
16
  "transport": {
17
17
  "type": "stdio"
18
18
  }
19
19
  }
20
20
  ],
21
21
  "title": "SBOR",
22
- "websiteUrl": "https://sbor.xyz"
22
+ "websiteUrl": "https://sbor.xyz",
23
+ "remotes": [
24
+ {
25
+ "type": "streamable-http",
26
+ "url": "https://mcp.sbor.xyz/mcp"
27
+ }
28
+ ]
23
29
  }
package/server.mjs CHANGED
@@ -18,13 +18,24 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
18
18
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
19
19
  import { z } from "zod";
20
20
 
21
- const VERSION = "1.1.3";
21
+ const VERSION = "1.2.0";
22
22
  const BASE = process.env.SBOR_BASE || "https://sbor.xyz";
23
23
  const UA = `sbor-mcp/${VERSION}`;
24
24
  const TIMEOUT_MS = 10_000;
25
25
  const STALE_AFTER_HOURS = 48;
26
26
  const INDICES = ["SBOR-USD", "SBOR-BTC", "SBOR-STX"];
27
27
 
28
+ /* The cost of borrowing USDC against bitcoin, from the Morpho Blue markets on
29
+ Base and Ethereum whose only collateral is cbBTC or WBTC. A reference, not an
30
+ SBOR index, published from 25 September 2026. It is a benchmark an agent can
31
+ compare against, so the tools that take a benchmark accept it too. */
32
+ const BTC_REF = "BTC-COLLATERAL-USDC";
33
+ const BENCHMARKS = [...INDICES, BTC_REF];
34
+ const BTC_REF_WHAT = "a reference, not an SBOR index: what it costs to borrow USDC against bitcoin wrapped by a custodian (cbBTC, WBTC), from the Morpho markets on Base and Ethereum whose only collateral is that bitcoin";
35
+
36
+ /* All SBOR tools only read published data. */
37
+ const READ_ONLY = { readOnlyHint: true, openWorldHint: false };
38
+
28
39
  /* Small cache so an agent asking three questions in a row makes one request. */
29
40
  const cache = new Map();
30
41
  async function getJson(path, ttlMs = 60_000){
@@ -72,6 +83,7 @@ const server = new McpServer({ name: "sbor", version: VERSION });
72
83
  /* ------------------------------------------------------------------ */
73
84
  server.registerTool("get_rate", {
74
85
  title: "Get the current SBOR fixing",
86
+ annotations: READ_ONLY,
75
87
  description:
76
88
  "The benchmark borrow and supply rate for lending on Stacks, read from " +
77
89
  "lending contract state. Use this to judge whether a lending offer is good: " +
@@ -79,15 +91,22 @@ server.registerTool("get_rate", {
79
91
  "supplying below the supply rate means earning less. Returns every currency " +
80
92
  "index unless one is named. Always read the freshness line first.",
81
93
  inputSchema: {
82
- index: z.enum(INDICES).optional().describe("Currency index. Omit for all of them.")
94
+ index: z.enum(BENCHMARKS).optional().describe(`Currency index, or ${BTC_REF} for borrowing USDC against bitcoin on Base and Ethereum. Omit for all of them.`)
83
95
  }
84
96
  }, async ({ index }) => {
85
97
  try {
86
98
  const d = await getJson("/api/v1/latest.json");
99
+ const ref = d.bitcoinCollateralUsdc;
100
+ const refLine = ref && typeof ref.borrow === "number"
101
+ ? `${BTC_REF}: borrow ${pct(ref.borrow)}, supply ${pct(ref.supply)}, depth ${usd(ref.depthUsd)}. This is ${BTC_REF_WHAT}.`
102
+ : `${BTC_REF}: not published in the current fixing${ref ? ", because not every market could be read" : ""}. Treat it as unknown, never as zero.`;
103
+ if (index === BTC_REF)
104
+ return text([freshness(d), refLine, `Source: ${BASE}/api/v1/latest.json`].join("\n"));
87
105
  if (index && !d.indices[index])
88
106
  return text(`${index} is not published in the current fixing. `
89
- + `When a market cannot be read, SBOR omits the index rather than publishing `
90
- + `a figure that is not real. Treat it as unknown, never as zero. `
107
+ + `When a market cannot be read, or its rate is not set by the market, SBOR `
108
+ + `omits the index rather than publishing a figure it cannot stand behind. `
109
+ + `Treat it as unknown, never as zero. `
91
110
  + `Published today: ${Object.keys(d.indices).join(", ")}.`);
92
111
 
93
112
  const wanted = index ? { [index]: d.indices[index] } : d.indices;
@@ -104,6 +123,7 @@ server.registerTool("get_rate", {
104
123
  freshness(d),
105
124
  ...lines,
106
125
  omitted.length ? `Not published: ${omitted.join(", ")}. A market that cannot be read is omitted, not estimated.` : "",
126
+ index ? "" : refLine,
107
127
  d.poxReference ? `Proof of Transfer staking yield ${pct(d.poxReference.apy)}. A staking yield, not a lending rate. Never add it to one.` : "",
108
128
  `Basis: ${d.basis}`,
109
129
  `Source: ${BASE}/api/v1/latest.json`
@@ -116,18 +136,21 @@ server.registerTool("get_rate", {
116
136
  /* ------------------------------------------------------------------ */
117
137
  server.registerTool("compare_rate", {
118
138
  title: "Compare a rate against the SBOR benchmark",
139
+ annotations: READ_ONLY,
119
140
  description:
120
141
  "Given a rate you have been offered, say whether it is above or below the " +
121
142
  "market for that currency, and by how much. This is the main reason SBOR exists. " +
122
143
  "Recommended use: if a borrow offer is more than 50 basis points above the " +
123
144
  "benchmark, stop and ask a human. Use SBOR to stop, never to start. " +
145
+ `For a USDC loan against bitcoin (cbBTC or WBTC) on Base or Ethereum, compare with ${BTC_REF}; ` +
146
+ "for lending on Stacks, with the index for the currency. " +
124
147
  "This tool refuses rather than guesses: an error result means there is no " +
125
148
  "trustworthy answer, not that the rate is bad.",
126
149
  inputSchema: {
127
150
  rate: z.number().gt(0).lte(100)
128
151
  .describe("The rate offered, as a percentage between 0 and 100. 4.2 means 4.2%, not 0.042."),
129
152
  side: z.enum(["borrow", "supply"]).describe("Whether you would be borrowing or supplying."),
130
- index: z.enum(INDICES).describe("Which currency.")
153
+ index: z.enum(BENCHMARKS).describe(`Which benchmark: a Stacks currency index, or ${BTC_REF} for borrowing USDC against bitcoin on Base and Ethereum.`)
131
154
  }
132
155
  }, async ({ rate, side, index }) => {
133
156
  try {
@@ -143,7 +166,16 @@ server.registerTool("compare_rate", {
143
166
  if (a > STALE_AFTER_HOURS)
144
167
  return refuse(`The last fixing is ${a.toFixed(1)} hours old, past the ${STALE_AFTER_HOURS} hour limit. No verdict on stale data. Fall back to your own logic.`);
145
168
 
146
- const ix = d.indices[index];
169
+ /* The bitcoin-collateral reference has the same shape where it matters:
170
+ a borrow and supply rate, and markets. */
171
+ const isRef = index === BTC_REF;
172
+ const ref = d.bitcoinCollateralUsdc;
173
+ const ix = isRef
174
+ ? (ref && typeof ref.borrow === "number"
175
+ ? { borrow: ref.borrow, supply: ref.supply, venues: ref.markets.map(m => m.chain),
176
+ markets: ref.markets.map(m => ({ ...m, venue: `Morpho on ${m.chain}`, asset: `${m.collateral}/USDC` })) }
177
+ : null)
178
+ : d.indices[index];
147
179
  if (!ix)
148
180
  return refuse(`${index} is not published in the current fixing, so there is no benchmark to compare against. `
149
181
  + `Treat this as unknown, not as zero. Published today: ${Object.keys(d.indices).join(", ")}.`);
@@ -185,7 +217,8 @@ server.registerTool("compare_rate", {
185
217
  `${index} ${side} benchmark is ${pct(bench)}. Your ${rate.toFixed(2)}% is ${verdict}.`,
186
218
  stop,
187
219
  best ? `Best constituent today: ${best.venue} ${best.asset} at ${pct(best[side])}, utilization ${pct(best.utilization)}.` : "",
188
- ix.venues.length === 1 ? `Note: this index covers one venue, so it is a reading of that venue rather than a market average.` : "",
220
+ !isRef && ix.venues.length === 1 ? `Note: this index covers one venue, so it is a reading of that venue rather than a market average.` : "",
221
+ isRef ? `${BTC_REF} is ${BTC_REF_WHAT}.` : "",
189
222
  freshness(d)
190
223
  ].filter(Boolean).join("\n"));
191
224
  } catch (e) { return fail(e); }
@@ -196,15 +229,26 @@ server.registerTool("compare_rate", {
196
229
  /* ------------------------------------------------------------------ */
197
230
  server.registerTool("list_markets", {
198
231
  title: "List the lending markets behind a rate",
232
+ annotations: READ_ONLY,
199
233
  description:
200
234
  "Every venue and asset in an index, with its borrow rate, supply rate, " +
201
235
  "utilization, depth and weight. Utilization explains why a rate sits where it does.",
202
236
  inputSchema: {
203
- index: z.enum(INDICES).optional().describe("Currency index. Omit for all of them.")
237
+ index: z.enum(BENCHMARKS).optional().describe(`Currency index, or ${BTC_REF}. Omit for the Stacks indices.`)
204
238
  }
205
239
  }, async ({ index }) => {
206
240
  try {
207
241
  const d = await getJson("/api/v1/latest.json");
242
+ if (index === BTC_REF){
243
+ const ref = d.bitcoinCollateralUsdc;
244
+ if (!ref?.markets?.length) return text(`${BTC_REF} is not published in the current fixing. Treat it as unknown, not as zero.`);
245
+ return text([freshness(d), "", `${BTC_REF}, ${BTC_REF_WHAT}.`, ...ref.markets.map(m =>
246
+ ` Morpho on ${m.chain}, ${m.collateral}/USDC: borrow ${pct(m.borrow)}, supply ${pct(m.supply)}, `
247
+ + `utilization ${pct(m.utilization)}, depth ${usd(m.depthUsd)}`
248
+ + (typeof m.weight === "number" ? `, weight ${(m.weight * 100).toFixed(1)}%` : "")),
249
+ ref.notRead ? `Not read today: ${ref.notRead.join("; ")}. The reference is withheld until every market is read.` : "",
250
+ "", `Source: ${BASE}/api/v1/latest.json`].filter(Boolean).join("\n"));
251
+ }
208
252
  const entries = index
209
253
  ? (d.indices[index] ? [[index, d.indices[index]]] : [])
210
254
  : Object.entries(d.indices);
@@ -225,6 +269,7 @@ server.registerTool("list_markets", {
225
269
  /* ------------------------------------------------------------------ */
226
270
  server.registerTool("get_history", {
227
271
  title: "Get the SBOR history",
272
+ annotations: READ_ONLY,
228
273
  description:
229
274
  "Daily fixings since the index began. Use this to see whether a rate is " +
230
275
  "unusual, or how the cost of capital has moved. Means are given per " +
@@ -279,11 +324,14 @@ server.registerTool("get_history", {
279
324
  /* ------------------------------------------------------------------ */
280
325
  server.registerTool("compare_chains", {
281
326
  title: "Compare Stacks rates against the same markets on other chains",
327
+ annotations: READ_ONLY,
282
328
  description:
283
329
  "The same asset classes on the largest lending markets on Ethereum, Base, " +
284
- "Hyperliquid and Solana, plus SOFR, the US repo rate. Context only: none of " +
285
- "these is ever a constituent of an SBOR index, and the other chains come from " +
286
- "DefiLlama rather than contract state, so small differences are expected.",
330
+ "Hyperliquid and Solana, plus SOFR, the US repo rate, and what it costs to " +
331
+ "borrow USDC against bitcoin on Base and Ethereum. Context only: none of " +
332
+ "these is ever a constituent of an SBOR index. Aave on Ethereum and Base and " +
333
+ "the bitcoin-collateral markets are read from contract state; the other venues " +
334
+ "come from DefiLlama, so small differences are expected for those.",
287
335
  inputSchema: {
288
336
  index: z.enum(INDICES).optional().describe("Only markets comparable to this index. Omit for all.")
289
337
  }
@@ -306,9 +354,15 @@ server.registerTool("compare_chains", {
306
354
  + ` A cheaper rate on Stacks reflects lower utilization, not lower risk.`
307
355
  : "";
308
356
 
357
+ const ref = d.bitcoinCollateralUsdc;
358
+ const refLine = ref && typeof ref.borrow === "number" && (!index || index === "SBOR-USD")
359
+ ? `Borrowing USDC against bitcoin (${BTC_REF}): ${pct(ref.borrow)}, depth ${usd(ref.depthUsd)}, from Morpho on Base and Ethereum, read from the contracts. A dollar on Stacks is borrowed against any crypto collateral, not only bitcoin.`
360
+ : "";
361
+
309
362
  return text([
310
363
  freshness(d), "",
311
364
  "Stacks", ...stacks, "",
365
+ ...(refLine ? [refLine, ""] : []),
312
366
  ext.length ? "Elsewhere" : "", ...ext, ext.length ? "" : "",
313
367
  sofr,
314
368
  d.externalReference?.note ?? ""
@@ -321,6 +375,7 @@ server.registerTool("compare_chains", {
321
375
  /* ------------------------------------------------------------------ */
322
376
  server.registerTool("get_methodology", {
323
377
  title: "How SBOR is calculated",
378
+ annotations: READ_ONLY,
324
379
  description:
325
380
  "The full methodology and integration policy: how the fixing is built, " +
326
381
  "what is excluded and why, and what SBOR will and will not do. Read this " +