first-light-mcp 1.0.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 +148 -0
- package/package.json +35 -0
- package/src/index.js +474 -0
package/README.md
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# First Light MCP
|
|
2
|
+
|
|
3
|
+
Native AI access to **BTX (Byron Bay pool) chain intelligence**. This is a
|
|
4
|
+
[Model Context Protocol](https://modelcontextprotocol.io) server that exposes the
|
|
5
|
+
First Light / BTX explorer REST API as a catalog of self-describing tools, so any
|
|
6
|
+
MCP client — Claude Desktop, an IDE agent, or your own framework — can connect and
|
|
7
|
+
query BTX data natively, without hand-building HTTP calls.
|
|
8
|
+
|
|
9
|
+
It is a **thin, stateless translation layer**. It holds no chain data and makes no
|
|
10
|
+
auth decisions of its own: each tool call maps to one authenticated request against
|
|
11
|
+
the live API (`https://api-explorer.btxbyronbay.com/api`), forwarding your Bearer
|
|
12
|
+
token unchanged, and returns the JSON **with its `caveat` intact**.
|
|
13
|
+
|
|
14
|
+
## Neutral-facts contract
|
|
15
|
+
|
|
16
|
+
Every tool returns **provable on-chain observations only** — never identity,
|
|
17
|
+
ownership, holdings, counterparty, accusation, risk verdict, or advice. Each result
|
|
18
|
+
is wrapped as:
|
|
19
|
+
|
|
20
|
+
```jsonc
|
|
21
|
+
{
|
|
22
|
+
"observation": { /* the API payload */ },
|
|
23
|
+
"caveat": "Neutral on-chain facts only. No entity names, identity, ownership, …",
|
|
24
|
+
"framing": "This is an on-chain observation, not a statement of identity, …"
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The `caveat` comes from the API; if an endpoint omits one, the canonical caveat is
|
|
29
|
+
injected so **no result ever leaves without it**. This contract is also stated once
|
|
30
|
+
in the server's `instructions`, advertised to the model at connect time.
|
|
31
|
+
|
|
32
|
+
## Requirements
|
|
33
|
+
|
|
34
|
+
- Node.js **18+** (uses global `fetch`).
|
|
35
|
+
- Optional: a First Light **Bearer token** for PRO/Enterprise tools. Public tools
|
|
36
|
+
work with no token.
|
|
37
|
+
|
|
38
|
+
## Install
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# Run directly with npx (no install):
|
|
42
|
+
npx first-light-mcp
|
|
43
|
+
|
|
44
|
+
# …or install globally:
|
|
45
|
+
npm install -g first-light-mcp
|
|
46
|
+
first-light-mcp
|
|
47
|
+
|
|
48
|
+
# …or from this folder:
|
|
49
|
+
npm install
|
|
50
|
+
npm start
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The server speaks MCP over **stdio** — an MCP client launches it as a subprocess.
|
|
54
|
+
Running it in a plain terminal just prints a readiness line to stderr and then waits
|
|
55
|
+
for JSON-RPC on stdin (that's expected; press Ctrl-C to exit).
|
|
56
|
+
|
|
57
|
+
## Configure your token
|
|
58
|
+
|
|
59
|
+
Set the token in the environment the client launches the server with:
|
|
60
|
+
|
|
61
|
+
| Variable | Purpose |
|
|
62
|
+
|---|---|
|
|
63
|
+
| `FIRST_LIGHT_TOKEN` | Your `flp_…` Bearer key. Sent as `Authorization: Bearer <token>` on every call. Omit for public-only use. |
|
|
64
|
+
| `FIRST_LIGHT_API_BASE` | Optional API base override (default `https://api-explorer.btxbyronbay.com/api`). |
|
|
65
|
+
| `FIRST_LIGHT_TIMEOUT_MS` | Optional per-request timeout in ms (default `30000`). |
|
|
66
|
+
|
|
67
|
+
If a PRO/Enterprise tool is called without a usable token, the result is a clean
|
|
68
|
+
`{ "error": "auth", "detail": "This tool needs a PRO/Enterprise token…" }` object
|
|
69
|
+
(a `403` from the API surfaces as `{ "error": "tier", … }`) — not data.
|
|
70
|
+
|
|
71
|
+
## Add to Claude Desktop
|
|
72
|
+
|
|
73
|
+
Edit `claude_desktop_config.json`:
|
|
74
|
+
|
|
75
|
+
- **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
76
|
+
- **Windows:** `%APPDATA%\Claude\claude_desktop_config.json`
|
|
77
|
+
|
|
78
|
+
```jsonc
|
|
79
|
+
{
|
|
80
|
+
"mcpServers": {
|
|
81
|
+
"first-light": {
|
|
82
|
+
"command": "npx",
|
|
83
|
+
"args": ["-y", "first-light-mcp"],
|
|
84
|
+
"env": {
|
|
85
|
+
"FIRST_LIGHT_TOKEN": "flp_your_token_here"
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Restart Claude Desktop. The First Light tools appear in the tools (plug) menu.
|
|
93
|
+
|
|
94
|
+
> Prefer not to use `npx`? Set `"command": "node"` and
|
|
95
|
+
> `"args": ["C:/path/to/first-light-mcp/src/index.js"]` (or the global
|
|
96
|
+
> `"command": "first-light-mcp"` after `npm install -g`).
|
|
97
|
+
|
|
98
|
+
The same block works for any MCP client that uses the standard
|
|
99
|
+
`command` / `args` / `env` stdio launcher (Cline, Continue, custom SDK clients, …).
|
|
100
|
+
|
|
101
|
+
## Tools
|
|
102
|
+
|
|
103
|
+
Public tools (🟢) need no token. Gated tools (🔒) need a PRO/Enterprise token.
|
|
104
|
+
|
|
105
|
+
### Public chain
|
|
106
|
+
| Tool | Endpoint | What it answers |
|
|
107
|
+
|---|---|---|
|
|
108
|
+
| 🟢 `network_glance` | `/network/glance` | Height, interval, difficulty, supply, hashrate right now. |
|
|
109
|
+
| 🟢 `get_block` | `/block/{ident}` | Block detail by height **or** hash. |
|
|
110
|
+
| 🟢 `get_tx` | `/tx/{txid}` | Transaction detail with prevouts resolved. |
|
|
111
|
+
| 🟢 `get_address` | `/address/{addr}` | Address aggregate + paginated tx rows (`page`). |
|
|
112
|
+
| 🟢 `blocks_latest` | `/blocks/latest` | Latest N blocks (`n`). |
|
|
113
|
+
| 🟢 `txs_latest` | `/txs/latest` | Latest N confirmed txs (`n`). |
|
|
114
|
+
| 🟢 `search` | `/search` | Resolve a height / hash / txid / address (`q`). |
|
|
115
|
+
| 🟢 `network_supply` | `/network/supply` | Emission & halving analytic. |
|
|
116
|
+
| 🟢 `mempool` | `/mempool` | Mempool summary + recent entries. |
|
|
117
|
+
|
|
118
|
+
### Market / data
|
|
119
|
+
| Tool | Endpoint | What it answers |
|
|
120
|
+
|---|---|---|
|
|
121
|
+
| 🟢 `market_summary` | `/market/summary` | Live exchange (SafeTrade) bid/ask/mid/last — indicative, thin market. |
|
|
122
|
+
| 🔒 `liquidity` | `/pro/liquidity` | On-chain liquidity funnel: available-float estimate vs venue depth. |
|
|
123
|
+
| 🔒 `usage` | `/pro/usage` | BTX usage view — neutral activity categories & trends. |
|
|
124
|
+
| 🟢 `wbtx_summary` | `/wbtx/summary` | wBTX bridge summary, both chains (testnet backing signal). |
|
|
125
|
+
| 🟢 `supply_demand` | `/supply-demand` | Supply map: cohorts, net-flow, HODL waves (`window`, `price=model\|market`). |
|
|
126
|
+
|
|
127
|
+
### Provenance
|
|
128
|
+
| Tool | Endpoint | What it answers |
|
|
129
|
+
|---|---|---|
|
|
130
|
+
| 🟢 `trace` | `/trace/{txid}` | Bounded fund tracer forward/back over the spend index (`dir`, `hops`). |
|
|
131
|
+
| 🔒 `cluster` | `/cluster/{addr}` | Co-spend set — **NOT** proof of common ownership (`format`). |
|
|
132
|
+
|
|
133
|
+
> `trace` and `cluster` are the provenance lenses. `trace` is a public bounded
|
|
134
|
+
> tracer (a token unlocks deeper tiers); `cluster` requires a PRO/Enterprise token.
|
|
135
|
+
|
|
136
|
+
## What this is / isn't
|
|
137
|
+
|
|
138
|
+
- **Is:** a stateless stdio adapter over the live First Light API; reuses the API's
|
|
139
|
+
existing auth, rate limits, tier gates, and neutral caveats verbatim.
|
|
140
|
+
- **Isn't:** a data store, a second source of truth, or an auth bypass. Every gate
|
|
141
|
+
is decided downstream by the API.
|
|
142
|
+
|
|
143
|
+
A **hosted remote-MCP variant** (HTTP/SSE, so cloud agents can connect without
|
|
144
|
+
running a subprocess) is a possible follow-up; this package is the stdio server.
|
|
145
|
+
|
|
146
|
+
## License
|
|
147
|
+
|
|
148
|
+
MIT.
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "first-light-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Model Context Protocol server exposing the First Light / BTX explorer chain-intelligence API as native, self-describing AI tools. Neutral on-chain facts only — every result carries its caveat.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"bin": {
|
|
8
|
+
"first-light-mcp": "src/index.js"
|
|
9
|
+
},
|
|
10
|
+
"main": "src/index.js",
|
|
11
|
+
"files": [
|
|
12
|
+
"src/",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"start": "node src/index.js",
|
|
17
|
+
"check": "node --check src/index.js"
|
|
18
|
+
},
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=18"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"mcp",
|
|
24
|
+
"model-context-protocol",
|
|
25
|
+
"btx",
|
|
26
|
+
"blockchain",
|
|
27
|
+
"explorer",
|
|
28
|
+
"first-light",
|
|
29
|
+
"chain-intelligence",
|
|
30
|
+
"claude"
|
|
31
|
+
],
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@modelcontextprotocol/sdk": "^1.12.0"
|
|
34
|
+
}
|
|
35
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* First Light MCP server
|
|
4
|
+
* ----------------------
|
|
5
|
+
* A thin, stateless Model Context Protocol (stdio) server that turns the First
|
|
6
|
+
* Light / BTX explorer REST API into native, self-describing AI tools.
|
|
7
|
+
*
|
|
8
|
+
* It holds no chain data and makes no auth decisions of its own: every request
|
|
9
|
+
* is forwarded to the live API (https://api-explorer.btxbyronbay.com/api) with
|
|
10
|
+
* the caller's `FIRST_LIGHT_TOKEN` as `Authorization: Bearer <token>`. Every gate
|
|
11
|
+
* (401 / 403 / 429) is decided downstream by the API. Every result is returned
|
|
12
|
+
* with its neutral `caveat` intact — provable on-chain facts only, never identity,
|
|
13
|
+
* ownership, holdings, or a risk verdict.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
17
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
18
|
+
import {
|
|
19
|
+
ListToolsRequestSchema,
|
|
20
|
+
CallToolRequestSchema,
|
|
21
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
22
|
+
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Config
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
const API_BASE = (
|
|
28
|
+
process.env.FIRST_LIGHT_API_BASE || "https://api-explorer.btxbyronbay.com/api"
|
|
29
|
+
).replace(/\/+$/, "");
|
|
30
|
+
const TOKEN = process.env.FIRST_LIGHT_TOKEN || "";
|
|
31
|
+
const NAME = "first-light-mcp";
|
|
32
|
+
const VERSION = "1.0.0";
|
|
33
|
+
const REQUEST_TIMEOUT_MS = Number(process.env.FIRST_LIGHT_TIMEOUT_MS || 30000);
|
|
34
|
+
|
|
35
|
+
// The neutral-facts contract, injected on any payload that lacks its own caveat
|
|
36
|
+
// (e.g. some public endpoints) so no tool result ever leaves without one.
|
|
37
|
+
const CANONICAL_CAVEAT =
|
|
38
|
+
"Neutral on-chain facts only. No entity names, identity, ownership, holdings, " +
|
|
39
|
+
"or risk/taint claims are asserted or implied.";
|
|
40
|
+
|
|
41
|
+
// Framing string attached to EVERY tool result, so the model reasons over the
|
|
42
|
+
// observation as an observation — never as an accusation, verdict, or advice.
|
|
43
|
+
const FRAMING =
|
|
44
|
+
"This is an on-chain observation, not a statement of identity, ownership, " +
|
|
45
|
+
"counterparty, holdings, risk, or a recommendation. Do not present it as an " +
|
|
46
|
+
"accusation, a compliance determination, or advice.";
|
|
47
|
+
|
|
48
|
+
const SERVER_INSTRUCTIONS = [
|
|
49
|
+
"First Light MCP exposes the BTX (Byron Bay pool) explorer chain-intelligence API",
|
|
50
|
+
"as native tools. It is a stateless translation layer over the live REST API.",
|
|
51
|
+
"",
|
|
52
|
+
"NEUTRAL-FACTS CONTRACT (hard product principle): every tool returns provable",
|
|
53
|
+
"on-chain observations ONLY. Never present a result as an identity, ownership,",
|
|
54
|
+
"holdings, counterparty, accusation, risk verdict, or advice. Each result carries",
|
|
55
|
+
"a `caveat` field (from the API, or the canonical caveat if the endpoint omits one)",
|
|
56
|
+
"and a `framing` note — surface them; do not strip, summarize, or relocate them.",
|
|
57
|
+
"",
|
|
58
|
+
"AUTH: public tools work with no token. Tools marked PRO/Enterprise require a valid",
|
|
59
|
+
"`FIRST_LIGHT_TOKEN` (Bearer key). If a gated tool is called without a usable token",
|
|
60
|
+
"the result is a clean `{ error: 'auth' | 'tier', ... }` object, not data.",
|
|
61
|
+
].join("\n");
|
|
62
|
+
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
// Tool registry — one tool = one question. Each entry:
|
|
65
|
+
// name, gated (needs a token), description (LLM-facing, ends with neutrality),
|
|
66
|
+
// inputSchema (JSON Schema), build(args) -> { path, query }
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
const S = (props = {}, required = []) => ({
|
|
70
|
+
type: "object",
|
|
71
|
+
properties: props,
|
|
72
|
+
required,
|
|
73
|
+
additionalProperties: false,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const TOOLS = [
|
|
77
|
+
// ---- Public chain -------------------------------------------------------
|
|
78
|
+
{
|
|
79
|
+
name: "network_glance",
|
|
80
|
+
gated: false,
|
|
81
|
+
description:
|
|
82
|
+
"Network glance: current height, block interval/target, difficulty, circulating supply, hashrate (matrix ops), last-block timing. Use for a fast 'how is the BTX chain doing right now' summary. A neutral network observation.",
|
|
83
|
+
inputSchema: S(),
|
|
84
|
+
build: () => ({ path: "/network/glance" }),
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: "get_block",
|
|
88
|
+
gated: false,
|
|
89
|
+
description:
|
|
90
|
+
"Block detail by height OR block hash. Pass a decimal height (e.g. '155314') or a 64-hex block hash. Returns header, tx list, and metadata. A neutral on-chain fact.",
|
|
91
|
+
inputSchema: S(
|
|
92
|
+
{
|
|
93
|
+
ident: {
|
|
94
|
+
type: "string",
|
|
95
|
+
description: "Block height (decimal) or 64-hex block hash.",
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
["ident"]
|
|
99
|
+
),
|
|
100
|
+
build: (a) => ({ path: `/block/${encodeURIComponent(a.ident)}` }),
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: "get_tx",
|
|
104
|
+
gated: false,
|
|
105
|
+
description:
|
|
106
|
+
"Transaction detail for a txid, with prevouts (inputs) resolved to their source addresses and values. A neutral on-chain fact.",
|
|
107
|
+
inputSchema: S(
|
|
108
|
+
{ txid: { type: "string", description: "64-hex transaction id." } },
|
|
109
|
+
["txid"]
|
|
110
|
+
),
|
|
111
|
+
build: (a) => ({ path: `/tx/${encodeURIComponent(a.txid)}` }),
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
name: "get_address",
|
|
115
|
+
gated: false,
|
|
116
|
+
description:
|
|
117
|
+
"Address page: aggregate totals (received/sent/balance) plus a paginated list of the address's transaction rows. `page` is optional (0-based). A neutral on-chain fact, not an ownership or identity claim.",
|
|
118
|
+
inputSchema: S(
|
|
119
|
+
{
|
|
120
|
+
addr: { type: "string", description: "BTX address." },
|
|
121
|
+
page: {
|
|
122
|
+
type: "integer",
|
|
123
|
+
minimum: 0,
|
|
124
|
+
description: "Optional page number for the tx rows (0-based).",
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
["addr"]
|
|
128
|
+
),
|
|
129
|
+
build: (a) => ({
|
|
130
|
+
path: `/address/${encodeURIComponent(a.addr)}`,
|
|
131
|
+
query: { page: a.page },
|
|
132
|
+
}),
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
name: "blocks_latest",
|
|
136
|
+
gated: false,
|
|
137
|
+
description:
|
|
138
|
+
"The latest N confirmed blocks (most recent first). `n` is optional. A neutral on-chain fact.",
|
|
139
|
+
inputSchema: S({
|
|
140
|
+
n: {
|
|
141
|
+
type: "integer",
|
|
142
|
+
minimum: 1,
|
|
143
|
+
maximum: 100,
|
|
144
|
+
description: "How many recent blocks to return (default server-side).",
|
|
145
|
+
},
|
|
146
|
+
}),
|
|
147
|
+
build: (a) => ({ path: "/blocks/latest", query: { n: a.n } }),
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
name: "txs_latest",
|
|
151
|
+
gated: false,
|
|
152
|
+
description:
|
|
153
|
+
"The latest N confirmed transactions (most recent first). `n` is optional. A neutral on-chain fact.",
|
|
154
|
+
inputSchema: S({
|
|
155
|
+
n: {
|
|
156
|
+
type: "integer",
|
|
157
|
+
minimum: 1,
|
|
158
|
+
maximum: 100,
|
|
159
|
+
description: "How many recent transactions to return (default server-side).",
|
|
160
|
+
},
|
|
161
|
+
}),
|
|
162
|
+
build: (a) => ({ path: "/txs/latest", query: { n: a.n } }),
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
name: "search",
|
|
166
|
+
gated: false,
|
|
167
|
+
description:
|
|
168
|
+
"Universal search over the chain: resolves a query that is a block height, block hash, txid, or address to the right entity. Use when you have an identifier but don't know its type. A neutral lookup.",
|
|
169
|
+
inputSchema: S(
|
|
170
|
+
{
|
|
171
|
+
q: {
|
|
172
|
+
type: "string",
|
|
173
|
+
description: "Height, block hash, txid, or address to resolve.",
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
["q"]
|
|
177
|
+
),
|
|
178
|
+
build: (a) => ({ path: "/search", query: { q: a.q } }),
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
name: "network_supply",
|
|
182
|
+
gated: false,
|
|
183
|
+
description:
|
|
184
|
+
"Emission & halving analytic: circulating/issued supply, emission schedule, halving context. A neutral supply arithmetic observation.",
|
|
185
|
+
inputSchema: S(),
|
|
186
|
+
build: () => ({ path: "/network/supply" }),
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
name: "mempool",
|
|
190
|
+
gated: false,
|
|
191
|
+
description:
|
|
192
|
+
"Mempool summary plus recent unconfirmed entries (size, fee context, sample of pending txs). A neutral node-vantage observation of the current mempool.",
|
|
193
|
+
inputSchema: S(),
|
|
194
|
+
build: () => ({ path: "/mempool" }),
|
|
195
|
+
},
|
|
196
|
+
|
|
197
|
+
// ---- Market / data ------------------------------------------------------
|
|
198
|
+
{
|
|
199
|
+
name: "market_summary",
|
|
200
|
+
gated: false,
|
|
201
|
+
description:
|
|
202
|
+
"Real market price summary from live exchange venue(s) (SafeTrade): best bid/ask, mid, last trade, index price, venue count. NOTE the quote is a USD stablecoin (~1:1 with USD, a label not a USD feed) and the market is thin/newly-listed — treat the price as indicative, not a deep-liquidity mark. Distinct from `/price` (the banked model price). Read the caveat.",
|
|
203
|
+
inputSchema: S(),
|
|
204
|
+
build: () => ({ path: "/market/summary" }),
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
name: "liquidity",
|
|
208
|
+
gated: true,
|
|
209
|
+
description:
|
|
210
|
+
"PRO/Enterprise. On-chain liquidity funnel: an ESTIMATE of plausibly-available (recently-moved unspent) supply versus single-venue order-book depth/spread/volume. A proxy for available float, NOT a claim any of it is offered for sale, and NOT price discovery. Read the caveat.",
|
|
211
|
+
inputSchema: S(),
|
|
212
|
+
build: () => ({ path: "/pro/liquidity" }),
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
name: "usage",
|
|
216
|
+
gated: true,
|
|
217
|
+
description:
|
|
218
|
+
"PRO/Enterprise. BTX usage view: how transparent on-chain value is being used, bucketed into neutral activity categories (e.g. exchange-flow, mined-and-held) with trends. BTX is mostly mined-and-held; categories that are 'not yet measurable' say so explicitly. Neutral activity aggregates, not identity. Read the caveat.",
|
|
219
|
+
inputSchema: S(),
|
|
220
|
+
build: () => ({ path: "/pro/usage" }),
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
name: "wbtx_summary",
|
|
224
|
+
gated: false,
|
|
225
|
+
description:
|
|
226
|
+
"wBTX bridge summary across both chains: bridged supply, reserve/backing signal, freshness, bridge address. NOTE: testnet data only — no mainnet, no real backing; the reserve badge is a mechanical backing-vs-supply + freshness signal, not an audit. Read the caveat.",
|
|
227
|
+
inputSchema: S(),
|
|
228
|
+
build: () => ({ path: "/wbtx/summary" }),
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
name: "supply_demand",
|
|
232
|
+
gated: false,
|
|
233
|
+
description:
|
|
234
|
+
"On-chain supply map: coin-age & balance cohorts, net-flow, HODL waves — optionally priced. `window` selects the lookback; `price` chooses 'model' (banked model price) or 'market' (live exchange price) for any USD-denominated figures. A neutral supply-structure observation, not trading advice.",
|
|
235
|
+
inputSchema: S({
|
|
236
|
+
window: {
|
|
237
|
+
type: "string",
|
|
238
|
+
description: "Lookback window (e.g. '24h', '7d', '30d', '1y', 'all').",
|
|
239
|
+
},
|
|
240
|
+
price: {
|
|
241
|
+
type: "string",
|
|
242
|
+
enum: ["model", "market"],
|
|
243
|
+
description: "Price source for USD figures: 'model' or 'market'.",
|
|
244
|
+
},
|
|
245
|
+
}),
|
|
246
|
+
build: (a) => ({
|
|
247
|
+
path: "/supply-demand",
|
|
248
|
+
query: { window: a.window, price: a.price },
|
|
249
|
+
}),
|
|
250
|
+
},
|
|
251
|
+
|
|
252
|
+
// ---- Provenance (PRO) ---------------------------------------------------
|
|
253
|
+
{
|
|
254
|
+
name: "trace",
|
|
255
|
+
gated: false, // public tracer per API; token forwarded when present for deeper tiers
|
|
256
|
+
description:
|
|
257
|
+
"Bounded fund tracer over the spend index: follows value forward or backward from a txid across a limited number of hops. `dir` = 'forward' (where funds went) or 'back' (where funds came from); `hops` bounds depth (clamped server-side to your tier). 'Traceable' is a proportional flow share on provable edges, NEVER an ownership, sender, or counterparty assertion.",
|
|
258
|
+
inputSchema: S(
|
|
259
|
+
{
|
|
260
|
+
txid: { type: "string", description: "64-hex transaction id to trace from." },
|
|
261
|
+
dir: {
|
|
262
|
+
type: "string",
|
|
263
|
+
enum: ["forward", "back"],
|
|
264
|
+
description: "Trace direction: 'forward' or 'back'.",
|
|
265
|
+
},
|
|
266
|
+
hops: {
|
|
267
|
+
type: "integer",
|
|
268
|
+
minimum: 1,
|
|
269
|
+
description: "Max hops to traverse (clamped to your tier).",
|
|
270
|
+
},
|
|
271
|
+
},
|
|
272
|
+
["txid"]
|
|
273
|
+
),
|
|
274
|
+
build: (a) => ({
|
|
275
|
+
path: `/trace/${encodeURIComponent(a.txid)}`,
|
|
276
|
+
query: { dir: a.dir, hops: a.hops },
|
|
277
|
+
}),
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
name: "cluster",
|
|
281
|
+
gated: true,
|
|
282
|
+
description:
|
|
283
|
+
"PRO/Enterprise. Co-spend set: other addresses that appeared together as inputs in the same transaction(s) as the queried address. This is a provable on-chain co-occurrence and is explicitly NOT proof of common ownership — never state or imply the addresses share an owner. `format` optionally selects the response shape.",
|
|
284
|
+
inputSchema: S(
|
|
285
|
+
{
|
|
286
|
+
addr: { type: "string", description: "BTX address to build the co-spend set for." },
|
|
287
|
+
format: {
|
|
288
|
+
type: "string",
|
|
289
|
+
description: "Optional response format selector supported by the API.",
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
["addr"]
|
|
293
|
+
),
|
|
294
|
+
build: (a) => ({
|
|
295
|
+
path: `/cluster/${encodeURIComponent(a.addr)}`,
|
|
296
|
+
query: { format: a.format },
|
|
297
|
+
}),
|
|
298
|
+
},
|
|
299
|
+
];
|
|
300
|
+
|
|
301
|
+
const TOOL_BY_NAME = new Map(TOOLS.map((t) => [t.name, t]));
|
|
302
|
+
|
|
303
|
+
// ---------------------------------------------------------------------------
|
|
304
|
+
// HTTP client + neutral-facts wrapping
|
|
305
|
+
// ---------------------------------------------------------------------------
|
|
306
|
+
|
|
307
|
+
function buildUrl(path, query) {
|
|
308
|
+
const url = new URL(API_BASE + path);
|
|
309
|
+
for (const [k, v] of Object.entries(query || {})) {
|
|
310
|
+
if (v !== undefined && v !== null && v !== "") url.searchParams.set(k, String(v));
|
|
311
|
+
}
|
|
312
|
+
return url;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** Wrap an API payload so the caveat is unavoidable and framing is always present. */
|
|
316
|
+
function wrapObservation(payload) {
|
|
317
|
+
let caveat = CANONICAL_CAVEAT;
|
|
318
|
+
if (payload && typeof payload === "object" && !Array.isArray(payload)) {
|
|
319
|
+
if (payload.caveat !== undefined && payload.caveat !== null && payload.caveat !== "") {
|
|
320
|
+
caveat = payload.caveat;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return { observation: payload, caveat, framing: FRAMING };
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** Perform the GET and return a structured result object (never throws for HTTP status). */
|
|
327
|
+
async function apiGet(tool, args) {
|
|
328
|
+
const { path, query } = tool.build(args);
|
|
329
|
+
const url = buildUrl(path, query);
|
|
330
|
+
|
|
331
|
+
const headers = {
|
|
332
|
+
Accept: "application/json",
|
|
333
|
+
"User-Agent": `${NAME}/${VERSION}`,
|
|
334
|
+
};
|
|
335
|
+
if (TOKEN) headers.Authorization = `Bearer ${TOKEN}`;
|
|
336
|
+
|
|
337
|
+
const controller = new AbortController();
|
|
338
|
+
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
339
|
+
let res, bodyText;
|
|
340
|
+
try {
|
|
341
|
+
res = await fetch(url, { headers, signal: controller.signal });
|
|
342
|
+
bodyText = await res.text();
|
|
343
|
+
} catch (err) {
|
|
344
|
+
clearTimeout(timer);
|
|
345
|
+
const aborted = err && err.name === "AbortError";
|
|
346
|
+
return {
|
|
347
|
+
isError: true,
|
|
348
|
+
payload: {
|
|
349
|
+
error: "network",
|
|
350
|
+
detail: aborted
|
|
351
|
+
? `Request timed out after ${REQUEST_TIMEOUT_MS}ms`
|
|
352
|
+
: `Failed to reach the First Light API: ${err && err.message ? err.message : String(err)}`,
|
|
353
|
+
endpoint: path,
|
|
354
|
+
},
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
clearTimeout(timer);
|
|
358
|
+
|
|
359
|
+
let json;
|
|
360
|
+
try {
|
|
361
|
+
json = bodyText ? JSON.parse(bodyText) : null;
|
|
362
|
+
} catch {
|
|
363
|
+
json = { raw: bodyText };
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Map the API status codes to a clean, model-legible error surface.
|
|
367
|
+
if (res.status === 401) {
|
|
368
|
+
return {
|
|
369
|
+
isError: true,
|
|
370
|
+
payload: {
|
|
371
|
+
error: "auth",
|
|
372
|
+
detail: TOKEN
|
|
373
|
+
? "The First Light API rejected the token (invalid, revoked, or expired). Check FIRST_LIGHT_TOKEN."
|
|
374
|
+
: "This tool needs a PRO/Enterprise token. Set the FIRST_LIGHT_TOKEN environment variable to your First Light Bearer key.",
|
|
375
|
+
endpoint: path,
|
|
376
|
+
},
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
if (res.status === 403) {
|
|
380
|
+
return {
|
|
381
|
+
isError: true,
|
|
382
|
+
payload: {
|
|
383
|
+
error: "tier",
|
|
384
|
+
detail:
|
|
385
|
+
"This tool needs a higher tier than your token allows (PRO/Enterprise). " +
|
|
386
|
+
((json && (json.detail || json.error)) || "Contact the operator to upgrade."),
|
|
387
|
+
endpoint: path,
|
|
388
|
+
},
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
if (res.status === 429) {
|
|
392
|
+
const retryAfter = res.headers.get("retry-after");
|
|
393
|
+
return {
|
|
394
|
+
isError: true,
|
|
395
|
+
payload: {
|
|
396
|
+
error: "rate_limited",
|
|
397
|
+
detail: "Per-key rate limit exceeded; back off and retry.",
|
|
398
|
+
retryAfter: retryAfter ? Number(retryAfter) : undefined,
|
|
399
|
+
endpoint: path,
|
|
400
|
+
},
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
if (res.status === 400) {
|
|
404
|
+
return {
|
|
405
|
+
isError: true,
|
|
406
|
+
payload: {
|
|
407
|
+
error: "bad_request",
|
|
408
|
+
detail: (json && (json.detail || json.error)) || "Invalid request parameters.",
|
|
409
|
+
endpoint: path,
|
|
410
|
+
},
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
if (!res.ok) {
|
|
414
|
+
return {
|
|
415
|
+
isError: true,
|
|
416
|
+
payload: {
|
|
417
|
+
error: "http_" + res.status,
|
|
418
|
+
detail: (json && (json.detail || json.error)) || bodyText || res.statusText,
|
|
419
|
+
endpoint: path,
|
|
420
|
+
},
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return { isError: false, payload: wrapObservation(json) };
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// ---------------------------------------------------------------------------
|
|
428
|
+
// MCP server wiring
|
|
429
|
+
// ---------------------------------------------------------------------------
|
|
430
|
+
|
|
431
|
+
const server = new Server(
|
|
432
|
+
{ name: NAME, version: VERSION },
|
|
433
|
+
{ capabilities: { tools: {} }, instructions: SERVER_INSTRUCTIONS }
|
|
434
|
+
);
|
|
435
|
+
|
|
436
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
437
|
+
tools: TOOLS.map((t) => ({
|
|
438
|
+
name: t.name,
|
|
439
|
+
description:
|
|
440
|
+
(t.gated ? "[PRO/Enterprise token required] " : "") + t.description,
|
|
441
|
+
inputSchema: t.inputSchema,
|
|
442
|
+
})),
|
|
443
|
+
}));
|
|
444
|
+
|
|
445
|
+
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
446
|
+
const { name, arguments: args } = req.params;
|
|
447
|
+
const tool = TOOL_BY_NAME.get(name);
|
|
448
|
+
if (!tool) {
|
|
449
|
+
return {
|
|
450
|
+
isError: true,
|
|
451
|
+
content: [{ type: "text", text: JSON.stringify({ error: "unknown_tool", detail: `No tool named '${name}'.` }, null, 2) }],
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const result = await apiGet(tool, args || {});
|
|
456
|
+
return {
|
|
457
|
+
isError: result.isError,
|
|
458
|
+
content: [{ type: "text", text: JSON.stringify(result.payload, null, 2) }],
|
|
459
|
+
};
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
async function main() {
|
|
463
|
+
const transport = new StdioServerTransport();
|
|
464
|
+
await server.connect(transport);
|
|
465
|
+
// Log to stderr only — stdout is the MCP JSON-RPC channel.
|
|
466
|
+
console.error(
|
|
467
|
+
`${NAME} v${VERSION} ready · API ${API_BASE} · token ${TOKEN ? "present" : "absent (public tools only)"} · ${TOOLS.length} tools`
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
main().catch((err) => {
|
|
472
|
+
console.error(`${NAME} fatal:`, err);
|
|
473
|
+
process.exit(1);
|
|
474
|
+
});
|