@bitsbound/mcp-server 1.0.0

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