claude-flow 2.7.33 → 2.7.35
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/.claude/settings.local.json +9 -2
- package/.claude/skills/agentic-jujutsu/SKILL.md +1 -1
- package/CHANGELOG.md +140 -0
- package/bin/claude-flow +1 -1
- package/dist/src/cli/commands/mcp.js +61 -7
- package/dist/src/cli/commands/mcp.js.map +1 -1
- package/dist/src/cli/init/index.js +55 -33
- package/dist/src/cli/init/index.js.map +1 -1
- package/dist/src/cli/simple-cli.js +182 -172
- package/dist/src/cli/simple-cli.js.map +1 -1
- package/dist/src/cli/simple-commands/init/agent-copier.js +9 -3
- package/dist/src/cli/simple-commands/init/agent-copier.js.map +1 -1
- package/dist/src/core/DatabaseManager.js +39 -9
- package/dist/src/core/DatabaseManager.js.map +1 -1
- package/dist/src/mcp/async/job-manager-mcp25.js +240 -0
- package/dist/src/mcp/async/job-manager-mcp25.js.map +1 -0
- package/dist/src/mcp/index.js +8 -0
- package/dist/src/mcp/index.js.map +1 -1
- package/dist/src/mcp/protocol/version-negotiation.js +182 -0
- package/dist/src/mcp/protocol/version-negotiation.js.map +1 -0
- package/dist/src/mcp/registry/mcp-registry-client-2025.js +210 -0
- package/dist/src/mcp/registry/mcp-registry-client-2025.js.map +1 -0
- package/dist/src/mcp/server-factory.js +189 -0
- package/dist/src/mcp/server-factory.js.map +1 -0
- package/dist/src/mcp/server-mcp-2025.js +283 -0
- package/dist/src/mcp/server-mcp-2025.js.map +1 -0
- package/dist/src/mcp/tool-registry-progressive.js +319 -0
- package/dist/src/mcp/tool-registry-progressive.js.map +1 -0
- package/dist/src/mcp/tools/_template.js +62 -0
- package/dist/src/mcp/tools/_template.js.map +1 -0
- package/dist/src/mcp/tools/loader.js +228 -0
- package/dist/src/mcp/tools/loader.js.map +1 -0
- package/dist/src/mcp/tools/system/search.js +224 -0
- package/dist/src/mcp/tools/system/search.js.map +1 -0
- package/dist/src/mcp/tools/system/status.js +168 -0
- package/dist/src/mcp/tools/system/status.js.map +1 -0
- package/dist/src/mcp/validation/schema-validator-2025.js +198 -0
- package/dist/src/mcp/validation/schema-validator-2025.js.map +1 -0
- package/dist/src/utils/error-recovery.js +215 -0
- package/dist/src/utils/error-recovery.js.map +1 -0
- package/dist/src/utils/metrics-reader.js +10 -0
- package/dist/src/utils/metrics-reader.js.map +1 -1
- package/docs/.claude-flow/metrics/performance.json +3 -3
- package/docs/.claude-flow/metrics/task-metrics.json +3 -3
- package/docs/.github-release-issue-v2.7.33.md +488 -0
- package/docs/AGENTDB_BRANCH_MERGE_VERIFICATION.md +436 -0
- package/docs/AUTOMATIC_ERROR_RECOVERY_v2.7.35.md +321 -0
- package/docs/BRANCH_REVIEW_SUMMARY.md +439 -0
- package/docs/CONFIRMATION_AUTOMATIC_ERROR_RECOVERY.md +384 -0
- package/docs/DEEP_CODE_REVIEW_v2.7.33.md +1159 -0
- package/docs/DOCKER_TEST_RESULTS_v2.7.35.md +305 -0
- package/docs/MCP_2025_FEATURE_CONFIRMATION.md +698 -0
- package/docs/NPM_PUBLISH_GUIDE_v2.7.33.md +628 -0
- package/docs/REGRESSION_TEST_REPORT_v2.7.33.md +397 -0
- package/docs/RELEASE_NOTES_v2.7.33.md +618 -0
- package/docs/RELEASE_READINESS_SUMMARY.md +377 -0
- package/docs/RELEASE_SUMMARY_v2.7.33.md +456 -0
- package/docs/agentic-flow-agentdb-mcp-integration.md +1198 -0
- package/docs/features/automatic-error-recovery.md +333 -0
- package/docs/github-issues/README.md +88 -0
- package/docs/github-issues/wsl-enotempty-automatic-recovery.md +470 -0
- package/docs/mcp-2025-implementation-summary.md +459 -0
- package/docs/mcp-spec-2025-implementation-plan.md +1330 -0
- package/docs/phase-1-2-implementation-summary.md +676 -0
- package/docs/regression-analysis-phase-1-2.md +555 -0
- package/docs/troubleshooting/wsl-better-sqlite3-error.md +239 -0
- package/package.json +5 -2
- package/scripts/create-github-issue.sh +64 -0
- package/scripts/test-docker-wsl.sh +198 -0
- package/src/cli/commands/mcp.ts +86 -9
- package/src/cli/init/index.ts +72 -42
- package/src/cli/simple-commands/init/agent-copier.js +10 -5
- package/src/core/DatabaseManager.ts +55 -9
- package/src/mcp/async/job-manager-mcp25.ts +456 -0
- package/src/mcp/index.ts +60 -0
- package/src/mcp/protocol/version-negotiation.ts +329 -0
- package/src/mcp/registry/mcp-registry-client-2025.ts +334 -0
- package/src/mcp/server-factory.ts +426 -0
- package/src/mcp/server-mcp-2025.ts +507 -0
- package/src/mcp/tool-registry-progressive.ts +539 -0
- package/src/mcp/tools/_template.ts +174 -0
- package/src/mcp/tools/loader.ts +362 -0
- package/src/mcp/tools/system/search.ts +276 -0
- package/src/mcp/tools/system/status.ts +206 -0
- package/src/mcp/validation/schema-validator-2025.ts +294 -0
- package/src/utils/error-recovery.ts +325 -0
- package/docs/AGENTDB_V1.6.1_DEEP_REVIEW.md +0 -386
- package/docs/AGENT_FOLDER_STRUCTURE_FIX.md +0 -192
- package/docs/RECENT_RELEASES_SUMMARY.md +0 -375
- package/docs/V2.7.31_RELEASE_NOTES.md +0 -375
- /package/.claude/agents/analysis/{analyze-code-quality.md → code-review/analyze-code-quality.md} +0 -0
- /package/.claude/agents/architecture/{arch-system-design.md → system-design/arch-system-design.md} +0 -0
- /package/.claude/agents/data/{data-ml-model.md → ml/data-ml-model.md} +0 -0
- /package/.claude/agents/development/{dev-backend-api.md → backend/dev-backend-api.md} +0 -0
- /package/.claude/agents/devops/{ops-cicd-github.md → ci-cd/ops-cicd-github.md} +0 -0
- /package/.claude/agents/documentation/{docs-api-openapi.md → api-docs/docs-api-openapi.md} +0 -0
- /package/.claude/agents/specialized/{spec-mobile-react-native.md → mobile/spec-mobile-react-native.md} +0 -0
- /package/.claude/agents/testing/{tdd-london-swarm.md → unit/tdd-london-swarm.md} +0 -0
- /package/.claude/agents/testing/{production-validator.md → validation/production-validator.md} +0 -0
|
@@ -0,0 +1,1330 @@
|
|
|
1
|
+
# MCP Spec 2025 Implementation Plan for Claude Flow
|
|
2
|
+
|
|
3
|
+
**Timeline**: RC November 14, 2025 → Final November 25, 2025
|
|
4
|
+
**Status**: Implementation Phase
|
|
5
|
+
**Priority**: CRITICAL - Spec-breaking changes require immediate action
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Executive Summary
|
|
10
|
+
|
|
11
|
+
The upcoming MCP spec introduces **3 major changes** that require updates to Claude Flow:
|
|
12
|
+
|
|
13
|
+
1. **Async Operations** (First-class support for long-running tasks)
|
|
14
|
+
2. **Registry-Backed Discovery** (Formal MCP Registry integration)
|
|
15
|
+
3. **Code Execution Pattern** (98% token reduction - already planned)
|
|
16
|
+
|
|
17
|
+
Combined with Anthropic's engineering guidance, these changes will:
|
|
18
|
+
- Enable resumable workflows without blocking
|
|
19
|
+
- Provide automatic server discovery via MCP Registry
|
|
20
|
+
- Reduce token usage by 98% (150k → 2k tokens)
|
|
21
|
+
- Improve privacy with execution-environment data processing
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## 🎯 Implementation Phases
|
|
26
|
+
|
|
27
|
+
### Phase 0: Pre-RC Preparation (Before Nov 14)
|
|
28
|
+
**Goal**: Have infrastructure ready for RC validation
|
|
29
|
+
|
|
30
|
+
### Phase 1: RC Validation Window (Nov 14-25)
|
|
31
|
+
**Goal**: Test compatibility and fix issues before final spec
|
|
32
|
+
|
|
33
|
+
### Phase 2: Production Rollout (After Nov 25)
|
|
34
|
+
**Goal**: Deploy to production with full spec compliance
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## 📋 Detailed Implementation Plan
|
|
39
|
+
|
|
40
|
+
## PHASE 0A: Async Operations Architecture (PRIORITY 1)
|
|
41
|
+
|
|
42
|
+
### Overview
|
|
43
|
+
MCP will support first-class async operations:
|
|
44
|
+
- Servers start long-running work and return job handles
|
|
45
|
+
- Clients poll or resume to retrieve results without blocking
|
|
46
|
+
- Job state persisted for resumability
|
|
47
|
+
|
|
48
|
+
### Current State: Claude Flow
|
|
49
|
+
- Tools execute synchronously in `handler()` functions
|
|
50
|
+
- No job persistence or state management
|
|
51
|
+
- Long-running tasks block the MCP connection
|
|
52
|
+
|
|
53
|
+
### Required Changes
|
|
54
|
+
|
|
55
|
+
#### 1. Job Management System
|
|
56
|
+
|
|
57
|
+
**Create**: `src/mcp/async/job-manager.ts`
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
import { EventEmitter } from 'events';
|
|
61
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
62
|
+
|
|
63
|
+
export type JobStatus =
|
|
64
|
+
| 'queued' // Job submitted but not started
|
|
65
|
+
| 'running' // Job in progress
|
|
66
|
+
| 'completed' // Job finished successfully
|
|
67
|
+
| 'failed' // Job failed with error
|
|
68
|
+
| 'cancelled'; // Job cancelled by user
|
|
69
|
+
|
|
70
|
+
export interface AsyncJob<TInput = any, TResult = any> {
|
|
71
|
+
id: string;
|
|
72
|
+
toolName: string;
|
|
73
|
+
status: JobStatus;
|
|
74
|
+
progress?: number; // 0-100
|
|
75
|
+
input: TInput;
|
|
76
|
+
result?: TResult;
|
|
77
|
+
error?: {
|
|
78
|
+
code: string;
|
|
79
|
+
message: string;
|
|
80
|
+
details?: any;
|
|
81
|
+
};
|
|
82
|
+
metadata: {
|
|
83
|
+
createdAt: Date;
|
|
84
|
+
startedAt?: Date;
|
|
85
|
+
completedAt?: Date;
|
|
86
|
+
estimatedDuration?: number; // milliseconds
|
|
87
|
+
actualDuration?: number;
|
|
88
|
+
};
|
|
89
|
+
context?: any; // Tool context
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface JobHandle {
|
|
93
|
+
jobId: string;
|
|
94
|
+
status: JobStatus;
|
|
95
|
+
progress?: number;
|
|
96
|
+
pollUrl?: string; // URL to poll for status
|
|
97
|
+
webhookUrl?: string; // Optional webhook for completion
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Manages async job lifecycle for MCP tools
|
|
102
|
+
* Supports polling, resumption, and cancellation
|
|
103
|
+
*/
|
|
104
|
+
export class JobManager extends EventEmitter {
|
|
105
|
+
private jobs: Map<string, AsyncJob> = new Map();
|
|
106
|
+
private executors: Map<string, Promise<any>> = new Map();
|
|
107
|
+
|
|
108
|
+
constructor(
|
|
109
|
+
private maxJobs: number = 1000,
|
|
110
|
+
private jobTTL: number = 86400000 // 24 hours
|
|
111
|
+
) {
|
|
112
|
+
super();
|
|
113
|
+
|
|
114
|
+
// Cleanup expired jobs every hour
|
|
115
|
+
setInterval(() => this.cleanupExpiredJobs(), 3600000);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Submit a new async job
|
|
120
|
+
* Returns job handle immediately, execution happens in background
|
|
121
|
+
*/
|
|
122
|
+
async submitJob<TInput, TResult>(
|
|
123
|
+
toolName: string,
|
|
124
|
+
input: TInput,
|
|
125
|
+
executor: (input: TInput, progress: (percent: number) => void) => Promise<TResult>,
|
|
126
|
+
context?: any
|
|
127
|
+
): Promise<JobHandle> {
|
|
128
|
+
// Check capacity
|
|
129
|
+
if (this.jobs.size >= this.maxJobs) {
|
|
130
|
+
this.cleanupExpiredJobs();
|
|
131
|
+
if (this.jobs.size >= this.maxJobs) {
|
|
132
|
+
throw new Error('Job queue full. Please try again later.');
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Create job
|
|
137
|
+
const jobId = uuidv4();
|
|
138
|
+
const job: AsyncJob<TInput, TResult> = {
|
|
139
|
+
id: jobId,
|
|
140
|
+
toolName,
|
|
141
|
+
status: 'queued',
|
|
142
|
+
input,
|
|
143
|
+
metadata: {
|
|
144
|
+
createdAt: new Date(),
|
|
145
|
+
},
|
|
146
|
+
context,
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
this.jobs.set(jobId, job);
|
|
150
|
+
|
|
151
|
+
// Start execution in background
|
|
152
|
+
this.executeJob(job, executor);
|
|
153
|
+
|
|
154
|
+
// Return handle immediately
|
|
155
|
+
return {
|
|
156
|
+
jobId,
|
|
157
|
+
status: 'queued',
|
|
158
|
+
pollUrl: `/mcp/jobs/${jobId}`,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Execute job in background
|
|
164
|
+
*/
|
|
165
|
+
private async executeJob<TInput, TResult>(
|
|
166
|
+
job: AsyncJob<TInput, TResult>,
|
|
167
|
+
executor: (input: TInput, progress: (percent: number) => void) => Promise<TResult>
|
|
168
|
+
): Promise<void> {
|
|
169
|
+
// Update status to running
|
|
170
|
+
job.status = 'running';
|
|
171
|
+
job.metadata.startedAt = new Date();
|
|
172
|
+
this.emit('job:started', job.id);
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
// Execute with progress callback
|
|
176
|
+
const progressCallback = (percent: number) => {
|
|
177
|
+
job.progress = Math.min(100, Math.max(0, percent));
|
|
178
|
+
this.emit('job:progress', job.id, job.progress);
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const result = await executor(job.input, progressCallback);
|
|
182
|
+
|
|
183
|
+
// Mark completed
|
|
184
|
+
job.status = 'completed';
|
|
185
|
+
job.result = result;
|
|
186
|
+
job.progress = 100;
|
|
187
|
+
job.metadata.completedAt = new Date();
|
|
188
|
+
job.metadata.actualDuration =
|
|
189
|
+
job.metadata.completedAt.getTime() - job.metadata.startedAt!.getTime();
|
|
190
|
+
|
|
191
|
+
this.emit('job:completed', job.id, result);
|
|
192
|
+
} catch (error: any) {
|
|
193
|
+
// Mark failed
|
|
194
|
+
job.status = 'failed';
|
|
195
|
+
job.error = {
|
|
196
|
+
code: error.code || 'EXECUTION_ERROR',
|
|
197
|
+
message: error.message,
|
|
198
|
+
details: error.details || error.stack,
|
|
199
|
+
};
|
|
200
|
+
job.metadata.completedAt = new Date();
|
|
201
|
+
|
|
202
|
+
this.emit('job:failed', job.id, error);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Get job status (for polling)
|
|
208
|
+
*/
|
|
209
|
+
getJobStatus(jobId: string): JobHandle | null {
|
|
210
|
+
const job = this.jobs.get(jobId);
|
|
211
|
+
if (!job) return null;
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
jobId: job.id,
|
|
215
|
+
status: job.status,
|
|
216
|
+
progress: job.progress,
|
|
217
|
+
pollUrl: `/mcp/jobs/${jobId}`,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Get full job details
|
|
223
|
+
*/
|
|
224
|
+
getJob(jobId: string): AsyncJob | null {
|
|
225
|
+
return this.jobs.get(jobId) || null;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Get job result (blocks until completed or failed)
|
|
230
|
+
*/
|
|
231
|
+
async waitForJob<TResult>(
|
|
232
|
+
jobId: string,
|
|
233
|
+
timeout: number = 300000 // 5 minutes default
|
|
234
|
+
): Promise<TResult> {
|
|
235
|
+
const job = this.jobs.get(jobId);
|
|
236
|
+
if (!job) {
|
|
237
|
+
throw new Error(`Job not found: ${jobId}`);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// If already completed, return immediately
|
|
241
|
+
if (job.status === 'completed') {
|
|
242
|
+
return job.result as TResult;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (job.status === 'failed') {
|
|
246
|
+
throw new Error(job.error?.message || 'Job failed');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Wait for completion
|
|
250
|
+
return new Promise((resolve, reject) => {
|
|
251
|
+
const timeoutHandle = setTimeout(() => {
|
|
252
|
+
this.removeListener('job:completed', completedHandler);
|
|
253
|
+
this.removeListener('job:failed', failedHandler);
|
|
254
|
+
reject(new Error('Job timeout'));
|
|
255
|
+
}, timeout);
|
|
256
|
+
|
|
257
|
+
const completedHandler = (completedJobId: string, result: TResult) => {
|
|
258
|
+
if (completedJobId === jobId) {
|
|
259
|
+
clearTimeout(timeoutHandle);
|
|
260
|
+
this.removeListener('job:failed', failedHandler);
|
|
261
|
+
resolve(result);
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
const failedHandler = (failedJobId: string, error: Error) => {
|
|
266
|
+
if (failedJobId === jobId) {
|
|
267
|
+
clearTimeout(timeoutHandle);
|
|
268
|
+
this.removeListener('job:completed', completedHandler);
|
|
269
|
+
reject(error);
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
this.on('job:completed', completedHandler);
|
|
274
|
+
this.on('job:failed', failedHandler);
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Cancel a running job
|
|
280
|
+
*/
|
|
281
|
+
async cancelJob(jobId: string): Promise<boolean> {
|
|
282
|
+
const job = this.jobs.get(jobId);
|
|
283
|
+
if (!job) return false;
|
|
284
|
+
|
|
285
|
+
if (job.status === 'completed' || job.status === 'failed') {
|
|
286
|
+
return false; // Already finished
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
job.status = 'cancelled';
|
|
290
|
+
job.metadata.completedAt = new Date();
|
|
291
|
+
|
|
292
|
+
this.emit('job:cancelled', jobId);
|
|
293
|
+
return true;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* List jobs with filtering
|
|
298
|
+
*/
|
|
299
|
+
listJobs(filter?: {
|
|
300
|
+
status?: JobStatus;
|
|
301
|
+
toolName?: string;
|
|
302
|
+
limit?: number;
|
|
303
|
+
}): AsyncJob[] {
|
|
304
|
+
let jobs = Array.from(this.jobs.values());
|
|
305
|
+
|
|
306
|
+
if (filter?.status) {
|
|
307
|
+
jobs = jobs.filter(j => j.status === filter.status);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (filter?.toolName) {
|
|
311
|
+
jobs = jobs.filter(j => j.toolName === filter.toolName);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Sort by creation time (newest first)
|
|
315
|
+
jobs.sort((a, b) =>
|
|
316
|
+
b.metadata.createdAt.getTime() - a.metadata.createdAt.getTime()
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
if (filter?.limit) {
|
|
320
|
+
jobs = jobs.slice(0, filter.limit);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return jobs;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Cleanup expired jobs
|
|
328
|
+
*/
|
|
329
|
+
private cleanupExpiredJobs(): number {
|
|
330
|
+
const now = Date.now();
|
|
331
|
+
let cleaned = 0;
|
|
332
|
+
|
|
333
|
+
for (const [jobId, job] of this.jobs.entries()) {
|
|
334
|
+
const age = now - job.metadata.createdAt.getTime();
|
|
335
|
+
|
|
336
|
+
// Remove if expired and not running
|
|
337
|
+
if (age > this.jobTTL && job.status !== 'running') {
|
|
338
|
+
this.jobs.delete(jobId);
|
|
339
|
+
cleaned++;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return cleaned;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Get metrics
|
|
348
|
+
*/
|
|
349
|
+
getMetrics() {
|
|
350
|
+
const jobs = Array.from(this.jobs.values());
|
|
351
|
+
|
|
352
|
+
return {
|
|
353
|
+
total: jobs.length,
|
|
354
|
+
byStatus: {
|
|
355
|
+
queued: jobs.filter(j => j.status === 'queued').length,
|
|
356
|
+
running: jobs.filter(j => j.status === 'running').length,
|
|
357
|
+
completed: jobs.filter(j => j.status === 'completed').length,
|
|
358
|
+
failed: jobs.filter(j => j.status === 'failed').length,
|
|
359
|
+
cancelled: jobs.filter(j => j.status === 'cancelled').length,
|
|
360
|
+
},
|
|
361
|
+
averageDuration: this.calculateAverageDuration(jobs),
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
private calculateAverageDuration(jobs: AsyncJob[]): number {
|
|
366
|
+
const completed = jobs.filter(j =>
|
|
367
|
+
j.status === 'completed' && j.metadata.actualDuration
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
if (completed.length === 0) return 0;
|
|
371
|
+
|
|
372
|
+
const total = completed.reduce((sum, j) =>
|
|
373
|
+
sum + (j.metadata.actualDuration || 0), 0
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
return total / completed.length;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
#### 2. Async Tool Wrapper
|
|
382
|
+
|
|
383
|
+
**Create**: `src/mcp/async/async-tool.ts`
|
|
384
|
+
|
|
385
|
+
```typescript
|
|
386
|
+
import type { MCPTool } from '../types.js';
|
|
387
|
+
import type { ILogger } from '../../interfaces/logger.js';
|
|
388
|
+
import { JobManager, JobHandle } from './job-manager.js';
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Wraps synchronous tools to support async execution
|
|
392
|
+
*/
|
|
393
|
+
export function makeToolAsync(
|
|
394
|
+
tool: MCPTool,
|
|
395
|
+
jobManager: JobManager,
|
|
396
|
+
logger: ILogger
|
|
397
|
+
): MCPTool {
|
|
398
|
+
return {
|
|
399
|
+
...tool,
|
|
400
|
+
name: `${tool.name}:async`,
|
|
401
|
+
description: `${tool.description} (Async version - returns job handle immediately)`,
|
|
402
|
+
|
|
403
|
+
// Add async-specific parameters
|
|
404
|
+
inputSchema: {
|
|
405
|
+
type: 'object',
|
|
406
|
+
properties: {
|
|
407
|
+
...((tool.inputSchema as any).properties || {}),
|
|
408
|
+
_async: {
|
|
409
|
+
type: 'object',
|
|
410
|
+
properties: {
|
|
411
|
+
mode: {
|
|
412
|
+
type: 'string',
|
|
413
|
+
enum: ['fire-and-forget', 'wait', 'poll'],
|
|
414
|
+
description: 'Execution mode',
|
|
415
|
+
default: 'poll',
|
|
416
|
+
},
|
|
417
|
+
timeout: {
|
|
418
|
+
type: 'number',
|
|
419
|
+
description: 'Timeout in ms (for wait mode)',
|
|
420
|
+
default: 300000,
|
|
421
|
+
},
|
|
422
|
+
webhook: {
|
|
423
|
+
type: 'string',
|
|
424
|
+
description: 'Webhook URL for completion notification',
|
|
425
|
+
},
|
|
426
|
+
},
|
|
427
|
+
},
|
|
428
|
+
},
|
|
429
|
+
required: (tool.inputSchema as any).required || [],
|
|
430
|
+
},
|
|
431
|
+
|
|
432
|
+
handler: async (input: any, context?: any) => {
|
|
433
|
+
const { _async, ...toolInput } = input;
|
|
434
|
+
const mode = _async?.mode || 'poll';
|
|
435
|
+
|
|
436
|
+
// Submit job
|
|
437
|
+
const jobHandle = await jobManager.submitJob(
|
|
438
|
+
tool.name,
|
|
439
|
+
toolInput,
|
|
440
|
+
async (input, onProgress) => {
|
|
441
|
+
// Execute original tool
|
|
442
|
+
return await tool.handler(input, context);
|
|
443
|
+
},
|
|
444
|
+
context
|
|
445
|
+
);
|
|
446
|
+
|
|
447
|
+
logger.info('Async job submitted', {
|
|
448
|
+
jobId: jobHandle.jobId,
|
|
449
|
+
tool: tool.name,
|
|
450
|
+
mode
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
// Handle different modes
|
|
454
|
+
if (mode === 'fire-and-forget') {
|
|
455
|
+
// Return job handle immediately
|
|
456
|
+
return {
|
|
457
|
+
async: true,
|
|
458
|
+
jobHandle,
|
|
459
|
+
message: 'Job submitted. Use jobs/status to check progress.',
|
|
460
|
+
};
|
|
461
|
+
} else if (mode === 'wait') {
|
|
462
|
+
// Wait for completion
|
|
463
|
+
try {
|
|
464
|
+
const result = await jobManager.waitForJob(
|
|
465
|
+
jobHandle.jobId,
|
|
466
|
+
_async?.timeout || 300000
|
|
467
|
+
);
|
|
468
|
+
|
|
469
|
+
return {
|
|
470
|
+
async: true,
|
|
471
|
+
jobHandle,
|
|
472
|
+
completed: true,
|
|
473
|
+
result,
|
|
474
|
+
};
|
|
475
|
+
} catch (error: any) {
|
|
476
|
+
return {
|
|
477
|
+
async: true,
|
|
478
|
+
jobHandle,
|
|
479
|
+
completed: false,
|
|
480
|
+
error: error.message,
|
|
481
|
+
message: 'Use jobs/status to check current state.',
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
} else {
|
|
485
|
+
// Poll mode (default)
|
|
486
|
+
return {
|
|
487
|
+
async: true,
|
|
488
|
+
jobHandle,
|
|
489
|
+
message: 'Job submitted. Poll the jobHandle.pollUrl to check status.',
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
},
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
#### 3. Job Management Tools
|
|
498
|
+
|
|
499
|
+
**Create**: `src/mcp/tools/jobs/status.ts`
|
|
500
|
+
|
|
501
|
+
```typescript
|
|
502
|
+
import type { MCPTool, ClaudeFlowToolContext } from '../../types.js';
|
|
503
|
+
import type { ILogger } from '../../../interfaces/logger.js';
|
|
504
|
+
|
|
505
|
+
export function createJobStatusTool(logger: ILogger): MCPTool {
|
|
506
|
+
return {
|
|
507
|
+
name: 'jobs/status',
|
|
508
|
+
description: 'Get status of an async job. Use for polling long-running operations.',
|
|
509
|
+
|
|
510
|
+
inputSchema: {
|
|
511
|
+
type: 'object',
|
|
512
|
+
properties: {
|
|
513
|
+
jobId: {
|
|
514
|
+
type: 'string',
|
|
515
|
+
description: 'Job ID returned from async tool call',
|
|
516
|
+
},
|
|
517
|
+
},
|
|
518
|
+
required: ['jobId'],
|
|
519
|
+
},
|
|
520
|
+
|
|
521
|
+
metadata: {
|
|
522
|
+
category: 'jobs',
|
|
523
|
+
tags: ['async', 'status', 'polling'],
|
|
524
|
+
detailLevel: 'standard',
|
|
525
|
+
},
|
|
526
|
+
|
|
527
|
+
handler: async (input: any, context?: ClaudeFlowToolContext) => {
|
|
528
|
+
if (!context?.jobManager) {
|
|
529
|
+
throw new Error('Job manager not available');
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const { jobId } = input;
|
|
533
|
+
const job = context.jobManager.getJob(jobId);
|
|
534
|
+
|
|
535
|
+
if (!job) {
|
|
536
|
+
return {
|
|
537
|
+
success: false,
|
|
538
|
+
error: 'Job not found',
|
|
539
|
+
jobId,
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
return {
|
|
544
|
+
success: true,
|
|
545
|
+
job: {
|
|
546
|
+
id: job.id,
|
|
547
|
+
toolName: job.toolName,
|
|
548
|
+
status: job.status,
|
|
549
|
+
progress: job.progress,
|
|
550
|
+
result: job.status === 'completed' ? job.result : undefined,
|
|
551
|
+
error: job.status === 'failed' ? job.error : undefined,
|
|
552
|
+
metadata: job.metadata,
|
|
553
|
+
},
|
|
554
|
+
};
|
|
555
|
+
},
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
export const toolMetadata = {
|
|
560
|
+
name: 'jobs/status',
|
|
561
|
+
description: 'Get async job status',
|
|
562
|
+
category: 'jobs',
|
|
563
|
+
detailLevel: 'standard',
|
|
564
|
+
};
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
**Create**: `src/mcp/tools/jobs/list.ts`
|
|
568
|
+
|
|
569
|
+
```typescript
|
|
570
|
+
import type { MCPTool, ClaudeFlowToolContext } from '../../types.js';
|
|
571
|
+
import type { ILogger } from '../../../interfaces/logger.js';
|
|
572
|
+
|
|
573
|
+
export function createJobListTool(logger: ILogger): MCPTool {
|
|
574
|
+
return {
|
|
575
|
+
name: 'jobs/list',
|
|
576
|
+
description: 'List async jobs with optional filtering',
|
|
577
|
+
|
|
578
|
+
inputSchema: {
|
|
579
|
+
type: 'object',
|
|
580
|
+
properties: {
|
|
581
|
+
status: {
|
|
582
|
+
type: 'string',
|
|
583
|
+
enum: ['queued', 'running', 'completed', 'failed', 'cancelled'],
|
|
584
|
+
description: 'Filter by status',
|
|
585
|
+
},
|
|
586
|
+
toolName: {
|
|
587
|
+
type: 'string',
|
|
588
|
+
description: 'Filter by tool name',
|
|
589
|
+
},
|
|
590
|
+
limit: {
|
|
591
|
+
type: 'number',
|
|
592
|
+
description: 'Maximum number of jobs to return',
|
|
593
|
+
default: 20,
|
|
594
|
+
},
|
|
595
|
+
},
|
|
596
|
+
},
|
|
597
|
+
|
|
598
|
+
handler: async (input: any, context?: ClaudeFlowToolContext) => {
|
|
599
|
+
if (!context?.jobManager) {
|
|
600
|
+
throw new Error('Job manager not available');
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
const jobs = context.jobManager.listJobs(input);
|
|
604
|
+
|
|
605
|
+
return {
|
|
606
|
+
success: true,
|
|
607
|
+
jobs: jobs.map(j => ({
|
|
608
|
+
id: j.id,
|
|
609
|
+
toolName: j.toolName,
|
|
610
|
+
status: j.status,
|
|
611
|
+
progress: j.progress,
|
|
612
|
+
createdAt: j.metadata.createdAt,
|
|
613
|
+
duration: j.metadata.actualDuration,
|
|
614
|
+
})),
|
|
615
|
+
total: jobs.length,
|
|
616
|
+
};
|
|
617
|
+
},
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
export const toolMetadata = {
|
|
622
|
+
name: 'jobs/list',
|
|
623
|
+
description: 'List async jobs',
|
|
624
|
+
category: 'jobs',
|
|
625
|
+
detailLevel: 'standard',
|
|
626
|
+
};
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
**Create**: `src/mcp/tools/jobs/cancel.ts`
|
|
630
|
+
|
|
631
|
+
```typescript
|
|
632
|
+
import type { MCPTool, ClaudeFlowToolContext } from '../../types.js';
|
|
633
|
+
import type { ILogger } from '../../../interfaces/logger.js';
|
|
634
|
+
|
|
635
|
+
export function createJobCancelTool(logger: ILogger): MCPTool {
|
|
636
|
+
return {
|
|
637
|
+
name: 'jobs/cancel',
|
|
638
|
+
description: 'Cancel a running async job',
|
|
639
|
+
|
|
640
|
+
inputSchema: {
|
|
641
|
+
type: 'object',
|
|
642
|
+
properties: {
|
|
643
|
+
jobId: {
|
|
644
|
+
type: 'string',
|
|
645
|
+
description: 'Job ID to cancel',
|
|
646
|
+
},
|
|
647
|
+
},
|
|
648
|
+
required: ['jobId'],
|
|
649
|
+
},
|
|
650
|
+
|
|
651
|
+
handler: async (input: any, context?: ClaudeFlowToolContext) => {
|
|
652
|
+
if (!context?.jobManager) {
|
|
653
|
+
throw new Error('Job manager not available');
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
const { jobId } = input;
|
|
657
|
+
const cancelled = await context.jobManager.cancelJob(jobId);
|
|
658
|
+
|
|
659
|
+
return {
|
|
660
|
+
success: cancelled,
|
|
661
|
+
jobId,
|
|
662
|
+
message: cancelled
|
|
663
|
+
? 'Job cancelled successfully'
|
|
664
|
+
: 'Job not found or already completed',
|
|
665
|
+
};
|
|
666
|
+
},
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
export const toolMetadata = {
|
|
671
|
+
name: 'jobs/cancel',
|
|
672
|
+
description: 'Cancel async job',
|
|
673
|
+
category: 'jobs',
|
|
674
|
+
detailLevel: 'standard',
|
|
675
|
+
};
|
|
676
|
+
```
|
|
677
|
+
|
|
678
|
+
#### 4. Integration with MCP Server
|
|
679
|
+
|
|
680
|
+
**Update**: `src/mcp/server.ts`
|
|
681
|
+
|
|
682
|
+
```typescript
|
|
683
|
+
import { JobManager } from './async/job-manager.js';
|
|
684
|
+
import { makeToolAsync } from './async/async-tool.js';
|
|
685
|
+
|
|
686
|
+
export class MCPServer {
|
|
687
|
+
private jobManager: JobManager;
|
|
688
|
+
|
|
689
|
+
constructor(config: MCPConfig, eventBus: IEventBus, logger: ILogger) {
|
|
690
|
+
// ... existing code ...
|
|
691
|
+
|
|
692
|
+
// Initialize job manager
|
|
693
|
+
this.jobManager = new JobManager(
|
|
694
|
+
config.async?.maxJobs || 1000,
|
|
695
|
+
config.async?.jobTTL || 86400000
|
|
696
|
+
);
|
|
697
|
+
|
|
698
|
+
logger.info('Job manager initialized', {
|
|
699
|
+
maxJobs: config.async?.maxJobs || 1000,
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* Register tool with optional async support
|
|
705
|
+
*/
|
|
706
|
+
async registerTool(tool: MCPTool, enableAsync: boolean = true): Promise<void> {
|
|
707
|
+
// Register synchronous version
|
|
708
|
+
await this.toolRegistry.registerTool(tool);
|
|
709
|
+
|
|
710
|
+
// Also register async version if enabled
|
|
711
|
+
if (enableAsync && this.isLongRunningTool(tool)) {
|
|
712
|
+
const asyncTool = makeToolAsync(tool, this.jobManager, this.logger);
|
|
713
|
+
await this.toolRegistry.registerTool(asyncTool);
|
|
714
|
+
|
|
715
|
+
this.logger.info('Registered async version of tool', {
|
|
716
|
+
tool: tool.name,
|
|
717
|
+
asyncName: asyncTool.name
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* Determine if tool should have async version
|
|
724
|
+
*/
|
|
725
|
+
private isLongRunningTool(tool: MCPTool): boolean {
|
|
726
|
+
// Tools that typically take > 5 seconds should be async
|
|
727
|
+
const longRunningCategories = [
|
|
728
|
+
'agents/spawn',
|
|
729
|
+
'agents/spawn-parallel',
|
|
730
|
+
'tasks/create',
|
|
731
|
+
'workflow/execute',
|
|
732
|
+
'data/filter',
|
|
733
|
+
'swarm/',
|
|
734
|
+
];
|
|
735
|
+
|
|
736
|
+
return longRunningCategories.some(prefix => tool.name.startsWith(prefix));
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
/**
|
|
740
|
+
* Add job manager to tool context
|
|
741
|
+
*/
|
|
742
|
+
private get toolContext(): ClaudeFlowToolContext {
|
|
743
|
+
return {
|
|
744
|
+
orchestrator: this.orchestrator,
|
|
745
|
+
swarmCoordinator: this.swarmCoordinator,
|
|
746
|
+
jobManager: this.jobManager, // NEW
|
|
747
|
+
sessionId: this.currentSessionId,
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
```
|
|
752
|
+
|
|
753
|
+
#### 5. Configuration Updates
|
|
754
|
+
|
|
755
|
+
**Update**: `src/config/default-config.ts`
|
|
756
|
+
|
|
757
|
+
```typescript
|
|
758
|
+
export interface MCPConfig {
|
|
759
|
+
// ... existing config ...
|
|
760
|
+
|
|
761
|
+
async: {
|
|
762
|
+
enabled: boolean;
|
|
763
|
+
maxJobs: number;
|
|
764
|
+
jobTTL: number; // milliseconds
|
|
765
|
+
defaultTimeout: number; // milliseconds
|
|
766
|
+
enableWebhooks: boolean;
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
export const defaultConfig: MCPConfig = {
|
|
771
|
+
// ... existing config ...
|
|
772
|
+
|
|
773
|
+
async: {
|
|
774
|
+
enabled: true,
|
|
775
|
+
maxJobs: 1000,
|
|
776
|
+
jobTTL: 86400000, // 24 hours
|
|
777
|
+
defaultTimeout: 300000, // 5 minutes
|
|
778
|
+
enableWebhooks: false,
|
|
779
|
+
},
|
|
780
|
+
};
|
|
781
|
+
```
|
|
782
|
+
|
|
783
|
+
---
|
|
784
|
+
|
|
785
|
+
## PHASE 0B: MCP Registry Integration (PRIORITY 2)
|
|
786
|
+
|
|
787
|
+
### Overview
|
|
788
|
+
The MCP Registry provides:
|
|
789
|
+
- Centralized server catalog with metadata
|
|
790
|
+
- API for server discovery
|
|
791
|
+
- Capability negotiation
|
|
792
|
+
- Health checks and monitoring
|
|
793
|
+
|
|
794
|
+
### Required Changes
|
|
795
|
+
|
|
796
|
+
#### 1. Registry Client
|
|
797
|
+
|
|
798
|
+
**Create**: `src/mcp/registry/client.ts`
|
|
799
|
+
|
|
800
|
+
```typescript
|
|
801
|
+
/**
|
|
802
|
+
* MCP Registry Client
|
|
803
|
+
* Integrates with official MCP Registry for server discovery
|
|
804
|
+
*/
|
|
805
|
+
|
|
806
|
+
export interface ServerMetadata {
|
|
807
|
+
name: string;
|
|
808
|
+
version: string;
|
|
809
|
+
description: string;
|
|
810
|
+
author: string;
|
|
811
|
+
homepage?: string;
|
|
812
|
+
repository?: string;
|
|
813
|
+
capabilities: {
|
|
814
|
+
tools: boolean;
|
|
815
|
+
resources: boolean;
|
|
816
|
+
prompts: boolean;
|
|
817
|
+
async: boolean;
|
|
818
|
+
streaming: boolean;
|
|
819
|
+
};
|
|
820
|
+
categories: string[];
|
|
821
|
+
tags: string[];
|
|
822
|
+
transport: Array<'stdio' | 'http' | 'websocket'>;
|
|
823
|
+
healthCheck?: {
|
|
824
|
+
endpoint: string;
|
|
825
|
+
interval: number; // seconds
|
|
826
|
+
};
|
|
827
|
+
security: {
|
|
828
|
+
authRequired: boolean;
|
|
829
|
+
authMethods: Array<'token' | 'basic' | 'oauth'>;
|
|
830
|
+
piiHandling: 'none' | 'tokenized' | 'encrypted';
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
export class RegistryClient {
|
|
835
|
+
private baseUrl: string = 'https://registry.mcp.anthropic.com/api/v1';
|
|
836
|
+
|
|
837
|
+
constructor(private apiKey?: string) {}
|
|
838
|
+
|
|
839
|
+
/**
|
|
840
|
+
* Publish server to registry
|
|
841
|
+
*/
|
|
842
|
+
async publishServer(metadata: ServerMetadata): Promise<{
|
|
843
|
+
success: boolean;
|
|
844
|
+
serverId: string;
|
|
845
|
+
url: string;
|
|
846
|
+
}> {
|
|
847
|
+
const response = await fetch(`${this.baseUrl}/servers`, {
|
|
848
|
+
method: 'POST',
|
|
849
|
+
headers: {
|
|
850
|
+
'Content-Type': 'application/json',
|
|
851
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
852
|
+
},
|
|
853
|
+
body: JSON.stringify(metadata),
|
|
854
|
+
});
|
|
855
|
+
|
|
856
|
+
if (!response.ok) {
|
|
857
|
+
throw new Error(`Failed to publish server: ${response.statusText}`);
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
return await response.json();
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
/**
|
|
864
|
+
* Update server metadata
|
|
865
|
+
*/
|
|
866
|
+
async updateServer(
|
|
867
|
+
serverId: string,
|
|
868
|
+
metadata: Partial<ServerMetadata>
|
|
869
|
+
): Promise<void> {
|
|
870
|
+
const response = await fetch(`${this.baseUrl}/servers/${serverId}`, {
|
|
871
|
+
method: 'PATCH',
|
|
872
|
+
headers: {
|
|
873
|
+
'Content-Type': 'application/json',
|
|
874
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
875
|
+
},
|
|
876
|
+
body: JSON.stringify(metadata),
|
|
877
|
+
});
|
|
878
|
+
|
|
879
|
+
if (!response.ok) {
|
|
880
|
+
throw new Error(`Failed to update server: ${response.statusText}`);
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
/**
|
|
885
|
+
* Report health status
|
|
886
|
+
*/
|
|
887
|
+
async reportHealth(
|
|
888
|
+
serverId: string,
|
|
889
|
+
health: {
|
|
890
|
+
status: 'healthy' | 'degraded' | 'unhealthy';
|
|
891
|
+
latency?: number;
|
|
892
|
+
metrics?: Record<string, number>;
|
|
893
|
+
}
|
|
894
|
+
): Promise<void> {
|
|
895
|
+
await fetch(`${this.baseUrl}/servers/${serverId}/health`, {
|
|
896
|
+
method: 'POST',
|
|
897
|
+
headers: {
|
|
898
|
+
'Content-Type': 'application/json',
|
|
899
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
900
|
+
},
|
|
901
|
+
body: JSON.stringify(health),
|
|
902
|
+
});
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
/**
|
|
906
|
+
* Search registry for servers
|
|
907
|
+
*/
|
|
908
|
+
async searchServers(query: {
|
|
909
|
+
category?: string;
|
|
910
|
+
tags?: string[];
|
|
911
|
+
capabilities?: string[];
|
|
912
|
+
limit?: number;
|
|
913
|
+
}): Promise<ServerMetadata[]> {
|
|
914
|
+
const params = new URLSearchParams();
|
|
915
|
+
if (query.category) params.set('category', query.category);
|
|
916
|
+
if (query.tags) params.set('tags', query.tags.join(','));
|
|
917
|
+
if (query.capabilities) params.set('capabilities', query.capabilities.join(','));
|
|
918
|
+
if (query.limit) params.set('limit', query.limit.toString());
|
|
919
|
+
|
|
920
|
+
const response = await fetch(`${this.baseUrl}/servers?${params}`);
|
|
921
|
+
|
|
922
|
+
if (!response.ok) {
|
|
923
|
+
throw new Error(`Failed to search servers: ${response.statusText}`);
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
return await response.json();
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
```
|
|
930
|
+
|
|
931
|
+
#### 2. Auto-Publish Script
|
|
932
|
+
|
|
933
|
+
**Create**: `scripts/publish-to-registry.ts`
|
|
934
|
+
|
|
935
|
+
```typescript
|
|
936
|
+
#!/usr/bin/env node
|
|
937
|
+
|
|
938
|
+
import { RegistryClient, ServerMetadata } from '../src/mcp/registry/client.js';
|
|
939
|
+
import { version } from '../package.json';
|
|
940
|
+
|
|
941
|
+
async function main() {
|
|
942
|
+
const apiKey = process.env.MCP_REGISTRY_API_KEY;
|
|
943
|
+
if (!apiKey) {
|
|
944
|
+
console.error('❌ MCP_REGISTRY_API_KEY not set');
|
|
945
|
+
process.exit(1);
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
const client = new RegistryClient(apiKey);
|
|
949
|
+
|
|
950
|
+
const metadata: ServerMetadata = {
|
|
951
|
+
name: 'claude-flow',
|
|
952
|
+
version,
|
|
953
|
+
description: 'Advanced MCP server with swarm coordination, async operations, and neural capabilities',
|
|
954
|
+
author: 'ruvnet',
|
|
955
|
+
homepage: 'https://github.com/ruvnet/claude-flow',
|
|
956
|
+
repository: 'https://github.com/ruvnet/claude-flow',
|
|
957
|
+
capabilities: {
|
|
958
|
+
tools: true,
|
|
959
|
+
resources: true,
|
|
960
|
+
prompts: true,
|
|
961
|
+
async: true,
|
|
962
|
+
streaming: true,
|
|
963
|
+
},
|
|
964
|
+
categories: [
|
|
965
|
+
'orchestration',
|
|
966
|
+
'swarm-coordination',
|
|
967
|
+
'task-management',
|
|
968
|
+
'memory-management',
|
|
969
|
+
],
|
|
970
|
+
tags: [
|
|
971
|
+
'async',
|
|
972
|
+
'swarm',
|
|
973
|
+
'neural',
|
|
974
|
+
'coordination',
|
|
975
|
+
'agentdb',
|
|
976
|
+
'reasoningbank',
|
|
977
|
+
],
|
|
978
|
+
transport: ['stdio', 'http'],
|
|
979
|
+
healthCheck: {
|
|
980
|
+
endpoint: '/health',
|
|
981
|
+
interval: 60,
|
|
982
|
+
},
|
|
983
|
+
security: {
|
|
984
|
+
authRequired: true,
|
|
985
|
+
authMethods: ['token', 'oauth'],
|
|
986
|
+
piiHandling: 'tokenized',
|
|
987
|
+
},
|
|
988
|
+
};
|
|
989
|
+
|
|
990
|
+
console.log('📤 Publishing to MCP Registry...');
|
|
991
|
+
|
|
992
|
+
const result = await client.publishServer(metadata);
|
|
993
|
+
|
|
994
|
+
console.log('✅ Published successfully!');
|
|
995
|
+
console.log(` Server ID: ${result.serverId}`);
|
|
996
|
+
console.log(` URL: ${result.url}`);
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
main().catch(console.error);
|
|
1000
|
+
```
|
|
1001
|
+
|
|
1002
|
+
#### 3. Health Check Endpoint
|
|
1003
|
+
|
|
1004
|
+
**Create**: `src/mcp/endpoints/health.ts`
|
|
1005
|
+
|
|
1006
|
+
```typescript
|
|
1007
|
+
export class HealthCheckHandler {
|
|
1008
|
+
constructor(
|
|
1009
|
+
private server: MCPServer,
|
|
1010
|
+
private jobManager: JobManager,
|
|
1011
|
+
private toolRegistry: ToolRegistry
|
|
1012
|
+
) {}
|
|
1013
|
+
|
|
1014
|
+
async getHealth(): Promise<{
|
|
1015
|
+
status: 'healthy' | 'degraded' | 'unhealthy';
|
|
1016
|
+
uptime: number;
|
|
1017
|
+
metrics: {
|
|
1018
|
+
activeJobs: number;
|
|
1019
|
+
totalTools: number;
|
|
1020
|
+
avgResponseTime: number;
|
|
1021
|
+
errorRate: number;
|
|
1022
|
+
};
|
|
1023
|
+
}> {
|
|
1024
|
+
const jobMetrics = this.jobManager.getMetrics();
|
|
1025
|
+
const toolMetrics = await this.toolRegistry.getMetrics();
|
|
1026
|
+
|
|
1027
|
+
// Calculate health status
|
|
1028
|
+
const errorRate = toolMetrics.failedInvocations / toolMetrics.totalInvocations;
|
|
1029
|
+
const status = errorRate > 0.1 ? 'unhealthy' : errorRate > 0.05 ? 'degraded' : 'healthy';
|
|
1030
|
+
|
|
1031
|
+
return {
|
|
1032
|
+
status,
|
|
1033
|
+
uptime: process.uptime(),
|
|
1034
|
+
metrics: {
|
|
1035
|
+
activeJobs: jobMetrics.byStatus.running,
|
|
1036
|
+
totalTools: toolMetrics.totalTools,
|
|
1037
|
+
avgResponseTime: toolMetrics.averageExecutionTime,
|
|
1038
|
+
errorRate,
|
|
1039
|
+
},
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
```
|
|
1044
|
+
|
|
1045
|
+
#### 4. CI Integration
|
|
1046
|
+
|
|
1047
|
+
**Update**: `.github/workflows/publish.yml`
|
|
1048
|
+
|
|
1049
|
+
```yaml
|
|
1050
|
+
name: Publish to MCP Registry
|
|
1051
|
+
|
|
1052
|
+
on:
|
|
1053
|
+
release:
|
|
1054
|
+
types: [published]
|
|
1055
|
+
push:
|
|
1056
|
+
branches: [main]
|
|
1057
|
+
|
|
1058
|
+
jobs:
|
|
1059
|
+
publish-registry:
|
|
1060
|
+
runs-on: ubuntu-latest
|
|
1061
|
+
steps:
|
|
1062
|
+
- uses: actions/checkout@v3
|
|
1063
|
+
|
|
1064
|
+
- name: Setup Node.js
|
|
1065
|
+
uses: actions/setup-node@v3
|
|
1066
|
+
with:
|
|
1067
|
+
node-version: '20'
|
|
1068
|
+
|
|
1069
|
+
- name: Install dependencies
|
|
1070
|
+
run: npm ci
|
|
1071
|
+
|
|
1072
|
+
- name: Build
|
|
1073
|
+
run: npm run build
|
|
1074
|
+
|
|
1075
|
+
- name: Run tests
|
|
1076
|
+
run: npm test
|
|
1077
|
+
|
|
1078
|
+
- name: Publish to MCP Registry
|
|
1079
|
+
env:
|
|
1080
|
+
MCP_REGISTRY_API_KEY: ${{ secrets.MCP_REGISTRY_API_KEY }}
|
|
1081
|
+
run: npm run registry:publish
|
|
1082
|
+
|
|
1083
|
+
- name: Update health check
|
|
1084
|
+
env:
|
|
1085
|
+
MCP_REGISTRY_API_KEY: ${{ secrets.MCP_REGISTRY_API_KEY }}
|
|
1086
|
+
run: npm run registry:health
|
|
1087
|
+
```
|
|
1088
|
+
|
|
1089
|
+
---
|
|
1090
|
+
|
|
1091
|
+
## PHASE 0C: Code Execution Pattern (Already Planned)
|
|
1092
|
+
|
|
1093
|
+
This is covered in the original plan (Phase 1: Progressive Disclosure, Phase 4: Data Processing).
|
|
1094
|
+
|
|
1095
|
+
**Status**: Implementation details provided in main plan
|
|
1096
|
+
|
|
1097
|
+
---
|
|
1098
|
+
|
|
1099
|
+
## PHASE 1: RC Validation (November 14-25)
|
|
1100
|
+
|
|
1101
|
+
### Test Suite for Spec Compliance
|
|
1102
|
+
|
|
1103
|
+
**Create**: `tests/mcp-spec-2025-compliance.test.ts`
|
|
1104
|
+
|
|
1105
|
+
```typescript
|
|
1106
|
+
import { describe, it, expect, beforeAll } from '@jest/globals';
|
|
1107
|
+
|
|
1108
|
+
describe('MCP Spec 2025 Compliance', () => {
|
|
1109
|
+
describe('Async Operations', () => {
|
|
1110
|
+
it('should return job handle immediately for long-running tasks', async () => {
|
|
1111
|
+
const result = await mcpClient.callTool('agents/spawn:async', {
|
|
1112
|
+
type: 'researcher',
|
|
1113
|
+
name: 'Test Agent',
|
|
1114
|
+
_async: { mode: 'poll' },
|
|
1115
|
+
});
|
|
1116
|
+
|
|
1117
|
+
expect(result).toHaveProperty('jobHandle');
|
|
1118
|
+
expect(result.jobHandle).toHaveProperty('jobId');
|
|
1119
|
+
expect(result.jobHandle).toHaveProperty('pollUrl');
|
|
1120
|
+
});
|
|
1121
|
+
|
|
1122
|
+
it('should allow polling for job status', async () => {
|
|
1123
|
+
const { jobHandle } = await mcpClient.callTool('agents/spawn:async', {
|
|
1124
|
+
type: 'researcher',
|
|
1125
|
+
name: 'Test Agent',
|
|
1126
|
+
_async: { mode: 'poll' },
|
|
1127
|
+
});
|
|
1128
|
+
|
|
1129
|
+
const status = await mcpClient.callTool('jobs/status', {
|
|
1130
|
+
jobId: jobHandle.jobId,
|
|
1131
|
+
});
|
|
1132
|
+
|
|
1133
|
+
expect(status.job.status).toMatch(/queued|running|completed/);
|
|
1134
|
+
});
|
|
1135
|
+
|
|
1136
|
+
it('should support wait mode with timeout', async () => {
|
|
1137
|
+
const result = await mcpClient.callTool('agents/spawn:async', {
|
|
1138
|
+
type: 'researcher',
|
|
1139
|
+
name: 'Test Agent',
|
|
1140
|
+
_async: { mode: 'wait', timeout: 10000 },
|
|
1141
|
+
});
|
|
1142
|
+
|
|
1143
|
+
expect(result).toHaveProperty('completed');
|
|
1144
|
+
if (result.completed) {
|
|
1145
|
+
expect(result).toHaveProperty('result');
|
|
1146
|
+
}
|
|
1147
|
+
});
|
|
1148
|
+
|
|
1149
|
+
it('should support job cancellation', async () => {
|
|
1150
|
+
const { jobHandle } = await mcpClient.callTool('workflow/execute:async', {
|
|
1151
|
+
workflowId: 'long-running-workflow',
|
|
1152
|
+
_async: { mode: 'poll' },
|
|
1153
|
+
});
|
|
1154
|
+
|
|
1155
|
+
const cancelled = await mcpClient.callTool('jobs/cancel', {
|
|
1156
|
+
jobId: jobHandle.jobId,
|
|
1157
|
+
});
|
|
1158
|
+
|
|
1159
|
+
expect(cancelled.success).toBe(true);
|
|
1160
|
+
|
|
1161
|
+
const status = await mcpClient.callTool('jobs/status', {
|
|
1162
|
+
jobId: jobHandle.jobId,
|
|
1163
|
+
});
|
|
1164
|
+
|
|
1165
|
+
expect(status.job.status).toBe('cancelled');
|
|
1166
|
+
});
|
|
1167
|
+
});
|
|
1168
|
+
|
|
1169
|
+
describe('Registry Integration', () => {
|
|
1170
|
+
it('should be discoverable in MCP Registry', async () => {
|
|
1171
|
+
const servers = await registryClient.searchServers({
|
|
1172
|
+
tags: ['claude-flow'],
|
|
1173
|
+
});
|
|
1174
|
+
|
|
1175
|
+
expect(servers).toContainEqual(
|
|
1176
|
+
expect.objectContaining({
|
|
1177
|
+
name: 'claude-flow',
|
|
1178
|
+
capabilities: expect.objectContaining({
|
|
1179
|
+
async: true,
|
|
1180
|
+
}),
|
|
1181
|
+
})
|
|
1182
|
+
);
|
|
1183
|
+
});
|
|
1184
|
+
|
|
1185
|
+
it('should report health status', async () => {
|
|
1186
|
+
const health = await fetch('http://localhost:3000/health');
|
|
1187
|
+
const data = await health.json();
|
|
1188
|
+
|
|
1189
|
+
expect(data.status).toMatch(/healthy|degraded|unhealthy/);
|
|
1190
|
+
expect(data.metrics).toHaveProperty('activeJobs');
|
|
1191
|
+
});
|
|
1192
|
+
});
|
|
1193
|
+
|
|
1194
|
+
describe('Code Execution Pattern', () => {
|
|
1195
|
+
it('should process data in execution environment', async () => {
|
|
1196
|
+
// Create large dataset (10,000 rows)
|
|
1197
|
+
const largeDataset = Array.from({ length: 10000 }, (_, i) => ({
|
|
1198
|
+
id: i,
|
|
1199
|
+
value: Math.random() * 1000,
|
|
1200
|
+
category: ['A', 'B', 'C'][i % 3],
|
|
1201
|
+
}));
|
|
1202
|
+
|
|
1203
|
+
// Store in memory
|
|
1204
|
+
await memoryStore.store('test-dataset', JSON.stringify(largeDataset));
|
|
1205
|
+
|
|
1206
|
+
// Filter in execution environment
|
|
1207
|
+
const result = await mcpClient.callTool('data/filter', {
|
|
1208
|
+
dataSource: 'memory',
|
|
1209
|
+
source: 'test-dataset',
|
|
1210
|
+
filters: [
|
|
1211
|
+
{ field: 'value', operator: 'gt', value: 500 },
|
|
1212
|
+
{ field: 'category', operator: 'eq', value: 'A' },
|
|
1213
|
+
],
|
|
1214
|
+
returnFormat: 'summary',
|
|
1215
|
+
});
|
|
1216
|
+
|
|
1217
|
+
// Result should be small (summary only)
|
|
1218
|
+
const resultSize = JSON.stringify(result).length;
|
|
1219
|
+
expect(resultSize).toBeLessThan(1000); // < 1KB
|
|
1220
|
+
|
|
1221
|
+
// Original dataset was ~500KB
|
|
1222
|
+
const originalSize = JSON.stringify(largeDataset).length;
|
|
1223
|
+
expect(originalSize).toBeGreaterThan(500000);
|
|
1224
|
+
|
|
1225
|
+
// Verify token savings
|
|
1226
|
+
const tokenReduction = ((originalSize - resultSize) / originalSize) * 100;
|
|
1227
|
+
expect(tokenReduction).toBeGreaterThan(98);
|
|
1228
|
+
});
|
|
1229
|
+
});
|
|
1230
|
+
});
|
|
1231
|
+
```
|
|
1232
|
+
|
|
1233
|
+
---
|
|
1234
|
+
|
|
1235
|
+
## PHASE 2: Production Rollout Checklist
|
|
1236
|
+
|
|
1237
|
+
### Pre-Rollout (Before Nov 25)
|
|
1238
|
+
- [ ] All async operation tests passing
|
|
1239
|
+
- [ ] Registry integration tested
|
|
1240
|
+
- [ ] Code execution pattern validated
|
|
1241
|
+
- [ ] Performance benchmarks meet targets
|
|
1242
|
+
- [ ] Security audit completed
|
|
1243
|
+
- [ ] Documentation updated
|
|
1244
|
+
|
|
1245
|
+
### Rollout (Nov 25)
|
|
1246
|
+
- [ ] Deploy to staging
|
|
1247
|
+
- [ ] Smoke tests in staging
|
|
1248
|
+
- [ ] Monitor metrics for 24 hours
|
|
1249
|
+
- [ ] Deploy to production
|
|
1250
|
+
- [ ] Monitor production metrics
|
|
1251
|
+
- [ ] Update registry with production endpoint
|
|
1252
|
+
|
|
1253
|
+
### Post-Rollout (After Nov 25)
|
|
1254
|
+
- [ ] Performance monitoring active
|
|
1255
|
+
- [ ] Token usage tracked
|
|
1256
|
+
- [ ] User feedback collected
|
|
1257
|
+
- [ ] Optimization iterations
|
|
1258
|
+
|
|
1259
|
+
---
|
|
1260
|
+
|
|
1261
|
+
## 📊 Success Metrics
|
|
1262
|
+
|
|
1263
|
+
### Token Reduction
|
|
1264
|
+
- **Target**: 98% reduction (150k → 2k tokens)
|
|
1265
|
+
- **Measure**: Average tokens per workflow
|
|
1266
|
+
- **Baseline**: Measure before implementation
|
|
1267
|
+
- **Validation**: A/B test with old vs new pattern
|
|
1268
|
+
|
|
1269
|
+
### Async Performance
|
|
1270
|
+
- **Target**: 95% of long-running tasks complete within timeout
|
|
1271
|
+
- **Measure**: Job completion rate, average duration
|
|
1272
|
+
- **Alert**: >5% failure rate
|
|
1273
|
+
|
|
1274
|
+
### Registry Health
|
|
1275
|
+
- **Target**: 99% uptime reported to registry
|
|
1276
|
+
- **Measure**: Health check success rate
|
|
1277
|
+
- **Alert**: Status degraded for >5 minutes
|
|
1278
|
+
|
|
1279
|
+
### User Adoption
|
|
1280
|
+
- **Target**: 80% of users switch to async pattern
|
|
1281
|
+
- **Measure**: Async tool usage vs sync tool usage
|
|
1282
|
+
- **Goal**: Deprecate sync versions of long-running tools
|
|
1283
|
+
|
|
1284
|
+
---
|
|
1285
|
+
|
|
1286
|
+
## 🚀 Quick Start Commands
|
|
1287
|
+
|
|
1288
|
+
```bash
|
|
1289
|
+
# Install dependencies
|
|
1290
|
+
npm install
|
|
1291
|
+
|
|
1292
|
+
# Run async operation tests
|
|
1293
|
+
npm run test:async
|
|
1294
|
+
|
|
1295
|
+
# Run registry integration tests
|
|
1296
|
+
npm run test:registry
|
|
1297
|
+
|
|
1298
|
+
# Run full compliance suite
|
|
1299
|
+
npm run test:compliance
|
|
1300
|
+
|
|
1301
|
+
# Publish to registry
|
|
1302
|
+
npm run registry:publish
|
|
1303
|
+
|
|
1304
|
+
# Start server with async support
|
|
1305
|
+
npm run start:async
|
|
1306
|
+
|
|
1307
|
+
# Monitor job queue
|
|
1308
|
+
npm run jobs:monitor
|
|
1309
|
+
```
|
|
1310
|
+
|
|
1311
|
+
---
|
|
1312
|
+
|
|
1313
|
+
## 📞 Escalation Path
|
|
1314
|
+
|
|
1315
|
+
- **Spec Questions**: mcp-spec@anthropic.com
|
|
1316
|
+
- **Registry Issues**: registry-support@anthropic.com
|
|
1317
|
+
- **Claude Flow Issues**: GitHub Issues
|
|
1318
|
+
- **Urgent**: Create issue with `[MCP-2025-URGENT]` prefix
|
|
1319
|
+
|
|
1320
|
+
---
|
|
1321
|
+
|
|
1322
|
+
## Next Steps
|
|
1323
|
+
|
|
1324
|
+
1. **Immediate**: Review this plan with team
|
|
1325
|
+
2. **This Week**: Implement Phase 0A (Async Operations)
|
|
1326
|
+
3. **Next Week**: Implement Phase 0B (Registry Integration)
|
|
1327
|
+
4. **Nov 14**: Begin RC validation
|
|
1328
|
+
5. **Nov 25**: Production rollout
|
|
1329
|
+
|
|
1330
|
+
Would you like me to start implementing any specific phase?
|