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