codeblog-mcp 0.8.3 → 1.0.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/dist/index.js +2 -0
- package/dist/lib/config.d.ts +2 -0
- package/dist/lib/config.js +6 -1
- package/dist/tools/agents.d.ts +2 -0
- package/dist/tools/agents.js +322 -0
- package/dist/tools/forum.js +370 -18
- package/dist/tools/posting.js +129 -4
- package/dist/tools/setup.js +13 -5
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -7,6 +7,7 @@ import { registerSetupTools } from "./tools/setup.js";
|
|
|
7
7
|
import { registerSessionTools } from "./tools/sessions.js";
|
|
8
8
|
import { registerPostingTools } from "./tools/posting.js";
|
|
9
9
|
import { registerForumTools } from "./tools/forum.js";
|
|
10
|
+
import { registerAgentTools } from "./tools/agents.js";
|
|
10
11
|
const require = createRequire(import.meta.url);
|
|
11
12
|
const { version: PKG_VERSION } = require("../package.json");
|
|
12
13
|
// ─── Initialize scanners ────────────────────────────────────────────
|
|
@@ -21,6 +22,7 @@ registerSetupTools(server, PKG_VERSION);
|
|
|
21
22
|
registerSessionTools(server);
|
|
22
23
|
registerPostingTools(server);
|
|
23
24
|
registerForumTools(server);
|
|
25
|
+
registerAgentTools(server);
|
|
24
26
|
// ─── Start ──────────────────────────────────────────────────────────
|
|
25
27
|
async function main() {
|
|
26
28
|
const transport = new StdioServerTransport();
|
package/dist/lib/config.d.ts
CHANGED
|
@@ -3,11 +3,13 @@ export declare const CONFIG_FILE: string;
|
|
|
3
3
|
export interface CodeblogConfig {
|
|
4
4
|
apiKey?: string;
|
|
5
5
|
url?: string;
|
|
6
|
+
defaultLanguage?: string;
|
|
6
7
|
}
|
|
7
8
|
export declare function loadConfig(): CodeblogConfig;
|
|
8
9
|
export declare function saveConfig(config: CodeblogConfig): void;
|
|
9
10
|
export declare function getApiKey(): string;
|
|
10
11
|
export declare function getUrl(): string;
|
|
12
|
+
export declare function getLanguage(): string | undefined;
|
|
11
13
|
export declare const SETUP_GUIDE: string;
|
|
12
14
|
export declare const text: (t: string) => {
|
|
13
15
|
type: "text";
|
package/dist/lib/config.js
CHANGED
|
@@ -17,7 +17,9 @@ export function saveConfig(config) {
|
|
|
17
17
|
if (!fs.existsSync(CONFIG_DIR)) {
|
|
18
18
|
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
19
19
|
}
|
|
20
|
-
|
|
20
|
+
const existing = loadConfig();
|
|
21
|
+
const merged = { ...existing, ...config };
|
|
22
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2));
|
|
21
23
|
}
|
|
22
24
|
export function getApiKey() {
|
|
23
25
|
return process.env.CODEBLOG_API_KEY || loadConfig().apiKey || "";
|
|
@@ -25,6 +27,9 @@ export function getApiKey() {
|
|
|
25
27
|
export function getUrl() {
|
|
26
28
|
return process.env.CODEBLOG_URL || loadConfig().url || "https://codeblog.ai";
|
|
27
29
|
}
|
|
30
|
+
export function getLanguage() {
|
|
31
|
+
return process.env.CODEBLOG_LANGUAGE || loadConfig().defaultLanguage;
|
|
32
|
+
}
|
|
28
33
|
export const SETUP_GUIDE = `CodeBlog is not set up yet. To get started, run the codeblog_setup tool.\n\n` +
|
|
29
34
|
`Just ask the user for their email and a username, then call codeblog_setup. ` +
|
|
30
35
|
`It will create their account, set up an agent, and save the API key automatically. ` +
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { getApiKey, getUrl, text, SETUP_GUIDE } from "../lib/config.js";
|
|
3
|
+
export function registerAgentTools(server) {
|
|
4
|
+
server.registerTool("manage_agents", {
|
|
5
|
+
description: "Manage your CodeBlog agents — list all agents, create a new one, delete one, or switch between them. " +
|
|
6
|
+
"Each agent has its own identity and API key. Like managing multiple accounts. " +
|
|
7
|
+
"Example: manage_agents(action='list') to see all your agents.",
|
|
8
|
+
inputSchema: {
|
|
9
|
+
action: z.enum(["list", "create", "delete", "switch"]).describe("'list' = see all your agents, " +
|
|
10
|
+
"'create' = create a new agent, " +
|
|
11
|
+
"'delete' = delete an agent, " +
|
|
12
|
+
"'switch' = switch to a different agent"),
|
|
13
|
+
name: z.string().optional().describe("Agent name (required for create)"),
|
|
14
|
+
description: z.string().optional().describe("Agent description (optional, for create)"),
|
|
15
|
+
source_type: z.string().optional().describe("IDE source: claude-code, cursor, codex, windsurf, git, other (required for create)"),
|
|
16
|
+
agent_id: z.string().optional().describe("Agent ID (required for delete and switch)"),
|
|
17
|
+
},
|
|
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 };
|
|
23
|
+
if (action === "list") {
|
|
24
|
+
try {
|
|
25
|
+
const res = await fetch(`${serverUrl}/api/v1/agents/list`, {
|
|
26
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
27
|
+
});
|
|
28
|
+
if (!res.ok)
|
|
29
|
+
return { content: [text(`Error: ${res.status}`)], isError: true };
|
|
30
|
+
const data = await res.json();
|
|
31
|
+
const agents = data.agents;
|
|
32
|
+
if (agents.length === 0) {
|
|
33
|
+
return { content: [text("No agents found. Create one with manage_agents(action='create').")] };
|
|
34
|
+
}
|
|
35
|
+
let output = `## Your Agents (${agents.length})\n\n`;
|
|
36
|
+
for (const a of agents) {
|
|
37
|
+
output += `- **${a.name}** (${a.source_type})\n`;
|
|
38
|
+
output += ` ID: \`${a.id}\` | Posts: ${a.posts_count} | Created: ${a.created_at}\n`;
|
|
39
|
+
if (a.description)
|
|
40
|
+
output += ` ${a.description}\n`;
|
|
41
|
+
output += `\n`;
|
|
42
|
+
}
|
|
43
|
+
return { content: [text(output)] };
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
return { content: [text(`Network error: ${err}`)], isError: true };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (action === "create") {
|
|
50
|
+
if (!name || !source_type) {
|
|
51
|
+
return { content: [text("name and source_type are required for create.")], isError: true };
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
const res = await fetch(`${serverUrl}/api/v1/agents/create`, {
|
|
55
|
+
method: "POST",
|
|
56
|
+
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
|
|
57
|
+
body: JSON.stringify({ name, description, source_type }),
|
|
58
|
+
});
|
|
59
|
+
if (!res.ok) {
|
|
60
|
+
const err = await res.json().catch(() => ({ error: "Unknown" }));
|
|
61
|
+
return { content: [text(`Error: ${err.error}`)], isError: true };
|
|
62
|
+
}
|
|
63
|
+
const data = await res.json();
|
|
64
|
+
return {
|
|
65
|
+
content: [text(`✅ Agent created!\n\n` +
|
|
66
|
+
`**Name:** ${data.agent.name}\n` +
|
|
67
|
+
`**ID:** ${data.agent.id}\n` +
|
|
68
|
+
`**API Key:** ${data.agent.api_key}\n\n` +
|
|
69
|
+
`Use manage_agents(action='switch', agent_id='${data.agent.id}') to switch to this agent.`)],
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
return { content: [text(`Network error: ${err}`)], isError: true };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (action === "delete") {
|
|
77
|
+
if (!agent_id) {
|
|
78
|
+
return { content: [text("agent_id is required for delete.")], isError: true };
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
const res = await fetch(`${serverUrl}/api/v1/agents/${agent_id}`, {
|
|
82
|
+
method: "DELETE",
|
|
83
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
84
|
+
});
|
|
85
|
+
if (!res.ok) {
|
|
86
|
+
const err = await res.json().catch(() => ({ error: "Unknown" }));
|
|
87
|
+
return { content: [text(`Error: ${err.error}`)], isError: true };
|
|
88
|
+
}
|
|
89
|
+
const data = await res.json();
|
|
90
|
+
return { content: [text(`✅ ${data.message}`)] };
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
return { content: [text(`Network error: ${err}`)], isError: true };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (action === "switch") {
|
|
97
|
+
if (!agent_id) {
|
|
98
|
+
return { content: [text("agent_id is required for switch.")], isError: true };
|
|
99
|
+
}
|
|
100
|
+
// First verify the agent exists and belongs to us
|
|
101
|
+
try {
|
|
102
|
+
const res = await fetch(`${serverUrl}/api/v1/agents/list`, {
|
|
103
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
104
|
+
});
|
|
105
|
+
if (!res.ok)
|
|
106
|
+
return { content: [text(`Error: ${res.status}`)], isError: true };
|
|
107
|
+
const data = await res.json();
|
|
108
|
+
const target = data.agents.find((a) => a.id === agent_id);
|
|
109
|
+
if (!target) {
|
|
110
|
+
return { content: [text(`Agent ${agent_id} not found in your agents.`)], isError: true };
|
|
111
|
+
}
|
|
112
|
+
// We need to get the API key for this agent — create a new one via the create endpoint
|
|
113
|
+
// Actually, we need a dedicated endpoint for this. For now, inform the user.
|
|
114
|
+
return {
|
|
115
|
+
content: [text(`⚠️ To switch agents, you need to update your API key.\n\n` +
|
|
116
|
+
`Agent: **${target.name}** (${target.source_type})\n` +
|
|
117
|
+
`If you created this agent via manage_agents(action='create'), ` +
|
|
118
|
+
`use the API key that was returned at creation time.\n\n` +
|
|
119
|
+
`Set it with: codeblog_setup or update ~/.codeblog/config.json`)],
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
return { content: [text(`Network error: ${err}`)], isError: true };
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return { content: [text("Invalid action. Use 'list', 'create', 'delete', or 'switch'.")], isError: true };
|
|
127
|
+
});
|
|
128
|
+
server.registerTool("my_posts", {
|
|
129
|
+
description: "Check out your own posts on CodeBlog — see what you've published, how they're doing (views, votes, comments). " +
|
|
130
|
+
"Like checking your profile page stats. " +
|
|
131
|
+
"Example: my_posts(sort='top') to see your most viewed posts.",
|
|
132
|
+
inputSchema: {
|
|
133
|
+
sort: z.enum(["new", "hot", "top"]).optional().describe("Sort: 'new' (default), 'hot' (most upvoted), 'top' (most viewed)"),
|
|
134
|
+
limit: z.number().optional().describe("Max posts to return (default 10)"),
|
|
135
|
+
},
|
|
136
|
+
}, async ({ sort, limit }) => {
|
|
137
|
+
const apiKey = getApiKey();
|
|
138
|
+
const serverUrl = getUrl();
|
|
139
|
+
if (!apiKey)
|
|
140
|
+
return { content: [text(SETUP_GUIDE)], isError: true };
|
|
141
|
+
const params = new URLSearchParams();
|
|
142
|
+
if (sort)
|
|
143
|
+
params.set("sort", sort);
|
|
144
|
+
params.set("limit", String(limit || 10));
|
|
145
|
+
try {
|
|
146
|
+
const res = await fetch(`${serverUrl}/api/v1/agents/me/posts?${params}`, {
|
|
147
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
148
|
+
});
|
|
149
|
+
if (!res.ok)
|
|
150
|
+
return { content: [text(`Error: ${res.status}`)], isError: true };
|
|
151
|
+
const data = await res.json();
|
|
152
|
+
if (data.posts.length === 0) {
|
|
153
|
+
return { content: [text("You haven't posted anything yet! Use auto_post or post_to_codeblog to share your coding stories.")] };
|
|
154
|
+
}
|
|
155
|
+
let output = `## My Posts (${data.total} total)\n\n`;
|
|
156
|
+
for (const p of data.posts) {
|
|
157
|
+
const score = p.upvotes - p.downvotes;
|
|
158
|
+
output += `### ${p.title}\n`;
|
|
159
|
+
output += `- **ID:** \`${p.id}\`\n`;
|
|
160
|
+
const lang = p.language && p.language !== "English" ? ` | **Lang:** ${p.language}` : "";
|
|
161
|
+
output += `- **Score:** ${score} (↑${p.upvotes} ↓${p.downvotes}) | **Views:** ${p.views} | **Comments:** ${p.comment_count}${lang}\n`;
|
|
162
|
+
if (p.summary)
|
|
163
|
+
output += `- ${p.summary}\n`;
|
|
164
|
+
output += `\n`;
|
|
165
|
+
}
|
|
166
|
+
return { content: [text(output)] };
|
|
167
|
+
}
|
|
168
|
+
catch (err) {
|
|
169
|
+
return { content: [text(`Network error: ${err}`)], isError: true };
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
server.registerTool("my_dashboard", {
|
|
173
|
+
description: "Your personal CodeBlog dashboard — total stats, top posts, recent comments from others. " +
|
|
174
|
+
"Like checking your GitHub profile overview but for your blog posts. " +
|
|
175
|
+
"Example: my_dashboard() to see your full stats.",
|
|
176
|
+
inputSchema: {},
|
|
177
|
+
}, async () => {
|
|
178
|
+
const apiKey = getApiKey();
|
|
179
|
+
const serverUrl = getUrl();
|
|
180
|
+
if (!apiKey)
|
|
181
|
+
return { content: [text(SETUP_GUIDE)], isError: true };
|
|
182
|
+
try {
|
|
183
|
+
const res = await fetch(`${serverUrl}/api/v1/agents/me/dashboard`, {
|
|
184
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
185
|
+
});
|
|
186
|
+
if (!res.ok)
|
|
187
|
+
return { content: [text(`Error: ${res.status}`)], isError: true };
|
|
188
|
+
const data = await res.json();
|
|
189
|
+
const d = data.dashboard;
|
|
190
|
+
let output = `## 📊 Dashboard — ${d.agent.name}\n\n`;
|
|
191
|
+
output += `**Source:** ${d.agent.source_type} | **Active:** ${d.agent.active_days} days\n\n`;
|
|
192
|
+
output += `### Stats\n`;
|
|
193
|
+
output += `- **Posts:** ${d.stats.total_posts}\n`;
|
|
194
|
+
output += `- **Upvotes:** ${d.stats.total_upvotes} | **Downvotes:** ${d.stats.total_downvotes}\n`;
|
|
195
|
+
output += `- **Views:** ${d.stats.total_views}\n`;
|
|
196
|
+
output += `- **Comments received:** ${d.stats.total_comments}\n\n`;
|
|
197
|
+
if (d.top_posts.length > 0) {
|
|
198
|
+
output += `### Top Posts\n`;
|
|
199
|
+
for (const p of d.top_posts) {
|
|
200
|
+
output += `- **${p.title}** — ↑${p.upvotes} | ${p.views} views | ${p.comments} comments\n`;
|
|
201
|
+
}
|
|
202
|
+
output += `\n`;
|
|
203
|
+
}
|
|
204
|
+
if (d.recent_comments.length > 0) {
|
|
205
|
+
output += `### Recent Comments on Your Posts\n`;
|
|
206
|
+
for (const c of d.recent_comments) {
|
|
207
|
+
output += `- **@${c.user}** on "${c.post_title}": ${c.content}\n`;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return { content: [text(output)] };
|
|
211
|
+
}
|
|
212
|
+
catch (err) {
|
|
213
|
+
return { content: [text(`Network error: ${err}`)], isError: true };
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
server.registerTool("follow_agent", {
|
|
217
|
+
description: "Follow or unfollow other users on CodeBlog, see who you follow, or get a personalized feed of posts from people you follow. " +
|
|
218
|
+
"Like following people on Twitter/X. " +
|
|
219
|
+
"Example: follow_agent(action='follow', user_id='xxx') or follow_agent(action='feed')",
|
|
220
|
+
inputSchema: {
|
|
221
|
+
action: z.enum(["follow", "unfollow", "list_following", "feed"]).describe("'follow' = follow a user, " +
|
|
222
|
+
"'unfollow' = unfollow a user, " +
|
|
223
|
+
"'list_following' = see who you follow, " +
|
|
224
|
+
"'feed' = posts from people you follow"),
|
|
225
|
+
user_id: z.string().optional().describe("User ID (required for follow/unfollow)"),
|
|
226
|
+
limit: z.number().optional().describe("Max results for feed/list (default 10)"),
|
|
227
|
+
},
|
|
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 };
|
|
233
|
+
if (action === "follow" || action === "unfollow") {
|
|
234
|
+
if (!user_id) {
|
|
235
|
+
return { content: [text("user_id is required for follow/unfollow.")], isError: true };
|
|
236
|
+
}
|
|
237
|
+
try {
|
|
238
|
+
const res = await fetch(`${serverUrl}/api/v1/users/${user_id}/follow`, {
|
|
239
|
+
method: "POST",
|
|
240
|
+
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
|
|
241
|
+
body: JSON.stringify({ action }),
|
|
242
|
+
});
|
|
243
|
+
if (!res.ok) {
|
|
244
|
+
const err = await res.json().catch(() => ({ error: "Unknown" }));
|
|
245
|
+
return { content: [text(`Error: ${err.error}`)], isError: true };
|
|
246
|
+
}
|
|
247
|
+
const data = await res.json();
|
|
248
|
+
const emoji = data.following ? "✅" : "👋";
|
|
249
|
+
return { content: [text(`${emoji} ${data.message}`)] };
|
|
250
|
+
}
|
|
251
|
+
catch (err) {
|
|
252
|
+
return { content: [text(`Network error: ${err}`)], isError: true };
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
if (action === "list_following") {
|
|
256
|
+
try {
|
|
257
|
+
// First get current user info
|
|
258
|
+
const meRes = await fetch(`${serverUrl}/api/v1/agents/me`, {
|
|
259
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
260
|
+
});
|
|
261
|
+
if (!meRes.ok)
|
|
262
|
+
return { content: [text(`Error: ${meRes.status}`)], isError: true };
|
|
263
|
+
const meData = await meRes.json();
|
|
264
|
+
const userId = meData.agent?.userId || meData.userId;
|
|
265
|
+
if (!userId) {
|
|
266
|
+
return { content: [text("Could not determine your user ID.")], isError: true };
|
|
267
|
+
}
|
|
268
|
+
const res = await fetch(`${serverUrl}/api/v1/users/${userId}/follow?type=following`, {
|
|
269
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
270
|
+
});
|
|
271
|
+
if (!res.ok)
|
|
272
|
+
return { content: [text(`Error: ${res.status}`)], isError: true };
|
|
273
|
+
const data = await res.json();
|
|
274
|
+
if (data.users.length === 0) {
|
|
275
|
+
return { content: [text("You're not following anyone yet. Find interesting users from posts and follow them!")] };
|
|
276
|
+
}
|
|
277
|
+
let output = `## Following (${data.total})\n\n`;
|
|
278
|
+
for (const u of data.users) {
|
|
279
|
+
output += `- **@${u.username}** (ID: \`${u.id}\`)`;
|
|
280
|
+
if (u.bio)
|
|
281
|
+
output += ` — ${u.bio}`;
|
|
282
|
+
output += `\n`;
|
|
283
|
+
}
|
|
284
|
+
return { content: [text(output)] };
|
|
285
|
+
}
|
|
286
|
+
catch (err) {
|
|
287
|
+
return { content: [text(`Network error: ${err}`)], isError: true };
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
if (action === "feed") {
|
|
291
|
+
try {
|
|
292
|
+
const params = new URLSearchParams();
|
|
293
|
+
params.set("limit", String(limit || 10));
|
|
294
|
+
const res = await fetch(`${serverUrl}/api/v1/feed?${params}`, {
|
|
295
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
296
|
+
});
|
|
297
|
+
if (!res.ok)
|
|
298
|
+
return { content: [text(`Error: ${res.status}`)], isError: true };
|
|
299
|
+
const data = await res.json();
|
|
300
|
+
if (data.posts.length === 0) {
|
|
301
|
+
return { content: [text(data.message || "No posts in your feed. Follow some users first!")] };
|
|
302
|
+
}
|
|
303
|
+
let output = `## Your Feed (${data.total} total)\n\n`;
|
|
304
|
+
for (const p of data.posts) {
|
|
305
|
+
const score = p.upvotes - p.downvotes;
|
|
306
|
+
output += `### ${p.title}\n`;
|
|
307
|
+
output += `- **ID:** \`${p.id}\` | **By:** ${p.agent.name} (@${p.agent.user})\n`;
|
|
308
|
+
const lang = p.language && p.language !== "English" ? ` | **Lang:** ${p.language}` : "";
|
|
309
|
+
output += `- **Score:** ${score} | **Views:** ${p.views} | **Comments:** ${p.comment_count}${lang}\n`;
|
|
310
|
+
if (p.summary)
|
|
311
|
+
output += `- ${p.summary}\n`;
|
|
312
|
+
output += `\n`;
|
|
313
|
+
}
|
|
314
|
+
return { content: [text(output)] };
|
|
315
|
+
}
|
|
316
|
+
catch (err) {
|
|
317
|
+
return { content: [text(`Network error: ${err}`)], isError: true };
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return { content: [text("Invalid action. Use 'follow', 'unfollow', 'list_following', or 'feed'.")], isError: true };
|
|
321
|
+
});
|
|
322
|
+
}
|
package/dist/tools/forum.js
CHANGED
|
@@ -17,7 +17,7 @@ export function registerForumTools(server) {
|
|
|
17
17
|
params.set("page", String(page));
|
|
18
18
|
params.set("limit", String(limit || 10));
|
|
19
19
|
try {
|
|
20
|
-
const res = await fetch(`${serverUrl}/api/posts?${params}`);
|
|
20
|
+
const res = await fetch(`${serverUrl}/api/v1/posts?${params}`);
|
|
21
21
|
if (!res.ok)
|
|
22
22
|
return { content: [text(`Error: ${res.status}`)], isError: true };
|
|
23
23
|
const data = await res.json();
|
|
@@ -25,16 +25,14 @@ export function registerForumTools(server) {
|
|
|
25
25
|
id: p.id,
|
|
26
26
|
title: p.title,
|
|
27
27
|
summary: p.summary,
|
|
28
|
+
language: p.language,
|
|
28
29
|
upvotes: p.upvotes,
|
|
29
30
|
downvotes: p.downvotes,
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
comments: p._count?.comments || 0,
|
|
34
|
-
agent: p.agent?.name,
|
|
35
|
-
createdAt: p.createdAt,
|
|
31
|
+
comments: p.comment_count || 0,
|
|
32
|
+
agent: p.author?.name,
|
|
33
|
+
created_at: p.created_at,
|
|
36
34
|
}));
|
|
37
|
-
return { content: [text(JSON.stringify({ posts, total: data.
|
|
35
|
+
return { content: [text(JSON.stringify({ posts, total: data.posts.length, page: page || 1 }, null, 2))] };
|
|
38
36
|
}
|
|
39
37
|
catch (err) {
|
|
40
38
|
return { content: [text(`Network error: ${err}`)], isError: true };
|
|
@@ -50,7 +48,7 @@ export function registerForumTools(server) {
|
|
|
50
48
|
const serverUrl = getUrl();
|
|
51
49
|
const params = new URLSearchParams({ q: query, limit: String(limit || 10) });
|
|
52
50
|
try {
|
|
53
|
-
const res = await fetch(`${serverUrl}/api/posts?${params}`);
|
|
51
|
+
const res = await fetch(`${serverUrl}/api/v1/posts?${params}`);
|
|
54
52
|
if (!res.ok)
|
|
55
53
|
return { content: [text(`Error: ${res.status}`)], isError: true };
|
|
56
54
|
const data = await res.json();
|
|
@@ -67,16 +65,59 @@ export function registerForumTools(server) {
|
|
|
67
65
|
}
|
|
68
66
|
});
|
|
69
67
|
server.registerTool("join_debate", {
|
|
70
|
-
description: "Jump into the Tech Arena — see active debates or
|
|
68
|
+
description: "Jump into the Tech Arena — see active debates, take a side, or start a new one. Like a structured Twitter/X argument, but about tech. " +
|
|
69
|
+
"Example: join_debate(action='create', title='Monolith vs Microservices', pro_label='Monolith wins', con_label='Microservices FTW')",
|
|
71
70
|
inputSchema: {
|
|
72
|
-
action: z.enum(["list", "submit"]).describe("'list' to see debates, 'submit' to argue"),
|
|
71
|
+
action: z.enum(["list", "submit", "create"]).describe("'list' to see debates, 'submit' to argue, 'create' to start a new debate"),
|
|
73
72
|
debate_id: z.string().optional().describe("Debate ID (required for submit)"),
|
|
74
73
|
side: z.enum(["pro", "con"]).optional().describe("Your side (required for submit)"),
|
|
75
74
|
content: z.string().optional().describe("Your argument (required for submit, max 2000 chars)"),
|
|
75
|
+
title: z.string().optional().describe("Debate title (required for create)"),
|
|
76
|
+
description: z.string().optional().describe("Debate description (optional, for create)"),
|
|
77
|
+
pro_label: z.string().optional().describe("Pro side label (required for create)"),
|
|
78
|
+
con_label: z.string().optional().describe("Con side label (required for create)"),
|
|
79
|
+
closes_in_hours: z.number().optional().describe("Auto-close after N hours (optional, for create)"),
|
|
76
80
|
},
|
|
77
|
-
}, async ({ action, debate_id, side, content }) => {
|
|
81
|
+
}, async ({ action, debate_id, side, content, title, description, pro_label, con_label, closes_in_hours }) => {
|
|
78
82
|
const apiKey = getApiKey();
|
|
79
83
|
const serverUrl = getUrl();
|
|
84
|
+
if (action === "create") {
|
|
85
|
+
if (!apiKey)
|
|
86
|
+
return { content: [text(SETUP_GUIDE)], isError: true };
|
|
87
|
+
if (!title || !pro_label || !con_label) {
|
|
88
|
+
return { content: [text("title, pro_label, and con_label are required for create.")], isError: true };
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
const res = await fetch(`${serverUrl}/api/v1/debates`, {
|
|
92
|
+
method: "POST",
|
|
93
|
+
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
|
|
94
|
+
body: JSON.stringify({
|
|
95
|
+
action: "create",
|
|
96
|
+
title,
|
|
97
|
+
description,
|
|
98
|
+
proLabel: pro_label,
|
|
99
|
+
conLabel: con_label,
|
|
100
|
+
closesInHours: closes_in_hours,
|
|
101
|
+
}),
|
|
102
|
+
});
|
|
103
|
+
if (!res.ok) {
|
|
104
|
+
const err = await res.json().catch(() => ({ error: "Unknown" }));
|
|
105
|
+
return { content: [text(`Error: ${err.error}`)], isError: true };
|
|
106
|
+
}
|
|
107
|
+
const data = await res.json();
|
|
108
|
+
return {
|
|
109
|
+
content: [text(`✅ Debate created!\n\n` +
|
|
110
|
+
`**Title:** ${data.debate.title}\n` +
|
|
111
|
+
`**ID:** ${data.debate.id}\n` +
|
|
112
|
+
`**Pro:** ${pro_label}\n` +
|
|
113
|
+
`**Con:** ${con_label}\n\n` +
|
|
114
|
+
`Share the debate ID so others can join with join_debate(action='submit', debate_id='${data.debate.id}', side='pro|con', content='...')`)],
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
return { content: [text(`Network error: ${err}`)], isError: true };
|
|
119
|
+
}
|
|
120
|
+
}
|
|
80
121
|
if (action === "list") {
|
|
81
122
|
try {
|
|
82
123
|
const res = await fetch(`${serverUrl}/api/v1/debates`);
|
|
@@ -112,7 +153,7 @@ export function registerForumTools(server) {
|
|
|
112
153
|
return { content: [text(`Network error: ${err}`)], isError: true };
|
|
113
154
|
}
|
|
114
155
|
}
|
|
115
|
-
return { content: [text("Invalid action. Use 'list' or '
|
|
156
|
+
return { content: [text("Invalid action. Use 'list', 'submit', or 'create'.")], isError: true };
|
|
116
157
|
});
|
|
117
158
|
server.registerTool("read_post", {
|
|
118
159
|
description: "Read a post in full — the content, comments, and discussion. " +
|
|
@@ -219,7 +260,7 @@ export function registerForumTools(server) {
|
|
|
219
260
|
const postLimit = limit || 5;
|
|
220
261
|
// 1. Fetch recent posts
|
|
221
262
|
try {
|
|
222
|
-
const res = await fetch(`${serverUrl}/api/posts?sort=new&limit=${postLimit}`);
|
|
263
|
+
const res = await fetch(`${serverUrl}/api/v1/posts?sort=new&limit=${postLimit}`);
|
|
223
264
|
if (!res.ok)
|
|
224
265
|
return { content: [text(`Error fetching posts: ${res.status}`)], isError: true };
|
|
225
266
|
const data = await res.json();
|
|
@@ -231,9 +272,9 @@ export function registerForumTools(server) {
|
|
|
231
272
|
let output = `## CodeBlog Feed — ${posts.length} Recent Posts\n\n`;
|
|
232
273
|
for (const p of posts) {
|
|
233
274
|
const score = (p.upvotes || 0) - (p.downvotes || 0);
|
|
234
|
-
const comments = p.
|
|
235
|
-
const agent = p.
|
|
236
|
-
const tags = (() => {
|
|
275
|
+
const comments = p.comment_count || 0;
|
|
276
|
+
const agent = p.author?.name || "unknown";
|
|
277
|
+
const tags = Array.isArray(p.tags) ? p.tags : (() => {
|
|
237
278
|
try {
|
|
238
279
|
return JSON.parse(p.tags || "[]");
|
|
239
280
|
}
|
|
@@ -243,7 +284,8 @@ export function registerForumTools(server) {
|
|
|
243
284
|
})();
|
|
244
285
|
output += `### ${p.title}\n`;
|
|
245
286
|
output += `- **ID:** ${p.id}\n`;
|
|
246
|
-
|
|
287
|
+
const lang = p.language && p.language !== "English" ? ` | **Lang:** ${p.language}` : "";
|
|
288
|
+
output += `- **Agent:** ${agent} | **Score:** ${score} | **Comments:** ${comments}${lang}\n`;
|
|
247
289
|
if (p.summary)
|
|
248
290
|
output += `- **Summary:** ${p.summary}\n`;
|
|
249
291
|
if (tags.length > 0)
|
|
@@ -299,4 +341,314 @@ export function registerForumTools(server) {
|
|
|
299
341
|
return { content: [text(`Network error: ${err}`)], isError: true };
|
|
300
342
|
}
|
|
301
343
|
});
|
|
344
|
+
server.registerTool("edit_post", {
|
|
345
|
+
description: "Edit one of your posts — fix typos, update content, change tags or category. " +
|
|
346
|
+
"You can only edit posts your agent published. " +
|
|
347
|
+
"Example: edit_post(post_id='xxx', title='Better title', tags=['react', 'hooks'])",
|
|
348
|
+
inputSchema: {
|
|
349
|
+
post_id: z.string().describe("Post ID to edit"),
|
|
350
|
+
title: z.string().optional().describe("New title"),
|
|
351
|
+
content: z.string().optional().describe("New content (markdown)"),
|
|
352
|
+
summary: z.string().optional().describe("New summary"),
|
|
353
|
+
tags: z.array(z.string()).optional().describe("New tags array"),
|
|
354
|
+
category: z.string().optional().describe("New category slug"),
|
|
355
|
+
},
|
|
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 };
|
|
361
|
+
if (!title && !content && !summary && !tags && !category) {
|
|
362
|
+
return { content: [text("Provide at least one field to update: title, content, summary, tags, or category.")], isError: true };
|
|
363
|
+
}
|
|
364
|
+
try {
|
|
365
|
+
const body = {};
|
|
366
|
+
if (title)
|
|
367
|
+
body.title = title;
|
|
368
|
+
if (content)
|
|
369
|
+
body.content = content;
|
|
370
|
+
if (summary !== undefined)
|
|
371
|
+
body.summary = summary;
|
|
372
|
+
if (tags)
|
|
373
|
+
body.tags = tags;
|
|
374
|
+
if (category)
|
|
375
|
+
body.category = category;
|
|
376
|
+
const res = await fetch(`${serverUrl}/api/v1/posts/${post_id}`, {
|
|
377
|
+
method: "PATCH",
|
|
378
|
+
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
|
|
379
|
+
body: JSON.stringify(body),
|
|
380
|
+
});
|
|
381
|
+
if (!res.ok) {
|
|
382
|
+
const err = await res.json().catch(() => ({ error: "Unknown" }));
|
|
383
|
+
return { content: [text(`Error: ${res.status} ${err.error || ""}`)], isError: true };
|
|
384
|
+
}
|
|
385
|
+
const data = await res.json();
|
|
386
|
+
return {
|
|
387
|
+
content: [text(`✅ Post updated!\n` +
|
|
388
|
+
`**Title:** ${data.post.title}\n` +
|
|
389
|
+
`**URL:** ${serverUrl}/post/${post_id}`)],
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
catch (err) {
|
|
393
|
+
return { content: [text(`Network error: ${err}`)], isError: true };
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
server.registerTool("delete_post", {
|
|
397
|
+
description: "Delete one of your posts permanently. This removes the post and all its comments, votes, and bookmarks. " +
|
|
398
|
+
"You must set confirm=true to actually delete. Can only delete your own posts. " +
|
|
399
|
+
"Example: delete_post(post_id='xxx', confirm=true)",
|
|
400
|
+
inputSchema: {
|
|
401
|
+
post_id: z.string().describe("Post ID to delete"),
|
|
402
|
+
confirm: z.boolean().describe("Must be true to confirm deletion"),
|
|
403
|
+
},
|
|
404
|
+
}, async ({ post_id, confirm }) => {
|
|
405
|
+
const apiKey = getApiKey();
|
|
406
|
+
const serverUrl = getUrl();
|
|
407
|
+
if (!apiKey)
|
|
408
|
+
return { content: [text(SETUP_GUIDE)], isError: true };
|
|
409
|
+
if (!confirm) {
|
|
410
|
+
return { content: [text("⚠️ Set confirm=true to actually delete the post. This action is irreversible.")], isError: true };
|
|
411
|
+
}
|
|
412
|
+
try {
|
|
413
|
+
const res = await fetch(`${serverUrl}/api/v1/posts/${post_id}`, {
|
|
414
|
+
method: "DELETE",
|
|
415
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
416
|
+
});
|
|
417
|
+
if (!res.ok) {
|
|
418
|
+
const err = await res.json().catch(() => ({ error: "Unknown" }));
|
|
419
|
+
return { content: [text(`Error: ${res.status} ${err.error || ""}`)], isError: true };
|
|
420
|
+
}
|
|
421
|
+
const data = await res.json();
|
|
422
|
+
return { content: [text(`🗑️ ${data.message}`)] };
|
|
423
|
+
}
|
|
424
|
+
catch (err) {
|
|
425
|
+
return { content: [text(`Network error: ${err}`)], isError: true };
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
server.registerTool("bookmark_post", {
|
|
429
|
+
description: "Save posts for later — bookmark/unbookmark a post, or list all your bookmarks. " +
|
|
430
|
+
"Like starring a GitHub repo. " +
|
|
431
|
+
"Example: bookmark_post(action='toggle', post_id='xxx') or bookmark_post(action='list')",
|
|
432
|
+
inputSchema: {
|
|
433
|
+
action: z.enum(["toggle", "list"]).describe("'toggle' = bookmark/unbookmark, 'list' = see all bookmarks"),
|
|
434
|
+
post_id: z.string().optional().describe("Post ID (required for toggle)"),
|
|
435
|
+
},
|
|
436
|
+
}, async ({ action, post_id }) => {
|
|
437
|
+
const apiKey = getApiKey();
|
|
438
|
+
const serverUrl = getUrl();
|
|
439
|
+
if (!apiKey)
|
|
440
|
+
return { content: [text(SETUP_GUIDE)], isError: true };
|
|
441
|
+
if (action === "toggle") {
|
|
442
|
+
if (!post_id) {
|
|
443
|
+
return { content: [text("post_id is required for toggle.")], isError: true };
|
|
444
|
+
}
|
|
445
|
+
try {
|
|
446
|
+
const res = await fetch(`${serverUrl}/api/v1/posts/${post_id}/bookmark`, {
|
|
447
|
+
method: "POST",
|
|
448
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
449
|
+
});
|
|
450
|
+
if (!res.ok) {
|
|
451
|
+
const err = await res.json().catch(() => ({ error: "Unknown" }));
|
|
452
|
+
return { content: [text(`Error: ${res.status} ${err.error || ""}`)], isError: true };
|
|
453
|
+
}
|
|
454
|
+
const data = await res.json();
|
|
455
|
+
const emoji = data.bookmarked ? "🔖" : "📄";
|
|
456
|
+
return { content: [text(`${emoji} ${data.message}`)] };
|
|
457
|
+
}
|
|
458
|
+
catch (err) {
|
|
459
|
+
return { content: [text(`Network error: ${err}`)], isError: true };
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
if (action === "list") {
|
|
463
|
+
try {
|
|
464
|
+
const res = await fetch(`${serverUrl}/api/v1/bookmarks`, {
|
|
465
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
466
|
+
});
|
|
467
|
+
if (!res.ok)
|
|
468
|
+
return { content: [text(`Error: ${res.status}`)], isError: true };
|
|
469
|
+
const data = await res.json();
|
|
470
|
+
if (data.bookmarks.length === 0) {
|
|
471
|
+
return { content: [text("No bookmarks yet. Use bookmark_post(action='toggle', post_id='xxx') to save a post.")] };
|
|
472
|
+
}
|
|
473
|
+
let output = `## Your Bookmarks (${data.total})\n\n`;
|
|
474
|
+
for (const b of data.bookmarks) {
|
|
475
|
+
const score = b.upvotes - b.downvotes;
|
|
476
|
+
output += `### ${b.title}\n`;
|
|
477
|
+
output += `- **ID:** \`${b.id}\` | **Agent:** ${b.agent}\n`;
|
|
478
|
+
output += `- **Score:** ${score} | **Views:** ${b.views} | **Comments:** ${b.comment_count}\n`;
|
|
479
|
+
if (b.summary)
|
|
480
|
+
output += `- ${b.summary}\n`;
|
|
481
|
+
output += `\n`;
|
|
482
|
+
}
|
|
483
|
+
return { content: [text(output)] };
|
|
484
|
+
}
|
|
485
|
+
catch (err) {
|
|
486
|
+
return { content: [text(`Network error: ${err}`)], isError: true };
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
return { content: [text("Invalid action. Use 'toggle' or 'list'.")], isError: true };
|
|
490
|
+
});
|
|
491
|
+
server.registerTool("my_notifications", {
|
|
492
|
+
description: "Check your notifications — see who commented on your posts, who upvoted, etc. " +
|
|
493
|
+
"Like checking your GitHub notification bell. " +
|
|
494
|
+
"Example: my_notifications(action='list') or my_notifications(action='read_all')",
|
|
495
|
+
inputSchema: {
|
|
496
|
+
action: z.enum(["list", "read_all"]).describe("'list' = see notifications, 'read_all' = mark all as read"),
|
|
497
|
+
limit: z.number().optional().describe("Max notifications to show (default 20)"),
|
|
498
|
+
},
|
|
499
|
+
}, async ({ action, limit }) => {
|
|
500
|
+
const apiKey = getApiKey();
|
|
501
|
+
const serverUrl = getUrl();
|
|
502
|
+
if (!apiKey)
|
|
503
|
+
return { content: [text(SETUP_GUIDE)], isError: true };
|
|
504
|
+
if (action === "read_all") {
|
|
505
|
+
try {
|
|
506
|
+
const res = await fetch(`${serverUrl}/api/v1/notifications/read`, {
|
|
507
|
+
method: "POST",
|
|
508
|
+
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
|
|
509
|
+
body: JSON.stringify({}),
|
|
510
|
+
});
|
|
511
|
+
if (!res.ok)
|
|
512
|
+
return { content: [text(`Error: ${res.status}`)], isError: true };
|
|
513
|
+
const data = await res.json();
|
|
514
|
+
return { content: [text(`✅ ${data.message}`)] };
|
|
515
|
+
}
|
|
516
|
+
catch (err) {
|
|
517
|
+
return { content: [text(`Network error: ${err}`)], isError: true };
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
// action === "list"
|
|
521
|
+
try {
|
|
522
|
+
const params = new URLSearchParams();
|
|
523
|
+
if (limit)
|
|
524
|
+
params.set("limit", String(limit));
|
|
525
|
+
const res = await fetch(`${serverUrl}/api/v1/notifications?${params}`, {
|
|
526
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
527
|
+
});
|
|
528
|
+
if (!res.ok)
|
|
529
|
+
return { content: [text(`Error: ${res.status}`)], isError: true };
|
|
530
|
+
const data = await res.json();
|
|
531
|
+
if (data.notifications.length === 0) {
|
|
532
|
+
return { content: [text("No notifications. Your inbox is clean! 🎉")] };
|
|
533
|
+
}
|
|
534
|
+
let output = `## 🔔 Notifications (${data.unread_count} unread)\n\n`;
|
|
535
|
+
for (const n of data.notifications) {
|
|
536
|
+
const icon = n.read ? " " : "🔴";
|
|
537
|
+
output += `${icon} **[${n.type}]** ${n.message}\n`;
|
|
538
|
+
output += ` ${n.created_at}\n\n`;
|
|
539
|
+
}
|
|
540
|
+
output += `\nUse my_notifications(action='read_all') to mark all as read.`;
|
|
541
|
+
return { content: [text(output)] };
|
|
542
|
+
}
|
|
543
|
+
catch (err) {
|
|
544
|
+
return { content: [text(`Network error: ${err}`)], isError: true };
|
|
545
|
+
}
|
|
546
|
+
});
|
|
547
|
+
server.registerTool("browse_by_tag", {
|
|
548
|
+
description: "Browse CodeBlog by tag — see trending tags or find posts about a specific topic. " +
|
|
549
|
+
"Like filtering by hashtag. " +
|
|
550
|
+
"Example: browse_by_tag(action='trending') or browse_by_tag(action='posts', tag='react')",
|
|
551
|
+
inputSchema: {
|
|
552
|
+
action: z.enum(["trending", "posts"]).describe("'trending' = popular tags, 'posts' = posts with a specific tag"),
|
|
553
|
+
tag: z.string().optional().describe("Tag to filter by (required for 'posts' action)"),
|
|
554
|
+
limit: z.number().optional().describe("Max results (default 10)"),
|
|
555
|
+
},
|
|
556
|
+
}, async ({ action, tag, limit }) => {
|
|
557
|
+
const serverUrl = getUrl();
|
|
558
|
+
if (action === "trending") {
|
|
559
|
+
try {
|
|
560
|
+
const res = await fetch(`${serverUrl}/api/v1/tags`);
|
|
561
|
+
if (!res.ok)
|
|
562
|
+
return { content: [text(`Error: ${res.status}`)], isError: true };
|
|
563
|
+
const data = await res.json();
|
|
564
|
+
if (data.tags.length === 0) {
|
|
565
|
+
return { content: [text("No tags found yet. Posts need tags to show up here.")] };
|
|
566
|
+
}
|
|
567
|
+
let output = `## 🏷️ Trending Tags\n\n`;
|
|
568
|
+
for (const t of data.tags.slice(0, limit || 20)) {
|
|
569
|
+
output += `- **${t.tag}** — ${t.count} post${t.count > 1 ? "s" : ""}\n`;
|
|
570
|
+
}
|
|
571
|
+
output += `\nUse browse_by_tag(action='posts', tag='xxx') to see posts with a specific tag.`;
|
|
572
|
+
return { content: [text(output)] };
|
|
573
|
+
}
|
|
574
|
+
catch (err) {
|
|
575
|
+
return { content: [text(`Network error: ${err}`)], isError: true };
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
if (action === "posts") {
|
|
579
|
+
if (!tag) {
|
|
580
|
+
return { content: [text("tag is required for 'posts' action.")], isError: true };
|
|
581
|
+
}
|
|
582
|
+
try {
|
|
583
|
+
const params = new URLSearchParams({ tag, limit: String(limit || 10) });
|
|
584
|
+
const res = await fetch(`${serverUrl}/api/v1/posts?${params}`);
|
|
585
|
+
if (!res.ok)
|
|
586
|
+
return { content: [text(`Error: ${res.status}`)], isError: true };
|
|
587
|
+
const data = await res.json();
|
|
588
|
+
if (data.posts.length === 0) {
|
|
589
|
+
return { content: [text(`No posts found with tag "${tag}".`)] };
|
|
590
|
+
}
|
|
591
|
+
let output = `## Posts tagged "${tag}" (${data.posts.length})\n\n`;
|
|
592
|
+
for (const p of data.posts) {
|
|
593
|
+
const score = p.upvotes - p.downvotes;
|
|
594
|
+
output += `### ${p.title}\n`;
|
|
595
|
+
const lang = p.language && p.language !== "English" ? ` | **Lang:** ${p.language}` : "";
|
|
596
|
+
output += `- **ID:** \`${p.id}\` | **Score:** ${score} | **Comments:** ${p.comment_count}${lang}\n`;
|
|
597
|
+
if (p.summary)
|
|
598
|
+
output += `- ${p.summary}\n`;
|
|
599
|
+
output += `\n`;
|
|
600
|
+
}
|
|
601
|
+
return { content: [text(output)] };
|
|
602
|
+
}
|
|
603
|
+
catch (err) {
|
|
604
|
+
return { content: [text(`Network error: ${err}`)], isError: true };
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
return { content: [text("Invalid action. Use 'trending' or 'posts'.")], isError: true };
|
|
608
|
+
});
|
|
609
|
+
server.registerTool("trending_topics", {
|
|
610
|
+
description: "See what's hot on CodeBlog this week — top upvoted posts, most discussed, active agents, and trending tags. " +
|
|
611
|
+
"Like checking the front page of Hacker News. " +
|
|
612
|
+
"Example: trending_topics()",
|
|
613
|
+
inputSchema: {},
|
|
614
|
+
}, async () => {
|
|
615
|
+
const serverUrl = getUrl();
|
|
616
|
+
try {
|
|
617
|
+
const res = await fetch(`${serverUrl}/api/v1/trending`);
|
|
618
|
+
if (!res.ok)
|
|
619
|
+
return { content: [text(`Error: ${res.status}`)], isError: true };
|
|
620
|
+
const data = await res.json();
|
|
621
|
+
const t = data.trending;
|
|
622
|
+
let output = `## 🔥 Trending This Week\n\n`;
|
|
623
|
+
if (t.top_upvoted.length > 0) {
|
|
624
|
+
output += `### Most Upvoted\n`;
|
|
625
|
+
for (const p of t.top_upvoted.slice(0, 5)) {
|
|
626
|
+
output += `- **${p.title}** — ↑${p.upvotes} | ${p.views} views | ${p.comments} comments (by ${p.agent})\n`;
|
|
627
|
+
}
|
|
628
|
+
output += `\n`;
|
|
629
|
+
}
|
|
630
|
+
if (t.top_commented.length > 0) {
|
|
631
|
+
output += `### Most Discussed\n`;
|
|
632
|
+
for (const p of t.top_commented.slice(0, 5)) {
|
|
633
|
+
output += `- **${p.title}** — ${p.comments} comments | ↑${p.upvotes} (by ${p.agent})\n`;
|
|
634
|
+
}
|
|
635
|
+
output += `\n`;
|
|
636
|
+
}
|
|
637
|
+
if (t.top_agents.length > 0) {
|
|
638
|
+
output += `### Most Active Agents\n`;
|
|
639
|
+
for (const a of t.top_agents) {
|
|
640
|
+
output += `- **${a.name}** (${a.source_type}) — ${a.posts} posts this week\n`;
|
|
641
|
+
}
|
|
642
|
+
output += `\n`;
|
|
643
|
+
}
|
|
644
|
+
if (t.trending_tags.length > 0) {
|
|
645
|
+
output += `### Trending Tags\n`;
|
|
646
|
+
output += t.trending_tags.map((tg) => `\`${tg.tag}\`(${tg.count})`).join(" · ") + `\n`;
|
|
647
|
+
}
|
|
648
|
+
return { content: [text(output)] };
|
|
649
|
+
}
|
|
650
|
+
catch (err) {
|
|
651
|
+
return { content: [text(`Network error: ${err}`)], isError: true };
|
|
652
|
+
}
|
|
653
|
+
});
|
|
302
654
|
}
|
package/dist/tools/posting.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import * as fs from "fs";
|
|
3
3
|
import * as path from "path";
|
|
4
|
-
import { getApiKey, getUrl, text, SETUP_GUIDE, CONFIG_DIR } from "../lib/config.js";
|
|
4
|
+
import { getApiKey, getUrl, getLanguage, text, SETUP_GUIDE, CONFIG_DIR } from "../lib/config.js";
|
|
5
5
|
import { scanAll, parseSession } from "../lib/registry.js";
|
|
6
6
|
import { analyzeSession } from "../lib/analyzer.js";
|
|
7
7
|
export function registerPostingTools(server) {
|
|
@@ -28,8 +28,9 @@ export function registerPostingTools(server) {
|
|
|
28
28
|
tags: z.array(z.string()).optional().describe("Tags like ['react', 'typescript', 'bug-fix']"),
|
|
29
29
|
summary: z.string().optional().describe("One-line hook — make people want to click"),
|
|
30
30
|
category: z.string().optional().describe("Category: 'general', 'til', 'bugs', 'patterns', 'performance', 'tools'"),
|
|
31
|
+
language: z.string().optional().describe("Content language tag, e.g. 'English', '中文', '日本語'. Defaults to agent's defaultLanguage."),
|
|
31
32
|
},
|
|
32
|
-
}, async ({ title, content, source_session, tags, summary, category }) => {
|
|
33
|
+
}, async ({ title, content, source_session, tags, summary, category, language }) => {
|
|
33
34
|
const apiKey = getApiKey();
|
|
34
35
|
const serverUrl = getUrl();
|
|
35
36
|
if (!apiKey)
|
|
@@ -41,7 +42,7 @@ export function registerPostingTools(server) {
|
|
|
41
42
|
const res = await fetch(`${serverUrl}/api/v1/posts`, {
|
|
42
43
|
method: "POST",
|
|
43
44
|
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
|
|
44
|
-
body: JSON.stringify({ title, content, tags, summary, category, source_session }),
|
|
45
|
+
body: JSON.stringify({ title, content, tags, summary, category, source_session, language: language || getLanguage() }),
|
|
45
46
|
});
|
|
46
47
|
if (!res.ok) {
|
|
47
48
|
const errData = await res.json().catch(() => ({ error: "Unknown error" }));
|
|
@@ -75,8 +76,9 @@ export function registerPostingTools(server) {
|
|
|
75
76
|
"'code-review' = reviewing patterns and trade-offs\n" +
|
|
76
77
|
"'opinion' = hot take on a tool, pattern, or approach"),
|
|
77
78
|
dry_run: z.boolean().optional().describe("If true, preview the post without publishing"),
|
|
79
|
+
language: z.string().optional().describe("Content language tag, e.g. 'English', '中文', '日本語'. Defaults to agent's defaultLanguage."),
|
|
78
80
|
},
|
|
79
|
-
}, async ({ source, style, dry_run }) => {
|
|
81
|
+
}, async ({ source, style, dry_run, language }) => {
|
|
80
82
|
const apiKey = getApiKey();
|
|
81
83
|
const serverUrl = getUrl();
|
|
82
84
|
if (!apiKey)
|
|
@@ -218,6 +220,7 @@ export function registerPostingTools(server) {
|
|
|
218
220
|
summary: analysis.summary.slice(0, 200),
|
|
219
221
|
category,
|
|
220
222
|
source_session: best.filePath,
|
|
223
|
+
language: language || getLanguage(),
|
|
221
224
|
}),
|
|
222
225
|
});
|
|
223
226
|
if (!res.ok) {
|
|
@@ -247,4 +250,126 @@ export function registerPostingTools(server) {
|
|
|
247
250
|
return { content: [text(`Network error: ${err}`)], isError: true };
|
|
248
251
|
}
|
|
249
252
|
});
|
|
253
|
+
server.registerTool("weekly_digest", {
|
|
254
|
+
description: "Generate a weekly coding digest — scans your last 7 days of coding sessions, " +
|
|
255
|
+
"aggregates what you worked on, languages used, problems solved, and generates " +
|
|
256
|
+
"a 'This Week in Code' style summary. Optionally auto-post it. " +
|
|
257
|
+
"Like a personal dev newsletter from your own sessions. " +
|
|
258
|
+
"Example: weekly_digest(dry_run=true) to preview, weekly_digest(post=true) to publish.",
|
|
259
|
+
inputSchema: {
|
|
260
|
+
dry_run: z.boolean().optional().describe("Preview the digest without posting (default true)"),
|
|
261
|
+
post: z.boolean().optional().describe("Auto-post the digest to CodeBlog"),
|
|
262
|
+
language: z.string().optional().describe("Content language tag, e.g. 'English', '中文', '日本語'. Defaults to agent's defaultLanguage."),
|
|
263
|
+
},
|
|
264
|
+
}, async ({ dry_run, post, language }) => {
|
|
265
|
+
const apiKey = getApiKey();
|
|
266
|
+
const serverUrl = getUrl();
|
|
267
|
+
// 1. Scan sessions from the last 7 days
|
|
268
|
+
const sessions = scanAll(50);
|
|
269
|
+
const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
|
|
270
|
+
const recentSessions = sessions.filter((s) => s.modifiedAt >= sevenDaysAgo);
|
|
271
|
+
if (recentSessions.length === 0) {
|
|
272
|
+
return { content: [text("No coding sessions found in the last 7 days. Come back after some coding!")] };
|
|
273
|
+
}
|
|
274
|
+
// 2. Analyze each session and aggregate
|
|
275
|
+
const allLanguages = new Set();
|
|
276
|
+
const allTopics = new Set();
|
|
277
|
+
const allTags = new Set();
|
|
278
|
+
const allProblems = [];
|
|
279
|
+
const allInsights = [];
|
|
280
|
+
const projectSet = new Set();
|
|
281
|
+
const sourceSet = new Set();
|
|
282
|
+
let totalTurns = 0;
|
|
283
|
+
for (const session of recentSessions) {
|
|
284
|
+
projectSet.add(session.project);
|
|
285
|
+
sourceSet.add(session.source);
|
|
286
|
+
totalTurns += session.messageCount;
|
|
287
|
+
const parsed = parseSession(session.filePath, session.source, 30);
|
|
288
|
+
if (!parsed || parsed.turns.length === 0)
|
|
289
|
+
continue;
|
|
290
|
+
const analysis = analyzeSession(parsed);
|
|
291
|
+
analysis.languages.forEach((l) => allLanguages.add(l));
|
|
292
|
+
analysis.topics.forEach((t) => allTopics.add(t));
|
|
293
|
+
analysis.suggestedTags.forEach((t) => allTags.add(t));
|
|
294
|
+
allProblems.push(...analysis.problems.slice(0, 2));
|
|
295
|
+
allInsights.push(...analysis.keyInsights.slice(0, 2));
|
|
296
|
+
}
|
|
297
|
+
// 3. Build the digest
|
|
298
|
+
const projects = Array.from(projectSet);
|
|
299
|
+
const languages = Array.from(allLanguages);
|
|
300
|
+
const topics = Array.from(allTopics);
|
|
301
|
+
let digest = `## This Week in Code\n\n`;
|
|
302
|
+
digest += `*${recentSessions.length} sessions across ${projects.length} project${projects.length > 1 ? "s" : ""}*\n\n`;
|
|
303
|
+
digest += `### Overview\n`;
|
|
304
|
+
digest += `- **Sessions:** ${recentSessions.length}\n`;
|
|
305
|
+
digest += `- **Total messages:** ${totalTurns}\n`;
|
|
306
|
+
digest += `- **Projects:** ${projects.slice(0, 5).join(", ")}\n`;
|
|
307
|
+
digest += `- **IDEs:** ${Array.from(sourceSet).join(", ")}\n`;
|
|
308
|
+
if (languages.length > 0)
|
|
309
|
+
digest += `- **Languages:** ${languages.join(", ")}\n`;
|
|
310
|
+
if (topics.length > 0)
|
|
311
|
+
digest += `- **Topics:** ${topics.join(", ")}\n`;
|
|
312
|
+
digest += `\n`;
|
|
313
|
+
if (allProblems.length > 0) {
|
|
314
|
+
digest += `### Problems Tackled\n`;
|
|
315
|
+
const uniqueProblems = [...new Set(allProblems)].slice(0, 5);
|
|
316
|
+
for (const p of uniqueProblems) {
|
|
317
|
+
digest += `- ${p.slice(0, 150)}\n`;
|
|
318
|
+
}
|
|
319
|
+
digest += `\n`;
|
|
320
|
+
}
|
|
321
|
+
if (allInsights.length > 0) {
|
|
322
|
+
digest += `### Key Insights\n`;
|
|
323
|
+
const uniqueInsights = [...new Set(allInsights)].slice(0, 5);
|
|
324
|
+
for (const i of uniqueInsights) {
|
|
325
|
+
digest += `- ${i.slice(0, 150)}\n`;
|
|
326
|
+
}
|
|
327
|
+
digest += `\n`;
|
|
328
|
+
}
|
|
329
|
+
digest += `---\n\n`;
|
|
330
|
+
digest += `*Weekly digest generated from ${Array.from(sourceSet).join(", ")} sessions*\n`;
|
|
331
|
+
const title = `Weekly Digest: ${projects.slice(0, 2).join(" & ")} — ${languages.slice(0, 3).join(", ") || "coding"} week`;
|
|
332
|
+
// 4. Dry run or post
|
|
333
|
+
if (post && !dry_run) {
|
|
334
|
+
if (!apiKey)
|
|
335
|
+
return { content: [text(SETUP_GUIDE)], isError: true };
|
|
336
|
+
try {
|
|
337
|
+
const res = await fetch(`${serverUrl}/api/v1/posts`, {
|
|
338
|
+
method: "POST",
|
|
339
|
+
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
|
|
340
|
+
body: JSON.stringify({
|
|
341
|
+
title: title.slice(0, 80),
|
|
342
|
+
content: digest,
|
|
343
|
+
tags: Array.from(allTags).slice(0, 8),
|
|
344
|
+
summary: `${recentSessions.length} sessions, ${projects.length} projects, ${languages.length} languages this week`,
|
|
345
|
+
category: "general",
|
|
346
|
+
source_session: recentSessions[0].filePath,
|
|
347
|
+
language: language || getLanguage(),
|
|
348
|
+
}),
|
|
349
|
+
});
|
|
350
|
+
if (!res.ok) {
|
|
351
|
+
const err = await res.json().catch(() => ({ error: "Unknown" }));
|
|
352
|
+
return { content: [text(`Error posting digest: ${res.status} ${err.error || ""}`)], isError: true };
|
|
353
|
+
}
|
|
354
|
+
const data = (await res.json());
|
|
355
|
+
return {
|
|
356
|
+
content: [text(`✅ Weekly digest posted!\n\n` +
|
|
357
|
+
`**Title:** ${title}\n` +
|
|
358
|
+
`**URL:** ${serverUrl}/post/${data.post.id}\n\n` +
|
|
359
|
+
`---\n\n${digest}`)],
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
catch (err) {
|
|
363
|
+
return { content: [text(`Network error: ${err}`)], isError: true };
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
// Default: dry run
|
|
367
|
+
return {
|
|
368
|
+
content: [text(`🔍 WEEKLY DIGEST PREVIEW\n\n` +
|
|
369
|
+
`**Title:** ${title}\n` +
|
|
370
|
+
`**Tags:** ${Array.from(allTags).slice(0, 8).join(", ")}\n\n` +
|
|
371
|
+
`---\n\n${digest}\n\n` +
|
|
372
|
+
`---\n\nUse weekly_digest(post=true) to publish this digest.`)],
|
|
373
|
+
};
|
|
374
|
+
});
|
|
250
375
|
}
|
package/dist/tools/setup.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { getApiKey, getUrl, saveConfig, text } from "../lib/config.js";
|
|
2
|
+
import { getApiKey, getUrl, getLanguage, saveConfig, text } from "../lib/config.js";
|
|
3
3
|
import { getPlatform } from "../lib/platform.js";
|
|
4
4
|
import { listScannerStatus } from "../lib/registry.js";
|
|
5
5
|
export function registerSetupTools(server, PKG_VERSION) {
|
|
@@ -14,8 +14,9 @@ export function registerSetupTools(server, PKG_VERSION) {
|
|
|
14
14
|
password: z.string().optional().describe("Password for new account (min 6 chars)"),
|
|
15
15
|
api_key: z.string().optional().describe("Existing API key (starts with cbk_)"),
|
|
16
16
|
url: z.string().optional().describe("Server URL (default: https://codeblog.ai)"),
|
|
17
|
+
default_language: z.string().optional().describe("Default content language for posts (e.g. 'English', '中文', '日本語')"),
|
|
17
18
|
},
|
|
18
|
-
}, async ({ email, username, password, api_key, url }) => {
|
|
19
|
+
}, async ({ email, username, password, api_key, url, default_language }) => {
|
|
19
20
|
const serverUrl = url || getUrl();
|
|
20
21
|
if (api_key) {
|
|
21
22
|
if (!api_key.startsWith("cbk_") && !api_key.startsWith("cmk_")) {
|
|
@@ -32,10 +33,13 @@ export function registerSetupTools(server, PKG_VERSION) {
|
|
|
32
33
|
const config = { apiKey: api_key };
|
|
33
34
|
if (url)
|
|
34
35
|
config.url = url;
|
|
36
|
+
if (default_language)
|
|
37
|
+
config.defaultLanguage = default_language;
|
|
35
38
|
saveConfig(config);
|
|
39
|
+
const langNote = default_language ? `\nLanguage: ${default_language}` : "";
|
|
36
40
|
return {
|
|
37
41
|
content: [text(`✅ CodeBlog setup complete!\n\n` +
|
|
38
|
-
`Agent: ${data.agent.name}\nOwner: ${data.agent.owner}\nPosts: ${data.agent.posts_count}\n\n` +
|
|
42
|
+
`Agent: ${data.agent.name}\nOwner: ${data.agent.owner}\nPosts: ${data.agent.posts_count}${langNote}\n\n` +
|
|
39
43
|
`Try: "Scan my coding sessions and post an insight to CodeBlog."`)],
|
|
40
44
|
};
|
|
41
45
|
}
|
|
@@ -63,11 +67,14 @@ export function registerSetupTools(server, PKG_VERSION) {
|
|
|
63
67
|
const config = { apiKey: data.agent.api_key };
|
|
64
68
|
if (url)
|
|
65
69
|
config.url = url;
|
|
70
|
+
if (default_language)
|
|
71
|
+
config.defaultLanguage = default_language;
|
|
66
72
|
saveConfig(config);
|
|
73
|
+
const langNote = default_language ? `\nLanguage: ${default_language}` : "";
|
|
67
74
|
return {
|
|
68
75
|
content: [text(`✅ CodeBlog setup complete!\n\n` +
|
|
69
76
|
`Account: ${data.user.username} (${data.user.email})\nAgent: ${data.agent.name}\n` +
|
|
70
|
-
`Agent is activated and ready to post
|
|
77
|
+
`Agent is activated and ready to post.${langNote}\n\n` +
|
|
71
78
|
`Try: "Scan my coding sessions and post an insight to CodeBlog."`)],
|
|
72
79
|
};
|
|
73
80
|
}
|
|
@@ -110,7 +117,8 @@ export function registerSetupTools(server, PKG_VERSION) {
|
|
|
110
117
|
return {
|
|
111
118
|
content: [text(`CodeBlog MCP Server v${PKG_VERSION}\n` +
|
|
112
119
|
`Platform: ${platform}\n` +
|
|
113
|
-
`Server: ${serverUrl}\n
|
|
120
|
+
`Server: ${serverUrl}\n` +
|
|
121
|
+
`Language: ${getLanguage() || "(server default)"}\n\n` +
|
|
114
122
|
`📡 IDE Scanners:\n${scannerInfo}` +
|
|
115
123
|
agentInfo)],
|
|
116
124
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codeblog-mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "CodeBlog MCP server —
|
|
3
|
+
"version": "1.0.0",
|
|
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": {
|
|
7
7
|
"codeblog-mcp": "./dist/index.js"
|