cd /news/developer-tools/turn-dev-to-into-an-ai-tool-build-yo… · home topics developer-tools article
[ARTICLE · art-66101] src=dev.to ↗ pub= topic=developer-tools verified=true sentiment=· neutral

Turn DEV.to Into an AI Tool: Build Your Own MCP Server

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.

read5 min views2 publishedJul 20, 2026

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?

That 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.

The important safety rule comes first:

Let the assistant create

draftsby default. Publishing should always be an explicit decision.

Our MCP server will sit between an MCP-compatible AI client and the DEV/Forem API:

AI assistant
     |
     | MCP tool call
     v
Local DEV.to MCP server
     |
     | HTTPS + API key
     v
DEV Community API

We will expose three tools:

list_my_articles

— retrieve your published or unpublished posts;get_article

— inspect an article by ID;create_article_draft

— 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.

You will need:

Create a project and install the official TypeScript SDK:

mkdir devto-mcp
cd devto-mcp
npm init -y
npm install @modelcontextprotocol/sdk zod@3
npm install -D typescript @types/node

Add "type": "module"

to package.json

, plus a build command:

{
  "type": "module",
  "scripts": {
    "build": "tsc"
  }
}

A minimal tsconfig.json

:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "build",
    "rootDir": "src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  },
  "include": ["src/**/*.ts"]
}

Store the key in an environment variable:

export DEVTO_API_KEY="your-key-here"

In PowerShell:

$env:DEVTO_API_KEY = "your-key-here"

Do not commit the key, place it in prompts, or return it from a tool. The MCP server should read it directly from its environment.

DEV currently recommends API v1. Requests use the api-key

header and this media type:

application/vnd.forem.api-v1+json

Create src/index.ts

:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

const apiKey = process.env.DEVTO_API_KEY;

if (!apiKey) {
  throw new Error("DEVTO_API_KEY is required");
}

const API_BASE = "https://dev.to/api";

async function devRequest<T>(
  path: string,
  init: RequestInit = {},
): Promise<T> {
  const response = await fetch(`${API_BASE}${path}`, {
    ...init,
    headers: {
      Accept: "application/vnd.forem.api-v1+json",
      "Content-Type": "application/json",
      "api-key": apiKey,
      ...init.headers,
    },
  });

  if (!response.ok) {
    const details = await response.text();
    throw new Error(
      `DEV API returned ${response.status}: ${details}`,
    );
  }

  return response.json() as Promise<T>;
}

const server = new McpServer({
  name: "devto",
  version: "0.1.0",
});

This helper centralizes authentication, API versioning, and error handling. It also prevents every tool from implementing headers differently.

The authenticated API provides separate endpoints for published, unpublished, and all articles.

server.tool(
  "list_my_articles",
  "List articles owned by the authenticated DEV user",
  {
    status: z.enum(["published", "unpublished", "all"])
      .default("published"),
    page: z.number().int().positive().default(1),
  },
  async ({ status, page }) => {
    const articles = await devRequest<Array<{
      id: number;
      title: string;
      url?: string;
      published_at?: string;
      published: boolean;
    }>>(
      `/articles/me/${status}?page=${page}&per_page=30`,
    );

    return {
      content: [{
        type: "text",
        text: JSON.stringify(articles, null, 2),
      }],
    };
  },
);

Using the authenticated /articles/me/*

endpoints is better than guessing a username. It also lets the server retrieve drafts, which public profile endpoints cannot do.

server.tool(
  "get_article",
  "Get a DEV article by numeric ID",
  {
    id: z.number().int().positive(),
  },
  async ({ id }) => {
    const article = await devRequest<unknown>(`/articles/${id}`);

    return {
      content: [{
        type: "text",
        text: JSON.stringify(article, null, 2),
      }],
    };
  },
);

Returning 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.

Here is the most important tool in the tutorial:

server.tool(
  "create_article_draft",
  "Create an unpublished DEV article draft",
  {
    title: z.string().min(1).max(128),
    body_markdown: z.string().min(1),
    tags: z.array(z.string()).max(4).default([]),
  },
  async ({ title, body_markdown, tags }) => {
    const article = await devRequest<unknown>("/articles", {
      method: "POST",
      body: JSON.stringify({
        article: {
          title,
          body_markdown,
          tags,
          published: false,
        },
      }),
    });

    return {
      content: [{
        type: "text",
        text: JSON.stringify(article, null, 2),
      }],
    };
  },
);

Notice that the tool does not accept a published

argument. It always sends published: false

.

That 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.

Complete the file with:

const transport = new StdioServerTransport();
await server.connect(transport);

Then build it:

npm run build

For STDIO servers, do not use console.log()

. Standard output carries MCP protocol messages, so ordinary logs can corrupt the connection. Use console.error()

for diagnostics.

The exact configuration file depends on the client, but the process definition usually looks like this:

{
  "mcpServers": {
    "devto": {
      "command": "node",
      "args": ["/absolute/path/devto-mcp/build/index.js"],
      "env": {
        "DEVTO_API_KEY": "your-key-here"
      }
    }
  }
}

Prefer a secret manager or inherited environment variable when your client supports one. A plain configuration file containing the key should never be committed.

Restart the client and verify that these tools appear:

list_my_articles
get_article
create_article_draft

Try this prompt:

List my published DEV articles. Then prepare a short tutorial
about this MCP integration and save it as a draft.
Do not publish it.

The assistant should:

list_my_articles

;create_article_draft

;Open your DEV dashboard and review the draft manually. Check the title, tags, links, code blocks, and claims before publishing.

This minimal implementation is useful, but a public MCP server should add:

You 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.

MCP is not only about giving an AI “more tools.” It is about designing a controlled interface between natural-language intent and real systems.

For DEV.to, that means:

A 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.

If you build your own DEV.to MCP server, I would love to hear which tools you expose—and which actions you intentionally leave out.

── more in #developer-tools 4 stories · sorted by recency
── more on @dev community 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/turn-dev-to-into-an-…] indexed:0 read:5min 2026-07-20 ·