@gethmy/mcp 2.23.0 → 2.25.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 +389 -141
- package/dist/index.js +356 -118
- package/dist/lib/api-client.js +137 -96
- package/dist/lib/config.js +79 -24
- package/dist/lib/oauth-refresh.js +76 -24
- package/package.json +1 -1
- package/src/api-client.ts +171 -5
- package/src/cli.ts +21 -12
- package/src/config.ts +201 -27
- package/src/playbook-metric-warnings.ts +56 -0
- package/src/prompt-builder.ts +9 -120
- package/src/read-consumer.ts +16 -0
- package/src/remote.ts +34 -6
- package/src/server.ts +195 -23
- package/src/tui/setup.ts +21 -6
package/src/server.ts
CHANGED
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
untrack,
|
|
30
30
|
} from "./auto-session.js";
|
|
31
31
|
import {
|
|
32
|
+
describeActiveContext,
|
|
32
33
|
getActiveProjectId,
|
|
33
34
|
getActiveWorkspaceId,
|
|
34
35
|
getApiUrl,
|
|
@@ -36,7 +37,7 @@ import {
|
|
|
36
37
|
getUserEmail,
|
|
37
38
|
isConfigured,
|
|
38
39
|
saveConfig,
|
|
39
|
-
|
|
40
|
+
setActiveContext,
|
|
40
41
|
setActiveWorkspace,
|
|
41
42
|
} from "./config.js";
|
|
42
43
|
import {
|
|
@@ -68,6 +69,7 @@ import {
|
|
|
68
69
|
} from "./memory-session.js";
|
|
69
70
|
import { lintTags, normalizeTags } from "./memory-tags.js";
|
|
70
71
|
import { onboardNewUser } from "./onboard.js";
|
|
72
|
+
import { collectPlaybookMetricWarnings } from "./playbook-metric-warnings.js";
|
|
71
73
|
import { stripSkillPreamble } from "./skills.js";
|
|
72
74
|
|
|
73
75
|
// --- Signed-upload handshake (artifacts & card attachments) ---
|
|
@@ -305,7 +307,16 @@ export interface ToolDeps {
|
|
|
305
307
|
isConfigured: () => boolean;
|
|
306
308
|
getActiveProjectId: () => string | null;
|
|
307
309
|
getActiveWorkspaceId: () => string | null;
|
|
308
|
-
|
|
310
|
+
/**
|
|
311
|
+
* Set the active project and its workspace TOGETHER (card #893). There is no
|
|
312
|
+
* project-only setter on purpose: writing the two independently is what let a
|
|
313
|
+
* project go active in a workspace it does not belong to, after which every
|
|
314
|
+
* workspace-scoped tool read the wrong workspace silently.
|
|
315
|
+
*/
|
|
316
|
+
setActiveContext: (context: {
|
|
317
|
+
projectId: string | null;
|
|
318
|
+
workspaceId: string | null;
|
|
319
|
+
}) => void;
|
|
309
320
|
setActiveWorkspace: (id: string | null) => void;
|
|
310
321
|
getApiUrl: () => string;
|
|
311
322
|
getMemoryDir: () => string | null;
|
|
@@ -1196,7 +1207,7 @@ export const TOOLS = {
|
|
|
1196
1207
|
},
|
|
1197
1208
|
harmony_classify_card: {
|
|
1198
1209
|
description:
|
|
1199
|
-
"
|
|
1210
|
+
"DEPRECATED — run sizing now happens at daemon pickup and is run-scoped, so nothing reads `model_tier`, `intent` or `complexity_score` any more; this tool still writes them, but only the type label has an effect. Prefer letting card creation apply the type label. Sets `intent` (plan/think/implement/review), `complexity_score` (0-10), `model_tier` (simple/advanced/research), stamps `classified_at`, and applies the type label (feature/bug/idea). Idempotent; never touches the user-owned `model_override`.",
|
|
1200
1211
|
inputSchema: {
|
|
1201
1212
|
type: "object",
|
|
1202
1213
|
properties: {
|
|
@@ -1469,11 +1480,17 @@ export const TOOLS = {
|
|
|
1469
1480
|
},
|
|
1470
1481
|
},
|
|
1471
1482
|
harmony_set_project_context: {
|
|
1472
|
-
description:
|
|
1483
|
+
description:
|
|
1484
|
+
"Set the active project context for subsequent operations. The project's workspace is set with it, so the two can never point at different places; pass workspaceId to skip the lookup.",
|
|
1473
1485
|
inputSchema: {
|
|
1474
1486
|
type: "object",
|
|
1475
1487
|
properties: {
|
|
1476
1488
|
projectId: { type: "string" },
|
|
1489
|
+
workspaceId: {
|
|
1490
|
+
type: "string",
|
|
1491
|
+
description:
|
|
1492
|
+
"The workspace this project belongs to. Optional — looked up when omitted.",
|
|
1493
|
+
},
|
|
1477
1494
|
},
|
|
1478
1495
|
required: ["projectId"],
|
|
1479
1496
|
},
|
|
@@ -2364,6 +2381,22 @@ export const TOOLS = {
|
|
|
2364
2381
|
description: "The playbook's ordered stage objects.",
|
|
2365
2382
|
items: { type: "object" },
|
|
2366
2383
|
},
|
|
2384
|
+
triggerType: {
|
|
2385
|
+
type: "string",
|
|
2386
|
+
enum: ["manual", "auto"],
|
|
2387
|
+
description:
|
|
2388
|
+
"'manual' (default) — the playbook is applied by a person. 'auto' — it claims matching cards itself, and requires autoBind.",
|
|
2389
|
+
},
|
|
2390
|
+
autoBind: {
|
|
2391
|
+
type: "object",
|
|
2392
|
+
description:
|
|
2393
|
+
"Auto-bind rule: {priority?: number, mode?: 'all'|'any', when: [{path, op, value}]}. Conditions are evaluated against the card's labels (lowercased), intent, complexity_score and priority with the gate operators eq/neq/gte/gt/lte/lt/contains/exists; 'contains' on labels is membership. Stored even while triggerType is 'manual', so a rule can be armed later without re-authoring it.",
|
|
2394
|
+
},
|
|
2395
|
+
catalogId: {
|
|
2396
|
+
type: "string",
|
|
2397
|
+
description:
|
|
2398
|
+
"Slug of the built-in template this came from (provenance only; never used to match).",
|
|
2399
|
+
},
|
|
2367
2400
|
},
|
|
2368
2401
|
required: ["name"],
|
|
2369
2402
|
},
|
|
@@ -2371,7 +2404,7 @@ export const TOOLS = {
|
|
|
2371
2404
|
|
|
2372
2405
|
harmony_update_playbook: {
|
|
2373
2406
|
description:
|
|
2374
|
-
"Update a playbook's name, description, steps/stages, enabled flag,
|
|
2407
|
+
"Update a playbook's name, description, steps/stages, enabled flag, lifecycle state ('active'|'deprecated'), or its auto-bind rule and arming.",
|
|
2375
2408
|
inputSchema: {
|
|
2376
2409
|
type: "object",
|
|
2377
2410
|
properties: {
|
|
@@ -2395,6 +2428,17 @@ export const TOOLS = {
|
|
|
2395
2428
|
enum: ["active", "deprecated"],
|
|
2396
2429
|
description: "Lifecycle state",
|
|
2397
2430
|
},
|
|
2431
|
+
triggerType: {
|
|
2432
|
+
type: "string",
|
|
2433
|
+
enum: ["manual", "auto"],
|
|
2434
|
+
description:
|
|
2435
|
+
"Arm ('auto') or disarm ('manual') automatic application. Arming requires a rule to be present or supplied in the same call.",
|
|
2436
|
+
},
|
|
2437
|
+
autoBind: {
|
|
2438
|
+
type: "object",
|
|
2439
|
+
description:
|
|
2440
|
+
"Replace the auto-bind rule: {priority?, mode?: 'all'|'any', when: [{path, op, value}]}. Pass null to remove it.",
|
|
2441
|
+
},
|
|
2398
2442
|
},
|
|
2399
2443
|
required: ["playbookId"],
|
|
2400
2444
|
},
|
|
@@ -3107,7 +3151,13 @@ async function handleToolCall(
|
|
|
3107
3151
|
// wrong-context resolve is visible immediately.
|
|
3108
3152
|
const established = activeProjectId == null;
|
|
3109
3153
|
if (established) {
|
|
3110
|
-
|
|
3154
|
+
// The pair, never the project alone (#893): `resolved.project`
|
|
3155
|
+
// carries its `workspaceId`, and discarding it here is what left
|
|
3156
|
+
// the active workspace pointing somewhere unrelated.
|
|
3157
|
+
deps.setActiveContext({
|
|
3158
|
+
projectId: resolved.project.id,
|
|
3159
|
+
workspaceId: resolved.project.workspaceId,
|
|
3160
|
+
});
|
|
3111
3161
|
}
|
|
3112
3162
|
return {
|
|
3113
3163
|
success: true,
|
|
@@ -3790,16 +3840,97 @@ async function handleToolCall(
|
|
|
3790
3840
|
|
|
3791
3841
|
case "harmony_set_project_context": {
|
|
3792
3842
|
const projectId = z.string().uuid().parse(args.projectId);
|
|
3793
|
-
|
|
3794
|
-
|
|
3843
|
+
|
|
3844
|
+
// Find the project's OWN workspace rather than keeping whichever one
|
|
3845
|
+
// happened to be active (#893). The two ids are one pair; setting half of
|
|
3846
|
+
// it is how a project ended up active in a foreign workspace, after which
|
|
3847
|
+
// every workspace-scoped tool read the wrong one and said nothing.
|
|
3848
|
+
//
|
|
3849
|
+
// An explicit `workspaceId` skips the lookup. Otherwise we search the
|
|
3850
|
+
// reachable workspaces, and REFUSE the whole call if that search cannot
|
|
3851
|
+
// name one — we never write half a pair.
|
|
3852
|
+
//
|
|
3853
|
+
// Refusing beats the two alternatives. Keeping the previous workspace is
|
|
3854
|
+
// the mismatch itself. Setting the project with a NULL workspace looks
|
|
3855
|
+
// safe but is not: on the remote transport an unpinned workspace falls
|
|
3856
|
+
// back to the grant's own, so the project would end up active inside it
|
|
3857
|
+
// anyway, and on stdio the write lands in a config file that outlives the
|
|
3858
|
+
// session. A project this connection cannot place is useless as context.
|
|
3859
|
+
const explicitWorkspaceId = args.workspaceId
|
|
3860
|
+
? z.string().uuid().parse(args.workspaceId)
|
|
3861
|
+
: null;
|
|
3862
|
+
|
|
3863
|
+
let owningWorkspaceId: string | null = explicitWorkspaceId;
|
|
3864
|
+
|
|
3865
|
+
if (!owningWorkspaceId) {
|
|
3866
|
+
try {
|
|
3867
|
+
const { workspaces } = await client.listWorkspaces();
|
|
3868
|
+
for (const workspace of workspaces as { id?: string }[]) {
|
|
3869
|
+
if (!workspace?.id) continue;
|
|
3870
|
+
const { projects } = await client.listProjects(workspace.id);
|
|
3871
|
+
if (
|
|
3872
|
+
(projects as { id?: string }[]).some((p) => p?.id === projectId)
|
|
3873
|
+
) {
|
|
3874
|
+
owningWorkspaceId = workspace.id;
|
|
3875
|
+
break;
|
|
3876
|
+
}
|
|
3877
|
+
}
|
|
3878
|
+
} catch (error) {
|
|
3879
|
+
// A timeout or a 500 says nothing about where the project lives.
|
|
3880
|
+
// Leave the pair untouched and say so.
|
|
3881
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
3882
|
+
return {
|
|
3883
|
+
success: false,
|
|
3884
|
+
activeProjectId: deps.getActiveProjectId(),
|
|
3885
|
+
activeWorkspaceId: deps.getActiveWorkspaceId(),
|
|
3886
|
+
note:
|
|
3887
|
+
`Could not resolve this project's workspace (${reason}), so the ` +
|
|
3888
|
+
`active context was left unchanged rather than half-written. ` +
|
|
3889
|
+
`Retry, or pass workspaceId explicitly to skip the lookup.`,
|
|
3890
|
+
};
|
|
3891
|
+
}
|
|
3892
|
+
|
|
3893
|
+
if (!owningWorkspaceId) {
|
|
3894
|
+
// Definitive: the listing succeeded and no reachable workspace holds
|
|
3895
|
+
// this project. Setting it would produce a context whose every
|
|
3896
|
+
// workspace-scoped call is wrong.
|
|
3897
|
+
return {
|
|
3898
|
+
success: false,
|
|
3899
|
+
activeProjectId: deps.getActiveProjectId(),
|
|
3900
|
+
activeWorkspaceId: deps.getActiveWorkspaceId(),
|
|
3901
|
+
note:
|
|
3902
|
+
`Project ${projectId} is not in any workspace this connection ` +
|
|
3903
|
+
`can reach, so the active context was left unchanged. Check ` +
|
|
3904
|
+
`harmony_list_projects, pass workspaceId explicitly, or ` +
|
|
3905
|
+
`reconnect with /mcp if it lives in another workspace.`,
|
|
3906
|
+
};
|
|
3907
|
+
}
|
|
3908
|
+
}
|
|
3909
|
+
|
|
3910
|
+
deps.setActiveContext({ projectId, workspaceId: owningWorkspaceId });
|
|
3911
|
+
return {
|
|
3912
|
+
success: true,
|
|
3913
|
+
activeProjectId: projectId,
|
|
3914
|
+
activeWorkspaceId: owningWorkspaceId,
|
|
3915
|
+
};
|
|
3795
3916
|
}
|
|
3796
3917
|
|
|
3797
3918
|
case "harmony_get_context": {
|
|
3919
|
+
// The rule lives in `describeActiveContext` and is called, not re-derived:
|
|
3920
|
+
// a second copy of an invariant is how the two halves drifted apart in
|
|
3921
|
+
// the first place. A project with no workspace still arrives from a
|
|
3922
|
+
// config written before #893.
|
|
3923
|
+
const report = describeActiveContext({
|
|
3924
|
+
projectId: deps.getActiveProjectId(),
|
|
3925
|
+
workspaceId: deps.getActiveWorkspaceId(),
|
|
3926
|
+
});
|
|
3798
3927
|
return {
|
|
3799
3928
|
success: true,
|
|
3800
3929
|
context: {
|
|
3801
|
-
activeWorkspaceId:
|
|
3802
|
-
activeProjectId:
|
|
3930
|
+
activeWorkspaceId: report.workspaceId,
|
|
3931
|
+
activeProjectId: report.projectId,
|
|
3932
|
+
consistent: report.consistent,
|
|
3933
|
+
...(report.note ? { note: report.note } : {}),
|
|
3803
3934
|
},
|
|
3804
3935
|
};
|
|
3805
3936
|
}
|
|
@@ -4550,6 +4681,12 @@ async function handleToolCall(
|
|
|
4550
4681
|
: undefined,
|
|
4551
4682
|
include_superseded: includeSuperseded,
|
|
4552
4683
|
include_episodes: includeEpisodes,
|
|
4684
|
+
// No consumer here on purpose (fix round 1, task 4): this fetches
|
|
4685
|
+
// an over-fetched candidate pool (up to `fetchLimit`, itself
|
|
4686
|
+
// padded above `requestedLimit`/`topK`) for Park to rescore, not
|
|
4687
|
+
// what the tool actually returns. Recording it would overstate
|
|
4688
|
+
// access_count by up to fetchLimit/finalCount per call. What we
|
|
4689
|
+
// actually deliver is recorded below, after trimming.
|
|
4553
4690
|
},
|
|
4554
4691
|
);
|
|
4555
4692
|
entities = (searchResult.entities ?? []) as any[];
|
|
@@ -4611,19 +4748,19 @@ async function handleToolCall(
|
|
|
4611
4748
|
trimmed = fitToBudget(trimmed, budgetTokens);
|
|
4612
4749
|
}
|
|
4613
4750
|
|
|
4614
|
-
//
|
|
4615
|
-
//
|
|
4616
|
-
//
|
|
4617
|
-
//
|
|
4618
|
-
//
|
|
4619
|
-
//
|
|
4620
|
-
//
|
|
4751
|
+
// Read accounting (task 4, fix round 1): record only the entities
|
|
4752
|
+
// actually delivered to the caller — after trimming and the budget
|
|
4753
|
+
// pass, not the larger over-fetched candidate pool the search above
|
|
4754
|
+
// deliberately left unrecorded. Routed through batch-touch, which
|
|
4755
|
+
// dispatches to the same recordEntityReads/record_entity_reads path
|
|
4756
|
+
// the search chokepoint uses when a consumer is given. Non-blocking;
|
|
4757
|
+
// best-effort.
|
|
4621
4758
|
if (trimmed.length > 0) {
|
|
4622
4759
|
const touchIds = trimmed
|
|
4623
4760
|
.map(({ entity }: { entity: any }) => entity?.id)
|
|
4624
4761
|
.filter((id: unknown): id is string => typeof id === "string");
|
|
4625
4762
|
if (touchIds.length > 0) {
|
|
4626
|
-
client.batchTouchMemoryEntities(touchIds).catch(() => {});
|
|
4763
|
+
client.batchTouchMemoryEntities(touchIds, "mcp-tool").catch(() => {});
|
|
4627
4764
|
}
|
|
4628
4765
|
}
|
|
4629
4766
|
|
|
@@ -5233,8 +5370,25 @@ async function handleToolCall(
|
|
|
5233
5370
|
name,
|
|
5234
5371
|
description: args.description as string | undefined,
|
|
5235
5372
|
steps: args.steps,
|
|
5373
|
+
// Auto-bind fields ride through untouched (#892) — the edge function
|
|
5374
|
+
// owns validation, so the rule cannot be shaped here and rejected
|
|
5375
|
+
// there, or vice versa.
|
|
5376
|
+
triggerType: args.triggerType as string | undefined,
|
|
5377
|
+
autoBind: args.autoBind,
|
|
5378
|
+
catalogId: args.catalogId as string | undefined,
|
|
5236
5379
|
});
|
|
5237
|
-
|
|
5380
|
+
// #922: surface custom-gate metrics no agent declares — non-blocking,
|
|
5381
|
+
// the playbook is already created.
|
|
5382
|
+
const warnings = await collectPlaybookMetricWarnings(
|
|
5383
|
+
client,
|
|
5384
|
+
workspaceId,
|
|
5385
|
+
args.steps,
|
|
5386
|
+
);
|
|
5387
|
+
return {
|
|
5388
|
+
success: true,
|
|
5389
|
+
playbook: result.playbook,
|
|
5390
|
+
...(warnings.length > 0 ? { warnings } : {}),
|
|
5391
|
+
};
|
|
5238
5392
|
}
|
|
5239
5393
|
|
|
5240
5394
|
case "harmony_update_playbook": {
|
|
@@ -5245,8 +5399,24 @@ async function handleToolCall(
|
|
|
5245
5399
|
steps: args.steps,
|
|
5246
5400
|
enabled: args.enabled as boolean | undefined,
|
|
5247
5401
|
state: args.state as string | undefined,
|
|
5402
|
+
// Arming and the rule (#892). `autoBind: null` clears the rule, so the
|
|
5403
|
+
// key is forwarded whenever present rather than only when truthy.
|
|
5404
|
+
triggerType: args.triggerType as string | undefined,
|
|
5405
|
+
...("autoBind" in args ? { autoBind: args.autoBind } : {}),
|
|
5248
5406
|
});
|
|
5249
|
-
|
|
5407
|
+
// #922: same non-blocking metric check as create. The playbook's own
|
|
5408
|
+
// workspace scopes the lookup — the active context may point elsewhere.
|
|
5409
|
+
const warnings = await collectPlaybookMetricWarnings(
|
|
5410
|
+
client,
|
|
5411
|
+
(result.playbook as { workspace_id?: string } | undefined)
|
|
5412
|
+
?.workspace_id,
|
|
5413
|
+
args.steps,
|
|
5414
|
+
);
|
|
5415
|
+
return {
|
|
5416
|
+
success: true,
|
|
5417
|
+
playbook: result.playbook,
|
|
5418
|
+
...(warnings.length > 0 ? { warnings } : {}),
|
|
5419
|
+
};
|
|
5250
5420
|
}
|
|
5251
5421
|
|
|
5252
5422
|
// Deprecated (#612) — see harmony_run_playbook above.
|
|
@@ -5338,8 +5508,10 @@ async function handleToolCall(
|
|
|
5338
5508
|
|
|
5339
5509
|
// Save config and reset client
|
|
5340
5510
|
deps.saveConfig({ apiKey: result.apiKey.rawKey });
|
|
5341
|
-
deps.
|
|
5342
|
-
|
|
5511
|
+
deps.setActiveContext({
|
|
5512
|
+
projectId: result.project.id,
|
|
5513
|
+
workspaceId: result.workspace.id,
|
|
5514
|
+
});
|
|
5343
5515
|
deps.resetClient();
|
|
5344
5516
|
|
|
5345
5517
|
return {
|
|
@@ -5365,7 +5537,7 @@ function createConfigDeps(): ToolDeps {
|
|
|
5365
5537
|
isConfigured,
|
|
5366
5538
|
getActiveProjectId: () => getActiveProjectId(),
|
|
5367
5539
|
getActiveWorkspaceId: () => getActiveWorkspaceId(),
|
|
5368
|
-
|
|
5540
|
+
setActiveContext: (context) => setActiveContext(context),
|
|
5369
5541
|
setActiveWorkspace: (id) => setActiveWorkspace(id),
|
|
5370
5542
|
getApiUrl,
|
|
5371
5543
|
getMemoryDir: () => getMemoryDir(),
|
package/src/tui/setup.ts
CHANGED
|
@@ -19,8 +19,7 @@ import {
|
|
|
19
19
|
loadConfig,
|
|
20
20
|
saveConfig,
|
|
21
21
|
saveLocalConfig,
|
|
22
|
-
|
|
23
|
-
setActiveWorkspace,
|
|
22
|
+
setActiveContext,
|
|
24
23
|
} from "../config.js";
|
|
25
24
|
import { loginWithBrowser, type OAuthTokens } from "../oauth-login.js";
|
|
26
25
|
import { onboardNewUser } from "../onboard.js";
|
|
@@ -960,8 +959,10 @@ export async function runSetup(options: SetupOptions = {}): Promise<void> {
|
|
|
960
959
|
|
|
961
960
|
// Save config immediately
|
|
962
961
|
saveConfig({ apiKey, userEmail, apiUrl: API_URL });
|
|
963
|
-
|
|
964
|
-
|
|
962
|
+
setActiveContext({
|
|
963
|
+
workspaceId: selectedWorkspaceIdFromSignup,
|
|
964
|
+
projectId: selectedProjectIdFromSignup,
|
|
965
|
+
});
|
|
965
966
|
|
|
966
967
|
p.log.success("Workspace and board created");
|
|
967
968
|
} catch (error) {
|
|
@@ -1563,8 +1564,22 @@ export async function runSetup(options: SetupOptions = {}): Promise<void> {
|
|
|
1563
1564
|
` ${colors.success("\u2713")} ${colors.dim(formatPath(getLocalConfigPath(cwd), home))} ${colors.dim("(created)")}`,
|
|
1564
1565
|
);
|
|
1565
1566
|
|
|
1566
|
-
|
|
1567
|
-
|
|
1567
|
+
// Mirror the choice into the GLOBAL default, explicitly (#893). The local
|
|
1568
|
+
// file above already pins this directory; this second write is what gives a
|
|
1569
|
+
// server started elsewhere — Claude Desktop, another repo — a context at
|
|
1570
|
+
// all. It must say `global: true`: without it the write would find the file
|
|
1571
|
+
// just created one line up and land back in it, and the global default
|
|
1572
|
+
// would stay empty. One pair either way — a project is never set without
|
|
1573
|
+
// its workspace.
|
|
1574
|
+
if (selectedWorkspaceId || selectedProjectId) {
|
|
1575
|
+
setActiveContext(
|
|
1576
|
+
{
|
|
1577
|
+
workspaceId: selectedWorkspaceId ?? null,
|
|
1578
|
+
projectId: selectedProjectId ?? null,
|
|
1579
|
+
},
|
|
1580
|
+
{ global: true },
|
|
1581
|
+
);
|
|
1582
|
+
}
|
|
1568
1583
|
}
|
|
1569
1584
|
|
|
1570
1585
|
// Step 10: Show completion message
|