@debugg-ai/debugg-ai-mcp 3.10.0 → 4.1.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/CHANGELOG.md +42 -0
- package/README.md +53 -1
- package/dist/handlers/environmentHandler.js +15 -0
- package/dist/handlers/environmentSessionsHandler.js +60 -0
- package/dist/handlers/probePageHandler.js +252 -139
- package/dist/handlers/runTestSuiteHandler.js +70 -51
- package/dist/handlers/testPageChangesHandler.js +33 -1
- package/dist/handlers/triggerCrawlHandler.js +26 -1
- package/dist/services/caddy/caddyProxy.js +611 -0
- package/dist/services/caddy/portLock.js +321 -0
- package/dist/services/index.js +31 -0
- package/dist/services/ngrok/tunnelManager.js +526 -625
- package/dist/services/ngrok/tunnelRegistry.js +57 -70
- package/dist/tools/environment.js +9 -3
- package/dist/tools/testPageChanges.js +4 -0
- package/dist/types/index.js +11 -0
- package/dist/utils/confirmDestructive.js +32 -6
- package/dist/utils/telemetry.js +16 -0
- package/dist/utils/tunnelContext.js +52 -15
- package/dist/utils/tunnelDisposition.js +9 -17
- package/package.json +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,48 @@ All notable changes to the DebuggAI MCP project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [Unreleased] — BREAKING
|
|
9
|
+
|
|
10
|
+
### Changed — one ngrok tunnel per session instead of one per local port
|
|
11
|
+
|
|
12
|
+
`check_app_in_browser`, `probe_page`, and `trigger_crawl` now share a **single**
|
|
13
|
+
ngrok tunnel per session (keyed per-caller on HTTP transport, so different
|
|
14
|
+
callers never share state), backed by a local Caddy reverse proxy that gets
|
|
15
|
+
repointed at whichever local port a call targets immediately before dispatch.
|
|
16
|
+
Previously every distinct local port tested in a session opened its own ngrok
|
|
17
|
+
tunnel — N ports meant N billed tunnels. Full design:
|
|
18
|
+
`docs/local-tunnel-multiplexer-architecture-2026-07-31.md`.
|
|
19
|
+
|
|
20
|
+
This replaces (not extends) a Feb 2026 attempt at the same goal that used
|
|
21
|
+
path-prefix routing (`/p/{port}/*`) and broke on any root-absolute asset/API
|
|
22
|
+
path (`/api/...`, `/_next/...` — the default in most modern frameworks). The
|
|
23
|
+
new design routes through a single dynamic upstream with zero path/host
|
|
24
|
+
rewriting, so that failure mode is structurally impossible rather than patched.
|
|
25
|
+
A live-browser regression test for exactly this case now exists and passes
|
|
26
|
+
(`__tests__/integration/caddyProxy.test.ts`).
|
|
27
|
+
|
|
28
|
+
**New runtime dependency, auto-installed:** `check_app_in_browser`/`probe_page`/`trigger_crawl`
|
|
29
|
+
now need the `caddy` binary for any `http://localhost:...` call. It installs itself — the
|
|
30
|
+
`@radically-straightforward/caddy` npm dependency downloads a pinned Caddy release (`2.11.3`) for
|
|
31
|
+
your platform during `npm install`/`npx`, the same pattern this project already uses for the
|
|
32
|
+
`ngrok` binary. Falls back to `CADDY_BIN` (or a system `caddy` on `PATH`) if that download never
|
|
33
|
+
ran (`npm install --ignore-scripts`, offline install) — fails fast with a clear
|
|
34
|
+
`CaddyBinaryNotFoundError` rather than a silent hang if none of those resolve.
|
|
35
|
+
`test_suite {action:"run"}` is unaffected either way (dedicated per-run tunnel, bypasses Caddy).
|
|
36
|
+
|
|
37
|
+
**Deployment precondition for HTTP transport:** multi-replica deployments that
|
|
38
|
+
want the one-tunnel-per-session guarantee need session-affine load-balancer
|
|
39
|
+
routing (consistent hash / sticky on the caller's bearer token). Without it,
|
|
40
|
+
tunnel count degrades to bounded, cost-only over-provisioning (never a
|
|
41
|
+
correctness issue) — see architecture doc §2.1.
|
|
42
|
+
|
|
43
|
+
### Fixed — two latent response-sanitization bugs surfaced by the above
|
|
44
|
+
|
|
45
|
+
- `probe_page` multi-target batches could cross-attribute a tunnel-hostname
|
|
46
|
+
rewrite from one target's result onto another's once targets started sharing
|
|
47
|
+
a session hostname (they didn't, before this change).
|
|
48
|
+
- `run_test_suite` had no defensive URL-sanitization call at all.
|
|
49
|
+
|
|
8
50
|
## [3.5.1]
|
|
9
51
|
|
|
10
52
|
### Fixed — default OAuth issuer points at the Django AS
|
package/README.md
CHANGED
|
@@ -10,6 +10,18 @@ AI-powered browser testing via the [Model Context Protocol](https://modelcontext
|
|
|
10
10
|
|
|
11
11
|
**Requires Node.js 20.20.0 or later** (transitive requirement from `posthog-node@^5.26.0`).
|
|
12
12
|
|
|
13
|
+
**Testing `http://localhost:...` URLs requires the `caddy` binary** — `check_app_in_browser`,
|
|
14
|
+
`probe_page`, and `trigger_crawl` tunnel localhost targets through a local Caddy reverse proxy.
|
|
15
|
+
This installs automatically: the `@radically-straightforward/caddy` npm dependency downloads a
|
|
16
|
+
pinned Caddy release for your platform during `npm install`/`npx`, same as this project already
|
|
17
|
+
does for the `ngrok` binary — nothing to install yourself in the normal case. If that download
|
|
18
|
+
never ran (`npm install --ignore-scripts`, an offline/air-gapped install), point `CADDY_BIN` at
|
|
19
|
+
your own install (`brew install caddy` / `apt install caddy` / see
|
|
20
|
+
[caddyserver.com/docs/install](https://caddyserver.com/docs/install)) — missing it surfaces as a
|
|
21
|
+
clear error on the first localhost-URL call, not a silent hang. Public-URL calls, every
|
|
22
|
+
non-browser tool, and `test_suite {action:"run"}` (which uses its own dedicated tunnel and
|
|
23
|
+
bypasses Caddy entirely) don't need it either way.
|
|
24
|
+
|
|
13
25
|
Get an API key at [debugg.ai](https://debugg.ai), then add to your MCP client config:
|
|
14
26
|
|
|
15
27
|
```json
|
|
@@ -32,6 +44,17 @@ Or with Docker:
|
|
|
32
44
|
docker run -i --rm --init -e DEBUGGAI_API_KEY=your_api_key quinnosha/debugg-ai-mcp
|
|
33
45
|
```
|
|
34
46
|
|
|
47
|
+
The `Dockerfile`'s `npm install` step would pick up `caddy` the same automatic way local installs
|
|
48
|
+
do, in principle — but as of this writing the `Dockerfile` doesn't `COPY` several directories the
|
|
49
|
+
build now needs (`handlers`, `tools`, `types`, `config`) and still references a `tunnels/`
|
|
50
|
+
directory that no longer exists, so a fresh build likely fails before that matters. That's a
|
|
51
|
+
pre-existing gap, unrelated to Caddy. The **currently published** `quinnosha/debugg-ai-mcp` image
|
|
52
|
+
predates the Caddy dependency regardless — localhost-URL calls to
|
|
53
|
+
`check_app_in_browser`/`probe_page`/`trigger_crawl` will fail with `CaddyBinaryNotFoundError`
|
|
54
|
+
inside that image until it's rebuilt (Dockerfile fixed) and republished, or `CADDY_BIN` points at
|
|
55
|
+
one baked in separately. Public-URL calls, the non-browser tools, and `test_suite {action:"run"}`
|
|
56
|
+
are unaffected either way.
|
|
57
|
+
|
|
35
58
|
## Tools
|
|
36
59
|
|
|
37
60
|
The server exposes **8** tools: three **Browser** tools plus one **action-based** tool per managed entity. The headline tools are `check_app_in_browser` (full AI agent) and `probe_page` (lightweight no-LLM page probe). The rest — `project`, `environment`, `test_suite`, `test_case`, `executions` — each take an `action` discriminator (e.g. `{"action":"list"}`) that selects the operation. Destructive `delete` actions require confirmation (an elicitation prompt where supported, otherwise `confirm: true`).
|
|
@@ -53,6 +76,7 @@ Runs an AI browser agent against your app. The agent navigates, interacts, and r
|
|
|
53
76
|
| `password` | string | Password for login (ephemeral — not persisted) |
|
|
54
77
|
| `loginCredentials` | array | Accounts for logins the agent hits **during** the task — `[{username, password, label?}]` |
|
|
55
78
|
| `useEnvironmentCredentials` | boolean | Default `true`. `false` forbids auto-filling the environment's stored credentials |
|
|
79
|
+
| `freshSession` | boolean | Default `false`. `true` forces a real login instead of reusing the warm session held for that account |
|
|
56
80
|
| `auth` | object | Auth precondition — `{precondition, entryUrl, deepUrl, environmentId, username, password}` |
|
|
57
81
|
| `repoName` | string | Override auto-detected git repo name (e.g. `my-org/my-repo`) |
|
|
58
82
|
|
|
@@ -68,6 +92,17 @@ Naming an account only in `description` does **not** make the agent use it — i
|
|
|
68
92
|
|
|
69
93
|
Set `useEnvironmentCredentials: false` when a silent fallback to the default test user would invalidate the check. The call is rejected if you opt out without naming an account, since the run would have no way to authenticate.
|
|
70
94
|
|
|
95
|
+
##### Session reuse: why a check can report "no login form"
|
|
96
|
+
|
|
97
|
+
Runs don't log in every time. After a verified login the backend captures that account's session and **restores** it on the next run for the same identity, which skips the login entirely — that's why a check can legitimately come back with `submitted: false` and no login form: it was already signed in. A restored run reports itself in `logins` with `reason: "restored_session"`, so you can tell it apart from a run that genuinely found no form.
|
|
98
|
+
|
|
99
|
+
Sessions are keyed per **account**, so naming a different account never reuses somebody else's. Two ways to bypass reuse:
|
|
100
|
+
|
|
101
|
+
- `freshSession: true` on a single call — log in for real this once, then re-capture. Use it when the login flow *is* what you're checking, when you suspect the stored session is stale, or when the app's only route between personas is a logout.
|
|
102
|
+
- `environment` tool, `action: "clearSessions"` — invalidate the stored sessions so subsequent runs log in. Narrow with `username` / `credentialId`; unscoped clears require confirmation because every account on the environment then re-authenticates.
|
|
103
|
+
|
|
104
|
+
Use `action: "sessions"` to see what an environment is currently holding and whether each would be reused.
|
|
105
|
+
|
|
71
106
|
Results report the identity actually used, so a wrong one is visible rather than masquerading as a broken app:
|
|
72
107
|
|
|
73
108
|
```json
|
|
@@ -117,7 +152,7 @@ Fires a server-side browser-agent crawl to populate the project's knowledge grap
|
|
|
117
152
|
| `includeHtml` | boolean | Return raw HTML in each result (default false) |
|
|
118
153
|
| `captureScreenshots` | boolean | Return one PNG per target (default true) |
|
|
119
154
|
|
|
120
|
-
|
|
155
|
+
All targets in a batch share one session tunnel, but only same-port (or all-public) batches share a **single** backend execution — 5 URLs on one port in one call is dramatically faster than 5 parallel single-URL calls. A batch that mixes multiple **local** ports decomposes into one sequential backend execution per port group (still one call, still one merged `results[]` in your original order, but N backend round-trips instead of one — slower, not rejected). Per-URL `error` field preserves batch resilience: a single failed target doesn't fail the others.
|
|
121
156
|
|
|
122
157
|
**`networkSummary` aggregation key is `origin + pathname`** — refetch loops (`?n=0..4` repeatedly hitting the same endpoint) collapse into a single entry with the count, so `/api/poll` showing up with `count: 47` is the actionable "infinite refetch loop" signal users originally asked for.
|
|
123
158
|
|
|
@@ -142,9 +177,13 @@ Team and repo resolve by **either** uuid **or** name (case-insensitive exact mat
|
|
|
142
177
|
| `create` | `{name, url, description?, projectUuid?, credentials?}` | Created env (optionally seeds credentials) |
|
|
143
178
|
| `update` | `{uuid, name?, url?, description?, addCredentials?, updateCredentials?, removeCredentialIds?}` | Patched env; credential ops run **remove → update → add** |
|
|
144
179
|
| `delete` | `{uuid, projectUuid?, confirm?}` | Deletes env (cascades credentials) — **requires confirmation** |
|
|
180
|
+
| `sessions` | `{uuid, username?, credentialId?}` | Captured login sessions the env holds, per account, with `isUsable` and a `usableCount` |
|
|
181
|
+
| `clearSessions` | `{uuid, username?, credentialId?, confirm?}` | Invalidates them so the next run logs in for real — **unscoped clears require confirmation** |
|
|
145
182
|
|
|
146
183
|
`projectUuid` auto-resolves from the git repo when omitted. Per-cred failures surface in `credentialWarnings[]` without blocking the env op.
|
|
147
184
|
|
|
185
|
+
`sessions` / `clearSessions` manage the warm authenticated sessions the backend reuses to skip login (see [Session reuse](#session-reuse-why-a-check-can-report-no-login-form)). Session contents are never returned — a session cookie is a bearer credential. `clearSessions` marks sessions invalid rather than deleting the rows, so reuse stops immediately while the capture history stays readable.
|
|
186
|
+
|
|
148
187
|
### `test_suite`
|
|
149
188
|
|
|
150
189
|
| Action | Params | Result |
|
|
@@ -294,6 +333,19 @@ flow against the advertised authorization server. The bearer is request-scoped
|
|
|
294
333
|
|
|
295
334
|
stdio installs need none of these.
|
|
296
335
|
|
|
336
|
+
**Multi-replica deployments (go/no-go before rollout):** tunnel state (the ngrok session tunnel,
|
|
337
|
+
its Caddy instance, and its port-route lock) is in-process, keyed per caller by a hash of the
|
|
338
|
+
bearer token — there is no cross-process coordination. Running several replicas behind a plain
|
|
339
|
+
round-robin load balancer means one caller's calls can land on different replicas and mint one
|
|
340
|
+
tunnel **per replica they hit** instead of one for the whole session (extra ngrok cost, bounded by
|
|
341
|
+
replica count, self-healing via the existing 55-minute idle auto-shutoff — never a cross-session
|
|
342
|
+
correctness bug, since any single tool call stays on one replica for its whole duration). To get
|
|
343
|
+
the intended "one tunnel per session" behavior on a multi-replica HTTP deployment, configure
|
|
344
|
+
**session-affine routing** at the load balancer (sticky/consistent-hash keyed on the same identity
|
|
345
|
+
`getSessionKey()` derives — in practice, the caller's `Authorization` bearer token). See
|
|
346
|
+
`docs/local-tunnel-multiplexer-architecture-2026-07-31.md` §2.1 for the full reasoning and the
|
|
347
|
+
honest degrade path if this isn't configured.
|
|
348
|
+
|
|
297
349
|
## Telemetry
|
|
298
350
|
|
|
299
351
|
The MCP server ships with telemetry enabled by default — an embedded write-only PostHog project key (`phc_*`) so the team can observe cache hit rates, poll cadence, tunnel reliability, and other operational metrics across the install base. Captured events:
|
|
@@ -3,6 +3,7 @@ import { searchEnvironmentsHandler } from './searchEnvironmentsHandler.js';
|
|
|
3
3
|
import { createEnvironmentHandler } from './createEnvironmentHandler.js';
|
|
4
4
|
import { updateEnvironmentHandler } from './updateEnvironmentHandler.js';
|
|
5
5
|
import { deleteEnvironmentHandler } from './deleteEnvironmentHandler.js';
|
|
6
|
+
import { clearEnvironmentSessionsHandler, listEnvironmentSessionsHandler, } from './environmentSessionsHandler.js';
|
|
6
7
|
export async function environmentHandler(input, ctx) {
|
|
7
8
|
switch (input.action) {
|
|
8
9
|
case 'get':
|
|
@@ -23,5 +24,19 @@ export async function environmentHandler(input, ctx) {
|
|
|
23
24
|
return refusal;
|
|
24
25
|
return deleteEnvironmentHandler({ uuid: input.uuid, projectUuid: input.projectUuid }, ctx);
|
|
25
26
|
}
|
|
27
|
+
case 'sessions':
|
|
28
|
+
return listEnvironmentSessionsHandler(input, ctx);
|
|
29
|
+
case 'clearSessions': {
|
|
30
|
+
// Confirmed like a delete when it is UNSCOPED. Clearing one account's
|
|
31
|
+
// session costs that account one login; clearing an environment's costs
|
|
32
|
+
// every account on it one, which is a different size of action and should
|
|
33
|
+
// not happen because a filter was mistyped.
|
|
34
|
+
if (!input.username && !input.credentialId) {
|
|
35
|
+
const refusal = await ensureConfirmed('clearSessions', `environment ${input.uuid}`, input, ctx);
|
|
36
|
+
if (refusal)
|
|
37
|
+
return refusal;
|
|
38
|
+
}
|
|
39
|
+
return clearEnvironmentSessionsHandler(input, ctx);
|
|
40
|
+
}
|
|
26
41
|
}
|
|
27
42
|
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Logger } from '../utils/logger.js';
|
|
2
|
+
import { handleExternalServiceError } from '../utils/errors.js';
|
|
3
|
+
import { DebuggAIServerClient } from '../services/index.js';
|
|
4
|
+
import { config } from '../config/index.js';
|
|
5
|
+
const logger = new Logger({ module: 'environmentSessionsHandler' });
|
|
6
|
+
function ok(payload) {
|
|
7
|
+
return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }] };
|
|
8
|
+
}
|
|
9
|
+
export async function listEnvironmentSessionsHandler(input, _context) {
|
|
10
|
+
logger.toolStart('environment.sessions', { uuid: input.uuid, username: input.username });
|
|
11
|
+
try {
|
|
12
|
+
const client = new DebuggAIServerClient(config.api.key);
|
|
13
|
+
await client.init();
|
|
14
|
+
const sessions = await client.listEnvironmentSessions(input.uuid, {
|
|
15
|
+
...(input.username ? { username: input.username } : {}),
|
|
16
|
+
...(input.credentialId ? { credentialId: input.credentialId } : {}),
|
|
17
|
+
});
|
|
18
|
+
// usableCount, not just the rows: "does this environment currently hold a
|
|
19
|
+
// session that will be restored?" is the question a caller is actually
|
|
20
|
+
// asking, and an expired row still reports status 'valid'.
|
|
21
|
+
const usableCount = sessions.filter(s => s.isUsable).length;
|
|
22
|
+
return ok({
|
|
23
|
+
environmentUuid: input.uuid,
|
|
24
|
+
sessions,
|
|
25
|
+
pageInfo: { totalCount: sessions.length, usableCount },
|
|
26
|
+
note: sessions.length === 0
|
|
27
|
+
? 'No captured sessions — every run for this environment logs in for real.'
|
|
28
|
+
: `${usableCount} of ${sessions.length} session(s) would be restored instead of logging in. `
|
|
29
|
+
+ 'Use action "clearSessions" to force a real login, or pass freshSession:true on a single run.',
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
throw handleExternalServiceError(error, 'DebuggAI', 'environment.sessions');
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export async function clearEnvironmentSessionsHandler(input, _context) {
|
|
37
|
+
logger.toolStart('environment.clearSessions', { uuid: input.uuid, username: input.username });
|
|
38
|
+
try {
|
|
39
|
+
const client = new DebuggAIServerClient(config.api.key);
|
|
40
|
+
await client.init();
|
|
41
|
+
const filters = {
|
|
42
|
+
...(input.username ? { username: input.username } : {}),
|
|
43
|
+
...(input.credentialId ? { credentialId: input.credentialId } : {}),
|
|
44
|
+
};
|
|
45
|
+
const { invalidated } = await client.clearEnvironmentSessions(input.uuid, filters);
|
|
46
|
+
const scope = input.username ?? input.credentialId ?? 'all accounts';
|
|
47
|
+
logger.info(`environment.clearSessions: invalidated ${invalidated} session(s) for ${scope}`);
|
|
48
|
+
return ok({
|
|
49
|
+
environmentUuid: input.uuid,
|
|
50
|
+
invalidated,
|
|
51
|
+
scope,
|
|
52
|
+
note: invalidated === 0
|
|
53
|
+
? 'Nothing to clear — no usable captured session matched.'
|
|
54
|
+
: 'The next run for this identity will perform a real login and re-capture.',
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
throw handleExternalServiceError(error, 'DebuggAI', 'environment.clearSessions');
|
|
59
|
+
}
|
|
60
|
+
}
|