UNPKG

@kya-os/mcp-bri

Version:

Give your MCP server cryptographic identity in 2 lines of code

629 lines 24.8 kB
"use strict"; /** * @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