easyJOB MCP Server
README.md
# easyJOB Model Context Protocol (MCP) Server
A clean, stable, and highly configurable Model Context Protocol (MCP) server for the **easyJOB REST API**.
This server manages dynamic OAuth authentication token fetching and caching, simplifies complex multi-query workflows (such as looking up user and job IDs to record time), and provides full read/write access to easyJOB via generic endpoints.
---
## Safety model
This server is normally pointed at a **production** easyJOB instance holding real
agency data, so it is deliberately narrow in what it can change:
- The only writes it performs are **time recordings**: booking hours and correcting
your own entries. Nothing else is created, restructured or deleted.
- `update_time_record` verifies ownership against `EASYJOB_USERNAME` and refuses to
touch anyone else's timesheet. The API token itself can write other people's
records, so this guard lives in the server rather than in the caller.
- `track_time_batch` **defaults to a dry run**: it validates and returns a preview
without writing. A batch is applied all-or-nothing, so it never lands half-done.
- Identical entries on the same day for the same job are refused as duplicates
unless `allow_duplicate` is set explicitly.
- The generic `create_or_update_entity` and `delete_entity` tools can touch any
record, so they are **hidden and blocked** unless you set
`EASYJOB_ENABLE_GENERIC_WRITES=1`. Everything above works without them.
- Query failures are detected properly: easyJOB signals them with HTTP 200, either
as an error envelope or as an empty body (see `API-NOTES.md`), which previously
could turn a typo into a silently empty report.
---
## Features
**Automated token management** — fetches and caches the OAuth bearer token, retries
transient network failures, and refreshes automatically when the API rejects a token.
### Analysis (read-only)
| Tool | Purpose |
|---|---|
| `search_jobs` | Find jobs by number, name, short name or customer; shows whether each accepts bookings |
| `get_job_details` | Complete job context in one call: header, customer, positions and hours booked per position |
| `list_job_activities` | Positions (Leistungen) with budget, booked and remaining hours — how you pick a valid `activity_id` |
| `job_budget_report` | Budget vs. actual per position, remaining budget, burn rate, hours per contributor |
| `list_time_records` | The individual records booked in a date range |
| `timesheet_report` | Totals grouped by day, job, activity or customer, plus working days with no bookings |
| `team_hours_report` | Hours per employee, optionally against recorded attendance (Arbeitszeit) |
| `list_absences` | Who is absent in a range, with type and days |
| `get_user_id` | Internal user ID of the configured login |
| `query_entities` | Escape hatch for arbitrary `FetchsonQuery` reads |
### Time recording (write)
| Tool | Purpose |
|---|---|
| `track_time` | Book hours on one job, with validation and duplicate protection |
| `track_time_batch` | Validate and preview several entries, then book them after confirmation |
| `update_time_record` | Correct one of **your own** records (hours, description, date, times, position) |
Budgets are read from each position's `EstimateQuantity`. Quantities in hours
(`Std.`, `h`) are used directly, quantities in days (`Tag(e)`, `Tage`, `AT`) are
converted at `EASYJOB_HOURS_PER_DAY` (default 8). Units such as `pauschal`,
`Stck.` or `Monat(e)` are not time budgets and are reported as `budgetHours: null`
alongside their raw quantity.
---
## `easyjob-sync` — booking Super Productivity time into easyJOB
[Super Productivity](https://github.com/super-productivity/super-productivity) (MIT,
macOS/Windows/Linux) serves as the local tracker. Its Local REST API only listens on
`127.0.0.1`, so the bridge is a **CLI that runs on your own machine** — the
containerised MCP server cannot reach it. Both share the same easyJOB client.
### One-time setup
1. In Super Productivity: **Settings → Misc → "Enable local REST API"**.
2. List what the tracker knows and map each project to an easyJOB job and position:
```bash
node --env-file=.env bin/easyjob-sync.js status
node --env-file=.env bin/easyjob-sync.js projects
node --env-file=.env bin/easyjob-sync.js map <spProjectId> --job UNI_2604_131 --activity 43966
node --env-file=.env bin/easyjob-sync.js map-tag <spTagId> --activity 43969 # tag overrides the position
node --env-file=.env bin/easyjob-sync.js map <spProjectId> --ignore # never book this project
```
The mapping lives in `~/.config/easyjob-sync/config.json` (override with
`EASYJOB_SYNC_CONFIG`). Super Productivity has no field for foreign keys, which is
why the mapping is kept here rather than in the tracker.
### Daily use
```bash
node --env-file=.env bin/easyjob-sync.js preview --from 2026-07-01 --to 2026-07-31
node --env-file=.env bin/easyjob-sync.js push --from 2026-07-01 --to 2026-07-31 --yes
```
`preview` never writes. `push` refuses to write without `--yes`, validates every
target job and position first, and aborts before writing anything if one is invalid.
### How the mapping works
- Time comes from each task's `timeSpentOnDay`, the finest granularity the tracker
stores. Entries are grouped per **day + job + position**, so one day on one
position becomes exactly one easyJOB record, with the task titles as description.
- **Rounding** happens once per group (default 15 minutes, `roundToMinutes`), never
per task, so several small tasks cannot inflate a day. The preview shows both the
rounded and the exact figure.
- Task-days below `minMinutes` (default 5) are dropped as tracking noise and listed
as skipped.
- **Re-running is safe.** Each record carries
`ExternalReference = sp:<date>:<jobId>:<activityId>` plus
`ThirdPartyApp = super-productivity`, and existing references are recognised and
skipped rather than booked twice.
- Projects without a mapping are **never** booked; they are reported with their
hours so you can decide.
### Time unit
Super Productivity does not document whether its API reports milliseconds or
seconds, and being wrong by a factor of 1000 would book nonsense. The unit is
therefore derived from the data (a single day cannot exceed 86400 seconds, and the
app ticks in whole seconds). If the range is too small to tell, the tool **stops**
and asks you to set `timeUnit` to `"ms"` or `"s"` in the config rather than guessing.
The detected unit and the reason are printed on every run.
---
## Installation & Setup
### 1. Install Dependencies
Run the following command inside this directory to install the `@modelcontextprotocol/sdk`:
```bash
npm install
```
### 2. Configuration (Environment Variables)
The server reads its configuration from environment variables. You can set these in your terminal, a `.env` file (if running locally/testing), or directly in your MCP client's configuration (e.g., Claude Desktop).
| Environment Variable | Description |
|---|---|
| `EASYJOB_API_BASE_URL` | **Required.** The absolute URL to the easyJOB app (e.g., `https://drid.because.cloud/drid/app`). |
| `EASYJOB_USERNAME` | The easyJOB login name (used for password grant token lookup & user ID resolution). |
| `EASYJOB_PASSWORD` | The easyJOB password (used for password grant token lookup). |
| `EASYJOB_CLIENT_ID` | OAuth Client ID (if using client credentials flow or required for password flow). |
| `EASYJOB_CLIENT_SECRET` | OAuth Client Secret (if using client credentials flow). |
| `EASYJOB_TOKEN_URL` | Optional. Custom OAuth Token URL (defaults to `${EASYJOB_API_BASE_URL}/oauth/token`). |
| `EASYJOB_API_TOKEN` | Optional. A static, long-lived bearer token (bypasses automatic auth flows). |
| `EASYJOB_HOURS_PER_DAY` | Optional. Factor for converting day-based position budgets into hours (default `8`). |
| `EASYJOB_ENABLE_GENERIC_WRITES` | Optional. Set to `1` to unlock `create_or_update_entity` and `delete_entity`. Leave unset on production. |
---
## Integration in Claude Desktop
To configure the server in Claude Desktop, edit your `claude_desktop_config.json` configuration file:
**On Linux/macOS:**
`~/.config/Claude/claude_desktop_config.json` or `~/Library/Application Support/Claude/claude_desktop_config.json`
Add the server config:
```json
{
"mcpServers": {
"easyjob": {
"command": "node",
"args": ["/absolute/path/to/jolly-hypatia/index.js"],
"env": {
"EASYJOB_API_BASE_URL": "https://drid.because.cloud/drid/app",
"EASYJOB_USERNAME": "your_username",
"EASYJOB_PASSWORD": "your_password",
"EASYJOB_CLIENT_ID": "optional_client_id",
"EASYJOB_CLIENT_SECRET": "optional_client_secret"
}
}
}
}
```
Restart Claude Desktop after editing the configuration.
---
## Available Tools
### 1. `get_user_id`
- **Description:** Get the internal easyJOB User ID for the user configured in the environment (`EASYJOB_USERNAME`).
- **Arguments:** None. (Resolves strictly to the authenticated user context for security).
### 2. `search_jobs`
- **Description:** Search for jobs by name, short name, or job number.
- **Arguments:**
- `query` (string, required): Wildcard search term.
### 3. `track_time`
- **Description:** Log working hours on a specific job in easyJOB. Creates a TimeRecording entry strictly for the configured user timesheet.
- **Arguments:**
- `job_id` (integer, required): Internal ID of the Job.
- `amount` (number, required): Time in hours (e.g., `1.5` or `0.25`).
- `description` (string, required): Work description.
- `date` (string, optional): Date in `YYYY-MM-DD` format (defaults to today).
- `time_recording_type_id` (integer, optional): Custom work type ID (from Activity/Leistung table).
### 4. `query_entities`
- **Description:** Run a custom FetchsonQuery.
- **Arguments:**
- `query` (object, required): A FetchsonQuery JSON definition.
### 5. `create_or_update_entity`
- **Description:** Create or update any entity in easyJOB.
- **Arguments:**
- `entity` (string, required): Entity name (e.g., `'Task'`).
- `data` (object, required): Entity fields and values.
- **Note:** Creating or updating `TimeRecording` entities via this tool is disabled for security. Use `track_time` instead.
### 6. `delete_entity`
- **Description:** Delete an entity by ID.
- **Arguments:**
- `entity` (string, required): Entity name.
- `id` (integer, required): Target entity ID.
---
## Available Prompts
### 1. `import-hours`
- **Description:** A guided prompt template to help the AI dry-run map, verify, and import a batch of working hours (timesheet) into easyJOB.
- **Arguments:**
- `hoursList` (string, required): The list or table of working hours to log.
---
## Available Resources
### 1. `easyjob://docs/skill`
- **Description:** The complete integration guide and safety protocols (content of `SKILL.md`) read dynamically from disk.
### 2. `easyjob://user/profile`
- **Description:** Dynamically retrieves the profile, department details, and status of the currently authenticated user from the easyJOB database.
TDQS
A3.7/5.0
Scored across 6 tools
Disambiguation4/5
Most tools have distinct purposes (track_time vs. generic CRUD, search_jobs, etc.), but the generic create_or_update_entity could cause slight confusion when a specialized tool like track_time exists for similar entities.
Naming Consistency5/5
All tool names follow a consistent verb_noun pattern using lowercase snake_case, e.g., create_or_update_entity, delete_entity, query_entities, search_jobs, track_time.
Tool Count5/5
Six tools cover the core domain (job management, time tracking) without being excessive or insufficient; each tool serves a clear purpose.
Completeness3/5
Generic CRUD tools cover basic operations but lack specialized tools for updating jobs or managing users; the set works but has notable gaps for a full workflow.
Maintenance
ActivitySlowing
ResponsivenessNo issues