Skip to content

Commit 4e2ced9

Browse files
committed
generalize template URL import injection
1 parent e36eb0b commit 4e2ced9

3 files changed

Lines changed: 79 additions & 1 deletion

File tree

packages/mcp-server/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,9 @@ the request body stays small and no extra MCP/LLM token budget is consumed.
9595
and avoids large inline payloads (the transfer happens out-of-band).
9696
- If instructions already contain an `/http/import` step, the MCP server sets/overrides its `url`.
9797
- If multiple URLs and a single `/http/import` step exists, it supplies a `url` array.
98+
- Templates (builtin or custom) should expose an `imported` step that the template uses as input.
99+
When you pass `template_id` (or `builtin_template`) and URL inputs, the MCP server injects an
100+
`/http/import` step named `imported` and fills its `url` value(s).
98101
99102
## Local vs hosted file access
100103

packages/mcp-server/src/server.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,12 @@ const isNonEmptyString = (value: unknown): value is string =>
334334
const isErrnoException = (value: unknown): value is NodeJS.ErrnoException =>
335335
isRecord(value) && typeof value.code === 'string'
336336

337+
const isHttpImportStep = (value: unknown): value is Record<string, unknown> =>
338+
isRecord(value) && value.robot === '/http/import'
339+
340+
const hasHttpImportStep = (steps: Record<string, unknown>): boolean =>
341+
Object.values(steps).some((step) => isHttpImportStep(step))
342+
337343
const getAssemblyIdFromUrl = (assemblyUrl: string): string => {
338344
const match = assemblyUrl.match(/\/assemblies\/([^/?#]+)/)
339345
if (!match) {
@@ -526,17 +532,45 @@ export const createTransloaditMcpServer = (
526532
if ('error' in liveClient) return liveClient.error
527533
const { client } = liveClient
528534
const tempCleanups: Array<() => Promise<void>> = []
535+
const warnings: Array<{ code: string; message: string; hint?: string; path?: string }> = []
536+
let templatePathHint: string | undefined
529537

530538
try {
531539
const fileInputs = files ?? []
540+
const hasUrlInputs = fileInputs.some((file) => file.kind === 'url')
532541
let params = parseInstructions(instructions) ?? ({} as CreateAssemblyParams)
533542

534543
if (builtin_template) {
535544
const templateId = buildBuiltinTemplateId(builtin_template.slug, builtin_template.version)
536545
const overrides = builtin_template.overrides
537-
params = (overrides && isRecord(overrides) ? overrides : {}) as CreateAssemblyParams
546+
const baseParams = (
547+
overrides && isRecord(overrides) ? { ...overrides } : {}
548+
) as CreateAssemblyParams
549+
550+
templatePathHint = 'builtin_template.slug'
551+
params = baseParams
538552
params.template_id = templateId
539553
}
554+
555+
if (hasUrlInputs && params.template_id) {
556+
const steps = isRecord(params.steps) ? { ...params.steps } : {}
557+
if (!hasHttpImportStep(steps)) {
558+
const imported = isRecord(steps.imported)
559+
? (steps.imported as Record<string, unknown>)
560+
: {}
561+
steps.imported = {
562+
...imported,
563+
robot: '/http/import',
564+
}
565+
params.steps = steps
566+
warnings.push({
567+
code: 'mcp_imported_step',
568+
message:
569+
'URL inputs with template_id require a step named "imported" that the template uses as input. The MCP server injected /http/import into that step.',
570+
path: templatePathHint ?? 'instructions.template_id',
571+
})
572+
}
573+
}
540574
const prep = await prepareInputFiles({
541575
inputFiles: fileInputs,
542576
params,
@@ -634,6 +668,7 @@ export const createTransloaditMcpServer = (
634668
assembly,
635669
upload: uploadSummary,
636670
next_steps: nextSteps,
671+
warnings: warnings.length > 0 ? warnings : undefined,
637672
})
638673
} finally {
639674
await Promise.all(tempCleanups.map((cleanup) => cleanup()))
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import type { Client } from '@modelcontextprotocol/sdk/client'
2+
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
3+
import { createMcpClient, parseToolPayload } from './mcp-client.ts'
4+
5+
describe('mcp-server builtin template imports', { timeout: 20000 }, () => {
6+
let client: Client
7+
8+
beforeAll(async () => {
9+
client = await createMcpClient()
10+
})
11+
12+
afterAll(async () => {
13+
await client?.close()
14+
})
15+
16+
it('injects an imported /http/import step for URL inputs', async () => {
17+
const result = await client.callTool({
18+
name: 'transloadit_create_assembly',
19+
arguments: {
20+
builtin_template: { slug: 'builtin/encode-hls-video' },
21+
files: [
22+
{
23+
kind: 'url',
24+
field: 'video',
25+
url: 'https://demos.transloadit.com/66/01604e7d0248109df8c7cc0f8daef8/snowflake.jpg',
26+
},
27+
],
28+
wait_for_completion: false,
29+
},
30+
})
31+
32+
const payload = parseToolPayload(result)
33+
expect(payload.status).toBe('ok')
34+
35+
const warnings = Array.isArray(payload.warnings) ? payload.warnings : []
36+
expect(warnings.length).toBeGreaterThan(0)
37+
const imported = warnings.find((warning) => warning.code === 'mcp_imported_step')
38+
expect(imported).toBeTruthy()
39+
})
40+
})

0 commit comments

Comments
 (0)