Skip to main content

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