Skip to main content
Glama
index.ts16.6 kB
/** * @fileoverview Ref MCP server with documentation search and URL reading tools. * Supports stdio and HTTP transports with dynamic configuration. */ import { Server } from '@modelcontextprotocol/sdk/server/index.js' import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js' import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js' import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, McpError, isInitializeRequest, type Tool, } from '@modelcontextprotocol/sdk/types.js' import axios from 'axios' import { createServer } from 'http' import { randomUUID } from 'crypto' // Tool configuration based on client type type ToolConfig = { searchToolName: string readToolName: string } const OPENAI_DEEP_RESEARCH_TOOL_CONFIG: ToolConfig = { searchToolName: 'search', readToolName: 'fetch', } const DEFAULT_TOOL_CONFIG: ToolConfig = { searchToolName: 'ref_search_documentation', readToolName: 'ref_read_url', } // Transport configuration from environment const TRANSPORT_TYPE = (process.env.TRANSPORT || 'stdio') as 'stdio' | 'http' const HTTP_PORT = parseInt(process.env.PORT || '8080', 10) // Global variables to store current request config let currentApiKey: string | undefined = undefined // Session management for HTTP transport const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {} const sessionClientInfo: { [sessionId: string]: string } = {} const servers: { [sessionId: string]: Server } = {} // DeepResearch shape for OpenAI compatibility type DeepResearchShape = { id: string title: string text: string url: string metadata?: any } // Function to create a new server instance function createServerInstance(mcpClient: string = 'unknown', sessionId?: string) { const toolConfig = mcpClient === 'openai-mcp' ? OPENAI_DEEP_RESEARCH_TOOL_CONFIG : DEFAULT_TOOL_CONFIG const searchTool: Tool = { name: toolConfig.searchToolName, description: `Search for documentation on the web or github as well from private resources like repos and pdfs. Use Ref '${toolConfig.readToolName}' to read the content of a url.`, inputSchema: { type: 'object', properties: { query: { type: 'string', description: `Query for documentation. Should include programming language and framework or library names. Searches public only docs by default, include ref_src=private to search a user's private docs.`, }, }, required: ['query'], }, } const readTool: Tool = { name: toolConfig.readToolName, description: `Read the content of a url as markdown. The entire exact URL from a Ref '${toolConfig.searchToolName}' result should be passed to this tool to read it.`, inputSchema: { type: 'object', properties: { url: { type: 'string', description: 'The URL of the webpage to read.', }, }, required: ['url'], }, } const server = new Server( { name: 'Ref', version: '3.0.0', }, { capabilities: { prompts: { listChanged: true, }, tools: {}, logging: {}, }, }, ) // Register request handlers server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [searchTool, readTool], })) server.setRequestHandler(ListPromptsRequestSchema, async () => ({ prompts: [ { name: 'search_docs', description: 'A quick way to check technical documentation. This prompt helps you search documentation for any technical platform, framework, API, service, database, or library.', arguments: [ { name: 'query', description: 'The rest of your prompt or question you want informed by docs', required: true, }, ], }, { name: 'my_docs', description: "Search through your private documentation, repos, and PDFs that you've uploaded to Ref.", arguments: [ { name: 'query', description: 'The rest of your prompt or question you want informed by your private docs', required: true, }, ], }, ], })) server.setRequestHandler(GetPromptRequestSchema, async (request) => { const { name, arguments: args } = request.params if (name === 'search_docs') { const query = args?.query as string if (!query) { throw new McpError(ErrorCode.InvalidParams, 'Missing required argument: query') } return { messages: [ { role: 'user', content: { type: 'text', text: `${query}\n\nSearch ref with source=public`, }, }, ], } } if (name === 'my_docs') { const query = args?.query as string if (!query) { throw new McpError(ErrorCode.InvalidParams, 'Missing required argument: query') } return { messages: [ { role: 'user', content: { type: 'text', text: `${query}\n\nSearch ref with source=private`, }, }, ], } } throw new McpError(ErrorCode.InvalidParams, `Unknown prompt: ${name}`) }) server.setRequestHandler(CallToolRequestSchema, async (request) => { if (request.params.name === toolConfig.searchToolName) { console.error('[search_documentation] arguments', request.params.arguments) const input = request.params.arguments as { query: string } return doSearch(input.query, mcpClient, sessionId) } if (request.params.name === toolConfig.readToolName) { const input = request.params.arguments as { url: string } return doRead(input.url, mcpClient, sessionId) } throw new McpError(ErrorCode.MethodNotFound, `Could not find tool: ${request.params.name}`) }) server.onerror = (error: any) => { console.error(error) } return server } const getRefUrl = () => { if (process.env.REF_URL) { return process.env.REF_URL } return 'https://api.ref.tools' } // Helper function to get API key from environment or current request const getApiKey = () => { return process.env.REF_ALPHA || process.env.REF_API_KEY || currentApiKey } // Helper function to get auth headers with session support const getAuthHeaders = (sessionId?: string) => { const headers: Record<string, string | undefined> = { 'X-Ref-Alpha': process.env.REF_ALPHA || (currentApiKey && !process.env.REF_API_KEY ? currentApiKey : undefined), 'X-Ref-Api-Key': process.env.REF_API_KEY || (currentApiKey && !process.env.REF_ALPHA ? currentApiKey : undefined), } if (sessionId) { headers['mcp-session-id'] = sessionId } return headers } function toDeepResearchShape(doc: any): DeepResearchShape { return { id: doc.url, title: doc.overview || doc.title || '', text: (doc.content || '').slice(0, 100), url: doc.url, metadata: { moduleId: doc.moduleId, }, } } async function doSearch(query: string, mcpClient: string = 'unknown', sessionId?: string) { const url = getRefUrl() + '/search_documentation?query=' + encodeURIComponent(query) console.error('[search]', url) if (!getApiKey()) { return { content: [ { type: 'text', text: 'Ref is not correctly configured. Reach out to hello@ref.tools for help.', }, ], } } try { const response = await axios.get(url, { headers: getAuthHeaders(sessionId), }) const data = response.data if (data.docs.length === 0) { return { content: [{ type: 'text', text: 'No results found' }], } } // Return different formats based on client type if (mcpClient === 'openai-mcp') { return { content: [ { type: 'text' as const, text: JSON.stringify(data.docs.map(toDeepResearchShape)), }, ], } } else { return { content: data.docs.map((doc: any) => ({ type: 'text' as const, text: `overview: ${doc.overview || ''} url: ${doc.url} moduleId: ${doc.moduleId || ''}`, })), } } } catch (error) { console.error('[search-error]', error) return { content: [ { type: 'text', text: `Error during documentation search: ${axios.isAxiosError(error) ? error.message : (error as Error).message}`, }, ], } } } async function doRead(url: string, mcpClient: string = 'unknown', sessionId?: string) { try { const readUrl = getRefUrl() + '/read?url=' + encodeURIComponent(url) console.error('[read]', readUrl) if (!getApiKey()) { return { content: [ { type: 'text', text: 'Ref is not correctly configured. Reach out to hello@ref.tools for help.', }, ], } } const response = await axios.get(readUrl, { headers: getAuthHeaders(sessionId), }) const data = response.data // Return different formats based on client type if (mcpClient === 'openai-mcp') { const result: DeepResearchShape = { id: url, title: data.title || '', text: data.content || '', url, } return { content: [ { type: 'text', text: JSON.stringify(result), }, ], } } else { return { content: [{ type: 'text', text: data.content || '' }], } } } catch (error) { console.error('[read-error]', error) return { content: [ { type: 'text', text: `Error reading URL: ${axios.isAxiosError(error) ? error.message : (error as Error).message}`, }, ], } } } async function main() { const transportType = TRANSPORT_TYPE if (transportType === 'http') { const httpServer = createServer(async (req, res) => { const url = new URL(req.url || '', `http://${req.headers.host}`).pathname // Set CORS headers for all responses res.setHeader('Access-Control-Allow-Origin', '*') res.setHeader('Access-Control-Allow-Methods', 'GET,POST,OPTIONS,DELETE') res.setHeader('Access-Control-Allow-Headers', 'Content-Type, MCP-Session-Id, mcp-session-id') // Handle preflight OPTIONS requests if (req.method === 'OPTIONS') { res.writeHead(200) res.end() return } try { if (url === '/mcp') { // Extract client info const userAgentHeader = req.headers['user-agent'] || req.headers['x-mcp-client'] || req.headers['mcp-client'] const userAgent: string = Array.isArray(userAgentHeader) ? userAgentHeader[0] || 'unknown' : userAgentHeader || 'unknown' // Get body for POST requests let body: any = {} if (req.method === 'POST') { const chunks: Buffer[] = [] for await (const chunk of req) { chunks.push(chunk) } const bodyString = Buffer.concat(chunks).toString() try { body = JSON.parse(bodyString) } catch (e) { // Ignore parse errors } } const sessionId = req.headers['mcp-session-id'] as string | undefined const mcpClient: string = (sessionId && sessionClientInfo[sessionId]) || body?.params?.clientInfo?.name || userAgent.split('/')[0] || 'unknown' console.error('MCP REQUEST', { headers: req.headers, method: req.method, url: req.url, sessionId, mcpClient, }) // Extract config from base64-encoded JSON parameter for Smithery compatibility const fullUrl = new URL(req.url || '', `http://${req.headers.host}`) const configParam = fullUrl.searchParams.get('config') if (configParam) { try { const decodedConfig = Buffer.from(configParam, 'base64').toString('utf-8') const config = JSON.parse(decodedConfig) if (config.refApiKey) { currentApiKey = config.refApiKey } } catch (error) { console.error('Failed to parse config parameter:', error) } } if (req.method === 'POST') { let transport: StreamableHTTPServerTransport if (sessionId && transports[sessionId]) { transport = transports[sessionId] } else if (!sessionId && isInitializeRequest(body)) { transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), enableJsonResponse: true, onsessioninitialized: (newSessionId) => { sessionClientInfo[newSessionId] = mcpClient transports[newSessionId] = transport const server = createServerInstance(mcpClient, newSessionId) servers[newSessionId] = server server.connect(transport).catch(console.error) }, }) transport.onclose = () => { if (transport.sessionId) { delete transports[transport.sessionId] delete servers[transport.sessionId] delete sessionClientInfo[transport.sessionId] } } } else { res.writeHead(400, { 'Content-Type': 'application/json' }) res.end( JSON.stringify({ jsonrpc: '2.0', error: { code: -32000, message: 'Bad Request: No valid session ID provided', }, id: null, }), ) return } await transport.handleRequest(req, res, body) } else if (req.method === 'DELETE') { console.log('DELETE request', transports[req.headers['mcp-session-id'] as string]) const sessionId = req.headers['mcp-session-id'] as string | undefined if (sessionId && transports[sessionId]) { await transports[sessionId].close() console.log('closed transport', sessionId) res.writeHead(200) res.end() return } else { res.writeHead(400, { 'Content-Type': 'application/json' }) res.end( JSON.stringify({ jsonrpc: '2.0', error: { code: -32000, message: 'Bad Request: No valid session ID provided', }, id: null, }), ) } } else { res.writeHead(405, { 'Content-Type': 'application/json' }) res.end(JSON.stringify({ error: 'Method not allowed' })) } } else if (url === '/ping') { res.writeHead(200, { 'Content-Type': 'text/plain' }) res.end('pong') } else { res.writeHead(404) res.end('Not found') } } catch (error) { console.error('Error handling request:', error) if (!res.headersSent) { res.writeHead(500) res.end('Internal Server Error') } } finally { // Clear config after request processing currentApiKey = undefined } }) httpServer.listen(HTTP_PORT, () => { console.error(`Ref MCP Server running on HTTP at http://localhost:${HTTP_PORT}/mcp`) }) } else { // Stdio transport (default) const sessionId = randomUUID() const server = createServerInstance('ref-tools-mcp-stdio', sessionId) const transport = new StdioServerTransport() await server.connect(transport) console.error('Ref MCP Server running on stdio') } } process.on('SIGINT', async () => { process.exit(0) }) main().catch((error) => { console.error('Fatal error running server:', error) process.exit(1) }) // Export the server for smithery export default function () { const server = createServerInstance() return server }

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/ref-tools/ref-tools-mcp'

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