Skip to main content
Glama
clsferguson

Web URL Reader MCP Server

by clsferguson
server.js7.04 kB
import express from 'express'; import cors from 'cors'; import { randomUUID } from 'node:crypto'; import { execFile } from 'node:child_process'; import { promisify } from 'node:util'; import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; import { z } from 'zod'; const execFileAsync = promisify(execFile); const CUSTOM_PREFIX = process.env.CUSTOM_PREFIX || ''; const INTERNAL_PORT = parseInt(process.env.INTERNAL_PORT || '8080', 10); // Timestamped logging function log(...args) { const ts = new Date().toISOString(); console.log(`[${ts}]`, ...args); } // Build "CUSTOM_PREFIX/<absolute-url>" while avoiding double slashes function buildPrefixed(url) { if (!CUSTOM_PREFIX) return url; const sep = CUSTOM_PREFIX.endsWith('/') ? '' : '/'; return `${CUSTOM_PREFIX}${sep}${url}`; } const app = express(); app.use(express.json()); // CORS: expose session header for browser-based clients app.use( cors({ origin: '*', credentials: false, exposedHeaders: ['Mcp-Session-Id'], allowedHeaders: ['Content-Type', 'mcp-session-id'] }) ); // In-memory transport map per session const transports = new Map(); // Helper: read session id from header or from query (?session=...) // Improves compatibility with clients that can’t attach custom headers. function getSessionId(req) { const h = req.headers['mcp-session-id']; const fromHeader = Array.isArray(h) ? h[0] : h; const fromQuery = typeof req.query?.session === 'string' ? req.query.session : undefined; return fromHeader || fromQuery || null; } // Create MCP server and register tools function createMcpServer() { const server = new McpServer({ name: 'mcp-web-url-reader', version: '0.1.0' }); server.registerTool( 'read_web_url', { title: 'Web URL Reader', description: 'Read the content from an URL. Use this for further information retrieving to understand the content of each URL.', inputSchema: { url: z.string().url().describe('Absolute URL starting with http:// or https://') } }, async ({ url }) => { const prefixed = buildPrefixed(url); // Log the request info log(`[tool:read_web_url] request`, { originalUrl: url, prefixedUrl: prefixed }); // Append HTTP status at end of stdout so we can log it without logging the body const STATUS_MARKER = '<<<MCP_HTTP_STATUS:'; const STATUS_END = '>>>'; try { const { stdout } = await execFileAsync( 'curl', [ '-sL', '--fail', prefixed, '-w', `\n${STATUS_MARKER}%{http_code}${STATUS_END}` ], { maxBuffer: 25 * 1024 * 1024 } ); // Split body and status using the marker let body = stdout; let httpCode = '000'; const idx = stdout.lastIndexOf(STATUS_MARKER); if (idx !== -1) { body = stdout.slice(0, idx); const tail = stdout.slice(idx + STATUS_MARKER.length); const endIdx = tail.indexOf(STATUS_END); if (endIdx !== -1) httpCode = tail.slice(0, endIdx).trim(); } // Log summary without printing the response body log(`[tool:read_web_url] response received`, { prefixedUrl: prefixed, httpCode, bytes: body.length }); return { content: [{ type: 'text', text: body }] }; } catch (err) { const msg = err && typeof err === 'object' && 'stderr' in err && err.stderr ? String(err.stderr) : String(err?.message || err); log(`[tool:read_web_url] error`, { prefixedUrl: prefixed, error: msg }); return { content: [{ type: 'text', text: `curl error for ${prefixed}: ${msg}` }], isError: true }; } } ); return server; } // POST: JSON-RPC over Streamable HTTP app.post('/mcp', async (req, res) => { const body = req.body || {}; const method = body?.method; let sessionId = getSessionId(req); let transport = sessionId ? transports.get(sessionId) : undefined; const isInit = method === 'initialize'; log(`[mcp] POST`, { method, sessionId: sessionId || null, isInit }); // Create a new session for initialize OR when no session is provided (compat mode) if (!transport && (isInit || !sessionId)) { log(`[mcp] creating new session`); // Pre-generate an ID so we can safely set the header immediately const newSessionId = randomUUID(); transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => newSessionId }); const server = createMcpServer(); transport.onclose = () => { if (transport.sessionId) transports.delete(transport.sessionId); server.close(); log(`[mcp] session closed`, { sessionId: transport.sessionId || newSessionId }); }; await server.connect(transport); // Store and advertise the session transports.set(newSessionId, transport); sessionId = newSessionId; if (sessionId) res.setHeader('Mcp-Session-Id', sessionId); log(`[mcp] session created`, { sessionId }); } if (!transport) { log(`[mcp] missing/invalid session`, { method, sessionId: sessionId || null }); return res.status(400).json({ jsonrpc: '2.0', error: { code: -32000, message: 'Bad Request: No valid session ID provided' }, id: body?.id ?? null }); } // Always expose session ID for clients to persist it if (sessionId) res.setHeader('Mcp-Session-Id', sessionId); // Hand off to the transport await transport.handleRequest(req, res, body); }); // GET: SSE stream for notifications (requires a valid session) app.get('/mcp', async (req, res) => { const sessionId = getSessionId(req); log(`[mcp] GET (SSE)`, { sessionId: sessionId || null }); const transport = sessionId ? transports.get(sessionId) : undefined; if (!transport) { log(`[mcp] GET invalid/missing session`, { sessionId: sessionId || null }); return res.status(400).send('Invalid or missing session ID'); } await transport.handleRequest(req, res); }); // DELETE: close a session app.delete('/mcp', async (req, res) => { const sessionId = getSessionId(req); log(`[mcp] DELETE`, { sessionId: sessionId || null }); const transport = sessionId ? transports.get(sessionId) : undefined; if (!transport) { log(`[mcp] DELETE invalid/missing session`, { sessionId: sessionId || null }); return res.status(400).send('Invalid or missing session ID'); } transports.delete(sessionId); transport.close(); log(`[mcp] session deleted`, { sessionId }); res.status(204).end(); }); // Health endpoint app.get('/', (_req, res) => { res.json({ status: 'ok', name: 'mcp-web-url-reader', prefixConfigured: Boolean(CUSTOM_PREFIX), port: INTERNAL_PORT }); }); app.listen(INTERNAL_PORT, () => { log(`MCP Streamable HTTP server listening on ${INTERNAL_PORT}`); });

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/clsferguson/mcp-web-url-reader'

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