@codazen/harmonica-mcp 5.0.0 → 5.1.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.
Files changed (2) hide show
  1. package/dist/index.js +228 -126
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -22032,22 +22032,33 @@ function registerAuditIntegrityTools(server, ctx, client) {
22032
22032
  );
22033
22033
  }
22034
22034
 
22035
- // ../../libs/harmonica-services/src/mcp/tools/bar-tools.ts
22035
+ // ../../libs/harmonica-services/src/tool-schemas/bar.ts
22036
22036
  var BAR_STATUSES = ["open", "closed"];
22037
22037
  var idSegmentSchema = external_exports.string().min(1).max(128).refine((id) => !id.includes("#"), {
22038
22038
  message: "must not contain '#' \u2014 it is a DynamoDB key delimiter"
22039
22039
  });
22040
+ var create_bar = {
22041
+ accountId: idSegmentSchema.describe("The Account ID this Bar belongs to"),
22042
+ barId: idSegmentSchema.describe(
22043
+ 'Caller-supplied Bar ID (must not contain "#")'
22044
+ ),
22045
+ startDate: external_exports.string().datetime().describe("ISO 8601 start of the planning period"),
22046
+ endDate: external_exports.string().datetime().describe("ISO 8601 end of the planning period; must be after startDate"),
22047
+ status: external_exports.enum(BAR_STATUSES).optional().describe('Defaults to "open"')
22048
+ };
22049
+ var get_bar = {
22050
+ barId: idSegmentSchema.describe("The Bar ID")
22051
+ };
22052
+ var list_account_bars = {
22053
+ accountId: idSegmentSchema.describe("The Account ID")
22054
+ };
22055
+
22056
+ // ../../libs/harmonica-services/src/mcp/tools/bar-tools.ts
22040
22057
  function registerBarTools(server, _ctx, client) {
22041
22058
  server.tool(
22042
22059
  "create_bar",
22043
22060
  "Create a Bar \u2014 the recurring planning-period time-box a client Account plans and delivers within (e.g. one or two weeks). Bars are Account-scoped and independent of any CadenceConfig already set on the Account; the caller supplies explicit start/end dates.",
22044
- {
22045
- accountId: idSegmentSchema.describe("The Account ID this Bar belongs to"),
22046
- barId: idSegmentSchema.describe('Caller-supplied Bar ID (must not contain "#")'),
22047
- startDate: external_exports.string().datetime().describe("ISO 8601 start of the planning period"),
22048
- endDate: external_exports.string().datetime().describe("ISO 8601 end of the planning period; must be after startDate"),
22049
- status: external_exports.enum(BAR_STATUSES).optional().describe('Defaults to "open"')
22050
- },
22061
+ create_bar,
22051
22062
  async ({ accountId, barId, startDate, endDate, status }) => {
22052
22063
  const startMs = new Date(startDate).getTime();
22053
22064
  const endMs = new Date(endDate).getTime();
@@ -22077,9 +22088,7 @@ function registerBarTools(server, _ctx, client) {
22077
22088
  server.tool(
22078
22089
  "get_bar",
22079
22090
  "Get a Bar by ID.",
22080
- {
22081
- barId: idSegmentSchema.describe("The Bar ID")
22082
- },
22091
+ get_bar,
22083
22092
  async ({ barId }) => {
22084
22093
  try {
22085
22094
  const bar = await client.getBar(barId);
@@ -22098,28 +22107,28 @@ function registerBarTools(server, _ctx, client) {
22098
22107
  }
22099
22108
  }
22100
22109
  );
22101
- server.tool(
22102
- "list_account_bars",
22103
- "List an Account's Bars, chronological by start date.",
22104
- {
22105
- accountId: idSegmentSchema.describe("The Account ID")
22106
- },
22107
- async ({ accountId }) => {
22108
- try {
22109
- const bars = await client.listAccountBars(accountId);
22110
- if (bars === void 0) {
22111
- return { content: [{ type: "text", text: `Account not found: ${accountId}` }], isError: true };
22112
- }
22113
- if (bars.length === 0) {
22114
- return { content: [{ type: "text", text: "No bars found." }] };
22110
+ for (const name of ["list_account_bars", "list_bars"]) {
22111
+ server.tool(
22112
+ name,
22113
+ "List an Account's Bars, chronological by start date.",
22114
+ list_account_bars,
22115
+ async ({ accountId }) => {
22116
+ try {
22117
+ const bars = await client.listAccountBars(accountId);
22118
+ if (bars === void 0) {
22119
+ return { content: [{ type: "text", text: `Account not found: ${accountId}` }], isError: true };
22120
+ }
22121
+ if (bars.length === 0) {
22122
+ return { content: [{ type: "text", text: "No bars found." }] };
22123
+ }
22124
+ const text = bars.map((b) => `${b.barId} ${b.startDate} \u2192 ${b.endDate} \u2014 ${b.status}`).join("\n");
22125
+ return { content: [{ type: "text", text }] };
22126
+ } catch (err) {
22127
+ return { content: [{ type: "text", text: `Failed to list bars: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
22115
22128
  }
22116
- const text = bars.map((b) => `${b.barId} ${b.startDate} \u2192 ${b.endDate} \u2014 ${b.status}`).join("\n");
22117
- return { content: [{ type: "text", text }] };
22118
- } catch (err) {
22119
- return { content: [{ type: "text", text: `Failed to list bars: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
22120
22129
  }
22121
- }
22122
- );
22130
+ );
22131
+ }
22123
22132
  }
22124
22133
 
22125
22134
  // ../../libs/harmonica-services/src/mcp/tools/baseline-tools.ts
@@ -25212,6 +25221,41 @@ var BeatDraftSuggestionSchema = external_exports.object({
25212
25221
  });
25213
25222
  var BeatDraftSuggestionsSchema = external_exports.array(BeatDraftSuggestionSchema);
25214
25223
 
25224
+ // ../../libs/harmonica-schemas/src/progress-evidence.ts
25225
+ var ProgressEvidenceQuerySchema = external_exports.object({
25226
+ start: external_exports.string().min(1).max(40),
25227
+ end: external_exports.string().min(1).max(40),
25228
+ timezone: external_exports.string().min(1).max(100),
25229
+ contributor: external_exports.string().trim().min(1).max(256).optional(),
25230
+ limit: external_exports.coerce.number().int().min(1).max(100).optional(),
25231
+ cursor: external_exports.string().min(1).max(4096).optional()
25232
+ }).strict();
25233
+ var ProgressCitationSchema = external_exports.object({ kind: external_exports.string(), id: external_exports.string(), url: external_exports.string() });
25234
+ var ProgressEvidenceItemSchema = external_exports.object({
25235
+ id: external_exports.string(),
25236
+ source: external_exports.enum(["activity", "session"]),
25237
+ occurredAt: external_exports.string(),
25238
+ recordedAt: external_exports.string(),
25239
+ summary: external_exports.string(),
25240
+ classification: external_exports.enum(["planned", "progressed", "delivered"]),
25241
+ attribution: external_exports.object({ basis: external_exports.enum(["recorded_actor", "session_initiator", "unknown"]), actorId: external_exports.string().nullable(), agentId: external_exports.string().optional() }),
25242
+ citations: external_exports.array(ProgressCitationSchema),
25243
+ context: external_exports.array(external_exports.object({ kind: external_exports.string(), id: external_exports.string(), title: external_exports.string().optional(), asOf: external_exports.literal("current") })),
25244
+ deliveryProof: external_exports.object({ kind: external_exports.literal("recorded_release"), occurredAt: external_exports.string(), citation: ProgressCitationSchema }).nullable()
25245
+ });
25246
+ var ProgressCoverageGapSchema = external_exports.object({ source: external_exports.string(), code: external_exports.string(), message: external_exports.string(), entityId: external_exports.string().optional() });
25247
+ var ProgressEvidenceResponseSchema = external_exports.object({
25248
+ systemId: external_exports.string(),
25249
+ accountId: external_exports.string().nullable(),
25250
+ window: external_exports.object({ start: external_exports.string(), end: external_exports.string(), timezone: external_exports.string() }),
25251
+ contributor: external_exports.string().nullable(),
25252
+ watermark: external_exports.string(),
25253
+ items: external_exports.array(ProgressEvidenceItemSchema),
25254
+ nextCursor: external_exports.string().optional(),
25255
+ coverage: external_exports.object({ status: external_exports.literal("partial"), gaps: external_exports.array(ProgressCoverageGapSchema) }),
25256
+ metrics: external_exports.object({ queryCalls: external_exports.number(), batchCalls: external_exports.number(), scannedCount: external_exports.number(), referenceCount: external_exports.number(), elapsedMs: external_exports.number() })
25257
+ });
25258
+
25215
25259
  // ../../libs/harmonica-schemas/src/api-responses.ts
25216
25260
  var HumanAssigneeSchema = external_exports.object({ email: external_exports.string(), name: external_exports.string() });
25217
25261
  var AgentAssigneeSchema = external_exports.object({ agentId: external_exports.string(), name: external_exports.string() });
@@ -25913,7 +25957,11 @@ var HumanAssigneeApiSchema = external_exports.object({
25913
25957
  name: external_exports.string(),
25914
25958
  email: external_exports.string()
25915
25959
  });
25960
+ var ArchiveWorkItemRequestSchema = external_exports.object({ reason: external_exports.string().trim().min(1).max(1e3) });
25916
25961
  var WorkItemApiSchema = external_exports.object({
25962
+ archived: external_exports.boolean().optional(),
25963
+ archivedAt: external_exports.string().optional(),
25964
+ archivedReason: external_exports.string().optional(),
25917
25965
  workItemId: external_exports.string(),
25918
25966
  trackId: external_exports.string(),
25919
25967
  teamspaceId: external_exports.string(),
@@ -25947,6 +25995,12 @@ var WorkItemApiSchema = external_exports.object({
25947
25995
  * not, so a move on either side can separate the two.
25948
25996
  */
25949
25997
  barId: external_exports.string().min(1).optional(),
25998
+ /**
25999
+ * Optional per-Track fractional ordering rank (N-4E09-7665). Absent when
26000
+ * the item is unranked — an unranked item is a first-class state. The
26001
+ * Work Queue uses this to order items within their displayed group.
26002
+ */
26003
+ rank: external_exports.number().optional(),
25950
26004
  createdBy: external_exports.string().optional(),
25951
26005
  version: external_exports.number().optional(),
25952
26006
  createdAt: external_exports.string(),
@@ -27017,19 +27071,19 @@ function registerMovementTools(server, _ctx, client) {
27017
27071
  );
27018
27072
  server.tool(
27019
27073
  "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.',
27074
+ `Create a Movement for an Account, in one of that Account's Teamspaces. A Movement is a 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
27075
  {
27022
27076
  teamspaceId: external_exports.string().describe("The teamspace ID"),
27023
27077
  title: external_exports.string().min(1).max(300).describe('Movement title (e.g. "Q3 Platform Modernisation SOW")'),
27024
27078
  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.")
27079
+ accountId: external_exports.string().min(1).max(128).describe("The Account this Movement belongs to. Required (rev-950e828e). Must be in the same org as the Teamspace and linked to it.")
27026
27080
  },
27027
27081
  async ({ teamspaceId, title, description, accountId }) => {
27028
27082
  const m = await client.createMovement({
27029
27083
  teamspaceId,
27030
27084
  title,
27031
27085
  ...description !== void 0 && { description },
27032
- ...accountId !== void 0 && { accountId }
27086
+ accountId
27033
27087
  });
27034
27088
  return {
27035
27089
  content: [
@@ -27048,7 +27102,7 @@ function registerMovementTools(server, _ctx, client) {
27048
27102
  movementId: external_exports.string().describe("The movement ID"),
27049
27103
  title: external_exports.string().min(1).max(300).optional().describe("New title"),
27050
27104
  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.")
27105
+ accountId: external_exports.string().min(1).max(128).optional().describe("Reassign this Movement to a different Account. Must be in the same org and linked to the teamspace. Omit to leave unchanged \u2014 a Movement cannot be left without an Account (rev-950e828e).")
27052
27106
  },
27053
27107
  async ({ movementId, title, description, accountId }) => {
27054
27108
  const hasUpdates = title !== void 0 || description !== void 0 || accountId !== void 0;
@@ -27058,12 +27112,11 @@ function registerMovementTools(server, _ctx, client) {
27058
27112
  isError: true
27059
27113
  };
27060
27114
  }
27061
- const resolvedAccountId = accountId === "" ? null : accountId;
27062
27115
  try {
27063
27116
  const m = await client.updateMovement(movementId, {
27064
27117
  ...title !== void 0 && { title },
27065
27118
  ...description !== void 0 && { description },
27066
- ...accountId !== void 0 && { accountId: resolvedAccountId }
27119
+ ...accountId !== void 0 && { accountId }
27067
27120
  });
27068
27121
  if (!m) {
27069
27122
  return {
@@ -27071,7 +27124,7 @@ function registerMovementTools(server, _ctx, client) {
27071
27124
  isError: true
27072
27125
  };
27073
27126
  }
27074
- const account = accountId !== void 0 ? resolvedAccountId === null ? " \u2014 Account: (unlinked)" : ` \u2014 Account: ${resolvedAccountId}` : m.accountId ? ` \u2014 Account: ${m.accountId}` : "";
27127
+ const account = ` \u2014 Account: ${accountId ?? m.accountId}`;
27075
27128
  return {
27076
27129
  content: [
27077
27130
  {
@@ -29521,6 +29574,24 @@ function registerPortfolioCoherenceTools(server, ctx, client) {
29521
29574
  );
29522
29575
  }
29523
29576
 
29577
+ // ../../libs/harmonica-services/src/mcp/tools/progress-evidence-tools.ts
29578
+ function registerProgressEvidenceTools(server, _ctx, client) {
29579
+ server.tool(
29580
+ "get_progress_evidence",
29581
+ "Read cited progress evidence for a system and [start,end) reporting window. Calendar dates use the required IANA timezone; timestamps need an offset. Optional contributor matches recorded actors or explicitly labeled session initiators, never assignees. Returns planned/progressed/delivered evidence, source gaps, a watermark and continuation. Follow nextCursor even on empty pages. No writes.",
29582
+ { systemId: external_exports.string().min(1).max(128).regex(/^[^#]+$/), ...ProgressEvidenceQuerySchema.shape },
29583
+ async ({ systemId, ...query }) => {
29584
+ try {
29585
+ const report = await client.getProgressEvidence(systemId, query);
29586
+ if (!report) return { isError: true, content: [{ type: "text", text: "System not found." }] };
29587
+ return { content: [{ type: "text", text: JSON.stringify(report, null, 2) }] };
29588
+ } catch (error2) {
29589
+ return { isError: true, content: [{ type: "text", text: `Progress evidence query failed: ${error2 instanceof Error ? error2.message : String(error2)}` }] };
29590
+ }
29591
+ }
29592
+ );
29593
+ }
29594
+
29524
29595
  // ../../libs/harmonica-services/src/mcp/tools/pulse-report-tools.ts
29525
29596
  function formatBeatRow(r) {
29526
29597
  const downbeatState = r.hasDownbeat ? "" : " \u2014 no Downbeat ever set";
@@ -31235,6 +31306,30 @@ function registerTeamspaceTools(server, ctx, client) {
31235
31306
  );
31236
31307
  }
31237
31308
 
31309
+ // ../../libs/harmonica-services/src/tool-schemas/track.ts
31310
+ var get_track = {
31311
+ trackId: external_exports.string().describe("The Track ID")
31312
+ };
31313
+ var update_track = {
31314
+ trackId: external_exports.string().describe("The Track ID"),
31315
+ title: external_exports.string().min(1).max(300).optional().describe("New title"),
31316
+ description: external_exports.string().max(2e3).optional().describe("New description"),
31317
+ deliverables: external_exports.string().max(2e4).optional().describe(`Replace the Track's deliverables Markdown ("" clears)`),
31318
+ acceptanceCriteria: external_exports.string().max(2e4).optional().describe(`Replace the Track's acceptance-criteria Markdown ("" clears)`),
31319
+ vuRate: external_exports.number().nonnegative().optional().describe("New VU rate")
31320
+ };
31321
+ var transition_track = {
31322
+ trackId: external_exports.string().describe("The Track ID"),
31323
+ targetState: external_exports.enum(["proposed", "approved", "active", "fulfilled", "cancelled"]).describe("Target lifecycle state"),
31324
+ approvalNoteId: external_exports.string().optional().describe(
31325
+ 'Required when transitioning to "approved" \u2014 the Note ID capturing the approval decision'
31326
+ ),
31327
+ reason: external_exports.string().optional().describe("Reason for the transition (especially useful for cancellation)"),
31328
+ confirm: external_exports.boolean().optional().default(false).describe(
31329
+ "false (default) = dry-run preview; true = execute the transition"
31330
+ )
31331
+ };
31332
+
31238
31333
  // ../../libs/harmonica-services/src/mcp/tools/track-tools.ts
31239
31334
  function registerTrackTools(server, ctx, client) {
31240
31335
  server.tool(
@@ -31255,9 +31350,7 @@ function registerTrackTools(server, ctx, client) {
31255
31350
  server.tool(
31256
31351
  "get_track",
31257
31352
  "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
- },
31353
+ get_track,
31261
31354
  async ({ trackId }) => {
31262
31355
  const t = await client.getTrack(trackId);
31263
31356
  if (!t) {
@@ -31331,14 +31424,7 @@ ${t.acceptanceCriteria}` : null,
31331
31424
  server.tool(
31332
31425
  "update_track",
31333
31426
  "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
- },
31427
+ update_track,
31342
31428
  async ({ trackId, title, description, deliverables, acceptanceCriteria, vuRate }) => {
31343
31429
  const t = await client.updateTrack(trackId, {
31344
31430
  ...title !== void 0 && { title },
@@ -31365,13 +31451,7 @@ ${t.acceptanceCriteria}` : null,
31365
31451
  server.tool(
31366
31452
  "transition_track",
31367
31453
  '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
- },
31454
+ transition_track,
31375
31455
  async ({ trackId, targetState, approvalNoteId, reason, confirm }) => {
31376
31456
  if (!confirm) {
31377
31457
  return {
@@ -32002,6 +32082,24 @@ function registerWorkItemTools(server, _ctx, client) {
32002
32082
  );
32003
32083
  }
32004
32084
 
32085
+ // ../../libs/harmonica-services/src/tool-schemas/work-order.ts
32086
+ var WORK_ORDER_SIZE_ENUM = ["XS", "S", "M", "L", "XL"];
32087
+ var create_work_order = {
32088
+ workRequestId: external_exports.string().min(1).describe("The Work Request this order was split from \u2014 immutable lineage"),
32089
+ title: external_exports.string().min(1).max(300).describe("Work order title"),
32090
+ description: external_exports.string().max(1e4).optional().describe("Optional detail beyond the title"),
32091
+ size: external_exports.enum(WORK_ORDER_SIZE_ENUM).optional().describe(
32092
+ "Optional t-shirt size. An order can arrive before it is sized; a request's composition counts unsized orders rather than hiding them."
32093
+ ),
32094
+ createdBy: external_exports.string().optional().describe("Optional creator identity for audit")
32095
+ };
32096
+ var get_work_order = {
32097
+ workOrderId: external_exports.string().describe("The Work Order ID (e.g., wo-abc123)")
32098
+ };
32099
+ var list_movement_work_orders = {
32100
+ movementId: external_exports.string().describe("The Movement (engagement) ID")
32101
+ };
32102
+
32005
32103
  // ../../libs/harmonica-services/src/mcp/tools/work-order-tools.ts
32006
32104
  var WORK_INTAKE_STATUS_ENUM = [
32007
32105
  "requested",
@@ -32014,7 +32112,7 @@ var NEEDS_DECISION_REASON_ENUM = [
32014
32112
  "exceeds_capacity",
32015
32113
  "incoherent_with_decision"
32016
32114
  ];
32017
- var WORK_ORDER_SIZE_ENUM = [
32115
+ var WORK_ORDER_SIZE_ENUM2 = [
32018
32116
  "XS",
32019
32117
  "S",
32020
32118
  "M",
@@ -32026,8 +32124,7 @@ function oneLine(value) {
32026
32124
  }
32027
32125
  function formatOrderLine(o) {
32028
32126
  const reason = o.needsDecisionReason ? ` [${o.needsDecisionReason}]` : "";
32029
- const track = o.trackId ? `on ${o.trackId}` : "unplaced";
32030
- return `[${o.workOrderId}] ${oneLine(o.title)} \u2014 ${o.status}${reason}, ${o.size ?? "unsized"}, ${track}`;
32127
+ return `[${o.workOrderId}] ${oneLine(o.title)} \u2014 ${o.status}${reason}, ${o.size ?? "unsized"}`;
32031
32128
  }
32032
32129
  function registerWorkOrderTools(server, _ctx, client) {
32033
32130
  server.tool(
@@ -32057,7 +32154,7 @@ function registerWorkOrderTools(server, _ctx, client) {
32057
32154
  );
32058
32155
  server.tool(
32059
32156
  "list_work_request_work_orders",
32060
- "List the Work Orders one Work Request split into \u2014 the lineage view. A four-way split shows four orders here, each possibly on a different Track and in a different state.",
32157
+ "List the Work Orders one Work Request split into \u2014 the lineage view. A four-way split shows four orders here, each with its own Items and independent state.",
32061
32158
  {
32062
32159
  workRequestId: external_exports.string().describe("The Work Request ID")
32063
32160
  },
@@ -32069,10 +32166,8 @@ function registerWorkOrderTools(server, _ctx, client) {
32069
32166
  );
32070
32167
  server.tool(
32071
32168
  "get_work_order",
32072
- "Get a Work Order by ID. Returns its title, status (with the routing reason when it is awaiting a decision), t-shirt size, the Track it sits on, and the Work Request it was split from.",
32073
- {
32074
- workOrderId: external_exports.string().describe("The Work Order ID (e.g., wo-abc123)")
32075
- },
32169
+ "Get a Work Order by ID. Returns its title, status (with the routing reason when it is awaiting a decision), t-shirt size, and the Work Request it was split from.",
32170
+ get_work_order,
32076
32171
  async ({ workOrderId }) => {
32077
32172
  const wo = await client.getWorkOrder(workOrderId);
32078
32173
  if (!wo) {
@@ -32082,7 +32177,6 @@ function registerWorkOrderTools(server, _ctx, client) {
32082
32177
  `ID: ${wo.workOrderId}`,
32083
32178
  `Title: ${oneLine(wo.title)}`,
32084
32179
  `From Work Request: ${wo.workRequestId}`,
32085
- `Track: ${wo.trackId}`,
32086
32180
  `Status: ${wo.status}`,
32087
32181
  wo.needsDecisionReason ? `Routing Reason: ${wo.needsDecisionReason}` : null,
32088
32182
  wo.size !== void 0 ? `Size: ${wo.size}` : "Size: unsized",
@@ -32094,41 +32188,37 @@ function registerWorkOrderTools(server, _ctx, client) {
32094
32188
  return { content: [{ type: "text", text: lines }] };
32095
32189
  }
32096
32190
  );
32097
- server.tool(
32098
- "create_work_order",
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.`,
32100
- {
32101
- workRequestId: external_exports.string().min(1).describe("The Work Request this order was split from \u2014 immutable lineage"),
32102
- title: external_exports.string().min(1).max(300).describe("Work order title"),
32103
- description: external_exports.string().max(1e4).optional().describe("Optional detail beyond the title"),
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."),
32105
- createdBy: external_exports.string().optional().describe("Optional creator identity for audit")
32106
- },
32107
- async ({ workRequestId, title, description, size, createdBy }) => {
32108
- const wo = await client.createWorkOrder({
32109
- workRequestId,
32110
- title,
32111
- ...description !== void 0 && { description },
32112
- ...size !== void 0 && { size },
32113
- ...createdBy !== void 0 && { createdBy }
32114
- });
32115
- return {
32116
- content: [{ type: "text", text: `Created work order: ${formatOrderLine(wo)}` }]
32117
- };
32118
- }
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
- );
32191
+ for (const name of ["create_work_order", "promote_work_request"]) {
32192
+ server.tool(
32193
+ name,
32194
+ `Create (promote) a Work Order from a Work Request. Each call creates another Order; it does not accept the Order or change Request status. 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; Tracks are assigned only to Work Items.`,
32195
+ create_work_order,
32196
+ async ({ workRequestId, title, description, size, createdBy }) => {
32197
+ const wo = await client.createWorkOrder({
32198
+ workRequestId,
32199
+ title,
32200
+ ...description !== void 0 && { description },
32201
+ ...size !== void 0 && { size },
32202
+ ...createdBy !== void 0 && { createdBy }
32203
+ });
32204
+ return {
32205
+ content: [{ type: "text", text: `Created work order: ${formatOrderLine(wo)}` }]
32206
+ };
32207
+ }
32208
+ );
32209
+ }
32210
+ for (const name of ["list_movement_work_orders", "list_work_orders"]) {
32211
+ server.tool(
32212
+ name,
32213
+ '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.',
32214
+ list_movement_work_orders,
32215
+ async ({ movementId }) => {
32216
+ const orders = await client.listMovementWorkOrders(movementId);
32217
+ const text = orders.length === 0 ? "No work orders found." : orders.map(formatOrderLine).join("\n");
32218
+ return { content: [{ type: "text", text }] };
32219
+ }
32220
+ );
32221
+ }
32132
32222
  server.tool(
32133
32223
  "update_work_order",
32134
32224
  "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.",
@@ -32136,7 +32226,7 @@ function registerWorkOrderTools(server, _ctx, client) {
32136
32226
  workOrderId: external_exports.string().describe("The Work Order ID"),
32137
32227
  title: external_exports.string().min(1).max(300).optional().describe("New title"),
32138
32228
  description: external_exports.string().max(1e4).nullable().optional().describe("New description. Pass null to clear; omit to leave untouched."),
32139
- size: external_exports.enum(WORK_ORDER_SIZE_ENUM).nullable().optional().describe("New t-shirt size. Pass null to return it to unsized; omit to leave untouched.")
32229
+ size: external_exports.enum(WORK_ORDER_SIZE_ENUM2).nullable().optional().describe("New t-shirt size. Pass null to return it to unsized; omit to leave untouched.")
32140
32230
  },
32141
32231
  async ({ workOrderId, title, description, size }) => {
32142
32232
  if (title === void 0 && description === void 0 && size === void 0) {
@@ -32225,11 +32315,27 @@ function registerWorkOrderTools(server, _ctx, client) {
32225
32315
  );
32226
32316
  }
32227
32317
 
32318
+ // ../../libs/harmonica-services/src/tool-schemas/work-request.ts
32319
+ var WORK_REQUEST_ORIGIN_ENUM = ["client", "internal"];
32320
+ var create_work_request = {
32321
+ movementId: external_exports.string().describe("The Movement (engagement) this request is filed against"),
32322
+ title: external_exports.string().min(1).max(300).describe("Short title for the ask"),
32323
+ body: external_exports.string().max(1e4).optional().describe("The ask as written, in the requester's own words"),
32324
+ requesterName: external_exports.string().min(1).describe("Who is asking \u2014 display name"),
32325
+ requesterEmail: external_exports.string().email().describe("Who is asking \u2014 email"),
32326
+ origin: external_exports.enum(WORK_REQUEST_ORIGIN_ENUM).describe(
32327
+ 'Where the ask came from: "client" or "internal". One entity covers both \u2014 only the entry point differs.'
32328
+ ),
32329
+ createdBy: external_exports.string().optional().describe("Optional creator identity for audit")
32330
+ };
32331
+ var get_work_request = {
32332
+ workRequestId: external_exports.string().describe("The Work Request ID (e.g., wr-abc123)")
32333
+ };
32334
+ var list_movement_work_requests = {
32335
+ movementId: external_exports.string().describe("The Movement (engagement) ID")
32336
+ };
32337
+
32228
32338
  // ../../libs/harmonica-services/src/mcp/tools/work-request-tools.ts
32229
- var WORK_REQUEST_ORIGIN_ENUM = [
32230
- "client",
32231
- "internal"
32232
- ];
32233
32339
  function oneLine2(value) {
32234
32340
  return value.replace(/[\r\n]+/g, " ");
32235
32341
  }
@@ -32243,9 +32349,7 @@ function registerWorkRequestTools(server, _ctx, client) {
32243
32349
  server.tool(
32244
32350
  "list_movement_work_requests",
32245
32351
  `List the Work Requests filed against a Movement. A Work Request is the customer's ask in their own language, before it is sized or agreed. Recognition cues: "what has the client asked for", "list the requests on this engagement" \u2192 list_movement_work_requests.`,
32246
- {
32247
- movementId: external_exports.string().describe("The Movement (engagement) ID")
32248
- },
32352
+ list_movement_work_requests,
32249
32353
  async ({ movementId }) => {
32250
32354
  const requests = await client.listMovementWorkRequests(movementId);
32251
32355
  const text = requests.length === 0 ? "No work requests found." : requests.map((wr) => `[${wr.workRequestId}] ${oneLine2(wr.title)} \u2014 from ${oneLine2(wr.requester.name)} (${wr.origin})`).join("\n");
@@ -32255,9 +32359,7 @@ function registerWorkRequestTools(server, _ctx, client) {
32255
32359
  server.tool(
32256
32360
  "get_work_request",
32257
32361
  "Get a Work Request with the Work Orders it split into and their combined size composition. Note there is no request-level status: each Order carries its own, because a split whose halves sit in different states cannot be reduced to one word.",
32258
- {
32259
- workRequestId: external_exports.string().describe("The Work Request ID (e.g., wr-abc123)")
32260
- },
32362
+ get_work_request,
32261
32363
  async ({ workRequestId }) => {
32262
32364
  const detail = await client.getWorkRequestDetail(workRequestId);
32263
32365
  if (!detail) {
@@ -32267,10 +32369,9 @@ function registerWorkRequestTools(server, _ctx, client) {
32267
32369
  const orderLines = orders.length === 0 ? ["Orders: none yet \u2014 this request has not been split"] : [
32268
32370
  "Orders:",
32269
32371
  ...orders.map((o) => {
32270
- const track = o.trackTitle ? `${oneLine2(o.trackTitle)} (${o.trackId})` : o.trackId;
32271
32372
  const reason = o.needsDecisionReason ? ` [${o.needsDecisionReason}]` : "";
32272
32373
  const sized = o.size ?? "unsized";
32273
- return ` [${o.workOrderId}] ${oneLine2(o.title)} \u2014 ${o.status}${reason}, ${sized}, on ${track}`;
32374
+ return ` [${o.workOrderId}] ${oneLine2(o.title)} \u2014 ${o.status}${reason}, ${sized}`;
32274
32375
  })
32275
32376
  ];
32276
32377
  const lines = [
@@ -32292,15 +32393,7 @@ function registerWorkRequestTools(server, _ctx, client) {
32292
32393
  server.tool(
32293
32394
  "create_work_request",
32294
32395
  "File a Work Request against a Movement \u2014 how work arrives, in the requester's own language, before it is sized or agreed. Splitting it into Work Orders is a separate step (create_work_order).",
32295
- {
32296
- movementId: external_exports.string().describe("The Movement (engagement) this request is filed against"),
32297
- title: external_exports.string().min(1).max(300).describe("Short title for the ask"),
32298
- body: external_exports.string().max(1e4).optional().describe("The ask as written, in the requester's own words"),
32299
- requesterName: external_exports.string().min(1).describe("Who is asking \u2014 display name"),
32300
- requesterEmail: external_exports.string().email().describe("Who is asking \u2014 email"),
32301
- origin: external_exports.enum(WORK_REQUEST_ORIGIN_ENUM).describe('Where the ask came from: "client" or "internal". One entity covers both \u2014 only the entry point differs.'),
32302
- createdBy: external_exports.string().optional().describe("Optional creator identity for audit")
32303
- },
32396
+ create_work_request,
32304
32397
  async ({ movementId, title, body, requesterName, requesterEmail, origin, createdBy }) => {
32305
32398
  const wr = await client.createWorkRequest({
32306
32399
  movementId,
@@ -32858,6 +32951,7 @@ function registerAllTools(server, ctx, client, profile) {
32858
32951
  registerNextActionsTools(server, ctx, client);
32859
32952
  registerCheckTools(server, ctx, client);
32860
32953
  registerActivityTools(server, ctx, client);
32954
+ registerProgressEvidenceTools(server, ctx, client);
32861
32955
  registerAuditIntegrityTools(server, ctx, client);
32862
32956
  registerAnalysisTools(server, ctx, client);
32863
32957
  registerOrganizationTools(server, ctx, client);
@@ -34523,10 +34617,13 @@ function createHttpClient(config2) {
34523
34617
  const { teamspaceId, title, description, accountId } = input;
34524
34618
  const body = {
34525
34619
  title,
34526
- ...description !== void 0 && { description },
34527
- ...accountId !== void 0 && { accountId }
34620
+ ...description !== void 0 && { description }
34528
34621
  };
34529
- const result = await request("POST", `/api/teamspaces/${encodeURIComponent(teamspaceId)}/movements`, body);
34622
+ const result = await request(
34623
+ "POST",
34624
+ `/api/teamspaces/${encodeURIComponent(teamspaceId)}/accounts/${encodeURIComponent(accountId)}/movements`,
34625
+ body
34626
+ );
34530
34627
  if (result === void 0) throw new Error("Resource not found when creating Movement");
34531
34628
  const parsed = MovementApiSchema.safeParse(result);
34532
34629
  if (!parsed.success) throw new Error(`Unexpected Movement shape from API: ${parsed.error.message}`);
@@ -34936,6 +35033,11 @@ function createHttpClient(config2) {
34936
35033
  const qs = asOf ? `?asOf=${encodeURIComponent(asOf)}` : "";
34937
35034
  return request("GET", `/api/accounts/${encodeURIComponent(accountId)}/cadence-schedule${qs}`);
34938
35035
  },
35036
+ getProgressEvidence: async (systemId, query) => {
35037
+ const qs = new URLSearchParams();
35038
+ for (const [key, value] of Object.entries(query)) if (value !== void 0) qs.set(key, String(value));
35039
+ return request("GET", `/api/systems/${encodeURIComponent(systemId)}/progress-evidence?${qs}`);
35040
+ },
34939
35041
  getPulseReport: async (accountId, asOf) => {
34940
35042
  const qs = asOf ? `?asOf=${encodeURIComponent(asOf)}` : "";
34941
35043
  return request("GET", `/api/accounts/${encodeURIComponent(accountId)}/pulse-report${qs}`);
@@ -35369,7 +35471,7 @@ function loadConfig() {
35369
35471
  };
35370
35472
  }
35371
35473
  async function main() {
35372
- console.error(`[harmonica-mcp] v${"5.0.0"} starting\u2026`);
35474
+ console.error(`[harmonica-mcp] v${"5.1.0"} starting\u2026`);
35373
35475
  const config2 = loadConfig();
35374
35476
  const client = createHttpClient({
35375
35477
  apiBaseUrl: config2.apiBaseUrl,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codazen/harmonica-mcp",
3
- "version": "5.0.0",
3
+ "version": "5.1.0",
4
4
  "description": "MCP server for Harmonica — connect any MCP-compatible AI assistant to Harmonica",
5
5
  "license": "MIT",
6
6
  "bin": {