Skip to content

Commit c4cfaf6

Browse files
kvzclaude
andcommitted
feat(mcp-server): add /.well-known/mcp/server-card.json endpoint
Serves static MCP server card metadata (tools, capabilities, auth info) without requiring authentication. This fixes Smithery's "No capabilities found" issue caused by its tool introspection being blocked by the auth wall. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent b8604aa commit c4cfaf6

3 files changed

Lines changed: 141 additions & 21 deletions

File tree

packages/mcp-server/src/express.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ 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'
89
import { createTransloaditMcpServer } from './server.ts'
910

1011
export type TransloaditMcpExpressOptions = TransloaditMcpHttpOptions & {
@@ -37,6 +38,15 @@ export const createTransloaditMcpExpressRouter = async (
3738
redactSecrets: [options.mcpToken, options.authKey, options.authSecret],
3839
})
3940

41+
const serverCardJson = JSON.stringify(buildServerCard(routePath))
42+
43+
router.get(serverCardPath, (_req, res) => {
44+
res.setHeader('Access-Control-Allow-Origin', '*')
45+
res.setHeader('Access-Control-Allow-Methods', 'GET,HEAD,OPTIONS')
46+
res.setHeader('Content-Type', 'application/json')
47+
res.status(200).send(serverCardJson)
48+
})
49+
4050
router.all(routePath, (req, res) => {
4151
void handler(req, res)
4252
})

packages/mcp-server/src/http.ts

Lines changed: 42 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type { SevLogger } from '@transloadit/sev-logger'
55
import { isBasicAuthorized, normalizePath, parsePathname } 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'
89
import type { TransloaditMcpServerOptions } from './server.ts'
910
import { createTransloaditMcpServer } from './server.ts'
1011

@@ -55,31 +56,51 @@ export const createTransloaditMcpHttpHandler = async (
5556
redactSecrets: [options.mcpToken, options.authKey, options.authSecret],
5657
})
5758

59+
const serverCardJson = JSON.stringify(buildServerCard(expectedPath))
60+
5861
const handler = (async (req, res) => {
59-
if (metricsPath) {
60-
const pathname = normalizePath(parsePathname(req.url, expectedPath))
61-
if (pathname === metricsPath) {
62-
if (metricsAuth && !isBasicAuthorized(req, metricsAuth)) {
63-
res.statusCode = 401
64-
res.setHeader('WWW-Authenticate', 'Basic realm="metrics"')
65-
res.end('Unauthorized')
66-
return
67-
}
68-
if (req.method !== 'GET' && req.method !== 'HEAD') {
69-
res.statusCode = 405
70-
res.end('Method Not Allowed')
71-
return
72-
}
62+
const pathname = normalizePath(parsePathname(req.url, expectedPath))
63+
64+
if (pathname === serverCardPath) {
65+
res.setHeader('Access-Control-Allow-Origin', '*')
66+
res.setHeader('Access-Control-Allow-Methods', 'GET,HEAD,OPTIONS')
67+
if (req.method === 'OPTIONS') {
68+
res.statusCode = 204
69+
res.end()
70+
return
71+
}
72+
if (req.method !== 'GET' && req.method !== 'HEAD') {
73+
res.statusCode = 405
74+
res.end('Method Not Allowed')
75+
return
76+
}
77+
res.statusCode = 200
78+
res.setHeader('Content-Type', 'application/json')
79+
res.end(req.method === 'HEAD' ? undefined : serverCardJson)
80+
return
81+
}
82+
83+
if (metricsPath && pathname === metricsPath) {
84+
if (metricsAuth && !isBasicAuthorized(req, metricsAuth)) {
85+
res.statusCode = 401
86+
res.setHeader('WWW-Authenticate', 'Basic realm="metrics"')
87+
res.end('Unauthorized')
88+
return
89+
}
90+
if (req.method !== 'GET' && req.method !== 'HEAD') {
91+
res.statusCode = 405
92+
res.end('Method Not Allowed')
93+
return
94+
}
7395

74-
res.statusCode = 200
75-
res.setHeader('Content-Type', getMetricsContentType())
76-
if (req.method === 'HEAD') {
77-
res.end()
78-
return
79-
}
80-
res.end(await getMetrics())
96+
res.statusCode = 200
97+
res.setHeader('Content-Type', getMetricsContentType())
98+
if (req.method === 'HEAD') {
99+
res.end()
81100
return
82101
}
102+
res.end(await getMetrics())
103+
return
83104
}
84105

85106
await mcpHandler(req, res)
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import packageJson from '../package.json' with { type: 'json' }
2+
3+
export const serverCardPath = '/.well-known/mcp/server-card.json'
4+
5+
type ServerCardTool = {
6+
name: string
7+
description: string
8+
}
9+
10+
type ServerCard = {
11+
$schema: string
12+
version: string
13+
protocolVersion: string
14+
serverInfo: { name: string; title: string; version: string }
15+
description: string
16+
documentationUrl: string
17+
iconUrl: string
18+
transport: { type: string; endpoint: string }
19+
authentication: { required: boolean; schemes: Array<{ type: string; description: string }> }
20+
capabilities: { tools: boolean }
21+
tools: ServerCardTool[]
22+
}
23+
24+
const tools: ServerCardTool[] = [
25+
{
26+
name: 'transloadit_lint_assembly_instructions',
27+
description:
28+
'Lint Assembly Instructions without creating an Assembly. Returns structured issues.',
29+
},
30+
{
31+
name: 'transloadit_create_assembly',
32+
description:
33+
'Create or resume an Assembly, optionally uploading files and waiting for completion.',
34+
},
35+
{
36+
name: 'transloadit_get_assembly_status',
37+
description: 'Fetch the latest Assembly status by URL or ID.',
38+
},
39+
{
40+
name: 'transloadit_wait_for_assembly',
41+
description: 'Polls until the Assembly completes or timeout is reached.',
42+
},
43+
{
44+
name: 'transloadit_list_robots',
45+
description: 'Returns a filtered list of robots with short summaries.',
46+
},
47+
{
48+
name: 'transloadit_get_robot_help',
49+
description: 'Returns a robot summary and parameter details.',
50+
},
51+
{
52+
name: 'transloadit_list_templates',
53+
description:
54+
'List Assembly Templates (owned and/or builtin). Tip: pass include_builtin: "exclusively-latest" to list builtins only.',
55+
},
56+
]
57+
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+
})

0 commit comments

Comments
 (0)