Z Blog

Bots downloading a chatbot plugin

The meme came immediately to mind…

Just created a new npm package for installing an AI chat widget, and I got excited for a moment because I thought, “it’s already being downloaded!?”

…after barely 48 hours.

Then I realized that it had to be scrapers. The irony of bots downloading a chatbot package makes me chuckle!

Anyway, the package was created for entrepreneurs using Svelte and BetterAuth. Entrepreneurs in a course at Nucamp use a starter repo to get their SaaS apps going, and the chatbot works with that stack.

I did a lot of it by vibe coding with Claude Code. Since Claude Code came out, there are so many old ideas I want to revisit or old projects I want to refactor! There’s just not enough time. But in this case I was able to quickly build something useful for budding entrepreneurs.

It’s scary posting something live, and we’ll see if I’m up to the job of maintainer! But it certainly has been a mostly fun process so far. If you’re reading this, please give the installation a shot in your Svelte-BetterAuth project, and let me know if you run into any issues!

Repetitive emails? Let AI reply for you (part 2)

IMPORTANT NOTE: This script will analyze the email, determine which reply from a set of templates is most appropriate, then customize that reply as needed for that particular customer/inquiry, adding in the customer’s name and adding your desired email signature. Then it will forward that generated reply back to you at the same address, for human review (as in-line text). You still need to then send the generated reply, it will NOT do the sending for you.

So we’ve refined the previous script a bit to add a JSON file full of reply templates. Now there’s a JSON added to the template literal prompt for {templateOptions}, which is then passed to the LLM.


The context size is increasing but the tradeoff is that we are getting increased functionality from our auto-responder (only intended for nuisance, non-sensitive emails or in line with your company security policy):

// Global variable to cache the loaded reply templates to avoid re-reading the file on every function call.
var cachedReplyTemplates = null;

/**
 * Main function to process forwarded emails.
 */
function processForwardedEmails() {
  try {
    console.log("Starting email processing...");

    var allThreads = [];
    var start = 0;
    var maxBatch = 100; // Process 100 threads at a time

    // Search for UNREAD emails that don't have "AUTOMATED REPLY" in the subject
    var searchQuery = 'is:unread -subject:"AUTOMATED REPLY"';

    do {
      var threads = GmailApp.search(searchQuery, start, maxBatch);
      allThreads = allThreads.concat(threads);
      start += threads.length;
      console.log("Found " + threads.length + " unread forwarded threads in batch, total " + allThreads.length);
    } while (threads.length === maxBatch);

    console.log("Total threads to process: " + allThreads.length);

    for (var i = 0; i < allThreads.length; i++) {
      var thread = allThreads[i];
      var messages = thread.getMessages();
      var latestMessage = messages[messages.length - 1];

      console.log("Processing email " + (i + 1) + ": " + thread.getFirstMessageSubject());
      console.log("From: " + latestMessage.getFrom());
      console.log("Is unread: " + thread.isUnread());

      // Extract original subject from the forwarded email content
      var originalSubject = extractOriginalSubject(latestMessage.getPlainBody(), thread.getFirstMessageSubject());
      console.log("Original subject: " + originalSubject);

      // Generate reply using Gemini
      console.log("Generating reply...");
      var reply = generateReply(latestMessage.getPlainBody());
      console.log("Reply generated, length: " + reply.length);

      // Send the generated reply back to you for review
      console.log("Sending reply to self...");
      sendReplyToSelf(originalSubject, latestMessage.getPlainBody(), reply);
      console.log("Reply sent!");

      // Mark as read, but DO NOT archive the email
      thread.markRead(); // First, mark as read
      console.log("Email marked as read.");
    }

    console.log("Processing complete!");
  } catch (error) {
    console.log("Error: " + error.toString());
    console.log("Stack trace: " + error.stack);
  }
}

/**
 * Extracts the original subject from a forwarded email's body.
 * @param {string} emailBody The plain text body of the email.
 * @param {string} forwardedSubject The subject of the forwarded email from Gmail.
 * @returns {string} The extracted original subject.
 */
function extractOriginalSubject(emailBody, forwardedSubject) {
  // Try to find "Subject:" in the forwarded email body
  var subjectMatch = emailBody.match(/Subject:\s*(.+)/i);

  if (subjectMatch && subjectMatch[1]) {
    return subjectMatch[1].trim();
  }

  // Fallback: remove "Fw:" or "Fwd:" from the Gmail subject
  var cleanedSubject = forwardedSubject.replace(/^(Fw:|Fwd:)\s*/i, '').trim();
  return cleanedSubject;
}

/**
 * Loads reply templates from the 'replies.json.html' file.
 * Caches the templates after the first load for efficiency.
 * @returns {object} An object containing the reply templates.
 */
function loadReplyTemplates() {
  if (cachedReplyTemplates === null) {
    try {
      var jsonString = HtmlService.createTemplateFromFile('replies.json').evaluate().getContent();
      cachedReplyTemplates = JSON.parse(jsonString);
      console.log("Reply templates loaded from replies.json.html");
    } catch (e) {
      console.error("Error loading or parsing replies.json.html: " + e.message);
      throw new Error("Failed to load reply templates. Please ensure 'replies.json.html' exists and contains valid JSON.");
    }
  }
  return cachedReplyTemplates;
}

/**
 * Generates a reply using the Gemini API, selecting from a set of pre-defined templates.
 * @param {string} emailBody The plain text body of the email to respond to.
 * @returns {string} The customized reply generated by Gemini.
 */
function generateReply(emailBody) {
  try {
    // IMPORTANT: Ensure you have your GEMINI_API_KEY set in Script Properties (File > Project properties > Script properties)
    var apiKey = PropertiesService.getScriptProperties().getProperty('GEMINI_API_KEY');

    // Ensure API Key is available
    if (!apiKey) {
      throw new Error("GEMINI_API_KEY is not set in Script Properties.");
    }

    // Load all available reply templates
    var templates = loadReplyTemplates();

    // Convert templates object to a string for the prompt
    // This provides Gemini with the full list of options and their content
    var templateOptions = JSON.stringify(templates, null, 2); // Pretty print for readability for AI

    var prompt = `You are an email assistant for TechCorp Solutions. Your task is to select the most appropriate pre-written reply template based on the content of the email and then customize it.

Here are the available reply templates, identified by their keys:
${templateOptions}

Instructions:
1.  **Analyze the "EMAIL TO RESPOND TO" carefully.**
2.  **Determine which of the provided templates is the best fit for the email's intent.**
3.  **Extract the sender's first name, product/service name (if mentioned, otherwise "not specified"), and company name (if mentioned, otherwise "not specified") from the email.**
4.  **Populate the chosen template's placeholders ({customerName}, {productName}, {companyName}, etc.) with the extracted information.** If a placeholder's information is "not specified", omit that part of the sentence or use a polite general phrase where applicable.
5.  **If no template is a good fit, respond with "noMatch".** In this case, provide a polite generic response that asks for more information.
6.  If the email is clearly not about the specific topic of the chosen template, politely redirect them to use the standard template but mention their specific topic.

EMAIL TO RESPOND TO:
${emailBody}

YOUR RESPONSE FORMAT:
Chosen Template Key: [Key of chosen template, or 'noMatch']
Customized Reply: [The full customized reply based on the chosen template and extracted info, or the generic response if 'noMatch']

Example of expected output if a template is chosen:
Chosen Template Key: pricingRequest
Customized Reply: Hello John, Thank you for your interest in our cloud hosting services. Our pricing starts at $99/month for the basic plan and $299/month for premium features. I'll send you a detailed quote within the next business day. You can also view our pricing at www.techcorp.com/pricing. Best regards, Sales Team

Example of expected output if customer name is not specified:
Chosen Template Key: generalInquiry
Customized Reply: Hi there, Thank you for reaching out to TechCorp Solutions. We have received your inquiry and will respond within 24 hours. For urgent matters, please call our support line at 555-0123. Best regards, Customer Service Team

Example of expected output if 'noMatch' is chosen:
Chosen Template Key: noMatch
Customized Reply: Dear Sender, thank you for your email. To assist you further, please provide more details about your inquiry.

---
Chosen Template Key:`; // Start the prompt to guide Gemini's output format

    var payload = {
      'contents': [{
        'parts': [{
          'text': prompt
        }]
      }]
    };

    var options = {
      'method': 'POST',
      'headers': {
        'Content-Type': 'application/json',
      },
      'payload': JSON.stringify(payload)
    };

    var response = UrlFetchApp.fetch(
      `https://generativelanguage.googleapis.com/v1/models/gemini-1.5-flash:generateContent?key=${apiKey}`,
      options
    );

    var data = JSON.parse(response.getContentText());

    if (data.candidates && data.candidates[0] && data.candidates[0].content) {
      var geminiOutput = data.candidates[0].content.parts[0].text;

      // Parse Gemini's structured output
      var lines = geminiOutput.split('\n');
      var chosenKey = '';
      var customizedReply = '';

      for (var i = 0; i < lines.length; i++) {
        if (lines[i].startsWith('Chosen Template Key:')) {
          chosenKey = lines[i].replace('Chosen Template Key:', '').trim();
        } else if (lines[i].startsWith('Customized Reply:')) {
          customizedReply = lines[i].replace('Customized Reply:', '').trim();
          // Concatenate remaining lines if the reply spans multiple lines
          for (var j = i + 1; j < lines.length; j++) {
            customizedReply += '\n' + lines[j].trim();
          }
          break; // Stop after finding the reply
        }
      }

      if (chosenKey === 'noMatch') {
        console.log("Gemini determined no suitable template. Generic reply generated.");
        return customizedReply; // Return the generic response
      } else if (templates[chosenKey]) {
        console.log("Gemini chose template: " + chosenKey);
        // Gemini is now responsible for populating the variables within the 'Customized Reply'
        return customizedReply;
      } else {
        console.warn("Gemini suggested an unknown template key: " + chosenKey + ". Returning generic error reply.");
        return "Error: Gemini suggested an invalid template or failed to choose. Please check manually.";
      }

    } else {
      // Log more details if the Gemini API response is not as expected
      console.log("Unexpected Gemini API response structure: " + JSON.stringify(data));
      return "Error generating reply - please check manually";
    }

  } catch (error) {
    console.log("Gemini API error: " + error.toString());
    return "Error generating reply: " + error.toString();
  }
}

/**
 * Sends the generated reply back to your specified email address for review.
 * @param {string} originalSubject The subject of the original forwarded email.
 * @param {string} originalBody The plain text body of the original forwarded email.
 * @param {string} generatedReply The reply generated by Gemini.
 */
function sendReplyToSelf(originalSubject, originalBody, generatedReply) {
  var subject = "AUTOMATED REPLY: " + originalSubject;
  var body = "Generated Reply:\n" + generatedReply + "\n\n--- Original Email ---\n" + originalBody;

  // Send to your review email address
  GmailApp.sendEmail("admin@techcorp.com", subject, body);
}

And the JSON file has camelcase keys representing a bunch of customer scenarios, with the potential responses as values.

{
  "generalInquiry": "Hi {customerName}, Thank you for reaching out to TechCorp Solutions. We have received your inquiry and will respond within 24 hours. For urgent matters, please call our support line at 555-0123. Best regards, Customer Service Team",
  
  "pricingRequest": "Hello {customerName}, Thank you for your interest in our services. Our pricing starts at $99/month for the basic plan and $299/month for premium features. I'll send you a detailed quote within the next business day. You can also view our pricing at www.techcorp.com/pricing. Best regards, Sales Team",
  
  "supportTicket": "Hi {customerName}, We've received your support request (Ticket #{ticketNumber}) and our technical team is reviewing it. Expected response time is 4-6 hours during business days. You can track your ticket status at www.techcorp.com/support. Thanks, Technical Support",
  
  "partnershipProposal": "Dear {partnerName}, Thank you for your partnership proposal. We appreciate your interest in collaborating with TechCorp. Our partnerships team will review your submission and respond within 5-7 business days. Regards, Business Development Team",
  
  "jobApplication": "Hello {applicantName}, Thank you for applying for the {positionTitle} role at TechCorp. We have received your application and will review it with our hiring team. If your qualifications match our needs, we will contact you within two weeks. Best wishes, HR Department",
  
  "vendorInquiry": "Hi {vendorName}, Thank you for introducing your services to TechCorp. We are currently evaluating our vendor relationships and will reach out if we have a need that matches your offerings. Please keep us updated on your services quarterly. Thanks, Procurement Team",
  
  "mediaRequest": "Hello {reporterName}, Thank you for your media inquiry. Our PR team will review your request and respond within 48 hours. For urgent press matters, please contact our media hotline at 555-0199. Regards, Public Relations Team",
  
  "refundRequest": "Dear {customerName}, We have received your refund request for order #{orderNumber}. Our billing department will process this within 3-5 business days and send you a confirmation email. If you have questions, please contact billing@techcorp.com. Customer Care Team",
  
  "scheduleMeeting": "Hi {contactName}, Thank you for requesting a meeting. Please use our scheduling link to book a convenient time: www.calendly.com/techcorp-meetings. Available slots are Monday through Friday, 9 AM to 5 PM EST. Looking forward to speaking with you, Sales Team",
  
  "productFeedback": "Hello {customerName}, Thank you for taking the time to share your feedback about {productName}. Your insights help us improve our products. Our product team will review your suggestions for future updates. We appreciate your continued support, Product Team",
  
  "serviceNotOffered": "Hi {customerName}, Thank you for your inquiry about {requestedService}. Unfortunately, TechCorp does not currently offer this service. We specialize in cloud infrastructure and software development solutions. You might want to try reaching out to companies that focus specifically on {serviceCategory}. We appreciate your interest, Customer Service Team",
  
  "unsolicitedAdvertising": "Hello, Thank you for reaching out. TechCorp is not interested in {advertisedProduct} at this time. Please remove our email address from your mailing list and do not send us further promotional materials. If you continue to contact us after this request, we will report it as spam. Regards, TechCorp Administration"
}

To try this if you haven’t already from my previous part 1 post, go to https://script.google.com/home and add a new project (you can make a new Gmail account just for forwarding). Then, add a new script. In the Code.gs file, paste this into the editor window. Then add another script and name it ‘replies.json.html’, pasting in the JSON file.

Inside the Code.gs script, go to the sendReplyToSelf() function and replace the dummy email with your own email address where you will review the generated messages.

Your Gemini API key can be entered in https://script.google.com/home/projects under Settings. Run a test by forwarding an email to this new Gmail address, then click the “Run” button at the top of the code editor page. You should see the execution log at the bottom for any errors.

After confirming that it’s working, of course you’ll want to replace the dummy scenarios in the JSON file with your own frequently-encountered scenarios. It’s a lot of work to type out each one, so perhaps ask an AI to edit the JSON using your company FAQ, or your Zendesk help center documentation, etc.

You can also add fallbacks to another API, like OpenAI for example, in case Gemini is too overloaded. Just add an additional key inside your Apps Script settings page:

function generateReply(emailBody) {
  var prompt = buildPrompt(emailBody);
  
  // 1. Try with Gemini first (primary AI)
  var geminiReply = callGeminiApi(prompt);
  
  if (geminiReply !== "ERROR_GEMINI_OVERLOADED") {
    // If Gemini didn't fail with a 503, return its response (or any other error)
    return geminiReply;
  }
  
  // 2. If Gemini is overloaded, try with OpenAI (fallback AI)
  console.warn("Gemini is overloaded. Attempting to use OpenAI as a fallback...");
  var openAiReply = callOpenAiApi(prompt);
  
  if (openAiReply.startsWith("ERROR_")) {
    console.error("OpenAI fallback also failed. Error: " + openAiReply);
  } else {
    console.log("Successfully generated reply with OpenAI fallback.");
  }
  
  // Return the OpenAI response, whether it's a success or an error
  return openAiReply;
}

How to Deploy after Testing

You might notice that you need to manually run this app or that the timing trigger will stop working after a certain interval — this is because the app needs to be properly deployed.

From inside the Google Apps Script code editor, you’ll see a “Deploy” button in the upper-right of the screen — click that.

Then Select “API Executable” as the Deployment Type. Restrict access to yourself only.

Have you used this or other email automation? Please do let me know what’s working for you, or if you need help setting this up!

Meta-meta prompting with Claude Desktop and Desktop Commander

Vibe-code the tools you need to move projects forward.
For example, a prompt generator for creating feature prompts. Instead of typing out, “Add a MUI React component dark-mode toggle, depicting a sun and moon icon,” along with the relevant guiding code snippets, have make a list of all sprint cycles and have an LLM extrapolate the prompts and put them into md files for you:

Tired of generating prompts? Build your own prompt generator.

Here’s a glance at the directory structure:

Here’s a share of my initial prompt to get things started:

https://github.com/studiozandra/meta-meta-prompt

What sorts of tools have you whipped up lately?

Memory with ChatGPT: Forget I mentioned it

Many people are unaware that ChatGPT is remembering aspects of their conversations, and building a profile on the user.

This can be good if you hate repeating yourself, or bad if you feel some kinda way about privacy.

OpenAI "Manage Memory" settings dialog
image from OpenAI

Open your ChatGPT page or app, and control ChatGPT’s memory!

Either…

  • Directly in the conversation: You can tell ChatGPT to remember something by explicitly stating it, or you can ask it what it remembers to review or modify its understanding. 
  • In settings: Navigate to Settings > Personalization > Manage Memory to view, delete, or clear all memories. 

For more info and recent updates, check out the official article from OpenAI. Claude also now allows users to enter personalization information under Account > Settings > Profile. For Gemini, chats are remembered for a specified time (currently 18 months) unless you go in and delete each one. However there is also an area to enter specific details about your life for Gemini to remember across all chats, under Settings & help > Saved info.

How to Install Claude Desktop with MCP Desktop Commander on Ubuntu/Debian Linux

If you’re a Linux user who wants to run Claude Desktop with powerful Model Context Protocol (MCP) (https://modelcontextprotocol.io/) tools, this guide will walk you through the entire process. We’ll install Claude Desktop and set up the Desktop Commander MCP server, which allows Claude to interact with your desktop environment.

This Linux-beginner-friendly guide is based on my experience installing it this week on Pop!_OS on an Asus Nvidia machine, and was of course created with help from Claude.

If you haven’t seen it in action, DesktopCommander MCP is a thing of beauty:

⚠️ Important Warnings and Prerequisites

Before you begin:

  • Backup any important data – always a good idea
  • System requirements: Ubuntu 20.04+ or Debian 11+/Debian-based distro (I’m running on Pop!_OS 22.04 LTS with Nvidia); Anthropic doesn’t publish RAM, disk space, or CPU requirements, but third-party online posts recommend 8GB ~ 32GB RAM
  • Terminal comfort: This guide involves using the terminal (https://ubuntu.com/tutorials/command-line-for-beginners), but all commands are provided step-by-step

What You’ll Install

  • Claude Desktop: The desktop application for Claude AI
  • Node.js via NVM: JavaScript runtime needed for MCP servers
  • Desktop Commander MCP: Allows Claude to interact with your desktop (take screenshots, run commands, etc.)

Step 1: Update Your System

First, let’s make sure your system packages are up to date.

  1. Open your terminal (https://help.ubuntu.com/community/UsingTheTerminal) by pressing Ctrl+Alt+T

  2. Update your package lists and upgrade existing packages:

sudo apt update && sudo apt upgrade -y

What this does: sudo (https://www.sudo.ws/about/) gives you administrator privileges, apt (https://ubuntu.com/server/docs/package-management) is Ubuntu/Debian’s package manager, and this ensures your system has the latest security updates.


Step 2: Install Essential Dependencies

Install tools we’ll need for the installation process:

sudo apt install -y curl wget gnupg2 software-properties-common

What these tools do:

  • curl (https://curl.se/): Downloads files from the internet
  • wget (https://www.gnu.org/software/wget/): Alternative download tool
  • gnupg2 (https://gnupg.org/): Handles cryptographic signatures for security
  • software-properties-common: Manages software repositories

Step 3: Install Node.js Using NVM

We need Node.js (https://nodejs.org/) to run MCP servers. We’ll use NVM (Node Version Manager) (https://github.com/nvm-sh/nvm) which makes managing Node.js versions easier.

  1. Download and install NVM if you don’t already have it installed:
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
  1. Restart your terminal or reload your shell configuration:
source ~/.bashrc
  1. Verify NVM was installed:
nvm --version
  1. Install the latest LTS (https://nodejs.org/en/about/previous-releases) version of Node.js:
nvm install --lts
nvm use --lts
  1. Verify Node.js and npm (https://www.npmjs.com/) are working:
node --version
npm --version

You should see version numbers after both commands.


Step 4: Download and Install Claude Desktop

Note: We’re using a community-maintained package since Anthropic (https://docs.anthropic.com/en/docs/claude-code/getting-started) doesn’t officially support Linux yet.

  1. Visit the Claude Desktop for Debian GitHub repository (https://github.com/aaddrick/claude-desktop-debian) in your web browser. (Unfortunately this package is deprecated, BUT there are a couple of active forks, so if you’re reading this in later months and the package is broken, check the active forks.)

  2. Go to the “Releases” section and download the latest .deb file (https://en.wikipedia.org/wiki/Deb_(file_format))

  3. Navigate to your Downloads folder in the terminal:

cd ~/Downloads
  1. Install the downloaded package (replace filename.deb with the actual filename):
sudo dpkg -i claude-desktop_*.deb
  1. If you encounter dependency issues, fix them with:
sudo apt install -f

What this does: dpkg (https://wiki.debian.org/dpkg) is the low-level package installer for Debian/Ubuntu systems. The -f flag fixes broken dependencies.


Step 5: Create Claude Desktop Configuration Directory

Claude Desktop needs a configuration file to know about MCP servers.

  1. Create the configuration directory:
mkdir -p ~/.config/Claude

What this does: mkdir -p (https://www.gnu.org/software/coreutils/manual/html_node/mkdir-invocation.html) creates directories and any necessary parent directories. The ~ symbol represents your home directory (https://tldp.org/LDP/abs/html/special-chars.html).


Step 6: Test Desktop Commander MCP Server

Before configuring Claude Desktop, let’s make sure the Desktop Commander MCP server works.

  1. Test the MCP server directly:
npx @wonderwhy-er/desktop-commander@latest

You should see output like:

Loading schemas.ts
Loading server.ts
Setting up request handlers...
[desktop-commander] Initialized FilteredStdioServerTransport
  1. Press Ctrl+C to stop the test server.

What this does: npx (https://docs.npmjs.com/cli/v7/commands/npx) runs Node.js packages without permanently installing them. This tests that the MCP server can start properly.


Step 7: Configure Claude Desktop for MCP

Now we’ll create the configuration file that tells Claude Desktop about our MCP server.

  1. Create the configuration file:
nano ~/.config/Claude/claude_desktop_config.json

What this does: opens nano (https://www.nano-editor.org/) a text editor for the terminal.

  1. Copy and paste this configuration into the file:
{
  "mcpServers": {
    "desktop-commander": {
      "command": "npx",
      "args": [
        "@wonderwhy-er/desktop-commander@latest"
      ]
    }
  }
}
  1. Save and exit nano:
    • Press Ctrl+X
    • Press Y to confirm saving
    • Press Enter to confirm the filename

Step 8: Launch Claude Desktop

  1. Start Claude Desktop from the graphical applications menu, or run from terminal:
claude-desktop
  1. Sign in with your Anthropic account when prompted

  2. Look for MCP indicators in the interface – you should see evidence that the Desktop Commander server is connected


Step 9: Test Your MCP Setup

  1. In Claude Desktop, try asking Claude to take a screenshot:

    “Can you take a screenshot of my current desktop?”

  2. Or ask Claude to check system information:

    “What’s my current working directory and what files are in my home folder?”

If Claude can respond to these requests with actual information about your system, congratulations! Your MCP setup is working.


Troubleshooting Common Issues

Issue: “Cannot find module” errors

Solution: This usually means there’s a problem with literal command substitution in your config file:

  1. Check your config file for any "$(which npx)" strings:
cat ~/.config/Claude/claude_desktop_config.json
  1. If you see "$(which npx)" anywhere, replace it with just "npx" – the simple form works when Node.js is properly installed via NVM

Issue: Claude Desktop won’t start

Solution: Check if all dependencies are installed:

sudo apt install -f
sudo apt update

Issue: MCP server not connecting

Solution: First, verify that Desktop Commander itself is working:

  1. Test the MCP server directly to confirm it’s functioning:
npx @wonderwhy-er/desktop-commander@latest

You should see output like:

Loading schemas.ts
Loading server.ts
Setting up request handlers...
[desktop-commander] Initialized FilteredStdioServerTransport
Loading configuration...
Configuration loaded successfully
Connecting server...
Server connected successfully
  1. If that works (press Ctrl+C to stop it), then the issue is with Claude Desktop’s configuration, not the MCP server itself

  2. Check Claude Desktop logs for connection errors – these are usually in the application’s output when started from terminal


Security Considerations

Important: The Desktop Commander MCP gives Claude significant access to your system, including:

  • Taking screenshots
  • Running terminal commands
  • Accessing files

Best practices:

  • Only use this setup on personal machines, not shared computers
  • Review what you’re asking Claude to do before confirming actions
  • Consider creating a separate user account for testing MCP functionality
  • Keep your system updated with security patches

Keeping Everything Updated

Update Node.js

nvm install --lts
nvm use --lts

Update MCP packages

The @latest tag in our configuration automatically uses the newest version, but you can manually update:

npm update -g @wonderwhy-er/desktop-commander

Update Claude Desktop

Check the project forks on GitHub periodically for new .deb package releases and install them using the same dpkg process.


Uninstalling (If Needed)

Remove Claude Desktop

sudo apt remove claude-desktop

Remove Node.js and NVM

nvm uninstall node
rm -rf ~/.nvm

Remove the NVM lines from your ~/.bashrc file using a text editor.

Remove configuration files

rm -rf ~/.config/Claude

Next Steps

Now that you have Claude Desktop with MCP working, you can:

  1. Explore other MCP servers: Check the MCP ecosystem (https://github.com/modelcontextprotocol) for additional tools
  2. Customize your setup: Add more MCP servers to your configuration
  3. Learn more about MCP: Read the official MCP documentation (https://modelcontextprotocol.io/docs)

Remember to always be cautious when giving AI systems access to your computer, and enjoy exploring the powerful combination of Claude AI with your Linux desktop.


Credits and Resources

  • Claude Desktop for Debian: Community package by aaddrick, deprecated recently but forked (https://github.com/aaddrick/claude-desktop-debian)
  • Desktop Commander MCP: Created by @wonderwhy-er (https://github.com/wonderwhy-er/desktop-commander) – Thanks, Dmitry, for your help on Discord confirming that it was indeed the config file
  • Model Context Protocol: Developed by Anthropic (https://modelcontextprotocol.io/)

This guide was created based on real troubleshooting experience and community feedback. If you encounter issues, check the GitHub repositories for these projects or ask for help in Discord/Linux communities.

Repetitive emails? Have your AI craft replies

Do you have one or more types of emails that you need to reply to, but that you are sick and tired of handling? “If I have to answer the same question ONE more time I’ll snap” -type of emails?

Now let’s say you are not the administrator of your corporate or business email account for whatever reason. You want to implement some automations, and you have a vague idea that this should be something an AI can handle, but you’ve no access to the admin control panel needed to connect thing up.

There is a hack for that! This should absolutely NOT be used for sensitive communication, as you would basically be doing an end-run around any security controls your administrator has put into place.

But for nuisance emails an other non-sensitive emails, you can set up a Gmail account and an automatic script which can call an AI to generate replies to these emails, and then send the text back to you.

Details published here: https://claude.ai/public/artifacts/5c2560bd-b18b-43fb-b16e-3f1288dd1a36

Contact me to get this up and running, and get rid of those annoying email tasks!

AI is Your 24-hr Virtual Assistant

Coming back from a conference or event with dozens of business cards? (Or worse – handwritten sticky notes)

Networking board at the 2025 NYC Small Business Expo

Ain’t nobody got time to sit down and type up all these cards and notes manually one by one.
Back in the day, you used to need a card reader machine, or more recently, a card scanning mobile app.

However now you just get your AI assistant to do things for you — simply upload the photos into the AI chat and request not only an OCR scan but specify the output as well:

Asking Claude what it can see can sometimes save time prompting

The trick is to take as clear a photo as possible so as to be able to extract the most accurate information.

You can even ask Claude (or your LLM of choice) to generate contact files / Vcards which can be easily imported into your email contacts.

Subscribe for more time-saving AI use-cases for your business! And don’t forget to book your session.

Launching AI training for Women in Business

Rolling out to Queens businesses now, with the broader New York area access coming soon

The best time to get started with AI was perhaps a few years ago.

The second-best time is now!

That’s what I’m sharing with female founders, women in business, and women-owned businesses in Queens, NY. The takeaway is — if you haven’t used AI tools to save you at least 2-10 hours of work per week yet, now’s your time!

I’ve been teaching a variety of topics for the past 14 years, so empathy, virtual tools, and teaching with metaphors, visual aids, and patience is my jam. I’m so happy to bring you into the world of AI tools and delegate those painful tasks you dread each week to your new AI assistant.

Is it invoicing? Marketing? Customer follow-up? Or just writing plans and proposals or reviewing lengthy contracts?

AI can help you save time and money. Book your consultation with me now to learn more (psst… it’s free).

Installing Linux on Asus ROG Strix, part 2

Well, I got the main partition resized thanks to this blog post. It was turning off things like hibernation and system restore. Then I shrank the Windows partition down to about 98 GB. Take that, Windows!

Still can’t get rid of the obnoxious logo

After a reboot, it was pretty smooth sailing. After installing the proprietary NVIDIA drivers and restarting with a special pin entered from the boot partition, I restarted again. However, I got the error “Nvidia kernel module missing, falling back to nouveau” no matter what I tried.

So I started thinking I would need to install Arch Linux, as it seems to support NVIDIA drivers well. But another contender emerged: Pop!_OS. Yes, of system76 fame. I had heard of it many times but never tried it.

I made another live USB stick and split the Fedora partition in half, installing Pop!_OS (Secure Boot needs to be shut off otherwise the live USB likely won’t start).

Lo and behold, it worked out of the box. The keyboard backlighting can be controlled with the function keys just fine, although I haven’t tried changing the colors yet. Pop!_OS even wakes from suspend after lid close, something that I’ve seen fail many times on other OSes.

So Pop!_OS, you have a new fan.

Installing Linux on Asus ROG Strix G16

How to lose your hair quickly

A trip to Microcenter is a candy store-like experience with good customer service and great selection. I bought a brand-new Asus ROG Strix, despite the exquisitely obnoxious branding and lighting, for my next Linux box.

I was told that for warranty purposes, should anything occur and I need to bring the machine in for service, I’d have to re-install Windows first. So while making the backup restore media and setting up (a local account) I decided perhaps it’s best to dual boot.

The Interwebs told me to go with Fedora, and who am I to argue. So I made a boot thumb drive and took it for a spin. I used to use Balena Etcher but this time I went with Fedora’s Media Writer tool which worked well (but for Windows 10 users, autoplay might need to be switched off before writing the drive). I was impressed that there was even some limited lighting control out of the box.

But the ASUS problems started coming fast and furious. Top priority had been to flash the BIOS in order to remove the hideous “Republic of Gamers” boot loading logo, but alas, the tool for that was not available for my specific motherboard. Oh well; I shall have to just avert my eyes during the boot sequence for now.

Next up, I couldn’t access the main partition from inside the Fedora live preview; in disk management there was a little padlock icon and the word “Bitlocker.”

What in tarnation. Pretty sure I toggled that off during Windows setup, so I was a tad confused. Booting back into Windows settings, there was a button which said, “Turn on Bitlocker”. Now I was even more confused, because it sure seemed to be on already.

Apparently Bitlocker encryption is default on Windows 11. Under Windows “Privacy & security”, there is a toggle to turn off Device Encryption. Once I shut that off, it said “decrypting” for a few minutes, and then I was able to go back into Fedora live OS and access the drive.

Another hurdle: Fedora tells me “Not enough free space on selected disks.” Microsoft hogs 5 partitions full of goodness-knows what. So I toddle off into Windows Disk Management to resize the main partition. Guess what — due to unmovable files, the partition could only be shrunk to about 800GB (leaving only about 200 for Fedora).

It’s bedtime, so tomorrow I will delete some expendable-looking things, run Disk Cleanup, and defragment to see if that will help. But I’m starting to think it’s not worth all this hassle to dual boot.

To be continued…