Govern: Self-Host (BYOK)
Deploy KYA-OS servers on your own infrastructure with full control
Goal
Deploy a KYA-OS server on your own infrastructure using the @kya-os/mcp-i packages. By the end of this cookbook, you'll have:
- A KYA-OS server running on Cloudflare Workers or Node.js
- Agent identity managed locally
- Full control over configuration and deployment
- Integration with Vouched KYA for delegation verification
- A working authorization flow: an agent that calls one of your tools is sent to a consent screen, the user approves the requested scopes (signing in with an identity provider first, if you configured OAuth or credentials), and the agent comes back holding a delegation your server verifies on every subsequent request
Best for: Teams who need full control over infrastructure, custom hosting requirements, or integration with existing deployments.
Prerequisites
- A Vouched KYA account with an API key
- Node.js 18+ (for local development);
@kya-os/bouncer-middlewareneeds Node.js 20.19+ or 22.12+ - Either:
- Cloudflare account (for Workers deployment)
- Your own Node.js hosting (Express, Docker, etc.)
Time Estimate
30 minutes
Choose Your Deployment Target
Cloudflare Workers (Recommended)
Edge deployment with global distribution, automatic scaling, and KV storage.
Pros:
- Zero-config scaling
- Global edge network
- Built-in KV storage
- Free tier available
Package: @kya-os/mcp-i-cloudflare
Steps
Scaffold the Project
Use the CLI to scaffold a Cloudflare Workers project (--mode mcp-i is the default):
npx @kya-os/create-mcpi-app my-mcp-server --platform cloudflare
cd my-mcp-server
npm installOr manually:
mkdir my-mcp-server && cd my-mcp-server
npm init -y
npm install @kya-os/mcp-i-cloudflare wranglerGenerate Agent Identity
Every KYA-OS server needs an Ed25519 key pair that produces a DID.
If you scaffolded with --platform cloudflare, an identity was already generated during scaffolding: the public DID lives in wrangler.toml (MCP_IDENTITY_AGENT_DID) and the private key in the git-ignored .dev.vars file.
To generate a fresh identity (new DID and keys) in any scaffolded KYA-OS project, run from the project directory:
npx @kya-os/create-mcpi-app regenerate-identityFor Node.js projects this writes .mcpi/identity.json; for Cloudflare projects it updates wrangler.toml and .dev.vars.
Important: Add .mcpi/ to your .gitignore. Never commit private keys to source control.
echo ".mcpi/" >> .gitignoreConfigure Environment Variables
Create a .env file (and .env.example for documentation):
# .env
# Checkpoint API key (from dashboard: see /docs/credentials for where to find it)
CHECKPOINT_API_KEY=sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
CHECKPOINT_PROJECT_ID=proj_abc123def456
# Environment
ENVIRONMENT=production
NODE_ENV=production
# Server URL (where your server will be accessible)
BASE_URL=https://mcp.yourdomain.com
# Identity (for Cloudflare, set as KV or secrets)
MCP_IDENTITY_PRIVATE_KEY=base64-private-key-here
# Optional: Anthropic/OpenAI for LLM features
ANTHROPIC_API_KEY=sk-ant-xxxxxAdd secrets to Wrangler:
wrangler secret put CHECKPOINT_API_KEY
wrangler secret put MCP_IDENTITY_PRIVATE_KEY
wrangler secret put ANTHROPIC_API_KEY # OptionalConfigure wrangler.toml:
name = "my-mcp-server"
main = "src/index.ts"
compatibility_date = "2024-01-01"
[vars]
ENVIRONMENT = "production"
# Required: replay-attack prevention (the only mandatory KV binding)
[[kv_namespaces]]
binding = "NONCE_CACHE"
id = "your-kv-namespace-id"
# Optional: archives generated proofs for auditability
[[kv_namespaces]]
binding = "PROOF_ARCHIVE"
id = "your-kv-namespace-id"
# Optional: persistent agent identity storage
[[kv_namespaces]]
binding = "IDENTITY_STORAGE"
id = "your-kv-namespace-id"
# Required for OAuth/delegation flows: delegation storage
[[kv_namespaces]]
binding = "DELEGATION_STORAGE"
id = "your-kv-namespace-id"Only NONCE_CACHE is required. PROOF_ARCHIVE, IDENTITY_STORAGE, and the tool-protection
cache (TOOL_PROTECTION_KV) are optional; add DELEGATION_STORAGE when you use OAuth/delegation
flows. Projects scaffolded with create-mcpi-app instead declare a single auto-provisioned KV
namespace that backs all of these logical stores.
Create the Server
These are the files a scaffolded project ships with (trimmed to the essentials). The entry
point delegates to createMCPIApp, which wires up the MCP transport, well-known endpoints,
and consent routes:
// src/index.ts
import { createMCPIApp } from '@kya-os/mcp-i-cloudflare';
import { MyMcpServerMCP } from './agent';
import { getRuntimeConfig } from './mcpi-runtime-config';
export default createMCPIApp({
AgentClass: MyMcpServerMCP,
getRuntimeConfig,
envPrefix: 'MY_MCP_SERVER', // Maps the prefixed KV binding (MY_MCP_SERVER_MCPI_KV) to standard names
});
// Export the Durable Object classes for Cloudflare Workers bindings
export { MyMcpServerMCP };
export { AuditProducer } from '@kya-os/mcp-i-cloudflare';// src/mcpi-runtime-config.ts
import { defineConfig, type CloudflareRuntimeConfig } from '@kya-os/mcp-i-cloudflare';
import type { CloudflareEnv } from '@kya-os/mcp-i-cloudflare';
import { greetTool } from './tools/greet';
export function getRuntimeConfig(env: CloudflareEnv): CloudflareRuntimeConfig {
const environment = (env.MCPI_ENV || env.ENVIRONMENT || 'development') as
| 'development'
| 'production';
return defineConfig({
environment,
vars: {
ENVIRONMENT: environment,
CHECKPOINT_API_KEY: env.CHECKPOINT_API_KEY,
CHECKPOINT_API_URL: env.CHECKPOINT_API_URL,
CHECKPOINT_PROJECT_ID: env.CHECKPOINT_PROJECT_ID,
},
});
}
export function getTools() {
return [greetTool];
}src/agent.ts (also scaffolded) extends MCPICloudflareAgent and registers each tool from
getTools() with automatic proof generation. Identity is not part of this config: the worker
reads it from its bindings: the MCP_IDENTITY_PRIVATE_KEY secret plus the
MCP_IDENTITY_AGENT_DID / MCP_IDENTITY_PUBLIC_KEY vars in wrangler.toml.
Register with KnowThat.ai (Optional)
Delegation verification does not require a registration call: the bouncer middleware authenticates to Vouched KYA with your API key and project ID. Registering your agent with the KnowThat.ai registry is optional: it creates a public agent profile and enables reputation tracking.
Ways to register:
- At scaffold time: with
npx @kya-os/create-mcpi-app,-y/CI runs register by default; pass--no-registerto skip the external registration. Interactive runs prompt for it (default yes) - Managed deploys: the dashboard deployment pipeline registers the identity for you (its Register Identity step) and provisions the
KTA_REGISTRATIONsecret
Deploy
# Development
wrangler dev
# Production
wrangler deployTest the Server
# Check DID document
curl https://your-server/.well-known/did.json
# Check agent metadata
curl https://your-server/.well-known/agent.json
# Health check (the Express example above serves /__health;
# scaffolded Cloudflare projects serve /health instead)
curl https://your-server/__health
# Test protected endpoint (should fail without delegation)
curl -X POST https://your-server/tools/read_file \
-H "Content-Type: application/json" \
-d '{"path": "/test"}'
# Expected: 401 UnauthorizedAdding Tools
Define tools as ToolDefinition objects: name, description, a JSON Schema inputSchema,
and a handler. Which tools require a delegation (and with which scopes) is not declared in
code: you configure it in the dashboard (next section). The Cloudflare runtime fetches that
configuration and enforces it before the handler runs. The Node runtime does not read it yet, so on
Node the scope check in your route (as in the Express example above) is what enforces.
// src/tools/index.ts
import type { ToolDefinition } from '@kya-os/mcp-i-cloudflare';
export const tools: ToolDefinition[] = [
{
name: 'read_file',
description: 'Reads content from a file',
inputSchema: {
type: 'object',
properties: {
path: { type: 'string', description: 'File path to read' },
},
required: ['path'],
},
handler: async (args: { path: string }) => {
// Your implementation
return { content: [{ type: 'text', text: 'file content' }] };
},
},
{
name: 'write_file',
description: 'Writes content to a file',
inputSchema: {
type: 'object',
properties: {
path: { type: 'string' },
content: { type: 'string' },
},
required: ['path', 'content'],
},
handler: async (args: { path: string; content: string }) => {
// Your implementation
return { content: [{ type: 'text', text: 'ok' }] };
},
},
];Configure in Dashboard
Register your tools in the Vouched KYA dashboard:
- Go to Policy → Auth
- Create an auth method with the scopes it grants, and assign it as the owner of each tool in the coverage table. Pick OAuth or credentials if the user should sign in with an identity provider first; pick consent-only if approving the scopes is enough
- Customize the consent screen text and branding on the same page
This enables the dashboard to show proper consent screens to users.
Tools reach Policy → Auth three ways. Registering the server with its tool list
(registerServer in @kya-os/checkpoint-mcp, or POST /api/v1/kya-os/servers) adds them
open, except a tool whose securitySchemes require an oauth2 token: it arrives protected
by consent. A submitted proof adds the tool it was for. Discovery reads your server's tools/list.
Proofs and discovery infer a risk level and may start a tool as requiring delegation. The tool
count on the Servers page is that server's own catalog, not this table: nothing there can be
protected.
The tool list itself is the Tools table in card 02 Authentication methods, and it fills itself as above. Discover tools in its header runs discovery on demand, and Remove tool… in a tool's menu removes a tool.
Prove Your Server's DID
A DID names one server across every org, so every POST /api/v1/kya-os/servers call proves the serverDid it names, including a re-registration of a DID your project already holds. A call with no proof answers 400 did_proof_required, naming the challenge endpoint.
A server that registers an OAuth resource proves its DID with resourceProof (next section). Any other server asks for a challenge with its DID alone and sends it back signed as didProof:
curl -X POST https://kya.vouched.id/api/v1/kya-os/servers/challenge \
-H "X-API-Key: $CHECKPOINT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"serverDid":"did:key:z6Mk…"}'
# → { "challenge": "eyJ…", "expiresAt": "…" } (valid 5 minutes, bound to this project and DID)
curl -X POST https://kya.vouched.id/api/v1/kya-os/servers \
-H "X-API-Key: $CHECKPOINT_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "serverDid": "did:key:z6Mk…", "url": "https://mcp.example.com/mcp", "didProof": "<compact JWS>" }'didProof is a compact JWS over {"challenge":"<challenge>"}, signed by a key the DID names. For a did:web, that is a key its DID document lists (alg EdDSA or ES256, kid a verification method under authentication or assertionMethod). For a did:key, it is the Ed25519 key the DID encodes (alg EdDSA), and nothing is fetched. A proof signed by any other key answers 403 invalid_did_proof. A DID method whose control cannot be proven answers 400 invalid_server_did.
registerServer in @kya-os/checkpoint-mcp 0.2.0 or later makes both calls on every registration, given the DID's key as signingKey: { privateKey, kid }. privateKey is a private JWK, a Node KeyObject or a WebCrypto CryptoKey. 0.1.0 sends no proof and gets 400 did_proof_required.
Register Your OAuth Resource
MCP clients that use OAuth (Claude, ChatGPT, Cursor) send the RFC 8707 resource parameter: your server's URL. Your org's authorization server issues a token only for a resource in its registry, and the token's aud is that resource's DID. A self-hosted server registers itself with the same POST /api/v1/kya-os/servers call, plus proof that it controls its did:web.
Two rules:
serverDidmust be adid:webon the resource URI's host, and a path DID's path must be a prefix of the URI's path:did:web:mcp.example.comordid:web:mcp.example.com:mcpforhttps://mcp.example.com/mcp, neverdid:web:mcp.example.com:other. Whoever serves the DID document at that place on the host can register the resource, and nobody else can.- The resource URI is https, with no query, fragment or credentials. It is stored in canonical form: lowercase host, no default port, no trailing slash.
1. Ask for a challenge (project API key, write permission):
curl -X POST https://kya.vouched.id/api/v1/kya-os/servers/challenge \
-H "X-API-Key: $CHECKPOINT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"serverDid":"did:web:mcp.example.com","resourceUri":"https://mcp.example.com/mcp"}'
# → { "challenge": "eyJ…", "expiresAt": "…" } (valid 5 minutes, bound to this project, URI and DID)2. Sign it and register. resourceProof is a compact JWS over {"challenge":"<challenge>"}, alg EdDSA or ES256, with kid set to a verification method your DID document lists under authentication or assertionMethod:
curl -X POST https://kya.vouched.id/api/v1/kya-os/servers \
-H "X-API-Key: $CHECKPOINT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"serverDid": "did:web:mcp.example.com",
"url": "https://mcp.example.com/mcp",
"resourceUri": "https://mcp.example.com/mcp",
"resourceProof": "<compact JWS>"
}'
# → { …, "resource": { "uri": "https://mcp.example.com/mcp", "did": "did:web:mcp.example.com" } }With @kya-os/checkpoint-mcp 0.2.0 or later, set resourceUri beside signingKey and registerServer sends both calls with resourceProof.
Vouched KYA fetches https://mcp.example.com/.well-known/did.json, verifies the signature against it, and only then writes anything. Re-registering the same resource is a no-op. A URI or DID another project holds answers 409 resource_conflict; a bad proof answers 403 invalid_resource_proof. Only 503 did_document_unavailable, sent with Retry-After, is worth retrying: Vouched KYA couldn't fetch the document this time (a timeout, a 5xx, or a 404 while a deploy switches over), and a server that registers as soon as it listens can ask before its host routes traffic to it. 403 did_document_invalid won't pass on retry: fix how the document is hosted. The host name must resolve in public DNS to a public address; serve the document directly (no redirects), under 64 KB, as valid JSON whose id is the DID.
Serve your own protected resource metadata at the RFC 9728 path-inserted URL (https://mcp.example.com/.well-known/oauth-protected-resource/mcp for the example), with resource set to the full resource URI and authorization_servers set to your org's issuer. Verify every token's aud against your serverDid.
Troubleshooting
Identity Not Loading
Delegation Verification Fails
- Check API key: Must be valid for your project
- Check project ID: The middleware authenticates with API key + project ID
- Check proof format: Proofs travel as a compact JWS at
_meta.proof.jwsin the request body
Cloudflare KV Issues
- KV not bound: Check
wrangler.tomlbindings - KV namespace missing: Create with
wrangler kv:namespace create
What You Learned
- How to scaffold a KYA-OS server project
- How to generate and manage agent identity
- How to deploy to Cloudflare Workers or Node.js
- How to add tools with scope requirements
- How to register with KnowThat.ai (optional)