@gethmy/mcp 2.9.5 → 2.9.7
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/README.md +1 -1
- package/dist/cli.js +86 -5
- package/dist/index.js +50 -0
- package/dist/lib/api-client.js +10 -0
- package/package.json +2 -1
- package/src/api-client.ts +33 -0
- package/src/server.ts +59 -0
- package/src/tui/setup.ts +90 -8
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ Enables AI coding agents (Claude Code, OpenAI Codex, Cursor) to interact with yo
|
|
|
5
5
|
|
|
6
6
|
## Features
|
|
7
7
|
|
|
8
|
-
- **
|
|
8
|
+
- **69 MCP Tools** for full board control, knowledge graph, and workflow plans
|
|
9
9
|
- **5 Global Skills** — `/hmy`, `/hmy-plan`, `/hmy-cleanup`, `/hmy-standup`, `/hmy-memory-prune`, served from the DB-backed [skill hub](../../docs/skills.md) with auto-update and admin-managed versioning
|
|
10
10
|
- **Knowledge Graph Memory** — Phase 1 surface: hybrid retrieval (vector + lexical + RRF), session-scoped working memory, activity feed. See [docs/memory.md](../../docs/memory.md)
|
|
11
11
|
- **GSD Workflow Plans** - plan/execute/verify/done lifecycle with auto card creation
|
package/dist/cli.js
CHANGED
|
@@ -1815,6 +1815,16 @@ class HarmonyApiClient {
|
|
|
1815
1815
|
async getCardByShortId(projectId, shortId) {
|
|
1816
1816
|
return this.request("GET", `/projects/${projectId}/cards/${shortId}`);
|
|
1817
1817
|
}
|
|
1818
|
+
async bulkGetCards(projectId, shortIds) {
|
|
1819
|
+
return this.request("POST", `/projects/${projectId}/cards/bulk-get`, {
|
|
1820
|
+
shortIds
|
|
1821
|
+
});
|
|
1822
|
+
}
|
|
1823
|
+
async bulkArchiveCards(projectId, shortIds) {
|
|
1824
|
+
return this.request("POST", `/projects/${projectId}/cards/bulk-archive`, {
|
|
1825
|
+
shortIds
|
|
1826
|
+
});
|
|
1827
|
+
}
|
|
1818
1828
|
async searchCards(query, options) {
|
|
1819
1829
|
const params = new URLSearchParams({ q: query });
|
|
1820
1830
|
if (options?.projectId) {
|
|
@@ -3680,6 +3690,34 @@ var TOOLS = {
|
|
|
3680
3690
|
required: ["shortId"]
|
|
3681
3691
|
}
|
|
3682
3692
|
},
|
|
3693
|
+
harmony_bulk_get_cards: {
|
|
3694
|
+
description: "Fetch multiple cards by short id in one call. Returns compact summaries " + "(id, shortId, title, column, priority, assignee, labels, archived) plus " + "any short ids not found. Requires project context. Prefer this over " + "repeated harmony_get_card_by_short_id when referencing multiple cards.",
|
|
3695
|
+
inputSchema: {
|
|
3696
|
+
type: "object",
|
|
3697
|
+
properties: {
|
|
3698
|
+
shortIds: {
|
|
3699
|
+
type: "array",
|
|
3700
|
+
items: { type: "number" },
|
|
3701
|
+
description: "Card short ids, e.g. [400, 401, 402]. Max 100 per call."
|
|
3702
|
+
}
|
|
3703
|
+
},
|
|
3704
|
+
required: ["shortIds"]
|
|
3705
|
+
}
|
|
3706
|
+
},
|
|
3707
|
+
harmony_bulk_archive_cards: {
|
|
3708
|
+
description: "Archive (soft-delete) multiple cards by short id in one call. Best-effort: " + "returns { succeeded: number[], failed: [{shortId, reason}] }. Archived " + "cards can be restored with unarchive. Requires project context.",
|
|
3709
|
+
inputSchema: {
|
|
3710
|
+
type: "object",
|
|
3711
|
+
properties: {
|
|
3712
|
+
shortIds: {
|
|
3713
|
+
type: "array",
|
|
3714
|
+
items: { type: "number" },
|
|
3715
|
+
description: "Card short ids to archive, e.g. [400, 401]. Max 100 per call."
|
|
3716
|
+
}
|
|
3717
|
+
},
|
|
3718
|
+
required: ["shortIds"]
|
|
3719
|
+
}
|
|
3720
|
+
},
|
|
3683
3721
|
harmony_create_column: {
|
|
3684
3722
|
description: "Create a new column in a project",
|
|
3685
3723
|
inputSchema: {
|
|
@@ -5169,6 +5207,18 @@ async function handleToolCall(name, args, deps) {
|
|
|
5169
5207
|
const result = await client3.getCardByShortId(projectId, shortId);
|
|
5170
5208
|
return { success: true, ...result };
|
|
5171
5209
|
}
|
|
5210
|
+
case "harmony_bulk_get_cards": {
|
|
5211
|
+
const shortIds = z.array(z.number().int().positive()).min(1).max(100).parse(args.shortIds);
|
|
5212
|
+
const projectId = getProjectId();
|
|
5213
|
+
const result = await client3.bulkGetCards(projectId, shortIds);
|
|
5214
|
+
return { success: true, ...result };
|
|
5215
|
+
}
|
|
5216
|
+
case "harmony_bulk_archive_cards": {
|
|
5217
|
+
const shortIds = z.array(z.number().int().positive()).min(1).max(100).parse(args.shortIds);
|
|
5218
|
+
const projectId = getProjectId();
|
|
5219
|
+
const result = await client3.bulkArchiveCards(projectId, shortIds);
|
|
5220
|
+
return { success: true, ...result };
|
|
5221
|
+
}
|
|
5172
5222
|
case "harmony_create_column": {
|
|
5173
5223
|
const name2 = z.string().min(1).max(100).parse(args.name);
|
|
5174
5224
|
const projectId = args.projectId || getProjectId();
|
|
@@ -7473,12 +7523,23 @@ async function resolveProjectSlug(apiKey, slug) {
|
|
|
7473
7523
|
}
|
|
7474
7524
|
});
|
|
7475
7525
|
if (response.status === 404)
|
|
7476
|
-
return
|
|
7526
|
+
return { status: "not_found" };
|
|
7527
|
+
if (response.status === 409) {
|
|
7528
|
+
const data2 = await response.json().catch(() => ({}));
|
|
7529
|
+
return {
|
|
7530
|
+
status: "ambiguous",
|
|
7531
|
+
candidates: Array.isArray(data2.candidates) ? data2.candidates : []
|
|
7532
|
+
};
|
|
7533
|
+
}
|
|
7477
7534
|
if (!response.ok) {
|
|
7478
7535
|
throw new Error(`Failed to resolve project slug: ${response.status}`);
|
|
7479
7536
|
}
|
|
7480
7537
|
const data = await response.json();
|
|
7481
|
-
return {
|
|
7538
|
+
return {
|
|
7539
|
+
status: "found",
|
|
7540
|
+
workspaceId: data.workspaceId,
|
|
7541
|
+
projectId: data.projectId
|
|
7542
|
+
};
|
|
7482
7543
|
}
|
|
7483
7544
|
async function getAgentFiles(agentId, cwd, installMode = "global") {
|
|
7484
7545
|
const home = homedir6();
|
|
@@ -8002,14 +8063,23 @@ ${colors.dim(url)}`);
|
|
|
8002
8063
|
let selectedProjectId = selectedProjectIdFromSignup || options.projectId;
|
|
8003
8064
|
let selectedWorkspaceName = selectedWorkspaceNameFromSignup;
|
|
8004
8065
|
let selectedProjectName = selectedProjectNameFromSignup;
|
|
8066
|
+
let ambiguousCandidates;
|
|
8005
8067
|
if (options.projectSlug && apiKey && (!selectedWorkspaceId || !selectedProjectId)) {
|
|
8006
8068
|
spinner3.start(`Resolving project slug "${options.projectSlug}"...`);
|
|
8007
8069
|
try {
|
|
8008
8070
|
const resolved = await resolveProjectSlug(apiKey, options.projectSlug);
|
|
8009
|
-
if (resolved) {
|
|
8071
|
+
if (resolved.status === "found") {
|
|
8010
8072
|
selectedWorkspaceId = selectedWorkspaceId || resolved.workspaceId;
|
|
8011
8073
|
selectedProjectId = selectedProjectId || resolved.projectId;
|
|
8012
8074
|
spinner3.stop(colors.success(`Resolved "${options.projectSlug}"`));
|
|
8075
|
+
} else if (resolved.status === "ambiguous") {
|
|
8076
|
+
ambiguousCandidates = resolved.candidates;
|
|
8077
|
+
spinner3.stop(colors.warning(`Slug "${options.projectSlug}" is ambiguous — it exists in multiple workspaces`));
|
|
8078
|
+
const list = resolved.candidates.map((c) => ` • ${c.workspaceName ?? c.workspaceId}`).join(`
|
|
8079
|
+
`);
|
|
8080
|
+
p3.log.warning(`"${options.projectSlug}" matches projects in multiple workspaces:
|
|
8081
|
+
${list}
|
|
8082
|
+
Specify the workspace with --workspace <id>, or select one below.`);
|
|
8013
8083
|
} else {
|
|
8014
8084
|
spinner3.stop(colors.warning(`No project found for slug "${options.projectSlug}"`));
|
|
8015
8085
|
}
|
|
@@ -8033,12 +8103,15 @@ ${colors.dim(url)}`);
|
|
|
8033
8103
|
}
|
|
8034
8104
|
if (needsContext && workspaces.length > 0) {
|
|
8035
8105
|
if (!selectedWorkspaceId) {
|
|
8036
|
-
const
|
|
8106
|
+
const candidateIds = new Set((ambiguousCandidates ?? []).map((c) => c.workspaceId));
|
|
8107
|
+
const pickable = candidateIds.size > 0 ? workspaces.filter((ws) => candidateIds.has(ws.id)) : workspaces;
|
|
8108
|
+
const workspaceChoices = pickable.length > 0 ? pickable : workspaces;
|
|
8109
|
+
const workspaceOptions = workspaceChoices.map((ws) => ({
|
|
8037
8110
|
value: ws.id,
|
|
8038
8111
|
label: ws.name
|
|
8039
8112
|
}));
|
|
8040
8113
|
const workspaceSelection = await p3.select({
|
|
8041
|
-
message: "Select workspace",
|
|
8114
|
+
message: candidateIds.size > 0 ? `Select workspace for "${options.projectSlug}"` : "Select workspace",
|
|
8042
8115
|
options: workspaceOptions
|
|
8043
8116
|
});
|
|
8044
8117
|
if (p3.isCancel(workspaceSelection)) {
|
|
@@ -8048,6 +8121,12 @@ ${colors.dim(url)}`);
|
|
|
8048
8121
|
selectedWorkspaceId = workspaceSelection;
|
|
8049
8122
|
}
|
|
8050
8123
|
selectedWorkspaceName = workspaces.find((w) => w.id === selectedWorkspaceId)?.name;
|
|
8124
|
+
if (!selectedProjectId) {
|
|
8125
|
+
const chosenCandidate = ambiguousCandidates?.find((c) => c.workspaceId === selectedWorkspaceId);
|
|
8126
|
+
if (chosenCandidate) {
|
|
8127
|
+
selectedProjectId = chosenCandidate.projectId;
|
|
8128
|
+
}
|
|
8129
|
+
}
|
|
8051
8130
|
spinner3.start("Fetching projects...");
|
|
8052
8131
|
let projects = [];
|
|
8053
8132
|
try {
|
|
@@ -8073,6 +8152,8 @@ ${colors.dim(url)}`);
|
|
|
8073
8152
|
}
|
|
8074
8153
|
selectedProjectId = projectSelection;
|
|
8075
8154
|
selectedProjectName = projects.find((p4) => p4.id === selectedProjectId)?.name;
|
|
8155
|
+
} else if (selectedProjectId && !selectedProjectName) {
|
|
8156
|
+
selectedProjectName = projects.find((p4) => p4.id === selectedProjectId)?.name;
|
|
8076
8157
|
}
|
|
8077
8158
|
}
|
|
8078
8159
|
}
|
package/dist/index.js
CHANGED
|
@@ -1810,6 +1810,16 @@ class HarmonyApiClient {
|
|
|
1810
1810
|
async getCardByShortId(projectId, shortId) {
|
|
1811
1811
|
return this.request("GET", `/projects/${projectId}/cards/${shortId}`);
|
|
1812
1812
|
}
|
|
1813
|
+
async bulkGetCards(projectId, shortIds) {
|
|
1814
|
+
return this.request("POST", `/projects/${projectId}/cards/bulk-get`, {
|
|
1815
|
+
shortIds
|
|
1816
|
+
});
|
|
1817
|
+
}
|
|
1818
|
+
async bulkArchiveCards(projectId, shortIds) {
|
|
1819
|
+
return this.request("POST", `/projects/${projectId}/cards/bulk-archive`, {
|
|
1820
|
+
shortIds
|
|
1821
|
+
});
|
|
1822
|
+
}
|
|
1813
1823
|
async searchCards(query, options) {
|
|
1814
1824
|
const params = new URLSearchParams({ q: query });
|
|
1815
1825
|
if (options?.projectId) {
|
|
@@ -3675,6 +3685,34 @@ var TOOLS = {
|
|
|
3675
3685
|
required: ["shortId"]
|
|
3676
3686
|
}
|
|
3677
3687
|
},
|
|
3688
|
+
harmony_bulk_get_cards: {
|
|
3689
|
+
description: "Fetch multiple cards by short id in one call. Returns compact summaries " + "(id, shortId, title, column, priority, assignee, labels, archived) plus " + "any short ids not found. Requires project context. Prefer this over " + "repeated harmony_get_card_by_short_id when referencing multiple cards.",
|
|
3690
|
+
inputSchema: {
|
|
3691
|
+
type: "object",
|
|
3692
|
+
properties: {
|
|
3693
|
+
shortIds: {
|
|
3694
|
+
type: "array",
|
|
3695
|
+
items: { type: "number" },
|
|
3696
|
+
description: "Card short ids, e.g. [400, 401, 402]. Max 100 per call."
|
|
3697
|
+
}
|
|
3698
|
+
},
|
|
3699
|
+
required: ["shortIds"]
|
|
3700
|
+
}
|
|
3701
|
+
},
|
|
3702
|
+
harmony_bulk_archive_cards: {
|
|
3703
|
+
description: "Archive (soft-delete) multiple cards by short id in one call. Best-effort: " + "returns { succeeded: number[], failed: [{shortId, reason}] }. Archived " + "cards can be restored with unarchive. Requires project context.",
|
|
3704
|
+
inputSchema: {
|
|
3705
|
+
type: "object",
|
|
3706
|
+
properties: {
|
|
3707
|
+
shortIds: {
|
|
3708
|
+
type: "array",
|
|
3709
|
+
items: { type: "number" },
|
|
3710
|
+
description: "Card short ids to archive, e.g. [400, 401]. Max 100 per call."
|
|
3711
|
+
}
|
|
3712
|
+
},
|
|
3713
|
+
required: ["shortIds"]
|
|
3714
|
+
}
|
|
3715
|
+
},
|
|
3678
3716
|
harmony_create_column: {
|
|
3679
3717
|
description: "Create a new column in a project",
|
|
3680
3718
|
inputSchema: {
|
|
@@ -5164,6 +5202,18 @@ async function handleToolCall(name, args, deps) {
|
|
|
5164
5202
|
const result = await client3.getCardByShortId(projectId, shortId);
|
|
5165
5203
|
return { success: true, ...result };
|
|
5166
5204
|
}
|
|
5205
|
+
case "harmony_bulk_get_cards": {
|
|
5206
|
+
const shortIds = z.array(z.number().int().positive()).min(1).max(100).parse(args.shortIds);
|
|
5207
|
+
const projectId = getProjectId();
|
|
5208
|
+
const result = await client3.bulkGetCards(projectId, shortIds);
|
|
5209
|
+
return { success: true, ...result };
|
|
5210
|
+
}
|
|
5211
|
+
case "harmony_bulk_archive_cards": {
|
|
5212
|
+
const shortIds = z.array(z.number().int().positive()).min(1).max(100).parse(args.shortIds);
|
|
5213
|
+
const projectId = getProjectId();
|
|
5214
|
+
const result = await client3.bulkArchiveCards(projectId, shortIds);
|
|
5215
|
+
return { success: true, ...result };
|
|
5216
|
+
}
|
|
5167
5217
|
case "harmony_create_column": {
|
|
5168
5218
|
const name2 = z.string().min(1).max(100).parse(args.name);
|
|
5169
5219
|
const projectId = args.projectId || getProjectId();
|
package/dist/lib/api-client.js
CHANGED
|
@@ -1263,6 +1263,16 @@ class HarmonyApiClient {
|
|
|
1263
1263
|
async getCardByShortId(projectId, shortId) {
|
|
1264
1264
|
return this.request("GET", `/projects/${projectId}/cards/${shortId}`);
|
|
1265
1265
|
}
|
|
1266
|
+
async bulkGetCards(projectId, shortIds) {
|
|
1267
|
+
return this.request("POST", `/projects/${projectId}/cards/bulk-get`, {
|
|
1268
|
+
shortIds
|
|
1269
|
+
});
|
|
1270
|
+
}
|
|
1271
|
+
async bulkArchiveCards(projectId, shortIds) {
|
|
1272
|
+
return this.request("POST", `/projects/${projectId}/cards/bulk-archive`, {
|
|
1273
|
+
shortIds
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1266
1276
|
async searchCards(query, options) {
|
|
1267
1277
|
const params = new URLSearchParams({ q: query });
|
|
1268
1278
|
if (options?.projectId) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gethmy/mcp",
|
|
3
|
-
"version": "2.9.
|
|
3
|
+
"version": "2.9.7",
|
|
4
4
|
"description": "MCP server for Harmony Kanban board - enables AI coding agents to manage your boards",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -75,6 +75,7 @@
|
|
|
75
75
|
},
|
|
76
76
|
"devDependencies": {
|
|
77
77
|
"@harmony/memory": "workspace:*",
|
|
78
|
+
"@types/bun": "^1.3.13",
|
|
78
79
|
"@types/node": "^25.5.0",
|
|
79
80
|
"typescript": "^6.0.1"
|
|
80
81
|
}
|
package/src/api-client.ts
CHANGED
|
@@ -570,6 +570,39 @@ export class HarmonyApiClient {
|
|
|
570
570
|
return this.request("GET", `/projects/${projectId}/cards/${shortId}`);
|
|
571
571
|
}
|
|
572
572
|
|
|
573
|
+
async bulkGetCards(
|
|
574
|
+
projectId: string,
|
|
575
|
+
shortIds: number[],
|
|
576
|
+
): Promise<{
|
|
577
|
+
cards: Array<{
|
|
578
|
+
id: string;
|
|
579
|
+
shortId: number;
|
|
580
|
+
title: string;
|
|
581
|
+
column: string | null;
|
|
582
|
+
priority: string | null;
|
|
583
|
+
assignee: string | null;
|
|
584
|
+
labels: string[];
|
|
585
|
+
archived: boolean;
|
|
586
|
+
}>;
|
|
587
|
+
notFound: number[];
|
|
588
|
+
}> {
|
|
589
|
+
return this.request("POST", `/projects/${projectId}/cards/bulk-get`, {
|
|
590
|
+
shortIds,
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
async bulkArchiveCards(
|
|
595
|
+
projectId: string,
|
|
596
|
+
shortIds: number[],
|
|
597
|
+
): Promise<{
|
|
598
|
+
succeeded: number[];
|
|
599
|
+
failed: Array<{ shortId: number; reason: string }>;
|
|
600
|
+
}> {
|
|
601
|
+
return this.request("POST", `/projects/${projectId}/cards/bulk-archive`, {
|
|
602
|
+
shortIds,
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
|
|
573
606
|
async searchCards(
|
|
574
607
|
query: string,
|
|
575
608
|
options?: { projectId?: string },
|
package/src/server.ts
CHANGED
|
@@ -469,6 +469,43 @@ export const TOOLS = {
|
|
|
469
469
|
required: ["shortId"],
|
|
470
470
|
},
|
|
471
471
|
},
|
|
472
|
+
harmony_bulk_get_cards: {
|
|
473
|
+
description:
|
|
474
|
+
"Fetch multiple cards by short id in one call. Returns compact summaries " +
|
|
475
|
+
"(id, shortId, title, column, priority, assignee, labels, archived) plus " +
|
|
476
|
+
"any short ids not found. Requires project context. Prefer this over " +
|
|
477
|
+
"repeated harmony_get_card_by_short_id when referencing multiple cards.",
|
|
478
|
+
inputSchema: {
|
|
479
|
+
type: "object",
|
|
480
|
+
properties: {
|
|
481
|
+
shortIds: {
|
|
482
|
+
type: "array",
|
|
483
|
+
items: { type: "number" },
|
|
484
|
+
description:
|
|
485
|
+
"Card short ids, e.g. [400, 401, 402]. Max 100 per call.",
|
|
486
|
+
},
|
|
487
|
+
},
|
|
488
|
+
required: ["shortIds"],
|
|
489
|
+
},
|
|
490
|
+
},
|
|
491
|
+
harmony_bulk_archive_cards: {
|
|
492
|
+
description:
|
|
493
|
+
"Archive (soft-delete) multiple cards by short id in one call. Best-effort: " +
|
|
494
|
+
"returns { succeeded: number[], failed: [{shortId, reason}] }. Archived " +
|
|
495
|
+
"cards can be restored with unarchive. Requires project context.",
|
|
496
|
+
inputSchema: {
|
|
497
|
+
type: "object",
|
|
498
|
+
properties: {
|
|
499
|
+
shortIds: {
|
|
500
|
+
type: "array",
|
|
501
|
+
items: { type: "number" },
|
|
502
|
+
description:
|
|
503
|
+
"Card short ids to archive, e.g. [400, 401]. Max 100 per call.",
|
|
504
|
+
},
|
|
505
|
+
},
|
|
506
|
+
required: ["shortIds"],
|
|
507
|
+
},
|
|
508
|
+
},
|
|
472
509
|
|
|
473
510
|
// Column operations
|
|
474
511
|
harmony_create_column: {
|
|
@@ -2241,6 +2278,28 @@ async function handleToolCall(
|
|
|
2241
2278
|
return { success: true, ...result };
|
|
2242
2279
|
}
|
|
2243
2280
|
|
|
2281
|
+
case "harmony_bulk_get_cards": {
|
|
2282
|
+
const shortIds = z
|
|
2283
|
+
.array(z.number().int().positive())
|
|
2284
|
+
.min(1)
|
|
2285
|
+
.max(100)
|
|
2286
|
+
.parse(args.shortIds);
|
|
2287
|
+
const projectId = getProjectId();
|
|
2288
|
+
const result = await client.bulkGetCards(projectId, shortIds);
|
|
2289
|
+
return { success: true, ...result };
|
|
2290
|
+
}
|
|
2291
|
+
|
|
2292
|
+
case "harmony_bulk_archive_cards": {
|
|
2293
|
+
const shortIds = z
|
|
2294
|
+
.array(z.number().int().positive())
|
|
2295
|
+
.min(1)
|
|
2296
|
+
.max(100)
|
|
2297
|
+
.parse(args.shortIds);
|
|
2298
|
+
const projectId = getProjectId();
|
|
2299
|
+
const result = await client.bulkArchiveCards(projectId, shortIds);
|
|
2300
|
+
return { success: true, ...result };
|
|
2301
|
+
}
|
|
2302
|
+
|
|
2244
2303
|
// Column operations
|
|
2245
2304
|
case "harmony_create_column": {
|
|
2246
2305
|
const name = z.string().min(1).max(100).parse(args.name);
|
package/src/tui/setup.ts
CHANGED
|
@@ -331,14 +331,31 @@ async function fetchProjects(
|
|
|
331
331
|
return data.projects || [];
|
|
332
332
|
}
|
|
333
333
|
|
|
334
|
+
export interface SlugCandidate {
|
|
335
|
+
workspaceId: string;
|
|
336
|
+
workspaceName: string | null;
|
|
337
|
+
projectId: string;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Outcome of resolving a project slug. `ambiguous` means the same slug exists
|
|
342
|
+
* in more than one of the caller's workspaces (HTTP 409) — common for bare
|
|
343
|
+
* web-onboarded slugs like "my-first-board" when a user belongs to several
|
|
344
|
+
* teams. The caller must disambiguate rather than treat it as not-found.
|
|
345
|
+
*/
|
|
346
|
+
export type ResolveSlugResult =
|
|
347
|
+
| { status: "found"; workspaceId: string; projectId: string }
|
|
348
|
+
| { status: "not_found" }
|
|
349
|
+
| { status: "ambiguous"; candidates: SlugCandidate[] };
|
|
350
|
+
|
|
334
351
|
/**
|
|
335
352
|
* Resolve a project slug to {workspaceId, projectId}. Used by
|
|
336
353
|
* `npx @gethmy/mcp setup <slug>` so users don't have to copy raw UUIDs.
|
|
337
354
|
*/
|
|
338
|
-
async function resolveProjectSlug(
|
|
355
|
+
export async function resolveProjectSlug(
|
|
339
356
|
apiKey: string,
|
|
340
357
|
slug: string,
|
|
341
|
-
): Promise<
|
|
358
|
+
): Promise<ResolveSlugResult> {
|
|
342
359
|
const response = await fetch(
|
|
343
360
|
`${API_URL}/v1/projects/resolve/${encodeURIComponent(slug)}`,
|
|
344
361
|
{
|
|
@@ -350,13 +367,26 @@ async function resolveProjectSlug(
|
|
|
350
367
|
},
|
|
351
368
|
);
|
|
352
369
|
|
|
353
|
-
if (response.status === 404) return
|
|
370
|
+
if (response.status === 404) return { status: "not_found" };
|
|
371
|
+
// 409 = slug matches projects in multiple workspaces. The body carries the
|
|
372
|
+
// candidate workspaces so we can tell the user which ones to pick from.
|
|
373
|
+
if (response.status === 409) {
|
|
374
|
+
const data = await response.json().catch(() => ({}));
|
|
375
|
+
return {
|
|
376
|
+
status: "ambiguous",
|
|
377
|
+
candidates: Array.isArray(data.candidates) ? data.candidates : [],
|
|
378
|
+
};
|
|
379
|
+
}
|
|
354
380
|
if (!response.ok) {
|
|
355
381
|
throw new Error(`Failed to resolve project slug: ${response.status}`);
|
|
356
382
|
}
|
|
357
383
|
|
|
358
384
|
const data = await response.json();
|
|
359
|
-
return {
|
|
385
|
+
return {
|
|
386
|
+
status: "found",
|
|
387
|
+
workspaceId: data.workspaceId,
|
|
388
|
+
projectId: data.projectId,
|
|
389
|
+
};
|
|
360
390
|
}
|
|
361
391
|
|
|
362
392
|
export interface FileToWrite {
|
|
@@ -1056,6 +1086,10 @@ export async function runSetup(options: SetupOptions = {}): Promise<void> {
|
|
|
1056
1086
|
let selectedWorkspaceName: string | undefined =
|
|
1057
1087
|
selectedWorkspaceNameFromSignup;
|
|
1058
1088
|
let selectedProjectName: string | undefined = selectedProjectNameFromSignup;
|
|
1089
|
+
// When a slug is ambiguous, the candidate workspaces (each already carrying
|
|
1090
|
+
// the matching projectId) narrow the interactive picker below to just those
|
|
1091
|
+
// workspaces instead of every workspace the user belongs to.
|
|
1092
|
+
let ambiguousCandidates: SlugCandidate[] | undefined;
|
|
1059
1093
|
|
|
1060
1094
|
// Resolve project slug shorthand (e.g. `npx @gethmy/mcp setup harmony-6590761b`).
|
|
1061
1095
|
// Slug wins over --workspace/--project flags only when those aren't already set
|
|
@@ -1068,10 +1102,28 @@ export async function runSetup(options: SetupOptions = {}): Promise<void> {
|
|
|
1068
1102
|
spinner.start(`Resolving project slug "${options.projectSlug}"...`);
|
|
1069
1103
|
try {
|
|
1070
1104
|
const resolved = await resolveProjectSlug(apiKey, options.projectSlug);
|
|
1071
|
-
if (resolved) {
|
|
1105
|
+
if (resolved.status === "found") {
|
|
1072
1106
|
selectedWorkspaceId = selectedWorkspaceId || resolved.workspaceId;
|
|
1073
1107
|
selectedProjectId = selectedProjectId || resolved.projectId;
|
|
1074
1108
|
spinner.stop(colors.success(`Resolved "${options.projectSlug}"`));
|
|
1109
|
+
} else if (resolved.status === "ambiguous") {
|
|
1110
|
+
// Same slug in multiple workspaces — don't silently pick one. Surface
|
|
1111
|
+
// the candidates so the user knows to specify a workspace; interactive
|
|
1112
|
+
// context selection below still lets them recover in a TTY, narrowed to
|
|
1113
|
+
// just these workspaces.
|
|
1114
|
+
ambiguousCandidates = resolved.candidates;
|
|
1115
|
+
spinner.stop(
|
|
1116
|
+
colors.warning(
|
|
1117
|
+
`Slug "${options.projectSlug}" is ambiguous — it exists in multiple workspaces`,
|
|
1118
|
+
),
|
|
1119
|
+
);
|
|
1120
|
+
const list = resolved.candidates
|
|
1121
|
+
.map((c) => ` • ${c.workspaceName ?? c.workspaceId}`)
|
|
1122
|
+
.join("\n");
|
|
1123
|
+
p.log.warning(
|
|
1124
|
+
`"${options.projectSlug}" matches projects in multiple workspaces:\n${list}\n` +
|
|
1125
|
+
"Specify the workspace with --workspace <id>, or select one below.",
|
|
1126
|
+
);
|
|
1075
1127
|
} else {
|
|
1076
1128
|
spinner.stop(
|
|
1077
1129
|
colors.warning(`No project found for slug "${options.projectSlug}"`),
|
|
@@ -1108,15 +1160,28 @@ export async function runSetup(options: SetupOptions = {}): Promise<void> {
|
|
|
1108
1160
|
}
|
|
1109
1161
|
|
|
1110
1162
|
if (needsContext && workspaces.length > 0) {
|
|
1111
|
-
// Select workspace
|
|
1163
|
+
// Select workspace. When the slug was ambiguous, restrict the choices to
|
|
1164
|
+
// the candidate workspaces so the user picks among the ones that actually
|
|
1165
|
+
// contain the slug (falling back to all workspaces if none still match).
|
|
1112
1166
|
if (!selectedWorkspaceId) {
|
|
1113
|
-
const
|
|
1167
|
+
const candidateIds = new Set(
|
|
1168
|
+
(ambiguousCandidates ?? []).map((c) => c.workspaceId),
|
|
1169
|
+
);
|
|
1170
|
+
const pickable =
|
|
1171
|
+
candidateIds.size > 0
|
|
1172
|
+
? workspaces.filter((ws) => candidateIds.has(ws.id))
|
|
1173
|
+
: workspaces;
|
|
1174
|
+
const workspaceChoices = pickable.length > 0 ? pickable : workspaces;
|
|
1175
|
+
const workspaceOptions = workspaceChoices.map((ws) => ({
|
|
1114
1176
|
value: ws.id,
|
|
1115
1177
|
label: ws.name,
|
|
1116
1178
|
}));
|
|
1117
1179
|
|
|
1118
1180
|
const workspaceSelection = await p.select({
|
|
1119
|
-
message:
|
|
1181
|
+
message:
|
|
1182
|
+
candidateIds.size > 0
|
|
1183
|
+
? `Select workspace for "${options.projectSlug}"`
|
|
1184
|
+
: "Select workspace",
|
|
1120
1185
|
options: workspaceOptions,
|
|
1121
1186
|
});
|
|
1122
1187
|
|
|
@@ -1132,6 +1197,17 @@ export async function runSetup(options: SetupOptions = {}): Promise<void> {
|
|
|
1132
1197
|
(w) => w.id === selectedWorkspaceId,
|
|
1133
1198
|
)?.name;
|
|
1134
1199
|
|
|
1200
|
+
// For an ambiguous slug, the chosen workspace already pins the project —
|
|
1201
|
+
// each candidate carries its projectId — so skip the project picker.
|
|
1202
|
+
if (!selectedProjectId) {
|
|
1203
|
+
const chosenCandidate = ambiguousCandidates?.find(
|
|
1204
|
+
(c) => c.workspaceId === selectedWorkspaceId,
|
|
1205
|
+
);
|
|
1206
|
+
if (chosenCandidate) {
|
|
1207
|
+
selectedProjectId = chosenCandidate.projectId;
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1135
1211
|
// Fetch and select project
|
|
1136
1212
|
spinner.start("Fetching projects...");
|
|
1137
1213
|
let projects: Project[] = [];
|
|
@@ -1167,6 +1243,12 @@ export async function runSetup(options: SetupOptions = {}): Promise<void> {
|
|
|
1167
1243
|
selectedProjectName = projects.find(
|
|
1168
1244
|
(p) => p.id === selectedProjectId,
|
|
1169
1245
|
)?.name;
|
|
1246
|
+
} else if (selectedProjectId && !selectedProjectName) {
|
|
1247
|
+
// Project was pre-resolved (e.g. from an ambiguous-slug candidate) so
|
|
1248
|
+
// the picker was skipped — backfill the name for the summary display.
|
|
1249
|
+
selectedProjectName = projects.find(
|
|
1250
|
+
(p) => p.id === selectedProjectId,
|
|
1251
|
+
)?.name;
|
|
1170
1252
|
}
|
|
1171
1253
|
}
|
|
1172
1254
|
}
|