- Home
- Blog
- How-to guides
- How to build an MCP server for your internal tools
How to build an MCP server for your internal tools
An MCP server lets Claude and other AI clients use your CRM, database or ticket queue through tools you define. Here is how to design one for the current 2026-07-28 spec, starting read-only.
To build an MCP server, you wrap one internal system, such as a CRM, a database or a ticket queue, in a small program that speaks the Model Context Protocol: it lists tools with JSON Schema inputs, runs them when an AI client calls, and returns the results. Start with one read-only tool over stdio, add OAuth when you move it to Streamable HTTP, and log every call.

What is an MCP server?
The Model Context Protocol is an open protocol that connects AI applications to tools and data. A host (Claude, an IDE, your own app) runs a client for each server it connects to, and they exchange JSON-RPC 2.0 messages.
The current revision is 2026-07-28, published on 28 July 2026, and it changed a lot. The protocol is now stateless: there is no initialize handshake, and every request carries its protocol version and client capabilities in _meta. Servers must answer server/discover with the versions they support. The TypeScript, Python, Go and C# SDKs support this revision; Rust support is in beta. Older guides that open with a handshake describe 2025-11-25 and earlier.
If you know function calling, a tool is the same idea: a name, a description and an input schema. MCP makes it portable, so one server works with any compliant client.
Tools, resources or prompts?
A server can offer three things, and the spec says who is in control of each:
| Primitive | Controlled by | Use it for | CRM example |
|---|---|---|---|
| Tools | The model | Lookups and actions | find_contact, get_deal |
| Resources | The application | Context the client attaches | The list of pipeline stages |
| Prompts | The user | Templates picked from a menu | "Brief me on this account" |
Most internal servers are mostly tools. The 2026-07-28 revision also deprecates Roots, Sampling and Logging, so new servers should not build on them.
stdio or Streamable HTTP?
stdio is for a server on the user's own machine. The client launches it as a subprocess and talks over stdin and stdout. Nothing but MCP messages may go to stdout, so write logs to stderr. Credentials come from the environment, not from OAuth.
Streamable HTTP is for a shared server your whole team uses. It exposes one endpoint, each message is an HTTP POST, and the server answers with JSON or a short SSE stream for that request. The current spec has no sessions; if a tool needs state across calls, return a handle (an opaque ID) and accept it as an argument. Three rules from the spec: validate the Origin header on every request, bind to 127.0.0.1 when running locally, and authenticate every connection. The older HTTP+SSE transport is deprecated.
How should authentication work?
Authorization is optional in the spec, but for company data treat it as required. On HTTP, your server is an OAuth 2.1 resource server. It must publish Protected Resource Metadata (RFC 9728) so clients can find the authorization server, check that each token was issued for this server, and refuse any other token. Never pass the user's token through to the CRM; call the CRM with credentials the server controls, scoped to that user.
Two useful details: tools/list may vary with the caller's scopes, so a sales rep can see fewer tools than a manager, and an under-scoped call should get a 403 naming the scope it needs.
Example design: a CRM lookup server
find_contact: inputquery(string, up to 100 characters) andlimit(integer, 1 to 10). Returnsid,name,company,ownerandlast_activityasstructuredContentthat matches anoutputSchema. Annotations:readOnlyHint: true,openWorldHint: false.get_deal: input a dealid; returns stage, value, next step and owner, filtered by what this user may see in the CRM.- Resource
crm://pipelines: stage names, so the model uses your real vocabulary. - Prompt
account-brief: a template the user picks, which calls both tools. - Errors come back as
isError: truewith a message the model can act on: "No contact matches 'Acme Ltd'; try the company's short name." - Limits: validate every input, rate limit per user and set a timeout on each CRM call. The spec says servers must validate inputs, apply access controls, rate limit and sanitise outputs.
Leave phone numbers and email addresses out of the first version unless a scope asks for them. Less data in the model's context means less to leak.

Read-only first, then writes behind approval
Ship the read-only tools, watch how people use them for a few weeks, then add writes. The spec says there should always be a human able to deny a tool call, and that clients must treat annotations as untrusted unless the server is trusted. So do not rely on readOnlyHint for safety; the server itself must refuse what the user may not do.
For writes, prefer tools that create a draft or a proposal a person confirms. In Fint, our accounting product, an AI agent proposes records and a person confirms them before any money is recorded. Remember too that CRM notes contain text written by outsiders, so tool output is data for the model, never instructions.
Log every call: user, tool, arguments, result size, duration and outcome. The 2026-07-28 spec carries OpenTelemetry trace context in _meta, so MCP calls can join your existing traces. Do not log tokens.
We use the same patterns in our lab: agent-talk, a mailbox that lets AI agents in different projects message each other, gives agents its mailbox as MCP tools.
Getting it built
Our MCP server development work follows this path: one system, read-only tools, OAuth on HTTP, audit logs, then approved writes. We build MCP servers for our own products, Fint, Desk and Cybe Social.
If you have an internal system your team wants to reach from Claude or another AI client, tell us what it is and who should see what, and we will sketch the first tools with you. Book a consultation.