MCP Server Development Guide: How to Build an MCP Server
By Atiq Md Rezaul Hoque Turjo · Published · Updated
MCP server development means building a small service that exposes tools, resources and prompts to AI applications through the Model Context Protocol. You choose one clear job, define a few well-described tools, pick a transport such as stdio, test with the MCP Inspector, then add authentication, logging and approval steps before connecting real systems.
Contents
- What MCP is and why it exists
- Architecture: host, client and server
- The three primitives: tools, resources and prompts
- Transports: stdio and streamable HTTP
- Plan before you write code
- Build an MCP server step by step
- Testing and debugging
- How to add an MCP server to a client
- Security and permissions
- Deployment and monitoring
- Common mistakes
- Build or use an existing server
- How NextCTL builds MCP servers
- Launch checklist
What MCP is and why it exists
The Model Context Protocol, or MCP, is an open standard that describes how AI applications connect to outside tools and data. Before a standard like this, every link between an AI application and a business system needed its own custom code. If you had three AI applications and four systems, you could end up writing twelve separate integrations, each with its own quirks.
MCP replaces that pile of one-off work with one agreed way to describe and call capabilities. A system describes what it offers once. Any compatible AI application can then discover those capabilities and use them. People often compare it to a universal connector: you build the plug once, and many devices can use it.
MCP server development is the work of building that plug for your own system. You decide what the AI may see and do, write the code that carries out each action, and set the rules that keep it safe. The protocol handles the conversation between the two sides, so you can focus on the part that is unique to your business.
The standard keeps changing, so treat this guide as a map and not as a reference manual. The official specification and SDK documentation are the final word on exact names, versions and behavior. Check them before you write production code.
Architecture: host, client and server
Three roles appear in every MCP setup. Knowing which is which will save you confusion later.
| Role | What it is | Example |
|---|---|---|
| Host | The AI application the person uses | A chat assistant, a coding tool or an agent you built |
| MCP client | A component inside the host that keeps one connection to one server | The connector code the host creates for each server it uses |
| MCP server | The program that exposes tools, resources and prompts | The service you are about to build |
A host can run many clients at once, one per server. That is how a single assistant can read a calendar, search a document store and create a ticket in the same conversation. Each server stays small and focused, and the host combines them.
JSON-RPC underneath
MCP messages use JSON-RPC, a simple format for sending a request and getting a response. A client sends a request such as “list your tools” or “call this tool with these arguments”. The server replies with a result or an error. You rarely write these messages yourself, because the SDKs handle them. It still helps to know they exist when you read logs or debug a connection.
The connection lifecycle
- The client connects and the two sides agree on a protocol version and what each supports.
- The client asks the server what it offers: its tools, resources and prompts.
- The host passes those descriptions to the AI model.
- When the model decides to act, the client sends a request to the server.
- The server runs your code and returns the result, which goes back to the model.
Notice where the logic lives. The AI model chooses what to ask for, but your server decides what actually happens. That split is the reason the server is the right place for permissions and safety rules.
The three primitives: tools, resources and prompts
A server can offer three kinds of things. Most first projects use only tools, but it helps to know all three.
Tools
MCP tools are actions the model can ask the server to run. Examples are “look up an order”, “create a support ticket” or “search the knowledge base”. Each tool has a name, a description and a schema that defines its inputs. When the model decides to use one, that request is called a tool call. Tool calls are the part of MCP that does real work, so they are also where most of the risk sits.
Resources
Resources are pieces of information the client can read, such as a file, a database record or a report. They are meant for context. A resource is identified by a URI, and the application usually decides when to load it. Use a resource when the main job is to give the model something to read and no action is needed.
Prompts
Prompts are reusable instruction templates that a person can pick, often from a menu. A prompt might set up a standard code review request or a weekly report. They help people get consistent results without retyping long instructions.
| Primitive | Who usually triggers it | Best for | Risk level |
|---|---|---|---|
| Tools | The model, during a task | Actions and lookups | Higher, because tools can change things |
| Resources | The application or the user | Reading context | Lower, but data can still be sensitive |
| Prompts | The user | Repeatable workflows | Low, but prompts still need review |
Transports: stdio and streamable HTTP
The transports carry JSON-RPC messages between the client and the server. The protocol defines more than one, and your choice affects how you deploy and secure the server.
Standard input output (stdio)
With the stdio transport, the client launches your server as a child process on the same machine. The two talk through standard input and output. This is the simplest option. There is no network port to open and no web server to run. It suits local tools, personal setups and development.
One rule matters a lot with stdio: your server must not print ordinary log text to standard output, because that channel carries the protocol messages. Send logs to standard error or to a file instead. Stray output on the wrong stream is one of the most common reasons a new server fails to connect.
Streamable HTTP
Streamable HTTP runs your server as a web service. Clients connect over the network, so one deployment can serve many people and many applications. This is the usual choice for shared, hosted or company-wide servers. It also means you must handle authentication, TLS, rate limits and uptime as you would for any web service.
Earlier versions of the specification described a different HTTP style based on server-sent events. Some older material still mentions it. Check the current specification for the transport details your SDK supports.
| Area | stdio | Streamable HTTP |
|---|---|---|
| Where it runs | On the user’s machine, started by the client | On a server you host |
| Who can use it | One local client | Many remote clients |
| Setup effort | Low | Higher, like any web service |
| Authentication | Inherits the local user’s environment | You must add it explicitly |
| Best for | Development, personal tools, local files | Shared team tools and hosted products |
A common path is to build and test over stdio, then add streamable HTTP when the server needs to be shared. The tool code stays the same. Only the way it is exposed changes.
Plan before you write code
Good MCP server development starts on paper. The code for a small server is short. The hard part is deciding what to expose.
Pick one job
Start with a single, concrete task, such as “let the support assistant check order status”. Avoid wrapping a whole system at once. A server that does one thing well is easier to secure, test and explain.
Design fewer, clearer tools
The model chooses tools by reading their names and descriptions. Vague or overlapping tools lead to wrong choices. Give each tool a plain verb-and-noun name, a description that says when to use it and when not to, and inputs with clear types. Return only the fields the task needs.
It also helps to think in terms of the user’s goal, not your internal API. If your system needs three calls to answer one common question, consider a single tool that does all three behind the scenes.
Decide read-only or write
Reading is lower risk than changing data. Start with read-only tools where you can. When you add actions that change things, such as sending a message or issuing a refund, plan the approval step at the same time.
Build an MCP server step by step
This section walks through a small server using the official TypeScript SDK, published as @modelcontextprotocol/sdk. The code is illustrative. SDK method names and import paths have changed between releases, so check the current SDK documentation for the exact API before you copy anything into a real project.
Step 1: Set up the project
Create a Node project, install the SDK and a schema library, and turn on TypeScript. The SDK uses a schema library to describe tool inputs.
# illustrative setup commands
mkdir order-status-server && cd order-status-server
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/nodeStep 2: Create the server object
The server object holds a name, a version and the list of things you register on it. Pick a name that tells a human what the server does, because it may appear in client menus and logs.
// illustrative sketch: check current SDK docs for exact names
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: "order-status",
version: "0.1.0",
});Step 3: Register a tool
A tool needs a name, a description, an input schema and a handler function. The handler receives validated input and returns content for the model to read. This example is read-only and returns only a small set of fields.
// illustrative: a read-only tool with validated input
server.tool(
"get_order_status",
"Look up the delivery status of one order by its reference. Read-only.",
{ orderRef: z.string().min(3).max(40) },
async ({ orderRef }) => {
const order = await findOrder(orderRef); // your own data access
if (!order) {
return {
content: [{ type: "text", text: "No order found for that reference." }],
isError: true,
};
}
return {
content: [
{ type: "text", text: "Order " + order.ref + " status: " + order.status },
],
};
}
);Three habits in this sketch are worth copying. The description says the tool is read-only. The input is validated before your code runs. The error message is clear enough for the model to explain the problem to the user. Here findOrder stands in for your own data access code.
Step 4: Add a resource or prompt if you need one
Not every server needs resources or prompts. Add them when they fit. A resource might expose a shipping policy document. A prompt might offer a ready-made “summarize this customer’s open orders” request. The SDK has matching registration methods for each, and the current documentation shows their exact signatures.
Step 5: Connect a transport and start
The last step connects the server to a transport. For local development, use stdio.
// illustrative: start the server over stdio
const transport = new StdioServerTransport();
await server.connect(transport);
// Log to stderr only. stdout carries protocol messages.
console.error("order-status server running on stdio");Compile the project and you have a working local server. To serve many clients later, you would swap in the HTTP transport from the SDK and add authentication in front of it. Your tool code does not need to change.
What about other languages?
Official SDKs exist for several languages, and the concepts are the same in each. You register tools with names, descriptions and schemas, then connect a transport. Choose the language your team already runs in production. A server you can maintain is worth more than one written in the newest tool.
Testing and debugging
Test in layers. Each layer catches a different kind of problem.
Start with the MCP Inspector
The MCP Inspector is an official developer tool for exploring and testing a server without an AI model in the loop. You point it at your server, and it shows the tools, resources and prompts you registered. You can fill in arguments, call a tool by hand and read the raw response. The usual way to start it is with npx:
# illustrative: launch the Inspector against a local stdio server
npx @modelcontextprotocol/inspector node build/index.jsCheck the Inspector’s own documentation for the current command, since options change. If your tools do not appear in the list, the problem is almost always in registration or startup, not in the model.
Unit-test the tool logic
Keep the real work in plain functions that you can test without any protocol code. Test normal results, empty results and errors. Then make the handler a thin layer that validates input and calls those functions.
Try bad input on purpose
Send missing fields, wrong types, very long strings and odd characters. Model-generated arguments can be wrong, and a malicious input can be worse. Your server should refuse bad input with a clear message and never crash or leak internal details.
Run real tasks with a real client
Connect an MCP client and give it realistic jobs. Watch which tools it picks, what arguments it sends and how it uses the results. This is where weak tool descriptions show up. If the model keeps choosing the wrong tool, rewrite the description before you change any code.
Debugging checklist
- Server will not connect. Check that nothing writes to standard output except protocol messages, and that the start command works in a plain terminal.
- Tools do not show up. Confirm they are registered before you connect the transport, and that the client was restarted after a config change.
- Calls fail with validation errors. Compare the arguments the client sent with your schema. Logging the raw request helps.
- Works in the Inspector but not in the client. Compare the working directory, environment variables and paths the client uses to start the process.
- Random timeouts. Look for slow downstream calls. Add timeouts in your own code so the failure is clear.
How to add an MCP server to a client
Once the server works, you need to add MCP to the client you want to use. Each client has its own way, but the pattern is similar. Most let you edit a configuration file or use a settings screen to list servers.
For a stdio server, the entry usually names the command that starts it, any arguments and any environment variables. For a remote server, the entry gives the URL and any credentials or sign-in settings. The sketch below shows the general shape only. Field names differ between clients, so copy the format from your client’s documentation.
// illustrative shape only: real field names vary by client
{
"mcpServers": {
"order-status": {
"command": "node",
"args": ["/absolute/path/to/build/index.js"],
"env": { "ORDERS_API_URL": "https://example.invalid/orders" }
}
}
}A few tips make this step smoother. Use absolute paths, because the client may start your process from a different folder. Pass secrets through environment variables and keep them out of shared config files. Restart the client after changes. Then ask it to list its available tools to confirm the server loaded.
Many clients also ask the user to approve a tool call before it runs. Leave that on while you test. It lets you see exactly what the model is trying to do.
Security and permissions
An MCP server connects an AI model to real systems, so treat it as a security boundary. The model can be wrong, and the content it reads can be hostile. Your server is the last place where you can say no.
Least privilege
Give each tool the narrowest access it needs. A tool that reads order status should not hold credentials that can delete orders. Use a dedicated service account for the server, with only the permissions the exposed tools require. If one tool is compromised or misused, the damage stays small.
Authentication and authorization
A stdio server runs as the local user, so it inherits that user’s access. A remote server over HTTP must prove who is calling. The specification includes an authorization approach based on OAuth for HTTP servers, so check the current text and use your SDK’s support for it. Whatever method you choose, verify the caller on every request, and check what that caller is allowed to do. Never rely on the model to decide who is allowed.
Validate input and limit output
Check types, ranges and allowed values for every argument. Use allow lists where you can. Never build database queries or shell commands by pasting raw model text into them. On the way out, return only what the task needs, and keep sensitive fields such as payment details out of responses.
Guard against prompt injection
Content a model reads, such as a web page, an email or a document, can contain instructions meant to steer it. If your server returns text from untrusted sources, the model may follow instructions hidden in it. Do not let such content approve actions on its own. Keep a clear line between data the model reads and commands that carry authority.
Human approval for risky tool calls
Some tool calls should never run without a person saying yes. Examples are sending money, deleting records, messaging customers and changing permissions. Build the approval step into the workflow. The client may offer its own prompts, but you should not depend on that alone. For high-impact actions, enforce approval inside the server, for example by returning a pending request that a person confirms elsewhere.
Logging and audit
Record which tool was called, with what arguments, by which client or user, and what happened. Follow your privacy rules, and avoid writing secrets or personal data into logs. Good logs are how you debug, how you answer questions after an incident, and how you earn trust from the people who rely on the server.
Other controls worth having
- Rate limits so a looping agent cannot flood a downstream system.
- Timeouts on every outside call.
- Separate environments so testing never touches production data.
- A stop switch that lets you disable a tool quickly.
- Dependency updates on a regular schedule, like any other service.
We make no claim that following these steps makes a system safe or compliant. Rules differ by industry and country, so involve your security and legal advisers early, especially for regulated data.
Deployment and monitoring
Choose where it runs
A stdio server can be shipped as a package or script that users install and the client launches. A streamable HTTP server is deployed like any web service: a container or a managed platform, behind TLS, with secrets in a proper store. Pick the model that matches who will use the server.
Version your tools
Clients and models come to depend on tool names and input shapes. Changing them silently can break agents that worked yesterday. Add new tools or optional inputs where you can. When you must make a breaking change, announce it, give the change a new version and keep the old one working for a while.
Monitor what matters
- Availability. Is the server up and responding?
- Errors. Which tools fail, and why?
- Latency. Slow tools make agents slow and can cause timeouts.
- Usage. Which tools are called, how often and by whom?
- Unusual patterns. Sudden spikes or repeated failures can signal a bug or abuse.
Assign an owner
A server connected to live systems needs someone responsible for it. That person watches the logs, applies updates, and reviews changes when the connected system or the MCP specification moves on. Without an owner, servers quietly decay.
Common mistakes
- Wrapping an entire API. Dozens of tools confuse the model. Expose the few that serve a real job.
- Vague descriptions. The model picks tools from the text you write. Say what each tool does, what it needs and when not to use it.
- Logging to standard output. On stdio this breaks the protocol stream.
- Trusting the model’s arguments. Always validate, even when the schema looks strict.
- Over-broad credentials. One powerful key shared by every tool turns a small bug into a big incident.
- No approval for risky actions. Do not leave money, deletion or external messages to chance.
- Returning too much data. Large or sensitive responses waste context and raise risk.
- Testing only the happy path. Agents will send odd requests. Test failures as carefully as successes.
- Hard-coded secrets. Use environment variables or a secret manager.
- Skipping the spec. The protocol changes. Re-read the current documentation before each major release.
Build or use an existing server
You do not always have to build an MCP server. Many public servers exist for popular tools. The right choice depends on what you need.
| Question | Lean toward an existing server | Lean toward building |
|---|---|---|
| Is the system common? | Yes, and a maintained server exists | No, it is internal or custom |
| Do you need custom rules? | No, defaults are fine | Yes, such as special approvals or filters |
| How sensitive is the data? | Low, and you have reviewed the code | High, and you need full control |
| Who maintains it? | A trusted outside team | Your team or a partner you choose |
If you use someone else’s server, review its code and permissions as you would any software that touches your data. A server you did not write still runs with the access you give it. Prefer sources you can inspect, and pin versions so updates do not surprise you.
Also ask whether you need MCP at all. A single fixed integration for one application may be simpler as a direct API connection. MCP pays off when several AI applications or agents need the same access, or when you want one governed gateway that can grow.
How NextCTL builds MCP servers
NextCTL builds MCP servers as part of wider AI work. Our approach follows the same order as this guide. We start with one job and a short list of tools. We design permissions and approval steps before we write handlers. We test with the Inspector and with real client tasks, then add logging and monitoring before a server goes near live data.
You can read about the service on our MCP page. MCP servers often sit inside larger projects, such as AI integration services that connect assistants to your CRM, ERP or internal tools. They are also a building block for agentic AI systems and for AI agent development, where agents need controlled access to real systems.
We cannot promise specific business results, because they depend on your systems, your data and how the server is used. What we can do is scope the work clearly, build with sensible defaults for security and tell you plainly what the limits are.
Launch checklist
- One clear job and a short list of tools, each with a plain name and description.
- Read-only tools first, with approval planned for any action that changes data.
- Input validated on every tool and output limited to what the task needs.
- A transport chosen on purpose: stdio for local use, streamable HTTP for shared use.
- Least-privilege credentials stored outside the code.
- Authentication on any remote server, checked on every request.
- Logs that record tool calls without leaking secrets, written to the right stream.
- Tests: unit tests, the MCP Inspector, bad inputs and real client tasks.
- A version plan, monitoring and a named owner.
- A review of the current specification and SDK documentation before release.
Where to go next
Start small, keep the server narrow and let real use guide what you add. If you want help connecting AI to your own systems, see our MCP service and AI integration services, or learn how agents use these servers in agentic AI and AI agent development. More guides are on the NextCTL blog.
Frequently Asked Questions
What is an MCP server?
What language should I use to build an MCP server?
What is the difference between stdio and streamable HTTP?
How do I test an MCP server?
Is an MCP server secure by default?
How do I add an MCP server to a client?
Should I build an MCP server or use an existing one?
See where your operations
are leaking revenue.
Tell us what you want to fix. We show where missed calls and slow follow up cost you most, with a written plan for what to fix first.
Or run a free tool first:Website AuditAI Readiness Scan