huly-mcp-selfhost 3.5.1 → 3.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,17 @@
2
2
 
3
3
  All notable changes to [huly-mcp-selfhost](https://www.npmjs.com/package/huly-mcp-selfhost). Versions follow [semver](https://semver.org).
4
4
 
5
+ ### 3.6.0 — 2026-11-12
6
+
7
+ - **New: Claude Desktop extension** — `huly-mcp-selfhost.mcpb` on every GitHub release: double-click, enter your Huly login in Claude Desktop's form (password and token go to the system keychain), done — no Node.js, no JSON. Built from the same bundled server; the release smoke-tests it before attaching it.
8
+ - **Clearer tool descriptions** for the tools rated weakest by Glama's Tool Definition Quality Score (`list_todos`, `search`, `create_document`, `delete_comment`, …): verb first, the key default or side effect, the tool to use instead. +732 characters in the default tool list (+1.9 %). `create_document` now points to `update_document` for the body.
9
+ - **`changes_since` is shorter** — fields set when an issue was created count as "new", and when one person made every change the name appears once in the heading (−14 % measured on 14 days of changes).
10
+ - **`triage_inbox`**: a comment that appears on several items (bots commenting on every PR) is shown once in full; GitHub-synced accounts are named by their login (`dependabot[bot] (GitHub)`) instead of "Unknown".
11
+ - **Aliases per tool**: `update_milestone`/`get_milestone` accept `milestoneLabel` for `label`, `update_component` accepts `componentLabel` — the names other tools use.
12
+ - **Release workflow** waits up to 15 minutes for npm before the MCP registry publish and retries it (3.5.1 needed a manual re-run).
13
+ - Website and README: navigation with jump links, back-to-top links, plugin icon for the Claude directory.
14
+ - Issue forms (bug report asks for `check_setup` output), questions in GitHub Discussions, private security reports.
15
+
5
16
  ### 3.5.1 — 2026-11-11
6
17
 
7
18
  - **Fixed: `resources/list` failed without a usable login** — it tried to list your open issues and errored when the login was missing or wrong (e.g. in an inspector or a client not set up yet). Now the guides are listed and issues are simply left out. Found through Glama's inspection.
package/README.md CHANGED
@@ -8,7 +8,22 @@
8
8
 
9
9
  **Huly for AI agents.** An MCP server for [Huly](https://huly.io) — self-hosted or Huly Cloud — that lets Claude and other agents work in your workspaces: issues, epics, milestones, documents, chat, inbox, planner, test management and more. 227 tools, built for agent workflows and to spend as few tokens as possible.
10
10
 
11
- [Website](https://jokeks2023.github.io/huly-mcp/) · [Quick start](#quick-start) · [Tools](#tools-227-total-grouped-in-toolsets) · [Claude Code plugin](#claude-code-plugin) · [Changelog](https://github.com/JoKeks2023/huly-mcp/blob/main/CHANGELOG.md)
11
+ <p align="center">
12
+ <b><a href="#quick-start">Quick start</a></b> ·
13
+ <a href="#compatible-clients">Clients</a> ·
14
+ <a href="#claude-code-plugin">Claude Code plugin</a> ·
15
+ <a href="#command-line">Command line</a> ·
16
+ <a href="#team-server-over-http">Team server</a> ·
17
+ <a href="#tools-227-total-grouped-in-toolsets">Tools</a> ·
18
+ <a href="#configuration">Configuration</a> ·
19
+ <a href="#authentication">Authentication</a>
20
+ <br>
21
+ <a href="https://jokeks2023.github.io/huly-mcp/">Website</a> ·
22
+ <a href="https://github.com/JoKeks2023/huly-mcp/wiki/">Docs</a> ·
23
+ <a href="https://github.com/JoKeks2023/huly-mcp/blob/main/CHANGELOG.md">Changelog</a> ·
24
+ <a href="https://www.npmjs.com/package/huly-mcp-selfhost">npm</a> ·
25
+ <a href="https://hub.docker.com/r/jokeks2023/huly-mcp">Docker Hub</a>
26
+ </p>
12
27
 
13
28
  ## Highlights
14
29
 
@@ -21,14 +36,16 @@
21
36
  - **Claude Code plugin:** `/huly:setup` asks once per repo how Huly is used there and writes the rules; skill, hooks and `/huly:*` commands. Guides and a `setup` prompt for every other MCP client too.
22
37
  - **Self-hosted first:** document and description content, file uploads and internal URLs work on `huly-selfhost`; one account, all its workspaces.
23
38
 
39
+ <div align="right"><a href="#huly-mcp-selfhost">↑ Back to top</a></div>
40
+
24
41
  ## Tokens, measured
25
42
 
26
43
  What the model reads — measured on a real self-hosted project (≈60 issues):
27
44
 
28
45
  | | Characters | ≈ Tokens |
29
46
  |---|--:|--:|
30
- | Tool list, default toolsets (99 tools, each callable directly) | 38,259 | 10.9k |
31
- | Tool list, pinned workspace | 34,567 | 9.9k |
47
+ | Tool list, default toolsets (99 tools, each callable directly) | 38,991 | 11.1k |
48
+ | Tool list, pinned workspace | 35,299 | 10.1k |
32
49
  | Open issues of a project (one call, ≈30 issues) | 2,768 | 0.8k |
33
50
  | One issue with description | 318 | 0.1k |
34
51
  | Project report (done, in progress, blocked, milestones, PRs, time) | ~2,700 | 0.8k |
@@ -40,7 +57,7 @@ Lists default to open items, empty fields are left out, and one call answers wha
40
57
 
41
58
  <picture>
42
59
  <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/JoKeks2023/huly-mcp/main/docs/assets/benchmark-dark.svg">
43
- <img alt="Characters per answer: huly-mcp-selfhost needs 2.9× less text per answer (median), 14× smaller tool definitions, one call instead of up to four" src="https://raw.githubusercontent.com/JoKeks2023/huly-mcp/main/docs/assets/benchmark-light.svg" width="900">
60
+ <img alt="Characters per answer: huly-mcp-selfhost needs 2.9× less text per answer (median), 13× smaller tool definitions, one call instead of up to four" src="https://raw.githubusercontent.com/JoKeks2023/huly-mcp/main/docs/assets/benchmark-light.svg" width="900">
44
61
  </picture>
45
62
 
46
63
  Same self-hosted Huly, same questions, the best parameters on both sides, measured 2026-11-11. For the other server only the text content is counted, not the duplicate JSON copy it also returns. Where we return more (components), it is because we include each component's description and code paths.
@@ -59,13 +76,15 @@ Same self-hosted Huly, same questions, the best parameters on both sides, measur
59
76
  | Milestones | 208 (1 call) | 503 (1 call) | 2.4× less |
60
77
  | Components | 1,302 (1 call) | 692 (1 call) | 2.0× more |
61
78
  | Documents of a teamspace | 106 (1 call) | 305 (1 call) | 2.9× less |
62
- | Tool definitions (49 tools with the same name) | 15,728 | 216,890 | 13.8× less |
79
+ | Tool definitions (49 tools with the same name) | 16,579 | 216,890 | 13.1× less |
63
80
 
64
81
  </details>
65
82
 
83
+ <div align="right"><a href="#huly-mcp-selfhost">↑ Back to top</a></div>
84
+
66
85
  ## Quick start
67
86
 
68
- You need Node.js 20+ and a Huly account — [Huly Cloud](https://huly.app) or self-hosted.
87
+ You need a Huly account — [Huly Cloud](https://huly.app) or self-hosted — and, except for the Claude Desktop extension, Node.js 20+.
69
88
 
70
89
  **Claude Code** — install the plugin, then fill in its settings when asked:
71
90
 
@@ -76,7 +95,9 @@ You need Node.js 20+ and a Huly account — [Huly Cloud](https://huly.app) or se
76
95
 
77
96
  Then run `/huly:setup` in a repository. Details: [Claude Code plugin](#claude-code-plugin).
78
97
 
79
- **Claude Desktop, Cursor, VS Code, Windsurf, Zed, …** — add the server to your client's MCP config:
98
+ **Claude Desktop** — download [`huly-mcp-selfhost.mcpb`](https://github.com/JoKeks2023/huly-mcp/releases/latest/download/huly-mcp-selfhost.mcpb), double-click it and enter your Huly login. Nothing else to install.
99
+
100
+ **Cursor, VS Code, Windsurf, Zed, …** (or Claude Desktop by hand) — add the server to your client's MCP config:
80
101
 
81
102
  ```json
82
103
  {
@@ -105,6 +126,8 @@ Then ask: *"What's on my plate today?"*, *"Plan the login redesign as an epic in
105
126
 
106
127
  ---
107
128
 
129
+ <div align="right"><a href="#huly-mcp-selfhost">↑ Back to top</a></div>
130
+
108
131
  ## Claude Code plugin
109
132
 
110
133
  The repo is also a Claude Code plugin marketplace. The plugin bundles the MCP server with a workflow skill, hooks and commands:
@@ -114,7 +137,7 @@ The repo is also a Claude Code plugin marketplace. The plugin bundles the MCP se
114
137
  /plugin install huly@huly-mcp
115
138
  ```
116
139
 
117
- > **Note:** the plugin starts the npm package at exactly its own version (e.g. `huly-mcp-selfhost@3.5.1`; the plugin directory does not accept version ranges), so a plugin update brings the matching server. To try a local checkout instead: `npm run build`, then point `plugin/.mcp.json` at `dist/index.js` and start `claude --plugin-dir ./plugin`.
140
+ > **Note:** the plugin starts the npm package at exactly its own version (e.g. `huly-mcp-selfhost@3.6.0`; the plugin directory does not accept version ranges), so a plugin update brings the matching server. To try a local checkout instead: `npm run build`, then point `plugin/.mcp.json` at `dist/index.js` and start `claude --plugin-dir ./plugin`.
118
141
 
119
142
  Claude Code then asks for your Huly settings (e-mail + password or token, and for self-hosted Huly the accounts and front URL; passwords and tokens go to the OS keychain).
120
143
 
@@ -135,6 +158,8 @@ Without the plugin, the same server works in Claude Code as a plain MCP server (
135
158
 
136
159
  ---
137
160
 
161
+ <div align="right"><a href="#huly-mcp-selfhost">↑ Back to top</a></div>
162
+
138
163
  ## Command line
139
164
 
140
165
  Every tool also works from the terminal — for scripts, CI and quick looks without a chat. Same login (`HULY_*` environment or `--env-file`), same safety rules (write scope, audit log, confirmation for hard-to-undo changes).
@@ -159,6 +184,8 @@ huly-mcp my_day --format json | jq -r .text
159
184
 
160
185
  ---
161
186
 
187
+ <div align="right"><a href="#huly-mcp-selfhost">↑ Back to top</a></div>
188
+
162
189
  ## Tools (227 total, grouped in toolsets)
163
190
 
164
191
  All tools except `list_workspaces` take a `workspace` argument — required for every change, optional for reads when only one workspace is available.
@@ -210,8 +237,8 @@ Context cost of `tools/list` (≈3.5 chars/token):
210
237
 
211
238
  | | default (99 tools) | `all` (227 tools) |
212
239
  |---|--:|--:|
213
- | several workspaces | ~38.3k chars ≈ **10.9k tokens** | ~83.5k chars ≈ **23.9k tokens** |
214
- | pinned with `HULY_WORKSPACE` | ~34.6k chars ≈ **9.9k tokens** | ~75.0k chars ≈ **21.4k tokens** |
240
+ | several workspaces | ~39.0k chars ≈ **11.1k tokens** | ~84.2k chars ≈ **24.1k tokens** |
241
+ | pinned with `HULY_WORKSPACE` | ~35.3k chars ≈ **10.1k tokens** | ~75.8k chars ≈ **21.7k tokens** |
215
242
 
216
243
  For comparison, 2.2.0 needed ≈12.8k tokens for 64 tools. **Using one workspace? Set `HULY_WORKSPACE`** — the `workspace` argument then disappears from every tool (−10%). **Claude Code** loads MCP tools on demand, so there even `HULY_TOOLSETS=all` costs little up front.
217
244
 
@@ -434,6 +461,8 @@ Options: `--run <name>`, `--parent-suite <suite>`, `--no-create` (only existing
434
461
 
435
462
  ---
436
463
 
464
+ <div align="right"><a href="#huly-mcp-selfhost">↑ Back to top</a></div>
465
+
437
466
  ## Workspaces (one account, several workspaces)
438
467
 
439
468
  A single Huly account can belong to several workspaces — e.g. a personal one and a team one. This server logs in **once** with your account credentials and discovers every workspace that account can access via the account service (`getUserWorkspaces()`). You **don't configure a workspace** in the environment.
@@ -452,6 +481,8 @@ A single Huly account can belong to several workspaces — e.g. a personal one a
452
481
 
453
482
  ---
454
483
 
484
+ <div align="right"><a href="#huly-mcp-selfhost">↑ Back to top</a></div>
485
+
455
486
  ## Configuration
456
487
 
457
488
  | Variable | Required | Purpose |
@@ -479,6 +510,8 @@ A single Huly account can belong to several workspaces — e.g. a personal one a
479
510
 
480
511
  ---
481
512
 
513
+ <div align="right"><a href="#huly-mcp-selfhost">↑ Back to top</a></div>
514
+
482
515
  ## Authentication
483
516
 
484
517
  Create a `.env` file in the project root (or pass via `env` in your client config). No workspace is needed in either option.
@@ -522,6 +555,8 @@ HULY_FRONT_URL=https://your-huly-instance.com # document content
522
555
 
523
556
  ---
524
557
 
558
+ <div align="right"><a href="#huly-mcp-selfhost">↑ Back to top</a></div>
559
+
525
560
  ## Compatible clients
526
561
 
527
562
  The same MCP server works across all major AI coding tools. All examples use **email + password** and the npm package via `npx`; for SSO accounts replace the two credentials with `"HULY_TOKEN": "your-token"` (see [Authentication](#authentication)). To run a local build instead, replace `npx` + args with `node` + `["/absolute/path/to/huly-mcp/dist/index.js"]`.
@@ -532,6 +567,10 @@ The same MCP server works across all major AI coding tools. All examples use **e
532
567
 
533
568
  ### Claude Desktop
534
569
 
570
+ **One click:** download [`huly-mcp-selfhost.mcpb`](https://github.com/JoKeks2023/huly-mcp/releases/latest/download/huly-mcp-selfhost.mcpb) and double-click it (or *Settings → Extensions → Advanced settings → Install Extension…*). Claude Desktop shows a form for your Huly login; password and token go to the system keychain. No Node.js or JSON needed — Claude Desktop brings its own Node.js.
571
+
572
+ **Or by hand** in the config file:
573
+
535
574
  - **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
536
575
  - **Windows:** `%APPDATA%\Claude\claude_desktop_config.json`
537
576
 
@@ -703,11 +742,13 @@ For network access (HTTP/SSE) use the [Docker image](#docker).
703
742
 
704
743
  ---
705
744
 
745
+ <div align="right"><a href="#huly-mcp-selfhost">↑ Back to top</a></div>
746
+
706
747
  ## Running the server
707
748
 
708
749
  ### npx (recommended)
709
750
 
710
- Every client example uses `"command": "npx", "args": ["-y", "huly-mcp-selfhost"]`. Pin a version (`huly-mcp-selfhost@3.5.1`) if you want updates only when you choose.
751
+ Every client example uses `"command": "npx", "args": ["-y", "huly-mcp-selfhost"]`. Pin a version (`huly-mcp-selfhost@3.6.0`) if you want updates only when you choose.
711
752
 
712
753
  ### From source
713
754
 
@@ -766,6 +807,8 @@ Set `MCP_ALLOWED_HOSTS` to the host names clients use (DNS-rebinding protection)
766
807
 
767
808
  ---
768
809
 
810
+ <div align="right"><a href="#huly-mcp-selfhost">↑ Back to top</a></div>
811
+
769
812
  ## Example prompts
770
813
 
771
814
  **Workspaces:**
@@ -824,6 +867,8 @@ Set `MCP_ALLOWED_HOSTS` to the host names clients use (DNS-rebinding protection)
824
867
 
825
868
  ---
826
869
 
870
+ <div align="right"><a href="#huly-mcp-selfhost">↑ Back to top</a></div>
871
+
827
872
  ## Document content
828
873
 
829
874
  ### Reading: `get_document`
@@ -890,6 +935,8 @@ The Mermaid block renders as a live interactive diagram in Huly's document edito
890
935
 
891
936
  ---
892
937
 
938
+ <div align="right"><a href="#huly-mcp-selfhost">↑ Back to top</a></div>
939
+
893
940
  ## Bulk CSV Import
894
941
 
895
942
  Import many issues at once from a CSV file — useful for migrating from other tools:
@@ -914,6 +961,8 @@ Required column: `title`. Optional: `priority` (Urgent/High/Medium/Low), `status
914
961
 
915
962
  ---
916
963
 
964
+ <div align="right"><a href="#huly-mcp-selfhost">↑ Back to top</a></div>
965
+
917
966
  ## Architecture
918
967
 
919
968
  ```text
@@ -940,6 +989,8 @@ See [`src/connection.ts`](https://github.com/JoKeks2023/huly-mcp/blob/main/src/c
940
989
 
941
990
  ---
942
991
 
992
+ <div align="right"><a href="#huly-mcp-selfhost">↑ Back to top</a></div>
993
+
943
994
  ## Development and CI
944
995
 
945
996
  ```bash
@@ -979,16 +1030,22 @@ Secrets:
979
1030
 
980
1031
  ---
981
1032
 
1033
+ <div align="right"><a href="#huly-mcp-selfhost">↑ Back to top</a></div>
1034
+
982
1035
  ## Changelog
983
1036
 
984
1037
  See [CHANGELOG.md](https://github.com/JoKeks2023/huly-mcp/blob/main/CHANGELOG.md).
985
1038
 
986
1039
  ---
987
1040
 
1041
+ <div align="right"><a href="#huly-mcp-selfhost">↑ Back to top</a></div>
1042
+
988
1043
  ## Credits
989
1044
 
990
1045
  Started as a fork of [varaprasadreddy9676/huly-mcp](https://github.com/varaprasadreddy9676/huly-mcp) (npm `huly-mcp-sdk`) by SaiVaraprasad Medapati and grown from its 33 tools to 227, with self-hosted support, toolsets, agent workflow and the Claude Code plugin. Published separately as `huly-mcp-selfhost`. Not affiliated with Huly Labs.
991
1046
 
1047
+ <div align="right"><a href="#huly-mcp-selfhost">↑ Back to top</a></div>
1048
+
992
1049
  ## Links
993
1050
 
994
1051
  - Website: https://jokeks2023.github.io/huly-mcp/
@@ -996,6 +1053,10 @@ Started as a fork of [varaprasadreddy9676/huly-mcp](https://github.com/varaprasa
996
1053
  - npm: https://www.npmjs.com/package/huly-mcp-selfhost · Docker: https://hub.docker.com/r/jokeks2023/huly-mcp · MCP Registry: `io.github.JoKeks2023/huly-mcp-selfhost`
997
1054
  - Security: [SECURITY.md](https://github.com/JoKeks2023/huly-mcp/blob/main/SECURITY.md)
998
1055
 
1056
+ <div align="right"><a href="#huly-mcp-selfhost">↑ Back to top</a></div>
1057
+
999
1058
  ## License
1000
1059
 
1001
1060
  [Eclipse Public License 2.0](https://github.com/JoKeks2023/huly-mcp/blob/main/LICENSE)
1061
+
1062
+ <div align="right"><a href="#huly-mcp-selfhost">↑ Back to top</a></div>
package/dist/index.js CHANGED
@@ -90627,7 +90627,7 @@ async function socialIdNames(client, ids) {
90627
90627
  if (ids.length === 0) return /* @__PURE__ */ new Map();
90628
90628
  const identities = await client.findAll(import_contact.default.class.SocialIdentity, { _id: { $in: ids } });
90629
90629
  const names2 = await peopleNames(client);
90630
- return new Map(identities.map((i) => [i._id, names2.get(i.attachedTo) ?? "Unknown"]));
90630
+ return new Map(identities.map((i) => [i._id, names2.get(i.attachedTo) ?? (i.type === "github" && typeof i.value === "string" ? `${i.value} (GitHub)` : "Unknown")]));
90631
90631
  }
90632
90632
  async function currentAccountUuid(client) {
90633
90633
  const me = await currentPerson(client);
@@ -92316,7 +92316,7 @@ var createDocument = wrapToolHandler(async (args) => {
92316
92316
  docId
92317
92317
  );
92318
92318
  return `\u2705 Document **"${args.title}"** created (id: \`${docId}\`) in teamspace "${teamspace.name}".
92319
- Open it in Huly to add content via the editor.`;
92319
+ Write its content with update_document.`;
92320
92320
  });
92321
92321
  var deleteDocument = wrapToolHandler(async (args) => {
92322
92322
  const client = await getConnection(args.workspace);
@@ -94223,20 +94223,26 @@ var changesSince = wrapToolHandler(async (args) => {
94223
94223
  ]);
94224
94224
  const authors = await socialIdNames(client, [...new Set([...updates, ...comments].map((m) => m.createdBy).filter((x) => x != null))]);
94225
94225
  const statusName2 = new Map(statuses.map((s) => [s._id, s.name]));
94226
- const lines = issues.map((issue2) => {
94226
+ const rows = issues.map((issue2) => {
94227
94227
  const ups = updates.filter((u) => u.attachedTo === issue2._id);
94228
- const fields = [...new Set(ups.map((u) => u.attributeUpdates?.attrKey).filter((k) => k != null && k !== "rank").map((k) => k === "attachedTo" ? "parent" : k))];
94229
94228
  const cs = comments.filter((c) => c.attachedTo === issue2._id);
94229
+ const isNew = issue2.createdOn >= since;
94230
+ const fields = isNew ? [] : [...new Set(ups.map((u) => u.attributeUpdates?.attrKey).filter((k) => k != null && k !== "rank").map((k) => k === "attachedTo" ? "parent" : k))];
94230
94231
  const who = [...new Set([...ups, ...cs].map((m) => authors.get(m.createdBy)).filter((n) => n != null))];
94231
94232
  const parts = [
94232
- issue2.createdOn >= since ? "new" : void 0,
94233
+ isNew ? "new" : void 0,
94233
94234
  fields.length > 0 ? `changed ${fields.join(", ")}` : void 0,
94234
94235
  cs.length > 0 ? `+${cs.length} comment(s)` : void 0
94235
94236
  ].filter((p) => p != null);
94237
+ return { issue: issue2, who, what: parts.length > 0 ? parts.join(" \xB7 ") : "updated" };
94238
+ });
94239
+ const everyone = [...new Set(rows.flatMap((r) => r.who))];
94240
+ const single = everyone.length === 1 && rows.every((r) => r.who.length === 1);
94241
+ const lines = rows.map(({ issue: issue2, who, what }) => {
94236
94242
  const kind = issue2._class === C.PullRequest ? "PR " : "";
94237
- return `- ${kind}**${issue2.identifier}** [${statusName2.get(issue2.status) ?? "?"}] ${issue2.title} \u2014 ${parts.length > 0 ? parts.join(" \xB7 ") : "updated"}${who.length > 0 ? ` (by ${who.join(", ")})` : ""} \xB7 ${formatDate(issue2.modifiedOn)}`;
94243
+ return `- ${kind}**${issue2.identifier}** [${statusName2.get(issue2.status) ?? "?"}] ${issue2.title} \u2014 ${what}${!single && who.length > 0 ? ` (by ${who.join(", ")})` : ""} \xB7 ${formatDate(issue2.modifiedOn)}`;
94238
94244
  });
94239
- return `## Changes since ${new Date(since).toISOString()} (${issues.length}${issues.length === args.limit ? "+" : ""})
94245
+ return `## Changes since ${new Date(since).toISOString()} (${issues.length}${issues.length === args.limit ? "+" : ""})${single ? `, all by ${everyone[0]}` : ""}
94240
94246
 
94241
94247
  ${lines.join("\n")}`;
94242
94248
  });
@@ -97322,6 +97328,7 @@ var triageInbox = wrapToolHandler(async (args) => {
97322
97328
  const ctxs = await client.findAll(C.DocNotifyContext, { _id: { $in: ctxIds } });
97323
97329
  const statuses = new Map((await client.findAll("core:class:Status", {})).map((s) => [s._id, s.name]));
97324
97330
  const names2 = await peopleNames(client);
97331
+ const seen = /* @__PURE__ */ new Map();
97325
97332
  const out = [`# Inbox triage \u2014 ${unread.length}${unread.length === 500 ? "+" : ""} unread on ${groups.size} item(s)${groups.size > limit ? `, newest ${limit} shown` : ""}`];
97326
97333
  for (const ctxId of ctxIds) {
97327
97334
  const notes = groups.get(ctxId);
@@ -97342,10 +97349,23 @@ var triageInbox = wrapToolHandler(async (args) => {
97342
97349
  const comments = msgs.filter((m) => typeof m.message === "string" && (m._class === "chunter:class:ChatMessage" || h.isDerived(m._class, "chunter:class:ChatMessage")));
97343
97350
  const fields = [...new Set(msgs.filter((m) => m._class === C.DocUpdateMessage).map((m) => m.action === "create" ? "created" : m.attributeUpdates?.attrKey === "attachedTo" ? "parent" : m.attributeUpdates?.attrKey).filter((k) => k != null && !["rank", "subIssues", "comments", "modifiedOn"].includes(k)))];
97344
97351
  if (fields.length > 0) out.push(`- changed: ${fields.join(", ")}`);
97345
- for (const m of comments.slice(0, 3).reverse()) out.push(`- \u{1F4AC} **${who.get(m.createdBy) ?? "?"}**: ${clip(plain(m.message), 200, "list_comments")}`);
97352
+ for (const m of comments.slice(0, 3).reverse()) {
97353
+ const text3 = plain(m.message);
97354
+ const author = who.get(m.createdBy) ?? "?";
97355
+ const key = `${author}\0${text3.slice(0, 160)}`;
97356
+ const first = seen.get(key);
97357
+ if (first != null) {
97358
+ first.count++;
97359
+ out.push(`- \u{1F4AC} **${author}**: _same comment as on ${first.where}_`);
97360
+ continue;
97361
+ }
97362
+ seen.set(key, { line: out.length, where: label2.split(/[:\s]/)[0], count: 1 });
97363
+ out.push(`- \u{1F4AC} **${author}**: ${clip(text3, 200, "list_comments")}`);
97364
+ }
97346
97365
  if (comments.length > 3) out.push(`- _${comments.length - 3} older new comment(s)_`);
97347
97366
  if (fields.length === 0 && comments.length === 0) out.push("- (no details recorded)");
97348
97367
  }
97368
+ for (const f of seen.values()) if (f.count > 1) out[f.line] += ` _(same on ${f.count} items)_`;
97349
97369
  out.push("", "_Done with an item? mark_notifications_read with its id (e.g. PROJ-12)._");
97350
97370
  return out.join("\n");
97351
97371
  });
@@ -97541,7 +97561,7 @@ var ListComponentsSchema = external_exports.object({
97541
97561
  var CreateComponentSchema = external_exports.object({
97542
97562
  workspace: WorkspaceArg,
97543
97563
  projectIdentifier: external_exports.string().describe("Project key"),
97544
- label: external_exports.string().min(1),
97564
+ label: external_exports.string().min(1).describe("Component name"),
97545
97565
  description: external_exports.string().optional(),
97546
97566
  lead: external_exports.string().optional().describe("Person name")
97547
97567
  });
@@ -97586,7 +97606,7 @@ var GetDocumentSchema = external_exports.object({
97586
97606
  var CreateDocumentSchema = external_exports.object({
97587
97607
  workspace: WorkspaceArg,
97588
97608
  teamspaceId: external_exports.string().describe("Teamspace id"),
97589
- title: external_exports.string().min(1),
97609
+ title: external_exports.string().min(1).describe("Document title"),
97590
97610
  parentId: external_exports.string().optional().describe("Parent document id; omit for top-level")
97591
97611
  });
97592
97612
  var SearchIssuesSchema = external_exports.object({
@@ -97606,7 +97626,7 @@ var ListLabelsSchema = external_exports.object({
97606
97626
  });
97607
97627
  var CreateLabelSchema = external_exports.object({
97608
97628
  workspace: WorkspaceArg,
97609
- title: external_exports.string().min(1),
97629
+ title: external_exports.string().min(1).describe("Label name"),
97610
97630
  color: external_exports.string().optional().describe("Hex color, e.g. #FF6B6B")
97611
97631
  });
97612
97632
  var AddLabelSchema = external_exports.object({
@@ -98500,7 +98520,7 @@ var TOOLS = [
98500
98520
  { name: "undo_changes", toolset: "core", description: "Undo your last changes made through this server: created \u2192 removed, changed \u2192 old values, deleted \u2192 restored. Dry run first", schema: UndoChangesSchema, handler: undoChanges },
98501
98521
  { name: "check_setup", toolset: "core", description: "Diagnose the setup: version and updates, login, workspaces, front URL, settings (no secrets)", schema: CheckSetupSchema, handler: checkSetup },
98502
98522
  { name: "list_projects", toolset: "tracker", description: "List all projects in a Huly workspace", schema: ListProjectsSchema, handler: listProjects },
98503
- { name: "get_project", toolset: "tracker", description: 'Get a project by its identifier (e.g. "PROJ")', schema: GetProjectSchema, handler: getProject },
98523
+ { name: "get_project", toolset: "tracker", description: 'Project by key (e.g. "PROJ"): name, description, issue count, statuses', schema: GetProjectSchema, handler: getProject },
98504
98524
  { name: "create_project", toolset: "tracker-admin", description: "Create a tracker project", schema: CreateProjectSchema, handler: createProject },
98505
98525
  { name: "update_project", toolset: "tracker-admin", description: "Update project name, description, default assignee/status", schema: UpdateProjectSchema, handler: updateProject },
98506
98526
  { name: "list_issues", toolset: "tracker", description: "List issues of a project; filters: status, priority, milestone, component, label, assignee, parent", schema: ListIssuesSchema, handler: listIssues },
@@ -98513,12 +98533,12 @@ var TOOLS = [
98513
98533
  { name: "list_sub_issues", toolset: "tracker", description: "Sub-issue tree of an issue with progress", schema: ListSubIssuesSchema, handler: listSubIssues },
98514
98534
  { name: "add_comment", toolset: "tracker", description: "Add a comment to an issue", schema: AddCommentSchema, handler: addComment },
98515
98535
  { name: "list_comments", toolset: "tracker", description: "List comments of an issue (with ids)", schema: ListCommentsSchema, handler: listComments },
98516
- { name: "delete_comment", toolset: "tracker", description: "Delete a comment by id", schema: DeleteCommentSchema, handler: deleteComment },
98536
+ { name: "delete_comment", toolset: "tracker", description: "Permanently delete a comment on an issue (commentId from list_comments); to edit: update_comment", schema: DeleteCommentSchema, handler: deleteComment },
98517
98537
  { name: "update_comment", toolset: "tracker", description: "Edit a comment by id", schema: UpdateCommentSchema, handler: updateComment },
98518
98538
  { name: "log_time", toolset: "tracker", description: "Log hours spent on an issue", schema: LogTimeSchema, handler: logTime },
98519
98539
  { name: "list_time_reports", toolset: "tracker", description: "Logged time for an issue or project, totals per person; person/date filters", schema: ListTimeReportsSchema, handler: listTimeReports },
98520
98540
  { name: "list_labels", toolset: "tracker", description: "List all labels in the workspace", schema: ListLabelsSchema, handler: listLabels },
98521
- { name: "create_label", toolset: "tracker", description: "Create a new label with an optional hex color", schema: CreateLabelSchema, handler: createLabel },
98541
+ { name: "create_label", toolset: "tracker", description: "Create a workspace-wide label (optional hex color); an existing title is reused. Tag an issue: add_label", schema: CreateLabelSchema, handler: createLabel },
98522
98542
  { name: "add_label", toolset: "tracker", description: "Add label to issue (auto-creates label)", schema: AddLabelSchema, handler: addLabel },
98523
98543
  { name: "remove_label", toolset: "tracker", description: "Remove a label from an issue", schema: RemoveLabelSchema, handler: removeLabel },
98524
98544
  { name: "update_label", toolset: "tracker-admin", description: "Rename/recolor a label (on every issue)", schema: UpdateLabelSchema, handler: updateLabel },
@@ -98536,14 +98556,14 @@ var TOOLS = [
98536
98556
  { name: "update_milestone", toolset: "tracker", description: "Update a milestone (name, target date, status, description)", schema: UpdateMilestoneSchema, handler: updateMilestone },
98537
98557
  { name: "delete_milestone", toolset: "tracker-admin", description: "Delete milestone; its issues are unassigned or moved (moveIssuesTo)", schema: DeleteMilestoneSchema, handler: deleteMilestone },
98538
98558
  { name: "list_components", toolset: "tracker", description: "List components (sub-areas) in a project", schema: ListComponentsSchema, handler: listComponents },
98539
- { name: "create_component", toolset: "tracker", description: "Create a new component in a project", schema: CreateComponentSchema, handler: createComponent },
98559
+ { name: "create_component", toolset: "tracker", description: "Create a component (sub-area) in a project; issues get it via componentLabel. Change it: update_component", schema: CreateComponentSchema, handler: createComponent },
98540
98560
  { name: "update_component", toolset: "tracker", description: "Update a component (name, description, lead)", schema: UpdateComponentSchema, handler: updateComponent },
98541
98561
  { name: "delete_component", toolset: "tracker-admin", description: "Delete component; its issues are unassigned or moved (moveIssuesTo)", schema: DeleteComponentSchema, handler: deleteComponent },
98542
- { name: "list_teamspaces", toolset: "documents", description: "List all document teamspaces in the workspace", schema: ListTeamspacesSchema, handler: listTeamspaces },
98562
+ { name: "list_teamspaces", toolset: "documents", description: "List document teamspaces with their ids (teamspaceId for list_documents, create_document)", schema: ListTeamspacesSchema, handler: listTeamspaces },
98543
98563
  { name: "create_teamspace", toolset: "documents", description: "Create a document teamspace", schema: CreateTeamspaceSchema, handler: createTeamspace },
98544
98564
  { name: "list_documents", toolset: "documents", description: "List documents in a teamspace", schema: ListDocumentsSchema, handler: listDocuments },
98545
98565
  { name: "get_document", toolset: "documents", description: "Document metadata and content", schema: GetDocumentSchema, handler: getDocument },
98546
- { name: "create_document", toolset: "documents", description: "Create a new document in a teamspace", schema: CreateDocumentSchema, handler: createDocument },
98566
+ { name: "create_document", toolset: "documents", description: "Create an empty document in a teamspace, optionally under a parent; returns its id. Write the body with update_document", schema: CreateDocumentSchema, handler: createDocument },
98547
98567
  { name: "delete_document", toolset: "documents", description: "Permanently delete a document by its ID", schema: DeleteDocumentSchema, handler: deleteDocument },
98548
98568
  { name: "update_document", toolset: "documents", description: "Replace content (Markdown), rename or move a document", schema: UpdateDocumentSchema, handler: updateDocument },
98549
98569
  { name: "list_document_comments", toolset: "collaboration", description: "Comments on a document", schema: ListDocumentCommentsSchema, handler: listDocumentComments },
@@ -98557,11 +98577,11 @@ var TOOLS = [
98557
98577
  { name: "list_messages", toolset: "chat", description: "List messages in a channel or direct message", schema: ListMessagesSchema, handler: listMessages },
98558
98578
  { name: "attach_file", toolset: "tracker", description: "Attach a file to an issue (base64-encoded content)", schema: AttachFileSchema, handler: attachFile },
98559
98579
  { name: "list_attachments", toolset: "tracker", description: "List files attached to an issue", schema: ListAttachmentsSchema, handler: listAttachments },
98560
- { name: "delete_attachment", toolset: "tracker", description: "Delete a file attachment from an issue", schema: DeleteAttachmentSchema, handler: deleteAttachment },
98580
+ { name: "delete_attachment", toolset: "tracker", description: "Permanently delete a file attached to an issue (attachmentId from list_attachments)", schema: DeleteAttachmentSchema, handler: deleteAttachment },
98561
98581
  { name: "list_issue_statuses", toolset: "tracker", description: "List all issue statuses (workflow states), grouped by phase", schema: ListIssueStatusesSchema, handler: listIssueStatuses },
98562
98582
  { name: "create_issue_status", toolset: "tracker-admin", description: "Create a workspace-wide issue status", schema: CreateIssueStatusSchema, handler: createIssueStatus },
98563
98583
  { name: "list_organizations", toolset: "contacts", description: "List all organizations (companies) in the workspace", schema: ListOrganizationsSchema, handler: listOrganizations },
98564
- { name: "get_organization", toolset: "contacts", description: "Get details of an organization, including description", schema: GetOrganizationSchema, handler: getOrganization },
98584
+ { name: "get_organization", toolset: "contacts", description: "Organization (company) by id from list_organizations: city, linked members, description", schema: GetOrganizationSchema, handler: getOrganization },
98565
98585
  { name: "create_organization", toolset: "contacts", description: "Create a new organization (company contact)", schema: CreateOrganizationSchema, handler: createOrganization },
98566
98586
  { name: "update_organization", toolset: "contacts", description: "Set the description of an organization from Markdown", schema: UpdateOrganizationSchema, handler: updateOrganization },
98567
98587
  // Chat: threads, editing, reactions, pins (channel messages and issue comments)
@@ -98569,9 +98589,9 @@ var TOOLS = [
98569
98589
  { name: "reply_to_message", toolset: "chat", description: "Reply in the thread of a message or comment", schema: ReplyToMessageSchema, handler: replyToMessage },
98570
98590
  { name: "update_message", toolset: "chat", description: "Edit a message or thread reply", schema: UpdateMessageSchema, handler: updateMessage },
98571
98591
  { name: "delete_message", toolset: "chat", description: "Delete a message or thread reply", schema: DeleteMessageSchema, handler: deleteMessage },
98572
- { name: "add_reaction", toolset: "chat", description: "React to a message/comment with an emoji", schema: ReactionSchema, handler: addReaction },
98592
+ { name: "add_reaction", toolset: "chat", description: "Add your emoji reaction to a message or comment (no-op if already there); undo: remove_reaction", schema: ReactionSchema, handler: addReaction },
98573
98593
  { name: "remove_reaction", toolset: "chat", description: "Remove your emoji reaction", schema: ReactionSchema, handler: removeReaction },
98574
- { name: "pin_message", toolset: "chat", description: "Pin or unpin a message", schema: PinMessageSchema, handler: pinMessage },
98594
+ { name: "pin_message", toolset: "chat", description: "Pin a message or comment in its channel; pinned=false unpins", schema: PinMessageSchema, handler: pinMessage },
98575
98595
  // Inbox
98576
98596
  { name: "list_notifications", toolset: "inbox", description: "Your inbox notifications, grouped by issue/PR/document", schema: ListNotificationsSchema, handler: listNotifications },
98577
98597
  { name: "triage_inbox", toolset: "inbox", description: "Unread items with what changed since last read: new comments, fields, status", schema: TriageInboxSchema, handler: triageInbox },
@@ -98580,7 +98600,7 @@ var TOOLS = [
98580
98600
  // Activity
98581
98601
  { name: "get_activity", toolset: "activity", description: "Change history and comments of an issue or object", schema: GetActivitySchema, handler: getActivity },
98582
98602
  // Planner
98583
- { name: "list_todos", toolset: "planner", description: "Your planner to-dos", schema: ListTodosSchema, handler: listTodos },
98603
+ { name: "list_todos", toolset: "planner", description: "List your own planner to-dos: open by default, in planner order; filter by status or due date", schema: ListTodosSchema, handler: listTodos },
98584
98604
  { name: "create_todo", toolset: "planner", description: "Create a to-do, optionally on an issue", schema: CreateTodoSchema, handler: createTodo },
98585
98605
  { name: "update_todo", toolset: "planner", description: "Update a to-do or mark it done", schema: UpdateTodoSchema, handler: updateTodo },
98586
98606
  { name: "delete_todo", toolset: "planner", description: "Delete a to-do", schema: DeleteTodoSchema, handler: deleteTodo },
@@ -98616,7 +98636,7 @@ var TOOLS = [
98616
98636
  { name: "find_project_for_repo", toolset: "agent", description: "Huly workspace + project linked to a GitHub repo", schema: FindProjectForRepoSchema, handler: findProjectForRepo },
98617
98637
  { name: "release_issue", toolset: "agent", description: "Release an agent claim", schema: ReleaseIssueSchema, handler: releaseIssue },
98618
98638
  // Generic access to any class
98619
- { name: "search", toolset: "generic", description: "Fulltext search across everything (issues, docs, messages, cards, files \u2026)", schema: SearchAllSchema, handler: searchAll },
98639
+ { name: "search", toolset: "generic", description: "Fulltext search across all content (issues, docs, messages, cards, files \u2026); issues only, with status: search_issues", schema: SearchAllSchema, handler: searchAll },
98620
98640
  { name: "list_classes", toolset: "generic", description: "Huly data classes (for modules without dedicated tools)", schema: ListClassesSchema, handler: listClasses },
98621
98641
  { name: "describe_class", toolset: "generic", description: "Attributes and types of a class", schema: DescribeClassSchema, handler: describeClass },
98622
98642
  { name: "query_documents", toolset: "generic", description: "Read documents of any class (secrets redacted)", schema: QueryDocumentsSchema, handler: queryDocuments },
@@ -99150,7 +99170,7 @@ function createServer(options = {}) {
99150
99170
  if (enabled.has("agent")) instructions += `
99151
99171
  - Guides: resources huly://guide/workflow, huly://guide/costs, huly://guide/onboarding, huly://guide/features; prompt "setup" writes a project's Huly rules.`;
99152
99172
  if (dynamic) instructions = instructions.replace("- Missing tools may be in a disabled toolset (HULY_TOOLSETS).", "- More tools (calendar, cards, drive, testing, recruiting, \u2026) load on demand: enable_toolsets.");
99153
- const server = new McpServer({ name: "huly-mcp-selfhost", version: "3.5.1" }, { instructions });
99173
+ const server = new McpServer({ name: "huly-mcp-selfhost", version: "3.6.0" }, { instructions });
99154
99174
  const asker = elicitationAsker(server);
99155
99175
  const registered = /* @__PURE__ */ new Map();
99156
99176
  for (const tool of TOOLS) {
@@ -99223,6 +99243,15 @@ var ARG_ALIASES = {
99223
99243
  component: "componentLabel",
99224
99244
  milestone: "milestoneLabel"
99225
99245
  };
99246
+ var TOOL_ARG_ALIASES = {
99247
+ get_milestone: { milestoneLabel: "label", milestone: "label" },
99248
+ update_milestone: { milestoneLabel: "label", milestone: "label" },
99249
+ update_component: { componentLabel: "label", component: "label" }
99250
+ };
99251
+ function aliasTarget(tool, name2, known) {
99252
+ const target = TOOL_ARG_ALIASES[tool]?.[name2] ?? ARG_ALIASES[name2];
99253
+ return target != null && known.has(target) ? target : void 0;
99254
+ }
99226
99255
  function normalizeArguments(tool, args, known) {
99227
99256
  const out = {};
99228
99257
  const unknown2 = [];
@@ -99231,8 +99260,8 @@ function normalizeArguments(tool, args, known) {
99231
99260
  out[k] = v;
99232
99261
  continue;
99233
99262
  }
99234
- const target = ARG_ALIASES[k];
99235
- if (target != null && known.has(target) && !(target in args)) {
99263
+ const target = aliasTarget(tool, k, known);
99264
+ if (target != null && !(target in args)) {
99236
99265
  out[target] = v;
99237
99266
  continue;
99238
99267
  }
@@ -104387,7 +104416,7 @@ function buildArgs(tool, parsed, io) {
104387
104416
  continue;
104388
104417
  }
104389
104418
  const name2 = camel(rawName);
104390
- const target = byName.has(name2) ? name2 : ARG_ALIASES[name2] != null && byName.has(ARG_ALIASES[name2]) ? ARG_ALIASES[name2] : void 0;
104419
+ const target = byName.has(name2) ? name2 : aliasTarget(tool.name, name2, new Set(byName.keys()));
104391
104420
  if (target == null) {
104392
104421
  const flags = params.filter((p) => p.name !== "workspace").map((p) => `--${kebab(p.name)}`);
104393
104422
  const hint = closest(`--${rawName}`, flags);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "huly-mcp-selfhost",
3
- "version": "3.5.1",
3
+ "version": "3.6.0",
4
4
  "description": "MCP server for Huly (self-hosted and Huly Cloud): 227 tools for Claude and other AI agents — issues, epics, documents, chat, planner, test management — with toolsets for low token use, an agent workflow and a Claude Code plugin.",
5
5
  "keywords": [
6
6
  "huly",
@@ -47,7 +47,8 @@
47
47
  "start": "node dist/index.js",
48
48
  "setup": "node scripts/setup.js",
49
49
  "prepublishOnly": "npm run build",
50
- "test": "node --import tsx --test test/*.test.ts"
50
+ "test": "node --import tsx --test test/*.test.ts",
51
+ "mcpb": "node scripts/mcpb.js"
51
52
  },
52
53
  "dependencies": {},
53
54
  "devDependencies": {