doc2vec 1.1.0 → 1.1.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.
- package/README.md +6 -2
- package/config.yaml +3 -0
- package/dist/doc2vec.js +49 -1
- package/doc2vec.ts +58 -1
- package/logger.ts +287 -0
- package/mcp/Dockerfile +14 -0
- package/mcp/README.md +166 -0
- package/mcp/package-lock.json +1754 -0
- package/mcp/package.json +41 -0
- package/mcp/src/index.ts +214 -0
- package/mcp/tsconfig.json +16 -0
- package/package.json +3 -3
- package/dist/doc2vec copy.js +0 -1437
- package/dist/logger copy.js +0 -246
package/README.md
CHANGED
|
@@ -9,6 +9,7 @@ The primary goal is to prepare documentation content for Retrieval-Augmented Gen
|
|
|
9
9
|
## Key Features
|
|
10
10
|
|
|
11
11
|
* **Website Crawling:** Recursively crawls websites starting from a given base URL.
|
|
12
|
+
* **Sitemap Support:** Extracts URLs from XML sitemaps to discover pages not linked in navigation.
|
|
12
13
|
* **GitHub Issues Integration:** Retrieves GitHub issues and comments, processing them into searchable chunks.
|
|
13
14
|
* **Local Directory Processing:** Scans local directories for files, converts content to searchable chunks.
|
|
14
15
|
* **Content Extraction:** Uses Puppeteer for rendering JavaScript-heavy pages and `@mozilla/readability` to extract the main article content.
|
|
@@ -79,6 +80,7 @@ Configuration is managed through two files:
|
|
|
79
80
|
|
|
80
81
|
For websites (`type: 'website'`):
|
|
81
82
|
* `url`: The starting URL for crawling the documentation site.
|
|
83
|
+
* `sitemap_url`: (Optional) URL to the site's XML sitemap for discovering additional pages not linked in navigation.
|
|
82
84
|
|
|
83
85
|
For GitHub repositories (`type: 'github'`):
|
|
84
86
|
* `repo`: Repository name in the format `'owner/repo'` (e.g., `'istio/istio'`).
|
|
@@ -114,6 +116,7 @@ Configuration is managed through two files:
|
|
|
114
116
|
product_name: 'argo'
|
|
115
117
|
version: 'stable'
|
|
116
118
|
url: 'https://argo-cd.readthedocs.io/en/stable/'
|
|
119
|
+
sitemap_url: 'https://argo-cd.readthedocs.io/en/stable/sitemap.xml'
|
|
117
120
|
max_size: 1048576
|
|
118
121
|
database_config:
|
|
119
122
|
type: 'sqlite'
|
|
@@ -180,7 +183,7 @@ The script will then:
|
|
|
180
183
|
3. Iterate through each source defined in the config.
|
|
181
184
|
4. Initialize the specified database connection.
|
|
182
185
|
5. Process each source according to its type:
|
|
183
|
-
- For websites: Crawl the site, extract content, convert to Markdown
|
|
186
|
+
- For websites: Crawl the site, process any sitemaps, extract content, convert to Markdown
|
|
184
187
|
- For GitHub repos: Fetch issues and comments, convert to Markdown
|
|
185
188
|
- For local directories: Scan files, process content (converting HTML to Markdown if needed)
|
|
186
189
|
6. For all sources: Chunk content, check for changes, generate embeddings (if needed), and store/update in the database.
|
|
@@ -214,7 +217,7 @@ This will:
|
|
|
214
217
|
|
|
215
218
|
3. Generate, embed, and store documentation chunks in the configured database(s).
|
|
216
219
|
|
|
217
|
-
If you don
|
|
220
|
+
If you don't specify a config path, it will look for config.yaml in the current working directory.
|
|
218
221
|
|
|
219
222
|
## Core Logic Flow
|
|
220
223
|
|
|
@@ -225,6 +228,7 @@ If you don’t specify a config path, it will look for config.yaml in the curren
|
|
|
225
228
|
2. **Process by Source Type:**
|
|
226
229
|
- **For Websites:**
|
|
227
230
|
* Start at the base `url`.
|
|
231
|
+
* If `sitemap_url` is provided, fetch and parse the sitemap to extract additional URLs.
|
|
228
232
|
* Use Puppeteer (`processPage`) to fetch and render HTML.
|
|
229
233
|
* Use Readability to extract main content.
|
|
230
234
|
* Sanitize HTML.
|
package/config.yaml
CHANGED
|
@@ -77,6 +77,7 @@ sources:
|
|
|
77
77
|
product_name: 'gloo-mesh-core'
|
|
78
78
|
version: 'latest'
|
|
79
79
|
url: 'https://docs.solo.io/gloo-mesh/latest/'
|
|
80
|
+
sitemap_url: 'https://docs.solo.io/gloo-mesh/sitemap.xml'
|
|
80
81
|
max_size: 1048576
|
|
81
82
|
database_config:
|
|
82
83
|
type: 'sqlite'
|
|
@@ -87,6 +88,7 @@ sources:
|
|
|
87
88
|
product_name: 'gloo-mesh-enterprise'
|
|
88
89
|
version: 'latest'
|
|
89
90
|
url: 'https://docs.solo.io/gloo-mesh-enterprise/latest/'
|
|
91
|
+
sitemap_url: 'https://docs.solo.io/gloo-mesh-enterprise/sitemap.xml'
|
|
90
92
|
max_size: 1048576
|
|
91
93
|
database_config:
|
|
92
94
|
type: 'sqlite'
|
|
@@ -107,6 +109,7 @@ sources:
|
|
|
107
109
|
product_name: 'gloo-gateway'
|
|
108
110
|
version: 'latest'
|
|
109
111
|
url: 'https://docs.solo.io/gateway/latest/'
|
|
112
|
+
sitemap_url: 'https://docs.solo.io/gateway/sitemap.xml'
|
|
110
113
|
max_size: 1048576
|
|
111
114
|
database_config:
|
|
112
115
|
type: 'sqlite'
|
package/dist/doc2vec.js
CHANGED
|
@@ -1158,10 +1158,58 @@ class Doc2Vec {
|
|
|
1158
1158
|
logger.error(`Error removing obsolete chunks from Qdrant:`, error);
|
|
1159
1159
|
}
|
|
1160
1160
|
}
|
|
1161
|
+
async parseSitemap(sitemapUrl, logger) {
|
|
1162
|
+
logger.info(`Parsing sitemap from ${sitemapUrl}`);
|
|
1163
|
+
try {
|
|
1164
|
+
const response = await axios_1.default.get(sitemapUrl);
|
|
1165
|
+
const $ = (0, cheerio_1.load)(response.data, { xmlMode: true });
|
|
1166
|
+
const urls = [];
|
|
1167
|
+
// Handle standard sitemaps
|
|
1168
|
+
$('url > loc').each((_, element) => {
|
|
1169
|
+
const url = $(element).text().trim();
|
|
1170
|
+
if (url) {
|
|
1171
|
+
urls.push(url);
|
|
1172
|
+
}
|
|
1173
|
+
});
|
|
1174
|
+
// Handle sitemap indexes (sitemaps that link to other sitemaps)
|
|
1175
|
+
const sitemapLinks = [];
|
|
1176
|
+
$('sitemap > loc').each((_, element) => {
|
|
1177
|
+
const nestedSitemapUrl = $(element).text().trim();
|
|
1178
|
+
if (nestedSitemapUrl) {
|
|
1179
|
+
sitemapLinks.push(nestedSitemapUrl);
|
|
1180
|
+
}
|
|
1181
|
+
});
|
|
1182
|
+
// Recursively process nested sitemaps
|
|
1183
|
+
for (const nestedSitemapUrl of sitemapLinks) {
|
|
1184
|
+
logger.debug(`Found nested sitemap: ${nestedSitemapUrl}`);
|
|
1185
|
+
const nestedUrls = await this.parseSitemap(nestedSitemapUrl, logger);
|
|
1186
|
+
urls.push(...nestedUrls);
|
|
1187
|
+
}
|
|
1188
|
+
logger.info(`Found ${urls.length} URLs in sitemap ${sitemapUrl}`);
|
|
1189
|
+
return urls;
|
|
1190
|
+
}
|
|
1191
|
+
catch (error) {
|
|
1192
|
+
logger.error(`Error parsing sitemap at ${sitemapUrl}:`, error);
|
|
1193
|
+
return [];
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1161
1196
|
async crawlWebsite(baseUrl, sourceConfig, processPageContent, parentLogger, visitedUrls) {
|
|
1162
1197
|
const logger = parentLogger.child('crawler');
|
|
1163
1198
|
const queue = [baseUrl];
|
|
1164
|
-
|
|
1199
|
+
// Process sitemap if provided
|
|
1200
|
+
if (sourceConfig.sitemap_url) {
|
|
1201
|
+
logger.section('SITEMAP PROCESSING');
|
|
1202
|
+
const sitemapUrls = await this.parseSitemap(sourceConfig.sitemap_url, logger);
|
|
1203
|
+
// Add sitemap URLs to the queue if they're within the website scope
|
|
1204
|
+
for (const url of sitemapUrls) {
|
|
1205
|
+
if (url.startsWith(sourceConfig.url) && !queue.includes(url)) {
|
|
1206
|
+
logger.debug(`Adding URL from sitemap to queue: ${url}`);
|
|
1207
|
+
queue.push(url);
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
logger.info(`Added ${queue.length - 1} URLs from sitemap to the crawl queue`);
|
|
1211
|
+
}
|
|
1212
|
+
logger.info(`Starting crawl from ${baseUrl} with ${queue.length} URLs in initial queue`);
|
|
1165
1213
|
let processedCount = 0;
|
|
1166
1214
|
let skippedCount = 0;
|
|
1167
1215
|
let skippedSizeCount = 0;
|
package/doc2vec.ts
CHANGED
|
@@ -50,6 +50,7 @@ interface LocalDirectorySourceConfig extends BaseSourceConfig {
|
|
|
50
50
|
interface WebsiteSourceConfig extends BaseSourceConfig {
|
|
51
51
|
type: 'website';
|
|
52
52
|
url: string;
|
|
53
|
+
sitemap_url?: string; // Optional sitemap URL to extract additional URLs to crawl
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
// Configuration specific to GitHub repo sources
|
|
@@ -1364,6 +1365,46 @@ class Doc2Vec {
|
|
|
1364
1365
|
}
|
|
1365
1366
|
}
|
|
1366
1367
|
|
|
1368
|
+
private async parseSitemap(sitemapUrl: string, logger: Logger): Promise<string[]> {
|
|
1369
|
+
logger.info(`Parsing sitemap from ${sitemapUrl}`);
|
|
1370
|
+
try {
|
|
1371
|
+
const response = await axios.get(sitemapUrl);
|
|
1372
|
+
const $ = load(response.data, { xmlMode: true });
|
|
1373
|
+
|
|
1374
|
+
const urls: string[] = [];
|
|
1375
|
+
|
|
1376
|
+
// Handle standard sitemaps
|
|
1377
|
+
$('url > loc').each((_, element) => {
|
|
1378
|
+
const url = $(element).text().trim();
|
|
1379
|
+
if (url) {
|
|
1380
|
+
urls.push(url);
|
|
1381
|
+
}
|
|
1382
|
+
});
|
|
1383
|
+
|
|
1384
|
+
// Handle sitemap indexes (sitemaps that link to other sitemaps)
|
|
1385
|
+
const sitemapLinks: string[] = [];
|
|
1386
|
+
$('sitemap > loc').each((_, element) => {
|
|
1387
|
+
const nestedSitemapUrl = $(element).text().trim();
|
|
1388
|
+
if (nestedSitemapUrl) {
|
|
1389
|
+
sitemapLinks.push(nestedSitemapUrl);
|
|
1390
|
+
}
|
|
1391
|
+
});
|
|
1392
|
+
|
|
1393
|
+
// Recursively process nested sitemaps
|
|
1394
|
+
for (const nestedSitemapUrl of sitemapLinks) {
|
|
1395
|
+
logger.debug(`Found nested sitemap: ${nestedSitemapUrl}`);
|
|
1396
|
+
const nestedUrls = await this.parseSitemap(nestedSitemapUrl, logger);
|
|
1397
|
+
urls.push(...nestedUrls);
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
logger.info(`Found ${urls.length} URLs in sitemap ${sitemapUrl}`);
|
|
1401
|
+
return urls;
|
|
1402
|
+
} catch (error) {
|
|
1403
|
+
logger.error(`Error parsing sitemap at ${sitemapUrl}:`, error);
|
|
1404
|
+
return [];
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1367
1408
|
private async crawlWebsite(
|
|
1368
1409
|
baseUrl: string,
|
|
1369
1410
|
sourceConfig: WebsiteSourceConfig,
|
|
@@ -1373,8 +1414,24 @@ class Doc2Vec {
|
|
|
1373
1414
|
): Promise<void> {
|
|
1374
1415
|
const logger = parentLogger.child('crawler');
|
|
1375
1416
|
const queue: string[] = [baseUrl];
|
|
1417
|
+
|
|
1418
|
+
// Process sitemap if provided
|
|
1419
|
+
if (sourceConfig.sitemap_url) {
|
|
1420
|
+
logger.section('SITEMAP PROCESSING');
|
|
1421
|
+
const sitemapUrls = await this.parseSitemap(sourceConfig.sitemap_url, logger);
|
|
1422
|
+
|
|
1423
|
+
// Add sitemap URLs to the queue if they're within the website scope
|
|
1424
|
+
for (const url of sitemapUrls) {
|
|
1425
|
+
if (url.startsWith(sourceConfig.url) && !queue.includes(url)) {
|
|
1426
|
+
logger.debug(`Adding URL from sitemap to queue: ${url}`);
|
|
1427
|
+
queue.push(url);
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
logger.info(`Added ${queue.length - 1} URLs from sitemap to the crawl queue`);
|
|
1432
|
+
}
|
|
1376
1433
|
|
|
1377
|
-
logger.info(`Starting crawl from ${baseUrl}`);
|
|
1434
|
+
logger.info(`Starting crawl from ${baseUrl} with ${queue.length} URLs in initial queue`);
|
|
1378
1435
|
let processedCount = 0;
|
|
1379
1436
|
let skippedCount = 0;
|
|
1380
1437
|
let skippedSizeCount = 0;
|
package/logger.ts
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enhanced Logger for structured and consistent logging
|
|
3
|
+
* Compatible with CommonJS and ESM environments
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Logger levels with their corresponding numeric values
|
|
8
|
+
*/
|
|
9
|
+
enum LogLevel {
|
|
10
|
+
DEBUG = 0,
|
|
11
|
+
INFO = 1,
|
|
12
|
+
WARN = 2,
|
|
13
|
+
ERROR = 3,
|
|
14
|
+
NONE = 100
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Configuration options for the Logger
|
|
19
|
+
*/
|
|
20
|
+
interface LoggerConfig {
|
|
21
|
+
level: LogLevel;
|
|
22
|
+
useTimestamp: boolean;
|
|
23
|
+
useColor: boolean;
|
|
24
|
+
logToFile?: string;
|
|
25
|
+
prettyPrint?: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Basic color functions that don't rely on external packages
|
|
30
|
+
*/
|
|
31
|
+
const colors = {
|
|
32
|
+
gray: (text: string) => `\x1b[90m${text}\x1b[0m`,
|
|
33
|
+
blue: (text: string) => `\x1b[34m${text}\x1b[0m`,
|
|
34
|
+
yellow: (text: string) => `\x1b[33m${text}\x1b[0m`,
|
|
35
|
+
red: (text: string) => `\x1b[31m${text}\x1b[0m`,
|
|
36
|
+
green: (text: string) => `\x1b[32m${text}\x1b[0m`,
|
|
37
|
+
reset: (text: string) => `\x1b[0m${text}\x1b[0m`
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Enhanced Logger class with color-coding, timestamp, and formatting
|
|
42
|
+
*/
|
|
43
|
+
class Logger {
|
|
44
|
+
private config: LoggerConfig;
|
|
45
|
+
private moduleName: string;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Create a new Logger instance
|
|
49
|
+
*
|
|
50
|
+
* @param moduleName Name of the module using this logger
|
|
51
|
+
* @param config Logger configuration options
|
|
52
|
+
*/
|
|
53
|
+
constructor(moduleName: string, config?: Partial<LoggerConfig>) {
|
|
54
|
+
this.moduleName = moduleName;
|
|
55
|
+
this.config = {
|
|
56
|
+
level: LogLevel.INFO,
|
|
57
|
+
useTimestamp: true,
|
|
58
|
+
useColor: true,
|
|
59
|
+
prettyPrint: true,
|
|
60
|
+
...config
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Format a log message with timestamp, level, and module information
|
|
66
|
+
*
|
|
67
|
+
* @param level Log level for this message
|
|
68
|
+
* @param message The message to log
|
|
69
|
+
* @param args Additional arguments to include
|
|
70
|
+
* @returns Formatted log message
|
|
71
|
+
*/
|
|
72
|
+
private formatMessage(level: string, message: string, args: any[] = []): string {
|
|
73
|
+
const timestamp = this.config.useTimestamp ?
|
|
74
|
+
`[${new Date().toISOString()}] ` : '';
|
|
75
|
+
|
|
76
|
+
const modulePrefix = this.moduleName ?
|
|
77
|
+
`[${this.moduleName}] ` : '';
|
|
78
|
+
|
|
79
|
+
const levelFormatted = `[${level.padEnd(5)}]`;
|
|
80
|
+
|
|
81
|
+
let formattedMessage = `${timestamp}${levelFormatted} ${modulePrefix}${message}`;
|
|
82
|
+
|
|
83
|
+
if (args.length > 0) {
|
|
84
|
+
const argsString = args.map(arg => {
|
|
85
|
+
if (arg instanceof Error) {
|
|
86
|
+
return `\n--- Error Details ---\nMessage: ${arg.message}\nStack:\n${arg.stack}\n--- End Error ---`;
|
|
87
|
+
}
|
|
88
|
+
else if (this.config.prettyPrint && typeof arg === 'object' && arg !== null) {
|
|
89
|
+
try {
|
|
90
|
+
return JSON.stringify(arg, null, 2);
|
|
91
|
+
} catch (e) {
|
|
92
|
+
return "[Unserializable Object]";
|
|
93
|
+
}
|
|
94
|
+
} else {
|
|
95
|
+
return String(arg);
|
|
96
|
+
}
|
|
97
|
+
}).join('\n');
|
|
98
|
+
|
|
99
|
+
if (this.config.prettyPrint) {
|
|
100
|
+
formattedMessage += `\n${argsString}`;
|
|
101
|
+
} else {
|
|
102
|
+
formattedMessage += ` ${args.map(String).join(' ')}`;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return formattedMessage;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Apply color to a message based on log level
|
|
111
|
+
*
|
|
112
|
+
* @param level Log level
|
|
113
|
+
* @param message Message to color
|
|
114
|
+
* @returns Colored message
|
|
115
|
+
*/
|
|
116
|
+
private colorize(level: LogLevel, message: string): string {
|
|
117
|
+
if (!this.config.useColor) return message;
|
|
118
|
+
|
|
119
|
+
switch (level) {
|
|
120
|
+
case LogLevel.DEBUG:
|
|
121
|
+
return colors.gray(message);
|
|
122
|
+
case LogLevel.INFO:
|
|
123
|
+
return colors.blue(message);
|
|
124
|
+
case LogLevel.WARN:
|
|
125
|
+
return colors.yellow(message);
|
|
126
|
+
case LogLevel.ERROR:
|
|
127
|
+
return colors.red(message);
|
|
128
|
+
default:
|
|
129
|
+
return message;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Log a debug message
|
|
135
|
+
*
|
|
136
|
+
* @param message Message to log
|
|
137
|
+
* @param args Additional arguments
|
|
138
|
+
*/
|
|
139
|
+
debug(message: string, ...args: any[]): void {
|
|
140
|
+
if (this.config.level <= LogLevel.DEBUG) {
|
|
141
|
+
const formattedMessage = this.formatMessage('DEBUG', message, args);
|
|
142
|
+
console.log(this.colorize(LogLevel.DEBUG, formattedMessage));
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Log an info message
|
|
148
|
+
*
|
|
149
|
+
* @param message Message to log
|
|
150
|
+
* @param args Additional arguments
|
|
151
|
+
*/
|
|
152
|
+
info(message: string, ...args: any[]): void {
|
|
153
|
+
if (this.config.level <= LogLevel.INFO) {
|
|
154
|
+
const formattedMessage = this.formatMessage('INFO', message, args);
|
|
155
|
+
console.log(this.colorize(LogLevel.INFO, formattedMessage));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Log a warning message
|
|
161
|
+
*
|
|
162
|
+
* @param message Message to log
|
|
163
|
+
* @param args Additional arguments
|
|
164
|
+
*/
|
|
165
|
+
warn(message: string, ...args: any[]): void {
|
|
166
|
+
if (this.config.level <= LogLevel.WARN) {
|
|
167
|
+
const formattedMessage = this.formatMessage('WARN', message, args);
|
|
168
|
+
console.warn(this.colorize(LogLevel.WARN, formattedMessage));
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Log an error message
|
|
174
|
+
*
|
|
175
|
+
* @param message Message to log
|
|
176
|
+
* @param args Additional arguments
|
|
177
|
+
*/
|
|
178
|
+
error(message: string, ...args: any[]): void {
|
|
179
|
+
if (this.config.level <= LogLevel.ERROR) {
|
|
180
|
+
const formattedMessage = this.formatMessage('ERROR', message, args);
|
|
181
|
+
console.error(this.colorize(LogLevel.ERROR, formattedMessage));
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Create a child logger with a more specific module name
|
|
187
|
+
*
|
|
188
|
+
* @param subModule Name of the sub-module
|
|
189
|
+
* @returns New logger instance
|
|
190
|
+
*/
|
|
191
|
+
child(subModule: string): Logger {
|
|
192
|
+
return new Logger(`${this.moduleName}:${subModule}`, this.config);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Format a section header to clearly separate logical parts of execution
|
|
197
|
+
*
|
|
198
|
+
* @param title Section title
|
|
199
|
+
* @returns Logger instance for chaining
|
|
200
|
+
*/
|
|
201
|
+
section(title: string): Logger {
|
|
202
|
+
if (this.config.level <= LogLevel.INFO) {
|
|
203
|
+
const separator = '='.repeat(Math.max(80 - title.length - 4, 10));
|
|
204
|
+
const message = `${separator} ${title} ${separator}`;
|
|
205
|
+
console.log(this.colorize(LogLevel.INFO, message));
|
|
206
|
+
}
|
|
207
|
+
return this;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Create a progress indicator
|
|
212
|
+
*
|
|
213
|
+
* @param title Title of the operation
|
|
214
|
+
* @param total Total number of items to process
|
|
215
|
+
* @returns Object with update and complete methods
|
|
216
|
+
*/
|
|
217
|
+
progress(title: string, total: number) {
|
|
218
|
+
let current = 0;
|
|
219
|
+
const startTime = Date.now();
|
|
220
|
+
|
|
221
|
+
const update = (increment = 1, message?: string) => {
|
|
222
|
+
if (this.config.level > LogLevel.INFO) return;
|
|
223
|
+
|
|
224
|
+
current += increment;
|
|
225
|
+
const percentage = Math.min(Math.floor((current / total) * 100), 100);
|
|
226
|
+
const elapsed = (Date.now() - startTime) / 1000;
|
|
227
|
+
let rate = current / elapsed;
|
|
228
|
+
|
|
229
|
+
let timeRemaining = '';
|
|
230
|
+
if (rate > 0 && current < total) {
|
|
231
|
+
const remainingSecs = (total - current) / rate;
|
|
232
|
+
timeRemaining = `, ETA: ${Math.floor(remainingSecs / 60)}m ${Math.floor(remainingSecs % 60)}s`;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const progressBar = this.createProgressBar(percentage);
|
|
236
|
+
|
|
237
|
+
const statusMsg = message ? ` - ${message}` : '';
|
|
238
|
+
console.log(this.colorize(
|
|
239
|
+
LogLevel.INFO,
|
|
240
|
+
this.formatMessage('INFO', `${title}: ${progressBar} ${percentage}% (${current}/${total}${timeRemaining})${statusMsg}`)
|
|
241
|
+
));
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const complete = (message = 'Completed') => {
|
|
245
|
+
if (this.config.level > LogLevel.INFO) return;
|
|
246
|
+
|
|
247
|
+
const elapsed = (Date.now() - startTime) / 1000;
|
|
248
|
+
const rate = total / elapsed;
|
|
249
|
+
|
|
250
|
+
console.log(this.colorize(
|
|
251
|
+
LogLevel.INFO,
|
|
252
|
+
this.formatMessage('INFO', `${title}: ${this.createProgressBar(100)} 100% (${total}/${total}) - ${message} in ${elapsed.toFixed(2)}s (${rate.toFixed(2)} items/sec)`)
|
|
253
|
+
));
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
return { update, complete };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Create a visual progress bar
|
|
261
|
+
*
|
|
262
|
+
* @param percentage Completion percentage
|
|
263
|
+
* @returns Visual progress bar
|
|
264
|
+
*/
|
|
265
|
+
private createProgressBar(percentage: number): string {
|
|
266
|
+
const width = 20;
|
|
267
|
+
const completeChars = Math.floor((percentage / 100) * width);
|
|
268
|
+
const incompleteChars = width - completeChars;
|
|
269
|
+
|
|
270
|
+
let bar = '[';
|
|
271
|
+
if (this.config.useColor) {
|
|
272
|
+
bar += colors.green('='.repeat(completeChars));
|
|
273
|
+
bar += ' '.repeat(incompleteChars);
|
|
274
|
+
} else {
|
|
275
|
+
bar += '='.repeat(completeChars);
|
|
276
|
+
bar += ' '.repeat(incompleteChars);
|
|
277
|
+
}
|
|
278
|
+
bar += ']';
|
|
279
|
+
|
|
280
|
+
return bar;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Create a default logger instance
|
|
285
|
+
const defaultLogger = new Logger('app');
|
|
286
|
+
|
|
287
|
+
export { Logger, LogLevel, defaultLogger };
|
package/mcp/Dockerfile
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
FROM node:20-slim AS base
|
|
2
|
+
|
|
3
|
+
# Copy project files
|
|
4
|
+
COPY package.json /usr/src/app/
|
|
5
|
+
COPY package-lock.json /usr/src/app/
|
|
6
|
+
COPY tsconfig.json /usr/src/app/
|
|
7
|
+
COPY src/index.ts /usr/src/app/src/
|
|
8
|
+
COPY *.db /data/
|
|
9
|
+
WORKDIR /usr/src/app
|
|
10
|
+
|
|
11
|
+
RUN npm install
|
|
12
|
+
RUN npm run build
|
|
13
|
+
|
|
14
|
+
ENTRYPOINT ["node", "build/index.js"]
|
package/mcp/README.md
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# SQLite Vector Documentation Query MCP Server
|
|
2
|
+
|
|
3
|
+
This is a Model Context Protocol (MCP) server that enables querying documentation stored in SQLite databases with vector embeddings. The server uses OpenAI's embedding API to convert natural language queries into vector embeddings and performs semantic search against documentation stored in SQLite databases.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Vector-based semantic search for documentation
|
|
8
|
+
- Filters by product name and version
|
|
9
|
+
- Uses OpenAI's embedding API for query embedding generation
|
|
10
|
+
- Fully compatible with the Model Context Protocol
|
|
11
|
+
- Simple Express-based API with Server-Sent Events (SSE) for real-time communication
|
|
12
|
+
|
|
13
|
+
## Prerequisites
|
|
14
|
+
|
|
15
|
+
- Node.js 20 or higher
|
|
16
|
+
- OpenAI API key
|
|
17
|
+
- Documentation stored in SQLite vector databases (using `sqlite-vec`)
|
|
18
|
+
|
|
19
|
+
## Environment Variables
|
|
20
|
+
|
|
21
|
+
| Variable | Description | Default |
|
|
22
|
+
|----------|-------------|---------|
|
|
23
|
+
| `OPENAI_API_KEY` | Your OpenAI API key (required) | - |
|
|
24
|
+
| `SQLITE_DB_DIR` | Directory containing SQLite databases | Current directory |
|
|
25
|
+
| `PORT` | Port to run the server on | 3001 |
|
|
26
|
+
|
|
27
|
+
## Local Setup and Running
|
|
28
|
+
|
|
29
|
+
1. Install dependencies:
|
|
30
|
+
```bash
|
|
31
|
+
npm install
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
2. Create a `.env` file with required environment variables:
|
|
35
|
+
```
|
|
36
|
+
OPENAI_API_KEY=your_openai_api_key
|
|
37
|
+
SQLITE_DB_DIR=/path/to/databases
|
|
38
|
+
PORT=3001
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
3. Build the TypeScript code:
|
|
42
|
+
```bash
|
|
43
|
+
npm run build
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
4. Start the server:
|
|
47
|
+
```bash
|
|
48
|
+
npm start
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Docker Setup
|
|
52
|
+
|
|
53
|
+
### Building the Docker Image
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
docker build -t sqlite-vec-mcp-server:latest .
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
This is going to include any `*.db` files in the `/data` directory of the image.
|
|
60
|
+
|
|
61
|
+
### Running with Docker
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
docker run -p 3001:3001 \
|
|
65
|
+
-e OPENAI_API_KEY=your_openai_api_key \
|
|
66
|
+
sqlite-vec-mcp-server:latest
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Create a Secret for the OpenAI API Key
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
kubectl create secret generic mcp-secrets \
|
|
73
|
+
--from-literal=OPENAI_API_KEY=your_openai_api_key
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Create a ConfigMap for Database Configuration
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
kubectl create configmap mcp-config \
|
|
80
|
+
--from-literal=SQLITE_DB_DIR=/data \
|
|
81
|
+
--from-literal=PORT=3001
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Create a Deployment
|
|
85
|
+
|
|
86
|
+
Create a file named `deployment.yaml`:
|
|
87
|
+
|
|
88
|
+
```yaml
|
|
89
|
+
apiVersion: apps/v1
|
|
90
|
+
kind: Deployment
|
|
91
|
+
metadata:
|
|
92
|
+
name: mcp-sqlite-vec
|
|
93
|
+
labels:
|
|
94
|
+
app: mcp-sqlite-vec
|
|
95
|
+
spec:
|
|
96
|
+
replicas: 1
|
|
97
|
+
selector:
|
|
98
|
+
matchLabels:
|
|
99
|
+
app: mcp-sqlite-vec
|
|
100
|
+
template:
|
|
101
|
+
metadata:
|
|
102
|
+
labels:
|
|
103
|
+
app: mcp-sqlite-vec
|
|
104
|
+
spec:
|
|
105
|
+
containers:
|
|
106
|
+
- name: mcp-sqlite-vec
|
|
107
|
+
image: sqlite-vec-mcp-server:latest
|
|
108
|
+
imagePullPolicy: Always
|
|
109
|
+
ports:
|
|
110
|
+
- containerPort: 3001
|
|
111
|
+
env:
|
|
112
|
+
- name: OPENAI_API_KEY
|
|
113
|
+
valueFrom:
|
|
114
|
+
secretKeyRef:
|
|
115
|
+
name: mcp-secrets
|
|
116
|
+
key: OPENAI_API_KEY
|
|
117
|
+
- name: SQLITE_DB_DIR
|
|
118
|
+
valueFrom:
|
|
119
|
+
configMapKeyRef:
|
|
120
|
+
name: mcp-config
|
|
121
|
+
key: SQLITE_DB_DIR
|
|
122
|
+
- name: PORT
|
|
123
|
+
valueFrom:
|
|
124
|
+
configMapKeyRef:
|
|
125
|
+
name: mcp-config
|
|
126
|
+
key: PORT
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Apply it:
|
|
130
|
+
```bash
|
|
131
|
+
kubectl apply -f deployment.yaml
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Create a Service
|
|
135
|
+
|
|
136
|
+
Create a file named `service.yaml`:
|
|
137
|
+
|
|
138
|
+
```yaml
|
|
139
|
+
apiVersion: v1
|
|
140
|
+
kind: Service
|
|
141
|
+
metadata:
|
|
142
|
+
name: mcp-sqlite-vec
|
|
143
|
+
spec:
|
|
144
|
+
selector:
|
|
145
|
+
app: mcp-sqlite-vec
|
|
146
|
+
ports:
|
|
147
|
+
- port: 3001
|
|
148
|
+
targetPort: 3001
|
|
149
|
+
type: ClusterIP
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Apply it:
|
|
153
|
+
```bash
|
|
154
|
+
kubectl apply -f service.yaml
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Using the MCP Server
|
|
158
|
+
|
|
159
|
+
The server implements a tool called `query-documentation` that can be used to query documentation.
|
|
160
|
+
|
|
161
|
+
### Tool Parameters
|
|
162
|
+
|
|
163
|
+
- `queryText` (string, required): The natural language query to search for
|
|
164
|
+
- `productName` (string, required): The name of the product documentation database to search within
|
|
165
|
+
- `version` (string, optional): The specific version of the product documentation
|
|
166
|
+
- `limit` (number, optional, default: 4): Maximum number of results to return
|