diff --git a/.github/workflows/deploy-github-pages.yml b/.github/workflows/deploy-github-pages.yml index 0d93a34..823a41e 100644 --- a/.github/workflows/deploy-github-pages.yml +++ b/.github/workflows/deploy-github-pages.yml @@ -38,7 +38,7 @@ jobs: - name: Build site env: - GITHUB_PAGES: 'true' + GITHUB_PAGES: "true" run: bun run build - name: Upload Pages artifact diff --git a/agents.md b/agents.md new file mode 100644 index 0000000..367f956 --- /dev/null +++ b/agents.md @@ -0,0 +1,96 @@ +# ContextVM Documentation Framework + +This repository houses the official documentation for the ContextVM protocol and its ecosystem. It is built with [Astro Starlight](https://starlight.astro.build/). + +This document describes the mental model, framework, and conventions used to organize the documentation. It serves as a guide for human contributors and AI agents alike to ensure the documentation remains consistent and high-quality over time. + +## Documentation Framework: Diátaxis + +We organize our documentation following the [Diátaxis framework](https://diataxis.fr/), which categorizes documentation into four distinct types based on the user's needs. + +For ContextVM, these are mapped as follows: + +1. **Tutorials** (`src/content/docs/tutorials/`) + - **Purpose**: Learning-oriented. Step-by-step guides for beginners to achieve a basic level of competence. + - **Tone**: Conversational, encouraging, prescriptive. + - **Examples**: "Client-Server Communication", "Build a Public Server". + +2. **How-to Guides** (`src/content/docs/how-to/`) + - **Purpose**: Problem-oriented. Actionable steps to solve a specific problem or achieve a specific goal for users who already understand the basics. + - **Tone**: Direct, task-focused, minimal context. + - **Examples**: "Bridge an Existing MCP Server", "Enable Encrypted Communication", "Payments / Lightning over NWC". + +3. **Reference** (`src/content/docs/reference/`) + - **Purpose**: Information-oriented. Accurate, comprehensive, and objective descriptions of the machinery. + - **Tone**: Objective, austere, precise. + - **Organization**: + - `spec/`: The core ContextVM protocol specification and contribution guidelines. + - `ceps/`: ContextVM Enhancement Proposals (CEPs). + - `ts-sdk/`: TypeScript SDK API reference, architectures, and modules. + - `rs-sdk/`: Rust SDK API reference, architectures, and modules. + +4. **Explanation / Getting Started** (`src/content/docs/getting-started/`) + - **Purpose**: Understanding-oriented. High-level concepts, quick overviews, and theoretical explanations. + - **Tone**: Explanatory, clarifying. + +## Directory Structure + +All Markdown and MDX files live within `src/content/docs/`. The directory structure maps directly to the Diátaxis framework described above. + +```text +src/content/docs/ +├── getting-started/ # Overviews and conceptual introductions +├── how-to/ # Action-oriented problem solving +│ ├── cvmi/ # CLI guides +│ ├── payments/ # Payment integration guides +│ └── ... # Other specific topic areas +├── reference/ # Information-oriented specs and SDKs +│ ├── ceps/ # Proposals +│ ├── rs-sdk/ # Rust SDK reference +│ ├── spec/ # Protocol specification +│ └── ts-sdk/ # TypeScript SDK reference +└── tutorials/ # Step-by-step learning paths +``` + +## SDK Scoping Rules + +To prevent top-level bloat and maintain clear context boundaries: + +1. **Architecture & Design**: Architectural concepts specific to an SDK (e.g., how the Rust SDK models transport layers) must be documented *within* that SDK's reference directory (`reference/rs-sdk/`), not as a top-level design section. +2. **Feature Decoupling**: If a feature is a standalone specification or broad concept (like Payments), its *conceptual* and *how-to* documentation lives in `how-to/payments/`. However, the *API reference* for that feature within a specific SDK lives in the SDK's reference section (e.g., `reference/ts-sdk/payments/overview.md`). + +## Conventions and Style Guidelines + +When adding or modifying documentation, strictly adhere to the following rules: + +### 1. File Naming and Frontmatter +- Use lowercase, kebab-case for filenames (e.g., `client-transport.md`). +- Every file MUST have valid YAML frontmatter containing a `title` and a `description`. +- **CRITICAL**: The `description` field MUST be a meaningful, human-readable summary of the page's contents. Do **not** use lazy templated phrases like "ContextVM Rust SDK documentation for X". + +### 2. Sidebar Registration +- Every `.md` or `.mdx` file added to `src/content/docs/` MUST be explicitly registered in `astro.config.mjs` within the `sidebar` array. +- The `slug` in the sidebar configuration is the file path relative to `src/content/docs/` *without* the `.md` or `.mdx` extension. +- Orphan pages (files not linked in the sidebar) are strictly prohibited. + +### 3. Internal Linking +- Internal links must use absolute paths starting with `/`, matching the file's slug. +- **Correct**: `[Rust SDK Overview](/reference/rs-sdk/overview)` +- **Incorrect**: `[Rust SDK Overview](../reference/rs-sdk/overview.md)` +- **Incorrect**: `[Rust SDK Overview](overview.md)` + +### 4. Writing Style +- **No Filler**: Eliminate robotic introductory sentences (e.g., "This page documents the...", "The following section will explain..."). Jump straight to the point. +- **Active Voice**: Use active voice and imperative mood. (e.g., "Use the `NostrMCPGateway` when..." instead of "The `NostrMCPGateway` should be used when..."). +- **Code-First**: Show, don't tell. Lead with practical code examples where appropriate. +- **Avoid Repetition**: Do not restate protocol mechanics in SDK documentation. Link to the relevant CEP or Spec section instead. + +## Adding a New Page: Checklist + +Whenever an agent or human adds a new page, they must execute this checklist: + +- [ ] Create the `.md` or `.mdx` file in the correct Diátaxis directory. +- [ ] Add YAML frontmatter (`title`, `description`). Ensure the description is bespoke and accurate. +- [ ] Add the page slug to the correct section of the `sidebar` in `astro.config.mjs`. +- [ ] Ensure all internal links point to absolute slugs (e.g., `/reference/ts-sdk/...`). +- [ ] Run `npm run build` (or `astro build`) locally to ensure Starlight compiles successfully and no 404 links exist. diff --git a/astro.config.mjs b/astro.config.mjs index c5bfdaf..856319f 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -1,245 +1,346 @@ // @ts-check -import { defineConfig } from 'astro/config'; -import starlight from '@astrojs/starlight'; +import { defineConfig } from "astro/config"; +import starlight from "@astrojs/starlight"; -const isGitHubPages = process.env.GITHUB_PAGES === 'true'; -const githubPagesBase = '/contextvm-docs'; +const isGitHubPages = process.env.GITHUB_PAGES === "true"; +const githubPagesBase = "/contextvm-docs"; // https://astro.build/config export default defineConfig({ site: isGitHubPages - ? 'https://contextvm.github.io/contextvm-docs' - : 'https://docs.contextvm.org', - base: isGitHubPages ? githubPagesBase : '/', + ? "https://contextvm.github.io/contextvm-docs" + : "https://docs.contextvm.org", + base: isGitHubPages ? githubPagesBase : "/", integrations: [ starlight({ - title: 'ContextVM Documentation', - description: 'Documentation for ContextVM', + title: "ContextVM Documentation", + description: "Documentation for ContextVM", logo: { - light: './src/assets/contextvm-logo.svg', - dark: './src/assets/contextvm-logo.svg', + light: "./src/assets/contextvm-logo.svg", + dark: "./src/assets/contextvm-logo.svg", replacesTitle: false, }, social: [ { - icon: 'github', - label: 'ContextVM', - href: 'https://github.com/contextvm/ts-sdk', + icon: "github", + label: "ContextVM", + href: "https://github.com/contextvm/ts-sdk", }, { - icon: 'external', - label: 'ContextVM Website', - href: 'https://contextvm.org/', + icon: "external", + label: "ContextVM Website", + href: "https://contextvm.org/", }, ], components: { - PageTitle: './src/components/starlight/PageTitle.astro', - ThemeProvider: './src/components/starlight/ThemeProvider.astro', - ThemeSelect: './src/components/starlight/ThemeSelect.astro', + PageTitle: "./src/components/starlight/PageTitle.astro", + ThemeProvider: "./src/components/starlight/ThemeProvider.astro", + ThemeSelect: "./src/components/starlight/ThemeSelect.astro", }, sidebar: [ { - label: 'Getting Started', + label: "Getting Started", items: [ - { label: 'Quick Overview', slug: 'getting-started/quick-overview' }, + { label: "Quick Overview", slug: "getting-started/quick-overview" }, ], }, { - label: 'Specification', + label: "Reference", items: [ - { label: 'Specification', slug: 'spec/ctxvm-draft-spec' }, - { label: 'CEP - Guidelines', slug: 'spec/cep-guidelines' }, { - label: 'CEPs', + label: "Specification", items: [ - { label: 'CEP-4: Encryption Support', slug: 'spec/ceps/cep-4' }, { - label: 'CEP-6: Public Server Announcements', - slug: 'spec/ceps/cep-6', + label: "Specification", + slug: "reference/spec/ctxvm-draft-spec", }, { - label: 'CEP-8: Capability Pricing and Payment Flow', - slug: 'spec/ceps/cep-8', + label: "CEP - Guidelines", + slug: "reference/spec/cep-guidelines", + }, + ], + }, + { + label: "CEPs", + items: [ + { + label: "CEP-4: Encryption Support", + slug: "reference/ceps/cep-4", + }, + { + label: "CEP-6: Public Server Announcements", + slug: "reference/ceps/cep-6", + }, + { + label: "CEP-8: Capability Pricing and Payment Flow", + slug: "reference/ceps/cep-8", }, { - label: 'CEP-15: Common Tool Schemas', - slug: 'spec/ceps/cep-15', + label: "CEP-15: Common Tool Schemas", + slug: "reference/ceps/cep-15", }, { - label: 'CEP-17: Server Relay List Metadata', - slug: 'spec/ceps/cep-17', + label: "CEP-17: Server Relay List Metadata", + slug: "reference/ceps/cep-17", }, { - label: 'CEP-19: Ephemeral Gift Wraps', - slug: 'spec/ceps/cep-19', + label: "CEP-19: Ephemeral Gift Wraps", + slug: "reference/ceps/cep-19", }, { - label: 'CEP-22: Oversized Payload Transfer', - slug: 'spec/ceps/cep-22', + label: "CEP-22: Oversized Payload Transfer", + slug: "reference/ceps/cep-22", }, { label: - 'CEP-23: Server Profile Metadata and Social Communications', - slug: 'spec/ceps/cep-23', + "CEP-23: Server Profile Metadata and Social Communications", + slug: "reference/ceps/cep-23", }, { - label: 'CEP-24: Server Reviews', - slug: 'spec/ceps/cep-24', + label: "CEP-24: Server Reviews", + slug: "reference/ceps/cep-24", }, { - label: 'CEP-41: Open-Ended Stream Transfer', - slug: 'spec/ceps/cep-41', + label: "CEP-41: Open-Ended Streams", + slug: "reference/ceps/cep-41", }, { - label: 'Informational', + label: "Informational", items: [ { - label: 'CEP-16: Client Public Key Injection', - slug: 'spec/ceps/informational/cep-16', + label: "CEP-16: Client Public Key Injection", + slug: "reference/ceps/informational/cep-16", }, { - label: - 'CEP-21: Payment Method Identifier (PMI) Recommendations', - slug: 'spec/ceps/informational/cep-21', + label: "CEP-21: PMI Recommendations", + slug: "reference/ceps/informational/cep-21", }, { - label: - 'CEP-35: Stateless Session Discovery and Capability Learning', - slug: 'spec/ceps/informational/cep-35', + label: "CEP-35: Stateless Session Discovery", + slug: "reference/ceps/informational/cep-35", }, ], }, ], }, - ], - }, - { - label: 'TypeScript SDK', - items: [ - { label: 'Quick Overview', slug: 'ts-sdk/quick-overview' }, { - label: 'Core Concepts', + label: "TypeScript SDK", items: [ - { label: 'Constants', slug: 'ts-sdk/core/constants' }, - { label: 'Interfaces', slug: 'ts-sdk/core/interfaces' }, - { label: 'Logging', slug: 'ts-sdk/core/logging' }, { - label: 'Relay handler', - slug: 'ts-sdk/relay/relay-handler-interface', + label: "Quick Overview", + slug: "reference/ts-sdk/quick-overview", }, { - label: 'Nostr signer', - slug: 'ts-sdk/signer/nostr-signer-interface', + label: "Core Concepts", + items: [ + { + label: "Constants", + slug: "reference/ts-sdk/core/constants", + }, + { + label: "Interfaces", + slug: "reference/ts-sdk/core/interfaces", + }, + { label: "Logging", slug: "reference/ts-sdk/core/logging" }, + { + label: "Relay Handler", + slug: "reference/ts-sdk/relay/relay-handler-interface", + }, + { + label: "Nostr Signer", + slug: "reference/ts-sdk/signer/nostr-signer-interface", + }, + { + label: "Encryption", + slug: "reference/ts-sdk/core/encryption", + }, + { + label: "Common Tool Schemas", + slug: "reference/ts-sdk/core/common-tool-schemas", + }, + ], }, - { label: 'Encryption', slug: 'ts-sdk/core/encryption' }, { - label: 'Common Tool Schemas', - slug: 'ts-sdk/core/common-tool-schemas', + label: "Transports", + items: [ + { + label: "Base Nostr Transport", + slug: "reference/ts-sdk/transports/base-nostr-transport", + }, + { + label: "Nostr Client Transport", + slug: "reference/ts-sdk/transports/nostr-client-transport", + }, + { + label: "Nostr Server Transport", + slug: "reference/ts-sdk/transports/nostr-server-transport", + }, + { + label: "Oversized Transfer", + slug: "reference/ts-sdk/transports/oversized-transfer", + }, + { + label: "Open Stream", + slug: "reference/ts-sdk/transports/open-stream", + }, + ], + }, + { + label: "Components", + items: [ + { + label: "Relay Handlers", + items: [ + { + label: "Applesauce Relay Pool", + slug: "reference/ts-sdk/relay/applesauce-relay-pool", + }, + { + label: "Custom Relay Handler", + slug: "reference/ts-sdk/relay/custom-relay-handler", + }, + ], + }, + { + label: "Signers", + items: [ + { + label: "Private Key Signer", + slug: "reference/ts-sdk/signer/private-key-signer", + }, + { + label: "Custom Signer Development", + slug: "reference/ts-sdk/signer/custom-signer-development", + }, + ], + }, + { + label: "Gateway", + slug: "reference/ts-sdk/gateway/overview", + }, + { label: "Proxy", slug: "reference/ts-sdk/proxy/overview" }, + { + label: "Payments", + items: [ + { + label: "Overview", + slug: "reference/ts-sdk/payments/overview", + }, + ], + }, + ], }, ], }, { - label: 'Transports', + label: "Rust SDK", items: [ + { label: "Overview", slug: "reference/rs-sdk/overview" }, { - label: 'Base Nostr Transport', - slug: 'ts-sdk/transports/base-nostr-transport', + label: "Native Server Guide", + slug: "reference/rs-sdk/server-transport", }, { - label: 'Nostr Client Transport', - slug: 'ts-sdk/transports/nostr-client-transport', + label: "Native Client Guide", + slug: "reference/rs-sdk/client-transport", }, + { label: "Gateway", slug: "reference/rs-sdk/gateway" }, + { label: "Proxy", slug: "reference/rs-sdk/proxy" }, + { label: "Discovery", slug: "reference/rs-sdk/discovery" }, + { label: "Encryption", slug: "reference/rs-sdk/encryption" }, { - label: 'Nostr Server Transport', - slug: 'ts-sdk/transports/nostr-server-transport', + label: "Transport Modes", + slug: "reference/rs-sdk/transport-modes", }, { - label: 'Oversized Transfer', - slug: 'ts-sdk/transports/oversized-transfer', + label: "Transports (Low-Level)", + slug: "reference/rs-sdk/transports", }, + { label: "Stateless Mode", slug: "reference/rs-sdk/stateless" }, + { label: "RMCP Integration", slug: "reference/rs-sdk/rmcp" }, + ], + }, + ], + }, + { + label: "How-to", + items: [ + { + label: "CVMI CLI", + items: [ + { label: "Overview", slug: "how-to/cvmi/overview" }, + { label: "Installation", slug: "how-to/cvmi/installation" }, + { label: "Commands", slug: "how-to/cvmi/commands" }, + { label: "Configuration", slug: "how-to/cvmi/configuration" }, { - label: 'Open Stream', - slug: 'ts-sdk/transports/open-stream', + label: "Skills Overview", + slug: "how-to/cvmi/skills/overview", }, ], }, { - label: 'Components', + label: "Payments", items: [ { - label: 'Relay Handlers', - items: [ - { - label: 'Applesauce Relay Pool', - slug: 'ts-sdk/relay/applesauce-relay-pool', - }, - { - label: 'Custom Relay Handler Development', - slug: 'ts-sdk/relay/custom-relay-handler', - }, - ], + label: "Getting Started", + slug: "how-to/payments/getting-started", }, + { label: "Server", slug: "how-to/payments/server" }, + { label: "Client", slug: "how-to/payments/client" }, { - label: 'Signers', + label: "Rails", items: [ { - label: 'Private Key Signer', - slug: 'ts-sdk/signer/private-key-signer', - }, - { - label: 'Custom Signer Development', - slug: 'ts-sdk/signer/custom-signer-development', + label: "Lightning over NWC", + slug: "how-to/payments/rails/lightning-nwc", }, ], }, - { label: 'Gateway', slug: 'ts-sdk/gateway/overview' }, - { label: 'Proxy', slug: 'ts-sdk/proxy/overview' }, { - label: 'Payments', - items: [ - { label: 'Overview', slug: 'ts-sdk/payments/overview' }, - { - label: 'Getting Started', - slug: 'ts-sdk/payments/getting-started', - }, - { label: 'Server', slug: 'ts-sdk/payments/server' }, - { label: 'Client', slug: 'ts-sdk/payments/client' }, - { - label: 'Rails', - items: [ - { - label: 'Lightning over NWC', - slug: 'ts-sdk/payments/rails/lightning-nwc', - }, - ], - }, - { - label: 'Build Your Own Rail', - slug: 'ts-sdk/payments/custom-rails', - }, - ], + label: "Build Your Own Rail", + slug: "how-to/payments/custom-rails", }, ], }, { - label: 'Tutorials', + label: "Encryption", items: [ { - label: 'Client-Server Communication', - slug: 'ts-sdk/tutorials/client-server-communication', + label: "Enable Encrypted Communication", + slug: "how-to/encryption", + }, + ], + }, + { + label: "Gateway", + items: [ + { + label: "Bridge an Existing MCP Server", + slug: "how-to/bridge-mcp-server", }, ], }, ], }, { - label: 'CVMI (CLI Tool)', + label: "Tutorials", items: [ - { label: 'Overview', slug: 'cvmi/overview' }, - { label: 'Installation', slug: 'cvmi/installation' }, - { label: 'Commands', slug: 'cvmi/commands' }, - { label: 'Configuration', slug: 'cvmi/configuration' }, - { label: 'Skills Overview', slug: 'cvmi/skills/overview' }, + { + label: "Client-Server Communication", + slug: "tutorials/client-server-communication", + }, + { + label: "Build a Public Server", + slug: "tutorials/build-a-public-server", + }, + { + label: "Server & Client (Rust)", + slug: "tutorials/rust-server-client", + }, + { + label: "Discover ContextVM Servers", + slug: "tutorials/discover-servers", + }, ], }, ], diff --git a/src/content.config.ts b/src/content.config.ts index 6a7b7a0..7fbcf2c 100644 --- a/src/content.config.ts +++ b/src/content.config.ts @@ -1,6 +1,6 @@ -import { defineCollection } from 'astro:content'; -import { docsLoader } from '@astrojs/starlight/loaders'; -import { docsSchema } from '@astrojs/starlight/schema'; +import { defineCollection } from "astro:content"; +import { docsLoader } from "@astrojs/starlight/loaders"; +import { docsSchema } from "@astrojs/starlight/schema"; export const collections = { docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }), diff --git a/src/content/docs/getting-started/quick-overview.md b/src/content/docs/getting-started/quick-overview.md index e015c53..c393941 100644 --- a/src/content/docs/getting-started/quick-overview.md +++ b/src/content/docs/getting-started/quick-overview.md @@ -15,19 +15,27 @@ Our documentation is organized into several main sections: ### 📋 Specification -- **[Specification](/spec/ctxvm-draft-spec)**: The official ContextVM draft specification detailing the protocol -- **[CEP - Guidelines](/spec/cep-guidelines)**: ContextVM Enhancement Proposal guidelines for contributing to the protocol +- **[Specification](/reference/spec/ctxvm-draft-spec)**: The official ContextVM draft specification detailing the protocol +- **[CEP - Guidelines](/reference/spec/cep-guidelines)**: ContextVM Enhancement Proposal guidelines for contributing to the protocol ### 🛠️ ts-SDK The TypeScript SDK provides tools and libraries for building applications with ContextVM: -- **[SDK Quick Overview](/ts-sdk/quick-overview)**: A comprehensive overview of the SDK's modules and core concepts +- **[SDK Quick Overview](/reference/ts-sdk/quick-overview)**: A comprehensive overview of the SDK's modules and core concepts - **Core Concepts**: Fundamental definitions, constants, interfaces, and utilities - **Transports**: Communication modules for MCP over Nostr - **Components**: Gateway, Relay Handlers, Signers, and Proxy implementations - **Tutorials**: Practical examples and guides +### 🦀 rs-SDK + +The Rust SDK provides a native Rust implementation of the ContextVM protocol: + +- **[SDK Overview](/reference/rs-sdk/overview)**: Introduction to the Rust SDK and its architecture +- **Native Transports**: Server, client, and low-level Nostr transport guides +- **Design & Architecture**: Detailed breakdown of components and implementation decisions + ## What is ContextVM? ContextVM is a protocol that bridges the Model Context Protocol (MCP) with the Nostr network, enabling decentralized communication. It allows MCP servers and clients to communicate over the Nostr protocol, leveraging its decentralized infrastructure for secure and private interactions. The protocol is designed to be used programmatically or by Large Language Models (LLMs). Client and server interactions can be triggered by a user's input through an interface or by an LLM, as the underlying MCP protocol allows LLMs to use it. @@ -36,20 +44,20 @@ ContextVM is a protocol that bridges the Model Context Protocol (MCP) with the N - **Decentralized Communication**: Use Nostr's decentralized network for MCP communication - **Security First**: Leveraging Nostr's cryptographic primitives for verification, authorization, and additional features -- **Easy Integration**: Typescript SDK to work with ContextVM +- **Easy Integration**: TypeScript and Rust SDKs to work with ContextVM ## Getting Started -1. **Read the Specification**: Start with the [ContextVM specification](/spec/ctxvm-draft-spec) to understand the protocol -2. **Explore the SDK**: Check out the [SDK Quick Overview](/ts-sdk/quick-overview) for development guidance +1. **Read the Specification**: Start with the [ContextVM specification](/reference/spec/ctxvm-draft-spec) to understand the protocol +2. **Explore the SDKs**: Check out the [TypeScript SDK Quick Overview](/reference/ts-sdk/quick-overview) or [Rust SDK Overview](/reference/rs-sdk/overview) for development guidance 3. **Follow Tutorials**: Work through practical examples to see ContextVM in action ## Next Steps Choose your path based on your interests: -- **Protocol Development**: Dive into the [Specification](/spec/ctxvm-draft-spec) to understand the protocol details -- **SDK Development**: Start with the [SDK Quick Overview](/ts-sdk/quick-overview) to begin building with ContextVM -- **Contributing**: Learn about contributing to the protocol with [CEP Guidelines](/spec/cep-guidelines) +- **Protocol Development**: Dive into the [Specification](/reference/spec/ctxvm-draft-spec) to understand the protocol details +- **SDK Development**: Start with the [TypeScript SDK Quick Overview](/reference/ts-sdk/quick-overview) or [Rust SDK Overview](/reference/rs-sdk/overview) to begin building with ContextVM +- **Contributing**: Learn about contributing to the protocol with [CEP Guidelines](/reference/spec/cep-guidelines) For the latest updates and community discussions, visit our [GitHub repository](https://github.com/contextvm/). diff --git a/src/content/docs/how-to/bridge-mcp-server.md b/src/content/docs/how-to/bridge-mcp-server.md new file mode 100644 index 0000000..e7945cc --- /dev/null +++ b/src/content/docs/how-to/bridge-mcp-server.md @@ -0,0 +1,111 @@ +--- +title: "Bridge an Existing MCP Server" +description: "How to expose an existing local or HTTP MCP server to the Nostr network using the Gateway." +--- + +# How-to: Bridge an Existing MCP Server + +If you already have a working MCP server built with another framework (like the official Python or TypeScript SDKs), you do not need to rewrite it to support ContextVM natively. + +Instead, you can use the `NostrMCPGateway`. The gateway acts as a bridge: it connects to your existing MCP server locally and exposes it over the Nostr network to ContextVM clients. + +## Step 1: Identify your MCP Server Transport + +Determine how your existing MCP server runs: +- **Stdio**: It runs as a local command-line process (e.g., `python server.py` or `node server.js`). +- **HTTP/SSE**: It runs as a web server exposing an SSE endpoint. + +## Step 2: Set up the Gateway Script + +Create a new file called `gateway.ts`. You will need to install the ContextVM SDK and the official MCP SDK for the client transports. + +```bash +npm install @contextvm/sdk @modelcontextprotocol/sdk +``` + +Add the following code to `gateway.ts`, adapting the transport to match your server type. + +### Example for a Stdio Server + +```typescript +import { NostrMCPGateway, PrivateKeySigner, ApplesauceRelayPool } from "@contextvm/sdk"; +import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; + +async function main() { + const signer = new PrivateKeySigner("your-gateway-private-key-hex"); + const pool = new ApplesauceRelayPool(["wss://relay.damus.io"]); + + // 1. Define how to reach your existing server + const localMcpTransport = new StdioClientTransport({ + command: "python", // or "node", "bun", etc. + args: ["path/to/your/existing/server.py"], + }); + + // 2. Configure the gateway + const gateway = new NostrMCPGateway({ + // The connection to your local server + mcpClientTransport: localMcpTransport, + + // The connection to the Nostr network + nostrTransportOptions: { + signer, + relayHandler: pool, + // Optional: allow any Nostr client to find your bridged server + isAnnouncedServer: true, + serverInfo: { + name: "Bridged Python Server" + } + }, + }); + + // 3. Start bridging! + await gateway.start(); + console.log(`Gateway running! Nostr Pubkey: ${await signer.getPublicKey()}`); +} + +main().catch(console.error); +``` + +## Step 3: Run the Gateway + +Execute your gateway script: + +```bash +bun run gateway.ts +``` + +Your existing MCP server is now accessible securely over the Nostr network. Any ContextVM client can connect to the printed Nostr Pubkey and interact with your server as if it were a native ContextVM server. + +## Advanced: Per-Client Isolation + +In the basic example above, the Gateway uses **Single-Client Mode**. This means if 10 Nostr clients connect to your Gateway, all their requests are funneled through a *single* `StdioClientTransport` instance to your backend server. + +If your backend server stores state per-connection, or if you are bridging to an HTTP SSE server that expects each client to have its own session, you should use **Per-Client Mode**. + +Instead of providing a single `mcpClientTransport`, provide a factory function using `createMcpClientTransport`: + +```typescript +import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; + +const gateway = new NostrMCPGateway({ + // Create a fresh transport for every unique Nostr client pubkey + createMcpClientTransport: ({ clientPubkey }) => { + console.log(`New Nostr client connected: ${clientPubkey}`); + return new StreamableHTTPClientTransport(new URL("http://localhost:3000/mcp")); + }, + + // Optional: Prevent memory leaks from stale sessions + maxClientTransports: 500, + + nostrTransportOptions: { + signer, + relayHandler: pool, + }, +}); +``` + +With this configuration, the Gateway automatically tracks Nostr clients. When a new Nostr client initializes a connection, the Gateway spins up a dedicated connection to your backend server specifically for that client. When `maxClientTransports` is reached, it safely evicts the oldest inactive session. + +## Further Reading + +- For detailed configuration options, see the [Gateway Reference](/reference/ts-sdk/gateway/overview). diff --git a/src/content/docs/cvmi/commands.md b/src/content/docs/how-to/cvmi/commands.md similarity index 98% rename from src/content/docs/cvmi/commands.md rename to src/content/docs/how-to/cvmi/commands.md index b0c4dac..243f2a3 100644 --- a/src/content/docs/cvmi/commands.md +++ b/src/content/docs/how-to/cvmi/commands.md @@ -278,4 +278,4 @@ All commands support these global options: ## Environment Variables -See the [Configuration](/cvmi/configuration) page for details on environment variables that affect command behavior. +See the [Configuration](/how-to/cvmi/configuration) page for details on environment variables that affect command behavior. diff --git a/src/content/docs/cvmi/configuration.md b/src/content/docs/how-to/cvmi/configuration.md similarity index 100% rename from src/content/docs/cvmi/configuration.md rename to src/content/docs/how-to/cvmi/configuration.md diff --git a/src/content/docs/cvmi/installation.md b/src/content/docs/how-to/cvmi/installation.md similarity index 92% rename from src/content/docs/cvmi/installation.md rename to src/content/docs/how-to/cvmi/installation.md index f610614..c33b30b 100644 --- a/src/content/docs/cvmi/installation.md +++ b/src/content/docs/how-to/cvmi/installation.md @@ -110,5 +110,5 @@ cvmi serve -- npx -y @modelcontextprotocol/server-filesystem /tmp Now that you have CVMI installed, explore: -- [Commands Reference](/cvmi/commands) - Learn about all available commands -- [Configuration](/cvmi/configuration) - Set up your environment and preferences +- [Commands Reference](/how-to/cvmi/commands) - Learn about all available commands +- [Configuration](/how-to/cvmi/configuration) - Set up your environment and preferences diff --git a/src/content/docs/cvmi/overview.md b/src/content/docs/how-to/cvmi/overview.md similarity index 94% rename from src/content/docs/cvmi/overview.md rename to src/content/docs/how-to/cvmi/overview.md index b9362b1..f8009d7 100644 --- a/src/content/docs/cvmi/overview.md +++ b/src/content/docs/how-to/cvmi/overview.md @@ -89,6 +89,6 @@ Generate strongly typed TypeScript clients directly from a live ContextVM server ## Next Steps -- [Install CVMI](/cvmi/installation) - Get started with CVMI -- [Commands Reference](/cvmi/commands) - Learn about available commands -- [Configuration](/cvmi/configuration) - Configure CVMI for your needs +- [Install CVMI](/how-to/cvmi/installation) - Get started with CVMI +- [Commands Reference](/how-to/cvmi/commands) - Learn about available commands +- [Configuration](/how-to/cvmi/configuration) - Configure CVMI for your needs diff --git a/src/content/docs/cvmi/skills/overview.md b/src/content/docs/how-to/cvmi/skills/overview.md similarity index 100% rename from src/content/docs/cvmi/skills/overview.md rename to src/content/docs/how-to/cvmi/skills/overview.md diff --git a/src/content/docs/how-to/encryption.md b/src/content/docs/how-to/encryption.md new file mode 100644 index 0000000..a03cacf --- /dev/null +++ b/src/content/docs/how-to/encryption.md @@ -0,0 +1,97 @@ +--- +title: "Enable Encrypted Communication" +description: "How to configure your ContextVM server and client for end-to-end NIP-44 encryption." +--- + +# How-to: Enable Encrypted Communication + +ContextVM supports end-to-end encryption to protect message contents and metadata from relays and observers. This implements the finalized [CEP-4](/reference/ceps/cep-4) and [CEP-19](/reference/ceps/cep-19) specifications. + +This guide explains how to enable encryption on both the server and client sides. + +## Encryption Overview + +ContextVM uses NIP-44 to encrypt payloads and NIP-59 gift-wraps to hide metadata. + +There are two primary configuration options you must understand: + +1. **`EncryptionMode`**: Should encryption be required, optional, or disabled? +2. **`GiftWrapMode`**: Should outer envelopes be saved permanently by relays (`Persistent`), or discarded rapidly (`Ephemeral`)? + +## Configure the Server + +By default, standard ContextVM server initialization uses plaintext. To change this, you update the `encryptionMode` in your transport configuration. + +```typescript +import { NostrServerTransport } from "@contextvm/sdk"; + +const transport = new NostrServerTransport({ + signer, + relayHandler: relayPool, + + // Accept both plaintext and encrypted connections + encryptionMode: "Optional", + + // Prefer ephemeral wraps if the client supports them + giftWrapMode: "Optional", +}); +``` + +### Server Encryption Modes + +- `"Optional"` (Default): Accepts both encrypted and plaintext requests. The server will mirror the client's choice—replying to encrypted requests with encrypted responses, and plaintext with plaintext. +- `"Required"`: Rejects any plaintext requests. Use this for highly sensitive tools. +- `"Disabled"`: Rejects encrypted requests. + +## Configure the Client + +The client is responsible for initiating the connection. If the client uses encryption, the server will follow suit (if its policy permits). + +```typescript +import { NostrClientTransport } from "@contextvm/sdk"; + +const transport = new NostrClientTransport({ + signer, + relayHandler: relayPool, + serverPubkey: "server-hex-key", + + // Force the client to encrypt all outbound messages + encryptionMode: "Required", + + // Request that relays delete the outer envelope quickly + giftWrapMode: "Ephemeral", +}); +``` + +### Client Encryption Modes + +- `"Required"`: Always encrypt outbound messages. If the server cannot decrypt them, the connection will fail. +- `"Optional"`: Attempt to use encryption if the server advertised support via its `support_encryption` discovery tag. Fall back to plaintext otherwise. +- `"Disabled"`: Always use plaintext. + +## Verifying Encryption + +To verify encryption is active, you can monitor the Nostr relay traffic using a raw relay inspector (or the logging output of the SDKs). + +- **Plaintext** traffic appears as `kind: 25910` events with the JSON-RPC payload in the `content`. +- **Encrypted** traffic appears as `kind: 1059` (persistent gift-wrap) or `kind: 21059` (ephemeral gift-wrap). The `content` will be a random-looking NIP-44 ciphertext block. + +## Rust Equivalent + +If you are using the `rs-sdk`, you configure these modes using the typed `EncryptionMode` and `GiftWrapMode` enums when building your transport configuration: + +```rust +use contextvm_sdk::{EncryptionMode, GiftWrapMode}; +use contextvm_sdk::transport::client::NostrClientTransportConfig; + +let config = NostrClientTransportConfig::default() + .with_relay_urls(vec!["wss://relay.damus.io".to_string()]) + .with_server_pubkey("") + .with_encryption_mode(EncryptionMode::Required) + .with_gift_wrap_mode(GiftWrapMode::Ephemeral); +``` + +## Related Specifications + +- [CEP-4: Encryption Support](/reference/ceps/cep-4) +- [CEP-19: Ephemeral Gift Wraps](/reference/ceps/cep-19) diff --git a/src/content/docs/ts-sdk/payments/client.md b/src/content/docs/how-to/payments/client.md similarity index 96% rename from src/content/docs/ts-sdk/payments/client.md rename to src/content/docs/how-to/payments/client.md index 542965c..8114149 100644 --- a/src/content/docs/ts-sdk/payments/client.md +++ b/src/content/docs/how-to/payments/client.md @@ -21,7 +21,7 @@ A `PaymentHandler` implements one PMI (one payment rail). The built-in handler s import { LnBolt11NwcPaymentHandler, withClientPayments, -} from '@contextvm/sdk/payments'; +} from "@contextvm/sdk/payments"; const handler = new LnBolt11NwcPaymentHandler({ nwcConnectionString: process.env.NWC_CLIENT_CONNECTION!, @@ -68,7 +68,7 @@ This is designed for: import { withClientPayments, type PaymentHandlerRequest, -} from '@contextvm/sdk/payments'; +} from "@contextvm/sdk/payments"; const paidTransport = withClientPayments(baseTransport, { handlers: [handler], @@ -76,7 +76,7 @@ const paidTransport = withClientPayments(baseTransport, { // ctx?.method examples: "tools/call", "prompts/get", "resources/read" // ctx?.capability examples: "tool:add", "prompt:welcome", "resource:greeting://alice" if (req.amount > 500) return false; - if (ctx?.capability === 'tool:expensive_tool') return false; + if (ctx?.capability === "tool:expensive_tool") return false; return true; }, }); diff --git a/src/content/docs/ts-sdk/payments/custom-rails.md b/src/content/docs/how-to/payments/custom-rails.md similarity index 94% rename from src/content/docs/ts-sdk/payments/custom-rails.md rename to src/content/docs/how-to/payments/custom-rails.md index e0cad7a..d37a65f 100644 --- a/src/content/docs/ts-sdk/payments/custom-rails.md +++ b/src/content/docs/how-to/payments/custom-rails.md @@ -35,10 +35,10 @@ import type { PaymentProcessor, PaymentProcessorCreateParams, PaymentProcessorVerifyParams, -} from '@contextvm/sdk/payments'; +} from "@contextvm/sdk/payments"; export class MyRailPaymentProcessor implements PaymentProcessor { - public readonly pmi = 'my-rail-v1'; + public readonly pmi = "my-rail-v1"; public async createPaymentRequired( params: PaymentProcessorCreateParams, @@ -54,7 +54,7 @@ export class MyRailPaymentProcessor implements PaymentProcessor { return { amount: params.amount, pay_req: JSON.stringify({ - invoiceId: '...', + invoiceId: "...", requestEventId: params.requestEventId, }), description: params.description, @@ -86,10 +86,10 @@ Skeleton: import type { PaymentHandler, PaymentHandlerRequest, -} from '@contextvm/sdk/payments'; +} from "@contextvm/sdk/payments"; export class MyRailPaymentHandler implements PaymentHandler { - public readonly pmi = 'my-rail-v1'; + public readonly pmi = "my-rail-v1"; public async canHandle(_req: PaymentHandlerRequest): Promise { // Optional: enforce client policy (max amount, disabled rail, etc.) diff --git a/src/content/docs/ts-sdk/payments/getting-started.md b/src/content/docs/how-to/payments/getting-started.md similarity index 84% rename from src/content/docs/ts-sdk/payments/getting-started.md rename to src/content/docs/how-to/payments/getting-started.md index 23254cb..8d40e03 100644 --- a/src/content/docs/ts-sdk/payments/getting-started.md +++ b/src/content/docs/how-to/payments/getting-started.md @@ -17,15 +17,15 @@ If you haven’t set up Nostr transports yet, start with the transport docs firs Define what is paid using `pricedCapabilities`. ```ts -import type { PricedCapability } from '@contextvm/sdk/payments'; +import type { PricedCapability } from "@contextvm/sdk/payments"; export const pricedCapabilities: PricedCapability[] = [ { - method: 'tools/call', - name: 'my-tool', + method: "tools/call", + name: "my-tool", amount: 10, - currencyUnit: 'sats', - description: 'Example paid tool', + currencyUnit: "sats", + description: "Example paid tool", }, ]; ``` @@ -38,8 +38,8 @@ Create a processor, then wrap your server transport. import { LnBolt11NwcPaymentProcessor, withServerPayments, -} from '@contextvm/sdk/payments'; -import { NostrServerTransport } from '@contextvm/sdk/transport'; +} from "@contextvm/sdk/payments"; +import { NostrServerTransport } from "@contextvm/sdk/transport"; const baseTransport = new NostrServerTransport({ signer, @@ -71,8 +71,8 @@ Create a handler and wrap your client transport. import { LnBolt11NwcPaymentHandler, withClientPayments, -} from '@contextvm/sdk/payments'; -import { NostrClientTransport } from '@contextvm/sdk/transport'; +} from "@contextvm/sdk/payments"; +import { NostrClientTransport } from "@contextvm/sdk/transport"; const baseTransport = new NostrClientTransport({ signer, @@ -97,7 +97,7 @@ Any request that matches `pricedCapabilities` will trigger the payment flow. ```ts await client.callTool({ - name: 'my-tool', + name: "my-tool", arguments: { example: true }, }); ``` diff --git a/src/content/docs/ts-sdk/payments/rails/lightning-nwc.md b/src/content/docs/how-to/payments/rails/lightning-nwc.md similarity index 92% rename from src/content/docs/ts-sdk/payments/rails/lightning-nwc.md rename to src/content/docs/how-to/payments/rails/lightning-nwc.md index 148e937..31fbd86 100644 --- a/src/content/docs/ts-sdk/payments/rails/lightning-nwc.md +++ b/src/content/docs/how-to/payments/rails/lightning-nwc.md @@ -24,7 +24,7 @@ In practice, both the server and the client will have an NWC connection string. ### Server processor ```ts -import { LnBolt11NwcPaymentProcessor } from '@contextvm/sdk/payments'; +import { LnBolt11NwcPaymentProcessor } from "@contextvm/sdk/payments"; const processor = new LnBolt11NwcPaymentProcessor({ nwcConnectionString: process.env.NWC_SERVER_CONNECTION!, @@ -36,7 +36,7 @@ The server-side NWC wallet must be able to **create invoices** and support whate ### Client handler ```ts -import { LnBolt11NwcPaymentHandler } from '@contextvm/sdk/payments'; +import { LnBolt11NwcPaymentHandler } from "@contextvm/sdk/payments"; const handler = new LnBolt11NwcPaymentHandler({ nwcConnectionString: process.env.NWC_CLIENT_CONNECTION!, diff --git a/src/content/docs/ts-sdk/payments/server.md b/src/content/docs/how-to/payments/server.md similarity index 89% rename from src/content/docs/ts-sdk/payments/server.md rename to src/content/docs/how-to/payments/server.md index 0fda9b8..2a7d5b0 100644 --- a/src/content/docs/ts-sdk/payments/server.md +++ b/src/content/docs/how-to/payments/server.md @@ -17,15 +17,15 @@ For priced requests, the middleware ensures **no unpaid forwarding**. You price individual capabilities by `method` + `name`. ```ts -import type { PricedCapability } from '@contextvm/sdk/payments'; +import type { PricedCapability } from "@contextvm/sdk/payments"; const pricedCapabilities: PricedCapability[] = [ - { method: 'tools/call', name: 'add', amount: 10, currencyUnit: 'sats' }, + { method: "tools/call", name: "add", amount: 10, currencyUnit: "sats" }, { - method: 'resources/read', - name: 'private://*', + method: "resources/read", + name: "private://*", amount: 5, - currencyUnit: 'sats', + currencyUnit: "sats", }, ]; ``` @@ -48,7 +48,7 @@ You can configure multiple processors (multiple PMIs). The server selects a proc import { LnBolt11NwcPaymentProcessor, withServerPayments, -} from '@contextvm/sdk/payments'; +} from "@contextvm/sdk/payments"; const processor = new LnBolt11NwcPaymentProcessor({ nwcConnectionString: process.env.NWC_SERVER_CONNECTION!, @@ -65,7 +65,7 @@ withServerPayments(transport, { `resolvePrice` runs on every priced request and returns the final quote. ```ts -import type { ResolvePriceFn } from '@contextvm/sdk/payments'; +import type { ResolvePriceFn } from "@contextvm/sdk/payments"; const resolvePrice: ResolvePriceFn = async ({ capability, @@ -102,12 +102,12 @@ Guidance: To reject a priced request without creating an invoice, return `{ reject: true, message? }` from `resolvePrice`. ```ts -import type { ResolvePriceFn } from '@contextvm/sdk/payments'; +import type { ResolvePriceFn } from "@contextvm/sdk/payments"; const resolvePrice: ResolvePriceFn = async ({ capability, clientPubkey }) => { const isBlocked = await isUserBlocked(clientPubkey); if (isBlocked) { - return { reject: true, message: 'Access denied' }; + return { reject: true, message: "Access denied" }; } return { amount: capability.amount }; diff --git a/src/content/docs/index.mdx b/src/content/docs/index.mdx index ae457d1..071625c 100644 --- a/src/content/docs/index.mdx +++ b/src/content/docs/index.mdx @@ -22,9 +22,9 @@ hero: variant: minimal --- -import { CardGrid } from '@astrojs/starlight/components'; -import IconLinkCard from '../../components/IconLinkCard.astro'; -import HeroGetStartedCTAStyles from '../../components/HeroGetStartedCTAStyles.astro'; +import { CardGrid } from "@astrojs/starlight/components"; +import IconLinkCard from "../../components/IconLinkCard.astro"; +import HeroGetStartedCTAStyles from "../../components/HeroGetStartedCTAStyles.astro"; @@ -34,42 +34,49 @@ import HeroGetStartedCTAStyles from '../../components/HeroGetStartedCTAStyles.as + diff --git a/src/content/docs/spec/ceps/cep-15.md b/src/content/docs/reference/ceps/cep-15.md similarity index 99% rename from src/content/docs/spec/ceps/cep-15.md rename to src/content/docs/reference/ceps/cep-15.md index 11433d5..ab9a320 100644 --- a/src/content/docs/spec/ceps/cep-15.md +++ b/src/content/docs/reference/ceps/cep-15.md @@ -495,7 +495,7 @@ Works identically across all providers implementing the same schema hash. ## Dependencies -- [CEP-6: Public Server Announcements](/spec/ceps/cep-6) +- [CEP-6: Public Server Announcements](/reference/ceps/cep-6) - [NIP-73: External Content IDs](https://github.com/nostr-protocol/nips/blob/master/73.md) - [RFC 8785: JSON Canonicalization Scheme (JCS)](https://tools.ietf.org/html/rfc8785) - [MCP Specification: Tools](https://modelcontextprotocol.io/specification/2025-11-25/server/tools) diff --git a/src/content/docs/spec/ceps/cep-17.md b/src/content/docs/reference/ceps/cep-17.md similarity index 97% rename from src/content/docs/spec/ceps/cep-17.md rename to src/content/docs/reference/ceps/cep-17.md index 3b5ce9a..561885d 100644 --- a/src/content/docs/spec/ceps/cep-17.md +++ b/src/content/docs/reference/ceps/cep-17.md @@ -138,7 +138,7 @@ Servers that do not publish a relay list event are treated the same as before - ## Dependencies -- [CEP-6: Public Server Announcements](/spec/ceps/cep-6) - Server announcements (kind 11316) provide the pubkey needed to query relay lists +- [CEP-6: Public Server Announcements](/reference/ceps/cep-6) - Server announcements (kind 11316) provide the pubkey needed to query relay lists ## Reference Implementation diff --git a/src/content/docs/spec/ceps/cep-19.md b/src/content/docs/reference/ceps/cep-19.md similarity index 96% rename from src/content/docs/spec/ceps/cep-19.md rename to src/content/docs/reference/ceps/cep-19.md index 68cf17d..81fb54f 100644 --- a/src/content/docs/spec/ceps/cep-19.md +++ b/src/content/docs/reference/ceps/cep-19.md @@ -105,6 +105,6 @@ A reference implementation is available in the [ContextVM SDK](https://github.co ## Dependencies -- [CEP-4: Encryption Support](/spec/ceps/cep-4) -- [CEP-6: Public Server Announcements](/spec/ceps/cep-6) -- [CEP-35: Stateless Session Discovery and Capability Learning](/spec/ceps/informational/cep-35) +- [CEP-4: Encryption Support](/reference/ceps/cep-4) +- [CEP-6: Public Server Announcements](/reference/ceps/cep-6) +- [CEP-35: Stateless Session Discovery and Capability Learning](/reference/ceps/informational/cep-35) diff --git a/src/content/docs/spec/ceps/cep-22.md b/src/content/docs/reference/ceps/cep-22.md similarity index 99% rename from src/content/docs/spec/ceps/cep-22.md rename to src/content/docs/reference/ceps/cep-22.md index db57c81..a826410 100644 --- a/src/content/docs/spec/ceps/cep-22.md +++ b/src/content/docs/reference/ceps/cep-22.md @@ -461,7 +461,7 @@ Implementations that ignore the new tags or do not understand the oversized-tran ## Dependencies -- [CEP-6: Public Server Announcements](/spec/ceps/cep-6) +- [CEP-6: Public Server Announcements](/reference/ceps/cep-6) ## Reference Implementation diff --git a/src/content/docs/spec/ceps/cep-23.md b/src/content/docs/reference/ceps/cep-23.md similarity index 98% rename from src/content/docs/spec/ceps/cep-23.md rename to src/content/docs/reference/ceps/cep-23.md index 489fad5..d5658dd 100644 --- a/src/content/docs/spec/ceps/cep-23.md +++ b/src/content/docs/reference/ceps/cep-23.md @@ -79,4 +79,4 @@ A reference implementation of this metadata publishing can be found in the [Cont ## Dependencies -- [CEP-6: Public Server Announcements](/spec/ceps/cep-6) +- [CEP-6: Public Server Announcements](/reference/ceps/cep-6) diff --git a/src/content/docs/spec/ceps/cep-24.md b/src/content/docs/reference/ceps/cep-24.md similarity index 98% rename from src/content/docs/spec/ceps/cep-24.md rename to src/content/docs/reference/ceps/cep-24.md index 74647a8..7b06c8e 100644 --- a/src/content/docs/spec/ceps/cep-24.md +++ b/src/content/docs/reference/ceps/cep-24.md @@ -112,4 +112,4 @@ A reference implementation for server review UI and data fetching can be found i ## Dependencies -- [CEP-6: Public Server Announcements](/spec/ceps/cep-6) +- [CEP-6: Public Server Announcements](/reference/ceps/cep-6) diff --git a/src/content/docs/spec/ceps/cep-4.md b/src/content/docs/reference/ceps/cep-4.md similarity index 98% rename from src/content/docs/spec/ceps/cep-4.md rename to src/content/docs/reference/ceps/cep-4.md index 4064207..80f6d03 100644 --- a/src/content/docs/spec/ceps/cep-4.md +++ b/src/content/docs/reference/ceps/cep-4.md @@ -118,4 +118,4 @@ A reference implementation can be found in the [ContextVM sdk](https://github.co ## Dependencies -- [cep-19: Ephemeral Gift Wraps](/spec/ceps/cep-19) +- [cep-19: Ephemeral Gift Wraps](/reference/ceps/cep-19) diff --git a/src/content/docs/spec/ceps/cep-41.md b/src/content/docs/reference/ceps/cep-41.md similarity index 98% rename from src/content/docs/spec/ceps/cep-41.md rename to src/content/docs/reference/ceps/cep-41.md index 1a236e7..cd85e01 100644 --- a/src/content/docs/spec/ceps/cep-41.md +++ b/src/content/docs/reference/ceps/cep-41.md @@ -518,11 +518,11 @@ This CEP introduces no breaking changes: ## Dependencies -- [CEP-6: Public Server Announcements](/spec/ceps/cep-6) -- [CEP-19: Ephemeral Gift Wraps](/spec/ceps/cep-19) -- [CEP-22: Oversized Payload Transfer](/spec/ceps/cep-22) -- [CEP-35: Discoverability Patterns for ContextVM Capabilities](/spec/ceps/informational/cep-35) +- [CEP-6: Public Server Announcements](/reference/ceps/cep-6) +- [CEP-19: Ephemeral Gift Wraps](/reference/ceps/cep-19) +- [CEP-22: Oversized Payload Transfer](/reference/ceps/cep-22) +- [CEP-35: Discoverability Patterns for ContextVM Capabilities](/reference/ceps/informational/cep-35) ## Reference Implementation -A reference implementation can be found in the ContextVM TS SDK. \ No newline at end of file +A reference implementation can be found in the ContextVM TS SDK. diff --git a/src/content/docs/spec/ceps/cep-6.md b/src/content/docs/reference/ceps/cep-6.md similarity index 95% rename from src/content/docs/spec/ceps/cep-6.md rename to src/content/docs/reference/ceps/cep-6.md index 815c540..e156679 100644 --- a/src/content/docs/spec/ceps/cep-6.md +++ b/src/content/docs/reference/ceps/cep-6.md @@ -103,7 +103,7 @@ Because direct-message replay is session-scoped, it does not replace public anno As in the Server Announcement event, the `content` field contains a JSON string with the list of capabilities. The list is the result of a call to the `list` method of each capability. -**Note**: For tools list announcements (kind 11317), see [CEP-15: Common Tool Schemas](/spec/ceps/cep-15) for additional tag conventions that enable schema discovery and ecosystem integration. +**Note**: For tools list announcements (kind 11317), see [CEP-15: Common Tool Schemas](/reference/ceps/cep-15) for additional tag conventions that enable schema discovery and ecosystem integration. ### Tools List Event Example @@ -161,6 +161,6 @@ A reference implementation can be found in the [ContextVM SDK server transport i ## Dependencies -- [CEP-4: Encryption Support](/spec/ceps/cep-4) -- [CEP-15: Common Tool Schemas](/spec/ceps/cep-15) — Extends tools list announcements with schema discovery and NIP-73 integration -- [CEP-35: Stateless Session Discovery and Capability Learning](/spec/ceps/informational/cep-35) +- [CEP-4: Encryption Support](/reference/ceps/cep-4) +- [CEP-15: Common Tool Schemas](/reference/ceps/cep-15) — Extends tools list announcements with schema discovery and NIP-73 integration +- [CEP-35: Stateless Session Discovery and Capability Learning](/reference/ceps/informational/cep-35) diff --git a/src/content/docs/spec/ceps/cep-8.md b/src/content/docs/reference/ceps/cep-8.md similarity index 97% rename from src/content/docs/spec/ceps/cep-8.md rename to src/content/docs/reference/ceps/cep-8.md index 852c674..2ce470e 100644 --- a/src/content/docs/spec/ceps/cep-8.md +++ b/src/content/docs/reference/ceps/cep-8.md @@ -37,7 +37,7 @@ This CEP defines: This CEP does **not** define: -- Privacy guarantees for payment messages (use encryption mechanisms in [CEP-4](/spec/ceps/cep-4) where required). +- Privacy guarantees for payment messages (use encryption mechanisms in [CEP-4](/reference/ceps/cep-4) where required). - Rate limiting / abuse prevention mechanisms. - Currency conversion rules or exchange rate discovery. @@ -152,7 +152,7 @@ Pricing information is advertised using the `cap` tag in server announcements an The `cap` tag indicates that using the `get_weather` tool costs 100 satoshis, allowing clients to display pricing to users. -When `cap` tags are attached to a capability list response, they describe the pricing surface of that specific response payload. They are response-local discovery metadata for the listed capabilities, not by themselves a replacement for the peer's general session discovery baseline as defined in [CEP-35: Stateless Session Discovery and Capability Learning](/spec/ceps/informational/cep-35). +When `cap` tags are attached to a capability list response, they describe the pricing surface of that specific response payload. They are response-local discovery metadata for the listed capabilities, not by themselves a replacement for the peer's general session discovery baseline as defined in [CEP-35: Stateless Session Discovery and Capability Learning](/reference/ceps/informational/cep-35). ### Payment Method Identifiers (PMI) @@ -176,7 +176,7 @@ PMIs MUST follow the format defined by the [W3C Payment Method Identifiers](http This CEP maintains no in-document registry of recommended PMIs. -Recommended PMIs and naming conventions are documented in the informational companion CEP, [CEP-21: Payment Method Identifier (PMI) Recommendations](/spec/ceps/informational/cep-21). +Recommended PMIs and naming conventions are documented in the informational companion CEP, [CEP-21: Payment Method Identifier (PMI) Recommendations](/reference/ceps/informational/cep-21). #### PMI Benefits and Roles @@ -189,7 +189,7 @@ Using standardized PMIs provides: ### PMI Discovery -PMI discovery allows clients and servers to determine compatibility with payment methods, similar to encryption support discovery in [CEP-4](/spec/ceps/cep-4). +PMI discovery allows clients and servers to determine compatibility with payment methods, similar to encryption support discovery in [CEP-4](/reference/ceps/cep-4). #### PMI Advertisement @@ -245,7 +245,7 @@ Servers can discover PMI support through: In stateless operation (no prior initialization), clients that want to use paid capabilities SHOULD include one or more `pmi` tags in the request event so the server can select a compatible payment method. -When sent on the first direct client-to-server message of a session, these `pmi` tags participate in the session discovery baseline described by [CEP-35: Stateless Session Discovery and Capability Learning](/spec/ceps/informational/cep-35). When sent on later requests, they are interpreted in the context of those requests unless another CEP explicitly defines stronger session-update semantics. +When sent on the first direct client-to-server message of a session, these `pmi` tags participate in the session discovery baseline described by [CEP-35: Stateless Session Discovery and Capability Learning](/reference/ceps/informational/cep-35). When sent on later requests, they are interpreted in the context of those requests unless another CEP explicitly defines stronger session-update semantics. ### Payment Flow @@ -492,7 +492,7 @@ A reference implementation of this CEP is available in the [ContextVM TypeScript ## Dependencies -- [CEP-4: Encryption Support](/spec/ceps/cep-4) -- [CEP-6: Public Server Announcements](/spec/ceps/cep-6) -- [CEP-35: Stateless Session Discovery and Capability Learning](/spec/ceps/informational/cep-35) +- [CEP-4: Encryption Support](/reference/ceps/cep-4) +- [CEP-6: Public Server Announcements](/reference/ceps/cep-6) +- [CEP-35: Stateless Session Discovery and Capability Learning](/reference/ceps/informational/cep-35) - [W3C Payment Method Identifiers](https://www.w3.org/TR/payment-method-id/) diff --git a/src/content/docs/spec/ceps/informational/cep-16.md b/src/content/docs/reference/ceps/informational/cep-16.md similarity index 100% rename from src/content/docs/spec/ceps/informational/cep-16.md rename to src/content/docs/reference/ceps/informational/cep-16.md diff --git a/src/content/docs/spec/ceps/informational/cep-21.md b/src/content/docs/reference/ceps/informational/cep-21.md similarity index 94% rename from src/content/docs/spec/ceps/informational/cep-21.md rename to src/content/docs/reference/ceps/informational/cep-21.md index 7181400..a64b2f3 100644 --- a/src/content/docs/spec/ceps/informational/cep-21.md +++ b/src/content/docs/reference/ceps/informational/cep-21.md @@ -11,7 +11,7 @@ description: Recommended PMIs and naming conventions for CEP-8 payments ## Abstract -This CEP provides **non-normative** recommendations for Payment Method Identifiers (PMIs) used with [CEP-8](/spec/ceps/cep-8). +This CEP provides **non-normative** recommendations for Payment Method Identifiers (PMIs) used with [CEP-8](/reference/ceps/cep-8). It exists to: @@ -61,4 +61,4 @@ This is a convention for discovery and interoperability; support is ultimately i ## Dependencies -- [CEP-8: Capability Pricing and Payment Flow](/spec/ceps/cep-8) +- [CEP-8: Capability Pricing and Payment Flow](/reference/ceps/cep-8) diff --git a/src/content/docs/spec/ceps/informational/cep-35.md b/src/content/docs/reference/ceps/informational/cep-35.md similarity index 94% rename from src/content/docs/spec/ceps/informational/cep-35.md rename to src/content/docs/reference/ceps/informational/cep-35.md index f0a95d7..06149f3 100644 --- a/src/content/docs/spec/ceps/informational/cep-35.md +++ b/src/content/docs/reference/ceps/informational/cep-35.md @@ -152,11 +152,11 @@ This CEP does not replace feature-specific CEPs. Instead, it defines the behavio Relevant specifications include: -- [CEP-6: Public Server Announcements](/spec/ceps/cep-6) -- [CEP-8: Capability Pricing and Payment Flow](/spec/ceps/cep-8) -- [CEP-17: Relay List Metadata Discovery](/spec/ceps/cep-17) -- [CEP-19: Ephemeral Gift Wrap for Encrypted Transport](/spec/ceps/cep-19) -- [CEP-22: Oversized Transfer](/spec/ceps/cep-22) +- [CEP-6: Public Server Announcements](/reference/ceps/cep-6) +- [CEP-8: Capability Pricing and Payment Flow](/reference/ceps/cep-8) +- [CEP-17: Relay List Metadata Discovery](/reference/ceps/cep-17) +- [CEP-19: Ephemeral Gift Wrap for Encrypted Transport](/reference/ceps/cep-19) +- [CEP-22: Oversized Transfer](/reference/ceps/cep-22) These CEPs define individual discovery vocabularies or feature semantics. This CEP defines how implementations should exchange and learn that information in stateless session context. @@ -192,7 +192,7 @@ This behavior is already established in the ContextVM TypeScript SDK, which serv ## Dependencies -- [CEP-6: Public Server Announcements](/spec/ceps/cep-6) -- [CEP-8: Capability Pricing and Payment Flow](/spec/ceps/cep-8) -- [CEP-19: Ephemeral Gift Wrap for Encrypted Transport](/spec/ceps/cep-19) -- [CEP-22: Oversized Transfer](/spec/ceps/cep-22) +- [CEP-6: Public Server Announcements](/reference/ceps/cep-6) +- [CEP-8: Capability Pricing and Payment Flow](/reference/ceps/cep-8) +- [CEP-19: Ephemeral Gift Wrap for Encrypted Transport](/reference/ceps/cep-19) +- [CEP-22: Oversized Transfer](/reference/ceps/cep-22) diff --git a/src/content/docs/reference/rs-sdk/.gitkeep b/src/content/docs/reference/rs-sdk/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/content/docs/reference/rs-sdk/client-transport.md b/src/content/docs/reference/rs-sdk/client-transport.md new file mode 100644 index 0000000..a39493c --- /dev/null +++ b/src/content/docs/reference/rs-sdk/client-transport.md @@ -0,0 +1,176 @@ +--- +title: "Native Client Guide" +description: "Learn how to build native ContextVM clients using NostrClientTransport." +--- + +The recommended architecture is: + +- define an `rmcp` client handler or use a lightweight client info object +- create a `NostrClientTransport` +- attach the transport with `rmcp`'s `ServiceExt` + +This follows the same pattern as the standard `rmcp` client examples, except the transport is ContextVM over Nostr instead of HTTP. + +## The high-level shape + +In `rmcp`, a client is typically started with `client_info.serve(transport)`. + +For ContextVM, the transport becomes `NostrClientTransport`. In the current SDK API, you pass that transport directly to `ServiceExt`; there is no extra adapter step in the public workflow. + +## Loading an existing private key + +The signer helper is not limited to ephemeral keys. If you already have a private key, load it with `from_sk()`. + +It accepts either: + +- a 64-character hex secret key +- an `nsec` bech32 secret key + +```rust +use contextvm_sdk::signer; + +let signer = signer::from_sk("")?; +println!("client pubkey: {}", signer.public_key().to_hex()); +``` + +Use `generate()` only when you explicitly want a new random identity for a short-lived client or test flow. + +## Example + +```rust +use anyhow::Context; +use contextvm_sdk::transport::client::{ + NostrClientTransport, NostrClientTransportConfig, +}; +use contextvm_sdk::{signer, EncryptionMode, GiftWrapMode}; +use rmcp::{ + model::{CallToolRequestParams, CallToolResult}, + ClientHandler, ServiceExt, +}; + +const RELAY_URL: &str = "wss://relay.contextvm.org"; + +#[derive(Clone, Default)] +struct DemoClient; + +impl ClientHandler for DemoClient {} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + let server_pubkey = std::env::args() + .nth(1) + .context("Usage: native_echo_client ")?; + + tracing_subscriber::fmt() + .with_env_filter( + tracing_subscriber::EnvFilter::from_default_env() + .add_directive("contextvm_sdk=info".parse()?) + .add_directive("rmcp=warn".parse()?), + ) + .init(); + + let signer = signer::generate(); + + println!("Native ContextVM echo client starting"); + println!("Relay: {RELAY_URL}"); + println!("Client pubkey: {}", signer.public_key().to_hex()); + println!("Target server pubkey: {server_pubkey}"); + + let transport = NostrClientTransport::new( + signer, + NostrClientTransportConfig::default() + .with_relay_urls(vec![RELAY_URL.to_string()]) + .with_server_pubkey(server_pubkey) + .with_encryption_mode(EncryptionMode::Optional) + .with_gift_wrap_mode(GiftWrapMode::Optional), + ) + .await?; + + let client = DemoClient.serve(transport).await?; + + let peer_info = client + .peer_info() + .expect("server did not provide peer info after initialize"); + println!("Connected to: {:?}", peer_info.server_info.name); + + let tools = client.list_all_tools().await?; + println!("Discovered {} tool(s):", tools.len()); + for tool in &tools { + println!("- {}", tool.name); + } + + let result = client + .call_tool(CallToolRequestParams { + name: "echo".into(), + arguments: serde_json::from_value(serde_json::json!({ + "message": "hello from native contextvm client" + })) + .ok(), + meta: None, + task: None, + }) + .await?; + + println!("Echo result: {}", first_text(&result)); + client.cancel().await?; + Ok(()) +} + +fn first_text(result: &CallToolResult) -> String { + result + .content + .iter() + .find_map(|content| { + if let rmcp::model::RawContent::Text(text) = &content.raw { + Some(text.text.clone()) + } else { + None + } + }) + .unwrap_or_default() +} +``` + +This is the ContextVM equivalent of the usual `rmcp` client workflow, but using `NostrClientTransport` directly. + +## What the transport adds + +`NostrClientTransport` adds ContextVM-specific client behavior on top of `rmcp` client semantics: + +- relay connection management via `NostrClientTransport::new()` +- target server selection through `server_pubkey` in `NostrClientTransportConfig` +- request and response correlation via `send()` +- server capability learning from discovery tags +- optional stateless behavior via `is_stateless` in `NostrClientTransportConfig` +- encrypted message reception and gift-wrap deduplication during notification handling + +## Configuration fields that matter first + +Start with these fields in `NostrClientTransportConfig`: + +- `relay_urls`: relays the client uses to reach the server (empty = use relay resolution) +- `server_pubkey`: the target server's public key (hex, npub, or nprofile with relay hints) +- `encryption_mode`: whether plaintext is allowed +- `gift_wrap_mode`: whether to use persistent or ephemeral wrapping +- `is_stateless`: whether initialize is emulated locally for stateless workflows +- `timeout`: how long request correlation waits for a response +- `discovery_relay_urls`: bootstrap relays for CEP-17 kind 10002 relay-list discovery (defaults to `DEFAULT_BOOTSTRAP_RELAY_URLS`) +- `fallback_operational_relay_urls`: non-authoritative relays probed in parallel with CEP-17 discovery + +## When to use this instead of the proxy + +Use this page's approach when you are writing a new Rust MCP client that should speak ContextVM natively. + +Use the proxy guide when you want a simpler message-oriented bridge and do not want the full `rmcp` running client model. + +## Behavioral notes + +- The client-side `rmcp` handshake is driven by `ServiceExt::serve()` on the client handler. +- The initialize request is sent automatically as part of the running client startup sequence. +- Stateless initialization behavior is covered by the conformance tests. +- Capability learning and gift-wrap handling happen inside the client transport implementation. +- When `relay_urls` is empty, `start()` runs 6-stage relay resolution before connecting: configured relays > nprofile hints > CEP-17 kind 10002 discovery > fallback probing > bootstrap defaults. Callers can set `server_pubkey` to an nprofile and omit `relay_urls` entirely. + +--- + +_This page was ported from the [ContextVM Rust SDK repository](https://github.com/ContextVM/rs-sdk/tree/main/docs/client-transport.md)._ diff --git a/src/content/docs/reference/rs-sdk/discovery.md b/src/content/docs/reference/rs-sdk/discovery.md new file mode 100644 index 0000000..f5a06fd --- /dev/null +++ b/src/content/docs/reference/rs-sdk/discovery.md @@ -0,0 +1,93 @@ +--- +title: "Discovery" +description: "Use public discovery helpers to find announced ContextVM servers and capabilities." +--- + +The Rust SDK exposes public discovery helpers for finding announced servers and their published capabilities. + +These functions query public announcement events so clients can find servers before opening a direct session. + +## What is discoverable + +The current implementation supports: + +- servers via `discover_servers()` +- tools via `discover_tools()` +- resources via `discover_resources()` +- prompts via `discover_prompts()` +- resource templates via `discover_resource_templates()` + +When the `rmcp` feature is enabled, typed variants are also available for tools, resources, prompts, and resource templates. + +## Minimal example + +This follows the repository discovery example. + +```rust +use contextvm_sdk::discovery; +use contextvm_sdk::relay::RelayPool; +use contextvm_sdk::signer; + +#[tokio::main] +async fn main() -> contextvm_sdk::Result<()> { + let keys = signer::generate(); + let relays = vec!["wss://relay.damus.io".to_string()]; + + let relay_pool = RelayPool::new(keys).await?; + relay_pool.connect(&relays).await?; + let client = relay_pool.client(); + + let servers = discovery::discover_servers(client, &relays).await?; + for server in &servers { + println!("server: {:?}", server.server_info.name); + + let tools = discovery::discover_tools(client, &server.pubkey_parsed, &relays).await?; + println!("tools: {}", tools.len()); + } + + relay_pool.disconnect().await?; + Ok(()) +} +``` + +## Discovery event model + +The event kinds follow the public announcement model summarized in the repository root README: + +- `11316`: server announcement +- `11317`: tools list +- `11318`: resources list +- `11319`: resource templates +- `11320`: prompts list + +These event kinds are the SDK's public discovery model for server metadata and advertised MCP capabilities. + +## CEP-17 automatic relay resolution + +Clients can discover which relays a server uses without hardcoding relay URLs. Set `server_pubkey` to an nprofile (which embeds relay hints) and leave `relay_urls` empty: + +```rust +NostrClientTransportConfig::default() + .with_server_pubkey("nprofile1...") // contains pubkey + relay hints +``` + +When `start()` is called with empty `relay_urls`, the transport runs a 6-stage resolution pipeline: + +1. **Configured relays** -- if `relay_urls` is non-empty, use them directly +2. **nprofile hints** -- relay URLs embedded in the nprofile identity +3. **CEP-17 discovery** -- fetch kind 10002 relay-list events from bootstrap relays +4. **Fallback probing** -- probe `fallback_operational_relay_urls` in parallel with discovery +5. **Sequential fallback** -- if the race winner returned empty, await the other +6. **Bootstrap default** -- fall back to `DEFAULT_BOOTSTRAP_RELAY_URLS` as last resort + +The `discovery_relay_urls` and `fallback_operational_relay_urls` config fields customize stages 3-4. + +## Important limitations + +- discovery is public metadata, not a replacement for direct transport negotiation +- the current helpers fetch and parse latest public lists, but they do not replace direct session learning +- direct session learning still matters for encryption preferences, gift-wrap support, and first-message capability hints on an active connection + +--- + +_This page was ported from the [ContextVM Rust SDK repository](https://github.com/ContextVM/rs-sdk/tree/main/docs/discovery.md)._ diff --git a/src/content/docs/reference/rs-sdk/encryption.md b/src/content/docs/reference/rs-sdk/encryption.md new file mode 100644 index 0000000..4de8e30 --- /dev/null +++ b/src/content/docs/reference/rs-sdk/encryption.md @@ -0,0 +1,83 @@ +--- +title: "Encryption" +description: "Configure NIP-44 encryption and NIP-59 gift-wrapping in the Rust SDK." +--- + +ContextVM encryption in this SDK is controlled by `EncryptionMode` and `GiftWrapMode`. + +At a high level, direct traffic can be sent as plaintext ContextVM events or as encrypted NIP-44 payloads wrapped in gift-wrap events, depending on the configured policy on both peers. + +## Encryption modes + +`EncryptionMode` has three modes: + +- `Optional`: accept both plaintext and encrypted traffic +- `Required`: require encrypted traffic +- `Disabled`: reject encrypted traffic and use plaintext only + +These semantics are not only conceptual; they are also exercised by the transport integration tests. + +## Gift-wrap modes + +`GiftWrapMode` controls which outer encrypted event kind is used: + +- `Optional`: accept both modes and default to persistent wrapping until ephemeral support is learned from the peer +- `Ephemeral`: use kind `21059` +- `Persistent`: use kind `1059` + +The helper methods `allows_kind()` and `supports_ephemeral()` show the expected policy behavior. + +## Practical rules + +### Plaintext transport + +Plaintext ContextVM messages use kind `25910` and keep the MCP JSON-RPC payload in the event content. + +### Encrypted transport + +Encrypted ContextVM messages: + +1. serialize the MCP payload to JSON +2. encrypt it with NIP-44 +3. wrap it as a gift-wrap event using kind `1059` or `21059` + +The implementation details live in the SDK encryption module. + +## Response mirroring + +One important implementation detail is that server responses mirror the client’s inbound encryption format when policy allows it. + +This behavior is verified by the transport integration tests and is important for interoperable mixed-mode deployments. + +## Deduplication + +Both client and server transports deduplicate encrypted outer gift-wrap event ids before delivering them. + +This is covered by the deduplication and encrypted transport integration tests. + +## Example configuration + +```rust +use contextvm_sdk::core::types::{EncryptionMode, GiftWrapMode}; +use contextvm_sdk::transport::client::NostrClientTransportConfig; + +let config = NostrClientTransportConfig::default() + .with_relay_urls(vec!["wss://relay.damus.io".to_string()]) + .with_server_pubkey("") + .with_encryption_mode(EncryptionMode::Optional) + .with_gift_wrap_mode(GiftWrapMode::Optional); +``` + +## Discovery tags + +Encryption support is also surfaced through discovery tags and first-message capability learning. + +In practice, this matters for: + +- public announcements +- the first direct server-to-client message +- stateless operation + +--- + +_This page was ported from the [ContextVM Rust SDK repository](https://github.com/ContextVM/rs-sdk/tree/main/docs/encryption.md)._ diff --git a/src/content/docs/reference/rs-sdk/gateway.md b/src/content/docs/reference/rs-sdk/gateway.md new file mode 100644 index 0000000..f0d1477 --- /dev/null +++ b/src/content/docs/reference/rs-sdk/gateway.md @@ -0,0 +1,138 @@ +--- +title: "Gateway" +description: "Expose an existing local MCP server to the Nostr network using NostrMCPGateway." +--- + +`NostrMCPGateway` is the simplest way to expose an MCP server through ContextVM. + +It wraps `NostrServerTransport`, receives incoming ContextVM requests from Nostr, and lets your application send responses back using the inbound event id. + +For native Rust applications, this is usually not the primary path. Most users should build an `rmcp` server and attach `NostrServerTransport` directly, as described in [native server guide](/reference/rs-sdk/server-transport). + +## When to use it + +Use the gateway when: + +- you already have MCP request handling logic +- you want a straightforward server loop +- you want optional public announcements without building directly on the transport layer + +Do not start here if you are writing a new native Rust MCP server from scratch. + +## Minimal example + +This follows the shape of the repository gateway example. + +```rust +use contextvm_sdk::core::types::{ + JsonRpcError, JsonRpcErrorResponse, JsonRpcMessage, JsonRpcResponse, ServerInfo, +}; +use contextvm_sdk::gateway::{GatewayConfig, NostrMCPGateway}; +use contextvm_sdk::signer; +use contextvm_sdk::transport::server::NostrServerTransportConfig; + +#[tokio::main] +async fn main() -> contextvm_sdk::Result<()> { + let keys = signer::generate(); + + let nostr_config = NostrServerTransportConfig::default() + .with_relay_urls(vec!["wss://relay.damus.io".to_string()]) + .with_server_info( + ServerInfo::default() + .with_name("Echo Server".to_string()) + .with_about("A simple ContextVM server".to_string()), + ) + .with_announced_server(true); + + let config = GatewayConfig::new(nostr_config); + + let mut gateway = NostrMCPGateway::new(keys, config).await?; + let mut rx = gateway.start().await?; + gateway.announce().await?; + + while let Some(req) = rx.recv().await { + let response = match &req.message { + JsonRpcMessage::Request(request) if request.method == "tools/list" => { + JsonRpcMessage::Response(JsonRpcResponse { + jsonrpc: "2.0".to_string(), + id: request.id.clone(), + result: serde_json::json!({ + "tools": [{ + "name": "echo", + "description": "Echo a message", + "inputSchema": { + "type": "object", + "properties": { + "message": { "type": "string" } + }, + "required": ["message"] + } + }] + }), + }) + } + JsonRpcMessage::Request(request) => JsonRpcMessage::ErrorResponse(JsonRpcErrorResponse { + jsonrpc: "2.0".to_string(), + id: request.id.clone(), + error: JsonRpcError { + code: -32601, + message: "Method not found".to_string(), + data: None, + }, + }), + _ => continue, + }; + + gateway.send_response(&req.event_id, response).await?; + } + + Ok(()) +} +``` + +## What the gateway gives you + +- a message channel of `IncomingRequest` +- automatic routing of responses by original Nostr event id through `send_response()` +- optional public announcements through `announce()` + +## When not to use it + +Prefer the native server transport path when: + +- your application is already modeled as an `rmcp` `ServerHandler` +- you want the normal `rmcp` running service lifecycle through `ServiceExt` +- you want docs and examples that match the broader `rmcp` ecosystem + +## Important server config + +The main operational knobs live on `NostrServerTransportConfig`: + +- `relay_urls`: relays to connect to +- `encryption_mode`: plaintext vs encrypted session policy +- `gift_wrap_mode`: choose between persistent and ephemeral gift wraps +- `server_info`: metadata used in public announcements +- `is_announced_server`: auto-publish announcements on start (CEP-6) +- `allowed_public_keys`: static client allowlist +- `excluded_capabilities`: allow public access to specific methods or capability names +- `max_sessions`, `cleanup_interval`, `session_timeout`: server-side session lifecycle +- `relay_list_urls`: relay URLs advertised in kind 10002 (CEP-17); defaults to `relay_urls` +- `bootstrap_relay_urls`: additional relays for publishing announcements (CEP-6/17) +- `publish_relay_list`: whether to publish kind 10002 relay list metadata; default `true` +- `profile_metadata`: optional profile metadata for kind 0 publication (CEP-23) + +## Behavioral notes + +- responses are routed using the inbound request event id, not just the JSON-RPC id +- for announced servers, public metadata publication is part of the supported flow +- authorization and allowlist bypass behavior are also exercised by the integration tests + +## rmcp path + +If your server already uses `rmcp`, the gateway also exposes the associated function `NostrMCPGateway::serve_handler()` so you can attach a handler directly without manually running the request loop. + +That said, the preferred native architecture is still `rmcp` service first and ContextVM transport second. + +--- + +_This page was ported from the [ContextVM Rust SDK repository](https://github.com/ContextVM/rs-sdk/tree/main/docs/gateway.md)._ diff --git a/src/content/docs/reference/rs-sdk/overview.md b/src/content/docs/reference/rs-sdk/overview.md new file mode 100644 index 0000000..a937d1e --- /dev/null +++ b/src/content/docs/reference/rs-sdk/overview.md @@ -0,0 +1,110 @@ +--- +title: "Rust SDK Overview" +description: "An overview of the ContextVM Rust SDK architecture and components." +--- + +The Rust SDK implements ContextVM: MCP over Nostr. + +In practice, it lets you transport MCP JSON-RPC messages through Nostr events, add server discovery through announcement events, and optionally encrypt direct traffic with NIP-44 plus gift wrapping. + +## The main mental model + +For native Rust applications, ContextVM is primarily a transport for `rmcp`. + +That means the usual shape is: + +1. define an `rmcp` server or client +2. create a ContextVM Nostr transport +3. attach the transport with `ServiceExt` + +This is the same pattern shown by the `rmcp` server and client examples. The only difference is that this SDK replaces stdio, HTTP, or raw sockets with Nostr transports. + +## Choose the right API + +Most users should start with one of these entry points: + +| Use case | Start with | +| ---------------------------------------------------------- | ----------------------------------------------------------------------------- | +| Build a native ContextVM server | `NostrServerTransport` + `rmcp` `ServiceExt` | +| Build a native ContextVM client | `NostrClientTransport` + `rmcp` `ServiceExt` | +| Expose an already-existing MCP server on Nostr | `NostrMCPGateway` | +| Connect to a remote ContextVM server with a simpler bridge | `NostrMCPProxy` | +| Discover public servers and capabilities | `discover_servers()` and related helpers | +| Work directly with the optional bridge layer | `NostrMCPGateway::serve_handler()` or `NostrMCPProxy::serve_client_handler()` | + +## Architecture + +The crate is organized in layers: + +- `core`: protocol types, validation, serialization, errors +- `relay`: relay pool abstraction +- `signer`: key generation and signer helpers +- `encryption`: NIP-44 and gift-wrap helpers +- `transport`: native ContextVM client and server transports +- `gateway`: wrapper for exposing an existing MCP server flow on Nostr +- `proxy`: wrapper for connecting to a remote server without the full `rmcp` client model +- `discovery`: announcement and capability discovery + +The application-facing `rmcp` layer provides the `ServiceExt` integration point together with the usual server and client startup flow. + +## Protocol model + +ContextVM keeps MCP semantics intact and uses Nostr only as the transport envelope. + +- MCP payloads are represented by `JsonRpcMessage` +- direct plaintext ContextVM traffic uses kind `25910` +- encrypted traffic uses gift-wrap kinds `1059` or `21059` +- public discovery uses kinds `11316` through `11320` +- routing is done with `p` tags and request/response correlation with `e` tags, as reflected in the repository root README + +## Core types you should know + +- `EncryptionMode`: `Optional`, `Required`, `Disabled` +- `GiftWrapMode`: `Optional`, `Ephemeral`, `Persistent` +- `contextvm_sdk::ServerInfo`: announcement metadata +- `CapabilityExclusion`: allowlist bypass rules for specific methods or capabilities + +## Typical workflows + +### Build a native server + +1. generate keys with `signer::generate()` or load an existing private key with `from_sk()` +2. configure `NostrServerTransportConfig` +3. create `NostrServerTransport` +4. attach it to an `rmcp` server with `ServiceExt` +5. optionally publish announcements with `announce()` + +### Build a native client + +1. generate keys with `signer::generate()` or load an existing private key with `from_sk()` +2. configure `NostrClientTransportConfig` +3. create `NostrClientTransport` +4. attach it to an `rmcp` client with `ServiceExt` + +### Bridge an existing server or client + +If you are not building a native `rmcp` service directly, use the wrapper layer: + +- `NostrMCPGateway` for server-side bridging +- `NostrMCPProxy` for client-side bridging + +### Discover servers + +1. create a `RelayPool` +2. query `discover_servers()` +3. fetch public tools, resources, and prompts with the discovery helpers + +## What is important in this implementation + +The Rust SDK already implements behavior that users should rely on: + +- stateless client initialization behavior +- announcement publication and deletion +- encryption negotiation and response mirroring +- rmcp conversion and routing flow + +Use the task-oriented pages in this directory for those details. Start with the native server and native client guides if you are building ContextVM applications directly. + +--- + +_This page was ported from the [ContextVM Rust SDK repository](https://github.com/ContextVM/rs-sdk/tree/main/docs/overview.md)._ diff --git a/src/content/docs/reference/rs-sdk/proxy.md b/src/content/docs/reference/rs-sdk/proxy.md new file mode 100644 index 0000000..ecb62a9 --- /dev/null +++ b/src/content/docs/reference/rs-sdk/proxy.md @@ -0,0 +1,121 @@ +--- +title: "Proxy" +description: "Communicate with a remote ContextVM server using NostrMCPProxy." +--- + +`NostrMCPProxy` is the simplest way to talk to a remote ContextVM server from Rust. + +It wraps `NostrClientTransport`, gives you a receiver for responses and notifications, and handles transport startup and shutdown. + +For native Rust applications, this is usually not the primary path. Most users should build an `rmcp` client and attach `NostrClientTransport` directly, as described in the native client guide. + +## When to use it + +Use the proxy when: + +- you already know the target server pubkey +- you want a lightweight request/response flow +- you do not need low-level transport hooks + +Do not start here if you are writing a new native Rust MCP client from scratch. + +## Loading an existing private key + +Like the native client transport, the proxy can reuse an existing Nostr identity instead of generating a new one. Load the signer with `from_sk()`: + +```rust +use contextvm_sdk::signer; + +let signer = signer::from_sk("")?; +``` + +Pass that signer to `NostrMCPProxy::new()` exactly as you would pass a freshly generated keypair. + +## Minimal example + +This follows the repository proxy example. + +```rust +use contextvm_sdk::core::types::{JsonRpcMessage, JsonRpcRequest}; +use contextvm_sdk::proxy::{NostrMCPProxy, ProxyConfig}; +use contextvm_sdk::signer; +use contextvm_sdk::transport::client::NostrClientTransportConfig; + +#[tokio::main] +async fn main() -> contextvm_sdk::Result<()> { + let keys = signer::from_sk("")?; + + let nostr_config = NostrClientTransportConfig::default() + .with_relay_urls(vec!["wss://relay.damus.io".to_string()]) + .with_server_pubkey(""); + + let config = ProxyConfig::new(nostr_config); + + let mut proxy = NostrMCPProxy::new(keys, config).await?; + let mut rx = proxy.start().await?; + + let request = JsonRpcMessage::Request(JsonRpcRequest { + jsonrpc: "2.0".to_string(), + id: serde_json::json!(1), + method: "tools/list".to_string(), + params: None, + }); + + proxy.send(&request).await?; + + if let Some(message) = rx.recv().await { + println!("{}", serde_json::to_string_pretty(&message)?); + } + + proxy.stop().await?; + Ok(()) +} +``` + +## Client config + +The main options live on `NostrClientTransportConfig`: + +- `relay_urls`: relays used for direct transport +- `server_pubkey`: target server identity in hex form +- `encryption_mode`: client encryption policy +- `gift_wrap_mode`: preferred gift-wrap kind policy +- `is_stateless`: emulate the initialize response locally +- `timeout`: pending request correlation retention + +## Stateless mode + +`is_stateless` is a major behavior switch. + +When enabled, the client can emulate initialize handling locally instead of waiting for a network roundtrip. This behavior is covered by the conformance tests. + +Use it when: + +- you want faster startup for short-lived clients +- you control the server behavior and know stateless operation is acceptable + +Avoid assuming that every server workflow depends only on stateless behavior. + +## Behavioral notes + +- responses are correlated at the transport level, not just by raw receive order +- the client learns peer capabilities from discovery tags on inbound messages +- encrypted traffic is deduplicated by outer gift-wrap event id before delivery + +## When not to use it + +Prefer the native client transport path when: + +- your application is already modeled as an `rmcp` `ClientHandler` +- you want the normal running-client workflow from `ServiceExt` +- you want examples that match the rest of the `rmcp` client ecosystem + +## rmcp path + +If you are building on `rmcp`, use the associated function `NostrMCPProxy::serve_client_handler()` instead of manually sending and receiving raw `JsonRpcMessage` values. + +That said, the preferred native architecture is still `rmcp` client first and ContextVM transport second. + +--- + +_This page was ported from the [ContextVM Rust SDK repository](https://github.com/ContextVM/rs-sdk/tree/main/docs/proxy.md)._ diff --git a/src/content/docs/reference/rs-sdk/rmcp.md b/src/content/docs/reference/rs-sdk/rmcp.md new file mode 100644 index 0000000..1cfb2eb --- /dev/null +++ b/src/content/docs/reference/rs-sdk/rmcp.md @@ -0,0 +1,51 @@ +--- +title: "RMCP Integration" +description: "Integrate ContextVM transports with the rmcp Rust MCP library." +--- + +For native Rust applications, `rmcp` is the main application layer and ContextVM is the transport layer. + +The Rust SDK exposes that integration behind the `rmcp` feature and re-exports the `rmcp` crate when that feature is enabled. The bridge lives in the SDK's rmcp transport layer. + +## Recommended mental model + +Use `rmcp` to define your server or client behavior, then attach ContextVM transports. + +That mirrors the transport-agnostic `rmcp` model, especially `ServiceExt` and the standard `handler.serve(transport)` pattern. + +The native entry points in this SDK are therefore: + +- `NostrServerTransport` for servers +- `NostrClientTransport` for clients + +For that workflow, use the native server and native client guides in this directory. + +## Server-side integration + +Use the associated function `NostrMCPGateway::serve_handler()` to serve an `rmcp` server handler directly over ContextVM. + +## Client-side integration + +Use the associated function `NostrMCPProxy::serve_client_handler()` to connect an `rmcp` client handler through the ContextVM client worker. + +## Why this still exists as a separate page + +The base SDK does not require `rmcp`. The core message model is represented by `JsonRpcMessage` and related internal JSON-RPC types. + +That separation keeps the transport usable as a lower-level protocol layer, but most application authors will want the `rmcp` path. + +The gateway and proxy APIs are convenience layers on top of this broader model, not the primary architecture for native apps. + +## Behavioral confidence + +The conversion pipeline is covered by the SDK test suite, which tests: + +- JSON-RPC parsing into internal message types +- internal-to-rmcp conversion +- rmcp-to-internal conversion +- request id preservation through the bridge +- event-id based routing assumptions used by the server worker + +--- + +_This page was ported from the [ContextVM Rust SDK repository](https://github.com/ContextVM/rs-sdk/tree/main/docs/rmcp.md)._ diff --git a/src/content/docs/reference/rs-sdk/server-transport.md b/src/content/docs/reference/rs-sdk/server-transport.md new file mode 100644 index 0000000..fbb0995 --- /dev/null +++ b/src/content/docs/reference/rs-sdk/server-transport.md @@ -0,0 +1,183 @@ +--- +title: "Native Server Guide" +description: "Learn how to build native ContextVM servers using NostrServerTransport." +--- + +The recommended architecture is: + +- define an `rmcp` server handler +- create a `NostrServerTransport` +- attach the transport to the handler with `rmcp`'s `ServiceExt` + +This is the same model used by the standard `rmcp` server examples, except the transport is Nostr instead of stdio. + +## The high-level shape + +In `rmcp`, a native server is normally started with `YourHandler.serve(transport)`. + +For ContextVM, the transport becomes `NostrServerTransport`. In the current SDK API, you pass that transport directly to `ServiceExt`; there is no extra adapter step in the public workflow. + +## Loading an existing private key + +If the server should run under a stable Nostr identity, load the signer from an existing private key with `from_sk()`: + +```rust +use contextvm_sdk::signer; + +let signer = signer::from_sk("")?; +println!("server pubkey: {}", signer.public_key().to_hex()); +``` + +This is the right choice for long-lived servers, announced servers, and deployments where clients must recognize the same public key across restarts. + +## Example + +```rust +use contextvm_sdk::transport::server::{ + NostrServerTransport, NostrServerTransportConfig, +}; +use contextvm_sdk::{signer, EncryptionMode, GiftWrapMode, ServerInfo}; +use rmcp::{ + ServerHandler, ServiceExt, + handler::server::{router::tool::ToolRouter, wrapper::Parameters}, + model::*, + schemars, tool, tool_handler, tool_router, +}; + +const RELAY_URL: &str = "wss://relay.contextvm.org"; + +#[derive(Clone)] +struct DemoServer { + tool_router: ToolRouter, +} + +impl DemoServer { + fn new() -> Self { + Self { + tool_router: Self::tool_router(), + } + } +} + +#[derive(Debug, serde::Deserialize, schemars::JsonSchema)] +struct EchoParams { + message: String, +} + +#[tool_router] +impl DemoServer { + #[tool(description = "Echo a message back unchanged")] + async fn echo( + &self, + Parameters(EchoParams { message }): Parameters, + ) -> Result { + Ok(CallToolResult::success(vec![Content::text(format!( + "Echo: {message}" + ))])) + } +} + +#[tool_handler] +impl ServerHandler for DemoServer { + fn get_info(&self) -> rmcp::model::ServerInfo { + rmcp::model::ServerInfo { + protocol_version: ProtocolVersion::LATEST, + capabilities: ServerCapabilities::builder().enable_tools().build(), + server_info: Implementation { + name: "contextvm-native-echo".to_string(), + title: Some("ContextVM Native Echo Server".to_string()), + version: "0.1.0".to_string(), + description: Some("Native rmcp echo server over ContextVM/Nostr".to_string()), + icons: None, + website_url: None, + }, + instructions: Some("Call the echo tool with a message string".to_string()), + } + } +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + tracing_subscriber::fmt() + .with_env_filter( + tracing_subscriber::EnvFilter::from_default_env() + .add_directive("contextvm_sdk=info".parse()?) + .add_directive("rmcp=warn".parse()?), + ) + .init(); + + let signer = signer::generate(); + let pubkey = signer.public_key().to_hex(); + + println!("Native ContextVM echo server starting"); + println!("Relay: {RELAY_URL}"); + println!("Server pubkey: {pubkey}"); + + let transport = NostrServerTransport::new( + signer, + NostrServerTransportConfig::default() + .with_relay_urls(vec![RELAY_URL.to_string()]) + .with_encryption_mode(EncryptionMode::Optional) + .with_gift_wrap_mode(GiftWrapMode::Optional) + .with_announced_server(false) + .with_server_info( + ServerInfo::default() + .with_name("contextvm-native-echo".to_string()) + .with_about("Native rmcp echo server example".to_string()), + ), + ) + .await?; + + let service = DemoServer::new().serve(transport).await?; + println!("Server ready. Press Ctrl+C to stop."); + service.waiting().await?; + Ok(()) +} +``` + +This follows the same flow as the repository's native echo server example. + +## What the transport adds + +`NostrServerTransport` is not just a byte stream adapter. It adds ContextVM-specific behavior on top of `rmcp` server semantics: + +- Nostr relay connectivity via `NostrServerTransport::new()` +- public announcements via `announce()` +- publication of tools, resources, prompts, and resource templates +- client authorization via `allowed_public_keys` in `NostrServerTransportConfig` +- capability exclusions via `CapabilityExclusion` +- encryption negotiation and response mirroring via `send_response()` +- session management and request routing inside the server event loop + +## Configuration fields that matter first + +Start with these fields in `NostrServerTransportConfig`: + +- `relay_urls`: relays the server will publish to and listen on +- `is_announced_server`: whether the server should participate in public discovery +- `encryption_mode`: plaintext vs encrypted policy +- `gift_wrap_mode`: persistent vs ephemeral wrapping policy +- `allowed_public_keys`: allowlist for private or restricted servers +- `excluded_capabilities`: allow specific methods without fully opening the server +- `relay_list_urls`: relay URLs advertised in kind 10002 (CEP-17); defaults to `relay_urls` +- `bootstrap_relay_urls`: additional relays for publishing announcements (CEP-6/17); merged with `relay_list_urls` +- `publish_relay_list`: whether to publish kind 10002 relay list metadata; default `true` +- `profile_metadata`: optional profile metadata for kind 0 publication (CEP-23) + +## When to use this instead of the gateway + +Use this page's approach when you are writing a new Rust MCP server. + +Use the gateway guide when you already have a request loop or existing local MCP service abstraction and want a thinner bridge. + +## Behavioral notes + +- The `rmcp` server handshake follows `ServiceExt::serve()` on the server handler. +- `rmcp` accepts pre-init ping and enters the main loop immediately after initialization completes. +- ContextVM response routing depends on request event ids. +- Encryption mirroring and announcement behavior are covered by the integration tests. +- When `is_announced_server` is `true`, the transport auto-publishes all announcement events on `start()` via synthetic MCP requests: kind 11316 (server announcement), kinds 11317-11320 (tools, resources, templates, prompts), kind 10002 (relay list), and kind 0 (profile metadata if configured). + +--- + +_This page was ported from the [ContextVM Rust SDK repository](https://github.com/ContextVM/rs-sdk/tree/main/docs/server-transport.md)._ diff --git a/src/content/docs/reference/rs-sdk/stateless.md b/src/content/docs/reference/rs-sdk/stateless.md new file mode 100644 index 0000000..1038b07 --- /dev/null +++ b/src/content/docs/reference/rs-sdk/stateless.md @@ -0,0 +1,109 @@ +--- +title: "Stateless Mode" +description: "Emulate initialization locally for faster startup using stateless client mode." +--- + +Stateless mode is a client-side transport behavior enabled through `NostrClientTransportConfig::with_stateless()`. + +It is designed for flows where the client should behave as if initialization succeeded without waiting for the server to answer over the network. + +## What stateless mode actually does + +When `is_stateless` is enabled on `NostrClientTransportConfig`, the client transport intercepts two parts of the normal MCP startup sequence inside `NostrClientTransport::send()`: + +- an outgoing `initialize` request +- an outgoing `notifications/initialized` notification + +For the `initialize` request, the transport locally emulates a successful initialize response instead of publishing the request to the relay network. + +For the `notifications/initialized` notification, the transport simply swallows the notification and does not send it over the network. + +## What does not change + +Stateless mode does not make the whole transport local-only. + +After initialization is emulated, normal requests are still serialized and sent through Nostr. + +That means stateless mode changes startup semantics, not the rest of the request/response transport model. + +## When to use it + +Use stateless mode when: + +- you want faster startup for short-lived clients +- you control both sides and do not need a server-provided initialize payload +- you are using the proxy or native client flow mainly for direct tool calls after startup + +Avoid it when: + +- you need the server's real initialize response +- your workflow depends on server-specific initialize metadata +- you want startup behavior to strictly follow the network exchange + +## Example + +```rust +use contextvm_sdk::transport::client::{ + NostrClientTransport, NostrClientTransportConfig, +}; +use contextvm_sdk::{signer, EncryptionMode, GiftWrapMode}; +use rmcp::{ClientHandler, ServiceExt}; + +#[derive(Clone, Default)] +struct DemoClient; + +impl ClientHandler for DemoClient {} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + let signer = signer::generate(); + + let transport = NostrClientTransport::new( + signer, + NostrClientTransportConfig::default() + .with_relay_urls(vec!["wss://relay.contextvm.org".to_string()]) + .with_server_pubkey("") + .with_encryption_mode(EncryptionMode::Optional) + .with_gift_wrap_mode(GiftWrapMode::Optional) + .with_stateless(true), + ) + .await?; + + let client = DemoClient.serve(transport).await?; + + // Initialize completed locally; subsequent requests still go over Nostr. + let tools = client.list_all_tools().await?; + println!("Discovered {} tool(s)", tools.len()); + + client.cancel().await?; + Ok(()) +} +``` + +## Emulated initialize shape + +The emulated initialize response includes: + +- `protocolVersion` +- `serverInfo` +- `capabilities` + +This is verified by the transport tests in the repository. + +The placeholder `serverInfo.name` used by the emulated response is currently `Emulated-Stateless-Server`. + +## Relationship to discovery and learned capabilities + +Stateless mode does not disable peer capability learning. + +The client still advertises its own capabilities through discovery tags, and it still learns server capabilities from inbound tags later in the session. + +So even in stateless mode, encryption and ephemeral gift-wrap support can still be learned from later inbound traffic. + +## Practical limitation + +Because the initialize roundtrip is skipped, stateless mode should be treated as an optimization for compatible workflows, not as a universal replacement for normal MCP startup. + +--- + +_This page was ported from the [ContextVM Rust SDK repository](https://github.com/ContextVM/rs-sdk/tree/main/docs/stateless.md)._ diff --git a/src/content/docs/reference/rs-sdk/transport-modes.md b/src/content/docs/reference/rs-sdk/transport-modes.md new file mode 100644 index 0000000..8794975 --- /dev/null +++ b/src/content/docs/reference/rs-sdk/transport-modes.md @@ -0,0 +1,123 @@ +--- +title: "Transport Modes" +description: "Understand the different transport connection modes available in ContextVM." +--- + +This page focuses on the transport behavior switches that are spread across the SDK APIs: + +- `EncryptionMode` +- `GiftWrapMode` +- stateless mode via `NostrClientTransportConfig::with_stateless()` + +The existing guides mention these knobs in context. This page collects them into one operational reference. + +## Encryption modes + +`EncryptionMode` controls whether the transport accepts or emits plaintext vs encrypted direct traffic. + +The three modes are: + +- `Optional`: mirror mode; encrypted traffic is allowed and plaintext is also allowed +- `Required`: reject plaintext direct traffic and require encrypted direct traffic +- `Disabled`: reject encrypted direct traffic and use plaintext only + +The exact enforcement is implemented in the client and server transport layers. + +### What `Optional` really means + +`Optional` does not mean "always plaintext is fine" or "always encrypt everything". + +At the base transport level, the outgoing encryption choice for direct traffic is mirror-oriented: + +- `EncryptionMode::Required` always encrypts direct traffic +- `EncryptionMode::Disabled` never encrypts direct traffic +- `EncryptionMode::Optional` uses the known encryption state of the peer interaction when available + +## Gift-wrap modes + +`GiftWrapMode` only matters when traffic is encrypted. + +The three modes are: + +- `Optional`: accept both persistent and ephemeral gift wraps; prefer persistent until ephemeral support is known +- `Ephemeral`: require kind `21059` +- `Persistent`: require kind `1059` + +The acceptance rules are defined by `GiftWrapMode::allows_kind()`, and whether a mode can advertise or choose ephemeral wrapping is defined by `GiftWrapMode::supports_ephemeral()`. + +### Client outbound behavior + +Client selection follows this behavior: + +- `Persistent` always uses persistent gift wrap +- `Ephemeral` always uses ephemeral gift wrap +- `Optional` uses persistent first, then switches to ephemeral once peer support is learned + +### Server outbound behavior + +Server response and notification selection follow the current transport implementation. + +Important server rules: + +- if a correlated encrypted request used a valid wrap kind, the server mirrors it when possible +- for notifications, learned client support for ephemeral gift wrap can change the fallback behavior +- `Optional` still falls back to persistent when no stronger signal exists + +## Stateless mode + +Stateless mode is client-only and is controlled by `NostrClientTransportConfig::with_stateless()`. + +It changes initialization behavior only: + +- outgoing `initialize` is emulated locally +- outgoing `notifications/initialized` is suppressed +- later requests still go through the normal Nostr transport path + +See the dedicated stateless mode guide in the Rust SDK reference section. + +## Capability tags and mode signaling + +Mode choices affect discovery and capability tags. + +### Client-side signaling + +Client tags follow this behavior: + +- if encryption is not disabled, the client advertises encryption support +- if gift-wrap mode is not persistent, the client also advertises ephemeral gift-wrap support + +### Server-side signaling + +Server announcement tags follow this behavior: + +- if encryption is not disabled, the server advertises encryption support +- if gift-wrap mode supports ephemeral wrapping, the server also advertises ephemeral support + +This is why `GiftWrapMode::Optional` and `GiftWrapMode::Ephemeral` both advertise ephemeral support, while `GiftWrapMode::Persistent` does not. + +## Example configurations + +```rust +use contextvm_sdk::{EncryptionMode, GiftWrapMode}; +use contextvm_sdk::transport::client::NostrClientTransportConfig; + +let interoperable = NostrClientTransportConfig::default() + .with_server_pubkey("") + .with_encryption_mode(EncryptionMode::Optional) + .with_gift_wrap_mode(GiftWrapMode::Optional); + +let strict_encrypted = NostrClientTransportConfig::default() + .with_server_pubkey("") + .with_encryption_mode(EncryptionMode::Required) + .with_gift_wrap_mode(GiftWrapMode::Persistent); + +let stateless_client = NostrClientTransportConfig::default() + .with_server_pubkey("") + .with_encryption_mode(EncryptionMode::Optional) + .with_gift_wrap_mode(GiftWrapMode::Optional) + .with_stateless(true); +``` + +--- + +_This page was ported from the [ContextVM Rust SDK repository](https://github.com/ContextVM/rs-sdk/tree/main/docs/transport-modes.md)._ diff --git a/src/content/docs/reference/rs-sdk/transports.md b/src/content/docs/reference/rs-sdk/transports.md new file mode 100644 index 0000000..b138fae --- /dev/null +++ b/src/content/docs/reference/rs-sdk/transports.md @@ -0,0 +1,144 @@ +--- +title: "Transports (Low-Level)" +description: "Low-level details on the core transport implementations in the Rust SDK." +--- + +Use this page when you want to understand the transport layer itself. + +If you are building a normal native server or client, start with the dedicated native server or native client guide first. + +- `NostrClientTransport`: client-side direct transport +- `NostrServerTransport`: server-side direct transport + +## Why use the transport layer directly + +Use transports directly when you need to: + +- integrate with your own request loop +- control announcement timing yourself +- tune authorization and session behavior +- embed the transport in higher-level abstractions + +## Signer choice + +All transport constructors accept an existing signer, not just a newly generated one. + +- Use `generate()` for temporary identities in examples, tests, and short-lived sessions. +- Use `from_sk()` when you need a stable identity backed by a pre-existing hex or `nsec` private key. + +```rust +use contextvm_sdk::signer; + +let signer = signer::from_sk("")?; +``` + +## Low-level client transport example + +```rust +use contextvm_sdk::core::types::{JsonRpcMessage, JsonRpcRequest}; +use contextvm_sdk::signer; +use contextvm_sdk::transport::client::{ + NostrClientTransport, NostrClientTransportConfig, +}; + +#[tokio::main] +async fn main() -> contextvm_sdk::Result<()> { + let keys = signer::generate(); + let mut transport = NostrClientTransport::new( + keys, + NostrClientTransportConfig::default() + .with_relay_urls(vec!["wss://relay.damus.io".to_string()]) + .with_server_pubkey(""), + ) + .await?; + + transport.start().await?; + let mut rx = transport.take_message_receiver().expect("receiver available"); + + transport + .send(&JsonRpcMessage::Request(JsonRpcRequest { + jsonrpc: "2.0".to_string(), + id: serde_json::json!(1), + method: "tools/list".to_string(), + params: None, + })) + .await?; + + if let Some(message) = rx.recv().await { + println!("received: {:?}", message); + } + + transport.close().await?; + Ok(()) +} +``` + +## Low-level server transport example + +```rust +use contextvm_sdk::core::types::{JsonRpcMessage, JsonRpcResponse}; +use contextvm_sdk::signer; +use contextvm_sdk::transport::server::{ + NostrServerTransport, NostrServerTransportConfig, +}; + +#[tokio::main] +async fn main() -> contextvm_sdk::Result<()> { + let keys = signer::generate(); + let mut transport = NostrServerTransport::new( + keys, + NostrServerTransportConfig::default().with_announced_server(true), + ) + .await?; + + transport.start().await?; + let mut rx = transport.take_message_receiver().expect("receiver available"); + + while let Some(req) = rx.recv().await { + if let Some(id) = req.message.id() { + transport + .send_response( + &req.event_id, + JsonRpcMessage::Response(JsonRpcResponse { + jsonrpc: "2.0".to_string(), + id: id.clone(), + result: serde_json::json!({"ok": true}), + }), + ) + .await?; + } + } + + Ok(()) +} +``` + +## Server-side semantics to understand + +The server transport does more than relay bytes. + +It manages: + +- multi-client session state +- request route storage +- authorization via `allowed_public_keys` +- allowlist bypasses via `CapabilityExclusion` +- announcement publication +- encryption negotiation and response mirroring + +Those behaviors are part of the server transport implementation and are exercised heavily by the integration tests. + +## What the server receives + +Incoming traffic is delivered as `IncomingRequest`, which includes: + +- the parsed `JsonRpcMessage` +- the client pubkey +- the original Nostr request event id +- whether the incoming message was encrypted + +That extra metadata is what allows correct response routing and encryption mirroring. + +--- + +_This page was ported from the [ContextVM Rust SDK repository](https://github.com/ContextVM/rs-sdk/tree/main/docs/transports.md)._ diff --git a/src/content/docs/spec/cep-guidelines.md b/src/content/docs/reference/spec/cep-guidelines.md similarity index 100% rename from src/content/docs/spec/cep-guidelines.md rename to src/content/docs/reference/spec/cep-guidelines.md diff --git a/src/content/docs/spec/ctxvm-draft-spec.md b/src/content/docs/reference/spec/ctxvm-draft-spec.md similarity index 99% rename from src/content/docs/spec/ctxvm-draft-spec.md rename to src/content/docs/reference/spec/ctxvm-draft-spec.md index 187885e..3b6d140 100644 --- a/src/content/docs/spec/ctxvm-draft-spec.md +++ b/src/content/docs/reference/spec/ctxvm-draft-spec.md @@ -322,8 +322,8 @@ The following CEPs have been accepted: The following CEPs have been finalized: -- [CEP-4: Encryption Support](/spec/ceps/cep-4) -- [CEP-6: Public Server Announcements](/spec/ceps/cep-6) +- [CEP-4: Encryption Support](/reference/ceps/cep-4) +- [CEP-6: Public Server Announcements](/reference/ceps/cep-6) ## Complete Protocol Flow diff --git a/src/content/docs/ts-sdk/core/common-tool-schemas.md b/src/content/docs/reference/ts-sdk/core/common-tool-schemas.md similarity index 70% rename from src/content/docs/ts-sdk/core/common-tool-schemas.md rename to src/content/docs/reference/ts-sdk/core/common-tool-schemas.md index adae1fa..8acab5a 100644 --- a/src/content/docs/ts-sdk/core/common-tool-schemas.md +++ b/src/content/docs/reference/ts-sdk/core/common-tool-schemas.md @@ -5,7 +5,7 @@ description: Compute and publish CEP-15 common tool schema hashes with the @cont # Common Tool Schemas -The `@contextvm/sdk` includes helpers for [CEP-15](/spec/ceps/cep-15) common tool schemas. +The `@contextvm/sdk` includes helpers for [CEP-15](/reference/ceps/cep-15) common tool schemas. For most server integrations, prefer `withCommonToolSchemas()`. It decorates `NostrServerTransport` and publishes the required metadata automatically. @@ -16,40 +16,42 @@ Use the lower-level utilities on this page when you need to precompute hashes, v The usual integration point is `withCommonToolSchemas()`: ```typescript -import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { NostrServerTransport, PrivateKeySigner, withCommonToolSchemas, -} from '@contextvm/sdk'; +} from "@contextvm/sdk"; const server = new McpServer({ - name: 'translation-server', - version: '1.0.0', + name: "translation-server", + version: "1.0.0", }); server.registerTool( - 'translate_text', + "translate_text", { - description: 'Translate text between languages.', + description: "Translate text between languages.", inputSchema: { - type: 'object', + type: "object", properties: { - text: { type: 'string' }, - target_language: { type: 'string' }, + text: { type: "string" }, + target_language: { type: "string" }, }, - required: ['text', 'target_language'], + required: ["text", "target_language"], }, outputSchema: { - type: 'object', + type: "object", properties: { - translated_text: { type: 'string' }, + translated_text: { type: "string" }, }, - required: ['translated_text'], + required: ["translated_text"], }, }, async ({ text, target_language }) => ({ - content: [{ type: 'text', text: `Translated to ${target_language}: ${text}` }], + content: [ + { type: "text", text: `Translated to ${target_language}: ${text}` }, + ], structuredContent: { translated_text: `Translated to ${target_language}: ${text}`, }, @@ -58,13 +60,13 @@ server.registerTool( const transport = withCommonToolSchemas( new NostrServerTransport({ - signer: new PrivateKeySigner('your-server-private-key'), - relayHandler: ['wss://relay.damus.io'], + signer: new PrivateKeySigner("your-server-private-key"), + relayHandler: ["wss://relay.damus.io"], isAnnouncedServer: true, }), { - tools: [{ name: 'translate_text' }], - categories: ['translation', 'language-tools'], + tools: [{ name: "translate_text" }], + categories: ["translation", "language-tools"], }, ); @@ -80,7 +82,7 @@ This is the recommended path because the SDK will automatically: `categories` are lightweight discoverability hints for announced `tools/list` events. The SDK trims whitespace, removes empty values, and deduplicates repeated categories while preserving the original order of the remaining entries. -For more transport-specific context, see [Nostr Server Transport](/ts-sdk/transports/nostr-server-transport#cep-15-common-tool-schemas). +For more transport-specific context, see [Nostr Server Transport](/reference/ts-sdk/transports/nostr-server-transport#cep-15-common-tool-schemas). ## Exports @@ -110,24 +112,24 @@ The resulting normalized schema is then suitable for deterministic hashing. ## Computing a schema hash ```typescript -import { computeCommonSchemaHash } from '@contextvm/sdk'; +import { computeCommonSchemaHash } from "@contextvm/sdk"; const schemaHash = computeCommonSchemaHash({ - name: 'translate_text', + name: "translate_text", inputSchema: { - type: 'object', + type: "object", properties: { - text: { type: 'string', description: 'Input text' }, - target_language: { type: 'string', title: 'Target language' }, + text: { type: "string", description: "Input text" }, + target_language: { type: "string", title: "Target language" }, }, - required: ['text', 'target_language'], + required: ["text", "target_language"], }, outputSchema: { - type: 'object', + type: "object", properties: { - translated_text: { type: 'string' }, + translated_text: { type: "string" }, }, - required: ['translated_text'], + required: ["translated_text"], }, }); ``` @@ -146,8 +148,8 @@ If you need to verify a schema hash yourself, compute it from the tool definitio import { COMMON_SCHEMA_META_NAMESPACE, computeCommonSchemaHash, -} from '@contextvm/sdk'; -import type { Tool } from '@modelcontextprotocol/sdk/types.js'; +} from "@contextvm/sdk"; +import type { Tool } from "@modelcontextprotocol/sdk/types.js"; function hasMatchingSchemaHash(tool: Tool): boolean { const expectedHash = computeCommonSchemaHash({ @@ -173,4 +175,4 @@ This is mainly useful for tests, debugging, and custom verification flows. ## Next Steps -For the recommended server-side integration, see [Nostr Server Transport](/ts-sdk/transports/nostr-server-transport#cep-15-common-tool-schemas). +For the recommended server-side integration, see [Nostr Server Transport](/reference/ts-sdk/transports/nostr-server-transport#cep-15-common-tool-schemas). diff --git a/src/content/docs/ts-sdk/core/constants.md b/src/content/docs/reference/ts-sdk/core/constants.md similarity index 96% rename from src/content/docs/ts-sdk/core/constants.md rename to src/content/docs/reference/ts-sdk/core/constants.md index fede072..49212e9 100644 --- a/src/content/docs/ts-sdk/core/constants.md +++ b/src/content/docs/reference/ts-sdk/core/constants.md @@ -44,11 +44,11 @@ The `announcementMethods` object maps capability types to their corresponding MC ```typescript export const announcementMethods = { - server: 'initialize', - tools: 'tools/list', - resources: 'resources/list', - resourceTemplates: 'resources/templates/list', - prompts: 'prompts/list', + server: "initialize", + tools: "tools/list", + resources: "resources/list", + resourceTemplates: "resources/templates/list", + prompts: "prompts/list", } as const; ``` diff --git a/src/content/docs/ts-sdk/core/encryption.md b/src/content/docs/reference/ts-sdk/core/encryption.md similarity index 97% rename from src/content/docs/ts-sdk/core/encryption.md rename to src/content/docs/reference/ts-sdk/core/encryption.md index b4fd466..b65d4e5 100644 --- a/src/content/docs/ts-sdk/core/encryption.md +++ b/src/content/docs/reference/ts-sdk/core/encryption.md @@ -36,8 +36,8 @@ The standard implementation of NIP-17 is designed for persistent private message Encryption is configured at the transport level using the `EncryptionMode` enum. You can set the desired mode when creating a `NostrClientTransport` or `NostrServerTransport`. ```typescript -import { NostrClientTransport } from '@contextvm/sdk'; -import { EncryptionMode } from '@contextvm/sdk'; +import { NostrClientTransport } from "@contextvm/sdk"; +import { EncryptionMode } from "@contextvm/sdk"; const transport = new NostrClientTransport({ // ... other options diff --git a/src/content/docs/ts-sdk/core/interfaces.md b/src/content/docs/reference/ts-sdk/core/interfaces.md similarity index 98% rename from src/content/docs/ts-sdk/core/interfaces.md rename to src/content/docs/reference/ts-sdk/core/interfaces.md index ca3bcae..6eef392 100644 --- a/src/content/docs/ts-sdk/core/interfaces.md +++ b/src/content/docs/reference/ts-sdk/core/interfaces.md @@ -74,9 +74,9 @@ The `EncryptionMode` enum defines the encryption policy for a transport. ```typescript export enum EncryptionMode { - OPTIONAL = 'optional', - REQUIRED = 'required', - DISABLED = 'disabled', + OPTIONAL = "optional", + REQUIRED = "required", + DISABLED = "disabled", } ``` diff --git a/src/content/docs/ts-sdk/core/logging.md b/src/content/docs/reference/ts-sdk/core/logging.md similarity index 79% rename from src/content/docs/ts-sdk/core/logging.md rename to src/content/docs/reference/ts-sdk/core/logging.md index ea7dbca..cead3ff 100644 --- a/src/content/docs/ts-sdk/core/logging.md +++ b/src/content/docs/reference/ts-sdk/core/logging.md @@ -12,13 +12,13 @@ The SDK uses Pino for high-performance logging with structured JSON output. By d [`typescript`](src/content/docs/ts-sdk/core/logging.md:10) ```typescript -import { createLogger } from '@contextvm/sdk/core'; +import { createLogger } from "@contextvm/sdk/core"; // Create a logger for your module -const logger = createLogger('my-module'); +const logger = createLogger("my-module"); -logger.info('Application started'); -logger.error('An error occurred', { error: 'details' }); +logger.info("Application started"); +logger.error("An error occurred", { error: "details" }); ``` #### Configuration Options @@ -26,14 +26,14 @@ logger.error('An error occurred', { error: 'details' }); [`typescript`](src/content/docs/ts-sdk/core/logging.md:20) ```typescript -import { createLogger, LoggerConfig } from '@contextvm/sdk/core'; +import { createLogger, LoggerConfig } from "@contextvm/sdk/core"; const config: LoggerConfig = { - level: 'debug', // Minimum log level (debug, info, warn, error) - file: 'app.log', // Optional: log to a file instead of stderr + level: "debug", // Minimum log level (debug, info, warn, error) + file: "app.log", // Optional: log to a file instead of stderr }; -const logger = createLogger('my-module', config); +const logger = createLogger("my-module", config); ``` **Note:** Pretty printing is automatically enabled when logs are written to stderr/stdout (not to a file) for better readability during development. @@ -72,11 +72,11 @@ LOG_ENABLED=false node app.js ```javascript // Set this in a