@pennant/mcp 1.0.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/README.md +78 -0
- package/dist/config.d.ts +16 -0
- package/dist/config.js +29 -0
- package/dist/config.js.map +1 -0
- package/dist/console-client.d.ts +55 -0
- package/dist/console-client.js +128 -0
- package/dist/console-client.js.map +1 -0
- package/dist/explain.d.ts +21 -0
- package/dist/explain.js +168 -0
- package/dist/explain.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +27 -0
- package/dist/index.js.map +1 -0
- package/dist/sdk-guide.d.ts +27 -0
- package/dist/sdk-guide.js +259 -0
- package/dist/sdk-guide.js.map +1 -0
- package/dist/tools.d.ts +5 -0
- package/dist/tools.js +306 -0
- package/dist/tools.js.map +1 -0
- package/dist/types.d.ts +69 -0
- package/dist/types.js +4 -0
- package/dist/types.js.map +1 -0
- package/package.json +35 -0
package/README.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# @pennant/mcp
|
|
2
|
+
|
|
3
|
+
A [Model Context Protocol](https://modelcontextprotocol.io) server for [Pennant](https://flypennant.com). It lets an AI assistant read your flags, explain why a flag is on or off for a user, change flags, and add a Pennant SDK to a codebase.
|
|
4
|
+
|
|
5
|
+
The server signs in to your console as a Pennant user, so that user's role, project access, and production approvals apply to everything the assistant does.
|
|
6
|
+
|
|
7
|
+
## Set up
|
|
8
|
+
|
|
9
|
+
Create a Pennant user for the assistant. Give it the **viewer** role to start, or **editor** if it should change flags. Then add the server to your client.
|
|
10
|
+
|
|
11
|
+
### Claude Code
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
claude mcp add pennant \
|
|
15
|
+
-e PENNANT_URL=https://flags.example.com \
|
|
16
|
+
-e PENNANT_EMAIL=assistant@example.com \
|
|
17
|
+
-e PENNANT_PASSWORD=... \
|
|
18
|
+
-- npx -y @pennant/mcp
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### Claude Desktop, Cursor, VS Code, and other clients
|
|
22
|
+
|
|
23
|
+
```json
|
|
24
|
+
{
|
|
25
|
+
"mcpServers": {
|
|
26
|
+
"pennant": {
|
|
27
|
+
"command": "npx",
|
|
28
|
+
"args": ["-y", "@pennant/mcp"],
|
|
29
|
+
"env": {
|
|
30
|
+
"PENNANT_URL": "https://flags.example.com",
|
|
31
|
+
"PENNANT_EMAIL": "assistant@example.com",
|
|
32
|
+
"PENNANT_PASSWORD": "..."
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Configuration
|
|
40
|
+
|
|
41
|
+
| Variable | Required | Meaning |
|
|
42
|
+
| -------------------- | -------- | --------------------------------------------------------------------------------------- |
|
|
43
|
+
| `PENNANT_URL` | yes | Public URL of your Pennant console. |
|
|
44
|
+
| `PENNANT_EMAIL` | yes | Pennant user the server signs in as. |
|
|
45
|
+
| `PENNANT_PASSWORD` | yes | That user's password. Single sign-on accounts need a local password for the server. |
|
|
46
|
+
| `PENNANT_PROJECT` | no | Project used when a tool does not name one. Defaults to `default`. |
|
|
47
|
+
| `PENNANT_CLIENT_KEY` | no | Project client key. Lets `explain_flag` confirm results, including percentage rollouts. |
|
|
48
|
+
| `PENNANT_READ_ONLY` | no | Set to `1` to leave out every tool that changes flags. |
|
|
49
|
+
|
|
50
|
+
## Tools
|
|
51
|
+
|
|
52
|
+
| Tool | What it does |
|
|
53
|
+
| ------------------- | ------------------------------------------------------------------------------------------ |
|
|
54
|
+
| `list_projects` | Projects the user can open. |
|
|
55
|
+
| `list_environments` | A project's environments. |
|
|
56
|
+
| `list_flags` | Flags with type, tags, parent, and on/off per environment. Filter by tag. |
|
|
57
|
+
| `get_flag` | One flag's full configuration. |
|
|
58
|
+
| `list_segments` | Saved audiences and their constraints. |
|
|
59
|
+
| `explain_flag` | Why a flag is on or off for a user, step by step, in the order the server checks. |
|
|
60
|
+
| `detect_stack` | Reads `package.json`, `pyproject.toml`, `go.mod`, or `Cargo.toml` and picks the right SDK. |
|
|
61
|
+
| `sdk_setup` | Install command, environment variables, and a first flag check, pointed at your console. |
|
|
62
|
+
| `create_flag` | Creates a flag. It starts off in every environment. |
|
|
63
|
+
| `set_flag_enabled` | Switches a flag on or off in one environment. |
|
|
64
|
+
| `update_targeting` | Changes strategy, constraints, variants, or segments in one environment. |
|
|
65
|
+
| `archive_flag` | Archives or restores a flag. |
|
|
66
|
+
|
|
67
|
+
When a project requires approval for production, `set_flag_enabled` and `update_targeting` open a change request instead of applying the change. An admin approves it in the console.
|
|
68
|
+
|
|
69
|
+
## Try it
|
|
70
|
+
|
|
71
|
+
- "Add Pennant to this app and put the new search page behind a flag called `new-search`."
|
|
72
|
+
- "Why is `checkout-v2` off for user `u-123` in production?"
|
|
73
|
+
- "Roll `new-search` out to 10% of users in development."
|
|
74
|
+
- "Which flags tagged `checkout` are still off in production?"
|
|
75
|
+
|
|
76
|
+
## Skills
|
|
77
|
+
|
|
78
|
+
The [`skills/`](../skills) folder has agent skills that pair with this server: adding the SDK, wrapping a feature in a flag, and cleaning up a flag after rollout.
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
type McpConfig = {
|
|
2
|
+
/** Public URL of the Pennant console, such as https://flags.example.com. */
|
|
3
|
+
url: string;
|
|
4
|
+
email: string;
|
|
5
|
+
password: string;
|
|
6
|
+
/** Project used when a tool call does not name one. */
|
|
7
|
+
project: string;
|
|
8
|
+
/** Optional project client key. Lets explain_flag confirm results against the live evaluate API. */
|
|
9
|
+
clientKey?: string;
|
|
10
|
+
/** When true, tools that change flags are not registered. */
|
|
11
|
+
readOnly: boolean;
|
|
12
|
+
};
|
|
13
|
+
type Env = Record<string, string | undefined>;
|
|
14
|
+
declare function readConfig(env?: Env): McpConfig | string;
|
|
15
|
+
export { readConfig };
|
|
16
|
+
export type { McpConfig };
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
function readConfig(env = process.env) {
|
|
2
|
+
const url = env.PENNANT_URL?.trim();
|
|
3
|
+
const email = env.PENNANT_EMAIL?.trim();
|
|
4
|
+
const password = env.PENNANT_PASSWORD;
|
|
5
|
+
const missing = [
|
|
6
|
+
url ? null : "PENNANT_URL",
|
|
7
|
+
email ? null : "PENNANT_EMAIL",
|
|
8
|
+
password ? null : "PENNANT_PASSWORD",
|
|
9
|
+
].filter(Boolean);
|
|
10
|
+
if (missing.length > 0)
|
|
11
|
+
return `Set ${missing.join(", ")} for the Pennant MCP server.`;
|
|
12
|
+
let parsed;
|
|
13
|
+
try {
|
|
14
|
+
parsed = new URL(url);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return "PENNANT_URL must be a full URL, such as https://flags.example.com.";
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
url: parsed.origin,
|
|
21
|
+
email: email,
|
|
22
|
+
password: password,
|
|
23
|
+
project: env.PENNANT_PROJECT?.trim() || "default",
|
|
24
|
+
clientKey: env.PENNANT_CLIENT_KEY?.trim() || undefined,
|
|
25
|
+
readOnly: env.PENNANT_READ_ONLY === "1" || env.PENNANT_READ_ONLY === "true",
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export { readConfig };
|
|
29
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAeA,SAAS,UAAU,CAAC,MAAW,OAAO,CAAC,GAAG;IACxC,MAAM,GAAG,GAAG,GAAG,CAAC,WAAW,EAAE,IAAI,EAAE,CAAA;IACnC,MAAM,KAAK,GAAG,GAAG,CAAC,aAAa,EAAE,IAAI,EAAE,CAAA;IACvC,MAAM,QAAQ,GAAG,GAAG,CAAC,gBAAgB,CAAA;IACrC,MAAM,OAAO,GAAG;QACd,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,aAAa;QAC1B,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,eAAe;QAC9B,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,kBAAkB;KACrC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;IACjB,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,OAAO,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,8BAA8B,CAAA;IAEtF,IAAI,MAAW,CAAA;IACf,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,GAAG,CAAC,GAAI,CAAC,CAAA;IACxB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,oEAAoE,CAAA;IAC7E,CAAC;IAED,OAAO;QACL,GAAG,EAAE,MAAM,CAAC,MAAM;QAClB,KAAK,EAAE,KAAM;QACb,QAAQ,EAAE,QAAS;QACnB,OAAO,EAAE,GAAG,CAAC,eAAe,EAAE,IAAI,EAAE,IAAI,SAAS;QACjD,SAAS,EAAE,GAAG,CAAC,kBAAkB,EAAE,IAAI,EAAE,IAAI,SAAS;QACtD,QAAQ,EAAE,GAAG,CAAC,iBAAiB,KAAK,GAAG,IAAI,GAAG,CAAC,iBAAiB,KAAK,MAAM;KAC5E,CAAA;AACH,CAAC;AAED,OAAO,EAAE,UAAU,EAAE,CAAA"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { EnvironmentConfig, EvaluationContext, Flag, FlagResult, FlagType, ProjectSummary, Segment, Strategy } from "./types.ts";
|
|
2
|
+
type ClientOptions = {
|
|
3
|
+
url: string;
|
|
4
|
+
email: string;
|
|
5
|
+
password: string;
|
|
6
|
+
clientKey?: string;
|
|
7
|
+
fetch?: typeof fetch;
|
|
8
|
+
};
|
|
9
|
+
type ChangeRequest = {
|
|
10
|
+
id: string;
|
|
11
|
+
flagKey: string;
|
|
12
|
+
environment: string;
|
|
13
|
+
status: string;
|
|
14
|
+
};
|
|
15
|
+
type ToggleResult = {
|
|
16
|
+
mode: "direct";
|
|
17
|
+
flag: Flag;
|
|
18
|
+
} | {
|
|
19
|
+
mode: "change-request";
|
|
20
|
+
changeRequest: ChangeRequest;
|
|
21
|
+
flag: Flag;
|
|
22
|
+
};
|
|
23
|
+
declare class ConsoleError extends Error {
|
|
24
|
+
readonly status: number;
|
|
25
|
+
constructor(status: number, message: string);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Talks to the Pennant console as a signed-in user, so roles, project membership,
|
|
29
|
+
* and production approvals apply exactly as they do in the browser.
|
|
30
|
+
*/
|
|
31
|
+
declare function createConsoleClient(options: ClientOptions): {
|
|
32
|
+
hasClientKey: boolean;
|
|
33
|
+
evaluate: (environment: string, context: EvaluationContext, project?: string) => Promise<Record<string, FlagResult> | null>;
|
|
34
|
+
listProjects(): Promise<ProjectSummary[]>;
|
|
35
|
+
listEnvironments(project: string): Promise<string[]>;
|
|
36
|
+
listFlags(project: string, includeArchived?: boolean): Promise<Flag[]>;
|
|
37
|
+
getFlag(project: string, key: string): Promise<Flag>;
|
|
38
|
+
listSegments(project: string): Promise<Segment[]>;
|
|
39
|
+
createFlag(project: string, input: {
|
|
40
|
+
key: string;
|
|
41
|
+
name: string;
|
|
42
|
+
description?: string;
|
|
43
|
+
type?: FlagType;
|
|
44
|
+
strategy?: Strategy;
|
|
45
|
+
}): Promise<Flag>;
|
|
46
|
+
updateFlag(project: string, key: string, patch: Record<string, unknown>): Promise<Flag>;
|
|
47
|
+
setEnabled(project: string, key: string, environment: string, enabled: boolean): Promise<ToggleResult>;
|
|
48
|
+
approvalRequired(project: string): Promise<boolean>;
|
|
49
|
+
proposeProductionChange(project: string, flagKey: string, proposed: EnvironmentConfig): Promise<{
|
|
50
|
+
changeRequest: ChangeRequest;
|
|
51
|
+
}>;
|
|
52
|
+
};
|
|
53
|
+
type ConsoleClient = ReturnType<typeof createConsoleClient>;
|
|
54
|
+
export { ConsoleError, createConsoleClient };
|
|
55
|
+
export type { ChangeRequest, ConsoleClient, ToggleResult };
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
class ConsoleError extends Error {
|
|
2
|
+
status;
|
|
3
|
+
constructor(status, message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.status = status;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Talks to the Pennant console as a signed-in user, so roles, project membership,
|
|
10
|
+
* and production approvals apply exactly as they do in the browser.
|
|
11
|
+
*/
|
|
12
|
+
function createConsoleClient(options) {
|
|
13
|
+
const fetchImpl = options.fetch ?? fetch;
|
|
14
|
+
let cookie = null;
|
|
15
|
+
async function signIn() {
|
|
16
|
+
const response = await fetchImpl(`${options.url}/api/auth/sign-in`, {
|
|
17
|
+
method: "POST",
|
|
18
|
+
headers: { "Content-Type": "application/json", Origin: options.url },
|
|
19
|
+
body: JSON.stringify({ email: options.email, password: options.password }),
|
|
20
|
+
});
|
|
21
|
+
if (!response.ok) {
|
|
22
|
+
throw new ConsoleError(response.status, await errorText(response, "Sign-in failed."));
|
|
23
|
+
}
|
|
24
|
+
const setCookie = response.headers.get("set-cookie");
|
|
25
|
+
const session = setCookie?.split(";")[0];
|
|
26
|
+
if (!session)
|
|
27
|
+
throw new ConsoleError(500, "Sign-in did not return a session.");
|
|
28
|
+
cookie = session;
|
|
29
|
+
}
|
|
30
|
+
async function request(method, path, project, body) {
|
|
31
|
+
const url = new URL(`${options.url}${path}`);
|
|
32
|
+
if (project)
|
|
33
|
+
url.searchParams.set("project", project);
|
|
34
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
35
|
+
if (!cookie)
|
|
36
|
+
await signIn();
|
|
37
|
+
const response = await fetchImpl(url, {
|
|
38
|
+
method,
|
|
39
|
+
headers: {
|
|
40
|
+
Cookie: cookie,
|
|
41
|
+
Origin: options.url,
|
|
42
|
+
...(body === undefined ? {} : { "Content-Type": "application/json" }),
|
|
43
|
+
},
|
|
44
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
45
|
+
});
|
|
46
|
+
// An expired session gets one fresh sign-in.
|
|
47
|
+
if (response.status === 401 && attempt === 0) {
|
|
48
|
+
cookie = null;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (!response.ok) {
|
|
52
|
+
throw new ConsoleError(response.status, await errorText(response, `${method} ${path} failed.`));
|
|
53
|
+
}
|
|
54
|
+
return (await response.json());
|
|
55
|
+
}
|
|
56
|
+
throw new ConsoleError(401, "Sign-in required.");
|
|
57
|
+
}
|
|
58
|
+
async function evaluate(environment, context, project) {
|
|
59
|
+
if (!options.clientKey)
|
|
60
|
+
return null;
|
|
61
|
+
const response = await fetchImpl(`${options.url}/api/client/evaluate`, {
|
|
62
|
+
method: "POST",
|
|
63
|
+
headers: { Authorization: `Bearer ${options.clientKey}`, "Content-Type": "application/json" },
|
|
64
|
+
body: JSON.stringify({ environment, context, ...(project ? { project } : {}) }),
|
|
65
|
+
});
|
|
66
|
+
if (!response.ok) {
|
|
67
|
+
throw new ConsoleError(response.status, await errorText(response, "Evaluate failed."));
|
|
68
|
+
}
|
|
69
|
+
const body = (await response.json());
|
|
70
|
+
return body.flags ?? {};
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
hasClientKey: Boolean(options.clientKey),
|
|
74
|
+
evaluate,
|
|
75
|
+
async listProjects() {
|
|
76
|
+
return (await request("GET", "/api/admin/projects")).projects;
|
|
77
|
+
},
|
|
78
|
+
async listEnvironments(project) {
|
|
79
|
+
return (await request("GET", "/api/admin/environments", project))
|
|
80
|
+
.environments;
|
|
81
|
+
},
|
|
82
|
+
async listFlags(project, includeArchived = false) {
|
|
83
|
+
const path = includeArchived ? "/api/admin/flags?archived=1" : "/api/admin/flags";
|
|
84
|
+
return (await request("GET", path, project)).flags;
|
|
85
|
+
},
|
|
86
|
+
async getFlag(project, key) {
|
|
87
|
+
return (await request("GET", `/api/admin/flags/${encodeURIComponent(key)}`, project)).flag;
|
|
88
|
+
},
|
|
89
|
+
async listSegments(project) {
|
|
90
|
+
return (await request("GET", "/api/admin/segments", project))
|
|
91
|
+
.segments;
|
|
92
|
+
},
|
|
93
|
+
async createFlag(project, input) {
|
|
94
|
+
return (await request("POST", "/api/admin/flags", project, input)).flag;
|
|
95
|
+
},
|
|
96
|
+
async updateFlag(project, key, patch) {
|
|
97
|
+
return (await request("PATCH", `/api/admin/flags/${encodeURIComponent(key)}`, project, patch)).flag;
|
|
98
|
+
},
|
|
99
|
+
async setEnabled(project, key, environment, enabled) {
|
|
100
|
+
return request("POST", `/api/admin/flags/${encodeURIComponent(key)}/toggle`, project, {
|
|
101
|
+
environment,
|
|
102
|
+
enabled,
|
|
103
|
+
});
|
|
104
|
+
},
|
|
105
|
+
async approvalRequired(project) {
|
|
106
|
+
const body = await request("GET", "/api/admin/change-requests", project);
|
|
107
|
+
return body.requireApprovalForProduction;
|
|
108
|
+
},
|
|
109
|
+
async proposeProductionChange(project, flagKey, proposed) {
|
|
110
|
+
return request("POST", "/api/admin/change-requests", project, {
|
|
111
|
+
flagKey,
|
|
112
|
+
environment: "production",
|
|
113
|
+
proposed,
|
|
114
|
+
});
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
async function errorText(response, fallback) {
|
|
119
|
+
try {
|
|
120
|
+
const body = (await response.json());
|
|
121
|
+
return typeof body.error === "string" ? body.error : fallback;
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return fallback;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
export { ConsoleError, createConsoleClient };
|
|
128
|
+
//# sourceMappingURL=console-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"console-client.js","sourceRoot":"","sources":["../src/console-client.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { EvaluationContext, Flag, FlagResult, Segment, Strategy } from "./types.ts";
|
|
2
|
+
type Explanation = {
|
|
3
|
+
/** null when only the server knows, which is a gradual rollout without a live result. */
|
|
4
|
+
enabled: boolean | null;
|
|
5
|
+
reason: string;
|
|
6
|
+
/** Every rule that was checked, in the order the server checks them. */
|
|
7
|
+
steps: string[];
|
|
8
|
+
};
|
|
9
|
+
declare function describeStrategy(strategy: Strategy): string;
|
|
10
|
+
/**
|
|
11
|
+
* Explains a flag for one context, in the same order the server evaluates:
|
|
12
|
+
* archive, environment switch, constraints, segments, parent flag, strategy.
|
|
13
|
+
* `live` holds evaluate results from the server, when a client key is configured.
|
|
14
|
+
*/
|
|
15
|
+
declare function explainFlag(flag: Flag, environment: string, context: EvaluationContext, lookups: {
|
|
16
|
+
flags: Map<string, Flag>;
|
|
17
|
+
segments: Map<string, Segment>;
|
|
18
|
+
live?: Record<string, FlagResult>;
|
|
19
|
+
}, visiting?: Set<string>): Explanation;
|
|
20
|
+
export { describeStrategy, explainFlag };
|
|
21
|
+
export type { Explanation };
|
package/dist/explain.js
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
function contextValue(context, name) {
|
|
2
|
+
if (name === "userId")
|
|
3
|
+
return context.userId;
|
|
4
|
+
if (name === "sessionId")
|
|
5
|
+
return context.sessionId;
|
|
6
|
+
if (name === "remoteAddress")
|
|
7
|
+
return context.remoteAddress;
|
|
8
|
+
if (name === "hostname")
|
|
9
|
+
return context.hostname;
|
|
10
|
+
return context.properties?.[name];
|
|
11
|
+
}
|
|
12
|
+
function constraintPasses(constraint, context) {
|
|
13
|
+
const value = contextValue(context, constraint.contextName);
|
|
14
|
+
if (constraint.operator === "IN")
|
|
15
|
+
return value !== undefined && constraint.values.includes(value);
|
|
16
|
+
return value === undefined || !constraint.values.includes(value);
|
|
17
|
+
}
|
|
18
|
+
function describeConstraint(constraint) {
|
|
19
|
+
const verb = constraint.operator === "IN" ? "is one of" : "is not one of";
|
|
20
|
+
return `${constraint.contextName} ${verb} ${constraint.values.join(", ")}`;
|
|
21
|
+
}
|
|
22
|
+
function describeStrategy(strategy) {
|
|
23
|
+
switch (strategy.type) {
|
|
24
|
+
case "everyone":
|
|
25
|
+
return "everyone";
|
|
26
|
+
case "gradual":
|
|
27
|
+
return `a ${strategy.percentage}% gradual rollout`;
|
|
28
|
+
case "allowlist":
|
|
29
|
+
return `an allowlist of ${strategy.userIds.length} user id${strategy.userIds.length === 1 ? "" : "s"}`;
|
|
30
|
+
case "remoteAddress":
|
|
31
|
+
return `an IP allowlist of ${strategy.addresses.length} address${strategy.addresses.length === 1 ? "" : "es"}`;
|
|
32
|
+
case "hostname":
|
|
33
|
+
return `a hostname list (${strategy.hostnames.join(", ")})`;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** Decides the strategy where it is knowable without the server's rollout bucket. */
|
|
37
|
+
function strategyResult(strategy, context, live) {
|
|
38
|
+
switch (strategy.type) {
|
|
39
|
+
case "everyone":
|
|
40
|
+
return { on: true, why: "The strategy is everyone." };
|
|
41
|
+
case "gradual": {
|
|
42
|
+
if (!context.userId)
|
|
43
|
+
return { on: false, why: "Gradual rollouts need a userId, and the context has none." };
|
|
44
|
+
if (strategy.percentage <= 0)
|
|
45
|
+
return { on: false, why: "The rollout is at 0%." };
|
|
46
|
+
if (strategy.percentage >= 100)
|
|
47
|
+
return { on: true, why: "The rollout is at 100%." };
|
|
48
|
+
if (live) {
|
|
49
|
+
return {
|
|
50
|
+
on: live.enabled,
|
|
51
|
+
why: live.enabled
|
|
52
|
+
? `${context.userId} falls inside the ${strategy.percentage}% rollout.`
|
|
53
|
+
: `${context.userId} falls outside the ${strategy.percentage}% rollout.`,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
on: null,
|
|
58
|
+
why: `Whether ${context.userId} is inside the ${strategy.percentage}% rollout is decided by the server. Set PENNANT_CLIENT_KEY to check it live.`,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
case "allowlist": {
|
|
62
|
+
const listed = Boolean(context.userId && strategy.userIds.includes(context.userId));
|
|
63
|
+
return {
|
|
64
|
+
on: listed,
|
|
65
|
+
why: listed
|
|
66
|
+
? `${context.userId} is on the allowlist.`
|
|
67
|
+
: "The userId is not on the allowlist.",
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
case "remoteAddress": {
|
|
71
|
+
const listed = Boolean(context.remoteAddress && strategy.addresses.includes(context.remoteAddress));
|
|
72
|
+
return {
|
|
73
|
+
on: listed,
|
|
74
|
+
why: listed
|
|
75
|
+
? "The remoteAddress is allowed."
|
|
76
|
+
: "The remoteAddress is not in the allowed list.",
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
case "hostname": {
|
|
80
|
+
const listed = Boolean(context.hostname && strategy.hostnames.includes(context.hostname));
|
|
81
|
+
return {
|
|
82
|
+
on: listed,
|
|
83
|
+
why: listed ? "The hostname is allowed." : "The hostname is not in the allowed list.",
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Explains a flag for one context, in the same order the server evaluates:
|
|
90
|
+
* archive, environment switch, constraints, segments, parent flag, strategy.
|
|
91
|
+
* `live` holds evaluate results from the server, when a client key is configured.
|
|
92
|
+
*/
|
|
93
|
+
function explainFlag(flag, environment, context, lookups, visiting = new Set()) {
|
|
94
|
+
const steps = [];
|
|
95
|
+
let parentUncertain = false;
|
|
96
|
+
const off = (reason) => ({
|
|
97
|
+
enabled: false,
|
|
98
|
+
reason,
|
|
99
|
+
steps: [...steps, reason],
|
|
100
|
+
});
|
|
101
|
+
if (flag.archived)
|
|
102
|
+
return off(`${flag.key} is archived, so it is never sent to apps.`);
|
|
103
|
+
const config = flag.environments[environment];
|
|
104
|
+
if (!config)
|
|
105
|
+
return off(`${flag.key} has no settings for the ${environment} environment.`);
|
|
106
|
+
if (!config.enabled)
|
|
107
|
+
return off(`${flag.key} is switched off in ${environment}.`);
|
|
108
|
+
steps.push(`${flag.key} is switched on in ${environment}.`);
|
|
109
|
+
for (const constraint of config.constraints ?? []) {
|
|
110
|
+
if (!constraintPasses(constraint, context)) {
|
|
111
|
+
return off(`The constraint "${describeConstraint(constraint)}" does not match this context.`);
|
|
112
|
+
}
|
|
113
|
+
steps.push(`Constraint "${describeConstraint(constraint)}" matches.`);
|
|
114
|
+
}
|
|
115
|
+
for (const id of config.segmentIds ?? []) {
|
|
116
|
+
const segment = lookups.segments.get(id);
|
|
117
|
+
if (!segment)
|
|
118
|
+
return off(`Segment ${id} no longer exists, so the flag stays off.`);
|
|
119
|
+
const failed = segment.constraints.find((constraint) => !constraintPasses(constraint, context));
|
|
120
|
+
if (failed)
|
|
121
|
+
return off(`The context is not in segment "${segment.name}": ${describeConstraint(failed)} does not match.`);
|
|
122
|
+
steps.push(`The context is in segment "${segment.name}".`);
|
|
123
|
+
}
|
|
124
|
+
if (flag.parentKey) {
|
|
125
|
+
if (visiting.has(flag.key))
|
|
126
|
+
return off("The parent chain loops back on itself.");
|
|
127
|
+
const parent = lookups.flags.get(flag.parentKey);
|
|
128
|
+
if (!parent)
|
|
129
|
+
return off(`Parent flag ${flag.parentKey} does not exist.`);
|
|
130
|
+
visiting.add(flag.key);
|
|
131
|
+
const parentResult = explainFlag(parent, environment, context, lookups, visiting);
|
|
132
|
+
visiting.delete(flag.key);
|
|
133
|
+
if (parentResult.enabled === false) {
|
|
134
|
+
return off(`Parent flag ${flag.parentKey} is off: ${parentResult.reason}`);
|
|
135
|
+
}
|
|
136
|
+
if (parentResult.enabled === null) {
|
|
137
|
+
parentUncertain = true;
|
|
138
|
+
steps.push(`Parent flag ${flag.parentKey} depends on its rollout: ${parentResult.reason}`);
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
steps.push(`Parent flag ${flag.parentKey} is on.`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const strategy = strategyResult(config.strategy, context, lookups.live?.[flag.key]);
|
|
145
|
+
steps.push(`The strategy is ${describeStrategy(config.strategy)}. ${strategy.why}`);
|
|
146
|
+
if (strategy.on === false)
|
|
147
|
+
return { enabled: false, reason: strategy.why, steps };
|
|
148
|
+
if (strategy.on === null)
|
|
149
|
+
return { enabled: null, reason: strategy.why, steps };
|
|
150
|
+
const live = lookups.live?.[flag.key];
|
|
151
|
+
if (parentUncertain) {
|
|
152
|
+
if (!live) {
|
|
153
|
+
const reason = `${flag.key} passes its own rules, but its parent depends on a rollout the server decides.`;
|
|
154
|
+
return { enabled: null, reason, steps: [...steps, reason] };
|
|
155
|
+
}
|
|
156
|
+
if (!live.enabled) {
|
|
157
|
+
const reason = `${flag.key} passes its own rules but is off, because its parent's rollout excludes this context.`;
|
|
158
|
+
return { enabled: false, reason, steps: [...steps, reason] };
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
const variant = live?.variant;
|
|
162
|
+
const reason = variant
|
|
163
|
+
? `${flag.key} is on in ${environment} and returns variant "${variant}".`
|
|
164
|
+
: `${flag.key} is on in ${environment}.`;
|
|
165
|
+
return { enabled: true, reason, steps: [...steps, reason] };
|
|
166
|
+
}
|
|
167
|
+
export { describeStrategy, explainFlag };
|
|
168
|
+
//# sourceMappingURL=explain.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"explain.js","sourceRoot":"","sources":["../src/explain.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { readConfig } from "./config.js";
|
|
5
|
+
import { createConsoleClient } from "./console-client.js";
|
|
6
|
+
import { registerTools } from "./tools.js";
|
|
7
|
+
const INSTRUCTIONS = `Pennant is a self-hosted feature-flag console. Use these tools to read flags, explain why a flag is on or off for a user, change flags, and add a Pennant SDK to a codebase.
|
|
8
|
+
|
|
9
|
+
- To add Pennant to a codebase: call detect_stack, then sdk_setup, then edit the code so the new behaviour runs only when the flag is on.
|
|
10
|
+
- New flags start off everywhere. Prefer turning a flag on in development first.
|
|
11
|
+
- Production changes may need approval. When a tool says a change request was opened, tell the user an admin must approve it in the console.
|
|
12
|
+
- Never put a client key in source code. Use the environment variables sdk_setup lists.`;
|
|
13
|
+
async function main() {
|
|
14
|
+
const config = readConfig();
|
|
15
|
+
if (typeof config === "string") {
|
|
16
|
+
console.error(config);
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
const server = new McpServer({ name: "pennant", version: "1.0.0" }, { instructions: INSTRUCTIONS });
|
|
20
|
+
registerTools(server, createConsoleClient(config), config);
|
|
21
|
+
await server.connect(new StdioServerTransport());
|
|
22
|
+
}
|
|
23
|
+
main().catch((error) => {
|
|
24
|
+
console.error(error);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
});
|
|
27
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AACnE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAA;AAEhF,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AACxC,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAA;AACzD,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAE1C,MAAM,YAAY,GAAG;;;;;wFAKmE,CAAA;AAExF,KAAK,UAAU,IAAI;IACjB,MAAM,MAAM,GAAG,UAAU,EAAE,CAAA;IAC3B,IAAI,OAAO,MAAM,KAAK,QAAQ,EAAE,CAAC;QAC/B,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,CAAA;QACrB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IACjB,CAAC;IAED,MAAM,MAAM,GAAG,IAAI,SAAS,CAC1B,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,OAAO,EAAE,EACrC,EAAE,YAAY,EAAE,YAAY,EAAE,CAC/B,CAAA;IACD,aAAa,CAAC,MAAM,EAAE,mBAAmB,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,CAAA;IAC1D,MAAM,MAAM,CAAC,OAAO,CAAC,IAAI,oBAAoB,EAAE,CAAC,CAAA;AAClD,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE;IACrB,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;IACpB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;AACjB,CAAC,CAAC,CAAA"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
declare const SDKS: readonly ["react", "vue", "js", "node", "python", "go", "rust"];
|
|
2
|
+
type SdkId = (typeof SDKS)[number];
|
|
3
|
+
type Detection = {
|
|
4
|
+
sdk: SdkId;
|
|
5
|
+
reason: string;
|
|
6
|
+
framework?: "next" | "vite" | "nuxt";
|
|
7
|
+
};
|
|
8
|
+
type SetupGuide = {
|
|
9
|
+
sdk: SdkId;
|
|
10
|
+
install: string;
|
|
11
|
+
env: Record<string, string>;
|
|
12
|
+
code: string;
|
|
13
|
+
notes: string[];
|
|
14
|
+
};
|
|
15
|
+
declare const MANIFESTS: readonly ["package.json", "pyproject.toml", "requirements.txt", "go.mod", "Cargo.toml"];
|
|
16
|
+
/** Picks SDKs from manifest file contents, keyed by file name. */
|
|
17
|
+
declare function detectStack(files: Map<string, string>): Detection[];
|
|
18
|
+
/** Reads the manifest files in one directory. Missing files are skipped. */
|
|
19
|
+
declare function readManifests(dir: string): Promise<Map<string, string>>;
|
|
20
|
+
/** Install line, environment variables, and a first flag check for one SDK. */
|
|
21
|
+
declare function setupGuide(sdk: SdkId, options: {
|
|
22
|
+
apiUrl: string;
|
|
23
|
+
flagKey?: string;
|
|
24
|
+
framework?: Detection["framework"];
|
|
25
|
+
}): SetupGuide;
|
|
26
|
+
export { MANIFESTS, SDKS, detectStack, readManifests, setupGuide };
|
|
27
|
+
export type { Detection, SdkId, SetupGuide };
|