How to build your own MCP server
Expose your own stack to an LLM agent: tools, resources, and transport; a minimal server; wrapping a real API safely with auth the model never sees; and deploying it Dockerized, scheduled, and logged.
What an MCP server is, and when you need a custom one
The Model Context Protocol is a standard way to hand an LLM a set of tools it can call and resources it can read. An MCP server is the process that exposes those tools; the agent (Claude, or an n8n AI Agent node) is the client that calls them. Plenty of services ship an official MCP server already — you need a custom one when you want to expose your own stack: an internal API, a database, a business process, or a third-party API wrapped with your own auth and guardrails so the agent does exactly what you allow and nothing else.
If your goal is only to give an agent access to GoHighLevel's own MCP, that already exists and the GHL MCP in n8n guide wires it up. Build your own when you need tools that don't exist yet, or when you want a curated, safe surface over something powerful.
Anatomy: tools, resources, transport
Three parts. Tools are functions the model can invoke — each has a name, a description the model reads to decide when to call it, and a typed input schema. Resources are readable context the model can pull in — a file, a record, a query result — addressed by URI. Transport is how client and server talk: stdio when the server runs as a local subprocess of the agent, or HTTP/SSE when it runs as a networked service. Local tooling for one developer is usually stdio; a shared server multiple agents hit is HTTP.
The single highest-leverage thing you write is the tool description. The model chooses tools by reading them, so a vague description means the tool gets called at the wrong time or ignored. Treat it as a prompt, not a docstring.
A minimal server
Using the TypeScript SDK, a working server is small. Define a tool with an input schema and a handler, then connect a transport:
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: 'acme-tools', version: '1.0.0' });
server.tool(
'get_contact',
'Fetch a single Acme CRM contact by its ID. Use when the user names or references a specific contact.',
{ contactId: z.string().describe('The Acme contact ID') },
async ({ contactId }) => {
const res = await fetch(`https://api.acme.com/v2/contacts/${contactId}`, {
headers: { Authorization: `Bearer ${process.env.ACME_TOKEN}` },
});
const data = await res.json();
return { content: [{ type: 'text', text: JSON.stringify(data) }] };
},
);
await server.connect(new StdioServerTransport());That's a complete MCP server exposing one tool. The agent now sees get_contact, reads its description to know when to use it, and calls it with a validated contactId.
Exposing a real API as tools
The discipline that separates a useful server from a dangerous one is curation. Don't mechanically map every API endpoint to a tool — that floods the model with choices and hands it destructive operations it doesn't need. Expose the few high-value actions the agent should actually perform, name and describe them in the agent's terms rather than the API's, and shape their inputs so the model can't express an invalid or harmful call. Wrapping GoHighLevel, that means a book_appointment tool that takes a slot and contact and internally handles the contact upsert and the mandatory Version header — not a raw passthrough that trusts the model to assemble the call correctly. Bake the awkward parts of the API into the tool so the model can't get them wrong.
Auth and secrets
The server holds the credentials; the model never sees them. Read tokens from the environment (or a secrets manager) at startup, exactly as above — the API key lives in the server process, and the agent only ever names a tool and its arguments. This is the core safety property of the pattern: you can give an agent real capability over a real system without ever exposing the credential to the model or its context. Scope the token to the least the tools need, and if the server is networked rather than stdio, put authentication in front of it — an HTTP MCP server with no auth is an open door to whatever it wraps, the same lesson the Edge Function guide draws about verifying inbound callers.
Running it under an agent
For Claude Code, register the server in the MCP config with its command and environment, and it becomes available in your sessions — the same harness setup that makes Claude Code useful against a real stack. An n8n workflow reaches an MCP server through the MCP Client Tool node feeding an AI Agent node, the wiring shown in the GHL MCP guide. Either way the server is the same; only the client changes.
Deploying: Dockerized, scheduled, logged
A stdio server that lives on your laptop is fine for development. For anything a team or a scheduled job relies on, containerize it: a small Docker image, the secrets injected as environment at run time, and — if it backs recurring automation — a scheduled Dockerized run with deduplication so a retried job doesn't double-act, and structured logging of every tool call so you can see what the agent actually did. That last part matters more than it sounds: an agent calling tools is taking real actions on your stack, and without a log of which tools ran with which arguments, debugging a bad run is guesswork. Dedup, logging, and scheduled containerized execution are the operational spine of running agent infrastructure in production, not an afterthought.
Gotchas
Over-broad tools. The more tools and the more powerful each one, the more often the model picks wrong. Fewer, sharper, safer tools beat a complete API mirror every time. Weak descriptions. If a tool fires at the wrong moment, the description is usually the bug, not the model. Unvalidated inputs. The schema is your guardrail — a tool that trusts free-form model output to be a valid ID or a safe query will eventually be handed neither. No observability. Log tool calls from day one; you cannot debug what you didn't record.
Need this workflow customized or debugged? Book a 30-min scope call →
Stuck configuring this workflow?
I build and fix n8n workflows for agencies and SaaS teams. 30-minute scope call — bring the problem, leave with an estimate.