agentleFS
LIXIAO199115LIXIAO199115/skillrig/skills/mcp-server-builder/SKILL.md

mcp-server-builder

Skill0 starsChanged 25 days agoMITView on GitHub

Use when building or debugging a Model Context Protocol server over stdio for Codex, Claude Code, Cursor, or another MCP client. Covers the JSON-RPC handshake, tools/list and tools/call schemas, error handling, logging, input validation, timeout and size limits, tests, and registration with codex mcp add.

17 other projects use a version of this.

Adopted by

Most recently taken up by

Used with

Projects with it also use agent-designer, env-secrets-manager and 1 more.

Other files people use alongside this one.

Files found in the same public projects as a version of this one, more often than they turn up anywhere else. Each project counts once, large collections count for less, and this file's own repository is left out.
  • Arnav-0Skill

    agent-designer

    Use when the user asks to design multi-agent systems, create agent architectures, define agent communication patterns, or build autonomous agent workflows.

    Also in 4 projects that use this

  • Arnav-0Skill

    env-secrets-manager

    Env & Secrets Manager

    Also in 3 projects that use this

  • JeffallanSkill

    rag-architect

    Designs and implements production-grade RAG systems by chunking documents, generating embeddings, configuring vector stores, building hybrid search pipelines, applying reranking, and evaluating retrieval quality. Use when building RAG systems, vector databases, or knowledge-grounded AI applications requiring semantic search, document retrieval, context augmentation, similarity search, or embedding-based indexing.

    Also in 3 projects that use this

The file

We split this skill into the rules it gives an agent

11 rules. 0 are common, 7 are distinctive, and 4 are very specific to this project.

  • 0

    Common

    Most of the 13 edited versions we read keep these.

  • 7

    Distinctive

    This author's own choices. Worth a look.

  • 4

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Use this skill when:

    line 22
  2. Very specific:

    Every request with an id gets exactly one response with the same id.

    line 64
  3. Very specific:

    Write one JSON object per line, terminated by \n.

    line 66
  4. Distinctive:

    Never write logs, banners, or progress to stdout.

    line 67
  5. Distinctive:

    Do not expose twenty endpoints because the underlying API has twenty.

    line 81
  6. Very specific:

    Use enum for fixed choices.

    line 99
  7. Distinctive:

    Do not accept a free-form shell command when a structured argument works.

    line 100
  8. Distinctive:

    Test malformed input and unknown tools too.

    line 201
  9. Very specific:

    Never put a token in the command line where it appears in process listings or shell history.

    line 217
  10. Distinctive:

    Never pass an argument to a shell.

    line 237
  11. Distinctive:

    Keep credentials in the environment, never in tool output.

    line 239

“Common” is measured: kept by at least 6 in 10 of the 13 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

MCP Server Builder

An MCP server is a process that exposes tools, resources, or prompts to an agent over JSON-RPC. The stdio transport is newline-delimited JSON: one message per line, no headers, no extra output.

Almost every broken server fails on one of four things: it prints logs to stdout, it never answers initialize, its tool schema does not match its arguments, or it throws instead of returning a tool error.

When to use

Use this skill when:

  • You need to expose a local API, database, or file operation to an agent.
  • A registered MCP server shows no tools.
  • A tool call hangs, returns an invalid response, or crashes the client.
  • You are reviewing an MCP server for security.
  • You are packaging a server inside a Codex plugin.

Minimal protocol

The client starts the process and sends:

{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"client","version":"1.0.0"}}}

The server replies with its capabilities and server info. The client then sends:

{"jsonrpc":"2.0","method":"notifications/initialized"}

Tool discovery:

{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}

Tool call:

{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"echo","arguments":{"text":"hi"}}}

Response:

{"jsonrpc":"2.0","id":3,"result":{"content":[{"type":"text","text":"hi"}]}}

Rules:

  • Every request with an id gets exactly one response with the same id.
  • Notifications have no id and get no response.
  • Write one JSON object per line, terminated by \n.
  • Never write logs, banners, or progress to stdout.
  • Protocol errors use JSON-RPC error codes. Tool failures use result.isError with content, so the model can read the failure.

Workflow

1. Decide the tool surface

A good tool is:

  • Named as a verb and a noun: search_issues, read_file, create_invoice.
  • Described in one sentence that states when to use it.
  • Narrow enough that its arguments are obvious.
  • Safe by default: read-only unless a write is the point.

Do not expose twenty endpoints because the underlying API has twenty. Expose the five operations an agent needs, with clear names.

2. Define input schemas

{
  "type": "object",
  "additionalProperties": false,
  "required": ["query"],
  "properties": {
    "query": { "type": "string", "minLength": 1, "maxLength": 500 },
    "limit": { "type": "integer", "minimum": 1, "maximum": 50, "default": 10 }
  }
}
  • Set additionalProperties: false.
  • Bound strings, numbers, and array lengths.
  • Use enum for fixed choices.
  • Do not accept a free-form shell command when a structured argument works.
  • Validate again at runtime. The client may not enforce the schema.

3. Implement the loop

import readline from 'node:readline';

const send = (message) => process.stdout.write(`${JSON.stringify(message)}\n`);
const text = (value) => ({ content: [{ type: 'text', text: String(value) }] });

const tools = [
  {
    name: 'echo',
    description: 'Return the input text. Use to test connectivity.',
    inputSchema: {
      type: 'object',
      additionalProperties: false,
      required: ['text'],
      properties: { text: { type: 'string', maxLength: 1000 } },
    },
  },
];

const rl = readline.createInterface({ input: process.stdin, crlfDelay: Infinity });

rl.on('line', async (line) => {
  if (!line.trim()) return;
  let request;
  try {
    request = JSON.parse(line);
  } catch {
    send({ jsonrpc: '2.0', id: null, error: { code: -32700, message: 'Parse error' } });
    return;
  }

  if (request.method === 'initialize') {
    send({
      jsonrpc: '2.0',
      id: request.id,
      result: {
        protocolVersion: request.params?.protocolVersion ?? '2025-06-18',
        capabilities: { tools: {} },
        serverInfo: { name: 'echo-server', version: '1.0.0' },
      },
    });
    return;
  }

  if (request.method === 'notifications/initialized') return;

  if (request.method === 'tools/list') {
    send({ jsonrpc: '2.0', id: request.id, result: { tools } });
    return;
  }

  if (request.method === 'tools/call') {
    const { name, arguments: args = {} } = request.params ?? {};
    try {
      if (name === 'echo') {
        if (typeof args.text !== 'string') throw new Error('text must be a string');
        send({ jsonrpc: '2.0', id: request.id, result: text(args.text) });
      } else {
        send({ jsonrpc: '2.0', id: request.id, error: { code: -32602, message: `Unknown tool: ${name}` } });
      }
    } catch (error) {
      send({
        jsonrpc: '2.0',
        id: request.id,
        result: { isError: true, content: [{ type: 'text', text: error.message }] },
      });
    }
    return;
  }

  if (request.method === 'ping') {
    send({ jsonrpc: '2.0', id: request.id, result: {} });
    return;
  }

  if (request.id !== undefined) {
    send({ jsonrpc: '2.0', id: request.id, error: { code: -32601, message: 'Method not found' } });
  }
});

console.error('echo-server ready'); // stderr only

4. Test without a client

printf '%s\n' \
  '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"t","version":"1"}}}' \
  '{"jsonrpc":"2.0","method":"notifications/initialized"}' \
  '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' \
  '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"echo","arguments":{"text":"hi"}}}' \
  | node server.mjs

Expected: four lines of JSON, no extra text. The notification produces no response, so three responses plus silence.

Test malformed input and unknown tools too. A server that crashes on bad JSON will take down the session.

5. Register with Codex

codex mcp add echo -- node /absolute/path/server.mjs
codex mcp list
codex mcp get echo

For an HTTP server:

codex mcp add remote --url https://example.com/mcp

Never put a token in the command line where it appears in process listings or shell history. Use --env or the server's own environment.

6. Handle failure modes

FailureResponse
Invalid JSONJSON-RPC -32700, keep serving
Unknown method-32601
Invalid arguments-32602 or isError with a readable message
Tool exceptionresult.isError with content
TimeoutAbort the work, return isError, do not hang forever
Oversized inputReject before parsing
Upstream 5xxReturn a short error with retry guidance
Client disconnectsStop work, exit cleanly

Set timeouts on every network call. Cap response size. Stream only if the client supports it.

Security

  • Validate every argument. The model can send anything.
  • Never pass an argument to a shell. Use an argument array.
  • Restrict file paths to an allowed root.
  • Keep credentials in the environment, never in tool output.
  • Make read-only tools read-only at the implementation level, not only in the description.
  • For destructive tools, require an explicit confirmation argument and return a dry-run result first.
  • Rate-limit expensive tools.
  • Log to stderr with secrets redacted.

Common failures

SymptomCauseFix
Client shows zero toolstools/list missing or wrong shapeReturn { tools: [...] }
Connection closes immediatelyPrinted to stdout before the handshakeRemove all stdout logging
Tool call hangsNo response sent on that pathEnsure every request id gets a response
Invalid argumentsSchema and runtime disagreeCompare inputSchema with the handler
Works in curl, not in clientNewline framing or bufferingFlush after each message, one per line
Random crashesUnhandled promise rejectionCatch per request

Verification checklist

  • Handshake works with a raw stdin test.
  • tools/list describes every tool with a bounded schema.
  • Every tool call returns a response, including errors.
  • Invalid JSON does not kill the process.
  • Logs go to stderr only.
  • Timeouts and size limits exist.
  • Arguments are validated at runtime.
  • File paths and shell arguments are contained.
  • Registration with codex mcp add works and codex mcp list shows it.
  • A real agent can call each tool.

References

  • references/protocol.md contains message examples, error codes, and a tool schema checklist.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.