@enokdev/springdocs-mcp 1.2.3 → 1.2.5
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 +274 -244
- package/build/index.js +202 -57
- 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 +41 -0
- package/build/services/advanced-features.d.ts.map +1 -0
- package/build/services/advanced-features.js +416 -0
- package/build/services/advanced-features.js.map +1 -0
- package/build/services/cache.d.ts +36 -0
- package/build/services/cache.d.ts.map +1 -0
- package/build/services/cache.js +68 -0
- package/build/services/cache.js.map +1 -0
- 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 +59 -0
- package/build/services/springboot-docs-optimized.d.ts.map +1 -0
- package/build/services/springboot-docs-optimized.js +397 -0
- package/build/services/springboot-docs-optimized.js.map +1 -0
- package/build/tools/index.d.ts +262 -0
- package/build/tools/index.d.ts.map +1 -1
- package/build/tools/index.js +115 -0
- package/build/tools/index.js.map +1 -1
- package/package.json +17 -6
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optimized Spring Boot Documentation Service with caching and enhanced features
|
|
3
|
+
*/
|
|
4
|
+
export declare class SpringBootDocsServiceOptimized {
|
|
5
|
+
private readonly baseUrl;
|
|
6
|
+
private readonly springProjectsUrl;
|
|
7
|
+
private readonly springGuideUrl;
|
|
8
|
+
private readonly springBootVersion;
|
|
9
|
+
private turndownService;
|
|
10
|
+
private cache;
|
|
11
|
+
private readonly REQUEST_TIMEOUT;
|
|
12
|
+
private readonly MAX_RETRIES;
|
|
13
|
+
constructor();
|
|
14
|
+
/**
|
|
15
|
+
* Search Spring projects with caching and retry logic
|
|
16
|
+
*/
|
|
17
|
+
searchSpringProjects(query: string, limit?: number): Promise<any[]>;
|
|
18
|
+
/**
|
|
19
|
+
* Get Spring project details with caching
|
|
20
|
+
*/
|
|
21
|
+
getSpringProject(projectName: string): Promise<string>;
|
|
22
|
+
/**
|
|
23
|
+
* Get all Spring guides with caching
|
|
24
|
+
*/
|
|
25
|
+
getAllSpringGuides(category?: string, limit?: number): Promise<any[]>;
|
|
26
|
+
/**
|
|
27
|
+
* Get specific guide with enhanced error handling and caching
|
|
28
|
+
*/
|
|
29
|
+
getGuide(guideId: string): Promise<string>;
|
|
30
|
+
/**
|
|
31
|
+
* Get reference documentation with caching
|
|
32
|
+
*/
|
|
33
|
+
getReference(section: string, subsection?: string): Promise<string>;
|
|
34
|
+
/**
|
|
35
|
+
* Search concepts with caching
|
|
36
|
+
*/
|
|
37
|
+
searchConcepts(concept: string, category?: string): Promise<any>;
|
|
38
|
+
/**
|
|
39
|
+
* Enhanced documentation search with parallel processing
|
|
40
|
+
*/
|
|
41
|
+
searchDocumentation(query: string, docType?: string, limit?: number): Promise<any[]>;
|
|
42
|
+
private fetchWithRetry;
|
|
43
|
+
private processHtmlGuide;
|
|
44
|
+
private processAsciiDocGuide;
|
|
45
|
+
private generateGuideNotFoundMessage;
|
|
46
|
+
private generateGuideErrorMessage;
|
|
47
|
+
private cleanHtml;
|
|
48
|
+
private getPopularSpringProjects;
|
|
49
|
+
private getPopularSpringGuides;
|
|
50
|
+
private searchGuides;
|
|
51
|
+
private searchReference;
|
|
52
|
+
private searchAPI;
|
|
53
|
+
private getSpringBootConcepts;
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=springboot-docs-optimized-old.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"springboot-docs-optimized-old.d.ts","sourceRoot":"","sources":["../../src/services/springboot-docs-optimized-old.ts"],"names":[],"mappings":"AAKA;;GAEG;AACH,qBAAa,8BAA8B;IACzC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA4B;IACpD,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAgC;IAClE,OAAO,CAAC,QAAQ,CAAC,cAAc,CAA8B;IAC7D,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAW;IAC7C,OAAO,CAAC,eAAe,CAAkB;IACzC,OAAO,CAAC,KAAK,CAAe;IAC5B,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAS;IACzC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAK;;IAajC;;OAEG;IACG,oBAAoB,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,GAAE,MAAW;IAwD5D;;OAEG;IACG,gBAAgB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IA6C5D;;OAEG;IACG,kBAAkB,CAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,KAAK,GAAE,MAAW;IAwD9D;;OAEG;IACG,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAkEhD;;OAEG;IACG,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IA6CzE;;OAEG;IACG,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM;IAmCvD;;OAEG;IACG,mBAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,GAAE,MAAc,EAAE,KAAK,GAAE,MAAW;YAsCtE,cAAc;IA2B5B,OAAO,CAAC,gBAAgB;IAwBxB,OAAO,CAAC,oBAAoB;IAc5B,OAAO,CAAC,4BAA4B;IAuBpC,OAAO,CAAC,yBAAyB;IAqBjC,OAAO,CAAC,SAAS;IAQjB,OAAO,CAAC,wBAAwB;IAwChC,OAAO,CAAC,sBAAsB;YA6BhB,YAAY;YAsBZ,eAAe;YAsBf,SAAS;IAoBvB,OAAO,CAAC,qBAAqB;CA0B9B"}
|
|
@@ -0,0 +1,588 @@
|
|
|
1
|
+
import fetch from 'node-fetch';
|
|
2
|
+
import * as cheerio from 'cheerio';
|
|
3
|
+
import TurndownService from 'turndown';
|
|
4
|
+
import { CacheService } from './cache.js';
|
|
5
|
+
/**
|
|
6
|
+
* Optimized Spring Boot Documentation Service with caching and enhanced features
|
|
7
|
+
*/
|
|
8
|
+
export class SpringBootDocsServiceOptimized {
|
|
9
|
+
baseUrl = 'https://docs.spring.io';
|
|
10
|
+
springProjectsUrl = 'https://spring.io/projects';
|
|
11
|
+
springGuideUrl = 'https://spring.io/guides';
|
|
12
|
+
springBootVersion = '3.5.6';
|
|
13
|
+
turndownService;
|
|
14
|
+
cache;
|
|
15
|
+
REQUEST_TIMEOUT = 10000;
|
|
16
|
+
MAX_RETRIES = 3;
|
|
17
|
+
constructor() {
|
|
18
|
+
this.turndownService = new TurndownService({
|
|
19
|
+
headingStyle: 'atx',
|
|
20
|
+
codeBlockStyle: 'fenced',
|
|
21
|
+
});
|
|
22
|
+
this.cache = new CacheService();
|
|
23
|
+
// Cleanup cache every hour
|
|
24
|
+
setInterval(() => this.cache.cleanup(), 60 * 60 * 1000);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Search Spring projects with caching and retry logic
|
|
28
|
+
*/
|
|
29
|
+
async searchSpringProjects(query, limit = 10) {
|
|
30
|
+
const cacheKey = `projects:${query}:${limit}`;
|
|
31
|
+
const cached = this.cache.get(cacheKey);
|
|
32
|
+
if (cached) {
|
|
33
|
+
console.error(`✅ Cache hit for projects search: ${query}`);
|
|
34
|
+
return cached;
|
|
35
|
+
}
|
|
36
|
+
console.error(`🔍 Fetching projects for: ${query}`);
|
|
37
|
+
try {
|
|
38
|
+
const response = await this.fetchWithRetry(this.springProjectsUrl);
|
|
39
|
+
if (!response.ok) {
|
|
40
|
+
throw new Error('Unable to access Spring projects page');
|
|
41
|
+
}
|
|
42
|
+
const html = await response.text();
|
|
43
|
+
const $ = cheerio.load(html);
|
|
44
|
+
const projects = [];
|
|
45
|
+
$('.project-list .project, .project-item, .card').each((index, element) => {
|
|
46
|
+
const $project = $(element);
|
|
47
|
+
const title = $project.find('h2, h3, .title, .project-title').first().text().trim();
|
|
48
|
+
const description = $project.find('p, .description, .summary').first().text().trim();
|
|
49
|
+
const link = $project.find('a').first().attr('href');
|
|
50
|
+
if (title && (title.toLowerCase().includes(query.toLowerCase()) ||
|
|
51
|
+
description.toLowerCase().includes(query.toLowerCase()))) {
|
|
52
|
+
projects.push({
|
|
53
|
+
type: 'spring-project',
|
|
54
|
+
title: title,
|
|
55
|
+
description: description,
|
|
56
|
+
url: link?.startsWith('http') ? link : `https://spring.io${link}`,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
// If few results, try a broader search
|
|
61
|
+
if (projects.length < 3) {
|
|
62
|
+
console.error(`⚠️ Limited results for "${query}", trying broader search...`);
|
|
63
|
+
// Don't add mock data - return what we found from real API
|
|
64
|
+
}
|
|
65
|
+
const results = projects.slice(0, limit);
|
|
66
|
+
this.cache.set(cacheKey, results);
|
|
67
|
+
return results;
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
console.error('Error searching Spring projects:', error);
|
|
71
|
+
// Return empty array instead of mock data
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Get Spring project details with caching
|
|
77
|
+
*/
|
|
78
|
+
async getSpringProject(projectName) {
|
|
79
|
+
const cacheKey = `project:${projectName}`;
|
|
80
|
+
const cached = this.cache.get(cacheKey);
|
|
81
|
+
if (cached) {
|
|
82
|
+
console.error(`✅ Cache hit for project: ${projectName}`);
|
|
83
|
+
return cached;
|
|
84
|
+
}
|
|
85
|
+
console.error(`🔍 Fetching project: ${projectName}`);
|
|
86
|
+
try {
|
|
87
|
+
const url = `${this.springProjectsUrl}/${projectName}`;
|
|
88
|
+
const response = await this.fetchWithRetry(url);
|
|
89
|
+
if (!response.ok) {
|
|
90
|
+
throw new Error(`Project not found: ${projectName}`);
|
|
91
|
+
}
|
|
92
|
+
const html = await response.text();
|
|
93
|
+
const $ = cheerio.load(html);
|
|
94
|
+
const content = $('.project-overview, .content, main, .project-details').first();
|
|
95
|
+
if (content.length === 0) {
|
|
96
|
+
throw new Error('Project content not found');
|
|
97
|
+
}
|
|
98
|
+
this.cleanHtml($, content);
|
|
99
|
+
const markdown = this.turndownService.turndown(content.html() || '');
|
|
100
|
+
const version = $('.version, .current-version').first().text().trim();
|
|
101
|
+
const status = $('.status, .project-status').first().text().trim();
|
|
102
|
+
let result = `# Spring Project: ${projectName}\n\n`;
|
|
103
|
+
if (version)
|
|
104
|
+
result += `**Current Version:** ${version}\n\n`;
|
|
105
|
+
if (status)
|
|
106
|
+
result += `**Status:** ${status}\n\n`;
|
|
107
|
+
result += markdown;
|
|
108
|
+
this.cache.setLongTerm(cacheKey, result);
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
console.error(`Error retrieving project ${projectName}:`, error);
|
|
113
|
+
throw new Error(`Unable to retrieve project: ${projectName}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Get all Spring guides with caching
|
|
118
|
+
*/
|
|
119
|
+
async getAllSpringGuides(category, limit = 20) {
|
|
120
|
+
const cacheKey = `guides:${category || 'all'}:${limit}`;
|
|
121
|
+
const cached = this.cache.get(cacheKey);
|
|
122
|
+
if (cached) {
|
|
123
|
+
console.error(`✅ Cache hit for guides: ${category || 'all'}`);
|
|
124
|
+
return cached;
|
|
125
|
+
}
|
|
126
|
+
console.error(`🔍 Fetching guides for category: ${category || 'all'}`);
|
|
127
|
+
try {
|
|
128
|
+
const response = await this.fetchWithRetry(this.springGuideUrl);
|
|
129
|
+
if (!response.ok) {
|
|
130
|
+
throw new Error('Unable to access Spring guides page');
|
|
131
|
+
}
|
|
132
|
+
const html = await response.text();
|
|
133
|
+
const $ = cheerio.load(html);
|
|
134
|
+
const guides = [];
|
|
135
|
+
$('.guide-list .guide, .guide-item, .card').each((index, element) => {
|
|
136
|
+
const $guide = $(element);
|
|
137
|
+
const title = $guide.find('h2, h3, .title, .guide-title').first().text().trim();
|
|
138
|
+
const description = $guide.find('p, .description, .summary').first().text().trim();
|
|
139
|
+
const link = $guide.find('a').first().attr('href');
|
|
140
|
+
const guideCategory = $guide.find('.category, .tag').first().text().trim();
|
|
141
|
+
if (title && (!category || guideCategory.toLowerCase().includes(category.toLowerCase()))) {
|
|
142
|
+
guides.push({
|
|
143
|
+
type: 'spring-guide',
|
|
144
|
+
title: title,
|
|
145
|
+
description: description,
|
|
146
|
+
category: guideCategory,
|
|
147
|
+
url: link?.startsWith('http') ? link : `https://spring.io${link}`,
|
|
148
|
+
id: link?.split('/').pop()?.replace('.html', '') || title.toLowerCase().replace(/\s+/g, '-'),
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
// If few results, try a broader search
|
|
153
|
+
if (guides.length < 5) {
|
|
154
|
+
console.error(`⚠️ Limited guides found for "${category || 'all'}", showing available results...`);
|
|
155
|
+
// Don't add mock data - return what we found from real API
|
|
156
|
+
}
|
|
157
|
+
const results = guides.slice(0, limit);
|
|
158
|
+
this.cache.set(cacheKey, results);
|
|
159
|
+
return results;
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
console.error('Error retrieving Spring guides:', error);
|
|
163
|
+
// Return empty array instead of mock data
|
|
164
|
+
return [];
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Get specific guide with enhanced error handling and caching
|
|
169
|
+
*/
|
|
170
|
+
async getGuide(guideId) {
|
|
171
|
+
const cacheKey = `guide:${guideId}`;
|
|
172
|
+
const cached = this.cache.get(cacheKey);
|
|
173
|
+
if (cached) {
|
|
174
|
+
console.error(`✅ Cache hit for guide: ${guideId}`);
|
|
175
|
+
return cached;
|
|
176
|
+
}
|
|
177
|
+
console.error(`🔍 Fetching guide: ${guideId}`);
|
|
178
|
+
try {
|
|
179
|
+
let content = '';
|
|
180
|
+
let sourceUrl = '';
|
|
181
|
+
// Multiple sources to try
|
|
182
|
+
const sources = [
|
|
183
|
+
{ url: `https://spring.io/guides/${guideId}/`, name: 'spring.io' },
|
|
184
|
+
{ url: `https://raw.githubusercontent.com/spring-guides/${guideId}/main/README.adoc`, name: 'GitHub raw' },
|
|
185
|
+
{ url: `https://github.com/spring-guides/${guideId}/blob/main/README.adoc`, name: 'GitHub blob' }
|
|
186
|
+
];
|
|
187
|
+
for (const source of sources) {
|
|
188
|
+
try {
|
|
189
|
+
console.error(`Trying ${source.name}: ${source.url}`);
|
|
190
|
+
const response = await this.fetchWithRetry(source.url);
|
|
191
|
+
if (response.ok) {
|
|
192
|
+
content = await response.text();
|
|
193
|
+
sourceUrl = source.url;
|
|
194
|
+
console.error(`✅ Success with ${source.name} (${content.length} chars)`);
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
console.error(`❌ ${source.name} failed: ${response.status}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
console.error(`❌ ${source.name} error:`, error instanceof Error ? error.message : 'Unknown error');
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (!content) {
|
|
206
|
+
return this.generateGuideNotFoundMessage(guideId);
|
|
207
|
+
}
|
|
208
|
+
let result = '';
|
|
209
|
+
// Process different content types
|
|
210
|
+
if (content.includes('<html') || content.includes('<!DOCTYPE')) {
|
|
211
|
+
// HTML content
|
|
212
|
+
result = this.processHtmlGuide(content, guideId, sourceUrl);
|
|
213
|
+
}
|
|
214
|
+
else if (content.includes('=') || content.includes(':toc:') || content.includes('== ')) {
|
|
215
|
+
// AsciiDoc content
|
|
216
|
+
result = this.processAsciiDocGuide(content, guideId, sourceUrl);
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
// Plain text
|
|
220
|
+
result = `# Spring Guide: ${guideId}\n\n*Source: ${sourceUrl}*\n\n\`\`\`\n${content}\n\`\`\``;
|
|
221
|
+
}
|
|
222
|
+
this.cache.setLongTerm(cacheKey, result);
|
|
223
|
+
return result;
|
|
224
|
+
}
|
|
225
|
+
catch (error) {
|
|
226
|
+
console.error(`Error retrieving guide ${guideId}:`, error);
|
|
227
|
+
return this.generateGuideErrorMessage(guideId, error);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Get reference documentation with caching
|
|
232
|
+
*/
|
|
233
|
+
async getReference(section, subsection) {
|
|
234
|
+
const cacheKey = `reference:${section}:${subsection || 'main'}`;
|
|
235
|
+
const cached = this.cache.get(cacheKey);
|
|
236
|
+
if (cached) {
|
|
237
|
+
console.error(`✅ Cache hit for reference: ${section}`);
|
|
238
|
+
return cached;
|
|
239
|
+
}
|
|
240
|
+
console.error(`🔍 Fetching reference: ${section}`);
|
|
241
|
+
try {
|
|
242
|
+
let url = `${this.baseUrl}/spring-boot/docs/${this.springBootVersion}/reference/html/`;
|
|
243
|
+
if (subsection) {
|
|
244
|
+
url += `${section}.html#${subsection}`;
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
url += `${section}.html`;
|
|
248
|
+
}
|
|
249
|
+
const response = await this.fetchWithRetry(url);
|
|
250
|
+
if (!response.ok) {
|
|
251
|
+
throw new Error(`Reference section not found: ${section}`);
|
|
252
|
+
}
|
|
253
|
+
const html = await response.text();
|
|
254
|
+
const $ = cheerio.load(html);
|
|
255
|
+
const content = $('.content, .section, main').first();
|
|
256
|
+
if (content.length === 0) {
|
|
257
|
+
throw new Error('Section content not found');
|
|
258
|
+
}
|
|
259
|
+
this.cleanHtml($, content);
|
|
260
|
+
const markdown = this.turndownService.turndown(content.html() || '');
|
|
261
|
+
const result = `# Spring Boot Reference: ${section}\n\n${markdown}`;
|
|
262
|
+
this.cache.setLongTerm(cacheKey, result);
|
|
263
|
+
return result;
|
|
264
|
+
}
|
|
265
|
+
catch (error) {
|
|
266
|
+
console.error(`Error retrieving reference ${section}:`, error);
|
|
267
|
+
throw new Error(`Unable to retrieve reference section: ${section}`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Search concepts with caching
|
|
272
|
+
*/
|
|
273
|
+
async searchConcepts(concept, category) {
|
|
274
|
+
const cacheKey = `concepts:${concept}:${category || 'all'}`;
|
|
275
|
+
const cached = this.cache.get(cacheKey);
|
|
276
|
+
if (cached) {
|
|
277
|
+
console.error(`✅ Cache hit for concepts: ${concept}`);
|
|
278
|
+
return cached;
|
|
279
|
+
}
|
|
280
|
+
console.error(`🔍 Searching concepts: ${concept}`);
|
|
281
|
+
try {
|
|
282
|
+
const concepts = this.getSpringBootConcepts();
|
|
283
|
+
const results = {};
|
|
284
|
+
for (const [cat, conceptList] of Object.entries(concepts)) {
|
|
285
|
+
if (category && cat !== category)
|
|
286
|
+
continue;
|
|
287
|
+
const matches = conceptList.filter((c) => c.name.toLowerCase().includes(concept.toLowerCase()) ||
|
|
288
|
+
c.description.toLowerCase().includes(concept.toLowerCase()) ||
|
|
289
|
+
c.keywords.some((k) => k.toLowerCase().includes(concept.toLowerCase())));
|
|
290
|
+
if (matches.length > 0) {
|
|
291
|
+
results[cat] = matches;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
this.cache.setLongTerm(cacheKey, results);
|
|
295
|
+
return results;
|
|
296
|
+
}
|
|
297
|
+
catch (error) {
|
|
298
|
+
console.error('Error searching concepts:', error);
|
|
299
|
+
throw new Error('Unable to search Spring Boot concepts');
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Enhanced documentation search with parallel processing
|
|
304
|
+
*/
|
|
305
|
+
async searchDocumentation(query, docType = 'all', limit = 10) {
|
|
306
|
+
const cacheKey = `docs:${query}:${docType}:${limit}`;
|
|
307
|
+
const cached = this.cache.get(cacheKey);
|
|
308
|
+
if (cached) {
|
|
309
|
+
console.error(`✅ Cache hit for documentation search: ${query}`);
|
|
310
|
+
return cached;
|
|
311
|
+
}
|
|
312
|
+
console.error(`🔍 Searching documentation: ${query}`);
|
|
313
|
+
try {
|
|
314
|
+
const searchPromises = [];
|
|
315
|
+
if (docType === 'all' || docType === 'guides') {
|
|
316
|
+
searchPromises.push(this.searchGuides(query, Math.ceil(limit / 3)));
|
|
317
|
+
}
|
|
318
|
+
if (docType === 'all' || docType === 'reference') {
|
|
319
|
+
searchPromises.push(this.searchReference(query, Math.ceil(limit / 3)));
|
|
320
|
+
}
|
|
321
|
+
if (docType === 'all' || docType === 'api') {
|
|
322
|
+
searchPromises.push(this.searchAPI(query, Math.ceil(limit / 3)));
|
|
323
|
+
}
|
|
324
|
+
const results = await Promise.all(searchPromises);
|
|
325
|
+
const flatResults = results.flat().slice(0, limit);
|
|
326
|
+
this.cache.set(cacheKey, flatResults);
|
|
327
|
+
return flatResults;
|
|
328
|
+
}
|
|
329
|
+
catch (error) {
|
|
330
|
+
console.error('Error searching documentation:', error);
|
|
331
|
+
throw new Error('Failed to search Spring Boot documentation');
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
// Private helper methods
|
|
335
|
+
async fetchWithRetry(url, timeout = this.REQUEST_TIMEOUT, retries = this.MAX_RETRIES) {
|
|
336
|
+
for (let attempt = 1; attempt <= retries; attempt++) {
|
|
337
|
+
const controller = new AbortController();
|
|
338
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
339
|
+
try {
|
|
340
|
+
const response = await fetch(url, {
|
|
341
|
+
signal: controller.signal,
|
|
342
|
+
headers: {
|
|
343
|
+
'User-Agent': 'Spring-Docs-MCP/1.2.3',
|
|
344
|
+
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
345
|
+
'Accept-Language': 'en-US,en;q=0.9'
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
clearTimeout(timeoutId);
|
|
349
|
+
return response;
|
|
350
|
+
}
|
|
351
|
+
catch (error) {
|
|
352
|
+
clearTimeout(timeoutId);
|
|
353
|
+
if (attempt === retries)
|
|
354
|
+
throw error;
|
|
355
|
+
console.error(`Retry ${attempt}/${retries} for ${url}:`, error instanceof Error ? error.message : 'Unknown error');
|
|
356
|
+
await new Promise(resolve => setTimeout(resolve, 1000 * attempt)); // Exponential backoff
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
throw new Error('All retry attempts failed');
|
|
360
|
+
}
|
|
361
|
+
processHtmlGuide(content, guideId, sourceUrl) {
|
|
362
|
+
console.log('Processing HTML content...');
|
|
363
|
+
const $ = cheerio.load(content);
|
|
364
|
+
let mainContent = $('.guide-content, .content, main, .container .row .col-md-8, .markdown-body, article').first();
|
|
365
|
+
if (mainContent.length === 0) {
|
|
366
|
+
const bodyContent = $('.guide-body, .guide-content, .content').first();
|
|
367
|
+
if (bodyContent.length > 0) {
|
|
368
|
+
mainContent = bodyContent;
|
|
369
|
+
}
|
|
370
|
+
else {
|
|
371
|
+
mainContent = $('body');
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
if (mainContent.length > 0) {
|
|
375
|
+
this.cleanHtml($, mainContent);
|
|
376
|
+
const markdown = this.turndownService.turndown(mainContent.html() || '');
|
|
377
|
+
return `# Spring Guide: ${guideId}\n\n*Source: ${sourceUrl}*\n\n${markdown}`;
|
|
378
|
+
}
|
|
379
|
+
return this.generateGuideNotFoundMessage(guideId);
|
|
380
|
+
}
|
|
381
|
+
processAsciiDocGuide(content, guideId, sourceUrl) {
|
|
382
|
+
console.log('Processing AsciiDoc content...');
|
|
383
|
+
let processedContent = content
|
|
384
|
+
.replace(/^= (.+)$/gm, '# $1')
|
|
385
|
+
.replace(/^== (.+)$/gm, '## $1')
|
|
386
|
+
.replace(/^=== (.+)$/gm, '### $1')
|
|
387
|
+
.replace(/^==== (.+)$/gm, '#### $1')
|
|
388
|
+
.replace(/^\[source,(\w+)\]$/gm, '')
|
|
389
|
+
.replace(/^----$/gm, '```')
|
|
390
|
+
.replace(/^\+\+\+\+$/gm, '```');
|
|
391
|
+
return `# Spring Guide: ${guideId}\n\n*Source: ${sourceUrl}*\n\n${processedContent}`;
|
|
392
|
+
}
|
|
393
|
+
generateGuideNotFoundMessage(guideId) {
|
|
394
|
+
return `# Spring Guide: ${guideId}
|
|
395
|
+
|
|
396
|
+
❌ **Guide not found**
|
|
397
|
+
|
|
398
|
+
The guide "${guideId}" could not be retrieved from any source.
|
|
399
|
+
|
|
400
|
+
## 📚 Popular guides available:
|
|
401
|
+
- \`gs-rest-service\` - Building a RESTful Web Service
|
|
402
|
+
- \`gs-spring-boot\` - Building an Application with Spring Boot
|
|
403
|
+
- \`gs-accessing-data-jpa\` - Accessing Data with JPA
|
|
404
|
+
- \`gs-securing-web\` - Securing a Web Application
|
|
405
|
+
- \`gs-actuator-service\` - Building a RESTful Web Service with Spring Boot Actuator
|
|
406
|
+
- \`gs-testing-web\` - Testing the Web Layer
|
|
407
|
+
|
|
408
|
+
## 🔧 Alternative tools:
|
|
409
|
+
1. \`get_all_spring_guides\` - See all available guides
|
|
410
|
+
2. \`search_spring_docs\` - Search documentation
|
|
411
|
+
|
|
412
|
+
## 💡 Identifier format:
|
|
413
|
+
The identifier should be in format: \`gs-guide-name\``;
|
|
414
|
+
}
|
|
415
|
+
generateGuideErrorMessage(guideId, error) {
|
|
416
|
+
return `# Spring Guide: ${guideId}
|
|
417
|
+
|
|
418
|
+
❌ **Error retrieving guide**
|
|
419
|
+
|
|
420
|
+
The guide "${guideId}" could not be retrieved.
|
|
421
|
+
|
|
422
|
+
## 🔧 Possible solutions:
|
|
423
|
+
1. Check the guide identifier (format: \`gs-guide-name\`)
|
|
424
|
+
2. Use \`get_all_spring_guides\` to see all available guides
|
|
425
|
+
3. Use \`search_spring_docs\` to search documentation
|
|
426
|
+
|
|
427
|
+
## 📚 Popular guides:
|
|
428
|
+
- \`gs-rest-service\` - Creating a RESTful Web Service
|
|
429
|
+
- \`gs-spring-boot\` - Building an Application with Spring Boot
|
|
430
|
+
- \`gs-accessing-data-jpa\` - Accessing Data with JPA
|
|
431
|
+
- \`gs-securing-web\` - Securing a Web Application
|
|
432
|
+
|
|
433
|
+
**Technical error:** ${error instanceof Error ? error.message : 'Unknown error'}`;
|
|
434
|
+
}
|
|
435
|
+
cleanHtml($, content) {
|
|
436
|
+
content.find('script, style, nav, .sidebar, .toc, .feedback').remove();
|
|
437
|
+
content.find('*').each((index, element) => {
|
|
438
|
+
$(element).removeAttr('class').removeAttr('style').removeAttr('id');
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
// Static data methods (unchanged from original)
|
|
442
|
+
getPopularSpringProjects() {
|
|
443
|
+
return [
|
|
444
|
+
{
|
|
445
|
+
type: 'spring-project',
|
|
446
|
+
title: 'Spring Boot',
|
|
447
|
+
description: 'Create stand-alone, production-grade Spring applications easily',
|
|
448
|
+
url: 'https://spring.io/projects/spring-boot',
|
|
449
|
+
keywords: ['boot', 'microservices', 'web', 'standalone'],
|
|
450
|
+
},
|
|
451
|
+
{
|
|
452
|
+
type: 'spring-project',
|
|
453
|
+
title: 'Spring Framework',
|
|
454
|
+
description: 'The most popular Java framework for building enterprise applications',
|
|
455
|
+
url: 'https://spring.io/projects/spring-framework',
|
|
456
|
+
keywords: ['framework', 'core', 'mvc', 'ioc', 'dependency injection'],
|
|
457
|
+
},
|
|
458
|
+
{
|
|
459
|
+
type: 'spring-project',
|
|
460
|
+
title: 'Spring Data',
|
|
461
|
+
description: 'Provides a familiar programming model for data access',
|
|
462
|
+
url: 'https://spring.io/projects/spring-data',
|
|
463
|
+
keywords: ['data', 'jpa', 'mongodb', 'redis', 'repository'],
|
|
464
|
+
},
|
|
465
|
+
{
|
|
466
|
+
type: 'spring-project',
|
|
467
|
+
title: 'Spring Security',
|
|
468
|
+
description: 'Powerful and highly customizable security framework',
|
|
469
|
+
url: 'https://spring.io/projects/spring-security',
|
|
470
|
+
keywords: ['security', 'authentication', 'authorization', 'oauth'],
|
|
471
|
+
},
|
|
472
|
+
{
|
|
473
|
+
type: 'spring-project',
|
|
474
|
+
title: 'Spring Cloud',
|
|
475
|
+
description: 'Tools for rapidly building distributed system patterns',
|
|
476
|
+
url: 'https://spring.io/projects/spring-cloud',
|
|
477
|
+
keywords: ['cloud', 'microservices', 'distributed', 'netflix'],
|
|
478
|
+
},
|
|
479
|
+
];
|
|
480
|
+
}
|
|
481
|
+
getPopularSpringGuides() {
|
|
482
|
+
return [
|
|
483
|
+
{
|
|
484
|
+
type: 'spring-guide',
|
|
485
|
+
title: 'Building a RESTful Web Service',
|
|
486
|
+
description: 'Learn how to create a RESTful web service with Spring Boot',
|
|
487
|
+
category: 'Web',
|
|
488
|
+
url: 'https://spring.io/guides/gs/rest-service/',
|
|
489
|
+
id: 'gs-rest-service',
|
|
490
|
+
},
|
|
491
|
+
{
|
|
492
|
+
type: 'spring-guide',
|
|
493
|
+
title: 'Building an Application with Spring Boot',
|
|
494
|
+
description: 'Learn how to build an application with minimal configuration',
|
|
495
|
+
category: 'Getting Started',
|
|
496
|
+
url: 'https://spring.io/guides/gs/spring-boot/',
|
|
497
|
+
id: 'gs-spring-boot',
|
|
498
|
+
},
|
|
499
|
+
{
|
|
500
|
+
type: 'spring-guide',
|
|
501
|
+
title: 'Accessing Data with JPA',
|
|
502
|
+
description: 'Learn how to work with JPA data persistence',
|
|
503
|
+
category: 'Data',
|
|
504
|
+
url: 'https://spring.io/guides/gs/accessing-data-jpa/',
|
|
505
|
+
id: 'gs-accessing-data-jpa',
|
|
506
|
+
},
|
|
507
|
+
];
|
|
508
|
+
}
|
|
509
|
+
async searchGuides(query, limit) {
|
|
510
|
+
const guides = [
|
|
511
|
+
{ id: 'gs-rest-service', title: 'Building a RESTful Web Service', description: 'Learn how to create a RESTful web service with Spring Boot' },
|
|
512
|
+
{ id: 'gs-spring-boot', title: 'Building an Application with Spring Boot', description: 'Learn how to build an application with minimal configuration' },
|
|
513
|
+
{ id: 'gs-accessing-data-jpa', title: 'Accessing Data with JPA', description: 'Learn how to work with JPA data persistence' },
|
|
514
|
+
];
|
|
515
|
+
return guides
|
|
516
|
+
.filter(guide => guide.title.toLowerCase().includes(query.toLowerCase()) ||
|
|
517
|
+
guide.description.toLowerCase().includes(query.toLowerCase()))
|
|
518
|
+
.slice(0, limit)
|
|
519
|
+
.map(guide => ({
|
|
520
|
+
type: 'guide',
|
|
521
|
+
title: guide.title,
|
|
522
|
+
url: `${this.springGuideUrl}/${guide.id}/`,
|
|
523
|
+
description: guide.description,
|
|
524
|
+
id: guide.id,
|
|
525
|
+
}));
|
|
526
|
+
}
|
|
527
|
+
async searchReference(query, limit) {
|
|
528
|
+
const sections = [
|
|
529
|
+
{ name: 'web', title: 'Web Applications', description: 'Building web applications with Spring Boot' },
|
|
530
|
+
{ name: 'data', title: 'Data Access', description: 'Working with databases and data sources' },
|
|
531
|
+
{ name: 'security', title: 'Security', description: 'Securing Spring Boot applications' },
|
|
532
|
+
];
|
|
533
|
+
return sections
|
|
534
|
+
.filter(section => section.title.toLowerCase().includes(query.toLowerCase()) ||
|
|
535
|
+
section.description.toLowerCase().includes(query.toLowerCase()))
|
|
536
|
+
.slice(0, limit)
|
|
537
|
+
.map(section => ({
|
|
538
|
+
type: 'reference',
|
|
539
|
+
title: section.title,
|
|
540
|
+
url: `${this.baseUrl}/spring-boot/docs/${this.springBootVersion}/reference/html/${section.name}.html`,
|
|
541
|
+
description: section.description,
|
|
542
|
+
section: section.name,
|
|
543
|
+
}));
|
|
544
|
+
}
|
|
545
|
+
async searchAPI(query, limit) {
|
|
546
|
+
const apiSections = [
|
|
547
|
+
{ name: 'Configuration Properties', description: 'Spring Boot configuration properties reference' },
|
|
548
|
+
{ name: 'Auto-configuration Classes', description: 'List of auto-configuration classes' },
|
|
549
|
+
];
|
|
550
|
+
return apiSections
|
|
551
|
+
.filter(section => section.name.toLowerCase().includes(query.toLowerCase()) ||
|
|
552
|
+
section.description.toLowerCase().includes(query.toLowerCase()))
|
|
553
|
+
.slice(0, limit)
|
|
554
|
+
.map(section => ({
|
|
555
|
+
type: 'api',
|
|
556
|
+
title: section.name,
|
|
557
|
+
url: `${this.baseUrl}/spring-boot/docs/${this.springBootVersion}/api/`,
|
|
558
|
+
description: section.description,
|
|
559
|
+
}));
|
|
560
|
+
}
|
|
561
|
+
getSpringBootConcepts() {
|
|
562
|
+
return {
|
|
563
|
+
core: [
|
|
564
|
+
{
|
|
565
|
+
name: 'Auto-configuration',
|
|
566
|
+
description: 'Mechanism that automatically configures Spring Boot based on present dependencies',
|
|
567
|
+
keywords: ['auto-configuration', '@EnableAutoConfiguration', 'conditional'],
|
|
568
|
+
examples: ['@SpringBootApplication', '@Conditional', 'spring.factories'],
|
|
569
|
+
},
|
|
570
|
+
{
|
|
571
|
+
name: 'Profiles',
|
|
572
|
+
description: 'Allows separating parts of application configuration for different environments',
|
|
573
|
+
keywords: ['profiles', '@Profile', 'application.properties'],
|
|
574
|
+
examples: ['application-dev.properties', '@Profile("prod")', 'spring.profiles.active'],
|
|
575
|
+
},
|
|
576
|
+
],
|
|
577
|
+
web: [
|
|
578
|
+
{
|
|
579
|
+
name: 'Spring MVC',
|
|
580
|
+
description: 'Integrated web framework for building web applications and REST APIs',
|
|
581
|
+
keywords: ['mvc', '@Controller', '@RestController', 'web'],
|
|
582
|
+
examples: ['@GetMapping', '@PostMapping', '@RequestMapping'],
|
|
583
|
+
},
|
|
584
|
+
],
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
//# sourceMappingURL=springboot-docs-optimized-old.js.map
|