Nostr Client Transport
Nostr Client Transport
Section titled “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
Section titled “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.
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
Section titled “NostrTransportOptions”To create an instance of NostrClientTransport
, you must provide a configuration object that implements the NostrTransportOptions
interface:
export interface NostrTransportOptions extends BaseNostrTransportOptions { serverPubkey: string; isStateless?: boolean;}
serverPubkey
: The public key of the target MCP server. The transport will only listen for events from this public key.isStateless
(optional): When set totrue
, 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 isfalse
.
Usage Example
Section titled “Usage Example”Here’s how you can use the NostrClientTransport
with an MCP client from the @modelcontextprotocol/sdk
:
import { Client } from "@modelcontextprotocol/sdk/client";import { NostrClientTransport } from "@contextvm/sdk";import { EncryptionMode } from "@contextvm/sdk";import { PrivateKeySigner } from "@contextvm/sdk";import { SimpleRelayPool } from "@contextvm/sdk";
// 1. Configure the signer and relay handlerconst signer = new PrivateKeySigner("your-private-key"); // Replace with your actual private keyconst relayPool = new SimpleRelayPool(["wss://relay.damus.io"]);
// 2. Set the public key of the target serverconst REMOTE_SERVER_PUBKEY = "remote-server-public-key";
// 3. Create the transport instanceconst clientNostrTransport = new NostrClientTransport({ signer, relayHandler: relayPool, serverPubkey: REMOTE_SERVER_PUBKEY, encryptionMode: EncryptionMode.OPTIONAL,});
// 4. Create and connect the MCP clientconst mcpClient = new Client({ name: "My Client", version: "1.0.0",});
await mcpClient.connect(clientNostrTransport);
// 5. Use the client to interact with the serverconst tools = await mcpClient.listTools();console.log("Available tools:", tools);
// 6. Close the connection when done// await mcpClient.close();
How It Works
Section titled “How It Works”start()
: WhenmcpClient.connect()
is called, it internally calls the transport’sstart()
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 thenotifications/initialized
message exchange. - In standard mode (
isStateless: false
or undefined), the client performs the full initialization roundtrip with the server.
- In stateless mode (
send(message)
: When you call an MCP method likemcpClient.listTools()
, the client creates a JSON-RPC request and passes it to the transport’ssend()
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
.
- 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.
Stateless Mode
Section titled “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:
const clientNostrTransport = new NostrClientTransport({ signer, relayHandler: relayPool, serverPubkey: REMOTE_SERVER_PUBKEY, encryptionMode: EncryptionMode.OPTIONAL, isStateless: true, // Enable stateless mode});
Note: The stateless mode might not work with all servers.
Next Steps
Section titled “Next Steps”Next, we will look at the server-side counterpart to this transport:
- Nostr Server Transport: For exposing MCP servers to the Nostr network.