@milcho0604/velog-mcp 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/LICENSE +21 -0
  2. package/README.ko.md +366 -0
  3. package/README.md +381 -0
  4. package/dist/auth.d.ts +57 -0
  5. package/dist/auth.js +124 -0
  6. package/dist/auth.js.map +1 -0
  7. package/dist/capabilities.d.ts +50 -0
  8. package/dist/capabilities.js +60 -0
  9. package/dist/capabilities.js.map +1 -0
  10. package/dist/client.d.ts +113 -0
  11. package/dist/client.js +322 -0
  12. package/dist/client.js.map +1 -0
  13. package/dist/format.d.ts +31 -0
  14. package/dist/format.js +66 -0
  15. package/dist/format.js.map +1 -0
  16. package/dist/graphql.d.ts +29 -0
  17. package/dist/graphql.js +82 -0
  18. package/dist/graphql.js.map +1 -0
  19. package/dist/index.d.ts +25 -0
  20. package/dist/index.js +149 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/me.d.ts +23 -0
  23. package/dist/me.js +35 -0
  24. package/dist/me.js.map +1 -0
  25. package/dist/ownership.d.ts +42 -0
  26. package/dist/ownership.js +62 -0
  27. package/dist/ownership.js.map +1 -0
  28. package/dist/plugin-env.d.ts +67 -0
  29. package/dist/plugin-env.js +102 -0
  30. package/dist/plugin-env.js.map +1 -0
  31. package/dist/ratelimit.d.ts +48 -0
  32. package/dist/ratelimit.js +79 -0
  33. package/dist/ratelimit.js.map +1 -0
  34. package/dist/render/chrome.d.ts +77 -0
  35. package/dist/render/chrome.js +287 -0
  36. package/dist/render/chrome.js.map +1 -0
  37. package/dist/render/cover.d.ts +29 -0
  38. package/dist/render/cover.js +195 -0
  39. package/dist/render/cover.js.map +1 -0
  40. package/dist/render/icons.d.ts +22 -0
  41. package/dist/render/icons.js +158 -0
  42. package/dist/render/icons.js.map +1 -0
  43. package/dist/render/index.d.ts +32 -0
  44. package/dist/render/index.js +137 -0
  45. package/dist/render/index.js.map +1 -0
  46. package/dist/render/page.d.ts +89 -0
  47. package/dist/render/page.js +761 -0
  48. package/dist/render/page.js.map +1 -0
  49. package/dist/render/tones.d.ts +30 -0
  50. package/dist/render/tones.js +46 -0
  51. package/dist/render/tones.js.map +1 -0
  52. package/dist/slug.d.ts +42 -0
  53. package/dist/slug.js +79 -0
  54. package/dist/slug.js.map +1 -0
  55. package/dist/tools/discover.d.ts +6 -0
  56. package/dist/tools/discover.js +106 -0
  57. package/dist/tools/discover.js.map +1 -0
  58. package/dist/tools/drafts.d.ts +23 -0
  59. package/dist/tools/drafts.js +227 -0
  60. package/dist/tools/drafts.js.map +1 -0
  61. package/dist/tools/export.d.ts +21 -0
  62. package/dist/tools/export.js +132 -0
  63. package/dist/tools/export.js.map +1 -0
  64. package/dist/tools/images.d.ts +34 -0
  65. package/dist/tools/images.js +556 -0
  66. package/dist/tools/images.js.map +1 -0
  67. package/dist/tools/posts.d.ts +14 -0
  68. package/dist/tools/posts.js +82 -0
  69. package/dist/tools/posts.js.map +1 -0
  70. package/dist/tools/profile-edit.d.ts +15 -0
  71. package/dist/tools/profile-edit.js +216 -0
  72. package/dist/tools/profile-edit.js.map +1 -0
  73. package/dist/tools/profile.d.ts +9 -0
  74. package/dist/tools/profile.js +133 -0
  75. package/dist/tools/profile.js.map +1 -0
  76. package/dist/tools/publish.d.ts +16 -0
  77. package/dist/tools/publish.js +424 -0
  78. package/dist/tools/publish.js.map +1 -0
  79. package/dist/tools/stats.d.ts +32 -0
  80. package/dist/tools/stats.js +154 -0
  81. package/dist/tools/stats.js.map +1 -0
  82. package/dist/types.d.ts +42 -0
  83. package/dist/types.js +3 -0
  84. package/dist/types.js.map +1 -0
  85. package/docs/PRD.md +146 -0
  86. package/docs/api-reference.md +329 -0
  87. package/docs/architecture.md +112 -0
  88. package/docs/decisions/0001-why-build-our-own.md +89 -0
  89. package/docs/decisions/0002-draft-only-write.md +84 -0
  90. package/docs/decisions/0003-token-env-only.md +109 -0
  91. package/docs/decisions/0004-capability-model.md +123 -0
  92. package/docs/decisions/0005-render-in-server.md +117 -0
  93. package/docs/decisions/0006-ship-as-plugin.md +532 -0
  94. package/docs/security.md +384 -0
  95. package/docs/tools.md +404 -0
  96. package/npm-shrinkwrap.json +2345 -0
  97. package/package.json +61 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Changhyun Kim
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.ko.md ADDED
@@ -0,0 +1,366 @@
1
+ # velog-mcp
2
+
3
+ [![Node](https://img.shields.io/badge/node-%3E%3D24-brightgreen)](https://nodejs.org)
4
+ [![License](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
5
+ [![Runtime deps](https://img.shields.io/badge/runtime%20deps-2-lightgrey)](package.json)
6
+
7
+ [벨로그](https://velog.io)를 Claude 같은 MCP 클라이언트에서 다루는 서버.
8
+ 글을 읽고, 초안을 쓰고, 발행하고, 통째로 백업한다.
9
+
10
+ **[English →](README.md)**
11
+
12
+ ---
13
+
14
+ ## 왜 또 만들었나
15
+
16
+ 벨로그 MCP 서버가 이미 둘 있다. 이 구현은 세 가지가 다르다.
17
+
18
+ **1. 발행은 기본값이 아니라 권한이다.**
19
+ 설치 직후에는 초안 작성과 **비공개 발행**까지 된다. 공개 발행은 환경변수를 넣어야
20
+ 열린다. 그 스위치는 모델이 못 건드린다 — MCP 설정 파일을 여는 사람만 바꿀 수 있다.
21
+
22
+ **2. 벨로그 동작을 추측하지 않고 실측했다.**
23
+ 벨로그 GraphQL 은 비공식이라 문서가 없다. 이 레포는 **실제로 어떻게 동작하는지**를
24
+ [velog-io/velog](https://github.com/velog-io/velog) 소스와 실호출로 확인해
25
+ 기록한다. 서버 쪽 함정 6가지가 [docs/api-reference.md](docs/api-reference.md) 에
26
+ 있다 — 오류 없이 빈 결과를 주는 경우, 발행글을 비공개로 만드는 경우 포함.
27
+
28
+ **3. 런타임 의존성 2개.** `@modelcontextprotocol/sdk` 와 `zod` 뿐이다.
29
+ HTTP·테스트 러너·타입스크립트 실행은 전부 Node 24 내장을 쓴다.
30
+
31
+ ---
32
+
33
+ ## 설치
34
+
35
+ **Node.js 24 이상**이 필요하다.
36
+
37
+ ### Claude Code 플러그인으로 (권장)
38
+
39
+ ```bash
40
+ /plugin marketplace add milcho0604/velog-mcp
41
+ /plugin install velog@milcho
42
+ ```
43
+
44
+ 설치할 때 값 네 개를 묻는다. **하나도 안 넣어도 설치되고, 읽기 전용으로 동작한다.**
45
+
46
+ | 물어보는 것 | 안 넣으면 |
47
+ | --- | --- |
48
+ | Velog refresh token | 읽기 전용 (조회·검색·통계는 그대로) |
49
+ | 공개 발행 허용 | 초안과 비공개 발행까지만 |
50
+ | 프로필 수정 허용 | 프로필 도구가 꺼짐 |
51
+ | 크롬 경로 | 표준 위치에서 자동으로 찾는다 |
52
+
53
+ **토큰이 macOS 키체인에 들어간다.** 설정 파일에 평문으로 남지 않는다 —
54
+ `sensitive: true` 로 선언한 값만 키체인으로 가고, 그건 테스트가 강제한다(P7).
55
+
56
+ 값을 나중에 바꾸려면 `/plugin manage`.
57
+
58
+ ### 직접 빌드해서
59
+
60
+ ```bash
61
+ git clone https://github.com/milcho0604/velog-mcp.git
62
+ cd velog-mcp
63
+ npm install && npm run build
64
+ ```
65
+
66
+ ## 설정
67
+
68
+ MCP 클라이언트 설정 파일(`claude_desktop_config.json`, `.mcp.json` 등)에 추가한다.
69
+
70
+ ```json
71
+ {
72
+ "mcpServers": {
73
+ "velog": {
74
+ "command": "node",
75
+ "args": ["/절대경로/velog-mcp/dist/index.js"],
76
+ "env": {
77
+ "VELOG_REFRESH_TOKEN": "여기에 토큰"
78
+ }
79
+ }
80
+ }
81
+ }
82
+ ```
83
+
84
+ Claude Code CLI 라면:
85
+
86
+ ```bash
87
+ claude mcp add velog -- node /절대경로/velog-mcp/dist/index.js
88
+ # 생성된 항목에 "env" 블록을 추가
89
+ ```
90
+
91
+ ### 토큰 얻는 법
92
+
93
+ 벨로그는 공개 쓰기 API 가 없어서 브라우저 세션 쿠키로 인증한다.
94
+
95
+ 1. [velog.io](https://velog.io) 에 로그인
96
+ 2. 개발자도구(`F12`) → **Application** → **Cookies** → `https://velog.io`
97
+ 3. **`refresh_token`** 값을 복사
98
+
99
+ **`VELOG_REFRESH_TOKEN` 하나만 넣으면 된다.** 벨로그 서버가 수명 짧은
100
+ `access_token` 을 알아서 재발급하고([`authPlugin.mts`](https://github.com/velog-io/velog/blob/main/apps/server/src/common/plugins/global/authPlugin.mts)),
101
+ 이 서버가 응답에 실려 오는 갱신 쿠키를 받아 쓴다. 한 번 넣으면 **30일** 간다.
102
+
103
+ `VELOG_ACCESS_TOKEN` 도 받지만 단독으로는 1시간이면 만료된다.
104
+
105
+ > 토큰은 환경변수로만 읽는다. 디스크에 쓰지 않고, 브라우저 쿠키 DB 나 OS 키체인을
106
+ > 건드리지 않는다. 다만 MCP 설정 파일에 적은 값은 그 파일에 평문으로 남는다 —
107
+ > 그 파일 관리는 사용자 몫이다.
108
+
109
+ **토큰이 없어도 서버는 뜬다.** 읽기 전용으로 동작하고, 공개 글 조회·검색·트렌딩·
110
+ 블로그 통계는 인증 없이 된다.
111
+
112
+ ---
113
+
114
+ ## 권한
115
+
116
+ | 환경변수 | 되는 것 |
117
+ | --- | --- |
118
+ | *(설정 없음)* | 전체 읽기 · 초안 작성 · **비공개 발행** · 그림 생성·업로드 — 도구 21개 |
119
+ | `VELOG_ALLOW_PUBLIC=1` | …**공개 발행** 추가 (`is_private` 파라미터가 생김) |
120
+ | `VELOG_ALLOW_PROFILE=1` | …**프로필 수정** 추가 (도구 5개) |
121
+
122
+ 두 스위치는 독립이다 — 하나만 켜도 되고 둘 다 켜도 된다.
123
+
124
+ ```json
125
+ "env": {
126
+ "VELOG_REFRESH_TOKEN": "...",
127
+ "VELOG_ALLOW_PUBLIC": "1",
128
+ "VELOG_ALLOW_PROFILE": "1"
129
+ }
130
+ ```
131
+
132
+ '켬'으로 인정하는 값은 `1`, `true`, `yes`, `on` 뿐이다. 나머지는 전부 꺼짐 —
133
+ 오타로 조용히 켜지지 않는다.
134
+
135
+ 공개 발행이 꺼져 있으면 어떤 도구에도 `is_private` 파라미터가 **존재하지 않는다.**
136
+ 모델이 공개를 요청할 방법 자체가 없다. 켜면 파라미터가 생기지만 기본값은 여전히
137
+ `true`(비공개)다.
138
+
139
+ ### 왜 비공개가 기본인가
140
+
141
+ 몸사리는 게 아니라 실측 근거가 있다. 벨로그의 발행 제한은 `is_private: false` 인
142
+ 글만 센다:
143
+
144
+ ```ts
145
+ // apps/server/src/services/PostApiService/index.mts
146
+ count({ where: { fk_user_id, is_private: false, released_at: { gt: 5분전 } } })
147
+ if (count >= 10) {
148
+ updateMany({ where: { fk_user_id, released_at: { gt: 5분전 } },
149
+ data: { is_private: true } }) // 최근 글을 '전부' 비공개로
150
+ }
151
+ ```
152
+
153
+ 비공개 글은 이 계수를 **올리지 않는다.** 다만 `isPostLimitReached()` 는 공개 여부를
154
+ 보기 **전에** 무조건 실행되므로, 이미 최근 5분에 공개 글이 10건 쌓여 있으면 비공개
155
+ 초안 요청도 그 파괴 동작을 촉발할 수 있다 — '올리지 않는다'와 '유발하지 않는다'는
156
+ 다르다. 그래서 쓰기 무재시도와 자체 상한을 함께 유지한다.
157
+
158
+ 공개 글은 계수를 올리고, 한번 공개되면 RSS·검색 색인·구독 메일로 이미 나간 뒤라
159
+ 지워도 회수가 안 된다. 명시적 opt-in 을 둘 만한 비대칭은 여기에 있다.
160
+
161
+ 자세한 내용: [docs/security.md](docs/security.md)
162
+
163
+ ---
164
+
165
+ ## 도구
166
+
167
+ 21개. 벨로그 상태를 바꾸는 건 그중 9개뿐이다.
168
+
169
+ ### 읽기 — 인증 불필요
170
+
171
+ | 도구 | 하는 일 |
172
+ | --- | --- |
173
+ | `velog_get_post` | 글 하나를 본문까지 |
174
+ | `velog_list_posts` | 사용자의 글 목록, 태그로 좁힐 수 있음 |
175
+ | `velog_search_posts` | 키워드 검색. `username` 을 주면 그 블로그 안에서만 |
176
+ | `velog_trending_posts` | 트렌딩 (`day`/`week`/`month`/`year`) |
177
+ | `velog_recent_posts` | 벨로그 전체 최신 글 |
178
+ | `velog_get_user` | 프로필·팔로워 수·소개 |
179
+ | `velog_list_series` | 시리즈 목록 (글 수와 id 포함) |
180
+ | `velog_user_tags` | 사용자가 쓰는 태그와 글 수 |
181
+
182
+ ### 읽기 — 인증 필요
183
+
184
+ | 도구 | 하는 일 |
185
+ | --- | --- |
186
+ | `velog_whoami` | 토큰이 어느 계정인지 (토큰 생존 확인용으로도) |
187
+ | `velog_list_drafts` | 내 초안 목록과 id |
188
+
189
+ ### 파생 — 벨로그에 없는 기능
190
+
191
+ | 도구 | 하는 일 |
192
+ | --- | --- |
193
+ | `velog_blog_stats` | 조회수·좋아요·댓글 집계, 상위 글, 연도별·태그별 분포 |
194
+ | `velog_export_posts` | 글을 YAML 프론트매터 붙은 마크다운으로 저장 |
195
+
196
+ ### 쓰기
197
+
198
+ | 도구 | 효과 |
199
+ | --- | --- |
200
+ | `velog_create_draft` | 초안 저장. 어떤 설정에서도 발행하지 않는다 |
201
+ | `velog_update_draft` | 초안 **전체 교체** — 생략한 필드는 초기화된다 |
202
+ | `velog_publish_post` | 새 글 발행 |
203
+ | `velog_publish_draft` | 기존 초안을 발행 (저장된 본문을 그대로 씀) |
204
+ | `velog_unpublish_post` | 발행글을 초안으로 되돌림 |
205
+ | `velog_update_post` | 발행글 수정 — 생략한 필드는 **유지된다** |
206
+
207
+ > `velog_update_draft` 는 생략하면 초기화하고, `velog_update_post` 는 유지한다.
208
+ > 의도한 비대칭이고 이유는 [docs/tools.md](docs/tools.md) 에 있다.
209
+
210
+ `username` 을 받는 도구 중 `velog_list_drafts`·`velog_blog_stats`·
211
+ `velog_export_posts`·`velog_search_posts` 는 생략하면 **내 계정**을 쓴다.
212
+
213
+ ### 그림 — 다이어그램·표지
214
+
215
+ | 도구 | 효과 |
216
+ | --- | --- |
217
+ | `velog_render_diagram` | 구성도·흐름도를 그려 올린다 |
218
+ | `velog_render_cover` | 글 표지 카드(1200×630)를 만든다 |
219
+ | `velog_upload_image` | 로컬 이미지를 올리고 마크다운을 돌려준다 |
220
+
221
+ 넘기는 건 **무엇이 있고 무엇이 어디로 흐르는지**뿐이다. 색·여백·글자 실측·모서리
222
+ 라운딩·캔버스 크기는 렌더러가 쥔다. 매번 처음부터 그리면 매번 다르게 생기기 때문이다.
223
+
224
+ 수치는 전부 실측이다. 노드 폭과 줄바꿈은 브라우저 `getBBox()` 로 잰다 — 글자수로
225
+ 추정하면 한글·영문이 섞인 라벨에서 반드시 틀린다. 캔버스는 다 그린 **뒤에** 내용
226
+ bbox 로 정하므로 그림이 잘릴 수가 없다.
227
+
228
+ 그리고 스스로 감사해서 다섯 가지를 보고한다:
229
+
230
+ ```
231
+ 카드 밖으로 삐져나온 글자 · 억지로 맞추려 눌린 자간
232
+ 노드를 관통하거나 노드 뒤에 숨은 선
233
+ 선끼리 겹침 · 노드끼리 겹침 · 라벨이 카드 위에 얹힘
234
+ ```
235
+
236
+ **감사에 하나라도 걸리면 올리지 않는다. 그리고 그걸 끄는 스위치는 없다.**
237
+ 벨로그에는 이미지 삭제 API 가 없고 업로드 한도도 깎이니, 어설픈 그림은 올리는 것보다
238
+ 고쳐 그리는 게 낫다. 모델이 스스로 켤 수 있는 우회는 방어가 아니다 —
239
+ 공개 발행 스위치와 같은 이유다([ADR 0004](docs/decisions/0004-capability-model.md)).
240
+ 그래도 올려야 하면 `upload:false` 로 그린 뒤 PNG 를 눈으로 확인하고
241
+ `velog_upload_image` 에 그 경로를 준다. 사람이 한 번 더 개입하게 된다.
242
+
243
+ 아이콘은 내장 28종(`server`·`database`·`cloud`·`clock`·`alert` …)이고 전부 도형
244
+ 조합이다. 밖에서 받아오는 게 하나도 없다 — 렌더러는 DNS 를 막은 채로 돈다.
245
+
246
+ **크롬이 필요하다** (크로미움 계열이면 된다: Edge·Brave·Chromium). macOS·리눅스·
247
+ 윈도우에서 알아서 찾고, 다른 데 있으면 `VELOG_CHROME_PATH` 로 지정한다.
248
+ 이 중 브라우저를 쓰는 건 `velog_render_diagram`·`velog_render_cover` **둘뿐**이고,
249
+ `velog_upload_image` 를 포함한 나머지 19개는 크롬 없이 동작한다.
250
+
251
+ **비용은 실측해서 밝혀 둔다.** 그림 한 장에 크롬 9~11개·최대 약 1GB 를 3~4초 쓰고
252
+ 0 으로 돌아온다. 이건 크롬의 바닥이지 우리 그림 탓이 아니다.
253
+ 좌표·글자·개수에는 전부 상한이 있고, 캔버스 상한(6000px/900만px)은 **페이지 안에서**
254
+ 걸린다 — 브라우저는 크기를 받는 순간 표면을 준비하므로 바깥에서 막으면 늦다.
255
+ **렌더는 줄을 세운다** — MCP 클라이언트가 도구를 병렬로 부르기 때문에, 안 그러면
256
+ 그림 다섯 장 요청에 크롬 45개·6GB 가 된다. 줄을 세우면 동시 4회도 한 장 분량으로
257
+ 고정된다. 10회 연속에서 누적이 없는 것도 확인했다.
258
+
259
+ ### 프로필 수정 — `VELOG_ALLOW_PROFILE=1`
260
+
261
+ 도구 5개가 추가된다: `velog_update_profile`(이름·한줄소개), `velog_update_about`,
262
+ `velog_update_blog_title`, `velog_update_social_links`, `velog_update_profile_image`.
263
+ 설정이 없으면 **등록조차 되지 않는다.**
264
+
265
+ 게이트를 둔 건 위험해서가 아니다 — 전부 되돌릴 수 있고 본인 계정에만 영향이며
266
+ 어디로도 배포되지 않는다. 이유는 **혼동**이다: 프로필의 `short_bio` 와 글의
267
+ `short_description` 은 이름이 비슷하다. "소개 좀 고쳐줘" 가 어느 쪽인지 모호할 때,
268
+ 스위치가 꺼져 있으면 잘못 짚어도 프로필에 손이 닿지 않는다.
269
+
270
+ `velog_update_profile` 은 **생략한 항목을 유지한다.** 벨로그의 `UpdateProfileInput`
271
+ 은 `display_name` 과 `short_bio` 를 둘 다 필수로 받아서 한쪽만 보내면 다른 쪽이
272
+ 빈 문자열로 덮인다 — 그래서 현재 값을 읽어 채워 보낸다.
273
+
274
+ ---
275
+
276
+ ## 사용법
277
+
278
+ 설정이 끝나면 MCP 클라이언트에 그냥 말하면 된다.
279
+
280
+ ```
281
+ "오늘 고친 버그로 벨로그 초안 잡아줘"
282
+ → 마크다운을 쓰고 초안으로 저장, 편집 URL 을 준다
283
+
284
+ "작년에 HTTP/2 로 뭐 썼더라"
285
+ → 내 글 안에서 검색
286
+
287
+ "내 글 조회수 상위 10개랑 어떤 태그가 제일 많이 읽혔는지"
288
+ → 블로그 전체를 훑어 집계
289
+
290
+ "내 글 전부 ~/blog-backup 에 백업해"
291
+ → 프론트매터 붙은 .md 로 저장
292
+
293
+ "그 초안 발행해줘"
294
+ → 기본은 비공개. 공개는 VELOG_ALLOW_PUBLIC=1 이 있어야 한다
295
+
296
+ "요청이 LB 에서 워커 거쳐 레디스까지 어떻게 흐르는지 그려줘"
297
+ → 그림을 그리고 자가감사한 뒤 올리고, 본문에 붙일 마크다운을 준다
298
+
299
+ "이 글 표지 이미지 만들어줘"
300
+ → 1200×630 카드. 주소를 velog_update_post 의 thumbnail 에 넣으면 표지가 된다
301
+ ```
302
+
303
+ MCP 클라이언트가 도구 호출 전에 승인을 받고, 되돌릴 수 없는 도구에는
304
+ `destructiveHint` 가 붙어 있다. 모르는 새 발행되는 일은 없다.
305
+
306
+ ### 백업 파일 형식
307
+
308
+ ```yaml
309
+ ---
310
+ title: "글 제목"
311
+ date: 2022-12-31T18:32:39.790Z
312
+ slug: "url-slug"
313
+ url: "https://velog.io/@username/url-slug"
314
+ tags: ["태그1", "태그2"]
315
+ likes: 260
316
+ views: 16323
317
+ ---
318
+
319
+ 마크다운 본문…
320
+ ```
321
+
322
+ ---
323
+
324
+ ## 개발
325
+
326
+ ```bash
327
+ npm test # node:test 로 .ts 직접 실행 — jest·ts-node 없음
328
+ npm run typecheck # 테스트 포함 — 종전엔 제외돼 실제 오류가 숨어 있었다
329
+ npm run lint # typescript-eslint (타입 기반)
330
+ npm run build # tsconfig.build.json (dist 에 테스트 미포함)
331
+ npm run schema:dump # 현재 벨로그 GraphQL 스키마 덤프
332
+ ```
333
+
334
+ 테스트 276건. `safety.test.ts` 가 보안 불변식(A1~A11)을, `render.test.ts` 가
335
+ 그림 쪽 불변식(R1~R23)을, `plugin.test.ts` 가 포장 불변식(P1~P26)을 고정한다.
336
+ 깨지면 우회하지 말고 왜 깨졌는지부터 볼 것.
337
+
338
+ 여기 있는 방어는 전부 **일부러 망가뜨려** 확인했다. 소스 변이 54종 + 발행 관문
339
+ 자체를 겨눈 변이 12종(`scripts/gate-mutation.sh`), 각각이 검사를 정확히 1건씩
340
+ 실패시켜야 한다. **방어를 지웠는데도 통과하는 테스트는 테스트가 아니다.**
341
+ 이 저장소에도 그런 게 여럿 있었고, 그렇게 해서 고쳤다.
342
+
343
+ ## 문서
344
+
345
+ | 문서 | 내용 |
346
+ | --- | --- |
347
+ | [docs/PRD.md](docs/PRD.md) | 기획서 — 목표·비목표·성공 기준 |
348
+ | [docs/architecture.md](docs/architecture.md) | 구조, Node 타입 스트리핑이 허용하는 TS 부분집합 |
349
+ | [docs/api-reference.md](docs/api-reference.md) | 벨로그 GraphQL 스키마 실측 + 서버 함정 |
350
+ | [docs/security.md](docs/security.md) | 토큰 취급, 권한 모델, 의도적으로 뺀 기능 |
351
+ | [docs/tools.md](docs/tools.md) | 도구 카탈로그와 주의사항 |
352
+ | [docs/decisions/](docs/decisions/) | 설계 결정 기록 (ADR) |
353
+
354
+ ## 참고
355
+
356
+ 벨로그 내부 GraphQL API 를 쓴다. 비공식이라 예고 없이 바뀔 수 있다.
357
+ 뭔가 깨지면 `npm run schema:dump` 를 돌려 `docs/api-reference.md` 와 diff 하는 게
358
+ 가장 빠르다.
359
+
360
+ 벨로그 [이용약관](https://velog.io/policy/terms)에는 자동화 접근을 제한하는 조항이
361
+ 없다. 본인 토큰으로 본인 글을 다루는 것은 권한 내 행위이고, 게시물 저작권은
362
+ 회원에게 귀속된다(제5조).
363
+
364
+ ## 라이선스
365
+
366
+ MIT