@debugg-ai/debugg-ai-mcp 3.1.0 → 3.3.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 +34 -0
- package/dist/handlers/searchExecutionsHandler.js +20 -5
- package/dist/handlers/testPageChangesHandler.js +21 -6
- package/dist/index.js +3 -2
- package/dist/utils/imageUtils.js +57 -0
- package/dist/utils/index.js +1 -0
- package/dist/utils/structuredContent.js +42 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,40 @@ 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
|
+
## [3.3.0]
|
|
9
|
+
|
|
10
|
+
### Added — Run artifacts returned as resource links
|
|
11
|
+
|
|
12
|
+
`check_app_in_browser` and `executions {action:"get"}` now surface execution
|
|
13
|
+
artifacts — **run recording, HAR, console log** — as MCP
|
|
14
|
+
[`resource_link`](https://modelcontextprotocol.io/specification/2025-06-18/server/tools)
|
|
15
|
+
content blocks pointing at their presigned URLs, instead of base64-inlining them.
|
|
16
|
+
Leaner responses, and the URLs stay renewable / fetchable on demand. The legacy
|
|
17
|
+
run-recording GIF (previously downloaded and inlined as multi-MB base64) is now a
|
|
18
|
+
link; the `browserSession` presigned URLs are auto-detected and linked
|
|
19
|
+
(deduped).
|
|
20
|
+
|
|
21
|
+
Screenshots are **deliberately kept inline** as image blocks so vision-capable
|
|
22
|
+
clients can still see them — the core visual-verification workflow. Helpers:
|
|
23
|
+
`resourceLinkBlock` + `artifactResourceLinks` in `utils/imageUtils.ts`.
|
|
24
|
+
|
|
25
|
+
## [3.2.0]
|
|
26
|
+
|
|
27
|
+
### Added — Structured tool output (`structuredContent`)
|
|
28
|
+
|
|
29
|
+
Every successful tool result now carries [`structuredContent`](https://modelcontextprotocol.io/specification/2025-06-18/server/tools)
|
|
30
|
+
— the parsed JSON payload — so clients can consume structured data directly
|
|
31
|
+
instead of re-parsing the text blob. The text block is kept for back-compat.
|
|
32
|
+
|
|
33
|
+
Promoted centrally in the CallTool path (`withStructuredContent` in
|
|
34
|
+
`utils/structuredContent.ts`) rather than touching every handler. No-op for
|
|
35
|
+
errors, non-object payloads, or multi-text results.
|
|
36
|
+
|
|
37
|
+
`outputSchema` is intentionally not declared: the action tools return
|
|
38
|
+
polymorphic shapes per action, a faithful schema would need top-level `oneOf`
|
|
39
|
+
(which the Anthropic API rejects), and a permissive schema adds no value.
|
|
40
|
+
`structuredContent` without a declared schema is spec-valid and is the win.
|
|
41
|
+
|
|
8
42
|
## [3.1.0]
|
|
9
43
|
|
|
10
44
|
### Added — Tool annotations (behavioral hints for clients)
|
|
@@ -13,7 +13,7 @@ import { handleExternalServiceError } from '../utils/errors.js';
|
|
|
13
13
|
import { DebuggAIServerClient } from '../services/index.js';
|
|
14
14
|
import { config } from '../config/index.js';
|
|
15
15
|
import { toPaginationParams } from '../utils/pagination.js';
|
|
16
|
-
import { fetchImageAsBase64, imageContentBlock } from '../utils/imageUtils.js';
|
|
16
|
+
import { fetchImageAsBase64, imageContentBlock, resourceLinkBlock, artifactResourceLinks } from '../utils/imageUtils.js';
|
|
17
17
|
const logger = new Logger({ module: 'searchExecutionsHandler' });
|
|
18
18
|
function notFound(uuid) {
|
|
19
19
|
return {
|
|
@@ -80,10 +80,25 @@ export async function searchExecutionsHandler(input, _context) {
|
|
|
80
80
|
if (img)
|
|
81
81
|
content.push(imageContentBlock(img.data, img.mimeType));
|
|
82
82
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
83
|
+
// Artifact links (bead 8qndk): run recording (legacy GIF field) + the
|
|
84
|
+
// browserSession presigned URLs (HAR / console log / recording). Linked,
|
|
85
|
+
// not base64-inlined. Screenshot stays inline above for vision.
|
|
86
|
+
const artifactLinks = [
|
|
87
|
+
...(gifUrl
|
|
88
|
+
? [resourceLinkBlock(gifUrl, `run-recording-${input.uuid}.gif`, {
|
|
89
|
+
mimeType: 'image/gif',
|
|
90
|
+
title: 'Run recording',
|
|
91
|
+
description: 'Animated recording of the execution (presigned URL — open or fetch on demand).',
|
|
92
|
+
})]
|
|
93
|
+
: []),
|
|
94
|
+
...artifactResourceLinks(execution.browserSession),
|
|
95
|
+
];
|
|
96
|
+
const seenArtifactUris = new Set();
|
|
97
|
+
for (const link of artifactLinks) {
|
|
98
|
+
if (link.uri && !seenArtifactUris.has(link.uri)) {
|
|
99
|
+
seenArtifactUris.add(link.uri);
|
|
100
|
+
content.push(link);
|
|
101
|
+
}
|
|
87
102
|
}
|
|
88
103
|
return { content };
|
|
89
104
|
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { config } from '../config/index.js';
|
|
7
7
|
import { Logger } from '../utils/logger.js';
|
|
8
8
|
import { handleExternalServiceError } from '../utils/errors.js';
|
|
9
|
-
import { fetchImageAsBase64, imageContentBlock } from '../utils/imageUtils.js';
|
|
9
|
+
import { fetchImageAsBase64, imageContentBlock, resourceLinkBlock, artifactResourceLinks } from '../utils/imageUtils.js';
|
|
10
10
|
import { DebuggAIServerClient } from '../services/index.js';
|
|
11
11
|
import { TunnelProvisionError } from '../services/tunnels.js';
|
|
12
12
|
import { resolveTargetUrl, buildContext, findExistingTunnel, ensureTunnel, sanitizeResponseUrls, touchTunnelById, } from '../utils/tunnelContext.js';
|
|
@@ -539,11 +539,26 @@ async function testPageChangesHandlerInner(input, context, rawProgressCallback)
|
|
|
539
539
|
if (img)
|
|
540
540
|
content.push(imageContentBlock(img.data, img.mimeType));
|
|
541
541
|
}
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
542
|
+
// Artifact links (bead 8qndk): run recording (legacy GIF field) + the
|
|
543
|
+
// browserSession presigned URLs (HAR / console log / recording). Returned as
|
|
544
|
+
// resource_links, not base64-inlined. Screenshots stay inline above so
|
|
545
|
+
// vision-capable clients still SEE them.
|
|
546
|
+
const artifactLinks = [
|
|
547
|
+
...(gifUrl
|
|
548
|
+
? [resourceLinkBlock(gifUrl, 'run-recording.gif', {
|
|
549
|
+
mimeType: 'image/gif',
|
|
550
|
+
title: 'Run recording',
|
|
551
|
+
description: 'Animated recording of the run (presigned URL — open or fetch on demand).',
|
|
552
|
+
})]
|
|
553
|
+
: []),
|
|
554
|
+
...artifactResourceLinks(sanitizedPayload.browserSession),
|
|
555
|
+
];
|
|
556
|
+
const seenArtifactUris = new Set();
|
|
557
|
+
for (const link of artifactLinks) {
|
|
558
|
+
if (link.uri && !seenArtifactUris.has(link.uri)) {
|
|
559
|
+
seenArtifactUris.add(link.uri);
|
|
560
|
+
content.push(link);
|
|
561
|
+
}
|
|
547
562
|
}
|
|
548
563
|
return { content };
|
|
549
564
|
}
|
package/dist/index.js
CHANGED
|
@@ -21,7 +21,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
21
21
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
22
22
|
import { config } from "./config/index.js";
|
|
23
23
|
import { initTools, getTools, getTool } from "./tools/index.js";
|
|
24
|
-
import { Logger, validateInput, createErrorResponse, toMCPError, handleConfigurationError, Telemetry, TelemetryEvents, } from "./utils/index.js";
|
|
24
|
+
import { Logger, validateInput, createErrorResponse, toMCPError, handleConfigurationError, Telemetry, TelemetryEvents, withStructuredContent, } from "./utils/index.js";
|
|
25
25
|
import { MCPErrorCode, MCPError, } from "./types/index.js";
|
|
26
26
|
// Logger and server are initialized lazily in main() to avoid triggering
|
|
27
27
|
// config loading at module load time. If config validation fails (bad env vars),
|
|
@@ -116,7 +116,8 @@ function registerHandlers() {
|
|
|
116
116
|
const toolDuration = Date.now() - toolStart;
|
|
117
117
|
requestLogger.info(`Tool execution completed: ${name}`);
|
|
118
118
|
Telemetry.capture(TelemetryEvents.TOOL_EXECUTED, { toolName: name, durationMs: toolDuration, success: true });
|
|
119
|
-
|
|
119
|
+
// Promote the JSON text payload to structuredContent (back-compat: text stays).
|
|
120
|
+
return withStructuredContent(result);
|
|
120
121
|
}
|
|
121
122
|
catch (error) {
|
|
122
123
|
const mcpError = toMCPError(error, 'tool execution');
|
package/dist/utils/imageUtils.js
CHANGED
|
@@ -39,3 +39,60 @@ function inferMimeFromUrl(url) {
|
|
|
39
39
|
export function imageContentBlock(data, mimeType) {
|
|
40
40
|
return { type: 'image', data, mimeType };
|
|
41
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* Build an MCP resource_link content block (MCP 2025-06-18) pointing at an
|
|
44
|
+
* (often presigned) artifact URL — leaner than inlining the bytes, and the URL
|
|
45
|
+
* stays renewable/on-demand. Use for large non-vision artifacts (run-recording
|
|
46
|
+
* GIFs, HAR, console logs) rather than base64-embedding them.
|
|
47
|
+
*/
|
|
48
|
+
export function resourceLinkBlock(uri, name, opts = {}) {
|
|
49
|
+
const block = { type: 'resource_link', uri, name };
|
|
50
|
+
if (opts.mimeType)
|
|
51
|
+
block.mimeType = opts.mimeType;
|
|
52
|
+
if (opts.title)
|
|
53
|
+
block.title = opts.title;
|
|
54
|
+
if (opts.description)
|
|
55
|
+
block.description = opts.description;
|
|
56
|
+
return block;
|
|
57
|
+
}
|
|
58
|
+
const MIME_BY_EXT = {
|
|
59
|
+
gif: 'image/gif', png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg',
|
|
60
|
+
webp: 'image/webp', mp4: 'video/mp4', webm: 'video/webm',
|
|
61
|
+
har: 'application/json', json: 'application/json', txt: 'text/plain', log: 'text/plain',
|
|
62
|
+
};
|
|
63
|
+
function titleize(key) {
|
|
64
|
+
return key
|
|
65
|
+
.replace(/[_-]+/g, ' ')
|
|
66
|
+
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
|
67
|
+
.replace(/\bUrl\b|\bUri\b/gi, '')
|
|
68
|
+
.replace(/\s+/g, ' ')
|
|
69
|
+
.trim()
|
|
70
|
+
.replace(/^\w/, (c) => c.toUpperCase());
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Build resource_link blocks for every (presigned) artifact URL found one level
|
|
74
|
+
* deep in `source` (e.g. an execution's browserSession: HAR, console log, run
|
|
75
|
+
* recording). Defensive about exact field names — it links any https value and
|
|
76
|
+
* skips tunnel/ngrok hosts. Returns [] for nullish/empty input.
|
|
77
|
+
*/
|
|
78
|
+
export function artifactResourceLinks(source) {
|
|
79
|
+
if (!source || typeof source !== 'object')
|
|
80
|
+
return [];
|
|
81
|
+
const out = [];
|
|
82
|
+
for (const [key, value] of Object.entries(source)) {
|
|
83
|
+
if (typeof value !== 'string')
|
|
84
|
+
continue;
|
|
85
|
+
if (!/^https?:\/\//i.test(value))
|
|
86
|
+
continue;
|
|
87
|
+
if (/ngrok|tunnel/i.test(value))
|
|
88
|
+
continue;
|
|
89
|
+
const ext = (value.split('?')[0].match(/\.([a-z0-9]+)$/i)?.[1] ?? '').toLowerCase();
|
|
90
|
+
const name = ext ? `${key}.${ext}` : key;
|
|
91
|
+
out.push(resourceLinkBlock(value, name, {
|
|
92
|
+
mimeType: MIME_BY_EXT[ext],
|
|
93
|
+
title: titleize(key),
|
|
94
|
+
description: 'Execution artifact (presigned URL — open or fetch on demand).',
|
|
95
|
+
}));
|
|
96
|
+
}
|
|
97
|
+
return out;
|
|
98
|
+
}
|
package/dist/utils/index.js
CHANGED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured tool output (epic 3eb5l).
|
|
3
|
+
*
|
|
4
|
+
* MCP 2025-06-18 added `structuredContent` on tool results: a machine-readable
|
|
5
|
+
* JSON object that mirrors the human-readable text block. Clients that support
|
|
6
|
+
* it consume parsed data directly instead of re-parsing the text blob; the text
|
|
7
|
+
* block stays for back-compat.
|
|
8
|
+
*
|
|
9
|
+
* Every leaf handler already returns its payload as `JSON.stringify(payload)` in
|
|
10
|
+
* a single text item, so rather than touch ~20 handlers we promote it in ONE
|
|
11
|
+
* place — the CallTool path in index.ts wraps each result with this helper.
|
|
12
|
+
*
|
|
13
|
+
* We intentionally do NOT declare `outputSchema` on the tools: the action tools
|
|
14
|
+
* return polymorphic shapes per action, a faithful schema would need top-level
|
|
15
|
+
* `oneOf` (which the Anthropic API rejects, same as input schemas), and a
|
|
16
|
+
* permissive `type:object` schema adds no value. `structuredContent` without a
|
|
17
|
+
* declared schema is spec-valid and is the actual win.
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* Attach `structuredContent` to a successful tool result when its single text
|
|
21
|
+
* block is a JSON object. No-op for errors, multi-text results, non-object
|
|
22
|
+
* payloads, or results that already set structuredContent.
|
|
23
|
+
*/
|
|
24
|
+
export function withStructuredContent(result) {
|
|
25
|
+
if (!result || result.isError || result.structuredContent)
|
|
26
|
+
return result;
|
|
27
|
+
const textItems = (result.content || []).filter((c) => c.type === 'text' && typeof c.text === 'string');
|
|
28
|
+
if (textItems.length !== 1)
|
|
29
|
+
return result;
|
|
30
|
+
let parsed;
|
|
31
|
+
try {
|
|
32
|
+
parsed = JSON.parse(textItems[0].text);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return result; // not JSON (shouldn't happen for our handlers) — leave as-is
|
|
36
|
+
}
|
|
37
|
+
// Spec requires structuredContent to be a JSON object (not array/primitive/null).
|
|
38
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
return { ...result, structuredContent: parsed };
|
|
42
|
+
}
|