@qulib/mcp 0.3.1 → 0.4.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 CHANGED
@@ -2,6 +2,28 @@
2
2
 
3
3
  **@qulib/mcp** is an MCP server that exposes Qulib so AI clients can analyze a deployed URL for release confidence, accessibility, broken links, console noise, and prioritized gaps (CLI entry `qulib-mcp`).
4
4
 
5
+ ## Setup
6
+
7
+ To enable LLM-powered scenario generation, add your Anthropic API key to the
8
+ `env` block in your MCP host config (Claude Desktop, Claude Code, Cursor, etc.):
9
+
10
+ ```json
11
+ {
12
+ "mcpServers": {
13
+ "qulib": {
14
+ "command": "npx",
15
+ "args": ["@qulib/mcp"],
16
+ "env": {
17
+ "ANTHROPIC_API_KEY": "sk-ant-..."
18
+ }
19
+ }
20
+ }
21
+ }
22
+ ```
23
+
24
+ Without this key, qulib still runs but uses built-in template scenarios only.
25
+ Your key is never stored by qulib — it is read from your local config at runtime.
26
+
5
27
  ## What it does
6
28
 
7
29
  Tools:
@@ -3,20 +3,103 @@ export declare function buildCompactAnalyzePayload(result: AnalyzeResult, includ
3
3
  includeFullReport: boolean;
4
4
  note: string;
5
5
  detectedAuth?: {
6
- type: "unknown" | "form-login" | "none" | "oauth" | "magic-link";
6
+ type: "unknown" | "form-login" | "oauth" | "magic-link" | "none";
7
7
  loginUrl: string | null;
8
- hasAuth: boolean;
9
8
  provider: string | null;
9
+ hasAuth: boolean;
10
10
  observedSelectors: {
11
11
  usernameSelector: string | null;
12
12
  passwordSelector: string | null;
13
13
  submitSelector: string | null;
14
14
  } | null;
15
15
  oauthButtons: {
16
- provider: string;
17
16
  text: string;
17
+ provider: string;
18
18
  }[];
19
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
+ repoInventory: {
46
+ scannedAt: string;
47
+ routes: {
48
+ path: string;
49
+ method: "unknown" | "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
50
+ file: string;
51
+ }[];
52
+ repoPath: string;
53
+ testFiles: {
54
+ type: "playwright" | "cypress-e2e" | "cypress-component" | "jest" | "vitest" | "other";
55
+ file: string;
56
+ coveredPaths: string[];
57
+ }[];
58
+ missingTestIds: string[];
59
+ cypressStructure: {
60
+ detected: boolean;
61
+ hasCommandsFile: boolean;
62
+ existingE2eFiles: string[];
63
+ existingComponentFiles: string[];
64
+ e2eFolder?: string | undefined;
65
+ componentFolder?: string | undefined;
66
+ fixturesFolder?: string | undefined;
67
+ supportFolder?: string | undefined;
68
+ };
69
+ framework?: {
70
+ confidence: "high" | "medium" | "low";
71
+ evidence: string[];
72
+ primary: "unknown" | "nextjs-app-router" | "nextjs-pages-router" | "express" | "remix" | "nuxt" | "sveltekit" | "astro" | "vite";
73
+ testFrameworks: ("playwright" | "cypress-e2e" | "cypress-component" | "jest" | "vitest" | "other")[];
74
+ } | undefined;
75
+ automationMaturity?: {
76
+ label: string;
77
+ level: number;
78
+ computedAt: string;
79
+ repoPath: string;
80
+ overallScore: number;
81
+ dimensions: {
82
+ recommendations: string[];
83
+ dimension: "test-coverage-breadth" | "framework-adoption" | "test-id-hygiene" | "ci-integration" | "auth-test-coverage" | "component-test-ratio";
84
+ score: number;
85
+ weight: number;
86
+ evidence: string[];
87
+ }[];
88
+ topRecommendations: string[];
89
+ } | undefined;
90
+ } | null;
91
+ decisionLogPreview: {
92
+ timestamp: string;
93
+ reason: string;
94
+ phase: "observe" | "think" | "act" | "harness";
95
+ decision: string;
96
+ metadata?: Record<string, unknown> | undefined;
97
+ }[];
98
+ automationMaturitySummary?: {
99
+ overallScore: number;
100
+ level: number;
101
+ label: string;
102
+ topRecommendations: string[];
20
103
  } | undefined;
21
104
  summary: {
22
105
  status: import("@qulib/core").AnalyzeStatus;
@@ -39,7 +122,7 @@ export declare function buildCompactAnalyzePayload(result: AnalyzeResult, includ
39
122
  topGaps: {
40
123
  path: string;
41
124
  category: "untested-route" | "a11y" | "console-error" | "broken-link" | "auth-surface" | "coverage";
42
- severity: "high" | "medium" | "low" | "critical";
125
+ severity: "critical" | "high" | "medium" | "low";
43
126
  reason: string;
44
127
  }[];
45
128
  costIntelligenceSummary: {
@@ -92,7 +175,7 @@ export declare function buildCompactAnalyzePayload(result: AnalyzeResult, includ
92
175
  gapsSample: {
93
176
  path: string;
94
177
  id: string;
95
- severity: "high" | "medium" | "low" | "critical";
178
+ severity: "critical" | "high" | "medium" | "low";
96
179
  reason: string;
97
180
  category: "untested-route" | "a11y" | "console-error" | "broken-link" | "auth-surface" | "coverage";
98
181
  recommendation?: string | undefined;
@@ -108,37 +191,5 @@ export declare function buildCompactAnalyzePayload(result: AnalyzeResult, includ
108
191
  pagesSkipped: number;
109
192
  budgetExceeded: boolean;
110
193
  };
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
194
  };
144
195
  //# sourceMappingURL=compact-analyze-payload.d.ts.map
@@ -1 +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"}
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBAkFs3L,CAAC;2BAA6C,CAAC;0BAA4C,CAAC;yBAA2C,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;4BAAkqT,CAAC;sBAA4C,CAAC;sBAA4C,CAAC;iBAAuC,CAAC;;;;;;;;;;;;;;;;;uBAAghB,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;EAD/4gB"}
@@ -78,6 +78,14 @@ export function buildCompactAnalyzePayload(result, includeFullReport) {
78
78
  pagesSkipped: result.routeInventory.pagesSkipped,
79
79
  budgetExceeded: result.routeInventory.budgetExceeded,
80
80
  },
81
+ ...(result.repoInventory?.automationMaturity && {
82
+ automationMaturitySummary: {
83
+ overallScore: result.repoInventory.automationMaturity.overallScore,
84
+ level: result.repoInventory.automationMaturity.level,
85
+ label: result.repoInventory.automationMaturity.label,
86
+ topRecommendations: result.repoInventory.automationMaturity.topRecommendations,
87
+ },
88
+ }),
81
89
  repoInventory: result.repoInventory,
82
90
  decisionLogPreview: result.decisionLog.slice(-8),
83
91
  ...(result.detectedAuth !== undefined && { detectedAuth: result.detectedAuth }),
package/dist/index.js CHANGED
@@ -1,11 +1,38 @@
1
1
  #!/usr/bin/env node
2
- import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
+ // Naming convention: qulib_{verb}_{noun}. Existing 3 tools predate this convention and retain their names for backwards compatibility.
3
+ //
4
+ // TODO(@qulib/mcp): When tool count exceeds ~10, evaluate MCP resource types and
5
+ // prompt templates as complementary surfaces. Tool explosion is an MCP anti-pattern.
6
+ // Prefer composable tools over one-tool-per-capability.
7
+ //
8
+ // TODO(@qulib/mcp): Evaluate tool-level permission modeling when MCP spec stabilizes.
9
+ // Today: all tools are equally trusted. Future: read-only tools (detect_auth, explore_auth)
10
+ // vs. write-capable tools (analyze_app with writeArtifacts) should carry different trust levels.
11
+ import { isAbsolute, normalize, resolve } from 'node:path';
12
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
13
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
- import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
5
- import { analyzeApp, detectAuth, exploreAuth } from '@qulib/core';
14
+ import { analyzeApp, detectAuth, exploreAuth, scanRepo, computeAutomationMaturity, } from '@qulib/core';
6
15
  import { z } from 'zod';
7
16
  import { buildCompactAnalyzePayload } from './compact-analyze-payload.js';
8
17
  import { log } from './logger.js';
18
+ function toolError(code, message, detail) {
19
+ return {
20
+ content: [
21
+ {
22
+ type: 'text',
23
+ text: JSON.stringify({ error: { code, message, detail: detail ?? null } }, null, 2),
24
+ },
25
+ ],
26
+ };
27
+ }
28
+ function stderrTelemetrySink() {
29
+ return {
30
+ emit(event) {
31
+ process.stderr.write(`${JSON.stringify(event)}\n`);
32
+ },
33
+ };
34
+ }
35
+ const telemetrySink = process.env.QULIB_TELEMETRY_STDERR === '1' ? stderrTelemetrySink() : undefined;
9
36
  const mcpProgressLog = {
10
37
  info: (message) => log.info(message),
11
38
  warn: (message) => log.warn(message),
@@ -37,115 +64,51 @@ const AnalyzeInputSchema = z.object({
37
64
  testGenerationLimit: z.number().int().positive().max(50).optional(),
38
65
  enableLlmScenarios: z.boolean().optional(),
39
66
  });
40
- const server = new Server({
67
+ const ScoreAutomationInputSchema = z.object({
68
+ repoPath: z.string().describe('Absolute path to the automation repository on the MCP host filesystem'),
69
+ includeFullDimensions: z
70
+ .boolean()
71
+ .optional()
72
+ .describe('When true, includes all dimension detail. Default false returns top recommendations only.'),
73
+ });
74
+ function validateAbsoluteRepoPath(repoPath) {
75
+ const norm = normalize(repoPath.trim());
76
+ if (!norm || norm === '.' || norm.includes('..')) {
77
+ throw new Error('repoPath must be absolute and must not contain path traversal segments');
78
+ }
79
+ if (!isAbsolute(norm)) {
80
+ throw new Error('repoPath must be an absolute path on the MCP host');
81
+ }
82
+ return resolve(norm);
83
+ }
84
+ const mcpServer = new McpServer({
41
85
  name: 'qulib-mcp',
42
- version: '0.1.0',
86
+ version: '0.4.1',
87
+ description: 'Qulib QA intelligence platform — gap analysis, auth exploration, and quality scoring for deployed web applications',
43
88
  }, {
44
89
  capabilities: {
45
90
  tools: {},
46
91
  },
47
92
  });
48
- server.setRequestHandler(ListToolsRequestSchema, async () => ({
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
- },
62
- {
63
- name: 'analyze_app',
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.',
65
- inputSchema: {
66
- type: 'object',
67
- properties: {
68
- url: { type: 'string', description: 'Full URL of the deployed app' },
69
- maxPagesToScan: { type: 'number', description: 'Max pages to crawl (default 10)' },
70
- timeoutMs: { type: 'number', description: 'Per-page timeout in milliseconds (default 30000)' },
71
- auth: {
72
- description: 'Optional auth: form-login credentials or path to a storage state JSON from `qulib auth init`',
73
- oneOf: [
74
- {
75
- type: 'object',
76
- properties: {
77
- type: { type: 'string', enum: ['form-login'] },
78
- loginUrl: { type: 'string' },
79
- username: { type: 'string' },
80
- password: { type: 'string' },
81
- usernameSelector: { type: 'string' },
82
- passwordSelector: { type: 'string' },
83
- submitSelector: { type: 'string' },
84
- successUrlContains: { type: 'string' },
85
- },
86
- required: [
87
- 'type',
88
- 'loginUrl',
89
- 'username',
90
- 'password',
91
- 'usernameSelector',
92
- 'passwordSelector',
93
- 'submitSelector',
94
- ],
95
- },
96
- {
97
- type: 'object',
98
- properties: {
99
- type: { type: 'string', enum: ['storage-state'] },
100
- path: { type: 'string', description: 'Absolute path to storage state JSON on the MCP host' },
101
- },
102
- required: ['type', 'path'],
103
- },
104
- ],
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
- },
123
- },
124
- required: ['url'],
125
- },
126
- },
127
- {
128
- name: 'detect_auth',
129
- description: 'Detect the authentication pattern used by a deployed web app. Returns the auth type (form-login, oauth, magic-link, none, or unknown) and a recommendation for how to configure qulib to scan past it.',
130
- inputSchema: {
131
- type: 'object',
132
- properties: {
133
- url: { type: 'string', description: 'Full URL of the deployed app or login page' },
134
- timeoutMs: { type: 'number', description: 'Page load timeout in milliseconds (default 15000)' },
135
- },
136
- required: ['url'],
137
- },
138
- },
139
- ],
140
- }));
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 ?? {});
93
+ if (!process.env.ANTHROPIC_API_KEY) {
94
+ process.stderr.write('[qulib] WARN ANTHROPIC_API_KEY is not set.\n' +
95
+ ' LLM scenario generation will be skipped — only template scenarios will run.\n' +
96
+ ' Add your key to the env block in your MCP host config.\n' +
97
+ ' See: https://github.com/TapeshN/qulib#setup\n');
98
+ }
99
+ const ExploreAuthToolInputSchema = z.object({
100
+ url: z.string().url().describe('Full URL of the deployed app or login page'),
101
+ timeoutMs: z.number().int().positive().optional().describe('Navigation timeout in milliseconds (default 20000)'),
102
+ });
103
+ const DetectAuthToolInputSchema = z.object({
104
+ url: z.string().url().describe('Full URL of the deployed app or login page'),
105
+ timeoutMs: z.number().int().positive().optional().describe('Page load timeout in milliseconds (default 15000)'),
106
+ });
107
+ mcpServer.registerTool('explore_auth', {
108
+ 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.',
109
+ inputSchema: ExploreAuthToolInputSchema,
110
+ }, async ({ url, timeoutMs }) => {
111
+ try {
149
112
  log.info(`explore_auth tool url=${url} timeoutMs=${timeoutMs ?? 20000}`);
150
113
  const result = await exploreAuth(url, timeoutMs, mcpProgressLog);
151
114
  log.info(`explore_auth tool done authRequired=${result.authRequired} paths=${result.authPaths.length}`);
@@ -153,13 +116,17 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
153
116
  content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
154
117
  };
155
118
  }
156
- if (request.params.name === 'detect_auth') {
157
- const { url, timeoutMs } = z
158
- .object({
159
- url: z.string().url(),
160
- timeoutMs: z.number().int().positive().optional(),
161
- })
162
- .parse(request.params.arguments ?? {});
119
+ catch (err) {
120
+ const msg = err instanceof Error ? err.message : String(err);
121
+ log.error(`explore_auth failed: ${msg}`);
122
+ return toolError('QULIB_AUTH_EXPLORE_FAILED', msg, err instanceof Error ? err.stack : undefined);
123
+ }
124
+ });
125
+ mcpServer.registerTool('detect_auth', {
126
+ description: 'Detect the authentication pattern used by a deployed web app. Returns the auth type (form-login, oauth, magic-link, none, or unknown) and a recommendation for how to configure qulib to scan past it.',
127
+ inputSchema: DetectAuthToolInputSchema,
128
+ }, async ({ url, timeoutMs }) => {
129
+ try {
163
130
  log.info(`detect_auth tool url=${url} timeoutMs=${timeoutMs ?? 15000}`);
164
131
  const result = await detectAuth(url, timeoutMs, mcpProgressLog);
165
132
  const providerSummary = result.oauthButtons.length > 0
@@ -170,63 +137,113 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
170
137
  content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
171
138
  };
172
139
  }
173
- if (request.params.name !== 'analyze_app') {
174
- throw new Error(`Unknown tool: ${request.params.name}`);
140
+ catch (err) {
141
+ const msg = err instanceof Error ? err.message : String(err);
142
+ log.error(`detect_auth failed: ${msg}`);
143
+ return toolError('QULIB_AUTH_DETECT_FAILED', msg, err instanceof Error ? err.stack : undefined);
175
144
  }
176
- const input = AnalyzeInputSchema.parse(request.params.arguments ?? {});
177
- const successIndicator = input.auth?.type === 'form-login' &&
178
- input.auth.successUrlContains !== undefined &&
179
- input.auth.successUrlContains !== ''
180
- ? { urlContains: input.auth.successUrlContains }
181
- : {};
182
- const authConfig = input.auth?.type === 'form-login'
183
- ? {
184
- type: 'form-login',
185
- loginUrl: input.auth.loginUrl,
186
- credentials: { username: input.auth.username, password: input.auth.password },
187
- selectors: {
188
- username: input.auth.usernameSelector,
189
- password: input.auth.passwordSelector,
190
- submit: input.auth.submitSelector,
191
- },
192
- successIndicator,
145
+ });
146
+ mcpServer.registerTool('analyze_app', {
147
+ 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.',
148
+ inputSchema: AnalyzeInputSchema,
149
+ }, async (input) => {
150
+ try {
151
+ const successIndicator = input.auth?.type === 'form-login' &&
152
+ input.auth.successUrlContains !== undefined &&
153
+ input.auth.successUrlContains !== ''
154
+ ? { urlContains: input.auth.successUrlContains }
155
+ : {};
156
+ const authConfig = input.auth?.type === 'form-login'
157
+ ? {
158
+ type: 'form-login',
159
+ loginUrl: input.auth.loginUrl,
160
+ credentials: { username: input.auth.username, password: input.auth.password },
161
+ selectors: {
162
+ username: input.auth.usernameSelector,
163
+ password: input.auth.passwordSelector,
164
+ submit: input.auth.submitSelector,
165
+ },
166
+ successIndicator,
167
+ }
168
+ : input.auth?.type === 'storage-state'
169
+ ? { type: 'storage-state', path: input.auth.path }
170
+ : undefined;
171
+ const harnessConfig = {
172
+ maxPagesToScan: input.maxPagesToScan ?? 10,
173
+ maxDepth: 3,
174
+ minPagesForConfidence: 3,
175
+ timeoutMs: input.timeoutMs ?? 30000,
176
+ retryCount: 0,
177
+ llmTokenBudget: input.llmTokenBudget ?? input.llmMaxOutputTokensPerCall ?? 4096,
178
+ llmMaxOutputTokensPerCall: input.llmMaxOutputTokensPerCall,
179
+ testGenerationLimit: input.testGenerationLimit ?? 5,
180
+ enableLlmScenarios: input.enableLlmScenarios !== false,
181
+ readOnlyMode: true,
182
+ requireHumanReview: false,
183
+ failOnConsoleError: false,
184
+ explorer: 'playwright',
185
+ defaultAdapter: 'playwright',
186
+ adapters: ['playwright'],
187
+ ...(authConfig && { auth: authConfig }),
188
+ };
189
+ const result = await analyzeApp({
190
+ url: input.url,
191
+ writeArtifacts: false,
192
+ config: harnessConfig,
193
+ progressLog: mcpProgressLog,
194
+ telemetry: telemetrySink,
195
+ });
196
+ const payload = buildCompactAnalyzePayload(result, input.includeFullReport === true);
197
+ return {
198
+ content: [
199
+ {
200
+ type: 'text',
201
+ text: JSON.stringify(payload, null, 2),
202
+ },
203
+ ],
204
+ };
205
+ }
206
+ catch (err) {
207
+ const msg = err instanceof Error ? err.message : String(err);
208
+ log.error(`analyze_app failed: ${msg}`);
209
+ return toolError('QULIB_SCAN_FAILED', msg, err instanceof Error ? err.stack : undefined);
210
+ }
211
+ });
212
+ mcpServer.registerTool('qulib_score_automation', {
213
+ description: 'Score an automation repository for QA maturity across six dimensions: test coverage breadth, framework adoption, test-id hygiene, CI integration, auth test coverage, and component test ratio. Returns an overall score (0–100), maturity level (L1–L5), and prioritized recommendations.',
214
+ inputSchema: ScoreAutomationInputSchema,
215
+ }, async ({ repoPath, includeFullDimensions }) => {
216
+ try {
217
+ // Security: repoPath is an absolute path on the MCP host. We validate it is absolute
218
+ // and does not contain path traversal sequences. The MCP host is responsible for ensuring
219
+ // the path is within an allowed directory. We do not enforce a sandbox here — that is a
220
+ // host-level concern.
221
+ const abs = validateAbsoluteRepoPath(repoPath);
222
+ log.info(`qulib_score_automation repoPath=${abs}`);
223
+ const repo = await scanRepo(abs);
224
+ const maturity = computeAutomationMaturity(repo);
225
+ const payload = includeFullDimensions === true
226
+ ? maturity
227
+ : {
228
+ overallScore: maturity.overallScore,
229
+ level: maturity.level,
230
+ label: maturity.label,
231
+ topRecommendations: maturity.topRecommendations,
232
+ repoPath: maturity.repoPath,
233
+ computedAt: maturity.computedAt,
234
+ };
235
+ return {
236
+ content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
237
+ };
238
+ }
239
+ catch (err) {
240
+ const msg = err instanceof Error ? err.message : String(err);
241
+ if (msg.includes('repoPath must')) {
242
+ return toolError('QULIB_INPUT_INVALID', msg, undefined);
193
243
  }
194
- : input.auth?.type === 'storage-state'
195
- ? { type: 'storage-state', path: input.auth.path }
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
- };
215
- const result = await analyzeApp({
216
- url: input.url,
217
- writeArtifacts: false,
218
- config: harnessConfig,
219
- progressLog: mcpProgressLog,
220
- });
221
- const payload = buildCompactAnalyzePayload(result, input.includeFullReport === true);
222
- return {
223
- content: [
224
- {
225
- type: 'text',
226
- text: JSON.stringify(payload, null, 2),
227
- },
228
- ],
229
- };
244
+ log.error(`qulib_score_automation failed: ${msg}`);
245
+ return toolError('QULIB_REPO_SCORE_FAILED', msg, err instanceof Error ? err.stack : undefined);
246
+ }
230
247
  });
231
248
  const transport = new StdioServerTransport();
232
- await server.connect(transport);
249
+ await mcpServer.connect(transport);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qulib/mcp",
3
- "version": "0.3.1",
3
+ "version": "0.4.1",
4
4
  "description": "MCP server for Qulib — AI-callable QA gap analysis",
5
5
  "license": "MIT",
6
6
  "author": "Tapesh Nagarwal",
@@ -33,7 +33,7 @@
33
33
  },
34
34
  "dependencies": {
35
35
  "@modelcontextprotocol/sdk": "^1.0.0",
36
- "@qulib/core": "0.3.1",
36
+ "@qulib/core": "0.4.1",
37
37
  "zod": "^3.23.0"
38
38
  },
39
39
  "devDependencies": {