MCP Server Specification for the Huly Recruit Module
A Model Context Protocol server that exposes Huly Recruit operations as tools any MCP-compatible client can call. It wraps @hcengineering/api-client and lets AI agents create and manage talents, vacancies, applications, and pipeline movements without touching the Huly UI.
@hcengineering/api-client (WebSocket)MCP over stdio (standard input/output). The server is a Node.js process that communicates with the MCP client via JSON-RPC 2.0 on stdin/stdout.
The server authenticates to Huly using email/password credentials via @hcengineering/api-client, which establishes a WebSocket connection with a NodeWebSocketFactory.
| Variable | Required | Default | Description |
|---|---|---|---|
HULY_URL | Optional | https://huly.app | Huly instance URL. Use http://localhost:8087 for self-hosted. |
HULY_EMAIL | Required | — | Email address of the Huly user account. |
HULY_PASSWORD | Required | — | Password for the Huly user account. |
HULY_WORKSPACE | Required | — | Workspace slug (e.g. agilex-recruitment). |
{
"mcpServers": {
"huly-recruit": {
"command": "npx",
"args": ["-y", "@agilex/huly-recruit-mcp"],
"env": {
"HULY_URL": "https://huly.app",
"HULY_EMAIL": "you@example.com",
"HULY_PASSWORD": "…",
"HULY_WORKSPACE": "your-workspace"
}
}
}
}
Create a Talent in Huly Recruit. A Talent is a Person (in contact:space:Contacts) with the recruit:mixin:Candidate mixin attached. Optionally attaches email, phone, LinkedIn, and Google Drive URL channels.
| Field | Type | Required | Description |
|---|---|---|---|
first_name | string | Yes | First/given name |
last_name | string | Yes | Last/family name |
title | string | No | Current job title — surfaces on the talent card |
city | string | No | City of residence |
email | string (email) | No | Email address — attached as Email channel |
phone | string | No | Phone number — attached as Phone channel |
linkedin | string | No | LinkedIn profile URL — attached as LinkedIn channel |
cv_drive_url | string (URL) | No | Google Drive URL for the CV. Huly stores the link only; the artifact lives in Workspace. Attached as a Homepage channel. |
profile_folder_url | string (URL) | No | Google Drive folder for all candidate artifacts. Attached as a Homepage channel. |
notes_doc_url | string (URL) | No | Google Doc for working notes. Attached as a Homepage channel. |
skills | string[] | No | Skill tags. NOTE: Accepted but not attached in v0.0.1 — uses the @hcengineering/tags subsystem, deferred. |
source | string | No | Origin label (e.g. "LinkedIn InMail", "Direct application"). |
{
"talent_id": "69eda41502dd3b119a5c8fda", // Huly Person doc ref
"name": "Kagiso Maile",
"channels_added": 6, // email + phone + linkedin + 3 drive URLs
"warnings": [] // or ["skills (5) accepted but not attached…"]
}
contact:class:Person in contact:space:Contacts"LastName,FirstName" (Huly convention)recruit:mixin:Candidate mixin with optional title and sourcecontact:class:Channel doc via addCollectionHomepage channels — same mechanism a recruiter uses when pasting a link in the Huly UICreate a Vacancy in Huly Recruit. Vacancies live in core:space:Space with type=recruit:template:DefaultVacancy. The tool auto-seeds the seven default pipeline statuses.
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Job title (e.g. "ML Platform Engineer (LLM/MLOps)") |
description | string | No | Short summary for the vacancy card. Markdown supported. |
full_description | string | No | Long-form job spec. Omit if the brief lives in Drive. |
brief_drive_url | string (URL) | No | Google Drive URL for the full vacancy brief. Appended to description as a link. |
{
"vacancy_id": "69f1a89d02dd3b119a5c9012",
"name": "V3 · ML Platform Engineer (LLM/MLOps)"
}
| # | Stage | Huly Ref | Category |
|---|---|---|---|
| 1 | Backlog | recruit:taskTypeStatus:Backlog | UnStarted |
| 2 | HR Interview | recruit:taskTypeStatus:HRInterview | Active |
| 3 | Technical Interview | recruit:taskTypeStatus:TechnicalInterview | Active |
| 4 | Test Task | recruit:taskTypeStatus:TestTask | Active |
| 5 | Offer | recruit:taskTypeStatus:Offer | Active |
| 6 | Won | recruit:taskTypeStatus:Won | Won |
| 7 | Lost | recruit:taskTypeStatus:Lost | Lost |
recruit:class:Vacancy which extends task:class:Project — it IS a Spacecore:space:Space (global space, not per-workspace)type = recruit:template:DefaultVacancycreateDoc does NOT trigger this side-effect, so the tool manually replicates the 7 default statusesmembers: [], owners: [], private: false, archived: false to avoid runtime errorsAttach a Talent to a Vacancy at a pipeline stage. Creates a recruit:class:Applicant linking the Person to the Vacancy. The Applicant lives in the Vacancy's space and is attached to the Person via the 'applications' collection.
| Field | Type | Required | Description |
|---|---|---|---|
talent_id | string | Yes | Person doc ID (returned by create_talent) |
vacancy_id | string | Yes | Vacancy doc ID (returned by create_vacancy) |
stage | enum | Yes | One of: Backlog, HR Interview, Technical Interview, Test Task, Offer, Won, Lost |
assignee_id | string | No | Recruiter Employee ref (responsible for this application) |
{
"application_id": "69f1b23402dd3b119a5c9045",
"talent_id": "69eda41502dd3b119a5c8fda",
"vacancy_id": "69f1a89d02dd3b119a5c9012",
"stage": "HR Interview"
}
recruit:class:Applicantspace = the Vacancy ID (a Vacancy IS a Space)attachedTo = the Person (Talent) IDcollection = 'applications'recruit:taskTypeStatus:* refidentifier is auto-generated by Huly from the vacancy's task counter| Tool | Phase | Status | Description |
|---|---|---|---|
move_application | 2 | Planned | Move an application to a different pipeline stage with validation |
find_talent | 3 | Planned | Search talents by name, email, or skills |
list_vacancies | 3 | Planned | List all vacancies with candidate counts per stage |
add_review | 3 | Planned | Attach an interview review/feedback to an application |
get_pipeline | 3 | Planned | Get full pipeline view for a vacancy (all applicants by stage) |
Person (contact:class:Person)
├── recruit:mixin:Candidate ← makes it a Talent
├── Channel[] (Email, Phone, LinkedIn, Homepage)
└── Application[] (one per Vacancy)
Vacancy (recruit:class:Vacancy extends task:class:Project)
├── statuses[] (7 default pipeline stages)
├── members[], owners[]
└── Applicant[] (one per Talent applied)
Applicant (recruit:class:Applicant)
├── space = Vacancy (Vacancy IS a Space)
├── attachedTo = Person (Talent)
├── status = pipeline stage ref
└── assignee = Employee ref (optional)
Verified 2026-04-26 against Huly hcengineering/platform main branch and confirmed via wire probes against a live workspace.
| Ref | Plugin | Purpose |
|---|---|---|
contact:class:Person | Contact | Base person entity |
contact:class:Channel | Contact | Communication channel (email, phone, etc.) |
contact:space:Contacts | Contact | Global contacts space |
recruit:class:Vacancy | Recruit | Job opening (extends Project → IS a Space) |
recruit:class:Applicant | Recruit | Talent ↔ Vacancy link with pipeline stage |
recruit:mixin:Candidate | Recruit | Mixin that makes a Person a Talent |
recruit:template:DefaultVacancy | Recruit | Default vacancy template with 7 stages |
contact:channelProvider:Email | Contact | Email channel type |
contact:channelProvider:Phone | Contact | Phone channel type |
contact:channelProvider:LinkedIn | Contact | LinkedIn channel type |
contact:channelProvider:Homepage | Contact | Generic URL channel (used for Drive links) |
The MCP server follows "Path B": Huly stores metadata and links only. Durable artifacts (CVs, briefs, scorecards, contracts) live in Google Workspace.
| Huly Stores (metadata) | Google Workspace Stores (artifacts) |
|---|---|
| Talent name, city, title, source | CV PDF in Google Drive |
| Channel links (email, phone, LinkedIn, Drive URLs) | Candidate folder in Drive |
| Vacancy name, description, pipeline stages | Full vacancy brief in Google Docs |
| Application stage, assignee | Interview scorecards in Sheets |
Drive URLs are stored as Homepage channels on the Person — the same mechanism a recruiter uses when pasting a link through the Huly UI. This ensures agents stay within capability parity with human users.
The repository includes a seed script that populates a complete demo workspace in ~12 seconds:
npm run seed:v3-demo
Creates:
Result: a fully populated Kanban board in Huly Recruit → Vacancies → V3.
.nvmrc included)git clone https://github.com/craig-agilex/huly-recruit-mcp.git
cd huly-recruit-mcp
npm install
cp .env.example .env.local # fill in HULY_EMAIL, HULY_PASSWORD, HULY_WORKSPACE
npm run dev # starts MCP server on stdio
npm run test:create-talent
npm run test:create-vacancy
npm run test:create-application
npm run seed:v3-demo # full demo seed
npm run build # → dist/index.mjs (ESM, Node 22 target)
| Aspect | Detail |
|---|---|
| Runtime | Node.js 22.12.0+ (ESM) |
| Language | TypeScript 5.4 (strict) |
| MCP SDK | @modelcontextprotocol/sdk ^1.29.0 |
| Huly Client | @hcengineering/api-client ^0.7.413 |
| Validation | Zod ^3.23.0 |
| Transport | stdio (JSON-RPC 2.0) |
| Build | tsup → ESM bundle |
| License | Apache 2.0 (matches Huly upstream) |
| Total LOC | 1,149 (source + scripts) |
@hcengineering/tags subsystem (deferred)create_talent always creates a new Person. find_talent (Phase 3) is needed to check for existing records first.@hcengineering/api-client uses WebSocket, which doesn't run in Cloudflare Workers. For edge integration, a thin HTTP wrapper service is needed.createRequire to load from ESM.@agilex/huly-recruit-mcp · v0.0.1 · GitHub · AgileX · May 2026