mcp-perforce-server 2.1.2 → 3.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 +24 -1
- package/dist/p4/parse.d.ts +18 -2
- package/dist/p4/parse.d.ts.map +1 -1
- package/dist/p4/parse.js +162 -1
- package/dist/p4/parse.js.map +1 -1
- package/dist/server.d.ts +17 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +496 -18
- package/dist/server.js.map +1 -1
- package/dist/tools/basic.d.ts +41 -0
- package/dist/tools/basic.d.ts.map +1 -1
- package/dist/tools/basic.js +285 -0
- package/dist/tools/basic.js.map +1 -1
- package/dist/tools/changelist.d.ts +2 -0
- package/dist/tools/changelist.d.ts.map +1 -1
- package/dist/tools/changelist.js +19 -1
- package/dist/tools/changelist.js.map +1 -1
- package/dist/tools/index.d.ts +2 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +11 -1
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/workflows.d.ts +40 -0
- package/dist/tools/workflows.d.ts.map +1 -0
- package/dist/tools/workflows.js +475 -0
- package/dist/tools/workflows.js.map +1 -0
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -77,7 +77,15 @@ const TOOL_HANDLERS = {
|
|
|
77
77
|
'p4.shelve': tools.p4Shelve,
|
|
78
78
|
'p4.unshelve': tools.p4Unshelve,
|
|
79
79
|
'p4.changes': tools.p4Changes,
|
|
80
|
+
'p4.review': tools.p4Review,
|
|
81
|
+
'p4.reviews': tools.p4Reviews,
|
|
82
|
+
'p4.interchanges': tools.p4Interchanges,
|
|
83
|
+
'p4.integrated': tools.p4Integrated,
|
|
84
|
+
'p4.review.bundle': tools.p4ReviewBundle,
|
|
85
|
+
'p4.change.inspect': tools.p4ChangeInspect,
|
|
86
|
+
'p4.path.synccheck': tools.p4PathSyncCheck,
|
|
80
87
|
'p4.blame': tools.p4Blame,
|
|
88
|
+
'p4.annotate': tools.p4Annotate,
|
|
81
89
|
'p4.copy': tools.p4Copy,
|
|
82
90
|
'p4.move': tools.p4Move,
|
|
83
91
|
'p4.integrate': tools.p4Integrate,
|
|
@@ -122,6 +130,33 @@ const WRITE_TOOLS = new Set([
|
|
|
122
130
|
'p4.merge',
|
|
123
131
|
]);
|
|
124
132
|
const CACHEABLE_TOOLS = new Set(Object.keys(TOOL_HANDLERS).filter((toolName) => !WRITE_TOOLS.has(toolName)));
|
|
133
|
+
const LOW_LATENCY_CACHE_TTL_TOOLS = new Set([
|
|
134
|
+
'p4.status',
|
|
135
|
+
'p4.opened',
|
|
136
|
+
'p4.changes',
|
|
137
|
+
'p4.review',
|
|
138
|
+
'p4.reviews',
|
|
139
|
+
'p4.interchanges',
|
|
140
|
+
'p4.integrated',
|
|
141
|
+
'p4.review.bundle',
|
|
142
|
+
'p4.change.inspect',
|
|
143
|
+
'p4.path.synccheck',
|
|
144
|
+
]);
|
|
145
|
+
const STABLE_CACHE_TTL_TOOLS = new Set([
|
|
146
|
+
'p4.info',
|
|
147
|
+
'p4.users',
|
|
148
|
+
'p4.user',
|
|
149
|
+
'p4.clients',
|
|
150
|
+
'p4.client',
|
|
151
|
+
'p4.labels',
|
|
152
|
+
'p4.label',
|
|
153
|
+
'p4.streams',
|
|
154
|
+
'p4.stream',
|
|
155
|
+
'p4.jobs',
|
|
156
|
+
'p4.job',
|
|
157
|
+
'p4.config.detect',
|
|
158
|
+
'p4.compliance',
|
|
159
|
+
]);
|
|
125
160
|
function getPerformanceMode() {
|
|
126
161
|
const mode = (process.env.P4_PERFORMANCE_MODE || 'fast').toLowerCase();
|
|
127
162
|
if (mode === 'balanced' || mode === 'secure') {
|
|
@@ -155,14 +190,52 @@ function getEnvInt(name, fallback) {
|
|
|
155
190
|
const parsed = parseInt(process.env[name] || '', 10);
|
|
156
191
|
return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
|
|
157
192
|
}
|
|
193
|
+
function parseToolCacheTtlOverrides(raw) {
|
|
194
|
+
const overrides = new Map();
|
|
195
|
+
if (!raw) {
|
|
196
|
+
return overrides;
|
|
197
|
+
}
|
|
198
|
+
for (const segment of raw.split(',')) {
|
|
199
|
+
const trimmed = segment.trim();
|
|
200
|
+
if (!trimmed) {
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
const splitIndex = trimmed.includes('=') ? trimmed.indexOf('=') : trimmed.indexOf(':');
|
|
204
|
+
if (splitIndex <= 0) {
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
const key = trimmed.slice(0, splitIndex).trim();
|
|
208
|
+
const valueRaw = trimmed.slice(splitIndex + 1).trim();
|
|
209
|
+
const value = parseInt(valueRaw, 10);
|
|
210
|
+
if (key && Number.isFinite(value) && value >= 0) {
|
|
211
|
+
overrides.set(key, value);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return overrides;
|
|
215
|
+
}
|
|
158
216
|
class MCPPerforceServer {
|
|
159
217
|
constructor() {
|
|
160
218
|
this.prettyJson = process.env.P4_PRETTY_JSON === 'true';
|
|
161
219
|
this.responseCacheEnabled = process.env.P4_RESPONSE_CACHE !== 'false';
|
|
162
220
|
this.responseCacheTtlMs = getEnvInt('P4_RESPONSE_CACHE_TTL_MS', getDefaultResponseCacheTtlMs());
|
|
163
221
|
this.responseCacheMaxEntries = getEnvInt('P4_RESPONSE_CACHE_MAX_ENTRIES', getDefaultResponseCacheMaxEntries());
|
|
222
|
+
this.responseCacheTtlOverrides = parseToolCacheTtlOverrides(process.env.P4_RESPONSE_CACHE_TTL_MAP);
|
|
223
|
+
this.negativeCacheEnabled = process.env.P4_NEGATIVE_CACHE !== 'false';
|
|
224
|
+
this.negativeCacheTtlMs = getEnvInt('P4_NEGATIVE_CACHE_TTL_MS', Math.max(1000, Math.min(this.responseCacheTtlMs, 5000)));
|
|
225
|
+
this.negativeCacheableErrorCodes = new Set([
|
|
226
|
+
'P4_INVALID_ARGS',
|
|
227
|
+
'P4_READONLY_MODE',
|
|
228
|
+
'P4_DELETE_DISABLED',
|
|
229
|
+
'P4_AUDIT_DISABLED',
|
|
230
|
+
'P4_CONFIG_NOT_FOUND',
|
|
231
|
+
]);
|
|
164
232
|
this.responseCache = new Map();
|
|
165
233
|
this.inFlightReadRequests = new Map();
|
|
234
|
+
this.toolPerformance = new Map();
|
|
235
|
+
this.perfMetricsSampleSize = getEnvInt('P4_PERF_METRICS_SAMPLE_SIZE', 200) || 200;
|
|
236
|
+
this.perfMetricsEnabled = process.env.P4_LOG_PERF_METRICS === 'true';
|
|
237
|
+
this.perfMetricsIntervalMs = getEnvInt('P4_LOG_PERF_METRICS_INTERVAL_MS', 60000) || 60000;
|
|
238
|
+
this.perfMetricsTimer = null;
|
|
166
239
|
this.cacheEpoch = 0;
|
|
167
240
|
this.server = new index_js_1.Server({
|
|
168
241
|
name: 'mcp-perforce-server',
|
|
@@ -180,6 +253,7 @@ class MCPPerforceServer {
|
|
|
180
253
|
};
|
|
181
254
|
this.setupToolHandlers();
|
|
182
255
|
this.setupErrorHandling();
|
|
256
|
+
this.setupPerformanceLogging();
|
|
183
257
|
}
|
|
184
258
|
setupErrorHandling() {
|
|
185
259
|
this.server.onerror = (error) => {
|
|
@@ -187,15 +261,35 @@ class MCPPerforceServer {
|
|
|
187
261
|
};
|
|
188
262
|
process.on('SIGINT', async () => {
|
|
189
263
|
log.info('Shutting down MCP Perforce server...');
|
|
264
|
+
this.stopPerformanceLogging();
|
|
190
265
|
await this.server.close();
|
|
191
266
|
process.exit(0);
|
|
192
267
|
});
|
|
193
268
|
process.on('SIGTERM', async () => {
|
|
194
269
|
log.info('Shutting down MCP Perforce server...');
|
|
270
|
+
this.stopPerformanceLogging();
|
|
195
271
|
await this.server.close();
|
|
196
272
|
process.exit(0);
|
|
197
273
|
});
|
|
198
274
|
}
|
|
275
|
+
setupPerformanceLogging() {
|
|
276
|
+
if (!this.perfMetricsEnabled || this.perfMetricsIntervalMs <= 0) {
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
this.perfMetricsTimer = setInterval(() => {
|
|
280
|
+
this.logPerformanceSnapshot();
|
|
281
|
+
}, this.perfMetricsIntervalMs);
|
|
282
|
+
if (typeof this.perfMetricsTimer.unref === 'function') {
|
|
283
|
+
this.perfMetricsTimer.unref();
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
stopPerformanceLogging() {
|
|
287
|
+
if (!this.perfMetricsTimer) {
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
clearInterval(this.perfMetricsTimer);
|
|
291
|
+
this.perfMetricsTimer = null;
|
|
292
|
+
}
|
|
199
293
|
serializeResult(value) {
|
|
200
294
|
if (this.prettyJson) {
|
|
201
295
|
return JSON.stringify(value, null, 2);
|
|
@@ -207,6 +301,19 @@ class MCPPerforceServer {
|
|
|
207
301
|
content: [{ type: 'text', text: this.serializeResult(value) }],
|
|
208
302
|
};
|
|
209
303
|
}
|
|
304
|
+
getToolCacheTtlMs(toolName) {
|
|
305
|
+
const override = this.responseCacheTtlOverrides.get(toolName);
|
|
306
|
+
if (override !== undefined) {
|
|
307
|
+
return override;
|
|
308
|
+
}
|
|
309
|
+
if (LOW_LATENCY_CACHE_TTL_TOOLS.has(toolName)) {
|
|
310
|
+
return Math.max(1000, Math.floor(this.responseCacheTtlMs / 2));
|
|
311
|
+
}
|
|
312
|
+
if (STABLE_CACHE_TTL_TOOLS.has(toolName)) {
|
|
313
|
+
return Math.max(this.responseCacheTtlMs, 15000);
|
|
314
|
+
}
|
|
315
|
+
return this.responseCacheTtlMs;
|
|
316
|
+
}
|
|
210
317
|
getCachedResult(cacheKey) {
|
|
211
318
|
const entry = this.responseCache.get(cacheKey);
|
|
212
319
|
if (!entry) {
|
|
@@ -216,21 +323,33 @@ class MCPPerforceServer {
|
|
|
216
323
|
this.responseCache.delete(cacheKey);
|
|
217
324
|
return undefined;
|
|
218
325
|
}
|
|
219
|
-
|
|
326
|
+
// LRU behavior: refresh recency on read hits.
|
|
327
|
+
this.responseCache.delete(cacheKey);
|
|
328
|
+
this.responseCache.set(cacheKey, entry);
|
|
329
|
+
return {
|
|
330
|
+
result: entry.value,
|
|
331
|
+
cacheStatus: entry.isNegative ? 'negative_hit' : 'hit',
|
|
332
|
+
};
|
|
220
333
|
}
|
|
221
|
-
setCachedResult(cacheKey, value) {
|
|
222
|
-
if (this.responseCacheMaxEntries <= 0 ||
|
|
334
|
+
setCachedResult(cacheKey, value, ttlMs, isNegative = false) {
|
|
335
|
+
if (this.responseCacheMaxEntries <= 0 || ttlMs <= 0) {
|
|
223
336
|
return;
|
|
224
337
|
}
|
|
225
|
-
if (this.responseCache.
|
|
338
|
+
if (this.responseCache.has(cacheKey)) {
|
|
339
|
+
this.responseCache.delete(cacheKey);
|
|
340
|
+
}
|
|
341
|
+
while (this.responseCache.size >= this.responseCacheMaxEntries) {
|
|
226
342
|
const oldestKey = this.responseCache.keys().next().value;
|
|
227
|
-
if (oldestKey) {
|
|
228
|
-
|
|
343
|
+
if (!oldestKey) {
|
|
344
|
+
break;
|
|
229
345
|
}
|
|
346
|
+
this.responseCache.delete(oldestKey);
|
|
230
347
|
}
|
|
231
348
|
this.responseCache.set(cacheKey, {
|
|
232
349
|
value,
|
|
233
|
-
expiresAt: Date.now() +
|
|
350
|
+
expiresAt: Date.now() + ttlMs,
|
|
351
|
+
isNegative,
|
|
352
|
+
ttlMs,
|
|
234
353
|
});
|
|
235
354
|
}
|
|
236
355
|
clearReadCache() {
|
|
@@ -242,6 +361,115 @@ class MCPPerforceServer {
|
|
|
242
361
|
buildCacheKey(name, args) {
|
|
243
362
|
return `${name}:${JSON.stringify(args ?? {})}`;
|
|
244
363
|
}
|
|
364
|
+
shouldCacheNegativeResult(result) {
|
|
365
|
+
if (!this.negativeCacheEnabled || !result || typeof result !== 'object') {
|
|
366
|
+
return false;
|
|
367
|
+
}
|
|
368
|
+
const resultRecord = result;
|
|
369
|
+
if (resultRecord.ok !== false) {
|
|
370
|
+
return false;
|
|
371
|
+
}
|
|
372
|
+
const errorCode = resultRecord.error?.code;
|
|
373
|
+
return typeof errorCode === 'string' && this.negativeCacheableErrorCodes.has(errorCode);
|
|
374
|
+
}
|
|
375
|
+
recordToolPerformance(toolName, durationMs, result, cacheStatus, subcallCounts) {
|
|
376
|
+
let stats = this.toolPerformance.get(toolName);
|
|
377
|
+
if (!stats) {
|
|
378
|
+
stats = {
|
|
379
|
+
calls: 0,
|
|
380
|
+
successes: 0,
|
|
381
|
+
errors: 0,
|
|
382
|
+
cacheHits: 0,
|
|
383
|
+
negativeCacheHits: 0,
|
|
384
|
+
cacheMisses: 0,
|
|
385
|
+
inFlightHits: 0,
|
|
386
|
+
totalDurationMs: 0,
|
|
387
|
+
durationsMs: [],
|
|
388
|
+
subcallTotals: {},
|
|
389
|
+
};
|
|
390
|
+
this.toolPerformance.set(toolName, stats);
|
|
391
|
+
}
|
|
392
|
+
stats.calls += 1;
|
|
393
|
+
stats.totalDurationMs += durationMs;
|
|
394
|
+
stats.durationsMs.push(durationMs);
|
|
395
|
+
if (stats.durationsMs.length > this.perfMetricsSampleSize) {
|
|
396
|
+
stats.durationsMs.shift();
|
|
397
|
+
}
|
|
398
|
+
const ok = !!(result && typeof result === 'object' && result.ok === true);
|
|
399
|
+
if (ok) {
|
|
400
|
+
stats.successes += 1;
|
|
401
|
+
}
|
|
402
|
+
else {
|
|
403
|
+
stats.errors += 1;
|
|
404
|
+
}
|
|
405
|
+
switch (cacheStatus) {
|
|
406
|
+
case 'hit':
|
|
407
|
+
stats.cacheHits += 1;
|
|
408
|
+
break;
|
|
409
|
+
case 'negative_hit':
|
|
410
|
+
stats.negativeCacheHits += 1;
|
|
411
|
+
break;
|
|
412
|
+
case 'in_flight':
|
|
413
|
+
stats.inFlightHits += 1;
|
|
414
|
+
break;
|
|
415
|
+
case 'miss':
|
|
416
|
+
stats.cacheMisses += 1;
|
|
417
|
+
break;
|
|
418
|
+
default:
|
|
419
|
+
break;
|
|
420
|
+
}
|
|
421
|
+
if (subcallCounts) {
|
|
422
|
+
for (const [name, value] of Object.entries(subcallCounts)) {
|
|
423
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
stats.subcallTotals[name] = (stats.subcallTotals[name] || 0) + value;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
getPercentile(samples, percentile) {
|
|
431
|
+
if (samples.length === 0) {
|
|
432
|
+
return 0;
|
|
433
|
+
}
|
|
434
|
+
const sorted = [...samples].sort((a, b) => a - b);
|
|
435
|
+
const index = Math.min(sorted.length - 1, Math.max(0, Math.floor((percentile / 100) * (sorted.length - 1))));
|
|
436
|
+
return Math.round(sorted[index]);
|
|
437
|
+
}
|
|
438
|
+
extractSubcallCounts(result) {
|
|
439
|
+
if (!result || typeof result !== 'object') {
|
|
440
|
+
return undefined;
|
|
441
|
+
}
|
|
442
|
+
const outer = result;
|
|
443
|
+
if (!outer.result || typeof outer.result !== 'object') {
|
|
444
|
+
return undefined;
|
|
445
|
+
}
|
|
446
|
+
const meta = outer.result.meta;
|
|
447
|
+
if (!meta || !meta.subcallCounts) {
|
|
448
|
+
return undefined;
|
|
449
|
+
}
|
|
450
|
+
return meta.subcallCounts;
|
|
451
|
+
}
|
|
452
|
+
logPerformanceSnapshot() {
|
|
453
|
+
if (this.toolPerformance.size === 0) {
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
const byCalls = [...this.toolPerformance.entries()].sort((a, b) => b[1].calls - a[1].calls);
|
|
457
|
+
const topEntries = byCalls.slice(0, 12).map(([toolName, stats]) => {
|
|
458
|
+
const cacheHitsTotal = stats.cacheHits + stats.negativeCacheHits + stats.inFlightHits;
|
|
459
|
+
const cacheHitRate = stats.calls > 0 ? Math.round((cacheHitsTotal / stats.calls) * 100) : 0;
|
|
460
|
+
const avgMs = stats.calls > 0 ? Math.round(stats.totalDurationMs / stats.calls) : 0;
|
|
461
|
+
return {
|
|
462
|
+
tool: toolName,
|
|
463
|
+
calls: stats.calls,
|
|
464
|
+
avgMs,
|
|
465
|
+
p50Ms: this.getPercentile(stats.durationsMs, 50),
|
|
466
|
+
p95Ms: this.getPercentile(stats.durationsMs, 95),
|
|
467
|
+
cacheHitRate,
|
|
468
|
+
subcalls: stats.subcallTotals,
|
|
469
|
+
};
|
|
470
|
+
});
|
|
471
|
+
log.info('Performance snapshot:', topEntries);
|
|
472
|
+
}
|
|
245
473
|
async executeTool(name, args) {
|
|
246
474
|
const handler = TOOL_HANDLERS[name];
|
|
247
475
|
if (!handler) {
|
|
@@ -251,29 +479,33 @@ class MCPPerforceServer {
|
|
|
251
479
|
}
|
|
252
480
|
async executeToolWithCaching(name, args) {
|
|
253
481
|
if (!this.responseCacheEnabled || !CACHEABLE_TOOLS.has(name)) {
|
|
254
|
-
return this.executeTool(name, args);
|
|
482
|
+
return { result: await this.executeTool(name, args), cacheStatus: 'uncacheable' };
|
|
255
483
|
}
|
|
256
484
|
const cacheKey = this.buildCacheKey(name, args);
|
|
257
485
|
const cachedResult = this.getCachedResult(cacheKey);
|
|
258
|
-
if (cachedResult
|
|
486
|
+
if (cachedResult) {
|
|
259
487
|
return cachedResult;
|
|
260
488
|
}
|
|
261
489
|
const inFlight = this.inFlightReadRequests.get(cacheKey);
|
|
262
490
|
if (inFlight) {
|
|
263
|
-
return inFlight;
|
|
491
|
+
return { result: await inFlight, cacheStatus: 'in_flight' };
|
|
264
492
|
}
|
|
265
493
|
const pending = this.executeTool(name, args);
|
|
266
494
|
this.inFlightReadRequests.set(cacheKey, pending);
|
|
267
495
|
const startedEpoch = this.cacheEpoch;
|
|
268
496
|
try {
|
|
269
497
|
const result = await pending;
|
|
270
|
-
if (startedEpoch === this.cacheEpoch
|
|
271
|
-
|
|
272
|
-
typeof result === 'object' &&
|
|
273
|
-
|
|
274
|
-
|
|
498
|
+
if (startedEpoch === this.cacheEpoch) {
|
|
499
|
+
const toolTtlMs = this.getToolCacheTtlMs(name);
|
|
500
|
+
if (result && typeof result === 'object' && result.ok === true) {
|
|
501
|
+
this.setCachedResult(cacheKey, result, toolTtlMs, false);
|
|
502
|
+
}
|
|
503
|
+
else if (this.shouldCacheNegativeResult(result)) {
|
|
504
|
+
const negativeTtl = Math.min(toolTtlMs, this.negativeCacheTtlMs);
|
|
505
|
+
this.setCachedResult(cacheKey, result, negativeTtl, true);
|
|
506
|
+
}
|
|
275
507
|
}
|
|
276
|
-
return result;
|
|
508
|
+
return { result, cacheStatus: 'miss' };
|
|
277
509
|
}
|
|
278
510
|
finally {
|
|
279
511
|
this.inFlightReadRequests.delete(cacheKey);
|
|
@@ -601,7 +833,7 @@ class MCPPerforceServer {
|
|
|
601
833
|
},
|
|
602
834
|
{
|
|
603
835
|
name: 'p4.describe',
|
|
604
|
-
description: 'Describe a changelist with metadata
|
|
836
|
+
description: 'Describe a changelist with metadata, affected files, and optional diff content',
|
|
605
837
|
inputSchema: {
|
|
606
838
|
type: 'object',
|
|
607
839
|
properties: {
|
|
@@ -612,6 +844,15 @@ class MCPPerforceServer {
|
|
|
612
844
|
],
|
|
613
845
|
description: 'Changelist number (required)',
|
|
614
846
|
},
|
|
847
|
+
includeDiff: {
|
|
848
|
+
type: 'boolean',
|
|
849
|
+
description: 'Include patch/diff content from p4 describe -d* (optional, default false)',
|
|
850
|
+
},
|
|
851
|
+
diffFormat: {
|
|
852
|
+
type: 'string',
|
|
853
|
+
enum: ['u', 'c', 'n', 's'],
|
|
854
|
+
description: 'Diff format when includeDiff=true: u=unified, c=context, n=RCS, s=summary',
|
|
855
|
+
},
|
|
615
856
|
workspacePath: {
|
|
616
857
|
type: 'string',
|
|
617
858
|
description: 'Path to workspace directory (optional, defaults to current directory)',
|
|
@@ -800,6 +1041,214 @@ class MCPPerforceServer {
|
|
|
800
1041
|
additionalProperties: false,
|
|
801
1042
|
},
|
|
802
1043
|
},
|
|
1044
|
+
{
|
|
1045
|
+
name: 'p4.review',
|
|
1046
|
+
description: 'List changelists pending review',
|
|
1047
|
+
inputSchema: {
|
|
1048
|
+
type: 'object',
|
|
1049
|
+
properties: {
|
|
1050
|
+
counter: {
|
|
1051
|
+
type: 'string',
|
|
1052
|
+
description: 'Review counter/token used by p4 review -t (optional)',
|
|
1053
|
+
},
|
|
1054
|
+
filespec: {
|
|
1055
|
+
type: 'string',
|
|
1056
|
+
description: 'Optional filespec to filter changelists',
|
|
1057
|
+
},
|
|
1058
|
+
workspacePath: {
|
|
1059
|
+
type: 'string',
|
|
1060
|
+
description: 'Path to workspace directory (optional, defaults to current directory)',
|
|
1061
|
+
},
|
|
1062
|
+
},
|
|
1063
|
+
additionalProperties: false,
|
|
1064
|
+
},
|
|
1065
|
+
},
|
|
1066
|
+
{
|
|
1067
|
+
name: 'p4.reviews',
|
|
1068
|
+
description: 'List reviewers for files or a changelist',
|
|
1069
|
+
inputSchema: {
|
|
1070
|
+
type: 'object',
|
|
1071
|
+
properties: {
|
|
1072
|
+
changelist: {
|
|
1073
|
+
type: 'string',
|
|
1074
|
+
description: 'Optional changelist number to resolve reviewers for',
|
|
1075
|
+
},
|
|
1076
|
+
files: {
|
|
1077
|
+
type: 'array',
|
|
1078
|
+
items: { type: 'string' },
|
|
1079
|
+
description: 'Optional file list/filespecs to resolve reviewers for',
|
|
1080
|
+
},
|
|
1081
|
+
workspacePath: {
|
|
1082
|
+
type: 'string',
|
|
1083
|
+
description: 'Path to workspace directory (optional, defaults to current directory)',
|
|
1084
|
+
},
|
|
1085
|
+
},
|
|
1086
|
+
additionalProperties: false,
|
|
1087
|
+
},
|
|
1088
|
+
},
|
|
1089
|
+
{
|
|
1090
|
+
name: 'p4.interchanges',
|
|
1091
|
+
description: 'List changelists not yet integrated between two paths',
|
|
1092
|
+
inputSchema: {
|
|
1093
|
+
type: 'object',
|
|
1094
|
+
properties: {
|
|
1095
|
+
sourcePath: {
|
|
1096
|
+
type: 'string',
|
|
1097
|
+
description: 'Source depot filespec/path (required)',
|
|
1098
|
+
},
|
|
1099
|
+
targetPath: {
|
|
1100
|
+
type: 'string',
|
|
1101
|
+
description: 'Target depot filespec/path (required)',
|
|
1102
|
+
},
|
|
1103
|
+
max: {
|
|
1104
|
+
type: 'number',
|
|
1105
|
+
description: 'Maximum number of changelists to return (optional)',
|
|
1106
|
+
},
|
|
1107
|
+
longDescription: {
|
|
1108
|
+
type: 'boolean',
|
|
1109
|
+
description: 'Include long descriptions (optional, equivalent to -l)',
|
|
1110
|
+
},
|
|
1111
|
+
workspacePath: {
|
|
1112
|
+
type: 'string',
|
|
1113
|
+
description: 'Path to workspace directory (optional, defaults to current directory)',
|
|
1114
|
+
},
|
|
1115
|
+
},
|
|
1116
|
+
required: ['sourcePath', 'targetPath'],
|
|
1117
|
+
additionalProperties: false,
|
|
1118
|
+
},
|
|
1119
|
+
},
|
|
1120
|
+
{
|
|
1121
|
+
name: 'p4.integrated',
|
|
1122
|
+
description: 'Show integration history for source/target paths',
|
|
1123
|
+
inputSchema: {
|
|
1124
|
+
type: 'object',
|
|
1125
|
+
properties: {
|
|
1126
|
+
sourcePath: {
|
|
1127
|
+
type: 'string',
|
|
1128
|
+
description: 'Source depot filespec/path (required)',
|
|
1129
|
+
},
|
|
1130
|
+
targetPath: {
|
|
1131
|
+
type: 'string',
|
|
1132
|
+
description: 'Optional target depot filespec/path',
|
|
1133
|
+
},
|
|
1134
|
+
workspacePath: {
|
|
1135
|
+
type: 'string',
|
|
1136
|
+
description: 'Path to workspace directory (optional, defaults to current directory)',
|
|
1137
|
+
},
|
|
1138
|
+
},
|
|
1139
|
+
required: ['sourcePath'],
|
|
1140
|
+
additionalProperties: false,
|
|
1141
|
+
},
|
|
1142
|
+
},
|
|
1143
|
+
{
|
|
1144
|
+
name: 'p4.review.bundle',
|
|
1145
|
+
description: 'Composite review workflow: pending changes with optional details/reviewers',
|
|
1146
|
+
inputSchema: {
|
|
1147
|
+
type: 'object',
|
|
1148
|
+
properties: {
|
|
1149
|
+
counter: {
|
|
1150
|
+
type: 'string',
|
|
1151
|
+
description: 'Optional review counter/token for p4 review -t',
|
|
1152
|
+
},
|
|
1153
|
+
filespec: {
|
|
1154
|
+
type: 'string',
|
|
1155
|
+
description: 'Optional filespec filter',
|
|
1156
|
+
},
|
|
1157
|
+
maxChanges: {
|
|
1158
|
+
type: 'number',
|
|
1159
|
+
description: 'Maximum changelists to include (optional, default 10)',
|
|
1160
|
+
},
|
|
1161
|
+
includeDescribe: {
|
|
1162
|
+
type: 'boolean',
|
|
1163
|
+
description: 'Include p4 describe data per changelist (optional, default true)',
|
|
1164
|
+
},
|
|
1165
|
+
includeReviewers: {
|
|
1166
|
+
type: 'boolean',
|
|
1167
|
+
description: 'Include p4 reviews per changelist (optional, default true)',
|
|
1168
|
+
},
|
|
1169
|
+
workspacePath: {
|
|
1170
|
+
type: 'string',
|
|
1171
|
+
description: 'Path to workspace directory (optional, defaults to current directory)',
|
|
1172
|
+
},
|
|
1173
|
+
},
|
|
1174
|
+
additionalProperties: false,
|
|
1175
|
+
},
|
|
1176
|
+
},
|
|
1177
|
+
{
|
|
1178
|
+
name: 'p4.change.inspect',
|
|
1179
|
+
description: 'Composite changelist inspection: describe + fixes + reviewers (+ optional file history)',
|
|
1180
|
+
inputSchema: {
|
|
1181
|
+
type: 'object',
|
|
1182
|
+
properties: {
|
|
1183
|
+
changelist: {
|
|
1184
|
+
type: 'string',
|
|
1185
|
+
description: 'Changelist number to inspect (required)',
|
|
1186
|
+
},
|
|
1187
|
+
includeDiff: {
|
|
1188
|
+
type: 'boolean',
|
|
1189
|
+
description: 'Include describe diff content in inspection output (optional, default false)',
|
|
1190
|
+
},
|
|
1191
|
+
diffFormat: {
|
|
1192
|
+
type: 'string',
|
|
1193
|
+
enum: ['u', 'c', 'n', 's'],
|
|
1194
|
+
description: 'Diff format for describe when includeDiff=true',
|
|
1195
|
+
},
|
|
1196
|
+
includeFileHistory: {
|
|
1197
|
+
type: 'boolean',
|
|
1198
|
+
description: 'Include p4 filelog for affected files (optional, default false)',
|
|
1199
|
+
},
|
|
1200
|
+
maxFilesWithHistory: {
|
|
1201
|
+
type: 'number',
|
|
1202
|
+
description: 'Maximum files to fetch filelog for (optional, default 5)',
|
|
1203
|
+
},
|
|
1204
|
+
maxRevisions: {
|
|
1205
|
+
type: 'number',
|
|
1206
|
+
description: 'Maximum revisions per filelog call (optional, default 5)',
|
|
1207
|
+
},
|
|
1208
|
+
workspacePath: {
|
|
1209
|
+
type: 'string',
|
|
1210
|
+
description: 'Path to workspace directory (optional, defaults to current directory)',
|
|
1211
|
+
},
|
|
1212
|
+
},
|
|
1213
|
+
required: ['changelist'],
|
|
1214
|
+
additionalProperties: false,
|
|
1215
|
+
},
|
|
1216
|
+
},
|
|
1217
|
+
{
|
|
1218
|
+
name: 'p4.path.synccheck',
|
|
1219
|
+
description: 'Composite path sync analysis using interchanges/integrated in one call',
|
|
1220
|
+
inputSchema: {
|
|
1221
|
+
type: 'object',
|
|
1222
|
+
properties: {
|
|
1223
|
+
sourcePath: {
|
|
1224
|
+
type: 'string',
|
|
1225
|
+
description: 'Source depot filespec/path (required)',
|
|
1226
|
+
},
|
|
1227
|
+
targetPath: {
|
|
1228
|
+
type: 'string',
|
|
1229
|
+
description: 'Target depot filespec/path (required)',
|
|
1230
|
+
},
|
|
1231
|
+
maxInterchanges: {
|
|
1232
|
+
type: 'number',
|
|
1233
|
+
description: 'Maximum interchanges per direction (optional, default 50)',
|
|
1234
|
+
},
|
|
1235
|
+
includeIntegrated: {
|
|
1236
|
+
type: 'boolean',
|
|
1237
|
+
description: 'Include p4 integrated history (optional, default true)',
|
|
1238
|
+
},
|
|
1239
|
+
checkBothDirections: {
|
|
1240
|
+
type: 'boolean',
|
|
1241
|
+
description: 'Run reverse-direction comparison too (optional, default true)',
|
|
1242
|
+
},
|
|
1243
|
+
workspacePath: {
|
|
1244
|
+
type: 'string',
|
|
1245
|
+
description: 'Path to workspace directory (optional, defaults to current directory)',
|
|
1246
|
+
},
|
|
1247
|
+
},
|
|
1248
|
+
required: ['sourcePath', 'targetPath'],
|
|
1249
|
+
additionalProperties: false,
|
|
1250
|
+
},
|
|
1251
|
+
},
|
|
803
1252
|
{
|
|
804
1253
|
name: 'p4.blame',
|
|
805
1254
|
description: 'Show file annotations with change history (like git blame)',
|
|
@@ -819,6 +1268,25 @@ class MCPPerforceServer {
|
|
|
819
1268
|
additionalProperties: false,
|
|
820
1269
|
},
|
|
821
1270
|
},
|
|
1271
|
+
{
|
|
1272
|
+
name: 'p4.annotate',
|
|
1273
|
+
description: 'Alias of p4.blame (line-by-line annotation)',
|
|
1274
|
+
inputSchema: {
|
|
1275
|
+
type: 'object',
|
|
1276
|
+
properties: {
|
|
1277
|
+
file: {
|
|
1278
|
+
type: 'string',
|
|
1279
|
+
description: 'File to annotate (required)',
|
|
1280
|
+
},
|
|
1281
|
+
workspacePath: {
|
|
1282
|
+
type: 'string',
|
|
1283
|
+
description: 'Path to workspace directory (optional, defaults to current directory)',
|
|
1284
|
+
},
|
|
1285
|
+
},
|
|
1286
|
+
required: ['file'],
|
|
1287
|
+
additionalProperties: false,
|
|
1288
|
+
},
|
|
1289
|
+
},
|
|
822
1290
|
// Medium Priority Tools
|
|
823
1291
|
{
|
|
824
1292
|
name: 'p4.copy',
|
|
@@ -1392,10 +1860,12 @@ class MCPPerforceServer {
|
|
|
1392
1860
|
}
|
|
1393
1861
|
}
|
|
1394
1862
|
const toolArgs = (args || {});
|
|
1395
|
-
const
|
|
1863
|
+
const execution = await this.executeToolWithCaching(name, toolArgs);
|
|
1864
|
+
const result = execution.result;
|
|
1396
1865
|
if (WRITE_TOOLS.has(name) && result && typeof result === 'object' && result.ok) {
|
|
1397
1866
|
this.clearReadCache();
|
|
1398
1867
|
}
|
|
1868
|
+
this.recordToolPerformance(name, Date.now() - startTime, result, execution.cacheStatus, this.extractSubcallCounts(result));
|
|
1399
1869
|
// Audit log successful operation
|
|
1400
1870
|
this.context.security.logAuditEntry({
|
|
1401
1871
|
tool: name,
|
|
@@ -1411,6 +1881,7 @@ class MCPPerforceServer {
|
|
|
1411
1881
|
catch (error) {
|
|
1412
1882
|
const duration = Date.now() - startTime;
|
|
1413
1883
|
const errorCode = error instanceof types_js_1.McpError ? error.code : 'INTERNAL_ERROR';
|
|
1884
|
+
this.recordToolPerformance(name, duration, { ok: false, error: { code: String(errorCode) } }, 'uncacheable');
|
|
1414
1885
|
log.error('Tool execution error:', error);
|
|
1415
1886
|
// Audit log failed operation
|
|
1416
1887
|
this.context.security.logAuditEntry({
|
|
@@ -1470,7 +1941,14 @@ Environment Variables:
|
|
|
1470
1941
|
P4_PRETTY_JSON=true Pretty-print JSON responses (default: compact JSON)
|
|
1471
1942
|
P4_RESPONSE_CACHE=false Disable read-result cache (default: enabled)
|
|
1472
1943
|
P4_RESPONSE_CACHE_TTL_MS=5000 Cache TTL in ms (default by mode: fast 5000, balanced 3000, secure 1000)
|
|
1944
|
+
P4_RESPONSE_CACHE_TTL_MAP='p4.info=30000,p4.review=2000' Per-tool TTL overrides
|
|
1473
1945
|
P4_RESPONSE_CACHE_MAX_ENTRIES=400 Max cached responses (default by mode)
|
|
1946
|
+
P4_NEGATIVE_CACHE=false Disable short-lived caching of predictable read errors
|
|
1947
|
+
P4_NEGATIVE_CACHE_TTL_MS=5000 Negative-cache TTL in ms
|
|
1948
|
+
P4_WORKFLOW_CONCURRENCY=6 Max concurrent subcalls in composite workflow tools
|
|
1949
|
+
P4_LOG_PERF_METRICS=true Enable periodic performance snapshot logs
|
|
1950
|
+
P4_LOG_PERF_METRICS_INTERVAL_MS=60000 Performance snapshot interval in ms
|
|
1951
|
+
P4_PERF_METRICS_SAMPLE_SIZE=200 Duration sample size per tool for p50/p95
|
|
1474
1952
|
|
|
1475
1953
|
Compliance & Security:
|
|
1476
1954
|
P4_ENABLE_AUDIT_LOGGING=true|false Override audit logging (default in fast mode: false)
|