Building a Provider
Step-by-step guide to creating a new health provider integration for Wellpipe.
Overview
A provider package connects Wellpipe to a health data source (like Oura, Garmin, or Fitbit). Each provider:
- Implements a client for the provider's API
- Defines MCP tools for accessing data
- Exports OAuth configuration (but doesn't handle the flow)
- Depends only on
@wellpipe/core
Project Setup
1. Create the Package
mkdir provider-oura
cd provider-oura
npm init -y
2. Configure package.json
{
"name": "@wellpipe/oura",
"version": "0.1.0",
"description": "Oura Ring integration for Wellpipe",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"type": "module",
"scripts": {
"build": "tsc",
"dev": "tsc --watch",
"test": "vitest run",
"typecheck": "tsc --noEmit"
},
"peerDependencies": {
"@wellpipe/core": "^0.1.0"
},
"devDependencies": {
"@wellpipe/core": "^0.1.0",
"typescript": "^5.3.0",
"vitest": "^1.0.0"
},
"dependencies": {
"zod": "^3.22.0"
}
}
3. Configure TypeScript
// tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "dist",
"rootDir": "src",
"strict": true,
"declaration": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src/**/*"]
}
Project Structure
provider-oura/
├── src/
│ ├── index.ts # Main exports
│ ├── client/
│ │ ├── index.ts # OuraClient class
│ │ ├── sleep.ts # Sleep API
│ │ ├── activity.ts # Activity API
│ │ └── readiness.ts # Readiness API
│ ├── tools/
│ │ ├── index.ts # All tools
│ │ ├── sleep.ts # Sleep tools
│ │ └── readiness.ts # Readiness tools
│ ├── types/
│ │ └── api.ts # API response types
│ └── oauth.ts # OAuth configuration
├── package.json
├── tsconfig.json
└── README.md
Implementing the Client
Main Client Class
// src/client/index.ts
import { TokenProvider } from '@wellpipe/core';
import { SleepApi } from './sleep';
import { ReadinessApi } from './readiness';
export interface OuraClientConfig {
tokenProvider: TokenProvider;
baseUrl?: string;
}
export class OuraClient {
private tokenProvider: TokenProvider;
private baseUrl: string;
readonly sleep: SleepApi;
readonly readiness: ReadinessApi;
constructor(config: OuraClientConfig) {
this.tokenProvider = config.tokenProvider;
this.baseUrl = config.baseUrl ?? 'https://api.ouraring.com/v2';
this.sleep = new SleepApi(this);
this.readiness = new ReadinessApi(this);
}
async fetch<T>(path: string, options?: RequestInit): Promise<T> {
const token = await this.tokenProvider.getAccessToken();
const response = await fetch(`${this.baseUrl}${path}`, {
...options,
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
...options?.headers,
},
});
if (!response.ok) {
throw new OuraApiError(response.status, await response.text());
}
return response.json();
}
}
export class OuraApiError extends Error {
constructor(public status: number, public body: string) {
super(`Oura API error: ${status}`);
this.name = 'OuraApiError';
}
}
API Module
// src/client/sleep.ts
import { z } from 'zod';
import type { OuraClient } from './index';
const SleepResponseSchema = z.object({
data: z.array(z.object({
id: z.string(),
day: z.string(),
bedtime_start: z.string(),
bedtime_end: z.string(),
score: z.number().nullable(),
// ... more fields
})),
next_token: z.string().nullable(),
});
export type SleepResponse = z.infer<typeof SleepResponseSchema>;
export class SleepApi {
constructor(private client: OuraClient) {}
async getRecent(days: number = 7): Promise<SleepResponse> {
const end = new Date().toISOString().split('T')[0];
const start = new Date(Date.now() - days * 24 * 60 * 60 * 1000)
.toISOString().split('T')[0];
const response = await this.client.fetch<unknown>(
`/usercollection/sleep?start_date=${start}&end_date=${end}`
);
return SleepResponseSchema.parse(response);
}
async getSummary(startDate: string, endDate: string): Promise<SleepSummary> {
const all = await this.fetchAll(startDate, endDate);
return {
period: { start: startDate, end: endDate, days: all.length },
averages: {
score: average(all.map(s => s.score).filter(Boolean)),
duration_hours: average(all.map(s => s.total_sleep_duration / 3600)),
}
};
}
}
Defining MCP Tools
Tool Structure
// src/tools/sleep.ts
import { z } from 'zod';
import type { OuraClient } from '../client';
export const sleepTools = [
{
name: 'get-oura-sleep-recent',
description: 'Get sleep data from Oura for the last N days',
inputSchema: z.object({
days: z.number().min(1).max(90).default(7),
}),
handler: async (args: { days?: number }, client: OuraClient) => {
const sleep = await client.sleep.getRecent(args.days ?? 7);
return {
content: [{
type: 'text' as const,
text: JSON.stringify(sleep, null, 2),
}],
};
},
},
{
name: 'get-oura-sleep-summary',
description: 'Get aggregated sleep metrics for a date range',
inputSchema: z.object({
start_date: z.string().describe('Start date (YYYY-MM-DD)'),
end_date: z.string().describe('End date (YYYY-MM-DD)'),
}),
handler: async (
args: { start_date: string; end_date: string },
client: OuraClient
) => {
const summary = await client.sleep.getSummary(args.start_date, args.end_date);
return {
content: [{
type: 'text' as const,
text: JSON.stringify(summary, null, 2),
}],
};
},
},
];
Combining Tools
// src/tools/index.ts
import { sleepTools } from './sleep';
import { readinessTools } from './readiness';
import { activityTools } from './activity';
export const allTools = [
...sleepTools,
...readinessTools,
...activityTools,
];
export { sleepTools, readinessTools, activityTools };
OAuth Configuration
// src/oauth.ts
export const ouraOAuthConfig = {
authorizationUrl: 'https://cloud.ouraring.com/oauth/authorize',
tokenUrl: 'https://api.ouraring.com/oauth/token',
scopes: ['daily', 'heartrate', 'personal', 'session', 'workout'],
clientId: process.env.OURA_CLIENT_ID,
};
Note: The provider exports configuration, but doesn't implement the OAuth flow. That's handled by the CLI or Cloud.
Main Exports
// src/index.ts
export { OuraClient, OuraClientConfig, OuraApiError } from './client';
export { allTools, sleepTools, readinessTools, activityTools } from './tools';
export { ouraOAuthConfig } from './oauth';
export type { SleepResponse, ReadinessResponse } from './client';
Testing
// src/client/sleep.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { OuraClient } from './index';
const mockTokenProvider = {
getAccessToken: vi.fn().mockResolvedValue('mock-token'),
getRefreshToken: vi.fn(),
updateTokens: vi.fn(),
isExpired: vi.fn().mockResolvedValue(false),
};
describe('SleepApi', () => {
let client: OuraClient;
beforeEach(() => {
vi.clearAllMocks();
client = new OuraClient({ tokenProvider: mockTokenProvider });
global.fetch = vi.fn();
});
it('should fetch recent sleep', async () => {
vi.mocked(fetch).mockResolvedValueOnce({
ok: true,
json: async () => ({
data: [{ id: 'sleep-1', score: 85 }],
next_token: null,
}),
} as Response);
const result = await client.sleep.getRecent(7);
expect(result.data).toHaveLength(1);
expect(result.data[0].score).toBe(85);
});
});
Documentation
Create a README.md:
# @wellpipe/oura
Oura Ring integration for Wellpipe.
## Installation
\`\`\`bash
npm install @wellpipe/oura
\`\`\`
## Usage
\`\`\`typescript
import { OuraClient, allTools } from '@wellpipe/oura';
const client = new OuraClient({
tokenProvider: yourTokenProvider,
});
// Fetch sleep data
const sleep = await client.sleep.getRecent(7);
\`\`\`
## Available Tools
- `get-oura-sleep-recent` - Recent sleep data
- `get-oura-sleep-summary` - Aggregated sleep metrics
- `get-oura-readiness-recent` - Recent readiness scores
- ...
Checklist
Before publishing:
- All API endpoints implemented
- Zod schemas for all responses
- MCP tools for all data types
- Tests for client and tools
- TypeScript strict mode passes
- README with usage examples
- OAuth config exported
Next Steps
- Adding MCP Tools - More detail on tool design
- Testing Guide - Comprehensive testing patterns
- Contributing - Submit your provider