@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,82 @@
1
+ /**
2
+ * 글 조회 도구 — 전부 읽기 전용.
3
+ *
4
+ * 인증이 없어도 공개 글은 조회된다 (ADR 0003).
5
+ */
6
+ import { z } from 'zod';
7
+ import { QUERY_POST, QUERY_POSTS } from "../graphql.js";
8
+ import { formatPostDetail, formatPostList, textResult } from "../format.js";
9
+ /** 읽기 도구 공통 표식 — 클라이언트가 위험도를 판단할 수 있게 한다. */
10
+ export const READ_ONLY = {
11
+ readOnlyHint: true,
12
+ destructiveHint: false,
13
+ openWorldHint: true,
14
+ };
15
+ export function registerPostTools(server, client) {
16
+ server.registerTool('velog_get_post', {
17
+ title: '벨로그 글 읽기',
18
+ description: '벨로그 글 하나를 본문까지 읽어온다. username + url_slug 조합이나 글 id 로 지정한다. ' +
19
+ '예: https://velog.io/@velopert/react-context-tutorial → username="velopert", url_slug="react-context-tutorial"',
20
+ inputSchema: {
21
+ username: z.string().optional().describe('@ 없이. 예: "velopert"'),
22
+ url_slug: z.string().optional().describe('URL 마지막 조각'),
23
+ id: z.string().optional().describe('글 UUID. 이걸 주면 username/url_slug 는 불필요'),
24
+ },
25
+ annotations: READ_ONLY,
26
+ }, async ({ username, url_slug, id }) => {
27
+ // 분기 안에서 값을 쓰면 타입이 그대로 좁혀져 단언이 필요 없다.
28
+ const input = {};
29
+ if (id) {
30
+ input['id'] = id;
31
+ }
32
+ else if (username && url_slug) {
33
+ input['username'] = username;
34
+ input['url_slug'] = url_slug;
35
+ }
36
+ else {
37
+ throw new Error('id 를 주거나, username 과 url_slug 를 함께 주세요.');
38
+ }
39
+ const data = await client.request(QUERY_POST, {
40
+ input,
41
+ });
42
+ if (!data.post)
43
+ return textResult('해당 글을 찾지 못했습니다.');
44
+ return textResult(formatPostDetail(data.post));
45
+ });
46
+ server.registerTool('velog_list_posts', {
47
+ title: '벨로그 글 목록',
48
+ description: '특정 사용자의 글 목록을 최신순으로 가져온다. tag 로 좁힐 수 있다. ' +
49
+ 'cursor 에 직전 응답의 마지막 글 id 를 주면 다음 페이지를 읽는다.',
50
+ inputSchema: {
51
+ username: z.string().describe('@ 없이'),
52
+ tag: z.string().optional().describe('이 태그가 달린 글만'),
53
+ limit: z.number().int().min(1).max(50).default(20),
54
+ cursor: z.string().optional().describe('이전 페이지 마지막 글의 id'),
55
+ },
56
+ annotations: READ_ONLY,
57
+ }, async ({ username, tag, limit, cursor }) => {
58
+ const input = { username, limit };
59
+ if (tag)
60
+ input['tag'] = tag;
61
+ if (cursor)
62
+ input['cursor'] = cursor;
63
+ const data = await client.request(QUERY_POSTS, {
64
+ input,
65
+ });
66
+ const posts = data.posts ?? [];
67
+ const body = formatPostList(posts);
68
+ const last = posts.at(-1);
69
+ // ★ tag 를 주면 벨로그가 요청 limit 을 무시하고 고정 20건을 준다
70
+ // (2026-07-30 실측: tag='React', limit=5 → 20건).
71
+ // 그래서 `posts.length === limit` 으로 다음 페이지를 판정하면
72
+ // 태그 조회에서 항상 '더 없음'으로 오판한다. 반환이 비지 않았으면
73
+ // 커서를 안내하는 쪽이 안전하다 — 헛걸음 한 번이 누락보다 낫다.
74
+ const hasMore = tag ? posts.length > 0 : posts.length >= limit;
75
+ const more = hasMore && last ? `\n\n다음 페이지: cursor="${last.id}"` : '';
76
+ const tagNote = tag
77
+ ? '\n(벨로그는 태그 조회에서 limit 을 무시하고 20건 단위로 줍니다)'
78
+ : '';
79
+ return textResult(body + more + tagNote);
80
+ });
81
+ }
82
+ //# sourceMappingURL=posts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"posts.js","sourceRoot":"","sources":["../../src/tools/posts.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * 프로필 수정 도구.
3
+ *
4
+ * `VELOG_ALLOW_PROFILE=1` 이 있어야 실제로 동작한다. 근거는 capabilities.ts 주석 —
5
+ * 위험해서가 아니라 `short_bio`(프로필)와 `short_description`(글) 혼동 때문이다.
6
+ *
7
+ * ★ 병합이 핵심이다. `UpdateProfileInput` 은 `display_name` 과 `short_bio` 를
8
+ * **둘 다 필수**로 받는다. 하나만 바꾸겠다고 한쪽만 보내면 다른 쪽이 빈 문자열로
9
+ * 덮인다. 그래서 항상 현재 값을 읽어 채운 뒤 보낸다.
10
+ * (초안 도구의 '생략=초기화'가 사고를 부른다는 걸 이미 확인했다.)
11
+ */
12
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
13
+ import type { VelogClient } from '../client.ts';
14
+ import type { Capabilities } from '../capabilities.ts';
15
+ export declare function registerProfileEditTools(server: McpServer, client: VelogClient, capabilities: Capabilities): void;
@@ -0,0 +1,216 @@
1
+ /**
2
+ * 프로필 수정 도구.
3
+ *
4
+ * `VELOG_ALLOW_PROFILE=1` 이 있어야 실제로 동작한다. 근거는 capabilities.ts 주석 —
5
+ * 위험해서가 아니라 `short_bio`(프로필)와 `short_description`(글) 혼동 때문이다.
6
+ *
7
+ * ★ 병합이 핵심이다. `UpdateProfileInput` 은 `display_name` 과 `short_bio` 를
8
+ * **둘 다 필수**로 받는다. 하나만 바꾸겠다고 한쪽만 보내면 다른 쪽이 빈 문자열로
9
+ * 덮인다. 그래서 항상 현재 값을 읽어 채운 뒤 보낸다.
10
+ * (초안 도구의 '생략=초기화'가 사고를 부른다는 걸 이미 확인했다.)
11
+ */
12
+ import { z } from 'zod';
13
+ import { textResult } from "../format.js";
14
+ import { isSafeImageUrl, isSafeProfileLink } from "../slug.js";
15
+ import { fetchCurrentUser, invalidateMe } from "../me.js";
16
+ const MUTATION_UPDATE_PROFILE = `
17
+ mutation UpdateProfile($input: UpdateProfileInput!) {
18
+ updateProfile(input: $input) { id display_name short_bio }
19
+ }
20
+ `;
21
+ const MUTATION_UPDATE_ABOUT = `
22
+ mutation UpdateAbout($input: UpdateAboutInput!) {
23
+ updateAbout(input: $input) { id about }
24
+ }
25
+ `;
26
+ const MUTATION_UPDATE_VELOG_TITLE = `
27
+ mutation UpdateVelogTitle($input: UpdateVelogTitleInput!) {
28
+ updateVelogTitle(input: $input) { title logo_image }
29
+ }
30
+ `;
31
+ const MUTATION_UPDATE_SOCIAL = `
32
+ mutation UpdateSocialInfo($input: UpdateSocialInfoInput!) {
33
+ updateSocialInfo(input: $input) { id profile_links }
34
+ }
35
+ `;
36
+ const MUTATION_UPDATE_THUMBNAIL = `
37
+ mutation UpdateThumbnail($input: UpdateThumbnailInput!) {
38
+ updateThumbnail(input: $input) { id thumbnail }
39
+ }
40
+ `;
41
+ /** 현재 프로필 전체. 병합에 필요해 한 번에 읽는다. */
42
+ const QUERY_MY_PROFILE = `
43
+ query MyProfile($input: GetUserInput!) {
44
+ user(input: $input) {
45
+ id
46
+ username
47
+ profile { display_name short_bio about thumbnail profile_links }
48
+ velog_config { title }
49
+ }
50
+ }
51
+ `;
52
+ /** 벨로그가 받는 SNS 링크 키. 임의 키를 넣으면 프로필 화면에서 안 보인다. */
53
+ const SOCIAL_KEYS = ['github', 'twitter', 'facebook', 'url', 'email'];
54
+ export function registerProfileEditTools(server, client, capabilities) {
55
+ // 꺼져 있으면 도구를 아예 등록하지 않는다. 목록에 없으면 부를 수도 없다.
56
+ if (!capabilities.editProfile)
57
+ return;
58
+ /** 현재 프로필을 읽는다. 병합의 기준값이다. */
59
+ async function current(toolName) {
60
+ client.requireAuth(toolName);
61
+ const me = await fetchCurrentUser(client);
62
+ if (!me.username) {
63
+ throw new Error(`${toolName}: 현재 계정의 username 을 확인하지 못했습니다.`);
64
+ }
65
+ const data = await client.request(QUERY_MY_PROFILE, {
66
+ input: { username: me.username },
67
+ });
68
+ if (!data.user) {
69
+ throw new Error(`${toolName}: 프로필을 불러오지 못했습니다.`);
70
+ }
71
+ return data.user;
72
+ }
73
+ server.registerTool('velog_update_profile', {
74
+ title: '프로필 수정 (이름·한줄소개)',
75
+ description: '벨로그 프로필의 표시 이름과 한줄 소개를 바꾼다. ' +
76
+ '★ 이건 **블로그 주인의 프로필**이다 — 글의 요약문(short_description)이 아니다. ' +
77
+ '생략한 항목은 현재 값을 그대로 유지한다.',
78
+ inputSchema: {
79
+ // 서버가 공백뿐인 이름을 거부하므로 여기서 먼저 막는다.
80
+ display_name: z
81
+ .string()
82
+ .max(255)
83
+ .refine((v) => v.trim().length > 0, '표시 이름은 공백만으로 둘 수 없습니다')
84
+ .optional()
85
+ .describe('표시 이름. 생략하면 유지'),
86
+ // ★ 서버가 short_bio 를 255자로 **조용히 자른다**(userResolvers 의 slice).
87
+ // 여기서 안 막으면 "저장했습니다" 라고 해놓고 뒷부분이 사라진다.
88
+ short_bio: z
89
+ .string()
90
+ .max(255, '한줄 소개는 255자까지입니다 (서버가 그 이상을 조용히 자릅니다)')
91
+ .optional()
92
+ .describe('한줄 소개 (255자 이내). 생략하면 유지'),
93
+ },
94
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true },
95
+ }, async ({ display_name, short_bio }) => {
96
+ const user = await current('velog_update_profile');
97
+ const p = user.profile;
98
+ // ★ 둘 다 필수 필드라 한쪽만 보내면 다른 쪽이 지워진다. 반드시 채워 보낸다.
99
+ const input = {
100
+ display_name: display_name ?? p?.display_name ?? '',
101
+ short_bio: short_bio ?? p?.short_bio ?? '',
102
+ };
103
+ await client.mutate(MUTATION_UPDATE_PROFILE, { input });
104
+ invalidateMe(client);
105
+ const after = await current('velog_update_profile');
106
+ return textResult([
107
+ '✅ 프로필을 수정했습니다.',
108
+ '',
109
+ `- 이름: ${after.profile?.display_name ?? '—'}`,
110
+ `- 한줄 소개: ${after.profile?.short_bio ?? '—'}`,
111
+ `- 확인: https://velog.io/@${after.username ?? ''}`,
112
+ ].join('\n'));
113
+ });
114
+ server.registerTool('velog_update_about', {
115
+ title: '소개글 수정',
116
+ description: '벨로그 "소개" 탭의 긴 글을 통째로 바꾼다(마크다운). ' +
117
+ '★ 전체 교체다 — 기존 내용에 덧붙이려면 먼저 velog_get_user 로 읽어 합친 뒤 넘길 것.',
118
+ inputSchema: {
119
+ about: z.string().describe('소개글 전문 (마크다운). 빈 문자열이면 소개글이 비워진다'),
120
+ },
121
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true },
122
+ }, async ({ about }) => {
123
+ client.requireAuth('velog_update_about');
124
+ await client.mutate(MUTATION_UPDATE_ABOUT, { input: { about } });
125
+ invalidateMe(client);
126
+ const after = await current('velog_update_about');
127
+ const saved = after.profile?.about ?? '';
128
+ return textResult([
129
+ '✅ 소개글을 수정했습니다.',
130
+ '',
131
+ `- 길이: ${saved.length}자`,
132
+ `- 확인: https://velog.io/@${after.username ?? ''}/about`,
133
+ '',
134
+ '--- 저장된 내용 앞부분 ---',
135
+ saved.slice(0, 300) + (saved.length > 300 ? '…' : ''),
136
+ ].join('\n'));
137
+ });
138
+ server.registerTool('velog_update_blog_title', {
139
+ title: '블로그 제목 수정',
140
+ description: '벨로그 상단에 뜨는 블로그 제목을 바꾼다 (예: "velopert.log").',
141
+ inputSchema: { title: z.string().min(1).describe('새 블로그 제목') },
142
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true },
143
+ }, async ({ title }) => {
144
+ client.requireAuth('velog_update_blog_title');
145
+ await client.mutate(MUTATION_UPDATE_VELOG_TITLE, { input: { title } });
146
+ const after = await current('velog_update_blog_title');
147
+ return textResult(`✅ 블로그 제목을 "${after.velog_config?.title ?? title}" 로 바꿨습니다.\n` +
148
+ `- 확인: https://velog.io/@${after.username ?? ''}`);
149
+ });
150
+ server.registerTool('velog_update_social_links', {
151
+ title: 'SNS 링크 수정',
152
+ description: `프로필의 SNS 링크를 바꾼다. 지원 키: ${SOCIAL_KEYS.join(', ')}. ` +
153
+ '생략한 키는 현재 값을 유지하고, 빈 문자열을 주면 그 항목이 지워진다.',
154
+ inputSchema: {
155
+ // 빈 문자열은 '삭제' 의미라 허용한다. 그 외에는 형식을 본다 —
156
+ // 잘못된 링크를 프로필에 박아두면 방문자가 깨진 링크를 밟는다.
157
+ github: z.string().max(100).optional().describe('사용자명 또는 URL'),
158
+ twitter: z.string().max(100).optional(),
159
+ facebook: z.string().max(100).optional(),
160
+ // 벨로그 웹과 같은 규칙으로 본다 — slug.ts 의 isSafeProfileLink 주석 참고.
161
+ url: z
162
+ .string()
163
+ .max(255)
164
+ .refine(isSafeProfileLink, 'javascript:·data: 같은 스킴은 넣을 수 없습니다')
165
+ .optional()
166
+ .describe('홈페이지. scheme 없이 example.com 도 됩니다 (빈 문자열이면 삭제)'),
167
+ email: z
168
+ .string()
169
+ .refine((v) => v === '' || /^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(v), '이메일 형식이 아닙니다')
170
+ .optional()
171
+ .describe('공개 이메일 (빈 문자열이면 삭제)'),
172
+ },
173
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true },
174
+ }, async (args) => {
175
+ const user = await current('velog_update_social_links');
176
+ const existing = user.profile?.profile_links ?? {};
177
+ // ★ profile_links 는 JSON 통째로 교체된다. 기존 값 위에 덮어써야
178
+ // 지정하지 않은 링크가 사라지지 않는다.
179
+ const merged = { ...existing };
180
+ for (const key of SOCIAL_KEYS) {
181
+ const value = args[key];
182
+ if (typeof value === 'string')
183
+ merged[key] = value;
184
+ }
185
+ await client.mutate(MUTATION_UPDATE_SOCIAL, {
186
+ input: { profile_links: merged },
187
+ });
188
+ invalidateMe(client);
189
+ const after = await current('velog_update_social_links');
190
+ const links = after.profile?.profile_links ?? {};
191
+ const shown = Object.entries(links)
192
+ .filter(([, v]) => typeof v === 'string' && v.length > 0)
193
+ .map(([k, v]) => ` - ${k}: ${String(v)}`)
194
+ .join('\n');
195
+ return textResult(`✅ SNS 링크를 수정했습니다.\n\n${shown || ' (등록된 링크 없음)'}`);
196
+ });
197
+ server.registerTool('velog_update_profile_image', {
198
+ title: '프로필 사진 변경',
199
+ description: '프로필 이미지를 지정한 URL 로 바꾼다. http(s) 이미지 주소만 받는다. ' +
200
+ '이미지 업로드 기능은 없으므로 이미 어딘가에 올라간 주소가 필요하다.',
201
+ inputSchema: {
202
+ url: z
203
+ .string()
204
+ .refine(isSafeImageUrl, 'http(s) 이미지 URL 만 허용합니다')
205
+ .describe('이미지 URL'),
206
+ },
207
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true },
208
+ }, async ({ url }) => {
209
+ client.requireAuth('velog_update_profile_image');
210
+ await client.mutate(MUTATION_UPDATE_THUMBNAIL, { input: { url } });
211
+ invalidateMe(client);
212
+ const after = await current('velog_update_profile_image');
213
+ return textResult(`✅ 프로필 사진을 바꿨습니다.\n- 현재: ${after.profile?.thumbnail ?? '—'}`);
214
+ });
215
+ }
216
+ //# sourceMappingURL=profile-edit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"profile-edit.js","sourceRoot":"","sources":["../../src/tools/profile-edit.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * 사용자·시리즈·태그 조회. 전부 읽기 전용이고 인증이 필요 없다.
3
+ *
4
+ * 시리즈와 태그는 "이 사람이 무엇을 꾸준히 쓰는가"를 한눈에 보여준다.
5
+ * 글 목록을 다 훑는 것보다 훨씬 싸다.
6
+ */
7
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
8
+ import type { VelogClient } from '../client.ts';
9
+ export declare function registerProfileTools(server: McpServer, client: VelogClient): void;
@@ -0,0 +1,133 @@
1
+ /**
2
+ * 사용자·시리즈·태그 조회. 전부 읽기 전용이고 인증이 필요 없다.
3
+ *
4
+ * 시리즈와 태그는 "이 사람이 무엇을 꾸준히 쓰는가"를 한눈에 보여준다.
5
+ * 글 목록을 다 훑는 것보다 훨씬 싸다.
6
+ */
7
+ import { z } from 'zod';
8
+ import { textResult } from "../format.js";
9
+ import { READ_ONLY } from "./posts.js";
10
+ import { fetchCurrentUser } from "../me.js";
11
+ const QUERY_USER = `
12
+ query GetUser($input: GetUserInput!) {
13
+ user(input: $input) {
14
+ id
15
+ username
16
+ followers_count
17
+ followings_count
18
+ profile { display_name short_bio thumbnail about profile_links }
19
+ velog_config { title }
20
+ }
21
+ }
22
+ `;
23
+ const QUERY_SERIES_LIST = `
24
+ query SeriesList($input: GetSeriesListInput!) {
25
+ seriesList(input: $input) {
26
+ id
27
+ name
28
+ url_slug
29
+ description
30
+ posts_count
31
+ updated_at
32
+ }
33
+ }
34
+ `;
35
+ const QUERY_USER_TAGS = `
36
+ query UserTags($input: UserTagsInput!) {
37
+ userTags(input: $input) {
38
+ tags { id name posts_count }
39
+ }
40
+ }
41
+ `;
42
+ export function registerProfileTools(server, client) {
43
+ server.registerTool('velog_whoami', {
44
+ title: '내 계정 확인',
45
+ description: '현재 토큰으로 인증된 계정을 확인한다. 토큰이 살아있는지 점검하는 용도로도 쓴다. ' +
46
+ '다른 도구에서 username 을 생략하면 여기서 얻는 계정을 쓴다.',
47
+ inputSchema: {},
48
+ annotations: READ_ONLY,
49
+ }, async () => {
50
+ client.requireAuth('velog_whoami');
51
+ const me = await fetchCurrentUser(client);
52
+ return textResult([
53
+ `✅ 인증됨 — @${me.username ?? '(username 없음)'}`,
54
+ '',
55
+ `- 이름: ${me.profile?.display_name ?? '—'}`,
56
+ `- 소개: ${me.profile?.short_bio ?? '—'}`,
57
+ `- 내 블로그: https://velog.io/@${me.username ?? ''}`,
58
+ '',
59
+ '※ 공개 발행은 VELOG_ALLOW_PUBLIC=1 이 설정돼 있을 때만 가능합니다.',
60
+ ].join('\n'));
61
+ });
62
+ server.registerTool('velog_get_user', {
63
+ title: '벨로그 사용자 정보',
64
+ description: '벨로그 사용자의 프로필과 팔로워 수를 조회한다.',
65
+ inputSchema: { username: z.string().describe('@ 없이') },
66
+ annotations: READ_ONLY,
67
+ }, async ({ username }) => {
68
+ const data = await client.request(QUERY_USER, {
69
+ input: { username },
70
+ });
71
+ const user = data.user;
72
+ if (!user)
73
+ return textResult(`@${username} 을(를) 찾지 못했습니다.`);
74
+ const p = user.profile;
75
+ const lines = [
76
+ `# @${user.username ?? username}`,
77
+ '',
78
+ `- 이름: ${p?.display_name ?? '—'}`,
79
+ `- 소개: ${p?.short_bio ?? '—'}`,
80
+ `- 블로그 제목: ${user.velog_config?.title ?? '—'}`,
81
+ `- 팔로워 ${user.followers_count ?? 0} · 팔로잉 ${user.followings_count ?? 0}`,
82
+ `- URL: https://velog.io/@${user.username ?? username}`,
83
+ ];
84
+ if (p?.about?.trim()) {
85
+ lines.push('', '## 소개글', p.about.trim().slice(0, 1000));
86
+ }
87
+ return textResult(lines.join('\n'));
88
+ });
89
+ server.registerTool('velog_list_series', {
90
+ title: '시리즈 목록',
91
+ description: '사용자의 연재 시리즈 목록. 각 시리즈에 글이 몇 편인지 함께 준다. ' +
92
+ '초안을 특정 시리즈에 넣으려면 여기서 얻은 id 를 velog_create_draft 의 series_id 에 준다.',
93
+ inputSchema: { username: z.string().describe('@ 없이') },
94
+ annotations: READ_ONLY,
95
+ }, async ({ username }) => {
96
+ const data = await client.request(QUERY_SERIES_LIST, {
97
+ input: { username },
98
+ });
99
+ const list = data.seriesList ?? [];
100
+ if (list.length === 0)
101
+ return textResult(`@${username} 의 시리즈가 없습니다.`);
102
+ const body = list
103
+ .map((s, i) => `${i + 1}. ${s.name ?? '(제목 없음)'} — ${s.posts_count ?? 0}편\n` +
104
+ ` id: \`${s.id}\`\n` +
105
+ ` https://velog.io/@${username}/series/${s.url_slug ?? ''}`)
106
+ .join('\n\n');
107
+ return textResult(`시리즈 ${list.length}개\n\n${body}`);
108
+ });
109
+ server.registerTool('velog_user_tags', {
110
+ title: '사용자 태그 목록',
111
+ description: '사용자가 쓴 태그와 각 태그의 글 수. "이 사람이 뭘 주로 쓰나"를 가장 싸게 파악하는 방법이다.',
112
+ inputSchema: {
113
+ username: z.string().describe('@ 없이'),
114
+ top: z.number().int().min(1).max(100).default(30),
115
+ },
116
+ annotations: READ_ONLY,
117
+ }, async ({ username, top }) => {
118
+ const data = await client.request(QUERY_USER_TAGS, {
119
+ input: { username },
120
+ });
121
+ const tags = data.userTags?.tags ?? [];
122
+ if (tags.length === 0)
123
+ return textResult(`@${username} 의 태그가 없습니다.`);
124
+ const sorted = [...tags]
125
+ .sort((a, b) => (b.posts_count ?? 0) - (a.posts_count ?? 0))
126
+ .slice(0, top);
127
+ const body = sorted
128
+ .map((t) => ` #${t.name ?? '?'} — ${t.posts_count ?? 0}편`)
129
+ .join('\n');
130
+ return textResult(`@${username} 의 태그 ${tags.length}개 (상위 ${sorted.length})\n\n${body}`);
131
+ });
132
+ }
133
+ //# sourceMappingURL=profile.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"profile.js","sourceRoot":"","sources":["../../src/tools/profile.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * 발행 도구.
3
+ *
4
+ * 기본은 **비공개 발행**이다. 공개 발행은 `VELOG_ALLOW_PUBLIC=1` 이 있어야 한다
5
+ * — 근거는 src/capabilities.ts 주석 참고 (비공개 글은 벨로그의 파괴적 제한에
6
+ * 걸리지 않지만 공개 글은 걸리고, RSS·검색·구독메일로 나가면 회수가 안 된다).
7
+ *
8
+ * 초안 작성과 발행을 **별도 도구**로 나눈 이유: 같은 도구에 `is_temp` 파라미터를
9
+ * 두면 초안을 쓰려다 값 하나 잘못 넣어 발행되는 경로가 생긴다. 이름이 다르면
10
+ * 그 사고가 안 난다.
11
+ */
12
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
13
+ import type { VelogClient } from '../client.ts';
14
+ import type { Capabilities } from '../capabilities.ts';
15
+ import type { PublishRateLimiter } from '../ratelimit.ts';
16
+ export declare function registerPublishTools(server: McpServer, client: VelogClient, capabilities: Capabilities, limiter: PublishRateLimiter): void;