touchstone-mcp-tools 1.0.2 → 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 +22 -1
- package/build/handlers/wolves-handler.js +52 -5
- package/build/schemas/wolves-tools.json +61 -3
- package/build/servers/wolves-server.js +7 -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 +2 -0
- package/build/utils/wolves-api.js +3 -0
- package/package.json +1 -1
|
@@ -45,7 +45,7 @@ export declare class WolvesHandler {
|
|
|
45
45
|
}): Promise<ToolResult>;
|
|
46
46
|
updateSubscribers(params: {
|
|
47
47
|
experiment_id: string;
|
|
48
|
-
subscribers
|
|
48
|
+
subscribers?: Array<{
|
|
49
49
|
id: string;
|
|
50
50
|
type: string;
|
|
51
51
|
tenantId: string;
|
|
@@ -60,4 +60,25 @@ export declare class WolvesHandler {
|
|
|
60
60
|
status?: string;
|
|
61
61
|
search?: string;
|
|
62
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>;
|
|
63
84
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { WolvesApiClient } from "../utils/wolves-api.js";
|
|
2
|
+
import { existsSync, mkdirSync, writeFileSync, readdirSync, readFileSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
2
4
|
// Generate 8-character variant ID: 4 lowercase letters + 4 digits
|
|
3
5
|
// Matches ExP-Hub UI convention (e.g., "kifj6658", "tvan3734")
|
|
4
6
|
function generateVariantId() {
|
|
@@ -75,12 +77,16 @@ export class WolvesHandler {
|
|
|
75
77
|
}
|
|
76
78
|
// Tool 6: update_subscribers
|
|
77
79
|
async updateSubscribers(params) {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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 });
|
|
81
87
|
return {
|
|
82
|
-
data: { experiment_id: params.experiment_id, subscriber_count:
|
|
83
|
-
message: `Updated ${
|
|
88
|
+
data: { experiment_id: params.experiment_id, subscriber_count: subscribers.length },
|
|
89
|
+
message: `Updated ${subscribers.length} subscriber(s) for experiment "${params.experiment_id}"`,
|
|
84
90
|
};
|
|
85
91
|
}
|
|
86
92
|
// Tool 8: get_experiment
|
|
@@ -104,4 +110,45 @@ export class WolvesHandler {
|
|
|
104
110
|
message: `Found ${result.items.length} experiment(s) (page ${result.page}/${result.total_pages}, total: ${result.total})`,
|
|
105
111
|
};
|
|
106
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
|
+
}
|
|
107
154
|
}
|
|
@@ -132,10 +132,10 @@
|
|
|
132
132
|
},
|
|
133
133
|
"update_subscribers": {
|
|
134
134
|
"name": "update_subscribers",
|
|
135
|
-
"description": "Update subscribers for an experiment. Subscribers receive notifications about experiment changes.
|
|
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
136
|
"inputSchema": {
|
|
137
137
|
"type": "object",
|
|
138
|
-
"required": ["experiment_id"
|
|
138
|
+
"required": ["experiment_id"],
|
|
139
139
|
"properties": {
|
|
140
140
|
"experiment_id": {
|
|
141
141
|
"type": "string",
|
|
@@ -143,7 +143,7 @@
|
|
|
143
143
|
},
|
|
144
144
|
"subscribers": {
|
|
145
145
|
"type": "array",
|
|
146
|
-
"description": "List of subscriber objects.
|
|
146
|
+
"description": "List of subscriber objects. If omitted, defaults to the current logged-in user.",
|
|
147
147
|
"items": {
|
|
148
148
|
"type": "object",
|
|
149
149
|
"required": ["id", "type", "tenantId"],
|
|
@@ -180,5 +180,63 @@
|
|
|
180
180
|
"search": { "type": "string", "description": "Search in experiment name" }
|
|
181
181
|
}
|
|
182
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
|
+
}
|
|
183
241
|
}
|
|
184
242
|
}
|
|
@@ -51,6 +51,12 @@ export class WolvesServer {
|
|
|
51
51
|
case "list_experiments":
|
|
52
52
|
result = await this.handler.listExperiments(args);
|
|
53
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;
|
|
54
60
|
default:
|
|
55
61
|
return wrapError(`Unknown tool: ${name}`);
|
|
56
62
|
}
|
|
@@ -82,22 +88,7 @@ function wrapSuccess(result) {
|
|
|
82
88
|
};
|
|
83
89
|
}
|
|
84
90
|
function wrapError(error) {
|
|
85
|
-
|
|
86
|
-
let statusCode;
|
|
87
|
-
let detail;
|
|
88
|
-
if (error && typeof error === "object" && "response" in error) {
|
|
89
|
-
// Axios error — extract API response details
|
|
90
|
-
const axiosError = error;
|
|
91
|
-
statusCode = axiosError.response?.status;
|
|
92
|
-
detail = axiosError.response?.data;
|
|
93
|
-
const apiDetail = detail?.detail || detail?.message || detail?.error || detail?.title;
|
|
94
|
-
message = apiDetail
|
|
95
|
-
? `API error ${statusCode}: ${typeof apiDetail === "string" ? apiDetail : JSON.stringify(apiDetail)}`
|
|
96
|
-
: `Request failed with status code ${statusCode}`;
|
|
97
|
-
}
|
|
98
|
-
else {
|
|
99
|
-
message = error instanceof Error ? error.message : String(error);
|
|
100
|
-
}
|
|
91
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
101
92
|
return {
|
|
102
93
|
content: [
|
|
103
94
|
{
|
|
@@ -105,8 +96,6 @@ function wrapError(error) {
|
|
|
105
96
|
text: JSON.stringify({
|
|
106
97
|
success: false,
|
|
107
98
|
error: message,
|
|
108
|
-
...(statusCode ? { statusCode } : {}),
|
|
109
|
-
...(detail ? { detail } : {}),
|
|
110
99
|
}, null, 2),
|
|
111
100
|
},
|
|
112
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
|
}
|
|
@@ -127,6 +128,7 @@ export declare class WolvesApiClient {
|
|
|
127
128
|
private client;
|
|
128
129
|
private tokenManager;
|
|
129
130
|
constructor(config?: TouchStoneApiConfig);
|
|
131
|
+
getCurrentUser(): TokenUserInfo;
|
|
130
132
|
listExperimentGroups(): Promise<ExperimentGroupResponse[]>;
|
|
131
133
|
listProgressionTemplates(groupId: string): Promise<ProgressionTemplateResponse[]>;
|
|
132
134
|
listAssignmentUnits(filter?: string): Promise<AssignmentUnitsListResponse>;
|
|
@@ -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;
|