import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { RipplingClient } from "../clients/rippling-client.js";
import { RipplingApiError } from "../utils/errors.js";
export function registerEmployeeTools(
server: McpServer,
client: RipplingClient
): void {
server.tool(
"list_employees",
"List active employees with pagination. Returns employee details including name, title, department, and work email.",
{
limit: z
.number()
.min(1)
.max(100)
.optional()
.describe("Max results per page (1-100, default 50)"),
offset: z
.number()
.min(0)
.optional()
.describe("Pagination offset (default 0)"),
},
async ({ limit, offset }) => {
try {
const employees = await client.listEmployees({ limit, offset });
return {
content: [
{
type: "text",
text: JSON.stringify(employees, null, 2),
},
],
};
} catch (error) {
if (error instanceof RipplingApiError) return error.toToolResult();
throw error;
}
}
);
server.tool(
"get_employee",
"Get detailed information for a specific employee by their ID",
{
employeeId: z.string().describe("The employee's role ID"),
},
async ({ employeeId }) => {
try {
const employee = await client.getEmployee(employeeId);
return {
content: [
{
type: "text",
text: JSON.stringify(employee, null, 2),
},
],
};
} catch (error) {
if (error instanceof RipplingApiError) return error.toToolResult();
throw error;
}
}
);
server.tool(
"list_all_employees",
"List all employees including terminated ones. Useful for complete workforce history.",
{
limit: z
.number()
.min(1)
.max(100)
.optional()
.describe("Max results per page (1-100, default 50)"),
offset: z
.number()
.min(0)
.optional()
.describe("Pagination offset (default 0)"),
ein: z
.string()
.optional()
.describe("Filter by Employer Identification Number"),
},
async ({ limit, offset, ein }) => {
try {
const employees = await client.listAllEmployees({
limit,
offset,
ein,
});
return {
content: [
{
type: "text",
text: JSON.stringify(employees, null, 2),
},
],
};
} catch (error) {
if (error instanceof RipplingApiError) return error.toToolResult();
throw error;
}
}
);
server.tool(
"search_employees",
"Search employees by name, email, title, or department. Fetches all active employees and filters client-side.",
{
query: z
.string()
.describe(
"Search query — matches against name, work email, title, and department"
),
limit: z
.number()
.min(1)
.max(100)
.optional()
.describe("Max results to return (default 10)"),
},
async ({ query, limit = 10 }) => {
try {
const queryLower = query.toLowerCase();
const allEmployees: unknown[] = [];
let offset = 0;
const pageSize = 100;
// Paginate through all employees to search
while (true) {
const page = await client.listEmployees({
limit: pageSize,
offset,
});
if (!Array.isArray(page) || page.length === 0) break;
allEmployees.push(...page);
if (page.length < pageSize) break;
offset += pageSize;
}
const matches = allEmployees.filter((emp) => {
const e = emp as Record<string, unknown>;
const searchFields = [
e.name,
e.firstName,
e.lastName,
e.preferredFirstName,
e.preferredLastName,
e.workEmail,
e.personalEmail,
e.title,
e.department,
];
return searchFields.some(
(field) =>
typeof field === "string" &&
field.toLowerCase().includes(queryLower)
);
});
const results = matches.slice(0, limit);
return {
content: [
{
type: "text",
text:
results.length > 0
? `Found ${matches.length} match(es) for "${query}":\n\n${JSON.stringify(results, null, 2)}`
: `No employees found matching "${query}"`,
},
],
};
} catch (error) {
if (error instanceof RipplingApiError) return error.toToolResult();
throw error;
}
}
);
}