Skip to content

Commit bb67dcf

Browse files
committed
feat: return empty array for resources/list, add asset TTL configuration and cleanup logic to manage ephemeral assets
1 parent 352f82d commit bb67dcf

File tree

5 files changed

+61
-13
lines changed

5 files changed

+61
-13
lines changed

packages/mcp-server/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,18 @@ Supported tools/resources:
2626
- `get_screenshot`: PNG capture with a downloadable asset link.
2727
- `tempad-assets` resource template (`asset://tempad/{hash}`) for binaries referenced by tool responses.
2828

29+
Notes:
30+
31+
- Assets are ephemeral and tool-linked; `resources/list` is intentionally empty to avoid cross-session/design-file pollution. Use `resource_link` blocks from tool results and `resources/read` (or the HTTP fallback URL) to fetch bytes.
32+
2933
## Configuration
3034

3135
Optional environment variables:
3236

3337
- `TEMPAD_MCP_TOOL_TIMEOUT`: Tool call timeout in milliseconds (default `15000`).
3438
- `TEMPAD_MCP_AUTO_ACTIVATE_GRACE`: Delay before auto-activating the sole connected extension (default `1500`).
3539
- `TEMPAD_MCP_MAX_ASSET_BYTES`: Maximum upload size for captured assets/screenshots in bytes (default `8388608`).
40+
- `TEMPAD_MCP_ASSET_TTL_MS`: Asset cleanup TTL in milliseconds based on last access; set `0` to disable (default `2592000000`).
3641
- `TEMPAD_MCP_RUNTIME_DIR`: Override runtime directory (defaults to system temp under `tempad-dev/run`).
3742
- `TEMPAD_MCP_LOG_DIR`: Override log directory (defaults to system temp under `tempad-dev/log`).
3843
- `TEMPAD_MCP_ASSET_DIR`: Override asset storage directory (defaults to system temp under `tempad-dev/assets`).

packages/mcp-server/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@tempad-dev/mcp",
3-
"version": "0.3.9",
3+
"version": "0.3.10",
44
"description": "MCP server for TemPad Dev.",
55
"bin": "dist/cli.mjs",
66
"files": [

packages/mcp-server/src/config.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {
22
MCP_AUTO_ACTIVATE_GRACE_MS,
3+
MCP_ASSET_TTL_MS,
34
MCP_MAX_ASSET_BYTES,
45
MCP_MAX_PAYLOAD_BYTES,
56
MCP_PORT_CANDIDATES,
@@ -11,6 +12,11 @@ function parsePositiveInt(envValue: string | undefined, fallback: number): numbe
1112
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback
1213
}
1314

15+
function parseNonNegativeInt(envValue: string | undefined, fallback: number): number {
16+
const parsed = envValue ? Number.parseInt(envValue, 10) : Number.NaN
17+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback
18+
}
19+
1420
function resolveToolTimeoutMs(): number {
1521
return parsePositiveInt(process.env.TEMPAD_MCP_TOOL_TIMEOUT, MCP_TOOL_TIMEOUT_MS)
1622
}
@@ -23,12 +29,17 @@ function resolveMaxAssetSizeBytes(): number {
2329
return parsePositiveInt(process.env.TEMPAD_MCP_MAX_ASSET_BYTES, MCP_MAX_ASSET_BYTES)
2430
}
2531

32+
function resolveAssetTtlMs(): number {
33+
return parseNonNegativeInt(process.env.TEMPAD_MCP_ASSET_TTL_MS, MCP_ASSET_TTL_MS)
34+
}
35+
2636
export function getMcpServerConfig() {
2737
return {
2838
wsPortCandidates: [...MCP_PORT_CANDIDATES],
2939
toolTimeoutMs: resolveToolTimeoutMs(),
3040
maxPayloadBytes: MCP_MAX_PAYLOAD_BYTES,
3141
autoActivateGraceMs: resolveAutoActivateGraceMs(),
32-
maxAssetSizeBytes: resolveMaxAssetSizeBytes()
42+
maxAssetSizeBytes: resolveMaxAssetSizeBytes(),
43+
assetTtlMs: resolveAssetTtlMs()
3344
}
3445
}

packages/mcp-server/src/hub.ts

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ import { PACKAGE_VERSION, log, RUNTIME_DIR, SOCK_PATH, ensureDir } from './share
3838
import { TOOL_DEFS, coercePayloadToToolResponse, createToolErrorResponse } from './tools'
3939

4040
const SHUTDOWN_TIMEOUT = 2000
41-
const { wsPortCandidates, toolTimeoutMs, maxPayloadBytes, autoActivateGraceMs } =
41+
const { wsPortCandidates, toolTimeoutMs, maxPayloadBytes, autoActivateGraceMs, assetTtlMs } =
4242
getMcpServerConfig()
4343

4444
log.info({ version: PACKAGE_VERSION }, 'TemPad MCP Hub starting...')
@@ -113,27 +113,24 @@ const assetStore = createAssetStore()
113113
const assetHttpServer = createAssetHttpServer(assetStore)
114114
await assetHttpServer.start()
115115
registerAssetResources()
116+
scheduleAssetCleanup()
116117

117118
function registerAssetResources(): void {
118119
const template = new ResourceTemplate(MCP_ASSET_URI_TEMPLATE, {
119120
list: async () => ({
120-
resources: assetStore
121-
.list()
122-
.filter((record) => existsSync(record.filePath))
123-
.map((record) => ({
124-
uri: buildAssetResourceUri(record.hash),
125-
name: formatAssetResourceName(record.hash),
126-
description: `${record.mimeType} (${formatBytes(record.size)})`,
127-
mimeType: record.mimeType
128-
}))
121+
// Intentionally keep resources/list empty: assets are ephemeral, tool-linked blobs.
122+
// Resource discovery would leak across sessions/design files and add UI noise.
123+
// Hosts should use resource_link from tool responses to access assets on demand.
124+
resources: []
129125
})
130126
})
131127

132128
mcp.registerResource(
133129
MCP_ASSET_RESOURCE_NAME,
134130
template,
135131
{
136-
description: 'Binary assets captured by the TemPad Dev hub.'
132+
description:
133+
'Exported PNG/SVG assets which can serve as screenshots or be referenced in code output.'
137134
},
138135
async (_uri, variables) => {
139136
const hash = typeof variables.hash === 'string' ? variables.hash : ''
@@ -142,6 +139,39 @@ function registerAssetResources(): void {
142139
)
143140
}
144141

142+
function scheduleAssetCleanup(): void {
143+
if (assetTtlMs <= 0) {
144+
log.info('Asset TTL cleanup disabled (TEMPAD_MCP_ASSET_TTL_MS=0).')
145+
return
146+
}
147+
pruneExpiredAssets(assetTtlMs)
148+
const intervalMs = Math.min(assetTtlMs, 24 * 60 * 60 * 1000)
149+
const timer = setInterval(() => {
150+
pruneExpiredAssets(assetTtlMs)
151+
}, intervalMs)
152+
unrefTimer(timer)
153+
log.info(
154+
{ ttlMs: assetTtlMs, intervalMs },
155+
'Asset TTL cleanup enabled (list remains empty; assets are tool-linked).'
156+
)
157+
}
158+
159+
function pruneExpiredAssets(ttlMs: number): void {
160+
const now = Date.now()
161+
let removed = 0
162+
let checked = 0
163+
for (const record of assetStore.list()) {
164+
checked += 1
165+
const lastAccess = Number.isFinite(record.lastAccess) ? record.lastAccess : record.uploadedAt
166+
if (!lastAccess) continue
167+
if (now - lastAccess > ttlMs) {
168+
assetStore.remove(record.hash)
169+
removed += 1
170+
}
171+
}
172+
log.info({ checked, removed, ttlMs }, 'Asset TTL sweep completed.')
173+
}
174+
145175
async function readAssetResource(hash: string) {
146176
if (!hash) {
147177
throw new Error('Missing asset hash in resource URI.')

packages/mcp-shared/src/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ export const MCP_AUTO_ACTIVATE_GRACE_MS = 1500
1111

1212
// Maximum allowed size for uploaded assets (bytes).
1313
export const MCP_MAX_ASSET_BYTES = 8 * 1024 * 1024
14+
// Default asset TTL before cleanup (ms). Set to 0 to disable.
15+
export const MCP_ASSET_TTL_MS = 30 * 24 * 60 * 60 * 1000
1416

1517
export const MCP_ASSET_RESOURCE_NAME = 'tempad-assets'
1618
export const MCP_ASSET_URI_PREFIX = 'asset://tempad/'

0 commit comments

Comments
 (0)