What we are building
In this guide we build a small but complete inventory MCP server: an agent will be able to look up stock levels (a tool) and read a live catalog (a resource). It is deliberately minimal so the shape of an MCP server is obvious. The same pattern scales to anything — a payments system, an internal database, a logistics API.
Before you write a line of code, it is worth asking whether you need to. If your system is GitHub, Slack, Stripe, Search Console, an ad platform, or one of the common REST products, BusinessMCP already exposes it as an MCP tool — connect it in a click and skip the build. Write your own server only for the genuinely proprietary parts of your stack, then connect it to your workspace so it lives beside everything else.
Project setup
You need Node.js 18+ and the official SDK.
mkdir inventory-mcp && cd inventory-mcp
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node
npx tsc --initA clean structure keeps tools and resources separate as the server grows:
inventory-mcp/
├── src/
│ ├── server.ts # wiring
│ ├── tools.ts # callable functions
│ └── resources.ts # readable data
└── package.jsonThe server skeleton
The SDK gives you a server object and a transport. Register capabilities, connect a transport, and you have a working MCP server.
// src/server.ts
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
import { registerTools } from './tools'
import { registerResources } from './resources'
const server = new McpServer({
name: 'inventory',
version: '1.0.0',
})
registerTools(server)
registerResources(server)
const transport = new StdioServerTransport()
await server.connect(transport)stdio is ideal for local desktop clients. For a hosted server reachable over the network, swap in the streamable-HTTP transport — the capability code below does not change.
Adding a tool
A tool is a name, a description the model reads, an input schema, and a handler. Descriptions matter: they are the model's only clue about when to use the tool, so write them like you are briefing a new teammate.
// src/tools.ts
import { z } from 'zod'
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import { db } from './db'
export function registerTools(server: McpServer) {
server.tool(
'get_stock',
'Look up the current stock level for a product by SKU',
{ sku: z.string().describe('The product SKU, e.g. "TS-BLK-M"') },
async ({ sku }) => {
const row = await db.stock(sku)
if (!row) return { content: [{ type: 'text', text: `No product found for SKU ${sku}` }] }
return {
content: [{ type: 'text', text: JSON.stringify({ sku, available: row.available }) }],
}
},
)
}Return structured, predictable content. The model reasons far better over clean JSON than over prose, and downstream automations can parse it.
Adding a resource
Where a tool *does* something, a resource *is* something the agent can read. Expose data that the model benefits from having in context.
// src/resources.ts
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import { db } from './db'
export function registerResources(server: McpServer) {
server.resource('catalog', 'inventory://catalog', async (uri) => {
const items = await db.catalog()
return {
contents: [
{ uri: uri.href, mimeType: 'application/json', text: JSON.stringify(items) },
],
}
})
}Authentication & scoping
Never ship an MCP server without auth. At minimum, require a bearer token on every request and reject anything unrecognized. Beyond that, the two rules that keep you safe are scope and audit: each token should map to an allowlist of tools it may call, and every invocation should be logged with the caller, the arguments, and the result.
Validate inputs with a schema (Zod above) on the server, not just in the declared schema — the model can and will send unexpected values. Treat every tool call as untrusted input to your backend.
Testing & deployment
Test tools like ordinary functions with an in-memory data layer, then run an integration pass against the real transport to confirm discovery and invocation work end to end. For deployment you have two options: run it yourself behind HTTPS with the streamable-HTTP transport, or connect it to BusinessMCP so it is authenticated, rate-limited, logged, and callable alongside your other tools through a single mcph_* key.
When you are ready to expose it to agents, see expose your MCP endpoint and MCP best practices.
Frequently asked questions
What language should I write an MCP server in?
Any language with a JSON-RPC capable runtime works. The official SDKs are TypeScript and Python, which is what most people reach for. The examples here use the TypeScript SDK.
Do I have to build a server to use BusinessMCP?
No. BusinessMCP already exposes your connected tools and unified data as a hosted MCP server at /api/mcp. Build your own only when you have a proprietary system that no connector covers — then connect it alongside everything else.
How do I secure a custom MCP server?
Require a bearer token on every request, validate all tool inputs, scope each token to an allowlist of tools, rate-limit per caller, and log every invocation. See our MCP best practices guide for the full checklist.
Keep going
Turn your company into one AI-ready data platform on a single hosted MCP endpoint.