# MCP Publisher Guide (/docs/publisher-guide/mcp)



This guide covers everything you need to build, deploy, and publish an MCP server on Voidnet Console — from server implementation to Stripe payout configuration.

***

## Overview [#overview]

A **publisher** is a developer who builds and hosts an MCP server and makes it available to buyers through the Voidnet Console marketplace. The Voidnet proxies buyer requests to your server, enforces rate limits and metering, and handles billing. Your server only needs to implement the standard MCP protocol and validate the publisher API key.

```
Your Apps / AI (Key or OAuth Token)  →  Voidnet  →  Your MCP Server
                                                ↑
                                          Publisher API Key
                                          (always vnp-sk-*)
```

**Buyers authenticate to the Voidnet** using either an API key (`vnb-sk-*`) or an OAuth 2.0 access token (JWT). The Voidnet validates their identity, then proxies the request to your server with your publisher API key (`vnp-sk-*`) in the `Authorization` header. Your server never sees the buyer's credentials — only the publisher key you already trust.

***

## Publisher API Key [#publisher-api-key]

Every published app gets a publisher API key. This key is generated in the [Voidnet Console](https://openvoidnet.com/console) when you publish an app and is **shown only once** — save it securely.

```
vnp-sk-27fc6268dc64a9ba2c4cb92489e9175c9bf404e260beb268b976f1a56582eff3
```

### How it's used [#how-its-used]

When the Voidnet proxies a request to your server, it sends the key in the `Authorization` header:

```
Authorization: Bearer vnp-sk-27fc6268...
```

**Your server must validate this header** on every request and reject invalid or missing keys with a `401` response.

### Key lifecycle [#key-lifecycle]

* **Generate** — Created when you publish an app or manually from the Console
* **Revoke** — You can revoke keys at any time; revoked keys immediately stop working
* **Rotate** — Generate a new key, update your server, then revoke the old one
* **Storage** — Keys are encrypted at rest in the Voidnet database

***

## Server Requirements [#server-requirements]

Your MCP server must meet these requirements to work with Voidnet Console:

### 1. HTTPS [#1-https]

All communication with the Voidnet is over HTTPS. You'll need a valid TLS certificate.

### 2. Single MCP endpoint [#2-single-mcp-endpoint]

The Voidnet sends all MCP requests to a single endpoint on your server — typically `/mcp`. This single endpoint handles all MCP methods.

### 3. Protocol version [#3-protocol-version]

Voidnet Console requires &#x2A;*MCP protocol version `2026-07-28`** (stateless). Your server should:

* Accept the `MCP-Protocol-Version: 2026-07-28` header
* Include `2026-07-28` in the `supportedVersions` array of the `server/discover` response
* Reject unsupported versions with `400` and error code `-32022`

**No sessions, no initialize handshake, no GET/DELETE endpoints.** The legacy 2025-11-25 stateful model is not supported.

### 4. Authentication [#4-authentication]

Your server must validate the `Authorization: Bearer <key>` header and return `401` for invalid or missing keys. See [Authentication](#authentication) below.

### 5. Required headers [#5-required-headers]

The Voidnet forwards these headers on every request:

| Header                 | Description                                  |
| ---------------------- | -------------------------------------------- |
| `MCP-Protocol-Version` | `2026-07-28` (gateway's protocol version)    |
| `Mcp-Method`           | JSON-RPC method name (e.g., `tools/call`)    |
| `Mcp-Name`             | Tool/resource/prompt name for routing        |
| `Authorization`        | `Bearer vnp-sk-...` (your publisher API key) |

Your server must **validate** that `MCP-Protocol-Version` matches the `_meta.protocolVersion` in the JSON-RPC request body. Mismatch returns `400` with error code `-32020` (HeaderMismatch).

***

## Authentication [#authentication]

The authentication contract between the Voidnet and your server is:

```
Request:
  Authorization: Bearer vnp-sk-27fc6268...

Response (valid key):
  200 OK

Response (invalid or missing key):
  401 Unauthorized
  {
    "jsonrpc": "2.0",
    "error": {
      "code": -32001,
      "message": "Unauthorized",
      "data": "Invalid or missing publisher API key"
    }
  }
```

Validate on **every request**. If the key is invalid, return `401` immediately — do not process the MCP request.

***

## MCP Protocol (stateless 2026-07-28) [#mcp-protocol-stateless-2026-07-28]

Your server must implement the following MCP 2026-07-28 methods. &#x2A;*No initialize handshake, no sessions, no `MCP-Session-Id` headers.**

### server/discover (REQUIRED) [#serverdiscover-required]

The Voidnet calls `server/discover` to discover your server's capabilities. This replaces the legacy `initialize` handshake.

**Request:**

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "server/discover"
}
```

**Response:**

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "supportedVersions": ["2026-07-28"],
    "capabilities": {
      "tools": { "listChanged": true },
      "resources": { "subscribe": false, "listChanged": true },
      "prompts": { "listChanged": true }
    },
    "serverInfo": { "name": "my-server", "version": "1.0.0" },
    "instructions": "Optional usage instructions for your server"
  }
}
```

* `supportedVersions`: Must include `2026-07-28`
* `capabilities`: Your server's capabilities (tools, resources, prompts)
* `serverInfo`: Your server's name and version
* `instructions`: Optional human-readable instructions

### tools/list [#toolslist]

Returns the tools your server provides:

```json
{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/list"
}
```

**Response** (include `ttlMs` and `cacheScope` for spec-sanctioned caching):

```json
{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "tools": [
      {
        "name": "my_tool",
        "description": "Does something useful",
        "inputSchema": {
          "type": "object",
          "properties": {
            "input": { "type": "string", "description": "The input value" }
          },
          "required": ["input"]
        }
      }
    ],
    "ttlMs": 300000,
    "cacheScope": "private"
  }
}
```

### tools/call [#toolscall]

Invokes a tool with the provided arguments:

```json
{
  "jsonrpc": "2.0",
  "id": 3,
  "method": "tools/call",
  "params": {
    "name": "my_tool",
    "arguments": { "input": "hello" }
  }
}
```

**Response** (include `resultType` — `complete` or `input_required` for MRTR):

```json
{
  "jsonrpc": "2.0",
  "id": 3,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "Result: hello"
      }
    ],
    "resultType": "complete"
  }
}
```

### resources/list (optional) [#resourceslist-optional]

```json
{
  "jsonrpc": "2.0",
  "id": 4,
  "method": "resources/list"
}
```

**Response:**

```json
{
  "jsonrpc": "2.0",
  "id": 4,
  "result": {
    "resources": [
      {
        "uri": "data://my-resource",
        "name": "My Resource",
        "description": "A data resource",
        "mimeType": "text/plain"
      }
    ],
    "ttlMs": 300000,
    "cacheScope": "private"
  }
}
```

### resources/read (optional) [#resourcesread-optional]

```json
{
  "jsonrpc": "2.0",
  "id": 5,
  "method": "resources/read",
  "params": { "uri": "data://my-resource" }
}
```

**Response:**

```json
{
  "jsonrpc": "2.0",
  "id": 5,
  "result": {
    "uri": "data://my-resource",
    "mimeType": "text/plain",
    "text": "Resource content here",
    "ttlMs": 300000,
    "cacheScope": "private"
  }
}
```

### prompts/list (optional) [#promptslist-optional]

```json
{
  "jsonrpc": "2.0",
  "id": 6,
  "method": "prompts/list"
}
```

### prompts/get (optional) [#promptsget-optional]

```json
{
  "jsonrpc": "2.0",
  "id": 7,
  "method": "prompts/get",
  "params": { "name": "system_instruction", "arguments": { "task": "write a summary" } }
}
```

### subscriptions/listen (optional) [#subscriptionslisten-optional]

For servers that support change notifications. The Voidnet calls this once per subscription:

```json
{
  "jsonrpc": "2.0",
  "id": 8,
  "method": "subscriptions/listen",
  "params": { "notifications": [] }
}
```

Acknowledge with `notifications/subscriptions/acknowledged`.

***

## Server Verification [#server-verification]

When you publish an app in the Console, Voidnet performs a **live verification** of your server:

1. The Voidnet calls `server/discover` to discover your server's capabilities
2. It calls `tools/list`, `resources/list`, and `prompts/list`
3. The discovered capabilities are cached and displayed in the Console

Your server must implement `server/discover` and the list methods above. Verification determines which tools, resources, and prompts buyers will see.

You can re-verify your server at any time from the app detail page — click **Refresh** in the MCP Configuration section.

***

## Domain Verification [#domain-verification]

Before you can publish an app, you must verify ownership of your server's domain:

1. **Initiate** — In the Console, start domain verification. A unique verification token is generated.

2. **Place the file** — Host the verification file at your domain's root or `/.well-known/` directory:

   ```
   https://your-domain.com/voidnet-site-verification-<token>.html
   ```

3. **Verify** — Voidnet Console fetches the file and confirms the content matches the expected token.

4. **Done** — The domain is marked as verified and can be used for publishing.

One verified domain can host multiple apps.

***

## Tier Configuration [#tier-configuration]

Every published app has configurable tiers:

### Free Tier [#free-tier]

* **Meter limit** — Max requests per month before the free tier is exhausted
* **Rate limit per minute** — Max requests per minute
* **Rate limit per day** — Max requests per day

### Paid Tier [#paid-tier]

* Same limits as free tier but higher
* **Price** — Set in USD with monthly, yearly, or one-time billing
* Requires a connected Stripe account (see below)

### How tiers work [#how-tiers-work]

1. A buyer discovers your app in the marketplace
2. If you offer a free tier, the buyer can start using it immediately
3. If you offer a paid tier, the buyer purchases a subscription
4. The Voidnet enforces rate limits and metering based on the buyer's tier
5. When a free tier buyer exhausts their meter, they're prompted to upgrade

***

## Stripe Connect Billing [#stripe-connect-billing]

To accept payments, you need a Stripe Express account connected to Voidnet Console:

1. **Connect** — In the Console Payments page, click "Connect with Stripe"
2. **Onboard** — Complete Stripe Express onboarding (identity verification, bank account)
3. **Sync** — Your Stripe account status appears in the Console

### Payout structure [#payout-structure]

| Component                    | Share         |
| ---------------------------- | ------------- |
| Publisher payout             | 80% (default) |
| Voidnet Console platform fee | 20% (default) |

Per-app — the default 80/20 can be lowered on individual apps (negotiated deals, set by Voidnet on request). The rate is locked at publish time; each purchase splits from that locked rate. Payouts are sent directly by Stripe to your connected bank account on Stripe's standard payout schedule.

### Pricing model [#pricing-model]

* The Voidnet uses the **Marketplace / Direct Charges** model
* You are the merchant of record for each transaction
* The Voidnet creates a PaymentIntent with an `application_fee` for Voidnet Console's per-app fee (default 20%)
* You receive 80% (default) — rounded: `fee = floor(amount*percent/100)`, `payout = amount - fee` so the two always sum to gross exactly (platform absorbs the cent). Stripe processing fees are separate and deducted from the gross before the split.

***

## Testing Your Server [#testing-your-server]

### 1. Local development [#1-local-development]

Run your MCP server locally with HTTPS (self-signed certs are fine for testing). The Console's verify-server tool supports localhost with `rejectUnauthorized: false` for external publishers. Internally, Voidnet engineers use `http` on localhost — external publishers must use `https`.

### 2. Verify server in Console [#2-verify-server-in-console]

From the **Publish MCP Interface** page:

1. Enter your **App Name** (unique per publisher)
2. Select a **verified domain** — the server URL is auto-populated from your domain
3. Add a **description**
4. Click **Verify Server**
5. The Console calls `server/discover` → `tools/list` → `resources/list` → `prompts/list` and displays your capabilities
6. **Save your publisher API key** — shown once in the success dialog
7. Configure tiers and click **Submit** to publish

### 3. Test with curl [#3-test-with-curl]

```bash
curl -X POST https://api.openvoidnet.com/v1-beta/mcp/your-username/your-app \
  -H "Authorization: Bearer vnb-sk-your-buyer-key" \
  -H "Content-Type: application/json" \
  -H "MCP-Protocol-Version: 2026-07-28" \
  -H "Mcp-Method: tools/list" \
  -H "Mcp-Name: tools/list" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/list"
  }'
```

***

## Going Live [#going-live]

### Prerequisites checklist [#prerequisites-checklist]

* [ ] Server is running HTTPS with a valid certificate
* [ ] Server validates the `Authorization: Bearer <key>` header
* [ ] Server implements MCP `2026-07-28` protocol (stateless)
* [ ] Server implements `server/discover` (required)
* [ ] Server exposes at least one tool
* [ ] Domain is verified
* [ ] Stripe Express account is connected (if offering paid tier)

### Publishing flow [#publishing-flow]

1. Go to **Console → Publish Apps** and select &#x2A;*AI Tools (MCP)**
2. Enter your **App Name** (unique per publisher, like a GitHub repo name)
3. Select a **verified domain** to set the server URL
4. Add a **description**
5. Click **Verify Server** — the Voidnet probes your MCP server with `server/discover` and discovers its capabilities
6. Review the detected **tools, resources, and prompts**
7. **Save your publisher API key** (`vnp-sk-*`) — it's shown only once in the success dialog. Without it, the Voidnet cannot proxy requests to your server
8. Close the dialog and configure **tiers** — free, paid, or both, with rate limits and metering limits
9. Click **Submit** — your app is created as a draft and immediately published to the marketplace
10. Your app now appears in **Published Apps** and the [Voidnet Console Marketplace](https://openvoidnet.com/marketplace)

### Drafts [#drafts]

Every app starts as a **draft** when you first verify the server. Drafts appear in the **Publish Apps** page under "Continue your drafts" and can be resumed at any time. Publishing (Submitting) sets the status to `published`, making the app visible in the marketplace.

| Status      | Visible in marketplace | Modifiable                                 |
| ----------- | ---------------------- | ------------------------------------------ |
| `draft`     | No                     | Yes — can edit tiers, re-verify, or delete |
| `published` | Yes                    | Yes — can update, re-verify, or delete     |

### Managing your app [#managing-your-app]

* **View details** — Console → Published Apps → click your app
* **API keys** — Generate and revoke keys from the API Keys tab on the app detail page
* **Logo** — Upload or remove an app logo from the app detail page (400×400 WebP, PNG, JPEG, or AVIF)
* **Re-verify** — Refresh server capabilities from the Overview tab on the app detail page
* **Analytics** — Track requests and revenue per app
* **Delete** — Remove your app (irreversible) from either the Published Apps list or Publish Apps page

***

## Reference: Server skeleton [#reference-server-skeleton]

A minimal MCP server in TypeScript/Node.js (stateless 2026-07-28):

```typescript
import { createServer } from "https";
import { readFileSync } from "fs";

const PUBLISHER_API_KEY = process.env.PUBLISHER_API_KEY;
const PORT = Number(process.env.PORT ?? 3051);

// Read and parse the JSON-RPC request body (1 MB cap).
function parseBody(req: import("http").IncomingMessage): Promise<any> {
  return new Promise((resolve, reject) => {
    const chunks: Buffer[] = [];
    let size = 0;
    req.on("data", (chunk: Buffer) => {
      size += chunk.length;
      if (size > 1024 * 1024) {
        reject(new Error("Request body too large"));
        req.destroy();
        return;
      }
      chunks.push(chunk);
    });
    req.on("end", () => {
      try {
        resolve(JSON.parse(Buffer.concat(chunks).toString("utf8")));
      } catch (err) {
        reject(err);
      }
    });
    req.on("error", reject);
  });
}

const server = createServer(
  { key: readFileSync("./key.pem"), cert: readFileSync("./cert.pem") },
  async (req, res) => {
  if (req.method === "GET" || req.method === "DELETE") {
    res.writeHead(405, { "Content-Type": "application/json" });
    res.end(JSON.stringify({ jsonrpc: "2.0", error: { code: -32601, message: "Method not allowed" } }));
    return;
  }

  // 1. Validate publisher API key
  const expected = `Bearer ${PUBLISHER_API_KEY}`;
  if (PUBLISHER_API_KEY && req.headers["authorization"] !== expected) {
    res.writeHead(401, { "Content-Type": "application/json" });
    res.end(JSON.stringify({ jsonrpc: "2.0", error: { code: -32001, message: "Unauthorized" } }));
    return;
  }

  // 2. Validate MCP-Protocol-Version header
  const protocolVersion = req.headers["mcp-protocol-version"] as string | undefined;
  if (protocolVersion !== "2026-07-28") {
    res.writeHead(400, { "Content-Type": "application/json" });
    res.end(JSON.stringify({ jsonrpc: "2.0", error: { code: -32022, message: "UnsupportedProtocolVersion", data: { supported: ["2026-07-28"], requested: protocolVersion } } }));
    return;
  }

  // 3. Parse MCP JSON-RPC and validate _meta protocolVersion matches header
  const body = await parseBody(req);
  const metaVersion = body.params?._meta?.protocolVersion ?? body.params?._meta?.["io.modelcontextprotocol/protocolVersion"];
  if (metaVersion && metaVersion !== protocolVersion) {
    res.writeHead(400, { "Content-Type": "application/json" });
    res.end(JSON.stringify({ jsonrpc: "2.0", error: { code: -32020, message: "HeaderMismatch" } }));
    return;
  }

  // 4. Dispatch in your own handleMcpRequest function — handle these for verification and store:
  //    server/discover, tools/list, tools/call, resources/list, resources/read,
  //    resources/templates/list, prompts/list, prompts/get, subscriptions/listen
  //    and return the JSON-RPC result object with ttlMs/cacheScope + resultType: "complete".
  const response = await handleMcpRequest(body);

  res.writeHead(200, { "Content-Type": "application/json" });
  res.end(JSON.stringify(response));
});

server.listen(PORT, () => console.log(`MCP listening on :${PORT}`));
```

Direct publisher test (no gateway):

```bash
curl -X POST http://localhost:3051/mcp \
  -H "Authorization: Bearer vnp-sk-test..." \
  -H "MCP-Protocol-Version: 2026-07-28" \
  -H "Mcp-Method: server/discover" -H "Mcp-Name: server/discover" \
  -d '{"jsonrpc":"2.0","id":1,"method":"server/discover"}'
```

> **Coming next:** `@openvoidnet/mcp-sdk` will wrap `McpServer` + `HttpTransport` so you don't copy this skeleton.

See the [Voidnet test server](https://github.com/void-labs/mcp-test-server-https) for a complete reference implementation.
