contextvm-sdk 0.1.1

Rust SDK for the ContextVM protocol — MCP over Nostr
Documentation
---
title: Nostr Client Transport
description: A client-side component for communicating with MCP servers over Nostr.
---

# Nostr Client Transport

The `NostrClientTransport` is a key component of the `@contextvm/sdk`, enabling MCP clients to communicate with remote MCP servers over the Nostr network. It implements the `Transport` interface from the `@modelcontextprotocol/sdk`, making it a plug-and-play solution for any MCP client.

## Overview

The `NostrClientTransport` handles all the complexities of Nostr-based communication, including:

- Connecting to Nostr relays.
- Subscribing to events from a specific server.
- Sending MCP requests as Nostr events.
- Receiving and processing responses and notifications.
- Handling encryption and decryption of messages.
- Working with server discovery flows based on announcements and relay-list metadata.

By using this transport, an MCP client can interact with a Nostr-enabled MCP server without needing to implement any Nostr-specific logic itself.

## `NostrTransportOptions`

To create an instance of `NostrClientTransport`, you must provide a configuration object that implements the `NostrTransportOptions` interface:

```typescript
export interface NostrTransportOptions extends Omit<
  BaseNostrTransportOptions,
  'relayHandler'
> {
  relayHandler?: RelayHandler | string[];
  serverPubkey: string;
  discoveryRelayUrls?: string[];
  fallbackOperationalRelayUrls?: string[];
  isStateless?: boolean;
}
```

- **`relayHandler`** (optional): Explicit operational relays for the client. Accepts either a `RelayHandler` instance or a `string[]`. When omitted, the client can still resolve operational relays from `nprofile` hints or CEP-17 discovery.
- **`serverPubkey`**: The target server identity. Accepts a hex pubkey, `npub`, or `nprofile`. The transport normalizes this to the server's hex public key internally.
- **`discoveryRelayUrls`** (optional): Relay URLs used only for CEP-17 relay-list discovery when no operational relays are explicitly configured.
- **`fallbackOperationalRelayUrls`** (optional): Non-authoritative operational relays that are probed in parallel with CEP-17 discovery when no explicit relays or `nprofile` hints are available.
- **`isStateless`** (optional): When set to `true`, enables stateless mode for the client transport. In stateless mode, the client emulates the server's initialize response without requiring a full server initialization roundtrip. This enables faster startup and reduced network overhead. Default is `false`.

When `discoveryRelayUrls` is omitted, the transport uses the SDK bootstrap relay defaults for CEP-17 discovery.

## Usage Example

Here's how you can use the `NostrClientTransport` with an MCP client from the `@modelcontextprotocol/sdk`:

```typescript
import { Client } from '@modelcontextprotocol/sdk/client';
import { NostrClientTransport } from '@contextvm/sdk';
import { EncryptionMode } from '@contextvm/sdk';
import { PrivateKeySigner } from '@contextvm/sdk';
import { ApplesauceRelayPool } from '@contextvm/sdk';

// 1. Configure the signer and relay handler
const signer = new PrivateKeySigner('your-private-key'); // Replace with your actual private key
const relayPool = new ApplesauceRelayPool(['wss://relay.damus.io']);

// 2. Set the identity of the target server
const REMOTE_SERVER = 'npub1...';

// 3. Create the transport instance
const clientNostrTransport = new NostrClientTransport({
  signer,
  relayHandler: relayPool,
  serverPubkey: REMOTE_SERVER,
  encryptionMode: EncryptionMode.OPTIONAL,
});

// 4. Create and connect the MCP client
const mcpClient = new Client({
  name: 'My Client',
  version: '1.0.0',
});

await mcpClient.connect(clientNostrTransport);

// 5. Use the client to interact with the server
const tools = await mcpClient.listTools();
console.log('Available tools:', tools);

// 6. Close the connection when done
// await mcpClient.close();
```

> **Note**: The `relayHandler` option also accepts a `string[]` of relay URLs, in which case an `ApplesauceRelayPool` will be created automatically. See the [Base Nostr Transport]/transports/base-nostr-transport documentation for details.

> **Note**: On the client transport, `relayHandler` is optional. If it is omitted, the transport can still resolve operational relays from `nprofile` relay hints or CEP-17 relay-list discovery.

### Identity input precedence

[`NostrClientTransport`](contextvm-docs/src/content/docs/ts-sdk/transports/nostr-client-transport.md:56) resolves server identity and relays conservatively:

1. explicit operational relays from `relayHandler`
2. relay hints embedded in `nprofile`
3. CEP-17 relay-list discovery via `discoveryRelayUrls`
4. `fallbackOperationalRelayUrls`
5. SDK bootstrap relay defaults when `discoveryRelayUrls` is omitted

This keeps the active relay set minimal and avoids automatically merging every possible relay source.

## How It Works

1.  **`start()`**: When `mcpClient.connect()` is called, it internally calls the transport's `start()` method. This method connects to the relays and subscribes to events targeting the client's public key.
    - In **stateless mode** (`isStateless: true`), the client emulates the server's initialize response without sending it over the network, skipping the `notifications/initialized` message exchange.
    - In **standard mode** (`isStateless: false` or undefined), the client performs the full initialization roundtrip with the server.
2.  **`send(message)`**: When you call an MCP method like `mcpClient.listTools()`, the client creates a JSON-RPC request and passes it to the transport's `send()` method. The transport then:
    - Wraps the message in a Nostr event.
    - Signs the event.
    - Optionally encrypts it.
    - Publishes it to the relays, targeting the `serverPubkey`.
3.  **Event Processing**: The transport listens for incoming events. When an event is received, it is decrypted (if necessary) and converted back into a JSON-RPC message.
    - If the message is a **response** (correlated by the original event ID), it is passed to the MCP client to resolve the pending request.
    - If the message is a **notification**, it is emitted through the `onmessage` handler.

## Server Discovery and Relay Selection

The client transport accepts a known [`serverPubkey`](contextvm-docs/src/content/docs/ts-sdk/transports/nostr-client-transport.md:28) in multiple forms and can now resolve operational relays automatically when needed.

Typical flow:

1. If explicit operational relays are configured, the transport uses them directly.
2. Else, if the server identity is an `nprofile` with relay hints, the transport uses those hints as the operational relay set.
3. Else, the transport starts CEP-17 relay-list discovery and fallback operational relay probing in parallel.
4. If CEP-17 returns a usable `kind:10002` relay list first, that authoritative result is used.
5. If [`fallbackOperationalRelayUrls`]contextvm-docs/src/content/docs/ts-sdk/transports/nostr-client-transport.md proves connectivity first while discovery is still unresolved, the transport proceeds with that non-authoritative relay set.
6. If [`discoveryRelayUrls`]contextvm-docs/src/content/docs/ts-sdk/transports/nostr-client-transport.md:31 is omitted, the transport still uses the SDK bootstrap relays for the CEP-17 lookup.
7. The transport prefers unmarked `r` tags as the operational relay set, matching the recommended ContextVM profile in [`CEP-17`]contextvm-docs/src/content/docs/spec/ceps/cep-17.md:52.

Example with discovery fallback:

```typescript
import {
  EncryptionMode,
  NostrClientTransport,
  PrivateKeySigner,
} from '@contextvm/sdk';

const clientNostrTransport = new NostrClientTransport({
  signer: new PrivateKeySigner('your-private-key'),
  serverPubkey: 'nprofile1...',
  discoveryRelayUrls: ['wss://relay.damus.io', 'wss://nos.lol'],
  encryptionMode: EncryptionMode.OPTIONAL,
});
```

Example with non-authoritative operational fallback:

```typescript
const clientNostrTransport = new NostrClientTransport({
  signer: new PrivateKeySigner('your-private-key'),
  serverPubkey: 'npub1...',
  discoveryRelayUrls: ['wss://relay.damus.io', 'wss://nos.lol'],
  fallbackOperationalRelayUrls: ['wss://local-relay.example.com'],
  encryptionMode: EncryptionMode.OPTIONAL,
});
```

Example using discovery with no explicit relay handler:

```typescript
const clientNostrTransport = new NostrClientTransport({
  signer: new PrivateKeySigner('your-private-key'),
  serverPubkey: 'npub1...',
  encryptionMode: EncryptionMode.OPTIONAL,
});
```

In this case the client will attempt to resolve operational relays automatically using the normal precedence rules.

### Authoritative vs fallback relays

- [`relayHandler`]contextvm-docs/src/content/docs/ts-sdk/transports/nostr-client-transport.md:29 is authoritative and explicit.
- [`discoveryRelayUrls`]contextvm-docs/src/content/docs/ts-sdk/transports/nostr-client-transport.md:31 identifies where the client should look for CEP-17 metadata.
- [`fallbackOperationalRelayUrls`]contextvm-docs/src/content/docs/ts-sdk/transports/nostr-client-transport.md is non-authoritative and only exists to reduce latency when discovery is slow or unresolved.

This separation keeps protocol correctness intact while still allowing practical connection recovery.

### Important distinction

Servers may publish discoverability events to additional bootstrap relays that are not included in their advertised `kind:10002` relay list. Clients should treat the published `r` tags as the authoritative operational relay set, while bootstrap relays are only helpful for finding that metadata in the first place.

Under the recommended ContextVM profile, clients should expect unmarked `r` tags and use them first. Directional `read` / `write` markers are treated as compatibility hints rather than the normal case.

## Stateless Mode

The stateless mode is designed to optimize performance by reducing the initialization overhead:

- **Faster Startup**: By emulating the initialize response, the client can begin operations immediately without waiting for server response.
- **Reduced Network Overhead**: Eliminates the need for the initialization roundtrip.
- **Use Cases**: Ideal for scenarios where the client needs to quickly connect and interact with the server, such as in serverless functions or short-lived processes.

To enable stateless mode, set `isStateless: true` in the transport configuration:

```typescript
const clientNostrTransport = new NostrClientTransport({
  signer,
  serverPubkey: REMOTE_SERVER,
  encryptionMode: EncryptionMode.OPTIONAL,
  isStateless: true, // Enable stateless mode
});
```

**Note**: The stateless mode might not work with all servers.

## Next Steps

Next, we will look at the server-side counterpart to this transport:

- **[Nostr Server Transport]/transports/nostr-server-transport**: For exposing MCP servers to the Nostr network.