Skip to content

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.

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.
  • You have completed the 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.

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:

import { McpServer, Tool } from "@modelcontextprotocol/sdk/server";
import { NostrServerTransport } from "@ctxvm/sdk/transport";
import { PrivateKeySigner } from "@ctxvm/sdk/signer";
import { SimpleRelayPool } from "@ctxvm/sdk/relay";
import { generateSecretKey, getPublicKey } from "nostr-tools/pure";
// --- 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 SimpleRelayPool(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
server.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,
isPublicServer: true, // Announce this server on the Nostr network
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);
});

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.

Terminal window
bun run server.ts

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


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

Create a new file named client.ts:

import { Client } from "@modelcontextprotocol/sdk/client";
import { NostrClientTransport } from "@ctxvm/sdk/transport";
import { PrivateKeySigner } from "@ctxvm/sdk/signer";
import { SimpleRelayPool } from "@ctxvm/sdk/relay";
// --- 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 SimpleRelayPool(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();
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);
});

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:

Terminal window
bun run client.ts

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.