preflight-mcp 0.4.4 → 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/README.md +70 -6
- package/dist/analysis/deep.js +159 -2
- package/dist/mcp/cursor.js +145 -0
- package/dist/mcp/envelope.js +61 -0
- package/dist/mcp/errorKinds.js +35 -0
- package/dist/mcp/pathRedaction.js +180 -0
- package/dist/mcp/responseBuilder.js +284 -0
- package/dist/mcp/responseMeta.js +37 -2
- package/dist/server.js +486 -21
- package/dist/tools/readFiles.js +224 -0
- package/dist/tools/searchAndRead.js +296 -0
- package/dist/trace/service.js +5 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -51,7 +51,8 @@ Preflight: 🔗 Trace links:
|
|
|
51
51
|
- 📖 **Auto-generated guides** — `START_HERE.md`, `AGENTS.md`, `OVERVIEW.md`
|
|
52
52
|
- ☁️ **Cloud sync** — Multi-path mirror backup for redundancy
|
|
53
53
|
- 🧠 **EDDA (Evidence-Driven Deep Analysis)** — Auto-generate auditable claims with evidence
|
|
54
|
-
- ⚡ **
|
|
54
|
+
- ⚡ **22 MCP tools + 5 prompts** — Complete toolkit for code exploration
|
|
55
|
+
- 📄 **Cursor pagination** — Handle large result sets efficiently (RFC v2)
|
|
55
56
|
|
|
56
57
|
<details>
|
|
57
58
|
<summary><b>All Features (click to expand)</b></summary>
|
|
@@ -75,7 +76,7 @@ Preflight: 🔗 Trace links:
|
|
|
75
76
|
- [Demo](#demo)
|
|
76
77
|
- [Core Features](#core-features)
|
|
77
78
|
- [Quick Start](#quick-start)
|
|
78
|
-
- [Tools](#tools-
|
|
79
|
+
- [Tools](#tools-22-total)
|
|
79
80
|
- [Prompts](#prompts-5-total)
|
|
80
81
|
- [Environment Variables](#environment-variables)
|
|
81
82
|
- [Contributing](#contributing)
|
|
@@ -160,10 +161,11 @@ Run end-to-end smoke test:
|
|
|
160
161
|
npm run smoke
|
|
161
162
|
```
|
|
162
163
|
|
|
163
|
-
## Tools (
|
|
164
|
+
## Tools (22 total)
|
|
164
165
|
|
|
165
166
|
### `preflight_list_bundles`
|
|
166
167
|
List bundle IDs in storage.
|
|
168
|
+
- **Cursor pagination** (v0.5.0): Use `cursor` parameter for large bundle lists
|
|
167
169
|
- Triggers: "show bundles", "查看bundle", "有哪些bundle"
|
|
168
170
|
|
|
169
171
|
### `preflight_create_bundle`
|
|
@@ -236,10 +238,15 @@ Important: **this tool is strictly read-only**.
|
|
|
236
238
|
- `fileTypeFilters`: Filter by extension (e.g., `[".py", ".ts"]`)
|
|
237
239
|
- `includeScore`: Include BM25 relevance score in results
|
|
238
240
|
|
|
241
|
+
**Cursor pagination** (v0.5.0):
|
|
242
|
+
- `cursor`: Pagination cursor from previous call for fetching next page
|
|
243
|
+
- Response includes `truncation.nextCursor` when more results available
|
|
244
|
+
|
|
239
245
|
**Deprecated parameters**: `ensureFresh`, `autoRepairIndex`, `maxAgeHours` are deprecated and will return warnings instead of errors.
|
|
240
246
|
|
|
241
247
|
### `preflight_search_by_tags`
|
|
242
248
|
Search across multiple bundles filtered by tags (line-based SQLite FTS5).
|
|
249
|
+
- **Cursor pagination** (v0.5.0): Use `cursor` parameter for large result sets
|
|
243
250
|
- Triggers: "search in MCP bundles", "在MCP项目中搜索", "搜索所有agent"
|
|
244
251
|
|
|
245
252
|
Notes:
|
|
@@ -250,6 +257,7 @@ Optional parameters:
|
|
|
250
257
|
- `tags`: Filter bundles by tags (e.g., `["mcp", "agents"]`)
|
|
251
258
|
- `scope`: Search scope (`docs`, `code`, or `all`)
|
|
252
259
|
- `limit`: Max total hits across all bundles
|
|
260
|
+
- `cursor`: Pagination cursor for fetching next page
|
|
253
261
|
|
|
254
262
|
### `preflight_evidence_dependency_graph`
|
|
255
263
|
Generate an evidence-based dependency graph. Two modes:
|
|
@@ -280,6 +288,7 @@ Create or update traceability links (code↔test, code↔doc, file↔requirement
|
|
|
280
288
|
### `preflight_trace_query`
|
|
281
289
|
Query traceability links (code↔test, code↔doc, commit↔ticket).
|
|
282
290
|
- **Proactive use**: LLM automatically queries trace links when analyzing specific files
|
|
291
|
+
- **Cursor pagination** (v0.5.0): Use `cursor` parameter for large result sets
|
|
283
292
|
- Returns `reason` and `nextSteps` when no edges found (helps LLM decide next action)
|
|
284
293
|
- Fast when `bundleId` is provided; can scan across bundles when omitted.
|
|
285
294
|
|
|
@@ -301,20 +310,39 @@ Parameters:
|
|
|
301
310
|
- `min_confidence`: 0-1 (default: 0.85)
|
|
302
311
|
- `limit`: Max suggestions (default: 50)
|
|
303
312
|
|
|
304
|
-
### `preflight_deep_analyze_bundle` *(
|
|
305
|
-
One-call deep analysis aggregating tree, search, deps, and
|
|
313
|
+
### `preflight_deep_analyze_bundle` *(Enhanced v0.5.1)*
|
|
314
|
+
One-call deep analysis aggregating tree, search, deps, traces, **overview content**, and **test detection**.
|
|
306
315
|
- Returns unified evidence pack with LLM-friendly summary
|
|
316
|
+
- **Now includes OVERVIEW.md, START_HERE.md, AGENTS.md, README content** (v0.5.1)
|
|
317
|
+
- **Auto-detects test frameworks** (jest, vitest, pytest, go, mocha) (v0.5.1)
|
|
318
|
+
- **Generates copyable `nextCommands`** for follow-up actions (v0.5.1)
|
|
307
319
|
- Auto-generates **claims** with evidence references
|
|
308
320
|
- Tracks analysis progress via **checklistStatus**
|
|
309
321
|
- Reports unanswered questions as **openQuestions**
|
|
310
|
-
- Triggers: "deep analyze", "comprehensive analysis", "深度分析"
|
|
322
|
+
- Triggers: "deep analyze", "comprehensive analysis", "深度分析", "快速了解项目"
|
|
323
|
+
|
|
324
|
+
**New in v0.5.1 - Aggregated content (reduces round-trips):**
|
|
325
|
+
- `includeOverview` (default: true): Include OVERVIEW.md, START_HERE.md, AGENTS.md
|
|
326
|
+
- `includeReadme` (default: true): Include repo README.md
|
|
327
|
+
- `includeTests` (default: true): Detect test directories and frameworks
|
|
311
328
|
|
|
312
329
|
Output includes:
|
|
330
|
+
- `overviewContent`: `{overview, startHere, agents, readme}` - bundle documentation content
|
|
331
|
+
- `testInfo`: `{detected, framework, testDirs, testFileCount, configFiles, hint}` - test detection result
|
|
332
|
+
- `nextCommands[]`: Copyable tool calls for next steps (can be directly used as arguments)
|
|
313
333
|
- `claims[]`: Auto-generated findings with evidence
|
|
314
334
|
- `checklistStatus`: Analysis progress (repo_tree, deps, entrypoints, etc.)
|
|
315
335
|
- `openQuestions[]`: Questions with `nextEvidenceToFetch` hints
|
|
316
336
|
- `summary`: Markdown summary with checklist and key findings
|
|
317
337
|
|
|
338
|
+
**Example `nextCommands` output:**
|
|
339
|
+
```json
|
|
340
|
+
[
|
|
341
|
+
{ "tool": "preflight_search_bundle", "description": "Search for specific code", "args": { "bundleId": "...", "query": "<填入关键词>" } },
|
|
342
|
+
{ "tool": "preflight_read_file", "description": "Read core module", "args": { "bundleId": "...", "file": "src/server.ts" } }
|
|
343
|
+
]
|
|
344
|
+
```
|
|
345
|
+
|
|
318
346
|
### `preflight_validate_report` *(NEW v0.4.0)*
|
|
319
347
|
Validate claims and evidence chains for auditability.
|
|
320
348
|
- Checks: missing evidence, invalid file references, broken snippet hashes
|
|
@@ -327,6 +355,37 @@ Parameters:
|
|
|
327
355
|
- `verifyFileExists`: Check evidence files exist (default: true)
|
|
328
356
|
- `strictMode`: Treat warnings as errors (default: false)
|
|
329
357
|
|
|
358
|
+
### `preflight_read_files` *(NEW v0.5.0)*
|
|
359
|
+
Batch read multiple files from a bundle in a single call.
|
|
360
|
+
- Reduces round-trips for evidence gathering
|
|
361
|
+
- **RFC v2 unified envelope**: Returns `ok`, `meta`, `data`, `evidence[]`
|
|
362
|
+
- Triggers: "read these files", "get content of", "批量读取"
|
|
363
|
+
|
|
364
|
+
Parameters:
|
|
365
|
+
- `bundleId`: Bundle ID
|
|
366
|
+
- `files[]`: Array of `{path, ranges?, withLineNumbers?}`
|
|
367
|
+
- `format`: `"json"` (default) or `"text"`
|
|
368
|
+
|
|
369
|
+
### `preflight_search_and_read` *(NEW v0.5.0)*
|
|
370
|
+
Search + excerpt in one call - finds relevant code and returns context.
|
|
371
|
+
- Combines search with automatic context extraction
|
|
372
|
+
- **RFC v2 unified envelope**: Returns `ok`, `meta`, `data`, `evidence[]`
|
|
373
|
+
- Triggers: "search and show code", "find and read", "搜索并读取"
|
|
374
|
+
|
|
375
|
+
Parameters:
|
|
376
|
+
- `bundleId`: Bundle ID
|
|
377
|
+
- `query`: Search query
|
|
378
|
+
- `contextLines`: Lines of context around matches (default: 5)
|
|
379
|
+
- `maxFiles`: Max files to read (default: 5)
|
|
380
|
+
- `format`: `"json"` (default) or `"text"`
|
|
381
|
+
|
|
382
|
+
### `preflight_get_dependency_graph` *(Simplified wrapper)*
|
|
383
|
+
Simplified dependency graph query.
|
|
384
|
+
- `scope: "global"` (default): Project-wide graph
|
|
385
|
+
- `scope: "target"` with `targetFile`: Single file dependencies
|
|
386
|
+
- `format: "summary"` (default): Aggregated view
|
|
387
|
+
- `format: "full"`: Raw graph data
|
|
388
|
+
|
|
330
389
|
### `preflight_cleanup_orphans`
|
|
331
390
|
Remove incomplete or corrupted bundles (bundles without valid manifest.json).
|
|
332
391
|
- Triggers: "clean up broken bundles", "remove orphans", "清理孤儿bundle"
|
|
@@ -393,6 +452,11 @@ Common kinds:
|
|
|
393
452
|
- `invalid_path` (unsafe path traversal attempt)
|
|
394
453
|
- `permission_denied`
|
|
395
454
|
- `index_missing_or_corrupt`
|
|
455
|
+
- `cursor_invalid` *(v0.5.0)*
|
|
456
|
+
- `cursor_expired` *(v0.5.0)*
|
|
457
|
+
- `rate_limited` *(v0.5.0)*
|
|
458
|
+
- `timeout` *(v0.5.0)*
|
|
459
|
+
- `pagination_required` *(v0.5.0)*
|
|
396
460
|
- `unknown`
|
|
397
461
|
|
|
398
462
|
This is designed so UIs/agents can reliably decide whether to:
|
package/dist/analysis/deep.js
CHANGED
|
@@ -8,7 +8,7 @@ import { createEmptyCoverageReport, isCoverageSufficient, } from '../types/evide
|
|
|
8
8
|
* This is called by the server after gathering data from each source.
|
|
9
9
|
*/
|
|
10
10
|
export function buildDeepAnalysis(bundleId, components) {
|
|
11
|
-
const { tree, search, deps, traces, focusPath, focusQuery, errors = [] } = components;
|
|
11
|
+
const { tree, search, deps, traces, overviewContent, testInfo, focusPath, focusQuery, errors = [] } = components;
|
|
12
12
|
// Build coverage report
|
|
13
13
|
const coverageReport = createEmptyCoverageReport();
|
|
14
14
|
if (tree) {
|
|
@@ -75,9 +75,28 @@ export function buildDeepAnalysis(bundleId, components) {
|
|
|
75
75
|
}
|
|
76
76
|
summaryParts.push('');
|
|
77
77
|
}
|
|
78
|
+
// Test detection summary
|
|
79
|
+
if (testInfo) {
|
|
80
|
+
summaryParts.push(`## Test Detection`);
|
|
81
|
+
if (testInfo.detected) {
|
|
82
|
+
summaryParts.push(`- Framework: ${testInfo.framework ?? 'unknown'}`);
|
|
83
|
+
summaryParts.push(`- Test files: ${testInfo.testFileCount}`);
|
|
84
|
+
if (testInfo.testDirs.length > 0) {
|
|
85
|
+
summaryParts.push(`- Test directories: ${testInfo.testDirs.slice(0, 3).join(', ')}`);
|
|
86
|
+
}
|
|
87
|
+
if (testInfo.configFiles.length > 0) {
|
|
88
|
+
summaryParts.push(`- Config files: ${testInfo.configFiles.join(', ')}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
summaryParts.push(`- No tests detected`);
|
|
93
|
+
}
|
|
94
|
+
summaryParts.push(`- 💡 ${testInfo.hint}`);
|
|
95
|
+
summaryParts.push('');
|
|
96
|
+
}
|
|
78
97
|
// Build checklist status
|
|
79
98
|
const checklistStatus = {
|
|
80
|
-
read_overview:
|
|
99
|
+
read_overview: !!(overviewContent?.overview || overviewContent?.startHere),
|
|
81
100
|
repo_tree: !!tree && tree.totalFiles > 0,
|
|
82
101
|
search_focus: !!search && search.totalHits > 0,
|
|
83
102
|
dependency_graph_global: !!deps && deps.totalNodes > 0,
|
|
@@ -271,6 +290,51 @@ export function buildDeepAnalysis(bundleId, components) {
|
|
|
271
290
|
if (isCoverageSufficient(coverageReport) && openQuestions.length === 0 && claims.length > 0) {
|
|
272
291
|
nextSteps.push('🎉 Analysis complete - all key areas covered. Ready for detailed review.');
|
|
273
292
|
}
|
|
293
|
+
// Build nextCommands (copyable JSON for LLM)
|
|
294
|
+
const nextCommands = [];
|
|
295
|
+
// Always suggest search as a useful next step
|
|
296
|
+
nextCommands.push({
|
|
297
|
+
tool: 'preflight_search_bundle',
|
|
298
|
+
description: 'Search for specific code or concepts',
|
|
299
|
+
args: { bundleId, query: '<填入关键词>', scope: 'all', limit: 30 },
|
|
300
|
+
});
|
|
301
|
+
// Suggest reading a specific entry point if identified
|
|
302
|
+
if (deps && deps.topImported.length > 0) {
|
|
303
|
+
const coreFile = deps.topImported[0].file;
|
|
304
|
+
nextCommands.push({
|
|
305
|
+
tool: 'preflight_read_file',
|
|
306
|
+
description: `Read core module: ${coreFile}`,
|
|
307
|
+
args: { bundleId, file: coreFile, withLineNumbers: true },
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
// Suggest dependency analysis for a specific file if entry point identified
|
|
311
|
+
if (deps && deps.topImporters.length > 0) {
|
|
312
|
+
const entryFile = deps.topImporters[0].file;
|
|
313
|
+
nextCommands.push({
|
|
314
|
+
tool: 'preflight_evidence_dependency_graph',
|
|
315
|
+
description: `Analyze dependencies of entry point: ${entryFile}`,
|
|
316
|
+
args: { bundleId, target: { file: entryFile } },
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
// Suggest trace discovery if no traces exist
|
|
320
|
+
if (!traces || traces.totalLinks === 0) {
|
|
321
|
+
nextCommands.push({
|
|
322
|
+
tool: 'preflight_suggest_traces',
|
|
323
|
+
description: 'Auto-discover test↔code relationships',
|
|
324
|
+
args: { bundleId, edge_type: 'tested_by', scope: 'repo' },
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
// Suggest focused tree if large directory detected
|
|
328
|
+
if (tree) {
|
|
329
|
+
const largeDir = tree.topDirs.find(d => d.fileCount > 50);
|
|
330
|
+
if (largeDir) {
|
|
331
|
+
nextCommands.push({
|
|
332
|
+
tool: 'preflight_repo_tree',
|
|
333
|
+
description: `Explore large directory: ${largeDir.path}`,
|
|
334
|
+
args: { bundleId, focusDir: largeDir.path, depth: 6 },
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
}
|
|
274
338
|
// Add checklist and claims to summary
|
|
275
339
|
summaryParts.push(`## Analysis Checklist`);
|
|
276
340
|
const checklistItems = [
|
|
@@ -307,11 +371,104 @@ export function buildDeepAnalysis(bundleId, components) {
|
|
|
307
371
|
search,
|
|
308
372
|
deps,
|
|
309
373
|
traces,
|
|
374
|
+
overviewContent,
|
|
375
|
+
testInfo,
|
|
310
376
|
claims,
|
|
311
377
|
checklistStatus,
|
|
312
378
|
openQuestions,
|
|
313
379
|
coverageReport,
|
|
314
380
|
summary: summaryParts.join('\n'),
|
|
315
381
|
nextSteps,
|
|
382
|
+
nextCommands,
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Detect test setup from file tree statistics.
|
|
387
|
+
* Scans for test directories, test files, and framework config files.
|
|
388
|
+
*/
|
|
389
|
+
export function detectTestInfo(stats, filesFound) {
|
|
390
|
+
const testDirs = [];
|
|
391
|
+
let testFileCount = 0;
|
|
392
|
+
const configFiles = [];
|
|
393
|
+
let framework = null;
|
|
394
|
+
// Common test directory patterns
|
|
395
|
+
const testDirPatterns = ['tests', 'test', '__tests__', 'spec', 'specs', 'e2e', 'integration'];
|
|
396
|
+
// Check byTopDir for test directories
|
|
397
|
+
if (stats.byTopDir) {
|
|
398
|
+
for (const [dir, count] of Object.entries(stats.byTopDir)) {
|
|
399
|
+
const dirLower = dir.toLowerCase();
|
|
400
|
+
if (testDirPatterns.some(p => dirLower === p || dirLower.endsWith('/' + p))) {
|
|
401
|
+
testDirs.push(dir);
|
|
402
|
+
testFileCount += count;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
// Framework detection from config files (if filesFound provided)
|
|
407
|
+
const frameworkConfigs = [
|
|
408
|
+
{ pattern: /^jest\.config\.(js|ts|mjs|cjs|json)$/i, framework: 'jest' },
|
|
409
|
+
{ pattern: /^vitest\.config\.(js|ts|mjs|cjs)$/i, framework: 'vitest' },
|
|
410
|
+
{ pattern: /^pytest\.ini$/i, framework: 'pytest' },
|
|
411
|
+
{ pattern: /^pyproject\.toml$/i, framework: 'pytest' }, // May contain pytest config
|
|
412
|
+
{ pattern: /^setup\.cfg$/i, framework: 'pytest' },
|
|
413
|
+
{ pattern: /^\.mocharc\.(js|json|yml|yaml)$/i, framework: 'mocha' },
|
|
414
|
+
{ pattern: /^mocha\.opts$/i, framework: 'mocha' },
|
|
415
|
+
];
|
|
416
|
+
if (filesFound) {
|
|
417
|
+
for (const file of filesFound) {
|
|
418
|
+
for (const cfg of frameworkConfigs) {
|
|
419
|
+
if (cfg.pattern.test(file.name)) {
|
|
420
|
+
configFiles.push(file.path);
|
|
421
|
+
if (!framework) {
|
|
422
|
+
framework = cfg.framework;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
// Infer framework from file extensions if not detected from config
|
|
429
|
+
if (!framework && stats.byExtension) {
|
|
430
|
+
// Check for test file patterns in extensions
|
|
431
|
+
const hasTs = (stats.byExtension['.ts'] ?? 0) > 0 || (stats.byExtension['.tsx'] ?? 0) > 0;
|
|
432
|
+
const hasPy = (stats.byExtension['.py'] ?? 0) > 0;
|
|
433
|
+
const hasGo = (stats.byExtension['.go'] ?? 0) > 0;
|
|
434
|
+
if (testDirs.length > 0 || testFileCount > 0) {
|
|
435
|
+
if (hasGo)
|
|
436
|
+
framework = 'go';
|
|
437
|
+
else if (hasPy)
|
|
438
|
+
framework = 'pytest';
|
|
439
|
+
else if (hasTs)
|
|
440
|
+
framework = 'unknown'; // Could be jest/vitest/mocha
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
// Count test files by pattern (approximate from extensions)
|
|
444
|
+
// This is a heuristic - actual test files may vary
|
|
445
|
+
if (testFileCount === 0 && stats.byExtension) {
|
|
446
|
+
// If no test directories found, estimate based on common patterns
|
|
447
|
+
// This is imprecise but gives a hint
|
|
448
|
+
}
|
|
449
|
+
const detected = testDirs.length > 0 || testFileCount > 0 || configFiles.length > 0;
|
|
450
|
+
// Generate hint based on detection results
|
|
451
|
+
let hint;
|
|
452
|
+
if (detected) {
|
|
453
|
+
if (testFileCount > 0) {
|
|
454
|
+
hint = `Found ${testFileCount} test files. Run preflight_suggest_traces to map code↔test relationships.`;
|
|
455
|
+
}
|
|
456
|
+
else if (configFiles.length > 0) {
|
|
457
|
+
hint = `Test config found (${configFiles[0]}). Run preflight_suggest_traces to discover test files.`;
|
|
458
|
+
}
|
|
459
|
+
else {
|
|
460
|
+
hint = `Test directories found. Run preflight_suggest_traces to map code↔test relationships.`;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
else {
|
|
464
|
+
hint = 'No tests detected. Consider adding tests or check if test files use non-standard naming.';
|
|
465
|
+
}
|
|
466
|
+
return {
|
|
467
|
+
detected,
|
|
468
|
+
framework,
|
|
469
|
+
testDirs,
|
|
470
|
+
testFileCount,
|
|
471
|
+
configFiles,
|
|
472
|
+
hint,
|
|
316
473
|
};
|
|
317
474
|
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RFC v2: Cursor encoding/decoding for stable pagination.
|
|
3
|
+
*
|
|
4
|
+
* Cursors are opaque, base64-encoded JSON objects that contain:
|
|
5
|
+
* - offset: The position in the result set
|
|
6
|
+
* - sortKey: The last seen sort key for stable ordering
|
|
7
|
+
* - tool: The tool that generated the cursor (for validation)
|
|
8
|
+
* - timestamp: When the cursor was created (for expiration)
|
|
9
|
+
*
|
|
10
|
+
* This enables LLMs to reliably paginate through large result sets.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Maximum cursor age in milliseconds (24 hours).
|
|
14
|
+
* Cursors older than this are considered expired.
|
|
15
|
+
*/
|
|
16
|
+
const MAX_CURSOR_AGE_MS = 24 * 60 * 60 * 1000;
|
|
17
|
+
/**
|
|
18
|
+
* Encode cursor state to an opaque string.
|
|
19
|
+
*/
|
|
20
|
+
export function encodeCursor(state) {
|
|
21
|
+
const json = JSON.stringify(state);
|
|
22
|
+
// Use base64url encoding (URL-safe, no padding)
|
|
23
|
+
return Buffer.from(json, 'utf8').toString('base64url');
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Decode cursor string to cursor state.
|
|
27
|
+
* Returns null if the cursor is invalid.
|
|
28
|
+
*/
|
|
29
|
+
export function decodeCursor(cursor) {
|
|
30
|
+
try {
|
|
31
|
+
const json = Buffer.from(cursor, 'base64url').toString('utf8');
|
|
32
|
+
const state = JSON.parse(json);
|
|
33
|
+
// Validate structure
|
|
34
|
+
if (typeof state !== 'object' ||
|
|
35
|
+
state === null ||
|
|
36
|
+
typeof state.offset !== 'number' ||
|
|
37
|
+
typeof state.tool !== 'string' ||
|
|
38
|
+
typeof state.timestamp !== 'number') {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
return state;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Validate a cursor for a specific tool.
|
|
49
|
+
* Checks structure, tool match, and expiration.
|
|
50
|
+
*/
|
|
51
|
+
export function validateCursor(cursor, expectedTool, options) {
|
|
52
|
+
const state = decodeCursor(cursor);
|
|
53
|
+
if (!state) {
|
|
54
|
+
return {
|
|
55
|
+
valid: false,
|
|
56
|
+
error: 'Invalid cursor format',
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
// Check tool match
|
|
60
|
+
if (!options?.allowToolMismatch && state.tool !== expectedTool) {
|
|
61
|
+
return {
|
|
62
|
+
valid: false,
|
|
63
|
+
state,
|
|
64
|
+
error: `Cursor was created by ${state.tool}, not ${expectedTool}`,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
// Check expiration
|
|
68
|
+
const maxAge = options?.maxAgeMs ?? MAX_CURSOR_AGE_MS;
|
|
69
|
+
const age = Date.now() - state.timestamp;
|
|
70
|
+
if (age > maxAge) {
|
|
71
|
+
return {
|
|
72
|
+
valid: false,
|
|
73
|
+
state,
|
|
74
|
+
error: `Cursor expired (age: ${Math.round(age / 1000)}s, max: ${Math.round(maxAge / 1000)}s)`,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
valid: true,
|
|
79
|
+
state,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Create a cursor for the next page of results.
|
|
84
|
+
*
|
|
85
|
+
* @param tool - The tool creating the cursor
|
|
86
|
+
* @param offset - Current offset (will be incremented by pageSize)
|
|
87
|
+
* @param pageSize - Number of items per page
|
|
88
|
+
* @param sortKey - Optional sort key for keyset pagination
|
|
89
|
+
* @param extra - Optional additional data
|
|
90
|
+
*/
|
|
91
|
+
export function createNextCursor(tool, offset, pageSize, sortKey, extra) {
|
|
92
|
+
const state = {
|
|
93
|
+
offset: offset + pageSize,
|
|
94
|
+
tool,
|
|
95
|
+
timestamp: Date.now(),
|
|
96
|
+
};
|
|
97
|
+
if (sortKey !== undefined)
|
|
98
|
+
state.sortKey = sortKey;
|
|
99
|
+
if (extra !== undefined)
|
|
100
|
+
state.extra = extra;
|
|
101
|
+
return encodeCursor(state);
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Parse cursor or return default offset.
|
|
105
|
+
* Convenience function for tool handlers.
|
|
106
|
+
*
|
|
107
|
+
* @param cursor - Optional cursor string
|
|
108
|
+
* @param tool - Expected tool name
|
|
109
|
+
* @param defaultOffset - Default offset if no cursor (default: 0)
|
|
110
|
+
* @returns Offset to use and any error message
|
|
111
|
+
*/
|
|
112
|
+
export function parseCursorOrDefault(cursor, tool, defaultOffset = 0) {
|
|
113
|
+
if (!cursor) {
|
|
114
|
+
return { offset: defaultOffset };
|
|
115
|
+
}
|
|
116
|
+
const validation = validateCursor(cursor, tool);
|
|
117
|
+
if (!validation.valid) {
|
|
118
|
+
return { offset: defaultOffset, error: validation.error };
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
offset: validation.state.offset,
|
|
122
|
+
sortKey: validation.state.sortKey,
|
|
123
|
+
extra: validation.state.extra,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Helper to determine if pagination should continue.
|
|
128
|
+
*
|
|
129
|
+
* @param returnedCount - Number of items returned in this page
|
|
130
|
+
* @param limit - Requested limit
|
|
131
|
+
* @param totalCount - Optional total count (if known)
|
|
132
|
+
* @param currentOffset - Current offset in result set
|
|
133
|
+
*/
|
|
134
|
+
export function shouldPaginate(returnedCount, limit, totalCount, currentOffset = 0) {
|
|
135
|
+
// If we got fewer items than requested, we're at the end
|
|
136
|
+
if (returnedCount < limit) {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
// If we know the total and have fetched everything, no more pages
|
|
140
|
+
if (totalCount !== undefined && currentOffset + returnedCount >= totalCount) {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
// Otherwise, assume there might be more
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RFC v2: Unified Response Envelope for all Preflight MCP tools.
|
|
3
|
+
*
|
|
4
|
+
* This module defines the standardized response structure that enables:
|
|
5
|
+
* - LLM-friendly JSON output with stable field names
|
|
6
|
+
* - Evidence-first design with traceable citations
|
|
7
|
+
* - Pagination/truncation support with cursor-based continuation
|
|
8
|
+
* - Structured error handling with recovery hints
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Schema version for response envelope.
|
|
12
|
+
* Increment when making breaking changes to envelope structure.
|
|
13
|
+
*/
|
|
14
|
+
export const SCHEMA_VERSION = '2.0';
|
|
15
|
+
/**
|
|
16
|
+
* Type guard to check if response is successful.
|
|
17
|
+
*/
|
|
18
|
+
export function isSuccessResponse(response) {
|
|
19
|
+
return response.ok === true && response.data !== undefined;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Type guard to check if response is an error.
|
|
23
|
+
*/
|
|
24
|
+
export function isErrorResponse(response) {
|
|
25
|
+
return response.ok === false && response.error !== undefined;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Helper to create a source range from line numbers.
|
|
29
|
+
*/
|
|
30
|
+
export function createRange(startLine, endLine, startCol, endCol) {
|
|
31
|
+
const range = { startLine, endLine };
|
|
32
|
+
if (startCol !== undefined)
|
|
33
|
+
range.startCol = startCol;
|
|
34
|
+
if (endCol !== undefined)
|
|
35
|
+
range.endCol = endCol;
|
|
36
|
+
return range;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Helper to create an evidence pointer.
|
|
40
|
+
*/
|
|
41
|
+
export function createEvidencePointer(path, range, options) {
|
|
42
|
+
const pointer = { path, range };
|
|
43
|
+
if (options?.uri)
|
|
44
|
+
pointer.uri = options.uri;
|
|
45
|
+
if (options?.snippet)
|
|
46
|
+
pointer.snippet = options.snippet;
|
|
47
|
+
if (options?.snippetSha256)
|
|
48
|
+
pointer.snippetSha256 = options.snippetSha256;
|
|
49
|
+
return pointer;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Format evidence pointer as citation string.
|
|
53
|
+
* Format: "path:startLine-endLine" or "path:line" for single line
|
|
54
|
+
*/
|
|
55
|
+
export function formatEvidenceCitation(pointer) {
|
|
56
|
+
const { path, range } = pointer;
|
|
57
|
+
if (range.startLine === range.endLine) {
|
|
58
|
+
return `${path}:${range.startLine}`;
|
|
59
|
+
}
|
|
60
|
+
return `${path}:${range.startLine}-${range.endLine}`;
|
|
61
|
+
}
|
package/dist/mcp/errorKinds.js
CHANGED
|
@@ -72,6 +72,41 @@ const LLM_RECOVERY_HINTS = {
|
|
|
72
72
|
This parameter is deprecated. The tool is now strictly read-only.
|
|
73
73
|
- For updates: use preflight_update_bundle first, then retry
|
|
74
74
|
- For repairs: use preflight_repair_bundle first, then retry`,
|
|
75
|
+
// RFC v2 additions
|
|
76
|
+
cursor_invalid: `💡 Recovery steps:
|
|
77
|
+
1. The cursor format is invalid or corrupted
|
|
78
|
+
2. Start fresh without a cursor to get the first page
|
|
79
|
+
3. Use the nextCursor from the previous response exactly as provided`,
|
|
80
|
+
cursor_expired: `💡 Recovery steps:
|
|
81
|
+
1. Cursors expire after 24 hours
|
|
82
|
+
2. Start fresh without a cursor to get the first page
|
|
83
|
+
3. Complete pagination within a reasonable time window`,
|
|
84
|
+
cursor_tool_mismatch: `💡 Recovery steps:
|
|
85
|
+
1. The cursor was created by a different tool
|
|
86
|
+
2. Use the cursor only with the same tool that created it
|
|
87
|
+
3. Start fresh without a cursor for this tool`,
|
|
88
|
+
rate_limited: `💡 Recovery steps:
|
|
89
|
+
1. You are making requests too quickly
|
|
90
|
+
2. Wait a few seconds before retrying
|
|
91
|
+
3. Consider batching multiple operations into single calls`,
|
|
92
|
+
timeout: `💡 Recovery steps:
|
|
93
|
+
1. The operation took too long to complete
|
|
94
|
+
2. Try with a smaller scope or limit
|
|
95
|
+
3. Use cursor pagination to process in smaller batches`,
|
|
96
|
+
pagination_required: `💡 Note:
|
|
97
|
+
The result set is large. Use cursor pagination:
|
|
98
|
+
1. Check the 'truncation' field in the response
|
|
99
|
+
2. Pass the 'nextCursor' value in subsequent calls
|
|
100
|
+
3. Continue until truncated=false`,
|
|
101
|
+
validation_error: `💡 Recovery steps:
|
|
102
|
+
1. Check that all required parameters are provided
|
|
103
|
+
2. Verify parameter types match the schema
|
|
104
|
+
3. Review the error message for specific field issues`,
|
|
105
|
+
partial_success: `💡 Note:
|
|
106
|
+
Some operations succeeded but others failed:
|
|
107
|
+
1. Check the 'warnings' array for details on failed items
|
|
108
|
+
2. Address individual issues and retry failed items
|
|
109
|
+
3. Successfully processed items are already applied`,
|
|
75
110
|
unknown: `💡 If this error persists:
|
|
76
111
|
1. Check the error message for specific details
|
|
77
112
|
2. Verify your input parameters match the tool's schema
|