npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@lazco/huly-mcp

v2.0.0

Published

MCP server for Huly: read and write Huly documents, issues, labels, and comments from Claude Desktop, Claude Code, Pi, or any MCP client

Readme

huly-mcp

CI License: AGPL v3 Node

MCP server for Huly — read and write Huly documents and issues from Claude Desktop, Claude Code, or any MCP client.

Huly does not ship an official MCP server, but it does publish an official SDK, @hcengineering/api-client, installable from the public npm registry with no GitHub token required. This project is a thin Model Context Protocol layer on top of that SDK.

Works with self-hosted Huly. Huly Cloud (huly.app) uses the same API surface and should work, but has not been verified — see Verification.

  • 22 tools — full document CRUD; issue listing with filters, create, update, labels, and comments; and the catalog of statuses, labels, milestones, components, and people
  • Zero build step — plain CommonJS, no TypeScript, no bundler
  • Actually writes document content — including the collaborative-editor workaround most naive implementations get silently wrong (see Implementation notes)

Contents


Requirements

  • Node.js 20 or newer (verified on v24.12.0)
  • A Huly instance — self-hosted, or Huly Cloud
  • A Huly account with access to the workspace you want to expose

Install

git clone https://github.com/Lazco-Corporation/huly-mcp.git
cd huly-mcp
npm install

Or install the published package:

npm install -g @lazco/huly-mcp

The unscoped name huly-mcp on npm belongs to an unrelated project, so this one ships under the @lazco scope.

Configuration

The server is configured entirely through environment variables.

| Variable | Required | Description | |---|---|---| | HULY_URL | yes | Base URL of your Huly instance, e.g. https://huly.example.com | | HULY_WORKSPACE | yes | Workspace URL slug (see below) | | HULY_TOKEN | one of | Auth token | | HULY_TOKEN_COMMAND | one of | Command that prints the token, e.g. from the macOS Keychain | | HULY_EMAIL | one of | Account email — requires a password source | | HULY_PASSWORD | one of | Account password | | HULY_PASSWORD_COMMAND | one of | Command that prints the password | | HULY_AUTH_MODE | no | auto (default: token first, password if the token is rejected), token, or password | | HULY_AUTH_BACKOFF_MS | no | Pause after a rejected password before trying again (default 600000) |

Configure a token, an email with a password, or both. Prefer the _COMMAND forms so no secret sits in a plain-text config file. See docs/authentication.md. | HULY_TIMEOUT_MS | no | Connection timeout in ms (default 30000) |

Finding your workspace slug

Log in to Huly and read the URL:

https://huly.example.com/workbench/my-workspace-slug
                                   ^^^^^^^^^^^^^^^^^ this part

Use the slug from the URL, not the name shown in the sidebar. They are frequently different, and this is the single most common setup mistake.

Claude Desktop

Edit your Claude Desktop config:

  • macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
  • Windows: %APPDATA%\Claude\claude_desktop_config.json
{
  "mcpServers": {
    "huly": {
      "command": "node",
      "args": ["/absolute/path/to/huly-mcp/src/index.js"],
      "env": {
        "HULY_URL": "https://huly.example.com",
        "HULY_WORKSPACE": "my-workspace-slug",
        "HULY_EMAIL": "[email protected]",
        "HULY_PASSWORD_COMMAND": "security find-generic-password -a [email protected] -s huly-mcp -w"
      }
    }
  }
}

This reads the password from the macOS Keychain at sign-in; store it once with security add-generic-password -a [email protected] -s huly-mcp -w. For Linux, a token, or a token with password fallback, see docs/authentication.md.

If node is not on the PATH Claude Desktop sees (common with nvm, fnm, or asdf), use the absolute path to the Node binary instead, e.g. /Users/you/.local/share/fnm/node-versions/v24.12.0/installation/bin/node.

A copy of this snippet lives in claude_desktop_config.example.json.

Claude Code

claude mcp add huly \
  --env HULY_URL=https://huly.example.com \
  --env HULY_WORKSPACE=my-workspace-slug \
  --env [email protected] \
  --env 'HULY_PASSWORD_COMMAND=security find-generic-password -a [email protected] -s huly-mcp -w' \
  -- node /absolute/path/to/huly-mcp/src/index.js

Verify before connecting

Run the bundled diagnostic before restarting your MCP client:

HULY_URL=https://huly.example.com \
HULY_WORKSPACE=my-workspace-slug \
[email protected] \
HULY_PASSWORD_COMMAND='security find-generic-password -a [email protected] -s huly-mcp -w' \
npm run doctor

It checks, in order: environment variables and sign-in order → server config.json → sign-in (including any token-to-password fallback) → data reads → collaborative content reads. Only restart your client once everything passes.

Authentication: password vs token

Configure either one, or both.

  • Token: no lockout risk, but it stops working when Huly revokes it, with no advance warning. Copy it from a browser request header (DevTools → Network → any /_accounts request → Authorization: Bearer ...).
  • Email and password: does not expire, but Huly can lock an account after repeated failed attempts. A wrong password shows up as AccountNotFound.
  • Both: the server uses the token and falls back to the password only if Huly rejects the token, and only when both belong to the same account. HULY_AUTH_MODE=token or password turns the fallback off.

The server signs in once per session. After a rejected password it pauses password sign-in for 10 minutes rather than adding to a lockout. huly_whoami shows which method signed in and whether a fallback happened.

Whichever you use, sign in as a dedicated, least-privilege account rather than a personal OWNER account, because the server acts with all of its permissions.

docs/authentication.md covers the details: storing secrets in macOS Keychain or libsecret with HULY_PASSWORD_COMMAND / HULY_TOKEN_COMMAND, the fallback rules, running doctor, reading the huly_whoami auth output, rotation, and a sign-in troubleshooting table.

Tools

Documents

| Tool | Purpose | |---|---| | huly_whoami | Connection check, including how the server signed in — run this first | | huly_list_teamspaces | List teamspaces | | huly_list_documents | List documents, filtered by space or parent | | huly_get_document | Read document content (markdown / html / markup) | | huly_search_documents | Search titles, optionally including content | | huly_create_document | Create a document, optionally as a child | | huly_update_document | Update title or content | | huly_delete_document | Delete a document (requires confirm=true) |

Issues

| Tool | Purpose | |---|---| | huly_list_projects | List Tracker projects | | huly_list_issues | List issues with filters (status, assignee, label, milestone, component, priority, parent, text) and pagination | | huly_get_issue | Read an issue: markdown description, and up to 100 comments as markdown with author and id | | huly_comment_issue | Comment on an issue; the message is markdown and renders formatted in Huly | | huly_create_issue | Create an issue or sub-issue with description, status, priority, assignee, labels, milestone, component, due date, estimation | | huly_update_issue | Change any of those fields; null clears assignee, milestone, component, or due date | | huly_set_issue_labels | Add and remove labels by title; createMissing creates unknown ones | | huly_update_comment | Replace a comment's markdown | | huly_delete_comment | Delete a comment (requires confirm=true) | | huly_list_statuses | Statuses of a project in workflow order, with category and the default | | huly_list_labels | Labels that can be applied to issues, optionally filtered by title | | huly_list_milestones | Milestones of a project with status and target date | | huly_list_components | Components of a project with lead and a markdown description excerpt | | huly_list_people | Active members who can be assigned, with email, optionally filtered |

huly_get_issue returns each comment as { id, author, message, createdOn }, oldest first, where message is markdown and author is the writer's display name (null if it cannot be resolved). commentCount is the total number of comments; commentsTruncated is true when there are more than the 100 returned.

Listing issues

huly_list_issues returns issues newest-modified first. Filters combine with AND, and a filter given a list matches any of its values:

| Filter | Accepts | |---|---| | projectId | Project id, identifier, or name | | status | Status name(s), e.g. "In Progress"; matched within the project when one is given | | statusCategory | backlog, todo, in-progress, done, canceled | | assignee | Person name (either order), email, me, or none | | label | Label title(s) | | milestone, component | Label(s) within the project, or none | | priority | Urgent, High, Medium, Low, No priority, or 0-4 | | parent | Issue identifier; returns its direct sub-issues | | text | Case-insensitive text inside the title |

none cannot be mixed with other values in one filter; make two calls instead.

Each row carries identifier, title, status, priority and priorityName, assignee, labels, milestone, component, parent, dueDate, projectId, and modifiedOn. The response has total (issues matching the filters) and nextCursor; pass it back as cursor with the same filters to get the next page. It is null on the last page. A cursor used with different filters is rejected.

huly_list_issues { "assignee": "me", "statusCategory": ["todo", "in-progress"] }
huly_list_issues { "projectId": "CLD", "label": "backend", "limit": 20 }
huly_list_issues { "parent": "CLD-142" }

Writing issues

huly_create_issue { "projectId": "CLD", "title": "Redis has no persistence",
                    "description": "## Context\n...", "priority": "High",
                    "assignee": "me", "labels": ["backend"], "parent": "CLD-142" }
-> { "created": true, "identifier": "CLD-161", ... }

huly_update_issue { "identifier": "CLD-161", "status": "In Progress" }
huly_set_issue_labels { "identifier": "CLD-161", "add": ["infra"], "remove": ["backend"] }

Every reference is resolved before anything is written, so a typo in a label or status never leaves a half-created issue or uses up an issue number. Use the catalog tools (huly_list_statuses, huly_list_labels, ...) to see valid values.

One write at a time. The server sends at most one write to Huly at a time; write calls that arrive in parallel wait in a queue, in order. Reads are not queued. On 2026-10-08, ten concurrent issue creations against one project stalled the Huly 0.7.426 transactor for every user until it was restarted, so this is deliberate. The queue is per server process: several MCP clients, each running its own server, are not coordinated, so avoid bulk writes from many clients at once. Set HULY_DEBUG_WRITES=1 to log each write's start and end to stderr.

Referring to things

Every parameter that takes an id also accepts the name people use:

| Kind | Accepted | Example | |---|---|---| | Project (projectId) | id, identifier, or name | CLD, lazco cloud | | Teamspace (teamspaceId) | id or name | Lazco Cloud | | Document (documentId, parentId) | id or exact title; with a teamspaceId on the same call, only that teamspace is searched | Release checklist | | Issue (issueId, identifier) | id or identifier | CLD-142, cld-142 |

Names match exactly, ignoring case and surrounding spaces. A tool never picks an entity by a partial or fuzzy match:

huly_list_issues { "projectId": "Lazco" }
-> Error: Project not found: Lazco. Did you mean: "CLD (Lazco Cloud)", "LAZGO (Lazgo!)"?

When a name matches more than one entity, nothing happens and the error lists the candidates:

huly_get_document { "documentId": "Roadmap" }
-> Error: Document "Roadmap" is ambiguous (2 matches): 6a94... (teamspace "Engineering"),
   6a95... (teamspace "Marketing"). Pass the id instead, or narrow it with teamspaceId.

Implementation notes

These are the non-obvious parts. If you are extending the server, read this section first.

Document content is not an ordinary field. Huly's Document.content is a reference to a collaborative-editing blob (MarkupBlobRef), so you cannot write a string to it with updateDoc. You must go through fetchMarkup / uploadMarkup, which talk to COLLABORATOR_URL. Creating a document with content is therefore two steps: create the document to get an id, then upload the markup and write the reference back.

Node needs an explicit WebSocket implementation. connect() runs over WebSocket and Node has no global WebSocket, so NodeWebSocketFactory must be passed in (already handled). If you front Huly with a reverse proxy, it must forward Upgrade / Connection headers.

Updating content must go through the collaborator — a trap worth knowing. api-client's uploadMarkup actually calls the collaborator's createContent, which means create a new blob. Used against a document that already has collaborative state, the server keeps its existing YDoc and silently discards the new blob — the API reports success and the content does not change. The correct call is the collaborator's updateContent (updateMarkup), which api-client does not expose, so src/huly.js builds its own collaborator client.

As a result huly_update_document branches: uploadMarkup when the document has no content yet, updateMarkup when it does. This bug is fixed and covered by end-to-end testing.

Content updates replace the whole body. The content argument to huly_update_document replaces the entire document. To append, read with huly_get_document, merge, then write back.

Comments are markup too. A comment's message is stored as a markup JSON string, not plain text. Writing raw text into it makes markdown syntax show literally, and reading it raw hands the model a ProseMirror tree. Both directions go through the same markdown converter as document content (src/markdown.js).

Markdown tables become HTML tables. Write a table and read it back and you get <table> markup rather than the original |---| syntax. The content is correct and renders correctly in the Huly UI, but the round trip is not byte-symmetrical.

CommonJS, not TypeScript. @hcengineering/[email protected] declares types/index.d.ts in its package.json, but that directory is not actually published with the package, so a TypeScript build fails immediately. Hence plain JavaScript.

Verification

Unit tests (npm test) cover configuration, sign-in decisions, markdown conversion, name resolution, pagination, write validation, and the write queue.

Live checks run against a self-hosted Huly 0.7.426 in a private sandbox project and teamspace, one write at a time: document create and repeated updates of existing content; issue create (minimal, fully specified, sub-issue), ten parallel creates through the queue with distinct identifiers, rejected references consuming no issue number; issue update including replacing an existing description and clearing fields; label add, remove, no-op, and createMissing; markdown comments with author, edit, and guarded delete; and an end-to-end run of a review-findings workflow (parent issue, five sub-issues with component and labels, status changes, comments, filtered listing).

Verified against: Huly 0.7.426, api-client 0.7.423, MCP SDK 1.30.0, Node v24.12.0.

When your Huly instance updates, and especially if api-client takes a major version bump, update this project's dependencies alongside it.

Troubleshooting

| Symptom | Fix | |---|---| | Workspace not found | HULY_WORKSPACE is set to the display name; use the URL slug instead | | Authentication failed / AccountNotFound | Wrong email or password (Huly does not say which), or an expired token. See sign-in troubleshooting | | Connection timeout | Reverse proxy is not forwarding WebSocket upgrades | | Tools not visible in the client | Fully quit and reopen Claude Desktop — closing the window is not enough | | Content reads fail | Check that COLLABORATOR_URL in the server's config.json is not empty |

Claude Desktop MCP log:

# macOS
tail -f ~/Library/Logs/Claude/mcp-server-huly.log

Lines like no document found, failed to apply model transaction, skipping come from the Huly server's model sync and are harmless noise — they do not indicate a failed tool call.

Contributing

Issues and pull requests are welcome. Start with CONTRIBUTING.md — it covers the development setup, the unit-test and live-verification split, and the two conventions that silently break things when violated.

Participation is governed by our Code of Conduct.

The short version:

  • Run npm run doctor against your own Huly instance before and after your change.
  • Keep the code plain CommonJS — see Implementation notes for why.
  • Never write to stdout from src/; it carries the JSON-RPC stream.
  • If you touch content read/write paths, test against a document that already has collaborative content, not just a fresh one. That is where the silent-failure trap lives.

Security

Do not report security issues in a public issue. See SECURITY.md for private reporting, and for operator notes worth reading before you deploy — in particular that this server acts with your full Huly account permissions and adds no authorisation layer of its own.

License

AGPL-3.0-or-later. See LICENSE.

Copyright (C) 2026 Lazco Corporation.

The AGPL's network clause applies: if you modify this server and let others use it over a network, you must offer them the modified source. Running it unmodified, or using it privately, carries no such obligation.