Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/mcp-allowlist-docs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@transloadit/mcp-server": patch
---

Document MCP client tool allowlists and keep verification tooling aligned.
37 changes: 37 additions & 0 deletions packages/mcp-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,16 @@ claude mcp add --transport stdio transloadit \
-- npx -y @transloadit/mcp-server stdio
```

For non-interactive runs (e.g. `claude -p`), explicitly allow MCP tools. Claude MCP
tools are named `mcp__<server>__<tool>`, so `mcp__transloadit__*` allows all tools
from this server.

```bash
claude -p "List templates" \
--allowedTools mcp__transloadit__* \
--output-format json
```

Codex CLI:

```bash
Expand All @@ -41,6 +51,15 @@ codex mcp add transloadit \
-- npx -y @transloadit/mcp-server stdio
```

To allowlist tools, add `enabled_tools` for the server in `~/.codex/config.toml`:

```toml
[mcp_servers.transloadit]
command = "npx"
args = ["-y", "@transloadit/mcp-server", "stdio"]
enabled_tools = ["transloadit_list_templates"]
```

Gemini CLI:

```bash
Expand All @@ -49,6 +68,24 @@ gemini mcp add --scope user transloadit npx -y @transloadit/mcp-server stdio \
--env TRANSLOADIT_SECRET=...
```

To allowlist tools, set `includeTools` for the server in `~/.gemini/settings.json`:

```json
{
"mcpServers": {
"transloadit": {
"command": "npx",
"args": ["-y", "@transloadit/mcp-server", "stdio"],
"env": {
"TRANSLOADIT_KEY": "...",
"TRANSLOADIT_SECRET": "..."
},
"includeTools": ["transloadit_list_templates"]
}
}
}
```

Cursor (`~/.cursor/mcp.json`):

```json
Expand Down
78 changes: 77 additions & 1 deletion scripts/verify-mcp-clients.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ type CliCheck = {
command: string
add: () => void
run: () => { ok: boolean; output: string }
cleanup?: () => void
}

const requiredEnv = ['TRANSLOADIT_KEY', 'TRANSLOADIT_SECRET']
Expand All @@ -21,6 +22,7 @@ if (envMissing.length > 0) {
const endpoint = process.env.TRANSLOADIT_ENDPOINT ?? 'https://api2.transloadit.com'
const commandTimeoutMs = Number(process.env.MCP_VERIFY_TIMEOUT_MS ?? 60_000)
const serverName = process.env.MCP_SERVER_NAME ?? 'transloadit'
const allowlistedTools = ['transloadit_list_templates']
const serverCommand = [
'npm',
'exec',
Expand Down Expand Up @@ -105,14 +107,17 @@ const runClaude = (prompt: string, expectedTemplateId: string): CliCheck => ({
throw new Error(`claude mcp add failed: ${result.stderr || result.stdout}`)
}
},
cleanup: () => {
runCommand('claude', ['mcp', 'remove', serverName])
},
run: () => {
const result = runCommand('claude', [
'-p',
prompt,
'--output-format',
'json',
'--allowedTools',
'transloadit_list_templates',
`mcp__${serverName}`,
'--permission-mode',
'acceptEdits',
])
Expand Down Expand Up @@ -143,6 +148,10 @@ const runCodex = (prompt: string, expectedTemplateId: string): CliCheck => ({
if (result.status !== 0) {
throw new Error(`codex mcp add failed: ${result.stderr || result.stdout}`)
}
updateCodexEnabledTools()
},
cleanup: () => {
runCommand('codex', ['mcp', 'remove', serverName])
},
run: () => {
const result = runCommand('codex', ['exec', '--full-auto', '--json', prompt])
Expand Down Expand Up @@ -181,13 +190,74 @@ const ensureGeminiSettings = (): void => {
TRANSLOADIT_SECRET: process.env.TRANSLOADIT_SECRET ?? '',
TRANSLOADIT_ENDPOINT: endpoint,
},
includeTools: allowlistedTools,
}

settings.mcpServers = mcpServers
console.log(`Writing Gemini MCP config to ${settingsPath} using current env credentials.`)
writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}\n`)
}

const cleanupGeminiSettings = (): void => {
const cwd = process.cwd()
const settingsPath = join(homedir(), '.gemini', 'settings.json')
if (settingsPath.startsWith(`${cwd}/`)) {
return
}
if (!existsSync(settingsPath)) {
return
}
let settings: Record<string, unknown> = {}
try {
settings = JSON.parse(readFileSync(settingsPath, 'utf8')) as Record<string, unknown>
} catch {
return
}
const mcpServers = (settings.mcpServers as Record<string, unknown>) ?? {}
if (!(serverName in mcpServers)) {
return
}
delete mcpServers[serverName]
settings.mcpServers = mcpServers
writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}\n`)
}

const updateCodexEnabledTools = (): void => {
const configPath = join(homedir(), '.codex', 'config.toml')
if (!existsSync(configPath)) {
return
}
const content = readFileSync(configPath, 'utf8')
const header = `[mcp_servers.${serverName}]`
const lines = content.split('\n')
const headerIndex = lines.findIndex((line) => line.trim() === header)
if (headerIndex === -1) {
return
}
let endIndex = lines.length
for (let i = headerIndex + 1; i < lines.length; i += 1) {
if (lines[i].startsWith('[')) {
endIndex = i
break
}
}

const enabledLine = `enabled_tools = [${allowlistedTools.map((tool) => `"${tool}"`).join(', ')}]`
let replaced = false
for (let i = headerIndex + 1; i < endIndex; i += 1) {
if (lines[i].trim().startsWith('enabled_tools')) {
lines[i] = enabledLine
replaced = true
break
}
}
if (!replaced) {
lines.splice(headerIndex + 1, 0, enabledLine)
}

writeFileSync(configPath, `${lines.join('\n')}\n`)
}

const runGemini = (prompt: string, expectedTemplateId: string): CliCheck => ({
name: 'Gemini CLI',
command: 'gemini',
Expand Down Expand Up @@ -217,6 +287,10 @@ const runGemini = (prompt: string, expectedTemplateId: string): CliCheck => ({
return
}
},
cleanup: () => {
runCommand('gemini', ['mcp', 'remove', serverName])
cleanupGeminiSettings()
},
run: () => {
const result = runCommand('gemini', [
'--prompt',
Expand Down Expand Up @@ -261,6 +335,8 @@ const main = async (): Promise<void> => {
ok: false,
output: error instanceof Error ? error.message : String(error),
})
} finally {
check.cleanup?.()
}
}

Expand Down