Adding MCP Tools
This guide explains how to add new MCP tools to an existing provider. Tools are the primary way AI assistants interact with health data.
Tool Structure
Every MCP tool has four components:
interface ToolDefinition {
name: string; // Unique identifier (kebab-case)
description: string; // What the tool does (for AI to understand)
inputSchema: z.ZodSchema; // Zod schema for parameters
handler: ToolHandler; // Function that executes the tool
}
Step 1: Define the Schema
Use Zod to define the input parameters:
// src/tools/schemas.ts
import { z } from 'zod';
export const GetSleepTrendSchema = z.object({
days: z.number()
.min(7)
.max(90)
.default(30)
.describe('Number of days to analyze'),
includeNaps: z.boolean()
.default(false)
.describe('Whether to include naps in the analysis'),
});
export type GetSleepTrendInput = z.infer<typeof GetSleepTrendSchema>;
Schema Best Practices
- Always add
.describe()to help AI understand parameters - Use sensible defaults with
.default() - Add validation constraints (
.min(),.max(), etc.) - Keep schemas simple - fewer parameters is better
Step 2: Implement the Handler
The handler receives validated input and returns the result:
// src/tools/sleep-trend.ts
import { WhoopClient } from '../client';
import { GetSleepTrendInput } from './schemas';
export async function getSleepTrend(
input: GetSleepTrendInput,
client: WhoopClient
): Promise<SleepTrendResult> {
const endDate = new Date();
const startDate = new Date(endDate.getTime() - input.days * 24 * 60 * 60 * 1000);
const sleepData = await client.getSleep(
startDate.toISOString().split('T')[0],
endDate.toISOString().split('T')[0]
);
// Filter naps if needed
const sessions = input.includeNaps
? sleepData
: sleepData.filter(s => !s.isNap);
// Calculate trend
const scores = sessions
.map(s => s.score)
.filter((s): s is number => s !== undefined);
const avgScore = scores.reduce((a, b) => a + b, 0) / scores.length;
const trend = calculateTrend(scores);
return {
period: {
start: startDate.toISOString().split('T')[0],
end: endDate.toISOString().split('T')[0],
days: input.days,
},
sessions: sessions.length,
averageScore: Math.round(avgScore * 10) / 10,
trend: trend > 0.5 ? 'improving' : trend < -0.5 ? 'declining' : 'stable',
trendValue: Math.round(trend * 100) / 100,
};
}
function calculateTrend(values: number[]): number {
if (values.length < 2) return 0;
// Simple linear regression slope
const n = values.length;
const sumX = (n * (n - 1)) / 2;
const sumY = values.reduce((a, b) => a + b, 0);
const sumXY = values.reduce((sum, y, x) => sum + x * y, 0);
const sumX2 = (n * (n - 1) * (2 * n - 1)) / 6;
return (n * sumXY - sumX * sumY) / (n * sumX2 - sumX * sumX);
}
Handler Best Practices
- Return structured data, not formatted text
- Handle edge cases (empty data, missing values)
- Keep calculations focused on the tool's purpose
- Use helper functions for complex logic
Step 3: Register the Tool
Add the tool to the registry:
// src/tools/index.ts
import { z } from 'zod';
import { GetSleepTrendSchema } from './schemas';
import { getSleepTrend } from './sleep-trend';
import { WhoopClient } from '../client';
export interface Tool {
name: string;
description: string;
inputSchema: z.ZodSchema;
handler: (input: unknown, client: WhoopClient) => Promise<unknown>;
}
export const sleepTools: Tool[] = [
// ... existing tools
{
name: 'get-sleep-trend',
description: 'Analyze sleep score trends over time. Returns whether sleep is improving, declining, or stable.',
inputSchema: GetSleepTrendSchema,
handler: async (input, client) => {
const validated = GetSleepTrendSchema.parse(input);
return getSleepTrend(validated, client);
},
},
];
Step 4: Write Tests
Test both the handler and schema:
// src/tools/sleep-trend.test.ts
import { describe, it, expect, vi } from 'vitest';
import { GetSleepTrendSchema } from './schemas';
import { getSleepTrend } from './sleep-trend';
import { MockWhoopClient } from '../testing';
describe('GetSleepTrendSchema', () => {
it('validates correct input', () => {
const result = GetSleepTrendSchema.parse({ days: 14 });
expect(result.days).toBe(14);
expect(result.includeNaps).toBe(false);
});
it('uses defaults', () => {
const result = GetSleepTrendSchema.parse({});
expect(result.days).toBe(30);
expect(result.includeNaps).toBe(false);
});
it('rejects invalid days', () => {
expect(() => GetSleepTrendSchema.parse({ days: 5 })).toThrow();
expect(() => GetSleepTrendSchema.parse({ days: 100 })).toThrow();
});
});
describe('getSleepTrend', () => {
it('calculates improving trend', async () => {
const client = new MockWhoopClient();
client.setSleepData([
{ id: '1', providerId: '1', score: 70, /* ... */ },
{ id: '2', providerId: '2', score: 75, /* ... */ },
{ id: '3', providerId: '3', score: 80, /* ... */ },
{ id: '4', providerId: '4', score: 85, /* ... */ },
]);
const result = await getSleepTrend({ days: 7, includeNaps: false }, client);
expect(result.trend).toBe('improving');
expect(result.trendValue).toBeGreaterThan(0);
});
it('excludes naps by default', async () => {
const client = new MockWhoopClient();
client.setSleepData([
{ id: '1', providerId: '1', isNap: false, score: 80, /* ... */ },
{ id: '2', providerId: '2', isNap: true, score: 60, /* ... */ },
]);
const result = await getSleepTrend({ days: 7, includeNaps: false }, client);
expect(result.sessions).toBe(1);
expect(result.averageScore).toBe(80);
});
});
Step 5: Document the Tool
Add documentation to the tools reference:
### get-sleep-trend
Analyze sleep score trends over a time period.
**Input:**
| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| `days` | number | No | 30 | Days to analyze (7-90) |
| `includeNaps` | boolean | No | false | Include naps |
**Output:**
| Field | Type | Description |
|-------|------|-------------|
| `period.start` | string | Start date |
| `period.end` | string | End date |
| `sessions` | number | Number of sleep sessions |
| `averageScore` | number | Average sleep score |
| `trend` | string | "improving", "declining", or "stable" |
| `trendValue` | number | Numeric trend value |
**Example Questions:**
- "Is my sleep getting better?"
- "Analyze my sleep trends for the past month"
Tool Design Guidelines
Naming
- Use kebab-case:
get-sleep-trend, notgetSleepTrend - Start with a verb:
get-,analyze-,compare- - Be specific:
get-hrvnotget-metric
Descriptions
Write descriptions for AI understanding:
// Good: Explains purpose and return value
description: 'Analyze sleep score trends over time. Returns whether sleep is improving, declining, or stable.'
// Bad: Too vague
description: 'Get sleep trends'
// Bad: Implementation detail
description: 'Fetches sleep data and calculates linear regression'
Parameters
Keep parameters minimal:
// Good: One required, one optional with default
inputSchema: z.object({
days: z.number().default(30),
});
// Bad: Too many parameters
inputSchema: z.object({
startDate: z.string(),
endDate: z.string(),
includeNaps: z.boolean(),
minScore: z.number(),
maxScore: z.number(),
sortBy: z.enum(['date', 'score']),
});
Output
Return structured data, let the AI format it:
// Good: Structured data
return {
trend: 'improving',
averageScore: 82.5,
sessions: 14,
};
// Bad: Pre-formatted text
return 'Your sleep is improving! Average score: 82.5 over 14 sessions.';
Common Patterns
Date Range Tools
const DateRangeSchema = z.object({
startDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
endDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
});
Latest/Last Tools
// No input needed
const GetLatestSchema = z.object({});
async function getLatest(input: {}, client: WhoopClient) {
const today = new Date().toISOString().split('T')[0];
const weekAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)
.toISOString()
.split('T')[0];
const data = await client.getSleep(weekAgo, today);
return data[data.length - 1] || null;
}
Comparison Tools
const ComparePeriodsSchema = z.object({
period1Days: z.number().default(7),
period2Days: z.number().default(7),
});
async function comparePeriods(input: ComparePeriods, client: WhoopClient) {
const now = new Date();
const period1End = now;
const period1Start = new Date(now.getTime() - input.period1Days * 86400000);
const period2End = period1Start;
const period2Start = new Date(period2End.getTime() - input.period2Days * 86400000);
const [recent, previous] = await Promise.all([
client.getSleep(format(period1Start), format(period1End)),
client.getSleep(format(period2Start), format(period2End)),
]);
return {
recent: summarize(recent),
previous: summarize(previous),
change: calculateChange(recent, previous),
};
}