@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,1441 @@
1
+ #!/usr/bin/env node
2
+
3
+ ////////////////////////////////////////
4
+ // # GOLDEN RULE!!!! HONOR ABOVE ALL!!!!
5
+ ////////////////////////////////////////
6
+ // NO NEW FILES!!!!!!!!!
7
+ ////////////////////////////////////////
8
+
9
+ /*
10
+ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
11
+ */
12
+ // ## REQUIREMENTS 'R'
13
+ /*
14
+ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
15
+ */
16
+
17
+
18
+ /*
19
+ §§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§
20
+ */
21
+ // ### DEFINITIONS, VALIDATIONS, AND TRANSFORMATIONS 'R_DVT'
22
+ /*
23
+ §§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§
24
+ */
25
+
26
+
27
+ /*
28
+ ######################################################################################################################################################################################################
29
+ */
30
+ // #### GLOBAL 'R_DVT_G' - Imports & Configuration
31
+ /*
32
+ ######################################################################################################################################################################################################
33
+ */
34
+
35
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
36
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
37
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
38
+ import {
39
+ CallToolRequestSchema,
40
+ ListToolsRequestSchema,
41
+ ListResourcesRequestSchema,
42
+ ReadResourceRequestSchema,
43
+ ListPromptsRequestSchema,
44
+ GetPromptRequestSchema,
45
+ ErrorCode,
46
+ McpError
47
+ } from '@modelcontextprotocol/sdk/types.js';
48
+
49
+ import { createServer, type IncomingMessage, type ServerResponse } from 'node:http';
50
+ import { randomUUID, createHmac, createHash, timingSafeEqual } from 'node:crypto';
51
+ import { Buffer } from 'node:buffer';
52
+ import { MongoClient, type Db } from 'mongodb';
53
+
54
+ // T: Import serverLogger early for MongoDB connection logging
55
+ import { serverLogger } from '../logger/Bitsbound_Kings_McpServer_Backend_Logger.js';
56
+
57
+ // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════
58
+ // MongoDB Connection 'R_DVT_G_D_MongoDB' - Per-customer API key validation
59
+ // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════
60
+
61
+ // D: MongoDB connection singleton
62
+ let mongoClient: MongoClient | null = null;
63
+ let mongoDb: Db | null = null;
64
+
65
+ // T: Get or create MongoDB connection
66
+ async function getMongoDb(): Promise<Db | null> {
67
+ if (mongoDb) return mongoDb;
68
+
69
+ const uri = process.env.MONGODB_URI;
70
+ if (!uri) {
71
+ serverLogger.warn('MONGODB_URI not configured - per-customer API key validation disabled');
72
+ return null;
73
+ }
74
+
75
+ try {
76
+ mongoClient = new MongoClient(uri);
77
+ await mongoClient.connect();
78
+ mongoDb = mongoClient.db('bitsbound_admin');
79
+ serverLogger.info('MongoDB connected for API key validation');
80
+ return mongoDb;
81
+ } catch (error) {
82
+ serverLogger.error('MongoDB connection failed', { error: String(error) });
83
+ return null;
84
+ }
85
+ }
86
+
87
+ // T: Validate customer API key against MongoDB 'bitsbound_admin.api_keys' collection
88
+ // Keys are stored with SHA-256 hash for security
89
+ async function validateApiKeyFromMongoDB(apiKey: string): Promise<{ valid: boolean; customerId?: string }> {
90
+ const db = await getMongoDb();
91
+ if (!db) {
92
+ // Fallback: accept any key if MongoDB not configured (for backward compatibility)
93
+ return { valid: true };
94
+ }
95
+
96
+ // T: Hash the provided API key to match stored format
97
+ const keyHash = createHash('sha256').update(apiKey).digest('hex');
98
+
99
+ try {
100
+ const apiKeyDoc = await db.collection('api_keys').findOne({
101
+ keyHash,
102
+ status: 'active'
103
+ });
104
+
105
+ if (apiKeyDoc) {
106
+ serverLogger.info('API key validated via MongoDB', {
107
+ customerId: apiKeyDoc.customerId,
108
+ keyPrefix: apiKey.substring(0, 10) + '...'
109
+ });
110
+ return { valid: true, customerId: apiKeyDoc.customerId };
111
+ }
112
+
113
+ serverLogger.warn('API key not found or inactive', { keyPrefix: apiKey.substring(0, 10) + '...' });
114
+ return { valid: false };
115
+ } catch (error) {
116
+ serverLogger.error('MongoDB API key lookup failed', { error: String(error) });
117
+ // Fallback on error - don't break existing customers
118
+ return { valid: true };
119
+ }
120
+ }
121
+
122
+ // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════
123
+ // OAuth 2.1 Configuration 'R_DVT_G_D_OAuth' - Premium authentication for claude.ai Custom Connectors
124
+ // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════
125
+
126
+ // D: OAuth 2.1 constants
127
+ const OAUTH_ISSUER = process.env.OAUTH_ISSUER || 'https://bitsbound-mcp-server.onrender.com';
128
+ const JWT_SECRET = process.env.JWT_SECRET || process.env.BITSBOUND_API_KEY || 'bitsbound-mcp-secret';
129
+ const TOKEN_EXPIRY_SECONDS = 3600; // 1 hour
130
+
131
+ // D: OAuth client registry (client_id -> client_secret mapping)
132
+ // In production, client_secret should be the BitsBound API key
133
+ interface OAuthClient {
134
+ clientSecret: string; // The BitsBound API key
135
+ name: string;
136
+ scopes: string[];
137
+ }
138
+
139
+ // D: Registered OAuth clients - production clients use their API key as client_secret
140
+ const oauthClients: Map<string, OAuthClient> = new Map([
141
+ // Default client for BitsBound - client_secret = API key
142
+ ['bitsbound-mcp', {
143
+ clientSecret: process.env.BITSBOUND_API_KEY || '',
144
+ name: 'BitsBound MCP Client',
145
+ scopes: ['mcp:read', 'mcp:write', 'contract:analyze']
146
+ }]
147
+ ]);
148
+
149
+ // D: JWT Payload structure
150
+ interface JWTPayload {
151
+ iss: string; // Issuer
152
+ sub: string; // Subject (client_id)
153
+ aud: string; // Audience
154
+ exp: number; // Expiration timestamp
155
+ iat: number; // Issued at timestamp
156
+ jti: string; // JWT ID (unique identifier)
157
+ scope: string; // OAuth scopes
158
+ bitsbound_api_key?: string; // Customer's BitsBound API key (encrypted in token)
159
+ }
160
+
161
+ // D: Active tokens for revocation support (jti -> expiry timestamp)
162
+ const activeTokens: Map<string, number> = new Map();
163
+
164
+ // D: Authorization codes for OAuth 2.1 Authorization Code flow with PKCE
165
+ interface AuthorizationCode {
166
+ clientId: string;
167
+ redirectUri: string;
168
+ codeChallenge: string;
169
+ codeChallengeMethod: string;
170
+ scopes: string[];
171
+ expiresAt: number;
172
+ }
173
+
174
+ // D: Authorization code registry (code -> AuthorizationCode)
175
+ const authorizationCodes: Map<string, AuthorizationCode> = new Map();
176
+
177
+ // D: Authorization code expiry (10 minutes - per OAuth 2.1 spec)
178
+ const AUTH_CODE_EXPIRY_SECONDS = 600;
179
+
180
+ // T: Base64URL encode (RFC 7515)
181
+ function base64UrlEncode(data: string | Buffer): string {
182
+ const buffer = typeof data === 'string' ? Buffer.from(data) : data;
183
+ return buffer.toString('base64')
184
+ .replace(/\+/g, '-')
185
+ .replace(/\//g, '_')
186
+ .replace(/=+$/, '');
187
+ }
188
+
189
+ // T: Base64URL decode (RFC 7515)
190
+ function base64UrlDecode(data: string): string {
191
+ const padded = data + '='.repeat((4 - data.length % 4) % 4);
192
+ return Buffer.from(padded.replace(/-/g, '+').replace(/_/g, '/'), 'base64').toString();
193
+ }
194
+
195
+ // T: Generate HMAC-SHA256 signature for JWT
196
+ function signJwt(header: string, payload: string): string {
197
+ const data = `${header}.${payload}`;
198
+ const signature = createHmac('sha256', JWT_SECRET)
199
+ .update(data)
200
+ .digest();
201
+ return base64UrlEncode(signature);
202
+ }
203
+
204
+ // T: Generate a JWT access token
205
+ function generateAccessToken(clientId: string, scopes: string[], bitsboundApiKey?: string): string {
206
+ const now = Math.floor(Date.now() / 1000);
207
+ const jti = randomUUID();
208
+
209
+ const header = {
210
+ alg: 'HS256',
211
+ typ: 'JWT'
212
+ };
213
+
214
+ const payload: JWTPayload = {
215
+ iss: OAUTH_ISSUER,
216
+ sub: clientId,
217
+ aud: `${OAUTH_ISSUER}/mcp`,
218
+ exp: now + TOKEN_EXPIRY_SECONDS,
219
+ iat: now,
220
+ jti,
221
+ scope: scopes.join(' '),
222
+ // T: Include customer's BitsBound API key in token (JWT is signed, so it's tamper-proof)
223
+ ...(bitsboundApiKey && { bitsbound_api_key: bitsboundApiKey })
224
+ };
225
+
226
+ const encodedHeader = base64UrlEncode(JSON.stringify(header));
227
+ const encodedPayload = base64UrlEncode(JSON.stringify(payload));
228
+ const signature = signJwt(encodedHeader, encodedPayload);
229
+
230
+ // T: Register token for revocation support
231
+ activeTokens.set(jti, payload.exp);
232
+
233
+ // T: Cleanup expired tokens periodically
234
+ cleanupExpiredTokens();
235
+
236
+ return `${encodedHeader}.${encodedPayload}.${signature}`;
237
+ }
238
+
239
+ // T: Verify JWT and return payload (or null if invalid)
240
+ function verifyAccessToken(token: string): JWTPayload | null {
241
+ try {
242
+ const parts = token.split('.');
243
+ if (parts.length !== 3) return null;
244
+
245
+ const [encodedHeader, encodedPayload, providedSignature] = parts;
246
+
247
+ // V: Verify signature
248
+ const expectedSignature = signJwt(encodedHeader, encodedPayload);
249
+ const providedBuffer = Buffer.from(providedSignature);
250
+ const expectedBuffer = Buffer.from(expectedSignature);
251
+
252
+ if (providedBuffer.length !== expectedBuffer.length) return null;
253
+ if (!timingSafeEqual(providedBuffer, expectedBuffer)) return null;
254
+
255
+ // V: Parse and validate payload
256
+ const payload: JWTPayload = JSON.parse(base64UrlDecode(encodedPayload));
257
+
258
+ // V: Check expiration
259
+ const now = Math.floor(Date.now() / 1000);
260
+ if (payload.exp <= now) {
261
+ activeTokens.delete(payload.jti);
262
+ return null;
263
+ }
264
+
265
+ // V: Check if token was revoked
266
+ if (!activeTokens.has(payload.jti)) {
267
+ return null;
268
+ }
269
+
270
+ // V: Verify issuer
271
+ if (payload.iss !== OAUTH_ISSUER) return null;
272
+
273
+ return payload;
274
+ } catch {
275
+ return null;
276
+ }
277
+ }
278
+
279
+ // T: Clean up expired tokens from registry
280
+ function cleanupExpiredTokens(): void {
281
+ const now = Math.floor(Date.now() / 1000);
282
+ for (const [jti, exp] of activeTokens) {
283
+ if (exp <= now) {
284
+ activeTokens.delete(jti);
285
+ }
286
+ }
287
+ }
288
+
289
+ // T: Validate client credentials (client_id + client_secret)
290
+ // Per-Customer OAuth: client_secret IS the customer's BitsBound API key (sk_live_xxx)
291
+ // Validates against MongoDB bitsbound_admin.api_keys collection
292
+ async function validateClientCredentials(clientId: string, clientSecret: string): Promise<OAuthClient | null> {
293
+ // V: For 'bitsbound-mcp' client, the client_secret is the customer's API key
294
+ if (clientId === 'bitsbound-mcp') {
295
+ // T: Validate customer's API key against MongoDB
296
+ if (clientSecret && clientSecret.startsWith('sk_live_')) {
297
+ const validation = await validateApiKeyFromMongoDB(clientSecret);
298
+ if (validation.valid) {
299
+ serverLogger.info('Per-customer OAuth: API key validated', {
300
+ customerId: validation.customerId,
301
+ keyPrefix: clientSecret.substring(0, 10) + '...'
302
+ });
303
+ return {
304
+ clientSecret,
305
+ name: `BitsBound Customer ${validation.customerId || 'unknown'}`,
306
+ scopes: ['mcp:read', 'mcp:write', 'contract:analyze']
307
+ };
308
+ }
309
+ // Key not valid in MongoDB
310
+ serverLogger.warn('Per-customer OAuth: API key rejected', {
311
+ keyPrefix: clientSecret.substring(0, 10) + '...'
312
+ });
313
+ return null;
314
+ }
315
+ }
316
+
317
+ // T: Fallback to static client registry
318
+ const client = oauthClients.get(clientId);
319
+ if (!client) return null;
320
+
321
+ // V: Timing-safe comparison for client_secret
322
+ const providedBuffer = Buffer.from(clientSecret);
323
+ const expectedBuffer = Buffer.from(client.clientSecret);
324
+
325
+ // V: Handle empty secrets (env var not set)
326
+ if (expectedBuffer.length === 0) {
327
+ // Fall back to using provided secret as API key (for flexibility)
328
+ return { ...client, clientSecret };
329
+ }
330
+
331
+ if (providedBuffer.length !== expectedBuffer.length) return null;
332
+ if (!timingSafeEqual(providedBuffer, expectedBuffer)) return null;
333
+
334
+ return client;
335
+ }
336
+
337
+ // T: Verify PKCE code_verifier against stored code_challenge (RFC 7636)
338
+ function verifyPkce(codeVerifier: string, codeChallenge: string, method: string): boolean {
339
+ if (method === 'plain') {
340
+ // Plain method: code_challenge === code_verifier
341
+ return codeVerifier === codeChallenge;
342
+ } else if (method === 'S256') {
343
+ // S256 method: code_challenge === BASE64URL(SHA256(code_verifier))
344
+ const sha256 = createHash('sha256').update(codeVerifier).digest();
345
+ const computedChallenge = base64UrlEncode(sha256);
346
+ return computedChallenge === codeChallenge;
347
+ }
348
+ return false;
349
+ }
350
+
351
+ // T: Generate authorization code
352
+ function generateAuthorizationCode(
353
+ clientId: string,
354
+ redirectUri: string,
355
+ codeChallenge: string,
356
+ codeChallengeMethod: string,
357
+ scopes: string[]
358
+ ): string {
359
+ const code = randomUUID();
360
+ const expiresAt = Math.floor(Date.now() / 1000) + AUTH_CODE_EXPIRY_SECONDS;
361
+
362
+ authorizationCodes.set(code, {
363
+ clientId,
364
+ redirectUri,
365
+ codeChallenge,
366
+ codeChallengeMethod,
367
+ scopes,
368
+ expiresAt
369
+ });
370
+
371
+ // T: Cleanup expired codes
372
+ cleanupExpiredAuthCodes();
373
+
374
+ return code;
375
+ }
376
+
377
+ // T: Validate and consume authorization code
378
+ function consumeAuthorizationCode(
379
+ code: string,
380
+ clientId: string,
381
+ redirectUri: string,
382
+ codeVerifier: string
383
+ ): { scopes: string[] } | null {
384
+ const authCode = authorizationCodes.get(code);
385
+ if (!authCode) return null;
386
+
387
+ // V: Check expiration
388
+ const now = Math.floor(Date.now() / 1000);
389
+ if (authCode.expiresAt <= now) {
390
+ authorizationCodes.delete(code);
391
+ return null;
392
+ }
393
+
394
+ // V: Verify client_id matches
395
+ if (authCode.clientId !== clientId) return null;
396
+
397
+ // V: Verify redirect_uri matches
398
+ if (authCode.redirectUri !== redirectUri) return null;
399
+
400
+ // V: Verify PKCE code_verifier
401
+ if (!verifyPkce(codeVerifier, authCode.codeChallenge, authCode.codeChallengeMethod)) {
402
+ return null;
403
+ }
404
+
405
+ // T: Consume the code (one-time use)
406
+ authorizationCodes.delete(code);
407
+
408
+ return { scopes: authCode.scopes };
409
+ }
410
+
411
+ // T: Clean up expired authorization codes
412
+ function cleanupExpiredAuthCodes(): void {
413
+ const now = Math.floor(Date.now() / 1000);
414
+ for (const [code, authCode] of authorizationCodes) {
415
+ if (authCode.expiresAt <= now) {
416
+ authorizationCodes.delete(code);
417
+ }
418
+ }
419
+ }
420
+
421
+ import type {
422
+ McpServerConfig,
423
+ AnalyzeContractInput,
424
+ GetAnalysisStatusInput,
425
+ AskSacInput,
426
+ GenerateRedlineInput,
427
+ GenerateNegotiationEmailInput,
428
+ ExtractClauseInput,
429
+ ComparePlaybookInput,
430
+ // Instant Swarm - Parallel Section Redlining
431
+ InstantSwarmInput,
432
+ // Quick Tools
433
+ QuickScanInput,
434
+ AskClauseInput,
435
+ CheckDealbreakersInput,
436
+ // File Download
437
+ DownloadFileInput,
438
+ ToolName,
439
+ PromptName
440
+ } from '../types/Bitsbound_Kings_McpServer_Backend_Types.js';
441
+ import { TOOL_DEFINITIONS, RESOURCE_DEFINITIONS, PROMPT_DEFINITIONS, PROMPT_MESSAGES, DEFAULT_API_URL } from '../types/Bitsbound_Kings_McpServer_Backend_Types.js';
442
+ import { BitsBoundBackautocrat } from '../orchestration/backautocrat/Bitsbound_Kings_McpServer_Backend_Orchestration_Backautocrat.js';
443
+ // Note: serverLogger imported at top for MongoDB connection logging
444
+
445
+
446
+ /*
447
+ ❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅
448
+ */
449
+ // ##### DEFINITIONS 'R_DVT_G_D' - MCP Server Class
450
+ /*
451
+ ❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅❅
452
+ */
453
+
454
+ // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════
455
+ // BitsBound MCP Server 'R_DVT_G_D_McpServer'
456
+ // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════
457
+
458
+ class BitsBoundMcpServer {
459
+ private readonly server: Server;
460
+ private readonly backautocrat: BitsBoundBackautocrat | null;
461
+
462
+ constructor(config: McpServerConfig) {
463
+ // T: Create backautocrat only if API key is available (graceful degradation)
464
+ if (config.BITSBOUND_API_KEY) {
465
+ this.backautocrat = new BitsBoundBackautocrat(config);
466
+ } else {
467
+ serverLogger.warn('BITSBOUND_API_KEY not configured - tools will be unavailable');
468
+ this.backautocrat = null;
469
+ }
470
+
471
+ this.server = new Server(
472
+ {
473
+ name: 'BitsBound',
474
+ version: '1.0.0',
475
+ // D: Server branding for claude.ai Custom Connectors
476
+ description: 'AI-powered contract analysis - partner-level redlines with Track Changes',
477
+ websiteUrl: 'https://bitsbound.com',
478
+ icons: [
479
+ {
480
+ src: 'https://www.bitsbound.com/favicon.svg',
481
+ mimeType: 'image/svg+xml'
482
+ }
483
+ ]
484
+ },
485
+ {
486
+ capabilities: {
487
+ tools: {},
488
+ resources: {},
489
+ prompts: {}
490
+ }
491
+ }
492
+ );
493
+
494
+ this.setupHandlers();
495
+ this.setupErrorHandling();
496
+ }
497
+
498
+ // ────────────────────────────────────────────────────────────────────────────────────────────────────────────────
499
+ // Setup: Error Handling 'R_DVT_G_T_ErrorHandling'
500
+ // ────────────────────────────────────────────────────────────────────────────────────────────────────────────────
501
+
502
+ private setupErrorHandling(): void {
503
+ this.server.onerror = (error): void => {
504
+ serverLogger.error('MCP Server error', { error: String(error) });
505
+ };
506
+
507
+ process.on('SIGINT', async () => {
508
+ serverLogger.info('Received SIGINT, shutting down...');
509
+ await this.server.close();
510
+ process.exit(0);
511
+ });
512
+
513
+ process.on('SIGTERM', async () => {
514
+ serverLogger.info('Received SIGTERM, shutting down...');
515
+ await this.server.close();
516
+ process.exit(0);
517
+ });
518
+ }
519
+
520
+ // ────────────────────────────────────────────────────────────────────────────────────────────────────────────────
521
+ // Setup: MCP Protocol Handlers 'R_DVT_G_T_Handlers'
522
+ // ────────────────────────────────────────────────────────────────────────────────────────────────────────────────
523
+
524
+ private setupHandlers(): void {
525
+ // List available tools
526
+ this.server.setRequestHandler(ListToolsRequestSchema, async () => {
527
+ serverLogger.debug('Listing tools');
528
+ return {
529
+ tools: Object.values(TOOL_DEFINITIONS)
530
+ };
531
+ });
532
+
533
+ // List available resources
534
+ this.server.setRequestHandler(ListResourcesRequestSchema, async () => {
535
+ serverLogger.debug('Listing resources');
536
+ return {
537
+ resources: Object.values(RESOURCE_DEFINITIONS).map(r => ({
538
+ uri: r.uriTemplate,
539
+ name: r.name,
540
+ description: r.description,
541
+ mimeType: r.mimeType
542
+ }))
543
+ };
544
+ });
545
+
546
+ // Handle tool calls
547
+ this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
548
+ const { name, arguments: args } = request.params;
549
+ serverLogger.info('Tool called', { tool: name });
550
+
551
+ try {
552
+ const result = await this.handleToolCall(name as ToolName, args as Record<string, unknown>);
553
+ return {
554
+ content: [
555
+ {
556
+ type: 'text',
557
+ text: JSON.stringify(result, null, 2)
558
+ }
559
+ ]
560
+ };
561
+ } catch (error) {
562
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
563
+ serverLogger.error('Tool call failed', { tool: name, error: errorMessage });
564
+ throw new McpError(ErrorCode.InternalError, errorMessage);
565
+ }
566
+ });
567
+
568
+ // Handle resource reads
569
+ this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
570
+ const { uri } = request.params;
571
+ serverLogger.info('Resource requested', { uri });
572
+
573
+ try {
574
+ const result = await this.handleResourceRead(uri);
575
+ return {
576
+ contents: [
577
+ {
578
+ uri,
579
+ mimeType: 'application/json',
580
+ text: JSON.stringify(result, null, 2)
581
+ }
582
+ ]
583
+ };
584
+ } catch (error) {
585
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
586
+ serverLogger.error('Resource read failed', { uri, error: errorMessage });
587
+ throw new McpError(ErrorCode.InternalError, errorMessage);
588
+ }
589
+ });
590
+
591
+ // ────────────────────────────────────────────────────────────────────────────────────────────────────────────────
592
+ // Prompt Handlers 'R_DVT_G_T_PromptHandlers' - User-facing workflow templates
593
+ // ────────────────────────────────────────────────────────────────────────────────────────────────────────────────
594
+
595
+ // List available prompts
596
+ this.server.setRequestHandler(ListPromptsRequestSchema, async () => {
597
+ serverLogger.debug('Listing prompts');
598
+ return {
599
+ prompts: Object.values(PROMPT_DEFINITIONS).map(p => ({
600
+ name: p.name,
601
+ description: p.description,
602
+ arguments: p.arguments
603
+ }))
604
+ };
605
+ });
606
+
607
+ // Get specific prompt content
608
+ this.server.setRequestHandler(GetPromptRequestSchema, async (request) => {
609
+ const { name, arguments: args } = request.params;
610
+ serverLogger.info('Prompt requested', { prompt: name });
611
+
612
+ // V: Validate prompt name
613
+ if (!(name in PROMPT_DEFINITIONS)) {
614
+ throw new McpError(ErrorCode.InvalidRequest, `Unknown prompt: ${name}`);
615
+ }
616
+
617
+ const promptName = name as PromptName;
618
+ const promptDef = PROMPT_DEFINITIONS[promptName];
619
+ let messages = PROMPT_MESSAGES[promptName];
620
+
621
+ // T: If prompt has arguments (like analysisId), inject them into the message
622
+ if (args && promptDef.arguments.length > 0) {
623
+ messages = messages.map(msg => ({
624
+ ...msg,
625
+ content: {
626
+ type: 'text' as const,
627
+ text: Object.entries(args).reduce(
628
+ (text, [key, value]) => text.replace(`{${key}}`, String(value)),
629
+ msg.content.text
630
+ )
631
+ }
632
+ }));
633
+ }
634
+
635
+ return {
636
+ description: promptDef.description,
637
+ messages
638
+ };
639
+ });
640
+ }
641
+
642
+
643
+ /*
644
+ ######################################################################################################################################################################################################
645
+ */
646
+ // #### LOCAL 'R_DVT_L' - Tool & Resource Handlers
647
+ /*
648
+ ######################################################################################################################################################################################################
649
+ */
650
+
651
+ // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════
652
+ // Helper: Require Backautocrat 'R_DVT_L_T_RequireBackautocrat'
653
+ // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════
654
+
655
+ private requireBackautocrat(): BitsBoundBackautocrat {
656
+ if (!this.backautocrat) {
657
+ throw new McpError(
658
+ ErrorCode.InternalError,
659
+ 'BitsBound API key not configured. Please set BITSBOUND_API_KEY environment variable on the server.'
660
+ );
661
+ }
662
+ return this.backautocrat;
663
+ }
664
+
665
+ // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════
666
+ // Tool Router 'R_DVT_L_T_ToolRouter' - Routes tool calls to appropriate handler
667
+ // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════
668
+
669
+ private async handleToolCall(
670
+ toolName: ToolName,
671
+ args: Record<string, unknown>
672
+ ): Promise<unknown> {
673
+ const backautocrat = this.requireBackautocrat();
674
+ switch (toolName) {
675
+ case 'process_contract':
676
+ return backautocrat.analyzeContract(args as unknown as AnalyzeContractInput);
677
+
678
+ case 'get_analysis_status':
679
+ return backautocrat.getAnalysisStatus(args as unknown as GetAnalysisStatusInput);
680
+
681
+ case 'ask_sac':
682
+ return backautocrat.askSac(args as unknown as AskSacInput);
683
+
684
+ case 'generate_redline':
685
+ return backautocrat.generateRedline(args as unknown as GenerateRedlineInput);
686
+
687
+ case 'generate_negotiation_email':
688
+ return backautocrat.generateNegotiationEmail(args as unknown as GenerateNegotiationEmailInput);
689
+
690
+ case 'extract_clause':
691
+ return backautocrat.extractClause(args as unknown as ExtractClauseInput);
692
+
693
+ case 'compare_playbook':
694
+ return backautocrat.comparePlaybook(args as unknown as ComparePlaybookInput);
695
+
696
+ // ═══════════════════════════════════════════════════════════════════════════════════════════
697
+ // INSTANT SWARM - Parallel N-Section Redlining (spawns N agents for N sections)
698
+ // ═══════════════════════════════════════════════════════════════════════════════════════════
699
+
700
+ case 'instant_swarm':
701
+ return backautocrat.instantSwarm(args as unknown as InstantSwarmInput);
702
+
703
+ // ═══════════════════════════════════════════════════════════════════════════════════════════
704
+ // QUICK TOOLS - Immediate analysis without full pipeline
705
+ // ═══════════════════════════════════════════════════════════════════════════════════════════
706
+
707
+ case 'quick_scan':
708
+ return backautocrat.quickScan(args as unknown as QuickScanInput);
709
+
710
+ case 'ask_clause':
711
+ return backautocrat.askClause(args as unknown as AskClauseInput);
712
+
713
+ case 'check_dealbreakers':
714
+ return backautocrat.checkDealbreakers(args as unknown as CheckDealbreakersInput);
715
+
716
+ // ═══════════════════════════════════════════════════════════════════════════════════════════
717
+ // FILE DOWNLOAD - Retrieves files from R2 (e.g., redlined DOCX)
718
+ // ═══════════════════════════════════════════════════════════════════════════════════════════
719
+
720
+ case 'download_file':
721
+ return backautocrat.downloadFile(args as unknown as DownloadFileInput);
722
+
723
+ default:
724
+ throw new Error(`Unknown tool: ${toolName}`);
725
+ }
726
+ }
727
+
728
+ // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════
729
+ // Resource Router 'R_DVT_L_T_ResourceRouter' - Routes resource reads to appropriate handler
730
+ // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════
731
+
732
+ private async handleResourceRead(uri: string): Promise<unknown> {
733
+ const backautocrat = this.requireBackautocrat();
734
+
735
+ // Parse URI: bitsbound://analysis/{analysisId}
736
+ const analysisMatch = uri.match(/^bitsbound:\/\/analysis\/(.+)$/);
737
+ if (analysisMatch) {
738
+ return backautocrat.getAnalysisResults(analysisMatch[1]);
739
+ }
740
+
741
+ // Parse URI: bitsbound://playbook/{playbookId}
742
+ const playbookMatch = uri.match(/^bitsbound:\/\/playbook\/(.+)$/);
743
+ if (playbookMatch) {
744
+ return backautocrat.getPlaybook(playbookMatch[1]);
745
+ }
746
+
747
+ throw new Error(`Unknown resource URI: ${uri}`);
748
+ }
749
+
750
+ // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════
751
+ // Run Server 'R_DVT_L_T_Run' - Starts the MCP server in stdio mode (for Claude Desktop)
752
+ // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════
753
+
754
+ async runStdio(): Promise<void> {
755
+ const transport = new StdioServerTransport();
756
+ await this.server.connect(transport);
757
+ serverLogger.info('BitsBound MCP Server running on stdio');
758
+ }
759
+
760
+ // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════
761
+ // Get Server Instance 'R_DVT_L_T_GetServer' - Returns the underlying MCP server for HTTP transport
762
+ // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════
763
+
764
+ getServer(): Server {
765
+ return this.server;
766
+ }
767
+ }
768
+
769
+
770
+ /*
771
+ ######################################################################################################################################################################################################
772
+ */
773
+ // #### HTTP SERVER 'R_DVT_Http' - HTTP/SSE Transport for claude.ai Custom Connectors
774
+ /*
775
+ ######################################################################################################################################################################################################
776
+ */
777
+
778
+ // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════
779
+ // HTTP MCP Server 'R_DVT_Http_Server' - Handles HTTP transport for remote MCP access
780
+ // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════
781
+
782
+ // D: Map of session ID to transport instance (for stateful sessions)
783
+ const sessionTransports: Map<string, StreamableHTTPServerTransport> = new Map();
784
+ // D: Map of session ID to MCP server instance (for stateful sessions)
785
+ const sessionServers: Map<string, BitsBoundMcpServer> = new Map();
786
+
787
+ // T: Extract API key from Authorization header
788
+ function extractApiKey(req: IncomingMessage): string | null {
789
+ const authHeader = req.headers.authorization;
790
+ if (!authHeader) return null;
791
+
792
+ // Support "Bearer sk_live_xxx" format
793
+ if (authHeader.startsWith('Bearer ')) {
794
+ return authHeader.slice(7);
795
+ }
796
+
797
+ return null;
798
+ }
799
+
800
+ // T: Send CORS headers for cross-origin requests
801
+ function setCorsHeaders(res: ServerResponse): void {
802
+ res.setHeader('Access-Control-Allow-Origin', '*');
803
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
804
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, Mcp-Session-Id');
805
+ res.setHeader('Access-Control-Expose-Headers', 'Mcp-Session-Id');
806
+ }
807
+
808
+ // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════
809
+ // OAuth 2.1 Endpoints 'R_DVT_Http_OAuth' - Discovery and Token endpoints for claude.ai
810
+ // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════
811
+
812
+ // T: Handle OAuth Authorization Server Metadata (RFC 8414)
813
+ function handleOAuthMetadata(_req: IncomingMessage, res: ServerResponse): void {
814
+ const metadata = {
815
+ issuer: OAUTH_ISSUER,
816
+ authorization_endpoint: `${OAUTH_ISSUER}/oauth/authorize`,
817
+ token_endpoint: `${OAUTH_ISSUER}/oauth/token`,
818
+ token_endpoint_auth_methods_supported: ['client_secret_post', 'client_secret_basic'],
819
+ grant_types_supported: ['authorization_code', 'client_credentials'],
820
+ response_types_supported: ['code'],
821
+ scopes_supported: ['mcp:read', 'mcp:write', 'contract:analyze'],
822
+ service_documentation: 'https://bitsbound.com/docs/mcp',
823
+ // OAuth 2.1 PKCE requirement
824
+ code_challenge_methods_supported: ['S256', 'plain'],
825
+ // MCP-specific metadata
826
+ mcp_endpoint: `${OAUTH_ISSUER}/mcp`,
827
+ mcp_version: '1.0.0'
828
+ };
829
+
830
+ res.writeHead(200, {
831
+ 'Content-Type': 'application/json',
832
+ 'Cache-Control': 'max-age=3600'
833
+ });
834
+ res.end(JSON.stringify(metadata, null, 2));
835
+ }
836
+
837
+ // T: Handle OAuth Protected Resource Metadata (RFC 9470)
838
+ function handleProtectedResourceMetadata(_req: IncomingMessage, res: ServerResponse): void {
839
+ const metadata = {
840
+ resource: `${OAUTH_ISSUER}/mcp`,
841
+ authorization_servers: [OAUTH_ISSUER],
842
+ bearer_methods_supported: ['header'],
843
+ scopes_supported: ['mcp:read', 'mcp:write', 'contract:analyze'],
844
+ resource_documentation: 'https://bitsbound.com/docs/mcp'
845
+ };
846
+
847
+ res.writeHead(200, {
848
+ 'Content-Type': 'application/json',
849
+ 'Cache-Control': 'max-age=3600'
850
+ });
851
+ res.end(JSON.stringify(metadata, null, 2));
852
+ }
853
+
854
+ // T: Handle OAuth Authorization endpoint (Authorization Code flow with PKCE)
855
+ function handleOAuthAuthorize(req: IncomingMessage, res: ServerResponse): void {
856
+ const url = new URL(req.url || '/', `http://${req.headers.host}`);
857
+
858
+ // D: Extract OAuth parameters from query string
859
+ const responseType = url.searchParams.get('response_type');
860
+ const clientId = url.searchParams.get('client_id');
861
+ const redirectUri = url.searchParams.get('redirect_uri');
862
+ const state = url.searchParams.get('state');
863
+ const scope = url.searchParams.get('scope');
864
+ const codeChallenge = url.searchParams.get('code_challenge');
865
+ const codeChallengeMethod = url.searchParams.get('code_challenge_method') || 'plain';
866
+
867
+ // V: Validate required parameters
868
+ if (responseType !== 'code') {
869
+ res.writeHead(400, { 'Content-Type': 'application/json' });
870
+ res.end(JSON.stringify({
871
+ error: 'unsupported_response_type',
872
+ error_description: 'Only response_type=code is supported'
873
+ }));
874
+ return;
875
+ }
876
+
877
+ if (!clientId) {
878
+ res.writeHead(400, { 'Content-Type': 'application/json' });
879
+ res.end(JSON.stringify({
880
+ error: 'invalid_request',
881
+ error_description: 'client_id is required'
882
+ }));
883
+ return;
884
+ }
885
+
886
+ if (!redirectUri) {
887
+ res.writeHead(400, { 'Content-Type': 'application/json' });
888
+ res.end(JSON.stringify({
889
+ error: 'invalid_request',
890
+ error_description: 'redirect_uri is required'
891
+ }));
892
+ return;
893
+ }
894
+
895
+ // V: PKCE is required for OAuth 2.1
896
+ if (!codeChallenge) {
897
+ res.writeHead(400, { 'Content-Type': 'application/json' });
898
+ res.end(JSON.stringify({
899
+ error: 'invalid_request',
900
+ error_description: 'code_challenge is required (PKCE)'
901
+ }));
902
+ return;
903
+ }
904
+
905
+ // D: Allowed scopes for all clients
906
+ const ALLOWED_SCOPES = ['mcp:read', 'mcp:write', 'contract:analyze'];
907
+
908
+ // T: Check if this is a registered confidential client or a public client (PKCE)
909
+ const client = oauthClients.get(clientId);
910
+
911
+ // T: For public clients (PKCE flow), we allow any client_id since security comes from PKCE
912
+ // For registered confidential clients, we use their configured scopes
913
+ const clientScopes = client ? client.scopes : ALLOWED_SCOPES;
914
+
915
+ // T: Parse and validate requested scopes
916
+ const requestedScopes = scope ? scope.split(' ') : clientScopes;
917
+ const grantedScopes = requestedScopes.filter(s => ALLOWED_SCOPES.includes(s));
918
+
919
+ // T: Generate authorization code
920
+ const code = generateAuthorizationCode(
921
+ clientId,
922
+ redirectUri,
923
+ codeChallenge,
924
+ codeChallengeMethod,
925
+ grantedScopes
926
+ );
927
+
928
+ serverLogger.info('Authorization code issued', {
929
+ clientId,
930
+ clientType: client ? 'confidential' : 'public',
931
+ scopes: grantedScopes
932
+ });
933
+
934
+ // T: Build redirect URL with authorization code
935
+ const redirectUrl = new URL(redirectUri);
936
+ redirectUrl.searchParams.set('code', code);
937
+ if (state) {
938
+ redirectUrl.searchParams.set('state', state);
939
+ }
940
+
941
+ // O: Redirect back to client with authorization code
942
+ res.writeHead(302, {
943
+ 'Location': redirectUrl.toString(),
944
+ 'Cache-Control': 'no-store'
945
+ });
946
+ res.end();
947
+ }
948
+
949
+ // T: Parse request body as URL-encoded form data
950
+ async function parseFormBody(req: IncomingMessage): Promise<URLSearchParams> {
951
+ return new Promise((resolve, reject) => {
952
+ let body = '';
953
+ req.on('data', (chunk: Buffer) => {
954
+ body += chunk.toString();
955
+ // V: Limit body size to 16KB for security
956
+ if (body.length > 16384) {
957
+ reject(new Error('Request body too large'));
958
+ }
959
+ });
960
+ req.on('end', () => resolve(new URLSearchParams(body)));
961
+ req.on('error', reject);
962
+ });
963
+ }
964
+
965
+ // T: Parse Basic Auth header
966
+ function parseBasicAuth(req: IncomingMessage): { clientId: string; clientSecret: string } | null {
967
+ const authHeader = req.headers.authorization;
968
+ if (!authHeader || !authHeader.startsWith('Basic ')) return null;
969
+
970
+ try {
971
+ const decoded = Buffer.from(authHeader.slice(6), 'base64').toString();
972
+ const colonIndex = decoded.indexOf(':');
973
+ if (colonIndex === -1) return null;
974
+
975
+ return {
976
+ clientId: decoded.slice(0, colonIndex),
977
+ clientSecret: decoded.slice(colonIndex + 1)
978
+ };
979
+ } catch {
980
+ return null;
981
+ }
982
+ }
983
+
984
+ // T: Handle OAuth Token endpoint (Authorization Code + Client Credentials grants)
985
+ async function handleOAuthToken(req: IncomingMessage, res: ServerResponse): Promise<void> {
986
+ // V: Only POST allowed
987
+ if (req.method !== 'POST') {
988
+ res.writeHead(405, { 'Content-Type': 'application/json', 'Allow': 'POST' });
989
+ res.end(JSON.stringify({ error: 'method_not_allowed' }));
990
+ return;
991
+ }
992
+
993
+ try {
994
+ const body = await parseFormBody(req);
995
+ const grantType = body.get('grant_type');
996
+
997
+ // T: Extract client credentials (supports both Basic auth and POST body)
998
+ let clientId: string | null = null;
999
+ let clientSecret: string | null = null;
1000
+
1001
+ // Try Basic auth first
1002
+ const basicAuth = parseBasicAuth(req);
1003
+ if (basicAuth) {
1004
+ clientId = basicAuth.clientId;
1005
+ clientSecret = basicAuth.clientSecret;
1006
+ } else {
1007
+ // Fall back to POST body
1008
+ clientId = body.get('client_id');
1009
+ clientSecret = body.get('client_secret');
1010
+ }
1011
+
1012
+ // ════════════════════════════════════════════════════════════════════════════════════════════
1013
+ // Authorization Code Grant (OAuth 2.1 with PKCE)
1014
+ // ════════════════════════════════════════════════════════════════════════════════════════════
1015
+ if (grantType === 'authorization_code') {
1016
+ const code = body.get('code');
1017
+ const redirectUri = body.get('redirect_uri');
1018
+ const codeVerifier = body.get('code_verifier');
1019
+
1020
+ // V: Validate required parameters
1021
+ if (!code || !redirectUri || !codeVerifier || !clientId) {
1022
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1023
+ res.end(JSON.stringify({
1024
+ error: 'invalid_request',
1025
+ error_description: 'code, redirect_uri, code_verifier, and client_id are required'
1026
+ }));
1027
+ return;
1028
+ }
1029
+
1030
+ // V: Validate and consume authorization code
1031
+ const result = consumeAuthorizationCode(code, clientId, redirectUri, codeVerifier);
1032
+ if (!result) {
1033
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1034
+ res.end(JSON.stringify({
1035
+ error: 'invalid_grant',
1036
+ error_description: 'Invalid or expired authorization code, or PKCE verification failed'
1037
+ }));
1038
+ return;
1039
+ }
1040
+
1041
+ // T: Generate access token with customer's BitsBound API key (client_secret)
1042
+ // The client_secret IS the customer's BitsBound API key (sk_live_xxx...)
1043
+ const accessToken = generateAccessToken(clientId, result.scopes, clientSecret || undefined);
1044
+
1045
+ serverLogger.info('OAuth token issued via authorization_code', {
1046
+ clientId,
1047
+ scopes: result.scopes,
1048
+ hasApiKey: !!clientSecret
1049
+ });
1050
+
1051
+ // O: Return token response
1052
+ res.writeHead(200, {
1053
+ 'Content-Type': 'application/json',
1054
+ 'Cache-Control': 'no-store',
1055
+ 'Pragma': 'no-cache'
1056
+ });
1057
+ res.end(JSON.stringify({
1058
+ access_token: accessToken,
1059
+ token_type: 'Bearer',
1060
+ expires_in: TOKEN_EXPIRY_SECONDS,
1061
+ scope: result.scopes.join(' ')
1062
+ }));
1063
+ return;
1064
+ }
1065
+
1066
+ // ════════════════════════════════════════════════════════════════════════════════════════════
1067
+ // Client Credentials Grant (Machine-to-Machine)
1068
+ // ════════════════════════════════════════════════════════════════════════════════════════════
1069
+ if (grantType === 'client_credentials') {
1070
+ // V: Require client credentials
1071
+ if (!clientId || !clientSecret) {
1072
+ res.writeHead(401, {
1073
+ 'Content-Type': 'application/json',
1074
+ 'WWW-Authenticate': 'Basic realm="BitsBound MCP"'
1075
+ });
1076
+ res.end(JSON.stringify({
1077
+ error: 'invalid_client',
1078
+ error_description: 'Client credentials required'
1079
+ }));
1080
+ return;
1081
+ }
1082
+
1083
+ // V: Validate client credentials (async for MongoDB per-customer API key validation)
1084
+ const client = await validateClientCredentials(clientId, clientSecret);
1085
+ if (!client) {
1086
+ res.writeHead(401, { 'Content-Type': 'application/json' });
1087
+ res.end(JSON.stringify({
1088
+ error: 'invalid_client',
1089
+ error_description: 'Invalid client credentials'
1090
+ }));
1091
+ return;
1092
+ }
1093
+
1094
+ // T: Parse requested scope (optional)
1095
+ const requestedScope = body.get('scope');
1096
+ const grantedScopes = requestedScope
1097
+ ? requestedScope.split(' ').filter(s => client.scopes.includes(s))
1098
+ : client.scopes;
1099
+
1100
+ // T: Generate access token with customer's BitsBound API key
1101
+ const accessToken = generateAccessToken(clientId, grantedScopes, clientSecret || undefined);
1102
+
1103
+ serverLogger.info('OAuth token issued via client_credentials', { clientId, scopes: grantedScopes });
1104
+
1105
+ // O: Return token response
1106
+ res.writeHead(200, {
1107
+ 'Content-Type': 'application/json',
1108
+ 'Cache-Control': 'no-store',
1109
+ 'Pragma': 'no-cache'
1110
+ });
1111
+ res.end(JSON.stringify({
1112
+ access_token: accessToken,
1113
+ token_type: 'Bearer',
1114
+ expires_in: TOKEN_EXPIRY_SECONDS,
1115
+ scope: grantedScopes.join(' ')
1116
+ }));
1117
+ return;
1118
+ }
1119
+
1120
+ // V: Unsupported grant type
1121
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1122
+ res.end(JSON.stringify({
1123
+ error: 'unsupported_grant_type',
1124
+ error_description: 'Supported grant types: authorization_code, client_credentials'
1125
+ }));
1126
+
1127
+ } catch (error) {
1128
+ serverLogger.error('OAuth token error', { error: String(error) });
1129
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1130
+ res.end(JSON.stringify({
1131
+ error: 'invalid_request',
1132
+ error_description: 'Failed to process token request'
1133
+ }));
1134
+ }
1135
+ }
1136
+
1137
+ // T: Handle HTTP requests for MCP protocol
1138
+ async function handleHttpRequest(req: IncomingMessage, res: ServerResponse): Promise<void> {
1139
+ setCorsHeaders(res);
1140
+
1141
+ // V: Handle CORS preflight
1142
+ if (req.method === 'OPTIONS') {
1143
+ res.writeHead(204);
1144
+ res.end();
1145
+ return;
1146
+ }
1147
+
1148
+ const url = new URL(req.url || '/', `http://${req.headers.host}`);
1149
+
1150
+ // ════════════════════════════════════════════════════════════════════════════════════════════
1151
+ // OAuth 2.1 Endpoint Routing
1152
+ // ════════════════════════════════════════════════════════════════════════════════════════════
1153
+
1154
+ // T: OAuth Authorization Server Metadata (RFC 8414)
1155
+ if (url.pathname === '/.well-known/oauth-authorization-server') {
1156
+ handleOAuthMetadata(req, res);
1157
+ return;
1158
+ }
1159
+
1160
+ // T: OAuth Protected Resource Metadata (RFC 9470)
1161
+ if (url.pathname === '/.well-known/oauth-protected-resource') {
1162
+ handleProtectedResourceMetadata(req, res);
1163
+ return;
1164
+ }
1165
+
1166
+ // T: OAuth Authorization endpoint (Authorization Code flow)
1167
+ if (url.pathname === '/oauth/authorize') {
1168
+ handleOAuthAuthorize(req, res);
1169
+ return;
1170
+ }
1171
+
1172
+ // T: OAuth Token endpoint
1173
+ if (url.pathname === '/oauth/token') {
1174
+ await handleOAuthToken(req, res);
1175
+ return;
1176
+ }
1177
+
1178
+ // T: Health check endpoint
1179
+ if (url.pathname === '/health') {
1180
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1181
+ res.end(JSON.stringify({
1182
+ status: 'healthy',
1183
+ mode: 'http',
1184
+ version: '1.0.0',
1185
+ oauth: {
1186
+ issuer: OAUTH_ISSUER,
1187
+ authorization_endpoint: `${OAUTH_ISSUER}/oauth/authorize`,
1188
+ token_endpoint: `${OAUTH_ISSUER}/oauth/token`,
1189
+ metadata_endpoint: `${OAUTH_ISSUER}/.well-known/oauth-authorization-server`
1190
+ }
1191
+ }));
1192
+ return;
1193
+ }
1194
+
1195
+ // V: Only handle /mcp endpoint beyond this point
1196
+ if (url.pathname !== '/mcp') {
1197
+ res.writeHead(404, { 'Content-Type': 'application/json' });
1198
+ res.end(JSON.stringify({
1199
+ error: 'Not Found',
1200
+ message: 'Use /mcp endpoint for MCP protocol',
1201
+ oauth_discovery: `${OAUTH_ISSUER}/.well-known/oauth-authorization-server`
1202
+ }));
1203
+ return;
1204
+ }
1205
+
1206
+ // ════════════════════════════════════════════════════════════════════════════════════════════
1207
+ // MCP Endpoint Authentication (JWT Bearer or API Key fallback)
1208
+ // ════════════════════════════════════════════════════════════════════════════════════════════
1209
+
1210
+ let apiKey = '';
1211
+ const bearerToken = extractApiKey(req);
1212
+
1213
+ if (bearerToken) {
1214
+ // T: Check if it's a JWT (has 3 parts separated by dots)
1215
+ if (bearerToken.includes('.') && bearerToken.split('.').length === 3) {
1216
+ // V: Validate JWT token
1217
+ const payload = verifyAccessToken(bearerToken);
1218
+ if (!payload) {
1219
+ res.writeHead(401, {
1220
+ 'Content-Type': 'application/json',
1221
+ 'WWW-Authenticate': 'Bearer realm="BitsBound MCP", error="invalid_token"'
1222
+ });
1223
+ res.end(JSON.stringify({
1224
+ error: 'invalid_token',
1225
+ error_description: 'The access token is invalid or expired'
1226
+ }));
1227
+ return;
1228
+ }
1229
+ // T: JWT is valid - extract customer's BitsBound API key from token
1230
+ // The API key was stored in the token during OAuth flow (client_secret = customer's API key)
1231
+ apiKey = payload.bitsbound_api_key || process.env.BITSBOUND_API_KEY || '';
1232
+ serverLogger.debug('Authenticated via JWT', {
1233
+ clientId: payload.sub,
1234
+ scopes: payload.scope,
1235
+ hasApiKey: !!payload.bitsbound_api_key
1236
+ });
1237
+ } else if (bearerToken.startsWith('sk_live_')) {
1238
+ // T: It's a raw BitsBound API key (direct auth without OAuth)
1239
+ // V: Validate against MongoDB
1240
+ const validation = await validateApiKeyFromMongoDB(bearerToken);
1241
+ if (!validation.valid) {
1242
+ res.writeHead(401, {
1243
+ 'Content-Type': 'application/json',
1244
+ 'WWW-Authenticate': 'Bearer realm="BitsBound MCP", error="invalid_token"'
1245
+ });
1246
+ res.end(JSON.stringify({
1247
+ error: 'invalid_token',
1248
+ error_description: 'Invalid BitsBound API key'
1249
+ }));
1250
+ return;
1251
+ }
1252
+ apiKey = bearerToken;
1253
+ serverLogger.info('Authenticated via direct API key', { customerId: validation.customerId });
1254
+ } else {
1255
+ // V: Unknown token format
1256
+ res.writeHead(401, {
1257
+ 'Content-Type': 'application/json',
1258
+ 'WWW-Authenticate': 'Bearer realm="BitsBound MCP", error="invalid_token"'
1259
+ });
1260
+ res.end(JSON.stringify({
1261
+ error: 'invalid_token',
1262
+ error_description: 'Invalid token format. Use OAuth or provide sk_live_xxx API key.'
1263
+ }));
1264
+ return;
1265
+ }
1266
+ } else {
1267
+ // V: No authentication provided - require OAuth
1268
+ // Return 401 to trigger OAuth flow in claude.ai
1269
+ res.writeHead(401, {
1270
+ 'Content-Type': 'application/json',
1271
+ 'WWW-Authenticate': `Bearer realm="BitsBound MCP", authorization_uri="${OAUTH_ISSUER}/oauth/authorize", token_uri="${OAUTH_ISSUER}/oauth/token"`
1272
+ });
1273
+ res.end(JSON.stringify({
1274
+ error: 'unauthorized',
1275
+ error_description: 'Authentication required. Please complete OAuth flow.',
1276
+ oauth_discovery: `${OAUTH_ISSUER}/.well-known/oauth-authorization-server`
1277
+ }));
1278
+ return;
1279
+ }
1280
+
1281
+ // D: Get or create session-based transport
1282
+ const sessionId = req.headers['mcp-session-id'] as string | undefined;
1283
+
1284
+ let transport: StreamableHTTPServerTransport;
1285
+ let mcpServer: BitsBoundMcpServer;
1286
+
1287
+ if (sessionId && sessionTransports.has(sessionId)) {
1288
+ // T: Reuse existing session
1289
+ transport = sessionTransports.get(sessionId)!;
1290
+ mcpServer = sessionServers.get(sessionId)!;
1291
+ serverLogger.debug('Reusing existing session', { sessionId });
1292
+ } else if (req.method === 'GET' || (req.method === 'POST' && !sessionId)) {
1293
+ // T: Create new session for initialization
1294
+ const config: McpServerConfig = {
1295
+ BITSBOUND_API_KEY: apiKey,
1296
+ BITSBOUND_API_URL: process.env.BITSBOUND_API_URL || DEFAULT_API_URL
1297
+ };
1298
+
1299
+ mcpServer = new BitsBoundMcpServer(config);
1300
+
1301
+ transport = new StreamableHTTPServerTransport({
1302
+ sessionIdGenerator: () => randomUUID(),
1303
+ onsessioninitialized: (newSessionId: string) => {
1304
+ serverLogger.info('Session initialized', { sessionId: newSessionId });
1305
+ sessionTransports.set(newSessionId, transport);
1306
+ sessionServers.set(newSessionId, mcpServer);
1307
+ },
1308
+ onsessionclosed: (closedSessionId: string) => {
1309
+ serverLogger.info('Session closed', { sessionId: closedSessionId });
1310
+ sessionTransports.delete(closedSessionId);
1311
+ sessionServers.delete(closedSessionId);
1312
+ }
1313
+ });
1314
+
1315
+ // T: Connect the MCP server to the transport
1316
+ await mcpServer.getServer().connect(transport);
1317
+ serverLogger.info('New MCP session created');
1318
+ } else {
1319
+ // V: Invalid request - session required but not found
1320
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1321
+ res.end(JSON.stringify({
1322
+ error: 'Bad Request',
1323
+ message: 'Session ID required for non-initialization requests'
1324
+ }));
1325
+ return;
1326
+ }
1327
+
1328
+ // T: Handle the request through StreamableHTTPServerTransport
1329
+ try {
1330
+ await transport.handleRequest(req, res);
1331
+ } catch (error) {
1332
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
1333
+ serverLogger.error('HTTP transport error', { error: errorMessage });
1334
+ if (!res.headersSent) {
1335
+ res.writeHead(500, { 'Content-Type': 'application/json' });
1336
+ res.end(JSON.stringify({ error: 'Internal Server Error', message: errorMessage }));
1337
+ }
1338
+ }
1339
+ }
1340
+
1341
+ // T: Start HTTP server for remote MCP access
1342
+ async function startHttpServer(port: number): Promise<void> {
1343
+ const httpServer = createServer(handleHttpRequest);
1344
+
1345
+ httpServer.listen(port, () => {
1346
+ serverLogger.info(`BitsBound MCP Server running on HTTP port ${port}`);
1347
+ serverLogger.info(`Remote MCP URL: http://localhost:${port}/mcp`);
1348
+ console.log(`\n╔═══════════════════════════════════════════════════════════════════════════════╗`);
1349
+ console.log(`║ BitsBound MCP Server (HTTP Mode) - OAuth 2.1 Enabled ║`);
1350
+ console.log(`╠═══════════════════════════════════════════════════════════════════════════════╣`);
1351
+ console.log(`║ MCP Endpoint: http://localhost:${port}/mcp`.padEnd(80) + `║`);
1352
+ console.log(`║ Health Check: http://localhost:${port}/health`.padEnd(80) + `║`);
1353
+ console.log(`╠═══════════════════════════════════════════════════════════════════════════════╣`);
1354
+ console.log(`║ OAuth 2.1 Endpoints: ║`);
1355
+ console.log(`║ Discovery: /.well-known/oauth-authorization-server ║`);
1356
+ console.log(`║ Token Endpoint: /oauth/token ║`);
1357
+ console.log(`╠═══════════════════════════════════════════════════════════════════════════════╣`);
1358
+ console.log(`║ For claude.ai Custom Connectors: ║`);
1359
+ console.log(`║ 1. Name: BitsBound ║`);
1360
+ console.log(`║ 2. URL: https://your-domain.com/mcp ║`);
1361
+ console.log(`║ 3. Client ID: bitsbound-mcp ║`);
1362
+ console.log(`║ 4. Client Secret: Your BitsBound API key (sk_live_xxx...) ║`);
1363
+ console.log(`╚═══════════════════════════════════════════════════════════════════════════════╝\n`);
1364
+ });
1365
+
1366
+ // T: Handle graceful shutdown
1367
+ process.on('SIGINT', () => {
1368
+ serverLogger.info('Received SIGINT, shutting down HTTP server...');
1369
+ httpServer.close(() => process.exit(0));
1370
+ });
1371
+
1372
+ process.on('SIGTERM', () => {
1373
+ serverLogger.info('Received SIGTERM, shutting down HTTP server...');
1374
+ httpServer.close(() => process.exit(0));
1375
+ });
1376
+ }
1377
+
1378
+
1379
+ /*
1380
+ ######################################################################################################################################################################################################
1381
+ */
1382
+ // #### EXECUTION 'R_E' - Main Entry Point
1383
+ /*
1384
+ ######################################################################################################################################################################################################
1385
+ */
1386
+
1387
+ // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════
1388
+ // Main Entry 'R_E_Main' - Reads config from environment and starts server
1389
+ // Supports two modes:
1390
+ // 1. STDIO mode (default) - For Claude Desktop local use
1391
+ // 2. HTTP mode (MCP_HTTP_PORT env or --http flag) - For claude.ai Custom Connectors
1392
+ // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════
1393
+
1394
+ async function main(): Promise<void> {
1395
+ serverLogger.info('Starting BitsBound MCP Server...');
1396
+
1397
+ // D: Check for HTTP mode
1398
+ const httpPort = process.env.MCP_HTTP_PORT || process.env.PORT;
1399
+ const isHttpMode = httpPort || process.argv.includes('--http');
1400
+
1401
+ if (isHttpMode) {
1402
+ // T: HTTP mode - for claude.ai Custom Connectors
1403
+ const port = parseInt(httpPort || '3001', 10);
1404
+ serverLogger.info('Starting in HTTP mode', { port });
1405
+ await startHttpServer(port);
1406
+ } else {
1407
+ // T: STDIO mode - for Claude Desktop
1408
+ const apiKey = process.env.BITSBOUND_API_KEY;
1409
+ if (!apiKey) {
1410
+ serverLogger.error('BITSBOUND_API_KEY environment variable is required');
1411
+ console.error('Error: BITSBOUND_API_KEY environment variable is required');
1412
+ console.error('');
1413
+ console.error('For Claude Desktop, set it in your config:');
1414
+ console.error(' "env": { "BITSBOUND_API_KEY": "sk_live_xxxxx" }');
1415
+ console.error('');
1416
+ console.error('For HTTP mode, set MCP_HTTP_PORT and include API key in Authorization header');
1417
+ console.error('');
1418
+ console.error('Get your API key at: https://account.bitsbound.com/api-keys');
1419
+ process.exit(1);
1420
+ }
1421
+
1422
+ const config: McpServerConfig = {
1423
+ BITSBOUND_API_KEY: apiKey,
1424
+ BITSBOUND_API_URL: process.env.BITSBOUND_API_URL || DEFAULT_API_URL
1425
+ };
1426
+
1427
+ serverLogger.info('Configuration loaded (stdio mode)', {
1428
+ apiUrl: config.BITSBOUND_API_URL,
1429
+ apiKeyPrefix: apiKey.substring(0, 10) + '...'
1430
+ });
1431
+
1432
+ const server = new BitsBoundMcpServer(config);
1433
+ await server.runStdio();
1434
+ }
1435
+ }
1436
+
1437
+ main().catch((error) => {
1438
+ serverLogger.error('Fatal error', { error: String(error) });
1439
+ console.error('Fatal error:', error);
1440
+ process.exit(1);
1441
+ });