@gethmy/mcp 2.17.0 → 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 +95 -5
- package/dist/index.js +95 -5
- package/dist/lib/api-client.js +22 -0
- package/package.json +1 -1
- package/src/api-client.ts +84 -1
- package/src/auto-session.ts +54 -1
- package/src/server.ts +143 -3
package/dist/cli.js
CHANGED
|
@@ -1499,6 +1499,21 @@ var TIMINGS = {
|
|
|
1499
1499
|
QUERY_STALE_TIME: 1000 * 60 * 5,
|
|
1500
1500
|
QUERY_GC_TIME: 1000 * 60 * 60 * 24
|
|
1501
1501
|
};
|
|
1502
|
+
// ../harmony-shared/dist/playbookStage.js
|
|
1503
|
+
var STAGE_DAEMON_OWNED_TOOLS = [
|
|
1504
|
+
"mcp__harmony__harmony_end_agent_session",
|
|
1505
|
+
"mcp__harmony__harmony_start_agent_session",
|
|
1506
|
+
"mcp__harmony__harmony_move_card"
|
|
1507
|
+
];
|
|
1508
|
+
// ../harmony-shared/dist/reviewTools.js
|
|
1509
|
+
var REVIEW_DISALLOWED_TOOLS = [
|
|
1510
|
+
...STAGE_DAEMON_OWNED_TOOLS,
|
|
1511
|
+
"mcp__harmony__harmony_update_card",
|
|
1512
|
+
"mcp__harmony__harmony_create_subtask",
|
|
1513
|
+
"mcp__harmony__harmony_update_subtask",
|
|
1514
|
+
"mcp__harmony__harmony_delete_subtask",
|
|
1515
|
+
"mcp__harmony__harmony_toggle_subtask"
|
|
1516
|
+
];
|
|
1502
1517
|
// ../harmony-shared/dist/stageHandoff.js
|
|
1503
1518
|
var HANDOFF_MARKER = "harmony:stage-handoff";
|
|
1504
1519
|
var HANDOFF_BLOCK_RE = new RegExp("```json\\s*\\n//\\s*" + HANDOFF_MARKER + "\\s*\\n([\\s\\S]*?)\\n```", "m");
|
|
@@ -1761,6 +1776,9 @@ class HarmonyApiClient {
|
|
|
1761
1776
|
async getWorkspaceMembers(workspaceId) {
|
|
1762
1777
|
return this.request("GET", `/workspaces/${workspaceId}/members`);
|
|
1763
1778
|
}
|
|
1779
|
+
async getAuthContext() {
|
|
1780
|
+
return this.request("GET", "/auth/context");
|
|
1781
|
+
}
|
|
1764
1782
|
async listWorkspaceAgents(workspaceId) {
|
|
1765
1783
|
return this.request("GET", `/workspaces/${workspaceId}/agents`);
|
|
1766
1784
|
}
|
|
@@ -1851,6 +1869,10 @@ class HarmonyApiClient {
|
|
|
1851
1869
|
async getCardByShortId(projectId, shortId) {
|
|
1852
1870
|
return this.request("GET", `/projects/${projectId}/cards/${shortId}`);
|
|
1853
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
|
+
}
|
|
1854
1876
|
async bulkGetCards(projectId, shortIds) {
|
|
1855
1877
|
return this.request("POST", `/projects/${projectId}/cards/bulk-get`, {
|
|
1856
1878
|
shortIds
|
|
@@ -2481,6 +2503,7 @@ var AUTO_START_TRIGGERS = new Set([
|
|
|
2481
2503
|
]);
|
|
2482
2504
|
var INACTIVITY_TIMEOUT_MS = 10 * 60 * 1000;
|
|
2483
2505
|
var CHECK_INTERVAL_MS = 60 * 1000;
|
|
2506
|
+
var HEARTBEAT_ACTIVITY_WINDOW_MS = 25 * 60 * 1000;
|
|
2484
2507
|
var DEFAULT_SCOPE = "__default__";
|
|
2485
2508
|
var scopes = new Map;
|
|
2486
2509
|
var inactivityTimer = null;
|
|
@@ -2618,17 +2641,25 @@ function noteSessionStatus(cardId, status, scopeId = DEFAULT_SCOPE) {
|
|
|
2618
2641
|
session.status = status;
|
|
2619
2642
|
}
|
|
2620
2643
|
function heartbeatActiveSessions() {
|
|
2644
|
+
const now = Date.now();
|
|
2621
2645
|
for (const scope of scopes.values()) {
|
|
2622
2646
|
const client3 = scope.clientGetter?.();
|
|
2623
2647
|
if (!client3)
|
|
2624
2648
|
continue;
|
|
2625
|
-
for (const session of scope.sessions.
|
|
2649
|
+
for (const [cardId, session] of [...scope.sessions.entries()]) {
|
|
2626
2650
|
if ((session.status ?? "working") !== "working")
|
|
2627
2651
|
continue;
|
|
2652
|
+
if (now - session.lastActivityAt > HEARTBEAT_ACTIVITY_WINDOW_MS)
|
|
2653
|
+
continue;
|
|
2628
2654
|
client3.updateAgentProgress(session.cardId, {
|
|
2629
2655
|
agentIdentifier: session.agentIdentifier,
|
|
2630
2656
|
agentName: session.agentName,
|
|
2631
|
-
status: "working"
|
|
2657
|
+
status: "working",
|
|
2658
|
+
noCreate: true
|
|
2659
|
+
}).then((res) => {
|
|
2660
|
+
if (res?.session === null && scope.sessions.get(cardId) === session) {
|
|
2661
|
+
scope.sessions.delete(cardId);
|
|
2662
|
+
}
|
|
2632
2663
|
}).catch(() => {});
|
|
2633
2664
|
}
|
|
2634
2665
|
}
|
|
@@ -5736,9 +5767,51 @@ async function handleToolCall(name, args, deps) {
|
|
|
5736
5767
|
}
|
|
5737
5768
|
if (hasShortId) {
|
|
5738
5769
|
const shortId = z.number().int().positive().parse(args.shortId);
|
|
5739
|
-
const
|
|
5740
|
-
|
|
5741
|
-
|
|
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.`);
|
|
5742
5815
|
}
|
|
5743
5816
|
const cardId = z.string().uuid().parse(args.cardId);
|
|
5744
5817
|
const result = await client3.getCard(cardId);
|
|
@@ -6148,6 +6221,23 @@ async function handleToolCall(name, args, deps) {
|
|
|
6148
6221
|
}
|
|
6149
6222
|
case "harmony_set_workspace_context": {
|
|
6150
6223
|
const workspaceId = z.string().uuid().parse(args.workspaceId);
|
|
6224
|
+
const { workspaces } = await client3.listWorkspaces();
|
|
6225
|
+
const available = workspaces ?? [];
|
|
6226
|
+
const match = available.find((w) => w?.id === workspaceId);
|
|
6227
|
+
if (!match) {
|
|
6228
|
+
const options = available.filter((w) => w?.id).map((w) => ` - ${w.name ?? "(unnamed)"} (${w.id})`).join(`
|
|
6229
|
+
`);
|
|
6230
|
+
throw new Error(`Workspace ${workspaceId} is not available to this connection.
|
|
6231
|
+
|
|
6232
|
+
` + `This MCP connection is authorized for a specific workspace, chosen ` + `during OAuth consent — it cannot read or write any other workspace, ` + `even ones you're a member of.
|
|
6233
|
+
|
|
6234
|
+
` + (options ? `Available here:
|
|
6235
|
+
${options}
|
|
6236
|
+
|
|
6237
|
+
` : `No workspaces are available to this connection.
|
|
6238
|
+
|
|
6239
|
+
`) + `To work in a different workspace, run /mcp to reconnect and select ` + `it on the consent screen.`);
|
|
6240
|
+
}
|
|
6151
6241
|
deps.setActiveWorkspace(workspaceId);
|
|
6152
6242
|
return { success: true, activeWorkspaceId: workspaceId };
|
|
6153
6243
|
}
|
package/dist/index.js
CHANGED
|
@@ -1494,6 +1494,21 @@ var TIMINGS = {
|
|
|
1494
1494
|
QUERY_STALE_TIME: 1000 * 60 * 5,
|
|
1495
1495
|
QUERY_GC_TIME: 1000 * 60 * 60 * 24
|
|
1496
1496
|
};
|
|
1497
|
+
// ../harmony-shared/dist/playbookStage.js
|
|
1498
|
+
var STAGE_DAEMON_OWNED_TOOLS = [
|
|
1499
|
+
"mcp__harmony__harmony_end_agent_session",
|
|
1500
|
+
"mcp__harmony__harmony_start_agent_session",
|
|
1501
|
+
"mcp__harmony__harmony_move_card"
|
|
1502
|
+
];
|
|
1503
|
+
// ../harmony-shared/dist/reviewTools.js
|
|
1504
|
+
var REVIEW_DISALLOWED_TOOLS = [
|
|
1505
|
+
...STAGE_DAEMON_OWNED_TOOLS,
|
|
1506
|
+
"mcp__harmony__harmony_update_card",
|
|
1507
|
+
"mcp__harmony__harmony_create_subtask",
|
|
1508
|
+
"mcp__harmony__harmony_update_subtask",
|
|
1509
|
+
"mcp__harmony__harmony_delete_subtask",
|
|
1510
|
+
"mcp__harmony__harmony_toggle_subtask"
|
|
1511
|
+
];
|
|
1497
1512
|
// ../harmony-shared/dist/stageHandoff.js
|
|
1498
1513
|
var HANDOFF_MARKER = "harmony:stage-handoff";
|
|
1499
1514
|
var HANDOFF_BLOCK_RE = new RegExp("```json\\s*\\n//\\s*" + HANDOFF_MARKER + "\\s*\\n([\\s\\S]*?)\\n```", "m");
|
|
@@ -1756,6 +1771,9 @@ class HarmonyApiClient {
|
|
|
1756
1771
|
async getWorkspaceMembers(workspaceId) {
|
|
1757
1772
|
return this.request("GET", `/workspaces/${workspaceId}/members`);
|
|
1758
1773
|
}
|
|
1774
|
+
async getAuthContext() {
|
|
1775
|
+
return this.request("GET", "/auth/context");
|
|
1776
|
+
}
|
|
1759
1777
|
async listWorkspaceAgents(workspaceId) {
|
|
1760
1778
|
return this.request("GET", `/workspaces/${workspaceId}/agents`);
|
|
1761
1779
|
}
|
|
@@ -1846,6 +1864,10 @@ class HarmonyApiClient {
|
|
|
1846
1864
|
async getCardByShortId(projectId, shortId) {
|
|
1847
1865
|
return this.request("GET", `/projects/${projectId}/cards/${shortId}`);
|
|
1848
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
|
+
}
|
|
1849
1871
|
async bulkGetCards(projectId, shortIds) {
|
|
1850
1872
|
return this.request("POST", `/projects/${projectId}/cards/bulk-get`, {
|
|
1851
1873
|
shortIds
|
|
@@ -2476,6 +2498,7 @@ var AUTO_START_TRIGGERS = new Set([
|
|
|
2476
2498
|
]);
|
|
2477
2499
|
var INACTIVITY_TIMEOUT_MS = 10 * 60 * 1000;
|
|
2478
2500
|
var CHECK_INTERVAL_MS = 60 * 1000;
|
|
2501
|
+
var HEARTBEAT_ACTIVITY_WINDOW_MS = 25 * 60 * 1000;
|
|
2479
2502
|
var DEFAULT_SCOPE = "__default__";
|
|
2480
2503
|
var scopes = new Map;
|
|
2481
2504
|
var inactivityTimer = null;
|
|
@@ -2613,17 +2636,25 @@ function noteSessionStatus(cardId, status, scopeId = DEFAULT_SCOPE) {
|
|
|
2613
2636
|
session.status = status;
|
|
2614
2637
|
}
|
|
2615
2638
|
function heartbeatActiveSessions() {
|
|
2639
|
+
const now = Date.now();
|
|
2616
2640
|
for (const scope of scopes.values()) {
|
|
2617
2641
|
const client3 = scope.clientGetter?.();
|
|
2618
2642
|
if (!client3)
|
|
2619
2643
|
continue;
|
|
2620
|
-
for (const session of scope.sessions.
|
|
2644
|
+
for (const [cardId, session] of [...scope.sessions.entries()]) {
|
|
2621
2645
|
if ((session.status ?? "working") !== "working")
|
|
2622
2646
|
continue;
|
|
2647
|
+
if (now - session.lastActivityAt > HEARTBEAT_ACTIVITY_WINDOW_MS)
|
|
2648
|
+
continue;
|
|
2623
2649
|
client3.updateAgentProgress(session.cardId, {
|
|
2624
2650
|
agentIdentifier: session.agentIdentifier,
|
|
2625
2651
|
agentName: session.agentName,
|
|
2626
|
-
status: "working"
|
|
2652
|
+
status: "working",
|
|
2653
|
+
noCreate: true
|
|
2654
|
+
}).then((res) => {
|
|
2655
|
+
if (res?.session === null && scope.sessions.get(cardId) === session) {
|
|
2656
|
+
scope.sessions.delete(cardId);
|
|
2657
|
+
}
|
|
2627
2658
|
}).catch(() => {});
|
|
2628
2659
|
}
|
|
2629
2660
|
}
|
|
@@ -5731,9 +5762,51 @@ async function handleToolCall(name, args, deps) {
|
|
|
5731
5762
|
}
|
|
5732
5763
|
if (hasShortId) {
|
|
5733
5764
|
const shortId = z.number().int().positive().parse(args.shortId);
|
|
5734
|
-
const
|
|
5735
|
-
|
|
5736
|
-
|
|
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.`);
|
|
5737
5810
|
}
|
|
5738
5811
|
const cardId = z.string().uuid().parse(args.cardId);
|
|
5739
5812
|
const result = await client3.getCard(cardId);
|
|
@@ -6143,6 +6216,23 @@ async function handleToolCall(name, args, deps) {
|
|
|
6143
6216
|
}
|
|
6144
6217
|
case "harmony_set_workspace_context": {
|
|
6145
6218
|
const workspaceId = z.string().uuid().parse(args.workspaceId);
|
|
6219
|
+
const { workspaces } = await client3.listWorkspaces();
|
|
6220
|
+
const available = workspaces ?? [];
|
|
6221
|
+
const match = available.find((w) => w?.id === workspaceId);
|
|
6222
|
+
if (!match) {
|
|
6223
|
+
const options = available.filter((w) => w?.id).map((w) => ` - ${w.name ?? "(unnamed)"} (${w.id})`).join(`
|
|
6224
|
+
`);
|
|
6225
|
+
throw new Error(`Workspace ${workspaceId} is not available to this connection.
|
|
6226
|
+
|
|
6227
|
+
` + `This MCP connection is authorized for a specific workspace, chosen ` + `during OAuth consent — it cannot read or write any other workspace, ` + `even ones you're a member of.
|
|
6228
|
+
|
|
6229
|
+
` + (options ? `Available here:
|
|
6230
|
+
${options}
|
|
6231
|
+
|
|
6232
|
+
` : `No workspaces are available to this connection.
|
|
6233
|
+
|
|
6234
|
+
`) + `To work in a different workspace, run /mcp to reconnect and select ` + `it on the consent screen.`);
|
|
6235
|
+
}
|
|
6146
6236
|
deps.setActiveWorkspace(workspaceId);
|
|
6147
6237
|
return { success: true, activeWorkspaceId: workspaceId };
|
|
6148
6238
|
}
|
package/dist/lib/api-client.js
CHANGED
|
@@ -946,6 +946,21 @@ var TIMINGS = {
|
|
|
946
946
|
QUERY_STALE_TIME: 1000 * 60 * 5,
|
|
947
947
|
QUERY_GC_TIME: 1000 * 60 * 60 * 24
|
|
948
948
|
};
|
|
949
|
+
// ../harmony-shared/dist/playbookStage.js
|
|
950
|
+
var STAGE_DAEMON_OWNED_TOOLS = [
|
|
951
|
+
"mcp__harmony__harmony_end_agent_session",
|
|
952
|
+
"mcp__harmony__harmony_start_agent_session",
|
|
953
|
+
"mcp__harmony__harmony_move_card"
|
|
954
|
+
];
|
|
955
|
+
// ../harmony-shared/dist/reviewTools.js
|
|
956
|
+
var REVIEW_DISALLOWED_TOOLS = [
|
|
957
|
+
...STAGE_DAEMON_OWNED_TOOLS,
|
|
958
|
+
"mcp__harmony__harmony_update_card",
|
|
959
|
+
"mcp__harmony__harmony_create_subtask",
|
|
960
|
+
"mcp__harmony__harmony_update_subtask",
|
|
961
|
+
"mcp__harmony__harmony_delete_subtask",
|
|
962
|
+
"mcp__harmony__harmony_toggle_subtask"
|
|
963
|
+
];
|
|
949
964
|
// ../harmony-shared/dist/stageHandoff.js
|
|
950
965
|
var HANDOFF_MARKER = "harmony:stage-handoff";
|
|
951
966
|
var HANDOFF_BLOCK_RE = new RegExp("```json\\s*\\n//\\s*" + HANDOFF_MARKER + "\\s*\\n([\\s\\S]*?)\\n```", "m");
|
|
@@ -1208,6 +1223,9 @@ class HarmonyApiClient {
|
|
|
1208
1223
|
async getWorkspaceMembers(workspaceId) {
|
|
1209
1224
|
return this.request("GET", `/workspaces/${workspaceId}/members`);
|
|
1210
1225
|
}
|
|
1226
|
+
async getAuthContext() {
|
|
1227
|
+
return this.request("GET", "/auth/context");
|
|
1228
|
+
}
|
|
1211
1229
|
async listWorkspaceAgents(workspaceId) {
|
|
1212
1230
|
return this.request("GET", `/workspaces/${workspaceId}/agents`);
|
|
1213
1231
|
}
|
|
@@ -1298,6 +1316,10 @@ class HarmonyApiClient {
|
|
|
1298
1316
|
async getCardByShortId(projectId, shortId) {
|
|
1299
1317
|
return this.request("GET", `/projects/${projectId}/cards/${shortId}`);
|
|
1300
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
|
+
}
|
|
1301
1323
|
async bulkGetCards(projectId, shortIds) {
|
|
1302
1324
|
return this.request("POST", `/projects/${projectId}/cards/bulk-get`, {
|
|
1303
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 {
|
|
@@ -461,6 +501,22 @@ export class HarmonyApiClient {
|
|
|
461
501
|
return this.request("GET", `/workspaces/${workspaceId}/members`);
|
|
462
502
|
}
|
|
463
503
|
|
|
504
|
+
/**
|
|
505
|
+
* The auth user THIS client's credential resolves to server-side — the id
|
|
506
|
+
* harmony-api stamps onto sessions it creates for this caller. The agent
|
|
507
|
+
* daemon uses it to assert its configured `userEmail` and its API key are the
|
|
508
|
+
* same user; a divergence would make it fail closed on its own comment
|
|
509
|
+
* artifacts (card #693). `GET /v1/auth/context` is long-deployed (the MCP
|
|
510
|
+
* transport authenticates every request through it).
|
|
511
|
+
*/
|
|
512
|
+
async getAuthContext(): Promise<{
|
|
513
|
+
userId: string;
|
|
514
|
+
source: "api_key" | "oauth" | "jwt";
|
|
515
|
+
workspaceId: string | null;
|
|
516
|
+
}> {
|
|
517
|
+
return this.request("GET", "/auth/context");
|
|
518
|
+
}
|
|
519
|
+
|
|
464
520
|
async listWorkspaceAgents(
|
|
465
521
|
workspaceId: string,
|
|
466
522
|
): Promise<{ agents: WorkspaceAgent[] }> {
|
|
@@ -678,6 +734,21 @@ export class HarmonyApiClient {
|
|
|
678
734
|
return this.request("GET", `/projects/${projectId}/cards/${shortId}`);
|
|
679
735
|
}
|
|
680
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
|
+
|
|
681
752
|
async bulkGetCards(
|
|
682
753
|
projectId: string,
|
|
683
754
|
shortIds: number[],
|
|
@@ -1067,6 +1138,12 @@ export class HarmonyApiClient {
|
|
|
1067
1138
|
recoveryBranch?: string;
|
|
1068
1139
|
attemptNumber?: number;
|
|
1069
1140
|
maxAttempts?: number;
|
|
1141
|
+
/**
|
|
1142
|
+
* Update-only: never create a session if none is live, answer
|
|
1143
|
+
* `{session: null}` instead. Set by the auto-session heartbeat, which must
|
|
1144
|
+
* report on a session but must never bring one back from the dead (#696).
|
|
1145
|
+
*/
|
|
1146
|
+
noCreate?: boolean;
|
|
1070
1147
|
},
|
|
1071
1148
|
): Promise<{ session: unknown; created: boolean }> {
|
|
1072
1149
|
return this.request("POST", `/cards/${cardId}/agent-context`, data);
|
|
@@ -1075,7 +1152,13 @@ export class HarmonyApiClient {
|
|
|
1075
1152
|
async endAgentSession(
|
|
1076
1153
|
cardId: string,
|
|
1077
1154
|
data?: {
|
|
1078
|
-
|
|
1155
|
+
/**
|
|
1156
|
+
* `cancelled` marks a HUMAN stop specifically — it is what arms the
|
|
1157
|
+
* server's human-stop cooldown (card #663). Never use it for a graceful
|
|
1158
|
+
* shutdown or an internal abort, which must stay `paused`/`failed` so the
|
|
1159
|
+
* card can be re-picked immediately (card #696).
|
|
1160
|
+
*/
|
|
1161
|
+
status?: "completed" | "paused" | "failed" | "cancelled";
|
|
1079
1162
|
progressPercent?: number;
|
|
1080
1163
|
costCents?: number;
|
|
1081
1164
|
inputTokens?: number;
|
package/src/auto-session.ts
CHANGED
|
@@ -116,6 +116,27 @@ export const AUTO_START_TRIGGERS = new Set([
|
|
|
116
116
|
export const INACTIVITY_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes
|
|
117
117
|
const CHECK_INTERVAL_MS = 60 * 1000; // 60 seconds
|
|
118
118
|
|
|
119
|
+
/**
|
|
120
|
+
* How long after the agent's last harmony tool call we keep heartbeating a
|
|
121
|
+
* session (card #696).
|
|
122
|
+
*
|
|
123
|
+
* WHY: the heartbeat asserts "this agent is still working". An *explicit*
|
|
124
|
+
* session (`/hmy`) is exempt from `checkInactivity`, so an unbounded heartbeat
|
|
125
|
+
* asserted that forever — bumping `updated_at` every 60s and starving the
|
|
126
|
+
* 30-minute `cleanup_stale_agent_sessions` cron, which is the only backstop for
|
|
127
|
+
* a session whose agent finished (card moved to Review) but never called
|
|
128
|
+
* `harmony_end_agent_session`. The session then showed "working" forever.
|
|
129
|
+
*
|
|
130
|
+
* Bounding the heartbeat by real activity restores that backstop: once the
|
|
131
|
+
* agent stops calling harmony tools we stop beating, `updated_at` goes stale,
|
|
132
|
+
* and the cron closes the row as `failed`/`stale`. Sits deliberately BELOW the
|
|
133
|
+
* cron's 30-minute threshold so a stalled session can actually reach it, and
|
|
134
|
+
* well above the milestone-gap the heartbeat exists to bridge (#608). Activity
|
|
135
|
+
* revives it — `trackActivity` refreshes `lastActivityAt` on every card-scoped
|
|
136
|
+
* tool call.
|
|
137
|
+
*/
|
|
138
|
+
export const HEARTBEAT_ACTIVITY_WINDOW_MS = 25 * 60 * 1000; // 25 minutes
|
|
139
|
+
|
|
119
140
|
/**
|
|
120
141
|
* Scope used when no `scopeId` is supplied. stdio is single-user, so it shares
|
|
121
142
|
* one scope; the unit tests also exercise this default scope exclusively.
|
|
@@ -429,22 +450,54 @@ export function noteSessionStatus(
|
|
|
429
450
|
* `updated_at`. Covers both auto-started and explicit sessions (both live in
|
|
430
451
|
* `scope.sessions`).
|
|
431
452
|
*
|
|
453
|
+
* Two bounds keep a heartbeat from outliving the work it reports on (card #696):
|
|
454
|
+
*
|
|
455
|
+
* 1. `noCreate` — a heartbeat may only ever UPDATE a live session, never create
|
|
456
|
+
* one. The endpoint shares a single upsert with `start_agent_session`, so
|
|
457
|
+
* without this a heartbeat for a session ended out-of-band (the UI "Stop",
|
|
458
|
+
* which cannot reach this process's map) re-created the row and re-added the
|
|
459
|
+
* `agent` label. The #663 human-stop cooldown only deferred that by 10
|
|
460
|
+
* minutes, so a stopped card reliably respawned exactly one cooldown later.
|
|
461
|
+
* `session: null` back means the session is gone → untrack, stop beating.
|
|
462
|
+
* 2. `HEARTBEAT_ACTIVITY_WINDOW_MS` — only beat while the agent is still calling
|
|
463
|
+
* harmony tools, so an abandoned session goes stale and the cron can close it.
|
|
464
|
+
*
|
|
432
465
|
* No double-heartbeat for the daemon: `packages/harmony-agent` has its own
|
|
433
466
|
* progress-tracker heartbeat and never routes through mcp-server auto-sessions.
|
|
434
467
|
*/
|
|
435
468
|
export function heartbeatActiveSessions(): void {
|
|
469
|
+
const now = Date.now();
|
|
436
470
|
for (const scope of scopes.values()) {
|
|
437
471
|
const client = scope.clientGetter?.();
|
|
438
472
|
if (!client) continue;
|
|
439
|
-
|
|
473
|
+
// Snapshot: the untrack-on-ended path below mutates the map.
|
|
474
|
+
for (const [cardId, session] of [...scope.sessions.entries()]) {
|
|
440
475
|
// Default-undefined status is treated as `working` (sessions created
|
|
441
476
|
// before a status was reported).
|
|
442
477
|
if ((session.status ?? "working") !== "working") continue;
|
|
478
|
+
// Silent agent → stop asserting it's working; hand the row to the cron.
|
|
479
|
+
if (now - session.lastActivityAt > HEARTBEAT_ACTIVITY_WINDOW_MS) continue;
|
|
443
480
|
client
|
|
444
481
|
.updateAgentProgress(session.cardId, {
|
|
445
482
|
agentIdentifier: session.agentIdentifier,
|
|
446
483
|
agentName: session.agentName,
|
|
447
484
|
status: "working",
|
|
485
|
+
noCreate: true,
|
|
486
|
+
})
|
|
487
|
+
.then((res) => {
|
|
488
|
+
// Ended out-of-band (UI "Stop", another client, the stale cron). Drop
|
|
489
|
+
// it so we never beat — nor respawn — this session again. Only an
|
|
490
|
+
// explicit null counts: an unknown/absent field must not untrack a
|
|
491
|
+
// live session.
|
|
492
|
+
//
|
|
493
|
+
// Identity-checked: this reply describes the session we beat, which
|
|
494
|
+
// may no longer be the one tracked for this card. An end + restart
|
|
495
|
+
// landing inside the request's round-trip replaces the map entry, and
|
|
496
|
+
// deleting by cardId alone would drop that NEW live session — leaving
|
|
497
|
+
// it unbeaten until the stale cron closed it early.
|
|
498
|
+
if (res?.session === null && scope.sessions.get(cardId) === session) {
|
|
499
|
+
scope.sessions.delete(cardId);
|
|
500
|
+
}
|
|
448
501
|
})
|
|
449
502
|
.catch(() => {
|
|
450
503
|
// Best-effort: a transient failure just defers the bump to the next
|
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);
|
|
@@ -3489,6 +3593,42 @@ async function handleToolCall(
|
|
|
3489
3593
|
|
|
3490
3594
|
case "harmony_set_workspace_context": {
|
|
3491
3595
|
const workspaceId = z.string().uuid().parse(args.workspaceId);
|
|
3596
|
+
|
|
3597
|
+
// Fail fast on a workspace this connection can't actually reach (#695).
|
|
3598
|
+
// An OAuth token is bound at consent time to the single workspace the user
|
|
3599
|
+
// picked ("It will be able to read and modify cards, plans, and memory in
|
|
3600
|
+
// the workspace you select below"), and harmony-api enforces that on every
|
|
3601
|
+
// call. Setting the context is a pure local assignment, so without this
|
|
3602
|
+
// check it always "succeeded" and the caller only discovered the binding
|
|
3603
|
+
// when some later, unrelated tool call 403'd mid-task — the exact confusion
|
|
3604
|
+
// this card reports. `listWorkspaces` is server-side scoped to the bound
|
|
3605
|
+
// workspace, so presence in it is the authoritative reachability answer for
|
|
3606
|
+
// OAuth and legacy-key connections alike.
|
|
3607
|
+
const { workspaces } = await client.listWorkspaces();
|
|
3608
|
+
const available = (workspaces ?? []) as Array<{
|
|
3609
|
+
id?: string;
|
|
3610
|
+
name?: string;
|
|
3611
|
+
}>;
|
|
3612
|
+
const match = available.find((w) => w?.id === workspaceId);
|
|
3613
|
+
|
|
3614
|
+
if (!match) {
|
|
3615
|
+
const options = available
|
|
3616
|
+
.filter((w) => w?.id)
|
|
3617
|
+
.map((w) => ` - ${w.name ?? "(unnamed)"} (${w.id})`)
|
|
3618
|
+
.join("\n");
|
|
3619
|
+
throw new Error(
|
|
3620
|
+
`Workspace ${workspaceId} is not available to this connection.\n\n` +
|
|
3621
|
+
`This MCP connection is authorized for a specific workspace, chosen ` +
|
|
3622
|
+
`during OAuth consent — it cannot read or write any other workspace, ` +
|
|
3623
|
+
`even ones you're a member of.\n\n` +
|
|
3624
|
+
(options
|
|
3625
|
+
? `Available here:\n${options}\n\n`
|
|
3626
|
+
: `No workspaces are available to this connection.\n\n`) +
|
|
3627
|
+
`To work in a different workspace, run /mcp to reconnect and select ` +
|
|
3628
|
+
`it on the consent screen.`,
|
|
3629
|
+
);
|
|
3630
|
+
}
|
|
3631
|
+
|
|
3492
3632
|
deps.setActiveWorkspace(workspaceId);
|
|
3493
3633
|
return { success: true, activeWorkspaceId: workspaceId };
|
|
3494
3634
|
}
|