Skip to main content
Glama

Payments Developer Portal MCP Server

index.ts21.1 kB
#!/usr/bin/env node import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; import axios from "axios"; import { marked } from "marked"; import NodeCache from "node-cache"; // Base URL for the Firebase hosting const FIREBASE_BASE_URL = "http://localhost:4000"; // Cache for storing documentation data to avoid repeated requests const cache = new NodeCache({ stdTTL: 300, checkperiod: 60 }); // Cache for 5 minutes // Resource types mapping const RESOURCE_TYPES: Record<string, string> = { api: "API Reference", documentation: "Documentation", guide: "Guide", reference: "Reference", }; // Create a new MCP server const server = new Server( { name: "payments-developer-portal", version: "1.0.0", }, { capabilities: { tools: {}, resources: {}, // Required for resources }, } ); // Utility function to fetch data from Firebase hosting async function fetchFromFirebase(path: string): Promise<any> { const cacheKey = `firebase_${path}`; const cachedData = cache.get(cacheKey); if (cachedData) { return cachedData; } try { const response = await axios.get(`${FIREBASE_BASE_URL}${path}`); cache.set(cacheKey, response.data); return response.data; } catch (error: any) { console.error(`Error fetching from Firebase: ${path}`, error.message); throw new Error(`Failed to fetch data from ${path}: ${error.message}`); } } // Fetch the docs index async function getDocsIndex(): Promise<any> { const cacheKey = "docs_index"; const cachedIndex = cache.get(cacheKey); if (cachedIndex) { return cachedIndex; } try { const response = await axios.get(`${FIREBASE_BASE_URL}/docs-index.json`); cache.set(cacheKey, response.data); return response.data; } catch (error: any) { console.error("Error fetching docs index:", error.message); throw new Error(`Failed to fetch docs index: ${error.message}`); } } // Convert Markdown to plain text async function markdownToPlainText(markdown: string): Promise<string> { const html = await marked.parse(markdown); return html .replace(/<[^>]*>/g, "") // Remove HTML tags .replace(/\n\s*\n/g, "\n\n"); // Normalize whitespace } // List available tools server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: "search_docs", description: "Search the developer portal documentation", inputSchema: { type: "object", properties: { query: { type: "string", description: "Search query", }, type: { type: "string", description: "Filter by resource type (api, documentation, guide, reference)", enum: ["api", "documentation", "guide", "reference"], }, category: { type: "string", description: "Filter by category", }, tag: { type: "string", description: "Filter by tag", }, limit: { type: "number", description: "Maximum number of results to return", default: 10, }, }, required: ["query"], }, }, { name: "get_resource_by_id", description: "Get a specific resource by its ID", inputSchema: { type: "object", properties: { id: { type: "string", description: "Resource ID", }, }, required: ["id"], }, }, { name: "list_categories", description: "List all available documentation categories", inputSchema: { type: "object", properties: { includeResources: { type: "boolean", description: "Whether to include resources in each category", default: false, }, }, }, }, { name: "list_resources_by_category", description: "List resources in a specific category", inputSchema: { type: "object", properties: { category: { type: "string", description: "Category ID", }, }, required: ["category"], }, }, { name: "list_resources_by_tag", description: "List resources with a specific tag", inputSchema: { type: "object", properties: { tag: { type: "string", description: "Tag name", }, }, required: ["tag"], }, }, { name: "get_related_resources", description: "Get resources related to a specific resource", inputSchema: { type: "object", properties: { id: { type: "string", description: "Resource ID", }, }, required: ["id"], }, }, ], })); // List available resources server.setRequestHandler(ListResourcesRequestSchema, async () => { try { const docsIndex = await getDocsIndex(); // Create resource URIs for each resource type const resources = [ { uri: "payments-docs://api", mimeType: "text/plain", name: "API References", }, { uri: "payments-docs://documentation", mimeType: "text/plain", name: "Documentation", }, { uri: "payments-docs://guides", mimeType: "text/plain", name: "Guides", }, { uri: "payments-docs://categories", mimeType: "text/plain", name: "Categories", }, { uri: "payments-docs://tags", mimeType: "text/plain", name: "Tags", }, ]; return { resources }; } catch (error: any) { console.error("Error listing resources:", error.message); throw new Error(`Failed to list resources: ${error.message}`); } }); // Handle resource reading server.setRequestHandler(ReadResourceRequestSchema, async (request) => { try { const docsIndex = await getDocsIndex(); const uri = request.params.uri; // Handle different resource types if (uri === "payments-docs://api") { const apiResources = docsIndex.resources.filter( (r: any) => r.type === "api" ); return { contents: [ { uri, mimeType: "text/plain", text: `API References (${apiResources.length}):\n\n${apiResources .map( (r: any) => `- ${r.title}: ${r.description}\n ID: ${r.id}\n Path: ${ r.path }\n Topics: ${r.topics.join(", ")}\n` ) .join("\n")}`, }, ], }; } else if (uri === "payments-docs://documentation") { const docResources = docsIndex.resources.filter( (r: any) => r.type === "documentation" ); return { contents: [ { uri, mimeType: "text/plain", text: `Documentation (${docResources.length}):\n\n${docResources .map( (r: any) => `- ${r.title}: ${r.description}\n ID: ${r.id}\n Path: ${ r.path }\n Topics: ${r.topics.join(", ")}\n` ) .join("\n")}`, }, ], }; } else if (uri === "payments-docs://guides") { const guideResources = docsIndex.resources.filter( (r: any) => r.type === "guide" ); return { contents: [ { uri, mimeType: "text/plain", text: `Guides (${guideResources.length}):\n\n${guideResources .map( (r: any) => `- ${r.title}: ${r.description}\n ID: ${r.id}\n Path: ${ r.path }\n Topics: ${r.topics.join(", ")}\n` ) .join("\n")}`, }, ], }; } else if (uri === "payments-docs://categories") { return { contents: [ { uri, mimeType: "text/plain", text: `Categories (${ docsIndex.categories.length }):\n\n${docsIndex.categories .map( (c: any) => `- ${c.name} (${c.resources.length} resources)\n ID: ${c.id}\n` ) .join("\n")}`, }, ], }; } else if (uri === "payments-docs://tags") { return { contents: [ { uri, mimeType: "text/plain", text: `Tags (${docsIndex.tags.length}):\n\n${docsIndex.tags .map( (t: any) => `- ${t.name} (${t.resources.length} resources)\n` ) .join("\n")}`, }, ], }; } else if (uri.startsWith("payments-docs://resource/")) { const resourceId = uri.replace("payments-docs://resource/", ""); const resource = docsIndex.resources.find( (r: any) => r.id === resourceId ); if (!resource) { throw new Error(`Resource not found: ${resourceId}`); } // Fetch the actual content of the resource let content = ""; try { const resourceData = await fetchFromFirebase(resource.path); if (resource.path.endsWith(".md")) { // For markdown files, convert to plain text content = await markdownToPlainText(resourceData); } else if (resource.path.endsWith(".json")) { // For JSON files, format nicely content = JSON.stringify(resourceData, null, 2); } else { content = String(resourceData); } } catch (error) { content = `[Content could not be fetched: ${error}]`; } return { contents: [ { uri, mimeType: resource.path.endsWith(".md") ? "text/markdown" : "application/json", text: `# ${resource.title}\n\n${resource.description}\n\n${content}`, }, ], }; } throw new Error(`Resource not found: ${uri}`); } catch (error: any) { console.error("Error reading resource:", error.message); throw new Error(`Failed to read resource: ${error.message}`); } }); // Handle tool calls server.setRequestHandler(CallToolRequestSchema, async (request) => { try { const docsIndex = await getDocsIndex(); if (request.params.name === "search_docs") { const { query, type, category, tag, limit = 10, } = request.params.arguments as any; // Filter resources based on criteria let results = docsIndex.resources; if (type) { results = results.filter((r: any) => r.type === type); } if (category) { const categoryObj = docsIndex.categories.find( (c: any) => c.id === category || c.name.toLowerCase() === category.toLowerCase() ); if (categoryObj) { results = results.filter((r: any) => categoryObj.resources.includes(r.id) ); } else { return { content: [ { type: "text", text: `Category not found: ${category}` }, ], isError: true, }; } } if (tag) { const tagObj = docsIndex.tags.find( (t: any) => t.name.toLowerCase() === tag.toLowerCase() ); if (tagObj) { results = results.filter((r: any) => tagObj.resources.includes(r.id)); } else { return { content: [{ type: "text", text: `Tag not found: ${tag}` }], isError: true, }; } } // Search in title, description, and topics const searchTerms = query.toLowerCase().split(/\s+/); results = results.filter((r: any) => { const searchableText = `${r.title} ${r.description} ${r.topics.join( " " )}`.toLowerCase(); return searchTerms.every((term: string) => searchableText.includes(term)); }); // Sort by relevance (importance) and limit results results = results .sort((a: any, b: any) => b.importance - a.importance) .slice(0, limit); if (results.length === 0) { return { content: [ { type: "text", text: `No results found for query: "${query}"${ type ? ` in type: ${type}` : "" }${category ? ` in category: ${category}` : ""}${ tag ? ` with tag: ${tag}` : "" }`, }, ], }; } return { content: [ { type: "text", text: `Found ${results.length} results for "${query}":\n\n${results .map( (r: any, i: number) => `${i + 1}. **${r.title}** (${ RESOURCE_TYPES[r.type] || r.type })\n ${r.description}\n ID: \`${ r.id }\`\n Topics: ${r.topics.join(", ")}\n` ) .join( "\n" )}\n\nTo view a specific resource, use the \`get_resource_by_id\` tool with the resource ID.`, }, ], }; } else if (request.params.name === "get_resource_by_id") { const { id } = request.params.arguments as any; const resource = docsIndex.resources.find((r: any) => r.id === id); if (!resource) { return { content: [ { type: "text", text: `Resource not found with ID: ${id}` }, ], isError: true, }; } // Fetch the actual content of the resource let content = ""; try { const resourceData = await fetchFromFirebase(resource.path); if (resource.path.endsWith(".md")) { // For markdown files, convert to plain text content = await markdownToPlainText(resourceData); } else if (resource.path.endsWith(".json")) { // For JSON files, format nicely content = JSON.stringify(resourceData, null, 2); } else { content = String(resourceData); } } catch (error) { content = `[Content could not be fetched: ${error}]`; } // Find related resources const relatedResources = resource.related && resource.related.length > 0 ? docsIndex.resources.filter((r: any) => resource.related.includes(r.id) ) : []; return { content: [ { type: "text", text: `# ${resource.title}\n\n${resource.description}\n\n` + `**Type:** ${RESOURCE_TYPES[resource.type] || resource.type}\n` + `**Topics:** ${resource.topics.join(", ")}\n` + (resource.difficulty ? `**Difficulty:** ${resource.difficulty}\n` : "") + (resource.estimatedTime ? `**Estimated Time:** ${resource.estimatedTime}\n` : "") + `**Last Updated:** ${resource.lastUpdated}\n\n` + `## Content\n\n${content}\n\n` + (relatedResources.length > 0 ? `## Related Resources\n\n${relatedResources .map((r: any) => `- ${r.title} (ID: \`${r.id}\`)`) .join("\n")}\n` : ""), }, ], }; } else if (request.params.name === "list_categories") { const { includeResources = false } = request.params.arguments as any; const categoriesText = docsIndex.categories .map((c: any) => { let text = `- **${c.name}** (ID: \`${c.id}\`): ${c.resources.length} resources`; if (includeResources) { const categoryResources = docsIndex.resources.filter((r: any) => c.resources.includes(r.id) ); text += `\n Resources:\n${categoryResources .map( (r: any) => ` - ${r.title} (ID: \`${r.id}\`, Type: ${ RESOURCE_TYPES[r.type] || r.type })` ) .join("\n")}`; } return text; }) .join("\n\n"); return { content: [ { type: "text", text: `# Documentation Categories (${docsIndex.categories.length})\n\n${categoriesText}`, }, ], }; } else if (request.params.name === "list_resources_by_category") { const { category } = request.params.arguments as any; const categoryObj = docsIndex.categories.find( (c: any) => c.id === category || c.name.toLowerCase() === category.toLowerCase() ); if (!categoryObj) { return { content: [{ type: "text", text: `Category not found: ${category}` }], isError: true, }; } const categoryResources = docsIndex.resources.filter((r: any) => categoryObj.resources.includes(r.id) ); return { content: [ { type: "text", text: `# ${categoryObj.name} (${ categoryResources.length } resources)\n\n${categoryResources .map( (r: any) => `- **${r.title}** (${RESOURCE_TYPES[r.type] || r.type})\n ${ r.description }\n ID: \`${r.id}\`\n Topics: ${r.topics.join(", ")}\n` ) .join("\n")}`, }, ], }; } else if (request.params.name === "list_resources_by_tag") { const { tag } = request.params.arguments as any; const tagObj = docsIndex.tags.find( (t: any) => t.name.toLowerCase() === tag.toLowerCase() ); if (!tagObj) { return { content: [{ type: "text", text: `Tag not found: ${tag}` }], isError: true, }; } const tagResources = docsIndex.resources.filter((r: any) => tagObj.resources.includes(r.id) ); return { content: [ { type: "text", text: `# Resources with tag: ${tagObj.name} (${ tagResources.length } resources)\n\n${tagResources .map( (r: any) => `- **${r.title}** (${RESOURCE_TYPES[r.type] || r.type})\n ${ r.description }\n ID: \`${r.id}\`\n Topics: ${r.topics.join(", ")}\n` ) .join("\n")}`, }, ], }; } else if (request.params.name === "get_related_resources") { const { id } = request.params.arguments as any; const resource = docsIndex.resources.find((r: any) => r.id === id); if (!resource) { return { content: [ { type: "text", text: `Resource not found with ID: ${id}` }, ], isError: true, }; } if (!resource.related || resource.related.length === 0) { return { content: [ { type: "text", text: `No related resources found for: ${resource.title} (ID: ${id})`, }, ], }; } const relatedResources = docsIndex.resources.filter((r: any) => resource.related.includes(r.id) ); return { content: [ { type: "text", text: `# Resources related to: ${ resource.title }\n\n${relatedResources .map( (r: any) => `- **${r.title}** (${RESOURCE_TYPES[r.type] || r.type})\n ${ r.description }\n ID: \`${r.id}\`\n Topics: ${r.topics.join(", ")}\n` ) .join("\n")}`, }, ], }; } throw new Error(`Unknown tool: ${request.params.name}`); } catch (error: any) { console.error("Error handling tool call:", error.message); return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true, }; } }); // Run the server async function runServer() { try { // Test connection to Firebase hosting console.error("Connecting to Firebase hosting..."); await axios.get(`${FIREBASE_BASE_URL}/docs-index.json`); console.error("Successfully connected to Firebase hosting"); const transport = new StdioServerTransport(); await server.connect(transport); console.error("Payments Developer Portal MCP Server running on stdio"); } catch (error: any) { console.error(`Failed to start server: ${error.message}`); process.exit(1); } } runServer().catch(console.error);

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/PraveenJoshua23/MCP-portal'

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