@codazen/harmonica-mcp 1.1.0 → 2.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.
- package/dist/index.js +503 -404
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -22147,22 +22147,22 @@ function registerBarTools(server, _ctx, client) {
|
|
|
22147
22147
|
// ../../libs/harmonica-services/src/mcp/tools/baseline-tools.ts
|
|
22148
22148
|
function registerBaselineTools(server, ctx, client) {
|
|
22149
22149
|
server.tool(
|
|
22150
|
-
"
|
|
22151
|
-
"Create Rev 1 baseline revisions for all eligible Beats in a
|
|
22150
|
+
"baseline_system",
|
|
22151
|
+
"Create Rev 1 baseline revisions for all eligible Beats in a system, transition them to live, and run build_quality checks. Returns a taskId to poll for progress. Skips Beats that already have revisions or are archived/deprecated.",
|
|
22152
22152
|
{
|
|
22153
|
-
|
|
22153
|
+
systemId: external_exports.string().describe("The system ID to baseline"),
|
|
22154
22154
|
beatIds: external_exports.array(external_exports.string()).optional().describe("Optional: only baseline these specific beat IDs"),
|
|
22155
22155
|
runChecks: external_exports.boolean().optional().default(true).describe("Whether to run build_quality checks (default: true)"),
|
|
22156
22156
|
localPath: external_exports.string().optional().describe("Absolute local filesystem path to the repo \u2014 skips git clone and drives revision state from code analysis (requires HARMONICA_ALLOW_LOCAL_REPO=true)")
|
|
22157
22157
|
},
|
|
22158
|
-
async ({
|
|
22158
|
+
async ({ systemId, beatIds, runChecks, localPath }) => {
|
|
22159
22159
|
try {
|
|
22160
|
-
await assertProjectInOrg(client,
|
|
22161
|
-
const task = await client.baselineProject(
|
|
22160
|
+
await assertProjectInOrg(client, systemId, ctx.orgId);
|
|
22161
|
+
const task = await client.baselineProject(systemId, { beatIds, runChecks, localPath });
|
|
22162
22162
|
return {
|
|
22163
22163
|
content: [{
|
|
22164
22164
|
type: "text",
|
|
22165
|
-
text: `
|
|
22165
|
+
text: `System baseline started. Job ID: ${task.taskId}
|
|
22166
22166
|
|
|
22167
22167
|
Poll progress with: get_job_status({ jobId: "${task.taskId}" })`
|
|
22168
22168
|
}]
|
|
@@ -22598,7 +22598,7 @@ ${beat.description}`);
|
|
|
22598
22598
|
return sections.join("\n");
|
|
22599
22599
|
}
|
|
22600
22600
|
|
|
22601
|
-
// ../../libs/harmonica-services/src/mcp/formatters/
|
|
22601
|
+
// ../../libs/harmonica-services/src/mcp/formatters/system-formatter.ts
|
|
22602
22602
|
function formatProjectSummaryTable(projects) {
|
|
22603
22603
|
if (projects.length === 0) return "_No projects found._";
|
|
22604
22604
|
const header = "| Project ID | Title | Status |\n|------------|-------|--------|";
|
|
@@ -23018,7 +23018,7 @@ function registerBeatTools(server, ctx, client) {
|
|
|
23018
23018
|
},
|
|
23019
23019
|
async ({ projectId, offset = 0, limit = 100, includeArchived }) => {
|
|
23020
23020
|
await assertProjectInOrg(client, projectId, ctx.orgId);
|
|
23021
|
-
let beats = await client.
|
|
23021
|
+
let beats = await client.listSystemBeats(projectId);
|
|
23022
23022
|
if (!includeArchived) {
|
|
23023
23023
|
beats = beats.filter((b) => b.beatStatus !== "archived");
|
|
23024
23024
|
}
|
|
@@ -23803,7 +23803,7 @@ function registerCheckTools(server, ctx, client) {
|
|
|
23803
23803
|
checks = await client.listBeatChecks(beatId, projectId, checkType);
|
|
23804
23804
|
scope = `beat ${beatId}`;
|
|
23805
23805
|
} else {
|
|
23806
|
-
const result = await client.
|
|
23806
|
+
const result = await client.listSystemChecks(projectId, checkType, { limit: 200 });
|
|
23807
23807
|
checks = result.checks;
|
|
23808
23808
|
scope = `project ${projectId}`;
|
|
23809
23809
|
}
|
|
@@ -24964,14 +24964,14 @@ ${rows.join("\n\n")}`;
|
|
|
24964
24964
|
}
|
|
24965
24965
|
function registerEmbeddingTools(server, ctx, client) {
|
|
24966
24966
|
server.tool(
|
|
24967
|
-
"
|
|
24968
|
-
"Batch-embed all Notes and Beats in a
|
|
24967
|
+
"embed_system_entities",
|
|
24968
|
+
"Batch-embed all Notes and Beats in a system. Generates vector embeddings for similarity search. Run this to populate or refresh embeddings.",
|
|
24969
24969
|
{
|
|
24970
|
-
|
|
24970
|
+
systemId: external_exports.string().describe("The system ID to embed entities for")
|
|
24971
24971
|
},
|
|
24972
|
-
async ({
|
|
24973
|
-
await assertProjectInOrg(client,
|
|
24974
|
-
const result = await client.embedProjectEntities(
|
|
24972
|
+
async ({ systemId }) => {
|
|
24973
|
+
await assertProjectInOrg(client, systemId, ctx.orgId);
|
|
24974
|
+
const result = await client.embedProjectEntities(systemId);
|
|
24975
24975
|
const text = `## Embedding Results
|
|
24976
24976
|
|
|
24977
24977
|
- Embedded: ${result.embedded}
|
|
@@ -26759,6 +26759,14 @@ var MeasureApiSchema = external_exports.object({
|
|
|
26759
26759
|
var MeasuresResponseSchema = external_exports.object({ measures: external_exports.array(MeasureApiSchema) }).passthrough();
|
|
26760
26760
|
var MeasureResponseSchema = external_exports.object({ measure: MeasureApiSchema }).passthrough();
|
|
26761
26761
|
var WorkItemStatusSchema = external_exports.enum(["not_started", "in_progress", "blocked", "done"]);
|
|
26762
|
+
var WORK_ITEM_EFFORT_SIZES = [
|
|
26763
|
+
"XS",
|
|
26764
|
+
"S",
|
|
26765
|
+
"M",
|
|
26766
|
+
"L",
|
|
26767
|
+
"XL"
|
|
26768
|
+
];
|
|
26769
|
+
var WorkItemEffortSizeSchema = external_exports.enum(WORK_ITEM_EFFORT_SIZES);
|
|
26762
26770
|
var HumanAssigneeApiSchema = external_exports.object({
|
|
26763
26771
|
name: external_exports.string(),
|
|
26764
26772
|
email: external_exports.string()
|
|
@@ -26769,10 +26777,16 @@ var WorkItemApiSchema = external_exports.object({
|
|
|
26769
26777
|
teamspaceId: external_exports.string(),
|
|
26770
26778
|
orgId: external_exports.string(),
|
|
26771
26779
|
title: external_exports.string(),
|
|
26772
|
-
|
|
26780
|
+
/** Optional since N-4E09-6672 — a work item may be unowned. */
|
|
26781
|
+
owner: HumanAssigneeApiSchema.optional(),
|
|
26773
26782
|
status: WorkItemStatusSchema,
|
|
26774
26783
|
committedEstimateHours: external_exports.number().optional(),
|
|
26775
26784
|
actualHours: external_exports.number().optional(),
|
|
26785
|
+
/** Delivery lens — rough t-shirt effort sizing (N-4E09-6672). */
|
|
26786
|
+
effortSize: WorkItemEffortSizeSchema.optional(),
|
|
26787
|
+
/** Commercial lens — deliberately unblended with `effortSize` (N-4E09-6672). */
|
|
26788
|
+
valueUnits: external_exports.number().optional(),
|
|
26789
|
+
costPerValueUnit: external_exports.number().optional(),
|
|
26776
26790
|
/** Optional capability-lineage link to a Beat Version (v2, N-4E09-6574). */
|
|
26777
26791
|
beatVersionId: external_exports.string().optional(),
|
|
26778
26792
|
createdBy: external_exports.string().optional(),
|
|
@@ -28593,31 +28607,31 @@ function registerOrganizationTools(server, ctx, client) {
|
|
|
28593
28607
|
}
|
|
28594
28608
|
);
|
|
28595
28609
|
server.tool(
|
|
28596
|
-
"
|
|
28597
|
-
"Transfer a
|
|
28610
|
+
"transfer_system",
|
|
28611
|
+
"Transfer a system from its current organization to a different one. The system must belong to the configured organization.",
|
|
28598
28612
|
{
|
|
28599
|
-
|
|
28613
|
+
systemId: external_exports.string().describe("The system ID to transfer"),
|
|
28600
28614
|
targetOrgId: external_exports.string().describe("The target organization ID")
|
|
28601
28615
|
},
|
|
28602
|
-
async ({
|
|
28603
|
-
const
|
|
28604
|
-
if (!
|
|
28605
|
-
return { content: [{ type: "text", text: `
|
|
28616
|
+
async ({ systemId, targetOrgId }) => {
|
|
28617
|
+
const system = await client.getSystem(systemId);
|
|
28618
|
+
if (!system) {
|
|
28619
|
+
return { content: [{ type: "text", text: `System not found: ${systemId}` }], isError: true };
|
|
28606
28620
|
}
|
|
28607
|
-
if (
|
|
28608
|
-
return { content: [{ type: "text", text: `
|
|
28621
|
+
if (system.orgId !== ctx.orgId) {
|
|
28622
|
+
return { content: [{ type: "text", text: `System "${systemId}" does not belong to the configured organization.` }], isError: true };
|
|
28609
28623
|
}
|
|
28610
|
-
const result = await client.transferProject(ctx.orgId,
|
|
28624
|
+
const result = await client.transferProject(ctx.orgId, systemId, targetOrgId);
|
|
28611
28625
|
if (result.error) {
|
|
28612
28626
|
return { content: [{ type: "text", text: result.error }], isError: true };
|
|
28613
28627
|
}
|
|
28614
28628
|
const transferred = result.project;
|
|
28615
28629
|
if (!transferred) {
|
|
28616
|
-
return { content: [{ type: "text", text: "Transfer succeeded but
|
|
28630
|
+
return { content: [{ type: "text", text: "Transfer succeeded but system data was not returned." }], isError: true };
|
|
28617
28631
|
}
|
|
28618
|
-
const text = `
|
|
28632
|
+
const text = `System transferred.
|
|
28619
28633
|
|
|
28620
|
-
**ID:** ${transferred.projectId}
|
|
28634
|
+
**System ID:** ${transferred.projectId}
|
|
28621
28635
|
**Title:** ${transferred.title}
|
|
28622
28636
|
**New Org:** ${transferred.orgId}`;
|
|
28623
28637
|
return { content: [{ type: "text", text }] };
|
|
@@ -28882,7 +28896,7 @@ function registerPortfolioCoherenceTools(server, ctx, client) {
|
|
|
28882
28896
|
isError: true
|
|
28883
28897
|
};
|
|
28884
28898
|
}
|
|
28885
|
-
const allProjectBeats = await client.
|
|
28899
|
+
const allProjectBeats = await client.listSystemBeats(projectId);
|
|
28886
28900
|
const beatMap = new Map(allProjectBeats.map((b) => [b.beatId, b]));
|
|
28887
28901
|
const primaryBeat = beatMap.get(resolvedPrimaryId);
|
|
28888
28902
|
if (!primaryBeat) {
|
|
@@ -28967,313 +28981,6 @@ function registerPortfolioCoherenceTools(server, ctx, client) {
|
|
|
28967
28981
|
);
|
|
28968
28982
|
}
|
|
28969
28983
|
|
|
28970
|
-
// ../../libs/harmonica-services/src/mcp/tools/project-lifecycle-tools.ts
|
|
28971
|
-
function registerProjectLifecycleTools(server, ctx, client) {
|
|
28972
|
-
server.tool(
|
|
28973
|
-
"transition_project_lifecycle",
|
|
28974
|
-
"Advance a Project's capability-maturity lifecycle state. Allowed transitions follow the state machine (Concept \u2192 Incubating \u2192 Piloting \u2192 Activated \u2192 Commercializing \u2192 Scaled, plus paused/killed/sunset/archived exits). The activated \u2192 commercializing edge requires a `decisionNoteId` pointing to a project-scoped Decision Note (the governance review). Other gates are advisory.",
|
|
28975
|
-
{
|
|
28976
|
-
projectId: external_exports.string().describe("The project ID"),
|
|
28977
|
-
targetState: external_exports.enum([
|
|
28978
|
-
"concept",
|
|
28979
|
-
"incubating",
|
|
28980
|
-
"piloting",
|
|
28981
|
-
"activated",
|
|
28982
|
-
"commercializing",
|
|
28983
|
-
"scaled",
|
|
28984
|
-
"paused",
|
|
28985
|
-
"killed",
|
|
28986
|
-
"sunset",
|
|
28987
|
-
"archived"
|
|
28988
|
-
]).describe("Target lifecycle state. Active: concept, incubating, piloting, activated, commercializing, scaled. Exits: paused, killed, sunset, archived."),
|
|
28989
|
-
reason: external_exports.string().optional().describe("Why this transition is being made (recorded in audit metadata)"),
|
|
28990
|
-
decisionNoteId: external_exports.string().optional().describe("Note ID of the governance Decision Note. Required for the activated \u2192 commercializing transition; optional otherwise.")
|
|
28991
|
-
},
|
|
28992
|
-
async ({ projectId, targetState, reason, decisionNoteId }) => {
|
|
28993
|
-
try {
|
|
28994
|
-
const project = await client.getSystem(projectId);
|
|
28995
|
-
if (!project) {
|
|
28996
|
-
return { content: [{ type: "text", text: `Project not found: "${projectId}"` }], isError: true };
|
|
28997
|
-
}
|
|
28998
|
-
if (project.orgId !== ctx.orgId) {
|
|
28999
|
-
return { content: [{ type: "text", text: `Project "${projectId}" is not in this organization` }], isError: true };
|
|
29000
|
-
}
|
|
29001
|
-
const result = await client.transitionProjectLifecycleState(projectId, targetState, {
|
|
29002
|
-
actor: { type: "human", id: ctx.user.userId, name: ctx.user.name },
|
|
29003
|
-
reason,
|
|
29004
|
-
decisionNoteId
|
|
29005
|
-
});
|
|
29006
|
-
if (!result.success) {
|
|
29007
|
-
const failedGates = result.error?.failedGates?.length ? ` (failed gates: ${result.error.failedGates.join(", ")})` : "";
|
|
29008
|
-
return {
|
|
29009
|
-
content: [{ type: "text", text: `Transition failed: ${result.error?.message ?? "Unknown error"}${failedGates}` }],
|
|
29010
|
-
isError: true
|
|
29011
|
-
};
|
|
29012
|
-
}
|
|
29013
|
-
const lines = [
|
|
29014
|
-
`Project lifecycle transitioned successfully.`,
|
|
29015
|
-
"",
|
|
29016
|
-
`**Project:** ${projectId}`,
|
|
29017
|
-
`**Title:** ${project.title}`,
|
|
29018
|
-
`**Transition:** ${result.previousState} \u2192 ${result.newState}`
|
|
29019
|
-
];
|
|
29020
|
-
if (reason) lines.push(`**Reason:** ${reason}`);
|
|
29021
|
-
if (decisionNoteId) lines.push(`**Decision Note:** ${decisionNoteId}`);
|
|
29022
|
-
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
29023
|
-
} catch (err) {
|
|
29024
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
29025
|
-
return { content: [{ type: "text", text: `Failed to transition project lifecycle: ${message}` }], isError: true };
|
|
29026
|
-
}
|
|
29027
|
-
}
|
|
29028
|
-
);
|
|
29029
|
-
}
|
|
29030
|
-
|
|
29031
|
-
// ../../libs/harmonica-services/src/mcp/tools/project-tools.ts
|
|
29032
|
-
var import_crypto5 = require("crypto");
|
|
29033
|
-
function accountLine(request, resolved) {
|
|
29034
|
-
const requested = request["accountId"];
|
|
29035
|
-
if (requested === null || requested === "") return "**Account:** (unlinked)";
|
|
29036
|
-
return resolved ? `**Account:** ${resolved}` : "";
|
|
29037
|
-
}
|
|
29038
|
-
var PROJECT_EMBEDDING_FIELDS = ["title", "description", "strategy"];
|
|
29039
|
-
function registerProjectTools(server, ctx, client) {
|
|
29040
|
-
const listSystemsHandler = async ({ teamspaceId }) => {
|
|
29041
|
-
try {
|
|
29042
|
-
const projects = await client.listOrgSystems(ctx.orgId);
|
|
29043
|
-
const trimmed = teamspaceId?.trim();
|
|
29044
|
-
const filtered = trimmed ? projects.filter((p) => p.teamspaceId === trimmed) : projects;
|
|
29045
|
-
const text = formatProjectSummaryTable(filtered);
|
|
29046
|
-
return { content: [{ type: "text", text }] };
|
|
29047
|
-
} catch (err) {
|
|
29048
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
29049
|
-
return { content: [{ type: "text", text: `Failed to list systems: ${message}` }], isError: true };
|
|
29050
|
-
}
|
|
29051
|
-
};
|
|
29052
|
-
server.tool(
|
|
29053
|
-
"list_systems",
|
|
29054
|
-
"List all systems (projects) in the configured organization",
|
|
29055
|
-
{
|
|
29056
|
-
teamspaceId: external_exports.string().optional().describe("Filter to systems belonging to a specific teamspace. Empty or whitespace-only treated as no filter.")
|
|
29057
|
-
},
|
|
29058
|
-
listSystemsHandler
|
|
29059
|
-
);
|
|
29060
|
-
server.tool(
|
|
29061
|
-
"list_projects",
|
|
29062
|
-
"[Deprecated \u2014 use list_systems] List all projects in the configured organization",
|
|
29063
|
-
{
|
|
29064
|
-
teamspaceId: external_exports.string().optional().describe("Filter to projects belonging to a specific teamspace. Empty or whitespace-only treated as no filter.")
|
|
29065
|
-
},
|
|
29066
|
-
listSystemsHandler
|
|
29067
|
-
);
|
|
29068
|
-
const getSystemContextSchema = {
|
|
29069
|
-
projectId: external_exports.string().describe("The system ID"),
|
|
29070
|
-
noteLimit: external_exports.number().int().min(1).max(MAX_CONTEXT_NOTE_LIMIT).optional().describe(
|
|
29071
|
-
`Max Notes to include, prioritised by note type (default ${DEFAULT_CONTEXT_NOTE_LIMIT}, max ${MAX_CONTEXT_NOTE_LIMIT}). Use list_notes or search for the full set.`
|
|
29072
|
-
)
|
|
29073
|
-
};
|
|
29074
|
-
const getSystemContextHandler = async ({
|
|
29075
|
-
projectId,
|
|
29076
|
-
noteLimit
|
|
29077
|
-
}) => {
|
|
29078
|
-
try {
|
|
29079
|
-
const [project, org] = await Promise.all([
|
|
29080
|
-
fetchProjectInOrg(client, projectId, ctx.orgId),
|
|
29081
|
-
client.getOrg(ctx.orgId)
|
|
29082
|
-
]);
|
|
29083
|
-
const notes = await client.listProjectNotes(projectId, {
|
|
29084
|
-
limit: noteLimit ?? DEFAULT_CONTEXT_NOTE_LIMIT
|
|
29085
|
-
});
|
|
29086
|
-
const text = formatProjectContext(project, notes, org?.coda);
|
|
29087
|
-
return { content: [{ type: "text", text }] };
|
|
29088
|
-
} catch (err) {
|
|
29089
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
29090
|
-
return { content: [{ type: "text", text: `Failed to get system context: ${message}` }], isError: true };
|
|
29091
|
-
}
|
|
29092
|
-
};
|
|
29093
|
-
server.tool(
|
|
29094
|
-
"get_system_context",
|
|
29095
|
-
"Get system metadata, description, and notes in one view",
|
|
29096
|
-
getSystemContextSchema,
|
|
29097
|
-
getSystemContextHandler
|
|
29098
|
-
);
|
|
29099
|
-
server.tool(
|
|
29100
|
-
"get_project_context",
|
|
29101
|
-
"[Deprecated \u2014 use get_system_context] Get project metadata, description, and notes in one view",
|
|
29102
|
-
{ projectId: external_exports.string().describe("The project ID") },
|
|
29103
|
-
getSystemContextHandler
|
|
29104
|
-
);
|
|
29105
|
-
const updateSystemSchema = {
|
|
29106
|
-
projectId: external_exports.string().describe("The system ID"),
|
|
29107
|
-
title: external_exports.string().optional().describe("New system title"),
|
|
29108
|
-
description: external_exports.string().optional().describe("New system description"),
|
|
29109
|
-
strategy: external_exports.string().optional().describe('System strategy markdown \u2014 Org Strategy + System Strategy ("System Coda")'),
|
|
29110
|
-
teamspaceId: external_exports.string().nullable().optional().describe("Teamspace ID to associate this system with; pass null to remove the association"),
|
|
29111
|
-
accountId: external_exports.string().nullable().optional().describe("Account that owns this System \u2014 the client or internal org unit. Pass null (or an empty string) to unlink. Reassigning moves the System so it is listed under exactly one Account."),
|
|
29112
|
-
repoOwner: external_exports.string().optional().describe("GitHub repository owner (org or user)"),
|
|
29113
|
-
repoName: external_exports.string().optional().describe("GitHub repository name"),
|
|
29114
|
-
repoDefaultBranch: external_exports.string().optional().describe('Default branch for code check (defaults to "main")'),
|
|
29115
|
-
rateLimitOverrides: external_exports.record(external_exports.string(), external_exports.object({ maxPerHour: external_exports.number().min(0) })).optional().describe('Per-task-type rate limit overrides, e.g. {"agent_chat":{"maxPerHour":100}}. Overrides env var and compiled defaults.')
|
|
29116
|
-
};
|
|
29117
|
-
const updateSystemHandler = async ({ projectId, ...updates }) => {
|
|
29118
|
-
try {
|
|
29119
|
-
const nonEmpty = Object.fromEntries(
|
|
29120
|
-
Object.entries(updates).filter(([, v]) => v !== void 0)
|
|
29121
|
-
);
|
|
29122
|
-
if (Object.keys(nonEmpty).length === 0) {
|
|
29123
|
-
return { content: [{ type: "text", text: "No updates provided." }], isError: true };
|
|
29124
|
-
}
|
|
29125
|
-
await assertProjectInOrg(client, projectId, ctx.orgId);
|
|
29126
|
-
if (nonEmpty["accountId"] === "") nonEmpty["accountId"] = null;
|
|
29127
|
-
if (typeof updates.teamspaceId === "string") {
|
|
29128
|
-
const teamspace = await client.getTeamspace(updates.teamspaceId);
|
|
29129
|
-
if (!teamspace) {
|
|
29130
|
-
return { content: [{ type: "text", text: "Teamspace not found" }], isError: true };
|
|
29131
|
-
}
|
|
29132
|
-
if (teamspace.orgId !== ctx.orgId) {
|
|
29133
|
-
return { content: [{ type: "text", text: "Teamspace does not belong to this organization" }], isError: true };
|
|
29134
|
-
}
|
|
29135
|
-
}
|
|
29136
|
-
const updated = await client.updateSystem(projectId, nonEmpty);
|
|
29137
|
-
if (!updated) {
|
|
29138
|
-
return { content: [{ type: "text", text: `System not found: "${projectId}"` }], isError: true };
|
|
29139
|
-
}
|
|
29140
|
-
if (PROJECT_EMBEDDING_FIELDS.some((f) => f in nonEmpty)) {
|
|
29141
|
-
void client.triggerProjectEmbedding(projectId);
|
|
29142
|
-
}
|
|
29143
|
-
const lines = [
|
|
29144
|
-
`System updated successfully.`,
|
|
29145
|
-
"",
|
|
29146
|
-
`**ID:** ${updated.projectId}`,
|
|
29147
|
-
`**Title:** ${updated.title}`,
|
|
29148
|
-
accountLine(nonEmpty, updated.accountId),
|
|
29149
|
-
updated.strategy ? `**Strategy:** (updated)` : "",
|
|
29150
|
-
updated.repoOwner ? `**Repo:** ${updated.repoOwner}/${updated.repoName}` : "",
|
|
29151
|
-
updated.repoDefaultBranch ? `**Default Branch:** ${updated.repoDefaultBranch}` : ""
|
|
29152
|
-
].filter(Boolean);
|
|
29153
|
-
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
29154
|
-
} catch (err) {
|
|
29155
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
29156
|
-
return { content: [{ type: "text", text: `Failed to update system: ${message}` }], isError: true };
|
|
29157
|
-
}
|
|
29158
|
-
};
|
|
29159
|
-
server.tool("update_system", "Update system settings such as title, description, repository configuration, or the Account that owns it", updateSystemSchema, updateSystemHandler);
|
|
29160
|
-
server.tool(
|
|
29161
|
-
"update_project",
|
|
29162
|
-
"[Deprecated \u2014 use update_system] Update project settings such as title, description, or repository configuration",
|
|
29163
|
-
{
|
|
29164
|
-
projectId: external_exports.string().describe("The project ID"),
|
|
29165
|
-
title: external_exports.string().optional().describe("New project title"),
|
|
29166
|
-
description: external_exports.string().optional().describe("New project description"),
|
|
29167
|
-
strategy: external_exports.string().optional().describe('Project strategy markdown \u2014 Org Strategy + Project Strategy ("Project Coda")'),
|
|
29168
|
-
teamspaceId: external_exports.string().nullable().optional().describe("Teamspace ID to associate this project with; pass null to remove the association"),
|
|
29169
|
-
repoOwner: external_exports.string().optional().describe("GitHub repository owner (org or user)"),
|
|
29170
|
-
repoName: external_exports.string().optional().describe("GitHub repository name"),
|
|
29171
|
-
repoDefaultBranch: external_exports.string().optional().describe('Default branch for code check (defaults to "main")'),
|
|
29172
|
-
rateLimitOverrides: external_exports.record(external_exports.string(), external_exports.object({ maxPerHour: external_exports.number().min(0) })).optional().describe('Per-task-type rate limit overrides, e.g. {"agent_chat":{"maxPerHour":100}}. Overrides env var and compiled defaults.')
|
|
29173
|
-
},
|
|
29174
|
-
updateSystemHandler
|
|
29175
|
-
);
|
|
29176
|
-
server.tool(
|
|
29177
|
-
"archive_project",
|
|
29178
|
-
"Archive a project, hiding it from the project dropdown and all active project views. Use this when a project is no longer active and should be removed from navigation.",
|
|
29179
|
-
{ projectId: external_exports.string().describe("The project ID to archive") },
|
|
29180
|
-
async ({ projectId }) => {
|
|
29181
|
-
const project = await client.getSystem(projectId);
|
|
29182
|
-
if (!project) {
|
|
29183
|
-
return { content: [{ type: "text", text: `Project not found: "${projectId}"` }], isError: true };
|
|
29184
|
-
}
|
|
29185
|
-
if (project.orgId !== ctx.orgId) {
|
|
29186
|
-
return { content: [{ type: "text", text: `Project "${projectId}" is not in this organization` }], isError: true };
|
|
29187
|
-
}
|
|
29188
|
-
try {
|
|
29189
|
-
const updated = await client.archiveSystem(projectId);
|
|
29190
|
-
if (!updated) {
|
|
29191
|
-
return { content: [{ type: "text", text: `Project not found: "${projectId}"` }], isError: true };
|
|
29192
|
-
}
|
|
29193
|
-
return {
|
|
29194
|
-
content: [{
|
|
29195
|
-
type: "text",
|
|
29196
|
-
text: [
|
|
29197
|
-
"Project archived successfully.",
|
|
29198
|
-
"",
|
|
29199
|
-
`**ID:** ${updated.projectId}`,
|
|
29200
|
-
`**Title:** ${updated.title}`,
|
|
29201
|
-
`**Status:** ${updated.status}`
|
|
29202
|
-
].join("\n")
|
|
29203
|
-
}]
|
|
29204
|
-
};
|
|
29205
|
-
} catch (err) {
|
|
29206
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
29207
|
-
return { content: [{ type: "text", text: `Failed to archive project: ${message}` }], isError: true };
|
|
29208
|
-
}
|
|
29209
|
-
}
|
|
29210
|
-
);
|
|
29211
|
-
const createSystemSchema = {
|
|
29212
|
-
title: external_exports.string().describe("System title"),
|
|
29213
|
-
description: external_exports.string().optional().describe("System description"),
|
|
29214
|
-
strategy: external_exports.string().optional().describe('System strategy markdown \u2014 Org Strategy + System Strategy ("System Coda")'),
|
|
29215
|
-
teamspaceId: external_exports.string().optional().describe("Teamspace ID to associate this system with"),
|
|
29216
|
-
repoOwner: external_exports.string().optional().describe("GitHub repository owner (org or user)"),
|
|
29217
|
-
repoName: external_exports.string().optional().describe("GitHub repository name"),
|
|
29218
|
-
repoDefaultBranch: external_exports.string().optional().describe('Default branch for code check (defaults to "main")')
|
|
29219
|
-
};
|
|
29220
|
-
const createSystemHandler = async ({ title, description, strategy, teamspaceId, repoOwner, repoName, repoDefaultBranch }) => {
|
|
29221
|
-
if (teamspaceId) {
|
|
29222
|
-
const teamspace = await client.getTeamspace(teamspaceId);
|
|
29223
|
-
if (!teamspace) {
|
|
29224
|
-
return { content: [{ type: "text", text: "Teamspace not found" }], isError: true };
|
|
29225
|
-
}
|
|
29226
|
-
if (teamspace.orgId !== ctx.orgId) {
|
|
29227
|
-
return { content: [{ type: "text", text: "Teamspace does not belong to this organization" }], isError: true };
|
|
29228
|
-
}
|
|
29229
|
-
}
|
|
29230
|
-
try {
|
|
29231
|
-
const project = await client.createSystem({
|
|
29232
|
-
projectId: (0, import_crypto5.randomUUID)(),
|
|
29233
|
-
orgId: ctx.orgId,
|
|
29234
|
-
ownerUserId: ctx.user.userId,
|
|
29235
|
-
title,
|
|
29236
|
-
description,
|
|
29237
|
-
strategy,
|
|
29238
|
-
status: "active",
|
|
29239
|
-
teamspaceId,
|
|
29240
|
-
repoOwner,
|
|
29241
|
-
repoName,
|
|
29242
|
-
repoDefaultBranch
|
|
29243
|
-
});
|
|
29244
|
-
const text = [
|
|
29245
|
-
`System created successfully.`,
|
|
29246
|
-
"",
|
|
29247
|
-
`**ID:** ${project.projectId}`,
|
|
29248
|
-
`**Title:** ${project.title}`,
|
|
29249
|
-
`**Status:** ${project.status}`,
|
|
29250
|
-
project.description ? `**Description:** ${project.description}` : "",
|
|
29251
|
-
project.repoOwner ? `**Repo:** ${project.repoOwner}/${project.repoName}` : "",
|
|
29252
|
-
project.repoDefaultBranch ? `**Default Branch:** ${project.repoDefaultBranch}` : ""
|
|
29253
|
-
].filter(Boolean).join("\n");
|
|
29254
|
-
return { content: [{ type: "text", text }] };
|
|
29255
|
-
} catch (err) {
|
|
29256
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
29257
|
-
return { content: [{ type: "text", text: `Failed to create system: ${message}` }], isError: true };
|
|
29258
|
-
}
|
|
29259
|
-
};
|
|
29260
|
-
server.tool("create_system", "Create a new system in the configured organization", createSystemSchema, createSystemHandler);
|
|
29261
|
-
server.tool(
|
|
29262
|
-
"create_project",
|
|
29263
|
-
"[Deprecated \u2014 use create_system] Create a new project in the configured organization",
|
|
29264
|
-
{
|
|
29265
|
-
title: external_exports.string().describe("Project title"),
|
|
29266
|
-
description: external_exports.string().optional().describe("Project description"),
|
|
29267
|
-
strategy: external_exports.string().optional().describe('Project strategy markdown \u2014 Org Strategy + Project Strategy ("Project Coda")'),
|
|
29268
|
-
teamspaceId: external_exports.string().optional().describe("Teamspace ID to associate this project with"),
|
|
29269
|
-
repoOwner: external_exports.string().optional().describe("GitHub repository owner (org or user)"),
|
|
29270
|
-
repoName: external_exports.string().optional().describe("GitHub repository name"),
|
|
29271
|
-
repoDefaultBranch: external_exports.string().optional().describe('Default branch for code check (defaults to "main")')
|
|
29272
|
-
},
|
|
29273
|
-
createSystemHandler
|
|
29274
|
-
);
|
|
29275
|
-
}
|
|
29276
|
-
|
|
29277
28984
|
// ../../libs/harmonica-services/src/mcp/tools/pulse-report-tools.ts
|
|
29278
28985
|
function formatBeatRow(r) {
|
|
29279
28986
|
const downbeatState = r.hasDownbeat ? "" : " \u2014 no Downbeat ever set";
|
|
@@ -29818,21 +29525,21 @@ function registerSessionTools(server, ctx, client) {
|
|
|
29818
29525
|
}
|
|
29819
29526
|
);
|
|
29820
29527
|
server.tool(
|
|
29821
|
-
"
|
|
29822
|
-
'List agent Sessions in a
|
|
29528
|
+
"list_system_sessions",
|
|
29529
|
+
'List agent Sessions in a system, optionally filtered by status or whether terminal sessions are included. Use this to answer "show me sessions in this system" or to find recent runs across all beats.',
|
|
29823
29530
|
{
|
|
29824
|
-
|
|
29531
|
+
systemId: external_exports.string().min(1).describe("The system ID"),
|
|
29825
29532
|
status: external_exports.enum(SESSION_STATUSES).optional().describe("Filter by status: active, idle, or closed"),
|
|
29826
29533
|
includeTerminal: external_exports.boolean().optional().describe("Include terminal (closed) sessions. Default false.")
|
|
29827
29534
|
},
|
|
29828
|
-
async ({
|
|
29829
|
-
const sessions = await client.listProjectSessions(
|
|
29535
|
+
async ({ systemId, status, includeTerminal }) => {
|
|
29536
|
+
const sessions = await client.listProjectSessions(systemId, {
|
|
29830
29537
|
status,
|
|
29831
29538
|
includeTerminal
|
|
29832
29539
|
});
|
|
29833
29540
|
return {
|
|
29834
29541
|
content: [
|
|
29835
|
-
{ type: "text", text: formatSessionList(sessions, `
|
|
29542
|
+
{ type: "text", text: formatSessionList(sessions, `system ${systemId}`) }
|
|
29836
29543
|
]
|
|
29837
29544
|
};
|
|
29838
29545
|
}
|
|
@@ -30062,18 +29769,18 @@ var import_promises = require("node:fs/promises");
|
|
|
30062
29769
|
var import_node_os = require("node:os");
|
|
30063
29770
|
var import_node_path = require("node:path");
|
|
30064
29771
|
|
|
30065
|
-
// ../../libs/harmonica-services/src/
|
|
30066
|
-
var SNAPSHOT_VERSION =
|
|
29772
|
+
// ../../libs/harmonica-services/src/system-snapshot.constants.ts
|
|
29773
|
+
var SNAPSHOT_VERSION = 4;
|
|
30067
29774
|
|
|
30068
29775
|
// ../../libs/harmonica-services/src/mcp/tools/snapshot-tools.ts
|
|
30069
29776
|
function registerSnapshotTools(server, ctx, client) {
|
|
30070
29777
|
server.tool(
|
|
30071
|
-
"
|
|
30072
|
-
"Export a complete
|
|
30073
|
-
{
|
|
30074
|
-
async ({
|
|
30075
|
-
await assertProjectInOrg(client,
|
|
30076
|
-
const result = await client.exportProjectSnapshot(
|
|
29778
|
+
"export_system_snapshot",
|
|
29779
|
+
"Export a complete system snapshot for environment sync. On a remote server (production/staging) returns an S3 presigned URL; on a local server writes to a temp file and returns the path.",
|
|
29780
|
+
{ systemId: external_exports.string().describe("The system ID to export") },
|
|
29781
|
+
async ({ systemId }) => {
|
|
29782
|
+
await assertProjectInOrg(client, systemId, ctx.orgId);
|
|
29783
|
+
const result = await client.exportProjectSnapshot(systemId);
|
|
30077
29784
|
if (typeof result === "string") {
|
|
30078
29785
|
if (!result.startsWith("https://")) {
|
|
30079
29786
|
throw new Error(`exportProjectSnapshot returned an unexpected string value (expected an https:// presigned URL): ${result.slice(0, 80)}`);
|
|
@@ -30082,7 +29789,7 @@ function registerSnapshotTools(server, ctx, client) {
|
|
|
30082
29789
|
content: [{
|
|
30083
29790
|
type: "text",
|
|
30084
29791
|
text: [
|
|
30085
|
-
"Snapshot exported to S3. Pass this URL to
|
|
29792
|
+
"Snapshot exported to S3. Pass this URL to import_system_snapshot on the target environment:",
|
|
30086
29793
|
"",
|
|
30087
29794
|
result,
|
|
30088
29795
|
"",
|
|
@@ -30092,10 +29799,10 @@ function registerSnapshotTools(server, ctx, client) {
|
|
|
30092
29799
|
};
|
|
30093
29800
|
}
|
|
30094
29801
|
const snapshot = normalizeSnapshot(result);
|
|
30095
|
-
const filename = `harmonica-snapshot-${
|
|
29802
|
+
const filename = `harmonica-snapshot-${systemId}-${Date.now()}.json`;
|
|
30096
29803
|
const filePath = (0, import_node_path.join)((0, import_node_os.tmpdir)(), filename);
|
|
30097
29804
|
await (0, import_promises.writeFile)(filePath, JSON.stringify(snapshot), "utf-8");
|
|
30098
|
-
const title = snapshot.
|
|
29805
|
+
const title = snapshot.system?.title ?? systemId;
|
|
30099
29806
|
const versionNote = snapshot.version !== SNAPSHOT_VERSION ? [`Warning: snapshot version ${snapshot.version} (local expects ${SNAPSHOT_VERSION}) \u2014 some counts may be zero.`, ""] : [];
|
|
30100
29807
|
const summary = [
|
|
30101
29808
|
`Exported "${title}" to: ${filePath}`,
|
|
@@ -30110,15 +29817,19 @@ function registerSnapshotTools(server, ctx, client) {
|
|
|
30110
29817
|
` PromptLogs: ${snapshot.promptLogs.length}`,
|
|
30111
29818
|
` Drops: ${snapshot.drops?.length ?? 0}`,
|
|
30112
29819
|
` Deliverables: ${snapshot.deliverables?.length ?? 0}`,
|
|
29820
|
+
` Teamspaces: ${snapshot.teamspaces?.length ?? 0}`,
|
|
29821
|
+
` Layers: ${snapshot.layers?.length ?? 0}`,
|
|
29822
|
+
` Tracks: ${snapshot.tracks?.length ?? 0}`,
|
|
29823
|
+
` Work Items: ${snapshot.workItems?.length ?? 0}`,
|
|
30113
29824
|
"",
|
|
30114
|
-
"Use
|
|
29825
|
+
"Use import_system_snapshot with this file path to import into another environment."
|
|
30115
29826
|
];
|
|
30116
29827
|
return { content: [{ type: "text", text: summary.join("\n") }] };
|
|
30117
29828
|
}
|
|
30118
29829
|
);
|
|
30119
29830
|
server.tool(
|
|
30120
|
-
"
|
|
30121
|
-
"Import a
|
|
29831
|
+
"import_system_snapshot",
|
|
29832
|
+
"Import a system snapshot into the current environment. Accepts an S3 presigned URL (https:// only), a file path, or inline JSON. Blocked in production by default.",
|
|
30122
29833
|
{
|
|
30123
29834
|
snapshot: external_exports.string().describe("S3 presigned URL (https://...), file path, or inline JSON string"),
|
|
30124
29835
|
targetTeamspaceId: external_exports.string().min(1).optional().describe(
|
|
@@ -30127,12 +29838,12 @@ function registerSnapshotTools(server, ctx, client) {
|
|
|
30127
29838
|
},
|
|
30128
29839
|
async ({ snapshot: snapshotInput, targetTeamspaceId }) => {
|
|
30129
29840
|
const isProduction = process.env.NODE_ENV === "production";
|
|
30130
|
-
const importAllowed = process.env.
|
|
29841
|
+
const importAllowed = process.env.ALLOW_SYSTEM_IMPORT === "true";
|
|
30131
29842
|
if (isProduction && !importAllowed) {
|
|
30132
29843
|
return {
|
|
30133
29844
|
content: [{
|
|
30134
29845
|
type: "text",
|
|
30135
|
-
text: "Import is disabled in production. Set
|
|
29846
|
+
text: "Import is disabled in production. Set ALLOW_SYSTEM_IMPORT=true to override."
|
|
30136
29847
|
}],
|
|
30137
29848
|
isError: true
|
|
30138
29849
|
};
|
|
@@ -30143,7 +29854,7 @@ function registerSnapshotTools(server, ctx, client) {
|
|
|
30143
29854
|
if (isPrivateHost(trimmed)) {
|
|
30144
29855
|
throw new Error("Snapshot URL must point to a public host \u2014 private, link-local, and loopback addresses are not permitted.");
|
|
30145
29856
|
}
|
|
30146
|
-
const result2 = await client.
|
|
29857
|
+
const result2 = await client.importSystemSnapshotFromUrl(trimmed, importOptions);
|
|
30147
29858
|
return { content: [{ type: "text", text: formatImportSummary(result2, targetTeamspaceId) }] };
|
|
30148
29859
|
}
|
|
30149
29860
|
if (trimmed.startsWith("http://")) {
|
|
@@ -30158,21 +29869,25 @@ function registerSnapshotTools(server, ctx, client) {
|
|
|
30158
29869
|
}
|
|
30159
29870
|
assertSnapshotShape(parsed, "");
|
|
30160
29871
|
const normalized = normalizeSnapshot(parsed);
|
|
30161
|
-
const result = await client.
|
|
29872
|
+
const result = await client.importSystemSnapshot(normalized, importOptions);
|
|
30162
29873
|
return { content: [{ type: "text", text: formatImportSummary(result, targetTeamspaceId) }] };
|
|
30163
29874
|
}
|
|
30164
29875
|
);
|
|
30165
29876
|
}
|
|
30166
29877
|
function normalizeSnapshot(raw) {
|
|
29878
|
+
const anyRaw = raw;
|
|
30167
29879
|
return {
|
|
30168
29880
|
...raw,
|
|
29881
|
+
// Compat: v3 and earlier snapshots have 'project' instead of 'system'
|
|
29882
|
+
system: raw.system ?? anyRaw["project"],
|
|
30169
29883
|
beats: raw.beats ?? [],
|
|
30170
29884
|
beatIndex: raw.beatIndex ?? [],
|
|
30171
29885
|
proposals: raw.proposals ?? [],
|
|
30172
29886
|
proposalIndex: raw.proposalIndex ?? [],
|
|
30173
29887
|
revisions: raw.revisions ?? [],
|
|
30174
29888
|
revisionIndex: raw.revisionIndex ?? [],
|
|
30175
|
-
|
|
29889
|
+
// Compat: v3 and earlier snapshots have 'projectRevisionIndex'
|
|
29890
|
+
systemRevisionIndex: raw.systemRevisionIndex ?? anyRaw["projectRevisionIndex"] ?? [],
|
|
30176
29891
|
activities: raw.activities ?? [],
|
|
30177
29892
|
activityIndex: raw.activityIndex ?? [],
|
|
30178
29893
|
notes: raw.notes ?? [],
|
|
@@ -30185,13 +29900,26 @@ function normalizeSnapshot(raw) {
|
|
|
30185
29900
|
promptLogs: raw.promptLogs ?? [],
|
|
30186
29901
|
beatVersions: raw.beatVersions ?? [],
|
|
30187
29902
|
beatBeatVersionIndex: raw.beatBeatVersionIndex ?? [],
|
|
30188
|
-
|
|
29903
|
+
// Compat: v3 and earlier snapshots have 'projectBeatVersionIndex'
|
|
29904
|
+
systemBeatVersionIndex: raw.systemBeatVersionIndex ?? anyRaw["projectBeatVersionIndex"] ?? [],
|
|
30189
29905
|
beatVersionDropAssignments: raw.beatVersionDropAssignments ?? [],
|
|
30190
29906
|
drops: raw.drops ?? [],
|
|
30191
29907
|
accountDropIndex: raw.accountDropIndex ?? [],
|
|
30192
29908
|
deliverables: raw.deliverables ?? [],
|
|
30193
29909
|
teamspaceDeliverableIndex: raw.teamspaceDeliverableIndex ?? [],
|
|
30194
|
-
|
|
29910
|
+
// Compat: v3 and earlier snapshots have 'projectDeliverableIndex'
|
|
29911
|
+
systemDeliverableIndex: raw.systemDeliverableIndex ?? anyRaw["projectDeliverableIndex"] ?? [],
|
|
29912
|
+
teamspaces: raw.teamspaces ?? [],
|
|
29913
|
+
teamspaceAccountIndex: raw.teamspaceAccountIndex ?? [],
|
|
29914
|
+
accountTeamspaceIndex: raw.accountTeamspaceIndex ?? [],
|
|
29915
|
+
layers: raw.layers ?? [],
|
|
29916
|
+
layerIndex: raw.layerIndex ?? [],
|
|
29917
|
+
tracks: raw.tracks ?? [],
|
|
29918
|
+
// Compat: early v4 snapshots (before field rename) have 'trackProjectIndex'
|
|
29919
|
+
trackSystemIndex: raw.trackSystemIndex ?? anyRaw["trackProjectIndex"] ?? [],
|
|
29920
|
+
trackTeamspaceIndex: raw.trackTeamspaceIndex ?? [],
|
|
29921
|
+
workItems: raw.workItems ?? [],
|
|
29922
|
+
workItemIndex: raw.workItemIndex ?? []
|
|
30195
29923
|
};
|
|
30196
29924
|
}
|
|
30197
29925
|
async function resolveSnapshotInput(input) {
|
|
@@ -30203,7 +29931,7 @@ async function resolveSnapshotInput(input) {
|
|
|
30203
29931
|
}
|
|
30204
29932
|
function formatImportSummary(result, targetTeamspaceId) {
|
|
30205
29933
|
const lines = [
|
|
30206
|
-
`Imported
|
|
29934
|
+
`Imported system: ${result.projectId}`,
|
|
30207
29935
|
` Beats: ${result.counts.beats}`,
|
|
30208
29936
|
` Proposals: ${result.counts.proposals}`,
|
|
30209
29937
|
` Revisions: ${result.counts.revisions}`,
|
|
@@ -30214,7 +29942,11 @@ function formatImportSummary(result, targetTeamspaceId) {
|
|
|
30214
29942
|
` PromptLogs: ${result.counts.promptLogs}`,
|
|
30215
29943
|
` Beat Versions: ${result.counts.beatVersions ?? 0}`,
|
|
30216
29944
|
` Drops: ${result.counts.drops ?? 0}`,
|
|
30217
|
-
` Deliverables: ${result.counts.deliverables ?? 0}
|
|
29945
|
+
` Deliverables: ${result.counts.deliverables ?? 0}`,
|
|
29946
|
+
` Teamspaces: ${result.counts.teamspaces ?? 0}`,
|
|
29947
|
+
` Layers: ${result.counts.layers ?? 0}`,
|
|
29948
|
+
` Tracks: ${result.counts.tracks ?? 0}`,
|
|
29949
|
+
` Work Items: ${result.counts.workItems ?? 0}`
|
|
30218
29950
|
];
|
|
30219
29951
|
if (result.errors.length > 0) {
|
|
30220
29952
|
lines.push("", `Errors (${result.errors.length}):`);
|
|
@@ -30261,9 +29993,9 @@ function assertSnapshotShape(val, sourceHint) {
|
|
|
30261
29993
|
if (typeof obj["version"] !== "number" || obj["version"] <= 0) {
|
|
30262
29994
|
throw new Error(`Snapshot${sourceHint} has an invalid or missing 'version' field \u2014 it may not be a valid project snapshot.`);
|
|
30263
29995
|
}
|
|
30264
|
-
const
|
|
30265
|
-
if (typeof
|
|
30266
|
-
throw new Error(`Snapshot${sourceHint} is missing '
|
|
29996
|
+
const systemOrProject = obj["system"] ?? obj["project"];
|
|
29997
|
+
if (typeof systemOrProject !== "object" || systemOrProject === null || typeof systemOrProject["projectId"] !== "string") {
|
|
29998
|
+
throw new Error(`Snapshot${sourceHint} is missing 'system.projectId' \u2014 it may not be a valid project snapshot.`);
|
|
30267
29999
|
}
|
|
30268
30000
|
}
|
|
30269
30001
|
|
|
@@ -30343,13 +30075,276 @@ function registerSubscriptionTools(server, ctx, client) {
|
|
|
30343
30075
|
);
|
|
30344
30076
|
}
|
|
30345
30077
|
|
|
30346
|
-
// ../../libs/harmonica-services/src/mcp/tools/
|
|
30347
|
-
|
|
30348
|
-
|
|
30349
|
-
|
|
30350
|
-
|
|
30351
|
-
|
|
30352
|
-
|
|
30078
|
+
// ../../libs/harmonica-services/src/mcp/tools/system-lifecycle-tools.ts
|
|
30079
|
+
function registerProjectLifecycleTools(server, ctx, client) {
|
|
30080
|
+
server.tool(
|
|
30081
|
+
"transition_system_lifecycle",
|
|
30082
|
+
"Advance a System's capability-maturity lifecycle state. Allowed transitions follow the state machine (Concept \u2192 Incubating \u2192 Piloting \u2192 Activated \u2192 Commercializing \u2192 Scaled, plus paused/killed/sunset/archived exits). The activated \u2192 commercializing edge requires a `decisionNoteId` pointing to a system-scoped Decision Note (the governance review). Other gates are advisory.",
|
|
30083
|
+
{
|
|
30084
|
+
systemId: external_exports.string().describe("The system ID"),
|
|
30085
|
+
targetState: external_exports.enum([
|
|
30086
|
+
"concept",
|
|
30087
|
+
"incubating",
|
|
30088
|
+
"piloting",
|
|
30089
|
+
"activated",
|
|
30090
|
+
"commercializing",
|
|
30091
|
+
"scaled",
|
|
30092
|
+
"paused",
|
|
30093
|
+
"killed",
|
|
30094
|
+
"sunset",
|
|
30095
|
+
"archived"
|
|
30096
|
+
]).describe("Target lifecycle state. Active: concept, incubating, piloting, activated, commercializing, scaled. Exits: paused, killed, sunset, archived."),
|
|
30097
|
+
reason: external_exports.string().optional().describe("Why this transition is being made (recorded in audit metadata)"),
|
|
30098
|
+
decisionNoteId: external_exports.string().optional().describe("Note ID of the governance Decision Note. Required for the activated \u2192 commercializing transition; optional otherwise.")
|
|
30099
|
+
},
|
|
30100
|
+
async ({ systemId, targetState, reason, decisionNoteId }) => {
|
|
30101
|
+
try {
|
|
30102
|
+
const system = await client.getSystem(systemId);
|
|
30103
|
+
if (!system) {
|
|
30104
|
+
return { content: [{ type: "text", text: `System not found: "${systemId}"` }], isError: true };
|
|
30105
|
+
}
|
|
30106
|
+
if (system.orgId !== ctx.orgId) {
|
|
30107
|
+
return { content: [{ type: "text", text: `System "${systemId}" is not in this organization` }], isError: true };
|
|
30108
|
+
}
|
|
30109
|
+
const result = await client.transitionProjectLifecycleState(systemId, targetState, {
|
|
30110
|
+
actor: { type: "human", id: ctx.user.userId, name: ctx.user.name },
|
|
30111
|
+
reason,
|
|
30112
|
+
decisionNoteId
|
|
30113
|
+
});
|
|
30114
|
+
if (!result.success) {
|
|
30115
|
+
const failedGates = result.error?.failedGates?.length ? ` (failed gates: ${result.error.failedGates.join(", ")})` : "";
|
|
30116
|
+
return {
|
|
30117
|
+
content: [{ type: "text", text: `Transition failed: ${result.error?.message ?? "Unknown error"}${failedGates}` }],
|
|
30118
|
+
isError: true
|
|
30119
|
+
};
|
|
30120
|
+
}
|
|
30121
|
+
const lines = [
|
|
30122
|
+
`System lifecycle transitioned successfully.`,
|
|
30123
|
+
"",
|
|
30124
|
+
`**System:** ${systemId}`,
|
|
30125
|
+
`**Title:** ${system.title}`,
|
|
30126
|
+
`**Transition:** ${result.previousState} \u2192 ${result.newState}`
|
|
30127
|
+
];
|
|
30128
|
+
if (reason) lines.push(`**Reason:** ${reason}`);
|
|
30129
|
+
if (decisionNoteId) lines.push(`**Decision Note:** ${decisionNoteId}`);
|
|
30130
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
30131
|
+
} catch (err) {
|
|
30132
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
30133
|
+
return { content: [{ type: "text", text: `Failed to transition system lifecycle: ${message}` }], isError: true };
|
|
30134
|
+
}
|
|
30135
|
+
}
|
|
30136
|
+
);
|
|
30137
|
+
}
|
|
30138
|
+
|
|
30139
|
+
// ../../libs/harmonica-services/src/mcp/tools/system-tools.ts
|
|
30140
|
+
var import_crypto5 = require("crypto");
|
|
30141
|
+
function accountLine(request, resolved) {
|
|
30142
|
+
const requested = request["accountId"];
|
|
30143
|
+
if (requested === null || requested === "") return "**Account:** (unlinked)";
|
|
30144
|
+
return resolved ? `**Account:** ${resolved}` : "";
|
|
30145
|
+
}
|
|
30146
|
+
var PROJECT_EMBEDDING_FIELDS = ["title", "description", "strategy"];
|
|
30147
|
+
function registerProjectTools(server, ctx, client) {
|
|
30148
|
+
const listSystemsHandler = async ({ teamspaceId }) => {
|
|
30149
|
+
try {
|
|
30150
|
+
const projects = await client.listOrgSystems(ctx.orgId);
|
|
30151
|
+
const trimmed = teamspaceId?.trim();
|
|
30152
|
+
const filtered = trimmed ? projects.filter((p) => p.teamspaceId === trimmed) : projects;
|
|
30153
|
+
const text = formatProjectSummaryTable(filtered);
|
|
30154
|
+
return { content: [{ type: "text", text }] };
|
|
30155
|
+
} catch (err) {
|
|
30156
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
30157
|
+
return { content: [{ type: "text", text: `Failed to list systems: ${message}` }], isError: true };
|
|
30158
|
+
}
|
|
30159
|
+
};
|
|
30160
|
+
server.tool(
|
|
30161
|
+
"list_systems",
|
|
30162
|
+
"List all systems (projects) in the configured organization",
|
|
30163
|
+
{
|
|
30164
|
+
teamspaceId: external_exports.string().optional().describe("Filter to systems belonging to a specific teamspace. Empty or whitespace-only treated as no filter.")
|
|
30165
|
+
},
|
|
30166
|
+
listSystemsHandler
|
|
30167
|
+
);
|
|
30168
|
+
const getSystemContextSchema = {
|
|
30169
|
+
systemId: external_exports.string().describe("The system ID"),
|
|
30170
|
+
noteLimit: external_exports.number().int().min(1).max(MAX_CONTEXT_NOTE_LIMIT).optional().describe(
|
|
30171
|
+
`Max Notes to include, prioritised by note type (default ${DEFAULT_CONTEXT_NOTE_LIMIT}, max ${MAX_CONTEXT_NOTE_LIMIT}). Use list_notes or search for the full set.`
|
|
30172
|
+
)
|
|
30173
|
+
};
|
|
30174
|
+
const getSystemContextHandler = async ({
|
|
30175
|
+
systemId,
|
|
30176
|
+
noteLimit
|
|
30177
|
+
}) => {
|
|
30178
|
+
try {
|
|
30179
|
+
const [project, org] = await Promise.all([
|
|
30180
|
+
fetchProjectInOrg(client, systemId, ctx.orgId),
|
|
30181
|
+
client.getOrg(ctx.orgId)
|
|
30182
|
+
]);
|
|
30183
|
+
const notes = await client.listProjectNotes(systemId, {
|
|
30184
|
+
limit: noteLimit ?? DEFAULT_CONTEXT_NOTE_LIMIT
|
|
30185
|
+
});
|
|
30186
|
+
const text = formatProjectContext(project, notes, org?.coda);
|
|
30187
|
+
return { content: [{ type: "text", text }] };
|
|
30188
|
+
} catch (err) {
|
|
30189
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
30190
|
+
return { content: [{ type: "text", text: `Failed to get system context: ${message}` }], isError: true };
|
|
30191
|
+
}
|
|
30192
|
+
};
|
|
30193
|
+
server.tool(
|
|
30194
|
+
"get_system_context",
|
|
30195
|
+
"Get system metadata, description, and notes in one view",
|
|
30196
|
+
getSystemContextSchema,
|
|
30197
|
+
getSystemContextHandler
|
|
30198
|
+
);
|
|
30199
|
+
const updateSystemSchema = {
|
|
30200
|
+
systemId: external_exports.string().describe("The system ID"),
|
|
30201
|
+
title: external_exports.string().optional().describe("New system title"),
|
|
30202
|
+
description: external_exports.string().optional().describe("New system description"),
|
|
30203
|
+
strategy: external_exports.string().optional().describe('System strategy markdown \u2014 Org Strategy + System Strategy ("System Coda")'),
|
|
30204
|
+
teamspaceId: external_exports.string().nullable().optional().describe("Teamspace ID to associate this system with; pass null to remove the association"),
|
|
30205
|
+
accountId: external_exports.string().nullable().optional().describe("Account that owns this System \u2014 the client or internal org unit. Pass null (or an empty string) to unlink. Reassigning moves the System so it is listed under exactly one Account."),
|
|
30206
|
+
repoOwner: external_exports.string().optional().describe("GitHub repository owner (org or user)"),
|
|
30207
|
+
repoName: external_exports.string().optional().describe("GitHub repository name"),
|
|
30208
|
+
repoDefaultBranch: external_exports.string().optional().describe('Default branch for code check (defaults to "main")'),
|
|
30209
|
+
rateLimitOverrides: external_exports.record(external_exports.string(), external_exports.object({ maxPerHour: external_exports.number().min(0) })).optional().describe('Per-task-type rate limit overrides, e.g. {"agent_chat":{"maxPerHour":100}}. Overrides env var and compiled defaults.')
|
|
30210
|
+
};
|
|
30211
|
+
const updateSystemHandler = async ({ systemId, ...updates }) => {
|
|
30212
|
+
try {
|
|
30213
|
+
const nonEmpty = Object.fromEntries(
|
|
30214
|
+
Object.entries(updates).filter(([, v]) => v !== void 0)
|
|
30215
|
+
);
|
|
30216
|
+
if (Object.keys(nonEmpty).length === 0) {
|
|
30217
|
+
return { content: [{ type: "text", text: "No updates provided." }], isError: true };
|
|
30218
|
+
}
|
|
30219
|
+
await assertProjectInOrg(client, systemId, ctx.orgId);
|
|
30220
|
+
if (nonEmpty["accountId"] === "") nonEmpty["accountId"] = null;
|
|
30221
|
+
if (typeof updates.teamspaceId === "string") {
|
|
30222
|
+
const teamspace = await client.getTeamspace(updates.teamspaceId);
|
|
30223
|
+
if (!teamspace) {
|
|
30224
|
+
return { content: [{ type: "text", text: "Teamspace not found" }], isError: true };
|
|
30225
|
+
}
|
|
30226
|
+
if (teamspace.orgId !== ctx.orgId) {
|
|
30227
|
+
return { content: [{ type: "text", text: "Teamspace does not belong to this organization" }], isError: true };
|
|
30228
|
+
}
|
|
30229
|
+
}
|
|
30230
|
+
const updated = await client.updateSystem(systemId, nonEmpty);
|
|
30231
|
+
if (!updated) {
|
|
30232
|
+
return { content: [{ type: "text", text: `System not found: "${systemId}"` }], isError: true };
|
|
30233
|
+
}
|
|
30234
|
+
if (PROJECT_EMBEDDING_FIELDS.some((f) => f in nonEmpty)) {
|
|
30235
|
+
void client.triggerProjectEmbedding(systemId);
|
|
30236
|
+
}
|
|
30237
|
+
const lines = [
|
|
30238
|
+
`System updated successfully.`,
|
|
30239
|
+
"",
|
|
30240
|
+
`**ID:** ${updated.projectId}`,
|
|
30241
|
+
`**Title:** ${updated.title}`,
|
|
30242
|
+
accountLine(nonEmpty, updated.accountId),
|
|
30243
|
+
updated.strategy ? `**Strategy:** (updated)` : "",
|
|
30244
|
+
updated.repoOwner ? `**Repo:** ${updated.repoOwner}/${updated.repoName}` : "",
|
|
30245
|
+
updated.repoDefaultBranch ? `**Default Branch:** ${updated.repoDefaultBranch}` : ""
|
|
30246
|
+
].filter(Boolean);
|
|
30247
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
30248
|
+
} catch (err) {
|
|
30249
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
30250
|
+
return { content: [{ type: "text", text: `Failed to update system: ${message}` }], isError: true };
|
|
30251
|
+
}
|
|
30252
|
+
};
|
|
30253
|
+
server.tool("update_system", "Update system settings such as title, description, repository configuration, or the Account that owns it", updateSystemSchema, updateSystemHandler);
|
|
30254
|
+
server.tool(
|
|
30255
|
+
"archive_system",
|
|
30256
|
+
"Archive a system, hiding it from the system dropdown and all active system views. Use this when a system is no longer active and should be removed from navigation.",
|
|
30257
|
+
{ systemId: external_exports.string().describe("The system ID to archive") },
|
|
30258
|
+
async ({ systemId }) => {
|
|
30259
|
+
const system = await client.getSystem(systemId);
|
|
30260
|
+
if (!system) {
|
|
30261
|
+
return { content: [{ type: "text", text: `System not found: "${systemId}"` }], isError: true };
|
|
30262
|
+
}
|
|
30263
|
+
if (system.orgId !== ctx.orgId) {
|
|
30264
|
+
return { content: [{ type: "text", text: `System "${systemId}" is not in this organization` }], isError: true };
|
|
30265
|
+
}
|
|
30266
|
+
try {
|
|
30267
|
+
const updated = await client.archiveSystem(systemId);
|
|
30268
|
+
if (!updated) {
|
|
30269
|
+
return { content: [{ type: "text", text: `System not found: "${systemId}"` }], isError: true };
|
|
30270
|
+
}
|
|
30271
|
+
return {
|
|
30272
|
+
content: [{
|
|
30273
|
+
type: "text",
|
|
30274
|
+
text: [
|
|
30275
|
+
"System archived successfully.",
|
|
30276
|
+
"",
|
|
30277
|
+
`**ID:** ${updated.projectId}`,
|
|
30278
|
+
`**Title:** ${updated.title}`,
|
|
30279
|
+
`**Status:** ${updated.status}`
|
|
30280
|
+
].join("\n")
|
|
30281
|
+
}]
|
|
30282
|
+
};
|
|
30283
|
+
} catch (err) {
|
|
30284
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
30285
|
+
return { content: [{ type: "text", text: `Failed to archive system: ${message}` }], isError: true };
|
|
30286
|
+
}
|
|
30287
|
+
}
|
|
30288
|
+
);
|
|
30289
|
+
const createSystemSchema = {
|
|
30290
|
+
title: external_exports.string().describe("System title"),
|
|
30291
|
+
description: external_exports.string().optional().describe("System description"),
|
|
30292
|
+
strategy: external_exports.string().optional().describe('System strategy markdown \u2014 Org Strategy + System Strategy ("System Coda")'),
|
|
30293
|
+
teamspaceId: external_exports.string().optional().describe("Teamspace ID to associate this system with"),
|
|
30294
|
+
repoOwner: external_exports.string().optional().describe("GitHub repository owner (org or user)"),
|
|
30295
|
+
repoName: external_exports.string().optional().describe("GitHub repository name"),
|
|
30296
|
+
repoDefaultBranch: external_exports.string().optional().describe('Default branch for code check (defaults to "main")')
|
|
30297
|
+
};
|
|
30298
|
+
const createSystemHandler = async ({ title, description, strategy, teamspaceId, repoOwner, repoName, repoDefaultBranch }) => {
|
|
30299
|
+
if (teamspaceId) {
|
|
30300
|
+
const teamspace = await client.getTeamspace(teamspaceId);
|
|
30301
|
+
if (!teamspace) {
|
|
30302
|
+
return { content: [{ type: "text", text: "Teamspace not found" }], isError: true };
|
|
30303
|
+
}
|
|
30304
|
+
if (teamspace.orgId !== ctx.orgId) {
|
|
30305
|
+
return { content: [{ type: "text", text: "Teamspace does not belong to this organization" }], isError: true };
|
|
30306
|
+
}
|
|
30307
|
+
}
|
|
30308
|
+
try {
|
|
30309
|
+
const project = await client.createSystem({
|
|
30310
|
+
projectId: (0, import_crypto5.randomUUID)(),
|
|
30311
|
+
orgId: ctx.orgId,
|
|
30312
|
+
ownerUserId: ctx.user.userId,
|
|
30313
|
+
title,
|
|
30314
|
+
description,
|
|
30315
|
+
strategy,
|
|
30316
|
+
status: "active",
|
|
30317
|
+
teamspaceId,
|
|
30318
|
+
repoOwner,
|
|
30319
|
+
repoName,
|
|
30320
|
+
repoDefaultBranch
|
|
30321
|
+
});
|
|
30322
|
+
const text = [
|
|
30323
|
+
`System created successfully.`,
|
|
30324
|
+
"",
|
|
30325
|
+
`**ID:** ${project.projectId}`,
|
|
30326
|
+
`**Title:** ${project.title}`,
|
|
30327
|
+
`**Status:** ${project.status}`,
|
|
30328
|
+
project.description ? `**Description:** ${project.description}` : "",
|
|
30329
|
+
project.repoOwner ? `**Repo:** ${project.repoOwner}/${project.repoName}` : "",
|
|
30330
|
+
project.repoDefaultBranch ? `**Default Branch:** ${project.repoDefaultBranch}` : ""
|
|
30331
|
+
].filter(Boolean).join("\n");
|
|
30332
|
+
return { content: [{ type: "text", text }] };
|
|
30333
|
+
} catch (err) {
|
|
30334
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
30335
|
+
return { content: [{ type: "text", text: `Failed to create system: ${message}` }], isError: true };
|
|
30336
|
+
}
|
|
30337
|
+
};
|
|
30338
|
+
server.tool("create_system", "Create a new system in the configured organization", createSystemSchema, createSystemHandler);
|
|
30339
|
+
}
|
|
30340
|
+
|
|
30341
|
+
// ../../libs/harmonica-services/src/mcp/tools/teamspace-tools.ts
|
|
30342
|
+
var import_node_crypto3 = require("node:crypto");
|
|
30343
|
+
var EnsembleMemberSchema = external_exports.discriminatedUnion("type", [
|
|
30344
|
+
external_exports.object({ type: external_exports.literal("human"), email: external_exports.string().email().max(254), name: external_exports.string().min(1).max(200) }),
|
|
30345
|
+
external_exports.object({ type: external_exports.literal("agent"), agentId: external_exports.string().min(1).max(100), name: external_exports.string().min(1).max(200) })
|
|
30346
|
+
]);
|
|
30347
|
+
function generateTeamspaceSlug(name) {
|
|
30353
30348
|
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60);
|
|
30354
30349
|
}
|
|
30355
30350
|
function formatEnsembleMember(member) {
|
|
@@ -30547,19 +30542,19 @@ function registerValueVelocityTools(server, ctx, client) {
|
|
|
30547
30542
|
}
|
|
30548
30543
|
);
|
|
30549
30544
|
server.tool(
|
|
30550
|
-
"
|
|
30551
|
-
"Rank all Beat Versions in a
|
|
30545
|
+
"list_system_beat_versions_ranked",
|
|
30546
|
+
"Rank all Beat Versions in a system by Value Velocity score. Only Beat Versions with confirmed inputs appear. Results are sorted descending by chosen score. Use this to prioritize work or surface the highest-impact items first.",
|
|
30552
30547
|
{
|
|
30553
|
-
|
|
30548
|
+
systemId: external_exports.string().describe("The system ID"),
|
|
30554
30549
|
lens: lensSchema.describe("Scoring lens: velocity | roi | valuePrimary (default: velocity)"),
|
|
30555
30550
|
withLeverage: external_exports.boolean().optional().describe("Include cascade leverage scores (v2). Default: false.")
|
|
30556
30551
|
},
|
|
30557
|
-
async ({
|
|
30552
|
+
async ({ systemId, lens, withLeverage }) => {
|
|
30558
30553
|
try {
|
|
30559
|
-
await assertProjectInOrg(client,
|
|
30554
|
+
await assertProjectInOrg(client, systemId, ctx.orgId);
|
|
30560
30555
|
const [rankedResult, staleWResult] = await Promise.allSettled([
|
|
30561
|
-
withLeverage ? client.rankProjectBeatVersionsWithLeverage(
|
|
30562
|
-
client.getStaleWBeatIds(
|
|
30556
|
+
withLeverage ? client.rankProjectBeatVersionsWithLeverage(systemId, lens) : client.rankProjectBeatVersions(systemId, lens),
|
|
30557
|
+
client.getStaleWBeatIds(systemId)
|
|
30563
30558
|
]);
|
|
30564
30559
|
if (rankedResult.status === "rejected") {
|
|
30565
30560
|
const msg = rankedResult.reason instanceof Error ? rankedResult.reason.message : String(rankedResult.reason);
|
|
@@ -30571,7 +30566,7 @@ function registerValueVelocityTools(server, ctx, client) {
|
|
|
30571
30566
|
return { content: [{ type: "text", text: "No Beat Versions with confirmed Value Velocity inputs found." }] };
|
|
30572
30567
|
}
|
|
30573
30568
|
const lines = [
|
|
30574
|
-
`Ranked Beat Versions for
|
|
30569
|
+
`Ranked Beat Versions for system ${systemId} (lens: ${lens}):`,
|
|
30575
30570
|
"",
|
|
30576
30571
|
withLeverage ? "| Rank | Beat Version | Title | Chosen Score | Leverage Score |" : "| Rank | Beat Version | Title | Chosen Score |",
|
|
30577
30572
|
withLeverage ? "|------|-------------|-------|-------------|----------------|" : "|------|-------------|-------|-------------|"
|
|
@@ -30798,8 +30793,15 @@ function registerValueVelocityTools(server, ctx, client) {
|
|
|
30798
30793
|
// ../../libs/harmonica-services/src/mcp/tools/work-item-tools.ts
|
|
30799
30794
|
var WORK_ITEM_STATUS_ENUM = ["not_started", "in_progress", "blocked", "done"];
|
|
30800
30795
|
function formatOwner(owner) {
|
|
30801
|
-
return `${owner.name} <${owner.email}
|
|
30802
|
-
}
|
|
30796
|
+
return owner ? `${owner.name} <${owner.email}>` : "unassigned";
|
|
30797
|
+
}
|
|
30798
|
+
var WORK_ITEM_EFFORT_SIZE_ENUM = [
|
|
30799
|
+
"XS",
|
|
30800
|
+
"S",
|
|
30801
|
+
"M",
|
|
30802
|
+
"L",
|
|
30803
|
+
"XL"
|
|
30804
|
+
];
|
|
30803
30805
|
function registerWorkItemTools(server, _ctx, client) {
|
|
30804
30806
|
server.tool(
|
|
30805
30807
|
"list_track_work_items",
|
|
@@ -30835,6 +30837,9 @@ function registerWorkItemTools(server, _ctx, client) {
|
|
|
30835
30837
|
`Status: ${wi.status}`,
|
|
30836
30838
|
wi.committedEstimateHours !== void 0 ? `Committed Estimate: ${wi.committedEstimateHours}h` : null,
|
|
30837
30839
|
wi.actualHours !== void 0 ? `Actual Hours: ${wi.actualHours}h` : null,
|
|
30840
|
+
wi.effortSize !== void 0 ? `Effort Size: ${wi.effortSize}` : null,
|
|
30841
|
+
wi.valueUnits !== void 0 ? `Value Units: ${wi.valueUnits}` : null,
|
|
30842
|
+
wi.costPerValueUnit !== void 0 ? `Cost per Value Unit: ${wi.costPerValueUnit}` : null,
|
|
30838
30843
|
wi.beatVersionId ? `Linked Beat Version: ${wi.beatVersionId}` : null,
|
|
30839
30844
|
estimateHint ? `Estimate Hint: ${estimateHint.hours}h (source: ${estimateHint.source})` : null,
|
|
30840
30845
|
wi.createdBy ? `Created By: ${wi.createdBy}` : null,
|
|
@@ -30850,18 +30855,30 @@ function registerWorkItemTools(server, _ctx, client) {
|
|
|
30850
30855
|
{
|
|
30851
30856
|
trackId: external_exports.string().describe("The Track (DeliverableGroup) ID this work item belongs to"),
|
|
30852
30857
|
title: external_exports.string().min(1).max(300).describe("Work item title"),
|
|
30853
|
-
ownerName: external_exports.string().min(1).describe("
|
|
30854
|
-
ownerEmail: external_exports.string().email().describe("
|
|
30858
|
+
ownerName: external_exports.string().min(1).optional().describe("Optional owner display name (requires ownerEmail too)"),
|
|
30859
|
+
ownerEmail: external_exports.string().email().optional().describe("Optional owner email (requires ownerName too). A work item may be left unowned."),
|
|
30855
30860
|
committedEstimateHours: external_exports.number().nonnegative().optional().describe("Optional committed estimate in hours"),
|
|
30861
|
+
effortSize: external_exports.enum(WORK_ITEM_EFFORT_SIZE_ENUM).optional().describe("Optional rough t-shirt effort size (delivery lens) \u2014 independent of valueUnits"),
|
|
30862
|
+
valueUnits: external_exports.number().nonnegative().optional().describe("Optional Value Units (commercial lens) \u2014 independent of effortSize"),
|
|
30863
|
+
costPerValueUnit: external_exports.number().nonnegative().optional().describe("Optional price per Value Unit"),
|
|
30856
30864
|
createdBy: external_exports.string().optional().describe("Optional creator identity for audit"),
|
|
30857
30865
|
beatVersionId: external_exports.string().min(1).optional().describe("Optional Beat Version to link for capability lineage. Must exist.")
|
|
30858
30866
|
},
|
|
30859
|
-
async ({ trackId, title, ownerName, ownerEmail, committedEstimateHours, createdBy, beatVersionId }) => {
|
|
30867
|
+
async ({ trackId, title, ownerName, ownerEmail, committedEstimateHours, effortSize, valueUnits, costPerValueUnit, createdBy, beatVersionId }) => {
|
|
30868
|
+
if (ownerName === void 0 !== (ownerEmail === void 0)) {
|
|
30869
|
+
return {
|
|
30870
|
+
content: [{ type: "text", text: "Setting the owner requires both ownerName and ownerEmail. Omit both to create an unowned work item." }],
|
|
30871
|
+
isError: true
|
|
30872
|
+
};
|
|
30873
|
+
}
|
|
30860
30874
|
const wi = await client.createWorkItem({
|
|
30861
30875
|
trackId,
|
|
30862
30876
|
title,
|
|
30863
|
-
owner: { name: ownerName, email: ownerEmail },
|
|
30877
|
+
...ownerName !== void 0 && ownerEmail !== void 0 && { owner: { name: ownerName, email: ownerEmail } },
|
|
30864
30878
|
...committedEstimateHours !== void 0 && { committedEstimateHours },
|
|
30879
|
+
...effortSize !== void 0 && { effortSize },
|
|
30880
|
+
...valueUnits !== void 0 && { valueUnits },
|
|
30881
|
+
...costPerValueUnit !== void 0 && { costPerValueUnit },
|
|
30865
30882
|
...createdBy !== void 0 && { createdBy },
|
|
30866
30883
|
...beatVersionId !== void 0 && { beatVersionId }
|
|
30867
30884
|
});
|
|
@@ -30877,17 +30894,21 @@ function registerWorkItemTools(server, _ctx, client) {
|
|
|
30877
30894
|
);
|
|
30878
30895
|
server.tool(
|
|
30879
30896
|
"update_work_item",
|
|
30880
|
-
"Update mutable content fields on a work item (title, owner, committedEstimateHours, actualHours, beatVersionId). To move it between Tracks use move_work_item; to change its status use transition_work_item_status.",
|
|
30897
|
+
"Update mutable content fields on a work item (title, owner, committedEstimateHours, actualHours, effortSize, valueUnits, costPerValueUnit, beatVersionId). To move it between Tracks use move_work_item; to change its status use transition_work_item_status.",
|
|
30881
30898
|
{
|
|
30882
30899
|
workItemId: external_exports.string().describe("The work item ID"),
|
|
30883
30900
|
title: external_exports.string().min(1).max(300).optional().describe("New title"),
|
|
30884
30901
|
ownerName: external_exports.string().min(1).optional().describe("New owner display name (requires ownerEmail too)"),
|
|
30885
30902
|
ownerEmail: external_exports.string().email().optional().describe("New owner email (requires ownerName too)"),
|
|
30903
|
+
unassignOwner: external_exports.boolean().optional().describe("Pass true to clear the owner, leaving the work item unowned. Cannot be combined with ownerName/ownerEmail."),
|
|
30886
30904
|
committedEstimateHours: external_exports.number().nonnegative().optional().describe("New committed estimate in hours"),
|
|
30887
30905
|
actualHours: external_exports.number().nonnegative().optional().describe("New actual hours logged"),
|
|
30906
|
+
effortSize: external_exports.enum(WORK_ITEM_EFFORT_SIZE_ENUM).nullable().optional().describe("Rough t-shirt effort size (delivery lens). Pass null to clear; omit to leave untouched."),
|
|
30907
|
+
valueUnits: external_exports.number().nonnegative().nullable().optional().describe("Value Units (commercial lens). Pass null to clear; omit to leave untouched."),
|
|
30908
|
+
costPerValueUnit: external_exports.number().nonnegative().nullable().optional().describe("Price per Value Unit. Pass null to clear; omit to leave untouched."),
|
|
30888
30909
|
beatVersionId: external_exports.string().min(1).nullable().optional().describe("Link to a Beat Version for capability lineage. Pass null to unlink; omit to leave the current link untouched.")
|
|
30889
30910
|
},
|
|
30890
|
-
async ({ workItemId, title, ownerName, ownerEmail, committedEstimateHours, actualHours, beatVersionId }) => {
|
|
30911
|
+
async ({ workItemId, title, ownerName, ownerEmail, unassignOwner, committedEstimateHours, actualHours, effortSize, valueUnits, costPerValueUnit, beatVersionId }) => {
|
|
30891
30912
|
const hasOwnerUpdate = ownerName !== void 0 || ownerEmail !== void 0;
|
|
30892
30913
|
if (hasOwnerUpdate && (ownerName === void 0 || ownerEmail === void 0)) {
|
|
30893
30914
|
return {
|
|
@@ -30895,7 +30916,13 @@ function registerWorkItemTools(server, _ctx, client) {
|
|
|
30895
30916
|
isError: true
|
|
30896
30917
|
};
|
|
30897
30918
|
}
|
|
30898
|
-
|
|
30919
|
+
if (unassignOwner && hasOwnerUpdate) {
|
|
30920
|
+
return {
|
|
30921
|
+
content: [{ type: "text", text: "Cannot set and clear the owner in one update \u2014 pass either unassignOwner or ownerName/ownerEmail." }],
|
|
30922
|
+
isError: true
|
|
30923
|
+
};
|
|
30924
|
+
}
|
|
30925
|
+
const hasUpdates = title !== void 0 || hasOwnerUpdate || unassignOwner === true || committedEstimateHours !== void 0 || actualHours !== void 0 || effortSize !== void 0 || valueUnits !== void 0 || costPerValueUnit !== void 0 || beatVersionId !== void 0;
|
|
30899
30926
|
if (!hasUpdates) {
|
|
30900
30927
|
return {
|
|
30901
30928
|
content: [{ type: "text", text: `No updates provided for work item: ${workItemId}` }],
|
|
@@ -30905,8 +30932,12 @@ function registerWorkItemTools(server, _ctx, client) {
|
|
|
30905
30932
|
const wi = await client.updateWorkItem(workItemId, {
|
|
30906
30933
|
...title !== void 0 && { title },
|
|
30907
30934
|
...hasOwnerUpdate && { owner: { name: ownerName, email: ownerEmail } },
|
|
30935
|
+
...unassignOwner === true && { owner: null },
|
|
30908
30936
|
...committedEstimateHours !== void 0 && { committedEstimateHours },
|
|
30909
30937
|
...actualHours !== void 0 && { actualHours },
|
|
30938
|
+
...effortSize !== void 0 && { effortSize },
|
|
30939
|
+
...valueUnits !== void 0 && { valueUnits },
|
|
30940
|
+
...costPerValueUnit !== void 0 && { costPerValueUnit },
|
|
30910
30941
|
...beatVersionId !== void 0 && { beatVersionId }
|
|
30911
30942
|
});
|
|
30912
30943
|
if (!wi) {
|
|
@@ -31371,7 +31402,7 @@ var TOOL_PROFILES = Object.freeze({
|
|
|
31371
31402
|
// Copilot's orchestrator degrades with large tool sets, so each module is trimmed
|
|
31372
31403
|
// to its query tools; create_note is the single write action.
|
|
31373
31404
|
"copilot": Object.freeze([
|
|
31374
|
-
allow(registerProjectTools, ["list_systems", "get_system_context"
|
|
31405
|
+
allow(registerProjectTools, ["list_systems", "get_system_context"]),
|
|
31375
31406
|
allow(registerBeatTools, ["list_beats", "get_beat", "list_revisions", "get_revision"]),
|
|
31376
31407
|
allow(registerBeatVersionTools, ["list_beat_versions", "get_beat_version"]),
|
|
31377
31408
|
allow(registerNoteTools, ["list_notes", "get_note", "create_note", "list_documents"]),
|
|
@@ -31452,7 +31483,7 @@ function registerBeatResources(server, ctx, client) {
|
|
|
31452
31483
|
async (uri, { projectId }) => {
|
|
31453
31484
|
const pid = String(projectId);
|
|
31454
31485
|
await assertProjectInOrg(client, pid, ctx.orgId);
|
|
31455
|
-
const beats = await client.
|
|
31486
|
+
const beats = await client.listSystemBeats(pid);
|
|
31456
31487
|
const text = formatBeatSummaryTable(beats);
|
|
31457
31488
|
return { contents: [{ uri: uri.href, text }] };
|
|
31458
31489
|
}
|
|
@@ -31486,7 +31517,7 @@ function registerGuidelinesResources(server, client) {
|
|
|
31486
31517
|
);
|
|
31487
31518
|
}
|
|
31488
31519
|
|
|
31489
|
-
// ../../libs/harmonica-services/src/mcp/resources/
|
|
31520
|
+
// ../../libs/harmonica-services/src/mcp/resources/system-resources.ts
|
|
31490
31521
|
function registerProjectResources(server, ctx, client) {
|
|
31491
31522
|
server.resource(
|
|
31492
31523
|
"project-context",
|
|
@@ -31494,7 +31525,7 @@ function registerProjectResources(server, ctx, client) {
|
|
|
31494
31525
|
async (uri, { projectId }) => {
|
|
31495
31526
|
const pid = String(projectId);
|
|
31496
31527
|
const project = await fetchProjectInOrg(client, pid, ctx.orgId);
|
|
31497
|
-
const notes = await client.
|
|
31528
|
+
const notes = await client.listSystemNotes(pid, { limit: DEFAULT_CONTEXT_NOTE_LIMIT });
|
|
31498
31529
|
const text = formatProjectContext(project, notes);
|
|
31499
31530
|
return { contents: [{ uri: uri.href, text }] };
|
|
31500
31531
|
}
|
|
@@ -31815,7 +31846,7 @@ function createHttpClient(config2) {
|
|
|
31815
31846
|
const result = await request("POST", `/api/systems/${encodeURIComponent(projectId)}/beats/next-id`);
|
|
31816
31847
|
return result.beatId;
|
|
31817
31848
|
},
|
|
31818
|
-
|
|
31849
|
+
listSystemBeats: async (projectId) => {
|
|
31819
31850
|
const result = await request("GET", `/api/systems/${encodeURIComponent(projectId)}/beats`);
|
|
31820
31851
|
return (result?.beats ?? []).map((b) => ({
|
|
31821
31852
|
...b,
|
|
@@ -31971,6 +32002,21 @@ function createHttpClient(config2) {
|
|
|
31971
32002
|
const result = await request("POST", `/api/layers/${encodeURIComponent(layerId)}/status`, { status, reason });
|
|
31972
32003
|
return result?.layer;
|
|
31973
32004
|
},
|
|
32005
|
+
listSystemNotes: async (projectId, filters) => {
|
|
32006
|
+
const params = new URLSearchParams();
|
|
32007
|
+
if (filters?.noteType) {
|
|
32008
|
+
const types = Array.isArray(filters.noteType) ? filters.noteType : [filters.noteType];
|
|
32009
|
+
types.forEach((t) => params.append("type", t));
|
|
32010
|
+
}
|
|
32011
|
+
if (filters?.status) params.set("status", filters.status);
|
|
32012
|
+
if (filters?.revisionId) params.set("revisionId", filters.revisionId);
|
|
32013
|
+
if (filters?.excludeChildren) params.set("excludeChildren", "true");
|
|
32014
|
+
if (filters?.significance) params.set("significance", filters.significance);
|
|
32015
|
+
if (filters?.limit !== void 0) params.set("limit", String(filters.limit));
|
|
32016
|
+
const qs = params.toString() ? `?${params.toString()}` : "";
|
|
32017
|
+
const result = await request("GET", `/api/systems/${encodeURIComponent(projectId)}/notes${qs}`);
|
|
32018
|
+
return result?.notes ?? [];
|
|
32019
|
+
},
|
|
31974
32020
|
listProjectNotes: async (projectId, filters) => {
|
|
31975
32021
|
const params = new URLSearchParams();
|
|
31976
32022
|
if (filters?.noteType) {
|
|
@@ -32110,6 +32156,14 @@ function createHttpClient(config2) {
|
|
|
32110
32156
|
const result = await request("GET", `/api/systems/${encodeURIComponent(projectId)}/revisions/pending${qs}`);
|
|
32111
32157
|
return result ?? [];
|
|
32112
32158
|
},
|
|
32159
|
+
listSystemRevisions: async (projectId, options) => {
|
|
32160
|
+
const params = new URLSearchParams();
|
|
32161
|
+
if (options?.status) params.set("status", options.status);
|
|
32162
|
+
if (options?.includeArchived) params.set("includeArchived", "true");
|
|
32163
|
+
const qs = params.toString() ? `?${params.toString()}` : "";
|
|
32164
|
+
const result = await request("GET", `/api/systems/${encodeURIComponent(projectId)}/revisions${qs}`);
|
|
32165
|
+
return result ?? [];
|
|
32166
|
+
},
|
|
32113
32167
|
listProjectRevisions: async (projectId, options) => {
|
|
32114
32168
|
const params = new URLSearchParams();
|
|
32115
32169
|
if (options?.status) params.set("status", options.status);
|
|
@@ -32339,6 +32393,14 @@ function createHttpClient(config2) {
|
|
|
32339
32393
|
// partitions, but the REST surface is unified per the project-level
|
|
32340
32394
|
// "avoid nested/duplicate routes" guidance.
|
|
32341
32395
|
getSession: (sessionId) => request("GET", `/api/sessions/${encodeURIComponent(sessionId)}`),
|
|
32396
|
+
listSystemSessions: async (projectId, options) => {
|
|
32397
|
+
const params = new URLSearchParams();
|
|
32398
|
+
params.set("projectId", projectId);
|
|
32399
|
+
if (options?.status) params.set("status", options.status);
|
|
32400
|
+
if (options?.includeTerminal) params.set("includeTerminal", "true");
|
|
32401
|
+
const result = await request("GET", `/api/sessions?${params.toString()}`);
|
|
32402
|
+
return result ?? [];
|
|
32403
|
+
},
|
|
32342
32404
|
listProjectSessions: async (projectId, options) => {
|
|
32343
32405
|
const params = new URLSearchParams();
|
|
32344
32406
|
params.set("projectId", projectId);
|
|
@@ -32526,6 +32588,14 @@ function createHttpClient(config2) {
|
|
|
32526
32588
|
getTask: async (taskId) => {
|
|
32527
32589
|
return request("GET", `/api/tasks/${encodeURIComponent(taskId)}`);
|
|
32528
32590
|
},
|
|
32591
|
+
listSystemTasks: async (projectId, options) => {
|
|
32592
|
+
const params = new URLSearchParams();
|
|
32593
|
+
if (options?.status) params.set("status", options.status);
|
|
32594
|
+
if (options?.limit) params.set("limit", String(options.limit));
|
|
32595
|
+
const qs = params.toString() ? `?${params.toString()}` : "";
|
|
32596
|
+
const result = await request("GET", `/api/systems/${encodeURIComponent(projectId)}/tasks${qs}`);
|
|
32597
|
+
return result?.tasks ?? [];
|
|
32598
|
+
},
|
|
32529
32599
|
listProjectTasks: async (projectId, options) => {
|
|
32530
32600
|
const params = new URLSearchParams();
|
|
32531
32601
|
if (options?.status) params.set("status", options.status);
|
|
@@ -32557,6 +32627,15 @@ function createHttpClient(config2) {
|
|
|
32557
32627
|
}, LONG_RUNNING_TIMEOUT_MS);
|
|
32558
32628
|
},
|
|
32559
32629
|
// Activity — forward to API endpoint
|
|
32630
|
+
listSystemActivities: async (projectId, options) => {
|
|
32631
|
+
const params = new URLSearchParams();
|
|
32632
|
+
if (options?.limit) params.set("limit", String(options.limit));
|
|
32633
|
+
if (options?.minImportance) params.set("importance", options.minImportance);
|
|
32634
|
+
const qs = params.toString();
|
|
32635
|
+
const path = `/api/systems/${encodeURIComponent(projectId)}/activities${qs ? `?${qs}` : ""}`;
|
|
32636
|
+
const result = await request("GET", path);
|
|
32637
|
+
return { entries: result.activities ?? [], hasMore: false };
|
|
32638
|
+
},
|
|
32560
32639
|
listProjectActivities: async (projectId, options) => {
|
|
32561
32640
|
const params = new URLSearchParams();
|
|
32562
32641
|
if (options?.limit) params.set("limit", String(options.limit));
|
|
@@ -32838,8 +32917,8 @@ function createHttpClient(config2) {
|
|
|
32838
32917
|
}
|
|
32839
32918
|
return download;
|
|
32840
32919
|
},
|
|
32841
|
-
|
|
32842
|
-
|
|
32920
|
+
importSystemSnapshot: (snapshot, options) => request("POST", "/api/systems/import", { ...snapshot, importOptions: options }, LONG_RUNNING_TIMEOUT_MS),
|
|
32921
|
+
importSystemSnapshotFromUrl: (url2, options) => request("POST", "/api/systems/import", { snapshotUrl: url2, importOptions: options }, LONG_RUNNING_TIMEOUT_MS),
|
|
32843
32922
|
// Embedding similarity
|
|
32844
32923
|
embedProjectEntities: (projectId) => request("POST", `/api/systems/${encodeURIComponent(projectId)}/embeddings/generate`),
|
|
32845
32924
|
findSimilarNotes: async (noteId, projectId, options) => {
|
|
@@ -32905,6 +32984,18 @@ function createHttpClient(config2) {
|
|
|
32905
32984
|
const res = await request("GET", `/api/checks/${encodeURIComponent(checkId)}`);
|
|
32906
32985
|
return res.check;
|
|
32907
32986
|
},
|
|
32987
|
+
listSystemChecks: async (projectId, checkType, options) => {
|
|
32988
|
+
const params = new URLSearchParams();
|
|
32989
|
+
if (checkType) params.set("type", checkType);
|
|
32990
|
+
if (options?.limit !== void 0) params.set("limit", String(options.limit));
|
|
32991
|
+
if (options?.cursor) params.set("cursor", options.cursor);
|
|
32992
|
+
const qs = params.toString() ? `?${params.toString()}` : "";
|
|
32993
|
+
const res = await request(
|
|
32994
|
+
"GET",
|
|
32995
|
+
`/api/systems/${encodeURIComponent(projectId)}/checks${qs}`
|
|
32996
|
+
);
|
|
32997
|
+
return { checks: res?.checks ?? [], nextCursor: res?.nextCursor };
|
|
32998
|
+
},
|
|
32908
32999
|
listProjectChecks: async (projectId, checkType, options) => {
|
|
32909
33000
|
const params = new URLSearchParams();
|
|
32910
33001
|
if (checkType) params.set("type", checkType);
|
|
@@ -33003,6 +33094,11 @@ function createHttpClient(config2) {
|
|
|
33003
33094
|
if (res === void 0) throw new Error(`Teamspace ${teamspaceId} not found`);
|
|
33004
33095
|
return res.deliverableGroups;
|
|
33005
33096
|
},
|
|
33097
|
+
listSystemDeliverableGroups: async (projectId) => {
|
|
33098
|
+
const res = await request("GET", `/api/systems/${encodeURIComponent(projectId)}/deliverable-groups`);
|
|
33099
|
+
if (res === void 0) throw new Error(`Project ${projectId} not found`);
|
|
33100
|
+
return res.deliverableGroups;
|
|
33101
|
+
},
|
|
33006
33102
|
listProjectDeliverableGroups: async (projectId) => {
|
|
33007
33103
|
const res = await request("GET", `/api/systems/${encodeURIComponent(projectId)}/deliverable-groups`);
|
|
33008
33104
|
if (res === void 0) throw new Error(`Project ${projectId} not found`);
|
|
@@ -33182,11 +33278,14 @@ function createHttpClient(config2) {
|
|
|
33182
33278
|
},
|
|
33183
33279
|
// WorkItems (WorkItem exposure layer — owned, statused unit of operational work under a Track)
|
|
33184
33280
|
createWorkItem: async (input) => {
|
|
33185
|
-
const { trackId, title, owner, committedEstimateHours, createdBy, beatVersionId } = input;
|
|
33281
|
+
const { trackId, title, owner, committedEstimateHours, effortSize, valueUnits, costPerValueUnit, createdBy, beatVersionId } = input;
|
|
33186
33282
|
const body = {
|
|
33187
33283
|
title,
|
|
33188
|
-
owner,
|
|
33284
|
+
...owner !== void 0 && { owner },
|
|
33189
33285
|
...committedEstimateHours !== void 0 && { committedEstimateHours },
|
|
33286
|
+
...effortSize !== void 0 && { effortSize },
|
|
33287
|
+
...valueUnits !== void 0 && { valueUnits },
|
|
33288
|
+
...costPerValueUnit !== void 0 && { costPerValueUnit },
|
|
33190
33289
|
...createdBy !== void 0 && { createdBy },
|
|
33191
33290
|
...beatVersionId !== void 0 && { beatVersionId }
|
|
33192
33291
|
};
|
|
@@ -33729,7 +33828,7 @@ function loadConfig() {
|
|
|
33729
33828
|
};
|
|
33730
33829
|
}
|
|
33731
33830
|
async function main() {
|
|
33732
|
-
console.error(`[harmonica-mcp] v${"
|
|
33831
|
+
console.error(`[harmonica-mcp] v${"2.1.0"} starting\u2026`);
|
|
33733
33832
|
const config2 = loadConfig();
|
|
33734
33833
|
const client = createHttpClient({
|
|
33735
33834
|
apiBaseUrl: config2.apiBaseUrl,
|