@codazen/harmonica-mcp 3.2.0 → 3.4.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 +305 -21
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -21261,13 +21261,40 @@ var EMPTY_COMPLETION_RESULT = {
|
|
|
21261
21261
|
};
|
|
21262
21262
|
|
|
21263
21263
|
// ../../libs/harmonica-services/src/mcp/server-factory.ts
|
|
21264
|
+
var DELIVERY_LATITUDE_INSTRUCTIONS = `AGENT DELIVERY LATITUDE (CHECK BEFORE YOU ACT)
|
|
21265
|
+
|
|
21266
|
+
Before you open a pull request, request the agent code review, or start watching
|
|
21267
|
+
a PR, resolve the delivery policy of the System the work belongs to and act on
|
|
21268
|
+
it. The policy hangs off the System and is reached through the Revision you are
|
|
21269
|
+
working \u2014 never through the repository, which may host many Systems.
|
|
21270
|
+
|
|
21271
|
+
- \`get_revision\` returns the resolved policy as an "Agent Delivery Policy"
|
|
21272
|
+
block, so the answer arrives in a call you were already making.
|
|
21273
|
+
- \`get_system_context\` carries the same block for a System you name directly.
|
|
21274
|
+
- For a doc-only or governance change that links a decision Note instead of a
|
|
21275
|
+
Revision, \`get_note\` on that Note carries the block.
|
|
21276
|
+
|
|
21277
|
+
Three independent toggles \u2014 openPullRequest, requestAgentReview,
|
|
21278
|
+
watchPullRequest:
|
|
21279
|
+
|
|
21280
|
+
- true: perform the step, do not ask, and say that you did it.
|
|
21281
|
+
- false: do NOT perform it and do not ask either \u2014 say the System's policy has
|
|
21282
|
+
it off, then move on. The tools also refuse a false step, including when you
|
|
21283
|
+
pass confirm: true.
|
|
21284
|
+
- unset, or unresolvable for any reason: ASK a human. Nothing except an explicit
|
|
21285
|
+
true is a grant, and every combination of the three is legitimate \u2014 never
|
|
21286
|
+
infer one toggle from another.
|
|
21287
|
+
|
|
21288
|
+
Re-read the policy before each step rather than caching it, so a human revoking
|
|
21289
|
+
a toggle mid-flight takes effect on your next action.`;
|
|
21264
21290
|
async function createMcpServer(ctx, client, registerTools, registerResources) {
|
|
21265
|
-
let
|
|
21291
|
+
let guidelines;
|
|
21266
21292
|
try {
|
|
21267
|
-
|
|
21293
|
+
guidelines = await client.getBeatGuidelines();
|
|
21268
21294
|
} catch (err) {
|
|
21269
21295
|
console.warn("[MCP] Failed to load beat guidelines for server instructions:", err);
|
|
21270
21296
|
}
|
|
21297
|
+
const instructions = [guidelines, DELIVERY_LATITUDE_INSTRUCTIONS].filter(Boolean).join("\n\n");
|
|
21271
21298
|
const server = new McpServer({
|
|
21272
21299
|
name: "harmonica",
|
|
21273
21300
|
version: "0.1.0"
|
|
@@ -22524,7 +22551,7 @@ Use get_job_status with jobId="${taskId}" to poll for results, or list_checks to
|
|
|
22524
22551
|
};
|
|
22525
22552
|
server.tool(
|
|
22526
22553
|
"check_beat_coda_quality",
|
|
22527
|
-
"Enqueue a Beat definition quality check across 7 dimensions (Distinctive, Harmonious, Substantial, Durable, Clear, Strategically Aligned,
|
|
22554
|
+
"Enqueue a Beat definition quality check across 7 dimensions (Distinctive, Harmonious, Substantial, Durable, Clear, Strategically Aligned, Outcome-Oriented) \u2014 grades the Beat's Coda and directing context, persisted as beat_coda_quality. By default returns a jobId immediately (fire-and-forget) \u2014 use get_job_status or list_checks to retrieve results. Set wait=true to block until the check completes and receive the scorecard inline. WARNING: wait=true holds the MCP session open for 30\u201360s per check \u2014 for bulk operations (more than one Beat), omit wait and poll get_job_status separately to avoid session exhaustion.",
|
|
22528
22555
|
schema,
|
|
22529
22556
|
handler
|
|
22530
22557
|
);
|
|
@@ -25204,6 +25231,124 @@ ${members}${parent}`;
|
|
|
25204
25231
|
);
|
|
25205
25232
|
}
|
|
25206
25233
|
|
|
25234
|
+
// ../../libs/harmonica-services/src/mcp/tools/feature-flag-tools.ts
|
|
25235
|
+
var STAGE_ENUM = external_exports.enum([
|
|
25236
|
+
"local",
|
|
25237
|
+
"staging",
|
|
25238
|
+
"staging-alpha",
|
|
25239
|
+
"staging-beta",
|
|
25240
|
+
"alpha",
|
|
25241
|
+
"beta",
|
|
25242
|
+
"prod"
|
|
25243
|
+
]);
|
|
25244
|
+
function registerFeatureFlagTools(server, _ctx, client) {
|
|
25245
|
+
server.tool(
|
|
25246
|
+
"list_flags",
|
|
25247
|
+
"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.",
|
|
25248
|
+
{
|
|
25249
|
+
includeArchived: external_exports.boolean().optional().describe("Include archived flags. Default false"),
|
|
25250
|
+
search: external_exports.string().optional().describe("Case-insensitive search against flag name and description"),
|
|
25251
|
+
cursor: external_exports.string().optional().describe("Pagination cursor from a prior list_flags result"),
|
|
25252
|
+
limit: external_exports.number().int().min(1).max(100).optional().describe("Flags per page. Default 20")
|
|
25253
|
+
},
|
|
25254
|
+
async ({ includeArchived, search, cursor, limit }) => {
|
|
25255
|
+
try {
|
|
25256
|
+
const page = await client.listFlags({ includeArchived, search, cursor, limit });
|
|
25257
|
+
if (page.flags.length === 0) {
|
|
25258
|
+
return { content: [{ type: "text", text: "No flags found." }] };
|
|
25259
|
+
}
|
|
25260
|
+
return { content: [{ type: "text", text: formatFlagList(page.flags, page.total, page.nextCursor) }] };
|
|
25261
|
+
} catch (err) {
|
|
25262
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
25263
|
+
return { content: [{ type: "text", text: `Failed to list flags: ${message}` }], isError: true };
|
|
25264
|
+
}
|
|
25265
|
+
}
|
|
25266
|
+
);
|
|
25267
|
+
server.tool(
|
|
25268
|
+
"get_flag",
|
|
25269
|
+
"Get full detail for a single feature flag by name, including its enabled stages and optional change history.",
|
|
25270
|
+
{
|
|
25271
|
+
name: external_exports.string().describe("Flag name"),
|
|
25272
|
+
includeHistory: external_exports.boolean().optional().describe("Include change history entries. Default false")
|
|
25273
|
+
},
|
|
25274
|
+
async ({ name, includeHistory }) => {
|
|
25275
|
+
try {
|
|
25276
|
+
const flag = await client.getFlag(name, { includeHistory });
|
|
25277
|
+
if (!flag) {
|
|
25278
|
+
return { content: [{ type: "text", text: `Flag "${name}" not found.` }] };
|
|
25279
|
+
}
|
|
25280
|
+
return { content: [{ type: "text", text: formatFlag(flag, flag.history) }] };
|
|
25281
|
+
} catch (err) {
|
|
25282
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
25283
|
+
return { content: [{ type: "text", text: `Failed to get flag: ${message}` }], isError: true };
|
|
25284
|
+
}
|
|
25285
|
+
}
|
|
25286
|
+
);
|
|
25287
|
+
server.tool(
|
|
25288
|
+
"check_flag",
|
|
25289
|
+
"Check whether a feature flag is enabled in a specific stage. Returns a simple enabled/disabled answer.",
|
|
25290
|
+
{
|
|
25291
|
+
name: external_exports.string().describe("Flag name"),
|
|
25292
|
+
stage: STAGE_ENUM.describe("Deployment stage to check against")
|
|
25293
|
+
},
|
|
25294
|
+
async ({ name, stage }) => {
|
|
25295
|
+
try {
|
|
25296
|
+
const enabled = await client.checkFlag(name, stage);
|
|
25297
|
+
const status = enabled ? "enabled" : "disabled";
|
|
25298
|
+
return {
|
|
25299
|
+
content: [{
|
|
25300
|
+
type: "text",
|
|
25301
|
+
text: `Flag "${name}" is **${status}** in stage "${stage}".`
|
|
25302
|
+
}]
|
|
25303
|
+
};
|
|
25304
|
+
} catch (err) {
|
|
25305
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
25306
|
+
return { content: [{ type: "text", text: `Failed to check flag: ${message}` }], isError: true };
|
|
25307
|
+
}
|
|
25308
|
+
}
|
|
25309
|
+
);
|
|
25310
|
+
}
|
|
25311
|
+
function formatFlag(flag, history) {
|
|
25312
|
+
const archived = flag.archivedAt ? " **(archived)**" : "";
|
|
25313
|
+
const enabled = flag.enabledIn.length > 0 ? flag.enabledIn.join(", ") : "(none)";
|
|
25314
|
+
const parts = [
|
|
25315
|
+
`# Flag: ${flag.name}${archived}`,
|
|
25316
|
+
`**Scope:** ${flag.scope}`,
|
|
25317
|
+
`**Enabled in:** ${enabled}`,
|
|
25318
|
+
flag.description ? `**Description:** ${flag.description}` : null,
|
|
25319
|
+
`**Last updated:** ${flag.updatedAt.slice(0, 10)} by ${flag.updatedBy}`
|
|
25320
|
+
].filter(Boolean).join("\n");
|
|
25321
|
+
if (!history?.length) return parts;
|
|
25322
|
+
const historyLines = history.map((h) => {
|
|
25323
|
+
const prev = h.previousEnabledIn.length > 0 ? h.previousEnabledIn.join(", ") : "(none)";
|
|
25324
|
+
const next = h.newEnabledIn.length > 0 ? h.newEnabledIn.join(", ") : "(none)";
|
|
25325
|
+
const note = h.note ? ` \u2014 ${h.note}` : "";
|
|
25326
|
+
return `- ${h.updatedAt.slice(0, 10)} ${h.updatedBy}: ${prev} \u2192 ${next}${note}`;
|
|
25327
|
+
}).join("\n");
|
|
25328
|
+
return `${parts}
|
|
25329
|
+
|
|
25330
|
+
## Change History
|
|
25331
|
+
${historyLines}`;
|
|
25332
|
+
}
|
|
25333
|
+
function formatFlagList(flags, total, nextCursor) {
|
|
25334
|
+
const rows = flags.map((f) => {
|
|
25335
|
+
const enabled = f.enabledIn.length > 0 ? f.enabledIn.join(", ") : "(none)";
|
|
25336
|
+
const archived = f.archivedAt ? " *(archived)*" : "";
|
|
25337
|
+
return `| ${f.name}${archived} | ${enabled} | ${f.updatedAt.slice(0, 10)} |`;
|
|
25338
|
+
});
|
|
25339
|
+
const table = [
|
|
25340
|
+
`# Feature Flags (${total} total, showing ${flags.length})`,
|
|
25341
|
+
"",
|
|
25342
|
+
"| Name | Enabled In | Updated |",
|
|
25343
|
+
"|---|---|---|",
|
|
25344
|
+
...rows
|
|
25345
|
+
].join("\n");
|
|
25346
|
+
if (!nextCursor) return table;
|
|
25347
|
+
return `${table}
|
|
25348
|
+
|
|
25349
|
+
More flags available. Pass \`cursor: "${nextCursor}"\` to list_flags to continue.`;
|
|
25350
|
+
}
|
|
25351
|
+
|
|
25207
25352
|
// ../../libs/harmonica-services/src/mcp/tools/layer-tools.ts
|
|
25208
25353
|
function registerLayerTools(server, ctx, client) {
|
|
25209
25354
|
server.tool(
|
|
@@ -25857,7 +26002,7 @@ var BEAT_QUALITY_DIMENSIONS = [
|
|
|
25857
26002
|
"durable",
|
|
25858
26003
|
"clear",
|
|
25859
26004
|
"strategicallyAligned",
|
|
25860
|
-
"
|
|
26005
|
+
"outcomeOriented"
|
|
25861
26006
|
];
|
|
25862
26007
|
var BeatDimensionScoreSchema = external_exports.object({
|
|
25863
26008
|
dimension: external_exports.enum(BEAT_QUALITY_DIMENSIONS),
|
|
@@ -27109,6 +27254,20 @@ var DISMISSED_NOTE_STATUS = "dismissed";
|
|
|
27109
27254
|
var NOTE_TYPE_VALUES = ["context", "assumption", "constraint", "guidance", "decision", "document"];
|
|
27110
27255
|
var NOTE_STATUS_VALUES = ["active", "unvalidated", "validated", "invalidated", "inProgress", "resolved", "superseded", "dismissed"];
|
|
27111
27256
|
var DECISION_SIGNIFICANCE_VALUES2 = ["strategic", "structural", "implementation"];
|
|
27257
|
+
async function deliveryPolicyBlock(client, note) {
|
|
27258
|
+
if (note.noteType !== "decision") return "";
|
|
27259
|
+
try {
|
|
27260
|
+
const resolution = await client.resolveDeliveryPolicyForNote(note.noteId);
|
|
27261
|
+
return `
|
|
27262
|
+
|
|
27263
|
+
${formatDeliveryPolicy(
|
|
27264
|
+
resolution.resolved ? resolution.policy : void 0,
|
|
27265
|
+
resolution.resolved ? void 0 : resolution.reason
|
|
27266
|
+
)}`;
|
|
27267
|
+
} catch {
|
|
27268
|
+
return "";
|
|
27269
|
+
}
|
|
27270
|
+
}
|
|
27112
27271
|
function registerNoteTools(server, ctx, client) {
|
|
27113
27272
|
server.tool(
|
|
27114
27273
|
"list_notes",
|
|
@@ -27179,7 +27338,7 @@ function registerNoteTools(server, ctx, client) {
|
|
|
27179
27338
|
const SOURCE_DOC_FETCH_LIMIT = 5e3;
|
|
27180
27339
|
let allNotesResult;
|
|
27181
27340
|
try {
|
|
27182
|
-
allNotesResult = await client.
|
|
27341
|
+
allNotesResult = await client.listAllSystemNotes(projectId, filters, SOURCE_DOC_FETCH_LIMIT);
|
|
27183
27342
|
} catch (err) {
|
|
27184
27343
|
const message = err instanceof Error ? err.message : String(err);
|
|
27185
27344
|
return { content: [{ type: "text", text: `Failed to list project notes: ${message}` }], isError: true };
|
|
@@ -27200,7 +27359,7 @@ function registerNoteTools(server, ctx, client) {
|
|
|
27200
27359
|
} else {
|
|
27201
27360
|
let projectNotesResult;
|
|
27202
27361
|
try {
|
|
27203
|
-
projectNotesResult = await client.
|
|
27362
|
+
projectNotesResult = await client.listAllSystemNotes(projectId, filters, limit, cursor);
|
|
27204
27363
|
} catch (err) {
|
|
27205
27364
|
const message = err instanceof Error ? err.message : String(err);
|
|
27206
27365
|
return { content: [{ type: "text", text: `Failed to list project notes: ${message}` }], isError: true };
|
|
@@ -27287,7 +27446,7 @@ function registerNoteTools(server, ctx, client) {
|
|
|
27287
27446
|
async ({ projectId }) => {
|
|
27288
27447
|
await assertProjectInOrg(client, projectId, ctx.orgId);
|
|
27289
27448
|
const DOCUMENT_FETCH_LIMIT = 5e3;
|
|
27290
|
-
const { notes: docs, hasMore: truncated } = await client.
|
|
27449
|
+
const { notes: docs, hasMore: truncated } = await client.listAllSystemNotes(projectId, { noteType: "document" }, DOCUMENT_FETCH_LIMIT);
|
|
27291
27450
|
const visibleDocs = docs.filter((d) => d.status !== DISMISSED_NOTE_STATUS);
|
|
27292
27451
|
const truncationNotice = truncated && visibleDocs.length > 0 ? "\n\n> \u26A0\uFE0F This project has more than 5000 documents. Some documents may not be shown.\n" : "";
|
|
27293
27452
|
const text = formatDocumentList(visibleDocs) + truncationNotice;
|
|
@@ -27325,7 +27484,7 @@ function registerNoteTools(server, ctx, client) {
|
|
|
27325
27484
|
if (!note) {
|
|
27326
27485
|
return { content: [{ type: "text", text: `Note not found: "${noteId}"` }], isError: true };
|
|
27327
27486
|
}
|
|
27328
|
-
const text = formatNoteDetail(note);
|
|
27487
|
+
const text = formatNoteDetail(note) + await deliveryPolicyBlock(client, note);
|
|
27329
27488
|
return { content: [{ type: "text", text }] };
|
|
27330
27489
|
} catch (err) {
|
|
27331
27490
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -28967,7 +29126,7 @@ function formatConsolidationResult(primaryId, primary, archivedBeatIds, notesRea
|
|
|
28967
29126
|
function registerPortfolioCoherenceTools(server, ctx, client) {
|
|
28968
29127
|
server.tool(
|
|
28969
29128
|
"check_portfolio_coherence",
|
|
28970
|
-
"Run a portfolio coherence check on a project. Clusters Beats by semantic similarity to surface fragmentation (merge candidates) and checks what fraction of Beats
|
|
29129
|
+
"Run a portfolio coherence check on a project. Clusters Beats by semantic similarity to surface fragmentation (merge candidates) and checks what fraction of Beats name an outcome their authoriser would recognise. By default returns a taskId immediately \u2014 set wait=true to block and receive the full report inline.",
|
|
28971
29130
|
{
|
|
28972
29131
|
projectId: external_exports.string().min(1).describe("The project ID"),
|
|
28973
29132
|
wait: external_exports.boolean().optional().describe("Block until the check completes and return the report inline. Default: false.")
|
|
@@ -29147,7 +29306,41 @@ function registerPulseReportTools(server, _ctx, client) {
|
|
|
29147
29306
|
);
|
|
29148
29307
|
}
|
|
29149
29308
|
|
|
29309
|
+
// ../../libs/harmonica-services/src/delivery-policy.decide.ts
|
|
29310
|
+
function decideDeliveryStep(resolution, step) {
|
|
29311
|
+
if (!resolution.resolved) return "ask";
|
|
29312
|
+
const value = resolution.policy[step];
|
|
29313
|
+
if (value === true) return "proceed";
|
|
29314
|
+
if (value === false) return "prohibited";
|
|
29315
|
+
return "ask";
|
|
29316
|
+
}
|
|
29317
|
+
|
|
29150
29318
|
// ../../libs/harmonica-services/src/mcp/tools/revision-lifecycle-tools.ts
|
|
29319
|
+
async function checkDeliveryStep(client, revisionId, step) {
|
|
29320
|
+
const resolution = await client.resolveDeliveryPolicy(revisionId).catch(() => ({ resolved: false, reason: "unreadable" }));
|
|
29321
|
+
switch (decideDeliveryStep(resolution, step)) {
|
|
29322
|
+
case "prohibited":
|
|
29323
|
+
return {
|
|
29324
|
+
refusal: `Refused: the System's delivery policy prohibits this step.
|
|
29325
|
+
|
|
29326
|
+
**System:** ${resolution.resolved ? resolution.systemId : "unknown"}
|
|
29327
|
+
**Setting:** \`${step}\` is false
|
|
29328
|
+
|
|
29329
|
+
This is a deliberate choice by whoever configured the System, and \`confirm: true\` does not override it. If it is wrong, a human changes it with \`update_system\` (\`agentDeliveryPolicy\`) \u2014 do not work around it.`
|
|
29330
|
+
};
|
|
29331
|
+
case "proceed":
|
|
29332
|
+
return { notice: `Delivery policy for \`${step}\`: granted \u2014 no need to ask.` };
|
|
29333
|
+
// 'ask' is named rather than left to `default:` so a fourth DeliveryDecision
|
|
29334
|
+
// cannot silently inherit this wording. Both notices state the policy rather
|
|
29335
|
+
// than what happened — they are also appended to responses where the action
|
|
29336
|
+
// itself was a no-op (a dry run, or a gating feature flag being off).
|
|
29337
|
+
case "ask":
|
|
29338
|
+
default:
|
|
29339
|
+
return {
|
|
29340
|
+
notice: `Delivery policy for \`${step}\`: ${resolution.resolved ? "not set" : `unresolvable (${resolution.reason})`} \u2014 nothing blocks you, but ask a human before treating this as approved.`
|
|
29341
|
+
};
|
|
29342
|
+
}
|
|
29343
|
+
}
|
|
29151
29344
|
function registerRevisionLifecycleTools(server, ctx, client) {
|
|
29152
29345
|
server.tool(
|
|
29153
29346
|
"create_revision",
|
|
@@ -29424,6 +29617,10 @@ Accepts the full PR metadata (number, url, branch, state, optional mergeSha). Fi
|
|
|
29424
29617
|
return { content: [{ type: "text", text: `Revision not found: "${revisionId}"` }], isError: true };
|
|
29425
29618
|
}
|
|
29426
29619
|
await assertBeatInOrg(client, revision.beatId, ctx.orgId);
|
|
29620
|
+
const latitude = await checkDeliveryStep(client, revisionId, "openPullRequest");
|
|
29621
|
+
if ("refusal" in latitude) {
|
|
29622
|
+
return { content: [{ type: "text", text: latitude.refusal }], isError: true };
|
|
29623
|
+
}
|
|
29427
29624
|
if (!confirm) {
|
|
29428
29625
|
const shortId = revisionId.split("-").slice(0, 2).join("-");
|
|
29429
29626
|
const lines2 = [
|
|
@@ -29445,6 +29642,7 @@ Accepts the full PR metadata (number, url, branch, state, optional mergeSha). Fi
|
|
|
29445
29642
|
);
|
|
29446
29643
|
}
|
|
29447
29644
|
}
|
|
29645
|
+
lines2.push("", latitude.notice);
|
|
29448
29646
|
return { content: [{ type: "text", text: lines2.join("\n") }] };
|
|
29449
29647
|
}
|
|
29450
29648
|
const result = await client.createPrForRevision(revisionId);
|
|
@@ -29458,7 +29656,9 @@ Accepts the full PR metadata (number, url, branch, state, optional mergeSha). Fi
|
|
|
29458
29656
|
`**PR:** #${result.result.prNumber} \u2014 ${result.result.prUrl}`,
|
|
29459
29657
|
`**Branch:** ${result.result.branch}`,
|
|
29460
29658
|
"",
|
|
29461
|
-
"The revision stays in `draft`. Transition it to `open` once the PR is marked ready for review."
|
|
29659
|
+
"The revision stays in `draft`. Transition it to `open` once the PR is marked ready for review.",
|
|
29660
|
+
"",
|
|
29661
|
+
latitude.notice
|
|
29462
29662
|
];
|
|
29463
29663
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
29464
29664
|
} catch (err) {
|
|
@@ -29483,11 +29683,17 @@ Accepts the full PR metadata (number, url, branch, state, optional mergeSha). Fi
|
|
|
29483
29683
|
return { content: [{ type: "text", text: `Revision not found: "${revisionId}"` }], isError: true };
|
|
29484
29684
|
}
|
|
29485
29685
|
await assertBeatInOrg(client, revision.beatId, ctx.orgId);
|
|
29686
|
+
const latitude = await checkDeliveryStep(client, revisionId, "requestAgentReview");
|
|
29687
|
+
if ("refusal" in latitude) {
|
|
29688
|
+
return { content: [{ type: "text", text: latitude.refusal }], isError: true };
|
|
29689
|
+
}
|
|
29486
29690
|
const result = await client.requestCodeReview({ revisionId, autoMerge, maxIterations, confirm });
|
|
29487
29691
|
if (!result.success) {
|
|
29488
29692
|
return { content: [{ type: "text", text: `Failed to request code review (${result.error.code}): ${result.error.message}` }], isError: true };
|
|
29489
29693
|
}
|
|
29490
|
-
return { content: [{ type: "text", text: result.result.message
|
|
29694
|
+
return { content: [{ type: "text", text: `${result.result.message}
|
|
29695
|
+
|
|
29696
|
+
${latitude.notice}` }] };
|
|
29491
29697
|
} catch (err) {
|
|
29492
29698
|
const message = err instanceof Error ? err.message : String(err);
|
|
29493
29699
|
return { content: [{ type: "text", text: `Failed to request code review: ${message}` }], isError: true };
|
|
@@ -30166,6 +30372,9 @@ async function resolveSnapshotInput(input) {
|
|
|
30166
30372
|
return trimmed;
|
|
30167
30373
|
}
|
|
30168
30374
|
function formatImportSummary(result, targetTeamspaceId) {
|
|
30375
|
+
if (!result.counts || result.counts.beats == null) {
|
|
30376
|
+
throw new Error(`Import of system ${result.projectId} completed but returned no counts \u2014 check worker logs for a TransactionCanceledException`);
|
|
30377
|
+
}
|
|
30169
30378
|
const lines = [
|
|
30170
30379
|
`Imported system: ${result.projectId}`,
|
|
30171
30380
|
` Beats: ${result.counts.beats}`,
|
|
@@ -30346,7 +30555,7 @@ function registerProjectLifecycleTools(server, ctx, client) {
|
|
|
30346
30555
|
if (system.orgId !== ctx.orgId) {
|
|
30347
30556
|
return { content: [{ type: "text", text: `System "${systemId}" is not in this organization` }], isError: true };
|
|
30348
30557
|
}
|
|
30349
|
-
const result = await client.
|
|
30558
|
+
const result = await client.transitionSystemLifecycleState(systemId, targetState, {
|
|
30350
30559
|
actor: { type: "human", id: ctx.user.userId, name: ctx.user.name },
|
|
30351
30560
|
reason,
|
|
30352
30561
|
decisionNoteId
|
|
@@ -30385,9 +30594,7 @@ function normalizeCreateAccountId(accountId) {
|
|
|
30385
30594
|
}
|
|
30386
30595
|
|
|
30387
30596
|
// ../../libs/harmonica-services/src/mcp/tools/system-tools.ts
|
|
30388
|
-
function accountLine(
|
|
30389
|
-
const requested = request["accountId"];
|
|
30390
|
-
if (requested === null || requested === "") return "**Account:** (unlinked)";
|
|
30597
|
+
function accountLine(_request, resolved) {
|
|
30391
30598
|
return resolved ? `**Account:** ${resolved}` : "";
|
|
30392
30599
|
}
|
|
30393
30600
|
var PROJECT_EMBEDDING_FIELDS = ["title", "description", "strategy"];
|
|
@@ -30449,7 +30656,7 @@ function registerProjectTools(server, ctx, client) {
|
|
|
30449
30656
|
description: external_exports.string().optional().describe("New system description"),
|
|
30450
30657
|
strategy: external_exports.string().optional().describe('System strategy markdown \u2014 Org Strategy + System Strategy ("System Coda")'),
|
|
30451
30658
|
teamspaceId: external_exports.string().nullable().optional().describe("Teamspace ID to associate this system with; pass null to remove the association"),
|
|
30452
|
-
accountId: external_exports.string().
|
|
30659
|
+
accountId: external_exports.string().min(1).optional().describe("Account that owns this System \u2014 the client or internal org unit. Omit to leave unchanged. A System cannot be unlinked: reassigning moves it so it is listed under exactly one Account."),
|
|
30453
30660
|
repoOwner: external_exports.string().optional().describe("GitHub repository owner (org or user)"),
|
|
30454
30661
|
repoName: external_exports.string().optional().describe("GitHub repository name"),
|
|
30455
30662
|
repoDefaultBranch: external_exports.string().optional().describe('Default branch for code check (defaults to "main")'),
|
|
@@ -30469,7 +30676,7 @@ function registerProjectTools(server, ctx, client) {
|
|
|
30469
30676
|
return { content: [{ type: "text", text: "No updates provided." }], isError: true };
|
|
30470
30677
|
}
|
|
30471
30678
|
await assertProjectInOrg(client, systemId, ctx.orgId);
|
|
30472
|
-
if (nonEmpty["accountId"] === "") nonEmpty["accountId"]
|
|
30679
|
+
if (nonEmpty["accountId"] === "") delete nonEmpty["accountId"];
|
|
30473
30680
|
if (typeof updates.teamspaceId === "string") {
|
|
30474
30681
|
const teamspace = await client.getTeamspace(updates.teamspaceId);
|
|
30475
30682
|
if (!teamspace) {
|
|
@@ -30543,7 +30750,7 @@ function registerProjectTools(server, ctx, client) {
|
|
|
30543
30750
|
description: external_exports.string().optional().describe("System description"),
|
|
30544
30751
|
strategy: external_exports.string().optional().describe('System strategy markdown \u2014 Org Strategy + System Strategy ("System Coda")'),
|
|
30545
30752
|
teamspaceId: external_exports.string().optional().describe("Teamspace ID to associate this system with"),
|
|
30546
|
-
accountId: external_exports.string().
|
|
30753
|
+
accountId: external_exports.string().min(1).describe("Account that owns this System \u2014 the client or internal org unit. REQUIRED: the Account is the only parent access follows, so a System without one is reachable by nobody."),
|
|
30547
30754
|
repoOwner: external_exports.string().optional().describe("GitHub repository owner (org or user)"),
|
|
30548
30755
|
repoName: external_exports.string().optional().describe("GitHub repository name"),
|
|
30549
30756
|
repoDefaultBranch: external_exports.string().optional().describe('Default branch for code check (defaults to "main")')
|
|
@@ -30591,7 +30798,7 @@ function registerProjectTools(server, ctx, client) {
|
|
|
30591
30798
|
return { content: [{ type: "text", text: `Failed to create system: ${message}` }], isError: true };
|
|
30592
30799
|
}
|
|
30593
30800
|
};
|
|
30594
|
-
server.tool("create_system", "Create a new system in the configured organization.
|
|
30801
|
+
server.tool("create_system", "Create a new system in the configured organization. accountId is required \u2014 it is the Account that owns the System, and the only parent access follows.", createSystemSchema, createSystemHandler);
|
|
30595
30802
|
}
|
|
30596
30803
|
|
|
30597
30804
|
// ../../libs/harmonica-services/src/mcp/tools/teamspace-tools.ts
|
|
@@ -31729,6 +31936,7 @@ function registerAllTools(server, ctx, client, profile) {
|
|
|
31729
31936
|
registerCadenceScheduleTools(server, ctx, client);
|
|
31730
31937
|
registerPulseReportTools(server, ctx, client);
|
|
31731
31938
|
registerDownbeatHarmonyReportTools(server, ctx, client);
|
|
31939
|
+
registerFeatureFlagTools(server, ctx, client);
|
|
31732
31940
|
}
|
|
31733
31941
|
|
|
31734
31942
|
// ../../libs/harmonica-services/src/mcp/resources/beat-resources.ts
|
|
@@ -32091,11 +32299,23 @@ function createHttpClient(config2) {
|
|
|
32091
32299
|
},
|
|
32092
32300
|
updateProject: (projectId, updates) => request("PATCH", `/api/systems/${encodeURIComponent(projectId)}`, updates),
|
|
32093
32301
|
archiveProject: (projectId) => request("PATCH", `/api/systems/${encodeURIComponent(projectId)}/archive`),
|
|
32302
|
+
getSystemFact: async (projectId) => {
|
|
32303
|
+
const result = await request("GET", `/api/systems/${encodeURIComponent(projectId)}/facts`);
|
|
32304
|
+
return result?.data;
|
|
32305
|
+
},
|
|
32094
32306
|
getProjectFact: async (projectId) => {
|
|
32095
32307
|
const result = await request("GET", `/api/systems/${encodeURIComponent(projectId)}/facts`);
|
|
32096
32308
|
return result?.data;
|
|
32097
32309
|
},
|
|
32310
|
+
saveSystemFact: (input) => request("PUT", `/api/systems/${encodeURIComponent(String(input.projectId))}/facts`, input),
|
|
32098
32311
|
saveProjectFact: (input) => request("PUT", `/api/systems/${encodeURIComponent(String(input.projectId))}/facts`, input),
|
|
32312
|
+
transitionSystemLifecycleState: async (projectId, targetState, options) => {
|
|
32313
|
+
return request("POST", `/api/systems/${encodeURIComponent(projectId)}/lifecycle/transitions`, {
|
|
32314
|
+
targetState,
|
|
32315
|
+
reason: options.reason,
|
|
32316
|
+
decisionNoteId: options.decisionNoteId
|
|
32317
|
+
});
|
|
32318
|
+
},
|
|
32099
32319
|
transitionProjectLifecycleState: async (projectId, targetState, options) => {
|
|
32100
32320
|
return request("POST", `/api/systems/${encodeURIComponent(projectId)}/lifecycle/transitions`, {
|
|
32101
32321
|
targetState,
|
|
@@ -32296,6 +32516,24 @@ function createHttpClient(config2) {
|
|
|
32296
32516
|
const result = await request("GET", `/api/systems/${encodeURIComponent(projectId)}/notes${qs}`);
|
|
32297
32517
|
return result?.notes ?? [];
|
|
32298
32518
|
},
|
|
32519
|
+
listAllSystemNotes: async (projectId, filters, limit, cursor) => {
|
|
32520
|
+
const params = new URLSearchParams();
|
|
32521
|
+
if (filters?.noteType) {
|
|
32522
|
+
const types = Array.isArray(filters.noteType) ? filters.noteType : [filters.noteType];
|
|
32523
|
+
types.forEach((t) => params.append("type", t));
|
|
32524
|
+
}
|
|
32525
|
+
if (filters?.excludeNoteType) params.set("excludeType", filters.excludeNoteType);
|
|
32526
|
+
if (filters?.status) params.set("status", filters.status);
|
|
32527
|
+
if (filters?.revisionId) params.set("revisionId", filters.revisionId);
|
|
32528
|
+
if (filters?.beatVersionId) params.set("beatVersionId", filters.beatVersionId);
|
|
32529
|
+
if (filters?.excludeChildren) params.set("excludeChildren", "true");
|
|
32530
|
+
if (filters?.significance) params.set("significance", filters.significance);
|
|
32531
|
+
if (limit !== void 0) params.set("limit", String(limit));
|
|
32532
|
+
if (cursor !== void 0) params.set("cursor", cursor);
|
|
32533
|
+
const qs = params.toString() ? `?${params.toString()}` : "";
|
|
32534
|
+
const result = await request("GET", `/api/systems/${encodeURIComponent(projectId)}/notes/all${qs}`);
|
|
32535
|
+
return { notes: result?.notes ?? [], cursor: result?.cursor, hasMore: result?.hasMore ?? false };
|
|
32536
|
+
},
|
|
32299
32537
|
listAllProjectNotes: async (projectId, filters, limit, cursor) => {
|
|
32300
32538
|
const params = new URLSearchParams();
|
|
32301
32539
|
if (filters?.noteType) {
|
|
@@ -32405,6 +32643,7 @@ function createHttpClient(config2) {
|
|
|
32405
32643
|
return result?.note ?? result;
|
|
32406
32644
|
},
|
|
32407
32645
|
// Gaps are deprecated — replaced by assumption Notes. Return empty results.
|
|
32646
|
+
getSystemGaps: async () => [],
|
|
32408
32647
|
getProjectGaps: async () => [],
|
|
32409
32648
|
updateInformationGap: async () => false,
|
|
32410
32649
|
// Revisions
|
|
@@ -32971,6 +33210,20 @@ function createHttpClient(config2) {
|
|
|
32971
33210
|
);
|
|
32972
33211
|
return result.score;
|
|
32973
33212
|
},
|
|
33213
|
+
rankSystemBeatVersions: async (projectId, lens) => {
|
|
33214
|
+
const result = await request(
|
|
33215
|
+
"GET",
|
|
33216
|
+
`/api/systems/${encodeURIComponent(projectId)}/beat-versions/ranked?lens=${encodeURIComponent(lens)}`
|
|
33217
|
+
);
|
|
33218
|
+
return result.ranked ?? [];
|
|
33219
|
+
},
|
|
33220
|
+
rankSystemBeatVersionsWithLeverage: async (projectId, lens) => {
|
|
33221
|
+
const result = await request(
|
|
33222
|
+
"GET",
|
|
33223
|
+
`/api/systems/${encodeURIComponent(projectId)}/beat-versions/ranked?lens=${encodeURIComponent(lens)}&withLeverage=true`
|
|
33224
|
+
);
|
|
33225
|
+
return result.ranked ?? [];
|
|
33226
|
+
},
|
|
32974
33227
|
rankProjectBeatVersions: async (projectId, lens) => {
|
|
32975
33228
|
const result = await request(
|
|
32976
33229
|
"GET",
|
|
@@ -33202,7 +33455,9 @@ function createHttpClient(config2) {
|
|
|
33202
33455
|
if (!enqueued?.taskId) {
|
|
33203
33456
|
throw new Error("importSystemSnapshotFromUrl: API did not return a taskId");
|
|
33204
33457
|
}
|
|
33205
|
-
|
|
33458
|
+
const importResult = await pollTaskResult(enqueued.taskId);
|
|
33459
|
+
if (importResult == null) throw new Error(`Task ${enqueued.taskId} completed but returned no result \u2014 check worker logs for a TransactionCanceledException`);
|
|
33460
|
+
return importResult;
|
|
33206
33461
|
},
|
|
33207
33462
|
// Embedding similarity
|
|
33208
33463
|
embedProjectEntities: (projectId) => request("POST", `/api/systems/${encodeURIComponent(projectId)}/embeddings/generate`),
|
|
@@ -33293,6 +33548,13 @@ function createHttpClient(config2) {
|
|
|
33293
33548
|
);
|
|
33294
33549
|
return { checks: res?.checks ?? [], nextCursor: res?.nextCursor };
|
|
33295
33550
|
},
|
|
33551
|
+
listLatestSystemChecks: async (projectId) => {
|
|
33552
|
+
const res = await request(
|
|
33553
|
+
"GET",
|
|
33554
|
+
`/api/checks/latest?projectId=${encodeURIComponent(projectId)}`
|
|
33555
|
+
);
|
|
33556
|
+
return res?.checks ?? [];
|
|
33557
|
+
},
|
|
33296
33558
|
listLatestProjectChecks: async (projectId) => {
|
|
33297
33559
|
const res = await request(
|
|
33298
33560
|
"GET",
|
|
@@ -33611,6 +33873,17 @@ function createHttpClient(config2) {
|
|
|
33611
33873
|
if (!res) return { resolved: false, reason: "revision_not_found" };
|
|
33612
33874
|
return res;
|
|
33613
33875
|
},
|
|
33876
|
+
// Mirrors the Revision route. A 404 becomes note_not_found rather than
|
|
33877
|
+
// revision_not_found: reporting the wrong missing entity sends a human
|
|
33878
|
+
// looking for something that was never involved.
|
|
33879
|
+
resolveDeliveryPolicyForNote: async (noteId) => {
|
|
33880
|
+
const res = await request(
|
|
33881
|
+
"GET",
|
|
33882
|
+
`/api/notes/${encodeURIComponent(noteId)}/delivery-policy`
|
|
33883
|
+
);
|
|
33884
|
+
if (!res) return { resolved: false, reason: "note_not_found" };
|
|
33885
|
+
return res;
|
|
33886
|
+
},
|
|
33614
33887
|
listAccountSystems: async (accountId) => {
|
|
33615
33888
|
const res = await request("GET", `/api/accounts/${encodeURIComponent(accountId)}/systems`);
|
|
33616
33889
|
return res?.systems ?? [];
|
|
@@ -33996,6 +34269,17 @@ function createHttpClient(config2) {
|
|
|
33996
34269
|
`/api/notes/${encodeURIComponent(childNoteId)}/used-by`
|
|
33997
34270
|
);
|
|
33998
34271
|
return result?.usedBy;
|
|
34272
|
+
},
|
|
34273
|
+
// Feature flags (B-135 v9) — not available in HTTP transport; the published
|
|
34274
|
+
// npm package has no DynamoDB credentials.
|
|
34275
|
+
listFlags: async () => {
|
|
34276
|
+
throw new Error("Feature flag queries are not available in HTTP transport mode");
|
|
34277
|
+
},
|
|
34278
|
+
getFlag: async () => {
|
|
34279
|
+
throw new Error("Feature flag queries are not available in HTTP transport mode");
|
|
34280
|
+
},
|
|
34281
|
+
checkFlag: async () => {
|
|
34282
|
+
throw new Error("Feature flag queries are not available in HTTP transport mode");
|
|
33999
34283
|
}
|
|
34000
34284
|
};
|
|
34001
34285
|
return client;
|
|
@@ -34046,7 +34330,7 @@ function loadConfig() {
|
|
|
34046
34330
|
};
|
|
34047
34331
|
}
|
|
34048
34332
|
async function main() {
|
|
34049
|
-
console.error(`[harmonica-mcp] v${"3.
|
|
34333
|
+
console.error(`[harmonica-mcp] v${"3.4.0"} starting\u2026`);
|
|
34050
34334
|
const config2 = loadConfig();
|
|
34051
34335
|
const client = createHttpClient({
|
|
34052
34336
|
apiBaseUrl: config2.apiBaseUrl,
|