@gethmy/mcp 2.17.1 → 2.18.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/dist/cli.js +49 -3
- package/dist/index.js +49 -3
- package/dist/lib/api-client.js +4 -0
- package/package.json +1 -1
- package/src/api-client.ts +55 -0
- package/src/server.ts +107 -3
package/dist/cli.js
CHANGED
|
@@ -1869,6 +1869,10 @@ class HarmonyApiClient {
|
|
|
1869
1869
|
async getCardByShortId(projectId, shortId) {
|
|
1870
1870
|
return this.request("GET", `/projects/${projectId}/cards/${shortId}`);
|
|
1871
1871
|
}
|
|
1872
|
+
async resolveCardByShortId(shortId, preferredProjectId) {
|
|
1873
|
+
const qs = preferredProjectId ? `?preferred_project_id=${encodeURIComponent(preferredProjectId)}` : "";
|
|
1874
|
+
return this.request("GET", `/cards/resolve/${shortId}${qs}`);
|
|
1875
|
+
}
|
|
1872
1876
|
async bulkGetCards(projectId, shortIds) {
|
|
1873
1877
|
return this.request("POST", `/projects/${projectId}/cards/bulk-get`, {
|
|
1874
1878
|
shortIds
|
|
@@ -5763,9 +5767,51 @@ async function handleToolCall(name, args, deps) {
|
|
|
5763
5767
|
}
|
|
5764
5768
|
if (hasShortId) {
|
|
5765
5769
|
const shortId = z.number().int().positive().parse(args.shortId);
|
|
5766
|
-
const
|
|
5767
|
-
|
|
5768
|
-
|
|
5770
|
+
const explicitProjectId = args.projectId;
|
|
5771
|
+
if (explicitProjectId) {
|
|
5772
|
+
const result2 = await client3.getCardByShortId(explicitProjectId, shortId);
|
|
5773
|
+
return { success: true, ...result2 };
|
|
5774
|
+
}
|
|
5775
|
+
const activeProjectId = deps.getActiveProjectId();
|
|
5776
|
+
const resolved = await client3.resolveCardByShortId(shortId, activeProjectId);
|
|
5777
|
+
if (resolved.kind === "found") {
|
|
5778
|
+
const cardTitle = resolved.card?.title ?? `#${shortId}`;
|
|
5779
|
+
const where = resolved.project.workspaceName ? `project "${resolved.project.name ?? resolved.project.id}" (workspace "${resolved.project.workspaceName}")` : `project "${resolved.project.name ?? resolved.project.id}"`;
|
|
5780
|
+
const established = activeProjectId == null;
|
|
5781
|
+
if (established) {
|
|
5782
|
+
deps.setActiveProject(resolved.project.id);
|
|
5783
|
+
}
|
|
5784
|
+
return {
|
|
5785
|
+
success: true,
|
|
5786
|
+
card: resolved.card,
|
|
5787
|
+
resolvedProject: resolved.project,
|
|
5788
|
+
activeProjectId: resolved.project.id,
|
|
5789
|
+
note: established ? `Resolved #${shortId} → "${cardTitle}" in ${where}. No active project was set — set to this for follow-up references.` : `Resolved #${shortId} → "${cardTitle}" in ${where} (your active project).`
|
|
5790
|
+
};
|
|
5791
|
+
}
|
|
5792
|
+
if (resolved.kind === "not_in_preferred") {
|
|
5793
|
+
const list = resolved.candidates.map((c) => ` • "${c.title}" — project "${c.projectName ?? c.projectId}"${c.workspaceName ? ` / workspace "${c.workspaceName}"` : ""} (projectId: ${c.projectId})`).join(`
|
|
5794
|
+
`);
|
|
5795
|
+
throw new Error(`#${shortId} is not in your active project (projectId: ${resolved.preferredProjectId}). ` + `It exists in ${resolved.candidates.length} other project(s) you can access:
|
|
5796
|
+
${list}
|
|
5797
|
+
|
|
5798
|
+
` + `Switch with harmony_set_project_context, or pass an explicit projectId to fetch it directly.`);
|
|
5799
|
+
}
|
|
5800
|
+
if (resolved.kind === "ambiguous") {
|
|
5801
|
+
const list = resolved.candidates.map((c) => ` • "${c.title}" — project "${c.projectName ?? c.projectId}"${c.workspaceName ? ` / workspace "${c.workspaceName}"` : ""} (projectId: ${c.projectId})`).join(`
|
|
5802
|
+
`);
|
|
5803
|
+
return {
|
|
5804
|
+
success: true,
|
|
5805
|
+
needsDisambiguation: true,
|
|
5806
|
+
shortId,
|
|
5807
|
+
candidates: resolved.candidates,
|
|
5808
|
+
message: `#${shortId} exists in ${resolved.candidates.length} projects you can access:
|
|
5809
|
+
${list}
|
|
5810
|
+
|
|
5811
|
+
` + `Ask which one is meant, then re-fetch with an explicit projectId ` + `(or call harmony_set_project_context first).`
|
|
5812
|
+
};
|
|
5813
|
+
}
|
|
5814
|
+
throw new Error(resolved.searchedProjectCount === 0 ? `#${shortId} can't be resolved: no project is accessible to this connection. ` + `Check the workspace this connection is authorized for with harmony_list_workspaces.` : `Card #${shortId} was not found in any of the ${resolved.searchedProjectCount} ` + `project(s) across ${resolved.searchedWorkspaceCount} workspace(s) this connection can access. ` + `Use harmony_list_projects to see them, or pass an explicit projectId.`);
|
|
5769
5815
|
}
|
|
5770
5816
|
const cardId = z.string().uuid().parse(args.cardId);
|
|
5771
5817
|
const result = await client3.getCard(cardId);
|
package/dist/index.js
CHANGED
|
@@ -1864,6 +1864,10 @@ class HarmonyApiClient {
|
|
|
1864
1864
|
async getCardByShortId(projectId, shortId) {
|
|
1865
1865
|
return this.request("GET", `/projects/${projectId}/cards/${shortId}`);
|
|
1866
1866
|
}
|
|
1867
|
+
async resolveCardByShortId(shortId, preferredProjectId) {
|
|
1868
|
+
const qs = preferredProjectId ? `?preferred_project_id=${encodeURIComponent(preferredProjectId)}` : "";
|
|
1869
|
+
return this.request("GET", `/cards/resolve/${shortId}${qs}`);
|
|
1870
|
+
}
|
|
1867
1871
|
async bulkGetCards(projectId, shortIds) {
|
|
1868
1872
|
return this.request("POST", `/projects/${projectId}/cards/bulk-get`, {
|
|
1869
1873
|
shortIds
|
|
@@ -5758,9 +5762,51 @@ async function handleToolCall(name, args, deps) {
|
|
|
5758
5762
|
}
|
|
5759
5763
|
if (hasShortId) {
|
|
5760
5764
|
const shortId = z.number().int().positive().parse(args.shortId);
|
|
5761
|
-
const
|
|
5762
|
-
|
|
5763
|
-
|
|
5765
|
+
const explicitProjectId = args.projectId;
|
|
5766
|
+
if (explicitProjectId) {
|
|
5767
|
+
const result2 = await client3.getCardByShortId(explicitProjectId, shortId);
|
|
5768
|
+
return { success: true, ...result2 };
|
|
5769
|
+
}
|
|
5770
|
+
const activeProjectId = deps.getActiveProjectId();
|
|
5771
|
+
const resolved = await client3.resolveCardByShortId(shortId, activeProjectId);
|
|
5772
|
+
if (resolved.kind === "found") {
|
|
5773
|
+
const cardTitle = resolved.card?.title ?? `#${shortId}`;
|
|
5774
|
+
const where = resolved.project.workspaceName ? `project "${resolved.project.name ?? resolved.project.id}" (workspace "${resolved.project.workspaceName}")` : `project "${resolved.project.name ?? resolved.project.id}"`;
|
|
5775
|
+
const established = activeProjectId == null;
|
|
5776
|
+
if (established) {
|
|
5777
|
+
deps.setActiveProject(resolved.project.id);
|
|
5778
|
+
}
|
|
5779
|
+
return {
|
|
5780
|
+
success: true,
|
|
5781
|
+
card: resolved.card,
|
|
5782
|
+
resolvedProject: resolved.project,
|
|
5783
|
+
activeProjectId: resolved.project.id,
|
|
5784
|
+
note: established ? `Resolved #${shortId} → "${cardTitle}" in ${where}. No active project was set — set to this for follow-up references.` : `Resolved #${shortId} → "${cardTitle}" in ${where} (your active project).`
|
|
5785
|
+
};
|
|
5786
|
+
}
|
|
5787
|
+
if (resolved.kind === "not_in_preferred") {
|
|
5788
|
+
const list = resolved.candidates.map((c) => ` • "${c.title}" — project "${c.projectName ?? c.projectId}"${c.workspaceName ? ` / workspace "${c.workspaceName}"` : ""} (projectId: ${c.projectId})`).join(`
|
|
5789
|
+
`);
|
|
5790
|
+
throw new Error(`#${shortId} is not in your active project (projectId: ${resolved.preferredProjectId}). ` + `It exists in ${resolved.candidates.length} other project(s) you can access:
|
|
5791
|
+
${list}
|
|
5792
|
+
|
|
5793
|
+
` + `Switch with harmony_set_project_context, or pass an explicit projectId to fetch it directly.`);
|
|
5794
|
+
}
|
|
5795
|
+
if (resolved.kind === "ambiguous") {
|
|
5796
|
+
const list = resolved.candidates.map((c) => ` • "${c.title}" — project "${c.projectName ?? c.projectId}"${c.workspaceName ? ` / workspace "${c.workspaceName}"` : ""} (projectId: ${c.projectId})`).join(`
|
|
5797
|
+
`);
|
|
5798
|
+
return {
|
|
5799
|
+
success: true,
|
|
5800
|
+
needsDisambiguation: true,
|
|
5801
|
+
shortId,
|
|
5802
|
+
candidates: resolved.candidates,
|
|
5803
|
+
message: `#${shortId} exists in ${resolved.candidates.length} projects you can access:
|
|
5804
|
+
${list}
|
|
5805
|
+
|
|
5806
|
+
` + `Ask which one is meant, then re-fetch with an explicit projectId ` + `(or call harmony_set_project_context first).`
|
|
5807
|
+
};
|
|
5808
|
+
}
|
|
5809
|
+
throw new Error(resolved.searchedProjectCount === 0 ? `#${shortId} can't be resolved: no project is accessible to this connection. ` + `Check the workspace this connection is authorized for with harmony_list_workspaces.` : `Card #${shortId} was not found in any of the ${resolved.searchedProjectCount} ` + `project(s) across ${resolved.searchedWorkspaceCount} workspace(s) this connection can access. ` + `Use harmony_list_projects to see them, or pass an explicit projectId.`);
|
|
5764
5810
|
}
|
|
5765
5811
|
const cardId = z.string().uuid().parse(args.cardId);
|
|
5766
5812
|
const result = await client3.getCard(cardId);
|
package/dist/lib/api-client.js
CHANGED
|
@@ -1316,6 +1316,10 @@ class HarmonyApiClient {
|
|
|
1316
1316
|
async getCardByShortId(projectId, shortId) {
|
|
1317
1317
|
return this.request("GET", `/projects/${projectId}/cards/${shortId}`);
|
|
1318
1318
|
}
|
|
1319
|
+
async resolveCardByShortId(shortId, preferredProjectId) {
|
|
1320
|
+
const qs = preferredProjectId ? `?preferred_project_id=${encodeURIComponent(preferredProjectId)}` : "";
|
|
1321
|
+
return this.request("GET", `/cards/resolve/${shortId}${qs}`);
|
|
1322
|
+
}
|
|
1319
1323
|
async bulkGetCards(projectId, shortIds) {
|
|
1320
1324
|
return this.request("POST", `/projects/${projectId}/cards/bulk-get`, {
|
|
1321
1325
|
shortIds
|
package/package.json
CHANGED
package/src/api-client.ts
CHANGED
|
@@ -164,6 +164,46 @@ export interface CardExternalLinkRow {
|
|
|
164
164
|
created_at: string;
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
+
/** One candidate card returned by the cross-project short-id resolver (#709). */
|
|
168
|
+
export interface ResolveCardCandidate {
|
|
169
|
+
cardId: string;
|
|
170
|
+
title: string;
|
|
171
|
+
projectId: string;
|
|
172
|
+
projectName: string | null;
|
|
173
|
+
workspaceId: string;
|
|
174
|
+
workspaceName: string | null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Discriminated result of GET /cards/resolve/:shortId (#709). `found` carries
|
|
178
|
+
* the hydrated card + its project (for sticky-context + confirm-back);
|
|
179
|
+
* `ambiguous` carries the candidates so the caller asks which one; `not_found`
|
|
180
|
+
* carries the searched scope for a legible failure; `not_in_preferred` (#428)
|
|
181
|
+
* means a deliberately-set active project (the hard scope) doesn't hold the id,
|
|
182
|
+
* and carries the projects that DO so the caller can switch or pass an explicit
|
|
183
|
+
* projectId — the tool never silently hops to another project. */
|
|
184
|
+
export type ResolveCardApiResult =
|
|
185
|
+
| {
|
|
186
|
+
kind: "found";
|
|
187
|
+
card: unknown;
|
|
188
|
+
project: {
|
|
189
|
+
id: string;
|
|
190
|
+
name: string | null;
|
|
191
|
+
workspaceId: string;
|
|
192
|
+
workspaceName: string | null;
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
| { kind: "ambiguous"; candidates: ResolveCardCandidate[] }
|
|
196
|
+
| {
|
|
197
|
+
kind: "not_found";
|
|
198
|
+
searchedWorkspaceCount: number;
|
|
199
|
+
searchedProjectCount: number;
|
|
200
|
+
}
|
|
201
|
+
| {
|
|
202
|
+
kind: "not_in_preferred";
|
|
203
|
+
preferredProjectId: string;
|
|
204
|
+
candidates: ResolveCardCandidate[];
|
|
205
|
+
};
|
|
206
|
+
|
|
167
207
|
/** Result of the classify-card classifier (card #415). Any field may be null
|
|
168
208
|
* if the LLM didn't return a usable value. `model_override` is never touched. */
|
|
169
209
|
export interface CardClassificationResult {
|
|
@@ -694,6 +734,21 @@ export class HarmonyApiClient {
|
|
|
694
734
|
return this.request("GET", `/projects/${projectId}/cards/${shortId}`);
|
|
695
735
|
}
|
|
696
736
|
|
|
737
|
+
// #709: resolve a `#shortId` across every project the caller can reach when no
|
|
738
|
+
// explicit project is in play (the remote/OAuth MCP seeds a workspace but no
|
|
739
|
+
// project). `preferredProjectId` biases to the session's active/sticky project
|
|
740
|
+
// so a deliberately-set context wins outright instead of reading as ambiguous.
|
|
741
|
+
// Always resolves (HTTP 200) with a discriminated body — the caller decides.
|
|
742
|
+
async resolveCardByShortId(
|
|
743
|
+
shortId: number,
|
|
744
|
+
preferredProjectId?: string | null,
|
|
745
|
+
): Promise<ResolveCardApiResult> {
|
|
746
|
+
const qs = preferredProjectId
|
|
747
|
+
? `?preferred_project_id=${encodeURIComponent(preferredProjectId)}`
|
|
748
|
+
: "";
|
|
749
|
+
return this.request("GET", `/cards/resolve/${shortId}${qs}`);
|
|
750
|
+
}
|
|
751
|
+
|
|
697
752
|
async bulkGetCards(
|
|
698
753
|
projectId: string,
|
|
699
754
|
shortIds: number[],
|
package/src/server.ts
CHANGED
|
@@ -2872,9 +2872,113 @@ async function handleToolCall(
|
|
|
2872
2872
|
}
|
|
2873
2873
|
if (hasShortId) {
|
|
2874
2874
|
const shortId = z.number().int().positive().parse(args.shortId);
|
|
2875
|
-
const
|
|
2876
|
-
|
|
2877
|
-
|
|
2875
|
+
const explicitProjectId = args.projectId as string | undefined;
|
|
2876
|
+
|
|
2877
|
+
// Explicit projectId is the deterministic override: fetch from exactly
|
|
2878
|
+
// that project and error if it's not there — never look elsewhere.
|
|
2879
|
+
if (explicitProjectId) {
|
|
2880
|
+
const result = await client.getCardByShortId(
|
|
2881
|
+
explicitProjectId,
|
|
2882
|
+
shortId,
|
|
2883
|
+
);
|
|
2884
|
+
return { success: true, ...result };
|
|
2885
|
+
}
|
|
2886
|
+
|
|
2887
|
+
// No explicit project: resolve the `#shortId` (#709). The active/sticky
|
|
2888
|
+
// project is a HARD SCOPE (#428) — when one is set, the resolver only
|
|
2889
|
+
// matches within it (a miss comes back as `not_in_preferred`, handled
|
|
2890
|
+
// below) and this read never repoints it. Cross-project auto-resolution
|
|
2891
|
+
// + sticky happens ONLY when no active project is set — the remote/OAuth
|
|
2892
|
+
// MCP case this feature exists for, where the session seeds a workspace
|
|
2893
|
+
// but no project.
|
|
2894
|
+
const activeProjectId = deps.getActiveProjectId();
|
|
2895
|
+
const resolved = await client.resolveCardByShortId(
|
|
2896
|
+
shortId,
|
|
2897
|
+
activeProjectId,
|
|
2898
|
+
);
|
|
2899
|
+
|
|
2900
|
+
if (resolved.kind === "found") {
|
|
2901
|
+
const cardTitle =
|
|
2902
|
+
(resolved.card as { title?: string } | null)?.title ??
|
|
2903
|
+
`#${shortId}`;
|
|
2904
|
+
const where = resolved.project.workspaceName
|
|
2905
|
+
? `project "${resolved.project.name ?? resolved.project.id}" (workspace "${resolved.project.workspaceName}")`
|
|
2906
|
+
: `project "${resolved.project.name ?? resolved.project.id}"`;
|
|
2907
|
+
// Sticky ONLY when this ESTABLISHES a context (none was set). A
|
|
2908
|
+
// deliberately-set active project is a hard scope: `found` there means
|
|
2909
|
+
// the card was already in it, so there is nothing to change — and we
|
|
2910
|
+
// never let a read silently repoint a context the user chose (which,
|
|
2911
|
+
// on the local stdio MCP, persists to ~/.harmony-mcp/config.json
|
|
2912
|
+
// across sessions). Confirm the target back either way so a
|
|
2913
|
+
// wrong-context resolve is visible immediately.
|
|
2914
|
+
const established = activeProjectId == null;
|
|
2915
|
+
if (established) {
|
|
2916
|
+
deps.setActiveProject(resolved.project.id);
|
|
2917
|
+
}
|
|
2918
|
+
return {
|
|
2919
|
+
success: true,
|
|
2920
|
+
card: resolved.card,
|
|
2921
|
+
resolvedProject: resolved.project,
|
|
2922
|
+
activeProjectId: resolved.project.id,
|
|
2923
|
+
note: established
|
|
2924
|
+
? `Resolved #${shortId} → "${cardTitle}" in ${where}. No active project was set — set to this for follow-up references.`
|
|
2925
|
+
: `Resolved #${shortId} → "${cardTitle}" in ${where} (your active project).`,
|
|
2926
|
+
};
|
|
2927
|
+
}
|
|
2928
|
+
|
|
2929
|
+
if (resolved.kind === "not_in_preferred") {
|
|
2930
|
+
// Hard scope (#428): the active project is a constraint the user set,
|
|
2931
|
+
// so a `#shortId` that isn't in it is an error — NOT a silent hop to
|
|
2932
|
+
// whatever other project happens to carry that number, and NOT a
|
|
2933
|
+
// change to the active project. Name where it *does* live so the
|
|
2934
|
+
// caller can switch context or fetch it explicitly.
|
|
2935
|
+
const list = resolved.candidates
|
|
2936
|
+
.map(
|
|
2937
|
+
(c) =>
|
|
2938
|
+
` • "${c.title}" — project "${c.projectName ?? c.projectId}"${
|
|
2939
|
+
c.workspaceName ? ` / workspace "${c.workspaceName}"` : ""
|
|
2940
|
+
} (projectId: ${c.projectId})`,
|
|
2941
|
+
)
|
|
2942
|
+
.join("\n");
|
|
2943
|
+
throw new Error(
|
|
2944
|
+
`#${shortId} is not in your active project (projectId: ${resolved.preferredProjectId}). ` +
|
|
2945
|
+
`It exists in ${resolved.candidates.length} other project(s) you can access:\n${list}\n\n` +
|
|
2946
|
+
`Switch with harmony_set_project_context, or pass an explicit projectId to fetch it directly.`,
|
|
2947
|
+
);
|
|
2948
|
+
}
|
|
2949
|
+
|
|
2950
|
+
if (resolved.kind === "ambiguous") {
|
|
2951
|
+
// Never guess: hand the candidates back so the caller can disambiguate.
|
|
2952
|
+
const list = resolved.candidates
|
|
2953
|
+
.map(
|
|
2954
|
+
(c) =>
|
|
2955
|
+
` • "${c.title}" — project "${c.projectName ?? c.projectId}"${
|
|
2956
|
+
c.workspaceName ? ` / workspace "${c.workspaceName}"` : ""
|
|
2957
|
+
} (projectId: ${c.projectId})`,
|
|
2958
|
+
)
|
|
2959
|
+
.join("\n");
|
|
2960
|
+
return {
|
|
2961
|
+
success: true,
|
|
2962
|
+
needsDisambiguation: true,
|
|
2963
|
+
shortId,
|
|
2964
|
+
candidates: resolved.candidates,
|
|
2965
|
+
message:
|
|
2966
|
+
`#${shortId} exists in ${resolved.candidates.length} projects you can access:\n${list}\n\n` +
|
|
2967
|
+
`Ask which one is meant, then re-fetch with an explicit projectId ` +
|
|
2968
|
+
`(or call harmony_set_project_context first).`,
|
|
2969
|
+
};
|
|
2970
|
+
}
|
|
2971
|
+
|
|
2972
|
+
// not_found — name the searched scope instead of the bare "No project
|
|
2973
|
+
// specified", and point at the tools that list the caller's options.
|
|
2974
|
+
throw new Error(
|
|
2975
|
+
resolved.searchedProjectCount === 0
|
|
2976
|
+
? `#${shortId} can't be resolved: no project is accessible to this connection. ` +
|
|
2977
|
+
`Check the workspace this connection is authorized for with harmony_list_workspaces.`
|
|
2978
|
+
: `Card #${shortId} was not found in any of the ${resolved.searchedProjectCount} ` +
|
|
2979
|
+
`project(s) across ${resolved.searchedWorkspaceCount} workspace(s) this connection can access. ` +
|
|
2980
|
+
`Use harmony_list_projects to see them, or pass an explicit projectId.`,
|
|
2981
|
+
);
|
|
2878
2982
|
}
|
|
2879
2983
|
const cardId = z.string().uuid().parse(args.cardId);
|
|
2880
2984
|
const result = await client.getCard(cardId);
|