UNPKG

@kya-os/mcp-i

Version:

COMING SOON:Production-ready MCP Identity with automatic registration, key rotation, and optimized performance

728 lines (727 loc) 30.9 kB
"use strict"; 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); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.MCPIdentity = exports.isAsyncRegistrationResponse = exports.pollRegistrationStatus = exports.showAgentStatus = exports.initWithDevExperience = exports.KeyRotationManager = exports.RuntimeDetector = exports.TransportFactory = exports.FileStorage = exports.MemoryStorage = exports.StorageFactory = exports.SilentLogger = exports.ConsoleLogger = exports.LoggerFactory = exports.resolveRegistries = exports.REGISTRY_TIERS = exports.RegistryFactory = void 0; exports.enableMCPIdentity = enableMCPIdentity; exports.createMCPMiddleware = createMCPMiddleware; const crypto = __importStar(require("./crypto")); const storage_1 = require("./storage"); const platform_info_1 = require("./platform-info"); const transport_1 = require("./transport"); const logger_1 = require("./logger"); const rotation_1 = require("./rotation"); const vercel_adapter_1 = require("./vercel-adapter"); const polling_1 = require("./polling"); __exportStar(require("./types"), exports); __exportStar(require("./vercel-adapter"), exports); var registry_1 = require("./registry"); Object.defineProperty(exports, "RegistryFactory", { enumerable: true, get: function () { return registry_1.RegistryFactory; } }); Object.defineProperty(exports, "REGISTRY_TIERS", { enumerable: true, get: function () { return registry_1.REGISTRY_TIERS; } }); Object.defineProperty(exports, "resolveRegistries", { enumerable: true, get: function () { return registry_1.resolveRegistries; } }); var logger_2 = require("./logger"); Object.defineProperty(exports, "LoggerFactory", { enumerable: true, get: function () { return logger_2.LoggerFactory; } }); Object.defineProperty(exports, "ConsoleLogger", { enumerable: true, get: function () { return logger_2.ConsoleLogger; } }); Object.defineProperty(exports, "SilentLogger", { enumerable: true, get: function () { return logger_2.SilentLogger; } }); var storage_2 = require("./storage"); Object.defineProperty(exports, "StorageFactory", { enumerable: true, get: function () { return storage_2.StorageFactory; } }); Object.defineProperty(exports, "MemoryStorage", { enumerable: true, get: function () { return storage_2.MemoryStorage; } }); Object.defineProperty(exports, "FileStorage", { enumerable: true, get: function () { return storage_2.FileStorage; } }); var transport_2 = require("./transport"); Object.defineProperty(exports, "TransportFactory", { enumerable: true, get: function () { return transport_2.TransportFactory; } }); Object.defineProperty(exports, "RuntimeDetector", { enumerable: true, get: function () { return transport_2.RuntimeDetector; } }); var rotation_2 = require("./rotation"); Object.defineProperty(exports, "KeyRotationManager", { enumerable: true, get: function () { return rotation_2.KeyRotationManager; } }); var dev_helper_1 = require("./dev-helper"); Object.defineProperty(exports, "initWithDevExperience", { enumerable: true, get: function () { return dev_helper_1.initWithDevExperience; } }); Object.defineProperty(exports, "showAgentStatus", { enumerable: true, get: function () { return dev_helper_1.showAgentStatus; } }); var polling_2 = require("./polling"); Object.defineProperty(exports, "pollRegistrationStatus", { enumerable: true, get: function () { return polling_2.pollRegistrationStatus; } }); Object.defineProperty(exports, "isAsyncRegistrationResponse", { enumerable: true, get: function () { return polling_2.isAsyncRegistrationResponse; } }); let globalIdentity = null; class MCPIdentity { constructor(identity, options = {}) { this.usedNonces = new Set(); if (options.logger) { logger_1.LoggerFactory.setLogger(options.logger); } else if (options.logLevel && options.logLevel !== 'silent') { logger_1.LoggerFactory.setLogger(logger_1.LoggerFactory.createConsoleLogger(options.logLevel)); } this.logger = (0, logger_1.getLogger)(); this.did = identity.did; this.publicKey = identity.publicKey; this.privateKey = identity.privateKey; this.encryptionPassword = options.encryptionPassword; this.timestampTolerance = options.timestampTolerance || 60000; this.enableNonceTracking = options.enableNonceTracking !== false; this.directories = identity.directories || 'verified'; this.storage = (typeof options?.storage === 'object' && 'load' in options.storage) ? options.storage : storage_1.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 : transport_1.TransportFactory.create({ transport: options.transport }); this.precomputed = { did: this.did, publicKey: this.publicKey, didBytes: new TextEncoder().encode(this.did), signatureCache: new Map() }; if (options.storage !== 'memory') { this.rotationManager = new rotation_1.KeyRotationManager(identity, this.transport, {}); } if (this.enableNonceTracking) { this.startNonceCleanup(); } } static async init(options) { const logger = options?.logger || (0, logger_1.getLogger)(options?.logLevel); if (globalIdentity) { return globalIdentity; } const isVercel = process.env.VERCEL || process.env.VERCEL_ENV; const isServerless = isVercel || process.env.AWS_LAMBDA_FUNCTION_NAME || process.env.FUNCTIONS_WORKER_RUNTIME; options?.onProgress?.({ stage: 'checking_existing', progress: 10, message: 'Checking for existing identity...' }); let identity = null; if (isServerless || options?.storage === 'memory') { identity = (0, vercel_adapter_1.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 } }); } } const storage = (typeof options?.storage === 'object' && 'load' in options.storage) ? options.storage : storage_1.StorageFactory.create({ storage: options?.storage, customPath: options?.persistencePath, memoryKey: options?.memoryKey, encryptionPassword: options?.encryptionPassword }); if (!identity) { identity = await storage.load(); } 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; } logger.info('No existing identity found, creating new identity...'); const transport = (typeof options?.transport === 'object' && 'post' in options.transport) ? options.transport : transport_1.TransportFactory.create({ transport: options?.transport }); const apiEndpoint = options?.apiEndpoint || 'https://knowthat.ai'; logger.info('Registering with knowthat.ai...'); 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' }); 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' }; 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 { 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); if (options?.mode === 'development' || process.env.NODE_ENV === 'development') { logger.warn('Running in offline development mode with temporary identity'); 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 { throw registrationError; } } 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' }; options?.onProgress?.({ stage: 'saving', progress: 90, message: 'Saving identity...' }); await storage.save(identity); 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 } }); if (isServerless) { (0, vercel_adapter_1.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}`); if (response.agent.claimUrl) { logger.info(`Claim your agent: ${response.agent.claimUrl}`); } 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); } } globalIdentity = new MCPIdentity(identity, options); return globalIdentity; } 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'); this.persistIdentity(); } else { this.logger.error('Key rotation failed:', result.error); } }); } 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; } checkKeyHealth() { if (!this.rotationManager) { return null; } return this.rotationManager.checkKeyHealth(); } 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; } async sign(message) { const messageStr = typeof message === 'string' ? message : message.toString('base64'); const cached = this.precomputed.signatureCache.get(messageStr); if (cached) { return cached; } const privateKey = await this.getPrivateKey(); const signature = await crypto.sign(message, privateKey); 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); if (this.rotationManager) { this.rotationManager.incrementSignatureCount(); } return signature; } async requestEditAccess() { const timestamp = Date.now(); const message = `edit-request:${this.did}:knowthat.ai:${timestamp}`; const signature = await this.sign(message); 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); 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() }; } async verify(message, signature, publicKey) { return crypto.verify(message, signature, publicKey || this.publicKey); } async respondToChallenge(challenge) { 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'); } if (this.enableNonceTracking) { if (this.usedNonces.has(challenge.nonce)) { throw new Error('Nonce already used'); } this.usedNonces.add(challenge.nonce); } const messageComponents = [ challenge.nonce, challenge.timestamp.toString(), this.did, challenge.verifier_did || '', (challenge.scope || []).join(',') ]; const message = messageComponents.join(':'); const signature = await this.sign(message); return { did: this.did, signature, timestamp: now, nonce: challenge.nonce, publicKey: this.publicKey }; } getCapabilities() { return { version: '1.0', did: this.did, publicKey: this.publicKey, conformanceLevel: 2, handshakeSupported: true, handshakeEndpoint: '/_mcp-i/handshake', verificationEndpoint: `https://knowthat.ai/api/agents/${this.did}/verify`, registry: 'knowthat.ai' }; } async signResponse(response) { const timestamp = new Date().toISOString(); const responseWithIdentity = { ...response, _mcp_identity: { did: this.did, signature: '', timestamp, conformanceLevel: 2 } }; const contentToSign = JSON.stringify({ ...response, _mcp_identity: { did: this.did, timestamp, conformanceLevel: 2 } }); responseWithIdentity._mcp_identity.signature = await this.sign(contentToSign); return responseWithIdentity; } static generateNonce() { return crypto.generateNonceSync(); } getDirectories() { return this.directories; } startNonceCleanup() { this.nonceCleanupInterval = setInterval(() => { if (this.usedNonces.size > 10000) { this.usedNonces.clear(); } }, this.timestampTolerance * 2); } destroy() { if (this.nonceCleanupInterval) { clearInterval(this.nonceCleanupInterval); } this.usedNonces.clear(); this.precomputed.signatureCache.clear(); } extractAgentName() { return process.env.MCP_SERVER_NAME || 'Unknown Agent'; } extractAgentId() { return process.env.AGENT_ID || ''; } extractAgentSlug() { const parts = this.did.split(':'); return parts[parts.length - 1]; } 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); } } } exports.MCPIdentity = MCPIdentity; async function enableMCPIdentity(options) { const identity = await MCPIdentity.init(options); try { patchMCPServer(identity); } catch (error) { const logger = (0, logger_1.getLogger)(); logger.debug('MCP Server not found, identity initialized for manual use'); } return identity; } function createMCPMiddleware(identity) { return (server) => { if (!server || typeof server !== 'object') { const logger = (0, logger_1.getLogger)(); logger.warn('Invalid MCP Server object passed to middleware'); return; } if (!server.setRequestHandler || typeof server.setRequestHandler !== 'function') { const logger = (0, logger_1.getLogger)(); logger.warn('MCP Server missing setRequestHandler method'); return; } const originalSetRequestHandler = server.setRequestHandler.bind(server); const originalConnect = server.connect ? server.connect.bind(server) : null; server.setRequestHandler = function (method, handler) { if (!method || typeof method !== 'string') { const logger = (0, logger_1.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 && typeof result === 'object' && 'content' in result) { return await identity.signResponse(result); } return result; }; return originalSetRequestHandler(method, wrappedHandler); }; if (originalConnect) { server.connect = async function (transport) { if (this.serverInfo && this.serverInfo.capabilities) { this.serverInfo.capabilities['mcp-i'] = identity.getCapabilities(); } this.setRequestHandler('mcp-i/challenge', async (request) => { return identity.respondToChallenge(request.params); }); return originalConnect.call(this, transport); }; } }; } function patchMCPServer(identity) { try { const MCPModule = require('@modelcontextprotocol/sdk/server/index.js'); const OriginalServer = MCPModule.Server; if (!OriginalServer) { return; } const middleware = createMCPMiddleware(identity); const OriginalConstructor = OriginalServer; MCPModule.Server = function (...args) { const instance = new OriginalConstructor(...args); try { middleware(instance, identity); } catch (error) { const logger = (0, logger_1.getLogger)(); logger.warn('Failed to apply MCP-I middleware:', error); } return instance; }; Object.setPrototypeOf(MCPModule.Server, OriginalConstructor); Object.setPrototypeOf(MCPModule.Server.prototype, OriginalConstructor.prototype); const logger = (0, logger_1.getLogger)(); logger.info('✨ MCP Server patched - all responses will be automatically signed'); } catch (error) { } } function determineEndpoint(options) { if (options.registryEndpoint === 'cli') return true; if (options.registryEndpoint === 'auto-register') return false; if (options.registryEndpoint === 'auto' || !options.registryEndpoint) { if (options.onProgress) return true; if (options.processingMode === 'sync') return true; if (process.stdout?.isTTY) return true; } return false; } async function registerViaCLI(transport, options) { const logger = (0, logger_1.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, headers: { 'Content-Type': 'application/json', 'User-Agent': `@kya-os/mcp-i/${(0, platform_info_1.generateClientInfo)().sdkVersion}` } }); logger.debug(`CLI registration completed in ${response.data.responseTime}ms`); options.onProgress?.({ stage: 'registering', progress: 80, message: 'Registration successful', data: { did: response.data.did, claimUrl: response.data.claimUrl } }); 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 } }; } async function autoRegister(transport, options) { const logger = (0, logger_1.getLogger)(); const shouldUseCLI = determineEndpoint(options); if (shouldUseCLI) { try { return await registerViaCLI(transport, options); } catch (error) { logger.debug(`CLI registration failed: ${error.message}, falling back to auto-register`); } } 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' }, clientInfo: (0, platform_info_1.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': `@kya-os/mcp-i/${requestBody.clientInfo.sdkVersion}` } }); logger.debug('Registration response received:', { status: response.status, hasData: !!response.data, isAsync: response.status === 202 }); if (response.status === 202 && (0, polling_1.isAsyncRegistrationResponse)(response.data)) { logger.info('Registration submitted, waiting for completion...'); options.onProgress?.({ stage: 'registering', progress: 70, message: 'Registration submitted, processing...', data: { jobId: response.data.jobId } }); const result = await (0, polling_1.pollRegistrationStatus)(response.data.status, transport, { pollInterval: 2000, maxPollingTime: 60000, logger, onProgress: (message, progress) => { options.onProgress?.({ stage: 'registering', progress: 70 + (progress || 0) * 0.1, message }); } }); logger.debug('Registration completed after polling:', { hasDid: !!result.did, hasAgent: !!result.agent }); return result; } 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.'); } 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'); } }