contextvm-sdk 0.1.1

Rust SDK for the ContextVM protocol — MCP over Nostr
Documentation
---
title: Tutorial Client-Server Communication
description: A step-by-step guide to setting up a basic MCP client and server that communicate directly over the Nostr network using the @contextvm/sdk.
---

# Tutorial: Client-Server Communication

This tutorial provides a complete, step-by-step guide to setting up a basic MCP client and server that communicate directly over the Nostr network using the `@contextvm/sdk`.

## Objective

We will build two separate scripts:

1.  `server.ts`: An MCP server that exposes a simple "echo" tool.
2.  `client.ts`: An MCP client that connects to the server, lists the available tools, and calls the "echo" tool.

## Prerequisites

- You have completed the [Quick Overview]/getting-started/quick-overview/.
- You have two Nostr private keys (one for the server, one for the client). You can generate new keys using various tools, or by running `nostr-tools` commands.

---

## 1. The Server (`server.ts`)

First, let's create the MCP server. This server will use the `NostrServerTransport` to listen for requests on the Nostr network.

Create a new file named `server.ts`:

```typescript
import { NostrServerTransport } from '@contextvm/sdk';
import { PrivateKeySigner } from '@contextvm/sdk';
import { ApplesauceRelayPool } from '@contextvm/sdk';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
// --- Configuration ---
// IMPORTANT: Replace with your own private key
const SERVER_PRIVATE_KEY_HEX =
  process.env.SERVER_PRIVATE_KEY || 'your-32-byte-server-private-key-in-hex';
const RELAYS = ['wss://relay.damus.io', 'wss://nos.lol'];

// --- Main Server Logic ---
async function main() {
  // 1. Setup Signer and Relay Pool
  const signer = new PrivateKeySigner(SERVER_PRIVATE_KEY_HEX);
  const relayPool = new ApplesauceRelayPool(RELAYS);
  const serverPubkey = await signer.getPublicKey();

  console.log(`Server Public Key: ${serverPubkey}`);
  console.log('Connecting to relays...');

  // 2. Create and Configure the MCP Server
  const mcpServer = new McpServer({
    name: 'nostr-echo-server',
    version: '1.0.0',
  });

  // 3. Define a simple "echo" tool
  mcpServer.registerTool(
    'echo',
    {
      title: 'Echo Tool',
      description: 'Echoes back the provided message',
      inputSchema: { message: z.string() },
    },
    async ({ message }) => ({
      content: [{ type: 'text', text: `Tool echo: ${message}` }],
    }),
  );

  // 4. Configure the Nostr Server Transport
  const serverTransport = new NostrServerTransport({
    signer,
    relayHandler: relayPool,
    isAnnouncedServer: true,
    serverInfo: {
      name: 'CTXVM Echo Server',
    },
  });

  // 5. Connect the server
  await mcpServer.connect(serverTransport);

  console.log('Server is running and listening for requests on Nostr...');
  console.log('Press Ctrl+C to exit.');
}

main().catch((error) => {
  console.error('Failed to start server:', error);
  process.exit(1);
});
```

### Running the Server

To run the server, execute the following command in your terminal. Be sure to replace the placeholder private key or set the `SERVER_PRIVATE_KEY` environment variable.

```bash
bun run server.ts
```

The server will start, print its public key, and wait for incoming client connections.

Because this example uses `isAnnouncedServer: true`, the server also publishes discoverability announcements. If you only want direct connections from clients that already know your public key, you can leave announcements disabled.

---

## 2. The Client (`client.ts`)

Next, let's create the client that will connect to our server.

Create a new file named `client.ts`:

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

// --- Configuration ---
// IMPORTANT: Replace with the server's public key from the server output
const SERVER_PUBKEY = 'the-public-key-printed-by-server.ts';

// IMPORTANT: Replace with your own private key
const CLIENT_PRIVATE_KEY_HEX =
  process.env.CLIENT_PRIVATE_KEY || 'your-32-byte-client-private-key-in-hex';
const RELAYS = ['wss://relay.damus.io', 'wss://nos.lol'];

// --- Main Client Logic ---
async function main() {
  // 1. Setup Signer and Relay Pool
  const signer = new PrivateKeySigner(CLIENT_PRIVATE_KEY_HEX);
  const relayPool = new ApplesauceRelayPool(RELAYS);

  console.log('Connecting to relays...');

  // 2. Configure the Nostr Client Transport
  const clientTransport = new NostrClientTransport({
    signer,
    relayHandler: relayPool,
    serverPubkey: SERVER_PUBKEY,
  });

  // 3. Create and connect the MCP Client
  const mcpClient = new Client({
    name: 'my-client',
    version: '0.0.1',
  });
  await mcpClient.connect(clientTransport);

  console.log('Connected to server!');

  // 4. List the available tools
  console.log('\nListing available tools...');
  const tools = await mcpClient.listTools();
  console.log('Tools:', tools);

  // 5. Call the "echo" tool
  console.log('\nCalling the "echo" tool...');
  const echoResult = await mcpClient.callTool({
    name: 'echo',
    arguments: { message: 'Hello, Nostr!' },
  });
  console.log('Echo result:', echoResult);

  // 6. Close the connection
  await mcpClient.close();
  console.log('\nConnection closed.');
}

main().catch((error) => {
  console.error('Client failed:', error);
  process.exit(1);
});
```

### Running the Client

Open a **new terminal window** (leave the server running in the first one). Before running the client, make sure to update the `SERVER_PUBKEY` variable with the public key that your `server.ts` script printed to the console.

Then, run the client:

```bash
bun run client.ts
```

## Expected Output

If everything is configured correctly, you should see the following output in the client's terminal:

```
Connecting to relays...
Connected to server!

Listing available tools...
Tools: {
  tools: [
    {
      name: 'echo',
      description: 'Replies with the input it received.',
      inputSchema: { ... }
    }
  ]
}

Calling the "echo" tool...
Echo result: You said: Hello, Nostr!

Connection closed.
```

And that's it! You've successfully created an MCP client and server that communicate securely and decentrally over the Nostr network.