Skip to content

Commit 027fcf9

Browse files
committed
fix(mcp-server): align server card with spec
1 parent c4cfaf6 commit 027fcf9

6 files changed

Lines changed: 331 additions & 47 deletions

File tree

.changeset/fix-mcp-server-card.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@transloadit/mcp-server": patch
3+
---
4+
5+
Fix MCP server card schema and add coverage for `/.well-known/mcp/server-card.json`.
6+

packages/mcp-server/src/express.ts

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import type { TransloaditMcpHttpOptions } from './http.ts'
55
import { isBasicAuthorized } from './http-helpers.ts'
66
import { createMcpRequestHandler } from './http-request-handler.ts'
77
import { getMetrics, getMetricsContentType } from './metrics.ts'
8-
import { buildServerCard, serverCardPath } from './server-card.ts'
98
import { createTransloaditMcpServer } from './server.ts'
9+
import { buildServerCard, serverCardPath } from './server-card.ts'
1010

1111
export type TransloaditMcpExpressOptions = TransloaditMcpHttpOptions & {
1212
path?: string
@@ -38,13 +38,43 @@ export const createTransloaditMcpExpressRouter = async (
3838
redactSecrets: [options.mcpToken, options.authKey, options.authSecret],
3939
})
4040

41-
const serverCardJson = JSON.stringify(buildServerCard(routePath))
41+
const serverCardJson = JSON.stringify(
42+
buildServerCard(routePath, { authKey: options.authKey, authSecret: options.authSecret }),
43+
)
4244

43-
router.get(serverCardPath, (_req, res) => {
45+
const sendServerCard = (res: express.Response, includeBody: boolean) => {
46+
res.setHeader('Access-Control-Allow-Origin', '*')
47+
res.setHeader('Access-Control-Allow-Methods', 'GET,HEAD,OPTIONS')
48+
res.setHeader(
49+
'Access-Control-Allow-Headers',
50+
'Authorization,Content-Type,Mcp-Session-Id,Last-Event-ID',
51+
)
52+
res.setHeader('Content-Type', 'application/json; charset=utf-8')
53+
res.setHeader('Cache-Control', 'public, max-age=3600')
54+
res.setHeader('X-Content-Type-Options', 'nosniff')
55+
if (includeBody) {
56+
res.status(200).send(serverCardJson)
57+
return
58+
}
59+
res.status(200).end()
60+
}
61+
62+
router.options(serverCardPath, (_req, res) => {
4463
res.setHeader('Access-Control-Allow-Origin', '*')
4564
res.setHeader('Access-Control-Allow-Methods', 'GET,HEAD,OPTIONS')
46-
res.setHeader('Content-Type', 'application/json')
47-
res.status(200).send(serverCardJson)
65+
res.setHeader(
66+
'Access-Control-Allow-Headers',
67+
'Authorization,Content-Type,Mcp-Session-Id,Last-Event-ID',
68+
)
69+
res.status(204).end()
70+
})
71+
72+
router.get(serverCardPath, (_req, res) => {
73+
sendServerCard(res, true)
74+
})
75+
76+
router.head(serverCardPath, (_req, res) => {
77+
sendServerCard(res, false)
4878
})
4979

5080
router.all(routePath, (req, res) => {

packages/mcp-server/src/http.ts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,17 @@ import { randomUUID } from 'node:crypto'
22
import type { IncomingMessage, ServerResponse } from 'node:http'
33
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'
44
import type { SevLogger } from '@transloadit/sev-logger'
5-
import { isBasicAuthorized, normalizePath, parsePathname } from './http-helpers.ts'
5+
import {
6+
applyCorsHeaders,
7+
isBasicAuthorized,
8+
normalizePath,
9+
parsePathname,
10+
} from './http-helpers.ts'
611
import { createMcpRequestHandler } from './http-request-handler.ts'
712
import { getMetrics, getMetricsContentType } from './metrics.ts'
8-
import { buildServerCard, serverCardPath } from './server-card.ts'
913
import type { TransloaditMcpServerOptions } from './server.ts'
1014
import { createTransloaditMcpServer } from './server.ts'
15+
import { buildServerCard, serverCardPath } from './server-card.ts'
1116

1217
export type TransloaditMcpHttpOptions = TransloaditMcpServerOptions & {
1318
allowedOrigins?: string[]
@@ -56,13 +61,18 @@ export const createTransloaditMcpHttpHandler = async (
5661
redactSecrets: [options.mcpToken, options.authKey, options.authSecret],
5762
})
5863

59-
const serverCardJson = JSON.stringify(buildServerCard(expectedPath))
64+
const serverCardJson = JSON.stringify(
65+
buildServerCard(expectedPath, { authKey: options.authKey, authSecret: options.authSecret }),
66+
)
6067

6168
const handler = (async (req, res) => {
6269
const pathname = normalizePath(parsePathname(req.url, expectedPath))
6370

6471
if (pathname === serverCardPath) {
65-
res.setHeader('Access-Control-Allow-Origin', '*')
72+
// Public discovery endpoint for registries; always allow CORS (optionally restricted by allowedOrigins).
73+
if (!applyCorsHeaders(req, res, options.allowedOrigins)) {
74+
return
75+
}
6676
res.setHeader('Access-Control-Allow-Methods', 'GET,HEAD,OPTIONS')
6777
if (req.method === 'OPTIONS') {
6878
res.statusCode = 204
@@ -75,7 +85,9 @@ export const createTransloaditMcpHttpHandler = async (
7585
return
7686
}
7787
res.statusCode = 200
78-
res.setHeader('Content-Type', 'application/json')
88+
res.setHeader('Content-Type', 'application/json; charset=utf-8')
89+
res.setHeader('Cache-Control', 'public, max-age=3600')
90+
res.setHeader('X-Content-Type-Options', 'nosniff')
7991
res.end(req.method === 'HEAD' ? undefined : serverCardJson)
8092
return
8193
}
Lines changed: 131 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
1+
import { LATEST_PROTOCOL_VERSION } from '@modelcontextprotocol/sdk/types.js'
12
import packageJson from '../package.json' with { type: 'json' }
23

34
export const serverCardPath = '/.well-known/mcp/server-card.json'
45

5-
type ServerCardTool = {
6+
type JsonSchemaObject = Record<string, unknown>
7+
8+
type ServerCardToolDefinition = {
69
name: string
10+
title: string
711
description: string
12+
inputSchema: JsonSchemaObject
813
}
914

1015
type ServerCard = {
@@ -16,74 +21,163 @@ type ServerCard = {
1621
documentationUrl: string
1722
iconUrl: string
1823
transport: { type: string; endpoint: string }
19-
authentication: { required: boolean; schemes: Array<{ type: string; description: string }> }
20-
capabilities: { tools: boolean }
21-
tools: ServerCardTool[]
24+
authentication?: { required: boolean; schemes: string[] }
25+
capabilities: { tools: { listChanged: boolean } }
26+
tools: ['dynamic'] | ServerCardToolDefinition[]
2227
}
2328

24-
const tools: ServerCardTool[] = [
29+
const tools: ServerCardToolDefinition[] = [
2530
{
2631
name: 'transloadit_lint_assembly_instructions',
32+
title: 'Lint Assembly Instructions',
2733
description:
2834
'Lint Assembly Instructions without creating an Assembly. Returns structured issues.',
35+
inputSchema: {
36+
type: 'object',
37+
additionalProperties: false,
38+
required: ['instructions'],
39+
properties: {
40+
instructions: { type: ['object', 'array', 'string', 'number', 'boolean', 'null'] },
41+
strict: { type: 'boolean', description: 'Treat warnings as errors.' },
42+
return_fixed: { type: 'boolean', description: 'Return normalized instructions when true.' },
43+
},
44+
},
2945
},
3046
{
3147
name: 'transloadit_create_assembly',
48+
title: 'Create or resume an Assembly',
3249
description:
3350
'Create or resume an Assembly, optionally uploading files and waiting for completion.',
51+
inputSchema: {
52+
type: 'object',
53+
additionalProperties: false,
54+
properties: {
55+
instructions: { type: ['object', 'array', 'string', 'number', 'boolean', 'null'] },
56+
files: {
57+
type: 'array',
58+
items: { type: 'object' },
59+
},
60+
fields: { type: 'object' },
61+
wait_for_completion: { type: 'boolean' },
62+
wait_timeout_ms: { type: 'number' },
63+
upload_concurrency: { type: 'number' },
64+
upload_chunk_size: { type: 'number' },
65+
upload_behavior: { type: 'string', enum: ['await', 'background', 'none'] },
66+
expected_uploads: { type: 'number' },
67+
assembly_url: { type: 'string' },
68+
},
69+
},
3470
},
3571
{
3672
name: 'transloadit_get_assembly_status',
73+
title: 'Get Assembly Status',
3774
description: 'Fetch the latest Assembly status by URL or ID.',
75+
inputSchema: {
76+
type: 'object',
77+
additionalProperties: false,
78+
properties: {
79+
assembly_url: { type: 'string' },
80+
assembly_id: { type: 'string' },
81+
},
82+
},
3883
},
3984
{
4085
name: 'transloadit_wait_for_assembly',
86+
title: 'Wait For Assembly Completion',
4187
description: 'Polls until the Assembly completes or timeout is reached.',
88+
inputSchema: {
89+
type: 'object',
90+
additionalProperties: false,
91+
properties: {
92+
assembly_url: { type: 'string' },
93+
assembly_id: { type: 'string' },
94+
timeout_ms: { type: 'number' },
95+
poll_interval_ms: { type: 'number' },
96+
},
97+
},
4298
},
4399
{
44100
name: 'transloadit_list_robots',
101+
title: 'List Robots',
45102
description: 'Returns a filtered list of robots with short summaries.',
103+
inputSchema: {
104+
type: 'object',
105+
additionalProperties: false,
106+
properties: {
107+
category: { type: 'string' },
108+
search: { type: 'string' },
109+
limit: { type: 'number' },
110+
cursor: { type: 'string' },
111+
},
112+
},
46113
},
47114
{
48115
name: 'transloadit_get_robot_help',
116+
title: 'Get Robot Help',
49117
description: 'Returns a robot summary and parameter details.',
118+
inputSchema: {
119+
type: 'object',
120+
additionalProperties: false,
121+
properties: {
122+
robot_name: { type: 'string' },
123+
robot_names: { type: 'array', items: { type: 'string' } },
124+
},
125+
},
50126
},
51127
{
52128
name: 'transloadit_list_templates',
129+
title: 'List Templates',
53130
description:
54131
'List Assembly Templates (owned and/or builtin). Tip: pass include_builtin: "exclusively-latest" to list builtins only.',
132+
inputSchema: {
133+
type: 'object',
134+
additionalProperties: false,
135+
properties: {
136+
page: { type: 'number' },
137+
page_size: { type: 'number' },
138+
sort: { type: 'string', enum: ['id', 'name', 'created', 'modified'] },
139+
order: { type: 'string', enum: ['asc', 'desc'] },
140+
keywords: { type: 'array', items: { type: 'string' } },
141+
include_builtin: {
142+
type: 'string',
143+
enum: ['all', 'latest', 'exclusively-all', 'exclusively-latest'],
144+
},
145+
include_content: { type: 'boolean' },
146+
},
147+
},
55148
},
56149
]
57150

58-
export const buildServerCard = (endpoint: string): ServerCard => ({
59-
$schema: 'https://static.modelcontextprotocol.io/schemas/mcp-server-card/v1.json',
60-
version: '1.0',
61-
protocolVersion: '2025-03-26',
62-
serverInfo: {
63-
name: 'transloadit-mcp',
64-
title: 'Transloadit MCP Server',
65-
version: packageJson.version,
66-
},
67-
description:
68-
'Agent-native media processing: video encoding, image manipulation, document conversion, and more via 86+ Robots.',
69-
documentationUrl: 'https://transloadit.com/docs/topics/ai-agents/',
70-
iconUrl: 'https://transloadit.com/favicon.ico',
71-
transport: {
72-
type: 'streamable-http',
73-
endpoint,
74-
},
75-
authentication: {
76-
required: true,
77-
schemes: [
78-
{
79-
type: 'bearer',
80-
description:
81-
'Transloadit API Bearer token. Self-hosted: set TRANSLOADIT_KEY and TRANSLOADIT_SECRET env vars (auto-mints tokens). Hosted: call the authenticate tool or pass a bearer token.',
82-
},
83-
],
84-
},
85-
capabilities: {
86-
tools: true,
87-
},
88-
tools,
89-
})
151+
export const buildServerCard = (
152+
endpoint: string,
153+
options: { authKey?: string; authSecret?: string } = {},
154+
): ServerCard => {
155+
const hasCredentials = Boolean(options.authKey && options.authSecret)
156+
157+
return {
158+
$schema: 'https://static.modelcontextprotocol.io/schemas/mcp-server-card/v1.json',
159+
version: '1.0',
160+
protocolVersion: LATEST_PROTOCOL_VERSION,
161+
serverInfo: {
162+
name: 'transloadit-mcp',
163+
title: 'Transloadit MCP Server',
164+
version: packageJson.version,
165+
},
166+
description:
167+
'Agent-native media processing: video encoding, image manipulation, document conversion, and more via 86+ Robots.',
168+
documentationUrl: 'https://transloadit.com/docs/topics/ai-agents/',
169+
iconUrl: 'https://transloadit.com/favicon.ico',
170+
transport: {
171+
type: 'streamable-http',
172+
endpoint,
173+
},
174+
authentication: {
175+
required: !hasCredentials,
176+
schemes: ['bearer'],
177+
},
178+
capabilities: {
179+
tools: { listChanged: false },
180+
},
181+
tools,
182+
}
183+
}

0 commit comments

Comments
 (0)