Skip to content

Commit 43aba64

Browse files
committed
sync: mcp-sap-docs cadb82c1450fb7014439d3d295ac5df897700bfe -> abap variant [skip-sync]
1 parent 5f91147 commit 43aba64

34 files changed

Lines changed: 2425 additions & 376 deletions

config/variants/abap.metadata.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -130,9 +130,9 @@
130130
"description": "SAP Business Technology Platform documentation",
131131
"libraryId": "/btp-cloud-platform",
132132
"sourcePath": "btp-cloud-platform/docs",
133-
"baseUrl": "https://github.com/SAP-docs/btp-cloud-platform/blob/main",
134-
"pathPattern": "/docs/{file}",
135-
"anchorStyle": "github"
133+
"baseUrl": "https://help.sap.com/docs/BTP/65de2977205c403bbc107264b8eccf4b",
134+
"pathPattern": "/{file}.html",
135+
"anchorStyle": "sap-help"
136136
},
137137
{
138138
"id": "sap-artificial-intelligence",
@@ -143,9 +143,9 @@
143143
"description": "SAP AI Core and SAP AI Launchpad documentation",
144144
"libraryId": "/sap-artificial-intelligence",
145145
"sourcePath": "sap-artificial-intelligence/docs",
146-
"baseUrl": "https://github.com/SAP-docs/sap-artificial-intelligence/blob/main",
147-
"pathPattern": "/docs/{file}",
148-
"anchorStyle": "github"
146+
"baseUrl": "https://help.sap.com/docs",
147+
"pathPattern": "/{file}.html",
148+
"anchorStyle": "sap-help"
149149
},
150150
{
151151
"id": "teched2025-dt260",

config/variants/sap-docs.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"/btp-cloud-platform",
3030
"/sap-artificial-intelligence",
3131
"/terraform-provider-btp",
32+
"/teched2025-dt260",
3233
"/architecture-center"
3334
],
3435
"submodulePaths": [
@@ -56,6 +57,7 @@
5657
"sources/sap-artificial-intelligence",
5758
"sources/abap-atc-cr-cv-s4hc",
5859
"sources/terraform-provider-btp",
60+
"sources/teched2025-dt260",
5961
"sources/architecture-center"
6062
],
6163
"tools": {

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
"inspect": "npx @modelcontextprotocol/inspector",
1616
"test": "npm run test:url-generation && npm run test:integration",
1717
"test:sparse-checkout": "npx vitest run test/sparse-checkout.test.ts",
18-
"test:url-generation": "npm run build:tsc && npx vitest run test/comprehensive-url-generation.test.ts test/prompts.test.ts test/software-heroes-afm.test.ts",
18+
"test:url-generation": "npm run build:tsc && npx vitest run test/comprehensive-url-generation.test.ts test/source-url-matrix.test.ts test/search-response-schema.test.ts test/prompts.test.ts test/software-heroes-afm.test.ts",
1919
"test:url-generation:debug": "npm run build:tsc && DEBUG_TESTS=true npx vitest run test/comprehensive-url-generation.test.ts",
2020
"test:mcp-urls": "npm run build:tsc && npx vitest run test/mcp-search-url-verification.test.ts",
2121
"test:integration": "npm run build && node test/tools/run-tests.js",

scripts/build-index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ const SOURCES: SourceConfig[] = [
211211
id: "/cap-fiori-showcase",
212212
name: "CAP Fiori Elements Feature Showcase",
213213
description: "SAP Fiori Elements features and annotations showcase using CAP",
214-
filePattern: "*.md",
214+
filePattern: "**/*.{md,cds}",
215215
type: "markdown" as const
216216
},
217217
{
@@ -923,4 +923,4 @@ async function main() {
923923
console.log("✅ Index built with", Object.keys(all).length, "libraries.");
924924
}
925925

926-
main();
926+
main();

src/lib/BaseServerHandler.ts

Lines changed: 52 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ import { search } from "./search.js";
4646
import { CONFIG } from "./config.js";
4747
import { loadMetadata, getDocUrlConfig } from "./metadata.js";
4848
import { generateDocumentationUrl, formatSearchResult } from "./url-generation/index.js";
49+
import { extractLibraryIdFromPath } from "./url-generation/utils.js";
50+
import { extractSourceUrlFromText, readSourceContentSync } from "./sourceContent.js";
4951
import { isToolEnabled, getVariantName } from "./variant.js";
5052
import { searchDiscoveryCenter, getDiscoveryCenterServiceDetails } from "./discoveryCenter/index.js";
5153

@@ -75,7 +77,7 @@ interface DocumentResult {
7577
/**
7678
* Create structured JSON response for search results (ChatGPT-compatible)
7779
*/
78-
function createSearchResponse(results: SearchResult[]): any {
80+
function createSearchResponse(results: SearchResult[], extra: Record<string, any> = {}): any {
7981
// Clean the results to avoid JSON serialization issues in MCP protocol
8082
const cleanedResults = results.map(result => ({
8183
// ChatGPT requires: id, title, url (other fields optional)
@@ -90,18 +92,44 @@ function createSearchResponse(results: SearchResult[]): any {
9092
metadata: result.metadata
9193
}));
9294

95+
const payload = { results: cleanedResults, ...extra };
96+
9397
// ChatGPT expects: { "results": [...] } in JSON-encoded text content
9498
return {
9599
content: [
96100
{
97101
type: "text",
98-
text: JSON.stringify({ results: cleanedResults })
102+
text: JSON.stringify(payload)
99103
}
100104
],
101-
structuredContent: { results: cleanedResults }
105+
structuredContent: payload
102106
};
103107
}
104108

109+
function createEmptySearchResponse(message: string, requestId?: string, extra: Record<string, any> = {}): any {
110+
return createSearchResponse([], {
111+
error: message,
112+
requestId: requestId || 'unknown',
113+
...extra
114+
});
115+
}
116+
117+
function isAbsoluteHttpUrl(url?: string): boolean {
118+
return !!url && /^https?:\/\//i.test(url);
119+
}
120+
121+
function chooseSearchResultUrl(docUrl: string | null, path: string | undefined, id: string): string {
122+
if (isAbsoluteHttpUrl(docUrl || undefined)) {
123+
return docUrl!;
124+
}
125+
126+
if (isAbsoluteHttpUrl(path)) {
127+
return path!;
128+
}
129+
130+
return `#${id}`;
131+
}
132+
105133
/**
106134
* Create structured JSON response for document fetch (ChatGPT-compatible)
107135
*/
@@ -1017,7 +1045,7 @@ RETURNS (JSON):
10171045
if (topResults.length === 0) {
10181046
console.log(`⚠️ [SEARCH TOOL] No results found for query: "${query}"`);
10191047
logger.logToolSuccess(name, timing.requestId, timing.startTime, 0, { fallback: false });
1020-
return createErrorResponse(
1048+
return createEmptySearchResponse(
10211049
`No results for "${query}". Try ABAP keywords ("SELECT", "LOOP", "RAP"), add "cloud" for ABAP Cloud syntax, or be more specific.`,
10221050
timing.requestId
10231051
);
@@ -1031,13 +1059,14 @@ RETURNS (JSON):
10311059
const topic = r.id.startsWith(libraryId) ? r.id.slice(libraryId.length + 1) : '';
10321060

10331061
const config = getDocUrlConfig(libraryId);
1034-
const docUrl = config ? generateDocumentationUrl(libraryId, r.relFile || '', r.text, config) : null;
1062+
const sourceContent = config ? readSourceContentSync(libraryId, r.relFile || '') : null;
1063+
const docUrl = config ? generateDocumentationUrl(libraryId, r.relFile || '', sourceContent || r.text, config) : null;
10351064

10361065
return {
10371066
// ChatGPT-required format: id, title, url
10381067
id: r.id,
10391068
title: r.text.split('\n')[0] || r.id,
1040-
url: docUrl || r.path || `#${r.id}`,
1069+
url: chooseSearchResultUrl(docUrl, r.path, r.id),
10411070
// Additional fields
10421071
library_id: libraryId,
10431072
topic: topic,
@@ -1074,7 +1103,7 @@ RETURNS (JSON):
10741103

10751104
if (!res.results.length) {
10761105
logger.logToolSuccess(name, timing.requestId, timing.startTime, 0, { fallback: true });
1077-
return createErrorResponse(
1106+
return createEmptySearchResponse(
10781107
res.error || `No fallback results for "${query}". Try ABAP keywords ("SELECT", "LOOP", "RAP"), add "cloud" for ABAP Cloud syntax, or be more specific.`,
10791108
timing.requestId
10801109
);
@@ -1097,7 +1126,7 @@ RETURNS (JSON):
10971126
return createSearchResponse(fallbackResults);
10981127
} catch (fallbackError) {
10991128
logger.logToolError(name, timing.requestId, timing.startTime, fallbackError, true);
1100-
return createErrorResponse(
1129+
return createEmptySearchResponse(
11011130
`Search temporarily unavailable. Wait 30 seconds and retry, or use more specific search terms.`,
11021131
timing.requestId
11031132
);
@@ -1135,13 +1164,15 @@ RETURNS (JSON):
11351164
}
11361165

11371166
// Transform document content to ChatGPT-compatible format
1138-
const config = getDocUrlConfig(library_id);
1139-
const docUrl = config ? generateDocumentationUrl(library_id, '', text, config) : null;
1167+
const fetchedSourceUrl = extractSourceUrlFromText(text);
1168+
const rootLibraryId = library_id.startsWith('/') ? extractLibraryIdFromPath(library_id) : library_id;
1169+
const config = getDocUrlConfig(rootLibraryId);
1170+
const docUrl = config ? generateDocumentationUrl(rootLibraryId, '', text, config) : null;
11401171
const document: DocumentResult = {
11411172
id: library_id,
11421173
title: library_id.replace(/^\//, '').replace(/\//g, ' > ') + (topic ? ` (${topic})` : ''),
11431174
text: text,
1144-
url: docUrl || `#${library_id}`,
1175+
url: fetchedSourceUrl || docUrl || `#${library_id}`,
11451176
metadata: {
11461177
source: 'abap-docs',
11471178
library: library_id,
@@ -1243,23 +1274,11 @@ RETURNS (JSON):
12431274

12441275
if (!communityResponse.results.length) {
12451276
logger.logToolSuccess(name, timing.requestId, timing.startTime, 0);
1246-
return {
1247-
content: [
1248-
{
1249-
type: "text",
1250-
text: JSON.stringify({
1251-
error: communityResponse.error || `No SAP Community posts found for "${query}". Try different keywords.`,
1252-
requestId: timing.requestId,
1253-
requestUrl,
1254-
}),
1255-
},
1256-
],
1257-
structuredContent: {
1258-
error: communityResponse.error || `No SAP Community posts found for "${query}". Try different keywords.`,
1259-
requestId: timing.requestId,
1260-
requestUrl,
1261-
},
1262-
};
1277+
return createEmptySearchResponse(
1278+
communityResponse.error || `No SAP Community posts found for "${query}". Try different keywords.`,
1279+
timing.requestId,
1280+
{ requestUrl }
1281+
);
12631282
}
12641283

12651284
const searchResults: SearchResult[] = communityResponse.results.map((r, index) => ({
@@ -1286,23 +1305,11 @@ RETURNS (JSON):
12861305
};
12871306
} catch (error) {
12881307
logger.logToolError(name, timing.requestId, timing.startTime, error);
1289-
return {
1290-
content: [
1291-
{
1292-
type: "text",
1293-
text: JSON.stringify({
1294-
error: "Error searching SAP Community. Please try again later.",
1295-
requestId: timing.requestId,
1296-
requestUrl,
1297-
}),
1298-
},
1299-
],
1300-
structuredContent: {
1301-
error: "Error searching SAP Community. Please try again later.",
1302-
requestId: timing.requestId,
1303-
requestUrl,
1304-
},
1305-
};
1308+
return createEmptySearchResponse(
1309+
"Error searching SAP Community. Please try again later.",
1310+
timing.requestId,
1311+
{ requestUrl }
1312+
);
13061313
}
13071314
}
13081315

src/lib/communityBestMatch.ts

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ type Options = {
2323
userAgent?: string; // optional UA override
2424
};
2525

26-
const LIQL_BASE = "https://community.sap.com/api/2.0/search";
26+
const COMMUNITY_BASE = "https://community.sap.com";
27+
const LIQL_BASE = `${COMMUNITY_BASE}/api/2.0/search`;
2728

2829
const stripTags = (html = "") =>
2930
html
@@ -37,13 +38,30 @@ const stripTags = (html = "") =>
3738
.trim();
3839

3940
// Extract post ID from view_href URL
40-
const extractPostId = (url: string): string | undefined => {
41+
export const extractPostId = (url: string): string | undefined => {
4142
const urlMatch = url.match(/\/(?:ba-p|td-p|qaq-p|qaa-p|m-p)\/(\d+)/);
4243
if (urlMatch) return urlMatch[1];
4344
const endMatch = url.match(/\/(\d+)(?:[#?]|$)/);
4445
return endMatch ? endMatch[1] : undefined;
4546
};
4647

48+
export function normalizeCommunityUrl(url?: string, postId?: string): string {
49+
if (url) {
50+
if (/^https?:\/\//i.test(url)) {
51+
return url;
52+
}
53+
54+
const normalizedPath = url.startsWith('/') ? url : `/${url}`;
55+
return `${COMMUNITY_BASE}${normalizedPath}`;
56+
}
57+
58+
if (postId) {
59+
return `${COMMUNITY_BASE}/t5/forums/messagepage/message-id/${encodeURIComponent(postId)}`;
60+
}
61+
62+
return COMMUNITY_BASE;
63+
}
64+
4765
/**
4866
* Build a LiQL URL for full-text search on SAP Community.
4967
* Uses MATCHES on subject and/or body (combined with OR per Khoros docs).
@@ -88,8 +106,9 @@ async function executeLiqlSearch(url: string, userAgent?: string): Promise<any[]
88106

89107
function mapItemsToHits(items: any[]): BestMatchHit[] {
90108
return items.map((item: any): BestMatchHit => {
91-
const viewHref = item.view_href || "";
92-
const postId = String(item.id || "") || extractPostId(viewHref);
109+
const rawViewHref = item.view_href || "";
110+
const postId = String(item.id || "") || extractPostId(rawViewHref);
111+
const viewHref = normalizeCommunityUrl(rawViewHref, postId);
93112
const snippet = item.search_snippet ? stripTags(item.search_snippet).slice(0, CONFIG.EXCERPT_LENGTH_COMMUNITY) : undefined;
94113
const published = item.post_time
95114
? new Date(item.post_time).toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric" })
@@ -239,7 +258,7 @@ export async function getCommunityPostsByIds(postIds: string[], userAgent?: stri
239258
// Process each post
240259
for (const post of data.data.items) {
241260
const postDate = post.post_time ? new Date(post.post_time).toLocaleDateString() : 'Unknown';
242-
const postUrl = post.view_href || `https://community.sap.com/t5/technology-blogs-by-sap/bg-p/t/${post.id}`;
261+
const postUrl = normalizeCommunityUrl(post.view_href, String(post.id || ""));
243262

244263
const fullContent = `# ${post.subject}
245264
@@ -281,4 +300,4 @@ export async function getCommunityPostByUrl(postUrl: string, userAgent?: string)
281300
return null;
282301
}
283302
return getCommunityPostById(postId, userAgent);
284-
}
303+
}

0 commit comments

Comments
 (0)