@debugg-ai/debugg-ai-mcp 3.2.0 → 3.4.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 +33 -0
- package/README.md +18 -0
- package/dist/handlers/resourcesHandler.js +127 -0
- package/dist/handlers/searchExecutionsHandler.js +20 -5
- package/dist/handlers/testPageChangesHandler.js +21 -6
- package/dist/index.js +27 -1
- package/dist/utils/imageUtils.js +57 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,39 @@ 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.4.0]
|
|
9
|
+
|
|
10
|
+
### Added — MCP Resources (browse projects / environments / executions)
|
|
11
|
+
|
|
12
|
+
The server now declares the `resources` capability and exposes the read-only
|
|
13
|
+
entities as addressable resources, so clients can browse and @-mention them as
|
|
14
|
+
context instead of only calling tools:
|
|
15
|
+
|
|
16
|
+
- **Collections** (`resources/list`): `debugg-ai://projects`, `debugg-ai://environments`, `debugg-ai://executions`
|
|
17
|
+
- **Templates** (`resources/templates/list`): `debugg-ai://project/{uuid}`, `debugg-ai://environment/{uuid}`, `debugg-ai://execution/{uuid}`
|
|
18
|
+
- **`resources/read`** dispatches each URI to the same entity handler the tools
|
|
19
|
+
use — identical data + auth, no drift — and returns the JSON payload.
|
|
20
|
+
|
|
21
|
+
Additive: clients without resource support keep using the tools unchanged.
|
|
22
|
+
Implementation in `handlers/resourcesHandler.ts`.
|
|
23
|
+
|
|
24
|
+
## [3.3.0]
|
|
25
|
+
|
|
26
|
+
### Added — Run artifacts returned as resource links
|
|
27
|
+
|
|
28
|
+
`check_app_in_browser` and `executions {action:"get"}` now surface execution
|
|
29
|
+
artifacts — **run recording, HAR, console log** — as MCP
|
|
30
|
+
[`resource_link`](https://modelcontextprotocol.io/specification/2025-06-18/server/tools)
|
|
31
|
+
content blocks pointing at their presigned URLs, instead of base64-inlining them.
|
|
32
|
+
Leaner responses, and the URLs stay renewable / fetchable on demand. The legacy
|
|
33
|
+
run-recording GIF (previously downloaded and inlined as multi-MB base64) is now a
|
|
34
|
+
link; the `browserSession` presigned URLs are auto-detected and linked
|
|
35
|
+
(deduped).
|
|
36
|
+
|
|
37
|
+
Screenshots are **deliberately kept inline** as image blocks so vision-capable
|
|
38
|
+
clients can still see them — the core visual-verification workflow. Helpers:
|
|
39
|
+
`resourceLinkBlock` + `artifactResourceLinks` in `utils/imageUtils.ts`.
|
|
40
|
+
|
|
8
41
|
## [3.2.0]
|
|
9
42
|
|
|
10
43
|
### Added — Structured tool output (`structuredContent`)
|
package/README.md
CHANGED
|
@@ -158,6 +158,24 @@ Every filter-mode response is paginated. Response shape:
|
|
|
158
158
|
|
|
159
159
|
Pass optional `page` (1-indexed, default 1) and `pageSize` (default 20, max 200; oversized values are clamped). No response is ever silently truncated.
|
|
160
160
|
|
|
161
|
+
## Resources
|
|
162
|
+
|
|
163
|
+
Alongside tools, the server exposes the read-only entities as MCP **resources**
|
|
164
|
+
so clients can browse and @-mention them as context:
|
|
165
|
+
|
|
166
|
+
| URI | What |
|
|
167
|
+
|---|---|
|
|
168
|
+
| `debugg-ai://projects` | All projects (first page) |
|
|
169
|
+
| `debugg-ai://environments` | Environments for the auto-detected project |
|
|
170
|
+
| `debugg-ai://executions` | Recent executions (first page) |
|
|
171
|
+
| `debugg-ai://project/{uuid}` | One project, full detail |
|
|
172
|
+
| `debugg-ai://environment/{uuid}` | One environment (credentials inline, passwords redacted) |
|
|
173
|
+
| `debugg-ai://execution/{uuid}` | One execution, full node detail + artifact links |
|
|
174
|
+
|
|
175
|
+
Reads dispatch to the same handlers as the `project` / `environment` /
|
|
176
|
+
`executions` tools, so the data and auth are identical. Resources are additive —
|
|
177
|
+
clients without resource support keep using the tools.
|
|
178
|
+
|
|
161
179
|
### Security invariants
|
|
162
180
|
|
|
163
181
|
- Passwords are write-only. They never appear in any response body from any tool.
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Resources (epic pglam).
|
|
3
|
+
*
|
|
4
|
+
* Exposes the read-only entities (projects / environments / executions) as
|
|
5
|
+
* addressable resources so clients can browse and @-mention them as context
|
|
6
|
+
* instead of (or alongside) calling the `project`/`environment`/`executions`
|
|
7
|
+
* tools. Reads reuse the exact tool handlers — same data, same auth, no drift.
|
|
8
|
+
*
|
|
9
|
+
* Collections (resources/list): debugg-ai://projects | environments | executions
|
|
10
|
+
* Items (resources/templates/list): debugg-ai://project/{uuid}
|
|
11
|
+
* debugg-ai://environment/{uuid}
|
|
12
|
+
* debugg-ai://execution/{uuid}
|
|
13
|
+
*
|
|
14
|
+
* Resources are additive: clients that don't support the capability simply
|
|
15
|
+
* keep using the tools.
|
|
16
|
+
*/
|
|
17
|
+
import { config } from '../config/index.js';
|
|
18
|
+
import { MCPError, MCPErrorCode, } from '../types/index.js';
|
|
19
|
+
import { projectHandler } from './projectHandler.js';
|
|
20
|
+
import { environmentHandler } from './environmentHandler.js';
|
|
21
|
+
import { executionsHandler } from './executionsHandler.js';
|
|
22
|
+
const SCHEME = 'debugg-ai';
|
|
23
|
+
const JSON_MIME = 'application/json';
|
|
24
|
+
/** Concrete collection resources returned by resources/list. */
|
|
25
|
+
export const RESOURCE_COLLECTIONS = [
|
|
26
|
+
{
|
|
27
|
+
uri: `${SCHEME}://projects`,
|
|
28
|
+
name: 'projects',
|
|
29
|
+
title: 'DebuggAI Projects',
|
|
30
|
+
description: 'All projects visible to this API key (first page).',
|
|
31
|
+
mimeType: JSON_MIME,
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
uri: `${SCHEME}://environments`,
|
|
35
|
+
name: 'environments',
|
|
36
|
+
title: 'DebuggAI Environments',
|
|
37
|
+
description: 'Environments for the auto-detected project (credentials redacted).',
|
|
38
|
+
mimeType: JSON_MIME,
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
uri: `${SCHEME}://executions`,
|
|
42
|
+
name: 'executions',
|
|
43
|
+
title: 'DebuggAI Executions',
|
|
44
|
+
description: 'Recent workflow executions (first page).',
|
|
45
|
+
mimeType: JSON_MIME,
|
|
46
|
+
},
|
|
47
|
+
];
|
|
48
|
+
/** URI templates returned by resources/templates/list. */
|
|
49
|
+
export const RESOURCE_TEMPLATES = [
|
|
50
|
+
{
|
|
51
|
+
uriTemplate: `${SCHEME}://project/{uuid}`,
|
|
52
|
+
name: 'project',
|
|
53
|
+
title: 'DebuggAI Project',
|
|
54
|
+
description: 'A single project by UUID, with full detail.',
|
|
55
|
+
mimeType: JSON_MIME,
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
uriTemplate: `${SCHEME}://environment/{uuid}`,
|
|
59
|
+
name: 'environment',
|
|
60
|
+
title: 'DebuggAI Environment',
|
|
61
|
+
description: 'A single environment by UUID, with credentials inline (passwords redacted).',
|
|
62
|
+
mimeType: JSON_MIME,
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
uriTemplate: `${SCHEME}://execution/{uuid}`,
|
|
66
|
+
name: 'execution',
|
|
67
|
+
title: 'DebuggAI Execution',
|
|
68
|
+
description: 'A single execution by UUID, with full node detail + artifact links.',
|
|
69
|
+
mimeType: JSON_MIME,
|
|
70
|
+
},
|
|
71
|
+
];
|
|
72
|
+
function readContext() {
|
|
73
|
+
return { timestamp: new Date() };
|
|
74
|
+
}
|
|
75
|
+
/** Pull the JSON payload that every entity handler emits as its single text block. */
|
|
76
|
+
function payloadText(res) {
|
|
77
|
+
const text = res.content?.find((c) => c.type === 'text')?.text;
|
|
78
|
+
return typeof text === 'string' ? text : '{}';
|
|
79
|
+
}
|
|
80
|
+
// debugg-ai://<kind> (collection)
|
|
81
|
+
// debugg-ai://<kind>/<uuid> (item)
|
|
82
|
+
const URI_RE = new RegExp(`^${SCHEME}://([a-z]+)(?:/([^/?#]+))?$`);
|
|
83
|
+
/**
|
|
84
|
+
* Resolve a debugg-ai:// resource URI by dispatching to the matching tool
|
|
85
|
+
* handler and wrapping its JSON payload as a resource content block.
|
|
86
|
+
*/
|
|
87
|
+
export async function readResource(uri) {
|
|
88
|
+
if (!config.api.key) {
|
|
89
|
+
throw new MCPError(MCPErrorCode.CONFIGURATION_ERROR, 'DEBUGGAI_API_KEY is not set. Configure it in your MCP server registration. Get a key at https://debugg.ai.', { missingEnvVars: ['DEBUGGAI_API_KEY'] });
|
|
90
|
+
}
|
|
91
|
+
const match = URI_RE.exec(uri);
|
|
92
|
+
if (!match) {
|
|
93
|
+
throw new MCPError(MCPErrorCode.INVALID_PARAMS, `Unrecognized resource URI: ${uri}`);
|
|
94
|
+
}
|
|
95
|
+
const [, kind, id] = match;
|
|
96
|
+
const ctx = readContext();
|
|
97
|
+
const requireId = () => {
|
|
98
|
+
if (!id) {
|
|
99
|
+
throw new MCPError(MCPErrorCode.INVALID_PARAMS, `Resource ${uri} requires a UUID: ${SCHEME}://${kind}/{uuid}`);
|
|
100
|
+
}
|
|
101
|
+
return id;
|
|
102
|
+
};
|
|
103
|
+
let res;
|
|
104
|
+
switch (kind) {
|
|
105
|
+
case 'projects':
|
|
106
|
+
res = await projectHandler({ action: 'list' }, ctx);
|
|
107
|
+
break;
|
|
108
|
+
case 'project':
|
|
109
|
+
res = await projectHandler({ action: 'get', uuid: requireId() }, ctx);
|
|
110
|
+
break;
|
|
111
|
+
case 'environments':
|
|
112
|
+
res = await environmentHandler({ action: 'list' }, ctx);
|
|
113
|
+
break;
|
|
114
|
+
case 'environment':
|
|
115
|
+
res = await environmentHandler({ action: 'get', uuid: requireId() }, ctx);
|
|
116
|
+
break;
|
|
117
|
+
case 'executions':
|
|
118
|
+
res = await executionsHandler({ action: 'list' }, ctx);
|
|
119
|
+
break;
|
|
120
|
+
case 'execution':
|
|
121
|
+
res = await executionsHandler({ action: 'get', uuid: requireId() }, ctx);
|
|
122
|
+
break;
|
|
123
|
+
default:
|
|
124
|
+
throw new MCPError(MCPErrorCode.INVALID_PARAMS, `Unknown resource kind "${kind}" in ${uri}`);
|
|
125
|
+
}
|
|
126
|
+
return { contents: [{ uri, mimeType: JSON_MIME, text: payloadText(res) }] };
|
|
127
|
+
}
|
|
@@ -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
|
@@ -18,9 +18,10 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
20
20
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
21
|
-
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
21
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ReadResourceRequestSchema, } 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 { readResource, RESOURCE_COLLECTIONS, RESOURCE_TEMPLATES } from "./handlers/resourcesHandler.js";
|
|
24
25
|
import { Logger, validateInput, createErrorResponse, toMCPError, handleConfigurationError, Telemetry, TelemetryEvents, withStructuredContent, } from "./utils/index.js";
|
|
25
26
|
import { MCPErrorCode, MCPError, } from "./types/index.js";
|
|
26
27
|
// Logger and server are initialized lazily in main() to avoid triggering
|
|
@@ -39,6 +40,9 @@ function createMCPServer() {
|
|
|
39
40
|
tools: {
|
|
40
41
|
listChanged: false,
|
|
41
42
|
},
|
|
43
|
+
resources: {
|
|
44
|
+
listChanged: false,
|
|
45
|
+
},
|
|
42
46
|
},
|
|
43
47
|
});
|
|
44
48
|
}
|
|
@@ -135,6 +139,28 @@ function registerHandlers() {
|
|
|
135
139
|
logger.info('Tools list requested', { toolCount: tools.length });
|
|
136
140
|
return { tools };
|
|
137
141
|
});
|
|
142
|
+
// Resources (epic pglam): browse projects/environments/executions as
|
|
143
|
+
// addressable read URIs. Reads dispatch to the same entity handlers as the
|
|
144
|
+
// tools, so data + auth stay consistent.
|
|
145
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
146
|
+
logger.info('Resources list requested', { resourceCount: RESOURCE_COLLECTIONS.length });
|
|
147
|
+
return { resources: RESOURCE_COLLECTIONS };
|
|
148
|
+
});
|
|
149
|
+
server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => {
|
|
150
|
+
return { resourceTemplates: RESOURCE_TEMPLATES };
|
|
151
|
+
});
|
|
152
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (req) => {
|
|
153
|
+
const uri = req.params?.uri;
|
|
154
|
+
logger.info('Resource read requested', { uri });
|
|
155
|
+
try {
|
|
156
|
+
return await readResource(uri);
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
const mcpError = toMCPError(error, 'resource read');
|
|
160
|
+
logger.error('Resource read failed', { uri, errorCode: mcpError.code, message: mcpError.message });
|
|
161
|
+
throw mcpError;
|
|
162
|
+
}
|
|
163
|
+
});
|
|
138
164
|
}
|
|
139
165
|
/**
|
|
140
166
|
* Main server initialization and startup
|
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
|
+
}
|