Warning
Writing to timesheets is turned off by default. If you decide to change this, be very careful, and always keep a human in the loop. These tools can add, edit, delete, and submit real time entries on a live government-contract ERP. An AI assistant can misread a request or pick the wrong project, date, or hours. Before submitting any timesheet, review it yourself in Unanet and confirm every entry is correct. Submission is one-way and locks the timesheet. Never let an agent submit on your behalf without your own final check.
This fork lets Claude Desktop talk to Nava's Unanet GovCon instance through the Model Context Protocol (MCP). It has been substantially updated from the original API-key design to use Unanet Platform REST: username/password login, short-lived bearer tokens, and safe-by-default tool registration.
The happy path is macOS + Claude Desktop.
By default, the server exposes one safe read-only tool:
unanet_get_my_leave_balances— reads your Unanet leave balances with minimized output.
Example in Claude Desktop:
With explicit opt-in, the server can also expose additional Platform REST read tools:
unanet_get_projectsunanet_get_project_detailsunanet_get_project_statusunanet_get_timesheetsunanet_get_my_timesheet_projectsunanet_get_company_infounanet_get_billing_statusunanet_get_financial_report
Write-capable tools are gated behind a separate flag. Some are intentionally disabled or require confirmation until their Platform REST write flow is safe enough for routine use.
Install Node.js 20 or newer from:
https://nodejs.org/
Choose the current LTS installer for macOS.
If you use Git:
git clone https://github.com/navapbc/unanet-mcp-server.git
cd unanet-mcp-serverIf you downloaded a ZIP, unzip it and open Terminal in the project folder.
./setup-mac.shThe script will:
- Check Node.js/npm.
- Install dependencies.
- Build the server.
- Create a local
.envfile if needed. - Add this MCP server to Claude Desktop's config.
Your password stays in the local .env file. It is not written into Claude Desktop's config.
Fully quit and reopen Claude Desktop.
Then ask Claude:
Show my Unanet leave balances.
If Claude can use the tool, the install worked.
Use this if you prefer to see each step.
cd /Users/YOU/path/to/unanet-mcp-server
npm install
npm run buildcp .env.example .envEdit .env:
UNANET_BASE_URL=https://navapbc.unanet.biz
UNANET_USERNAME=your-username
UNANET_PASSWORD=your-password
UNANET_APP_NAME=NavaUnanetMCP
# Safe default: only the leave-balance tool is exposed.
UNANET_ENABLE_LEGACY_READ_TOOLS=false
UNANET_ENABLE_WRITE_TOOLS=falseTo expose additional read tools:
UNANET_ENABLE_LEGACY_READ_TOOLS=trueTo expose write-capable tools:
UNANET_ENABLE_WRITE_TOOLS=trueOnly enable write tools when you intentionally want Claude to see them. Timesheet writes require confirm: true; several other legacy write tools still fail closed until they have safe Platform REST implementations.
Edit:
~/Library/Application Support/Claude/claude_desktop_config.json
Add or merge this entry:
{
"mcpServers": {
"unanet": {
"command": "/bin/bash",
"args": [
"-lc",
"cd /absolute/path/to/unanet-mcp-server && node dist/index.js"
]
}
}
}Why use bash -lc? The server loads .env from the project directory, so Claude needs to start it from that folder.
Restart Claude Desktop after changing the config.
| Variable | Required | Default | Purpose |
|---|---|---|---|
UNANET_BASE_URL |
Yes | https://navapbc.unanet.biz |
Unanet tenant origin. Do not include /platform/rest. |
UNANET_USERNAME |
Yes | — | Your Unanet username. |
UNANET_PASSWORD |
Yes | — | Your Unanet password. |
UNANET_APP_NAME |
No | NavaUnanetMCP |
Value sent in the X-Una-App header. |
UNANET_ENABLE_LEGACY_READ_TOOLS |
No | false |
Enables additional read tools/resources. Name is historical. These now use Platform REST bearer auth. |
UNANET_ENABLE_WRITE_TOOLS |
No | false |
Enables mutating tools. Use carefully. |
UNANET_ALLOWED_BASE_URLS |
No | Nava tenant only | Comma-separated allow-list for additional production origins. |
UNANET_ALLOW_INSECURE_LOCAL_MOCK |
No | false |
Allows http://localhost / 127.0.0.1 for local mock testing only. |
LOG_LEVEL |
No | info |
Reserved for logging behavior. |
Legacy UNANET_API_KEY and UNANET_FIRM_CODE are no longer needed for the Platform REST tools in this fork.
| Tool | Description |
|---|---|
unanet_get_my_leave_balances |
Read your leave balances for a date range. |
unanet_get_my_expenses |
Read your expense reports and minimized line-item details. |
unanet_get_personal_development_fund_status |
Estimate personal/professional-development spending from expense reports; remaining balance is calculated only when a budget is provided. |
unanet_get_my_expense_report |
Read one expense report, optionally with validation. |
unanet_get_expense_reference_data |
Read expense types, payment methods, and currency codes needed to draft reports. |
unanet_validate_expense_report |
Validate a draft expense report without submitting it. |
unanet_get_expense_attachments |
List the receipt attachments on one of your expense reports (metadata only). |
Requires:
UNANET_ENABLE_LEGACY_READ_TOOLS=true| Tool | Description |
|---|---|
unanet_get_projects |
Search projects with optional filters. |
unanet_get_project_details |
Retrieve a project by key/id. |
unanet_get_project_status |
Retrieve project status-style summary data. |
unanet_get_timesheets |
Search your timesheets by date range, including entry-level timeslip summaries. |
unanet_get_my_timesheet_projects |
List projects available to charge on your timesheet for a given date. |
unanet_get_company_info |
Retrieve organization/company details. |
unanet_get_billing_status |
Retrieve billing-adjacent project invoice setup/account data. |
unanet_get_financial_report |
Limited invoice-search-backed financial reporting. |
Requires:
UNANET_ENABLE_WRITE_TOOLS=true| Tool | Current behavior |
|---|---|
unanet_update_timesheet |
Live write; adds a time entry (one row per project/day) to your in-progress timesheet. Does not submit for approval, does not overwrite existing rows, and rejects a second entry for a project/day that already has one (use edit). Requires confirm: true. |
unanet_edit_timeslip |
Live write; changes the hours and/or comment of an existing entry in place. Identify by timeslipKey, or projectId + date; ambiguous matches return candidates instead of guessing. Requires confirm: true. |
unanet_delete_timeslip |
Live write; clears an entry (sets it to 0 hours and removes the comment). Unanet has no true row delete, so the project may still appear on the sheet with no hours. Requires confirm: true. |
unanet_submit_timesheet_for_approval |
Live write; validates the timesheet, then submits it for approval. Refuses on errors; holds on warnings unless ignoreWarnings: true. One-way action — requires confirm: true. |
unanet_create_expense_report_draft |
Live write; creates a draft expense report shell with explicit project allocations. Does not submit. Requires confirm: true. |
unanet_add_expense_detail |
Live write; adds one line item to a draft expense report using explicit expense type, payment method, currency, and wizard keys. Does not submit. Requires confirm: true. |
unanet_add_expense_attachment |
Live write; uploads a receipt to a draft report from a local filePath or inline base64 data, optionally associating it with a line via detailId. MIME type is inferred from the extension; 25 MB cap; INUSE-only. After uploading it reads the attachment list back and returns verified: true/false (Unanet's write responses are unreliable, so it confirms the file actually landed rather than trusting the POST). Does not submit. Requires confirm: true. |
unanet_update_project_budget |
Disabled/fail-closed; Platform REST requires a full project update payload. |
unanet_update_lead |
Disabled/fail-closed; no Platform REST lead endpoint identified. |
unanet_create_opportunity |
Disabled/fail-closed; no Platform REST opportunity endpoint identified. |
unanet_generate_invoice |
Disabled/fail-closed; no generic generate endpoint identified. |
unanet_approve_timesheet |
Live write; requires confirm: true. |
unanet_create_contact |
Live write; requires confirm: true and an organization id. |
This fork uses two clients in src/auth.ts:
createPlatformRestClient— raw Platform REST client used for login and the leave tool.createUnanetClient— Platform REST client that auto-prefixes/platform/rest, fetches/caches a bearer token, and addsAuthorization: Bearer <token>.
Authentication flow:
POST /platform/rest/login
→ cache token in memory
→ call /platform/rest/... with Authorization: Bearer <token>
Security choices:
.envis ignored..env.testis no longer tracked.- Production base URLs must be HTTPS and allow-listed.
- Local insecure mock URLs require explicit opt-in.
- Tokens are cached in memory only.
- Default Claude tool surface is read-only.
npm install
npm run build
npm test
npm run mock-serverProject layout:
src/
├── index.ts # MCP server entry point
├── auth.ts # Platform REST auth, base URL validation, token cache
├── mock-server.ts # Local mock Unanet API for tests
├── tools/
│ ├── leave.ts # Default safe read-only leave-balance tool
│ ├── projects.ts # Project tools
│ ├── timesheet.ts # Timesheet/expense tools
│ ├── contacts.ts # Organization/contact tools
│ └── financials.ts # Billing/financial tools
├── resources/
│ └── reports.ts # Optional MCP resources
└── types/
└── unanet.ts # Shared types
-
Fully quit and reopen Claude Desktop.
-
Confirm the server builds:
npm run build
-
Confirm the Claude config points at the right folder:
~/Library/Application Support/Claude/claude_desktop_config.json -
Confirm
.envexists in the project folder.
Check:
UNANET_BASE_URL=https://navapbc.unanet.bizUNANET_USERNAMEis setUNANET_PASSWORDis set- You can log into Unanet normally with the same credentials
The server intentionally rejects arbitrary production hosts. If you need another Unanet tenant, add it explicitly:
UNANET_ALLOWED_BASE_URLS=https://other-tenant.unanet.bizThis fork is primarily used on macOS at Nava. A Windows helper script still exists:
setup-windows.bat
It is best-effort and less exercised than the Mac setup path.
MIT License. See LICENSE for details.
