Checkpoint Docs
Cookbooks

Govern: MCP to MCP-I Migration

Add identity and authorization to an existing MCP server

Goal

Add MCP-I (Model Context Protocol with Identity) 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 16+
  • A Checkpoint account with an API key

Time Estimate

25-30 minutes


What Changes

ComponentBefore (MCP)After (MCP-I)
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 — MCP-I adds an authorization layer on top.


Steps

Install MCP-I Packages

Add the bouncer middleware to your project:

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

Generate Agent Identity

Your MCP-I server needs a persistent identity (Ed25519 key pair → DID).

npx @kya-os/mcp-i generate-identity

This creates .mcpi/identity.json:

{
  "did": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK",
  "privateKey": "base64-encoded-private-key",
  "publicKey": "base64-encoded-public-key",
  "createdAt": "2024-01-15T10:00:00.000Z"
}

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=as_live_xxxxxxxxxxxx
CHECKPOINT_PROJECT_ID=proj_abc123def456

# For production, inline the identity
MCP_IDENTITY_PRIVATE_KEY=base64-private-key-here

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

Add Well-Known Endpoints

MCP-I servers must expose discovery endpoints for their identity:

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

// Load identity
const identity = JSON.parse(
  fs.readFileSync(path.join(process.cwd(), '.mcpi', 'identity.json'), 'utf-8')
);

// Initialize runtime
const runtime = await createMCPIRuntime({
  environment: process.env.NODE_ENV || 'development',
  identity,
  wellKnown: {
    baseUrl: process.env.BASE_URL!,
    agentName: 'Your MCP Server',
    agentDescription: 'Your server description',
  },
});

// Well-known DID document
app.get('/.well-known/did.json', async (req, res) => {
  const didDocument = await runtime.getDIDDocument();
  res.json(didDocument);
});

// Well-known agent metadata
app.get('/.well-known/agent.json', async (req, res) => {
  const agentMetadata = await runtime.getAgentMetadata();
  res.json(agentMetadata);
});
import { createMCPIRuntime } from '@kya-os/mcp-i';
import fs from 'fs';

const identity = JSON.parse(fs.readFileSync('.mcpi/identity.json', 'utf-8'));

const runtime = await createMCPIRuntime({
  environment: process.env.NODE_ENV || 'development',
  identity,
  wellKnown: {
    baseUrl: process.env.BASE_URL!,
    agentName: 'Your MCP Server',
  },
});

fastify.get('/.well-known/did.json', async () => {
  return runtime.getDIDDocument();
});

fastify.get('/.well-known/agent.json', async () => {
  return runtime.getAgentMetadata();
});
import { Hono } from 'hono';
import { createMCPIRuntime } from '@kya-os/mcp-i';

const app = new Hono();

const identity = JSON.parse(await Deno.readTextFile('.mcpi/identity.json'));

const runtime = await createMCPIRuntime({
  environment: 'production',
  identity,
  wellKnown: {
    baseUrl: process.env.BASE_URL!,
    agentName: 'Your MCP Server',
  },
});

app.get('/.well-known/did.json', async (c) => {
  return c.json(await runtime.getDIDDocument());
});

app.get('/.well-known/agent.json', async (c) => {
  return c.json(await runtime.getAgentMetadata());
});

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 (MCP-I protected):

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

// Create middleware instance
const bouncer = createBouncerMiddleware({
  apiKey: process.env.AGENTSHIELD_API_KEY!,
  projectId: process.env.CHECKPOINT_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
  const { path } = req.body;
  const content = await fs.readFile(path, 'utf-8');

  // Generate proof for the response
  const proof = await runtime.generateProof({ content }, { agentDid, toolName: 'read_file' });

  res.json({ content, proof });
});

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

Add your tools to the Checkpoint dashboard for proper consent screens:

  1. Go to Control Access → Tools
  2. For each tool, add:
FieldValue
Nameread_file
Display NameRead File
DescriptionReads content from a file on the server
Scopesfiles:read
SensitiveNo

Repeat for all tools you want to protect.

Add Proof Generation

Generate cryptographic proofs for tool responses:

import { createMCPIRuntime } from '@kya-os/mcp-i';

// In your tool handler
app.post('/tools/read_file', bouncer, async (req, res) => {
  const { agentDid } = req.bouncer;
  const { path } = req.body;

  // Execute tool
  const content = await fs.readFile(path, 'utf-8');

  // Generate proof
  const proof = await runtime.generateProof(
    { content, path },
    {
      agentDid,
      toolName: 'read_file',
      timestamp: Date.now(),
    }
  );

  // Return result with proof
  res.json({
    result: { content },
    proof,
  });
});

Proofs allow clients to verify the response came from your server and hasn't been tampered with.

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" \
  -H "X-MCPI-Proof: <proof-from-delegation>" \
  -d '{"path": "/test.txt"}'

# Expected: 200 OK with content and proof

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
Add proof generation to responses
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 CHECKPOINT_PROJECT_ID
"No proof provided"Missing headerClient must send proof

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 MCP-I 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 for responses
  • A gradual migration strategy

Next Steps

GoalResource
Configure auth methodsAuth Methods
Customize consent flowConsent Flows
Understand delegationsDelegations
Full MCP-I serverSelf-Host