touchstone-mcp-tools 1.0.0 → 1.0.1
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.
|
@@ -22,7 +22,8 @@ export class WolvesHandler {
|
|
|
22
22
|
}
|
|
23
23
|
// Tool 3: list_assignment_units
|
|
24
24
|
async listAssignmentUnits(params) {
|
|
25
|
-
const
|
|
25
|
+
const filter = params.filter ?? "IsArchived eq false";
|
|
26
|
+
const result = await this.api.listAssignmentUnits(filter);
|
|
26
27
|
return {
|
|
27
28
|
data: result,
|
|
28
29
|
message: `Found ${result.value.length} assignment unit(s)`,
|
|
@@ -79,7 +79,22 @@ function wrapSuccess(result) {
|
|
|
79
79
|
};
|
|
80
80
|
}
|
|
81
81
|
function wrapError(error) {
|
|
82
|
-
|
|
82
|
+
let message;
|
|
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
|
+
}
|
|
83
98
|
return {
|
|
84
99
|
content: [
|
|
85
100
|
{
|
|
@@ -87,6 +102,8 @@ function wrapError(error) {
|
|
|
87
102
|
text: JSON.stringify({
|
|
88
103
|
success: false,
|
|
89
104
|
error: message,
|
|
105
|
+
...(statusCode ? { statusCode } : {}),
|
|
106
|
+
...(detail ? { detail } : {}),
|
|
90
107
|
}, null, 2),
|
|
91
108
|
},
|
|
92
109
|
],
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
export declare class TokenManager {
|
|
2
|
-
private
|
|
3
|
-
private
|
|
4
|
-
private static readonly SCOPE;
|
|
5
|
-
private static readonly BUFFER_MS;
|
|
2
|
+
private cachedTokens;
|
|
3
|
+
private tokenFilePath;
|
|
6
4
|
constructor();
|
|
7
5
|
getToken(): Promise<string>;
|
|
8
|
-
private
|
|
6
|
+
private isAccessTokenValid;
|
|
7
|
+
private refreshAccessToken;
|
|
8
|
+
private interactiveLogin;
|
|
9
|
+
private waitForAuthCode;
|
|
10
|
+
private saveTokens;
|
|
11
|
+
private loadCachedTokens;
|
|
12
|
+
private openBrowser;
|
|
9
13
|
}
|
|
@@ -1,32 +1,189 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createServer } from "http";
|
|
2
|
+
import { randomBytes, createHash } from "crypto";
|
|
3
|
+
import { exec } from "child_process";
|
|
4
|
+
import { URL, URLSearchParams } from "url";
|
|
5
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
|
|
6
|
+
import { join } from "path";
|
|
7
|
+
import { tmpdir, platform } from "os";
|
|
8
|
+
// Mars app — the only app ID trusted by ExP API
|
|
9
|
+
const MARS_CLIENT_ID = "4728d04c-aaa7-4d80-9a24-f6f7d45c1be3";
|
|
10
|
+
const MICROSOFT_TENANT_ID = "72f988bf-86f1-41af-91ab-2d7cd011db47";
|
|
11
|
+
const TOKEN_ENDPOINT = `https://login.microsoftonline.com/${MICROSOFT_TENANT_ID}/oauth2/v2.0/token`;
|
|
12
|
+
const AUTHORIZE_ENDPOINT = `https://login.microsoftonline.com/${MICROSOFT_TENANT_ID}/oauth2/v2.0/authorize`;
|
|
13
|
+
const REDIRECT_URI = "http://localhost:4000/auth";
|
|
14
|
+
const SCOPE = "openid profile User.Read";
|
|
15
|
+
// Refresh token 5 minutes before access token expiry
|
|
16
|
+
const BUFFER_MS = 5 * 60 * 1000;
|
|
17
|
+
const LOGIN_TIMEOUT_MS = 120_000;
|
|
2
18
|
export class TokenManager {
|
|
3
|
-
|
|
4
|
-
|
|
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;
|
|
19
|
+
cachedTokens = null;
|
|
20
|
+
tokenFilePath;
|
|
9
21
|
constructor() {
|
|
10
|
-
|
|
22
|
+
const cacheDir = join(tmpdir(), "touchstone-mcp");
|
|
23
|
+
if (!existsSync(cacheDir)) {
|
|
24
|
+
mkdirSync(cacheDir, { recursive: true });
|
|
25
|
+
}
|
|
26
|
+
this.tokenFilePath = join(cacheDir, "token-cache.json");
|
|
27
|
+
this.loadCachedTokens();
|
|
11
28
|
}
|
|
12
29
|
async getToken() {
|
|
13
|
-
|
|
14
|
-
|
|
30
|
+
// 1. Check cached access token
|
|
31
|
+
if (this.isAccessTokenValid()) {
|
|
32
|
+
return this.cachedTokens.access_token;
|
|
33
|
+
}
|
|
34
|
+
// 2. Try refresh token
|
|
35
|
+
if (this.cachedTokens?.refresh_token) {
|
|
36
|
+
try {
|
|
37
|
+
await this.refreshAccessToken();
|
|
38
|
+
return this.cachedTokens.access_token;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// Refresh failed (expired or revoked), fall through to interactive login
|
|
42
|
+
console.error("Refresh token expired, interactive login required.");
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// 3. Interactive browser login
|
|
46
|
+
await this.interactiveLogin();
|
|
47
|
+
return this.cachedTokens.access_token;
|
|
48
|
+
}
|
|
49
|
+
// ---- Private: Token lifecycle ----
|
|
50
|
+
isAccessTokenValid() {
|
|
51
|
+
if (!this.cachedTokens?.access_token)
|
|
52
|
+
return false;
|
|
53
|
+
return Date.now() < this.cachedTokens.expires_at - BUFFER_MS;
|
|
54
|
+
}
|
|
55
|
+
async refreshAccessToken() {
|
|
56
|
+
const resp = await fetch(TOKEN_ENDPOINT, {
|
|
57
|
+
method: "POST",
|
|
58
|
+
headers: {
|
|
59
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
60
|
+
"Origin": REDIRECT_URI.replace("/auth", ""),
|
|
61
|
+
},
|
|
62
|
+
body: new URLSearchParams({
|
|
63
|
+
client_id: MARS_CLIENT_ID,
|
|
64
|
+
scope: SCOPE,
|
|
65
|
+
refresh_token: this.cachedTokens.refresh_token,
|
|
66
|
+
grant_type: "refresh_token",
|
|
67
|
+
}),
|
|
68
|
+
});
|
|
69
|
+
const data = await resp.json();
|
|
70
|
+
if (!data.access_token) {
|
|
71
|
+
throw new Error(data.error_description || "Token refresh failed");
|
|
72
|
+
}
|
|
73
|
+
this.saveTokens(data);
|
|
74
|
+
}
|
|
75
|
+
async interactiveLogin() {
|
|
76
|
+
// PKCE
|
|
77
|
+
const codeVerifier = randomBytes(32).toString("base64url");
|
|
78
|
+
const codeChallenge = createHash("sha256").update(codeVerifier).digest("base64url");
|
|
79
|
+
const authParams = new URLSearchParams({
|
|
80
|
+
client_id: MARS_CLIENT_ID,
|
|
81
|
+
response_type: "code",
|
|
82
|
+
redirect_uri: REDIRECT_URI,
|
|
83
|
+
scope: SCOPE,
|
|
84
|
+
response_mode: "query",
|
|
85
|
+
prompt: "select_account",
|
|
86
|
+
code_challenge: codeChallenge,
|
|
87
|
+
code_challenge_method: "S256",
|
|
88
|
+
});
|
|
89
|
+
const authUrl = `${AUTHORIZE_ENDPOINT}?${authParams}`;
|
|
90
|
+
// Wait for auth code via local HTTP server
|
|
91
|
+
const code = await this.waitForAuthCode(authUrl);
|
|
92
|
+
// Exchange code for tokens
|
|
93
|
+
const resp = await fetch(TOKEN_ENDPOINT, {
|
|
94
|
+
method: "POST",
|
|
95
|
+
headers: {
|
|
96
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
97
|
+
"Origin": REDIRECT_URI.replace("/auth", ""),
|
|
98
|
+
},
|
|
99
|
+
body: new URLSearchParams({
|
|
100
|
+
client_id: MARS_CLIENT_ID,
|
|
101
|
+
scope: SCOPE,
|
|
102
|
+
code,
|
|
103
|
+
redirect_uri: REDIRECT_URI,
|
|
104
|
+
grant_type: "authorization_code",
|
|
105
|
+
code_verifier: codeVerifier,
|
|
106
|
+
}),
|
|
107
|
+
});
|
|
108
|
+
const data = await resp.json();
|
|
109
|
+
if (!data.access_token) {
|
|
110
|
+
throw new Error(`Interactive login failed: ${data.error_description || JSON.stringify(data)}`);
|
|
15
111
|
}
|
|
112
|
+
this.saveTokens(data);
|
|
113
|
+
console.error("TouchStone login successful.");
|
|
114
|
+
}
|
|
115
|
+
waitForAuthCode(authUrl) {
|
|
116
|
+
return new Promise((resolve, reject) => {
|
|
117
|
+
const server = createServer((req, res) => {
|
|
118
|
+
const url = new URL(req.url || "/", "http://localhost:4000");
|
|
119
|
+
const authCode = url.searchParams.get("code");
|
|
120
|
+
const error = url.searchParams.get("error_description");
|
|
121
|
+
if (authCode) {
|
|
122
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
123
|
+
res.end("Login successful. You can close this window.");
|
|
124
|
+
server.close();
|
|
125
|
+
resolve(authCode);
|
|
126
|
+
}
|
|
127
|
+
else if (error) {
|
|
128
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
129
|
+
res.end(`Login failed: ${error}`);
|
|
130
|
+
server.close();
|
|
131
|
+
reject(new Error(error));
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
res.writeHead(200);
|
|
135
|
+
res.end("");
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
server.listen(4000, () => {
|
|
139
|
+
console.error("Opening browser for TouchStone login...");
|
|
140
|
+
console.error("If browser does not open, visit: " + authUrl);
|
|
141
|
+
this.openBrowser(authUrl);
|
|
142
|
+
});
|
|
143
|
+
setTimeout(() => {
|
|
144
|
+
server.close();
|
|
145
|
+
reject(new Error("Login timed out after 2 minutes. Please try again."));
|
|
146
|
+
}, LOGIN_TIMEOUT_MS);
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
// ---- Private: Token persistence ----
|
|
150
|
+
saveTokens(data) {
|
|
151
|
+
this.cachedTokens = {
|
|
152
|
+
access_token: data.access_token,
|
|
153
|
+
refresh_token: data.refresh_token,
|
|
154
|
+
expires_at: Date.now() + (data.expires_in || 3600) * 1000,
|
|
155
|
+
};
|
|
16
156
|
try {
|
|
17
|
-
this.
|
|
18
|
-
return this.cachedToken.token;
|
|
157
|
+
writeFileSync(this.tokenFilePath, JSON.stringify(this.cachedTokens), { mode: 0o600 });
|
|
19
158
|
}
|
|
20
|
-
catch
|
|
21
|
-
|
|
22
|
-
`Details: ${error instanceof Error ? error.message : String(error)}`);
|
|
159
|
+
catch {
|
|
160
|
+
// Non-fatal: tokens still work in memory
|
|
23
161
|
}
|
|
24
162
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
163
|
+
loadCachedTokens() {
|
|
164
|
+
try {
|
|
165
|
+
if (existsSync(this.tokenFilePath)) {
|
|
166
|
+
const data = JSON.parse(readFileSync(this.tokenFilePath, "utf8"));
|
|
167
|
+
if (data.access_token && data.refresh_token) {
|
|
168
|
+
this.cachedTokens = data;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
// Corrupted cache file, ignore
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
// ---- Private: Cross-platform browser open ----
|
|
177
|
+
openBrowser(url) {
|
|
178
|
+
const os = platform();
|
|
179
|
+
const cmd = os === "win32"
|
|
180
|
+
? `cmd.exe /c start "" "${url}"`
|
|
181
|
+
: os === "darwin"
|
|
182
|
+
? `open "${url}"`
|
|
183
|
+
: `xdg-open "${url}"`;
|
|
184
|
+
exec(cmd, (err) => {
|
|
185
|
+
if (err)
|
|
186
|
+
console.error("Could not open browser automatically.");
|
|
187
|
+
});
|
|
31
188
|
}
|
|
32
189
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "touchstone-mcp-tools",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "MCP tools for TouchStone Experimentation platform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "build/index.js",
|
|
@@ -20,7 +20,6 @@
|
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
23
|
-
"@azure/identity": "^4.6.0",
|
|
24
23
|
"axios": "^1.7.0"
|
|
25
24
|
},
|
|
26
25
|
"devDependencies": {
|
package/README.md
DELETED
|
@@ -1,199 +0,0 @@
|
|
|
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
|