Tutorial Client-Server Communication
Tutorial: Client-Server Communication
Section titled “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
Section titled “Objective”We will build two separate scripts:
server.ts: An MCP server that exposes a simple “echo” tool.client.ts: An MCP client that connects to the server, lists the available tools, and calls the “echo” tool.
Prerequisites
Section titled “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-toolscommands.
1. The Server (server.ts)
Section titled “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 { NostrServerTransport } from "@contextvm/sdk";import { PrivateKeySigner } from "@contextvm/sdk";import { SimpleRelayPool } from "@contextvm/sdk";import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";import { z } from "zod";// --- Configuration ---// IMPORTANT: Replace with your own private keyconst 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 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, 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
Section titled “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.tsThe server will start, print its public key, and wait for incoming client connections.
2. The Client (client.ts)
Section titled “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 "@contextvm/sdk";import { PrivateKeySigner } from "@contextvm/sdk";import { SimpleRelayPool } from "@contextvm/sdk";
// --- Configuration ---// IMPORTANT: Replace with the server's public key from the server outputconst SERVER_PUBKEY = "the-public-key-printed-by-server.ts";
// IMPORTANT: Replace with your own private keyconst 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({ 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
Section titled “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.tsExpected Output
Section titled “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.