gainsight-px-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 +180 -0
- package/dist/__tests__/config.test.d.ts +1 -0
- package/dist/__tests__/config.test.js +30 -0
- package/dist/__tests__/http-client.test.d.ts +1 -0
- package/dist/__tests__/http-client.test.js +67 -0
- package/dist/__tests__/server.test.d.ts +1 -0
- package/dist/__tests__/server.test.js +57 -0
- package/dist/config.d.ts +5 -0
- package/dist/config.js +9 -0
- package/dist/http-client.d.ts +14 -0
- package/dist/http-client.js +58 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +29 -0
- package/dist/tools/accounts.d.ts +2 -0
- package/dist/tools/accounts.js +38 -0
- package/dist/tools/admin.d.ts +2 -0
- package/dist/tools/admin.js +14 -0
- package/dist/tools/engagements.d.ts +2 -0
- package/dist/tools/engagements.js +38 -0
- package/dist/tools/events.d.ts +2 -0
- package/dist/tools/events.js +49 -0
- package/dist/tools/features.d.ts +2 -0
- package/dist/tools/features.js +31 -0
- package/dist/tools/knowledge-center.d.ts +2 -0
- package/dist/tools/knowledge-center.js +18 -0
- package/dist/tools/segments.d.ts +2 -0
- package/dist/tools/segments.js +17 -0
- package/dist/tools/survey.d.ts +2 -0
- package/dist/tools/survey.js +15 -0
- package/dist/tools/users.d.ts +2 -0
- package/dist/tools/users.js +38 -0
- package/package.json +40 -0
package/README.md
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# Gainsight PX MCP Server
|
|
2
|
+
|
|
3
|
+
An [MCP (Model Context Protocol)](https://modelcontextprotocol.io/) server that connects AI tools like Claude, Cursor, and Windsurf to the [Gainsight PX REST API](https://support.gainsight.com/PX/API_for_Developers/Use_the_PX_REST_API).
|
|
4
|
+
|
|
5
|
+
Query and manage your PX data — users, accounts, engagements, features, segments, events, and more — directly from your AI assistant.
|
|
6
|
+
|
|
7
|
+
## Prerequisites
|
|
8
|
+
|
|
9
|
+
- Node.js 18 or later
|
|
10
|
+
- A Gainsight PX API key ([how to generate one](https://support.gainsight.com/PX/API_for_Developers/Use_the_PX_REST_API#Generate_an_API_Key))
|
|
11
|
+
|
|
12
|
+
## Quick Start
|
|
13
|
+
|
|
14
|
+
### Claude Desktop
|
|
15
|
+
|
|
16
|
+
Add to your `claude_desktop_config.json`:
|
|
17
|
+
|
|
18
|
+
```json
|
|
19
|
+
{
|
|
20
|
+
"mcpServers": {
|
|
21
|
+
"gainsight-px": {
|
|
22
|
+
"command": "npx",
|
|
23
|
+
"args": ["-y", "@gainsight/px-mcp-server"],
|
|
24
|
+
"env": {
|
|
25
|
+
"PX_API_KEY": "your-api-key-here"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Claude Code
|
|
33
|
+
|
|
34
|
+
Add to your project's `.mcp.json`:
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
{
|
|
38
|
+
"mcpServers": {
|
|
39
|
+
"gainsight-px": {
|
|
40
|
+
"command": "npx",
|
|
41
|
+
"args": ["-y", "@gainsight/px-mcp-server"],
|
|
42
|
+
"env": {
|
|
43
|
+
"PX_API_KEY": "your-api-key-here"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Cursor
|
|
51
|
+
|
|
52
|
+
Add to your Cursor MCP settings:
|
|
53
|
+
|
|
54
|
+
```json
|
|
55
|
+
{
|
|
56
|
+
"mcpServers": {
|
|
57
|
+
"gainsight-px": {
|
|
58
|
+
"command": "npx",
|
|
59
|
+
"args": ["-y", "@gainsight/px-mcp-server"],
|
|
60
|
+
"env": {
|
|
61
|
+
"PX_API_KEY": "your-api-key-here"
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Configuration
|
|
69
|
+
|
|
70
|
+
| Environment Variable | Required | Default | Description |
|
|
71
|
+
|---------------------|----------|---------|-------------|
|
|
72
|
+
| `PX_API_KEY` | Yes | — | Your Gainsight PX API key |
|
|
73
|
+
| `PX_API_BASE_URL` | No | `https://api.aptrinsic.com` | API base URL for your data center |
|
|
74
|
+
|
|
75
|
+
### Data Centers
|
|
76
|
+
|
|
77
|
+
| Region | Base URL |
|
|
78
|
+
|--------|----------|
|
|
79
|
+
| US | `https://api.aptrinsic.com` (default) |
|
|
80
|
+
| EU | `https://api-eu.aptrinsic.com` |
|
|
81
|
+
| US2 | `https://api-us2.aptrinsic.com` |
|
|
82
|
+
|
|
83
|
+
If your PX instance is in EU or US2, set the `PX_API_BASE_URL` environment variable accordingly.
|
|
84
|
+
|
|
85
|
+
## Available Tools
|
|
86
|
+
|
|
87
|
+
### Users
|
|
88
|
+
| Tool | Description |
|
|
89
|
+
|------|-------------|
|
|
90
|
+
| `px_list_users` | List users with filtering, sorting, and pagination |
|
|
91
|
+
| `px_get_user` | Get a user by identifyId |
|
|
92
|
+
| `px_create_user` | Create a new user |
|
|
93
|
+
| `px_update_user` | Update a user |
|
|
94
|
+
| `px_delete_user` | Delete a user |
|
|
95
|
+
|
|
96
|
+
### Accounts
|
|
97
|
+
| Tool | Description |
|
|
98
|
+
|------|-------------|
|
|
99
|
+
| `px_list_accounts` | List accounts with filtering, sorting, and pagination |
|
|
100
|
+
| `px_get_account` | Get an account by ID |
|
|
101
|
+
| `px_create_account` | Create a new account |
|
|
102
|
+
| `px_update_account` | Update an account |
|
|
103
|
+
| `px_delete_account` | Delete an account |
|
|
104
|
+
|
|
105
|
+
### Engagements
|
|
106
|
+
| Tool | Description |
|
|
107
|
+
|------|-------------|
|
|
108
|
+
| `px_list_engagements` | List engagements with optional content type filter |
|
|
109
|
+
| `px_get_engagement` | Get an engagement by ID |
|
|
110
|
+
| `px_set_engagement_state` | Start or pause an engagement |
|
|
111
|
+
| `px_delete_engagement` | Delete an engagement |
|
|
112
|
+
|
|
113
|
+
### Features
|
|
114
|
+
| Tool | Description |
|
|
115
|
+
|------|-------------|
|
|
116
|
+
| `px_list_features` | List features |
|
|
117
|
+
| `px_get_feature` | Get a feature by ID |
|
|
118
|
+
| `px_get_feature_adoption` | Get feature adoption statistics |
|
|
119
|
+
|
|
120
|
+
### Segments
|
|
121
|
+
| Tool | Description |
|
|
122
|
+
|------|-------------|
|
|
123
|
+
| `px_list_segments` | List segments |
|
|
124
|
+
| `px_get_segment` | Get a segment by ID |
|
|
125
|
+
|
|
126
|
+
### Events
|
|
127
|
+
| Tool | Description |
|
|
128
|
+
|------|-------------|
|
|
129
|
+
| `px_get_events` | Query events by type (pageView, identify, click, email, formSubmit, lead, sessionInitialized, segmentMatch, engagementView, feature_match) |
|
|
130
|
+
| `px_get_custom_events` | Query custom events |
|
|
131
|
+
| `px_create_custom_event` | Create a custom event |
|
|
132
|
+
|
|
133
|
+
### Survey
|
|
134
|
+
| Tool | Description |
|
|
135
|
+
|------|-------------|
|
|
136
|
+
| `px_get_survey_responses` | Query survey responses |
|
|
137
|
+
|
|
138
|
+
### Knowledge Center
|
|
139
|
+
| Tool | Description |
|
|
140
|
+
|------|-------------|
|
|
141
|
+
| `px_list_kc_bots` | List Knowledge Center bots |
|
|
142
|
+
| `px_get_kc_bot` | Get a Knowledge Center bot by ID |
|
|
143
|
+
|
|
144
|
+
### Admin
|
|
145
|
+
| Tool | Description |
|
|
146
|
+
|------|-------------|
|
|
147
|
+
| `px_get_subscription` | Get subscription details |
|
|
148
|
+
| `px_get_model_attributes` | Get user or account model attributes |
|
|
149
|
+
|
|
150
|
+
## Filtering and Pagination
|
|
151
|
+
|
|
152
|
+
### Filter Syntax
|
|
153
|
+
|
|
154
|
+
List tools support filtering with the format `fieldName{operator}fieldValue`:
|
|
155
|
+
|
|
156
|
+
| Operator | Description | Example |
|
|
157
|
+
|----------|-------------|---------|
|
|
158
|
+
| `==` | Equals | `email==john@example.com` |
|
|
159
|
+
| `!=` | Not equals | `status!=INACTIVE` |
|
|
160
|
+
| `<` | Less than | `lastSeenDate<1700000000000` |
|
|
161
|
+
| `<=` | Less than or equal | `score<=50` |
|
|
162
|
+
| `>` | Greater than | `signUpDate>1700000000000` |
|
|
163
|
+
| `>=` | Greater than or equal | `score>=80` |
|
|
164
|
+
| `~` | Contains (like) | `email~@example.com` |
|
|
165
|
+
| `!~` | Does not contain | `name!~test` |
|
|
166
|
+
|
|
167
|
+
Pass multiple filters as an array — they are combined with AND logic.
|
|
168
|
+
|
|
169
|
+
### Pagination
|
|
170
|
+
|
|
171
|
+
- **Users, Accounts, Events**: Scroll-based — use the `scrollId` from the response to fetch the next page.
|
|
172
|
+
- **Engagements, Features, Segments, KC Bots**: Page-number-based — use `pageSize` and `pageNumber` (0-indexed).
|
|
173
|
+
|
|
174
|
+
## Rate Limits
|
|
175
|
+
|
|
176
|
+
The PX API enforces rate limits of approximately 200 requests per second and 1 million requests per day per subscription.
|
|
177
|
+
|
|
178
|
+
## License
|
|
179
|
+
|
|
180
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach } from "vitest";
|
|
2
|
+
describe("config", () => {
|
|
3
|
+
const originalEnv = { ...process.env };
|
|
4
|
+
afterEach(() => {
|
|
5
|
+
process.env = { ...originalEnv };
|
|
6
|
+
// Clear module cache so config re-reads env vars
|
|
7
|
+
});
|
|
8
|
+
it("validateConfig throws when PX_API_KEY is missing", async () => {
|
|
9
|
+
delete process.env.PX_API_KEY;
|
|
10
|
+
// Dynamic import to get fresh module with current env
|
|
11
|
+
const { validateConfig, config } = await import("../config.js?" + Date.now());
|
|
12
|
+
config.apiKey = "";
|
|
13
|
+
expect(() => validateConfig()).toThrow("PX_API_KEY environment variable is required");
|
|
14
|
+
});
|
|
15
|
+
it("validateConfig passes when PX_API_KEY is set", async () => {
|
|
16
|
+
const { validateConfig, config } = await import("../config.js?" + Date.now());
|
|
17
|
+
config.apiKey = "test-key";
|
|
18
|
+
expect(() => validateConfig()).not.toThrow();
|
|
19
|
+
});
|
|
20
|
+
it("defaults baseUrl to US data center", async () => {
|
|
21
|
+
delete process.env.PX_API_BASE_URL;
|
|
22
|
+
const { config } = await import("../config.js?" + Date.now());
|
|
23
|
+
expect(config.baseUrl).toBe("https://api.aptrinsic.com");
|
|
24
|
+
});
|
|
25
|
+
it("strips trailing slash from baseUrl", async () => {
|
|
26
|
+
process.env.PX_API_BASE_URL = "https://api-eu.aptrinsic.com/";
|
|
27
|
+
const { config } = await import("../config.js?" + Date.now());
|
|
28
|
+
expect(config.baseUrl).not.toMatch(/\/$/);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { buildUrl, formatError, jsonResult } from "../http-client.js";
|
|
3
|
+
describe("buildUrl", () => {
|
|
4
|
+
const base = "https://api.aptrinsic.com";
|
|
5
|
+
it("builds a simple path", () => {
|
|
6
|
+
expect(buildUrl(base, "/v1/users")).toBe("https://api.aptrinsic.com/v1/users");
|
|
7
|
+
});
|
|
8
|
+
it("appends scalar query params", () => {
|
|
9
|
+
const url = buildUrl(base, "/v1/users", { pageSize: 25, sort: "name:asc" });
|
|
10
|
+
const parsed = new URL(url);
|
|
11
|
+
expect(parsed.searchParams.get("pageSize")).toBe("25");
|
|
12
|
+
expect(parsed.searchParams.get("sort")).toBe("name:asc");
|
|
13
|
+
});
|
|
14
|
+
it("appends array query params (multiple filter values)", () => {
|
|
15
|
+
const url = buildUrl(base, "/v1/users", {
|
|
16
|
+
filter: ["email==a@b.com", "name~test"],
|
|
17
|
+
});
|
|
18
|
+
const parsed = new URL(url);
|
|
19
|
+
expect(parsed.searchParams.getAll("filter")).toEqual(["email==a@b.com", "name~test"]);
|
|
20
|
+
});
|
|
21
|
+
it("skips undefined params", () => {
|
|
22
|
+
const url = buildUrl(base, "/v1/users", { pageSize: 10, scrollId: undefined });
|
|
23
|
+
const parsed = new URL(url);
|
|
24
|
+
expect(parsed.searchParams.get("pageSize")).toBe("10");
|
|
25
|
+
expect(parsed.searchParams.has("scrollId")).toBe(false);
|
|
26
|
+
});
|
|
27
|
+
it("works with EU data center base URL", () => {
|
|
28
|
+
expect(buildUrl("https://api-eu.aptrinsic.com", "/v1/accounts")).toBe("https://api-eu.aptrinsic.com/v1/accounts");
|
|
29
|
+
});
|
|
30
|
+
it("handles encoded path params", () => {
|
|
31
|
+
const id = encodeURIComponent("user@example.com");
|
|
32
|
+
const url = buildUrl(base, `/v1/users/${id}`);
|
|
33
|
+
expect(url).toContain("user%40example.com");
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
describe("formatError", () => {
|
|
37
|
+
it("returns auth message for 401", () => {
|
|
38
|
+
expect(formatError(401, "Unauthorized")).toContain("Authentication failed");
|
|
39
|
+
});
|
|
40
|
+
it("returns rate limit message for 429", () => {
|
|
41
|
+
expect(formatError(429, "Too many requests")).toContain("Rate limit exceeded");
|
|
42
|
+
});
|
|
43
|
+
it("returns not found message for 404", () => {
|
|
44
|
+
expect(formatError(404, "Not found")).toContain("Resource not found");
|
|
45
|
+
});
|
|
46
|
+
it("returns generic message for other status codes", () => {
|
|
47
|
+
expect(formatError(500, { error: "Internal" })).toContain("API error 500");
|
|
48
|
+
});
|
|
49
|
+
it("stringifies object error data", () => {
|
|
50
|
+
expect(formatError(500, { message: "fail" })).toContain('"message":"fail"');
|
|
51
|
+
});
|
|
52
|
+
it("includes string error data as-is", () => {
|
|
53
|
+
expect(formatError(401, "Bad key")).toContain("Bad key");
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
describe("jsonResult", () => {
|
|
57
|
+
it("wraps data as MCP text content", () => {
|
|
58
|
+
const result = jsonResult({ name: "test" });
|
|
59
|
+
expect(result.content).toHaveLength(1);
|
|
60
|
+
expect(result.content[0].type).toBe("text");
|
|
61
|
+
expect(JSON.parse(result.content[0].text)).toEqual({ name: "test" });
|
|
62
|
+
});
|
|
63
|
+
it("pretty-prints JSON", () => {
|
|
64
|
+
const result = jsonResult({ a: 1 });
|
|
65
|
+
expect(result.content[0].text).toContain("\n");
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { registerUserTools } from "../tools/users.js";
|
|
4
|
+
import { registerAccountTools } from "../tools/accounts.js";
|
|
5
|
+
import { registerEngagementTools } from "../tools/engagements.js";
|
|
6
|
+
import { registerFeatureTools } from "../tools/features.js";
|
|
7
|
+
import { registerSegmentTools } from "../tools/segments.js";
|
|
8
|
+
import { registerEventTools } from "../tools/events.js";
|
|
9
|
+
import { registerSurveyTools } from "../tools/survey.js";
|
|
10
|
+
import { registerKnowledgeCenterTools } from "../tools/knowledge-center.js";
|
|
11
|
+
import { registerAdminTools } from "../tools/admin.js";
|
|
12
|
+
describe("tool registration", () => {
|
|
13
|
+
let server;
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
server = new McpServer({ name: "test", version: "0.0.0" });
|
|
16
|
+
});
|
|
17
|
+
it("registers user tools without error", () => {
|
|
18
|
+
expect(() => registerUserTools(server)).not.toThrow();
|
|
19
|
+
});
|
|
20
|
+
it("registers account tools without error", () => {
|
|
21
|
+
expect(() => registerAccountTools(server)).not.toThrow();
|
|
22
|
+
});
|
|
23
|
+
it("registers engagement tools without error", () => {
|
|
24
|
+
expect(() => registerEngagementTools(server)).not.toThrow();
|
|
25
|
+
});
|
|
26
|
+
it("registers feature tools without error", () => {
|
|
27
|
+
expect(() => registerFeatureTools(server)).not.toThrow();
|
|
28
|
+
});
|
|
29
|
+
it("registers segment tools without error", () => {
|
|
30
|
+
expect(() => registerSegmentTools(server)).not.toThrow();
|
|
31
|
+
});
|
|
32
|
+
it("registers event tools without error", () => {
|
|
33
|
+
expect(() => registerEventTools(server)).not.toThrow();
|
|
34
|
+
});
|
|
35
|
+
it("registers survey tools without error", () => {
|
|
36
|
+
expect(() => registerSurveyTools(server)).not.toThrow();
|
|
37
|
+
});
|
|
38
|
+
it("registers knowledge center tools without error", () => {
|
|
39
|
+
expect(() => registerKnowledgeCenterTools(server)).not.toThrow();
|
|
40
|
+
});
|
|
41
|
+
it("registers admin tools without error", () => {
|
|
42
|
+
expect(() => registerAdminTools(server)).not.toThrow();
|
|
43
|
+
});
|
|
44
|
+
it("registers all 27 tools together without error", () => {
|
|
45
|
+
expect(() => {
|
|
46
|
+
registerUserTools(server);
|
|
47
|
+
registerAccountTools(server);
|
|
48
|
+
registerEngagementTools(server);
|
|
49
|
+
registerFeatureTools(server);
|
|
50
|
+
registerSegmentTools(server);
|
|
51
|
+
registerEventTools(server);
|
|
52
|
+
registerSurveyTools(server);
|
|
53
|
+
registerKnowledgeCenterTools(server);
|
|
54
|
+
registerAdminTools(server);
|
|
55
|
+
}).not.toThrow();
|
|
56
|
+
});
|
|
57
|
+
});
|
package/dist/config.d.ts
ADDED
package/dist/config.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export const config = {
|
|
2
|
+
apiKey: process.env.PX_API_KEY || "",
|
|
3
|
+
baseUrl: (process.env.PX_API_BASE_URL || "https://api-staging.aptrinsic.com").replace(/\/$/, ""),
|
|
4
|
+
};
|
|
5
|
+
export function validateConfig() {
|
|
6
|
+
if (!config.apiKey) {
|
|
7
|
+
throw new Error("PX_API_KEY environment variable is required");
|
|
8
|
+
}
|
|
9
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
interface ApiResponse {
|
|
2
|
+
status: number;
|
|
3
|
+
data: unknown;
|
|
4
|
+
}
|
|
5
|
+
export declare function buildUrl(baseUrl: string, path: string, queryParams?: Record<string, string | string[] | number | undefined>): string;
|
|
6
|
+
export declare function formatError(status: number, data: unknown): string;
|
|
7
|
+
export declare function pxApi(method: string, path: string, queryParams?: Record<string, string | string[] | number | undefined>, body?: unknown): Promise<ApiResponse>;
|
|
8
|
+
export declare function jsonResult(data: unknown): {
|
|
9
|
+
content: Array<{
|
|
10
|
+
type: "text";
|
|
11
|
+
text: string;
|
|
12
|
+
}>;
|
|
13
|
+
};
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { config } from "./config.js";
|
|
2
|
+
export function buildUrl(baseUrl, path, queryParams) {
|
|
3
|
+
const url = new URL(baseUrl + path);
|
|
4
|
+
if (queryParams) {
|
|
5
|
+
for (const [key, value] of Object.entries(queryParams)) {
|
|
6
|
+
if (value === undefined)
|
|
7
|
+
continue;
|
|
8
|
+
if (Array.isArray(value)) {
|
|
9
|
+
for (const v of value) {
|
|
10
|
+
url.searchParams.append(key, v);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
url.searchParams.set(key, String(value));
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return url.toString();
|
|
19
|
+
}
|
|
20
|
+
export function formatError(status, data) {
|
|
21
|
+
const msg = status === 401
|
|
22
|
+
? "Authentication failed — check PX_API_KEY"
|
|
23
|
+
: status === 429
|
|
24
|
+
? "Rate limit exceeded — try again later"
|
|
25
|
+
: status === 404
|
|
26
|
+
? "Resource not found"
|
|
27
|
+
: `API error ${status}`;
|
|
28
|
+
return `${msg}: ${typeof data === "string" ? data : JSON.stringify(data)}`;
|
|
29
|
+
}
|
|
30
|
+
export async function pxApi(method, path, queryParams, body) {
|
|
31
|
+
const url = buildUrl(config.baseUrl, path, queryParams);
|
|
32
|
+
const headers = {
|
|
33
|
+
"X-APTRINSIC-API-KEY": config.apiKey,
|
|
34
|
+
"Content-Type": "application/json",
|
|
35
|
+
};
|
|
36
|
+
const res = await fetch(url, {
|
|
37
|
+
method,
|
|
38
|
+
headers,
|
|
39
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
40
|
+
});
|
|
41
|
+
let data;
|
|
42
|
+
const text = await res.text();
|
|
43
|
+
try {
|
|
44
|
+
data = JSON.parse(text);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
data = text;
|
|
48
|
+
}
|
|
49
|
+
if (!res.ok) {
|
|
50
|
+
throw new Error(formatError(res.status, data));
|
|
51
|
+
}
|
|
52
|
+
return { status: res.status, data };
|
|
53
|
+
}
|
|
54
|
+
export function jsonResult(data) {
|
|
55
|
+
return {
|
|
56
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
57
|
+
};
|
|
58
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
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 { validateConfig } from "./config.js";
|
|
5
|
+
import { registerUserTools } from "./tools/users.js";
|
|
6
|
+
import { registerAccountTools } from "./tools/accounts.js";
|
|
7
|
+
import { registerEngagementTools } from "./tools/engagements.js";
|
|
8
|
+
import { registerFeatureTools } from "./tools/features.js";
|
|
9
|
+
import { registerSegmentTools } from "./tools/segments.js";
|
|
10
|
+
import { registerEventTools } from "./tools/events.js";
|
|
11
|
+
import { registerSurveyTools } from "./tools/survey.js";
|
|
12
|
+
import { registerKnowledgeCenterTools } from "./tools/knowledge-center.js";
|
|
13
|
+
import { registerAdminTools } from "./tools/admin.js";
|
|
14
|
+
validateConfig();
|
|
15
|
+
const server = new McpServer({
|
|
16
|
+
name: "gainsight-px",
|
|
17
|
+
version: "1.0.0",
|
|
18
|
+
});
|
|
19
|
+
registerUserTools(server);
|
|
20
|
+
registerAccountTools(server);
|
|
21
|
+
registerEngagementTools(server);
|
|
22
|
+
registerFeatureTools(server);
|
|
23
|
+
registerSegmentTools(server);
|
|
24
|
+
registerEventTools(server);
|
|
25
|
+
registerSurveyTools(server);
|
|
26
|
+
registerKnowledgeCenterTools(server);
|
|
27
|
+
registerAdminTools(server);
|
|
28
|
+
const transport = new StdioServerTransport();
|
|
29
|
+
await server.connect(transport);
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { pxApi, jsonResult } from "../http-client.js";
|
|
3
|
+
export function registerAccountTools(server) {
|
|
4
|
+
server.tool("px_list_accounts", "List PX accounts with optional filtering, sorting, and scroll-based pagination. Filter syntax: fieldName{op}value.", {
|
|
5
|
+
filter: z.array(z.string()).optional().describe("Filter expressions"),
|
|
6
|
+
sort: z.string().optional().describe("Sort field and direction, e.g. 'name:asc'"),
|
|
7
|
+
pageSize: z.number().optional().describe("Results per page (default 25, max 1000)"),
|
|
8
|
+
scrollId: z.string().optional().describe("Scroll ID from previous response for next page"),
|
|
9
|
+
}, async ({ filter, sort, pageSize, scrollId }) => {
|
|
10
|
+
const res = await pxApi("GET", "/v1/accounts", { filter, sort, pageSize, scrollId });
|
|
11
|
+
return jsonResult(res.data);
|
|
12
|
+
});
|
|
13
|
+
server.tool("px_get_account", "Get a single PX account by ID", {
|
|
14
|
+
accountId: z.string().describe("The account ID"),
|
|
15
|
+
}, async ({ accountId }) => {
|
|
16
|
+
const res = await pxApi("GET", `/v1/accounts/${encodeURIComponent(accountId)}`);
|
|
17
|
+
return jsonResult(res.data);
|
|
18
|
+
});
|
|
19
|
+
server.tool("px_create_account", "Create a new PX account", {
|
|
20
|
+
accountData: z.record(z.string(), z.unknown()).describe("Account object JSON with id, name, propertyKeys, etc."),
|
|
21
|
+
}, async ({ accountData }) => {
|
|
22
|
+
const res = await pxApi("POST", "/v1/accounts", undefined, accountData);
|
|
23
|
+
return jsonResult(res.data);
|
|
24
|
+
});
|
|
25
|
+
server.tool("px_update_account", "Update an existing PX account by ID", {
|
|
26
|
+
accountId: z.string().describe("The account ID"),
|
|
27
|
+
accountData: z.record(z.string(), z.unknown()).describe("Fields to update"),
|
|
28
|
+
}, async ({ accountId, accountData }) => {
|
|
29
|
+
const res = await pxApi("PUT", `/v1/accounts/${encodeURIComponent(accountId)}`, undefined, accountData);
|
|
30
|
+
return jsonResult(res.data);
|
|
31
|
+
});
|
|
32
|
+
server.tool("px_delete_account", "Delete a PX account by ID (cascading delete of associated users)", {
|
|
33
|
+
accountId: z.string().describe("The account ID"),
|
|
34
|
+
}, async ({ accountId }) => {
|
|
35
|
+
const res = await pxApi("DELETE", `/v1/accounts/${encodeURIComponent(accountId)}`);
|
|
36
|
+
return jsonResult(res.data);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { pxApi, jsonResult } from "../http-client.js";
|
|
3
|
+
export function registerAdminTools(server) {
|
|
4
|
+
server.tool("px_get_subscription", "Get PX subscription details for the authenticated account", {}, async () => {
|
|
5
|
+
const res = await pxApi("GET", "/v1/admin/subscription");
|
|
6
|
+
return jsonResult(res.data);
|
|
7
|
+
});
|
|
8
|
+
server.tool("px_get_model_attributes", "Get PX user or account model attributes (custom and standard fields)", {
|
|
9
|
+
type: z.enum(["user", "account"]).describe("Model type to retrieve attributes for"),
|
|
10
|
+
}, async ({ type }) => {
|
|
11
|
+
const res = await pxApi("GET", `/v1/admin/model/${type}/attributes`);
|
|
12
|
+
return jsonResult(res.data);
|
|
13
|
+
});
|
|
14
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { pxApi, jsonResult } from "../http-client.js";
|
|
3
|
+
export function registerEngagementTools(server) {
|
|
4
|
+
server.tool("px_list_engagements", "List PX engagements with optional content type filter and page-based pagination", {
|
|
5
|
+
contentTypes: z.array(z.string()).optional().describe("Filter by content types, e.g. ['DIALOG', 'SLIDER', 'GUIDE']"),
|
|
6
|
+
pageSize: z.number().optional().describe("Results per page (default 200, max 500)"),
|
|
7
|
+
pageNumber: z.number().optional().describe("Page number (0-indexed)"),
|
|
8
|
+
}, async ({ contentTypes, pageSize, pageNumber }) => {
|
|
9
|
+
const params = { pageSize, pageNumber };
|
|
10
|
+
if (contentTypes)
|
|
11
|
+
params.contentTypes = contentTypes.join(",");
|
|
12
|
+
const res = await pxApi("GET", "/v1/engagement", params);
|
|
13
|
+
return jsonResult(res.data);
|
|
14
|
+
});
|
|
15
|
+
server.tool("px_get_engagement", "Get a single PX engagement by ID", {
|
|
16
|
+
engagementId: z.string().describe("The engagement ID"),
|
|
17
|
+
}, async ({ engagementId }) => {
|
|
18
|
+
const res = await pxApi("GET", `/v1/engagement/${encodeURIComponent(engagementId)}`);
|
|
19
|
+
return jsonResult(res.data);
|
|
20
|
+
});
|
|
21
|
+
server.tool("px_set_engagement_state", "Start or pause a PX engagement", {
|
|
22
|
+
engagementId: z.string().describe("The engagement ID"),
|
|
23
|
+
state: z.enum(["START", "PAUSE"]).describe("Target state"),
|
|
24
|
+
envs: z.array(z.string()).optional().describe("Environments to apply to"),
|
|
25
|
+
}, async ({ engagementId, state, envs }) => {
|
|
26
|
+
const body = { engagementId, state };
|
|
27
|
+
if (envs)
|
|
28
|
+
body.envs = envs;
|
|
29
|
+
const res = await pxApi("PUT", "/v1/engagement/state", undefined, body);
|
|
30
|
+
return jsonResult(res.data);
|
|
31
|
+
});
|
|
32
|
+
server.tool("px_delete_engagement", "Delete a PX engagement by ID", {
|
|
33
|
+
engagementId: z.string().describe("The engagement ID"),
|
|
34
|
+
}, async ({ engagementId }) => {
|
|
35
|
+
const res = await pxApi("DELETE", `/v1/engagement/${encodeURIComponent(engagementId)}`);
|
|
36
|
+
return jsonResult(res.data);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { pxApi, jsonResult } from "../http-client.js";
|
|
3
|
+
const EVENT_TYPE_PATHS = {
|
|
4
|
+
pageView: "/v1/events/pageView",
|
|
5
|
+
identify: "/v1/events/identify",
|
|
6
|
+
click: "/v1/events/click",
|
|
7
|
+
email: "/v1/events/email",
|
|
8
|
+
formSubmit: "/v1/events/formSubmit",
|
|
9
|
+
lead: "/v1/events/lead",
|
|
10
|
+
sessionInitialized: "/v1/events/session",
|
|
11
|
+
segmentMatch: "/v1/events/segment_match",
|
|
12
|
+
engagementView: "/v1/events/engagementView",
|
|
13
|
+
feature_match: "/v1/events/feature_match",
|
|
14
|
+
};
|
|
15
|
+
export function registerEventTools(server) {
|
|
16
|
+
server.tool("px_get_events", "Query PX events by type with filtering, sorting, and scroll-based pagination. Date values are epoch milliseconds. Max historical range: 190 days.", {
|
|
17
|
+
eventType: z.enum([
|
|
18
|
+
"pageView", "identify", "click", "email", "formSubmit",
|
|
19
|
+
"lead", "sessionInitialized", "segmentMatch", "engagementView", "feature_match",
|
|
20
|
+
]).describe("Type of event to query"),
|
|
21
|
+
filter: z.array(z.string()).optional().describe("Filter expressions"),
|
|
22
|
+
sort: z.string().optional().describe("Sort field and direction"),
|
|
23
|
+
pageSize: z.number().optional().describe("Results per page (default 25, max 1000)"),
|
|
24
|
+
scrollId: z.string().optional().describe("Scroll ID for next page"),
|
|
25
|
+
dateRangeStart: z.number().optional().describe("Start date as epoch ms"),
|
|
26
|
+
dateRangeEnd: z.number().optional().describe("End date as epoch ms"),
|
|
27
|
+
}, async ({ eventType, filter, sort, pageSize, scrollId, dateRangeStart, dateRangeEnd }) => {
|
|
28
|
+
const path = EVENT_TYPE_PATHS[eventType];
|
|
29
|
+
const res = await pxApi("GET", path, { filter, sort, pageSize, scrollId, dateRangeStart, dateRangeEnd });
|
|
30
|
+
return jsonResult(res.data);
|
|
31
|
+
});
|
|
32
|
+
server.tool("px_get_custom_events", "Query PX custom events with filtering, sorting, and scroll-based pagination", {
|
|
33
|
+
filter: z.array(z.string()).optional().describe("Filter expressions"),
|
|
34
|
+
sort: z.string().optional().describe("Sort field and direction"),
|
|
35
|
+
pageSize: z.number().optional().describe("Results per page (default 25, max 1000)"),
|
|
36
|
+
scrollId: z.string().optional().describe("Scroll ID for next page"),
|
|
37
|
+
dateRangeStart: z.number().optional().describe("Start date as epoch ms"),
|
|
38
|
+
dateRangeEnd: z.number().optional().describe("End date as epoch ms"),
|
|
39
|
+
}, async ({ filter, sort, pageSize, scrollId, dateRangeStart, dateRangeEnd }) => {
|
|
40
|
+
const res = await pxApi("GET", "/v1/events/custom", { filter, sort, pageSize, scrollId, dateRangeStart, dateRangeEnd });
|
|
41
|
+
return jsonResult(res.data);
|
|
42
|
+
});
|
|
43
|
+
server.tool("px_create_custom_event", "Create a custom event in PX", {
|
|
44
|
+
eventData: z.record(z.string(), z.unknown()).describe("Custom event JSON with identifyId, propertyKey, eventName, date, attributes, etc."),
|
|
45
|
+
}, async ({ eventData }) => {
|
|
46
|
+
const res = await pxApi("POST", "/v1/events/custom", undefined, eventData);
|
|
47
|
+
return jsonResult(res.data);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { pxApi, jsonResult } from "../http-client.js";
|
|
3
|
+
export function registerFeatureTools(server) {
|
|
4
|
+
server.tool("px_list_features", "List PX features with page-based pagination", {
|
|
5
|
+
pageSize: z.number().optional().describe("Results per page (default 25, max 200)"),
|
|
6
|
+
pageNumber: z.number().optional().describe("Page number (0-indexed)"),
|
|
7
|
+
propertyKey: z.string().optional().describe("Filter by product ID"),
|
|
8
|
+
}, async ({ pageSize, pageNumber, propertyKey }) => {
|
|
9
|
+
const res = await pxApi("GET", "/v1/feature", { pageSize, pageNumber, propertyKey });
|
|
10
|
+
return jsonResult(res.data);
|
|
11
|
+
});
|
|
12
|
+
server.tool("px_get_feature", "Get a single PX feature by ID", {
|
|
13
|
+
featureId: z.string().describe("The feature ID"),
|
|
14
|
+
}, async ({ featureId }) => {
|
|
15
|
+
const res = await pxApi("GET", `/v1/feature/${encodeURIComponent(featureId)}`);
|
|
16
|
+
return jsonResult(res.data);
|
|
17
|
+
});
|
|
18
|
+
server.tool("px_get_feature_adoption", "Get feature adoption statistics for a given date range", {
|
|
19
|
+
featureId: z.string().describe("The feature ID"),
|
|
20
|
+
dateRangeStart: z.number().describe("Start date as epoch milliseconds"),
|
|
21
|
+
dateRangeEnd: z.number().optional().describe("End date as epoch milliseconds (defaults to now)"),
|
|
22
|
+
propertyKey: z.string().describe("Product ID"),
|
|
23
|
+
}, async ({ featureId, dateRangeStart, dateRangeEnd, propertyKey }) => {
|
|
24
|
+
const res = await pxApi("GET", `/v1/feature/adoption/${encodeURIComponent(featureId)}`, {
|
|
25
|
+
dateRangeStart,
|
|
26
|
+
dateRangeEnd,
|
|
27
|
+
propertyKey,
|
|
28
|
+
});
|
|
29
|
+
return jsonResult(res.data);
|
|
30
|
+
});
|
|
31
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { pxApi, jsonResult } from "../http-client.js";
|
|
3
|
+
export function registerKnowledgeCenterTools(server) {
|
|
4
|
+
server.tool("px_list_kc_bots", "List PX Knowledge Center bots with page-based pagination", {
|
|
5
|
+
productId: z.string().optional().describe("Filter by product ID"),
|
|
6
|
+
pageSize: z.number().optional().describe("Results per page"),
|
|
7
|
+
pageNumber: z.number().optional().describe("Page number (0-indexed)"),
|
|
8
|
+
}, async ({ productId, pageSize, pageNumber }) => {
|
|
9
|
+
const res = await pxApi("GET", "/v1/kcbot", { productId, pageSize, pageNumber });
|
|
10
|
+
return jsonResult(res.data);
|
|
11
|
+
});
|
|
12
|
+
server.tool("px_get_kc_bot", "Get a single PX Knowledge Center bot by ID", {
|
|
13
|
+
kcId: z.string().describe("The Knowledge Center bot ID"),
|
|
14
|
+
}, async ({ kcId }) => {
|
|
15
|
+
const res = await pxApi("GET", `/v1/kcbot/${encodeURIComponent(kcId)}`);
|
|
16
|
+
return jsonResult(res.data);
|
|
17
|
+
});
|
|
18
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { pxApi, jsonResult } from "../http-client.js";
|
|
3
|
+
export function registerSegmentTools(server) {
|
|
4
|
+
server.tool("px_list_segments", "List PX segments with page-based pagination", {
|
|
5
|
+
pageSize: z.number().optional().describe("Results per page (default 25, max 200)"),
|
|
6
|
+
pageNumber: z.number().optional().describe("Page number (0-indexed)"),
|
|
7
|
+
}, async ({ pageSize, pageNumber }) => {
|
|
8
|
+
const res = await pxApi("GET", "/v1/segment", { pageSize, pageNumber });
|
|
9
|
+
return jsonResult(res.data);
|
|
10
|
+
});
|
|
11
|
+
server.tool("px_get_segment", "Get a single PX segment by ID", {
|
|
12
|
+
segmentId: z.string().describe("The segment ID"),
|
|
13
|
+
}, async ({ segmentId }) => {
|
|
14
|
+
const res = await pxApi("GET", `/v1/segment/${encodeURIComponent(segmentId)}`);
|
|
15
|
+
return jsonResult(res.data);
|
|
16
|
+
});
|
|
17
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { pxApi, jsonResult } from "../http-client.js";
|
|
3
|
+
export function registerSurveyTools(server) {
|
|
4
|
+
server.tool("px_get_survey_responses", "Query PX survey response events with filtering, sorting, and scroll-based pagination", {
|
|
5
|
+
filter: z.array(z.string()).optional().describe("Filter expressions"),
|
|
6
|
+
sort: z.string().optional().describe("Sort field and direction"),
|
|
7
|
+
pageSize: z.number().optional().describe("Results per page (default 25, max 1000)"),
|
|
8
|
+
scrollId: z.string().optional().describe("Scroll ID for next page"),
|
|
9
|
+
dateRangeStart: z.number().optional().describe("Start date as epoch ms"),
|
|
10
|
+
dateRangeEnd: z.number().optional().describe("End date as epoch ms"),
|
|
11
|
+
}, async ({ filter, sort, pageSize, scrollId, dateRangeStart, dateRangeEnd }) => {
|
|
12
|
+
const res = await pxApi("GET", "/v1/survey/responses", { filter, sort, pageSize, scrollId, dateRangeStart, dateRangeEnd });
|
|
13
|
+
return jsonResult(res.data);
|
|
14
|
+
});
|
|
15
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { pxApi, jsonResult } from "../http-client.js";
|
|
3
|
+
export function registerUserTools(server) {
|
|
4
|
+
server.tool("px_list_users", "List PX users with optional filtering, sorting, and scroll-based pagination. Filter syntax: fieldName{op}value (ops: ==, !=, <, <=, >, >=, ~, !~). Multiple filters are ANDed; pass multiple filter strings for OR.", {
|
|
5
|
+
filter: z.array(z.string()).optional().describe("Filter expressions, e.g. ['email==john@example.com']"),
|
|
6
|
+
sort: z.string().optional().describe("Sort field and direction, e.g. 'lastSeenDate:desc'"),
|
|
7
|
+
pageSize: z.number().optional().describe("Results per page (default 25, max 1000)"),
|
|
8
|
+
scrollId: z.string().optional().describe("Scroll ID from previous response for next page"),
|
|
9
|
+
}, async ({ filter, sort, pageSize, scrollId }) => {
|
|
10
|
+
const res = await pxApi("GET", "/v1/users", { filter, sort, pageSize, scrollId });
|
|
11
|
+
return jsonResult(res.data);
|
|
12
|
+
});
|
|
13
|
+
server.tool("px_get_user", "Get a single PX user by their identifyId", {
|
|
14
|
+
identifyId: z.string().describe("The user's identifyId"),
|
|
15
|
+
}, async ({ identifyId }) => {
|
|
16
|
+
const res = await pxApi("GET", `/v1/users/${encodeURIComponent(identifyId)}`);
|
|
17
|
+
return jsonResult(res.data);
|
|
18
|
+
});
|
|
19
|
+
server.tool("px_create_user", "Create a new PX user. Required fields: identifyId, propertyKeys (with at least one product ID).", {
|
|
20
|
+
userData: z.record(z.string(), z.unknown()).describe("User object JSON with identifyId, propertyKeys, etc."),
|
|
21
|
+
}, async ({ userData }) => {
|
|
22
|
+
const res = await pxApi("POST", "/v1/users", undefined, userData);
|
|
23
|
+
return jsonResult(res.data);
|
|
24
|
+
});
|
|
25
|
+
server.tool("px_update_user", "Update an existing PX user by identifyId", {
|
|
26
|
+
identifyId: z.string().describe("The user's identifyId"),
|
|
27
|
+
userData: z.record(z.string(), z.unknown()).describe("Fields to update"),
|
|
28
|
+
}, async ({ identifyId, userData }) => {
|
|
29
|
+
const res = await pxApi("PUT", `/v1/users/${encodeURIComponent(identifyId)}`, undefined, userData);
|
|
30
|
+
return jsonResult(res.data);
|
|
31
|
+
});
|
|
32
|
+
server.tool("px_delete_user", "Delete a PX user by identifyId", {
|
|
33
|
+
identifyId: z.string().describe("The user's identifyId"),
|
|
34
|
+
}, async ({ identifyId }) => {
|
|
35
|
+
const res = await pxApi("DELETE", `/v1/users/${encodeURIComponent(identifyId)}`);
|
|
36
|
+
return jsonResult(res.data);
|
|
37
|
+
});
|
|
38
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "gainsight-px-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server for the Gainsight PX REST API — query and manage users, accounts, engagements, features, segments, events, and more from any MCP-compatible AI tool.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"px-mcp-server": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsc",
|
|
15
|
+
"test": "vitest run",
|
|
16
|
+
"prepublishOnly": "npm run build",
|
|
17
|
+
"start": "node dist/index.js"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"mcp",
|
|
21
|
+
"gainsight",
|
|
22
|
+
"gainsight-px",
|
|
23
|
+
"model-context-protocol",
|
|
24
|
+
"ai",
|
|
25
|
+
"claude",
|
|
26
|
+
"cursor"
|
|
27
|
+
],
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@modelcontextprotocol/sdk": "^1.12.1"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/node": "^22.0.0",
|
|
34
|
+
"typescript": "^5.7.0",
|
|
35
|
+
"vitest": "^4.1.0"
|
|
36
|
+
},
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=18.0.0"
|
|
39
|
+
}
|
|
40
|
+
}
|