BambooHR MCP
Enables AI assistants to interact with BambooHR for employee management, hiring, time-off, documents, and reporting, with read-only tools by default and optional write capabilities.
README
BambooHR MCP (Admin)
An administrative Model Context Protocol 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
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:
curl http://localhost:3000/health
For a local desktop client instead:
npm run start:stdio
Configuration
Everything is environment-driven. See .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
- Log in to BambooHR and open your profile menu (bottom-left).
- Choose API Keys → Add New Key.
- Name it (e.g. "Copilot Studio agent") and click Generate Key.
- 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 below. To run it locally instead, a Dockerfile is included:
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 is a ready Swagger 2.0 definition. Replace the host line with your deployed hostname, then:
- Go to your agent's Tools page → Add a tool → New tool → Custom connector.
- In Power Apps, choose New custom connector → Import OpenAPI file and select the YAML.
- On the Security step, the definition declares an API key in the
X-API-Keyheader. Supply the value of yourMCP_API_KEYwhen you create the connection. - 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
{
"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 viaBAMBOOHR_ALLOW_TOKEN_HEADER. - Read-only by default. Write tools are withheld from
tools/listentirely 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_BYTESreturns 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_TOOLSnarrows 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— 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— on every push tomain, 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.
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.
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 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:
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 restore id-token: write to the deploy job's permissions, drop the publish-profile line, and add before the deploy step:
- 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 already points at bamboohrmcp.azurewebsites.net. Change host: if you deploy elsewhere, then import it as described above.
Development
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 and a method on BambooHRClient in 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.logcorrupts the stream. Diagnostics go toconsole.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()insrc/tools/index.tshandles this.
License
MIT. See LICENSE.
Contributors
- Encore Shao (github.com/encoreshao)
Recommended Servers
playwright-mcp
A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.
Audiense Insights MCP Server
Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.
Magic Component Platform (MCP)
An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.
VeyraX MCP
Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.
Kagi MCP Server
An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.
graphlit-mcp-server
The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.
Neon Database
MCP server for interacting with Neon Management API and databases
Exa Search
A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.
Qdrant Server
This repository is an example of how to create a MCP server for Qdrant, a vector search engine.
E2B
Using MCP to run code via e2b.