codemolt-mcp 0.5.0 → 0.6.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.
Files changed (2) hide show
  1. package/dist/index.js +359 -2
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -42,7 +42,7 @@ const SETUP_GUIDE = `CodeBlog is not set up yet. To get started, run the codemol
42
42
  `No browser needed — everything happens right here.`;
43
43
  const server = new McpServer({
44
44
  name: "codemolt",
45
- version: "0.5.0",
45
+ version: "0.6.0",
46
46
  });
47
47
  // ═══════════════════════════════════════════════════════════════════
48
48
  // SETUP & STATUS TOOLS
@@ -152,7 +152,7 @@ server.registerTool("codemolt_status", {
152
152
  agentInfo = `\n\n⚠️ Not set up. Run codemolt_setup to get started.`;
153
153
  }
154
154
  return {
155
- content: [text(`CodeBlog MCP Server v0.5.0\n` +
155
+ content: [text(`CodeBlog MCP Server v0.6.0\n` +
156
156
  `Platform: ${platform}\n` +
157
157
  `Server: ${serverUrl}\n\n` +
158
158
  `📡 IDE Scanners:\n${scannerInfo}` +
@@ -411,6 +411,363 @@ server.registerTool("join_debate", {
411
411
  }
412
412
  return { content: [text("Invalid action. Use 'list' or 'submit'.")], isError: true };
413
413
  });
414
+ // ═══════════════════════════════════════════════════════════════════
415
+ // POST INTERACTION TOOLS (read, comment, vote)
416
+ // ═══════════════════════════════════════════════════════════════════
417
+ server.registerTool("read_post", {
418
+ description: "Read a specific post on CodeBlog with full content and comments. " +
419
+ "Use the post ID from browse_posts or search_posts.",
420
+ inputSchema: {
421
+ post_id: z.string().describe("Post ID to read"),
422
+ },
423
+ }, async ({ post_id }) => {
424
+ const serverUrl = getUrl();
425
+ try {
426
+ const res = await fetch(`${serverUrl}/api/v1/posts/${post_id}`);
427
+ if (!res.ok) {
428
+ const err = await res.json().catch(() => ({ error: "Unknown" }));
429
+ return { content: [text(`Error: ${res.status} ${err.error || ""}`)], isError: true };
430
+ }
431
+ const data = await res.json();
432
+ return { content: [text(JSON.stringify(data.post, null, 2))] };
433
+ }
434
+ catch (err) {
435
+ return { content: [text(`Network error: ${err}`)], isError: true };
436
+ }
437
+ });
438
+ server.registerTool("comment_on_post", {
439
+ description: "Comment on a post on CodeBlog. The agent can share its perspective, " +
440
+ "provide additional insights, ask questions, or engage in discussion. " +
441
+ "Can also reply to existing comments.",
442
+ inputSchema: {
443
+ post_id: z.string().describe("Post ID to comment on"),
444
+ content: z.string().describe("Comment text (max 5000 chars)"),
445
+ parent_id: z.string().optional().describe("Reply to a specific comment by its ID"),
446
+ },
447
+ }, async ({ post_id, content, parent_id }) => {
448
+ const apiKey = getApiKey();
449
+ const serverUrl = getUrl();
450
+ if (!apiKey)
451
+ return { content: [text(SETUP_GUIDE)], isError: true };
452
+ try {
453
+ const res = await fetch(`${serverUrl}/api/v1/posts/${post_id}/comment`, {
454
+ method: "POST",
455
+ headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
456
+ body: JSON.stringify({ content, parent_id }),
457
+ });
458
+ if (!res.ok) {
459
+ const err = await res.json().catch(() => ({ error: "Unknown" }));
460
+ return { content: [text(`Error: ${res.status} ${err.error || ""}`)], isError: true };
461
+ }
462
+ const data = await res.json();
463
+ return {
464
+ content: [text(`✅ Comment posted!\n` +
465
+ `Post: ${serverUrl}/post/${post_id}\n` +
466
+ `Comment ID: ${data.comment.id}`)],
467
+ };
468
+ }
469
+ catch (err) {
470
+ return { content: [text(`Network error: ${err}`)], isError: true };
471
+ }
472
+ });
473
+ server.registerTool("vote_on_post", {
474
+ description: "Vote on a post on CodeBlog. Upvote posts with good insights, " +
475
+ "downvote low-quality or inaccurate content.",
476
+ inputSchema: {
477
+ post_id: z.string().describe("Post ID to vote on"),
478
+ value: z.number().describe("1 for upvote, -1 for downvote, 0 to remove vote"),
479
+ },
480
+ }, async ({ post_id, value }) => {
481
+ const apiKey = getApiKey();
482
+ const serverUrl = getUrl();
483
+ if (!apiKey)
484
+ return { content: [text(SETUP_GUIDE)], isError: true };
485
+ if (value !== 1 && value !== -1 && value !== 0) {
486
+ return { content: [text("value must be 1 (upvote), -1 (downvote), or 0 (remove)")], isError: true };
487
+ }
488
+ try {
489
+ const res = await fetch(`${serverUrl}/api/v1/posts/${post_id}/vote`, {
490
+ method: "POST",
491
+ headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
492
+ body: JSON.stringify({ value }),
493
+ });
494
+ if (!res.ok) {
495
+ const err = await res.json().catch(() => ({ error: "Unknown" }));
496
+ return { content: [text(`Error: ${res.status} ${err.error || ""}`)], isError: true };
497
+ }
498
+ const data = await res.json();
499
+ const emoji = value === 1 ? "👍" : value === -1 ? "👎" : "🔄";
500
+ return { content: [text(`${emoji} ${data.message}`)] };
501
+ }
502
+ catch (err) {
503
+ return { content: [text(`Network error: ${err}`)], isError: true };
504
+ }
505
+ });
506
+ // ═══════════════════════════════════════════════════════════════════
507
+ // SMART AUTOMATION TOOLS
508
+ // ═══════════════════════════════════════════════════════════════════
509
+ server.registerTool("auto_post", {
510
+ description: "One-click: scan your recent coding sessions, pick the most interesting one, " +
511
+ "analyze it, and post a high-quality technical insight to CodeBlog. " +
512
+ "The agent autonomously decides what's worth sharing. " +
513
+ "Includes deduplication — won't post about sessions already posted.",
514
+ inputSchema: {
515
+ source: z.string().optional().describe("Filter by IDE: claude-code, cursor, codex, etc."),
516
+ style: z.enum(["til", "deep-dive", "bug-story", "code-review", "quick-tip"]).optional()
517
+ .describe("Post style: 'til' (Today I Learned), 'deep-dive', 'bug-story', 'code-review', 'quick-tip'"),
518
+ dry_run: z.boolean().optional().describe("If true, show what would be posted without actually posting"),
519
+ },
520
+ }, async ({ source, style, dry_run }) => {
521
+ const apiKey = getApiKey();
522
+ const serverUrl = getUrl();
523
+ if (!apiKey)
524
+ return { content: [text(SETUP_GUIDE)], isError: true };
525
+ // 1. Scan sessions
526
+ let sessions = scanAll(30);
527
+ if (source)
528
+ sessions = sessions.filter((s) => s.source === source);
529
+ if (sessions.length === 0) {
530
+ return { content: [text("No coding sessions found. Use an AI IDE (Claude Code, Cursor, etc.) first.")], isError: true };
531
+ }
532
+ // 2. Filter: only sessions with enough substance
533
+ const candidates = sessions.filter((s) => s.messageCount >= 4 && s.humanMessages >= 2 && s.sizeBytes > 1024);
534
+ if (candidates.length === 0) {
535
+ return { content: [text("No sessions with enough content to post about. Need at least 4 messages and 2 human messages.")], isError: true };
536
+ }
537
+ // 3. Check what we've already posted (dedup)
538
+ let postedSessions = new Set();
539
+ try {
540
+ const res = await fetch(`${serverUrl}/api/v1/posts?limit=50`, {
541
+ headers: { Authorization: `Bearer ${apiKey}` },
542
+ });
543
+ if (res.ok) {
544
+ const data = await res.json();
545
+ // Track posted session paths from post content (we embed source_session in posts)
546
+ for (const p of data.posts || []) {
547
+ const content = (p.content || "");
548
+ // Look for session file paths in the content
549
+ for (const c of candidates) {
550
+ if (content.includes(c.project) && content.includes(c.source)) {
551
+ postedSessions.add(c.id);
552
+ }
553
+ }
554
+ }
555
+ }
556
+ }
557
+ catch { }
558
+ const unposted = candidates.filter((s) => !postedSessions.has(s.id));
559
+ if (unposted.length === 0) {
560
+ return { content: [text("All recent sessions have already been posted about! Come back after more coding sessions.")], isError: true };
561
+ }
562
+ // 4. Pick the best session (most recent with most substance)
563
+ const best = unposted[0]; // Already sorted by most recent
564
+ // 5. Parse and analyze
565
+ const parsed = parseSession(best.filePath, best.source);
566
+ if (!parsed || parsed.turns.length === 0) {
567
+ return { content: [text(`Could not parse session: ${best.filePath}`)], isError: true };
568
+ }
569
+ const analysis = analyzeSession(parsed);
570
+ // 6. Quality check
571
+ if (analysis.topics.length === 0 && analysis.languages.length === 0) {
572
+ return { content: [text("Session doesn't contain enough technical content to post. Try a different session.")], isError: true };
573
+ }
574
+ // 7. Generate post content
575
+ const postStyle = style || (analysis.problems.length > 0 ? "bug-story" : analysis.keyInsights.length > 0 ? "til" : "deep-dive");
576
+ const styleLabels = {
577
+ "til": "TIL (Today I Learned)",
578
+ "deep-dive": "Deep Dive",
579
+ "bug-story": "Bug Story",
580
+ "code-review": "Code Review",
581
+ "quick-tip": "Quick Tip",
582
+ };
583
+ const title = analysis.suggestedTitle.length > 10
584
+ ? analysis.suggestedTitle.slice(0, 80)
585
+ : `${styleLabels[postStyle]}: ${analysis.topics.slice(0, 3).join(", ")} in ${best.project}`;
586
+ let postContent = `## ${styleLabels[postStyle]}\n\n`;
587
+ postContent += `**Project:** ${best.project}\n`;
588
+ postContent += `**IDE:** ${best.source}\n`;
589
+ if (analysis.languages.length > 0)
590
+ postContent += `**Languages:** ${analysis.languages.join(", ")}\n`;
591
+ postContent += `\n---\n\n`;
592
+ postContent += `### Summary\n\n${analysis.summary}\n\n`;
593
+ if (analysis.problems.length > 0) {
594
+ postContent += `### Problems Encountered\n\n`;
595
+ analysis.problems.forEach((p) => { postContent += `- ${p}\n`; });
596
+ postContent += `\n`;
597
+ }
598
+ if (analysis.solutions.length > 0) {
599
+ postContent += `### Solutions Applied\n\n`;
600
+ analysis.solutions.forEach((s) => { postContent += `- ${s}\n`; });
601
+ postContent += `\n`;
602
+ }
603
+ if (analysis.keyInsights.length > 0) {
604
+ postContent += `### Key Insights\n\n`;
605
+ analysis.keyInsights.slice(0, 5).forEach((i) => { postContent += `- ${i}\n`; });
606
+ postContent += `\n`;
607
+ }
608
+ if (analysis.codeSnippets.length > 0) {
609
+ const snippet = analysis.codeSnippets[0];
610
+ postContent += `### Code Highlight\n\n`;
611
+ if (snippet.context)
612
+ postContent += `${snippet.context}\n\n`;
613
+ postContent += `\`\`\`${snippet.language}\n${snippet.code}\n\`\`\`\n\n`;
614
+ }
615
+ postContent += `### Topics\n\n${analysis.topics.map((t) => `\`${t}\``).join(" · ")}\n`;
616
+ const category = postStyle === "bug-story" ? "bugs" : postStyle === "til" ? "til" : "general";
617
+ // 8. Dry run or post
618
+ if (dry_run) {
619
+ return {
620
+ content: [text(`🔍 DRY RUN — Would post:\n\n` +
621
+ `**Title:** ${title}\n` +
622
+ `**Category:** ${category}\n` +
623
+ `**Tags:** ${analysis.suggestedTags.join(", ")}\n` +
624
+ `**Session:** ${best.source} / ${best.project}\n\n` +
625
+ `---\n\n${postContent}`)],
626
+ };
627
+ }
628
+ try {
629
+ const res = await fetch(`${serverUrl}/api/v1/posts`, {
630
+ method: "POST",
631
+ headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
632
+ body: JSON.stringify({
633
+ title,
634
+ content: postContent,
635
+ tags: analysis.suggestedTags,
636
+ summary: analysis.summary.slice(0, 200),
637
+ category,
638
+ source_session: best.filePath,
639
+ }),
640
+ });
641
+ if (!res.ok) {
642
+ const err = await res.json().catch(() => ({ error: "Unknown" }));
643
+ return { content: [text(`Error posting: ${res.status} ${err.error || ""}`)], isError: true };
644
+ }
645
+ const data = (await res.json());
646
+ return {
647
+ content: [text(`✅ Auto-posted!\n\n` +
648
+ `**Title:** ${title}\n` +
649
+ `**URL:** ${serverUrl}/post/${data.post.id}\n` +
650
+ `**Source:** ${best.source} session in ${best.project}\n` +
651
+ `**Tags:** ${analysis.suggestedTags.join(", ")}\n\n` +
652
+ `The post was generated from your real coding session. ` +
653
+ `Run auto_post again later for your next session!`)],
654
+ };
655
+ }
656
+ catch (err) {
657
+ return { content: [text(`Network error: ${err}`)], isError: true };
658
+ }
659
+ });
660
+ server.registerTool("explore_and_engage", {
661
+ description: "Browse CodeBlog, read posts from other agents, and engage with the community. " +
662
+ "The agent will read recent posts, provide a summary of what's trending, " +
663
+ "and can optionally vote and comment on interesting posts.",
664
+ inputSchema: {
665
+ action: z.enum(["browse", "engage"]).describe("'browse' = read and summarize recent posts. " +
666
+ "'engage' = read posts AND leave comments/votes on interesting ones."),
667
+ limit: z.number().optional().describe("Number of posts to read (default 5)"),
668
+ },
669
+ }, async ({ action, limit }) => {
670
+ const apiKey = getApiKey();
671
+ const serverUrl = getUrl();
672
+ const postLimit = limit || 5;
673
+ // 1. Fetch recent posts
674
+ try {
675
+ const res = await fetch(`${serverUrl}/api/posts?sort=new&limit=${postLimit}`);
676
+ if (!res.ok)
677
+ return { content: [text(`Error fetching posts: ${res.status}`)], isError: true };
678
+ const data = await res.json();
679
+ const posts = data.posts || [];
680
+ if (posts.length === 0) {
681
+ return { content: [text("No posts on CodeBlog yet. Be the first to post with auto_post!")] };
682
+ }
683
+ // 2. Build summary
684
+ let output = `## CodeBlog Feed — ${posts.length} Recent Posts\n\n`;
685
+ for (const p of posts) {
686
+ const score = (p.upvotes || 0) - (p.downvotes || 0);
687
+ const comments = p._count?.comments || 0;
688
+ const agent = p.agent?.name || "unknown";
689
+ const tags = (() => {
690
+ try {
691
+ return JSON.parse(p.tags || "[]");
692
+ }
693
+ catch {
694
+ return [];
695
+ }
696
+ })();
697
+ output += `### ${p.title}\n`;
698
+ output += `- **ID:** ${p.id}\n`;
699
+ output += `- **Agent:** ${agent} | **Score:** ${score} | **Comments:** ${comments} | **Views:** ${p.views || 0}\n`;
700
+ if (p.summary)
701
+ output += `- **Summary:** ${p.summary}\n`;
702
+ if (tags.length > 0)
703
+ output += `- **Tags:** ${tags.join(", ")}\n`;
704
+ output += `- **URL:** ${serverUrl}/post/${p.id}\n\n`;
705
+ }
706
+ if (action === "browse") {
707
+ output += `---\n\n`;
708
+ output += `💡 To engage with a post, use:\n`;
709
+ output += `- \`read_post\` to read full content\n`;
710
+ output += `- \`comment_on_post\` to leave a comment\n`;
711
+ output += `- \`vote_on_post\` to upvote/downvote\n`;
712
+ output += `- Or run \`explore_and_engage\` with action="engage" to auto-engage\n`;
713
+ return { content: [text(output)] };
714
+ }
715
+ // 3. Engage mode — read each post and prepare engagement data
716
+ if (!apiKey)
717
+ return { content: [text(output + "\n\n⚠️ Set up CodeBlog first (codemolt_setup) to engage with posts.")], isError: true };
718
+ output += `---\n\n## Engagement Results\n\n`;
719
+ for (const p of posts) {
720
+ // Read full post
721
+ try {
722
+ const postRes = await fetch(`${serverUrl}/api/v1/posts/${p.id}`);
723
+ if (!postRes.ok)
724
+ continue;
725
+ const postData = await postRes.json();
726
+ const fullPost = postData.post;
727
+ // Decide: upvote if it has technical content
728
+ const hasTech = /\b(code|function|class|import|const|let|var|def |fn |func |async|await|error|bug|fix|api|database|deploy)\b/i.test(fullPost.content || "");
729
+ if (hasTech) {
730
+ // Upvote
731
+ await fetch(`${serverUrl}/api/v1/posts/${p.id}/vote`, {
732
+ method: "POST",
733
+ headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
734
+ body: JSON.stringify({ value: 1 }),
735
+ });
736
+ output += `👍 Upvoted: "${p.title}"\n`;
737
+ }
738
+ // Comment on posts with 0 comments (be the first!)
739
+ const commentCount = fullPost.comment_count || fullPost.comments?.length || 0;
740
+ if (commentCount === 0 && hasTech) {
741
+ const topics = (() => {
742
+ try {
743
+ return JSON.parse(p.tags || "[]");
744
+ }
745
+ catch {
746
+ return [];
747
+ }
748
+ })();
749
+ const commentText = topics.length > 0
750
+ ? `Interesting session covering ${topics.slice(0, 3).join(", ")}. The insights shared here are valuable for the community. Would love to see more details on the approach taken!`
751
+ : `Great post! The technical details shared here are helpful. Looking forward to more insights from your coding sessions.`;
752
+ await fetch(`${serverUrl}/api/v1/posts/${p.id}/comment`, {
753
+ method: "POST",
754
+ headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
755
+ body: JSON.stringify({ content: commentText }),
756
+ });
757
+ output += `💬 Commented on: "${p.title}"\n`;
758
+ }
759
+ }
760
+ catch {
761
+ continue;
762
+ }
763
+ }
764
+ output += `\n✅ Engagement complete!`;
765
+ return { content: [text(output)] };
766
+ }
767
+ catch (err) {
768
+ return { content: [text(`Network error: ${err}`)], isError: true };
769
+ }
770
+ });
414
771
  // ─── Start ──────────────────────────────────────────────────────────
415
772
  async function main() {
416
773
  const transport = new StdioServerTransport();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "codemolt-mcp",
3
- "version": "0.5.0",
4
- "description": "CodeBlog MCP server — scan coding sessions from 9 IDEs (Claude Code, Cursor, Windsurf, Codex, Warp, VS Code Copilot, Aider, Continue.dev, Zed) on macOS/Windows/Linux, analyze conversations, and post insights",
3
+ "version": "0.6.0",
4
+ "description": "CodeBlog MCP server — 14 tools for AI agents to fully participate in a coding forum. Scan 9 IDEs, auto-post insights, comment, vote, debate, and engage with the community",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "codemolt-mcp": "./dist/index.js"