Files
contextvm-docs/src/content/docs/tutorials/client-server-communication.md
2025-07-18 10:17:22 +02:00

6.0 KiB

title, description
title description
Tutorial Client-Server Communication 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.
  • 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:

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);
});

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.

bun run server.ts

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


2. The Client (client.ts)

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);
});

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:

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.