claude-flow 1.0.0
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/LICENSE +21 -0
- package/README.md +612 -0
- package/bin/claude-flow +0 -0
- package/bin/claude-flow-simple +0 -0
- package/bin/claude-flow-typecheck +0 -0
- package/deno.json +84 -0
- package/package.json +45 -0
- package/scripts/check-links.ts +274 -0
- package/scripts/check-performance-regression.ts +168 -0
- package/scripts/claude-sparc.sh +562 -0
- package/scripts/coverage-report.ts +692 -0
- package/scripts/demo-task-system.ts +224 -0
- package/scripts/install.js +72 -0
- package/scripts/test-batch-tasks.ts +29 -0
- package/scripts/test-coordination-features.ts +238 -0
- package/scripts/test-mcp.ts +251 -0
- package/scripts/test-runner.ts +571 -0
- package/scripts/validate-examples.ts +288 -0
- package/src/cli/cli-core.ts +273 -0
- package/src/cli/commands/agent.ts +83 -0
- package/src/cli/commands/config.ts +442 -0
- package/src/cli/commands/help.ts +765 -0
- package/src/cli/commands/index.ts +963 -0
- package/src/cli/commands/mcp.ts +191 -0
- package/src/cli/commands/memory.ts +74 -0
- package/src/cli/commands/monitor.ts +403 -0
- package/src/cli/commands/session.ts +595 -0
- package/src/cli/commands/start.ts +156 -0
- package/src/cli/commands/status.ts +345 -0
- package/src/cli/commands/task.ts +79 -0
- package/src/cli/commands/workflow.ts +763 -0
- package/src/cli/completion.ts +553 -0
- package/src/cli/formatter.ts +310 -0
- package/src/cli/index.ts +211 -0
- package/src/cli/main.ts +23 -0
- package/src/cli/repl.ts +1050 -0
- package/src/cli/simple-cli.js +211 -0
- package/src/cli/simple-cli.ts +211 -0
- package/src/coordination/README.md +400 -0
- package/src/coordination/advanced-scheduler.ts +487 -0
- package/src/coordination/circuit-breaker.ts +366 -0
- package/src/coordination/conflict-resolution.ts +490 -0
- package/src/coordination/dependency-graph.ts +475 -0
- package/src/coordination/index.ts +63 -0
- package/src/coordination/manager.ts +460 -0
- package/src/coordination/messaging.ts +290 -0
- package/src/coordination/metrics.ts +585 -0
- package/src/coordination/resources.ts +322 -0
- package/src/coordination/scheduler.ts +390 -0
- package/src/coordination/work-stealing.ts +224 -0
- package/src/core/config.ts +627 -0
- package/src/core/event-bus.ts +186 -0
- package/src/core/json-persistence.ts +183 -0
- package/src/core/logger.ts +262 -0
- package/src/core/orchestrator-fixed.ts +312 -0
- package/src/core/orchestrator.ts +1234 -0
- package/src/core/persistence.ts +276 -0
- package/src/mcp/auth.ts +438 -0
- package/src/mcp/claude-flow-tools.ts +1280 -0
- package/src/mcp/load-balancer.ts +510 -0
- package/src/mcp/router.ts +240 -0
- package/src/mcp/server.ts +548 -0
- package/src/mcp/session-manager.ts +418 -0
- package/src/mcp/tools.ts +180 -0
- package/src/mcp/transports/base.ts +21 -0
- package/src/mcp/transports/http.ts +457 -0
- package/src/mcp/transports/stdio.ts +254 -0
- package/src/memory/backends/base.ts +22 -0
- package/src/memory/backends/markdown.ts +283 -0
- package/src/memory/backends/sqlite.ts +329 -0
- package/src/memory/cache.ts +238 -0
- package/src/memory/indexer.ts +238 -0
- package/src/memory/manager.ts +572 -0
- package/src/terminal/adapters/base.ts +29 -0
- package/src/terminal/adapters/native.ts +504 -0
- package/src/terminal/adapters/vscode.ts +340 -0
- package/src/terminal/manager.ts +308 -0
- package/src/terminal/pool.ts +271 -0
- package/src/terminal/session.ts +250 -0
- package/src/terminal/vscode-bridge.ts +242 -0
- package/src/utils/errors.ts +231 -0
- package/src/utils/helpers.ts +476 -0
- package/src/utils/types.ts +493 -0
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session manager for MCP connections
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
MCPSession,
|
|
7
|
+
MCPInitializeParams,
|
|
8
|
+
MCPProtocolVersion,
|
|
9
|
+
MCPCapabilities,
|
|
10
|
+
MCPAuthConfig,
|
|
11
|
+
MCPConfig,
|
|
12
|
+
} from '../utils/types.ts';
|
|
13
|
+
import { ILogger } from '../core/logger.ts';
|
|
14
|
+
import { MCPError } from '../utils/errors.ts';
|
|
15
|
+
import { createHash, timingSafeEqual } from 'node:crypto';
|
|
16
|
+
|
|
17
|
+
export interface ISessionManager {
|
|
18
|
+
createSession(transport: 'stdio' | 'http' | 'websocket'): MCPSession;
|
|
19
|
+
getSession(id: string): MCPSession | undefined;
|
|
20
|
+
initializeSession(sessionId: string, params: MCPInitializeParams): void;
|
|
21
|
+
authenticateSession(sessionId: string, credentials: unknown): boolean;
|
|
22
|
+
updateActivity(sessionId: string): void;
|
|
23
|
+
removeSession(sessionId: string): void;
|
|
24
|
+
getActiveSessions(): MCPSession[];
|
|
25
|
+
cleanupExpiredSessions(): void;
|
|
26
|
+
getSessionMetrics(): {
|
|
27
|
+
total: number;
|
|
28
|
+
active: number;
|
|
29
|
+
authenticated: number;
|
|
30
|
+
expired: number;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Session manager implementation
|
|
36
|
+
*/
|
|
37
|
+
export class SessionManager implements ISessionManager {
|
|
38
|
+
private sessions = new Map<string, MCPSession>();
|
|
39
|
+
private authConfig: MCPAuthConfig;
|
|
40
|
+
private sessionTimeout: number;
|
|
41
|
+
private maxSessions: number;
|
|
42
|
+
private cleanupInterval?: number;
|
|
43
|
+
|
|
44
|
+
constructor(
|
|
45
|
+
private config: MCPConfig,
|
|
46
|
+
private logger: ILogger,
|
|
47
|
+
) {
|
|
48
|
+
this.authConfig = config.auth || { enabled: false, method: 'token' };
|
|
49
|
+
this.sessionTimeout = config.sessionTimeout || 3600000; // 1 hour default
|
|
50
|
+
this.maxSessions = config.maxSessions || 100;
|
|
51
|
+
|
|
52
|
+
// Start cleanup timer
|
|
53
|
+
this.cleanupInterval = setInterval(() => {
|
|
54
|
+
this.cleanupExpiredSessions();
|
|
55
|
+
}, 60000); // Clean up every minute
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
createSession(transport: 'stdio' | 'http' | 'websocket'): MCPSession {
|
|
59
|
+
// Check session limit
|
|
60
|
+
if (this.sessions.size >= this.maxSessions) {
|
|
61
|
+
// Try to clean up expired sessions first
|
|
62
|
+
this.cleanupExpiredSessions();
|
|
63
|
+
|
|
64
|
+
if (this.sessions.size >= this.maxSessions) {
|
|
65
|
+
throw new MCPError('Maximum number of sessions reached');
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const sessionId = this.generateSessionId();
|
|
70
|
+
const now = new Date();
|
|
71
|
+
|
|
72
|
+
const session: MCPSession = {
|
|
73
|
+
id: sessionId,
|
|
74
|
+
clientInfo: { name: 'unknown', version: 'unknown' },
|
|
75
|
+
protocolVersion: { major: 0, minor: 0, patch: 0 },
|
|
76
|
+
capabilities: {},
|
|
77
|
+
isInitialized: false,
|
|
78
|
+
createdAt: now,
|
|
79
|
+
lastActivity: now,
|
|
80
|
+
transport,
|
|
81
|
+
authenticated: !this.authConfig.enabled, // If auth disabled, session is authenticated
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
this.sessions.set(sessionId, session);
|
|
85
|
+
|
|
86
|
+
this.logger.info('Session created', {
|
|
87
|
+
sessionId,
|
|
88
|
+
transport,
|
|
89
|
+
totalSessions: this.sessions.size,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
return session;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
getSession(id: string): MCPSession | undefined {
|
|
96
|
+
const session = this.sessions.get(id);
|
|
97
|
+
if (session && this.isSessionExpired(session)) {
|
|
98
|
+
this.removeSession(id);
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
return session;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
initializeSession(sessionId: string, params: MCPInitializeParams): void {
|
|
105
|
+
const session = this.getSession(sessionId);
|
|
106
|
+
if (!session) {
|
|
107
|
+
throw new MCPError(`Session not found: ${sessionId}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Validate protocol version
|
|
111
|
+
this.validateProtocolVersion(params.protocolVersion);
|
|
112
|
+
|
|
113
|
+
// Update session with initialization params
|
|
114
|
+
session.clientInfo = params.clientInfo;
|
|
115
|
+
session.protocolVersion = params.protocolVersion;
|
|
116
|
+
session.capabilities = params.capabilities;
|
|
117
|
+
session.isInitialized = true;
|
|
118
|
+
session.lastActivity = new Date();
|
|
119
|
+
|
|
120
|
+
this.logger.info('Session initialized', {
|
|
121
|
+
sessionId,
|
|
122
|
+
clientInfo: params.clientInfo,
|
|
123
|
+
protocolVersion: params.protocolVersion,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
authenticateSession(sessionId: string, credentials: unknown): boolean {
|
|
128
|
+
const session = this.getSession(sessionId);
|
|
129
|
+
if (!session) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (!this.authConfig.enabled) {
|
|
134
|
+
session.authenticated = true;
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
let authenticated = false;
|
|
139
|
+
|
|
140
|
+
switch (this.authConfig.method) {
|
|
141
|
+
case 'token':
|
|
142
|
+
authenticated = this.authenticateToken(credentials);
|
|
143
|
+
break;
|
|
144
|
+
case 'basic':
|
|
145
|
+
authenticated = this.authenticateBasic(credentials);
|
|
146
|
+
break;
|
|
147
|
+
case 'oauth':
|
|
148
|
+
authenticated = this.authenticateOAuth(credentials);
|
|
149
|
+
break;
|
|
150
|
+
default:
|
|
151
|
+
this.logger.warn('Unknown authentication method', {
|
|
152
|
+
method: this.authConfig.method,
|
|
153
|
+
});
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (authenticated) {
|
|
158
|
+
session.authenticated = true;
|
|
159
|
+
session.authData = this.extractAuthData(credentials);
|
|
160
|
+
session.lastActivity = new Date();
|
|
161
|
+
|
|
162
|
+
this.logger.info('Session authenticated', {
|
|
163
|
+
sessionId,
|
|
164
|
+
method: this.authConfig.method,
|
|
165
|
+
});
|
|
166
|
+
} else {
|
|
167
|
+
this.logger.warn('Session authentication failed', {
|
|
168
|
+
sessionId,
|
|
169
|
+
method: this.authConfig.method,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return authenticated;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
updateActivity(sessionId: string): void {
|
|
177
|
+
const session = this.getSession(sessionId);
|
|
178
|
+
if (session) {
|
|
179
|
+
session.lastActivity = new Date();
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
removeSession(sessionId: string): void {
|
|
184
|
+
const session = this.sessions.get(sessionId);
|
|
185
|
+
if (session) {
|
|
186
|
+
this.sessions.delete(sessionId);
|
|
187
|
+
this.logger.info('Session removed', {
|
|
188
|
+
sessionId,
|
|
189
|
+
duration: Date.now() - session.createdAt.getTime(),
|
|
190
|
+
transport: session.transport,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
getActiveSessions(): MCPSession[] {
|
|
196
|
+
const activeSessions: MCPSession[] = [];
|
|
197
|
+
for (const session of this.sessions.values()) {
|
|
198
|
+
if (!this.isSessionExpired(session)) {
|
|
199
|
+
activeSessions.push(session);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return activeSessions;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
cleanupExpiredSessions(): void {
|
|
206
|
+
const expiredSessions: string[] = [];
|
|
207
|
+
|
|
208
|
+
for (const [sessionId, session] of this.sessions) {
|
|
209
|
+
if (this.isSessionExpired(session)) {
|
|
210
|
+
expiredSessions.push(sessionId);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
for (const sessionId of expiredSessions) {
|
|
215
|
+
this.removeSession(sessionId);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (expiredSessions.length > 0) {
|
|
219
|
+
this.logger.info('Cleaned up expired sessions', {
|
|
220
|
+
count: expiredSessions.length,
|
|
221
|
+
remainingSessions: this.sessions.size,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
getSessionMetrics(): {
|
|
227
|
+
total: number;
|
|
228
|
+
active: number;
|
|
229
|
+
authenticated: number;
|
|
230
|
+
expired: number;
|
|
231
|
+
} {
|
|
232
|
+
let active = 0;
|
|
233
|
+
let authenticated = 0;
|
|
234
|
+
let expired = 0;
|
|
235
|
+
|
|
236
|
+
for (const session of this.sessions.values()) {
|
|
237
|
+
if (this.isSessionExpired(session)) {
|
|
238
|
+
expired++;
|
|
239
|
+
} else {
|
|
240
|
+
active++;
|
|
241
|
+
if (session.authenticated) {
|
|
242
|
+
authenticated++;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return {
|
|
248
|
+
total: this.sessions.size,
|
|
249
|
+
active,
|
|
250
|
+
authenticated,
|
|
251
|
+
expired,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
destroy(): void {
|
|
256
|
+
if (this.cleanupInterval) {
|
|
257
|
+
clearInterval(this.cleanupInterval);
|
|
258
|
+
}
|
|
259
|
+
this.sessions.clear();
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
private generateSessionId(): string {
|
|
263
|
+
const timestamp = Date.now().toString(36);
|
|
264
|
+
const random = Math.random().toString(36).substr(2, 9);
|
|
265
|
+
return `session_${timestamp}_${random}`;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
private isSessionExpired(session: MCPSession): boolean {
|
|
269
|
+
const now = Date.now();
|
|
270
|
+
const sessionAge = now - session.lastActivity.getTime();
|
|
271
|
+
return sessionAge > this.sessionTimeout;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
private validateProtocolVersion(version: MCPProtocolVersion): void {
|
|
275
|
+
// Currently supporting MCP version 2024-11-05
|
|
276
|
+
const supportedVersions = [
|
|
277
|
+
{ major: 2024, minor: 11, patch: 5 },
|
|
278
|
+
];
|
|
279
|
+
|
|
280
|
+
const isSupported = supportedVersions.some(
|
|
281
|
+
(supported) =>
|
|
282
|
+
supported.major === version.major &&
|
|
283
|
+
supported.minor === version.minor &&
|
|
284
|
+
supported.patch === version.patch,
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
if (!isSupported) {
|
|
288
|
+
throw new MCPError(
|
|
289
|
+
`Unsupported protocol version: ${version.major}.${version.minor}.${version.patch}`,
|
|
290
|
+
{ supportedVersions }
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
private authenticateToken(credentials: unknown): boolean {
|
|
296
|
+
if (!this.authConfig.tokens || this.authConfig.tokens.length === 0) {
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const token = this.extractToken(credentials);
|
|
301
|
+
if (!token) {
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Use timing-safe comparison to prevent timing attacks
|
|
306
|
+
return this.authConfig.tokens.some((validToken) => {
|
|
307
|
+
const encoder = new TextEncoder();
|
|
308
|
+
const validTokenBytes = encoder.encode(validToken);
|
|
309
|
+
const providedTokenBytes = encoder.encode(token);
|
|
310
|
+
|
|
311
|
+
if (validTokenBytes.length !== providedTokenBytes.length) {
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return timingSafeEqual(validTokenBytes, providedTokenBytes);
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
private authenticateBasic(credentials: unknown): boolean {
|
|
320
|
+
if (!this.authConfig.users || this.authConfig.users.length === 0) {
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const { username, password } = this.extractBasicAuth(credentials);
|
|
325
|
+
if (!username || !password) {
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const user = this.authConfig.users.find((u) => u.username === username);
|
|
330
|
+
if (!user) {
|
|
331
|
+
return false;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Hash the provided password and compare
|
|
335
|
+
const hashedPassword = this.hashPassword(password);
|
|
336
|
+
const expectedHashedPassword = this.hashPassword(user.password);
|
|
337
|
+
|
|
338
|
+
const encoder = new TextEncoder();
|
|
339
|
+
const hashedPasswordBytes = encoder.encode(hashedPassword);
|
|
340
|
+
const expectedHashedPasswordBytes = encoder.encode(expectedHashedPassword);
|
|
341
|
+
|
|
342
|
+
if (hashedPasswordBytes.length !== expectedHashedPasswordBytes.length) {
|
|
343
|
+
return false;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return timingSafeEqual(hashedPasswordBytes, expectedHashedPasswordBytes);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
private authenticateOAuth(credentials: unknown): boolean {
|
|
350
|
+
// TODO: Implement OAuth authentication
|
|
351
|
+
// This would typically involve validating JWT tokens
|
|
352
|
+
this.logger.warn('OAuth authentication not yet implemented');
|
|
353
|
+
return false;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
private extractToken(credentials: unknown): string | null {
|
|
357
|
+
if (typeof credentials === 'string') {
|
|
358
|
+
return credentials;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (typeof credentials === 'object' && credentials !== null) {
|
|
362
|
+
const creds = credentials as Record<string, unknown>;
|
|
363
|
+
if (typeof creds.token === 'string') {
|
|
364
|
+
return creds.token;
|
|
365
|
+
}
|
|
366
|
+
if (typeof creds.authorization === 'string') {
|
|
367
|
+
const match = creds.authorization.match(/^Bearer\s+(.+)$/);
|
|
368
|
+
return match ? match[1] : null;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return null;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
private extractBasicAuth(credentials: unknown): { username?: string; password?: string } {
|
|
376
|
+
if (typeof credentials === 'object' && credentials !== null) {
|
|
377
|
+
const creds = credentials as Record<string, unknown>;
|
|
378
|
+
|
|
379
|
+
if (typeof creds.username === 'string' && typeof creds.password === 'string') {
|
|
380
|
+
return {
|
|
381
|
+
username: creds.username,
|
|
382
|
+
password: creds.password,
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (typeof creds.authorization === 'string') {
|
|
387
|
+
const match = creds.authorization.match(/^Basic\s+(.+)$/);
|
|
388
|
+
if (match) {
|
|
389
|
+
try {
|
|
390
|
+
const decoded = atob(match[1]);
|
|
391
|
+
const [username, password] = decoded.split(':', 2);
|
|
392
|
+
return { username, password };
|
|
393
|
+
} catch {
|
|
394
|
+
return {};
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return {};
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
private extractAuthData(credentials: unknown): any {
|
|
404
|
+
if (typeof credentials === 'object' && credentials !== null) {
|
|
405
|
+
const creds = credentials as Record<string, unknown>;
|
|
406
|
+
return {
|
|
407
|
+
token: this.extractToken(credentials),
|
|
408
|
+
user: creds.username || creds.user,
|
|
409
|
+
permissions: creds.permissions || [],
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
return {};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
private hashPassword(password: string): string {
|
|
416
|
+
return createHash('sha256').update(password).digest('hex');
|
|
417
|
+
}
|
|
418
|
+
}
|
package/src/mcp/tools.ts
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool registry for MCP
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { MCPTool } from '../utils/types.ts';
|
|
6
|
+
import { ILogger } from '../core/logger.ts';
|
|
7
|
+
import { MCPError } from '../utils/errors.ts';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Tool registry implementation
|
|
11
|
+
*/
|
|
12
|
+
export class ToolRegistry {
|
|
13
|
+
private tools = new Map<string, MCPTool>();
|
|
14
|
+
|
|
15
|
+
constructor(private logger: ILogger) {}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Registers a new tool
|
|
19
|
+
*/
|
|
20
|
+
register(tool: MCPTool): void {
|
|
21
|
+
if (this.tools.has(tool.name)) {
|
|
22
|
+
throw new MCPError(`Tool already registered: ${tool.name}`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Validate tool schema
|
|
26
|
+
this.validateTool(tool);
|
|
27
|
+
|
|
28
|
+
this.tools.set(tool.name, tool);
|
|
29
|
+
this.logger.debug('Tool registered', { name: tool.name });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Unregisters a tool
|
|
34
|
+
*/
|
|
35
|
+
unregister(name: string): void {
|
|
36
|
+
if (!this.tools.has(name)) {
|
|
37
|
+
throw new MCPError(`Tool not found: ${name}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
this.tools.delete(name);
|
|
41
|
+
this.logger.debug('Tool unregistered', { name });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Gets a tool by name
|
|
46
|
+
*/
|
|
47
|
+
getTool(name: string): MCPTool | undefined {
|
|
48
|
+
return this.tools.get(name);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Lists all registered tools
|
|
53
|
+
*/
|
|
54
|
+
listTools(): Array<{ name: string; description: string }> {
|
|
55
|
+
return Array.from(this.tools.values()).map(tool => ({
|
|
56
|
+
name: tool.name,
|
|
57
|
+
description: tool.description,
|
|
58
|
+
}));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Gets the number of registered tools
|
|
63
|
+
*/
|
|
64
|
+
getToolCount(): number {
|
|
65
|
+
return this.tools.size;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Executes a tool
|
|
70
|
+
*/
|
|
71
|
+
async executeTool(name: string, input: unknown): Promise<unknown> {
|
|
72
|
+
const tool = this.tools.get(name);
|
|
73
|
+
if (!tool) {
|
|
74
|
+
throw new MCPError(`Tool not found: ${name}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
this.logger.debug('Executing tool', { name, input });
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
// Validate input against schema
|
|
81
|
+
this.validateInput(tool, input);
|
|
82
|
+
|
|
83
|
+
// Execute tool handler
|
|
84
|
+
const result = await tool.handler(input);
|
|
85
|
+
|
|
86
|
+
this.logger.debug('Tool executed successfully', { name });
|
|
87
|
+
return result;
|
|
88
|
+
} catch (error) {
|
|
89
|
+
this.logger.error('Tool execution failed', { name, error });
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Validates tool definition
|
|
96
|
+
*/
|
|
97
|
+
private validateTool(tool: MCPTool): void {
|
|
98
|
+
if (!tool.name || typeof tool.name !== 'string') {
|
|
99
|
+
throw new MCPError('Tool name must be a non-empty string');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (!tool.description || typeof tool.description !== 'string') {
|
|
103
|
+
throw new MCPError('Tool description must be a non-empty string');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (typeof tool.handler !== 'function') {
|
|
107
|
+
throw new MCPError('Tool handler must be a function');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!tool.inputSchema || typeof tool.inputSchema !== 'object') {
|
|
111
|
+
throw new MCPError('Tool inputSchema must be an object');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Validate tool name format (namespace/name)
|
|
115
|
+
if (!tool.name.includes('/')) {
|
|
116
|
+
throw new MCPError('Tool name must be in format: namespace/name');
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Validates input against tool schema
|
|
122
|
+
*/
|
|
123
|
+
private validateInput(tool: MCPTool, input: unknown): void {
|
|
124
|
+
// Simple validation - in production, use a JSON Schema validator
|
|
125
|
+
const schema = tool.inputSchema as any;
|
|
126
|
+
|
|
127
|
+
if (schema.type === 'object' && schema.properties) {
|
|
128
|
+
if (typeof input !== 'object' || input === null) {
|
|
129
|
+
throw new MCPError('Input must be an object');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const inputObj = input as Record<string, unknown>;
|
|
133
|
+
|
|
134
|
+
// Check required properties
|
|
135
|
+
if (schema.required && Array.isArray(schema.required)) {
|
|
136
|
+
for (const prop of schema.required) {
|
|
137
|
+
if (!(prop in inputObj)) {
|
|
138
|
+
throw new MCPError(`Missing required property: ${prop}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Check property types
|
|
144
|
+
for (const [prop, propSchema] of Object.entries(schema.properties)) {
|
|
145
|
+
if (prop in inputObj) {
|
|
146
|
+
const value = inputObj[prop];
|
|
147
|
+
const expectedType = (propSchema as any).type;
|
|
148
|
+
|
|
149
|
+
if (expectedType && !this.checkType(value, expectedType)) {
|
|
150
|
+
throw new MCPError(
|
|
151
|
+
`Invalid type for property ${prop}: expected ${expectedType}`,
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Checks if a value matches a JSON Schema type
|
|
161
|
+
*/
|
|
162
|
+
private checkType(value: unknown, type: string): boolean {
|
|
163
|
+
switch (type) {
|
|
164
|
+
case 'string':
|
|
165
|
+
return typeof value === 'string';
|
|
166
|
+
case 'number':
|
|
167
|
+
return typeof value === 'number';
|
|
168
|
+
case 'boolean':
|
|
169
|
+
return typeof value === 'boolean';
|
|
170
|
+
case 'object':
|
|
171
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
172
|
+
case 'array':
|
|
173
|
+
return Array.isArray(value);
|
|
174
|
+
case 'null':
|
|
175
|
+
return value === null;
|
|
176
|
+
default:
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base transport interface for MCP
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { MCPRequest, MCPResponse, MCPNotification } from '../../utils/types.ts';
|
|
6
|
+
|
|
7
|
+
export type RequestHandler = (request: MCPRequest) => Promise<MCPResponse>;
|
|
8
|
+
export type NotificationHandler = (notification: MCPNotification) => Promise<void>;
|
|
9
|
+
|
|
10
|
+
export interface ITransport {
|
|
11
|
+
start(): Promise<void>;
|
|
12
|
+
stop(): Promise<void>;
|
|
13
|
+
onRequest(handler: RequestHandler): void;
|
|
14
|
+
onNotification?(handler: NotificationHandler): void;
|
|
15
|
+
sendNotification?(notification: MCPNotification): Promise<void>;
|
|
16
|
+
getHealthStatus(): Promise<{
|
|
17
|
+
healthy: boolean;
|
|
18
|
+
error?: string;
|
|
19
|
+
metrics?: Record<string, number>;
|
|
20
|
+
}>;
|
|
21
|
+
}
|