@stellar-mcp/client
v0.2.0
Published
Type-safe programmatic client for Stellar MCP servers — discover tools, call contracts, sign and submit transactions
Downloads
361
Readme
@stellar-mcp/client
Type-safe programmatic client for Stellar MCP servers — discover tools, call Soroban contracts, and sign/submit transactions with pluggable signer adapters.
Overview
@stellar-mcp/client is the SDK layer for interacting with MCP servers generated by stellar mcp generate. It speaks the MCP JSON-RPC 2.0 protocol over HTTP and exposes a clean TypeScript API for:
- Tool discovery — list all tools a server exposes and their schemas
- Contract calls — invoke any server tool and receive typed results (including XDR for write operations)
- Transaction signing — pluggable signer adapters for secret keys, Freighter browser wallets, and PasskeyKit smart wallets
- Transaction confirmation — poll the Stellar RPC until a transaction finalises
Installation
npm install @stellar-mcp/clientPeer Dependencies
| Package | Required | Use case |
|---|---|---|
| @modelcontextprotocol/sdk | Yes | MCP transport layer |
| @stellar/stellar-sdk | Yes | XDR signing, RPC calls |
| @creit.tech/stellar-wallets-kit | Optional | Freighter browser signer |
| passkey-kit | Optional | PasskeyKit smart wallet signer |
# Minimal (Node.js / server-side)
npm install @modelcontextprotocol/sdk @stellar/stellar-sdk
# With browser wallet support
npm install @creit.tech/stellar-wallets-kit
# With PasskeyKit smart wallet support
npm install passkey-kitQuick Start — Type-Safe (Recommended)
Generate a typed client file from your running MCP server:
npx mcp-generate-types --url http://localhost:3001/mcp --out ./src/mcp-types.tsThe generated types reflect your contract's functions — tool names, arg shapes, and result types are all derived from whatever contract you passed to stellar mcp generate. The snippet below uses a token-factory contract as an example; a DEX, AMM, or any other Soroban contract would produce different tool names.
import { createMCPClient } from './src/mcp-types'; // generated — reflects your contract
import { secretKeySigner } from '@stellar-mcp/client';
import { Networks } from '@stellar/stellar-sdk';
const client = createMCPClient({
url: 'http://localhost:3001/mcp',
networkPassphrase: Networks.TESTNET,
rpcUrl: 'https://soroban-testnet.stellar.org',
});
// Tool names, arg types, and result types all come from your contract.
// Example below is a token-factory contract — yours will differ.
const { data: admin } = await client.call('get-admin'); // contract-specific
const { data: count } = await client.call('get-token-count'); // contract-specific
const { xdr } = await client.call('deploy-token', {
deployer: 'GABC...',
config: { name: 'MyToken', symbol: 'MTK', decimals: 7, /* ... */ },
});
const result = await client.signAndSubmit(xdr!, {
signer: secretKeySigner(process.env.SECRET_KEY!),
});
console.log(result.hash, result.status); // → 'SUCCESS'
client.close();Quick Start — Untyped
Use MCPClient directly without code generation. Tool names and args are string / unknown.
import { MCPClient, secretKeySigner } from '@stellar-mcp/client';
import { Networks } from '@stellar/stellar-sdk';
const client = new MCPClient({
url: 'http://localhost:3001/mcp',
networkPassphrase: Networks.TESTNET,
rpcUrl: 'https://soroban-testnet.stellar.org',
});
const tools = await client.listTools();
console.log(tools.map(t => t.name));
const { data: admin } = await client.call('get-admin');
const { xdr } = await client.call('deploy-token', { deployer: 'GABC...', config: { /* ... */ } });
const result = await client.signAndSubmit(xdr!, { signer: secretKeySigner(process.env.SECRET_KEY!) });
client.close();mcp-generate-types CLI
Connects to a live MCP server and generates a typed TypeScript file with:
- A TypeScript interface per tool (
DeployTokenArgs,GetAdminArgs, …) - A
ServerToolsToolMap that links each tool name to its arg and result types - A
createMCPClientfactory function — the recommended way to create clients
# Generate types from a running server
npx mcp-generate-types --url http://localhost:3001/mcp
# Custom output path
npx mcp-generate-types --url http://localhost:3001/mcp --out ./src/mcp-types.ts
# Help
npx mcp-generate-types --helpOptions:
| Flag | Default | Description |
|---|---|---|
| --url | required | MCP server URL |
| --out | mcp-types.ts | Output file path |
What the generated file looks like:
The CLI introspects your live server and emits one arg interface + one result interface per tool. The names come directly from your contract — get-admin and deploy-token below are specific to a token-factory contract. A DEX might generate swap, add-liquidity, etc.
// Auto-generated by @stellar-mcp/client — DO NOT EDIT
// (example output for a token-factory contract — your contract's tools will differ)
import { MCPClient, type MCPClientOptions, type ToolMap } from '@stellar-mcp/client';
// ─── Arg Types ────────────────────────────────────────────────────────────────
export interface DeployTokenArgs { // one interface per tool, named after your contract fn
deployer: string;
config: {
admin: string;
name: string;
symbol: string;
decimals: number;
token_type: { tag: 'Allowlist' } | { tag: 'Pausable' } | { tag: 'Regulated' };
// ...
};
}
export type GetAdminArgs = Record<string, never>;
// ─── Result Types ─────────────────────────────────────────────────────────────
export interface GetAdminResult { // compiled from the tool's outputSchema
xdr: string;
simulationResult?: string;
}
export interface DeployTokenResult {
xdr: string; // unsigned XDR — pass to signAndSubmit()
simulationResult?: unknown;
}
// ─── Tool Map ─────────────────────────────────────────────────────────────────
interface ServerTools extends ToolMap {
'get-admin': { args: GetAdminArgs; result: GetAdminResult };
'deploy-token': { args: DeployTokenArgs; result: DeployTokenResult };
// ... one entry per tool exposed by the server
}
export function createMCPClient(options: MCPClientOptions): MCPClient<ServerTools> {
return new MCPClient<ServerTools>(options);
}Result types are compiled from each tool's outputSchema — read-only tools get precise types (e.g. simulationResult?: string), write tools always include the unsigned xdr field.
Regenerate when the server changes:
npx mcp-generate-types --url http://localhost:3001/mcp --out ./src/mcp-types.tsSchema Utilities
Helper functions for building UIs on top of MCP tool definitions. These work on the JSON Schema from ToolInfo.inputSchema and are presentation-layer agnostic — equally useful for Telegram bots, React forms, CLIs, or any custom interface.
import {
extractArgs,
buildToolArgs,
parseArgValue,
isReadOperation,
argKey,
type ArgDef,
} from '@stellar-mcp/client';extractArgs(tool)
Flattens a tool's inputSchema into a sorted, display-ready ArgDef[]. Handles nested objects (recursively expanded with path tracking), discriminated unions (oneOf/anyOf with { tag: "Value" } pattern → enum fields), nullable types, and enum fields. Required args come before optional at every level.
const args = extractArgs(tool);
// [{ name: 'deployer', path: ['deployer'], type: 'string', required: true }, ...]buildToolArgs(args, collected)
Reconstructs the nested args object from a flat key→value map, ready to pass to client.call(). Automatically wraps discriminated union values as { tag: value }.
const toolArgs = buildToolArgs(args, {
deployer: 'GABC...',
'config.admin': 'GDEF...',
'config.decimals': 7,
});
// { deployer: 'GABC...', config: { admin: 'GDEF...', decimals: 7 } }parseArgValue(value, arg)
Coerces a user-typed string into the correct JS type for the given ArgDef.
parseArgValue('42', { type: 'number', ... }) // → 42
parseArgValue('true', { type: 'boolean', ... }) // → true
parseArgValue('{"a":1}', { type: 'object', ... }) // → { a: 1 }
parseArgValue('', { type: 'string', nullable: true, ... }) // → nullisReadOperation(toolName)
Heuristic that returns true for tool names starting with read-only prefixes (get, list, query, fetch, find, search, is, has, check, count, show, view, read).
isReadOperation('get-admin') // true
isReadOperation('list_tokens') // true
isReadOperation('deploy-token') // falseArgDef
interface ArgDef {
name: string;
path: string[]; // e.g. ['config', 'admin'] for nested fields
type: string; // 'string' | 'number' | 'boolean' | 'object' | 'array' | 'enum' | 'any'
description: string;
required: boolean;
enum?: string[]; // present for enum / discriminated union fields
group?: string; // section header for nested object groups
nullable?: boolean;
unionTag?: boolean; // if true, value must be wrapped as { tag: value }
}API Reference
new MCPClient(options)
Creates a client instance. The connection is lazy — it opens on the first method call.
interface MCPClientOptions {
url: string; // Full MCP server URL, e.g. 'http://localhost:3001/mcp'
networkPassphrase: string; // Stellar network passphrase (Networks.TESTNET / Networks.PUBLIC)
rpcUrl: string; // Soroban RPC endpoint
timeout?: number; // Request timeout in ms (default: 30 000)
}client.listTools()
Discover all tools the server exposes.
const tools = await client.listTools();
// [{ name: 'deploy-token', description: '...', inputSchema: { ... } }, ...]Returns ToolInfo[]:
interface ToolInfo {
name: string;
description: string;
inputSchema: Record<string, unknown>; // JSON Schema for tool arguments
outputSchema?: Record<string, unknown>; // JSON Schema for the result (when server provides it)
}client.call(toolName, args?)
Call any MCP tool by name.
// Read-only tool
const { data } = await client.call('get-token-count');
// Write tool — data contains the full response, xdr is extracted automatically
const { data, xdr, simulationResult } = await client.call('deploy-token', {
deployer: 'GABC...',
config: { /* ... */ },
});Returns CallResult<TData>:
interface CallResult<TData = unknown> {
data: TData; // Full parsed JSON from server (typed when using createMCPClient)
xdr?: string; // Transaction XDR (write ops only)
simulationResult?: unknown; // Simulation metadata, if server includes it
}Throws MCPToolError if the server returns an error response.
client.simulate(toolName, args?)
Preview a transaction without signing or submitting. Returns the assembled XDR and the estimated fee in stroops — useful for showing users the cost before asking them to confirm.
const preview = await client.simulate('deploy-token', {
deployer: 'GABC...',
config: { /* ... */ },
});
console.log(`Estimated fee: ${preview.fee} stroops`);
// Only sign+submit if the user confirms:
const { hash } = await client.signAndSubmit(preview.xdr!, {
signer: secretKeySigner(process.env.SECRET_KEY!),
});
const result = await client.waitForConfirmation(hash);Returns SimulateResult<TData>:
| Field | Type | Description |
|---|---|---|
| xdr | string \| undefined | Assembled transaction XDR, ready to sign |
| fee | string \| undefined | Estimated fee in stroops (extracted from XDR) |
| simulationResult | TData \| undefined | Decoded return value (for read-only tools, this is the answer) |
For read-only tools (get-admin, get-token-count, etc.) xdr and fee are undefined — use simulationResult for the value.
This completes the full 4-step Soroban lifecycle originally specified:
simulate → inspect → signAndSubmit → waitForConfirmation
client.signAndSubmit(xdr, options)
Sign and submit a transaction using a signer adapter.
const result = await client.signAndSubmit(xdr!, {
signer: secretKeySigner(process.env.SECRET_KEY!),
});
// { hash: 'a1b2...', status: 'SUCCESS', result: ... }Returns SubmitResult:
interface SubmitResult {
hash: string;
status: string; // 'SUCCESS' | 'FAILED' | ...
result?: unknown;
}client.waitForConfirmation(hash)
Poll the Soroban RPC until a transaction hash confirms. Useful if you submitted a transaction separately.
const result = await client.waitForConfirmation('a1b2c3...');client.close()
Close the transport connection. Safe to call multiple times.
Signers
Signers are pluggable adapters that implement the Signer interface. Pass one to client.signAndSubmit().
secretKeySigner(secretKey) — Node.js & server-side
Delegates signing and submission to the MCP server's built-in sign-and-submit tool. The server handles auth entry signing, fresh sequence numbers, and LaunchTube submission. The secret key is passed per-request and never stored.
Security note:
secretKeySignertransmits the secret key to the MCP server'ssign-and-submittool. Only use this with trusted, local, or TLS-secured servers. For untrusted servers, preferconnectFreighter()which signs client-side.
import { secretKeySigner } from '@stellar-mcp/client';
// or via entry point:
import { secretKeySigner } from '@stellar-mcp/client/signers/secret';
const result = await client.signAndSubmit(xdr!, {
signer: secretKeySigner(process.env.SECRET_KEY!),
});connectFreighter(networkPassphrase) — Browser only (recommended)
The recommended way to use Freighter in browser dApps. Connects to the wallet once and returns both the wallet address (for your UI) and a pre-connected signer (for transactions). The signer closes over the live wallet connection — it will not re-prompt for the address on each signAndSubmit() call.
Requires @creit.tech/stellar-wallets-kit as a peer dependency.
import { connectFreighter } from '@stellar-mcp/client';
import { Networks } from '@stellar/stellar-sdk';
// Connect once at startup or on button click
const { address, signer } = await connectFreighter(Networks.TESTNET);
// Show address in your UI
headerEl.textContent = address;
// Later, when user submits a transaction:
const result = await client.signAndSubmit(xdr!, { signer });
// Freighter prompts only for signing — no re-connect popupReturns FreighterConnection:
interface FreighterConnection {
address: string; // wallet public key (G...)
signer: Signer; // pre-connected, pass to signAndSubmit()
}Flow:
- Creates
StellarWalletsKitonce - Fetches wallet address once — stored in closure
- Returns
{ address, signer }immediately - On each
signAndSubmit: callsprepare-transaction→ Freighter sign popup → RPC submit → poll
freighterSigner(options?) — Browser only (stateless variant)
The stateless variant — re-connects on every signAndSubmit() call. Useful in scripts or server-side rendering contexts where you don't need to display the wallet address before a transaction. For browser dApps, prefer connectFreighter() above.
Requires @creit.tech/stellar-wallets-kit as a peer dependency.
import { freighterSigner } from '@stellar-mcp/client';
// or via entry point:
import { freighterSigner } from '@stellar-mcp/client/signers/freighter';
const result = await client.signAndSubmit(xdr!, {
signer: freighterSigner(),
});passkeyKitSigner(options) — PasskeyKit smart wallets
Delegates to the MCP server's sign-and-submit tool with a smart wallet contract ID. The server reads its WALLET_SIGNER_SECRET for auth entry signing and uses the provided feePayerSecret for the transaction envelope.
Requires passkey-kit as a peer dependency on the MCP server side.
import { passkeyKitSigner } from '@stellar-mcp/client';
// or via entry point:
import { passkeyKitSigner } from '@stellar-mcp/client/signers/passkey';
const result = await client.signAndSubmit(xdr!, {
signer: passkeyKitSigner({
walletContractId: 'CABC...',
feePayerSecret: process.env.FEE_PAYER_SECRET!,
}),
});Custom Signers
Implement the Signer interface to add your own signing logic:
import type { Signer, SignerContext, SubmitResult } from '@stellar-mcp/client';
const mySigner: Signer = {
async execute(xdr: string, context: SignerContext): Promise<SubmitResult> {
// context provides: rpcUrl, networkPassphrase, mcpCall()
const signedXdr = await mySigningLib.sign(xdr, context.networkPassphrase);
// submit via mcpCall or directly to RPC...
return { hash: '...', status: 'SUCCESS' };
},
};Error Handling
All SDK errors extend MCPClientError:
| Class | Thrown when |
|---|---|
| MCPConnectionError | Cannot connect to MCP server (transport failure, timeout, handshake error) |
| MCPToolError | Server returns isError: true or an error-shaped response — includes toolName |
| TransactionError | Transaction submission or confirmation fails — includes hash when available |
import {
MCPConnectionError,
MCPToolError,
TransactionError,
} from '@stellar-mcp/client';
try {
const { xdr } = await client.call('deploy-token', params);
await client.signAndSubmit(xdr!, { signer });
} catch (err) {
if (err instanceof MCPToolError) {
console.error(`Tool '${err.toolName}' failed:`, err.message);
} else if (err instanceof MCPConnectionError) {
console.error('Could not reach MCP server:', err.message);
} else if (err instanceof TransactionError) {
console.error('Transaction failed:', err.message, 'hash:', err.hash);
}
}Transport
The client automatically negotiates the transport protocol:
- StreamableHTTP (preferred) — single endpoint, stateless HTTP POST
- SSE (fallback) — legacy Server-Sent Events transport
Both protocols use the same URL. Generated MCP servers support both when started with USE_HTTP=true.
Logging
The SDK ships a built-in logger that is silent by default. Enable it for debugging:
import { logger } from '@stellar-mcp/client';
logger.setLevel('debug'); // 'debug' | 'info' | 'warn' | 'error' | 'silent'Integration Tests
Integration tests run against a real MCP server and are gated on an environment variable to keep CI safe:
Server URL: MCP servers generated by
stellar mcp generaterun onhttp://localhost:3001/mcpby default (USE_HTTP=true PORT=3001). All examples below assume this URL. Override with theMCP_URLenv var if your server is on a different port.
# Start your generated MCP server first:
USE_HTTP=true PORT=3001 node /path/to/generated/dist/index.js
# Build the SDK first (CLI tests require dist/cli/generate-types.js)
npm run build
# Read-only tests + CLI generate-types tests (no secret key required)
MCP_URL=http://localhost:3001/mcp RUN_INTEGRATION=1 npm run test:integration
# Full suite including deploy + submit
MCP_URL=http://localhost:3001/mcp \
RUN_INTEGRATION=1 \
TEST_ADMIN_ADDRESS=G... \
TEST_SECRET_KEY=S... \
npm run test:integration| Variable | Default | Required |
|---|---|---|
| RUN_INTEGRATION | — | Must be 1 to run |
| MCP_URL | http://localhost:3001/mcp | No |
| RPC_URL | https://soroban-testnet.stellar.org | No |
| NETWORK_PASSPHRASE | Test SDF Network ; September 2015 | No |
| TEST_ADMIN_ADDRESS | — | Yes for write tests |
| TEST_SECRET_KEY | — | Yes for write tests |
Development
npm run build # compile to dist/
npm run dev # watch mode
npm test # unit tests (90 tests, no server required)
npm run lint # ESLint
npm run format # Prettier --write
npm run format:check # Prettier --check (used in CI)
npm run typecheck # tsc --noEmitLicense
MIT
