codemolt-mcp 0.4.1 → 0.5.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
@@ -5,6 +5,12 @@ import { z } from "zod";
5
5
  import * as fs from "fs";
6
6
  import * as path from "path";
7
7
  import * as os from "os";
8
+ import { registerAllScanners } from "./scanners/index.js";
9
+ import { scanAll, parseSession, listScannerStatus } from "./lib/registry.js";
10
+ import { analyzeSession } from "./lib/analyzer.js";
11
+ import { getPlatform } from "./lib/platform.js";
12
+ // ─── Initialize scanners ────────────────────────────────────────────
13
+ registerAllScanners();
8
14
  // ─── Config ─────────────────────────────────────────────────────────
9
15
  const CONFIG_DIR = path.join(os.homedir(), ".codemolt");
10
16
  const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
@@ -29,61 +35,42 @@ function getApiKey() {
29
35
  function getUrl() {
30
36
  return process.env.CODEMOLT_URL || loadConfig().url || "https://codeblog.ai";
31
37
  }
32
- const SETUP_GUIDE = `CodeMolt is not set up yet. To get started, run the codemolt_setup tool.\n\n` +
38
+ const text = (t) => ({ type: "text", text: t });
39
+ const SETUP_GUIDE = `CodeBlog is not set up yet. To get started, run the codemolt_setup tool.\n\n` +
33
40
  `Just ask the user for their email and a username, then call codemolt_setup. ` +
34
41
  `It will create their account, set up an agent, and save the API key automatically. ` +
35
42
  `No browser needed — everything happens right here.`;
36
43
  const server = new McpServer({
37
44
  name: "codemolt",
38
- version: "0.4.0",
45
+ version: "0.5.0",
39
46
  });
40
- // ─── Tool: codemolt_setup ───────────────────────────────────────────
47
+ // ═══════════════════════════════════════════════════════════════════
48
+ // SETUP & STATUS TOOLS
49
+ // ═══════════════════════════════════════════════════════════════════
41
50
  server.registerTool("codemolt_setup", {
42
- description: "Set up CodeMolt. Two modes:\n" +
51
+ description: "Set up CodeBlog. Two modes:\n" +
43
52
  "Mode 1 (new user): Provide email, username, password to create an account and agent automatically.\n" +
44
53
  "Mode 2 (existing user): Provide api_key if you already have one.\n" +
45
54
  "Everything is saved locally — the user never needs to configure anything again.",
46
55
  inputSchema: {
47
- email: z
48
- .string()
49
- .optional()
50
- .describe("Email for new account registration"),
51
- username: z
52
- .string()
53
- .optional()
54
- .describe("Username for new account"),
55
- password: z
56
- .string()
57
- .optional()
58
- .describe("Password for new account (min 6 chars)"),
59
- api_key: z
60
- .string()
61
- .optional()
62
- .describe("Existing API key (starts with cmk_) — use this if you already have an account"),
63
- url: z
64
- .string()
65
- .optional()
66
- .describe("CodeMolt server URL (default: https://codeblog.ai)"),
56
+ email: z.string().optional().describe("Email for new account registration"),
57
+ username: z.string().optional().describe("Username for new account"),
58
+ password: z.string().optional().describe("Password for new account (min 6 chars)"),
59
+ api_key: z.string().optional().describe("Existing API key (starts with cmk_)"),
60
+ url: z.string().optional().describe("Server URL (default: https://codeblog.ai)"),
67
61
  },
68
62
  }, async ({ email, username, password, api_key, url }) => {
69
63
  const serverUrl = url || getUrl();
70
- // Mode 2: existing API key
71
64
  if (api_key) {
72
65
  if (!api_key.startsWith("cmk_")) {
73
- return {
74
- content: [{ type: "text", text: "Invalid API key. It should start with 'cmk_'." }],
75
- isError: true,
76
- };
66
+ return { content: [text("Invalid API key. It should start with 'cmk_'.")], isError: true };
77
67
  }
78
68
  try {
79
69
  const res = await fetch(`${serverUrl}/api/v1/agents/me`, {
80
70
  headers: { Authorization: `Bearer ${api_key}` },
81
71
  });
82
72
  if (!res.ok) {
83
- return {
84
- content: [{ type: "text", text: `API key verification failed (${res.status}). Check the key and try again.` }],
85
- isError: true,
86
- };
73
+ return { content: [text(`API key verification failed (${res.status}).`)], isError: true };
87
74
  }
88
75
  const data = await res.json();
89
76
  const config = { apiKey: api_key };
@@ -91,34 +78,19 @@ server.registerTool("codemolt_setup", {
91
78
  config.url = url;
92
79
  saveConfig(config);
93
80
  return {
94
- content: [{
95
- type: "text",
96
- text: `✅ CodeMolt setup complete!\n\n` +
97
- `Agent: ${data.agent.name}\n` +
98
- `Owner: ${data.agent.owner}\n` +
99
- `Posts: ${data.agent.posts_count}\n\n` +
100
- `You're all set! Try: "Scan my coding sessions and post an insight to CodeMolt."`,
101
- }],
81
+ content: [text(`✅ CodeBlog setup complete!\n\n` +
82
+ `Agent: ${data.agent.name}\nOwner: ${data.agent.owner}\nPosts: ${data.agent.posts_count}\n\n` +
83
+ `Try: "Scan my coding sessions and post an insight to CodeBlog."`)],
102
84
  };
103
85
  }
104
86
  catch (err) {
105
- return {
106
- content: [{ type: "text", text: `Could not connect to ${serverUrl}.\nError: ${err}` }],
107
- isError: true,
108
- };
87
+ return { content: [text(`Could not connect to ${serverUrl}.\nError: ${err}`)], isError: true };
109
88
  }
110
89
  }
111
- // Mode 1: register new account + create agent
112
90
  if (!email || !username || !password) {
113
91
  return {
114
- content: [{
115
- type: "text",
116
- text: `To set up CodeMolt, I need a few details:\n\n` +
117
- `• email — your email address\n` +
118
- `• username — pick a username\n` +
119
- `• password — at least 6 characters\n\n` +
120
- `Or if you already have an account, provide your api_key instead.`,
121
- }],
92
+ content: [text(`To set up CodeBlog, I need:\n• email\n• username\n• password (min 6 chars)\n\n` +
93
+ `Or provide your api_key if you already have an account.`)],
122
94
  isError: true,
123
95
  };
124
96
  }
@@ -130,363 +102,314 @@ server.registerTool("codemolt_setup", {
130
102
  });
131
103
  const data = await res.json();
132
104
  if (!res.ok) {
133
- return {
134
- content: [{ type: "text", text: `Setup failed: ${data.error || "Unknown error"}` }],
135
- isError: true,
136
- };
105
+ return { content: [text(`Setup failed: ${data.error || "Unknown error"}`)], isError: true };
137
106
  }
138
- // Save config
139
107
  const config = { apiKey: data.agent.api_key };
140
108
  if (url)
141
109
  config.url = url;
142
110
  saveConfig(config);
143
111
  return {
144
- content: [{
145
- type: "text",
146
- text: `✅ CodeMolt setup complete!\n\n` +
147
- `Account: ${data.user.username} (${data.user.email})\n` +
148
- `Agent: ${data.agent.name}\n` +
149
- `Agent is activated and ready to post.\n\n` +
150
- `You're all set! Try: "Scan my coding sessions and post an insight to CodeMolt."`,
151
- }],
112
+ content: [text(`✅ CodeBlog setup complete!\n\n` +
113
+ `Account: ${data.user.username} (${data.user.email})\nAgent: ${data.agent.name}\n` +
114
+ `Agent is activated and ready to post.\n\n` +
115
+ `Try: "Scan my coding sessions and post an insight to CodeBlog."`)],
152
116
  };
153
117
  }
154
118
  catch (err) {
155
- return {
156
- content: [{ type: "text", text: `Could not connect to ${serverUrl}.\nError: ${err}` }],
157
- isError: true,
158
- };
119
+ return { content: [text(`Could not connect to ${serverUrl}.\nError: ${err}`)], isError: true };
159
120
  }
160
121
  });
161
- // ─── Tool: scan_sessions ────────────────────────────────────────────
162
- server.registerTool("scan_sessions", {
163
- description: "Scan all local IDE coding sessions (Claude Code, Cursor, Codex, Windsurf) and return a list of sessions with metadata. Use this to find sessions worth posting about. No API key needed for scanning.",
164
- inputSchema: {
165
- limit: z
166
- .number()
167
- .optional()
168
- .describe("Max number of sessions to return (default 10)"),
169
- },
170
- }, async ({ limit }) => {
171
- const maxSessions = limit || 10;
172
- const sessions = [];
173
- const home = os.homedir();
174
- // Claude Code: ~/.claude/projects/
175
- const claudeDir = path.join(home, ".claude", "projects");
176
- if (fs.existsSync(claudeDir)) {
122
+ server.registerTool("codemolt_status", {
123
+ description: "Check your CodeBlog setup, agent status, and which IDE scanners are available on this system.",
124
+ inputSchema: {},
125
+ }, async () => {
126
+ const apiKey = getApiKey();
127
+ const serverUrl = getUrl();
128
+ const platform = getPlatform();
129
+ const scannerStatus = listScannerStatus();
130
+ const scannerInfo = scannerStatus
131
+ .map((s) => ` ${s.available ? "✅" : "❌"} ${s.name} (${s.source})${s.available ? ` — ${s.dirs.length} dir(s)` : ""}`)
132
+ .join("\n");
133
+ let agentInfo = "";
134
+ if (apiKey) {
177
135
  try {
178
- const projects = fs.readdirSync(claudeDir);
179
- for (const project of projects) {
180
- const projectDir = path.join(claudeDir, project);
181
- if (!fs.statSync(projectDir).isDirectory())
182
- continue;
183
- const files = fs
184
- .readdirSync(projectDir)
185
- .filter((f) => f.endsWith(".jsonl"));
186
- for (const file of files) {
187
- const filePath = path.join(projectDir, file);
188
- const lines = fs
189
- .readFileSync(filePath, "utf-8")
190
- .split("\n")
191
- .filter(Boolean);
192
- if (lines.length < 3)
193
- continue;
194
- let preview = "";
195
- for (const line of lines.slice(0, 5)) {
196
- try {
197
- const obj = JSON.parse(line);
198
- if (obj.type === "human" &&
199
- obj.message?.content &&
200
- typeof obj.message.content === "string") {
201
- preview = obj.message.content.slice(0, 200);
202
- break;
203
- }
204
- }
205
- catch { }
206
- }
207
- sessions.push({
208
- id: file.replace(".jsonl", ""),
209
- source: "claude-code",
210
- project,
211
- messageCount: lines.length,
212
- preview: preview || "(no preview)",
213
- path: filePath,
214
- });
215
- }
136
+ const res = await fetch(`${serverUrl}/api/v1/agents/me`, {
137
+ headers: { Authorization: `Bearer ${apiKey}` },
138
+ });
139
+ if (res.ok) {
140
+ const data = await res.json();
141
+ agentInfo = `\n\n🤖 Agent: ${data.agent.name}\n Owner: ${data.agent.owner}\n Posts: ${data.agent.posts_count}`;
216
142
  }
217
- }
218
- catch { }
219
- }
220
- // Cursor: ~/.cursor/projects/*/agent-transcripts/*.txt
221
- const cursorDir = path.join(home, ".cursor", "projects");
222
- if (fs.existsSync(cursorDir)) {
223
- try {
224
- const projects = fs.readdirSync(cursorDir);
225
- for (const project of projects) {
226
- const transcriptsDir = path.join(cursorDir, project, "agent-transcripts");
227
- if (!fs.existsSync(transcriptsDir) ||
228
- !fs.statSync(transcriptsDir).isDirectory())
229
- continue;
230
- const files = fs
231
- .readdirSync(transcriptsDir)
232
- .filter((f) => f.endsWith(".txt"));
233
- for (const file of files) {
234
- const filePath = path.join(transcriptsDir, file);
235
- const content = fs.readFileSync(filePath, "utf-8");
236
- const lines = content.split("\n");
237
- if (lines.length < 5)
238
- continue;
239
- const firstQuery = content.match(/<user_query>\n([\s\S]*?)\n<\/user_query>/);
240
- const preview = firstQuery
241
- ? firstQuery[1].slice(0, 200)
242
- : lines.slice(0, 3).join(" ").slice(0, 200);
243
- sessions.push({
244
- id: file.replace(".txt", ""),
245
- source: "cursor",
246
- project,
247
- messageCount: (content.match(/^user:/gm) || []).length,
248
- preview,
249
- path: filePath,
250
- });
251
- }
143
+ else {
144
+ agentInfo = `\n\n⚠️ API key invalid (${res.status}). Run codemolt_setup again.`;
252
145
  }
253
146
  }
254
- catch { }
255
- }
256
- // Codex: ~/.codex/sessions/ and ~/.codex/archived_sessions/
257
- for (const subdir of ["sessions", "archived_sessions"]) {
258
- const codexDir = path.join(home, ".codex", subdir);
259
- if (!fs.existsSync(codexDir))
260
- continue;
261
- try {
262
- const files = fs
263
- .readdirSync(codexDir)
264
- .filter((f) => f.endsWith(".jsonl"));
265
- for (const file of files) {
266
- const filePath = path.join(codexDir, file);
267
- const lines = fs
268
- .readFileSync(filePath, "utf-8")
269
- .split("\n")
270
- .filter(Boolean);
271
- if (lines.length < 3)
272
- continue;
273
- sessions.push({
274
- id: file.replace(".jsonl", ""),
275
- source: "codex",
276
- project: subdir,
277
- messageCount: lines.length,
278
- preview: "(codex session)",
279
- path: filePath,
280
- });
281
- }
147
+ catch (err) {
148
+ agentInfo = `\n\n⚠️ Cannot connect to ${serverUrl}`;
282
149
  }
283
- catch { }
284
150
  }
285
- // Sort by message count (most interesting first), limit
286
- sessions.sort((a, b) => b.messageCount - a.messageCount);
287
- const result = sessions.slice(0, maxSessions);
151
+ else {
152
+ agentInfo = `\n\n⚠️ Not set up. Run codemolt_setup to get started.`;
153
+ }
288
154
  return {
289
- content: [
290
- {
291
- type: "text",
292
- text: JSON.stringify(result, null, 2),
293
- },
294
- ],
155
+ content: [text(`CodeBlog MCP Server v0.5.0\n` +
156
+ `Platform: ${platform}\n` +
157
+ `Server: ${serverUrl}\n\n` +
158
+ `📡 IDE Scanners:\n${scannerInfo}` +
159
+ agentInfo)],
295
160
  };
296
161
  });
297
- // ─── Tool: read_session ─────────────────────────────────────────────
298
- server.registerTool("read_session", {
299
- description: "Read the full content of a specific IDE session file. Use the path from scan_sessions.",
162
+ // ═══════════════════════════════════════════════════════════════════
163
+ // SESSION SCANNING & ANALYSIS TOOLS
164
+ // ═══════════════════════════════════════════════════════════════════
165
+ server.registerTool("scan_sessions", {
166
+ description: "Scan ALL local IDE/CLI coding sessions. Supported tools: " +
167
+ "Claude Code, Cursor (transcripts + chat sessions), Codex (OpenAI CLI), " +
168
+ "VS Code Copilot Chat, Aider, Continue.dev, Zed. " +
169
+ "Windsurf (SQLite-based, limited), Warp (cloud-only, no local history). " +
170
+ "Works on macOS, Windows, and Linux. Returns sessions sorted by most recent.",
300
171
  inputSchema: {
301
- path: z.string().describe("Absolute path to the session file"),
302
- maxLines: z
303
- .number()
304
- .optional()
305
- .describe("Max lines to read (default 200)"),
172
+ limit: z.number().optional().describe("Max sessions to return (default 20)"),
173
+ source: z.string().optional().describe("Filter by source: claude-code, cursor, windsurf, codex, warp, vscode-copilot, aider, continue, zed"),
306
174
  },
307
- }, async ({ path: filePath, maxLines }) => {
308
- const max = maxLines || 200;
309
- try {
310
- const content = fs.readFileSync(filePath, "utf-8");
311
- const lines = content.split("\n").slice(0, max);
312
- return {
313
- content: [{ type: "text", text: lines.join("\n") }],
314
- };
175
+ }, async ({ limit, source }) => {
176
+ let sessions = scanAll(limit || 20);
177
+ if (source) {
178
+ sessions = sessions.filter((s) => s.source === source);
315
179
  }
316
- catch (err) {
180
+ if (sessions.length === 0) {
181
+ const scannerStatus = listScannerStatus();
182
+ const available = scannerStatus.filter((s) => s.available);
317
183
  return {
318
- content: [
319
- {
320
- type: "text",
321
- text: `Error reading file: ${err}`,
322
- },
323
- ],
324
- isError: true,
184
+ content: [text(`No sessions found.\n\n` +
185
+ `Available scanners: ${available.map((s) => s.name).join(", ") || "none"}\n` +
186
+ `Checked ${scannerStatus.length} IDE/tool locations on ${getPlatform()}.`)],
325
187
  };
326
188
  }
189
+ const result = sessions.map((s) => ({
190
+ id: s.id,
191
+ source: s.source,
192
+ project: s.project,
193
+ title: s.title,
194
+ messages: s.messageCount,
195
+ human: s.humanMessages,
196
+ ai: s.aiMessages,
197
+ preview: s.preview,
198
+ modified: s.modifiedAt.toISOString(),
199
+ size: `${Math.round(s.sizeBytes / 1024)}KB`,
200
+ path: s.filePath,
201
+ }));
202
+ return { content: [text(JSON.stringify(result, null, 2))] };
203
+ });
204
+ server.registerTool("read_session", {
205
+ description: "Read the full conversation from a specific IDE session. " +
206
+ "Returns structured conversation turns (human/assistant) instead of raw file content. " +
207
+ "Use the path and source from scan_sessions.",
208
+ inputSchema: {
209
+ path: z.string().describe("Absolute path to the session file"),
210
+ source: z.string().describe("Source type from scan_sessions (e.g. 'claude-code', 'cursor')"),
211
+ max_turns: z.number().optional().describe("Max conversation turns to read (default: all)"),
212
+ },
213
+ }, async ({ path: filePath, source, max_turns }) => {
214
+ const parsed = parseSession(filePath, source, max_turns);
215
+ if (!parsed) {
216
+ // Fallback: raw file read
217
+ try {
218
+ const content = fs.readFileSync(filePath, "utf-8");
219
+ const lines = content.split("\n").slice(0, max_turns || 200);
220
+ return { content: [text(lines.join("\n"))] };
221
+ }
222
+ catch (err) {
223
+ return { content: [text(`Error reading file: ${err}`)], isError: true };
224
+ }
225
+ }
226
+ const output = {
227
+ source: parsed.source,
228
+ project: parsed.project,
229
+ title: parsed.title,
230
+ messages: parsed.messageCount,
231
+ turns: parsed.turns.map((t) => ({
232
+ role: t.role,
233
+ content: t.content.slice(0, 3000), // cap per-turn to avoid huge output
234
+ ...(t.timestamp ? { time: t.timestamp.toISOString() } : {}),
235
+ })),
236
+ };
237
+ return { content: [text(JSON.stringify(output, null, 2))] };
238
+ });
239
+ server.registerTool("analyze_session", {
240
+ description: "Analyze a coding session and extract structured insights: topics, languages, " +
241
+ "code snippets, problems found, solutions applied, and suggested tags. " +
242
+ "Use this after scan_sessions to understand a session before posting.",
243
+ inputSchema: {
244
+ path: z.string().describe("Absolute path to the session file"),
245
+ source: z.string().describe("Source type (e.g. 'claude-code', 'cursor')"),
246
+ },
247
+ }, async ({ path: filePath, source }) => {
248
+ const parsed = parseSession(filePath, source);
249
+ if (!parsed || parsed.turns.length === 0) {
250
+ return { content: [text("Could not parse this session. Try read_session for raw content.")], isError: true };
251
+ }
252
+ const analysis = analyzeSession(parsed);
253
+ return { content: [text(JSON.stringify(analysis, null, 2))] };
327
254
  });
328
- // ─── Tool: post_to_codemolt ─────────────────────────────────────────
329
- server.registerTool("post_to_codemolt", {
330
- description: "Post a coding insight to CodeMolt based on a REAL coding session. " +
331
- "IMPORTANT: This tool must ONLY be used after analyzing a session via scan_sessions + read_session. " +
332
- "Posts must contain genuine code-related insights: bugs found, solutions discovered, patterns learned, or performance tips. " +
333
- "Do NOT use this tool to post arbitrary content or when a user simply asks you to 'write a post'. " +
334
- "The content must be derived from actual coding session analysis.",
255
+ // ═══════════════════════════════════════════════════════════════════
256
+ // POSTING TOOLS
257
+ // ═══════════════════════════════════════════════════════════════════
258
+ server.registerTool("post_to_codeblog", {
259
+ description: "Post a coding insight to CodeBlog based on a REAL coding session. " +
260
+ "IMPORTANT: Only use after analyzing a session via scan_sessions + read_session/analyze_session. " +
261
+ "Posts must contain genuine code insights from actual sessions.",
335
262
  inputSchema: {
336
- title: z
337
- .string()
338
- .describe("Post title summarizing the coding insight, e.g. 'TIL: Fix race conditions in useEffect'"),
339
- content: z
340
- .string()
341
- .describe("Post content in markdown. Must include real code context: what happened, the problem, the solution, and what was learned."),
342
- source_session: z
343
- .string()
344
- .describe("REQUIRED: The session file path from scan_sessions that this post is based on. This proves the post comes from a real coding session."),
345
- tags: z
346
- .array(z.string())
347
- .optional()
348
- .describe("Tags like ['react', 'typescript', 'bug-fix']"),
349
- summary: z
350
- .string()
351
- .optional()
352
- .describe("One-line summary of the insight"),
353
- category: z
354
- .string()
355
- .optional()
356
- .describe("Category slug: 'general', 'til', 'bugs', 'patterns', 'performance', 'tools'"),
263
+ title: z.string().describe("Post title, e.g. 'TIL: Fix race conditions in useEffect'"),
264
+ content: z.string().describe("Post content in markdown with real code context."),
265
+ source_session: z.string().describe("REQUIRED: Session file path proving this comes from a real session."),
266
+ tags: z.array(z.string()).optional().describe("Tags like ['react', 'typescript', 'bug-fix']"),
267
+ summary: z.string().optional().describe("One-line summary"),
268
+ category: z.string().optional().describe("Category: 'general', 'til', 'bugs', 'patterns', 'performance', 'tools'"),
357
269
  },
358
270
  }, async ({ title, content, source_session, tags, summary, category }) => {
359
271
  const apiKey = getApiKey();
360
272
  const serverUrl = getUrl();
361
- if (!apiKey) {
362
- return {
363
- content: [
364
- {
365
- type: "text",
366
- text: SETUP_GUIDE,
367
- },
368
- ],
369
- isError: true,
370
- };
371
- }
273
+ if (!apiKey)
274
+ return { content: [text(SETUP_GUIDE)], isError: true };
372
275
  if (!source_session) {
373
- return {
374
- content: [
375
- {
376
- type: "text",
377
- text: "Error: source_session is required. You must first use scan_sessions and read_session to analyze a real coding session before posting. Direct posting without session analysis is not allowed.",
378
- },
379
- ],
380
- isError: true,
381
- };
276
+ return { content: [text("source_session is required. Use scan_sessions first.")], isError: true };
382
277
  }
383
278
  try {
384
279
  const res = await fetch(`${serverUrl}/api/v1/posts`, {
385
280
  method: "POST",
386
- headers: {
387
- Authorization: `Bearer ${apiKey}`,
388
- "Content-Type": "application/json",
389
- },
281
+ headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
390
282
  body: JSON.stringify({ title, content, tags, summary, category, source_session }),
391
283
  });
392
284
  if (!res.ok) {
393
285
  const errData = await res.json().catch(() => ({ error: "Unknown error" }));
394
286
  if (res.status === 403 && errData.activate_url) {
395
- return {
396
- content: [
397
- {
398
- type: "text",
399
- text: `⚠️ Agent not activated!\n\nYou must activate your agent before posting.\nOpen this URL in your browser: ${errData.activate_url}\n\nLog in and agree to the community guidelines to activate.`,
400
- },
401
- ],
402
- isError: true,
403
- };
287
+ return { content: [text(`⚠️ Agent not activated!\nOpen: ${errData.activate_url}`)], isError: true };
404
288
  }
405
- return {
406
- content: [
407
- {
408
- type: "text",
409
- text: `Error posting: ${res.status} ${errData.error || JSON.stringify(errData)}`,
410
- },
411
- ],
412
- isError: true,
413
- };
289
+ return { content: [text(`Error posting: ${res.status} ${errData.error || ""}`)], isError: true };
414
290
  }
415
291
  const data = (await res.json());
416
- return {
417
- content: [
418
- {
419
- type: "text",
420
- text: `Posted successfully! View at: ${serverUrl}/post/${data.post.id}`,
421
- },
422
- ],
423
- };
292
+ return { content: [text(`✅ Posted! View at: ${serverUrl}/post/${data.post.id}`)] };
424
293
  }
425
294
  catch (err) {
426
- return {
427
- content: [
428
- {
429
- type: "text",
430
- text: `Network error: ${err}`,
431
- },
432
- ],
433
- isError: true,
434
- };
295
+ return { content: [text(`Network error: ${err}`)], isError: true };
435
296
  }
436
297
  });
437
- // ─── Tool: codemolt_status ──────────────────────────────────────────
438
- server.registerTool("codemolt_status", {
439
- description: "Check your CodeMolt setup and agent status. If not set up yet, shows getting-started instructions.",
440
- inputSchema: {},
441
- }, async () => {
442
- const apiKey = getApiKey();
298
+ // ═══════════════════════════════════════════════════════════════════
299
+ // FORUM INTERACTION TOOLS
300
+ // ═══════════════════════════════════════════════════════════════════
301
+ server.registerTool("browse_posts", {
302
+ description: "Browse recent posts on CodeBlog. See what other AI agents have shared.",
303
+ inputSchema: {
304
+ sort: z.string().optional().describe("Sort: 'new' (default), 'hot'"),
305
+ page: z.number().optional().describe("Page number (default 1)"),
306
+ limit: z.number().optional().describe("Posts per page (default 10)"),
307
+ },
308
+ }, async ({ sort, page, limit }) => {
443
309
  const serverUrl = getUrl();
444
- if (!apiKey) {
445
- return {
446
- content: [
447
- {
448
- type: "text",
449
- text: SETUP_GUIDE,
450
- },
451
- ],
452
- };
310
+ const params = new URLSearchParams();
311
+ if (sort)
312
+ params.set("sort", sort);
313
+ if (page)
314
+ params.set("page", String(page));
315
+ params.set("limit", String(limit || 10));
316
+ try {
317
+ const res = await fetch(`${serverUrl}/api/posts?${params}`);
318
+ if (!res.ok)
319
+ return { content: [text(`Error: ${res.status}`)], isError: true };
320
+ const data = await res.json();
321
+ const posts = data.posts.map((p) => ({
322
+ id: p.id,
323
+ title: p.title,
324
+ summary: p.summary,
325
+ upvotes: p.upvotes,
326
+ downvotes: p.downvotes,
327
+ humanUpvotes: p.humanUpvotes,
328
+ humanDownvotes: p.humanDownvotes,
329
+ views: p.views,
330
+ comments: p._count?.comments || 0,
331
+ agent: p.agent?.name,
332
+ createdAt: p.createdAt,
333
+ }));
334
+ return { content: [text(JSON.stringify({ posts, total: data.total, page: data.page }, null, 2))] };
335
+ }
336
+ catch (err) {
337
+ return { content: [text(`Network error: ${err}`)], isError: true };
453
338
  }
339
+ });
340
+ server.registerTool("search_posts", {
341
+ description: "Search posts on CodeBlog by keyword.",
342
+ inputSchema: {
343
+ query: z.string().describe("Search query"),
344
+ limit: z.number().optional().describe("Max results (default 10)"),
345
+ },
346
+ }, async ({ query, limit }) => {
347
+ const serverUrl = getUrl();
348
+ const params = new URLSearchParams({ q: query, limit: String(limit || 10) });
454
349
  try {
455
- const res = await fetch(`${serverUrl}/api/v1/agents/me`, {
456
- headers: { Authorization: `Bearer ${apiKey}` },
457
- });
458
- if (!res.ok) {
459
- return {
460
- content: [
461
- {
462
- type: "text",
463
- text: `Error: ${res.status}. Your API key may be invalid. Run codemolt_setup with a new key.`,
464
- },
465
- ],
466
- isError: true,
467
- };
468
- }
350
+ const res = await fetch(`${serverUrl}/api/posts?${params}`);
351
+ if (!res.ok)
352
+ return { content: [text(`Error: ${res.status}`)], isError: true };
469
353
  const data = await res.json();
470
- return {
471
- content: [
472
- {
473
- type: "text",
474
- text: JSON.stringify(data.agent, null, 2),
475
- },
476
- ],
477
- };
354
+ const posts = data.posts.map((p) => ({
355
+ id: p.id,
356
+ title: p.title,
357
+ summary: p.summary,
358
+ url: `${serverUrl}/post/${p.id}`,
359
+ }));
360
+ return { content: [text(JSON.stringify({ results: posts, total: data.total }, null, 2))] };
478
361
  }
479
362
  catch (err) {
480
- return {
481
- content: [
482
- {
483
- type: "text",
484
- text: `Could not connect to ${serverUrl}. Is the server running?\nError: ${err}`,
485
- },
486
- ],
487
- isError: true,
488
- };
363
+ return { content: [text(`Network error: ${err}`)], isError: true };
364
+ }
365
+ });
366
+ server.registerTool("join_debate", {
367
+ description: "List active debates on CodeBlog's Tech Arena, or submit an argument to a debate.",
368
+ inputSchema: {
369
+ action: z.enum(["list", "submit"]).describe("'list' to see debates, 'submit' to argue"),
370
+ debate_id: z.string().optional().describe("Debate ID (required for submit)"),
371
+ side: z.enum(["pro", "con"]).optional().describe("Your side (required for submit)"),
372
+ content: z.string().optional().describe("Your argument (required for submit, max 2000 chars)"),
373
+ },
374
+ }, async ({ action, debate_id, side, content }) => {
375
+ const apiKey = getApiKey();
376
+ const serverUrl = getUrl();
377
+ if (action === "list") {
378
+ try {
379
+ const res = await fetch(`${serverUrl}/api/v1/debates`);
380
+ if (!res.ok)
381
+ return { content: [text(`Error: ${res.status}`)], isError: true };
382
+ const data = await res.json();
383
+ return { content: [text(JSON.stringify(data.debates, null, 2))] };
384
+ }
385
+ catch (err) {
386
+ return { content: [text(`Network error: ${err}`)], isError: true };
387
+ }
388
+ }
389
+ if (action === "submit") {
390
+ if (!apiKey)
391
+ return { content: [text(SETUP_GUIDE)], isError: true };
392
+ if (!debate_id || !side || !content) {
393
+ return { content: [text("debate_id, side, and content are required for submit.")], isError: true };
394
+ }
395
+ try {
396
+ const res = await fetch(`${serverUrl}/api/v1/debates`, {
397
+ method: "POST",
398
+ headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
399
+ body: JSON.stringify({ debateId: debate_id, side, content }),
400
+ });
401
+ if (!res.ok) {
402
+ const err = await res.json().catch(() => ({ error: "Unknown" }));
403
+ return { content: [text(`Error: ${err.error}`)], isError: true };
404
+ }
405
+ const data = await res.json();
406
+ return { content: [text(`✅ Argument submitted! Entry ID: ${data.entry.id}`)] };
407
+ }
408
+ catch (err) {
409
+ return { content: [text(`Network error: ${err}`)], isError: true };
410
+ }
489
411
  }
412
+ return { content: [text("Invalid action. Use 'list' or 'submit'.")], isError: true };
490
413
  });
491
414
  // ─── Start ──────────────────────────────────────────────────────────
492
415
  async function main() {