server-dev
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
thresholdBytesorchunkSizeBytesfor stricter relay environments - tighten receiver
policyvalues 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
relayHandlerfor the relays where your server actually operates - Use
relayListUrlsonly if you need to override the advertised relay list - Use
bootstrapRelayUrlswhen you want broader discoverability publication without advertising those relays as operational endpoints - Set
publishRelayList: falseonly 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:
serverInfopowers ContextVM discovery and initialize semanticsprofileMetadatapowers an optional Nostr social/profile identity viakind: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
outputSchemawhen clients should be able to rely on a stable output shape. - Return
structuredContentfor machine-readable data. - Return
contentonly for human-readable output. It does not need to duplicatestructuredContent. - If human-readable output is unnecessary,
contentcan be an empty array:[]. - A good pattern is: concise
contentfor people, completestructuredContentfor 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:
- Implement and test your server logic using a standard MCP transport (commonly STDIO) so it can be inspected.
- Use the Inspector to iterate on tool schemas and error handling.
- Once stable, swap the transport to
NostrServerTransport.
If you need details on Inspector usage and common debugging steps, read:
Reference Materials
references/transport-config.md- All configuration optionsreferences/security-patterns.md- Access control patternsreferences/gateway-pattern.md- Exposing existing servers- MCP structured output guide: define
outputSchema, returnstructuredContent, and keepcontenthuman-oriented
More from contextvm/cvmi
overview
Understand ContextVM protocol fundamentals, architecture, and core concepts. Use when users need to learn about ContextVM basics, how it bridges MCP with Nostr, protocol design principles, event kinds, or the relationship between MCP and Nostr in decentralized communication.
32concepts
Understand ContextVM core concepts, architecture decisions, and frequently asked questions. Use when users need clarification on what ContextVM is, why it uses Nostr, decentralization benefits, public vs private servers, network topology, or comparisons with traditional MCP.
32typescript-sdk
Use the @contextvm/sdk TypeScript SDK effectively. Reference for core interfaces, signers, relay handlers, transports, encryption, logging, and SDK patterns. Use when implementing SDK components, extending interfaces, configuring transports, or debugging SDK usage.
32client-dev
Build MCP clients that connect to ContextVM servers over Nostr. Use when creating clients, discovering servers, connecting to remote servers, handling encrypted connections, or implementing the proxy pattern for existing MCP clients.
31payments
Implement CEP-8 payments in ContextVM using the @contextvm/sdk payments middleware. Use when building paid servers/clients, configuring priced capabilities, integrating payment processors/handlers, or troubleshooting payment notification flows.
24troubleshooting
Diagnose and resolve ContextVM connection issues, relay problems, encryption failures, authentication errors, and common deployment problems. Use when users report errors, connection failures, timeout issues, or unexpected behavior with ContextVM clients, servers, gateways, or proxies.
17