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
| Component | Before (MCP) | After (MCP-I) |
|---|---|---|
| Identity | None | DID (did:key:z6Mk...) |
| Authorization | None or custom | Delegation-based |
| Tool access | Open | Scope-gated |
| Audit trail | None | Proofs + dashboard |
| Discovery | None | Well-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-iGenerate Agent Identity
Your MCP-I server needs a persistent identity (Ed25519 key pair → DID).
npx @kya-os/mcp-i generate-identityThis 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/" >> .gitignoreNever 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.comAdd 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:
- Go to Control Access → Tools
- For each tool, add:
| Field | Value |
|---|---|
| Name | read_file |
| Display Name | Read File |
| Description | Reads content from a file on the server |
| Scopes | files:read |
| Sensitive | No |
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 metadataTest 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 delegationTest 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 proofMigration Checklist
| Step | Status |
|---|---|
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 bouncerPhase 3: Full Protection
Eventually protect all tools:
// All tools protected
app.use('/tools', bouncer);Troubleshooting
Bouncer Returns 401 for All Requests
| Symptom | Cause | Fix |
|---|---|---|
| "Invalid API key" | Wrong key | Check AGENTSHIELD_API_KEY |
| "Project not found" | Wrong project | Check CHECKPOINT_PROJECT_ID |
| "No proof provided" | Missing header | Client 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
| Goal | Resource |
|---|---|
| Configure auth methods | Auth Methods |
| Customize consent flow | Consent Flows |
| Understand delegations | Delegations |
| Full MCP-I server | Self-Host |