This is page 3 of 4. Use http://codebase.md/marianfoo/mcp-sap-docs?page={x} to view the full context.
# Directory Structure
```
├── .cursor
│ └── rules
│ ├── 00-overview.mdc
│ ├── 10-search-stack.mdc
│ ├── 20-tools-and-apis.mdc
│ ├── 30-tests-and-output.mdc
│ ├── 40-deploy.mdc
│ ├── 50-metadata-config.mdc
│ ├── 60-adding-github-sources.mdc
│ ├── 70-tool-usage-guide.mdc
│ └── 80-abap-integration.mdc
├── .cursorignore
├── .gitattributes
├── .github
│ ├── ISSUE_TEMPLATE
│ │ ├── config.yml
│ │ ├── missing-documentation.yml
│ │ └── new-documentation-source.yml
│ └── workflows
│ ├── deploy-mcp-sap-docs.yml
│ ├── test-pr.yml
│ └── update-submodules.yml
├── .gitignore
├── .gitmodules
├── .npmignore
├── .vscode
│ ├── extensions.json
│ └── settings.json
├── docs
│ ├── ABAP-INTEGRATION-SUMMARY.md
│ ├── ABAP-MULTI-VERSION-INTEGRATION.md
│ ├── ABAP-STANDARD-INTEGRATION.md
│ ├── ABAP-USAGE-GUIDE.md
│ ├── ARCHITECTURE.md
│ ├── COMMUNITY-SEARCH-IMPLEMENTATION.md
│ ├── CONTENT-SIZE-LIMITS.md
│ ├── CURSOR-SETUP.md
│ ├── DEV.md
│ ├── FTS5-IMPLEMENTATION-COMPLETE.md
│ ├── LLM-FRIENDLY-IMPROVEMENTS.md
│ ├── METADATA-CONSOLIDATION.md
│ ├── TEST-SEARCH.md
│ └── TESTS.md
├── ecosystem.config.cjs
├── index.html
├── LICENSE
├── package-lock.json
├── package.json
├── README.md
├── REMOTE_SETUP.md
├── scripts
│ ├── build-fts.ts
│ ├── build-index.ts
│ ├── check-version.js
│ └── summarize-src.js
├── server.json
├── setup.sh
├── src
│ ├── global.d.ts
│ ├── http-server.ts
│ ├── lib
│ │ ├── BaseServerHandler.ts
│ │ ├── communityBestMatch.ts
│ │ ├── config.ts
│ │ ├── localDocs.ts
│ │ ├── logger.ts
│ │ ├── metadata.ts
│ │ ├── sapHelp.ts
│ │ ├── search.ts
│ │ ├── searchDb.ts
│ │ ├── truncate.ts
│ │ ├── types.ts
│ │ └── url-generation
│ │ ├── abap.ts
│ │ ├── BaseUrlGenerator.ts
│ │ ├── cap.ts
│ │ ├── cloud-sdk.ts
│ │ ├── dsag.ts
│ │ ├── GenericUrlGenerator.ts
│ │ ├── index.ts
│ │ ├── README.md
│ │ ├── sapui5.ts
│ │ ├── utils.ts
│ │ └── wdi5.ts
│ ├── metadata.json
│ ├── server.ts
│ └── streamable-http-server.ts
├── test
│ ├── _utils
│ │ ├── httpClient.js
│ │ └── parseResults.js
│ ├── community-search.ts
│ ├── comprehensive-url-generation.test.ts
│ ├── performance
│ │ └── README.md
│ ├── prompts.test.ts
│ ├── quick-url-test.ts
│ ├── README.md
│ ├── tools
│ │ ├── run-tests.js
│ │ ├── sap_docs_search
│ │ │ ├── search-cap-docs.js
│ │ │ ├── search-cloud-sdk-ai.js
│ │ │ ├── search-cloud-sdk-js.js
│ │ │ └── search-sapui5-docs.js
│ │ ├── search-url-verification.js
│ │ ├── search.generic.spec.js
│ │ └── search.smoke.js
│ ├── url-status.ts
│ └── validate-urls.ts
├── test-community-search.js
├── test-search-interactive.ts
├── test-search.http
├── test-search.ts
├── tsconfig.json
└── vitest.config.ts
```
# Files
--------------------------------------------------------------------------------
/scripts/summarize-src.js:
--------------------------------------------------------------------------------
```javascript
#!/usr/bin/env node
import fs from 'fs/promises';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Configuration
const SRC_DIR = path.join(__dirname, '..', 'src');
const TEST_DIR = path.join(__dirname, '..', 'test');
const SRC_OUTPUT_FILE = path.join(__dirname, '..', 'src_context.txt');
const TEST_OUTPUT_FILE = path.join(__dirname, '..', 'test_context.txt');
// Content inclusion settings
const INCLUDE_CONTENT = true;
const MAX_CONTENT_SIZE = 50000; // Max characters per file to include
const CONTENT_PREVIEW_SIZE = 500; // Characters for preview when file is too large
// File type patterns
const FILE_PATTERNS = {
typescript: /\.(ts|tsx)$/,
javascript: /\.(js|jsx)$/,
json: /\.json$/,
markdown: /\.(md|mdx)$/,
yaml: /\.(yml|yaml)$/,
xml: /\.xml$/,
html: /\.html$/,
css: /\.css$/,
scss: /\.scss$/,
sql: /\.sql$/,
config: /\.(config|conf)$/
};
// Function to get file stats
async function getFileStats(filePath) {
try {
const stats = await fs.stat(filePath);
return {
size: stats.size,
created: stats.birthtime,
modified: stats.mtime,
isDirectory: stats.isDirectory()
};
} catch (error) {
return null;
}
}
// Function to get file type
function getFileType(filename) {
for (const [type, pattern] of Object.entries(FILE_PATTERNS)) {
if (pattern.test(filename)) {
return type;
}
}
return 'other';
}
// Function to count lines in a file
async function countLines(filePath) {
try {
const content = await fs.readFile(filePath, 'utf-8');
return content.split('\n').length;
} catch (error) {
return 0;
}
}
// Function to extract imports from TypeScript/JavaScript files
async function extractImports(filePath) {
try {
const content = await fs.readFile(filePath, 'utf-8');
const importRegex = /import\s+(?:(?:\{[^}]*\}|\*\s+as\s+\w+|\w+)(?:\s*,\s*(?:\{[^}]*\}|\*\s+as\s+\w+|\w+))*\s+from\s+)?['"`]([^'"`]+)['"`]/g;
const imports = [];
let match;
while ((match = importRegex.exec(content)) !== null) {
imports.push(match[1]);
}
return imports;
} catch (error) {
return [];
}
}
// Function to extract exports from TypeScript/JavaScript files
async function extractExports(filePath) {
try {
const content = await fs.readFile(filePath, 'utf-8');
const exports = [];
// Named exports: export { name1, name2 }
const namedExportRegex = /export\s*\{\s*([^}]+)\s*\}/g;
let match;
while ((match = namedExportRegex.exec(content)) !== null) {
const names = match[1].split(',').map(n => n.trim().split(' as ')[0].trim());
exports.push(...names);
}
// Function/class exports: export function name() {} or export class Name {}
const functionClassRegex = /export\s+(?:async\s+)?(?:function|class)\s+(\w+)/g;
while ((match = functionClassRegex.exec(content)) !== null) {
exports.push(match[1]);
}
// Variable exports: export const name = ...
const variableRegex = /export\s+(?:const|let|var)\s+(\w+)/g;
while ((match = variableRegex.exec(content)) !== null) {
exports.push(match[1]);
}
// Default export
if (content.includes('export default')) {
exports.push('default');
}
return [...new Set(exports)]; // Remove duplicates
} catch (error) {
return [];
}
}
// Function to read file content with size limit
async function getFileContent(filePath, maxSize = MAX_CONTENT_SIZE) {
try {
const content = await fs.readFile(filePath, 'utf-8');
if (content.length <= maxSize) {
return {
content,
truncated: false,
originalSize: content.length
};
} else {
return {
content: content.substring(0, CONTENT_PREVIEW_SIZE) + '\n\n... [Content truncated - file too large] ...\n\n' + content.substring(content.length - CONTENT_PREVIEW_SIZE),
truncated: true,
originalSize: content.length
};
}
} catch (error) {
return {
content: `[Error reading file: ${error.message}]`,
truncated: false,
originalSize: 0
};
}
}
// Function to extract metadata from file content
function extractMetadata(content, fileType, filePath) {
const metadata = {
functions: [],
classes: [],
interfaces: [],
types: [],
constants: [],
comments: []
};
if (fileType === 'typescript' || fileType === 'javascript') {
// Extract functions
const functionRegex = /(?:export\s+)?(?:async\s+)?function\s+(\w+)/g;
let match;
while ((match = functionRegex.exec(content)) !== null) {
metadata.functions.push(match[1]);
}
// Extract arrow functions
const arrowFunctionRegex = /(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?\([^)]*\)\s*=>/g;
while ((match = arrowFunctionRegex.exec(content)) !== null) {
metadata.functions.push(match[1]);
}
// Extract classes
const classRegex = /(?:export\s+)?class\s+(\w+)/g;
while ((match = classRegex.exec(content)) !== null) {
metadata.classes.push(match[1]);
}
// Extract interfaces (TypeScript)
if (fileType === 'typescript') {
const interfaceRegex = /(?:export\s+)?interface\s+(\w+)/g;
while ((match = interfaceRegex.exec(content)) !== null) {
metadata.interfaces.push(match[1]);
}
// Extract type aliases
const typeRegex = /(?:export\s+)?type\s+(\w+)/g;
while ((match = typeRegex.exec(content)) !== null) {
metadata.types.push(match[1]);
}
}
// Extract constants
const constRegex = /(?:export\s+)?const\s+([A-Z_][A-Z0-9_]*)\s*=/g;
while ((match = constRegex.exec(content)) !== null) {
metadata.constants.push(match[1]);
}
// Extract JSDoc comments
const jsdocRegex = /\/\*\*[\s\S]*?\*\//g;
const jsdocMatches = content.match(jsdocRegex);
if (jsdocMatches) {
metadata.comments = jsdocMatches.slice(0, 3); // First 3 JSDoc comments
}
}
return metadata;
}
// Function to analyze directory recursively
async function analyzeDirectory(dirPath, relativePath = '') {
const items = await fs.readdir(dirPath);
const analysis = {
files: [],
directories: [],
totalFiles: 0,
totalLines: 0,
fileTypes: {},
imports: new Set(),
largestFiles: [],
recentFiles: []
};
for (const item of items) {
const fullPath = path.join(dirPath, item);
const itemRelativePath = path.join(relativePath, item);
const stats = await getFileStats(fullPath);
if (!stats) continue;
if (stats.isDirectory) {
analysis.directories.push({
name: item,
path: itemRelativePath,
stats
});
// Recursively analyze subdirectory
const subAnalysis = await analyzeDirectory(fullPath, itemRelativePath);
analysis.files.push(...subAnalysis.files);
analysis.totalFiles += subAnalysis.totalFiles;
analysis.totalLines += subAnalysis.totalLines;
analysis.imports = new Set([...analysis.imports, ...subAnalysis.imports]);
// Merge file types
for (const [type, count] of Object.entries(subAnalysis.fileTypes)) {
analysis.fileTypes[type] = (analysis.fileTypes[type] || 0) + count;
}
} else {
const fileType = getFileType(item);
const lines = await countLines(fullPath);
const imports = fileType === 'typescript' || fileType === 'javascript'
? await extractImports(fullPath)
: [];
const exports = fileType === 'typescript' || fileType === 'javascript'
? await extractExports(fullPath)
: [];
// Get file content if enabled
const fileContent = INCLUDE_CONTENT ? await getFileContent(fullPath) : null;
const metadata = fileContent ? extractMetadata(fileContent.content, fileType, fullPath) : null;
const fileInfo = {
name: item,
path: itemRelativePath,
type: fileType,
size: stats.size,
lines,
created: stats.created,
modified: stats.modified,
imports,
exports,
content: fileContent,
metadata
};
analysis.files.push(fileInfo);
analysis.totalFiles++;
analysis.totalLines += lines;
analysis.fileTypes[fileType] = (analysis.fileTypes[fileType] || 0) + 1;
// Add imports to global set
imports.forEach(imp => analysis.imports.add(imp));
}
}
return analysis;
}
// Function to format file size
function formatFileSize(bytes) {
const sizes = ['B', 'KB', 'MB', 'GB'];
if (bytes === 0) return '0 B';
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
}
// Function to format date
function formatDate(date) {
return date.toISOString().split('T')[0];
}
// Main function to analyze a specific directory
async function generateSummaryForDirectory(dirPath, outputFile, dirName) {
console.log(`🔍 Analyzing ${dirName} folder...`);
try {
// Check if directory exists
const dirExists = await fs.access(dirPath).then(() => true).catch(() => false);
if (!dirExists) {
throw new Error(`Directory not found: ${dirPath}`);
}
// Analyze the entire directory
const analysis = await analyzeDirectory(dirPath);
// Sort files by size and modification date
analysis.largestFiles = analysis.files
.sort((a, b) => b.size - a.size)
.slice(0, 10);
analysis.recentFiles = analysis.files
.sort((a, b) => b.modified - a.modified)
.slice(0, 10);
// Generate summary content
const summary = generateSummaryContent(analysis, dirName);
// Write to file
await fs.writeFile(outputFile, summary, 'utf-8');
console.log(`✅ Summary written to: ${outputFile}`);
console.log(`📊 Total files: ${analysis.totalFiles}`);
console.log(`📝 Total lines: ${analysis.totalLines.toLocaleString()}`);
console.log(`📁 Directories: ${analysis.directories.length}`);
return analysis;
} catch (error) {
console.error(`❌ Error generating ${dirName} summary:`, error.message);
throw error;
}
}
// Main function
async function generateSummary() {
try {
// Analyze src directory
await generateSummaryForDirectory(SRC_DIR, SRC_OUTPUT_FILE, 'src');
console.log('');
// Analyze test directory
await generateSummaryForDirectory(TEST_DIR, TEST_OUTPUT_FILE, 'test');
console.log('\n🎉 Both summaries generated successfully!');
} catch (error) {
console.error('❌ Error generating summaries:', error.message);
process.exit(1);
}
}
// Function to generate summary content
function generateSummaryContent(analysis, dirName = 'src') {
const now = new Date();
const isTestDir = dirName === 'test';
let content = `# ${isTestDir ? 'Test Code' : 'Source Code'} Analysis Summary
Generated: ${now.toISOString()}
Project: SAP Docs MCP
${isTestDir ? 'Test' : 'Source'} Directory: ${dirName}/
## 📊 Overview
- Total Files: ${analysis.totalFiles.toLocaleString()}
- Total Lines of Code: ${analysis.totalLines.toLocaleString()}
- Directories: ${analysis.directories.length}
- Unique Imports: ${analysis.imports.size}
## 📁 Directory Structure
${analysis.directories.map(dir => `- ${dir.path}/`).join('\n')}
## 📄 File Types Distribution
${Object.entries(analysis.fileTypes)
.sort(([,a], [,b]) => b - a)
.map(([type, count]) => `- ${type}: ${count} files`)
.join('\n')}
## 🔝 Largest Files (by size)
${analysis.largestFiles.map((file, index) =>
`${index + 1}. ${file.path} (${formatFileSize(file.size)}, ${file.lines} lines)`
).join('\n')}
## ⏰ Recently Modified Files
${analysis.recentFiles.map((file, index) =>
`${index + 1}. ${file.path} (${formatDate(file.modified)})`
).join('\n')}
## 📋 Detailed File Analysis
${analysis.files
.sort((a, b) => a.path.localeCompare(b.path))
.map(file => {
let fileSection = `### 📄 ${file.path}
**Type:** ${file.type}
**Size:** ${formatFileSize(file.size)}
**Lines:** ${file.lines}
**Modified:** ${formatDate(file.modified)}`;
if (file.imports.length > 0) {
fileSection += `\n**Imports:** ${file.imports.join(', ')}`;
}
if (file.exports.length > 0) {
fileSection += `\n**Exports:** ${file.exports.join(', ')}`;
}
if (file.metadata) {
const meta = file.metadata;
if (meta.functions.length > 0) {
fileSection += `\n**Functions:** ${meta.functions.join(', ')}`;
}
if (meta.classes.length > 0) {
fileSection += `\n**Classes:** ${meta.classes.join(', ')}`;
}
if (meta.interfaces.length > 0) {
fileSection += `\n**Interfaces:** ${meta.interfaces.join(', ')}`;
}
if (meta.types.length > 0) {
fileSection += `\n**Types:** ${meta.types.join(', ')}`;
}
if (meta.constants.length > 0) {
fileSection += `\n**Constants:** ${meta.constants.join(', ')}`;
}
}
if (file.content && INCLUDE_CONTENT) {
fileSection += `\n\n**Content:**`;
if (file.content.truncated) {
fileSection += ` (${formatFileSize(file.content.originalSize)} - truncated)`;
}
fileSection += `\n\`\`\`${file.type === 'typescript' ? 'typescript' : file.type === 'javascript' ? 'javascript' : ''}\n${file.content.content}\n\`\`\``;
}
return fileSection;
})
.join('\n\n')}
## 🔗 Most Common Imports
${Array.from(analysis.imports)
.sort()
.slice(0, 20)
.map(imp => `- ${imp}`)
.join('\n')}
## 🔍 Code Analysis Summary
${(() => {
const allFunctions = analysis.files.flatMap(f => f.metadata?.functions || []);
const allClasses = analysis.files.flatMap(f => f.metadata?.classes || []);
const allInterfaces = analysis.files.flatMap(f => f.metadata?.interfaces || []);
const allTypes = analysis.files.flatMap(f => f.metadata?.types || []);
const allExports = analysis.files.flatMap(f => f.exports || []);
return `- Total Functions: ${allFunctions.length}
- Total Classes: ${allClasses.length}
- Total Interfaces: ${allInterfaces.length}
- Total Types: ${allTypes.length}
- Total Exports: ${allExports.length}
- Files with Content: ${analysis.files.filter(f => f.content).length}`;
})()}
## 📈 Statistics
- Average file size: ${formatFileSize(analysis.files.reduce((sum, f) => sum + f.size, 0) / analysis.totalFiles)}
- Average lines per file: ${Math.round(analysis.totalLines / analysis.totalFiles)}
- Most common file type: ${Object.entries(analysis.fileTypes).sort(([,a], [,b]) => b - a)[0]?.[0] || 'N/A'}
- Oldest file: ${analysis.files.length > 0 ? formatDate(analysis.files.reduce((oldest, f) => f.created < oldest.created ? f : oldest).created) : 'N/A'}
- Newest file: ${analysis.files.length > 0 ? formatDate(analysis.files.reduce((newest, f) => f.modified > newest.modified ? f : newest).modified) : 'N/A'}
- Content included: ${INCLUDE_CONTENT ? 'Yes' : 'No'}
- Max content size per file: ${formatFileSize(MAX_CONTENT_SIZE)}
---
Generated by summarize-src.js for ${dirName}/ directory
`;
return content;
}
// Run the script
generateSummary();
```
--------------------------------------------------------------------------------
/.github/workflows/update-submodules.yml:
--------------------------------------------------------------------------------
```yaml
name: Documentation Update & Database Health Monitor
on:
# Run daily at 4 AM UTC for submodule updates
schedule:
- cron: '0 4 * * *'
# Run every 2 hours for database health monitoring
- cron: '0 */2 * * *'
# Allow manual triggering with options
workflow_dispatch:
inputs:
health_check_only:
description: 'Perform health check only (no submodule update)'
required: false
default: 'false'
type: boolean
force_rebuild:
description: 'Force database rebuild even if healthy'
required: false
default: 'false'
type: boolean
jobs:
update-submodules:
name: Update Submodules on Server
runs-on: ubuntu-22.04
environment:
name: remove server
steps:
- name: Update submodules on server with database safety checks
uses: appleboy/[email protected]
with:
host: ${{ secrets.SERVER_IP }}
username: ${{ secrets.SERVER_USERNAME }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
set -e
cd /opt/mcp-sap/mcp-sap-docs
DB_PATH="/opt/mcp-sap/mcp-sap-docs/dist/data/docs.sqlite"
HEALTH_CHECK_ONLY="${{ inputs.health_check_only }}"
FORCE_REBUILD="${{ inputs.force_rebuild }}"
# Determine if this is a health check or full update
IS_HEALTH_CHECK_SCHEDULE=false
if [ "$(date +%H)" != "04" ] && [ "$HEALTH_CHECK_ONLY" != "true" ]; then
IS_HEALTH_CHECK_SCHEDULE=true
echo "=== Database Health Check (Scheduled) Started ==="
elif [ "$HEALTH_CHECK_ONLY" = "true" ]; then
echo "=== Database Health Check (Manual) Started ==="
else
echo "=== Documentation Update with Database Safety Started ==="
fi
echo "📅 $(date)"
echo "🔍 Health check only: $HEALTH_CHECK_ONLY"
echo "🔧 Force rebuild: $FORCE_REBUILD"
# Function to check SQLite database integrity
check_db_integrity() {
local db_path="$1"
if [ -f "$db_path" ]; then
echo "🔍 Checking database integrity: $db_path"
if sqlite3 "$db_path" "PRAGMA integrity_check;" 2>/dev/null | grep -q "ok"; then
echo "✅ Database integrity OK"
return 0
else
echo "❌ Database corruption detected"
return 1
fi
else
echo "ℹ️ Database file does not exist: $db_path"
return 1
fi
}
echo "==> Database health check"
DB_WAS_CORRUPT=0
DB_NEEDS_REPAIR=0
if ! check_db_integrity "$DB_PATH"; then
DB_WAS_CORRUPT=1
DB_NEEDS_REPAIR=1
echo "⚠️ Database corruption detected"
fi
# Force rebuild if requested
if [ "$FORCE_REBUILD" = "true" ]; then
DB_NEEDS_REPAIR=1
echo "🔄 Force rebuild requested"
fi
# For health checks, test search functionality
if [ "$IS_HEALTH_CHECK_SCHEDULE" = "true" ] || [ "$HEALTH_CHECK_ONLY" = "true" ]; then
echo "==> Testing search functionality"
SEARCH_TEST=$(curl -s -X POST http://127.0.0.1:3001/mcp \
-H "Content-Type: application/json" \
-d '{"role": "user", "content": "health check search"}' || echo "curl_failed")
if echo "$SEARCH_TEST" | grep -q "SqliteError\|SQLITE_CORRUPT\|Tool execution failed\|curl_failed"; then
DB_NEEDS_REPAIR=1
echo "❌ Search functionality failed - repair needed"
echo "Response: $SEARCH_TEST"
else
echo "✅ Search functionality OK"
fi
fi
# Only proceed with backup and rebuilds if needed
REPAIR_PERFORMED=0
SUBMODULES_UPDATED=0
if [ "$DB_NEEDS_REPAIR" -eq 1 ]; then
echo ""
echo "🔧 DATABASE REPAIR REQUIRED"
echo "=========================="
echo "==> Creating backup before repair"
BACKUP_DIR="/opt/mcp-sap/backups"
mkdir -p "$BACKUP_DIR"
if [ -f "$DB_PATH" ] && [ "$DB_WAS_CORRUPT" -eq 0 ]; then
BACKUP_PATH="$BACKUP_DIR/pre-repair-$(date +%Y%m%d-%H%M%S).sqlite"
cp "$DB_PATH" "$BACKUP_PATH"
echo "✅ Database backed up to $BACKUP_PATH"
fi
echo "==> Checking system resources"
# Check disk space
AVAILABLE_MB=$(df /opt/mcp-sap --output=avail -m | tail -n1)
if [ "$AVAILABLE_MB" -lt 1000 ]; then
echo "❌ ERROR: Insufficient disk space. Available: ${AVAILABLE_MB}MB, Required: 1000MB"
exit 1
fi
echo "✅ Disk space OK: ${AVAILABLE_MB}MB available"
# Check memory
AVAILABLE_KB=$(awk '/MemAvailable/ { print $2 }' /proc/meminfo)
AVAILABLE_MB_MEM=$((AVAILABLE_KB / 1024))
if [ "$AVAILABLE_MB_MEM" -lt 512 ]; then
echo "❌ ERROR: Insufficient memory. Available: ${AVAILABLE_MB_MEM}MB, Required: 512MB"
exit 1
fi
echo "✅ Memory OK: ${AVAILABLE_MB_MEM}MB available"
echo "==> Gracefully stopping services for repair"
pm2 stop all || true
sleep 3
# Remove corrupted database
echo "🗑️ Removing corrupted database"
rm -f "$DB_PATH"
# Rebuild database
echo "🔨 Rebuilding database..."
npm run build:fts
REPAIR_PERFORMED=1
elif [ "$IS_HEALTH_CHECK_SCHEDULE" != "true" ] && [ "$HEALTH_CHECK_ONLY" != "true" ]; then
echo ""
echo "📦 SUBMODULE UPDATE PROCESS"
echo "=========================="
echo "==> Creating backup before update"
BACKUP_DIR="/opt/mcp-sap/backups"
mkdir -p "$BACKUP_DIR"
if [ -f "$DB_PATH" ]; then
BACKUP_PATH="$BACKUP_DIR/pre-update-$(date +%Y%m%d-%H%M%S).sqlite"
cp "$DB_PATH" "$BACKUP_PATH"
echo "✅ Database backed up to $BACKUP_PATH"
fi
echo "==> Checking system resources"
# Check disk space
AVAILABLE_MB=$(df /opt/mcp-sap --output=avail -m | tail -n1)
if [ "$AVAILABLE_MB" -lt 500 ]; then
echo "❌ ERROR: Insufficient disk space. Available: ${AVAILABLE_MB}MB, Required: 500MB"
exit 1
fi
echo "✅ Disk space OK: ${AVAILABLE_MB}MB available"
echo "==> Gracefully stopping services for update"
pm2 stop all || true
sleep 3
# Configure git for HTTPS
git config --global url."https://github.com/".insteadOf [email protected]:
echo "==> Checking for submodule updates"
# Get current submodule commits
git submodule status > /tmp/before-update.txt || true
# Reuse setup.sh to ensure shallow, single-branch submodules and build
echo "==> Running setup script (includes submodule update and rebuild)"
SKIP_NESTED_SUBMODULES=1 bash setup.sh
# Check what changed
git submodule status > /tmp/after-update.txt || true
if ! diff -q /tmp/before-update.txt /tmp/after-update.txt >/dev/null 2>&1; then
echo "✅ Submodules were updated - changes detected"
SUBMODULES_UPDATED=1
else
echo "ℹ️ No submodule changes detected"
SUBMODULES_UPDATED=0
fi
else
echo "ℹ️ Health check only - no database repair or submodule update needed"
fi
# Post-operation database integrity check (only if changes were made)
if [ "$REPAIR_PERFORMED" -eq 1 ] || [ "$SUBMODULES_UPDATED" -eq 1 ]; then
echo "==> Post-operation database integrity check"
if ! check_db_integrity "$DB_PATH"; then
echo "❌ ERROR: Database corruption after operation"
# Try to restore from backup if available
if [ -f "$BACKUP_PATH" ] && [ "$DB_WAS_CORRUPT" -eq 0 ]; then
echo "🔄 Attempting to restore from backup..."
cp "$BACKUP_PATH" "$DB_PATH"
if check_db_integrity "$DB_PATH"; then
echo "✅ Successfully restored from backup"
else
echo "❌ Backup is also corrupted - forcing fresh rebuild"
rm -f "$DB_PATH"
npm run build:fts
fi
else
echo "🔄 No clean backup available - forcing fresh rebuild"
rm -f "$DB_PATH"
npm run build:fts
fi
# Final check
if ! check_db_integrity "$DB_PATH"; then
echo "❌ CRITICAL: Could not create working database"
exit 1
fi
fi
fi
# Restart services if they were stopped
if [ "$REPAIR_PERFORMED" -eq 1 ] || [ "$SUBMODULES_UPDATED" -eq 1 ]; then
echo "==> Creating logs directory"
mkdir -p /opt/mcp-sap/logs
chown -R "$USER":"$USER" /opt/mcp-sap/logs
echo "==> Restarting services"
pm2 start all
pm2 save
sleep 10
fi
echo "==> Health verification"
# Test search functionality
SEARCH_TEST=$(curl -s -X POST http://127.0.0.1:3001/mcp \
-H "Content-Type: application/json" \
-d '{"role": "user", "content": "test search"}' || echo "curl_failed")
if echo "$SEARCH_TEST" | grep -q "SqliteError\|SQLITE_CORRUPT\|Tool execution failed\|curl_failed"; then
echo "❌ ERROR: Search functionality test failed after update"
echo "Response: $SEARCH_TEST"
exit 1
else
echo "✅ Search functionality verified"
fi
echo "==> Cleanup old backups (keep last 7)"
ls -t "$BACKUP_DIR"/*.sqlite 2>/dev/null | tail -n +8 | xargs -r rm --
echo ""
if [ "$IS_HEALTH_CHECK_SCHEDULE" = "true" ] || [ "$HEALTH_CHECK_ONLY" = "true" ]; then
echo "=== Health Check Completed Successfully ==="
echo "🔍 Database health: $([ "$DB_NEEDS_REPAIR" -eq 0 ] && echo "Healthy" || echo "Repaired")"
echo "🔧 Repair performed: $([ "$REPAIR_PERFORMED" -eq 1 ] && echo "Yes" || echo "No")"
else
echo "=== Documentation Update Completed Successfully ==="
echo "📊 Submodules updated: $([ "$SUBMODULES_UPDATED" -eq 1 ] && echo "Yes" || echo "No")"
echo "🔧 Database repair: $([ "$REPAIR_PERFORMED" -eq 1 ] && echo "Yes" || echo "No")"
fi
echo "📅 Completion time: $(date)"
- name: Create success summary
if: success()
run: |
echo "## ✅ Documentation Update Complete (Enhanced with DB Safety)" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Update Results:" >> $GITHUB_STEP_SUMMARY
echo "✅ **Submodules**: Updated to latest remote commits" >> $GITHUB_STEP_SUMMARY
echo "✅ **Search Index**: Rebuilt with enhanced safety checks" >> $GITHUB_STEP_SUMMARY
echo "✅ **Database Health**: Verified before and after update" >> $GITHUB_STEP_SUMMARY
echo "✅ **Services**: Restarted and verified working" >> $GITHUB_STEP_SUMMARY
echo "✅ **Backup**: Pre-update backup created (if needed)" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Safety Features Added:" >> $GITHUB_STEP_SUMMARY
echo "- 🔍 **Database integrity checks** before and after update" >> $GITHUB_STEP_SUMMARY
echo "- 📦 **Automatic backup** of healthy database before changes" >> $GITHUB_STEP_SUMMARY
echo "- 🔧 **Automatic corruption recovery** with backup restoration" >> $GITHUB_STEP_SUMMARY
echo "- 🧪 **Search functionality verification** after update" >> $GITHUB_STEP_SUMMARY
echo "- 💾 **System resource validation** before rebuild" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "🕐 **Update time**: $(date -u)" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "The MCP server now has the latest SAP documentation with verified database integrity."
- name: Create failure summary
if: failure()
run: |
echo "## ❌ Documentation Update Failed" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "🚨 **Status**: Enhanced update process encountered errors" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Enhanced Troubleshooting:" >> $GITHUB_STEP_SUMMARY
echo "**Check these areas on the server:**" >> $GITHUB_STEP_SUMMARY
echo "1. **Database corruption**: Check for SQLite errors in logs" >> $GITHUB_STEP_SUMMARY
echo "2. **System resources**: Disk space and memory availability" >> $GITHUB_STEP_SUMMARY
echo "3. **Service status**: PM2 process health and logs" >> $GITHUB_STEP_SUMMARY
echo "4. **Backup availability**: Check /opt/mcp-sap/backups/" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Quick Recovery Commands:" >> $GITHUB_STEP_SUMMARY
echo "\`\`\`bash" >> $GITHUB_STEP_SUMMARY
echo "# Check PM2 status and logs" >> $GITHUB_STEP_SUMMARY
echo "pm2 status && pm2 logs --lines 50" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "# Manual database rebuild if corruption detected" >> $GITHUB_STEP_SUMMARY
echo "cd /opt/mcp-sap/mcp-sap-docs" >> $GITHUB_STEP_SUMMARY
echo "pm2 stop all" >> $GITHUB_STEP_SUMMARY
echo "rm -f dist/data/docs.sqlite" >> $GITHUB_STEP_SUMMARY
echo "npm run build:fts" >> $GITHUB_STEP_SUMMARY
echo "pm2 start all" >> $GITHUB_STEP_SUMMARY
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "🕐 **Failure time**: $(date -u)" >> $GITHUB_STEP_SUMMARY
```
--------------------------------------------------------------------------------
/src/metadata.json:
--------------------------------------------------------------------------------
```json
{
"version": 1,
"updated_at": "2025-01-14",
"description": "Centralized configuration for SAP Docs MCP search system",
"sources": [
{
"id": "sapui5",
"type": "documentation",
"lang": "en",
"boost": 0.1,
"tags": ["ui5", "frontend", "javascript"],
"description": "SAPUI5 framework documentation",
"libraryId": "/sapui5",
"sourcePath": "sapui5-docs/docs",
"baseUrl": "https://ui5.sap.com",
"pathPattern": "/#/topic/{file}",
"anchorStyle": "custom"
},
{
"id": "cap",
"type": "documentation",
"lang": "en",
"boost": 0.1,
"tags": ["backend", "nodejs", "java", "cds"],
"description": "SAP Cloud Application Programming model",
"libraryId": "/cap",
"sourcePath": "cap-docs",
"baseUrl": "https://cap.cloud.sap",
"pathPattern": "/docs/{file}",
"anchorStyle": "docsify"
},
{
"id": "openui5-api",
"type": "api",
"lang": "en",
"boost": 0.1,
"tags": ["api", "controls", "ui5"],
"description": "OpenUI5 API documentation",
"libraryId": "/openui5-api",
"sourcePath": "openui5/src",
"baseUrl": "https://sdk.openui5.org",
"pathPattern": "/#/api/{file}",
"anchorStyle": "custom"
},
{
"id": "openui5-samples",
"type": "samples",
"lang": "en",
"boost": 0.05,
"tags": ["samples", "examples", "ui5"],
"description": "OpenUI5 code samples",
"libraryId": "/openui5-samples",
"sourcePath": "openui5/src",
"baseUrl": "https://sdk.openui5.org",
"pathPattern": "/entity/{file}",
"anchorStyle": "custom"
},
{
"id": "wdi5",
"type": "documentation",
"lang": "en",
"boost": 0.05,
"tags": ["testing", "e2e", "webdriver"],
"description": "wdi5 testing framework",
"libraryId": "/wdi5",
"sourcePath": "wdi5/docs",
"baseUrl": "https://ui5-community.github.io/wdi5",
"pathPattern": "#{file}",
"anchorStyle": "docsify"
},
{
"id": "ui5-tooling",
"type": "documentation",
"lang": "en",
"boost": 0.0,
"tags": ["tooling", "build", "cli"],
"description": "UI5 Tooling documentation",
"libraryId": "/ui5-tooling",
"sourcePath": "ui5-tooling/docs",
"baseUrl": "https://sap.github.io/ui5-tooling/v4",
"pathPattern": "/pages/{file}",
"anchorStyle": "github"
},
{
"id": "cloud-mta-build-tool",
"type": "documentation",
"lang": "en",
"boost": 0.0,
"tags": ["mta", "build", "deployment"],
"description": "Cloud MTA Build Tool",
"libraryId": "/cloud-mta-build-tool",
"sourcePath": "cloud-mta-build-tool/docs/docs",
"baseUrl": "https://sap.github.io/cloud-mta-build-tool",
"pathPattern": "/{file}",
"anchorStyle": "github"
},
{
"id": "ui5-webcomponents",
"type": "documentation",
"lang": "en",
"boost": 0.0,
"tags": ["webcomponents", "ui5"],
"description": "UI5 Web Components",
"libraryId": "/ui5-webcomponents",
"sourcePath": "ui5-webcomponents/docs",
"baseUrl": "https://sap.github.io/ui5-webcomponents/docs",
"pathPattern": "/{file}",
"anchorStyle": "github"
},
{
"id": "cloud-sdk-js",
"type": "documentation",
"lang": "en",
"boost": 0.05,
"tags": ["sdk", "javascript", "cloud"],
"description": "SAP Cloud SDK for JavaScript",
"libraryId": "/cloud-sdk-js",
"sourcePath": "cloud-sdk/docs-js",
"baseUrl": "https://sap.github.io/cloud-sdk/docs/js",
"pathPattern": "/{file}",
"anchorStyle": "github"
},
{
"id": "cloud-sdk-java",
"type": "documentation",
"lang": "en",
"boost": 0.05,
"tags": ["sdk", "java", "cloud"],
"description": "SAP Cloud SDK for Java",
"libraryId": "/cloud-sdk-java",
"sourcePath": "cloud-sdk/docs-java",
"baseUrl": "https://sap.github.io/cloud-sdk/docs/java",
"pathPattern": "/{file}",
"anchorStyle": "github"
},
{
"id": "cloud-sdk-ai-js",
"type": "documentation",
"lang": "en",
"boost": 0.05,
"tags": ["ai", "sdk", "javascript"],
"description": "SAP Cloud SDK AI for JavaScript",
"libraryId": "/cloud-sdk-ai-js",
"sourcePath": "cloud-sdk-ai/docs-js",
"baseUrl": "https://sap.github.io/ai-sdk/docs/js",
"pathPattern": "/{file}",
"anchorStyle": "github"
},
{
"id": "cloud-sdk-ai-java",
"type": "documentation",
"lang": "en",
"boost": 0.05,
"tags": ["ai", "sdk", "java"],
"description": "SAP Cloud SDK AI for Java",
"libraryId": "/cloud-sdk-ai-java",
"sourcePath": "cloud-sdk-ai/docs-java",
"baseUrl": "https://sap.github.io/ai-sdk/docs/java",
"pathPattern": "/{file}",
"anchorStyle": "github"
},
{
"id": "ui5-typescript",
"type": "documentation",
"lang": "en",
"boost": 0.1,
"tags": ["ui5", "typescript", "types", "frontend"],
"description": "UI5 TypeScript",
"libraryId": "/ui5-typescript",
"sourcePath": "ui5-typescript",
"baseUrl": "https://github.com/UI5/typescript/blob/gh-pages",
"pathPattern": "/{file}",
"anchorStyle": "github"
},
{
"id": "ui5-cc-spreadsheetimporter",
"type": "documentation",
"lang": "en",
"boost": 0.05,
"tags": ["ui5", "spreadsheet", "importer", "custom-control"],
"description": "UI5 CC Spreadsheet Importer",
"libraryId": "/ui5-cc-spreadsheetimporter",
"sourcePath": "ui5-cc-spreadsheetimporter/docs",
"baseUrl": "https://docs.spreadsheet-importer.com",
"pathPattern": "/pages/{file}/",
"anchorStyle": "github"
},
{
"id": "abap-cheat-sheets",
"type": "documentation",
"lang": "en",
"boost": 0.05,
"tags": ["abap", "syntax", "cheat-sheets", "examples", "backend"],
"description": "ABAP Cheat Sheets",
"libraryId": "/abap-cheat-sheets",
"sourcePath": "abap-cheat-sheets",
"baseUrl": "https://github.com/SAP-samples/abap-cheat-sheets/blob/main",
"pathPattern": "/{file}",
"anchorStyle": "github"
},
{
"id": "sap-styleguides",
"type": "documentation",
"lang": "en",
"boost": 0.06,
"tags": ["abap", "clean-code", "style-guide", "best-practices", "code-review"],
"description": "SAP Style Guides",
"libraryId": "/sap-styleguides",
"sourcePath": "sap-styleguides",
"baseUrl": "https://github.com/SAP/styleguides/blob/main",
"pathPattern": "/{file}",
"anchorStyle": "github"
},
{
"id": "dsag-abap-leitfaden",
"type": "documentation",
"lang": "de",
"boost": 0.05,
"tags": ["abap", "leitfaden", "best-practices", "german", "dsag", "clean-core"],
"description": "DSAG ABAP Leitfaden",
"libraryId": "/dsag-abap-leitfaden",
"sourcePath": "dsag-abap-leitfaden/docs",
"baseUrl": "https://1dsag.github.io/ABAP-Leitfaden",
"pathPattern": "/{file}/",
"anchorStyle": "github"
},
{
"id": "abap-fiori-showcase",
"type": "documentation",
"lang": "en",
"boost": 0.08,
"tags": ["fiori-elements", "abap", "rap", "annotations", "odata-v4", "showcase"],
"description": "ABAP Platform Fiori Feature Showcase",
"libraryId": "/abap-fiori-showcase",
"sourcePath": "abap-fiori-showcase",
"baseUrl": "https://github.com/SAP-samples/abap-platform-fiori-feature-showcase/blob/main",
"pathPattern": "/{file}",
"anchorStyle": "github"
},
{
"id": "cap-fiori-showcase",
"type": "documentation",
"lang": "en",
"boost": 0.08,
"tags": ["fiori-elements", "cap", "annotations", "odata-v4", "showcase", "nodejs"],
"description": "CAP Fiori Elements Feature Showcase",
"libraryId": "/cap-fiori-showcase",
"sourcePath": "cap-fiori-showcase",
"baseUrl": "https://github.com/SAP-samples/fiori-elements-feature-showcase/blob/main",
"pathPattern": "/{file}",
"anchorStyle": "github"
},
{
"id": "abap-docs-758",
"type": "documentation",
"lang": "en",
"boost": 0.05,
"tags": ["abap", "keyword-documentation", "language-reference", "syntax", "programming", "7.58"],
"description": "Official ABAP Keyword Documentation (7.58)",
"libraryId": "/abap-docs-758",
"sourcePath": "abap-docs/docs/7.58/md",
"baseUrl": "https://help.sap.com/doc/abapdocu_758_index_htm/7.58/en-US",
"pathPattern": "/{file}",
"anchorStyle": "sap-help"
},
{
"id": "abap-docs-757",
"type": "documentation",
"lang": "en",
"boost": 0.02,
"tags": ["abap", "keyword-documentation", "language-reference", "syntax", "programming", "7.57"],
"description": "Official ABAP Keyword Documentation (7.57)",
"libraryId": "/abap-docs-757",
"sourcePath": "abap-docs/docs/7.57/md",
"baseUrl": "https://help.sap.com/doc/abapdocu_757_index_htm/7.57/en-US",
"pathPattern": "/{file}",
"anchorStyle": "sap-help"
},
{
"id": "abap-docs-756",
"type": "documentation",
"lang": "en",
"boost": 0.01,
"tags": ["abap", "keyword-documentation", "language-reference", "syntax", "programming", "7.56"],
"description": "Official ABAP Keyword Documentation (7.56)",
"libraryId": "/abap-docs-756",
"sourcePath": "abap-docs/docs/7.56/md",
"baseUrl": "https://help.sap.com/doc/abapdocu_756_index_htm/7.56/en-US",
"pathPattern": "/{file}",
"anchorStyle": "sap-help"
},
{
"id": "abap-docs-755",
"type": "documentation",
"lang": "en",
"boost": 0.01,
"tags": ["abap", "keyword-documentation", "language-reference", "syntax", "programming", "7.55"],
"description": "Official ABAP Keyword Documentation (7.55)",
"libraryId": "/abap-docs-755",
"sourcePath": "abap-docs/docs/7.55/md",
"baseUrl": "https://help.sap.com/doc/abapdocu_755_index_htm/7.55/en-US",
"pathPattern": "/{file}",
"anchorStyle": "sap-help"
},
{
"id": "abap-docs-754",
"type": "documentation",
"lang": "en",
"boost": 0.01,
"tags": ["abap", "keyword-documentation", "language-reference", "syntax", "programming", "7.54"],
"description": "Official ABAP Keyword Documentation (7.54)",
"libraryId": "/abap-docs-754",
"sourcePath": "abap-docs/docs/7.54/md",
"baseUrl": "https://help.sap.com/doc/abapdocu_754_index_htm/7.54/en-US",
"pathPattern": "/{file}",
"anchorStyle": "sap-help"
},
{
"id": "abap-docs-753",
"type": "documentation",
"lang": "en",
"boost": 0.01,
"tags": ["abap", "keyword-documentation", "language-reference", "syntax", "programming", "7.53"],
"description": "Official ABAP Keyword Documentation (7.53)",
"libraryId": "/abap-docs-753",
"sourcePath": "abap-docs/docs/7.53/md",
"baseUrl": "https://help.sap.com/doc/abapdocu_753_index_htm/7.53/en-US",
"pathPattern": "/{file}",
"anchorStyle": "sap-help"
},
{
"id": "abap-docs-752",
"type": "documentation",
"lang": "en",
"boost": 0.01,
"tags": ["abap", "keyword-documentation", "language-reference", "syntax", "programming", "7.52"],
"description": "Official ABAP Keyword Documentation (7.52)",
"libraryId": "/abap-docs-752",
"sourcePath": "abap-docs/docs/7.52/md",
"baseUrl": "https://help.sap.com/doc/abapdocu_752_index_htm/7.52/en-US",
"pathPattern": "/{file}",
"anchorStyle": "sap-help"
},
{
"id": "abap-docs-latest",
"type": "documentation",
"lang": "en",
"boost": 1.0,
"tags": ["abap", "keyword-documentation", "language-reference", "syntax", "programming", "latest"],
"description": "Official ABAP Keyword Documentation (Latest)",
"libraryId": "/abap-docs-latest",
"sourcePath": "abap-docs/docs/latest/md",
"baseUrl": "https://help.sap.com/doc/abapdocu_latest_index_htm/latest/en-US",
"pathPattern": "/{file}",
"anchorStyle": "sap-help"
}
],
"synonyms": [
{ "from": "ui5", "to": ["sapui5", "openui5"] },
{ "from": "button", "to": ["btn", "sap.m.Button"] },
{ "from": "table", "to": ["sap.m.Table", "sap.ui.table.Table"] },
{ "from": "wizard", "to": ["sap.m.Wizard"] },
{ "from": "testing", "to": ["wdi5", "e2e", "automation"] },
{ "from": "cds", "to": ["cap", "cloud application programming"] },
{ "from": "odata", "to": ["rest", "api", "service"] },
{ "from": "typescript", "to": ["ts", "types", "type definitions"] },
{ "from": "spreadsheet", "to": ["excel", "csv", "import", "upload"] },
{ "from": "abap", "to": ["advanced business application programming", "sap programming"] },
{ "from": "clean-code", "to": ["clean code", "best practices", "style guide"] },
{ "from": "style-guide", "to": ["styleguide", "coding standards", "best practices"] },
{ "from": "leitfaden", "to": ["guidelines", "guide", "best practices"] },
{ "from": "clean-core", "to": ["clean core", "extensibility", "cloud development"] },
{ "from": "fiori-elements", "to": ["fiori elements", "annotations", "odata"] },
{ "from": "rap", "to": ["restful application programming", "abap restful"] },
{ "from": "annotations", "to": ["ui5 annotations", "odata annotations", "fiori annotations"] }
],
"acronyms": {
"CAP": ["Cloud Application Programming", "cds"],
"CDS": ["Core Data Services", "cap"],
"UI5": ["sapui5", "openui5"],
"BTP": ["Business Technology Platform", "cloud"],
"MTA": ["Multi-Target Application"],
"CQL": ["CDS Query Language", "query"],
"OData": ["Open Data Protocol", "rest", "api"],
"TS": ["TypeScript", "types"],
"ABAP": ["Advanced Business Application Programming", "sap programming"],
"DSAG": ["Deutschsprachige SAP-Anwendergruppe", "german sap user group"],
"RAP": ["RESTful Application Programming", "abap restful", "business object"]
},
"contextBoosts": {
"SAP Cloud SDK": {
"/cloud-sdk-ai-js": 1.0,
"/cloud-sdk-ai-java": 1.0,
"/cloud-sdk-js": 0.8,
"/cloud-sdk-java": 0.8,
"/cap": 0.2
},
"UI5": {
"/sapui5": 0.9,
"/openui5-api": 0.9,
"/openui5-samples": 0.9,
"/ui5-typescript": 0.8
},
"wdi5": {
"/wdi5": 1.0,
"/openui5-api": 0.4,
"/openui5-samples": 0.4,
"/sapui5": 0.4
},
"UI5 Web Components": {
"/ui5-webcomponents": 1.0
},
"UI5 Tooling": {
"/ui5-tooling": 1.0
},
"Cloud MTA Build Tool": {
"/cloud-mta-build-tool": 1.0
},
"CAP": {
"/cap": 1.0,
"/cap-fiori-showcase": 0.9,
"/sapui5": 0.2
},
"TypeScript": {
"/ui5-typescript": 1.0,
"/sapui5": 0.4,
"/openui5-api": 0.4
},
"UI5 CC Spreadsheet Importer": {
"/ui5-cc-spreadsheetimporter": 1.0,
"/sapui5": 0.3,
"/openui5-api": 0.3
},
"ABAP": {
"/abap-docs-latest": 1.0,
"/abap-cheat-sheets": 0.8,
"/sap-styleguides": 0.7,
"/dsag-abap-leitfaden": 0.6,
"/abap-docs-758": 0.05,
"/abap-docs-757": 0.02,
"/abap-docs-756": 0.01,
"/abap-docs-755": 0.01,
"/abap-docs-754": 0.01,
"/abap-docs-753": 0.01,
"/abap-docs-752": 0.01,
"/cap": 0.2
},
"Clean Code": {
"/sap-styleguides": 1.0,
"/dsag-abap-leitfaden": 0.8,
"/abap-cheat-sheets": 0.4
},
"DSAG": {
"/dsag-abap-leitfaden": 1.0,
"/abap-cheat-sheets": 0.5,
"/sap-styleguides": 0.3
},
"Fiori Elements": {
"/abap-fiori-showcase": 1.0,
"/cap-fiori-showcase": 1.0,
"/sapui5": 0.6,
"/cap": 0.5
},
"RAP": {
"/abap-fiori-showcase": 1.0,
"/abap-cheat-sheets": 0.6,
"/cap": 0.3
},
"7.58": {
"/abap-docs-758": 2.0,
"/abap-docs-latest": 0.3
},
"7.57": {
"/abap-docs-757": 2.0,
"/abap-docs-latest": 0.3
},
"7.56": {
"/abap-docs-756": 2.0,
"/abap-docs-latest": 0.3
},
"7.55": {
"/abap-docs-755": 2.0,
"/abap-docs-latest": 0.3
},
"7.54": {
"/abap-docs-754": 2.0,
"/abap-docs-latest": 0.3
},
"7.53": {
"/abap-docs-753": 2.0,
"/abap-docs-latest": 0.3
},
"7.52": {
"/abap-docs-752": 2.0,
"/abap-docs-latest": 0.3
},
"latest": {
"/abap-docs-latest": 1.5,
"/abap-docs-758": 0.1
}
},
"libraryMappings": {
"sapui5": "sapui5",
"cap": "cap",
"cloud-sdk-js": "cloud-sdk-js",
"cloud-sdk-ai-js": "cloud-sdk-ai-js",
"openui5-api": "sapui5",
"openui5-samples": "sapui5",
"wdi5": "wdi5",
"ui5-tooling": "ui5-tooling",
"cloud-mta-build-tool": "cloud-mta-build-tool",
"ui5-webcomponents": "ui5-webcomponents",
"cloud-sdk-java": "cloud-sdk-java",
"cloud-sdk-ai-java": "cloud-sdk-ai-java",
"ui5-typescript": "ui5-typescript",
"ui5-cc-spreadsheetimporter": "ui5-cc-spreadsheetimporter",
"abap-cheat-sheets": "abap-cheat-sheets",
"sap-styleguides": "sap-styleguides",
"dsag-abap-leitfaden": "dsag-abap-leitfaden",
"abap-fiori-showcase": "abap-fiori-showcase",
"cap-fiori-showcase": "cap-fiori-showcase",
"abap-docs-758": "abap-docs",
"abap-docs-757": "abap-docs",
"abap-docs-756": "abap-docs",
"abap-docs-755": "abap-docs",
"abap-docs-754": "abap-docs",
"abap-docs-753": "abap-docs",
"abap-docs-752": "abap-docs",
"abap-docs-latest": "abap-docs"
},
"contextEmojis": {
"CAP": "🏗️",
"wdi5": "🧪",
"UI5": "🎨",
"UI5 Web Components": "🕹️",
"SAP Cloud SDK": "🌐",
"UI5 Tooling": "🔧",
"Cloud MTA Build Tool": "🚢",
"TypeScript": "📝",
"UI5 CC Spreadsheet Importer": "📊",
"ABAP": "💻",
"Clean Code": "✨",
"DSAG": "🇩🇪",
"Fiori Elements": "📱",
"RAP": "⚡",
"MIXED": "🔀"
}
}
```
--------------------------------------------------------------------------------
/test/comprehensive-url-generation.test.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Comprehensive URL Generation Test Suite
*
* This test suite validates the URL generation system for SAP documentation sources.
* It tests both the main generateDocumentationUrl function and individual generator classes
* for 10+ different documentation sources including CAP, Cloud SDK, UI5, wdi5, etc.
*
* Key Features:
* - Reads from real source files when available (automatic path mapping)
* - Falls back to test data when source files don't exist
* - Uses real configuration from metadata.json (no hardcoded configs)
* - Comprehensive coverage of all URL generation patterns
* - Debug mode available with DEBUG_TESTS=true environment variable
*
* Running Tests:
* - npm run test:url-generation # Run URL generation tests
* - npm run test:url-generation:debug # Run with debug output
* - DEBUG_TESTS=true npx vitest run test/comprehensive-url-generation.test.ts
*
* Architecture:
* The system uses an abstract BaseUrlGenerator class with source-specific implementations
* for different documentation platforms. Each generator handles its own URL patterns,
* frontmatter parsing, and path transformations.
*/
import { describe, it, expect } from 'vitest';
import {
generateDocumentationUrl,
CloudSdkUrlGenerator,
SapUi5UrlGenerator,
CapUrlGenerator,
Wdi5UrlGenerator,
DsagUrlGenerator,
GenericUrlGenerator
} from '../src/lib/url-generation/index.js';
import { AbapUrlGenerator, generateAbapUrl } from '../src/lib/url-generation/abap.js';
import { DocUrlConfig, getDocUrlConfig } from '../src/lib/metadata.js';
describe('Comprehensive URL Generation System', () => {
/**
* Retrieves URL configuration from metadata.json for a given library
* @param libraryId - The library identifier (e.g., '/cloud-sdk-js')
* @returns Configuration object with baseUrl, pathPattern, and anchorStyle
* @throws Error if no configuration is found
*/
function getConfigForLibrary(libraryId: string): DocUrlConfig {
const config = getDocUrlConfig(libraryId);
if (!config) {
throw new Error(`No configuration found for library: ${libraryId}`);
}
return config;
}
/**
* Maps libraryId + relFile to actual source file path in the filesystem
* Handles different repository structures and path transformations
* @param libraryId - The library identifier
* @param relFile - The relative file path within the library
* @returns Full path to the actual source file
* @throws Error if no path mapping exists for the library
*/
function getSourceFilePath(libraryId: string, relFile: string): string {
const pathMappings: Record<string, { basePath: string; transform?: (relFile: string) => string }> = {
'/cap': { basePath: 'sources/cap-docs' },
'/cloud-mta-build-tool': { basePath: 'sources/cloud-mta-build-tool' },
'/cloud-sdk-js': { basePath: 'sources/cloud-sdk/docs-js' },
'/cloud-sdk-ai-js': { basePath: 'sources/cloud-sdk-ai/docs-js' },
'/openui5-api': {
basePath: 'sources/openui5',
transform: (relFile) => {
// Transform src/sap/m/Button.js → src/sap.m/src/sap/m/Button.js
const match = relFile.match(/^src\/sap\/([^\/]+)\/(.+)$/);
if (match) {
const [, module, file] = match;
return `src/sap.${module}/src/sap/${module}/${file}`;
}
return relFile;
}
},
'/openui5-samples': { basePath: 'sources/openui5' },
'/sapui5': { basePath: 'sources/sapui5-docs/docs' },
'/ui5-tooling': { basePath: 'sources/ui5-tooling/docs' },
'/ui5-webcomponents': { basePath: 'sources/ui5-webcomponents/docs' },
'/wdi5': { basePath: 'sources/wdi5/docs' },
'/ui5-typescript': { basePath: 'sources/ui5-typescript' },
'/ui5-cc-spreadsheetimporter': { basePath: 'sources/ui5-cc-spreadsheetimporter/docs' },
'/abap-cheat-sheets': { basePath: 'sources/abap-cheat-sheets' },
'/sap-styleguides': { basePath: 'sources/sap-styleguides' },
'/dsag-abap-leitfaden': { basePath: 'sources/dsag-abap-leitfaden/docs' },
'/abap-fiori-showcase': { basePath: 'sources/abap-fiori-showcase' },
'/cap-fiori-showcase': { basePath: 'sources/cap-fiori-showcase' }
};
const mapping = pathMappings[libraryId];
if (!mapping) {
throw new Error(`No source path mapping found for library: ${libraryId}`);
}
const transformedRelFile = mapping.transform ? mapping.transform(relFile) : relFile;
return `${mapping.basePath}/${transformedRelFile}`;
}
/**
* Reads file content from actual source files with graceful fallback
* @param libraryId - The library identifier (e.g., '/cloud-sdk-js')
* @param relFile - The relative file path within the library
* @returns File content as string, or null if file doesn't exist
*/
function readFileContent(libraryId: string, relFile: string): string | null {
const fs = require('fs');
const path = require('path');
try {
const sourceFilePath = getSourceFilePath(libraryId, relFile);
const fullPath = path.resolve(sourceFilePath);
return fs.readFileSync(fullPath, 'utf8');
} catch (error: any) {
console.warn(`Could not read file for ${libraryId}/${relFile}:`, error.message);
// Return null to trigger fallback to test data
return null;
}
}
/**
* Test cases for comprehensive URL generation testing
*
* Each test case defines:
* - name: Human-readable test description
* - libraryId: Library identifier from metadata.json
* - relFile: Relative file path within the library (used for path mapping)
* - expectedUrl: Expected generated URL for validation
* - frontmatter: Fallback YAML frontmatter (used when real file not found)
* - content: Fallback content (used when real file not found)
*
* The system will attempt to read real source files first, falling back to
* the provided frontmatter/content if the file doesn't exist.
*/
const testCases = [
{
name: 'CAP - CDS Log Documentation',
libraryId: '/cap',
relFile: 'node.js/cds-log.md',
expectedUrl: 'https://cap.cloud.sap/docs/#/node.js/cds-log',
frontmatter: '---\nid: cds-log\ntitle: Logging\n---\n',
content: '# Logging\n\nCAP provides structured logging capabilities...'
},
{
name: 'Cloud MTA Build Tool - Download Page',
libraryId: '/cloud-mta-build-tool',
relFile: 'docs/download.md',
expectedUrl: 'https://sap.github.io/cloud-mta-build-tool/download',
frontmatter: '',
content: '\nYou can install the Cloud MTA Build Tool...'
},
{
name: 'Cloud SDK JS - Kubernetes Migration',
libraryId: '/cloud-sdk-js',
relFile: 'environments/migrate-sdk-application-from-btp-cf-to-kubernetes.mdx',
expectedUrl: 'https://sap.github.io/cloud-sdk/docs/js/environments/kubernetes',
frontmatter: '---\nid: kubernetes\ntitle: Migrate your App from SAP BTP CF to Kubernetes\n---\n',
content: '# Migrate a Cloud Foundry Application to a Kubernetes Cluster\n\nThis guide details...'
},
{
name: 'Cloud SDK AI JS - Orchestration',
libraryId: '/cloud-sdk-ai-js',
relFile: 'langchain/orchestration.mdx',
expectedUrl: 'https://sap.github.io/ai-sdk/docs/js/langchain/orchestration',
frontmatter: '---\nid: orchestration\ntitle: Orchestration Integration\n---\n',
content: '# Orchestration Integration\n\nThe @sap-ai-sdk/langchain packages provides...'
},
{
name: 'OpenUI5 API - Button Control',
libraryId: '/openui5-api',
relFile: 'src/sap/m/Button.js',
expectedUrl: 'https://sdk.openui5.org/#/api/sap.m.Button',
frontmatter: '',
content: 'sap.ui.define([\n "./library",\n "sap/ui/core/Control",\n // Button control implementation'
},
{
name: 'OpenUI5 Samples - ButtonWithBadge',
libraryId: '/openui5-samples',
relFile: 'src/sap.m/test/sap/m/demokit/sample/ButtonWithBadge/Component.js',
expectedUrl: 'https://sdk.openui5.org/entity/sap.m.Button/sample/sap.m.sample.ButtonWithBadge',
frontmatter: '',
content: 'sap.ui.define([\n "sap/ui/core/UIComponent"\n], function (UIComponent) {\n // Sample implementation'
},
{
name: 'SAPUI5 - Multi-Selection Navigation',
libraryId: '/sapui5',
relFile: '06_SAP_Fiori_Elements/multi-selection-for-intent-based-navigation-640cabf.md',
expectedUrl: 'https://ui5.sap.com/#/topic/640cabfd35c3469aacf31be28924d50d',
frontmatter: '---\nid: 640cabfd35c3469aacf31be28924d50d\ntopic: 640cabfd35c3469aacf31be28924d50d\ntitle: Multi-Selection for Intent-Based Navigation\n---\n',
content: '# Multi-Selection for Intent-Based Navigation\n\nThis feature allows...'
},
{
name: 'UI5 Tooling - Builder Documentation',
libraryId: '/ui5-tooling',
relFile: 'pages/Builder.md',
expectedUrl: 'https://sap.github.io/ui5-tooling/v4/pages/Builder#ui5-builder',
frontmatter: '',
content: '# UI5 Builder\n\nThe UI5 Builder module takes care of building your project...'
},
{
name: 'UI5 Web Components - Configuration',
libraryId: '/ui5-webcomponents',
relFile: '2-advanced/01-configuration.md',
expectedUrl: 'https://sap.github.io/ui5-webcomponents/docs/01-configuration#configuration',
frontmatter: '',
content: '# Configuration\n\nThis section explains how you can configure UI5 Web Components...'
},
{
name: 'wdi5 - Locators Documentation',
libraryId: '/wdi5',
relFile: 'locators.md',
expectedUrl: 'https://ui5-community.github.io/wdi5/#/locators',
frontmatter: '---\nid: locators\ntitle: Locators\n---\n',
content: '# Locators\n\nwdi5 provides various locators for UI5 controls...'
},
{
name: 'UI5 TypeScript - FAQ Documentation',
libraryId: '/ui5-typescript',
relFile: 'faq.md',
expectedUrl: 'https://github.com/UI5/typescript/blob/gh-pages/faq#faq---frequently-asked-questions-for-the-ui5-type-definitions',
frontmatter: '',
content: '# FAQ - Frequently Asked Questions for the UI5 Type Definitions\n\nWhile the [main page](README.md) answers the high-level questions...'
},
{
name: 'UI5 CC Spreadsheet Importer - Checks Documentation',
libraryId: '/ui5-cc-spreadsheetimporter',
relFile: 'pages/Checks.md',
expectedUrl: 'https://docs.spreadsheet-importer.com/pages/Checks/#error-types',
frontmatter: '',
content: '## Error Types\n\nThe following types of errors are handled by the UI5 Spreadsheet Upload Control...'
},
{
name: 'ABAP Cheat Sheets - Internal Tables',
libraryId: '/abap-cheat-sheets',
relFile: '01_Internal_Tables.md',
expectedUrl: 'https://github.com/SAP-samples/abap-cheat-sheets/blob/main/01_Internal_Tables#internal-tables',
frontmatter: '',
content: '# Internal Tables\n\nThis cheat sheet contains a selection of syntax examples and notes on internal tables...'
},
{
name: 'SAP Style Guides - Clean ABAP',
libraryId: '/sap-styleguides',
relFile: 'clean-abap/CleanABAP.md',
expectedUrl: 'https://github.com/SAP/styleguides/blob/main/CleanABAP#clean-abap',
frontmatter: '',
content: '# Clean ABAP\n\n> [**中文**](CleanABAP_zh.md)\n\nThis style guide presents the essentials of clean ABAP...'
},
{
name: 'DSAG ABAP Leitfaden - Clean Core',
libraryId: '/dsag-abap-leitfaden',
relFile: 'clean-core/what-is-clean-core.md',
expectedUrl: 'https://1dsag.github.io/ABAP-Leitfaden/clean-core/what-is-clean-core/#was-ist-clean-core',
frontmatter: '',
content: '# Was ist Clean Core?\n\nClean Core ist ein Konzept von SAP, das darauf abzielt...'
},
{
name: 'ABAP Platform Fiori Feature Showcase - General Features',
libraryId: '/abap-fiori-showcase',
relFile: '01_general_features.md',
expectedUrl: 'https://github.com/SAP-samples/abap-platform-fiori-feature-showcase/blob/main/01_general_features#general-features',
frontmatter: '',
content: '# General Features\n\nThis section describes the features that are generally used throughout...'
},
{
name: 'CAP Fiori Elements Feature Showcase - README',
libraryId: '/cap-fiori-showcase',
relFile: 'README.md',
expectedUrl: 'https://github.com/SAP-samples/fiori-elements-feature-showcase/blob/main/README#sap-fiori-elements-for-odata-v4-feature-showcase',
frontmatter: '',
content: '# SAP Fiori Elements for OData V4 Feature Showcase\n\nThis app showcases different features of SAP Fiori elements...'
}
// Note: Some sources like CAP, Cloud SDK AI, wdi5, etc. may need different file mappings
// or fallback to mock content if actual files don't exist in expected locations
];
describe('Main URL Generation Function', () => {
testCases.forEach(({ name, libraryId, relFile, expectedUrl, frontmatter, content }) => {
it(`should generate correct URL for ${name}`, () => {
// Step 1: Get configuration from metadata.json
const config = getConfigForLibrary(libraryId);
// Step 2: Try to read from actual source file first, fallback to test data
let fileContent = readFileContent(libraryId, relFile);
let contentSource = 'real file';
if (!fileContent) {
// Fallback to hardcoded test data when real file is not available
fileContent = frontmatter ? `${frontmatter}\n${content}` : content;
contentSource = 'test data';
}
// For debugging: log which content source was used
if (process.env.DEBUG_TESTS === 'true') {
console.log(`\n[${name}] Using ${contentSource}`);
console.log(`File path: ${libraryId}/${relFile}`);
console.log(`Content preview: ${fileContent.slice(0, 100)}...`);
}
// Step 3: Generate URL using the URL generation system
const result = generateDocumentationUrl(libraryId, relFile, fileContent, config);
// Step 4: Validate the result
expect(result).toBe(expectedUrl);
});
});
});
describe('Individual Generator Classes', () => {
describe('CloudSdkUrlGenerator', () => {
it('should generate URLs using frontmatter ID', () => {
const config = getConfigForLibrary('/cloud-sdk-js');
const generator = new CloudSdkUrlGenerator('/cloud-sdk-js', config);
const content = '---\nid: kubernetes\n---\n# Migration Guide';
const result = generator.generateUrl({
libraryId: '/cloud-sdk-js',
relFile: 'environments/migrate.mdx',
content,
config
});
expect(result).toBe('https://sap.github.io/cloud-sdk/docs/js/environments/kubernetes');
});
it('should handle AI SDK variants differently', () => {
const config = getConfigForLibrary('/cloud-sdk-ai-js');
const generator = new CloudSdkUrlGenerator('/cloud-sdk-ai-js', config);
const content = '---\nid: orchestration\n---\n# Orchestration';
const result = generator.generateUrl({
libraryId: '/cloud-sdk-ai-js',
relFile: 'langchain/orchestration.mdx',
content,
config
});
expect(result).toBe('https://sap.github.io/ai-sdk/docs/js/langchain/orchestration');
});
});
describe('SapUi5UrlGenerator', () => {
it('should generate topic-based URLs for SAPUI5', () => {
const config = getConfigForLibrary('/sapui5');
const generator = new SapUi5UrlGenerator('/sapui5', config);
const content = '---\nid: 123e4567-e89b-12d3-a456-426614174000\n---\n# Topic Content';
const result = generator.generateUrl({
libraryId: '/sapui5',
relFile: 'docs/topic.md',
content,
config
});
expect(result).toBe('https://ui5.sap.com/#/topic/123e4567-e89b-12d3-a456-426614174000');
});
it('should generate API URLs for OpenUI5 controls', () => {
const config = getConfigForLibrary('/openui5-api');
const generator = new SapUi5UrlGenerator('/openui5-api', config);
const content = 'sap.ui.define([\n "sap/m/Button"\n], function(Button) {';
const result = generator.generateUrl({
libraryId: '/openui5-api',
relFile: 'src/sap/m/Button.js',
content,
config
});
expect(result).toBe('https://sdk.openui5.org/#/api/sap.m.Button');
});
});
describe('CapUrlGenerator', () => {
it('should generate docsify-style URLs', () => {
const config = getConfigForLibrary('/cap');
const generator = new CapUrlGenerator('/cap', config);
const content = '---\nid: getting-started\n---\n# Getting Started';
const result = generator.generateUrl({
libraryId: '/cap',
relFile: 'guides/getting-started.md',
content,
config
});
expect(result).toBe('https://cap.cloud.sap/docs/#/guides/getting-started');
});
it('should handle CDS-specific sections', () => {
const config = getConfigForLibrary('/cap');
const generator = new CapUrlGenerator('/cap', config);
const content = '---\nslug: cds-types\n---\n# CDS Types';
const result = generator.generateUrl({
libraryId: '/cap',
relFile: 'cds/types.md',
content,
config
});
expect(result).toBe('https://cap.cloud.sap/docs/#/cds/cds-types');
});
});
describe('Wdi5UrlGenerator', () => {
it('should generate docsify-style URLs for wdi5', () => {
const config = getConfigForLibrary('/wdi5');
const generator = new Wdi5UrlGenerator('/wdi5', config);
const content = '---\nid: locators\n---\n# Locators';
const result = generator.generateUrl({
libraryId: '/wdi5',
relFile: 'locators.md',
content,
config
});
expect(result).toBe('https://ui5-community.github.io/wdi5/#/locators');
});
it('should handle configuration-specific sections', () => {
const config = getConfigForLibrary('/wdi5');
const generator = new Wdi5UrlGenerator('/wdi5', config);
const content = '---\nid: basic-config\n---\n# Basic Configuration';
const result = generator.generateUrl({
libraryId: '/wdi5',
relFile: 'configuration/basic.md',
content,
config
});
expect(result).toBe('https://ui5-community.github.io/wdi5/#/configuration/basic-config');
});
});
describe('DsagUrlGenerator', () => {
it('should generate GitHub Pages URLs with path transformation', () => {
const config = getConfigForLibrary('/dsag-abap-leitfaden');
const generator = new DsagUrlGenerator('/dsag-abap-leitfaden', config);
const content = '# Was ist Clean Core?\n\nClean Core ist ein Konzept von SAP...';
const result = generator.generateUrl({
libraryId: '/dsag-abap-leitfaden',
relFile: 'clean-core/what-is-clean-core.md',
content,
config
});
expect(result).toBe('https://1dsag.github.io/ABAP-Leitfaden/clean-core/what-is-clean-core/#was-ist-clean-core');
});
it('should handle root-level documentation', () => {
const config = getConfigForLibrary('/dsag-abap-leitfaden');
const generator = new DsagUrlGenerator('/dsag-abap-leitfaden', config);
const content = '# ABAP Leitfaden\n\nDer DSAG ABAP Leitfaden...';
const result = generator.generateUrl({
libraryId: '/dsag-abap-leitfaden',
relFile: 'README.md',
content,
config
});
expect(result).toBe('https://1dsag.github.io/ABAP-Leitfaden/README/#abap-leitfaden');
});
});
describe('GenericUrlGenerator', () => {
it('should handle generic sources with frontmatter', () => {
const config = getConfigForLibrary('/ui5-tooling'); // Use a real generic source
const generator = new GenericUrlGenerator('/ui5-tooling', config);
const content = '---\nid: test-doc\n---\n# Test Document';
const result = generator.generateUrl({
libraryId: '/ui5-tooling',
relFile: 'pages/test.md',
content,
config
});
expect(result).toBe('https://sap.github.io/ui5-tooling/v4/pages/test-doc#test-document');
});
it('should fallback to filename when no frontmatter', () => {
const config = getConfigForLibrary('/ui5-tooling'); // Use a real generic source
const generator = new GenericUrlGenerator('/ui5-tooling', config);
const content = '# Test Document\n\nSome content...';
const result = generator.generateUrl({
libraryId: '/ui5-tooling',
relFile: 'pages/test.md',
content,
config
});
expect(result).toBe('https://sap.github.io/ui5-tooling/v4/pages/test#test-document');
});
});
describe('AbapUrlGenerator', () => {
it('should generate correct cloud URLs for latest version', () => {
// Mock config for ABAP documentation
const config: DocUrlConfig = {
baseUrl: 'https://help.sap.com/doc/abapdocu_cp_index_htm/CLOUD/en-US',
pathPattern: '/latest/en-US/{filename}',
anchorStyle: 'lowercase-with-dashes'
};
const generator = new AbapUrlGenerator('/abap-docs-latest', config);
const result = generator.generateSourceSpecificUrl({
libraryId: '/abap-docs-latest',
relFile: 'abeninline_declarations.md',
content: '# Inline Declarations',
config
});
expect(result).toBe('https://help.sap.com/doc/abapdocu_cp_index_htm/CLOUD/en-US/abeninline_declarations.html');
});
it('should generate correct cloud URLs for version 9.16', () => {
const config: DocUrlConfig = {
baseUrl: 'https://help.sap.com/doc/abapdocu_cp_index_htm/CLOUD/en-US',
pathPattern: '/9.16/en-US/{filename}',
anchorStyle: 'lowercase-with-dashes'
};
const generator = new AbapUrlGenerator('/abap-docs-916', config);
const result = generator.generateSourceSpecificUrl({
libraryId: '/abap-docs-916',
relFile: 'md/abapselect.md',
content: '# SELECT Statement',
config
});
expect(result).toBe('https://help.sap.com/doc/abapdocu_cp_index_htm/CLOUD/en-US/abapselect.html');
});
it('should generate correct cloud URLs for S/4HANA 2025 version 8.10', () => {
const config: DocUrlConfig = {
baseUrl: 'https://help.sap.com/doc/abapdocu_cp_index_htm/CLOUD/en-US',
pathPattern: '/8.10/en-US/{filename}',
anchorStyle: 'lowercase-with-dashes'
};
const generator = new AbapUrlGenerator('/abap-docs-810', config);
const result = generator.generateSourceSpecificUrl({
libraryId: '/abap-docs-810',
relFile: 'abaploop.md',
content: '# LOOP Statement',
config
});
expect(result).toBe('https://help.sap.com/doc/abapdocu_cp_index_htm/CLOUD/en-US/abaploop.html');
});
it('should generate correct legacy URLs for version 7.58', () => {
const config: DocUrlConfig = {
baseUrl: 'https://help.sap.com/doc/abapdocu_758_index_htm/7.58/en-US',
pathPattern: '/7.58/en-US/{filename}',
anchorStyle: 'lowercase-with-dashes'
};
const generator = new AbapUrlGenerator('/abap-docs-758', config);
const result = generator.generateSourceSpecificUrl({
libraryId: '/abap-docs-758',
relFile: 'md/abapdata.md',
content: '# DATA Statement',
config
});
expect(result).toBe('https://help.sap.com/doc/abapdocu_758_index_htm/7.58/en-US/abapdata.html');
});
it('should handle anchors correctly', () => {
const config: DocUrlConfig = {
baseUrl: 'https://help.sap.com/doc/abapdocu_cp_index_htm/CLOUD/en-US',
pathPattern: '/latest/en-US/{filename}',
anchorStyle: 'lowercase-with-dashes'
};
const generator = new AbapUrlGenerator('/abap-docs-latest', config);
const result = generator.generateSourceSpecificUrl({
libraryId: '/abap-docs-latest',
relFile: 'abeninline_declarations.md',
content: '# Inline Declarations',
config,
anchor: 'syntax'
});
expect(result).toBe('https://help.sap.com/doc/abapdocu_cp_index_htm/CLOUD/en-US/abeninline_declarations.html#syntax');
});
it('should correctly extract version from library ID', () => {
const testCases = [
{ libraryId: '/abap-docs-758', expected: '7.58' },
{ libraryId: '/abap-docs-latest', expected: 'latest' },
{ libraryId: '/abap-docs-916', expected: '9.16' },
{ libraryId: '/abap-docs-810', expected: '8.10' }
];
testCases.forEach(({ libraryId, expected }) => {
const config: DocUrlConfig = {
baseUrl: 'https://example.com',
pathPattern: `/${expected}/en-US/{filename}`,
anchorStyle: 'lowercase-with-dashes'
};
const generator = new AbapUrlGenerator(libraryId, config);
// Test the version extraction by checking the generated URL
const result = generator.generateSourceSpecificUrl({
libraryId,
relFile: 'test.md',
content: '# Test',
config
});
if (expected === 'latest' || parseFloat(expected) >= 9.1 || parseFloat(expected) >= 8.1) {
expect(result).toContain('abapdocu_cp_index_htm/CLOUD');
} else {
const versionCode = expected.replace('.', '');
expect(result).toContain(`abapdocu_${versionCode}_index_htm/${expected}`);
}
});
});
it('should use .html extension instead of .htm for file extension', () => {
const config: DocUrlConfig = {
baseUrl: 'https://help.sap.com/doc/abapdocu_cp_index_htm/CLOUD/en-US',
pathPattern: '/latest/en-US/{filename}',
anchorStyle: 'lowercase-with-dashes'
};
const generator = new AbapUrlGenerator('/abap-docs-latest', config);
const result = generator.generateSourceSpecificUrl({
libraryId: '/abap-docs-latest',
relFile: 'abeninline_declarations.md',
content: '# Inline Declarations',
config
});
// Should use .html file extension (not .htm file extension)
expect(result).toContain('abeninline_declarations.html');
expect(result).toMatch(/\.html$/);
expect(result).not.toMatch(/\.htm$/);
});
it('should point to latest cloud version instead of legacy 7.58 version', () => {
const config: DocUrlConfig = {
baseUrl: 'https://help.sap.com/doc/abapdocu_cp_index_htm/CLOUD/en-US',
pathPattern: '/latest/en-US/{filename}',
anchorStyle: 'lowercase-with-dashes'
};
const generator = new AbapUrlGenerator('/abap-docs-latest', config);
const result = generator.generateSourceSpecificUrl({
libraryId: '/abap-docs-latest',
relFile: 'abeninline_declarations.md',
content: '# Inline Declarations',
config
});
// Should use the new cloud URL pattern instead of the old 7.58 pattern
expect(result).toContain('abapdocu_cp_index_htm/CLOUD');
expect(result).not.toContain('abapdocu_758_index_htm/7.58');
expect(result).not.toContain('abapdocu_latest_index_htm/latest');
// The full URL should match the expected cloud pattern
expect(result).toBe('https://help.sap.com/doc/abapdocu_cp_index_htm/CLOUD/en-US/abeninline_declarations.html');
});
});
});
describe('Error Handling', () => {
it('should return null for missing config', () => {
const result = generateDocumentationUrl('/unknown', 'file.md', 'content', null as any);
expect(result).toBeNull();
});
it('should handle malformed frontmatter gracefully', () => {
// Test with a non-existent library ID that will use the generic generator
const config = getConfigForLibrary('/ui5-tooling'); // Use a real config for fallback testing
const content = '---\ninvalid: yaml: content:\n---\n# Content';
const result = generateDocumentationUrl('/ui5-tooling', 'test.md', content, config);
expect(result).not.toBeNull();
});
});
describe('URL Pattern Validation', () => {
testCases.forEach(({ name, expectedUrl }) => {
it(`should generate valid URL format for ${name}`, () => {
expect(expectedUrl).toMatch(/^https?:\/\//);
expect(() => new URL(expectedUrl)).not.toThrow();
});
});
});
});
```
--------------------------------------------------------------------------------
/src/lib/BaseServerHandler.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Base Server Handler - Shared functionality for MCP servers
* Eliminates code duplication between stdio and HTTP server implementations
*
* IMPORTANT FOR LLMs/AI ASSISTANTS:
* =================================
* The function names in this MCP server may appear with different prefixes depending on your MCP client:
* - Simple names: search, fetch, sap_community_search, sap_help_search, sap_help_get
* - Prefixed names: mcp_sap-docs-remote_search, mcp_sap-docs-remote_fetch, etc.
*
* Try the simple names first, then the prefixed versions if they don't work.
*
* Note: sap_docs_search and sap_docs_get are legacy aliases for backward compatibility.
*/
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import {
ListResourcesRequestSchema,
ReadResourceRequestSchema,
CallToolRequestSchema,
ListToolsRequestSchema,
ListPromptsRequestSchema,
GetPromptRequestSchema
} from "@modelcontextprotocol/sdk/types.js";
import {
searchLibraries,
fetchLibraryDocumentation,
listDocumentationResources,
readDocumentationResource,
searchCommunity
} from "./localDocs.js";
import { searchSapHelp, getSapHelpContent } from "./sapHelp.js";
import { SearchResponse } from "./types.js";
import { logger } from "./logger.js";
import { search } from "./search.js";
import { CONFIG } from "./config.js";
import { loadMetadata, getDocUrlConfig } from "./metadata.js";
import { generateDocumentationUrl, formatSearchResult } from "./url-generation/index.js";
/**
* Helper functions for creating structured JSON responses compatible with ChatGPT and all MCP clients
*/
interface SearchResult {
id: string;
title: string;
url: string;
snippet?: string;
score?: number;
metadata?: Record<string, any>;
}
interface DocumentResult {
id: string;
title: string;
text: string;
url: string;
metadata?: Record<string, any>;
}
/**
* Create structured JSON response for search results (ChatGPT-compatible)
*/
function createSearchResponse(results: SearchResult[]): any {
// Clean the results to avoid JSON serialization issues in MCP protocol
const cleanedResults = results.map(result => ({
// ChatGPT requires: id, title, url (other fields optional)
id: result.id,
title: result.title ? result.title.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '').replace(/\r\n/g, '\n').replace(/\r/g, '\n') : result.title,
url: result.url,
// Additional fields for enhanced functionality
snippet: result.snippet ? result.snippet.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '').replace(/\r\n/g, '\n').replace(/\r/g, '\n') : result.snippet,
score: result.score,
metadata: result.metadata
}));
// ChatGPT expects: { "results": [...] } in JSON-encoded text content
return {
content: [
{
type: "text",
text: JSON.stringify({ results: cleanedResults })
}
]
};
}
/**
* Create structured JSON response for document fetch (ChatGPT-compatible)
*/
function createDocumentResponse(document: DocumentResult): any {
// Clean the text content to avoid JSON serialization issues in MCP protocol
const cleanedDocument = {
// ChatGPT requires: id, title, text, url, metadata
id: document.id,
title: document.title,
text: document.text
.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '') // Remove control chars except \n, \r, \t
.replace(/\r\n/g, '\n') // Normalize line endings
.replace(/\r/g, '\n'), // Convert remaining \r to \n
url: document.url,
metadata: document.metadata
};
// ChatGPT expects document object as JSON-encoded text content
return {
content: [
{
type: "text",
text: JSON.stringify(cleanedDocument)
}
]
};
}
/**
* Create error response in structured JSON format
*/
function createErrorResponse(error: string, requestId?: string): any {
return {
content: [
{
type: "text",
text: JSON.stringify({
error,
requestId: requestId || 'unknown'
})
}
]
};
}
export interface ServerConfig {
name: string;
description: string;
version: string;
}
/**
* Helper function to extract client metadata from request
*/
function extractClientMetadata(request: any): Record<string, any> {
const metadata: Record<string, any> = {};
// Try to extract available metadata from the request
if (request.meta) {
metadata.meta = request.meta;
}
// Extract any client identification from headers or other sources
if (request.headers) {
metadata.headers = request.headers;
}
// Extract transport information if available
if (request.transport) {
metadata.transport = request.transport;
}
// Extract session or connection info
if (request.id) {
metadata.requestId = request.id;
}
return metadata;
}
/**
* Base Server Handler Class
* Provides shared functionality for all MCP server implementations
*/
export class BaseServerHandler {
/**
* Configure server with shared resource and tool handlers
*/
static configureServer(srv: Server): void {
// Only setup resource handlers if resources capability is enabled
// DISABLED: Resources capability causes 60,000+ resources which breaks Cursor
// this.setupResourceHandlers(srv);
this.setupToolHandlers(srv);
const capabilities = (srv as unknown as { _capabilities?: { prompts?: object } })._capabilities;
if (capabilities?.prompts) {
this.setupPromptHandlers(srv);
}
}
/**
* Setup resource handlers (shared between all server types)
*/
private static setupResourceHandlers(srv: Server): void {
// List available resources
srv.setRequestHandler(ListResourcesRequestSchema, async () => {
const resources = await listDocumentationResources();
return { resources };
});
// Read resource contents
srv.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const { uri } = request.params;
try {
return await readDocumentationResource(uri);
} catch (error: any) {
return {
contents: [{
uri,
mimeType: "text/plain",
text: `Error reading resource: ${error.message}`
}]
};
}
});
}
/**
* Setup tool handlers (shared between all server types)
*/
private static setupToolHandlers(srv: Server): void {
// List available tools
srv.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "sap_community_search",
description: `SEARCH SAP COMMUNITY: sap_community_search(query="search terms")
FUNCTION NAME: sap_community_search (or mcp_sap-docs-remote_sap_community_search)
FINDS: Blog posts, discussions, solutions from SAP Community
INCLUDES: Engagement data (kudos), ranked by "Best Match"
TYPICAL WORKFLOW:
1. sap_community_search(query="your problem + error code")
2. fetch(id="community-12345") for full posts
BEST FOR TROUBLESHOOTING:
• Include error codes: "415 error", "500 error"
• Be specific: "CAP action binary upload 415"
• Use real scenarios: "wizard implementation issues"`,
inputSchema: {
type: "object",
properties: {
query: {
type: "string",
description: "Search terms for SAP Community. Include error codes and specific technical details.",
examples: [
"CAP action parameter binary file upload 415 error",
"wizard implementation best practices",
"fiori elements authentication",
"UI5 deployment issues",
"wdi5 test automation problems"
]
}
},
required: ["query"]
}
},
{
name: "sap_help_search",
description: `SEARCH SAP HELP PORTAL: sap_help_search(query="product + topic")
FUNCTION NAME: sap_help_search (or mcp_sap-docs-remote_sap_help_search)
SEARCHES: Official SAP Help Portal (help.sap.com)
COVERS: Product guides, implementation guides, technical documentation
TYPICAL WORKFLOW:
1. sap_help_search(query="product name + configuration topic")
2. sap_help_get(result_id="sap-help-12345abc")
BEST PRACTICES:
• Include product names: "S/4HANA", "BTP", "Fiori"
• Add specific tasks: "configuration", "setup", "deployment"
• Use official SAP terminology`,
inputSchema: {
type: "object",
properties: {
query: {
type: "string",
description: "Search terms for SAP Help Portal. Include product names and specific topics.",
examples: [
"S/4HANA configuration",
"Fiori Launchpad setup",
"BTP integration",
"ABAP development guide",
"SAP Analytics Cloud setup"
]
}
},
required: ["query"]
}
},
{
name: "sap_help_get",
description: `GET SAP HELP PAGE: sap_help_get(result_id="sap-help-12345abc")
FUNCTION NAME: sap_help_get (or mcp_sap-docs-remote_sap_help_get)
RETRIEVES: Complete SAP Help Portal page content
REQUIRES: Exact result_id from sap_help_search
USAGE PATTERN:
1. Get ID from sap_help_search results
2. Use exact ID (don't modify the format)
3. Receive full page content + metadata`,
inputSchema: {
type: "object",
properties: {
result_id: {
type: "string",
description: "Exact ID from sap_help_search results. Copy the ID exactly as returned.",
examples: [
"sap-help-12345abc",
"sap-help-98765def"
]
}
},
required: ["result_id"]
}
},
{
name: "search",
description: `SEARCH SAP DOCS: search(query="search terms")
FUNCTION NAME: search
COVERS: ABAP (all versions), UI5, CAP, wdi5, OpenUI5 APIs, Cloud SDK
AUTO-DETECTS: ABAP versions from query (e.g. "LOOP 7.57", defaults to 7.58)
TYPICAL WORKFLOW:
1. search(query="your search terms")
2. fetch(id="result_id_from_step_1")
QUERY TIPS:
• Be specific: "CAP action binary parameter" not just "CAP"
• Include error codes: "415 error CAP action"
• Use technical terms: "LargeBinary MediaType XMLHttpRequest"
• For ABAP: Include version like "7.58" or "latest"`,
inputSchema: {
type: "object",
properties: {
query: {
type: "string",
description: "Search terms using natural language. Be specific and include technical terms.",
examples: [
"CAP binary data LargeBinary MediaType",
"UI5 button properties",
"wdi5 testing locators",
"ABAP SELECT statements 7.58",
"415 error CAP action parameter"
]
}
},
required: ["query"]
}
},
{
name: "fetch",
description: `GET SPECIFIC DOCS: fetch(id="result_id")
FUNCTION NAME: fetch
RETRIEVES: Full content from search results
WORKS WITH: Document IDs returned by search
ChatGPT COMPATIBLE:
• Uses "id" parameter (required by ChatGPT)
• Returns structured JSON content
• Includes full document text and metadata`,
inputSchema: {
type: "object",
properties: {
id: {
type: "string",
description: "Unique document ID from search results. Use exact IDs returned by search.",
examples: [
"/cap/guides/domain-modeling",
"/sapui5/controls/button-properties",
"/openui5-api/sap/m/Button",
"/abap-docs-758/inline-declarations",
"community-12345"
]
}
},
required: ["id"]
}
},
]
};
});
// Handle tool execution
srv.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
const clientMetadata = extractClientMetadata(request);
if (name === "sap_docs_search" || name === "search") {
const { query } = args as { query: string };
// Enhanced logging with timing
const timing = logger.logToolStart(name, query, clientMetadata);
try {
// Use hybrid search with reranking
const results = await search(query, {
k: CONFIG.RETURN_K
});
const topResults = results;
if (topResults.length === 0) {
logger.logToolSuccess(name, timing.requestId, timing.startTime, 0, { fallback: false });
return createErrorResponse(
`No results for "${query}". Try UI5 controls ("button", "table"), CAP topics ("actions", "binary"), testing ("wdi5", "e2e"), ABAP with versions ("SELECT 7.58"), or include error codes ("415 error").`,
timing.requestId
);
}
// Transform results to ChatGPT-compatible format with id, title, url
const searchResults: SearchResult[] = topResults.map((r, index) => {
// Extract library_id and topic from document ID
const libraryIdMatch = r.id.match(/^(\/[^\/]+)/);
const libraryId = libraryIdMatch ? libraryIdMatch[1] : (r.sourceId ? `/${r.sourceId}` : r.id);
const topic = r.id.startsWith(libraryId) ? r.id.slice(libraryId.length + 1) : '';
const config = getDocUrlConfig(libraryId);
const docUrl = config ? generateDocumentationUrl(libraryId, '', r.text, config) : null;
return {
// ChatGPT-required format: id, title, url
id: r.id, // Use full document ID as required by ChatGPT
title: r.text.split('\n')[0] || r.id,
url: docUrl || `#${r.id}`,
// Additional fields for backward compatibility
library_id: libraryId,
topic: topic,
snippet: r.text ? r.text.substring(0, CONFIG.EXCERPT_LENGTH_MAIN) + '...' : '',
score: r.finalScore,
metadata: {
source: r.sourceId || 'sap-docs',
library: libraryId,
bm25Score: r.bm25,
rank: index + 1
}
};
});
logger.logToolSuccess(name, timing.requestId, timing.startTime, topResults.length, { fallback: false });
return createSearchResponse(searchResults);
} catch (error) {
logger.logToolError(name, timing.requestId, timing.startTime, error, false);
logger.info('Attempting fallback to original search after hybrid search failure');
// Fallback to original search
try {
const res: SearchResponse = await searchLibraries(query);
if (!res.results.length) {
logger.logToolSuccess(name, timing.requestId, timing.startTime, 0, { fallback: true });
return createErrorResponse(
res.error || `No fallback results for "${query}". Try UI5 controls ("button", "table"), CAP topics ("actions", "binary"), testing ("wdi5", "e2e"), ABAP with versions ("SELECT 7.58"), or include error codes.`,
timing.requestId
);
}
// Transform fallback results to structured format
const fallbackResults: SearchResult[] = res.results.map((r, index) => ({
id: r.id || `fallback-${index}`,
title: r.title || 'SAP Documentation',
url: r.url || `#${r.id}`,
snippet: r.description ? r.description.substring(0, 200) + '...' : '',
metadata: {
source: 'fallback-search',
rank: index + 1
}
}));
logger.logToolSuccess(name, timing.requestId, timing.startTime, res.results.length, { fallback: true });
return createSearchResponse(fallbackResults);
} catch (fallbackError) {
logger.logToolError(name, timing.requestId, timing.startTime, fallbackError, true);
return createErrorResponse(
`Search temporarily unavailable. Wait 30 seconds and retry, try sap_community_search instead, or use more specific search terms.`,
timing.requestId
);
}
}
}
if (name === "sap_community_search") {
const { query } = args as { query: string };
// Enhanced logging with timing
const timing = logger.logToolStart(name, query, clientMetadata);
try {
const res: SearchResponse = await searchCommunity(query);
if (!res.results.length) {
logger.logToolSuccess(name, timing.requestId, timing.startTime, 0);
return createErrorResponse(
res.error || `No SAP Community posts found for "${query}". Try different keywords or check your connection.`,
timing.requestId
);
}
// Transform community search results to ChatGPT-compatible format
const communityResults: SearchResult[] = res.results.map((r: any, index) => ({
// ChatGPT-required format: id, title, url
id: r.id || `community-${index}`,
title: r.title || 'SAP Community Post',
url: r.url || `#${r.id}`,
// Additional fields for enhanced functionality
library_id: r.library_id || `community-${index}`,
topic: r.topic || '',
snippet: r.snippet || (r.description ? r.description.substring(0, 200) + '...' : ''),
score: r.score || 0,
metadata: r.metadata || {
source: 'sap-community',
likes: r.likes,
author: r.author,
postTime: r.postTime,
rank: index + 1
}
}));
logger.logToolSuccess(name, timing.requestId, timing.startTime, res.results.length);
return createSearchResponse(communityResults);
} catch (error) {
logger.logToolError(name, timing.requestId, timing.startTime, error);
return createErrorResponse(
`SAP Community search service temporarily unavailable. Please try again later.`,
timing.requestId
);
}
}
if (name === "sap_docs_get" || name === "fetch") {
// Handle both old format (library_id) and new ChatGPT format (id)
const library_id = (args as any).library_id || (args as any).id;
const topic = (args as any).topic || "";
if (!library_id) {
const timing = logger.logToolStart(name, 'missing_id', clientMetadata);
logger.logToolError(name, timing.requestId, timing.startTime, new Error('Missing id parameter'));
return createErrorResponse(
`Missing required parameter: id. Please provide a document ID from search results.`,
timing.requestId
);
}
// Enhanced logging with timing
const searchKey = library_id + (topic ? `/${topic}` : '');
const timing = logger.logToolStart(name, searchKey, clientMetadata);
try {
const text = await fetchLibraryDocumentation(library_id, topic);
if (!text) {
logger.logToolSuccess(name, timing.requestId, timing.startTime, 0);
return createErrorResponse(
`Nothing found for ${library_id}`,
timing.requestId
);
}
// Transform document content to ChatGPT-compatible format
const config = getDocUrlConfig(library_id);
const docUrl = config ? generateDocumentationUrl(library_id, '', text, config) : null;
const document: DocumentResult = {
id: library_id,
title: library_id.replace(/^\//, '').replace(/\//g, ' > ') + (topic ? ` (${topic})` : ''),
text: text,
url: docUrl || `#${library_id}`,
metadata: {
source: 'sap-docs',
library: library_id,
topic: topic || undefined,
contentLength: text.length
}
};
logger.logToolSuccess(name, timing.requestId, timing.startTime, 1, {
contentLength: text.length,
libraryId: library_id,
topic: topic || undefined
});
return createDocumentResponse(document);
} catch (error) {
logger.logToolError(name, timing.requestId, timing.startTime, error);
return createErrorResponse(
`Error retrieving documentation for ${library_id}. Please try again later.`,
timing.requestId
);
}
}
if (name === "sap_help_search") {
const { query } = args as { query: string };
// Enhanced logging with timing
const timing = logger.logToolStart(name, query, clientMetadata);
try {
const res: SearchResponse = await searchSapHelp(query);
if (!res.results.length) {
logger.logToolSuccess(name, timing.requestId, timing.startTime, 0);
return createErrorResponse(
res.error || `No SAP Help results found for "${query}". Try different keywords or check your connection.`,
timing.requestId
);
}
// Transform SAP Help search results to ChatGPT-compatible format
const helpResults: SearchResult[] = res.results.map((r, index) => ({
// ChatGPT-required format: id, title, url
id: r.id || `sap-help-${index}`,
title: r.title || 'SAP Help Document',
url: r.url || `#${r.id}`,
// Additional fields for enhanced functionality
snippet: r.description ? r.description.substring(0, 200) + '...' : '',
metadata: {
source: 'sap-help',
totalSnippets: r.totalSnippets,
rank: index + 1
}
}));
logger.logToolSuccess(name, timing.requestId, timing.startTime, res.results.length);
return createSearchResponse(helpResults);
} catch (error) {
logger.logToolError(name, timing.requestId, timing.startTime, error);
return createErrorResponse(
`SAP Help search service temporarily unavailable. Please try again later.`,
timing.requestId
);
}
}
if (name === "sap_help_get") {
const { result_id } = args as { result_id: string };
// Enhanced logging with timing
const timing = logger.logToolStart(name, result_id, clientMetadata);
try {
const content = await getSapHelpContent(result_id);
// Transform SAP Help content to structured format
const document: DocumentResult = {
id: result_id,
title: `SAP Help Document (${result_id})`,
text: content,
url: `https://help.sap.com/#${result_id}`,
metadata: {
source: 'sap-help',
resultId: result_id,
contentLength: content.length
}
};
logger.logToolSuccess(name, timing.requestId, timing.startTime, 1, {
contentLength: content.length,
resultId: result_id
});
return createDocumentResponse(document);
} catch (error) {
logger.logToolError(name, timing.requestId, timing.startTime, error);
return createErrorResponse(
`Error retrieving SAP Help content. Please try again later.`,
timing.requestId
);
}
}
throw new Error(`Unknown tool: ${name}`);
});
}
/**
* Setup prompt handlers (shared between all server types)
*/
private static setupPromptHandlers(srv: Server): void {
// List available prompts
srv.setRequestHandler(ListPromptsRequestSchema, async () => {
return {
prompts: [
{
name: "sap_search_help",
title: "SAP Documentation Search Helper",
description: "Helps users construct effective search queries for SAP documentation",
arguments: [
{
name: "domain",
description: "SAP domain (UI5, CAP, ABAP, etc.)",
required: false
},
{
name: "context",
description: "Specific context or technology area",
required: false
}
]
},
{
name: "sap_troubleshoot",
title: "SAP Issue Troubleshooting Guide",
description: "Guides users through troubleshooting common SAP development issues",
arguments: [
{
name: "error_message",
description: "Error message or symptom description",
required: false
},
{
name: "technology",
description: "SAP technology stack (UI5, CAP, ABAP, etc.)",
required: false
}
]
}
]
};
});
// Get specific prompt
srv.setRequestHandler(GetPromptRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
switch (name) {
case "sap_search_help":
const domain = args?.domain || "general SAP";
const context = args?.context || "development";
return {
description: `Search helper for ${domain} documentation`,
messages: [
{
role: "user",
content: {
type: "text",
text: `I need help searching ${domain} documentation for ${context}. What search terms should I use to find the most relevant results?
Here are some tips for effective SAP documentation searches:
**For UI5/Frontend:**
- Include specific control names (e.g., "Table", "Button", "ObjectPage")
- Mention UI5 version if relevant
- Use terms like "properties", "events", "aggregations"
**For CAP/Backend:**
- Include CDS concepts (e.g., "entity", "service", "annotation")
- Mention specific features (e.g., "authentication", "authorization", "events")
- Use terms like "deployment", "configuration"
**For ABAP:**
- Include version number (e.g., "7.58", "latest")
- Use specific statement types (e.g., "SELECT", "LOOP", "MODIFY")
- Include object types (e.g., "class", "method", "interface")
**General Tips:**
- Be specific rather than broad
- Include error codes if troubleshooting
- Use technical terms rather than business descriptions
- Combine multiple related terms
What specific topic are you looking for help with?`
}
}
]
};
case "sap_troubleshoot":
const errorMessage = args?.error_message || "an issue";
const technology = args?.technology || "SAP";
return {
description: `Troubleshooting guide for ${technology}`,
messages: [
{
role: "user",
content: {
type: "text",
text: `I'm experiencing ${errorMessage} with ${technology}. Let me help you troubleshoot this systematically.
**Step 1: Information Gathering**
- What is the exact error message or symptom?
- When does this occur (during development, runtime, deployment)?
- What were you trying to accomplish?
- What technology stack are you using?
**Step 2: Initial Search Strategy**
Let me search the SAP documentation for similar issues:
**For UI5 Issues:**
- Search for the exact error message
- Include control or component names
- Look for browser console errors
**For CAP Issues:**
- Check service definitions and annotations
- Look for deployment configuration
- Verify database connections
**For ABAP Issues:**
- Include ABAP version in search
- Look for syntax or runtime errors
- Check object dependencies
**Step 3: Common Solutions**
Based on the issue type, I'll search for:
- Official SAP documentation
- Community discussions
- Code examples and samples
Please provide more details about your specific issue, and I'll search for relevant solutions.`
}
}
]
};
default:
throw new Error(`Unknown prompt: ${name}`);
}
});
}
/**
* Initialize metadata system (shared initialization logic)
*/
static initializeMetadata(): void {
logger.info('Initializing BM25 search system...');
try {
loadMetadata();
logger.info('Search system ready with metadata');
} catch (error) {
logger.warn('Metadata loading failed, using defaults', { error: String(error) });
logger.info('Search system ready');
}
}
}
```
--------------------------------------------------------------------------------
/scripts/build-index.ts:
--------------------------------------------------------------------------------
```typescript
// Build pipeline step 1: Creates dist/data/index.json (bundle of all docs from submodules)
import fg from "fast-glob";
import fs from "fs/promises";
import path, { join } from "path";
import matter from "gray-matter";
interface DocEntry {
id: string; // "/sapui5/<rel-path>", "/cap/<rel-path>", "/openui5-api/<rel-path>", or "/openui5-samples/<rel-path>"
title: string;
description: string;
snippetCount: number;
relFile: string; // path relative to sources/…
type?: "markdown" | "jsdoc" | "sample" | "markdown-section"; // type of documentation
controlName?: string; // extracted UI5 control name (e.g., "Wizard", "Button")
namespace?: string; // UI5 namespace (e.g., "sap.m", "sap.f")
keywords?: string[]; // searchable keywords and tags
properties?: string[]; // control properties for API docs
events?: string[]; // control events for API docs
aggregations?: string[]; // control aggregations for API docs
parentDocument?: string; // for sections, the ID of the parent document
sectionStartLine?: number; // for sections, the line number where the section starts
headingLevel?: number; // for sections, the heading level (2=##, 3=###, 4=####)
}
interface LibraryBundle {
id: string; // "/sapui5" | "/cap" | "/openui5-api" | "/openui5-samples"
name: string; // "SAPUI5", "CAP", "OpenUI5 API", "OpenUI5 Samples"
description: string;
docs: DocEntry[];
}
interface SourceConfig {
repoName: string;
absDir: string;
id: string;
name: string;
description: string;
filePattern: string;
exclude?: string;
type: "markdown" | "jsdoc" | "sample";
}
const SOURCES: SourceConfig[] = [
{
repoName: "sapui5-docs",
absDir: join("sources", "sapui5-docs", "docs"),
id: "/sapui5",
name: "SAPUI5",
description: "Official SAPUI5 Markdown documentation",
filePattern: "**/*.md",
type: "markdown" as const
},
{
repoName: "cap-docs",
absDir: join("sources", "cap-docs"),
id: "/cap",
name: "SAP Cloud Application Programming Model (CAP)",
description: "CAP (Capire) reference & guides",
filePattern: "**/*.md",
type: "markdown" as const
},
{
repoName: "openui5",
absDir: join("sources", "openui5", "src"),
id: "/openui5-api",
name: "OpenUI5 API",
description: "OpenUI5 Control API documentation and JSDoc",
filePattern: "**/src/**/*.js",
exclude: "**/test/**/*",
type: "jsdoc" as const
},
{
repoName: "openui5",
absDir: join("sources", "openui5", "src"),
id: "/openui5-samples",
name: "OpenUI5 Samples",
description: "OpenUI5 demokit sample applications and code examples",
filePattern: "**/demokit/sample/**/*.{js,xml,json,html}",
type: "sample" as const
},
{
repoName: "wdi5",
absDir: join("sources", "wdi5", "docs"),
id: "/wdi5",
name: "wdi5",
description: "wdi5 end-to-end test framework documentation",
filePattern: "**/*.md",
type: "markdown" as const
},
{
repoName: "ui5-tooling",
absDir: join("sources", "ui5-tooling", "docs"),
id: "/ui5-tooling",
name: "UI5 Tooling ",
description: "UI5 Tooling documentation",
filePattern: "**/*.md",
type: "markdown" as const
},
{
repoName: "cloud-mta-build-tool",
absDir: join("sources", "cloud-mta-build-tool", "docs", "docs"),
id: "/cloud-mta-build-tool",
name: "Cloud MTA Build Tool",
description: "Cloud MTA Build Tool documentation",
filePattern: "**/*.md",
type: "markdown" as const
},
{
repoName: "ui5-webcomponents",
absDir: join("sources", "ui5-webcomponents", "docs"),
id: "/ui5-webcomponents",
name: "UI5 Web Components",
description: "UI5 Web Components documentation",
filePattern: "**/*.md",
type: "markdown" as const
},
{
repoName: "cloud-sdk",
absDir: join("sources", "cloud-sdk", "docs-js"),
id: "/cloud-sdk-js",
name: "Cloud SDK (JavaScript)",
description: "Cloud SDK (JavaScript) documentation",
filePattern: "**/*.mdx",
type: "markdown" as const
},
{
repoName: "cloud-sdk",
absDir: join("sources", "cloud-sdk", "docs-java"),
id: "/cloud-sdk-java",
name: "Cloud SDK (Java)",
description: "Cloud SDK (Java) documentation",
filePattern: "**/*.mdx",
type: "markdown" as const
},
{
repoName: "cloud-sdk-ai",
absDir: join("sources", "cloud-sdk-ai", "docs-js"),
id: "/cloud-sdk-ai-js",
name: "Cloud SDK AI (JavaScript)",
description: "Cloud SDK AI (JavaScript) documentation",
filePattern: "**/*.mdx",
type: "markdown" as const
},
{
repoName: "cloud-sdk-ai",
absDir: join("sources", "cloud-sdk-ai", "docs-java"),
id: "/cloud-sdk-ai-java",
name: "Cloud SDK AI (Java)",
description: "Cloud SDK AI (Java) documentation",
filePattern: "**/*.mdx",
type: "markdown" as const
},
{
repoName: "ui5-typescript",
absDir: join("sources", "ui5-typescript"),
id: "/ui5-typescript",
name: "UI5 TypeScript",
description: "Official entry point to anything TypeScript related for UI5",
filePattern: "*.md",
type: "markdown" as const
},
{
repoName: "ui5-cc-spreadsheetimporter",
absDir: join("sources", "ui5-cc-spreadsheetimporter", "docs"),
id: "/ui5-cc-spreadsheetimporter",
name: "UI5 CC Spreadsheet Importer",
description: "UI5 Custom Control for importing spreadsheet data",
filePattern: "**/*.md",
type: "markdown" as const
},
{
repoName: "abap-cheat-sheets",
absDir: join("sources", "abap-cheat-sheets"),
id: "/abap-cheat-sheets",
name: "ABAP Cheat Sheets",
description: "Comprehensive ABAP syntax examples and cheat sheets",
filePattern: "*.md",
type: "markdown" as const
},
{
repoName: "sap-styleguides",
absDir: join("sources", "sap-styleguides"),
id: "/sap-styleguides",
name: "SAP Style Guides",
description: "SAP coding style guides and best practices including Clean ABAP",
filePattern: "**/*.md",
type: "markdown" as const
},
{
repoName: "dsag-abap-leitfaden",
absDir: join("sources", "dsag-abap-leitfaden", "docs"),
id: "/dsag-abap-leitfaden",
name: "DSAG ABAP Leitfaden",
description: "German ABAP guidelines and best practices by DSAG",
filePattern: "**/*.md",
type: "markdown" as const
},
{
repoName: "abap-fiori-showcase",
absDir: join("sources", "abap-fiori-showcase"),
id: "/abap-fiori-showcase",
name: "ABAP Platform Fiori Feature Showcase",
description: "Annotation-driven SAP Fiori Elements features for OData V4 using ABAP RAP",
filePattern: "*.md",
type: "markdown" as const
},
{
repoName: "cap-fiori-showcase",
absDir: join("sources", "cap-fiori-showcase"),
id: "/cap-fiori-showcase",
name: "CAP Fiori Elements Feature Showcase",
description: "SAP Fiori Elements features and annotations showcase using CAP",
filePattern: "*.md",
type: "markdown" as const
},
{
repoName: "abap-docs",
absDir: join("sources", "abap-docs", "docs", "7.58", "md"),
id: "/abap-docs-758",
name: "ABAP Keyword Documentation (7.58)",
description: "Official ABAP language reference and syntax documentation (version 7.58) - individual files optimized for LLM consumption",
filePattern: "*.md",
type: "markdown" as const
},
{
repoName: "abap-docs",
absDir: join("sources", "abap-docs", "docs", "7.57", "md"),
id: "/abap-docs-757",
name: "ABAP Keyword Documentation (7.57)",
description: "Official ABAP language reference and syntax documentation (version 7.57) - individual files optimized for LLM consumption",
filePattern: "*.md",
type: "markdown" as const
},
{
repoName: "abap-docs",
absDir: join("sources", "abap-docs", "docs", "7.56", "md"),
id: "/abap-docs-756",
name: "ABAP Keyword Documentation (7.56)",
description: "Official ABAP language reference and syntax documentation (version 7.56) - individual files optimized for LLM consumption",
filePattern: "*.md",
type: "markdown" as const
},
{
repoName: "abap-docs",
absDir: join("sources", "abap-docs", "docs", "7.55", "md"),
id: "/abap-docs-755",
name: "ABAP Keyword Documentation (7.55)",
description: "Official ABAP language reference and syntax documentation (version 7.55) - individual files optimized for LLM consumption",
filePattern: "*.md",
type: "markdown" as const
},
{
repoName: "abap-docs",
absDir: join("sources", "abap-docs", "docs", "7.54", "md"),
id: "/abap-docs-754",
name: "ABAP Keyword Documentation (7.54)",
description: "Official ABAP language reference and syntax documentation (version 7.54) - individual files optimized for LLM consumption",
filePattern: "*.md",
type: "markdown" as const
},
{
repoName: "abap-docs",
absDir: join("sources", "abap-docs", "docs", "7.53", "md"),
id: "/abap-docs-753",
name: "ABAP Keyword Documentation (7.53)",
description: "Official ABAP language reference and syntax documentation (version 7.53) - individual files optimized for LLM consumption",
filePattern: "*.md",
type: "markdown" as const
},
{
repoName: "abap-docs",
absDir: join("sources", "abap-docs", "docs", "7.52", "md"),
id: "/abap-docs-752",
name: "ABAP Keyword Documentation (7.52)",
description: "Official ABAP language reference and syntax documentation (version 7.52) - individual files optimized for LLM consumption",
filePattern: "*.md",
type: "markdown" as const
},
{
repoName: "abap-docs",
absDir: join("sources", "abap-docs", "docs", "latest", "md"),
id: "/abap-docs-latest",
name: "ABAP Keyword Documentation (Latest)",
description: "Official ABAP language reference and syntax documentation (latest version) - individual files optimized for LLM consumption",
filePattern: "*.md",
type: "markdown" as const
}
];
// Extract meaningful content from ABAP documentation files
function extractAbapContent(content: string, filename: string): { title: string; description: string; snippetCount: number } {
const lines = content.split(/\r?\n/);
// Skip attribution header (first few lines with "📖 Official SAP Documentation")
let contentStart = 0;
for (let i = 0; i < lines.length; i++) {
if (lines[i].includes('📖 Official SAP Documentation') || lines[i].startsWith('> **📖')) {
// Skip until we find the actual content (after attribution and separators)
for (let j = i; j < lines.length; j++) {
if (lines[j].trim() === '' || lines[j].includes('* * *') || lines[j].includes('---')) {
continue;
}
if (!lines[j].startsWith('>')) {
contentStart = j;
break;
}
}
break;
}
}
// Find the actual title (first non-metadata heading)
let title = filename.replace('.md', '').replace('aben', '');
for (let i = contentStart; i < lines.length; i++) {
const line = lines[i].trim();
if (line && !line.startsWith('AS ABAP Release') && !line.startsWith('[ABAP -') && !line.startsWith('[![') && !line.includes('Mail Feedback')) {
if (line.match(/^[A-Z][a-zA-Z\s]+$/)) {
// Found a proper title (like "Inline Declarations")
title = line;
contentStart = i + 1;
break;
}
}
}
// Extract meaningful description from content
const contentLines = lines.slice(contentStart);
const meaningfulLines = [];
for (const line of contentLines) {
const trimmed = line.trim();
// Skip empty lines, separators, and navigation
if (!trimmed || trimmed === '---' || trimmed === '* * *' || trimmed.startsWith('[ABAP -') || trimmed.includes('Mail Feedback')) {
continue;
}
// Skip metadata lines
if (trimmed.startsWith('AS ABAP Release') || trimmed.includes('©Copyright')) {
continue;
}
// Stop at "Continue" or "Programming Guideline" sections
if (trimmed.startsWith('Continue') || trimmed.startsWith('Programming Guideline')) {
break;
}
meaningfulLines.push(trimmed);
// Stop when we have enough content for a good description
if (meaningfulLines.join(' ').length > 300) {
break;
}
}
// Build description from meaningful content
let description = meaningfulLines.join(' ').trim();
// If description is too short, add version info
if (description.length < 50) {
const versionMatch = filename.match(/abap-docs-(\d+)/);
const version = versionMatch ? versionMatch[1] : '7.58';
description = `${title} - ABAP ${version} language reference`;
}
// Extract ABAP-specific terms for better searchability
const abapTerms: string[] = [];
const descriptionLower = description.toLowerCase();
// Common ABAP statement keywords
const statements = ['data', 'final', 'field-symbol', 'select', 'loop', 'if', 'try', 'catch', 'class', 'method'];
statements.forEach(stmt => {
if (descriptionLower.includes(stmt)) {
abapTerms.push(stmt);
}
});
// Add statement context if found
if (abapTerms.length > 0) {
description += ` | Statements: ${abapTerms.join(', ')}`;
}
// Count code snippets (ABAP typically has fewer but more meaningful ones)
const snippetCount = (content.match(/```/g)?.length || 0) / 2;
return {
title,
description: description.substring(0, 400), // Allow longer descriptions for ABAP
snippetCount
};
}
// Extract information from sample files (JS, XML, JSON, HTML)
function extractSampleInfo(content: string, filePath: string) {
const fileName = path.basename(filePath);
const fileExt = path.extname(filePath);
const sampleDir = path.dirname(filePath);
// Extract control name from the path (e.g., "Button", "Wizard", "Table")
const pathParts = sampleDir.split('/');
const sampleIndex = pathParts.findIndex(part => part === 'sample');
const controlName = sampleIndex >= 0 && sampleIndex < pathParts.length - 1
? pathParts[sampleIndex + 1]
: path.basename(sampleDir);
let title = `${controlName} Sample - ${fileName}`;
let description = `Sample implementation of ${controlName} control`;
let snippetCount = 0;
// Extract specific information based on file type
if (fileExt === '.js') {
// JavaScript sample files
const jsContent = content.toLowerCase();
// Look for common UI5 patterns
if (jsContent.includes('controller')) {
title = `${controlName} Sample Controller`;
description = `Controller implementation for ${controlName} sample`;
} else if (jsContent.includes('component')) {
title = `${controlName} Sample Component`;
description = `Component definition for ${controlName} sample`;
}
// Count meaningful code patterns
const codePatterns = [
/function\s*\(/g,
/onPress\s*:/g,
/on[A-Z][a-zA-Z]*\s*:/g,
/\.attach[A-Z][a-zA-Z]*/g,
/new\s+sap\./g
];
snippetCount = codePatterns.reduce((count, pattern) => {
return count + (content.match(pattern)?.length || 0);
}, 0);
} else if (fileExt === '.xml') {
// XML view files
title = `${controlName} Sample View`;
description = `XML view implementation for ${controlName} sample`;
// Count XML controls and bindings
const xmlPatterns = [
/<[a-zA-Z][^>]*>/g,
/\{[^}]+\}/g, // bindings
/press=/g,
/text=/g
];
snippetCount = xmlPatterns.reduce((count, pattern) => {
return count + (content.match(pattern)?.length || 0);
}, 0);
} else if (fileExt === '.json') {
// Manifest or model files
if (fileName.includes('manifest')) {
title = `${controlName} Sample Manifest`;
description = `Application manifest for ${controlName} sample`;
} else {
title = `${controlName} Sample Data`;
description = `Sample data model for ${controlName} control`;
}
try {
const jsonObj = JSON.parse(content);
snippetCount = Object.keys(jsonObj).length;
} catch {
snippetCount = 1;
}
} else if (fileExt === '.html') {
// HTML files
title = `${controlName} Sample HTML`;
description = `HTML page for ${controlName} sample`;
const htmlPatterns = [
/<script[^>]*>/g,
/<div[^>]*>/g,
/data-sap-ui-/g
];
snippetCount = htmlPatterns.reduce((count, pattern) => {
return count + (content.match(pattern)?.length || 0);
}, 0);
}
// Add library information from path
const libraryMatch = filePath.match(/src\/([^\/]+)\/test/);
if (libraryMatch) {
const library = libraryMatch[1];
description += ` (${library} library)`;
}
return {
title,
description,
snippetCount: Math.max(1, snippetCount) // Ensure at least 1
};
}
// Extract JSDoc information from JavaScript files with enhanced metadata
function extractJSDocInfo(content: string, fileName: string) {
const lines = content.split(/\r?\n/);
// Try to find the main class/control definition
const classMatch = content.match(/\.extend\s*\(\s*["']([^"']+)["']/);
const fullControlName = classMatch ? classMatch[1] : path.basename(fileName, ".js");
// Extract namespace and control name
const namespaceMatch = fullControlName.match(/^(sap\.[^.]+)\.(.*)/);
const namespace = namespaceMatch ? namespaceMatch[1] : '';
const controlName = namespaceMatch ? namespaceMatch[2] : fullControlName;
// Extract main class JSDoc comment
const jsdocMatch = content.match(/\/\*\*\s*([\s\S]*?)\*\//);
let description = "";
if (jsdocMatch) {
// Clean up JSDoc comment and extract description
const jsdocContent = jsdocMatch[1]
.split('\n')
.map(line => line.replace(/^\s*\*\s?/, ''))
.join('\n')
.trim();
// Extract the main description (everything before @tags)
const firstAtIndex = jsdocContent.indexOf('@');
description = firstAtIndex > -1
? jsdocContent.substring(0, firstAtIndex).trim()
: jsdocContent;
// Clean up common JSDoc patterns
description = description
.replace(/^\s*Constructor for a new.*$/m, '')
.replace(/^\s*@param.*$/gm, '')
.replace(/^\s*@.*$/gm, '')
.replace(/\n\s*\n/g, '\n')
.trim();
}
// Extract properties, events, aggregations with better parsing
const properties: string[] = [];
const events: string[] = [];
const aggregations: string[] = [];
const keywords: string[] = [];
// Extract properties
const propertiesSection = content.match(/properties\s*:\s*\{([\s\S]*?)\n\s*\}/);
if (propertiesSection) {
const propMatches = propertiesSection[1].matchAll(/(\w+)\s*:\s*\{/g);
for (const match of propMatches) {
properties.push(match[1]);
}
}
// Extract events
const eventsSection = content.match(/events\s*:\s*\{([\s\S]*?)\n\s*\}/);
if (eventsSection) {
const eventMatches = eventsSection[1].matchAll(/(\w+)\s*:\s*\{/g);
for (const match of eventMatches) {
events.push(match[1]);
}
}
// Extract aggregations
const aggregationsSection = content.match(/aggregations\s*:\s*\{([\s\S]*?)\n\s*\}/);
if (aggregationsSection) {
const aggMatches = aggregationsSection[1].matchAll(/(\w+)\s*:\s*\{/g);
for (const match of aggMatches) {
aggregations.push(match[1]);
}
}
// Generate keywords based on control name and content
keywords.push(controlName.toLowerCase());
if (namespace) keywords.push(namespace);
if (fullControlName !== controlName) keywords.push(fullControlName);
// Add common UI5 control keywords based on control name
const controlLower = controlName.toLowerCase();
if (controlLower.includes('wizard')) keywords.push('wizard', 'step', 'multi-step', 'process');
if (controlLower.includes('button')) keywords.push('button', 'click', 'press', 'action');
if (controlLower.includes('table')) keywords.push('table', 'grid', 'data', 'row', 'column');
if (controlLower.includes('dialog')) keywords.push('dialog', 'popup', 'modal', 'overlay');
if (controlLower.includes('input')) keywords.push('input', 'field', 'text', 'form');
if (controlLower.includes('list')) keywords.push('list', 'item', 'collection');
if (controlLower.includes('panel')) keywords.push('panel', 'container', 'layout');
if (controlLower.includes('page')) keywords.push('page', 'navigation', 'view');
// Add property/event-based keywords
if (properties.includes('text')) keywords.push('text');
if (properties.includes('value')) keywords.push('value');
if (events.includes('press')) keywords.push('press', 'click');
if (events.includes('change')) keywords.push('change', 'update');
// Count code blocks and property definitions
const codeBlockCount = (content.match(/```/g)?.length || 0) / 2;
const propertyCount = properties.length + events.length + aggregations.length;
return {
title: fullControlName,
description: description || `OpenUI5 control: ${fullControlName}`,
snippetCount: Math.max(1, codeBlockCount + Math.floor(propertyCount / 3)),
controlName,
namespace,
keywords: [...new Set(keywords)],
properties,
events,
aggregations
};
}
function extractMarkdownSections(content: string, lines: string[], src: any, relFile: string, docs: DocEntry[]) {
const sections: { title: string; content: string; startLine: number; level: number }[] = [];
let currentSection: { title: string; content: string; startLine: number; level: number } | null = null;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// Check for headings (##, ###, ####)
let headingLevel = 0;
let headingText = '';
if (line.startsWith('#### ')) {
headingLevel = 4;
headingText = line.slice(5).trim();
} else if (line.startsWith('### ')) {
headingLevel = 3;
headingText = line.slice(4).trim();
} else if (line.startsWith('## ')) {
headingLevel = 2;
headingText = line.slice(3).trim();
}
if (headingLevel > 0) {
// Save previous section if it exists
if (currentSection) {
sections.push(currentSection);
}
// Start new section
currentSection = {
title: headingText,
content: '',
startLine: i,
level: headingLevel
};
} else if (currentSection) {
// Add content to current section
currentSection.content += line + '\n';
}
}
// Add the last section
if (currentSection) {
sections.push(currentSection);
}
// Create separate docs entries for meaningful sections
for (const section of sections) {
// Skip very short sections or those with placeholder titles
if (section.content.trim().length < 100 || section.title.length < 3) {
continue;
}
// Generate description from section content, including code blocks for better searchability
const contentLines = section.content.split('\n').filter(l => l.trim() && !l.startsWith('#'));
// Extract code blocks content for technical terms
const codeBlocks = section.content.match(/```[\s\S]*?```/g) || [];
const codeContent = codeBlocks
.map(block => block.replace(/```[\w]*\n?/g, '').replace(/```/g, ''))
.join(' ')
.replace(/\s+/g, ' ')
.trim();
// Combine description with code content for better indexing
let description = contentLines.slice(0, 3).join(' ').trim() || section.title;
// Include important technical terms from code blocks (like annotation qualifiers)
if (codeContent) {
// Extract meaningful technical terms (identifiers, annotation qualifiers, etc.)
const technicalTerms = (codeContent.match(/[@#]?\w+(?:\.\w+)*(?:#\w+)?/g) || [])
.filter((term: string) => term.length > 3 && !['true', 'false', 'null', 'undefined', 'function', 'return'].includes(term.toLowerCase()))
.slice(0, 10); // Limit to prevent bloating
if (technicalTerms.length > 0) {
description += ' ' + technicalTerms.join(' ');
}
}
// Count code snippets in this section
const snippetCount = (section.content.match(/```/g)?.length || 0) / 2;
// Create section entry
const sectionId = `${src.id}/${relFile.replace(/\.md$/, "")}#${section.title.toLowerCase().replace(/[^a-z0-9]+/g, '-')}`;
docs.push({
id: sectionId,
title: section.title,
description: description.substring(0, 300) + (description.length > 300 ? '...' : ''),
snippetCount,
relFile,
type: 'markdown-section' as any,
parentDocument: `${src.id}/${relFile.replace(/\.md$/, "")}`,
sectionStartLine: section.startLine,
headingLevel: section.level
});
}
}
async function main() {
await fs.mkdir("dist/data", { recursive: true });
const all: Record<string, LibraryBundle> = {};
for (const src of SOURCES) {
const patterns = [src.filePattern];
if (src.exclude) {
patterns.push(`!${src.exclude}`);
}
const files = await fg(patterns, { cwd: src.absDir, absolute: true });
const docs: DocEntry[] = [];
for (const absPath of files) {
const rel = path.relative(src.absDir, absPath).replace(/\\/g, "/");
const raw = await fs.readFile(absPath, "utf8");
let title: string;
let description: string;
let snippetCount: number;
let id: string;
if (src.type === "markdown") {
// Handle markdown files with error handling for malformed frontmatter
let frontmatter, content;
try {
const parsed = matter(raw);
frontmatter = parsed.data;
content = parsed.content;
} catch (yamlError: any) {
console.warn(`YAML parsing failed for ${rel}, using fallback:`, yamlError?.message || yamlError);
// Fallback: extract content without frontmatter
const lines = raw.split('\n');
const contentStartIndex = lines.findIndex((line, index) => line.trim() === '---' && index > 0) + 1;
frontmatter = {};
content = contentStartIndex > 0 ? lines.slice(contentStartIndex).join('\n') : raw;
}
const lines = content.split(/\r?\n/);
// Use frontmatter for title and description (works for ABAP and other sources)
title = frontmatter?.title ||
lines.find((l) => l.startsWith("# "))?.slice(2).trim() ||
path.basename(rel, ".md");
// Enhanced description from frontmatter or content
if (frontmatter?.description) {
description = frontmatter.description;
} else if (frontmatter?.synopsis && content.includes("{{ $frontmatter.synopsis }}")) {
description = frontmatter.synopsis;
} else {
// Fallback to content extraction
const rawDescription = lines.find((l) => l.trim() && !l.startsWith("#"))?.trim() || "";
description = rawDescription;
}
snippetCount = (content.match(/```/g)?.length || 0) / 2;
id = `${src.id}/${rel.replace(/\.md$/, "")}`;
// Extract individual sections as separate entries for all markdown docs
if (content.includes('##')) {
extractMarkdownSections(content, lines, src, rel, docs);
}
} else if (src.type === "jsdoc") {
// Handle JavaScript files with JSDoc
const jsDocInfo = extractJSDocInfo(raw, path.basename(absPath));
title = jsDocInfo.title;
description = jsDocInfo.description;
snippetCount = jsDocInfo.snippetCount;
id = `${src.id}/${rel.replace(/\.js$/, "")}`;
// Skip files that don't look like UI5 controls
if (!raw.includes('.extend') || !raw.includes('metadata')) {
continue;
}
docs.push({
id,
title,
description,
snippetCount,
relFile: rel,
type: src.type,
controlName: jsDocInfo.controlName,
namespace: jsDocInfo.namespace,
keywords: jsDocInfo.keywords,
properties: jsDocInfo.properties,
events: jsDocInfo.events,
aggregations: jsDocInfo.aggregations
});
} else if (src.type === "sample") {
// Handle sample files (JS, XML, JSON, HTML)
const sampleInfo = extractSampleInfo(raw, rel);
title = sampleInfo.title;
description = sampleInfo.description;
snippetCount = sampleInfo.snippetCount;
id = `${src.id}/${rel.replace(/\.(js|xml|json|html)$/, "")}`;
// Skip empty files or non-meaningful samples
if (raw.trim().length < 50) {
continue;
}
// Extract control name from sample path for better searchability
const pathParts = rel.split('/');
const sampleIndex = pathParts.findIndex(part => part === 'sample');
const controlName = sampleIndex >= 0 && sampleIndex < pathParts.length - 1
? pathParts[sampleIndex + 1]
: path.basename(path.dirname(rel));
// Generate sample keywords
const keywords = [controlName.toLowerCase(), 'sample', 'example'];
if (rel.includes('.xml')) keywords.push('view', 'xml');
if (rel.includes('.js')) keywords.push('controller', 'javascript');
if (rel.includes('.json')) keywords.push('model', 'data', 'configuration');
if (rel.includes('manifest')) keywords.push('manifest', 'app');
docs.push({
id,
title,
description,
snippetCount,
relFile: rel,
type: src.type,
controlName,
keywords: [...new Set(keywords)]
});
} else {
continue; // Skip unknown file types
}
// For markdown files, still use the basic structure
if (src.type === "markdown") {
docs.push({
id,
title,
description,
snippetCount,
relFile: rel,
type: src.type
});
}
}
const bundle: LibraryBundle = {
id: src.id,
name: src.name,
description: src.description,
docs
};
all[src.id] = bundle;
await fs.writeFile(
path.join("dist", "data", `data${src.id}.json`.replace(/\//g, "_")),
JSON.stringify(bundle, null, 2)
);
}
await fs.writeFile("dist/data/index.json", JSON.stringify(all, null, 2));
console.log("✅ Index built with", Object.keys(all).length, "libraries.");
}
main();
```