@codazen/harmonica-mcp 3.3.0 → 3.5.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 +392 -17
- 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"
|
|
@@ -25204,6 +25231,208 @@ ${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
|
+
server.tool(
|
|
25311
|
+
"create_flag",
|
|
25312
|
+
"Create or update a feature flag with the specified enabled stages. Idempotent \u2014 calling with an existing flag name replaces its enabledIn list.",
|
|
25313
|
+
{
|
|
25314
|
+
name: external_exports.string().min(1).describe('Flag name (e.g. "rbac:enforcement")'),
|
|
25315
|
+
enabledIn: external_exports.array(STAGE_ENUM).describe("Stages to enable the flag in"),
|
|
25316
|
+
description: external_exports.string().optional().describe("Human-readable description of what this flag gates"),
|
|
25317
|
+
note: external_exports.string().optional().describe("Reason for this change (recorded in change history)")
|
|
25318
|
+
},
|
|
25319
|
+
async ({ name, enabledIn, description, note }) => {
|
|
25320
|
+
if (!ctx.user.email) {
|
|
25321
|
+
return { content: [{ type: "text", text: "Write operations require an authenticated user with a valid email." }], isError: true };
|
|
25322
|
+
}
|
|
25323
|
+
try {
|
|
25324
|
+
const flag = await client.createFlag(name, enabledIn, { description, note, updatedBy: ctx.user.email });
|
|
25325
|
+
return { content: [{ type: "text", text: formatFlag(flag) }] };
|
|
25326
|
+
} catch (err) {
|
|
25327
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
25328
|
+
return { content: [{ type: "text", text: `Failed to create flag: ${message}` }], isError: true };
|
|
25329
|
+
}
|
|
25330
|
+
}
|
|
25331
|
+
);
|
|
25332
|
+
server.tool(
|
|
25333
|
+
"set_flag",
|
|
25334
|
+
"Update which stages a feature flag is enabled in. Replaces the current enabledIn list entirely.",
|
|
25335
|
+
{
|
|
25336
|
+
name: external_exports.string().min(1).describe("Flag name"),
|
|
25337
|
+
enabledIn: external_exports.array(STAGE_ENUM).describe("New set of stages to enable the flag in. Pass [] to disable everywhere without archiving."),
|
|
25338
|
+
note: external_exports.string().optional().describe("Reason for this change (recorded in change history)")
|
|
25339
|
+
},
|
|
25340
|
+
async ({ name, enabledIn, note }) => {
|
|
25341
|
+
if (!ctx.user.email) {
|
|
25342
|
+
return { content: [{ type: "text", text: "Write operations require an authenticated user with a valid email." }], isError: true };
|
|
25343
|
+
}
|
|
25344
|
+
try {
|
|
25345
|
+
const flag = await client.setFlag(name, enabledIn, { note, updatedBy: ctx.user.email });
|
|
25346
|
+
return { content: [{ type: "text", text: formatFlag(flag) }] };
|
|
25347
|
+
} catch (err) {
|
|
25348
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
25349
|
+
return { content: [{ type: "text", text: `Failed to set flag: ${message}` }], isError: true };
|
|
25350
|
+
}
|
|
25351
|
+
}
|
|
25352
|
+
);
|
|
25353
|
+
server.tool(
|
|
25354
|
+
"archive_flag",
|
|
25355
|
+
"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.",
|
|
25356
|
+
{
|
|
25357
|
+
name: external_exports.string().min(1).describe("Flag name"),
|
|
25358
|
+
note: external_exports.string().optional().describe("Reason for archiving (recorded in change history)")
|
|
25359
|
+
},
|
|
25360
|
+
async ({ name, note }) => {
|
|
25361
|
+
if (!ctx.user.email) {
|
|
25362
|
+
return { content: [{ type: "text", text: "Write operations require an authenticated user with a valid email." }], isError: true };
|
|
25363
|
+
}
|
|
25364
|
+
try {
|
|
25365
|
+
const flag = await client.archiveFlag(name, { note, updatedBy: ctx.user.email });
|
|
25366
|
+
return { content: [{ type: "text", text: formatFlag(flag) }] };
|
|
25367
|
+
} catch (err) {
|
|
25368
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
25369
|
+
return { content: [{ type: "text", text: `Failed to archive flag: ${message}` }], isError: true };
|
|
25370
|
+
}
|
|
25371
|
+
}
|
|
25372
|
+
);
|
|
25373
|
+
server.tool(
|
|
25374
|
+
"unarchive_flag",
|
|
25375
|
+
"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.",
|
|
25376
|
+
{
|
|
25377
|
+
name: external_exports.string().min(1).describe("Flag name"),
|
|
25378
|
+
note: external_exports.string().optional().describe("Reason for restoring (recorded in change history)")
|
|
25379
|
+
},
|
|
25380
|
+
async ({ name, note }) => {
|
|
25381
|
+
if (!ctx.user.email) {
|
|
25382
|
+
return { content: [{ type: "text", text: "Write operations require an authenticated user with a valid email." }], isError: true };
|
|
25383
|
+
}
|
|
25384
|
+
try {
|
|
25385
|
+
const flag = await client.unarchiveFlag(name, { note, updatedBy: ctx.user.email });
|
|
25386
|
+
return { content: [{ type: "text", text: formatFlag(flag) }] };
|
|
25387
|
+
} catch (err) {
|
|
25388
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
25389
|
+
return { content: [{ type: "text", text: `Failed to unarchive flag: ${message}` }], isError: true };
|
|
25390
|
+
}
|
|
25391
|
+
}
|
|
25392
|
+
);
|
|
25393
|
+
}
|
|
25394
|
+
function formatFlag(flag, history) {
|
|
25395
|
+
const archived = flag.archivedAt ? " **(archived)**" : "";
|
|
25396
|
+
const enabled = flag.enabledIn.length > 0 ? flag.enabledIn.join(", ") : "(none)";
|
|
25397
|
+
const parts = [
|
|
25398
|
+
`# Flag: ${flag.name}${archived}`,
|
|
25399
|
+
`**Scope:** ${flag.scope}`,
|
|
25400
|
+
`**Enabled in:** ${enabled}`,
|
|
25401
|
+
flag.description ? `**Description:** ${flag.description}` : null,
|
|
25402
|
+
`**Last updated:** ${flag.updatedAt.slice(0, 10)} by ${flag.updatedBy}`,
|
|
25403
|
+
flag.archivedAt ? `**Archived:** ${flag.archivedAt.slice(0, 10)} by ${flag.archivedBy}` : null
|
|
25404
|
+
].filter(Boolean).join("\n");
|
|
25405
|
+
if (!history?.length) return parts;
|
|
25406
|
+
const historyLines = history.map((h) => {
|
|
25407
|
+
const prev = h.previousEnabledIn.length > 0 ? h.previousEnabledIn.join(", ") : "(none)";
|
|
25408
|
+
const next = h.newEnabledIn.length > 0 ? h.newEnabledIn.join(", ") : "(none)";
|
|
25409
|
+
const note = h.note ? ` \u2014 ${h.note}` : "";
|
|
25410
|
+
return `- ${h.updatedAt.slice(0, 10)} ${h.updatedBy}: ${prev} \u2192 ${next}${note}`;
|
|
25411
|
+
}).join("\n");
|
|
25412
|
+
return `${parts}
|
|
25413
|
+
|
|
25414
|
+
## Change History
|
|
25415
|
+
${historyLines}`;
|
|
25416
|
+
}
|
|
25417
|
+
function formatFlagList(flags, total, nextCursor) {
|
|
25418
|
+
const rows = flags.map((f) => {
|
|
25419
|
+
const enabled = f.enabledIn.length > 0 ? f.enabledIn.join(", ") : "(none)";
|
|
25420
|
+
const archived = f.archivedAt ? " *(archived)*" : "";
|
|
25421
|
+
return `| ${f.name}${archived} | ${enabled} | ${f.updatedAt.slice(0, 10)} |`;
|
|
25422
|
+
});
|
|
25423
|
+
const table = [
|
|
25424
|
+
`# Feature Flags (${total} total, showing ${flags.length})`,
|
|
25425
|
+
"",
|
|
25426
|
+
"| Name | Enabled In | Updated |",
|
|
25427
|
+
"|---|---|---|",
|
|
25428
|
+
...rows
|
|
25429
|
+
].join("\n");
|
|
25430
|
+
if (!nextCursor) return table;
|
|
25431
|
+
return `${table}
|
|
25432
|
+
|
|
25433
|
+
More flags available. Pass \`cursor: "${nextCursor}"\` to list_flags to continue.`;
|
|
25434
|
+
}
|
|
25435
|
+
|
|
25207
25436
|
// ../../libs/harmonica-services/src/mcp/tools/layer-tools.ts
|
|
25208
25437
|
function registerLayerTools(server, ctx, client) {
|
|
25209
25438
|
server.tool(
|
|
@@ -27109,6 +27338,20 @@ var DISMISSED_NOTE_STATUS = "dismissed";
|
|
|
27109
27338
|
var NOTE_TYPE_VALUES = ["context", "assumption", "constraint", "guidance", "decision", "document"];
|
|
27110
27339
|
var NOTE_STATUS_VALUES = ["active", "unvalidated", "validated", "invalidated", "inProgress", "resolved", "superseded", "dismissed"];
|
|
27111
27340
|
var DECISION_SIGNIFICANCE_VALUES2 = ["strategic", "structural", "implementation"];
|
|
27341
|
+
async function deliveryPolicyBlock(client, note) {
|
|
27342
|
+
if (note.noteType !== "decision") return "";
|
|
27343
|
+
try {
|
|
27344
|
+
const resolution = await client.resolveDeliveryPolicyForNote(note.noteId);
|
|
27345
|
+
return `
|
|
27346
|
+
|
|
27347
|
+
${formatDeliveryPolicy(
|
|
27348
|
+
resolution.resolved ? resolution.policy : void 0,
|
|
27349
|
+
resolution.resolved ? void 0 : resolution.reason
|
|
27350
|
+
)}`;
|
|
27351
|
+
} catch {
|
|
27352
|
+
return "";
|
|
27353
|
+
}
|
|
27354
|
+
}
|
|
27112
27355
|
function registerNoteTools(server, ctx, client) {
|
|
27113
27356
|
server.tool(
|
|
27114
27357
|
"list_notes",
|
|
@@ -27179,7 +27422,7 @@ function registerNoteTools(server, ctx, client) {
|
|
|
27179
27422
|
const SOURCE_DOC_FETCH_LIMIT = 5e3;
|
|
27180
27423
|
let allNotesResult;
|
|
27181
27424
|
try {
|
|
27182
|
-
allNotesResult = await client.
|
|
27425
|
+
allNotesResult = await client.listAllSystemNotes(projectId, filters, SOURCE_DOC_FETCH_LIMIT);
|
|
27183
27426
|
} catch (err) {
|
|
27184
27427
|
const message = err instanceof Error ? err.message : String(err);
|
|
27185
27428
|
return { content: [{ type: "text", text: `Failed to list project notes: ${message}` }], isError: true };
|
|
@@ -27200,7 +27443,7 @@ function registerNoteTools(server, ctx, client) {
|
|
|
27200
27443
|
} else {
|
|
27201
27444
|
let projectNotesResult;
|
|
27202
27445
|
try {
|
|
27203
|
-
projectNotesResult = await client.
|
|
27446
|
+
projectNotesResult = await client.listAllSystemNotes(projectId, filters, limit, cursor);
|
|
27204
27447
|
} catch (err) {
|
|
27205
27448
|
const message = err instanceof Error ? err.message : String(err);
|
|
27206
27449
|
return { content: [{ type: "text", text: `Failed to list project notes: ${message}` }], isError: true };
|
|
@@ -27287,7 +27530,7 @@ function registerNoteTools(server, ctx, client) {
|
|
|
27287
27530
|
async ({ projectId }) => {
|
|
27288
27531
|
await assertProjectInOrg(client, projectId, ctx.orgId);
|
|
27289
27532
|
const DOCUMENT_FETCH_LIMIT = 5e3;
|
|
27290
|
-
const { notes: docs, hasMore: truncated } = await client.
|
|
27533
|
+
const { notes: docs, hasMore: truncated } = await client.listAllSystemNotes(projectId, { noteType: "document" }, DOCUMENT_FETCH_LIMIT);
|
|
27291
27534
|
const visibleDocs = docs.filter((d) => d.status !== DISMISSED_NOTE_STATUS);
|
|
27292
27535
|
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
27536
|
const text = formatDocumentList(visibleDocs) + truncationNotice;
|
|
@@ -27325,7 +27568,7 @@ function registerNoteTools(server, ctx, client) {
|
|
|
27325
27568
|
if (!note) {
|
|
27326
27569
|
return { content: [{ type: "text", text: `Note not found: "${noteId}"` }], isError: true };
|
|
27327
27570
|
}
|
|
27328
|
-
const text = formatNoteDetail(note);
|
|
27571
|
+
const text = formatNoteDetail(note) + await deliveryPolicyBlock(client, note);
|
|
27329
27572
|
return { content: [{ type: "text", text }] };
|
|
27330
27573
|
} catch (err) {
|
|
27331
27574
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -29147,7 +29390,41 @@ function registerPulseReportTools(server, _ctx, client) {
|
|
|
29147
29390
|
);
|
|
29148
29391
|
}
|
|
29149
29392
|
|
|
29393
|
+
// ../../libs/harmonica-services/src/delivery-policy.decide.ts
|
|
29394
|
+
function decideDeliveryStep(resolution, step) {
|
|
29395
|
+
if (!resolution.resolved) return "ask";
|
|
29396
|
+
const value = resolution.policy[step];
|
|
29397
|
+
if (value === true) return "proceed";
|
|
29398
|
+
if (value === false) return "prohibited";
|
|
29399
|
+
return "ask";
|
|
29400
|
+
}
|
|
29401
|
+
|
|
29150
29402
|
// ../../libs/harmonica-services/src/mcp/tools/revision-lifecycle-tools.ts
|
|
29403
|
+
async function checkDeliveryStep(client, revisionId, step) {
|
|
29404
|
+
const resolution = await client.resolveDeliveryPolicy(revisionId).catch(() => ({ resolved: false, reason: "unreadable" }));
|
|
29405
|
+
switch (decideDeliveryStep(resolution, step)) {
|
|
29406
|
+
case "prohibited":
|
|
29407
|
+
return {
|
|
29408
|
+
refusal: `Refused: the System's delivery policy prohibits this step.
|
|
29409
|
+
|
|
29410
|
+
**System:** ${resolution.resolved ? resolution.systemId : "unknown"}
|
|
29411
|
+
**Setting:** \`${step}\` is false
|
|
29412
|
+
|
|
29413
|
+
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.`
|
|
29414
|
+
};
|
|
29415
|
+
case "proceed":
|
|
29416
|
+
return { notice: `Delivery policy for \`${step}\`: granted \u2014 no need to ask.` };
|
|
29417
|
+
// 'ask' is named rather than left to `default:` so a fourth DeliveryDecision
|
|
29418
|
+
// cannot silently inherit this wording. Both notices state the policy rather
|
|
29419
|
+
// than what happened — they are also appended to responses where the action
|
|
29420
|
+
// itself was a no-op (a dry run, or a gating feature flag being off).
|
|
29421
|
+
case "ask":
|
|
29422
|
+
default:
|
|
29423
|
+
return {
|
|
29424
|
+
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.`
|
|
29425
|
+
};
|
|
29426
|
+
}
|
|
29427
|
+
}
|
|
29151
29428
|
function registerRevisionLifecycleTools(server, ctx, client) {
|
|
29152
29429
|
server.tool(
|
|
29153
29430
|
"create_revision",
|
|
@@ -29424,6 +29701,10 @@ Accepts the full PR metadata (number, url, branch, state, optional mergeSha). Fi
|
|
|
29424
29701
|
return { content: [{ type: "text", text: `Revision not found: "${revisionId}"` }], isError: true };
|
|
29425
29702
|
}
|
|
29426
29703
|
await assertBeatInOrg(client, revision.beatId, ctx.orgId);
|
|
29704
|
+
const latitude = await checkDeliveryStep(client, revisionId, "openPullRequest");
|
|
29705
|
+
if ("refusal" in latitude) {
|
|
29706
|
+
return { content: [{ type: "text", text: latitude.refusal }], isError: true };
|
|
29707
|
+
}
|
|
29427
29708
|
if (!confirm) {
|
|
29428
29709
|
const shortId = revisionId.split("-").slice(0, 2).join("-");
|
|
29429
29710
|
const lines2 = [
|
|
@@ -29445,6 +29726,7 @@ Accepts the full PR metadata (number, url, branch, state, optional mergeSha). Fi
|
|
|
29445
29726
|
);
|
|
29446
29727
|
}
|
|
29447
29728
|
}
|
|
29729
|
+
lines2.push("", latitude.notice);
|
|
29448
29730
|
return { content: [{ type: "text", text: lines2.join("\n") }] };
|
|
29449
29731
|
}
|
|
29450
29732
|
const result = await client.createPrForRevision(revisionId);
|
|
@@ -29458,7 +29740,9 @@ Accepts the full PR metadata (number, url, branch, state, optional mergeSha). Fi
|
|
|
29458
29740
|
`**PR:** #${result.result.prNumber} \u2014 ${result.result.prUrl}`,
|
|
29459
29741
|
`**Branch:** ${result.result.branch}`,
|
|
29460
29742
|
"",
|
|
29461
|
-
"The revision stays in `draft`. Transition it to `open` once the PR is marked ready for review."
|
|
29743
|
+
"The revision stays in `draft`. Transition it to `open` once the PR is marked ready for review.",
|
|
29744
|
+
"",
|
|
29745
|
+
latitude.notice
|
|
29462
29746
|
];
|
|
29463
29747
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
29464
29748
|
} catch (err) {
|
|
@@ -29483,11 +29767,17 @@ Accepts the full PR metadata (number, url, branch, state, optional mergeSha). Fi
|
|
|
29483
29767
|
return { content: [{ type: "text", text: `Revision not found: "${revisionId}"` }], isError: true };
|
|
29484
29768
|
}
|
|
29485
29769
|
await assertBeatInOrg(client, revision.beatId, ctx.orgId);
|
|
29770
|
+
const latitude = await checkDeliveryStep(client, revisionId, "requestAgentReview");
|
|
29771
|
+
if ("refusal" in latitude) {
|
|
29772
|
+
return { content: [{ type: "text", text: latitude.refusal }], isError: true };
|
|
29773
|
+
}
|
|
29486
29774
|
const result = await client.requestCodeReview({ revisionId, autoMerge, maxIterations, confirm });
|
|
29487
29775
|
if (!result.success) {
|
|
29488
29776
|
return { content: [{ type: "text", text: `Failed to request code review (${result.error.code}): ${result.error.message}` }], isError: true };
|
|
29489
29777
|
}
|
|
29490
|
-
return { content: [{ type: "text", text: result.result.message
|
|
29778
|
+
return { content: [{ type: "text", text: `${result.result.message}
|
|
29779
|
+
|
|
29780
|
+
${latitude.notice}` }] };
|
|
29491
29781
|
} catch (err) {
|
|
29492
29782
|
const message = err instanceof Error ? err.message : String(err);
|
|
29493
29783
|
return { content: [{ type: "text", text: `Failed to request code review: ${message}` }], isError: true };
|
|
@@ -30349,7 +30639,7 @@ function registerProjectLifecycleTools(server, ctx, client) {
|
|
|
30349
30639
|
if (system.orgId !== ctx.orgId) {
|
|
30350
30640
|
return { content: [{ type: "text", text: `System "${systemId}" is not in this organization` }], isError: true };
|
|
30351
30641
|
}
|
|
30352
|
-
const result = await client.
|
|
30642
|
+
const result = await client.transitionSystemLifecycleState(systemId, targetState, {
|
|
30353
30643
|
actor: { type: "human", id: ctx.user.userId, name: ctx.user.name },
|
|
30354
30644
|
reason,
|
|
30355
30645
|
decisionNoteId
|
|
@@ -30388,9 +30678,7 @@ function normalizeCreateAccountId(accountId) {
|
|
|
30388
30678
|
}
|
|
30389
30679
|
|
|
30390
30680
|
// ../../libs/harmonica-services/src/mcp/tools/system-tools.ts
|
|
30391
|
-
function accountLine(
|
|
30392
|
-
const requested = request["accountId"];
|
|
30393
|
-
if (requested === null || requested === "") return "**Account:** (unlinked)";
|
|
30681
|
+
function accountLine(_request, resolved) {
|
|
30394
30682
|
return resolved ? `**Account:** ${resolved}` : "";
|
|
30395
30683
|
}
|
|
30396
30684
|
var PROJECT_EMBEDDING_FIELDS = ["title", "description", "strategy"];
|
|
@@ -30452,7 +30740,7 @@ function registerProjectTools(server, ctx, client) {
|
|
|
30452
30740
|
description: external_exports.string().optional().describe("New system description"),
|
|
30453
30741
|
strategy: external_exports.string().optional().describe('System strategy markdown \u2014 Org Strategy + System Strategy ("System Coda")'),
|
|
30454
30742
|
teamspaceId: external_exports.string().nullable().optional().describe("Teamspace ID to associate this system with; pass null to remove the association"),
|
|
30455
|
-
accountId: external_exports.string().
|
|
30743
|
+
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."),
|
|
30456
30744
|
repoOwner: external_exports.string().optional().describe("GitHub repository owner (org or user)"),
|
|
30457
30745
|
repoName: external_exports.string().optional().describe("GitHub repository name"),
|
|
30458
30746
|
repoDefaultBranch: external_exports.string().optional().describe('Default branch for code check (defaults to "main")'),
|
|
@@ -30472,7 +30760,7 @@ function registerProjectTools(server, ctx, client) {
|
|
|
30472
30760
|
return { content: [{ type: "text", text: "No updates provided." }], isError: true };
|
|
30473
30761
|
}
|
|
30474
30762
|
await assertProjectInOrg(client, systemId, ctx.orgId);
|
|
30475
|
-
if (nonEmpty["accountId"] === "") nonEmpty["accountId"]
|
|
30763
|
+
if (nonEmpty["accountId"] === "") delete nonEmpty["accountId"];
|
|
30476
30764
|
if (typeof updates.teamspaceId === "string") {
|
|
30477
30765
|
const teamspace = await client.getTeamspace(updates.teamspaceId);
|
|
30478
30766
|
if (!teamspace) {
|
|
@@ -30546,7 +30834,7 @@ function registerProjectTools(server, ctx, client) {
|
|
|
30546
30834
|
description: external_exports.string().optional().describe("System description"),
|
|
30547
30835
|
strategy: external_exports.string().optional().describe('System strategy markdown \u2014 Org Strategy + System Strategy ("System Coda")'),
|
|
30548
30836
|
teamspaceId: external_exports.string().optional().describe("Teamspace ID to associate this system with"),
|
|
30549
|
-
accountId: external_exports.string().
|
|
30837
|
+
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."),
|
|
30550
30838
|
repoOwner: external_exports.string().optional().describe("GitHub repository owner (org or user)"),
|
|
30551
30839
|
repoName: external_exports.string().optional().describe("GitHub repository name"),
|
|
30552
30840
|
repoDefaultBranch: external_exports.string().optional().describe('Default branch for code check (defaults to "main")')
|
|
@@ -30594,7 +30882,7 @@ function registerProjectTools(server, ctx, client) {
|
|
|
30594
30882
|
return { content: [{ type: "text", text: `Failed to create system: ${message}` }], isError: true };
|
|
30595
30883
|
}
|
|
30596
30884
|
};
|
|
30597
|
-
server.tool("create_system", "Create a new system in the configured organization.
|
|
30885
|
+
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);
|
|
30598
30886
|
}
|
|
30599
30887
|
|
|
30600
30888
|
// ../../libs/harmonica-services/src/mcp/tools/teamspace-tools.ts
|
|
@@ -31732,6 +32020,7 @@ function registerAllTools(server, ctx, client, profile) {
|
|
|
31732
32020
|
registerCadenceScheduleTools(server, ctx, client);
|
|
31733
32021
|
registerPulseReportTools(server, ctx, client);
|
|
31734
32022
|
registerDownbeatHarmonyReportTools(server, ctx, client);
|
|
32023
|
+
registerFeatureFlagTools(server, ctx, client);
|
|
31735
32024
|
}
|
|
31736
32025
|
|
|
31737
32026
|
// ../../libs/harmonica-services/src/mcp/resources/beat-resources.ts
|
|
@@ -32094,11 +32383,23 @@ function createHttpClient(config2) {
|
|
|
32094
32383
|
},
|
|
32095
32384
|
updateProject: (projectId, updates) => request("PATCH", `/api/systems/${encodeURIComponent(projectId)}`, updates),
|
|
32096
32385
|
archiveProject: (projectId) => request("PATCH", `/api/systems/${encodeURIComponent(projectId)}/archive`),
|
|
32386
|
+
getSystemFact: async (projectId) => {
|
|
32387
|
+
const result = await request("GET", `/api/systems/${encodeURIComponent(projectId)}/facts`);
|
|
32388
|
+
return result?.data;
|
|
32389
|
+
},
|
|
32097
32390
|
getProjectFact: async (projectId) => {
|
|
32098
32391
|
const result = await request("GET", `/api/systems/${encodeURIComponent(projectId)}/facts`);
|
|
32099
32392
|
return result?.data;
|
|
32100
32393
|
},
|
|
32394
|
+
saveSystemFact: (input) => request("PUT", `/api/systems/${encodeURIComponent(String(input.projectId))}/facts`, input),
|
|
32101
32395
|
saveProjectFact: (input) => request("PUT", `/api/systems/${encodeURIComponent(String(input.projectId))}/facts`, input),
|
|
32396
|
+
transitionSystemLifecycleState: async (projectId, targetState, options) => {
|
|
32397
|
+
return request("POST", `/api/systems/${encodeURIComponent(projectId)}/lifecycle/transitions`, {
|
|
32398
|
+
targetState,
|
|
32399
|
+
reason: options.reason,
|
|
32400
|
+
decisionNoteId: options.decisionNoteId
|
|
32401
|
+
});
|
|
32402
|
+
},
|
|
32102
32403
|
transitionProjectLifecycleState: async (projectId, targetState, options) => {
|
|
32103
32404
|
return request("POST", `/api/systems/${encodeURIComponent(projectId)}/lifecycle/transitions`, {
|
|
32104
32405
|
targetState,
|
|
@@ -32299,6 +32600,24 @@ function createHttpClient(config2) {
|
|
|
32299
32600
|
const result = await request("GET", `/api/systems/${encodeURIComponent(projectId)}/notes${qs}`);
|
|
32300
32601
|
return result?.notes ?? [];
|
|
32301
32602
|
},
|
|
32603
|
+
listAllSystemNotes: async (projectId, filters, limit, cursor) => {
|
|
32604
|
+
const params = new URLSearchParams();
|
|
32605
|
+
if (filters?.noteType) {
|
|
32606
|
+
const types = Array.isArray(filters.noteType) ? filters.noteType : [filters.noteType];
|
|
32607
|
+
types.forEach((t) => params.append("type", t));
|
|
32608
|
+
}
|
|
32609
|
+
if (filters?.excludeNoteType) params.set("excludeType", filters.excludeNoteType);
|
|
32610
|
+
if (filters?.status) params.set("status", filters.status);
|
|
32611
|
+
if (filters?.revisionId) params.set("revisionId", filters.revisionId);
|
|
32612
|
+
if (filters?.beatVersionId) params.set("beatVersionId", filters.beatVersionId);
|
|
32613
|
+
if (filters?.excludeChildren) params.set("excludeChildren", "true");
|
|
32614
|
+
if (filters?.significance) params.set("significance", filters.significance);
|
|
32615
|
+
if (limit !== void 0) params.set("limit", String(limit));
|
|
32616
|
+
if (cursor !== void 0) params.set("cursor", cursor);
|
|
32617
|
+
const qs = params.toString() ? `?${params.toString()}` : "";
|
|
32618
|
+
const result = await request("GET", `/api/systems/${encodeURIComponent(projectId)}/notes/all${qs}`);
|
|
32619
|
+
return { notes: result?.notes ?? [], cursor: result?.cursor, hasMore: result?.hasMore ?? false };
|
|
32620
|
+
},
|
|
32302
32621
|
listAllProjectNotes: async (projectId, filters, limit, cursor) => {
|
|
32303
32622
|
const params = new URLSearchParams();
|
|
32304
32623
|
if (filters?.noteType) {
|
|
@@ -32408,6 +32727,7 @@ function createHttpClient(config2) {
|
|
|
32408
32727
|
return result?.note ?? result;
|
|
32409
32728
|
},
|
|
32410
32729
|
// Gaps are deprecated — replaced by assumption Notes. Return empty results.
|
|
32730
|
+
getSystemGaps: async () => [],
|
|
32411
32731
|
getProjectGaps: async () => [],
|
|
32412
32732
|
updateInformationGap: async () => false,
|
|
32413
32733
|
// Revisions
|
|
@@ -32974,6 +33294,20 @@ function createHttpClient(config2) {
|
|
|
32974
33294
|
);
|
|
32975
33295
|
return result.score;
|
|
32976
33296
|
},
|
|
33297
|
+
rankSystemBeatVersions: async (projectId, lens) => {
|
|
33298
|
+
const result = await request(
|
|
33299
|
+
"GET",
|
|
33300
|
+
`/api/systems/${encodeURIComponent(projectId)}/beat-versions/ranked?lens=${encodeURIComponent(lens)}`
|
|
33301
|
+
);
|
|
33302
|
+
return result.ranked ?? [];
|
|
33303
|
+
},
|
|
33304
|
+
rankSystemBeatVersionsWithLeverage: async (projectId, lens) => {
|
|
33305
|
+
const result = await request(
|
|
33306
|
+
"GET",
|
|
33307
|
+
`/api/systems/${encodeURIComponent(projectId)}/beat-versions/ranked?lens=${encodeURIComponent(lens)}&withLeverage=true`
|
|
33308
|
+
);
|
|
33309
|
+
return result.ranked ?? [];
|
|
33310
|
+
},
|
|
32977
33311
|
rankProjectBeatVersions: async (projectId, lens) => {
|
|
32978
33312
|
const result = await request(
|
|
32979
33313
|
"GET",
|
|
@@ -33298,6 +33632,13 @@ function createHttpClient(config2) {
|
|
|
33298
33632
|
);
|
|
33299
33633
|
return { checks: res?.checks ?? [], nextCursor: res?.nextCursor };
|
|
33300
33634
|
},
|
|
33635
|
+
listLatestSystemChecks: async (projectId) => {
|
|
33636
|
+
const res = await request(
|
|
33637
|
+
"GET",
|
|
33638
|
+
`/api/checks/latest?projectId=${encodeURIComponent(projectId)}`
|
|
33639
|
+
);
|
|
33640
|
+
return res?.checks ?? [];
|
|
33641
|
+
},
|
|
33301
33642
|
listLatestProjectChecks: async (projectId) => {
|
|
33302
33643
|
const res = await request(
|
|
33303
33644
|
"GET",
|
|
@@ -33616,6 +33957,17 @@ function createHttpClient(config2) {
|
|
|
33616
33957
|
if (!res) return { resolved: false, reason: "revision_not_found" };
|
|
33617
33958
|
return res;
|
|
33618
33959
|
},
|
|
33960
|
+
// Mirrors the Revision route. A 404 becomes note_not_found rather than
|
|
33961
|
+
// revision_not_found: reporting the wrong missing entity sends a human
|
|
33962
|
+
// looking for something that was never involved.
|
|
33963
|
+
resolveDeliveryPolicyForNote: async (noteId) => {
|
|
33964
|
+
const res = await request(
|
|
33965
|
+
"GET",
|
|
33966
|
+
`/api/notes/${encodeURIComponent(noteId)}/delivery-policy`
|
|
33967
|
+
);
|
|
33968
|
+
if (!res) return { resolved: false, reason: "note_not_found" };
|
|
33969
|
+
return res;
|
|
33970
|
+
},
|
|
33619
33971
|
listAccountSystems: async (accountId) => {
|
|
33620
33972
|
const res = await request("GET", `/api/accounts/${encodeURIComponent(accountId)}/systems`);
|
|
33621
33973
|
return res?.systems ?? [];
|
|
@@ -34001,6 +34353,29 @@ function createHttpClient(config2) {
|
|
|
34001
34353
|
`/api/notes/${encodeURIComponent(childNoteId)}/used-by`
|
|
34002
34354
|
);
|
|
34003
34355
|
return result?.usedBy;
|
|
34356
|
+
},
|
|
34357
|
+
// Feature flags (B-135 v9/v10) — not available in HTTP transport; the published
|
|
34358
|
+
// npm package has no DynamoDB credentials.
|
|
34359
|
+
listFlags: async () => {
|
|
34360
|
+
throw new Error("Feature flag queries are not available in HTTP transport mode");
|
|
34361
|
+
},
|
|
34362
|
+
getFlag: async () => {
|
|
34363
|
+
throw new Error("Feature flag queries are not available in HTTP transport mode");
|
|
34364
|
+
},
|
|
34365
|
+
checkFlag: async () => {
|
|
34366
|
+
throw new Error("Feature flag queries are not available in HTTP transport mode");
|
|
34367
|
+
},
|
|
34368
|
+
createFlag: async () => {
|
|
34369
|
+
throw new Error("Feature flag writes are not available in HTTP transport mode");
|
|
34370
|
+
},
|
|
34371
|
+
setFlag: async () => {
|
|
34372
|
+
throw new Error("Feature flag writes are not available in HTTP transport mode");
|
|
34373
|
+
},
|
|
34374
|
+
archiveFlag: async () => {
|
|
34375
|
+
throw new Error("Feature flag writes are not available in HTTP transport mode");
|
|
34376
|
+
},
|
|
34377
|
+
unarchiveFlag: async () => {
|
|
34378
|
+
throw new Error("Feature flag writes are not available in HTTP transport mode");
|
|
34004
34379
|
}
|
|
34005
34380
|
};
|
|
34006
34381
|
return client;
|
|
@@ -34051,7 +34426,7 @@ function loadConfig() {
|
|
|
34051
34426
|
};
|
|
34052
34427
|
}
|
|
34053
34428
|
async function main() {
|
|
34054
|
-
console.error(`[harmonica-mcp] v${"3.
|
|
34429
|
+
console.error(`[harmonica-mcp] v${"3.5.0"} starting\u2026`);
|
|
34055
34430
|
const config2 = loadConfig();
|
|
34056
34431
|
const client = createHttpClient({
|
|
34057
34432
|
apiBaseUrl: config2.apiBaseUrl,
|