@enokdev/springdocs-mcp 1.2.7 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/README.md +108 -40
  2. package/build/config.d.ts +9 -0
  3. package/build/config.d.ts.map +1 -0
  4. package/build/config.js +32 -0
  5. package/build/config.js.map +1 -0
  6. package/build/format.d.ts +6 -0
  7. package/build/format.d.ts.map +1 -0
  8. package/build/format.js +23 -0
  9. package/build/format.js.map +1 -0
  10. package/build/http-server.d.ts +15 -0
  11. package/build/http-server.d.ts.map +1 -0
  12. package/build/http-server.js +131 -0
  13. package/build/http-server.js.map +1 -0
  14. package/build/index.js +184 -69
  15. package/build/index.js.map +1 -1
  16. package/build/prompts.d.ts +23 -0
  17. package/build/prompts.d.ts.map +1 -0
  18. package/build/prompts.js +112 -0
  19. package/build/prompts.js.map +1 -0
  20. package/build/resources.d.ts +31 -0
  21. package/build/resources.d.ts.map +1 -0
  22. package/build/resources.js +66 -0
  23. package/build/resources.js.map +1 -0
  24. package/build/services/advanced-features.d.ts +19 -9
  25. package/build/services/advanced-features.d.ts.map +1 -1
  26. package/build/services/advanced-features.js +266 -191
  27. package/build/services/advanced-features.js.map +1 -1
  28. package/build/services/boot-wiki.d.ts +25 -0
  29. package/build/services/boot-wiki.d.ts.map +1 -0
  30. package/build/services/boot-wiki.js +103 -0
  31. package/build/services/boot-wiki.js.map +1 -0
  32. package/build/services/cache.d.ts +18 -3
  33. package/build/services/cache.d.ts.map +1 -1
  34. package/build/services/cache.js +84 -3
  35. package/build/services/cache.js.map +1 -1
  36. package/build/services/dependency-finder.d.ts +46 -0
  37. package/build/services/dependency-finder.d.ts.map +1 -0
  38. package/build/services/dependency-finder.js +181 -0
  39. package/build/services/dependency-finder.js.map +1 -0
  40. package/build/services/diagnosis.d.ts +46 -0
  41. package/build/services/diagnosis.d.ts.map +1 -0
  42. package/build/services/diagnosis.js +481 -0
  43. package/build/services/diagnosis.js.map +1 -0
  44. package/build/services/guides-list.d.ts +16 -0
  45. package/build/services/guides-list.d.ts.map +1 -0
  46. package/build/services/guides-list.js +45 -0
  47. package/build/services/guides-list.js.map +1 -0
  48. package/build/services/http.d.ts +21 -0
  49. package/build/services/http.d.ts.map +1 -0
  50. package/build/services/http.js +168 -0
  51. package/build/services/http.js.map +1 -0
  52. package/build/services/initializr.d.ts +41 -0
  53. package/build/services/initializr.d.ts.map +1 -0
  54. package/build/services/initializr.js +135 -0
  55. package/build/services/initializr.js.map +1 -0
  56. package/build/services/markdown.d.ts +56 -0
  57. package/build/services/markdown.d.ts.map +1 -0
  58. package/build/services/markdown.js +146 -0
  59. package/build/services/markdown.js.map +1 -0
  60. package/build/services/migration-sources.d.ts +13 -0
  61. package/build/services/migration-sources.d.ts.map +1 -0
  62. package/build/services/migration-sources.js +48 -0
  63. package/build/services/migration-sources.js.map +1 -0
  64. package/build/services/release-notes.d.ts +23 -0
  65. package/build/services/release-notes.d.ts.map +1 -0
  66. package/build/services/release-notes.js +94 -0
  67. package/build/services/release-notes.js.map +1 -0
  68. package/build/services/search-index.d.ts +33 -0
  69. package/build/services/search-index.d.ts.map +1 -0
  70. package/build/services/search-index.js +147 -0
  71. package/build/services/search-index.js.map +1 -0
  72. package/build/services/spring-projects-config.d.ts +98 -0
  73. package/build/services/spring-projects-config.d.ts.map +1 -0
  74. package/build/services/spring-projects-config.js +397 -0
  75. package/build/services/spring-projects-config.js.map +1 -0
  76. package/build/services/springboot-docs-optimized.d.ts +72 -21
  77. package/build/services/springboot-docs-optimized.d.ts.map +1 -1
  78. package/build/services/springboot-docs-optimized.js +354 -208
  79. package/build/services/springboot-docs-optimized.js.map +1 -1
  80. package/build/services/url.d.ts +15 -0
  81. package/build/services/url.d.ts.map +1 -0
  82. package/build/services/url.js +40 -0
  83. package/build/services/url.js.map +1 -0
  84. package/build/tools/index.d.ts +393 -2
  85. package/build/tools/index.d.ts.map +1 -1
  86. package/build/tools/index.js +188 -15
  87. package/build/tools/index.js.map +1 -1
  88. package/build/validation.d.ts +7 -0
  89. package/build/validation.d.ts.map +1 -0
  90. package/build/validation.js +50 -0
  91. package/build/validation.js.map +1 -0
  92. package/build/version.d.ts +3 -0
  93. package/build/version.d.ts.map +1 -0
  94. package/build/version.js +6 -0
  95. package/build/version.js.map +1 -0
  96. package/package.json +9 -7
  97. package/build/debug.d.ts +0 -3
  98. package/build/debug.d.ts.map +0 -1
  99. package/build/debug.js +0 -292
  100. package/build/debug.js.map +0 -1
  101. package/build/services/advanced-features-old.d.ts +0 -45
  102. package/build/services/advanced-features-old.d.ts.map +0 -1
  103. package/build/services/advanced-features-old.js +0 -792
  104. package/build/services/advanced-features-old.js.map +0 -1
  105. package/build/services/springboot-docs-optimized-old.d.ts +0 -55
  106. package/build/services/springboot-docs-optimized-old.d.ts.map +0 -1
  107. package/build/services/springboot-docs-optimized-old.js +0 -588
  108. package/build/services/springboot-docs-optimized-old.js.map +0 -1
  109. package/build/services/springboot-docs.d.ts +0 -65
  110. package/build/services/springboot-docs.d.ts.map +0 -1
  111. package/build/services/springboot-docs.js +0 -708
  112. package/build/services/springboot-docs.js.map +0 -1
@@ -1,25 +1,128 @@
1
- import fetch from 'node-fetch';
2
1
  import * as cheerio from 'cheerio';
3
- import TurndownService from 'turndown';
4
2
  import { CacheService } from './cache.js';
3
+ import { fetchWithRetry, githubRateLimitMessage } from './http.js';
4
+ import { fetchSpringGuidesList } from './guides-list.js';
5
+ import { turndownService, extractContent } from './markdown.js';
6
+ import { absoluteSpringUrl } from './url.js';
7
+ import { springProjectsConfig } from './spring-projects-config.js';
8
+ import { renderDiagnosis } from './diagnosis.js';
9
+ import { filterReleaseBody, normalizeReleaseVersion, isMinorVersion, latestStableOfMinor } from './release-notes.js';
5
10
  /**
6
11
  * Advanced features service for Spring documentation - uses ONLY real Spring documentation APIs
7
12
  * No mock data - everything is fetched from actual Spring documentation sources
8
13
  */
9
14
  export class AdvancedFeaturesService {
10
15
  cache;
11
- turndownService;
12
16
  baseUrl = 'https://docs.spring.io';
13
17
  springProjectsUrl = 'https://spring.io/projects';
14
- springGuideUrl = 'https://spring.io/guides';
15
- REQUEST_TIMEOUT = 10000;
16
- MAX_RETRIES = 3;
17
- constructor() {
18
- this.cache = new CacheService();
19
- this.turndownService = new TurndownService({
20
- headingStyle: 'atx',
21
- codeBlockStyle: 'fenced',
22
- });
18
+ projectsConfig;
19
+ constructor(cache = new CacheService(), projectsConfig = springProjectsConfig) {
20
+ this.cache = cache;
21
+ this.projectsConfig = projectsConfig;
22
+ }
23
+ /**
24
+ * Get the GitHub release notes of a Spring project (a given version or the latest one),
25
+ * optionally filtered on breaking changes, new features or deprecations.
26
+ */
27
+ async getReleaseNotes(project, version, focus = 'all') {
28
+ const config = this.projectsConfig.getProject(project);
29
+ if (!config.githubRepo) {
30
+ throw new Error(`Release notes are not available for ${config.displayName}`);
31
+ }
32
+ // Validate before any network call
33
+ const normalized = normalizeReleaseVersion(version);
34
+ const repo = config.githubRepo;
35
+ const tagPrefix = config.githubTagPrefix ?? '';
36
+ const minor = normalized !== undefined && isMinorVersion(normalized);
37
+ const tag = normalized === undefined || minor ? undefined : `${tagPrefix}${normalized}`;
38
+ const cacheKey = `release:${project}:${normalized ?? 'latest'}`;
39
+ let release = this.cache.get(cacheKey);
40
+ if (!release) {
41
+ let data;
42
+ if (minor) {
43
+ data = await this.findLatestOfMinor(repo, normalized, tagPrefix);
44
+ if (!data) {
45
+ throw new Error(`No release found: ${config.displayName} ${normalized}.x (no stable release of that minor). See https://github.com/${repo}/releases`);
46
+ }
47
+ }
48
+ else {
49
+ const url = tag === undefined
50
+ ? `https://api.github.com/repos/${repo}/releases/latest`
51
+ : `https://api.github.com/repos/${repo}/releases/tags/${encodeURIComponent(tag)}`;
52
+ const response = await this.fetchWithRetry(url);
53
+ if (response.status === 404) {
54
+ throw new Error(tag === undefined
55
+ ? `No release found for ${config.displayName}`
56
+ : `Release not found: ${config.displayName} ${normalized} (tag "${tag}"). See https://github.com/${repo}/releases`);
57
+ }
58
+ this.assertNotRateLimited(response);
59
+ if (!response.ok) {
60
+ throw new Error(`Failed to fetch release data: ${response.status}`);
61
+ }
62
+ data = await response.json();
63
+ }
64
+ release = {
65
+ tag: data.tag_name,
66
+ name: data.name || null,
67
+ publishedAt: data.published_at || null,
68
+ url: data.html_url,
69
+ prerelease: Boolean(data.prerelease),
70
+ body: data.body ?? null,
71
+ };
72
+ // Only successful responses are cached; "latest" and "X.Y" move, so they keep the short TTL
73
+ if (normalized === undefined || minor) {
74
+ this.cache.set(cacheKey, release);
75
+ }
76
+ else {
77
+ this.cache.setLongTerm(cacheKey, release);
78
+ }
79
+ }
80
+ let output = `# ${config.displayName} ${release.name || release.tag}\n\n`;
81
+ output += `**Released:** ${release.publishedAt ? release.publishedAt.slice(0, 10) : 'unknown'}\n`;
82
+ if (release.prerelease)
83
+ output += `**Pre-release:** yes\n`;
84
+ output += `**Release notes:** ${release.url}\n`;
85
+ output += `**Focus:** ${focus}\n\n`;
86
+ const filtered = filterReleaseBody(release.body, focus);
87
+ if (filtered.trim()) {
88
+ const { content, truncated } = extractContent(filtered, 'full');
89
+ output += content;
90
+ if (truncated) {
91
+ output += `\n\n---\n*Release notes truncated at 50,000 characters. Full notes: ${release.url}*`;
92
+ }
93
+ }
94
+ else {
95
+ output += `No ${focus} entries found in these release notes. See the full notes: ${release.url}`;
96
+ }
97
+ return output;
98
+ }
99
+ /** Throws a clear error when GitHub refused the request because of its rate limit. */
100
+ assertNotRateLimited(response) {
101
+ if (response.rateLimit || response.status === 403 || response.status === 429) {
102
+ throw new Error(githubRateLimitMessage(response.rateLimit));
103
+ }
104
+ }
105
+ /** Latest stable release of an "X.Y" minor, paginating the GitHub releases list. */
106
+ async findLatestOfMinor(repo, minor, tagPrefix) {
107
+ const MAX_PAGES = 5;
108
+ let best;
109
+ let seenOlderPage = false;
110
+ for (let page = 1; page <= MAX_PAGES && !seenOlderPage; page++) {
111
+ const response = await this.fetchWithRetry(`https://api.github.com/repos/${repo}/releases?per_page=100&page=${page}`);
112
+ this.assertNotRateLimited(response);
113
+ if (!response.ok) {
114
+ throw new Error(`Failed to fetch release data: ${response.status}`);
115
+ }
116
+ const releases = await response.json();
117
+ if (!Array.isArray(releases) || releases.length === 0)
118
+ break;
119
+ const candidate = latestStableOfMinor(releases, minor, tagPrefix);
120
+ // Once found, a page without any release of that minor means the rest is older
121
+ if (best && !candidate)
122
+ seenOlderPage = true;
123
+ best = latestStableOfMinor(candidate ? [...(best ? [best] : []), candidate] : [], minor, tagPrefix) ?? best;
124
+ }
125
+ return best;
23
126
  }
24
127
  /**
25
128
  * Search across the entire Spring ecosystem using real APIs
@@ -36,81 +139,45 @@ export class AdvancedFeaturesService {
36
139
  categories: {}
37
140
  };
38
141
  try {
39
- if (scope === 'all' || scope === 'projects') {
40
- results.categories.projects = await this.searchProjects(query, limit);
41
- }
42
- if (scope === 'all' || scope === 'guides') {
43
- results.categories.guides = await this.searchGuides(query, limit);
44
- }
45
- if (scope === 'all' || scope === 'docs') {
46
- results.categories.documentation = await this.searchDocumentation(query, limit);
47
- }
48
- if (scope === 'all' || scope === 'api') {
49
- results.categories.api = await this.searchAPI(query, limit);
142
+ const searches = [
143
+ ['projects', 'projects', () => this.searchProjects(query, limit)],
144
+ ['guides', 'guides', () => this.searchGuides(query, limit)],
145
+ ['docs', 'documentation', () => this.searchDocumentation(query, limit)],
146
+ ['api', 'api', () => this.searchAPI(query, limit)],
147
+ ['ai', 'ai', () => this.searchSpringAI(query, limit)],
148
+ ];
149
+ const selected = searches.filter(([name]) => scope === 'all' || scope === name);
150
+ // Run the searches concurrently; a failed one is reported, never cached
151
+ const settled = await Promise.allSettled(selected.map(([, , run]) => run()));
152
+ const unavailable = [];
153
+ selected.forEach(([, category], index) => {
154
+ const outcome = settled[index];
155
+ if (outcome.status === 'fulfilled') {
156
+ results.categories[category] = outcome.value;
157
+ }
158
+ else {
159
+ results.categories[category] = [];
160
+ unavailable.push(category);
161
+ console.error(`Ecosystem source "${category}" failed:`, outcome.reason instanceof Error ? outcome.reason.message : outcome.reason);
162
+ }
163
+ });
164
+ if (selected.length > 0 && unavailable.length === selected.length) {
165
+ throw new Error(`Unable to search the Spring ecosystem: all sources failed (${unavailable.join(', ')})`);
50
166
  }
167
+ results.unavailable = unavailable;
51
168
  results.totalResults = Object.values(results.categories)
52
169
  .reduce((total, category) => total + (category?.length || 0), 0);
53
170
  const formattedResult = this.formatEcosystemResults(results);
54
- this.cache.set(cacheKey, formattedResult);
171
+ if (unavailable.length === 0) {
172
+ this.cache.set(cacheKey, formattedResult);
173
+ }
55
174
  return formattedResult;
56
175
  }
57
176
  catch (error) {
58
177
  console.error('Error searching ecosystem:', error);
59
- return `# Spring Ecosystem Search Results\n\nError searching for "${query}": ${error instanceof Error ? error.message : 'Unknown error'}`;
178
+ throw error;
60
179
  }
61
180
  }
62
- /**
63
- * Extract content intelligently - same as in SpringBootDocsServiceOptimized
64
- */
65
- extractIntelligentContent(markdown, detailLevel = 'medium') {
66
- const limits = {
67
- 'summary': 1500,
68
- 'medium': 4000,
69
- 'full': 8000
70
- };
71
- const maxLength = limits[detailLevel] || limits['medium'];
72
- if (markdown.length <= maxLength)
73
- return markdown;
74
- const lines = markdown.split('\n');
75
- let result = '';
76
- let inCodeBlock = false;
77
- let codeBlockContent = '';
78
- let currentLength = 0;
79
- for (const line of lines) {
80
- if (line.trim().startsWith('```')) {
81
- inCodeBlock = !inCodeBlock;
82
- if (!inCodeBlock && codeBlockContent) {
83
- const blockToAdd = codeBlockContent + line + '\n';
84
- if (currentLength + blockToAdd.length <= maxLength * 0.8) {
85
- result += blockToAdd;
86
- currentLength += blockToAdd.length;
87
- }
88
- codeBlockContent = '';
89
- }
90
- else {
91
- codeBlockContent = line + '\n';
92
- }
93
- continue;
94
- }
95
- if (inCodeBlock) {
96
- codeBlockContent += line + '\n';
97
- continue;
98
- }
99
- if (line.startsWith('#') || line.startsWith('-') || line.startsWith('*') || line.trim().startsWith('>')) {
100
- if (currentLength + line.length + 1 <= maxLength) {
101
- result += line + '\n';
102
- currentLength += line.length + 1;
103
- }
104
- }
105
- else if (line.trim() && currentLength + line.length + 1 <= maxLength) {
106
- result += line + '\n';
107
- currentLength += line.length + 1;
108
- }
109
- if (currentLength >= maxLength)
110
- break;
111
- }
112
- return result.trim();
113
- }
114
181
  /**
115
182
  * Get tutorials by fetching from actual Spring Boot guides
116
183
  */
@@ -135,52 +202,71 @@ export class AdvancedFeaturesService {
135
202
  if (content.length === 0) {
136
203
  throw new Error('No content found in guide');
137
204
  }
138
- const markdown = this.turndownService.turndown(content.html() || '');
139
- const extractedContent = this.extractIntelligentContent(markdown, detailLevel);
140
- const needsTruncation = markdown.length > extractedContent.length;
141
- const result = `# ${guide.title}\n\n**Level:** ${level}\n**Detail Level:** ${detailLevel}\n**Source:** ${guide.url}\n\n${extractedContent}${needsTruncation ? '\n\n---\n*Content truncated. Use detail_level="full" for complete tutorial or visit the link above.*' : ''}`;
205
+ const markdown = turndownService.turndown(content.html() || '');
206
+ const { content: extractedContent, truncated } = extractContent(markdown, detailLevel);
207
+ const result = `# ${guide.title}\n\n**Level:** ${level}\n**Detail Level:** ${detailLevel}\n**Source:** ${guide.url}\n\n${extractedContent}${truncated ? (detailLevel === 'full' ? '\n\n---\n*Content truncated at 50,000 characters even in full mode. Visit the link above for the complete tutorial.*' : '\n\n---\n*Content truncated. Use detail_level="full" for complete tutorial or visit the link above.*') : ''}`;
142
208
  this.cache.set(cacheKey, result);
143
209
  return result;
144
210
  }
145
211
  catch (error) {
146
212
  console.error('Error fetching tutorial:', error);
147
- return `# Tutorial Error\n\nUnable to fetch tutorial for "${topic}": ${error instanceof Error ? error.message : 'Unknown error'}`;
213
+ throw error;
148
214
  }
149
215
  }
150
216
  /**
151
217
  * Compare Spring Boot versions using real release notes
152
218
  */
153
219
  async compareVersions(version1, version2, focus = 'all') {
154
- const cacheKey = `versions:${version1}:${version2}:${focus}`;
220
+ const cacheKey = 'versions:' + JSON.stringify([version1, version2, focus]);
155
221
  const cached = this.cache.get(cacheKey);
156
222
  if (cached)
157
223
  return cached;
158
224
  try {
159
- // Fetch release notes from GitHub
160
- const releaseNotesUrl = `https://api.github.com/repos/spring-projects/spring-boot/releases`;
161
- const response = await this.fetchWithRetry(releaseNotesUrl);
162
- if (!response.ok) {
163
- throw new Error(`Failed to fetch release data: ${response.status}`);
225
+ // Fetch release notes from GitHub, paginating until both exact tags are found
226
+ const MAX_PAGES = 5;
227
+ const matches = (r, version) => r.tag_name === version || r.tag_name === `v${version}`;
228
+ let release1;
229
+ let release2;
230
+ for (let page = 1; page <= MAX_PAGES && !(release1 && release2); page++) {
231
+ const releaseNotesUrl = `https://api.github.com/repos/spring-projects/spring-boot/releases?per_page=100&page=${page}`;
232
+ const response = await this.fetchWithRetry(releaseNotesUrl);
233
+ this.assertNotRateLimited(response);
234
+ if (!response.ok) {
235
+ throw new Error(`Failed to fetch release data: ${response.status}`);
236
+ }
237
+ const releases = await response.json();
238
+ if (!Array.isArray(releases) || releases.length === 0)
239
+ break;
240
+ release1 = release1 ?? releases.find((r) => matches(r, version1));
241
+ release2 = release2 ?? releases.find((r) => matches(r, version2));
164
242
  }
165
- const releases = await response.json();
166
- const release1 = releases.find((r) => r.tag_name.includes(version1));
167
- const release2 = releases.find((r) => r.tag_name.includes(version2));
168
243
  if (!release1 || !release2) {
169
- return `# Version Comparison: ${version1} vs ${version2}\n\nUnable to find release information for one or both versions.\n\nAvailable versions can be found at: https://github.com/spring-projects/spring-boot/releases`;
244
+ return `# Version Comparison: ${version1} vs ${version2}\n\nUnable to find release information for one or both versions. Versions must match a release tag exactly, in the X.Y.Z format (e.g. 3.5.0, not 3.5).\n\nAvailable versions can be found at: https://github.com/spring-projects/spring-boot/releases`;
170
245
  }
246
+ const excerpt = (release) => {
247
+ const body = filterReleaseBody(release.body, focus);
248
+ if (!body.trim()) {
249
+ return focus === 'all'
250
+ ? ''
251
+ : `No ${focus} entries found in these release notes. See the full notes: ${release.html_url}`;
252
+ }
253
+ return `${body.substring(0, 1000)}...`;
254
+ };
171
255
  const result = `# Spring Boot Version Comparison: ${version1} vs ${version2}
172
256
 
257
+ **Focus:** ${focus}
258
+
173
259
  ## Version ${version1}
174
260
  **Released:** ${new Date(release1.published_at).toLocaleDateString()}
175
261
  **Release Notes:** ${release1.html_url}
176
262
 
177
- ${release1.body.substring(0, 1000)}...
263
+ ${excerpt(release1)}
178
264
 
179
265
  ## Version ${version2}
180
266
  **Released:** ${new Date(release2.published_at).toLocaleDateString()}
181
267
  **Release Notes:** ${release2.html_url}
182
268
 
183
- ${release2.body.substring(0, 1000)}...
269
+ ${excerpt(release2)}
184
270
 
185
271
  ## Migration Recommendations
186
272
  1. Review the full release notes at the URLs above
@@ -194,7 +280,7 @@ For detailed migration guides, visit: https://github.com/spring-projects/spring-
194
280
  }
195
281
  catch (error) {
196
282
  console.error('Error comparing versions:', error);
197
- return `# Version Comparison Error\n\nUnable to compare versions ${version1} and ${version2}: ${error instanceof Error ? error.message : 'Unknown error'}`;
283
+ throw error;
198
284
  }
199
285
  }
200
286
  /**
@@ -234,7 +320,7 @@ For detailed migration guides, visit: https://github.com/spring-projects/spring-
234
320
  throw new Error('No content found in documentation');
235
321
  }
236
322
  // Convert to markdown and format
237
- const markdown = this.turndownService.turndown(content.html() || '');
323
+ const markdown = turndownService.turndown(content.html() || '');
238
324
  const result = `# Spring Boot ${category.charAt(0).toUpperCase() + category.slice(1)} Best Practices
239
325
 
240
326
  **Experience Level:** ${experienceLevel}
@@ -248,52 +334,15 @@ For complete documentation, visit: ${docUrl}`;
248
334
  }
249
335
  catch (error) {
250
336
  console.error('Error fetching best practices:', error);
251
- return `# Best Practices Error\n\nUnable to fetch best practices for "${category}": ${error instanceof Error ? error.message : 'Unknown error'}`;
337
+ throw error;
252
338
  }
253
339
  }
254
340
  /**
255
341
  * Diagnose issues using Spring Boot documentation
256
342
  */
257
343
  async diagnoseIssues(errorMessage, component, stackTrace) {
258
- const cacheKey = `diagnosis:${errorMessage.substring(0, 50)}`;
259
- const cached = this.cache.get(cacheKey);
260
- if (cached)
261
- return cached;
262
- try {
263
- // Search for the error in Spring Boot documentation
264
- const searchQuery = errorMessage.split(' ').slice(0, 3).join(' ');
265
- const docs = await this.searchDocumentation(searchQuery, 3);
266
- let result = `# Spring Boot Issue Diagnosis\n\n**Error:** ${errorMessage}\n`;
267
- if (component) {
268
- result += `**Component:** ${component}\n`;
269
- }
270
- result += `\n## Relevant Documentation\n\n`;
271
- if (docs.length > 0) {
272
- docs.forEach((doc, index) => {
273
- result += `${index + 1}. **${doc.title}**\n ${doc.url}\n\n`;
274
- });
275
- }
276
- else {
277
- result += `No specific documentation found for this error.\n\n`;
278
- }
279
- result += `## General Troubleshooting Steps\n\n`;
280
- result += `1. Check the Spring Boot documentation: https://docs.spring.io/spring-boot/docs/current/reference/html/\n`;
281
- result += `2. Search Spring Boot issues: https://github.com/spring-projects/spring-boot/issues\n`;
282
- result += `3. Enable debug logging: \`logging.level.org.springframework=DEBUG\`\n`;
283
- result += `4. Check actuator health endpoint: \`/actuator/health\`\n\n`;
284
- if (stackTrace) {
285
- result += `## Stack Trace Analysis\n\nFor detailed stack trace analysis, consider:\n`;
286
- result += `- Looking for the root cause in the stack trace\n`;
287
- result += `- Checking for configuration issues\n`;
288
- result += `- Verifying dependency versions\n\n`;
289
- }
290
- this.cache.set(cacheKey, result);
291
- return result;
292
- }
293
- catch (error) {
294
- console.error('Error diagnosing issue:', error);
295
- return `# Diagnosis Error\n\nUnable to diagnose issue: ${error instanceof Error ? error.message : 'Unknown error'}`;
296
- }
344
+ // Local analysis: no network call, bounded and deterministic
345
+ return renderDiagnosis({ errorMessage, component, stackTrace });
297
346
  }
298
347
  // Real implementation methods (no mock data)
299
348
  async searchProjects(query, limit) {
@@ -309,9 +358,8 @@ For complete documentation, visit: ${docUrl}`;
309
358
  const $project = $(element);
310
359
  const name = $project.find('h2, h3, .title, .project-title, .card-title').first().text().trim();
311
360
  const description = $project.find('p, .description, .summary, .card-text').first().text().trim();
312
- const link = $project.find('a').first().attr('href');
313
- if (name && description) {
314
- const url = link?.startsWith('http') ? link : `https://spring.io${link}`;
361
+ const url = absoluteSpringUrl($project.find('a').first().attr('href'));
362
+ if (name && description && url) {
315
363
  if (name.toLowerCase().includes(query.toLowerCase()) ||
316
364
  description.toLowerCase().includes(query.toLowerCase())) {
317
365
  projects.push({ name, description, url, type: 'project' });
@@ -322,37 +370,23 @@ For complete documentation, visit: ${docUrl}`;
322
370
  }
323
371
  catch (error) {
324
372
  console.error('Error searching projects:', error);
325
- return [];
373
+ throw error;
326
374
  }
327
375
  }
328
376
  async searchGuides(query, limit) {
329
377
  try {
330
- const response = await this.fetchWithRetry(this.springGuideUrl);
331
- if (!response.ok) {
332
- throw new Error('Failed to fetch Spring guides');
333
- }
334
- const html = await response.text();
335
- const $ = cheerio.load(html);
336
- const guides = [];
337
- $('.guide-item, .card, .guide-card, .list-item').each((index, element) => {
338
- const $guide = $(element);
339
- const title = $guide.find('h2, h3, .title, .guide-title, .card-title, a').first().text().trim();
340
- const description = $guide.find('p, .description, .summary, .card-text').first().text().trim();
341
- const link = $guide.find('a').first().attr('href');
342
- const type = $guide.find('.badge, .label, .type').first().text().trim() || 'Guide';
343
- if (title) {
344
- const url = link?.startsWith('http') ? link : `https://spring.io${link}`;
345
- if (title.toLowerCase().includes(query.toLowerCase()) ||
346
- description.toLowerCase().includes(query.toLowerCase())) {
347
- guides.push({ title, description: description || 'Spring Boot guide', url, type });
348
- }
349
- }
350
- });
351
- return guides.slice(0, limit);
378
+ // The /guides HTML is rendered client-side (no links): use the shared page-data list
379
+ const guides = await fetchSpringGuidesList();
380
+ const needle = query.toLowerCase();
381
+ return guides
382
+ .filter(guide => guide.title.toLowerCase().includes(needle) ||
383
+ guide.description.toLowerCase().includes(needle))
384
+ .map(guide => ({ title: guide.title, description: guide.description, url: guide.url, type: 'Guide' }))
385
+ .slice(0, limit);
352
386
  }
353
387
  catch (error) {
354
388
  console.error('Error searching guides:', error);
355
- return [];
389
+ throw error;
356
390
  }
357
391
  }
358
392
  async searchDocumentation(query, limit) {
@@ -378,7 +412,58 @@ For complete documentation, visit: ${docUrl}`;
378
412
  }
379
413
  catch (error) {
380
414
  console.error('Error searching documentation:', error);
381
- return [];
415
+ throw error;
416
+ }
417
+ }
418
+ /**
419
+ * Search Spring AI documentation and resources
420
+ * Covers ChatClient, RAG, embeddings, vector stores, and LLM integrations
421
+ */
422
+ async searchSpringAI(query, limit) {
423
+ try {
424
+ const aiDocsUrl = 'https://docs.spring.io/spring-ai/reference/';
425
+ const response = await this.fetchWithRetry(aiDocsUrl);
426
+ if (!response.ok) {
427
+ throw new Error('Failed to fetch Spring AI documentation');
428
+ }
429
+ const html = await response.text();
430
+ const $ = cheerio.load(html);
431
+ const aiDocs = [];
432
+ // Search in navigation, table of contents, and main links
433
+ $('nav a, .toc a, .nav-link, .sidebar a, .chapter a').each((index, element) => {
434
+ const $link = $(element);
435
+ const title = $link.text().trim();
436
+ const href = $link.attr('href');
437
+ if (title && href && title.toLowerCase().includes(query.toLowerCase())) {
438
+ const url = href.startsWith('http') ? href : `${aiDocsUrl}${href}`;
439
+ aiDocs.push({
440
+ title: `Spring AI: ${title}`,
441
+ url,
442
+ type: 'Spring AI Documentation'
443
+ });
444
+ }
445
+ });
446
+ // If no results from navigation, search in content headings
447
+ if (aiDocs.length === 0) {
448
+ $('h1, h2, h3, h4').each((index, element) => {
449
+ const $heading = $(element);
450
+ const title = $heading.text().trim();
451
+ const id = $heading.attr('id');
452
+ if (title && title.toLowerCase().includes(query.toLowerCase())) {
453
+ const url = id ? `${aiDocsUrl}#${id}` : aiDocsUrl;
454
+ aiDocs.push({
455
+ title: `Spring AI: ${title}`,
456
+ url,
457
+ type: 'Spring AI Documentation'
458
+ });
459
+ }
460
+ });
461
+ }
462
+ return aiDocs.slice(0, limit);
463
+ }
464
+ catch (error) {
465
+ console.error('Error searching Spring AI documentation:', error);
466
+ throw error;
382
467
  }
383
468
  }
384
469
  async searchAPI(query, limit) {
@@ -386,7 +471,8 @@ For complete documentation, visit: ${docUrl}`;
386
471
  { title: 'Spring Boot API Documentation', url: 'https://docs.spring.io/spring-boot/docs/current/api/', keywords: ['boot', 'autoconfiguration', 'starters'] },
387
472
  { title: 'Spring Framework API', url: 'https://docs.spring.io/spring-framework/docs/current/javadoc-api/', keywords: ['core', 'context', 'beans', 'web'] },
388
473
  { title: 'Spring Security API', url: 'https://docs.spring.io/spring-security/site/docs/current/api/', keywords: ['security', 'authentication', 'config'] },
389
- { title: 'Spring Data JPA API', url: 'https://docs.spring.io/spring-data/jpa/docs/current/api/', keywords: ['jpa', 'repository', 'query'] }
474
+ { title: 'Spring Data JPA API', url: 'https://docs.spring.io/spring-data/jpa/docs/current/api/', keywords: ['jpa', 'repository', 'query'] },
475
+ { title: 'Spring AI API', url: 'https://docs.spring.io/spring-ai/reference/api/', keywords: ['ai', 'llm', 'rag', 'embeddings', 'chatclient', 'vector', 'openai', 'anthropic'] }
390
476
  ];
391
477
  const queryLower = query.toLowerCase();
392
478
  return apis
@@ -425,42 +511,31 @@ For complete documentation, visit: ${docUrl}`;
425
511
  });
426
512
  }
427
513
  if (results.categories.api?.length > 0) {
428
- output += `## Api\n\n`;
514
+ output += `## API\n\n`;
429
515
  results.categories.api.forEach((api, index) => {
430
516
  output += `${index + 1}. **${api.title}**\n`;
431
517
  output += ` URL: ${api.url}\n\n`;
432
518
  });
433
519
  }
520
+ if (results.categories.ai?.length > 0) {
521
+ output += `## Spring AI\n\n`;
522
+ results.categories.ai.forEach((aiDoc, index) => {
523
+ output += `${index + 1}. **${aiDoc.title}**\n`;
524
+ if (aiDoc.description)
525
+ output += ` ${aiDoc.description}\n`;
526
+ output += ` URL: ${aiDoc.url}\n\n`;
527
+ });
528
+ }
434
529
  if (results.totalResults === 0) {
435
530
  output += `No results found for "${results.query}" in scope "${results.scope}".`;
436
531
  }
532
+ if (results.unavailable?.length > 0) {
533
+ output += `\n\n⚠️ Some sources were unavailable: ${results.unavailable.join(', ')}. Results may be incomplete.`;
534
+ }
437
535
  return output;
438
536
  }
439
- async fetchWithRetry(url, timeout = this.REQUEST_TIMEOUT, retries = this.MAX_RETRIES) {
440
- for (let attempt = 1; attempt <= retries; attempt++) {
441
- const controller = new AbortController();
442
- const timeoutId = setTimeout(() => controller.abort(), timeout);
443
- try {
444
- const response = await fetch(url, {
445
- signal: controller.signal,
446
- headers: {
447
- 'User-Agent': 'Spring-Docs-MCP/1.2.4',
448
- 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
449
- 'Accept-Language': 'en-US,en;q=0.9'
450
- }
451
- });
452
- clearTimeout(timeoutId);
453
- return response;
454
- }
455
- catch (error) {
456
- clearTimeout(timeoutId);
457
- if (attempt === retries)
458
- throw error;
459
- console.error(`Retry ${attempt}/${retries} for ${url}:`, error instanceof Error ? error.message : 'Unknown error');
460
- await new Promise(resolve => setTimeout(resolve, 1000 * attempt));
461
- }
462
- }
463
- throw new Error('All retry attempts failed');
537
+ fetchWithRetry(url, timeout, retries) {
538
+ return fetchWithRetry(url, timeout, retries);
464
539
  }
465
540
  }
466
541
  //# sourceMappingURL=advanced-features.js.map