@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.
- package/README.md +108 -40
- package/build/config.d.ts +9 -0
- package/build/config.d.ts.map +1 -0
- package/build/config.js +32 -0
- package/build/config.js.map +1 -0
- package/build/format.d.ts +6 -0
- package/build/format.d.ts.map +1 -0
- package/build/format.js +23 -0
- package/build/format.js.map +1 -0
- package/build/http-server.d.ts +15 -0
- package/build/http-server.d.ts.map +1 -0
- package/build/http-server.js +131 -0
- package/build/http-server.js.map +1 -0
- package/build/index.js +184 -69
- package/build/index.js.map +1 -1
- package/build/prompts.d.ts +23 -0
- package/build/prompts.d.ts.map +1 -0
- package/build/prompts.js +112 -0
- package/build/prompts.js.map +1 -0
- package/build/resources.d.ts +31 -0
- package/build/resources.d.ts.map +1 -0
- package/build/resources.js +66 -0
- package/build/resources.js.map +1 -0
- package/build/services/advanced-features.d.ts +19 -9
- package/build/services/advanced-features.d.ts.map +1 -1
- package/build/services/advanced-features.js +266 -191
- package/build/services/advanced-features.js.map +1 -1
- package/build/services/boot-wiki.d.ts +25 -0
- package/build/services/boot-wiki.d.ts.map +1 -0
- package/build/services/boot-wiki.js +103 -0
- package/build/services/boot-wiki.js.map +1 -0
- package/build/services/cache.d.ts +18 -3
- package/build/services/cache.d.ts.map +1 -1
- package/build/services/cache.js +84 -3
- package/build/services/cache.js.map +1 -1
- package/build/services/dependency-finder.d.ts +46 -0
- package/build/services/dependency-finder.d.ts.map +1 -0
- package/build/services/dependency-finder.js +181 -0
- package/build/services/dependency-finder.js.map +1 -0
- package/build/services/diagnosis.d.ts +46 -0
- package/build/services/diagnosis.d.ts.map +1 -0
- package/build/services/diagnosis.js +481 -0
- package/build/services/diagnosis.js.map +1 -0
- package/build/services/guides-list.d.ts +16 -0
- package/build/services/guides-list.d.ts.map +1 -0
- package/build/services/guides-list.js +45 -0
- package/build/services/guides-list.js.map +1 -0
- package/build/services/http.d.ts +21 -0
- package/build/services/http.d.ts.map +1 -0
- package/build/services/http.js +168 -0
- package/build/services/http.js.map +1 -0
- package/build/services/initializr.d.ts +41 -0
- package/build/services/initializr.d.ts.map +1 -0
- package/build/services/initializr.js +135 -0
- package/build/services/initializr.js.map +1 -0
- package/build/services/markdown.d.ts +56 -0
- package/build/services/markdown.d.ts.map +1 -0
- package/build/services/markdown.js +146 -0
- package/build/services/markdown.js.map +1 -0
- package/build/services/migration-sources.d.ts +13 -0
- package/build/services/migration-sources.d.ts.map +1 -0
- package/build/services/migration-sources.js +48 -0
- package/build/services/migration-sources.js.map +1 -0
- package/build/services/release-notes.d.ts +23 -0
- package/build/services/release-notes.d.ts.map +1 -0
- package/build/services/release-notes.js +94 -0
- package/build/services/release-notes.js.map +1 -0
- package/build/services/search-index.d.ts +33 -0
- package/build/services/search-index.d.ts.map +1 -0
- package/build/services/search-index.js +147 -0
- package/build/services/search-index.js.map +1 -0
- package/build/services/spring-projects-config.d.ts +98 -0
- package/build/services/spring-projects-config.d.ts.map +1 -0
- package/build/services/spring-projects-config.js +397 -0
- package/build/services/spring-projects-config.js.map +1 -0
- package/build/services/springboot-docs-optimized.d.ts +72 -21
- package/build/services/springboot-docs-optimized.d.ts.map +1 -1
- package/build/services/springboot-docs-optimized.js +354 -208
- package/build/services/springboot-docs-optimized.js.map +1 -1
- package/build/services/url.d.ts +15 -0
- package/build/services/url.d.ts.map +1 -0
- package/build/services/url.js +40 -0
- package/build/services/url.js.map +1 -0
- package/build/tools/index.d.ts +393 -2
- package/build/tools/index.d.ts.map +1 -1
- package/build/tools/index.js +188 -15
- package/build/tools/index.js.map +1 -1
- package/build/validation.d.ts +7 -0
- package/build/validation.d.ts.map +1 -0
- package/build/validation.js +50 -0
- package/build/validation.js.map +1 -0
- package/build/version.d.ts +3 -0
- package/build/version.d.ts.map +1 -0
- package/build/version.js +6 -0
- package/build/version.js.map +1 -0
- package/package.json +9 -7
- package/build/debug.d.ts +0 -3
- package/build/debug.d.ts.map +0 -1
- package/build/debug.js +0 -292
- package/build/debug.js.map +0 -1
- package/build/services/advanced-features-old.d.ts +0 -45
- package/build/services/advanced-features-old.d.ts.map +0 -1
- package/build/services/advanced-features-old.js +0 -792
- package/build/services/advanced-features-old.js.map +0 -1
- package/build/services/springboot-docs-optimized-old.d.ts +0 -55
- package/build/services/springboot-docs-optimized-old.d.ts.map +0 -1
- package/build/services/springboot-docs-optimized-old.js +0 -588
- package/build/services/springboot-docs-optimized-old.js.map +0 -1
- package/build/services/springboot-docs.d.ts +0 -65
- package/build/services/springboot-docs.d.ts.map +0 -1
- package/build/services/springboot-docs.js +0 -708
- 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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
139
|
-
const extractedContent =
|
|
140
|
-
const
|
|
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
|
-
|
|
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 =
|
|
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
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
259
|
-
|
|
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
|
|
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
|
-
|
|
373
|
+
throw error;
|
|
326
374
|
}
|
|
327
375
|
}
|
|
328
376
|
async searchGuides(query, limit) {
|
|
329
377
|
try {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 += `##
|
|
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
|
-
|
|
440
|
-
|
|
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
|