touchstone-mcp-tools 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 +199 -0
- package/build/handlers/wolves-handler.d.ts +55 -0
- package/build/handlers/wolves-handler.js +77 -0
- package/build/index.d.ts +5 -0
- package/build/index.js +5 -0
- package/build/schemas/wolves-tools.json +157 -0
- package/build/servers/wolves-server.d.ts +10 -0
- package/build/servers/wolves-server.js +104 -0
- package/build/utils/token-manager.d.ts +9 -0
- package/build/utils/token-manager.js +32 -0
- package/build/utils/wolves-api.d.ts +134 -0
- package/build/utils/wolves-api.js +80 -0
- package/package.json +38 -0
package/README.md
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# wolves-mcp-tools
|
|
2
|
+
|
|
3
|
+
MCP (Model Context Protocol) server for the [Wolves Experimentation](https://wolves.microsoft.com) platform. Enables AI coding agents (Claude Code, Copilot CLI, Codex CLI) to create and manage A/B experiments through natural language interaction.
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
- **Node.js** >= 18.0.0
|
|
8
|
+
- **Azure CLI** — You must be logged in via `az login` for authentication. The server uses `DefaultAzureCredential` to acquire tokens scoped to `https://graph.microsoft.com/.default`.
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
# Install from npm
|
|
14
|
+
npm install wolves-mcp-tools
|
|
15
|
+
|
|
16
|
+
# Or run directly via npx (no install needed)
|
|
17
|
+
npx -y wolves-mcp-tools
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## MCP Configuration
|
|
21
|
+
|
|
22
|
+
Add the following to your `.mcp.json` to register the server with your AI coding agent:
|
|
23
|
+
|
|
24
|
+
```json
|
|
25
|
+
{
|
|
26
|
+
"mcpServers": {
|
|
27
|
+
"wolves-tools": {
|
|
28
|
+
"type": "stdio",
|
|
29
|
+
"command": "npx",
|
|
30
|
+
"args": ["-y", "wolves-mcp-tools"],
|
|
31
|
+
"description": "Wolves MCP tools for experiment management"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Available Tools
|
|
38
|
+
|
|
39
|
+
| Tool | Description |
|
|
40
|
+
|------|-------------|
|
|
41
|
+
| `create_experiment` | Create a new A/B experiment in Wolves. Checks for duplicate names before creation. |
|
|
42
|
+
| `update_experiment` | Update an existing experiment. Fetches current state and merges changes to prevent data loss. |
|
|
43
|
+
| `list_experiments` | List all experiments with optional client-side filtering by status and result limiting. |
|
|
44
|
+
| `get_experiment` | Get detailed information about a specific experiment by its UUID. |
|
|
45
|
+
| `list_metrics` | List available metrics, optionally filtered by type (`count`, `sum`, `ratio`, `conversion`) or status. |
|
|
46
|
+
| `list_api_keys` | List API keys, optionally filtered by experiment ID. |
|
|
47
|
+
| `update_api_keys` | Update an API key's name, active status, or experiment/gate permissions. |
|
|
48
|
+
|
|
49
|
+
### create_experiment
|
|
50
|
+
|
|
51
|
+
Creates a new A/B experiment with the specified groups and configuration.
|
|
52
|
+
|
|
53
|
+
**Parameters:**
|
|
54
|
+
|
|
55
|
+
| Parameter | Type | Required | Default | Description |
|
|
56
|
+
|-----------|------|----------|---------|-------------|
|
|
57
|
+
| `name` | string | Yes | — | Experiment name |
|
|
58
|
+
| `groups` | array | Yes | — | Experiment groups (variants). First group defaults to control. |
|
|
59
|
+
| `hypothesis` | string | No | — | Experiment hypothesis |
|
|
60
|
+
| `allocationPercentage` | number | No | 100 | Overall traffic allocation (0-100) |
|
|
61
|
+
| `targetDurationDays` | number | No | 14 | Target experiment duration in days |
|
|
62
|
+
| `type` | string | No | `"ab"` | Experiment type |
|
|
63
|
+
| `idType` | string | No | `"session_id"` | ID type for assignment |
|
|
64
|
+
|
|
65
|
+
Each group requires `name` (string) and `size` (number, 0-100). Optional fields: `isControl` (boolean), `parameters` (array of `{name, type, value}`).
|
|
66
|
+
|
|
67
|
+
### update_experiment
|
|
68
|
+
|
|
69
|
+
Updates an existing experiment's configuration. Only provided fields are updated.
|
|
70
|
+
|
|
71
|
+
**Parameters:**
|
|
72
|
+
|
|
73
|
+
| Parameter | Type | Required | Description |
|
|
74
|
+
|-----------|------|----------|-------------|
|
|
75
|
+
| `experiment_id` | string (UUID) | Yes | The experiment to update |
|
|
76
|
+
| `name` | string | No | New experiment name |
|
|
77
|
+
| `hypothesis` | string | No | New hypothesis |
|
|
78
|
+
| `allocationPercentage` | number | No | New traffic allocation (0-100) |
|
|
79
|
+
| `groups` | array | No | Updated groups (replaces all existing) |
|
|
80
|
+
| `targetDurationDays` | number | No | New target duration in days |
|
|
81
|
+
| `type` | string | No | Experiment type |
|
|
82
|
+
| `idType` | string | No | ID type for assignment |
|
|
83
|
+
| `targetingCriteria` | string | No | Targeting criteria expression |
|
|
84
|
+
| `analysisType` | string | No | Analysis type |
|
|
85
|
+
| `defaultConfidenceInterval` | number | No | Default confidence interval |
|
|
86
|
+
|
|
87
|
+
### list_experiments
|
|
88
|
+
|
|
89
|
+
Lists all experiments with optional filtering.
|
|
90
|
+
|
|
91
|
+
| Parameter | Type | Required | Description |
|
|
92
|
+
|-----------|------|----------|-------------|
|
|
93
|
+
| `status` | string | No | Filter by: `"setup"`, `"in_progress"`, or `"completed"` |
|
|
94
|
+
| `limit` | number | No | Maximum number of results to return |
|
|
95
|
+
|
|
96
|
+
### get_experiment
|
|
97
|
+
|
|
98
|
+
| Parameter | Type | Required | Description |
|
|
99
|
+
|-----------|------|----------|-------------|
|
|
100
|
+
| `experiment_id` | string (UUID) | Yes | The experiment to retrieve |
|
|
101
|
+
|
|
102
|
+
### list_metrics
|
|
103
|
+
|
|
104
|
+
| Parameter | Type | Required | Description |
|
|
105
|
+
|-----------|------|----------|-------------|
|
|
106
|
+
| `type` | string | No | Filter by: `"count"`, `"sum"`, `"ratio"`, or `"conversion"` |
|
|
107
|
+
| `status` | string | No | Filter by metric status |
|
|
108
|
+
|
|
109
|
+
### list_api_keys
|
|
110
|
+
|
|
111
|
+
| Parameter | Type | Required | Description |
|
|
112
|
+
|-----------|------|----------|-------------|
|
|
113
|
+
| `experiment_id` | string | No | Filter keys by experiment ID |
|
|
114
|
+
|
|
115
|
+
### update_api_keys
|
|
116
|
+
|
|
117
|
+
| Parameter | Type | Required | Description |
|
|
118
|
+
|-----------|------|----------|-------------|
|
|
119
|
+
| `key_id` | string (UUID) | Yes | The API key to update |
|
|
120
|
+
| `name` | string | No | New name for the API key |
|
|
121
|
+
| `is_active` | boolean | No | Whether the key is active |
|
|
122
|
+
| `experiment_ids` | string[] | No | Experiment IDs this key has access to (replaces existing) |
|
|
123
|
+
| `gate_ids` | string[] | No | Gate IDs this key has access to (replaces existing) |
|
|
124
|
+
|
|
125
|
+
## Development
|
|
126
|
+
|
|
127
|
+
### Setup
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
cd mcp-tools
|
|
131
|
+
npm install
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Build
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
npm run build
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
This compiles TypeScript from `src/` to `build/` using `tsc`.
|
|
141
|
+
|
|
142
|
+
### Run
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
# Run compiled build
|
|
146
|
+
npm start
|
|
147
|
+
|
|
148
|
+
# Run in development mode (ts-node, no build needed)
|
|
149
|
+
npm run dev
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Test
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
# Run all tests
|
|
156
|
+
npm test
|
|
157
|
+
|
|
158
|
+
# Run tests with coverage report
|
|
159
|
+
npm run test:coverage
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Publish to npm
|
|
163
|
+
|
|
164
|
+
The package is configured for publishing to the public npm registry.
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
# 1. Login to npm (one-time)
|
|
168
|
+
npm login
|
|
169
|
+
|
|
170
|
+
# 2. Publish (automatically runs build via prepublishOnly)
|
|
171
|
+
npm publish
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
The `files` field in `package.json` ensures only the `build/` directory is included in the published package. No source code, tests, or documentation are shipped.
|
|
175
|
+
|
|
176
|
+
## Architecture
|
|
177
|
+
|
|
178
|
+
```
|
|
179
|
+
mcp-tools/src/
|
|
180
|
+
+-- index.ts # Barrel exports
|
|
181
|
+
+-- servers/
|
|
182
|
+
| +-- wolves-server.ts # MCP server wiring + CLI entry point
|
|
183
|
+
+-- handlers/
|
|
184
|
+
| +-- wolves-handler.ts # Business logic (7 tool methods)
|
|
185
|
+
+-- utils/
|
|
186
|
+
| +-- token-manager.ts # DefaultAzureCredential + token caching
|
|
187
|
+
| +-- wolves-api.ts # Axios HTTP client for Wolves REST API
|
|
188
|
+
+-- schemas/
|
|
189
|
+
+-- wolves-tools.json # MCP tool definitions (JSON Schema)
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
- **Server** — Registers MCP tools and routes calls to handler methods via stdio transport.
|
|
193
|
+
- **Handler** — Implements business logic including duplicate name checking, fetch-then-merge updates, and client-side filtering.
|
|
194
|
+
- **API Client** — Axios-based HTTP client with automatic Bearer token injection.
|
|
195
|
+
- **Token Manager** — Acquires and caches Azure AD tokens using `DefaultAzureCredential`, auto-refreshing 5 minutes before expiry.
|
|
196
|
+
|
|
197
|
+
## License
|
|
198
|
+
|
|
199
|
+
MIT
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { WolvesApiClient } from "../utils/wolves-api.js";
|
|
2
|
+
export interface ToolResult {
|
|
3
|
+
data: any;
|
|
4
|
+
message: string;
|
|
5
|
+
}
|
|
6
|
+
export declare class WolvesHandler {
|
|
7
|
+
private api;
|
|
8
|
+
constructor(api?: WolvesApiClient);
|
|
9
|
+
listExperimentGroups(): Promise<ToolResult>;
|
|
10
|
+
listProgressionTemplates(params: {
|
|
11
|
+
group_id: string;
|
|
12
|
+
}): Promise<ToolResult>;
|
|
13
|
+
listAssignmentUnits(params: {
|
|
14
|
+
filter?: string;
|
|
15
|
+
}): Promise<ToolResult>;
|
|
16
|
+
searchSubscribers(params: {
|
|
17
|
+
search: string;
|
|
18
|
+
count?: number;
|
|
19
|
+
fetch_user_photo?: boolean;
|
|
20
|
+
}): Promise<ToolResult>;
|
|
21
|
+
createExperiment(params: {
|
|
22
|
+
name: string;
|
|
23
|
+
experimentation_group: string;
|
|
24
|
+
variants: Array<{
|
|
25
|
+
id?: string;
|
|
26
|
+
type: "control" | "treatment";
|
|
27
|
+
description?: string;
|
|
28
|
+
}>;
|
|
29
|
+
description?: string;
|
|
30
|
+
assignment_unit_id?: string;
|
|
31
|
+
subscribers?: string[];
|
|
32
|
+
metrics?: Array<{
|
|
33
|
+
name: string;
|
|
34
|
+
type: string;
|
|
35
|
+
description?: string;
|
|
36
|
+
}>;
|
|
37
|
+
progressions?: Array<{
|
|
38
|
+
name: string;
|
|
39
|
+
feature_gates?: any[];
|
|
40
|
+
stages?: any[];
|
|
41
|
+
progression_template_id?: string;
|
|
42
|
+
progression_template_parameters?: Record<string, any>;
|
|
43
|
+
progression_parameters?: any[];
|
|
44
|
+
}>;
|
|
45
|
+
}): Promise<ToolResult>;
|
|
46
|
+
getExperiment(params: {
|
|
47
|
+
experiment_id: string;
|
|
48
|
+
}): Promise<ToolResult>;
|
|
49
|
+
listExperiments(params: {
|
|
50
|
+
page?: number;
|
|
51
|
+
page_size?: number;
|
|
52
|
+
status?: string;
|
|
53
|
+
search?: string;
|
|
54
|
+
}): Promise<ToolResult>;
|
|
55
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { WolvesApiClient } from "../utils/wolves-api.js";
|
|
2
|
+
export class WolvesHandler {
|
|
3
|
+
api;
|
|
4
|
+
constructor(api) {
|
|
5
|
+
this.api = api || new WolvesApiClient();
|
|
6
|
+
}
|
|
7
|
+
// Tool 1: list_experiment_groups
|
|
8
|
+
async listExperimentGroups() {
|
|
9
|
+
const result = await this.api.listExperimentGroups();
|
|
10
|
+
return {
|
|
11
|
+
data: result,
|
|
12
|
+
message: `Found ${result.length} experiment group(s)`,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
// Tool 2: list_progression_templates
|
|
16
|
+
async listProgressionTemplates(params) {
|
|
17
|
+
const result = await this.api.listProgressionTemplates(params.group_id);
|
|
18
|
+
return {
|
|
19
|
+
data: result,
|
|
20
|
+
message: `Found ${result.length} progression template(s) for group "${params.group_id}"`,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
// Tool 3: list_assignment_units
|
|
24
|
+
async listAssignmentUnits(params) {
|
|
25
|
+
const result = await this.api.listAssignmentUnits(params.filter);
|
|
26
|
+
return {
|
|
27
|
+
data: result,
|
|
28
|
+
message: `Found ${result.value.length} assignment unit(s)`,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
// Tool 4: search_subscribers
|
|
32
|
+
async searchSubscribers(params) {
|
|
33
|
+
const result = await this.api.searchSubscribers(params.search, params.count, params.fetch_user_photo);
|
|
34
|
+
return {
|
|
35
|
+
data: result,
|
|
36
|
+
message: `Found ${result.length} subscriber(s) matching "${params.search}"`,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
// Tool 5: create_experiment
|
|
40
|
+
async createExperiment(params) {
|
|
41
|
+
const result = await this.api.createExperiment({
|
|
42
|
+
name: params.name,
|
|
43
|
+
description: params.description,
|
|
44
|
+
experimentation_group: params.experimentation_group,
|
|
45
|
+
assignment_unit_id: params.assignment_unit_id,
|
|
46
|
+
subscribers: params.subscribers,
|
|
47
|
+
variants: params.variants,
|
|
48
|
+
metrics: params.metrics,
|
|
49
|
+
progressions: params.progressions,
|
|
50
|
+
});
|
|
51
|
+
return {
|
|
52
|
+
data: result,
|
|
53
|
+
message: `Experiment "${result.name}" created (ID: ${result.id})`,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
// Tool 6: get_experiment
|
|
57
|
+
async getExperiment(params) {
|
|
58
|
+
const result = await this.api.getExperiment(params.experiment_id);
|
|
59
|
+
return {
|
|
60
|
+
data: result,
|
|
61
|
+
message: `Retrieved experiment "${result.name}" (status: ${result.status})`,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
// Tool 7: list_experiments
|
|
65
|
+
async listExperiments(params) {
|
|
66
|
+
const result = await this.api.listExperiments({
|
|
67
|
+
page: params.page,
|
|
68
|
+
page_size: params.page_size,
|
|
69
|
+
status: params.status,
|
|
70
|
+
search: params.search,
|
|
71
|
+
});
|
|
72
|
+
return {
|
|
73
|
+
data: result,
|
|
74
|
+
message: `Found ${result.items.length} experiment(s) (page ${result.page}/${result.total_pages}, total: ${result.total})`,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
}
|
package/build/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { WolvesServer } from "./servers/wolves-server.js";
|
|
2
|
+
export { WolvesHandler, type ToolResult } from "./handlers/wolves-handler.js";
|
|
3
|
+
export { WolvesApiClient } from "./utils/wolves-api.js";
|
|
4
|
+
export { TokenManager } from "./utils/token-manager.js";
|
|
5
|
+
export type { TouchStoneApiConfig, ExperimentGroupResponse, ProgressionTemplateResponse, AssignmentUnitItem, AssignmentUnitsListResponse, SubscriberSearchResult, VariantInput, MetricInput, ProgressionInput, CreateExperimentRequest, ExperimentDetail, ExperimentSummary, PaginatedExperimentsResponse, } from "./utils/wolves-api.js";
|
package/build/index.js
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
{
|
|
2
|
+
"list_experiment_groups": {
|
|
3
|
+
"name": "list_experiment_groups",
|
|
4
|
+
"description": "List all experimentation groups in TouchStone. Returns group IDs and names for use when creating experiments.",
|
|
5
|
+
"inputSchema": {
|
|
6
|
+
"type": "object",
|
|
7
|
+
"properties": {}
|
|
8
|
+
}
|
|
9
|
+
},
|
|
10
|
+
"list_progression_templates": {
|
|
11
|
+
"name": "list_progression_templates",
|
|
12
|
+
"description": "List progression templates for a specific experimentation group. Returns available templates that can be used when creating experiment progressions.",
|
|
13
|
+
"inputSchema": {
|
|
14
|
+
"type": "object",
|
|
15
|
+
"required": ["group_id"],
|
|
16
|
+
"properties": {
|
|
17
|
+
"group_id": {
|
|
18
|
+
"type": "string",
|
|
19
|
+
"description": "The experimentation group ID (e.g., 'exponexpws~authoring')"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"list_assignment_units": {
|
|
25
|
+
"name": "list_assignment_units",
|
|
26
|
+
"description": "List assignment units in TouchStone. Assignment units define how users are assigned to experiment variants (e.g., by user ID, session ID).",
|
|
27
|
+
"inputSchema": {
|
|
28
|
+
"type": "object",
|
|
29
|
+
"properties": {
|
|
30
|
+
"filter": {
|
|
31
|
+
"type": "string",
|
|
32
|
+
"description": "OData filter expression (e.g., 'IsArchived eq false')"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"search_subscribers": {
|
|
38
|
+
"name": "search_subscribers",
|
|
39
|
+
"description": "Search for subscribers by name or email. Subscribers can be added to experiments to receive notifications.",
|
|
40
|
+
"inputSchema": {
|
|
41
|
+
"type": "object",
|
|
42
|
+
"required": ["search"],
|
|
43
|
+
"properties": {
|
|
44
|
+
"search": {
|
|
45
|
+
"type": "string",
|
|
46
|
+
"description": "Search string for subscriber name or email"
|
|
47
|
+
},
|
|
48
|
+
"count": {
|
|
49
|
+
"type": "number",
|
|
50
|
+
"description": "Maximum number of results to return (default: 5)"
|
|
51
|
+
},
|
|
52
|
+
"fetch_user_photo": {
|
|
53
|
+
"type": "boolean",
|
|
54
|
+
"description": "Whether to include user photos in results (default: false)"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
"create_experiment": {
|
|
60
|
+
"name": "create_experiment",
|
|
61
|
+
"description": "Create a new experiment in TouchStone. Requires a name, experimentation group, and at least one variant (control/treatment).",
|
|
62
|
+
"inputSchema": {
|
|
63
|
+
"type": "object",
|
|
64
|
+
"required": ["name", "experimentation_group", "variants"],
|
|
65
|
+
"properties": {
|
|
66
|
+
"name": {
|
|
67
|
+
"type": "string",
|
|
68
|
+
"description": "Name of the experiment"
|
|
69
|
+
},
|
|
70
|
+
"description": {
|
|
71
|
+
"type": "string",
|
|
72
|
+
"description": "Description of the experiment"
|
|
73
|
+
},
|
|
74
|
+
"experimentation_group": {
|
|
75
|
+
"type": "string",
|
|
76
|
+
"description": "Experimentation group ID (e.g., 'exponexpws~test')"
|
|
77
|
+
},
|
|
78
|
+
"assignment_unit_id": {
|
|
79
|
+
"type": "string",
|
|
80
|
+
"description": "Assignment unit ID (e.g., 'userid')"
|
|
81
|
+
},
|
|
82
|
+
"subscribers": {
|
|
83
|
+
"type": "array",
|
|
84
|
+
"items": { "type": "string" },
|
|
85
|
+
"description": "List of subscriber display names to notify"
|
|
86
|
+
},
|
|
87
|
+
"variants": {
|
|
88
|
+
"type": "array",
|
|
89
|
+
"description": "Experiment variants. Must include at least one control and one treatment.",
|
|
90
|
+
"minItems": 1,
|
|
91
|
+
"items": {
|
|
92
|
+
"type": "object",
|
|
93
|
+
"required": ["type"],
|
|
94
|
+
"properties": {
|
|
95
|
+
"id": { "type": "string", "description": "Variant ID (auto-generated if omitted)" },
|
|
96
|
+
"type": { "type": "string", "enum": ["control", "treatment"], "description": "Variant type" },
|
|
97
|
+
"description": { "type": "string", "description": "Variant description" }
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
"metrics": {
|
|
102
|
+
"type": "array",
|
|
103
|
+
"description": "Metrics to track for this experiment",
|
|
104
|
+
"items": {
|
|
105
|
+
"type": "object",
|
|
106
|
+
"required": ["name", "type"],
|
|
107
|
+
"properties": {
|
|
108
|
+
"name": { "type": "string", "description": "Metric name" },
|
|
109
|
+
"type": { "type": "string", "description": "Metric type (e.g., 'Primary', 'Guardrail')" },
|
|
110
|
+
"description": { "type": "string", "description": "Metric description" }
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
"progressions": {
|
|
115
|
+
"type": "array",
|
|
116
|
+
"description": "Progression configurations for staged rollout",
|
|
117
|
+
"items": {
|
|
118
|
+
"type": "object",
|
|
119
|
+
"required": ["name"],
|
|
120
|
+
"properties": {
|
|
121
|
+
"name": { "type": "string", "description": "Progression name" },
|
|
122
|
+
"feature_gates": { "type": "array", "description": "Feature gate conditions" },
|
|
123
|
+
"stages": { "type": "array", "description": "Rollout stages" },
|
|
124
|
+
"progression_template_id": { "type": "string", "description": "ID of the progression template to use" },
|
|
125
|
+
"progression_template_parameters": { "type": "object", "description": "Template parameters" },
|
|
126
|
+
"progression_parameters": { "type": "array", "description": "Progression parameters" }
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
"get_experiment": {
|
|
134
|
+
"name": "get_experiment",
|
|
135
|
+
"description": "Get detailed information about a specific experiment by its ID.",
|
|
136
|
+
"inputSchema": {
|
|
137
|
+
"type": "object",
|
|
138
|
+
"required": ["experiment_id"],
|
|
139
|
+
"properties": {
|
|
140
|
+
"experiment_id": { "type": "string", "description": "UUID of the experiment" }
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
"list_experiments": {
|
|
145
|
+
"name": "list_experiments",
|
|
146
|
+
"description": "List experiments with pagination and optional filtering. Returns experiment summaries with pagination metadata.",
|
|
147
|
+
"inputSchema": {
|
|
148
|
+
"type": "object",
|
|
149
|
+
"properties": {
|
|
150
|
+
"page": { "type": "number", "description": "Page number (default: 1)" },
|
|
151
|
+
"page_size": { "type": "number", "description": "Results per page (default: 20, max: 100)" },
|
|
152
|
+
"status": { "type": "string", "description": "Filter by status: 'new', 'running', or 'stopped'" },
|
|
153
|
+
"search": { "type": "string", "description": "Search in experiment name" }
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { WolvesHandler } from "../handlers/wolves-handler.js";
|
|
3
|
+
export declare class WolvesServer {
|
|
4
|
+
private server;
|
|
5
|
+
private handler;
|
|
6
|
+
constructor(handler?: WolvesHandler);
|
|
7
|
+
private setupToolHandlers;
|
|
8
|
+
private handleToolCall;
|
|
9
|
+
run(): Promise<void>;
|
|
10
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { ListToolsRequestSchema, CallToolRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
5
|
+
import { WolvesHandler } from "../handlers/wolves-handler.js";
|
|
6
|
+
import toolDefinitions from "../schemas/wolves-tools.json" with { type: "json" };
|
|
7
|
+
export class WolvesServer {
|
|
8
|
+
server;
|
|
9
|
+
handler;
|
|
10
|
+
constructor(handler) {
|
|
11
|
+
this.handler = handler || new WolvesHandler();
|
|
12
|
+
this.server = new Server({ name: "touchstone-tools", version: "1.0.0" }, { capabilities: { tools: {} } });
|
|
13
|
+
this.setupToolHandlers();
|
|
14
|
+
}
|
|
15
|
+
setupToolHandlers() {
|
|
16
|
+
// List all available tools
|
|
17
|
+
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
18
|
+
tools: Object.values(toolDefinitions),
|
|
19
|
+
}));
|
|
20
|
+
// Route tool calls to handler methods
|
|
21
|
+
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
22
|
+
const { name, arguments: args } = request.params;
|
|
23
|
+
return this.handleToolCall(name, args);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
async handleToolCall(name, args) {
|
|
27
|
+
try {
|
|
28
|
+
let result;
|
|
29
|
+
switch (name) {
|
|
30
|
+
case "list_experiment_groups":
|
|
31
|
+
result = await this.handler.listExperimentGroups();
|
|
32
|
+
break;
|
|
33
|
+
case "list_progression_templates":
|
|
34
|
+
result = await this.handler.listProgressionTemplates(args);
|
|
35
|
+
break;
|
|
36
|
+
case "list_assignment_units":
|
|
37
|
+
result = await this.handler.listAssignmentUnits(args);
|
|
38
|
+
break;
|
|
39
|
+
case "search_subscribers":
|
|
40
|
+
result = await this.handler.searchSubscribers(args);
|
|
41
|
+
break;
|
|
42
|
+
case "create_experiment":
|
|
43
|
+
result = await this.handler.createExperiment(args);
|
|
44
|
+
break;
|
|
45
|
+
case "get_experiment":
|
|
46
|
+
result = await this.handler.getExperiment(args);
|
|
47
|
+
break;
|
|
48
|
+
case "list_experiments":
|
|
49
|
+
result = await this.handler.listExperiments(args);
|
|
50
|
+
break;
|
|
51
|
+
default:
|
|
52
|
+
return wrapError(`Unknown tool: ${name}`);
|
|
53
|
+
}
|
|
54
|
+
return wrapSuccess(result);
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
return wrapError(error);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
async run() {
|
|
61
|
+
const transport = new StdioServerTransport();
|
|
62
|
+
await this.server.connect(transport);
|
|
63
|
+
console.error("TouchStone MCP server running on stdio");
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// ---- Response helpers ----
|
|
67
|
+
function wrapSuccess(result) {
|
|
68
|
+
return {
|
|
69
|
+
content: [
|
|
70
|
+
{
|
|
71
|
+
type: "text",
|
|
72
|
+
text: JSON.stringify({
|
|
73
|
+
success: true,
|
|
74
|
+
data: result.data,
|
|
75
|
+
message: result.message,
|
|
76
|
+
}, null, 2),
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function wrapError(error) {
|
|
82
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
83
|
+
return {
|
|
84
|
+
content: [
|
|
85
|
+
{
|
|
86
|
+
type: "text",
|
|
87
|
+
text: JSON.stringify({
|
|
88
|
+
success: false,
|
|
89
|
+
error: message,
|
|
90
|
+
}, null, 2),
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
isError: true,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
// ---- Entry point ----
|
|
97
|
+
import { fileURLToPath } from "url";
|
|
98
|
+
import { realpathSync } from "fs";
|
|
99
|
+
const currentFilePath = fileURLToPath(import.meta.url);
|
|
100
|
+
const entryPointPath = process.argv[1] ? realpathSync(process.argv[1]) : "";
|
|
101
|
+
if (currentFilePath === entryPointPath) {
|
|
102
|
+
const server = new WolvesServer();
|
|
103
|
+
server.run().catch(console.error);
|
|
104
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { DefaultAzureCredential } from "@azure/identity";
|
|
2
|
+
export class TokenManager {
|
|
3
|
+
credential;
|
|
4
|
+
cachedToken = null;
|
|
5
|
+
// TouchStone API validates against Microsoft Graph
|
|
6
|
+
static SCOPE = "https://graph.microsoft.com/.default";
|
|
7
|
+
// Refresh token 5 minutes before expiry
|
|
8
|
+
static BUFFER_MS = 5 * 60 * 1000;
|
|
9
|
+
constructor() {
|
|
10
|
+
this.credential = new DefaultAzureCredential();
|
|
11
|
+
}
|
|
12
|
+
async getToken() {
|
|
13
|
+
if (this.isTokenValid()) {
|
|
14
|
+
return this.cachedToken.token;
|
|
15
|
+
}
|
|
16
|
+
try {
|
|
17
|
+
this.cachedToken = await this.credential.getToken(TokenManager.SCOPE);
|
|
18
|
+
return this.cachedToken.token;
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
throw new Error("Azure authentication failed. Please ensure you are logged in by running `az login`. " +
|
|
22
|
+
`Details: ${error instanceof Error ? error.message : String(error)}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
isTokenValid() {
|
|
26
|
+
if (!this.cachedToken)
|
|
27
|
+
return false;
|
|
28
|
+
const now = Date.now();
|
|
29
|
+
const expiresAt = this.cachedToken.expiresOnTimestamp - TokenManager.BUFFER_MS;
|
|
30
|
+
return now < expiresAt;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { type AxiosError } from "axios";
|
|
2
|
+
export interface TouchStoneApiConfig {
|
|
3
|
+
baseUrl?: string;
|
|
4
|
+
}
|
|
5
|
+
export interface ExperimentGroupResponse {
|
|
6
|
+
id: string;
|
|
7
|
+
name: string;
|
|
8
|
+
description: string | null;
|
|
9
|
+
created_at: string | null;
|
|
10
|
+
updated_at: string | null;
|
|
11
|
+
}
|
|
12
|
+
export interface ProgressionTemplateResponse {
|
|
13
|
+
id: string;
|
|
14
|
+
experiment_group_id: string;
|
|
15
|
+
name: string;
|
|
16
|
+
description: string | null;
|
|
17
|
+
template_config: Record<string, any>;
|
|
18
|
+
created_at: string;
|
|
19
|
+
updated_at: string;
|
|
20
|
+
}
|
|
21
|
+
export interface AssignmentUnitItem {
|
|
22
|
+
id: string;
|
|
23
|
+
displayName: string;
|
|
24
|
+
description: string | null;
|
|
25
|
+
isArchived: boolean;
|
|
26
|
+
runtimeId: string | null;
|
|
27
|
+
eTag: string | null;
|
|
28
|
+
experimentationGroups: string[];
|
|
29
|
+
}
|
|
30
|
+
export interface AssignmentUnitsListResponse {
|
|
31
|
+
count: number | null;
|
|
32
|
+
value: AssignmentUnitItem[];
|
|
33
|
+
nextLink: string | null;
|
|
34
|
+
nextLinkQuery: string | null;
|
|
35
|
+
}
|
|
36
|
+
export interface SubscriberSearchResult {
|
|
37
|
+
displayName: string;
|
|
38
|
+
mail: string | null;
|
|
39
|
+
mailNickname: string | null;
|
|
40
|
+
userPrincipalName: string | null;
|
|
41
|
+
department: string | null;
|
|
42
|
+
userPhotoBase64: string | null;
|
|
43
|
+
id: string;
|
|
44
|
+
type: string;
|
|
45
|
+
tenantId: string;
|
|
46
|
+
}
|
|
47
|
+
export interface VariantInput {
|
|
48
|
+
id?: string;
|
|
49
|
+
type: "control" | "treatment";
|
|
50
|
+
description?: string;
|
|
51
|
+
}
|
|
52
|
+
export interface MetricInput {
|
|
53
|
+
name: string;
|
|
54
|
+
type: string;
|
|
55
|
+
description?: string;
|
|
56
|
+
}
|
|
57
|
+
export interface ProgressionInput {
|
|
58
|
+
name: string;
|
|
59
|
+
feature_gates?: any[];
|
|
60
|
+
stages?: any[];
|
|
61
|
+
progression_template_id?: string;
|
|
62
|
+
progression_template_parameters?: Record<string, any>;
|
|
63
|
+
progression_parameters?: any[];
|
|
64
|
+
}
|
|
65
|
+
export interface CreateExperimentRequest {
|
|
66
|
+
name: string;
|
|
67
|
+
description?: string;
|
|
68
|
+
experimentation_group: string;
|
|
69
|
+
assignment_unit_id?: string;
|
|
70
|
+
subscribers?: string[];
|
|
71
|
+
variants: VariantInput[];
|
|
72
|
+
metrics?: MetricInput[];
|
|
73
|
+
progressions?: ProgressionInput[];
|
|
74
|
+
}
|
|
75
|
+
export interface ExperimentDetail {
|
|
76
|
+
id: string;
|
|
77
|
+
name: string;
|
|
78
|
+
description: string | null;
|
|
79
|
+
status: string;
|
|
80
|
+
experiment_group_id: string | null;
|
|
81
|
+
experiment_group_name: string | null;
|
|
82
|
+
owner_email: string | null;
|
|
83
|
+
subscribers: string[];
|
|
84
|
+
variants: any[];
|
|
85
|
+
metrics: any[];
|
|
86
|
+
progressions: any[];
|
|
87
|
+
created_at: string;
|
|
88
|
+
updated_at: string;
|
|
89
|
+
started_at: string | null;
|
|
90
|
+
}
|
|
91
|
+
export interface ExperimentSummary {
|
|
92
|
+
id: string;
|
|
93
|
+
name: string;
|
|
94
|
+
description: string | null;
|
|
95
|
+
status: string;
|
|
96
|
+
experiment_group_id: string | null;
|
|
97
|
+
experiment_group_name: string | null;
|
|
98
|
+
owner_email: string | null;
|
|
99
|
+
subscribers: string[];
|
|
100
|
+
variant_count: number;
|
|
101
|
+
metric_count: number;
|
|
102
|
+
progression_count: number;
|
|
103
|
+
created_at: string;
|
|
104
|
+
updated_at: string;
|
|
105
|
+
started_at: string | null;
|
|
106
|
+
}
|
|
107
|
+
export interface PaginatedExperimentsResponse {
|
|
108
|
+
items: ExperimentSummary[];
|
|
109
|
+
total: number;
|
|
110
|
+
page: number;
|
|
111
|
+
page_size: number;
|
|
112
|
+
total_pages: number;
|
|
113
|
+
}
|
|
114
|
+
export declare function normalizeError(error: AxiosError): {
|
|
115
|
+
message: string;
|
|
116
|
+
statusCode: number;
|
|
117
|
+
};
|
|
118
|
+
export declare class WolvesApiClient {
|
|
119
|
+
private client;
|
|
120
|
+
private tokenManager;
|
|
121
|
+
constructor(config?: TouchStoneApiConfig);
|
|
122
|
+
listExperimentGroups(): Promise<ExperimentGroupResponse[]>;
|
|
123
|
+
listProgressionTemplates(groupId: string): Promise<ProgressionTemplateResponse[]>;
|
|
124
|
+
listAssignmentUnits(filter?: string): Promise<AssignmentUnitsListResponse>;
|
|
125
|
+
searchSubscribers(search: string, count?: number, fetchUserPhoto?: boolean): Promise<SubscriberSearchResult[]>;
|
|
126
|
+
createExperiment(data: CreateExperimentRequest): Promise<ExperimentDetail>;
|
|
127
|
+
getExperiment(experimentId: string): Promise<ExperimentDetail>;
|
|
128
|
+
listExperiments(params?: {
|
|
129
|
+
page?: number;
|
|
130
|
+
page_size?: number;
|
|
131
|
+
status?: string;
|
|
132
|
+
search?: string;
|
|
133
|
+
}): Promise<PaginatedExperimentsResponse>;
|
|
134
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
import { TokenManager } from "./token-manager.js";
|
|
3
|
+
// ---- Error Handling ----
|
|
4
|
+
export function normalizeError(error) {
|
|
5
|
+
if (error.response) {
|
|
6
|
+
const detail = error.response.data?.detail || error.message;
|
|
7
|
+
return { message: detail, statusCode: error.response.status };
|
|
8
|
+
}
|
|
9
|
+
if (error.request) {
|
|
10
|
+
return { message: "No response from TouchStone API", statusCode: 0 };
|
|
11
|
+
}
|
|
12
|
+
return { message: error.message, statusCode: 0 };
|
|
13
|
+
}
|
|
14
|
+
// ---- API Client ----
|
|
15
|
+
export class WolvesApiClient {
|
|
16
|
+
client;
|
|
17
|
+
tokenManager;
|
|
18
|
+
constructor(config) {
|
|
19
|
+
this.tokenManager = new TokenManager();
|
|
20
|
+
const baseURL = config?.baseUrl
|
|
21
|
+
|| process.env.TOUCHSTONE_API_BASE_URL
|
|
22
|
+
|| "https://touchstone-dev.azurewebsites.net";
|
|
23
|
+
this.client = axios.create({
|
|
24
|
+
baseURL,
|
|
25
|
+
headers: {
|
|
26
|
+
"Content-Type": "application/json",
|
|
27
|
+
"X-Auth-Provider": "microsoft",
|
|
28
|
+
},
|
|
29
|
+
timeout: 30000,
|
|
30
|
+
});
|
|
31
|
+
// Request interceptor: inject Bearer token
|
|
32
|
+
this.client.interceptors.request.use(async (config) => {
|
|
33
|
+
const token = await this.tokenManager.getToken();
|
|
34
|
+
config.headers.Authorization = `Bearer ${token}`;
|
|
35
|
+
return config;
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
// ---- Experiment Group endpoints ----
|
|
39
|
+
async listExperimentGroups() {
|
|
40
|
+
const response = await this.client.get("/api/experiments/experiment-groups");
|
|
41
|
+
return response.data;
|
|
42
|
+
}
|
|
43
|
+
// ---- Progression Template endpoints ----
|
|
44
|
+
async listProgressionTemplates(groupId) {
|
|
45
|
+
const response = await this.client.get(`/api/experiments/experiment-groups/${encodeURIComponent(groupId)}/progression-templates`);
|
|
46
|
+
return response.data;
|
|
47
|
+
}
|
|
48
|
+
// ---- Assignment Unit endpoints ----
|
|
49
|
+
async listAssignmentUnits(filter) {
|
|
50
|
+
const params = {};
|
|
51
|
+
if (filter) {
|
|
52
|
+
params.filter = filter;
|
|
53
|
+
}
|
|
54
|
+
const response = await this.client.get("/api/experiments/assignment-units", { params });
|
|
55
|
+
return response.data;
|
|
56
|
+
}
|
|
57
|
+
// ---- Subscriber endpoints ----
|
|
58
|
+
async searchSubscribers(search, count, fetchUserPhoto) {
|
|
59
|
+
const params = {
|
|
60
|
+
search,
|
|
61
|
+
count: count ?? 5,
|
|
62
|
+
fetch_user_photo: fetchUserPhoto ?? false,
|
|
63
|
+
};
|
|
64
|
+
const response = await this.client.get("/api/experiments/subscribers/search", { params });
|
|
65
|
+
return response.data;
|
|
66
|
+
}
|
|
67
|
+
// ---- Experiment endpoints ----
|
|
68
|
+
async createExperiment(data) {
|
|
69
|
+
const response = await this.client.post("/api/experiments/experiments", data);
|
|
70
|
+
return response.data;
|
|
71
|
+
}
|
|
72
|
+
async getExperiment(experimentId) {
|
|
73
|
+
const response = await this.client.get(`/api/experiments/experiments/${experimentId}`);
|
|
74
|
+
return response.data;
|
|
75
|
+
}
|
|
76
|
+
async listExperiments(params) {
|
|
77
|
+
const response = await this.client.get("/api/experiments/experiments", { params });
|
|
78
|
+
return response.data;
|
|
79
|
+
}
|
|
80
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "touchstone-mcp-tools",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP tools for TouchStone Experimentation platform",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "build/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"touchstone-mcp-tools": "build/servers/wolves-server.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"build"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsc",
|
|
15
|
+
"start": "node build/servers/wolves-server.js",
|
|
16
|
+
"dev": "tsx src/servers/wolves-server.ts",
|
|
17
|
+
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
|
|
18
|
+
"test:coverage": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage",
|
|
19
|
+
"prepublishOnly": "npm run build"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
23
|
+
"@azure/identity": "^4.6.0",
|
|
24
|
+
"axios": "^1.7.0"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"tsx": "^4.0.0",
|
|
28
|
+
"typescript": "^5.0.0",
|
|
29
|
+
"@types/node": "^22.0.0",
|
|
30
|
+
"jest": "^29.7.0",
|
|
31
|
+
"ts-jest": "^29.2.0",
|
|
32
|
+
"@jest/globals": "^29.7.0"
|
|
33
|
+
},
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=18.0.0"
|
|
36
|
+
},
|
|
37
|
+
"license": "MIT"
|
|
38
|
+
}
|