@kya-os/mcp-bri
Version:
Give your MCP server cryptographic identity in 2 lines of code
629 lines • 24.8 kB
JavaScript
;
/**
* @kya-os/mcp-i - Ultra-light MCP Identity auto-registration
*
* Enable any MCP server to get a verifiable identity with just 2 lines of code:
*
* ```typescript
* import "@kya-os/mcp-i/auto"; // That's it! Your server now has identity
* ```
*
* Or with configuration:
* ```typescript
* import { enableMCPIdentity } from "@kya-os/mcp-i";
* await enableMCPIdentity({ name: "My Amazing Agent" });
* ```
*
* Future multi-registry support:
* ```typescript
* await enableMCPIdentity({
* name: "My Agent",
* // Additional registries will be supported as directories adopt MCP-I
* });
* ```
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __exportStar = (this && this.__exportStar) || function(m, exports) {
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.MCPIdentity = exports.resolveRegistries = exports.REGISTRY_TIERS = exports.RegistryFactory = void 0;
exports.enableMCPIdentity = enableMCPIdentity;
const axios_1 = __importDefault(require("axios"));
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const crypto = __importStar(require("./crypto"));
const registry_1 = require("./registry");
// Re-export types and registry utilities for convenience
__exportStar(require("./types"), exports);
var registry_2 = require("./registry");
Object.defineProperty(exports, "RegistryFactory", { enumerable: true, get: function () { return registry_2.RegistryFactory; } });
Object.defineProperty(exports, "REGISTRY_TIERS", { enumerable: true, get: function () { return registry_2.REGISTRY_TIERS; } });
Object.defineProperty(exports, "resolveRegistries", { enumerable: true, get: function () { return registry_2.resolveRegistries; } });
// Global identity instance
let globalIdentity = null;
class MCPIdentity {
constructor(identity, options = {}) {
this.usedNonces = new Set();
this.registryAdapters = new Map();
this.did = identity.did;
this.publicKey = identity.publicKey;
this.privateKey = identity.privateKey;
this.timestampTolerance = options.timestampTolerance || 60000; // 60 seconds
this.enableNonceTracking = options.enableNonceTracking !== false;
// Multi-registry initialization
this.didHost = identity.didHost || 'knowthat';
this.registries = identity.registries || [
{
name: this.didHost,
status: 'active',
type: 'primary',
registeredAt: identity.registeredAt
}
];
// Start nonce cleanup if tracking is enabled
if (this.enableNonceTracking) {
this.startNonceCleanup();
}
}
/**
* Initialize MCP Identity - the main entry point
*
* IMPORTANT: This only makes API calls in these cases:
* 1. First time ever (no identity exists) - registers with all specified registries
* 2. Adding new registries to existing identity - only calls the new registries
*
* After initial registration, all subsequent calls load from disk with NO API calls.
*/
static async init(options) {
// Return existing global identity if already initialized
if (globalIdentity) {
// Check if we need to sync to new registries
if (options?.registries) {
const requestedRegistries = (0, registry_1.resolveRegistries)(options.registries);
await globalIdentity.syncToRegistries(requestedRegistries);
}
return globalIdentity;
}
// Try to load existing identity
let identity = await loadIdentity(options?.persistencePath);
// Handle existing identity
if (identity) {
console.log('[MCP-I] Loaded existing identity:', identity.did);
globalIdentity = new MCPIdentity(identity, options);
// Sync to any new registries specified in options
if (options?.registries) {
const requestedRegistries = (0, registry_1.resolveRegistries)(options.registries);
await globalIdentity.syncToRegistries(requestedRegistries);
}
return globalIdentity;
}
// No existing identity - need to create new one
console.log('[MCP-I] No existing identity found, creating new identity...');
// Resolve which registries to use
const registriesToUse = (0, registry_1.resolveRegistries)(options?.registries);
const primaryRegistry = options?.didHost || 'knowthat';
// Ensure primary registry is first
const orderedRegistries = [
primaryRegistry,
...registriesToUse.filter(r => r !== primaryRegistry)
];
console.log('[MCP-I] Publishing to registries:', orderedRegistries.join(', '));
// Create DID at primary registry (KnowThat by default)
const primaryAdapter = registry_1.RegistryFactory.getAdapter(primaryRegistry);
if (!primaryAdapter || primaryAdapter.type !== 'primary') {
throw new Error(`Primary registry ${primaryRegistry} not available or not a primary registry`);
}
const publishData = {
did: '', // Will be filled after registration
name: options?.name || process.env.MCP_SERVER_NAME || 'Unnamed MCP Server',
description: options?.description,
repository: options?.repository,
publicKey: '', // Will be filled after key generation
metadata: {}
};
// Generate keys first
console.log('[MCP-I] Generating cryptographic keys...');
const keyPair = await crypto.generateKeyPair();
publishData.publicKey = keyPair.publicKey;
// Register with primary registry
console.log(`[MCP-I] Registering with primary registry: ${primaryRegistry}`);
const primaryResult = await primaryAdapter.publish(publishData);
if (!primaryResult.success) {
throw new Error(`Failed to register with primary registry: ${primaryResult.error}`);
}
// Get the full registration response (this is a bit hacky but maintains compatibility)
const response = await autoRegister({
name: publishData.name,
description: publishData.description,
repository: publishData.repository,
apiEndpoint: primaryRegistry === 'knowthat' ? 'https://knowthat.ai' : options?.apiEndpoint || 'https://knowthat.ai'
});
// Create persisted identity
identity = {
did: response.did,
publicKey: keyPair.publicKey,
privateKey: keyPair.privateKey,
agentId: response.agent.id,
agentSlug: response.agent.slug,
registeredAt: new Date().toISOString(),
didHost: primaryRegistry,
registries: [{
name: primaryRegistry,
status: 'active',
type: 'primary',
registeredAt: new Date().toISOString(),
registryAgentId: response.agent.id
}]
};
// Save identity before publishing to other registries
await saveIdentity(identity, options?.persistencePath);
console.log('[MCP-I] ✅ Success! Your agent has been registered.');
console.log(`[MCP-I] DID: ${response.did}`);
console.log(`[MCP-I] Profile: ${response.agent.url}`);
// Create MCPIdentity instance
globalIdentity = new MCPIdentity(identity, options);
// Publish to additional registries
if (orderedRegistries.length > 1) {
console.log('[MCP-I] Publishing to additional registries...');
await globalIdentity.syncToRegistries(orderedRegistries.slice(1));
}
return globalIdentity;
}
/**
* Sync identity to additional registries
*
* RATE LIMITED: Each registry can only be synced once per minute to prevent spam.
* Skips registries that are already active.
*/
async syncToRegistries(registryNames) {
const publishData = {
did: this.did,
name: this.extractAgentName(),
description: '', // Would need to store this
repository: '', // Would need to store this
publicKey: this.publicKey,
agentId: this.extractAgentId(),
agentSlug: this.extractAgentSlug(),
metadata: {}
};
for (const registryName of registryNames) {
// Skip if already registered
if (this.registries.some(r => r.name === registryName && r.status === 'active')) {
continue;
}
const adapter = registry_1.RegistryFactory.getAdapter(registryName);
if (!adapter) {
console.warn(`[MCP-I] Registry ${registryName} not available, skipping`);
continue;
}
try {
console.log(`[MCP-I] Publishing to ${registryName}...`);
const result = await adapter.publish(publishData);
if (result.success) {
// Update registry status
const existingIndex = this.registries.findIndex(r => r.name === registryName);
const newStatus = {
name: registryName,
status: 'active',
type: adapter.type,
registeredAt: new Date().toISOString(),
registryAgentId: result.registryAgentId
};
if (existingIndex >= 0) {
this.registries[existingIndex] = newStatus;
}
else {
this.registries.push(newStatus);
}
console.log(`[MCP-I] ✅ Published to ${registryName}: ${result.profileUrl || 'success'}`);
}
else {
console.error(`[MCP-I] ❌ Failed to publish to ${registryName}: ${result.error}`);
// Update registry status with error
this.registries.push({
name: registryName,
status: 'failed',
type: adapter.type,
error: result.error,
lastSyncAt: new Date().toISOString()
});
}
}
catch (error) {
console.error(`[MCP-I] ❌ Error publishing to ${registryName}:`, error.message);
this.registries.push({
name: registryName,
status: 'failed',
type: 'secondary',
error: error.message,
lastSyncAt: new Date().toISOString()
});
}
}
// Save updated registry status
await this.persistRegistryStatus();
}
/**
* Add a new registry
*/
async addRegistry(registryName) {
await this.syncToRegistries([registryName]);
}
/**
* Add multiple registries
*/
async addRegistries(registryNames) {
await this.syncToRegistries(registryNames);
}
/**
* Get current registry status
*/
getRegistryStatus() {
return [...this.registries];
}
/**
* Check if registered with a specific registry
*/
isRegisteredWith(registryName) {
return this.registries.some(r => r.name === registryName && r.status === 'active');
}
/**
* Sign a message with the agent's private key using Ed25519
*/
async sign(message) {
return crypto.sign(message, this.privateKey);
}
/**
* Verify a signature against a public key
*/
async verify(message, signature, publicKey) {
return crypto.verify(message, signature, publicKey || this.publicKey);
}
/**
* Respond to an MCP-I challenge
*/
async respondToChallenge(challenge) {
// Validate timestamp to prevent replay attacks
const now = Date.now();
const challengeAge = now - challenge.timestamp;
if (challengeAge > this.timestampTolerance) {
throw new Error('Challenge expired');
}
if (challengeAge < 0) {
throw new Error('Challenge timestamp is in the future');
}
// Check for nonce reuse if tracking is enabled
if (this.enableNonceTracking) {
if (this.usedNonces.has(challenge.nonce)) {
throw new Error('Nonce already used');
}
this.usedNonces.add(challenge.nonce);
}
// Create the message to sign
const messageComponents = [
challenge.nonce,
challenge.timestamp.toString(),
this.did,
challenge.verifier_did || '',
(challenge.scope || []).join(',')
];
const message = messageComponents.join(':');
// Sign the challenge
const signature = await this.sign(message);
// Return the response
return {
did: this.did,
signature,
timestamp: now,
nonce: challenge.nonce,
publicKey: this.publicKey
};
}
/**
* Get MCP-I capabilities for advertisement
*/
getCapabilities() {
const activeRegistries = this.registries.filter(r => r.status === 'active');
const secondaryRegistries = activeRegistries
.filter(r => r.type === 'secondary')
.map(r => r.name);
return {
version: '1.0',
did: this.did,
publicKey: this.publicKey,
conformanceLevel: 2, // Level 2: Full crypto with challenge-response
handshakeSupported: true,
handshakeEndpoint: '/_mcp-i/handshake',
verificationEndpoint: `https://${this.didHost === 'knowthat' ? 'knowthat.ai' : this.didHost}/api/agents/${this.did}/verify`,
registries: {
primary: this.didHost,
secondary: secondaryRegistries
}
};
}
/**
* Sign an MCP response with identity metadata
*/
async signResponse(response) {
const timestamp = new Date().toISOString();
const responseWithIdentity = {
...response,
_mcp_identity: {
did: this.did,
signature: '', // Will be filled below
timestamp,
conformanceLevel: 2
}
};
// Sign the response content (excluding the signature field)
const contentToSign = JSON.stringify({
...response,
_mcp_identity: {
did: this.did,
timestamp,
conformanceLevel: 2
}
});
responseWithIdentity._mcp_identity.signature = await this.sign(contentToSign);
return responseWithIdentity;
}
/**
* Generate a new nonce for challenges
*/
static generateNonce() {
return crypto.generateNonce();
}
/**
* Request edit access to agent profile
* Returns a signed URL for editing the agent on the registry
*/
async requestEditAccess(registryName) {
const registry = registryName || this.didHost;
const timestamp = Date.now();
const message = `edit-request:${this.did}:${registry}:${timestamp}`;
const signature = await this.sign(message);
// Construct edit URL with proof of ownership
const baseUrl = registry === 'knowthat'
? 'https://knowthat.ai'
: `https://${registry}`;
const editUrl = new URL(`${baseUrl}/agents/edit`);
editUrl.searchParams.set('did', this.did);
editUrl.searchParams.set('timestamp', timestamp.toString());
editUrl.searchParams.set('signature', signature);
return editUrl.toString();
}
/**
* Clean up old nonces periodically to prevent memory leaks
*/
startNonceCleanup() {
// Clean up nonces older than 2x the timestamp tolerance
this.nonceCleanupInterval = setInterval(() => {
// In a production system, you'd track nonce timestamps
// For now, we'll clear all nonces periodically
if (this.usedNonces.size > 10000) {
this.usedNonces.clear();
}
}, this.timestampTolerance * 2);
}
/**
* Clean up resources
*/
destroy() {
if (this.nonceCleanupInterval) {
clearInterval(this.nonceCleanupInterval);
}
this.usedNonces.clear();
}
/**
* Helper to extract agent name from DID
*/
extractAgentName() {
// Try to get from persisted data or environment
return process.env.MCP_SERVER_NAME || 'Unknown Agent';
}
/**
* Helper to extract agent ID
*/
extractAgentId() {
return process.env.AGENT_ID || '';
}
/**
* Helper to extract agent slug
*/
extractAgentSlug() {
const parts = this.did.split(':');
return parts[parts.length - 1];
}
/**
* Persist updated registry status
*/
async persistRegistryStatus() {
try {
const filePath = path.join(process.cwd(), '.mcp-identity.json');
const current = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
current.registries = this.registries;
fs.writeFileSync(filePath, JSON.stringify(current, null, 2));
}
catch (error) {
console.error('[MCP-I] Failed to persist registry status:', error);
}
}
}
exports.MCPIdentity = MCPIdentity;
/**
* Enable MCP Identity for any MCP server
* This is the main integration point that patches the MCP Server
*/
async function enableMCPIdentity(options) {
const identity = await MCPIdentity.init(options);
// Try to patch MCP Server if available
try {
patchMCPServer(identity);
}
catch (error) {
console.log('[MCP-I] MCP Server not found, identity initialized for manual use');
}
return identity;
}
/**
* Patch the MCP Server to automatically add identity
*/
function patchMCPServer(identity) {
try {
// Try to import the MCP SDK
const MCPModule = require('@modelcontextprotocol/sdk/server/index.js');
const OriginalServer = MCPModule.Server;
if (!OriginalServer) {
return;
}
// Store original methods
const originalSetRequestHandler = OriginalServer.prototype.setRequestHandler;
const originalConnect = OriginalServer.prototype.connect;
// Patch setRequestHandler to wrap all responses
OriginalServer.prototype.setRequestHandler = function (method, handler) {
const wrappedHandler = async (...args) => {
const result = await handler(...args);
// If result has content, sign it
if (result && typeof result === 'object' && 'content' in result) {
const signed = await identity.signResponse(result);
return signed;
}
return result;
};
return originalSetRequestHandler.call(this, method, wrappedHandler);
};
// Patch connect to advertise MCP-I capabilities
OriginalServer.prototype.connect = async function (transport) {
// Add MCP-I capabilities to server info
if (this.serverInfo && this.serverInfo.capabilities) {
this.serverInfo.capabilities['mcp-i'] = identity.getCapabilities();
}
// Set up MCP-I handshake handler
this.setRequestHandler('mcp-i/challenge', async (request) => {
return identity.respondToChallenge(request.params);
});
// Call original connect
return originalConnect.call(this, transport);
};
console.log('[MCP-I] ✨ MCP Server patched - all responses will be automatically signed');
}
catch (error) {
// MCP SDK not available, that's okay
}
}
// Helper functions (inline to keep package standalone)
async function loadIdentity(customPath) {
// Check environment variables first (already loaded by process)
if (process.env.AGENT_DID && process.env.AGENT_PUBLIC_KEY && process.env.AGENT_PRIVATE_KEY) {
const identity = {
did: process.env.AGENT_DID,
publicKey: process.env.AGENT_PUBLIC_KEY,
privateKey: process.env.AGENT_PRIVATE_KEY,
agentId: process.env.AGENT_ID || '',
agentSlug: process.env.AGENT_SLUG || '',
registeredAt: new Date().toISOString(),
didHost: process.env.AGENT_DID_HOST || 'knowthat'
};
// Try to load registry status from file
try {
const filePath = customPath || path.join(process.cwd(), '.mcp-identity.json');
if (fs.existsSync(filePath)) {
const fileContent = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
if (fileContent.registries) {
identity.registries = fileContent.registries;
}
}
}
catch {
// Ignore errors
}
return identity;
}
// Check JSON file (most reliable for programmatic access)
const filePath = customPath || path.join(process.cwd(), '.mcp-identity.json');
try {
if (fs.existsSync(filePath)) {
const content = fs.readFileSync(filePath, 'utf-8');
return JSON.parse(content);
}
}
catch {
// Ignore errors
}
return null;
}
async function saveIdentity(identity, customPath) {
// Save as JSON for programmatic access
const filePath = customPath || path.join(process.cwd(), '.mcp-identity.json');
fs.writeFileSync(filePath, JSON.stringify(identity, null, 2));
console.log('[MCP-I] Identity saved to .mcp-identity.json');
}
async function autoRegister(options) {
try {
const response = await axios_1.default.post(`${options.apiEndpoint}/api/agents/auto-register`, {
metadata: {
name: options.name,
description: options.description,
repository: options.repository,
version: '1.0.0'
},
clientInfo: {
sdkVersion: '0.2.0',
language: 'typescript',
platform: 'node' // Always node for MCP servers
}
}, {
timeout: 30000,
headers: {
'Content-Type': 'application/json',
'User-Agent': '@kya-os/mcp-i/0.2.0'
}
});
return response.data;
}
catch (error) {
if (error.response?.status === 429) {
throw new Error('Rate limit exceeded. Please try again later.');
}
throw new Error(error.response?.data?.message ||
error.message ||
'Failed to auto-register agent');
}
}
//# sourceMappingURL=index.js.map