Checkpoint Docs
Cookbooks

Govern: MCP to KYA-OS Migration

Add identity and authorization to an existing MCP server

Goal

Add KYA-OS capabilities to your existing MCP server. By the end of this cookbook, you'll have:

  • An agent identity (DID) for your existing server
  • Delegation-based authorization on tool endpoints
  • Cryptographic proof generation for responses
  • Dashboard integration for monitoring

Best for: Teams with existing MCP servers who want to add identity and governance without a full rewrite.

Prerequisites

  • An existing MCP server (stdio, SSE, or HTTP)
  • Node.js 20+
  • A Checkpoint account with an API key

Time Estimate

25-30 minutes


What Changes

ComponentBefore (MCP)After (KYA-OS)
IdentityNoneDID (did:key:z6Mk...)
AuthorizationNone or customDelegation-based
Tool accessOpenScope-gated
Audit trailNoneProofs + dashboard
DiscoveryNoneWell-known endpoints

Your server's core functionality stays the same — KYA-OS adds an authorization layer on top.


Steps

Install KYA-OS Packages

Add the bouncer middleware to your project:

npm install @kya-os/bouncer-middleware @kya-os/mcp-i

Generate Agent Identity

Your KYA-OS server needs a persistent identity (Ed25519 key pair → DID). The @kya-os/create-mcpi-app package exports a generateIdentity helper for this:

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}`);
node scripts/generate-identity.mjs

This creates .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"
}

Prefer a guided setup? npx @kya-os/create-mcpi-app wrap detects your existing MCP server, generates wrapper files, writes .mcpi/identity.json, and adds .mcpi/ to .gitignore for you.

Add to .gitignore:

echo ".mcpi/" >> .gitignore

Never commit private keys. Use environment variables for production.

Configure Environment

Add Checkpoint configuration to your environment:

# .env
AGENTSHIELD_API_KEY=sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
AGENTSHIELD_PROJECT_ID=proj_abc123def456

# For production, inline the full identity triple
MCP_IDENTITY_PRIVATE_KEY=base64-private-key-here
MCP_IDENTITY_PUBLIC_KEY=base64-public-key-here
MCP_IDENTITY_AGENT_DID=did:key:z6Mk...

# Your server's public URL
BASE_URL=https://your-mcp-server.com

Two env names, one key: AGENTSHIELD_API_KEY is the KYA-OS/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 Well-Known Endpoints

KYA-OS servers must expose discovery endpoints for their identity:

// Add to your Express app
import { createMCPIRuntime } from '@kya-os/mcp-i';

// The runtime loads its own identity: MCP_IDENTITY_PRIVATE_KEY /
// MCP_IDENTITY_PUBLIC_KEY / MCP_IDENTITY_AGENT_DID env vars when set,
// otherwise the identity.json inside `identity.devIdentityPath`.
const runtime = createMCPIRuntime({
  identity: {
    environment: process.env.NODE_ENV === 'production' ? 'production' : 'development',
    devIdentityPath: '.mcpi', // directory containing identity.json
  },
});
await runtime.initialize();

const wellKnown = runtime.createWellKnownHandler({
  serviceName: 'Your MCP Server',
  serviceEndpoint: process.env.BASE_URL,
});

// Well-known DID document
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();
  }
});

// Well-known agent metadata
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();
  }
});
import { createMCPIRuntime } from '@kya-os/mcp-i';

const runtime = createMCPIRuntime({
  identity: {
    environment: process.env.NODE_ENV === 'production' ? 'production' : 'development',
    devIdentityPath: '.mcpi',
  },
});
await runtime.initialize();

const wellKnown = runtime.createWellKnownHandler({
  serviceName: 'Your MCP Server',
  serviceEndpoint: process.env.BASE_URL,
});

fastify.get('/.well-known/did.json', async (request, reply) => {
  const response = await wellKnown('/.well-known/did.json');
  if (response && 'status' in response) {
    reply.status(response.status).headers(response.headers);
    return response.body;
  }
  reply.status(404);
});

fastify.get('/.well-known/agent.json', async (request, reply) => {
  const response = await wellKnown('/.well-known/agent.json');
  if (response && 'status' in response) {
    reply.status(response.status).headers(response.headers);
    return response.body;
  }
  reply.status(404);
});
import { Hono } from 'hono';
import { createMCPIRuntime } from '@kya-os/mcp-i';

const app = new Hono();

const runtime = createMCPIRuntime({
  identity: {
    environment: process.env.NODE_ENV === 'production' ? 'production' : 'development',
    devIdentityPath: '.mcpi',
  },
});
await runtime.initialize();

const wellKnown = runtime.createWellKnownHandler({
  serviceName: 'Your MCP Server',
  serviceEndpoint: process.env.BASE_URL,
});

app.get('/.well-known/did.json', async (c) => {
  const response = await wellKnown('/.well-known/did.json');
  if (response && 'status' in response) {
    return c.body(response.body, response.status, response.headers);
  }
  return c.notFound();
});

app.get('/.well-known/agent.json', async (c) => {
  const response = await wellKnown('/.well-known/agent.json');
  if (response && 'status' in response) {
    return c.body(response.body, response.status, response.headers);
  }
  return c.notFound();
});

Protect Tool Endpoints

Wrap your existing tool handlers with the bouncer middleware:

Before (unprotected):

// Original tool handler
app.post('/tools/read_file', async (req, res) => {
  const { path } = req.body;
  const content = await fs.readFile(path, 'utf-8');
  res.json({ content });
});

After (KYA-OS protected):

import { createBouncerMiddleware } from '@kya-os/bouncer-middleware';

// Create middleware instance
const bouncer = createBouncerMiddleware({
  apiKey: process.env.AGENTSHIELD_API_KEY!,
  projectId: process.env.AGENTSHIELD_PROJECT_ID!,
});

// Protected tool handler
app.post('/tools/read_file', bouncer, async (req, res) => {
  // Bouncer verified the delegation — access agent info
  const { agentDid, scopes, reputation } = req.bouncer;

  // Check required scope
  if (!scopes.includes('files:read')) {
    return res.status(403).json({
      error: 'Scope not granted',
      required: 'files:read',
      granted: scopes,
    });
  }

  // Execute the tool through the runtime — it runs your handler and
  // generates a signed proof automatically. The proof stays out-of-band
  // (see the Add Proof Generation step); it is never added to the response.
  const result = await runtime.processToolCall(
    'read_file',
    req.body,
    async ({ path }: { path: string }) => {
      const content = await fs.readFile(path, 'utf-8');
      return { content };
    }
  );

  res.json(result);
});

Add Scope Checks per Tool

Define scope requirements for each of your tools:

// tools/config.ts
export const toolScopes: Record<string, string[]> = {
  read_file: ['files:read'],
  write_file: ['files:write'],
  delete_file: ['files:delete'],
  list_directory: ['files:read'],
  send_email: ['email:send'],
  read_calendar: ['calendar:read'],
  create_event: ['calendar:write'],
};

// Reusable scope checker
function requireScopes(toolName: string) {
  return (req, res, next) => {
    const { scopes } = req.bouncer;
    const required = toolScopes[toolName] || [];

    const missing = required.filter((s) => !scopes.includes(s));
    if (missing.length > 0) {
      return res.status(403).json({
        error: 'Insufficient scopes',
        required,
        missing,
        granted: scopes,
      });
    }

    next();
  };
}

// Usage
app.post('/tools/read_file', bouncer, requireScopes('read_file'), handler);
app.post('/tools/write_file', bouncer, requireScopes('write_file'), handler);

Register Tools in Dashboard

Register your tools in the Checkpoint dashboard for proper consent screens:

  1. Discover (or manually add/remove) your server's tools on the legacy tools surface at /dashboard/{orgId}/{projectId}/control-access/tools (legacy surface, moving to Access)
  2. Go to Policy → Auth and assign each tool an owning protection with the scopes it requires — for example, read_file owned by a protection granting files:read

Repeat the assignment for all tools you want to protect; tools left unassigned stay open.

Add Proof Generation

The runtime generates cryptographic proofs for you. Wrap tool execution in runtime.processToolCall(toolName, args, handler, session?) — it runs your handler, signs a proof over the result, and stores the proof for out-of-band retrieval:

// `runtime` is the KYA-OS runtime initialized in the well-known step
app.post('/tools/read_file', bouncer, async (req, res) => {
  const { path } = req.body;

  // Executes the tool and generates a signed proof automatically
  const result = await runtime.processToolCall('read_file', { path }, async () => {
    const content = await fs.readFile(path, 'utf-8');
    return { content, path };
  });

  // The proof is NOT in the response body — retrieve it out-of-band
  // if you want to inspect or archive it
  const proof = runtime.getLastProof();

  res.json(result);
});

Proofs let verifiers confirm a response came from your server and hasn't been tampered with. They stay outside the response body by design, so agents and LLM clients never see cryptographic material.

Test the Migration

Test well-known endpoints:

curl https://your-server/.well-known/did.json
# Should return DID document

curl https://your-server/.well-known/agent.json
# Should return agent metadata

Test protected endpoint without delegation:

curl -X POST https://your-server/tools/read_file \
  -H "Content-Type: application/json" \
  -d '{"path": "/test.txt"}'

# Expected: 401 Unauthorized - No delegation

Test with delegation (after obtaining one via OAuth flow):

curl -X POST https://your-server/tools/read_file \
  -H "Content-Type: application/json" \
  -d '{"path": "/test.txt", "_meta": {"proof": {"jws": "<compact-jws-from-delegation-flow>"}}}'

# Expected: 200 OK with the tool result (proofs stay out-of-band)

Migration Checklist

StepStatus
Install @kya-os/bouncer-middleware and @kya-os/mcp-i
Generate agent identity (.mcpi/identity.json)
Add .mcpi/ to .gitignore
Configure environment variables
Add /.well-known/did.json endpoint
Add /.well-known/agent.json endpoint
Wrap tool handlers with bouncer middleware
Add scope checks per tool
Wrap tool execution with processToolCall
Register tools in Checkpoint dashboard
Test protected endpoints
Deploy and verify

Gradual Migration Strategy

You don't have to migrate all tools at once. Use a gradual approach:

Phase 1: Identity Only

Add well-known endpoints without protecting tools:

// Just add discovery, no protection yet
app.get('/.well-known/did.json', ...);
app.get('/.well-known/agent.json', ...);

Phase 2: Protect Sensitive Tools

Wrap only high-risk tools:

// Sensitive tools get protection
app.post('/tools/write_file', bouncer, ...);
app.post('/tools/delete_file', bouncer, ...);
app.post('/tools/send_email', bouncer, ...);

// Read-only tools stay open (for now)
app.post('/tools/read_file', ...); // No bouncer

Phase 3: Full Protection

Eventually protect all tools:

// All tools protected
app.use('/tools', bouncer);

Troubleshooting

Bouncer Returns 401 for All Requests

SymptomCauseFix
"Invalid API key"Wrong keyCheck AGENTSHIELD_API_KEY
"Project not found"Wrong projectCheck AGENTSHIELD_PROJECT_ID
MISSING_PROOFNo _meta.proof.jws in the bodyClient must send the proof envelope

Scopes Always Empty

  • Check delegation — Delegation may not grant required scopes
  • Check tool registration — Tools must be registered in dashboard
  • Check consent flow — User must consent to specific scopes

Proof Verification Fails

  • Check identity — Private key must match DID
  • Check timestamp — Proofs have a validity window
  • Check format — Proof must be properly signed JWS

What You Learned

  • How to add KYA-OS identity to an existing MCP server
  • How to protect tools with delegation-based authorization
  • How to add scope requirements per tool
  • How to generate proofs automatically with processToolCall
  • A gradual migration strategy

Next Steps

GoalResource
Configure auth methodsAuth Methods
Customize consent flowConsent Flows
Understand delegationsDelegations
Full KYA-OS serverSelf-Host