agentsclimarketplace

Mcp server dev

Skill arthjean/skills/skills/mcp-server-dev

Build MCP (Model Context Protocol) servers in TypeScript with @modelcontextprotocol/sdk. Use when writing, reviewing, or refactoring MCP server code: (1) Creating MCP servers with McpServer, (2) Registering tools with registerTool, inputSchema, outputSchema, Zod validation, (3) Defining resources and resource templates, (4) Defining prompts with arguments, (5) Transports: StdioServerTransport, NodeStreamableHTTPServerTransport, SSE, (6) Tool annotations (readOnlyHint, destructiveHint, idempotentHint), (7) Error handling and isError responses, (8) Dynamic tool loading and tool list change notifications, (9) Middleware patterns for MCP tools, (10) Testing MCP servers with vitest, (11) Publishing and configuring for Claude Code, Cursor, Windsurf, (12) Any @modelcontextprotocol/sdk imports.From its SKILL.md

Install
npx -y skills add arthjean/skills --skill mcp-server-dev

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

2 things to look at

  • no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
  • 3 stars3 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

10.9 KB, ~2.6k tokens by cl100k_base, as published. Nobody here has run it

MCP Server Development — TypeScript SDK

What is MCP

The Model Context Protocol is an open standard for connecting AI assistants (Claude, Cursor, etc.) to external tools and data. An MCP server exposes tools, resources, and prompts that clients can discover and invoke.

Quick Setup

bun init
bun add @modelcontextprotocol/sdk zod
// src/index.ts
import { McpServer } from '@modelcontextprotocol/server'
import { StdioServerTransport } from '@modelcontextprotocol/node'
import * as z from 'zod/v4'

const server = new McpServer({
  name: 'my-mcp-server',
  version: '1.0.0',
})

// Register a tool
server.registerTool(
  'greet',
  {
    description: 'Greet a user by name',
    inputSchema: z.object({
      name: z.string().describe('Name of the person to greet'),
    }),
  },
  async ({ name }) => ({
    content: [{ type: 'text', text: `Hello, ${name}!` }],
  })
)

// Start server with stdio transport
const transport = new StdioServerTransport()
await server.connect(transport)
// package.json essentials
{
  "type": "module",
  "bin": { "my-mcp-server": "./dist/index.js" },
  "scripts": {
    "build": "tsc",
    "start": "node dist/index.js"
  }
}

Three Primitives

PrimitivePurposeClient ActionExample
ToolsExecute actions, return resultsLLM decides when to callAPI calls, calculations, file ops
ResourcesExpose read-only dataUser selects, attached to contextFiles, DB records, API data
PromptsReusable prompt templatesUser selects from menuCode review template, debug prompt

Registering Tools

import * as z from 'zod/v4'

// Basic tool
server.registerTool(
  'calculate-bmi',
  {
    title: 'BMI Calculator',
    description: 'Calculate Body Mass Index from weight and height',
    inputSchema: z.object({
      weightKg: z.number().describe('Weight in kilograms'),
      heightM: z.number().positive().describe('Height in meters'),
    }),
  },
  async ({ weightKg, heightM }) => {
    const bmi = weightKg / (heightM * heightM)
    return {
      content: [{ type: 'text', text: `BMI: ${bmi.toFixed(1)}` }],
    }
  }
)

// Tool with output schema (MCP 2025-06-18)
server.registerTool(
  'calculate-bmi',
  {
    description: 'Calculate BMI',
    inputSchema: z.object({
      weightKg: z.number(),
      heightM: z.number(),
    }),
    outputSchema: z.object({
      bmi: z.number(),
      category: z.string(),
    }),
    annotations: {
      title: 'BMI Calculator',
      readOnlyHint: true,
      idempotentHint: true,
    },
  },
  async ({ weightKg, heightM }) => {
    const bmi = weightKg / (heightM * heightM)
    const output = { bmi, category: bmi < 25 ? 'normal' : 'overweight' }
    return {
      content: [{ type: 'text', text: JSON.stringify(output) }],
      structuredContent: output,
    }
  }
)

// Tool with no parameters
server.registerTool(
  'ping',
  { description: 'Health check', inputSchema: z.object({}) },
  async () => ({ content: [{ type: 'text', text: 'pong' }] })
)

See references/tools-resources-prompts.md for resources, prompts, error responses, and content types.

Tool Annotations (MCP 2025-06-18)

annotations: {
  title: 'Human-Readable Title',       // Display name
  readOnlyHint: true,                   // No side effects
  destructiveHint: false,               // Does not destroy data
  idempotentHint: true,                 // Same input = same result
  longRunningHint: false,               // Completes quickly
}

Annotations help clients decide how to present and auto-approve tools. A readOnlyHint: true tool is safer for auto-approval than a destructiveHint: true one.

Transports

// 1. Stdio — Local process, Claude Code / CLI (most common)
import { StdioServerTransport } from '@modelcontextprotocol/node'
const transport = new StdioServerTransport()
await server.connect(transport)

// 2. Streamable HTTP — Remote server, session management
import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node'
import { randomUUID } from 'node:crypto'
import express from 'express'

const app = express()
app.use(express.json())

const transport = new NodeStreamableHTTPServerTransport({
  sessionIdGenerator: () => randomUUID(),
})
await server.connect(transport)

app.post('/mcp', async (req, res) => {
  await transport.handleRequest(req, res, req.body)
})
app.get('/mcp', async (req, res) => {
  await transport.handleRequest(req, res)
})
app.delete('/mcp', async (req, res) => {
  await transport.handleRequest(req, res)
})
app.listen(3000)

// 3. Stateless HTTP — JSON responses, no SSE
const transport = new NodeStreamableHTTPServerTransport({
  sessionIdGenerator: () => randomUUID(),
  enableJsonResponse: true,  // Disables SSE, returns plain JSON
})

See references/transports.md for session management, SSE, reconnection, and multi-client patterns.

Error Handling

server.registerTool(
  'fetch-data',
  {
    description: 'Fetch data from API',
    inputSchema: z.object({ url: z.string().url() }),
  },
  async ({ url }) => {
    try {
      const res = await fetch(url)
      if (!res.ok) {
        return {
          content: [{ type: 'text', text: `HTTP ${res.status}: ${res.statusText}` }],
          isError: true,
        }
      }
      const data = await res.text()
      return { content: [{ type: 'text', text: data }] }
    } catch (err) {
      return {
        content: [{ type: 'text', text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
        isError: true,
      }
    }
  }
)

Rules:

  • Return isError: true for application-level errors (the LLM sees the error and can retry)
  • Throw exceptions only for protocol-level errors (invalid request, server bug)
  • Always include a human-readable error message in content

Client Configuration

Claude Code (~/.claude/settings.json)

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

Claude Desktop (claude_desktop_config.json)

{
  "mcpServers": {
    "my-server": {
      "command": "npx",
      "args": ["-y", "my-mcp-server"],
      "env": { "API_KEY": "..." }
    }
  }
}

Cursor (.cursor/mcp.json)

{
  "mcpServers": {
    "my-server": {
      "command": "npx",
      "args": ["-y", "my-mcp-server"],
      "env": { "API_KEY": "..." }
    }
  }
}

Project Structure

my-mcp-server/
  src/
    index.ts              # Entry: server creation + transport
    tools/
      registry.ts         # Tool registry (if many tools)
      my-tool.ts          # Individual tool definitions
      another-tool.ts
    resources/
      my-resource.ts      # Resource definitions
    prompts/
      my-prompt.ts        # Prompt definitions
    utils/
      validation.ts       # Shared Zod schemas
  bin/
    cli.js                # #!/usr/bin/env node entry
  tests/
    tools.test.ts         # Tool unit tests
  package.json
  tsconfig.json

See references/advanced-patterns.md for dynamic tool loading, middleware, tool registry, and the lazy MCP pattern.

See references/testing-distribution.md for testing with vitest, CLI setup, npm publishing, and auto-setup scripts.

Common Pitfalls

  • v2 requires z.object() wrappers. Raw shapes like { name: z.string() } no longer work — wrap with z.object({}).
  • Use zod/v4 not zod. The SDK v2 requires Zod v4 (import * as z from 'zod/v4').
  • "type": "module" is required. The SDK uses ESM. All imports need .js extensions in TypeScript.
  • Stdio transport = no console.log. Stdout is the MCP protocol channel. Use console.error for debug output, or implement MCP logging.
  • isError is for the LLM, not the protocol. Return isError: true with a helpful message so the model can understand and recover.
  • Tool names must be unique. Registering a duplicate name will overwrite the previous tool.
  • Content array must not be empty. Always return at least one content item, even for "no result" cases.
  • Schema descriptions matter. LLMs use description fields on both tools and individual parameters to decide what to call and how.

References

Done When

  • MCP server compiles without errors (tsc)
  • All tools registered with inputSchema (Zod v4) and descriptions
  • Transport configured (stdio for CLI, HTTP for remote)
  • Error handling returns isError: true with human-readable messages
  • Client configuration generated for target platform (Claude Code/Desktop/Cursor)
  • Common pitfalls avoided (ESM, zod/v4, no console.log with stdio)

Constraints (Three-Tier)

ALWAYS

  • Use zod/v4 for schema definitions — not zod
  • Use z.object({}) wrappers — raw shapes don't work in SDK v2
  • Set "type": "module" in package.json
  • Return isError: true for application errors, throw only for protocol errors
  • Include descriptions on both tools and individual parameters

ASK FIRST

  • Transport choice when not specified (stdio vs HTTP)
  • Target client platform for configuration generation

NEVER

  • Use console.log with stdio transport — stdout is the MCP channel
  • Register duplicate tool names — they silently overwrite
  • Return empty content arrays — always include at least one content item
  • Use zod import instead of zod/v4

What ships with it: 4 files

35.4 KB alongside SKILL.md

Keep looking

Skills are one crate of 326,790. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.