@bitsbound/mcp-server 1.0.5 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/orchestration/backautocrat/Bitsbound_Kings_McpServer_Backend_Orchestration_Backautocrat.d.ts +21 -5
- package/dist/orchestration/backautocrat/Bitsbound_Kings_McpServer_Backend_Orchestration_Backautocrat.d.ts.map +1 -1
- package/dist/orchestration/backautocrat/Bitsbound_Kings_McpServer_Backend_Orchestration_Backautocrat.js +118 -237
- package/dist/orchestration/backautocrat/Bitsbound_Kings_McpServer_Backend_Orchestration_Backautocrat.js.map +1 -1
- package/dist/server/Bitsbound_Kings_McpServer_Backend_Server.js +31 -0
- package/dist/server/Bitsbound_Kings_McpServer_Backend_Server.js.map +1 -1
- package/dist/types/Bitsbound_Kings_McpServer_Backend_Types.d.ts +318 -46
- package/dist/types/Bitsbound_Kings_McpServer_Backend_Types.d.ts.map +1 -1
- package/dist/types/Bitsbound_Kings_McpServer_Backend_Types.js +399 -8
- package/dist/types/Bitsbound_Kings_McpServer_Backend_Types.js.map +1 -1
- package/logger/Bitsbound_Kings_McpServer_Backend_Logger.ts +130 -0
- package/orchestration/backautocrat/Bitsbound_Kings_McpServer_Backend_Orchestration_Backautocrat.ts +715 -0
- package/package.json +12 -17
- package/server/Bitsbound_Kings_McpServer_Backend_Server.ts +1441 -0
- package/server.json +20 -0
- package/tsconfig.json +21 -0
- package/types/Bitsbound_Kings_McpServer_Backend_Types.node.test.mjs +14 -0
- package/types/Bitsbound_Kings_McpServer_Backend_Types.ts +1667 -0
|
@@ -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
|
+
});
|