@qulib/mcp 0.1.1 → 0.2.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.
Files changed (3) hide show
  1. package/README.md +43 -4
  2. package/dist/index.js +92 -48
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -4,9 +4,12 @@
4
4
 
5
5
  ## What it does
6
6
 
7
- One tool: `analyze_app(url, auth?)`
7
+ Tools:
8
8
 
9
- Returns:
9
+ - **`analyze_app(url, auth?)`** — full quality scan (optional form-login auth).
10
+ - **`detect_auth(url, timeoutMs?)`** — detect whether the site uses form login, OAuth, magic link, etc., and get a plain-language recommendation (including when to use manual `qulib auth init` and storage state).
11
+
12
+ Returns from `analyze_app`:
10
13
 
11
14
  - Release confidence score (0-100)
12
15
  - Accessibility violations (axe-core, WCAG 2 A/AA)
@@ -14,7 +17,7 @@ Returns:
14
17
  - Console errors and coverage warnings
15
18
  - Prioritized gaps with severity
16
19
 
17
- Supports optional form-login auth for scanning authenticated pages.
20
+ Supports optional form-login auth for scanning authenticated pages. If auth is required but not configured, the scan can stop early with `mode: auth-required` and guidance in `detectedAuth` / the decision log.
18
21
 
19
22
  ## Install for Claude Code
20
23
 
@@ -37,6 +40,18 @@ Add this under `mcpServers` in `claude_desktop_config.json` (Claude Desktop) or
37
40
  }
38
41
  ```
39
42
 
43
+ ## One-time browser setup
44
+
45
+ qulib uses Playwright under the hood. After your MCP host first runs the qulib server, you'll need to install Chromium:
46
+
47
+ ```bash
48
+ npx playwright install chromium
49
+ ```
50
+
51
+ This is a one-time step. You'll only need to do it again if Playwright's browser version is bumped in a future qulib release.
52
+
53
+ If you skip this step, the first tool call will return a clear error telling you to run the command.
54
+
40
55
  ## Example usage
41
56
 
42
57
  Ask Claude:
@@ -47,9 +62,33 @@ Claude will call `analyze_app({ url: "https://example.com" })` and reason about
47
62
 
48
63
  ## Authenticated scanning
49
64
 
65
+ ### Form login (automated)
66
+
50
67
  > "Use Qulib to scan my staging app at https://staging.example.com. Log in as user@example.com with password Test123, the login form is at /login with selectors [data-testid='email'], [data-testid='password'], and [data-testid='submit']."
51
68
 
52
- Claude will pass auth credentials to the tool; Qulib signs in, then scans.
69
+ Claude will pass auth credentials to `analyze_app`; Qulib signs in, then scans.
70
+
71
+ ### OAuth, SSO, magic link, or anything that cannot be scripted
72
+
73
+ OAuth and similar flows need human consent on the provider domain; Qulib does not automate them. Use the **CLI** (same machine as the browser):
74
+
75
+ ```bash
76
+ qulib auth init --base-url https://app.example.com
77
+ ```
78
+
79
+ Log in manually in the opened window, press ENTER in the terminal, then reuse the saved JSON with:
80
+
81
+ ```bash
82
+ qulib analyze --url https://app.example.com --auth-storage-state ./qulib-storage-state.json
83
+ ```
84
+
85
+ For MCP-driven workflows, run `auth init` on the machine where the MCP server runs, then pass `auth: { type: 'storage-state', path: '/absolute/path/to/qulib-storage-state.json' }` to `analyze_app`.
86
+
87
+ ### Detecting auth before you configure anything
88
+
89
+ > "Use qulib's `detect_auth` tool on https://app.example.com — what auth pattern does it use and what should I do next?"
90
+
91
+ The tool returns `type`, `oauthButtons`, `recommendation`, and related fields so the agent can explain options honestly.
53
92
 
54
93
  ## Known limitations
55
94
 
package/dist/index.js CHANGED
@@ -2,24 +2,27 @@
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 } from '@qulib/core';
5
+ import { analyzeApp, detectAuth } from '@qulib/core';
6
6
  import { z } from 'zod';
7
+ const FormLoginMcpAuthSchema = z.object({
8
+ type: z.literal('form-login'),
9
+ loginUrl: z.string().url(),
10
+ username: z.string(),
11
+ password: z.string(),
12
+ usernameSelector: z.string(),
13
+ passwordSelector: z.string(),
14
+ submitSelector: z.string(),
15
+ successUrlContains: z.string().optional(),
16
+ });
17
+ const StorageStateMcpAuthSchema = z.object({
18
+ type: z.literal('storage-state'),
19
+ path: z.string().min(1),
20
+ });
7
21
  const AnalyzeInputSchema = z.object({
8
22
  url: z.string().url(),
9
23
  maxPagesToScan: z.number().int().min(1).max(50).optional(),
10
24
  timeoutMs: z.number().int().positive().optional(),
11
- auth: z
12
- .object({
13
- type: z.literal('form-login'),
14
- loginUrl: z.string().url(),
15
- username: z.string(),
16
- password: z.string(),
17
- usernameSelector: z.string(),
18
- passwordSelector: z.string(),
19
- submitSelector: z.string(),
20
- successUrlContains: z.string().optional(),
21
- })
22
- .optional(),
25
+ auth: z.discriminatedUnion('type', [FormLoginMcpAuthSchema, StorageStateMcpAuthSchema]).optional(),
23
26
  });
24
27
  const server = new Server({
25
28
  name: 'qulib-mcp',
@@ -33,7 +36,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
33
36
  tools: [
34
37
  {
35
38
  name: 'analyze_app',
36
- description: 'Analyze a deployed web app for quality gaps. Returns a release confidence score (0-100), accessibility violations, broken links, and prioritized risks. Supports optional form-login authentication.',
39
+ description: 'Analyze a deployed web app for quality gaps. Returns a release confidence score (0-100), accessibility violations, broken links, and prioritized risks. Supports optional form-login or storage-state (Playwright) authentication.',
37
40
  inputSchema: {
38
41
  type: 'object',
39
42
  properties: {
@@ -41,42 +44,95 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
41
44
  maxPagesToScan: { type: 'number', description: 'Max pages to crawl (default 10)' },
42
45
  timeoutMs: { type: 'number', description: 'Per-page timeout in milliseconds (default 30000)' },
43
46
  auth: {
44
- type: 'object',
45
- description: 'Optional form-login auth',
46
- properties: {
47
- type: { type: 'string', enum: ['form-login'] },
48
- loginUrl: { type: 'string' },
49
- username: { type: 'string' },
50
- password: { type: 'string' },
51
- usernameSelector: { type: 'string' },
52
- passwordSelector: { type: 'string' },
53
- submitSelector: { type: 'string' },
54
- successUrlContains: { type: 'string' },
55
- },
56
- required: [
57
- 'type',
58
- 'loginUrl',
59
- 'username',
60
- 'password',
61
- 'usernameSelector',
62
- 'passwordSelector',
63
- 'submitSelector',
47
+ description: 'Optional auth: form-login credentials or path to a storage state JSON from `qulib auth init`',
48
+ oneOf: [
49
+ {
50
+ type: 'object',
51
+ properties: {
52
+ type: { type: 'string', enum: ['form-login'] },
53
+ loginUrl: { type: 'string' },
54
+ username: { type: 'string' },
55
+ password: { type: 'string' },
56
+ usernameSelector: { type: 'string' },
57
+ passwordSelector: { type: 'string' },
58
+ submitSelector: { type: 'string' },
59
+ successUrlContains: { type: 'string' },
60
+ },
61
+ required: [
62
+ 'type',
63
+ 'loginUrl',
64
+ 'username',
65
+ 'password',
66
+ 'usernameSelector',
67
+ 'passwordSelector',
68
+ 'submitSelector',
69
+ ],
70
+ },
71
+ {
72
+ type: 'object',
73
+ properties: {
74
+ type: { type: 'string', enum: ['storage-state'] },
75
+ path: { type: 'string', description: 'Absolute path to storage state JSON on the MCP host' },
76
+ },
77
+ required: ['type', 'path'],
78
+ },
64
79
  ],
65
80
  },
66
81
  },
67
82
  required: ['url'],
68
83
  },
69
84
  },
85
+ {
86
+ name: 'detect_auth',
87
+ 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.',
88
+ inputSchema: {
89
+ type: 'object',
90
+ properties: {
91
+ url: { type: 'string', description: 'Full URL of the deployed app or login page' },
92
+ timeoutMs: { type: 'number', description: 'Page load timeout in milliseconds (default 15000)' },
93
+ },
94
+ required: ['url'],
95
+ },
96
+ },
70
97
  ],
71
98
  }));
72
99
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
100
+ if (request.params.name === 'detect_auth') {
101
+ const { url, timeoutMs } = z
102
+ .object({
103
+ url: z.string().url(),
104
+ timeoutMs: z.number().int().positive().optional(),
105
+ })
106
+ .parse(request.params.arguments ?? {});
107
+ const result = await detectAuth(url, timeoutMs);
108
+ return {
109
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
110
+ };
111
+ }
73
112
  if (request.params.name !== 'analyze_app') {
74
113
  throw new Error(`Unknown tool: ${request.params.name}`);
75
114
  }
76
115
  const input = AnalyzeInputSchema.parse(request.params.arguments ?? {});
77
- const successIndicator = input.auth?.successUrlContains !== undefined && input.auth.successUrlContains !== ''
116
+ const successIndicator = input.auth?.type === 'form-login' &&
117
+ input.auth.successUrlContains !== undefined &&
118
+ input.auth.successUrlContains !== ''
78
119
  ? { urlContains: input.auth.successUrlContains }
79
120
  : {};
121
+ const authConfig = input.auth?.type === 'form-login'
122
+ ? {
123
+ type: 'form-login',
124
+ loginUrl: input.auth.loginUrl,
125
+ credentials: { username: input.auth.username, password: input.auth.password },
126
+ selectors: {
127
+ username: input.auth.usernameSelector,
128
+ password: input.auth.passwordSelector,
129
+ submit: input.auth.submitSelector,
130
+ },
131
+ successIndicator,
132
+ }
133
+ : input.auth?.type === 'storage-state'
134
+ ? { type: 'storage-state', path: input.auth.path }
135
+ : undefined;
80
136
  const result = await analyzeApp({
81
137
  url: input.url,
82
138
  writeArtifacts: false,
@@ -94,19 +150,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
94
150
  explorer: 'playwright',
95
151
  defaultAdapter: 'playwright',
96
152
  adapters: ['playwright'],
97
- ...(input.auth && {
98
- auth: {
99
- type: 'form-login',
100
- loginUrl: input.auth.loginUrl,
101
- credentials: { username: input.auth.username, password: input.auth.password },
102
- selectors: {
103
- username: input.auth.usernameSelector,
104
- password: input.auth.passwordSelector,
105
- submit: input.auth.submitSelector,
106
- },
107
- successIndicator,
108
- },
109
- }),
153
+ ...(authConfig && { auth: authConfig }),
110
154
  },
111
155
  });
112
156
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qulib/mcp",
3
- "version": "0.1.1",
3
+ "version": "0.2.1",
4
4
  "description": "MCP server for Qulib — AI-callable QA gap analysis",
5
5
  "license": "MIT",
6
6
  "author": "Tapesh Nagarwal",
@@ -31,7 +31,7 @@
31
31
  "dev": "tsx src/index.ts"
32
32
  },
33
33
  "dependencies": {
34
- "@qulib/core": "0.1.1",
34
+ "@qulib/core": "0.2.1",
35
35
  "@modelcontextprotocol/sdk": "^1.0.0",
36
36
  "zod": "^3.23.0"
37
37
  },