How I Quickly Turn CSV Files Into Charts (Without Excel or Coding)

How I Quickly Turn CSV Files Into Charts (Without Excel or Coding)
A few days ago, I opened a CSV file thinking it would take me 2 minutes to visualize the data.
I was wrong.
I tried Excel… menus everywhere.
I thought about using Python… but that felt like overkill for a simple chart.
All I really wanted was this:
👉 Upload file → select columns → see a chart

The Problem

If you’ve worked with CSV files, you’ve probably faced this too:

  • Excel takes too many steps
  • Formatting breaks easily
  • Python requires setup and coding

It felt like there should be a simpler way.

What I Did

After trying a few options, I realized something:
There wasn’t a simple tool that just lets you upload a CSV and instantly see a chart.
So I built one.
It’s called DataPlotter - a simple tool where you can upload a CSV or Excel file and generate charts instantly in your browser.

👉 https://dataplotter.de

I originally built it just to solve my own frustration, but it turned out to be really useful for quick visualizations.

How It Works

1. Prepare Your CSV

Make sure:

  • First row has column names
  • Data is clean (no symbols like $ or commas)

Example:
X, Y
1, 5
2, 7
3, 6

2. Upload Your File
Upload your CSV or Excel file into the tool.

3. Select X and Y

Choose:

  • X-axis
  • Y-axis

Your chart appears instantly.

4. Customize (Optional)
You can:
Switch chart types
Add labels
Adjust colors

Final Thoughts

CSV files are everywhere, but raw data isn’t very helpful on its own.
For me, the goal was simple: remove friction.
No installs. No coding. No unnecessary steps.
That’s exactly why I built DataPlotter.
If you just want a quick chart without the usual hassle, this approach works really well.

Try it here
👉 https://dataplotter.de

How I used Next.js and Claude 3.5 to stop my PM from nagging me about Jira

We’ve all been there.

It’s 10:30 AM. You drop your update in the #daily-standup Slack channel: “Finally squashed that weird auth bug on the login portal, moving on to the database migration.”

Two hours later, your Product Manager DMs you: “Hey, awesome job on the auth bug! Did you remember to move the ticket to Done in Jira?”

Context switching is the absolute worst part of being a developer. We are already communicating our status naturally in Slack—why do we have to open a new tab, wait for Jira or Linear to load, find the right sprint, and click a button just to say the exact same thing?

Last weekend, I decided I’d had enough. I built a Slack bot that reads my daily updates, figures out which ticket I’m talking about, and auto-closes it for me.

Here is how I built it using Next.js, the Slack Events API, and Claude 3.5 Sonnet.

The Architecture

The flow is surprisingly simple, but the execution requires some strict timing:

  1. Developer posts a message in a monitored Slack channel.
  2. Slack fires a webhook to my Next.js API route.
  3. The API fetches the user’s currently open tasks from Jira/Linear.
  4. Claude 3.5 evaluates the Slack message against the open tasks to see if there’s a match.
  5. If confidence is > 95%, the bot hits the PM tool’s API to mark it “Done” and replies in Slack.

Challenge 1: Beating the Slack 3-Second Timeout

Slack’s Events API is notoriously strict. When they send a webhook payload, you have exactly 3 seconds to respond with a 200 OK, or Slack assumes your app is dead and retries the event (which leads to duplicate AI calls and chaos).

Because calling the Anthropic API and the Jira API takes way longer than 3 seconds, you cannot await the AI processing in the main thread.

To solve this in Next.js (deployed on Vercel), I used the waitUntil function from @vercel/functions. This allows the serverless function to return the 200 OK immediately to Slack, while keeping the execution environment alive to finish the AI pipeline in the background:

// src/app/api/webhooks/slack/route.ts
import { waitUntil } from '@vercel/functions';
import { NextRequest, NextResponse } from 'next/server';
import { processSlackMessage } from '@/lib/ai/pipeline';

export const POST = async (request: NextRequest): Promise<NextResponse> => {
  // ... verify signatures and parse payload ...

  const event = payload.event;

  if (event.type === 'message') {
    // 1. Queue the heavy AI processing in the background
    waitUntil(
      processSlackMessage({
        teamId: payload.team_id,
        channelId: event.channel,
        userId: event.user,
        messageText: event.text,
        messageTs: event.ts,
      }).catch(console.error)
    );
  }

  // 2. Immediately return 200 OK to satisfy Slack's 3-second rule
  return NextResponse.json({ ok: true });
};

Challenge 2: Stopping the AI from Hallucinating Tasks

The scariest part of this build was the idea of an AI accidentally closing the wrong ticket. If a developer says “I’m going to start working on the header tomorrow”, the AI shouldn’t close the “Header Redesign” ticket just because the words match.

To fix this, I engineered a highly specific system prompt for Claude 3.5. Instead of just asking “does this match?”, I force the LLM to return a strict JSON object with a calculated confidence_score.

I gave Claude very specific scoring guidelines:

// src/lib/ai/prompts.ts
const SYSTEM_PROMPT = `You are NudgeBot's task-matching engine. Given a Slack message and a list of the sender's open tasks, determine whether the message indicates that one or more tasks have been completed.

Return ONLY valid JSON in this exact shape:
{
  "matches": [
    {
      "task_id": "<uuid>",
      "confidence_score": <0.0–1.0>,
      "reasoning": "<1–2 sentences>"
    }
  ]
}

Scoring guidelines:
  0.95–1.00  Explicit: "finished X", "deployed X", PR merged for X
  0.80–0.94  Strong: past-tense action verbs ("shipped", "pushed") + context
  0.65–0.79  Moderate: implicit completion language, partial name overlap
  0.50–0.64  Weak: tangentially related, ambiguous phrasing

Rules:
- Past tense, "just", "finally", deployment language are positive signals.
- Questions, future tense ("will", "going to"), and negations ("not done") are NOT completions.
- Return empty matches if nothing matches above 0.50.`;

In the backend pipeline, if Claude returns a score of 0.95 or higher, the app automatically closes the ticket. If it’s between 0.70 and 0.94, the bot sends an ephemeral (private) message to the user in Slack with a button asking: “Looks like you finished the Auth Bug. Should I close the ticket for you?”

The “Trust” Feature (Audit Trails)

If you automate project management, your PMs will eventually get paranoid about why things are moving.

Whenever the bot closes a ticket, it makes a secondary API call to Jira/Linear to leave a comment on the ticket: ✅ Marked as done by NudgeBot based on a Slack update from @Aditya: "Finally squashed that weird auth bug."

Complete transparency, zero extra clicks.

Don’t want to build it yourself?

Building this was a super fun weekend project, but managing OAuth flows for Slack, Jira, Linear, Asana, and Trello is a massive headache.

Because we are developers and we appreciate not reinventing the wheel, I actually packaged this exact codebase into a 1-click installable SaaS.

If you want to stop your PMs from nagging you, you can invite the bot to your workspace and use it for free here: NudgeBot.xyz.

Let me know in the comments if you’d trust an AI to manage your project board!

Why Godot’s architecture makes it the best engine for AI-assisted development

Game engines weren’t designed with AI code assistants in mind. They were designed for humans clicking through visual editors. But some architectures happen to be far more readable to language models than others, and that gap matters now that 36% of game developers use AI tools at work (GDC 2026 State of the Industry).

Godot’s architecture is unusually good for AI-assisted workflows. Not because anyone planned it that way, but because the same design decisions that make Godot lightweight and hackable for humans also make it parseable and modifiable by language models.

Everything is human-readable text

Open a .tscn file (Godot’s scene format) in any text editor and you can read it:

[gd_scene load_steps=3 format=3]

[ext_resource type="Script" path="res://player.gd" id="1"]
[ext_resource type="Texture2D" path="res://icon.svg" id="2"]

[node name="Player" type="CharacterBody2D"]
script = ExtResource("1")
speed = 200.0

[node name="Sprite" type="Sprite2D" parent="."]
texture = ExtResource("2")

[node name="Collision" type="CollisionShape2D" parent="."]

Scripts are .gd files. Resources are .tres files. Project settings live in project.godot. All plain text, all version-controllable, all parseable by an LLM without any intermediate conversion step.

Compare that to Unity. Unity scenes are serialized YAML, but they reference objects by GUIDs that map to a separate .meta file for every asset in the project. A language model reading a Unity scene file sees something like m_Script: {fileID: 11500000, guid: a3b5d47f2c1e4a8b9d6f3e7c1a2b4d5e} and has no way to resolve what that GUID points to without crawling the entire asset database. Unreal stores assets in proprietary .uasset binary. AI tools working with Unreal typically need to operate at the C++ or Blueprint API level rather than reading project files directly.

This text-first approach means an AI assistant can read a Godot project the same way a developer does: open the files and understand the structure.

GDScript has one idiomatic way to do things

GDScript is a domain-specific language built for Godot. It has roughly 850 built-in classes with consistent naming conventions (snake_case everywhere, signals declared with the signal keyword, exported properties with @export).

That consistency matters for language models. When there’s one standard pattern for declaring movement logic, connecting signals, or building UI, the model doesn’t have to guess which of six possible approaches the developer is using.

extends CharacterBody2D

@export var speed := 200.0

func _physics_process(delta: float) -> void:
    var direction := Input.get_vector("left", "right", "up", "down")
    velocity = direction * speed
    move_and_slide()

This is the standard way to handle character movement in Godot 4. There’s no competing framework, no alternative input system, no dependency injection pattern. An AI model trained on Godot projects will see this pattern thousands of times.

Unity’s C# ecosystem is the opposite. You can use the old Input.GetAxis, the new Input System package, Rewired, InControl, or a custom abstraction. You can structure your project with MonoBehaviours, ScriptableObjects, ECS, or some hybrid. The language model has to figure out which stack the current project uses before it can write useful code.

The scene tree is a consistent data structure

Every Godot project is organized as a tree of nodes. A CharacterBody2D has a Sprite2D child and a CollisionShape2D child. A UI is a tree of Control nodes. This isn’t a convention; it’s the only way Godot works.

That structural consistency means an AI tool can reason about a Godot project at the architectural level. “Add a health bar to the player” translates to a deterministic operation: add Control children to the player’s scene tree, attach a script, connect the right signals. The AI doesn’t need to ask which UI framework you’re using because there’s only one.

Aspect Godot Unity Unreal
Scene format Text (.tscn) YAML with GUIDs Binary (.uasset)
Scripting GDScript (domain-specific) C# (general-purpose) C++ / Blueprints (visual)
Project structure Node tree (enforced) GameObject + Components (flexible) Actor + Component (flexible)
Plugin architecture GDExtension + EditorPlugin Package Manager + native plugins C++ modules + Blueprints
File readability Fully human-readable Partially (GUID references) Binary, not readable
Standard patterns ~1 way per task Multiple competing approaches Multiple competing approaches

The plugin system allows deep editor integration

Godot’s EditorPlugin API gives addons access to the editor itself: the scene tree dock, the inspector, the script editor, the output panel. A plugin can add custom docks, intercept editor events, and modify scenes programmatically.

This is why AI tools built for Godot can go beyond code generation. They can read the scene tree, create nodes, modify properties, and validate physics setups, all through the editor’s own API. Tools like Ziva use this to provide 30+ specialized tools that operate on the project at the engine level rather than just generating text in a chat window. That’s a fundamentally different capability than a generic AI assistant that can only paste code.

Unity’s editor is extensible too, but the extension points are more fragmented. Custom inspectors, editor windows, and asset processors each use different APIs. Unreal’s editor extension requires C++ and a full engine rebuild.

The open-source factor

Godot is MIT-licensed with over 102,000 GitHub stars. Every line of engine code is readable. When an AI tool needs to understand how a particular engine feature works internally, the source is right there.

This matters in practice. If an AI tool generates code that calls a Godot API in an unexpected way, a developer (or the AI itself) can read the engine source to understand why the behavior doesn’t match expectations. With proprietary engines, you’re limited to official documentation and community knowledge.

The open source model also means AI tools can be distributed as standard Godot plugins through the Asset Library, with no special licensing or SDK agreements. The barrier to building and sharing AI tools for Godot is just “write a plugin and publish it.”

This is already happening

The GDC 2026 State of the Industry report found that among developers who use AI, 81% use it for research and brainstorming and 47% use it for code assistance. Only 5% use AI for player-facing features. AI in game development is a productivity tool, not a content generator.

Godot’s architecture aligns with exactly those productivity use cases. The text-based file formats mean an AI can understand project structure. The consistent scripting language means generated code is more likely to be correct on the first try. The scene tree gives AI tools a reliable abstraction for reasoning about game architecture.

Godot went from 19% to 40% of GMTK Game Jam submissions between 2024 and 2025. Steam games shipped with Godot doubled year over year for three consecutive years. The engine is growing fast, and the developer tooling ecosystem is growing with it.

The engines that are easiest for AI to work with will attract the best AI tooling. And the engines with the best AI tooling will attract developers who want to move fast. Godot is positioned well on both counts.

Rust MCP Server Setup Guide for Vibe CLI

For anyone else trying to configure Mistral’s Vibe CLI combined with the Rust Analyzer MCP server (or any MCP server that runs into the naming conflict that the Rust Analyzer does) – hopefully this guide will save you the 5 hours it stole from my life.

This comprehensive guide documents all the steps required to get the Rust MCP server working with Vibe CLI, including fixes for naming conflicts, JSON parsing issues, and tool discovery problems.

Table of Contents

  1. Prerequisites
  2. Initial Setup
  3. Issue 1: Conflicting Binary
  4. Issue 2: JSON Parsing Errors
  5. Issue 3: Tool Name Prefixing
  6. Final Configuration
  7. Testing the Setup
  8. Troubleshooting
  9. Complete File Changes

Prerequisites

Before starting, ensure you have:

  • Rust toolchain installed (rustup, cargo)
  • Node.js 22.x+ (for some MCP servers)
  • Docker (for containerized MCP servers)
  • Vibe CLI installed
  • Git

Initial Setup

1. Clone the Rust MCP Server Repository

cd /Users/jordanhudgens/code/dashtrack/tools
git clone https://github.com/dexwritescode/rust-mcp
cd rust-mcp

2. Build the Server

cargo build --release

The binary will be located at: /Users/jordanhudgens/code/dashtrack/tools/rust-mcp/target/release/rustmcp

Issue 1: Conflicting Binary

Problem

Vibe CLI auto-discovers MCP servers in your PATH. If you have the official rust-analyzer-mcp binary installed via cargo, it will conflict with your custom server. I’m assuming it’s happening with other MCP servers, this is just the one I ran into.

Symptoms

  • Tools appear with rust-analyzer_* prefix
  • Naming conflicts between auto-discovered and configured servers
  • “Unknown tool” errors

Solution

Rename the conflicting binary:

mv ~/.cargo/bin/rust-analyzer-mcp ~/.cargo/bin/rust-analyzer-mcp-backup

Verification

which rust-analyzer-mcp  # Should return "rust-analyzer-mcp not found"
ls ~/.cargo/bin/rust-analyzer-mcp-backup  # Should show the renamed binary

Issue 2: JSON Parsing Errors

Problem

The Rust MCP server outputs plain text startup messages that break Vibe’s JSON-RPC parser.

Symptoms

Failed to parse JSONRPC message from server
pydantic_core._pydantic_core.ValidationError: 1 validation error for JSONRPCMessage
  Invalid JSON: expected value at line 1 column 1 [type=json_invalid, input_value='Starting Rust MCP Server', input_type=str]

Solution

Remove the println! statements from src/main.rs:

File: /Users/jordanhudgens/code/dashtrack/tools/rust-mcp/src/main.rs

// BEFORE (lines 12-13):
println!("Starting Rust MCP Server");
println!("Server running on stdio transport...");

// AFTER (remove these lines completely):
// No startup messages - MCP servers should only output JSON-RPC

Complete Fixed main.rs

use anyhow::Result;
use rmcp::{ServiceExt, transport::stdio};
use rustmcp::server::RustMcpServer;

#[tokio::main]
async fn main() -> Result<()> {
    // Initialize the rust-analyzer integration
    let mut rust_server = RustMcpServer::new();
    rust_server.start().await?;

    // Note: The #[tool] macros generate additional tools beyond our manual list
    // Start the MCP server using the ServiceExt trait
    let service = rust_server.serve(stdio()).await?;
    service.waiting().await?;

    Ok(())
}

Issue 3: Tool Name Prefixing

Problem

Initial assumption was that Vibe prefixes tool names with the server name (e.g., rustmcp_workspace_symbols), but this turned out to be incorrect.

Discovery Process

  1. Initial hypothesis: Vibe prefixes tools with server name
  2. Implemented stripping: Added splitn(2, '_').nth(1).unwrap_or(name)
  3. Found issue: Tools were being called as just symbols instead of workspace_symbols
  4. Realization: Vibe does NOT prefix tool names
  5. Final fix: Removed the prefix stripping logic entirely

Solution

Remove the prefix stripping logic from src/tools/types.rs:

File: /Users/jordanhudgens/code/dashtrack/tools/rust-mcp/src/tools/types.rs

// BEFORE:
pub async fn execute_tool(
    name: &str,
    args: Value,
    analyzer: &mut RustAnalyzerClient,
) -> Result<ToolResult> {
    // Strip any MCP host prefix (e.g. "rustmcp_analyze_manifest" → "analyze_manifest")
    let name = name.splitn(2, '_').nth(1).unwrap_or(name);
    match name {

// AFTER:
pub async fn execute_tool(
    name: &str,
    args: Value,
    analyzer: &mut RustAnalyzerClient,
) -> Result<ToolResult> {
    // Strip any MCP host prefix (e.g. "rustmcp_analyze_manifest" → "analyze_manifest")
    // Vibe doesn't actually prefix tool names, so we don't need to strip anything
    match name {

Final Configuration

1. Update Vibe Config

File: /Users/jordanhudgens/.vibe/config.toml

Add the Rust MCP server configuration:

[[mcp_servers]]
name = "rustmcp"
transport = "stdio"
command = "/Users/jordanhudgens/code/dashtrack/tools/rust-mcp/target/release/rustmcp"
args = []
startup_timeout_sec = 30.0

2. Rebuild the Server

cd /Users/jordanhudgens/code/dashtrack/tools/rust-mcp
cargo build --release

3. Restart Terminal

Close and reopen your terminal to ensure all environment changes take effect.

Testing the Setup

Test Basic Functionality

vibe

The Vibe CLI should start without JSON parsing errors.

Test Specific Tools

Workspace Symbols

rustmcp_workspace_symbols

With parameters:

{"query": "test"}

Find Definition

rustmcp_find_definition

With parameters:

{"file_path": "/path/to/file.rs", "line": 10, "character": 5}

Get Diagnostics

rustmcp_get_diagnostics

With parameters:

{"file_path": "/path/to/file.rs"}

Expected Results

  • No JSON parsing errors
  • Tools should execute and return proper responses
  • No “Unknown tool” errors
  • Server should be discovered as rustmcp with all available tools

Troubleshooting

“Unknown tool” Errors

Cause: Tool name mismatch or server not properly initialized.

Solution:

  1. Verify the server is running: ps aux | grep rustmcp
  2. Check tool names in the server code
  3. Ensure no prefix stripping logic is active

JSON Parsing Errors

Cause: Server outputting non-JSON text.

Solution:

  1. Remove all println!, eprintln!, and print! statements
  2. Ensure only JSON-RPC messages are output
  3. Check for any debug statements that might output to stdout

Server Not Discovered

Cause: Configuration issue or binary path incorrect.

Solution:

  1. Verify the binary path in config.toml
  2. Ensure the binary is executable: chmod +x /Users/jordanhudgens/code/dashtrack/tools/rust-mcp/target/release/rustmcp
  3. Check for typos in the server name

Conflicting Tools

Cause: Multiple MCP servers with overlapping tool names.

Solution:

  1. Ensure no other rust-analyzer-mcp binary exists in PATH
  2. Check for auto-discovered servers with vibe --debug
  3. Remove or rename conflicting binaries

Complete File Changes

1. /Users/jordanhudgens/code/dashtrack/tools/rust-mcp/src/main.rs

Before:

use anyhow::Result;
use rmcp::{ServiceExt, transport::stdio};
use rustmcp::server::RustMcpServer;

#[tokio::main]
async fn main() -> Result<()> {
    // Initialize the rust-analyzer integration
    let mut rust_server = RustMcpServer::new();
    rust_server.start().await?;

    // Note: The #[tool] macros generate additional tools beyond our manual list
    println!("Starting Rust MCP Server");
    println!("Server running on stdio transport...");

    // Start the MCP server using the ServiceExt trait
    let service = rust_server.serve(stdio()).await?;
    service.waiting().await?;

    Ok(())
}

After:

use anyhow::Result;
use rmcp::{ServiceExt, transport::stdio};
use rustmcp::server::RustMcpServer;

#[tokio::main]
async fn main() -> Result<()> {
    // Initialize the rust-analyzer integration
    let mut rust_server = RustMcpServer::new();
    rust_server.start().await?;

    // Note: The #[tool] macros generate additional tools beyond our manual list
    // Start the MCP server using the ServiceExt trait
    let service = rust_server.serve(stdio()).await?;
    service.waiting().await?;

    Ok(())
}

Changes: Removed lines 12-13 (println! statements)

2. /Users/jordanhudgens/code/dashtrack/tools/rust-mcp/src/tools/types.rs

Before:

pub async fn execute_tool(
    name: &str,
    args: Value,
    analyzer: &mut RustAnalyzerClient,
) -> Result<ToolResult> {
    match name {

After:

pub async fn execute_tool(
    name: &str,
    args: Value,
    analyzer: &mut RustAnalyzerClient,
) -> Result<ToolResult> {
    // Strip any MCP host prefix (e.g. "rustmcp_analyze_manifest" → "analyze_manifest")
    // Vibe doesn't actually prefix tool names, so we don't need to strip anything
    match name {

Changes: Added comment explaining that no prefix stripping is needed

3. /Users/jordanhudgens/.vibe/config.toml

Added:

[[mcp_servers]]
name = "rustmcp"
transport = "stdio"
command = "/Users/jordanhudgens/code/dashtrack/tools/rust-mcp/target/release/rustmcp"
args = []
startup_timeout_sec = 30.0

4. Renamed Binary

mv ~/.cargo/bin/rust-analyzer-mcp ~/.cargo/bin/rust-analyzer-mcp-backup

Available Rust MCP Tools

The Rust MCP server provides the following tools:

Analysis Tools

  • find_definition – Find the definition of a symbol
  • find_references – Find all references to a symbol
  • get_diagnostics – Get compiler diagnostics for a file
  • get_type_hierarchy – Get type hierarchy for a symbol
  • validate_lifetimes – Validate and suggest lifetime annotations

Navigation Tools

  • workspace_symbols – Search for symbols in the workspace
  • find_definition – Find symbol definitions

Refactoring Tools

  • rename_symbol – Rename a symbol with scope awareness
  • extract_function – Extract selected code into a new function
  • inline_function – Inline a function call
  • change_signature – Change the signature of a function
  • organize_imports – Organize and sort import statements

Code Generation Tools

  • generate_struct – Generate a struct with specified fields
  • generate_enum – Generate an enum with specified variants
  • generate_trait_impl – Generate a trait implementation
  • generate_tests – Generate unit tests for a function

Project Tools

  • analyze_manifest – Parse and analyze Cargo.toml
  • run_cargo_check – Execute cargo check
  • suggest_dependencies – Suggest crate dependencies
  • create_module – Create a new Rust module
  • move_items – Move code items between files

Formatting Tools

  • format_code – Apply rustfmt formatting
  • apply_clippy_suggestions – Apply clippy lint suggestions

Maintenance

Updating the Server

cd /Users/jordanhudgens/code/dashtrack/tools/rust-mcp
git pull origin main
cargo build --release

Reapplying Fixes After Update

If you update the server and the issues reappear, reapply the fixes:

  1. Remove startup messages (if they were re-added):
   # Edit src/main.rs and remove any println! statements
  1. Ensure no prefix stripping (if it was re-added):
   # Edit src/tools/types.rs and remove any prefix stripping logic
  1. Rebuild:
   cargo build --release

Conclusion

This guide documents the complete process to get the Rust MCP server working with Vibe CLI. The key issues were:

  1. Conflicting binary in PATH causing auto-discovery conflicts
  2. JSON parsing errors from startup messages breaking the protocol
  3. Incorrect assumption about tool name prefixing leading to unnecessary stripping logic

With these issues resolved, the Rust MCP server provides full Rust analysis capabilities directly within Vibe CLI.

References

I built a health check for inherited codebases — and vibe-coded apps that now became important

I have just launched repowatch — repowatch.io 🎉

A quick, lightweight health check for your (or inherited) codebase.
Simply upload a .zip or connect to your GitHub and Gitlab repos and get a scorecard across: code quality, test confidence, security basics, and AI-risk signals. Heck, Download a full recommendations report in paid tiers, have at it.

Built for the slightly chaotic reality of inherited repos, shadow AI, and vibe-coded apps that somehow ended up important. It’s still early, but live and looking forward to any feedback.

Keen to see whether this actually lands for teams dealing with that kind of challenge. If you want to give it a spin, use code TESTWEEK at checkout for 99%! 😲 off your first month, but only until 1 April.

Thanks dev.to community,

45 MCP Tools: Everything Your Claude Agent Can Do with a Wallet

Ever tried asking Claude to check your crypto balance or swap tokens? You probably got a polite “I can’t access external wallets” response. That changes today.

WAIaaS is an MCP server that gives Claude Desktop a complete wallet toolkit. Add one configuration block, and your AI assistant can check balances, send transactions, execute DeFi strategies, and manage NFTs — all through natural conversation.

Why MCP + Wallets Matter

Claude’s Model Context Protocol (MCP) lets you extend the AI with custom tools. But most MCP servers focus on file systems, databases, or APIs. WAIaaS fills the blockchain gap.

Instead of switching between Claude for analysis and MetaMask for execution, you get both in one interface. Ask Claude to “check my DeFi positions and rebalance if necessary” — and it can actually do it.

The stakes are real money, so WAIaaS includes production-grade security: policy engines, approval workflows, and default-deny enforcement. Your agent gets superpowers, but you stay in control.

45 MCP Tools in One Package

WAIaaS provides 45 MCP tools across five categories:

Wallet Management (8 tools)

  • get-balance — Native token balance
  • get-assets — All token balances with prices
  • get-address — Wallet address and chain info
  • get-wallet-info — Complete wallet overview
  • list-nfts — NFT collection with metadata
  • get-nft-metadata — Individual NFT details
  • get-health-factor — DeFi health across protocols
  • resolve-asset — Token info by symbol/address

Transaction Operations (11 tools)

  • send-token — Native and token transfers
  • transfer-nft — NFT transfers with metadata
  • send-batch — Multiple transactions atomically
  • approve-token — Token spending approvals
  • call-contract — Arbitrary contract calls
  • sign-transaction — Raw transaction signing
  • sign-message — Message signing for auth
  • simulate-transaction — Dry-run before execution
  • get-transaction — Transaction details by ID
  • list-transactions — Transaction history
  • get-nonce — Account nonce for advanced usage

DeFi Integration (8 tools)

  • get-defi-positions — All lending/staking positions
  • action-provider — Execute DeFi actions
  • hyperliquid — Perpetual futures trading
  • polymarket — Prediction market trades
  • get-provider-status — DeFi protocol health
  • list-offchain-actions — Available DeFi operations
  • encode-calldata — Generate transaction data
  • x402-fetch — HTTP requests with automatic payment

Here’s Claude executing a Jupiter swap on Solana:

# Behind the scenes when Claude calls action-provider
curl -X POST http://127.0.0.1:3100/v1/actions/jupiter-swap/swap 
  -H "Content-Type: application/json" 
  -H "Authorization: Bearer wai_sess_<token>" 
  -d '{
    "inputMint": "So11111111111111111111111111111111111111112",
    "outputMint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
    "amount": "100000000"
  }'

Advanced Features (10 tools)

  • build-userop — ERC-4337 Account Abstraction
  • sign-userop — Gasless transaction signing
  • erc8004-get-agent-info — Onchain agent reputation
  • erc8004-get-reputation — Trust scoring
  • erc8004-get-validation-status — Agent validation
  • erc8128-sign-request — HTTP request signing
  • erc8128-verify-signature — Signature verification
  • wc-connect — WalletConnect pairing
  • wc-disconnect — WalletConnect termination
  • wc-status — Connection status

System Management (8 tools)

  • get-policies — Security policy overview
  • list-sessions — Active agent sessions
  • list-credentials — Stored credentials
  • connect-info — Network and RPC status
  • get-rpc-proxy-url — Blockchain RPC access
  • list-incoming-transactions — Deposit monitoring
  • get-incoming-summary — Deposit analytics
  • get-tokens — Supported token list

Quick Setup: 5 Minutes to AI Wallet

Step 1: Install WAIaaS

npm install -g @waiaas/cli
waiaas init
waiaas start

Step 2: Create a wallet and session

waiaas quickset --mode mainnet

This creates wallets for both Ethereum and Solana, plus MCP sessions for each. You’ll see output like:

Created wallet: ethereum-mainnet (0x742d35Cc...)
Created wallet: solana-mainnet (5Fz9k2bN...)
Created session: wai_sess_eyJhbGciOiJIUzI1NiJ9...

Step 3: Configure Claude Desktop

Add this to ~/Library/Application Support/Claude/claude_desktop_config.json:

{
  "mcpServers": {
    "waiaas": {
      "command": "npx",
      "args": ["-y", "@waiaas/mcp"],
      "env": {
        "WAIAAS_BASE_URL": "http://127.0.0.1:3100",
        "WAIAAS_SESSION_TOKEN": "wai_sess_eyJhbGciOiJIUzI1NiJ9...",
        "WAIAAS_DATA_DIR": "~/.waiaas"
      }
    }
  }
}

Or use the auto-setup command:

waiaas mcp setup --all

Step 4: Restart Claude Desktop

Claude will now see 45 new tools. The MCP connection indicator shows “waiaas” as connected.

Step 5: Try it out

Open a new Claude conversation and ask:

  • “Check my wallet balance”
  • “What tokens do I own?”
  • “Show my DeFi positions”
  • “Swap 0.1 SOL for USDC on Jupiter”

Multi-Wallet Configuration

For production agents, configure separate MCP servers per wallet:

{
  "mcpServers": {
    "waiaas-trading": {
      "command": "npx", 
      "args": ["-y", "@waiaas/mcp"],
      "env": {
        "WAIAAS_BASE_URL": "http://127.0.0.1:3100",
        "WAIAAS_AGENT_ID": "019c47d6-51ef-7f43-a76b-d50e875d95f4",
        "WAIAAS_AGENT_NAME": "trading-agent"
      }
    },
    "waiaas-defi": {
      "command": "npx",
      "args": ["-y", "@waiaas/mcp"],
      "env": {
        "WAIAAS_BASE_URL": "http://127.0.0.1:3100", 
        "WAIAAS_AGENT_ID": "019c4cd2-86e8-758f-a61e-9c560307c788",
        "WAIAAS_AGENT_NAME": "defi-manager"
      }
    }
  }
}

Each agent sees only its assigned wallet and tools. Perfect for specialized trading bots or portfolio managers.

Real-World Example: DeFi Portfolio Manager

Here’s what happens when you ask Claude to “optimize my DeFi yield”:

  1. Assessment: Claude calls get-defi-positions to see your current lending/staking
  2. Analysis: Compares yields across Aave, Lido, Kamino using get-provider-status
  3. Strategy: Identifies better opportunities (e.g., “Your USDC earns 3% on Aave, but Kamino offers 5.2%”)
  4. Execution: Calls action-provider to withdraw from Aave and deposit to Kamino
  5. Confirmation: Uses get-transaction to verify completion

All in natural conversation. No switching apps, no manual transaction building.

Security Without Friction

WAIaaS enforces 21 policy types across 4 security tiers:

  • INSTANT: Small amounts (under $10) execute immediately
  • NOTIFY: Medium amounts ($10-$100) execute with notifications
  • DELAY: Large amounts ($100-$1000) wait 5 minutes (cancellable)
  • APPROVAL: Very large amounts (over $1000) require human approval

The policy engine uses default-deny enforcement. Without explicit ALLOWED_TOKENS or CONTRACT_WHITELIST policies, transactions are blocked. Your agent can’t accidentally drain funds or interact with malicious contracts.

For approval-tier transactions, Claude shows you the details and waits. You approve via WalletConnect, Telegram, or push notification. The agent then completes the workflow.

What’s Next

You’ve got 45 MCP tools and production-grade wallet infrastructure. Start with simple balance checks and token transfers, then explore DeFi automation as you get comfortable.

The MCP ecosystem is growing fast — WAIaaS puts you at the intersection of AI and onchain activity. Ready to give your Claude agent a wallet? Check out the complete setup guide at https://github.com/minhoyoo-iotrust/WAIaaS or explore the full documentation at https://waiaas.ai.

Back to the terminal — I built an invoicing tool that lives where you work

I love my terminal. It keeps me focussed and efficient. And it may sound silly, but it also makes me feel like I’m part of a community.

In a world increasingly shaped by LLMs and AI agents, I’ve found myself feeling nostalgic for the basics.

That’s why, for all my freelancer CLI-friends who struggle with context-switching; hate the complexity of today’s invoicing tools; or simply like the idea of more CLI <3, I have created Billed.

All you need to do is to run npm i billed and get started with bill --init-config.

It’s all run locally, there are no integrations needed and you can customise.

I’d love to know what you think!

Sub-Agent Documents Everything in Notion

The Prompt that birthed Escriber

I have a problem. Not a “my code does not compile” problem. A “I have so many ideas and projects and notes that my brain just gives up and reboots” problem. 😵‍💫

So I did what any reasonable developer would do: I built an AI agent inside Open Claw whose only job is to document everything beautifully in Notion. His name is Escriber.

The Prompt that birthed Escriber

This is my submission for the Notion MCP Challenge. Let me tell you, building a focused single-purpose agent with 14 Notion tools is exactly as satisfying as it sounds.

What I Built

Escriber is agent #2 of 5 on my OpenClaw multi-agent team.

Escriber telling me about himself

His SOUL.md says exactly this: “I document. Beautifully. In Notion.” And for anyone who has not used OpenClaw, they give these agents Soul.md files that define who the agent is and give them a small personality.

That is it. Full stop. He cannot debug your code. He cannot give life advice. He has one lane and he stays in it — which, honestly, is more discipline than I have most the time. 😆

He lives in Discord as a bot or I can use the web chat interface to call on him directly. He can be slightly artisanal. Dry wit is baked in by design, because documentation does not have to be soulless. He makes use of my Synap bot to write summaries for projects and clean them up like I would say them, then he will store what Synap sends back.

He is not a Swiss Army knife. He is a scalpel. And sometimes that is exactly what you need.

Video Demo

Show Us the Code

Escriber talks to Notion through mcporter — an MCP relay that proxies tool calls to Notion’s official MCP server. Every Notion interaction looks like this:

mcporter call notion.<tool_name> --args '{ ... }'

Clean. Consistent. One pattern to rule them all.

Here is the full arsenal — 14 tools, one agent, zero excuses for undocumented projects:

Tool What It Does
notion.search Find existing pages/databases before creating duplicates
notion.create_page Spin up a new page anywhere in the workspace
notion.retrieve_page Read a page’s metadata
notion.update_page Update page properties
notion.append_block_children Add content blocks to an existing page
notion.retrieve_block_children Read what is already on a page
notion.retrieve_block Fetch a specific block
notion.update_block Edit an existing block
notion.delete_block Remove a block
notion.create_database Create a new database
notion.retrieve_database Read database structure
notion.update_database Modify database schema
notion.query_database Filter and search database entries
notion.create_database_item Add a row to a database

Escriber also has workflow rules baked into his instructions. Because tools without discipline are just chaos with an API:

  • Search first. Always check if the page already exists before creating a new one.
  • Read before writing. Retrieve block children before appending to avoid structural disasters.
  • Right block type. Callouts for decisions. Toggles for details. Headings for structure. Not everything is a paragraph.
  • Structure intentionally. Measure twice, cut once. 🤣

How I Used Notion MCP

Let me tell you how this actually works.

mcporter connects Escriber to Notion’s official MCP server at https://mcp.notion.com/mcp. Every tool call gets proxied through that relay — no direct API wrangling, no custom request builders. The MCP layer handles it. Escriber just speaks tool.

Here is what that looks like day-to-day:

New project starts?
create_page to spin up the project hub. create_database_item to log it in the master projects database. Done in seconds.

Wrapping up a build session?
append_block_children to the existing project page. Build notes, decisions made, what broke, what did not. Structured. Timestamped. Actually findable later.

JB’s Job Listings
create_page My recruiter agent stored a list of jobs using Esciber every morning. Future-me will be grateful. Past-me was not doing this. These are stored in Notion daily, and sortable ☠️

Notion Job Notes

Let me tell you about the shame moment that made this real.

I went back through my own Notion workspace a few weeks ago. Pages with titles like “ideas v3 FINAL” and “project notes DO NOT DELETE” containing exactly… three bullet points and a YouTube link. That is it. That is the legacy. A graveyard of good intentions. ☠️

Escriber exists because I needed someone with more documentation discipline than me. Turns out that someone is a focused AI agent with a soul file and 14 Notion tools.

One job. One lane. Real personality. And the docs actually get written now.

“Optimization starts with a single point of failure—or a single point of success. Own the lane.”

Also, thanks to Bloggy, for taking the time to help with this post.

Escriber is part of my OpenClaw agent team. If you are curious about building multi-agent setups in openclaw, drop a comment — I am writing more about this whole stack as it comes together.

How I Tried to Handle Customer Support in Telegram and Ended Up Building a Tool for It

This didn’t start as a startup idea. I wasn’t trying to build a SaaS product. I just needed a simple way to handle customer support for my own projects without exposing my personal Telegram account or dealing with complex tools.

At first, Telegram felt perfect. Everyone already uses it, there’s no onboarding, no friction, no extra accounts. A client just sends a message and you reply. It’s fast, natural, and works out of the box.

And for a while, it really does work.

The problems don’t show up immediately. In the beginning, you have a few conversations per day, everything is manageable, and you can keep context in your head. Then you launch another project. Then another. Messages start coming in at different times, from different people, about different things.

You reply from your personal account, then maybe create a separate work account, then start forwarding messages to yourself or teammates. Over time, Telegram turns into a mix of personal chats, client conversations, and random forwarded messages. It becomes harder to understand what’s going on at any given moment.

The tricky part is that it doesn’t break in an obvious way. It slowly degrades. Responses get delayed, some messages are missed, sometimes two people reply to the same client, sometimes no one replies because everyone assumes someone else already did. Nothing critical, but enough to hurt the overall experience.

I tried a few workarounds. Multiple accounts, groups, pinned messages, manual labeling, forwarding flows. All of them help a bit, but none of them solve the core issue. Telegram is just a list of chats. There is no structure, no clear overview, no shared context.

Switching to a CRM or helpdesk didn’t feel right either. Not because those tools are bad, but because they’re often overkill for small teams or solo projects. You get dashboards, ticket statuses, workflows, onboarding, and another system you need to constantly keep open. Meanwhile, your actual communication still happens in Telegram, just routed through something else.

At some point it became clear that I didn’t need another tool. I needed a way to make Telegram itself more structured.

The idea ended up being very simple. Instead of clients messaging me directly, they message a bot. Every incoming message is forwarded into a private Telegram group where each client gets their own thread. One client, one thread. All history stays in one place, no forwarding, no confusion.

You reply directly inside that thread, and the bot sends the response back to the client.

Nothing fundamentally changes from the user’s perspective. It still feels like a normal Telegram chat. But internally, everything becomes structured. You can immediately see all active conversations, understand which ones need attention, and multiple people can work in parallel without stepping on each other.

Most importantly, your personal account is no longer part of the support flow at all.

I originally built this just to solve my own problem. But after using it for a while, I realized this pattern applies to a lot of people. Freelancers, small teams, indie projects, small businesses. Many of them rely on Telegram for communication, run into the same limitations, and don’t want to switch to heavy tools.

That’s how it turned into a small product.

If you’re curious how this approach works in practice, I put together a simple explanation here:

👉 https://gramdeskbot.com

It’s not trying to replace a CRM or compete with full helpdesk platforms. It’s just adding a missing layer to Telegram so it can function as a lightweight support system.

And in practice, for small setups, that turned out to be a much better fit than anything more complex.

If you’re already using Telegram as your main communication channel, you’ll likely run into the same issues at some point. The question is whether you keep patching it manually or introduce a bit of structure on top of what you already use.

RustCheat: A Minimal CLI Cheat Sheet for Rust

It’s been a little while since I really used Rust. Understanding this, I Noticed I forgot some of my syntax. Usually when I forget syntax I do a “quick” google search which might take me down a rabbit hole of where I either get distracted or need to google a bunch of other things. so I created a simple cli app so that for little things I never have to leave my terminal

DISCLAIMER this is my first cli app that I’ve published to a registry so any constructive criticism would be appreciated. the original cheatsheet that I had inspiration from was by Francesco Ciulla.

Rust Cheat Crate

If you would like to contribute to this project you can checkout the repo here thanks so much for your contribution!