context-mcp-server 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +464 -0
  2. package/codegraph/__init__.py +0 -0
  3. package/codegraph/__main__.py +24 -0
  4. package/codegraph/__pycache__/__init__.cpython-313.pyc +0 -0
  5. package/codegraph/__pycache__/__main__.cpython-313.pyc +0 -0
  6. package/codegraph/__pycache__/cache.cpython-313.pyc +0 -0
  7. package/codegraph/__pycache__/config.cpython-313.pyc +0 -0
  8. package/codegraph/__pycache__/report.cpython-313.pyc +0 -0
  9. package/codegraph/__pycache__/scanner.cpython-313.pyc +0 -0
  10. package/codegraph/__pycache__/server.cpython-313.pyc +0 -0
  11. package/codegraph/cache.py +137 -0
  12. package/codegraph/config.py +31 -0
  13. package/codegraph/extractors/__init__.py +0 -0
  14. package/codegraph/extractors/__pycache__/__init__.cpython-313.pyc +0 -0
  15. package/codegraph/extractors/__pycache__/ast_extractor.cpython-313.pyc +0 -0
  16. package/codegraph/extractors/__pycache__/audio_extractor.cpython-313.pyc +0 -0
  17. package/codegraph/extractors/__pycache__/doc_extractor.cpython-313.pyc +0 -0
  18. package/codegraph/extractors/__pycache__/image_extractor.cpython-313.pyc +0 -0
  19. package/codegraph/extractors/ast_extractor.py +222 -0
  20. package/codegraph/extractors/audio_extractor.py +8 -0
  21. package/codegraph/extractors/doc_extractor.py +34 -0
  22. package/codegraph/extractors/image_extractor.py +26 -0
  23. package/codegraph/graph/__init__.py +0 -0
  24. package/codegraph/graph/__pycache__/__init__.cpython-313.pyc +0 -0
  25. package/codegraph/graph/__pycache__/builder.cpython-313.pyc +0 -0
  26. package/codegraph/graph/__pycache__/clustering.cpython-313.pyc +0 -0
  27. package/codegraph/graph/__pycache__/query.cpython-313.pyc +0 -0
  28. package/codegraph/graph/builder.py +145 -0
  29. package/codegraph/graph/clustering.py +40 -0
  30. package/codegraph/graph/query.py +283 -0
  31. package/codegraph/report.py +115 -0
  32. package/codegraph/scanner.py +92 -0
  33. package/codegraph/server.py +514 -0
  34. package/package.json +62 -0
  35. package/src/cli.js +1010 -0
  36. package/src/config.js +89 -0
  37. package/src/db.js +786 -0
  38. package/src/guard.js +20 -0
  39. package/src/hooks/autoContext.js +17 -0
  40. package/src/hooks/autoLink.js +7 -0
  41. package/src/http.js +765 -0
  42. package/src/index.js +47 -0
  43. package/src/search.js +50 -0
  44. package/src/server.js +80 -0
  45. package/src/summarizer.js +124 -0
  46. package/src/templates/AGENTS.md +76 -0
  47. package/src/templates/CLAUDE.md +94 -0
  48. package/src/templates/GEMINI.md +76 -0
  49. package/src/templates/cursor-rules.mdc +41 -0
  50. package/src/templates/windsurf-rules.md +35 -0
  51. package/src/tools/codegraph.js +215 -0
  52. package/src/tools/context.js +188 -0
  53. package/src/tools/discussion.js +123 -0
  54. package/src/tools/errorCheck.js +65 -0
  55. package/src/tools/fileTools.js +185 -0
  56. package/src/tools/gitTools.js +259 -0
  57. package/src/tools/search.js +55 -0
  58. package/src/vector.js +153 -0
package/src/http.js ADDED
@@ -0,0 +1,765 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * context-mcp HTTP server — Streamable HTTP transport (OAuth 2.0)
4
+ *
5
+ * Enables web-based AI clients (ChatGPT, Claude.ai, etc.) to connect
6
+ * to context-mcp over HTTP.
7
+ *
8
+ * Auth: OAuth 2.0 Client Credentials flow
9
+ * 1. Client sends POST /oauth/token with client_id & client_secret
10
+ * 2. Server returns an access_token
11
+ * 3. Client uses Authorization: Bearer <token> on /mcp requests
12
+ *
13
+ * Setup:
14
+ * 1. Run: ctx online
15
+ * 2. Config auto-generated in ~/.context-mcp/contextconfig.json
16
+ * 3. Add http://localhost:3100 as MCP connector in Claude.ai / ChatGPT
17
+ */
18
+
19
+ import { createServer as createHTTPServer } from 'node:http';
20
+ import { randomUUID, createHash, createHmac, timingSafeEqual } from 'node:crypto';
21
+ import { dirname, join } from 'node:path';
22
+ import { fileURLToPath } from 'node:url';
23
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
24
+ import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
25
+ import { getConfig, getConfigPath } from './config.js';
26
+ import { createServer } from './server.js';
27
+
28
+ const __dirname = dirname(fileURLToPath(import.meta.url));
29
+ const ROOT = join(__dirname, '..');
30
+
31
+ // ── CLI flags ─────────────────────────────────────────────────────────────────
32
+ // --port <number> HTTP port (default: 3100)
33
+ // --host <string> Bind host (default: localhost)
34
+ // --access-git Enable git tools
35
+ // --data-dir <path> Override ~/.context-mcp storage directory
36
+ // --help Show usage
37
+
38
+ const _args = process.argv.slice(2);
39
+
40
+ if (_args.includes('--help') || _args.includes('-h')) {
41
+ console.log(`
42
+ context-mcp-http — Persistent AI memory MCP server (HTTP/OAuth transport)
43
+
44
+ Usage:
45
+ context-mcp-http [options]
46
+ npx context-mcp-server@latest [options]
47
+
48
+ Options:
49
+ --port <number> HTTP listen port (default: 3100)
50
+ --host <string> Bind address (default: localhost)
51
+ --access-git Enable git tools for connected clients
52
+ --data-dir <path> Override storage directory (default: ~/.context-mcp)
53
+ Also settable via env: CONTEXT_MCP_DIR=<path>
54
+ --help, -h Show this help
55
+
56
+ Platform setup (HTTP — for Claude.ai, ChatGPT, web clients):
57
+ 1. Start the server: ctx online
58
+ (or directly: context-mcp-http --port 3100)
59
+ 2. Add http://localhost:3100 as a remote MCP connector in your AI client.
60
+ Use the CLIENT_ID and CLIENT_SECRET from ~/.context-mcp/contextconfig.json
61
+
62
+ Examples:
63
+ context-mcp-http
64
+ context-mcp-http --port 4000 --access-git
65
+ context-mcp-http --host 0.0.0.0 --port 3100
66
+ context-mcp-http --data-dir /my/project/.ctx
67
+ `);
68
+ process.exit(0);
69
+ }
70
+
71
+ function _getFlag(flag, defaultVal) {
72
+ const idx = _args.indexOf(flag);
73
+ if (idx !== -1 && _args[idx + 1] && !_args[idx + 1].startsWith('--')) return _args[idx + 1];
74
+ return defaultVal;
75
+ }
76
+
77
+ if (_args.includes('--access-git')) process.env.CONTEXT_MCP_ACCESS_GIT = 'true';
78
+ const _dataDirIdx = _args.indexOf('--data-dir');
79
+ if (_dataDirIdx !== -1 && _args[_dataDirIdx + 1]) process.env.CONTEXT_MCP_DIR = _args[_dataDirIdx + 1];
80
+
81
+ // ── Load Config ──────────────────────────────────────────────────────────────
82
+
83
+ const config = getConfig();
84
+
85
+ // ── Config ───────────────────────────────────────────────────────────────────
86
+
87
+ const PORT = Number(_getFlag('--port', null)) || config.port || 3100;
88
+ const HOST = _getFlag('--host', null) || config.host || 'localhost';
89
+ const CLIENT_ID = config.client_id || 'context-mcp';
90
+ const CLIENT_SECRET = config.client_secret || '';
91
+
92
+ // ── Validate credentials ────────────────────────────────────────────────────
93
+
94
+ if (!CLIENT_ID || !CLIENT_SECRET) {
95
+ console.error(`
96
+ \x1b[31m✗ ERROR: CLIENT_ID and CLIENT_SECRET are required.\x1b[0m
97
+
98
+ These are managed automatically via contextconfig.json
99
+ Location: ${getConfigPath()}
100
+
101
+ The server auto-generates credentials on first run.
102
+ Check the config file for your CLIENT_ID and CLIENT_SECRET.
103
+ `);
104
+ process.exit(1);
105
+ }
106
+
107
+ // ── OAuth 2.0 token store ────────────────────────────────────────────────────
108
+
109
+ const activeTokens = new Map(); // token -> { expiresAt }
110
+ const authCodes = new Map(); // code -> { code_challenge, redirect_uri, expiresAt }
111
+ const TOKEN_TTL = 3600 * 1000; // 1 hour
112
+ const CODE_TTL = 5 * 60 * 1000; // 5 minutes
113
+
114
+ function issueToken() {
115
+ return issueJWT({ sub: CLIENT_ID, scope: 'mcp' }, CLIENT_SECRET, TOKEN_TTL / 1000);
116
+ }
117
+
118
+ function isValidToken(token) {
119
+ // Try JWT validation first (stateless)
120
+ if (token.includes('.')) {
121
+ return verifyJWT(token, CLIENT_SECRET) !== null;
122
+ }
123
+ // Fallback: opaque UUID in map
124
+ const entry = activeTokens.get(token);
125
+ if (!entry) return false;
126
+ if (Date.now() > entry.expiresAt) { activeTokens.delete(token); return false; }
127
+ return true;
128
+ }
129
+
130
+ // Clean expired tokens & codes every 10 minutes
131
+ setInterval(() => {
132
+ const now = Date.now();
133
+ for (const [token, { expiresAt }] of activeTokens) {
134
+ if (now > expiresAt) activeTokens.delete(token);
135
+ }
136
+ for (const [code, { expiresAt }] of authCodes) {
137
+ if (now > expiresAt) authCodes.delete(code);
138
+ }
139
+ }, 600_000);
140
+
141
+
142
+
143
+ // ── HMAC request signing ─────────────────────────────────────────────────────
144
+
145
+ const HMAC_WINDOW_MS = 30_000;
146
+
147
+ function makeHmacSignature(secret, timestampMs, body) {
148
+ return createHmac('sha256', secret)
149
+ .update(`${timestampMs}:${body}`)
150
+ .digest('hex');
151
+ }
152
+
153
+ function verifyHmac(secret, req, body) {
154
+ const ts = req.headers['x-timestamp'];
155
+ const sig = req.headers['x-signature'];
156
+ if (!ts || !sig) return false;
157
+ const now = Date.now();
158
+ if (Math.abs(now - Number(ts)) > HMAC_WINDOW_MS) return false;
159
+ const expected = makeHmacSignature(secret, ts, body);
160
+ try {
161
+ return timingSafeEqual(Buffer.from(sig, 'hex'), Buffer.from(expected, 'hex'));
162
+ } catch { return false; }
163
+ }
164
+
165
+ // ── JWT access token validation ──────────────────────────────────────────────
166
+
167
+ function decodeJWT(token) {
168
+ try {
169
+ const [, payload] = token.split('.');
170
+ return JSON.parse(Buffer.from(payload, 'base64url').toString());
171
+ } catch { return null; }
172
+ }
173
+
174
+ function verifyJWT(token, secret) {
175
+ const parts = token.split('.');
176
+ if (parts.length !== 3) return null;
177
+ const sig = createHmac('sha256', secret).update(`${parts[0]}.${parts[1]}`).digest('base64url');
178
+ try {
179
+ if (!timingSafeEqual(Buffer.from(sig), Buffer.from(parts[2]))) return null;
180
+ } catch { return null; }
181
+ const payload = decodeJWT(token);
182
+ if (!payload) return null;
183
+ if (payload.exp && Date.now() / 1000 > payload.exp) return null;
184
+ return payload;
185
+ }
186
+
187
+ function issueJWT(payload, secret, ttlSeconds = 3600) {
188
+ const header = Buffer.from(JSON.stringify({ alg: 'HS256', typ: 'JWT' })).toString('base64url');
189
+ const body = Buffer.from(JSON.stringify({ ...payload, iat: Math.floor(Date.now() / 1000), exp: Math.floor(Date.now() / 1000) + ttlSeconds })).toString('base64url');
190
+ const sig = createHmac('sha256', secret).update(`${header}.${body}`).digest('base64url');
191
+ return `${header}.${body}.${sig}`;
192
+ }
193
+
194
+ // ── MCP session management ───────────────────────────────────────────────────
195
+
196
+ const sessions = new Map();
197
+
198
+ async function createMCPSession() {
199
+ const id = randomUUID();
200
+ const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => id });
201
+ const server = createServer({
202
+ enableFileTools: true,
203
+ enableGitTools: process.env.CONTEXT_MCP_ACCESS_GIT === 'true' || config.access_git === true,
204
+ });
205
+ await server.connect(transport);
206
+ sessions.set(id, { transport, server });
207
+ transport.onclose = () => sessions.delete(id);
208
+ return { transport, server };
209
+ }
210
+
211
+ // ── Helpers ──────────────────────────────────────────────────────────────────
212
+
213
+ const ALLOWED_ORIGINS = [
214
+ 'https://claude.ai',
215
+ `http://localhost:${PORT}`,
216
+ `http://127.0.0.1:${PORT}`,
217
+ ...(config.allowed_origins || []),
218
+ ];
219
+
220
+ function corsHeaders(reqOrigin) {
221
+ const origin = ALLOWED_ORIGINS.includes(reqOrigin) ? reqOrigin : ALLOWED_ORIGINS[0];
222
+ return {
223
+ 'Access-Control-Allow-Origin': origin,
224
+ 'Vary': 'Origin',
225
+ 'Access-Control-Allow-Methods': 'GET, POST, DELETE, OPTIONS',
226
+ 'Access-Control-Allow-Headers': 'Content-Type, Authorization, Mcp-Session-Id, Accept',
227
+ 'Access-Control-Expose-Headers': 'Mcp-Session-Id',
228
+ 'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',
229
+ };
230
+ }
231
+
232
+ function sendJSON(res, statusCode, data, reqOrigin) {
233
+ res.writeHead(statusCode, { ...corsHeaders(reqOrigin), 'Content-Type': 'application/json' });
234
+ res.end(JSON.stringify(data));
235
+ }
236
+
237
+ // Simple in-memory rate limiter: ip -> { count, resetAt }
238
+ const _rateLimits = new Map();
239
+ function checkRate(ip, limit, windowMs) {
240
+ const now = Date.now();
241
+ let e = _rateLimits.get(ip) ?? { count: 0, resetAt: now + windowMs };
242
+ if (now > e.resetAt) e = { count: 0, resetAt: now + windowMs };
243
+ e.count++;
244
+ _rateLimits.set(ip, e);
245
+ return e.count <= limit;
246
+ }
247
+
248
+ const MAX_BODY_BYTES = 10 * 1024 * 1024; // 10 MB
249
+
250
+ async function readBody(req) {
251
+ const chunks = [];
252
+ let totalBytes = 0;
253
+ for await (const chunk of req) {
254
+ totalBytes += chunk.length;
255
+ if (totalBytes > MAX_BODY_BYTES) {
256
+ req.destroy();
257
+ throw new Error('Request body too large (max 10 MB)');
258
+ }
259
+ chunks.push(chunk);
260
+ }
261
+ return Buffer.concat(chunks).toString();
262
+ }
263
+
264
+ // ── Request handler ──────────────────────────────────────────────────────────
265
+
266
+ async function handleRequest(req, res) {
267
+ const url = new URL(req.url, `http://${HOST}:${PORT}`);
268
+
269
+ const reqOrigin = req.headers['origin'] || '';
270
+ const clientIp = req.socket?.remoteAddress || 'unknown';
271
+
272
+ // CORS preflight
273
+ if (req.method === 'OPTIONS') {
274
+ res.writeHead(204, corsHeaders(reqOrigin));
275
+ res.end();
276
+ return;
277
+ }
278
+
279
+ // Health check (public)
280
+ if (url.pathname === '/health' && req.method === 'GET') {
281
+ sendJSON(res, 200, { status: 'ok', sessions: sessions.size }, reqOrigin);
282
+ return;
283
+ }
284
+
285
+ // ── OAuth 2.0 Authorization endpoint ──
286
+ if (url.pathname === '/authorize' && req.method === 'GET') {
287
+ const response_type = url.searchParams.get('response_type');
288
+ const client_id = url.searchParams.get('client_id');
289
+ const redirect_uri = url.searchParams.get('redirect_uri');
290
+ const state = url.searchParams.get('state');
291
+ const code_challenge = url.searchParams.get('code_challenge');
292
+
293
+ if (response_type !== 'code') {
294
+ sendJSON(res, 400, { error: 'unsupported_response_type', error_description: 'Only response_type=code is supported' }, reqOrigin);
295
+ return;
296
+ }
297
+
298
+ if (client_id !== CLIENT_ID) {
299
+ sendJSON(res, 401, { error: 'invalid_client' }, reqOrigin);
300
+ return;
301
+ }
302
+
303
+ if (!redirect_uri) {
304
+ sendJSON(res, 400, { error: 'invalid_request', error_description: 'redirect_uri is required' }, reqOrigin);
305
+ return;
306
+ }
307
+
308
+ // Validate redirect_uri against whitelist to prevent open redirect attacks
309
+ const allowedRedirectUris = config.allowed_redirect_uris ?? ['https://claude.ai'];
310
+ if (!allowedRedirectUris.some(u => redirect_uri.startsWith(u))) {
311
+ sendJSON(res, 400, { error: 'invalid_request', error_description: 'redirect_uri not allowed' }, reqOrigin);
312
+ return;
313
+ }
314
+
315
+ // Generate an authorization code
316
+ const code = randomUUID();
317
+ authCodes.set(code, {
318
+ code_challenge,
319
+ redirect_uri,
320
+ expiresAt: Date.now() + CODE_TTL
321
+ });
322
+
323
+ // Auto-redirect back to the client (e.g. Claude.ai)
324
+ const redirectUrl = new URL(redirect_uri);
325
+ redirectUrl.searchParams.set('code', code);
326
+ if (state) redirectUrl.searchParams.set('state', state);
327
+
328
+ res.writeHead(302, { Location: redirectUrl.toString() });
329
+ res.end();
330
+ return;
331
+ }
332
+
333
+ // ── OAuth 2.0 token endpoint ──
334
+ if ((url.pathname === '/oauth/token' || url.pathname === '/token') && req.method === 'POST') {
335
+ // Rate limit: 10 requests per minute per IP
336
+ if (!checkRate(clientIp, 10, 60_000)) {
337
+ sendJSON(res, 429, { error: 'rate_limit_exceeded', error_description: 'Too many token requests' }, reqOrigin);
338
+ return;
339
+ }
340
+
341
+ const bodyStr = await readBody(req);
342
+ let params;
343
+
344
+ const ct = req.headers['content-type'] || '';
345
+
346
+ if (ct.includes('application/json')) {
347
+ try { params = JSON.parse(bodyStr); } catch { params = {}; }
348
+ } else {
349
+ params = Object.fromEntries(new URLSearchParams(bodyStr));
350
+ }
351
+
352
+ // Also check Basic Auth in headers just in case Claude is using it
353
+ let clientId = params.client_id;
354
+ let clientSecret = params.client_secret;
355
+ const authHeader = req.headers['authorization'];
356
+ if (authHeader && authHeader.startsWith('Basic ')) {
357
+ const b64 = authHeader.slice(6);
358
+ const [u, p] = Buffer.from(b64, 'base64').toString().split(':');
359
+ clientId = clientId || u;
360
+ clientSecret = clientSecret || p;
361
+ }
362
+
363
+ const { grant_type, code, code_verifier } = params;
364
+
365
+ if (clientId !== CLIENT_ID || clientSecret !== CLIENT_SECRET) {
366
+ sendJSON(res, 401, { error: 'invalid_client', error_description: 'Invalid client_id or client_secret' }, reqOrigin);
367
+ return;
368
+ }
369
+
370
+ if (grant_type === 'authorization_code') {
371
+ const codeEntry = authCodes.get(code);
372
+ if (!codeEntry || Date.now() > codeEntry.expiresAt) {
373
+ sendJSON(res, 400, { error: 'invalid_grant', error_description: 'Invalid or expired authorization code' }, reqOrigin);
374
+ return;
375
+ }
376
+
377
+ if (codeEntry.code_challenge && code_verifier) {
378
+ const hash = createHash('sha256').update(code_verifier).digest('base64')
379
+ .replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
380
+ if (hash !== codeEntry.code_challenge) {
381
+ sendJSON(res, 400, { error: 'invalid_grant', error_description: 'PKCE verification failed' }, reqOrigin);
382
+ return;
383
+ }
384
+ }
385
+
386
+ authCodes.delete(code);
387
+ } else if (grant_type !== 'client_credentials') {
388
+ sendJSON(res, 400, { error: 'unsupported_grant_type', error_description: 'Unsupported grant type' }, reqOrigin);
389
+ return;
390
+ }
391
+
392
+ const token = issueToken();
393
+ sendJSON(res, 200, {
394
+ access_token: token,
395
+ token_type: 'Bearer',
396
+ expires_in: TOKEN_TTL / 1000,
397
+ });
398
+ return;
399
+ }
400
+
401
+ // ── OAuth discovery (well-known) ──
402
+ if (url.pathname === '/.well-known/oauth-authorization-server' && req.method === 'GET') {
403
+ const base = `${req.headers['x-forwarded-proto'] || 'https'}://${req.headers.host || `${HOST}:${PORT}`}`;
404
+ sendJSON(res, 200, {
405
+ issuer: base,
406
+ authorization_endpoint: `${base}/authorize`,
407
+ token_endpoint: `${base}/oauth/token`,
408
+ grant_types_supported: ['client_credentials', 'authorization_code'],
409
+ response_types_supported: ['code'],
410
+ code_challenge_methods_supported: ['S256'],
411
+ token_endpoint_auth_methods_supported: ['client_secret_post', 'client_secret_basic'],
412
+ }, reqOrigin);
413
+ return;
414
+ }
415
+
416
+ // User-friendly HTML guide for the root route (GET only)
417
+ if (url.pathname === '/' && req.method === 'GET') {
418
+ const html = `
419
+ <!DOCTYPE html>
420
+ <html lang="en">
421
+ <head>
422
+ <meta charset="UTF-8">
423
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
424
+ <title>context-mcp | Secure Transport</title>
425
+ <link rel="preconnect" href="https://fonts.googleapis.com">
426
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
427
+ <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600&family=JetBrains+Mono&display=swap" rel="stylesheet">
428
+ <style>
429
+ :root {
430
+ --bg: #05070a;
431
+ --card: rgba(15, 18, 25, 0.8);
432
+ --brand: #00f2ff;
433
+ --accent: #bc13fe;
434
+ --text: #e2e8f0;
435
+ --muted: #64748b;
436
+ --success: #10b981;
437
+ --glass: rgba(255, 255, 255, 0.03);
438
+ --border: rgba(255, 255, 255, 0.1);
439
+ }
440
+
441
+ * { box-sizing: border-box; }
442
+ body {
443
+ font-family: 'Outfit', sans-serif;
444
+ background: var(--bg);
445
+ background-image:
446
+ radial-gradient(circle at 20% 30%, rgba(0, 242, 255, 0.05) 0%, transparent 40%),
447
+ radial-gradient(circle at 80% 70%, rgba(188, 19, 254, 0.05) 0%, transparent 40%);
448
+ color: var(--text);
449
+ margin: 0;
450
+ display: flex;
451
+ align-items: center;
452
+ justify-content: center;
453
+ min-height: 100vh;
454
+ line-height: 1.6;
455
+ overflow-x: hidden;
456
+ }
457
+
458
+ .container {
459
+ width: 100%;
460
+ max-width: 680px;
461
+ padding: 40px;
462
+ position: relative;
463
+ }
464
+
465
+ .glass-card {
466
+ background: var(--card);
467
+ backdrop-filter: blur(12px);
468
+ -webkit-backdrop-filter: blur(12px);
469
+ border: 1px solid var(--border);
470
+ border-radius: 24px;
471
+ padding: 48px;
472
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
473
+ position: relative;
474
+ z-index: 1;
475
+ overflow: hidden;
476
+ }
477
+
478
+ .glass-card::before {
479
+ content: '';
480
+ position: absolute;
481
+ top: 0; left: 0; right: 0; height: 2px;
482
+ background: linear-gradient(90deg, transparent, var(--brand), var(--accent), transparent);
483
+ opacity: 0.5;
484
+ }
485
+
486
+ .logo-area {
487
+ display: flex;
488
+ align-items: center;
489
+ gap: 16px;
490
+ margin-bottom: 32px;
491
+ }
492
+
493
+ .status-pulse {
494
+ width: 12px;
495
+ height: 12px;
496
+ background: var(--success);
497
+ border-radius: 50%;
498
+ position: relative;
499
+ box-shadow: 0 0 15px var(--success);
500
+ }
501
+
502
+ .status-pulse::after {
503
+ content: '';
504
+ position: absolute;
505
+ top: -4px; left: -4px; right: -4px; bottom: -4px;
506
+ border: 2px solid var(--success);
507
+ border-radius: 50%;
508
+ animation: pulse 2s infinite;
509
+ }
510
+
511
+ @keyframes pulse {
512
+ 0% { transform: scale(1); opacity: 0.8; }
513
+ 100% { transform: scale(2); opacity: 0; }
514
+ }
515
+
516
+ h1 {
517
+ font-size: 32px;
518
+ font-weight: 600;
519
+ margin: 0;
520
+ background: linear-gradient(135deg, #fff 0%, #94a3b8 100%);
521
+ -webkit-background-clip: text;
522
+ -webkit-text-fill-color: transparent;
523
+ letter-spacing: -0.02em;
524
+ }
525
+
526
+ p { color: var(--muted); font-size: 17px; margin-bottom: 32px; }
527
+
528
+ .setup-grid {
529
+ display: grid;
530
+ gap: 20px;
531
+ margin-top: 40px;
532
+ }
533
+
534
+ .step {
535
+ background: var(--glass);
536
+ border: 1px solid var(--border);
537
+ border-radius: 16px;
538
+ padding: 20px;
539
+ transition: all 0.3s ease;
540
+ }
541
+
542
+ .step:hover {
543
+ border-color: rgba(0, 242, 255, 0.3);
544
+ transform: translateY(-2px);
545
+ background: rgba(255, 255, 255, 0.05);
546
+ }
547
+
548
+ .step-num {
549
+ display: inline-block;
550
+ width: 24px;
551
+ height: 24px;
552
+ background: var(--brand);
553
+ color: var(--bg);
554
+ border-radius: 6px;
555
+ text-align: center;
556
+ font-size: 14px;
557
+ font-weight: 600;
558
+ line-height: 24px;
559
+ margin-bottom: 12px;
560
+ }
561
+
562
+ .step-title {
563
+ font-weight: 600;
564
+ color: var(--text);
565
+ margin-bottom: 8px;
566
+ display: block;
567
+ }
568
+
569
+ .step-content {
570
+ font-size: 14px;
571
+ color: var(--muted);
572
+ }
573
+
574
+ code {
575
+ font-family: 'JetBrains Mono', monospace;
576
+ background: rgba(0, 0, 0, 0.3);
577
+ color: var(--brand);
578
+ padding: 4px 8px;
579
+ border-radius: 6px;
580
+ font-size: 13px;
581
+ border: 1px solid rgba(0, 242, 255, 0.1);
582
+ }
583
+
584
+ .badge {
585
+ display: inline-flex;
586
+ align-items: center;
587
+ background: rgba(16, 185, 129, 0.1);
588
+ color: var(--success);
589
+ padding: 4px 12px;
590
+ border-radius: 20px;
591
+ font-size: 12px;
592
+ font-weight: 600;
593
+ margin-left: auto;
594
+ }
595
+
596
+ footer {
597
+ margin-top: 32px;
598
+ text-align: center;
599
+ font-size: 13px;
600
+ color: var(--muted);
601
+ }
602
+ </style>
603
+ </head>
604
+ <body>
605
+ <div class="container">
606
+ <div class="glass-card">
607
+ <div class="logo-area">
608
+ <div class="status-pulse"></div>
609
+ <h1>context-mcp</h1>
610
+ <div class="badge">HTTP LOCAL</div>
611
+ </div>
612
+
613
+ <p>Your AI memory server is running and ready for connections from Claude.ai or ChatGPT.</p>
614
+
615
+ <div class="setup-grid">
616
+ <div class="step">
617
+ <span class="step-num">1</span>
618
+ <span class="step-title">Add MCP Connector</span>
619
+ <span class="step-content">Go to your AI client → Settings → Integrations → <b>Add MCP Connector</b>.</span>
620
+ </div>
621
+
622
+ <div class="step">
623
+ <span class="step-num">2</span>
624
+ <span class="step-title">Server URL</span>
625
+ <span class="step-content">Enter: <code>${req.headers['x-forwarded-proto'] || req.socket?.encrypted ? 'https' : 'http'}://${req.headers.host}</code></span>
626
+ </div>
627
+
628
+ <div class="step">
629
+ <span class="step-num">3</span>
630
+ <span class="step-title">Credentials</span>
631
+ <span class="step-content">
632
+ Client ID &amp; Secret: see <code>~/.context-mcp/contextconfig.json</code>
633
+ </span>
634
+ </div>
635
+ </div>
636
+ </div>
637
+ <footer>
638
+ &copy; ${new Date().getFullYear()} context-mcp • Premium AI Memory
639
+ </footer>
640
+ </div>
641
+ </body>
642
+ </html>
643
+ `;
644
+ res.writeHead(200, { 'Content-Type': 'text/html' });
645
+ res.end(html);
646
+ return;
647
+ }
648
+
649
+ // Allow both /mcp and / to handle MCP requests (but not GET / — that serves the HTML guide)
650
+ if (url.pathname !== '/mcp' && !(url.pathname === '/' && req.method !== 'GET')) {
651
+ sendJSON(res, 404, { error: `Not found: ${url.pathname}` }, reqOrigin);
652
+ return;
653
+ }
654
+
655
+ // ── Auth: validate Bearer token ──
656
+ const auth = req.headers['authorization'];
657
+ const token = auth?.startsWith('Bearer ') ? auth.slice(7) : null;
658
+ if (!token || !isValidToken(token)) {
659
+ sendJSON(res, 401, { error: 'invalid_token', error_description: 'Missing or expired token. POST /oauth/token first.' }, reqOrigin);
660
+ return;
661
+ }
662
+
663
+ const sessionId = req.headers['mcp-session-id'] || null;
664
+
665
+ try {
666
+ if (req.method === 'POST') {
667
+ const bodyStr = await readBody(req);
668
+ let body;
669
+ try { body = JSON.parse(bodyStr); } catch { body = null; }
670
+
671
+ if (body && isInitializeRequest(body)) {
672
+ const { transport } = await createMCPSession();
673
+ await transport.handleRequest(req, res, body);
674
+ return;
675
+ }
676
+
677
+ if (!sessionId || !sessions.has(sessionId)) {
678
+ sendJSON(res, 400, { error: 'Missing or invalid Mcp-Session-Id.' }, reqOrigin);
679
+ return;
680
+ }
681
+ const { transport } = sessions.get(sessionId);
682
+ await transport.handleRequest(req, res, body);
683
+
684
+ } else if (req.method === 'GET') {
685
+ if (!sessionId || !sessions.has(sessionId)) {
686
+ sendJSON(res, 400, { error: 'Missing or invalid Mcp-Session-Id' }, reqOrigin);
687
+ return;
688
+ }
689
+ const { transport } = sessions.get(sessionId);
690
+ await transport.handleRequest(req, res);
691
+
692
+ } else if (req.method === 'DELETE') {
693
+ if (sessionId && sessions.has(sessionId)) {
694
+ const { transport } = sessions.get(sessionId);
695
+ await transport.close();
696
+ sessions.delete(sessionId);
697
+ }
698
+ sendJSON(res, 200, { closed: true }, reqOrigin);
699
+
700
+ } else {
701
+ sendJSON(res, 405, { error: 'Method not allowed' }, reqOrigin);
702
+ }
703
+ } catch (err) {
704
+ console.error('MCP HTTP error:', err.message);
705
+ if (!res.headersSent) {
706
+ sendJSON(res, 500, { error: err.message }, reqOrigin);
707
+ }
708
+ }
709
+ }
710
+
711
+ // ── Start server ─────────────────────────────────────────────────────────────
712
+
713
+ async function start() {
714
+ const server = createHTTPServer(handleRequest);
715
+
716
+ server.listen(PORT, async () => {
717
+ const LOGO = `
718
+ \x1b[96m ██████╗ ██████╗ ███╗ ██╗████████╗███████╗██╗ ██╗████████╗
719
+ ██╔════╝██╔═══██╗████╗ ██║╚══██╔══╝██╔════╝╚██╗██╔╝╚══██╔══╝
720
+ ██║ ██║ ██║██╔██╗ ██║ ██║ █████╗ ╚███╔╝ ██║
721
+ ██║ ██║ ██║██║╚██╗██║ ██║ ██╔══╝ ██╔██╗ ██║
722
+ ╚██████╗╚██████╔╝██║ ╚████║ ██║ ███████╗██╔╝ ██╗ ██║
723
+ ╚═════╝ ╚═════╝ ╚═╝ ╚═══╝ ╚═╝ ╚══════╝╚═╝ ╚═╝ ╚═╝\x1b[0m`;
724
+
725
+ console.log(LOGO);
726
+ console.log(`
727
+ \x1b[90m┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓\x1b[0m
728
+ \x1b[90m┃\x1b[0m \x1b[1m\x1b[95mcontext-mcp Web AI Server\x1b[0m \x1b[90m┃\x1b[0m
729
+ \x1b[90m┃\x1b[0m \x1b[90m┃\x1b[0m
730
+ \x1b[90m┃\x1b[0m \x1b[2mEndpoint:\x1b[0m \x1b[96m${'http://' + HOST + ':' + PORT}\x1b[0m \x1b[90m┃\x1b[0m
731
+ \x1b[90m┃\x1b[0m \x1b[2mConfig:\x1b[0m \x1b[2m${getConfigPath().padEnd(36)}\x1b[0m\x1b[90m┃\x1b[0m
732
+ \x1b[90m┃\x1b[0m \x1b[2mMCP path:\x1b[0m \x1b[94mPOST /mcp\x1b[0m \x1b[90m┃\x1b[0m
733
+ \x1b[90m┃\x1b[0m \x1b[2mOAuth:\x1b[0m \x1b[94mPOST /oauth/token\x1b[0m \x1b[90m┃\x1b[0m
734
+ \x1b[90m┃\x1b[0m \x1b[2mHealth:\x1b[0m \x1b[94mGET /health\x1b[0m \x1b[90m┃\x1b[0m
735
+ \x1b[90m┃\x1b[0m \x1b[90m┃\x1b[0m
736
+ \x1b[90m┃\x1b[0m \x1b[2mAuth:\x1b[0m 🔒 Client Credentials + OAuth 2.0 \x1b[90m┃\x1b[0m
737
+ \x1b[90m┃\x1b[0m \x1b[90m┃\x1b[0m
738
+ \x1b[90m┃\x1b[0m \x1b[2mClient ID:\x1b[0m \x1b[92m${CLIENT_ID.padEnd(34)}\x1b[0m\x1b[90m┃\x1b[0m
739
+ \x1b[90m┃\x1b[0m \x1b[2mSecret:\x1b[0m \x1b[2m${CLIENT_SECRET.slice(0, 8)}... (see config)\x1b[0m\x1b[90m┃\x1b[0m
740
+ \x1b[90m┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛\x1b[0m
741
+ `);
742
+
743
+ });
744
+
745
+ // Graceful shutdown
746
+ const shutdown = async (signal) => {
747
+ console.log(`\n[http] Received ${signal}, shutting down...`);
748
+
749
+ server.close(() => {
750
+ console.log('[http] Server closed');
751
+ process.exit(0);
752
+ });
753
+
754
+ // Force shutdown after 10s
755
+ setTimeout(() => {
756
+ console.error('[http] Forced shutdown');
757
+ process.exit(1);
758
+ }, 10000);
759
+ };
760
+
761
+ process.on('SIGTERM', () => shutdown('SIGTERM'));
762
+ process.on('SIGINT', () => shutdown('SIGINT'));
763
+ }
764
+
765
+ start();