@claude-flow/mcp 3.0.0-alpha.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.
Files changed (92) hide show
  1. package/.agentic-flow/intelligence.json +16 -0
  2. package/README.md +428 -0
  3. package/__tests__/integration.test.ts +449 -0
  4. package/__tests__/mcp.test.ts +641 -0
  5. package/dist/connection-pool.d.ts +36 -0
  6. package/dist/connection-pool.d.ts.map +1 -0
  7. package/dist/connection-pool.js +273 -0
  8. package/dist/connection-pool.js.map +1 -0
  9. package/dist/index.d.ts +75 -0
  10. package/dist/index.d.ts.map +1 -0
  11. package/dist/index.js +85 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/oauth.d.ts +146 -0
  14. package/dist/oauth.d.ts.map +1 -0
  15. package/dist/oauth.js +318 -0
  16. package/dist/oauth.js.map +1 -0
  17. package/dist/prompt-registry.d.ts +90 -0
  18. package/dist/prompt-registry.d.ts.map +1 -0
  19. package/dist/prompt-registry.js +209 -0
  20. package/dist/prompt-registry.js.map +1 -0
  21. package/dist/rate-limiter.d.ts +86 -0
  22. package/dist/rate-limiter.d.ts.map +1 -0
  23. package/dist/rate-limiter.js +197 -0
  24. package/dist/rate-limiter.js.map +1 -0
  25. package/dist/resource-registry.d.ts +144 -0
  26. package/dist/resource-registry.d.ts.map +1 -0
  27. package/dist/resource-registry.js +405 -0
  28. package/dist/resource-registry.js.map +1 -0
  29. package/dist/sampling.d.ts +102 -0
  30. package/dist/sampling.d.ts.map +1 -0
  31. package/dist/sampling.js +268 -0
  32. package/dist/sampling.js.map +1 -0
  33. package/dist/schema-validator.d.ts +30 -0
  34. package/dist/schema-validator.d.ts.map +1 -0
  35. package/dist/schema-validator.js +182 -0
  36. package/dist/schema-validator.js.map +1 -0
  37. package/dist/server.d.ts +122 -0
  38. package/dist/server.d.ts.map +1 -0
  39. package/dist/server.js +829 -0
  40. package/dist/server.js.map +1 -0
  41. package/dist/session-manager.d.ts +55 -0
  42. package/dist/session-manager.d.ts.map +1 -0
  43. package/dist/session-manager.js +252 -0
  44. package/dist/session-manager.js.map +1 -0
  45. package/dist/task-manager.d.ts +81 -0
  46. package/dist/task-manager.d.ts.map +1 -0
  47. package/dist/task-manager.js +337 -0
  48. package/dist/task-manager.js.map +1 -0
  49. package/dist/tool-registry.d.ts +88 -0
  50. package/dist/tool-registry.d.ts.map +1 -0
  51. package/dist/tool-registry.js +353 -0
  52. package/dist/tool-registry.js.map +1 -0
  53. package/dist/transport/http.d.ts +55 -0
  54. package/dist/transport/http.d.ts.map +1 -0
  55. package/dist/transport/http.js +446 -0
  56. package/dist/transport/http.js.map +1 -0
  57. package/dist/transport/index.d.ts +50 -0
  58. package/dist/transport/index.d.ts.map +1 -0
  59. package/dist/transport/index.js +181 -0
  60. package/dist/transport/index.js.map +1 -0
  61. package/dist/transport/stdio.d.ts +43 -0
  62. package/dist/transport/stdio.d.ts.map +1 -0
  63. package/dist/transport/stdio.js +194 -0
  64. package/dist/transport/stdio.js.map +1 -0
  65. package/dist/transport/websocket.d.ts +65 -0
  66. package/dist/transport/websocket.d.ts.map +1 -0
  67. package/dist/transport/websocket.js +314 -0
  68. package/dist/transport/websocket.js.map +1 -0
  69. package/dist/types.d.ts +473 -0
  70. package/dist/types.d.ts.map +1 -0
  71. package/dist/types.js +40 -0
  72. package/dist/types.js.map +1 -0
  73. package/package.json +42 -0
  74. package/src/connection-pool.ts +344 -0
  75. package/src/index.ts +253 -0
  76. package/src/oauth.ts +447 -0
  77. package/src/prompt-registry.ts +296 -0
  78. package/src/rate-limiter.ts +266 -0
  79. package/src/resource-registry.ts +530 -0
  80. package/src/sampling.ts +363 -0
  81. package/src/schema-validator.ts +213 -0
  82. package/src/server.ts +1134 -0
  83. package/src/session-manager.ts +339 -0
  84. package/src/task-manager.ts +427 -0
  85. package/src/tool-registry.ts +475 -0
  86. package/src/transport/http.ts +532 -0
  87. package/src/transport/index.ts +233 -0
  88. package/src/transport/stdio.ts +252 -0
  89. package/src/transport/websocket.ts +396 -0
  90. package/src/types.ts +664 -0
  91. package/tsconfig.json +20 -0
  92. package/vitest.config.ts +13 -0
@@ -0,0 +1,296 @@
1
+ /**
2
+ * @claude-flow/mcp - Prompt Registry
3
+ *
4
+ * MCP 2025-11-25 compliant prompt management
5
+ * Supports: list, get, arguments, templates, embedded resources
6
+ */
7
+
8
+ import { EventEmitter } from 'events';
9
+ import type {
10
+ MCPPrompt,
11
+ PromptArgument,
12
+ PromptMessage,
13
+ PromptListResult,
14
+ PromptGetResult,
15
+ PromptContent,
16
+ TextContent,
17
+ EmbeddedResource,
18
+ ResourceContent,
19
+ ILogger,
20
+ } from './types.js';
21
+
22
+ export type PromptHandler = (
23
+ args: Record<string, string>
24
+ ) => Promise<PromptMessage[]>;
25
+
26
+ export interface PromptDefinition extends MCPPrompt {
27
+ handler: PromptHandler;
28
+ }
29
+
30
+ export interface PromptRegistryOptions {
31
+ maxPrompts?: number;
32
+ validateArguments?: boolean;
33
+ }
34
+
35
+ export class PromptRegistry extends EventEmitter {
36
+ private prompts: Map<string, PromptDefinition> = new Map();
37
+
38
+ private readonly options: Required<PromptRegistryOptions>;
39
+
40
+ constructor(
41
+ private readonly logger: ILogger,
42
+ options: PromptRegistryOptions = {}
43
+ ) {
44
+ super();
45
+ this.options = {
46
+ maxPrompts: options.maxPrompts ?? 1000,
47
+ validateArguments: options.validateArguments ?? true,
48
+ };
49
+ }
50
+
51
+ /**
52
+ * Register a prompt
53
+ */
54
+ register(prompt: PromptDefinition): boolean {
55
+ if (this.prompts.size >= this.options.maxPrompts) {
56
+ this.logger.error('Maximum prompts reached', { max: this.options.maxPrompts });
57
+ return false;
58
+ }
59
+
60
+ if (this.prompts.has(prompt.name)) {
61
+ this.logger.warn('Prompt already registered', { name: prompt.name });
62
+ return false;
63
+ }
64
+
65
+ this.prompts.set(prompt.name, prompt);
66
+
67
+ this.logger.debug('Prompt registered', { name: prompt.name });
68
+ this.emit('prompt:registered', { name: prompt.name });
69
+ this.emitListChanged();
70
+
71
+ return true;
72
+ }
73
+
74
+ /**
75
+ * Unregister a prompt
76
+ */
77
+ unregister(name: string): boolean {
78
+ if (!this.prompts.has(name)) {
79
+ return false;
80
+ }
81
+
82
+ this.prompts.delete(name);
83
+
84
+ this.logger.debug('Prompt unregistered', { name });
85
+ this.emit('prompt:unregistered', { name });
86
+ this.emitListChanged();
87
+
88
+ return true;
89
+ }
90
+
91
+ /**
92
+ * List prompts with pagination
93
+ */
94
+ list(cursor?: string, pageSize: number = 50): PromptListResult {
95
+ const allPrompts = Array.from(this.prompts.values()).map((p) => ({
96
+ name: p.name,
97
+ title: p.title,
98
+ description: p.description,
99
+ arguments: p.arguments,
100
+ }));
101
+
102
+ let startIndex = 0;
103
+ if (cursor) {
104
+ const decoded = this.decodeCursor(cursor);
105
+ startIndex = decoded.offset;
106
+ }
107
+
108
+ const endIndex = Math.min(startIndex + pageSize, allPrompts.length);
109
+ const prompts = allPrompts.slice(startIndex, endIndex);
110
+
111
+ const result: PromptListResult = { prompts };
112
+
113
+ if (endIndex < allPrompts.length) {
114
+ result.nextCursor = this.encodeCursor({ offset: endIndex });
115
+ }
116
+
117
+ return result;
118
+ }
119
+
120
+ /**
121
+ * Get a prompt with arguments
122
+ */
123
+ async get(
124
+ name: string,
125
+ args: Record<string, string> = {}
126
+ ): Promise<PromptGetResult> {
127
+ const prompt = this.prompts.get(name);
128
+ if (!prompt) {
129
+ throw new Error(`Prompt not found: ${name}`);
130
+ }
131
+
132
+ // Validate required arguments
133
+ if (this.options.validateArguments && prompt.arguments) {
134
+ for (const arg of prompt.arguments) {
135
+ if (arg.required && !(arg.name in args)) {
136
+ throw new Error(`Missing required argument: ${arg.name}`);
137
+ }
138
+ }
139
+ }
140
+
141
+ const messages = await prompt.handler(args);
142
+
143
+ this.emit('prompt:get', { name, argCount: Object.keys(args).length });
144
+
145
+ return {
146
+ description: prompt.description,
147
+ messages,
148
+ };
149
+ }
150
+
151
+ /**
152
+ * Get prompt by name
153
+ */
154
+ getPrompt(name: string): MCPPrompt | undefined {
155
+ const prompt = this.prompts.get(name);
156
+ if (!prompt) return undefined;
157
+
158
+ return {
159
+ name: prompt.name,
160
+ title: prompt.title,
161
+ description: prompt.description,
162
+ arguments: prompt.arguments,
163
+ };
164
+ }
165
+
166
+ /**
167
+ * Check if prompt exists
168
+ */
169
+ hasPrompt(name: string): boolean {
170
+ return this.prompts.has(name);
171
+ }
172
+
173
+ /**
174
+ * Get prompt count
175
+ */
176
+ getPromptCount(): number {
177
+ return this.prompts.size;
178
+ }
179
+
180
+ /**
181
+ * Get stats
182
+ */
183
+ getStats(): {
184
+ totalPrompts: number;
185
+ promptsWithArgs: number;
186
+ } {
187
+ let promptsWithArgs = 0;
188
+ for (const prompt of this.prompts.values()) {
189
+ if (prompt.arguments && prompt.arguments.length > 0) {
190
+ promptsWithArgs++;
191
+ }
192
+ }
193
+
194
+ return {
195
+ totalPrompts: this.prompts.size,
196
+ promptsWithArgs,
197
+ };
198
+ }
199
+
200
+ /**
201
+ * Encode cursor for pagination
202
+ */
203
+ private encodeCursor(data: { offset: number }): string {
204
+ return Buffer.from(JSON.stringify(data)).toString('base64');
205
+ }
206
+
207
+ /**
208
+ * Decode cursor for pagination
209
+ */
210
+ private decodeCursor(cursor: string): { offset: number } {
211
+ try {
212
+ return JSON.parse(Buffer.from(cursor, 'base64').toString('utf-8'));
213
+ } catch {
214
+ return { offset: 0 };
215
+ }
216
+ }
217
+
218
+ /**
219
+ * Emit listChanged notification
220
+ */
221
+ private emitListChanged(): void {
222
+ this.emit('prompts:listChanged');
223
+ }
224
+ }
225
+
226
+ export function createPromptRegistry(
227
+ logger: ILogger,
228
+ options?: PromptRegistryOptions
229
+ ): PromptRegistry {
230
+ return new PromptRegistry(logger, options);
231
+ }
232
+
233
+ /**
234
+ * Helper to define a prompt
235
+ */
236
+ export function definePrompt(
237
+ name: string,
238
+ description: string,
239
+ handler: PromptHandler,
240
+ options?: {
241
+ title?: string;
242
+ arguments?: PromptArgument[];
243
+ }
244
+ ): PromptDefinition {
245
+ return {
246
+ name,
247
+ description,
248
+ title: options?.title,
249
+ arguments: options?.arguments,
250
+ handler,
251
+ };
252
+ }
253
+
254
+ /**
255
+ * Helper to create a text message
256
+ */
257
+ export function textMessage(
258
+ role: 'user' | 'assistant',
259
+ text: string
260
+ ): PromptMessage {
261
+ return {
262
+ role,
263
+ content: {
264
+ type: 'text',
265
+ text,
266
+ } as TextContent,
267
+ };
268
+ }
269
+
270
+ /**
271
+ * Helper to create a message with embedded resource
272
+ */
273
+ export function resourceMessage(
274
+ role: 'user' | 'assistant',
275
+ resource: ResourceContent
276
+ ): PromptMessage {
277
+ return {
278
+ role,
279
+ content: {
280
+ type: 'resource',
281
+ resource,
282
+ } as EmbeddedResource,
283
+ };
284
+ }
285
+
286
+ /**
287
+ * Template string interpolation for prompts
288
+ */
289
+ export function interpolate(
290
+ template: string,
291
+ args: Record<string, string>
292
+ ): string {
293
+ return template.replace(/\{(\w+)\}/g, (match, key) => {
294
+ return args[key] ?? match;
295
+ });
296
+ }
@@ -0,0 +1,266 @@
1
+ /**
2
+ * @claude-flow/mcp - Rate Limiter
3
+ *
4
+ * Token bucket rate limiting for DoS protection
5
+ */
6
+
7
+ import { EventEmitter } from 'events';
8
+ import type { ILogger } from './types.js';
9
+
10
+ export interface RateLimitConfig {
11
+ /** Requests per second */
12
+ requestsPerSecond: number;
13
+ /** Burst size (max tokens) */
14
+ burstSize: number;
15
+ /** Per-session limits (if different from global) */
16
+ perSessionLimit?: number;
17
+ /** Cleanup interval for expired sessions */
18
+ cleanupInterval?: number;
19
+ }
20
+
21
+ export interface RateLimitResult {
22
+ allowed: boolean;
23
+ remaining: number;
24
+ resetIn: number; // ms until bucket refills
25
+ retryAfter?: number; // seconds to wait (for 429 response)
26
+ }
27
+
28
+ interface TokenBucket {
29
+ tokens: number;
30
+ lastRefill: number;
31
+ }
32
+
33
+ const DEFAULT_CONFIG: RateLimitConfig = {
34
+ requestsPerSecond: 100,
35
+ burstSize: 200,
36
+ perSessionLimit: 50,
37
+ cleanupInterval: 60000, // 1 minute
38
+ };
39
+
40
+ export class RateLimiter extends EventEmitter {
41
+ private readonly config: Required<RateLimitConfig>;
42
+ private globalBucket: TokenBucket;
43
+ private sessionBuckets: Map<string, TokenBucket> = new Map();
44
+ private cleanupTimer?: NodeJS.Timeout;
45
+
46
+ constructor(
47
+ private readonly logger: ILogger,
48
+ config: Partial<RateLimitConfig> = {}
49
+ ) {
50
+ super();
51
+ this.config = { ...DEFAULT_CONFIG, ...config } as Required<RateLimitConfig>;
52
+
53
+ // Initialize global bucket
54
+ this.globalBucket = {
55
+ tokens: this.config.burstSize,
56
+ lastRefill: Date.now(),
57
+ };
58
+
59
+ this.startCleanup();
60
+ }
61
+
62
+ /**
63
+ * Check if request is allowed (global limit)
64
+ */
65
+ checkGlobal(): RateLimitResult {
66
+ return this.checkBucket(this.globalBucket, this.config.requestsPerSecond, this.config.burstSize);
67
+ }
68
+
69
+ /**
70
+ * Check if request is allowed (per-session limit)
71
+ */
72
+ checkSession(sessionId: string): RateLimitResult {
73
+ let bucket = this.sessionBuckets.get(sessionId);
74
+ if (!bucket) {
75
+ bucket = {
76
+ tokens: this.config.perSessionLimit,
77
+ lastRefill: Date.now(),
78
+ };
79
+ this.sessionBuckets.set(sessionId, bucket);
80
+ }
81
+
82
+ return this.checkBucket(
83
+ bucket,
84
+ this.config.perSessionLimit / 10, // Refill rate (10 seconds to full)
85
+ this.config.perSessionLimit
86
+ );
87
+ }
88
+
89
+ /**
90
+ * Check both global and session limits
91
+ */
92
+ check(sessionId?: string): RateLimitResult {
93
+ const globalResult = this.checkGlobal();
94
+ if (!globalResult.allowed) {
95
+ this.emit('rate-limit:global', { remaining: globalResult.remaining });
96
+ return globalResult;
97
+ }
98
+
99
+ if (sessionId) {
100
+ const sessionResult = this.checkSession(sessionId);
101
+ if (!sessionResult.allowed) {
102
+ this.emit('rate-limit:session', { sessionId, remaining: sessionResult.remaining });
103
+ return sessionResult;
104
+ }
105
+ return sessionResult;
106
+ }
107
+
108
+ return globalResult;
109
+ }
110
+
111
+ /**
112
+ * Consume a token (call after request is processed)
113
+ */
114
+ consume(sessionId?: string): void {
115
+ this.consumeFromBucket(this.globalBucket);
116
+ if (sessionId) {
117
+ const bucket = this.sessionBuckets.get(sessionId);
118
+ if (bucket) {
119
+ this.consumeFromBucket(bucket);
120
+ }
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Reset session bucket
126
+ */
127
+ resetSession(sessionId: string): void {
128
+ this.sessionBuckets.delete(sessionId);
129
+ this.logger.debug('Rate limit session reset', { sessionId });
130
+ }
131
+
132
+ /**
133
+ * Get current stats
134
+ */
135
+ getStats(): {
136
+ globalTokens: number;
137
+ globalBurstSize: number;
138
+ sessionCount: number;
139
+ config: RateLimitConfig;
140
+ } {
141
+ this.refillBucket(this.globalBucket, this.config.requestsPerSecond, this.config.burstSize);
142
+ return {
143
+ globalTokens: Math.floor(this.globalBucket.tokens),
144
+ globalBurstSize: this.config.burstSize,
145
+ sessionCount: this.sessionBuckets.size,
146
+ config: this.config,
147
+ };
148
+ }
149
+
150
+ /**
151
+ * Destroy the rate limiter
152
+ */
153
+ destroy(): void {
154
+ if (this.cleanupTimer) {
155
+ clearInterval(this.cleanupTimer);
156
+ this.cleanupTimer = undefined;
157
+ }
158
+ this.sessionBuckets.clear();
159
+ this.removeAllListeners();
160
+ }
161
+
162
+ /**
163
+ * Check bucket and refill tokens
164
+ */
165
+ private checkBucket(
166
+ bucket: TokenBucket,
167
+ refillRate: number,
168
+ maxTokens: number
169
+ ): RateLimitResult {
170
+ this.refillBucket(bucket, refillRate, maxTokens);
171
+
172
+ if (bucket.tokens >= 1) {
173
+ return {
174
+ allowed: true,
175
+ remaining: Math.floor(bucket.tokens) - 1,
176
+ resetIn: Math.ceil((maxTokens - bucket.tokens) / refillRate * 1000),
177
+ };
178
+ }
179
+
180
+ // Calculate when bucket will have 1 token
181
+ const tokensNeeded = 1 - bucket.tokens;
182
+ const retryAfter = Math.ceil(tokensNeeded / refillRate);
183
+
184
+ return {
185
+ allowed: false,
186
+ remaining: 0,
187
+ resetIn: retryAfter * 1000,
188
+ retryAfter,
189
+ };
190
+ }
191
+
192
+ /**
193
+ * Refill tokens based on elapsed time
194
+ */
195
+ private refillBucket(bucket: TokenBucket, refillRate: number, maxTokens: number): void {
196
+ const now = Date.now();
197
+ const elapsed = (now - bucket.lastRefill) / 1000; // seconds
198
+ const tokensToAdd = elapsed * refillRate;
199
+
200
+ bucket.tokens = Math.min(maxTokens, bucket.tokens + tokensToAdd);
201
+ bucket.lastRefill = now;
202
+ }
203
+
204
+ /**
205
+ * Consume a token from bucket
206
+ */
207
+ private consumeFromBucket(bucket: TokenBucket): void {
208
+ if (bucket.tokens >= 1) {
209
+ bucket.tokens -= 1;
210
+ }
211
+ }
212
+
213
+ /**
214
+ * Start cleanup timer for expired sessions
215
+ */
216
+ private startCleanup(): void {
217
+ this.cleanupTimer = setInterval(() => {
218
+ const now = Date.now();
219
+ const expireTime = this.config.cleanupInterval * 2;
220
+
221
+ for (const [sessionId, bucket] of this.sessionBuckets) {
222
+ if (now - bucket.lastRefill > expireTime) {
223
+ this.sessionBuckets.delete(sessionId);
224
+ this.logger.debug('Rate limit session expired', { sessionId });
225
+ }
226
+ }
227
+ }, this.config.cleanupInterval);
228
+ }
229
+ }
230
+
231
+ export function createRateLimiter(
232
+ logger: ILogger,
233
+ config?: Partial<RateLimitConfig>
234
+ ): RateLimiter {
235
+ return new RateLimiter(logger, config);
236
+ }
237
+
238
+ /**
239
+ * Express/Connect middleware for rate limiting
240
+ */
241
+ export function rateLimitMiddleware(rateLimiter: RateLimiter) {
242
+ return (req: any, res: any, next: () => void) => {
243
+ const sessionId = req.headers['x-session-id'] || req.ip;
244
+ const result = rateLimiter.check(sessionId);
245
+
246
+ res.setHeader('X-RateLimit-Remaining', result.remaining);
247
+ res.setHeader('X-RateLimit-Reset', Math.ceil(Date.now() / 1000) + Math.ceil(result.resetIn / 1000));
248
+
249
+ if (!result.allowed) {
250
+ res.setHeader('Retry-After', result.retryAfter);
251
+ res.status(429).json({
252
+ jsonrpc: '2.0',
253
+ id: null,
254
+ error: {
255
+ code: -32000,
256
+ message: 'Rate limit exceeded',
257
+ data: { retryAfter: result.retryAfter },
258
+ },
259
+ });
260
+ return;
261
+ }
262
+
263
+ rateLimiter.consume(sessionId);
264
+ next();
265
+ };
266
+ }