Accelerating Node.js development with mcp-node

Matteo CollinaMatteo Collina
9 min read

Over the last year, there has been incredible hype around AI and how it will supposedly replace developers. I would have been a fool not to investigate whether there was any truth to this claim. Therefore, I embarked on a journey to learn about Claude Desktop and its “agentic” capabilities.

Two weeks ago, I used Claude Desktop to code a pure-typescript implementation of the JQ language. I was incredibly pleased with what I’ve found, and while I concluded that AIs will not replace humans anytime soon, the future present is bright with AI-assisted development.

In this blog, I’ll show my setup, and explore how Claude works.

Today, we are open-sourcing mcp-node, our utility to control Node.js via the Model Context Protocol. It allows the entire TDD cycle from within Claude Desktop, and I use it daily. We are also open-sourcing the Flowing JSON Grep Handler (fgh on npm), our implementation of the JQ Language written in TypeScript, which I wrote with the assistance of Claude and mcp-node.

Here is a quick demo of Claude Desktop raising code coverage of fgh by 2% in roughly 12 minutes of work (3x speed up):

My rough estimation is that it would have required me 1 hour of work to achieve the same result without AI.

You can take a look at the actual prompt that I used here.

I often let Claude work while I do other tasks, too.

How did I achieve this?

  1. Modern Node.js features like type stripping and node:test makes it extremely easy to iterate

  2. Nvm allows you to switch Node.js version easily

  3. mcp-node, our utility to execute Node.js and NPM via the MCP protocol

  4. The filesystem mcp

Mcp-node introduction

I was evaluating Claude on the Web, and while I asked it to write some code for me, I felt as though it repeatedly tried to run some Node.js code. Therefore, I give it a way to interact with my local environment using a model-context protocol server.

The Model Context Protocol (MCP) is an open standard that allows AI assistants to securely interact with external tools and services on a user's device. It enables AI assistants to perform real-world actions, like running code and accessing files, within a secure and permission-controlled environment.

mcp-node creates a secure channel through which AI assistants can run Node.js operations on your machine—with your explicit permission. Every command, script, or code evaluation triggers a notification that requires your approval, displaying exactly what will be executed, where, and with what inputs. This permission-based architecture ensures you maintain complete control while allowing your AI assistant to become an active participant in your development workflow.

It provides a few tools and resources to perform the following tasks:

  • Run Node.js Scripts: Execute JavaScript files with full support for command-line arguments and standard input

  • Execute npm Scripts: Run commands defined in your project's package.json

  • Evaluate JavaScript Code: Test code snippets directly without creating separate files

  • Node.js Version Management: Switch between different Node.js versions via NVM integration

Once configured, you can ask Claude to:

  • Run Node.js scripts: "Could you run my analysis.js script with the data.json file as an argument?"

  • Execute npm commands: "Please run the 'build' script in my project"

  • Evaluate JavaScript code: "Can you test this function for me with different inputs?"

  • Switch Node.js versions: "Switch to Node.js v18 and run the compatibility test"

Each request will trigger a permission notification, giving you full control over what gets executed.

The Permission System: Security by Design

The permission system in mcp-node creates a secure barrier between AI assistants and your system using native desktop notifications:

  1. Request Detection: When an AI assistant tries to run Node.js code or commands

  2. Notification Display: A desktop notification shows the exact command, working directory, and any input data

  3. User Control: Nothing executes until you explicitly approve

  4. Automatic Approval: If no action is taken, requests are automatically approved after 60 seconds

This security-first approach with native notifications ensures you always maintain visibility and control over what AI assistants can do on your system, while still allowing for smooth automation after the initial approval.

Getting Started with mcp-node

Setting up mcp-node is straightforward:

# Install globally
npm install -g mcp-node

# Or install as a project dependency
npm install --save-dev mcp-node

Configuration with Claude for Desktop

To use mcp-node with Claude, add it to your Claude for Desktop configuration as well as the filesystem and fetch servers:

{
  "mcpServers": {
    "node-runner": {
      "command": "npx",
      "args": ["-y", "mcp-node"],
      "env": {
        "DISABLE_NOTIFICATIONS": "false"
      }
    },
    "filesystem": {
      "command": "npx",
      "args": [
        "-y",
        "@modelcontextprotocol/server-filesystem",
        "/path/to/your/repos"
      ]
    },
    "fetch": {
      "command": "docker",
      "args": ["run", "-i", "--rm", "mcp/fetch"]
    }
  }
}

One of the most important things you must do is tell Claude (or any other MCP-compatible client) that it can use this. For my test project, I added the following to my project instructions and CLAUDE.md file:

# CLAUDE.md - FGH Project Guide

## Build & Test Commands
- Build: `npm run build`
- Clean build: `npm run clean && npm run build`
- Run all tests: `npm test`
- Run single test: `node --no-warnings --experimental-strip-types --test test/fgh.test.ts`
- Run specific test file: `node --no-warnings --experimental-strip-types --test test/comma-operator.test.ts`
- Lint: `npm run lint`
- Fix linting issues: `npm run lint:fix`
- Run examples: `npm run examples`

## Code Style Guidelines
- Use TypeScript with proper type annotations
- Follow ES module import syntax (import/export)
- Use descriptive variable names in camelCase
- Document public functions with JSDoc comments
- Use functional programming patterns when possible
- Maintain clear separation between lexer, parser, and generator
- Handle errors with proper type checking and optional chaining
- Avoid tight coupling between components
- Follow neostandard ESLint rules
- Keep functions small and focused on a single responsibility
- never add special cases for specific inputs

Check out FGH, the Flowing JSON Grep Handler

To evaluate the performance of this setup, I created a clone of JQ, the popular CLI utility, to process JSON files. This clone is called FGH, the Flowing JSON Grep Handler.

I’ve always admired JQ for its elegance in processing data in bash, and very often sought out the same capability in JS/TS apps, but I couldn’t find one. So, I built it with the help of Claude. FGH is a TypeScript implementation of the JQ language that brings powerful JSON querying capabilities to JavaScript. It allows you to write concise expressions to extract, filter, and transform complex data structures with an intuitive syntax.

FGH compiles a growing subset of the JQ language into executable JS functions, providing optimized performance for single queries and compiled expressions. Its functional approach promotes clean, maintainable code when working with nested JSON data.

Installing it as simple as npm install fgh, and using it’s even simpler:

import fgh from 'fgh';

// Sample data - perhaps from an API response
const data = {
  products: [
    { id: 'p1', name: 'Ergonomic Keyboard', price: 129.99, stock: 15, tags: ['electronics', 'office'] },
    { id: 'p2', name: 'Wireless Mouse', price: 49.99, stock: 0, tags: ['electronics', 'accessories'] },
    { id: 'p3', name: 'Monitor Stand', price: 79.99, stock: 8, tags: ['office', 'accessories'] },
    { id: 'p4', name: 'USB-C Hub', price: 39.99, stock: 22, tags: ['electronics', 'connectivity'] }
  ],
  storeInfo: {
    location: 'Online',
    currency: 'USD',
    taxRate: 0.08
  }
};

// Extract product names (one-off query)
const productNames = fgh.query('.products[].name', data);
console.log(productNames); 
// ['Ergonomic Keyboard', 'Wireless Mouse', 'Monitor Stand', 'USB-C Hub']

// Compile a reusable function for filtering in-stock products
const getInStockProducts = fgh.compile('.products[] | select(.stock > 0)');
const inStockItems = getInStockProducts(data);
console.log(inStockItems); 
// [
//   { id: 'p1', name: 'Ergonomic Keyboard', price: 129.99, stock: 15, tags: ['electronics', 'office'] },
//   { id: 'p3', name: 'Monitor Stand', price: 79.99, stock: 8, tags: ['office', 'accessories'] },
//   { id: 'p4', name: 'USB-C Hub', price: 39.99, stock: 22, tags: ['electronics', 'connectivity'] }
// ]

// Extract multiple fields with the comma operator
const productSummary = fgh.query('.products[] | {id: .id, name: .name, price_with_tax: (.price * (1 + .storeInfo.taxRate))}', data);
console.log(productSummary);
// [
//   { id: 'p1', name: 'Ergonomic Keyboard', price_with_tax: 140.3892 },
//   { id: 'p2', name: 'Wireless Mouse', price_with_tax: 53.9892 },
//   { id: 'p3', name: 'Monitor Stand', price_with_tax: 86.3892 },
//   { id: 'p4', name: 'USB-C Hub', price_with_tax: 43.1892 }
// ]

// Find products by tag using the select filter
const officeProducts = fgh.query('.products[] | select(.tags[] == "office") | .name', data);
console.log(officeProducts); 
// ['Ergonomic Keyboard', 'Monitor Stand']

We also provide a tiny CLI:

npm i fgh -g
echo '{ "question": 42 }' | fgh '.question'

Thanks to all the new features of Node.js core, fgh has no dependencies.

Brilliant at grunt work, worthless at “system thinking”

Using mcp-node to build fgh felt like magic for a few days. I could implement feature after feature (and have the tests passing!), producing roughly one day of work in one hour of AI-assisted coding.
Specifically, it felt great to create the lexer and the parser for the JQ grammar. Moreover, writing a lot of edge cases for tests takes a lot of time.

I found this setup fantastic for:

  1. Monotonous and repetitive tasks, like writing tests, fixing types, or linting. It will shine if you can set it up with a repetitive loop when it sees a failing output, quickly moving to success.

  2. By-the-book implementations are given precise requirements, such as writing an EBNF grammar, implementing a Lexer, a Parser, and even walking an Abstract Syntax Tree.

  3. Prototype creation and evolution to allow non-developers to create working prototypes quickly.

Unfortunately, it’s not all good news.

As much as I enjoyed the creation process, the code generated crumbled on itself. I had to manually write 1318 of the 7587 lines of code (numbers taken at the time of this writing), which was ~17% of the actual implementation.

Here are the critical mistakes Claude made:

  1. It tended to solve failing tests by adding special cases for them in the source code, like changing the behavior depending on the test file being executed or different for specific hardcoded inputs.

  2. It failed to realize the whole architecture was wrong, and it could never satisfy all requirements - in this case, it kept adding properties to the return data to distinguish between cases. I tried several times to have Claude fix it, but ultimately, I had to jump in and implement a new design (see https://github.com/platformatic/fgh/pull/12).

  3. It wrote conflicting tests, usually from my unclear requirements, as is usual with any development task.

Based on my experience, the future for software developers is bright.

We need to learn how to leverage AIs - and build our agents - to optimize our development workflow. Given what I see being produced, humans must stay in the loop as the conduit and auditor of AI work, or else the final product will quickly become an unmaintainable bowl of spaghetti code.

2
Subscribe to my newsletter

Read articles from Matteo Collina directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Matteo Collina
Matteo Collina