@sparkleideas/claims 3.5.2-patch.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.
@@ -0,0 +1,1977 @@
1
+ /**
2
+ * V3 MCP Claims Tools
3
+ *
4
+ * MCP tools for issue claiming and work coordination:
5
+ *
6
+ * Core Claiming (7 tools):
7
+ * - claims/issue_claim - Claim an issue to work on
8
+ * - claims/issue_release - Release a claim
9
+ * - claims/issue_handoff - Request handoff to another agent/human
10
+ * - claims/issue_status_update - Update claim status
11
+ * - claims/issue_list_available - List unclaimed issues
12
+ * - claims/issue_list_mine - List my claims
13
+ * - claims/issue_board - View claim board (who's working on what)
14
+ *
15
+ * Work Stealing (4 tools):
16
+ * - claims/issue_mark_stealable - Mark my claim as stealable
17
+ * - claims/issue_steal - Steal a stealable issue
18
+ * - claims/issue_get_stealable - List stealable issues
19
+ * - claims/issue_contest_steal - Contest a steal
20
+ *
21
+ * Load Balancing (3 tools):
22
+ * - claims/agent_load_info - Get agent's current load
23
+ * - claims/swarm_rebalance - Trigger swarm rebalancing
24
+ * - claims/swarm_load_overview - Get swarm-wide load distribution
25
+ *
26
+ * Additionally provides:
27
+ * - claims/claim_history - Get claim history for an issue
28
+ * - claims/claim_metrics - Get claiming metrics
29
+ * - claims/claim_config - Configure claiming behavior
30
+ *
31
+ * Implements ADR-005: MCP-First API Design
32
+ */
33
+
34
+ import { z } from 'zod';
35
+ import { randomBytes } from 'crypto';
36
+
37
+ // ============================================================================
38
+ // Type Definitions (compatible with v3/mcp/types.ts)
39
+ // ============================================================================
40
+
41
+ /**
42
+ * JSON Schema type for tool input
43
+ */
44
+ interface JSONSchema {
45
+ type: string;
46
+ properties?: Record<string, JSONSchema>;
47
+ required?: string[];
48
+ items?: JSONSchema;
49
+ enum?: string[];
50
+ description?: string;
51
+ default?: unknown;
52
+ minimum?: number;
53
+ maximum?: number;
54
+ minLength?: number;
55
+ maxLength?: number;
56
+ pattern?: string;
57
+ additionalProperties?: boolean | JSONSchema;
58
+ }
59
+
60
+ /**
61
+ * Tool execution context
62
+ */
63
+ interface ToolContext {
64
+ sessionId: string;
65
+ requestId?: string | number | null;
66
+ orchestrator?: unknown;
67
+ swarmCoordinator?: unknown;
68
+ agentManager?: unknown;
69
+ claimsService?: ClaimsService;
70
+ metadata?: Record<string, unknown>;
71
+ }
72
+
73
+ /**
74
+ * Tool handler function type
75
+ */
76
+ type ToolHandler<TInput = unknown, TOutput = unknown> = (
77
+ input: TInput,
78
+ context?: ToolContext
79
+ ) => Promise<TOutput>;
80
+
81
+ /**
82
+ * MCP Tool definition
83
+ */
84
+ interface MCPTool<TInput = unknown, TOutput = unknown> {
85
+ name: string;
86
+ description: string;
87
+ inputSchema: JSONSchema;
88
+ handler: ToolHandler<TInput, TOutput>;
89
+ category?: string;
90
+ tags?: string[];
91
+ version?: string;
92
+ deprecated?: boolean;
93
+ cacheable?: boolean;
94
+ cacheTTL?: number;
95
+ timeout?: number;
96
+ }
97
+
98
+ // ============================================================================
99
+ // Claims-Specific Types
100
+ // ============================================================================
101
+
102
+ type ClaimantType = 'human' | 'agent';
103
+ type ClaimStatus = 'active' | 'blocked' | 'in-review' | 'completed' | 'released' | 'stolen';
104
+ type IssuePriority = 'critical' | 'high' | 'medium' | 'low';
105
+ type HandoffReason = 'blocked' | 'expertise-needed' | 'capacity' | 'reassignment' | 'other';
106
+
107
+ interface Claim {
108
+ id: string;
109
+ issueId: string;
110
+ claimantType: ClaimantType;
111
+ claimantId: string;
112
+ status: ClaimStatus;
113
+ priority: IssuePriority;
114
+ stealable: boolean;
115
+ stealableReason?: string;
116
+ claimedAt: string;
117
+ lastActivityAt: string;
118
+ expiresAt?: string;
119
+ metadata?: Record<string, unknown>;
120
+ }
121
+
122
+ interface Issue {
123
+ id: string;
124
+ title: string;
125
+ description?: string;
126
+ priority: IssuePriority;
127
+ labels?: string[];
128
+ repository?: string;
129
+ createdAt: string;
130
+ updatedAt?: string;
131
+ claimedBy?: string;
132
+ metadata?: Record<string, unknown>;
133
+ }
134
+
135
+ interface AgentLoad {
136
+ agentId: string;
137
+ agentType: string;
138
+ currentClaims: number;
139
+ maxClaims: number;
140
+ utilizationPercent: number;
141
+ activeTasks: number;
142
+ queuedTasks: number;
143
+ averageTaskDuration: number;
144
+ lastActivityAt: string;
145
+ }
146
+
147
+ interface ClaimHistoryEntry {
148
+ timestamp: string;
149
+ action: string;
150
+ actorId: string;
151
+ actorType: ClaimantType;
152
+ details?: Record<string, unknown>;
153
+ }
154
+
155
+ /**
156
+ * Claims Service Interface
157
+ * Defines the contract for claims management operations
158
+ */
159
+ interface ClaimsService {
160
+ claimIssue(params: {
161
+ issueId: string;
162
+ claimantType: ClaimantType;
163
+ claimantId: string;
164
+ priority?: IssuePriority;
165
+ expiresInMs?: number;
166
+ }): Promise<Claim>;
167
+
168
+ releaseClaim(params: {
169
+ issueId: string;
170
+ claimantId: string;
171
+ reason?: string;
172
+ }): Promise<{ released: boolean; releasedAt: string }>;
173
+
174
+ requestHandoff(params: {
175
+ issueId: string;
176
+ fromId: string;
177
+ toId?: string;
178
+ toType?: ClaimantType;
179
+ reason: HandoffReason;
180
+ notes?: string;
181
+ }): Promise<{ handoffId: string; status: string }>;
182
+
183
+ updateClaimStatus(params: {
184
+ issueId: string;
185
+ claimantId: string;
186
+ status: ClaimStatus;
187
+ progress?: number;
188
+ notes?: string;
189
+ }): Promise<Claim>;
190
+
191
+ listAvailableIssues(params: {
192
+ priority?: IssuePriority;
193
+ labels?: string[];
194
+ repository?: string;
195
+ limit?: number;
196
+ offset?: number;
197
+ }): Promise<{ issues: Issue[]; total: number }>;
198
+
199
+ listMyClaims(params: {
200
+ claimantId: string;
201
+ status?: ClaimStatus;
202
+ limit?: number;
203
+ offset?: number;
204
+ }): Promise<{ claims: Claim[]; total: number }>;
205
+
206
+ getClaimBoard(params: {
207
+ includeAgents?: boolean;
208
+ includeHumans?: boolean;
209
+ groupBy?: 'claimant' | 'priority' | 'status';
210
+ }): Promise<{
211
+ claims: Claim[];
212
+ byClaimant?: Record<string, Claim[]>;
213
+ byPriority?: Record<IssuePriority, Claim[]>;
214
+ byStatus?: Record<ClaimStatus, Claim[]>;
215
+ }>;
216
+
217
+ markStealable(params: {
218
+ issueId: string;
219
+ claimantId: string;
220
+ reason?: string;
221
+ }): Promise<{ marked: boolean; markedAt: string }>;
222
+
223
+ stealClaim(params: {
224
+ issueId: string;
225
+ stealerId: string;
226
+ stealerType: ClaimantType;
227
+ reason?: string;
228
+ }): Promise<{
229
+ stolen: boolean;
230
+ claim: Claim;
231
+ previousClaimant: string;
232
+ contestWindow: number;
233
+ }>;
234
+
235
+ getStealableIssues(params: {
236
+ priority?: IssuePriority;
237
+ limit?: number;
238
+ }): Promise<{ issues: Array<Issue & { stealableReason?: string }>; total: number }>;
239
+
240
+ contestSteal(params: {
241
+ issueId: string;
242
+ contesterId: string;
243
+ reason: string;
244
+ }): Promise<{
245
+ contested: boolean;
246
+ resolution: 'pending' | 'upheld' | 'reversed';
247
+ resolvedAt?: string;
248
+ }>;
249
+
250
+ getAgentLoad(params: {
251
+ agentId: string;
252
+ }): Promise<AgentLoad>;
253
+
254
+ rebalanceSwarm(params: {
255
+ strategy?: 'round-robin' | 'least-loaded' | 'priority-based' | 'capability-based';
256
+ dryRun?: boolean;
257
+ }): Promise<{
258
+ rebalanced: boolean;
259
+ changes: Array<{ issueId: string; from: string; to: string }>;
260
+ dryRun: boolean;
261
+ }>;
262
+
263
+ getLoadOverview(): Promise<{
264
+ totalAgents: number;
265
+ totalClaims: number;
266
+ averageLoad: number;
267
+ agents: AgentLoad[];
268
+ bottlenecks: string[];
269
+ recommendations: string[];
270
+ }>;
271
+
272
+ getClaimHistory(params: {
273
+ issueId: string;
274
+ limit?: number;
275
+ }): Promise<{ history: ClaimHistoryEntry[]; total: number }>;
276
+
277
+ getMetrics(): Promise<{
278
+ totalClaims: number;
279
+ activeClaims: number;
280
+ completedClaims: number;
281
+ stolenClaims: number;
282
+ averageClaimDuration: number;
283
+ claimsByPriority: Record<IssuePriority, number>;
284
+ claimsByStatus: Record<ClaimStatus, number>;
285
+ }>;
286
+ }
287
+
288
+ // ============================================================================
289
+ // Secure ID Generation
290
+ // ============================================================================
291
+
292
+ function generateSecureId(prefix: string): string {
293
+ const timestamp = Date.now().toString(36);
294
+ const random = randomBytes(12).toString('hex');
295
+ return `${prefix}-${timestamp}-${random}`;
296
+ }
297
+
298
+ // ============================================================================
299
+ // In-Memory Store (for simple implementation without service)
300
+ // ============================================================================
301
+
302
+ const claimStore = new Map<string, Claim>();
303
+ const issueStore = new Map<string, Issue>();
304
+
305
+ // Initialize with some mock data
306
+ function initializeMockData(): void {
307
+ if (issueStore.size === 0) {
308
+ const mockIssues: Issue[] = [
309
+ {
310
+ id: 'issue-1',
311
+ title: 'Implement user authentication',
312
+ priority: 'high',
313
+ labels: ['feature', 'security'],
314
+ createdAt: new Date().toISOString(),
315
+ },
316
+ {
317
+ id: 'issue-2',
318
+ title: 'Fix memory leak in agent coordinator',
319
+ priority: 'critical',
320
+ labels: ['bug', 'performance'],
321
+ createdAt: new Date().toISOString(),
322
+ },
323
+ {
324
+ id: 'issue-3',
325
+ title: 'Add unit tests for claims module',
326
+ priority: 'medium',
327
+ labels: ['testing'],
328
+ createdAt: new Date().toISOString(),
329
+ },
330
+ ];
331
+ mockIssues.forEach(issue => issueStore.set(issue.id, issue));
332
+ }
333
+ }
334
+
335
+ // ============================================================================
336
+ // Input Schemas
337
+ // ============================================================================
338
+
339
+ // Core Claiming Schemas
340
+ const issueClaimSchema = z.object({
341
+ issueId: z.string().min(1).describe('Issue ID to claim'),
342
+ claimantType: z.enum(['human', 'agent']).describe('Type of claimant'),
343
+ claimantId: z.string().min(1).describe('ID of the claimant'),
344
+ priority: z.enum(['critical', 'high', 'medium', 'low']).optional()
345
+ .describe('Override priority for the claim'),
346
+ expiresInMs: z.number().int().positive().optional()
347
+ .describe('Claim expiration time in milliseconds'),
348
+ });
349
+
350
+ const issueReleaseSchema = z.object({
351
+ issueId: z.string().min(1).describe('Issue ID to release'),
352
+ claimantId: z.string().min(1).describe('ID of the current claimant'),
353
+ reason: z.string().optional().describe('Reason for releasing the claim'),
354
+ });
355
+
356
+ const issueHandoffSchema = z.object({
357
+ issueId: z.string().min(1).describe('Issue ID for handoff'),
358
+ fromId: z.string().min(1).describe('Current claimant ID'),
359
+ toId: z.string().optional().describe('Target claimant ID (optional for open handoff)'),
360
+ toType: z.enum(['human', 'agent']).optional().describe('Target claimant type'),
361
+ reason: z.enum(['blocked', 'expertise-needed', 'capacity', 'reassignment', 'other'])
362
+ .describe('Reason for handoff'),
363
+ notes: z.string().optional().describe('Additional notes for handoff'),
364
+ });
365
+
366
+ const issueStatusUpdateSchema = z.object({
367
+ issueId: z.string().min(1).describe('Issue ID to update'),
368
+ claimantId: z.string().min(1).describe('Current claimant ID'),
369
+ status: z.enum(['active', 'blocked', 'in-review', 'completed']).describe('New status'),
370
+ progress: z.number().min(0).max(100).optional().describe('Progress percentage (0-100)'),
371
+ notes: z.string().optional().describe('Status update notes'),
372
+ });
373
+
374
+ const issueListAvailableSchema = z.object({
375
+ priority: z.enum(['critical', 'high', 'medium', 'low']).optional()
376
+ .describe('Filter by priority'),
377
+ labels: z.array(z.string()).optional().describe('Filter by labels'),
378
+ repository: z.string().optional().describe('Filter by repository'),
379
+ limit: z.number().int().positive().max(100).default(50).describe('Maximum results'),
380
+ offset: z.number().int().nonnegative().default(0).describe('Pagination offset'),
381
+ });
382
+
383
+ const issueListMineSchema = z.object({
384
+ claimantId: z.string().min(1).describe('Claimant ID'),
385
+ status: z.enum(['active', 'blocked', 'in-review', 'completed', 'released', 'stolen']).optional()
386
+ .describe('Filter by status'),
387
+ limit: z.number().int().positive().max(100).default(50).describe('Maximum results'),
388
+ offset: z.number().int().nonnegative().default(0).describe('Pagination offset'),
389
+ });
390
+
391
+ const issueBoardSchema = z.object({
392
+ includeAgents: z.boolean().default(true).describe('Include agent claims'),
393
+ includeHumans: z.boolean().default(true).describe('Include human claims'),
394
+ groupBy: z.enum(['claimant', 'priority', 'status']).optional()
395
+ .describe('Group claims by field'),
396
+ });
397
+
398
+ // Work Stealing Schemas
399
+ const issueMarkStealableSchema = z.object({
400
+ issueId: z.string().min(1).describe('Issue ID to mark as stealable'),
401
+ claimantId: z.string().min(1).describe('Current claimant ID'),
402
+ reason: z.string().optional().describe('Reason for making stealable'),
403
+ });
404
+
405
+ const issueStealSchema = z.object({
406
+ issueId: z.string().min(1).describe('Issue ID to steal'),
407
+ stealerId: z.string().min(1).describe('ID of the stealer'),
408
+ stealerType: z.enum(['human', 'agent']).describe('Type of stealer'),
409
+ reason: z.string().optional().describe('Reason for stealing'),
410
+ });
411
+
412
+ const issueGetStealableSchema = z.object({
413
+ priority: z.enum(['critical', 'high', 'medium', 'low']).optional()
414
+ .describe('Filter by priority'),
415
+ limit: z.number().int().positive().max(100).default(50).describe('Maximum results'),
416
+ });
417
+
418
+ const issueContestStealSchema = z.object({
419
+ issueId: z.string().min(1).describe('Issue ID being contested'),
420
+ contesterId: z.string().min(1).describe('ID of the contester'),
421
+ reason: z.string().min(1).describe('Reason for contesting'),
422
+ });
423
+
424
+ // Load Balancing Schemas
425
+ const agentLoadInfoSchema = z.object({
426
+ agentId: z.string().min(1).describe('Agent ID to get load info for'),
427
+ });
428
+
429
+ const swarmRebalanceSchema = z.object({
430
+ strategy: z.enum(['round-robin', 'least-loaded', 'priority-based', 'capability-based'])
431
+ .default('least-loaded').describe('Rebalancing strategy'),
432
+ dryRun: z.boolean().default(false).describe('Simulate without making changes'),
433
+ });
434
+
435
+ const swarmLoadOverviewSchema = z.object({
436
+ includeRecommendations: z.boolean().default(true)
437
+ .describe('Include optimization recommendations'),
438
+ });
439
+
440
+ // Additional Tools Schemas
441
+ const claimHistorySchema = z.object({
442
+ issueId: z.string().min(1).describe('Issue ID to get history for'),
443
+ limit: z.number().int().positive().max(100).default(50).describe('Maximum entries'),
444
+ });
445
+
446
+ const claimMetricsSchema = z.object({
447
+ timeRange: z.enum(['1h', '24h', '7d', '30d', 'all']).default('24h')
448
+ .describe('Time range for metrics'),
449
+ });
450
+
451
+ const claimConfigSchema = z.object({
452
+ action: z.enum(['get', 'set']).describe('Get or set configuration'),
453
+ config: z.object({
454
+ defaultExpirationMs: z.number().int().positive().optional(),
455
+ maxClaimsPerAgent: z.number().int().positive().optional(),
456
+ contestWindowMs: z.number().int().positive().optional(),
457
+ autoReleaseOnInactivityMs: z.number().int().positive().optional(),
458
+ }).optional().describe('Configuration values (for set action)'),
459
+ });
460
+
461
+ // ============================================================================
462
+ // Tool Handlers
463
+ // ============================================================================
464
+
465
+ /**
466
+ * Claim an issue to work on
467
+ */
468
+ async function handleIssueClaim(
469
+ input: z.infer<typeof issueClaimSchema>,
470
+ context?: ToolContext
471
+ ): Promise<{
472
+ claimId: string;
473
+ issueId: string;
474
+ claimantId: string;
475
+ claimantType: ClaimantType;
476
+ status: ClaimStatus;
477
+ claimedAt: string;
478
+ expiresAt?: string;
479
+ }> {
480
+ initializeMockData();
481
+
482
+ // Try to use claims service if available
483
+ if (context?.claimsService) {
484
+ const claim = await context.claimsService.claimIssue(input);
485
+ return {
486
+ claimId: claim.id,
487
+ issueId: claim.issueId,
488
+ claimantId: claim.claimantId,
489
+ claimantType: claim.claimantType,
490
+ status: claim.status,
491
+ claimedAt: claim.claimedAt,
492
+ expiresAt: claim.expiresAt,
493
+ };
494
+ }
495
+
496
+ // Simple implementation
497
+ const issue = issueStore.get(input.issueId);
498
+ if (!issue) {
499
+ throw new Error(`Issue not found: ${input.issueId}`);
500
+ }
501
+
502
+ if (issue.claimedBy) {
503
+ throw new Error(`Issue ${input.issueId} is already claimed by ${issue.claimedBy}`);
504
+ }
505
+
506
+ const claimId = generateSecureId('claim');
507
+ const claimedAt = new Date().toISOString();
508
+ const expiresAt = input.expiresInMs
509
+ ? new Date(Date.now() + input.expiresInMs).toISOString()
510
+ : undefined;
511
+
512
+ const claim: Claim = {
513
+ id: claimId,
514
+ issueId: input.issueId,
515
+ claimantType: input.claimantType,
516
+ claimantId: input.claimantId,
517
+ status: 'active',
518
+ priority: input.priority || issue.priority,
519
+ stealable: false,
520
+ claimedAt,
521
+ lastActivityAt: claimedAt,
522
+ expiresAt,
523
+ };
524
+
525
+ claimStore.set(claimId, claim);
526
+ issue.claimedBy = input.claimantId;
527
+
528
+ return {
529
+ claimId,
530
+ issueId: input.issueId,
531
+ claimantId: input.claimantId,
532
+ claimantType: input.claimantType,
533
+ status: 'active',
534
+ claimedAt,
535
+ expiresAt,
536
+ };
537
+ }
538
+
539
+ /**
540
+ * Release a claim
541
+ */
542
+ async function handleIssueRelease(
543
+ input: z.infer<typeof issueReleaseSchema>,
544
+ context?: ToolContext
545
+ ): Promise<{
546
+ released: boolean;
547
+ issueId: string;
548
+ releasedAt: string;
549
+ reason?: string;
550
+ }> {
551
+ if (context?.claimsService) {
552
+ const result = await context.claimsService.releaseClaim(input);
553
+ return {
554
+ released: result.released,
555
+ issueId: input.issueId,
556
+ releasedAt: result.releasedAt,
557
+ reason: input.reason,
558
+ };
559
+ }
560
+
561
+ // Simple implementation
562
+ const issue = issueStore.get(input.issueId);
563
+ if (!issue) {
564
+ throw new Error(`Issue not found: ${input.issueId}`);
565
+ }
566
+
567
+ if (issue.claimedBy !== input.claimantId) {
568
+ throw new Error(`Issue ${input.issueId} is not claimed by ${input.claimantId}`);
569
+ }
570
+
571
+ // Find and update the claim
572
+ for (const claim of claimStore.values()) {
573
+ if (claim.issueId === input.issueId && claim.claimantId === input.claimantId) {
574
+ claim.status = 'released';
575
+ claim.lastActivityAt = new Date().toISOString();
576
+ }
577
+ }
578
+
579
+ issue.claimedBy = undefined;
580
+
581
+ return {
582
+ released: true,
583
+ issueId: input.issueId,
584
+ releasedAt: new Date().toISOString(),
585
+ reason: input.reason,
586
+ };
587
+ }
588
+
589
+ /**
590
+ * Request handoff to another agent/human
591
+ */
592
+ async function handleIssueHandoff(
593
+ input: z.infer<typeof issueHandoffSchema>,
594
+ context?: ToolContext
595
+ ): Promise<{
596
+ handoffId: string;
597
+ issueId: string;
598
+ fromId: string;
599
+ toId?: string;
600
+ toType?: ClaimantType;
601
+ status: 'pending' | 'accepted' | 'rejected';
602
+ reason: HandoffReason;
603
+ createdAt: string;
604
+ }> {
605
+ if (context?.claimsService) {
606
+ const result = await context.claimsService.requestHandoff(input);
607
+ return {
608
+ handoffId: result.handoffId,
609
+ issueId: input.issueId,
610
+ fromId: input.fromId,
611
+ toId: input.toId,
612
+ toType: input.toType,
613
+ status: result.status as 'pending' | 'accepted' | 'rejected',
614
+ reason: input.reason,
615
+ createdAt: new Date().toISOString(),
616
+ };
617
+ }
618
+
619
+ // Simple implementation
620
+ const handoffId = generateSecureId('handoff');
621
+
622
+ return {
623
+ handoffId,
624
+ issueId: input.issueId,
625
+ fromId: input.fromId,
626
+ toId: input.toId,
627
+ toType: input.toType,
628
+ status: 'pending',
629
+ reason: input.reason,
630
+ createdAt: new Date().toISOString(),
631
+ };
632
+ }
633
+
634
+ /**
635
+ * Update claim status
636
+ */
637
+ async function handleIssueStatusUpdate(
638
+ input: z.infer<typeof issueStatusUpdateSchema>,
639
+ context?: ToolContext
640
+ ): Promise<{
641
+ issueId: string;
642
+ status: ClaimStatus;
643
+ progress?: number;
644
+ updatedAt: string;
645
+ notes?: string;
646
+ }> {
647
+ if (context?.claimsService) {
648
+ const claim = await context.claimsService.updateClaimStatus(input);
649
+ return {
650
+ issueId: claim.issueId,
651
+ status: claim.status,
652
+ progress: input.progress,
653
+ updatedAt: claim.lastActivityAt,
654
+ notes: input.notes,
655
+ };
656
+ }
657
+
658
+ // Simple implementation
659
+ for (const claim of claimStore.values()) {
660
+ if (claim.issueId === input.issueId && claim.claimantId === input.claimantId) {
661
+ claim.status = input.status;
662
+ claim.lastActivityAt = new Date().toISOString();
663
+ if (input.progress !== undefined) {
664
+ claim.metadata = { ...claim.metadata, progress: input.progress };
665
+ }
666
+ return {
667
+ issueId: input.issueId,
668
+ status: input.status,
669
+ progress: input.progress,
670
+ updatedAt: claim.lastActivityAt,
671
+ notes: input.notes,
672
+ };
673
+ }
674
+ }
675
+
676
+ throw new Error(`No active claim found for issue ${input.issueId} by ${input.claimantId}`);
677
+ }
678
+
679
+ /**
680
+ * List unclaimed issues
681
+ */
682
+ async function handleIssueListAvailable(
683
+ input: z.infer<typeof issueListAvailableSchema>,
684
+ context?: ToolContext
685
+ ): Promise<{
686
+ issues: Issue[];
687
+ total: number;
688
+ limit: number;
689
+ offset: number;
690
+ }> {
691
+ initializeMockData();
692
+
693
+ if (context?.claimsService) {
694
+ const result = await context.claimsService.listAvailableIssues(input);
695
+ return {
696
+ ...result,
697
+ limit: input.limit,
698
+ offset: input.offset,
699
+ };
700
+ }
701
+
702
+ // Simple implementation
703
+ let issues = Array.from(issueStore.values()).filter(issue => !issue.claimedBy);
704
+
705
+ if (input.priority) {
706
+ issues = issues.filter(issue => issue.priority === input.priority);
707
+ }
708
+ if (input.labels && input.labels.length > 0) {
709
+ issues = issues.filter(issue =>
710
+ input.labels!.some(label => issue.labels?.includes(label))
711
+ );
712
+ }
713
+ if (input.repository) {
714
+ issues = issues.filter(issue => issue.repository === input.repository);
715
+ }
716
+
717
+ const total = issues.length;
718
+ const paginated = issues.slice(input.offset, input.offset + input.limit);
719
+
720
+ return {
721
+ issues: paginated,
722
+ total,
723
+ limit: input.limit,
724
+ offset: input.offset,
725
+ };
726
+ }
727
+
728
+ /**
729
+ * List my claims
730
+ */
731
+ async function handleIssueListMine(
732
+ input: z.infer<typeof issueListMineSchema>,
733
+ context?: ToolContext
734
+ ): Promise<{
735
+ claims: Claim[];
736
+ total: number;
737
+ limit: number;
738
+ offset: number;
739
+ }> {
740
+ if (context?.claimsService) {
741
+ const result = await context.claimsService.listMyClaims(input);
742
+ return {
743
+ ...result,
744
+ limit: input.limit,
745
+ offset: input.offset,
746
+ };
747
+ }
748
+
749
+ // Simple implementation
750
+ let claims = Array.from(claimStore.values())
751
+ .filter(claim => claim.claimantId === input.claimantId);
752
+
753
+ if (input.status) {
754
+ claims = claims.filter(claim => claim.status === input.status);
755
+ }
756
+
757
+ const total = claims.length;
758
+ const paginated = claims.slice(input.offset, input.offset + input.limit);
759
+
760
+ return {
761
+ claims: paginated,
762
+ total,
763
+ limit: input.limit,
764
+ offset: input.offset,
765
+ };
766
+ }
767
+
768
+ /**
769
+ * View claim board
770
+ */
771
+ async function handleIssueBoard(
772
+ input: z.infer<typeof issueBoardSchema>,
773
+ context?: ToolContext
774
+ ): Promise<{
775
+ claims: Claim[];
776
+ totalClaims: number;
777
+ byClaimant?: Record<string, number>;
778
+ byPriority?: Record<IssuePriority, number>;
779
+ byStatus?: Record<ClaimStatus, number>;
780
+ }> {
781
+ if (context?.claimsService) {
782
+ const result = await context.claimsService.getClaimBoard(input);
783
+ return {
784
+ claims: result.claims,
785
+ totalClaims: result.claims.length,
786
+ byClaimant: result.byClaimant
787
+ ? Object.fromEntries(Object.entries(result.byClaimant).map(([k, v]) => [k, v.length]))
788
+ : undefined,
789
+ byPriority: result.byPriority
790
+ ? Object.fromEntries(Object.entries(result.byPriority).map(([k, v]) => [k, v.length])) as Record<IssuePriority, number>
791
+ : undefined,
792
+ byStatus: result.byStatus
793
+ ? Object.fromEntries(Object.entries(result.byStatus).map(([k, v]) => [k, v.length])) as Record<ClaimStatus, number>
794
+ : undefined,
795
+ };
796
+ }
797
+
798
+ // Simple implementation
799
+ let claims = Array.from(claimStore.values());
800
+
801
+ if (!input.includeAgents) {
802
+ claims = claims.filter(c => c.claimantType !== 'agent');
803
+ }
804
+ if (!input.includeHumans) {
805
+ claims = claims.filter(c => c.claimantType !== 'human');
806
+ }
807
+
808
+ const result: {
809
+ claims: Claim[];
810
+ totalClaims: number;
811
+ byClaimant?: Record<string, number>;
812
+ byPriority?: Record<IssuePriority, number>;
813
+ byStatus?: Record<ClaimStatus, number>;
814
+ } = {
815
+ claims,
816
+ totalClaims: claims.length,
817
+ };
818
+
819
+ if (input.groupBy === 'claimant') {
820
+ result.byClaimant = {};
821
+ claims.forEach(c => {
822
+ result.byClaimant![c.claimantId] = (result.byClaimant![c.claimantId] || 0) + 1;
823
+ });
824
+ } else if (input.groupBy === 'priority') {
825
+ result.byPriority = { critical: 0, high: 0, medium: 0, low: 0 };
826
+ claims.forEach(c => {
827
+ result.byPriority![c.priority]++;
828
+ });
829
+ } else if (input.groupBy === 'status') {
830
+ result.byStatus = { active: 0, blocked: 0, 'in-review': 0, completed: 0, released: 0, stolen: 0 };
831
+ claims.forEach(c => {
832
+ result.byStatus![c.status]++;
833
+ });
834
+ }
835
+
836
+ return result;
837
+ }
838
+
839
+ /**
840
+ * Mark claim as stealable
841
+ */
842
+ async function handleIssueMarkStealable(
843
+ input: z.infer<typeof issueMarkStealableSchema>,
844
+ context?: ToolContext
845
+ ): Promise<{
846
+ marked: boolean;
847
+ issueId: string;
848
+ markedAt: string;
849
+ reason?: string;
850
+ }> {
851
+ if (context?.claimsService) {
852
+ const result = await context.claimsService.markStealable(input);
853
+ return {
854
+ marked: result.marked,
855
+ issueId: input.issueId,
856
+ markedAt: result.markedAt,
857
+ reason: input.reason,
858
+ };
859
+ }
860
+
861
+ // Simple implementation
862
+ for (const claim of claimStore.values()) {
863
+ if (claim.issueId === input.issueId && claim.claimantId === input.claimantId) {
864
+ claim.stealable = true;
865
+ claim.stealableReason = input.reason;
866
+ claim.lastActivityAt = new Date().toISOString();
867
+ return {
868
+ marked: true,
869
+ issueId: input.issueId,
870
+ markedAt: claim.lastActivityAt,
871
+ reason: input.reason,
872
+ };
873
+ }
874
+ }
875
+
876
+ throw new Error(`No active claim found for issue ${input.issueId} by ${input.claimantId}`);
877
+ }
878
+
879
+ /**
880
+ * Steal a stealable issue
881
+ */
882
+ async function handleIssueSteal(
883
+ input: z.infer<typeof issueStealSchema>,
884
+ context?: ToolContext
885
+ ): Promise<{
886
+ stolen: boolean;
887
+ issueId: string;
888
+ newClaimId: string;
889
+ previousClaimant: string;
890
+ contestWindowMs: number;
891
+ stolenAt: string;
892
+ }> {
893
+ if (context?.claimsService) {
894
+ const result = await context.claimsService.stealClaim(input);
895
+ return {
896
+ stolen: result.stolen,
897
+ issueId: input.issueId,
898
+ newClaimId: result.claim.id,
899
+ previousClaimant: result.previousClaimant,
900
+ contestWindowMs: result.contestWindow,
901
+ stolenAt: result.claim.claimedAt,
902
+ };
903
+ }
904
+
905
+ // Simple implementation
906
+ for (const claim of claimStore.values()) {
907
+ if (claim.issueId === input.issueId && claim.stealable) {
908
+ const previousClaimant = claim.claimantId;
909
+
910
+ // Update old claim
911
+ claim.status = 'stolen';
912
+ claim.lastActivityAt = new Date().toISOString();
913
+
914
+ // Create new claim
915
+ const newClaimId = generateSecureId('claim');
916
+ const stolenAt = new Date().toISOString();
917
+ const newClaim: Claim = {
918
+ id: newClaimId,
919
+ issueId: input.issueId,
920
+ claimantType: input.stealerType,
921
+ claimantId: input.stealerId,
922
+ status: 'active',
923
+ priority: claim.priority,
924
+ stealable: false,
925
+ claimedAt: stolenAt,
926
+ lastActivityAt: stolenAt,
927
+ };
928
+ claimStore.set(newClaimId, newClaim);
929
+
930
+ // Update issue
931
+ const issue = issueStore.get(input.issueId);
932
+ if (issue) {
933
+ issue.claimedBy = input.stealerId;
934
+ }
935
+
936
+ return {
937
+ stolen: true,
938
+ issueId: input.issueId,
939
+ newClaimId,
940
+ previousClaimant,
941
+ contestWindowMs: 300000, // 5 minutes
942
+ stolenAt,
943
+ };
944
+ }
945
+ }
946
+
947
+ throw new Error(`Issue ${input.issueId} is not stealable or not claimed`);
948
+ }
949
+
950
+ /**
951
+ * Get stealable issues
952
+ */
953
+ async function handleIssueGetStealable(
954
+ input: z.infer<typeof issueGetStealableSchema>,
955
+ context?: ToolContext
956
+ ): Promise<{
957
+ issues: Array<{
958
+ issueId: string;
959
+ title: string;
960
+ priority: IssuePriority;
961
+ currentClaimant: string;
962
+ stealableReason?: string;
963
+ }>;
964
+ total: number;
965
+ }> {
966
+ if (context?.claimsService) {
967
+ const result = await context.claimsService.getStealableIssues(input);
968
+ return {
969
+ issues: result.issues.map(issue => ({
970
+ issueId: issue.id,
971
+ title: issue.title,
972
+ priority: issue.priority,
973
+ currentClaimant: issue.claimedBy || 'unknown',
974
+ stealableReason: issue.stealableReason,
975
+ })),
976
+ total: result.total,
977
+ };
978
+ }
979
+
980
+ // Simple implementation
981
+ let stealableClaims = Array.from(claimStore.values()).filter(c => c.stealable);
982
+
983
+ if (input.priority) {
984
+ stealableClaims = stealableClaims.filter(c => c.priority === input.priority);
985
+ }
986
+
987
+ const issues = stealableClaims.slice(0, input.limit).map(claim => {
988
+ const issue = issueStore.get(claim.issueId);
989
+ return {
990
+ issueId: claim.issueId,
991
+ title: issue?.title || 'Unknown',
992
+ priority: claim.priority,
993
+ currentClaimant: claim.claimantId,
994
+ stealableReason: claim.stealableReason,
995
+ };
996
+ });
997
+
998
+ return {
999
+ issues,
1000
+ total: stealableClaims.length,
1001
+ };
1002
+ }
1003
+
1004
+ /**
1005
+ * Contest a steal
1006
+ */
1007
+ async function handleIssueContestSteal(
1008
+ input: z.infer<typeof issueContestStealSchema>,
1009
+ context?: ToolContext
1010
+ ): Promise<{
1011
+ contested: boolean;
1012
+ contestId: string;
1013
+ issueId: string;
1014
+ contesterId: string;
1015
+ status: 'pending' | 'upheld' | 'reversed';
1016
+ contestedAt: string;
1017
+ }> {
1018
+ if (context?.claimsService) {
1019
+ const result = await context.claimsService.contestSteal(input);
1020
+ return {
1021
+ contested: result.contested,
1022
+ contestId: generateSecureId('contest'),
1023
+ issueId: input.issueId,
1024
+ contesterId: input.contesterId,
1025
+ status: result.resolution,
1026
+ contestedAt: result.resolvedAt || new Date().toISOString(),
1027
+ };
1028
+ }
1029
+
1030
+ // Simple implementation
1031
+ return {
1032
+ contested: true,
1033
+ contestId: generateSecureId('contest'),
1034
+ issueId: input.issueId,
1035
+ contesterId: input.contesterId,
1036
+ status: 'pending',
1037
+ contestedAt: new Date().toISOString(),
1038
+ };
1039
+ }
1040
+
1041
+ /**
1042
+ * Get agent load info
1043
+ */
1044
+ async function handleAgentLoadInfo(
1045
+ input: z.infer<typeof agentLoadInfoSchema>,
1046
+ context?: ToolContext
1047
+ ): Promise<AgentLoad> {
1048
+ if (context?.claimsService) {
1049
+ return context.claimsService.getAgentLoad(input);
1050
+ }
1051
+
1052
+ // Simple implementation
1053
+ const claims = Array.from(claimStore.values())
1054
+ .filter(c => c.claimantId === input.agentId && c.status === 'active');
1055
+
1056
+ return {
1057
+ agentId: input.agentId,
1058
+ agentType: 'worker',
1059
+ currentClaims: claims.length,
1060
+ maxClaims: 5,
1061
+ utilizationPercent: Math.min(100, (claims.length / 5) * 100),
1062
+ activeTasks: claims.length,
1063
+ queuedTasks: 0,
1064
+ averageTaskDuration: 3600000, // 1 hour
1065
+ lastActivityAt: new Date().toISOString(),
1066
+ };
1067
+ }
1068
+
1069
+ /**
1070
+ * Trigger swarm rebalancing
1071
+ */
1072
+ async function handleSwarmRebalance(
1073
+ input: z.infer<typeof swarmRebalanceSchema>,
1074
+ context?: ToolContext
1075
+ ): Promise<{
1076
+ rebalanced: boolean;
1077
+ strategy: string;
1078
+ changes: Array<{ issueId: string; from: string; to: string }>;
1079
+ dryRun: boolean;
1080
+ rebalancedAt: string;
1081
+ }> {
1082
+ if (context?.claimsService) {
1083
+ const result = await context.claimsService.rebalanceSwarm(input);
1084
+ return {
1085
+ rebalanced: result.rebalanced,
1086
+ strategy: input.strategy,
1087
+ changes: result.changes,
1088
+ dryRun: result.dryRun,
1089
+ rebalancedAt: new Date().toISOString(),
1090
+ };
1091
+ }
1092
+
1093
+ // Simple implementation - no actual rebalancing
1094
+ return {
1095
+ rebalanced: !input.dryRun,
1096
+ strategy: input.strategy,
1097
+ changes: [],
1098
+ dryRun: input.dryRun,
1099
+ rebalancedAt: new Date().toISOString(),
1100
+ };
1101
+ }
1102
+
1103
+ /**
1104
+ * Get swarm-wide load overview
1105
+ */
1106
+ async function handleSwarmLoadOverview(
1107
+ input: z.infer<typeof swarmLoadOverviewSchema>,
1108
+ context?: ToolContext
1109
+ ): Promise<{
1110
+ totalAgents: number;
1111
+ totalClaims: number;
1112
+ averageLoad: number;
1113
+ agents: Array<{ agentId: string; currentClaims: number; utilizationPercent: number }>;
1114
+ bottlenecks: string[];
1115
+ recommendations: string[];
1116
+ }> {
1117
+ if (context?.claimsService) {
1118
+ const result = await context.claimsService.getLoadOverview();
1119
+ return {
1120
+ ...result,
1121
+ agents: result.agents.map(a => ({
1122
+ agentId: a.agentId,
1123
+ currentClaims: a.currentClaims,
1124
+ utilizationPercent: a.utilizationPercent,
1125
+ })),
1126
+ };
1127
+ }
1128
+
1129
+ // Simple implementation
1130
+ const claimsByAgent = new Map<string, number>();
1131
+ for (const claim of claimStore.values()) {
1132
+ if (claim.status === 'active') {
1133
+ claimsByAgent.set(claim.claimantId, (claimsByAgent.get(claim.claimantId) || 0) + 1);
1134
+ }
1135
+ }
1136
+
1137
+ const agents = Array.from(claimsByAgent.entries()).map(([agentId, claims]) => ({
1138
+ agentId,
1139
+ currentClaims: claims,
1140
+ utilizationPercent: Math.min(100, (claims / 5) * 100),
1141
+ }));
1142
+
1143
+ const totalClaims = Array.from(claimsByAgent.values()).reduce((a, b) => a + b, 0);
1144
+ const avgLoad = agents.length > 0
1145
+ ? agents.reduce((a, b) => a + b.utilizationPercent, 0) / agents.length
1146
+ : 0;
1147
+
1148
+ const result: {
1149
+ totalAgents: number;
1150
+ totalClaims: number;
1151
+ averageLoad: number;
1152
+ agents: Array<{ agentId: string; currentClaims: number; utilizationPercent: number }>;
1153
+ bottlenecks: string[];
1154
+ recommendations: string[];
1155
+ } = {
1156
+ totalAgents: agents.length,
1157
+ totalClaims,
1158
+ averageLoad: Math.round(avgLoad * 100) / 100,
1159
+ agents,
1160
+ bottlenecks: [],
1161
+ recommendations: [],
1162
+ };
1163
+
1164
+ if (input.includeRecommendations) {
1165
+ const overloaded = agents.filter(a => a.utilizationPercent > 80);
1166
+ if (overloaded.length > 0) {
1167
+ result.bottlenecks = overloaded.map(a => a.agentId);
1168
+ result.recommendations.push('Consider rebalancing claims to reduce load on overloaded agents');
1169
+ }
1170
+ if (avgLoad > 70) {
1171
+ result.recommendations.push('Swarm is under high load. Consider scaling up agent count.');
1172
+ }
1173
+ if (avgLoad < 30 && agents.length > 1) {
1174
+ result.recommendations.push('Swarm has low utilization. Consider consolidating agents.');
1175
+ }
1176
+ }
1177
+
1178
+ return result;
1179
+ }
1180
+
1181
+ /**
1182
+ * Get claim history
1183
+ */
1184
+ async function handleClaimHistory(
1185
+ input: z.infer<typeof claimHistorySchema>,
1186
+ context?: ToolContext
1187
+ ): Promise<{
1188
+ issueId: string;
1189
+ history: ClaimHistoryEntry[];
1190
+ total: number;
1191
+ }> {
1192
+ if (context?.claimsService) {
1193
+ const result = await context.claimsService.getClaimHistory(input);
1194
+ return {
1195
+ issueId: input.issueId,
1196
+ history: result.history,
1197
+ total: result.total,
1198
+ };
1199
+ }
1200
+
1201
+ // Simple implementation - mock history
1202
+ const claims = Array.from(claimStore.values())
1203
+ .filter(c => c.issueId === input.issueId)
1204
+ .sort((a, b) => new Date(b.claimedAt).getTime() - new Date(a.claimedAt).getTime());
1205
+
1206
+ const history: ClaimHistoryEntry[] = claims.flatMap(claim => [
1207
+ {
1208
+ timestamp: claim.claimedAt,
1209
+ action: 'claimed',
1210
+ actorId: claim.claimantId,
1211
+ actorType: claim.claimantType,
1212
+ },
1213
+ ...(claim.status !== 'active' ? [{
1214
+ timestamp: claim.lastActivityAt,
1215
+ action: claim.status,
1216
+ actorId: claim.claimantId,
1217
+ actorType: claim.claimantType,
1218
+ }] : []),
1219
+ ]).slice(0, input.limit);
1220
+
1221
+ return {
1222
+ issueId: input.issueId,
1223
+ history,
1224
+ total: history.length,
1225
+ };
1226
+ }
1227
+
1228
+ /**
1229
+ * Get claim metrics
1230
+ */
1231
+ async function handleClaimMetrics(
1232
+ input: z.infer<typeof claimMetricsSchema>,
1233
+ context?: ToolContext
1234
+ ): Promise<{
1235
+ timeRange: string;
1236
+ totalClaims: number;
1237
+ activeClaims: number;
1238
+ completedClaims: number;
1239
+ stolenClaims: number;
1240
+ averageClaimDurationMs: number;
1241
+ claimsByPriority: Record<IssuePriority, number>;
1242
+ claimsByStatus: Record<ClaimStatus, number>;
1243
+ }> {
1244
+ if (context?.claimsService) {
1245
+ const metrics = await context.claimsService.getMetrics();
1246
+ return {
1247
+ timeRange: input.timeRange,
1248
+ totalClaims: metrics.totalClaims,
1249
+ activeClaims: metrics.activeClaims,
1250
+ completedClaims: metrics.completedClaims,
1251
+ stolenClaims: metrics.stolenClaims,
1252
+ averageClaimDurationMs: metrics.averageClaimDuration,
1253
+ claimsByPriority: metrics.claimsByPriority,
1254
+ claimsByStatus: metrics.claimsByStatus,
1255
+ };
1256
+ }
1257
+
1258
+ // Simple implementation
1259
+ const claims = Array.from(claimStore.values());
1260
+ const byPriority: Record<IssuePriority, number> = { critical: 0, high: 0, medium: 0, low: 0 };
1261
+ const byStatus: Record<ClaimStatus, number> = {
1262
+ active: 0, blocked: 0, 'in-review': 0, completed: 0, released: 0, stolen: 0,
1263
+ };
1264
+
1265
+ claims.forEach(c => {
1266
+ byPriority[c.priority]++;
1267
+ byStatus[c.status]++;
1268
+ });
1269
+
1270
+ return {
1271
+ timeRange: input.timeRange,
1272
+ totalClaims: claims.length,
1273
+ activeClaims: byStatus.active,
1274
+ completedClaims: byStatus.completed,
1275
+ stolenClaims: byStatus.stolen,
1276
+ averageClaimDurationMs: 3600000, // 1 hour mock
1277
+ claimsByPriority: byPriority,
1278
+ claimsByStatus: byStatus,
1279
+ };
1280
+ }
1281
+
1282
+ /**
1283
+ * Get/set claim configuration
1284
+ */
1285
+ async function handleClaimConfig(
1286
+ input: z.infer<typeof claimConfigSchema>,
1287
+ _context?: ToolContext
1288
+ ): Promise<{
1289
+ action: 'get' | 'set';
1290
+ config: {
1291
+ defaultExpirationMs: number;
1292
+ maxClaimsPerAgent: number;
1293
+ contestWindowMs: number;
1294
+ autoReleaseOnInactivityMs: number;
1295
+ };
1296
+ updatedAt?: string;
1297
+ }> {
1298
+ // Default configuration
1299
+ const defaultConfig = {
1300
+ defaultExpirationMs: 86400000, // 24 hours
1301
+ maxClaimsPerAgent: 5,
1302
+ contestWindowMs: 300000, // 5 minutes
1303
+ autoReleaseOnInactivityMs: 7200000, // 2 hours
1304
+ };
1305
+
1306
+ if (input.action === 'get') {
1307
+ return {
1308
+ action: 'get',
1309
+ config: defaultConfig,
1310
+ };
1311
+ }
1312
+
1313
+ // Set action
1314
+ const newConfig = {
1315
+ ...defaultConfig,
1316
+ ...input.config,
1317
+ };
1318
+
1319
+ return {
1320
+ action: 'set',
1321
+ config: newConfig,
1322
+ updatedAt: new Date().toISOString(),
1323
+ };
1324
+ }
1325
+
1326
+ // ============================================================================
1327
+ // Tool Definitions
1328
+ // ============================================================================
1329
+
1330
+ // Core Claiming Tools
1331
+
1332
+ export const issueClaimTool: MCPTool = {
1333
+ name: 'claims/issue_claim',
1334
+ description: 'Claim an issue to work on. Prevents duplicate work by ensuring only one agent/human works on an issue at a time.',
1335
+ inputSchema: {
1336
+ type: 'object',
1337
+ properties: {
1338
+ issueId: { type: 'string', description: 'Issue ID to claim' },
1339
+ claimantType: { type: 'string', enum: ['human', 'agent'], description: 'Type of claimant' },
1340
+ claimantId: { type: 'string', description: 'ID of the claimant' },
1341
+ priority: {
1342
+ type: 'string',
1343
+ enum: ['critical', 'high', 'medium', 'low'],
1344
+ description: 'Override priority for the claim',
1345
+ },
1346
+ expiresInMs: {
1347
+ type: 'number',
1348
+ description: 'Claim expiration time in milliseconds',
1349
+ minimum: 1,
1350
+ },
1351
+ },
1352
+ required: ['issueId', 'claimantType', 'claimantId'],
1353
+ },
1354
+ handler: async (input, context) => {
1355
+ const validated = issueClaimSchema.parse(input);
1356
+ return handleIssueClaim(validated, context);
1357
+ },
1358
+ category: 'claims',
1359
+ tags: ['claims', 'issue', 'coordination'],
1360
+ version: '1.0.0',
1361
+ };
1362
+
1363
+ export const issueReleaseTool: MCPTool = {
1364
+ name: 'claims/issue_release',
1365
+ description: 'Release a claim on an issue, making it available for others to work on.',
1366
+ inputSchema: {
1367
+ type: 'object',
1368
+ properties: {
1369
+ issueId: { type: 'string', description: 'Issue ID to release' },
1370
+ claimantId: { type: 'string', description: 'ID of the current claimant' },
1371
+ reason: { type: 'string', description: 'Reason for releasing the claim' },
1372
+ },
1373
+ required: ['issueId', 'claimantId'],
1374
+ },
1375
+ handler: async (input, context) => {
1376
+ const validated = issueReleaseSchema.parse(input);
1377
+ return handleIssueRelease(validated, context);
1378
+ },
1379
+ category: 'claims',
1380
+ tags: ['claims', 'issue', 'release'],
1381
+ version: '1.0.0',
1382
+ };
1383
+
1384
+ export const issueHandoffTool: MCPTool = {
1385
+ name: 'claims/issue_handoff',
1386
+ description: 'Request handoff of an issue to another agent or human. Useful when blocked or needing specific expertise.',
1387
+ inputSchema: {
1388
+ type: 'object',
1389
+ properties: {
1390
+ issueId: { type: 'string', description: 'Issue ID for handoff' },
1391
+ fromId: { type: 'string', description: 'Current claimant ID' },
1392
+ toId: { type: 'string', description: 'Target claimant ID (optional for open handoff)' },
1393
+ toType: { type: 'string', enum: ['human', 'agent'], description: 'Target claimant type' },
1394
+ reason: {
1395
+ type: 'string',
1396
+ enum: ['blocked', 'expertise-needed', 'capacity', 'reassignment', 'other'],
1397
+ description: 'Reason for handoff',
1398
+ },
1399
+ notes: { type: 'string', description: 'Additional notes for handoff' },
1400
+ },
1401
+ required: ['issueId', 'fromId', 'reason'],
1402
+ },
1403
+ handler: async (input, context) => {
1404
+ const validated = issueHandoffSchema.parse(input);
1405
+ return handleIssueHandoff(validated, context);
1406
+ },
1407
+ category: 'claims',
1408
+ tags: ['claims', 'issue', 'handoff', 'coordination'],
1409
+ version: '1.0.0',
1410
+ };
1411
+
1412
+ export const issueStatusUpdateTool: MCPTool = {
1413
+ name: 'claims/issue_status_update',
1414
+ description: 'Update the status of a claimed issue. Track progress and communicate blockers.',
1415
+ inputSchema: {
1416
+ type: 'object',
1417
+ properties: {
1418
+ issueId: { type: 'string', description: 'Issue ID to update' },
1419
+ claimantId: { type: 'string', description: 'Current claimant ID' },
1420
+ status: {
1421
+ type: 'string',
1422
+ enum: ['active', 'blocked', 'in-review', 'completed'],
1423
+ description: 'New status',
1424
+ },
1425
+ progress: {
1426
+ type: 'number',
1427
+ description: 'Progress percentage (0-100)',
1428
+ minimum: 0,
1429
+ maximum: 100,
1430
+ },
1431
+ notes: { type: 'string', description: 'Status update notes' },
1432
+ },
1433
+ required: ['issueId', 'claimantId', 'status'],
1434
+ },
1435
+ handler: async (input, context) => {
1436
+ const validated = issueStatusUpdateSchema.parse(input);
1437
+ return handleIssueStatusUpdate(validated, context);
1438
+ },
1439
+ category: 'claims',
1440
+ tags: ['claims', 'issue', 'status', 'progress'],
1441
+ version: '1.0.0',
1442
+ };
1443
+
1444
+ export const issueListAvailableTool: MCPTool = {
1445
+ name: 'claims/issue_list_available',
1446
+ description: 'List all unclaimed issues available for work. Filter by priority, labels, or repository.',
1447
+ inputSchema: {
1448
+ type: 'object',
1449
+ properties: {
1450
+ priority: {
1451
+ type: 'string',
1452
+ enum: ['critical', 'high', 'medium', 'low'],
1453
+ description: 'Filter by priority',
1454
+ },
1455
+ labels: {
1456
+ type: 'array',
1457
+ items: { type: 'string' },
1458
+ description: 'Filter by labels',
1459
+ },
1460
+ repository: { type: 'string', description: 'Filter by repository' },
1461
+ limit: {
1462
+ type: 'number',
1463
+ description: 'Maximum results',
1464
+ minimum: 1,
1465
+ maximum: 100,
1466
+ default: 50,
1467
+ },
1468
+ offset: {
1469
+ type: 'number',
1470
+ description: 'Pagination offset',
1471
+ minimum: 0,
1472
+ default: 0,
1473
+ },
1474
+ },
1475
+ },
1476
+ handler: async (input, context) => {
1477
+ const validated = issueListAvailableSchema.parse(input);
1478
+ return handleIssueListAvailable(validated, context);
1479
+ },
1480
+ category: 'claims',
1481
+ tags: ['claims', 'issue', 'list', 'available'],
1482
+ version: '1.0.0',
1483
+ cacheable: true,
1484
+ cacheTTL: 5000,
1485
+ };
1486
+
1487
+ export const issueListMineTool: MCPTool = {
1488
+ name: 'claims/issue_list_mine',
1489
+ description: 'List all issues claimed by a specific claimant. Filter by status.',
1490
+ inputSchema: {
1491
+ type: 'object',
1492
+ properties: {
1493
+ claimantId: { type: 'string', description: 'Claimant ID' },
1494
+ status: {
1495
+ type: 'string',
1496
+ enum: ['active', 'blocked', 'in-review', 'completed', 'released', 'stolen'],
1497
+ description: 'Filter by status',
1498
+ },
1499
+ limit: {
1500
+ type: 'number',
1501
+ description: 'Maximum results',
1502
+ minimum: 1,
1503
+ maximum: 100,
1504
+ default: 50,
1505
+ },
1506
+ offset: {
1507
+ type: 'number',
1508
+ description: 'Pagination offset',
1509
+ minimum: 0,
1510
+ default: 0,
1511
+ },
1512
+ },
1513
+ required: ['claimantId'],
1514
+ },
1515
+ handler: async (input, context) => {
1516
+ const validated = issueListMineSchema.parse(input);
1517
+ return handleIssueListMine(validated, context);
1518
+ },
1519
+ category: 'claims',
1520
+ tags: ['claims', 'issue', 'list', 'my-claims'],
1521
+ version: '1.0.0',
1522
+ cacheable: true,
1523
+ cacheTTL: 2000,
1524
+ };
1525
+
1526
+ export const issueBoardTool: MCPTool = {
1527
+ name: 'claims/issue_board',
1528
+ description: 'View the claim board showing who is working on what. Group by claimant, priority, or status.',
1529
+ inputSchema: {
1530
+ type: 'object',
1531
+ properties: {
1532
+ includeAgents: {
1533
+ type: 'boolean',
1534
+ description: 'Include agent claims',
1535
+ default: true,
1536
+ },
1537
+ includeHumans: {
1538
+ type: 'boolean',
1539
+ description: 'Include human claims',
1540
+ default: true,
1541
+ },
1542
+ groupBy: {
1543
+ type: 'string',
1544
+ enum: ['claimant', 'priority', 'status'],
1545
+ description: 'Group claims by field',
1546
+ },
1547
+ },
1548
+ },
1549
+ handler: async (input, context) => {
1550
+ const validated = issueBoardSchema.parse(input);
1551
+ return handleIssueBoard(validated, context);
1552
+ },
1553
+ category: 'claims',
1554
+ tags: ['claims', 'board', 'overview', 'coordination'],
1555
+ version: '1.0.0',
1556
+ cacheable: true,
1557
+ cacheTTL: 5000,
1558
+ };
1559
+
1560
+ // Work Stealing Tools
1561
+
1562
+ export const issueMarkStealableTool: MCPTool = {
1563
+ name: 'claims/issue_mark_stealable',
1564
+ description: 'Mark a claimed issue as stealable, allowing other agents/humans to take over the work.',
1565
+ inputSchema: {
1566
+ type: 'object',
1567
+ properties: {
1568
+ issueId: { type: 'string', description: 'Issue ID to mark as stealable' },
1569
+ claimantId: { type: 'string', description: 'Current claimant ID' },
1570
+ reason: { type: 'string', description: 'Reason for making stealable' },
1571
+ },
1572
+ required: ['issueId', 'claimantId'],
1573
+ },
1574
+ handler: async (input, context) => {
1575
+ const validated = issueMarkStealableSchema.parse(input);
1576
+ return handleIssueMarkStealable(validated, context);
1577
+ },
1578
+ category: 'claims',
1579
+ tags: ['claims', 'stealing', 'mark'],
1580
+ version: '1.0.0',
1581
+ };
1582
+
1583
+ export const issueStealTool: MCPTool = {
1584
+ name: 'claims/issue_steal',
1585
+ description: 'Steal a stealable issue from another claimant. The previous claimant has a contest window to object.',
1586
+ inputSchema: {
1587
+ type: 'object',
1588
+ properties: {
1589
+ issueId: { type: 'string', description: 'Issue ID to steal' },
1590
+ stealerId: { type: 'string', description: 'ID of the stealer' },
1591
+ stealerType: { type: 'string', enum: ['human', 'agent'], description: 'Type of stealer' },
1592
+ reason: { type: 'string', description: 'Reason for stealing' },
1593
+ },
1594
+ required: ['issueId', 'stealerId', 'stealerType'],
1595
+ },
1596
+ handler: async (input, context) => {
1597
+ const validated = issueStealSchema.parse(input);
1598
+ return handleIssueSteal(validated, context);
1599
+ },
1600
+ category: 'claims',
1601
+ tags: ['claims', 'stealing', 'takeover'],
1602
+ version: '1.0.0',
1603
+ };
1604
+
1605
+ export const issueGetStealableTool: MCPTool = {
1606
+ name: 'claims/issue_get_stealable',
1607
+ description: 'List all issues marked as stealable. Filter by priority.',
1608
+ inputSchema: {
1609
+ type: 'object',
1610
+ properties: {
1611
+ priority: {
1612
+ type: 'string',
1613
+ enum: ['critical', 'high', 'medium', 'low'],
1614
+ description: 'Filter by priority',
1615
+ },
1616
+ limit: {
1617
+ type: 'number',
1618
+ description: 'Maximum results',
1619
+ minimum: 1,
1620
+ maximum: 100,
1621
+ default: 50,
1622
+ },
1623
+ },
1624
+ },
1625
+ handler: async (input, context) => {
1626
+ const validated = issueGetStealableSchema.parse(input);
1627
+ return handleIssueGetStealable(validated, context);
1628
+ },
1629
+ category: 'claims',
1630
+ tags: ['claims', 'stealing', 'list'],
1631
+ version: '1.0.0',
1632
+ cacheable: true,
1633
+ cacheTTL: 5000,
1634
+ };
1635
+
1636
+ export const issueContestStealTool: MCPTool = {
1637
+ name: 'claims/issue_contest_steal',
1638
+ description: 'Contest a steal within the contest window. Provide a reason for the contest.',
1639
+ inputSchema: {
1640
+ type: 'object',
1641
+ properties: {
1642
+ issueId: { type: 'string', description: 'Issue ID being contested' },
1643
+ contesterId: { type: 'string', description: 'ID of the contester' },
1644
+ reason: { type: 'string', description: 'Reason for contesting' },
1645
+ },
1646
+ required: ['issueId', 'contesterId', 'reason'],
1647
+ },
1648
+ handler: async (input, context) => {
1649
+ const validated = issueContestStealSchema.parse(input);
1650
+ return handleIssueContestSteal(validated, context);
1651
+ },
1652
+ category: 'claims',
1653
+ tags: ['claims', 'stealing', 'contest'],
1654
+ version: '1.0.0',
1655
+ };
1656
+
1657
+ // Load Balancing Tools
1658
+
1659
+ export const agentLoadInfoTool: MCPTool = {
1660
+ name: 'claims/agent_load_info',
1661
+ description: 'Get current load information for a specific agent including claims, tasks, and utilization.',
1662
+ inputSchema: {
1663
+ type: 'object',
1664
+ properties: {
1665
+ agentId: { type: 'string', description: 'Agent ID to get load info for' },
1666
+ },
1667
+ required: ['agentId'],
1668
+ },
1669
+ handler: async (input, context) => {
1670
+ const validated = agentLoadInfoSchema.parse(input);
1671
+ return handleAgentLoadInfo(validated, context);
1672
+ },
1673
+ category: 'claims',
1674
+ tags: ['claims', 'load', 'agent', 'metrics'],
1675
+ version: '1.0.0',
1676
+ cacheable: true,
1677
+ cacheTTL: 2000,
1678
+ };
1679
+
1680
+ export const swarmRebalanceTool: MCPTool = {
1681
+ name: 'claims/swarm_rebalance',
1682
+ description: 'Trigger rebalancing of claims across the swarm to optimize load distribution.',
1683
+ inputSchema: {
1684
+ type: 'object',
1685
+ properties: {
1686
+ strategy: {
1687
+ type: 'string',
1688
+ enum: ['round-robin', 'least-loaded', 'priority-based', 'capability-based'],
1689
+ description: 'Rebalancing strategy',
1690
+ default: 'least-loaded',
1691
+ },
1692
+ dryRun: {
1693
+ type: 'boolean',
1694
+ description: 'Simulate without making changes',
1695
+ default: false,
1696
+ },
1697
+ },
1698
+ },
1699
+ handler: async (input, context) => {
1700
+ const validated = swarmRebalanceSchema.parse(input);
1701
+ return handleSwarmRebalance(validated, context);
1702
+ },
1703
+ category: 'claims',
1704
+ tags: ['claims', 'load', 'swarm', 'rebalance'],
1705
+ version: '1.0.0',
1706
+ };
1707
+
1708
+ export const swarmLoadOverviewTool: MCPTool = {
1709
+ name: 'claims/swarm_load_overview',
1710
+ description: 'Get swarm-wide load distribution including all agents, bottlenecks, and optimization recommendations.',
1711
+ inputSchema: {
1712
+ type: 'object',
1713
+ properties: {
1714
+ includeRecommendations: {
1715
+ type: 'boolean',
1716
+ description: 'Include optimization recommendations',
1717
+ default: true,
1718
+ },
1719
+ },
1720
+ },
1721
+ handler: async (input, context) => {
1722
+ const validated = swarmLoadOverviewSchema.parse(input);
1723
+ return handleSwarmLoadOverview(validated, context);
1724
+ },
1725
+ category: 'claims',
1726
+ tags: ['claims', 'load', 'swarm', 'overview', 'metrics'],
1727
+ version: '1.0.0',
1728
+ cacheable: true,
1729
+ cacheTTL: 5000,
1730
+ };
1731
+
1732
+ // Additional Tools
1733
+
1734
+ export const claimHistoryTool: MCPTool = {
1735
+ name: 'claims/claim_history',
1736
+ description: 'Get the claim history for a specific issue showing all past claims and actions.',
1737
+ inputSchema: {
1738
+ type: 'object',
1739
+ properties: {
1740
+ issueId: { type: 'string', description: 'Issue ID to get history for' },
1741
+ limit: {
1742
+ type: 'number',
1743
+ description: 'Maximum entries',
1744
+ minimum: 1,
1745
+ maximum: 100,
1746
+ default: 50,
1747
+ },
1748
+ },
1749
+ required: ['issueId'],
1750
+ },
1751
+ handler: async (input, context) => {
1752
+ const validated = claimHistorySchema.parse(input);
1753
+ return handleClaimHistory(validated, context);
1754
+ },
1755
+ category: 'claims',
1756
+ tags: ['claims', 'history', 'audit'],
1757
+ version: '1.0.0',
1758
+ cacheable: true,
1759
+ cacheTTL: 10000,
1760
+ };
1761
+
1762
+ export const claimMetricsTool: MCPTool = {
1763
+ name: 'claims/claim_metrics',
1764
+ description: 'Get claiming metrics including totals, averages, and distributions by priority and status.',
1765
+ inputSchema: {
1766
+ type: 'object',
1767
+ properties: {
1768
+ timeRange: {
1769
+ type: 'string',
1770
+ enum: ['1h', '24h', '7d', '30d', 'all'],
1771
+ description: 'Time range for metrics',
1772
+ default: '24h',
1773
+ },
1774
+ },
1775
+ },
1776
+ handler: async (input, context) => {
1777
+ const validated = claimMetricsSchema.parse(input);
1778
+ return handleClaimMetrics(validated, context);
1779
+ },
1780
+ category: 'claims',
1781
+ tags: ['claims', 'metrics', 'analytics'],
1782
+ version: '1.0.0',
1783
+ cacheable: true,
1784
+ cacheTTL: 30000,
1785
+ };
1786
+
1787
+ export const claimConfigTool: MCPTool = {
1788
+ name: 'claims/claim_config',
1789
+ description: 'Get or set claiming configuration including expiration times, limits, and contest windows.',
1790
+ inputSchema: {
1791
+ type: 'object',
1792
+ properties: {
1793
+ action: { type: 'string', enum: ['get', 'set'], description: 'Get or set configuration' },
1794
+ config: {
1795
+ type: 'object',
1796
+ description: 'Configuration values (for set action)',
1797
+ properties: {
1798
+ defaultExpirationMs: { type: 'number', minimum: 1 },
1799
+ maxClaimsPerAgent: { type: 'number', minimum: 1 },
1800
+ contestWindowMs: { type: 'number', minimum: 1 },
1801
+ autoReleaseOnInactivityMs: { type: 'number', minimum: 1 },
1802
+ },
1803
+ },
1804
+ },
1805
+ required: ['action'],
1806
+ },
1807
+ handler: async (input, context) => {
1808
+ const validated = claimConfigSchema.parse(input);
1809
+ return handleClaimConfig(validated, context);
1810
+ },
1811
+ category: 'claims',
1812
+ tags: ['claims', 'config', 'settings'],
1813
+ version: '1.0.0',
1814
+ };
1815
+
1816
+ // ============================================================================
1817
+ // Tool Collections
1818
+ // ============================================================================
1819
+
1820
+ /**
1821
+ * Core claiming tools (7 tools)
1822
+ */
1823
+ export const coreClaimingTools: MCPTool[] = [
1824
+ issueClaimTool,
1825
+ issueReleaseTool,
1826
+ issueHandoffTool,
1827
+ issueStatusUpdateTool,
1828
+ issueListAvailableTool,
1829
+ issueListMineTool,
1830
+ issueBoardTool,
1831
+ ];
1832
+
1833
+ /**
1834
+ * Work stealing tools (4 tools)
1835
+ */
1836
+ export const workStealingTools: MCPTool[] = [
1837
+ issueMarkStealableTool,
1838
+ issueStealTool,
1839
+ issueGetStealableTool,
1840
+ issueContestStealTool,
1841
+ ];
1842
+
1843
+ /**
1844
+ * Load balancing tools (3 tools)
1845
+ */
1846
+ export const loadBalancingTools: MCPTool[] = [
1847
+ agentLoadInfoTool,
1848
+ swarmRebalanceTool,
1849
+ swarmLoadOverviewTool,
1850
+ ];
1851
+
1852
+ /**
1853
+ * Additional tools (3 tools)
1854
+ */
1855
+ export const additionalClaimsTools: MCPTool[] = [
1856
+ claimHistoryTool,
1857
+ claimMetricsTool,
1858
+ claimConfigTool,
1859
+ ];
1860
+
1861
+ /**
1862
+ * All claims tools (17 tools total)
1863
+ */
1864
+ export const claimsTools: MCPTool[] = [
1865
+ ...coreClaimingTools,
1866
+ ...workStealingTools,
1867
+ ...loadBalancingTools,
1868
+ ...additionalClaimsTools,
1869
+ ];
1870
+
1871
+ // ============================================================================
1872
+ // Registration Function
1873
+ // ============================================================================
1874
+
1875
+ /**
1876
+ * Register all claims tools with an MCP server or tool registry
1877
+ *
1878
+ * @param registry - Tool registry or server to register with
1879
+ * @returns Number of tools registered
1880
+ *
1881
+ * @example
1882
+ * ```typescript
1883
+ * import { registerClaimsTools, claimsTools } from '@sparkleideas/claims';
1884
+ *
1885
+ * // Register all tools
1886
+ * const count = registerClaimsTools(server);
1887
+ * console.log(`Registered ${count} claims tools`);
1888
+ *
1889
+ * // Or use tools directly
1890
+ * server.registerTools(claimsTools);
1891
+ * ```
1892
+ */
1893
+ export function registerClaimsTools(
1894
+ registry: { registerTool?: (tool: MCPTool) => void; register?: (tool: MCPTool) => void }
1895
+ ): number {
1896
+ const registerFn = registry.registerTool || registry.register;
1897
+
1898
+ if (!registerFn) {
1899
+ throw new Error('Registry must have a registerTool or register method');
1900
+ }
1901
+
1902
+ claimsTools.forEach(tool => registerFn.call(registry, tool));
1903
+
1904
+ return claimsTools.length;
1905
+ }
1906
+
1907
+ /**
1908
+ * Get claims tools by category
1909
+ *
1910
+ * @param category - Category name: 'core', 'stealing', 'load', or 'additional'
1911
+ * @returns Array of tools in that category
1912
+ */
1913
+ export function getClaimsToolsByCategory(
1914
+ category: 'core' | 'stealing' | 'load' | 'additional'
1915
+ ): MCPTool[] {
1916
+ switch (category) {
1917
+ case 'core':
1918
+ return coreClaimingTools;
1919
+ case 'stealing':
1920
+ return workStealingTools;
1921
+ case 'load':
1922
+ return loadBalancingTools;
1923
+ case 'additional':
1924
+ return additionalClaimsTools;
1925
+ default:
1926
+ return [];
1927
+ }
1928
+ }
1929
+
1930
+ /**
1931
+ * Get a specific claims tool by name
1932
+ *
1933
+ * @param name - Tool name (e.g., 'claims/issue_claim')
1934
+ * @returns The tool if found, undefined otherwise
1935
+ */
1936
+ export function getClaimsToolByName(name: string): MCPTool | undefined {
1937
+ return claimsTools.find(tool => tool.name === name);
1938
+ }
1939
+
1940
+ // ============================================================================
1941
+ // Default Export
1942
+ // ============================================================================
1943
+
1944
+ export default {
1945
+ // All tools
1946
+ claimsTools,
1947
+
1948
+ // Tool categories
1949
+ coreClaimingTools,
1950
+ workStealingTools,
1951
+ loadBalancingTools,
1952
+ additionalClaimsTools,
1953
+
1954
+ // Individual tools
1955
+ issueClaimTool,
1956
+ issueReleaseTool,
1957
+ issueHandoffTool,
1958
+ issueStatusUpdateTool,
1959
+ issueListAvailableTool,
1960
+ issueListMineTool,
1961
+ issueBoardTool,
1962
+ issueMarkStealableTool,
1963
+ issueStealTool,
1964
+ issueGetStealableTool,
1965
+ issueContestStealTool,
1966
+ agentLoadInfoTool,
1967
+ swarmRebalanceTool,
1968
+ swarmLoadOverviewTool,
1969
+ claimHistoryTool,
1970
+ claimMetricsTool,
1971
+ claimConfigTool,
1972
+
1973
+ // Utility functions
1974
+ registerClaimsTools,
1975
+ getClaimsToolsByCategory,
1976
+ getClaimsToolByName,
1977
+ };