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 Checkpoint 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 Checkpoint account with an API key
- Node.js 18+ (for local development)
- 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
Node.js / Express
Self-hosted Node.js server with full control over the runtime.
Pros:
- Full runtime control
- Any hosting provider
- Existing infrastructure integration
Package: @kya-os/bouncer-middleware
Docker Container
Containerized deployment for Kubernetes or container orchestration.
Pros:
- Portable deployment
- Kubernetes-native
- Consistent environments
Package: @kya-os/mcp-i
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 wranglermkdir my-mcp-server && cd my-mcp-server
npm init -y
npm install @kya-os/bouncer-middleware @kya-os/mcp-i express
npm install -D typescript @types/express @types/nodemkdir my-mcp-server && cd my-mcp-server
npm init -y
npm install @kya-os/mcp-i express
npm install -D typescript @types/express @types/nodeCreate a Dockerfile:
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY dist ./dist
EXPOSE 3000
CMD ["node", "dist/index.js"]Generate 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.
In any project, use the generateIdentity helper exported by @kya-os/create-mcpi-app:
npm install -D @kya-os/create-mcpi-app// scripts/generate-identity.mjs
import { generateIdentity } from '@kya-os/create-mcpi-app/helpers';
import fs from 'node:fs';
const identity = await generateIdentity();
fs.mkdirSync('.mcpi', { recursive: true });
fs.writeFileSync('.mcpi/identity.json', JSON.stringify(identity, null, 2));
console.log(`Generated DID: ${identity.did}`);Run it:
node scripts/generate-identity.mjsThis writes .mcpi/identity.json:
{
"did": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK",
"kid": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK#key-1",
"privateKey": "base64-encoded-private-key",
"publicKey": "base64-encoded-public-key",
"createdAt": "2024-01-15T10:00:00.000Z",
"type": "development"
}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)
AGENTSHIELD_API_KEY=sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
AGENTSHIELD_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-xxxxxTwo env names, one key: AGENTSHIELD_API_KEY is the KYA-OS worker/bouncer naming convention for
the same dashboard API key the Checkpoint SDKs read as CHECKPOINT_API_KEY — see
Credentials for where to find the key and which name applies where. The
bouncer middleware reads no environment variables itself — you pass values into its config
explicitly.
Add secrets to Wrangler:
wrangler secret put AGENTSHIELD_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.
Install dotenv:
npm install dotenvLoad in your entry point:
import 'dotenv/config';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,
AGENTSHIELD_API_KEY: env.AGENTSHIELD_API_KEY,
AGENTSHIELD_API_URL: env.AGENTSHIELD_API_URL,
AGENTSHIELD_PROJECT_ID: env.AGENTSHIELD_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.
// src/index.ts
import express from 'express';
import { createBouncerMiddleware } from '@kya-os/bouncer-middleware';
import { createMCPIRuntime } from '@kya-os/mcp-i';
const app = express();
app.use(express.json());
// Initialize the KYA-OS runtime. It loads its own identity:
// MCP_IDENTITY_PRIVATE_KEY / MCP_IDENTITY_PUBLIC_KEY / MCP_IDENTITY_AGENT_DID
// env vars when set, otherwise .mcpi/identity.json.
const runtime = createMCPIRuntime({
identity: {
environment: process.env.ENVIRONMENT === 'production' ? 'production' : 'development',
devIdentityPath: '.mcpi', // directory containing identity.json
},
});
await runtime.initialize();
const identity = await runtime.getIdentity();
// Well-known endpoints
const wellKnown = runtime.createWellKnownHandler({
serviceName: 'My MCP Server',
serviceEndpoint: process.env.BASE_URL,
});
app.get('/.well-known/did.json', async (req, res) => {
const response = await wellKnown('/.well-known/did.json');
if (response && 'status' in response) {
res.status(response.status).set(response.headers).send(response.body);
} else {
res.status(404).end();
}
});
app.get('/.well-known/agent.json', async (req, res) => {
const response = await wellKnown('/.well-known/agent.json');
if (response && 'status' in response) {
res.status(response.status).set(response.headers).send(response.body);
} else {
res.status(404).end();
}
});
// Protected tool endpoints
app.post(
'/tools/:toolName',
createBouncerMiddleware({
apiKey: process.env.AGENTSHIELD_API_KEY!,
projectId: process.env.AGENTSHIELD_PROJECT_ID!,
}),
async (req, res) => {
const { toolName } = req.params;
const { scopes } = req.bouncer;
// Verify scope for this tool
const requiredScope = `${toolName}:execute`;
if (!scopes.includes(requiredScope)) {
return res.status(403).json({
error: 'Insufficient scope',
required: requiredScope,
granted: scopes,
});
}
// Execute the tool through the runtime — it runs your handler and
// generates a signed proof automatically. Proofs stay out-of-band
// (runtime.getLastProof()), never in the response body.
const result = await runtime.processToolCall(toolName, req.body, async (args) =>
executeToolInternal(toolName, args)
);
res.json(result);
}
);
// Health check
app.get('/__health', (req, res) => {
res.json({ status: 'ok', did: identity.did });
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`KYA-OS server running on port ${PORT}`);
console.log(`Agent DID: ${identity.did}`);
});Register with KnowThat.ai (Optional)
Delegation verification does not require a registration call — the bouncer middleware authenticates to Checkpoint 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) -
From a project that has an identity — run the
mcpiCLI's register command. It reads.mcpi/identity.json(or identity env vars), registers the agent with KnowThat.ai, and saves a claim receipt. Requires aKYA_VOUCHED_API_KEYenvironment variable.npx @kya-os/cli register -
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 deployBuild and run:
npm run build
node dist/index.jsFor production, use PM2 or similar:
npm install -g pm2
pm2 start dist/index.js --name mcp-server# Build
npm run build
docker build -t my-mcp-server .
# Run
docker run -p 3000:3000 \
-e AGENTSHIELD_API_KEY=sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \
-e AGENTSHIELD_PROJECT_ID=proj_xxx \
-e BASE_URL=https://mcp.yourdomain.com \
-v $(pwd)/.mcpi:/app/.mcpi:ro \
my-mcp-serverTest 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), and the runtime enforces it before the
handler runs.
// 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 Checkpoint 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.
Your tools appear on Policy → Auth on their own once the server is reachable. If one doesn't
show up, triggering discovery and manually adding or removing tools is the single step still on
the older Control Access surface (/dashboard/{orgId}/{projectId}/control-access/tools).
Troubleshooting
Identity Not Loading
| Symptom | Cause | Fix |
|---|---|---|
| "Cannot read identity" | File not found | Check .mcpi/identity.json exists |
| Invalid DID format | Corrupted key | Regenerate identity |
| Key mismatch | Wrong env variable | Verify MCP_IDENTITY_PRIVATE_KEY |
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)
Next Steps
| Goal | Resource |
|---|---|
| Add OAuth authentication | OAuth Integration |
| Configure auth methods | Auth Methods |
| Understand delegations | Delegations |
| Managed deployment | Dashboard Deploy |
