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:

  1. Communicates via stdin/stdout (stdio transport) or HTTP (SSE transport)
  2. Exposes tools that agents can call
  3. 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