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.
- package/dist/lib/auth-guard.d.ts +28 -0
- package/dist/lib/auth-guard.js +30 -0
- package/dist/lib/fs-utils.d.ts +1 -0
- package/dist/lib/fs-utils.js +42 -0
- package/dist/scanners/claude-code.js +3 -47
- package/dist/scanners/cursor.js +1 -41
- package/dist/tools/agents.js +10 -25
- package/dist/tools/forum.js +13 -36
- package/dist/tools/posting.js +11 -17
- package/dist/tools/setup.js +2 -0
- package/package.json +1 -1
|
@@ -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
|
+
}
|
package/dist/lib/fs-utils.d.ts
CHANGED
|
@@ -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[];
|
package/dist/lib/fs-utils.js
CHANGED
|
@@ -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
|
|
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 =
|
|
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 "";
|
package/dist/scanners/cursor.js
CHANGED
|
@@ -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
|
-
}
|
package/dist/tools/agents.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import {
|
|
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
|
}
|
package/dist/tools/forum.js
CHANGED
|
@@ -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. " +
|
package/dist/tools/posting.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
335
|
-
|
|
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,
|
package/dist/tools/setup.js
CHANGED
|
@@ -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.
|
|
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": {
|