@codazen/harmonica-mcp 4.4.1 → 5.0.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/index.js +1501 -1196
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -21584,12 +21584,12 @@ async function assertMovementExists(client, movementId) {
|
|
|
21584
21584
|
throw new Error(`Movement not found: "${movementId}"`);
|
|
21585
21585
|
}
|
|
21586
21586
|
}
|
|
21587
|
-
async function
|
|
21588
|
-
const
|
|
21589
|
-
if (!
|
|
21590
|
-
throw new Error(`
|
|
21587
|
+
async function fetchTrack(client, trackId) {
|
|
21588
|
+
const track = await client.getTrack(trackId);
|
|
21589
|
+
if (!track) {
|
|
21590
|
+
throw new Error(`Track not found: "${trackId}"`);
|
|
21591
21591
|
}
|
|
21592
|
-
return
|
|
21592
|
+
return track;
|
|
21593
21593
|
}
|
|
21594
21594
|
async function fetchNotebook(client, notebookId) {
|
|
21595
21595
|
const notebook = await client.getNotebook(notebookId);
|
|
@@ -21913,7 +21913,7 @@ function registerActivityTools(server, ctx, client) {
|
|
|
21913
21913
|
minImportance: importance
|
|
21914
21914
|
});
|
|
21915
21915
|
if (entries.length === 0) {
|
|
21916
|
-
return { content: [{ type: "text", text: "No activities found for this
|
|
21916
|
+
return { content: [{ type: "text", text: "No activities found for this system." }] };
|
|
21917
21917
|
}
|
|
21918
21918
|
const lines = [
|
|
21919
21919
|
`# Project Activities (${entries.length}${hasMore ? "+" : ""})`,
|
|
@@ -22416,6 +22416,54 @@ var CHECK_MAX_SCORES = {
|
|
|
22416
22416
|
root_coda_quality: 5
|
|
22417
22417
|
};
|
|
22418
22418
|
|
|
22419
|
+
// ../../libs/harmonica-services/src/mcp/errors.ts
|
|
22420
|
+
var BeatQualityGateError = class extends Error {
|
|
22421
|
+
code = "beat_quality_gate_failed";
|
|
22422
|
+
failure;
|
|
22423
|
+
constructor(failure) {
|
|
22424
|
+
super(failure.message);
|
|
22425
|
+
this.failure = failure;
|
|
22426
|
+
}
|
|
22427
|
+
};
|
|
22428
|
+
var PlanVersionsIdempotencyError = class extends Error {
|
|
22429
|
+
code = "plan_versions_already_exist";
|
|
22430
|
+
existingBeatVersionIds;
|
|
22431
|
+
constructor(existingBeatVersionIds) {
|
|
22432
|
+
super(
|
|
22433
|
+
`Beat already has ${existingBeatVersionIds.length} planned Beat Version(s). Pass force=true to plan again.`
|
|
22434
|
+
);
|
|
22435
|
+
this.existingBeatVersionIds = existingBeatVersionIds;
|
|
22436
|
+
}
|
|
22437
|
+
};
|
|
22438
|
+
function isRenderableGateFailure(value) {
|
|
22439
|
+
if (typeof value !== "object" || value === null) return false;
|
|
22440
|
+
const f = value;
|
|
22441
|
+
return typeof f.reason === "string" && typeof f.message === "string";
|
|
22442
|
+
}
|
|
22443
|
+
var CheckResultPendingError = class extends Error {
|
|
22444
|
+
code = "check_result_pending";
|
|
22445
|
+
taskId;
|
|
22446
|
+
constructor(taskId) {
|
|
22447
|
+
super(`Task ${taskId} completed but its scorecard is not yet readable on the blocking path.`);
|
|
22448
|
+
this.name = "CheckResultPendingError";
|
|
22449
|
+
this.taskId = taskId;
|
|
22450
|
+
}
|
|
22451
|
+
};
|
|
22452
|
+
function renderCheckResultPending(err, alsoTry = ["list_checks"]) {
|
|
22453
|
+
const paths = [`get_job_status (jobId="${err.taskId}")`, ...alsoTry].join(" or ");
|
|
22454
|
+
return {
|
|
22455
|
+
content: [{
|
|
22456
|
+
type: "text",
|
|
22457
|
+
text: [
|
|
22458
|
+
`**Status:** completed \u2014 scorecard not yet readable on the blocking path`,
|
|
22459
|
+
`**Job ID:** ${err.taskId}`,
|
|
22460
|
+
"",
|
|
22461
|
+
`The check succeeded on the async path. Retrieve the scorecard with ${paths}.`
|
|
22462
|
+
].join("\n")
|
|
22463
|
+
}]
|
|
22464
|
+
};
|
|
22465
|
+
}
|
|
22466
|
+
|
|
22419
22467
|
// ../../libs/harmonica-services/src/mcp/tools/beat-quality-tools.ts
|
|
22420
22468
|
var POLL_INTERVAL_MS = 4e3;
|
|
22421
22469
|
var POLL_TIMEOUT_MS = 18e4;
|
|
@@ -22438,7 +22486,7 @@ async function pollForCheck(client, taskId) {
|
|
|
22438
22486
|
consecutiveErrors = 0;
|
|
22439
22487
|
if (!task) throw new Error(`Task ${taskId} not found`);
|
|
22440
22488
|
if (task.status === "completed") {
|
|
22441
|
-
if (!task.result) throw new
|
|
22489
|
+
if (!task.result) throw new CheckResultPendingError(taskId);
|
|
22442
22490
|
return task.result;
|
|
22443
22491
|
}
|
|
22444
22492
|
if (task.status === "failed") throw new Error(`Quality check failed: ${task.error ?? "unknown error"}`);
|
|
@@ -22486,6 +22534,7 @@ Use get_job_status with jobId="${taskId}" to poll for results, or list_checks to
|
|
|
22486
22534
|
const check2 = await pollForCheck(client, taskId);
|
|
22487
22535
|
return { content: [{ type: "text", text: formatScorecard(check2) }] };
|
|
22488
22536
|
} catch (err) {
|
|
22537
|
+
if (err instanceof CheckResultPendingError) return renderCheckResultPending(err);
|
|
22489
22538
|
const message = err instanceof Error ? err.message : String(err);
|
|
22490
22539
|
return { content: [{ type: "text", text: `Quality check failed: ${message}` }], isError: true };
|
|
22491
22540
|
}
|
|
@@ -23094,7 +23143,7 @@ function registerBeatTools(server, ctx, client) {
|
|
|
23094
23143
|
beatId: external_exports.string().optional().describe("Override beat ID (auto-generated if omitted)"),
|
|
23095
23144
|
humanAssignee: humanAssigneeSchema.optional().describe('Human assignee (e.g. { email: "lwu@codazen.com", name: "Liam Wu" })'),
|
|
23096
23145
|
agentAssignee: agentAssigneeSchema.optional().describe('Agent assignee (e.g. { agentId: "maya", name: "Maya" })'),
|
|
23097
|
-
branch: external_exports.string().optional().describe('Git branch for code assessment (falls back to
|
|
23146
|
+
branch: external_exports.string().optional().describe('Git branch for code assessment (falls back to System.repoDefaultBranch \u2192 "main")')
|
|
23098
23147
|
},
|
|
23099
23148
|
async ({ systemId, title, description, tags, priority, estimatedEffort, beatId, humanAssignee, agentAssignee, branch }) => {
|
|
23100
23149
|
try {
|
|
@@ -23302,31 +23351,6 @@ var BeatVersionRelocateError = class extends Error {
|
|
|
23302
23351
|
var REVISION_STATUSES = ["active", "archived"];
|
|
23303
23352
|
var BEAT_VERSION_STATUSES = ["active", "archived"];
|
|
23304
23353
|
|
|
23305
|
-
// ../../libs/harmonica-services/src/mcp/errors.ts
|
|
23306
|
-
var BeatQualityGateError = class extends Error {
|
|
23307
|
-
code = "beat_quality_gate_failed";
|
|
23308
|
-
failure;
|
|
23309
|
-
constructor(failure) {
|
|
23310
|
-
super(failure.message);
|
|
23311
|
-
this.failure = failure;
|
|
23312
|
-
}
|
|
23313
|
-
};
|
|
23314
|
-
var PlanVersionsIdempotencyError = class extends Error {
|
|
23315
|
-
code = "plan_versions_already_exist";
|
|
23316
|
-
existingBeatVersionIds;
|
|
23317
|
-
constructor(existingBeatVersionIds) {
|
|
23318
|
-
super(
|
|
23319
|
-
`Beat already has ${existingBeatVersionIds.length} planned Beat Version(s). Pass force=true to plan again.`
|
|
23320
|
-
);
|
|
23321
|
-
this.existingBeatVersionIds = existingBeatVersionIds;
|
|
23322
|
-
}
|
|
23323
|
-
};
|
|
23324
|
-
function isRenderableGateFailure(value) {
|
|
23325
|
-
if (typeof value !== "object" || value === null) return false;
|
|
23326
|
-
const f = value;
|
|
23327
|
-
return typeof f.reason === "string" && typeof f.message === "string";
|
|
23328
|
-
}
|
|
23329
|
-
|
|
23330
23354
|
// ../../libs/harmonica-services/src/mcp/tools/beat-version-tools.ts
|
|
23331
23355
|
function registerBeatVersionTools(server, ctx, client) {
|
|
23332
23356
|
server.tool(
|
|
@@ -24077,260 +24101,6 @@ function registerClaimTools(server, ctx, client) {
|
|
|
24077
24101
|
);
|
|
24078
24102
|
}
|
|
24079
24103
|
|
|
24080
|
-
// ../../libs/harmonica-services/src/mcp/tools/deliverable-group-tools.ts
|
|
24081
|
-
function registerDeliverableGroupTools(server, ctx, client) {
|
|
24082
|
-
server.tool(
|
|
24083
|
-
"list_deliverable_groups",
|
|
24084
|
-
"List all DeliverableGroups for a Teamspace. A DeliverableGroup is a scoped initiative \u2014 client SOW, internal phase, or engagement container \u2014 that organises Deliverables and Drops.",
|
|
24085
|
-
{
|
|
24086
|
-
teamspaceId: external_exports.string().describe("The teamspace ID")
|
|
24087
|
-
},
|
|
24088
|
-
async ({ teamspaceId }) => {
|
|
24089
|
-
const groups = await client.listTeamspaceDeliverableGroups(teamspaceId);
|
|
24090
|
-
const text = groups.length === 0 ? "No deliverable groups found." : groups.map(
|
|
24091
|
-
(g) => `[${g.deliverableGroupId}] ${g.groupCode} \u2014 ${g.title} (${g.status})`
|
|
24092
|
-
).join("\n");
|
|
24093
|
-
return { content: [{ type: "text", text }] };
|
|
24094
|
-
}
|
|
24095
|
-
);
|
|
24096
|
-
server.tool(
|
|
24097
|
-
"list_movement_deliverable_groups",
|
|
24098
|
-
`List the DeliverableGroups scoped to a Movement (client SOW). Returns the commitments grouped under that engagement. Recognition cues: "what's in this SOW", "this engagement's deliverable groups" \u2192 list_movement_deliverable_groups.`,
|
|
24099
|
-
{
|
|
24100
|
-
movementId: external_exports.string().describe("The Movement ID (e.g., mv-abc123)")
|
|
24101
|
-
},
|
|
24102
|
-
async ({ movementId }) => {
|
|
24103
|
-
await assertMovementExists(client, movementId);
|
|
24104
|
-
const groups = await client.listMovementDeliverableGroups(movementId);
|
|
24105
|
-
const text = groups.length === 0 ? "No deliverable groups found for this Movement." : groups.map(
|
|
24106
|
-
(g) => `[${g.deliverableGroupId}] ${g.groupCode} \u2014 ${g.title} (${g.status})`
|
|
24107
|
-
).join("\n");
|
|
24108
|
-
return { content: [{ type: "text", text }] };
|
|
24109
|
-
}
|
|
24110
|
-
);
|
|
24111
|
-
server.tool(
|
|
24112
|
-
"get_deliverable_group",
|
|
24113
|
-
"Get a DeliverableGroup by ID. Returns full details including commercial lifecycle status, VU rate, and audit stamps.",
|
|
24114
|
-
{
|
|
24115
|
-
deliverableGroupId: external_exports.string().describe("The DeliverableGroup ID")
|
|
24116
|
-
},
|
|
24117
|
-
async ({ deliverableGroupId }) => {
|
|
24118
|
-
const g = await client.getDeliverableGroup(deliverableGroupId);
|
|
24119
|
-
if (!g) {
|
|
24120
|
-
return {
|
|
24121
|
-
content: [{ type: "text", text: `DeliverableGroup not found: ${deliverableGroupId}` }]
|
|
24122
|
-
};
|
|
24123
|
-
}
|
|
24124
|
-
const lines = [
|
|
24125
|
-
`ID: ${g.deliverableGroupId}`,
|
|
24126
|
-
`Code: ${g.groupCode}`,
|
|
24127
|
-
`Title: ${g.title}`,
|
|
24128
|
-
`Teamspace: ${g.teamspaceId}`,
|
|
24129
|
-
`Status: ${g.status}`,
|
|
24130
|
-
`VU Rate: ${g.vuRate}`,
|
|
24131
|
-
g.description ? `Description: ${g.description}` : null,
|
|
24132
|
-
g.deliverables ? `Deliverables:
|
|
24133
|
-
${g.deliverables}` : null,
|
|
24134
|
-
g.acceptanceCriteria ? `Acceptance Criteria:
|
|
24135
|
-
${g.acceptanceCriteria}` : null,
|
|
24136
|
-
g.parentGroupId ? `Parent Group: ${g.parentGroupId}` : null,
|
|
24137
|
-
g.proposedAt ? `Proposed: ${g.proposedAt}` : null,
|
|
24138
|
-
g.approvedAt ? `Approved: ${g.approvedAt}` : null,
|
|
24139
|
-
`Version: ${g.version}`,
|
|
24140
|
-
`Created: ${g.createdAt}`,
|
|
24141
|
-
`Updated: ${g.updatedAt}`
|
|
24142
|
-
].filter(Boolean).join("\n");
|
|
24143
|
-
return { content: [{ type: "text", text: lines }] };
|
|
24144
|
-
}
|
|
24145
|
-
);
|
|
24146
|
-
const createTrackSchema = {
|
|
24147
|
-
teamspaceId: external_exports.string().describe("The teamspace ID"),
|
|
24148
|
-
title: external_exports.string().min(1).max(300).describe('Track title (e.g. "SOW-2026-Q2 / Platform Modernisation")'),
|
|
24149
|
-
description: external_exports.string().max(2e3).optional().describe("Optional description"),
|
|
24150
|
-
deliverables: external_exports.string().max(2e4).optional().describe("The Track's deliverables as Markdown bullet points (parallel to acceptanceCriteria)"),
|
|
24151
|
-
acceptanceCriteria: external_exports.string().max(2e4).optional().describe("The Track's acceptance criteria (Markdown), parallel to its deliverables"),
|
|
24152
|
-
vuRate: external_exports.number().nonnegative().describe("Value-Unit rate (billing rate for this engagement)"),
|
|
24153
|
-
systemId: external_exports.string().optional().describe("Optional project to scope this Track to"),
|
|
24154
|
-
movementId: external_exports.string().optional().describe("Optional Movement (client SOW) to scope this Track to"),
|
|
24155
|
-
parentGroupId: external_exports.string().optional().describe("Optional parent Track ID for nested grouping (Phase 0 scaffolding)")
|
|
24156
|
-
};
|
|
24157
|
-
const createTrackHandler = async ({ teamspaceId, title, description, deliverables, acceptanceCriteria, vuRate, systemId, movementId, parentGroupId }) => {
|
|
24158
|
-
const teamspace = await fetchTeamspace(client, teamspaceId);
|
|
24159
|
-
if (movementId !== void 0) {
|
|
24160
|
-
await assertMovementExists(client, movementId);
|
|
24161
|
-
}
|
|
24162
|
-
if (parentGroupId !== void 0) {
|
|
24163
|
-
await fetchDeliverableGroup(client, parentGroupId);
|
|
24164
|
-
}
|
|
24165
|
-
const g = await client.createDeliverableGroup({
|
|
24166
|
-
teamspaceId,
|
|
24167
|
-
orgId: teamspace.orgId,
|
|
24168
|
-
title,
|
|
24169
|
-
...description !== void 0 && { description },
|
|
24170
|
-
...deliverables !== void 0 && { deliverables },
|
|
24171
|
-
...acceptanceCriteria !== void 0 && { acceptanceCriteria },
|
|
24172
|
-
vuRate,
|
|
24173
|
-
...systemId !== void 0 && { systemId },
|
|
24174
|
-
...movementId !== void 0 && { movementId },
|
|
24175
|
-
...parentGroupId !== void 0 && { parentGroupId },
|
|
24176
|
-
createdBy: ctx.user.userId
|
|
24177
|
-
});
|
|
24178
|
-
return {
|
|
24179
|
-
content: [
|
|
24180
|
-
{
|
|
24181
|
-
type: "text",
|
|
24182
|
-
text: `Created Track: [${g.deliverableGroupId}] ${g.groupCode} \u2014 ${g.title} (${g.status})`
|
|
24183
|
-
}
|
|
24184
|
-
]
|
|
24185
|
-
};
|
|
24186
|
-
};
|
|
24187
|
-
server.tool(
|
|
24188
|
-
"create_track",
|
|
24189
|
-
'Create a Track (a DeliverableGroup) in a Teamspace. Use for scoped initiatives \u2014 client SOWs, internal phases, engagement containers \u2014 that collect Deliverables and Drops. Do NOT use for software capabilities the product provides to end users (those are Beats). Recognition cues: "this engagement", "this SOW", "this phase of work", "the client project", "this track" \u2192 Track.',
|
|
24190
|
-
createTrackSchema,
|
|
24191
|
-
createTrackHandler
|
|
24192
|
-
);
|
|
24193
|
-
server.tool(
|
|
24194
|
-
"create_deliverable_group",
|
|
24195
|
-
"DEPRECATED alias for create_track \u2014 use create_track instead. Creates a Track (DeliverableGroup) in a Teamspace.",
|
|
24196
|
-
createTrackSchema,
|
|
24197
|
-
createTrackHandler
|
|
24198
|
-
);
|
|
24199
|
-
server.tool(
|
|
24200
|
-
"update_deliverable_group",
|
|
24201
|
-
"Update mutable content fields on a DeliverableGroup / Track (title, description, deliverables, acceptanceCriteria, vuRate). To change commercial status use transition_deliverable_group instead.",
|
|
24202
|
-
{
|
|
24203
|
-
deliverableGroupId: external_exports.string().describe("The DeliverableGroup ID"),
|
|
24204
|
-
title: external_exports.string().min(1).max(300).optional().describe("New title"),
|
|
24205
|
-
description: external_exports.string().max(2e3).optional().describe("New description"),
|
|
24206
|
-
deliverables: external_exports.string().max(2e4).optional().describe(`Replace the Track's deliverables Markdown ("" clears)`),
|
|
24207
|
-
acceptanceCriteria: external_exports.string().max(2e4).optional().describe(`Replace the Track's acceptance-criteria Markdown ("" clears)`),
|
|
24208
|
-
vuRate: external_exports.number().nonnegative().optional().describe("New VU rate")
|
|
24209
|
-
},
|
|
24210
|
-
async ({ deliverableGroupId, title, description, deliverables, acceptanceCriteria, vuRate }) => {
|
|
24211
|
-
const g = await client.updateDeliverableGroup(deliverableGroupId, {
|
|
24212
|
-
...title !== void 0 && { title },
|
|
24213
|
-
...description !== void 0 && { description },
|
|
24214
|
-
...deliverables !== void 0 && { deliverables },
|
|
24215
|
-
...acceptanceCriteria !== void 0 && { acceptanceCriteria },
|
|
24216
|
-
...vuRate !== void 0 && { vuRate }
|
|
24217
|
-
});
|
|
24218
|
-
if (!g) {
|
|
24219
|
-
return {
|
|
24220
|
-
content: [{ type: "text", text: `DeliverableGroup not found: ${deliverableGroupId}` }]
|
|
24221
|
-
};
|
|
24222
|
-
}
|
|
24223
|
-
return {
|
|
24224
|
-
content: [
|
|
24225
|
-
{
|
|
24226
|
-
type: "text",
|
|
24227
|
-
text: `Updated DeliverableGroup: [${g.deliverableGroupId}] ${g.groupCode} \u2014 ${g.title} (${g.status})`
|
|
24228
|
-
}
|
|
24229
|
-
]
|
|
24230
|
-
};
|
|
24231
|
-
}
|
|
24232
|
-
);
|
|
24233
|
-
server.tool(
|
|
24234
|
-
"transition_deliverable_group",
|
|
24235
|
-
'Transition a DeliverableGroup through its commercial lifecycle: draft \u2192 proposed \u2192 approved \u2192 active \u2192 fulfilled. Cancelled is reachable from any non-terminal state. Advancing to "approved" requires an approvalNoteId. Use confirm=false (default) to preview the transition without writing; confirm=true to execute.',
|
|
24236
|
-
{
|
|
24237
|
-
deliverableGroupId: external_exports.string().describe("The DeliverableGroup ID"),
|
|
24238
|
-
targetState: external_exports.enum(["proposed", "approved", "active", "fulfilled", "cancelled"]).describe("Target lifecycle state"),
|
|
24239
|
-
approvalNoteId: external_exports.string().optional().describe('Required when transitioning to "approved" \u2014 the Note ID capturing the approval decision'),
|
|
24240
|
-
reason: external_exports.string().optional().describe("Reason for the transition (especially useful for cancellation)"),
|
|
24241
|
-
confirm: external_exports.boolean().optional().default(false).describe("false (default) = dry-run preview; true = execute the transition")
|
|
24242
|
-
},
|
|
24243
|
-
async ({ deliverableGroupId, targetState, approvalNoteId, reason, confirm }) => {
|
|
24244
|
-
if (!confirm) {
|
|
24245
|
-
return {
|
|
24246
|
-
content: [
|
|
24247
|
-
{
|
|
24248
|
-
type: "text",
|
|
24249
|
-
text: `Dry-run: would transition DeliverableGroup ${deliverableGroupId} \u2192 ${targetState}${approvalNoteId ? ` (approvalNoteId: ${approvalNoteId})` : ""}${reason ? ` \u2014 ${reason}` : ""}. Call again with confirm=true to execute.`
|
|
24250
|
-
}
|
|
24251
|
-
]
|
|
24252
|
-
};
|
|
24253
|
-
}
|
|
24254
|
-
const g = await client.transitionDeliverableGroupStatus(
|
|
24255
|
-
deliverableGroupId,
|
|
24256
|
-
targetState,
|
|
24257
|
-
{
|
|
24258
|
-
...approvalNoteId !== void 0 && { approvalNoteId },
|
|
24259
|
-
...reason !== void 0 && { reason }
|
|
24260
|
-
}
|
|
24261
|
-
);
|
|
24262
|
-
if (!g) {
|
|
24263
|
-
return {
|
|
24264
|
-
content: [{ type: "text", text: `DeliverableGroup not found: ${deliverableGroupId}` }]
|
|
24265
|
-
};
|
|
24266
|
-
}
|
|
24267
|
-
return {
|
|
24268
|
-
content: [
|
|
24269
|
-
{
|
|
24270
|
-
type: "text",
|
|
24271
|
-
text: `Transitioned DeliverableGroup [${g.deliverableGroupId}] ${g.groupCode} \u2192 ${g.status}`
|
|
24272
|
-
}
|
|
24273
|
-
]
|
|
24274
|
-
};
|
|
24275
|
-
}
|
|
24276
|
-
);
|
|
24277
|
-
server.tool(
|
|
24278
|
-
"list_deliverable_group_drops",
|
|
24279
|
-
'List all Drops linked to a DeliverableGroup. Use to answer "what releases are part of this phase?" or "which software drops are tied to this engagement milestone?"',
|
|
24280
|
-
{
|
|
24281
|
-
deliverableGroupId: external_exports.string().describe("The DeliverableGroup ID")
|
|
24282
|
-
},
|
|
24283
|
-
async ({ deliverableGroupId }) => {
|
|
24284
|
-
const items = await client.listDeliverableGroupDrops(deliverableGroupId);
|
|
24285
|
-
const text = items.length === 0 ? "No drops linked to this group." : items.map((d) => `[${d.dropId}] account: ${d.accountId}`).join("\n");
|
|
24286
|
-
return { content: [{ type: "text", text }] };
|
|
24287
|
-
}
|
|
24288
|
-
);
|
|
24289
|
-
server.tool(
|
|
24290
|
-
"assign_drop_to_deliverable_group",
|
|
24291
|
-
`Link a Drop to a DeliverableGroup so it appears on the parent Movement's timed-release axis (the Drops shown on the Movement overview). A Drop belongs to at most one group; linking moves it if it was in another group. Use to answer "put this release under this SOW/phase" or "add a Drop to this Movement".`,
|
|
24292
|
-
{
|
|
24293
|
-
deliverableGroupId: external_exports.string().describe("The DeliverableGroup ID"),
|
|
24294
|
-
dropId: external_exports.string().describe("The Drop ID to link to the group")
|
|
24295
|
-
},
|
|
24296
|
-
async ({ deliverableGroupId, dropId }) => {
|
|
24297
|
-
await fetchDeliverableGroup(client, deliverableGroupId);
|
|
24298
|
-
const drop = await client.getDrop(dropId);
|
|
24299
|
-
if (!drop) {
|
|
24300
|
-
return { content: [{ type: "text", text: `Drop not found: ${dropId}` }] };
|
|
24301
|
-
}
|
|
24302
|
-
await assertAccountExists(client, drop.accountId);
|
|
24303
|
-
const ok = await client.setDropDeliverableGroup(deliverableGroupId, dropId);
|
|
24304
|
-
if (!ok) {
|
|
24305
|
-
return { content: [{ type: "text", text: `Drop not found: ${dropId}` }] };
|
|
24306
|
-
}
|
|
24307
|
-
return {
|
|
24308
|
-
content: [{ type: "text", text: `Linked Drop ${dropId} to DeliverableGroup ${deliverableGroupId}.` }]
|
|
24309
|
-
};
|
|
24310
|
-
}
|
|
24311
|
-
);
|
|
24312
|
-
server.tool(
|
|
24313
|
-
"list_deliverable_group_beat_versions",
|
|
24314
|
-
'List the Beat Versions linked to a DeliverableGroup via its work items (Work Plan). Use to answer "what feature increments are scoped under this SOW/phase?"',
|
|
24315
|
-
{
|
|
24316
|
-
deliverableGroupId: external_exports.string().describe("The DeliverableGroup ID")
|
|
24317
|
-
},
|
|
24318
|
-
async ({ deliverableGroupId }) => {
|
|
24319
|
-
await fetchDeliverableGroup(client, deliverableGroupId);
|
|
24320
|
-
const workItems = await client.listTrackWorkItems(deliverableGroupId);
|
|
24321
|
-
const bvIds = [...new Set(
|
|
24322
|
-
workItems.map((wi) => wi.beatVersionId).filter((id) => id !== void 0)
|
|
24323
|
-
)];
|
|
24324
|
-
if (bvIds.length === 0) {
|
|
24325
|
-
return { content: [{ type: "text", text: "No beat versions linked via work items in this group." }] };
|
|
24326
|
-
}
|
|
24327
|
-
const hydrated = await Promise.all(bvIds.map((id) => client.getBeatVersion(id)));
|
|
24328
|
-
const text = hydrated.filter((bv) => bv !== void 0).map((bv) => `[${bv.beatVersionId}] v${bv.versionNumber} \u2014 ${bv.title} (${bv.status})`).join("\n");
|
|
24329
|
-
return { content: [{ type: "text", text }] };
|
|
24330
|
-
}
|
|
24331
|
-
);
|
|
24332
|
-
}
|
|
24333
|
-
|
|
24334
24104
|
// ../../libs/harmonica-services/src/mcp/tools/downbeat-harmony-report-tools.ts
|
|
24335
24105
|
function formatQualifyingBeat(b) {
|
|
24336
24106
|
return `- \`${b.beatId}\` ${b.title}: ${b.signals.join(", ")}`;
|
|
@@ -24464,7 +24234,7 @@ async function pollForCheck2(client, taskId) {
|
|
|
24464
24234
|
consecutiveErrors = 0;
|
|
24465
24235
|
if (!task) throw new Error(`Task ${taskId} not found`);
|
|
24466
24236
|
if (task.status === "completed") {
|
|
24467
|
-
if (!task.result) throw new
|
|
24237
|
+
if (!task.result) throw new CheckResultPendingError(taskId);
|
|
24468
24238
|
return task.result;
|
|
24469
24239
|
}
|
|
24470
24240
|
if (task.status === "failed") throw new Error(`Drop quality check failed: ${task.error ?? "unknown error"}`);
|
|
@@ -24520,7 +24290,7 @@ function registerDropQualityTools(server, ctx, client) {
|
|
|
24520
24290
|
`Drop quality check enqueued for ${dropId}.`,
|
|
24521
24291
|
`Task ID: ${taskId}`,
|
|
24522
24292
|
"",
|
|
24523
|
-
`Use get_job_status with jobId="${taskId}" to
|
|
24293
|
+
`Use get_job_status with jobId="${taskId}" to view results \u2014 list_checks has no dropId filter.`
|
|
24524
24294
|
].join("\n")
|
|
24525
24295
|
}]
|
|
24526
24296
|
};
|
|
@@ -24528,6 +24298,7 @@ function registerDropQualityTools(server, ctx, client) {
|
|
|
24528
24298
|
const check2 = await pollForCheck2(client, taskId);
|
|
24529
24299
|
return { content: [{ type: "text", text: formatReport(check2) }] };
|
|
24530
24300
|
} catch (err) {
|
|
24301
|
+
if (err instanceof CheckResultPendingError) return renderCheckResultPending(err, []);
|
|
24531
24302
|
const message = err instanceof Error ? err.message : String(err);
|
|
24532
24303
|
return { content: [{ type: "text", text: `Drop quality check failed: ${message}` }], isError: true };
|
|
24533
24304
|
}
|
|
@@ -24914,8 +24685,8 @@ function registerEmbeddingTools(server, ctx, client) {
|
|
|
24914
24685
|
"search",
|
|
24915
24686
|
"Free-text semantic search across system, teamspace, or org scope. Provide exactly one of systemId / teamspaceId / orgId. Teamspace and org searches fan out across child projects and merge by score. Cross-project results carry the source project name.",
|
|
24916
24687
|
{
|
|
24917
|
-
systemId: external_exports.string().optional().describe("Search within a single
|
|
24918
|
-
teamspaceId: external_exports.string().optional().describe("Search across all
|
|
24688
|
+
systemId: external_exports.string().optional().describe("Search within a single System"),
|
|
24689
|
+
teamspaceId: external_exports.string().optional().describe("Search across all Systems in a teamspace"),
|
|
24919
24690
|
orgId: external_exports.string().optional().describe("Search across every system in this organization \u2014 see list_organizations"),
|
|
24920
24691
|
query: external_exports.string().describe("Natural language search query"),
|
|
24921
24692
|
limit: external_exports.coerce.number().int().min(1).max(50).optional().default(10).describe("Max results to return. Default 10"),
|
|
@@ -25075,782 +24846,6 @@ ${members}${parent}`;
|
|
|
25075
24846
|
);
|
|
25076
24847
|
}
|
|
25077
24848
|
|
|
25078
|
-
// ../../libs/harmonica-services/src/mcp/tools/feature-flag-tools.ts
|
|
25079
|
-
var STAGE_ENUM = external_exports.enum([
|
|
25080
|
-
"local",
|
|
25081
|
-
"staging",
|
|
25082
|
-
"staging-alpha",
|
|
25083
|
-
"staging-beta",
|
|
25084
|
-
"alpha",
|
|
25085
|
-
"beta",
|
|
25086
|
-
"prod"
|
|
25087
|
-
]);
|
|
25088
|
-
function registerFeatureFlagTools(server, ctx, client) {
|
|
25089
|
-
server.tool(
|
|
25090
|
-
"list_flags",
|
|
25091
|
-
"List feature flags and their enabled stages. Use check_flag to test a single flag against a specific stage, or get_flag for full detail on one flag.",
|
|
25092
|
-
{
|
|
25093
|
-
includeArchived: external_exports.boolean().optional().describe("Include archived flags. Default false"),
|
|
25094
|
-
search: external_exports.string().optional().describe("Case-insensitive search against flag name and description"),
|
|
25095
|
-
cursor: external_exports.string().optional().describe("Pagination cursor from a prior list_flags result"),
|
|
25096
|
-
limit: external_exports.number().int().min(1).max(100).optional().describe("Flags per page. Default 20")
|
|
25097
|
-
},
|
|
25098
|
-
async ({ includeArchived, search, cursor, limit }) => {
|
|
25099
|
-
try {
|
|
25100
|
-
const page = await client.listFlags({ includeArchived, search, cursor, limit });
|
|
25101
|
-
if (page.flags.length === 0) {
|
|
25102
|
-
return { content: [{ type: "text", text: "No flags found." }] };
|
|
25103
|
-
}
|
|
25104
|
-
return { content: [{ type: "text", text: formatFlagList(page.flags, page.total, page.nextCursor) }] };
|
|
25105
|
-
} catch (err) {
|
|
25106
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
25107
|
-
return { content: [{ type: "text", text: `Failed to list flags: ${message}` }], isError: true };
|
|
25108
|
-
}
|
|
25109
|
-
}
|
|
25110
|
-
);
|
|
25111
|
-
server.tool(
|
|
25112
|
-
"get_flag",
|
|
25113
|
-
"Get full detail for a single feature flag by name, including its enabled stages and optional change history.",
|
|
25114
|
-
{
|
|
25115
|
-
name: external_exports.string().describe("Flag name"),
|
|
25116
|
-
includeHistory: external_exports.boolean().optional().describe("Include change history entries. Default false")
|
|
25117
|
-
},
|
|
25118
|
-
async ({ name, includeHistory }) => {
|
|
25119
|
-
try {
|
|
25120
|
-
const flag = await client.getFlag(name, { includeHistory });
|
|
25121
|
-
if (!flag) {
|
|
25122
|
-
return { content: [{ type: "text", text: `Flag "${name}" not found.` }] };
|
|
25123
|
-
}
|
|
25124
|
-
return { content: [{ type: "text", text: formatFlag(flag, flag.history) }] };
|
|
25125
|
-
} catch (err) {
|
|
25126
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
25127
|
-
return { content: [{ type: "text", text: `Failed to get flag: ${message}` }], isError: true };
|
|
25128
|
-
}
|
|
25129
|
-
}
|
|
25130
|
-
);
|
|
25131
|
-
server.tool(
|
|
25132
|
-
"check_flag",
|
|
25133
|
-
"Check whether a feature flag is enabled in a specific stage. Returns a simple enabled/disabled answer.",
|
|
25134
|
-
{
|
|
25135
|
-
name: external_exports.string().describe("Flag name"),
|
|
25136
|
-
stage: STAGE_ENUM.describe("Deployment stage to check against")
|
|
25137
|
-
},
|
|
25138
|
-
async ({ name, stage }) => {
|
|
25139
|
-
try {
|
|
25140
|
-
const enabled = await client.checkFlag(name, stage);
|
|
25141
|
-
const status = enabled ? "enabled" : "disabled";
|
|
25142
|
-
return {
|
|
25143
|
-
content: [{
|
|
25144
|
-
type: "text",
|
|
25145
|
-
text: `Flag "${name}" is **${status}** in stage "${stage}".`
|
|
25146
|
-
}]
|
|
25147
|
-
};
|
|
25148
|
-
} catch (err) {
|
|
25149
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
25150
|
-
return { content: [{ type: "text", text: `Failed to check flag: ${message}` }], isError: true };
|
|
25151
|
-
}
|
|
25152
|
-
}
|
|
25153
|
-
);
|
|
25154
|
-
server.tool(
|
|
25155
|
-
"create_flag",
|
|
25156
|
-
"Create or update a feature flag with the specified enabled stages. Idempotent \u2014 calling with an existing flag name replaces its enabledIn list.",
|
|
25157
|
-
{
|
|
25158
|
-
name: external_exports.string().min(1).describe('Flag name (e.g. "rbac:enforcement")'),
|
|
25159
|
-
enabledIn: external_exports.array(STAGE_ENUM).describe("Stages to enable the flag in"),
|
|
25160
|
-
description: external_exports.string().optional().describe("Human-readable description of what this flag gates"),
|
|
25161
|
-
note: external_exports.string().optional().describe("Reason for this change (recorded in change history)")
|
|
25162
|
-
},
|
|
25163
|
-
async ({ name, enabledIn, description, note }) => {
|
|
25164
|
-
if (!ctx.user.email) {
|
|
25165
|
-
return { content: [{ type: "text", text: "Write operations require an authenticated user with a valid email." }], isError: true };
|
|
25166
|
-
}
|
|
25167
|
-
try {
|
|
25168
|
-
const flag = await client.createFlag(name, enabledIn, { description, note, updatedBy: ctx.user.email });
|
|
25169
|
-
return { content: [{ type: "text", text: formatFlag(flag) }] };
|
|
25170
|
-
} catch (err) {
|
|
25171
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
25172
|
-
return { content: [{ type: "text", text: `Failed to create flag: ${message}` }], isError: true };
|
|
25173
|
-
}
|
|
25174
|
-
}
|
|
25175
|
-
);
|
|
25176
|
-
server.tool(
|
|
25177
|
-
"set_flag",
|
|
25178
|
-
"Update which stages a feature flag is enabled in. Replaces the current enabledIn list entirely.",
|
|
25179
|
-
{
|
|
25180
|
-
name: external_exports.string().min(1).describe("Flag name"),
|
|
25181
|
-
enabledIn: external_exports.array(STAGE_ENUM).describe("New set of stages to enable the flag in. Pass [] to disable everywhere without archiving."),
|
|
25182
|
-
note: external_exports.string().optional().describe("Reason for this change (recorded in change history)")
|
|
25183
|
-
},
|
|
25184
|
-
async ({ name, enabledIn, note }) => {
|
|
25185
|
-
if (!ctx.user.email) {
|
|
25186
|
-
return { content: [{ type: "text", text: "Write operations require an authenticated user with a valid email." }], isError: true };
|
|
25187
|
-
}
|
|
25188
|
-
try {
|
|
25189
|
-
const flag = await client.setFlag(name, enabledIn, { note, updatedBy: ctx.user.email });
|
|
25190
|
-
return { content: [{ type: "text", text: formatFlag(flag) }] };
|
|
25191
|
-
} catch (err) {
|
|
25192
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
25193
|
-
return { content: [{ type: "text", text: `Failed to set flag: ${message}` }], isError: true };
|
|
25194
|
-
}
|
|
25195
|
-
}
|
|
25196
|
-
);
|
|
25197
|
-
server.tool(
|
|
25198
|
-
"archive_flag",
|
|
25199
|
-
"Archive a feature flag. Clears its enabledIn list and marks it archived so it cannot be re-enabled via set_flag. Use unarchive_flag to restore it.",
|
|
25200
|
-
{
|
|
25201
|
-
name: external_exports.string().min(1).describe("Flag name"),
|
|
25202
|
-
note: external_exports.string().optional().describe("Reason for archiving (recorded in change history)")
|
|
25203
|
-
},
|
|
25204
|
-
async ({ name, note }) => {
|
|
25205
|
-
if (!ctx.user.email) {
|
|
25206
|
-
return { content: [{ type: "text", text: "Write operations require an authenticated user with a valid email." }], isError: true };
|
|
25207
|
-
}
|
|
25208
|
-
try {
|
|
25209
|
-
const flag = await client.archiveFlag(name, { note, updatedBy: ctx.user.email });
|
|
25210
|
-
return { content: [{ type: "text", text: formatFlag(flag) }] };
|
|
25211
|
-
} catch (err) {
|
|
25212
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
25213
|
-
return { content: [{ type: "text", text: `Failed to archive flag: ${message}` }], isError: true };
|
|
25214
|
-
}
|
|
25215
|
-
}
|
|
25216
|
-
);
|
|
25217
|
-
server.tool(
|
|
25218
|
-
"unarchive_flag",
|
|
25219
|
-
"Restore an archived feature flag to operable state. The flag is re-enabled with an empty enabledIn list \u2014 use set_flag afterward to enable it in specific stages.",
|
|
25220
|
-
{
|
|
25221
|
-
name: external_exports.string().min(1).describe("Flag name"),
|
|
25222
|
-
note: external_exports.string().optional().describe("Reason for restoring (recorded in change history)")
|
|
25223
|
-
},
|
|
25224
|
-
async ({ name, note }) => {
|
|
25225
|
-
if (!ctx.user.email) {
|
|
25226
|
-
return { content: [{ type: "text", text: "Write operations require an authenticated user with a valid email." }], isError: true };
|
|
25227
|
-
}
|
|
25228
|
-
try {
|
|
25229
|
-
const flag = await client.unarchiveFlag(name, { note, updatedBy: ctx.user.email });
|
|
25230
|
-
return { content: [{ type: "text", text: formatFlag(flag) }] };
|
|
25231
|
-
} catch (err) {
|
|
25232
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
25233
|
-
return { content: [{ type: "text", text: `Failed to unarchive flag: ${message}` }], isError: true };
|
|
25234
|
-
}
|
|
25235
|
-
}
|
|
25236
|
-
);
|
|
25237
|
-
}
|
|
25238
|
-
function formatFlag(flag, history) {
|
|
25239
|
-
const archived = flag.archivedAt ? " **(archived)**" : "";
|
|
25240
|
-
const enabled = flag.enabledIn.length > 0 ? flag.enabledIn.join(", ") : "(none)";
|
|
25241
|
-
const parts = [
|
|
25242
|
-
`# Flag: ${flag.name}${archived}`,
|
|
25243
|
-
`**Scope:** ${flag.scope}`,
|
|
25244
|
-
`**Enabled in:** ${enabled}`,
|
|
25245
|
-
flag.description ? `**Description:** ${flag.description}` : null,
|
|
25246
|
-
`**Last updated:** ${flag.updatedAt.slice(0, 10)} by ${flag.updatedBy}`,
|
|
25247
|
-
flag.archivedAt ? `**Archived:** ${flag.archivedAt.slice(0, 10)} by ${flag.archivedBy}` : null
|
|
25248
|
-
].filter(Boolean).join("\n");
|
|
25249
|
-
if (!history?.length) return parts;
|
|
25250
|
-
const historyLines = history.map((h) => {
|
|
25251
|
-
const prev = h.previousEnabledIn.length > 0 ? h.previousEnabledIn.join(", ") : "(none)";
|
|
25252
|
-
const next = h.newEnabledIn.length > 0 ? h.newEnabledIn.join(", ") : "(none)";
|
|
25253
|
-
const note = h.note ? ` \u2014 ${h.note}` : "";
|
|
25254
|
-
return `- ${h.updatedAt.slice(0, 10)} ${h.updatedBy}: ${prev} \u2192 ${next}${note}`;
|
|
25255
|
-
}).join("\n");
|
|
25256
|
-
return `${parts}
|
|
25257
|
-
|
|
25258
|
-
## Change History
|
|
25259
|
-
${historyLines}`;
|
|
25260
|
-
}
|
|
25261
|
-
function formatFlagList(flags, total, nextCursor) {
|
|
25262
|
-
const rows = flags.map((f) => {
|
|
25263
|
-
const enabled = f.enabledIn.length > 0 ? f.enabledIn.join(", ") : "(none)";
|
|
25264
|
-
const archived = f.archivedAt ? " *(archived)*" : "";
|
|
25265
|
-
return `| ${f.name}${archived} | ${enabled} | ${f.updatedAt.slice(0, 10)} |`;
|
|
25266
|
-
});
|
|
25267
|
-
const table = [
|
|
25268
|
-
`# Feature Flags (${total} total, showing ${flags.length})`,
|
|
25269
|
-
"",
|
|
25270
|
-
"| Name | Enabled In | Updated |",
|
|
25271
|
-
"|---|---|---|",
|
|
25272
|
-
...rows
|
|
25273
|
-
].join("\n");
|
|
25274
|
-
if (!nextCursor) return table;
|
|
25275
|
-
return `${table}
|
|
25276
|
-
|
|
25277
|
-
More flags available. Pass \`cursor: "${nextCursor}"\` to list_flags to continue.`;
|
|
25278
|
-
}
|
|
25279
|
-
|
|
25280
|
-
// ../../libs/harmonica-services/src/mcp/tools/layer-tools.ts
|
|
25281
|
-
function registerLayerTools(server, ctx, client) {
|
|
25282
|
-
server.tool(
|
|
25283
|
-
"list_system_layers",
|
|
25284
|
-
"List the architectural Layers declared on a System (e.g. frontend, backend, data). Layers are an evaluation lens for plan/build quality checks, not a work container.",
|
|
25285
|
-
{
|
|
25286
|
-
systemId: external_exports.string().describe("The System ID")
|
|
25287
|
-
},
|
|
25288
|
-
async ({ systemId }) => {
|
|
25289
|
-
try {
|
|
25290
|
-
await assertSystemExists(client, systemId);
|
|
25291
|
-
} catch (err) {
|
|
25292
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
25293
|
-
return { content: [{ type: "text", text: `Access denied: ${message}` }], isError: true };
|
|
25294
|
-
}
|
|
25295
|
-
let layers;
|
|
25296
|
-
try {
|
|
25297
|
-
layers = await client.listSystemLayers(systemId);
|
|
25298
|
-
} catch (err) {
|
|
25299
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
25300
|
-
return { content: [{ type: "text", text: `Failed to list Layers: ${message}` }], isError: true };
|
|
25301
|
-
}
|
|
25302
|
-
const text = formatLayerList(layers);
|
|
25303
|
-
return { content: [{ type: "text", text }] };
|
|
25304
|
-
}
|
|
25305
|
-
);
|
|
25306
|
-
server.tool(
|
|
25307
|
-
"get_layer",
|
|
25308
|
-
"Get full details of a single Layer by ID, including its repo mapping and parent Layer (if it was split from one).",
|
|
25309
|
-
{
|
|
25310
|
-
systemId: external_exports.string().describe("The System ID (for access control)"),
|
|
25311
|
-
layerId: external_exports.string().describe("The Layer ID")
|
|
25312
|
-
},
|
|
25313
|
-
async ({ systemId, layerId }) => {
|
|
25314
|
-
try {
|
|
25315
|
-
await assertSystemExists(client, systemId);
|
|
25316
|
-
} catch (err) {
|
|
25317
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
25318
|
-
return { content: [{ type: "text", text: `Access denied: ${message}` }], isError: true };
|
|
25319
|
-
}
|
|
25320
|
-
let layer;
|
|
25321
|
-
try {
|
|
25322
|
-
layer = await client.getLayer(layerId);
|
|
25323
|
-
} catch (err) {
|
|
25324
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
25325
|
-
return { content: [{ type: "text", text: `Failed to get Layer: ${message}` }], isError: true };
|
|
25326
|
-
}
|
|
25327
|
-
if (!layer) {
|
|
25328
|
-
return { content: [{ type: "text", text: `Layer not found: "${layerId}"` }], isError: true };
|
|
25329
|
-
}
|
|
25330
|
-
if (layer.systemId !== systemId) {
|
|
25331
|
-
return { content: [{ type: "text", text: `Layer "${layerId}" does not belong to System "${systemId}".` }], isError: true };
|
|
25332
|
-
}
|
|
25333
|
-
const text = formatLayerDetail(layer);
|
|
25334
|
-
return { content: [{ type: "text", text }] };
|
|
25335
|
-
}
|
|
25336
|
-
);
|
|
25337
|
-
server.tool(
|
|
25338
|
-
"create_layer",
|
|
25339
|
-
"Declare a new architectural Layer on a System (e.g. frontend, backend, data). Layers are an evaluation lens for plan/build quality checks. layerKey must be a lowercase slug and unique per System.",
|
|
25340
|
-
{
|
|
25341
|
-
systemId: external_exports.string().describe("The System ID"),
|
|
25342
|
-
layerKey: external_exports.string().regex(/^[a-z]([a-z0-9-]*[a-z0-9])?$/, "layerKey must be a lowercase slug (letters, digits, hyphens; start with a letter, no trailing hyphen)").describe('Stable lowercase slug for the Layer role, e.g. "frontend" or "agent-workers"'),
|
|
25343
|
-
name: external_exports.string().describe('Display name, e.g. "Frontend"'),
|
|
25344
|
-
description: external_exports.string().optional().describe("Optional description of the Layer"),
|
|
25345
|
-
parentLayerId: external_exports.string().optional().describe("Optional parent Layer to nest under (must be a Layer on the same System)"),
|
|
25346
|
-
repoPatterns: external_exports.array(external_exports.string()).optional().describe("Optional repo path patterns owned by this Layer")
|
|
25347
|
-
},
|
|
25348
|
-
async ({ systemId, layerKey, name, description, parentLayerId, repoPatterns }) => {
|
|
25349
|
-
try {
|
|
25350
|
-
await assertSystemExists(client, systemId);
|
|
25351
|
-
} catch (err) {
|
|
25352
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
25353
|
-
return { content: [{ type: "text", text: `Access denied: ${message}` }], isError: true };
|
|
25354
|
-
}
|
|
25355
|
-
let layer;
|
|
25356
|
-
try {
|
|
25357
|
-
layer = await client.createSystemLayer({
|
|
25358
|
-
systemId,
|
|
25359
|
-
layerKey,
|
|
25360
|
-
name,
|
|
25361
|
-
...description !== void 0 && { description },
|
|
25362
|
-
...parentLayerId !== void 0 && { parentLayerId },
|
|
25363
|
-
...repoPatterns !== void 0 && { repoPatterns }
|
|
25364
|
-
});
|
|
25365
|
-
} catch (err) {
|
|
25366
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
25367
|
-
return { content: [{ type: "text", text: `Failed to create Layer: ${message}` }], isError: true };
|
|
25368
|
-
}
|
|
25369
|
-
if (!layer) {
|
|
25370
|
-
return { content: [{ type: "text", text: `System "${systemId}" not found or not writable.` }], isError: true };
|
|
25371
|
-
}
|
|
25372
|
-
return { content: [{ type: "text", text: `Created Layer.
|
|
25373
|
-
|
|
25374
|
-
${formatLayerDetail(layer)}` }] };
|
|
25375
|
-
}
|
|
25376
|
-
);
|
|
25377
|
-
server.tool(
|
|
25378
|
-
"update_layer",
|
|
25379
|
-
"Update a Layer's editable fields (name, description, repo mapping). Omitted fields are left unchanged. Archiving/reviving a Layer goes through transition_layer, not this tool. Requires write access to the System.",
|
|
25380
|
-
{
|
|
25381
|
-
systemId: external_exports.string().describe("The System ID the Layer belongs to (for access control)"),
|
|
25382
|
-
layerId: external_exports.string().describe("The Layer ID to update"),
|
|
25383
|
-
name: external_exports.string().optional().describe("New display name"),
|
|
25384
|
-
description: external_exports.string().optional().describe("New description"),
|
|
25385
|
-
repoPatterns: external_exports.array(external_exports.string()).optional().describe("Replacement repo mapping (replaces the existing list)")
|
|
25386
|
-
},
|
|
25387
|
-
async ({ systemId, layerId, name, description, repoPatterns }) => {
|
|
25388
|
-
try {
|
|
25389
|
-
await assertSystemExists(client, systemId);
|
|
25390
|
-
} catch (err) {
|
|
25391
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
25392
|
-
return { content: [{ type: "text", text: `Access denied: ${message}` }], isError: true };
|
|
25393
|
-
}
|
|
25394
|
-
let existing;
|
|
25395
|
-
try {
|
|
25396
|
-
existing = await client.getLayer(layerId);
|
|
25397
|
-
} catch (err) {
|
|
25398
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
25399
|
-
return { content: [{ type: "text", text: `Failed to load Layer: ${message}` }], isError: true };
|
|
25400
|
-
}
|
|
25401
|
-
if (!existing) return { content: [{ type: "text", text: `Layer not found: "${layerId}"` }], isError: true };
|
|
25402
|
-
if (existing.systemId !== systemId) {
|
|
25403
|
-
return { content: [{ type: "text", text: `Layer "${layerId}" does not belong to System "${systemId}".` }], isError: true };
|
|
25404
|
-
}
|
|
25405
|
-
let layer;
|
|
25406
|
-
try {
|
|
25407
|
-
layer = await client.updateSystemLayer(layerId, {
|
|
25408
|
-
...name !== void 0 && { name },
|
|
25409
|
-
...description !== void 0 && { description },
|
|
25410
|
-
...repoPatterns !== void 0 && { repoPatterns }
|
|
25411
|
-
});
|
|
25412
|
-
} catch (err) {
|
|
25413
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
25414
|
-
return { content: [{ type: "text", text: `Failed to update Layer: ${message}` }], isError: true };
|
|
25415
|
-
}
|
|
25416
|
-
if (!layer) return { content: [{ type: "text", text: `Layer not found: "${layerId}"` }], isError: true };
|
|
25417
|
-
return { content: [{ type: "text", text: `Updated Layer.
|
|
25418
|
-
|
|
25419
|
-
${formatLayerDetail(layer)}` }] };
|
|
25420
|
-
}
|
|
25421
|
-
);
|
|
25422
|
-
server.tool(
|
|
25423
|
-
"transition_layer",
|
|
25424
|
-
"Archive or revive a Layer. Archiving requires a reason (audit trail); reviving sets it back to active. Requires write access to the System.",
|
|
25425
|
-
{
|
|
25426
|
-
systemId: external_exports.string().describe("The System ID the Layer belongs to (for access control)"),
|
|
25427
|
-
layerId: external_exports.string().describe("The Layer ID to transition"),
|
|
25428
|
-
status: external_exports.enum(["active", "archived"]).describe('Target status: "archived" to retire, "active" to revive'),
|
|
25429
|
-
reason: external_exports.string().optional().describe('Why the Layer is being archived (required when status is "archived")')
|
|
25430
|
-
},
|
|
25431
|
-
async ({ systemId, layerId, status, reason }) => {
|
|
25432
|
-
try {
|
|
25433
|
-
await assertSystemExists(client, systemId);
|
|
25434
|
-
} catch (err) {
|
|
25435
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
25436
|
-
return { content: [{ type: "text", text: `Access denied: ${message}` }], isError: true };
|
|
25437
|
-
}
|
|
25438
|
-
let existing;
|
|
25439
|
-
try {
|
|
25440
|
-
existing = await client.getLayer(layerId);
|
|
25441
|
-
} catch (err) {
|
|
25442
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
25443
|
-
return { content: [{ type: "text", text: `Failed to load Layer: ${message}` }], isError: true };
|
|
25444
|
-
}
|
|
25445
|
-
if (!existing) return { content: [{ type: "text", text: `Layer not found: "${layerId}"` }], isError: true };
|
|
25446
|
-
if (existing.systemId !== systemId) {
|
|
25447
|
-
return { content: [{ type: "text", text: `Layer "${layerId}" does not belong to System "${systemId}".` }], isError: true };
|
|
25448
|
-
}
|
|
25449
|
-
let layer;
|
|
25450
|
-
try {
|
|
25451
|
-
layer = await client.setSystemLayerStatus(layerId, status, reason);
|
|
25452
|
-
} catch (err) {
|
|
25453
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
25454
|
-
return { content: [{ type: "text", text: `Failed to transition Layer: ${message}` }], isError: true };
|
|
25455
|
-
}
|
|
25456
|
-
if (!layer) return { content: [{ type: "text", text: `Layer not found: "${layerId}"` }], isError: true };
|
|
25457
|
-
return { content: [{ type: "text", text: `Layer ${status === "archived" ? "archived" : "revived"}.
|
|
25458
|
-
|
|
25459
|
-
${formatLayerDetail(layer)}` }] };
|
|
25460
|
-
}
|
|
25461
|
-
);
|
|
25462
|
-
}
|
|
25463
|
-
|
|
25464
|
-
// ../../libs/harmonica-services/src/mcp/tools/membership-tools.ts
|
|
25465
|
-
var orgIdArg = external_exports.string().describe("The organization ID \u2014 see list_organizations");
|
|
25466
|
-
function registerMembershipTools(server, ctx, client) {
|
|
25467
|
-
server.tool(
|
|
25468
|
-
"list_org_members",
|
|
25469
|
-
"List all members of an organization",
|
|
25470
|
-
{
|
|
25471
|
-
orgId: orgIdArg
|
|
25472
|
-
},
|
|
25473
|
-
async ({ orgId }) => {
|
|
25474
|
-
try {
|
|
25475
|
-
const members = await client.listOrgMembers(orgId);
|
|
25476
|
-
if (members.length === 0) {
|
|
25477
|
-
return { content: [{ type: "text", text: "No members found." }] };
|
|
25478
|
-
}
|
|
25479
|
-
const header = "| User ID | Name | Role | Title |";
|
|
25480
|
-
const divider = "|---------|------|------|-------|";
|
|
25481
|
-
const rows = members.map(
|
|
25482
|
-
(m) => `| ${m.userId} | ${m.name} | ${m.role} | ${m.title ?? "\u2014"} |`
|
|
25483
|
-
);
|
|
25484
|
-
return { content: [{ type: "text", text: [header, divider, ...rows].join("\n") }] };
|
|
25485
|
-
} catch (err) {
|
|
25486
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
25487
|
-
return { content: [{ type: "text", text: `Failed to list members: ${message}` }], isError: true };
|
|
25488
|
-
}
|
|
25489
|
-
}
|
|
25490
|
-
);
|
|
25491
|
-
server.tool(
|
|
25492
|
-
"add_member",
|
|
25493
|
-
"Grant a user access to an organization. Their name and avatar are filled in when they first sign in.",
|
|
25494
|
-
{
|
|
25495
|
-
orgId: orgIdArg,
|
|
25496
|
-
email: external_exports.string().email().describe("Email address (used as the user ID)"),
|
|
25497
|
-
role: external_exports.enum(["member", "admin", "client"]).optional().describe('Organization role (defaults to "member")')
|
|
25498
|
-
},
|
|
25499
|
-
async ({ orgId, email: email2, role }) => {
|
|
25500
|
-
try {
|
|
25501
|
-
const member = await client.addOrgMember(orgId, { email: email2, role });
|
|
25502
|
-
const lines = [
|
|
25503
|
-
"Member added successfully.",
|
|
25504
|
-
"",
|
|
25505
|
-
`**User ID:** ${member.userId}`,
|
|
25506
|
-
`**Email:** ${member.email}`,
|
|
25507
|
-
`**Name:** ${member.name}`,
|
|
25508
|
-
`**Role:** ${member.role}`,
|
|
25509
|
-
member.title ? `**Title:** ${member.title}` : ""
|
|
25510
|
-
].filter(Boolean);
|
|
25511
|
-
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
25512
|
-
} catch (err) {
|
|
25513
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
25514
|
-
return { content: [{ type: "text", text: `Failed to add member: ${message}` }], isError: true };
|
|
25515
|
-
}
|
|
25516
|
-
}
|
|
25517
|
-
);
|
|
25518
|
-
server.tool(
|
|
25519
|
-
"update_member_role",
|
|
25520
|
-
"Change a member's role within an organization.",
|
|
25521
|
-
{
|
|
25522
|
-
orgId: orgIdArg,
|
|
25523
|
-
userId: external_exports.string().describe("User ID (email address) of the member to update"),
|
|
25524
|
-
role: external_exports.enum(["member", "admin", "client"]).describe("New organization role")
|
|
25525
|
-
},
|
|
25526
|
-
async ({ orgId, userId, role }) => {
|
|
25527
|
-
try {
|
|
25528
|
-
const member = await client.updateOrgMember(orgId, userId, { role });
|
|
25529
|
-
if (!member) {
|
|
25530
|
-
return { content: [{ type: "text", text: `Member not found: "${userId}"` }], isError: true };
|
|
25531
|
-
}
|
|
25532
|
-
const lines = [
|
|
25533
|
-
"Member updated successfully.",
|
|
25534
|
-
"",
|
|
25535
|
-
`**User ID:** ${member.userId}`,
|
|
25536
|
-
`**Email:** ${member.email}`,
|
|
25537
|
-
`**Name:** ${member.name}`,
|
|
25538
|
-
`**Role:** ${member.role}`,
|
|
25539
|
-
member.title ? `**Title:** ${member.title}` : "",
|
|
25540
|
-
member.responsibility ? `**Responsibility:** ${member.responsibility}` : ""
|
|
25541
|
-
].filter(Boolean);
|
|
25542
|
-
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
25543
|
-
} catch (err) {
|
|
25544
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
25545
|
-
return { content: [{ type: "text", text: `Failed to update member: ${message}` }], isError: true };
|
|
25546
|
-
}
|
|
25547
|
-
}
|
|
25548
|
-
);
|
|
25549
|
-
server.tool(
|
|
25550
|
-
"remove_member",
|
|
25551
|
-
"Remove a user from an organization.",
|
|
25552
|
-
{
|
|
25553
|
-
orgId: orgIdArg,
|
|
25554
|
-
userId: external_exports.string().describe("User ID (email address) of the member to remove")
|
|
25555
|
-
},
|
|
25556
|
-
async ({ orgId, userId }) => {
|
|
25557
|
-
try {
|
|
25558
|
-
await client.removeOrgMember(orgId, userId);
|
|
25559
|
-
return { content: [{ type: "text", text: `Member "${userId}" has been removed from the organization.` }] };
|
|
25560
|
-
} catch (err) {
|
|
25561
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
25562
|
-
return { content: [{ type: "text", text: `Failed to remove member: ${message}` }], isError: true };
|
|
25563
|
-
}
|
|
25564
|
-
}
|
|
25565
|
-
);
|
|
25566
|
-
}
|
|
25567
|
-
|
|
25568
|
-
// ../../libs/harmonica-services/src/mcp/tools/movement-tools.ts
|
|
25569
|
-
function registerMovementTools(server, _ctx, client) {
|
|
25570
|
-
server.tool(
|
|
25571
|
-
"list_movements",
|
|
25572
|
-
'List all Movements for a Teamspace. A Movement is a Teamspace-scoped client-SOW container for an engagement. Recognition cues: "this SOW", "this engagement", "what contracts are active" \u2192 list_movements.',
|
|
25573
|
-
{
|
|
25574
|
-
teamspaceId: external_exports.string().describe("The teamspace ID")
|
|
25575
|
-
},
|
|
25576
|
-
async ({ teamspaceId }) => {
|
|
25577
|
-
const movements = await client.listTeamspaceMovements(teamspaceId);
|
|
25578
|
-
const text = movements.length === 0 ? "No movements found." : movements.map((m) => `[${m.movementId}] ${m.movementCode} \u2014 ${m.title} (${m.status})`).join("\n");
|
|
25579
|
-
return { content: [{ type: "text", text }] };
|
|
25580
|
-
}
|
|
25581
|
-
);
|
|
25582
|
-
server.tool(
|
|
25583
|
-
"get_movement",
|
|
25584
|
-
"Get a Movement by ID. Returns full details including status, approval timestamps, and audit stamps.",
|
|
25585
|
-
{
|
|
25586
|
-
movementId: external_exports.string().describe("The movement ID (e.g., mv-abc123)")
|
|
25587
|
-
},
|
|
25588
|
-
async ({ movementId }) => {
|
|
25589
|
-
const m = await client.getMovement(movementId);
|
|
25590
|
-
if (!m) {
|
|
25591
|
-
return { content: [{ type: "text", text: `Movement not found: ${movementId}` }] };
|
|
25592
|
-
}
|
|
25593
|
-
const lines = [
|
|
25594
|
-
`ID: ${m.movementId}`,
|
|
25595
|
-
`Code: ${m.movementCode}`,
|
|
25596
|
-
`Title: ${m.title}`,
|
|
25597
|
-
`Teamspace: ${m.teamspaceId}`,
|
|
25598
|
-
`Status: ${m.status}`,
|
|
25599
|
-
m.description ? `Description: ${m.description}` : null,
|
|
25600
|
-
m.proposedAt ? `Proposed: ${m.proposedAt}` : null,
|
|
25601
|
-
m.approvedAt ? `Approved: ${m.approvedAt}` : null,
|
|
25602
|
-
m.approvalNoteId ? `Approval Note: ${m.approvalNoteId}` : null,
|
|
25603
|
-
m.createdBy ? `Created By: ${m.createdBy}` : null,
|
|
25604
|
-
`Version: ${m.version}`,
|
|
25605
|
-
`Created: ${m.createdAt}`,
|
|
25606
|
-
`Updated: ${m.updatedAt}`
|
|
25607
|
-
].filter(Boolean).join("\n");
|
|
25608
|
-
return { content: [{ type: "text", text: lines }] };
|
|
25609
|
-
}
|
|
25610
|
-
);
|
|
25611
|
-
server.tool(
|
|
25612
|
-
"create_movement",
|
|
25613
|
-
'Create a Movement in a Teamspace. A Movement is a Teamspace-scoped client-SOW container for an engagement \u2014 it is NOT a Beat. Recognition cues: "new SOW", "create a contract scope", "start an engagement container" \u2192 create_movement.',
|
|
25614
|
-
{
|
|
25615
|
-
teamspaceId: external_exports.string().describe("The teamspace ID"),
|
|
25616
|
-
title: external_exports.string().min(1).max(300).describe('Movement title (e.g. "Q3 Platform Modernisation SOW")'),
|
|
25617
|
-
description: external_exports.string().max(2e3).optional().describe("Optional description"),
|
|
25618
|
-
accountId: external_exports.string().min(1).max(128).optional().describe("Optional Account this Movement rolls up to. Must belong to the same org as the Teamspace.")
|
|
25619
|
-
},
|
|
25620
|
-
async ({ teamspaceId, title, description, accountId }) => {
|
|
25621
|
-
const m = await client.createMovement({
|
|
25622
|
-
teamspaceId,
|
|
25623
|
-
title,
|
|
25624
|
-
...description !== void 0 && { description },
|
|
25625
|
-
...accountId !== void 0 && { accountId }
|
|
25626
|
-
});
|
|
25627
|
-
return {
|
|
25628
|
-
content: [
|
|
25629
|
-
{
|
|
25630
|
-
type: "text",
|
|
25631
|
-
text: `Created Movement: [${m.movementId}] ${m.movementCode} \u2014 ${m.title} (${m.status})`
|
|
25632
|
-
}
|
|
25633
|
-
]
|
|
25634
|
-
};
|
|
25635
|
-
}
|
|
25636
|
-
);
|
|
25637
|
-
server.tool(
|
|
25638
|
-
"update_movement",
|
|
25639
|
-
"Update mutable content fields on a Movement (title, description). To change status use transition_movement_status instead.",
|
|
25640
|
-
{
|
|
25641
|
-
movementId: external_exports.string().describe("The movement ID"),
|
|
25642
|
-
title: external_exports.string().min(1).max(300).optional().describe("New title"),
|
|
25643
|
-
description: external_exports.string().max(2e3).optional().describe("New description"),
|
|
25644
|
-
accountId: external_exports.string().max(128).nullable().optional().describe("Account this Movement rolls up to. Must belong to the same org. Pass null (or an empty string) to unlink.")
|
|
25645
|
-
},
|
|
25646
|
-
async ({ movementId, title, description, accountId }) => {
|
|
25647
|
-
const hasUpdates = title !== void 0 || description !== void 0 || accountId !== void 0;
|
|
25648
|
-
if (!hasUpdates) {
|
|
25649
|
-
return {
|
|
25650
|
-
content: [{ type: "text", text: `No updates provided for Movement: ${movementId}` }],
|
|
25651
|
-
isError: true
|
|
25652
|
-
};
|
|
25653
|
-
}
|
|
25654
|
-
const resolvedAccountId = accountId === "" ? null : accountId;
|
|
25655
|
-
try {
|
|
25656
|
-
const m = await client.updateMovement(movementId, {
|
|
25657
|
-
...title !== void 0 && { title },
|
|
25658
|
-
...description !== void 0 && { description },
|
|
25659
|
-
...accountId !== void 0 && { accountId: resolvedAccountId }
|
|
25660
|
-
});
|
|
25661
|
-
if (!m) {
|
|
25662
|
-
return {
|
|
25663
|
-
content: [{ type: "text", text: `Movement not found: ${movementId}` }],
|
|
25664
|
-
isError: true
|
|
25665
|
-
};
|
|
25666
|
-
}
|
|
25667
|
-
const account = accountId !== void 0 ? resolvedAccountId === null ? " \u2014 Account: (unlinked)" : ` \u2014 Account: ${resolvedAccountId}` : m.accountId ? ` \u2014 Account: ${m.accountId}` : "";
|
|
25668
|
-
return {
|
|
25669
|
-
content: [
|
|
25670
|
-
{
|
|
25671
|
-
type: "text",
|
|
25672
|
-
text: `Updated Movement: [${m.movementId}] ${m.movementCode} \u2014 ${m.title} (${m.status})${account}`
|
|
25673
|
-
}
|
|
25674
|
-
]
|
|
25675
|
-
};
|
|
25676
|
-
} catch (err) {
|
|
25677
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
25678
|
-
return {
|
|
25679
|
-
content: [{ type: "text", text: `Failed to update Movement: ${message}` }],
|
|
25680
|
-
isError: true
|
|
25681
|
-
};
|
|
25682
|
-
}
|
|
25683
|
-
}
|
|
25684
|
-
);
|
|
25685
|
-
server.tool(
|
|
25686
|
-
"transition_movement_status",
|
|
25687
|
-
'Transition a Movement through its commercial lifecycle: draft \u2192 proposed \u2192 approved \u2192 active \u2192 fulfilled. Cancelled is reachable from any non-terminal state and is how a superseded Movement is retired. Advancing to "approved" requires an approvalNoteId. Use confirm=false (default) to preview the transition without writing; confirm=true to execute.',
|
|
25688
|
-
{
|
|
25689
|
-
movementId: external_exports.string().describe("The movement ID"),
|
|
25690
|
-
targetState: external_exports.enum(["proposed", "approved", "active", "fulfilled", "cancelled"]).describe("Target lifecycle state"),
|
|
25691
|
-
approvalNoteId: external_exports.string().optional().describe('Required when transitioning to "approved" \u2014 the Note ID capturing the approval decision'),
|
|
25692
|
-
reason: external_exports.string().optional().describe("Reason for the transition. Strongly recommended for cancellation \u2014 retiring a Movement with no recorded reason leaves no audit trail"),
|
|
25693
|
-
confirm: external_exports.boolean().optional().default(false).describe("false (default) = dry-run preview; true = execute the transition")
|
|
25694
|
-
},
|
|
25695
|
-
async ({ movementId, targetState, approvalNoteId, reason, confirm }) => {
|
|
25696
|
-
if (!confirm) {
|
|
25697
|
-
return {
|
|
25698
|
-
content: [
|
|
25699
|
-
{
|
|
25700
|
-
type: "text",
|
|
25701
|
-
text: `Dry-run: would transition Movement ${movementId} \u2192 ${targetState}${approvalNoteId ? ` (approvalNoteId: ${approvalNoteId})` : ""}${reason ? ` \u2014 ${reason}` : ""}. Call again with confirm=true to execute.`
|
|
25702
|
-
}
|
|
25703
|
-
]
|
|
25704
|
-
};
|
|
25705
|
-
}
|
|
25706
|
-
try {
|
|
25707
|
-
const m = await client.transitionMovementStatus(movementId, targetState, {
|
|
25708
|
-
...approvalNoteId !== void 0 && { approvalNoteId },
|
|
25709
|
-
...reason !== void 0 && { reason }
|
|
25710
|
-
});
|
|
25711
|
-
if (!m) {
|
|
25712
|
-
return {
|
|
25713
|
-
content: [{ type: "text", text: `Movement not found: ${movementId}` }],
|
|
25714
|
-
isError: true
|
|
25715
|
-
};
|
|
25716
|
-
}
|
|
25717
|
-
return {
|
|
25718
|
-
content: [
|
|
25719
|
-
{
|
|
25720
|
-
type: "text",
|
|
25721
|
-
text: `Transitioned Movement: [${m.movementId}] ${m.movementCode} \u2014 ${m.title} \u2192 ${m.status}`
|
|
25722
|
-
}
|
|
25723
|
-
]
|
|
25724
|
-
};
|
|
25725
|
-
} catch (err) {
|
|
25726
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
25727
|
-
return {
|
|
25728
|
-
content: [{ type: "text", text: `Failed to transition Movement: ${message}` }],
|
|
25729
|
-
isError: true
|
|
25730
|
-
};
|
|
25731
|
-
}
|
|
25732
|
-
}
|
|
25733
|
-
);
|
|
25734
|
-
}
|
|
25735
|
-
|
|
25736
|
-
// ../../libs/harmonica-services/src/mcp/tools/next-actions-tools.ts
|
|
25737
|
-
var PRIORITY_ICONS = {
|
|
25738
|
-
P0: "\u{1F6A8}",
|
|
25739
|
-
P1: "\u{1F680}",
|
|
25740
|
-
P2: "\u{1F528}",
|
|
25741
|
-
P3: "\u{1F4CB}",
|
|
25742
|
-
P4: "\u{1F4A1}",
|
|
25743
|
-
P5: "\u2728"
|
|
25744
|
-
};
|
|
25745
|
-
function formatNextActions(actions) {
|
|
25746
|
-
if (actions.length === 0) {
|
|
25747
|
-
return "No actions found. Project is up to date.";
|
|
25748
|
-
}
|
|
25749
|
-
const lines = ["# Next Actions\n"];
|
|
25750
|
-
for (const action of actions) {
|
|
25751
|
-
const icon = PRIORITY_ICONS[action.priority] ?? "\u2022";
|
|
25752
|
-
lines.push(`## ${icon} ${action.priority} \u2014 ${action.category}`);
|
|
25753
|
-
lines.push(`**${action.title}**`);
|
|
25754
|
-
lines.push(action.description);
|
|
25755
|
-
if (action.entityId) {
|
|
25756
|
-
const parts = [`${action.entityType}: ${action.entityId}`];
|
|
25757
|
-
if (action.beatId) parts.push(`Beat: ${action.beatId}`);
|
|
25758
|
-
lines.push(parts.join(" | "));
|
|
25759
|
-
}
|
|
25760
|
-
if (action.count) {
|
|
25761
|
-
lines.push(`Count: ${action.count}`);
|
|
25762
|
-
}
|
|
25763
|
-
lines.push("");
|
|
25764
|
-
}
|
|
25765
|
-
return lines.join("\n");
|
|
25766
|
-
}
|
|
25767
|
-
function registerNextActionsTools(server, ctx, client) {
|
|
25768
|
-
server.tool(
|
|
25769
|
-
"get_next_actions",
|
|
25770
|
-
"Get prioritized list of recommended next actions for a system. Analyzes Beats, Revisions, and Notes to surface the highest-priority work: P0 (urgent bugs) \u2192 P1 (deploy) \u2192 P2 (build) \u2192 P3 (plan) \u2192 P4 (compose) \u2192 P5 (maintenance).",
|
|
25771
|
-
{
|
|
25772
|
-
systemId: external_exports.string().describe("The system ID"),
|
|
25773
|
-
limit: external_exports.coerce.number().optional().default(10).describe("Max actions to return")
|
|
25774
|
-
},
|
|
25775
|
-
async ({ systemId, limit }) => {
|
|
25776
|
-
try {
|
|
25777
|
-
await assertSystemExists(client, systemId);
|
|
25778
|
-
const actions = await client.getNextActions(systemId, { limit });
|
|
25779
|
-
return { content: [{ type: "text", text: formatNextActions(actions) }] };
|
|
25780
|
-
} catch (err) {
|
|
25781
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
25782
|
-
return { content: [{ type: "text", text: `Failed to get next actions: ${message}` }], isError: true };
|
|
25783
|
-
}
|
|
25784
|
-
}
|
|
25785
|
-
);
|
|
25786
|
-
server.tool(
|
|
25787
|
-
"list_triage_actions",
|
|
25788
|
-
"List active triage actions from the ledger (open + acted). This is the stateful view of what needs attention \u2014 includes status tracking, task progress, and escalation state. Use this instead of get_next_actions when triage has been run.",
|
|
25789
|
-
{
|
|
25790
|
-
systemId: external_exports.string().describe("The system ID")
|
|
25791
|
-
},
|
|
25792
|
-
async ({ systemId }) => {
|
|
25793
|
-
try {
|
|
25794
|
-
await assertSystemExists(client, systemId);
|
|
25795
|
-
const actions = await client.listTriageActions(systemId);
|
|
25796
|
-
return { content: [{ type: "text", text: formatTriageActions(actions) }] };
|
|
25797
|
-
} catch (err) {
|
|
25798
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
25799
|
-
return { content: [{ type: "text", text: `Failed to list triage actions: ${message}` }], isError: true };
|
|
25800
|
-
}
|
|
25801
|
-
}
|
|
25802
|
-
);
|
|
25803
|
-
server.tool(
|
|
25804
|
-
"dismiss_triage_action",
|
|
25805
|
-
'Dismiss a triage action (human decides "not now"). The action will not be re-escalated until the underlying state changes.',
|
|
25806
|
-
{
|
|
25807
|
-
systemId: external_exports.string().describe("The system ID"),
|
|
25808
|
-
actionKey: external_exports.string().describe("The action key to dismiss (from list_triage_actions)")
|
|
25809
|
-
},
|
|
25810
|
-
async ({ systemId, actionKey }) => {
|
|
25811
|
-
try {
|
|
25812
|
-
await assertSystemExists(client, systemId);
|
|
25813
|
-
await client.dismissTriageAction(systemId, actionKey, ctx.user?.userId ?? "mcp");
|
|
25814
|
-
return { content: [{ type: "text", text: `Action ${actionKey} dismissed.` }] };
|
|
25815
|
-
} catch (err) {
|
|
25816
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
25817
|
-
return { content: [{ type: "text", text: `Failed to dismiss action: ${message}` }], isError: true };
|
|
25818
|
-
}
|
|
25819
|
-
}
|
|
25820
|
-
);
|
|
25821
|
-
}
|
|
25822
|
-
var STATUS_ICONS = {
|
|
25823
|
-
open: "\u2B1A",
|
|
25824
|
-
acted: "\u25D0",
|
|
25825
|
-
dismissed: "\u2715",
|
|
25826
|
-
resolved: "\u2713"
|
|
25827
|
-
};
|
|
25828
|
-
function formatTriageActions(actions) {
|
|
25829
|
-
if (actions.length === 0) {
|
|
25830
|
-
return "No active triage actions. Run triage to populate the ledger.";
|
|
25831
|
-
}
|
|
25832
|
-
const lines = [`# Triage Actions (${actions.length})
|
|
25833
|
-
`];
|
|
25834
|
-
for (const action of actions) {
|
|
25835
|
-
const pIcon = PRIORITY_ICONS[action.priority] ?? "\u2022";
|
|
25836
|
-
const sIcon = STATUS_ICONS[action.status] ?? "?";
|
|
25837
|
-
lines.push(`## ${pIcon} ${action.priority} \u2014 ${action.category} ${sIcon} ${action.status.toUpperCase()}`);
|
|
25838
|
-
lines.push(`**${action.title}**`);
|
|
25839
|
-
lines.push(action.description);
|
|
25840
|
-
const meta = [];
|
|
25841
|
-
if (action.entityId) meta.push(`Entity: ${action.entityId}`);
|
|
25842
|
-
if (action.taskId) meta.push(`Task: ${action.taskId}${action.taskStatus ? ` (${action.taskStatus})` : ""}`);
|
|
25843
|
-
if (action.decision) meta.push(`Decision: ${action.decision}`);
|
|
25844
|
-
if (meta.length > 0) lines.push(meta.join(" | "));
|
|
25845
|
-
lines.push(`Action key: \`${action.actionKey}\` | First seen: ${action.firstSeenAt}`);
|
|
25846
|
-
lines.push("");
|
|
25847
|
-
}
|
|
25848
|
-
return lines.join("\n");
|
|
25849
|
-
}
|
|
25850
|
-
|
|
25851
|
-
// ../../libs/harmonica-services/src/mcp/tools/note-element-tools.ts
|
|
25852
|
-
var import_crypto3 = require("crypto");
|
|
25853
|
-
|
|
25854
24849
|
// ../../libs/harmonica-schemas/src/azure-marketplace.ts
|
|
25855
24850
|
var AzureMarketplaceSubscriptionSchema = external_exports.object({
|
|
25856
24851
|
id: external_exports.string(),
|
|
@@ -26602,6 +25597,7 @@ var OrganizationSummarySchema = external_exports.object({
|
|
|
26602
25597
|
name: external_exports.string(),
|
|
26603
25598
|
slug: external_exports.string(),
|
|
26604
25599
|
logoUrl: external_exports.string().optional(),
|
|
25600
|
+
capabilityProfile: external_exports.enum(["azure-core"]).optional(),
|
|
26605
25601
|
status: external_exports.string(),
|
|
26606
25602
|
role: external_exports.string(),
|
|
26607
25603
|
systemCount: external_exports.number(),
|
|
@@ -26942,6 +25938,15 @@ var WorkItemApiSchema = external_exports.object({
|
|
|
26942
25938
|
* named group, not hide them.
|
|
26943
25939
|
*/
|
|
26944
25940
|
workOrderId: external_exports.string().min(1).optional(),
|
|
25941
|
+
/**
|
|
25942
|
+
* Optional link to the Bar this item is planned into (B-298). Absent means
|
|
25943
|
+
* unplanned, which is a supported state — a reader grouping a queue by Bar
|
|
25944
|
+
* must render those as their own named group, not hide them. An id that
|
|
25945
|
+
* doesn't resolve within the Account in view is work the reader cannot
|
|
25946
|
+
* place, not work that is unplanned: a Bar is Account-scoped but a Track is
|
|
25947
|
+
* not, so a move on either side can separate the two.
|
|
25948
|
+
*/
|
|
25949
|
+
barId: external_exports.string().min(1).optional(),
|
|
26945
25950
|
createdBy: external_exports.string().optional(),
|
|
26946
25951
|
version: external_exports.number().optional(),
|
|
26947
25952
|
createdAt: external_exports.string(),
|
|
@@ -26951,8 +25956,68 @@ var WorkItemEstimateHintApiSchema = external_exports.object({
|
|
|
26951
25956
|
hours: external_exports.number(),
|
|
26952
25957
|
source: external_exports.literal("beat_version_value_velocity")
|
|
26953
25958
|
});
|
|
25959
|
+
var BAR_STATUSES2 = ["open", "closed"];
|
|
25960
|
+
var BarApiSchema = external_exports.object({
|
|
25961
|
+
barId: external_exports.string(),
|
|
25962
|
+
accountId: external_exports.string(),
|
|
25963
|
+
/** ISO 8601 date the Bar opens. */
|
|
25964
|
+
startDate: external_exports.string(),
|
|
25965
|
+
/** ISO 8601 date the Bar closes. */
|
|
25966
|
+
endDate: external_exports.string(),
|
|
25967
|
+
status: external_exports.enum(BAR_STATUSES2),
|
|
25968
|
+
createdAt: external_exports.string(),
|
|
25969
|
+
updatedAt: external_exports.string()
|
|
25970
|
+
}).passthrough();
|
|
25971
|
+
var BarsResponseSchema = external_exports.object({ bars: external_exports.array(BarApiSchema) }).passthrough();
|
|
26954
25972
|
var WorkItemsResponseSchema = external_exports.object({ workItems: external_exports.array(WorkItemApiSchema) }).passthrough();
|
|
26955
25973
|
var WorkItemResponseSchema = external_exports.object({ workItem: WorkItemApiSchema, estimateHint: WorkItemEstimateHintApiSchema.optional() }).passthrough();
|
|
25974
|
+
var EXTERNAL_MAPPING_SUBJECT_TYPES = [
|
|
25975
|
+
"movement",
|
|
25976
|
+
"work_order",
|
|
25977
|
+
"work_item"
|
|
25978
|
+
];
|
|
25979
|
+
var ExternalMappingSubjectTypeApiSchema = external_exports.enum(EXTERNAL_MAPPING_SUBJECT_TYPES);
|
|
25980
|
+
var EXTERNAL_MAPPING_PROVIDERS = [
|
|
25981
|
+
"jira",
|
|
25982
|
+
"linear",
|
|
25983
|
+
"github",
|
|
25984
|
+
"asana",
|
|
25985
|
+
"other"
|
|
25986
|
+
];
|
|
25987
|
+
var ExternalMappingProviderApiSchema = external_exports.enum(EXTERNAL_MAPPING_PROVIDERS);
|
|
25988
|
+
var EXTERNAL_MAPPING_SYNC_DIRECTIONS = [
|
|
25989
|
+
"none",
|
|
25990
|
+
"inbound",
|
|
25991
|
+
"outbound",
|
|
25992
|
+
"bidirectional"
|
|
25993
|
+
];
|
|
25994
|
+
var ExternalMappingSyncDirectionApiSchema = external_exports.enum(EXTERNAL_MAPPING_SYNC_DIRECTIONS);
|
|
25995
|
+
var EXTERNAL_MAPPING_SYNC_STATUSES = [
|
|
25996
|
+
"ok",
|
|
25997
|
+
"stale",
|
|
25998
|
+
"error",
|
|
25999
|
+
"unlinked"
|
|
26000
|
+
];
|
|
26001
|
+
var ExternalMappingSyncStatusApiSchema = external_exports.enum(EXTERNAL_MAPPING_SYNC_STATUSES);
|
|
26002
|
+
var ExternalMappingApiSchema = external_exports.object({
|
|
26003
|
+
externalMappingId: external_exports.string(),
|
|
26004
|
+
subjectType: ExternalMappingSubjectTypeApiSchema,
|
|
26005
|
+
subjectId: external_exports.string(),
|
|
26006
|
+
teamspaceId: external_exports.string(),
|
|
26007
|
+
orgId: external_exports.string(),
|
|
26008
|
+
provider: ExternalMappingProviderApiSchema,
|
|
26009
|
+
externalKey: external_exports.string(),
|
|
26010
|
+
url: external_exports.string().optional(),
|
|
26011
|
+
syncDirection: ExternalMappingSyncDirectionApiSchema,
|
|
26012
|
+
lastSyncedAt: external_exports.string().optional(),
|
|
26013
|
+
syncStatus: ExternalMappingSyncStatusApiSchema,
|
|
26014
|
+
createdBy: external_exports.string().optional(),
|
|
26015
|
+
version: external_exports.number().optional(),
|
|
26016
|
+
createdAt: external_exports.string(),
|
|
26017
|
+
updatedAt: external_exports.string()
|
|
26018
|
+
}).passthrough();
|
|
26019
|
+
var ExternalMappingsResponseSchema = external_exports.object({ externalMappings: external_exports.array(ExternalMappingApiSchema) }).passthrough();
|
|
26020
|
+
var EXTERNAL_MAPPING_SUBJECT_NOT_FOUND_CODE = "SUBJECT_NOT_FOUND";
|
|
26956
26021
|
var WORK_INTAKE_STATUSES = [
|
|
26957
26022
|
"requested",
|
|
26958
26023
|
"needs_decision",
|
|
@@ -26999,8 +26064,14 @@ var WorkOrderApiSchema = external_exports.object({
|
|
|
26999
26064
|
workOrderId: external_exports.string(),
|
|
27000
26065
|
/** Immutable lineage back to the ask this Order was split from. */
|
|
27001
26066
|
workRequestId: external_exports.string(),
|
|
27002
|
-
/**
|
|
27003
|
-
|
|
26067
|
+
/**
|
|
26068
|
+
* Optional: an Order may carry no Track at all. Under N-4E09-7470 a Track is
|
|
26069
|
+
* attribution, not an Order's parent, and an Order can be raised against a
|
|
26070
|
+
* Request before any placement exists. Requiring it here made the API reject
|
|
26071
|
+
* its own valid responses — a Movement holding one unplaced Order failed to
|
|
26072
|
+
* parse, so the whole list threw.
|
|
26073
|
+
*/
|
|
26074
|
+
trackId: external_exports.string().optional(),
|
|
27004
26075
|
teamspaceId: external_exports.string(),
|
|
27005
26076
|
orgId: external_exports.string(),
|
|
27006
26077
|
title: external_exports.string(),
|
|
@@ -27116,7 +26187,7 @@ var LayerRubricTemplateApiSchema = external_exports.object({
|
|
|
27116
26187
|
var LayerRubricTemplatesResponseSchema = external_exports.object({
|
|
27117
26188
|
layerRubricTemplates: external_exports.array(LayerRubricTemplateApiSchema)
|
|
27118
26189
|
}).passthrough();
|
|
27119
|
-
var
|
|
26190
|
+
var TRACK_STATUSES = [
|
|
27120
26191
|
"draft",
|
|
27121
26192
|
"proposed",
|
|
27122
26193
|
"approved",
|
|
@@ -27124,19 +26195,18 @@ var DELIVERABLE_GROUP_STATUSES = [
|
|
|
27124
26195
|
"fulfilled",
|
|
27125
26196
|
"cancelled"
|
|
27126
26197
|
];
|
|
27127
|
-
var
|
|
27128
|
-
var
|
|
27129
|
-
|
|
27130
|
-
|
|
26198
|
+
var TrackStatusSchema = external_exports.enum(TRACK_STATUSES);
|
|
26199
|
+
var TrackApiSchema = external_exports.object({
|
|
26200
|
+
trackId: external_exports.string(),
|
|
26201
|
+
trackCode: external_exports.string(),
|
|
27131
26202
|
teamspaceId: external_exports.string(),
|
|
27132
|
-
systemId: external_exports.string().nullish(),
|
|
27133
26203
|
movementId: external_exports.string().nullish(),
|
|
27134
26204
|
orgId: external_exports.string(),
|
|
27135
26205
|
title: external_exports.string(),
|
|
27136
26206
|
description: external_exports.string().nullish(),
|
|
27137
26207
|
deliverables: external_exports.string().nullish(),
|
|
27138
26208
|
acceptanceCriteria: external_exports.string().nullish(),
|
|
27139
|
-
status:
|
|
26209
|
+
status: TrackStatusSchema,
|
|
27140
26210
|
vuRate: external_exports.number().nullish(),
|
|
27141
26211
|
proposedAt: external_exports.string().nullish(),
|
|
27142
26212
|
approvedAt: external_exports.string().nullish(),
|
|
@@ -27147,8 +26217,8 @@ var DeliverableGroupApiSchema = external_exports.object({
|
|
|
27147
26217
|
createdAt: external_exports.string(),
|
|
27148
26218
|
updatedAt: external_exports.string()
|
|
27149
26219
|
}).passthrough();
|
|
27150
|
-
var
|
|
27151
|
-
|
|
26220
|
+
var TracksResponseSchema = external_exports.object({
|
|
26221
|
+
tracks: external_exports.array(TrackApiSchema)
|
|
27152
26222
|
}).passthrough();
|
|
27153
26223
|
var MovementDropSummaryApiSchema = external_exports.object({
|
|
27154
26224
|
dropId: external_exports.string(),
|
|
@@ -27178,7 +26248,7 @@ var WorkPlanBeatVersionApiSchema = external_exports.object({
|
|
|
27178
26248
|
createdAt: external_exports.string(),
|
|
27179
26249
|
updatedAt: external_exports.string()
|
|
27180
26250
|
}).passthrough();
|
|
27181
|
-
var
|
|
26251
|
+
var TrackBeatVersionsResponseSchema = external_exports.object({
|
|
27182
26252
|
beatVersions: external_exports.array(WorkPlanBeatVersionApiSchema)
|
|
27183
26253
|
}).passthrough();
|
|
27184
26254
|
var RevisionClaimActorSchema = external_exports.union([
|
|
@@ -27275,7 +26345,918 @@ var DropProposalApiSchema = external_exports.object({
|
|
|
27275
26345
|
scoredCandidates: external_exports.array(ScoredDropCandidateSchema).optional()
|
|
27276
26346
|
}).passthrough();
|
|
27277
26347
|
|
|
26348
|
+
// ../../libs/harmonica-services/src/mcp/tools/external-mapping-tools.ts
|
|
26349
|
+
var SUBJECT_TYPE_ENUM = ["movement", "work_order", "work_item"];
|
|
26350
|
+
var PROVIDER_ENUM = ["jira", "linear", "github", "asana", "other"];
|
|
26351
|
+
var SYNC_DIRECTION_ENUM = ["none", "inbound", "outbound", "bidirectional"];
|
|
26352
|
+
var SYNC_STATUS_ENUM = ["ok", "stale", "error", "unlinked"];
|
|
26353
|
+
function formatMapping(m) {
|
|
26354
|
+
const parts = [
|
|
26355
|
+
`[${m.externalMappingId}] ${m.provider}:${m.externalKey}`,
|
|
26356
|
+
`status: ${m.syncStatus}`,
|
|
26357
|
+
`direction: ${m.syncDirection}`
|
|
26358
|
+
];
|
|
26359
|
+
if (m.url) parts.push(`url: ${m.url}`);
|
|
26360
|
+
if (m.lastSyncedAt) parts.push(`last synced: ${m.lastSyncedAt}`);
|
|
26361
|
+
return parts.join(" \u2014 ");
|
|
26362
|
+
}
|
|
26363
|
+
function registerExternalMappingTools(server, _ctx, client) {
|
|
26364
|
+
server.tool(
|
|
26365
|
+
"add_external_mapping",
|
|
26366
|
+
"Add an External Mapping \u2014 a reference-only link from a Project (Movement), Work Order, or Work Item to an external tracker such as Jira, Linear, GitHub, or Asana. Nothing talks to the external system; sync direction is recorded intent only (N-4E09-6311). Zero to many mappings per subject; creating one checks the subject exists and is in the caller's scope.",
|
|
26367
|
+
{
|
|
26368
|
+
subjectType: external_exports.enum(SUBJECT_TYPE_ENUM).describe("The type of Harmonica entity being linked: movement (project), work_order, or work_item"),
|
|
26369
|
+
subjectId: external_exports.string().min(1).max(128).describe("The ID of the subject entity"),
|
|
26370
|
+
provider: external_exports.enum(PROVIDER_ENUM).describe("The external tracker provider"),
|
|
26371
|
+
externalKey: external_exports.string().min(1).max(500).describe('Provider-specific key, e.g. a Jira issue key "PROJ-123" or a Linear issue id'),
|
|
26372
|
+
url: external_exports.string().url().optional().describe("Optional human-readable URL to the external object"),
|
|
26373
|
+
syncDirection: external_exports.enum(SYNC_DIRECTION_ENUM).optional().describe('Recorded intent for a later sync increment. Defaults to "none". Nothing syncs in this version.')
|
|
26374
|
+
},
|
|
26375
|
+
async ({ subjectType, subjectId, provider, externalKey, url, syncDirection }) => {
|
|
26376
|
+
try {
|
|
26377
|
+
const em = await client.addExternalMapping({
|
|
26378
|
+
subjectType,
|
|
26379
|
+
subjectId,
|
|
26380
|
+
provider,
|
|
26381
|
+
externalKey,
|
|
26382
|
+
...url !== void 0 && { url },
|
|
26383
|
+
...syncDirection !== void 0 && { syncDirection }
|
|
26384
|
+
});
|
|
26385
|
+
return {
|
|
26386
|
+
content: [
|
|
26387
|
+
{
|
|
26388
|
+
type: "text",
|
|
26389
|
+
text: `Added external mapping: ${formatMapping(em)}`
|
|
26390
|
+
}
|
|
26391
|
+
]
|
|
26392
|
+
};
|
|
26393
|
+
} catch (err) {
|
|
26394
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
26395
|
+
const isSubjectMissing = err instanceof Error && err.code === EXTERNAL_MAPPING_SUBJECT_NOT_FOUND_CODE;
|
|
26396
|
+
return {
|
|
26397
|
+
content: [{
|
|
26398
|
+
type: "text",
|
|
26399
|
+
text: isSubjectMissing ? `**Field:** subjectId
|
|
26400
|
+
**Detail:** ${subjectType} ${subjectId} was not found, or is outside your scope.
|
|
26401
|
+
**Action:** Check the ID, or list the subject first to confirm it exists.` : `Failed to add external mapping: ${message}`
|
|
26402
|
+
}],
|
|
26403
|
+
isError: true
|
|
26404
|
+
};
|
|
26405
|
+
}
|
|
26406
|
+
}
|
|
26407
|
+
);
|
|
26408
|
+
server.tool(
|
|
26409
|
+
"update_external_mapping",
|
|
26410
|
+
'Update an External Mapping \u2014 change the external key, URL, sync direction, or sync status. Use syncStatus "unlinked" when the external object no longer exists (the mapping is kept as evidence, not deleted).',
|
|
26411
|
+
{
|
|
26412
|
+
externalMappingId: external_exports.string().describe("The external mapping ID (e.g., em-abc123)"),
|
|
26413
|
+
externalKey: external_exports.string().min(1).max(500).optional().describe("New provider-specific key"),
|
|
26414
|
+
url: external_exports.string().url().nullable().optional().describe("New URL. Pass null to clear; omit to leave untouched."),
|
|
26415
|
+
syncDirection: external_exports.enum(SYNC_DIRECTION_ENUM).optional().describe("New recorded sync intent"),
|
|
26416
|
+
syncStatus: external_exports.enum(SYNC_STATUS_ENUM).optional().describe('New sync status. Use "unlinked" when the external object is gone \u2014 the mapping is kept as evidence.')
|
|
26417
|
+
},
|
|
26418
|
+
async ({ externalMappingId, externalKey, url, syncDirection, syncStatus }) => {
|
|
26419
|
+
const hasUpdates = externalKey !== void 0 || url !== void 0 || syncDirection !== void 0 || syncStatus !== void 0;
|
|
26420
|
+
if (!hasUpdates) {
|
|
26421
|
+
return {
|
|
26422
|
+
content: [{ type: "text", text: `No updates provided for external mapping: ${externalMappingId}` }],
|
|
26423
|
+
isError: true
|
|
26424
|
+
};
|
|
26425
|
+
}
|
|
26426
|
+
try {
|
|
26427
|
+
const em = await client.updateExternalMapping(externalMappingId, {
|
|
26428
|
+
...externalKey !== void 0 && { externalKey },
|
|
26429
|
+
...url !== void 0 && { url },
|
|
26430
|
+
...syncDirection !== void 0 && { syncDirection },
|
|
26431
|
+
...syncStatus !== void 0 && { syncStatus }
|
|
26432
|
+
});
|
|
26433
|
+
if (!em) {
|
|
26434
|
+
return {
|
|
26435
|
+
content: [{ type: "text", text: `External mapping not found: ${externalMappingId}` }],
|
|
26436
|
+
isError: true
|
|
26437
|
+
};
|
|
26438
|
+
}
|
|
26439
|
+
return {
|
|
26440
|
+
content: [
|
|
26441
|
+
{
|
|
26442
|
+
type: "text",
|
|
26443
|
+
text: `Updated external mapping: ${formatMapping(em)}`
|
|
26444
|
+
}
|
|
26445
|
+
]
|
|
26446
|
+
};
|
|
26447
|
+
} catch (err) {
|
|
26448
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
26449
|
+
return {
|
|
26450
|
+
content: [{ type: "text", text: `Failed to update external mapping: ${message}` }],
|
|
26451
|
+
isError: true
|
|
26452
|
+
};
|
|
26453
|
+
}
|
|
26454
|
+
}
|
|
26455
|
+
);
|
|
26456
|
+
server.tool(
|
|
26457
|
+
"list_external_mappings",
|
|
26458
|
+
"List all External Mappings for a subject (Project/Movement, Work Order, or Work Item). Shows where this entity is tracked in external systems. Use subjectType and subjectId from the entity you are inspecting.",
|
|
26459
|
+
{
|
|
26460
|
+
subjectType: external_exports.enum(SUBJECT_TYPE_ENUM).describe("The type of Harmonica entity: movement, work_order, or work_item"),
|
|
26461
|
+
subjectId: external_exports.string().min(1).max(128).describe("The ID of the subject entity")
|
|
26462
|
+
},
|
|
26463
|
+
async ({ subjectType, subjectId }) => {
|
|
26464
|
+
try {
|
|
26465
|
+
const mappings = await client.listExternalMappings(subjectType, subjectId);
|
|
26466
|
+
const text = mappings.length === 0 ? `No external mappings found for ${subjectType} ${subjectId}.` : mappings.map(formatMapping).join("\n");
|
|
26467
|
+
return { content: [{ type: "text", text }] };
|
|
26468
|
+
} catch (err) {
|
|
26469
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
26470
|
+
const isSubjectMissing = err instanceof Error && err.code === EXTERNAL_MAPPING_SUBJECT_NOT_FOUND_CODE;
|
|
26471
|
+
return {
|
|
26472
|
+
content: [{
|
|
26473
|
+
type: "text",
|
|
26474
|
+
text: isSubjectMissing ? `**Field:** subjectId
|
|
26475
|
+
**Detail:** ${subjectType} ${subjectId} was not found, or is outside your scope.
|
|
26476
|
+
**Action:** Check the ID, or list the subject first to confirm it exists.` : `Failed to list external mappings: ${message}`
|
|
26477
|
+
}],
|
|
26478
|
+
isError: true
|
|
26479
|
+
};
|
|
26480
|
+
}
|
|
26481
|
+
}
|
|
26482
|
+
);
|
|
26483
|
+
}
|
|
26484
|
+
|
|
26485
|
+
// ../../libs/harmonica-services/src/mcp/tools/feature-flag-tools.ts
|
|
26486
|
+
var STAGE_ENUM = external_exports.enum([
|
|
26487
|
+
"local",
|
|
26488
|
+
"staging",
|
|
26489
|
+
"staging-alpha",
|
|
26490
|
+
"staging-beta",
|
|
26491
|
+
"alpha",
|
|
26492
|
+
"beta",
|
|
26493
|
+
"prod"
|
|
26494
|
+
]);
|
|
26495
|
+
function registerFeatureFlagTools(server, ctx, client) {
|
|
26496
|
+
server.tool(
|
|
26497
|
+
"list_flags",
|
|
26498
|
+
"List feature flags and their enabled stages. Use check_flag to test a single flag against a specific stage, or get_flag for full detail on one flag.",
|
|
26499
|
+
{
|
|
26500
|
+
includeArchived: external_exports.boolean().optional().describe("Include archived flags. Default false"),
|
|
26501
|
+
search: external_exports.string().optional().describe("Case-insensitive search against flag name and description"),
|
|
26502
|
+
cursor: external_exports.string().optional().describe("Pagination cursor from a prior list_flags result"),
|
|
26503
|
+
limit: external_exports.number().int().min(1).max(100).optional().describe("Flags per page. Default 20")
|
|
26504
|
+
},
|
|
26505
|
+
async ({ includeArchived, search, cursor, limit }) => {
|
|
26506
|
+
try {
|
|
26507
|
+
const page = await client.listFlags({ includeArchived, search, cursor, limit });
|
|
26508
|
+
if (page.flags.length === 0) {
|
|
26509
|
+
return { content: [{ type: "text", text: "No flags found." }] };
|
|
26510
|
+
}
|
|
26511
|
+
return { content: [{ type: "text", text: formatFlagList(page.flags, page.total, page.nextCursor) }] };
|
|
26512
|
+
} catch (err) {
|
|
26513
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
26514
|
+
return { content: [{ type: "text", text: `Failed to list flags: ${message}` }], isError: true };
|
|
26515
|
+
}
|
|
26516
|
+
}
|
|
26517
|
+
);
|
|
26518
|
+
server.tool(
|
|
26519
|
+
"get_flag",
|
|
26520
|
+
"Get full detail for a single feature flag by name, including its enabled stages and optional change history.",
|
|
26521
|
+
{
|
|
26522
|
+
name: external_exports.string().describe("Flag name"),
|
|
26523
|
+
includeHistory: external_exports.boolean().optional().describe("Include change history entries. Default false")
|
|
26524
|
+
},
|
|
26525
|
+
async ({ name, includeHistory }) => {
|
|
26526
|
+
try {
|
|
26527
|
+
const flag = await client.getFlag(name, { includeHistory });
|
|
26528
|
+
if (!flag) {
|
|
26529
|
+
return { content: [{ type: "text", text: `Flag "${name}" not found.` }] };
|
|
26530
|
+
}
|
|
26531
|
+
return { content: [{ type: "text", text: formatFlag(flag, flag.history) }] };
|
|
26532
|
+
} catch (err) {
|
|
26533
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
26534
|
+
return { content: [{ type: "text", text: `Failed to get flag: ${message}` }], isError: true };
|
|
26535
|
+
}
|
|
26536
|
+
}
|
|
26537
|
+
);
|
|
26538
|
+
server.tool(
|
|
26539
|
+
"check_flag",
|
|
26540
|
+
"Check whether a feature flag is enabled in a specific stage. Returns a simple enabled/disabled answer.",
|
|
26541
|
+
{
|
|
26542
|
+
name: external_exports.string().describe("Flag name"),
|
|
26543
|
+
stage: STAGE_ENUM.describe("Deployment stage to check against")
|
|
26544
|
+
},
|
|
26545
|
+
async ({ name, stage }) => {
|
|
26546
|
+
try {
|
|
26547
|
+
const enabled = await client.checkFlag(name, stage);
|
|
26548
|
+
const status = enabled ? "enabled" : "disabled";
|
|
26549
|
+
return {
|
|
26550
|
+
content: [{
|
|
26551
|
+
type: "text",
|
|
26552
|
+
text: `Flag "${name}" is **${status}** in stage "${stage}".`
|
|
26553
|
+
}]
|
|
26554
|
+
};
|
|
26555
|
+
} catch (err) {
|
|
26556
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
26557
|
+
return { content: [{ type: "text", text: `Failed to check flag: ${message}` }], isError: true };
|
|
26558
|
+
}
|
|
26559
|
+
}
|
|
26560
|
+
);
|
|
26561
|
+
server.tool(
|
|
26562
|
+
"create_flag",
|
|
26563
|
+
"Create or update a feature flag with the specified enabled stages. Idempotent \u2014 calling with an existing flag name replaces its enabledIn list.",
|
|
26564
|
+
{
|
|
26565
|
+
name: external_exports.string().min(1).describe('Flag name (e.g. "rbac:enforcement")'),
|
|
26566
|
+
enabledIn: external_exports.array(STAGE_ENUM).describe("Stages to enable the flag in"),
|
|
26567
|
+
description: external_exports.string().optional().describe("Human-readable description of what this flag gates"),
|
|
26568
|
+
note: external_exports.string().optional().describe("Reason for this change (recorded in change history)")
|
|
26569
|
+
},
|
|
26570
|
+
async ({ name, enabledIn, description, note }) => {
|
|
26571
|
+
if (!ctx.user.email) {
|
|
26572
|
+
return { content: [{ type: "text", text: "Write operations require an authenticated user with a valid email." }], isError: true };
|
|
26573
|
+
}
|
|
26574
|
+
try {
|
|
26575
|
+
const flag = await client.createFlag(name, enabledIn, { description, note, updatedBy: ctx.user.email });
|
|
26576
|
+
return { content: [{ type: "text", text: formatFlag(flag) }] };
|
|
26577
|
+
} catch (err) {
|
|
26578
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
26579
|
+
return { content: [{ type: "text", text: `Failed to create flag: ${message}` }], isError: true };
|
|
26580
|
+
}
|
|
26581
|
+
}
|
|
26582
|
+
);
|
|
26583
|
+
server.tool(
|
|
26584
|
+
"set_flag",
|
|
26585
|
+
"Update which stages a feature flag is enabled in. Replaces the current enabledIn list entirely.",
|
|
26586
|
+
{
|
|
26587
|
+
name: external_exports.string().min(1).describe("Flag name"),
|
|
26588
|
+
enabledIn: external_exports.array(STAGE_ENUM).describe("New set of stages to enable the flag in. Pass [] to disable everywhere without archiving."),
|
|
26589
|
+
note: external_exports.string().optional().describe("Reason for this change (recorded in change history)")
|
|
26590
|
+
},
|
|
26591
|
+
async ({ name, enabledIn, note }) => {
|
|
26592
|
+
if (!ctx.user.email) {
|
|
26593
|
+
return { content: [{ type: "text", text: "Write operations require an authenticated user with a valid email." }], isError: true };
|
|
26594
|
+
}
|
|
26595
|
+
try {
|
|
26596
|
+
const flag = await client.setFlag(name, enabledIn, { note, updatedBy: ctx.user.email });
|
|
26597
|
+
return { content: [{ type: "text", text: formatFlag(flag) }] };
|
|
26598
|
+
} catch (err) {
|
|
26599
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
26600
|
+
return { content: [{ type: "text", text: `Failed to set flag: ${message}` }], isError: true };
|
|
26601
|
+
}
|
|
26602
|
+
}
|
|
26603
|
+
);
|
|
26604
|
+
server.tool(
|
|
26605
|
+
"archive_flag",
|
|
26606
|
+
"Archive a feature flag. Clears its enabledIn list and marks it archived so it cannot be re-enabled via set_flag. Use unarchive_flag to restore it.",
|
|
26607
|
+
{
|
|
26608
|
+
name: external_exports.string().min(1).describe("Flag name"),
|
|
26609
|
+
note: external_exports.string().optional().describe("Reason for archiving (recorded in change history)")
|
|
26610
|
+
},
|
|
26611
|
+
async ({ name, note }) => {
|
|
26612
|
+
if (!ctx.user.email) {
|
|
26613
|
+
return { content: [{ type: "text", text: "Write operations require an authenticated user with a valid email." }], isError: true };
|
|
26614
|
+
}
|
|
26615
|
+
try {
|
|
26616
|
+
const flag = await client.archiveFlag(name, { note, updatedBy: ctx.user.email });
|
|
26617
|
+
return { content: [{ type: "text", text: formatFlag(flag) }] };
|
|
26618
|
+
} catch (err) {
|
|
26619
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
26620
|
+
return { content: [{ type: "text", text: `Failed to archive flag: ${message}` }], isError: true };
|
|
26621
|
+
}
|
|
26622
|
+
}
|
|
26623
|
+
);
|
|
26624
|
+
server.tool(
|
|
26625
|
+
"unarchive_flag",
|
|
26626
|
+
"Restore an archived feature flag to operable state. The flag is re-enabled with an empty enabledIn list \u2014 use set_flag afterward to enable it in specific stages.",
|
|
26627
|
+
{
|
|
26628
|
+
name: external_exports.string().min(1).describe("Flag name"),
|
|
26629
|
+
note: external_exports.string().optional().describe("Reason for restoring (recorded in change history)")
|
|
26630
|
+
},
|
|
26631
|
+
async ({ name, note }) => {
|
|
26632
|
+
if (!ctx.user.email) {
|
|
26633
|
+
return { content: [{ type: "text", text: "Write operations require an authenticated user with a valid email." }], isError: true };
|
|
26634
|
+
}
|
|
26635
|
+
try {
|
|
26636
|
+
const flag = await client.unarchiveFlag(name, { note, updatedBy: ctx.user.email });
|
|
26637
|
+
return { content: [{ type: "text", text: formatFlag(flag) }] };
|
|
26638
|
+
} catch (err) {
|
|
26639
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
26640
|
+
return { content: [{ type: "text", text: `Failed to unarchive flag: ${message}` }], isError: true };
|
|
26641
|
+
}
|
|
26642
|
+
}
|
|
26643
|
+
);
|
|
26644
|
+
}
|
|
26645
|
+
function formatFlag(flag, history) {
|
|
26646
|
+
const archived = flag.archivedAt ? " **(archived)**" : "";
|
|
26647
|
+
const enabled = flag.enabledIn.length > 0 ? flag.enabledIn.join(", ") : "(none)";
|
|
26648
|
+
const parts = [
|
|
26649
|
+
`# Flag: ${flag.name}${archived}`,
|
|
26650
|
+
`**Scope:** ${flag.scope}`,
|
|
26651
|
+
`**Enabled in:** ${enabled}`,
|
|
26652
|
+
flag.description ? `**Description:** ${flag.description}` : null,
|
|
26653
|
+
`**Last updated:** ${flag.updatedAt.slice(0, 10)} by ${flag.updatedBy}`,
|
|
26654
|
+
flag.archivedAt ? `**Archived:** ${flag.archivedAt.slice(0, 10)} by ${flag.archivedBy}` : null
|
|
26655
|
+
].filter(Boolean).join("\n");
|
|
26656
|
+
if (!history?.length) return parts;
|
|
26657
|
+
const historyLines = history.map((h) => {
|
|
26658
|
+
const prev = h.previousEnabledIn.length > 0 ? h.previousEnabledIn.join(", ") : "(none)";
|
|
26659
|
+
const next = h.newEnabledIn.length > 0 ? h.newEnabledIn.join(", ") : "(none)";
|
|
26660
|
+
const note = h.note ? ` \u2014 ${h.note}` : "";
|
|
26661
|
+
return `- ${h.updatedAt.slice(0, 10)} ${h.updatedBy}: ${prev} \u2192 ${next}${note}`;
|
|
26662
|
+
}).join("\n");
|
|
26663
|
+
return `${parts}
|
|
26664
|
+
|
|
26665
|
+
## Change History
|
|
26666
|
+
${historyLines}`;
|
|
26667
|
+
}
|
|
26668
|
+
function formatFlagList(flags, total, nextCursor) {
|
|
26669
|
+
const rows = flags.map((f) => {
|
|
26670
|
+
const enabled = f.enabledIn.length > 0 ? f.enabledIn.join(", ") : "(none)";
|
|
26671
|
+
const archived = f.archivedAt ? " *(archived)*" : "";
|
|
26672
|
+
return `| ${f.name}${archived} | ${enabled} | ${f.updatedAt.slice(0, 10)} |`;
|
|
26673
|
+
});
|
|
26674
|
+
const table = [
|
|
26675
|
+
`# Feature Flags (${total} total, showing ${flags.length})`,
|
|
26676
|
+
"",
|
|
26677
|
+
"| Name | Enabled In | Updated |",
|
|
26678
|
+
"|---|---|---|",
|
|
26679
|
+
...rows
|
|
26680
|
+
].join("\n");
|
|
26681
|
+
if (!nextCursor) return table;
|
|
26682
|
+
return `${table}
|
|
26683
|
+
|
|
26684
|
+
More flags available. Pass \`cursor: "${nextCursor}"\` to list_flags to continue.`;
|
|
26685
|
+
}
|
|
26686
|
+
|
|
26687
|
+
// ../../libs/harmonica-services/src/mcp/tools/layer-tools.ts
|
|
26688
|
+
function registerLayerTools(server, ctx, client) {
|
|
26689
|
+
server.tool(
|
|
26690
|
+
"list_system_layers",
|
|
26691
|
+
"List the architectural Layers declared on a System (e.g. frontend, backend, data). Layers are an evaluation lens for plan/build quality checks, not a work container.",
|
|
26692
|
+
{
|
|
26693
|
+
systemId: external_exports.string().describe("The System ID")
|
|
26694
|
+
},
|
|
26695
|
+
async ({ systemId }) => {
|
|
26696
|
+
try {
|
|
26697
|
+
await assertSystemExists(client, systemId);
|
|
26698
|
+
} catch (err) {
|
|
26699
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
26700
|
+
return { content: [{ type: "text", text: `Access denied: ${message}` }], isError: true };
|
|
26701
|
+
}
|
|
26702
|
+
let layers;
|
|
26703
|
+
try {
|
|
26704
|
+
layers = await client.listSystemLayers(systemId);
|
|
26705
|
+
} catch (err) {
|
|
26706
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
26707
|
+
return { content: [{ type: "text", text: `Failed to list Layers: ${message}` }], isError: true };
|
|
26708
|
+
}
|
|
26709
|
+
const text = formatLayerList(layers);
|
|
26710
|
+
return { content: [{ type: "text", text }] };
|
|
26711
|
+
}
|
|
26712
|
+
);
|
|
26713
|
+
server.tool(
|
|
26714
|
+
"get_layer",
|
|
26715
|
+
"Get full details of a single Layer by ID, including its repo mapping and parent Layer (if it was split from one).",
|
|
26716
|
+
{
|
|
26717
|
+
systemId: external_exports.string().describe("The System ID (for access control)"),
|
|
26718
|
+
layerId: external_exports.string().describe("The Layer ID")
|
|
26719
|
+
},
|
|
26720
|
+
async ({ systemId, layerId }) => {
|
|
26721
|
+
try {
|
|
26722
|
+
await assertSystemExists(client, systemId);
|
|
26723
|
+
} catch (err) {
|
|
26724
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
26725
|
+
return { content: [{ type: "text", text: `Access denied: ${message}` }], isError: true };
|
|
26726
|
+
}
|
|
26727
|
+
let layer;
|
|
26728
|
+
try {
|
|
26729
|
+
layer = await client.getLayer(layerId);
|
|
26730
|
+
} catch (err) {
|
|
26731
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
26732
|
+
return { content: [{ type: "text", text: `Failed to get Layer: ${message}` }], isError: true };
|
|
26733
|
+
}
|
|
26734
|
+
if (!layer) {
|
|
26735
|
+
return { content: [{ type: "text", text: `Layer not found: "${layerId}"` }], isError: true };
|
|
26736
|
+
}
|
|
26737
|
+
if (layer.systemId !== systemId) {
|
|
26738
|
+
return { content: [{ type: "text", text: `Layer "${layerId}" does not belong to System "${systemId}".` }], isError: true };
|
|
26739
|
+
}
|
|
26740
|
+
const text = formatLayerDetail(layer);
|
|
26741
|
+
return { content: [{ type: "text", text }] };
|
|
26742
|
+
}
|
|
26743
|
+
);
|
|
26744
|
+
server.tool(
|
|
26745
|
+
"create_layer",
|
|
26746
|
+
"Declare a new architectural Layer on a System (e.g. frontend, backend, data). Layers are an evaluation lens for plan/build quality checks. layerKey must be a lowercase slug and unique per System.",
|
|
26747
|
+
{
|
|
26748
|
+
systemId: external_exports.string().describe("The System ID"),
|
|
26749
|
+
layerKey: external_exports.string().regex(/^[a-z]([a-z0-9-]*[a-z0-9])?$/, "layerKey must be a lowercase slug (letters, digits, hyphens; start with a letter, no trailing hyphen)").describe('Stable lowercase slug for the Layer role, e.g. "frontend" or "agent-workers"'),
|
|
26750
|
+
name: external_exports.string().describe('Display name, e.g. "Frontend"'),
|
|
26751
|
+
description: external_exports.string().optional().describe("Optional description of the Layer"),
|
|
26752
|
+
parentLayerId: external_exports.string().optional().describe("Optional parent Layer to nest under (must be a Layer on the same System)"),
|
|
26753
|
+
repoPatterns: external_exports.array(external_exports.string()).optional().describe("Optional repo path patterns owned by this Layer")
|
|
26754
|
+
},
|
|
26755
|
+
async ({ systemId, layerKey, name, description, parentLayerId, repoPatterns }) => {
|
|
26756
|
+
try {
|
|
26757
|
+
await assertSystemExists(client, systemId);
|
|
26758
|
+
} catch (err) {
|
|
26759
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
26760
|
+
return { content: [{ type: "text", text: `Access denied: ${message}` }], isError: true };
|
|
26761
|
+
}
|
|
26762
|
+
let layer;
|
|
26763
|
+
try {
|
|
26764
|
+
layer = await client.createSystemLayer({
|
|
26765
|
+
systemId,
|
|
26766
|
+
layerKey,
|
|
26767
|
+
name,
|
|
26768
|
+
...description !== void 0 && { description },
|
|
26769
|
+
...parentLayerId !== void 0 && { parentLayerId },
|
|
26770
|
+
...repoPatterns !== void 0 && { repoPatterns }
|
|
26771
|
+
});
|
|
26772
|
+
} catch (err) {
|
|
26773
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
26774
|
+
return { content: [{ type: "text", text: `Failed to create Layer: ${message}` }], isError: true };
|
|
26775
|
+
}
|
|
26776
|
+
if (!layer) {
|
|
26777
|
+
return { content: [{ type: "text", text: `System "${systemId}" not found or not writable.` }], isError: true };
|
|
26778
|
+
}
|
|
26779
|
+
return { content: [{ type: "text", text: `Created Layer.
|
|
26780
|
+
|
|
26781
|
+
${formatLayerDetail(layer)}` }] };
|
|
26782
|
+
}
|
|
26783
|
+
);
|
|
26784
|
+
server.tool(
|
|
26785
|
+
"update_layer",
|
|
26786
|
+
"Update a Layer's editable fields (name, description, repo mapping). Omitted fields are left unchanged. Archiving/reviving a Layer goes through transition_layer, not this tool. Requires write access to the System.",
|
|
26787
|
+
{
|
|
26788
|
+
systemId: external_exports.string().describe("The System ID the Layer belongs to (for access control)"),
|
|
26789
|
+
layerId: external_exports.string().describe("The Layer ID to update"),
|
|
26790
|
+
name: external_exports.string().optional().describe("New display name"),
|
|
26791
|
+
description: external_exports.string().optional().describe("New description"),
|
|
26792
|
+
repoPatterns: external_exports.array(external_exports.string()).optional().describe("Replacement repo mapping (replaces the existing list)")
|
|
26793
|
+
},
|
|
26794
|
+
async ({ systemId, layerId, name, description, repoPatterns }) => {
|
|
26795
|
+
try {
|
|
26796
|
+
await assertSystemExists(client, systemId);
|
|
26797
|
+
} catch (err) {
|
|
26798
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
26799
|
+
return { content: [{ type: "text", text: `Access denied: ${message}` }], isError: true };
|
|
26800
|
+
}
|
|
26801
|
+
let existing;
|
|
26802
|
+
try {
|
|
26803
|
+
existing = await client.getLayer(layerId);
|
|
26804
|
+
} catch (err) {
|
|
26805
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
26806
|
+
return { content: [{ type: "text", text: `Failed to load Layer: ${message}` }], isError: true };
|
|
26807
|
+
}
|
|
26808
|
+
if (!existing) return { content: [{ type: "text", text: `Layer not found: "${layerId}"` }], isError: true };
|
|
26809
|
+
if (existing.systemId !== systemId) {
|
|
26810
|
+
return { content: [{ type: "text", text: `Layer "${layerId}" does not belong to System "${systemId}".` }], isError: true };
|
|
26811
|
+
}
|
|
26812
|
+
let layer;
|
|
26813
|
+
try {
|
|
26814
|
+
layer = await client.updateSystemLayer(layerId, {
|
|
26815
|
+
...name !== void 0 && { name },
|
|
26816
|
+
...description !== void 0 && { description },
|
|
26817
|
+
...repoPatterns !== void 0 && { repoPatterns }
|
|
26818
|
+
});
|
|
26819
|
+
} catch (err) {
|
|
26820
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
26821
|
+
return { content: [{ type: "text", text: `Failed to update Layer: ${message}` }], isError: true };
|
|
26822
|
+
}
|
|
26823
|
+
if (!layer) return { content: [{ type: "text", text: `Layer not found: "${layerId}"` }], isError: true };
|
|
26824
|
+
return { content: [{ type: "text", text: `Updated Layer.
|
|
26825
|
+
|
|
26826
|
+
${formatLayerDetail(layer)}` }] };
|
|
26827
|
+
}
|
|
26828
|
+
);
|
|
26829
|
+
server.tool(
|
|
26830
|
+
"transition_layer",
|
|
26831
|
+
"Archive or revive a Layer. Archiving requires a reason (audit trail); reviving sets it back to active. Requires write access to the System.",
|
|
26832
|
+
{
|
|
26833
|
+
systemId: external_exports.string().describe("The System ID the Layer belongs to (for access control)"),
|
|
26834
|
+
layerId: external_exports.string().describe("The Layer ID to transition"),
|
|
26835
|
+
status: external_exports.enum(["active", "archived"]).describe('Target status: "archived" to retire, "active" to revive'),
|
|
26836
|
+
reason: external_exports.string().optional().describe('Why the Layer is being archived (required when status is "archived")')
|
|
26837
|
+
},
|
|
26838
|
+
async ({ systemId, layerId, status, reason }) => {
|
|
26839
|
+
try {
|
|
26840
|
+
await assertSystemExists(client, systemId);
|
|
26841
|
+
} catch (err) {
|
|
26842
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
26843
|
+
return { content: [{ type: "text", text: `Access denied: ${message}` }], isError: true };
|
|
26844
|
+
}
|
|
26845
|
+
let existing;
|
|
26846
|
+
try {
|
|
26847
|
+
existing = await client.getLayer(layerId);
|
|
26848
|
+
} catch (err) {
|
|
26849
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
26850
|
+
return { content: [{ type: "text", text: `Failed to load Layer: ${message}` }], isError: true };
|
|
26851
|
+
}
|
|
26852
|
+
if (!existing) return { content: [{ type: "text", text: `Layer not found: "${layerId}"` }], isError: true };
|
|
26853
|
+
if (existing.systemId !== systemId) {
|
|
26854
|
+
return { content: [{ type: "text", text: `Layer "${layerId}" does not belong to System "${systemId}".` }], isError: true };
|
|
26855
|
+
}
|
|
26856
|
+
let layer;
|
|
26857
|
+
try {
|
|
26858
|
+
layer = await client.setSystemLayerStatus(layerId, status, reason);
|
|
26859
|
+
} catch (err) {
|
|
26860
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
26861
|
+
return { content: [{ type: "text", text: `Failed to transition Layer: ${message}` }], isError: true };
|
|
26862
|
+
}
|
|
26863
|
+
if (!layer) return { content: [{ type: "text", text: `Layer not found: "${layerId}"` }], isError: true };
|
|
26864
|
+
return { content: [{ type: "text", text: `Layer ${status === "archived" ? "archived" : "revived"}.
|
|
26865
|
+
|
|
26866
|
+
${formatLayerDetail(layer)}` }] };
|
|
26867
|
+
}
|
|
26868
|
+
);
|
|
26869
|
+
}
|
|
26870
|
+
|
|
26871
|
+
// ../../libs/harmonica-services/src/mcp/tools/membership-tools.ts
|
|
26872
|
+
var orgIdArg = external_exports.string().describe("The organization ID \u2014 see list_organizations");
|
|
26873
|
+
function registerMembershipTools(server, ctx, client) {
|
|
26874
|
+
server.tool(
|
|
26875
|
+
"list_org_members",
|
|
26876
|
+
"List all members of an organization",
|
|
26877
|
+
{
|
|
26878
|
+
orgId: orgIdArg
|
|
26879
|
+
},
|
|
26880
|
+
async ({ orgId }) => {
|
|
26881
|
+
try {
|
|
26882
|
+
const members = await client.listOrgMembers(orgId);
|
|
26883
|
+
if (members.length === 0) {
|
|
26884
|
+
return { content: [{ type: "text", text: "No members found." }] };
|
|
26885
|
+
}
|
|
26886
|
+
const header = "| User ID | Name | Role | Title |";
|
|
26887
|
+
const divider = "|---------|------|------|-------|";
|
|
26888
|
+
const rows = members.map(
|
|
26889
|
+
(m) => `| ${m.userId} | ${m.name} | ${m.role} | ${m.title ?? "\u2014"} |`
|
|
26890
|
+
);
|
|
26891
|
+
return { content: [{ type: "text", text: [header, divider, ...rows].join("\n") }] };
|
|
26892
|
+
} catch (err) {
|
|
26893
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
26894
|
+
return { content: [{ type: "text", text: `Failed to list members: ${message}` }], isError: true };
|
|
26895
|
+
}
|
|
26896
|
+
}
|
|
26897
|
+
);
|
|
26898
|
+
server.tool(
|
|
26899
|
+
"add_member",
|
|
26900
|
+
"Grant a user access to an organization. Their name and avatar are filled in when they first sign in.",
|
|
26901
|
+
{
|
|
26902
|
+
orgId: orgIdArg,
|
|
26903
|
+
email: external_exports.string().email().describe("Email address (used as the user ID)"),
|
|
26904
|
+
role: external_exports.enum(["member", "admin", "client"]).optional().describe('Organization role (defaults to "member")')
|
|
26905
|
+
},
|
|
26906
|
+
async ({ orgId, email: email2, role }) => {
|
|
26907
|
+
try {
|
|
26908
|
+
const member = await client.addOrgMember(orgId, { email: email2, role });
|
|
26909
|
+
const lines = [
|
|
26910
|
+
"Member added successfully.",
|
|
26911
|
+
"",
|
|
26912
|
+
`**User ID:** ${member.userId}`,
|
|
26913
|
+
`**Email:** ${member.email}`,
|
|
26914
|
+
`**Name:** ${member.name}`,
|
|
26915
|
+
`**Role:** ${member.role}`,
|
|
26916
|
+
member.title ? `**Title:** ${member.title}` : ""
|
|
26917
|
+
].filter(Boolean);
|
|
26918
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
26919
|
+
} catch (err) {
|
|
26920
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
26921
|
+
return { content: [{ type: "text", text: `Failed to add member: ${message}` }], isError: true };
|
|
26922
|
+
}
|
|
26923
|
+
}
|
|
26924
|
+
);
|
|
26925
|
+
server.tool(
|
|
26926
|
+
"update_member_role",
|
|
26927
|
+
"Change a member's role within an organization.",
|
|
26928
|
+
{
|
|
26929
|
+
orgId: orgIdArg,
|
|
26930
|
+
userId: external_exports.string().describe("User ID (email address) of the member to update"),
|
|
26931
|
+
role: external_exports.enum(["member", "admin", "client"]).describe("New organization role")
|
|
26932
|
+
},
|
|
26933
|
+
async ({ orgId, userId, role }) => {
|
|
26934
|
+
try {
|
|
26935
|
+
const member = await client.updateOrgMember(orgId, userId, { role });
|
|
26936
|
+
if (!member) {
|
|
26937
|
+
return { content: [{ type: "text", text: `Member not found: "${userId}"` }], isError: true };
|
|
26938
|
+
}
|
|
26939
|
+
const lines = [
|
|
26940
|
+
"Member updated successfully.",
|
|
26941
|
+
"",
|
|
26942
|
+
`**User ID:** ${member.userId}`,
|
|
26943
|
+
`**Email:** ${member.email}`,
|
|
26944
|
+
`**Name:** ${member.name}`,
|
|
26945
|
+
`**Role:** ${member.role}`,
|
|
26946
|
+
member.title ? `**Title:** ${member.title}` : "",
|
|
26947
|
+
member.responsibility ? `**Responsibility:** ${member.responsibility}` : ""
|
|
26948
|
+
].filter(Boolean);
|
|
26949
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
26950
|
+
} catch (err) {
|
|
26951
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
26952
|
+
return { content: [{ type: "text", text: `Failed to update member: ${message}` }], isError: true };
|
|
26953
|
+
}
|
|
26954
|
+
}
|
|
26955
|
+
);
|
|
26956
|
+
server.tool(
|
|
26957
|
+
"remove_member",
|
|
26958
|
+
"Remove a user from an organization.",
|
|
26959
|
+
{
|
|
26960
|
+
orgId: orgIdArg,
|
|
26961
|
+
userId: external_exports.string().describe("User ID (email address) of the member to remove")
|
|
26962
|
+
},
|
|
26963
|
+
async ({ orgId, userId }) => {
|
|
26964
|
+
try {
|
|
26965
|
+
await client.removeOrgMember(orgId, userId);
|
|
26966
|
+
return { content: [{ type: "text", text: `Member "${userId}" has been removed from the organization.` }] };
|
|
26967
|
+
} catch (err) {
|
|
26968
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
26969
|
+
return { content: [{ type: "text", text: `Failed to remove member: ${message}` }], isError: true };
|
|
26970
|
+
}
|
|
26971
|
+
}
|
|
26972
|
+
);
|
|
26973
|
+
}
|
|
26974
|
+
|
|
26975
|
+
// ../../libs/harmonica-services/src/mcp/tools/movement-tools.ts
|
|
26976
|
+
function registerMovementTools(server, _ctx, client) {
|
|
26977
|
+
server.tool(
|
|
26978
|
+
"list_movements",
|
|
26979
|
+
'List all Movements for a Teamspace. A Movement is a Teamspace-scoped client-SOW container for an engagement. Recognition cues: "this SOW", "this engagement", "what contracts are active" \u2192 list_movements.',
|
|
26980
|
+
{
|
|
26981
|
+
teamspaceId: external_exports.string().describe("The teamspace ID")
|
|
26982
|
+
},
|
|
26983
|
+
async ({ teamspaceId }) => {
|
|
26984
|
+
const movements = await client.listTeamspaceMovements(teamspaceId);
|
|
26985
|
+
const text = movements.length === 0 ? "No movements found." : movements.map((m) => `[${m.movementId}] ${m.movementCode} \u2014 ${m.title} (${m.status})`).join("\n");
|
|
26986
|
+
return { content: [{ type: "text", text }] };
|
|
26987
|
+
}
|
|
26988
|
+
);
|
|
26989
|
+
server.tool(
|
|
26990
|
+
"get_movement",
|
|
26991
|
+
"Get a Movement by ID. Returns full details including status, approval timestamps, and audit stamps.",
|
|
26992
|
+
{
|
|
26993
|
+
movementId: external_exports.string().describe("The movement ID (e.g., mv-abc123)")
|
|
26994
|
+
},
|
|
26995
|
+
async ({ movementId }) => {
|
|
26996
|
+
const m = await client.getMovement(movementId);
|
|
26997
|
+
if (!m) {
|
|
26998
|
+
return { content: [{ type: "text", text: `Movement not found: ${movementId}` }] };
|
|
26999
|
+
}
|
|
27000
|
+
const lines = [
|
|
27001
|
+
`ID: ${m.movementId}`,
|
|
27002
|
+
`Code: ${m.movementCode}`,
|
|
27003
|
+
`Title: ${m.title}`,
|
|
27004
|
+
`Teamspace: ${m.teamspaceId}`,
|
|
27005
|
+
`Status: ${m.status}`,
|
|
27006
|
+
m.description ? `Description: ${m.description}` : null,
|
|
27007
|
+
m.proposedAt ? `Proposed: ${m.proposedAt}` : null,
|
|
27008
|
+
m.approvedAt ? `Approved: ${m.approvedAt}` : null,
|
|
27009
|
+
m.approvalNoteId ? `Approval Note: ${m.approvalNoteId}` : null,
|
|
27010
|
+
m.createdBy ? `Created By: ${m.createdBy}` : null,
|
|
27011
|
+
`Version: ${m.version}`,
|
|
27012
|
+
`Created: ${m.createdAt}`,
|
|
27013
|
+
`Updated: ${m.updatedAt}`
|
|
27014
|
+
].filter(Boolean).join("\n");
|
|
27015
|
+
return { content: [{ type: "text", text: lines }] };
|
|
27016
|
+
}
|
|
27017
|
+
);
|
|
27018
|
+
server.tool(
|
|
27019
|
+
"create_movement",
|
|
27020
|
+
'Create a Movement in a Teamspace. A Movement is a Teamspace-scoped client-SOW container for an engagement \u2014 it is NOT a Beat. Recognition cues: "new SOW", "create a contract scope", "start an engagement container" \u2192 create_movement.',
|
|
27021
|
+
{
|
|
27022
|
+
teamspaceId: external_exports.string().describe("The teamspace ID"),
|
|
27023
|
+
title: external_exports.string().min(1).max(300).describe('Movement title (e.g. "Q3 Platform Modernisation SOW")'),
|
|
27024
|
+
description: external_exports.string().max(2e3).optional().describe("Optional description"),
|
|
27025
|
+
accountId: external_exports.string().min(1).max(128).optional().describe("Optional Account this Movement rolls up to. Must belong to the same org as the Teamspace.")
|
|
27026
|
+
},
|
|
27027
|
+
async ({ teamspaceId, title, description, accountId }) => {
|
|
27028
|
+
const m = await client.createMovement({
|
|
27029
|
+
teamspaceId,
|
|
27030
|
+
title,
|
|
27031
|
+
...description !== void 0 && { description },
|
|
27032
|
+
...accountId !== void 0 && { accountId }
|
|
27033
|
+
});
|
|
27034
|
+
return {
|
|
27035
|
+
content: [
|
|
27036
|
+
{
|
|
27037
|
+
type: "text",
|
|
27038
|
+
text: `Created Movement: [${m.movementId}] ${m.movementCode} \u2014 ${m.title} (${m.status})`
|
|
27039
|
+
}
|
|
27040
|
+
]
|
|
27041
|
+
};
|
|
27042
|
+
}
|
|
27043
|
+
);
|
|
27044
|
+
server.tool(
|
|
27045
|
+
"update_movement",
|
|
27046
|
+
"Update mutable content fields on a Movement (title, description). To change status use transition_movement_status instead.",
|
|
27047
|
+
{
|
|
27048
|
+
movementId: external_exports.string().describe("The movement ID"),
|
|
27049
|
+
title: external_exports.string().min(1).max(300).optional().describe("New title"),
|
|
27050
|
+
description: external_exports.string().max(2e3).optional().describe("New description"),
|
|
27051
|
+
accountId: external_exports.string().max(128).nullable().optional().describe("Account this Movement rolls up to. Must belong to the same org. Pass null (or an empty string) to unlink.")
|
|
27052
|
+
},
|
|
27053
|
+
async ({ movementId, title, description, accountId }) => {
|
|
27054
|
+
const hasUpdates = title !== void 0 || description !== void 0 || accountId !== void 0;
|
|
27055
|
+
if (!hasUpdates) {
|
|
27056
|
+
return {
|
|
27057
|
+
content: [{ type: "text", text: `No updates provided for Movement: ${movementId}` }],
|
|
27058
|
+
isError: true
|
|
27059
|
+
};
|
|
27060
|
+
}
|
|
27061
|
+
const resolvedAccountId = accountId === "" ? null : accountId;
|
|
27062
|
+
try {
|
|
27063
|
+
const m = await client.updateMovement(movementId, {
|
|
27064
|
+
...title !== void 0 && { title },
|
|
27065
|
+
...description !== void 0 && { description },
|
|
27066
|
+
...accountId !== void 0 && { accountId: resolvedAccountId }
|
|
27067
|
+
});
|
|
27068
|
+
if (!m) {
|
|
27069
|
+
return {
|
|
27070
|
+
content: [{ type: "text", text: `Movement not found: ${movementId}` }],
|
|
27071
|
+
isError: true
|
|
27072
|
+
};
|
|
27073
|
+
}
|
|
27074
|
+
const account = accountId !== void 0 ? resolvedAccountId === null ? " \u2014 Account: (unlinked)" : ` \u2014 Account: ${resolvedAccountId}` : m.accountId ? ` \u2014 Account: ${m.accountId}` : "";
|
|
27075
|
+
return {
|
|
27076
|
+
content: [
|
|
27077
|
+
{
|
|
27078
|
+
type: "text",
|
|
27079
|
+
text: `Updated Movement: [${m.movementId}] ${m.movementCode} \u2014 ${m.title} (${m.status})${account}`
|
|
27080
|
+
}
|
|
27081
|
+
]
|
|
27082
|
+
};
|
|
27083
|
+
} catch (err) {
|
|
27084
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
27085
|
+
return {
|
|
27086
|
+
content: [{ type: "text", text: `Failed to update Movement: ${message}` }],
|
|
27087
|
+
isError: true
|
|
27088
|
+
};
|
|
27089
|
+
}
|
|
27090
|
+
}
|
|
27091
|
+
);
|
|
27092
|
+
server.tool(
|
|
27093
|
+
"transition_movement_status",
|
|
27094
|
+
'Transition a Movement through its commercial lifecycle: draft \u2192 proposed \u2192 approved \u2192 active \u2192 fulfilled. Cancelled is reachable from any non-terminal state and is how a superseded Movement is retired. Advancing to "approved" requires an approvalNoteId. Use confirm=false (default) to preview the transition without writing; confirm=true to execute.',
|
|
27095
|
+
{
|
|
27096
|
+
movementId: external_exports.string().describe("The movement ID"),
|
|
27097
|
+
targetState: external_exports.enum(["proposed", "approved", "active", "fulfilled", "cancelled"]).describe("Target lifecycle state"),
|
|
27098
|
+
approvalNoteId: external_exports.string().optional().describe('Required when transitioning to "approved" \u2014 the Note ID capturing the approval decision'),
|
|
27099
|
+
reason: external_exports.string().optional().describe("Reason for the transition. Strongly recommended for cancellation \u2014 retiring a Movement with no recorded reason leaves no audit trail"),
|
|
27100
|
+
confirm: external_exports.boolean().optional().default(false).describe("false (default) = dry-run preview; true = execute the transition")
|
|
27101
|
+
},
|
|
27102
|
+
async ({ movementId, targetState, approvalNoteId, reason, confirm }) => {
|
|
27103
|
+
if (!confirm) {
|
|
27104
|
+
return {
|
|
27105
|
+
content: [
|
|
27106
|
+
{
|
|
27107
|
+
type: "text",
|
|
27108
|
+
text: `Dry-run: would transition Movement ${movementId} \u2192 ${targetState}${approvalNoteId ? ` (approvalNoteId: ${approvalNoteId})` : ""}${reason ? ` \u2014 ${reason}` : ""}. Call again with confirm=true to execute.`
|
|
27109
|
+
}
|
|
27110
|
+
]
|
|
27111
|
+
};
|
|
27112
|
+
}
|
|
27113
|
+
try {
|
|
27114
|
+
const m = await client.transitionMovementStatus(movementId, targetState, {
|
|
27115
|
+
...approvalNoteId !== void 0 && { approvalNoteId },
|
|
27116
|
+
...reason !== void 0 && { reason }
|
|
27117
|
+
});
|
|
27118
|
+
if (!m) {
|
|
27119
|
+
return {
|
|
27120
|
+
content: [{ type: "text", text: `Movement not found: ${movementId}` }],
|
|
27121
|
+
isError: true
|
|
27122
|
+
};
|
|
27123
|
+
}
|
|
27124
|
+
return {
|
|
27125
|
+
content: [
|
|
27126
|
+
{
|
|
27127
|
+
type: "text",
|
|
27128
|
+
text: `Transitioned Movement: [${m.movementId}] ${m.movementCode} \u2014 ${m.title} \u2192 ${m.status}`
|
|
27129
|
+
}
|
|
27130
|
+
]
|
|
27131
|
+
};
|
|
27132
|
+
} catch (err) {
|
|
27133
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
27134
|
+
return {
|
|
27135
|
+
content: [{ type: "text", text: `Failed to transition Movement: ${message}` }],
|
|
27136
|
+
isError: true
|
|
27137
|
+
};
|
|
27138
|
+
}
|
|
27139
|
+
}
|
|
27140
|
+
);
|
|
27141
|
+
}
|
|
27142
|
+
|
|
27143
|
+
// ../../libs/harmonica-services/src/mcp/tools/next-actions-tools.ts
|
|
27144
|
+
var PRIORITY_ICONS = {
|
|
27145
|
+
P0: "\u{1F6A8}",
|
|
27146
|
+
P1: "\u{1F680}",
|
|
27147
|
+
P2: "\u{1F528}",
|
|
27148
|
+
P3: "\u{1F4CB}",
|
|
27149
|
+
P4: "\u{1F4A1}",
|
|
27150
|
+
P5: "\u2728"
|
|
27151
|
+
};
|
|
27152
|
+
function formatNextActions(actions) {
|
|
27153
|
+
if (actions.length === 0) {
|
|
27154
|
+
return "No actions found. Project is up to date.";
|
|
27155
|
+
}
|
|
27156
|
+
const lines = ["# Next Actions\n"];
|
|
27157
|
+
for (const action of actions) {
|
|
27158
|
+
const icon = PRIORITY_ICONS[action.priority] ?? "\u2022";
|
|
27159
|
+
lines.push(`## ${icon} ${action.priority} \u2014 ${action.category}`);
|
|
27160
|
+
lines.push(`**${action.title}**`);
|
|
27161
|
+
lines.push(action.description);
|
|
27162
|
+
if (action.entityId) {
|
|
27163
|
+
const parts = [`${action.entityType}: ${action.entityId}`];
|
|
27164
|
+
if (action.beatId) parts.push(`Beat: ${action.beatId}`);
|
|
27165
|
+
lines.push(parts.join(" | "));
|
|
27166
|
+
}
|
|
27167
|
+
if (action.count) {
|
|
27168
|
+
lines.push(`Count: ${action.count}`);
|
|
27169
|
+
}
|
|
27170
|
+
lines.push("");
|
|
27171
|
+
}
|
|
27172
|
+
return lines.join("\n");
|
|
27173
|
+
}
|
|
27174
|
+
function registerNextActionsTools(server, ctx, client) {
|
|
27175
|
+
server.tool(
|
|
27176
|
+
"get_next_actions",
|
|
27177
|
+
"Get prioritized list of recommended next actions for a system. Analyzes Beats, Revisions, and Notes to surface the highest-priority work: P0 (urgent bugs) \u2192 P1 (deploy) \u2192 P2 (build) \u2192 P3 (plan) \u2192 P4 (compose) \u2192 P5 (maintenance).",
|
|
27178
|
+
{
|
|
27179
|
+
systemId: external_exports.string().describe("The system ID"),
|
|
27180
|
+
limit: external_exports.coerce.number().optional().default(10).describe("Max actions to return")
|
|
27181
|
+
},
|
|
27182
|
+
async ({ systemId, limit }) => {
|
|
27183
|
+
try {
|
|
27184
|
+
await assertSystemExists(client, systemId);
|
|
27185
|
+
const actions = await client.getNextActions(systemId, { limit });
|
|
27186
|
+
return { content: [{ type: "text", text: formatNextActions(actions) }] };
|
|
27187
|
+
} catch (err) {
|
|
27188
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
27189
|
+
return { content: [{ type: "text", text: `Failed to get next actions: ${message}` }], isError: true };
|
|
27190
|
+
}
|
|
27191
|
+
}
|
|
27192
|
+
);
|
|
27193
|
+
server.tool(
|
|
27194
|
+
"list_triage_actions",
|
|
27195
|
+
"List active triage actions from the ledger (open + acted). This is the stateful view of what needs attention \u2014 includes status tracking, task progress, and escalation state. Use this instead of get_next_actions when triage has been run.",
|
|
27196
|
+
{
|
|
27197
|
+
systemId: external_exports.string().describe("The system ID")
|
|
27198
|
+
},
|
|
27199
|
+
async ({ systemId }) => {
|
|
27200
|
+
try {
|
|
27201
|
+
await assertSystemExists(client, systemId);
|
|
27202
|
+
const actions = await client.listTriageActions(systemId);
|
|
27203
|
+
return { content: [{ type: "text", text: formatTriageActions(actions) }] };
|
|
27204
|
+
} catch (err) {
|
|
27205
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
27206
|
+
return { content: [{ type: "text", text: `Failed to list triage actions: ${message}` }], isError: true };
|
|
27207
|
+
}
|
|
27208
|
+
}
|
|
27209
|
+
);
|
|
27210
|
+
server.tool(
|
|
27211
|
+
"dismiss_triage_action",
|
|
27212
|
+
'Dismiss a triage action (human decides "not now"). The action will not be re-escalated until the underlying state changes.',
|
|
27213
|
+
{
|
|
27214
|
+
systemId: external_exports.string().describe("The system ID"),
|
|
27215
|
+
actionKey: external_exports.string().describe("The action key to dismiss (from list_triage_actions)")
|
|
27216
|
+
},
|
|
27217
|
+
async ({ systemId, actionKey }) => {
|
|
27218
|
+
try {
|
|
27219
|
+
await assertSystemExists(client, systemId);
|
|
27220
|
+
await client.dismissTriageAction(systemId, actionKey, ctx.user?.userId ?? "mcp");
|
|
27221
|
+
return { content: [{ type: "text", text: `Action ${actionKey} dismissed.` }] };
|
|
27222
|
+
} catch (err) {
|
|
27223
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
27224
|
+
return { content: [{ type: "text", text: `Failed to dismiss action: ${message}` }], isError: true };
|
|
27225
|
+
}
|
|
27226
|
+
}
|
|
27227
|
+
);
|
|
27228
|
+
}
|
|
27229
|
+
var STATUS_ICONS = {
|
|
27230
|
+
open: "\u2B1A",
|
|
27231
|
+
acted: "\u25D0",
|
|
27232
|
+
dismissed: "\u2715",
|
|
27233
|
+
resolved: "\u2713"
|
|
27234
|
+
};
|
|
27235
|
+
function formatTriageActions(actions) {
|
|
27236
|
+
if (actions.length === 0) {
|
|
27237
|
+
return "No active triage actions. Run triage to populate the ledger.";
|
|
27238
|
+
}
|
|
27239
|
+
const lines = [`# Triage Actions (${actions.length})
|
|
27240
|
+
`];
|
|
27241
|
+
for (const action of actions) {
|
|
27242
|
+
const pIcon = PRIORITY_ICONS[action.priority] ?? "\u2022";
|
|
27243
|
+
const sIcon = STATUS_ICONS[action.status] ?? "?";
|
|
27244
|
+
lines.push(`## ${pIcon} ${action.priority} \u2014 ${action.category} ${sIcon} ${action.status.toUpperCase()}`);
|
|
27245
|
+
lines.push(`**${action.title}**`);
|
|
27246
|
+
lines.push(action.description);
|
|
27247
|
+
const meta = [];
|
|
27248
|
+
if (action.entityId) meta.push(`Entity: ${action.entityId}`);
|
|
27249
|
+
if (action.taskId) meta.push(`Task: ${action.taskId}${action.taskStatus ? ` (${action.taskStatus})` : ""}`);
|
|
27250
|
+
if (action.decision) meta.push(`Decision: ${action.decision}`);
|
|
27251
|
+
if (meta.length > 0) lines.push(meta.join(" | "));
|
|
27252
|
+
lines.push(`Action key: \`${action.actionKey}\` | First seen: ${action.firstSeenAt}`);
|
|
27253
|
+
lines.push("");
|
|
27254
|
+
}
|
|
27255
|
+
return lines.join("\n");
|
|
27256
|
+
}
|
|
27257
|
+
|
|
27278
27258
|
// ../../libs/harmonica-services/src/mcp/tools/note-element-tools.ts
|
|
27259
|
+
var import_crypto3 = require("crypto");
|
|
27279
27260
|
var parentKindSchema = external_exports.enum(["notebook", "note"]);
|
|
27280
27261
|
function formatEl(e) {
|
|
27281
27262
|
const binding = e.query ? `**Live collection:** ${e.query.noteType} @ ${e.query.scope} (limit ${e.query.limit}${e.query.sortBy ? `, ${e.query.sortBy}` : ""})` : `**Child Note:** ${e.childNoteId ?? "(none)"}`;
|
|
@@ -27930,7 +27911,13 @@ ${text}` }] };
|
|
|
27930
27911
|
try {
|
|
27931
27912
|
await assertSystemExists(client, systemId);
|
|
27932
27913
|
const note = await client.getNote(noteId);
|
|
27933
|
-
if (!note
|
|
27914
|
+
if (!note) {
|
|
27915
|
+
return { content: [{ type: "text", text: `Note not found: "${noteId}"` }], isError: true };
|
|
27916
|
+
}
|
|
27917
|
+
if (!note.systemId) {
|
|
27918
|
+
return { content: [{ type: "text", text: `Note "${noteId}" has no system identifier \u2014 record may need B-247 migration re-check.` }], isError: true };
|
|
27919
|
+
}
|
|
27920
|
+
if (note.systemId !== systemId) {
|
|
27934
27921
|
return { content: [{ type: "text", text: `Note not found: "${noteId}"` }], isError: true };
|
|
27935
27922
|
}
|
|
27936
27923
|
const removeReason = reason ?? "Removed via remove_note";
|
|
@@ -27957,7 +27944,13 @@ ${text}` }] };
|
|
|
27957
27944
|
try {
|
|
27958
27945
|
await assertSystemExists(client, systemId);
|
|
27959
27946
|
const note = await client.getNote(noteId);
|
|
27960
|
-
if (!note
|
|
27947
|
+
if (!note) {
|
|
27948
|
+
return { content: [{ type: "text", text: `Document not found: "${noteId}"` }], isError: true };
|
|
27949
|
+
}
|
|
27950
|
+
if (!note.systemId) {
|
|
27951
|
+
return { content: [{ type: "text", text: `Document "${noteId}" has no system identifier \u2014 record may need B-247 migration re-check.` }], isError: true };
|
|
27952
|
+
}
|
|
27953
|
+
if (note.systemId !== systemId) {
|
|
27961
27954
|
return { content: [{ type: "text", text: `Document not found: "${noteId}"` }], isError: true };
|
|
27962
27955
|
}
|
|
27963
27956
|
if (note.noteType !== "document") {
|
|
@@ -28020,7 +28013,7 @@ ${trimmed}` : `# ${title}`;
|
|
|
28020
28013
|
}
|
|
28021
28014
|
|
|
28022
28015
|
// ../../libs/harmonica-services/src/mcp/tools/notebook-tools.ts
|
|
28023
|
-
var SCOPE_TYPE_VALUES = ["org", "teamspace", "system", "account"
|
|
28016
|
+
var SCOPE_TYPE_VALUES = ["org", "teamspace", "system", "account"];
|
|
28024
28017
|
var scopeRefSchema = external_exports.object({
|
|
28025
28018
|
scopeType: external_exports.enum(SCOPE_TYPE_VALUES).describe("Scope type \u2014 org, teamspace, system, or account"),
|
|
28026
28019
|
scopeId: external_exports.string().min(1).describe("ID of the scope entity")
|
|
@@ -28226,7 +28219,7 @@ ${formatTree(tree.root)}` }] };
|
|
|
28226
28219
|
heading: external_exports.string().min(1).max(200).describe('Section label, e.g. "Decision Log"'),
|
|
28227
28220
|
parentNoteId: external_exports.string().optional().describe("Parent page's Note ID; defaults to the notebook root"),
|
|
28228
28221
|
noteType: external_exports.enum(["context", "constraint", "guidance", "decision", "document"]).optional().describe("Note type to collect (default: decision)"),
|
|
28229
|
-
systemId: external_exports.string().optional().describe("
|
|
28222
|
+
systemId: external_exports.string().optional().describe("System scope; if omitted, the Notebook's first bound system scope is used"),
|
|
28230
28223
|
limit: external_exports.number().int().min(1).max(500).optional().describe("Max members to show (default: 25)"),
|
|
28231
28224
|
order: external_exports.enum(["newest", "oldest"]).optional().describe("Sort order (default: newest)"),
|
|
28232
28225
|
significance: external_exports.enum(["strategic", "structural", "implementation"]).optional().describe("Only for decision notes \u2014 filter to a significance tier (e.g. strategic). Omit to include all.")
|
|
@@ -28244,16 +28237,25 @@ ${formatTree(tree.root)}` }] };
|
|
|
28244
28237
|
isError: true
|
|
28245
28238
|
};
|
|
28246
28239
|
}
|
|
28247
|
-
let
|
|
28248
|
-
if (!
|
|
28240
|
+
let scopeSystemId = systemId;
|
|
28241
|
+
if (!scopeSystemId) {
|
|
28242
|
+
let bindings;
|
|
28243
|
+
let bindingsFetchFailed = false;
|
|
28249
28244
|
try {
|
|
28250
|
-
|
|
28251
|
-
|
|
28245
|
+
bindings = await client.getNotebookScopeBindings(notebookId);
|
|
28246
|
+
scopeSystemId = bindings?.find((b) => b.scopeType === "system")?.scopeId;
|
|
28252
28247
|
} catch {
|
|
28248
|
+
bindingsFetchFailed = true;
|
|
28249
|
+
}
|
|
28250
|
+
if (!scopeSystemId) {
|
|
28251
|
+
if (!bindingsFetchFailed) {
|
|
28252
|
+
const hasLegacyProjectBinding = bindings?.some((b) => b.scopeType === "project");
|
|
28253
|
+
if (hasLegacyProjectBinding) {
|
|
28254
|
+
return { content: [{ type: "text", text: `Notebook "${notebookId}" has only a legacy 'project' scope binding \u2014 record may need B-247 migration re-check.` }], isError: true };
|
|
28255
|
+
}
|
|
28256
|
+
}
|
|
28257
|
+
return { content: [{ type: "text", text: "No system scope: pass systemId or bind the notebook to a system first." }], isError: true };
|
|
28253
28258
|
}
|
|
28254
|
-
}
|
|
28255
|
-
if (!scopeProjectId) {
|
|
28256
|
-
return { content: [{ type: "text", text: "No system scope: pass systemId or bind the notebook to a system first." }], isError: true };
|
|
28257
28259
|
}
|
|
28258
28260
|
let tree;
|
|
28259
28261
|
try {
|
|
@@ -28282,7 +28284,7 @@ ${formatTree(tree.root)}` }] };
|
|
|
28282
28284
|
parentId: placement.parentNoteId,
|
|
28283
28285
|
query: {
|
|
28284
28286
|
source: "notes",
|
|
28285
|
-
scope: `
|
|
28287
|
+
scope: `system:${scopeSystemId}`,
|
|
28286
28288
|
noteType: noteType ?? "decision",
|
|
28287
28289
|
limit: limit ?? 25,
|
|
28288
28290
|
sortBy: order === "oldest" ? "createdAt_asc" : "createdAt_desc",
|
|
@@ -28301,7 +28303,7 @@ ${formatTree(tree.root)}` }] };
|
|
|
28301
28303
|
return { content: [{ type: "text", text: `Failed to add collection: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
|
|
28302
28304
|
}
|
|
28303
28305
|
return {
|
|
28304
|
-
content: [{ type: "text", text: `Live collection added: **${heading}** (${noteType ?? "decision"} @
|
|
28306
|
+
content: [{ type: "text", text: `Live collection added: **${heading}** (${noteType ?? "decision"} @ system:${scopeSystemId}) under ${placement.parentNoteId} at depth ${placement.childDepth}, position ${position}.` }]
|
|
28305
28307
|
};
|
|
28306
28308
|
}
|
|
28307
28309
|
);
|
|
@@ -28314,7 +28316,7 @@ ${formatTree(tree.root)}` }] };
|
|
|
28314
28316
|
parentNoteId: external_exports.string().optional().describe("The page Note ID the section sits under; defaults to the notebook root"),
|
|
28315
28317
|
heading: external_exports.string().min(1).max(200).optional().describe("New section label"),
|
|
28316
28318
|
noteType: external_exports.enum(["context", "constraint", "guidance", "decision", "document"]).optional().describe("New note type to collect"),
|
|
28317
|
-
systemId: external_exports.string().optional().describe("New
|
|
28319
|
+
systemId: external_exports.string().optional().describe("New system scope"),
|
|
28318
28320
|
limit: external_exports.number().int().min(1).max(500).optional().describe("New max members to show"),
|
|
28319
28321
|
order: external_exports.enum(["newest", "oldest"]).optional().describe("New sort order"),
|
|
28320
28322
|
significance: external_exports.enum(["strategic", "structural", "implementation"]).optional().describe("New significance-tier filter (decision notes only)")
|
|
@@ -28356,9 +28358,14 @@ ${formatTree(tree.root)}` }] };
|
|
|
28356
28358
|
return { content: [{ type: "text", text: `Element "${elementId}" is not a live collection \u2014 its configuration cannot be edited here.` }], isError: true };
|
|
28357
28359
|
}
|
|
28358
28360
|
const base = existing.query;
|
|
28361
|
+
const normalizedScope = systemId ? `system:${systemId}` : base.scope.replace(/^project:/, "system:");
|
|
28362
|
+
const colonIdx = normalizedScope.indexOf(":");
|
|
28363
|
+
if (colonIdx <= 0 || colonIdx === normalizedScope.length - 1) {
|
|
28364
|
+
return { content: [{ type: "text", text: `Stored scope '${base.scope}' has no system id \u2014 pass an explicit systemId to re-bind this collection.` }], isError: true };
|
|
28365
|
+
}
|
|
28359
28366
|
const mergedQuery = {
|
|
28360
28367
|
source: "notes",
|
|
28361
|
-
scope:
|
|
28368
|
+
scope: normalizedScope,
|
|
28362
28369
|
noteType: noteType ?? base.noteType,
|
|
28363
28370
|
limit: limit ?? base.limit,
|
|
28364
28371
|
sortBy: order !== void 0 ? order === "oldest" ? "createdAt_asc" : "createdAt_desc" : base.sortBy,
|
|
@@ -28862,8 +28869,7 @@ Error: ${msg}`);
|
|
|
28862
28869
|
const createdTracks = [];
|
|
28863
28870
|
for (const trackInput of tracksToCreate) {
|
|
28864
28871
|
try {
|
|
28865
|
-
const track = await client.
|
|
28866
|
-
orgId,
|
|
28872
|
+
const track = await client.createTrack({
|
|
28867
28873
|
teamspaceId: teamspace.teamspaceId,
|
|
28868
28874
|
movementId: createdMovementId,
|
|
28869
28875
|
title: trackInput.title,
|
|
@@ -28871,7 +28877,7 @@ Error: ${msg}`);
|
|
|
28871
28877
|
vuRate: 0,
|
|
28872
28878
|
createdBy: ctx.user?.userId
|
|
28873
28879
|
});
|
|
28874
|
-
createdTracks.push({ id: track.
|
|
28880
|
+
createdTracks.push({ id: track.trackId, title: track.title });
|
|
28875
28881
|
} catch (err) {
|
|
28876
28882
|
const msg = err instanceof Error ? err.message : String(err);
|
|
28877
28883
|
lines.unshift(`\u26A0\uFE0F Onboarding batch partially created \u2014 stopped at: track:${trackInput.title}
|
|
@@ -28933,7 +28939,7 @@ async function pollForRootCodaCheck(client, taskId) {
|
|
|
28933
28939
|
consecutiveErrors = 0;
|
|
28934
28940
|
if (!task) throw new Error(`Task ${taskId} not found`);
|
|
28935
28941
|
if (task.status === "completed") {
|
|
28936
|
-
if (!task.result) throw new
|
|
28942
|
+
if (!task.result) throw new CheckResultPendingError(taskId);
|
|
28937
28943
|
const result = task.result;
|
|
28938
28944
|
if (!Array.isArray(result.dimensions) || typeof result.overallScore !== "number") {
|
|
28939
28945
|
throw new Error(`Task ${taskId} did not return a root Coda scorecard \u2014 was it a run_check task?`);
|
|
@@ -28967,7 +28973,7 @@ ${d.rationale}${d.suggestions?.length ? `
|
|
|
28967
28973
|
function registerOrganizationTools(server, ctx, client) {
|
|
28968
28974
|
server.tool(
|
|
28969
28975
|
"get_organization",
|
|
28970
|
-
"Get details for an organization by ID \u2014 returns name, slug, status, industry, and
|
|
28976
|
+
"Get details for an organization by ID \u2014 returns name, slug, status, industry, description, and coda",
|
|
28971
28977
|
{
|
|
28972
28978
|
orgId: external_exports.string().describe("The organization ID")
|
|
28973
28979
|
},
|
|
@@ -28982,7 +28988,9 @@ function registerOrganizationTools(server, ctx, client) {
|
|
|
28982
28988
|
`**Slug:** ${org.slug}`,
|
|
28983
28989
|
`**Status:** ${org.status}`,
|
|
28984
28990
|
org.industry ? `**Industry:** ${org.industry}` : "",
|
|
28985
|
-
org.description ? `**Description:** ${org.description}` : ""
|
|
28991
|
+
org.description ? `**Description:** ${org.description}` : "",
|
|
28992
|
+
org.coda ? `**Coda:**
|
|
28993
|
+
${org.coda}` : ""
|
|
28986
28994
|
].filter(Boolean);
|
|
28987
28995
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
28988
28996
|
}
|
|
@@ -29138,7 +29146,7 @@ function registerOrganizationTools(server, ctx, client) {
|
|
|
29138
29146
|
`Root Coda quality check enqueued for ${orgId}.`,
|
|
29139
29147
|
`Task ID: ${taskId}`,
|
|
29140
29148
|
"",
|
|
29141
|
-
`Use get_job_status with jobId="${taskId}" to
|
|
29149
|
+
`Use get_job_status with jobId="${taskId}" to view results \u2014 org-scoped checks are not listed by list_checks.`
|
|
29142
29150
|
].join("\n")
|
|
29143
29151
|
}]
|
|
29144
29152
|
};
|
|
@@ -29146,6 +29154,7 @@ function registerOrganizationTools(server, ctx, client) {
|
|
|
29146
29154
|
const check2 = await pollForRootCodaCheck(client, taskId);
|
|
29147
29155
|
return { content: [{ type: "text", text: formatRootCodaReport(check2) }] };
|
|
29148
29156
|
} catch (err) {
|
|
29157
|
+
if (err instanceof CheckResultPendingError) return renderCheckResultPending(err, []);
|
|
29149
29158
|
const message = err instanceof Error ? err.message : String(err);
|
|
29150
29159
|
return { content: [{ type: "text", text: `Root Coda quality check failed: ${message}` }], isError: true };
|
|
29151
29160
|
}
|
|
@@ -29181,7 +29190,7 @@ async function pollForCheck3(client, taskId) {
|
|
|
29181
29190
|
}
|
|
29182
29191
|
consecutiveErrors = 0;
|
|
29183
29192
|
if (task.status === "completed") {
|
|
29184
|
-
if (!task.result) throw new
|
|
29193
|
+
if (!task.result) throw new CheckResultPendingError(taskId);
|
|
29185
29194
|
return task.result;
|
|
29186
29195
|
}
|
|
29187
29196
|
if (task.status === "failed") throw new Error(`Beat version quality check failed: ${task.error ?? "unknown error"}`);
|
|
@@ -29242,6 +29251,7 @@ Use get_job_status with jobId="${task.taskId}" to poll for results, or list_chec
|
|
|
29242
29251
|
const check2 = await pollForCheck3(client, task.taskId);
|
|
29243
29252
|
return { content: [{ type: "text", text: formatScorecard2(check2) }] };
|
|
29244
29253
|
} catch (err) {
|
|
29254
|
+
if (err instanceof CheckResultPendingError) return renderCheckResultPending(err);
|
|
29245
29255
|
const message = err instanceof Error ? err.message : String(err);
|
|
29246
29256
|
return { content: [{ type: "text", text: `Beat version quality check failed: ${message}` }], isError: true };
|
|
29247
29257
|
}
|
|
@@ -29286,7 +29296,7 @@ async function pollForCheck4(client, taskId) {
|
|
|
29286
29296
|
consecutiveErrors = 0;
|
|
29287
29297
|
if (!task) throw new Error(`Task ${taskId} not found`);
|
|
29288
29298
|
if (task.status === "completed") {
|
|
29289
|
-
if (!task.result) throw new
|
|
29299
|
+
if (!task.result) throw new CheckResultPendingError(taskId);
|
|
29290
29300
|
return task.result;
|
|
29291
29301
|
}
|
|
29292
29302
|
if (task.status === "failed") throw new Error(`Portfolio coherence check failed: ${task.error ?? "unknown error"}`);
|
|
@@ -29390,6 +29400,7 @@ function registerPortfolioCoherenceTools(server, ctx, client) {
|
|
|
29390
29400
|
const check2 = await pollForCheck4(client, taskId);
|
|
29391
29401
|
return { content: [{ type: "text", text: formatReport2(check2) }] };
|
|
29392
29402
|
} catch (err) {
|
|
29403
|
+
if (err instanceof CheckResultPendingError) return renderCheckResultPending(err);
|
|
29393
29404
|
const message = err instanceof Error ? err.message : String(err);
|
|
29394
29405
|
return { content: [{ type: "text", text: `Portfolio coherence check failed: ${message}` }], isError: true };
|
|
29395
29406
|
}
|
|
@@ -29618,9 +29629,9 @@ function registerRevisionLifecycleTools(server, ctx, client) {
|
|
|
29618
29629
|
if (!beat) {
|
|
29619
29630
|
return { content: [{ type: "text", text: `Beat not found: "${beatId}"` }], isError: true };
|
|
29620
29631
|
}
|
|
29621
|
-
const beatSystemId = beat.systemId
|
|
29632
|
+
const beatSystemId = beat.systemId;
|
|
29622
29633
|
if (!beatSystemId) {
|
|
29623
|
-
return { content: [{ type: "text", text: `Beat "${beatId}" has no system identifier \u2014 record may
|
|
29634
|
+
return { content: [{ type: "text", text: `Beat "${beatId}" has no system identifier \u2014 record may need B-247 migration re-check.` }], isError: true };
|
|
29624
29635
|
}
|
|
29625
29636
|
let parentBv;
|
|
29626
29637
|
if (beatVersionId !== void 0) {
|
|
@@ -29628,9 +29639,9 @@ function registerRevisionLifecycleTools(server, ctx, client) {
|
|
|
29628
29639
|
if (!parentBv) {
|
|
29629
29640
|
return { content: [{ type: "text", text: `Beat Version not found: "${beatVersionId}"` }], isError: true };
|
|
29630
29641
|
}
|
|
29631
|
-
const bvSystemId = parentBv.systemId
|
|
29642
|
+
const bvSystemId = parentBv.systemId;
|
|
29632
29643
|
if (!bvSystemId) {
|
|
29633
|
-
return { content: [{ type: "text", text: `Beat Version "${beatVersionId}" has no system identifier \u2014 record may
|
|
29644
|
+
return { content: [{ type: "text", text: `Beat Version "${beatVersionId}" has no system identifier \u2014 record may need B-247 migration re-check.` }], isError: true };
|
|
29634
29645
|
}
|
|
29635
29646
|
if (parentBv.beatId !== beat.beatId || bvSystemId !== beatSystemId) {
|
|
29636
29647
|
return { content: [{ type: "text", text: `Beat Version "${beatVersionId}" belongs to a different Beat.` }], isError: true };
|
|
@@ -29739,13 +29750,13 @@ function registerRevisionLifecycleTools(server, ctx, client) {
|
|
|
29739
29750
|
if (!bv) {
|
|
29740
29751
|
return { content: [{ type: "text", text: `Beat Version not found: "${beatVersionId}"` }], isError: true };
|
|
29741
29752
|
}
|
|
29742
|
-
const bvSystemId = bv.systemId
|
|
29753
|
+
const bvSystemId = bv.systemId;
|
|
29743
29754
|
if (!bvSystemId) {
|
|
29744
|
-
return { content: [{ type: "text", text: `Beat Version "${beatVersionId}" has no system identifier \u2014 record may
|
|
29755
|
+
return { content: [{ type: "text", text: `Beat Version "${beatVersionId}" has no system identifier \u2014 record may need B-247 migration re-check.` }], isError: true };
|
|
29745
29756
|
}
|
|
29746
|
-
const revSystemId = revision.systemId
|
|
29757
|
+
const revSystemId = revision.systemId;
|
|
29747
29758
|
if (!revSystemId) {
|
|
29748
|
-
return { content: [{ type: "text", text: `Revision "${revisionId}" has no system identifier \u2014 record may
|
|
29759
|
+
return { content: [{ type: "text", text: `Revision "${revisionId}" has no system identifier \u2014 record may need B-247 migration re-check.` }], isError: true };
|
|
29749
29760
|
}
|
|
29750
29761
|
if (bv.beatId !== revision.beatId || bvSystemId !== revSystemId) {
|
|
29751
29762
|
return { content: [{ type: "text", text: `Beat Version "${beatVersionId}" belongs to a different Beat.` }], isError: true };
|
|
@@ -30149,7 +30160,7 @@ async function pollForCheck5(client, taskId) {
|
|
|
30149
30160
|
}
|
|
30150
30161
|
consecutiveErrors = 0;
|
|
30151
30162
|
if (task.status === "completed") {
|
|
30152
|
-
if (!task.result) throw new
|
|
30163
|
+
if (!task.result) throw new CheckResultPendingError(taskId);
|
|
30153
30164
|
return task.result;
|
|
30154
30165
|
}
|
|
30155
30166
|
if (task.status === "failed") throw new Error(`Revision quality check failed: ${task.error ?? "unknown error"}`);
|
|
@@ -30211,6 +30222,7 @@ Use get_job_status with jobId="${task.taskId}" to poll for results, or list_chec
|
|
|
30211
30222
|
const check2 = await pollForCheck5(client, task.taskId);
|
|
30212
30223
|
return { content: [{ type: "text", text: formatScorecard3(check2) }] };
|
|
30213
30224
|
} catch (err) {
|
|
30225
|
+
if (err instanceof CheckResultPendingError) return renderCheckResultPending(err);
|
|
30214
30226
|
const message = err instanceof Error ? err.message : String(err);
|
|
30215
30227
|
return { content: [{ type: "text", text: `Revision quality check failed: ${message}` }], isError: true };
|
|
30216
30228
|
}
|
|
@@ -30633,17 +30645,13 @@ function normalizeSnapshot(raw) {
|
|
|
30633
30645
|
accountDropIndex: raw.accountDropIndex ?? [],
|
|
30634
30646
|
deliverables: raw.deliverables ?? [],
|
|
30635
30647
|
teamspaceDeliverableIndex: raw.teamspaceDeliverableIndex ?? [],
|
|
30636
|
-
|
|
30637
|
-
systemDeliverableIndex: raw.systemDeliverableIndex ?? anyRaw["projectDeliverableIndex"] ?? [],
|
|
30648
|
+
systemDeliverableIndex: raw.systemDeliverableIndex ?? [],
|
|
30638
30649
|
teamspaces: raw.teamspaces ?? [],
|
|
30639
30650
|
teamspaceAccountIndex: raw.teamspaceAccountIndex ?? [],
|
|
30640
30651
|
accountTeamspaceIndex: raw.accountTeamspaceIndex ?? [],
|
|
30641
30652
|
layers: raw.layers ?? [],
|
|
30642
30653
|
layerIndex: raw.layerIndex ?? [],
|
|
30643
30654
|
tracks: raw.tracks ?? [],
|
|
30644
|
-
// Compat: early v4 snapshots (before field rename) have 'trackProjectIndex'
|
|
30645
|
-
trackSystemIndex: raw.trackSystemIndex ?? anyRaw["trackProjectIndex"] ?? [],
|
|
30646
|
-
trackTeamspaceIndex: raw.trackTeamspaceIndex ?? [],
|
|
30647
30655
|
workItems: raw.workItems ?? [],
|
|
30648
30656
|
workItemIndex: raw.workItemIndex ?? [],
|
|
30649
30657
|
accounts: raw.accounts ?? []
|
|
@@ -30735,10 +30743,7 @@ function assertSnapshotShape(val, sourceHint) {
|
|
|
30735
30743
|
|
|
30736
30744
|
// ../../libs/harmonica-services/src/mcp/tools/subscription-tools.ts
|
|
30737
30745
|
var ENTITY_TYPES = ["system", "beat", "revision"];
|
|
30738
|
-
var entityTypeField = external_exports.
|
|
30739
|
-
(v) => v === "project" ? "system" : v,
|
|
30740
|
-
external_exports.enum(ENTITY_TYPES)
|
|
30741
|
-
);
|
|
30746
|
+
var entityTypeField = external_exports.enum(ENTITY_TYPES);
|
|
30742
30747
|
function registerSubscriptionTools(server, ctx, client) {
|
|
30743
30748
|
server.tool(
|
|
30744
30749
|
"subscribe_to_entity",
|
|
@@ -31230,6 +31235,222 @@ function registerTeamspaceTools(server, ctx, client) {
|
|
|
31230
31235
|
);
|
|
31231
31236
|
}
|
|
31232
31237
|
|
|
31238
|
+
// ../../libs/harmonica-services/src/mcp/tools/track-tools.ts
|
|
31239
|
+
function registerTrackTools(server, ctx, client) {
|
|
31240
|
+
server.tool(
|
|
31241
|
+
"list_movement_tracks",
|
|
31242
|
+
`List the Tracks scoped to a Movement (client SOW). Returns the commitments grouped under that engagement. Recognition cues: "what's in this SOW", "this engagement's tracks" \u2192 list_movement_tracks.`,
|
|
31243
|
+
{
|
|
31244
|
+
movementId: external_exports.string().describe("The Movement ID (e.g., mv-abc123)")
|
|
31245
|
+
},
|
|
31246
|
+
async ({ movementId }) => {
|
|
31247
|
+
await assertMovementExists(client, movementId);
|
|
31248
|
+
const tracks = await client.listMovementTracks(movementId);
|
|
31249
|
+
const text = tracks.length === 0 ? "No tracks found for this Movement." : tracks.map(
|
|
31250
|
+
(t) => `[${t.trackId}] ${t.trackCode} \u2014 ${t.title} (${t.status})`
|
|
31251
|
+
).join("\n");
|
|
31252
|
+
return { content: [{ type: "text", text }] };
|
|
31253
|
+
}
|
|
31254
|
+
);
|
|
31255
|
+
server.tool(
|
|
31256
|
+
"get_track",
|
|
31257
|
+
"Get a Track by ID. Returns full details including commercial lifecycle status, VU rate, and audit stamps.",
|
|
31258
|
+
{
|
|
31259
|
+
trackId: external_exports.string().describe("The Track ID")
|
|
31260
|
+
},
|
|
31261
|
+
async ({ trackId }) => {
|
|
31262
|
+
const t = await client.getTrack(trackId);
|
|
31263
|
+
if (!t) {
|
|
31264
|
+
return {
|
|
31265
|
+
content: [{ type: "text", text: `Track not found: ${trackId}` }]
|
|
31266
|
+
};
|
|
31267
|
+
}
|
|
31268
|
+
const lines = [
|
|
31269
|
+
`ID: ${t.trackId}`,
|
|
31270
|
+
`Code: ${t.trackCode}`,
|
|
31271
|
+
`Title: ${t.title}`,
|
|
31272
|
+
`Teamspace: ${t.teamspaceId}`,
|
|
31273
|
+
t.movementId ? `Movement: ${t.movementId}` : null,
|
|
31274
|
+
`Status: ${t.status}`,
|
|
31275
|
+
`VU Rate: ${t.vuRate}`,
|
|
31276
|
+
t.description ? `Description: ${t.description}` : null,
|
|
31277
|
+
t.deliverables ? `Deliverables:
|
|
31278
|
+
${t.deliverables}` : null,
|
|
31279
|
+
t.acceptanceCriteria ? `Acceptance Criteria:
|
|
31280
|
+
${t.acceptanceCriteria}` : null,
|
|
31281
|
+
t.parentGroupId ? `Parent Track: ${t.parentGroupId}` : null,
|
|
31282
|
+
t.proposedAt ? `Proposed: ${t.proposedAt}` : null,
|
|
31283
|
+
t.approvedAt ? `Approved: ${t.approvedAt}` : null,
|
|
31284
|
+
`Version: ${t.version}`,
|
|
31285
|
+
`Created: ${t.createdAt}`,
|
|
31286
|
+
`Updated: ${t.updatedAt}`
|
|
31287
|
+
].filter(Boolean).join("\n");
|
|
31288
|
+
return { content: [{ type: "text", text: lines }] };
|
|
31289
|
+
}
|
|
31290
|
+
);
|
|
31291
|
+
const createTrackSchema = {
|
|
31292
|
+
teamspaceId: external_exports.string().describe("The teamspace ID"),
|
|
31293
|
+
title: external_exports.string().min(1).max(300).describe('Track title (e.g. "SOW-2026-Q2 / Platform Modernisation")'),
|
|
31294
|
+
description: external_exports.string().max(2e3).optional().describe("Optional description"),
|
|
31295
|
+
deliverables: external_exports.string().max(2e4).optional().describe("The Track's deliverables as Markdown bullet points (parallel to acceptanceCriteria)"),
|
|
31296
|
+
acceptanceCriteria: external_exports.string().max(2e4).optional().describe("The Track's acceptance criteria (Markdown), parallel to its deliverables"),
|
|
31297
|
+
vuRate: external_exports.number().nonnegative().describe("Value-Unit rate (billing rate for this engagement)"),
|
|
31298
|
+
movementId: external_exports.string().describe("The Movement (client SOW) this Track belongs to. Required: a Track is created on its Movement, which supplies the Teamspace (rev-1880d2f0)."),
|
|
31299
|
+
parentGroupId: external_exports.string().optional().describe("Optional parent Track ID for nested grouping (Phase 0 scaffolding)")
|
|
31300
|
+
};
|
|
31301
|
+
const createTrackHandler = async ({ teamspaceId, title, description, deliverables, acceptanceCriteria, vuRate, movementId, parentGroupId }) => {
|
|
31302
|
+
if (movementId !== void 0) {
|
|
31303
|
+
await assertMovementExists(client, movementId);
|
|
31304
|
+
}
|
|
31305
|
+
const t = await client.createTrack({
|
|
31306
|
+
teamspaceId,
|
|
31307
|
+
title,
|
|
31308
|
+
...description !== void 0 && { description },
|
|
31309
|
+
...deliverables !== void 0 && { deliverables },
|
|
31310
|
+
...acceptanceCriteria !== void 0 && { acceptanceCriteria },
|
|
31311
|
+
vuRate,
|
|
31312
|
+
...movementId !== void 0 && { movementId },
|
|
31313
|
+
...parentGroupId !== void 0 && { parentGroupId },
|
|
31314
|
+
createdBy: ctx.user.userId
|
|
31315
|
+
});
|
|
31316
|
+
return {
|
|
31317
|
+
content: [
|
|
31318
|
+
{
|
|
31319
|
+
type: "text",
|
|
31320
|
+
text: `Created Track: [${t.trackId}] ${t.trackCode} \u2014 ${t.title} (${t.status})`
|
|
31321
|
+
}
|
|
31322
|
+
]
|
|
31323
|
+
};
|
|
31324
|
+
};
|
|
31325
|
+
server.tool(
|
|
31326
|
+
"create_track",
|
|
31327
|
+
'Create a Track under a Movement. Use for scoped initiatives \u2014 client SOWs, internal phases, engagement containers \u2014 that collect Deliverables and Drops. Do NOT use for software capabilities the product provides to end users (those are Beats). Recognition cues: "this engagement", "this SOW", "this phase of work", "the client project", "this track" \u2192 Track.',
|
|
31328
|
+
createTrackSchema,
|
|
31329
|
+
createTrackHandler
|
|
31330
|
+
);
|
|
31331
|
+
server.tool(
|
|
31332
|
+
"update_track",
|
|
31333
|
+
"Update mutable content fields on a Track (title, description, deliverables, acceptanceCriteria, vuRate). To change commercial status use transition_track instead.",
|
|
31334
|
+
{
|
|
31335
|
+
trackId: external_exports.string().describe("The Track ID"),
|
|
31336
|
+
title: external_exports.string().min(1).max(300).optional().describe("New title"),
|
|
31337
|
+
description: external_exports.string().max(2e3).optional().describe("New description"),
|
|
31338
|
+
deliverables: external_exports.string().max(2e4).optional().describe(`Replace the Track's deliverables Markdown ("" clears)`),
|
|
31339
|
+
acceptanceCriteria: external_exports.string().max(2e4).optional().describe(`Replace the Track's acceptance-criteria Markdown ("" clears)`),
|
|
31340
|
+
vuRate: external_exports.number().nonnegative().optional().describe("New VU rate")
|
|
31341
|
+
},
|
|
31342
|
+
async ({ trackId, title, description, deliverables, acceptanceCriteria, vuRate }) => {
|
|
31343
|
+
const t = await client.updateTrack(trackId, {
|
|
31344
|
+
...title !== void 0 && { title },
|
|
31345
|
+
...description !== void 0 && { description },
|
|
31346
|
+
...deliverables !== void 0 && { deliverables },
|
|
31347
|
+
...acceptanceCriteria !== void 0 && { acceptanceCriteria },
|
|
31348
|
+
...vuRate !== void 0 && { vuRate }
|
|
31349
|
+
});
|
|
31350
|
+
if (!t) {
|
|
31351
|
+
return {
|
|
31352
|
+
content: [{ type: "text", text: `Track not found: ${trackId}` }]
|
|
31353
|
+
};
|
|
31354
|
+
}
|
|
31355
|
+
return {
|
|
31356
|
+
content: [
|
|
31357
|
+
{
|
|
31358
|
+
type: "text",
|
|
31359
|
+
text: `Updated Track: [${t.trackId}] ${t.trackCode} \u2014 ${t.title} (${t.status})`
|
|
31360
|
+
}
|
|
31361
|
+
]
|
|
31362
|
+
};
|
|
31363
|
+
}
|
|
31364
|
+
);
|
|
31365
|
+
server.tool(
|
|
31366
|
+
"transition_track",
|
|
31367
|
+
'Transition a Track through its commercial lifecycle: draft \u2192 proposed \u2192 approved \u2192 active \u2192 fulfilled. Cancelled is reachable from any non-terminal state. Advancing to "approved" requires an approvalNoteId. Use confirm=false (default) to preview the transition without writing; confirm=true to execute.',
|
|
31368
|
+
{
|
|
31369
|
+
trackId: external_exports.string().describe("The Track ID"),
|
|
31370
|
+
targetState: external_exports.enum(["proposed", "approved", "active", "fulfilled", "cancelled"]).describe("Target lifecycle state"),
|
|
31371
|
+
approvalNoteId: external_exports.string().optional().describe('Required when transitioning to "approved" \u2014 the Note ID capturing the approval decision'),
|
|
31372
|
+
reason: external_exports.string().optional().describe("Reason for the transition (especially useful for cancellation)"),
|
|
31373
|
+
confirm: external_exports.boolean().optional().default(false).describe("false (default) = dry-run preview; true = execute the transition")
|
|
31374
|
+
},
|
|
31375
|
+
async ({ trackId, targetState, approvalNoteId, reason, confirm }) => {
|
|
31376
|
+
if (!confirm) {
|
|
31377
|
+
return {
|
|
31378
|
+
content: [
|
|
31379
|
+
{
|
|
31380
|
+
type: "text",
|
|
31381
|
+
text: `Dry-run: would transition Track ${trackId} \u2192 ${targetState}${approvalNoteId ? ` (approvalNoteId: ${approvalNoteId})` : ""}${reason ? ` \u2014 ${reason}` : ""}. Call again with confirm=true to execute.`
|
|
31382
|
+
}
|
|
31383
|
+
]
|
|
31384
|
+
};
|
|
31385
|
+
}
|
|
31386
|
+
const t = await client.transitionTrackStatus(
|
|
31387
|
+
trackId,
|
|
31388
|
+
targetState,
|
|
31389
|
+
{
|
|
31390
|
+
...approvalNoteId !== void 0 && { approvalNoteId },
|
|
31391
|
+
...reason !== void 0 && { reason }
|
|
31392
|
+
}
|
|
31393
|
+
);
|
|
31394
|
+
if (!t) {
|
|
31395
|
+
return {
|
|
31396
|
+
content: [{ type: "text", text: `Track not found: ${trackId}` }]
|
|
31397
|
+
};
|
|
31398
|
+
}
|
|
31399
|
+
return {
|
|
31400
|
+
content: [
|
|
31401
|
+
{
|
|
31402
|
+
type: "text",
|
|
31403
|
+
text: `Transitioned Track [${t.trackId}] ${t.trackCode} \u2192 ${t.status}`
|
|
31404
|
+
}
|
|
31405
|
+
]
|
|
31406
|
+
};
|
|
31407
|
+
}
|
|
31408
|
+
);
|
|
31409
|
+
server.tool(
|
|
31410
|
+
"assign_drop_to_track",
|
|
31411
|
+
`Link a Drop to a Track so it appears on the parent Movement's timed-release axis (the Drops shown on the Movement overview). A Drop belongs to at most one group; linking moves it if it was in another group. Use to answer "put this release under this SOW/phase" or "add a Drop to this Movement".`,
|
|
31412
|
+
{
|
|
31413
|
+
trackId: external_exports.string().describe("The Track ID"),
|
|
31414
|
+
dropId: external_exports.string().describe("The Drop ID to link to the group")
|
|
31415
|
+
},
|
|
31416
|
+
async ({ trackId, dropId }) => {
|
|
31417
|
+
await fetchTrack(client, trackId);
|
|
31418
|
+
const drop = await client.getDrop(dropId);
|
|
31419
|
+
if (!drop) {
|
|
31420
|
+
return { content: [{ type: "text", text: `Drop not found: ${dropId}` }] };
|
|
31421
|
+
}
|
|
31422
|
+
await assertAccountExists(client, drop.accountId);
|
|
31423
|
+
const ok = await client.setDropTrack(trackId, dropId);
|
|
31424
|
+
if (!ok) {
|
|
31425
|
+
return { content: [{ type: "text", text: `Drop not found: ${dropId}` }] };
|
|
31426
|
+
}
|
|
31427
|
+
return {
|
|
31428
|
+
content: [{ type: "text", text: `Linked Drop ${dropId} to Track ${trackId}.` }]
|
|
31429
|
+
};
|
|
31430
|
+
}
|
|
31431
|
+
);
|
|
31432
|
+
server.tool(
|
|
31433
|
+
"list_track_beat_versions",
|
|
31434
|
+
'List the Beat Versions linked to a Track via its work items (Work Plan). Use to answer "what feature increments are scoped under this SOW/phase?"',
|
|
31435
|
+
{
|
|
31436
|
+
trackId: external_exports.string().describe("The Track ID")
|
|
31437
|
+
},
|
|
31438
|
+
async ({ trackId }) => {
|
|
31439
|
+
await fetchTrack(client, trackId);
|
|
31440
|
+
const workItems = await client.listTrackWorkItems(trackId);
|
|
31441
|
+
const bvIds = [...new Set(
|
|
31442
|
+
workItems.map((wi) => wi.beatVersionId).filter((id) => id !== void 0)
|
|
31443
|
+
)];
|
|
31444
|
+
if (bvIds.length === 0) {
|
|
31445
|
+
return { content: [{ type: "text", text: "No beat versions linked via work items in this track." }] };
|
|
31446
|
+
}
|
|
31447
|
+
const hydrated = await Promise.all(bvIds.map((id) => client.getBeatVersion(id)));
|
|
31448
|
+
const text = hydrated.filter((bv) => bv !== void 0).map((bv) => `[${bv.beatVersionId}] v${bv.versionNumber} \u2014 ${bv.title} (${bv.status})`).join("\n");
|
|
31449
|
+
return { content: [{ type: "text", text }] };
|
|
31450
|
+
}
|
|
31451
|
+
);
|
|
31452
|
+
}
|
|
31453
|
+
|
|
31233
31454
|
// ../../libs/harmonica-services/src/mcp/tools/value-velocity-tools.ts
|
|
31234
31455
|
var lensSchema = external_exports.enum(["velocity", "roi", "valuePrimary"]).default("velocity");
|
|
31235
31456
|
function resolveActorId(ctx) {
|
|
@@ -31608,6 +31829,7 @@ function registerWorkItemTools(server, _ctx, client) {
|
|
|
31608
31829
|
wi.costPerValueUnit !== void 0 ? `Cost per Value Unit: ${wi.costPerValueUnit}` : null,
|
|
31609
31830
|
wi.beatVersionId ? `Linked Beat Version: ${wi.beatVersionId}` : null,
|
|
31610
31831
|
wi.workOrderId ? `Work Order: ${wi.workOrderId}` : "Work Order: none (unattached)",
|
|
31832
|
+
wi.barId ? `Bar: ${wi.barId}` : "Bar: none (unplanned)",
|
|
31611
31833
|
estimateHint ? `Estimate Hint: ${estimateHint.hours}h (source: ${estimateHint.source})` : null,
|
|
31612
31834
|
wi.createdBy ? `Created By: ${wi.createdBy}` : null,
|
|
31613
31835
|
`Created: ${wi.createdAt}`,
|
|
@@ -31618,7 +31840,7 @@ function registerWorkItemTools(server, _ctx, client) {
|
|
|
31618
31840
|
);
|
|
31619
31841
|
server.tool(
|
|
31620
31842
|
"create_work_item",
|
|
31621
|
-
'Create a work item under a Track (DeliverableGroup). A work item is an owned, statused unit of operational work \u2014 it is NOT a Beat. Starts in "not_started" status. Optionally link it to a Beat Version for capability lineage (v2, N-4E09-6574) \u2014 the link is one-directional and never required. Optionally link it to a Work Order it delivers part of; leaving it unattached is a supported state, not a gap to fill (N-4E09-7160).',
|
|
31843
|
+
'Create a work item under a Track (DeliverableGroup). A work item is an owned, statused unit of operational work \u2014 it is NOT a Beat. Starts in "not_started" status. Optionally link it to a Beat Version for capability lineage (v2, N-4E09-6574) \u2014 the link is one-directional and never required. Optionally link it to a Work Order it delivers part of; leaving it unattached is a supported state, not a gap to fill (N-4E09-7160). Optionally link it to a Bar it is planned into; leaving it unplanned is likewise a real state, not a gap (B-298).',
|
|
31622
31844
|
{
|
|
31623
31845
|
trackId: external_exports.string().describe("The Track (DeliverableGroup) ID this work item belongs to"),
|
|
31624
31846
|
title: external_exports.string().min(1).max(300).describe("Work item title"),
|
|
@@ -31630,9 +31852,10 @@ function registerWorkItemTools(server, _ctx, client) {
|
|
|
31630
31852
|
costPerValueUnit: external_exports.number().nonnegative().optional().describe("Optional price per Value Unit"),
|
|
31631
31853
|
createdBy: external_exports.string().optional().describe("Optional creator identity for audit"),
|
|
31632
31854
|
beatVersionId: external_exports.string().min(1).optional().describe("Optional Beat Version to link for capability lineage. Must exist."),
|
|
31633
|
-
workOrderId: external_exports.string().min(1).max(128).optional().describe("Optional Work Order this item delivers part of. Must exist. Omit when there is no Order \u2014 unattached work is legitimate, so do not invent one.")
|
|
31855
|
+
workOrderId: external_exports.string().min(1).max(128).optional().describe("Optional Work Order this item delivers part of. Must exist. Omit when there is no Order \u2014 unattached work is legitimate, so do not invent one."),
|
|
31856
|
+
barId: external_exports.string().min(1).max(128).optional().describe("Optional Bar this item is planned into. Must exist, in this org. Omit when the work is not planned into a Bar yet \u2014 unplanned work is a real state, so do not reach for the current Bar to fill the field.")
|
|
31634
31857
|
},
|
|
31635
|
-
async ({ trackId, title, ownerName, ownerEmail, committedEstimateHours, effortSize, valueUnits, costPerValueUnit, createdBy, beatVersionId, workOrderId }) => {
|
|
31858
|
+
async ({ trackId, title, ownerName, ownerEmail, committedEstimateHours, effortSize, valueUnits, costPerValueUnit, createdBy, beatVersionId, workOrderId, barId }) => {
|
|
31636
31859
|
if (ownerName === void 0 !== (ownerEmail === void 0)) {
|
|
31637
31860
|
return {
|
|
31638
31861
|
content: [{ type: "text", text: "Setting the owner requires both ownerName and ownerEmail. Omit both to create an unowned work item." }],
|
|
@@ -31649,7 +31872,8 @@ function registerWorkItemTools(server, _ctx, client) {
|
|
|
31649
31872
|
...costPerValueUnit !== void 0 && { costPerValueUnit },
|
|
31650
31873
|
...createdBy !== void 0 && { createdBy },
|
|
31651
31874
|
...beatVersionId !== void 0 && { beatVersionId },
|
|
31652
|
-
...workOrderId !== void 0 && { workOrderId }
|
|
31875
|
+
...workOrderId !== void 0 && { workOrderId },
|
|
31876
|
+
...barId !== void 0 && { barId }
|
|
31653
31877
|
});
|
|
31654
31878
|
return {
|
|
31655
31879
|
content: [
|
|
@@ -31663,7 +31887,7 @@ function registerWorkItemTools(server, _ctx, client) {
|
|
|
31663
31887
|
);
|
|
31664
31888
|
server.tool(
|
|
31665
31889
|
"update_work_item",
|
|
31666
|
-
"Update mutable content fields on a work item (title, owner, committedEstimateHours, actualHours, effortSize, valueUnits, costPerValueUnit, beatVersionId, workOrderId). To move it between Tracks use move_work_item; to change its status use transition_work_item_status.",
|
|
31890
|
+
"Update mutable content fields on a work item (title, owner, committedEstimateHours, actualHours, effortSize, valueUnits, costPerValueUnit, beatVersionId, workOrderId, barId). To move it between Tracks use move_work_item; to change its status use transition_work_item_status.",
|
|
31667
31891
|
{
|
|
31668
31892
|
workItemId: external_exports.string().describe("The work item ID"),
|
|
31669
31893
|
title: external_exports.string().min(1).max(300).optional().describe("New title"),
|
|
@@ -31676,9 +31900,10 @@ function registerWorkItemTools(server, _ctx, client) {
|
|
|
31676
31900
|
valueUnits: external_exports.number().nonnegative().nullable().optional().describe("Value Units (commercial lens). Pass null to clear; omit to leave untouched."),
|
|
31677
31901
|
costPerValueUnit: external_exports.number().nonnegative().nullable().optional().describe("Price per Value Unit. Pass null to clear; omit to leave untouched."),
|
|
31678
31902
|
beatVersionId: external_exports.string().min(1).nullable().optional().describe("Link to a Beat Version for capability lineage. Pass null to unlink; omit to leave the current link untouched."),
|
|
31679
|
-
workOrderId: external_exports.string().min(1).max(128).nullable().optional().describe("Link to the Work Order this item delivers part of. Pass null to unlink, leaving the item unattached; omit to leave the current link untouched.")
|
|
31903
|
+
workOrderId: external_exports.string().min(1).max(128).nullable().optional().describe("Link to the Work Order this item delivers part of. Pass null to unlink, leaving the item unattached; omit to leave the current link untouched."),
|
|
31904
|
+
barId: external_exports.string().min(1).max(128).nullable().optional().describe("Link to the Bar this item is planned into. Pass null to unlink, leaving the item unplanned; omit to leave the current link untouched.")
|
|
31680
31905
|
},
|
|
31681
|
-
async ({ workItemId, title, ownerName, ownerEmail, unassignOwner, committedEstimateHours, actualHours, effortSize, valueUnits, costPerValueUnit, beatVersionId, workOrderId }) => {
|
|
31906
|
+
async ({ workItemId, title, ownerName, ownerEmail, unassignOwner, committedEstimateHours, actualHours, effortSize, valueUnits, costPerValueUnit, beatVersionId, workOrderId, barId }) => {
|
|
31682
31907
|
const hasOwnerUpdate = ownerName !== void 0 || ownerEmail !== void 0;
|
|
31683
31908
|
if (hasOwnerUpdate && (ownerName === void 0 || ownerEmail === void 0)) {
|
|
31684
31909
|
return {
|
|
@@ -31694,7 +31919,7 @@ function registerWorkItemTools(server, _ctx, client) {
|
|
|
31694
31919
|
}
|
|
31695
31920
|
const hasUpdates = title !== void 0 || hasOwnerUpdate || unassignOwner === true || committedEstimateHours !== void 0 || actualHours !== void 0 || effortSize !== void 0 || valueUnits !== void 0 || costPerValueUnit !== void 0 || beatVersionId !== void 0 || // `!== undefined`, never truthiness: an unlink arrives as `null`, and
|
|
31696
31921
|
// a truthy check here would report "no updates provided" and drop it.
|
|
31697
|
-
workOrderId !== void 0;
|
|
31922
|
+
workOrderId !== void 0 || barId !== void 0;
|
|
31698
31923
|
if (!hasUpdates) {
|
|
31699
31924
|
return {
|
|
31700
31925
|
content: [{ type: "text", text: `No updates provided for work item: ${workItemId}` }],
|
|
@@ -31711,7 +31936,8 @@ function registerWorkItemTools(server, _ctx, client) {
|
|
|
31711
31936
|
...valueUnits !== void 0 && { valueUnits },
|
|
31712
31937
|
...costPerValueUnit !== void 0 && { costPerValueUnit },
|
|
31713
31938
|
...beatVersionId !== void 0 && { beatVersionId },
|
|
31714
|
-
...workOrderId !== void 0 && { workOrderId }
|
|
31939
|
+
...workOrderId !== void 0 && { workOrderId },
|
|
31940
|
+
...barId !== void 0 && { barId }
|
|
31715
31941
|
});
|
|
31716
31942
|
if (!wi) {
|
|
31717
31943
|
return {
|
|
@@ -31800,19 +32026,33 @@ function oneLine(value) {
|
|
|
31800
32026
|
}
|
|
31801
32027
|
function formatOrderLine(o) {
|
|
31802
32028
|
const reason = o.needsDecisionReason ? ` [${o.needsDecisionReason}]` : "";
|
|
31803
|
-
|
|
32029
|
+
const track = o.trackId ? `on ${o.trackId}` : "unplaced";
|
|
32030
|
+
return `[${o.workOrderId}] ${oneLine(o.title)} \u2014 ${o.status}${reason}, ${o.size ?? "unsized"}, ${track}`;
|
|
31804
32031
|
}
|
|
31805
32032
|
function registerWorkOrderTools(server, _ctx, client) {
|
|
31806
32033
|
server.tool(
|
|
31807
32034
|
"list_track_work_orders",
|
|
31808
|
-
|
|
32035
|
+
"DEPRECATED \u2014 use list_movement_work_orders instead. A Work Order now belongs to its Request and Movement, not its Track. This alias returns a migration message for one minor release rather than failing outright.",
|
|
32036
|
+
// Optional on purpose: a caller that has already dropped the argument is
|
|
32037
|
+
// exactly the caller this alias exists to greet. Requiring it would answer
|
|
32038
|
+
// the migrated caller with a schema error instead of the migration message.
|
|
31809
32039
|
{
|
|
31810
|
-
trackId: external_exports.string().describe("
|
|
32040
|
+
trackId: external_exports.string().optional().describe("Ignored \u2014 the Track (DeliverableGroup) ID is no longer meaningful for Order scoping")
|
|
31811
32041
|
},
|
|
31812
|
-
async (
|
|
31813
|
-
|
|
31814
|
-
|
|
31815
|
-
|
|
32042
|
+
async () => {
|
|
32043
|
+
return {
|
|
32044
|
+
content: [
|
|
32045
|
+
{
|
|
32046
|
+
type: "text",
|
|
32047
|
+
text: "list_track_work_orders is deprecated. Use list_movement_work_orders(movementId) to fetch all Orders for an engagement \u2014 Orders are now scoped to their Movement, not their Track. If you have only a Track id, its Movement is on the Track itself: get_deliverable_group(trackId) returns movementId."
|
|
32048
|
+
}
|
|
32049
|
+
],
|
|
32050
|
+
// The caller asked for a list and is getting none. A result without
|
|
32051
|
+
// this flag reads as a successful empty listing to any client that
|
|
32052
|
+
// checks the field rather than the prose — the same reason every other
|
|
32053
|
+
// "your call did nothing" return in this file sets it.
|
|
32054
|
+
isError: true
|
|
32055
|
+
};
|
|
31816
32056
|
}
|
|
31817
32057
|
);
|
|
31818
32058
|
server.tool(
|
|
@@ -31856,18 +32096,16 @@ function registerWorkOrderTools(server, _ctx, client) {
|
|
|
31856
32096
|
);
|
|
31857
32097
|
server.tool(
|
|
31858
32098
|
"create_work_order",
|
|
31859
|
-
|
|
32099
|
+
`Split a Work Request into an agreed Work Order. Called once per piece: a request covering two separate lines of work becomes two orders. Starts in "requested" status \u2014 moving it past the coherence gate is transition_work_order_status. The Order's scope is derived from the Work Request's Movement; no Track is required.`,
|
|
31860
32100
|
{
|
|
31861
|
-
trackId: external_exports.string().describe("The Track (DeliverableGroup) this order is placed on"),
|
|
31862
32101
|
workRequestId: external_exports.string().min(1).describe("The Work Request this order was split from \u2014 immutable lineage"),
|
|
31863
32102
|
title: external_exports.string().min(1).max(300).describe("Work order title"),
|
|
31864
32103
|
description: external_exports.string().max(1e4).optional().describe("Optional detail beyond the title"),
|
|
31865
32104
|
size: external_exports.enum(WORK_ORDER_SIZE_ENUM).optional().describe("Optional t-shirt size. An order can arrive before it is sized; a request's composition counts unsized orders rather than hiding them."),
|
|
31866
32105
|
createdBy: external_exports.string().optional().describe("Optional creator identity for audit")
|
|
31867
32106
|
},
|
|
31868
|
-
async ({
|
|
32107
|
+
async ({ workRequestId, title, description, size, createdBy }) => {
|
|
31869
32108
|
const wo = await client.createWorkOrder({
|
|
31870
|
-
trackId,
|
|
31871
32109
|
workRequestId,
|
|
31872
32110
|
title,
|
|
31873
32111
|
...description !== void 0 && { description },
|
|
@@ -31879,9 +32117,21 @@ function registerWorkOrderTools(server, _ctx, client) {
|
|
|
31879
32117
|
};
|
|
31880
32118
|
}
|
|
31881
32119
|
);
|
|
32120
|
+
server.tool(
|
|
32121
|
+
"list_movement_work_orders",
|
|
32122
|
+
'List the Work Orders belonging to a Movement (engagement). A Work Order is the internal unit of agreed work \u2014 it is split from a Work Request and scoped to the Movement that owns the Request. Recognition cues: "what is committed on this engagement", "list the orders" \u2192 list_movement_work_orders.',
|
|
32123
|
+
{
|
|
32124
|
+
movementId: external_exports.string().describe("The Movement (engagement) ID")
|
|
32125
|
+
},
|
|
32126
|
+
async ({ movementId }) => {
|
|
32127
|
+
const orders = await client.listMovementWorkOrders(movementId);
|
|
32128
|
+
const text = orders.length === 0 ? "No work orders found." : orders.map(formatOrderLine).join("\n");
|
|
32129
|
+
return { content: [{ type: "text", text }] };
|
|
32130
|
+
}
|
|
32131
|
+
);
|
|
31882
32132
|
server.tool(
|
|
31883
32133
|
"update_work_order",
|
|
31884
|
-
"Update content fields on a Work Order (title, description, size). To
|
|
32134
|
+
"Update content fields on a Work Order (title, description, size). To change its status use transition_work_order_status. There is no moving an Order between Tracks \u2014 an Order belongs to its Work Request, and a Track is attribution carried by the Work Items (set workOrderId on them via update_work_item). The Work Request it came from cannot be changed \u2014 that lineage is immutable.",
|
|
31885
32135
|
{
|
|
31886
32136
|
workOrderId: external_exports.string().describe("The Work Order ID"),
|
|
31887
32137
|
title: external_exports.string().min(1).max(300).optional().describe("New title"),
|
|
@@ -31910,18 +32160,26 @@ function registerWorkOrderTools(server, _ctx, client) {
|
|
|
31910
32160
|
);
|
|
31911
32161
|
server.tool(
|
|
31912
32162
|
"move_work_order",
|
|
31913
|
-
"
|
|
32163
|
+
"DEPRECATED \u2014 this tool still responds but no longer moves anything. Work Orders no longer live on a Track; a Track is now attribution on the Work Item. To associate work with a Track, set workOrderId on the relevant Work Items via update_work_item instead.",
|
|
32164
|
+
// Both optional for the same reason as list_track_work_orders: the tool's
|
|
32165
|
+
// only job now is to deliver the migration message, so it must not refuse
|
|
32166
|
+
// the callers who have already stopped sending these.
|
|
31914
32167
|
{
|
|
31915
|
-
workOrderId: external_exports.string().describe("
|
|
31916
|
-
newTrackId: external_exports.string().describe("
|
|
32168
|
+
workOrderId: external_exports.string().optional().describe("Ignored \u2014 a Work Order is no longer moveable between Tracks"),
|
|
32169
|
+
newTrackId: external_exports.string().optional().describe("Ignored \u2014 a Track (DeliverableGroup) is no longer a valid parent for Orders")
|
|
31917
32170
|
},
|
|
31918
|
-
async (
|
|
31919
|
-
const wo = await client.moveWorkOrder(workOrderId, newTrackId);
|
|
31920
|
-
if (!wo) {
|
|
31921
|
-
return { content: [{ type: "text", text: `Work order not found: ${workOrderId}` }] };
|
|
31922
|
-
}
|
|
32171
|
+
async () => {
|
|
31923
32172
|
return {
|
|
31924
|
-
content: [
|
|
32173
|
+
content: [
|
|
32174
|
+
{
|
|
32175
|
+
type: "text",
|
|
32176
|
+
text: "move_work_order is deprecated and has no effect. Work Orders are no longer scoped to a Track. To assign work to a Track, set workOrderId on the relevant Work Items via update_work_item."
|
|
32177
|
+
}
|
|
32178
|
+
],
|
|
32179
|
+
// "Has no effect" must be machine-readable, not only prose: without
|
|
32180
|
+
// this, a client that moved an Order and checked the result field
|
|
32181
|
+
// records the move as done.
|
|
32182
|
+
isError: true
|
|
31925
32183
|
};
|
|
31926
32184
|
}
|
|
31927
32185
|
);
|
|
@@ -32606,7 +32864,7 @@ function registerAllTools(server, ctx, client, profile) {
|
|
|
32606
32864
|
registerSubscriptionTools(server, ctx, client);
|
|
32607
32865
|
registerBaselineTools(server, ctx, client);
|
|
32608
32866
|
registerTeamspaceTools(server, ctx, client);
|
|
32609
|
-
|
|
32867
|
+
registerTrackTools(server, ctx, client);
|
|
32610
32868
|
registerDropTools(server, ctx, client);
|
|
32611
32869
|
registerSessionTools(server, ctx, client);
|
|
32612
32870
|
registerOnboardingTools(server, ctx, client);
|
|
@@ -32614,6 +32872,7 @@ function registerAllTools(server, ctx, client, profile) {
|
|
|
32614
32872
|
registerNotebookTools(server, ctx, client);
|
|
32615
32873
|
registerAccountTools(server, ctx, client);
|
|
32616
32874
|
registerMovementTools(server, ctx, client);
|
|
32875
|
+
registerExternalMappingTools(server, ctx, client);
|
|
32617
32876
|
registerWorkItemTools(server, ctx, client);
|
|
32618
32877
|
registerWorkRequestTools(server, ctx, client);
|
|
32619
32878
|
registerWorkOrderTools(server, ctx, client);
|
|
@@ -32728,6 +32987,13 @@ var ApiError = class extends Error {
|
|
|
32728
32987
|
this.name = "ApiError";
|
|
32729
32988
|
}
|
|
32730
32989
|
};
|
|
32990
|
+
var ExternalMappingSubjectNotFoundError = class extends Error {
|
|
32991
|
+
code = EXTERNAL_MAPPING_SUBJECT_NOT_FOUND_CODE;
|
|
32992
|
+
constructor(subjectType, subjectId) {
|
|
32993
|
+
super(`Subject ${subjectType} ${subjectId} not found`);
|
|
32994
|
+
this.name = "ExternalMappingSubjectNotFoundError";
|
|
32995
|
+
}
|
|
32996
|
+
};
|
|
32731
32997
|
function bodyCode(body) {
|
|
32732
32998
|
try {
|
|
32733
32999
|
const parsed = JSON.parse(body);
|
|
@@ -34210,60 +34476,46 @@ function createHttpClient(config2) {
|
|
|
34210
34476
|
updateTeamspace: async (teamspaceId, updates) => {
|
|
34211
34477
|
return request("PATCH", `/api/teamspaces/${encodeURIComponent(teamspaceId)}`, updates);
|
|
34212
34478
|
},
|
|
34213
|
-
//
|
|
34214
|
-
|
|
34215
|
-
const res = await request("GET", `/api/
|
|
34216
|
-
if (res === void 0) throw new Error(`Teamspace ${teamspaceId} not found`);
|
|
34217
|
-
return res.deliverableGroups;
|
|
34218
|
-
},
|
|
34219
|
-
listSystemDeliverableGroups: async (systemId) => {
|
|
34220
|
-
const res = await request("GET", `/api/systems/${encodeURIComponent(systemId)}/deliverable-groups`);
|
|
34221
|
-
if (res === void 0) throw new Error(`Project ${systemId} not found`);
|
|
34222
|
-
return res.deliverableGroups;
|
|
34223
|
-
},
|
|
34224
|
-
listMovementDeliverableGroups: async (movementId) => {
|
|
34225
|
-
const res = await request("GET", `/api/movements/${encodeURIComponent(movementId)}/deliverable-groups`);
|
|
34479
|
+
// Tracks
|
|
34480
|
+
listMovementTracks: async (movementId) => {
|
|
34481
|
+
const res = await request("GET", `/api/movements/${encodeURIComponent(movementId)}/tracks`);
|
|
34226
34482
|
if (res === void 0) throw new Error(`Movement ${movementId} not found`);
|
|
34227
|
-
return res.
|
|
34483
|
+
return res.tracks;
|
|
34228
34484
|
},
|
|
34229
|
-
|
|
34230
|
-
return request("GET", `/api/
|
|
34485
|
+
getTrack: async (trackId) => {
|
|
34486
|
+
return request("GET", `/api/tracks/${encodeURIComponent(trackId)}`);
|
|
34231
34487
|
},
|
|
34232
|
-
|
|
34233
|
-
const { title, description, deliverables, acceptanceCriteria, vuRate, movementId, parentGroupId, createdBy } = input;
|
|
34488
|
+
createTrack: async (input) => {
|
|
34489
|
+
const { title, description, deliverables, acceptanceCriteria, vuRate, movementId, parentGroupId, createdBy, teamspaceId } = input;
|
|
34234
34490
|
const body = {
|
|
34235
34491
|
title,
|
|
34236
34492
|
...description !== void 0 && { description },
|
|
34237
34493
|
...deliverables !== void 0 && { deliverables },
|
|
34238
34494
|
...acceptanceCriteria !== void 0 && { acceptanceCriteria },
|
|
34239
34495
|
vuRate,
|
|
34240
|
-
// movementId rides in the body (not the route) so it composes with the
|
|
34241
|
-
// project route when a group is both project- and movement-scoped.
|
|
34242
34496
|
...movementId !== void 0 && { movementId },
|
|
34243
34497
|
...parentGroupId !== void 0 && { parentGroupId },
|
|
34244
34498
|
...createdBy !== void 0 && { createdBy }
|
|
34245
34499
|
};
|
|
34246
|
-
|
|
34247
|
-
|
|
34500
|
+
if (!movementId) {
|
|
34501
|
+
throw new Error(`Cannot create Track "${title}": a movementId is required (teamspaceId ${teamspaceId} alone has no create route).`);
|
|
34502
|
+
}
|
|
34503
|
+
const url2 = `/api/movements/${encodeURIComponent(movementId)}/tracks`;
|
|
34248
34504
|
const result = await request("POST", url2, body);
|
|
34249
|
-
if (result === void 0) throw new Error(`Resource not found when creating
|
|
34505
|
+
if (result === void 0) throw new Error(`Resource not found when creating Track`);
|
|
34250
34506
|
return result;
|
|
34251
34507
|
},
|
|
34252
|
-
|
|
34253
|
-
return request("PATCH", `/api/
|
|
34508
|
+
updateTrack: async (trackId, updates) => {
|
|
34509
|
+
return request("PATCH", `/api/tracks/${encodeURIComponent(trackId)}`, updates);
|
|
34254
34510
|
},
|
|
34255
|
-
|
|
34256
|
-
return request("PATCH", `/api/
|
|
34511
|
+
transitionTrackStatus: async (trackId, targetState, opts) => {
|
|
34512
|
+
return request("PATCH", `/api/tracks/${encodeURIComponent(trackId)}/status`, {
|
|
34257
34513
|
targetState,
|
|
34258
34514
|
...opts ?? {}
|
|
34259
34515
|
});
|
|
34260
34516
|
},
|
|
34261
|
-
|
|
34262
|
-
const res = await request("
|
|
34263
|
-
return res?.drops ?? [];
|
|
34264
|
-
},
|
|
34265
|
-
setDropDeliverableGroup: async (deliverableGroupId, dropId) => {
|
|
34266
|
-
const res = await request("POST", `/api/deliverable-groups/${encodeURIComponent(deliverableGroupId)}/drops`, { dropId });
|
|
34517
|
+
setDropTrack: async (trackId, dropId) => {
|
|
34518
|
+
const res = await request("POST", `/api/tracks/${encodeURIComponent(trackId)}/drops`, { dropId });
|
|
34267
34519
|
return res?.success === true;
|
|
34268
34520
|
},
|
|
34269
34521
|
// Movements (B-242 / teamspace-scoped SOW container)
|
|
@@ -34311,9 +34563,55 @@ function createHttpClient(config2) {
|
|
|
34311
34563
|
if (!parsed.success) throw new Error(`Unexpected movements list shape from API: ${parsed.error.message}`);
|
|
34312
34564
|
return parsed.data.movements;
|
|
34313
34565
|
},
|
|
34566
|
+
// ExternalMappings (reference-only links to external trackers — B-301 / N-4E09-6311)
|
|
34567
|
+
addExternalMapping: async (input) => {
|
|
34568
|
+
const { subjectType, subjectId, provider, externalKey, url: url2, syncDirection } = input;
|
|
34569
|
+
const body = {
|
|
34570
|
+
subjectType,
|
|
34571
|
+
subjectId,
|
|
34572
|
+
provider,
|
|
34573
|
+
externalKey,
|
|
34574
|
+
...url2 !== void 0 && { url: url2 },
|
|
34575
|
+
...syncDirection !== void 0 && { syncDirection }
|
|
34576
|
+
};
|
|
34577
|
+
let res;
|
|
34578
|
+
try {
|
|
34579
|
+
res = await request("POST", `/api/external-mappings`, body);
|
|
34580
|
+
} catch (err) {
|
|
34581
|
+
if (err instanceof ApiError && err.status === 400 && bodyCode(err.body) === EXTERNAL_MAPPING_SUBJECT_NOT_FOUND_CODE) {
|
|
34582
|
+
throw new ExternalMappingSubjectNotFoundError(subjectType, subjectId);
|
|
34583
|
+
}
|
|
34584
|
+
throw err;
|
|
34585
|
+
}
|
|
34586
|
+
if (res === void 0) throw new ExternalMappingSubjectNotFoundError(subjectType, subjectId);
|
|
34587
|
+
const parsed = ExternalMappingApiSchema.safeParse(res.externalMapping);
|
|
34588
|
+
if (!parsed.success) throw new Error(`Unexpected ExternalMapping shape from API: ${parsed.error.message}`);
|
|
34589
|
+
return parsed.data;
|
|
34590
|
+
},
|
|
34591
|
+
updateExternalMapping: async (externalMappingId, updates) => {
|
|
34592
|
+
const res = await request(
|
|
34593
|
+
"PATCH",
|
|
34594
|
+
`/api/external-mappings/${encodeURIComponent(externalMappingId)}`,
|
|
34595
|
+
updates
|
|
34596
|
+
);
|
|
34597
|
+
if (res === void 0) return void 0;
|
|
34598
|
+
const parsed = ExternalMappingApiSchema.safeParse(res.externalMapping);
|
|
34599
|
+
if (!parsed.success) throw new Error(`Unexpected ExternalMapping shape from API: ${parsed.error.message}`);
|
|
34600
|
+
return parsed.data;
|
|
34601
|
+
},
|
|
34602
|
+
listExternalMappings: async (subjectType, subjectId) => {
|
|
34603
|
+
const res = await request(
|
|
34604
|
+
"GET",
|
|
34605
|
+
`/api/external-mappings?subjectType=${encodeURIComponent(subjectType)}&subjectId=${encodeURIComponent(subjectId)}`
|
|
34606
|
+
);
|
|
34607
|
+
if (res === void 0) throw new ExternalMappingSubjectNotFoundError(subjectType, subjectId);
|
|
34608
|
+
const parsed = ExternalMappingsResponseSchema.safeParse(res);
|
|
34609
|
+
if (!parsed.success) throw new Error(`Unexpected ExternalMappings list shape from API: ${parsed.error.message}`);
|
|
34610
|
+
return parsed.data.externalMappings;
|
|
34611
|
+
},
|
|
34314
34612
|
// WorkItems (WorkItem exposure layer — owned, statused unit of operational work under a Track)
|
|
34315
34613
|
createWorkItem: async (input) => {
|
|
34316
|
-
const { trackId, title, owner, committedEstimateHours, effortSize, valueUnits, costPerValueUnit, createdBy, beatVersionId, workOrderId } = input;
|
|
34614
|
+
const { trackId, title, owner, committedEstimateHours, effortSize, valueUnits, costPerValueUnit, createdBy, beatVersionId, workOrderId, barId } = input;
|
|
34317
34615
|
const body = {
|
|
34318
34616
|
title,
|
|
34319
34617
|
...owner !== void 0 && { owner },
|
|
@@ -34323,11 +34621,12 @@ function createHttpClient(config2) {
|
|
|
34323
34621
|
...costPerValueUnit !== void 0 && { costPerValueUnit },
|
|
34324
34622
|
...createdBy !== void 0 && { createdBy },
|
|
34325
34623
|
...beatVersionId !== void 0 && { beatVersionId },
|
|
34326
|
-
...workOrderId !== void 0 && { workOrderId }
|
|
34624
|
+
...workOrderId !== void 0 && { workOrderId },
|
|
34625
|
+
...barId !== void 0 && { barId }
|
|
34327
34626
|
};
|
|
34328
34627
|
const res = await request(
|
|
34329
34628
|
"POST",
|
|
34330
|
-
`/api/
|
|
34629
|
+
`/api/tracks/${encodeURIComponent(trackId)}/work-items`,
|
|
34331
34630
|
body
|
|
34332
34631
|
);
|
|
34333
34632
|
if (res === void 0) throw new Error(`Track ${trackId} not found`);
|
|
@@ -34357,7 +34656,7 @@ function createHttpClient(config2) {
|
|
|
34357
34656
|
return parsed.data;
|
|
34358
34657
|
},
|
|
34359
34658
|
listTrackWorkItems: async (trackId) => {
|
|
34360
|
-
const res = await request("GET", `/api/
|
|
34659
|
+
const res = await request("GET", `/api/tracks/${encodeURIComponent(trackId)}/work-items`);
|
|
34361
34660
|
if (res === void 0) throw new Error(`Track ${trackId} not found`);
|
|
34362
34661
|
const parsed = WorkItemsResponseSchema.safeParse(res);
|
|
34363
34662
|
if (!parsed.success) throw new Error(`Unexpected work items list shape from API: ${parsed.error.message}`);
|
|
@@ -34441,9 +34740,8 @@ function createHttpClient(config2) {
|
|
|
34441
34740
|
return parsed.data.workRequests;
|
|
34442
34741
|
},
|
|
34443
34742
|
createWorkOrder: async (input) => {
|
|
34444
|
-
const {
|
|
34743
|
+
const { workRequestId, title, description, size, createdBy } = input;
|
|
34445
34744
|
const body = {
|
|
34446
|
-
workRequestId,
|
|
34447
34745
|
title,
|
|
34448
34746
|
...description !== void 0 && { description },
|
|
34449
34747
|
...size !== void 0 && { size },
|
|
@@ -34451,10 +34749,10 @@ function createHttpClient(config2) {
|
|
|
34451
34749
|
};
|
|
34452
34750
|
const res = await request(
|
|
34453
34751
|
"POST",
|
|
34454
|
-
`/api/
|
|
34752
|
+
`/api/work-requests/${encodeURIComponent(workRequestId)}/work-orders`,
|
|
34455
34753
|
body
|
|
34456
34754
|
);
|
|
34457
|
-
if (res === void 0) throw new Error(`
|
|
34755
|
+
if (res === void 0) throw new Error(`Work request ${workRequestId} not found`);
|
|
34458
34756
|
const parsed = WorkOrderApiSchema.safeParse(res.workOrder);
|
|
34459
34757
|
if (!parsed.success) throw new Error(`Unexpected WorkOrder shape from API: ${parsed.error.message}`);
|
|
34460
34758
|
return parsed.data;
|
|
@@ -34477,8 +34775,15 @@ function createHttpClient(config2) {
|
|
|
34477
34775
|
if (!parsed.success) throw new Error(`Unexpected WorkOrder shape from API: ${parsed.error.message}`);
|
|
34478
34776
|
return parsed.data;
|
|
34479
34777
|
},
|
|
34778
|
+
listMovementWorkOrders: async (movementId) => {
|
|
34779
|
+
const res = await request("GET", `/api/movements/${encodeURIComponent(movementId)}/work-orders`);
|
|
34780
|
+
if (res === void 0) throw new Error(`Movement ${movementId} not found`);
|
|
34781
|
+
const parsed = WorkOrdersResponseSchema.safeParse(res);
|
|
34782
|
+
if (!parsed.success) throw new Error(`Unexpected work orders list shape from API: ${parsed.error.message}`);
|
|
34783
|
+
return parsed.data.workOrders;
|
|
34784
|
+
},
|
|
34480
34785
|
listTrackWorkOrders: async (trackId) => {
|
|
34481
|
-
const res = await request("GET", `/api/
|
|
34786
|
+
const res = await request("GET", `/api/tracks/${encodeURIComponent(trackId)}/work-orders`);
|
|
34482
34787
|
if (res === void 0) throw new Error(`Track ${trackId} not found`);
|
|
34483
34788
|
const parsed = WorkOrdersResponseSchema.safeParse(res);
|
|
34484
34789
|
if (!parsed.success) throw new Error(`Unexpected work orders list shape from API: ${parsed.error.message}`);
|
|
@@ -34798,7 +35103,7 @@ function createHttpClient(config2) {
|
|
|
34798
35103
|
`/api/systems/${encodeURIComponent(systemId)}/beats/draft`,
|
|
34799
35104
|
input
|
|
34800
35105
|
);
|
|
34801
|
-
if (!result) throw new Error(`Beat draft failed:
|
|
35106
|
+
if (!result) throw new Error(`Beat draft failed: system ${systemId} not found`);
|
|
34802
35107
|
return result;
|
|
34803
35108
|
},
|
|
34804
35109
|
triggerSystemEmbedding: async (systemId) => {
|
|
@@ -35064,7 +35369,7 @@ function loadConfig() {
|
|
|
35064
35369
|
};
|
|
35065
35370
|
}
|
|
35066
35371
|
async function main() {
|
|
35067
|
-
console.error(`[harmonica-mcp] v${"
|
|
35372
|
+
console.error(`[harmonica-mcp] v${"5.0.0"} starting\u2026`);
|
|
35068
35373
|
const config2 = loadConfig();
|
|
35069
35374
|
const client = createHttpClient({
|
|
35070
35375
|
apiBaseUrl: config2.apiBaseUrl,
|