{"slug": "turn-dev-to-into-an-ai-tool-build-your-own-mcp-server", "title": "Turn DEV.to Into an AI Tool: Build Your Own MCP Server", "summary": "A developer built an MCP server that connects AI assistants to the DEV Community API, enabling tools to list articles, inspect posts, and create drafts without manual copying. The TypeScript-based server exposes three tools and reads the API key from the environment for safety.", "body_md": "What if your AI assistant could list your DEV Community posts, inspect an article, and prepare a new draft without you copying content between browser tabs?\n\nThat is exactly the kind of workflow the **Model Context Protocol (MCP)** enables. In this tutorial, you will learn how a DEV.to MCP integration works, how to test it safely, and how to build a small version yourself with TypeScript.\n\nThe important safety rule comes first:\n\nLet the assistant create\n\ndraftsby default. Publishing should always be an explicit decision.\n\nOur MCP server will sit between an MCP-compatible AI client and the DEV/Forem API:\n\n```\nAI assistant\n     |\n     | MCP tool call\n     v\nLocal DEV.to MCP server\n     |\n     | HTTPS + API key\n     v\nDEV Community API\n```\n\nWe will expose three tools:\n\n`list_my_articles`\n\n— retrieve your published or unpublished posts;`get_article`\n\n— inspect an article by ID;`create_article_draft`\n\n— save Markdown as an unpublished draft.MCP servers may expose tools, resources, and prompts. Tools are the right primitive here because each operation has explicit inputs and may call an external API.\n\nYou will need:\n\nCreate a project and install the official TypeScript SDK:\n\n```\nmkdir devto-mcp\ncd devto-mcp\nnpm init -y\nnpm install @modelcontextprotocol/sdk zod@3\nnpm install -D typescript @types/node\n```\n\nAdd `\"type\": \"module\"`\n\nto `package.json`\n\n, plus a build command:\n\n```\n{\n  \"type\": \"module\",\n  \"scripts\": {\n    \"build\": \"tsc\"\n  }\n}\n```\n\nA minimal `tsconfig.json`\n\n:\n\n```\n{\n  \"compilerOptions\": {\n    \"target\": \"ES2022\",\n    \"module\": \"Node16\",\n    \"moduleResolution\": \"Node16\",\n    \"outDir\": \"build\",\n    \"rootDir\": \"src\",\n    \"strict\": true,\n    \"esModuleInterop\": true,\n    \"skipLibCheck\": true\n  },\n  \"include\": [\"src/**/*.ts\"]\n}\n```\n\nStore the key in an environment variable:\n\n```\nexport DEVTO_API_KEY=\"your-key-here\"\n```\n\nIn PowerShell:\n\n```\n$env:DEVTO_API_KEY = \"your-key-here\"\n```\n\nDo not commit the key, place it in prompts, or return it from a tool. The MCP server should read it directly from its environment.\n\nDEV currently recommends API v1. Requests use the `api-key`\n\nheader and this media type:\n\n```\napplication/vnd.forem.api-v1+json\n```\n\nCreate `src/index.ts`\n\n:\n\n``` js\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport { z } from \"zod\";\n\nconst apiKey = process.env.DEVTO_API_KEY;\n\nif (!apiKey) {\n  throw new Error(\"DEVTO_API_KEY is required\");\n}\n\nconst API_BASE = \"https://dev.to/api\";\n\nasync function devRequest<T>(\n  path: string,\n  init: RequestInit = {},\n): Promise<T> {\n  const response = await fetch(`${API_BASE}${path}`, {\n    ...init,\n    headers: {\n      Accept: \"application/vnd.forem.api-v1+json\",\n      \"Content-Type\": \"application/json\",\n      \"api-key\": apiKey,\n      ...init.headers,\n    },\n  });\n\n  if (!response.ok) {\n    const details = await response.text();\n    throw new Error(\n      `DEV API returned ${response.status}: ${details}`,\n    );\n  }\n\n  return response.json() as Promise<T>;\n}\n\nconst server = new McpServer({\n  name: \"devto\",\n  version: \"0.1.0\",\n});\n```\n\nThis helper centralizes authentication, API versioning, and error handling. It also prevents every tool from implementing headers differently.\n\nThe authenticated API provides separate endpoints for published, unpublished, and all articles.\n\n```\nserver.tool(\n  \"list_my_articles\",\n  \"List articles owned by the authenticated DEV user\",\n  {\n    status: z.enum([\"published\", \"unpublished\", \"all\"])\n      .default(\"published\"),\n    page: z.number().int().positive().default(1),\n  },\n  async ({ status, page }) => {\n    const articles = await devRequest<Array<{\n      id: number;\n      title: string;\n      url?: string;\n      published_at?: string;\n      published: boolean;\n    }>>(\n      `/articles/me/${status}?page=${page}&per_page=30`,\n    );\n\n    return {\n      content: [{\n        type: \"text\",\n        text: JSON.stringify(articles, null, 2),\n      }],\n    };\n  },\n);\n```\n\nUsing the authenticated `/articles/me/*`\n\nendpoints is better than guessing a username. It also lets the server retrieve drafts, which public profile endpoints cannot do.\n\n```\nserver.tool(\n  \"get_article\",\n  \"Get a DEV article by numeric ID\",\n  {\n    id: z.number().int().positive(),\n  },\n  async ({ id }) => {\n    const article = await devRequest<unknown>(`/articles/${id}`);\n\n    return {\n      content: [{\n        type: \"text\",\n        text: JSON.stringify(article, null, 2),\n      }],\n    };\n  },\n);\n```\n\nReturning structured JSON as text is simple and works across clients. For a production server, you can additionally provide structured content when your target clients support it.\n\nHere is the most important tool in the tutorial:\n\n```\nserver.tool(\n  \"create_article_draft\",\n  \"Create an unpublished DEV article draft\",\n  {\n    title: z.string().min(1).max(128),\n    body_markdown: z.string().min(1),\n    tags: z.array(z.string()).max(4).default([]),\n  },\n  async ({ title, body_markdown, tags }) => {\n    const article = await devRequest<unknown>(\"/articles\", {\n      method: \"POST\",\n      body: JSON.stringify({\n        article: {\n          title,\n          body_markdown,\n          tags,\n          published: false,\n        },\n      }),\n    });\n\n    return {\n      content: [{\n        type: \"text\",\n        text: JSON.stringify(article, null, 2),\n      }],\n    };\n  },\n);\n```\n\nNotice that the tool does **not** accept a `published`\n\nargument. It always sends `published: false`\n\n.\n\nThat is deliberate capability design. A prompt saying “please do not publish” is weaker than a tool that simply cannot publish. If you later add publishing, make it a separate tool with a clear name and require explicit user approval in the client workflow.\n\nComplete the file with:\n\n``` js\nconst transport = new StdioServerTransport();\nawait server.connect(transport);\n```\n\nThen build it:\n\n```\nnpm run build\n```\n\nFor STDIO servers, do not use `console.log()`\n\n. Standard output carries MCP protocol messages, so ordinary logs can corrupt the connection. Use `console.error()`\n\nfor diagnostics.\n\nThe exact configuration file depends on the client, but the process definition usually looks like this:\n\n```\n{\n  \"mcpServers\": {\n    \"devto\": {\n      \"command\": \"node\",\n      \"args\": [\"/absolute/path/devto-mcp/build/index.js\"],\n      \"env\": {\n        \"DEVTO_API_KEY\": \"your-key-here\"\n      }\n    }\n  }\n}\n```\n\nPrefer a secret manager or inherited environment variable when your client supports one. A plain configuration file containing the key should never be committed.\n\nRestart the client and verify that these tools appear:\n\n```\nlist_my_articles\nget_article\ncreate_article_draft\n```\n\nTry this prompt:\n\n```\nList my published DEV articles. Then prepare a short tutorial\nabout this MCP integration and save it as a draft.\nDo not publish it.\n```\n\nThe assistant should:\n\n`list_my_articles`\n\n;`create_article_draft`\n\n;Open your DEV dashboard and review the draft manually. Check the title, tags, links, code blocks, and claims before publishing.\n\nThis minimal implementation is useful, but a public MCP server should add:\n\nYou should also avoid returning full article bodies when a title-and-ID summary is enough. Smaller tool responses reduce noise and make the assistant's next decision easier.\n\nMCP is not only about giving an AI “more tools.” It is about designing a controlled interface between natural-language intent and real systems.\n\nFor DEV.to, that means:\n\nA good integration makes the safe path the easy path. Start with reading. Add draft creation. Review the behavior. Only then consider publishing or updating public content.\n\nIf you build your own DEV.to MCP server, I would love to hear which tools you expose—and which actions you intentionally leave out.", "url": "https://wpnews.pro/news/turn-dev-to-into-an-ai-tool-build-your-own-mcp-server", "canonical_source": "https://dev.to/paladini/turn-devto-into-an-ai-tool-build-your-own-mcp-server-57b6", "published_at": "2026-07-20 21:12:35+00:00", "updated_at": "2026-07-20 21:31:48.469845+00:00", "lang": "en", "topics": ["developer-tools", "ai-agents", "artificial-intelligence"], "entities": ["DEV Community", "Model Context Protocol", "TypeScript"], "alternates": {"html": "https://wpnews.pro/news/turn-dev-to-into-an-ai-tool-build-your-own-mcp-server", "markdown": "https://wpnews.pro/news/turn-dev-to-into-an-ai-tool-build-your-own-mcp-server.md", "text": "https://wpnews.pro/news/turn-dev-to-into-an-ai-tool-build-your-own-mcp-server.txt", "jsonld": "https://wpnews.pro/news/turn-dev-to-into-an-ai-tool-build-your-own-mcp-server.jsonld"}}