Bullhorn MCP Agent
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@Bullhorn MCP Agent Find recent placements from the last 30 days"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Bullhorn MCP Agent
A Python Model Context Protocol (MCP) server for Bullhorn, extended into a recruiting-domain operating layer for AI agents and enterprise workflows.
This project began from the open-source osherai/bullhorn-mcp-python project and has been substantially expanded with tenant-aware schema mapping, per-user Bullhorn authentication, controlled writes, recruiting activity models, audit and approval controls, and agent-oriented workflow tooling.
The goal is to provide a governed layer between AI systems and Bullhorn rather than giving a model direct access to credentials, unrestricted REST endpoints, or tenant-specific business logic.
Current Status
This project is currently in active POC and test-stage development.
It is not yet distributed for production use.
The current focus is validating:
authentication and session isolation
tenant configuration and mapping
permission boundaries
recruiting-domain semantics
safe read/write patterns
auditability
security controls
historical analytics architecture
enterprise Workspace integration
Bullhorn remains the system of record.
Related MCP server: HireFrog MCP
What This Project Is Becoming
This is no longer just a thin REST wrapper around Bullhorn.
The MCP is being developed as a recruiting-domain operating layer that sits between AI systems and Bullhorn.
Its responsibilities include:
authenticating users
isolating Bullhorn sessions
enforcing permissions
translating canonical recruiting concepts into tenant-specific Bullhorn fields
validating workflows
controlling writes
auditing actions
exposing structured recruiting data
supporting analytics and historical intelligence
keeping the model away from raw credentials and unrestricted API access
Conceptually:
ChatGPT Workspace / AI Agent
|
| MCP
v
Bullhorn Recruiting Orchestrator
|
|-- Authentication / session isolation
|-- Permissions
|-- Tenant mappings
|-- Canonical recruiting schema
|-- Safe reads
|-- Controlled writes
|-- Approval gates
|-- Audit
|-- Activity / analytics
|
| Bullhorn REST API
v
BullhornLicense
License: Source-available for self-use, internal organizational use, modification, forking, and open development. Resale, white-labeling, commercial packaging, or offering the software itself as a paid hosted service requires prior express written permission. Portions derived from upstream MIT-licensed projects remain subject to their original licenses.
See LICENSE (CaseyTTS Source-Available No-Resale License 1.0) and THIRD_PARTY_LICENSES/ for the preserved upstream MIT License of osherai/bullhorn-mcp-python. This license is not an OSI-approved open-source license.
Available Tools
23 toolsbullhorn_sessionA
Show, start, complete or end your own Bullhorn sign-in for this workspace.
Sign-in happens in your browser through Bullhorn (including SSO/MFA); never give passwords or MFA codes to the assistant. After the browser step, the page shows a one-time code: the user enters it here with action="complete_link".
Args: action: "status" (default), "login" (returns a sign-in link), "complete_link" (finish linking with the code from the browser page) or "logout". confirmation: The one-time code from the browser page; only for "complete_link".
Returns: JSON with the session status, the sign-in link, {"linked": true} or {"logged_out": true}.
Inputs: action, confirmation. Returns: result
commit_mapping_changesA
Approve or reject a stored proposal. The actor comes from BULLHORN_MCP_ACTOR.
Args: proposal_id: The proposal_id returned by propose_mapping_changes. diff_hash: The diff_hash returned by propose_mapping_changes. decision: "approve" or "reject".
Returns: JSON with status (committed, rejected or refused), version (on commit) and correlation_id.
Inputs: decision (required), diff_hash (required), proposal_id (required). Returns: result
confirm_writeA
Approve or reject a previewed write. The approver comes from BULLHORN_MCP_ACTOR.
The write runs only if the preview_hash matches, the preview has not expired or been used, the actor is allowed, the scope and setup gate still hold, and nothing previewed has changed (otherwise: refused, e.g. stale_preview).
Args: operation_id: The operation_id returned by the preview. preview_hash: The preview_hash returned by the preview. decision: "approve" or "reject".
Returns: JSON with the normalized write result (status committed, partially_committed, failed, failed_orphan, rejected or refused).
Inputs: decision (required), operation_id (required), preview_hash (required). Returns: result
connection_statusA
Check whether Bullhorn API credentials are configured and connectivity is available.
Inputs: no named parameters. Returns: result
create_noteA
Create a Bullhorn note through the safe-write pipeline (preview by default).
With dry_run=True (the default) nothing is written to Bullhorn: the result holds operation_id and preview_hash. Execute exactly that preview with confirm_write(operation_id, preview_hash, "approve"). dry_run=False is accepted only when BULLHORN_NOTE_CREATE_MODE=direct. Writing requires the note.create scope (BULLHORN_ENABLED_WRITE_SCOPES) and BULLHORN_MCP_ACTOR.
Args: target_type: The person the note is about: "candidate" or "client_contact". target_id: The record id of the target. action_type: A tenant note action value (exact; never substituted). comments: The note text (1 to 10,000 characters; stored unchanged). associations: Additional records, each {"type": ..., "id": ...} (at most 10). idempotency_key: Optional caller key (kept 7 days) that prevents a duplicate write. dry_run: Preview only (default true).
Returns: JSON with operation, status, entity_type, record_id, associations, targets, activity, correlation_id, and operation_id/preview_hash for a preview.
Inputs: comments (required), target_id (required), action_type (required), target_type (required), +3 more. Returns: result
discover_schemaA
Discover the tenant's Bullhorn field metadata (metadata only; no record data, no samples).
Writes the local discovery snapshot and drift report; never changes mappings and never writes to Bullhorn.
Args: entities: Canonical or Bullhorn entity names to discover (default: every catalog entity).
Returns: JSON with per-entity summaries and the drift report.
Inputs: entities. Returns: result
find_recordsA
Find canonical records of one entity with structured, AND-combined filters.
Only canonical names are accepted (never Bullhorn field names or query text). Every filter is applied exactly; a filter, operator or sort without a verified Bullhorn mechanism returns status "unsupported" and nothing is read.
Args: entity: candidate, job, submission, placement, client_corporation, client_contact, appointment or user. filters: 0 to 10 objects {"field": , "op": , "value": ...}. Operators by type: id/reference eq, in; string eq, in, is_null; boolean eq; integer/number eq, in, gt, gte, lt, lte; datetime gte, lt, is_null (ISO-8601 with offset, or a date = midnight in the reporting timezone). "in" takes 1 to 50 values. concept: Optional tenant concept (e.g. client_submission, interview_completed, offer_extended): only records whose current value matches the tenant's mapping. fields: 1 to 30 canonical field names to return (default: a standard set). sort: {"field", "direction"}; no sort is verified yet (returns "unsupported"). include_deleted: Include deleted records (default false; not verified, returns "unsupported"). limit: 1 to 100 (default 25). cursor: The next_cursor of a previous call with the same arguments.
Returns: JSON with status, entity, records, count, truncated, complete, next_cursor, consistency, reporting_timezone, provenance and warnings.
Inputs: entity (required), sort, limit, cursor, +4 more. Returns: result
get_activityA
Read recruiting activity events, one independent block per concept.
Needs scope_type + scope_id, or both date_from and date_to (an unscoped range is at most 366 days). Business concepts (client_submission, interviews, offers) come only from the tenant's configured mappings; a missing definition returns "definition_missing", an unverified mechanism "unsupported" (never guessed).
Args: concepts: 1 to 8 concept IDs (e.g. job_created, submission_created, client_submission, interview_scheduled, interview_completed, placement_created, note_created). scope_type: candidate, job, client_corporation, client_contact, placement or submission. scope_id: The record id for scope_type (required with it). recruiter_id: A user id; filters on each concept's attribution field. date_from: Inclusive ISO-8601 date or datetime with offset. date_to: Exclusive ISO-8601 date or datetime with offset. limit: 1 to 200 events per concept (default 50). cursor: The next_cursor of a previous call with the same arguments.
Returns: JSON with status, concepts (per-concept blocks), next_cursor, reporting_timezone, provenance and warnings.
Inputs: concepts (required), limit, cursor, date_to, +4 more. Returns: result
get_candidateA
Get details for a specific candidate by ID.
Args: candidate_id: The Candidate ID fields: Comma-separated fields to return (default: all common fields)
Returns: JSON object with candidate details
Inputs: candidate_id (required), fields. Returns: result
get_candidate_filesB
List file attachments for a Bullhorn Candidate.
Inputs: candidate_id (required). Returns: result
get_jobA
Get details for a specific job order by ID.
Args: job_id: The JobOrder ID fields: Comma-separated fields to return (default: all common fields)
Returns: JSON object with job details
Inputs: job_id (required), fields. Returns: result
get_mapping_profileA
Read the tenant mapping profile, its history, a version diff, or a proposal.
Args: view: One of "active", "version", "history", "diff", "proposal". version: The version for view="version"; the first version for view="diff". other_version: The second version for view="diff" (default: the active version). entity: Only show mappings of this canonical entity (views "active"/"version"). search: A substring filter; for view="proposal" it is the proposal ID (omit it to list open proposals).
Returns: JSON for the requested view.
Inputs: view, entity, search, version, +1 more. Returns: result
get_notesA
Read canonical notes (and their note_created events) for one record.
Exactly one primary scope is required: target_type + target_id, or one of candidate_id, job_id, client_corporation_id, client_contact_id, placement_id, submission_id. Only verified Bullhorn mechanisms are used; a scope or filter without one returns status "unsupported_filter" with the supported filters (it is never approximated).
Args: target_type: "candidate", "job", "client_contact", "client_corporation", "placement" or "submission". target_id: The record id for target_type. candidate_id: Primary scope: a candidate id. job_id: Primary scope: a job id. client_corporation_id: Primary scope: a client corporation id. client_contact_id: Primary scope: a client contact id. placement_id: Primary scope: a placement id. submission_id: Primary scope: a submission id. action_type: A tenant note action value (validated; never substituted). author: A Bullhorn user id or name. date_from: Inclusive ISO-8601 date or datetime with offset (a date means midnight in the reporting timezone). date_to: Exclusive ISO-8601 date or datetime with offset. include_deleted: Include deleted notes (default false). limit: Page size, 1 to 50 (default 20). start: Offset of the page (default 0).
Returns: JSON with status, notes, events, next_start, truncated and warnings.
Inputs: limit, start, author, job_id, +11 more. Returns: result
get_recent_placementsC
Get recent Bullhorn placements from the last X days.
Inputs: days, limit. Returns: result
get_recruiting_metricsA
Recruiting metrics per calendar period: counts and same-period stage ratios.
Counts come from the tenant's configured activity concepts; a metric whose concept is not configured returns its status (e.g. "definition_missing") and no numbers. A count that could not be enumerated completely returns "incomplete" (never a partial number).
Args: metrics: 1 to 8 of jobs_created, client_submissions, interviews_scheduled, offers_extended, placements_created, submission_to_interview, interview_to_offer, offer_to_placement. date_from: Inclusive date (YYYY-MM-DD) in the reporting timezone, on a period boundary. date_to: Exclusive date (YYYY-MM-DD) on a period boundary; at most 12 months or 4 quarters. period: "month" (default) or "quarter".
Returns: JSON with status, tier, period, reporting_timezone, definition and per-metric cells.
Inputs: date_to (required), metrics (required), date_from (required), period. Returns: result
list_candidatesA
List and filter candidates from Bullhorn CRM.
Args: query: Lucene search query (e.g., "lastName:Smith" or "skillSet:Python") status: Filter by candidate status limit: Maximum number of results (1-500, default 20) fields: Comma-separated fields to return
Returns: JSON array of candidates
Examples: - list_candidates() - Get recent candidates - list_candidates(query="skillSet:Python") - Find Python developers - list_candidates(query="lastName:Smith AND status:Active") - list_candidates(status="Active", limit=50)
Inputs: limit, query, fields, status. Returns: result
list_jobsA
List and filter job orders from Bullhorn CRM.
Args: query: Lucene search query (e.g., "title:Engineer AND isOpen:1") status: Filter by job status limit: Maximum number of results (1-500, default 20) fields: Comma-separated fields to return
Returns: JSON array of job orders
Examples: - list_jobs() - Get recent jobs - list_jobs(query="isOpen:1") - Get open jobs - list_jobs(query="title:Software AND employmentType:Direct Hire", limit=10) - list_jobs(status="Accepting Candidates")
Inputs: limit, query, fields, status. Returns: result
manage_mapping_profileA
Validate or export the active mapping profile.
Import and rollback are proposals (propose_mapping_changes with import_document / rollback_to), so nothing is ever activated silently.
Args: action: "validate" (fresh metadata discovery + validation of the active version, recorded locally) or "export" (write the active profile to a new file). path: For "export": a new .yaml/.yml file outside the setup store. format: For "export": "v2" (default) or "v1".
Returns: JSON describing the result.
Inputs: action (required), path, format. Returns: result
propose_mapping_changesA
Propose mapping changes (the dry-run): validate, diff, hash and store a proposal.
Nothing is activated; only a local proposal file is written. Commit it with commit_mapping_changes using the returned proposal_id and diff_hash.
Args: changes: Change operations, each an object with an "op": init_tenant, apply_verified_defaults, set_field_mapping, deactivate_field_mapping, remove_field_mapping, set_value_mapping, deactivate_value_mapping, remove_value_mapping, set_setting, rollback_to, import_document. base_version: The version the change is built on (default: the active version).
Returns: JSON with proposal_id, diff_hash, expires_at, diff, conflicts, dependencies, validation.
Inputs: changes (required), base_version. Returns: result
query_entitiesA
Query Bullhorn entities using SQL-like WHERE syntax.
Args: entity: Entity type (JobOrder, Candidate, etc.) where: WHERE clause (e.g., "salary > 100000 AND status='Active'") limit: Maximum number of results (1-500, default 20) fields: Comma-separated fields to return order_by: Sort order (e.g., "-dateAdded" for newest first)
Returns: JSON array of matching entities
Examples: - query_entities(entity="JobOrder", where="salary > 100000") - query_entities(entity="Candidate", where="status='Active'", order_by="-dateAdded")
Inputs: where (required), entity (required), limit, fields, +1 more. Returns: result
search_entitiesA
Search any Bullhorn entity type using Lucene query syntax.
Args: entity: Entity type (JobOrder, Candidate, Placement, ClientCorporation, ClientContact, etc.) query: Lucene search query limit: Maximum number of results (1-500, default 20) fields: Comma-separated fields to return
Returns: JSON array of matching entities
Examples: - search_entities(entity="Placement", query="status:Approved") - search_entities(entity="ClientCorporation", query="name:Acme*") - search_entities(entity="JobSubmission", query="jobOrder.id:12345")
Inputs: query (required), entity (required), limit, fields. Returns: result
setup_statusA
Report the tenant setup state, missing requirements and per-capability readiness.
Args: check_connection: Also log in to Bullhorn to confirm connectivity and the REST URL fingerprint (default true).
Returns: JSON with state, missing_requirements, capabilities, active_version, open_proposals and last_validation.
Inputs: check_connection. Returns: result
upload_candidate_resumeA
Upload a local resume file to a Bullhorn Candidate.
Args: candidate_id: The Candidate ID file_path: Local filesystem path to the resume file file_type: Bullhorn file type classification (default: "SAMPLE") external_id: Bullhorn external ID for the file (default: "Portfolio") dry_run: If True, validate inputs and return a preview of what would be uploaded without performing the upload
Returns: JSON object with the upload result, or - when dry_run is True - a preview of what would have been uploaded.
Inputs: file_path (required), candidate_id (required), dry_run, file_type, +1 more. Returns: result
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 23 tools
There is substantial overlap between the generic search tools (search_entities Lucene, query_entities SQL-like, find_records structured filters) and the specific ones (list_jobs, list_candidates, get_recent_placements), so an agent can easily pick the wrong path to the same data. get_notes versus get_activity (which includes note_created) and connection_status versus setup_status versus bullhorn_session also blur together. The very detailed descriptions do help differentiate them, keeping this from being worse than a 3.
Most tools follow a verb_noun snake_case pattern (list_jobs, get_candidate, propose_mapping_changes, confirm_write), which is readable and predictable. However several tools break the pattern with noun or non-standard verb forms (connection_status, setup_status, bullhorn_session, manage_mapping_profile). The mix is understandable but not fully consistent.
23 tools is on the heavy side and sits in the borderline range where redundancy costs appear. The breadth (CRM reads, mapping-profile governance, safe-write pipeline, activity and metrics) justifies many tools, but the generic search trio overlaps with the resource-specific list tools, suggesting some could be consolidated.
The server has strong read coverage (jobs, candidates, placements, notes, activity, metrics) and a well-governed write path, but it lacks create/update for the core CRM entities (no candidate/job/submission/placement creation). For a Bullhorn agent the only real writes are notes and resume upload, a notable lifecycle gap.
Maintenance
Related MCP Connectors
Connect MCP clients to 2,000+ AI models without managing provider API keys.
Remote MCP for 1,500+ APIs. Vault-managed credentials; OAuth or API key. Search, load, and execute.
Build and run AI workflows, agents, data tables and 1000+ API integrations from any MCP client.
Real-time ATS job postings, company career feeds & B2B hiring signals MCP. OAuth or API key.
Related MCP Servers
- AlicenseAqualityDmaintenanceEnables AI assistants to query Bullhorn CRM data using natural language through direct REST API access with OAuth 2.0 authentication.61MIT

HireFrog MCPofficial
AlicenseNot gradedqualityCmaintenanceEnables MCP-compatible AI assistants to securely access a user's job-search account, letting them search and save jobs, analyze job fit, and retrieve job queue and profile summaries with links back to the web app.MIT- AlicenseNot gradedqualityCmaintenanceA hosted, OAuth-authenticated MCP endpoint exposing 176 tools that let AI assistants read and write jobs, candidates, applications, interviews, scorecards, prospects, talent pools, and imports within an organization's own applicant tracking data, scoped to the signed-in user's existing permissions.MIT
- AlicenseAqualityCmaintenanceEnables AI clients to manage JobZyn jobs and candidates through MCP tools for creating, updating, unpublishing jobs, retrieving candidate applications, and linking external IDs.571 npmMIT