A CLI + MCP server for managing tasks, projects, labels, documents, milestones, and comments in Huly — built on the official @hcengineering/api-client.
A portable integration for Claude (Code / Desktop), Google Antigravity, and OpenAI Codex. The same capabilities ship two ways:
- Skill — a file-based
SKILL.md+ zero-install bundled CLI. Loaded directly by Claude and Antigravity. - MCP server —
@fioenix/huly-mcp, the universal interface. Works for all three (Codex has no skill system, so it uses MCP +AGENTS.md).
| Agent | Recommended | How |
|---|---|---|
| Claude Code / Desktop | Skill | npx skills add fioenix/huly-skill (drops SKILL.md + references/ into .claude/skills/). MCP also works — see below. |
| Google Antigravity | Skill | Copy skills/huly-skill/ → <project>/.agents/skills/huly-skill/ (or the global skills dir). MCP also works. |
| OpenAI Codex | MCP + AGENTS.md |
Add the server to ~/.codex/config.toml (snippet below); Codex reads AGENTS.md for usage. |
Zero-install clone (any agent, no npm install needed):
git clone https://github.com/fioenix/huly-skill.git && cd huly-skillAll three use the same credentials (see Setup). Ready-to-paste MCP config templates per agent live in examples/agents/.
Set these environment variables before using any command:
export HULY_HOST="https://huly.app"
export HULY_WORKSPACE_ID="your-workspace-uuid"
export HULY_API_KEY="your-api-token"- HULY_HOST: Your Huly instance URL
- HULY_WORKSPACE_ID: Found in Huly Settings > Workspace
- HULY_API_KEY: issued from the workbench's workspace settings, admin-only — see below
HULY_API_KEY is a JWT whose payload binds one account to one workspace:
{ "account": "<account uuid>", "workspace": "<workspace uuid>", "extra": {} }
Consequences worth knowing before you share one:
- It acts as that person. Everything created through it is attributed to that
account, and runs with that account's role. There is no way to override the
author from the client:
AuthOptionsaccepts only credentials, no "on behalf of" field. A token shared across a team makes every task look like its owner's. meis the token's owner, not whoever is typing — unlessHULY_ACTORis set (below).- It carries no
expclaim, so it stays valid until the server's signing secret changes. Treat it like a password.
Where a token comes from. In the Huly workbench, API access tokens are issued from the workspace settings area (Settings → General). That area is restricted to workspace admins, so an ordinary member cannot mint one — on a team, expect to ask an admin.
Two things follow, and they pull against each other:
- A token identifies one account, so correct attribution wants one token per person.
- Issuing tokens is admin-only, so getting one per person needs an admin to issue each of them.
If your admin issues a single token for everyone to share, every task will carry
that admin's name and that admin's role — usually OWNER. That is a workable
setup for a read-mostly integration and a poor one for a team that writes.
When a team shares one token, set two optional variables per person:
export HULY_ACTOR="Nguyen Van A" # who is operating this CLI
export HULY_DEFAULT_ASSIGNEE="me" # assignee when --assignee is omittedHULY_ACTOR makes me resolve to that person and appends
Requested by: <name> to tasks they create. huly whoami shows which identity
is in effect.
This is a label, not a permission. Huly still records the token's owner as
the author — there is no client-side way to change that — and anyone can set
HULY_ACTOR to any name. It answers "who asked for this", not "who is allowed
to do this", and it does nothing about the shared token's role.
@hcengineering/api-client also exposes
getWorkspaceToken(host, { email, password, workspace }), which returns a token
for whoever authenticates. Note this repo's pinned @hcengineering/account-client
(and its latest release) expose no token-creation call, so the settings screen is
the supported route.
./bin/huly.cjs whoamihuly tasks --assignee me # My tasks
huly tasks --project DELTA --overdue # Overdue in project
huly tasks --parent LAMBD-568 # Direct children of a parent (Epic / sub-issue group)
huly tasks --milestone-id <id> # All tasks attached to a milestone
huly task DELTA-123 # Task details by identifier
huly task-by-id <internalId> # Lookup by internal _id (e.g. from childInfo)
huly create task "Title" --project DELTA --priority HIGH --due tomorrow
huly create task "Sub-task" --project DELTA --parent DELTA-16 # as a sub-issue
huly update task DELTA-123 --status "Done" --add-comment "Completed"
huly delete task DELTA-123 --yes # Requires confirmationhuly kinds --project OMEGA # Task types in a project → IDs for --kind-idKinds are scoped by project type, so the same name (e.g. KPI) can carry a
different ID in another project. Read the ID from the project you write to.
huly users # Everyone in the workspace → IDs for --assignee
huly users --active-only # Active members onlyhuly sub-issues LAMBD-568 # Recursive tree (default)
huly sub-issues LAMBD-568 --no-recursive # Direct children only
huly sub-issues LAMBD-568 --json --flat # Flat list for programmatic useSolves the "tasks only returns top-level" pain — walks attachedTo one findAll per level, no per-child round-trip.
huly activity LAMBD-568 # Field changes + comments, newest first
huly activity LAMBD-568 --updates-only # Only status/assignee/label changes
huly activity LAMBD-568 --comments-only --jsonReads activity:class:DocUpdateMessage + chunter:class:ChatMessage, resolves status and assignee refs to human names, and renders each comment body to markdown.
huly comments list <objectId> # Comments on any object (issue, milestone, doc, …)
huly comments list <milestoneId> --class milestone
huly comments get <messageId> # One comment by _id (the "message" param in a chunter link)Generalises comment reading beyond issues: queries chunter:class:ChatMessage by attachedTo (not hard-wired to Issue), so it works on milestones, documents, and any Doc. Thread replies (chunter:class:ThreadMessage) are nested under their parent in replies. Bodies are inline markup-JSON, converted to markdown locally. For issues, huly activity <identifier> is friendlier (accepts LAMBD-568, merges changes + comments).
huly report daily --assignee me # Today's summary
huly report weekly # Week summaryhuly labels list # All labels
huly labels create "bug" --color 3 # Create label
huly labels assign DELTA-123 <id> # Assign to task
huly labels show DELTA-123 # Show task labelshuly docs teamspaces # List teamspaces
huly docs list "My Documents" # List docs
huly docs read "My Documents" "Notes" # Read as markdown
huly docs create "Title" -t "My Documents" --file ./content.md
huly docs create-teamspace "Engineering"huly milestones list --project DELTA
huly milestones create "Sprint 1" --project DELTA --target 2026-04-15
huly milestones complete <id> --project DELTA
huly milestones report <milestoneId> # Issues grouped by Epic with sub-treesAppend --json to any command for structured output:
huly tasks --assignee me --jsonSuccess is { "status": "ok", … }; failure is:
{ "status": "error", "error": "Task not found: DELTA-9", "code": "not_found", "retryable": false }code is one of auth, connection, not_found, invalid_input, unknown, and
only connection is retryable — repeating any other call unchanged fails the same
way. auth errors carry a hint. The MCP tools return the identical envelope.
Besides the CLI skill, the same Huly operations are exposed as an MCP server — a better fit for Claude Cowork and any MCP-capable client. It is published to npm as @fioenix/huly-mcp and runs via npx with no install step.
A single entry point picks its transport from HULY_MCP_TRANSPORT (stdio default, or http). The server is single-workspace: one set of Huly credentials, shared by all callers. Full templates: examples/agents/.
Both use the same mcpServers JSON shape:
{
"mcpServers": {
"huly": {
"command": "npx",
"args": ["-y", "@fioenix/huly-mcp@latest"],
"env": {
"HULY_MCP_TRANSPORT": "stdio",
"HULY_HOST": "https://huly.app",
"HULY_WORKSPACE_ID": "your-workspace-uuid",
"HULY_API_KEY": "your-api-token"
}
}
}
}Claude: claude mcp add or a project .mcp.json. Antigravity: place under ~/.gemini/config/mcp_config.json.
[mcp_servers.huly]
command = "npx"
args = ["-y", "@fioenix/huly-mcp@latest"]
[mcp_servers.huly.env]
HULY_MCP_TRANSPORT = "stdio"
HULY_HOST = "https://huly.app"
HULY_WORKSPACE_ID = "your-workspace-uuid"
HULY_API_KEY = "your-api-token"Or via CLI: codex mcp add huly --env HULY_HOST=… --env HULY_WORKSPACE_ID=… --env HULY_API_KEY=… -- npx -y @fioenix/huly-mcp@latest.
HULY_MCP_TRANSPORT=http \
HULY_HOST=https://huly.app \
HULY_WORKSPACE_ID=your-workspace-uuid \
HULY_API_KEY=your-api-token \
HULY_MCP_AUTH_TOKEN=your-shared-secret \
npx -y @fioenix/huly-mcp@latestServes POST /mcp (default port 3000, override with PORT) and GET /health. When HULY_MCP_AUTH_TOKEN is set, callers must send Authorization: Bearer <token> — because the server holds Huly credentials, always set it for any non-local deployment.
Per-caller Huly credentials. A caller can bring its own token so Huly attributes its writes to it rather than to whoever owns the server's token:
| Header | Overrides |
|---|---|
x-huly-token |
HULY_API_KEY |
x-huly-url |
HULY_HOST |
x-huly-workspace |
HULY_WORKSPACE_ID |
The process environment stays the default, so a shared token keeps working
unchanged. Set HULY_REQUIRE_CALLER_TOKEN=true to reject requests that omit
x-huly-token with 401 instead of quietly falling back to the server's token —
worth doing on any deployment where attribution matters. huly_context reports
which source is in effect.
Building from source instead of npm: pnpm build produces bin/mcp.cjs (run with node bin/mcp.cjs). See npm-package/ for the published package, and RELEASING.md for the release checklist.
See AGENTS.md for the full agent integration guide, or skills/huly-skill/SKILL.md for the skill definition.
All dependencies are bundled into a single bin/bundle.cjs via esbuild — no npm install or GitHub PAT required. The @hcengineering/api-client expects browser APIs (indexedDB, window), which are polyfilled automatically.
dearlordylord/huly-mcp (npm
@firfi/huly-mcp) started in
February 2026, six weeks before this project, and covers far more of Huly than we
do. Reading it sharpened this codebase in several concrete ways: an offline
configuration tool, per-caller credentials over HTTP headers, machine-readable error
codes, release-artifact verification, and the discipline of capping and projecting
list results all began as ideas taken from there — each
reimplemented in this project's own style, kept deliberately smaller. Their
lazy-tool PRD
is also the best measurement of MCP tool-surface context cost we have found
anywhere, and it independently reached the same conclusion we did about Huly's
unreleased revocable API tokens.
Thanks to its author. Two projects can solve the same problem at different scales and both be right: theirs goes wide, this one stays small enough to read in one sitting. A criterion-by-criterion comparison, including where they are ahead and where we deliberately diverge, is in reference/comparison.md.
Bug reports, feature requests and pull requests are welcome — see CONTRIBUTING.md for how to build, test and verify a change, and for what this project deliberately says no to. Vulnerabilities go through SECURITY.md rather than a public issue.
MIT