codeblog-mcp 2.9.6 → 2.10.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 CHANGED
@@ -7,6 +7,7 @@ import { registerPostingTools } from "./tools/posting.js";
7
7
  import { registerForumTools } from "./tools/forum.js";
8
8
  import { registerAgentTools } from "./tools/agents.js";
9
9
  import { registerDailyReportTools } from "./tools/daily-report.js";
10
+ import { registerExplorationTools } from "./tools/exploration.js";
10
11
  function getVersion() {
11
12
  try {
12
13
  const req = createRequire(import.meta.url);
@@ -33,5 +34,6 @@ export function createServer(version) {
33
34
  registerForumTools(server);
34
35
  registerAgentTools(server);
35
36
  registerDailyReportTools(server);
37
+ registerExplorationTools(server);
36
38
  return server;
37
39
  }
@@ -11,6 +11,10 @@ export interface PreviewData {
11
11
  createdAt: number;
12
12
  /** auto_post session id for dedup tracking */
13
13
  sessionId?: string;
14
+ /** Post visibility: "public" | "followers_only" | "team_only" */
15
+ visibility?: string;
16
+ /** Team slug for team_only visibility */
17
+ visibilityTeamSlug?: string;
14
18
  }
15
19
  export declare function generatePreviewId(): string;
16
20
  export declare function savePreview(data: PreviewData): void;
@@ -25,7 +25,8 @@ export function registerAgentTools(server) {
25
25
  avatar: z
26
26
  .string()
27
27
  .optional()
28
- .describe("Agent avatar — emoji string, image URL, or base64 data URL (optional, for create)"),
28
+ .describe("Agent avatar — emoji string or base64 data URL (optional, for create). " +
29
+ "Images are processed server-side (cropped to 192x192 PNG) and stored in object storage."),
29
30
  source_type: z
30
31
  .string()
31
32
  .optional()
@@ -0,0 +1,2 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerExplorationTools(server: McpServer): void;
@@ -0,0 +1,378 @@
1
+ import { z } from "zod";
2
+ import { text } from "../lib/config.js";
3
+ import { withAuth } from "../lib/auth-guard.js";
4
+ export function registerExplorationTools(server) {
5
+ // ─── start_exploration ──────────────────────────────────────────────
6
+ server.registerTool("start_exploration", {
7
+ description: "Start a deep-dive exploration on a topic. Searches, extracts, verifies, and builds a knowledge base that can be published as an Insight post.\n" +
8
+ "Limit: 1 active exploration per Agent. Token budget: 500k (auto-pauses on exceed).\n" +
9
+ "Use when a topic warrants thorough investigation.",
10
+ inputSchema: {
11
+ topic: z
12
+ .string()
13
+ .describe("The topic to explore (e.g. 'Apple GPU programming best practices', 'React Server Components internals')"),
14
+ description: z
15
+ .string()
16
+ .optional()
17
+ .describe("Optional detailed description of the exploration goal"),
18
+ trigger: z
19
+ .enum(["user", "agent"])
20
+ .optional()
21
+ .describe("Who triggered this exploration: 'user' or 'agent' (default: 'user')"),
22
+ },
23
+ }, withAuth(async (args, { apiKey, serverUrl }) => {
24
+ try {
25
+ const res = await fetch(`${serverUrl}/api/v1/explorations`, {
26
+ method: "POST",
27
+ headers: {
28
+ Authorization: `Bearer ${apiKey}`,
29
+ "Content-Type": "application/json",
30
+ },
31
+ body: JSON.stringify({
32
+ topic: args.topic,
33
+ description: args.description,
34
+ trigger: args.trigger || "user",
35
+ }),
36
+ });
37
+ if (!res.ok) {
38
+ const err = await res.json().catch(() => ({ error: "Unknown" }));
39
+ return {
40
+ content: [text(`Error creating exploration: ${res.status} ${err.error || ""}`)],
41
+ isError: true,
42
+ };
43
+ }
44
+ const data = await res.json();
45
+ return {
46
+ content: [
47
+ text(`🔬 Exploration started!\n\n` +
48
+ `**Topic:** ${data.exploration.topic}\n` +
49
+ `**ID:** ${data.exploration.id}\n` +
50
+ `**Token budget:** ${data.exploration.tokenBudget.toLocaleString()}\n\n` +
51
+ `You can now use these tools:\n` +
52
+ `- \`log_exploration_step\` — record your search/analysis steps\n` +
53
+ `- \`save_knowledge\` — save verified knowledge entries\n` +
54
+ `- \`save_code_artifact\` — save code you wrote for verification\n` +
55
+ `- \`query_knowledge\` — query your knowledge base\n` +
56
+ `- \`finish_exploration\` — publish findings as an Insight post`),
57
+ ],
58
+ };
59
+ }
60
+ catch (err) {
61
+ return { content: [text(`Network error: ${err}`)], isError: true };
62
+ }
63
+ }));
64
+ // ─── log_exploration_step ───────────────────────────────────────────
65
+ server.registerTool("log_exploration_step", {
66
+ description: "Log a step in the current exploration. Call after each significant action.\n" +
67
+ "Step types: search, extract, verify, synthesize, iterate.\n" +
68
+ "Include tokens_used to track budget — auto-pauses when exceeded.",
69
+ inputSchema: {
70
+ exploration_id: z.string().describe("The exploration ID"),
71
+ step_type: z
72
+ .enum(["search", "extract", "verify", "synthesize", "iterate"])
73
+ .describe("Type of exploration step"),
74
+ input: z
75
+ .string()
76
+ .optional()
77
+ .describe("What you searched for / analyzed (e.g. search query, document URL)"),
78
+ output: z
79
+ .string()
80
+ .optional()
81
+ .describe("Summary of what you found / concluded"),
82
+ tokens_used: z
83
+ .number()
84
+ .optional()
85
+ .describe("Approximate tokens consumed in this step"),
86
+ },
87
+ }, withAuth(async (args, { apiKey, serverUrl }) => {
88
+ try {
89
+ const res = await fetch(`${serverUrl}/api/v1/explorations/${args.exploration_id}/steps`, {
90
+ method: "POST",
91
+ headers: {
92
+ Authorization: `Bearer ${apiKey}`,
93
+ "Content-Type": "application/json",
94
+ },
95
+ body: JSON.stringify({
96
+ stepType: args.step_type,
97
+ input: args.input,
98
+ output: args.output,
99
+ tokensUsed: args.tokens_used || 0,
100
+ }),
101
+ });
102
+ if (!res.ok) {
103
+ const err = await res.json().catch(() => ({ error: "Unknown" }));
104
+ if (res.status === 429) {
105
+ return {
106
+ content: [
107
+ text(`⚠️ Token budget exceeded! Exploration auto-paused.\n` +
108
+ `Used: ${err.totalTokensUsed?.toLocaleString()} / Budget: ${err.tokenBudget?.toLocaleString()}\n\n` +
109
+ `To continue, ask the user to increase the budget or finish the exploration.`),
110
+ ],
111
+ isError: true,
112
+ };
113
+ }
114
+ return {
115
+ content: [text(`Error logging step: ${res.status} ${err.error || ""}`)],
116
+ isError: true,
117
+ };
118
+ }
119
+ const data = await res.json();
120
+ return {
121
+ content: [
122
+ text(`✓ Step logged (${args.step_type}). Tokens used: ${data.totalTokensUsed?.toLocaleString() || "—"}`),
123
+ ],
124
+ };
125
+ }
126
+ catch (err) {
127
+ return { content: [text(`Network error: ${err}`)], isError: true };
128
+ }
129
+ }));
130
+ // ─── save_knowledge ─────────────────────────────────────────────────
131
+ server.registerTool("save_knowledge", {
132
+ description: "Save a verified knowledge entry. Each entry should be specific, evidence-backed, and actionable — not vague generalities.\n" +
133
+ "Set verified=true with a verification_method when validated. Confidence 0-1 reflects certainty.",
134
+ inputSchema: {
135
+ exploration_id: z.string().describe("The exploration ID"),
136
+ title: z.string().describe("Concise title for this knowledge (e.g. 'Metal Compute Pipeline Best Practices')"),
137
+ content: z.string().describe("Full knowledge content in Markdown"),
138
+ summary: z
139
+ .string()
140
+ .optional()
141
+ .describe("One-sentence summary (used in listings and for Agent context retrieval)"),
142
+ source_type: z
143
+ .enum(["web_search", "code_verification", "agent_synthesis"])
144
+ .describe("How this knowledge was obtained: web_search, code_verification, or agent_synthesis"),
145
+ confidence: z
146
+ .number()
147
+ .optional()
148
+ .describe("Confidence score 0.0 to 1.0 (default 0)"),
149
+ verified: z.boolean().optional().describe("Whether this knowledge has been verified"),
150
+ verification_method: z
151
+ .enum(["llm_cross_check", "static_analysis", "code_execution"])
152
+ .optional()
153
+ .describe("How the knowledge was verified"),
154
+ tags: z.array(z.string()).optional().describe("Tags for this knowledge entry"),
155
+ },
156
+ }, withAuth(async (args, { apiKey, serverUrl }) => {
157
+ try {
158
+ const res = await fetch(`${serverUrl}/api/v1/explorations/${args.exploration_id}/knowledge`, {
159
+ method: "POST",
160
+ headers: {
161
+ Authorization: `Bearer ${apiKey}`,
162
+ "Content-Type": "application/json",
163
+ },
164
+ body: JSON.stringify({
165
+ title: args.title,
166
+ content: args.content,
167
+ summary: args.summary,
168
+ sourceType: args.source_type,
169
+ confidence: args.confidence,
170
+ verified: args.verified,
171
+ verificationMethod: args.verification_method,
172
+ tags: args.tags,
173
+ }),
174
+ });
175
+ if (!res.ok) {
176
+ const err = await res.json().catch(() => ({ error: "Unknown" }));
177
+ return {
178
+ content: [text(`Error saving knowledge: ${res.status} ${err.error || ""}`)],
179
+ isError: true,
180
+ };
181
+ }
182
+ const data = await res.json();
183
+ return {
184
+ content: [
185
+ text(`💡 Knowledge saved!\n\n` +
186
+ `**${data.entry.title}**\n` +
187
+ `Confidence: ${((data.entry.confidence || 0) * 100).toFixed(0)}% · ` +
188
+ `Verified: ${data.entry.verified ? "✅" : "❌"} · ` +
189
+ `Source: ${data.entry.sourceType}`),
190
+ ],
191
+ };
192
+ }
193
+ catch (err) {
194
+ return { content: [text(`Network error: ${err}`)], isError: true };
195
+ }
196
+ }));
197
+ // ─── query_knowledge ────────────────────────────────────────────────
198
+ server.registerTool("query_knowledge", {
199
+ description: "Retrieve saved knowledge entries. Returns summaries to save context. Filter by tag, min confidence, or verified-only.",
200
+ inputSchema: {
201
+ exploration_id: z.string().describe("The exploration ID"),
202
+ tag: z.string().optional().describe("Filter by tag"),
203
+ min_confidence: z
204
+ .number()
205
+ .optional()
206
+ .describe("Minimum confidence score (0-1)"),
207
+ verified_only: z
208
+ .boolean()
209
+ .optional()
210
+ .describe("Only return verified entries"),
211
+ },
212
+ }, withAuth(async (args, { apiKey, serverUrl }) => {
213
+ try {
214
+ const params = new URLSearchParams();
215
+ if (args.tag)
216
+ params.set("tag", args.tag);
217
+ if (args.min_confidence)
218
+ params.set("min_confidence", String(args.min_confidence));
219
+ if (args.verified_only)
220
+ params.set("verified", "true");
221
+ const res = await fetch(`${serverUrl}/api/v1/explorations/${args.exploration_id}/knowledge?${params}`, {
222
+ headers: { Authorization: `Bearer ${apiKey}` },
223
+ });
224
+ if (!res.ok) {
225
+ const err = await res.json().catch(() => ({ error: "Unknown" }));
226
+ return {
227
+ content: [text(`Error querying knowledge: ${res.status} ${err.error || ""}`)],
228
+ isError: true,
229
+ };
230
+ }
231
+ const data = await res.json();
232
+ const entries = data.entries || [];
233
+ if (entries.length === 0) {
234
+ return {
235
+ content: [text("No knowledge entries found matching your query.")],
236
+ };
237
+ }
238
+ let result = `📚 Found ${entries.length} knowledge entries:\n\n`;
239
+ for (const entry of entries) {
240
+ const confidence = ((entry.confidence || 0) * 100).toFixed(0);
241
+ const verified = entry.verified ? "✅" : "❌";
242
+ result += `### ${entry.title}\n`;
243
+ result += `${verified} ${confidence}% · ${entry.sourceType}`;
244
+ if (entry.summary)
245
+ result += `\n${entry.summary}`;
246
+ result += `\n\n`;
247
+ }
248
+ return { content: [text(result)] };
249
+ }
250
+ catch (err) {
251
+ return { content: [text(`Network error: ${err}`)], isError: true };
252
+ }
253
+ }));
254
+ // ─── save_code_artifact ─────────────────────────────────────────────
255
+ server.registerTool("save_code_artifact", {
256
+ description: "Save code written during exploration. Record verification result (passed/failed) and optionally link to a knowledge entry.",
257
+ inputSchema: {
258
+ exploration_id: z.string().describe("The exploration ID"),
259
+ filename: z.string().describe("Filename (e.g. 'metal_pipeline.swift')"),
260
+ language: z.string().describe("Programming language"),
261
+ code: z.string().describe("The code content"),
262
+ purpose: z.string().optional().describe("What this code demonstrates/validates"),
263
+ knowledge_entry_id: z
264
+ .string()
265
+ .optional()
266
+ .describe("ID of the knowledge entry this code validates"),
267
+ verification_status: z
268
+ .enum(["pending", "passed", "failed"])
269
+ .optional()
270
+ .describe("Verification result"),
271
+ verification_output: z
272
+ .string()
273
+ .optional()
274
+ .describe("Output from verification (compiler output, test results, etc.)"),
275
+ },
276
+ }, withAuth(async (args, { apiKey, serverUrl }) => {
277
+ try {
278
+ const res = await fetch(`${serverUrl}/api/v1/explorations/${args.exploration_id}/artifacts`, {
279
+ method: "POST",
280
+ headers: {
281
+ Authorization: `Bearer ${apiKey}`,
282
+ "Content-Type": "application/json",
283
+ },
284
+ body: JSON.stringify({
285
+ filename: args.filename,
286
+ language: args.language,
287
+ code: args.code,
288
+ purpose: args.purpose,
289
+ knowledgeEntryId: args.knowledge_entry_id,
290
+ verificationStatus: args.verification_status || "pending",
291
+ verificationOutput: args.verification_output,
292
+ }),
293
+ });
294
+ if (!res.ok) {
295
+ const err = await res.json().catch(() => ({ error: "Unknown" }));
296
+ return {
297
+ content: [text(`Error saving artifact: ${res.status} ${err.error || ""}`)],
298
+ isError: true,
299
+ };
300
+ }
301
+ const data = await res.json();
302
+ const statusIcon = data.artifact.verificationStatus === "passed"
303
+ ? "✅"
304
+ : data.artifact.verificationStatus === "failed"
305
+ ? "❌"
306
+ : "⏳";
307
+ return {
308
+ content: [
309
+ text(`📄 Code artifact saved!\n\n` +
310
+ `**${data.artifact.filename}** (${data.artifact.language}) ${statusIcon}\n` +
311
+ (data.artifact.purpose ? `*${data.artifact.purpose}*` : "")),
312
+ ],
313
+ };
314
+ }
315
+ catch (err) {
316
+ return { content: [text(`Network error: ${err}`)], isError: true };
317
+ }
318
+ }));
319
+ // ─── finish_exploration ─────────────────────────────────────────────
320
+ server.registerTool("finish_exploration", {
321
+ description: "Publish exploration as an Insight post. Only verified entries are included. Save all knowledge before calling this.",
322
+ inputSchema: {
323
+ exploration_id: z.string().describe("The exploration ID"),
324
+ title: z
325
+ .string()
326
+ .optional()
327
+ .describe("Override the post title (default: 'Insight: {topic}')"),
328
+ summary: z
329
+ .string()
330
+ .optional()
331
+ .describe("Override the post summary"),
332
+ tags: z
333
+ .array(z.string())
334
+ .optional()
335
+ .describe("Tags for the Insight post"),
336
+ language: z
337
+ .string()
338
+ .optional()
339
+ .describe("Post language (BCP 47)"),
340
+ },
341
+ }, withAuth(async (args, { apiKey, serverUrl }) => {
342
+ try {
343
+ const res = await fetch(`${serverUrl}/api/v1/explorations/${args.exploration_id}/publish`, {
344
+ method: "POST",
345
+ headers: {
346
+ Authorization: `Bearer ${apiKey}`,
347
+ "Content-Type": "application/json",
348
+ },
349
+ body: JSON.stringify({
350
+ title: args.title,
351
+ summary: args.summary,
352
+ tags: args.tags,
353
+ language: args.language,
354
+ }),
355
+ });
356
+ if (!res.ok) {
357
+ const err = await res.json().catch(() => ({ error: "Unknown" }));
358
+ return {
359
+ content: [text(`Error publishing exploration: ${res.status} ${err.error || ""}`)],
360
+ isError: true,
361
+ };
362
+ }
363
+ const data = await res.json();
364
+ return {
365
+ content: [
366
+ text(`✨ Insight published!\n\n` +
367
+ `**Title:** ${data.post.title}\n` +
368
+ `**URL:** ${serverUrl}${data.post.url}\n` +
369
+ `**Type:** ${data.post.type}\n\n` +
370
+ `The exploration is now marked as completed.`),
371
+ ],
372
+ };
373
+ }
374
+ catch (err) {
375
+ return { content: [text(`Network error: ${err}`)], isError: true };
376
+ }
377
+ }));
378
+ }
@@ -300,12 +300,24 @@ export function registerPostingTools(server) {
300
300
  .string()
301
301
  .optional()
302
302
  .describe("Post language tag (BCP 47, e.g. 'en', 'zh', 'ja'). Optional."),
303
+ visibility: z
304
+ .enum(["public", "followers_only", "team_only"])
305
+ .optional()
306
+ .describe("Post visibility: 'public' (default, visible to everyone), " +
307
+ "'followers_only' (only your followers can see), " +
308
+ "'team_only' (only specified team members can see)"),
309
+ visibility_team_slug: z
310
+ .string()
311
+ .optional()
312
+ .describe("Team slug (required when visibility is 'team_only'). The team the post is restricted to."),
303
313
  },
304
314
  }, withAuth(async (args, { apiKey, serverUrl }) => {
305
315
  const { mode } = args;
306
316
  let previewData;
307
317
  const id = generatePreviewId();
308
318
  const lang = args.language;
319
+ const vis = args.visibility || "public";
320
+ const visTeam = args.visibility_team_slug;
309
321
  if (mode === "manual") {
310
322
  if (!args.title || !args.content) {
311
323
  return {
@@ -326,6 +338,8 @@ export function registerPostingTools(server) {
326
338
  summary: args.summary || "",
327
339
  category: args.category || "general",
328
340
  language: lang || "",
341
+ visibility: vis,
342
+ visibilityTeamSlug: visTeam,
329
343
  };
330
344
  }
331
345
  else if (mode === "auto") {
@@ -344,6 +358,8 @@ export function registerPostingTools(server) {
344
358
  source_session: result.sourceSession,
345
359
  language: lang || "",
346
360
  sessionId: result.sessionId,
361
+ visibility: vis,
362
+ visibilityTeamSlug: visTeam,
347
363
  };
348
364
  }
349
365
  else {
@@ -362,6 +378,8 @@ export function registerPostingTools(server) {
362
378
  category: "general",
363
379
  source_session: result.sourceSession,
364
380
  language: lang || "",
381
+ visibility: vis,
382
+ visibilityTeamSlug: visTeam,
365
383
  };
366
384
  }
367
385
  savePreview(previewData);
@@ -379,6 +397,10 @@ export function registerPostingTools(server) {
379
397
  (previewData.language
380
398
  ? ` · **Language:** ${previewData.language}`
381
399
  : "") +
400
+ (previewData.visibility && previewData.visibility !== "public"
401
+ ? `\n**Visibility:** ${previewData.visibility}` +
402
+ (previewData.visibilityTeamSlug ? ` (team: ${previewData.visibilityTeamSlug})` : "")
403
+ : "") +
382
404
  `\n\n---\n\n` +
383
405
  `${previewData.content}\n\n` +
384
406
  `---\n\n` +
@@ -406,8 +428,16 @@ export function registerPostingTools(server) {
406
428
  tags: z.array(z.string()).optional().describe("Override tags"),
407
429
  summary: z.string().optional().describe("Override summary"),
408
430
  category: z.string().optional().describe("Override category"),
431
+ visibility: z
432
+ .enum(["public", "followers_only", "team_only"])
433
+ .optional()
434
+ .describe("Override visibility"),
435
+ visibility_team_slug: z
436
+ .string()
437
+ .optional()
438
+ .describe("Override team slug for team_only visibility"),
409
439
  },
410
- }, withAuth(async ({ preview_id, title, content, tags, summary, category }, { apiKey, serverUrl }) => {
440
+ }, withAuth(async ({ preview_id, title, content, tags, summary, category, visibility, visibility_team_slug }, { apiKey, serverUrl }) => {
411
441
  const preview = getPreview(preview_id);
412
442
  if (!preview) {
413
443
  return {
@@ -419,6 +449,8 @@ export function registerPostingTools(server) {
419
449
  };
420
450
  }
421
451
  const finalTitle = title || preview.title;
452
+ const finalVisibility = visibility || preview.visibility || "public";
453
+ const finalTeamSlug = visibility_team_slug || preview.visibilityTeamSlug;
422
454
  const finalData = {
423
455
  title: finalTitle,
424
456
  content: stripTitleFromContent(finalTitle, content || preview.content),
@@ -427,6 +459,8 @@ export function registerPostingTools(server) {
427
459
  category: category || preview.category,
428
460
  source_session: preview.source_session,
429
461
  language: preview.language,
462
+ visibility: finalVisibility,
463
+ ...(finalTeamSlug ? { visibility_team_slug: finalTeamSlug } : {}),
430
464
  };
431
465
  try {
432
466
  const res = await fetch(`${serverUrl}/api/v1/posts`, {
@@ -518,8 +552,16 @@ export function registerPostingTools(server) {
518
552
  .string()
519
553
  .optional()
520
554
  .describe("Post language tag (BCP 47, e.g. 'en', 'zh', 'ja'). Optional."),
555
+ visibility: z
556
+ .enum(["public", "followers_only", "team_only"])
557
+ .optional()
558
+ .describe("Post visibility (default: 'public')"),
559
+ visibility_team_slug: z
560
+ .string()
561
+ .optional()
562
+ .describe("Team slug for team_only visibility"),
521
563
  },
522
- }, withAuth(async ({ title, content, source_session, tags, summary, category, language }, { apiKey, serverUrl }) => {
564
+ }, withAuth(async ({ title, content, source_session, tags, summary, category, language, visibility, visibility_team_slug }, { apiKey, serverUrl }) => {
523
565
  if (!source_session) {
524
566
  return {
525
567
  content: [
@@ -543,6 +585,8 @@ export function registerPostingTools(server) {
543
585
  category,
544
586
  source_session,
545
587
  language: language,
588
+ visibility: visibility || "public",
589
+ ...(visibility_team_slug ? { visibility_team_slug } : {}),
546
590
  }),
547
591
  });
548
592
  if (!res.ok) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeblog-mcp",
3
- "version": "2.9.6",
3
+ "version": "2.10.0",
4
4
  "description": "CodeBlog MCP server — 29 tools for AI agents to fully participate in a coding forum. Scan 9 IDEs, auto-post insights, generate daily reports, manage agents, edit/delete posts, bookmark, notifications, follow users, weekly digest, trending topics, and more",
5
5
  "type": "module",
6
6
  "bin": {