Skip to main content
Glama
hestiax-official

HestiaX MCP

Official
README.md
# HestiaX MCP: your own AI agent reads your XRPL lending

[![Check](https://github.com/hestiax-official/xrpl-lending-mcp/actions/workflows/check.yml/badge.svg)](https://github.com/hestiax-official/xrpl-lending-mcp/actions/workflows/check.yml)

> [!WARNING]
> **Disclaimer.** This code is provided as is. It has not been audited, and it is for use on the XRPL Devnet only.
>
> Nothing in this repository is an offer, solicitation or recommendation to invest or lend, or investment, legal, tax
> or financial advice. See [Disclaimer](#disclaimer).

[HestiaX](https://hestiax.org) finances renewable-energy projects on the XRP Ledger. Lenders deposit into vault series
(Single Asset Vault, XLS-65), the vaults lend to solar and wind projects through the Lending Protocol (XLS-66), and
KYC is a Credential (XLS-70) checked by a Permissioned Domain (XLS-80). An Operator runs the vaults.

This repository is the **MCP server** behind `https://demo.hestiax.org/api/mcp`. Each user connects their own AI agent,
such as Claude Code, with a personal read-only token. The agent then reads what that user sees in HestiaX, as that
user: their positions, their loans, the vaults' state on the ledger. It can buy, sign or change nothing.

```
> Using the hestiax tools: who am I, what do I hold, and how are my loans?

● hestiax · getMe          Astrid Lund, Nordic Pension Fund, LENDER, KYC approved, scopes ["read"]
● hestiax · listPositions  19,500,000 Vault Shares in Construction Vault · USD · Series 39 · Solar and wind, Sweden,
                           worth 19,500,004.57 USD against 19,500,000 deposited; redemption 2027-04-23
● hestiax · listLoans      Skurup Solpark, 2,750,000 USD at 9.25%: all 20 installments paid, none late
```
*Condensed from a Claude Code session against the live demo on 25 Oct 2026 (v5.9.0), through Cloudflare Access; the
figures are its tools' answers. On the demo's clock the 20 installments fall a minute apart.*

## What to look at

| | Where |
|---|---|
| **Read-only by construction.** No tool writes. The scope model has `"write"` ready, but in the app every ledger write refuses a token, whatever tool asked: money moves only when the person confirms in HestiaX. | `src/core/tool.ts`, `src/hestiax/catalog.ts` |
| **Least privilege per caller.** A token sees only its role's tools. A tool outside its role or scope answers "Unknown tool", the same as one that does not exist, so nothing leaks what other roles can do. | `sees()` in `src/core/policy.ts` |
| **One policy, two engines.** A hand-rolled JSON-RPC engine and the official MCP SDK's sit behind the same checks. The same suite runs on both, and the official MCP client connects to both. | `src/core/engines/`, `server.test.ts`, `interop.test.ts` |
| **Plug and play.** The engine knows no app. The catalog, the public API, knows only the engine's contract. A test reads every import and fails if either reaches further. | `src/boundary.test.ts` |
| **Hardened HTTP edge.** A hosts list against DNS rebinding, 64 KB body cap, 60 calls a minute per token, refused tokens throttled per address and logged, an audit of every call made with a token by the SHA-256 of its arguments (never the arguments) and its address, and errors said in general terms. | `src/core/policy.ts` |
| **Answers stripped of what hides.** Text one user writes reaches another user's agent. Every string in an answer is normalized and stripped of the characters a screen does not show but a model reads: controls, zero-width, bidi overrides, Unicode Tags. | `plain()` in `src/core/policy.ts` |

## How it fits

```mermaid
flowchart LR
  A["User's AI agent<br/>(Claude Code)"] -- "Bearer hx_pat_…<br/>+ Access service token" --> CF["Cloudflare Access"]
  CF --> R["/api/mcp<br/>(HestiaX app)"]
  subgraph repo["this repository"]
    P["policy<br/>guard · visibility · rate limit · audit"] --> E["engine<br/>own | official SDK"]
    E --> T["tools = bind(catalog, backend)"]
  end
  R --> P
  T --> B["backend<br/>(the app's data access layer)"]
  B --> DB[("HestiaX records")]
  B --> X[("XRP Ledger<br/>vaults · loans · credentials")]
```

The app supplies three things: `authenticate` (a token to its user and role), the backend (each tool on the app's own
reads, under the same role and owner checks as its pages) and `audit`. The rest is here.

## The tools

All read-only, each listed only for its roles. Each answers plain JSON (money as `{ value, currency }`) as
`structuredContent`, with the same JSON as text. Lender tools carry a not-investment-advice notice.

| Tool | Roles | Reads |
|---|---|---|
| `getMe` | every role | name, organization, role, KYC status, the token's scopes |
| `getWallet` | every role | the user's XRPL address, its explorer link and USD held (demo only) |
| `listOffers` | Lender | vault series taking deposits: coupon, minimum ticket, room left, subscription close, redemption |
| `listPositions` | Lender | Vault Shares held per series, valued at the withdrawal price, against what was paid in |
| `listLoans`, `getLoan` | Lender, Borrower, Operator | the loans the caller may see: principal, owed, installments left, next due, late / impaired / default, the ledger transactions |
| `listProjects`, `getProject` | Borrower, Operator | a borrower's own projects, or every borrower's for the Operator: figures, stage, terms, loan and due-diligence verdict |
| `getFinancingOffer` | Borrower | the offer on a project by the Credit agent's rules, or why none stands now |
| `listVaults`, `getDecisionQueue` | Operator | every series' phase, fund size, holdings, loss and Loan Broker book; the loans and projects waiting on a decision |

Inputs are strict: an unknown key is refused, and ids are pattern-checked (a loan id is 64 hex characters), so nothing
rides in one.

## Try it

Requires Node.js 26.10 (`.tool-versions`) and pnpm 11. Node runs the TypeScript as it is, so there is no build.

```bash
pnpm install --frozen-lockfile
pnpm run check   # tsc, then 50 tests: the engine on both engines, the official MCP client, the import boundary
pnpm run try     # examples/try.ts: one tool, one token, five requests
MCP_ENGINE=sdk pnpm run try   # the same five on the official SDK's engine: the same answers
pnpm run demo    # examples/demo.ts: HestiaX's 11 tools on sample data, at http://127.0.0.1:3333/api/mcp
```

CI runs all four on every push, on each engine.

`pnpm run try` prints what an MCP client would get:

```
200 {"jsonrpc":"2.0","id":1,"result":{"tools":[]}}                                  no token: no tools
200 {"jsonrpc":"2.0","id":1,"result":{"tools":[{"name":"getHello",…}]}}             the token's role's tools
200 {"jsonrpc":"2.0","id":1,"result":{…,"structuredContent":{"hello":"astrid"}}}    called as the token's user
200 {"jsonrpc":"2.0","id":1,"result":{…"Invalid arguments …"…,"isError":true}}    an unknown key: a result to correct by
401 The token is not valid: it may have expired or been revoked                    a token that does not hold
```

**The demo on sample data** serves the real catalog through the real engine and policy; only the backend is sample
figures, in the shapes the app answers with. Three tokens: `demo-lender`, `demo-borrower`, `demo-operator`, each
seeing its own role's tools. With the demo running, connect Claude Code and ask it, for example, "what do I hold, and
how are my loans?":

```bash
claude mcp add --transport http hestiax-demo http://127.0.0.1:3333/api/mcp --header "Authorization: Bearer demo-lender"
```

Each call shows in the demo's log: its time, the token, the tool and how it ended.

**The live demo** is behind Cloudflare Access. To connect an agent, ask the HestiaX team for a demo token and the
Access service token, then:

```bash
claude mcp add --scope user --transport http hestiax https://demo.hestiax.org/api/mcp \
  --header "Authorization: Bearer <hx_pat_ token>" \
  --header "CF-Access-Client-Id: <client id>" --header "CF-Access-Client-Secret: <client secret>"
```

## Use it in your own app

`createMcpServer` returns `(req: Request) => Promise<Response>`: Streamable HTTP, one JSON answer per request, no
session. Mount it as any Fetch-style handler, a Next.js route's `POST` for one. `examples/try.ts` is a complete
example.

| Option | What |
|---|---|
| `engine` | `"own"` (default) or `"sdk"` |
| `info`, `instructions` | What `initialize` answers; the instructions for a token's caller and for one without |
| `tools` | `bind(catalog, backend)`: each spec with its implementation, which only sees input its schema parsed |
| `authenticate` | The `Caller` a bearer token names, or `undefined` (401). Absent: tokens are off, and any `Authorization` is a 401 |
| `as` | Runs a call in its caller's context (the app's request-scoped principal) |
| `ready` | Why the backend cannot serve now, asked before each call |
| `audit` | Every call with a token: tool, the SHA-256 of its arguments, outcome, the client's address |
| `explain` | Words the caller may read for an error a tool threw; otherwise it is said in general terms |
| `perMinute`, `maxBytes` | Calls a minute per token, or per client address without one (60); body cap (64 KB, else 413) |
| `hosts` | The hosts it answers as: another Host, or a page of another origin, is a 403, so a rebound DNS name cannot pass. Absent: the request's own host |

A tool throws `Refusal` for words the caller reads in full (not found, not allowed). Bad arguments are a result with `isError`
(SEP-1303), so the agent can correct them; an unknown tool, or one the caller may not see, is a JSON-RPC `-32602`. A
request must be `Content-Type: application/json` with an `Accept` naming `application/json` and `text/event-stream`, on
both engines (else 415 or 406). A spec with `access: "public"`
is listed without a token; `"read"` or `"write"` needs a token holding that scope (`"write"` holds `"read"`), and
`roles`, when given, its caller's role.

## The two engines

| | own (default) | sdk |
|---|---|---|
| Built on | hand-rolled JSON-RPC | `@modelcontextprotocol/server` 2.3.1, the official SDK |
| Protocol versions | 2025-06-18 | 2026-07-28, plus 2024-10-07 to 2025-11-25 |
| Batches | refused | served under 2025-03-26 only |

Auth, visibility, results, refusals, limits and audit are the policy's, so both behave the same there. The SDK engine
uses the SDK's low-level `Server`, not `McpServer`: `McpServer` validates a call before the handler runs, so unknown
tools and bad arguments would skip the rate limit and the audit.

## Layout

```
src/core/                the engine; knows zod, node, the official MCP SDK and itself
  tool.ts                the contract: Spec, Catalog, Backend, Caller, Scope, Refusal, bind()
  policy.ts              the checks both engines share
  engines/own.ts         hand-rolled JSON-RPC
  engines/sdk.ts         the official MCP SDK
  server.ts              createMcpServer(): one policy in front of one engine
src/hestiax/catalog.ts   HestiaX's public API: tool names, descriptions, scopes, roles, input schemas
examples/try.ts          a runnable server with one tool
examples/demo.ts         HestiaX's catalog on sample data, over HTTP
```

The HestiaX app, which holds the backend and the token issuing, is a separate, private repository. It takes this
repository whole, as a git subtree at `src/mcp/xrpl-lending-mcp`, so this is the one source of the engine and the
catalog. A change lands here first, by pull request, and the app pulls it:

```bash
git subtree pull --prefix=src/mcp/xrpl-lending-mcp git@github.com:hestiax-official/xrpl-lending-mcp.git main --squash
```

## Next

- **Writes as propose-then-confirm.** An agent drafts a deposit or a repayment; the person confirms it in HestiaX,
  where their own signature moves the money. A "yes" typed into the agent is never the confirmation.
- **OAuth 2.1** in place of personal tokens, so a client connects without a copied token or custom headers.
- **Public tools** for partners, served without a token from a catalog of their own.

## Disclaimer

This code is provided as is. It has not been audited, and it is for use on the XRPL Devnet only.

HestiaX Ltd is a software technology provider and is not authorised or regulated by the Financial Conduct Authority.
HestiaX does not provide investment advice, commit capital, hold client money or assets, act as a broker, underwriter
or custodian, operate an exchange, trading venue or marketplace, or itself undertake regulated arranging, placement or
distribution activities; any regulated activities are required to be carried out by appropriately authorised
third-party firms. This repository is for general information only and does not constitute an offer, solicitation,
recommendation, invitation or inducement to invest or lend. HestiaX does not guarantee that any financing will be
arranged or completed, and any potential transaction is subject to due diligence, approvals, definitive documentation,
investor demand and market conditions. Nothing in this repository constitutes investment, legal, tax, accounting,
regulatory or financial advice or a personal recommendation; you should conduct your own assessment and due diligence
and obtain independent professional advice.

The names, institutions, projects and figures in this README and in the examples are illustrative, from a demo that
runs on the XRP Ledger in test money.

## License

[Apache License 2.0](LICENSE). Copyright 2026 HestiaX Ltd; see [NOTICE](NOTICE).