claude codeai codingai tools

Setting Up MCP Servers with Claude Code: A Practical Walkthrough

A practical walkthrough of setting up MCP (Model Context Protocol) servers with Claude Code, covering installation, configuration, transport modes, tool definitions, and how to connect AI models to real-world APIs and services. Working examples you can copy and run today.

Setting Up MCP Servers with Claude Code: A Practical Walkthrough
Cristian Da Conceicao
Founder of Picasso IA

If you have spent any time in Claude Code and noticed the MCP section in settings, you have probably wondered what it actually does, how hard it is to wire up, and whether it is worth the effort. Short answer: yes, significantly worth it. MCP (Model Context Protocol) is the mechanism that lets Claude reach outside its context window and call real functions, query real databases, and interact with real APIs, all from inside a conversation.

This article walks through everything from understanding what MCP is to running your first custom server with Claude Code, including real configuration examples you can copy immediately.

Developer workspace with multiple monitors showing code and terminal windows

What MCP Actually Is

MCP is an open protocol developed by Anthropic that standardizes how AI models communicate with external tools and data sources. Think of it as a structured handshake: your server declares what tools it offers, and Claude calls them with proper arguments and handles the results.

Before MCP, every integration was custom. You wrote special system prompts, hacked together function-calling schemas, and hoped the model followed the spec. MCP formalizes all of that into a single, predictable layer.

The protocol defines three core primitives:

PrimitiveDescription
ToolsFunctions the model can call (e.g., search, fetch, write file)
ResourcesData the model can read (e.g., files, database records)
PromptsReusable prompt templates exposed by the server

These three primitives cover virtually every integration scenario you will encounter. Tools handle actions, resources handle data access, and prompts handle reusable interaction patterns. The protocol is transport-agnostic, meaning the same server code works over stdio for local development and over HTTP for production deployments.

Two Transport Modes

MCP servers run in one of two transport modes. Knowing the difference saves you hours of debugging.

Developer desk with laptop open to terminal showing npm install commands and handwritten notes

stdio (Standard I/O)

The client (Claude Code) spawns your server as a subprocess and communicates over stdin/stdout. This is the simplest setup for local tools and personal workflows. No ports, no networking, no authentication needed.

{
  "mcpServers": {
    "my-tool": {
      "command": "node",
      "args": ["/absolute/path/to/server/dist/index.js"]
    }
  }
}

HTTP with SSE

Your server runs as an independent HTTP process. Claude Code connects to it over the network. This is the right choice for shared team servers, cloud deployments, or any server that needs to stay running between sessions.

{
  "mcpServers": {
    "my-tool": {
      "url": "http://localhost:3000/sse"
    }
  }
}

💡 Start with stdio. It requires zero networking setup and is much easier to debug. Switch to HTTP transport only when you need shared access or persistent server state.

Installing the MCP SDK

Every MCP server starts the same way: install the official SDK and configure TypeScript for ESM.

mkdir my-mcp-server && cd my-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript tsx @types/node

Initialize TypeScript:

npx tsc --init

Update tsconfig.json to target ESM modules:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "./dist",
    "strict": true
  }
}

Add scripts to package.json:

{
  "type": "module",
  "scripts": {
    "build": "tsc",
    "dev": "tsx src/index.ts"
  }
}

The "type": "module" field is not optional. Without it, Node treats your files as CommonJS and every ESM import fails at startup.

Writing Your First Tool

Create src/index.ts and define your first tool with a typed Zod schema:

Low-angle view of dual monitors showing TypeScript MCP server code and running localhost server

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

const server = new McpServer({
  name: "my-first-server",
  version: "1.0.0",
});

server.tool(
  "get_weather",
  "Get current weather for a city",
  {
    city: z.string().describe("City name"),
    units: z.enum(["celsius", "fahrenheit"]).optional().default("celsius"),
  },
  async ({ city, units }) => {
    const temp = units === "celsius" ? "22°C" : "72°F";
    return {
      content: [
        {
          type: "text",
          text: `Weather in ${city}: ${temp}, partly cloudy`,
        },
      ],
    };
  }
);

const transport = new StdioServerTransport();
await server.connect(transport);

Build it and verify it compiles clean:

npx tsc
node dist/index.js

That is a working MCP server. It exposes one tool with a typed schema that Claude validates before calling.

Registering With Claude Code

Open Claude Code settings and navigate to the MCP section. Add your server entry using the absolute path to the compiled output:

{
  "mcpServers": {
    "my-first-server": {
      "command": "node",
      "args": ["/absolute/path/to/my-mcp-server/dist/index.js"]
    }
  }
}

Restart Claude Code. Open a new conversation and ask: "What is the weather in Paris?"

If everything is wired correctly, Claude will call get_weather with city: "Paris" and surface the result in its response. You will see the tool call appear in the conversation.

💡 Always use absolute paths in your MCP config. Relative paths break silently depending on how Claude Code resolves its working directory at launch time.

Structuring a Real Server

Side profile of focused developer in ergonomic chair reviewing a JSON config file

Real servers need clean separation between schema definitions, handlers, and service logic. Here is the file structure that scales without becoming unmaintainable:

src/
  index.ts            # Entry point and server setup
  tools/
    definitions.ts    # Zod schemas for each tool input
    handlers.ts       # Business logic per tool
  services/
    api.ts            # External API calls
    db.ts             # Database access layer

definitions.ts holds all Zod schemas:

import { z } from "zod";

export const searchInputSchema = {
  query: z.string().min(1).describe("Search query text"),
  limit: z.number().int().min(1).max(50).optional().default(10),
};

handlers.ts holds the implementation:

export async function handleSearch(
  args: { query: string; limit?: number }
) {
  const results = await searchApi(args.query, args.limit ?? 10);
  return {
    content: [{ type: "text" as const, text: JSON.stringify(results, null, 2) }],
  };
}

index.ts wires them together in a single server.tool() call per tool. This separation means you can test handlers without a running server and swap schemas without touching business logic.

5 Common Configuration Mistakes

These are the errors that trip up almost everyone building their first MCP server.

Aerial top-down flat lay of developer workstation with keyboard, notebook, and architecture diagrams

1. Missing .js file extensions in imports

Node16 module resolution requires explicit .js extensions even inside TypeScript source files. Omitting them causes runtime import failures that are confusing because the TypeScript compiler does not catch them.

// This fails at runtime with ERR_MODULE_NOT_FOUND
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp";

// This works correctly
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";

2. Using console.log() in stdio servers

In stdio mode, stdout is the protocol channel. Any console.log() call writes arbitrary text into that channel and corrupts the MCP stream. Use console.error() for all debug output.

3. Missing await on server connection

// Wrong: process may exit before connection completes
server.connect(transport);

// Correct: wait for connection handshake
await server.connect(transport);

4. Relative paths in Claude Code config

Claude Code launches from varying working directories. Always hardcode absolute paths or resolve them at startup using import.meta.url.

5. Overly broad tool descriptions

Claude uses your tool description to decide when to call it. Vague descriptions like "does stuff" lead to the tool being called too often or never. Be specific: "Fetch a GitHub pull request by owner, repo, and PR number."

HTTP Transport for Team Servers

When your server needs to be shared across a team or run in a cloud environment, HTTP with SSE is the right transport:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import express from "express";

const app = express();
const server = new McpServer({ name: "team-server", version: "1.0.0" });

const transports: Record<string, SSEServerTransport> = {};

app.get("/sse", async (req, res) => {
  const transport = new SSEServerTransport("/messages", res);
  transports[transport.sessionId] = transport;
  await server.connect(transport);
});

app.post("/messages", express.json(), async (req, res) => {
  const sessionId = req.query.sessionId as string;
  const transport = transports[sessionId];
  if (transport) await transport.handlePostMessage(req, res);
});

app.listen(3000, () => console.error("MCP server listening on :3000"));

Each Claude Code client stores its own session transport so multiple users can connect simultaneously without interference.

Exposing Resources

Resources let Claude read structured data without explicit tool calls. They are the right primitive for configuration, documentation, or cached state that Claude should be aware of without you needing to define a dedicated fetch tool.

Extreme close-up of laptop screen showing successful MCP server connection message

server.resource(
  "config://app",
  "Application configuration and feature flags",
  async (uri) => ({
    contents: [
      {
        uri: uri.toString(),
        mimeType: "application/json",
        text: JSON.stringify({
          version: "2.1.0",
          features: { darkMode: true, betaSearch: false },
          limits: { maxResults: 50, timeoutMs: 5000 },
        }),
      },
    ],
  })
);

Claude can reference config://app in its context and read this data proactively, reducing the number of tool invocations needed in a conversation.

Debugging With MCP Inspector

The MCP Inspector is essential during development. It gives you a visual interface to call your tools directly without going through Claude Code:

npx @modelcontextprotocol/inspector node dist/index.js

Open http://localhost:5173. You will see all registered tools, their schemas, and a form to call each one directly with arbitrary inputs. The raw request and response JSON is visible, making it trivial to spot type mismatches or missing fields.

For HTTP servers:

npx @modelcontextprotocol/inspector http://localhost:3000/sse

💡 Watch for schema mismatches. If Claude Code says a tool is registered but never calls it, the most common cause is a Zod schema that rejects the arguments the model provides. The Inspector lets you reproduce this without involving Claude at all.

Connecting to LLMs via MCP

One of the highest-value patterns is building MCP servers that orchestrate calls to multiple AI models. Your server becomes the middleware layer, and Claude Code becomes the coordinator.

Minimalist standing desk home office flooded with morning light from large windows

You can expose a tool that routes requests to Deepseek R1 for deep reasoning, GPT 5 for creative writing, or Llama 4 Scout Instruct for fast document processing. Claude decides which tool to call based on the task at hand.

server.tool(
  "route_to_model",
  "Route a task to the most suitable language model for the job",
  {
    task: z.enum(["reasoning", "creative", "summarize"]),
    input: z.string().describe("The text input to process"),
  },
  async ({ task, input }) => {
    const modelMap = {
      reasoning: "deepseek-r1",
      creative: "gpt-5",
      summarize: "llama-4-scout",
    };
    const result = await callModelApi(modelMap[task], input);
    return { content: [{ type: "text", text: result }] };
  }
);

With Claude Opus 4.7 as the orchestrator and your MCP server as the dispatch layer, you get a multi-model system that routes intelligently without complex infrastructure.

Picasso IA provides API access to models including Claude 4.5 Sonnet, Gemini 2.5 Flash, and Claude 4 Sonnet, making it practical to build multi-model MCP tools without managing separate API keys and client libraries for every provider.

Tool Error Handling

MCP tools should never throw unhandled exceptions. Return structured error content so Claude can report failures cleanly and decide what to do next:

server.tool(
  "safe_fetch",
  "Fetch content from an external URL",
  { url: z.string().url() },
  async ({ url }) => {
    try {
      const response = await fetch(url);
      if (!response.ok) {
        return {
          content: [
            {
              type: "text",
              text: `Request failed: HTTP ${response.status} from ${url}`,
            },
          ],
          isError: true,
        };
      }
      return { content: [{ type: "text", text: await response.text() }] };
    } catch (err) {
      return {
        content: [{ type: "text", text: `Network error: ${String(err)}` }],
        isError: true,
      };
    }
  }
);

The isError: true flag signals to Claude that the call failed. Claude will then decide whether to retry, use a fallback, or surface the error to the user.

What To Build Next

Developer's hands hovering over keyboard with TypeScript tool definitions highlighted on monitor

Once you have the basics working, the practical territory opens up. Here are the patterns teams are shipping with MCP today:

Use CaseWhat It Does
Database toolClaude writes and runs scoped SQL queries safely
File system browserRead/write access to specific project directories
API wrapperExposes Jira, GitHub, or Slack as callable tools
Image pipelineConnects Claude to text-to-image generation APIs
Code sandboxRuns and tests code snippets in an isolated container
RAG retrieverSearches a vector database and returns relevant chunks

The image pipeline use case is particularly powerful. You build an MCP server that accepts a text prompt from Claude, calls a text-to-image model, uploads the result to cloud storage, and returns the URL, all in a single tool call that Claude chains naturally in conversation without additional orchestration code.

For teams building workflows that include image generation, the 90+ models available on platforms like Picasso IA can be wired in as MCP tools, giving Claude direct access to diffusion models, upscalers, and editing pipelines from within a conversation.

Try It on Picasso IA

Young woman developer smiling at laptop in casual home setting with bookshelves and plants

If you want to see what is possible with AI models before building your own MCP integrations, Picasso IA puts over 90 text-to-image models and dozens of large language models directly in your browser. Run Claude Opus 4.6, GPT 5, Deepseek R1, or Llama 4 Maverick Instruct without any setup.

It is the fastest way to test a model's output before committing to an API integration. Pick a model, send a prompt, and see exactly what you will be working with in your MCP toolchain. Whether you are generating images for a project, testing prompt structures, or exploring how different models handle the same input, the platform gives you fast access without the infrastructure overhead.

Create an account, pick a model, and start generating images or text today, no config files required.

Share this article