@qulib/mcp 0.1.1 → 0.2.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 +31 -4
- package/dist/index.js +92 -48
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -4,9 +4,12 @@
|
|
|
4
4
|
|
|
5
5
|
## What it does
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Tools:
|
|
8
8
|
|
|
9
|
-
|
|
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
|
|
|
@@ -47,9 +50,33 @@ Claude will call `analyze_app({ url: "https://example.com" })` and reason about
|
|
|
47
50
|
|
|
48
51
|
## Authenticated scanning
|
|
49
52
|
|
|
53
|
+
### Form login (automated)
|
|
54
|
+
|
|
50
55
|
> "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
56
|
|
|
52
|
-
Claude will pass auth credentials to
|
|
57
|
+
Claude will pass auth credentials to `analyze_app`; Qulib signs in, then scans.
|
|
58
|
+
|
|
59
|
+
### OAuth, SSO, magic link, or anything that cannot be scripted
|
|
60
|
+
|
|
61
|
+
OAuth and similar flows need human consent on the provider domain; Qulib does not automate them. Use the **CLI** (same machine as the browser):
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
qulib auth init --base-url https://app.example.com
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Log in manually in the opened window, press ENTER in the terminal, then reuse the saved JSON with:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
qulib analyze --url https://app.example.com --auth-storage-state ./qulib-storage-state.json
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
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`.
|
|
74
|
+
|
|
75
|
+
### Detecting auth before you configure anything
|
|
76
|
+
|
|
77
|
+
> "Use qulib's `detect_auth` tool on https://app.example.com — what auth pattern does it use and what should I do next?"
|
|
78
|
+
|
|
79
|
+
The tool returns `type`, `oauthButtons`, `recommendation`, and related fields so the agent can explain options honestly.
|
|
53
80
|
|
|
54
81
|
## Known limitations
|
|
55
82
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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?.
|
|
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
|
-
...(
|
|
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.
|
|
3
|
+
"version": "0.2.0",
|
|
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.
|
|
34
|
+
"@qulib/core": "0.2.0",
|
|
35
35
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
36
36
|
"zod": "^3.23.0"
|
|
37
37
|
},
|