@kya-os/mcp-i 0.1.0-alpha.2.9 → 0.1.0-alpha.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{crypto.js → cjs/crypto.js} +17 -2
- package/dist/esm/auto.d.ts +13 -0
- package/dist/esm/auto.d.ts.map +1 -0
- package/dist/esm/auto.js +30 -0
- package/dist/esm/auto.js.map +1 -0
- package/dist/esm/crypto.d.ts +51 -0
- package/dist/esm/crypto.d.ts.map +1 -0
- package/dist/esm/crypto.js +230 -0
- package/dist/esm/crypto.js.map +1 -0
- package/dist/esm/dev-helper.d.ts +15 -0
- package/dist/esm/dev-helper.d.ts.map +1 -0
- package/dist/esm/dev-helper.js +63 -0
- package/dist/esm/dev-helper.js.map +1 -0
- package/dist/esm/encrypted-storage.d.ts +19 -0
- package/dist/esm/encrypted-storage.d.ts.map +1 -0
- package/dist/esm/encrypted-storage.js +48 -0
- package/dist/esm/encrypted-storage.js.map +1 -0
- package/dist/esm/index.d.ts +128 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +671 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/logger.d.ts +46 -0
- package/dist/esm/logger.d.ts.map +1 -0
- package/dist/esm/logger.js +76 -0
- package/dist/esm/logger.js.map +1 -0
- package/dist/esm/nextjs.d.ts +22 -0
- package/dist/esm/nextjs.d.ts.map +1 -0
- package/dist/esm/nextjs.js +82 -0
- package/dist/esm/nextjs.js.map +1 -0
- package/dist/esm/package.json +1 -0
- package/dist/esm/registry/index.d.ts +43 -0
- package/dist/esm/registry/index.d.ts.map +1 -0
- package/dist/esm/registry/index.js +89 -0
- package/dist/esm/registry/index.js.map +1 -0
- package/dist/esm/registry/knowthat.d.ts +30 -0
- package/dist/esm/registry/knowthat.d.ts.map +1 -0
- package/dist/esm/registry/knowthat.js +106 -0
- package/dist/esm/registry/knowthat.js.map +1 -0
- package/dist/esm/rotation.d.ts +57 -0
- package/dist/esm/rotation.d.ts.map +1 -0
- package/dist/esm/rotation.js +133 -0
- package/dist/esm/rotation.js.map +1 -0
- package/dist/esm/storage.d.ts +65 -0
- package/dist/esm/storage.d.ts.map +1 -0
- package/dist/esm/storage.js +160 -0
- package/dist/esm/storage.js.map +1 -0
- package/dist/esm/transport.d.ts +52 -0
- package/dist/esm/transport.d.ts.map +1 -0
- package/dist/esm/transport.js +209 -0
- package/dist/esm/transport.js.map +1 -0
- package/dist/esm/types.d.ts +188 -0
- package/dist/esm/types.d.ts.map +1 -0
- package/dist/esm/types.js +5 -0
- package/dist/esm/types.js.map +1 -0
- package/dist/esm/vercel-adapter.d.ts +26 -0
- package/dist/esm/vercel-adapter.d.ts.map +1 -0
- package/dist/esm/vercel-adapter.js +80 -0
- package/dist/esm/vercel-adapter.js.map +1 -0
- package/package.json +26 -20
- /package/dist/{auto.d.ts → cjs/auto.d.ts} +0 -0
- /package/dist/{auto.js → cjs/auto.js} +0 -0
- /package/dist/{crypto.d.ts → cjs/crypto.d.ts} +0 -0
- /package/dist/{dev-helper.d.ts → cjs/dev-helper.d.ts} +0 -0
- /package/dist/{dev-helper.js → cjs/dev-helper.js} +0 -0
- /package/dist/{encrypted-storage.d.ts → cjs/encrypted-storage.d.ts} +0 -0
- /package/dist/{encrypted-storage.js → cjs/encrypted-storage.js} +0 -0
- /package/dist/{index.d.ts → cjs/index.d.ts} +0 -0
- /package/dist/{index.js → cjs/index.js} +0 -0
- /package/dist/{logger.d.ts → cjs/logger.d.ts} +0 -0
- /package/dist/{logger.js → cjs/logger.js} +0 -0
- /package/dist/{nextjs.d.ts → cjs/nextjs.d.ts} +0 -0
- /package/dist/{nextjs.js → cjs/nextjs.js} +0 -0
- /package/dist/{registry → cjs/registry}/index.d.ts +0 -0
- /package/dist/{registry → cjs/registry}/index.js +0 -0
- /package/dist/{registry → cjs/registry}/knowthat.d.ts +0 -0
- /package/dist/{registry → cjs/registry}/knowthat.js +0 -0
- /package/dist/{rotation.d.ts → cjs/rotation.d.ts} +0 -0
- /package/dist/{rotation.js → cjs/rotation.js} +0 -0
- /package/dist/{storage.d.ts → cjs/storage.d.ts} +0 -0
- /package/dist/{storage.js → cjs/storage.js} +0 -0
- /package/dist/{transport.d.ts → cjs/transport.d.ts} +0 -0
- /package/dist/{transport.js → cjs/transport.js} +0 -0
- /package/dist/{types.d.ts → cjs/types.d.ts} +0 -0
- /package/dist/{types.js → cjs/types.js} +0 -0
- /package/dist/{vercel-adapter.d.ts → cjs/vercel-adapter.d.ts} +0 -0
- /package/dist/{vercel-adapter.js → cjs/vercel-adapter.js} +0 -0
|
@@ -0,0 +1,671 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @kya-os/mcp-i - Optimized MCP Identity with production features
|
|
3
|
+
*
|
|
4
|
+
* Enable any MCP server to get a verifiable identity with just 2 lines of code:
|
|
5
|
+
*
|
|
6
|
+
* ```typescript
|
|
7
|
+
* import "@kya-os/mcp-i/auto"; // That's it! Your server now has identity
|
|
8
|
+
* ```
|
|
9
|
+
*/
|
|
10
|
+
import * as crypto from './crypto.js';
|
|
11
|
+
import { StorageFactory } from './storage.js';
|
|
12
|
+
import { TransportFactory } from './transport.js';
|
|
13
|
+
import { LoggerFactory, getLogger } from './logger.js';
|
|
14
|
+
import { KeyRotationManager } from './rotation.js';
|
|
15
|
+
import { loadIdentityFromEnv, showVercelDeveloperInstructions } from './vercel-adapter.js';
|
|
16
|
+
// Re-export types and utilities
|
|
17
|
+
export * from './types.js';
|
|
18
|
+
export * from './vercel-adapter.js';
|
|
19
|
+
export { RegistryFactory, REGISTRY_TIERS, resolveRegistries } from './registry/index.js';
|
|
20
|
+
export { LoggerFactory, ConsoleLogger, SilentLogger } from './logger.js';
|
|
21
|
+
export { StorageFactory, MemoryStorage, FileStorage } from './storage.js';
|
|
22
|
+
export { TransportFactory, RuntimeDetector } from './transport.js';
|
|
23
|
+
export { KeyRotationManager } from './rotation.js';
|
|
24
|
+
export { initWithDevExperience, showAgentStatus } from './dev-helper.js';
|
|
25
|
+
// Global identity instance
|
|
26
|
+
let globalIdentity = null;
|
|
27
|
+
export class MCPIdentity {
|
|
28
|
+
did;
|
|
29
|
+
publicKey;
|
|
30
|
+
privateKey;
|
|
31
|
+
timestampTolerance;
|
|
32
|
+
enableNonceTracking;
|
|
33
|
+
usedNonces = new Set();
|
|
34
|
+
nonceCleanupInterval;
|
|
35
|
+
encryptionPassword;
|
|
36
|
+
decryptedPrivateKey;
|
|
37
|
+
// Directory preferences
|
|
38
|
+
directories;
|
|
39
|
+
// Optimized storage
|
|
40
|
+
storage;
|
|
41
|
+
transport;
|
|
42
|
+
logger;
|
|
43
|
+
// Key rotation
|
|
44
|
+
rotationManager;
|
|
45
|
+
// Precomputed values
|
|
46
|
+
precomputed;
|
|
47
|
+
constructor(identity, options = {}) {
|
|
48
|
+
// Initialize logger first
|
|
49
|
+
if (options.logger) {
|
|
50
|
+
LoggerFactory.setLogger(options.logger);
|
|
51
|
+
}
|
|
52
|
+
else if (options.logLevel && options.logLevel !== 'silent') {
|
|
53
|
+
LoggerFactory.setLogger(LoggerFactory.createConsoleLogger(options.logLevel));
|
|
54
|
+
}
|
|
55
|
+
this.logger = getLogger();
|
|
56
|
+
// Core identity
|
|
57
|
+
this.did = identity.did;
|
|
58
|
+
this.publicKey = identity.publicKey;
|
|
59
|
+
this.privateKey = identity.privateKey;
|
|
60
|
+
// Store encryption password for later decryption
|
|
61
|
+
this.encryptionPassword = options.encryptionPassword;
|
|
62
|
+
// Security options
|
|
63
|
+
this.timestampTolerance = options.timestampTolerance || 60000; // 60 seconds
|
|
64
|
+
this.enableNonceTracking = options.enableNonceTracking !== false;
|
|
65
|
+
// Directory preferences
|
|
66
|
+
this.directories = identity.directories || 'verified';
|
|
67
|
+
// Initialize storage and transport
|
|
68
|
+
this.storage = StorageFactory.create({
|
|
69
|
+
storage: options.storage,
|
|
70
|
+
customPath: options.persistencePath,
|
|
71
|
+
memoryKey: options.memoryKey || this.did,
|
|
72
|
+
encryptionPassword: options.encryptionPassword
|
|
73
|
+
});
|
|
74
|
+
this.transport = TransportFactory.create({
|
|
75
|
+
transport: options.transport
|
|
76
|
+
});
|
|
77
|
+
// Precompute values for performance
|
|
78
|
+
this.precomputed = {
|
|
79
|
+
did: this.did,
|
|
80
|
+
publicKey: this.publicKey,
|
|
81
|
+
didBytes: new TextEncoder().encode(this.did),
|
|
82
|
+
signatureCache: new Map()
|
|
83
|
+
};
|
|
84
|
+
// Initialize key rotation if not in memory storage
|
|
85
|
+
if (options.storage !== 'memory') {
|
|
86
|
+
this.rotationManager = new KeyRotationManager(identity, this.transport, {});
|
|
87
|
+
}
|
|
88
|
+
// Start nonce cleanup if tracking is enabled
|
|
89
|
+
if (this.enableNonceTracking) {
|
|
90
|
+
this.startNonceCleanup();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Initialize MCP Identity - the main entry point
|
|
95
|
+
*/
|
|
96
|
+
static async init(options) {
|
|
97
|
+
const logger = options?.logger || getLogger();
|
|
98
|
+
// Return existing global identity if already initialized
|
|
99
|
+
if (globalIdentity) {
|
|
100
|
+
return globalIdentity;
|
|
101
|
+
}
|
|
102
|
+
// Check for Vercel/serverless environment
|
|
103
|
+
const isVercel = process.env.VERCEL || process.env.VERCEL_ENV;
|
|
104
|
+
const isServerless = isVercel || process.env.AWS_LAMBDA_FUNCTION_NAME || process.env.FUNCTIONS_WORKER_RUNTIME;
|
|
105
|
+
// Try to load from environment variables first (for serverless)
|
|
106
|
+
let identity = null;
|
|
107
|
+
if (isServerless || options?.storage === 'memory') {
|
|
108
|
+
identity = loadIdentityFromEnv();
|
|
109
|
+
if (identity) {
|
|
110
|
+
logger.info('✅ Loaded existing identity from environment variables');
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// Initialize storage
|
|
114
|
+
const storage = StorageFactory.create({
|
|
115
|
+
storage: options?.storage,
|
|
116
|
+
customPath: options?.persistencePath,
|
|
117
|
+
memoryKey: options?.memoryKey,
|
|
118
|
+
encryptionPassword: options?.encryptionPassword
|
|
119
|
+
});
|
|
120
|
+
// Try to load from storage if not found in env
|
|
121
|
+
if (!identity) {
|
|
122
|
+
identity = await storage.load();
|
|
123
|
+
}
|
|
124
|
+
// Handle existing identity
|
|
125
|
+
if (identity) {
|
|
126
|
+
logger.info('Loaded existing identity:', identity.did);
|
|
127
|
+
globalIdentity = new MCPIdentity(identity, options);
|
|
128
|
+
return globalIdentity;
|
|
129
|
+
}
|
|
130
|
+
// No existing identity - need to create new one
|
|
131
|
+
logger.info('No existing identity found, creating new identity...');
|
|
132
|
+
// Initialize transport for registration
|
|
133
|
+
const transport = TransportFactory.create({
|
|
134
|
+
transport: options?.transport
|
|
135
|
+
});
|
|
136
|
+
// Always use knowthat.ai as the registry
|
|
137
|
+
const apiEndpoint = options?.apiEndpoint || 'https://knowthat.ai';
|
|
138
|
+
logger.info('Registering with knowthat.ai...');
|
|
139
|
+
// Generate keys first
|
|
140
|
+
logger.info('Generating cryptographic keys...');
|
|
141
|
+
const keyPair = await crypto.generateKeyPair();
|
|
142
|
+
// Prepare registration data with public key
|
|
143
|
+
const registrationData = {
|
|
144
|
+
name: options?.name || process.env.MCP_SERVER_NAME || 'Unnamed MCP Server',
|
|
145
|
+
description: options?.description,
|
|
146
|
+
repository: options?.repository,
|
|
147
|
+
publicKey: keyPair.publicKey,
|
|
148
|
+
directories: options?.directories,
|
|
149
|
+
isDraft: options?.mode !== 'production' // Default to draft mode for safety
|
|
150
|
+
};
|
|
151
|
+
logger.debug('Registration data:', {
|
|
152
|
+
name: registrationData.name,
|
|
153
|
+
hasDescription: !!registrationData.description,
|
|
154
|
+
hasRepository: !!registrationData.repository,
|
|
155
|
+
hasPublicKey: !!registrationData.publicKey,
|
|
156
|
+
directories: registrationData.directories,
|
|
157
|
+
isDraft: registrationData.isDraft
|
|
158
|
+
});
|
|
159
|
+
let response;
|
|
160
|
+
try {
|
|
161
|
+
// Register with knowthat.ai
|
|
162
|
+
response = await autoRegister(transport, {
|
|
163
|
+
...registrationData,
|
|
164
|
+
apiEndpoint,
|
|
165
|
+
directories: options?.directories
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
catch (registrationError) {
|
|
169
|
+
logger.error('Failed to register with knowthat.ai:', registrationError.message);
|
|
170
|
+
// For development/testing, allow offline mode with a temporary DID
|
|
171
|
+
if (options?.mode === 'development' || process.env.NODE_ENV === 'development') {
|
|
172
|
+
logger.warn('Running in offline development mode with temporary identity');
|
|
173
|
+
// Generate a temporary development DID
|
|
174
|
+
const tempSlug = `temp-${Date.now()}-${Math.random().toString(36).substring(7)}`;
|
|
175
|
+
response = {
|
|
176
|
+
did: `did:web:localhost:agents:${tempSlug}`,
|
|
177
|
+
agent: {
|
|
178
|
+
id: tempSlug,
|
|
179
|
+
slug: tempSlug,
|
|
180
|
+
name: registrationData.name,
|
|
181
|
+
url: `http://localhost:3000/agents/${tempSlug}`,
|
|
182
|
+
claimUrl: `http://localhost:3000/agents/claim?did=${tempSlug}`
|
|
183
|
+
},
|
|
184
|
+
keys: {
|
|
185
|
+
publicKey: keyPair.publicKey,
|
|
186
|
+
privateKey: keyPair.privateKey
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
logger.warn('⚠️ Using temporary development identity. This will not persist across restarts.');
|
|
190
|
+
logger.warn('⚠️ To use a permanent identity, ensure you can connect to knowthat.ai');
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
// In production, registration is required
|
|
194
|
+
throw registrationError;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
// Create persisted identity
|
|
198
|
+
identity = {
|
|
199
|
+
did: response.did,
|
|
200
|
+
publicKey: keyPair.publicKey,
|
|
201
|
+
privateKey: keyPair.privateKey,
|
|
202
|
+
agentId: response.agent.id,
|
|
203
|
+
agentSlug: response.agent.slug,
|
|
204
|
+
registeredAt: new Date().toISOString(),
|
|
205
|
+
directories: options?.directories || 'verified'
|
|
206
|
+
};
|
|
207
|
+
// Save identity
|
|
208
|
+
await storage.save(identity);
|
|
209
|
+
// Show enhanced developer instructions for Vercel/serverless
|
|
210
|
+
if (isServerless) {
|
|
211
|
+
showVercelDeveloperInstructions(identity, response.agent.claimUrl);
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
logger.info('✅ Success! Your agent has been registered.');
|
|
215
|
+
logger.info(`DID: ${response.did}`);
|
|
216
|
+
logger.info(`Profile: ${response.agent.url}`);
|
|
217
|
+
// Show claim URL if in draft mode
|
|
218
|
+
if (response.agent.claimUrl) {
|
|
219
|
+
logger.info(`Claim your agent: ${response.agent.claimUrl}`);
|
|
220
|
+
}
|
|
221
|
+
// Show directory submission info
|
|
222
|
+
if (options?.directories && options.directories !== 'none') {
|
|
223
|
+
const dirMessage = options.directories === 'verified'
|
|
224
|
+
? 'Your agent will be submitted to all verified directories'
|
|
225
|
+
: `Your agent will be submitted to: ${options.directories.join(', ')}`;
|
|
226
|
+
logger.info(dirMessage);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
// Create MCPIdentity instance
|
|
230
|
+
globalIdentity = new MCPIdentity(identity, options);
|
|
231
|
+
return globalIdentity;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Enable automatic key rotation
|
|
235
|
+
*/
|
|
236
|
+
async enableAutoRotation(policy) {
|
|
237
|
+
if (!this.rotationManager) {
|
|
238
|
+
throw new Error('Key rotation not available in memory storage mode');
|
|
239
|
+
}
|
|
240
|
+
this.rotationManager.setupAutoRotation((result) => {
|
|
241
|
+
if (result.success) {
|
|
242
|
+
this.logger.info('Keys rotated successfully');
|
|
243
|
+
// Update storage with new keys
|
|
244
|
+
this.persistIdentity();
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
this.logger.error('Key rotation failed:', result.error);
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Manually rotate keys
|
|
253
|
+
*/
|
|
254
|
+
async rotateKeys(reason) {
|
|
255
|
+
if (!this.rotationManager) {
|
|
256
|
+
throw new Error('Key rotation not available in memory storage mode');
|
|
257
|
+
}
|
|
258
|
+
const result = await this.rotationManager.rotateKeys(reason);
|
|
259
|
+
if (result.success) {
|
|
260
|
+
await this.persistIdentity();
|
|
261
|
+
}
|
|
262
|
+
return result;
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Check key health
|
|
266
|
+
*/
|
|
267
|
+
checkKeyHealth() {
|
|
268
|
+
if (!this.rotationManager) {
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
return this.rotationManager.checkKeyHealth();
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Get decrypted private key (lazy decryption)
|
|
275
|
+
*/
|
|
276
|
+
async getPrivateKey() {
|
|
277
|
+
if (this.decryptedPrivateKey) {
|
|
278
|
+
return this.decryptedPrivateKey;
|
|
279
|
+
}
|
|
280
|
+
if (this.encryptionPassword && this.privateKey.startsWith('enc:')) {
|
|
281
|
+
try {
|
|
282
|
+
this.decryptedPrivateKey = await crypto.decrypt(this.privateKey, this.encryptionPassword);
|
|
283
|
+
this.logger.debug('Private key decrypted successfully');
|
|
284
|
+
return this.decryptedPrivateKey;
|
|
285
|
+
}
|
|
286
|
+
catch (error) {
|
|
287
|
+
this.logger.error('Failed to decrypt private key:', error);
|
|
288
|
+
throw new Error('Invalid encryption password');
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return this.privateKey;
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Sign a message with caching
|
|
295
|
+
*/
|
|
296
|
+
async sign(message) {
|
|
297
|
+
const messageStr = typeof message === 'string' ? message : message.toString('base64');
|
|
298
|
+
// Check cache
|
|
299
|
+
const cached = this.precomputed.signatureCache.get(messageStr);
|
|
300
|
+
if (cached) {
|
|
301
|
+
return cached;
|
|
302
|
+
}
|
|
303
|
+
// Get decrypted private key
|
|
304
|
+
const privateKey = await this.getPrivateKey();
|
|
305
|
+
// Sign and cache
|
|
306
|
+
const signature = await crypto.sign(message, privateKey);
|
|
307
|
+
// Limit cache size
|
|
308
|
+
if (this.precomputed.signatureCache.size > 100) {
|
|
309
|
+
const firstKey = this.precomputed.signatureCache.keys().next().value;
|
|
310
|
+
if (firstKey) {
|
|
311
|
+
this.precomputed.signatureCache.delete(firstKey);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
this.precomputed.signatureCache.set(messageStr, signature);
|
|
315
|
+
// Track signature count for rotation
|
|
316
|
+
if (this.rotationManager) {
|
|
317
|
+
this.rotationManager.incrementSignatureCount();
|
|
318
|
+
}
|
|
319
|
+
return signature;
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Request edit access with claim URL support
|
|
323
|
+
*/
|
|
324
|
+
async requestEditAccess() {
|
|
325
|
+
const timestamp = Date.now();
|
|
326
|
+
const message = `edit-request:${this.did}:knowthat.ai:${timestamp}`;
|
|
327
|
+
const signature = await this.sign(message);
|
|
328
|
+
// Construct edit URL with proof of ownership
|
|
329
|
+
const baseUrl = 'https://knowthat.ai';
|
|
330
|
+
const editUrl = new URL(`${baseUrl}/agents/edit`);
|
|
331
|
+
editUrl.searchParams.set('did', this.did);
|
|
332
|
+
editUrl.searchParams.set('timestamp', timestamp.toString());
|
|
333
|
+
editUrl.searchParams.set('signature', signature);
|
|
334
|
+
// Also generate claim URL if agent is not yet claimed
|
|
335
|
+
const claimUrl = new URL(`${baseUrl}/agents/claim`);
|
|
336
|
+
claimUrl.searchParams.set('did', this.did);
|
|
337
|
+
claimUrl.searchParams.set('timestamp', timestamp.toString());
|
|
338
|
+
claimUrl.searchParams.set('signature', signature);
|
|
339
|
+
return {
|
|
340
|
+
editUrl: editUrl.toString(),
|
|
341
|
+
claimUrl: claimUrl.toString()
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Verify a signature
|
|
346
|
+
*/
|
|
347
|
+
async verify(message, signature, publicKey) {
|
|
348
|
+
return crypto.verify(message, signature, publicKey || this.publicKey);
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Respond to an MCP-I challenge
|
|
352
|
+
*/
|
|
353
|
+
async respondToChallenge(challenge) {
|
|
354
|
+
// Validate timestamp to prevent replay attacks
|
|
355
|
+
const now = Date.now();
|
|
356
|
+
const challengeAge = now - challenge.timestamp;
|
|
357
|
+
if (challengeAge > this.timestampTolerance) {
|
|
358
|
+
throw new Error('Challenge expired');
|
|
359
|
+
}
|
|
360
|
+
if (challengeAge < 0) {
|
|
361
|
+
throw new Error('Challenge timestamp is in the future');
|
|
362
|
+
}
|
|
363
|
+
// Check for nonce reuse if tracking is enabled
|
|
364
|
+
if (this.enableNonceTracking) {
|
|
365
|
+
if (this.usedNonces.has(challenge.nonce)) {
|
|
366
|
+
throw new Error('Nonce already used');
|
|
367
|
+
}
|
|
368
|
+
this.usedNonces.add(challenge.nonce);
|
|
369
|
+
}
|
|
370
|
+
// Create the message to sign
|
|
371
|
+
const messageComponents = [
|
|
372
|
+
challenge.nonce,
|
|
373
|
+
challenge.timestamp.toString(),
|
|
374
|
+
this.did,
|
|
375
|
+
challenge.verifier_did || '',
|
|
376
|
+
(challenge.scope || []).join(',')
|
|
377
|
+
];
|
|
378
|
+
const message = messageComponents.join(':');
|
|
379
|
+
// Sign the challenge
|
|
380
|
+
const signature = await this.sign(message);
|
|
381
|
+
// Return the response
|
|
382
|
+
return {
|
|
383
|
+
did: this.did,
|
|
384
|
+
signature,
|
|
385
|
+
timestamp: now,
|
|
386
|
+
nonce: challenge.nonce,
|
|
387
|
+
publicKey: this.publicKey
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Get MCP-I capabilities for advertisement
|
|
392
|
+
*/
|
|
393
|
+
getCapabilities() {
|
|
394
|
+
return {
|
|
395
|
+
version: '1.0',
|
|
396
|
+
did: this.did,
|
|
397
|
+
publicKey: this.publicKey,
|
|
398
|
+
conformanceLevel: 2, // Level 2: Full crypto with challenge-response
|
|
399
|
+
handshakeSupported: true,
|
|
400
|
+
handshakeEndpoint: '/_mcp-i/handshake',
|
|
401
|
+
verificationEndpoint: `https://knowthat.ai/api/agents/${this.did}/verify`,
|
|
402
|
+
registry: 'knowthat.ai'
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Sign an MCP response with identity metadata
|
|
407
|
+
*/
|
|
408
|
+
async signResponse(response) {
|
|
409
|
+
const timestamp = new Date().toISOString();
|
|
410
|
+
const responseWithIdentity = {
|
|
411
|
+
...response,
|
|
412
|
+
_mcp_identity: {
|
|
413
|
+
did: this.did,
|
|
414
|
+
signature: '', // Will be filled below
|
|
415
|
+
timestamp,
|
|
416
|
+
conformanceLevel: 2
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
// Sign the response content (excluding the signature field)
|
|
420
|
+
const contentToSign = JSON.stringify({
|
|
421
|
+
...response,
|
|
422
|
+
_mcp_identity: {
|
|
423
|
+
did: this.did,
|
|
424
|
+
timestamp,
|
|
425
|
+
conformanceLevel: 2
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
responseWithIdentity._mcp_identity.signature = await this.sign(contentToSign);
|
|
429
|
+
return responseWithIdentity;
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Generate a new nonce for challenges
|
|
433
|
+
*/
|
|
434
|
+
static generateNonce() {
|
|
435
|
+
return crypto.generateNonceSync();
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Get directory preferences
|
|
439
|
+
*/
|
|
440
|
+
getDirectories() {
|
|
441
|
+
return this.directories;
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Clean up old nonces periodically to prevent memory leaks
|
|
445
|
+
*/
|
|
446
|
+
startNonceCleanup() {
|
|
447
|
+
// Clean up nonces older than 2x the timestamp tolerance
|
|
448
|
+
this.nonceCleanupInterval = setInterval(() => {
|
|
449
|
+
// In a production system, you'd track nonce timestamps
|
|
450
|
+
// For now, we'll clear all nonces periodically
|
|
451
|
+
if (this.usedNonces.size > 10000) {
|
|
452
|
+
this.usedNonces.clear();
|
|
453
|
+
}
|
|
454
|
+
}, this.timestampTolerance * 2);
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Clean up resources
|
|
458
|
+
*/
|
|
459
|
+
destroy() {
|
|
460
|
+
if (this.nonceCleanupInterval) {
|
|
461
|
+
clearInterval(this.nonceCleanupInterval);
|
|
462
|
+
}
|
|
463
|
+
this.usedNonces.clear();
|
|
464
|
+
this.precomputed.signatureCache.clear();
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Helper to extract agent name from DID
|
|
468
|
+
*/
|
|
469
|
+
extractAgentName() {
|
|
470
|
+
// Try to get from persisted data or environment
|
|
471
|
+
return process.env.MCP_SERVER_NAME || 'Unknown Agent';
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* Helper to extract agent ID
|
|
475
|
+
*/
|
|
476
|
+
extractAgentId() {
|
|
477
|
+
return process.env.AGENT_ID || '';
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Helper to extract agent slug
|
|
481
|
+
*/
|
|
482
|
+
extractAgentSlug() {
|
|
483
|
+
const parts = this.did.split(':');
|
|
484
|
+
return parts[parts.length - 1];
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Persist full identity (for key rotation)
|
|
488
|
+
*/
|
|
489
|
+
async persistIdentity() {
|
|
490
|
+
try {
|
|
491
|
+
const identity = {
|
|
492
|
+
did: this.did,
|
|
493
|
+
publicKey: this.publicKey,
|
|
494
|
+
privateKey: this.privateKey,
|
|
495
|
+
agentId: this.extractAgentId(),
|
|
496
|
+
agentSlug: this.extractAgentSlug(),
|
|
497
|
+
registeredAt: new Date().toISOString(),
|
|
498
|
+
directories: this.directories
|
|
499
|
+
};
|
|
500
|
+
await this.storage.save(identity);
|
|
501
|
+
}
|
|
502
|
+
catch (error) {
|
|
503
|
+
this.logger.error('Failed to persist identity:', error);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Enable MCP Identity for any MCP server
|
|
509
|
+
*/
|
|
510
|
+
export async function enableMCPIdentity(options) {
|
|
511
|
+
const identity = await MCPIdentity.init(options);
|
|
512
|
+
// Try to patch MCP Server if available
|
|
513
|
+
try {
|
|
514
|
+
patchMCPServer(identity);
|
|
515
|
+
}
|
|
516
|
+
catch (error) {
|
|
517
|
+
const logger = getLogger();
|
|
518
|
+
logger.debug('MCP Server not found, identity initialized for manual use');
|
|
519
|
+
}
|
|
520
|
+
return identity;
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* Create MCP middleware for manual integration
|
|
524
|
+
*/
|
|
525
|
+
export function createMCPMiddleware(identity) {
|
|
526
|
+
return (server) => {
|
|
527
|
+
// Validate server object
|
|
528
|
+
if (!server || typeof server !== 'object') {
|
|
529
|
+
const logger = getLogger();
|
|
530
|
+
logger.warn('Invalid MCP Server object passed to middleware');
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
// Check if methods exist before patching
|
|
534
|
+
if (!server.setRequestHandler || typeof server.setRequestHandler !== 'function') {
|
|
535
|
+
const logger = getLogger();
|
|
536
|
+
logger.warn('MCP Server missing setRequestHandler method');
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
// Store original methods
|
|
540
|
+
const originalSetRequestHandler = server.setRequestHandler.bind(server);
|
|
541
|
+
const originalConnect = server.connect ? server.connect.bind(server) : null;
|
|
542
|
+
// Patch setRequestHandler to wrap all responses
|
|
543
|
+
server.setRequestHandler = function (method, handler) {
|
|
544
|
+
// Add defensive check for undefined method
|
|
545
|
+
if (!method || typeof method !== 'string') {
|
|
546
|
+
const logger = getLogger();
|
|
547
|
+
logger.warn('setRequestHandler called with invalid method:', method);
|
|
548
|
+
return originalSetRequestHandler.call(this, method, handler);
|
|
549
|
+
}
|
|
550
|
+
const wrappedHandler = async (...args) => {
|
|
551
|
+
const result = await handler(...args);
|
|
552
|
+
// If result has content, sign it
|
|
553
|
+
if (result && typeof result === 'object' && 'content' in result) {
|
|
554
|
+
return await identity.signResponse(result);
|
|
555
|
+
}
|
|
556
|
+
return result;
|
|
557
|
+
};
|
|
558
|
+
return originalSetRequestHandler(method, wrappedHandler);
|
|
559
|
+
};
|
|
560
|
+
// Patch connect to advertise MCP-I capabilities
|
|
561
|
+
if (originalConnect) {
|
|
562
|
+
server.connect = async function (transport) {
|
|
563
|
+
// Add MCP-I capabilities to server info
|
|
564
|
+
if (this.serverInfo && this.serverInfo.capabilities) {
|
|
565
|
+
this.serverInfo.capabilities['mcp-i'] = identity.getCapabilities();
|
|
566
|
+
}
|
|
567
|
+
// Set up MCP-I handshake handler
|
|
568
|
+
this.setRequestHandler('mcp-i/challenge', async (request) => {
|
|
569
|
+
return identity.respondToChallenge(request.params);
|
|
570
|
+
});
|
|
571
|
+
// Call original connect
|
|
572
|
+
return originalConnect.call(this, transport);
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* Patch the MCP Server to automatically add identity
|
|
579
|
+
*/
|
|
580
|
+
function patchMCPServer(identity) {
|
|
581
|
+
try {
|
|
582
|
+
// Try to import the MCP SDK
|
|
583
|
+
const MCPModule = require('@modelcontextprotocol/sdk/server/index.js');
|
|
584
|
+
const OriginalServer = MCPModule.Server;
|
|
585
|
+
if (!OriginalServer) {
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
// Apply middleware
|
|
589
|
+
const middleware = createMCPMiddleware(identity);
|
|
590
|
+
// Patch the constructor
|
|
591
|
+
const OriginalConstructor = OriginalServer;
|
|
592
|
+
MCPModule.Server = function (...args) {
|
|
593
|
+
const instance = new OriginalConstructor(...args);
|
|
594
|
+
try {
|
|
595
|
+
middleware(instance, identity);
|
|
596
|
+
}
|
|
597
|
+
catch (error) {
|
|
598
|
+
const logger = getLogger();
|
|
599
|
+
logger.warn('Failed to apply MCP-I middleware:', error);
|
|
600
|
+
}
|
|
601
|
+
return instance;
|
|
602
|
+
};
|
|
603
|
+
// Copy static properties
|
|
604
|
+
Object.setPrototypeOf(MCPModule.Server, OriginalConstructor);
|
|
605
|
+
Object.setPrototypeOf(MCPModule.Server.prototype, OriginalConstructor.prototype);
|
|
606
|
+
const logger = getLogger();
|
|
607
|
+
logger.info('✨ MCP Server patched - all responses will be automatically signed');
|
|
608
|
+
}
|
|
609
|
+
catch (error) {
|
|
610
|
+
// MCP SDK not available, that's okay
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
// Helper function for auto-registration
|
|
614
|
+
async function autoRegister(transport, options) {
|
|
615
|
+
const logger = getLogger();
|
|
616
|
+
try {
|
|
617
|
+
const requestBody = {
|
|
618
|
+
metadata: {
|
|
619
|
+
name: options.name,
|
|
620
|
+
description: options.description,
|
|
621
|
+
repository: options.repository,
|
|
622
|
+
publicKey: options.publicKey,
|
|
623
|
+
version: '1.0.0',
|
|
624
|
+
isDraft: options.isDraft,
|
|
625
|
+
directories: options.directories || 'verified' // Default to all verified directories
|
|
626
|
+
},
|
|
627
|
+
clientInfo: {
|
|
628
|
+
sdkVersion: '0.1.0-alpha.2.5',
|
|
629
|
+
language: 'typescript',
|
|
630
|
+
platform: typeof process !== 'undefined' ? 'node' : 'browser'
|
|
631
|
+
}
|
|
632
|
+
};
|
|
633
|
+
logger.debug('Sending registration request to:', `${options.apiEndpoint}/api/agents/auto-register`);
|
|
634
|
+
const response = await transport.post(`${options.apiEndpoint}/api/agents/auto-register`, requestBody, {
|
|
635
|
+
timeout: 30000,
|
|
636
|
+
headers: {
|
|
637
|
+
'Content-Type': 'application/json',
|
|
638
|
+
'User-Agent': '@kya-os/mcp-i/0.1.0-alpha.2.5'
|
|
639
|
+
}
|
|
640
|
+
});
|
|
641
|
+
logger.debug('Registration response received:', {
|
|
642
|
+
status: response.status,
|
|
643
|
+
hasData: !!response.data,
|
|
644
|
+
hasDid: !!response.data?.did,
|
|
645
|
+
hasAgent: !!response.data?.agent
|
|
646
|
+
});
|
|
647
|
+
return response.data;
|
|
648
|
+
}
|
|
649
|
+
catch (error) {
|
|
650
|
+
logger.error('Registration failed:', {
|
|
651
|
+
message: error.message,
|
|
652
|
+
status: error.status,
|
|
653
|
+
response: error.response
|
|
654
|
+
});
|
|
655
|
+
if (error.message?.includes('429')) {
|
|
656
|
+
throw new Error('Rate limit exceeded. Please try again later.');
|
|
657
|
+
}
|
|
658
|
+
// Provide more helpful error messages
|
|
659
|
+
if (error.message?.includes('500')) {
|
|
660
|
+
throw new Error('Server error during registration. This might be a temporary issue.\n' +
|
|
661
|
+
'Please try again in a few moments, or register manually at https://knowthat.ai/submit-agent');
|
|
662
|
+
}
|
|
663
|
+
if (error.message?.includes('400')) {
|
|
664
|
+
throw new Error('Invalid registration data. Please ensure all required fields are provided:\n' +
|
|
665
|
+
'- name: A unique name for your agent\n' +
|
|
666
|
+
'- publicKey: Generated automatically (check if generation succeeded)');
|
|
667
|
+
}
|
|
668
|
+
throw new Error(error.message || 'Failed to auto-register agent');
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
//# sourceMappingURL=index.js.map
|