npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

rippling-node-sdk

v0.2.0

Published

Elegant, type-safe Node.js SDK for the Rippling REST API

Readme

rippling-node-sdk

version downloads

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-sdk

Requires 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 first Page<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 as Rippling.NotFoundError, etc.
  • Cancellable via AbortSignal plumbed through every method.
  • Resilient by default. 30s timeout, jittered exponential backoff, Retry-After honored.
  • 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

class Rippling

Default and named export.

new Rippling(); // reads RIPPLING_API_KEY
new Rippling("sk_..."); // string shorthand
new Rippling({ apiKey: "sk_...", ...options }); // full options

RipplingClientOptions

| 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 get

When 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 installUrl

2. 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 MyApp

What the bin does:

  1. Mounts mcpAuthRouter from @modelcontextprotocol/sdk at /.well-known/oauth-authorization-server and /.well-known/oauth-protected-resource (RFC 8414 + RFC 9728), advertising itself as both AS and RS.
  2. Configures a ProxyOAuthServerProvider whose authorizationUrl is your Rippling app listing's /apps/PLATFORM/{APPNAME}/authorize and whose tokenUrl is https://api.rippling.com/api/o/token/.
  3. Validates incoming bearer tokens by calling Rippling /sso-me. Auth failures translate to InvalidTokenError (401) and permission failures to InsufficientScopeError (403) per the MCP spec.
  4. Per request, builds a fresh Rippling client tied to the user's token and connects it to a StreamableHTTPServerTransport-backed MCP server. Each user gets their own scoped tool surface.
  5. 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.