Nostr Server Transport
--- title: Nostr Server Transport description: A server-side component for exposing MCP servers over Nostr. --- # Nostr Server Transport The `NostrServerTransport` is the server-side counterpart to the [`NostrClientTransport`](/transports/nostr-client-transport). It allows an MCP server to expose its capabilities to the Nostr network, making them discoverable and usable by any Nostr-enabled client. Like the client transport, it implements the `Transport` interface from the `@modelcontextprotocol/sdk`. ## Overview The `NostrServerTransport` is responsible for: - Listening for incoming MCP requests from Nostr clients. - Managing individual client sessions and their state (e.g., initialization, encryption). - Handling request/response correlation to ensure responses are sent to the correct client. - Sending responses and notifications back to clients over Nostr. - Optionally announcing the server and its capabilities to the network for public discovery. ## `NostrServerTransportOptions` The transport is configured via the `NostrServerTransportOptions` interface: ```typescript export interface NostrServerTransportOptions extends BaseNostrTransportOptions { serverInfo?: ServerInfo; profileMetadata?: ProfileMetadata; /** @deprecated Use isAnnouncedServer instead. */ isPublicServer?: boolean; isAnnouncedServer?: boolean; publishRelayList?: boolean; relayListUrls?: string[]; bootstrapRelayUrls?: string[]; allowedPublicKeys?: string[]; /** Optional callback for dynamic public key authorization. Returns true to allow the pubkey. */ isPubkeyAllowed?: (clientPubkey: string) => boolean | Promise<boolean>; /** List of capabilities that are excluded from public key whitelisting requirements */ excludedCapabilities?: CapabilityExclusion[]; /** Optional callback for dynamic capability exclusions. Returns true to bypass pubkey authorization. */ isCapabilityExcluded?: ( exclusion: CapabilityExclusion, ) => boolean | Promise<boolean>; /** Log level for the NostrServerTransport: 'debug' | 'info' | 'warn' | 'error' | 'silent' */ logLevel?: LogLevel; /** * Whether to inject the client's public key into the _meta field of incoming messages. * @default false */ injectClientPubkey?: boolean; } ``` - **`serverInfo`**: (Optional) Information about the server (`name`, `picture`, `website`) to be used in public announcements. - **`profileMetadata`**: (Optional) NIP-01 `kind:0` metadata for the server profile. When provided, the transport publishes a signed `kind:0` event at startup as defined by CEP-23. - **`isAnnouncedServer`**: (Optional) If `true`, the transport publishes public announcement events for relay-based discovery. Defaults to `false`. - **`isPublicServer`**: (Deprecated) Legacy alias for `isAnnouncedServer`. - **`publishRelayList`**: (Optional) If `true`, the transport publishes a NIP-65 relay list (`kind:10002`) even when `isAnnouncedServer` is `false`. Defaults to `true`. - **`relayListUrls`**: (Optional) Explicit relay URLs to advertise in the published relay list. If omitted, the SDK derives them from the configured relay handler when possible. - **`bootstrapRelayUrls`**: (Optional) Extra relays used only as publication targets for discoverability events such as `kind:11316` and `kind:10002`. These are not automatically advertised in the relay list. - **`allowedPublicKeys`**: (Optional) A list of client public keys that are allowed to connect. If not provided, any client can connect. - **`isPubkeyAllowed`**: (Optional) A dynamic authorization callback that receives a client public key and returns `true` to allow the connection. Can be async. When used with `allowedPublicKeys`, both checks must pass (AND logic). - **`excludedCapabilities`**: (Optional) A list of capabilities that are excluded from public key whitelisting requirements. This allows certain operations from disallowed public keys, enhancing security policy flexibility while maintaining backward compatibility. - **`isCapabilityExcluded`**: (Optional) A dynamic capability exclusion callback that receives a capability exclusion pattern and returns `true` to bypass pubkey authorization for that capability. Can be async. Evaluated after static `excludedCapabilities`. - **`injectClientPubkey`**: (Optional) If `true`, the transport will inject the client's public key into the `_meta` field of requests passed to the underlying server. Defaults to `false`. ## CEP-23 Server Profile Publication `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. ### `ProfileMetadata` The `profileMetadata` object is serialized as JSON and published as a NIP-01 `kind:0` event. ```typescript export interface ProfileMetadata { name?: string; about?: string; picture?: string; banner?: string; website?: string; nip05?: string; lud16?: string; [key: string]: unknown; } ``` ### Publication behavior - Publication is **opt-in** and only happens when `profileMetadata` is provided. - `kind:0` publication is independent from `isAnnouncedServer`. - 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. ### Example: announced server with a public profile ```typescript const transport = new NostrServerTransport({ signer, relayHandler: relayPool, isAnnouncedServer: true, publishRelayList: 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', }, }); ``` ### Example: private server with profile publication only ```typescript const transport = new NostrServerTransport({ signer, relayHandler: relayPool, isAnnouncedServer: false, profileMetadata: { name: 'Private Profile Server', about: 'Publishes a CEP-23 profile without public capability announcements', website: 'https://example.com/private-server', }, bootstrapRelayUrls: ['wss://relay.damus.io'], }); ``` In this configuration, the server remains outside the public capability-announcement flow but still publishes a canonical Nostr profile that clients and operators can render. ### Capability Exclusion The `CapabilityExclusion` interface allows you to define specific capabilities that bypass the public key whitelisting requirements: ```typescript /** * Represents a capability exclusion pattern that can bypass whitelisting. * Can be either a method-only pattern (e.g., 'tools/list') or a method + name pattern (e.g., 'tools/call, get_weather'). */ export interface CapabilityExclusion { /** The JSON-RPC method to exclude from whitelisting (e.g., 'tools/call', 'tools/list') */ method: string; /** Optional capability name to specifically exclude (e.g., 'get_weather') */ name?: string; } ``` #### How Capability Exclusion Works Capability exclusion provides fine-grained control over access by allowing specific operations to be performed even by clients that are not in the `allowedPublicKeys` list. This is useful for: - Allowing public access to server discovery endpoints like `tools/list` - Permitting specific tool calls from untrusted clients - Maintaining backward compatibility with existing clients #### Exclusion Patterns - **Method-only exclusion**: `{ method: 'tools/list' }` - Excludes all calls to the `tools/list` method - **Method + name exclusion**: `{ method: 'tools/call', name: 'add' }` - Excludes only the `add` tool from the `tools/call` method ## Dynamic Authorization In addition to static configuration, you can provide dynamic authorization callbacks for more flexible access control policies. ### Dynamic Public Key Authorization Use `isPubkeyAllowed` to implement runtime authorization logic: ```typescript const transport = new NostrServerTransport({ signer, relayHandler: relayPool, // Static allowlist (optional - can be used alone or with dynamic check) allowedPublicKeys: ['known-trusted-client'], // Dynamic authorization callback isPubkeyAllowed: async (clientPubkey) => { // Check against a database, external service, or custom logic const isAllowed = await checkDatabaseForAccess(clientPubkey); return isAllowed; }, }); ``` When both `allowedPublicKeys` and `isPubkeyAllowed` are configured, a client must pass **both** checks (AND logic) to be authorized. ### Dynamic Capability Exclusions Use `isCapabilityExcluded` to dynamically determine which capabilities bypass whitelisting: ```typescript const transport = new NostrServerTransport({ signer, relayHandler: relayPool, allowedPublicKeys: ['trusted-client'], // Static exclusions excludedCapabilities: [{ method: 'tools/list' }], // Dynamic exclusion callback - evaluated after static exclusions isCapabilityExcluded: async (exclusion) => { // Check if this specific capability should be public if (exclusion.method === 'tools/call' && exclusion.name === 'get_weather') { return await isWeatherServicePublic(); } return false; }, }); ``` The dynamic callback receives the exclusion pattern being checked and returns `true` to allow the capability without pubkey authorization. ### Combining Static and Dynamic Authorization You can mix static and dynamic approaches for maximum flexibility: ```typescript const transport = new NostrServerTransport({ signer, relayHandler: relayPool, isAnnouncedServer: true, // Hardcoded trusted clients allowedPublicKeys: ['admin-pubkey', 'service-account-pubkey'], // Dynamic check for additional clients isPubkeyAllowed: async (clientPubkey) => { // Check subscription status in database const subscription = await db.subscriptions.findByPubkey(clientPubkey); return subscription?.isActive ?? false; }, // Public capabilities anyone can use excludedCapabilities: [ { method: 'tools/list' }, { method: 'tools/call', name: 'get_status' }, ], // 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; }, }); ``` ## Usage Example Here's how to use the `NostrServerTransport` with an `McpServer` from the `@modelcontextprotocol/sdk`: ```typescript import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { NostrServerTransport } from '@contextvm/sdk'; import { PrivateKeySigner } from '@contextvm/sdk'; import { ApplesauceRelayPool } from '@contextvm/sdk'; // 1. Configure the signer and relay pool const signer = new PrivateKeySigner('your-server-private-key'); const relayPool = new ApplesauceRelayPool(['wss://relay.damus.io']); // 2. Create the McpServer instance const mcpServer = new McpServer({ name: 'demo-server', version: '1.0.0', }); // Register your server's tools, resources, etc. // mcpServer.tool(...); // 3. Create the NostrServerTransport instance const serverNostrTransport = new NostrServerTransport({ signer: signer, relayHandler: relayPool, isAnnouncedServer: true, publishRelayList: true, bootstrapRelayUrls: ['wss://relay.damus.io', 'wss://nos.lol'], profileMetadata: { name: 'My Awesome MCP Server', about: 'Public MCP provider on Nostr', picture: 'https://example.com/avatar.png', website: 'https://example.com', }, serverInfo: { name: 'My Awesome MCP Server', website: 'https://example.com', }, allowedPublicKeys: ['trusted-client-key'], // Only allow specific clients excludedCapabilities: [ { method: 'tools/list' }, // Allow any client to list available tools { method: 'tools/call', name: 'get_weather' }, // Allow any client to call get_weather tool ], injectClientPubkey: true, // Enable client public key injection }); // 4. Connect the server await mcpServer.connect(serverNostrTransport); console.log('MCP server is running and available on Nostr.'); // Keep the process running... // To shut down: await mcpServer.close(); ``` > **Note**: The `relayHandler` option also accepts a `string[]` of relay URLs, in which case an `ApplesauceRelayPool` will be created automatically. See the [Base Nostr Transport](/transports/base-nostr-transport) documentation for details. ## How It Works 1. **`start()`**: When `mcpServer.connect()` is called, the transport connects to the relays and subscribes to events targeting the server's public key. If `isAnnouncedServer` is `true`, it publishes public announcement events. Independently, if `publishRelayList` is enabled, it also publishes relay-list metadata. If `profileMetadata` is configured, it publishes a CEP-23 `kind:0` profile event. 2. **Incoming Events**: The transport listens for events from clients. For each client, it maintains a `ClientSession`. 3. **Request Handling**: When a valid request is received from an authorized client, the transport forwards it to the `McpServer`'s internal logic via the `onmessage` handler. It replaces the request's original ID with the unique Nostr event ID to prevent ID collisions between different clients. - If `injectClientPubkey` is enabled, the client's public key is injected into the request's `_meta` field before being passed to the server. 4. **Response Handling**: When the `McpServer` sends a response, the transport's `send()` method is called. The transport looks up the original request details from the client's session, restores the original request ID, and sends the response back to the correct client, referencing the original event ID. 5. **Discoverability publication**: Public announcement events (kinds 11316-11320) are controlled by `isAnnouncedServer`. Relay-list metadata (`kind:10002`) is controlled independently by `publishRelayList`. Profile metadata (`kind:0`) is controlled independently by `profileMetadata`. ## Relay List Discoverability Servers can publish a NIP-65 relay list so clients can discover where the server is reachable. ### Default Behavior - `isAnnouncedServer: true` enables public announcement publication - `publishRelayList` defaults to `true` for both public and private servers - if `relayListUrls` is omitted, the SDK derives advertised relays from the configured relay handler when possible - `bootstrapRelayUrls` can be used to publish discoverability events to extra relays without advertising them as operational relays ### Why bootstrap relays exist Operational relays and discoverability relays do not always need to be identical: - **Operational relays** are where the server actually handles requests and responses - **Bootstrap relays** are additional relays used to make the server easier to discover This separation helps keep the published relay list focused while still improving network visibility. ## Discoverability event matrix The transport now exposes three independent publication surfaces: | Purpose | Event kind(s) | Controlled by | | ---------------------------------- | --------------- | ------------------- | | ContextVM capability announcements | `11316`-`11320` | `isAnnouncedServer` | | Relay discoverability | `10002` | `publishRelayList` | | Nostr profile identity | `0` | `profileMetadata` | This allows release-time configurations such as: - fully public servers that publish all three surfaces; - private servers that only publish relay metadata; - private or semi-private servers that publish a `kind:0` profile without publishing capability announcements. ## Session Management The `NostrServerTransport` manages a session for each unique client public key. Each session tracks: - If the client has completed the MCP initialization handshake. - Whether the session is encrypted. - A map of pending requests to correlate responses. - The timestamp of the last activity, used for cleaning up inactive sessions. ## Security and Policy Flexibility The capability exclusion feature provides enhanced security policy flexibility by allowing you to create a whitelist-based security model with specific exceptions. This approach is particularly useful for: ### Use Cases 1. **Public Discovery**: Allow any client to discover your server's capabilities via `tools/list` while restricting actual tool usage to authorized clients. 2. **Limited Public Access**: Permit specific, safe operations from untrusted clients while maintaining security for sensitive operations. 3. **Backward Compatibility**: Gradually introduce stricter security policies while maintaining compatibility with existing clients. 4. **Tiered Access**: Create different levels of access where certain capabilities are available to all clients, while others require explicit authorization. ## Client Public Key Injection When the `injectClientPubkey` option is enabled, the transport injects the client's public key into the `_meta` field of requests passed to the underlying MCP server. This enables servers to access client identification information for authentication, authorization, and enhanced integration purposes. ### How It Works 1. When a request is received from a client, the transport extracts the client's public key from the Nostr event 2. The transport embeds the `clientPubkey` field in the message's `_meta` field 3. The modified request is then passed to the underlying server The injected metadata follows this structure: ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "example_tool", "arguments": {} }, "_meta": { "clientPubkey": "<client-public-key-hex>" } } ``` ### Use Cases - **Authentication**: Servers can verify client identity without additional protocol overhead - **Authorization**: Implement per-client access controls based on public key - **Logging**: Track client activity and usage patterns - **Rate Limiting**: Apply rate limits on a per-client basis - **Personalization**: Provide client-specific responses or data ## Structured Tool Outputs `NostrServerTransport` does not change the MCP tool result model, so structured outputs work the same way they do on any other MCP transport. This is especially useful when your server is meant for programmatic usage and clients should be able to depend on a stable result shape. Define an `outputSchema` on the tool and return `structuredContent` from the handler: ```typescript import * as z from 'zod/v4'; 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.enum(['sunny', 'cloudy', 'rainy', 'stormy', 'snowy']), humidity: z.number().min(0).max(100), }), }, async ({ city, country }) => { const structuredContent = { temperature: { celsius: 22, fahrenheit: 71.6, }, conditions: 'sunny' as const, humidity: 45, }; return { content: [ { type: 'text', text: `Weather for ${city}, ${country}: ${structuredContent.temperature.celsius}°C and ${structuredContent.conditions}.`, }, ], structuredContent, }; }, ); ``` Guidance: - Use `structuredContent` for machine-readable output. - Use `content` for human-readable output only. - `content` does not need to duplicate `structuredContent`. - If no human-readable output is needed, `content` can be `[]`. ## Next Steps Now that you understand how the transports work, let's dive into the **[Signer](/signer/nostr-signer-interface)**, the component responsible for cryptographic signatures.Nostr Server Transport
Section titled “Nostr Server Transport”The NostrServerTransport is the server-side counterpart to the NostrClientTransport. It allows an MCP server to expose its capabilities to the Nostr network, making them discoverable and usable by any Nostr-enabled client. Like the client transport, it implements the Transport interface from the @modelcontextprotocol/sdk.
Overview
Section titled “Overview”The NostrServerTransport is responsible for:
- Listening for incoming MCP requests from Nostr clients.
- Managing individual client sessions and their state (e.g., initialization, encryption).
- Handling request/response correlation to ensure responses are sent to the correct client.
- Sending responses and notifications back to clients over Nostr.
- Optionally announcing the server and its capabilities to the network for public discovery.
NostrServerTransportOptions
Section titled “NostrServerTransportOptions”The transport is configured via the NostrServerTransportOptions interface:
export interface NostrServerTransportOptions extends BaseNostrTransportOptions { serverInfo?: ServerInfo; profileMetadata?: ProfileMetadata; /** @deprecated Use isAnnouncedServer instead. */ isPublicServer?: boolean; isAnnouncedServer?: boolean; publishRelayList?: boolean; relayListUrls?: string[]; bootstrapRelayUrls?: string[]; allowedPublicKeys?: string[]; /** Optional callback for dynamic public key authorization. Returns true to allow the pubkey. */ isPubkeyAllowed?: (clientPubkey: string) => boolean | Promise<boolean>; /** List of capabilities that are excluded from public key whitelisting requirements */ excludedCapabilities?: CapabilityExclusion[]; /** Optional callback for dynamic capability exclusions. Returns true to bypass pubkey authorization. */ isCapabilityExcluded?: ( exclusion: CapabilityExclusion, ) => boolean | Promise<boolean>; /** Log level for the NostrServerTransport: 'debug' | 'info' | 'warn' | 'error' | 'silent' */ logLevel?: LogLevel; /** * Whether to inject the client's public key into the _meta field of incoming messages. * @default false */ injectClientPubkey?: boolean;}serverInfo: (Optional) Information about the server (name,picture,website) to be used in public announcements.profileMetadata: (Optional) NIP-01kind:0metadata for the server profile. When provided, the transport publishes a signedkind:0event at startup as defined by CEP-23.isAnnouncedServer: (Optional) Iftrue, the transport publishes public announcement events for relay-based discovery. Defaults tofalse.isPublicServer: (Deprecated) Legacy alias forisAnnouncedServer.publishRelayList: (Optional) Iftrue, the transport publishes a NIP-65 relay list (kind:10002) even whenisAnnouncedServerisfalse. Defaults totrue.relayListUrls: (Optional) Explicit relay URLs to advertise in the published relay list. If omitted, the SDK derives them from the configured relay handler when possible.bootstrapRelayUrls: (Optional) Extra relays used only as publication targets for discoverability events such askind:11316andkind:10002. These are not automatically advertised in the relay list.allowedPublicKeys: (Optional) A list of client public keys that are allowed to connect. If not provided, any client can connect.isPubkeyAllowed: (Optional) A dynamic authorization callback that receives a client public key and returnstrueto allow the connection. Can be async. When used withallowedPublicKeys, both checks must pass (AND logic).excludedCapabilities: (Optional) A list of capabilities that are excluded from public key whitelisting requirements. This allows certain operations from disallowed public keys, enhancing security policy flexibility while maintaining backward compatibility.isCapabilityExcluded: (Optional) A dynamic capability exclusion callback that receives a capability exclusion pattern and returnstrueto bypass pubkey authorization for that capability. Can be async. Evaluated after staticexcludedCapabilities.injectClientPubkey: (Optional) Iftrue, the transport will inject the client’s public key into the_metafield of requests passed to the underlying server. Defaults tofalse.
CEP-23 Server Profile Publication
Section titled “CEP-23 Server Profile Publication”serverInfo and profileMetadata serve different purposes:
serverInfopowers ContextVM discovery and initialize semantics.profileMetadatapowers 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.
ProfileMetadata
Section titled “ProfileMetadata”The profileMetadata object is serialized as JSON and published as a NIP-01 kind:0 event.
export interface ProfileMetadata { name?: string; about?: string; picture?: string; banner?: string; website?: string; nip05?: string; lud16?: string; [key: string]: unknown;}Publication behavior
Section titled “Publication behavior”- Publication is opt-in and only happens when
profileMetadatais provided. kind:0publication is independent fromisAnnouncedServer.- 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
bootstrapRelayUrlsalso help distribute profile metadata in local or non-WebSocket relay environments.
Example: announced server with a public profile
Section titled “Example: announced server with a public profile”const transport = new NostrServerTransport({ signer, relayHandler: relayPool, isAnnouncedServer: true, publishRelayList: 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', },});Example: private server with profile publication only
Section titled “Example: private server with profile publication only”const transport = new NostrServerTransport({ signer, relayHandler: relayPool, isAnnouncedServer: false, profileMetadata: { name: 'Private Profile Server', about: 'Publishes a CEP-23 profile without public capability announcements', website: 'https://example.com/private-server', }, bootstrapRelayUrls: ['wss://relay.damus.io'],});In this configuration, the server remains outside the public capability-announcement flow but still publishes a canonical Nostr profile that clients and operators can render.
Capability Exclusion
Section titled “Capability Exclusion”The CapabilityExclusion interface allows you to define specific capabilities that bypass the public key whitelisting requirements:
/** * Represents a capability exclusion pattern that can bypass whitelisting. * Can be either a method-only pattern (e.g., 'tools/list') or a method + name pattern (e.g., 'tools/call, get_weather'). */export interface CapabilityExclusion { /** The JSON-RPC method to exclude from whitelisting (e.g., 'tools/call', 'tools/list') */ method: string; /** Optional capability name to specifically exclude (e.g., 'get_weather') */ name?: string;}How Capability Exclusion Works
Section titled “How Capability Exclusion Works”Capability exclusion provides fine-grained control over access by allowing specific operations to be performed even by clients that are not in the allowedPublicKeys list. This is useful for:
- Allowing public access to server discovery endpoints like
tools/list - Permitting specific tool calls from untrusted clients
- Maintaining backward compatibility with existing clients
Exclusion Patterns
Section titled “Exclusion Patterns”- Method-only exclusion:
{ method: 'tools/list' }- Excludes all calls to thetools/listmethod - Method + name exclusion:
{ method: 'tools/call', name: 'add' }- Excludes only theaddtool from thetools/callmethod
Dynamic Authorization
Section titled “Dynamic Authorization”In addition to static configuration, you can provide dynamic authorization callbacks for more flexible access control policies.
Dynamic Public Key Authorization
Section titled “Dynamic Public Key Authorization”Use isPubkeyAllowed to implement runtime authorization logic:
const transport = new NostrServerTransport({ signer, relayHandler: relayPool, // Static allowlist (optional - can be used alone or with dynamic check) allowedPublicKeys: ['known-trusted-client'], // Dynamic authorization callback isPubkeyAllowed: async (clientPubkey) => { // Check against a database, external service, or custom logic const isAllowed = await checkDatabaseForAccess(clientPubkey); return isAllowed; },});When both allowedPublicKeys and isPubkeyAllowed are configured, a client must pass both checks (AND logic) to be authorized.
Dynamic Capability Exclusions
Section titled “Dynamic Capability Exclusions”Use isCapabilityExcluded to dynamically determine which capabilities bypass whitelisting:
const transport = new NostrServerTransport({ signer, relayHandler: relayPool, allowedPublicKeys: ['trusted-client'], // Static exclusions excludedCapabilities: [{ method: 'tools/list' }], // Dynamic exclusion callback - evaluated after static exclusions isCapabilityExcluded: async (exclusion) => { // Check if this specific capability should be public if (exclusion.method === 'tools/call' && exclusion.name === 'get_weather') { return await isWeatherServicePublic(); } return false; },});The dynamic callback receives the exclusion pattern being checked and returns true to allow the capability without pubkey authorization.
Combining Static and Dynamic Authorization
Section titled “Combining Static and Dynamic Authorization”You can mix static and dynamic approaches for maximum flexibility:
const transport = new NostrServerTransport({ signer, relayHandler: relayPool, isAnnouncedServer: true, // Hardcoded trusted clients allowedPublicKeys: ['admin-pubkey', 'service-account-pubkey'], // Dynamic check for additional clients isPubkeyAllowed: async (clientPubkey) => { // Check subscription status in database const subscription = await db.subscriptions.findByPubkey(clientPubkey); return subscription?.isActive ?? false; }, // Public capabilities anyone can use excludedCapabilities: [ { method: 'tools/list' }, { method: 'tools/call', name: 'get_status' }, ], // 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; },});Usage Example
Section titled “Usage Example”Here’s how to use the NostrServerTransport with an McpServer from the @modelcontextprotocol/sdk:
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';import { NostrServerTransport } from '@contextvm/sdk';import { PrivateKeySigner } from '@contextvm/sdk';import { ApplesauceRelayPool } from '@contextvm/sdk';
// 1. Configure the signer and relay poolconst signer = new PrivateKeySigner('your-server-private-key');const relayPool = new ApplesauceRelayPool(['wss://relay.damus.io']);
// 2. Create the McpServer instanceconst mcpServer = new McpServer({ name: 'demo-server', version: '1.0.0',});
// Register your server's tools, resources, etc.// mcpServer.tool(...);
// 3. Create the NostrServerTransport instanceconst serverNostrTransport = new NostrServerTransport({ signer: signer, relayHandler: relayPool, isAnnouncedServer: true, publishRelayList: true, bootstrapRelayUrls: ['wss://relay.damus.io', 'wss://nos.lol'], profileMetadata: { name: 'My Awesome MCP Server', about: 'Public MCP provider on Nostr', picture: 'https://example.com/avatar.png', website: 'https://example.com', }, serverInfo: { name: 'My Awesome MCP Server', website: 'https://example.com', }, allowedPublicKeys: ['trusted-client-key'], // Only allow specific clients excludedCapabilities: [ { method: 'tools/list' }, // Allow any client to list available tools { method: 'tools/call', name: 'get_weather' }, // Allow any client to call get_weather tool ], injectClientPubkey: true, // Enable client public key injection});
// 4. Connect the serverawait mcpServer.connect(serverNostrTransport);
console.log('MCP server is running and available on Nostr.');
// Keep the process running...// To shut down: await mcpServer.close();Note: The
relayHandleroption also accepts astring[]of relay URLs, in which case anApplesauceRelayPoolwill be created automatically. See the Base Nostr Transport documentation for details.
How It Works
Section titled “How It Works”start(): WhenmcpServer.connect()is called, the transport connects to the relays and subscribes to events targeting the server’s public key. IfisAnnouncedServeristrue, it publishes public announcement events. Independently, ifpublishRelayListis enabled, it also publishes relay-list metadata. IfprofileMetadatais configured, it publishes a CEP-23kind:0profile event.- Incoming Events: The transport listens for events from clients. For each client, it maintains a
ClientSession. - Request Handling: When a valid request is received from an authorized client, the transport forwards it to the
McpServer’s internal logic via theonmessagehandler. It replaces the request’s original ID with the unique Nostr event ID to prevent ID collisions between different clients.- If
injectClientPubkeyis enabled, the client’s public key is injected into the request’s_metafield before being passed to the server.
- If
- Response Handling: When the
McpServersends a response, the transport’ssend()method is called. The transport looks up the original request details from the client’s session, restores the original request ID, and sends the response back to the correct client, referencing the original event ID. - Discoverability publication: Public announcement events (kinds 11316-11320) are controlled by
isAnnouncedServer. Relay-list metadata (kind:10002) is controlled independently bypublishRelayList. Profile metadata (kind:0) is controlled independently byprofileMetadata.
Relay List Discoverability
Section titled “Relay List Discoverability”Servers can publish a NIP-65 relay list so clients can discover where the server is reachable.
Default Behavior
Section titled “Default Behavior”isAnnouncedServer: trueenables public announcement publicationpublishRelayListdefaults totruefor both public and private servers- if
relayListUrlsis omitted, the SDK derives advertised relays from the configured relay handler when possible bootstrapRelayUrlscan be used to publish discoverability events to extra relays without advertising them as operational relays
Why bootstrap relays exist
Section titled “Why bootstrap relays exist”Operational relays and discoverability relays do not always need to be identical:
- Operational relays are where the server actually handles requests and responses
- Bootstrap relays are additional relays used to make the server easier to discover
This separation helps keep the published relay list focused while still improving network visibility.
Discoverability event matrix
Section titled “Discoverability event matrix”The transport now exposes three independent publication surfaces:
| Purpose | Event kind(s) | Controlled by |
|---|---|---|
| ContextVM capability announcements | 11316-11320 | isAnnouncedServer |
| Relay discoverability | 10002 | publishRelayList |
| Nostr profile identity | 0 | profileMetadata |
This allows release-time configurations such as:
- fully public servers that publish all three surfaces;
- private servers that only publish relay metadata;
- private or semi-private servers that publish a
kind:0profile without publishing capability announcements.
Session Management
Section titled “Session Management”The NostrServerTransport manages a session for each unique client public key. Each session tracks:
- If the client has completed the MCP initialization handshake.
- Whether the session is encrypted.
- A map of pending requests to correlate responses.
- The timestamp of the last activity, used for cleaning up inactive sessions.
Security and Policy Flexibility
Section titled “Security and Policy Flexibility”The capability exclusion feature provides enhanced security policy flexibility by allowing you to create a whitelist-based security model with specific exceptions. This approach is particularly useful for:
Use Cases
Section titled “Use Cases”-
Public Discovery: Allow any client to discover your server’s capabilities via
tools/listwhile restricting actual tool usage to authorized clients. -
Limited Public Access: Permit specific, safe operations from untrusted clients while maintaining security for sensitive operations.
-
Backward Compatibility: Gradually introduce stricter security policies while maintaining compatibility with existing clients.
-
Tiered Access: Create different levels of access where certain capabilities are available to all clients, while others require explicit authorization.
Client Public Key Injection
Section titled “Client Public Key Injection”When the injectClientPubkey option is enabled, the transport injects the client’s public key into the _meta field of requests passed to the underlying MCP server. This enables servers to access client identification information for authentication, authorization, and enhanced integration purposes.
How It Works
Section titled “How It Works”- When a request is received from a client, the transport extracts the client’s public key from the Nostr event
- The transport embeds the
clientPubkeyfield in the message’s_metafield - The modified request is then passed to the underlying server
The injected metadata follows this structure:
{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "example_tool", "arguments": {} }, "_meta": { "clientPubkey": "<client-public-key-hex>" }}Use Cases
Section titled “Use Cases”- Authentication: Servers can verify client identity without additional protocol overhead
- Authorization: Implement per-client access controls based on public key
- Logging: Track client activity and usage patterns
- Rate Limiting: Apply rate limits on a per-client basis
- Personalization: Provide client-specific responses or data
Structured Tool Outputs
Section titled “Structured Tool Outputs”NostrServerTransport does not change the MCP tool result model, so structured outputs work the same way they do on any other MCP transport. This is especially useful when your server is meant for programmatic usage and clients should be able to depend on a stable result shape.
Define an outputSchema on the tool and return structuredContent from the handler:
import * as z from 'zod/v4';
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.enum(['sunny', 'cloudy', 'rainy', 'stormy', 'snowy']), humidity: z.number().min(0).max(100), }), }, async ({ city, country }) => { const structuredContent = { temperature: { celsius: 22, fahrenheit: 71.6, }, conditions: 'sunny' as const, humidity: 45, };
return { content: [ { type: 'text', text: `Weather for ${city}, ${country}: ${structuredContent.temperature.celsius}°C and ${structuredContent.conditions}.`, }, ], structuredContent, }; },);Guidance:
- Use
structuredContentfor machine-readable output. - Use
contentfor human-readable output only. contentdoes not need to duplicatestructuredContent.- If no human-readable output is needed,
contentcan be[].
Next Steps
Section titled “Next Steps”Now that you understand how the transports work, let’s dive into the Signer, the component responsible for cryptographic signatures.