server-dev

Installation
SKILL.md

ContextVM Server Development

Build MCP servers that expose capabilities over Nostr using the @contextvm/sdk.

Quick Start

Create a basic ContextVM server:

import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { NostrServerTransport } from '@contextvm/sdk';
import { PrivateKeySigner } from '@contextvm/sdk';
import { ApplesauceRelayPool } from '@contextvm/sdk';

const signer = new PrivateKeySigner(process.env.SERVER_PRIVATE_KEY!);
const relayPool = new ApplesauceRelayPool(['wss://relay.contextvm.org', 'wss://cvm.otherstuff.ai']);

const server = new McpServer({
  name: 'my-server',
  version: '1.0.0',
});

// Register tools
server.registerTool('echo', { description: 'Echo back the input' }, async ({ message }) => ({
  content: [{ type: 'text', text: `Echo: ${message}` }],
}));

const transport = new NostrServerTransport({
  signer,
  relayHandler: relayPool,
  serverInfo: {
    name: 'My ContextVM Server',
    website: 'https://example.com',
  },
});

await server.connect(transport);
console.log('Server running on Nostr');

NostrServerTransport Options

Option Type Description
signer NostrSigner Required. Signs all Nostr events
relayHandler RelayHandler | string[] Required. Relay connection manager.
serverInfo ServerInfo Optional. Metadata for announcements
profileMetadata ProfileMetadata Optional. Nostr kind:0 social profile (CEP-23)
isAnnouncedServer boolean Publish server announcements. Default: false
publishRelayList boolean Publish kind:10002 relay-list metadata
relayListUrls string[] Explicit relay URLs to advertise
bootstrapRelayUrls string[] Extra discoverability publication relays
allowedPublicKeys string[] Whitelist client public keys
isPubkeyAllowed function Dynamic pubkey authorization callback
excludedCapabilities CapabilityExclusion[] Bypass whitelist for specific methods
isCapabilityExcluded function Dynamic capability exclusion callback
injectClientPubkey boolean Inject client pubkey into _meta. Default: false
injectRequestEventId boolean Inject inbound request event ID into _meta. Default: false
encryptionMode EncryptionMode OPTIONAL, REQUIRED, or DISABLED
oversizedTransfer object CEP-22 oversized payload transfer configuration

Oversized Transfer

NostrServerTransport supports CEP-22 oversized payload transfer automatically.

  • enabled by default
  • automatically reassembles oversized incoming client requests
  • automatically fragments oversized server responses when needed
  • does not require server tool handlers to manage chunking directly

Typical configuration:

const transport = new NostrServerTransport({
  signer,
  relayHandler: relayPool,
  oversizedTransfer: {
    enabled: true,
  },
});

Useful reasons to tune it:

  • disable the feature entirely with enabled: false
  • lower thresholdBytes or chunkSizeBytes for stricter relay environments
  • tighten receiver policy values to reduce memory exposure

This is especially relevant for servers that return large tool results or accept large structured inputs.

Access Control

Public Key Whitelisting

Restrict which clients can connect:

const transport = new NostrServerTransport({
  signer,
  relayHandler: relayPool,
  allowedPublicKeys: ['client1-pubkey-hex', 'client2-pubkey-hex'],
});

Capability Exclusions

Allow specific operations from any client:

const transport = new NostrServerTransport({
  signer,
  relayHandler: relayPool,
  allowedPublicKeys: ['trusted-client'],
  excludedCapabilities: [
    { method: 'tools/list' }, // Anyone can list tools
    { method: 'tools/call', name: 'public_tool' }, // Specific tool is public
  ],
});

Dynamic Authorization

Use callbacks for runtime authorization decisions:

const transport = new NostrServerTransport({
  signer,
  relayHandler: relayPool,
  // Static allowlist (optional)
  allowedPublicKeys: ['admin-pubkey'],
  // Dynamic authorization - both must pass when both are configured
  isPubkeyAllowed: async (clientPubkey) => {
    const subscription = await db.subscriptions.findByPubkey(clientPubkey);
    return subscription?.isActive ?? false;
  },
  // Dynamic capability exclusions
  isCapabilityExcluded: async (exclusion) => {
    // Check feature flags for temporarily public capabilities
    if (exclusion.method === 'tools/call') {
      return await featureFlags.isToolPublic(exclusion.name);
    }
    return false;
  },
});

Public Server Announcements

Enable discovery by publishing replaceable events:

const transport = new NostrServerTransport({
  signer,
  relayHandler: relayPool,
  isAnnouncedServer: true,
  publishRelayList: true,
  bootstrapRelayUrls: ['wss://relay.damus.io', 'wss://nos.lol'],
  serverInfo: {
    name: 'Weather Service',
    about: 'Get weather data worldwide',
    website: 'https://weather.example.com',
  },
});

Publishes events on kinds 11316-11320 with your server's capabilities. In the TypeScript SDK, publishRelayList is independent from isAnnouncedServer and defaults to enabled, so relay-list metadata is published unless you explicitly opt out.

CEP-15 Common Tool Schemas

If a tool is meant to implement a shared public contract, decorate the transport with withCommonToolSchemas() before connecting the server.

const transport = withCommonToolSchemas(
  new NostrServerTransport({
    signer,
    relayHandler: relayPool,
    isAnnouncedServer: true,
  }),
  {
    tools: [{ name: 'translate_text' }],
  }
);

await server.connect(transport);

This makes the SDK publish _meta['io.contextvm/common-schema'].schemaHash in tools/list and matching i/k announcement tags for the opted-in tools.

Use this only for tools that intentionally follow a shared CEP-15 schema. Tool name and outputSchema affect compatibility, and remote $ref values must be resolved before hashing.

Relay-list publication strategy

  • CEP-17 is protocol-level and implementation-agnostic; the defaults below describe the TypeScript SDK behavior, not a protocol requirement
  • Use relayHandler for the relays where your server actually operates
  • Use relayListUrls only if you need to override the advertised relay list
  • Use bootstrapRelayUrls when you want broader discoverability publication without advertising those relays as operational endpoints
  • Set publishRelayList: false only if you intentionally want to disable CEP-17 relay-list publication

Server Profile Metadata (CEP-23)

Publish a Nostr kind:0 social profile alongside your server. This is opt-in and independent from isAnnouncedServer.

serverInfo and profileMetadata serve different purposes:

  • serverInfo powers ContextVM discovery and initialize semantics
  • profileMetadata powers an optional Nostr social/profile identity via kind:0

This separation matters because some servers want to be discoverable over ContextVM without maintaining a public social profile, while others want both.

const transport = new NostrServerTransport({
  signer,
  relayHandler: relayPool,
  isAnnouncedServer: true,
  profileMetadata: {
    name: 'My Awesome MCP Server',
    about: 'Public MCP provider on Nostr',
    picture: 'https://example.com/avatar.png',
    website: 'https://example.com',
    nip05: 'server@example.com',
  },
  serverInfo: {
    name: 'My Awesome MCP Server',
    website: 'https://example.com',
  },
});

A server can publish profile metadata even when it does not publish public announcement events. The profile event is sent through the same discoverability publication path as relay-list and announcement events, so bootstrapRelayUrls also help distribute profile metadata in local or non-WebSocket relay environments.

Client Public Key Injection

Access the client's identity in your tools:

const transport = new NostrServerTransport({
  signer,
  relayHandler: relayPool,
  injectClientPubkey: true,
});

// In your tool handler, access _meta.clientPubkey
server.registerTool("personalized", {...}, async (args, extra) => {
  const clientPubkey = extra._meta?.clientPubkey;
  // Use pubkey for personalization, rate limiting, etc.
});

Request Event ID Injection

When injectRequestEventId is enabled, the inbound Nostr event ID is injected into _meta.requestEventId. Use transport.getNostrRequestEvent() inside a tool handler to retrieve the full signed Nostr event, including the sender's pubkey and all event metadata.

const transport = new NostrServerTransport({
  signer,
  relayHandler: relayPool,
  injectRequestEventId: true,
});

server.registerTool(
  'whoami',
  {
    description: 'Returns the public key of the client that invoked this tool.',
    inputSchema: {},
  },
  async (_args, extra) => {
    const requestEventId = extra._meta?.requestEventId;
    if (requestEventId) {
      const requestEvent = transport.getNostrRequestEvent(requestEventId);
      if (requestEvent) {
        return {
          content: [
            {
              type: 'text',
              text: `Called by ${requestEvent.pubkey} at timestamp ${requestEvent.created_at}`,
            },
          ],
        };
      }
    }
    return {
      content: [{ type: 'text', text: 'unknown caller' }],
    };
  }
);

Structured Outputs

Use structured outputs when your server is primarily consumed programmatically and clients benefit from validated machine-readable tool results.

import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import * as z from 'zod/v4';

const server = new McpServer({
  name: 'weather-server',
  version: '1.0.0',
});

server.registerTool(
  'get_weather',
  {
    description: 'Get weather information for a city',
    inputSchema: z.object({ city: z.string(), country: z.string() }),
    outputSchema: z.object({
      temperature: z.object({ celsius: z.number(), fahrenheit: z.number() }),
      conditions: z.string(),
    }),
  },
  async () => ({
    content: [],
    structuredContent: {
      temperature: { celsius: 22, fahrenheit: 71.6 },
      conditions: 'sunny',
    },
  })
);
  • Use outputSchema when clients should be able to rely on a stable output shape.
  • Return structuredContent for machine-readable data.
  • Return content only for human-readable output. It does not need to duplicate structuredContent.
  • If human-readable output is unnecessary, content can be an empty array: [].
  • A good pattern is: concise content for people, complete structuredContent for code.

Server Templates

See assets/server-template.ts for a complete starting point.

Debugging (MCP Inspector)

Use the MCP Inspector to validate your MCP server behavior (tools/resources/prompts schemas, request/response shape) before exposing it via ContextVM.

From the MCP docs, the Inspector is typically run via npx:

npx @modelcontextprotocol/inspector <command>

Practical workflow for ContextVM:

  1. Implement and test your server logic using a standard MCP transport (commonly STDIO) so it can be inspected.
  2. Use the Inspector to iterate on tool schemas and error handling.
  3. Once stable, swap the transport to NostrServerTransport.

If you need details on Inspector usage and common debugging steps, read:

Reference Materials

Related skills

More from contextvm/cvmi

Installs
31
Repository
contextvm/cvmi
GitHub Stars
1
First Seen
Feb 12, 2026