@kya-os/mcp-i
Version:
COMING SOON:Production-ready MCP Identity with automatic registration, key rotation, and optimized performance
853 lines • 33.2 kB
JavaScript
/**
* @kya-os/mcp-i - Optimized MCP Identity with production features
*
* 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
* ```
*/
import * as crypto from './crypto.js';
import { StorageFactory } from './storage.js';
import { generateClientInfo } from './platform-info.js';
import { TransportFactory } from './transport.js';
import { LoggerFactory, getLogger } from './logger.js';
import { KeyRotationManager } from './rotation.js';
import { loadIdentityFromEnv, showVercelDeveloperInstructions } from './vercel-adapter.js';
import { pollRegistrationStatus, isAsyncRegistrationResponse } from './polling.js';
// Re-export types and utilities
export * from './types.js';
export * from './vercel-adapter.js';
export { RegistryFactory, REGISTRY_TIERS, resolveRegistries } from './registry/index.js';
export { LoggerFactory, ConsoleLogger, SilentLogger } from './logger.js';
export { StorageFactory, MemoryStorage, FileStorage } from './storage.js';
export { TransportFactory, RuntimeDetector } from './transport.js';
export { KeyRotationManager } from './rotation.js';
export { initWithDevExperience, showAgentStatus } from './dev-helper.js';
export { pollRegistrationStatus, isAsyncRegistrationResponse } from './polling.js';
// Global identity instance
let globalIdentity = null;
export class MCPIdentity {
did;
publicKey;
privateKey;
timestampTolerance;
enableNonceTracking;
usedNonces = new Set();
nonceCleanupInterval;
encryptionPassword;
decryptedPrivateKey;
// Directory preferences
directories;
// Optimized storage
storage;
transport;
logger;
// Key rotation
rotationManager;
// Precomputed values
precomputed;
constructor(identity, options = {}) {
// Initialize logger first
if (options.logger) {
LoggerFactory.setLogger(options.logger);
}
else if (options.logLevel && options.logLevel !== 'silent') {
LoggerFactory.setLogger(LoggerFactory.createConsoleLogger(options.logLevel));
}
this.logger = getLogger();
// Core identity
this.did = identity.did;
this.publicKey = identity.publicKey;
this.privateKey = identity.privateKey;
// Store encryption password for later decryption
this.encryptionPassword = options.encryptionPassword;
// Security options
this.timestampTolerance = options.timestampTolerance || 60000; // 60 seconds
this.enableNonceTracking = options.enableNonceTracking !== false;
// Directory preferences
this.directories = identity.directories || 'verified';
// Initialize storage and transport - allow passing instances for testing
this.storage = (typeof options?.storage === 'object' && 'load' in options.storage)
? options.storage
: StorageFactory.create({
storage: options.storage,
customPath: options.persistencePath,
memoryKey: options.memoryKey || this.did,
encryptionPassword: options.encryptionPassword
});
this.transport = (typeof options?.transport === 'object' && 'post' in options.transport)
? options.transport
: TransportFactory.create({
transport: options.transport
});
// Precompute values for performance
this.precomputed = {
did: this.did,
publicKey: this.publicKey,
didBytes: new TextEncoder().encode(this.did),
signatureCache: new Map()
};
// Initialize key rotation if not in memory storage
if (options.storage !== 'memory') {
this.rotationManager = new KeyRotationManager(identity, this.transport, {});
}
// Start nonce cleanup if tracking is enabled
if (this.enableNonceTracking) {
this.startNonceCleanup();
}
}
/**
* Initialize MCP Identity - the main entry point
*/
static async init(options) {
// Configure logger based on logLevel before any logging
const logger = options?.logger || getLogger(options?.logLevel);
// Return existing global identity if already initialized
if (globalIdentity) {
return globalIdentity;
}
// Check for Vercel/serverless environment
const isVercel = process.env.VERCEL || process.env.VERCEL_ENV;
const isServerless = isVercel || process.env.AWS_LAMBDA_FUNCTION_NAME || process.env.FUNCTIONS_WORKER_RUNTIME;
// Emit progress event
options?.onProgress?.({
stage: 'checking_existing',
progress: 10,
message: 'Checking for existing identity...'
});
// Try to load from environment variables first (for serverless)
let identity = null;
if (isServerless || options?.storage === 'memory') {
identity = loadIdentityFromEnv();
if (identity) {
logger.info('✅ Loaded existing identity from environment variables');
options?.onProgress?.({
stage: 'complete',
progress: 100,
message: 'Loaded existing identity from environment',
data: { did: identity.did }
});
}
}
// Initialize storage - allow passing storage instance for testing
const storage = (typeof options?.storage === 'object' && 'load' in options.storage)
? options.storage
: StorageFactory.create({
storage: options?.storage,
customPath: options?.persistencePath,
memoryKey: options?.memoryKey,
encryptionPassword: options?.encryptionPassword
});
// Try to load from storage if not found in env
if (!identity) {
identity = await storage.load();
}
// Handle existing identity
if (identity) {
logger.info('Loaded existing identity:', identity.did);
options?.onProgress?.({
stage: 'complete',
progress: 100,
message: 'Loaded existing identity',
data: { did: identity.did }
});
globalIdentity = new MCPIdentity(identity, options);
return globalIdentity;
}
// No existing identity - need to create new one
logger.info('No existing identity found, creating new identity...');
// Initialize transport for registration - allow passing transport instance for testing
const transport = (typeof options?.transport === 'object' && 'post' in options.transport)
? options.transport
: TransportFactory.create({
transport: options?.transport
});
// Always use knowthat.ai as the registry
const apiEndpoint = options?.apiEndpoint || 'https://knowthat.ai';
logger.info('Registering with knowthat.ai...');
// Generate keys first
logger.info('Generating cryptographic keys...');
options?.onProgress?.({
stage: 'generating_keys',
progress: 30,
message: 'Generating cryptographic keys...'
});
const keyPair = await crypto.generateKeyPair();
options?.onProgress?.({
stage: 'generating_keys',
progress: 50,
message: 'Keys generated successfully'
});
// Prepare registration data with public key
const registrationData = {
name: options?.name || process.env.MCP_SERVER_NAME || 'Unnamed MCP Server',
description: options?.description,
repository: options?.repository,
publicKey: keyPair.publicKey,
directories: options?.directories,
isDraft: options?.mode !== 'production' // Default to draft mode for safety
};
logger.debug('Registration data:', {
name: registrationData.name,
hasDescription: !!registrationData.description,
hasRepository: !!registrationData.repository,
hasPublicKey: !!registrationData.publicKey,
directories: registrationData.directories,
isDraft: registrationData.isDraft
});
let response;
try {
// Register with knowthat.ai
options?.onProgress?.({
stage: 'registering',
progress: 60,
message: 'Registering with KYA-OS network...'
});
response = await autoRegister(transport, {
...registrationData,
apiEndpoint,
directories: options?.directories,
processingMode: options?.processingMode,
registryEndpoint: options?.registryEndpoint,
onProgress: options?.onProgress
});
options?.onProgress?.({
stage: 'registering',
progress: 80,
message: 'Registration successful'
});
}
catch (registrationError) {
logger.error('Failed to register with knowthat.ai:', registrationError.message);
// For development/testing, allow offline mode with a temporary DID
if (options?.mode === 'development' || process.env.NODE_ENV === 'development') {
logger.warn('Running in offline development mode with temporary identity');
// Generate a temporary development DID
const tempSlug = `temp-${Date.now()}-${Math.random().toString(36).substring(7)}`;
response = {
did: `did:web:localhost:agents:${tempSlug}`,
agent: {
id: tempSlug,
slug: tempSlug,
name: registrationData.name,
url: `http://localhost:3000/agents/${tempSlug}`,
claimUrl: `http://localhost:3000/agents/claim?did=${tempSlug}`
},
keys: {
publicKey: keyPair.publicKey,
privateKey: keyPair.privateKey
}
};
logger.warn('⚠️ Using temporary development identity. This will not persist across restarts.');
logger.warn('⚠️ To use a permanent identity, ensure you can connect to knowthat.ai');
}
else {
// In production, registration is required
throw registrationError;
}
}
// 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(),
directories: options?.directories || 'verified'
};
// Save identity
options?.onProgress?.({
stage: 'saving',
progress: 90,
message: 'Saving identity...'
});
await storage.save(identity);
// Emit completion event with full data
options?.onProgress?.({
stage: 'complete',
progress: 100,
message: 'Identity created successfully',
data: {
did: identity.did,
publicKey: identity.publicKey,
agentId: identity.agentId,
agentSlug: identity.agentSlug,
claimUrl: response.agent.claimUrl
}
});
// Show enhanced developer instructions for Vercel/serverless
if (isServerless) {
showVercelDeveloperInstructions(identity, response.agent.claimUrl);
}
else {
logger.info('✅ Success! Your agent has been registered.');
logger.info(`DID: ${response.did}`);
logger.info(`Profile: ${response.agent.url}`);
// Show claim URL if in draft mode
if (response.agent.claimUrl) {
logger.info(`Claim your agent: ${response.agent.claimUrl}`);
}
// Show directory submission info
if (options?.directories && options.directories !== 'none') {
const dirMessage = options.directories === 'verified'
? 'Your agent will be submitted to all verified directories'
: `Your agent will be submitted to: ${options.directories.join(', ')}`;
logger.info(dirMessage);
}
}
// Create MCPIdentity instance
globalIdentity = new MCPIdentity(identity, options);
return globalIdentity;
}
/**
* Enable automatic key rotation
*/
async enableAutoRotation(policy) {
if (!this.rotationManager) {
throw new Error('Key rotation not available in memory storage mode');
}
this.rotationManager.setupAutoRotation((result) => {
if (result.success) {
this.logger.info('Keys rotated successfully');
// Update storage with new keys
this.persistIdentity();
}
else {
this.logger.error('Key rotation failed:', result.error);
}
});
}
/**
* Manually rotate keys
*/
async rotateKeys(reason) {
if (!this.rotationManager) {
throw new Error('Key rotation not available in memory storage mode');
}
const result = await this.rotationManager.rotateKeys(reason);
if (result.success) {
await this.persistIdentity();
}
return result;
}
/**
* Check key health
*/
checkKeyHealth() {
if (!this.rotationManager) {
return null;
}
return this.rotationManager.checkKeyHealth();
}
/**
* Get decrypted private key (lazy decryption)
*/
async getPrivateKey() {
if (this.decryptedPrivateKey) {
return this.decryptedPrivateKey;
}
if (this.encryptionPassword && this.privateKey.startsWith('enc:')) {
try {
this.decryptedPrivateKey = await crypto.decrypt(this.privateKey, this.encryptionPassword);
this.logger.debug('Private key decrypted successfully');
return this.decryptedPrivateKey;
}
catch (error) {
this.logger.error('Failed to decrypt private key:', error);
throw new Error('Invalid encryption password');
}
}
return this.privateKey;
}
/**
* Sign a message with caching
*/
async sign(message) {
const messageStr = typeof message === 'string' ? message : message.toString('base64');
// Check cache
const cached = this.precomputed.signatureCache.get(messageStr);
if (cached) {
return cached;
}
// Get decrypted private key
const privateKey = await this.getPrivateKey();
// Sign and cache
const signature = await crypto.sign(message, privateKey);
// Limit cache size
if (this.precomputed.signatureCache.size > 100) {
const firstKey = this.precomputed.signatureCache.keys().next().value;
if (firstKey) {
this.precomputed.signatureCache.delete(firstKey);
}
}
this.precomputed.signatureCache.set(messageStr, signature);
// Track signature count for rotation
if (this.rotationManager) {
this.rotationManager.incrementSignatureCount();
}
return signature;
}
/**
* Request edit access with claim URL support
*/
async requestEditAccess() {
const timestamp = Date.now();
const message = `edit-request:${this.did}:knowthat.ai:${timestamp}`;
const signature = await this.sign(message);
// Construct edit URL with proof of ownership
const baseUrl = 'https://knowthat.ai';
const editUrl = new URL(`${baseUrl}/agents/edit`);
editUrl.searchParams.set('did', this.did);
editUrl.searchParams.set('timestamp', timestamp.toString());
editUrl.searchParams.set('signature', signature);
// Also generate claim URL if agent is not yet claimed
const claimUrl = new URL(`${baseUrl}/agents/claim`);
claimUrl.searchParams.set('did', this.did);
claimUrl.searchParams.set('timestamp', timestamp.toString());
claimUrl.searchParams.set('signature', signature);
return {
editUrl: editUrl.toString(),
claimUrl: claimUrl.toString()
};
}
/**
* Verify a signature
*/
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() {
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://knowthat.ai/api/agents/${this.did}/verify`,
registry: 'knowthat.ai'
};
}
/**
* 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.generateNonceSync();
}
/**
* Get directory preferences
*/
getDirectories() {
return this.directories;
}
/**
* 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();
this.precomputed.signatureCache.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 full identity (for key rotation)
*/
async persistIdentity() {
try {
const identity = {
did: this.did,
publicKey: this.publicKey,
privateKey: this.privateKey,
agentId: this.extractAgentId(),
agentSlug: this.extractAgentSlug(),
registeredAt: new Date().toISOString(),
directories: this.directories
};
await this.storage.save(identity);
}
catch (error) {
this.logger.error('Failed to persist identity:', error);
}
}
}
/**
* Enable MCP Identity for any MCP server
*/
export async function enableMCPIdentity(options) {
const identity = await MCPIdentity.init(options);
// Try to patch MCP Server if available
try {
patchMCPServer(identity);
}
catch (error) {
const logger = getLogger();
logger.debug('MCP Server not found, identity initialized for manual use');
}
return identity;
}
/**
* Create MCP middleware for manual integration
*/
export function createMCPMiddleware(identity) {
return (server) => {
// Validate server object
if (!server || typeof server !== 'object') {
const logger = getLogger();
logger.warn('Invalid MCP Server object passed to middleware');
return;
}
// Check if methods exist before patching
if (!server.setRequestHandler || typeof server.setRequestHandler !== 'function') {
const logger = getLogger();
logger.warn('MCP Server missing setRequestHandler method');
return;
}
// Store original methods
const originalSetRequestHandler = server.setRequestHandler.bind(server);
const originalConnect = server.connect ? server.connect.bind(server) : null;
// Patch setRequestHandler to wrap all responses
server.setRequestHandler = function (method, handler) {
// Add defensive check for undefined method
if (!method || typeof method !== 'string') {
const logger = getLogger();
logger.warn('setRequestHandler called with invalid method:', method);
return originalSetRequestHandler.call(this, 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) {
return await identity.signResponse(result);
}
return result;
};
return originalSetRequestHandler(method, wrappedHandler);
};
// Patch connect to advertise MCP-I capabilities
if (originalConnect) {
server.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);
};
}
};
}
/**
* 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;
}
// Apply middleware
const middleware = createMCPMiddleware(identity);
// Patch the constructor
const OriginalConstructor = OriginalServer;
MCPModule.Server = function (...args) {
const instance = new OriginalConstructor(...args);
try {
middleware(instance, identity);
}
catch (error) {
const logger = getLogger();
logger.warn('Failed to apply MCP-I middleware:', error);
}
return instance;
};
// Copy static properties
Object.setPrototypeOf(MCPModule.Server, OriginalConstructor);
Object.setPrototypeOf(MCPModule.Server.prototype, OriginalConstructor.prototype);
const logger = getLogger();
logger.info('✨ MCP Server patched - all responses will be automatically signed');
}
catch (error) {
// MCP SDK not available, that's okay
}
}
// Helper function to determine which endpoint to use
function determineEndpoint(options) {
// Explicit configuration takes precedence
if (options.registryEndpoint === 'cli')
return true;
if (options.registryEndpoint === 'auto-register')
return false;
// Auto mode: use CLI endpoint if we're in a context that suggests CLI usage
if (options.registryEndpoint === 'auto' || !options.registryEndpoint) {
// Check if we have a progress callback (indicates CLI usage)
if (options.onProgress)
return true;
// Check if processingMode is explicitly set to sync (CLI preference)
if (options.processingMode === 'sync')
return true;
// Check if we're in a TTY environment (interactive terminal)
if (process.stdout?.isTTY)
return true;
}
return false;
}
// Helper function for CLI registration
async function registerViaCLI(transport, options) {
const logger = getLogger();
logger.debug('Using fast CLI registration endpoint');
const response = await transport.post(`${options.apiEndpoint}/api/agents/cli-register`, {
name: options.name,
description: options.description,
repository: options.repository,
publicKey: options.publicKey
}, {
timeout: 5000, // 5 second timeout for CLI endpoint
headers: {
'Content-Type': 'application/json',
'User-Agent': `-os/mcp-i/${generateClientInfo().sdkVersion}`
}
});
logger.debug(`CLI registration completed in ${response.data.responseTime}ms`);
// Report progress
options.onProgress?.({
stage: 'registering',
progress: 80,
message: 'Registration successful',
data: {
did: response.data.did,
claimUrl: response.data.claimUrl
}
});
// Transform to match AutoRegisterResponse format
return {
success: response.data.success,
did: response.data.did,
agent: {
id: response.data.agent.id,
slug: response.data.agent.slug,
name: response.data.agent.name,
url: response.data.agent.url,
claimUrl: response.data.claimUrl
},
keys: response.data.keys || {
publicKey: options.publicKey || '',
privateKey: undefined
}
};
}
// Helper function for auto-registration (now supports async v2.0 API and CLI endpoint)
async function autoRegister(transport, options) {
const logger = getLogger();
// Determine which endpoint to use
const shouldUseCLI = determineEndpoint(options);
// Try CLI endpoint first if appropriate
if (shouldUseCLI) {
try {
return await registerViaCLI(transport, options);
}
catch (error) {
logger.debug(`CLI registration failed: ${error.message}, falling back to auto-register`);
// Fall through to auto-register
}
}
// Use auto-register endpoint
try {
const requestBody = {
metadata: {
name: options.name,
description: options.description,
repository: options.repository,
publicKey: options.publicKey,
version: '1.0.0',
isDraft: options.isDraft,
directories: options.directories || 'verified' // Default to all verified directories
},
clientInfo: generateClientInfo({
processingMode: options.processingMode === 'auto' ? undefined : options.processingMode,
customMetadata: {
source: 'auto-register'
}
})
};
logger.debug('Sending registration request to:', `${options.apiEndpoint}/api/agents/auto-register`);
const response = await transport.post(`${options.apiEndpoint}/api/agents/auto-register`, requestBody, {
timeout: 30000,
headers: {
'Content-Type': 'application/json',
'User-Agent': `-os/mcp-i/${requestBody.clientInfo.sdkVersion}`
}
});
logger.debug('Registration response received:', {
status: response.status,
hasData: !!response.data,
isAsync: response.status === 202
});
// Check if we got an async response (v2.0 API)
if (response.status === 202 && isAsyncRegistrationResponse(response.data)) {
logger.info('Registration submitted, waiting for completion...');
// Report progress
options.onProgress?.({
stage: 'registering',
progress: 70,
message: 'Registration submitted, processing...',
data: { jobId: response.data.jobId }
});
// Poll for completion using simple 2-second intervals
const result = await pollRegistrationStatus(response.data.status, // Status URL
transport, {
pollInterval: 2000, // Fixed 2-second intervals
maxPollingTime: 60000, // 60 seconds max
logger,
onProgress: (message, progress) => {
// Update progress during polling
options.onProgress?.({
stage: 'registering',
progress: 70 + (progress || 0) * 0.1, // 70-80% during polling
message
});
}
});
logger.debug('Registration completed after polling:', {
hasDid: !!result.did,
hasAgent: !!result.agent
});
return result;
}
// Handle sync response (backward compatibility)
logger.debug('Got synchronous registration response');
return response.data;
}
catch (error) {
logger.error('Registration failed:', {
message: error.message,
status: error.status,
response: error.response
});
if (error.message?.includes('429')) {
throw new Error('Rate limit exceeded. Please try again later.');
}
// Provide more helpful error messages
if (error.message?.includes('500')) {
throw new Error('Server error during registration. This might be a temporary issue.\n' +
'Please try again in a few moments, or register manually at https://knowthat.ai/submit-agent');
}
if (error.message?.includes('400')) {
throw new Error('Invalid registration data. Please ensure all required fields are provided:\n' +
'- name: A unique name for your agent\n' +
'- publicKey: Generated automatically (check if generation succeeded)');
}
throw new Error(error.message || 'Failed to auto-register agent');
}
}
//# sourceMappingURL=index.js.map