touchstone-mcp-tools 1.0.1 → 1.0.3
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/build/handlers/wolves-handler.d.ts +29 -0
- package/build/handlers/wolves-handler.js +79 -3
- package/build/schemas/wolves-tools.json +86 -1
- package/build/servers/wolves-server.js +10 -18
- package/build/utils/token-manager.d.ts +8 -0
- package/build/utils/token-manager.js +16 -0
- package/build/utils/wolves-api.d.ts +11 -0
- package/build/utils/wolves-api.js +6 -0
- package/package.json +1 -1
|
@@ -43,6 +43,14 @@ export declare class WolvesHandler {
|
|
|
43
43
|
progression_parameters?: any[];
|
|
44
44
|
}>;
|
|
45
45
|
}): Promise<ToolResult>;
|
|
46
|
+
updateSubscribers(params: {
|
|
47
|
+
experiment_id: string;
|
|
48
|
+
subscribers?: Array<{
|
|
49
|
+
id: string;
|
|
50
|
+
type: string;
|
|
51
|
+
tenantId: string;
|
|
52
|
+
}>;
|
|
53
|
+
}): Promise<ToolResult>;
|
|
46
54
|
getExperiment(params: {
|
|
47
55
|
experiment_id: string;
|
|
48
56
|
}): Promise<ToolResult>;
|
|
@@ -52,4 +60,25 @@ export declare class WolvesHandler {
|
|
|
52
60
|
status?: string;
|
|
53
61
|
search?: string;
|
|
54
62
|
}): Promise<ToolResult>;
|
|
63
|
+
logExperimentAction(params: {
|
|
64
|
+
action: string;
|
|
65
|
+
experiment: {
|
|
66
|
+
id: string;
|
|
67
|
+
name: string;
|
|
68
|
+
description?: string;
|
|
69
|
+
status?: string;
|
|
70
|
+
experiment_group?: string;
|
|
71
|
+
assignment_unit?: string;
|
|
72
|
+
progression_template?: string;
|
|
73
|
+
variants?: Array<{
|
|
74
|
+
id: string;
|
|
75
|
+
type: string;
|
|
76
|
+
description?: string;
|
|
77
|
+
}>;
|
|
78
|
+
subscribers?: string[];
|
|
79
|
+
};
|
|
80
|
+
}): Promise<ToolResult>;
|
|
81
|
+
getExperimentHistory(params: {
|
|
82
|
+
limit?: number;
|
|
83
|
+
}): Promise<ToolResult>;
|
|
55
84
|
}
|
|
@@ -1,4 +1,20 @@
|
|
|
1
1
|
import { WolvesApiClient } from "../utils/wolves-api.js";
|
|
2
|
+
import { existsSync, mkdirSync, writeFileSync, readdirSync, readFileSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
// Generate 8-character variant ID: 4 lowercase letters + 4 digits
|
|
5
|
+
// Matches ExP-Hub UI convention (e.g., "kifj6658", "tvan3734")
|
|
6
|
+
function generateVariantId() {
|
|
7
|
+
const letters = "abcdefghijklmnopqrstuvwxyz";
|
|
8
|
+
const digits = "0123456789";
|
|
9
|
+
let result = "";
|
|
10
|
+
for (let i = 0; i < 4; i++) {
|
|
11
|
+
result += letters.charAt(Math.floor(Math.random() * letters.length));
|
|
12
|
+
}
|
|
13
|
+
for (let i = 0; i < 4; i++) {
|
|
14
|
+
result += digits.charAt(Math.floor(Math.random() * digits.length));
|
|
15
|
+
}
|
|
16
|
+
return result;
|
|
17
|
+
}
|
|
2
18
|
export class WolvesHandler {
|
|
3
19
|
api;
|
|
4
20
|
constructor(api) {
|
|
@@ -39,13 +55,18 @@ export class WolvesHandler {
|
|
|
39
55
|
}
|
|
40
56
|
// Tool 5: create_experiment
|
|
41
57
|
async createExperiment(params) {
|
|
58
|
+
// Auto-generate 8-char variant IDs for variants that don't have one
|
|
59
|
+
const variants = params.variants.map((v) => ({
|
|
60
|
+
...v,
|
|
61
|
+
id: v.id || generateVariantId(),
|
|
62
|
+
}));
|
|
42
63
|
const result = await this.api.createExperiment({
|
|
43
64
|
name: params.name,
|
|
44
65
|
description: params.description,
|
|
45
66
|
experimentation_group: params.experimentation_group,
|
|
46
67
|
assignment_unit_id: params.assignment_unit_id,
|
|
47
68
|
subscribers: params.subscribers,
|
|
48
|
-
variants
|
|
69
|
+
variants,
|
|
49
70
|
metrics: params.metrics,
|
|
50
71
|
progressions: params.progressions,
|
|
51
72
|
});
|
|
@@ -54,7 +75,21 @@ export class WolvesHandler {
|
|
|
54
75
|
message: `Experiment "${result.name}" created (ID: ${result.id})`,
|
|
55
76
|
};
|
|
56
77
|
}
|
|
57
|
-
// Tool 6:
|
|
78
|
+
// Tool 6: update_subscribers
|
|
79
|
+
async updateSubscribers(params) {
|
|
80
|
+
let subscribers = params.subscribers;
|
|
81
|
+
// Default to current user from JWT when no subscribers provided
|
|
82
|
+
if (!subscribers || subscribers.length === 0) {
|
|
83
|
+
const user = this.api.getCurrentUser();
|
|
84
|
+
subscribers = [{ id: user.oid, type: "UserAccount", tenantId: user.tenantId }];
|
|
85
|
+
}
|
|
86
|
+
await this.api.updateSubscribers(params.experiment_id, { subscribers });
|
|
87
|
+
return {
|
|
88
|
+
data: { experiment_id: params.experiment_id, subscriber_count: subscribers.length },
|
|
89
|
+
message: `Updated ${subscribers.length} subscriber(s) for experiment "${params.experiment_id}"`,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
// Tool 8: get_experiment
|
|
58
93
|
async getExperiment(params) {
|
|
59
94
|
const result = await this.api.getExperiment(params.experiment_id);
|
|
60
95
|
return {
|
|
@@ -62,7 +97,7 @@ export class WolvesHandler {
|
|
|
62
97
|
message: `Retrieved experiment "${result.name}" (status: ${result.status})`,
|
|
63
98
|
};
|
|
64
99
|
}
|
|
65
|
-
// Tool
|
|
100
|
+
// Tool 8: list_experiments
|
|
66
101
|
async listExperiments(params) {
|
|
67
102
|
const result = await this.api.listExperiments({
|
|
68
103
|
page: params.page,
|
|
@@ -75,4 +110,45 @@ export class WolvesHandler {
|
|
|
75
110
|
message: `Found ${result.items.length} experiment(s) (page ${result.page}/${result.total_pages}, total: ${result.total})`,
|
|
76
111
|
};
|
|
77
112
|
}
|
|
113
|
+
// Tool 9: log_experiment_action
|
|
114
|
+
async logExperimentAction(params) {
|
|
115
|
+
const expDir = join(process.cwd(), ".exp");
|
|
116
|
+
if (!existsSync(expDir)) {
|
|
117
|
+
mkdirSync(expDir, { recursive: true });
|
|
118
|
+
}
|
|
119
|
+
const now = new Date();
|
|
120
|
+
const timestamp = now.toISOString();
|
|
121
|
+
const filename = timestamp.replace(/[:.]/g, "-") + ".json";
|
|
122
|
+
const record = {
|
|
123
|
+
action: params.action,
|
|
124
|
+
timestamp,
|
|
125
|
+
experiment: params.experiment,
|
|
126
|
+
};
|
|
127
|
+
writeFileSync(join(expDir, filename), JSON.stringify(record, null, 2));
|
|
128
|
+
return {
|
|
129
|
+
data: record,
|
|
130
|
+
message: `Logged "${params.action}" for experiment "${params.experiment.name}"`,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
// Tool 10: get_experiment_history
|
|
134
|
+
async getExperimentHistory(params) {
|
|
135
|
+
const expDir = join(process.cwd(), ".exp");
|
|
136
|
+
const limit = params.limit ?? 10;
|
|
137
|
+
if (!existsSync(expDir)) {
|
|
138
|
+
return { data: [], message: "No experiment history found" };
|
|
139
|
+
}
|
|
140
|
+
const files = readdirSync(expDir)
|
|
141
|
+
.filter((f) => f.endsWith(".json"))
|
|
142
|
+
.sort()
|
|
143
|
+
.reverse()
|
|
144
|
+
.slice(0, limit);
|
|
145
|
+
const records = files.map((f) => {
|
|
146
|
+
const content = readFileSync(join(expDir, f), "utf8");
|
|
147
|
+
return JSON.parse(content);
|
|
148
|
+
});
|
|
149
|
+
return {
|
|
150
|
+
data: records,
|
|
151
|
+
message: `Found ${records.length} experiment history record(s)`,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
78
154
|
}
|
|
@@ -92,7 +92,7 @@
|
|
|
92
92
|
"type": "object",
|
|
93
93
|
"required": ["type"],
|
|
94
94
|
"properties": {
|
|
95
|
-
"id": { "type": "string", "description": "Variant ID (
|
|
95
|
+
"id": { "type": "string", "description": "Variant ID. 8-character alphanumeric (4 letters + 4 digits, e.g., 'kifj6658'). Auto-generated if omitted." },
|
|
96
96
|
"type": { "type": "string", "enum": ["control", "treatment"], "description": "Variant type" },
|
|
97
97
|
"description": { "type": "string", "description": "Variant description" }
|
|
98
98
|
}
|
|
@@ -130,6 +130,33 @@
|
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
132
|
},
|
|
133
|
+
"update_subscribers": {
|
|
134
|
+
"name": "update_subscribers",
|
|
135
|
+
"description": "Update subscribers for an experiment. Subscribers receive notifications about experiment changes. If no subscribers are provided, defaults to the current logged-in user (resolved from auth token).",
|
|
136
|
+
"inputSchema": {
|
|
137
|
+
"type": "object",
|
|
138
|
+
"required": ["experiment_id"],
|
|
139
|
+
"properties": {
|
|
140
|
+
"experiment_id": {
|
|
141
|
+
"type": "string",
|
|
142
|
+
"description": "UUID of the experiment to update subscribers for"
|
|
143
|
+
},
|
|
144
|
+
"subscribers": {
|
|
145
|
+
"type": "array",
|
|
146
|
+
"description": "List of subscriber objects. If omitted, defaults to the current logged-in user.",
|
|
147
|
+
"items": {
|
|
148
|
+
"type": "object",
|
|
149
|
+
"required": ["id", "type", "tenantId"],
|
|
150
|
+
"properties": {
|
|
151
|
+
"id": { "type": "string", "description": "Subscriber's user ID (from search_subscribers)" },
|
|
152
|
+
"type": { "type": "string", "description": "Subscriber type (e.g., 'UserAccount')" },
|
|
153
|
+
"tenantId": { "type": "string", "description": "Subscriber's tenant ID (from search_subscribers)" }
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
},
|
|
133
160
|
"get_experiment": {
|
|
134
161
|
"name": "get_experiment",
|
|
135
162
|
"description": "Get detailed information about a specific experiment by its ID.",
|
|
@@ -153,5 +180,63 @@
|
|
|
153
180
|
"search": { "type": "string", "description": "Search in experiment name" }
|
|
154
181
|
}
|
|
155
182
|
}
|
|
183
|
+
},
|
|
184
|
+
"log_experiment_action": {
|
|
185
|
+
"name": "log_experiment_action",
|
|
186
|
+
"description": "Log an experiment action to local history (.exp/ directory). Called after creating an experiment to enable history-based recommendations in future runs.",
|
|
187
|
+
"inputSchema": {
|
|
188
|
+
"type": "object",
|
|
189
|
+
"required": ["action", "experiment"],
|
|
190
|
+
"properties": {
|
|
191
|
+
"action": {
|
|
192
|
+
"type": "string",
|
|
193
|
+
"description": "The action performed (e.g., 'create_experiment')"
|
|
194
|
+
},
|
|
195
|
+
"experiment": {
|
|
196
|
+
"type": "object",
|
|
197
|
+
"required": ["id", "name"],
|
|
198
|
+
"description": "Experiment details to log",
|
|
199
|
+
"properties": {
|
|
200
|
+
"id": { "type": "string", "description": "Experiment UUID" },
|
|
201
|
+
"name": { "type": "string", "description": "Experiment name" },
|
|
202
|
+
"description": { "type": "string", "description": "Experiment description" },
|
|
203
|
+
"status": { "type": "string", "description": "Experiment status" },
|
|
204
|
+
"experiment_group": { "type": "string", "description": "Experiment group ID used" },
|
|
205
|
+
"assignment_unit": { "type": "string", "description": "Assignment unit ID used" },
|
|
206
|
+
"progression_template": { "type": "string", "description": "Progression template ID used" },
|
|
207
|
+
"variants": {
|
|
208
|
+
"type": "array",
|
|
209
|
+
"description": "Variants configured",
|
|
210
|
+
"items": {
|
|
211
|
+
"type": "object",
|
|
212
|
+
"properties": {
|
|
213
|
+
"id": { "type": "string" },
|
|
214
|
+
"type": { "type": "string" },
|
|
215
|
+
"description": { "type": "string" }
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
"subscribers": {
|
|
220
|
+
"type": "array",
|
|
221
|
+
"items": { "type": "string" },
|
|
222
|
+
"description": "Subscriber display names"
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
"get_experiment_history": {
|
|
230
|
+
"name": "get_experiment_history",
|
|
231
|
+
"description": "Get recent experiment history from local .exp/ directory. Returns the most recent records sorted by timestamp (newest first). Used to provide MRU-based recommendations when selecting experiment configuration.",
|
|
232
|
+
"inputSchema": {
|
|
233
|
+
"type": "object",
|
|
234
|
+
"properties": {
|
|
235
|
+
"limit": {
|
|
236
|
+
"type": "number",
|
|
237
|
+
"description": "Maximum number of history records to return (default: 10)"
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
156
241
|
}
|
|
157
242
|
}
|
|
@@ -42,12 +42,21 @@ export class WolvesServer {
|
|
|
42
42
|
case "create_experiment":
|
|
43
43
|
result = await this.handler.createExperiment(args);
|
|
44
44
|
break;
|
|
45
|
+
case "update_subscribers":
|
|
46
|
+
result = await this.handler.updateSubscribers(args);
|
|
47
|
+
break;
|
|
45
48
|
case "get_experiment":
|
|
46
49
|
result = await this.handler.getExperiment(args);
|
|
47
50
|
break;
|
|
48
51
|
case "list_experiments":
|
|
49
52
|
result = await this.handler.listExperiments(args);
|
|
50
53
|
break;
|
|
54
|
+
case "log_experiment_action":
|
|
55
|
+
result = await this.handler.logExperimentAction(args);
|
|
56
|
+
break;
|
|
57
|
+
case "get_experiment_history":
|
|
58
|
+
result = await this.handler.getExperimentHistory(args);
|
|
59
|
+
break;
|
|
51
60
|
default:
|
|
52
61
|
return wrapError(`Unknown tool: ${name}`);
|
|
53
62
|
}
|
|
@@ -79,22 +88,7 @@ function wrapSuccess(result) {
|
|
|
79
88
|
};
|
|
80
89
|
}
|
|
81
90
|
function wrapError(error) {
|
|
82
|
-
|
|
83
|
-
let statusCode;
|
|
84
|
-
let detail;
|
|
85
|
-
if (error && typeof error === "object" && "response" in error) {
|
|
86
|
-
// Axios error — extract API response details
|
|
87
|
-
const axiosError = error;
|
|
88
|
-
statusCode = axiosError.response?.status;
|
|
89
|
-
detail = axiosError.response?.data;
|
|
90
|
-
const apiDetail = detail?.detail || detail?.message || detail?.error || detail?.title;
|
|
91
|
-
message = apiDetail
|
|
92
|
-
? `API error ${statusCode}: ${typeof apiDetail === "string" ? apiDetail : JSON.stringify(apiDetail)}`
|
|
93
|
-
: `Request failed with status code ${statusCode}`;
|
|
94
|
-
}
|
|
95
|
-
else {
|
|
96
|
-
message = error instanceof Error ? error.message : String(error);
|
|
97
|
-
}
|
|
91
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
98
92
|
return {
|
|
99
93
|
content: [
|
|
100
94
|
{
|
|
@@ -102,8 +96,6 @@ function wrapError(error) {
|
|
|
102
96
|
text: JSON.stringify({
|
|
103
97
|
success: false,
|
|
104
98
|
error: message,
|
|
105
|
-
...(statusCode ? { statusCode } : {}),
|
|
106
|
-
...(detail ? { detail } : {}),
|
|
107
99
|
}, null, 2),
|
|
108
100
|
},
|
|
109
101
|
],
|
|
@@ -1,8 +1,16 @@
|
|
|
1
|
+
export interface TokenUserInfo {
|
|
2
|
+
oid: string;
|
|
3
|
+
tenantId: string;
|
|
4
|
+
email: string;
|
|
5
|
+
alias: string;
|
|
6
|
+
name: string;
|
|
7
|
+
}
|
|
1
8
|
export declare class TokenManager {
|
|
2
9
|
private cachedTokens;
|
|
3
10
|
private tokenFilePath;
|
|
4
11
|
constructor();
|
|
5
12
|
getToken(): Promise<string>;
|
|
13
|
+
getUserInfo(): TokenUserInfo;
|
|
6
14
|
private isAccessTokenValid;
|
|
7
15
|
private refreshAccessToken;
|
|
8
16
|
private interactiveLogin;
|
|
@@ -46,6 +46,22 @@ export class TokenManager {
|
|
|
46
46
|
await this.interactiveLogin();
|
|
47
47
|
return this.cachedTokens.access_token;
|
|
48
48
|
}
|
|
49
|
+
// ---- Public: User info from JWT ----
|
|
50
|
+
getUserInfo() {
|
|
51
|
+
if (!this.cachedTokens?.access_token) {
|
|
52
|
+
throw new Error("No access token available. Call getToken() first.");
|
|
53
|
+
}
|
|
54
|
+
const payload = this.cachedTokens.access_token.split(".")[1];
|
|
55
|
+
const decoded = JSON.parse(Buffer.from(payload, "base64url").toString());
|
|
56
|
+
const email = decoded.upn || decoded.unique_name || "";
|
|
57
|
+
return {
|
|
58
|
+
oid: decoded.oid || "",
|
|
59
|
+
tenantId: decoded.tid || "",
|
|
60
|
+
email,
|
|
61
|
+
alias: email.split("@")[0],
|
|
62
|
+
name: decoded.name || "",
|
|
63
|
+
};
|
|
64
|
+
}
|
|
49
65
|
// ---- Private: Token lifecycle ----
|
|
50
66
|
isAccessTokenValid() {
|
|
51
67
|
if (!this.cachedTokens?.access_token)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type AxiosError } from "axios";
|
|
2
|
+
import { type TokenUserInfo } from "./token-manager.js";
|
|
2
3
|
export interface TouchStoneApiConfig {
|
|
3
4
|
baseUrl?: string;
|
|
4
5
|
}
|
|
@@ -44,6 +45,14 @@ export interface SubscriberSearchResult {
|
|
|
44
45
|
type: string;
|
|
45
46
|
tenantId: string;
|
|
46
47
|
}
|
|
48
|
+
export interface SubscriberInput {
|
|
49
|
+
id: string;
|
|
50
|
+
type: string;
|
|
51
|
+
tenantId: string;
|
|
52
|
+
}
|
|
53
|
+
export interface UpdateSubscribersRequest {
|
|
54
|
+
subscribers: SubscriberInput[];
|
|
55
|
+
}
|
|
47
56
|
export interface VariantInput {
|
|
48
57
|
id?: string;
|
|
49
58
|
type: "control" | "treatment";
|
|
@@ -119,11 +128,13 @@ export declare class WolvesApiClient {
|
|
|
119
128
|
private client;
|
|
120
129
|
private tokenManager;
|
|
121
130
|
constructor(config?: TouchStoneApiConfig);
|
|
131
|
+
getCurrentUser(): TokenUserInfo;
|
|
122
132
|
listExperimentGroups(): Promise<ExperimentGroupResponse[]>;
|
|
123
133
|
listProgressionTemplates(groupId: string): Promise<ProgressionTemplateResponse[]>;
|
|
124
134
|
listAssignmentUnits(filter?: string): Promise<AssignmentUnitsListResponse>;
|
|
125
135
|
searchSubscribers(search: string, count?: number, fetchUserPhoto?: boolean): Promise<SubscriberSearchResult[]>;
|
|
126
136
|
createExperiment(data: CreateExperimentRequest): Promise<ExperimentDetail>;
|
|
137
|
+
updateSubscribers(experimentId: string, data: UpdateSubscribersRequest): Promise<void>;
|
|
127
138
|
getExperiment(experimentId: string): Promise<ExperimentDetail>;
|
|
128
139
|
listExperiments(params?: {
|
|
129
140
|
page?: number;
|
|
@@ -36,6 +36,9 @@ export class WolvesApiClient {
|
|
|
36
36
|
});
|
|
37
37
|
}
|
|
38
38
|
// ---- Experiment Group endpoints ----
|
|
39
|
+
getCurrentUser() {
|
|
40
|
+
return this.tokenManager.getUserInfo();
|
|
41
|
+
}
|
|
39
42
|
async listExperimentGroups() {
|
|
40
43
|
const response = await this.client.get("/api/experiments/experiment-groups");
|
|
41
44
|
return response.data;
|
|
@@ -69,6 +72,9 @@ export class WolvesApiClient {
|
|
|
69
72
|
const response = await this.client.post("/api/experiments/experiments", data);
|
|
70
73
|
return response.data;
|
|
71
74
|
}
|
|
75
|
+
async updateSubscribers(experimentId, data) {
|
|
76
|
+
await this.client.put(`/api/experiments/experiments/${encodeURIComponent(experimentId)}/subscribers`, data);
|
|
77
|
+
}
|
|
72
78
|
async getExperiment(experimentId) {
|
|
73
79
|
const response = await this.client.get(`/api/experiments/experiments/${experimentId}`);
|
|
74
80
|
return response.data;
|