@bitsbound/mcp-server 1.0.5 → 1.0.7

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,715 @@
1
+ ////////////////////////////////////////
2
+ // # GOLDEN RULE!!!! HONOR ABOVE ALL!!!!
3
+ ////////////////////////////////////////
4
+ // NO NEW FILES!!!!!!!!!
5
+ ////////////////////////////////////////
6
+
7
+ /*
8
+ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
9
+ */
10
+ // ## REQUIREMENTS 'R'
11
+ /*
12
+ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
13
+ */
14
+
15
+
16
+ /*
17
+ §§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§
18
+ */
19
+ // ### DEFINITIONS, VALIDATIONS, AND TRANSFORMATIONS 'R_DVT'
20
+ /*
21
+ §§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§
22
+ */
23
+
24
+
25
+ /*
26
+ ######################################################################################################################################################################################################
27
+ */
28
+ // #### GLOBAL 'R_DVT_G' - Imports & Configuration
29
+ /*
30
+ ######################################################################################################################################################################################################
31
+ */
32
+
33
+ import { readFile, writeFile } from 'fs/promises';
34
+ import { basename } from 'path';
35
+
36
+ import type {
37
+ McpServerConfig,
38
+ AnalyzeContractInput,
39
+ AnalyzeContractOutput,
40
+ GetAnalysisStatusInput,
41
+ AnalysisStatusOutput,
42
+ AskSacInput,
43
+ SacResponseOutput,
44
+ GenerateRedlineInput,
45
+ RedlineOutput,
46
+ GenerateNegotiationEmailInput,
47
+ NegotiationEmailOutput,
48
+ ExtractClauseInput,
49
+ ClauseExtractionOutput,
50
+ ComparePlaybookInput,
51
+ PlaybookComparisonOutput,
52
+ BitsBoundApiResponse,
53
+ StartAnalysisResponse,
54
+ AnalysisProgressResponse,
55
+ // Instant Swarm - Parallel Section Redlining
56
+ InstantSwarmInput,
57
+ InstantSwarmOutput,
58
+ // Quick Tools
59
+ QuickScanInput,
60
+ QuickScanOutput,
61
+ AskClauseInput,
62
+ AskClauseOutput,
63
+ CheckDealbreakersInput,
64
+ CheckDealbreakersOutput,
65
+ // File Download
66
+ DownloadFileInput
67
+ } from '../../types/Bitsbound_Kings_McpServer_Backend_Types.js';
68
+ import { DEFAULT_API_URL } from '../../types/Bitsbound_Kings_McpServer_Backend_Types.js';
69
+ import { apiLogger } from '../../logger/Bitsbound_Kings_McpServer_Backend_Logger.js';
70
+
71
+
72
+ /*
73
+ ❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅
74
+ */
75
+ // ##### DEFINITIONS 'R_DVT_G_D' - Backautocrat Class
76
+ /*
77
+ ❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅
78
+ */
79
+
80
+ // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════
81
+ // Backautocrat 'R_DVT_G_D_Backautocrat' - Orchestrates API calls to BitsBound SaaS backend
82
+ // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════
83
+
84
+ export class BitsBoundBackautocrat {
85
+ private readonly apiUrl: string;
86
+ private readonly apiKey: string;
87
+
88
+ constructor(config: McpServerConfig) {
89
+ this.apiUrl = config.BITSBOUND_API_URL || DEFAULT_API_URL;
90
+ this.apiKey = config.BITSBOUND_API_KEY;
91
+
92
+ if (!this.apiKey) {
93
+ throw new Error('BITSBOUND_API_KEY is required');
94
+ }
95
+ }
96
+
97
+ // ────────────────────────────────────────────────────────────────────────────────────────────────────────────────
98
+ // Private: HTTP Request Helper 'R_DVT_G_T_Request'
99
+ // ────────────────────────────────────────────────────────────────────────────────────────────────────────────────
100
+
101
+ private async request<T>(
102
+ method: 'GET' | 'POST',
103
+ endpoint: string,
104
+ body?: Record<string, unknown>
105
+ ): Promise<BitsBoundApiResponse<T>> {
106
+ const url = `${this.apiUrl}${endpoint}`;
107
+
108
+ apiLogger.debug('Making API request', { method, endpoint });
109
+
110
+ try {
111
+ const response = await fetch(url, {
112
+ method,
113
+ headers: {
114
+ 'Content-Type': 'application/json',
115
+ 'Authorization': `Bearer ${this.apiKey}`,
116
+ 'X-MCP-Client': 'bitsbound-mcp-server/1.0.0'
117
+ },
118
+ body: body ? JSON.stringify(body) : undefined
119
+ });
120
+
121
+ if (!response.ok) {
122
+ const errorText = await response.text();
123
+ apiLogger.error('API request failed', {
124
+ status: response.status,
125
+ error: errorText
126
+ });
127
+ return {
128
+ success: false,
129
+ error: `API error ${response.status}: ${errorText}`
130
+ };
131
+ }
132
+
133
+ const data = await response.json() as T;
134
+ apiLogger.debug('API request successful', { endpoint });
135
+
136
+ return { success: true, data };
137
+ } catch (error) {
138
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
139
+ apiLogger.error('API request exception', { error: errorMessage });
140
+ return {
141
+ success: false,
142
+ error: `Request failed: ${errorMessage}`
143
+ };
144
+ }
145
+ }
146
+
147
+
148
+ /*
149
+ ######################################################################################################################################################################################################
150
+ */
151
+ // #### LOCAL 'R_DVT_L' - Tool Handler Methods
152
+ /*
153
+ ######################################################################################################################################################################################################
154
+ */
155
+
156
+ // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════
157
+ // Tool: process_contract 'R_DVT_L_T_ProcessContract'
158
+ // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════
159
+
160
+ async analyzeContract(input: AnalyzeContractInput): Promise<AnalyzeContractOutput> {
161
+ let docxBase64: string;
162
+ let fileName: string;
163
+
164
+ // Handle filePath (RECOMMENDED) or docxBase64
165
+ if (input.filePath) {
166
+ // Read file from disk and base64 encode it
167
+ apiLogger.info('Reading contract from file path', { filePath: input.filePath });
168
+ try {
169
+ const fileBuffer = await readFile(input.filePath);
170
+ docxBase64 = fileBuffer.toString('base64');
171
+ fileName = input.fileName || basename(input.filePath);
172
+ apiLogger.info('File read successfully', { fileName, sizeBytes: fileBuffer.length });
173
+ } catch (error) {
174
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
175
+ apiLogger.error('Failed to read file', { filePath: input.filePath, error: errorMessage });
176
+ throw new Error(`Failed to read file at ${input.filePath}: ${errorMessage}`);
177
+ }
178
+ } else if (input.docxBase64) {
179
+ // Use provided base64
180
+ docxBase64 = input.docxBase64;
181
+ fileName = input.fileName || 'contract.docx';
182
+ } else {
183
+ throw new Error('Either filePath or docxBase64 is required. Use filePath for best results (e.g., "/Users/you/Documents/contract.docx")');
184
+ }
185
+
186
+ apiLogger.info('Starting contract analysis', { fileName });
187
+
188
+ const response = await this.request<StartAnalysisResponse>(
189
+ 'POST',
190
+ '/api/v1/mcp/analyze',
191
+ {
192
+ contract_content: docxBase64,
193
+ filename: fileName,
194
+ analysis_type: input.analysisDepth || 'standard',
195
+ perspective: input.perspective || 'customer'
196
+ }
197
+ );
198
+
199
+ if (!response.success || !response.data) {
200
+ throw new Error(response.error || 'Failed to start analysis');
201
+ }
202
+
203
+ return {
204
+ analysisId: response.data.analysisId,
205
+ status: 'queued',
206
+ estimatedTimeMinutes: response.data.estimatedMinutes,
207
+ initialRiskSummary: 'Analysis queued. Use get_analysis_status to check progress.'
208
+ };
209
+ }
210
+
211
+ // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════
212
+ // Tool: get_analysis_status 'R_DVT_L_T_GetStatus'
213
+ // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════
214
+
215
+ async getAnalysisStatus(input: GetAnalysisStatusInput): Promise<AnalysisStatusOutput> {
216
+ apiLogger.info('Checking analysis status', { analysisId: input.analysisId });
217
+
218
+ const response = await this.request<AnalysisProgressResponse>(
219
+ 'GET',
220
+ `/api/v1/mcp/analysis/${input.analysisId}/status`
221
+ );
222
+
223
+ if (!response.success || !response.data) {
224
+ throw new Error(response.error || 'Failed to get analysis status');
225
+ }
226
+
227
+ const data = response.data;
228
+ return {
229
+ analysisId: data.analysisId,
230
+ status: data.status as AnalysisStatusOutput['status'],
231
+ progressPercent: data.progress,
232
+ currentPhase: data.phase,
233
+ phasesCompleted: data.phasesCompleted,
234
+ favorabilityScore: data.results?.favorabilityScore,
235
+ topRisks: data.results?.topRisks.map((r: { severity: string; section: string; risk: string }) => `[${r.severity.toUpperCase()}] ${r.section}: ${r.risk}`)
236
+ };
237
+ }
238
+
239
+ // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════
240
+ // Tool: ask_sac 'R_DVT_L_T_AskSac'
241
+ // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════
242
+
243
+ async askSac(input: AskSacInput): Promise<SacResponseOutput> {
244
+ apiLogger.info('Asking SAC', { analysisId: input.analysisId });
245
+
246
+ const response = await this.request<{
247
+ response: string;
248
+ citations?: Array<{ section: string; text: string; page?: number }>;
249
+ suggestions?: string[];
250
+ }>(
251
+ 'POST',
252
+ '/api/v1/mcp/sac/chat',
253
+ {
254
+ analysisId: input.analysisId,
255
+ question: input.question,
256
+ includeClauseCitations: input.includeClauseCitations ?? true
257
+ }
258
+ );
259
+
260
+ if (!response.success || !response.data) {
261
+ throw new Error(response.error || 'Failed to get SAC response');
262
+ }
263
+
264
+ return {
265
+ response: response.data.response,
266
+ clauseCitations: response.data.citations?.map((c: { section: string; text: string; page?: number }) => ({
267
+ sectionName: c.section,
268
+ clauseText: c.text,
269
+ pageNumber: c.page
270
+ })),
271
+ followUpSuggestions: response.data.suggestions
272
+ };
273
+ }
274
+
275
+ // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════
276
+ // Tool: generate_redline 'R_DVT_L_T_GenerateRedline'
277
+ // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════
278
+
279
+ async generateRedline(input: GenerateRedlineInput): Promise<RedlineOutput> {
280
+ apiLogger.info('Generating redline', { analysisId: input.analysisId });
281
+
282
+ const response = await this.request<{
283
+ docxBase64: string;
284
+ downloadUrl: string;
285
+ changesCount: number;
286
+ summary: string;
287
+ }>(
288
+ 'POST',
289
+ '/api/v1/mcp/redline/generate',
290
+ {
291
+ analysisId: input.analysisId,
292
+ aggressiveness: input.aggressiveness ?? 3,
293
+ includeComments: input.includeComments ?? true
294
+ }
295
+ );
296
+
297
+ if (!response.success || !response.data) {
298
+ throw new Error(response.error || 'Failed to generate redline');
299
+ }
300
+
301
+ return {
302
+ redlinedDocxBase64: response.data.docxBase64,
303
+ downloadUrl: response.data.downloadUrl,
304
+ changesCount: response.data.changesCount,
305
+ changesSummary: response.data.summary
306
+ };
307
+ }
308
+
309
+ // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════
310
+ // Tool: generate_negotiation_email 'R_DVT_L_T_GenerateEmail'
311
+ // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════
312
+
313
+ async generateNegotiationEmail(input: GenerateNegotiationEmailInput): Promise<NegotiationEmailOutput> {
314
+ apiLogger.info('Generating negotiation email', { analysisId: input.analysisId });
315
+
316
+ const response = await this.request<{
317
+ subject: string;
318
+ body: string;
319
+ keyPoints: string[];
320
+ }>(
321
+ 'POST',
322
+ '/api/v1/mcp/email/generate',
323
+ {
324
+ analysisId: input.analysisId,
325
+ tone: input.tone ?? 'collaborative',
326
+ recipientRole: input.recipientRole
327
+ }
328
+ );
329
+
330
+ if (!response.success || !response.data) {
331
+ throw new Error(response.error || 'Failed to generate email');
332
+ }
333
+
334
+ return response.data;
335
+ }
336
+
337
+ // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════
338
+ // Tool: extract_clause 'R_DVT_L_T_ExtractClause'
339
+ // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════
340
+
341
+ async extractClause(input: ExtractClauseInput): Promise<ClauseExtractionOutput> {
342
+ apiLogger.info('Extracting clause', {
343
+ analysisId: input.analysisId,
344
+ clauseType: input.clauseType
345
+ });
346
+
347
+ const response = await this.request<{
348
+ clauseType: string;
349
+ clauseText: string;
350
+ riskLevel: 'low' | 'medium' | 'high' | 'critical';
351
+ riskAnalysis: string;
352
+ suggestedImprovements: string[];
353
+ marketComparison?: string;
354
+ }>(
355
+ 'GET',
356
+ `/api/v1/mcp/analysis/${input.analysisId}/clause/${input.clauseType}`
357
+ );
358
+
359
+ if (!response.success || !response.data) {
360
+ throw new Error(response.error || 'Failed to extract clause');
361
+ }
362
+
363
+ return response.data;
364
+ }
365
+
366
+ // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════
367
+ // Tool: compare_playbook 'R_DVT_L_T_ComparePlaybook'
368
+ // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════
369
+
370
+ async comparePlaybook(input: ComparePlaybookInput): Promise<PlaybookComparisonOutput> {
371
+ apiLogger.info('Comparing against playbook', {
372
+ analysisId: input.analysisId,
373
+ playbookId: input.playbookId
374
+ });
375
+
376
+ const response = await this.request<{
377
+ deviations: Array<{
378
+ section: string;
379
+ playbookRequirement: string;
380
+ actualClause: string;
381
+ severity: 'minor' | 'moderate' | 'major';
382
+ recommendation: string;
383
+ }>;
384
+ complianceScore: number;
385
+ requiredApprovals: string[];
386
+ }>(
387
+ 'POST',
388
+ '/api/v1/mcp/playbook/compare',
389
+ {
390
+ analysisId: input.analysisId,
391
+ playbookId: input.playbookId
392
+ }
393
+ );
394
+
395
+ if (!response.success || !response.data) {
396
+ throw new Error(response.error || 'Failed to compare playbook');
397
+ }
398
+
399
+ return response.data;
400
+ }
401
+
402
+ // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════
403
+ // Resource: Get Analysis Results 'R_DVT_L_T_GetAnalysis'
404
+ // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════
405
+
406
+ async getAnalysisResults(analysisId: string): Promise<Record<string, unknown>> {
407
+ apiLogger.info('Fetching full analysis results', { analysisId });
408
+
409
+ const response = await this.request<Record<string, unknown>>(
410
+ 'GET',
411
+ `/api/v1/mcp/analysis/${analysisId}/full`
412
+ );
413
+
414
+ if (!response.success || !response.data) {
415
+ throw new Error(response.error || 'Failed to fetch analysis');
416
+ }
417
+
418
+ return response.data;
419
+ }
420
+
421
+ // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════
422
+ // Resource: Get Playbook 'R_DVT_L_T_GetPlaybook'
423
+ // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════
424
+
425
+ async getPlaybook(playbookId: string): Promise<Record<string, unknown>> {
426
+ apiLogger.info('Fetching playbook', { playbookId });
427
+
428
+ const response = await this.request<Record<string, unknown>>(
429
+ 'GET',
430
+ `/api/v1/mcp/playbook/${playbookId}`
431
+ );
432
+
433
+ if (!response.success || !response.data) {
434
+ throw new Error(response.error || 'Failed to fetch playbook');
435
+ }
436
+
437
+ return response.data;
438
+ }
439
+
440
+
441
+ /*
442
+ ######################################################################################################################################################################################################
443
+ */
444
+ // #### INSTANT SWARM 'R_DVT_L_InstantSwarm' - Parallel Section Redlining
445
+ /*
446
+ ######################################################################################################################################################################################################
447
+ */
448
+
449
+ // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════
450
+ // Tool: instant_swarm 'R_DVT_L_T_InstantSwarm' - Parallel redlining across ALL sections simultaneously
451
+ // Spawns N agents for N sections (dynamic - not fixed) for maximum parallelization
452
+ // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════
453
+
454
+ async instantSwarm(input: InstantSwarmInput): Promise<InstantSwarmOutput> {
455
+ apiLogger.info('Starting Instant Swarm - parallel section redlining', {
456
+ analysisId: input.analysisId,
457
+ aggressivenessLevel: input.aggressivenessLevel,
458
+ partyPosition: input.partyPosition,
459
+ targetSections: input.targetSections || 'all'
460
+ });
461
+
462
+ // T: Call the existing /api/v1/sac/autopilot/swarm endpoint on the SaaS backend
463
+ // This endpoint spawns N parallel SAC sessions for N sections, each writing to isolated R2 branches
464
+ const response = await this.request<{
465
+ success: boolean;
466
+ totalSections: number;
467
+ completedSections: number;
468
+ failedSections: number;
469
+ mergedDocumentPath?: string;
470
+ mergedDocumentBase64?: string;
471
+ sectionResults: Array<{
472
+ sectionId: string;
473
+ sectionName: string;
474
+ success: boolean;
475
+ redlinesApplied: number;
476
+ commentsApplied: number;
477
+ keyChanges?: string[];
478
+ error?: string;
479
+ executionTimeMs: number;
480
+ }>;
481
+ totalRedlinesApplied: number;
482
+ totalCommentsApplied: number;
483
+ executionTimeMs: number;
484
+ tokenUsage?: {
485
+ inputTokens: number;
486
+ outputTokens: number;
487
+ };
488
+ }>(
489
+ 'POST',
490
+ '/api/v1/mcp/instant/swarm', // MCP-specific endpoint that wraps /sac/autopilot/swarm
491
+ {
492
+ analysisId: input.analysisId,
493
+ aggressivenessLevel: input.aggressivenessLevel,
494
+ partyPosition: input.partyPosition,
495
+ ourPartyName: input.ourPartyName,
496
+ counterpartyName: input.counterpartyName,
497
+ targetSections: input.targetSections
498
+ }
499
+ );
500
+
501
+ if (!response.success || !response.data) {
502
+ throw new Error(response.error || 'Failed to execute Instant Swarm');
503
+ }
504
+
505
+ const data = response.data;
506
+
507
+ return {
508
+ success: data.success,
509
+ totalSections: data.totalSections,
510
+ completedSections: data.completedSections,
511
+ failedSections: data.failedSections,
512
+ redlinedDocumentBase64: data.mergedDocumentBase64,
513
+ redlinedDocumentUrl: data.mergedDocumentPath,
514
+ sectionResults: data.sectionResults.map(sr => ({
515
+ sectionId: sr.sectionId,
516
+ sectionName: sr.sectionName,
517
+ success: sr.success,
518
+ redlinesApplied: sr.redlinesApplied,
519
+ commentsApplied: sr.commentsApplied,
520
+ keyChanges: sr.keyChanges,
521
+ error: sr.error,
522
+ executionTimeMs: sr.executionTimeMs
523
+ })),
524
+ totalRedlinesApplied: data.totalRedlinesApplied,
525
+ totalCommentsApplied: data.totalCommentsApplied,
526
+ executionTimeMs: data.executionTimeMs,
527
+ tokenUsage: data.tokenUsage
528
+ };
529
+ }
530
+
531
+
532
+ /*
533
+ ######################################################################################################################################################################################################
534
+ */
535
+ // #### QUICK TOOLS 'R_DVT_L_QuickTools' - Immediate Analysis
536
+ /*
537
+ ######################################################################################################################################################################################################
538
+ */
539
+
540
+ // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════
541
+ // Tool: quick_scan 'R_DVT_L_T_QuickScan' - Instant contract analysis (5-10 seconds)
542
+ // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════
543
+
544
+ async quickScan(input: QuickScanInput): Promise<QuickScanOutput> {
545
+ apiLogger.info('Starting quick scan', {
546
+ fileName: input.fileName,
547
+ perspective: input.perspective || 'customer'
548
+ });
549
+
550
+ const response = await this.request<QuickScanOutput>(
551
+ 'POST',
552
+ '/api/v1/mcp/instant/quick-scan',
553
+ {
554
+ contractText: input.contractText,
555
+ fileName: input.fileName,
556
+ perspective: input.perspective || 'customer'
557
+ }
558
+ );
559
+
560
+ if (!response.success || !response.data) {
561
+ throw new Error(response.error || 'Failed to perform quick scan');
562
+ }
563
+
564
+ return response.data;
565
+ }
566
+
567
+ // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════
568
+ // Tool: ask_clause 'R_DVT_L_T_AskClause' - Instant clause Q&A (2-5 seconds)
569
+ // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════
570
+
571
+ async askClause(input: AskClauseInput): Promise<AskClauseOutput> {
572
+ apiLogger.info('Asking about clause', {
573
+ question: input.question.substring(0, 50) + '...',
574
+ clauseType: input.clauseType || 'any'
575
+ });
576
+
577
+ const response = await this.request<AskClauseOutput>(
578
+ 'POST',
579
+ '/api/v1/mcp/instant/ask-clause',
580
+ {
581
+ contractText: input.contractText,
582
+ question: input.question,
583
+ clauseType: input.clauseType || 'any'
584
+ }
585
+ );
586
+
587
+ if (!response.success || !response.data) {
588
+ throw new Error(response.error || 'Failed to get clause answer');
589
+ }
590
+
591
+ return response.data;
592
+ }
593
+
594
+ // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════
595
+ // Tool: check_dealbreakers 'R_DVT_L_T_CheckDealbreakers' - Instant playbook compliance (3-5 seconds)
596
+ // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════
597
+
598
+ async checkDealbreakers(input: CheckDealbreakersInput): Promise<CheckDealbreakersOutput> {
599
+ apiLogger.info('Checking dealbreakers', {
600
+ playbookId: input.playbookId || 'default'
601
+ });
602
+
603
+ const response = await this.request<CheckDealbreakersOutput>(
604
+ 'POST',
605
+ '/api/v1/mcp/instant/check-dealbreakers',
606
+ {
607
+ contractText: input.contractText,
608
+ playbookId: input.playbookId
609
+ }
610
+ );
611
+
612
+ if (!response.success || !response.data) {
613
+ throw new Error(response.error || 'Failed to check dealbreakers');
614
+ }
615
+
616
+ return response.data;
617
+ }
618
+
619
+ // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════
620
+ // Download File 'R_DVT_L_T_DownloadFile' - Download file from R2 via MCP endpoint
621
+ // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════
622
+
623
+ /**
624
+ * T_DOWNLOAD_FILE: Download a file from BitsBound R2 storage
625
+ *
626
+ * TIR{ADVT}O Context:
627
+ * - T: User requests to download a file (e.g., redlined DOCX)
628
+ * - I: downloadUrl from get_analysis_status deliverables
629
+ * - R{ADVT}:
630
+ * - D: MCP download endpoint URL pattern
631
+ * - V: URL must be a valid BitsBound download URL
632
+ * - T: Fetch file via HTTP, optionally save to disk
633
+ * - O: { success, base64Content, savedToPath?, fileName, fileSizeBytes }
634
+ */
635
+ async downloadFile(input: DownloadFileInput): Promise<{
636
+ success: boolean;
637
+ base64Content?: string;
638
+ savedToPath?: string;
639
+ fileName?: string;
640
+ fileSizeBytes?: number;
641
+ error?: string;
642
+ }> {
643
+ const { downloadUrl, saveToPath } = input;
644
+
645
+ apiLogger.info('Downloading file', { downloadUrl, saveToPath });
646
+
647
+ try {
648
+ // The download URL is a full URL - just fetch it with auth header
649
+ const response = await fetch(downloadUrl, {
650
+ method: 'GET',
651
+ headers: {
652
+ 'X-API-Key': this.apiKey
653
+ }
654
+ });
655
+
656
+ if (!response.ok) {
657
+ const errorText = await response.text();
658
+ throw new Error(`Download failed: ${response.status} ${response.statusText} - ${errorText}`);
659
+ }
660
+
661
+ // Get the file as ArrayBuffer
662
+ const arrayBuffer = await response.arrayBuffer();
663
+ const buffer = Buffer.from(arrayBuffer);
664
+
665
+ // Extract filename from Content-Disposition header or URL
666
+ const contentDisposition = response.headers.get('Content-Disposition');
667
+ let fileName = 'document.docx';
668
+ if (contentDisposition) {
669
+ const match = contentDisposition.match(/filename="?([^"]+)"?/);
670
+ if (match) {
671
+ fileName = match[1];
672
+ }
673
+ } else {
674
+ // Try to extract from URL
675
+ const urlParts = downloadUrl.split('/');
676
+ const lastPart = urlParts[urlParts.length - 1];
677
+ if (lastPart && !lastPart.includes('?')) {
678
+ fileName = decodeURIComponent(lastPart);
679
+ }
680
+ }
681
+
682
+ // If saveToPath is provided, write to disk
683
+ if (saveToPath) {
684
+ await writeFile(saveToPath, buffer);
685
+ apiLogger.info('File saved to disk', { path: saveToPath, size: buffer.length });
686
+
687
+ return {
688
+ success: true,
689
+ savedToPath: saveToPath,
690
+ fileName,
691
+ fileSizeBytes: buffer.length
692
+ };
693
+ }
694
+
695
+ // Otherwise, return as base64
696
+ const base64Content = buffer.toString('base64');
697
+ apiLogger.info('File downloaded as base64', { fileName, size: buffer.length });
698
+
699
+ return {
700
+ success: true,
701
+ base64Content,
702
+ fileName,
703
+ fileSizeBytes: buffer.length
704
+ };
705
+ } catch (error) {
706
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
707
+ apiLogger.error('Download failed', { error: errorMessage });
708
+
709
+ return {
710
+ success: false,
711
+ error: errorMessage
712
+ };
713
+ }
714
+ }
715
+ }