Skip to main content
Glama
kennyrivaldi

stellar-copilot-mcp

by kennyrivaldi
README.md
# stellar-copilot-mcp

MCP server for Stellar. Ask an AI assistant what an account holds, why a transaction failed, or
what a Soroban contract does — and get an answer in plain language. Optionally, propose payments
the user approves in their own wallet.

**Holds no keys. Signs nothing. Submits nothing.** Reads use public chain data. Transactions are
built unsigned and handed to a page the user controls; signing happens in Freighter and nowhere
else.

---

## Tools

| Tool | Answers |
|---|---|
| `explain_account` | "What do I hold?" · "Why can't I spend my whole balance?" · "Is my trustline set up?" |
| `diagnose_transaction` | "Why did this fail?" — decodes transaction and operation result codes into causes and fixes, and recovers Soroban contract error codes |
| `explain_contract` | "What can this contract do?" — reads a deployed contract's published interface |

Running the HTTP transport adds three more:

| Tool | Purpose |
|---|---|
| `start_pairing` | Returns a link the user opens in the browser where Freighter lives |
| `get_pairing_status` | Whether the wallet connected, and how an approval turned out |
| `propose_payment` | Builds an **unsigned** payment and sends it to the user's approval page |

The stdio binary exposes **only the three read tools**. Pairing needs a server to host the
approval page and hold session state, which stdio has neither of.

## Install

Requires Node 20+.

```bash
npm install && npm run build
```

### Claude Code

```bash
claude mcp add stellar-copilot -- node /absolute/path/to/stellar-copilot-mcp/dist/index.js
```

### Claude Desktop

Add to `claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "stellar-copilot": {
      "command": "node",
      "args": ["/absolute/path/to/stellar-copilot-mcp/dist/index.js"],
      "env": { "STELLAR_NETWORK": "testnet" }
    }
  }
}
```

Cursor and other local clients take the same command; see their connector docs.

### Remote clients (ChatGPT, Gemini)

Remote clients connect over a URL rather than spawning a process, so run the HTTP transport:

```bash
npm run start:http
```

Serves MCP at `http://127.0.0.1:3000/mcp` and a health check at `/health`. Point your client at
the `/mcp` URL.

**To use it from Claude, ChatGPT, or Gemini you need a public HTTPS URL** — those clients cannot
reach localhost. A `Dockerfile` and `fly.toml` are included. See **[DEPLOY.md](DEPLOY.md)**.

## Configuration

| Variable | Default | Notes |
|---|---|---|
| `STELLAR_NETWORK` | `testnet` | `testnet` or `public` |
| `STELLAR_HORIZON_URL` | network default | Override for a private Horizon |
| `STELLAR_RPC_URL` | testnet default; **empty on mainnet** | Required on `public` — see below |

HTTP transport only:

| Variable | Default | Notes |
|---|---|---|
| `PORT` | `3000` | Listen port |
| `HOST` | `127.0.0.1` | Bind address. Keep it on loopback unless it is behind a reverse proxy. |
| `MCP_PATH` | `/mcp` | Endpoint path |
| `MCP_ALLOWED_HOSTS` | localhost variants | Comma-separated. **Required when deployed under a real hostname**, or requests are rejected with 403. |
| `MCP_ALLOWED_ORIGINS` | unset | Comma-separated browser origins, if any |

**On mainnet you must set `STELLAR_RPC_URL`.** SDF does not operate a public mainnet Soroban RPC
endpoint, so there is no sensible default. Horizon-backed tools (`explain_account`,
`diagnose_transaction`) work on mainnet without it; `explain_contract` needs it and will tell you so
rather than failing at startup.

## Try it

```bash
npm run inspect   # MCP Inspector
```

Or drive it directly:

```bash
printf '%s\n%s\n%s\n' \
  '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"cli","version":"0"}}}' \
  '{"jsonrpc":"2.0","method":"notifications/initialized"}' \
  '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}' \
  | node dist/index.js
```

## Development

```bash
npm run typecheck   # tsc --noEmit
npm run dev         # tsc --watch
npm run build       # emit to dist/
```

### Two constraints to respect when adding code

**stdout is the JSON-RPC channel.** Never write to it. All logging goes to stderr — a stray
`console.log` corrupts the protocol stream, and the failure looks like a client bug.

**The HTTP transport is stateless.** No session IDs, no shared state — a fresh server per request.
Every tool is a pure read, so there is nothing to remember between calls, and stateless means no
session table to leak, expire, or scale. It also means POST is the only meaningful method: there is
no stream to open and no session to delete.

**DNS rebinding protection is on by default.** Without it, any web page the user visits could POST
to a server bound on localhost and drive its tools from the browser. Deploying under a real hostname
means adding it to `MCP_ALLOWED_HOSTS`.

**Keep this server read-only.** No key material, no signing, no submission. Transaction building and
signing belong behind an independent simulation-and-approval step in a separate deployable
(see `../TECHNICAL-SPEC.md`). The boundary is structural, not a policy note.

### Decoding contract errors

Contract-defined codes (`Error(Contract, #1205)`) mean whatever that contract's
`#[contracterror]` enum says, so they need a protocol table. Pass a `protocol` hint:

```
diagnose_transaction(hash: "...", protocol: "blend-v2-pool")
```

Known tables: `blend-v1-pool`, `blend-v2-pool`, `soroswap-pair` — see
`src/lib/contractErrorTables.ts`.

Tables are keyed by *protocol*, not contract address, because Blend pools are permissionless:
every pool is a separate deployment sharing one error enum, so an address-keyed registry would
mean enumerating every pool that will ever exist.

`KNOWN_CONTRACTS` maps verified addresses to protocols and is intentionally empty. An address
goes in only once confirmed against a published deployment list — a wrong entry would attach a
confident wrong explanation to someone's real failed transaction. Every table must cite the
source and date it was read from; a test enforces this.

### Adding a failure explanation

`src/lib/resultCodes.ts` maps XDR result-code names to a `{ meaning, fix }` pair. Add the code, write
the explanation for someone who has never read the Stellar docs, and say what to do about it.

Contract-*defined* codes are not in the result XDR, and they are not recoverable from the
historical record either: Horizon 27 returns no transaction meta at all, and public Soroban RPC
nodes leave `diagnosticEvents` empty. The server recovers them by re-simulating the call, and
says so in its output — simulation runs against *current* ledger state, so it is evidence rather
than proof of the original error.

## Status

Verified against live Stellar testnet on 30 July 2026: clean typecheck under `strict` +
`noUncheckedIndexedAccess`, working MCP handshake, correct reserve math on a Friendbot-funded account,
and correct diagnosis of a real failed Soroban contract call.

62 unit tests (including the HTTP transport and the approval page end to end) and 9 live
integration tests pass. The pairing flow, independent decoding, and injection blocking are
verified in a real browser.

**Not yet verified:** Freighter signing itself. It is a browser extension, so `signTransaction`
and the submit path need a machine with Freighter installed. Known gap: `KNOWN_CONTRACTS` is empty, so
contract errors need an explicit `protocol` hint until verified deployment addresses are added.

## Proposing transactions

Only over the HTTP transport:

```bash
PUBLIC_BASE_URL=https://your-host npm run start:http
```

The flow:

1. `start_pairing` returns a link. The user opens it where Freighter is installed.
2. The page connects the wallet and reports the address back.
3. `propose_payment` builds an **unsigned** transaction and queues it for the page.
4. **The page decodes the XDR itself** and shows what it actually does.
5. The user signs in Freighter. The page submits.
6. `get_pairing_status` reports the outcome.

### Why the page decodes it again

The assistant's description of a transaction is treated as an untrusted claim, never as truth.
If a prompt injection made the model build a malicious transaction, the model's *description* of
that transaction would be malicious too — so the only thing that catches it is comparing the
description against independently decoded reality.

When they disagree, the page shows the mismatch and **disables the approve button**. A warning a
user can click straight past is not a control.

For the same reason, **no tool returns a decoded preview to the model.** If it could read the
decode, it could misreport it, and the user would be approving the model's account of the
transaction rather than the transaction. A test asserts no such tool exists.

`PUBLIC_BASE_URL` must be an origin the user's browser can reach; it is what pairing links point
at. Sessions are in-memory, expire after 30 minutes idle, and hold no key material.

## Privacy

The server runs locally, stores nothing, and handles only public blockchain identifiers — never
keys or credentials. It queries public Stellar infrastructure (Horizon, Soroban RPC), both of which
you can repoint via environment variables. Full policy: [PRIVACY.md](PRIVACY.md).

## Licence

Apache-2.0

TDQS

A4.4/5.0

Scored across 3 tools

Disambiguation5/5

Each tool addresses a distinct aspect of Stellar (account state, transaction outcomes, contract interfaces), with no functional overlap. An agent can reliably select the correct tool based on the user's question.

Naming Consistency5/5

All tool names follow a consistent verb_noun pattern in snake_case (explain_account, diagnose_transaction, explain_contract). While the verbs differ, the structural pattern is uniform and predictable.

Tool Count5/5

Three tools is well-scoped for a focused explanatory server. Each tool covers a core need, and the count is within the typical 3-15 range without being too heavy or too thin.

Completeness4/5

The tools cover the primary explanatory use cases for Stellar: account state, transaction diagnosis, and contract interface. Minor gaps might include explaining specific operations or network-level details, but these are not critical for the stated purpose.

Maintenance

ActivityStale
ResponsivenessNo issues