@greenflags/mcp 0.2.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.
@@ -0,0 +1,31 @@
1
+ import { type McpToolResult } from "../result.js";
2
+ export declare const toolDefinition: {
3
+ name: string;
4
+ description: string;
5
+ annotations: {
6
+ destructiveHint: boolean;
7
+ idempotentHint: boolean;
8
+ };
9
+ inputSchema: {
10
+ type: "object";
11
+ properties: {
12
+ project: {
13
+ type: string;
14
+ description: string;
15
+ };
16
+ project_id: {
17
+ type: string;
18
+ description: string;
19
+ };
20
+ flag: {
21
+ type: string;
22
+ description: string;
23
+ };
24
+ flag_id: {
25
+ type: string;
26
+ description: string;
27
+ };
28
+ };
29
+ };
30
+ };
31
+ export declare function handler(args: Record<string, unknown>): Promise<McpToolResult>;
@@ -0,0 +1,45 @@
1
+ import { callApi } from "../api.js";
2
+ import { requireProjectId } from "../config.js";
3
+ import { okResult } from "../result.js";
4
+ import { invalidateFlagCache, resolveFlag } from "../resolvers.js";
5
+ export const toolDefinition = {
6
+ name: "archive_flag",
7
+ description: "Archive a flag (soft-delete: stops being served but kept for history).\n" +
8
+ "Use when: a flag is no longer needed. Prefer set_flag_value(false) if you only want to turn it off temporarily.\n" +
9
+ "Prerequisites: a project + an existing flag (key or id). Call list_flags to find candidates.\n" +
10
+ "Side effects: DESTRUCTIVE (flag stops serving). Idempotent — re-archiving returns changed:false.\n" +
11
+ 'Example: archive_flag(flag: "legacy-banner").',
12
+ annotations: {
13
+ destructiveHint: true,
14
+ idempotentHint: true,
15
+ },
16
+ inputSchema: {
17
+ type: "object",
18
+ properties: {
19
+ project: {
20
+ type: "string",
21
+ description: "Project id or slug. Optional if GREENFLAGS_PROJECT_ID is set.",
22
+ },
23
+ project_id: { type: "string", description: "Legacy alias for `project`." },
24
+ flag: {
25
+ type: "string",
26
+ description: "Flag key (preferred) or flag id.",
27
+ },
28
+ flag_id: { type: "string", description: "Legacy alias for `flag`." },
29
+ },
30
+ },
31
+ };
32
+ export async function handler(args) {
33
+ const projectId = requireProjectId(args["project"] ?? args["project_id"]);
34
+ const flagArg = (args["flag"] ?? args["flag_id"]);
35
+ if (typeof flagArg !== "string" || flagArg.trim() === "") {
36
+ throw new Error("`flag` (key or id) is required.");
37
+ }
38
+ const current = await resolveFlag(projectId, flagArg.trim());
39
+ const data = await callApi(`/admin/projects/${encodeURIComponent(projectId)}/flags/${encodeURIComponent(current.id)}/archive`, "POST");
40
+ invalidateFlagCache(projectId);
41
+ const before = { status: current.status };
42
+ const after = data.flag;
43
+ const changed = before.status !== after.status;
44
+ return okResult({ flag: after, before, changed }, `Flag ${after.key} archived${changed ? "" : " (was already archived)"}.`);
45
+ }
@@ -0,0 +1,41 @@
1
+ import { type McpToolResult } from "../result.js";
2
+ export declare const toolDefinition: {
3
+ name: string;
4
+ description: string;
5
+ annotations: {
6
+ destructiveHint: boolean;
7
+ idempotentHint: boolean;
8
+ };
9
+ inputSchema: {
10
+ type: "object";
11
+ properties: {
12
+ project: {
13
+ type: string;
14
+ description: string;
15
+ };
16
+ project_id: {
17
+ type: string;
18
+ description: string;
19
+ };
20
+ key: {
21
+ type: string;
22
+ description: string;
23
+ };
24
+ name: {
25
+ type: string;
26
+ description: string;
27
+ };
28
+ description: {
29
+ type: string;
30
+ description: string;
31
+ };
32
+ type: {
33
+ type: string;
34
+ enum: string[];
35
+ description: string;
36
+ };
37
+ };
38
+ required: string[];
39
+ };
40
+ };
41
+ export declare function handler(args: Record<string, unknown>): Promise<McpToolResult>;
@@ -0,0 +1,57 @@
1
+ import { callApi } from "../api.js";
2
+ import { requireProjectId } from "../config.js";
3
+ import { okResult } from "../result.js";
4
+ import { invalidateFlagCache } from "../resolvers.js";
5
+ export const toolDefinition = {
6
+ name: "create_flag",
7
+ description: "Create a new feature flag in a project. Defaults to a boolean flag; pass `type` for string/number/json.\n" +
8
+ "Use when: the user wants a brand-new flag. To change an existing flag use update_flag; to set its value use set_flag_value.\n" +
9
+ "Prerequisites: a project. The key must be unique within the project (409 on conflict).\n" +
10
+ "Side effects: creates a flag (not idempotent — re-calling with the same key fails).\n" +
11
+ 'Example: create_flag(key: "dark-mode", name: "Dark Mode").',
12
+ annotations: {
13
+ destructiveHint: false,
14
+ idempotentHint: false,
15
+ },
16
+ inputSchema: {
17
+ type: "object",
18
+ properties: {
19
+ project: {
20
+ type: "string",
21
+ description: "Project id or slug. Optional if GREENFLAGS_PROJECT_ID is set.",
22
+ },
23
+ project_id: { type: "string", description: "Legacy alias for `project`." },
24
+ key: {
25
+ type: "string",
26
+ description: "Unique flag key (e.g. 'dark-mode').",
27
+ },
28
+ name: { type: "string", description: "Human-readable flag name." },
29
+ description: { type: "string", description: "Optional description." },
30
+ type: {
31
+ type: "string",
32
+ enum: ["boolean", "string", "number", "json"],
33
+ description: "Flag value type. Defaults to 'boolean'.",
34
+ },
35
+ },
36
+ required: ["key", "name"],
37
+ },
38
+ };
39
+ export async function handler(args) {
40
+ const projectId = requireProjectId(args["project"] ?? args["project_id"]);
41
+ const key = String(args["key"] ?? "").trim();
42
+ const name = String(args["name"] ?? "").trim();
43
+ if (!key || !name)
44
+ throw new Error("`key` and `name` are required.");
45
+ const type = typeof args["type"] === "string"
46
+ ? args["type"]
47
+ : "boolean";
48
+ const data = await callApi(`/admin/projects/${encodeURIComponent(projectId)}/flags`, "POST", {
49
+ key,
50
+ name,
51
+ description: args["description"] ?? null,
52
+ type,
53
+ });
54
+ invalidateFlagCache(projectId);
55
+ const f = data.flag;
56
+ return okResult({ flag: f }, `Flag created: ${f.key} (id ${f.id}, type ${f.type})`);
57
+ }
@@ -0,0 +1,32 @@
1
+ import { type McpToolResult } from "../result.js";
2
+ export declare const toolDefinition: {
3
+ name: string;
4
+ description: string;
5
+ annotations: {
6
+ readOnlyHint: boolean;
7
+ idempotentHint: boolean;
8
+ };
9
+ inputSchema: {
10
+ type: "object";
11
+ properties: {
12
+ project: {
13
+ type: string;
14
+ description: string;
15
+ };
16
+ project_id: {
17
+ type: string;
18
+ description: string;
19
+ };
20
+ flag: {
21
+ type: string;
22
+ description: string;
23
+ };
24
+ flag_key: {
25
+ type: string;
26
+ description: string;
27
+ };
28
+ };
29
+ required: never[];
30
+ };
31
+ };
32
+ export declare function handler(args: Record<string, unknown>): Promise<McpToolResult>;
@@ -0,0 +1,48 @@
1
+ import { callApi } from "../api.js";
2
+ import { requireProjectId } from "../config.js";
3
+ import { okResult } from "../result.js";
4
+ import { listAllFlags } from "../resolvers.js";
5
+ export const toolDefinition = {
6
+ name: "get_flag",
7
+ description: "Get a single flag's metadata (id, key, name, type, status, description) by key (preferred) or id.\n" +
8
+ "Use when: you need one flag's details. For its per-environment values use get_flag_values instead.\n" +
9
+ "Prerequisites: a project (call list_projects if unknown, or set GREENFLAGS_PROJECT_ID).\n" +
10
+ "Side effects: none (read-only).\n" +
11
+ 'Example: get_flag(flag: "dark-mode").',
12
+ annotations: {
13
+ readOnlyHint: true,
14
+ idempotentHint: true,
15
+ },
16
+ inputSchema: {
17
+ type: "object",
18
+ properties: {
19
+ project: {
20
+ type: "string",
21
+ description: "Project id or slug. Optional if GREENFLAGS_PROJECT_ID is set.",
22
+ },
23
+ project_id: { type: "string", description: "Legacy alias for `project`." },
24
+ flag: {
25
+ type: "string",
26
+ description: "Flag key (e.g. 'dark-mode') or flag id. Key is preferred — it uses a single backend call.",
27
+ },
28
+ flag_key: { type: "string", description: "Legacy alias for `flag`." },
29
+ },
30
+ required: [],
31
+ },
32
+ };
33
+ export async function handler(args) {
34
+ const projectId = requireProjectId(args["project"] ?? args["project_id"]);
35
+ const flagArg = (args["flag"] ?? args["flag_key"]);
36
+ if (typeof flagArg !== "string" || flagArg.trim() === "") {
37
+ throw new Error("`flag` (key or id) is required.");
38
+ }
39
+ let flagKey = flagArg.trim();
40
+ const all = await listAllFlags(projectId);
41
+ const byId = all.find((f) => f.id === flagKey);
42
+ if (byId)
43
+ flagKey = byId.key;
44
+ const data = await callApi(`/admin/projects/${encodeURIComponent(projectId)}/flags/by-key/${encodeURIComponent(flagKey)}`);
45
+ const f = data.flag;
46
+ const summary = `${f.key} (${f.type}, ${f.status}): ${f.name}${f.description ? ` — ${f.description}` : ""}`;
47
+ return okResult({ flag: f }, summary);
48
+ }
@@ -0,0 +1,35 @@
1
+ import { type McpToolResult } from "../result.js";
2
+ export declare const toolDefinition: {
3
+ name: string;
4
+ description: string;
5
+ annotations: {
6
+ readOnlyHint: boolean;
7
+ idempotentHint: boolean;
8
+ };
9
+ inputSchema: {
10
+ type: "object";
11
+ properties: {
12
+ project: {
13
+ type: string;
14
+ description: string;
15
+ };
16
+ project_id: {
17
+ type: string;
18
+ description: string;
19
+ };
20
+ flag: {
21
+ type: string;
22
+ description: string;
23
+ };
24
+ flag_key: {
25
+ type: string;
26
+ description: string;
27
+ };
28
+ environment: {
29
+ type: string;
30
+ description: string;
31
+ };
32
+ };
33
+ };
34
+ };
35
+ export declare function handler(args: Record<string, unknown>): Promise<McpToolResult>;
@@ -0,0 +1,77 @@
1
+ import { callApi } from "../api.js";
2
+ import { requireProjectId } from "../config.js";
3
+ import { okResult } from "../result.js";
4
+ import { listAllEnvironments, resolveEnvironmentId, resolveFlag, } from "../resolvers.js";
5
+ export const toolDefinition = {
6
+ name: "get_flag_values",
7
+ description: "Get a flag's value across all environments, or a single environment when `environment` is given.\n" +
8
+ "Use when: answering \"is X enabled in prod?\" or \"what value does Y have in staging?\".\n" +
9
+ "Prerequisites: a project + an existing flag (key or id).\n" +
10
+ "Side effects: none (read-only).\n" +
11
+ 'Example: get_flag_values(flag: "checkout-v2", environment: "prod").',
12
+ annotations: {
13
+ readOnlyHint: true,
14
+ idempotentHint: true,
15
+ },
16
+ inputSchema: {
17
+ type: "object",
18
+ properties: {
19
+ project: {
20
+ type: "string",
21
+ description: "Project id or slug. Optional if GREENFLAGS_PROJECT_ID is set.",
22
+ },
23
+ project_id: { type: "string", description: "Legacy alias for `project`." },
24
+ flag: {
25
+ type: "string",
26
+ description: "Flag key (preferred) or flag id.",
27
+ },
28
+ flag_key: { type: "string", description: "Legacy alias for `flag`." },
29
+ environment: {
30
+ type: "string",
31
+ description: "Optional environment slug or id. When set, returns only that env value.",
32
+ },
33
+ },
34
+ },
35
+ };
36
+ export async function handler(args) {
37
+ const projectId = requireProjectId(args["project"] ?? args["project_id"]);
38
+ const flagArg = (args["flag"] ?? args["flag_key"]);
39
+ if (typeof flagArg !== "string" || flagArg.trim() === "") {
40
+ throw new Error("`flag` (key or id) is required.");
41
+ }
42
+ const envArg = typeof args["environment"] === "string" ? args["environment"] : undefined;
43
+ const flag = await resolveFlag(projectId, flagArg.trim());
44
+ const envs = await listAllEnvironments(projectId);
45
+ const envById = new Map(envs.map((e) => [e.id, e]));
46
+ const data = await callApi(`/admin/projects/${encodeURIComponent(projectId)}/flags/${encodeURIComponent(flag.id)}/values`);
47
+ let rows = data.values;
48
+ let envFilter;
49
+ if (envArg) {
50
+ const envId = await resolveEnvironmentId(projectId, envArg);
51
+ rows = rows.filter((r) => r.environmentId === envId);
52
+ const e = envById.get(envId);
53
+ envFilter = { id: envId, slug: e?.slug ?? envId };
54
+ }
55
+ const values = rows.map((r) => {
56
+ const e = envById.get(r.environmentId);
57
+ return {
58
+ environment_id: r.environmentId,
59
+ environment_slug: e?.slug ?? null,
60
+ environment_name: e?.name ?? null,
61
+ is_sensitive: Boolean(e?.is_sensitive),
62
+ type: r.type,
63
+ value: r.value,
64
+ updated_by_user_id: r.updatedByUserId,
65
+ };
66
+ });
67
+ const summary = values.length === 0
68
+ ? `Flag '${flag.key}' has no value set${envFilter ? ` in env '${envFilter.slug}'` : ""}.`
69
+ : `Flag '${flag.key}' values:\n${values
70
+ .map((v) => `- ${v.environment_slug ?? v.environment_id}: ${JSON.stringify(v.value)}`)
71
+ .join("\n")}`;
72
+ return okResult({
73
+ flag: { id: flag.id, key: flag.key, type: flag.type },
74
+ environment: envFilter ?? null,
75
+ values,
76
+ }, summary);
77
+ }
@@ -0,0 +1,23 @@
1
+ import { type McpToolResult } from "../result.js";
2
+ export declare const toolDefinition: {
3
+ name: string;
4
+ description: string;
5
+ annotations: {
6
+ readOnlyHint: boolean;
7
+ idempotentHint: boolean;
8
+ };
9
+ inputSchema: {
10
+ type: "object";
11
+ properties: {
12
+ project: {
13
+ type: string;
14
+ description: string;
15
+ };
16
+ project_id: {
17
+ type: string;
18
+ description: string;
19
+ };
20
+ };
21
+ };
22
+ };
23
+ export declare function handler(args: Record<string, unknown>): Promise<McpToolResult>;
@@ -0,0 +1,41 @@
1
+ import { requireProjectId } from "../config.js";
2
+ import { okResult } from "../result.js";
3
+ import { listAllEnvironments } from "../resolvers.js";
4
+ export const toolDefinition = {
5
+ name: "list_environments",
6
+ description: "List all environments in a project (e.g. dev, staging, prod), each with id, slug, and is_sensitive.\n" +
7
+ "Use when: you need environment slugs/ids, or to check which envs are sensitive before a write.\n" +
8
+ "Prerequisites: a project (call list_projects if unknown, or set GREENFLAGS_PROJECT_ID).\n" +
9
+ "Side effects: none (read-only). Writes to is_sensitive envs require confirm:true in set_flag_value.\n" +
10
+ 'Example: "what environments exist?" before set_flag_value.',
11
+ annotations: {
12
+ readOnlyHint: true,
13
+ idempotentHint: true,
14
+ },
15
+ inputSchema: {
16
+ type: "object",
17
+ properties: {
18
+ project: {
19
+ type: "string",
20
+ description: "Project id or slug. Optional if GREENFLAGS_PROJECT_ID is set.",
21
+ },
22
+ project_id: { type: "string", description: "Legacy alias for `project`." },
23
+ },
24
+ },
25
+ };
26
+ export async function handler(args) {
27
+ const projectId = requireProjectId(args["project"] ?? args["project_id"]);
28
+ const envs = await listAllEnvironments(projectId);
29
+ const items = envs.map((e) => ({
30
+ id: e.id,
31
+ name: e.name,
32
+ slug: e.slug,
33
+ is_sensitive: Boolean(e.is_sensitive),
34
+ }));
35
+ const summary = items.length === 0
36
+ ? "No environments in project."
37
+ : items
38
+ .map((e) => `- ${e.name} (slug: ${e.slug}, id: ${e.id})${e.is_sensitive ? " [sensitive]" : ""}`)
39
+ .join("\n");
40
+ return okResult({ items, total: items.length }, summary);
41
+ }
@@ -0,0 +1,36 @@
1
+ import { type McpToolResult } from "../result.js";
2
+ export declare const toolDefinition: {
3
+ name: string;
4
+ description: string;
5
+ annotations: {
6
+ readOnlyHint: boolean;
7
+ idempotentHint: boolean;
8
+ };
9
+ inputSchema: {
10
+ type: "object";
11
+ properties: {
12
+ project: {
13
+ type: string;
14
+ description: string;
15
+ };
16
+ project_id: {
17
+ type: string;
18
+ description: string;
19
+ };
20
+ environment: {
21
+ type: string;
22
+ description: string;
23
+ };
24
+ status: {
25
+ type: string;
26
+ enum: string[];
27
+ description: string;
28
+ };
29
+ search: {
30
+ type: string;
31
+ description: string;
32
+ };
33
+ };
34
+ };
35
+ };
36
+ export declare function handler(args: Record<string, unknown>): Promise<McpToolResult>;
@@ -0,0 +1,94 @@
1
+ import { callApi } from "../api.js";
2
+ import { requireProjectId } from "../config.js";
3
+ import { okResult } from "../result.js";
4
+ import { listAllFlags, resolveEnvironment, } from "../resolvers.js";
5
+ export const toolDefinition = {
6
+ name: "list_flags",
7
+ description: "List flags in a project, optionally filtered by status/search and showing each flag's value in one environment.\n" +
8
+ "Use when: browsing flags, or answering questions like \"which flags are off in prod?\" (pass environment + status).\n" +
9
+ "Prerequisites: a project (call list_projects if unknown, or set GREENFLAGS_PROJECT_ID).\n" +
10
+ "Side effects: none (read-only).\n" +
11
+ 'Example: list_flags(environment: "prod", status: "active", search: "checkout").',
12
+ annotations: {
13
+ readOnlyHint: true,
14
+ idempotentHint: true,
15
+ },
16
+ inputSchema: {
17
+ type: "object",
18
+ properties: {
19
+ project: {
20
+ type: "string",
21
+ description: "Project id or slug. Optional if GREENFLAGS_PROJECT_ID is set in the MCP server environment.",
22
+ },
23
+ project_id: {
24
+ type: "string",
25
+ description: "Legacy alias for `project`.",
26
+ },
27
+ environment: {
28
+ type: "string",
29
+ description: "Optional environment slug or id. When set, each flag entry includes `value_in_env`.",
30
+ },
31
+ status: {
32
+ type: "string",
33
+ enum: ["active", "archived"],
34
+ description: "Optional filter by status.",
35
+ },
36
+ search: {
37
+ type: "string",
38
+ description: "Optional case-insensitive substring filter applied to `key` and `name`.",
39
+ },
40
+ },
41
+ },
42
+ };
43
+ export async function handler(args) {
44
+ const projectId = requireProjectId(args["project"] ?? args["project_id"]);
45
+ const status = typeof args["status"] === "string" ? args["status"] : undefined;
46
+ const search = typeof args["search"] === "string" && args["search"].trim() !== ""
47
+ ? args["search"].toLowerCase().trim()
48
+ : undefined;
49
+ const envArg = typeof args["environment"] === "string" ? args["environment"] : undefined;
50
+ const flags = await listAllFlags(projectId);
51
+ let env;
52
+ if (envArg) {
53
+ const e = await resolveEnvironment(projectId, envArg);
54
+ env = { id: e.id, slug: e.slug, is_sensitive: Boolean(e.is_sensitive) };
55
+ }
56
+ let filtered = flags;
57
+ if (status)
58
+ filtered = filtered.filter((f) => f.status === status);
59
+ if (search)
60
+ filtered = filtered.filter((f) => f.key.toLowerCase().includes(search) ||
61
+ f.name.toLowerCase().includes(search));
62
+ const items = await Promise.all(filtered.map(async (f) => {
63
+ let value_in_env = undefined;
64
+ if (env) {
65
+ const data = await callApi(`/admin/projects/${encodeURIComponent(projectId)}/flags/${encodeURIComponent(f.id)}/values`);
66
+ const v = data.values.find((row) => row.environmentId === env.id);
67
+ value_in_env = v ? v.value : null;
68
+ }
69
+ return {
70
+ id: f.id,
71
+ key: f.key,
72
+ name: f.name,
73
+ type: f.type,
74
+ status: f.status,
75
+ description: f.description ?? null,
76
+ ...(env ? { value_in_env } : {}),
77
+ };
78
+ }));
79
+ const head = items
80
+ .slice(0, 5)
81
+ .map((i) => env
82
+ ? `- ${i.key} (${i.status}) = ${JSON.stringify(i.value_in_env)}`
83
+ : `- ${i.key} (${i.status}): ${i.name}`)
84
+ .join("\n");
85
+ const summary = items.length === 0
86
+ ? "No flags match."
87
+ : `Found ${items.length} flag(s)${env ? ` in env ${env.slug}` : ""}${status ? ` with status=${status}` : ""}${search ? ` matching "${search}"` : ""}.\n${head}${items.length > 5 ? "\n..." : ""}`;
88
+ return okResult({
89
+ items,
90
+ total: items.length,
91
+ filtered: Boolean(status || search || env),
92
+ environment: env ?? null,
93
+ }, summary);
94
+ }
@@ -0,0 +1,14 @@
1
+ import { type McpToolResult } from "../result.js";
2
+ export declare const toolDefinition: {
3
+ name: string;
4
+ description: string;
5
+ annotations: {
6
+ readOnlyHint: boolean;
7
+ idempotentHint: boolean;
8
+ };
9
+ inputSchema: {
10
+ type: "object";
11
+ properties: {};
12
+ };
13
+ };
14
+ export declare function handler(_args: Record<string, unknown>): Promise<McpToolResult>;
@@ -0,0 +1,34 @@
1
+ import { callApi } from "../api.js";
2
+ import { okResult } from "../result.js";
3
+ export const toolDefinition = {
4
+ name: "list_projects",
5
+ description: "List all projects in the authenticated workspace, returning each project's id, name, and slug.\n" +
6
+ "Use when: you don't yet know which project to operate on — every other tool needs a project.\n" +
7
+ "Prerequisites: none. This is the typical entry point.\n" +
8
+ "Side effects: none (read-only).\n" +
9
+ 'Example: "what projects do I have?" or as the first step before listing flags.',
10
+ annotations: {
11
+ readOnlyHint: true,
12
+ idempotentHint: true,
13
+ },
14
+ inputSchema: {
15
+ type: "object",
16
+ properties: {},
17
+ },
18
+ };
19
+ export async function handler(_args) {
20
+ const data = await callApi("/admin/projects");
21
+ const items = data.projects.map((p) => ({
22
+ id: p.id,
23
+ name: p.name,
24
+ slug: p.slug,
25
+ description: p.description ?? null,
26
+ }));
27
+ const summary = items.length === 0
28
+ ? "No projects in workspace."
29
+ : `Found ${items.length} project(s): ${items
30
+ .slice(0, 5)
31
+ .map((p) => p.slug)
32
+ .join(", ")}${items.length > 5 ? ", ..." : ""}`;
33
+ return okResult({ items, total: items.length }, summary);
34
+ }