@kylewadegrove/cutline-mcp-cli 0.4.2 → 0.5.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.
- package/Dockerfile +11 -0
- package/README.md +177 -107
- package/dist/auth/callback.js +30 -32
- package/dist/auth/keychain.js +7 -15
- package/dist/commands/init.d.ts +4 -0
- package/dist/commands/init.js +246 -0
- package/dist/commands/login.js +39 -45
- package/dist/commands/logout.js +13 -19
- package/dist/commands/serve.d.ts +1 -0
- package/dist/commands/serve.js +38 -0
- package/dist/commands/setup.d.ts +5 -0
- package/dist/commands/setup.js +267 -0
- package/dist/commands/status.js +29 -35
- package/dist/commands/upgrade.js +44 -38
- package/dist/index.js +38 -14
- package/dist/servers/chunk-7FHM2GD3.js +5836 -0
- package/dist/servers/chunk-IVWF7VYZ.js +10086 -0
- package/dist/servers/chunk-JBJYSV4P.js +139 -0
- package/dist/servers/chunk-KMUSQOTJ.js +47 -0
- package/dist/servers/chunk-PD2HN2R5.js +908 -0
- package/dist/servers/chunk-PU7TL6S3.js +91 -0
- package/dist/servers/chunk-TGSEURMN.js +46 -0
- package/dist/servers/chunk-UBBAYTW3.js +946 -0
- package/dist/servers/cutline-server.js +11512 -0
- package/dist/servers/exploration-server.js +1030 -0
- package/dist/servers/graph-metrics-DCNR7JZN.js +12 -0
- package/dist/servers/integrations-server.js +121 -0
- package/dist/servers/output-server.js +120 -0
- package/dist/servers/pipeline-O5GJPNR4.js +20 -0
- package/dist/servers/premortem-handoff-XT4K3YDJ.js +10 -0
- package/dist/servers/premortem-server.js +958 -0
- package/dist/servers/score-history-HO5KRVGC.js +6 -0
- package/dist/servers/tools-server.js +291 -0
- package/dist/utils/config-store.js +13 -21
- package/dist/utils/config.js +2 -6
- package/mcpb/manifest.json +77 -0
- package/package.json +55 -9
- package/server.json +42 -0
- package/smithery.yaml +10 -0
- package/src/auth/callback.ts +0 -102
- package/src/auth/keychain.ts +0 -16
- package/src/commands/login.ts +0 -202
- package/src/commands/logout.ts +0 -30
- package/src/commands/status.ts +0 -153
- package/src/commands/upgrade.ts +0 -121
- package/src/index.ts +0 -40
- package/src/utils/config-store.ts +0 -46
- package/src/utils/config.ts +0 -65
- package/tsconfig.json +0 -22
|
@@ -0,0 +1,908 @@
|
|
|
1
|
+
// ../mcp/dist/mcp/src/utils.js
|
|
2
|
+
import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
|
|
3
|
+
import { initializeApp, applicationDefault, getApps, getApp } from "firebase-admin/app";
|
|
4
|
+
import { getAuth } from "firebase-admin/auth";
|
|
5
|
+
import { getFirestore } from "firebase-admin/firestore";
|
|
6
|
+
import fs from "fs";
|
|
7
|
+
import path from "path";
|
|
8
|
+
import os from "os";
|
|
9
|
+
var isInitializing = false;
|
|
10
|
+
var isInitialized = false;
|
|
11
|
+
function initFirebase(environment) {
|
|
12
|
+
if (isInitialized) {
|
|
13
|
+
const apps2 = getApps();
|
|
14
|
+
if (apps2 && Array.isArray(apps2) && apps2.length > 0) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
isInitialized = false;
|
|
18
|
+
}
|
|
19
|
+
if (isInitializing) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const apps = getApps();
|
|
23
|
+
if (apps && Array.isArray(apps) && apps.length > 0) {
|
|
24
|
+
isInitialized = true;
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
isInitializing = true;
|
|
28
|
+
try {
|
|
29
|
+
let projectId = process.env.GOOGLE_CLOUD_PROJECT || process.env.GCLOUD_PROJECT;
|
|
30
|
+
if (!projectId && environment) {
|
|
31
|
+
projectId = environment === "staging" ? "cutline-staging" : "cutline-prod";
|
|
32
|
+
}
|
|
33
|
+
if (!projectId) {
|
|
34
|
+
try {
|
|
35
|
+
const configPath = path.join(os.homedir(), ".cutline-mcp", "config.json");
|
|
36
|
+
if (fs.existsSync(configPath)) {
|
|
37
|
+
const content = fs.readFileSync(configPath, "utf-8");
|
|
38
|
+
const config = JSON.parse(content);
|
|
39
|
+
if (config.environment === "staging") {
|
|
40
|
+
projectId = "cutline-staging";
|
|
41
|
+
} else if (config.environment === "production") {
|
|
42
|
+
projectId = "cutline-prod";
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
} catch (e) {
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
projectId = projectId || "cutline-prod";
|
|
49
|
+
try {
|
|
50
|
+
const app = initializeApp({
|
|
51
|
+
projectId,
|
|
52
|
+
credential: applicationDefault()
|
|
53
|
+
});
|
|
54
|
+
getFirestore(app).settings({ ignoreUndefinedProperties: true });
|
|
55
|
+
isInitialized = true;
|
|
56
|
+
} catch (error) {
|
|
57
|
+
if (error?.code === "app/already-exists" || error?.message?.includes("already exists")) {
|
|
58
|
+
isInitialized = true;
|
|
59
|
+
} else {
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
} finally {
|
|
64
|
+
isInitializing = false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
var MAX_REQUEST_SIZE = 10 * 1024 * 1024;
|
|
68
|
+
var MAX_JSON_STRING_LENGTH = 50 * 1024 * 1024;
|
|
69
|
+
function validateRequestSize(args) {
|
|
70
|
+
if (!args || typeof args !== "object") {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
const jsonString = JSON.stringify(args);
|
|
75
|
+
if (jsonString.length > MAX_JSON_STRING_LENGTH) {
|
|
76
|
+
throw new McpError(ErrorCode.InvalidParams, `Request payload too large. Maximum size is ${MAX_REQUEST_SIZE / (1024 * 1024)}MB`);
|
|
77
|
+
}
|
|
78
|
+
} catch (error) {
|
|
79
|
+
if (error instanceof McpError)
|
|
80
|
+
throw error;
|
|
81
|
+
console.error("[MCP] Failed to validate request size:", error);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function mapErrorToMcp(error, context) {
|
|
85
|
+
const errorId = `err-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
86
|
+
const isClientError = error instanceof McpError || error?.code && typeof error.code === "string" && error.code.startsWith("4");
|
|
87
|
+
console.error("Operation failed:", {
|
|
88
|
+
errorId,
|
|
89
|
+
tool: context?.tool,
|
|
90
|
+
operation: context?.operation,
|
|
91
|
+
error: error instanceof Error ? {
|
|
92
|
+
name: error.name,
|
|
93
|
+
message: error.message,
|
|
94
|
+
stack: error.stack
|
|
95
|
+
} : String(error)
|
|
96
|
+
});
|
|
97
|
+
if (error instanceof McpError) {
|
|
98
|
+
return error;
|
|
99
|
+
}
|
|
100
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
101
|
+
const sanitizedMessage = sanitizeErrorMessage(message);
|
|
102
|
+
if (message.includes("NOT_FOUND") || message.includes("404")) {
|
|
103
|
+
return new McpError(ErrorCode.InvalidRequest, `Resource not found`);
|
|
104
|
+
}
|
|
105
|
+
if (message.includes("PERMISSION_DENIED") || message.includes("403")) {
|
|
106
|
+
return new McpError(ErrorCode.InvalidRequest, `Permission denied`);
|
|
107
|
+
}
|
|
108
|
+
if (message.includes("UNAUTHENTICATED") || message.includes("401")) {
|
|
109
|
+
return new McpError(ErrorCode.InvalidRequest, `Unauthenticated`);
|
|
110
|
+
}
|
|
111
|
+
if (isClientError) {
|
|
112
|
+
return new McpError(ErrorCode.InvalidParams, sanitizedMessage);
|
|
113
|
+
}
|
|
114
|
+
return new McpError(ErrorCode.InternalError, `Internal error: ${sanitizedMessage}`);
|
|
115
|
+
}
|
|
116
|
+
function sanitizeErrorMessage(message) {
|
|
117
|
+
let sanitized = message.replace(/\/[^\s]+/g, "[path]").replace(/[^\s]+@[^\s]+/g, "[email]").replace(/[A-Za-z0-9_-]{40,}/g, "[token]").slice(0, 500);
|
|
118
|
+
return sanitized || "An error occurred";
|
|
119
|
+
}
|
|
120
|
+
async function validateAuth(authToken) {
|
|
121
|
+
if (!authToken)
|
|
122
|
+
return null;
|
|
123
|
+
try {
|
|
124
|
+
const apps = getApps();
|
|
125
|
+
const app = apps.length > 0 ? getApp() : void 0;
|
|
126
|
+
if (!app) {
|
|
127
|
+
throw new Error("Firebase Admin not initialized");
|
|
128
|
+
}
|
|
129
|
+
const auth = getAuth(app);
|
|
130
|
+
return await auth.verifyIdToken(authToken);
|
|
131
|
+
} catch (e) {
|
|
132
|
+
const errorMsg = e?.message || String(e);
|
|
133
|
+
console.error("[MCP Auth] Token verification failed:", errorMsg.slice(0, 200));
|
|
134
|
+
throw new McpError(ErrorCode.InvalidRequest, `Invalid auth token: ${errorMsg.slice(0, 100)}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
async function validateSubscription(uid) {
|
|
138
|
+
try {
|
|
139
|
+
const apps = getApps();
|
|
140
|
+
const app = apps.length > 0 ? getApp() : void 0;
|
|
141
|
+
if (!app) {
|
|
142
|
+
console.error("[MCP Auth] Firebase Admin not initialized for subscription check");
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
const firestore = getFirestore(app);
|
|
146
|
+
const userDoc = await firestore.collection("users").doc(uid).get();
|
|
147
|
+
const subscription = userDoc.data()?.subscription;
|
|
148
|
+
if (!subscription) {
|
|
149
|
+
console.error(`[MCP Auth] No subscription found for user ${uid}`);
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
const validStatuses = ["active", "trialing"];
|
|
153
|
+
const isValid = validStatuses.includes(subscription.status);
|
|
154
|
+
if (!isValid) {
|
|
155
|
+
console.error(`[MCP Auth] Subscription status is ${subscription.status}, expected one of: ${validStatuses.join(", ")}`);
|
|
156
|
+
}
|
|
157
|
+
return isValid;
|
|
158
|
+
} catch (e) {
|
|
159
|
+
console.error("[MCP Auth] Error checking subscription:", e);
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
async function requirePremium(authToken) {
|
|
164
|
+
const decoded = await validateAuth(authToken);
|
|
165
|
+
if (!decoded) {
|
|
166
|
+
throw new McpError(ErrorCode.InvalidRequest, "Authentication required. Run 'cutline-mcp login' to authenticate.");
|
|
167
|
+
}
|
|
168
|
+
const hasSubscription = await validateSubscription(decoded.uid);
|
|
169
|
+
if (!hasSubscription) {
|
|
170
|
+
throw new McpError(ErrorCode.InvalidRequest, "Premium subscription required. Please upgrade at https://thecutline.ai/upgrade");
|
|
171
|
+
}
|
|
172
|
+
return decoded;
|
|
173
|
+
}
|
|
174
|
+
async function resolveAuthContext(authToken) {
|
|
175
|
+
const decoded = await requirePremiumWithAutoAuth(authToken);
|
|
176
|
+
const accountType = decoded.accountType;
|
|
177
|
+
const ownerUid = decoded.ownerUid;
|
|
178
|
+
if (accountType === "agent") {
|
|
179
|
+
if (!ownerUid) {
|
|
180
|
+
throw new McpError(ErrorCode.InvalidRequest, "Agent account is misconfigured: missing ownerUid claim.");
|
|
181
|
+
}
|
|
182
|
+
const ownerHasSubscription = await validateSubscription(ownerUid);
|
|
183
|
+
if (!ownerHasSubscription) {
|
|
184
|
+
throw new McpError(ErrorCode.InvalidRequest, "The owner account's premium subscription is inactive. Agent access requires an active owner subscription.");
|
|
185
|
+
}
|
|
186
|
+
return { decoded, isAgent: true, effectiveUid: ownerUid };
|
|
187
|
+
}
|
|
188
|
+
return { decoded, isAgent: false, effectiveUid: decoded.uid };
|
|
189
|
+
}
|
|
190
|
+
var MAX_CACHE_SIZE = 100;
|
|
191
|
+
var CACHE_CLEANUP_INTERVAL = 5 * 60 * 1e3;
|
|
192
|
+
var tokenCache = /* @__PURE__ */ new Map();
|
|
193
|
+
var lastCleanup = Date.now();
|
|
194
|
+
function cleanupTokenCache() {
|
|
195
|
+
const now = Date.now();
|
|
196
|
+
for (const [key, value] of tokenCache.entries()) {
|
|
197
|
+
if (now >= value.expiresAt) {
|
|
198
|
+
tokenCache.delete(key);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (tokenCache.size > MAX_CACHE_SIZE) {
|
|
202
|
+
const entriesToRemove = tokenCache.size - MAX_CACHE_SIZE;
|
|
203
|
+
const keysToRemove = [];
|
|
204
|
+
for (const key of tokenCache.keys()) {
|
|
205
|
+
if (keysToRemove.length >= entriesToRemove)
|
|
206
|
+
break;
|
|
207
|
+
keysToRemove.push(key);
|
|
208
|
+
}
|
|
209
|
+
keysToRemove.forEach((key) => tokenCache.delete(key));
|
|
210
|
+
}
|
|
211
|
+
lastCleanup = now;
|
|
212
|
+
}
|
|
213
|
+
function getCachedToken(refreshToken) {
|
|
214
|
+
if (Date.now() - lastCleanup > CACHE_CLEANUP_INTERVAL) {
|
|
215
|
+
cleanupTokenCache();
|
|
216
|
+
}
|
|
217
|
+
const cached = tokenCache.get(refreshToken);
|
|
218
|
+
if (!cached) {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
if (Date.now() >= cached.expiresAt) {
|
|
222
|
+
tokenCache.delete(refreshToken);
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
cached.lastUsed = Date.now();
|
|
226
|
+
tokenCache.delete(refreshToken);
|
|
227
|
+
tokenCache.set(refreshToken, cached);
|
|
228
|
+
return cached.idToken;
|
|
229
|
+
}
|
|
230
|
+
function setCachedToken(refreshToken, idToken, expiresAt) {
|
|
231
|
+
if (tokenCache.size >= MAX_CACHE_SIZE) {
|
|
232
|
+
cleanupTokenCache();
|
|
233
|
+
}
|
|
234
|
+
tokenCache.delete(refreshToken);
|
|
235
|
+
tokenCache.set(refreshToken, {
|
|
236
|
+
idToken,
|
|
237
|
+
expiresAt,
|
|
238
|
+
lastUsed: Date.now()
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
var cachedApiKey = null;
|
|
242
|
+
var API_KEY_CACHE_TTL = 36e5;
|
|
243
|
+
async function getFirebaseApiKey(environment) {
|
|
244
|
+
const env = environment || "production";
|
|
245
|
+
if (env === "staging") {
|
|
246
|
+
if (process.env.FIREBASE_API_KEY_STAGING) {
|
|
247
|
+
return process.env.FIREBASE_API_KEY_STAGING;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
const apiKey = process.env.FIREBASE_API_KEY || process.env.NEXT_PUBLIC_FIREBASE_API_KEY;
|
|
251
|
+
if (apiKey) {
|
|
252
|
+
return apiKey;
|
|
253
|
+
}
|
|
254
|
+
if (cachedApiKey && cachedApiKey.environment === env && Date.now() - cachedApiKey.fetchedAt < API_KEY_CACHE_TTL) {
|
|
255
|
+
return cachedApiKey.key;
|
|
256
|
+
}
|
|
257
|
+
const baseUrl = env === "staging" ? "https://cutline-staging.web.app" : "https://thecutline.ai";
|
|
258
|
+
try {
|
|
259
|
+
const response = await fetch(`${baseUrl}/api/firebase-config`, {
|
|
260
|
+
headers: { "Accept": "application/json" },
|
|
261
|
+
signal: (() => {
|
|
262
|
+
const controller = new AbortController();
|
|
263
|
+
setTimeout(() => controller.abort(), 5e3);
|
|
264
|
+
return controller.signal;
|
|
265
|
+
})()
|
|
266
|
+
});
|
|
267
|
+
if (!response.ok) {
|
|
268
|
+
throw new Error(`HTTP ${response.status}`);
|
|
269
|
+
}
|
|
270
|
+
const data = await response.json();
|
|
271
|
+
if (!data.apiKey) {
|
|
272
|
+
throw new Error("No apiKey in response");
|
|
273
|
+
}
|
|
274
|
+
cachedApiKey = { key: data.apiKey, environment: env, fetchedAt: Date.now() };
|
|
275
|
+
return data.apiKey;
|
|
276
|
+
} catch (error) {
|
|
277
|
+
throw new Error(`Firebase API key not found. Set FIREBASE_API_KEY environment variable or ensure ${baseUrl}/api/firebase-config is accessible.`);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
async function exchangeRefreshToken(refreshToken, firebaseApiKey, maxRetries = 3) {
|
|
281
|
+
const FIREBASE_API_KEY = firebaseApiKey || await getFirebaseApiKey();
|
|
282
|
+
let lastError;
|
|
283
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
284
|
+
try {
|
|
285
|
+
const response = await fetch(`https://securetoken.googleapis.com/v1/token?key=${FIREBASE_API_KEY}`, {
|
|
286
|
+
method: "POST",
|
|
287
|
+
headers: { "Content-Type": "application/json" },
|
|
288
|
+
body: JSON.stringify({
|
|
289
|
+
grant_type: "refresh_token",
|
|
290
|
+
refresh_token: refreshToken
|
|
291
|
+
}),
|
|
292
|
+
// Add timeout to prevent hanging requests
|
|
293
|
+
signal: (() => {
|
|
294
|
+
const controller = new AbortController();
|
|
295
|
+
setTimeout(() => controller.abort(), 1e4);
|
|
296
|
+
return controller.signal;
|
|
297
|
+
})()
|
|
298
|
+
});
|
|
299
|
+
if (!response.ok) {
|
|
300
|
+
const error = await response.json();
|
|
301
|
+
const errorMessage = error.error?.message || "Unknown error";
|
|
302
|
+
if (response.status >= 400 && response.status < 500) {
|
|
303
|
+
throw new Error(`Token exchange failed: ${errorMessage}`);
|
|
304
|
+
}
|
|
305
|
+
lastError = new Error(`Token exchange failed (attempt ${attempt + 1}/${maxRetries}): ${errorMessage}`);
|
|
306
|
+
if (attempt < maxRetries - 1) {
|
|
307
|
+
await new Promise((resolve) => setTimeout(resolve, 500 * Math.pow(2, attempt)));
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
throw lastError;
|
|
311
|
+
}
|
|
312
|
+
const data = await response.json();
|
|
313
|
+
const idToken = data.id_token;
|
|
314
|
+
try {
|
|
315
|
+
const parts = idToken.split(".");
|
|
316
|
+
if (parts.length === 3) {
|
|
317
|
+
const payload = JSON.parse(Buffer.from(parts[1], "base64").toString());
|
|
318
|
+
const tokenProjectId = payload.aud || payload.project_id;
|
|
319
|
+
console.error(`[MCP Auth] Token exchanged successfully for project: ${tokenProjectId}`);
|
|
320
|
+
}
|
|
321
|
+
} catch (e) {
|
|
322
|
+
}
|
|
323
|
+
return idToken;
|
|
324
|
+
} catch (error) {
|
|
325
|
+
if (error.name === "AbortError" || error.name === "TypeError" || error.message?.includes("fetch")) {
|
|
326
|
+
lastError = error;
|
|
327
|
+
if (attempt < maxRetries - 1) {
|
|
328
|
+
await new Promise((resolve) => setTimeout(resolve, 500 * Math.pow(2, attempt)));
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
} else {
|
|
332
|
+
throw error;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
throw lastError || new Error("Token exchange failed after retries");
|
|
337
|
+
}
|
|
338
|
+
async function getStoredToken() {
|
|
339
|
+
if (process.env.CUTLINE_MCP_REFRESH_TOKEN) {
|
|
340
|
+
console.error("[MCP Auth] Using token from CUTLINE_MCP_REFRESH_TOKEN env var");
|
|
341
|
+
return {
|
|
342
|
+
refreshToken: process.env.CUTLINE_MCP_REFRESH_TOKEN,
|
|
343
|
+
environment: process.env.CUTLINE_ENV === "staging" ? "staging" : "production"
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
try {
|
|
347
|
+
const configPath = path.join(os.homedir(), ".cutline-mcp", "config.json");
|
|
348
|
+
if (fs.existsSync(configPath)) {
|
|
349
|
+
const content = fs.readFileSync(configPath, "utf-8");
|
|
350
|
+
const config = JSON.parse(content);
|
|
351
|
+
if (config.refreshToken) {
|
|
352
|
+
console.error("[MCP Auth] Using token from ~/.cutline-mcp/config.json", config.environment ? `(environment: ${config.environment})` : "");
|
|
353
|
+
return {
|
|
354
|
+
refreshToken: config.refreshToken,
|
|
355
|
+
environment: config.environment
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
} catch (e) {
|
|
360
|
+
console.error("[MCP Auth] Failed to read config file:", e);
|
|
361
|
+
}
|
|
362
|
+
try {
|
|
363
|
+
console.error("[MCP Auth Debug] Attempting to import keytar...");
|
|
364
|
+
const keytar = await import("keytar");
|
|
365
|
+
console.error("[MCP Auth Debug] Keytar imported successfully, getting password...");
|
|
366
|
+
const token = await keytar.getPassword("cutline-mcp", "refresh-token");
|
|
367
|
+
console.error("[MCP Auth Debug] Token retrieved:", token ? "YES (length: " + token.length + ")" : "NO TOKEN FOUND");
|
|
368
|
+
if (token) {
|
|
369
|
+
return { refreshToken: token };
|
|
370
|
+
}
|
|
371
|
+
} catch (error) {
|
|
372
|
+
console.error("[MCP Auth Error] Failed to access keychain:", error);
|
|
373
|
+
}
|
|
374
|
+
return null;
|
|
375
|
+
}
|
|
376
|
+
async function requirePremiumWithAutoAuth(authToken) {
|
|
377
|
+
let idToken = authToken;
|
|
378
|
+
if (!idToken) {
|
|
379
|
+
const storedTokenInfo = await getStoredToken();
|
|
380
|
+
if (storedTokenInfo?.environment) {
|
|
381
|
+
initFirebase(storedTokenInfo.environment);
|
|
382
|
+
} else {
|
|
383
|
+
initFirebase();
|
|
384
|
+
}
|
|
385
|
+
if (storedTokenInfo) {
|
|
386
|
+
const { refreshToken, environment } = storedTokenInfo;
|
|
387
|
+
const apiKeyToUse = await getFirebaseApiKey(environment || "production");
|
|
388
|
+
const cached = getCachedToken(refreshToken);
|
|
389
|
+
if (cached) {
|
|
390
|
+
idToken = cached;
|
|
391
|
+
} else {
|
|
392
|
+
try {
|
|
393
|
+
idToken = await exchangeRefreshToken(refreshToken, apiKeyToUse);
|
|
394
|
+
setCachedToken(refreshToken, idToken, Date.now() + 50 * 60 * 1e3);
|
|
395
|
+
} catch (e) {
|
|
396
|
+
console.error("Token exchange failed:", e);
|
|
397
|
+
tokenCache.delete(refreshToken);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
return await requirePremium(idToken);
|
|
403
|
+
}
|
|
404
|
+
async function requireAuthOnly(authToken) {
|
|
405
|
+
let idToken = authToken;
|
|
406
|
+
if (!idToken) {
|
|
407
|
+
const storedTokenInfo = await getStoredToken();
|
|
408
|
+
if (storedTokenInfo?.environment) {
|
|
409
|
+
initFirebase(storedTokenInfo.environment);
|
|
410
|
+
} else {
|
|
411
|
+
initFirebase();
|
|
412
|
+
}
|
|
413
|
+
if (storedTokenInfo) {
|
|
414
|
+
const { refreshToken, environment } = storedTokenInfo;
|
|
415
|
+
const apiKeyToUse = await getFirebaseApiKey(environment || "production");
|
|
416
|
+
const cached = getCachedToken(refreshToken);
|
|
417
|
+
if (cached) {
|
|
418
|
+
idToken = cached;
|
|
419
|
+
} else {
|
|
420
|
+
try {
|
|
421
|
+
idToken = await exchangeRefreshToken(refreshToken, apiKeyToUse);
|
|
422
|
+
setCachedToken(refreshToken, idToken, Date.now() + 50 * 60 * 1e3);
|
|
423
|
+
} catch (e) {
|
|
424
|
+
console.error("Token exchange failed:", e);
|
|
425
|
+
tokenCache.delete(refreshToken);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
const decoded = await validateAuth(idToken);
|
|
431
|
+
if (!decoded) {
|
|
432
|
+
throw new McpError(ErrorCode.InvalidRequest, "Authentication required. Run 'cutline-mcp login' to authenticate.");
|
|
433
|
+
}
|
|
434
|
+
return decoded;
|
|
435
|
+
}
|
|
436
|
+
async function resolveAuthContextFree(authToken) {
|
|
437
|
+
const decoded = await requireAuthOnly(authToken);
|
|
438
|
+
const accountType = decoded.accountType;
|
|
439
|
+
const ownerUid = decoded.ownerUid;
|
|
440
|
+
if (accountType === "agent" && ownerUid) {
|
|
441
|
+
return { decoded, isAgent: true, effectiveUid: ownerUid };
|
|
442
|
+
}
|
|
443
|
+
return { decoded, isAgent: false, effectiveUid: decoded.uid };
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// ../mcp/dist/mcp/src/shared/sanitize.js
|
|
447
|
+
var NULL_AND_CONTROL_RE = /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g;
|
|
448
|
+
var HTML_TAG_RE = /<\/?[a-z][^>]*>/gi;
|
|
449
|
+
var SCRIPT_RE = /<script[\s>][\s\S]*?<\/script>/gi;
|
|
450
|
+
var EVENT_HANDLER_RE = /\s+on\w+\s*=\s*["'][^"']*["']/gi;
|
|
451
|
+
function stripControlChars(input) {
|
|
452
|
+
return input.replace(NULL_AND_CONTROL_RE, "");
|
|
453
|
+
}
|
|
454
|
+
function stripHtmlTags(input) {
|
|
455
|
+
return input.replace(SCRIPT_RE, "").replace(EVENT_HANDLER_RE, "").replace(HTML_TAG_RE, "");
|
|
456
|
+
}
|
|
457
|
+
var DEFAULT_MAX_LENGTH = 5e4;
|
|
458
|
+
function sanitizeText(input, options = {}) {
|
|
459
|
+
const { maxLength = DEFAULT_MAX_LENGTH, stripHtml = true } = options;
|
|
460
|
+
let result = stripControlChars(input);
|
|
461
|
+
if (stripHtml) {
|
|
462
|
+
result = stripHtmlTags(result);
|
|
463
|
+
}
|
|
464
|
+
result = result.trim();
|
|
465
|
+
if (result.length > maxLength) {
|
|
466
|
+
result = result.slice(0, maxLength);
|
|
467
|
+
}
|
|
468
|
+
return result;
|
|
469
|
+
}
|
|
470
|
+
function sanitizeArgs(value, options = {}) {
|
|
471
|
+
if (typeof value === "string") {
|
|
472
|
+
return sanitizeText(value, options);
|
|
473
|
+
}
|
|
474
|
+
if (Array.isArray(value)) {
|
|
475
|
+
return value.map((item) => sanitizeArgs(item, options));
|
|
476
|
+
}
|
|
477
|
+
if (value !== null && typeof value === "object" && !(value instanceof Date)) {
|
|
478
|
+
const result = {};
|
|
479
|
+
for (const [key, val] of Object.entries(value)) {
|
|
480
|
+
result[key] = sanitizeArgs(val, options);
|
|
481
|
+
}
|
|
482
|
+
return result;
|
|
483
|
+
}
|
|
484
|
+
return value;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// ../mcp/dist/mcp/src/shared/pii-scanner.js
|
|
488
|
+
var patterns = [
|
|
489
|
+
// ── Secrets ──────────────────────────────────────────────────────────────
|
|
490
|
+
{
|
|
491
|
+
type: "aws_key",
|
|
492
|
+
pattern: /AKIA[0-9A-Z]{16}/g,
|
|
493
|
+
severity: "critical",
|
|
494
|
+
description: "AWS Access Key ID",
|
|
495
|
+
validate: (m) => m.length === 20
|
|
496
|
+
},
|
|
497
|
+
{
|
|
498
|
+
type: "aws_secret",
|
|
499
|
+
pattern: /(?:aws_secret_access_key|secret_access_key|aws_secret)\s*[=:]\s*[A-Za-z0-9/+=]{40}/g,
|
|
500
|
+
severity: "critical",
|
|
501
|
+
description: "AWS Secret Access Key"
|
|
502
|
+
},
|
|
503
|
+
{
|
|
504
|
+
type: "stripe_key",
|
|
505
|
+
pattern: /[sr]k_(live|test)_[0-9a-zA-Z]{24,}/g,
|
|
506
|
+
severity: "critical",
|
|
507
|
+
description: "Stripe API key"
|
|
508
|
+
},
|
|
509
|
+
{
|
|
510
|
+
type: "stripe_key",
|
|
511
|
+
pattern: /pk_(live|test)_[0-9a-zA-Z]{24,}/g,
|
|
512
|
+
severity: "critical",
|
|
513
|
+
description: "Stripe publishable key"
|
|
514
|
+
},
|
|
515
|
+
{
|
|
516
|
+
type: "github_token",
|
|
517
|
+
pattern: /gh[ps]_[A-Za-z0-9]{36,}/g,
|
|
518
|
+
severity: "critical",
|
|
519
|
+
description: "GitHub personal access token"
|
|
520
|
+
},
|
|
521
|
+
{
|
|
522
|
+
type: "github_token",
|
|
523
|
+
pattern: /github_pat_[A-Za-z0-9_]{20,}/g,
|
|
524
|
+
severity: "critical",
|
|
525
|
+
description: "GitHub fine-grained personal access token"
|
|
526
|
+
},
|
|
527
|
+
{
|
|
528
|
+
type: "bearer_token",
|
|
529
|
+
pattern: /Bearer\s+[A-Za-z0-9\-._~+/]+=*/g,
|
|
530
|
+
severity: "critical",
|
|
531
|
+
description: "Bearer authorization token"
|
|
532
|
+
},
|
|
533
|
+
{
|
|
534
|
+
type: "generic_api_key",
|
|
535
|
+
pattern: /(?:api[_-]?key|apikey|api[_-]?secret|secret[_-]?key)\s*[=:"']\s*["']?([A-Za-z0-9\-._~+/]{20,})["']?/gi,
|
|
536
|
+
severity: "warning",
|
|
537
|
+
description: "Generic API key/secret assignment"
|
|
538
|
+
},
|
|
539
|
+
// ── PII ──────────────────────────────────────────────────────────────────
|
|
540
|
+
{
|
|
541
|
+
type: "credit_card",
|
|
542
|
+
pattern: /\b(?:4[0-9]{3}|5[1-5][0-9]{2}|3[47][0-9]{2}|6(?:011|5[0-9]{2}))[- ]?[0-9]{4}[- ]?[0-9]{4}[- ]?[0-9]{4}\b/g,
|
|
543
|
+
severity: "critical",
|
|
544
|
+
description: "Credit card number (Visa, MC, Amex, Discover)"
|
|
545
|
+
},
|
|
546
|
+
{
|
|
547
|
+
type: "ssn",
|
|
548
|
+
pattern: /(?:ssn|social\s*security(?:\s*number)?|ss#)\s*(?:number\s*)?[:\s]?\s*(\d{3})-?(\d{2})-?(\d{4})/gi,
|
|
549
|
+
severity: "critical",
|
|
550
|
+
description: "US Social Security Number (context-required)",
|
|
551
|
+
validate: (_m, _full, _idx) => true
|
|
552
|
+
},
|
|
553
|
+
{
|
|
554
|
+
type: "phone_number",
|
|
555
|
+
pattern: /(?:\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]\d{3}[-.\s]\d{4}/g,
|
|
556
|
+
severity: "warning",
|
|
557
|
+
description: "US phone number"
|
|
558
|
+
},
|
|
559
|
+
{
|
|
560
|
+
type: "email",
|
|
561
|
+
pattern: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g,
|
|
562
|
+
severity: "info",
|
|
563
|
+
description: "Email address"
|
|
564
|
+
}
|
|
565
|
+
];
|
|
566
|
+
var DEFAULT_PATTERNS = Object.freeze(patterns.map((p) => ({ ...p })));
|
|
567
|
+
var REDACT_LABELS = {
|
|
568
|
+
aws_key: "[AWS_KEY]",
|
|
569
|
+
aws_secret: "[AWS_SECRET]",
|
|
570
|
+
stripe_key: "[STRIPE_KEY]",
|
|
571
|
+
github_token: "[GITHUB_TOKEN]",
|
|
572
|
+
bearer_token: "[BEARER_TOKEN]",
|
|
573
|
+
generic_api_key: "[API_KEY]",
|
|
574
|
+
credit_card: "[CREDIT_CARD]",
|
|
575
|
+
ssn: "[SSN]",
|
|
576
|
+
phone_number: "[PHONE]",
|
|
577
|
+
email: "[EMAIL]"
|
|
578
|
+
};
|
|
579
|
+
function scanForPii(input) {
|
|
580
|
+
return runScan(input, patterns);
|
|
581
|
+
}
|
|
582
|
+
function runScan(input, defs) {
|
|
583
|
+
if (!input) {
|
|
584
|
+
return { matches: [], flags_by_type: {}, scanned_chars: 0, redacted: "" };
|
|
585
|
+
}
|
|
586
|
+
const allMatches = [];
|
|
587
|
+
for (const def of defs) {
|
|
588
|
+
const re = new RegExp(def.pattern.source, def.pattern.flags);
|
|
589
|
+
let m;
|
|
590
|
+
while ((m = re.exec(input)) !== null) {
|
|
591
|
+
const text = m[0];
|
|
592
|
+
const start = m.index;
|
|
593
|
+
const end = start + text.length;
|
|
594
|
+
if (def.validate && !def.validate(text, input, start)) {
|
|
595
|
+
continue;
|
|
596
|
+
}
|
|
597
|
+
allMatches.push({
|
|
598
|
+
type: def.type,
|
|
599
|
+
text,
|
|
600
|
+
start,
|
|
601
|
+
end,
|
|
602
|
+
severity: def.severity
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
allMatches.sort((a, b) => a.start - b.start);
|
|
607
|
+
const deduped = deduplicateOverlaps(allMatches);
|
|
608
|
+
const flags_by_type = {};
|
|
609
|
+
for (const match of deduped) {
|
|
610
|
+
flags_by_type[match.type] = (flags_by_type[match.type] || 0) + 1;
|
|
611
|
+
}
|
|
612
|
+
const redacted = buildRedacted(input, deduped);
|
|
613
|
+
return {
|
|
614
|
+
matches: deduped,
|
|
615
|
+
flags_by_type,
|
|
616
|
+
scanned_chars: input.length,
|
|
617
|
+
redacted
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
function deduplicateOverlaps(sorted) {
|
|
621
|
+
if (sorted.length <= 1)
|
|
622
|
+
return sorted;
|
|
623
|
+
const severityRank = {
|
|
624
|
+
critical: 3,
|
|
625
|
+
warning: 2,
|
|
626
|
+
info: 1
|
|
627
|
+
};
|
|
628
|
+
const result = [sorted[0]];
|
|
629
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
630
|
+
const prev = result[result.length - 1];
|
|
631
|
+
const curr = sorted[i];
|
|
632
|
+
if (curr.start < prev.end) {
|
|
633
|
+
const prevRank = severityRank[prev.severity];
|
|
634
|
+
const currRank = severityRank[curr.severity];
|
|
635
|
+
if (currRank > prevRank || currRank === prevRank && curr.end - curr.start > prev.end - prev.start) {
|
|
636
|
+
result[result.length - 1] = curr;
|
|
637
|
+
}
|
|
638
|
+
} else {
|
|
639
|
+
result.push(curr);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
return result;
|
|
643
|
+
}
|
|
644
|
+
function buildRedacted(input, matches) {
|
|
645
|
+
if (matches.length === 0)
|
|
646
|
+
return input;
|
|
647
|
+
const parts = [];
|
|
648
|
+
let cursor = 0;
|
|
649
|
+
for (const match of matches) {
|
|
650
|
+
if (match.start > cursor) {
|
|
651
|
+
parts.push(input.slice(cursor, match.start));
|
|
652
|
+
}
|
|
653
|
+
parts.push(REDACT_LABELS[match.type] || `[${match.type.toUpperCase()}]`);
|
|
654
|
+
cursor = match.end;
|
|
655
|
+
}
|
|
656
|
+
if (cursor < input.length) {
|
|
657
|
+
parts.push(input.slice(cursor));
|
|
658
|
+
}
|
|
659
|
+
return parts.join("");
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// ../mcp/dist/mcp/src/shared/boundary-guard.js
|
|
663
|
+
var DEFAULT_OPTS = { stripHtml: false, maxLength: 1e5 };
|
|
664
|
+
function guardBoundary(toolName, rawArgs, opts = DEFAULT_OPTS) {
|
|
665
|
+
const args = sanitizeArgs(rawArgs, opts);
|
|
666
|
+
const piiResults = [];
|
|
667
|
+
for (const [key, val] of Object.entries(args)) {
|
|
668
|
+
if (typeof val === "string" && val.length > 0) {
|
|
669
|
+
const scan = scanForPii(val);
|
|
670
|
+
if (scan.matches.length > 0) {
|
|
671
|
+
piiResults.push({ field: key, result: scan });
|
|
672
|
+
auditLog("pii_detected", "tool_call", toolName, {
|
|
673
|
+
field: key,
|
|
674
|
+
flags_by_type: scan.flags_by_type,
|
|
675
|
+
match_count: scan.matches.length,
|
|
676
|
+
scanned_chars: scan.scanned_chars
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
return {
|
|
682
|
+
args,
|
|
683
|
+
piiDetected: piiResults.length > 0,
|
|
684
|
+
piiResults
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
function guardOutput(toolName, response) {
|
|
688
|
+
let totalRedactions = 0;
|
|
689
|
+
const content = response.content.map((block) => {
|
|
690
|
+
if (block.type !== "text" || !block.text)
|
|
691
|
+
return block;
|
|
692
|
+
const scan = scanForPii(block.text);
|
|
693
|
+
if (scan.matches.length === 0)
|
|
694
|
+
return block;
|
|
695
|
+
totalRedactions += scan.matches.length;
|
|
696
|
+
return { ...block, text: scan.redacted };
|
|
697
|
+
});
|
|
698
|
+
if (totalRedactions > 0) {
|
|
699
|
+
auditLog("secret_in_output", "tool_response", toolName, {
|
|
700
|
+
redaction_count: totalRedactions
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
return {
|
|
704
|
+
...response,
|
|
705
|
+
content,
|
|
706
|
+
_secretsRedacted: totalRedactions > 0,
|
|
707
|
+
_redactionCount: totalRedactions
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
function auditLog(eventType, resourceType, resourceId, data) {
|
|
711
|
+
console.error(JSON.stringify({
|
|
712
|
+
audit: true,
|
|
713
|
+
severity: "INFO",
|
|
714
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
715
|
+
eventType,
|
|
716
|
+
resourceType,
|
|
717
|
+
resourceId,
|
|
718
|
+
data
|
|
719
|
+
}));
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// ../mcp/dist/mcp/src/shared/perf-tracker.js
|
|
723
|
+
var PerfTracker = class {
|
|
724
|
+
entries = /* @__PURE__ */ new Map();
|
|
725
|
+
windowMs;
|
|
726
|
+
failureAlertThreshold;
|
|
727
|
+
onFailureAlert;
|
|
728
|
+
latencyThresholds;
|
|
729
|
+
maxEntriesPerTool;
|
|
730
|
+
startedAt = Date.now();
|
|
731
|
+
alertActive = false;
|
|
732
|
+
constructor(options = {}) {
|
|
733
|
+
this.windowMs = options.windowMs ?? 5 * 60 * 1e3;
|
|
734
|
+
this.failureAlertThreshold = options.failureAlertThreshold ?? 0.05;
|
|
735
|
+
this.onFailureAlert = options.onFailureAlert;
|
|
736
|
+
this.latencyThresholds = options.latencyThresholds ?? [];
|
|
737
|
+
this.maxEntriesPerTool = options.maxEntriesPerTool ?? 1e4;
|
|
738
|
+
}
|
|
739
|
+
record(tool, latencyMs, success) {
|
|
740
|
+
if (!this.entries.has(tool)) {
|
|
741
|
+
this.entries.set(tool, []);
|
|
742
|
+
}
|
|
743
|
+
const bucket = this.entries.get(tool);
|
|
744
|
+
bucket.push({ timestamp: Date.now(), latencyMs, success });
|
|
745
|
+
if (bucket.length > this.maxEntriesPerTool) {
|
|
746
|
+
bucket.splice(0, bucket.length - this.maxEntriesPerTool);
|
|
747
|
+
}
|
|
748
|
+
this.checkFailureAlert();
|
|
749
|
+
}
|
|
750
|
+
getToolMetrics(tool) {
|
|
751
|
+
const active = this.getActiveEntries(tool);
|
|
752
|
+
if (!active || active.length === 0)
|
|
753
|
+
return void 0;
|
|
754
|
+
return this.computeMetrics(active);
|
|
755
|
+
}
|
|
756
|
+
getSnapshot() {
|
|
757
|
+
const tools = {};
|
|
758
|
+
let totalCalls = 0;
|
|
759
|
+
let totalFailures = 0;
|
|
760
|
+
for (const tool of this.entries.keys()) {
|
|
761
|
+
const active = this.getActiveEntries(tool);
|
|
762
|
+
if (active.length === 0)
|
|
763
|
+
continue;
|
|
764
|
+
const metrics = this.computeMetrics(active);
|
|
765
|
+
tools[tool] = metrics;
|
|
766
|
+
totalCalls += metrics.totalCalls;
|
|
767
|
+
totalFailures += metrics.failureCount;
|
|
768
|
+
}
|
|
769
|
+
return {
|
|
770
|
+
tools,
|
|
771
|
+
totalCalls,
|
|
772
|
+
globalFailureRate: totalCalls > 0 ? totalFailures / totalCalls : 0,
|
|
773
|
+
uptimeMs: Date.now() - this.startedAt,
|
|
774
|
+
windowMs: this.windowMs
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
getThresholdBreaches() {
|
|
778
|
+
const breaches = [];
|
|
779
|
+
for (const threshold of this.latencyThresholds) {
|
|
780
|
+
const metrics = this.getToolMetrics(threshold.tool);
|
|
781
|
+
if (!metrics)
|
|
782
|
+
continue;
|
|
783
|
+
const actual = metrics[threshold.percentile];
|
|
784
|
+
if (actual > threshold.maxMs) {
|
|
785
|
+
breaches.push({ ...threshold, actual });
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
return breaches;
|
|
789
|
+
}
|
|
790
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
791
|
+
// INTERNALS
|
|
792
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
793
|
+
getActiveEntries(tool) {
|
|
794
|
+
const bucket = this.entries.get(tool);
|
|
795
|
+
if (!bucket)
|
|
796
|
+
return [];
|
|
797
|
+
const cutoff = Date.now() - this.windowMs;
|
|
798
|
+
const active = bucket.filter((e) => e.timestamp >= cutoff);
|
|
799
|
+
this.entries.set(tool, active);
|
|
800
|
+
return active;
|
|
801
|
+
}
|
|
802
|
+
computeMetrics(entries) {
|
|
803
|
+
const latencies = entries.map((e) => e.latencyMs).sort((a, b) => a - b);
|
|
804
|
+
const successCount = entries.filter((e) => e.success).length;
|
|
805
|
+
const failureCount = entries.length - successCount;
|
|
806
|
+
const sum = latencies.reduce((a, b) => a + b, 0);
|
|
807
|
+
return {
|
|
808
|
+
totalCalls: entries.length,
|
|
809
|
+
successCount,
|
|
810
|
+
failureCount,
|
|
811
|
+
failureRate: entries.length > 0 ? failureCount / entries.length : 0,
|
|
812
|
+
p50: percentile(latencies, 0.5),
|
|
813
|
+
p95: percentile(latencies, 0.95),
|
|
814
|
+
p99: percentile(latencies, 0.99),
|
|
815
|
+
avgMs: Math.round(sum / entries.length),
|
|
816
|
+
minMs: latencies[0],
|
|
817
|
+
maxMs: latencies[latencies.length - 1]
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
checkFailureAlert() {
|
|
821
|
+
const snap = this.getSnapshot();
|
|
822
|
+
if (snap.totalCalls === 0)
|
|
823
|
+
return;
|
|
824
|
+
if (snap.globalFailureRate > this.failureAlertThreshold) {
|
|
825
|
+
if (!this.alertActive) {
|
|
826
|
+
this.alertActive = true;
|
|
827
|
+
auditLog2("perf_failure_alert", snap);
|
|
828
|
+
this.onFailureAlert?.(snap);
|
|
829
|
+
}
|
|
830
|
+
} else {
|
|
831
|
+
this.alertActive = false;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
};
|
|
835
|
+
function percentile(sorted, p) {
|
|
836
|
+
if (sorted.length === 0)
|
|
837
|
+
return 0;
|
|
838
|
+
if (sorted.length === 1)
|
|
839
|
+
return sorted[0];
|
|
840
|
+
const idx = (sorted.length - 1) * p;
|
|
841
|
+
const lo = Math.floor(idx);
|
|
842
|
+
const hi = Math.ceil(idx);
|
|
843
|
+
if (lo === hi)
|
|
844
|
+
return sorted[lo];
|
|
845
|
+
return sorted[lo] + (sorted[hi] - sorted[lo]) * (idx - lo);
|
|
846
|
+
}
|
|
847
|
+
function auditLog2(eventType, snapshot) {
|
|
848
|
+
const toolSummary = {};
|
|
849
|
+
for (const [tool, m] of Object.entries(snapshot.tools)) {
|
|
850
|
+
toolSummary[tool] = { calls: m.totalCalls, failRate: m.failureRate, p95: m.p95 };
|
|
851
|
+
}
|
|
852
|
+
console.error(JSON.stringify({
|
|
853
|
+
audit: true,
|
|
854
|
+
severity: "ERROR",
|
|
855
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
856
|
+
eventType,
|
|
857
|
+
resourceType: "mcp_perf",
|
|
858
|
+
data: {
|
|
859
|
+
globalFailureRate: snapshot.globalFailureRate,
|
|
860
|
+
totalCalls: snapshot.totalCalls,
|
|
861
|
+
tools: toolSummary
|
|
862
|
+
}
|
|
863
|
+
}));
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// ../mcp/dist/mcp/src/shared/perf-tracker-instance.js
|
|
867
|
+
var LATENCY_THRESHOLDS = [
|
|
868
|
+
{ tool: "exploration_start", percentile: "p99", maxMs: 500 },
|
|
869
|
+
{ tool: "discovery_start", percentile: "p99", maxMs: 500 },
|
|
870
|
+
{ tool: "constraints_query", percentile: "p95", maxMs: 800 },
|
|
871
|
+
{ tool: "constraints_semantic_query", percentile: "p95", maxMs: 800 },
|
|
872
|
+
{ tool: "template_discover", percentile: "p95", maxMs: 500 },
|
|
873
|
+
{ tool: "template_list", percentile: "p95", maxMs: 500 }
|
|
874
|
+
];
|
|
875
|
+
var perfTracker = new PerfTracker({
|
|
876
|
+
windowMs: 5 * 60 * 1e3,
|
|
877
|
+
failureAlertThreshold: 0.05,
|
|
878
|
+
latencyThresholds: LATENCY_THRESHOLDS,
|
|
879
|
+
maxEntriesPerTool: 5e3
|
|
880
|
+
});
|
|
881
|
+
async function withPerfTracking(toolName, fn) {
|
|
882
|
+
const start = Date.now();
|
|
883
|
+
let success = true;
|
|
884
|
+
try {
|
|
885
|
+
return await fn();
|
|
886
|
+
} catch (err) {
|
|
887
|
+
success = false;
|
|
888
|
+
throw err;
|
|
889
|
+
} finally {
|
|
890
|
+
perfTracker.record(toolName, Date.now() - start, success);
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
export {
|
|
895
|
+
initFirebase,
|
|
896
|
+
validateRequestSize,
|
|
897
|
+
mapErrorToMcp,
|
|
898
|
+
validateAuth,
|
|
899
|
+
validateSubscription,
|
|
900
|
+
resolveAuthContext,
|
|
901
|
+
getStoredToken,
|
|
902
|
+
requirePremiumWithAutoAuth,
|
|
903
|
+
resolveAuthContextFree,
|
|
904
|
+
guardBoundary,
|
|
905
|
+
guardOutput,
|
|
906
|
+
perfTracker,
|
|
907
|
+
withPerfTracking
|
|
908
|
+
};
|