Skip to main content
Glama

woffu-adapter-mcp

An MCP server for Woffu: clock in the days you have left, see what time off you have and when it expires, and tell how much of your team is actually around next sprint.

Ask your agent, in whatever words you use:

  • "Which days haven't I clocked in this month?" — then confirm, and it fills them.

  • "How many holidays do I have left, and when do they expire?"

  • "Book me the 14th of August off" — then confirm, and it files the request.

  • "Who is off in August?" / "How much of the team is around next sprint?"

  • "Is Maria away the week of the 20th?"

  • "When is the next public holiday?"

Nothing is written to your timesheet or filed on your behalf without you seeing it first.

Tools

Read-only unless it says otherwise.

Tool

What it answers

list_pending_days(month?)

Workdays with no hours logged, each with the exact time ranges that would be sent. Plus lockedDays: unfilled days Woffu no longer lets you edit.

clock_in_days(dates[], ranges?)

Writes. Clocks in the days you name. Every one is re-checked against Woffu first, and refused with a reason rather than a blanket "not pending".

get_time_off_balance()

What is left in each bucket and when it expires — soonest first, because whatever is left in a bucket is lost on that date.

list_time_off_requests(status?)

Your requests: pending, approved or denied. Each carries its requestId, which is what edit and cancel take.

request_time_off(type, from, to?, startTime?, endTime?, comment?)

Writes. Files a request for a leave type, validated against Woffu first. Whole-day types take a date range; hourly types take a day and a time span.

edit_time_off_request(requestId, from?, to?, startTime?, endTime?, comment?)

Writes. Changes a request's dates, times or comment. The leave type cannot change — cancel and file anew for that.

cancel_time_off_request(requestId)

Writes. Withdraws a request; a pending one is deleted and its balance returned.

list_holidays(from?)

The public holidays ahead. One costs no days from any bucket, and one next to a weekend makes a long weekend.

get_team_availability(from?, to?, team?, person?)

How many days each teammate is around over a range, and how many they are off. For sizing a sprint before committing to it.

Clocking in

clock_in_days needs an explicit list of dates — there is no "clock in everything", so you always see what is about to be logged. ranges overrides the schedule when you want specific hours ([{"in": "09:00", "out": "13:30"}, ...]); omit it and each day uses the theoretical schedule Woffu itself proposes, which is nearly always what you meant. A weekend, a public holiday, an absence, a day already logged or one Woffu has locked is refused by name, so you can tell which of those it was.

Working a public holiday is possible in Woffu, but only by filing a "work on a holiday" request. That is not implemented, so holidays are simply refused.

Requesting time off

request_time_off files a request for a leave type by name — the same names get_time_off_balance lists ("Vacaciones", "Horas Ajuste Calendario"...). Whole-day types take from and an optional to; the day count is whatever Woffu charges after its own weekends and holidays, so a Friday-to-Monday range costs two days, not four. Hourly types take a single from with startTime and endTime instead.

Every request is checked against Woffu before anything is written: an overdrawn balance, a weekend or a public holiday is refused with a reason rather than filed. Types that need a supporting document, and half-day requests, are out of scope — file those in Woffu directly.

edit_time_off_request and cancel_time_off_request both take a requestId from list_time_off_requests. Editing changes dates, times or the comment; the leave type is fixed, so to change that you cancel and file a new one. Cancelling a pending request deletes it and returns the balance it held.

Team availability

Per person: workingDays in the range (weekends and public holidays already out), daysOff already approved, pendingDaysOff still awaiting approval, and availableDays — working days minus approved time off. totals carries the whole team's capacity in person-days. Least-available first, since those are the people who constrain the sprint.

Pending absences are not subtracted from availableDays: they are the risk, not the plan, so they are reported next to it. Woffu does not count them as absent either.

It reads your own team by default — no id, no name, nothing to configure. Name another team to look at it instead, or a person to narrow the answer to one of them.

Related MCP server: woffu-mcp

Install

Nothing to download. The server is published to npm, so npx fetches and runs it — the package name is the whole prerequisite:

claude mcp add woffu \
  -e WOFFU_HOST=yourcompany.woffu.com \
  -e WOFFU_USERNAME=you@company.com \
  -e WOFFU_PASSWORD='...' \
  -- npx -y woffu-adapter-mcp

npx -y woffu-adapter-mcp always pulls the latest published version. Pin it if you would rather it did not move under you: woffu-adapter-mcp@1.0.0 takes any published version.

Configuration

Variable

Required

Example

What it is

WOFFU_HOST

Yes

yourcompany.woffu.com

Your Woffu domain. Paste the whole URL if it is easier; only the host is kept.

WOFFU_USERNAME

Yes

you@company.com

Your Woffu login.

WOFFU_PASSWORD

Yes

********

Used once, to request a token. Never stored.

WOFFU_TEAM

No

platform

Only to make get_team_availability default to a team other than your own. Your own is read off your profile.

WOFFU_TZ

No

Europe/Madrid

The office time zone, any IANA name. It decides which instant a 09:00 clock-in lands on. Defaults to Europe/Madrid.

The password is only used to request a token and is never stored. The token is cached, in ~/.woffu-manager/token.json (mode 600), and Woffu issues it with a one-year lifetime — so logging in happens once. Delete that file to force a new login.

GitHub Copilot, in VS Code

Put this in .vscode/mcp.json (or the user-level one, via MCP: Open User Configuration), then hit the Start action the file shows above the entry. Copilot says servers, not mcpServers, and a local server has to declare "type": "stdio".

The password goes in as an input, so VS Code asks once, masks it and keeps it in the OS keychain — which means the file can be committed with the rest of the project without a password in it:

{
  "inputs": [
    {
      "type": "promptString",
      "id": "woffu-password",
      "description": "Woffu password",
      "password": true
    }
  ],
  "servers": {
    "woffu": {
      "type": "stdio",
      "command": "npx",
      "args": ["-y", "woffu-adapter-mcp"],
      "env": {
        "WOFFU_HOST": "yourcompany.woffu.com",
        "WOFFU_USERNAME": "you@company.com",
        "WOFFU_PASSWORD": "${input:woffu-password}"
      }
    }
  }
}

The tools show up under the 🛠️ icon in Copilot Chat, in Agent mode.

Any other MCP client

{
  "mcpServers": {
    "woffu": {
      "command": "npx",
      "args": ["-y", "woffu-adapter-mcp"],
      "env": {
        "WOFFU_HOST": "yourcompany.woffu.com",
        "WOFFU_USERNAME": "you@company.com",
        "WOFFU_PASSWORD": "..."
      }
    }
  }
}

Clone it only to work on it:

git clone git@github.com:alechagonzalo/woffu-adapter-mcp.git
cd woffu-adapter-mcp
npm install && npm test

What counts as "pending"

Two layers, because neither is enough on its own:

  1. From the month summary: weekends, holidays, events, absences (absenceEvents / pendingAbsenceEvents: leave, adjustment days, sick leave), future days and days with hours already worked are all excluded. isUserEditable is the hard gate — Woffu refuses writes wherever it is false.

  2. From the day's slots: a day that was clocked in but not yet confirmed still shows zero hours in the summary. What gives it away is a real signId on its slots. Without this second check the same day would be clocked in twice.

Today is included, even mid-workday: its full theoretical schedule gets logged.

The Woffu API, in case it needs revisiting

  • POST /api/svc/accounts/authorization/token — OAuth2 login (grant_type=password). Returns accessToken (a JWT carrying the UserId, which is where we read it from) and expiresIn.

  • GET /api/svc/core/diariesquery/users/{id}/diaries/summary/presence — days in a month.

  • GET /api/svc/core/diariesquery/diarysummaries/{diarySummaryId}/workday/slots/self — a day's exact time ranges. The summary only gives first entry, last exit and total break; it never says where the break falls. Only this endpoint knows a workday is 09:00–13:30 plus 14:00–17:53. The slots come back with signId: 0: they are the proposal the UI preloads, not actual clocked time.

  • PUT /api/svc/core/users/{id}/diarysummaries/workday/slots/self — saves. Times travel in UTC, signType goes from 1 (proposal) to 3 (manual clock-in, which is how Woffu displays them), and signStatus to 1. It answers 200 with an empty body.

  • GET /api/users/calendar-events/next — upcoming holidays. Type is 2 on every one seen so far; other values are surfaced rather than assumed to be holidays.

  • GET /api/user-agreement-events/availability — time-off balance per bucket. Carries no dates.

  • POST /api/users/allocations/row — the allocations behind one bucket, and the only place the expiry date lives. Body: {agreementEventId, pageIndex, pageSize, createdOnFilter: {filterType: "CurrentCycle"}, affectedCycleFilter: "CurrentCycle"}. The cycle filter matters: /allocations/table reports across cycles and disagrees with the availability endpoint (12 days vs 9), while the current cycle agrees.

  • GET /api/users/{id} — the profile. DepartmentId is the user's team, and it is the same number the calendar wants as an EntityId, so nobody has to configure their own team. DepartmentName comes back empty; only the calendar spells the name out.

  • GET /api/scope/search?query=&types=999678 — resolves a team name to the {EntityId, EntityType} pair the calendar wants; a team answers with EntityType 32. types is a bitmask over the scopes the calendar's own search box asks for.

  • POST /api/svc/core/calendar — the shared calendar: one entry per person, one per day, already clipped to the range. visibilities is not optional: with an empty list Woffu answers with the whole company rather than the team. keyword matches a person's name. requestStates: 3 brings back approved and pending absences. No total comes back, so paging runs until a short page. eventDuration / absenceEventsDuration are an enum, not a count of days: 1 is a full day and 2 is an hours-only absence (isTimeOnly: true, startDate "09:00" rather than a date). Summing them books a half-day as two days off — isTimeOnly is what tells them apart. Absences do land on weekends and holidays, and cost nobody a day. presenceEvents is telework: the person is still available.

  • POST /api/svc/core/calendar/totals-simple — presents/absents per day. Unused: it counts every weekend as "everyone absent". The per-person calendar above agrees with it exactly on working days, once the same absences are counted.

  • GET /api/users/requests/list?statusType= — time-off requests. statusType is 10 pending, 20 approved, 30 denied; it is required, so all three are fetched to list everything. Amounts arrive as {Resource, Values}, where the resource name is the only hint at the unit, and hours come split as ["8", "30"] meaning 8h30m.

scripts/inspect.mjs /api/some/endpoint dumps any endpoint using the cached token — useful when reverse-engineering a new surface.

npm test covers the filters, the day-counting rules, and that the payload matches, field for field, the one the web app sends.

Install Server
A
license - permissive license
A
quality
C
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/alechagonzalo/woffu-adapter-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server