@qulib/mcp 0.4.2 → 0.5.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 CHANGED
@@ -24,6 +24,25 @@ To enable LLM-powered scenario generation, add your Anthropic API key to the
24
24
  Without this key, qulib still runs but uses built-in template scenarios only.
25
25
  Your key is never stored by qulib — it is read from your local config at runtime.
26
26
 
27
+ After updating this config, restart your MCP host (Claude Desktop / Claude Code / Cursor) so the new environment variables are picked up.
28
+
29
+ For verbose server-side stderr logs while troubleshooting host wiring, add:
30
+
31
+ ```json
32
+ {
33
+ "mcpServers": {
34
+ "qulib": {
35
+ "command": "npx",
36
+ "args": ["@qulib/mcp"],
37
+ "env": {
38
+ "ANTHROPIC_API_KEY": "sk-ant-...",
39
+ "QULIB_DEBUG": "1"
40
+ }
41
+ }
42
+ }
43
+ }
44
+ ```
45
+
27
46
  ## What it does
28
47
 
29
48
  Tools:
package/dist/index.js CHANGED
@@ -16,7 +16,7 @@ const requirePkg = createRequire(import.meta.url);
16
16
  const pkg = requirePkg('../package.json');
17
17
  import { analyzeApp, detectAuth, exploreAuth, scanRepo, computeAutomationMaturity, } from '@qulib/core';
18
18
  import { z } from 'zod';
19
- import { buildCompactAnalyzePayload } from './compact-analyze-payload.js';
19
+ import { summarizeAnalyzeResult } from './summarize-analyze-result.js';
20
20
  import { log } from './logger.js';
21
21
  function toolError(code, message, detail) {
22
22
  return {
@@ -202,7 +202,7 @@ mcpServer.registerTool('analyze_app', {
202
202
  progressLog: mcpProgressLog,
203
203
  telemetry: telemetrySink,
204
204
  });
205
- const payload = buildCompactAnalyzePayload(result, input.includeFullReport === true);
205
+ const payload = summarizeAnalyzeResult(result, input.includeFullReport === true);
206
206
  return {
207
207
  content: [
208
208
  {
@@ -0,0 +1,170 @@
1
+ import type { AnalyzeResult } from '@qulib/core';
2
+ export declare function summarizeAnalyzeResult(result: AnalyzeResult, includeFullReport: boolean): AnalyzeResult | {
3
+ includeFullReport: boolean;
4
+ note: string;
5
+ detectedAuth?: {
6
+ type: "unknown" | "form-login" | "oauth" | "magic-link" | "none";
7
+ loginUrl: string | null;
8
+ provider: string | null;
9
+ hasAuth: boolean;
10
+ observedSelectors: {
11
+ usernameSelector: string | null;
12
+ passwordSelector: string | null;
13
+ submitSelector: string | null;
14
+ } | null;
15
+ oauthButtons: {
16
+ text: string;
17
+ provider: string;
18
+ }[];
19
+ recommendation: string;
20
+ authOptions?: {
21
+ type: "unknown" | "form-login" | "oauth" | "oauth-unknown" | "form-multi" | "magic-link";
22
+ label: string;
23
+ id: string;
24
+ provider: string | null;
25
+ source: "built-in" | "user-local" | "heuristic";
26
+ automatable: boolean;
27
+ confidence: "high" | "medium" | "low";
28
+ requirements: {
29
+ method: "storage-state";
30
+ instruction: string;
31
+ } | {
32
+ method: "credentials";
33
+ fields: {
34
+ type: "password" | "text" | "email" | "select" | "checkbox";
35
+ name: string;
36
+ label: string;
37
+ observedOptions: string[];
38
+ }[];
39
+ } | {
40
+ method: "unknown";
41
+ instruction: string;
42
+ };
43
+ }[] | undefined;
44
+ } | undefined;
45
+ repoInventorySummary: {
46
+ framework?: {
47
+ primary: "unknown" | "nextjs-app-router" | "nextjs-pages-router" | "express" | "remix" | "nuxt" | "sveltekit" | "astro" | "vite";
48
+ confidence: "high" | "medium" | "low";
49
+ testFrameworks: ("playwright" | "cypress-e2e" | "cypress-component" | "jest" | "vitest" | "other")[];
50
+ evidenceCount: number;
51
+ } | undefined;
52
+ repoPath: string;
53
+ scannedAt: string;
54
+ routeCount: number;
55
+ testFileCount: number;
56
+ missingTestIdCount: number;
57
+ interactiveTsxFilesScanned: number | null;
58
+ cypressDetected: boolean;
59
+ } | null;
60
+ decisionLogPreview: {
61
+ timestamp: string;
62
+ reason: string;
63
+ phase: "observe" | "think" | "act" | "harness";
64
+ decision: string;
65
+ metadata?: Record<string, unknown> | undefined;
66
+ }[];
67
+ automationMaturitySummary?: {
68
+ overallScore: number;
69
+ level: number;
70
+ label: string;
71
+ topRecommendations: string[];
72
+ dimensions: {
73
+ guidance?: string | undefined;
74
+ dimension: "test-coverage-breadth" | "framework-adoption" | "test-id-hygiene" | "ci-integration" | "auth-test-coverage" | "component-test-ratio";
75
+ score: number;
76
+ applicability: "unknown" | "applicable" | "not_applicable";
77
+ }[];
78
+ } | undefined;
79
+ summary: {
80
+ status: import("@qulib/core").AnalyzeStatus;
81
+ coverageScore: number | null;
82
+ releaseConfidence: number | null;
83
+ mode: "url-only" | "url-repo" | "auth-required";
84
+ coveragePagesScanned: number;
85
+ coverageBudgetExceeded: boolean;
86
+ coverageWarning: "auth-required" | "budget-exceeded" | "low-coverage" | "navigation-failures" | null;
87
+ gapCount: number;
88
+ scenarioCount: number;
89
+ generatedTestCount: number;
90
+ publicSurface: {
91
+ pageCount: number;
92
+ gapCount: number;
93
+ accessibilityViolationCount: number;
94
+ brokenLinkCount: number;
95
+ } | null;
96
+ };
97
+ topGaps: {
98
+ path: string;
99
+ category: "untested-route" | "a11y" | "console-error" | "broken-link" | "auth-surface" | "coverage";
100
+ severity: "critical" | "high" | "medium" | "low";
101
+ reason: string;
102
+ }[];
103
+ costIntelligenceSummary: {
104
+ maxOutputTokensPerLlmCall: number;
105
+ usageDataQuality: "none" | "actual" | "estimated" | "mixed";
106
+ totalInputTokens: number;
107
+ totalOutputTokens: number;
108
+ budgetWarningCount: number;
109
+ maturityLevel: number;
110
+ maturityLabel: string;
111
+ } | null;
112
+ costIntelligence: {
113
+ maxOutputTokensPerLlmCall: number;
114
+ budgetRole: "max-output-tokens-per-llm-call";
115
+ records: {
116
+ provider: string;
117
+ model: string;
118
+ inputTokens: number;
119
+ outputTokens: number;
120
+ operationType: "scenario-generation";
121
+ timestamp: string;
122
+ dataQuality: "none" | "actual" | "estimated" | "mixed";
123
+ estimatedCostUsd?: number | undefined;
124
+ promptHash?: string | undefined;
125
+ resultHash?: string | undefined;
126
+ notes?: string | undefined;
127
+ }[];
128
+ budgetWarnings: string[];
129
+ usageSummary: {
130
+ dataQuality: "none" | "actual" | "estimated" | "mixed";
131
+ totalInputTokens: number;
132
+ totalOutputTokens: number;
133
+ };
134
+ repeatedOperations: {
135
+ recommendation: string;
136
+ promptHash: string;
137
+ count: number;
138
+ }[];
139
+ deterministicMaturity: {
140
+ label: string;
141
+ level: number;
142
+ rationale: string;
143
+ ceilingNote?: string | undefined;
144
+ };
145
+ conversionRecommendations: string[];
146
+ } | null;
147
+ nextDeterministicChecks: string[];
148
+ gapAnalysisPreview: {
149
+ analyzedAt: string;
150
+ gapsSample: {
151
+ path: string;
152
+ id: string;
153
+ severity: "critical" | "high" | "medium" | "low";
154
+ reason: string;
155
+ category: "untested-route" | "a11y" | "console-error" | "broken-link" | "auth-surface" | "coverage";
156
+ recommendation?: string | undefined;
157
+ description?: string | undefined;
158
+ }[];
159
+ scenariosOmitted: number;
160
+ generatedTestsOmitted: number;
161
+ };
162
+ routeInventorySummary: {
163
+ scannedAt: string;
164
+ baseUrl: string;
165
+ routeCount: number;
166
+ pagesSkipped: number;
167
+ budgetExceeded: boolean;
168
+ };
169
+ };
170
+ //# sourceMappingURL=summarize-analyze-result.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"summarize-analyze-result.d.ts","sourceRoot":"","sources":["../src/summarize-analyze-result.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAqBjD,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,aAAa,EAAE,iBAAiB,EAAE,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;4BA6G0td,CAAC;sBAA4C,CAAC;sBAA4C,CAAC;iBAAuC,CAAC;;;;;;;;;;;;;;;;;uBAAghB,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;EADr8e"}
@@ -0,0 +1,121 @@
1
+ const severityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
2
+ function topGapsBySeverity(gaps, limit) {
3
+ return [...gaps].sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]).slice(0, limit);
4
+ }
5
+ function nextDeterministicChecks(gaps, conversion) {
6
+ const out = [];
7
+ const byCat = new Map();
8
+ for (const g of gaps) {
9
+ byCat.set(g.category, (byCat.get(g.category) ?? 0) + 1);
10
+ }
11
+ for (const [cat, n] of [...byCat.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3)) {
12
+ out.push(`Add or tighten deterministic coverage for **${cat}** (${n} gap(s) in this scan).`);
13
+ }
14
+ out.push(...conversion.slice(0, 2));
15
+ return out.slice(0, 5);
16
+ }
17
+ export function summarizeAnalyzeResult(result, includeFullReport) {
18
+ if (includeFullReport) {
19
+ return result;
20
+ }
21
+ const g = result.gapAnalysis;
22
+ const ci = g.costIntelligence;
23
+ const top = topGapsBySeverity(result.gaps, 5);
24
+ const costSummary = ci
25
+ ? {
26
+ maxOutputTokensPerLlmCall: ci.maxOutputTokensPerLlmCall,
27
+ usageDataQuality: ci.usageSummary.dataQuality,
28
+ totalInputTokens: ci.usageSummary.totalInputTokens,
29
+ totalOutputTokens: ci.usageSummary.totalOutputTokens,
30
+ budgetWarningCount: ci.budgetWarnings.length,
31
+ maturityLevel: ci.deterministicMaturity.level,
32
+ maturityLabel: ci.deterministicMaturity.label,
33
+ }
34
+ : null;
35
+ const ps = result.publicSurface;
36
+ const repo = result.repoInventory;
37
+ const repoInventorySummary = repo
38
+ ? {
39
+ repoPath: repo.repoPath,
40
+ scannedAt: repo.scannedAt,
41
+ routeCount: repo.routes.length,
42
+ testFileCount: repo.testFiles.length,
43
+ missingTestIdCount: repo.missingTestIds.length,
44
+ interactiveTsxFilesScanned: repo.interactiveTsxFilesScanned ?? null,
45
+ cypressDetected: repo.cypressStructure.detected,
46
+ ...(repo.framework && {
47
+ framework: {
48
+ primary: repo.framework.primary,
49
+ confidence: repo.framework.confidence,
50
+ testFrameworks: repo.framework.testFrameworks,
51
+ evidenceCount: repo.framework.evidence.length,
52
+ },
53
+ }),
54
+ }
55
+ : null;
56
+ return {
57
+ summary: {
58
+ status: result.status,
59
+ coverageScore: result.coverageScore,
60
+ releaseConfidence: g.releaseConfidence,
61
+ mode: g.mode,
62
+ coveragePagesScanned: g.coveragePagesScanned,
63
+ coverageBudgetExceeded: g.coverageBudgetExceeded,
64
+ coverageWarning: g.coverageWarning ?? null,
65
+ gapCount: g.gaps.length,
66
+ scenarioCount: g.scenarios.length,
67
+ generatedTestCount: g.generatedTests.length,
68
+ publicSurface: ps === null
69
+ ? null
70
+ : {
71
+ pageCount: ps.pages.length,
72
+ gapCount: ps.gaps.length,
73
+ accessibilityViolationCount: ps.accessibilityViolations.length,
74
+ brokenLinkCount: ps.brokenLinks.length,
75
+ },
76
+ },
77
+ topGaps: top.map((x) => ({
78
+ path: x.path,
79
+ category: x.category,
80
+ severity: x.severity,
81
+ reason: x.reason,
82
+ })),
83
+ costIntelligenceSummary: costSummary,
84
+ costIntelligence: ci ?? null,
85
+ nextDeterministicChecks: ci
86
+ ? nextDeterministicChecks(result.gaps, ci.conversionRecommendations)
87
+ : nextDeterministicChecks(result.gaps, []),
88
+ gapAnalysisPreview: {
89
+ analyzedAt: g.analyzedAt,
90
+ gapsSample: g.gaps.slice(0, 8),
91
+ scenariosOmitted: g.scenarios.length,
92
+ generatedTestsOmitted: g.generatedTests.length,
93
+ },
94
+ routeInventorySummary: {
95
+ scannedAt: result.routeInventory.scannedAt,
96
+ baseUrl: result.routeInventory.baseUrl,
97
+ routeCount: result.routeInventory.routes.length,
98
+ pagesSkipped: result.routeInventory.pagesSkipped,
99
+ budgetExceeded: result.routeInventory.budgetExceeded,
100
+ },
101
+ ...(repo?.automationMaturity && {
102
+ automationMaturitySummary: {
103
+ overallScore: repo.automationMaturity.overallScore,
104
+ level: repo.automationMaturity.level,
105
+ label: repo.automationMaturity.label,
106
+ topRecommendations: repo.automationMaturity.topRecommendations,
107
+ dimensions: repo.automationMaturity.dimensions.map((d) => ({
108
+ dimension: d.dimension,
109
+ score: d.score,
110
+ applicability: d.applicability ?? 'applicable',
111
+ ...(d.guidance !== undefined && { guidance: d.guidance }),
112
+ })),
113
+ },
114
+ }),
115
+ repoInventorySummary,
116
+ decisionLogPreview: result.decisionLog.slice(-8),
117
+ ...(result.detectedAuth !== undefined && { detectedAuth: result.detectedAuth }),
118
+ includeFullReport: false,
119
+ note: 'Summary-first payload. Pass includeFullReport: true for the full gapAnalysis (all scenarios, generated tests) and the full repoInventory (test files, missing test IDs).',
120
+ };
121
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qulib/mcp",
3
- "version": "0.4.2",
3
+ "version": "0.5.0",
4
4
  "description": "MCP server for Qulib — AI-callable QA gap analysis",
5
5
  "license": "MIT",
6
6
  "author": "Tapesh Nagarwal",
@@ -29,11 +29,11 @@
29
29
  "scripts": {
30
30
  "build": "npm --prefix ../.. run build -w @qulib/core && tsc && chmod +x dist/index.js",
31
31
  "dev": "tsx src/index.ts",
32
- "test": "node --import tsx/esm --test src/compact-analyze-payload.test.ts"
32
+ "test": "node --import tsx/esm --test src/__tests__/summarize-analyze-result.test.ts"
33
33
  },
34
34
  "dependencies": {
35
35
  "@modelcontextprotocol/sdk": "^1.0.0",
36
- "@qulib/core": "0.4.2",
36
+ "@qulib/core": "0.5.0",
37
37
  "zod": "^3.23.0"
38
38
  },
39
39
  "devDependencies": {