@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.
@@ -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
- * Optimized Spring Boot Documentation Service with caching and enhanced features
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
- * Search Spring projects with caching and retry logic
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
- $('.project-list .project, .project-item, .card').each((index, element) => {
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
- // Fallback to popular projects
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 with caching
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('Project content not found');
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 version = $('.version, .current-version').first().text().trim();
107
- const status = $('.status, .project-status').first().text().trim();
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 retrieving project ${projectName}:`, error);
119
- throw new Error(`Unable to retrieve project: ${projectName}`);
160
+ console.error(`Error fetching project ${projectName}:`, error);
161
+ throw error;
120
162
  }
121
163
  }
122
164
  /**
123
- * Get all Spring guides with caching
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
- $('.guide-list .guide, .guide-item, .card').each((index, element) => {
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
- const guideCategory = $guide.find('.category, .tag').first().text().trim();
147
- if (title && (!category || guideCategory.toLowerCase().includes(category.toLowerCase()))) {
148
- guides.push({
149
- type: 'spring-guide',
150
- title: title,
151
- description: description,
152
- category: guideCategory,
153
- url: link?.startsWith('http') ? link : `https://spring.io${link}`,
154
- id: link?.split('/').pop()?.replace('.html', '') || title.toLowerCase().replace(/\s+/g, '-'),
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 this.getPopularSpringGuides().slice(0, limit);
208
+ return [];
170
209
  }
171
210
  }
172
211
  /**
173
- * Get specific guide with enhanced error handling and caching
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
- try {
184
- let content = '';
185
- let sourceUrl = '';
186
- // Multiple sources to try
187
- const sources = [
188
- { url: `https://spring.io/guides/${guideId}/`, name: 'spring.io' },
189
- { url: `https://raw.githubusercontent.com/spring-guides/${guideId}/main/README.adoc`, name: 'GitHub raw' },
190
- { url: `https://github.com/spring-guides/${guideId}/blob/main/README.adoc`, name: 'GitHub blob' }
191
- ];
192
- for (const source of sources) {
193
- try {
194
- console.error(`Trying ${source.name}: ${source.url}`);
195
- const response = await this.fetchWithRetry(source.url);
196
- if (response.ok) {
197
- content = await response.text();
198
- sourceUrl = source.url;
199
- console.error(`✅ Success with ${source.name} (${content.length} chars)`);
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
- catch (error) {
207
- console.error(`❌ ${source.name} error:`, error instanceof Error ? error.message : 'Unknown error');
240
+ else {
241
+ console.error(`❌ ${source.name} failed: ${response.status}`);
208
242
  }
209
243
  }
210
- if (!content) {
211
- return this.generateGuideNotFoundMessage(guideId);
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(`Error retrieving guide ${guideId}:`, error);
232
- return this.generateGuideErrorMessage(guideId, error);
257
+ console.error('Error processing guide content:', error);
258
+ throw error;
233
259
  }
234
260
  }
235
261
  /**
236
- * Get reference documentation with caching
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
- let url = `${this.baseUrl}/spring-boot/docs/${this.springBootVersion}/reference/html/`;
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('Section content not found');
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 retrieving reference ${section}:`, error);
272
- throw new Error(`Unable to retrieve reference section: ${section}`);
296
+ console.error(`Error fetching reference ${section}:`, error);
297
+ throw error;
273
298
  }
274
299
  }
275
300
  /**
276
- * Search concepts with caching
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
- const concepts = this.getSpringBootConcepts();
288
- const results = {};
289
- for (const [cat, conceptList] of Object.entries(concepts)) {
290
- if (category && cat !== category)
291
- continue;
292
- const matches = conceptList.filter((c) => c.name.toLowerCase().includes(concept.toLowerCase()) ||
293
- c.description.toLowerCase().includes(concept.toLowerCase()) ||
294
- c.keywords.some((k) => k.toLowerCase().includes(concept.toLowerCase())));
295
- if (matches.length > 0) {
296
- results[cat] = matches;
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
- this.cache.setLongTerm(cacheKey, results);
300
- return results;
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
- throw new Error('Unable to search Spring Boot concepts');
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
- * Enhanced documentation search with parallel processing
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
- searchPromises.push(this.searchGuides(query, Math.ceil(limit / 3)));
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 === 'reference') {
324
- searchPromises.push(this.searchReference(query, Math.ceil(limit / 3)));
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 === 'api') {
327
- searchPromises.push(this.searchAPI(query, Math.ceil(limit / 3)));
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
- const results = await Promise.all(searchPromises);
330
- const flatResults = results.flat().slice(0, limit);
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
- throw new Error('Failed to search Spring Boot documentation');
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.3',
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