argusqa-os 9.4.3 → 9.4.5
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/package.json
CHANGED
|
@@ -1,69 +1,69 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "argusqa-os",
|
|
3
|
-
"version": "9.4.
|
|
4
|
-
"mcpName": "io.github.ironclawdevs27/argus",
|
|
5
|
-
"description": "Argus — AI-powered automated dev-testing platform using Chrome DevTools MCP and Claude Code",
|
|
6
|
-
"keywords": [
|
|
7
|
-
"mcp",
|
|
8
|
-
"mcp-server",
|
|
9
|
-
"claude",
|
|
10
|
-
"qa",
|
|
11
|
-
"testing",
|
|
12
|
-
"accessibility",
|
|
13
|
-
"automation",
|
|
14
|
-
"chrome-devtools",
|
|
15
|
-
"web-testing"
|
|
16
|
-
],
|
|
17
|
-
"author": "ironclawdevs",
|
|
18
|
-
"license": "MIT",
|
|
19
|
-
"homepage": "https://argus-qa.com",
|
|
20
|
-
"repository": {
|
|
21
|
-
"type": "git",
|
|
22
|
-
"url": "git+https://github.com/ironclawdevs27/Argus.git"
|
|
23
|
-
},
|
|
24
|
-
"files": [
|
|
25
|
-
"src/",
|
|
26
|
-
".mcp.json",
|
|
27
|
-
"glama.json"
|
|
28
|
-
],
|
|
29
|
-
"type": "module",
|
|
30
|
-
"engines": {
|
|
31
|
-
"node": ">=20.19.0"
|
|
32
|
-
},
|
|
33
|
-
"bin": {
|
|
34
|
-
"argus": "src/cli/init.js",
|
|
35
|
-
"argus-mcp": "src/mcp-server.js",
|
|
36
|
-
"argusqa-os": "src/mcp-server.js"
|
|
37
|
-
},
|
|
38
|
-
"scripts": {
|
|
39
|
-
"setup": "node -e \"import('fs').then(fs => fs.default.mkdirSync('./reports', { recursive: true }))\"",
|
|
40
|
-
"init": "node src/cli/init.js",
|
|
41
|
-
"crawl": "node src/orchestration/crawl-and-report.js",
|
|
42
|
-
"compare": "node src/orchestration/env-comparison.js",
|
|
43
|
-
"watch": "node src/orchestration/watch-mode.js",
|
|
44
|
-
"server": "node src/server/index.js",
|
|
45
|
-
"harness": "node test-harness/server.js",
|
|
46
|
-
"harness:staging": "PORT=3101 node test-harness/server.js",
|
|
47
|
-
"test:harness": "node test-harness/validate.js",
|
|
48
|
-
"test:unit": "vitest run test/unit",
|
|
49
|
-
"test": "npm run test:unit && npm run test:harness",
|
|
50
|
-
"report:html": "node src/utils/html-reporter.js",
|
|
51
|
-
"mcp-server": "node src/mcp-server.js"
|
|
52
|
-
},
|
|
53
|
-
"dependencies": {
|
|
54
|
-
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
55
|
-
"@opentelemetry/api": "^1.9.1",
|
|
56
|
-
"@opentelemetry/sdk-node": "^0.218.0",
|
|
57
|
-
"@slack/web-api": "^7.16.0",
|
|
58
|
-
"dotenv": "^16.4.5",
|
|
59
|
-
"express": "^5.2.1",
|
|
60
|
-
"pino": "^10.3.1",
|
|
61
|
-
"pino-pretty": "^13.1.3",
|
|
62
|
-
"pixelmatch": "^7.2.0",
|
|
63
|
-
"pngjs": "^7.0.0",
|
|
64
|
-
"zod": "^4.4.3"
|
|
65
|
-
},
|
|
66
|
-
"devDependencies": {
|
|
67
|
-
"vitest": "^4.1.7"
|
|
68
|
-
}
|
|
69
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "argusqa-os",
|
|
3
|
+
"version": "9.4.5",
|
|
4
|
+
"mcpName": "io.github.ironclawdevs27/argus",
|
|
5
|
+
"description": "Argus — AI-powered automated dev-testing platform using Chrome DevTools MCP and Claude Code",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"mcp",
|
|
8
|
+
"mcp-server",
|
|
9
|
+
"claude",
|
|
10
|
+
"qa",
|
|
11
|
+
"testing",
|
|
12
|
+
"accessibility",
|
|
13
|
+
"automation",
|
|
14
|
+
"chrome-devtools",
|
|
15
|
+
"web-testing"
|
|
16
|
+
],
|
|
17
|
+
"author": "ironclawdevs",
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"homepage": "https://argus-qa.com",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/ironclawdevs27/Argus.git"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"src/",
|
|
26
|
+
".mcp.json",
|
|
27
|
+
"glama.json"
|
|
28
|
+
],
|
|
29
|
+
"type": "module",
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=20.19.0"
|
|
32
|
+
},
|
|
33
|
+
"bin": {
|
|
34
|
+
"argus": "src/cli/init.js",
|
|
35
|
+
"argus-mcp": "src/mcp-server.js",
|
|
36
|
+
"argusqa-os": "src/mcp-server.js"
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"setup": "node -e \"import('fs').then(fs => fs.default.mkdirSync('./reports', { recursive: true }))\"",
|
|
40
|
+
"init": "node src/cli/init.js",
|
|
41
|
+
"crawl": "node src/orchestration/crawl-and-report.js",
|
|
42
|
+
"compare": "node src/orchestration/env-comparison.js",
|
|
43
|
+
"watch": "node src/orchestration/watch-mode.js",
|
|
44
|
+
"server": "node src/server/index.js",
|
|
45
|
+
"harness": "node test-harness/server.js",
|
|
46
|
+
"harness:staging": "PORT=3101 node test-harness/server.js",
|
|
47
|
+
"test:harness": "node test-harness/validate.js",
|
|
48
|
+
"test:unit": "vitest run test/unit",
|
|
49
|
+
"test": "npm run test:unit && npm run test:harness",
|
|
50
|
+
"report:html": "node src/utils/html-reporter.js",
|
|
51
|
+
"mcp-server": "node src/mcp-server.js"
|
|
52
|
+
},
|
|
53
|
+
"dependencies": {
|
|
54
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
55
|
+
"@opentelemetry/api": "^1.9.1",
|
|
56
|
+
"@opentelemetry/sdk-node": "^0.218.0",
|
|
57
|
+
"@slack/web-api": "^7.16.0",
|
|
58
|
+
"dotenv": "^16.4.5",
|
|
59
|
+
"express": "^5.2.1",
|
|
60
|
+
"pino": "^10.3.1",
|
|
61
|
+
"pino-pretty": "^13.1.3",
|
|
62
|
+
"pixelmatch": "^7.2.0",
|
|
63
|
+
"pngjs": "^7.0.0",
|
|
64
|
+
"zod": "^4.4.3"
|
|
65
|
+
},
|
|
66
|
+
"devDependencies": {
|
|
67
|
+
"vitest": "^4.1.7"
|
|
68
|
+
}
|
|
69
|
+
}
|
package/src/config/targets.js
CHANGED
|
@@ -167,7 +167,7 @@ export const auth = null;
|
|
|
167
167
|
* Each flow is a named sequence of steps executed end-to-end by flow-runner.js.
|
|
168
168
|
* Supported actions:
|
|
169
169
|
* navigate — { action: 'navigate', path: '/route' } or url: 'https://...' for absolute
|
|
170
|
-
* fill — { action: 'fill', selector: 'input#email', value: '
|
|
170
|
+
* fill — { action: 'fill', selector: 'input#email', value: 'user@example.com' }
|
|
171
171
|
* Add typing: true to dispatch real keyboard events (needed for input-validation handlers)
|
|
172
172
|
* click — { action: 'click', selector: 'button[type=submit]' }
|
|
173
173
|
* press_key — { action: 'press_key', key: 'Tab' | 'Enter' | 'Escape' | 'ArrowDown' | ... }
|
package/src/mcp-server.js
CHANGED
|
@@ -1,312 +1,312 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Argus MCP Server (v9.4.
|
|
4
|
-
*
|
|
5
|
-
* Exposes Argus as an MCP server so Claude (or any MCP client) can call
|
|
6
|
-
* argus_audit, argus_audit_full, argus_compare, argus_last_report, and
|
|
7
|
-
* argus_get_context (fix loop) directly from a conversation without using the CLI.
|
|
8
|
-
*
|
|
9
|
-
* Architecture: MCP-inside-MCP
|
|
10
|
-
* Claude (MCP client)
|
|
11
|
-
* → Argus MCP Server (this file)
|
|
12
|
-
* → chrome-devtools-mcp client (mcp-client.js)
|
|
13
|
-
* → Chrome (CDP)
|
|
14
|
-
*
|
|
15
|
-
* Registration: add to .mcp.json as "argus" server.
|
|
16
|
-
* Run standalone: node src/mcp-server.js
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
20
|
-
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
21
|
-
import {
|
|
22
|
-
ListToolsRequestSchema,
|
|
23
|
-
CallToolRequestSchema,
|
|
24
|
-
} from '@modelcontextprotocol/sdk/types.js';
|
|
25
|
-
import fs from 'fs';
|
|
26
|
-
import path from 'path';
|
|
27
|
-
|
|
28
|
-
import { createMcpClient } from './utils/mcp-client.js';
|
|
29
|
-
import { crawlRouteCheap, runCrawl } from './orchestration/crawl-and-report.js';
|
|
30
|
-
import { runComparison } from './orchestration/env-comparison.js';
|
|
31
|
-
import { WatchSession } from './orchestration/watch-mode.js';
|
|
32
|
-
import { CdpBrowserAdapter } from './adapters/browser.js';
|
|
33
|
-
|
|
34
|
-
const REPORTS_DIR = path.resolve(process.cwd(), 'reports');
|
|
35
|
-
|
|
36
|
-
// Fix loop: stores up to 20 snapshots keyed by snapshot_id so argus_get_context
|
|
37
|
-
// can diff before/after a fix. Keys are evicted oldest-first when the limit is hit.
|
|
38
|
-
const snapshotStore = new Map();
|
|
39
|
-
const MAX_SNAPSHOTS = 20;
|
|
40
|
-
|
|
41
|
-
// Audit cache: stores argus_audit results keyed by URL so cache:true skips re-crawl.
|
|
42
|
-
const auditCache = new Map();
|
|
43
|
-
const MAX_AUDIT_CACHE = 20;
|
|
44
|
-
|
|
45
|
-
function storeSnapshot(id, findings) {
|
|
46
|
-
snapshotStore.set(id, findings);
|
|
47
|
-
if (snapshotStore.size > MAX_SNAPSHOTS) {
|
|
48
|
-
snapshotStore.delete(snapshotStore.keys().next().value);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function cacheAudit(url, result) {
|
|
53
|
-
auditCache.set(url, { result, ts: Date.now() });
|
|
54
|
-
if (auditCache.size > MAX_AUDIT_CACHE) {
|
|
55
|
-
auditCache.delete(auditCache.keys().next().value);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// ── Tool definitions ─────────────────────────────────────────────────────────
|
|
60
|
-
|
|
61
|
-
const TOOLS = [
|
|
62
|
-
{
|
|
63
|
-
name: 'argus_audit',
|
|
64
|
-
description: 'Fast QA audit on a URL via Chrome DevTools Protocol. Runs 8 analyzers in one pass: JS errors, unhandled rejections, network failures (4xx/5xx), API frequency loops, CSS cascade issues, SEO violations, security header checks, and accessibility. Returns { findings: [{severity, type, message, url}], summary: {critical, warning, info} }. Use for CI smoke tests and pre-deploy gates. Pass cache: true to skip re-crawl on repeat calls to the same URL within a session — useful in tight fix loops. For Lighthouse scoring and memory leak detection, use argus_audit_full. Requires Chrome running with --remote-debugging-port=9222.',
|
|
65
|
-
inputSchema: {
|
|
66
|
-
type: 'object',
|
|
67
|
-
properties: {
|
|
68
|
-
url: { type: 'string', description: 'Full URL to audit, including protocol and path (e.g. http://localhost:3000/checkout). Must be reachable by the running Chrome instance.' },
|
|
69
|
-
critical: { type: 'boolean', description: 'When true, console.error calls are escalated to critical severity. Set true for business-critical routes (login, checkout, dashboard) where any error is a blocker.', default: false },
|
|
70
|
-
cache: { type: 'boolean', description: 'When true, returns the cached result for this URL if one exists (from a previous argus_audit call in this session) without re-crawling. Use in fix loops to cheaply re-read the last audit while iterating on a fix. Cache is per-session, max 20 entries, LRU eviction.', default: false },
|
|
71
|
-
},
|
|
72
|
-
required: ['url'],
|
|
73
|
-
},
|
|
74
|
-
},
|
|
75
|
-
{
|
|
76
|
-
name: 'argus_audit_full',
|
|
77
|
-
description: 'Deep QA audit — extends argus_audit with Lighthouse performance/accessibility scoring, responsive layout checks across 4 viewports (320/768/1280/1920px), memory leak detection via heap snapshot, hover-state regression detection, and accessibility tree snapshot. Returns full JSON report with findings by severity, Lighthouse scores, and layout overflow details. Use when argus_audit passes clean but visual or performance regressions are suspected. Requires Chrome running with --remote-debugging-port=9222.',
|
|
78
|
-
inputSchema: {
|
|
79
|
-
type: 'object',
|
|
80
|
-
properties: {
|
|
81
|
-
url: { type: 'string', description: 'Full URL to audit, including protocol and path (e.g. https://example.com/dashboard). Must be reachable by the running Chrome instance.' },
|
|
82
|
-
critical: { type: 'boolean', description: 'When true, console.error calls are escalated to critical severity. Set true for business-critical routes (login, checkout, dashboard) where any error is a blocker.', default: false },
|
|
83
|
-
},
|
|
84
|
-
required: ['url'],
|
|
85
|
-
},
|
|
86
|
-
},
|
|
87
|
-
{
|
|
88
|
-
name: 'argus_compare',
|
|
89
|
-
description: 'Diffs dev vs staging environments side-by-side. Navigates both URLs, captures screenshots, and runs the full analyzer suite on each, then surfaces regressions — findings present in staging but not dev, or with changed severity. Returns { regressions: [{type, devSeverity, stagingSeverity}], screenshots, summary }. Run before promoting a build to staging to catch environment-specific bugs. Set TARGET_DEV_URL and TARGET_STAGING_URL env vars before starting the server; omit TARGET_STAGING_URL to run CSS-analysis-only mode.',
|
|
90
|
-
inputSchema: { type: 'object', properties: {} },
|
|
91
|
-
},
|
|
92
|
-
{
|
|
93
|
-
name: 'argus_last_report',
|
|
94
|
-
description: 'Returns the most recent Argus JSON report from the reports/ directory. Report includes a findings array and severity summary (critical/warning/info counts). Returns { "error": "No reports found in reports/" } when no audits have been run yet. Use to retrieve prior results without re-running a scan, or to pipe findings into another analysis tool.',
|
|
95
|
-
inputSchema: { type: 'object', properties: {} },
|
|
96
|
-
},
|
|
97
|
-
{
|
|
98
|
-
name: 'argus_watch_snapshot',
|
|
99
|
-
description: 'Snapshots the currently open Chrome tab without navigating — captures console errors, network failures (4xx/5xx), CORS blocks, and auth failures in one poll. Returns { findings: [{severity, type, message, url}], newConsole, newNetwork }. Use during active development to inspect what is happening on the current page without running a full audit. Pass tabId to inspect a specific tab (get IDs from argus_get_context or list_pages). Without tabId, reads the active tab. Requires Chrome on --remote-debugging-port=9222 with a page already open.',
|
|
100
|
-
inputSchema: {
|
|
101
|
-
type: 'object',
|
|
102
|
-
properties: {
|
|
103
|
-
url: { type: 'string', description: 'Optional base URL to attribute findings to (default: TARGET_DEV_URL env var). Does not navigate — reads the currently open Chrome tab.' },
|
|
104
|
-
tabId: { type: 'string', description: 'Optional Chrome page/tab ID (e.g. from a prior argus_get_context response). When provided, switches focus to that tab before snapshotting — useful for SPAs that spawn new windows or multi-tab flows.' },
|
|
105
|
-
},
|
|
106
|
-
},
|
|
107
|
-
},
|
|
108
|
-
{
|
|
109
|
-
name: 'argus_get_context',
|
|
110
|
-
description: 'Captures everything currently broken on the open Chrome tab and formats it as a diagnostic context for Claude to read and suggest fixes. Does NOT navigate — reads the live tab state after user interactions, in authenticated sessions, or mid-flow. Returns { snapshot_id, summary, url, timestamp, critical_issues, warnings, js_errors, network_failures, console_errors, recent_requests, open_tabs }. Fix loop: pass the snapshot_id from a previous call as snapshot_id to get a diff — the response will include resolved (cleared since last snapshot), new_issues (appeared since last snapshot), and persisting (unchanged). Multi-tab: pass tabId to inspect a specific tab, or omit to read the active tab. The open_tabs array always lists all currently open Chrome tabs. Workflow: call argus_get_context → Claude suggests fix → apply fix → call argus_get_context with snapshot_id → verify resolved array is non-empty. Requires Chrome on --remote-debugging-port=9222.',
|
|
111
|
-
inputSchema: {
|
|
112
|
-
type: 'object',
|
|
113
|
-
properties: {
|
|
114
|
-
url: { type: 'string', description: 'Optional base URL to attribute findings to (default: TARGET_DEV_URL env var). Does not navigate — inspects the currently open Chrome tab.' },
|
|
115
|
-
snapshot_id: { type: 'string', description: 'Optional snapshot_id from a previous argus_get_context call. When provided, the response includes resolved/new_issues/persisting arrays showing what changed since that snapshot.' },
|
|
116
|
-
tabId: { type: 'string', description: 'Optional Chrome page/tab ID. When provided, switches focus to that specific tab before capturing context — useful for SPAs that spawn new windows (e.g. OAuth popups, checkout flows). Get tab IDs from the open_tabs array in a prior argus_get_context response.' },
|
|
117
|
-
},
|
|
118
|
-
},
|
|
119
|
-
},
|
|
120
|
-
];
|
|
121
|
-
|
|
122
|
-
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
123
|
-
|
|
124
|
-
async function withMcp(fn) {
|
|
125
|
-
const mcp = await createMcpClient();
|
|
126
|
-
try {
|
|
127
|
-
return await fn(mcp);
|
|
128
|
-
} finally {
|
|
129
|
-
try { mcp.close(); } catch { /* ignore — process already gone */ }
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// ── Tool handlers ─────────────────────────────────────────────────────────────
|
|
134
|
-
|
|
135
|
-
async function handleAudit({ url, critical = false, cache = false }) {
|
|
136
|
-
if (cache && auditCache.has(url)) {
|
|
137
|
-
const { result, ts } = auditCache.get(url);
|
|
138
|
-
return { content: [{ type: 'text', text: JSON.stringify({ ...result, _cached: true, _cachedAt: new Date(ts).toISOString() }, null, 2) }] };
|
|
139
|
-
}
|
|
140
|
-
return withMcp(async (mcp) => {
|
|
141
|
-
const parsed = new URL(url);
|
|
142
|
-
const route = { path: parsed.pathname + parsed.search + parsed.hash, name: 'audit', critical };
|
|
143
|
-
const raw = await crawlRouteCheap(route, parsed.origin, mcp);
|
|
144
|
-
const findings = Array.isArray(raw.errors) ? raw.errors : [];
|
|
145
|
-
const result = {
|
|
146
|
-
findings,
|
|
147
|
-
summary: {
|
|
148
|
-
critical: findings.filter(f => f.severity === 'critical').length,
|
|
149
|
-
warning: findings.filter(f => f.severity === 'warning').length,
|
|
150
|
-
info: findings.filter(f => f.severity === 'info').length,
|
|
151
|
-
},
|
|
152
|
-
url: raw.url,
|
|
153
|
-
pageTitle: raw.pageTitle,
|
|
154
|
-
screenshot: raw.screenshot,
|
|
155
|
-
};
|
|
156
|
-
if (cache) cacheAudit(url, result);
|
|
157
|
-
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
async function handleAuditFull({ url, critical = false }) {
|
|
162
|
-
return withMcp(async (mcp) => {
|
|
163
|
-
const parsed = new URL(url);
|
|
164
|
-
const report = await runCrawl(
|
|
165
|
-
mcp,
|
|
166
|
-
[{ path: parsed.pathname + parsed.search + parsed.hash, name: 'audit', critical }],
|
|
167
|
-
parsed.origin,
|
|
168
|
-
);
|
|
169
|
-
return { content: [{ type: 'text', text: JSON.stringify(report, null, 2) }] };
|
|
170
|
-
});
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
async function handleCompare() {
|
|
174
|
-
return withMcp(async (mcp) => {
|
|
175
|
-
const report = await runComparison(mcp);
|
|
176
|
-
return { content: [{ type: 'text', text: JSON.stringify(report, null, 2) }] };
|
|
177
|
-
});
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
async function handleWatchSnapshot({ url, tabId } = {}) {
|
|
181
|
-
return withMcp(async (mcp) => {
|
|
182
|
-
const browser = new CdpBrowserAdapter(mcp);
|
|
183
|
-
if (tabId) await browser.selectPage(tabId);
|
|
184
|
-
const baseUrl = url ?? process.env.TARGET_DEV_URL ?? 'http://localhost:3000';
|
|
185
|
-
const session = new WatchSession(browser, baseUrl);
|
|
186
|
-
const result = await session.poll();
|
|
187
|
-
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
188
|
-
});
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
async function handleGetContext({ url, snapshot_id: prevId, tabId } = {}) {
|
|
192
|
-
return withMcp(async (mcp) => {
|
|
193
|
-
const browser = new CdpBrowserAdapter(mcp);
|
|
194
|
-
if (tabId) await browser.selectPage(tabId);
|
|
195
|
-
const baseUrl = url ?? process.env.TARGET_DEV_URL ?? 'http://localhost:3000';
|
|
196
|
-
const session = new WatchSession(browser, baseUrl);
|
|
197
|
-
const { findings, newConsole, newNetwork } = await session.poll();
|
|
198
|
-
|
|
199
|
-
// List all open tabs so the caller can target a specific tab on the next call.
|
|
200
|
-
let open_tabs = [];
|
|
201
|
-
try {
|
|
202
|
-
const pages = await browser.listPages();
|
|
203
|
-
if (Array.isArray(pages)) {
|
|
204
|
-
open_tabs = pages.map(p => ({ id: p.id ?? p.pageId, url: p.url, title: p.title }));
|
|
205
|
-
}
|
|
206
|
-
} catch { /* list_pages not available in all Chrome configs — degrade gracefully */ }
|
|
207
|
-
|
|
208
|
-
const newId = Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
|
|
209
|
-
storeSnapshot(newId, findings);
|
|
210
|
-
|
|
211
|
-
const critical = findings.filter(f => f.severity === 'critical');
|
|
212
|
-
const warnings = findings.filter(f => f.severity === 'warning');
|
|
213
|
-
|
|
214
|
-
const findingKey = (f) => `${f.type}::${(f.message ?? '').slice(0, 120)}`;
|
|
215
|
-
|
|
216
|
-
let resolved = [];
|
|
217
|
-
let persisting = [];
|
|
218
|
-
let new_issues = findings;
|
|
219
|
-
const isDiff = prevId && snapshotStore.has(prevId);
|
|
220
|
-
|
|
221
|
-
if (isDiff) {
|
|
222
|
-
const prev = snapshotStore.get(prevId);
|
|
223
|
-
const prevKeys = new Set(prev.map(findingKey));
|
|
224
|
-
const curKeys = new Set(findings.map(findingKey));
|
|
225
|
-
resolved = prev.filter(f => !curKeys.has(findingKey(f)));
|
|
226
|
-
persisting = findings.filter(f => prevKeys.has(findingKey(f)));
|
|
227
|
-
new_issues = findings.filter(f => !prevKeys.has(findingKey(f)));
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
let summary;
|
|
231
|
-
if (isDiff) {
|
|
232
|
-
if (resolved.length > 0 && critical.length === 0 && warnings.length === 0) {
|
|
233
|
-
summary = `All issues resolved on ${baseUrl}. ${resolved.length} finding${resolved.length > 1 ? 's' : ''} cleared since last snapshot.`;
|
|
234
|
-
} else if (resolved.length > 0) {
|
|
235
|
-
summary = `${resolved.length} issue${resolved.length > 1 ? 's' : ''} resolved on ${baseUrl}. ${new_issues.length} new + ${persisting.length} persisting (${critical.length} critical). Pass the new snapshot_id to continue the fix loop.`;
|
|
236
|
-
} else if (critical.length === 0 && warnings.length === 0) {
|
|
237
|
-
summary = `No issues on ${baseUrl} — console and network are clean.`;
|
|
238
|
-
} else {
|
|
239
|
-
summary = `No change on ${baseUrl}: ${critical.length} critical + ${warnings.length} warning${warnings.length !== 1 ? 's' : ''} still present. Check the persisting array for what hasn't been fixed.`;
|
|
240
|
-
}
|
|
241
|
-
} else if (critical.length === 0 && warnings.length === 0) {
|
|
242
|
-
summary = `No issues detected on ${baseUrl} — console and network are clean.`;
|
|
243
|
-
} else if (critical.length > 0) {
|
|
244
|
-
summary = `${critical.length} critical issue${critical.length > 1 ? 's' : ''} + ${warnings.length} warning${warnings.length !== 1 ? 's' : ''} detected on ${baseUrl}. Focus on critical issues first. Pass snapshot_id to the next argus_get_context call after applying a fix to see what changed.`;
|
|
245
|
-
} else {
|
|
246
|
-
summary = `${warnings.length} warning${warnings.length !== 1 ? 's' : ''} detected on ${baseUrl}. No critical errors. Pass snapshot_id to the next call to verify fixes.`;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
const context = {
|
|
250
|
-
snapshot_id: newId,
|
|
251
|
-
summary,
|
|
252
|
-
url: baseUrl,
|
|
253
|
-
timestamp: new Date().toISOString(),
|
|
254
|
-
critical_issues: critical,
|
|
255
|
-
warnings,
|
|
256
|
-
js_errors: findings.filter(f => f.type === 'js-error' || f.type === 'unhandled-rejection'),
|
|
257
|
-
network_failures: findings.filter(f => f.type === 'network-error' || f.type === 'cors-error' || f.type === 'auth-error'),
|
|
258
|
-
console_errors: newConsole.filter(m => m.level === 'error' || m.level === 'warning'),
|
|
259
|
-
recent_requests: newNetwork.slice(-20),
|
|
260
|
-
open_tabs,
|
|
261
|
-
...(isDiff ? { resolved, new_issues, persisting } : {}),
|
|
262
|
-
};
|
|
263
|
-
|
|
264
|
-
return { content: [{ type: 'text', text: JSON.stringify(context, null, 2) }] };
|
|
265
|
-
});
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
async function handleLastReport() {
|
|
269
|
-
if (!fs.existsSync(REPORTS_DIR)) {
|
|
270
|
-
return { content: [{ type: 'text', text: '{"error":"No reports found in reports/"}' }] };
|
|
271
|
-
}
|
|
272
|
-
const files = fs.readdirSync(REPORTS_DIR).filter(f => f.endsWith('.json'));
|
|
273
|
-
if (files.length === 0) {
|
|
274
|
-
return { content: [{ type: 'text', text: '{"error":"No reports found in reports/"}' }] };
|
|
275
|
-
}
|
|
276
|
-
const latest = files
|
|
277
|
-
.map(f => ({ f, mt: fs.statSync(path.join(REPORTS_DIR, f)).mtimeMs }))
|
|
278
|
-
.sort((a, b) => b.mt - a.mt)[0].f;
|
|
279
|
-
const json = fs.readFileSync(path.join(REPORTS_DIR, latest), 'utf8');
|
|
280
|
-
return { content: [{ type: 'text', text: json }] };
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
// ── Server bootstrap ──────────────────────────────────────────────────────────
|
|
284
|
-
|
|
285
|
-
const server = new Server(
|
|
286
|
-
{ name: 'argus', version: '9.4.
|
|
287
|
-
{ capabilities: { tools: {} } },
|
|
288
|
-
);
|
|
289
|
-
|
|
290
|
-
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
|
|
291
|
-
|
|
292
|
-
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
293
|
-
try {
|
|
294
|
-
switch (req.params.name) {
|
|
295
|
-
case 'argus_audit': return await handleAudit(req.params.arguments ?? {});
|
|
296
|
-
case 'argus_audit_full': return await handleAuditFull(req.params.arguments ?? {});
|
|
297
|
-
case 'argus_compare': return await handleCompare();
|
|
298
|
-
case 'argus_last_report': return await handleLastReport();
|
|
299
|
-
case 'argus_watch_snapshot': return await handleWatchSnapshot(req.params.arguments ?? {});
|
|
300
|
-
case 'argus_get_context': return await handleGetContext(req.params.arguments ?? {});
|
|
301
|
-
default: throw new Error(`Unknown tool: ${req.params.name}`);
|
|
302
|
-
}
|
|
303
|
-
} catch (err) {
|
|
304
|
-
return {
|
|
305
|
-
content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }],
|
|
306
|
-
isError: true,
|
|
307
|
-
};
|
|
308
|
-
}
|
|
309
|
-
});
|
|
310
|
-
|
|
311
|
-
const transport = new StdioServerTransport();
|
|
312
|
-
await server.connect(transport);
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Argus MCP Server (v9.4.5)
|
|
4
|
+
*
|
|
5
|
+
* Exposes Argus as an MCP server so Claude (or any MCP client) can call
|
|
6
|
+
* argus_audit, argus_audit_full, argus_compare, argus_last_report, and
|
|
7
|
+
* argus_get_context (fix loop) directly from a conversation without using the CLI.
|
|
8
|
+
*
|
|
9
|
+
* Architecture: MCP-inside-MCP
|
|
10
|
+
* Claude (MCP client)
|
|
11
|
+
* → Argus MCP Server (this file)
|
|
12
|
+
* → chrome-devtools-mcp client (mcp-client.js)
|
|
13
|
+
* → Chrome (CDP)
|
|
14
|
+
*
|
|
15
|
+
* Registration: add to .mcp.json as "argus" server.
|
|
16
|
+
* Run standalone: node src/mcp-server.js
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
20
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
21
|
+
import {
|
|
22
|
+
ListToolsRequestSchema,
|
|
23
|
+
CallToolRequestSchema,
|
|
24
|
+
} from '@modelcontextprotocol/sdk/types.js';
|
|
25
|
+
import fs from 'fs';
|
|
26
|
+
import path from 'path';
|
|
27
|
+
|
|
28
|
+
import { createMcpClient } from './utils/mcp-client.js';
|
|
29
|
+
import { crawlRouteCheap, runCrawl } from './orchestration/crawl-and-report.js';
|
|
30
|
+
import { runComparison } from './orchestration/env-comparison.js';
|
|
31
|
+
import { WatchSession } from './orchestration/watch-mode.js';
|
|
32
|
+
import { CdpBrowserAdapter } from './adapters/browser.js';
|
|
33
|
+
|
|
34
|
+
const REPORTS_DIR = path.resolve(process.cwd(), 'reports');
|
|
35
|
+
|
|
36
|
+
// Fix loop: stores up to 20 snapshots keyed by snapshot_id so argus_get_context
|
|
37
|
+
// can diff before/after a fix. Keys are evicted oldest-first when the limit is hit.
|
|
38
|
+
const snapshotStore = new Map();
|
|
39
|
+
const MAX_SNAPSHOTS = 20;
|
|
40
|
+
|
|
41
|
+
// Audit cache: stores argus_audit results keyed by URL so cache:true skips re-crawl.
|
|
42
|
+
const auditCache = new Map();
|
|
43
|
+
const MAX_AUDIT_CACHE = 20;
|
|
44
|
+
|
|
45
|
+
function storeSnapshot(id, findings) {
|
|
46
|
+
snapshotStore.set(id, findings);
|
|
47
|
+
if (snapshotStore.size > MAX_SNAPSHOTS) {
|
|
48
|
+
snapshotStore.delete(snapshotStore.keys().next().value);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function cacheAudit(url, result) {
|
|
53
|
+
auditCache.set(url, { result, ts: Date.now() });
|
|
54
|
+
if (auditCache.size > MAX_AUDIT_CACHE) {
|
|
55
|
+
auditCache.delete(auditCache.keys().next().value);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ── Tool definitions ─────────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
const TOOLS = [
|
|
62
|
+
{
|
|
63
|
+
name: 'argus_audit',
|
|
64
|
+
description: 'Fast QA audit on a URL via Chrome DevTools Protocol. Runs 8 analyzers in one pass: JS errors, unhandled rejections, network failures (4xx/5xx), API frequency loops, CSS cascade issues, SEO violations, security header checks, and accessibility. Returns { findings: [{severity, type, message, url}], summary: {critical, warning, info} }. Use for CI smoke tests and pre-deploy gates. Pass cache: true to skip re-crawl on repeat calls to the same URL within a session — useful in tight fix loops. For Lighthouse scoring and memory leak detection, use argus_audit_full. Requires Chrome running with --remote-debugging-port=9222.',
|
|
65
|
+
inputSchema: {
|
|
66
|
+
type: 'object',
|
|
67
|
+
properties: {
|
|
68
|
+
url: { type: 'string', description: 'Full URL to audit, including protocol and path (e.g. http://localhost:3000/checkout). Must be reachable by the running Chrome instance.' },
|
|
69
|
+
critical: { type: 'boolean', description: 'When true, console.error calls are escalated to critical severity. Set true for business-critical routes (login, checkout, dashboard) where any error is a blocker.', default: false },
|
|
70
|
+
cache: { type: 'boolean', description: 'When true, returns the cached result for this URL if one exists (from a previous argus_audit call in this session) without re-crawling. Use in fix loops to cheaply re-read the last audit while iterating on a fix. Cache is per-session, max 20 entries, LRU eviction.', default: false },
|
|
71
|
+
},
|
|
72
|
+
required: ['url'],
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
name: 'argus_audit_full',
|
|
77
|
+
description: 'Deep QA audit — extends argus_audit with Lighthouse performance/accessibility scoring, responsive layout checks across 4 viewports (320/768/1280/1920px), memory leak detection via heap snapshot, hover-state regression detection, and accessibility tree snapshot. Returns full JSON report with findings by severity, Lighthouse scores, and layout overflow details. Use when argus_audit passes clean but visual or performance regressions are suspected. Requires Chrome running with --remote-debugging-port=9222.',
|
|
78
|
+
inputSchema: {
|
|
79
|
+
type: 'object',
|
|
80
|
+
properties: {
|
|
81
|
+
url: { type: 'string', description: 'Full URL to audit, including protocol and path (e.g. https://example.com/dashboard). Must be reachable by the running Chrome instance.' },
|
|
82
|
+
critical: { type: 'boolean', description: 'When true, console.error calls are escalated to critical severity. Set true for business-critical routes (login, checkout, dashboard) where any error is a blocker.', default: false },
|
|
83
|
+
},
|
|
84
|
+
required: ['url'],
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: 'argus_compare',
|
|
89
|
+
description: 'Diffs dev vs staging environments side-by-side. Navigates both URLs, captures screenshots, and runs the full analyzer suite on each, then surfaces regressions — findings present in staging but not dev, or with changed severity. Returns { regressions: [{type, devSeverity, stagingSeverity}], screenshots, summary }. Run before promoting a build to staging to catch environment-specific bugs. Set TARGET_DEV_URL and TARGET_STAGING_URL env vars before starting the server; omit TARGET_STAGING_URL to run CSS-analysis-only mode.',
|
|
90
|
+
inputSchema: { type: 'object', properties: {} },
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
name: 'argus_last_report',
|
|
94
|
+
description: 'Returns the most recent Argus JSON report from the reports/ directory. Report includes a findings array and severity summary (critical/warning/info counts). Returns { "error": "No reports found in reports/" } when no audits have been run yet. Use to retrieve prior results without re-running a scan, or to pipe findings into another analysis tool.',
|
|
95
|
+
inputSchema: { type: 'object', properties: {} },
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
name: 'argus_watch_snapshot',
|
|
99
|
+
description: 'Snapshots the currently open Chrome tab without navigating — captures console errors, network failures (4xx/5xx), CORS blocks, and auth failures in one poll. Returns { findings: [{severity, type, message, url}], newConsole, newNetwork }. Use during active development to inspect what is happening on the current page without running a full audit. Pass tabId to inspect a specific tab (get IDs from argus_get_context or list_pages). Without tabId, reads the active tab. Requires Chrome on --remote-debugging-port=9222 with a page already open.',
|
|
100
|
+
inputSchema: {
|
|
101
|
+
type: 'object',
|
|
102
|
+
properties: {
|
|
103
|
+
url: { type: 'string', description: 'Optional base URL to attribute findings to (default: TARGET_DEV_URL env var). Does not navigate — reads the currently open Chrome tab.' },
|
|
104
|
+
tabId: { type: 'string', description: 'Optional Chrome page/tab ID (e.g. from a prior argus_get_context response). When provided, switches focus to that tab before snapshotting — useful for SPAs that spawn new windows or multi-tab flows.' },
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
name: 'argus_get_context',
|
|
110
|
+
description: 'Captures everything currently broken on the open Chrome tab and formats it as a diagnostic context for Claude to read and suggest fixes. Does NOT navigate — reads the live tab state after user interactions, in authenticated sessions, or mid-flow. Returns { snapshot_id, summary, url, timestamp, critical_issues, warnings, js_errors, network_failures, console_errors, recent_requests, open_tabs }. Fix loop: pass the snapshot_id from a previous call as snapshot_id to get a diff — the response will include resolved (cleared since last snapshot), new_issues (appeared since last snapshot), and persisting (unchanged). Multi-tab: pass tabId to inspect a specific tab, or omit to read the active tab. The open_tabs array always lists all currently open Chrome tabs. Workflow: call argus_get_context → Claude suggests fix → apply fix → call argus_get_context with snapshot_id → verify resolved array is non-empty. Requires Chrome on --remote-debugging-port=9222.',
|
|
111
|
+
inputSchema: {
|
|
112
|
+
type: 'object',
|
|
113
|
+
properties: {
|
|
114
|
+
url: { type: 'string', description: 'Optional base URL to attribute findings to (default: TARGET_DEV_URL env var). Does not navigate — inspects the currently open Chrome tab.' },
|
|
115
|
+
snapshot_id: { type: 'string', description: 'Optional snapshot_id from a previous argus_get_context call. When provided, the response includes resolved/new_issues/persisting arrays showing what changed since that snapshot.' },
|
|
116
|
+
tabId: { type: 'string', description: 'Optional Chrome page/tab ID. When provided, switches focus to that specific tab before capturing context — useful for SPAs that spawn new windows (e.g. OAuth popups, checkout flows). Get tab IDs from the open_tabs array in a prior argus_get_context response.' },
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
async function withMcp(fn) {
|
|
125
|
+
const mcp = await createMcpClient();
|
|
126
|
+
try {
|
|
127
|
+
return await fn(mcp);
|
|
128
|
+
} finally {
|
|
129
|
+
try { mcp.close(); } catch { /* ignore — process already gone */ }
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ── Tool handlers ─────────────────────────────────────────────────────────────
|
|
134
|
+
|
|
135
|
+
async function handleAudit({ url, critical = false, cache = false }) {
|
|
136
|
+
if (cache && auditCache.has(url)) {
|
|
137
|
+
const { result, ts } = auditCache.get(url);
|
|
138
|
+
return { content: [{ type: 'text', text: JSON.stringify({ ...result, _cached: true, _cachedAt: new Date(ts).toISOString() }, null, 2) }] };
|
|
139
|
+
}
|
|
140
|
+
return withMcp(async (mcp) => {
|
|
141
|
+
const parsed = new URL(url);
|
|
142
|
+
const route = { path: parsed.pathname + parsed.search + parsed.hash, name: 'audit', critical };
|
|
143
|
+
const raw = await crawlRouteCheap(route, parsed.origin, mcp);
|
|
144
|
+
const findings = Array.isArray(raw.errors) ? raw.errors : [];
|
|
145
|
+
const result = {
|
|
146
|
+
findings,
|
|
147
|
+
summary: {
|
|
148
|
+
critical: findings.filter(f => f.severity === 'critical').length,
|
|
149
|
+
warning: findings.filter(f => f.severity === 'warning').length,
|
|
150
|
+
info: findings.filter(f => f.severity === 'info').length,
|
|
151
|
+
},
|
|
152
|
+
url: raw.url,
|
|
153
|
+
pageTitle: raw.pageTitle,
|
|
154
|
+
screenshot: raw.screenshot,
|
|
155
|
+
};
|
|
156
|
+
if (cache) cacheAudit(url, result);
|
|
157
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function handleAuditFull({ url, critical = false }) {
|
|
162
|
+
return withMcp(async (mcp) => {
|
|
163
|
+
const parsed = new URL(url);
|
|
164
|
+
const report = await runCrawl(
|
|
165
|
+
mcp,
|
|
166
|
+
[{ path: parsed.pathname + parsed.search + parsed.hash, name: 'audit', critical }],
|
|
167
|
+
parsed.origin,
|
|
168
|
+
);
|
|
169
|
+
return { content: [{ type: 'text', text: JSON.stringify(report, null, 2) }] };
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function handleCompare() {
|
|
174
|
+
return withMcp(async (mcp) => {
|
|
175
|
+
const report = await runComparison(mcp);
|
|
176
|
+
return { content: [{ type: 'text', text: JSON.stringify(report, null, 2) }] };
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function handleWatchSnapshot({ url, tabId } = {}) {
|
|
181
|
+
return withMcp(async (mcp) => {
|
|
182
|
+
const browser = new CdpBrowserAdapter(mcp);
|
|
183
|
+
if (tabId) await browser.selectPage(tabId);
|
|
184
|
+
const baseUrl = url ?? process.env.TARGET_DEV_URL ?? 'http://localhost:3000';
|
|
185
|
+
const session = new WatchSession(browser, baseUrl);
|
|
186
|
+
const result = await session.poll();
|
|
187
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function handleGetContext({ url, snapshot_id: prevId, tabId } = {}) {
|
|
192
|
+
return withMcp(async (mcp) => {
|
|
193
|
+
const browser = new CdpBrowserAdapter(mcp);
|
|
194
|
+
if (tabId) await browser.selectPage(tabId);
|
|
195
|
+
const baseUrl = url ?? process.env.TARGET_DEV_URL ?? 'http://localhost:3000';
|
|
196
|
+
const session = new WatchSession(browser, baseUrl);
|
|
197
|
+
const { findings, newConsole, newNetwork } = await session.poll();
|
|
198
|
+
|
|
199
|
+
// List all open tabs so the caller can target a specific tab on the next call.
|
|
200
|
+
let open_tabs = [];
|
|
201
|
+
try {
|
|
202
|
+
const pages = await browser.listPages();
|
|
203
|
+
if (Array.isArray(pages)) {
|
|
204
|
+
open_tabs = pages.map(p => ({ id: p.id ?? p.pageId, url: p.url, title: p.title }));
|
|
205
|
+
}
|
|
206
|
+
} catch { /* list_pages not available in all Chrome configs — degrade gracefully */ }
|
|
207
|
+
|
|
208
|
+
const newId = Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
|
|
209
|
+
storeSnapshot(newId, findings);
|
|
210
|
+
|
|
211
|
+
const critical = findings.filter(f => f.severity === 'critical');
|
|
212
|
+
const warnings = findings.filter(f => f.severity === 'warning');
|
|
213
|
+
|
|
214
|
+
const findingKey = (f) => `${f.type}::${(f.message ?? '').slice(0, 120)}`;
|
|
215
|
+
|
|
216
|
+
let resolved = [];
|
|
217
|
+
let persisting = [];
|
|
218
|
+
let new_issues = findings;
|
|
219
|
+
const isDiff = prevId && snapshotStore.has(prevId);
|
|
220
|
+
|
|
221
|
+
if (isDiff) {
|
|
222
|
+
const prev = snapshotStore.get(prevId);
|
|
223
|
+
const prevKeys = new Set(prev.map(findingKey));
|
|
224
|
+
const curKeys = new Set(findings.map(findingKey));
|
|
225
|
+
resolved = prev.filter(f => !curKeys.has(findingKey(f)));
|
|
226
|
+
persisting = findings.filter(f => prevKeys.has(findingKey(f)));
|
|
227
|
+
new_issues = findings.filter(f => !prevKeys.has(findingKey(f)));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
let summary;
|
|
231
|
+
if (isDiff) {
|
|
232
|
+
if (resolved.length > 0 && critical.length === 0 && warnings.length === 0) {
|
|
233
|
+
summary = `All issues resolved on ${baseUrl}. ${resolved.length} finding${resolved.length > 1 ? 's' : ''} cleared since last snapshot.`;
|
|
234
|
+
} else if (resolved.length > 0) {
|
|
235
|
+
summary = `${resolved.length} issue${resolved.length > 1 ? 's' : ''} resolved on ${baseUrl}. ${new_issues.length} new + ${persisting.length} persisting (${critical.length} critical). Pass the new snapshot_id to continue the fix loop.`;
|
|
236
|
+
} else if (critical.length === 0 && warnings.length === 0) {
|
|
237
|
+
summary = `No issues on ${baseUrl} — console and network are clean.`;
|
|
238
|
+
} else {
|
|
239
|
+
summary = `No change on ${baseUrl}: ${critical.length} critical + ${warnings.length} warning${warnings.length !== 1 ? 's' : ''} still present. Check the persisting array for what hasn't been fixed.`;
|
|
240
|
+
}
|
|
241
|
+
} else if (critical.length === 0 && warnings.length === 0) {
|
|
242
|
+
summary = `No issues detected on ${baseUrl} — console and network are clean.`;
|
|
243
|
+
} else if (critical.length > 0) {
|
|
244
|
+
summary = `${critical.length} critical issue${critical.length > 1 ? 's' : ''} + ${warnings.length} warning${warnings.length !== 1 ? 's' : ''} detected on ${baseUrl}. Focus on critical issues first. Pass snapshot_id to the next argus_get_context call after applying a fix to see what changed.`;
|
|
245
|
+
} else {
|
|
246
|
+
summary = `${warnings.length} warning${warnings.length !== 1 ? 's' : ''} detected on ${baseUrl}. No critical errors. Pass snapshot_id to the next call to verify fixes.`;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const context = {
|
|
250
|
+
snapshot_id: newId,
|
|
251
|
+
summary,
|
|
252
|
+
url: baseUrl,
|
|
253
|
+
timestamp: new Date().toISOString(),
|
|
254
|
+
critical_issues: critical,
|
|
255
|
+
warnings,
|
|
256
|
+
js_errors: findings.filter(f => f.type === 'js-error' || f.type === 'unhandled-rejection'),
|
|
257
|
+
network_failures: findings.filter(f => f.type === 'network-error' || f.type === 'cors-error' || f.type === 'auth-error'),
|
|
258
|
+
console_errors: newConsole.filter(m => m.level === 'error' || m.level === 'warning'),
|
|
259
|
+
recent_requests: newNetwork.slice(-20),
|
|
260
|
+
open_tabs,
|
|
261
|
+
...(isDiff ? { resolved, new_issues, persisting } : {}),
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
return { content: [{ type: 'text', text: JSON.stringify(context, null, 2) }] };
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async function handleLastReport() {
|
|
269
|
+
if (!fs.existsSync(REPORTS_DIR)) {
|
|
270
|
+
return { content: [{ type: 'text', text: '{"error":"No reports found in reports/"}' }] };
|
|
271
|
+
}
|
|
272
|
+
const files = fs.readdirSync(REPORTS_DIR).filter(f => f.endsWith('.json'));
|
|
273
|
+
if (files.length === 0) {
|
|
274
|
+
return { content: [{ type: 'text', text: '{"error":"No reports found in reports/"}' }] };
|
|
275
|
+
}
|
|
276
|
+
const latest = files
|
|
277
|
+
.map(f => ({ f, mt: fs.statSync(path.join(REPORTS_DIR, f)).mtimeMs }))
|
|
278
|
+
.sort((a, b) => b.mt - a.mt)[0].f;
|
|
279
|
+
const json = fs.readFileSync(path.join(REPORTS_DIR, latest), 'utf8');
|
|
280
|
+
return { content: [{ type: 'text', text: json }] };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ── Server bootstrap ──────────────────────────────────────────────────────────
|
|
284
|
+
|
|
285
|
+
const server = new Server(
|
|
286
|
+
{ name: 'argus', version: '9.4.5' },
|
|
287
|
+
{ capabilities: { tools: {} } },
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
|
|
291
|
+
|
|
292
|
+
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
293
|
+
try {
|
|
294
|
+
switch (req.params.name) {
|
|
295
|
+
case 'argus_audit': return await handleAudit(req.params.arguments ?? {});
|
|
296
|
+
case 'argus_audit_full': return await handleAuditFull(req.params.arguments ?? {});
|
|
297
|
+
case 'argus_compare': return await handleCompare();
|
|
298
|
+
case 'argus_last_report': return await handleLastReport();
|
|
299
|
+
case 'argus_watch_snapshot': return await handleWatchSnapshot(req.params.arguments ?? {});
|
|
300
|
+
case 'argus_get_context': return await handleGetContext(req.params.arguments ?? {});
|
|
301
|
+
default: throw new Error(`Unknown tool: ${req.params.name}`);
|
|
302
|
+
}
|
|
303
|
+
} catch (err) {
|
|
304
|
+
return {
|
|
305
|
+
content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }],
|
|
306
|
+
isError: true,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
const transport = new StdioServerTransport();
|
|
312
|
+
await server.connect(transport);
|
|
@@ -37,7 +37,7 @@ const RAW_STAGING_URL = process.env.TARGET_STAGING_URL ?? '';
|
|
|
37
37
|
// Validate as a parseable URL with a non-localhost hostname — checking only against
|
|
38
38
|
// one hardcoded placeholder string misses 'TODO', 'your-url-here', http://localhost, etc.
|
|
39
39
|
const STAGING_URL_SET = (() => {
|
|
40
|
-
if (!RAW_STAGING_URL || RAW_STAGING_URL === 'https://staging.
|
|
40
|
+
if (!RAW_STAGING_URL || RAW_STAGING_URL === 'https://staging.example.com') return false;
|
|
41
41
|
try {
|
|
42
42
|
const u = new URL(RAW_STAGING_URL);
|
|
43
43
|
return u.hostname !== 'localhost' && u.hostname !== '127.0.0.1' && u.hostname !== '';
|
|
@@ -93,7 +93,7 @@ export async function handleSlashCommand(req, res) {
|
|
|
93
93
|
if (!targetUrl) {
|
|
94
94
|
return res.json({
|
|
95
95
|
response_type: 'ephemeral',
|
|
96
|
-
text: '⚠️ Usage: `/argus-retest <url>`\nExample: `/argus-retest https://staging.
|
|
96
|
+
text: '⚠️ Usage: `/argus-retest <url>`\nExample: `/argus-retest https://staging.example.com/checkout`',
|
|
97
97
|
});
|
|
98
98
|
}
|
|
99
99
|
|
package/src/utils/telemetry.js
CHANGED
|
@@ -30,7 +30,7 @@ async function maybeStartSdk() {
|
|
|
30
30
|
const { resourceFromAttributes } = await import('@opentelemetry/resources');
|
|
31
31
|
const resource = resourceFromAttributes({
|
|
32
32
|
'service.name': 'argus',
|
|
33
|
-
'service.version': '9.
|
|
33
|
+
'service.version': '9.4.4',
|
|
34
34
|
});
|
|
35
35
|
|
|
36
36
|
if (consoleMode && !hasOtlpEndpoint) {
|