rippling-node-sdk
v0.2.0
Published
Elegant, type-safe Node.js SDK for the Rippling REST API
Maintainers
Readme
rippling-node-sdk
The Node.js SDK for the Rippling REST API. Covers all 44 documented resources across HRIS, Organizational Data, Data, Talent, Time, Developer, and Saved Supergroups.
Install
npm install rippling-node-sdkRequires Node.js >= 22 (uses platform fetch, AbortSignal.any, and Error.cause).
Usage
import Rippling from "rippling-node-sdk";
const client = new Rippling();
// reads RIPPLING_API_KEY from env. Or pass: new Rippling("sk_…") | new Rippling({ apiKey })
for await (const worker of client.workers.list({
filter: { status: "ACTIVE" },
expand: ["user", "compensation"],
order_by: "created_at desc",
})) {
console.log(worker.user?.name?.display_name, worker.compensation?.annual_compensation?.value);
}A single await returns the first page (Stripe-style) — the same value also async-iterates over every page:
const page = await client.workers.list({ limit: 25 });
console.log(page.data, page.has_more, page.cursor);
for await (const worker of client.workers.list()) {
/* auto-paginates */
}Need an OAuth-authenticated MCP server (Claude.ai web, Anthropic API, hosted gateways)? Three lines:
import { runHttpServer } from "rippling-node-sdk/mcp";
await runHttpServer({
publicUrl: "https://my-mcp.example.com",
ripplingPlatform: "PLATFORM", // from your Rippling app listing
ripplingAppName: "MyApp",
});The server advertises OAuth metadata at /.well-known/oauth-authorization-server + /.well-known/oauth-protected-resource, proxies user consent to Rippling, validates bearer tokens via /sso-me, and runs the MCP transport at /mcp. Every request gets its own Rippling client scoped to the user's token. Or run it from the CLI: npx rippling-mcp --http --public-url https://… --rippling-platform PLATFORM --rippling-app-name MyApp. See MCP server for the full reference.
Highlights
- Default + named export.
import Rippling from "rippling-node-sdk"works. - Awaitable + iterable lists.
await list()returns the firstPage<T>; the same value async-iterates over every page. - Three filter styles. Object literals (Prisma-style), a typed combinator DSL, or raw strings from the docs.
- One error hierarchy with
status,code,requestId,body,bodyText. Reachable as named imports and asRippling.NotFoundError, etc. - Cancellable via
AbortSignalplumbed through every method. - Resilient by default. 30s timeout, jittered exponential backoff,
Retry-Afterhonored. - Snake_case responses. Field names match the Rippling docs character-for-character.
- 44 typed resources spanning HRIS, Organizational Data, Data, Talent, Time, Developer, and Saved Supergroups.
API Reference
- Client
- Errors
- Pagination
- Filters
- Common method shapes
- Resources
- OAuth
- Raw HTTP escape hatch
- MCP server (
rippling-node-sdk/mcp) - Type index
Client
class Rippling
Default and named export.
new Rippling(); // reads RIPPLING_API_KEY
new Rippling("sk_..."); // string shorthand
new Rippling({ apiKey: "sk_...", ...options }); // full optionsRipplingClientOptions
| Option | Type | Default | Notes |
| ---------------- | ------------------------ | --------------------------------- | ------------------------------------------------------------------------------- |
| apiKey | string | process.env.RIPPLING_API_KEY | Bearer token. Required (after env fallback). |
| baseUrl | string | "https://rest.ripplingapis.com" | Base URL of the Rippling REST API. |
| apiVersion | string | undefined | Sets the Rippling-Api-Version header (e.g. "2024-01-31"). |
| timeoutMs | number | 30_000 | Per-request timeout. Aborts via internal AbortSignal. |
| maxRetries | number | 3 | Retries 408/425/429/5xx and network/timeout failures with jittered backoff. |
| userAgent | string | "rippling-node-sdk/<version>" | Sent as User-Agent. Override per app for traffic attribution. |
| fetch | typeof fetch | globalThis.fetch | Bring your own fetch implementation. |
| defaultHeaders | Record<string, string> | {} | Merged into every outbound request (lower precedence than per-request headers). |
RequestOptions
Accepted by every method. All optional.
| Field | Type | Notes |
| ------------ | ------------------------ | -------------------------------------------------------- |
| signal | AbortSignal | Cancellation. Composed with the internal timeout signal. |
| timeoutMs | number | Override per-call timeout. |
| maxRetries | number | Override per-call retry budget. 0 to opt out entirely. |
| headers | Record<string, string> | Merged over defaultHeaders. |
| apiVersion | string | Per-call override of Rippling-Api-Version. |
ListOptions extends RequestOptions and adds:
| Field | Type | Notes |
| ---------- | ------------------------------------------------- | ----------------------------------------------------------- |
| filter | string \| FilterExpression \| FilterObjectInput | Three forms accepted. See Filters. |
| expand | string \| readonly string[] | Expand related objects. Comma-joined in the URL. |
| order_by | string \| readonly string[] | Snake_case (matches Rippling docs). |
| orderBy | string \| readonly string[] | CamelCase alias of order_by. |
| limit | number | Default 50, most endpoints cap at 100. |
| cursor | string | Forward cursor. Usually managed for you by PaginatedList. |
RetrieveOptions extends RequestOptions and adds expand.
Errors
Every failure is a subclass of RipplingError:
class RipplingError extends Error {
status?: number; // HTTP status code (when applicable)
code?: string; // Rippling error code from the response body
requestId?: string; // request-id header (multiple variants checked)
body?: unknown; // parsed response body
bodyText?: string; // raw response body (preserved when JSON parse fails)
cause?: unknown; // underlying cause (Error.cause, e.g. network error)
isRetryable: boolean; // true on rate-limit/server/network/timeout
}Reachable as static members on Rippling and as named exports.
| HTTP / cause | Class |
| ---------------- | ---------------------------------------------------------------------------------------- |
| Generic / base | RipplingError aka Rippling.Error |
| Setup / config | RipplingConfigError aka Rippling.ConfigError |
| 401 | RipplingAuthError aka Rippling.AuthError |
| 403 | RipplingPermissionError aka Rippling.PermissionError |
| 404 | RipplingNotFoundError aka Rippling.NotFoundError |
| 4xx (other) | RipplingValidationError aka Rippling.ValidationError |
| 429 | RipplingRateLimitError (+ retryAfterMs) aka Rippling.RateLimitError |
| 5xx | RipplingServerError aka Rippling.ServerError |
| Network failure | RipplingNetworkError aka Rippling.NetworkError |
| Timeout | RipplingTimeoutError aka Rippling.TimeoutError |
| Caller cancelled | RipplingAbortError aka Rippling.AbortError |
| OAuth flow | RipplingOAuthError (+ oauthError, oauthErrorDescription) aka Rippling.OAuthError |
RateLimitError, ServerError, NetworkError, and TimeoutError all expose isRetryable === true. The SDK retries them automatically with jittered exponential backoff capped at 8s, honoring Retry-After. Set maxRetries: 0 to opt out.
try {
await client.workers.retrieve("nope");
} catch (caught) {
if (caught instanceof Rippling.NotFoundError) {
/* 404 */
}
if (caught instanceof Rippling.RateLimitError) {
console.log("retry after", caught.retryAfterMs);
}
}Pagination
Every list() returns a PaginatedList<T>:
class PaginatedList<T> implements AsyncIterable<T>, PromiseLike<Page<T>> {
page(options?: ListOptions): Promise<Page<T>>;
pages(options?: RequestOptions): AsyncIterableIterator<Page<T>>;
toArray(options?: RequestOptions): Promise<T[]>;
collect(options?: RequestOptions): Promise<{ results: T[]; redactedFields: readonly RedactedField[] }>;
then(...): Promise<Page<T>>; // makes `await list()` work
[Symbol.asyncIterator](): AsyncIterableIterator<T>; // makes `for await (item of list)` work
}Page<T> exposes both snake_case and camelCase aliases for ergonomics:
class Page<T> {
results: readonly T[]; // alias: data
data: readonly T[];
next_link: string | null; // alias: nextLink
has_more: boolean; // alias: hasMore
cursor: string | undefined; // parsed from next_link
redacted_fields: readonly { name: string; reason: string }[]; // alias: redactedFields
}// Stripe / OpenAI style — first page.
const page = await client.users.list({ limit: 25 });
page.data; // User[]
// Auto-paginate flat across all pages.
for await (const user of client.users.list()) {
/* ... */
}
// Page-by-page (great for batch processing / backpressure).
for await (const page of client.users.list().pages()) {
console.log(page.results.length);
}
// Collect everything (use carefully on large datasets).
const all = await client.users.list().toArray();
// Same as toArray, plus the union of redacted_fields metadata across pages.
const { results, redactedFields } = await client.users.list().collect();Filters
Three styles, one wire format. Pick whichever fits your code.
Object literal (Prisma / Mongo prior)
Top-level keys are AND-ed together. Empty objects and undefined values are no-ops, so dynamic filter building Just Works.
client.workers.list({
filter: {
status: "ACTIVE", // shorthand for { eq: "ACTIVE" }
legal_entity_id: { in: ["abc", "def"] },
created_at: { gte: new Date("2025-01-01") },
or: [{ status: "ACTIVE" }, { status: "HIRED" }],
},
});Operator keys: eq, ne, gt, gte, lt, lte, in. Connectives: and, or.
Combinator DSL (typed, composable)
import { and, eq, gte, isIn } from "rippling-node-sdk";
await client.workers.list({
filter: and(
eq("status", "ACTIVE"),
isIn("legal_entity_id", ["abc", "def"]),
gte("created_at", new Date("2025-01-01")),
),
});| Function | Result |
| ----------------------- | ------------------------------------------------------------------- |
| eq(field, value) | field eq <value> |
| ne(field, value) | field ne <value> |
| gt(field, value) | field gt <value> |
| gte(field, value) | field gte <value> |
| lt(field, value) | field lt <value> |
| lte(field, value) | field lte <value> |
| isIn(field, values) | field in (<v1>,<v2>,…) |
| and(...expressions) | (a) and (b) and … |
| or(...expressions) | (a) or (b) or … |
| raw(expression) | passes string through verbatim |
| filterFromObject(obj) | converts object form (used internally; exported for advanced cases) |
Strings are auto-quoted ('-doubled for escaping), Date is ISO-encoded, null becomes null, booleans become true/false.
Raw string
await client.workers.list({ filter: "status eq 'ACTIVE'" });Common method shapes
Most resources extend the same shape. When a resource extends ListableResource<T>:
list(options?: ListOptions): PaginatedList<T>;
get(id: string, options?: RetrieveOptions): Promise<T>;
retrieve(id: string, options?: RetrieveOptions): Promise<T>; // alias of getWhen a resource extends WritableResource<T, CreateBody?, UpdateBody?>, it adds:
create(body: CreateBody, options?: RequestOptions): Promise<T>;
update(id: string, body: UpdateBody, options?: RequestOptions): Promise<T>;
delete(id: string, options?: RequestOptions): Promise<void>;In the catalogues below, List + retrieve means just list/get/retrieve. Full CRUD means it has all of the above. Bespoke methods are listed explicitly.
Resources
HRIS
| Accessor | Class | Methods |
| ---------------------- | ----------------------- | ------------------------------------------------------------ |
| client.workers | WorkersResource | List + retrieve. Result: Worker. |
| client.users | UsersResource | List + retrieve. Result: User. |
| client.compensations | CompensationsResource | List + retrieve. Result: Compensation. |
| client.entitlements | EntitlementsResource | List + retrieve. Result: Entitlement. |
| client.sso | SsoResource | me(options?) → Promise<SsoMe>. |
| client.draftHires | DraftHiresResource | createBulk(body, options?) → Promise<DraftHireResponse>. |
const ssoInfo = await client.sso.me();
const result = await client.draftHires.createBulk({ workers: [{ email: "[email protected]" }] });Organizational Data
| Accessor | Class | Methods |
| ------------------------------ | ------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| client.departments | DepartmentsResource | List + retrieve. Result: Department. |
| client.employmentTypes | EmploymentTypesResource | List + retrieve. Result: EmploymentType. |
| client.legalEntities | LegalEntitiesResource | List + retrieve. Result: LegalEntity. |
| client.workLocations | WorkLocationsResource | List + retrieve. Result: WorkLocation. |
| client.titles | TitlesResource | Full CRUD. Result: Title. |
| client.jobAssignments | JobAssignmentsResource | Full CRUD + changeLog(id, options?) → PaginatedList<AttributeChangeLogEntry>. Result: JobAssignment. |
| client.jobCodes | JobCodesResource | Full CRUD + changeLog(id, options?). Result: JobCode. |
| client.jobPayRateExceptions | JobPayRateExceptionsResource | List + retrieve + changeLog(id, options?). Result: JobPayRateException. |
| client.jobDimensions | JobDimensionsResource | Full CRUD. Result: JobDimension. |
| client.levels | LevelsResource | List + retrieve. Result: Level. |
| client.tracks | TracksResource | List + retrieve. Result: Track. |
| client.teams | TeamsResource | List + retrieve. Result: Team. |
| client.businessPartners | BusinessPartnersResource | List + retrieve + create(body, options?) + delete(id, options?). Result: BusinessPartner. |
| client.businessPartnerGroups | BusinessPartnerGroupsResource | List + retrieve + create(body, options?) + delete(id, options?). Result: BusinessPartnerGroup. |
Data
| Accessor | Class | Methods |
| ------------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------- |
| client.customFields | CustomFieldsResource | list(options?) → PaginatedList<CustomField> (read-only). |
| client.customObjects | CustomObjectsResource | List + get/retrieve(apiName) + create/update/delete(apiName, …) + fields(apiName) + records(apiName). |
| client.objectCategories | ObjectCategoriesResource | Full CRUD. Result: ObjectCategory. |
CustomObjectsResource.records(apiName) returns a typed CustomObjectRecordsResource:
class CustomObjectRecordsResource {
list<T>(options?: ListOptions): PaginatedList<T>;
get<T>(recordId, options?): Promise<T>;
retrieve<T>(recordId, options?): Promise<T>;
getByExternalId<T>(externalId, options?): Promise<T>;
create<T>(body, options?): Promise<T>;
update<T>(recordId, body, options?): Promise<T>;
delete(recordId, options?): Promise<void>;
query<T>(body: CustomObjectQueryRequest, options?): Promise<Page<T>>;
bulkCreate<T>(rows, options?): Promise<BulkResult<T>>;
bulkUpdate<T>(rows, options?): Promise<BulkResult<T>>;
bulkDelete(ids, options?): Promise<BulkResult<{ id: string }>>;
}const pets = client.customObjects.records<{ id: string; name: string }>("pet__c");
const fido = await pets.create({ name: "Fido" });
const matches = await pets.query({ filter: "name eq 'Fido'", limit: 50 });
const { succeeded, failed } = await pets.bulkCreate([{ name: "Rex" }, { name: "Spot" }]);Talent
| Accessor | Class | Methods |
| --------------------------------- | ---------------------------------- | ---------------------------------------------------- |
| client.compensationBandsDetails | CompensationBandsDetailsResource | List + retrieve. Result: CompensationBandsDetails. |
| client.jobFunctions | JobFunctionsResource | List + retrieve. Result: JobFunction. |
| client.locationFactors | LocationFactorsResource | List + retrieve. Result: LocationFactor. |
| client.headcountPositions | HeadcountPositionsResource | List + retrieve. Result: HeadcountPosition. |
| client.headcountPriorities | HeadcountPrioritiesResource | List + retrieve. Result: HeadcountPriority. |
| client.candidates | CandidatesResource | List + retrieve. Result: Candidate. |
| client.candidateApplications | CandidateApplicationsResource | List + retrieve. Result: CandidateApplication. |
| client.jobRequisitions | JobRequisitionsResource | List + retrieve. Result: JobRequisition. |
Time
| Accessor | Class | Methods |
| ------------------------- | -------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| client.schedules | SchedulesResource | Full CRUD. Result: Schedule. |
| client.shiftAssignments | ShiftAssignmentsResource | Full CRUD + updateCustomFields(id, fields, options?) → Promise<ShiftAssignment>. |
| client.unassignedShifts | UnassignedShiftsResource | Full CRUD + updateCustomFields(id, fields, options?) + assign(id, { worker_id }, options?) → Promise<ShiftAssignment>. |
| client.shiftInputs | ShiftInputsResource | Full CRUD. Result: ShiftInput. |
| client.timeCards | TimeCardsResource | List + retrieve. Result: TimeCard. |
| client.timeEntries | TimeEntriesResource | Full CRUD. Result: TimeEntry. |
| client.leaveAccruals | LeaveAccrualsResource | List + retrieve + create(body, options?) → Promise<LeaveAccrual>. |
| client.leaveBalances | LeaveBalancesResource | List + retrieve. Result: LeaveBalance. |
| client.leaveRequests | LeaveRequestsResource | List + retrieve + create(body, options?) + update(id, body, options?) → Promise<LeaveRequest>. |
| client.leaveTypes | LeaveTypesResource | List + retrieve. Result: LeaveType. |
| client.kioskBadges | KioskBadgesResource | Full CRUD. Result: KioskBadge. |
const assigned = await client.unassignedShifts.assign(unassignedShiftId, { worker_id });
await client.shiftAssignments.updateCustomFields(shiftId, { project: "alpha" });
const request = await client.leaveRequests.create({
worker_id,
leave_type_id,
start_date: "2026-05-01",
end_date: "2026-05-05",
});Developer
| Accessor | Class | Methods |
| ------------------ | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| client.functions | FunctionsResource | list(options?) → PaginatedList<RipplingFunction>. get/retrieve(apiName, options?). create(body: FunctionRequest). update(apiName, body, options?). executions(apiName) → FunctionExecutionsResource. deployments(apiName) → FunctionDeploymentsResource. createDevBundle(apiName, body?, options?) → Promise<FunctionDevBundle>. |
FunctionExecutionsResource and FunctionDeploymentsResource each expose list, get/retrieve, create, and logs(id, options?) (a PaginatedList of log entries).
const fn = await client.functions.create({
name: "My Function",
api_name: "my_function",
code_draft: "export const onRipplingEvent = async () => ({});",
});
const execution = await client.functions
.executions(fn.api_name!)
.create({ inputs: { foo: "bar" } });
for await (const log of client.functions.executions(fn.api_name!).logs(execution.id)) {
console.log(log.level, log.message);
}Saved Supergroups
| Accessor | Class | Methods |
| -------------------- | --------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| client.supergroups | SupergroupsResource | list(options?) → PaginatedList<Supergroup>. get/retrieve(id, options?). members(id) → SupergroupMembersResource. |
SupergroupMembersResource:
class SupergroupMembersResource {
all(options?): PaginatedList<SupergroupMember>;
inclusions(options?): PaginatedList<SupergroupMember>;
setInclusions({ worker_ids }, options?): Promise<{ updated: number }>;
exclusions(options?): PaginatedList<SupergroupMember>;
setExclusions({ worker_ids }, options?): Promise<{ updated: number }>;
}await client.supergroups.members(groupId).setInclusions({ worker_ids: ["w1", "w2"] });
for await (const member of client.supergroups.members(groupId).all()) {
console.log(member.worker_id, member.reason);
}OAuth
For Rippling app-shop integrations that authenticate users via OAuth 2.0 (authorization code flow), four helpers cover the full lifecycle:
import Rippling, {
buildInstallUrl,
exchangeCode,
refresh,
markAppInstalled,
} from "rippling-node-sdk";
// Or namespaced: Rippling.OAuth.exchangeCode(...)1. Send the user to Rippling
const installUrl = buildInstallUrl({
platform: "PLATFORM", // your platform identifier from the app listing
appName: "MyApp",
redirectUri: "https://my.app/oauth/callback",
state: crypto.randomUUID(),
scope: ["employee:workEmail", "employee:name"],
});
// Redirect user to installUrl2. Exchange the code for tokens
In your redirect_uri handler, exchange the ?code=… query param within 300 seconds:
const tokens = await exchangeCode({
clientId: process.env.RIPPLING_CLIENT_ID!,
clientSecret: process.env.RIPPLING_CLIENT_SECRET!,
code: req.query.code as string,
redirectUri: "https://my.app/oauth/callback",
});
// tokens: { access_token, token_type: "Bearer", expires_in, refresh_token, scope }3. Use the access token with the SDK
const client = new Rippling({ apiKey: tokens.access_token });
for await (const worker of client.workers.list()) {
/* ... */
}4. Refresh when expires_in runs out
Access tokens currently last ~36 hours (expires_in: 129600). Refresh tokens are long-lived:
const fresh = await refresh({
clientId: process.env.RIPPLING_CLIENT_ID!,
clientSecret: process.env.RIPPLING_CLIENT_SECRET!,
refreshToken: tokens.refresh_token!,
});5. Optional: mark the install complete
await markAppInstalled({ accessToken: tokens.access_token });Errors
OAuth-specific failures throw RipplingOAuthError (subclass of RipplingError), with extra oauthError and oauthErrorDescription fields populated from the OAuth 2.0 error response ({ error, error_description }):
try {
await exchangeCode(/* ... */);
} catch (caught) {
if (caught instanceof Rippling.OAuthError && caught.oauthError === "invalid_grant") {
// authorization code expired or already used — restart the flow
}
}Options
| Helper | Required | Optional |
| ------------------ | ------------------------------------------------- | -------------------------------------------- |
| buildInstallUrl | platform, appName | redirectUri, state, scope, baseUrl |
| exchangeCode | clientId, clientSecret, code, redirectUri | signal, fetch, timeoutMs, tokenUrl |
| refresh | clientId, clientSecret, refreshToken | signal, fetch, timeoutMs, tokenUrl |
| markAppInstalled | accessToken | signal, fetch, timeoutMs, apiBaseUrl |
All helpers accept signal for cancellation, fetch for transport injection, and timeoutMs (default 30s) — same conventions as the rest of the SDK.
Raw HTTP escape hatch
For endpoints not yet typed, drop down to the raw HTTP layer without losing retries, timeouts, or error handling:
client.request<T>(
method: "GET" | "POST" | "PATCH" | "PUT" | "DELETE",
path: string,
options?: { body?: unknown; query?: string; request?: RequestOptions },
): Promise<T>;const result = await client.request<{ results: unknown[] }>("GET", "/some-new-endpoint", {
query: "?limit=10",
});MCP server (rippling-node-sdk/mcp)
This package ships an MCP server that exposes the Rippling read APIs as tools. Drop it into Claude Desktop, Cursor, or any MCP host and your assistant can list workers, look up compensations, query custom objects, etc.
Quick start with Claude Desktop
After npm install rippling-node-sdk, add to ~/Library/Application Support/Claude/claude_desktop_config.json (or the equivalent on your platform):
{
"mcpServers": {
"rippling": {
"command": "npx",
"args": ["-y", "rippling-node-sdk", "rippling-mcp"],
"env": { "RIPPLING_API_KEY": "sk_..." }
}
}
}Or, if you've installed the package locally, point at the bin directly:
{
"mcpServers": {
"rippling": {
"command": "node",
"args": ["/ABSOLUTE/PATH/TO/node_modules/rippling-node-sdk/bin/rippling-mcp.mjs"],
"env": { "RIPPLING_API_KEY": "sk_..." }
}
}
}Restart the host. Your assistant now has 55 read-only Rippling tools (rippling_workers_list, rippling_workers_get, rippling_compensations_list, rippling_custom_object_records_list, rippling_sso_me, …).
CLI flags
rippling-mcp [--preset <core|all>] [--tools <name,name,...>] [--write] [--help]| Flag | Effect |
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------- |
| --preset core | Curated 17-tool subset for tight context budgets (workers, users, compensations, leave, departments, …). |
| --preset all | All 55 read tools (default). |
| --tools a,b,c | Comma-separated allow-list. Combines with --preset (intersection). |
| --write | Adds 11 write tools (create/update/delete for titles, time_entries, shift_assignments, leave_requests). Off by default. |
| --http | Boot an OAuth 2.1 + PKCE-protected HTTP MCP server (instead of stdio). See HTTP mode below. |
| --public-url, --rippling-* | HTTP-mode flags. See --help. |
| --help | Print usage. |
Pass these via the args array in your host config:
{
"mcpServers": {
"rippling": {
"command": "npx",
"args": ["-y", "rippling-node-sdk", "rippling-mcp", "--preset", "core"],
"env": { "RIPPLING_API_KEY": "sk_..." }
}
}
}Programmatic embedding
Use the same export to register Rippling tools on your own MCP server, with HTTP transport, or alongside other tools:
import { createMcpServer, runStdioServer, registerTools } from "rippling-node-sdk/mcp";
await runStdioServer({ clientOptions: { apiKey: process.env.RIPPLING_API_KEY! } });const { server, client, registeredTools } = createMcpServer({
clientOptions: { apiKey: process.env.RIPPLING_API_KEY! },
toolFilter: (name) => name.startsWith("rippling_workers_") || name === "rippling_sso_me",
});
console.log("registered", registeredTools.length, "Rippling tools");| Export | Purpose |
| ----------------------------------------- | ------------------------------------------------------------------------------------- |
| runStdioServer(options?) | Boot a stdio MCP server in one call. Used by the rippling-mcp bin. |
| createMcpServer(options?) | Build a configured McpServer instance with tools registered. Pass to any transport. |
| registerTools(server, client, options?) | Register Rippling tools onto an existing McpServer. |
| ALL_TOOLS / listToolNames(preset?) | Inspect or filter the tool catalog. |
| runHttpServer(options) | Boot an OAuth-protected HTTP MCP server (see below). |
| buildRipplingOAuthProvider(options) | Construct a ProxyOAuthServerProvider that proxies to Rippling's OAuth endpoints. |
| verifyRipplingToken(token, options?) | Validate a Rippling access token via /sso-me and return MCP AuthInfo. |
| InMemoryClientsStore | RFC 7591 (DCR) client registry kept in-memory; pluggable via staticClients. |
McpServerOptions: clientOptions, client, name, version, preset ("core" | "all"), includeWrites, toolFilter. Writes are off by default — pass includeWrites: true only if your host approves.
Each registered tool propagates the host's per-call AbortSignal through to the underlying Rippling fetch, so cancelling a tool in your MCP host aborts the in-flight request.
HTTP mode (MCP OAuth)
Remote MCP hosts (Anthropic API, hosted gateways, Claude.ai web) can't ship the user's API token; they need OAuth. Per the MCP authorization spec, an HTTP-transport MCP server acts as both a Resource Server and (optionally) a proxy Authorization Server delegating to an upstream IdP — in our case, Rippling.
npx rippling-mcp --http \
--public-url https://my-mcp.example.com \
--rippling-platform PLATFORM \
--rippling-app-name MyAppWhat the bin does:
- Mounts
mcpAuthRouterfrom@modelcontextprotocol/sdkat/.well-known/oauth-authorization-serverand/.well-known/oauth-protected-resource(RFC 8414 + RFC 9728), advertising itself as both AS and RS. - Configures a
ProxyOAuthServerProviderwhoseauthorizationUrlis your Rippling app listing's/apps/PLATFORM/{APPNAME}/authorizeand whosetokenUrlishttps://api.rippling.com/api/o/token/. - Validates incoming bearer tokens by calling Rippling
/sso-me. Auth failures translate toInvalidTokenError(401) and permission failures toInsufficientScopeError(403) per the MCP spec. - Per request, builds a fresh
Ripplingclient tied to the user's token and connects it to aStreamableHTTPServerTransport-backed MCP server. Each user gets their own scoped tool surface. - Supports RFC 7591 dynamic client registration via an in-memory store (disable with
--no-dynamic-registration).
Programmatically:
import { runHttpServer } from "rippling-node-sdk/mcp";
await runHttpServer({
publicUrl: "https://my-mcp.example.com",
ripplingPlatform: "PLATFORM",
ripplingAppName: "MyApp",
port: 3000,
preset: "core",
});You can also bring your own provider, Express app, or static clients:
import {
runHttpServer,
buildRipplingOAuthProvider,
InMemoryClientsStore,
} from "rippling-node-sdk/mcp";
const clientsStore = new InMemoryClientsStore({
staticClients: [
{
client_id: "my_pre_registered_client",
client_id_issued_at: 0,
client_name: "My MCP Host",
redirect_uris: ["https://host.example.com/oauth/callback"],
},
],
});
const provider = buildRipplingOAuthProvider({
authorizationUrl: "https://app.rippling.com/apps/PLATFORM/MyApp/authorize",
clientsStore,
});
await runHttpServer({
provider,
publicUrl: "https://my-mcp.example.com",
ripplingPlatform: "PLATFORM",
ripplingAppName: "MyApp",
});Security. The MCP spec mandates RFC 8707 audience binding — clients must include
resource=<your MCP server URL>in authorization & token requests, and the server must reject tokens issued for other audiences. The proxy provider passes through whatever Rippling issues; if your Rippling deployment doesn't support audience binding, treat the MCP server as a single-tenant boundary and rely on token freshness (verified per request via/sso-me).
Cancellation & timeouts
const controller = new AbortController();
setTimeout(() => controller.abort(), 5_000);
await client.workers.list({ signal: controller.signal }).toArray();
await client.workers.retrieve(id, { timeoutMs: 2_000, maxRetries: 0 });Type index
All resource result types extend BaseRecord ({ id, created_at, updated_at }). Fields are optional / nullable to match the Rippling redaction model — anything the token can't see comes back as null, with metadata in __meta.redacted_fields (also exposed on Page<T>.redacted_fields and PaginatedList.collect().redactedFields).
- Shared:
BaseRecord,Money,RedactedField,MetaResponse,PageResponse<T>,RipplingClientOptions,RequestOptions,ListOptions,RetrieveOptions,RipplingHeaders,FilterExpression,FilterObject,FilterOperators,FilterObjectInput,CollectedResults<T>. - HRIS:
Worker,User,Compensation,Entitlement,SsoMe,DraftHireRequest,DraftHireResponse,DraftHireWorkerInput,DraftHire. - Organizational Data:
Department,EmploymentType,LegalEntity,WorkLocation,Title,JobAssignment,JobCode,JobPayRateException,JobDimension,Level,Track,Team,BusinessPartner,BusinessPartnerGroup,AttributeChangeLogEntry. - Data:
CustomField,CustomObject,CustomObjectField,CustomObjectRecord,ObjectCategory,CustomObjectQueryRequest,BulkResult<T>,BulkFailure<T>. - Talent:
CompensationBandsDetails,JobFunction,LocationFactor,HeadcountPosition,HeadcountPriority,Candidate,CandidateApplication,JobRequisition. - Time:
Schedule,ShiftAssignment,UnassignedShift,ShiftInput,TimeCard,TimeEntry,LeaveAccrual,LeaveBalance,LeaveRequest,LeaveType,KioskBadge. - Developer:
RipplingFunction,FunctionRequest,FunctionExecution,FunctionExecutionRequest,FunctionExecutionLog,FunctionDeployment,FunctionDeploymentRequest,FunctionDeploymentLog,FunctionDevBundle. - Saved Supergroups:
Supergroup,SupergroupMember,SetMembershipRequest. - OAuth:
OAuthTokens,OAuthClientCredentials,OAuthRequestOptions,BuildInstallUrlOptions,ExchangeCodeOptions,RefreshOptions,MarkAppInstalledOptions,MarkAppInstalledResponse.
Resources & contributing back
Looking to contribute back? Check out the Contributing Guide.
Find a bug? Head over to the issue tracker — pull requests welcome too.
License
rippling-node-sdk is MIT-licensed open-source software. See LICENSE.
