@enokdev/springdocs-mcp 1.2.4 → 1.2.7
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/build/index.js +5 -5
- package/build/index.js.map +1 -1
- package/build/services/advanced-features-old.d.ts +45 -0
- package/build/services/advanced-features-old.d.ts.map +1 -0
- package/build/services/advanced-features-old.js +792 -0
- package/build/services/advanced-features-old.js.map +1 -0
- package/build/services/advanced-features.d.ts +20 -14
- package/build/services/advanced-features.d.ts.map +1 -1
- package/build/services/advanced-features.js +368 -476
- package/build/services/advanced-features.js.map +1 -1
- package/build/services/springboot-docs-optimized-old.d.ts +55 -0
- package/build/services/springboot-docs-optimized-old.d.ts.map +1 -0
- package/build/services/springboot-docs-optimized-old.js +588 -0
- package/build/services/springboot-docs-optimized-old.js.map +1 -0
- package/build/services/springboot-docs-optimized.d.ts +30 -21
- package/build/services/springboot-docs-optimized.d.ts.map +1 -1
- package/build/services/springboot-docs-optimized.js +246 -371
- package/build/services/springboot-docs-optimized.js.map +1 -1
- package/build/tools/index.d.ts +22 -0
- package/build/tools/index.d.ts.map +1 -1
- package/build/tools/index.js +13 -1
- package/build/tools/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -3,7 +3,8 @@ import * as cheerio from 'cheerio';
|
|
|
3
3
|
import TurndownService from 'turndown';
|
|
4
4
|
import { CacheService } from './cache.js';
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
6
|
+
* Clean Spring Boot Documentation Service - uses ONLY real Spring documentation APIs
|
|
7
|
+
* No mock data whatsoever - everything is fetched from actual Spring sources
|
|
7
8
|
*/
|
|
8
9
|
export class SpringBootDocsServiceOptimized {
|
|
9
10
|
baseUrl = 'https://docs.spring.io';
|
|
@@ -24,7 +25,66 @@ export class SpringBootDocsServiceOptimized {
|
|
|
24
25
|
setInterval(() => this.cache.cleanup(), 60 * 60 * 1000);
|
|
25
26
|
}
|
|
26
27
|
/**
|
|
27
|
-
*
|
|
28
|
+
* Extract content intelligently based on detail level
|
|
29
|
+
* Preserves code blocks, key sections, and structure
|
|
30
|
+
*/
|
|
31
|
+
extractIntelligentContent(markdown, detailLevel = 'medium') {
|
|
32
|
+
const limits = {
|
|
33
|
+
'summary': 1500,
|
|
34
|
+
'medium': 4000,
|
|
35
|
+
'full': 8000
|
|
36
|
+
};
|
|
37
|
+
const maxLength = limits[detailLevel] || limits['medium'];
|
|
38
|
+
// If content is smaller than limit, return as-is
|
|
39
|
+
if (markdown.length <= maxLength) {
|
|
40
|
+
return markdown;
|
|
41
|
+
}
|
|
42
|
+
// Extract important sections
|
|
43
|
+
const lines = markdown.split('\n');
|
|
44
|
+
let result = '';
|
|
45
|
+
let inCodeBlock = false;
|
|
46
|
+
let codeBlockContent = '';
|
|
47
|
+
let currentLength = 0;
|
|
48
|
+
for (const line of lines) {
|
|
49
|
+
// Track code blocks
|
|
50
|
+
if (line.trim().startsWith('```')) {
|
|
51
|
+
inCodeBlock = !inCodeBlock;
|
|
52
|
+
if (!inCodeBlock && codeBlockContent) {
|
|
53
|
+
// Keep complete code blocks
|
|
54
|
+
const blockToAdd = codeBlockContent + line + '\n';
|
|
55
|
+
if (currentLength + blockToAdd.length <= maxLength * 0.8) { // Reserve 20% for text
|
|
56
|
+
result += blockToAdd;
|
|
57
|
+
currentLength += blockToAdd.length;
|
|
58
|
+
}
|
|
59
|
+
codeBlockContent = '';
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
codeBlockContent = line + '\n';
|
|
63
|
+
}
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (inCodeBlock) {
|
|
67
|
+
codeBlockContent += line + '\n';
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
// Keep headers, important lines
|
|
71
|
+
if (line.startsWith('#') || line.startsWith('-') || line.startsWith('*') || line.trim().startsWith('>')) {
|
|
72
|
+
if (currentLength + line.length + 1 <= maxLength) {
|
|
73
|
+
result += line + '\n';
|
|
74
|
+
currentLength += line.length + 1;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
else if (line.trim() && currentLength + line.length + 1 <= maxLength) {
|
|
78
|
+
result += line + '\n';
|
|
79
|
+
currentLength += line.length + 1;
|
|
80
|
+
}
|
|
81
|
+
if (currentLength >= maxLength)
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
return result.trim();
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Search Spring projects with caching and retry logic - REAL API ONLY
|
|
28
88
|
*/
|
|
29
89
|
async searchSpringProjects(query, limit = 10) {
|
|
30
90
|
const cacheKey = `projects:${query}:${limit}`;
|
|
@@ -42,7 +102,8 @@ export class SpringBootDocsServiceOptimized {
|
|
|
42
102
|
const html = await response.text();
|
|
43
103
|
const $ = cheerio.load(html);
|
|
44
104
|
const projects = [];
|
|
45
|
-
|
|
105
|
+
// Parse actual Spring projects page
|
|
106
|
+
$('.project-list .project, .project-item, .card, .project-card, .project').each((_, element) => {
|
|
46
107
|
const $project = $(element);
|
|
47
108
|
const title = $project.find('h2, h3, .title, .project-title').first().text().trim();
|
|
48
109
|
const description = $project.find('p, .description, .summary').first().text().trim();
|
|
@@ -57,29 +118,17 @@ export class SpringBootDocsServiceOptimized {
|
|
|
57
118
|
});
|
|
58
119
|
}
|
|
59
120
|
});
|
|
60
|
-
// If few results, add popular projects
|
|
61
|
-
if (projects.length < 3) {
|
|
62
|
-
const popularProjects = this.getPopularSpringProjects()
|
|
63
|
-
.filter(project => project.title.toLowerCase().includes(query.toLowerCase()) ||
|
|
64
|
-
project.description.toLowerCase().includes(query.toLowerCase()) ||
|
|
65
|
-
project.keywords.some((k) => k.toLowerCase().includes(query.toLowerCase())));
|
|
66
|
-
projects.push(...popularProjects);
|
|
67
|
-
}
|
|
68
121
|
const results = projects.slice(0, limit);
|
|
69
122
|
this.cache.set(cacheKey, results);
|
|
70
123
|
return results;
|
|
71
124
|
}
|
|
72
125
|
catch (error) {
|
|
73
126
|
console.error('Error searching Spring projects:', error);
|
|
74
|
-
|
|
75
|
-
return this.getPopularSpringProjects()
|
|
76
|
-
.filter(project => project.title.toLowerCase().includes(query.toLowerCase()) ||
|
|
77
|
-
project.description.toLowerCase().includes(query.toLowerCase()))
|
|
78
|
-
.slice(0, limit);
|
|
127
|
+
return [];
|
|
79
128
|
}
|
|
80
129
|
}
|
|
81
130
|
/**
|
|
82
|
-
* Get Spring project details
|
|
131
|
+
* Get Spring project details - REAL API ONLY
|
|
83
132
|
*/
|
|
84
133
|
async getSpringProject(projectName) {
|
|
85
134
|
const cacheKey = `project:${projectName}`;
|
|
@@ -90,7 +139,7 @@ export class SpringBootDocsServiceOptimized {
|
|
|
90
139
|
}
|
|
91
140
|
console.error(`🔍 Fetching project: ${projectName}`);
|
|
92
141
|
try {
|
|
93
|
-
const url = `${this.springProjectsUrl}/${projectName}`;
|
|
142
|
+
const url = `${this.springProjectsUrl}/${projectName.toLowerCase().replace(/\s+/g, '-')}`;
|
|
94
143
|
const response = await this.fetchWithRetry(url);
|
|
95
144
|
if (!response.ok) {
|
|
96
145
|
throw new Error(`Project not found: ${projectName}`);
|
|
@@ -99,28 +148,21 @@ export class SpringBootDocsServiceOptimized {
|
|
|
99
148
|
const $ = cheerio.load(html);
|
|
100
149
|
const content = $('.project-overview, .content, main, .project-details').first();
|
|
101
150
|
if (content.length === 0) {
|
|
102
|
-
throw new Error('
|
|
151
|
+
throw new Error('No content found for project');
|
|
103
152
|
}
|
|
104
|
-
this.cleanHtml($, content);
|
|
105
153
|
const markdown = this.turndownService.turndown(content.html() || '');
|
|
106
|
-
const
|
|
107
|
-
const
|
|
108
|
-
let result = `# Spring Project: ${projectName}\n\n`;
|
|
109
|
-
if (version)
|
|
110
|
-
result += `**Current Version:** ${version}\n\n`;
|
|
111
|
-
if (status)
|
|
112
|
-
result += `**Status:** ${status}\n\n`;
|
|
113
|
-
result += markdown;
|
|
154
|
+
const projectUrl = `${this.springProjectsUrl}/${projectName.toLowerCase().replace(/\s+/g, '-')}`;
|
|
155
|
+
const result = `# ${projectName}\n\n${markdown.substring(0, 1500)}...\n\nFor complete project info, visit: ${projectUrl}`;
|
|
114
156
|
this.cache.setLongTerm(cacheKey, result);
|
|
115
157
|
return result;
|
|
116
158
|
}
|
|
117
159
|
catch (error) {
|
|
118
|
-
console.error(`Error
|
|
119
|
-
throw
|
|
160
|
+
console.error(`Error fetching project ${projectName}:`, error);
|
|
161
|
+
throw error;
|
|
120
162
|
}
|
|
121
163
|
}
|
|
122
164
|
/**
|
|
123
|
-
* Get all Spring guides
|
|
165
|
+
* Get all Spring guides - REAL API ONLY
|
|
124
166
|
*/
|
|
125
167
|
async getAllSpringGuides(category, limit = 20) {
|
|
126
168
|
const cacheKey = `guides:${category || 'all'}:${limit}`;
|
|
@@ -138,104 +180,94 @@ export class SpringBootDocsServiceOptimized {
|
|
|
138
180
|
const html = await response.text();
|
|
139
181
|
const $ = cheerio.load(html);
|
|
140
182
|
const guides = [];
|
|
141
|
-
|
|
183
|
+
// Parse actual Spring guides page
|
|
184
|
+
$('.guide-item, .card, .guide-card, .list-item, .guide').each((_, element) => {
|
|
142
185
|
const $guide = $(element);
|
|
143
|
-
const title = $guide.find('h2, h3, .title, .guide-title').first().text().trim();
|
|
186
|
+
const title = $guide.find('h2, h3, .title, .guide-title, a').first().text().trim();
|
|
144
187
|
const description = $guide.find('p, .description, .summary').first().text().trim();
|
|
188
|
+
const guideCategory = $guide.find('.category, .badge, .label').first().text().trim();
|
|
145
189
|
const link = $guide.find('a').first().attr('href');
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
}
|
|
190
|
+
if (title) {
|
|
191
|
+
if (!category || guideCategory.toLowerCase().includes(category.toLowerCase())) {
|
|
192
|
+
guides.push({
|
|
193
|
+
type: 'spring-guide',
|
|
194
|
+
title: title,
|
|
195
|
+
description: description || 'Spring guide',
|
|
196
|
+
category: guideCategory || 'General',
|
|
197
|
+
url: link?.startsWith('http') ? link : `https://spring.io${link}`,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
156
200
|
}
|
|
157
201
|
});
|
|
158
|
-
// If few results, add popular guides
|
|
159
|
-
if (guides.length < 5) {
|
|
160
|
-
const popularGuides = this.getPopularSpringGuides();
|
|
161
|
-
guides.push(...popularGuides);
|
|
162
|
-
}
|
|
163
202
|
const results = guides.slice(0, limit);
|
|
164
203
|
this.cache.set(cacheKey, results);
|
|
165
204
|
return results;
|
|
166
205
|
}
|
|
167
206
|
catch (error) {
|
|
168
207
|
console.error('Error retrieving Spring guides:', error);
|
|
169
|
-
return
|
|
208
|
+
return [];
|
|
170
209
|
}
|
|
171
210
|
}
|
|
172
211
|
/**
|
|
173
|
-
* Get specific guide
|
|
212
|
+
* Get specific guide content - REAL API ONLY
|
|
174
213
|
*/
|
|
175
|
-
async getGuide(guideId) {
|
|
176
|
-
const cacheKey = `guide:${guideId}`;
|
|
214
|
+
async getGuide(guideId, detailLevel = 'medium') {
|
|
215
|
+
const cacheKey = `guide:${guideId}:${detailLevel}`;
|
|
177
216
|
const cached = this.cache.get(cacheKey);
|
|
178
217
|
if (cached) {
|
|
179
|
-
console.error(`✅ Cache hit for guide: ${guideId}`);
|
|
218
|
+
console.error(`✅ Cache hit for guide: ${guideId} (${detailLevel})`);
|
|
180
219
|
return cached;
|
|
181
220
|
}
|
|
182
|
-
console.error(`🔍 Fetching guide: ${guideId}`);
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
break;
|
|
201
|
-
}
|
|
202
|
-
else {
|
|
203
|
-
console.error(`❌ ${source.name} failed: ${response.status}`);
|
|
204
|
-
}
|
|
221
|
+
console.error(`🔍 Fetching guide: ${guideId} with detail level: ${detailLevel}`);
|
|
222
|
+
// Try multiple sources for guides
|
|
223
|
+
const sources = [
|
|
224
|
+
{ name: 'Spring.io', url: `${this.springGuideUrl}/${guideId}/` },
|
|
225
|
+
{ name: 'GitHub', url: `https://github.com/spring-guides/${guideId}` },
|
|
226
|
+
{ name: 'Spring.io alt', url: `${this.springGuideUrl}/gs/${guideId}/` }
|
|
227
|
+
];
|
|
228
|
+
let content = '';
|
|
229
|
+
let sourceUrl = '';
|
|
230
|
+
for (const source of sources) {
|
|
231
|
+
try {
|
|
232
|
+
console.error(`Trying ${source.name}: ${source.url}`);
|
|
233
|
+
const response = await this.fetchWithRetry(source.url);
|
|
234
|
+
if (response.ok) {
|
|
235
|
+
content = await response.text();
|
|
236
|
+
sourceUrl = source.url;
|
|
237
|
+
console.error(`✅ Success with ${source.name} (${content.length} chars)`);
|
|
238
|
+
break;
|
|
205
239
|
}
|
|
206
|
-
|
|
207
|
-
console.error(`❌ ${source.name}
|
|
240
|
+
else {
|
|
241
|
+
console.error(`❌ ${source.name} failed: ${response.status}`);
|
|
208
242
|
}
|
|
209
243
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
}
|
|
213
|
-
let result = '';
|
|
214
|
-
// Process different content types
|
|
215
|
-
if (content.includes('<html') || content.includes('<!DOCTYPE')) {
|
|
216
|
-
// HTML content
|
|
217
|
-
result = this.processHtmlGuide(content, guideId, sourceUrl);
|
|
218
|
-
}
|
|
219
|
-
else if (content.includes('=') || content.includes(':toc:') || content.includes('== ')) {
|
|
220
|
-
// AsciiDoc content
|
|
221
|
-
result = this.processAsciiDocGuide(content, guideId, sourceUrl);
|
|
222
|
-
}
|
|
223
|
-
else {
|
|
224
|
-
// Plain text
|
|
225
|
-
result = `# Spring Guide: ${guideId}\n\n*Source: ${sourceUrl}*\n\n\`\`\`\n${content}\n\`\`\``;
|
|
244
|
+
catch (error) {
|
|
245
|
+
console.error(`❌ ${source.name} error:`, error instanceof Error ? error.message : 'Unknown');
|
|
226
246
|
}
|
|
247
|
+
}
|
|
248
|
+
if (!content) {
|
|
249
|
+
throw new Error(`Guide not found: ${guideId}`);
|
|
250
|
+
}
|
|
251
|
+
try {
|
|
252
|
+
const result = this.processHtmlGuide(content, guideId, sourceUrl, detailLevel);
|
|
227
253
|
this.cache.setLongTerm(cacheKey, result);
|
|
228
254
|
return result;
|
|
229
255
|
}
|
|
230
256
|
catch (error) {
|
|
231
|
-
console.error(
|
|
232
|
-
|
|
257
|
+
console.error('Error processing guide content:', error);
|
|
258
|
+
throw error;
|
|
233
259
|
}
|
|
234
260
|
}
|
|
235
261
|
/**
|
|
236
|
-
* Get reference documentation
|
|
262
|
+
* Get Spring reference documentation - alias for backward compatibility
|
|
237
263
|
*/
|
|
238
264
|
async getReference(section, subsection) {
|
|
265
|
+
return this.getSpringReference(section, subsection);
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Get Spring reference documentation - REAL API ONLY
|
|
269
|
+
*/
|
|
270
|
+
async getSpringReference(section, subsection) {
|
|
239
271
|
const cacheKey = `reference:${section}:${subsection || 'main'}`;
|
|
240
272
|
const cached = this.cache.get(cacheKey);
|
|
241
273
|
if (cached) {
|
|
@@ -244,13 +276,7 @@ export class SpringBootDocsServiceOptimized {
|
|
|
244
276
|
}
|
|
245
277
|
console.error(`🔍 Fetching reference: ${section}`);
|
|
246
278
|
try {
|
|
247
|
-
|
|
248
|
-
if (subsection) {
|
|
249
|
-
url += `${section}.html#${subsection}`;
|
|
250
|
-
}
|
|
251
|
-
else {
|
|
252
|
-
url += `${section}.html`;
|
|
253
|
-
}
|
|
279
|
+
const url = `${this.baseUrl}/spring-boot/docs/${this.springBootVersion}/reference/html/${section}.html`;
|
|
254
280
|
const response = await this.fetchWithRetry(url);
|
|
255
281
|
if (!response.ok) {
|
|
256
282
|
throw new Error(`Reference section not found: ${section}`);
|
|
@@ -259,84 +285,159 @@ export class SpringBootDocsServiceOptimized {
|
|
|
259
285
|
const $ = cheerio.load(html);
|
|
260
286
|
const content = $('.content, .section, main').first();
|
|
261
287
|
if (content.length === 0) {
|
|
262
|
-
throw new Error('
|
|
288
|
+
throw new Error('No content found in reference documentation');
|
|
263
289
|
}
|
|
264
|
-
this.cleanHtml($, content);
|
|
265
290
|
const markdown = this.turndownService.turndown(content.html() || '');
|
|
266
|
-
const result = `# Spring Boot Reference: ${section}\n\n${markdown}`;
|
|
291
|
+
const result = `# Spring Boot Reference: ${section}\n\n${markdown.substring(0, 1500)}...\n\nFor complete reference, visit: ${url}`;
|
|
267
292
|
this.cache.setLongTerm(cacheKey, result);
|
|
268
293
|
return result;
|
|
269
294
|
}
|
|
270
295
|
catch (error) {
|
|
271
|
-
console.error(`Error
|
|
272
|
-
throw
|
|
296
|
+
console.error(`Error fetching reference ${section}:`, error);
|
|
297
|
+
throw error;
|
|
273
298
|
}
|
|
274
299
|
}
|
|
275
300
|
/**
|
|
276
|
-
* Search concepts
|
|
301
|
+
* Search Spring concepts - alias for backward compatibility
|
|
277
302
|
*/
|
|
278
303
|
async searchConcepts(concept, category) {
|
|
304
|
+
return this.searchSpringConcepts(concept, category);
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Search Spring concepts - using real documentation
|
|
308
|
+
*/
|
|
309
|
+
async searchSpringConcepts(concept, category) {
|
|
279
310
|
const cacheKey = `concepts:${concept}:${category || 'all'}`;
|
|
280
311
|
const cached = this.cache.get(cacheKey);
|
|
281
|
-
if (cached)
|
|
282
|
-
console.error(`✅ Cache hit for concepts: ${concept}`);
|
|
312
|
+
if (cached)
|
|
283
313
|
return cached;
|
|
284
|
-
}
|
|
285
|
-
console.error(`🔍 Searching concepts: ${concept}`);
|
|
286
314
|
try {
|
|
287
|
-
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
315
|
+
// Search in Spring Boot reference documentation
|
|
316
|
+
const searchUrl = `${this.baseUrl}/spring-boot/docs/current/reference/html/`;
|
|
317
|
+
const response = await this.fetchWithRetry(searchUrl);
|
|
318
|
+
if (!response.ok) {
|
|
319
|
+
throw new Error('Unable to access Spring Boot documentation');
|
|
320
|
+
}
|
|
321
|
+
const html = await response.text();
|
|
322
|
+
const $ = cheerio.load(html);
|
|
323
|
+
// Look for concept in documentation
|
|
324
|
+
let conceptContent = '';
|
|
325
|
+
let foundSections = 0;
|
|
326
|
+
const maxSections = 3;
|
|
327
|
+
$('h1, h2, h3, h4').each((_, element) => {
|
|
328
|
+
if (foundSections >= maxSections)
|
|
329
|
+
return false; // Stop after 3 sections
|
|
330
|
+
const heading = $(element);
|
|
331
|
+
const headingText = heading.text().toLowerCase();
|
|
332
|
+
if (headingText.includes(concept.toLowerCase())) {
|
|
333
|
+
const section = heading.parent();
|
|
334
|
+
const sectionMarkdown = this.turndownService.turndown(section.html() || '');
|
|
335
|
+
conceptContent += sectionMarkdown.substring(0, 500) + '\n\n';
|
|
336
|
+
foundSections++;
|
|
297
337
|
}
|
|
338
|
+
});
|
|
339
|
+
if (!conceptContent) {
|
|
340
|
+
conceptContent = `# Spring Concept: ${concept}\n\nConcept not found in documentation. Try searching for more specific terms.`;
|
|
298
341
|
}
|
|
299
|
-
|
|
300
|
-
|
|
342
|
+
else {
|
|
343
|
+
conceptContent = `# Spring Concept: ${concept}\n\n${conceptContent}\n\nFor complete documentation, visit: ${searchUrl}`;
|
|
344
|
+
}
|
|
345
|
+
this.cache.set(cacheKey, conceptContent);
|
|
346
|
+
return conceptContent;
|
|
301
347
|
}
|
|
302
348
|
catch (error) {
|
|
303
349
|
console.error('Error searching concepts:', error);
|
|
304
|
-
|
|
350
|
+
return `# Spring Concept Search Error\n\nUnable to search for concept "${concept}": ${error instanceof Error ? error.message : 'Unknown error'}`;
|
|
305
351
|
}
|
|
306
352
|
}
|
|
307
353
|
/**
|
|
308
|
-
*
|
|
354
|
+
* Search documentation with real API - alias for backward compatibility
|
|
309
355
|
*/
|
|
310
356
|
async searchDocumentation(query, docType = 'all', limit = 10) {
|
|
357
|
+
return this.searchSpringDocs(query, docType, limit);
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Search documentation with real API
|
|
361
|
+
*/
|
|
362
|
+
async searchSpringDocs(query, docType = 'all', limit = 10) {
|
|
311
363
|
const cacheKey = `docs:${query}:${docType}:${limit}`;
|
|
312
364
|
const cached = this.cache.get(cacheKey);
|
|
313
|
-
if (cached)
|
|
314
|
-
console.error(`✅ Cache hit for documentation search: ${query}`);
|
|
365
|
+
if (cached)
|
|
315
366
|
return cached;
|
|
316
|
-
|
|
317
|
-
console.error(`🔍 Searching documentation: ${query}`);
|
|
367
|
+
const results = [];
|
|
318
368
|
try {
|
|
319
|
-
const searchPromises = [];
|
|
320
369
|
if (docType === 'all' || docType === 'guides') {
|
|
321
|
-
|
|
370
|
+
const guides = await this.getAllSpringGuides(undefined, limit);
|
|
371
|
+
const filteredGuides = guides.filter(guide => guide.title.toLowerCase().includes(query.toLowerCase()) ||
|
|
372
|
+
guide.description.toLowerCase().includes(query.toLowerCase()));
|
|
373
|
+
results.push(...filteredGuides);
|
|
322
374
|
}
|
|
323
|
-
if (docType === 'all' || docType === '
|
|
324
|
-
|
|
375
|
+
if (docType === 'all' || docType === 'projects') {
|
|
376
|
+
const projects = await this.searchSpringProjects(query, limit);
|
|
377
|
+
results.push(...projects);
|
|
325
378
|
}
|
|
326
|
-
if (docType === 'all' || docType === '
|
|
327
|
-
|
|
379
|
+
if (docType === 'all' || docType === 'reference') {
|
|
380
|
+
// Search in reference documentation
|
|
381
|
+
const refResults = await this.searchInReference(query, limit);
|
|
382
|
+
results.push(...refResults);
|
|
328
383
|
}
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
this.cache.set(cacheKey, flatResults);
|
|
332
|
-
return flatResults;
|
|
384
|
+
this.cache.set(cacheKey, results);
|
|
385
|
+
return results.slice(0, limit);
|
|
333
386
|
}
|
|
334
387
|
catch (error) {
|
|
335
388
|
console.error('Error searching documentation:', error);
|
|
336
|
-
|
|
389
|
+
return [];
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
async searchInReference(query, limit) {
|
|
393
|
+
try {
|
|
394
|
+
const response = await this.fetchWithRetry(`${this.baseUrl}/spring-boot/docs/current/reference/html/`);
|
|
395
|
+
if (!response.ok)
|
|
396
|
+
return [];
|
|
397
|
+
const html = await response.text();
|
|
398
|
+
const $ = cheerio.load(html);
|
|
399
|
+
const results = [];
|
|
400
|
+
$('nav a, .toc a, .nav-link').each((_, element) => {
|
|
401
|
+
const link = $(element);
|
|
402
|
+
const text = link.text().trim();
|
|
403
|
+
const href = link.attr('href');
|
|
404
|
+
if (text && href && text.toLowerCase().includes(query.toLowerCase())) {
|
|
405
|
+
results.push({
|
|
406
|
+
type: 'reference',
|
|
407
|
+
title: `Spring Boot: ${text}`,
|
|
408
|
+
url: href.startsWith('http') ? href : `${this.baseUrl}/spring-boot/docs/current/reference/html/${href}`,
|
|
409
|
+
description: `Reference documentation section`
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
return results.slice(0, limit);
|
|
414
|
+
}
|
|
415
|
+
catch (error) {
|
|
416
|
+
console.error('Error searching reference:', error);
|
|
417
|
+
return [];
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
processHtmlGuide(content, guideId, sourceUrl, detailLevel = 'medium') {
|
|
421
|
+
console.log(`Processing HTML content with detail level: ${detailLevel}...`);
|
|
422
|
+
const $ = cheerio.load(content);
|
|
423
|
+
// Remove navigation and footer elements
|
|
424
|
+
$('nav, footer, .navbar, .sidebar, #js-sidebar').remove();
|
|
425
|
+
// Get main content
|
|
426
|
+
const mainContent = $('.content, .guide-content, main, .markdown-body, .guide-body, article').first();
|
|
427
|
+
let markdown;
|
|
428
|
+
if (mainContent.length === 0) {
|
|
429
|
+
console.log('No main content found, using body');
|
|
430
|
+
$('script, style').remove();
|
|
431
|
+
markdown = this.turndownService.turndown($('body').html() || '');
|
|
432
|
+
}
|
|
433
|
+
else {
|
|
434
|
+
markdown = this.turndownService.turndown(mainContent.html() || '');
|
|
337
435
|
}
|
|
436
|
+
// Use intelligent extraction
|
|
437
|
+
const extractedContent = this.extractIntelligentContent(markdown, detailLevel);
|
|
438
|
+
const needsTruncation = markdown.length > extractedContent.length;
|
|
439
|
+
return `# Spring Guide: ${guideId}\n\n**Source:** ${sourceUrl}\n**Detail Level:** ${detailLevel}\n\n${extractedContent}${needsTruncation ? '\n\n---\n*Content truncated for brevity. Use detail_level="full" for complete guide or visit the link above.*' : ''}`;
|
|
338
440
|
}
|
|
339
|
-
// Private helper methods
|
|
340
441
|
async fetchWithRetry(url, timeout = this.REQUEST_TIMEOUT, retries = this.MAX_RETRIES) {
|
|
341
442
|
for (let attempt = 1; attempt <= retries; attempt++) {
|
|
342
443
|
const controller = new AbortController();
|
|
@@ -345,7 +446,7 @@ export class SpringBootDocsServiceOptimized {
|
|
|
345
446
|
const response = await fetch(url, {
|
|
346
447
|
signal: controller.signal,
|
|
347
448
|
headers: {
|
|
348
|
-
'User-Agent': 'Spring-Docs-MCP/1.2.
|
|
449
|
+
'User-Agent': 'Spring-Docs-MCP/1.2.4',
|
|
349
450
|
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
350
451
|
'Accept-Language': 'en-US,en;q=0.9'
|
|
351
452
|
}
|
|
@@ -363,231 +464,5 @@ export class SpringBootDocsServiceOptimized {
|
|
|
363
464
|
}
|
|
364
465
|
throw new Error('All retry attempts failed');
|
|
365
466
|
}
|
|
366
|
-
processHtmlGuide(content, guideId, sourceUrl) {
|
|
367
|
-
console.log('Processing HTML content...');
|
|
368
|
-
const $ = cheerio.load(content);
|
|
369
|
-
let mainContent = $('.guide-content, .content, main, .container .row .col-md-8, .markdown-body, article').first();
|
|
370
|
-
if (mainContent.length === 0) {
|
|
371
|
-
const bodyContent = $('.guide-body, .guide-content, .content').first();
|
|
372
|
-
if (bodyContent.length > 0) {
|
|
373
|
-
mainContent = bodyContent;
|
|
374
|
-
}
|
|
375
|
-
else {
|
|
376
|
-
mainContent = $('body');
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
if (mainContent.length > 0) {
|
|
380
|
-
this.cleanHtml($, mainContent);
|
|
381
|
-
const markdown = this.turndownService.turndown(mainContent.html() || '');
|
|
382
|
-
return `# Spring Guide: ${guideId}\n\n*Source: ${sourceUrl}*\n\n${markdown}`;
|
|
383
|
-
}
|
|
384
|
-
return this.generateGuideNotFoundMessage(guideId);
|
|
385
|
-
}
|
|
386
|
-
processAsciiDocGuide(content, guideId, sourceUrl) {
|
|
387
|
-
console.log('Processing AsciiDoc content...');
|
|
388
|
-
let processedContent = content
|
|
389
|
-
.replace(/^= (.+)$/gm, '# $1')
|
|
390
|
-
.replace(/^== (.+)$/gm, '## $1')
|
|
391
|
-
.replace(/^=== (.+)$/gm, '### $1')
|
|
392
|
-
.replace(/^==== (.+)$/gm, '#### $1')
|
|
393
|
-
.replace(/^\[source,(\w+)\]$/gm, '')
|
|
394
|
-
.replace(/^----$/gm, '```')
|
|
395
|
-
.replace(/^\+\+\+\+$/gm, '```');
|
|
396
|
-
return `# Spring Guide: ${guideId}\n\n*Source: ${sourceUrl}*\n\n${processedContent}`;
|
|
397
|
-
}
|
|
398
|
-
generateGuideNotFoundMessage(guideId) {
|
|
399
|
-
return `# Spring Guide: ${guideId}
|
|
400
|
-
|
|
401
|
-
❌ **Guide not found**
|
|
402
|
-
|
|
403
|
-
The guide "${guideId}" could not be retrieved from any source.
|
|
404
|
-
|
|
405
|
-
## 📚 Popular guides available:
|
|
406
|
-
- \`gs-rest-service\` - Building a RESTful Web Service
|
|
407
|
-
- \`gs-spring-boot\` - Building an Application with Spring Boot
|
|
408
|
-
- \`gs-accessing-data-jpa\` - Accessing Data with JPA
|
|
409
|
-
- \`gs-securing-web\` - Securing a Web Application
|
|
410
|
-
- \`gs-actuator-service\` - Building a RESTful Web Service with Spring Boot Actuator
|
|
411
|
-
- \`gs-testing-web\` - Testing the Web Layer
|
|
412
|
-
|
|
413
|
-
## 🔧 Alternative tools:
|
|
414
|
-
1. \`get_all_spring_guides\` - See all available guides
|
|
415
|
-
2. \`search_spring_docs\` - Search documentation
|
|
416
|
-
|
|
417
|
-
## 💡 Identifier format:
|
|
418
|
-
The identifier should be in format: \`gs-guide-name\``;
|
|
419
|
-
}
|
|
420
|
-
generateGuideErrorMessage(guideId, error) {
|
|
421
|
-
return `# Spring Guide: ${guideId}
|
|
422
|
-
|
|
423
|
-
❌ **Error retrieving guide**
|
|
424
|
-
|
|
425
|
-
The guide "${guideId}" could not be retrieved.
|
|
426
|
-
|
|
427
|
-
## 🔧 Possible solutions:
|
|
428
|
-
1. Check the guide identifier (format: \`gs-guide-name\`)
|
|
429
|
-
2. Use \`get_all_spring_guides\` to see all available guides
|
|
430
|
-
3. Use \`search_spring_docs\` to search documentation
|
|
431
|
-
|
|
432
|
-
## 📚 Popular guides:
|
|
433
|
-
- \`gs-rest-service\` - Creating a RESTful Web Service
|
|
434
|
-
- \`gs-spring-boot\` - Building an Application with Spring Boot
|
|
435
|
-
- \`gs-accessing-data-jpa\` - Accessing Data with JPA
|
|
436
|
-
- \`gs-securing-web\` - Securing a Web Application
|
|
437
|
-
|
|
438
|
-
**Technical error:** ${error instanceof Error ? error.message : 'Unknown error'}`;
|
|
439
|
-
}
|
|
440
|
-
cleanHtml($, content) {
|
|
441
|
-
content.find('script, style, nav, .sidebar, .toc, .feedback').remove();
|
|
442
|
-
content.find('*').each((index, element) => {
|
|
443
|
-
$(element).removeAttr('class').removeAttr('style').removeAttr('id');
|
|
444
|
-
});
|
|
445
|
-
}
|
|
446
|
-
// Static data methods (unchanged from original)
|
|
447
|
-
getPopularSpringProjects() {
|
|
448
|
-
return [
|
|
449
|
-
{
|
|
450
|
-
type: 'spring-project',
|
|
451
|
-
title: 'Spring Boot',
|
|
452
|
-
description: 'Create stand-alone, production-grade Spring applications easily',
|
|
453
|
-
url: 'https://spring.io/projects/spring-boot',
|
|
454
|
-
keywords: ['boot', 'microservices', 'web', 'standalone'],
|
|
455
|
-
},
|
|
456
|
-
{
|
|
457
|
-
type: 'spring-project',
|
|
458
|
-
title: 'Spring Framework',
|
|
459
|
-
description: 'The most popular Java framework for building enterprise applications',
|
|
460
|
-
url: 'https://spring.io/projects/spring-framework',
|
|
461
|
-
keywords: ['framework', 'core', 'mvc', 'ioc', 'dependency injection'],
|
|
462
|
-
},
|
|
463
|
-
{
|
|
464
|
-
type: 'spring-project',
|
|
465
|
-
title: 'Spring Data',
|
|
466
|
-
description: 'Provides a familiar programming model for data access',
|
|
467
|
-
url: 'https://spring.io/projects/spring-data',
|
|
468
|
-
keywords: ['data', 'jpa', 'mongodb', 'redis', 'repository'],
|
|
469
|
-
},
|
|
470
|
-
{
|
|
471
|
-
type: 'spring-project',
|
|
472
|
-
title: 'Spring Security',
|
|
473
|
-
description: 'Powerful and highly customizable security framework',
|
|
474
|
-
url: 'https://spring.io/projects/spring-security',
|
|
475
|
-
keywords: ['security', 'authentication', 'authorization', 'oauth'],
|
|
476
|
-
},
|
|
477
|
-
{
|
|
478
|
-
type: 'spring-project',
|
|
479
|
-
title: 'Spring Cloud',
|
|
480
|
-
description: 'Tools for rapidly building distributed system patterns',
|
|
481
|
-
url: 'https://spring.io/projects/spring-cloud',
|
|
482
|
-
keywords: ['cloud', 'microservices', 'distributed', 'netflix'],
|
|
483
|
-
},
|
|
484
|
-
];
|
|
485
|
-
}
|
|
486
|
-
getPopularSpringGuides() {
|
|
487
|
-
return [
|
|
488
|
-
{
|
|
489
|
-
type: 'spring-guide',
|
|
490
|
-
title: 'Building a RESTful Web Service',
|
|
491
|
-
description: 'Learn how to create a RESTful web service with Spring Boot',
|
|
492
|
-
category: 'Web',
|
|
493
|
-
url: 'https://spring.io/guides/gs/rest-service/',
|
|
494
|
-
id: 'gs-rest-service',
|
|
495
|
-
},
|
|
496
|
-
{
|
|
497
|
-
type: 'spring-guide',
|
|
498
|
-
title: 'Building an Application with Spring Boot',
|
|
499
|
-
description: 'Learn how to build an application with minimal configuration',
|
|
500
|
-
category: 'Getting Started',
|
|
501
|
-
url: 'https://spring.io/guides/gs/spring-boot/',
|
|
502
|
-
id: 'gs-spring-boot',
|
|
503
|
-
},
|
|
504
|
-
{
|
|
505
|
-
type: 'spring-guide',
|
|
506
|
-
title: 'Accessing Data with JPA',
|
|
507
|
-
description: 'Learn how to work with JPA data persistence',
|
|
508
|
-
category: 'Data',
|
|
509
|
-
url: 'https://spring.io/guides/gs/accessing-data-jpa/',
|
|
510
|
-
id: 'gs-accessing-data-jpa',
|
|
511
|
-
},
|
|
512
|
-
];
|
|
513
|
-
}
|
|
514
|
-
async searchGuides(query, limit) {
|
|
515
|
-
const guides = [
|
|
516
|
-
{ id: 'gs-rest-service', title: 'Building a RESTful Web Service', description: 'Learn how to create a RESTful web service with Spring Boot' },
|
|
517
|
-
{ id: 'gs-spring-boot', title: 'Building an Application with Spring Boot', description: 'Learn how to build an application with minimal configuration' },
|
|
518
|
-
{ id: 'gs-accessing-data-jpa', title: 'Accessing Data with JPA', description: 'Learn how to work with JPA data persistence' },
|
|
519
|
-
];
|
|
520
|
-
return guides
|
|
521
|
-
.filter(guide => guide.title.toLowerCase().includes(query.toLowerCase()) ||
|
|
522
|
-
guide.description.toLowerCase().includes(query.toLowerCase()))
|
|
523
|
-
.slice(0, limit)
|
|
524
|
-
.map(guide => ({
|
|
525
|
-
type: 'guide',
|
|
526
|
-
title: guide.title,
|
|
527
|
-
url: `${this.springGuideUrl}/${guide.id}/`,
|
|
528
|
-
description: guide.description,
|
|
529
|
-
id: guide.id,
|
|
530
|
-
}));
|
|
531
|
-
}
|
|
532
|
-
async searchReference(query, limit) {
|
|
533
|
-
const sections = [
|
|
534
|
-
{ name: 'web', title: 'Web Applications', description: 'Building web applications with Spring Boot' },
|
|
535
|
-
{ name: 'data', title: 'Data Access', description: 'Working with databases and data sources' },
|
|
536
|
-
{ name: 'security', title: 'Security', description: 'Securing Spring Boot applications' },
|
|
537
|
-
];
|
|
538
|
-
return sections
|
|
539
|
-
.filter(section => section.title.toLowerCase().includes(query.toLowerCase()) ||
|
|
540
|
-
section.description.toLowerCase().includes(query.toLowerCase()))
|
|
541
|
-
.slice(0, limit)
|
|
542
|
-
.map(section => ({
|
|
543
|
-
type: 'reference',
|
|
544
|
-
title: section.title,
|
|
545
|
-
url: `${this.baseUrl}/spring-boot/docs/${this.springBootVersion}/reference/html/${section.name}.html`,
|
|
546
|
-
description: section.description,
|
|
547
|
-
section: section.name,
|
|
548
|
-
}));
|
|
549
|
-
}
|
|
550
|
-
async searchAPI(query, limit) {
|
|
551
|
-
const apiSections = [
|
|
552
|
-
{ name: 'Configuration Properties', description: 'Spring Boot configuration properties reference' },
|
|
553
|
-
{ name: 'Auto-configuration Classes', description: 'List of auto-configuration classes' },
|
|
554
|
-
];
|
|
555
|
-
return apiSections
|
|
556
|
-
.filter(section => section.name.toLowerCase().includes(query.toLowerCase()) ||
|
|
557
|
-
section.description.toLowerCase().includes(query.toLowerCase()))
|
|
558
|
-
.slice(0, limit)
|
|
559
|
-
.map(section => ({
|
|
560
|
-
type: 'api',
|
|
561
|
-
title: section.name,
|
|
562
|
-
url: `${this.baseUrl}/spring-boot/docs/${this.springBootVersion}/api/`,
|
|
563
|
-
description: section.description,
|
|
564
|
-
}));
|
|
565
|
-
}
|
|
566
|
-
getSpringBootConcepts() {
|
|
567
|
-
return {
|
|
568
|
-
core: [
|
|
569
|
-
{
|
|
570
|
-
name: 'Auto-configuration',
|
|
571
|
-
description: 'Mechanism that automatically configures Spring Boot based on present dependencies',
|
|
572
|
-
keywords: ['auto-configuration', '@EnableAutoConfiguration', 'conditional'],
|
|
573
|
-
examples: ['@SpringBootApplication', '@Conditional', 'spring.factories'],
|
|
574
|
-
},
|
|
575
|
-
{
|
|
576
|
-
name: 'Profiles',
|
|
577
|
-
description: 'Allows separating parts of application configuration for different environments',
|
|
578
|
-
keywords: ['profiles', '@Profile', 'application.properties'],
|
|
579
|
-
examples: ['application-dev.properties', '@Profile("prod")', 'spring.profiles.active'],
|
|
580
|
-
},
|
|
581
|
-
],
|
|
582
|
-
web: [
|
|
583
|
-
{
|
|
584
|
-
name: 'Spring MVC',
|
|
585
|
-
description: 'Integrated web framework for building web applications and REST APIs',
|
|
586
|
-
keywords: ['mvc', '@Controller', '@RestController', 'web'],
|
|
587
|
-
examples: ['@GetMapping', '@PostMapping', '@RequestMapping'],
|
|
588
|
-
},
|
|
589
|
-
],
|
|
590
|
-
};
|
|
591
|
-
}
|
|
592
467
|
}
|
|
593
468
|
//# sourceMappingURL=springboot-docs-optimized.js.map
|