MCP Publisher Guide
Build, deploy, and publish MCP servers on the Voidnet Console platform
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
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
Every published app gets a publisher API key. This key is generated in the Voidnet Console when you publish an app and is shown only once — save it securely.
vnp-sk-27fc6268dc64a9ba2c4cb92489e9175c9bf404e260beb268b976f1a56582eff3How it's 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
- 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
Your MCP server must meet these requirements to work with Voidnet Console:
1. HTTPS
All communication with the Voidnet is over HTTPS. You'll need a valid TLS certificate.
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
Voidnet Console requires MCP protocol version 2026-07-28 (stateless). Your server should:
- Accept the
MCP-Protocol-Version: 2026-07-28header - Return
2026-07-28in theserver/discoverresponse - Reject unsupported versions with
400and error code-32022
No sessions, no initialize handshake, no GET/DELETE endpoints. The legacy 2025-11-25 stateful model is not supported.
4. Authentication
Your server must validate the Authorization: Bearer <key> header and return 401 for invalid or missing keys. See Authentication below.
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
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)
Your server must implement the following MCP 2026-07-28 methods. No initialize handshake, no sessions, no MCP-Session-Id headers.
server/discover (REQUIRED)
The Voidnet calls server/discover to discover your server's capabilities. This replaces the legacy initialize handshake.
Request:
{
"jsonrpc": "2.0",
"id": 1,
"method": "server/discover"
}Response:
{
"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 include2026-07-28capabilities: Your server's capabilities (tools, resources, prompts)serverInfo: Your server's name and versioninstructions: Optional human-readable instructions
tools/list
Returns the tools your server provides:
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/list"
}Response (include ttlMs and cacheScope for spec-sanctioned caching):
{
"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
Invokes a tool with the provided arguments:
{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "my_tool",
"arguments": { "input": "hello" }
}
}Response (include resultType — complete or input_required for MRTR):
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"content": [
{
"type": "text",
"text": "Result: hello"
}
],
"resultType": "complete"
}
}resources/list (optional)
{
"jsonrpc": "2.0",
"id": 4,
"method": "resources/list"
}Response:
{
"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)
{
"jsonrpc": "2.0",
"id": 5,
"method": "resources/read",
"params": { "uri": "data://my-resource" }
}Response:
{
"jsonrpc": "2.0",
"id": 5,
"result": {
"uri": "data://my-resource",
"mimeType": "text/plain",
"text": "Resource content here",
"ttlMs": 300000,
"cacheScope": "private"
}
}prompts/list (optional)
{
"jsonrpc": "2.0",
"id": 6,
"method": "prompts/list"
}prompts/get (optional)
{
"jsonrpc": "2.0",
"id": 7,
"method": "prompts/get",
"params": { "name": "system_instruction", "arguments": { "task": "write a summary" } }
}subscriptions/listen (optional)
For servers that support change notifications. The Voidnet calls this once per subscription:
{
"jsonrpc": "2.0",
"id": 8,
"method": "subscriptions/listen",
"params": { "notifications": [] }
}Acknowledge with notifications/subscriptions/acknowledged.
Server Verification
When you publish an app in the Console, Voidnet performs a live verification of your server:
- The Voidnet calls
server/discoverto discover your server's capabilities - It calls
tools/list,resources/list, andprompts/list - 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
Before you can publish an app, you must verify ownership of your server's domain:
-
Initiate — In the Console, start domain verification. A unique verification token is generated.
-
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 -
Verify — Voidnet Console fetches the file and confirms the content matches the expected token.
-
Done — The domain is marked as verified and can be used for publishing.
One verified domain can host multiple apps.
Tier Configuration
Every published app has configurable tiers:
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
- 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
- A buyer discovers your app in the marketplace
- If you offer a free tier, the buyer can start using it immediately
- If you offer a paid tier, the buyer purchases a subscription
- The Voidnet enforces rate limits and metering based on the buyer's tier
- When a free tier buyer exhausts their meter, they're prompted to upgrade
Stripe Connect Billing
To accept payments, you need a Stripe Express account connected to Voidnet Console:
- Connect — In the Console Payments page, click "Connect with Stripe"
- Onboard — Complete Stripe Express onboarding (identity verification, bank account)
- Sync — Your Stripe account status appears in the Console
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 (e.g., founding-member 85/15 or negotiated deals) via Console. The split is locked at purchase time. Payouts are sent directly by Stripe to your connected bank account on Stripe's standard payout schedule.
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_feefor Voidnet Console's per-app fee (default 20%) - You receive 80% (default) minus any Stripe processing fees — rounded:
fee = floor(amount*percent/100),payout = amount - feeso the two always sum to gross exactly (platform absorbs the cent)
Testing Your Server
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
From the Publish MCP Interface page:
- Enter your App Name (unique per publisher)
- Select a verified domain — the server URL is auto-populated from your domain
- Add a description
- Click Verify Server
- The Console calls
server/discover→tools/list→resources/list→prompts/listand displays your capabilities - Save your publisher API key — shown once in the success dialog
- Configure tiers and click Submit to publish
3. Test with curl
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
Prerequisites checklist
- Server is running HTTPS with a valid certificate
- Server validates the
Authorization: Bearer <key>header - Server implements MCP
2026-07-28protocol (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
- Go to Console → Publish Apps and select AI Tools (MCP)
- Enter your App Name (unique per publisher, like a GitHub repo name)
- Select a verified domain to set the server URL
- Add a description
- Click Verify Server — the Voidnet probes your MCP server with
server/discoverand discovers its capabilities - Review the detected tools, resources, and prompts
- 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 - Close the dialog and configure tiers — free, paid, or both, with rate limits and metering limits
- Click Submit — your app is created as a draft and immediately published to the marketplace
- Your app now appears in Published Apps and the Voidnet Console Marketplace
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
- 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
A minimal MCP server in TypeScript/Node.js (stateless 2026-07-28):
import { createServer } from "https";
import { readFileSync } from "fs";
const PUBLISHER_API_KEY = process.env.PUBLISHER_API_KEY;
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 — you must 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
const response = await handleMcpRequest(body); // include ttlMs/cacheScope + resultType: "complete"
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):
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-sdkwill wrapMcpServer+HttpTransportso you don't copy this skeleton.
See the Voidnet test server for a complete reference implementation.