Back to Browse

Velog MCP Server

Developer ToolsLow Risk10.0MCP RegistryLocal
Free

Server data from the Official MCP Registry

Read and write Velog (velog.io), the Korean developer blog. Public publishing is opt-in.

About

Read and write Velog (velog.io), the Korean developer blog. Public publishing is opt-in.

Security Report

10.0
Low Risk10.0Low Risk

Valid MCP server (2 strong, 2 medium validity signals). No known CVEs in dependencies. Package registry verified. Imported from the Official MCP Registry.

4 files analyzed · 1 issue found

Security scores are indicators to help you make informed decisions, not guarantees. Always review permissions before connecting any MCP server.

Permissions Required

This plugin requests these system permissions. Most are normal for its category.

file_system

Check that this permission is expected for this type of plugin.

HTTP Network Access

Connects to external APIs or services over the internet.

Shell Command Execution

Runs commands on your machine. Be cautious — only use if you trust this plugin.

What You'll Need

Set these up before or after installing:

Velog access token. Either token enables writing; this one expires sooner.Required

Environment variable: VELOG_ACCESS_TOKEN

Velog refresh token. Either token enables writing; this one lasts 30 days.Required

Environment variable: VELOG_REFRESH_TOKEN

Set to 1 to allow public publishing. Off by default; the model cannot set this.Optional

Environment variable: VELOG_ALLOW_PUBLIC

Set to 1 to allow editing profile fields. Reading the profile always works.Optional

Environment variable: VELOG_ALLOW_PROFILE

Path to a local Chrome binary, used for diagram and cover rendering.Optional

Environment variable: VELOG_CHROME_PATH

How to Install

Add this to your MCP configuration file:

{
  "mcpServers": {
    "io-github-milcho0604-velog": {
      "env": {
        "VELOG_CHROME_PATH": "your-velog-chrome-path-here",
        "VELOG_ACCESS_TOKEN": "your-velog-access-token-here",
        "VELOG_ALLOW_PUBLIC": "your-velog-allow-public-here",
        "VELOG_ALLOW_PROFILE": "your-velog-allow-profile-here",
        "VELOG_REFRESH_TOKEN": "your-velog-refresh-token-here"
      },
      "args": [
        "-y",
        "@milcho0604/velog-mcp"
      ],
      "command": "npx"
    }
  }
}

Documentation

View on GitHub

From the project's GitHub README.

velog-mcp

npm Node License Runtime deps

An MCP server for Velog, the Korean developer blogging platform. Read your blog, draft posts, publish them, and back everything up — from Claude or any MCP client.

한국어 문서 →


Why another one?

Two Velog MCP servers already exist. This one differs in three ways.

1. Publishing is a permission, not a default. Out of the box the server can create drafts and publish privately. Public publishing requires you to set an environment variable. The model cannot flip that switch — only you can, in your MCP config.

2. Every quirk is measured, not assumed. Velog's GraphQL API is undocumented. This repo records what it actually does, verified against velog-io/velog source and live calls. Six server-side quirks are written up in docs/api-reference.md — including one that silently returns an empty list, and one that can turn your published posts private.

3. Two runtime dependencies. @modelcontextprotocol/sdk and zod. HTTP, test runner, and TypeScript execution all come from Node itself.


Install

Requires Node.js 22.18 or newer. What runs is the compiled dist/index.js, but development and verification execute .ts directly, and 22.18 is the first release where that works without a flag. CI covers 22.18, 24 and 26.

As a Claude Code plugin (recommended)

/plugin marketplace add milcho0604/velog-mcp
/plugin install velog@milcho

Installation asks for four values. Leave them all blank and it still installs, running read-only.

PromptIf left blank
Velog refresh tokenRead-only (browse, search, stats still work)
Allow public publishingDrafts and private publishing only
Allow profile editsProfile tools stay off
Chrome pathFound automatically in standard locations

The token goes into the macOS Keychain, not into a settings file in plaintext. Only values declared sensitive: true reach the Keychain, and a test enforces that declaration (P7).

Change values later with /plugin manage.

As a plain MCP server

Published on npm, so nothing to clone — your MCP client runs it via npx. See Configure for the config block and the client-support note.

claude mcp add velog -e VELOG_REFRESH_TOKEN=your_refresh_token \
  -- npx -y @milcho0604/velog-mcp@0.6.1

The token stays in your client's config file here. The plugin route above puts it in the Keychain instead.

From source

git clone https://github.com/milcho0604/velog-mcp.git
cd velog-mcp
npm install && npm run build

Configure

Add this to your MCP client config (claude_desktop_config.json, .mcp.json, …):

{
  "mcpServers": {
    "velog": {
      "command": "npx",
      "args": ["-y", "@milcho0604/velog-mcp@0.6.1"],
      "env": {
        "VELOG_REFRESH_TOKEN": "your_refresh_token"
      }
    }
  }
}

With the Claude Code CLI:

claude mcp add velog -e VELOG_REFRESH_TOKEN=your_refresh_token \
  -- npx -y @milcho0604/velog-mcp@0.6.1

To run a local checkout instead, swap the command for node /absolute/path/to/velog-mcp/dist/index.js.

Which clients can run this? This is a stdio server: the client starts it as a local process. That works in Claude Code, Claude Desktop, Cursor, and other clients that run MCP servers locally. It does not work in the claude.ai or ChatGPT web apps — both accept only remote MCP servers reachable over HTTP, since the connection originates from their servers rather than your machine. Using it there would mean hosting it publicly and handing your Velog token to that deployment, which defeats the point of keeping the token on your own machine.

Getting your token

Velog has no public write API, so the server authenticates with your browser session cookie.

  1. Log in at velog.io
  2. Open DevTools (F12) → ApplicationCookieshttps://velog.io
  3. Copy the value of refresh_token

VELOG_REFRESH_TOKEN alone is enough. Velog's server reissues the short-lived access_token on its own (authPlugin.mts), and this server picks the refreshed cookie out of the response. One paste lasts 30 days.

VELOG_ACCESS_TOKEN also works but expires in about an hour by itself.

Tokens are read from the environment only. They are never written to disk, and the server never reads your browser's cookie database or your OS keychain. Whatever you put in your MCP config file does live there in plain text, though — that file is yours to protect.

Without a token the server still starts, read-only. Public posts, search, trending, and blog stats all work unauthenticated.


Permissions

EnvironmentWhat you get
(nothing set)Read everything · create drafts · publish privately · draw and upload images — 21 tools
VELOG_ALLOW_PUBLIC=1…plus public publishing (adds an is_private parameter)
VELOG_ALLOW_PROFILE=1…plus profile editing (adds 5 tools)

The two switches are independent — enable either, both, or neither.

"env": {
  "VELOG_REFRESH_TOKEN": "...",
  "VELOG_ALLOW_PUBLIC": "1",
  "VELOG_ALLOW_PROFILE": "1"
}

Accepted as "on": 1, true, yes, on. Anything else is off — a typo won't quietly enable it.

When public publishing is off, the is_private parameter does not exist on any tool, so the model has no way to ask for it. When it's on, is_private appears and still defaults to true.

Why private-by-default

Not caution for its own sake. Velog's rate limiter counts only is_private: false posts:

// apps/server/src/services/PostApiService/index.mts
count({ where: { fk_user_id, is_private: false, released_at: { gt: fiveMinutesAgo } } })
if (count >= 10) {
  updateMany({ where: { fk_user_id, released_at: { gt: fiveMinutesAgo } },
               data: { is_private: true } })   // flips *everything* recent to private
}

Private posts don't increment that count. But isPostLimitReached() runs unconditionally, before privacy is examined — so if ten public posts already exist in the last five minutes, even a private draft request can trigger the sweep. "Doesn't increment" is not "can't trigger." That's why write retries stay disabled and the local limiter stays in place.

Public posts do increment it, and once a post is public it has already gone out through RSS, search indexes, and subscriber email, none of which a delete reaches. That asymmetry is what deserves an explicit opt-in.

Full reasoning: docs/security.md


Tools

21 tools. Only 9 of them change anything on Velog.

Reading — no auth required

ToolPurpose
velog_get_postRead one post, body included
velog_list_postsA user's posts, optionally filtered by tag
velog_search_postsKeyword search; pass username to search inside one blog
velog_trending_postsTrending by day / week / month / year
velog_recent_postsNewest posts across Velog
velog_get_userProfile, follower counts, bio
velog_list_seriesA user's series, with post counts and IDs
velog_user_tagsTags a user writes about, with counts

Reading — auth required

ToolPurpose
velog_whoamiWhich account the token belongs to (also a token health check)
velog_list_draftsYour saved drafts, with IDs

Derived — things Velog doesn't provide

ToolPurpose
velog_blog_statsAggregate views/likes/comments, top posts, per-year and per-tag breakdown
velog_export_postsSave posts as Markdown files with YAML front matter

Writing

ToolEffect
velog_create_draftSave a draft. Never publishes, under any configuration
velog_update_draftReplace a draft entirely — omitted fields are reset
velog_publish_postPublish a new post
velog_publish_draftPublish an existing draft, reusing its stored body
velog_unpublish_postSend a published post back to drafts
velog_update_postEdit a published post — omitted fields are kept

velog_update_draft resets what you omit; velog_update_post preserves it. The asymmetry is deliberate — see docs/tools.md.

Automatic thumbnail

Omit thumbnail and the first image in the body becomes the thumbnail, so list and share cards aren't text-only. What was chosen is always reported back, along with the other candidates when there is more than one.

thumbnailBehaviour
omittedfirst image in the body
a URLused as given
nullopt out — leave it empty on purpose

Images inside code fences and inline code are excluded, so a markdown example never becomes your thumbnail. velog_update_post never replaces an existing thumbnail — editing a title should not change the card. There, null means "don't fill it in", not "delete it".

Series — by name, in one call

Pass series_name and the server resolves it before saving, then sends the id in the same request — writing and filing happen in one call. Names are matched ignoring case and surrounding whitespace; series_id wins if you know it.

⚠️ If the name isn't found, nothing is written — saving without the series would look like it worked. The available series are listed in the error.

Omit both and the result carries your series list. That lookup never fails the write (cancellation included — reporting failure after a successful save makes retries duplicate the post).

⚠️ The velog API cannot create a series — there is no series mutation, and WritePostInput only accepts series_id. Create one on velog once, then this server can attach posts to it.

Tools that take a usernamevelog_list_drafts, velog_blog_stats, velog_export_posts, velog_search_posts — fall back to your own account when you omit it.

Diagrams and images

ToolEffect
velog_render_diagramDraw an architecture/flow diagram and upload it
velog_render_coverDraw a 1200×630 cover card for a post
velog_upload_imageUpload a local image file, get the Markdown back

You describe what exists and what flows where; the renderer owns everything else — palette, spacing, text measurement, corner rounding, canvas size. That is deliberate: a diagram redrawn from scratch each time looks different each time.

Every measurement is real. Node widths and line breaks come from the browser's getBBox(), never from a character count — with mixed Korean and English text, counting characters is wrong every time. The canvas is sized after drawing, from the content's bounding box, so a diagram cannot be clipped.

Then it audits itself and reports five classes of defect:

text spilling outside its card · letter-spacing squeezed to fit
a line crossing (or hiding behind) a node
two lines overlapping · two nodes overlapping · a label sitting on a card

If the audit finds anything, nothing is uploaded — and there is no flag to turn that off. Velog has no delete-image API and every upload counts against your quota, so a flawed diagram is worth redrawing rather than shipping. An override that the model can set itself is not a safeguard (same reasoning as the publishing switch in ADR 0004). If you really want a flawed diagram online, render with upload: false, look at the PNG, then pass its path to velog_upload_image.

Icons are 28 built-in glyphs (server, database, cloud, clock, alert, …) drawn from primitive shapes. Nothing is fetched — the renderer runs with DNS disabled.

Requires Chrome (or any Chromium-based browser: Edge, Brave, Chromium). It is found automatically on macOS/Linux/Windows; set VELOG_CHROME_PATH if yours lives elsewhere. Only velog_render_diagram and velog_render_cover need it — velog_upload_image just reads a local file, so it and the other 18 tools work without a browser.

Cost, measured: one diagram is ~1 GB peak across 9–11 Chrome processes for 3–4 seconds, then back to zero. That's Chrome's floor, not our content. Coordinates, text lengths and array sizes are all bounded, and the canvas cap (6000px / 9M px) is enforced inside the page — a browser commits to a surface the moment it receives width and height, so checking after the fact is too late. Renders are serialized — MCP clients call tools in parallel, and without that a five-diagram request would mean 45 Chrome processes and 6 GB. Serialized, four concurrent requests still peak at one render's worth. Ten renders in a row show no accumulation.

Profile editing — VELOG_ALLOW_PROFILE=1

Five more tools appear: velog_update_profile (display name, bio), velog_update_about, velog_update_blog_title, velog_update_social_links, velog_update_profile_image. Without the flag they aren't registered at all.

The gate isn't about danger — these are reversible, affect only your own account, and aren't distributed anywhere. It's about confusion: a profile's short_bio and a post's short_description sound alike. "Fix my description" is ambiguous, and with the switch off a wrong guess can't reach your profile.

velog_update_profile keeps what you omit. Velog's UpdateProfileInput requires both display_name and short_bio, so sending one alone would blank the other — the tool reads your current values and fills them in.


Usage

Once it's configured, just talk to your MCP client.

"Draft a Velog post about the bug I fixed today"
   → writes Markdown, saves it as a draft, hands back the edit URL

"What did I write about HTTP/2 last year?"
   → searches inside your own posts

"Show my top 10 posts by views, and which tags get read most"
   → walks your whole blog and aggregates

"Back up all my posts to ~/blog-backup"
   → writes .md files with front matter

"Publish that draft"
   → private by default; public only with VELOG_ALLOW_PUBLIC=1

"Draw how the request flows from the LB through the workers to Redis"
   → renders a diagram, audits it, uploads it, hands back the Markdown line

"Make a cover image for this post"
   → 1200×630 card; pass the URL to velog_update_post's thumbnail

Your MCP client asks for approval before each tool call, and irreversible tools carry destructiveHint, so nothing gets published without you seeing it first.

Exported file format

---
title: "Post title"
date: 2022-12-31T18:32:39.790Z
slug: "url-slug"
url: "https://velog.io/@username/url-slug"
tags: ["tag1", "tag2"]
likes: 260
views: 16323
---

Post body in Markdown…

Development

npm test              # node:test, runs .ts directly — no jest, no ts-node
npm run typecheck     # includes tests — they used to be excluded, which hid real errors
npm run lint          # typescript-eslint, type-aware
npm run build         # tsconfig.build.json (tests excluded from dist)
npm run schema:dump   # dump Velog's current GraphQL schema

276 tests. src/__tests__/safety.test.ts pins the security invariants (A1–A11), render.test.ts pins the drawing ones (R1–R23), and plugin.test.ts pins the packaging ones (P1–P26) — if any fails, find out why instead of working around it.

Every guard here was checked by breaking it on purpose: 54 mutations against the source, plus 12 against the publish gate itself (scripts/gate-mutation.sh), each of which must make exactly one check fail. A test that still passes with the guard removed is not a test. Several in this repo did pass at first, and that is how they got fixed.

Documentation

DocumentContents
docs/PRD.mdGoals, non-goals, success criteria
docs/architecture.mdLayering, and the TypeScript subset Node's type stripping allows
docs/api-reference.mdMeasured Velog GraphQL schema and server quirks
docs/security.mdToken handling, capability model, what's deliberately unimplemented
docs/tools.mdFull tool catalog with gotchas
docs/decisions/Architecture decision records
CHANGELOG.mdWhat was broken and what got fixed, per release

Notes

This talks to Velog's internal GraphQL API, which is undocumented and can change without warning. When something breaks, run npm run schema:dump and diff it against docs/api-reference.md — that's the fastest way to find what moved.

Velog's terms of service contain no clause restricting automated access. Using your own token to manage your own posts stays within scope, and your posts remain yours (Article 5).

License

MIT

Reviews

No reviews yet

Be the first to review this server!