Skip to main content
Glama
by wuyuwenj
main.ts8.51 kB
import express, { Request, Response } from 'express'; import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; import { ReadResourceResult } from '@modelcontextprotocol/sdk/types.js'; import cors from 'cors'; import { log, Actor } from 'apify'; import { scrapeApifyDocs, DocPage } from './scraper.js'; import { registerTools } from './tools.js'; // Initialize the Apify Actor environment await Actor.init(); // Global docs storage let cachedDocs: DocPage[] = []; interface ActorInput { startUrls?: Array<{ url: string }>; maxPages?: number; } // Initialize documentation on startup async function initializeDocs() { log.info('Initializing documentation...'); // Try to load from Key-Value Store first (cache) const cached = await Actor.getValue<DocPage[]>('scraped-docs'); if (cached && cached.length > 0) { log.info(`Loaded ${cached.length} docs from cache`); cachedDocs = cached; } else { log.info('Scraping documentation (this may take a few minutes)...'); try { // Read Actor input const input = await Actor.getInput<ActorInput>() ?? {}; // Extract URLs from input format const startUrls = input.startUrls?.map((item) => item.url) ?? undefined; const maxPages = input.maxPages ?? undefined; log.info('Scraping with configuration:', { startUrls, maxPages }); cachedDocs = await scrapeApifyDocs({ startUrls, maxPages, }); await Actor.setValue('scraped-docs', cachedDocs); log.info(`Documentation cached: ${cachedDocs.length} pages`); } catch (error) { log.error('Failed to scrape documentation:', { error }); cachedDocs = []; } } log.info(`Loaded ${cachedDocs.length} documentation pages`); } // Initialize docs before starting server await initializeDocs(); const getServer = () => { // Create an MCP server with implementation details const server = new McpServer( { name: 'api-docs-mcp', version: '1.0.0', }, { capabilities: { logging: {} } }, ); // Register all documentation tools registerTools(server, cachedDocs); // Create a resource for server info server.registerResource( 'server-info', 'https://apify.com/docs-mcp/info', { mimeType: 'text/plain' }, async (): Promise<ReadResourceResult> => { return { contents: [ { uri: 'https://apify.com/docs-mcp/info', text: `Apify Docs MCP Server\n\nThis MCP server provides access to Apify SDK documentation.\n\nAvailable tools:\n- list_available_docs: List all documentation pages\n- search_docs: Search through documentation\n- get_docs_by_id: Get full content of a specific page\n- get_api_reference: Get API reference for functions/classes\n- get_code_example: Get code examples for features\n\nDocumentation pages loaded: ${cachedDocs.length}`, }, ], }; }, ); return server; }; const app = express(); app.use(express.json()); // Configure CORS to expose Mcp-Session-Id header for browser-based clients app.use( cors({ origin: '*', exposedHeaders: ['Mcp-Session-Id'], }), ); // Readiness probe and status endpoint app.get('/', (req: Request, res: Response) => { if (req.headers['x-apify-container-server-readiness-probe']) { log.info('Readiness probe'); res.end('ok\n'); return; } // Build the MCP endpoint URL from container web server URL const webServerUrl = process.env.ACTOR_WEB_SERVER_URL; // Use the container web server URL, fallback to hardcoded URL const mcpUrl = webServerUrl ? `${webServerUrl}/mcp` : 'https://wuyuwen0--apify-api-doc-actor.apify.actor/mcp'; // Generate the Claude CLI command const cliCommand = `claude mcp add --transport http api-docs ${mcpUrl} --header "Authorization: Bearer YOUR_APIFY_TOKEN"`; // Return server status for regular GET requests res.json({ status: 'running', name: 'api-docs-mcp', version: '1.0.0', docs_loaded: cachedDocs.length, tools: ['list_available_docs', 'search_docs', 'get_docs_by_id', 'get_api_reference', 'get_code_example'], mcp_endpoint: mcpUrl, cli_command: cliCommand, setup_instructions: { step1: 'Get your Apify API token from https://console.apify.com/account/integrations', step2: 'Copy and run the command above, replacing YOUR_APIFY_TOKEN with your actual token', step3: 'Restart Claude Code to load the new MCP server', }, }); }); app.post('/mcp', async (req: Request, res: Response) => { const server = getServer(); try { const transport: StreamableHTTPServerTransport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined, }); await server.connect(transport); await transport.handleRequest(req, res, req.body); res.on('close', () => { log.info('Request closed'); transport.close(); server.close(); }); } catch (error) { log.error('Error handling MCP request:', { error }); if (!res.headersSent) { res.status(500).json({ jsonrpc: '2.0', error: { code: -32603, message: 'Internal server error', }, id: null, }); } } }); app.get('/mcp', (_req: Request, res: Response) => { log.info('Received GET MCP request'); res.writeHead(405).end( JSON.stringify({ jsonrpc: '2.0', error: { code: -32000, message: 'Method not allowed.', }, id: null, }), ); }); app.delete('/mcp', (_req: Request, res: Response) => { log.info('Received DELETE MCP request'); res.writeHead(405).end( JSON.stringify({ jsonrpc: '2.0', error: { code: -32000, message: 'Method not allowed.', }, id: null, }), ); }); // Endpoint to refresh documentation cache app.post('/refresh-docs', async (_req: Request, res: Response) => { log.info('Refreshing documentation cache...'); try { // Re-read Actor input for refresh const input = await Actor.getInput<ActorInput>() ?? {}; const startUrls = input.startUrls?.map((item) => item.url) ?? undefined; const maxPages = input.maxPages ?? undefined; log.info('Refreshing with configuration:', { startUrls, maxPages }); cachedDocs = await scrapeApifyDocs({ startUrls, maxPages, }); await Actor.setValue('scraped-docs', cachedDocs); res.json({ success: true, docs_loaded: cachedDocs.length }); } catch (error) { log.error('Failed to refresh docs:', { error }); res.status(500).json({ success: false, error: 'Failed to refresh documentation' }); } }); // Start the server const PORT = process.env.ACTOR_WEB_SERVER_PORT ? parseInt(process.env.ACTOR_WEB_SERVER_PORT) : 3000; app.listen(PORT, (error) => { if (error) { log.error('Failed to start server:', { error }); process.exit(1); } log.info(`MCP Server listening on port ${PORT}`); log.info(`Documentation pages loaded: ${cachedDocs.length}`); // Output the CLI command for easy setup const webServerUrl = process.env.ACTOR_WEB_SERVER_URL; const mcpUrl = webServerUrl ? `${webServerUrl}/mcp` : 'https://wuyuwen0--apify-api-doc-actor.apify.actor/mcp'; const cliCommand = `claude mcp add --transport http api-docs ${mcpUrl} --header "Authorization: Bearer YOUR_APIFY_TOKEN"`; log.info('='.repeat(80)); log.info('To add this MCP server to Claude Code, run:'); log.info(cliCommand); log.info('Replace YOUR_APIFY_TOKEN with your token from: https://console.apify.com/account/integrations'); log.info('='.repeat(80)); }); // Handle server shutdown process.on('SIGINT', async () => { log.info('Shutting down server...'); process.exit(0); });

Latest Blog Posts

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/wuyuwenj/api-doc-mcp'

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