@qulib/mcp 0.2.1 → 0.3.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 +35 -2
- package/dist/compact-analyze-payload.d.ts +144 -0
- package/dist/compact-analyze-payload.d.ts.map +1 -0
- package/dist/compact-analyze-payload.js +87 -0
- package/dist/index.js +86 -20
- package/dist/logger.d.ts +7 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +29 -0
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
|
|
7
7
|
Tools:
|
|
8
8
|
|
|
9
|
-
- **`
|
|
10
|
-
- **`
|
|
9
|
+
- **`explore_auth(url, timeoutMs?)`** — list all sign-in paths (OAuth, unknown SSO heuristics, forms, magic link) and what the agent must collect before `analyze_app`. Prefer this on unfamiliar apps.
|
|
10
|
+
- **`analyze_app`** — quality scan (optional form-login or storage-state auth). **Default payload is summary-first:** `summary`, `topGaps`, `costIntelligenceSummary`, `nextDeterministicChecks`, small previews. Set **`includeFullReport: true`** for the full `analyzeApp` result (all scenarios). Optional harness overrides: **`llmMaxOutputTokensPerCall`**, **`llmTokenBudget`** (legacy), **`testGenerationLimit`**, **`enableLlmScenarios`** (default true when omitted).
|
|
11
|
+
- **`detect_auth(url, timeoutMs?)`** — single-pattern auth guess with a short recommendation (lighter than `explore_auth`).
|
|
11
12
|
|
|
12
13
|
Returns from `analyze_app`:
|
|
13
14
|
|
|
@@ -52,6 +53,38 @@ This is a one-time step. You'll only need to do it again if Playwright's browser
|
|
|
52
53
|
|
|
53
54
|
If you skip this step, the first tool call will return a clear error telling you to run the command.
|
|
54
55
|
|
|
56
|
+
## Agentic auth exploration (`explore_auth`)
|
|
57
|
+
|
|
58
|
+
On unfamiliar apps, call **`explore_auth`** before **`analyze_app`**. The response lists each sign-in path (curated public OAuth/SSO, password forms, magic-link wording, and **heuristic** unknown buttons such as tenant-specific SSO). Each path includes **`requirements`** (e.g. storage-state vs credentials) and **`suggestedAgentBehavior`**.
|
|
59
|
+
|
|
60
|
+
When the model sees **`unrecognizedButtons`**, it can ask the user to register a label on the **MCP host** with the CLI:
|
|
61
|
+
|
|
62
|
+
`qulib auth providers add --id <kebab-id> --label "..." --pattern "..."` — patterns are saved under **`~/.qulib/providers.json`** and merged with the built-in list on the next `explore_auth` / `explore-auth`. Nothing is auto-written without an explicit `providers add`.
|
|
63
|
+
|
|
64
|
+
## Compact vs full `analyze_app` response
|
|
65
|
+
|
|
66
|
+
| | Default (`includeFullReport` omitted or false) | `includeFullReport: true` |
|
|
67
|
+
|--|--|--|
|
|
68
|
+
| Size | Small: top gaps, cost summary, next checks | Full `gapAnalysis` with every scenario |
|
|
69
|
+
| When to use | Routine agent turns, chat context limits | Deep dives, exporting full scenario JSON |
|
|
70
|
+
|
|
71
|
+
Example (full):
|
|
72
|
+
|
|
73
|
+
```json
|
|
74
|
+
{ "url": "https://example.com", "includeFullReport": true }
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Example (tighter LLM envelope from MCP):
|
|
78
|
+
|
|
79
|
+
```json
|
|
80
|
+
{
|
|
81
|
+
"url": "https://example.com",
|
|
82
|
+
"llmMaxOutputTokensPerCall": 2048,
|
|
83
|
+
"testGenerationLimit": 5,
|
|
84
|
+
"enableLlmScenarios": true
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
55
88
|
## Example usage
|
|
56
89
|
|
|
57
90
|
Ask Claude:
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import type { AnalyzeResult } from '@qulib/core';
|
|
2
|
+
export declare function buildCompactAnalyzePayload(result: AnalyzeResult, includeFullReport: boolean): AnalyzeResult | {
|
|
3
|
+
includeFullReport: boolean;
|
|
4
|
+
note: string;
|
|
5
|
+
detectedAuth?: {
|
|
6
|
+
type: "unknown" | "form-login" | "none" | "oauth" | "magic-link";
|
|
7
|
+
loginUrl: string | null;
|
|
8
|
+
hasAuth: boolean;
|
|
9
|
+
provider: string | null;
|
|
10
|
+
observedSelectors: {
|
|
11
|
+
usernameSelector: string | null;
|
|
12
|
+
passwordSelector: string | null;
|
|
13
|
+
submitSelector: string | null;
|
|
14
|
+
} | null;
|
|
15
|
+
oauthButtons: {
|
|
16
|
+
provider: string;
|
|
17
|
+
text: string;
|
|
18
|
+
}[];
|
|
19
|
+
recommendation: string;
|
|
20
|
+
} | undefined;
|
|
21
|
+
summary: {
|
|
22
|
+
status: import("@qulib/core").AnalyzeStatus;
|
|
23
|
+
coverageScore: number | null;
|
|
24
|
+
releaseConfidence: number | null;
|
|
25
|
+
mode: "url-only" | "url-repo" | "auth-required";
|
|
26
|
+
coveragePagesScanned: number;
|
|
27
|
+
coverageBudgetExceeded: boolean;
|
|
28
|
+
coverageWarning: "auth-required" | "budget-exceeded" | "low-coverage" | "navigation-failures" | null;
|
|
29
|
+
gapCount: number;
|
|
30
|
+
scenarioCount: number;
|
|
31
|
+
generatedTestCount: number;
|
|
32
|
+
publicSurface: {
|
|
33
|
+
pageCount: number;
|
|
34
|
+
gapCount: number;
|
|
35
|
+
accessibilityViolationCount: number;
|
|
36
|
+
brokenLinkCount: number;
|
|
37
|
+
} | null;
|
|
38
|
+
};
|
|
39
|
+
topGaps: {
|
|
40
|
+
path: string;
|
|
41
|
+
category: "untested-route" | "a11y" | "console-error" | "broken-link" | "auth-surface" | "coverage";
|
|
42
|
+
severity: "high" | "medium" | "low" | "critical";
|
|
43
|
+
reason: string;
|
|
44
|
+
}[];
|
|
45
|
+
costIntelligenceSummary: {
|
|
46
|
+
maxOutputTokensPerLlmCall: number;
|
|
47
|
+
usageDataQuality: "none" | "actual" | "estimated" | "mixed";
|
|
48
|
+
totalInputTokens: number;
|
|
49
|
+
totalOutputTokens: number;
|
|
50
|
+
budgetWarningCount: number;
|
|
51
|
+
maturityLevel: number;
|
|
52
|
+
maturityLabel: string;
|
|
53
|
+
} | null;
|
|
54
|
+
costIntelligence: {
|
|
55
|
+
maxOutputTokensPerLlmCall: number;
|
|
56
|
+
budgetRole: "max-output-tokens-per-llm-call";
|
|
57
|
+
records: {
|
|
58
|
+
provider: string;
|
|
59
|
+
model: string;
|
|
60
|
+
inputTokens: number;
|
|
61
|
+
outputTokens: number;
|
|
62
|
+
operationType: "scenario-generation";
|
|
63
|
+
timestamp: string;
|
|
64
|
+
dataQuality: "none" | "actual" | "estimated" | "mixed";
|
|
65
|
+
estimatedCostUsd?: number | undefined;
|
|
66
|
+
promptHash?: string | undefined;
|
|
67
|
+
resultHash?: string | undefined;
|
|
68
|
+
notes?: string | undefined;
|
|
69
|
+
}[];
|
|
70
|
+
budgetWarnings: string[];
|
|
71
|
+
usageSummary: {
|
|
72
|
+
dataQuality: "none" | "actual" | "estimated" | "mixed";
|
|
73
|
+
totalInputTokens: number;
|
|
74
|
+
totalOutputTokens: number;
|
|
75
|
+
};
|
|
76
|
+
repeatedOperations: {
|
|
77
|
+
recommendation: string;
|
|
78
|
+
promptHash: string;
|
|
79
|
+
count: number;
|
|
80
|
+
}[];
|
|
81
|
+
deterministicMaturity: {
|
|
82
|
+
label: string;
|
|
83
|
+
level: number;
|
|
84
|
+
rationale: string;
|
|
85
|
+
ceilingNote?: string | undefined;
|
|
86
|
+
};
|
|
87
|
+
conversionRecommendations: string[];
|
|
88
|
+
} | null;
|
|
89
|
+
nextDeterministicChecks: string[];
|
|
90
|
+
gapAnalysisPreview: {
|
|
91
|
+
analyzedAt: string;
|
|
92
|
+
gapsSample: {
|
|
93
|
+
path: string;
|
|
94
|
+
id: string;
|
|
95
|
+
severity: "high" | "medium" | "low" | "critical";
|
|
96
|
+
reason: string;
|
|
97
|
+
category: "untested-route" | "a11y" | "console-error" | "broken-link" | "auth-surface" | "coverage";
|
|
98
|
+
recommendation?: string | undefined;
|
|
99
|
+
description?: string | undefined;
|
|
100
|
+
}[];
|
|
101
|
+
scenariosOmitted: number;
|
|
102
|
+
generatedTestsOmitted: number;
|
|
103
|
+
};
|
|
104
|
+
routeInventorySummary: {
|
|
105
|
+
scannedAt: string;
|
|
106
|
+
baseUrl: string;
|
|
107
|
+
routeCount: number;
|
|
108
|
+
pagesSkipped: number;
|
|
109
|
+
budgetExceeded: boolean;
|
|
110
|
+
};
|
|
111
|
+
repoInventory: {
|
|
112
|
+
scannedAt: string;
|
|
113
|
+
routes: {
|
|
114
|
+
path: string;
|
|
115
|
+
method: "unknown" | "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
|
|
116
|
+
file: string;
|
|
117
|
+
}[];
|
|
118
|
+
repoPath: string;
|
|
119
|
+
testFiles: {
|
|
120
|
+
type: "playwright" | "cypress-e2e" | "cypress-component" | "jest" | "vitest" | "other";
|
|
121
|
+
file: string;
|
|
122
|
+
coveredPaths: string[];
|
|
123
|
+
}[];
|
|
124
|
+
missingTestIds: string[];
|
|
125
|
+
cypressStructure: {
|
|
126
|
+
detected: boolean;
|
|
127
|
+
hasCommandsFile: boolean;
|
|
128
|
+
existingE2eFiles: string[];
|
|
129
|
+
existingComponentFiles: string[];
|
|
130
|
+
e2eFolder?: string | undefined;
|
|
131
|
+
componentFolder?: string | undefined;
|
|
132
|
+
fixturesFolder?: string | undefined;
|
|
133
|
+
supportFolder?: string | undefined;
|
|
134
|
+
};
|
|
135
|
+
} | null;
|
|
136
|
+
decisionLogPreview: {
|
|
137
|
+
timestamp: string;
|
|
138
|
+
reason: string;
|
|
139
|
+
phase: "observe" | "think" | "act" | "harness";
|
|
140
|
+
decision: string;
|
|
141
|
+
metadata?: Record<string, unknown> | undefined;
|
|
142
|
+
}[];
|
|
143
|
+
};
|
|
144
|
+
//# sourceMappingURL=compact-analyze-payload.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compact-analyze-payload.d.ts","sourceRoot":"","sources":["../src/compact-analyze-payload.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAqBjD,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,aAAa,EAAE,iBAAiB,EAAE,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;4BA0E0igB,CAAC;sBAA4C,CAAC;sBAA4C,CAAC;iBAAuC,CAAC;;;;;;;;;;;;;;;;;uBAAghB,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBAAt7e,CAAC;2BAA6C,CAAC;0BAA4C,CAAC;yBAA2C,CAAC;;;;;;;;;;EAD3+C"}
|
|
@@ -0,0 +1,87 @@
|
|
|
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 buildCompactAnalyzePayload(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
|
+
return {
|
|
37
|
+
summary: {
|
|
38
|
+
status: result.status,
|
|
39
|
+
coverageScore: result.coverageScore,
|
|
40
|
+
releaseConfidence: g.releaseConfidence,
|
|
41
|
+
mode: g.mode,
|
|
42
|
+
coveragePagesScanned: g.coveragePagesScanned,
|
|
43
|
+
coverageBudgetExceeded: g.coverageBudgetExceeded,
|
|
44
|
+
coverageWarning: g.coverageWarning ?? null,
|
|
45
|
+
gapCount: g.gaps.length,
|
|
46
|
+
scenarioCount: g.scenarios.length,
|
|
47
|
+
generatedTestCount: g.generatedTests.length,
|
|
48
|
+
publicSurface: ps === null
|
|
49
|
+
? null
|
|
50
|
+
: {
|
|
51
|
+
pageCount: ps.pages.length,
|
|
52
|
+
gapCount: ps.gaps.length,
|
|
53
|
+
accessibilityViolationCount: ps.accessibilityViolations.length,
|
|
54
|
+
brokenLinkCount: ps.brokenLinks.length,
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
topGaps: top.map((x) => ({
|
|
58
|
+
path: x.path,
|
|
59
|
+
category: x.category,
|
|
60
|
+
severity: x.severity,
|
|
61
|
+
reason: x.reason,
|
|
62
|
+
})),
|
|
63
|
+
costIntelligenceSummary: costSummary,
|
|
64
|
+
costIntelligence: ci ?? null,
|
|
65
|
+
nextDeterministicChecks: ci
|
|
66
|
+
? nextDeterministicChecks(result.gaps, ci.conversionRecommendations)
|
|
67
|
+
: nextDeterministicChecks(result.gaps, []),
|
|
68
|
+
gapAnalysisPreview: {
|
|
69
|
+
analyzedAt: g.analyzedAt,
|
|
70
|
+
gapsSample: g.gaps.slice(0, 8),
|
|
71
|
+
scenariosOmitted: g.scenarios.length,
|
|
72
|
+
generatedTestsOmitted: g.generatedTests.length,
|
|
73
|
+
},
|
|
74
|
+
routeInventorySummary: {
|
|
75
|
+
scannedAt: result.routeInventory.scannedAt,
|
|
76
|
+
baseUrl: result.routeInventory.baseUrl,
|
|
77
|
+
routeCount: result.routeInventory.routes.length,
|
|
78
|
+
pagesSkipped: result.routeInventory.pagesSkipped,
|
|
79
|
+
budgetExceeded: result.routeInventory.budgetExceeded,
|
|
80
|
+
},
|
|
81
|
+
repoInventory: result.repoInventory,
|
|
82
|
+
decisionLogPreview: result.decisionLog.slice(-8),
|
|
83
|
+
...(result.detectedAuth !== undefined && { detectedAuth: result.detectedAuth }),
|
|
84
|
+
includeFullReport: false,
|
|
85
|
+
note: 'Summary-first payload. Pass includeFullReport: true for full gapAnalysis (all scenarios and generatedTests).',
|
|
86
|
+
};
|
|
87
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -2,8 +2,16 @@
|
|
|
2
2
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
3
3
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
4
|
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
5
|
-
import { analyzeApp, detectAuth } from '@qulib/core';
|
|
5
|
+
import { analyzeApp, detectAuth, exploreAuth } from '@qulib/core';
|
|
6
6
|
import { z } from 'zod';
|
|
7
|
+
import { buildCompactAnalyzePayload } from './compact-analyze-payload.js';
|
|
8
|
+
import { log } from './logger.js';
|
|
9
|
+
const mcpProgressLog = {
|
|
10
|
+
info: (message) => log.info(message),
|
|
11
|
+
warn: (message) => log.warn(message),
|
|
12
|
+
error: (message) => log.error(message),
|
|
13
|
+
debug: (message) => log.debug(message),
|
|
14
|
+
};
|
|
7
15
|
const FormLoginMcpAuthSchema = z.object({
|
|
8
16
|
type: z.literal('form-login'),
|
|
9
17
|
loginUrl: z.string().url(),
|
|
@@ -23,6 +31,11 @@ const AnalyzeInputSchema = z.object({
|
|
|
23
31
|
maxPagesToScan: z.number().int().min(1).max(50).optional(),
|
|
24
32
|
timeoutMs: z.number().int().positive().optional(),
|
|
25
33
|
auth: z.discriminatedUnion('type', [FormLoginMcpAuthSchema, StorageStateMcpAuthSchema]).optional(),
|
|
34
|
+
includeFullReport: z.boolean().optional(),
|
|
35
|
+
llmTokenBudget: z.number().int().positive().optional(),
|
|
36
|
+
llmMaxOutputTokensPerCall: z.number().int().positive().optional(),
|
|
37
|
+
testGenerationLimit: z.number().int().positive().max(50).optional(),
|
|
38
|
+
enableLlmScenarios: z.boolean().optional(),
|
|
26
39
|
});
|
|
27
40
|
const server = new Server({
|
|
28
41
|
name: 'qulib-mcp',
|
|
@@ -34,9 +47,21 @@ const server = new Server({
|
|
|
34
47
|
});
|
|
35
48
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
36
49
|
tools: [
|
|
50
|
+
{
|
|
51
|
+
name: 'explore_auth',
|
|
52
|
+
description: 'Use this BEFORE analyze_app when scanning unfamiliar apps. Returns all detected sign-in paths with per-path requirements describing what credentials or actions the agent must collect from the user before calling analyze_app. Combines built-in OAuth/SSO labels, user-local patterns from ~/.qulib/providers.json, and heuristic unknown buttons.',
|
|
53
|
+
inputSchema: {
|
|
54
|
+
type: 'object',
|
|
55
|
+
properties: {
|
|
56
|
+
url: { type: 'string', description: 'Full URL of the deployed app or login page' },
|
|
57
|
+
timeoutMs: { type: 'number', description: 'Navigation timeout in milliseconds (default 20000)' },
|
|
58
|
+
},
|
|
59
|
+
required: ['url'],
|
|
60
|
+
},
|
|
61
|
+
},
|
|
37
62
|
{
|
|
38
63
|
name: 'analyze_app',
|
|
39
|
-
description: 'Analyze a deployed web app for quality gaps.
|
|
64
|
+
description: 'Analyze a deployed web app for quality gaps. Default response is summary-first (top gaps, cost summary, next checks). Set includeFullReport for the full gapAnalysis. Optional llmMaxOutputTokensPerCall / llmTokenBudget (legacy), testGenerationLimit, enableLlmScenarios align with @qulib/core HarnessConfig.',
|
|
40
65
|
inputSchema: {
|
|
41
66
|
type: 'object',
|
|
42
67
|
properties: {
|
|
@@ -78,6 +103,23 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
78
103
|
},
|
|
79
104
|
],
|
|
80
105
|
},
|
|
106
|
+
includeFullReport: {
|
|
107
|
+
type: 'boolean',
|
|
108
|
+
description: 'When true, returns the full analyzeApp payload including all scenarios. Default false returns a summary-first shape.',
|
|
109
|
+
},
|
|
110
|
+
llmTokenBudget: {
|
|
111
|
+
type: 'number',
|
|
112
|
+
description: 'Legacy per-completion max output tokens (same as HarnessConfig.llmTokenBudget). Prefer llmMaxOutputTokensPerCall when both are set.',
|
|
113
|
+
},
|
|
114
|
+
llmMaxOutputTokensPerCall: {
|
|
115
|
+
type: 'number',
|
|
116
|
+
description: 'Optional override for per-completion max output tokens (maps to HarnessConfig.llmMaxOutputTokensPerCall).',
|
|
117
|
+
},
|
|
118
|
+
testGenerationLimit: { type: 'number', description: 'Max gaps fed into scenario generation (default 5).' },
|
|
119
|
+
enableLlmScenarios: {
|
|
120
|
+
type: 'boolean',
|
|
121
|
+
description: 'When false, never calls an LLM for scenarios (default true when omitted).',
|
|
122
|
+
},
|
|
81
123
|
},
|
|
82
124
|
required: ['url'],
|
|
83
125
|
},
|
|
@@ -97,6 +139,20 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
97
139
|
],
|
|
98
140
|
}));
|
|
99
141
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
142
|
+
if (request.params.name === 'explore_auth') {
|
|
143
|
+
const { url, timeoutMs } = z
|
|
144
|
+
.object({
|
|
145
|
+
url: z.string().url(),
|
|
146
|
+
timeoutMs: z.number().int().positive().optional(),
|
|
147
|
+
})
|
|
148
|
+
.parse(request.params.arguments ?? {});
|
|
149
|
+
log.info(`explore_auth tool url=${url} timeoutMs=${timeoutMs ?? 20000}`);
|
|
150
|
+
const result = await exploreAuth(url, timeoutMs, mcpProgressLog);
|
|
151
|
+
log.info(`explore_auth tool done authRequired=${result.authRequired} paths=${result.authPaths.length}`);
|
|
152
|
+
return {
|
|
153
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
154
|
+
};
|
|
155
|
+
}
|
|
100
156
|
if (request.params.name === 'detect_auth') {
|
|
101
157
|
const { url, timeoutMs } = z
|
|
102
158
|
.object({
|
|
@@ -104,7 +160,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
104
160
|
timeoutMs: z.number().int().positive().optional(),
|
|
105
161
|
})
|
|
106
162
|
.parse(request.params.arguments ?? {});
|
|
107
|
-
|
|
163
|
+
log.info(`detect_auth tool url=${url} timeoutMs=${timeoutMs ?? 15000}`);
|
|
164
|
+
const result = await detectAuth(url, timeoutMs, mcpProgressLog);
|
|
165
|
+
const providerSummary = result.oauthButtons.length > 0
|
|
166
|
+
? result.oauthButtons.map((b) => b.provider).join(', ')
|
|
167
|
+
: result.provider ?? 'none';
|
|
168
|
+
log.info(`detect_auth tool done type=${result.type} providers=${providerSummary} automatable=${result.type === 'form-login'}`);
|
|
108
169
|
return {
|
|
109
170
|
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
110
171
|
};
|
|
@@ -133,31 +194,36 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
133
194
|
: input.auth?.type === 'storage-state'
|
|
134
195
|
? { type: 'storage-state', path: input.auth.path }
|
|
135
196
|
: undefined;
|
|
197
|
+
const harnessConfig = {
|
|
198
|
+
maxPagesToScan: input.maxPagesToScan ?? 10,
|
|
199
|
+
maxDepth: 3,
|
|
200
|
+
minPagesForConfidence: 3,
|
|
201
|
+
timeoutMs: input.timeoutMs ?? 30000,
|
|
202
|
+
retryCount: 0,
|
|
203
|
+
llmTokenBudget: input.llmTokenBudget ?? input.llmMaxOutputTokensPerCall ?? 4096,
|
|
204
|
+
llmMaxOutputTokensPerCall: input.llmMaxOutputTokensPerCall,
|
|
205
|
+
testGenerationLimit: input.testGenerationLimit ?? 5,
|
|
206
|
+
enableLlmScenarios: input.enableLlmScenarios !== false,
|
|
207
|
+
readOnlyMode: true,
|
|
208
|
+
requireHumanReview: false,
|
|
209
|
+
failOnConsoleError: false,
|
|
210
|
+
explorer: 'playwright',
|
|
211
|
+
defaultAdapter: 'playwright',
|
|
212
|
+
adapters: ['playwright'],
|
|
213
|
+
...(authConfig && { auth: authConfig }),
|
|
214
|
+
};
|
|
136
215
|
const result = await analyzeApp({
|
|
137
216
|
url: input.url,
|
|
138
217
|
writeArtifacts: false,
|
|
139
|
-
config:
|
|
140
|
-
|
|
141
|
-
maxDepth: 3,
|
|
142
|
-
minPagesForConfidence: 3,
|
|
143
|
-
timeoutMs: input.timeoutMs ?? 30000,
|
|
144
|
-
retryCount: 0,
|
|
145
|
-
llmTokenBudget: 1,
|
|
146
|
-
testGenerationLimit: 1,
|
|
147
|
-
readOnlyMode: true,
|
|
148
|
-
requireHumanReview: false,
|
|
149
|
-
failOnConsoleError: false,
|
|
150
|
-
explorer: 'playwright',
|
|
151
|
-
defaultAdapter: 'playwright',
|
|
152
|
-
adapters: ['playwright'],
|
|
153
|
-
...(authConfig && { auth: authConfig }),
|
|
154
|
-
},
|
|
218
|
+
config: harnessConfig,
|
|
219
|
+
progressLog: mcpProgressLog,
|
|
155
220
|
});
|
|
221
|
+
const payload = buildCompactAnalyzePayload(result, input.includeFullReport === true);
|
|
156
222
|
return {
|
|
157
223
|
content: [
|
|
158
224
|
{
|
|
159
225
|
type: 'text',
|
|
160
|
-
text: JSON.stringify(
|
|
226
|
+
text: JSON.stringify(payload, null, 2),
|
|
161
227
|
},
|
|
162
228
|
],
|
|
163
229
|
};
|
package/dist/logger.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"AAiBA,eAAO,MAAM,GAAG;kBACA,MAAM,GAAG,IAAI;kBAGb,MAAM,GAAG,IAAI;mBAGZ,MAAM,GAAG,IAAI;mBAGb,MAAM,GAAG,IAAI;CAK7B,CAAC"}
|
package/dist/logger.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
function pad2(n) {
|
|
2
|
+
return String(n).padStart(2, '0');
|
|
3
|
+
}
|
|
4
|
+
function wallTime() {
|
|
5
|
+
const d = new Date();
|
|
6
|
+
return `${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}`;
|
|
7
|
+
}
|
|
8
|
+
function isDebugEnabled() {
|
|
9
|
+
return process.env.QULIB_DEBUG === '1';
|
|
10
|
+
}
|
|
11
|
+
function emit(level, message) {
|
|
12
|
+
process.stderr.write(`[qulib ${wallTime()}] ${level} ${message}\n`);
|
|
13
|
+
}
|
|
14
|
+
export const log = {
|
|
15
|
+
info(message) {
|
|
16
|
+
emit('INFO', message);
|
|
17
|
+
},
|
|
18
|
+
warn(message) {
|
|
19
|
+
emit('WARN', message);
|
|
20
|
+
},
|
|
21
|
+
error(message) {
|
|
22
|
+
emit('ERROR', message);
|
|
23
|
+
},
|
|
24
|
+
debug(message) {
|
|
25
|
+
if (isDebugEnabled()) {
|
|
26
|
+
emit('DEBUG', message);
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qulib/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "MCP server for Qulib — AI-callable QA gap analysis",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Tapesh Nagarwal",
|
|
@@ -27,12 +27,13 @@
|
|
|
27
27
|
"README.md"
|
|
28
28
|
],
|
|
29
29
|
"scripts": {
|
|
30
|
-
"build": "tsc && chmod +x dist/index.js",
|
|
31
|
-
"dev": "tsx src/index.ts"
|
|
30
|
+
"build": "npm --prefix ../.. run build -w @qulib/core && tsc && chmod +x dist/index.js",
|
|
31
|
+
"dev": "tsx src/index.ts",
|
|
32
|
+
"test": "node --import tsx/esm --test src/compact-analyze-payload.test.ts"
|
|
32
33
|
},
|
|
33
34
|
"dependencies": {
|
|
34
|
-
"@qulib/core": "0.2.1",
|
|
35
35
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
36
|
+
"@qulib/core": "0.3.0",
|
|
36
37
|
"zod": "^3.23.0"
|
|
37
38
|
},
|
|
38
39
|
"devDependencies": {
|