DS DevShelfHub Projects · AI tools
Tutorials / Claude Code / Building MCP Servers
Claude Code Advanced · 11 min read Page 21 of 25

Building Custom MCP Servers

By DevShelfHub

Earlier you connected Claude to existing MCP servers. Now you'll build your own — exposing your organization's tools, data, and prompt templates, packaging it as a plugin, and sharing it with your team or the community marketplace.

Series progress21 / 25
Building Custom MCP Servers — Claude Code tutorial on server architecture, Node.js stdio servers, and plugin packaging

MCP server architecture

An MCP server is a process that speaks the MCP protocol. It exposes three kinds of capability to Claude:

Tools

Functions Claude can call — like API endpoints.

Resources

Data Claude can read — like file contents or database records.

Prompts

Pre-defined instruction templates invokable as slash commands.

The server communicates over stdio (local) or HTTP/SSE (remote). Claude Code connects to it through your .mcp.json or user-level MCP config.

A simple stdio server in Node.js

Here's a minimal server that exposes one tool — fetching a Jira issue by key. Register a tools/list handler so Claude knows what's available, and a tools/call handler to do the work.

javascript
// my-company-mcp/index.js
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";

const server = new Server({
  name: "my-company-tools",
  version: "1.0.0"
}, {
  capabilities: { tools: {}, resources: {}, prompts: {} }
});

// Register a tool
server.setRequestHandler("tools/call", async ({ params }) => {
  if (params.name === "get_jira_issue") {
    const issue = await jiraClient.getIssue(params.arguments.issue_key);
    return {
      content: [{ type: "text", text: JSON.stringify(issue) }]
    };
  }
});

// Register tool definitions
server.setRequestHandler("tools/list", async () => ({
  tools: [{
    name: "get_jira_issue",
    description: "Fetch a Jira issue by key. Returns title, description, status, and comments.",
    inputSchema: {
      type: "object",
      properties: {
        issue_key: { type: "string", description: "Jira issue key, e.g. PROJ-123" }
      },
      required: ["issue_key"]
    }
  }]
}));

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

Packaging as a Claude Code plugin

To distribute the server, wrap it in a plugin. A plugin bundles one or more MCP servers alongside skills and docs in a predictable layout:

text
my-company-plugin/
  plugin.json           # Plugin manifest
  servers/
    jira/
      index.js          # MCP server entry point
      package.json
  skills/
    create-issue.md     # Claude skill that uses the MCP tools
  README.md

The manifest declares the server, its launch command, and any environment variables:

json
// plugin.json
{
  "name": "my-company-tools",
  "version": "1.0.0",
  "description": "Internal tools for Acme Corp development workflow",
  "mcpServers": {
    "jira": {
      "command": "node",
      "args": ["servers/jira/index.js"],
      "env": {
        "JIRA_API_TOKEN": "${JIRA_API_TOKEN}",
        "JIRA_DOMAIN": "${JIRA_DOMAIN}"
      }
    }
  }
}

Resources and prompts

Beyond tools, expose resources Claude can @-reference and prompts that show up as slash commands. Register a list handler for each:

javascript
// Expose resources (data Claude can @-reference)
server.setRequestHandler("resources/list", async () => ({
  resources: [{
    uri: "jira://recent-issues",
    name: "Recent Jira Issues",
    description: "Issues created or updated in the last 7 days"
  }]
}));

// Expose prompts (slash commands)
server.setRequestHandler("prompts/list", async () => ({
  prompts: [{
    name: "create-issue-from-bug",
    description: "Create a Jira issue from a bug report",
    arguments: [
      { name: "description", description: "Bug description", required: true }
    ]
  }]
}));

Tips for good tool design

The model only knows what your descriptions tell it. Treat tool definitions like documentation written for a smart colleague who can't see your code.

  • Write descriptions as if explaining to a non-technical colleague.
  • Use concrete examples in descriptions — "e.g. PROJ-123".
  • Keep tool scope narrow: one tool, one responsibility.
  • Return structured data Claude can reason about, not just raw JSON.
  • Add error messages that help Claude understand what went wrong and how to fix it.

Submitting to the marketplace

Ready to share publicly? The community marketplace path is straightforward:

1.

Host the plugin on a public GitHub repository.

2.

Follow the plugin structure exactly.

3.

Add a comprehensive README with setup instructions.

4.

Submit via the marketplace submission form in the official docs.

5.

Plugins go through Anthropic security review before listing.

Practice project

Build an MCP server that connects Claude Code to your most-used work tool (Jira, Linear, Notion, PagerDuty, …). Expose two or three well-described tools, package the server as a Claude Code plugin, and share it with your team.

Once your server works, learn how to drive it programmatically with The Agent SDK, or revisit the full Claude Code tutorial series to see where MCP fits in the bigger picture.

Notes

Tool descriptions are your API docs

Claude picks tools from names and descriptions alone. Vague one-liners cause wrong tool calls; include parameter examples, id formats, and what the tool returns on failure.

Stdio servers must flush and exit cleanly

Debug console.log on stdout corrupts the JSON-RPC stream. Log to stderr only. Hung processes leave Claude Code with a zombie server until session restart.

Context bloat from too many tools

Every registered tool adds tokens to every turn. Split large integrations into focused servers and enable MCP tool search on the client when you exceed a dozen tools.

OAuth in CI needs pre-provisioned tokens

Remote HTTP servers with browser OAuth work interactively but break in headless CI. Store refresh tokens in secrets and document the rotation path before shipping a server to production pipelines.

Quick summary

  • An MCP server exposes tools, resources, and prompts over stdio or HTTP/SSE
  • A minimal Node.js server needs tools/list and tools/call handlers
  • Package it as a plugin with plugin.json to distribute servers, skills, and docs together
  • Write descriptions for a non-technical reader, keep tools narrow, and submit to the marketplace via GitHub

Building MCP Servers FAQ

What is an MCP server?

An MCP server is a process that speaks the MCP protocol and exposes three kinds of capability to Claude: tools Claude can call, resources Claude can read, and prompts that show up as slash commands.

How do you build a custom MCP server for Claude Code?

Build a process that speaks the MCP protocol and registers handlers for the capabilities you want to expose. A minimal Node.js server needs a tools/list handler so Claude knows what's available and a tools/call handler to do the work.

How does an MCP server communicate with Claude Code?

The server communicates over stdio for local servers or HTTP/SSE for remote ones. A local stdio server in Node.js connects through a StdioServerTransport.

How do you package an MCP server as a Claude Code plugin?

Wrap the server in a plugin that bundles one or more MCP servers alongside skills and docs in a predictable layout. A plugin.json manifest declares each server, its launch command, and any environment variables.

How do you connect a custom MCP server to Claude Code?

Claude Code connects to the server through your .mcp.json or user-level MCP config. When packaged as a plugin, the plugin.json manifest declares the server and its launch command for Claude Code to start.