npm package: @stellar-mcp/client
Description: Type-safe programmatic client for Stellar MCP servers — discover tools, call contracts, sign and submit transactions
Author: Jose Toscano
Latest version: 0.2.0

# @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

```bash
npm install @stellar-mcp/client
```

### Peer 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 |

```bash
# 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-kit
```

---

## Quick Start — Type-Safe (Recommended)

Generate a typed client file from your running MCP server:

```bash
npx mcp-generate-types --url http://localhost:3001/mcp --out ./src/mcp-types.ts
```

The 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.

```ts
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`.

```ts
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 `ServerTools` ToolMap that links each tool name to its arg and result types
- A `createMCPClient` factory function — the recommended way to create clients

```bash
# 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 --help
```

**Options:**

| 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.

```ts
// 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:**

```bash
npx mcp-generate-types --url http://localhost:3001/mcp --out ./src/mcp-types.ts
```

---

## Schema 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.

```ts
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.

```ts
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 }`.

```ts
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`.

```ts
parseArgValue('42', { type: 'number', ... })   // → 42
parseArgValue('true', { type: 'boolean', ... }) // → true
parseArgValue('{"a":1}', { type: 'object', ... }) // → { a: 1 }
parseArgValue('', { type: 'string', nullable: true, ... }) // → null
```

### `isReadOperation(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`).

```ts
isReadOperation('get-admin')    // true
isReadOperation('list_tokens')  // true
isReadOperation('deploy-token') // false
```

### `ArgDef`

```ts
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.

```ts
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.

```ts
const tools = await client.listTools();
// [{ name: 'deploy-token', description: '...', inputSchema: { ... } }, ...]
```

Returns `ToolInfo[]`:

```ts
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.

```ts
// 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>`:

```ts
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.

```ts
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.

```ts
const result = await client.signAndSubmit(xdr!, {
  signer: secretKeySigner(process.env.SECRET_KEY!),
});
// { hash: 'a1b2...', status: 'SUCCESS', result: ... }
```

Returns `SubmitResult`:

```ts
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.

```ts
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:** `secretKeySigner` transmits the secret key to the MCP server's `sign-and-submit` tool. Only use this with trusted, local, or TLS-secured servers. For untrusted servers, prefer `connectFreighter()` which signs client-side.

```ts
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.

```ts
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 popup
```

Returns `FreighterConnection`:

```ts
interface FreighterConnection {
  address: string;  // wallet public key (G...)
  signer:  Signer;  // pre-connected, pass to signAndSubmit()
}
```

**Flow:**
1. Creates `StellarWalletsKit` once
2. Fetches wallet address once — stored in closure
3. Returns `{ address, signer }` immediately
4. On each `signAndSubmit`: calls `prepare-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.

```ts
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.

```ts
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:

```ts
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 |

```ts
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:

1. **StreamableHTTP** (preferred) — single endpoint, stateless HTTP POST
2. **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:

```ts
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 generate` run on **`http://localhost:3001/mcp`** by default (`USE_HTTP=true PORT=3001`). All examples below assume this URL. Override with the `MCP_URL` env var if your server is on a different port.

```bash
# 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

```bash
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 --noEmit
```

---

## License

MIT