Skip to main content
Glama
index.ts9.15 kB
import express from "express"; import { randomUUID } from "node:crypto"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js"; import { z } from "zod"; import { DevToAPI } from "./devto-api.ts"; import { createTextResult } from "./lib/utils.ts"; import { logger } from "./logger.ts"; import { getConfig } from "./config.ts"; const getServer = () => { const server = new McpServer({ name: "dev-to-mcp", version: "1.0.0", }); const devToAPI = new DevToAPI(); server.registerTool( "get_articles", { title: "Get Articles", description: "Get articles from dev.to. Can filter by username, tag, or other parameters.", annotations: { readOnlyHint: true, openWorldHint: true }, inputSchema: { username: z.string().optional().describe("Filter articles by username"), tag: z.string().optional().describe("Filter articles by tag"), top: z .number() .optional() .describe( "Number representing the number of days since publication for top articles (1, 7, 30, or infinity)", ), page: z .number() .optional() .default(1) .describe("Pagination page number (default: 1)"), per_page: z .number() .optional() .default(30) .describe("Number of articles per page (default: 30, max: 1000)"), state: z .enum(["fresh", "rising", "all"]) .optional() .describe("Filter by article state"), }, }, async (args) => { logger.info({ args }, "Getting articles"); try { const data = await devToAPI.getArticles(args); logger.debug({ articlesCount: Array.isArray(data) ? data.length : 'unknown' }, "Articles retrieved"); return createTextResult(data); } catch (error) { logger.error({ error, args }, "Failed to get articles"); throw error; } }, ); server.registerTool( "get_article", { title: "Get Article", description: "Get a specific article by ID or path", annotations: { readOnlyHint: true, openWorldHint: true }, inputSchema: { id: z.number().optional().describe("Article ID"), path: z .string() .optional() .describe('Article path (e.g., "username/article-slug")'), }, }, async (args) => { logger.info({ args }, "Getting article"); if (!args.id && !args.path) { logger.error({ args }, "Neither id nor path provided for get_article"); throw new Error("Either id or path must be provided"); } try { const data = await devToAPI.getArticle(args); logger.debug({ articleId: args.id, articlePath: args.path }, "Article retrieved"); return createTextResult(data); } catch (error) { logger.error({ error, args }, "Failed to get article"); throw error; } }, ); server.registerTool( "get_user", { title: "Get User", description: "Get user information by ID or username", annotations: { readOnlyHint: true, openWorldHint: true }, inputSchema: { id: z.number().optional().describe("User ID"), username: z.string().optional().describe("Username"), }, }, async (args) => { logger.info({ args }, "Getting user"); if (!args.id && !args.username) { logger.error({ args }, "Neither id nor username provided for get_user"); throw new Error("Either id or username must be provided"); } try { const data = await devToAPI.getUser(args); logger.debug({ userId: args.id, username: args.username }, "User retrieved"); return createTextResult(data); } catch (error) { logger.error({ error, args }, "Failed to get user"); throw error; } }, ); server.registerTool( "get_tags", { title: "Get Tags", description: "Get popular tags from dev.to", annotations: { readOnlyHint: true, openWorldHint: true }, inputSchema: { page: z .number() .optional() .default(1) .describe("Pagination page number (default: 1)"), per_page: z .number() .optional() .default(10) .describe("Number of tags per page (default: 10, max: 1000)"), }, }, async (args) => { logger.info({ args }, "Getting tags"); try { const data = await devToAPI.getTags(args); logger.debug({ tagsCount: Array.isArray(data) ? data.length : 'unknown' }, "Tags retrieved"); return createTextResult(data); } catch (error) { logger.error({ error, args }, "Failed to get tags"); throw error; } }, ); server.registerTool( "get_comments", { title: "Get Comments", description: "Get comments for a specific article", annotations: { readOnlyHint: true, openWorldHint: true }, inputSchema: { article_id: z.number().describe("Article ID to get comments for"), }, }, async (args) => { logger.info({ args }, "Getting comments"); try { const data = await devToAPI.getComments(args); logger.debug({ commentsCount: Array.isArray(data) ? data.length : 'unknown' }, "Comments retrieved"); return createTextResult(data); } catch (error) { logger.error({ error, args }, "Failed to get comments"); throw error; } }, ); server.registerTool( "search_articles", { title: "Search Articles", description: "Search articles using query parameters", annotations: { readOnlyHint: true, openWorldHint: true }, inputSchema: { q: z.string().describe("Search query"), page: z .number() .optional() .default(1) .describe("Pagination page number (default: 1)"), per_page: z .number() .optional() .default(30) .describe("Number of articles per page (default: 30, max: 1000)"), search_fields: z .string() .optional() .describe( "Comma-separated list of fields to search (title, body_text, tag_list)", ), }, }, async (args) => { logger.info({ args }, "Searching articles"); try { const data = await devToAPI.searchArticles(args); logger.debug({ resultsCount: Array.isArray(data) ? data.length : 'unknown' }, "Article search completed"); return createTextResult(data); } catch (error) { logger.error({ error, args }, "Failed to search articles"); throw error; } }, ); return server; }; const app = express(); app.use(express.json()); const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {}; const mcpHandler = async (req: express.Request, res: express.Response) => { const sessionId = req.headers["mcp-session-id"] as string | undefined; if (req.method === "POST" && !sessionId && isInitializeRequest(req.body)) { logger.info("Initializing new MCP session"); const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), onsessioninitialized: (sessionId) => { logger.info({ sessionId }, "MCP session initialized"); transports[sessionId] = transport; }, }); const server = getServer(); await server.connect(transport); await transport.handleRequest(req, res, req.body); return; } if (sessionId && transports[sessionId]) { logger.debug({ sessionId }, "Handling request for existing session"); const transport = transports[sessionId]; await transport.handleRequest(req, res, req.body); return; } if (req.method === "POST" && !sessionId) { logger.warn("POST request without session ID for non-initialization request"); res .status(400) .json({ error: "Session ID required for non-initialization requests" }); return; } if (sessionId && !transports[sessionId]) { logger.warn({ sessionId }, "Request for unknown session"); res.status(404).json({ error: "Session not found" }); return; } if (req.method === "GET") { res.json({ name: "dev-to-mcp", version: "1.0.0", description: "MCP server for dev.to public API", capabilities: ["tools"], }); } }; app.post("/mcp", mcpHandler); app.get("/mcp", mcpHandler); async function main() { const config = getConfig(); const port = config.PORT; app.listen(port, () => { logger.info({ port, environment: config.NODE_ENV }, "Dev.to MCP Server started"); }); } main().catch((error) => { logger.error({ error }, "Server startup failed"); process.exit(1); });

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/nickytonline/dev-to-mcp'

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