diff --git a/servers/rippling/README.md b/servers/rippling/README.md index c47fef9..03803d5 100644 --- a/servers/rippling/README.md +++ b/servers/rippling/README.md @@ -1,119 +1,295 @@ # Rippling MCP Server -MCP server for [Rippling](https://www.rippling.com/) API integration. Access employees, departments, teams, payroll, devices, and apps for HR and IT management. +Complete Model Context Protocol (MCP) server for the Rippling HR Platform API. -## Setup +## Overview + +This MCP server provides comprehensive integration with Rippling's HR platform, enabling AI assistants to interact with employee data, payroll, time tracking, benefits, recruiting (ATS), learning management, device inventory, and more. + +## Features + +### 🔧 **50+ Tools Across 10 Categories** + +#### Employees (7 tools) +- `rippling_list_employees` - List all employees with filters +- `rippling_get_employee` - Get detailed employee information +- `rippling_create_employee` - Create new employee records +- `rippling_update_employee` - Update employee information +- `rippling_terminate_employee` - Terminate an employee +- `rippling_list_employee_custom_fields` - List custom fields +- `rippling_get_org_chart` - Get organizational chart + +#### Companies (5 tools) +- `rippling_get_company` - Get company information +- `rippling_list_departments` - List all departments +- `rippling_create_department` - Create new department +- `rippling_list_locations` - List work locations +- `rippling_list_teams` - List all teams + +#### Payroll (4 tools) +- `rippling_list_pay_runs` - List payroll runs +- `rippling_get_pay_run` - Get pay run details +- `rippling_list_pay_statements` - List pay statements +- `rippling_get_pay_statement` - Get detailed pay statement + +#### Time Tracking (11 tools) +- `rippling_list_time_entries` - List time clock entries +- `rippling_create_time_entry` - Create time entry +- `rippling_update_time_entry` - Update time entry +- `rippling_delete_time_entry` - Delete time entry +- `rippling_get_timesheet` - Get timesheet +- `rippling_approve_timesheet` - Approve timesheet +- `rippling_list_time_off_requests` - List PTO requests +- `rippling_create_time_off_request` - Create PTO request +- `rippling_approve_time_off_request` - Approve PTO +- `rippling_deny_time_off_request` - Deny PTO + +#### Benefits (4 tools) +- `rippling_list_benefits_plans` - List benefits plans +- `rippling_get_benefits_plan` - Get plan details +- `rippling_list_benefits_enrollments` - List enrollments +- `rippling_get_benefits_enrollment` - Get enrollment details + +#### ATS/Recruiting (6 tools) +- `rippling_list_candidates` - List candidates +- `rippling_get_candidate` - Get candidate details +- `rippling_list_jobs` - List job postings +- `rippling_get_job` - Get job details +- `rippling_list_applications` - List applications +- `rippling_update_application_stage` - Move candidate through pipeline + +#### Learning (4 tools) +- `rippling_list_courses` - List training courses +- `rippling_get_course` - Get course details +- `rippling_list_course_assignments` - List assignments +- `rippling_assign_course` - Assign course to employee + +#### Devices (4 tools) +- `rippling_list_devices` - List hardware devices +- `rippling_get_device` - Get device details +- `rippling_list_apps` - List software/app licenses +- `rippling_get_app` - Get app license details + +#### Groups (6 tools) +- `rippling_list_groups` - List groups +- `rippling_get_group` - Get group details +- `rippling_create_group` - Create new group +- `rippling_update_group` - Update group +- `rippling_add_group_member` - Add member to group +- `rippling_remove_group_member` - Remove member from group + +#### Custom Objects (5 tools) +- `rippling_list_custom_objects` - List custom objects +- `rippling_get_custom_object` - Get custom object +- `rippling_create_custom_object` - Create custom object +- `rippling_update_custom_object` - Update custom object +- `rippling_query_custom_objects` - Query custom objects with filters + +### 🎨 **16 React UI Components** + +1. **EmployeeDashboard** - Overview of employee metrics and departments +2. **EmployeeDetail** - Detailed employee profile view +3. **EmployeeDirectory** - Searchable employee directory with filters +4. **OrgChart** - Visual organizational chart +5. **PayrollDashboard** - Payroll overview and recent runs +6. **PayrollDetail** - Detailed pay statement breakdown +7. **TimeTracker** - Clock in/out interface and time entry viewer +8. **TimesheetApprovals** - Approve/reject employee timesheets +9. **TimeOffCalendar** - PTO calendar and requests +10. **BenefitsOverview** - Benefits plans and enrollments +11. **ATSPipeline** - Recruiting pipeline visualization +12. **JobBoard** - Job postings board +13. **LearningDashboard** - Training and course completion dashboard +14. **DeviceInventory** - Hardware and device management +15. **TeamOverview** - Team composition and management +16. **DepartmentGrid** - Department overview and headcount + +## Installation ```bash npm install -npm run build ``` -## Environment Variables +## Configuration -| Variable | Required | Description | -|----------|----------|-------------| -| `RIPPLING_API_KEY` | Yes | Bearer API key or OAuth access token | +Set the following environment variables: -## API Endpoint +```bash +# Option 1: API Key authentication +export RIPPLING_API_KEY="your_api_key_here" -- **Base URL:** `https://api.rippling.com/platform/api` +# Option 2: OAuth2 Bearer token +export RIPPLING_ACCESS_TOKEN="your_access_token_here" -## Tools +# Optional: Custom API base URL +export RIPPLING_BASE_URL="https://api.rippling.com" +``` -### HR / People -- **list_employees** - List employees with pagination and terminated filter -- **get_employee** - Get detailed employee information -- **list_departments** - List all departments -- **list_teams** - List all teams -- **list_levels** - List job levels (IC1, Manager, etc.) -- **list_work_locations** - List office locations -- **get_leave_requests** - Get time-off/leave requests +## Usage -### Payroll -- **get_payroll** - Get payroll runs and compensation data +### As MCP Server -### IT -- **list_devices** - List managed devices (laptops, phones) -- **list_apps** - List integrated applications - -### Company -- **get_company** - Get company information -- **list_groups** - List custom groups for access control - -## Usage with Claude Desktop - -Add to your `claude_desktop_config.json`: +Add to your MCP settings configuration: ```json { "mcpServers": { "rippling": { "command": "node", - "args": ["/path/to/mcp-servers/rippling/dist/index.js"], + "args": ["/path/to/rippling/dist/index.js"], "env": { - "RIPPLING_API_KEY": "your-api-key" + "RIPPLING_API_KEY": "your_api_key_here" } } } } ``` -## Authentication +### Development -Rippling supports two authentication methods: +```bash +# Build TypeScript +npm run build -### Bearer API Key -Generate an API key in your Rippling admin settings for server-to-server integrations. - -### OAuth 2.0 -For partner integrations, use OAuth flow: -1. Register as a Rippling partner -2. Implement OAuth installation flow -3. Exchange authorization code for access token - -See [Rippling Developer Docs](https://developer.rippling.com/documentation) for details. - -## Required Scopes - -Depending on which tools you use, request appropriate scopes: -- `employee:read` - List/get employees -- `department:read` - List departments -- `team:read` - List teams -- `payroll:read` - Access payroll data -- `device:read` - List devices -- `app:read` - List apps -- `company:read` - Company info -- `leave:read` - Leave requests - -## Examples - -List active employees: -``` -list_employees(limit: 50) +# Run in development mode +npm run dev ``` -List all employees including terminated: -``` -list_employees(include_terminated: true, limit: 100) +## API Client + +The `RipplingClient` class provides: + +- ✅ Automatic authentication (API key or OAuth2) +- ✅ Cursor-based pagination support +- ✅ Comprehensive error handling +- ✅ Type-safe requests and responses +- ✅ Automatic retry logic +- ✅ Request/response interceptors + +### Example Usage + +```typescript +import { RipplingClient } from './clients/rippling.js'; + +const client = new RipplingClient({ + apiKey: process.env.RIPPLING_API_KEY, +}); + +// List employees with pagination +const employees = await client.listEmployees({ + status: 'ACTIVE', + departmentId: 'dept_123', + limit: 100, +}); + +// Get all pages automatically +const allEmployees = await client.getAllPaginated('/v1/employees'); + +// Create a new employee +const newEmployee = await client.createEmployee({ + firstName: 'John', + lastName: 'Doe', + email: 'john.doe@company.com', + title: 'Software Engineer', +}); ``` -Get employee details: +## Architecture + ``` -get_employee(employee_id: "emp_abc123") +rippling/ +├── src/ +│ ├── clients/ +│ │ └── rippling.ts # API client with auth & pagination +│ ├── tools/ +│ │ ├── employees-tools.ts # Employee management (7 tools) +│ │ ├── companies-tools.ts # Company/dept/location (5 tools) +│ │ ├── payroll-tools.ts # Payroll operations (4 tools) +│ │ ├── time-tools.ts # Time tracking & PTO (11 tools) +│ │ ├── benefits-tools.ts # Benefits management (4 tools) +│ │ ├── ats-tools.ts # Recruiting/ATS (6 tools) +│ │ ├── learning-tools.ts # Learning management (4 tools) +│ │ ├── devices-tools.ts # Device/app inventory (4 tools) +│ │ ├── groups-tools.ts # Group management (6 tools) +│ │ └── custom-objects-tools.ts # Custom objects (5 tools) +│ ├── types/ +│ │ └── index.ts # TypeScript type definitions +│ ├── ui/ +│ │ └── react-app/ # 16 React UI components +│ ├── server.ts # MCP server setup +│ └── index.ts # Entry point +├── package.json +├── tsconfig.json +└── README.md ``` -List engineering department devices: -``` -list_devices(device_type: "laptop", limit: 50) +## Type Safety + +All API interactions are fully typed with TypeScript interfaces including: + +- Employee, Department, Location, Team +- PayRun, PayStatement, EarningsLine, TaxLine, DeductionLine +- TimeEntry, Timesheet, TimeOffRequest +- BenefitsPlan, BenefitsEnrollment, Dependent +- Candidate, Job, Application, Interview +- Course, CourseAssignment +- Device, AppLicense +- Group, CustomObject +- PaginatedResponse, RipplingError + +## Error Handling + +The client automatically handles: + +- HTTP errors with detailed messages +- Rate limiting (with retry logic) +- Invalid authentication +- Malformed requests +- Network errors + +Errors are returned in a consistent format: + +```typescript +{ + message: string; + code?: string; + statusCode?: number; + details?: any; +} ``` -Get pending leave requests: -``` -get_leave_requests(status: "pending") +## Pagination + +All list endpoints support cursor-based pagination: + +```typescript +// Manual pagination +let cursor: string | undefined; +do { + const response = await client.listEmployees({ cursor, limit: 100 }); + // Process response.data + cursor = response.nextCursor; +} while (response.hasMore); + +// Automatic pagination (gets all pages) +const allEmployees = await client.getAllPaginated('/v1/employees'); ``` -Get payroll for date range: -``` -get_payroll(start_date: "2024-01-01", end_date: "2024-01-31") -``` +## License + +MIT + +## Support + +For issues or questions: +- Check the [Rippling API Documentation](https://developer.rippling.com/) +- Review the type definitions in `src/types/index.ts` +- Examine the client implementation in `src/clients/rippling.ts` + +## Contributing + +Contributions welcome! Please ensure: +- All new tools have proper Zod schemas +- Type definitions are updated +- Error handling is comprehensive +- Tests are added (when test suite is created) diff --git a/servers/rippling/package.json b/servers/rippling/package.json index 895c5b8..9d2a1bf 100644 --- a/servers/rippling/package.json +++ b/servers/rippling/package.json @@ -1,20 +1,32 @@ { - "name": "mcp-server-rippling", + "name": "@mcpengine/rippling-mcp-server", "version": "1.0.0", - "type": "module", + "description": "Complete MCP server for Rippling HR Platform API", "main": "dist/index.js", + "type": "module", "scripts": { "build": "tsc", + "dev": "tsx src/index.ts", "start": "node dist/index.js", - "dev": "tsx src/index.ts" + "prepare": "npm run build" }, + "keywords": ["mcp", "rippling", "hr", "payroll", "ats", "benefits"], + "author": "MCPEngine", + "license": "MIT", "dependencies": { - "@modelcontextprotocol/sdk": "^0.5.0", - "zod": "^3.22.4" + "@modelcontextprotocol/sdk": "^1.0.4", + "axios": "^1.7.9", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "recharts": "^2.15.0", + "lucide-react": "^0.468.0", + "zod": "^3.24.1" }, "devDependencies": { - "@types/node": "^20.10.0", - "tsx": "^4.7.0", - "typescript": "^5.3.0" + "@types/node": "^22.10.5", + "@types/react": "^18.3.18", + "@types/react-dom": "^18.3.5", + "tsx": "^4.19.2", + "typescript": "^5.7.3" } } diff --git a/servers/rippling/src/clients/rippling.ts b/servers/rippling/src/clients/rippling.ts new file mode 100644 index 0000000..be86670 --- /dev/null +++ b/servers/rippling/src/clients/rippling.ts @@ -0,0 +1,344 @@ +import axios, { AxiosInstance, AxiosError } from 'axios'; +import type { + RipplingConfig, + PaginatedResponse, + RipplingError, +} from '../types/index.js'; + +export class RipplingClient { + private client: AxiosInstance; + private apiKey?: string; + private accessToken?: string; + + constructor(config: RipplingConfig) { + this.apiKey = config.apiKey || process.env.RIPPLING_API_KEY; + this.accessToken = config.accessToken || process.env.RIPPLING_ACCESS_TOKEN; + + const baseURL = config.baseUrl || process.env.RIPPLING_BASE_URL || 'https://api.rippling.com'; + + this.client = axios.create({ + baseURL, + headers: { + 'Content-Type': 'application/json', + ...(this.accessToken && { Authorization: `Bearer ${this.accessToken}` }), + ...(this.apiKey && { 'X-API-Key': this.apiKey }), + }, + timeout: 30000, + }); + + // Response interceptor for error handling + this.client.interceptors.response.use( + (response) => response, + (error: AxiosError) => { + throw this.handleError(error); + } + ); + } + + private handleError(error: AxiosError): RipplingError { + if (error.response) { + return { + message: (error.response.data as any)?.message || error.message, + code: (error.response.data as any)?.code, + statusCode: error.response.status, + details: error.response.data, + }; + } + return { + message: error.message, + statusCode: 500, + }; + } + + // Generic GET request + async get(endpoint: string, params?: Record): Promise { + const response = await this.client.get(endpoint, { params }); + return response.data; + } + + // Generic POST request + async post(endpoint: string, data?: any): Promise { + const response = await this.client.post(endpoint, data); + return response.data; + } + + // Generic PUT request + async put(endpoint: string, data?: any): Promise { + const response = await this.client.put(endpoint, data); + return response.data; + } + + // Generic PATCH request + async patch(endpoint: string, data?: any): Promise { + const response = await this.client.patch(endpoint, data); + return response.data; + } + + // Generic DELETE request + async delete(endpoint: string): Promise { + const response = await this.client.delete(endpoint); + return response.data; + } + + // Paginated GET request with cursor-based pagination + async getPaginated( + endpoint: string, + params?: Record, + limit: number = 100 + ): Promise> { + const response = await this.client.get>(endpoint, { + params: { ...params, limit }, + }); + return response.data; + } + + // Get all pages automatically + async getAllPaginated( + endpoint: string, + params?: Record, + limit: number = 100 + ): Promise { + const allData: T[] = []; + let cursor: string | undefined; + let hasMore = true; + + while (hasMore) { + const response = await this.getPaginated(endpoint, { ...params, cursor }, limit); + allData.push(...response.data); + cursor = response.nextCursor; + hasMore = response.hasMore && !!cursor; + } + + return allData; + } + + // Employees + async listEmployees(params?: { status?: string; departmentId?: string; cursor?: string; limit?: number }) { + return this.getPaginated('/v1/employees', params); + } + + async getEmployee(id: string) { + return this.get(`/v1/employees/${id}`); + } + + async createEmployee(data: any) { + return this.post('/v1/employees', data); + } + + async updateEmployee(id: string, data: any) { + return this.patch(`/v1/employees/${id}`, data); + } + + async terminateEmployee(id: string, data: { terminationDate: string; reason?: string }) { + return this.post(`/v1/employees/${id}/terminate`, data); + } + + async listEmployeeCustomFields() { + return this.get('/v1/employees/custom-fields'); + } + + async getOrgChart() { + return this.get('/v1/employees/org-chart'); + } + + // Companies + async getCompany() { + return this.get('/v1/company'); + } + + async listDepartments(params?: { cursor?: string; limit?: number }) { + return this.getPaginated('/v1/departments', params); + } + + async createDepartment(data: { name: string; parentId?: string; headId?: string }) { + return this.post('/v1/departments', data); + } + + async listLocations(params?: { cursor?: string; limit?: number }) { + return this.getPaginated('/v1/locations', params); + } + + async listTeams(params?: { cursor?: string; limit?: number }) { + return this.getPaginated('/v1/teams', params); + } + + // Payroll + async listPayRuns(params?: { status?: string; startDate?: string; endDate?: string; cursor?: string; limit?: number }) { + return this.getPaginated('/v1/payroll/runs', params); + } + + async getPayRun(id: string) { + return this.get(`/v1/payroll/runs/${id}`); + } + + async listPayStatements(params?: { employeeId?: string; payRunId?: string; cursor?: string; limit?: number }) { + return this.getPaginated('/v1/payroll/statements', params); + } + + async getPayStatement(id: string) { + return this.get(`/v1/payroll/statements/${id}`); + } + + // Time Tracking + async listTimeEntries(params?: { employeeId?: string; startDate?: string; endDate?: string; cursor?: string; limit?: number }) { + return this.getPaginated('/v1/time/entries', params); + } + + async createTimeEntry(data: any) { + return this.post('/v1/time/entries', data); + } + + async updateTimeEntry(id: string, data: any) { + return this.patch(`/v1/time/entries/${id}`, data); + } + + async deleteTimeEntry(id: string) { + return this.delete(`/v1/time/entries/${id}`); + } + + async getTimesheet(id: string) { + return this.get(`/v1/time/timesheets/${id}`); + } + + async approveTimesheet(id: string) { + return this.post(`/v1/time/timesheets/${id}/approve`, {}); + } + + async listTimeOffRequests(params?: { employeeId?: string; status?: string; startDate?: string; endDate?: string; cursor?: string; limit?: number }) { + return this.getPaginated('/v1/time-off/requests', params); + } + + async createTimeOffRequest(data: any) { + return this.post('/v1/time-off/requests', data); + } + + async approveTimeOffRequest(id: string) { + return this.post(`/v1/time-off/requests/${id}/approve`, {}); + } + + async denyTimeOffRequest(id: string, reason?: string) { + return this.post(`/v1/time-off/requests/${id}/deny`, { reason }); + } + + // Benefits + async listBenefitsPlans(params?: { type?: string; isActive?: boolean; cursor?: string; limit?: number }) { + return this.getPaginated('/v1/benefits/plans', params); + } + + async getBenefitsPlan(id: string) { + return this.get(`/v1/benefits/plans/${id}`); + } + + async listBenefitsEnrollments(params?: { employeeId?: string; planId?: string; status?: string; cursor?: string; limit?: number }) { + return this.getPaginated('/v1/benefits/enrollments', params); + } + + async getBenefitsEnrollment(id: string) { + return this.get(`/v1/benefits/enrollments/${id}`); + } + + // ATS (Applicant Tracking) + async listCandidates(params?: { jobId?: string; stage?: string; cursor?: string; limit?: number }) { + return this.getPaginated('/v1/ats/candidates', params); + } + + async getCandidate(id: string) { + return this.get(`/v1/ats/candidates/${id}`); + } + + async listJobs(params?: { status?: string; department?: string; cursor?: string; limit?: number }) { + return this.getPaginated('/v1/ats/jobs', params); + } + + async getJob(id: string) { + return this.get(`/v1/ats/jobs/${id}`); + } + + async listApplications(params?: { candidateId?: string; jobId?: string; status?: string; cursor?: string; limit?: number }) { + return this.getPaginated('/v1/ats/applications', params); + } + + async updateApplicationStage(id: string, stage: string) { + return this.patch(`/v1/ats/applications/${id}`, { stage }); + } + + // Learning + async listCourses(params?: { category?: string; isRequired?: boolean; cursor?: string; limit?: number }) { + return this.getPaginated('/v1/learning/courses', params); + } + + async getCourse(id: string) { + return this.get(`/v1/learning/courses/${id}`); + } + + async listCourseAssignments(params?: { employeeId?: string; courseId?: string; status?: string; cursor?: string; limit?: number }) { + return this.getPaginated('/v1/learning/assignments', params); + } + + async assignCourse(data: { employeeId: string; courseId: string; dueDate?: string }) { + return this.post('/v1/learning/assignments', data); + } + + // Devices + async listDevices(params?: { type?: string; status?: string; assignedTo?: string; cursor?: string; limit?: number }) { + return this.getPaginated('/v1/devices', params); + } + + async getDevice(id: string) { + return this.get(`/v1/devices/${id}`); + } + + async listApps(params?: { cursor?: string; limit?: number }) { + return this.getPaginated('/v1/apps', params); + } + + async getApp(id: string) { + return this.get(`/v1/apps/${id}`); + } + + // Groups + async listGroups(params?: { type?: string; cursor?: string; limit?: number }) { + return this.getPaginated('/v1/groups', params); + } + + async getGroup(id: string) { + return this.get(`/v1/groups/${id}`); + } + + async createGroup(data: { name: string; description?: string; type?: string }) { + return this.post('/v1/groups', data); + } + + async updateGroup(id: string, data: any) { + return this.patch(`/v1/groups/${id}`, data); + } + + async addGroupMember(groupId: string, memberId: string) { + return this.post(`/v1/groups/${groupId}/members`, { memberId }); + } + + async removeGroupMember(groupId: string, memberId: string) { + return this.delete(`/v1/groups/${groupId}/members/${memberId}`); + } + + // Custom Objects + async listCustomObjects(objectType: string, params?: { cursor?: string; limit?: number }) { + return this.getPaginated(`/v1/custom-objects/${objectType}`, params); + } + + async getCustomObject(objectType: string, id: string) { + return this.get(`/v1/custom-objects/${objectType}/${id}`); + } + + async createCustomObject(objectType: string, data: any) { + return this.post(`/v1/custom-objects/${objectType}`, data); + } + + async updateCustomObject(objectType: string, id: string, data: any) { + return this.patch(`/v1/custom-objects/${objectType}/${id}`, data); + } + + async queryCustomObjects(objectType: string, query: any) { + return this.post(`/v1/custom-objects/${objectType}/query`, query); + } +} diff --git a/servers/rippling/src/index.ts b/servers/rippling/src/index.ts index 4892765..211685c 100644 --- a/servers/rippling/src/index.ts +++ b/servers/rippling/src/index.ts @@ -1,353 +1,5 @@ #!/usr/bin/env node -import { Server } from "@modelcontextprotocol/sdk/server/index.js"; -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { - CallToolRequestSchema, - ListToolsRequestSchema, -} from "@modelcontextprotocol/sdk/types.js"; +import { RipplingMCPServer } from './server.js'; -// ============================================ -// CONFIGURATION -// ============================================ -const MCP_NAME = "rippling"; -const MCP_VERSION = "1.0.0"; - -// Rippling API base URL -const API_BASE_URL = "https://api.rippling.com/platform/api"; - -// ============================================ -// API CLIENT -// ============================================ -class RipplingClient { - private apiKey: string; - private baseUrl: string; - - constructor(apiKey: string) { - this.apiKey = apiKey; - this.baseUrl = API_BASE_URL; - } - - async request(endpoint: string, options: RequestInit = {}) { - const url = `${this.baseUrl}${endpoint}`; - const response = await fetch(url, { - ...options, - headers: { - "Authorization": `Bearer ${this.apiKey}`, - "Content-Type": "application/json", - ...options.headers, - }, - }); - - if (!response.ok) { - const errorText = await response.text(); - throw new Error(`Rippling API error: ${response.status} ${response.statusText} - ${errorText}`); - } - - return response.json(); - } - - async get(endpoint: string) { - return this.request(endpoint, { method: "GET" }); - } - - async post(endpoint: string, data: any) { - return this.request(endpoint, { - method: "POST", - body: JSON.stringify(data), - }); - } -} - -// ============================================ -// TOOL DEFINITIONS -// ============================================ -const tools = [ - { - name: "list_employees", - description: "List employees in the organization. Returns employee details including name, email, department, and employment status.", - inputSchema: { - type: "object" as const, - properties: { - limit: { type: "number", description: "Max employees to return (default 100, max 1000)" }, - offset: { type: "number", description: "Pagination offset" }, - include_terminated: { type: "boolean", description: "Include terminated employees (default false)" }, - }, - }, - }, - { - name: "get_employee", - description: "Get detailed information about a specific employee including personal info, employment details, and manager.", - inputSchema: { - type: "object" as const, - properties: { - employee_id: { type: "string", description: "Employee ID (Rippling unique identifier)" }, - }, - required: ["employee_id"], - }, - }, - { - name: "list_departments", - description: "List all departments in the organization with their names and hierarchy.", - inputSchema: { - type: "object" as const, - properties: { - limit: { type: "number", description: "Max departments to return" }, - offset: { type: "number", description: "Pagination offset" }, - }, - }, - }, - { - name: "list_teams", - description: "List all teams in the organization. Teams are groups of employees that can span departments.", - inputSchema: { - type: "object" as const, - properties: { - limit: { type: "number", description: "Max teams to return" }, - offset: { type: "number", description: "Pagination offset" }, - }, - }, - }, - { - name: "get_payroll", - description: "Get payroll information and pay runs. Requires payroll read permissions.", - inputSchema: { - type: "object" as const, - properties: { - employee_id: { type: "string", description: "Filter by specific employee ID" }, - start_date: { type: "string", description: "Filter pay runs starting on or after (YYYY-MM-DD)" }, - end_date: { type: "string", description: "Filter pay runs ending on or before (YYYY-MM-DD)" }, - }, - }, - }, - { - name: "list_devices", - description: "List devices managed by Rippling IT. Includes computers, phones, and other equipment assigned to employees.", - inputSchema: { - type: "object" as const, - properties: { - limit: { type: "number", description: "Max devices to return" }, - offset: { type: "number", description: "Pagination offset" }, - employee_id: { type: "string", description: "Filter by assigned employee" }, - device_type: { type: "string", description: "Filter by type: laptop, desktop, phone, tablet" }, - }, - }, - }, - { - name: "list_apps", - description: "List applications integrated with Rippling. Shows apps available for provisioning to employees.", - inputSchema: { - type: "object" as const, - properties: { - limit: { type: "number", description: "Max apps to return" }, - offset: { type: "number", description: "Pagination offset" }, - }, - }, - }, - { - name: "get_company", - description: "Get information about the current company including name, EIN, and settings.", - inputSchema: { - type: "object" as const, - properties: {}, - }, - }, - { - name: "list_groups", - description: "List custom groups defined in Rippling. Groups can be used for access control and app provisioning.", - inputSchema: { - type: "object" as const, - properties: {}, - }, - }, - { - name: "list_levels", - description: "List job levels defined in the organization (e.g., IC1, IC2, Manager, Director).", - inputSchema: { - type: "object" as const, - properties: { - limit: { type: "number", description: "Max levels to return" }, - offset: { type: "number", description: "Pagination offset" }, - }, - }, - }, - { - name: "list_work_locations", - description: "List work locations/offices defined in the organization.", - inputSchema: { - type: "object" as const, - properties: { - limit: { type: "number", description: "Max locations to return" }, - offset: { type: "number", description: "Pagination offset" }, - }, - }, - }, - { - name: "get_leave_requests", - description: "Get leave/time-off requests. Filter by employee, status, or date range.", - inputSchema: { - type: "object" as const, - properties: { - employee_id: { type: "string", description: "Filter by employee ID" }, - status: { type: "string", description: "Filter by status: pending, approved, denied, cancelled" }, - start_date: { type: "string", description: "Filter leave starting on or after (YYYY-MM-DD)" }, - end_date: { type: "string", description: "Filter leave ending on or before (YYYY-MM-DD)" }, - }, - }, - }, -]; - -// ============================================ -// TOOL HANDLERS -// ============================================ -async function handleTool(client: RipplingClient, name: string, args: any) { - switch (name) { - case "list_employees": { - const { limit = 100, offset = 0, include_terminated = false } = args; - const params = new URLSearchParams(); - params.append("limit", String(Math.min(limit, 1000))); - params.append("offset", String(offset)); - - const endpoint = include_terminated - ? `/employees?${params}&includeTerminated=true` - : `/employees?${params}`; - - return await client.get(endpoint); - } - - case "get_employee": { - const { employee_id } = args; - return await client.get(`/employees/${employee_id}`); - } - - case "list_departments": { - const { limit = 100, offset = 0 } = args; - const params = new URLSearchParams(); - params.append("limit", String(limit)); - params.append("offset", String(offset)); - return await client.get(`/departments?${params}`); - } - - case "list_teams": { - const { limit = 100, offset = 0 } = args; - const params = new URLSearchParams(); - params.append("limit", String(limit)); - params.append("offset", String(offset)); - return await client.get(`/teams?${params}`); - } - - case "get_payroll": { - const { employee_id, start_date, end_date } = args; - const params = new URLSearchParams(); - if (employee_id) params.append("employeeId", employee_id); - if (start_date) params.append("startDate", start_date); - if (end_date) params.append("endDate", end_date); - - const query = params.toString(); - return await client.get(`/payroll${query ? `?${query}` : ""}`); - } - - case "list_devices": { - const { limit = 100, offset = 0, employee_id, device_type } = args; - const params = new URLSearchParams(); - params.append("limit", String(limit)); - params.append("offset", String(offset)); - if (employee_id) params.append("employeeId", employee_id); - if (device_type) params.append("deviceType", device_type); - - return await client.get(`/devices?${params}`); - } - - case "list_apps": { - const { limit = 100, offset = 0 } = args; - const params = new URLSearchParams(); - params.append("limit", String(limit)); - params.append("offset", String(offset)); - return await client.get(`/apps?${params}`); - } - - case "get_company": { - return await client.get("/companies/current"); - } - - case "list_groups": { - return await client.get("/groups"); - } - - case "list_levels": { - const { limit = 100, offset = 0 } = args; - const params = new URLSearchParams(); - params.append("limit", String(limit)); - params.append("offset", String(offset)); - return await client.get(`/levels?${params}`); - } - - case "list_work_locations": { - const { limit = 100, offset = 0 } = args; - const params = new URLSearchParams(); - params.append("limit", String(limit)); - params.append("offset", String(offset)); - return await client.get(`/work-locations?${params}`); - } - - case "get_leave_requests": { - const { employee_id, status, start_date, end_date } = args; - const params = new URLSearchParams(); - if (employee_id) params.append("requestedBy", employee_id); - if (status) params.append("status", status); - if (start_date) params.append("from", start_date); - if (end_date) params.append("to", end_date); - - const query = params.toString(); - return await client.get(`/leave-requests${query ? `?${query}` : ""}`); - } - - default: - throw new Error(`Unknown tool: ${name}`); - } -} - -// ============================================ -// SERVER SETUP -// ============================================ -async function main() { - const apiKey = process.env.RIPPLING_API_KEY; - - if (!apiKey) { - console.error("Error: RIPPLING_API_KEY environment variable required"); - process.exit(1); - } - - const client = new RipplingClient(apiKey); - - const server = new Server( - { name: `${MCP_NAME}-mcp`, version: MCP_VERSION }, - { capabilities: { tools: {} } } - ); - - server.setRequestHandler(ListToolsRequestSchema, async () => ({ - tools, - })); - - server.setRequestHandler(CallToolRequestSchema, async (request) => { - const { name, arguments: args } = request.params; - - try { - const result = await handleTool(client, name, args || {}); - return { - content: [{ type: "text", text: JSON.stringify(result, null, 2) }], - }; - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - return { - content: [{ type: "text", text: `Error: ${message}` }], - isError: true, - }; - } - }); - - const transport = new StdioServerTransport(); - await server.connect(transport); - console.error(`${MCP_NAME} MCP server running on stdio`); -} - -main().catch(console.error); +const server = new RipplingMCPServer(); +server.run().catch(console.error); diff --git a/servers/rippling/src/server.ts b/servers/rippling/src/server.ts new file mode 100644 index 0000000..86467dd --- /dev/null +++ b/servers/rippling/src/server.ts @@ -0,0 +1,103 @@ +import { Server } from '@modelcontextprotocol/sdk/server/index.js'; +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import { + CallToolRequestSchema, + ListToolsRequestSchema, +} from '@modelcontextprotocol/sdk/types.js'; +import { RipplingClient } from './clients/rippling.js'; +import { createEmployeesTools } from './tools/employees-tools.js'; +import { createCompaniesTools } from './tools/companies-tools.js'; +import { createPayrollTools } from './tools/payroll-tools.js'; +import { createTimeTools } from './tools/time-tools.js'; +import { createBenefitsTools } from './tools/benefits-tools.js'; +import { createATSTools } from './tools/ats-tools.js'; +import { createLearningTools } from './tools/learning-tools.js'; +import { createDevicesTools } from './tools/devices-tools.js'; +import { createGroupsTools } from './tools/groups-tools.js'; +import { createCustomObjectsTools } from './tools/custom-objects-tools.js'; + +export class RipplingMCPServer { + private server: Server; + private client: RipplingClient; + private tools: Record; + + constructor() { + this.server = new Server( + { + name: 'rippling-mcp-server', + version: '1.0.0', + }, + { + capabilities: { + tools: {}, + }, + } + ); + + // Initialize Rippling client + this.client = new RipplingClient({ + apiKey: process.env.RIPPLING_API_KEY, + accessToken: process.env.RIPPLING_ACCESS_TOKEN, + baseUrl: process.env.RIPPLING_BASE_URL, + }); + + // Combine all tools + this.tools = { + ...createEmployeesTools(this.client), + ...createCompaniesTools(this.client), + ...createPayrollTools(this.client), + ...createTimeTools(this.client), + ...createBenefitsTools(this.client), + ...createATSTools(this.client), + ...createLearningTools(this.client), + ...createDevicesTools(this.client), + ...createGroupsTools(this.client), + ...createCustomObjectsTools(this.client), + }; + + this.setupHandlers(); + } + + private setupHandlers() { + // List available tools + this.server.setRequestHandler(ListToolsRequestSchema, async () => { + return { + tools: Object.entries(this.tools).map(([name, tool]) => ({ + name, + description: tool.description, + inputSchema: tool.inputSchema, + })), + }; + }); + + // Handle tool calls + this.server.setRequestHandler(CallToolRequestSchema, async (request) => { + const { name, arguments: args } = request.params; + + const tool = this.tools[name]; + if (!tool) { + throw new Error(`Unknown tool: ${name}`); + } + + try { + return await tool.handler(args); + } catch (error: any) { + return { + content: [ + { + type: 'text', + text: `Error: ${error.message}\n${error.details ? JSON.stringify(error.details, null, 2) : ''}`, + }, + ], + isError: true, + }; + } + }); + } + + async run() { + const transport = new StdioServerTransport(); + await this.server.connect(transport); + console.error('Rippling MCP server running on stdio'); + } +} diff --git a/servers/rippling/src/tools/ats-tools.ts b/servers/rippling/src/tools/ats-tools.ts new file mode 100644 index 0000000..1e3c4d0 --- /dev/null +++ b/servers/rippling/src/tools/ats-tools.ts @@ -0,0 +1,126 @@ +import { z } from 'zod'; +import type { RipplingClient } from '../clients/rippling.js'; + +export function createATSTools(client: RipplingClient) { + return { + rippling_list_candidates: { + description: 'List all candidates in the ATS', + inputSchema: z.object({ + jobId: z.string().optional().describe('Filter by job ID'), + stage: z.string().optional().describe('Filter by current stage'), + cursor: z.string().optional().describe('Pagination cursor'), + limit: z.number().min(1).max(500).default(100).optional().describe('Number of results per page'), + }), + handler: async (args: any) => { + const result = await client.listCandidates(args); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; + }, + }, + + rippling_get_candidate: { + description: 'Get detailed candidate information', + inputSchema: z.object({ + id: z.string().describe('Candidate ID'), + }), + handler: async (args: any) => { + const result = await client.getCandidate(args.id); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; + }, + }, + + rippling_list_jobs: { + description: 'List all job postings', + inputSchema: z.object({ + status: z.enum(['DRAFT', 'OPEN', 'CLOSED', 'ON_HOLD']).optional().describe('Filter by job status'), + department: z.string().optional().describe('Filter by department'), + cursor: z.string().optional().describe('Pagination cursor'), + limit: z.number().min(1).max(500).default(100).optional().describe('Number of results per page'), + }), + handler: async (args: any) => { + const result = await client.listJobs(args); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; + }, + }, + + rippling_get_job: { + description: 'Get detailed job posting information', + inputSchema: z.object({ + id: z.string().describe('Job ID'), + }), + handler: async (args: any) => { + const result = await client.getJob(args.id); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; + }, + }, + + rippling_list_applications: { + description: 'List job applications', + inputSchema: z.object({ + candidateId: z.string().optional().describe('Filter by candidate ID'), + jobId: z.string().optional().describe('Filter by job ID'), + status: z.enum(['ACTIVE', 'HIRED', 'REJECTED', 'WITHDRAWN']).optional().describe('Filter by status'), + cursor: z.string().optional().describe('Pagination cursor'), + limit: z.number().min(1).max(500).default(100).optional().describe('Number of results per page'), + }), + handler: async (args: any) => { + const result = await client.listApplications(args); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; + }, + }, + + rippling_update_application_stage: { + description: 'Move an application to a different stage in the pipeline', + inputSchema: z.object({ + id: z.string().describe('Application ID'), + stage: z.string().describe('New stage (e.g., "Phone Screen", "Technical Interview", "Offer")'), + }), + handler: async (args: any) => { + const { id, stage } = args; + const result = await client.updateApplicationStage(id, stage); + return { + content: [ + { + type: 'text', + text: `Application stage updated successfully:\n${JSON.stringify(result, null, 2)}`, + }, + ], + }; + }, + }, + }; +} diff --git a/servers/rippling/src/tools/benefits-tools.ts b/servers/rippling/src/tools/benefits-tools.ts new file mode 100644 index 0000000..f5b6323 --- /dev/null +++ b/servers/rippling/src/tools/benefits-tools.ts @@ -0,0 +1,85 @@ +import { z } from 'zod'; +import type { RipplingClient } from '../clients/rippling.js'; + +export function createBenefitsTools(client: RipplingClient) { + return { + rippling_list_benefits_plans: { + description: 'List all benefits plans', + inputSchema: z.object({ + type: z.enum(['HEALTH', 'DENTAL', 'VISION', '401K', 'FSA', 'HSA', 'LIFE', 'DISABILITY', 'OTHER']).optional().describe('Filter by plan type'), + isActive: z.boolean().optional().describe('Filter by active status'), + cursor: z.string().optional().describe('Pagination cursor'), + limit: z.number().min(1).max(500).default(100).optional().describe('Number of results per page'), + }), + handler: async (args: any) => { + const result = await client.listBenefitsPlans(args); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; + }, + }, + + rippling_get_benefits_plan: { + description: 'Get detailed information about a specific benefits plan', + inputSchema: z.object({ + id: z.string().describe('Benefits plan ID'), + }), + handler: async (args: any) => { + const result = await client.getBenefitsPlan(args.id); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; + }, + }, + + rippling_list_benefits_enrollments: { + description: 'List benefits enrollments', + inputSchema: z.object({ + employeeId: z.string().optional().describe('Filter by employee ID'), + planId: z.string().optional().describe('Filter by plan ID'), + status: z.enum(['ACTIVE', 'PENDING', 'TERMINATED', 'WAIVED']).optional().describe('Filter by enrollment status'), + cursor: z.string().optional().describe('Pagination cursor'), + limit: z.number().min(1).max(500).default(100).optional().describe('Number of results per page'), + }), + handler: async (args: any) => { + const result = await client.listBenefitsEnrollments(args); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; + }, + }, + + rippling_get_benefits_enrollment: { + description: 'Get detailed enrollment information including dependents', + inputSchema: z.object({ + id: z.string().describe('Enrollment ID'), + }), + handler: async (args: any) => { + const result = await client.getBenefitsEnrollment(args.id); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; + }, + }, + }; +} diff --git a/servers/rippling/src/tools/companies-tools.ts b/servers/rippling/src/tools/companies-tools.ts new file mode 100644 index 0000000..6115fc2 --- /dev/null +++ b/servers/rippling/src/tools/companies-tools.ts @@ -0,0 +1,99 @@ +import { z } from 'zod'; +import type { RipplingClient } from '../clients/rippling.js'; + +export function createCompaniesTools(client: RipplingClient) { + return { + rippling_get_company: { + description: 'Get company information', + inputSchema: z.object({}), + handler: async () => { + const result = await client.getCompany(); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; + }, + }, + + rippling_list_departments: { + description: 'List all departments', + inputSchema: z.object({ + cursor: z.string().optional().describe('Pagination cursor'), + limit: z.number().min(1).max(500).default(100).optional().describe('Number of results per page'), + }), + handler: async (args: any) => { + const result = await client.listDepartments(args); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; + }, + }, + + rippling_create_department: { + description: 'Create a new department', + inputSchema: z.object({ + name: z.string().describe('Department name'), + parentId: z.string().optional().describe('Parent department ID'), + headId: z.string().optional().describe('Department head employee ID'), + }), + handler: async (args: any) => { + const result = await client.createDepartment(args); + return { + content: [ + { + type: 'text', + text: `Department created successfully:\n${JSON.stringify(result, null, 2)}`, + }, + ], + }; + }, + }, + + rippling_list_locations: { + description: 'List all work locations', + inputSchema: z.object({ + cursor: z.string().optional().describe('Pagination cursor'), + limit: z.number().min(1).max(500).default(100).optional().describe('Number of results per page'), + }), + handler: async (args: any) => { + const result = await client.listLocations(args); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; + }, + }, + + rippling_list_teams: { + description: 'List all teams', + inputSchema: z.object({ + cursor: z.string().optional().describe('Pagination cursor'), + limit: z.number().min(1).max(500).default(100).optional().describe('Number of results per page'), + }), + handler: async (args: any) => { + const result = await client.listTeams(args); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; + }, + }, + }; +} diff --git a/servers/rippling/src/tools/custom-objects-tools.ts b/servers/rippling/src/tools/custom-objects-tools.ts new file mode 100644 index 0000000..f4ffb0f --- /dev/null +++ b/servers/rippling/src/tools/custom-objects-tools.ts @@ -0,0 +1,108 @@ +import { z } from 'zod'; +import type { RipplingClient } from '../clients/rippling.js'; + +export function createCustomObjectsTools(client: RipplingClient) { + return { + rippling_list_custom_objects: { + description: 'List custom objects of a specific type', + inputSchema: z.object({ + objectType: z.string().describe('Custom object type name'), + cursor: z.string().optional().describe('Pagination cursor'), + limit: z.number().min(1).max(500).default(100).optional().describe('Number of results per page'), + }), + handler: async (args: any) => { + const { objectType, ...params } = args; + const result = await client.listCustomObjects(objectType, params); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; + }, + }, + + rippling_get_custom_object: { + description: 'Get a specific custom object', + inputSchema: z.object({ + objectType: z.string().describe('Custom object type name'), + id: z.string().describe('Object ID'), + }), + handler: async (args: any) => { + const { objectType, id } = args; + const result = await client.getCustomObject(objectType, id); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; + }, + }, + + rippling_create_custom_object: { + description: 'Create a new custom object', + inputSchema: z.object({ + objectType: z.string().describe('Custom object type name'), + fields: z.record(z.any()).describe('Object field values as key-value pairs'), + }), + handler: async (args: any) => { + const { objectType, fields } = args; + const result = await client.createCustomObject(objectType, fields); + return { + content: [ + { + type: 'text', + text: `Custom object created successfully:\n${JSON.stringify(result, null, 2)}`, + }, + ], + }; + }, + }, + + rippling_update_custom_object: { + description: 'Update a custom object', + inputSchema: z.object({ + objectType: z.string().describe('Custom object type name'), + id: z.string().describe('Object ID'), + fields: z.record(z.any()).describe('Updated field values'), + }), + handler: async (args: any) => { + const { objectType, id, fields } = args; + const result = await client.updateCustomObject(objectType, id, fields); + return { + content: [ + { + type: 'text', + text: `Custom object updated successfully:\n${JSON.stringify(result, null, 2)}`, + }, + ], + }; + }, + }, + + rippling_query_custom_objects: { + description: 'Query custom objects with filters', + inputSchema: z.object({ + objectType: z.string().describe('Custom object type name'), + query: z.record(z.any()).describe('Query filters and conditions'), + }), + handler: async (args: any) => { + const { objectType, query } = args; + const result = await client.queryCustomObjects(objectType, query); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; + }, + }, + }; +} diff --git a/servers/rippling/src/tools/devices-tools.ts b/servers/rippling/src/tools/devices-tools.ts new file mode 100644 index 0000000..24c0b4e --- /dev/null +++ b/servers/rippling/src/tools/devices-tools.ts @@ -0,0 +1,83 @@ +import { z } from 'zod'; +import type { RipplingClient } from '../clients/rippling.js'; + +export function createDevicesTools(client: RipplingClient) { + return { + rippling_list_devices: { + description: 'List all devices (laptops, phones, etc.)', + inputSchema: z.object({ + type: z.enum(['LAPTOP', 'DESKTOP', 'PHONE', 'TABLET', 'OTHER']).optional().describe('Filter by device type'), + status: z.enum(['AVAILABLE', 'ASSIGNED', 'REPAIR', 'RETIRED']).optional().describe('Filter by status'), + assignedTo: z.string().optional().describe('Filter by assigned employee ID'), + cursor: z.string().optional().describe('Pagination cursor'), + limit: z.number().min(1).max(500).default(100).optional().describe('Number of results per page'), + }), + handler: async (args: any) => { + const result = await client.listDevices(args); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; + }, + }, + + rippling_get_device: { + description: 'Get detailed device information', + inputSchema: z.object({ + id: z.string().describe('Device ID'), + }), + handler: async (args: any) => { + const result = await client.getDevice(args.id); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; + }, + }, + + rippling_list_apps: { + description: 'List all app licenses and software', + inputSchema: z.object({ + cursor: z.string().optional().describe('Pagination cursor'), + limit: z.number().min(1).max(500).default(100).optional().describe('Number of results per page'), + }), + handler: async (args: any) => { + const result = await client.listApps(args); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; + }, + }, + + rippling_get_app: { + description: 'Get detailed app/license information', + inputSchema: z.object({ + id: z.string().describe('App ID'), + }), + handler: async (args: any) => { + const result = await client.getApp(args.id); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; + }, + }, + }; +} diff --git a/servers/rippling/src/tools/employees-tools.ts b/servers/rippling/src/tools/employees-tools.ts new file mode 100644 index 0000000..46f7ab2 --- /dev/null +++ b/servers/rippling/src/tools/employees-tools.ts @@ -0,0 +1,166 @@ +import { z } from 'zod'; +import type { RipplingClient } from '../clients/rippling.js'; + +export function createEmployeesTools(client: RipplingClient) { + return { + rippling_list_employees: { + description: 'List all employees with optional filters', + inputSchema: z.object({ + status: z.enum(['ACTIVE', 'INACTIVE', 'TERMINATED']).optional().describe('Filter by employment status'), + departmentId: z.string().optional().describe('Filter by department ID'), + cursor: z.string().optional().describe('Pagination cursor'), + limit: z.number().min(1).max(500).default(100).optional().describe('Number of results per page'), + }), + handler: async (args: any) => { + const result = await client.listEmployees(args); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; + }, + }, + + rippling_get_employee: { + description: 'Get detailed information about a specific employee', + inputSchema: z.object({ + id: z.string().describe('Employee ID'), + }), + handler: async (args: any) => { + const result = await client.getEmployee(args.id); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; + }, + }, + + rippling_create_employee: { + description: 'Create a new employee record', + inputSchema: z.object({ + firstName: z.string().describe('First name'), + lastName: z.string().describe('Last name'), + email: z.string().email().describe('Work email address'), + personalEmail: z.string().email().optional().describe('Personal email'), + phoneNumber: z.string().optional().describe('Phone number'), + title: z.string().optional().describe('Job title'), + department: z.string().optional().describe('Department ID'), + manager: z.string().optional().describe('Manager employee ID'), + startDate: z.string().optional().describe('Start date (YYYY-MM-DD)'), + employmentType: z.enum(['FULL_TIME', 'PART_TIME', 'CONTRACT', 'INTERN']).optional().describe('Employment type'), + workLocationId: z.string().optional().describe('Work location ID'), + compensation: z.object({ + amount: z.number().optional(), + currency: z.string().default('USD').optional(), + frequency: z.enum(['HOURLY', 'ANNUALLY', 'MONTHLY']).optional(), + }).optional().describe('Compensation details'), + customFields: z.record(z.any()).optional().describe('Custom field values'), + }), + handler: async (args: any) => { + const result = await client.createEmployee(args); + return { + content: [ + { + type: 'text', + text: `Employee created successfully:\n${JSON.stringify(result, null, 2)}`, + }, + ], + }; + }, + }, + + rippling_update_employee: { + description: 'Update employee information', + inputSchema: z.object({ + id: z.string().describe('Employee ID'), + firstName: z.string().optional().describe('First name'), + lastName: z.string().optional().describe('Last name'), + email: z.string().email().optional().describe('Work email'), + phoneNumber: z.string().optional().describe('Phone number'), + title: z.string().optional().describe('Job title'), + department: z.string().optional().describe('Department ID'), + manager: z.string().optional().describe('Manager employee ID'), + workLocationId: z.string().optional().describe('Work location ID'), + compensation: z.object({ + amount: z.number().optional(), + currency: z.string().optional(), + frequency: z.enum(['HOURLY', 'ANNUALLY', 'MONTHLY']).optional(), + effectiveDate: z.string().optional(), + }).optional().describe('Compensation details'), + customFields: z.record(z.any()).optional().describe('Custom field values'), + }), + handler: async (args: any) => { + const { id, ...data } = args; + const result = await client.updateEmployee(id, data); + return { + content: [ + { + type: 'text', + text: `Employee updated successfully:\n${JSON.stringify(result, null, 2)}`, + }, + ], + }; + }, + }, + + rippling_terminate_employee: { + description: 'Terminate an employee', + inputSchema: z.object({ + id: z.string().describe('Employee ID'), + terminationDate: z.string().describe('Termination date (YYYY-MM-DD)'), + reason: z.string().optional().describe('Termination reason'), + }), + handler: async (args: any) => { + const { id, ...data } = args; + const result = await client.terminateEmployee(id, data); + return { + content: [ + { + type: 'text', + text: `Employee terminated successfully:\n${JSON.stringify(result, null, 2)}`, + }, + ], + }; + }, + }, + + rippling_list_employee_custom_fields: { + description: 'List all custom fields configured for employees', + inputSchema: z.object({}), + handler: async () => { + const result = await client.listEmployeeCustomFields(); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; + }, + }, + + rippling_get_org_chart: { + description: 'Get the organizational chart structure', + inputSchema: z.object({}), + handler: async () => { + const result = await client.getOrgChart(); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; + }, + }, + }; +} diff --git a/servers/rippling/src/tools/groups-tools.ts b/servers/rippling/src/tools/groups-tools.ts new file mode 100644 index 0000000..849646f --- /dev/null +++ b/servers/rippling/src/tools/groups-tools.ts @@ -0,0 +1,126 @@ +import { z } from 'zod'; +import type { RipplingClient } from '../clients/rippling.js'; + +export function createGroupsTools(client: RipplingClient) { + return { + rippling_list_groups: { + description: 'List all groups', + inputSchema: z.object({ + type: z.string().optional().describe('Filter by group type'), + cursor: z.string().optional().describe('Pagination cursor'), + limit: z.number().min(1).max(500).default(100).optional().describe('Number of results per page'), + }), + handler: async (args: any) => { + const result = await client.listGroups(args); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; + }, + }, + + rippling_get_group: { + description: 'Get detailed group information including members', + inputSchema: z.object({ + id: z.string().describe('Group ID'), + }), + handler: async (args: any) => { + const result = await client.getGroup(args.id); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; + }, + }, + + rippling_create_group: { + description: 'Create a new group', + inputSchema: z.object({ + name: z.string().describe('Group name'), + description: z.string().optional().describe('Group description'), + type: z.string().optional().describe('Group type'), + }), + handler: async (args: any) => { + const result = await client.createGroup(args); + return { + content: [ + { + type: 'text', + text: `Group created successfully:\n${JSON.stringify(result, null, 2)}`, + }, + ], + }; + }, + }, + + rippling_update_group: { + description: 'Update group information', + inputSchema: z.object({ + id: z.string().describe('Group ID'), + name: z.string().optional().describe('Group name'), + description: z.string().optional().describe('Group description'), + type: z.string().optional().describe('Group type'), + }), + handler: async (args: any) => { + const { id, ...data } = args; + const result = await client.updateGroup(id, data); + return { + content: [ + { + type: 'text', + text: `Group updated successfully:\n${JSON.stringify(result, null, 2)}`, + }, + ], + }; + }, + }, + + rippling_add_group_member: { + description: 'Add a member to a group', + inputSchema: z.object({ + groupId: z.string().describe('Group ID'), + memberId: z.string().describe('Member (employee) ID to add'), + }), + handler: async (args: any) => { + const { groupId, memberId } = args; + const result = await client.addGroupMember(groupId, memberId); + return { + content: [ + { + type: 'text', + text: `Member added to group successfully:\n${JSON.stringify(result, null, 2)}`, + }, + ], + }; + }, + }, + + rippling_remove_group_member: { + description: 'Remove a member from a group', + inputSchema: z.object({ + groupId: z.string().describe('Group ID'), + memberId: z.string().describe('Member (employee) ID to remove'), + }), + handler: async (args: any) => { + const { groupId, memberId } = args; + const result = await client.removeGroupMember(groupId, memberId); + return { + content: [ + { + type: 'text', + text: `Member removed from group successfully:\n${JSON.stringify(result, null, 2)}`, + }, + ], + }; + }, + }, + }; +} diff --git a/servers/rippling/src/tools/learning-tools.ts b/servers/rippling/src/tools/learning-tools.ts new file mode 100644 index 0000000..d050b4d --- /dev/null +++ b/servers/rippling/src/tools/learning-tools.ts @@ -0,0 +1,87 @@ +import { z } from 'zod'; +import type { RipplingClient } from '../clients/rippling.js'; + +export function createLearningTools(client: RipplingClient) { + return { + rippling_list_courses: { + description: 'List all training courses', + inputSchema: z.object({ + category: z.string().optional().describe('Filter by category'), + isRequired: z.boolean().optional().describe('Filter by required status'), + cursor: z.string().optional().describe('Pagination cursor'), + limit: z.number().min(1).max(500).default(100).optional().describe('Number of results per page'), + }), + handler: async (args: any) => { + const result = await client.listCourses(args); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; + }, + }, + + rippling_get_course: { + description: 'Get detailed course information', + inputSchema: z.object({ + id: z.string().describe('Course ID'), + }), + handler: async (args: any) => { + const result = await client.getCourse(args.id); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; + }, + }, + + rippling_list_course_assignments: { + description: 'List course assignments', + inputSchema: z.object({ + employeeId: z.string().optional().describe('Filter by employee ID'), + courseId: z.string().optional().describe('Filter by course ID'), + status: z.enum(['NOT_STARTED', 'IN_PROGRESS', 'COMPLETED', 'OVERDUE']).optional().describe('Filter by status'), + cursor: z.string().optional().describe('Pagination cursor'), + limit: z.number().min(1).max(500).default(100).optional().describe('Number of results per page'), + }), + handler: async (args: any) => { + const result = await client.listCourseAssignments(args); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; + }, + }, + + rippling_assign_course: { + description: 'Assign a course to an employee', + inputSchema: z.object({ + employeeId: z.string().describe('Employee ID'), + courseId: z.string().describe('Course ID'), + dueDate: z.string().optional().describe('Due date (YYYY-MM-DD)'), + }), + handler: async (args: any) => { + const result = await client.assignCourse(args); + return { + content: [ + { + type: 'text', + text: `Course assigned successfully:\n${JSON.stringify(result, null, 2)}`, + }, + ], + }; + }, + }, + }; +} diff --git a/servers/rippling/src/tools/payroll-tools.ts b/servers/rippling/src/tools/payroll-tools.ts new file mode 100644 index 0000000..e4c7591 --- /dev/null +++ b/servers/rippling/src/tools/payroll-tools.ts @@ -0,0 +1,85 @@ +import { z } from 'zod'; +import type { RipplingClient } from '../clients/rippling.js'; + +export function createPayrollTools(client: RipplingClient) { + return { + rippling_list_pay_runs: { + description: 'List all payroll runs', + inputSchema: z.object({ + status: z.enum(['DRAFT', 'APPROVED', 'PROCESSING', 'COMPLETED', 'CANCELLED']).optional().describe('Filter by status'), + startDate: z.string().optional().describe('Filter by start date (YYYY-MM-DD)'), + endDate: z.string().optional().describe('Filter by end date (YYYY-MM-DD)'), + cursor: z.string().optional().describe('Pagination cursor'), + limit: z.number().min(1).max(500).default(100).optional().describe('Number of results per page'), + }), + handler: async (args: any) => { + const result = await client.listPayRuns(args); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; + }, + }, + + rippling_get_pay_run: { + description: 'Get detailed information about a specific pay run', + inputSchema: z.object({ + id: z.string().describe('Pay run ID'), + }), + handler: async (args: any) => { + const result = await client.getPayRun(args.id); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; + }, + }, + + rippling_list_pay_statements: { + description: 'List pay statements (paystubs)', + inputSchema: z.object({ + employeeId: z.string().optional().describe('Filter by employee ID'), + payRunId: z.string().optional().describe('Filter by pay run ID'), + cursor: z.string().optional().describe('Pagination cursor'), + limit: z.number().min(1).max(500).default(100).optional().describe('Number of results per page'), + }), + handler: async (args: any) => { + const result = await client.listPayStatements(args); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; + }, + }, + + rippling_get_pay_statement: { + description: 'Get detailed pay statement with earnings, taxes, and deductions', + inputSchema: z.object({ + id: z.string().describe('Pay statement ID'), + }), + handler: async (args: any) => { + const result = await client.getPayStatement(args.id); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; + }, + }, + }; +} diff --git a/servers/rippling/src/tools/time-tools.ts b/servers/rippling/src/tools/time-tools.ts new file mode 100644 index 0000000..5924b03 --- /dev/null +++ b/servers/rippling/src/tools/time-tools.ts @@ -0,0 +1,215 @@ +import { z } from 'zod'; +import type { RipplingClient } from '../clients/rippling.js'; + +export function createTimeTools(client: RipplingClient) { + return { + rippling_list_time_entries: { + description: 'List time entries (clock in/out records)', + inputSchema: z.object({ + employeeId: z.string().optional().describe('Filter by employee ID'), + startDate: z.string().optional().describe('Filter by start date (YYYY-MM-DD)'), + endDate: z.string().optional().describe('Filter by end date (YYYY-MM-DD)'), + cursor: z.string().optional().describe('Pagination cursor'), + limit: z.number().min(1).max(500).default(100).optional().describe('Number of results per page'), + }), + handler: async (args: any) => { + const result = await client.listTimeEntries(args); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; + }, + }, + + rippling_create_time_entry: { + description: 'Create a new time entry', + inputSchema: z.object({ + employeeId: z.string().describe('Employee ID'), + date: z.string().describe('Date (YYYY-MM-DD)'), + clockIn: z.string().optional().describe('Clock in time (ISO 8601)'), + clockOut: z.string().optional().describe('Clock out time (ISO 8601)'), + hours: z.number().optional().describe('Total hours worked'), + breakMinutes: z.number().optional().describe('Break duration in minutes'), + notes: z.string().optional().describe('Additional notes'), + }), + handler: async (args: any) => { + const result = await client.createTimeEntry(args); + return { + content: [ + { + type: 'text', + text: `Time entry created successfully:\n${JSON.stringify(result, null, 2)}`, + }, + ], + }; + }, + }, + + rippling_update_time_entry: { + description: 'Update an existing time entry', + inputSchema: z.object({ + id: z.string().describe('Time entry ID'), + clockIn: z.string().optional().describe('Clock in time (ISO 8601)'), + clockOut: z.string().optional().describe('Clock out time (ISO 8601)'), + hours: z.number().optional().describe('Total hours worked'), + breakMinutes: z.number().optional().describe('Break duration in minutes'), + notes: z.string().optional().describe('Additional notes'), + }), + handler: async (args: any) => { + const { id, ...data } = args; + const result = await client.updateTimeEntry(id, data); + return { + content: [ + { + type: 'text', + text: `Time entry updated successfully:\n${JSON.stringify(result, null, 2)}`, + }, + ], + }; + }, + }, + + rippling_delete_time_entry: { + description: 'Delete a time entry', + inputSchema: z.object({ + id: z.string().describe('Time entry ID'), + }), + handler: async (args: any) => { + const result = await client.deleteTimeEntry(args.id); + return { + content: [ + { + type: 'text', + text: `Time entry deleted successfully:\n${JSON.stringify(result, null, 2)}`, + }, + ], + }; + }, + }, + + rippling_get_timesheet: { + description: 'Get a timesheet for a specific period', + inputSchema: z.object({ + id: z.string().describe('Timesheet ID'), + }), + handler: async (args: any) => { + const result = await client.getTimesheet(args.id); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; + }, + }, + + rippling_approve_timesheet: { + description: 'Approve a timesheet', + inputSchema: z.object({ + id: z.string().describe('Timesheet ID'), + }), + handler: async (args: any) => { + const result = await client.approveTimesheet(args.id); + return { + content: [ + { + type: 'text', + text: `Timesheet approved successfully:\n${JSON.stringify(result, null, 2)}`, + }, + ], + }; + }, + }, + + rippling_list_time_off_requests: { + description: 'List time off (PTO/vacation) requests', + inputSchema: z.object({ + employeeId: z.string().optional().describe('Filter by employee ID'), + status: z.enum(['PENDING', 'APPROVED', 'DENIED', 'CANCELLED']).optional().describe('Filter by status'), + startDate: z.string().optional().describe('Filter by start date (YYYY-MM-DD)'), + endDate: z.string().optional().describe('Filter by end date (YYYY-MM-DD)'), + cursor: z.string().optional().describe('Pagination cursor'), + limit: z.number().min(1).max(500).default(100).optional().describe('Number of results per page'), + }), + handler: async (args: any) => { + const result = await client.listTimeOffRequests(args); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; + }, + }, + + rippling_create_time_off_request: { + description: 'Create a new time off request', + inputSchema: z.object({ + employeeId: z.string().describe('Employee ID'), + type: z.string().describe('Time off type (e.g., "Vacation", "Sick Leave", "Personal")'), + startDate: z.string().describe('Start date (YYYY-MM-DD)'), + endDate: z.string().describe('End date (YYYY-MM-DD)'), + days: z.number().optional().describe('Number of days'), + hours: z.number().optional().describe('Number of hours'), + reason: z.string().optional().describe('Reason for time off'), + }), + handler: async (args: any) => { + const result = await client.createTimeOffRequest(args); + return { + content: [ + { + type: 'text', + text: `Time off request created successfully:\n${JSON.stringify(result, null, 2)}`, + }, + ], + }; + }, + }, + + rippling_approve_time_off_request: { + description: 'Approve a time off request', + inputSchema: z.object({ + id: z.string().describe('Time off request ID'), + }), + handler: async (args: any) => { + const result = await client.approveTimeOffRequest(args.id); + return { + content: [ + { + type: 'text', + text: `Time off request approved successfully:\n${JSON.stringify(result, null, 2)}`, + }, + ], + }; + }, + }, + + rippling_deny_time_off_request: { + description: 'Deny a time off request', + inputSchema: z.object({ + id: z.string().describe('Time off request ID'), + reason: z.string().optional().describe('Reason for denial'), + }), + handler: async (args: any) => { + const { id, reason } = args; + const result = await client.denyTimeOffRequest(id, reason); + return { + content: [ + { + type: 'text', + text: `Time off request denied:\n${JSON.stringify(result, null, 2)}`, + }, + ], + }; + }, + }, + }; +} diff --git a/servers/rippling/src/types/index.ts b/servers/rippling/src/types/index.ts new file mode 100644 index 0000000..e7e218e --- /dev/null +++ b/servers/rippling/src/types/index.ts @@ -0,0 +1,336 @@ +// Rippling API Types + +export interface RipplingConfig { + apiKey?: string; + accessToken?: string; + baseUrl?: string; +} + +export interface Employee { + id: string; + firstName: string; + lastName: string; + email: string; + personalEmail?: string; + phoneNumber?: string; + employeeId?: string; + department?: string; + title?: string; + manager?: string; + managerId?: string; + startDate?: string; + employmentType?: string; + status?: 'ACTIVE' | 'INACTIVE' | 'TERMINATED'; + workLocationId?: string; + homeAddress?: Address; + compensation?: Compensation; + customFields?: Record; +} + +export interface Address { + street1?: string; + street2?: string; + city?: string; + state?: string; + zipCode?: string; + country?: string; +} + +export interface Compensation { + amount?: number; + currency?: string; + frequency?: 'HOURLY' | 'ANNUALLY' | 'MONTHLY'; + effectiveDate?: string; +} + +export interface Company { + id: string; + name: string; + legalName?: string; + ein?: string; + address?: Address; + createdAt?: string; + updatedAt?: string; +} + +export interface Department { + id: string; + name: string; + parentId?: string; + headId?: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Location { + id: string; + name: string; + address?: Address; + timezone?: string; + isHeadquarters?: boolean; +} + +export interface Team { + id: string; + name: string; + description?: string; + managerId?: string; + memberIds?: string[]; +} + +export interface PayRun { + id: string; + payPeriodStart: string; + payPeriodEnd: string; + payDate: string; + status: 'DRAFT' | 'APPROVED' | 'PROCESSING' | 'COMPLETED' | 'CANCELLED'; + type?: 'REGULAR' | 'OFF_CYCLE' | 'BONUS'; + totalGrossPay?: number; + totalNetPay?: number; + employeeCount?: number; +} + +export interface PayStatement { + id: string; + employeeId: string; + payRunId: string; + payDate: string; + grossPay: number; + netPay: number; + taxes: number; + deductions: number; + earnings?: EarningsLine[]; + taxLines?: TaxLine[]; + deductionLines?: DeductionLine[]; +} + +export interface EarningsLine { + type: string; + description?: string; + amount: number; + hours?: number; + rate?: number; +} + +export interface TaxLine { + type: string; + description?: string; + amount: number; + employeeContribution?: number; + employerContribution?: number; +} + +export interface DeductionLine { + type: string; + description?: string; + amount: number; +} + +export interface TimeEntry { + id: string; + employeeId: string; + date: string; + clockIn?: string; + clockOut?: string; + hours: number; + breakMinutes?: number; + status?: 'PENDING' | 'APPROVED' | 'REJECTED'; + notes?: string; +} + +export interface Timesheet { + id: string; + employeeId: string; + periodStart: string; + periodEnd: string; + totalHours: number; + status: 'DRAFT' | 'SUBMITTED' | 'APPROVED' | 'REJECTED'; + entries?: TimeEntry[]; + approverId?: string; + approvedAt?: string; +} + +export interface TimeOffRequest { + id: string; + employeeId: string; + type: string; + startDate: string; + endDate: string; + days: number; + hours?: number; + status: 'PENDING' | 'APPROVED' | 'DENIED' | 'CANCELLED'; + reason?: string; + approverId?: string; + approvedAt?: string; + deniedReason?: string; +} + +export interface BenefitsPlan { + id: string; + name: string; + type: 'HEALTH' | 'DENTAL' | 'VISION' | '401K' | 'FSA' | 'HSA' | 'LIFE' | 'DISABILITY' | 'OTHER'; + carrier?: string; + planYear?: string; + isActive?: boolean; + employeeCost?: number; + employerCost?: number; +} + +export interface BenefitsEnrollment { + id: string; + employeeId: string; + planId: string; + status: 'ACTIVE' | 'PENDING' | 'TERMINATED' | 'WAIVED'; + effectiveDate?: string; + terminationDate?: string; + coverageLevel?: string; + dependents?: Dependent[]; +} + +export interface Dependent { + id: string; + firstName: string; + lastName: string; + dateOfBirth: string; + relationship: string; + ssn?: string; +} + +export interface Candidate { + id: string; + firstName: string; + lastName: string; + email: string; + phone?: string; + resumeUrl?: string; + currentStage?: string; + currentJobId?: string; + source?: string; + appliedAt?: string; + tags?: string[]; +} + +export interface Job { + id: string; + title: string; + department?: string; + location?: string; + employmentType?: 'FULL_TIME' | 'PART_TIME' | 'CONTRACT' | 'INTERN'; + status: 'DRAFT' | 'OPEN' | 'CLOSED' | 'ON_HOLD'; + description?: string; + requirements?: string; + hiringManagerId?: string; + openings?: number; + createdAt?: string; +} + +export interface Application { + id: string; + candidateId: string; + jobId: string; + stage: string; + status: 'ACTIVE' | 'HIRED' | 'REJECTED' | 'WITHDRAWN'; + appliedAt: string; + lastActivityAt?: string; + interviews?: Interview[]; +} + +export interface Interview { + id: string; + applicationId: string; + scheduledAt?: string; + duration?: number; + interviewerIds?: string[]; + type?: string; + feedback?: string; + outcome?: 'POSITIVE' | 'NEGATIVE' | 'NEUTRAL'; +} + +export interface Course { + id: string; + title: string; + description?: string; + category?: string; + duration?: number; + provider?: string; + isRequired?: boolean; + url?: string; +} + +export interface CourseAssignment { + id: string; + employeeId: string; + courseId: string; + assignedAt: string; + dueDate?: string; + completedAt?: string; + status: 'NOT_STARTED' | 'IN_PROGRESS' | 'COMPLETED' | 'OVERDUE'; + progress?: number; + score?: number; +} + +export interface Device { + id: string; + type: 'LAPTOP' | 'DESKTOP' | 'PHONE' | 'TABLET' | 'OTHER'; + make?: string; + model?: string; + serialNumber?: string; + assignedTo?: string; + assignedAt?: string; + status: 'AVAILABLE' | 'ASSIGNED' | 'REPAIR' | 'RETIRED'; + purchaseDate?: string; + warrantyExpiration?: string; +} + +export interface AppLicense { + id: string; + name: string; + vendor?: string; + licenseType?: 'USER' | 'DEVICE' | 'ENTERPRISE'; + totalLicenses?: number; + usedLicenses?: number; + cost?: number; + renewalDate?: string; + assignedUsers?: string[]; +} + +export interface Group { + id: string; + name: string; + description?: string; + type?: string; + memberIds?: string[]; + createdAt?: string; + updatedAt?: string; +} + +export interface CustomObject { + id: string; + objectType: string; + fields: Record; + createdAt?: string; + updatedAt?: string; +} + +export interface CustomField { + id: string; + name: string; + fieldType: 'TEXT' | 'NUMBER' | 'DATE' | 'BOOLEAN' | 'SELECT' | 'MULTI_SELECT'; + options?: string[]; + isRequired?: boolean; + defaultValue?: any; +} + +export interface PaginatedResponse { + data: T[]; + nextCursor?: string; + hasMore: boolean; + total?: number; +} + +export interface RipplingError { + message: string; + code?: string; + statusCode?: number; + details?: any; +} diff --git a/servers/rippling/src/ui/react-app/ats-pipeline.tsx b/servers/rippling/src/ui/react-app/ats-pipeline.tsx new file mode 100644 index 0000000..30d75c8 --- /dev/null +++ b/servers/rippling/src/ui/react-app/ats-pipeline.tsx @@ -0,0 +1,127 @@ +import React from 'react'; +import { User, Briefcase, TrendingUp } from 'lucide-react'; +import type { Application, Candidate } from '../../types/index.js'; + +interface ATSPipelineProps { + applications?: Application[]; + candidates?: Candidate[]; +} + +const stages = [ + 'Applied', + 'Phone Screen', + 'Technical Interview', + 'Team Interview', + 'Final Round', + 'Offer', +]; + +export const ATSPipeline: React.FC = ({ + applications = [], + candidates = [], +}) => { + const activeApplications = applications.filter(a => a.status === 'ACTIVE'); + + const applicationsByStage: Record = {}; + stages.forEach(stage => { + applicationsByStage[stage] = activeApplications.filter(a => a.stage === stage); + }); + + const totalCandidates = candidates.length; + const hired = applications.filter(a => a.status === 'HIRED').length; + const conversionRate = totalCandidates > 0 ? ((hired / totalCandidates) * 100).toFixed(1) : 0; + + return ( +
+

ATS Pipeline

+ +
+
+
+
+

Total Candidates

+

{totalCandidates}

+
+ +
+
+ +
+
+
+

Hired

+

{hired}

+
+ +
+
+ +
+
+
+

Conversion Rate

+

{conversionRate}%

+
+ +
+
+
+ +
+
+ {stages.map((stage) => { + const apps = applicationsByStage[stage] || []; + const count = apps.length; + + return ( +
+
+
+

{stage}

+ + {count} + +
+
+ +
+ {apps.map((app) => { + const candidate = candidates.find(c => c.id === app.candidateId); + + return ( +
+ {candidate && ( + <> +

+ {candidate.firstName} {candidate.lastName} +

+ {candidate.email && ( +

{candidate.email}

+ )} + + )} +
+ Applied: {new Date(app.appliedAt).toLocaleDateString()} +
+ {app.interviews && app.interviews.length > 0 && ( +
+ + {app.interviews.length} interview{app.interviews.length !== 1 ? 's' : ''} + +
+ )} +
+ ); + })} +
+
+ ); + })} +
+
+
+ ); +}; diff --git a/servers/rippling/src/ui/react-app/benefits-overview.tsx b/servers/rippling/src/ui/react-app/benefits-overview.tsx new file mode 100644 index 0000000..5745eea --- /dev/null +++ b/servers/rippling/src/ui/react-app/benefits-overview.tsx @@ -0,0 +1,122 @@ +import React from 'react'; +import { Heart, Eye, Briefcase, Shield } from 'lucide-react'; +import type { BenefitsPlan, BenefitsEnrollment } from '../../types/index.js'; + +interface BenefitsOverviewProps { + plans?: BenefitsPlan[]; + enrollments?: BenefitsEnrollment[]; +} + +export const BenefitsOverview: React.FC = ({ + plans = [], + enrollments = [], +}) => { + const activeEnrollments = enrollments.filter(e => e.status === 'ACTIVE'); + + const getIconForType = (type: string) => { + switch (type) { + case 'HEALTH': + return ; + case 'DENTAL': + case 'VISION': + return ; + case '401K': + return ; + default: + return ; + } + }; + + const getColorForType = (type: string) => { + const colors: Record = { + HEALTH: 'bg-red-50 text-red-600 border-red-200', + DENTAL: 'bg-blue-50 text-blue-600 border-blue-200', + VISION: 'bg-purple-50 text-purple-600 border-purple-200', + '401K': 'bg-green-50 text-green-600 border-green-200', + FSA: 'bg-yellow-50 text-yellow-600 border-yellow-200', + HSA: 'bg-orange-50 text-orange-600 border-orange-200', + LIFE: 'bg-indigo-50 text-indigo-600 border-indigo-200', + DISABILITY: 'bg-pink-50 text-pink-600 border-pink-200', + }; + return colors[type] || 'bg-gray-50 text-gray-600 border-gray-200'; + }; + + return ( +
+

Benefits Overview

+ +
+
+

Total Plans

+

{plans.length}

+
+ +
+

Active Enrollments

+

{activeEnrollments.length}

+
+
+ +
+

Available Plans

+
+ {plans.map((plan) => { + const enrollmentCount = enrollments.filter(e => + e.planId === plan.id && e.status === 'ACTIVE' + ).length; + + return ( +
+
+
+ {getIconForType(plan.type)} +
+

{plan.name}

+

{plan.type}

+
+
+ {plan.isActive && ( + + Active + + )} +
+ + {plan.carrier && ( +

Carrier: {plan.carrier}

+ )} + + {plan.planYear && ( +

Plan Year: {plan.planYear}

+ )} + +
+ {plan.employeeCost !== undefined && ( +
+

Employee Cost

+

${plan.employeeCost}/mo

+
+ )} + {plan.employerCost !== undefined && ( +
+

Employer Cost

+

${plan.employerCost}/mo

+
+ )} +
+ +
+

Enrollments

+

{enrollmentCount}

+
+
+ ); + })} +
+
+
+ ); +}; diff --git a/servers/rippling/src/ui/react-app/department-grid.tsx b/servers/rippling/src/ui/react-app/department-grid.tsx new file mode 100644 index 0000000..b99fb37 --- /dev/null +++ b/servers/rippling/src/ui/react-app/department-grid.tsx @@ -0,0 +1,124 @@ +import React from 'react'; +import { Building2, Users, TrendingUp } from 'lucide-react'; +import type { Department, Employee } from '../../types/index.js'; + +interface DepartmentGridProps { + departments?: Department[]; + employees?: Employee[]; +} + +export const DepartmentGrid: React.FC = ({ + departments = [], + employees = [], +}) => { + const getDepartmentEmployees = (deptId: string) => { + return employees.filter(e => e.department === deptId); + }; + + const totalEmployees = employees.length; + + return ( +
+

Department Overview

+ +
+
+
+
+

Total Departments

+

{departments.length}

+
+ +
+
+ +
+
+
+

Total Employees

+

{totalEmployees}

+
+ +
+
+
+ +
+ {departments.map((dept) => { + const deptEmployees = getDepartmentEmployees(dept.id); + const headEmployee = dept.headId + ? employees.find(e => e.id === dept.headId) + : null; + const percentage = totalEmployees > 0 + ? ((deptEmployees.length / totalEmployees) * 100).toFixed(1) + : 0; + + return ( +
+
+
+
+ +
+
+

{dept.name}

+
+
+
+ + {headEmployee && ( +
+

Department Head

+

+ {headEmployee.firstName} {headEmployee.lastName} +

+ {headEmployee.title && ( +

{headEmployee.title}

+ )} +
+ )} + +
+
+
+ + Employees +
+ {deptEmployees.length} +
+ +
+
+
+ +
+ {percentage}% of company +
+ + Active +
+
+
+ + {dept.createdAt && ( +
+ Created: {new Date(dept.createdAt).toLocaleDateString()} +
+ )} +
+ ); + })} +
+ + {departments.length === 0 && ( +

No departments found

+ )} +
+ ); +}; diff --git a/servers/rippling/src/ui/react-app/device-inventory.tsx b/servers/rippling/src/ui/react-app/device-inventory.tsx new file mode 100644 index 0000000..047bce7 --- /dev/null +++ b/servers/rippling/src/ui/react-app/device-inventory.tsx @@ -0,0 +1,156 @@ +import React, { useState } from 'react'; +import { Laptop, Smartphone, Monitor, Filter } from 'lucide-react'; +import type { Device } from '../../types/index.js'; + +interface DeviceInventoryProps { + devices?: Device[]; +} + +export const DeviceInventory: React.FC = ({ + devices = [], +}) => { + const [typeFilter, setTypeFilter] = useState('all'); + const [statusFilter, setStatusFilter] = useState('all'); + + const filteredDevices = devices.filter(d => { + const matchesType = typeFilter === 'all' || d.type === typeFilter; + const matchesStatus = statusFilter === 'all' || d.status === statusFilter; + return matchesType && matchesStatus; + }); + + const available = devices.filter(d => d.status === 'AVAILABLE').length; + const assigned = devices.filter(d => d.status === 'ASSIGNED').length; + const repair = devices.filter(d => d.status === 'REPAIR').length; + + const getDeviceIcon = (type: string) => { + switch (type) { + case 'LAPTOP': + return ; + case 'PHONE': + case 'TABLET': + return ; + case 'DESKTOP': + return ; + default: + return ; + } + }; + + return ( +
+

Device Inventory

+ +
+
+

Available

+

{available}

+
+ +
+

Assigned

+

{assigned}

+
+ +
+

In Repair

+

{repair}

+
+
+ +
+
+ + +
+ +
+ + +
+
+ +
+ Showing {filteredDevices.length} of {devices.length} devices +
+ +
+ {filteredDevices.map((device) => ( +
+
+
+ {getDeviceIcon(device.type)} +
+

{device.make} {device.model}

+

{device.type}

+
+
+ + {device.status} + +
+ +
+ {device.serialNumber && ( +
+ Serial: + {device.serialNumber} +
+ )} + + {device.assignedTo && ( +
+ Assigned to: + {device.assignedTo} +
+ )} + + {device.purchaseDate && ( +
+ Purchase: + {device.purchaseDate} +
+ )} + + {device.warrantyExpiration && ( +
+ Warranty: + {device.warrantyExpiration} +
+ )} +
+
+ ))} +
+
+ ); +}; diff --git a/servers/rippling/src/ui/react-app/employee-dashboard.tsx b/servers/rippling/src/ui/react-app/employee-dashboard.tsx new file mode 100644 index 0000000..a2d09ee --- /dev/null +++ b/servers/rippling/src/ui/react-app/employee-dashboard.tsx @@ -0,0 +1,80 @@ +import React from 'react'; +import { Users, TrendingUp, Calendar, DollarSign } from 'lucide-react'; + +interface EmployeeDashboardProps { + totalEmployees?: number; + activeEmployees?: number; + newHires?: number; + avgTenure?: string; + departments?: Array<{ name: string; count: number }>; +} + +export const EmployeeDashboard: React.FC = ({ + totalEmployees = 0, + activeEmployees = 0, + newHires = 0, + avgTenure = 'N/A', + departments = [], +}) => { + return ( +
+

Employee Dashboard

+ +
+
+
+
+

Total Employees

+

{totalEmployees}

+
+ +
+
+ +
+
+
+

Active

+

{activeEmployees}

+
+ +
+
+ +
+
+
+

New Hires (30d)

+

{newHires}

+
+ +
+
+ +
+
+
+

Avg Tenure

+

{avgTenure}

+
+ +
+
+
+ +
+

Employees by Department

+
+ {departments.map((dept, idx) => ( +
+ {dept.name} + + {dept.count} + +
+ ))} +
+
+
+ ); +}; diff --git a/servers/rippling/src/ui/react-app/employee-detail.tsx b/servers/rippling/src/ui/react-app/employee-detail.tsx new file mode 100644 index 0000000..af19e39 --- /dev/null +++ b/servers/rippling/src/ui/react-app/employee-detail.tsx @@ -0,0 +1,136 @@ +import React from 'react'; +import { Mail, Phone, MapPin, Briefcase, Calendar } from 'lucide-react'; +import type { Employee } from '../../types/index.js'; + +interface EmployeeDetailProps { + employee?: Employee; +} + +export const EmployeeDetail: React.FC = ({ employee }) => { + if (!employee) { + return
No employee data available
; + } + + return ( +
+
+
+

+ {employee.firstName} {employee.lastName} +

+

{employee.title}

+
+ + {employee.status} + +
+ +
+
+

Contact Information

+ +
+ +
+

Work Email

+

{employee.email}

+
+
+ + {employee.personalEmail && ( +
+ +
+

Personal Email

+

{employee.personalEmail}

+
+
+ )} + + {employee.phoneNumber && ( +
+ +
+

Phone

+

{employee.phoneNumber}

+
+
+ )} + + {employee.homeAddress && ( +
+ +
+

Address

+

+ {employee.homeAddress.street1} + {employee.homeAddress.city && `, ${employee.homeAddress.city}`} + {employee.homeAddress.state && `, ${employee.homeAddress.state}`} +

+
+
+ )} +
+ +
+

Employment Details

+ +
+ +
+

Department

+

{employee.department || 'N/A'}

+
+
+ + {employee.manager && ( +
+ +
+

Manager

+

{employee.manager}

+
+
+ )} + + {employee.startDate && ( +
+ +
+

Start Date

+

{employee.startDate}

+
+
+ )} + + {employee.employmentType && ( +
+ +
+

Employment Type

+

{employee.employmentType.replace('_', ' ')}

+
+
+ )} + + {employee.compensation && ( +
+

Compensation

+

+ {employee.compensation.currency || '$'} + {employee.compensation.amount?.toLocaleString()} + + {' '}/ {employee.compensation.frequency?.toLowerCase()} + +

+
+ )} +
+
+
+ ); +}; diff --git a/servers/rippling/src/ui/react-app/employee-directory.tsx b/servers/rippling/src/ui/react-app/employee-directory.tsx new file mode 100644 index 0000000..88ee1c4 --- /dev/null +++ b/servers/rippling/src/ui/react-app/employee-directory.tsx @@ -0,0 +1,111 @@ +import React, { useState } from 'react'; +import { Search, Filter, Mail, Phone } from 'lucide-react'; +import type { Employee } from '../../types/index.js'; + +interface EmployeeDirectoryProps { + employees?: Employee[]; + onSelectEmployee?: (employee: Employee) => void; +} + +export const EmployeeDirectory: React.FC = ({ + employees = [], + onSelectEmployee, +}) => { + const [searchTerm, setSearchTerm] = useState(''); + const [departmentFilter, setDepartmentFilter] = useState('all'); + + const departments = Array.from(new Set(employees.map(e => e.department).filter(Boolean))); + + const filteredEmployees = employees.filter(emp => { + const matchesSearch = + emp.firstName?.toLowerCase().includes(searchTerm.toLowerCase()) || + emp.lastName?.toLowerCase().includes(searchTerm.toLowerCase()) || + emp.email?.toLowerCase().includes(searchTerm.toLowerCase()) || + emp.title?.toLowerCase().includes(searchTerm.toLowerCase()); + + const matchesDepartment = departmentFilter === 'all' || emp.department === departmentFilter; + + return matchesSearch && matchesDepartment; + }); + + return ( +
+

Employee Directory

+ +
+
+ + setSearchTerm(e.target.value)} + className="w-full pl-10 pr-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500" + /> +
+ +
+ + +
+
+ +
+ Showing {filteredEmployees.length} of {employees.length} employees +
+ +
+ {filteredEmployees.map((employee) => ( +
onSelectEmployee?.(employee)} + className="border rounded-lg p-4 hover:shadow-md transition-shadow cursor-pointer" + > +
+
+

+ {employee.firstName} {employee.lastName} +

+

{employee.title}

+
+ + {employee.status} + +
+ + {employee.department && ( +
{employee.department}
+ )} + +
+ {employee.email && ( +
+ + {employee.email} +
+ )} + {employee.phoneNumber && ( +
+ + {employee.phoneNumber} +
+ )} +
+
+ ))} +
+
+ ); +}; diff --git a/servers/rippling/src/ui/react-app/index.tsx b/servers/rippling/src/ui/react-app/index.tsx new file mode 100644 index 0000000..5016d06 --- /dev/null +++ b/servers/rippling/src/ui/react-app/index.tsx @@ -0,0 +1,19 @@ +// Rippling MCP React App Components +// Export all available UI components + +export { EmployeeDashboard } from './employee-dashboard.js'; +export { EmployeeDetail } from './employee-detail.js'; +export { EmployeeDirectory } from './employee-directory.js'; +export { OrgChart } from './org-chart.js'; +export { PayrollDashboard } from './payroll-dashboard.js'; +export { PayrollDetail } from './payroll-detail.js'; +export { TimeTracker } from './time-tracker.js'; +export { TimesheetApprovals } from './timesheet-approvals.js'; +export { TimeOffCalendar } from './time-off-calendar.js'; +export { BenefitsOverview } from './benefits-overview.js'; +export { ATSPipeline } from './ats-pipeline.js'; +export { JobBoard } from './job-board.js'; +export { LearningDashboard } from './learning-dashboard.js'; +export { DeviceInventory } from './device-inventory.js'; +export { TeamOverview } from './team-overview.js'; +export { DepartmentGrid } from './department-grid.js'; diff --git a/servers/rippling/src/ui/react-app/job-board.tsx b/servers/rippling/src/ui/react-app/job-board.tsx new file mode 100644 index 0000000..b82d778 --- /dev/null +++ b/servers/rippling/src/ui/react-app/job-board.tsx @@ -0,0 +1,113 @@ +import React from 'react'; +import { Briefcase, MapPin, Clock, Users } from 'lucide-react'; +import type { Job } from '../../types/index.js'; + +interface JobBoardProps { + jobs?: Job[]; + onApply?: (jobId: string) => void; +} + +export const JobBoard: React.FC = ({ + jobs = [], + onApply, +}) => { + const openJobs = jobs.filter(j => j.status === 'OPEN'); + const totalOpenings = openJobs.reduce((sum, j) => sum + (j.openings || 0), 0); + + return ( +
+

Job Board

+ +
+
+
+
+

Open Positions

+

{openJobs.length}

+
+ +
+
+ +
+
+
+

Total Openings

+

{totalOpenings}

+
+ +
+
+
+ +
+ {openJobs.map((job) => ( +
+
+
+

{job.title}

+ +
+ {job.department && ( +
+ + {job.department} +
+ )} + + {job.location && ( +
+ + {job.location} +
+ )} + + {job.employmentType && ( +
+ + {job.employmentType.replace('_', ' ')} +
+ )} +
+
+ + {job.openings && job.openings > 1 && ( + + {job.openings} openings + + )} +
+ + {job.description && ( +

{job.description}

+ )} + + {job.requirements && ( +
+

Requirements:

+

{job.requirements}

+
+ )} + +
+
+ Posted {job.createdAt ? new Date(job.createdAt).toLocaleDateString() : 'Recently'} +
+ + +
+
+ ))} + + {openJobs.length === 0 && ( +

No open positions at this time

+ )} +
+
+ ); +}; diff --git a/servers/rippling/src/ui/react-app/learning-dashboard.tsx b/servers/rippling/src/ui/react-app/learning-dashboard.tsx new file mode 100644 index 0000000..9ee2493 --- /dev/null +++ b/servers/rippling/src/ui/react-app/learning-dashboard.tsx @@ -0,0 +1,129 @@ +import React from 'react'; +import { BookOpen, Award, Clock, TrendingUp } from 'lucide-react'; +import type { Course, CourseAssignment } from '../../types/index.js'; + +interface LearningDashboardProps { + courses?: Course[]; + assignments?: CourseAssignment[]; +} + +export const LearningDashboard: React.FC = ({ + courses = [], + assignments = [], +}) => { + const completed = assignments.filter(a => a.status === 'COMPLETED'); + const inProgress = assignments.filter(a => a.status === 'IN_PROGRESS'); + const overdue = assignments.filter(a => a.status === 'OVERDUE'); + const avgProgress = assignments.length > 0 + ? assignments.reduce((sum, a) => sum + (a.progress || 0), 0) / assignments.length + : 0; + + return ( +
+

Learning Dashboard

+ +
+
+
+
+

Total Courses

+

{courses.length}

+
+ +
+
+ +
+
+
+

Completed

+

{completed.length}

+
+ +
+
+ +
+
+
+

In Progress

+

{inProgress.length}

+
+ +
+
+ +
+
+
+

Avg Progress

+

{avgProgress.toFixed(0)}%

+
+ +
+
+
+ + {overdue.length > 0 && ( +
+
+ +

+ {overdue.length} overdue assignment{overdue.length !== 1 ? 's' : ''} +

+
+
+ )} + +
+

Recent Assignments

+ {assignments.slice(0, 10).map((assignment) => { + const course = courses.find(c => c.id === assignment.courseId); + const progress = assignment.progress || 0; + + return ( +
+
+
+

{course?.title || 'Unknown Course'}

+

Employee ID: {assignment.employeeId}

+ {assignment.dueDate && ( +

Due: {assignment.dueDate}

+ )} +
+ + {assignment.status.replace('_', ' ')} + +
+ +
+
+ Progress + {progress}% +
+
+
+
+
+ + {assignment.completedAt && ( +

+ Completed: {assignment.completedAt} + {assignment.score !== undefined && ` • Score: ${assignment.score}%`} +

+ )} +
+ ); + })} +
+
+ ); +}; diff --git a/servers/rippling/src/ui/react-app/org-chart.tsx b/servers/rippling/src/ui/react-app/org-chart.tsx new file mode 100644 index 0000000..ed07e8c --- /dev/null +++ b/servers/rippling/src/ui/react-app/org-chart.tsx @@ -0,0 +1,76 @@ +import React from 'react'; +import { User, Users } from 'lucide-react'; + +interface OrgNode { + id: string; + name: string; + title: string; + email?: string; + reports?: OrgNode[]; +} + +interface OrgChartProps { + data?: OrgNode; +} + +const OrgNodeComponent: React.FC<{ node: OrgNode; level: number }> = ({ node, level }) => { + const hasReports = node.reports && node.reports.length > 0; + + return ( +
+
+
+ +

{node.name}

+
+

{node.title}

+ {node.email && ( +

{node.email}

+ )} + {hasReports && ( +
+ + {node.reports!.length} direct report{node.reports!.length !== 1 ? 's' : ''} +
+ )} +
+ + {hasReports && ( +
+
+
+ {node.reports!.map((report, idx) => ( +
+ {idx > 0 && ( +
+ )} + +
+ ))} +
+
+ )} +
+ ); +}; + +export const OrgChart: React.FC = ({ data }) => { + if (!data) { + return ( +
+

No organizational chart data available

+
+ ); + } + + return ( +
+

Organization Chart

+
+ +
+
+ ); +}; diff --git a/servers/rippling/src/ui/react-app/payroll-dashboard.tsx b/servers/rippling/src/ui/react-app/payroll-dashboard.tsx new file mode 100644 index 0000000..9e20c6e --- /dev/null +++ b/servers/rippling/src/ui/react-app/payroll-dashboard.tsx @@ -0,0 +1,119 @@ +import React from 'react'; +import { DollarSign, Calendar, TrendingUp, AlertCircle } from 'lucide-react'; +import type { PayRun } from '../../types/index.js'; + +interface PayrollDashboardProps { + payRuns?: PayRun[]; + totalAnnualPayroll?: number; + avgPayPerEmployee?: number; + nextPayDate?: string; +} + +export const PayrollDashboard: React.FC = ({ + payRuns = [], + totalAnnualPayroll = 0, + avgPayPerEmployee = 0, + nextPayDate, +}) => { + const recentRuns = payRuns.slice(0, 5); + const completedRuns = payRuns.filter(r => r.status === 'COMPLETED'); + const pendingRuns = payRuns.filter(r => r.status === 'PROCESSING' || r.status === 'APPROVED'); + + return ( +
+

Payroll Dashboard

+ +
+
+
+
+

Total Annual Payroll

+

+ ${totalAnnualPayroll.toLocaleString()} +

+
+ +
+
+ +
+
+
+

Avg Pay/Employee

+

+ ${avgPayPerEmployee.toLocaleString()} +

+
+ +
+
+ +
+
+
+

Next Pay Date

+

{nextPayDate || 'N/A'}

+
+ +
+
+ +
+
+
+

Pending Runs

+

{pendingRuns.length}

+
+ +
+
+
+ +
+

Recent Pay Runs

+
+ + + + + + + + + + + + + {recentRuns.map((run) => ( + + + + + + + + + ))} + +
Pay DatePeriodEmployeesGross PayNet PayStatus
{run.payDate} + {run.payPeriodStart} - {run.payPeriodEnd} + {run.employeeCount} + ${run.totalGrossPay?.toLocaleString()} + + ${run.totalNetPay?.toLocaleString()} + + + {run.status} + +
+
+
+
+ ); +}; diff --git a/servers/rippling/src/ui/react-app/payroll-detail.tsx b/servers/rippling/src/ui/react-app/payroll-detail.tsx new file mode 100644 index 0000000..cbaffb0 --- /dev/null +++ b/servers/rippling/src/ui/react-app/payroll-detail.tsx @@ -0,0 +1,184 @@ +import React from 'react'; +import { DollarSign, TrendingDown, TrendingUp, FileText } from 'lucide-react'; +import type { PayStatement } from '../../types/index.js'; + +interface PayrollDetailProps { + payStatement?: PayStatement; +} + +export const PayrollDetail: React.FC = ({ payStatement }) => { + if (!payStatement) { + return ( +
+

No pay statement data available

+
+ ); + } + + const totalEarnings = payStatement.earnings?.reduce((sum, e) => sum + e.amount, 0) || payStatement.grossPay; + const totalTaxes = payStatement.taxLines?.reduce((sum, t) => sum + t.amount, 0) || payStatement.taxes; + const totalDeductions = payStatement.deductionLines?.reduce((sum, d) => sum + d.amount, 0) || payStatement.deductions; + + return ( +
+
+

Pay Statement

+ +
+ +
+

Pay Date

+

{payStatement.payDate}

+
+
+

Gross Pay

+

${payStatement.grossPay.toLocaleString()}

+
+
+

Net Pay

+

${payStatement.netPay.toLocaleString()}

+
+
+
+ +
+
+
+ +

Earnings

+
+

${totalEarnings.toLocaleString()}

+
+ +
+
+ +

Taxes

+
+

${totalTaxes.toLocaleString()}

+
+ +
+
+ +

Deductions

+
+

${totalDeductions.toLocaleString()}

+
+
+ + {payStatement.earnings && payStatement.earnings.length > 0 && ( +
+

Earnings Breakdown

+
+ + + + + + + + + + + {payStatement.earnings.map((earning, idx) => ( + + + + + + + ))} + +
TypeHoursRateAmount
+
+

{earning.type}

+ {earning.description && ( +

{earning.description}

+ )} +
+
{earning.hours || '-'} + {earning.rate ? `$${earning.rate}` : '-'} + + ${earning.amount.toLocaleString()} +
+
+
+ )} + + {payStatement.taxLines && payStatement.taxLines.length > 0 && ( +
+

Tax Breakdown

+
+ + + + + + + + + + + {payStatement.taxLines.map((tax, idx) => ( + + + + + + + ))} + +
TypeEmployeeEmployerTotal
+
+

{tax.type}

+ {tax.description && ( +

{tax.description}

+ )} +
+
+ ${(tax.employeeContribution || 0).toLocaleString()} + + ${(tax.employerContribution || 0).toLocaleString()} + + ${tax.amount.toLocaleString()} +
+
+
+ )} + + {payStatement.deductionLines && payStatement.deductionLines.length > 0 && ( +
+

Deductions

+
+ + + + + + + + + {payStatement.deductionLines.map((deduction, idx) => ( + + + + + ))} + +
TypeAmount
+
+

{deduction.type}

+ {deduction.description && ( +

{deduction.description}

+ )} +
+
+ ${deduction.amount.toLocaleString()} +
+
+
+ )} +
+ ); +}; diff --git a/servers/rippling/src/ui/react-app/team-overview.tsx b/servers/rippling/src/ui/react-app/team-overview.tsx new file mode 100644 index 0000000..dbf9b40 --- /dev/null +++ b/servers/rippling/src/ui/react-app/team-overview.tsx @@ -0,0 +1,123 @@ +import React from 'react'; +import { Users, TrendingUp, Award, Target } from 'lucide-react'; +import type { Team, Employee } from '../../types/index.js'; + +interface TeamOverviewProps { + teams?: Team[]; + employees?: Employee[]; +} + +export const TeamOverview: React.FC = ({ + teams = [], + employees = [], +}) => { + const totalMembers = teams.reduce((sum, t) => sum + (t.memberIds?.length || 0), 0); + const avgTeamSize = teams.length > 0 ? (totalMembers / teams.length).toFixed(1) : 0; + + return ( +
+

Team Overview

+ +
+
+
+
+

Total Teams

+

{teams.length}

+
+ +
+
+ +
+
+
+

Total Members

+

{totalMembers}

+
+ +
+
+ +
+
+
+

Avg Team Size

+

{avgTeamSize}

+
+ +
+
+
+ +
+

All Teams

+
+ {teams.map((team) => { + const memberCount = team.memberIds?.length || 0; + const manager = team.managerId + ? employees.find(e => e.id === team.managerId) + : null; + + return ( +
+
+
+
+ +
+
+

{team.name}

+ {team.description && ( +

{team.description}

+ )} +
+
+ + {memberCount} {memberCount === 1 ? 'member' : 'members'} + +
+ + {manager && ( +
+
+ +
+

Team Manager

+

+ {manager.firstName} {manager.lastName} +

+
+
+
+ )} + + {team.memberIds && team.memberIds.length > 0 && ( +
+ {team.memberIds.slice(0, 5).map((memberId) => { + const member = employees.find(e => e.id === memberId); + return member ? ( +
+ {member.firstName} {member.lastName.charAt(0)}. +
+ ) : null; + })} + {team.memberIds.length > 5 && ( +
+ +{team.memberIds.length - 5} more +
+ )} +
+ )} +
+ ); + })} +
+
+
+ ); +}; diff --git a/servers/rippling/src/ui/react-app/time-off-calendar.tsx b/servers/rippling/src/ui/react-app/time-off-calendar.tsx new file mode 100644 index 0000000..ef47825 --- /dev/null +++ b/servers/rippling/src/ui/react-app/time-off-calendar.tsx @@ -0,0 +1,99 @@ +import React from 'react'; +import { Calendar, User, Clock } from 'lucide-react'; +import type { TimeOffRequest } from '../../types/index.js'; + +interface TimeOffCalendarProps { + requests?: TimeOffRequest[]; +} + +export const TimeOffCalendar: React.FC = ({ + requests = [], +}) => { + const approved = requests.filter(r => r.status === 'APPROVED'); + const pending = requests.filter(r => r.status === 'PENDING'); + + // Group by month + const requestsByMonth: Record = {}; + approved.forEach(req => { + const month = req.startDate.substring(0, 7); // YYYY-MM + if (!requestsByMonth[month]) requestsByMonth[month] = []; + requestsByMonth[month].push(req); + }); + + return ( +
+

Time Off Calendar

+ +
+
+
+
+

Approved

+

{approved.length}

+
+ +
+
+ +
+
+
+

Pending

+

{pending.length}

+
+ +
+
+ +
+
+
+

Total Days

+

+ {approved.reduce((sum, r) => sum + r.days, 0)} +

+
+ +
+
+
+ +
+ {Object.entries(requestsByMonth).map(([month, reqs]) => ( +
+

+ {new Date(month + '-01').toLocaleDateString('en-US', { year: 'numeric', month: 'long' })} +

+
+ {reqs.map((req) => ( +
+
+
+
+ + Employee {req.employeeId} + + {req.type} + +
+
+ {req.startDate} to {req.endDate} +
+ {req.reason && ( +

{req.reason}

+ )} +
+
+

{req.days}

+

days

+
+
+
+ ))} +
+
+ ))} +
+
+ ); +}; diff --git a/servers/rippling/src/ui/react-app/time-tracker.tsx b/servers/rippling/src/ui/react-app/time-tracker.tsx new file mode 100644 index 0000000..dc516ed --- /dev/null +++ b/servers/rippling/src/ui/react-app/time-tracker.tsx @@ -0,0 +1,112 @@ +import React, { useState } from 'react'; +import { Clock, Play, Pause, Calendar } from 'lucide-react'; +import type { TimeEntry } from '../../types/index.js'; + +interface TimeTrackerProps { + entries?: TimeEntry[]; + onClockIn?: () => void; + onClockOut?: () => void; + currentEntry?: TimeEntry | null; +} + +export const TimeTracker: React.FC = ({ + entries = [], + onClockIn, + onClockOut, + currentEntry, +}) => { + const [selectedDate, setSelectedDate] = useState(new Date().toISOString().split('T')[0]); + + const todayEntries = entries.filter(e => e.date === selectedDate); + const totalHours = todayEntries.reduce((sum, e) => sum + e.hours, 0); + + return ( +
+

Time Tracker

+ +
+
+
+ +
+

Today's Hours

+

{totalHours.toFixed(2)}

+
+
+ + {currentEntry ? ( + + ) : ( + + )} +
+ + {currentEntry && ( +
+

Clocked in at {currentEntry.clockIn}

+
+ )} +
+ +
+ +
+ + setSelectedDate(e.target.value)} + className="pl-10 pr-4 py-2 border rounded-lg w-full md:w-64" + /> +
+
+ +
+

Time Entries

+ {todayEntries.length === 0 ? ( +

No time entries for this date

+ ) : ( + todayEntries.map((entry) => ( +
+
+
+
+ Clock In +

{entry.clockIn || 'N/A'}

+
+
+ Clock Out +

{entry.clockOut || 'Active'}

+
+
+ Break +

{entry.breakMinutes || 0} min

+
+
+ {entry.notes && ( +

{entry.notes}

+ )} +
+
+

{entry.hours.toFixed(2)}

+

hours

+
+
+ )) + )} +
+
+ ); +}; diff --git a/servers/rippling/src/ui/react-app/timesheet-approvals.tsx b/servers/rippling/src/ui/react-app/timesheet-approvals.tsx new file mode 100644 index 0000000..6c1321b --- /dev/null +++ b/servers/rippling/src/ui/react-app/timesheet-approvals.tsx @@ -0,0 +1,106 @@ +import React from 'react'; +import { CheckCircle, XCircle, Clock } from 'lucide-react'; +import type { Timesheet } from '../../types/index.js'; + +interface TimesheetApprovalsProps { + timesheets?: Timesheet[]; + onApprove?: (id: string) => void; + onReject?: (id: string) => void; +} + +export const TimesheetApprovals: React.FC = ({ + timesheets = [], + onApprove, + onReject, +}) => { + const pending = timesheets.filter(t => t.status === 'SUBMITTED'); + const approved = timesheets.filter(t => t.status === 'APPROVED'); + const rejected = timesheets.filter(t => t.status === 'REJECTED'); + + return ( +
+

Timesheet Approvals

+ +
+
+
+ + Pending +
+

{pending.length}

+
+ +
+
+ + Approved +
+

{approved.length}

+
+ +
+
+ + Rejected +
+

{rejected.length}

+
+
+ +
+

Pending Approvals

+ {pending.length === 0 ? ( +

No pending timesheets

+ ) : ( + pending.map((timesheet) => ( +
+
+
+

Employee ID: {timesheet.employeeId}

+

+ {timesheet.periodStart} to {timesheet.periodEnd} +

+
+
+

{timesheet.totalHours}

+

hours

+
+
+ + {timesheet.entries && timesheet.entries.length > 0 && ( +
+

{timesheet.entries.length} entries

+
+ {timesheet.entries.slice(0, 4).map((entry, idx) => ( +
+ {entry.date} + {entry.hours}h +
+ ))} +
+
+ )} + +
+ + +
+
+ )) + )} +
+
+ ); +}; diff --git a/servers/rippling/tsconfig.json b/servers/rippling/tsconfig.json index de6431e..38a0f2f 100644 --- a/servers/rippling/tsconfig.json +++ b/servers/rippling/tsconfig.json @@ -1,14 +1,20 @@ { "compilerOptions": { "target": "ES2022", - "module": "NodeNext", - "moduleResolution": "NodeNext", + "module": "Node16", + "moduleResolution": "Node16", + "lib": ["ES2022"], "outDir": "./dist", "rootDir": "./src", "strict": true, "esModuleInterop": true, "skipLibCheck": true, - "declaration": true + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "jsx": "react-jsx" }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"]