Tutorials Search / Native Mac IDE / Author your own MCP server
πŸ“ Written ● Advanced Updated 2026-05-13

How do I author my own MCP server?

TL;DR: Write a Node or Python program that implements the MCP protocol (a few hundred lines using the official SDK), expose typed tools that wrap your service's API, point LingCode at it with command + args in mcp.json. The agent now has typed access to whatever your service does.

The day you wish "the agent could just talk to our internal X" is the day you should write an MCP server. It's a few hundred lines, fronts a service you already have, and turns "the agent doesn't know about that" into "the agent has a typed tool for it."

Connecting an existing MCP server covers consuming what someone else built. Authoring is the inverse: you have a service the agent doesn't know about β€” an internal admin API, a database with a schema only your team understands, a CI system, a feature-flag platform β€” and you want the agent to be able to use it without giving it a shell and hoping for the best.

The shape of the work is small. An MCP server is a process that speaks a defined JSON-RPC protocol over stdio or HTTP. It exposes a list of tools, each with a name, description, and JSON schema for arguments and return values. When the agent calls a tool, your code runs, you talk to whatever underlying service you wanted to expose, and you return a result. That's it. The agent doesn't see credentials, doesn't reason about endpoints, doesn't make up arguments β€” it sees typed tools with sensible names.

What you get for the effort: a tool the agent uses correctly the first time, every time. No more "I asked it to look up a Linear ticket and it tried to curl the wrong URL." The semantics live in the server, where you can test them; the agent just calls them.

What you'll learn

Step 1: Pick a runtime

1

TypeScript or Python β€” both are first-class

The MCP spec is language-neutral; the two reference SDKs are:

  • TypeScript: @modelcontextprotocol/sdk. Tiny dependency footprint, runs under Node, easy to ship as an npx command. Best when the service you're wrapping is JavaScript-friendly (a Node API, a JSON service).
  • Python: mcp on PyPI. Best when the service is Python-friendly (a Django admin, a pandas pipeline, ML tooling).

Pick whichever your team writes more comfortably; the protocol is identical. The examples below use TypeScript.

Step 2: Scaffold the project

2

Minimum viable server

mkdir my-mcp-server && cd my-mcp-server
npm init -y
npm i @modelcontextprotocol/sdk zod

Then create server.ts:

import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
  CallToolRequestSchema,
  ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";

const server = new Server(
  { name: "my-internal-tools", version: "0.1.0" },
  { capabilities: { tools: {} } }
);

// Define your tools' input schemas
const LookupUserArgs = z.object({
  email: z.string().email(),
});

server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [
    {
      name: "lookup_user",
      description: "Look up a user by email in the internal database. Returns the user's id, name, plan, and signup date.",
      inputSchema: {
        type: "object",
        properties: {
          email: { type: "string", format: "email", description: "The user's email address" },
        },
        required: ["email"],
      },
    },
  ],
}));

server.setRequestHandler(CallToolRequestSchema, async (req) => {
  if (req.params.name === "lookup_user") {
    const { email } = LookupUserArgs.parse(req.params.arguments);
    const user = await fetchUserFromOurDB(email);
    return { content: [{ type: "text", text: JSON.stringify(user) }] };
  }
  throw new Error(`Unknown tool: ${req.params.name}`);
});

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

That's a working MCP server. It exposes one tool, validates its arguments, calls an underlying function, returns a structured result.

Step 3: Write the description carefully

3

The description is your prompt to the model

When the agent reads its tool list, the description is what it uses to decide when to call your tool. A vague description ("user lookup") gets called wrong; a specific one ("Look up a user by email; returns id, name, plan, and signup date") gets called correctly.

Imperative voice, name the exact inputs, mention what's returned. If there are restrictions or gotchas, put them in the description β€” not in a runtime error that the agent will see only after it's already tried wrong. "Returns 404 if the user doesn't exist; check first by calling list_users with a partial match."

Per-argument descriptions matter too. JSON Schema's description on each property is also surfaced to the agent. Spend a sentence on each: "the user's email, lowercased; pass the canonical form." Better arguments mean fewer wasted tool calls.

Step 4: Hold the credential server-side

4

The agent should never see the secret

The whole point of MCP versus "ask the agent to curl an API" is that the credential stays in the server. When you call your internal DB, the connection string lives in process.env.INTERNAL_DB_URL, set in the MCP server's environment, not in any prompt the agent sees.

This is also how you scope access: a server that holds a read-only DB role can expose lookup_user but not delete_user. The agent has no way to escalate; the credential simply doesn't grant it.

Step 5: Test the server before integrating

5

Stdio is easy to drive from a script

An MCP server over stdio reads JSON-RPC from stdin and writes to stdout. You can drive it from any test harness:

# A tiny smoke test
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | node server.js

If the server lists its tools correctly, the schema is right. Wire up the official MCP inspector (npx @modelcontextprotocol/inspector) for a richer dev loop β€” it gives you a UI for calling each tool, inspecting responses, and watching the JSON-RPC traffic.

Step 6: Wire it into LingCode

6

Same config as any other MCP server

In .claude/mcp.json (project) or ~/.claude/mcp.json (user):

{
  "mcpServers": {
    "internal": {
      "command": "node",
      "args": ["/Users/you/code/my-mcp-server/server.js"],
      "env": {
        "INTERNAL_DB_URL": "postgres://..."
      }
    }
  }
}

Tools appear as mcp__internal__lookup_user. First call per session prompts for permission. From there, the agent uses it like any built-in tool.

Logging: stderr is fair game for your server's own logs. stdout is reserved for protocol messages β€” don't console.log() there, or the agent will receive malformed JSON-RPC and disconnect. Common foot-gun in TypeScript servers; trivial to avoid once you know.

What's next