codeblog-mcp 1.6.0 → 1.7.2

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,28 @@
1
+ type ToolResult = {
2
+ content: Array<{
3
+ type: "text";
4
+ text: string;
5
+ }>;
6
+ isError?: boolean;
7
+ };
8
+ /**
9
+ * Pre-check: ensure API key is configured.
10
+ * Returns { apiKey, serverUrl } on success, or a ToolResult error to return early.
11
+ */
12
+ export declare function requireAuth(): {
13
+ apiKey: string;
14
+ serverUrl: string;
15
+ } | ToolResult;
16
+ /**
17
+ * Type guard: check if requireAuth returned an error result.
18
+ */
19
+ export declare function isAuthError(result: ReturnType<typeof requireAuth>): result is ToolResult;
20
+ /**
21
+ * Wrap a tool handler that requires authentication.
22
+ * Automatically checks API key and injects { apiKey, serverUrl } into the handler.
23
+ */
24
+ export declare function withAuth<TArgs, TResult>(handler: (args: TArgs, ctx: {
25
+ apiKey: string;
26
+ serverUrl: string;
27
+ }) => Promise<TResult>): (args: TArgs) => Promise<TResult | ToolResult>;
28
+ export {};
@@ -0,0 +1,30 @@
1
+ import { getApiKey, getUrl, text, SETUP_GUIDE } from "./config.js";
2
+ /**
3
+ * Pre-check: ensure API key is configured.
4
+ * Returns { apiKey, serverUrl } on success, or a ToolResult error to return early.
5
+ */
6
+ export function requireAuth() {
7
+ const apiKey = getApiKey();
8
+ const serverUrl = getUrl();
9
+ if (!apiKey)
10
+ return { content: [text(SETUP_GUIDE)], isError: true };
11
+ return { apiKey, serverUrl };
12
+ }
13
+ /**
14
+ * Type guard: check if requireAuth returned an error result.
15
+ */
16
+ export function isAuthError(result) {
17
+ return "content" in result && "isError" in result;
18
+ }
19
+ /**
20
+ * Wrap a tool handler that requires authentication.
21
+ * Automatically checks API key and injects { apiKey, serverUrl } into the handler.
22
+ */
23
+ export function withAuth(handler) {
24
+ return async (args) => {
25
+ const auth = requireAuth();
26
+ if (isAuthError(auth))
27
+ return auth;
28
+ return handler(args, auth);
29
+ };
30
+ }
@@ -6,4 +6,5 @@ export declare function listFiles(dir: string, extensions?: string[], recursive?
6
6
  export declare function listDirs(dir: string): string[];
7
7
  export declare function exists(p: string): boolean;
8
8
  export declare function extractProjectDescription(projectPath: string): string | null;
9
+ export declare function decodeDirNameToPath(dirName: string): string | null;
9
10
  export declare function readJsonl<T = unknown>(filePath: string): T[];
@@ -1,5 +1,6 @@
1
1
  import * as fs from "fs";
2
2
  import * as path from "path";
3
+ import { getPlatform } from "./platform.js";
3
4
  // Safely read a file, return null on error
4
5
  export function safeReadFile(filePath) {
5
6
  try {
@@ -127,6 +128,47 @@ export function extractProjectDescription(projectPath) {
127
128
  }
128
129
  return null;
129
130
  }
131
+ // Decode a hyphen-encoded directory name back to a real filesystem path.
132
+ // e.g. "-Users-zhaoyifei-my-cool-project" → "/Users/zhaoyifei/my-cool-project"
133
+ // On Windows, names like "c-Users-PC-project" → "C:\Users\PC\project".
134
+ // Greedy strategy: try longest segments first, check if path exists on disk.
135
+ export function decodeDirNameToPath(dirName) {
136
+ const platform = getPlatform();
137
+ const stripped = dirName.startsWith("-") ? dirName.slice(1) : dirName;
138
+ const parts = stripped.split("-");
139
+ let currentPath = "";
140
+ let i = 0;
141
+ // On Windows, the first part may be a drive letter (e.g. "c" → "C:")
142
+ if (platform === "windows" && parts.length > 0 && /^[a-zA-Z]$/.test(parts[0])) {
143
+ currentPath = parts[0].toUpperCase() + ":";
144
+ i = 1;
145
+ }
146
+ while (i < parts.length) {
147
+ let bestMatch = "";
148
+ let bestLen = 0;
149
+ for (let end = parts.length; end > i; end--) {
150
+ const segment = parts.slice(i, end).join("-");
151
+ const candidate = currentPath + path.sep + segment;
152
+ try {
153
+ if (fs.existsSync(candidate)) {
154
+ bestMatch = candidate;
155
+ bestLen = end - i;
156
+ break;
157
+ }
158
+ }
159
+ catch { /* ignore */ }
160
+ }
161
+ if (bestLen > 0) {
162
+ currentPath = bestMatch;
163
+ i += bestLen;
164
+ }
165
+ else {
166
+ currentPath += path.sep + parts[i];
167
+ i++;
168
+ }
169
+ }
170
+ return currentPath || null;
171
+ }
130
172
  // Read JSONL file (one JSON object per line)
131
173
  export function readJsonl(filePath) {
132
174
  const content = safeReadFile(filePath);
@@ -1,7 +1,7 @@
1
1
  import * as path from "path";
2
2
  import * as fs from "fs";
3
- import { getHome, getPlatform } from "../lib/platform.js";
4
- import { listFiles, listDirs, safeStats, readJsonl, extractProjectDescription } from "../lib/fs-utils.js";
3
+ import { getHome } from "../lib/platform.js";
4
+ import { listFiles, listDirs, safeStats, readJsonl, extractProjectDescription, decodeDirNameToPath } from "../lib/fs-utils.js";
5
5
  export const claudeCodeScanner = {
6
6
  name: "Claude Code",
7
7
  sourceType: "claude-code",
@@ -40,7 +40,7 @@ export const claudeCodeScanner = {
40
40
  let projectPath = cwdLine?.cwd || null;
41
41
  // Fallback: derive from directory name (e.g. "-Users-zhaoyifei-Foo" → "/Users/zhaoyifei/Foo")
42
42
  if (!projectPath && project.startsWith("-")) {
43
- projectPath = decodeClaudeProjectDir(project);
43
+ projectPath = decodeDirNameToPath(project);
44
44
  }
45
45
  const projectName = projectPath ? path.basename(projectPath) : project;
46
46
  // Get project description from README/package.json
@@ -134,50 +134,6 @@ export const claudeCodeScanner = {
134
134
  };
135
135
  },
136
136
  };
137
- // Decode Claude Code project directory name back to a real path.
138
- // e.g. "-Users-zhaoyifei-VibeCodingWork-ai-code-forum" → "/Users/zhaoyifei/VibeCodingWork/ai-code-forum"
139
- // The challenge: hyphens in the dir name could be path separators OR part of a folder name.
140
- // Strategy: greedily build path segments, checking which paths actually exist on disk.
141
- function decodeClaudeProjectDir(dirName) {
142
- const platform = getPlatform();
143
- // Remove leading dash
144
- const stripped = dirName.startsWith("-") ? dirName.slice(1) : dirName;
145
- const parts = stripped.split("-");
146
- let currentPath = "";
147
- let i = 0;
148
- // On Windows, the first part may be a drive letter (e.g. "c" → "C:")
149
- if (platform === "windows" && parts.length > 0 && /^[a-zA-Z]$/.test(parts[0])) {
150
- currentPath = parts[0].toUpperCase() + ":";
151
- i = 1;
152
- }
153
- while (i < parts.length) {
154
- // Try progressively longer segments (greedy: longest existing path wins)
155
- let bestMatch = "";
156
- let bestLen = 0;
157
- for (let end = parts.length; end > i; end--) {
158
- const segment = parts.slice(i, end).join("-");
159
- const candidate = currentPath + path.sep + segment;
160
- try {
161
- if (fs.existsSync(candidate)) {
162
- bestMatch = candidate;
163
- bestLen = end - i;
164
- break; // Found longest match
165
- }
166
- }
167
- catch { /* ignore */ }
168
- }
169
- if (bestLen > 0) {
170
- currentPath = bestMatch;
171
- i += bestLen;
172
- }
173
- else {
174
- // No existing path found, just use single segment
175
- currentPath += path.sep + parts[i];
176
- i++;
177
- }
178
- }
179
- return currentPath || null;
180
- }
181
137
  function extractContent(msg) {
182
138
  if (!msg.message?.content)
183
139
  return "";
@@ -2,7 +2,7 @@ import * as path from "path";
2
2
  import * as fs from "fs";
3
3
  import BetterSqlite3 from "better-sqlite3";
4
4
  import { getHome, getPlatform } from "../lib/platform.js";
5
- import { listFiles, listDirs, safeReadFile, safeReadJson, safeStats, extractProjectDescription } from "../lib/fs-utils.js";
5
+ import { listFiles, listDirs, safeReadFile, safeReadJson, safeStats, extractProjectDescription, decodeDirNameToPath } from "../lib/fs-utils.js";
6
6
  // Cursor stores conversations in THREE places (all supported for version compatibility):
7
7
  //
8
8
  // FORMAT 1 — Agent transcripts (plain text, XML-like tags):
@@ -405,43 +405,3 @@ function parseVscdbSession(virtualPath, maxTurns) {
405
405
  };
406
406
  }, null);
407
407
  }
408
- // Decode a directory name like "Users-zhaoyifei-my-cool-project" back to a real path.
409
- // On Windows, names look like "c-Users-PC-project" → "C:\Users\PC\project".
410
- // Greedy strategy: try longest segments first, check if path exists on disk.
411
- function decodeDirNameToPath(dirName) {
412
- const platform = getPlatform();
413
- const stripped = dirName.startsWith("-") ? dirName.slice(1) : dirName;
414
- const parts = stripped.split("-");
415
- let currentPath = "";
416
- let i = 0;
417
- // On Windows, the first part may be a drive letter (e.g. "c" → "C:")
418
- if (platform === "windows" && parts.length > 0 && /^[a-zA-Z]$/.test(parts[0])) {
419
- currentPath = parts[0].toUpperCase() + ":";
420
- i = 1;
421
- }
422
- while (i < parts.length) {
423
- let bestMatch = "";
424
- let bestLen = 0;
425
- for (let end = parts.length; end > i; end--) {
426
- const segment = parts.slice(i, end).join("-");
427
- const candidate = currentPath + path.sep + segment;
428
- try {
429
- if (fs.existsSync(candidate)) {
430
- bestMatch = candidate;
431
- bestLen = end - i;
432
- break;
433
- }
434
- }
435
- catch { /* ignore */ }
436
- }
437
- if (bestLen > 0) {
438
- currentPath = bestMatch;
439
- i += bestLen;
440
- }
441
- else {
442
- currentPath += path.sep + parts[i];
443
- i++;
444
- }
445
- }
446
- return currentPath || null;
447
- }
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
- import { getApiKey, getUrl, text, SETUP_GUIDE } from "../lib/config.js";
2
+ import { text } from "../lib/config.js";
3
+ import { withAuth } from "../lib/auth-guard.js";
3
4
  export function registerAgentTools(server) {
4
5
  server.registerTool("manage_agents", {
5
6
  description: "Manage your CodeBlog agents — list all agents, create a new one, delete one, or switch between them. " +
@@ -15,11 +16,7 @@ export function registerAgentTools(server) {
15
16
  source_type: z.string().optional().describe("IDE source: claude-code, cursor, codex, windsurf, git, other (required for create)"),
16
17
  agent_id: z.string().optional().describe("Agent ID (required for delete and switch)"),
17
18
  },
18
- }, async ({ action, name, description, source_type, agent_id }) => {
19
- const apiKey = getApiKey();
20
- const serverUrl = getUrl();
21
- if (!apiKey)
22
- return { content: [text(SETUP_GUIDE)], isError: true };
19
+ }, withAuth(async ({ action, name, description, source_type, agent_id }, { apiKey, serverUrl }) => {
23
20
  if (action === "list") {
24
21
  try {
25
22
  const res = await fetch(`${serverUrl}/api/v1/agents/list`, {
@@ -124,7 +121,7 @@ export function registerAgentTools(server) {
124
121
  }
125
122
  }
126
123
  return { content: [text("Invalid action. Use 'list', 'create', 'delete', or 'switch'.")], isError: true };
127
- });
124
+ }));
128
125
  server.registerTool("my_posts", {
129
126
  description: "Check out your own posts on CodeBlog — see what you've published, how they're doing (views, votes, comments). " +
130
127
  "Like checking your profile page stats. " +
@@ -133,11 +130,7 @@ export function registerAgentTools(server) {
133
130
  sort: z.enum(["new", "hot", "top"]).optional().describe("Sort: 'new' (default), 'hot' (most upvoted), 'top' (most viewed)"),
134
131
  limit: z.number().optional().describe("Max posts to return (default 10)"),
135
132
  },
136
- }, async ({ sort, limit }) => {
137
- const apiKey = getApiKey();
138
- const serverUrl = getUrl();
139
- if (!apiKey)
140
- return { content: [text(SETUP_GUIDE)], isError: true };
133
+ }, withAuth(async ({ sort, limit }, { apiKey, serverUrl }) => {
141
134
  const params = new URLSearchParams();
142
135
  if (sort)
143
136
  params.set("sort", sort);
@@ -168,17 +161,13 @@ export function registerAgentTools(server) {
168
161
  catch (err) {
169
162
  return { content: [text(`Network error: ${err}`)], isError: true };
170
163
  }
171
- });
164
+ }));
172
165
  server.registerTool("my_dashboard", {
173
166
  description: "Your personal CodeBlog dashboard — total stats, top posts, recent comments from others. " +
174
167
  "Like checking your GitHub profile overview but for your blog posts. " +
175
168
  "Example: my_dashboard() to see your full stats.",
176
169
  inputSchema: {},
177
- }, async () => {
178
- const apiKey = getApiKey();
179
- const serverUrl = getUrl();
180
- if (!apiKey)
181
- return { content: [text(SETUP_GUIDE)], isError: true };
170
+ }, withAuth(async (_args, { apiKey, serverUrl }) => {
182
171
  try {
183
172
  const res = await fetch(`${serverUrl}/api/v1/agents/me/dashboard`, {
184
173
  headers: { Authorization: `Bearer ${apiKey}` },
@@ -212,7 +201,7 @@ export function registerAgentTools(server) {
212
201
  catch (err) {
213
202
  return { content: [text(`Network error: ${err}`)], isError: true };
214
203
  }
215
- });
204
+ }));
216
205
  server.registerTool("follow_agent", {
217
206
  description: "Follow or unfollow other users on CodeBlog, see who you follow, or get a personalized feed of posts from people you follow. " +
218
207
  "Like following people on Twitter/X. " +
@@ -225,11 +214,7 @@ export function registerAgentTools(server) {
225
214
  user_id: z.string().optional().describe("User ID (required for follow/unfollow)"),
226
215
  limit: z.number().optional().describe("Max results for feed/list (default 10)"),
227
216
  },
228
- }, async ({ action, user_id, limit }) => {
229
- const apiKey = getApiKey();
230
- const serverUrl = getUrl();
231
- if (!apiKey)
232
- return { content: [text(SETUP_GUIDE)], isError: true };
217
+ }, withAuth(async ({ action, user_id, limit }, { apiKey, serverUrl }) => {
233
218
  if (action === "follow" || action === "unfollow") {
234
219
  if (!user_id) {
235
220
  return { content: [text("user_id is required for follow/unfollow.")], isError: true };
@@ -318,5 +303,5 @@ export function registerAgentTools(server) {
318
303
  }
319
304
  }
320
305
  return { content: [text("Invalid action. Use 'follow', 'unfollow', 'list_following', or 'feed'.")], isError: true };
321
- });
306
+ }));
322
307
  }
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { getApiKey, getUrl, text, SETUP_GUIDE } from "../lib/config.js";
3
+ import { withAuth } from "../lib/auth-guard.js";
3
4
  export function registerForumTools(server) {
4
5
  server.registerTool("browse_posts", {
5
6
  description: "Check out what's trending on CodeBlog — see what other devs and AI agents are posting about. Like scrolling your tech feed.",
@@ -188,11 +189,7 @@ export function registerForumTools(server) {
188
189
  "Bad: 'Great article! Very informative.' (max 5000 chars)"),
189
190
  parent_id: z.string().optional().describe("Reply to a specific comment by its ID"),
190
191
  },
191
- }, async ({ post_id, content, parent_id }) => {
192
- const apiKey = getApiKey();
193
- const serverUrl = getUrl();
194
- if (!apiKey)
195
- return { content: [text(SETUP_GUIDE)], isError: true };
192
+ }, withAuth(async ({ post_id, content, parent_id }, { apiKey, serverUrl }) => {
196
193
  try {
197
194
  const res = await fetch(`${serverUrl}/api/v1/posts/${post_id}/comment`, {
198
195
  method: "POST",
@@ -213,7 +210,7 @@ export function registerForumTools(server) {
213
210
  catch (err) {
214
211
  return { content: [text(`Network error: ${err}`)], isError: true };
215
212
  }
216
- });
213
+ }));
217
214
  server.registerTool("vote_on_post", {
218
215
  description: "Upvote or downvote a post. Upvote stuff that's genuinely useful or interesting. " +
219
216
  "Downvote low-effort or inaccurate content.",
@@ -221,11 +218,7 @@ export function registerForumTools(server) {
221
218
  post_id: z.string().describe("Post ID to vote on"),
222
219
  value: z.union([z.literal(1), z.literal(-1), z.literal(0)]).describe("1 for upvote, -1 for downvote, 0 to remove vote"),
223
220
  },
224
- }, async ({ post_id, value }) => {
225
- const apiKey = getApiKey();
226
- const serverUrl = getUrl();
227
- if (!apiKey)
228
- return { content: [text(SETUP_GUIDE)], isError: true };
221
+ }, withAuth(async ({ post_id, value }, { apiKey, serverUrl }) => {
229
222
  try {
230
223
  const res = await fetch(`${serverUrl}/api/v1/posts/${post_id}/vote`, {
231
224
  method: "POST",
@@ -243,7 +236,7 @@ export function registerForumTools(server) {
243
236
  catch (err) {
244
237
  return { content: [text(`Network error: ${err}`)], isError: true };
245
238
  }
246
- });
239
+ }));
247
240
  server.registerTool("explore_and_engage", {
248
241
  description: "Scroll through CodeBlog like checking your morning tech feed. " +
249
242
  "'browse' = catch up on what's new. " +
@@ -353,11 +346,7 @@ export function registerForumTools(server) {
353
346
  tags: z.array(z.string()).optional().describe("New tags array"),
354
347
  category: z.string().optional().describe("New category slug"),
355
348
  },
356
- }, async ({ post_id, title, content, summary, tags, category }) => {
357
- const apiKey = getApiKey();
358
- const serverUrl = getUrl();
359
- if (!apiKey)
360
- return { content: [text(SETUP_GUIDE)], isError: true };
349
+ }, withAuth(async ({ post_id, title, content, summary, tags, category }, { apiKey, serverUrl }) => {
361
350
  if (!title && !content && !summary && !tags && !category) {
362
351
  return { content: [text("Provide at least one field to update: title, content, summary, tags, or category.")], isError: true };
363
352
  }
@@ -392,7 +381,7 @@ export function registerForumTools(server) {
392
381
  catch (err) {
393
382
  return { content: [text(`Network error: ${err}`)], isError: true };
394
383
  }
395
- });
384
+ }));
396
385
  server.registerTool("delete_post", {
397
386
  description: "Delete one of your posts permanently. This removes the post and all its comments, votes, and bookmarks. " +
398
387
  "You must set confirm=true to actually delete. Can only delete your own posts. " +
@@ -401,11 +390,7 @@ export function registerForumTools(server) {
401
390
  post_id: z.string().describe("Post ID to delete"),
402
391
  confirm: z.boolean().describe("Must be true to confirm deletion"),
403
392
  },
404
- }, async ({ post_id, confirm }) => {
405
- const apiKey = getApiKey();
406
- const serverUrl = getUrl();
407
- if (!apiKey)
408
- return { content: [text(SETUP_GUIDE)], isError: true };
393
+ }, withAuth(async ({ post_id, confirm }, { apiKey, serverUrl }) => {
409
394
  if (!confirm) {
410
395
  return { content: [text("⚠️ Set confirm=true to actually delete the post. This action is irreversible.")], isError: true };
411
396
  }
@@ -424,7 +409,7 @@ export function registerForumTools(server) {
424
409
  catch (err) {
425
410
  return { content: [text(`Network error: ${err}`)], isError: true };
426
411
  }
427
- });
412
+ }));
428
413
  server.registerTool("bookmark_post", {
429
414
  description: "Save posts for later — bookmark/unbookmark a post, or list all your bookmarks. " +
430
415
  "Like starring a GitHub repo. " +
@@ -433,11 +418,7 @@ export function registerForumTools(server) {
433
418
  action: z.enum(["toggle", "list"]).describe("'toggle' = bookmark/unbookmark, 'list' = see all bookmarks"),
434
419
  post_id: z.string().optional().describe("Post ID (required for toggle)"),
435
420
  },
436
- }, async ({ action, post_id }) => {
437
- const apiKey = getApiKey();
438
- const serverUrl = getUrl();
439
- if (!apiKey)
440
- return { content: [text(SETUP_GUIDE)], isError: true };
421
+ }, withAuth(async ({ action, post_id }, { apiKey, serverUrl }) => {
441
422
  if (action === "toggle") {
442
423
  if (!post_id) {
443
424
  return { content: [text("post_id is required for toggle.")], isError: true };
@@ -487,7 +468,7 @@ export function registerForumTools(server) {
487
468
  }
488
469
  }
489
470
  return { content: [text("Invalid action. Use 'toggle' or 'list'.")], isError: true };
490
- });
471
+ }));
491
472
  server.registerTool("my_notifications", {
492
473
  description: "Check your notifications — see who commented on your posts, who upvoted, etc. " +
493
474
  "Like checking your GitHub notification bell. " +
@@ -496,11 +477,7 @@ export function registerForumTools(server) {
496
477
  action: z.enum(["list", "read_all"]).describe("'list' = see notifications, 'read_all' = mark all as read"),
497
478
  limit: z.number().optional().describe("Max notifications to show (default 20)"),
498
479
  },
499
- }, async ({ action, limit }) => {
500
- const apiKey = getApiKey();
501
- const serverUrl = getUrl();
502
- if (!apiKey)
503
- return { content: [text(SETUP_GUIDE)], isError: true };
480
+ }, withAuth(async ({ action, limit }, { apiKey, serverUrl }) => {
504
481
  if (action === "read_all") {
505
482
  try {
506
483
  const res = await fetch(`${serverUrl}/api/v1/notifications/read`, {
@@ -543,7 +520,7 @@ export function registerForumTools(server) {
543
520
  catch (err) {
544
521
  return { content: [text(`Network error: ${err}`)], isError: true };
545
522
  }
546
- });
523
+ }));
547
524
  server.registerTool("browse_by_tag", {
548
525
  description: "Browse CodeBlog by tag — see trending tags or find posts about a specific topic. " +
549
526
  "Like filtering by hashtag. " +
@@ -1,7 +1,8 @@
1
1
  import { z } from "zod";
2
2
  import * as fs from "fs";
3
3
  import * as path from "path";
4
- import { getApiKey, getUrl, getLanguage, text, SETUP_GUIDE, CONFIG_DIR } from "../lib/config.js";
4
+ import { getUrl, getLanguage, text, CONFIG_DIR } from "../lib/config.js";
5
+ import { withAuth, requireAuth, isAuthError } from "../lib/auth-guard.js";
5
6
  import { scanAll, parseSession } from "../lib/registry.js";
6
7
  import { analyzeSession } from "../lib/analyzer.js";
7
8
  export function registerPostingTools(server) {
@@ -30,11 +31,7 @@ export function registerPostingTools(server) {
30
31
  category: z.string().optional().describe("Category: 'general', 'til', 'bugs', 'patterns', 'performance', 'tools'"),
31
32
  language: z.string().optional().describe("Content language tag, e.g. 'English', '中文', '日本語'. Defaults to agent's defaultLanguage."),
32
33
  },
33
- }, async ({ title, content, source_session, tags, summary, category, language }) => {
34
- const apiKey = getApiKey();
35
- const serverUrl = getUrl();
36
- if (!apiKey)
37
- return { content: [text(SETUP_GUIDE)], isError: true };
34
+ }, withAuth(async ({ title, content, source_session, tags, summary, category, language }, { apiKey, serverUrl }) => {
38
35
  if (!source_session) {
39
36
  return { content: [text("source_session is required. Use scan_sessions first.")], isError: true };
40
37
  }
@@ -57,7 +54,7 @@ export function registerPostingTools(server) {
57
54
  catch (err) {
58
55
  return { content: [text(`Network error: ${err}`)], isError: true };
59
56
  }
60
- });
57
+ }));
61
58
  server.registerTool("auto_post", {
62
59
  description: "One-click: scan your recent coding sessions, find the juiciest story, " +
63
60
  "and write a casual tech blog post about it. Like having a dev friend write up your coding war story. " +
@@ -78,11 +75,7 @@ export function registerPostingTools(server) {
78
75
  dry_run: z.boolean().optional().describe("If true, preview the post without publishing"),
79
76
  language: z.string().optional().describe("Content language tag, e.g. 'English', '中文', '日本語'. Defaults to agent's defaultLanguage."),
80
77
  },
81
- }, async ({ source, style, dry_run, language }) => {
82
- const apiKey = getApiKey();
83
- const serverUrl = getUrl();
84
- if (!apiKey)
85
- return { content: [text(SETUP_GUIDE)], isError: true };
78
+ }, withAuth(async ({ source, style, dry_run, language }, { apiKey, serverUrl }) => {
86
79
  // 1. Scan sessions
87
80
  let sessions = scanAll(30, source || undefined);
88
81
  if (sessions.length === 0) {
@@ -249,7 +242,7 @@ export function registerPostingTools(server) {
249
242
  catch (err) {
250
243
  return { content: [text(`Network error: ${err}`)], isError: true };
251
244
  }
252
- });
245
+ }));
253
246
  server.registerTool("weekly_digest", {
254
247
  description: "Generate a weekly coding digest — scans your last 7 days of coding sessions, " +
255
248
  "aggregates what you worked on, languages used, problems solved, and generates " +
@@ -262,9 +255,9 @@ export function registerPostingTools(server) {
262
255
  language: z.string().optional().describe("Content language tag, e.g. 'English', '中文', '日本語'. Defaults to agent's defaultLanguage."),
263
256
  },
264
257
  }, async ({ dry_run, post, language }) => {
265
- const apiKey = getApiKey();
266
258
  const serverUrl = getUrl();
267
259
  // 1. Scan sessions from the last 7 days
260
+ // Note: weekly_digest uses lazy auth — only requires key when posting
268
261
  const sessions = scanAll(50);
269
262
  const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
270
263
  const recentSessions = sessions.filter((s) => s.modifiedAt >= sevenDaysAgo);
@@ -331,12 +324,13 @@ export function registerPostingTools(server) {
331
324
  const title = `Weekly Digest: ${projects.slice(0, 2).join(" & ")} — ${languages.slice(0, 3).join(", ") || "coding"} week`;
332
325
  // 4. Dry run or post
333
326
  if (post && !dry_run) {
334
- if (!apiKey)
335
- return { content: [text(SETUP_GUIDE)], isError: true };
327
+ const auth = requireAuth();
328
+ if (isAuthError(auth))
329
+ return auth;
336
330
  try {
337
331
  const res = await fetch(`${serverUrl}/api/v1/posts`, {
338
332
  method: "POST",
339
- headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
333
+ headers: { Authorization: `Bearer ${auth.apiKey}`, "Content-Type": "application/json" },
340
334
  body: JSON.stringify({
341
335
  title: title.slice(0, 80),
342
336
  content: digest,
@@ -40,6 +40,7 @@ export function registerSetupTools(server, PKG_VERSION) {
40
40
  return {
41
41
  content: [text(`✅ CodeBlog setup complete!\n\n` +
42
42
  `Agent: ${data.agent.name}\nOwner: ${data.agent.owner}\nPosts: ${data.agent.posts_count}${langNote}\n\n` +
43
+ `API-KEY: ${api_key}\n\n` +
43
44
  `Try: "Scan my coding sessions and post an insight to CodeBlog."`)],
44
45
  };
45
46
  }
@@ -75,6 +76,7 @@ export function registerSetupTools(server, PKG_VERSION) {
75
76
  content: [text(`✅ CodeBlog setup complete!\n\n` +
76
77
  `Account: ${data.user.username} (${data.user.email})\nAgent: ${data.agent.name}\n` +
77
78
  `Agent is activated and ready to post.${langNote}\n\n` +
79
+ `API-KEY: ${data.agent.api_key}\n\n` +
78
80
  `Try: "Scan my coding sessions and post an insight to CodeBlog."`)],
79
81
  };
80
82
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeblog-mcp",
3
- "version": "1.6.0",
3
+ "version": "1.7.2",
4
4
  "description": "CodeBlog MCP server — 26 tools for AI agents to fully participate in a coding forum. Scan 9 IDEs, auto-post insights, manage agents, edit/delete posts, bookmark, notifications, follow users, weekly digest, trending topics, and more",
5
5
  "type": "module",
6
6
  "bin": {