regen-koi-mcp 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +170 -64
- package/dist/auth-store.d.ts +47 -0
- package/dist/auth-store.d.ts.map +1 -0
- package/dist/auth-store.js +86 -0
- package/dist/auth-store.js.map +1 -0
- package/dist/auth.d.ts +33 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +85 -0
- package/dist/auth.js.map +1 -0
- package/dist/graph_tool.d.ts.map +1 -1
- package/dist/graph_tool.js +16 -20
- package/dist/graph_tool.js.map +1 -1
- package/dist/index.js +166 -280
- package/dist/index.js.map +1 -1
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +6 -27
- package/dist/tools.js.map +1 -1
- package/package.json +1 -1
- package/dist/hybrid-client.d.ts +0 -61
- package/dist/hybrid-client.d.ts.map +0 -1
- package/dist/hybrid-client.js +0 -303
- package/dist/hybrid-client.js.map +0 -1
- package/dist/query_router.d.ts +0 -81
- package/dist/query_router.d.ts.map +0 -1
- package/dist/query_router.js +0 -205
- package/dist/query_router.js.map +0 -1
- package/dist/unified_search.d.ts +0 -109
- package/dist/unified_search.d.ts.map +0 -1
- package/dist/unified_search.js +0 -352
- package/dist/unified_search.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -19,15 +19,14 @@ import dotenv from 'dotenv';
|
|
|
19
19
|
import { TOOLS } from './tools.js';
|
|
20
20
|
// Use enhanced SPARQL client with focused retrieval
|
|
21
21
|
import { SPARQLClient } from './sparql-client-enhanced.js';
|
|
22
|
-
import HybridSearchClient from './hybrid-client.js';
|
|
23
|
-
import { QueryRouter } from './query_router.js';
|
|
24
|
-
import { UnifiedSearch } from './unified_search.js';
|
|
25
22
|
import { executeGraphTool } from './graph_tool.js';
|
|
26
23
|
// Production hardening modules
|
|
27
24
|
import { logger } from './logger.js';
|
|
28
25
|
import { recordQuery, getMetricsMarkdown, getMetricsSummary } from './metrics.js';
|
|
29
26
|
import { validateToolInput } from './validation.js';
|
|
30
27
|
import { queryCache } from './cache.js';
|
|
28
|
+
// Shared auth module
|
|
29
|
+
import { USER_EMAIL, getAccessToken, setAccessToken } from './auth.js';
|
|
31
30
|
// Load environment variables
|
|
32
31
|
dotenv.config();
|
|
33
32
|
// Configuration
|
|
@@ -35,54 +34,51 @@ const KOI_API_ENDPOINT = process.env.KOI_API_ENDPOINT || 'https://regen.gaiaai.x
|
|
|
35
34
|
const KOI_API_KEY = process.env.KOI_API_KEY || '';
|
|
36
35
|
const SERVER_NAME = process.env.MCP_SERVER_NAME || 'regen-koi';
|
|
37
36
|
const SERVER_VERSION = process.env.MCP_SERVER_VERSION || '1.0.0';
|
|
37
|
+
console.error(`[${SERVER_NAME}] User email for auth: ${USER_EMAIL}`);
|
|
38
|
+
// Check if user is authenticated (with caching)
|
|
39
|
+
async function isUserAuthenticated() {
|
|
40
|
+
const token = getAccessToken();
|
|
41
|
+
if (!token)
|
|
42
|
+
return false;
|
|
43
|
+
// Validate token with server
|
|
44
|
+
try {
|
|
45
|
+
const response = await axios.get(`${KOI_API_ENDPOINT}/auth/status`, {
|
|
46
|
+
headers: { 'Authorization': `Bearer ${token}` },
|
|
47
|
+
timeout: 5000
|
|
48
|
+
});
|
|
49
|
+
return response.data.authenticated || false;
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
38
55
|
// API client configuration
|
|
56
|
+
// SECURITY: Authorization header is set dynamically based on access token
|
|
39
57
|
const apiClient = axios.create({
|
|
40
58
|
baseURL: KOI_API_ENDPOINT,
|
|
41
59
|
timeout: 30000,
|
|
42
60
|
headers: {
|
|
43
61
|
'Content-Type': 'application/json',
|
|
44
|
-
|
|
62
|
+
'X-User-Email': USER_EMAIL, // Kept for logging purposes only, not for auth
|
|
45
63
|
}
|
|
46
64
|
});
|
|
65
|
+
// Add request interceptor to dynamically include access token
|
|
66
|
+
apiClient.interceptors.request.use((config) => {
|
|
67
|
+
const token = getAccessToken();
|
|
68
|
+
if (token && config.headers) {
|
|
69
|
+
config.headers['Authorization'] = `Bearer ${token}`;
|
|
70
|
+
}
|
|
71
|
+
return config;
|
|
72
|
+
});
|
|
47
73
|
// Tool definitions are imported from tools.ts
|
|
48
74
|
class KOIServer {
|
|
49
75
|
server;
|
|
50
76
|
sparqlClient;
|
|
51
|
-
hybridClient;
|
|
52
|
-
queryRouter = null;
|
|
53
|
-
unifiedSearch = null;
|
|
54
77
|
constructor() {
|
|
55
78
|
this.sparqlClient = new SPARQLClient();
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
if (process.env.GRAPH_DB_HOST && process.env.GRAPH_DB_NAME) {
|
|
60
|
-
this.queryRouter = new QueryRouter({
|
|
61
|
-
host: process.env.GRAPH_DB_HOST,
|
|
62
|
-
port: parseInt(process.env.GRAPH_DB_PORT || '5432'),
|
|
63
|
-
database: process.env.GRAPH_DB_NAME,
|
|
64
|
-
user: process.env.GRAPH_DB_USER,
|
|
65
|
-
password: process.env.GRAPH_DB_PASSWORD,
|
|
66
|
-
entitySimilarityThreshold: parseFloat(process.env.ENTITY_SIMILARITY_THRESHOLD || '0.15'),
|
|
67
|
-
});
|
|
68
|
-
this.unifiedSearch = new UnifiedSearch({
|
|
69
|
-
host: process.env.GRAPH_DB_HOST,
|
|
70
|
-
port: parseInt(process.env.GRAPH_DB_PORT || '5432'),
|
|
71
|
-
database: process.env.GRAPH_DB_NAME,
|
|
72
|
-
user: process.env.GRAPH_DB_USER,
|
|
73
|
-
password: process.env.GRAPH_DB_PASSWORD,
|
|
74
|
-
graphName: process.env.GRAPH_NAME || 'regen_graph',
|
|
75
|
-
embeddingDimension: parseInt(process.env.EMBEDDING_DIM || '1536'),
|
|
76
|
-
rrfConstant: parseInt(process.env.RRF_K || '60'),
|
|
77
|
-
});
|
|
78
|
-
console.error(`[${SERVER_NAME}] Initialized QueryRouter and UnifiedSearch`);
|
|
79
|
-
}
|
|
80
|
-
else {
|
|
81
|
-
console.error(`[${SERVER_NAME}] Graph database configuration not found - hybrid_search and query_code_graph tools will be unavailable`);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
catch (error) {
|
|
85
|
-
console.error(`[${SERVER_NAME}] Failed to initialize graph components:`, error);
|
|
79
|
+
// Check for graph database configuration (only for logging)
|
|
80
|
+
if (!(process.env.GRAPH_DB_HOST && process.env.GRAPH_DB_NAME)) {
|
|
81
|
+
console.error(`[${SERVER_NAME}] Graph database configuration not found - query_code_graph tool will rely on fallback`);
|
|
86
82
|
}
|
|
87
83
|
this.server = new Server({
|
|
88
84
|
name: SERVER_NAME,
|
|
@@ -121,7 +117,7 @@ class KOIServer {
|
|
|
121
117
|
}, `Executing tool: ${name}`);
|
|
122
118
|
try {
|
|
123
119
|
// Input validation for applicable tools
|
|
124
|
-
const validationRequired = ['
|
|
120
|
+
const validationRequired = ['search', 'search_github_docs', 'get_repo_overview', 'get_tech_stack', 'generate_weekly_digest'];
|
|
125
121
|
if (validationRequired.includes(name)) {
|
|
126
122
|
const validation = validateToolInput(name, args);
|
|
127
123
|
if (!validation.success) {
|
|
@@ -144,11 +140,8 @@ class KOIServer {
|
|
|
144
140
|
case 'query_code_graph':
|
|
145
141
|
result = await executeGraphTool(args);
|
|
146
142
|
break;
|
|
147
|
-
case '
|
|
148
|
-
result = await this.
|
|
149
|
-
break;
|
|
150
|
-
case 'search_knowledge':
|
|
151
|
-
result = await this.searchKnowledge(args);
|
|
143
|
+
case 'search':
|
|
144
|
+
result = await this.search(args);
|
|
152
145
|
break;
|
|
153
146
|
case 'get_stats':
|
|
154
147
|
result = await this.getStats(args);
|
|
@@ -372,8 +365,8 @@ class KOIServer {
|
|
|
372
365
|
return { content: [{ type: 'text', text: `Error: ${e instanceof Error ? e.message : String(e)}` }] };
|
|
373
366
|
}
|
|
374
367
|
}
|
|
375
|
-
async
|
|
376
|
-
const { query, limit =
|
|
368
|
+
async search(args) {
|
|
369
|
+
const { query, limit = 10, published_from, published_to, include_undated = false } = args || {};
|
|
377
370
|
const vectorFilters = {};
|
|
378
371
|
// Respect explicit date filter
|
|
379
372
|
if (published_from || published_to) {
|
|
@@ -395,30 +388,7 @@ class KOIServer {
|
|
|
395
388
|
if (include_undated) {
|
|
396
389
|
vectorFilters.include_undated = true;
|
|
397
390
|
}
|
|
398
|
-
//
|
|
399
|
-
if (useHybrid) {
|
|
400
|
-
try {
|
|
401
|
-
const results = await this.hybridClient.hybridSearch(query, {
|
|
402
|
-
sparqlLimit: limit * 2,
|
|
403
|
-
vectorLimit: limit,
|
|
404
|
-
fusionStrategy: 'rrf',
|
|
405
|
-
filters: vectorFilters
|
|
406
|
-
});
|
|
407
|
-
const formattedResults = this.hybridClient.formatResults(results);
|
|
408
|
-
return {
|
|
409
|
-
content: [
|
|
410
|
-
{
|
|
411
|
-
type: 'text',
|
|
412
|
-
text: formattedResults,
|
|
413
|
-
},
|
|
414
|
-
],
|
|
415
|
-
};
|
|
416
|
-
}
|
|
417
|
-
catch (error) {
|
|
418
|
-
console.error('Hybrid search failed, falling back to vector-only:', error);
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
// Fallback to original vector search
|
|
391
|
+
// Call the KOI API (which handles hybrid search with entity boosting)
|
|
422
392
|
try {
|
|
423
393
|
const body = { question: query, limit };
|
|
424
394
|
if (Object.keys(vectorFilters).length > 0)
|
|
@@ -616,16 +586,23 @@ class KOIServer {
|
|
|
616
586
|
async getStats(args) {
|
|
617
587
|
const { detailed = false } = args;
|
|
618
588
|
try {
|
|
619
|
-
const response = await apiClient.get('/
|
|
620
|
-
const
|
|
589
|
+
const response = await apiClient.get('/stats');
|
|
590
|
+
const stats = response.data;
|
|
621
591
|
let formatted = `# KOI Knowledge Base Statistics\n\n`;
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
formatted += `- **
|
|
625
|
-
formatted += `- **
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
592
|
+
// Main statistics
|
|
593
|
+
formatted += `- **Total Documents**: ${stats.total_documents?.toLocaleString() || 'Unknown'}\n`;
|
|
594
|
+
formatted += `- **Recent (7 days)**: ${stats.recent_7_days?.toLocaleString() || 'Unknown'}\n`;
|
|
595
|
+
formatted += `- **API Endpoint**: ${KOI_API_ENDPOINT}\n\n`;
|
|
596
|
+
// Source breakdown
|
|
597
|
+
if (stats.by_source && Object.keys(stats.by_source).length > 0) {
|
|
598
|
+
formatted += `## Documents by Source\n\n`;
|
|
599
|
+
// Sort sources by count (descending)
|
|
600
|
+
const sortedSources = Object.entries(stats.by_source)
|
|
601
|
+
.sort(([, a], [, b]) => b - a);
|
|
602
|
+
for (const [source, count] of sortedSources) {
|
|
603
|
+
formatted += `- **${source}**: ${count.toLocaleString()}\n`;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
629
606
|
return {
|
|
630
607
|
content: [
|
|
631
608
|
{
|
|
@@ -656,17 +633,13 @@ class KOIServer {
|
|
|
656
633
|
output += `\n## System Health\n\n`;
|
|
657
634
|
output += `- **Graph DB Configured:** ${!!(process.env.GRAPH_DB_HOST && process.env.GRAPH_DB_NAME)}\n`;
|
|
658
635
|
output += `- **KOI API Endpoint:** ${KOI_API_ENDPOINT}\n`;
|
|
659
|
-
output += `- **Query Router Available:** ${!!this.queryRouter}\n`;
|
|
660
|
-
output += `- **Unified Search Available:** ${!!this.unifiedSearch}\n`;
|
|
661
636
|
// Add raw JSON for programmatic access
|
|
662
637
|
const jsonData = JSON.stringify({
|
|
663
638
|
metrics: metricsSummary,
|
|
664
639
|
cache: cacheStats,
|
|
665
640
|
config: {
|
|
666
641
|
api_endpoint: KOI_API_ENDPOINT,
|
|
667
|
-
graph_db_configured: !!(process.env.GRAPH_DB_HOST && process.env.GRAPH_DB_NAME)
|
|
668
|
-
query_router_available: !!this.queryRouter,
|
|
669
|
-
unified_search_available: !!this.unifiedSearch
|
|
642
|
+
graph_db_configured: !!(process.env.GRAPH_DB_HOST && process.env.GRAPH_DB_NAME)
|
|
670
643
|
}
|
|
671
644
|
}, null, 2);
|
|
672
645
|
return {
|
|
@@ -921,7 +894,7 @@ class KOIServer {
|
|
|
921
894
|
catch (apiError) {
|
|
922
895
|
console.error(`[${SERVER_NAME}] API weekly-digest endpoint failed, using search fallback:`, apiError);
|
|
923
896
|
// Final fallback: use search
|
|
924
|
-
const searchResults = await this.
|
|
897
|
+
const searchResults = await this.search({
|
|
925
898
|
query: 'Regen Network activity updates discussions governance',
|
|
926
899
|
limit: 100,
|
|
927
900
|
published_from: startDate,
|
|
@@ -1443,67 +1416,132 @@ class KOIServer {
|
|
|
1443
1416
|
}
|
|
1444
1417
|
/**
|
|
1445
1418
|
* Authenticate user with @regen.network email for access to private documentation
|
|
1419
|
+
*
|
|
1420
|
+
* RFC 8628 Device Authorization Grant:
|
|
1421
|
+
* 1. Server generates device_code (secret) and user_code (public)
|
|
1422
|
+
* 2. User manually goes to verification_uri and enters user_code
|
|
1423
|
+
* 3. MCP polls for completion using device_code
|
|
1424
|
+
*
|
|
1425
|
+
* SECURITY: Prevents phishing because user must manually type code from their device.
|
|
1426
|
+
* Attacker cannot force victim to authorize attacker's device_code.
|
|
1446
1427
|
*/
|
|
1447
1428
|
async authenticateUser() {
|
|
1448
1429
|
const startTime = Date.now();
|
|
1449
1430
|
try {
|
|
1450
1431
|
console.error(`[${SERVER_NAME}] Tool=regen_koi_authenticate Event=start`);
|
|
1451
|
-
//
|
|
1452
|
-
const
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1432
|
+
// Load saved auth state from disk
|
|
1433
|
+
const { loadAuthState, saveAuthState, clearDeviceCode, hasValidAccessToken, hasValidDeviceCode } = await import('./auth-store.js');
|
|
1434
|
+
const state = loadAuthState();
|
|
1435
|
+
// Check 1: Already authenticated?
|
|
1436
|
+
if (hasValidAccessToken(state)) {
|
|
1437
|
+
console.error(`[${SERVER_NAME}] Tool=regen_koi_authenticate Event=already_authenticated User=${state.userEmail}`);
|
|
1438
|
+
return {
|
|
1439
|
+
content: [{
|
|
1440
|
+
type: 'text',
|
|
1441
|
+
text: `## Already Authenticated\n\nYou are already authenticated as **${state.userEmail}**.\n\nYour session is valid until ${new Date(state.accessTokenExpiresAt).toLocaleString()}.\n\n✅ You have access to private Regen Network documentation.`
|
|
1442
|
+
}]
|
|
1443
|
+
};
|
|
1463
1444
|
}
|
|
1464
|
-
//
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
await open(auth_url);
|
|
1468
|
-
let output = `## Authentication Started\n\n`;
|
|
1469
|
-
output += `✅ Opening browser for OAuth login...\n\n`;
|
|
1470
|
-
output += `**Please:**\n`;
|
|
1471
|
-
output += `1. Log in with your **@regen.network** email\n`;
|
|
1472
|
-
output += `2. Grant the requested permissions (email, profile)\n`;
|
|
1473
|
-
output += `3. The browser will show a success message when complete\n\n`;
|
|
1474
|
-
output += `**After authenticating:**\n`;
|
|
1475
|
-
output += `- Your token is saved on the server\n`;
|
|
1476
|
-
output += `- Future queries will automatically include private Drive data\n`;
|
|
1477
|
-
output += `- You won't need to authenticate again unless the token expires\n\n`;
|
|
1478
|
-
output += `**Polling for authentication completion...**\n`;
|
|
1479
|
-
// Poll for authentication status
|
|
1480
|
-
const pollUrl = `${KOI_API_ENDPOINT}/auth/status?user_email=${encodeURIComponent(userEmail)}`;
|
|
1481
|
-
const maxAttempts = 60; // 2 minutes
|
|
1482
|
-
const pollInterval = 2000; // 2 seconds
|
|
1483
|
-
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
1484
|
-
await new Promise(resolve => setTimeout(resolve, pollInterval));
|
|
1445
|
+
// Check 2: Have pending device code? Check its status
|
|
1446
|
+
if (hasValidDeviceCode(state)) {
|
|
1447
|
+
console.error(`[${SERVER_NAME}] Tool=regen_koi_authenticate Event=check_status UserCode=${state.userCode}`);
|
|
1485
1448
|
try {
|
|
1486
|
-
const
|
|
1487
|
-
|
|
1449
|
+
const tokenResponse = await axios.post(`${KOI_API_ENDPOINT}/auth/token`, {
|
|
1450
|
+
device_code: state.deviceCode,
|
|
1451
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:device_code'
|
|
1452
|
+
});
|
|
1453
|
+
const data = tokenResponse.data;
|
|
1454
|
+
// Still pending?
|
|
1455
|
+
if (data.error === 'authorization_pending') {
|
|
1456
|
+
const expiresInMin = Math.floor((state.deviceCodeExpiresAt - Date.now()) / 60000);
|
|
1457
|
+
const ACTIVATION_URL = 'https://regen.gaiaai.xyz/activate';
|
|
1458
|
+
return {
|
|
1459
|
+
content: [{
|
|
1460
|
+
type: 'text',
|
|
1461
|
+
text: `## Authentication Pending\n\n**Still waiting for you to complete authentication.**\n\n### Instructions:\n\n1. Go to: [${ACTIVATION_URL}](${ACTIVATION_URL})\n2. Enter code: **\`${state.userCode}\`**\n3. Sign in with your **@regen.network** email\n\n---\n\n*Code expires in ${expiresInMin} minutes.*\n\n**After completing authentication, run this tool again to retrieve your session token.**`
|
|
1462
|
+
}]
|
|
1463
|
+
};
|
|
1464
|
+
}
|
|
1465
|
+
// Expired or other error?
|
|
1466
|
+
if (data.error) {
|
|
1467
|
+
// Clear expired device code
|
|
1468
|
+
saveAuthState(clearDeviceCode(state));
|
|
1469
|
+
if (data.error === 'expired_token') {
|
|
1470
|
+
return {
|
|
1471
|
+
content: [{
|
|
1472
|
+
type: 'text',
|
|
1473
|
+
text: `## Authentication Expired\n\nYour authentication code has expired.\n\n**Run this tool again to get a new code.**`
|
|
1474
|
+
}]
|
|
1475
|
+
};
|
|
1476
|
+
}
|
|
1477
|
+
if (data.error === 'access_denied') {
|
|
1478
|
+
return {
|
|
1479
|
+
content: [{
|
|
1480
|
+
type: 'text',
|
|
1481
|
+
text: `## Access Denied\n\n${data.error_description || 'Only @regen.network email addresses are permitted.'}`
|
|
1482
|
+
}]
|
|
1483
|
+
};
|
|
1484
|
+
}
|
|
1485
|
+
throw new Error(data.error_description || data.error);
|
|
1486
|
+
}
|
|
1487
|
+
// Success!
|
|
1488
|
+
if (data.access_token) {
|
|
1488
1489
|
console.error(`[${SERVER_NAME}] Tool=regen_koi_authenticate Event=success Duration=${Date.now() - startTime}ms`);
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1490
|
+
// Save token to file and in-memory cache
|
|
1491
|
+
const tokenExpiry = data.expires_in
|
|
1492
|
+
? Date.now() + (data.expires_in * 1000)
|
|
1493
|
+
: Date.now() + 3600000; // Default 1 hour
|
|
1494
|
+
setAccessToken(data.access_token, tokenExpiry);
|
|
1495
|
+
saveAuthState({
|
|
1496
|
+
accessToken: data.access_token,
|
|
1497
|
+
accessTokenExpiresAt: tokenExpiry,
|
|
1498
|
+
userEmail: data.email
|
|
1499
|
+
});
|
|
1493
1500
|
return {
|
|
1494
1501
|
content: [{
|
|
1495
1502
|
type: 'text',
|
|
1496
|
-
text:
|
|
1503
|
+
text: `## ✅ Authentication Successful!\n\nYou now have access to internal Regen Network documentation.\n\nPrivate Notion data from the main Regen workspace is now accessible.\n\n**Session expires:** ${new Date(tokenExpiry).toLocaleString()}`
|
|
1497
1504
|
}]
|
|
1498
1505
|
};
|
|
1499
1506
|
}
|
|
1500
1507
|
}
|
|
1501
|
-
catch (
|
|
1502
|
-
|
|
1508
|
+
catch (checkError) {
|
|
1509
|
+
console.error(`[${SERVER_NAME}] Error checking auth status:`, checkError);
|
|
1510
|
+
// Clear device code and let user try again
|
|
1511
|
+
saveAuthState(clearDeviceCode(state));
|
|
1512
|
+
throw checkError;
|
|
1503
1513
|
}
|
|
1504
1514
|
}
|
|
1505
|
-
//
|
|
1506
|
-
|
|
1515
|
+
// Check 3: No state - start new auth flow
|
|
1516
|
+
console.error(`[${SERVER_NAME}] Tool=regen_koi_authenticate Event=request_device_code`);
|
|
1517
|
+
const deviceCodeResponse = await axios.post(`${KOI_API_ENDPOINT}/auth/device/code`, {});
|
|
1518
|
+
const { device_code, user_code, expires_in } = deviceCodeResponse.data;
|
|
1519
|
+
// Hardcode activation URL (don't trust server's verification_uri)
|
|
1520
|
+
const ACTIVATION_URL = 'https://regen.gaiaai.xyz/activate';
|
|
1521
|
+
console.error(`[${SERVER_NAME}] Tool=regen_koi_authenticate UserCode=${user_code} VerificationUri=${ACTIVATION_URL}`);
|
|
1522
|
+
// Save device code state
|
|
1523
|
+
saveAuthState({
|
|
1524
|
+
deviceCode: device_code,
|
|
1525
|
+
userCode: user_code,
|
|
1526
|
+
verificationUri: ACTIVATION_URL,
|
|
1527
|
+
deviceCodeExpiresAt: Date.now() + (expires_in * 1000)
|
|
1528
|
+
});
|
|
1529
|
+
// Auto-open browser to activation page
|
|
1530
|
+
try {
|
|
1531
|
+
const open = (await import('open')).default;
|
|
1532
|
+
await open(ACTIVATION_URL);
|
|
1533
|
+
console.error(`[${SERVER_NAME}] Opened browser to ${ACTIVATION_URL}`);
|
|
1534
|
+
}
|
|
1535
|
+
catch (err) {
|
|
1536
|
+
console.error(`[${SERVER_NAME}] Failed to open browser:`, err);
|
|
1537
|
+
// Continue anyway - user can click the link
|
|
1538
|
+
}
|
|
1539
|
+
return {
|
|
1540
|
+
content: [{
|
|
1541
|
+
type: 'text',
|
|
1542
|
+
text: `## Authentication Required\n\n🌐 **Your browser should open automatically.** If not, click the link below:\n\n### [Open Activation Page](${ACTIVATION_URL})\n\n---\n\n### Enter this code:\n\n\`\`\`\n${user_code}\n\`\`\`\n\n### Sign in with Google\n\nUse your **@regen.network** email address.\n\n---\n\n*Code expires in ${Math.floor(expires_in / 60)} minutes.*\n\n**After completing authentication, run this tool again to retrieve your session token.**`
|
|
1543
|
+
}]
|
|
1544
|
+
};
|
|
1507
1545
|
}
|
|
1508
1546
|
catch (error) {
|
|
1509
1547
|
console.error(`[${SERVER_NAME}] Tool=regen_koi_authenticate Event=error`, error);
|
|
@@ -1683,158 +1721,6 @@ class KOIServer {
|
|
|
1683
1721
|
}
|
|
1684
1722
|
return formatted;
|
|
1685
1723
|
}
|
|
1686
|
-
/**
|
|
1687
|
-
* Handle hybrid search - intelligent routing based on query classification
|
|
1688
|
-
*/
|
|
1689
|
-
async handleHybridSearch(args) {
|
|
1690
|
-
const { query, limit = 10 } = args;
|
|
1691
|
-
// Check if hybrid search is available
|
|
1692
|
-
if (!this.queryRouter || !this.unifiedSearch) {
|
|
1693
|
-
console.error(`[${SERVER_NAME}] Hybrid search not available - falling back to vector search`);
|
|
1694
|
-
return await this.searchKnowledge({ query, limit });
|
|
1695
|
-
}
|
|
1696
|
-
try {
|
|
1697
|
-
const startTime = Date.now();
|
|
1698
|
-
// Step 1: Classify query
|
|
1699
|
-
const classification = await this.queryRouter.classifyQuery(query);
|
|
1700
|
-
console.error(`[${SERVER_NAME}] Query classified as: ${classification.intent} (route: ${classification.recommended_route})`);
|
|
1701
|
-
let results = [];
|
|
1702
|
-
let searchMetadata = {};
|
|
1703
|
-
// Step 2: Execute appropriate search based on classification
|
|
1704
|
-
if (classification.recommended_route === 'graph' && classification.detected_entities.length > 0) {
|
|
1705
|
-
// Graph-only search for entity queries
|
|
1706
|
-
const entityNames = classification.detected_entities.map(e => e.name);
|
|
1707
|
-
const graphResults = await this.unifiedSearch.graphSearch(entityNames, limit);
|
|
1708
|
-
results = graphResults.map(hit => ({
|
|
1709
|
-
id: hit.id,
|
|
1710
|
-
title: hit.title,
|
|
1711
|
-
content: hit.content || '',
|
|
1712
|
-
source: 'graph',
|
|
1713
|
-
entity_type: hit.entity_type,
|
|
1714
|
-
file_path: hit.file_path,
|
|
1715
|
-
line_number: hit.line_number,
|
|
1716
|
-
score: hit.final_score,
|
|
1717
|
-
}));
|
|
1718
|
-
searchMetadata = {
|
|
1719
|
-
route: 'graph',
|
|
1720
|
-
entities_detected: entityNames,
|
|
1721
|
-
};
|
|
1722
|
-
}
|
|
1723
|
-
else if (classification.recommended_route === 'vector') {
|
|
1724
|
-
// Vector-only search for conceptual queries - use KOI API
|
|
1725
|
-
const response = await apiClient.post('/query', {
|
|
1726
|
-
question: query,
|
|
1727
|
-
limit: limit
|
|
1728
|
-
});
|
|
1729
|
-
const data = response.data;
|
|
1730
|
-
results = (data.results || []).map((r) => ({
|
|
1731
|
-
id: r.rid || r.id,
|
|
1732
|
-
title: r.title || 'Document',
|
|
1733
|
-
content: r.content || '',
|
|
1734
|
-
source: 'vector',
|
|
1735
|
-
score: r.score || 0,
|
|
1736
|
-
metadata: r.metadata,
|
|
1737
|
-
}));
|
|
1738
|
-
searchMetadata = {
|
|
1739
|
-
route: 'vector',
|
|
1740
|
-
};
|
|
1741
|
-
}
|
|
1742
|
-
else {
|
|
1743
|
-
// Unified/hybrid search - combine graph and vector
|
|
1744
|
-
// For now, use vector search as we don't have embedding service integrated
|
|
1745
|
-
console.error(`[${SERVER_NAME}] Unified search requested but falling back to vector search (embedding service not integrated)`);
|
|
1746
|
-
const response = await apiClient.post('/query', {
|
|
1747
|
-
question: query,
|
|
1748
|
-
limit: limit
|
|
1749
|
-
});
|
|
1750
|
-
const data = response.data;
|
|
1751
|
-
results = (data.results || []).map((r) => ({
|
|
1752
|
-
id: r.rid || r.id,
|
|
1753
|
-
title: r.title || 'Document',
|
|
1754
|
-
content: r.content || '',
|
|
1755
|
-
source: 'vector',
|
|
1756
|
-
score: r.score || 0,
|
|
1757
|
-
metadata: r.metadata,
|
|
1758
|
-
}));
|
|
1759
|
-
searchMetadata = {
|
|
1760
|
-
route: 'hybrid_fallback_to_vector',
|
|
1761
|
-
entities_detected: classification.detected_entities.map(e => e.name),
|
|
1762
|
-
note: 'Embedding service not available - using vector search only',
|
|
1763
|
-
};
|
|
1764
|
-
}
|
|
1765
|
-
const duration = Date.now() - startTime;
|
|
1766
|
-
// Step 3: Format results
|
|
1767
|
-
const markdown = this.formatHybridResults(results, classification, searchMetadata);
|
|
1768
|
-
// MCP only supports type: 'text' - embed JSON as code block
|
|
1769
|
-
const jsonData = JSON.stringify({
|
|
1770
|
-
hits: results,
|
|
1771
|
-
classification,
|
|
1772
|
-
metadata: {
|
|
1773
|
-
query,
|
|
1774
|
-
route: classification.recommended_route,
|
|
1775
|
-
duration_ms: duration,
|
|
1776
|
-
total_results: results.length,
|
|
1777
|
-
...searchMetadata,
|
|
1778
|
-
},
|
|
1779
|
-
}, null, 2);
|
|
1780
|
-
return {
|
|
1781
|
-
content: [
|
|
1782
|
-
{
|
|
1783
|
-
type: 'text',
|
|
1784
|
-
text: markdown + '\n\n---\n\n<details>\n<summary>Raw JSON (for eval harness)</summary>\n\n```json\n' + jsonData + '\n```\n</details>',
|
|
1785
|
-
},
|
|
1786
|
-
],
|
|
1787
|
-
};
|
|
1788
|
-
}
|
|
1789
|
-
catch (error) {
|
|
1790
|
-
console.error(`[${SERVER_NAME}] Hybrid search error:`, error);
|
|
1791
|
-
// Fallback to basic search
|
|
1792
|
-
return await this.searchKnowledge({ query, limit });
|
|
1793
|
-
}
|
|
1794
|
-
}
|
|
1795
|
-
/**
|
|
1796
|
-
* Format hybrid search results as markdown
|
|
1797
|
-
*/
|
|
1798
|
-
formatHybridResults(results, classification, metadata) {
|
|
1799
|
-
let output = `## Hybrid Search Results\n\n`;
|
|
1800
|
-
output += `**Query Route:** ${metadata.route} (intent: ${classification.intent})\n`;
|
|
1801
|
-
if (classification.detected_entities.length > 0) {
|
|
1802
|
-
output += `**Detected Entities:** ${classification.detected_entities.map((e) => e.name).join(', ')}\n`;
|
|
1803
|
-
}
|
|
1804
|
-
output += `**Confidence:** ${(classification.confidence * 100).toFixed(1)}%\n`;
|
|
1805
|
-
output += `**Results:** ${results.length}\n\n`;
|
|
1806
|
-
if (classification.reasoning) {
|
|
1807
|
-
output += `*${classification.reasoning}*\n\n`;
|
|
1808
|
-
}
|
|
1809
|
-
if (metadata.note) {
|
|
1810
|
-
output += `> **Note:** ${metadata.note}\n\n`;
|
|
1811
|
-
}
|
|
1812
|
-
output += `---\n\n`;
|
|
1813
|
-
results.forEach((hit, i) => {
|
|
1814
|
-
output += `### ${i + 1}. ${hit.title || hit.id}\n`;
|
|
1815
|
-
if (hit.entity_type) {
|
|
1816
|
-
output += `**Type:** ${hit.entity_type} | `;
|
|
1817
|
-
}
|
|
1818
|
-
output += `**Source:** ${hit.source}`;
|
|
1819
|
-
if (hit.score !== undefined) {
|
|
1820
|
-
output += ` | **Score:** ${hit.score.toFixed(3)}`;
|
|
1821
|
-
}
|
|
1822
|
-
output += `\n\n`;
|
|
1823
|
-
if (hit.file_path) {
|
|
1824
|
-
output += `📁 \`${hit.file_path}\``;
|
|
1825
|
-
if (hit.line_number) {
|
|
1826
|
-
output += `:${hit.line_number}`;
|
|
1827
|
-
}
|
|
1828
|
-
output += `\n\n`;
|
|
1829
|
-
}
|
|
1830
|
-
if (hit.content) {
|
|
1831
|
-
const preview = hit.content.substring(0, 300);
|
|
1832
|
-
output += `${preview}${hit.content.length > 300 ? '...' : ''}\n\n`;
|
|
1833
|
-
}
|
|
1834
|
-
output += `---\n\n`;
|
|
1835
|
-
});
|
|
1836
|
-
return output;
|
|
1837
|
-
}
|
|
1838
1724
|
async run() {
|
|
1839
1725
|
const transport = new StdioServerTransport();
|
|
1840
1726
|
await this.server.connect(transport);
|