@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.
- package/LICENSE +21 -0
- package/README.ko.md +366 -0
- package/README.md +381 -0
- package/dist/auth.d.ts +57 -0
- package/dist/auth.js +124 -0
- package/dist/auth.js.map +1 -0
- package/dist/capabilities.d.ts +50 -0
- package/dist/capabilities.js +60 -0
- package/dist/capabilities.js.map +1 -0
- package/dist/client.d.ts +113 -0
- package/dist/client.js +322 -0
- package/dist/client.js.map +1 -0
- package/dist/format.d.ts +31 -0
- package/dist/format.js +66 -0
- package/dist/format.js.map +1 -0
- package/dist/graphql.d.ts +29 -0
- package/dist/graphql.js +82 -0
- package/dist/graphql.js.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +149 -0
- package/dist/index.js.map +1 -0
- package/dist/me.d.ts +23 -0
- package/dist/me.js +35 -0
- package/dist/me.js.map +1 -0
- package/dist/ownership.d.ts +42 -0
- package/dist/ownership.js +62 -0
- package/dist/ownership.js.map +1 -0
- package/dist/plugin-env.d.ts +67 -0
- package/dist/plugin-env.js +102 -0
- package/dist/plugin-env.js.map +1 -0
- package/dist/ratelimit.d.ts +48 -0
- package/dist/ratelimit.js +79 -0
- package/dist/ratelimit.js.map +1 -0
- package/dist/render/chrome.d.ts +77 -0
- package/dist/render/chrome.js +287 -0
- package/dist/render/chrome.js.map +1 -0
- package/dist/render/cover.d.ts +29 -0
- package/dist/render/cover.js +195 -0
- package/dist/render/cover.js.map +1 -0
- package/dist/render/icons.d.ts +22 -0
- package/dist/render/icons.js +158 -0
- package/dist/render/icons.js.map +1 -0
- package/dist/render/index.d.ts +32 -0
- package/dist/render/index.js +137 -0
- package/dist/render/index.js.map +1 -0
- package/dist/render/page.d.ts +89 -0
- package/dist/render/page.js +761 -0
- package/dist/render/page.js.map +1 -0
- package/dist/render/tones.d.ts +30 -0
- package/dist/render/tones.js +46 -0
- package/dist/render/tones.js.map +1 -0
- package/dist/slug.d.ts +42 -0
- package/dist/slug.js +79 -0
- package/dist/slug.js.map +1 -0
- package/dist/tools/discover.d.ts +6 -0
- package/dist/tools/discover.js +106 -0
- package/dist/tools/discover.js.map +1 -0
- package/dist/tools/drafts.d.ts +23 -0
- package/dist/tools/drafts.js +227 -0
- package/dist/tools/drafts.js.map +1 -0
- package/dist/tools/export.d.ts +21 -0
- package/dist/tools/export.js +132 -0
- package/dist/tools/export.js.map +1 -0
- package/dist/tools/images.d.ts +34 -0
- package/dist/tools/images.js +556 -0
- package/dist/tools/images.js.map +1 -0
- package/dist/tools/posts.d.ts +14 -0
- package/dist/tools/posts.js +82 -0
- package/dist/tools/posts.js.map +1 -0
- package/dist/tools/profile-edit.d.ts +15 -0
- package/dist/tools/profile-edit.js +216 -0
- package/dist/tools/profile-edit.js.map +1 -0
- package/dist/tools/profile.d.ts +9 -0
- package/dist/tools/profile.js +133 -0
- package/dist/tools/profile.js.map +1 -0
- package/dist/tools/publish.d.ts +16 -0
- package/dist/tools/publish.js +424 -0
- package/dist/tools/publish.js.map +1 -0
- package/dist/tools/stats.d.ts +32 -0
- package/dist/tools/stats.js +154 -0
- package/dist/tools/stats.js.map +1 -0
- package/dist/types.d.ts +42 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/docs/PRD.md +146 -0
- package/docs/api-reference.md +329 -0
- package/docs/architecture.md +112 -0
- package/docs/decisions/0001-why-build-our-own.md +89 -0
- package/docs/decisions/0002-draft-only-write.md +84 -0
- package/docs/decisions/0003-token-env-only.md +109 -0
- package/docs/decisions/0004-capability-model.md +123 -0
- package/docs/decisions/0005-render-in-server.md +117 -0
- package/docs/decisions/0006-ship-as-plugin.md +532 -0
- package/docs/security.md +384 -0
- package/docs/tools.md +404 -0
- package/npm-shrinkwrap.json +2345 -0
- package/package.json +61 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"page.js","sourceRoot":"","sources":["../../src/render/page.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAGH,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,EAAE,cAAc,EAAc,KAAK,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AA+CrE;;;GAGG;AACH,MAAM,CAAC,MAAM,OAAO,GAAG,IAAI,CAAC;AAC5B,MAAM,CAAC,MAAM,QAAQ,GAAG,SAAS,CAAC;AA+DlC,SAAS,OAAO,CAAC,IAAiB;IACjC,MAAM,MAAM,GAAG,CAAC,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QAC/E,GAAG,EAAE,CAAC,CAAC,GAAG;QACV,IAAI,EAAE,CAAC,CAAC,IAAI;QACZ,KAAK,EAAE,CAAC,CAAC,KAAK;QACd,IAAI,EAAE,CAAC,CAAC,IAAI,IAAI,EAAE;KAClB,CAAC,CAAC,CAAC;IAEJ,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC;QACvC,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,IAAI,CAAC,EAAE;QACnB,CAAC,EAAE,CAAC,CAAC,CAAC;QACN,CAAC,EAAE,CAAC,CAAC,CAAC;QACN,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,mBAAmB;QAChC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;QACX,KAAK,EAAE,CAAC,CAAC,KAAK;QACd,GAAG,EAAE,CAAC,CAAC,GAAG,IAAI,EAAE;QAChB,IAAI,EAAE,CAAC,CAAC,IAAI,IAAI,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE;QAC3C,SAAS,EAAE,IAAI,CAAC,CAAC,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC,KAAK;QAC3C,GAAG,EAAE,CAAC,CAAC,GAAG,IAAI,EAAE;QAChB,QAAQ,EAAE,IAAI,CAAC,CAAC,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC,KAAK;KACzC,CAAC,CAAC,CAAC;IAEJ,gDAAgD;IAChD,MAAM,KAAK,GAAoC,EAAE,CAAC;IAClD,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;QACvB,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QAC3B,IAAI,CAAC,CAAC,IAAI,IAAI,IAAI;YAAE,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC1C,CAAC;IAED,MAAM,MAAM,GAAG,CAAC,IAAI,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;QAC5C,MAAM,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QAC/B,MAAM,GAAG,GAAmC;YAC3C,IAAI,EAAE,CAAC,CAAC,IAAI;YACZ,GAAG,EAAE,CAAC,CAAC,GAAG,IAAI,EAAE;YAChB,IAAI,EAAE,CAAC,CAAC,IAAI;YACZ,MAAM,EAAE,CAAC,CAAC,MAAM;YAChB,OAAO,EAAE,CAAC,CAAC,OAAO,IAAI,EAAE;SACxB,CAAC;QACF,IAAI,CAAC,CAAC,CAAC,KAAK,SAAS;YAAE,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QACnC,IAAI,CAAC,CAAC,CAAC,KAAK,SAAS;YAAE,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QACnC,IAAI,CAAC,CAAC,CAAC,KAAK,SAAS;YAAE,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QACnC,IAAI,CAAC,CAAC,CAAC,KAAK,SAAS;YAAE,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QACnC,OAAO,GAAG,CAAC;IACZ,CAAC,CAAC,CAAC;IAEH,MAAM,aAAa,GAAG,MAAM,CAAC,CAAC,CAAC,EAAE,GAAG,IAAI,GAAG,CAAC;IAC5C,MAAM,KAAK,GAAG,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QAC5C,KAAK,EAAE,CAAC,CAAC,KAAK,IAAI,aAAa;QAC/B,IAAI,EAAE,CAAC,CAAC,IAAI,IAAI,EAAE;QAClB,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE;QACd,MAAM,EAAE,CAAC,CAAC,MAAM,IAAI,EAAE;QACtB,KAAK,EAAE,CAAC,CAAC,KAAK,IAAI,EAAE;QACpB,OAAO,EAAE,CAAC,CAAC,QAAQ,IAAI,IAAI;QAC3B,WAAW,EAAE,CAAC,CAAC,YAAY,IAAI,QAAQ;KACvC,CAAC,CAAC,CAAC;IAEJ,OAAO;QACN,KAAK,EAAE,IAAI,CAAC,KAAK;QACjB,QAAQ,EAAE,IAAI,CAAC,QAAQ,IAAI,EAAE;QAC7B,MAAM,EAAE,IAAI,CAAC,MAAM,IAAI,IAAI;QAC3B,MAAM;QACN,MAAM;QACN,KAAK;QACL,KAAK;QACL,KAAK;QACL,MAAM,EAAE,OAAO;QACf,OAAO,EAAE,QAAQ;KACjB,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,SAAS,QAAQ,CAAC,KAAc;IAC/B,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;AACvD,CAAC;AAED,MAAM,KAAK,GAAG;;;;;;;;;;;;;;;CAeb,CAAC;AAEF,gEAAgE;AAChE,8CAA8C;AAC9C,4CAA4C;AAC5C,MAAM,MAAM,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAwkBd,CAAC;AAEF,MAAM,UAAU,gBAAgB,CAAC,IAAiB;IACjD,MAAM,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/B,OAAO;QACN,iBAAiB;QACjB,uEAAuE;QACvE,UAAU,KAAK,UAAU;QACzB,eAAe;QACf,6EAA6E;QAC7E,6CAA6C,QAAQ,CAAC,QAAQ,CAAC,WAAW;QAC1E,WAAW,MAAM,WAAW;QAC5B,gBAAgB;KAChB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACd,CAAC;AAED,sCAAsC;AACtC,MAAM,UAAU,UAAU,CAAC,GAAW;IACrC,MAAM,MAAM,GAAG,0CAA0C,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACpE,IAAI,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACjB,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAA6B,CAAC;QAC/D,MAAM,IAAI,KAAK,CACd,eAAe,IAAI,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,KAAK;YACnC,OAAO,OAAO,WAAW,CAAC,QAAQ,GAAG,SAAS,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,UAAU;YACpE,wCAAwC;YACxC,yBAAyB,CAC1B,CAAC;IACH,CAAC;IACD,MAAM,MAAM,GAAG,oCAAoC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC9D,IAAI,MAAM,EAAE,CAAC,CAAC,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,mBAAmB,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IACjE,MAAM,KAAK,GAAG,kCAAkC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC3D,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACjB,MAAM,IAAI,KAAK,CACd,6CAA6C;YAC5C,8BAA8B,CAC/B,CAAC;IACH,CAAC;IACD,mDAAmD;IACnD,OAAO,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAgB,CAAC;AAC5C,CAAC;AAED,oCAAoC;AACpC,MAAM,UAAU,WAAW,CAAC,CAAc;IACzC,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,MAAM,GAAG,GAAG,CAAC,KAAa,EAAE,KAAe,EAAQ,EAAE;QACpD,IAAI,KAAK,CAAC,MAAM;YAAE,KAAK,CAAC,IAAI,CAAC,KAAK,KAAK,IAAI,KAAK,CAAC,MAAM,MAAM,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IACnF,CAAC,CAAC;IACF,GAAG,CAAC,SAAS,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;IACvB,GAAG,CAAC,mBAAmB,EAAE,CAAC,CAAC,UAAU,CAAC,CAAC;IACvC,GAAG,CAAC,WAAW,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC;IAC1B,GAAG,CAAC,QAAQ,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC;IACzB,GAAG,CAAC,UAAU,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC;IAC3B,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC;IACtB,IAAI,CAAC,KAAK,CAAC,MAAM;QAAE,OAAO,kCAAkC,CAAC;IAC7D,OAAO,oBAAoB,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;AAC/C,CAAC;AAED,MAAM,CAAC,MAAM,SAAS,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 색 팔레트.
|
|
3
|
+
*
|
|
4
|
+
* ★ 색을 자유 문자열로 받지 않는 이유가 두 가지다.
|
|
5
|
+
*
|
|
6
|
+
* ① 보안 — 이 값들은 결국 브라우저에서 SVG 속성이 된다. `url(#x)` 나
|
|
7
|
+
* `image-set(...)` 같은 값이 들어가면 우리가 의도하지 않은 참조가 생긴다.
|
|
8
|
+
* (렌더러가 네트워크를 못 쓰게 막아두긴 했지만, 입력 단계에서 한 번 더 좁힌다.)
|
|
9
|
+
* ② 일관성 — 파스텔 배경 + 진한 테두리 조합을 매번 모델이 고르면 그림마다
|
|
10
|
+
* 톤이 달라진다. 이름으로만 고르게 해서 어느 글에 넣어도 같은 인상이 되게 한다.
|
|
11
|
+
*/
|
|
12
|
+
/** 그룹 박스용 — 옅은 배경 + 같은 계열의 진한 테두리. */
|
|
13
|
+
export interface Tone {
|
|
14
|
+
readonly fill: string;
|
|
15
|
+
readonly stroke: string;
|
|
16
|
+
/** 이 톤을 액센트(배지·선)로 쓸 때의 진한 색 */
|
|
17
|
+
readonly solid: string;
|
|
18
|
+
}
|
|
19
|
+
export declare const TONES: Record<string, Tone>;
|
|
20
|
+
export declare const TONE_NAMES: ReadonlyArray<string>;
|
|
21
|
+
export declare function tone(name: string | undefined, fallback?: string): Tone;
|
|
22
|
+
export declare function isHexColor(value: string): boolean;
|
|
23
|
+
/** 평면(흐름 종류) 기본값 — 요청/외부/데이터/모니터링. */
|
|
24
|
+
export interface Plane {
|
|
25
|
+
readonly key: string;
|
|
26
|
+
readonly name: string;
|
|
27
|
+
readonly color: string;
|
|
28
|
+
readonly dash?: string | undefined;
|
|
29
|
+
}
|
|
30
|
+
export declare const DEFAULT_PLANES: readonly Plane[];
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 색 팔레트.
|
|
3
|
+
*
|
|
4
|
+
* ★ 색을 자유 문자열로 받지 않는 이유가 두 가지다.
|
|
5
|
+
*
|
|
6
|
+
* ① 보안 — 이 값들은 결국 브라우저에서 SVG 속성이 된다. `url(#x)` 나
|
|
7
|
+
* `image-set(...)` 같은 값이 들어가면 우리가 의도하지 않은 참조가 생긴다.
|
|
8
|
+
* (렌더러가 네트워크를 못 쓰게 막아두긴 했지만, 입력 단계에서 한 번 더 좁힌다.)
|
|
9
|
+
* ② 일관성 — 파스텔 배경 + 진한 테두리 조합을 매번 모델이 고르면 그림마다
|
|
10
|
+
* 톤이 달라진다. 이름으로만 고르게 해서 어느 글에 넣어도 같은 인상이 되게 한다.
|
|
11
|
+
*/
|
|
12
|
+
export const TONES = {
|
|
13
|
+
slate: { fill: '#f8fafc', stroke: '#94a3b8', solid: '#475569' },
|
|
14
|
+
gray: { fill: '#f9fafb', stroke: '#9ca3af', solid: '#4b5563' },
|
|
15
|
+
blue: { fill: '#eff6ff', stroke: '#3b82f6', solid: '#2563eb' },
|
|
16
|
+
green: { fill: '#f0fdf4', stroke: '#22c55e', solid: '#16a34a' },
|
|
17
|
+
amber: { fill: '#fff7ed', stroke: '#f59e0b', solid: '#d97706' },
|
|
18
|
+
yellow: { fill: '#fefce8', stroke: '#ca8a04', solid: '#a16207' },
|
|
19
|
+
purple: { fill: '#faf5ff', stroke: '#a855f7', solid: '#7e5bd0' },
|
|
20
|
+
teal: { fill: '#f0fdfa', stroke: '#14b8a6', solid: '#0e7490' },
|
|
21
|
+
rose: { fill: '#fff1f2', stroke: '#f43f5e', solid: '#e11d48' },
|
|
22
|
+
indigo: { fill: '#eef2ff', stroke: '#6366f1', solid: '#4f46e5' },
|
|
23
|
+
};
|
|
24
|
+
export const TONE_NAMES = Object.keys(TONES);
|
|
25
|
+
export function tone(name, fallback = 'gray') {
|
|
26
|
+
return TONES[name ?? fallback] ?? TONES[fallback] ?? {
|
|
27
|
+
fill: '#f9fafb',
|
|
28
|
+
stroke: '#9ca3af',
|
|
29
|
+
solid: '#4b5563',
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* 직접 지정한 색은 `#rgb` / `#rrggbb` 만 받는다.
|
|
34
|
+
* 함수 표기(`url(...)`, `var(...)`)와 상대 URL 을 원천 차단하기 위해서다.
|
|
35
|
+
*/
|
|
36
|
+
const HEX = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
|
|
37
|
+
export function isHexColor(value) {
|
|
38
|
+
return HEX.test(value);
|
|
39
|
+
}
|
|
40
|
+
export const DEFAULT_PLANES = [
|
|
41
|
+
{ key: 'r', name: '요청', color: '#64748b' },
|
|
42
|
+
{ key: 'e', name: '외부 호출', color: '#7e5bd0' },
|
|
43
|
+
{ key: 'd', name: '데이터', color: '#d97706' },
|
|
44
|
+
{ key: 'm', name: '관측', color: '#0e7490', dash: '6 4' },
|
|
45
|
+
];
|
|
46
|
+
//# sourceMappingURL=tones.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tones.js","sourceRoot":"","sources":["../../src/render/tones.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAUH,MAAM,CAAC,MAAM,KAAK,GAAyB;IAC1C,KAAK,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,EAAE;IAC/D,IAAI,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,EAAE;IAC9D,IAAI,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,EAAE;IAC9D,KAAK,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,EAAE;IAC/D,KAAK,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,EAAE;IAC/D,MAAM,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,EAAE;IAChE,MAAM,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,EAAE;IAChE,IAAI,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,EAAE;IAC9D,IAAI,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,EAAE;IAC9D,MAAM,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,EAAE;CAChE,CAAC;AAEF,MAAM,CAAC,MAAM,UAAU,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAA0B,CAAC;AAEtE,MAAM,UAAU,IAAI,CAAC,IAAwB,EAAE,QAAQ,GAAG,MAAM;IAC/D,OAAO,KAAK,CAAC,IAAI,IAAI,QAAQ,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,IAAI;QACpD,IAAI,EAAE,SAAS;QACf,MAAM,EAAE,SAAS;QACjB,KAAK,EAAE,SAAS;KAChB,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,GAAG,GAAG,sCAAsC,CAAC;AAEnD,MAAM,UAAU,UAAU,CAAC,KAAa;IACvC,OAAO,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AACxB,CAAC;AAUD,MAAM,CAAC,MAAM,cAAc,GAAqB;IAC/C,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE;IAC1C,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE;IAC7C,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE;IAC3C,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE;CACvD,CAAC"}
|
package/dist/slug.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* url_slug 생성.
|
|
3
|
+
*
|
|
4
|
+
* 벨로그는 `WritePostInput.url_slug` 를 필수로 받는다. 사용자가 안 주면 제목에서
|
|
5
|
+
* 만든다. 벨로그 자체는 한글 슬러그를 허용하므로(예: /@witwint/프로메테우스)
|
|
6
|
+
* 한글을 로마자로 바꾸지 않고 그대로 둔다 — 억지 음차보다 원문이 낫다.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* 이미지 URL 검사.
|
|
10
|
+
*
|
|
11
|
+
* ★ zod 의 `z.string().url()` 로는 부족하다. 실측하면 아래를 전부 통과시킨다:
|
|
12
|
+
* javascript:alert(1) data:text/html,<script>…
|
|
13
|
+
* file:///etc/passwd http://127.0.0.1/…
|
|
14
|
+
* URL 형식이 맞는지만 보고 스킴은 안 따지기 때문이다. 썸네일은 결국 남의
|
|
15
|
+
* 페이지에서 렌더되므로 http/https 로 못 박는다.
|
|
16
|
+
*/
|
|
17
|
+
export declare function isSafeHttpUrl(value: string): boolean;
|
|
18
|
+
/**
|
|
19
|
+
* 이미지용. 현재 규칙은 http(s) + 호스트 존재로 같아서 그대로 위임한다.
|
|
20
|
+
* 구현을 복사하지 않는다 — 한쪽만 고쳐지는 사고를 이 레포에서 이미 두 번 겪었다.
|
|
21
|
+
*/
|
|
22
|
+
export declare const isSafeImageUrl: typeof isSafeHttpUrl;
|
|
23
|
+
/** URL 에서 실제로 문제가 되는 문자만 제거한다. */
|
|
24
|
+
export declare function slugify(title: string): string;
|
|
25
|
+
/** 벨로그 슬러그 길이 상한이 문서화돼 있지 않아 보수적으로 자른다. */
|
|
26
|
+
export declare const MAX_SLUG_LENGTH = 120;
|
|
27
|
+
export declare function toUrlSlug(title: string, provided?: string): string;
|
|
28
|
+
/**
|
|
29
|
+
* 프로필 홈페이지 링크 검사.
|
|
30
|
+
*
|
|
31
|
+
* 벨로그 웹은 값에 http(s) 스킴이 없으면 렌더링 시점에 `https://` 를 붙인다
|
|
32
|
+
* (apps/web/src/lib/includeProtocol.ts). 그래서 `example.com` 도, `example.com:8080`
|
|
33
|
+
* 도 정상 입력이다. 우리가 http(s) 만 받으면 정상 사용을 막는다.
|
|
34
|
+
*
|
|
35
|
+
* ★ 반대로 막아야 하는 건 `javascript:`·`data:` 같은 위험 스킴인데, 정규식으로
|
|
36
|
+
* '스킴처럼 생겼나' 를 보면 두 방향으로 다 틀린다:
|
|
37
|
+
* - `example.com:8080` 을 스킴으로 오인해 거부
|
|
38
|
+
* - `" javascript:…"` 처럼 앞에 공백을 붙이면 통과 (실측 확인)
|
|
39
|
+
* 그래서 정규식 대신 **웹과 같은 방식으로 실제 URL 을 만들어 본다.**
|
|
40
|
+
* 앞뒤 공백은 먼저 제거한다 — 공백 하나로 검사가 뚫리면 안 된다.
|
|
41
|
+
*/
|
|
42
|
+
export declare function isSafeProfileLink(value: string): boolean;
|
package/dist/slug.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* url_slug 생성.
|
|
3
|
+
*
|
|
4
|
+
* 벨로그는 `WritePostInput.url_slug` 를 필수로 받는다. 사용자가 안 주면 제목에서
|
|
5
|
+
* 만든다. 벨로그 자체는 한글 슬러그를 허용하므로(예: /@witwint/프로메테우스)
|
|
6
|
+
* 한글을 로마자로 바꾸지 않고 그대로 둔다 — 억지 음차보다 원문이 낫다.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* 이미지 URL 검사.
|
|
10
|
+
*
|
|
11
|
+
* ★ zod 의 `z.string().url()` 로는 부족하다. 실측하면 아래를 전부 통과시킨다:
|
|
12
|
+
* javascript:alert(1) data:text/html,<script>…
|
|
13
|
+
* file:///etc/passwd http://127.0.0.1/…
|
|
14
|
+
* URL 형식이 맞는지만 보고 스킴은 안 따지기 때문이다. 썸네일은 결국 남의
|
|
15
|
+
* 페이지에서 렌더되므로 http/https 로 못 박는다.
|
|
16
|
+
*/
|
|
17
|
+
export function isSafeHttpUrl(value) {
|
|
18
|
+
let url;
|
|
19
|
+
try {
|
|
20
|
+
url = new URL(value);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:')
|
|
26
|
+
return false;
|
|
27
|
+
return url.hostname.length > 0;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* 이미지용. 현재 규칙은 http(s) + 호스트 존재로 같아서 그대로 위임한다.
|
|
31
|
+
* 구현을 복사하지 않는다 — 한쪽만 고쳐지는 사고를 이 레포에서 이미 두 번 겪었다.
|
|
32
|
+
*/
|
|
33
|
+
export const isSafeImageUrl = isSafeHttpUrl;
|
|
34
|
+
/** URL 에서 실제로 문제가 되는 문자만 제거한다. */
|
|
35
|
+
export function slugify(title) {
|
|
36
|
+
const slug = title
|
|
37
|
+
.trim()
|
|
38
|
+
.toLowerCase()
|
|
39
|
+
// 경로·쿼리·프래그먼트 구분자와 공백류를 하이픈으로
|
|
40
|
+
.replace(/[\s/?#&=+%<>\\[\]{}|^~`"'`]+/g, '-')
|
|
41
|
+
// 나머지 구두점 제거 (한글·영숫자·하이픈·밑줄만 남긴다)
|
|
42
|
+
.replace(/[^\p{Letter}\p{Number}\-_]/gu, '')
|
|
43
|
+
.replace(/-{2,}/g, '-')
|
|
44
|
+
.replace(/^-+|-+$/g, '');
|
|
45
|
+
// 전부 걸러진 경우(기호만으로 된 제목) 빈 슬러그를 주면 벨로그가 거부한다.
|
|
46
|
+
return slug || 'untitled';
|
|
47
|
+
}
|
|
48
|
+
/** 벨로그 슬러그 길이 상한이 문서화돼 있지 않아 보수적으로 자른다. */
|
|
49
|
+
export const MAX_SLUG_LENGTH = 120;
|
|
50
|
+
export function toUrlSlug(title, provided) {
|
|
51
|
+
const base = provided?.trim() ? slugify(provided) : slugify(title);
|
|
52
|
+
if (base.length <= MAX_SLUG_LENGTH)
|
|
53
|
+
return base;
|
|
54
|
+
// 자른 자리에 하이픈이 남지 않게 한다.
|
|
55
|
+
return base.slice(0, MAX_SLUG_LENGTH).replace(/-+$/, '');
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* 프로필 홈페이지 링크 검사.
|
|
59
|
+
*
|
|
60
|
+
* 벨로그 웹은 값에 http(s) 스킴이 없으면 렌더링 시점에 `https://` 를 붙인다
|
|
61
|
+
* (apps/web/src/lib/includeProtocol.ts). 그래서 `example.com` 도, `example.com:8080`
|
|
62
|
+
* 도 정상 입력이다. 우리가 http(s) 만 받으면 정상 사용을 막는다.
|
|
63
|
+
*
|
|
64
|
+
* ★ 반대로 막아야 하는 건 `javascript:`·`data:` 같은 위험 스킴인데, 정규식으로
|
|
65
|
+
* '스킴처럼 생겼나' 를 보면 두 방향으로 다 틀린다:
|
|
66
|
+
* - `example.com:8080` 을 스킴으로 오인해 거부
|
|
67
|
+
* - `" javascript:…"` 처럼 앞에 공백을 붙이면 통과 (실측 확인)
|
|
68
|
+
* 그래서 정규식 대신 **웹과 같은 방식으로 실제 URL 을 만들어 본다.**
|
|
69
|
+
* 앞뒤 공백은 먼저 제거한다 — 공백 하나로 검사가 뚫리면 안 된다.
|
|
70
|
+
*/
|
|
71
|
+
export function isSafeProfileLink(value) {
|
|
72
|
+
const trimmed = value.trim();
|
|
73
|
+
if (trimmed === '')
|
|
74
|
+
return true; // 빈 값 = 삭제
|
|
75
|
+
// 있는 그대로 http(s) 이거나, https:// 를 붙였을 때 http(s) 가 되면 통과.
|
|
76
|
+
// javascript:·data: 는 두 경우 모두 http(s) 가 되지 않는다.
|
|
77
|
+
return isSafeHttpUrl(trimmed) || isSafeHttpUrl(`https://${trimmed}`);
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=slug.js.map
|
package/dist/slug.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slug.js","sourceRoot":"","sources":["../src/slug.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;;;;;;;GAQG;AACH,MAAM,UAAU,aAAa,CAAC,KAAa;IAC1C,IAAI,GAAQ,CAAC;IACb,IAAI,CAAC;QACJ,GAAG,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC;IACtB,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,KAAK,CAAC;IACd,CAAC;IACD,IAAI,GAAG,CAAC,QAAQ,KAAK,OAAO,IAAI,GAAG,CAAC,QAAQ,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IACxE,OAAO,GAAG,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC;AAChC,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,aAAa,CAAC;AAE5C,kCAAkC;AAClC,MAAM,UAAU,OAAO,CAAC,KAAa;IACpC,MAAM,IAAI,GAAG,KAAK;SAChB,IAAI,EAAE;SACN,WAAW,EAAE;QACd,8BAA8B;SAC7B,OAAO,CAAC,+BAA+B,EAAE,GAAG,CAAC;QAC9C,kCAAkC;SACjC,OAAO,CAAC,8BAA8B,EAAE,EAAE,CAAC;SAC3C,OAAO,CAAC,QAAQ,EAAE,GAAG,CAAC;SACtB,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;IAE1B,6CAA6C;IAC7C,OAAO,IAAI,IAAI,UAAU,CAAC;AAC3B,CAAC;AAED,2CAA2C;AAC3C,MAAM,CAAC,MAAM,eAAe,GAAG,GAAG,CAAC;AAEnC,MAAM,UAAU,SAAS,CAAC,KAAa,EAAE,QAAiB;IACzD,MAAM,IAAI,GAAG,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;IACnE,IAAI,IAAI,CAAC,MAAM,IAAI,eAAe;QAAE,OAAO,IAAI,CAAC;IAChD,wBAAwB;IACxB,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,eAAe,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;AAC1D,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,iBAAiB,CAAC,KAAa;IAC9C,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IAC7B,IAAI,OAAO,KAAK,EAAE;QAAE,OAAO,IAAI,CAAC,CAAC,WAAW;IAC5C,wDAAwD;IACxD,gDAAgD;IAChD,OAAO,aAAa,CAAC,OAAO,CAAC,IAAI,aAAa,CAAC,WAAW,OAAO,EAAE,CAAC,CAAC;AACtE,CAAC"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 탐색 도구 — 검색·트렌딩·최신. 전부 읽기 전용이고 인증이 필요 없다.
|
|
3
|
+
*/
|
|
4
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
5
|
+
import type { VelogClient } from '../client.ts';
|
|
6
|
+
export declare function registerDiscoverTools(server: McpServer, client: VelogClient): void;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 탐색 도구 — 검색·트렌딩·최신. 전부 읽기 전용이고 인증이 필요 없다.
|
|
3
|
+
*/
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { QUERY_RECENT_POSTS, QUERY_SEARCH_POSTS, QUERY_TRENDING_POSTS, } from "../graphql.js";
|
|
6
|
+
import { formatPostList, textResult } from "../format.js";
|
|
7
|
+
import { READ_ONLY } from "./posts.js";
|
|
8
|
+
/** 벨로그 트렌딩이 받는 기간 값. */
|
|
9
|
+
const TIMEFRAMES = ['day', 'week', 'month', 'year'];
|
|
10
|
+
/**
|
|
11
|
+
* ★ timeframe='year' 에는 별도 상한이 있다. 넘기면 **에러가 아니라 빈 배열**이 온다.
|
|
12
|
+
*
|
|
13
|
+
* // velog-io/velog · apps/server/src/services/PostService/index.ts
|
|
14
|
+
* if (timeframe === 'year' && (offset > 1000 || limit > 20)) {
|
|
15
|
+
* console.log('Detected GraphQL Abuse', ip)
|
|
16
|
+
* return []
|
|
17
|
+
* }
|
|
18
|
+
*
|
|
19
|
+
* 최악의 실패 모드다 — 오류가 없으니 "올해 인기글이 없나보다"로 오독되고,
|
|
20
|
+
* 서버는 우리를 abuse 로 기록한다. 2026-07-30 실측으로 재현 확인
|
|
21
|
+
* (year+limit:50 → [], year+limit:20 → 정상).
|
|
22
|
+
* 그래서 넘기기 전에 우리가 깎고, 깎았다는 사실을 사용자에게 말한다.
|
|
23
|
+
*/
|
|
24
|
+
const YEAR_MAX_LIMIT = 20;
|
|
25
|
+
const YEAR_MAX_OFFSET = 1000;
|
|
26
|
+
export function registerDiscoverTools(server, client) {
|
|
27
|
+
server.registerTool('velog_search_posts', {
|
|
28
|
+
title: '벨로그 글 검색',
|
|
29
|
+
description: '키워드로 벨로그 전체를 검색한다. username 을 주면 그 사람 글 안에서만 찾는다 — ' +
|
|
30
|
+
'"내가 예전에 쓴 그 글" 을 찾을 때 이 조합을 쓴다.',
|
|
31
|
+
inputSchema: {
|
|
32
|
+
keyword: z.string().min(1).describe('검색어'),
|
|
33
|
+
username: z.string().optional().describe('이 사용자의 글로 한정'),
|
|
34
|
+
limit: z.number().int().min(1).max(50).default(20),
|
|
35
|
+
offset: z.number().int().min(0).default(0).describe('페이지네이션'),
|
|
36
|
+
},
|
|
37
|
+
annotations: READ_ONLY,
|
|
38
|
+
}, async ({ keyword, username, limit, offset }) => {
|
|
39
|
+
const input = { keyword, limit, offset };
|
|
40
|
+
if (username)
|
|
41
|
+
input['username'] = username;
|
|
42
|
+
const data = await client.request(QUERY_SEARCH_POSTS, { input });
|
|
43
|
+
const result = data.searchPosts;
|
|
44
|
+
const posts = result.posts ?? [];
|
|
45
|
+
const body = formatPostList(posts, {
|
|
46
|
+
total: result.count,
|
|
47
|
+
showAuthor: !username,
|
|
48
|
+
});
|
|
49
|
+
// count 는 '필터 이전' 총계다. 벨로그는 조회 후 일부 글을 걸러내므로
|
|
50
|
+
// 반환 건수가 limit 보다 적을 수 있다 (2026-07-30 실측: "프로메테우스"
|
|
51
|
+
// limit=5 → 4건, "리액트" limit=5 → 5건). 그래도 offset 산술 자체는
|
|
52
|
+
// 정상이라(중복·누락 0 확인) 다음 페이지는 limit 만큼 더한다.
|
|
53
|
+
// 반환 건수로 판정하면 필터링된 페이지에서 조기 종료된다.
|
|
54
|
+
const more = offset + limit < result.count ? `\n\n다음 페이지: offset=${offset + limit}` : '';
|
|
55
|
+
return textResult(body + more);
|
|
56
|
+
});
|
|
57
|
+
server.registerTool('velog_trending_posts', {
|
|
58
|
+
title: '벨로그 트렌딩',
|
|
59
|
+
description: '벨로그 인기 글. 기간을 골라 본다.',
|
|
60
|
+
inputSchema: {
|
|
61
|
+
timeframe: z.enum(TIMEFRAMES).default('week'),
|
|
62
|
+
limit: z.number().int().min(1).max(50).default(20),
|
|
63
|
+
offset: z.number().int().min(0).default(0),
|
|
64
|
+
},
|
|
65
|
+
annotations: READ_ONLY,
|
|
66
|
+
}, async ({ timeframe, limit, offset }) => {
|
|
67
|
+
const notes = [];
|
|
68
|
+
let safeLimit = limit;
|
|
69
|
+
let safeOffset = offset;
|
|
70
|
+
if (timeframe === 'year') {
|
|
71
|
+
if (limit > YEAR_MAX_LIMIT) {
|
|
72
|
+
safeLimit = YEAR_MAX_LIMIT;
|
|
73
|
+
notes.push(`limit 을 ${limit}→${YEAR_MAX_LIMIT} 로 낮췄습니다 ` +
|
|
74
|
+
`(벨로그는 year 기간에 limit>${YEAR_MAX_LIMIT} 이면 빈 결과를 줍니다).`);
|
|
75
|
+
}
|
|
76
|
+
if (offset > YEAR_MAX_OFFSET) {
|
|
77
|
+
safeOffset = YEAR_MAX_OFFSET;
|
|
78
|
+
notes.push(`offset 을 ${offset}→${YEAR_MAX_OFFSET} 로 낮췄습니다.`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const data = await client.request(QUERY_TRENDING_POSTS, { input: { timeframe, limit: safeLimit, offset: safeOffset } });
|
|
82
|
+
const head = notes.length > 0 ? `⚠️ ${notes.join(' ')}\n\n` : '';
|
|
83
|
+
return textResult(head +
|
|
84
|
+
`[트렌딩 · ${timeframe}]\n\n` +
|
|
85
|
+
formatPostList(data.trendingPosts ?? [], { showAuthor: true }));
|
|
86
|
+
});
|
|
87
|
+
server.registerTool('velog_recent_posts', {
|
|
88
|
+
title: '벨로그 최신 글',
|
|
89
|
+
description: '벨로그 전체 최신 글. 지금 무슨 글이 올라오는지 훑을 때.',
|
|
90
|
+
inputSchema: {
|
|
91
|
+
limit: z.number().int().min(1).max(50).default(20),
|
|
92
|
+
cursor: z.string().optional().describe('이전 페이지 마지막 글의 id'),
|
|
93
|
+
},
|
|
94
|
+
annotations: READ_ONLY,
|
|
95
|
+
}, async ({ limit, cursor }) => {
|
|
96
|
+
const input = { limit };
|
|
97
|
+
if (cursor)
|
|
98
|
+
input['cursor'] = cursor;
|
|
99
|
+
const data = await client.request(QUERY_RECENT_POSTS, { input });
|
|
100
|
+
const posts = data.recentPosts ?? [];
|
|
101
|
+
const last = posts.at(-1);
|
|
102
|
+
const more = posts.length === limit && last ? `\n\n다음 페이지: cursor="${last.id}"` : '';
|
|
103
|
+
return textResult(formatPostList(posts, { showAuthor: true }) + more);
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=discover.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"discover.js","sourceRoot":"","sources":["../../src/tools/discover.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 초안 도구 — 이 파일이 이 레포의 '유일한' 쓰기 경로다.
|
|
3
|
+
*
|
|
4
|
+
* ★★ is_temp 는 상수다. 도구 입력 스키마에 없으므로 호출자가 덮어쓸 수 없고,
|
|
5
|
+
* 따라서 **이 도구들은** 어떤 설정에서도 글을 발행하지 않는다.
|
|
6
|
+
* 발행은 별도 파일(tools/publish.ts)이 담당한다 — 이름이 다르면 초안을
|
|
7
|
+
* 쓰려다 실수로 발행되는 사고가 안 난다.
|
|
8
|
+
*
|
|
9
|
+
* 이 파일을 고칠 때는 ADR 0004 를 먼저 읽을 것.
|
|
10
|
+
*/
|
|
11
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
12
|
+
import type { VelogClient } from '../client.ts';
|
|
13
|
+
import { formatPostList } from '../format.ts';
|
|
14
|
+
export declare function registerDraftTools(server: McpServer, client: VelogClient): void;
|
|
15
|
+
/** 테스트에서 상수를 직접 검사하기 위해 노출한다. */
|
|
16
|
+
export declare const __testing: {
|
|
17
|
+
DRAFT_ONLY: {
|
|
18
|
+
readonly is_temp: true;
|
|
19
|
+
readonly is_markdown: true;
|
|
20
|
+
readonly is_private: true;
|
|
21
|
+
};
|
|
22
|
+
formatPostList: typeof formatPostList;
|
|
23
|
+
};
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 초안 도구 — 이 파일이 이 레포의 '유일한' 쓰기 경로다.
|
|
3
|
+
*
|
|
4
|
+
* ★★ is_temp 는 상수다. 도구 입력 스키마에 없으므로 호출자가 덮어쓸 수 없고,
|
|
5
|
+
* 따라서 **이 도구들은** 어떤 설정에서도 글을 발행하지 않는다.
|
|
6
|
+
* 발행은 별도 파일(tools/publish.ts)이 담당한다 — 이름이 다르면 초안을
|
|
7
|
+
* 쓰려다 실수로 발행되는 사고가 안 난다.
|
|
8
|
+
*
|
|
9
|
+
* 이 파일을 고칠 때는 ADR 0004 를 먼저 읽을 것.
|
|
10
|
+
*/
|
|
11
|
+
import { z } from 'zod';
|
|
12
|
+
import { QUERY_POSTS } from "../graphql.js";
|
|
13
|
+
import { formatPostList, textResult } from "../format.js";
|
|
14
|
+
import { toUrlSlug, isSafeImageUrl } from "../slug.js";
|
|
15
|
+
import { resolveMyUsername } from "../me.js";
|
|
16
|
+
import { assertOwned, assertOwnsSeries } from "../ownership.js";
|
|
17
|
+
import { READ_ONLY } from "./posts.js";
|
|
18
|
+
/**
|
|
19
|
+
* ★ 발행 차단 지점. 이 값은 파라미터가 아니라 상수다.
|
|
20
|
+
* true = 임시저장(남에게 보이지 않음), false = 발행(되돌릴 수 없음).
|
|
21
|
+
*/
|
|
22
|
+
const DRAFT_ONLY = { is_temp: true, is_markdown: true, is_private: true };
|
|
23
|
+
const MUTATION_WRITE_POST = `
|
|
24
|
+
mutation WriteDraft($input: WritePostInput!) {
|
|
25
|
+
writePost(input: $input) { id title url_slug is_temp user { username } }
|
|
26
|
+
}
|
|
27
|
+
`;
|
|
28
|
+
const MUTATION_EDIT_POST = `
|
|
29
|
+
mutation EditDraft($input: EditPostInput!) {
|
|
30
|
+
editPost(input: $input) { id title url_slug is_temp user { username } }
|
|
31
|
+
}
|
|
32
|
+
`;
|
|
33
|
+
/** 수정 전 상태 확인용. 필요한 필드만 받는다. */
|
|
34
|
+
const QUERY_POST_STATE = `
|
|
35
|
+
query PostState($input: ReadPostInput!) {
|
|
36
|
+
post(input: $input) { id title is_temp meta user { username } }
|
|
37
|
+
}
|
|
38
|
+
`;
|
|
39
|
+
/** 벨로그가 정말 임시저장으로 받았는지 응답으로 확인한다. */
|
|
40
|
+
function assertStayedDraft(post) {
|
|
41
|
+
if (post.is_temp !== true) {
|
|
42
|
+
throw new Error(`예상치 못한 상태: 벨로그가 이 글을 임시저장이 아닌 것으로 저장했습니다 ` +
|
|
43
|
+
`(is_temp=${post.is_temp}, id=${post.id}). 즉시 벨로그에서 확인하세요.`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function draftResult(post, verb) {
|
|
47
|
+
const username = post.user?.username;
|
|
48
|
+
const where = username
|
|
49
|
+
? `https://velog.io/write?id=${post.id}`
|
|
50
|
+
: `(id: ${post.id})`;
|
|
51
|
+
return [
|
|
52
|
+
`✅ 초안을 ${verb}했습니다. **아직 발행되지 않았습니다.**`,
|
|
53
|
+
'',
|
|
54
|
+
`- 제목: ${post.title}`,
|
|
55
|
+
`- id: \`${post.id}\``,
|
|
56
|
+
`- 상태: 임시저장 (is_temp=true) — 나만 볼 수 있음`,
|
|
57
|
+
`- 편집: ${where}`,
|
|
58
|
+
'',
|
|
59
|
+
'발행하려면 velog_publish_draft 를 쓰거나, 벨로그에서 직접 "출간하기"를 누르세요.',
|
|
60
|
+
].join('\n');
|
|
61
|
+
}
|
|
62
|
+
export function registerDraftTools(server, client) {
|
|
63
|
+
server.registerTool('velog_create_draft', {
|
|
64
|
+
title: '벨로그 초안 작성',
|
|
65
|
+
description: '벨로그에 임시저장 글(초안)을 만든다. 발행되지 않으며 작성자 본인만 볼 수 있다. ' +
|
|
66
|
+
'이 도구는 어떤 설정에서도 발행하지 않는다 — 발행하려면 velog_publish_draft 를 따로 부를 것. ' +
|
|
67
|
+
'body 는 마크다운으로 쓴다.',
|
|
68
|
+
inputSchema: {
|
|
69
|
+
title: z.string().min(1).describe('글 제목'),
|
|
70
|
+
body: z.string().min(1).describe('본문 (마크다운)'),
|
|
71
|
+
tags: z.array(z.string()).default([]).describe('태그 목록'),
|
|
72
|
+
url_slug: z.string().optional().describe('생략하면 제목에서 생성'),
|
|
73
|
+
thumbnail: z
|
|
74
|
+
.string()
|
|
75
|
+
.refine(isSafeImageUrl, 'http(s) 이미지 URL 만 허용합니다')
|
|
76
|
+
.optional()
|
|
77
|
+
.describe('썸네일 이미지 URL (http/https 만)'),
|
|
78
|
+
series_id: z
|
|
79
|
+
.string()
|
|
80
|
+
.optional()
|
|
81
|
+
.describe('소속시킬 시리즈 id. ★ 벨로그는 임시저장 단계에서 이걸 무시한다 — ' +
|
|
82
|
+
'초안 생성 후 velog_update_draft 로 다시 지정해야 실제로 붙는다'),
|
|
83
|
+
},
|
|
84
|
+
// 되돌릴 수 있는 쓰기다 — 비공개 초안이므로 파괴적이지 않다.
|
|
85
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
|
|
86
|
+
}, async ({ title, body, tags, url_slug, thumbnail, series_id }) => {
|
|
87
|
+
client.requireAuth('velog_create_draft');
|
|
88
|
+
if (series_id)
|
|
89
|
+
await assertOwnsSeries(client, series_id, 'velog_create_draft');
|
|
90
|
+
// ★ 초안은 is_private:true 라 벨로그 계수(is_private:false 만 셈)에
|
|
91
|
+
// 잡히지 않는다. 그래서 상한을 걸지 않는다 — 상한은 '공개 발행' 쪽에 있다.
|
|
92
|
+
const input = {
|
|
93
|
+
title,
|
|
94
|
+
body,
|
|
95
|
+
tags,
|
|
96
|
+
url_slug: toUrlSlug(title, url_slug),
|
|
97
|
+
meta: {},
|
|
98
|
+
...DRAFT_ONLY, // ★ 마지막에 펼쳐서 위 값들이 덮어쓸 수 없게 한다
|
|
99
|
+
};
|
|
100
|
+
if (thumbnail)
|
|
101
|
+
input['thumbnail'] = thumbnail;
|
|
102
|
+
if (series_id)
|
|
103
|
+
input['series_id'] = series_id;
|
|
104
|
+
// mutate 는 재시도하지 않는다 — 응답 유실 시 초안이 중복 생성된다.
|
|
105
|
+
const data = await client.mutate(MUTATION_WRITE_POST, { input });
|
|
106
|
+
assertStayedDraft(data.writePost);
|
|
107
|
+
// ★ 벨로그는 임시저장 생성 시 series_id 를 조용히 버린다:
|
|
108
|
+
// // apps/server/src/services/PostApiService/index.mts
|
|
109
|
+
// if (series_id && !data.is_temp) await appendToSeries(...)
|
|
110
|
+
// edit 경로에는 이 조건이 없어 update_draft 로는 붙는다. 이 비대칭을
|
|
111
|
+
// 사용자가 알 방법이 없으므로 직접 알린다.
|
|
112
|
+
const seriesNote = series_id
|
|
113
|
+
? '\n\n⚠️ 시리즈는 **적용되지 않았습니다.** 벨로그가 임시저장 생성 단계에서는' +
|
|
114
|
+
' series_id 를 무시합니다. 방금 만든 초안 id 로 velog_update_draft 를' +
|
|
115
|
+
' 한 번 호출하면 그때 실제로 붙습니다.'
|
|
116
|
+
: '';
|
|
117
|
+
return textResult(draftResult(data.writePost, '저장') + seriesNote);
|
|
118
|
+
});
|
|
119
|
+
server.registerTool('velog_update_draft', {
|
|
120
|
+
title: '벨로그 초안 수정',
|
|
121
|
+
description: '기존 초안을 **통째로 교체**한다. 부분 수정이 아니다. ' +
|
|
122
|
+
'생략한 필드는 유지되지 않고 초기화된다 — tags 를 안 주면 기존 태그가 전부 지워지고, ' +
|
|
123
|
+
'url_slug 를 안 주면 제목에서 새로 만들어 주소가 바뀌며, series_id 를 안 주면 ' +
|
|
124
|
+
'기존 시리즈 연결이 끊긴다. 그래서 수정 전에 velog_get_post 로 현재 값을 읽어 ' +
|
|
125
|
+
'바꾸지 않을 필드도 그대로 다시 넘기는 것을 권한다. ' +
|
|
126
|
+
'발행된 글의 id 는 거부한다(비공개로 내려가는 사고 방지).',
|
|
127
|
+
inputSchema: {
|
|
128
|
+
id: z.string().min(1).describe('초안의 id (velog_list_drafts 로 확인)'),
|
|
129
|
+
title: z.string().min(1),
|
|
130
|
+
body: z.string().min(1).describe('본문 전체 (마크다운). 부분 수정이 아니라 교체다'),
|
|
131
|
+
tags: z.array(z.string()).default([]),
|
|
132
|
+
url_slug: z.string().optional(),
|
|
133
|
+
thumbnail: z
|
|
134
|
+
.string()
|
|
135
|
+
.refine(isSafeImageUrl, 'http(s) 이미지 URL 만 허용합니다')
|
|
136
|
+
.optional(),
|
|
137
|
+
series_id: z.string().optional(),
|
|
138
|
+
},
|
|
139
|
+
// ★ destructive 가 맞다. 생략 필드가 보존되지 않고 초기화된다 —
|
|
140
|
+
// MCP 명세상 destructiveHint:false 는 '추가만 한다'는 뜻이라 거짓이 된다.
|
|
141
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true },
|
|
142
|
+
}, async ({ id, title, body, tags, url_slug, thumbnail, series_id }) => {
|
|
143
|
+
client.requireAuth('velog_update_draft');
|
|
144
|
+
// ★ 두 가지를 확인한 뒤에야 수정한다.
|
|
145
|
+
// ① 내 글인가 — 벨로그 서버가 edit 에서 소유권을 안 본다(ownership.ts)
|
|
146
|
+
// ② 정말 초안인가 — editPost 는 is_temp 를 덮어쓰므로 발행글 id 가
|
|
147
|
+
// 들어오면 그 글이 조용히 비공개로 내려간다
|
|
148
|
+
const before = await client.request(QUERY_POST_STATE, { input: { id } });
|
|
149
|
+
if (!before.post) {
|
|
150
|
+
throw new Error(`id=${id} 인 글을 찾지 못했습니다. velog_list_drafts 로 id 를 확인하세요.`);
|
|
151
|
+
}
|
|
152
|
+
await assertOwned(client, before.post, 'velog_update_draft');
|
|
153
|
+
if (series_id)
|
|
154
|
+
await assertOwnsSeries(client, series_id, 'velog_update_draft');
|
|
155
|
+
if (before.post.is_temp !== true) {
|
|
156
|
+
throw new Error(`id=${id} 는 이미 발행된 글입니다. 이 도구로 수정하면 임시저장으로 내려가 ` +
|
|
157
|
+
`비공개가 되므로 중단했습니다. 발행된 글은 벨로그에서 직접 수정하세요.`);
|
|
158
|
+
}
|
|
159
|
+
const input = {
|
|
160
|
+
id,
|
|
161
|
+
title,
|
|
162
|
+
body,
|
|
163
|
+
tags,
|
|
164
|
+
url_slug: toUrlSlug(title, url_slug),
|
|
165
|
+
// ★ {} 를 보내면 short_description 등 표시 데이터가 지워진다.
|
|
166
|
+
// 서버가 받은 값을 그대로 DB 에 넣으므로 기존 값을 실어야 한다.
|
|
167
|
+
// (publish.ts 만 고치고 여기를 또 빠뜨렸었다 — 소유권 때와 같은 실수)
|
|
168
|
+
meta: before.post.meta ?? {},
|
|
169
|
+
...DRAFT_ONLY,
|
|
170
|
+
};
|
|
171
|
+
if (thumbnail)
|
|
172
|
+
input['thumbnail'] = thumbnail;
|
|
173
|
+
if (series_id)
|
|
174
|
+
input['series_id'] = series_id;
|
|
175
|
+
const data = await client.mutate(MUTATION_EDIT_POST, {
|
|
176
|
+
input,
|
|
177
|
+
});
|
|
178
|
+
// ★ editPost 의 응답으로는 사후 상태를 알 수 없다. 공식 구현이
|
|
179
|
+
// `return { ...post, url_slug: data.url_slug }` 로 **갱신 전에 읽은**
|
|
180
|
+
// post 를 돌려주고 url_slug 만 덮기 때문이다
|
|
181
|
+
// (apps/server/src/services/PostApiService/index.mts).
|
|
182
|
+
// 그래서 응답의 is_temp 는 '수정 전' 값이다 — 그걸 검사해봐야 사전확인을
|
|
183
|
+
// 한 번 더 하는 것에 불과하다. 진짜 사후 확인은 재조회뿐이다.
|
|
184
|
+
// ★ 재조회 결과를 '보기만' 하면 검증이 아니다. null 도 실패로 처리한다.
|
|
185
|
+
const after = await client.request(QUERY_POST_STATE, { input: { id } });
|
|
186
|
+
if (!after.post) {
|
|
187
|
+
throw new Error(`velog_update_draft: 수정 후 글(id=${id})을 다시 찾지 못했습니다. ` +
|
|
188
|
+
'벨로그에서 상태를 직접 확인하세요.');
|
|
189
|
+
}
|
|
190
|
+
if (after.post.is_temp !== true) {
|
|
191
|
+
throw new Error(`⚠️ 수정 후 확인 결과 이 글이 임시저장이 아닙니다 ` +
|
|
192
|
+
`(id=${id}, is_temp=${after.post.is_temp}). ` +
|
|
193
|
+
'수정 직전에 다른 곳에서 발행됐을 수 있습니다. 벨로그에서 상태를 확인하세요.');
|
|
194
|
+
}
|
|
195
|
+
// ★ data.editPost 는 '갱신 전' 객체다(공식 서버가 그렇게 반환한다).
|
|
196
|
+
// 그걸로 문구를 만들면 제목을 바꿔도 옛 제목이 표시된다.
|
|
197
|
+
// 이미 재조회했으므로 그 결과를 얹는다.
|
|
198
|
+
return textResult(draftResult({ ...data.editPost, title: after.post.title ?? data.editPost.title ?? null }, '수정'));
|
|
199
|
+
});
|
|
200
|
+
server.registerTool('velog_list_drafts', {
|
|
201
|
+
title: '내 초안 목록',
|
|
202
|
+
description: '내 임시저장 글 목록. 초안을 이어 쓰거나 수정하기 전에 id 를 여기서 확인한다.',
|
|
203
|
+
inputSchema: {
|
|
204
|
+
username: z
|
|
205
|
+
.string()
|
|
206
|
+
.optional()
|
|
207
|
+
.describe('생략하면 토큰의 계정을 쓴다. 남의 초안은 어차피 안 보인다'),
|
|
208
|
+
limit: z.number().int().min(1).max(50).default(20),
|
|
209
|
+
},
|
|
210
|
+
annotations: READ_ONLY,
|
|
211
|
+
}, async ({ username, limit }) => {
|
|
212
|
+
client.requireAuth('velog_list_drafts');
|
|
213
|
+
// 토큰이 있으면 서버가 이미 누군지 안다. 사용자가 매번 칠 이유가 없다.
|
|
214
|
+
const target = username ?? (await resolveMyUsername(client));
|
|
215
|
+
const data = await client.request(QUERY_POSTS, { input: { username: target, limit, temp_only: true } });
|
|
216
|
+
const posts = data.posts ?? [];
|
|
217
|
+
if (posts.length === 0)
|
|
218
|
+
return textResult('임시저장된 초안이 없습니다.');
|
|
219
|
+
const withIds = posts
|
|
220
|
+
.map((p, i) => `${i + 1}. ${p.title}\n id: \`${p.id}\``)
|
|
221
|
+
.join('\n\n');
|
|
222
|
+
return textResult(`초안 ${posts.length}건\n\n${withIds}`);
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
/** 테스트에서 상수를 직접 검사하기 위해 노출한다. */
|
|
226
|
+
export const __testing = { DRAFT_ONLY, formatPostList };
|
|
227
|
+
//# sourceMappingURL=drafts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"drafts.js","sourceRoot":"","sources":["../../src/tools/drafts.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1D,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AACvD,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAC7C,OAAO,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAEhE,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAEvC;;;GAGG;AACH,MAAM,UAAU,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAW,CAAC;AAEnF,MAAM,mBAAmB,GAAG;;;;CAI3B,CAAC;AAEF,MAAM,kBAAkB,GAAG;;;;CAI1B,CAAC;AAEF,gCAAgC;AAChC,MAAM,gBAAgB,GAAG;;;;CAIxB,CAAC;AAeF,qCAAqC;AACrC,SAAS,iBAAiB,CAAC,IAAiB;IAC3C,IAAI,IAAI,CAAC,OAAO,KAAK,IAAI,EAAE,CAAC;QAC3B,MAAM,IAAI,KAAK,CACd,2CAA2C;YAC1C,YAAY,IAAI,CAAC,OAAO,QAAQ,IAAI,CAAC,EAAE,oBAAoB,CAC5D,CAAC;IACH,CAAC;AACF,CAAC;AAED,SAAS,WAAW,CAAC,IAAiB,EAAE,IAAY;IACnD,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,EAAE,QAAQ,CAAC;IACrC,MAAM,KAAK,GAAG,QAAQ;QACrB,CAAC,CAAC,6BAA6B,IAAI,CAAC,EAAE,EAAE;QACxC,CAAC,CAAC,QAAQ,IAAI,CAAC,EAAE,GAAG,CAAC;IACtB,OAAO;QACN,SAAS,IAAI,0BAA0B;QACvC,EAAE;QACF,SAAS,IAAI,CAAC,KAAK,EAAE;QACrB,WAAW,IAAI,CAAC,EAAE,IAAI;QACtB,uCAAuC;QACvC,SAAS,KAAK,EAAE;QAChB,EAAE;QACF,yDAAyD;KACzD,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACd,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,MAAiB,EAAE,MAAmB;IACxE,MAAM,CAAC,YAAY,CAClB,oBAAoB,EACpB;QACC,KAAK,EAAE,WAAW;QAClB,WAAW,EACV,iDAAiD;YACjD,iEAAiE;YACjE,mBAAmB;QACpB,WAAW,EAAE;YACZ,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC;YACzC,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC;YAC7C,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC;YACvD,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,cAAc,CAAC;YACxD,SAAS,EAAE,CAAC;iBACV,MAAM,EAAE;iBACR,MAAM,CAAC,cAAc,EAAE,yBAAyB,CAAC;iBACjD,QAAQ,EAAE;iBACV,QAAQ,CAAC,4BAA4B,CAAC;YACxC,SAAS,EAAE,CAAC;iBACV,MAAM,EAAE;iBACR,QAAQ,EAAE;iBACV,QAAQ,CACR,0CAA0C;gBACzC,8CAA8C,CAC/C;SACF;QACD,qCAAqC;QACrC,WAAW,EAAE,EAAE,YAAY,EAAE,KAAK,EAAE,eAAe,EAAE,KAAK,EAAE,cAAc,EAAE,KAAK,EAAE;KACnF,EACD,KAAK,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,SAAS,EAAE,SAAS,EAAE,EAAE,EAAE;QAC/D,MAAM,CAAC,WAAW,CAAC,oBAAoB,CAAC,CAAC;QACzC,IAAI,SAAS;YAAE,MAAM,gBAAgB,CAAC,MAAM,EAAE,SAAS,EAAE,oBAAoB,CAAC,CAAC;QAC/E,wDAAwD;QACxD,iDAAiD;QAEjD,MAAM,KAAK,GAA4B;YACtC,KAAK;YACL,IAAI;YACJ,IAAI;YACJ,QAAQ,EAAE,SAAS,CAAC,KAAK,EAAE,QAAQ,CAAC;YACpC,IAAI,EAAE,EAAE;YACR,GAAG,UAAU,EAAE,+BAA+B;SAC9C,CAAC;QACF,IAAI,SAAS;YAAE,KAAK,CAAC,WAAW,CAAC,GAAG,SAAS,CAAC;QAC9C,IAAI,SAAS;YAAE,KAAK,CAAC,WAAW,CAAC,GAAG,SAAS,CAAC;QAE9C,4CAA4C;QAC5C,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,MAAM,CAC/B,mBAAmB,EACnB,EAAE,KAAK,EAAE,CACT,CAAC;QACF,iBAAiB,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QAElC,wCAAwC;QACxC,yDAAyD;QACzD,8DAA8D;QAC9D,iDAAiD;QACjD,0BAA0B;QAC1B,MAAM,UAAU,GAAG,SAAS;YAC3B,CAAC,CAAC,gDAAgD;gBACjD,wDAAwD;gBACxD,wBAAwB;YACzB,CAAC,CAAC,EAAE,CAAC;QACN,OAAO,UAAU,CAAC,WAAW,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,GAAG,UAAU,CAAC,CAAC;IACnE,CAAC,CACD,CAAC;IAEF,MAAM,CAAC,YAAY,CAClB,oBAAoB,EACpB;QACC,KAAK,EAAE,WAAW;QAClB,WAAW,EACV,mCAAmC;YACnC,sDAAsD;YACtD,wDAAwD;YACxD,sDAAsD;YACtD,gCAAgC;YAChC,oCAAoC;QACrC,WAAW,EAAE;YACZ,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,iCAAiC,CAAC;YACjE,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;YACxB,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,8BAA8B,CAAC;YAChE,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;YACrC,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;YAC/B,SAAS,EAAE,CAAC;iBACV,MAAM,EAAE;iBACR,MAAM,CAAC,cAAc,EAAE,yBAAyB,CAAC;iBACjD,QAAQ,EAAE;YACZ,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;SAChC;QACD,6CAA6C;QAC7C,0DAA0D;QAC1D,WAAW,EAAE,EAAE,YAAY,EAAE,KAAK,EAAE,eAAe,EAAE,IAAI,EAAE,cAAc,EAAE,IAAI,EAAE;KACjF,EACD,KAAK,EAAE,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,SAAS,EAAE,SAAS,EAAE,EAAE,EAAE;QACnE,MAAM,CAAC,WAAW,CAAC,oBAAoB,CAAC,CAAC;QAEzC,wBAAwB;QACxB,sDAAsD;QACtD,oDAAoD;QACpD,+BAA+B;QAC/B,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,OAAO,CAOhC,gBAAgB,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC;QACxC,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;YAClB,MAAM,IAAI,KAAK,CACd,MAAM,EAAE,iDAAiD,CACzD,CAAC;QACH,CAAC;QACD,MAAM,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,IAAI,EAAE,oBAAoB,CAAC,CAAC;QAC7D,IAAI,SAAS;YAAE,MAAM,gBAAgB,CAAC,MAAM,EAAE,SAAS,EAAE,oBAAoB,CAAC,CAAC;QAC/E,IAAI,MAAM,CAAC,IAAI,CAAC,OAAO,KAAK,IAAI,EAAE,CAAC;YAClC,MAAM,IAAI,KAAK,CACd,MAAM,EAAE,wCAAwC;gBAC/C,yCAAyC,CAC1C,CAAC;QACH,CAAC;QAED,MAAM,KAAK,GAA4B;YACtC,EAAE;YACF,KAAK;YACL,IAAI;YACJ,IAAI;YACJ,QAAQ,EAAE,SAAS,CAAC,KAAK,EAAE,QAAQ,CAAC;YACpC,+CAA+C;YAC/C,0CAA0C;YAC1C,kDAAkD;YAClD,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,IAAI,IAAI,EAAE;YAC5B,GAAG,UAAU;SACb,CAAC;QACF,IAAI,SAAS;YAAE,KAAK,CAAC,WAAW,CAAC,GAAG,SAAS,CAAC;QAC9C,IAAI,SAAS;YAAE,KAAK,CAAC,WAAW,CAAC,GAAG,SAAS,CAAC;QAE9C,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,MAAM,CAA4B,kBAAkB,EAAE;YAC/E,KAAK;SACL,CAAC,CAAC;QAEH,2CAA2C;QAC3C,iEAAiE;QACjE,mCAAmC;QACnC,yDAAyD;QACzD,kDAAkD;QAClD,wCAAwC;QACxC,+CAA+C;QAC/C,MAAM,KAAK,GAAG,MAAM,MAAM,CAAC,OAAO,CAE/B,gBAAgB,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC;QACxC,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;YACjB,MAAM,IAAI,KAAK,CACd,iCAAiC,EAAE,kBAAkB;gBACpD,qBAAqB,CACtB,CAAC;QACH,CAAC;QACD,IAAI,KAAK,CAAC,IAAI,CAAC,OAAO,KAAK,IAAI,EAAE,CAAC;YACjC,MAAM,IAAI,KAAK,CACd,gCAAgC;gBAC/B,OAAO,EAAE,aAAa,KAAK,CAAC,IAAI,CAAC,OAAO,KAAK;gBAC7C,6CAA6C,CAC9C,CAAC;QACH,CAAC;QAED,iDAAiD;QACjD,oCAAoC;QACpC,0BAA0B;QAC1B,OAAO,UAAU,CAChB,WAAW,CACV,EAAE,GAAG,IAAI,CAAC,QAAQ,EAAE,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,QAAQ,CAAC,KAAK,IAAI,IAAI,EAAE,EAC5E,IAAI,CACJ,CACD,CAAC;IACH,CAAC,CACD,CAAC;IAEF,MAAM,CAAC,YAAY,CAClB,mBAAmB,EACnB;QACC,KAAK,EAAE,SAAS;QAChB,WAAW,EACV,gDAAgD;QACjD,WAAW,EAAE;YACZ,QAAQ,EAAE,CAAC;iBACT,MAAM,EAAE;iBACR,QAAQ,EAAE;iBACV,QAAQ,CAAC,mCAAmC,CAAC;YAC/C,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;SAClD;QACD,WAAW,EAAE,SAAS;KACtB,EACD,KAAK,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE,EAAE;QAC7B,MAAM,CAAC,WAAW,CAAC,mBAAmB,CAAC,CAAC;QAExC,2CAA2C;QAC3C,MAAM,MAAM,GAAG,QAAQ,IAAI,CAAC,MAAM,iBAAiB,CAAC,MAAM,CAAC,CAAC,CAAC;QAE7D,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,OAAO,CAChC,WAAW,EACX,EAAE,KAAK,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,EAAE,CACvD,CAAC;QACF,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC;QAC/B,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,UAAU,CAAC,iBAAiB,CAAC,CAAC;QAE7D,MAAM,OAAO,GAAG,KAAK;aACnB,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,KAAK,eAAe,CAAC,CAAC,EAAE,IAAI,CAAC;aAC1D,IAAI,CAAC,MAAM,CAAC,CAAC;QACf,OAAO,UAAU,CAAC,MAAM,KAAK,CAAC,MAAM,QAAQ,OAAO,EAAE,CAAC,CAAC;IACxD,CAAC,CACD,CAAC;AACH,CAAC;AAED,iCAAiC;AACjC,MAAM,CAAC,MAAM,SAAS,GAAG,EAAE,UAAU,EAAE,cAAc,EAAE,CAAC"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 글 백업 — 벨로그 글을 프론트매터 붙인 마크다운으로 내려받는다.
|
|
3
|
+
*
|
|
4
|
+
* ★ 이 레포에서 파일을 쓰는 유일한 모듈이다. 쓰는 내용은 '글 본문'뿐이고
|
|
5
|
+
* 토큰은 절대 닿지 않는다. safety.test.ts 가 이 파일만 예외로 둔다.
|
|
6
|
+
*
|
|
7
|
+
* 벨로그에 공식 내보내기가 없어서 만든다. 서비스가 사라져도 글은 남아야 한다.
|
|
8
|
+
*/
|
|
9
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
10
|
+
import type { VelogClient } from '../client.ts';
|
|
11
|
+
import type { VelogPostDetail } from '../types.ts';
|
|
12
|
+
export declare function toMarkdown(post: VelogPostDetail, username: string): string;
|
|
13
|
+
/**
|
|
14
|
+
* 파일명 안전화.
|
|
15
|
+
*
|
|
16
|
+
* 슬러그에 경로 구분자가 들어오면 지정한 디렉터리 밖에 쓸 수 있다.
|
|
17
|
+
* slugify 가 이미 `/` 를 제거하지만, 여기서 한 번 더 막는다 —
|
|
18
|
+
* 파일을 쓰는 코드는 이중으로 방어한다.
|
|
19
|
+
*/
|
|
20
|
+
export declare function safeFileName(slug: string, index: number): string;
|
|
21
|
+
export declare function registerExportTools(server: McpServer, client: VelogClient): void;
|