@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
@@ -0,0 +1,424 @@
1
+ /**
2
+ * 발행 도구.
3
+ *
4
+ * 기본은 **비공개 발행**이다. 공개 발행은 `VELOG_ALLOW_PUBLIC=1` 이 있어야 한다
5
+ * — 근거는 src/capabilities.ts 주석 참고 (비공개 글은 벨로그의 파괴적 제한에
6
+ * 걸리지 않지만 공개 글은 걸리고, RSS·검색·구독메일로 나가면 회수가 안 된다).
7
+ *
8
+ * 초안 작성과 발행을 **별도 도구**로 나눈 이유: 같은 도구에 `is_temp` 파라미터를
9
+ * 두면 초안을 쓰려다 값 하나 잘못 넣어 발행되는 경로가 생긴다. 이름이 다르면
10
+ * 그 사고가 안 난다.
11
+ */
12
+ import { z } from 'zod';
13
+ import { textResult } from "../format.js";
14
+ import { toUrlSlug, isSafeImageUrl } from "../slug.js";
15
+ import { assertOwned, assertOwnsSeries } from "../ownership.js";
16
+ const MUTATION_WRITE_POST = `
17
+ mutation PublishPost($input: WritePostInput!) {
18
+ writePost(input: $input) { id title url_slug is_temp is_private user { username } }
19
+ }
20
+ `;
21
+ const MUTATION_EDIT_POST = `
22
+ mutation EditPost($input: EditPostInput!) {
23
+ editPost(input: $input) { id title url_slug is_temp is_private user { username } }
24
+ }
25
+ `;
26
+ const QUERY_POST_FULL = `
27
+ query PostForEdit($input: ReadPostInput!) {
28
+ post(input: $input) {
29
+ id title body url_slug is_temp is_private thumbnail tags meta
30
+ series { id }
31
+ user { username }
32
+ }
33
+ }
34
+ `;
35
+ function resultLines(post, verb) {
36
+ const username = post.user?.username;
37
+ const url = username && post.url_slug
38
+ ? `https://velog.io/@${username}/${post.url_slug}`
39
+ : `(id: ${post.id})`;
40
+ const visibility = post.is_private ? '🔒 비공개' : '🌍 공개';
41
+ return [
42
+ `✅ ${verb}했습니다.`,
43
+ '',
44
+ `- 제목: ${post.title ?? '(제목 없음)'}`,
45
+ `- 상태: ${post.is_temp ? '임시저장' : '발행됨'} · ${visibility}`,
46
+ `- URL: ${url}`,
47
+ `- id: \`${post.id}\``,
48
+ ].join('\n');
49
+ }
50
+ /**
51
+ * ★ 사후 검증. 재조회만 하고 결과를 안 보면 검증이 아니다.
52
+ *
53
+ * 확인이 필요한 이유가 실재한다:
54
+ * - 벨로그는 발행 제한에 걸리면 요청과 무관하게 글을 비공개로 바꾼다.
55
+ * "공개로 발행했다"고 보고했는데 실제로는 비공개일 수 있다.
56
+ * - editPost 응답은 '갱신 전' 상태라 그것만 봐서는 아무것도 알 수 없다.
57
+ * - 재조회가 null 을 주면 뭔가 잘못된 것인데, 이전 상태로 성공을 보고하면
58
+ * 사용자가 그걸 모른다.
59
+ */
60
+ /** 키 순서에 좌우되지 않는 값 비교. meta 가 임의 JSON 이라 필요하다. */
61
+ function deepEqual(a, b) {
62
+ if (a === b)
63
+ return true;
64
+ if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null) {
65
+ return false;
66
+ }
67
+ if (Array.isArray(a) !== Array.isArray(b))
68
+ return false;
69
+ if (Array.isArray(a) && Array.isArray(b)) {
70
+ return a.length === b.length && a.every((v, i) => deepEqual(v, b[i]));
71
+ }
72
+ const ka = Object.keys(a);
73
+ const kb = Object.keys(b);
74
+ if (ka.length !== kb.length)
75
+ return false;
76
+ return ka.every((k) => Object.hasOwn(b, k) &&
77
+ deepEqual(a[k], b[k]));
78
+ }
79
+ /** 비교 불가 표식. 어떤 실제 값과도 같지 않다. */
80
+ const NOT_COMPARABLE = Symbol('not-comparable');
81
+ async function verifyAfter(client, id, toolName, expected) {
82
+ const after = await client.request(QUERY_POST_FULL, {
83
+ input: { id },
84
+ });
85
+ const post = after.post;
86
+ if (!post) {
87
+ throw new Error(`${toolName}: 작업 후 글(id=${id})을 다시 찾지 못했습니다. ` +
88
+ '벨로그에서 상태를 직접 확인하세요.');
89
+ }
90
+ const mismatches = [];
91
+ if (post.is_temp !== expected.is_temp) {
92
+ mismatches.push(`발행 상태가 예상과 다릅니다 (기대 is_temp=${expected.is_temp}, 실제 ${post.is_temp})`);
93
+ }
94
+ if (post.is_private !== expected.is_private) {
95
+ mismatches.push(`공개 범위가 예상과 다릅니다 (기대 is_private=${expected.is_private}, 실제 ${post.is_private})` +
96
+ (!expected.is_private && post.is_private === true
97
+ ? ' — 벨로그 발행 제한에 걸려 비공개로 전환됐을 수 있습니다'
98
+ : ''));
99
+ }
100
+ const sent = expected.content;
101
+ if (sent) {
102
+ const checks = [
103
+ ['제목', sent['title'], post.title],
104
+ // ★ 길이만 비교하면 같은 길이의 다른 본문이 통과한다. 내용을 본다.
105
+ // 보낸 값이 문자열이 아니면(있을 수 없지만) 절대 안 맞는 표식을 쓴다.
106
+ ['본문', typeof sent['body'] === 'string' ? sent['body'] : NOT_COMPARABLE, post.body ?? ''],
107
+ ['주소(url_slug)', sent['url_slug'], post.url_slug],
108
+ ['썸네일', sent['thumbnail'] ?? null, post.thumbnail ?? null],
109
+ ['시리즈', sent['series_id'] ?? null, post.series?.id ?? null],
110
+ ];
111
+ for (const [label, want, got] of checks) {
112
+ if (want !== got) {
113
+ mismatches.push(`${label}이(가) 저장되지 않았습니다 (보낸 값 ≠ 저장된 값)`);
114
+ }
115
+ }
116
+ // 태그는 서버가 trim·정렬을 바꿀 수 있어 정규화해 비교한다.
117
+ if (!sameTags(sent['tags'], post.tags)) {
118
+ mismatches.push('태그가 저장되지 않았습니다 (보낸 값 ≠ 저장된 값)');
119
+ }
120
+ // ★ '비었나'만 보면 {cover, short_description} → {cover} 같은 부분 손실을
121
+ // 놓친다. 보낸 키·값이 **전부** 남았는지 본다. 서버가 키를 더 채우는 건 허용.
122
+ const sentMeta = sent['meta'];
123
+ if (sentMeta && typeof sentMeta === 'object') {
124
+ const savedMeta = (post.meta ?? {});
125
+ // ★ JSON.stringify 비교는 키 순서에 걸린다({a,b} vs {b,a}). meta 는 임의
126
+ // JSON 이고 PostgreSQL JSON 컬럼이라 순서가 바뀔 수 있다. 의미로 비교한다.
127
+ const lost = Object.entries(sentMeta).filter(([k, v]) => !deepEqual(savedMeta[k], v));
128
+ if (lost.length > 0) {
129
+ mismatches.push(`meta 가 보존되지 않았습니다 (${lost.map(([k]) => k).join(', ')})`);
130
+ }
131
+ }
132
+ }
133
+ if (mismatches.length > 0) {
134
+ throw new Error(`⚠️ ${toolName} 결과 확인 실패:\n - ${mismatches.join('\n - ')}`);
135
+ }
136
+ return post;
137
+ }
138
+ /**
139
+ * 서버가 태그에 하는 짓을 그대로 재현해 비교한다.
140
+ *
141
+ * trim() → slice(0, 255) → 중복 제거
142
+ *
143
+ * 이렇게 안 하면 중복 태그나 255자 초과 태그를 보냈을 때 **정상 저장됐는데도**
144
+ * 실패로 보고한다. 순서는 서버가 바꿀 수 있어 정렬 후 비교한다.
145
+ * ★ 구분자로 join 하지 않는다 — 예전에 NUL 로 join 했다가 소스가 바이너리로
146
+ * 판정돼 git diff 가 깨졌다. 배열끼리 비교하면 그 문제가 없다.
147
+ */
148
+ function normalizeTags(tags) {
149
+ const seen = new Set();
150
+ for (const tag of tags ?? []) {
151
+ seen.add(tag.trim().slice(0, 255));
152
+ }
153
+ return [...seen].sort();
154
+ }
155
+ function sameTags(a, b) {
156
+ const x = normalizeTags(a);
157
+ const y = normalizeTags(b);
158
+ return x.length === y.length && x.every((v, i) => v === y[i]);
159
+ }
160
+ /** 공개 발행일 때만 벨로그 계수에 잡힌다 — 그때만 상한을 적용한다. */
161
+ function guardIfPublic(isPrivate, limiter) {
162
+ if (!isPrivate)
163
+ limiter.check();
164
+ }
165
+ export function registerPublishTools(server, client, capabilities, limiter) {
166
+ /**
167
+ * 공개 발행이 켜져 있을 때만 노출하는 파라미터.
168
+ *
169
+ * ★ '새 글'과 '기존 글 수정'의 기본값이 달라야 한다.
170
+ * 새 글: 생략하면 비공개 (안전한 쪽)
171
+ * 수정 : 생략하면 **기존 값 유지** — .default(true) 를 걸면 공개글을
172
+ * 수정만 해도 조용히 비공개가 된다. 실측으로 잡은 버그다.
173
+ */
174
+ const visibilityField = capabilities.publicPublish
175
+ ? {
176
+ is_private: z
177
+ .boolean()
178
+ .default(true)
179
+ .describe('true=비공개 발행(기본), false=공개 발행. ' +
180
+ '공개하면 RSS·검색·구독 메일로 나가며 지워도 회수되지 않는다.'),
181
+ }
182
+ : {};
183
+ /** 수정용 — 기본값을 두지 않는다. 생략 = 기존 공개 범위 유지. */
184
+ const visibilityFieldForUpdate = capabilities.publicPublish
185
+ ? {
186
+ is_private: z
187
+ .boolean()
188
+ .optional()
189
+ .describe('생략하면 이 글의 현재 공개 범위를 그대로 둔다. ' +
190
+ 'true=비공개로 전환, false=공개로 전환.'),
191
+ }
192
+ : {};
193
+ /** 설정에 따라 실제로 쓸 is_private 값을 정한다. 모델이 못 넘는 선이다. */
194
+ function resolvePrivacy(requested) {
195
+ if (!capabilities.publicPublish)
196
+ return true; // 공개 불가 — 무조건 비공개
197
+ return requested ?? true;
198
+ }
199
+ const publicNote = capabilities.publicPublish
200
+ ? ''
201
+ : ' 현재 설정에서는 **비공개로만** 발행된다 ' +
202
+ '(공개 발행은 VELOG_ALLOW_PUBLIC=1 이 필요하다).';
203
+ server.registerTool('velog_publish_post', {
204
+ title: '벨로그 글 발행',
205
+ description: '새 글을 바로 발행한다. 초안을 거치지 않는다.' +
206
+ publicNote +
207
+ ' 되돌리려면 velog_unpublish_post 로 초안으로 내릴 수 있다.',
208
+ inputSchema: {
209
+ title: z.string().min(1),
210
+ body: z.string().min(1).describe('본문 (마크다운)'),
211
+ tags: z.array(z.string()).default([]),
212
+ url_slug: z.string().optional().describe('생략하면 제목에서 생성'),
213
+ thumbnail: z
214
+ .string()
215
+ .refine(isSafeImageUrl, 'http(s) 이미지 URL 만 허용합니다')
216
+ .optional(),
217
+ series_id: z.string().optional(),
218
+ ...visibilityField,
219
+ },
220
+ // 공개 발행은 되돌릴 수 없다(RSS·검색·메일). destructive 로 표시한다.
221
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false },
222
+ }, async (args) => {
223
+ client.requireAuth('velog_publish_post');
224
+ const { title, body, tags, url_slug, thumbnail, series_id } = args;
225
+ const isPrivate = resolvePrivacy(args.is_private);
226
+ if (series_id)
227
+ await assertOwnsSeries(client, series_id, 'velog_publish_post');
228
+ // ★ 상한은 '검증을 다 통과한 뒤' 소비한다. 앞에 두면 잘못된 입력으로
229
+ // 5번 실패해도 5분간 공개 발행이 막힌다.
230
+ guardIfPublic(isPrivate, limiter);
231
+ const input = {
232
+ title,
233
+ body,
234
+ tags,
235
+ url_slug: toUrlSlug(title, url_slug),
236
+ is_markdown: true,
237
+ is_temp: false,
238
+ is_private: isPrivate,
239
+ meta: {},
240
+ };
241
+ if (thumbnail)
242
+ input['thumbnail'] = thumbnail;
243
+ if (series_id)
244
+ input['series_id'] = series_id;
245
+ const data = await client.mutate(MUTATION_WRITE_POST, { input });
246
+ return textResult(resultLines(data.writePost, '발행'));
247
+ });
248
+ server.registerTool('velog_publish_draft', {
249
+ title: '초안 발행',
250
+ description: '기존 임시저장 글을 발행한다. 본문은 저장된 내용을 그대로 쓴다 — ' +
251
+ '다시 넘길 필요가 없다.' +
252
+ publicNote,
253
+ inputSchema: {
254
+ id: z.string().min(1).describe('초안 id (velog_list_drafts 로 확인)'),
255
+ ...visibilityField,
256
+ },
257
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false },
258
+ }, async (args) => {
259
+ client.requireAuth('velog_publish_draft');
260
+ const { id } = args;
261
+ const isPrivate = resolvePrivacy(args.is_private);
262
+ // ★ 저장된 내용을 그대로 살려 발행한다. 호출자가 본문을 다시 넘기게
263
+ // 하면 그 과정에서 태그·슬러그·시리즈가 날아간다(editPost 는 전체 교체).
264
+ const current = await client.request(QUERY_POST_FULL, { input: { id } });
265
+ const post = current.post;
266
+ if (!post)
267
+ throw new Error(`id=${id} 인 글을 찾지 못했습니다.`);
268
+ await assertOwned(client, post, 'velog_publish_draft');
269
+ if (post.is_temp !== true) {
270
+ throw new Error(`id=${id} 는 이미 발행된 글입니다. 공개 범위만 바꾸려면 velog_update_post 를 쓰세요.`);
271
+ }
272
+ // 검증을 다 통과한 뒤에 상한을 소비한다.
273
+ guardIfPublic(isPrivate, limiter);
274
+ const input = {
275
+ id,
276
+ title: post.title ?? '(제목 없음)',
277
+ body: post.body ?? '',
278
+ tags: post.tags ?? [],
279
+ url_slug: post.url_slug ?? toUrlSlug(post.title ?? id),
280
+ is_markdown: true,
281
+ is_temp: false,
282
+ is_private: isPrivate,
283
+ // ★ meta 를 {} 로 보내면 short_description 같은 표시 데이터가 지워진다.
284
+ // 서버가 받은 값을 그대로 DB 에 넣으므로 반드시 기존 값을 실어야 한다.
285
+ meta: post.meta ?? {},
286
+ };
287
+ if (post.thumbnail)
288
+ input['thumbnail'] = post.thumbnail;
289
+ if (post.series?.id)
290
+ input['series_id'] = post.series.id;
291
+ await client.mutate(MUTATION_EDIT_POST, { input });
292
+ const after = await verifyAfter(client, id, 'velog_publish_draft', {
293
+ is_temp: false,
294
+ is_private: isPrivate,
295
+ content: input,
296
+ });
297
+ return textResult(resultLines(after, '발행'));
298
+ });
299
+ server.registerTool('velog_unpublish_post', {
300
+ title: '발행 취소 (초안으로 되돌리기)',
301
+ description: '발행된 글을 임시저장으로 되돌린다. 글은 사라지지 않고 초안 목록으로 간다. ' +
302
+ '★ 이미 나간 RSS·구독 메일은 회수되지 않는다 — 검색엔진 캐시도 한동안 남는다.',
303
+ inputSchema: { id: z.string().min(1).describe('발행된 글의 id') },
304
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true },
305
+ }, async ({ id }) => {
306
+ client.requireAuth('velog_unpublish_post');
307
+ const current = await client.request(QUERY_POST_FULL, { input: { id } });
308
+ const post = current.post;
309
+ if (!post)
310
+ throw new Error(`id=${id} 인 글을 찾지 못했습니다.`);
311
+ await assertOwned(client, post, 'velog_unpublish_post');
312
+ if (post.is_temp === true) {
313
+ return textResult(`id=${id} 는 이미 임시저장 상태입니다. 아무것도 하지 않았습니다.`);
314
+ }
315
+ const input = {
316
+ id,
317
+ title: post.title ?? '(제목 없음)',
318
+ body: post.body ?? '',
319
+ tags: post.tags ?? [],
320
+ url_slug: post.url_slug ?? toUrlSlug(post.title ?? id),
321
+ is_markdown: true,
322
+ is_temp: true,
323
+ // ★ is_private 를 기존 값(공개글이면 false)으로 두면 안 된다.
324
+ // is_temp:true + is_private:false 인 초안이 생겨 다시 벨로그 계수
325
+ // 대상이 되고, 초안은 비공개라는 불변식(DRAFT_ONLY)도 깨진다.
326
+ // 공식 서버의 외부연동 삭제 알림도 false→true 전환에서만 나간다.
327
+ is_private: true,
328
+ meta: post.meta ?? {},
329
+ };
330
+ if (post.thumbnail)
331
+ input['thumbnail'] = post.thumbnail;
332
+ if (post.series?.id)
333
+ input['series_id'] = post.series.id;
334
+ await client.mutate(MUTATION_EDIT_POST, { input });
335
+ const after = await verifyAfter(client, id, 'velog_unpublish_post', {
336
+ is_temp: true,
337
+ is_private: true,
338
+ content: input,
339
+ });
340
+ return textResult(`${resultLines(after, '초안으로 되돌리기')}\n\n` +
341
+ '※ 이미 배포된 RSS·구독 메일과 검색엔진 캐시는 회수되지 않습니다.');
342
+ });
343
+ server.registerTool('velog_update_post', {
344
+ title: '발행글 수정',
345
+ description: '이미 발행된 글을 수정한다. 발행 상태(is_temp:false)는 유지된다. ' +
346
+ '생략한 필드는 **기존 값을 그대로 유지**한다 — 초안 도구와 달리 전체 교체가 아니다. ' +
347
+ '초안 id 는 거부한다(초안 수정은 velog_update_draft).' +
348
+ (capabilities.publicPublish
349
+ ? ' is_private 로 공개 범위도 바꿀 수 있고, 생략하면 현재 범위를 유지한다.'
350
+ : ' ★현재 설정에서는 공개 글을 수정하면 **비공개로 내려간다** — ' +
351
+ '공개 상태를 유지하려면 VELOG_ALLOW_PUBLIC=1 이 필요하다.'),
352
+ inputSchema: {
353
+ id: z.string().min(1),
354
+ title: z.string().min(1).optional(),
355
+ body: z.string().min(1).optional().describe('생략하면 기존 본문 유지'),
356
+ tags: z.array(z.string()).optional().describe('생략하면 기존 태그 유지'),
357
+ url_slug: z.string().optional().describe('생략하면 기존 주소 유지'),
358
+ thumbnail: z
359
+ .string()
360
+ .refine(isSafeImageUrl, 'http(s) 이미지 URL 만 허용합니다')
361
+ .optional(),
362
+ series_id: z.string().optional(),
363
+ ...visibilityFieldForUpdate,
364
+ },
365
+ // 생략 필드는 보존하지만 넘긴 필드는 덮어쓴다. MCP 명세상 false 는
366
+ // '추가만 한다'는 뜻이라 거짓이 된다.
367
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true },
368
+ }, async (args) => {
369
+ client.requireAuth('velog_update_post');
370
+ const { id, title, body, tags, url_slug, thumbnail, series_id } = args;
371
+ // ★ 병합 수정. 기존 값을 읽어 생략 필드를 채운다 — 초안 도구에서
372
+ // '생략하면 초기화'가 사고를 부른다는 걸 확인했으므로 이쪽은 보존한다.
373
+ const current = await client.request(QUERY_POST_FULL, { input: { id } });
374
+ const post = current.post;
375
+ if (!post)
376
+ throw new Error(`id=${id} 인 글을 찾지 못했습니다.`);
377
+ await assertOwned(client, post, 'velog_update_post');
378
+ // 초안은 velog_update_draft 담당이다. 여기로 오면 is_temp:false 를 보내
379
+ // 의도치 않게 발행되므로 막는다.
380
+ // ★ `=== true` 만 막으면 null·누락이 '발행글'로 통과해 is_temp:false 가
381
+ // 나간다(fail-open). is_temp 는 스키마상 nullable 이므로
382
+ // '확실히 false 인 경우'만 진행한다.
383
+ if (post.is_temp !== false) {
384
+ throw new Error(`id=${id} 의 발행 상태를 확인할 수 없거나 임시저장 글입니다 ` +
385
+ `(is_temp=${post.is_temp}). 초안 수정은 velog_update_draft 를 쓰세요.`);
386
+ }
387
+ if (series_id)
388
+ await assertOwnsSeries(client, series_id, 'velog_update_post');
389
+ const requested = args.is_private;
390
+ const isPrivate = capabilities.publicPublish
391
+ ? (requested ?? post.is_private ?? true)
392
+ : true;
393
+ // 비공개 → 공개로 바뀌는 경우에만 계수 대상이 된다.
394
+ if (post.is_private !== false && !isPrivate)
395
+ limiter.check();
396
+ const nextTitle = title ?? post.title ?? '(제목 없음)';
397
+ const input = {
398
+ id,
399
+ title: nextTitle,
400
+ body: body ?? post.body ?? '',
401
+ tags: tags ?? post.tags ?? [],
402
+ url_slug: url_slug ? toUrlSlug(nextTitle, url_slug) : (post.url_slug ?? toUrlSlug(nextTitle)),
403
+ is_markdown: true,
404
+ is_temp: false,
405
+ is_private: isPrivate,
406
+ // ★ meta 를 {} 로 보내면 short_description 등이 지워진다. 기존 값 유지.
407
+ meta: post.meta ?? {},
408
+ };
409
+ const nextThumbnail = thumbnail ?? post.thumbnail;
410
+ if (nextThumbnail)
411
+ input['thumbnail'] = nextThumbnail;
412
+ const nextSeries = series_id ?? post.series?.id;
413
+ if (nextSeries)
414
+ input['series_id'] = nextSeries;
415
+ await client.mutate(MUTATION_EDIT_POST, { input });
416
+ const after = await verifyAfter(client, id, 'velog_update_post', {
417
+ is_temp: false,
418
+ is_private: isPrivate,
419
+ content: input,
420
+ });
421
+ return textResult(resultLines(after, '수정'));
422
+ });
423
+ }
424
+ //# sourceMappingURL=publish.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"publish.js","sourceRoot":"","sources":["../../src/tools/publish.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * 통계 도구 — 벨로그에 없는 기능이라 우리가 집계한다.
3
+ *
4
+ * 벨로그는 글별 조회수를 보여주지만 "내 블로그 전체" 관점의 집계는 없다.
5
+ * Post 에 likes/views/comments_count 가 직접 붙어 있어 목록만 긁으면 만들 수 있다.
6
+ */
7
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
8
+ import type { VelogClient } from '../client.ts';
9
+ import type { VelogPostSummary } from '../types.ts';
10
+ /**
11
+ * 사용자의 글을 커서로 끝까지 긁는다.
12
+ *
13
+ * maxPages 로 상한을 둔다 — 무한 루프 방지이자 이용약관 8조(정상 운영 방해)
14
+ * 회피다. 상한에 걸리면 조용히 자르지 않고 호출자에게 알린다.
15
+ */
16
+ /**
17
+ * 수집이 어떻게 끝났는지. `truncated: boolean` 하나로는 '다 봤다'와
18
+ * '커서가 막혀서 멈췄다'를 구분할 수 없어 오보고가 난다.
19
+ */
20
+ export type FetchOutcome =
21
+ /** 마지막 페이지까지 봤다. */
22
+ 'complete'
23
+ /** maxPages 상한에 걸렸다. 더 있을 수 있다. */
24
+ | 'page_limit'
25
+ /** 커서가 안 움직였다. 벨로그가 커서를 못 찾으면 1페이지를 다시 준다. */
26
+ | 'cursor_stalled';
27
+ export declare function fetchAllPosts(client: VelogClient, username: string, maxPages: number): Promise<{
28
+ posts: VelogPostSummary[];
29
+ truncated: boolean;
30
+ outcome: FetchOutcome;
31
+ }>;
32
+ export declare function registerStatsTools(server: McpServer, client: VelogClient): void;
@@ -0,0 +1,154 @@
1
+ /**
2
+ * 통계 도구 — 벨로그에 없는 기능이라 우리가 집계한다.
3
+ *
4
+ * 벨로그는 글별 조회수를 보여주지만 "내 블로그 전체" 관점의 집계는 없다.
5
+ * Post 에 likes/views/comments_count 가 직접 붙어 있어 목록만 긁으면 만들 수 있다.
6
+ */
7
+ import { z } from 'zod';
8
+ import { QUERY_POSTS } from "../graphql.js";
9
+ import { postUrl, textResult } from "../format.js";
10
+ import { READ_ONLY } from "./posts.js";
11
+ import { resolveMyUsername } from "../me.js";
12
+ /** 한 번에 받는 최대치. 벨로그 커넥션 풀이 작아 크게 잡지 않는다. */
13
+ const PAGE_SIZE = 50;
14
+ export async function fetchAllPosts(client, username, maxPages) {
15
+ const posts = [];
16
+ // ★ 중복 방어. 커서가 안 움직이면(벨로그가 같은 페이지를 반복 반환) 같은 글을
17
+ // 여러 번 담아 집계가 배수로 부풀려진다. 실측 재현: 커서 고착 시 50편이
18
+ // 250편으로 계수됨 — 조회수 총계가 5배가 된다. 통계 도구에서 이건 치명적이다.
19
+ const seenIds = new Set();
20
+ const seenCursors = new Set();
21
+ let cursor;
22
+ for (let page = 0; page < maxPages; page++) {
23
+ const input = { username, limit: PAGE_SIZE };
24
+ if (cursor)
25
+ input['cursor'] = cursor;
26
+ const data = await client.request(QUERY_POSTS, { input });
27
+ const batch = data.posts ?? [];
28
+ let added = 0;
29
+ for (const post of batch) {
30
+ if (post.id && seenIds.has(post.id))
31
+ continue;
32
+ if (post.id)
33
+ seenIds.add(post.id);
34
+ posts.push(post);
35
+ added++;
36
+ }
37
+ // 새로 들어온 게 없다 = 커서가 제자리다. '다 봤다'가 아니다.
38
+ if (added === 0) {
39
+ return { posts, truncated: true, outcome: 'cursor_stalled' };
40
+ }
41
+ // 한 페이지를 못 채웠으면 진짜 마지막이다.
42
+ if (batch.length < PAGE_SIZE) {
43
+ return { posts, truncated: false, outcome: 'complete' };
44
+ }
45
+ const next = batch.at(-1)?.id;
46
+ if (!next || seenCursors.has(next)) {
47
+ return { posts, truncated: true, outcome: 'cursor_stalled' };
48
+ }
49
+ seenCursors.add(next);
50
+ cursor = next;
51
+ }
52
+ return { posts, truncated: true, outcome: 'page_limit' };
53
+ }
54
+ function sum(posts) {
55
+ return posts.reduce((acc, p) => ({
56
+ views: acc.views + (p.views ?? 0),
57
+ likes: acc.likes + (p.likes ?? 0),
58
+ comments: acc.comments + (p.comments_count ?? 0),
59
+ }), { views: 0, likes: 0, comments: 0 });
60
+ }
61
+ const num = (n) => n.toLocaleString('ko-KR');
62
+ function tagBreakdown(posts, top) {
63
+ const counts = new Map();
64
+ for (const p of posts) {
65
+ for (const tag of p.tags ?? []) {
66
+ const cur = counts.get(tag) ?? { posts: 0, views: 0 };
67
+ counts.set(tag, { posts: cur.posts + 1, views: cur.views + (p.views ?? 0) });
68
+ }
69
+ }
70
+ if (counts.size === 0)
71
+ return '(태그 없음)';
72
+ return [...counts.entries()]
73
+ .sort((a, b) => b[1].views - a[1].views)
74
+ .slice(0, top)
75
+ .map(([tag, s]) => ` #${tag} — ${s.posts}편 · 👁${num(s.views)}`)
76
+ .join('\n');
77
+ }
78
+ function yearBreakdown(posts) {
79
+ const byYear = new Map();
80
+ for (const p of posts) {
81
+ const year = p.released_at?.slice(0, 4) ?? '미상';
82
+ const cur = byYear.get(year) ?? { posts: 0, views: 0 };
83
+ byYear.set(year, { posts: cur.posts + 1, views: cur.views + (p.views ?? 0) });
84
+ }
85
+ return [...byYear.entries()]
86
+ .sort((a, b) => b[0].localeCompare(a[0]))
87
+ .map(([year, s]) => ` ${year} — ${s.posts}편 · 👁${num(s.views)}`)
88
+ .join('\n');
89
+ }
90
+ export function registerStatsTools(server, client) {
91
+ server.registerTool('velog_blog_stats', {
92
+ title: '블로그 통계',
93
+ description: '한 사용자의 글 전체를 긁어 조회수·좋아요·댓글을 집계한다. ' +
94
+ '연도별·태그별 분포와 상위 글 순위를 함께 낸다. 벨로그에 없는 화면이라 직접 계산한다. ' +
95
+ '글이 많으면 여러 번 요청하므로 몇 초 걸릴 수 있다.',
96
+ inputSchema: {
97
+ username: z
98
+ .string()
99
+ .optional()
100
+ .describe('@ 없이. 생략하면 인증된 내 계정을 쓴다'),
101
+ top: z.number().int().min(1).max(30).default(10).describe('상위 몇 편까지 보여줄지'),
102
+ max_pages: z
103
+ .number()
104
+ .int()
105
+ .min(1)
106
+ .max(20)
107
+ .default(10)
108
+ .describe(`최대 페이지 수 (1페이지=${PAGE_SIZE}편). 과도한 요청 방지용 상한`),
109
+ },
110
+ annotations: READ_ONLY,
111
+ }, async ({ username, top, max_pages }) => {
112
+ const target = username ?? (await resolveMyUsername(client));
113
+ const { posts, truncated, outcome } = await fetchAllPosts(client, target, max_pages);
114
+ if (posts.length === 0)
115
+ return textResult(`@${target} 의 공개 글이 없습니다.`);
116
+ const totals = sum(posts);
117
+ const avgViews = Math.round(totals.views / posts.length);
118
+ const ranked = [...posts]
119
+ .sort((a, b) => (b.views ?? 0) - (a.views ?? 0))
120
+ .slice(0, top)
121
+ .map((p, i) => `${String(i + 1).padStart(2)}. 👁${num(p.views ?? 0).padStart(7)} ` +
122
+ `♥${String(p.likes ?? 0).padStart(4)} ${p.title}\n` +
123
+ ` ${postUrl(p)}`)
124
+ .join('\n');
125
+ const report = [
126
+ `# @${target} 블로그 통계`,
127
+ '',
128
+ `- 글 ${num(posts.length)}편${truncated ? ' (전부는 아닙니다 — 아래 참고)' : ''}`,
129
+ `- 총 조회수 👁 ${num(totals.views)}`,
130
+ `- 총 좋아요 ♥ ${num(totals.likes)}`,
131
+ `- 총 댓글 💬 ${num(totals.comments)}`,
132
+ `- 글당 평균 조회수 ${num(avgViews)}`,
133
+ '',
134
+ `## 조회수 상위 ${Math.min(top, posts.length)}편`,
135
+ ranked,
136
+ '',
137
+ '## 연도별',
138
+ yearBreakdown(posts),
139
+ '',
140
+ '## 태그별 (조회수 순 상위 10)',
141
+ tagBreakdown(posts, 10),
142
+ ].join('\n');
143
+ // 왜 덜 봤는지에 따라 사용자가 할 일이 다르다. 뭉뚱그리면 안 된다.
144
+ const warning = outcome === 'page_limit'
145
+ ? `\n\n⚠️ 상한 ${max_pages}페이지(${max_pages * PAGE_SIZE}편)에서 멈췄습니다. ` +
146
+ '전체를 보려면 max_pages 를 올리세요.'
147
+ : outcome === 'cursor_stalled'
148
+ ? '\n\n⚠️ 벨로그 페이지네이션이 더 진행되지 않아 여기서 멈췄습니다. ' +
149
+ '집계는 위 편수 기준이며 실제 글은 더 있을 수 있습니다.'
150
+ : '';
151
+ return textResult(report + warning);
152
+ });
153
+ }
154
+ //# sourceMappingURL=stats.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stats.js","sourceRoot":"","sources":["../../src/tools/stats.ts"],"names":[],"mappings":"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"}