Creating Custom MCP Servers
Build your own MCP server to connect TeamDay agents to any API, database, or internal tool. This guide covers the basics using the official MCP SDK.
Overview
An MCP server is a program that:
- Communicates via stdin/stdout (stdio transport) or HTTP (SSE transport)
- Exposes tools that agents can call
- Optionally provides resources (data) and prompts (templates)
You write the server, register it with TeamDay, and agents can use it.
Quick Start with TypeScript
1. Set Up the Project
mkdir my-mcp-server
cd my-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk
npm install -D typescript @types/node
2. Create the Server
// src/index.ts
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: "my-custom-tool",
version: "1.0.0",
});
// Define a tool
server.tool(
"lookup_customer",
"Look up customer information by email or ID",
{
query: z.string().describe("Customer email or ID"),
},
async ({ query }) => {
// Your business logic here
const customer = await fetchCustomer(query);
return {
content: [
{
type: "text",
text: JSON.stringify(customer, null, 2),
},
],
};
}
);
// Define another tool
server.tool(
"create_ticket",
"Create a support ticket",
{
title: z.string().describe("Ticket title"),
description: z.string().describe("Detailed description"),
priority: z.enum(["low", "medium", "high"]).default("medium"),
},
async ({ title, description, priority }) => {
const ticket = await createSupportTicket(title, description, priority);
return {
content: [
{
type: "text",
text: `Created ticket #${ticket.id}: ${ticket.url}`,
},
],
};
}
);
// Start the server
const transport = new StdioServerTransport();
await server.connect(transport);
3. Build and Test
npx tsc
node dist/index.js
Test with the MCP Inspector:
npx @modelcontextprotocol/inspector node dist/index.js
4. Register with TeamDay
Add to .mcp.json in your Space:
{
"mcpServers": {
"my-custom-tool": {
"command": "node",
"args": ["/path/to/my-mcp-server/dist/index.js"],
"env": {
"DATABASE_URL": "${DATABASE_URL}"
}
}
}
}
Or create via CLI:
teamday mcps create \
--type stdio \
--name "My Custom Tool" \
--description "Customer lookup and ticket creation"
Python Quick Start
# server.py
from mcp.server import Server
from mcp.server.stdio import stdio_server
import mcp.types as types
server = Server("my-python-tool")
@server.list_tools()
async def list_tools() -> list[types.Tool]:
return [
types.Tool(
name="query_database",
description="Run a SQL query against the analytics database",
inputSchema={
"type": "object",
"properties": {
"sql": {
"type": "string",
"description": "SQL query to execute"
}
},
"required": ["sql"]
}
)
]
@server.call_tool()
async def call_tool(name: str, arguments: dict) -> list[types.TextContent]:
if name == "query_database":
result = await run_query(arguments["sql"])
return [types.TextContent(type="text", text=str(result))]
raise ValueError(f"Unknown tool: {name}")
async def main():
async with stdio_server() as (read, write):
await server.run(read, write, server.create_initialization_options())
import asyncio
asyncio.run(main())
Install dependencies:
pip install mcp
Register:
{
"mcpServers": {
"my-python-tool": {
"command": "python",
"args": ["server.py"]
}
}
}
Tool Design Guidelines
Keep Tools Focused
Each tool should do one thing well:
// Good: focused, clear purpose
server.tool("get_order_status", "Check the status of an order by ID", ...);
server.tool("list_recent_orders", "List orders from the last N days", ...);
// Bad: too broad
server.tool("manage_orders", "Do anything with orders", ...);
Use Descriptive Parameters
The describe() method helps the agent understand what to pass:
{
email: z.string().email().describe("Customer email address"),
limit: z.number().default(10).describe("Max results to return"),
status: z.enum(["active", "inactive"]).describe("Filter by account status"),
}
Return Structured Data
Return JSON for data that the agent will process:
return {
content: [{
type: "text",
text: JSON.stringify({
customers: results,
totalCount: count,
page: page,
}, null, 2)
}]
};
Handle Errors Gracefully
server.tool("lookup", "...", schema, async (args) => {
try {
const result = await fetchData(args.query);
return { content: [{ type: "text", text: JSON.stringify(result) }] };
} catch (error) {
return {
content: [{
type: "text",
text: `Error: ${error.message}. Please verify the input and try again.`
}],
isError: true,
};
}
});
Resources
MCP servers can also expose data resources that agents can read:
server.resource(
"config",
"config://app",
async (uri) => ({
contents: [{
uri: uri.href,
text: JSON.stringify(appConfig),
mimeType: "application/json",
}]
})
);
Deployment Options
Local (stdio)
Run as a local process. Best for development and single-machine setups.
{
"command": "node",
"args": ["dist/index.js"]
}
Docker
Package as a container for consistent environments:
FROM node:20-slim
WORKDIR /app
COPY . .
RUN npm install && npx tsc
CMD ["node", "dist/index.js"]
{
"command": "docker",
"args": ["run", "-i", "--rm", "my-mcp-server"]
}
npx (Published Package)
Publish to npm for easy distribution:
{
"command": "npx",
"args": ["-y", "@your-org/my-mcp-server"]
}
Testing
MCP Inspector
The official MCP Inspector lets you test your server interactively:
npx @modelcontextprotocol/inspector node dist/index.js
Manual Testing
Send JSON-RPC messages via stdin:
echo '{"jsonrpc":"2.0","method":"tools/list","id":1}' | node dist/index.js
In TeamDay
Add the MCP to a Space and chat with an agent. Ask it to use your custom tools.
Further Reading
- MCP Specification — Full protocol specification
- MCP GitHub — SDKs and examples
- Installing MCP Servers — How to register MCPs with TeamDay
- Skills — Alternative for local workflows (no server needed)