@codazen/harmonica-mcp 3.4.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.
Files changed (2) hide show
  1. package/dist/index.js +100 -4
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -25241,7 +25241,7 @@ var STAGE_ENUM = external_exports.enum([
25241
25241
  "beta",
25242
25242
  "prod"
25243
25243
  ]);
25244
- function registerFeatureFlagTools(server, _ctx, client) {
25244
+ function registerFeatureFlagTools(server, ctx, client) {
25245
25245
  server.tool(
25246
25246
  "list_flags",
25247
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.",
@@ -25307,6 +25307,89 @@ function registerFeatureFlagTools(server, _ctx, client) {
25307
25307
  }
25308
25308
  }
25309
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
+ );
25310
25393
  }
25311
25394
  function formatFlag(flag, history) {
25312
25395
  const archived = flag.archivedAt ? " **(archived)**" : "";
@@ -25316,7 +25399,8 @@ function formatFlag(flag, history) {
25316
25399
  `**Scope:** ${flag.scope}`,
25317
25400
  `**Enabled in:** ${enabled}`,
25318
25401
  flag.description ? `**Description:** ${flag.description}` : null,
25319
- `**Last updated:** ${flag.updatedAt.slice(0, 10)} by ${flag.updatedBy}`
25402
+ `**Last updated:** ${flag.updatedAt.slice(0, 10)} by ${flag.updatedBy}`,
25403
+ flag.archivedAt ? `**Archived:** ${flag.archivedAt.slice(0, 10)} by ${flag.archivedBy}` : null
25320
25404
  ].filter(Boolean).join("\n");
25321
25405
  if (!history?.length) return parts;
25322
25406
  const historyLines = history.map((h) => {
@@ -34270,7 +34354,7 @@ function createHttpClient(config2) {
34270
34354
  );
34271
34355
  return result?.usedBy;
34272
34356
  },
34273
- // Feature flags (B-135 v9) — not available in HTTP transport; the published
34357
+ // Feature flags (B-135 v9/v10) — not available in HTTP transport; the published
34274
34358
  // npm package has no DynamoDB credentials.
34275
34359
  listFlags: async () => {
34276
34360
  throw new Error("Feature flag queries are not available in HTTP transport mode");
@@ -34280,6 +34364,18 @@ function createHttpClient(config2) {
34280
34364
  },
34281
34365
  checkFlag: async () => {
34282
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");
34283
34379
  }
34284
34380
  };
34285
34381
  return client;
@@ -34330,7 +34426,7 @@ function loadConfig() {
34330
34426
  };
34331
34427
  }
34332
34428
  async function main() {
34333
- console.error(`[harmonica-mcp] v${"3.4.0"} starting\u2026`);
34429
+ console.error(`[harmonica-mcp] v${"3.5.0"} starting\u2026`);
34334
34430
  const config2 = loadConfig();
34335
34431
  const client = createHttpClient({
34336
34432
  apiBaseUrl: config2.apiBaseUrl,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codazen/harmonica-mcp",
3
- "version": "3.4.0",
3
+ "version": "3.5.0",
4
4
  "description": "MCP server for Harmonica — connect any MCP-compatible AI assistant to Harmonica",
5
5
  "license": "MIT",
6
6
  "bin": {