Skip to main content
Workflows Library MCP Directory Realtime AI News Sponsor Tier Subscribe
Front Page / AI Tools / Deep Dive

Build a Payroll & HR Agentic MCP Server for Kredily-Style Workforce Operations

Inspired by Kredily 3.0's KAI launch on August 14, 2026, this dispatch builds payroll-hr-mcp, a FastMCP TypeScript server exposing read-only payroll overview, attendance, statutory compliance, and pay-slip queries to agents. Write tools — payslip runs and reimbursement approvals — are gated behind a human-approval workflow with OAuth 2.0 enforcement.

Deepak Bagada

Deepak Bagada

CEO, SaaSNext

Aug 17, 2026 Published
|
Aug 17, 2026 Updated
|
10 Minutes Reading Time
Core Takeaways for Founders & Builders
  • Kredily 3.0 launched KAI on August 14, 2026, making payroll the proving ground for agentic back-office operations.
  • The safe design keeps a deterministic engine underneath; the agent orchestrates, never calculates.
  • payroll-hr-mcp exposes read-only overview, attendance, compliance, and pay-slip tools always available to authenticated agents.
  • Write tools create intents only — payslip runs and reimbursement approvals require a scope token plus a human gate outside the model.
  • OAuth 2.0 audience validation, scopes, and per-hop credential isolation make the server production-defensible.

By Deepak Bagada, CEO at SaaSNext & Principal AI Architect.

On August 14, 2026, Kredily 3.0 launched KAI, its agentic AI layer for payroll and HR, and the signal it sent to every builder is unmistakable: the back office is now a first-class target for autonomous agents, and payroll is the proving ground. Kredily's own numbers — 25,000+ businesses and over a million employees on the platform — tell the scale story, but the architectural story is what matters for your own stack. KAI works because it keeps a deterministic payroll engine underneath and puts agents in an orchestration role, never in a calculation role. Salaries are paid, statutory returns are filed, and compliance stays audit-ready while agents absorb the mechanical work.

This dispatch builds the MCP version of that pattern. You will implement payroll-hr-mcp, a FastMCP TypeScript server that exposes read-only payroll overview, attendance, statutory compliance, and pay-slip queries to any MCP client agent, protected by OAuth 2.0, with write tools — payslip run and reimbursement approval — gated behind a human-approval workflow. If your company runs payroll-adjacent agentic work, or you want to understand how agent safety is designed for money-moving workflows, this is the reference build. Keep the MCP directory open while you code — the FastMCP tooling and patterns used here mirror what you will register there.

Why payroll is the right first agentic back-office target

Payroll is the least forgiving software category in business. It runs on fixed pay dates, immutable tax calendars, and statutory deadlines with real penalties attached. That makes it the perfect stress test for agent safety. A payroll agent that hallucinates an attendance figure is embarrassing; a payroll agent that releases a salary run without approval is a compliance incident. Kredily KAI chose this category deliberately because success here generalizes to accounts payable, vendor onboarding, tax reconciliation, and expense audit. The pattern is identical: high volume, rule-driven, deadline-bound, compliance-heavy, and full of clear approval points where a human must remain in the loop.

The latest-ai-news roundup has covered the broader agentic-back-office wave all month. What stands out in the KAI launch is that the safety design is not bolted on. Approval gates are enforced outside the model: the agent proposes, the server persists an intent, and a human approves or rejects through an independent channel before any irreversible action executes. That is the exact architecture you will replicate in TypeScript below.

What the payroll-hr-mcp server exposes

The server has two tool families, and the split is deliberate. Read tools are always available to any authenticated agent: payroll overview for the current period, attendance summaries per employee and date range, statutory compliance status per act and deadline, and pay-slip queries that respect a strict row-level access rule. Write tools exist but are useless on their own: running a payslip run or approving a reimbursement requires a scope token, a validated reason, and a human approval outside the model's control. An agent can draft and prepare all day; it can never complete a payment by itself.

This mirrors Kredily KAI's own boundary. The agent layer collects attendance, validates inputs, flags exceptions, and prepares filing payloads; the deterministic engine owns the math and the compliance rules. In our MCP server, the same discipline shows up as a separation between what the model is allowed to propose and what the server is allowed to execute. The workflows library has a matching human-in-the-loop template if you want the LangGraph variant of the same idea.

Architecture

flowchart TD
    A[MCP client agent] -->|OAuth 2.0 bearer token| B[payroll-hr-mcp FastMCP server]
    B --> C{Token validation}
    C -->|invalid| Z[401 token rejected]
    C -->|valid| D{Route request}
    D -->|read tool| E[Payroll data service]
    E --> F[Attendance service]
    E --> G[Compliance status service]
    E --> H[Pay-slip query w/ row-level filter]
    D -->|write tool| I[Intent registry]
    I --> J[Human approval gate]
    J -->|rejected| K[Audit: rejected intent]
    J -->|approved w/ scope token| L[Payroll engine / ledger]
    L --> M[Audit trail]
    F --> M
    G --> M
    H --> M

The request path is deliberately boring. Reads hit stateless data services. Writes never execute directly: they land in an intent registry, become a pending approval, and only the approval channel — operated by a human with the right role — can promote them to execution against the payroll engine.

The TypeScript implementation

The build uses the @modelcontextprotocol/sdk with McpServer and zod. FastMCP semantics are implemented through registerTool, so the server below is a complete, runnable shape for your index.ts.

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

type PayrollContext = {
  tenantId: string;
  role: string;
  scopes: string[];
};

export class PayrollHrMcp {
  private server: McpServer;
  private engine: PayrollEngine;
  private approvals: ApprovalService;
  private audit: AuditTrail;

  constructor() {
    this.engine = new PayrollEngine();
    this.approvals = new ApprovalService();
    this.audit = new AuditTrail();
    this.server = new McpServer({ name: "payroll-hr-mcp", version: "1.0.0" });
    this.registerReadTools();
    this.registerWriteTools();
  }

  private registerReadTools() {
    this.server.registerTool(
      "payroll_overview",
      "Read-only summary of the current payroll period for the calling tenant: gross pay, statutory deductions, and net payable.",
      { period: z.string().describe("Pay period, format YYYY-MM") },
      async ({ period }, extra) => {
        const ctx = this.contextFrom(extra);
        this.audit.log(ctx.tenantId, "payroll_overview", { period }, "read");
        return this.engine.overview(ctx.tenantId, period);
      }
    );

    this.server.registerTool(
      "attendance_summary",
      "Read-only attendance totals for an employee across a date range, with pending-exception count.",
      {
        employeeId: z.string().describe("Employee identifier"),
        from: z.string().describe("ISO start date"),
        to: z.string().describe("ISO end date")
      },
      async ({ employeeId, from, to }, extra) => {
        const ctx = this.contextFrom(extra);
        this.audit.log(ctx.tenantId, "attendance_summary", { employeeId }, "read");
        return this.engine.attendance(ctx.tenantId, employeeId, from, to);
      }
    );

    this.server.registerTool(
      "compliance_status",
      "Read-only statutory compliance status: which filings are due, current progress, and next deadline.",
      { period: z.string().describe("Pay period, format YYYY-MM") },
      async ({ period }, extra) => {
        const ctx = this.contextFrom(extra);
        this.audit.log(ctx.tenantId, "compliance_status", { period }, "read");
        return this.engine.compliance(ctx.tenantId, period);
      }
    );

    this.server.registerTool(
      "get_payslip",
      "Fetch a single employee's payslip. Row-level access: the caller must have payroll:read scope for the tenant and role approval for the employee.",
      { payslipId: z.string().describe("Payslip identifier") },
      async ({ payslipId }, extra) => {
        const ctx = this.contextFrom(extra);
        if (!ctx.scopes.includes("payroll:read")) {
          throw new Error("Missing payroll:read scope");
        }
        this.audit.log(ctx.tenantId, "get_payslip", { payslipId }, "read");
        return this.engine.payslip(ctx.tenantId, payslipId, ctx.role);
      }
    );
  }

  private registerWriteTools() {
    this.server.registerTool(
      "run_payslip",
      "DRAFT a payslip run for a period. This is a write intent: it creates a pending approval and NEVER releases funds without human approval.",
      {
        period: z.string().describe("Pay period, format YYYY-MM"),
        reason: z.string().min(8).describe("Business justification for the run"),
        dryRun: z.boolean().default(true).describe("Compute a preview instead of staging a run")
      },
      async ({ period, reason, dryRun }, extra) => {
        const ctx = this.contextFrom(extra);
        if (!ctx.scopes.includes("payroll:run")) {
          throw new Error("Missing payroll:run scope");
        }
        this.audit.log(ctx.tenantId, "run_payslip", { period }, "write-intent");
        if (dryRun) {
          return this.engine.preview(ctx.tenantId, period);
        }
        return this.approvals.createIntent("payslip_run", ctx.tenantId, { period, reason });
      }
    );

    this.server.registerTool(
      "approve_reimbursement",
      "PREPARE a reimbursement approval for an employee expense claim. Completion requires a human approver through the approval channel.",
      {
        claimId: z.string().describe("Expense claim identifier"),
        decision: z.enum(["approve", "reject"]).describe("Recommended decision"),
        note: z.string().optional().describe("Approver note")
      },
      async ({ claimId, decision, note }, extra) => {
        const ctx = this.contextFrom(extra);
        if (!ctx.scopes.includes("reimburse:approve")) {
          throw new Error("Missing reimburse:approve scope");
        }
        this.audit.log(ctx.tenantId, "approve_reimbursement", { claimId }, "write-intent");
        return this.approvals.createIntent("reimbursement", ctx.tenantId, { claimId, decision, note });
      }
    );
  }

  private contextFrom(extra: any): PayrollContext {
    const token = extra?.session?.auth?.token;
    return decodeAndValidateToken(token);
  }

  async start() {
    const transport = new StdioServerTransport();
    await this.server.connect(transport);
  }
}

Two implementation details are worth calling out. First, every tool resolves the OAuth context from the MCP session before touching any data — the token carries the tenant, role, and scopes that every authorization check reads. Second, the write tools are named and described as intent-creation operations, not as execution operations. That naming is not cosmetic: it keeps the model honest about what the tool actually does, and it keeps the audit trail readable.

inputSchema

The server's tool schemas, expressed as standard JSON Schema, are what the MCP client registers and what the model sees. The shape below is generated from the zod schemas above and is the contract your client agent will validate against.

{
  "payroll_overview": {
    "type": "object",
    "properties": {
      "period": { "type": "string", "description": "Pay period, format YYYY-MM" }
    },
    "required": ["period"],
    "additionalProperties": false
  },
  "attendance_summary": {
    "type": "object",
    "properties": {
      "employeeId": { "type": "string" },
      "from": { "type": "string" },
      "to": { "type": "string" }
    },
    "required": ["employeeId", "from", "to"],
    "additionalProperties": false
  },
  "run_payslip": {
    "type": "object",
    "properties": {
      "period": { "type": "string" },
      "reason": { "type": "string", "minLength": 8 },
      "dryRun": { "type": "boolean", "default": true }
    },
    "required": ["period", "reason"],
    "additionalProperties": false
  }
}

Keep additionalProperties: false everywhere. In a money-moving server, an unvalidated extra field on a write tool is an invitation to smuggling unexpected data into an execution path.

mcpServers config

Any MCP client — Claude Desktop, an IDE, or a custom agent host — connects with this block. The oauth entry points at your authorization server and the scopes the client requests up front.

{
  "mcpServers": {
    "payroll-hr": {
      "command": "node",
      "args": ["dist/index.js"],
      "env": {
        "PAYROLL_AUTH_URL": "https://auth.example.com",
        "PAYROLL_AUDIENCE": "api.payroll.example.com",
        "PAYROLL_TENANT": "acme-corp",
        "PAYROLL_LOG_LEVEL": "info"
      },
      "oauth": {
        "authorization_url": "https://auth.example.com/authorize",
        "token_url": "https://auth.example.com/oauth/token",
        "scopes": ["payroll:read", "payroll:run", "reimburse:approve"],
        "audience": "api.payroll.example.com"
      }
    }
  }
}

Note the scopes are requested up front at handshake. Write scopes are only granted to clients that genuinely need them — a reporting agent should request payroll:read only.

OAuth 2.0 security

OAuth 2.0 is the right boundary for a payroll server because it is not a checkbox — it is an enforcement layer. Three requirements matter in production. First, authorization: the aud (audience) claim in every access token must equal api.payroll.example.com; a token minted for a different API is rejected before any handler runs. Second, scopes: every tool checks the scopes present in the token, not the scopes the client claimed in its config. A client can request payroll:run at handshake and be denied at runtime. Third, credential isolation: the server must never forward a user's token to the payroll engine, and the engine's service account must be a separate principal with its own short-lived token. Each hop authenticates as a different identity, so a leaked token at any layer has a bounded blast radius. PKCE must be enabled for public clients, and refresh tokens must be rotated and stored server-side only.

Retry rules

Payroll endpoints are called in batches, and retries must never double-pay. Follow these rules. Read tools: retry up to three times with exponential backoff (base 200 ms, factor 2, jitter 25%), treating 429 and 503 as retryable and everything 4xx as terminal. Write-intent creation: retry once after 1 second only on a 408 or 5xx; the intent registry is idempotent, keyed by intent ID, so a duplicate create returns the existing intent instead of a new one. Idempotency keys: every write carries one, and the engine rejects a replayed key. Approval calls: zero automatic retries. If an approval submission times out, the operator verifies the intent state before resubmitting — in payroll, at-most-once beats at-least-once every time. Timeouts: reads at 10 seconds, write-intent staging at 15 seconds, approval at 30 seconds.

Frequently Asked Questions

How does the payroll MCP server enforce human approval?

Write tools never execute money movement. They create an intent in the approval registry, and a human with the right role approves or rejects through a separate approval channel. The model can prepare a payslip run but cannot release it.

What scopes does an agent need to read payslips?

The payroll:read scope, plus role-level row access for the specific employee. Payslip queries enforce row-level security inside the data service, not just at the tool boundary.

Can this server work alongside Kredily's KAI?

Yes. The pattern is provider-agnostic: a deterministic payroll engine with an agent orchestration layer. You can point payroll-hr-mcp at Kredily, a legacy payroll engine, or an in-house ledger.

Why are write tools described as intent creation?

Because naming shapes both the model's behavior and the audit trail. A tool that says "create a pending intent" is used to create pending intents; a tool that says "run payslips" invites overconfident execution.

What is the most common production mistake?

Failing to validate the audience claim, or trusting client-declared scopes at runtime. Both leak authorization to anyone who can mint or reuse a token for the wrong API.

Closing thoughts

Kredily's KAI launch on August 14, 2026 made the back office an agentic battleground, and payroll is the category where the rules are strictest. The payroll-hr-mcp server shows the production shape of that idea: read tools that are always available, write tools that are structurally incapable of unilateral action, OAuth 2.0 enforcing tenant, role, and scope at every call, and an audit trail that survives inspection. Build the boundary first, attach the agents second, and the back office will be the safest place you ever run agents.

Executive Briefing

Enjoyed this breakdown? Get our morning dispatch in your inbox.

Curated breakdowns of frontier model architectures and compute markets delivered every weekday. Zero fluff.

Frequently Asked Questions
Write tools never execute money movement. They create an intent in the approval registry, and a human with the right role approves or rejects through a separate approval channel. The model can prepare a payslip run but cannot release it.
The payroll:read scope, plus role-level row access for the specific employee. Payslip queries enforce row-level security inside the data service, not just at the tool boundary.
Yes. The pattern is provider-agnostic: a deterministic payroll engine with an agent orchestration layer. You can point payroll-hr-mcp at Kredily, a legacy payroll engine, or an in-house ledger.
Because naming shapes both the model's behavior and the audit trail. A tool that says 'create a pending intent' is used to create pending intents; a tool that says 'run payslips' invites overconfident execution.
Failing to validate the audience claim, or trusting client-declared scopes at runtime. Both leak authorization to anyone who can mint or reuse a token for the wrong API.
Deepak Bagada
Author Profile

Deepak Bagada

CEO, SaaSNext

Deepak Bagada is the CEO of SaaSNext and founder of Daily AI World. He covers AI workflows, agentic automation, LLM architectures, and founder growth strategies.

Related Intelligence Analysis

Briefing AI Tools

Vercel AI SDK Tool Calling React: 5 Steps (2026)

Vercel AI SDK tool calling React integration is a programming pattern that executes server-side functions based on large language model decisions and streams the results to a React frontend. By combining streamText with...

Deepak Bagada Deepak Bagada
12m read
Breaking AI Tools

Fact-Density vs. Word Count: The New SEO for 2026

Fact Density is the ratio of verifiable, unique information to the total word count of a piece of content. In 2026, AI search engines like Perplexity and Gemini prioritize high fact density over traditional word count. A...

Deepak Bagada Deepak Bagada
4m read
Audio Briefing
Accessibility Preferences
High Contrast Mode
Accessible Reading Font

Keyboard Shortcuts

Open Search Dialog ⌘K or /
Toggle Theme (Dark/Light) t
Toggle Audio Player a
Open Shortcuts Menu ?
Close Active Dialog Esc