@molpha/mcp 0.1.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/LICENSE +21 -0
- package/README.md +270 -0
- package/dist/cli/doctor.js +27 -0
- package/dist/cli/provision.js +84 -0
- package/dist/src/apiconfig.js +47 -0
- package/dist/src/artifacts.js +80 -0
- package/dist/src/clients.js +51 -0
- package/dist/src/config.js +102 -0
- package/dist/src/determinism.js +31 -0
- package/dist/src/env.js +14 -0
- package/dist/src/errors.js +104 -0
- package/dist/src/feed.js +53 -0
- package/dist/src/guardrails.js +62 -0
- package/dist/src/hex.js +45 -0
- package/dist/src/mcp.js +82 -0
- package/dist/src/sdk.js +12 -0
- package/dist/src/server.js +17 -0
- package/dist/src/setup-validation.js +340 -0
- package/dist/src/signer/backends/memory.js +41 -0
- package/dist/src/signer/backends/privy.js +48 -0
- package/dist/src/signer/backends/turnkey.js +53 -0
- package/dist/src/signer/factory.js +37 -0
- package/dist/src/signer/types.js +1 -0
- package/dist/src/solana-address.js +29 -0
- package/dist/src/solana-compat.js +22 -0
- package/dist/src/submit.js +46 -0
- package/dist/src/subscription.js +48 -0
- package/dist/src/tools/agent_status.js +26 -0
- package/dist/src/tools/derive_feed.js +39 -0
- package/dist/src/tools/describe_feed.js +44 -0
- package/dist/src/tools/execute.js +60 -0
- package/dist/src/tools/fetch_verified.js +116 -0
- package/dist/src/tools/get_capabilities.js +45 -0
- package/dist/src/tools/get_latest.js +22 -0
- package/dist/src/tools/index.js +19 -0
- package/dist/src/tools/schemas.js +9 -0
- package/dist/src/tools/types.js +1 -0
- package/dist/src/tools/verify.js +28 -0
- package/dist/src/verifiers.js +95 -0
- package/dist/src/x402.js +552 -0
- package/package.json +81 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Molpha
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
# Molpha MCP
|
|
2
|
+
|
|
3
|
+
[](https://github.com/Molpha/mcp/actions/workflows/ci.yml)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
[](package.json)
|
|
6
|
+
|
|
7
|
+
A [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server that lets AI agents create, fetch, verify, and publish [Molpha](https://docs.molpha.io/) oracle data.
|
|
8
|
+
|
|
9
|
+
Molpha turns HTTP API responses into threshold-signed payloads that can be verified on Solana, EVM, and Starknet. This server exposes that workflow as a small set of MCP tools and keeps signing behind a local, Privy, or Turnkey-backed wallet.
|
|
10
|
+
|
|
11
|
+
> [!WARNING]
|
|
12
|
+
> This release targets Solana Devnet and Sepolia verifier networks. Treat it as testnet software, not a production security boundary. Write tools spend SOL and may consume subscription quota unless dry-run mode is enabled.
|
|
13
|
+
|
|
14
|
+
## What you can do
|
|
15
|
+
|
|
16
|
+
- Discover the active registry, oracle nodes, gateways, and verifier deployments.
|
|
17
|
+
- Derive a feedId locally from a declarative API spec — no transaction, no subscription required. Feeds are created lazily on first settle.
|
|
18
|
+
- Request a threshold-signed result and build verifier arguments for multiple chains, paid for either by an active USDC subscription or a self-funded [x402](#x402-pay-per-request) round.
|
|
19
|
+
- Build EVM/Starknet verifier call args, or submit a verified update to a Solana feed.
|
|
20
|
+
- Use a local keypair, Privy server wallet, or Turnkey wallet without changing the MCP tool surface.
|
|
21
|
+
- Put daily caps and a global dry-run default around agent-initiated writes and x402 spend.
|
|
22
|
+
|
|
23
|
+
## MCP tools
|
|
24
|
+
|
|
25
|
+
| Tool | Access | Description |
|
|
26
|
+
| --- | --- | --- |
|
|
27
|
+
| `molpha_get_capabilities` | Read | Return the registry version, node set, gateways, chains, verifier metadata, and x402 caps. |
|
|
28
|
+
| `molpha_describe_feed` | Read | Read a feed's on-chain state and subscription status. Pass `feedId`, or `apiConfig` + `signaturesRequired` to derive it. |
|
|
29
|
+
| `molpha_derive_feed` | Read | Locally derive a feedId from `apiConfig` + `signaturesRequired`. No transaction. |
|
|
30
|
+
| `molpha_agent_status` | Read | Read the x402 agent escrow (USDC balance, committed amount, quoted next price) for the current signer. |
|
|
31
|
+
| `molpha_fetch_verified` | Read/quota/spend | Run a signing round and return the signed artifact plus verifier arguments. `payment: "subscription" \| "x402" \| "auto"` selects how the round is paid for; `autoSubmit: true` settles the Solana leg in the same call. |
|
|
32
|
+
| `molpha_get_latest` | Read | Read the latest value stored in a Solana feed account. |
|
|
33
|
+
| `molpha_verify` | Read | Build EVM/Starknet verifier address and call arguments (calldata only, by design). |
|
|
34
|
+
| `molpha_execute` | Write | Submit a signed data update to Solana. Accepts the `molpha_fetch_verified` output unmodified. |
|
|
35
|
+
|
|
36
|
+
`molpha_verify` stops at calldata **by design**: the Molpha verifier is stateless, so the agent executes `verify()` itself and the server never submits an EVM/Starknet transaction or vouches for a result it did not verify on-chain. Solana is the one leg this server settles — via `molpha_execute` or `molpha_fetch_verified`'s `autoSubmit` — and there is no standalone Solana verify-simulation path; submit, then read the result back with `molpha_get_latest`.
|
|
37
|
+
|
|
38
|
+
`molpha_execute` and `molpha_verify` take the `molpha_fetch_verified` response as-is: no field remapping between calls, and short hex fields (the gateway emits a one-signer `signersBitmap` as `"4"`) are zero-padded to their canonical widths server-side.
|
|
39
|
+
|
|
40
|
+
## Quick start
|
|
41
|
+
|
|
42
|
+
### Requirements
|
|
43
|
+
|
|
44
|
+
- Node.js 20 or later
|
|
45
|
+
- A Solana wallet funded with Devnet SOL
|
|
46
|
+
- Devnet USDC if you plan to use an active subscription or the x402 pay-per-request path
|
|
47
|
+
- An MCP client such as Cursor, Claude Desktop, or Codex
|
|
48
|
+
|
|
49
|
+
### 1. Install and build
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
git clone https://github.com/Molpha/mcp.git
|
|
53
|
+
cd mcp
|
|
54
|
+
npm ci
|
|
55
|
+
cp .env.example .env
|
|
56
|
+
npm run build
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### 2. Configure a signer
|
|
60
|
+
|
|
61
|
+
For local development, point `OWNER_KEYPAIR` at a Solana JSON keypair. Use an absolute path when an MCP client launches the server.
|
|
62
|
+
|
|
63
|
+
```dotenv
|
|
64
|
+
SIGNER_BACKEND=memory
|
|
65
|
+
OWNER_KEYPAIR=/absolute/path/to/owner-keypair.json
|
|
66
|
+
SOLANA_RPC=https://api.devnet.solana.com
|
|
67
|
+
GATEWAY_ENDPOINTS=
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The same wallet owns feeds and any x402 escrow, authenticates gateway requests, and signs Solana transactions. Do not commit `.env`, wallet files, or credentials.
|
|
71
|
+
|
|
72
|
+
Other supported signer configurations:
|
|
73
|
+
|
|
74
|
+
| Backend | Required configuration |
|
|
75
|
+
| --- | --- |
|
|
76
|
+
| Local keypair | `SIGNER_BACKEND=memory`, `OWNER_KEYPAIR` |
|
|
77
|
+
| Privy | `SIGNER_BACKEND=keychain`, `KEYCHAIN_BACKEND=privy`, `PRIVY_APP_ID`, `PRIVY_APP_SECRET`, `PRIVY_WALLET_ID`, `PRIVY_WALLET_ADDRESS` |
|
|
78
|
+
| Turnkey | `SIGNER_BACKEND=keychain`, `KEYCHAIN_BACKEND=turnkey`, `TURNKEY_API_PUBLIC_KEY`, `TURNKEY_API_PRIVATE_KEY`, `TURNKEY_ORGANIZATION_ID`, `TURNKEY_WALLET_ADDRESS` |
|
|
79
|
+
|
|
80
|
+
See [.env.example](.env.example) and the ready-to-edit files in [examples](examples) for every backend.
|
|
81
|
+
|
|
82
|
+
### 3. Check the setup
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
npm run doctor
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
The doctor checks the compiled entry point, signer configuration, wallet availability, and Solana RPC. It also prints configuration snippets with resolved absolute paths.
|
|
89
|
+
|
|
90
|
+
### 4. Bootstrap a subscription
|
|
91
|
+
|
|
92
|
+
Subscription operations debit USDC, so they are deliberately kept out of the MCP tool surface. Run the provisioning CLI once with the same signer configuration used by the server:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
# Preview without sending a transaction
|
|
96
|
+
npm run provision -- subscribe --plan Basic --max-price-usdc 20000000 --dry-run
|
|
97
|
+
|
|
98
|
+
# Subscribe, with a maximum approved price of 20 USDC (6 decimals)
|
|
99
|
+
npm run provision -- subscribe --plan Basic --max-price-usdc 20000000
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Extend an existing subscription with:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
npm run provision -- extend --max-price-usdc 20000000
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
`--max-price-usdc` is a safety cap in raw USDC base units, not a quoted plan price. The transaction aborts if the live on-chain price exceeds the cap.
|
|
109
|
+
|
|
110
|
+
### 5. Connect an MCP client
|
|
111
|
+
|
|
112
|
+
Pick one of the install paths below. In both cases the server speaks stdio JSON-RPC and needs the same signer settings from step 2.
|
|
113
|
+
|
|
114
|
+
#### Claude Desktop (MCPB)
|
|
115
|
+
|
|
116
|
+
Package the built tree into an [MCP Bundle](https://github.com/modelcontextprotocol/mcpb) and install it as a desktop extension. The repo already includes a [`manifest.json`](manifest.json) that declares the Node entry point, tools, and user-config fields.
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
npm run build
|
|
120
|
+
npx @anthropic-ai/mcpb pack . molpha-mcp.mcpb
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
That writes `molpha-mcp.mcpb` in the repo root. Install it in Claude Desktop by any of:
|
|
124
|
+
|
|
125
|
+
- Double-click the `.mcpb` file
|
|
126
|
+
- Drag it into the Claude Desktop window
|
|
127
|
+
- Settings → Extensions → Advanced settings → Install Extension… → select the `.mcpb` file
|
|
128
|
+
|
|
129
|
+
During install, set the signer backend and related fields (local keypair path, Privy, or Turnkey). Those map to the same env vars as `.env.example`.
|
|
130
|
+
|
|
131
|
+
#### Cursor, Codex, or manual Claude Desktop config
|
|
132
|
+
|
|
133
|
+
Point the client at the built server (`dist/src/server.js`), not at `src/server.ts`:
|
|
134
|
+
|
|
135
|
+
```json
|
|
136
|
+
{
|
|
137
|
+
"mcpServers": {
|
|
138
|
+
"molpha": {
|
|
139
|
+
"command": "node",
|
|
140
|
+
"args": ["/absolute/path/to/mcp/dist/src/server.js"],
|
|
141
|
+
"env": {
|
|
142
|
+
"SIGNER_BACKEND": "memory",
|
|
143
|
+
"OWNER_KEYPAIR": "/absolute/path/to/owner-keypair.json",
|
|
144
|
+
"SOLANA_RPC": "https://api.devnet.solana.com"
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
- Cursor: save the JSON under `mcpServers` in `.cursor/mcp.json` or `~/.cursor/mcp.json`. Ready-to-edit copies live in [examples](examples) (`cursor-memory.mcp.json`, `cursor-privy.mcp.json`, `cursor-turnkey.mcp.json`).
|
|
152
|
+
- Claude Desktop: add the same block to `claude_desktop_config.json` and restart, or prefer the MCPB path above.
|
|
153
|
+
- Codex: use one of the [Codex TOML examples](examples/codex-memory.toml), or run `codex mcp add molpha -- node /absolute/path/to/mcp/dist/src/server.js` and then add the signer variables to `config.toml`.
|
|
154
|
+
|
|
155
|
+
Restart the client after changing configuration or rebuilding. A direct `node dist/src/server.js` invocation waits silently for JSON-RPC on stdin; that is expected for a stdio server.
|
|
156
|
+
|
|
157
|
+
## Example prompts
|
|
158
|
+
|
|
159
|
+
Once the server is connected, these prompts exercise the main workflows.
|
|
160
|
+
|
|
161
|
+
### Discover the network
|
|
162
|
+
|
|
163
|
+
> Use Molpha to inspect the current oracle capabilities. Summarize the registry version, node count, supported chains, gateway endpoints, and verifier addresses. Do not make any writes.
|
|
164
|
+
|
|
165
|
+
### Preview a feed
|
|
166
|
+
|
|
167
|
+
> Derive a Molpha feedId for `https://api.example.com/v1/finalized/price` using the JSON path `$.price` and 3 required signatures. Show me the API config hash, the derived feedId, and any determinism warnings. Do not send a transaction.
|
|
168
|
+
|
|
169
|
+
Replace the example URL with a public endpoint that returns stable, independently reproducible data. Live ticker endpoints may produce different values across oracle nodes and fail to reach quorum.
|
|
170
|
+
|
|
171
|
+
### Fetch and verify a result
|
|
172
|
+
|
|
173
|
+
> For that same feed, fetch a signed result with `payment: "auto"` and a maximum age of 60 seconds, for the EVM chain. Summarize the signed value, timestamp, registry version, quorum, and EVM verifier call, and tell me whether it was paid for via subscription or x402. Treat the signed artifact as the trust anchor; do not trust the value by itself.
|
|
174
|
+
|
|
175
|
+
### Check x402 spend before paying
|
|
176
|
+
|
|
177
|
+
> Call `molpha_agent_status` for 3 required signatures. Tell me the escrow's USDC balance, committed amount, and quoted next price before I authorize any x402 spend.
|
|
178
|
+
|
|
179
|
+
### Publish with an approval checkpoint
|
|
180
|
+
|
|
181
|
+
> Read the latest value for Molpha feed `<FEED_ID>`. If I provide a newer signed result, preview `molpha_execute` with `dryRun: true`, explain the fee-paying wallet and exact write, and wait for my confirmation before submitting it to Solana.
|
|
182
|
+
|
|
183
|
+
## Architecture
|
|
184
|
+
|
|
185
|
+
```mermaid
|
|
186
|
+
flowchart LR
|
|
187
|
+
Client["MCP client<br/>Cursor · Claude · Codex"] <-->|"stdio / JSON-RPC"| Server["Molpha MCP server"]
|
|
188
|
+
Server --> Guardrails["Write guardrails<br/>dry-run · daily caps · x402 spend caps"]
|
|
189
|
+
Server --> Signer["Signer adapter"]
|
|
190
|
+
Signer --> Memory["Local keypair"]
|
|
191
|
+
Signer --> Keychain["Privy or Turnkey"]
|
|
192
|
+
Server --> SDK["Molpha SDK"]
|
|
193
|
+
Server --> X402["x402 client<br/>escrow funding · AgentRequestAuth"]
|
|
194
|
+
SDK --> Gateway["Molpha gateway<br/>subscription round"]
|
|
195
|
+
X402 --> Gateway2["Molpha gateway<br/>x402 agent round"]
|
|
196
|
+
Gateway <--> Nodes["Oracle node quorum"]
|
|
197
|
+
Gateway2 <--> Nodes
|
|
198
|
+
SDK <--> Solana["Solana program<br/>feeds · subscriptions · agent escrows"]
|
|
199
|
+
X402 <--> Solana
|
|
200
|
+
Gateway --> Artifact["Threshold-signed<br/>data update"]
|
|
201
|
+
Gateway2 --> Artifact
|
|
202
|
+
Artifact --> Server
|
|
203
|
+
Server --> Args["EVM / Starknet verifier args,<br/>or submit_data_update on Solana"]
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
The server is an adapter and policy boundary, not a new source of truth:
|
|
207
|
+
|
|
208
|
+
1. The MCP client invokes a typed tool over stdio.
|
|
209
|
+
2. The signer authenticates gateway requests and owner transactions.
|
|
210
|
+
3. Oracle nodes independently fetch the committed API configuration and produce one aggregate signature after reaching quorum.
|
|
211
|
+
4. The server returns the self-contained signed artifact. Solana can verify or store it; EVM and Starknet consumers receive contract-ready verifier arguments.
|
|
212
|
+
5. Consumers trust a value only after verifying the signed payload against its registry version.
|
|
213
|
+
|
|
214
|
+
Provisioning is a separate CLI path because subscribing or extending debits USDC. It is never available to an autonomous MCP tool call.
|
|
215
|
+
|
|
216
|
+
## Configuration
|
|
217
|
+
|
|
218
|
+
| Variable | Default | Purpose |
|
|
219
|
+
| --- | --- | --- |
|
|
220
|
+
| `SIGNER_BACKEND` | `memory` | `memory` or `keychain` |
|
|
221
|
+
| `KEYCHAIN_BACKEND` | — | `privy` or `turnkey` for a keychain signer |
|
|
222
|
+
| `OWNER_KEYPAIR` | — | Local Solana JSON keypair path |
|
|
223
|
+
| `SOLANA_RPC` | `https://api.devnet.solana.com` | Solana RPC endpoint |
|
|
224
|
+
| `GATEWAY_ENDPOINTS` | `https://dev-gateway.molpha.io` | Comma-separated **Molpha gateway** base URLs (not your Solana RPC). Must expose `/v1/nodes` and signing routes (`/v1/agent/execute` for x402, `/v1/round/execute` for subscription). Run `npm run doctor` to verify. |
|
|
225
|
+
| `MOLPHA_EVM_NETWORKS` | `evm-sepolia` | Comma-separated EVM verifier networks |
|
|
226
|
+
| `MOLPHA_STARKNET_NETWORKS` | `starknet-sepolia` | Comma-separated Starknet verifier networks |
|
|
227
|
+
| `MOLPHA_MAX_EXECUTES_PER_DAY` | `100` | Process-local daily Solana-submit cap |
|
|
228
|
+
| `MOLPHA_DRY_RUN` | `false` | Preview all writes when set to `true` |
|
|
229
|
+
| `MOLPHA_X402_MAX_PRICE_USDC` | `1` | Refuse to fund an x402 round priced above this (decimal USDC) |
|
|
230
|
+
| `MOLPHA_X402_MAX_SPEND_PER_DAY_USDC` | `10` | Process-local daily x402 spend cap (decimal USDC) |
|
|
231
|
+
| `MOLPHA_X402_GATEWAY_PDA` | — | Optional: skip the x402 discovery round-trip when the gateway's PDA is known out of band |
|
|
232
|
+
|
|
233
|
+
The daily counters are process-local and reset when the server restarts. They are safety rails, not durable rate limits.
|
|
234
|
+
|
|
235
|
+
## x402 pay-per-request
|
|
236
|
+
|
|
237
|
+
`molpha_fetch_verified` accepts a `payment` argument:
|
|
238
|
+
|
|
239
|
+
- `"subscription"` — use the caller's active USDC subscription (see [Bootstrap a subscription](#4-bootstrap-a-subscription)). Fails if the subscription is inactive or out of quota.
|
|
240
|
+
- `"x402"` — self-fund the round from a per-signer escrow account, with no subscription required. If the escrow is underfunded, the MCP server funds it from the signer's own USDC balance (creating the escrow's associated token account if needed) up to `MOLPHA_X402_MAX_PRICE_USDC` per round and `MOLPHA_X402_MAX_SPEND_PER_DAY_USDC` per day, then refuses with a clear error above those caps.
|
|
241
|
+
- `"auto"` (default) — use the subscription when active, otherwise fall back to `"x402"`.
|
|
242
|
+
|
|
243
|
+
Call `molpha_agent_status` to inspect the escrow (USDC balance, amount already committed to unsettled rounds, and the quoted price for a given quorum) before spending, or to confirm a round settled. `encryptSecrets` (private API secrets) is not yet supported on the x402 path — use `payment: "subscription"` for feeds with encrypted secrets.
|
|
244
|
+
|
|
245
|
+
## Development
|
|
246
|
+
|
|
247
|
+
```bash
|
|
248
|
+
npm run dev # start from TypeScript for local development
|
|
249
|
+
npm run typecheck # validate types without emitting files
|
|
250
|
+
npm test # run the Vitest suite
|
|
251
|
+
npm run build # compile src/, cli/, and tests into dist/
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
Before opening a pull request, run:
|
|
255
|
+
|
|
256
|
+
```bash
|
|
257
|
+
npm run typecheck && npm test && npm run build
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
Bug reports and focused pull requests are welcome. For security issues, use [GitHub's private vulnerability reporting](https://github.com/Molpha/mcp/security/advisories/new) instead of a public issue.
|
|
261
|
+
|
|
262
|
+
## Documentation
|
|
263
|
+
|
|
264
|
+
- [Molpha protocol documentation](https://docs.molpha.io/)
|
|
265
|
+
- [Client configuration examples](examples)
|
|
266
|
+
- [MCPB manifest](manifest.json) for Claude Desktop / `.mcpb` packaging
|
|
267
|
+
|
|
268
|
+
## License
|
|
269
|
+
|
|
270
|
+
Released under the [MIT License](LICENSE).
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import "../src/env.js";
|
|
3
|
+
import { buildCodexTomlSnippet, buildMcpJsonSnippet, checkBuildArtifact, checkGatewayEndpoints, checkSignerAvailability, checkSolanaRpc, validateSignerEnv } from "../src/setup-validation.js";
|
|
4
|
+
import { loadConfig } from "../src/config.js";
|
|
5
|
+
const config = loadConfig();
|
|
6
|
+
const checks = [
|
|
7
|
+
checkBuildArtifact(),
|
|
8
|
+
...validateSignerEnv(),
|
|
9
|
+
await checkSignerAvailability(),
|
|
10
|
+
await checkSolanaRpc(config.solanaRpc),
|
|
11
|
+
await checkGatewayEndpoints(config.gatewayEndpoints)
|
|
12
|
+
];
|
|
13
|
+
const failed = checks.filter((check) => !check.ok);
|
|
14
|
+
console.log("Molpha MCP setup check\n");
|
|
15
|
+
for (const check of checks) {
|
|
16
|
+
const status = check.ok ? "ok" : "FAIL";
|
|
17
|
+
console.log(`[${status}] ${check.name}: ${check.message}`);
|
|
18
|
+
}
|
|
19
|
+
if (failed.length > 0) {
|
|
20
|
+
console.error(`\n${failed.length} check(s) failed. Fix env vars or key material, then re-run npm run doctor.`);
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
console.log("\nSuggested MCP JSON (Cursor / Claude Desktop):\n");
|
|
24
|
+
console.log(buildMcpJsonSnippet());
|
|
25
|
+
console.log("Suggested Codex config.toml snippet:\n");
|
|
26
|
+
console.log(buildCodexTomlSnippet());
|
|
27
|
+
console.log("Next: add the JSON to ~/.cursor/mcp.json or Claude Desktop config, restart the client, and call molpha_get_capabilities.");
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import "../src/env.js";
|
|
3
|
+
import { parseArgs } from "node:util";
|
|
4
|
+
import { createSolanaClient, requireMethod } from "../src/clients.js";
|
|
5
|
+
import { loadConfig } from "../src/config.js";
|
|
6
|
+
import { getSdkExport } from "../src/sdk.js";
|
|
7
|
+
import { createSigner } from "../src/signer/factory.js";
|
|
8
|
+
const { values, positionals } = parseArgs({
|
|
9
|
+
allowPositionals: true,
|
|
10
|
+
options: {
|
|
11
|
+
plan: { type: "string", default: process.env.MOLPHA_PLAN ?? "Basic" },
|
|
12
|
+
"max-price-usdc": { type: "string", default: process.env.MAX_PRICE_USDC },
|
|
13
|
+
"dry-run": { type: "boolean", default: false }
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
const command = positionals[0] ?? "subscribe";
|
|
17
|
+
if (command !== "subscribe" && command !== "extend") {
|
|
18
|
+
throw new Error("usage: molpha-provision <subscribe|extend> [--plan Basic] [--max-price-usdc <amount>] [--dry-run]");
|
|
19
|
+
}
|
|
20
|
+
const config = loadConfig();
|
|
21
|
+
const signer = await createSigner(config);
|
|
22
|
+
const solana = createSolanaClient(config, signer);
|
|
23
|
+
const planName = values.plan ?? "Basic";
|
|
24
|
+
const plan = resolvePlanId(planName);
|
|
25
|
+
const maxPriceUsdc = values["max-price-usdc"];
|
|
26
|
+
if (!values["dry-run"] && !maxPriceUsdc) {
|
|
27
|
+
throw new Error("--max-price-usdc or MAX_PRICE_USDC is required for non-dry-run bootstrap");
|
|
28
|
+
}
|
|
29
|
+
const summary = {
|
|
30
|
+
command,
|
|
31
|
+
owner: signer.publicKey,
|
|
32
|
+
plan: planName,
|
|
33
|
+
maxPriceUsdc,
|
|
34
|
+
gatewayEndpoints: config.gatewayEndpoints,
|
|
35
|
+
solanaRpc: config.solanaRpc,
|
|
36
|
+
note: "Bootstrap only — fetch and execute run in the MCP runtime with the same OWNER_KEYPAIR. A subscription is optional: molpha_fetch_verified falls back to the self-funded x402 path when unsubscribed."
|
|
37
|
+
};
|
|
38
|
+
if (values["dry-run"]) {
|
|
39
|
+
console.log(JSON.stringify({ dryRun: true, ...summary }, null, 2));
|
|
40
|
+
process.exit(0);
|
|
41
|
+
}
|
|
42
|
+
const getPlan = requireMethod(solana, "getPlan");
|
|
43
|
+
const selectedPlan = await getPlan(plan);
|
|
44
|
+
if (command === "subscribe") {
|
|
45
|
+
const subscribe = requireMethod(solana, "subscribe");
|
|
46
|
+
const subscription = await subscribe(plan, { maxPriceUsdc });
|
|
47
|
+
console.log(stringifyJson({ ...summary, selectedPlan, subscription }));
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
const extendSubscription = requireMethod(solana, "extendSubscription");
|
|
51
|
+
const subscription = await extendSubscription({ maxPriceUsdc });
|
|
52
|
+
console.log(stringifyJson({ ...summary, selectedPlan, subscription }));
|
|
53
|
+
}
|
|
54
|
+
function resolvePlanId(plan) {
|
|
55
|
+
const normalized = plan.trim().toLowerCase();
|
|
56
|
+
const byName = {
|
|
57
|
+
basic: 0,
|
|
58
|
+
standard: 1,
|
|
59
|
+
professional: 2,
|
|
60
|
+
enterprise: 3
|
|
61
|
+
};
|
|
62
|
+
const bySdkEnum = getSdkExport("PlanType")?.[plan];
|
|
63
|
+
const numeric = byName[normalized] ?? bySdkEnum ?? Number.parseInt(plan, 10);
|
|
64
|
+
if (!Number.isInteger(numeric) || Number(numeric) < 0 || Number(numeric) > 3) {
|
|
65
|
+
throw new Error(`unsupported plan "${plan}"; expected Basic, Standard, Professional, Enterprise, or 0-3`);
|
|
66
|
+
}
|
|
67
|
+
return Number(numeric);
|
|
68
|
+
}
|
|
69
|
+
function stringifyJson(value) {
|
|
70
|
+
return JSON.stringify(value, jsonReplacer, 2);
|
|
71
|
+
}
|
|
72
|
+
function jsonReplacer(_key, value) {
|
|
73
|
+
if (typeof value === "bigint") {
|
|
74
|
+
return value.toString();
|
|
75
|
+
}
|
|
76
|
+
if (value &&
|
|
77
|
+
typeof value === "object" &&
|
|
78
|
+
value.constructor.name === "PublicKey" &&
|
|
79
|
+
"toBase58" in value &&
|
|
80
|
+
typeof value.toBase58 === "function") {
|
|
81
|
+
return value.toBase58();
|
|
82
|
+
}
|
|
83
|
+
return value;
|
|
84
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { getAddressEncoder } from "@solana/kit";
|
|
2
|
+
import { requireSdkExport } from "./sdk.js";
|
|
3
|
+
/**
|
|
4
|
+
* apiConfig canonicalization shared by feed derivation and the x402 client.
|
|
5
|
+
*
|
|
6
|
+
* The gateway's Go canonicalizer (`CanonicalAPIConfigHash`) marshals the
|
|
7
|
+
* `headers` map with `encoding/json`, which always sorts map keys
|
|
8
|
+
* alphabetically. The SDK's `canonicalizeAPIConfig`/`deriveApiConfigHash`
|
|
9
|
+
* hash `JSON.stringify` of a plain JS object, which preserves insertion
|
|
10
|
+
* order instead. Any caller that derives a feedId or apiConfigHash
|
|
11
|
+
* client-side (feed derivation preview, x402 AgentRequestAuth signing) must
|
|
12
|
+
* byte-match the gateway's hash, so headers are sorted here before handing
|
|
13
|
+
* the config to the SDK's canonicalizer.
|
|
14
|
+
*/
|
|
15
|
+
export function sortApiConfigHeaders(apiConfig) {
|
|
16
|
+
if (!apiConfig.headers) {
|
|
17
|
+
return apiConfig;
|
|
18
|
+
}
|
|
19
|
+
const headers = apiConfig.headers;
|
|
20
|
+
const sorted = {};
|
|
21
|
+
for (const key of Object.keys(headers).sort()) {
|
|
22
|
+
const value = headers[key];
|
|
23
|
+
if (value !== undefined) {
|
|
24
|
+
sorted[key] = value;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return { ...apiConfig, headers: sorted };
|
|
28
|
+
}
|
|
29
|
+
/** Sorts headers, then hands the config to the SDK's canonicalizer (see above for why). */
|
|
30
|
+
export function canonicalizeApiConfig(apiConfig) {
|
|
31
|
+
const canonicalize = requireSdkExport("canonicalizeAPIConfig");
|
|
32
|
+
return canonicalize(sortApiConfigHeaders(apiConfig));
|
|
33
|
+
}
|
|
34
|
+
export function deriveApiConfigHash(canonicalApiConfig) {
|
|
35
|
+
return requireSdkExport("deriveApiConfigHash")(canonicalApiConfig);
|
|
36
|
+
}
|
|
37
|
+
export function deriveFeedIdString(owner, apiConfigHash, signaturesRequired) {
|
|
38
|
+
const derive = requireSdkExport("deriveFeedIdString");
|
|
39
|
+
return derive(Buffer.from(getAddressEncoder().encode(owner)), apiConfigHash, signaturesRequired);
|
|
40
|
+
}
|
|
41
|
+
/** Full feedId derivation: canonicalize -> hash -> feedId, for a given owner + quorum. */
|
|
42
|
+
export function deriveFeedId(apiConfig, signaturesRequired, owner) {
|
|
43
|
+
const canonicalApiConfig = canonicalizeApiConfig(apiConfig);
|
|
44
|
+
const apiConfigHash = deriveApiConfigHash(canonicalApiConfig);
|
|
45
|
+
const feedId = deriveFeedIdString(owner, apiConfigHash, signaturesRequired);
|
|
46
|
+
return { feedId, apiConfigHash, canonicalApiConfig };
|
|
47
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/** Shape a gateway DataUpdateResult into the spec-friendly signed artifact. */
|
|
2
|
+
import { toCanonicalHex } from "./hex.js";
|
|
3
|
+
/** Fixed byte widths the SDK and the Solana program enforce on the flat result. */
|
|
4
|
+
const HEX_WIDTHS = {
|
|
5
|
+
feedId: 32,
|
|
6
|
+
valuePacked: 32,
|
|
7
|
+
s: 32,
|
|
8
|
+
commitmentAddr: 20,
|
|
9
|
+
signersBitmap: 32
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Canonicalize every fixed-width hex field on a flat signed result. `value` is a
|
|
13
|
+
* decimal string, not hex, and the numeric fields are left alone.
|
|
14
|
+
*/
|
|
15
|
+
export function normalizeSignedResult(raw) {
|
|
16
|
+
const out = { ...raw };
|
|
17
|
+
for (const [field, bytes] of Object.entries(HEX_WIDTHS)) {
|
|
18
|
+
const value = raw[field];
|
|
19
|
+
if (value !== undefined && value !== null && value !== "") {
|
|
20
|
+
out[field] = toCanonicalHex(String(value), bytes, field);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return out;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Accept either shape a caller can plausibly hold: the artifact this server
|
|
27
|
+
* emits from `molpha_fetch_verified` (`{ dataUpdate, signature }`) or the flat
|
|
28
|
+
* SDK/gateway shape (`{ s, commitmentAddr, timestamp }`). Returns the flat shape
|
|
29
|
+
* with hex fields canonicalized, so no tool needs a hand-written remap.
|
|
30
|
+
*/
|
|
31
|
+
export function toSignedResult(input) {
|
|
32
|
+
const dataUpdate = asRecord(input.dataUpdate);
|
|
33
|
+
const signature = asRecord(input.signature);
|
|
34
|
+
if (!dataUpdate && !signature) {
|
|
35
|
+
return normalizeSignedResult(input);
|
|
36
|
+
}
|
|
37
|
+
const du = dataUpdate ?? {};
|
|
38
|
+
const sig = signature ?? {};
|
|
39
|
+
return normalizeSignedResult({
|
|
40
|
+
feedId: du.feedId ?? input.feedId,
|
|
41
|
+
value: du.value ?? input.value,
|
|
42
|
+
valuePacked: du.valuePacked ?? input.valuePacked,
|
|
43
|
+
timestamp: du.canonicalTimestamp ?? du.timestamp ?? input.timestamp,
|
|
44
|
+
registryVersion: du.registryVersion ?? input.registryVersion,
|
|
45
|
+
signaturesRequired: du.signaturesRequired ?? input.signaturesRequired,
|
|
46
|
+
signersBitmap: sig.signersBitmap ?? input.signersBitmap,
|
|
47
|
+
s: sig.signature ?? sig.s ?? input.s,
|
|
48
|
+
commitmentAddr: sig.commitment ?? sig.commitmentAddr ?? input.commitmentAddr,
|
|
49
|
+
fresh: input.fresh ?? true
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
export function toDataUpdateArtifact(result) {
|
|
53
|
+
// Normalize on the way out so the artifact this server emits is byte-for-byte
|
|
54
|
+
// acceptable to molpha_execute / molpha_verify without caller-side padding.
|
|
55
|
+
const normalized = normalizeSignedResult(result);
|
|
56
|
+
return {
|
|
57
|
+
value: String(normalized.value ?? ""),
|
|
58
|
+
fresh: Boolean(normalized.fresh ?? true),
|
|
59
|
+
dataUpdate: {
|
|
60
|
+
feedId: String(normalized.feedId ?? ""),
|
|
61
|
+
registryVersion: Number(normalized.registryVersion ?? 0),
|
|
62
|
+
signaturesRequired: Number(normalized.signaturesRequired ?? 0),
|
|
63
|
+
value: String(normalized.value ?? ""),
|
|
64
|
+
...(normalized.valuePacked !== undefined
|
|
65
|
+
? { valuePacked: String(normalized.valuePacked) }
|
|
66
|
+
: {}),
|
|
67
|
+
canonicalTimestamp: Number(normalized.timestamp ?? 0)
|
|
68
|
+
},
|
|
69
|
+
signature: {
|
|
70
|
+
signature: String(normalized.s ?? ""),
|
|
71
|
+
commitment: String(normalized.commitmentAddr ?? ""),
|
|
72
|
+
signersBitmap: String(normalized.signersBitmap ?? "")
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function asRecord(value) {
|
|
77
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
78
|
+
? value
|
|
79
|
+
: undefined;
|
|
80
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Connection } from "@solana/web3.js";
|
|
2
|
+
import { loadConfig } from "./config.js";
|
|
3
|
+
import { parseSolanaPubkey } from "./solana-address.js";
|
|
4
|
+
import { toLegacyPublicKey } from "./solana-compat.js";
|
|
5
|
+
import { getSdkExport, requireSdkExport } from "./sdk.js";
|
|
6
|
+
import { createSigner } from "./signer/factory.js";
|
|
7
|
+
let cachedContextPromise;
|
|
8
|
+
export function getMolphaContext() {
|
|
9
|
+
cachedContextPromise ??= createMolphaContext(loadConfig());
|
|
10
|
+
return cachedContextPromise;
|
|
11
|
+
}
|
|
12
|
+
export async function createMolphaContext(config) {
|
|
13
|
+
const signer = await createSigner(config);
|
|
14
|
+
const connection = new Connection(config.solanaRpc, "confirmed");
|
|
15
|
+
const solana = createSolanaClient(config, signer, connection);
|
|
16
|
+
return {
|
|
17
|
+
config,
|
|
18
|
+
gateway: createGateway(config, solana, signer),
|
|
19
|
+
solana,
|
|
20
|
+
signer,
|
|
21
|
+
connection
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export function createGateway(config, solana, signer) {
|
|
25
|
+
const Gateway = requireSdkExport("MolphaGateway");
|
|
26
|
+
// SDK Signer type = (message: Uint8Array) => Promise<Uint8Array>
|
|
27
|
+
const defaultSigner = (msg) => signer.signMessage(msg);
|
|
28
|
+
return new Gateway(config.gatewayEndpoints, () => requireMethod(solana, "getRegistrySelectionConfig")(), defaultSigner, signer.publicKey);
|
|
29
|
+
}
|
|
30
|
+
export function createSolanaClient(config, signer, connection = new Connection(config.solanaRpc, "confirmed")) {
|
|
31
|
+
const SolanaClient = requireSdkExport("MolphaSolanaClient");
|
|
32
|
+
const wallet = {
|
|
33
|
+
publicKey: toLegacyPublicKey(signer.publicKey),
|
|
34
|
+
signTransaction: (tx) => signer.signTransaction(tx),
|
|
35
|
+
signAllTransactions: (txs) => signer.signAllTransactions(txs),
|
|
36
|
+
};
|
|
37
|
+
return SolanaClient.create({
|
|
38
|
+
connection,
|
|
39
|
+
wallet
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
export function getMolphaProgramId() {
|
|
43
|
+
return parseSolanaPubkey(requireSdkExport("MOLPHA_PROGRAM_ADDRESS"), "MOLPHA_PROGRAM_ADDRESS");
|
|
44
|
+
}
|
|
45
|
+
export function requireMethod(target, methodName) {
|
|
46
|
+
const method = target[methodName];
|
|
47
|
+
if (typeof method !== "function") {
|
|
48
|
+
throw new Error(`Molpha SDK client is missing ${methodName}()`);
|
|
49
|
+
}
|
|
50
|
+
return method.bind(target);
|
|
51
|
+
}
|