Skip to main content
Glama

SAP Documentation MCP Server

by marianfoo
BaseServerHandler.ts•32.1 kB
/** * 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: sap_docs_search, sap_community_search, sap_docs_get, sap_help_search, sap_help_get * - Prefixed names: mcp_sap-docs-remote_sap_docs_search, mcp_sap-docs-remote_sap_community_search, etc. * * Try the simple names first, then the prefixed versions if they don't work. */ 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 */ function createSearchResponse(results: SearchResult[]): any { // Clean the results to avoid JSON serialization issues in MCP protocol const cleanedResults = results.map(result => ({ ...result, 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, 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 })); return { content: [ { type: "text", text: JSON.stringify({ results: cleanedResults }) } ] }; } /** * Create structured JSON response for document fetch */ function createDocumentResponse(document: DocumentResult): any { // Clean the text content to avoid JSON serialization issues in MCP protocol const cleanedDocument = { ...document, 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 }; 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 { this.setupResourceHandlers(srv); this.setupToolHandlers(srv); // this.setupPromptHandlers(srv); // Temporarily disabled to fix session init } /** * 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_docs_search", description: `SEARCH SAP DOCS: sap_docs_search(query="search terms") FUNCTION NAME: sap_docs_search (or mcp_sap-docs-remote_sap_docs_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. sap_docs_search(query="your search terms") 2. sap_docs_get(library_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: "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. sap_docs_get(library_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_docs_get", description: `GET SPECIFIC DOCS: sap_docs_get(library_id, topic) FUNCTION NAME: sap_docs_get (or mcp_sap-docs-remote_sap_docs_get) RETRIEVES: Full documentation content based on search results šŸ“‹ PARAMETER SCHEMA: • library_id: The library identifier (e.g., "/cap", "/sapui5", "/wdi5") • topic: Specific document/section path within the library (optional) šŸŽÆ USAGE PATTERNS FROM SEARCH RESULTS: • Search shows: Library: "/cap", Topic: "guides/domain-modeling#compositions" • Call: sap_docs_get(library_id="/cap", topic="guides/domain-modeling#compositions") • Search shows: Library: "/openui5-api", Topic: "sap/m/Button" • Call: sap_docs_get(library_id="/openui5-api", topic="sap/m/Button") • Search shows: Library: "/cap" (no topic) • Call: sap_docs_get(library_id="/cap") āœ… ALWAYS use the exact library_id and topic shown in search results āŒ DON'T combine them into a single parameter`, inputSchema: { type: "object", properties: { library_id: { type: "string", description: "Library identifier from search results. Always use the value shown as 'Library:' in search results.", examples: [ "/cap", "/sapui5", "/openui5-api", "/abap-docs-758", "/wdi5", "community-12345" ] }, topic: { type: "string", description: "Optional topic filter for library IDs only (not specific document IDs).", examples: [ "binary", "authentication", "properties", "methods", "locators" ] } }, required: ["library_id"] } }, { 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 (alias for sap_docs_search): search(query="search terms") FUNCTION NAME: search (alias for sap_docs_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(library_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 (alias for sap_docs_get): fetch(library_id="result_id") FUNCTION NAME: fetch (alias for sap_docs_get) RETRIEVES: Full content from search results WORKS WITH: Library IDs, document IDs, community post IDs COMMON PATTERNS: • Broad exploration: library_id="/cap", topic="binary" • Specific API: library_id="/openui5-api/sap/m/Button" • Community posts: library_id="community-12345" • ABAP docs: library_id="/abap-docs-758/abeninline_declarations"`, inputSchema: { type: "object", properties: { library_id: { type: "string", description: "ID from search results. Use exact IDs returned by search functions.", examples: [ "/cap", "/sapui5", "/openui5-api/sap/m/Button", "/abap-docs-758", "community-12345" ] }, topic: { type: "string", description: "Optional topic filter for library IDs only (not specific document IDs).", examples: [ "binary", "authentication", "properties", "methods", "locators" ] } }, required: ["library_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 structured JSON format compatible with the localDocs format 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 { library_id: libraryId, topic: topic, id: r.id, title: r.text.split('\n')[0] || r.id, url: docUrl || `#${r.id}`, snippet: r.text ? r.text.substring(0, 200) + '...' : '', 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 structured format matching the new library_id/topic format const communityResults: SearchResult[] = res.results.map((r: any, index) => ({ library_id: r.library_id || `community-${index}`, topic: r.topic || '', id: r.id || `community-${index}`, title: r.title || 'SAP Community Post', url: r.url || `#${r.id}`, 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") { const { library_id, topic = "" } = args as { library_id: string; topic?: string; }; // 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 structured 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 structured format const helpResults: SearchResult[] = res.results.map((r, index) => ({ id: r.id || `sap-help-${index}`, title: r.title || 'SAP Help Document', url: r.url || `#${r.id}`, 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", displayName: "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", displayName: "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'); } } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/marianfoo/mcp-sap-docs'

If you have feedback or need assistance with the MCP directory API, please join our Discord server