Mcp

Integrating MCP with Claude Desktop

A beginner-friendly guide to integrating MCP servers with Claude Desktop, covering configuration, building custom servers, debugging connections, and practical workflow examples.

Integrating MCP with Claude Desktop

Overview

Claude Desktop is Anthropic's standalone application that brings Claude to your desktop as a native app, and its most powerful feature is the ability to connect to MCP (Model Context Protocol) servers that give Claude access to your local tools, files, databases, and APIs. If you have ever wished you could just ask Claude to query your database, read your project files, or run your build scripts, MCP integration with Claude Desktop is how you get there. This guide covers everything from first-time configuration to building custom servers, debugging connection failures, and establishing real production workflows.

Prerequisites

  • Claude Desktop installed on macOS, Windows, or Linux (download from claude.ai/download)
  • Node.js 18+ installed (LTS recommended)
  • npm for package management
  • Basic familiarity with JSON configuration files
  • A terminal or command prompt
  • Comfort reading log files for debugging

What Claude Desktop Is and How It Uses MCP

Claude Desktop is not just a wrapper around the Claude web interface. It is a full MCP host, which means it contains a built-in MCP client that can connect to one or more MCP servers running on your machine. When you configure an MCP server in Claude Desktop, the application spawns that server as a child process, establishes a JSON-RPC 2.0 connection over stdio, and then exposes the server's tools to Claude during your conversations.

This is the key architectural insight: Claude Desktop acts as the host, and your MCP servers are separate processes that it manages. You do not need to write any client code. Claude Desktop handles the client side entirely. Your job is to build or configure the servers, and tell Claude Desktop where to find them.

When you start a conversation in Claude Desktop and it detects configured MCP servers, it automatically connects to each one, discovers their available tools, and makes those tools available to Claude. You will see a small hammer icon in the chat interface indicating tools are available. When Claude determines it needs to use a tool -- say, reading a file or querying a database -- it asks for your permission, calls the tool, and incorporates the result into its response.

The claude_desktop_config.json File

Everything starts with one file: claude_desktop_config.json. This is where you declare which MCP servers Claude Desktop should connect to. The location depends on your operating system:

macOS:

~/Library/Application Support/Claude/claude_desktop_config.json

Windows:

%APPDATA%\Claude\claude_desktop_config.json

Which typically resolves to:

C:\Users\YourName\AppData\Roaming\Claude\claude_desktop_config.json

Linux:

~/.config/Claude/claude_desktop_config.json

If this file does not exist, create it. The basic structure looks like this:

{
  "mcpServers": {
    "server-name": {
      "command": "node",
      "args": ["path/to/your/server.js"],
      "env": {
        "SOME_VAR": "some-value"
      }
    }
  }
}

The mcpServers object is a map of server names to their configuration. Each key is a name you choose (it shows up in the Claude Desktop UI), and each value describes how to launch the server.

Configuring Your First MCP Server

Let me walk through the simplest possible setup. We will use the official filesystem MCP server, which gives Claude read and write access to directories you specify.

First, install it globally:

npm install -g @modelcontextprotocol/server-filesystem

Then edit your claude_desktop_config.json:

{
  "mcpServers": {
    "filesystem": {
      "command": "npx",
      "args": [
        "-y",
        "@modelcontextprotocol/server-filesystem",
        "/Users/shane/projects",
        "/Users/shane/documents"
      ]
    }
  }
}

After saving the file, restart Claude Desktop completely -- not just close and reopen a conversation, but quit the application and relaunch it. Claude Desktop only reads the config file at startup.

Once it restarts, you should see the hammer icon in your chat. Click it to see the available tools. The filesystem server exposes tools like read_file, write_file, list_directory, and search_files.

Stdio Transport Configuration

Claude Desktop uses the stdio transport exclusively for local MCP servers. This means it launches your server as a child process and communicates over stdin/stdout. The configuration has three main fields:

command -- The executable to run. This is typically node, npx, python, or a path to a binary.

args -- An array of command-line arguments passed to the command. For Node.js servers, the first argument is usually the path to your server script.

env -- An optional object of environment variables to set for the server process. These are merged with the existing environment, not a replacement.

Here is a more detailed example:

{
  "mcpServers": {
    "my-project-tools": {
      "command": "node",
      "args": ["/Users/shane/mcp-servers/project-tools/server.js"],
      "env": {
        "DATABASE_URL": "postgresql://localhost:5432/myapp",
        "PROJECT_ROOT": "/Users/shane/projects/my-webapp",
        "NODE_ENV": "development"
      }
    }
  }
}

A few critical rules about stdio transport:

  1. Your server must read JSON-RPC from stdin and write JSON-RPC to stdout. The MCP SDK handles this for you.
  2. Never write anything to stdout that is not a JSON-RPC message. If you use console.log() in your server code, it goes to stdout and corrupts the protocol. Use console.error() for all logging -- it goes to stderr, which Claude Desktop captures separately.
  3. The command must be an absolute path or resolvable in PATH. Claude Desktop does not necessarily share your shell's PATH configuration. If node is not found, use the full path like /usr/local/bin/node.

Understanding the Server Lifecycle

Claude Desktop manages the lifecycle of your MCP servers, and understanding this saves a lot of debugging time.

Startup: When Claude Desktop launches (or when you restart it after a config change), it spawns each configured server as a child process. It sends an initialize message over stdio, receives the server's capabilities, and then sends an initialized notification to signal the connection is ready.

During conversations: The servers stay running in the background. When Claude needs a tool, Claude Desktop sends a tools/call request to the appropriate server, waits for the response, and feeds it back to Claude. Between tool calls, the server is idle but still running.

Shutdown: When you quit Claude Desktop, it sends a shutdown signal to each server process. If the server does not exit within a timeout, it is forcefully terminated.

Crashes: If a server process crashes, Claude Desktop will attempt to restart it when you next need a tool from that server. You will see an error in the UI if it cannot reconnect. This means a bug in your server that causes a crash on startup will result in repeated restart attempts.

Configuration changes: Claude Desktop only reads claude_desktop_config.json at startup. If you edit the file while Claude Desktop is running, nothing happens until you restart the application.

Building a Custom MCP Server for Claude Desktop

Let me walk through building a server from scratch. We will create a simple server that exposes two tools: one to get the current time in different timezones, and another to calculate hash digests of text.

mkdir mcp-utility-server
cd mcp-utility-server
npm init -y
npm install @modelcontextprotocol/sdk
// server.js
var { McpServer } = require("@modelcontextprotocol/sdk/server/mcp.js");
var { StdioServerTransport } = require("@modelcontextprotocol/sdk/server/stdio.js");
var crypto = require("crypto");
var { z } = require("zod");

var server = new McpServer({
  name: "utility-server",
  version: "1.0.0",
  description: "Utility tools for time and hashing"
});

server.tool(
  "get_current_time",
  "Get the current date and time in a specific timezone",
  {
    timezone: z.string().describe("IANA timezone name like America/New_York or UTC")
  },
  function(params) {
    try {
      var now = new Date();
      var formatted = now.toLocaleString("en-US", {
        timeZone: params.timezone,
        dateStyle: "full",
        timeStyle: "long"
      });
      return {
        content: [{ type: "text", text: formatted }]
      };
    } catch (err) {
      return {
        content: [{ type: "text", text: "Invalid timezone: " + params.timezone }],
        isError: true
      };
    }
  }
);

server.tool(
  "hash_text",
  "Generate a hash digest of the provided text",
  {
    text: z.string().describe("The text to hash"),
    algorithm: z.enum(["md5", "sha1", "sha256", "sha512"]).describe("Hash algorithm to use")
  },
  function(params) {
    var hash = crypto.createHash(params.algorithm);
    hash.update(params.text);
    var digest = hash.digest("hex");
    return {
      content: [{ type: "text", text: params.algorithm.toUpperCase() + ": " + digest }]
    };
  }
);

async function main() {
  var transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("Utility MCP server running on stdio");
}

main().catch(function(err) {
  console.error("Fatal error:", err);
  process.exit(1);
});

You also need zod as a dependency since the MCP SDK uses it for schema validation:

npm install zod

Now configure it in claude_desktop_config.json:

{
  "mcpServers": {
    "utilities": {
      "command": "node",
      "args": ["/Users/shane/mcp-utility-server/server.js"]
    }
  }
}

Restart Claude Desktop, and you can now ask things like "What time is it in Tokyo?" or "Give me the SHA-256 hash of 'hello world'" and Claude will use your tools.

Testing with the MCP Inspector

Before connecting a server to Claude Desktop, you should test it with the MCP Inspector. This is an interactive debugging tool that lets you connect to any MCP server, browse its tools, and call them manually.

npx @modelcontextprotocol/inspector node /Users/shane/mcp-utility-server/server.js

This opens a web UI (typically at http://localhost:5173) where you can:

  1. See all registered tools, resources, and prompts
  2. View the JSON schema for each tool's parameters
  3. Fill in parameters and call tools interactively
  4. See the raw JSON-RPC messages going back and forth
  5. Check for errors or unexpected responses

I strongly recommend this workflow: build your server, test it with the Inspector, then connect it to Claude Desktop. If something is broken at the protocol level, the Inspector tells you immediately with clear error messages. Debugging through Claude Desktop alone is much harder because the errors are buried in log files.

Environment Variables and Secrets in Config

The env field in your server configuration is where you pass secrets, API keys, and environment-specific settings. These environment variables are only set for the server process -- they do not leak into other servers or into Claude Desktop itself.

{
  "mcpServers": {
    "database-tools": {
      "command": "node",
      "args": ["/Users/shane/mcp-servers/db-tools/server.js"],
      "env": {
        "DB_HOST": "localhost",
        "DB_PORT": "5432",
        "DB_NAME": "production_analytics",
        "DB_USER": "readonly_user",
        "DB_PASSWORD": "s3cret-pa$$word",
        "QUERY_TIMEOUT_MS": "5000"
      }
    }
  }
}

A word of caution: claude_desktop_config.json is stored in plain text on your filesystem. If you are putting database passwords or API keys in there, make sure the file permissions are restrictive. On macOS and Linux:

chmod 600 ~/Library/Application\ Support/Claude/claude_desktop_config.json

On Windows, ensure the file is in your user profile directory (which it is by default under %APPDATA%), and consider using Windows Credential Manager for particularly sensitive values.

For secrets you do not want in the config file at all, you can have your server read from other sources -- environment variables inherited from the system, a .env file, or a secrets manager. Just be aware that the server process does inherit the system environment, so variables set in your shell profile will be available unless overridden.

Configuring Multiple Servers

Claude Desktop supports multiple MCP servers running simultaneously. Each server is an independent process with its own tools. Claude sees all tools from all connected servers and can use them in the same conversation.

{
  "mcpServers": {
    "filesystem": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-filesystem", "/Users/shane/projects"]
    },
    "github": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-github"],
      "env": {
        "GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_xxxxxxxxxxxxxxxxxxxx"
      }
    },
    "postgres": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-postgres"],
      "env": {
        "POSTGRES_CONNECTION_STRING": "postgresql://user:pass@localhost:5432/mydb"
      }
    },
    "project-tools": {
      "command": "node",
      "args": ["/Users/shane/mcp-servers/project-tools/server.js"],
      "env": {
        "PROJECT_ROOT": "/Users/shane/projects/webapp"
      }
    }
  }
}

With this configuration, you could ask Claude to "look at the recent pull requests on GitHub, check the database for any user signups in the last 24 hours, and summarize what is in my project's src directory" -- all in a single conversation. Claude will orchestrate calls across all three servers.

Keep in mind that each server is a separate process consuming memory and CPU. Four or five lightweight servers is fine. Running twenty heavy servers simultaneously will slow down your machine.

Using Community MCP Servers

The MCP ecosystem has grown rapidly. There are well-maintained community servers for common tasks. Here are the ones I use regularly:

Filesystem Server

{
  "mcpServers": {
    "filesystem": {
      "command": "npx",
      "args": [
        "-y",
        "@modelcontextprotocol/server-filesystem",
        "/Users/shane/projects",
        "/Users/shane/notes"
      ]
    }
  }
}

The arguments after the package name are the directories you allow the server to access. It will refuse to read or write outside these boundaries, which is an important security feature.

GitHub Server

{
  "mcpServers": {
    "github": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-github"],
      "env": {
        "GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_your_token_here"
      }
    }
  }
}

This gives Claude access to repositories, issues, pull requests, and file contents. Generate a personal access token with repo scope from GitHub Settings > Developer Settings > Personal Access Tokens.

PostgreSQL Server

{
  "mcpServers": {
    "postgres": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-postgres"],
      "env": {
        "POSTGRES_CONNECTION_STRING": "postgresql://analyst:readonly@localhost:5432/app_db"
      }
    }
  }
}

I strongly recommend using a read-only database user for this. You do not want Claude accidentally running DROP TABLE because you asked it to "clean up the schema."

Debugging Connection Issues

When a server fails to connect, Claude Desktop does not always give you a helpful error in the UI. You need to check the logs.

Reading Claude Desktop Logs

macOS:

tail -f ~/Library/Logs/Claude/mcp*.log

Windows:

%APPDATA%\Claude\logs\mcp*.log

Open these in any text editor or use PowerShell:

Get-Content "$env:APPDATA\Claude\logs\mcp*.log" -Tail 50 -Wait

Linux:

tail -f ~/.config/Claude/logs/mcp*.log

The logs contain the raw JSON-RPC messages between Claude Desktop and your servers, plus stderr output from your server processes. This is where you find the actual error messages.

Common Log Errors

Server process crashed on startup:

[error] Server "my-server" process exited with code 1
[error] stderr: Error: Cannot find module '/Users/shane/wrong-path/server.js'

This means the path in your args is wrong. Double-check it.

Node not found:

[error] Failed to spawn server "my-server": spawn node ENOENT

Claude Desktop cannot find the node executable. Use the full path:

{
  "command": "/usr/local/bin/node",
  "args": ["/Users/shane/server.js"]
}

On Windows you might need:

{
  "command": "C:\\Program Files\\nodejs\\node.exe",
  "args": ["C:\\Users\\shane\\server.js"]
}

Missing dependencies:

[error] stderr: Error: Cannot find module '@modelcontextprotocol/sdk'

You forgot to run npm install in your server's directory. The server process runs from whatever directory it is in, and it needs its node_modules.

Protocol error -- writing to stdout:

[error] Failed to parse server message: Unexpected token 'S', "Server sta"... is not valid JSON

This means your server is writing non-JSON content to stdout, usually a stray console.log(). Change all console.log() calls to console.error() in your server code.

Practical Workflow Examples

Database Querying Through Claude

With the PostgreSQL server configured, you can have conversations like this:

You: How many users signed up this week? Break it down by day.

Claude: Let me query the database for that. [Uses query tool: SELECT DATE(created_at) as day, COUNT() as signups FROM users WHERE created_at >= NOW() - INTERVAL '7 days' GROUP BY DATE(created_at) ORDER BY day]*

Here are the signups for this week:

  • Monday: 142
  • Tuesday: 156
  • Wednesday: 189
  • Thursday: 134
  • Friday: 201
  • Saturday: 88
  • Sunday: 67

Friday was your peak day with 201 signups. Weekend numbers dropped significantly.

File Management

With the filesystem server:

You: Look through my project's src directory and find any files that import the deprecated 'request' library. List them and suggest what to replace it with.

Claude: [Uses search_files and read_file tools to scan the codebase]

I found 4 files still using the deprecated request library...

API Interactions

With a custom server that wraps an API:

You: Check the status of our production deployment and tell me if any services are unhealthy.

Claude: [Uses check_service_health tool]

All 12 services are healthy. The API gateway response time is averaging 45ms, which is within normal range...

Performance Tips

Keep servers lightweight. Claude Desktop spawns your server at startup and keeps it running. If your server takes 10 seconds to start because it pre-loads a massive dataset, that delays the entire Claude Desktop startup.

Lazy-load expensive resources. Do not connect to databases or load files in your server's initialization code. Wait until a tool is actually called:

var dbPool = null;

function getPool() {
  if (!dbPool) {
    var { Pool } = require("pg");
    dbPool = new Pool({
      connectionString: process.env.DATABASE_URL
    });
  }
  return dbPool;
}

server.tool("query_db", "Run a read-only SQL query", { sql: z.string() }, function(params) {
  var pool = getPool(); // Only connects on first use
  return pool.query(params.sql).then(function(result) {
    return {
      content: [{ type: "text", text: JSON.stringify(result.rows, null, 2) }]
    };
  });
});

Avoid blocking the event loop. If a tool does heavy computation, use setImmediate or worker threads. A blocked event loop means the server cannot respond to heartbeat messages, and Claude Desktop may think it crashed.

Set timeouts on external calls. If your tool calls an external API and that API is down, the tool call hangs indefinitely. Always set timeouts:

var controller = new AbortController();
var timeout = setTimeout(function() { controller.abort(); }, 10000);

fetch(url, { signal: controller.signal })
  .then(function(response) { return response.json(); })
  .finally(function() { clearTimeout(timeout); });

Security Considerations

MCP servers run with the same permissions as the user who launched Claude Desktop. This means they can access anything you can access. Here is what to think about:

Filesystem boundaries. The filesystem MCP server lets you specify which directories are accessible. Use this. Do not give it access to your entire home directory or root. Be explicit about what Claude can read and write.

Database access. Always use a read-only database user for MCP database servers unless you specifically need write access. Create a dedicated user with SELECT-only grants:

CREATE USER mcp_readonly WITH PASSWORD 'secure-password';
GRANT CONNECT ON DATABASE myapp TO mcp_readonly;
GRANT USAGE ON SCHEMA public TO mcp_readonly;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO mcp_readonly;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO mcp_readonly;

API keys and tokens. Review what permissions your API tokens have. A GitHub token with delete_repo scope is far more dangerous than one with just repo:read.

Tool approval. Claude Desktop asks for your approval before calling any tool. Do not blindly click "Allow" -- read what the tool is about to do. If Claude wants to write a file or run a command, make sure you understand the implications.

Network access. MCP servers can make outbound network requests. A malicious server could exfiltrate data. Only use servers you trust -- either ones you wrote yourself, or well-known community servers with public source code.

Complete Working Example: Project Dashboard MCP Server

Let me build a real, practical MCP server that gives Claude Desktop access to a project's documentation, npm scripts, and git status. This is the kind of server I actually use day-to-day.

Project Setup

mkdir project-dashboard-mcp
cd project-dashboard-mcp
npm init -y
npm install @modelcontextprotocol/sdk zod

The Server Code

// server.js
var { McpServer } = require("@modelcontextprotocol/sdk/server/mcp.js");
var { StdioServerTransport } = require("@modelcontextprotocol/sdk/server/stdio.js");
var { z } = require("zod");
var fs = require("fs");
var path = require("path");
var childProcess = require("child_process");

var PROJECT_ROOT = process.env.PROJECT_ROOT;

if (!PROJECT_ROOT) {
  console.error("PROJECT_ROOT environment variable is required");
  process.exit(1);
}

if (!fs.existsSync(PROJECT_ROOT)) {
  console.error("PROJECT_ROOT does not exist: " + PROJECT_ROOT);
  process.exit(1);
}

var server = new McpServer({
  name: "project-dashboard",
  version: "1.0.0",
  description: "Project dashboard tools for " + path.basename(PROJECT_ROOT)
});

// Tool 1: Read project documentation
server.tool(
  "read_docs",
  "Read a documentation file from the project. Supports .md, .txt, and .rst files.",
  {
    filename: z.string().describe("Name of the doc file relative to project root, e.g. README.md or docs/setup.md")
  },
  function(params) {
    var filePath = path.resolve(PROJECT_ROOT, params.filename);

    // Security: prevent path traversal
    if (!filePath.startsWith(path.resolve(PROJECT_ROOT))) {
      return {
        content: [{ type: "text", text: "Error: path traversal detected. Access denied." }],
        isError: true
      };
    }

    var allowedExtensions = [".md", ".txt", ".rst", ".adoc"];
    var ext = path.extname(filePath).toLowerCase();
    if (allowedExtensions.indexOf(ext) === -1) {
      return {
        content: [{ type: "text", text: "Error: only documentation files are allowed (" + allowedExtensions.join(", ") + ")" }],
        isError: true
      };
    }

    try {
      var content = fs.readFileSync(filePath, "utf8");
      return {
        content: [{ type: "text", text: content }]
      };
    } catch (err) {
      return {
        content: [{ type: "text", text: "Error reading file: " + err.message }],
        isError: true
      };
    }
  }
);

// Tool 2: List available documentation files
server.tool(
  "list_docs",
  "List all documentation files (.md, .txt, .rst) in the project",
  {},
  function() {
    var docs = [];

    function walkDir(dir, prefix) {
      var entries;
      try {
        entries = fs.readdirSync(dir, { withFileTypes: true });
      } catch (err) {
        return;
      }

      entries.forEach(function(entry) {
        var relativePath = prefix ? prefix + "/" + entry.name : entry.name;

        if (entry.isDirectory() && entry.name !== "node_modules" && entry.name !== ".git") {
          walkDir(path.join(dir, entry.name), relativePath);
        } else if (entry.isFile()) {
          var ext = path.extname(entry.name).toLowerCase();
          if ([".md", ".txt", ".rst", ".adoc"].indexOf(ext) !== -1) {
            docs.push(relativePath);
          }
        }
      });
    }

    walkDir(PROJECT_ROOT, "");

    if (docs.length === 0) {
      return {
        content: [{ type: "text", text: "No documentation files found in the project." }]
      };
    }

    return {
      content: [{ type: "text", text: "Documentation files:\n" + docs.map(function(d) { return "  - " + d; }).join("\n") }]
    };
  }
);

// Tool 3: Get package.json scripts
server.tool(
  "get_npm_scripts",
  "List all npm scripts defined in the project's package.json",
  {},
  function() {
    var packagePath = path.join(PROJECT_ROOT, "package.json");

    try {
      var raw = fs.readFileSync(packagePath, "utf8");
      var pkg = JSON.parse(raw);
      var scripts = pkg.scripts || {};
      var scriptNames = Object.keys(scripts);

      if (scriptNames.length === 0) {
        return {
          content: [{ type: "text", text: "No scripts defined in package.json." }]
        };
      }

      var output = "NPM Scripts for " + (pkg.name || "unknown") + ":\n\n";
      scriptNames.forEach(function(name) {
        output += "  " + name + ": " + scripts[name] + "\n";
      });

      return {
        content: [{ type: "text", text: output }]
      };
    } catch (err) {
      return {
        content: [{ type: "text", text: "Error reading package.json: " + err.message }],
        isError: true
      };
    }
  }
);

// Tool 4: Get git status
server.tool(
  "get_git_status",
  "Get the current git status of the project including branch, modified files, and recent commits",
  {
    includeLog: z.boolean().optional().describe("Include the 5 most recent commit messages (default: false)")
  },
  function(params) {
    var output = "";

    try {
      // Current branch
      var branch = childProcess.execSync("git rev-parse --abbrev-ref HEAD", {
        cwd: PROJECT_ROOT,
        encoding: "utf8"
      }).trim();
      output += "Branch: " + branch + "\n\n";

      // Status
      var status = childProcess.execSync("git status --short", {
        cwd: PROJECT_ROOT,
        encoding: "utf8"
      }).trim();
      output += "Changes:\n" + (status || "  (working tree clean)") + "\n";

      // Optional: recent commits
      if (params.includeLog) {
        var log = childProcess.execSync('git log --oneline -5', {
          cwd: PROJECT_ROOT,
          encoding: "utf8"
        }).trim();
        output += "\nRecent commits:\n" + log + "\n";
      }

      return {
        content: [{ type: "text", text: output }]
      };
    } catch (err) {
      return {
        content: [{ type: "text", text: "Error getting git status: " + err.message }],
        isError: true
      };
    }
  }
);

// Tool 5: Get project overview
server.tool(
  "get_project_overview",
  "Get a high-level overview of the project: name, version, dependencies, and directory structure",
  {},
  function() {
    var output = "";

    // Package info
    try {
      var raw = fs.readFileSync(path.join(PROJECT_ROOT, "package.json"), "utf8");
      var pkg = JSON.parse(raw);
      output += "Project: " + (pkg.name || "unknown") + "\n";
      output += "Version: " + (pkg.version || "unknown") + "\n";
      output += "Description: " + (pkg.description || "none") + "\n\n";

      var deps = Object.keys(pkg.dependencies || {});
      var devDeps = Object.keys(pkg.devDependencies || {});
      output += "Dependencies (" + deps.length + "): " + deps.join(", ") + "\n";
      output += "Dev Dependencies (" + devDeps.length + "): " + devDeps.join(", ") + "\n\n";
    } catch (err) {
      output += "Could not read package.json: " + err.message + "\n\n";
    }

    // Top-level directory listing
    try {
      var entries = fs.readdirSync(PROJECT_ROOT, { withFileTypes: true });
      var dirs = [];
      var files = [];

      entries.forEach(function(entry) {
        if (entry.name.charAt(0) === "." || entry.name === "node_modules") return;
        if (entry.isDirectory()) {
          dirs.push(entry.name + "/");
        } else {
          files.push(entry.name);
        }
      });

      output += "Directory structure:\n";
      dirs.forEach(function(d) { output += "  " + d + "\n"; });
      files.forEach(function(f) { output += "  " + f + "\n"; });
    } catch (err) {
      output += "Could not read directory: " + err.message + "\n";
    }

    return {
      content: [{ type: "text", text: output }]
    };
  }
);

async function main() {
  var transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("Project dashboard MCP server running for: " + PROJECT_ROOT);
}

main().catch(function(err) {
  console.error("Fatal error:", err);
  process.exit(1);
});

Claude Desktop Configuration

Add this to your claude_desktop_config.json:

{
  "mcpServers": {
    "project-dashboard": {
      "command": "node",
      "args": ["/Users/shane/project-dashboard-mcp/server.js"],
      "env": {
        "PROJECT_ROOT": "/Users/shane/projects/my-webapp"
      }
    }
  }
}

Testing with the Inspector

Before connecting to Claude Desktop, verify everything works:

PROJECT_ROOT=/Users/shane/projects/my-webapp npx @modelcontextprotocol/inspector node /Users/shane/project-dashboard-mcp/server.js

In the Inspector UI, test each tool:

  • Call list_docs to see all documentation files
  • Call read_docs with filename: "README.md"
  • Call get_npm_scripts to see available scripts
  • Call get_git_status with includeLog: true
  • Call get_project_overview for the full project summary

Once everything returns clean results, restart Claude Desktop.

Real Conversation Example

Here is what a real conversation looks like with this server connected:

You: Give me an overview of this project and tell me what the recent git activity looks like.

Claude: Let me pull up the project details and recent git activity.

[Calls get_project_overview] [Calls get_git_status with includeLog: true]

Here is the overview of your project:

my-webapp (v2.3.1) is a web application with 12 production dependencies including Express, PostgreSQL, and Redis. You have 8 dev dependencies for testing and linting.

The project structure includes src/, tests/, docs/, and config/ directories, along with standard config files like Dockerfile and .eslintrc.

Git Status: You are on the feature/user-profiles branch. There are 3 modified files:

  • src/routes/users.js
  • src/models/user.js
  • tests/users.test.js

Recent commits:

  1. a3f82d1 - Add profile picture upload endpoint
  2. 9c1e4b2 - Refactor user model to include bio field
  3. 7d5fa98 - Add user profile page route
  4. 2b8c3e1 - Merge pull request #47 from feature/auth-improvements
  5. f1a9d72 - Fix token refresh race condition

It looks like you are actively working on user profiles, with changes to routes, models, and tests. Want me to look at the actual code changes or check the documentation?

You: Yes, read the README and tell me if the user profiles feature is documented yet.

Claude: [Calls read_docs with filename: "README.md"]

I have read through the README. The user profiles feature is not documented yet. The README covers installation, API endpoints for authentication and products, but there is no section for user profiles. Want me to help draft documentation for the new endpoints you are building?

Common Issues and Troubleshooting

1. Server appears in config but no tools show up

Log output:

[warn] Server "my-server" connected but reported 0 tools

This means the server starts and connects fine, but it has no tools registered. Check that your server.tool() calls are executing before the server.connect() call. If you are registering tools asynchronously, make sure you await them before connecting the transport.

2. npx server fails with EACCES

Log output:

[error] stderr: npm ERR! code EACCES
[error] stderr: npm ERR! syscall mkdir
[error] stderr: npm ERR! path /usr/local/lib/node_modules

The npx command is trying to install the package globally and lacks permissions. Fix this by using npx -y (which caches in the user directory) or install the package locally first and reference the script directly.

3. Server works in Inspector but not in Claude Desktop

Log output:

[error] Server "my-server" initialization timeout after 10000ms

The server takes too long to start. This often happens when your server's initialization code makes network calls (connecting to a database, fetching remote config). Move expensive initialization into the tool handlers so the server can respond to initialize immediately and connect to external services lazily.

4. JSON parse errors after server has been running

Log output:

[error] Failed to parse message from server "my-server":
SyntaxError: Unexpected token 'D' at position 0

Something in your code is writing to stdout. Common culprits: a library that logs to stdout by default, a console.log() in a callback you forgot about, or an unhandled promise rejection that prints to stdout. Audit every code path for stdout writes. Redirect your database driver's query logging to stderr. Set DEBUG_FD=2 if you use the debug npm package.

5. Server disconnects during long tool execution

Log output:

[error] Server "my-server" connection lost: pipe closed
[info] Attempting to restart server "my-server"

Your tool handler is taking too long and the process is being terminated. Break long-running operations into smaller chunks, or stream results back if the SDK supports it. For database queries, set a query timeout so a runaway query does not block the server indefinitely.

Best Practices

  • Always test servers with the MCP Inspector before connecting to Claude Desktop. The Inspector gives you immediate, clear feedback. Claude Desktop buries errors in log files. Save yourself the frustration.

  • Use console.error() for all logging in your MCP server, never console.log(). Stdout is reserved for the JSON-RPC protocol. A single stray console.log() will corrupt the communication and cause cryptic parse errors.

  • Lazy-load expensive resources like database connections and API clients. Connect when the first tool call arrives, not at server startup. This keeps startup fast and avoids errors when services are temporarily unavailable.

  • Implement path traversal protection in any file-access tools. Always resolve paths and verify they stay within the allowed directory. Never trust user input (or Claude's input) with raw file paths. Use path.resolve() and verify the result starts with your allowed root.

  • Use read-only database credentials for MCP database servers. Unless your workflow specifically requires writes, a read-only user eliminates an entire class of risks. Claude is good at generating correct SQL, but "good" is not "perfect."

  • Keep your claude_desktop_config.json minimal and use environment variables for secrets. Do not hardcode values that change between environments. If you share your config between machines, environment-specific values like paths and credentials should come from the env field or from system environment variables.

  • Set timeouts on all external operations inside tool handlers. Network calls, database queries, file system operations on network drives -- all of these can hang indefinitely. A 10-second timeout is reasonable for most interactive use cases.

  • Version your MCP servers. Use the version field in the server metadata and keep a changelog. When Claude Desktop updates or the MCP SDK updates, you want to know which version of your server was running when something broke.

  • Restart Claude Desktop after every config change. The application only reads claude_desktop_config.json at startup. Editing the file while Claude Desktop is running has no effect, and this trips up every developer at least once.

References

Powered by Contentful