# Creating Custom MCP Servers

Build your own MCP servers to give TeamDay agents access to custom APIs, databases, and internal tools.

# 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

```bash
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

```typescript
// 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

```bash
npx tsc
node dist/index.js
```

Test with the MCP Inspector:

```bash
npx @modelcontextprotocol/inspector node dist/index.js
```

### 4. Register with TeamDay

Add to `.mcp.json` in your Space:

```json
{
  "mcpServers": {
    "my-custom-tool": {
      "command": "node",
      "args": ["/path/to/my-mcp-server/dist/index.js"],
      "env": {
        "DATABASE_URL": "${DATABASE_URL}"
      }
    }
  }
}
```

Or create via CLI:

```bash
teamday mcps create \
  --type stdio \
  --name "My Custom Tool" \
  --description "Customer lookup and ticket creation"
```

---

## Python Quick Start

```python
# 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:

```bash
pip install mcp
```

Register:

```json
{
  "mcpServers": {
    "my-python-tool": {
      "command": "python",
      "args": ["server.py"]
    }
  }
}
```

---

## Tool Design Guidelines

### Keep Tools Focused

Each tool should do one thing well:

```typescript
// 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:

```typescript
{
  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:

```typescript
return {
  content: [{
    type: "text",
    text: JSON.stringify({
      customers: results,
      totalCount: count,
      page: page,
    }, null, 2)
  }]
};
```

### Handle Errors Gracefully

```typescript
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:

```typescript
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.

```json
{
  "command": "node",
  "args": ["dist/index.js"]
}
```

### Docker

Package as a container for consistent environments:

```dockerfile
FROM node:20-slim
WORKDIR /app
COPY . .
RUN npm install && npx tsc
CMD ["node", "dist/index.js"]
```

```json
{
  "command": "docker",
  "args": ["run", "-i", "--rm", "my-mcp-server"]
}
```

### npx (Published Package)

Publish to npm for easy distribution:

```json
{
  "command": "npx",
  "args": ["-y", "@your-org/my-mcp-server"]
}
```

---

## Testing

### MCP Inspector

The official MCP Inspector lets you test your server interactively:

```bash
npx @modelcontextprotocol/inspector node dist/index.js
```

### Manual Testing

Send JSON-RPC messages via stdin:

```bash
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](https://modelcontextprotocol.io/specification/2025-11-25) — Full protocol specification
- [MCP GitHub](https://github.com/modelcontextprotocol) — SDKs and examples
- [Installing MCP Servers](https://docs.teamday.ai/guides/mcp-servers/installing) — How to register MCPs with TeamDay
- [Skills](https://docs.teamday.ai/guides/skills) — Alternative for local workflows (no server needed)
