Module 7: Testing, Debugging, and Integration
Troubleshooting and Common Errors
Troubleshooting and Common Errors
Capsule description
This capsule is your reference guide. When something fails — and it will fail — you come here. Every error has the same format: symptoms (what you see), diagnosis (how to investigate), cause (why it happens), and solution (how to fix it). No ambiguity, no beating around the bush.
The errors are organized by category: connection, transport, tools, resources, permissions, and configuration. At the end, there's a mini-project that integrates everything learned in this module: building a complete test suite for one of your MCP servers.
Connection errors
Error 1: "Server failed to start"
Symptoms: Claude Code shows "disconnected" in /mcp. MCP Inspector doesn't connect.
Diagnosis:
# Run the server directly
node dist/index.js < /dev/null 2>&1
echo $? # exit code other than 0 = error
# For Python
python server.py < /dev/null 2>&1
Causes and solutions:
| Cause | Solution |
|---|---|
| Build not updated | npm run build |
| Missing dependency | npm install |
| Incorrect path in settings | Verify the absolute path in ~/.claude/settings.json |
| Port in use (HTTP transport) | Change the port or kill the existing process |
| Syntax error in the code | Review the output of tsc --noEmit |
Error 2: "Connection refused"
Symptoms: The client tries to connect but receives "connection refused."
Diagnosis:
# Verify that the server is running
ps aux | grep "node dist/index.js"
# Verify the port (for HTTP transport)
lsof -i :3000
Cause: The server isn't listening or is listening on a different port/interface.
Solution:
// Verify that the transport starts correctly
const transport = new StdioServerTransport();
await server.connect(transport);
// For stdio, there's no port — the problem is that the server doesn't start
Error 3: "ENOENT: no such file or directory"
Symptoms: Claude Code can't find the server's executable.
Diagnosis:
# Verify that the file exists
ls -la ~/path/to/dist/index.js
# Verify that the command exists
which node
which python
Solution: Use absolute paths in the configuration:
{
"mcpServers": {
"my-server": {
"command": "/usr/local/bin/node",
"args": ["/Users/your-user/projects/my-server/dist/index.js"]
}
}
}
Transport errors
Error 4: "Unexpected token in JSON"
Symptoms: The server starts but requests fail with "parse error."
Diagnosis: Look for non-JSON output on stdout:
node dist/index.js < /dev/null 2>/dev/null
# If you see text that is NOT JSON → that's the problem
Cause: console.log() in a server with stdio transport. Any text on stdout that isn't JSON-RPC breaks the client's parser.
Solution:
// ❌ Breaks stdio
console.log("Server started");
// ✅ Use stderr for logs
console.error("[INFO] Server started");
Review all your code looking for console.log and change it to console.error. Include dependencies that might use console.log internally.
Error 5: "Timeout waiting for response"
Symptoms: The request is sent but the response never arrives.
Diagnosis:
// Add an explicit timeout in your tool
async ({ filePath }) => {
console.error(`[DEBUG] read_file start: ${filePath}`);
const start = Date.now();
try {
const content = await fs.readFile(filePath, "utf-8");
console.error(`[DEBUG] read_file completed in ${Date.now() - start}ms`);
return { content: [{ type: "text" as const, text: content }] };
} catch (error) {
console.error(`[ERROR] read_file failed after ${Date.now() - start}ms`);
throw error;
}
}
Common causes:
| Cause | Solution |
|---|---|
| I/O operation that never resolves | Add a timeout with Promise.race |
| Deadlock in async code | Check that there are no circular awaits |
| Server processing a previous request | Verify that there are no blocking operations |
| Network call to a downed service | Add a timeout to HTTP requests |
Error 6: "Protocol version mismatch"
Symptoms: "Unsupported protocol version" in the initialization response.
Cause: Incompatible SDK versions between client and server.
Solution:
# Update the SDK
npm install @modelcontextprotocol/sdk@latest
# Verify the installed version
npm list @modelcontextprotocol/sdk
Tools errors
Error 7: "Tool not found"
Symptoms: Claude Code tries to use a tool but receives "tool not found."
Diagnosis:
# Verify with MCP Inspector
npx @modelcontextprotocol/inspector node dist/index.js
# Does the tool appear in the list?
Causes and solutions:
| Cause | Solution |
|---|---|
| Tool not registered | Verify that server.tool(...) runs |
| Typo in the tool's name | Compare the name in the code vs the one Claude Code uses |
| Conditional registration that fails | Check that the condition is met |
| Outdated build | npm run build |
Error 8: "Invalid arguments"
Symptoms: "Validation error" or "Invalid arguments" when invoking a tool.
Diagnosis: Review which arguments Claude Code sends vs what your schema expects:
In MCP Inspector → Messages panel:
Request: { "name": "read_file", "arguments": { "path": "/tmp/test.txt" } }
^^^^
Your schema expects "filePath", not "path"
Cause: Mismatch between what Claude Code sends and what your Zod schema expects.
Solution:
// Verify that your description guides Claude correctly
server.tool(
"read_file",
"Reads a file. The filePath parameter must be the full path to the file.",
{ filePath: z.string().describe("Full path to the file (e.g., /home/user/file.txt)") },
// ...
);
Error 9: "Tool execution failed"
Symptoms: The tool is invoked but returns a generic error.
Diagnosis: Add detailed logging in the handler:
async ({ filePath }) => {
try {
// ... your logic
} catch (error) {
console.error("[ERROR] Tool failed:", error);
return {
content: [{ type: "text" as const, text: `Error: ${(error as Error).message}\nStack: ${(error as Error).stack}` }],
isError: true,
};
}
}
Cause: Uncaught exception in the tool's handler. Without try/catch, the error propagates as a generic protocol error.
Resources errors
Error 10: "Resource not found"
Symptoms: Trying to read a resource returns "resource not found."
Diagnosis:
# Verify that the resource is registered
# In MCP Inspector, Resources panel → does the URI appear?
Cause: The request's URI doesn't match the registered URI exactly.
Solution:
// Verify the exact match
server.resource(
"project-status",
"status://project", // ← This exact URI
{ description: "..." },
async () => { ... }
);
// The client must use exactly "status://project"
// Not "status://project/" (trailing slash)
// Not "Status://project" (case sensitive)
Error 11: "Resource returned invalid data"
Symptoms: The resource returns data but the client can't process it.
Cause: The response format doesn't comply with the MCP schema.
Solution: Make sure to return the correct format:
// ✅ Correct format
async () => ({
contents: [{
uri: "status://project",
text: JSON.stringify({ status: "ok" }),
mimeType: "application/json",
}],
})
// ❌ Incorrect format (missing uri or mimeType)
async () => ({
contents: [{
text: "something",
}],
})
Permissions and configuration errors
Error 12: "Permission denied"
Symptoms: Claude Code refuses to invoke a tool.
Cause: The MCP permissions aren't configured or were denied.
Solution:
# Verify permissions in Claude Code
/mcp
# If the tool appears but can't be used, review the filesystem permissions
ls -la /path/that/the/tool/accesses
Error 13: "JSON parse error in settings"
Symptoms: Claude Code doesn't recognize any MCP server.
Diagnosis:
# Validate the settings file's JSON
python3 -c "import json; json.load(open('$HOME/.claude/settings.json'))"
Common causes: Trailing comma, single quotes instead of double, or comments (JSON doesn't support comments).
// ❌ Common JSON errors
{
"mcpServers": {
"server": {
"command": "node",
"args": ["index.js"], // ← trailing comma if it's the last field
}
}
}
// ✅ Valid JSON
{
"mcpServers": {
"server": {
"command": "node",
"args": ["index.js"]
}
}
}
Quick reference table
| Error | Most probable cause | Quick fix |
|---|---|---|
| Server disconnected | Server crashes on startup | npm run build && node dist/index.js < /dev/null |
| Connection refused | Server isn't running | Check the process and path |
| ENOENT | Incorrect path in settings | Use absolute paths |
| JSON parse error (transport) | console.log in stdio server | Change to console.error |
| Timeout | Blocking I/O operation | Add a timeout with Promise.race |
| Tool not found | Tool not registered or old build | npm run build, verify with Inspector |
| Invalid arguments | Schema mismatch | Verify the parameter names in Zod |
| Resource not found | URI mismatch (case, trailing slash) | Compare the exact URI |
| Permission denied | Permissions not accepted | /mcp to check the status |
| Settings parse error | Invalid JSON | Validate with python -c "import json; ..." |
Mini-project: Complete test suite for an MCP server
Goal
Build a complete test suite for one of the MCP servers you created in modules 4 or 5. The test suite must cover the three testing layers and serve as a template for the module 8 project.
Requirements
Your test suite must include:
- At least 3 unit tests for tools — happy path, error handling, edge cases
- At least 2 unit tests for resources — valid data, correct format
- At least 3 integration tests — capabilities, protocol, complete flow
- At least 2 error handling tests — invalid inputs, downed services
- Setup and teardown — temporary files, cleanup, fixtures
- Logging — the server must have logging to stderr
Project structure
my-mcp-server/
├── src/
│ ├── index.ts # Server with createServer() exported
│ └── logger.ts # Logger to stderr
├── tests/
│ ├── tools.test.ts # Unit tests for tools
│ ├── resources.test.ts # Unit tests for resources
│ └── server.test.ts # Integration tests
├── vitest.config.ts
├── tsconfig.json
└── package.json
For Python:
my-mcp-server-python/
├── server.py
├── logger.py
├── tests/
│ ├── __init__.py
│ ├── test_tools.py
│ ├── test_resources.py
│ └── test_server.py
└── pyproject.toml
Step 1: Test framework setup
TypeScript:
npm install -D vitest
// vitest.config.ts
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
globals: true,
testTimeout: 10000,
},
});
Python:
pip install pytest pytest-asyncio
# pyproject.toml
[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]
Step 2: Testing helper
// tests/helpers.ts
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
import { createServer } from "../src/index.js";
export async function createTestClient(): Promise<{ client: Client; cleanup: () => Promise<void> }> {
const server = createServer();
const client = new Client({ name: "test-client", version: "1.0.0" });
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
await server.connect(serverTransport);
await client.connect(clientTransport);
return {
client,
cleanup: async () => { await client.close(); },
};
}
Step 3: Write the tests
Use the patterns from capsule 02 as a reference. Here's a minimal template:
// tests/tools.test.ts
import { describe, it, expect, beforeAll, afterAll } from "vitest";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { createTestClient } from "./helpers.js";
import fs from "fs/promises";
import os from "os";
import path from "path";
let client: Client;
let cleanup: () => Promise<void>;
let tmpDir: string;
beforeAll(async () => {
({ client, cleanup } = await createTestClient());
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "mcp-test-"));
// Create test files according to your tools
});
afterAll(async () => {
await fs.rm(tmpDir, { recursive: true, force: true });
await cleanup();
});
describe("Tool: [your tool's name]", () => {
it("happy path — returns correct result", async () => {
const result = await client.callTool({
name: "your_tool",
arguments: { /* valid parameters */ },
});
expect(result.isError).toBeUndefined();
// Verify the content of the result
});
it("error handling — invalid input", async () => {
const result = await client.callTool({
name: "your_tool",
arguments: { /* invalid parameters */ },
});
expect(result.isError).toBe(true);
});
it("edge case — [describe the edge case]", async () => {
// Test for a specific edge case of your tool
});
});
// tests/server.test.ts
import { describe, it, expect, beforeAll, afterAll } from "vitest";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { createTestClient } from "./helpers.js";
let client: Client;
let cleanup: () => Promise<void>;
beforeAll(async () => {
({ client, cleanup } = await createTestClient());
});
afterAll(async () => { await cleanup(); });
describe("Server integration", () => {
it("lists all the registered tools", async () => {
const { tools } = await client.listTools();
expect(tools.length).toBeGreaterThan(0);
// Verify that your specific tools appear
});
it("each tool has a description and schema", async () => {
const { tools } = await client.listTools();
for (const tool of tools) {
expect(tool.description).toBeTruthy();
expect(tool.inputSchema).toBeDefined();
}
});
it("resources are accessible", async () => {
const { resources } = await client.listResources();
for (const resource of resources) {
const result = await client.readResource({ uri: resource.uri });
expect(result.contents).toHaveLength(1);
}
});
});
Step 4: Run and verify
# TypeScript
npm test
# Python
pytest -v
# Expected output:
# ✓ Tool: [name] > happy path
# ✓ Tool: [name] > error handling
# ✓ Tool: [name] > edge case
# ✓ Resource: [name] > valid data
# ✓ Resource: [name] > correct format
# ✓ Server > lists tools
# ✓ Server > tools with description
# ✓ Server > accessible resources
# ✓ Error handling > invalid input
# ✓ Error handling > downed service
#
# 10 tests passed
Mini-project success criteria
□ At least 10 tests in total
□ Covers tools (happy path + error + edge case)
□ Covers resources (data + format)
□ Covers integration (capabilities + protocol)
□ Setup/teardown works (temporary files are cleaned up)
□ All the tests pass green
□ The server has logging to stderr
□ npm test / pytest runs everything with a single command
Module summary
Across the 5 capsules of this module, you learned:
- Capsule 01: The transition from "it works" to "it works reliably" — why testing is an investment, not bureaucracy
- Capsule 02: Automated tests with Vitest and pytest — unit tests, integration tests, InMemoryTransport
- Capsule 03: Debugging with MCP Inspector, logging to stderr, performance tracing
- Capsule 04: Claude Code configuration — settings, scopes, permissions, step-by-step verification
- Capsule 05: Troubleshooting guide for the 13 most common errors + test suite mini-project
What you can do now
- ✅ Write automated tests for any MCP server
- ✅ Debug problems with MCP Inspector and logging
- ✅ Configure MCP servers in Claude Code with confidence
- ✅ Diagnose and resolve common errors without searching Google
- ✅ Build a complete test suite as a base for real projects
What comes next
Module 8 is the final project. Everything converges: you build a production-ready MCP server connected to real data, with a complete test suite, documentation, and an end-to-end demo in Claude Code. The testing, debugging, and configuration patterns you learned here are the direct foundation of the project.
The transition is: "You already know how to build, test, debug, and connect MCP servers. Now build a real one from start to finish."
Additional resources
- Vitest Documentation — Testing framework for TypeScript
- pytest Documentation — Testing framework for Python
- MCP Inspector — Visual debugging tool
- Claude Code MCP Documentation — Official configuration
- MCP Specification — Error Handling — Error handling in the protocol
- MCP TypeScript SDK — Official SDK with examples
- MCP Python SDK — Official SDK for Python
- Node.js Debugging Guide — Debugging Node.js applications