@milcho0604/velog-mcp 0.4.1 → 0.5.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.
@@ -0,0 +1,139 @@
1
+ /**
2
+ * 썸네일 자동 채움 — 본문에 이미 있는 이미지를 목록 카드에 쓴다.
3
+ *
4
+ * ★ 왜 있는가 — 썸네일이 없으면 글 목록·공유 카드가 글자만 나온다. 그런데
5
+ * 본문에 그림을 넣은 사람은 이미 쓸 만한 이미지를 가지고 있다. 한 번 더
6
+ * 지정하게 만들 이유가 없다.
7
+ *
8
+ * ★★ 이 모듈은 **본문을 고치지 않는다.** 읽기만 한다. 반환값은 '무엇을 쓸지'와
9
+ * '왜 그렇게 정했는지'뿐이고, 실제 적용은 호출부가 한다.
10
+ */
11
+ import { isSafeImageUrl } from "./slug.js";
12
+ /** 문자 개수. 괄호 균형 검사에만 쓴다. */
13
+ function countOf(text, ch) {
14
+ let n = 0;
15
+ for (const c of text)
16
+ if (c === ch)
17
+ n++;
18
+ return n;
19
+ }
20
+ /**
21
+ * 마크다운 본문에서 이미지 URL 을 등장 순서대로 뽑는다.
22
+ *
23
+ * 다루는 형태:
24
+ * - `![alt](url)` · `![alt](url "제목")`
25
+ * - `<img src="url">` (HTML 을 섞어 쓰는 사람이 많다)
26
+ *
27
+ * ⚠️ 코드블록 안의 이미지는 **제외한다.** 예제로 적어둔 마크다운이 썸네일이
28
+ * 되어버리면 황당하다. 펜스(``` 또는 ~~~)와 인라인 코드(`...`)를 먼저 지운다.
29
+ */
30
+ export function extractImageUrls(body) {
31
+ // ★ 지우는 순서가 중요하다. 펜스를 먼저 지워야 그 안의 백틱이 인라인 코드로
32
+ // 잘못 짝지어지지 않는다.
33
+ const withoutCode = body
34
+ .replace(/^[ \t]*(`{3,}|~{3,})[\s\S]*?^[ \t]*\1[ \t]*$/gm, '')
35
+ .replace(/`[^`\n]*`/g, '');
36
+ const found = [];
37
+ const push = (raw) => {
38
+ if (!raw)
39
+ return;
40
+ // 마크다운은 URL 뒤에 제목을 붙일 수 있다: (url "제목")
41
+ const url = raw.trim().split(/\s+/)[0]?.replace(/^<|>$/g, '');
42
+ if (!url || !isSafeImageUrl(url))
43
+ return;
44
+ // ★★ 괄호가 안 맞으면 **버린다.** `![a](https://x/y(1).png)` 는 `)` 에서 끊겨
45
+ // `https://x/y(1` 이 잡히는데, 이것도 https 라 URL 검사를 통과한다.
46
+ // 그대로 두면 **깨진 주소가 조용히 썸네일이 된다** — 아무도 모르게 목록
47
+ // 카드만 깨진다. 애매하면 넣지 않는 쪽이 맞다.
48
+ if (countOf(url, '(') !== countOf(url, ')'))
49
+ return;
50
+ if (!found.includes(url))
51
+ found.push(url);
52
+ };
53
+ // alt 안의 대괄호 한 겹까지 허용한다: `![도표[1]](url)`
54
+ for (const m of withoutCode.matchAll(/!\[(?:[^[\]]|\[[^\]]*\])*\]\(([^)]+)\)/g))
55
+ push(m[1]);
56
+ for (const m of withoutCode.matchAll(/<img\b[^>]*?\ssrc\s*=\s*["']([^"']+)["']/gi)) {
57
+ push(m[1]);
58
+ }
59
+ return found;
60
+ }
61
+ /**
62
+ * 무엇을 썸네일로 쓸지 정한다.
63
+ *
64
+ * 우선순위:
65
+ * 1. 호출자가 준 값 → 그대로 쓴다 (`explicit`)
66
+ * 2. 호출자가 **끄겠다고** 명시 → 안 쓴다 (`opted-out`)
67
+ * 3. 본문 첫 이미지 → 자동 (`auto`)
68
+ * 4. 본문에 이미지가 없음 → 안 쓴다 (`none`)
69
+ *
70
+ * ★ 끄는 방법을 둔 이유 — 이 도구는 나만 쓰는 게 아니다. 썸네일을 **일부러**
71
+ * 비워 두는 사람이 있고, 자동 채움이 그 의도를 조용히 덮으면 안 된다.
72
+ * 빈 문자열이나 null 을 주면 "비워 둬라"로 읽는다.
73
+ */
74
+ export function chooseThumbnail(requested, body) {
75
+ const candidates = extractImageUrls(body);
76
+ if (typeof requested === 'string' && requested.trim() !== '') {
77
+ return { url: requested, candidates, reason: 'explicit' };
78
+ }
79
+ // ★ null 과 빈 문자열은 "자동으로 넣지 마라"는 뜻이다. undefined(미지정)와
80
+ // 구분해야 한다 — 미지정은 "알아서 해달라"이고 이건 명시적 거부다.
81
+ if (requested !== undefined) {
82
+ return { url: undefined, candidates, reason: 'opted-out' };
83
+ }
84
+ const first = candidates[0];
85
+ if (first === undefined)
86
+ return { url: undefined, candidates, reason: 'none' };
87
+ return { url: first, candidates, reason: 'auto' };
88
+ }
89
+ /**
90
+ * 병합 수정(velog_update_post)용. 생성과 규칙이 다르다.
91
+ *
92
+ * ★★ **기존 썸네일이 최우선이다.** 이미 붙어 있는 그림을 본문 첫 이미지로
93
+ * 갈아치우면, 제목만 고치려던 사람이 목록 카드가 바뀌는 걸 당한다.
94
+ * 자동 채움은 **비어 있을 때만** 한다.
95
+ *
96
+ * ⚠️ `null` 을 줘도 **기존 썸네일을 지우지 않는다.** 여기서 null 은 "자동으로
97
+ * 채우지 마라"이지 "지워라"가 아니다. 지우는 건 되돌리기 어려운데 그 의도를
98
+ * null 하나로 단정할 수 없다 — 지우려면 벨로그에서 직접 하는 게 맞다.
99
+ */
100
+ export function chooseThumbnailForUpdate(requested, existing, body) {
101
+ if (typeof requested === 'string' && requested.trim() !== '') {
102
+ return { url: requested, candidates: extractImageUrls(body), reason: 'explicit' };
103
+ }
104
+ // ★★ 생략하면 **기존 값을 그대로 잇는다. 비어 있어도 마찬가지다.**
105
+ // 예전엔 기존이 비었을 때 본문 첫 이미지로 채웠는데, 이 도구는 문서에
106
+ // "생략한 필드는 유지된다"고 약속해 뒀다. 제목만 고치려던 호출이 **일부러
107
+ // 비워 둔 썸네일을 채워버리는** 건 그 약속을 깨는 것이다.
108
+ // 자동 채움은 새로 쓰는 경로(create/publish)에만 둔다.
109
+ // (코덱스 교차검증에서 잡혔다.)
110
+ return { url: existing ?? undefined, candidates: [], reason: 'explicit' };
111
+ }
112
+ /**
113
+ * 결과 메시지에 붙일 안내. 붙일 말이 없으면 빈 문자열.
114
+ *
115
+ * ★ 조용히 하지 않는다. 자동으로 넣었으면 **넣었다고 말한다.** 내가 시키지 않은
116
+ * 변경이 결과에 안 보이면 그게 사고다.
117
+ */
118
+ export function describeThumbnail(choice) {
119
+ if (choice.reason === 'auto') {
120
+ const others = choice.candidates.length - 1;
121
+ const head = `\n\n🖼️ 썸네일을 본문 첫 이미지로 **자동 설정**했습니다.\n ${choice.url}`;
122
+ if (others <= 0)
123
+ return head;
124
+ const list = choice.candidates
125
+ .slice(1, 6)
126
+ .map((u, i) => ` ${i + 2}. ${u}`)
127
+ .join('\n');
128
+ const more = others > 5 ? `\n … 외 ${others - 5}개` : '';
129
+ return (`${head}\n\n 본문에 이미지가 ${choice.candidates.length}개 있습니다. ` +
130
+ `다른 걸 쓰려면 thumbnail 에 아래 중 하나를 주세요.\n${list}${more}`);
131
+ }
132
+ if (choice.reason === 'none') {
133
+ return ('\n\n💡 썸네일이 없습니다. 본문에 이미지가 없어 자동으로 채우지 못했습니다 — ' +
134
+ '목록·공유 카드에 그림이 필요하면 thumbnail 에 이미지 URL 을 주거나 ' +
135
+ 'velog_render_diagram / velog_upload_image 로 하나 만들어 붙이세요.');
136
+ }
137
+ return '';
138
+ }
139
+ //# sourceMappingURL=thumbnail.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"thumbnail.js","sourceRoot":"","sources":["../src/thumbnail.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAE3C,4BAA4B;AAC5B,SAAS,OAAO,CAAC,IAAY,EAAE,EAAU;IACxC,IAAI,CAAC,GAAG,CAAC,CAAC;IACV,KAAK,MAAM,CAAC,IAAI,IAAI;QAAE,IAAI,CAAC,KAAK,EAAE;YAAE,CAAC,EAAE,CAAC;IACxC,OAAO,CAAC,CAAC;AACV,CAAC;AAYD;;;;;;;;;GASG;AACH,MAAM,UAAU,gBAAgB,CAAC,IAAY;IAC5C,8CAA8C;IAC9C,kBAAkB;IAClB,MAAM,WAAW,GAAG,IAAI;SACtB,OAAO,CAAC,gDAAgD,EAAE,EAAE,CAAC;SAC7D,OAAO,CAAC,YAAY,EAAE,EAAE,CAAC,CAAC;IAE5B,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,MAAM,IAAI,GAAG,CAAC,GAAuB,EAAQ,EAAE;QAC9C,IAAI,CAAC,GAAG;YAAE,OAAO;QACjB,uCAAuC;QACvC,MAAM,GAAG,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;QAC9D,IAAI,CAAC,GAAG,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC;YAAE,OAAO;QACzC,+DAA+D;QAC/D,sDAAsD;QACtD,+CAA+C;QAC/C,+BAA+B;QAC/B,IAAI,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,KAAK,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC;YAAE,OAAO;QACpD,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC;YAAE,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC3C,CAAC,CAAC;IAEF,yCAAyC;IACzC,KAAK,MAAM,CAAC,IAAI,WAAW,CAAC,QAAQ,CAAC,yCAAyC,CAAC;QAAE,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC5F,KAAK,MAAM,CAAC,IAAI,WAAW,CAAC,QAAQ,CAAC,4CAA4C,CAAC,EAAE,CAAC;QACpF,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACZ,CAAC;IACD,OAAO,KAAK,CAAC;AACd,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,eAAe,CAC9B,SAAoC,EACpC,IAAY;IAEZ,MAAM,UAAU,GAAG,gBAAgB,CAAC,IAAI,CAAC,CAAC;IAE1C,IAAI,OAAO,SAAS,KAAK,QAAQ,IAAI,SAAS,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;QAC9D,OAAO,EAAE,GAAG,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC;IAC3D,CAAC;IACD,qDAAqD;IACrD,2CAA2C;IAC3C,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;QAC7B,OAAO,EAAE,GAAG,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC;IAC5D,CAAC;IACD,MAAM,KAAK,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC;IAC5B,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,EAAE,GAAG,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;IAC/E,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;AACnD,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,wBAAwB,CACvC,SAAoC,EACpC,QAAmC,EACnC,IAAY;IAEZ,IAAI,OAAO,SAAS,KAAK,QAAQ,IAAI,SAAS,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;QAC9D,OAAO,EAAE,GAAG,EAAE,SAAS,EAAE,UAAU,EAAE,gBAAgB,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC;IACnF,CAAC;IACD,2CAA2C;IAC3C,4CAA4C;IAC5C,+CAA+C;IAC/C,sCAAsC;IACtC,0CAA0C;IAC1C,sBAAsB;IACtB,OAAO,EAAE,GAAG,EAAE,QAAQ,IAAI,SAAS,EAAE,UAAU,EAAE,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC;AAC3E,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,iBAAiB,CAAC,MAAuB;IACxD,IAAI,MAAM,CAAC,MAAM,KAAK,MAAM,EAAE,CAAC;QAC9B,MAAM,MAAM,GAAG,MAAM,CAAC,UAAU,CAAC,MAAM,GAAG,CAAC,CAAC;QAC5C,MAAM,IAAI,GACT,6CAA6C,MAAM,CAAC,GAAG,EAAE,CAAC;QAC3D,IAAI,MAAM,IAAI,CAAC;YAAE,OAAO,IAAI,CAAC;QAC7B,MAAM,IAAI,GAAG,MAAM,CAAC,UAAU;aAC5B,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC;aACX,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;aAClC,IAAI,CAAC,IAAI,CAAC,CAAC;QACb,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,YAAY,MAAM,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;QACzD,OAAO,CACN,GAAG,IAAI,mBAAmB,MAAM,CAAC,UAAU,CAAC,MAAM,UAAU;YAC5D,uCAAuC,IAAI,GAAG,IAAI,EAAE,CACpD,CAAC;IACH,CAAC;IACD,IAAI,MAAM,CAAC,MAAM,KAAK,MAAM,EAAE,CAAC;QAC9B,OAAO,CACN,iDAAiD;YACjD,+CAA+C;YAC/C,0DAA0D,CAC1D,CAAC;IACH,CAAC;IACD,OAAO,EAAE,CAAC;AACX,CAAC"}
@@ -15,6 +15,9 @@ import { toUrlSlug, isSafeImageUrl } from "../slug.js";
15
15
  import { resolveMyUsername } from "../me.js";
16
16
  import { assertOwned, assertOwnsSeries } from "../ownership.js";
17
17
  import { READ_ONLY } from "./posts.js";
18
+ import { serializeWrite } from "../serial.js";
19
+ import { chooseThumbnail, describeThumbnail } from "../thumbnail.js";
20
+ import { seriesHintSafely, resolveSeriesId } from "../series.js";
18
21
  /**
19
22
  * ★ 발행 차단 지점. 이 값은 파라미터가 아니라 상수다.
20
23
  * true = 임시저장(남에게 보이지 않음), false = 발행(되돌릴 수 없음).
@@ -36,6 +39,22 @@ const QUERY_POST_STATE = `
36
39
  post(input: $input) { id title is_temp meta user { username } }
37
40
  }
38
41
  `;
42
+ /**
43
+ * 썸네일 입력 — 세 상태를 구분해야 한다.
44
+ *
45
+ * 미지정(undefined) → 본문 첫 이미지로 **자동** 채운다
46
+ * URL → 그대로 쓴다
47
+ * `null` / `""` → **자동 채움을 끈다** (일부러 비워 두는 사람의 의도를 지킨다)
48
+ *
49
+ * ★ nullable 을 뺐다가는 "끄는 방법"이 사라진다. 그러면 자동 채움이 강제가 된다.
50
+ */
51
+ const THUMBNAIL_FIELD = z
52
+ .string()
53
+ .refine((v) => v === '' || isSafeImageUrl(v), 'http(s) 이미지 URL 만 허용합니다')
54
+ .nullable()
55
+ .optional()
56
+ .describe('썸네일 이미지 URL (http/https). 생략하면 본문 첫 이미지로 자동 설정한다. ' +
57
+ '자동 설정을 원하지 않으면 null 을 준다');
39
58
  /** 벨로그가 정말 임시저장으로 받았는지 응답으로 확인한다. */
40
59
  function assertStayedDraft(post) {
41
60
  if (post.is_temp !== true) {
@@ -70,21 +89,31 @@ export function registerDraftTools(server, client) {
70
89
  body: z.string().min(1).describe('본문 (마크다운)'),
71
90
  tags: z.array(z.string()).default([]).describe('태그 목록'),
72
91
  url_slug: z.string().optional().describe('생략하면 제목에서 생성'),
73
- thumbnail: z
74
- .string()
75
- .refine(isSafeImageUrl, 'http(s) 이미지 URL 만 허용합니다')
76
- .optional()
77
- .describe('썸네일 이미지 URL (http/https 만)'),
92
+ thumbnail: THUMBNAIL_FIELD,
78
93
  series_id: z
79
94
  .string()
80
95
  .optional()
81
96
  .describe('소속시킬 시리즈 id. ★ 벨로그는 임시저장 단계에서 이걸 무시한다 — ' +
82
- '초안 생성 후 velog_update_draft 로 다시 지정해야 실제로 붙는다'),
97
+ '초안 생성 후 velog_update_draft 로 다시 지정해야 실제로 붙는다. ' +
98
+ '생략하면 결과에 내 시리즈 목록을 함께 돌려준다'),
99
+ series_name: z
100
+ .string()
101
+ .min(1)
102
+ .optional()
103
+ .describe('시리즈 **이름**(id 대신). 저장 전에 내 시리즈에서 찾는다. ' +
104
+ '못 찾으면 저장하지 않고 목록을 알려준다'),
83
105
  },
84
106
  // 되돌릴 수 있는 쓰기다 — 비공개 초안이므로 파괴적이지 않다.
85
107
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
86
- }, async ({ title, body, tags, url_slug, thumbnail, series_id }) => {
108
+ }, async ({ title, body, tags, url_slug, thumbnail, series_id: rawSeriesId, series_name }, extra) =>
109
+ // ★ 같은 대상에 대한 쓰기는 줄을 세운다 — 이유는 src/serial.ts
110
+ serializeWrite('post:new', async () => {
87
111
  client.requireAuth('velog_create_draft');
112
+ let series_id = rawSeriesId?.trim() || undefined;
113
+ // ★ 이름을 id 로. **저장 전에** 한다 — 못 찾으면 아무것도 안 쓴 채 멈춘다.
114
+ if (series_name !== undefined && series_id === undefined) {
115
+ series_id = await resolveSeriesId(client, await resolveMyUsername(client, extra.signal), series_name, 'velog_create_draft', extra.signal);
116
+ }
88
117
  if (series_id)
89
118
  await assertOwnsSeries(client, series_id, 'velog_create_draft');
90
119
  // ★ 초안은 is_private:true 라 벨로그 계수(is_private:false 만 셈)에
@@ -97,12 +126,14 @@ export function registerDraftTools(server, client) {
97
126
  meta: {},
98
127
  ...DRAFT_ONLY, // ★ 마지막에 펼쳐서 위 값들이 덮어쓸 수 없게 한다
99
128
  };
100
- if (thumbnail)
101
- input['thumbnail'] = thumbnail;
129
+ // ★ 본문에 그림이 있으면 썸네일로 쓴다. 결정 근거는 아래에서 그대로 알린다.
130
+ const thumb = chooseThumbnail(thumbnail, body);
131
+ if (thumb.url)
132
+ input['thumbnail'] = thumb.url;
102
133
  if (series_id)
103
134
  input['series_id'] = series_id;
104
135
  // mutate 는 재시도하지 않는다 — 응답 유실 시 초안이 중복 생성된다.
105
- const data = await client.mutate(MUTATION_WRITE_POST, { input });
136
+ const data = await client.mutate(MUTATION_WRITE_POST, { input }, { signal: extra.signal });
106
137
  assertStayedDraft(data.writePost);
107
138
  // ★ 벨로그는 임시저장 생성 시 series_id 를 조용히 버린다:
108
139
  // // apps/server/src/services/PostApiService/index.mts
@@ -113,9 +144,11 @@ export function registerDraftTools(server, client) {
113
144
  ? '\n\n⚠️ 시리즈는 **적용되지 않았습니다.** 벨로그가 임시저장 생성 단계에서는' +
114
145
  ' series_id 를 무시합니다. 방금 만든 초안 id 로 velog_update_draft 를' +
115
146
  ' 한 번 호출하면 그때 실제로 붙습니다.'
116
- : '';
117
- return textResult(draftResult(data.writePost, '저장') + seriesNote);
118
- });
147
+ : // ★ 힌트 조회가 실패해도 저장은 이미 끝났다 — 전부 삼킨다.
148
+ await seriesHintSafely(client, async () => data.writePost.user?.username ??
149
+ (await resolveMyUsername(client, extra.signal)), series_id, extra.signal);
150
+ return textResult(draftResult(data.writePost, '저장') + describeThumbnail(thumb) + seriesNote);
151
+ }));
119
152
  server.registerTool('velog_update_draft', {
120
153
  title: '벨로그 초안 수정',
121
154
  description: '기존 초안을 **통째로 교체**한다. 부분 수정이 아니다. ' +
@@ -130,16 +163,21 @@ export function registerDraftTools(server, client) {
130
163
  body: z.string().min(1).describe('본문 전체 (마크다운). 부분 수정이 아니라 교체다'),
131
164
  tags: z.array(z.string()).default([]),
132
165
  url_slug: z.string().optional(),
133
- thumbnail: z
134
- .string()
135
- .refine(isSafeImageUrl, 'http(s) 이미지 URL 만 허용합니다')
136
- .optional(),
166
+ thumbnail: THUMBNAIL_FIELD,
137
167
  series_id: z.string().optional(),
168
+ series_name: z
169
+ .string()
170
+ .min(1)
171
+ .optional()
172
+ .describe('시리즈 **이름**(id 대신). 저장 전에 내 시리즈에서 찾는다. ' +
173
+ '못 찾으면 저장하지 않고 목록을 알려준다'),
138
174
  },
139
175
  // ★ destructive 가 맞다. 생략 필드가 보존되지 않고 초기화된다 —
140
176
  // MCP 명세상 destructiveHint:false 는 '추가만 한다'는 뜻이라 거짓이 된다.
141
177
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true },
142
- }, async ({ id, title, body, tags, url_slug, thumbnail, series_id }) => {
178
+ }, async ({ id, title, body, tags, url_slug, thumbnail, series_id: rawSeriesId, series_name }, extra) =>
179
+ // ★ 같은 대상에 대한 쓰기는 줄을 세운다 — 이유는 src/serial.ts
180
+ serializeWrite(`post:${id}`, async () => {
143
181
  client.requireAuth('velog_update_draft');
144
182
  // ★ 두 가지를 확인한 뒤에야 수정한다.
145
183
  // ① 내 글인가 — 벨로그 서버가 edit 에서 소유권을 안 본다(ownership.ts)
@@ -150,6 +188,10 @@ export function registerDraftTools(server, client) {
150
188
  throw new Error(`id=${id} 인 글을 찾지 못했습니다. velog_list_drafts 로 id 를 확인하세요.`);
151
189
  }
152
190
  await assertOwned(client, before.post, 'velog_update_draft');
191
+ let series_id = rawSeriesId?.trim() || undefined;
192
+ if (series_name !== undefined && series_id === undefined) {
193
+ series_id = await resolveSeriesId(client, before.post.user?.username ?? (await resolveMyUsername(client, extra.signal)), series_name, 'velog_update_draft', extra.signal);
194
+ }
153
195
  if (series_id)
154
196
  await assertOwnsSeries(client, series_id, 'velog_update_draft');
155
197
  if (before.post.is_temp !== true) {
@@ -168,13 +210,12 @@ export function registerDraftTools(server, client) {
168
210
  meta: before.post.meta ?? {},
169
211
  ...DRAFT_ONLY,
170
212
  };
171
- if (thumbnail)
172
- input['thumbnail'] = thumbnail;
213
+ const thumb = chooseThumbnail(thumbnail, body);
214
+ if (thumb.url)
215
+ input['thumbnail'] = thumb.url;
173
216
  if (series_id)
174
217
  input['series_id'] = series_id;
175
- const data = await client.mutate(MUTATION_EDIT_POST, {
176
- input,
177
- });
218
+ const data = await client.mutate(MUTATION_EDIT_POST, { input }, { signal: extra.signal });
178
219
  // ★ editPost 의 응답으로는 사후 상태를 알 수 없다. 공식 구현이
179
220
  // `return { ...post, url_slug: data.url_slug }` 로 **갱신 전에 읽은**
180
221
  // post 를 돌려주고 url_slug 만 덮기 때문이다
@@ -195,8 +236,14 @@ export function registerDraftTools(server, client) {
195
236
  // ★ data.editPost 는 '갱신 전' 객체다(공식 서버가 그렇게 반환한다).
196
237
  // 그걸로 문구를 만들면 제목을 바꿔도 옛 제목이 표시된다.
197
238
  // 이미 재조회했으므로 그 결과를 얹는다.
198
- return textResult(draftResult({ ...data.editPost, title: after.post.title ?? data.editPost.title ?? null }, '수정'));
199
- });
239
+ // ★ 초안 수정은 series_id 가 실제로 먹는 경로다(생성과 달리). 그래서
240
+ // 여기서는 "안 넣었으면 무엇에 넣을 수 있는지"를 알리는 값이 크다.
241
+ const ownerName = before.post.user?.username;
242
+ const seriesNote = await seriesHintSafely(client, async () => ownerName ?? (await resolveMyUsername(client, extra.signal)), series_id, extra.signal);
243
+ return textResult(draftResult({ ...data.editPost, title: after.post.title ?? data.editPost.title ?? null }, '수정') +
244
+ describeThumbnail(thumb) +
245
+ seriesNote);
246
+ }));
200
247
  server.registerTool('velog_list_drafts', {
201
248
  title: '내 초안 목록',
202
249
  description: '내 임시저장 글 목록. 초안을 이어 쓰거나 수정하기 전에 id 를 여기서 확인한다.',
@@ -1 +1 @@
1
- {"version":3,"file":"drafts.js","sourceRoot":"","sources":["../../src/tools/drafts.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"drafts.js","sourceRoot":"","sources":["../../src/tools/drafts.ts"],"names":[],"mappings":"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"}
@@ -15,6 +15,31 @@ import { slugify } from "../slug.js";
15
15
  import { fetchAllPosts } from "./stats.js";
16
16
  import { READ_ONLY } from "./posts.js";
17
17
  import { resolveMyUsername } from "../me.js";
18
+ /** 벨로그 커넥션 풀(limit 5)을 배려하는 요청 간격. */
19
+ const GAP_MS = 250;
20
+ /**
21
+ * 한 호출에 쓸 수 있는 시간.
22
+ *
23
+ * ★ 왜 필요한가 — MCP SDK 의 클라이언트측 기본 요청 타임아웃은 60초다
24
+ * (shared/protocol.js: DEFAULT_REQUEST_TIMEOUT_MSEC = 60000).
25
+ * 그런데 이 도구는 글 한 편마다 상세를 따로 받는다. 실측(2026-08-07, 벨로그
26
+ * 공개 글 3편): 본문 왕복 평균 1,313ms. 간격 250ms 를 더하면 편당 약 1.5초다.
27
+ * **기본값 limit=50 이면 약 78초** — 60초를 넘는다. 즉 기본 인자로 부르면
28
+ * 클라이언트가 먼저 포기했고, 그런데도 루프는 계속 돌며 파일을 썼다.
29
+ * 화면에는 '실패'가 뜨는데 디스크에는 파일이 생기는 상태였다.
30
+ *
31
+ * 그래서 60초 안쪽에서 **우리가 먼저 멈추고 무엇을 했는지 보고한다.**
32
+ * 파일 이름이 글 순서로 정해지므로 같은 out_dir 로 다시 부르면 그대로 이어진다.
33
+ */
34
+ const TIME_BUDGET_MS = 50_000;
35
+ /**
36
+ * 한 편을 더 시작하기 전에 남겨둬야 할 여유.
37
+ *
38
+ * 데드라인 직전에 루프로 들어가면 간격 250ms 에 상세 요청 한 번이 통째로 붙는다.
39
+ * 실측 왕복이 1.3초대이므로 3초면 정상 한 편은 넉넉히 들어가고, 안 들어갈 만하면
40
+ * 아예 시작하지 않아 예산을 지킨다.
41
+ */
42
+ const PER_POST_RESERVE_MS = 3_000;
18
43
  /** YAML 문자열 값. 따옴표·백슬래시를 이스케이프해 프론트매터가 깨지지 않게 한다. */
19
44
  function yamlString(value) {
20
45
  return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
@@ -80,28 +105,48 @@ export function registerExportTools(server, client) {
80
105
  // 벨로그 쪽 상태는 안 바뀌지만 로컬 파일은 '덮어쓴다'. MCP 명세상
81
106
  // destructiveHint:false 는 '추가만 한다'는 뜻이라 거짓이 된다.
82
107
  annotations: { ...READ_ONLY, readOnlyHint: false, destructiveHint: true },
83
- }, async ({ username, out_dir, limit }) => {
84
- const target = username ?? (await resolveMyUsername(client));
108
+ }, async ({ username, out_dir, limit }, extra) => {
109
+ // ★ 예산 시계는 **핸들러가 시작하자마자** 켠다. 예전엔 목록 조회가 끝난
110
+ // 뒤에 켰는데, 그 앞의 계정 조회·목록 4페이지가 이미 몇 초를 먹는다.
111
+ // '한 호출에 쓸 수 있는 시간'을 재는데 그 앞을 빼면 예산이 아니다.
112
+ const deadline = Date.now() + TIME_BUDGET_MS;
113
+ const target = username ?? (await resolveMyUsername(client, extra.signal));
85
114
  const dir = resolve(out_dir);
86
115
  await mkdir(dir, { recursive: true });
87
116
  const maxPages = Math.ceil(limit / 50);
88
- const { posts } = await fetchAllPosts(client, target, maxPages);
117
+ const { posts } = await fetchAllPosts(client, target, maxPages, extra.signal);
89
118
  const targets = posts.slice(0, limit);
90
119
  if (targets.length === 0)
91
120
  return textResult(`@${target} 의 공개 글이 없습니다.`);
92
121
  const written = [];
93
122
  const failed = [];
123
+ /** 다 못 돈 이유. 없으면 끝까지 돈 것이다. */
124
+ let stoppedBy = null;
94
125
  for (const [index, summary] of targets.entries()) {
126
+ // ★ 클라이언트가 취소했으면 즉시 멈춘다. 여기서 안 보면 사용자는 이미
127
+ // 포기했는데 파일은 계속 쌓인다 — 화면과 디스크가 어긋난다.
128
+ if (extra.signal.aborted) {
129
+ stoppedBy = '취소';
130
+ break;
131
+ }
132
+ // ★ 시간 예산. 이유는 TIME_BUDGET_MS 주석 참고.
133
+ // ★ 남은 시간이 '한 편 분량'도 안 되면 시작하지 않는다. 데드라인
134
+ // 직전에 들어가면 간격 250ms + 상세 요청(최대 재시도 예산)이
135
+ // 그대로 붙어 예산을 훌쩍 넘긴다.
136
+ if (Date.now() + PER_POST_RESERVE_MS >= deadline) {
137
+ stoppedBy = '시간';
138
+ break;
139
+ }
95
140
  // 벨로그 커넥션 풀(limit 5)을 배려한다. 몰아치면 상대가 먼저 죽는다.
96
141
  if (index > 0)
97
- await new Promise((r) => setTimeout(r, 250));
142
+ await new Promise((r) => setTimeout(r, GAP_MS));
98
143
  try {
99
144
  // 목록에는 body 가 없으므로 한 편씩 상세를 받는다.
100
145
  const data = await client.request(QUERY_POST,
101
146
  // url_slug 가 없으면 id 로 조회한다.
102
147
  summary.url_slug
103
148
  ? { input: { username: target, url_slug: summary.url_slug } }
104
- : { input: { id: summary.id } });
149
+ : { input: { id: summary.id } }, { signal: extra.signal });
105
150
  if (!data.post) {
106
151
  failed.push(`${summary.title} (본문 조회 실패)`);
107
152
  continue;
@@ -111,16 +156,32 @@ export function registerExportTools(server, client) {
111
156
  written.push(name);
112
157
  }
113
158
  catch (error) {
159
+ if (extra.signal.aborted) {
160
+ stoppedBy = '취소';
161
+ break;
162
+ }
114
163
  const reason = error instanceof Error ? error.message : String(error);
115
164
  failed.push(`${summary.title} — ${reason}`);
116
165
  }
117
166
  }
167
+ const done = written.length + failed.length;
118
168
  const lines = [
119
169
  `✅ ${written.length}편을 저장했습니다.`,
120
170
  '',
121
171
  `- 위치: ${dir}`,
122
172
  `- 형식: YAML 프론트매터 + 마크다운 본문`,
123
173
  ];
174
+ if (stoppedBy !== null) {
175
+ const left = targets.length - done;
176
+ lines.push('', stoppedBy === '취소'
177
+ ? `⏹️ 요청이 취소되어 ${done}/${targets.length}편에서 멈췄습니다.`
178
+ : `⏱️ ${TIME_BUDGET_MS / 1000}초 예산에 도달해 ${done}/${targets.length}편에서 멈췄습니다.`, done === 0
179
+ ? '준비 조회(계정·글 목록)만으로 예산을 다 썼습니다. 벨로그가 느린 ' +
180
+ '상태일 수 있으니 잠시 뒤 다시 시도하세요 — limit 을 줄여도 소용없습니다.'
181
+ : `남은 ${left}편은 저장되지 않았습니다. **같은 out_dir 로 다시 부르면** ` +
182
+ '이미 받은 파일을 덮어쓰며 이어집니다(파일 이름이 글 순서로 정해집니다). ' +
183
+ '한 번에 끝내려면 limit 을 줄여 나눠 부르세요.');
184
+ }
124
185
  if (failed.length > 0) {
125
186
  lines.push('', `⚠️ ${failed.length}편 실패:`, ...failed.slice(0, 10).map((f) => ` - ${f}`));
126
187
  if (failed.length > 10)
@@ -1 +1 @@
1
- {"version":3,"file":"export.js","sourceRoot":"","sources":["../../src/tools/export.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"export.js","sourceRoot":"","sources":["../../src/tools/export.ts"],"names":[],"mappings":"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"}
@@ -25,6 +25,7 @@ import { z } from 'zod';
25
25
  import { textResult } from "../format.js";
26
26
  import { ICON_NAMES, TONE_NAMES, formatAudit, formatCoverAudit, renderCover, renderDiagram, } from "../render/index.js";
27
27
  import { isHexColor } from "../render/tones.js";
28
+ import { makeSerializer } from "../serial.js";
28
29
  /** 우리가 올리는 것 — 서버 상한(30MB)보다 낮게 잡는다. */
29
30
  const MAX_UPLOAD_BYTES = 10 * 1024 * 1024;
30
31
  /**
@@ -321,6 +322,17 @@ const planeSchema = z.object({
321
322
  function markdown(alt, url) {
322
323
  return `![${alt}](${url})`;
323
324
  }
325
+ /**
326
+ * 업로드도 줄을 세운다.
327
+ *
328
+ * ★ 파일당 10MB 상한은 있는데 **동시성 상한이 없었다.** 한 요청이 10MB 버퍼와
329
+ * 그 multipart 사본을 최대 60초 들고 있으므로, 병렬 호출 수만큼 그대로 곱해진다.
330
+ * 렌더는 같은 이유로 이미 줄을 세워 크롬 메모리를 1GB 에 묶어뒀는데
331
+ * 업로드만 빠져 있었다.
332
+ * ★ 벨로그 쪽 사정도 같은 방향이다 — 1분 20건을 넘기면 계정을 막는다
333
+ * (ImageService.detectAbuse). 몰아치지 않는 편이 안전하다.
334
+ */
335
+ const serializeUpload = makeSerializer();
324
336
  export function registerImageTools(server, client) {
325
337
  /** 렌더 결과를 올리고 결과 문구를 만든다. 감사에 걸린 게 있으면 올리지 않는다. */
326
338
  async function finish(args) {
@@ -360,11 +372,19 @@ export function registerImageTools(server, client) {
360
372
  return lines;
361
373
  }
362
374
  if (args.upload) {
363
- const bytes = new Uint8Array(await readFile(args.pngPath));
364
- const uploadOptions = args.postId
365
- ? { type: 'post', contentType: 'image/png', refId: args.postId }
366
- : { type: 'post', contentType: 'image/png' };
367
- const url = await client.uploadImage(bytes, `${basename(args.pngPath, '.png')}.png`, uploadOptions);
375
+ // ★ 파일 읽기도 **줄 안에서** 한다. 밖에서 읽으면 대기 중인 요청마다
376
+ // 10MB 버퍼가 메모리에 올라간 채 순서를 기다린다 — 줄을 세운 이유
377
+ // (메모리 상한)가 그대로 사라진다. 코덱스 교차검증에서 잡았다.
378
+ const url = await serializeUpload(async () => {
379
+ // ★ 줄에서 기다리는 동안 취소됐을 수 있다. 읽기 전에 확인한다 —
380
+ // 안 그러면 이미 포기한 요청이 파일부터 읽고 버퍼를 잡는다.
381
+ args.signal.throwIfAborted();
382
+ const bytes = new Uint8Array(await readFile(args.pngPath));
383
+ const uploadOptions = args.postId
384
+ ? { type: 'post', contentType: 'image/png', refId: args.postId, signal: args.signal }
385
+ : { type: 'post', contentType: 'image/png', signal: args.signal };
386
+ return client.uploadImage(bytes, `${basename(args.pngPath, '.png')}.png`, uploadOptions);
387
+ });
368
388
  lines.push('본문에 붙여넣을 마크다운:');
369
389
  lines.push('');
370
390
  lines.push(markdown(args.alt, url));
@@ -429,7 +449,7 @@ export function registerImageTools(server, client) {
429
449
  .describe('붙일 글 id. 주면 벨로그가 내 글인지 확인한 뒤 받는다'),
430
450
  },
431
451
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
432
- }, async (args) => {
452
+ }, async (args, extra) => {
433
453
  if (args.upload)
434
454
  client.requireAuth('velog_render_diagram');
435
455
  const spec = {
@@ -466,6 +486,7 @@ export function registerImageTools(server, client) {
466
486
  alt: args.alt ?? args.title,
467
487
  ...(args.post_id ? { postId: args.post_id } : {}),
468
488
  what: '다이어그램',
489
+ signal: extra.signal,
469
490
  })).join('\n'));
470
491
  });
471
492
  server.registerTool('velog_render_cover', {
@@ -484,7 +505,7 @@ export function registerImageTools(server, client) {
484
505
  post_id: z.string().max(64).optional(),
485
506
  },
486
507
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
487
- }, async (args) => {
508
+ }, async (args, extra) => {
488
509
  if (args.upload)
489
510
  client.requireAuth('velog_render_cover');
490
511
  const spec = { title: args.title };
@@ -512,6 +533,7 @@ export function registerImageTools(server, client) {
512
533
  alt: args.title,
513
534
  ...(args.post_id ? { postId: args.post_id } : {}),
514
535
  what: '표지',
536
+ signal: extra.signal,
515
537
  })).join('\n'));
516
538
  });
517
539
  server.registerTool('velog_upload_image', {
@@ -530,7 +552,7 @@ export function registerImageTools(server, client) {
530
552
  post_id: z.string().max(64).optional().describe('붙일 글 id (서버가 소유권을 확인한다)'),
531
553
  },
532
554
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
533
- }, async (args) => {
555
+ }, async (args, extra) => {
534
556
  client.requireAuth('velog_upload_image');
535
557
  // ★ 문자열 그대로 비교하면 `/tmp/x/./a.png` 하나로 빠져나간다.
536
558
  // 경로를 정규화하고, 심볼릭 링크까지 푼 형태로도 본다.
@@ -538,12 +560,26 @@ export function registerImageTools(server, client) {
538
560
  throw new Error('이 PNG 는 이 서버의 자가감사에서 떨어진 산출물입니다 — 올릴 수 없습니다.\n' +
539
561
  '그림을 고쳐 다시 그리세요 (velog_render_diagram).');
540
562
  }
541
- const { bytes, kind } = await readImageFile(args.path);
542
563
  const name = basename(args.path);
543
- const uploadOptions = args.post_id
544
- ? { type: args.type, contentType: kind.mime, refId: args.post_id }
545
- : { type: args.type, contentType: kind.mime };
546
- const url = await client.uploadImage(bytes, name, uploadOptions);
564
+ // ★ 읽기부터 줄 안이다 — 이유는 위 finish() 의 주석과 같다.
565
+ // 대기 중인 요청이 10MB 씩 들고 서 있으면 상한을 둔 의미가 없다.
566
+ const { url, kind, bytes } = await serializeUpload(async () => {
567
+ extra.signal.throwIfAborted();
568
+ // ★ 거부 목록은 줄 밖에서 한 번 봤지만, 기다리는 사이에 이 PNG 가
569
+ // 감사에서 떨어져 목록에 오를 수 있다. 읽기 직전에 다시 본다.
570
+ if (await isRejected(args.path)) {
571
+ throw new Error('이 PNG 는 이 서버의 자가감사에서 떨어진 산출물입니다 — 올릴 수 없습니다.');
572
+ }
573
+ const read = await readImageFile(args.path);
574
+ const uploadOptions = args.post_id
575
+ ? { type: args.type, contentType: read.kind.mime, refId: args.post_id, signal: extra.signal }
576
+ : { type: args.type, contentType: read.kind.mime, signal: extra.signal };
577
+ return {
578
+ url: await client.uploadImage(read.bytes, name, uploadOptions),
579
+ kind: read.kind,
580
+ bytes: read.bytes,
581
+ };
582
+ });
547
583
  return textResult([
548
584
  `✅ 올렸습니다 — ${kind.mime} · ${(bytes.length / 1024).toFixed(0)}KB`,
549
585
  '',