BambooHR MCP
by aakarsh1t
README.md
# BambooHR MCP (Admin)
An administrative [Model Context Protocol](https://modelcontextprotocol.io) server for BambooHR, in Node.js and TypeScript.
It runs in two modes from the same codebase:
- **Streamable HTTP** — a remote server you host, for **Microsoft Copilot Studio** and any other remote MCP client.
- **stdio** — a local process, for Claude Desktop, VS Code, and other desktop MCP clients.
35 tools cover the employee directory, employee records and their historical tables, hiring and the applicant pipeline, employee and company documents, time-off requests and balances, reports, BambooHR user accounts, and time tracking. Record-changing tools are off by default.

---
## Quick start
```sh
git clone https://github.com/encoreshao/bamboohr-mcp.git
cd bamboohr-mcp
npm install
cp .env.example .env # fill in BAMBOOHR_TOKEN and BAMBOOHR_COMPANY_DOMAIN
npm run build
npm start # HTTP on http://0.0.0.0:3000/mcp
```
Check it is alive:
```sh
curl http://localhost:3000/health
```
For a local desktop client instead:
```sh
npm run start:stdio
```
---
## Configuration
Everything is environment-driven. See [.env.example](.env.example) for the annotated list.
| Variable | Required | Default | Purpose |
| --- | --- | --- | --- |
| `BAMBOOHR_TOKEN` | yes | — | API token. Carries exactly the permissions of the user who created it. |
| `BAMBOOHR_COMPANY_DOMAIN` | yes | — | Subdomain of your BambooHR URL (`acme` in `acme.bamboohr.com`). |
| `BAMBOOHR_EMPLOYEE_ID` | no | — | Default employee for self-service tools. |
| `MCP_API_KEY` | for public deploys | — | Shared secret required as `X-API-Key`. Unset means **no authentication**. |
| `BAMBOOHR_ENABLE_WRITES` | no | `false` | Master switch for every record-changing tool. |
| `BAMBOOHR_ALLOWED_TOOLS` | no | all | Comma-separated allowlist of tool names. |
| `BAMBOOHR_ALLOW_TOKEN_HEADER` | no | `false` | Let callers pass their own token via `X-BambooHR-Token`. |
| `BAMBOOHR_MAX_RESPONSE_BYTES` | no | `350000` | Cap on a single tool response. |
| `MCP_TRANSPORT` | no | `http` | `http` or `stdio`. `--stdio` on the command line does the same. |
| `PORT` / `HOST` | no | `3000` / `0.0.0.0` | HTTP bind address. |
### Creating the API token
1. Log in to BambooHR and open your profile menu (bottom-left).
2. Choose **API Keys** → **Add New Key**.
3. Name it (e.g. "Copilot Studio agent") and click **Generate Key**.
4. Copy it immediately — BambooHR shows it once.
**The key inherits the access level of the user who created it.** A key made by a full admin can read compensation and terminate employees. Create it as a user whose permissions match what the agent should be able to do — that BambooHR user is your real permission boundary, not this server.
---
## Connecting to Copilot Studio
Copilot Studio reaches MCP servers over **Streamable HTTP only** — it cannot launch a local stdio process. So the server has to be hosted somewhere with a public HTTPS address.
### 1. Deploy
CI/CD to Azure App Service is wired up — see [Deployment](#deployment) below. To run it locally instead, a `Dockerfile` is included:
```sh
docker build -t bamboohr-mcp .
docker run -p 3000:3000 --env-file .env bamboohr-mcp
```
Set the environment variables as secrets wherever you host it — never bake the token into an image.
### 2. Create the custom connector
[connector/copilot-studio-connector.yaml](connector/copilot-studio-connector.yaml) is a ready Swagger 2.0 definition. Replace the `host` line with your deployed hostname, then:
1. Go to your agent's **Tools** page → **Add a tool** → **New tool** → **Custom connector**.
2. In Power Apps, choose **New custom connector** → **Import OpenAPI file** and select the YAML.
3. On the **Security** step, the definition declares an API key in the `X-API-Key` header. Supply the value of your `MCP_API_KEY` when you create the connection.
4. **Create connector**, then add it to your agent.
The critical line is `x-ms-agentic-protocol: mcp-streamable-1.0` on the `POST /mcp` operation — that is what tells Copilot Studio to speak MCP rather than treat the endpoint as a plain REST action.
Copilot Studio's MCP onboarding wizard is the alternative route and takes the same URL and header.
---
## Connecting a local MCP client
```json
{
"mcpServers": {
"bamboohr": {
"command": "node",
"args": ["/absolute/path/to/bamboohr-mcp/dist/index.js", "--stdio"],
"env": {
"BAMBOOHR_TOKEN": "your_api_token_here",
"BAMBOOHR_COMPANY_DOMAIN": "yourcompany",
"BAMBOOHR_ENABLE_WRITES": "false"
}
}
}
}
```
---
## Tools
Read-only tools (always available):
| Tool | What it does |
| --- | --- |
| `bamboohr_search_employees` | Search the directory by name, email, title, department, or location. Paged. |
| `bamboohr_get_employee` | One employee's record, with an optional explicit field list. |
| `bamboohr_list_employee_fields` | Every field id in the account — discover before reading or writing. |
| `bamboohr_list_field_options` | Allowed values for list fields (department, division, location…). |
| `bamboohr_list_tables` | Historical tables available on employee records. |
| `bamboohr_get_employee_table` | Rows of one table: job history, compensation, employment status. |
| `bamboohr_list_users` | BambooHR user accounts and access levels — for access reviews. |
| `bamboohr_get_changed_employees` | Records inserted/updated/deleted since a timestamp. |
| `bamboohr_whos_out` | Who is out over a date range, company-wide. |
| `bamboohr_list_time_off_requests` | Requests filtered by range, status, employee, type — the approval queue. |
| `bamboohr_get_time_off_balances` | Projected balances for an employee as of a date. |
| `bamboohr_list_time_off_types` | Configured time-off types. |
| `bamboohr_list_time_off_policies` | Accrual policies. |
| `bamboohr_run_report` | Run a saved company report by ID. |
| `bamboohr_run_custom_report` | Ad-hoc report over any field list. |
| `bamboohr_list_projects` | Time-tracking projects and tasks. |
| `bamboohr_get_timesheet_entries` | Timesheet entries over a date range, one or many employees. |
| `bamboohr_list_job_openings` | Job openings with applicant counts. |
| `bamboohr_list_applications` | The candidate pipeline, filtered by job, status, or search. |
| `bamboohr_get_application` | Full detail of one application, including answers and status history. |
| `bamboohr_list_applicant_statuses` | Configured applicant statuses and their IDs. |
| `bamboohr_list_employee_files` | An employee's document categories and file metadata. |
| `bamboohr_list_company_files` | Company-wide document categories and files. |
| `bamboohr_download_employee_file` | Download a document, base64-encoded. Small files only. |
| `bamboohr_server_info` | How this server is configured and which tools it exposes. |
Write tools (**only registered when `BAMBOOHR_ENABLE_WRITES=true`**):
| Tool | What it does |
| --- | --- |
| `bamboohr_create_employee` | Create an employee record. |
| `bamboohr_update_employee` | Update fields on an existing employee. |
| `bamboohr_add_employee_table_row` | Append a promotion, raise, or status change to a historical table. |
| `bamboohr_set_time_off_request_status` | Approve, deny, or cancel a time-off request. |
| `bamboohr_submit_work_hours` | Log hours against a project and task. |
| `bamboohr_set_application_status` | Advance, reject, or hire a candidate. |
| `bamboohr_add_application_comment` | Record interview feedback against an application. |
| `bamboohr_upload_employee_file` | Attach a document to an employee, supplied base64-encoded. |
| `bamboohr_update_employee_file` | Rename, recategorise, or reshare a document. |
| `bamboohr_delete_employee_file` | Permanently delete a document. Not recoverable. |
Each write tool takes a required `confirm` argument that must be `true`. Field maps can be passed either as a `fields` object or as a `fieldsJson` string — use the string form from Copilot Studio, whose connector layer handles free-form objects poorly.
### Compensation and offboarding
There are no dedicated tools for these; BambooHR stores both as historical tables on the employee record, so they go through the generic table tools. Read salary history with `bamboohr_get_employee_table(employeeId, "compensation")` and record a raise with `bamboohr_add_employee_table_row`. Terminations are a row in `employmentStatus`. Both are permission-gated by the API key's user.
### Not available: Global Employment
BambooHR's Global Employment is an embedded EOR service delivered with Remote — hiring and onboarding begin in BambooHR, but payroll and benefits live in Remote's platform. It exposes no endpoints in the BambooHR v1 API, so there is nothing to build tools against. Integrating it would mean going to Remote's API as a separate service.
---
## Security model
- **The server holds the BambooHR token.** Callers authenticate to *the server* with `MCP_API_KEY`; they never see or supply the HR credential. Per-request tokens are possible but opt-in via `BAMBOOHR_ALLOW_TOKEN_HEADER`.
- **Read-only by default.** Write tools are withheld from `tools/list` entirely when writes are disabled — the model is never told a capability exists that the server will refuse.
- **Nothing is shared between requests.** Each HTTP request builds its own context, client, and MCP server instance, so one caller's credentials and employee context can never bleed into another's. There is no mutable global config.
- **Responses are capped.** Anything over `BAMBOOHR_MAX_RESPONSE_BYTES` returns an actionable "narrow your query" error rather than a 500 KB payload that Copilot Studio would reject with an opaque HTTP 400.
- **Least privilege lives in BambooHR.** `BAMBOOHR_ALLOWED_TOOLS` narrows the surface, but the token's own access level is the boundary that actually matters.
Set `MCP_API_KEY` before exposing the server publicly. It logs a warning at startup if you have not.
---
## Deployment
Two GitHub Actions workflows:
- [`.github/workflows/ci.yml`](.github/workflows/ci.yml) — typecheck, build, and test on Node 20 and 22 for every push and pull request, plus a guard asserting the default build still gates its write tools.
- [`.github/workflows/deploy.yml`](.github/workflows/deploy.yml) — on every push to `main`, builds a production zip, deploys it to Azure App Service, then fails the run if the app does not come up **or** if it comes up missing its BambooHR credentials or its API key. A deploy that lands but cannot serve traffic is reported as a failure, not a pass.
The deploy workflow targets the Web App **`BambooHRMCP`** (`https://bamboohrmcp.azurewebsites.net`) by default. Override it with an `AZURE_WEBAPP_NAME` repository variable.
### 1. Create the Web App
Skip this if the app already exists — creating it through the Portal's Deployment Center does the same thing.
```sh
az group create --name bamboohr-mcp-rg --location eastus
az appservice plan create \
--name bamboohr-mcp-plan --resource-group bamboohr-mcp-rg \
--is-linux --sku B1
az webapp create \
--name BambooHRMCP --resource-group bamboohr-mcp-rg \
--plan bamboohr-mcp-plan --runtime "NODE:20-lts"
az webapp config set \
--name BambooHRMCP --resource-group bamboohr-mcp-rg \
--startup-file "node dist/index.js"
```
### 2. Set app settings
These live in Azure, never in the repo. `.env` is for local development only and is gitignored.
```sh
az webapp config appsettings set \
--name BambooHRMCP --resource-group bamboohr-mcp-rg \
--settings \
BAMBOOHR_TOKEN="<your-bamboohr-token>" \
BAMBOOHR_COMPANY_DOMAIN="<your-subdomain>" \
MCP_API_KEY="<your-long-random-key>" \
BAMBOOHR_ENABLE_WRITES="false" \
SCM_DO_BUILD_DURING_DEPLOYMENT="false" \
WEBSITE_RUN_FROM_PACKAGE="1"
```
`SCM_DO_BUILD_DURING_DEPLOYMENT=false` matters: the zip already contains `dist/` and production `node_modules`, and letting Oryx rebuild on the server would only introduce drift.
### 3. Authentication
The workflow deploys with the **publish profile** that Azure's Deployment Center stored in the repository as `AZUREAPPSERVICE_PUBLISHPROFILE_F89A515B4E6341C788E87EFCEC7A991B`. Nothing further to configure — connecting the app through the Portal already did it.
If you ever regenerate the publish profile, or wire up a different Web App, update that secret under **Settings → Secrets and variables → Actions** and change the name in [deploy.yml](.github/workflows/deploy.yml) to match.
Two optional repository *variables*:
| Name | Purpose |
| --- | --- |
| `AZURE_WEBAPP_NAME` | Target Web App. Defaults to `BambooHRMCP`. |
| `AZURE_WEBAPP_SLOT` | Deployment slot. Defaults to `production`. |
Also create an environment named `production` under **Settings → Environments**. Adding required reviewers there turns every deploy into an approval gate, which is worth doing for a server holding an HR admin token.
<details>
<summary><strong>Moving to OIDC instead</strong> — short-lived tokens, no long-lived credential stored in GitHub</summary>
A publish profile is a long-lived credential with deploy rights sitting in GitHub. OIDC replaces it with a token minted per run. Create an Entra app registration, grant it Contributor on the Web App, and add federated credentials:
```sh
az ad app create --display-name bamboohr-mcp-deploy
# note the appId, then:
az ad sp create --id <APP-ID>
az role assignment create \
--assignee <APP-ID> --role Contributor \
--scope /subscriptions/<SUB-ID>/resourceGroups/bamboohr-mcp-rg
az ad app federated-credential create --id <APP-ID> --parameters '{
"name": "github-main",
"issuer": "https://token.actions.githubusercontent.com",
"subject": "repo:aakarsh1t/BambooHR-MCP:ref:refs/heads/main",
"audiences": ["api://AzureADTokenExchange"]
}'
```
The `subject` must match exactly how the workflow runs. Because the deploy job uses a GitHub Environment, add a second credential with subject `repo:aakarsh1t/BambooHR-MCP:environment:production`.
Then add secrets `AZURE_CLIENT_ID`, `AZURE_TENANT_ID`, and `AZURE_SUBSCRIPTION_ID` — none is a credential on its own — and in [deploy.yml](.github/workflows/deploy.yml) restore `id-token: write` to the deploy job's `permissions`, drop the `publish-profile` line, and add before the deploy step:
```yaml
- name: Sign in to Azure
uses: azure/login@v3
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
```
</details>
### 4. Point Copilot Studio at it
[connector/copilot-studio-connector.yaml](connector/copilot-studio-connector.yaml) already points at `bamboohrmcp.azurewebsites.net`. Change `host:` if you deploy elsewhere, then import it as described above.
---
## Development
```sh
npm run dev # HTTP, ts-node, no build step
npm run dev:stdio # stdio, ts-node
npm run typecheck # tsc --noEmit
npm run build # emit dist/
npm test # run test/ against the built output
npm run verify # typecheck + build + test, same as CI
```
Layout:
```
src/
index.ts entry point and transport selection
http.ts express app, auth, stateless /mcp endpoint
server.ts builds an McpServer for one request context
config.ts environment loading and per-request context
tools/index.ts tool definitions, schemas, and gating
apis/bamboohr.ts typed BambooHR v1 client
utils/ response shaping and models
test/
smoke.test.js transport, auth, gating, and error-shaping tests
```
The tests run against `dist/`, so build first — `npm run verify` does both in order. They use throwaway credentials and need no BambooHR account: everything asserted is about transport, gating, and error shaping.
Adding a tool means one `define(...)` call in [src/tools/index.ts](src/tools/index.ts) and a method on `BambooHRClient` in [src/apis/bamboohr.ts](src/apis/bamboohr.ts). Give it `readOnlyHint: true` only if it genuinely does not change anything — that flag is what decides whether writes gating applies.
Two notes for anyone extending this:
- In stdio mode **nothing may be written to stdout** — it is the JSON-RPC channel and a stray `console.log` corrupts the stream. Diagnostics go to `console.error`.
- Zero-argument tools go through the four-argument `server.tool()` form. SDK 1.11 decides whether an argument is a Zod shape by checking for a ZodType value, so an empty `{}` schema gets mistaken for the annotations object and the real annotations get called as the handler. `registerTool()` in `src/tools/index.ts` handles this.
---
## License
MIT. See [LICENSE](LICENSE).
## Contributors
- Encore Shao ([github.com/encoreshao](https://github.com/encoreshao))
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues