Skip to content

Commit 56ada34

Browse files
authored
Clean up verify-mcp clients (#310)
* Clean up MCP config after verification * Document MCP tool permissions for CLI clients * Document MCP allowlists for CLI clients * Add changeset for MCP allowlist docs * Clarify MCP tool allowlists in README
1 parent 48d0a10 commit 56ada34

3 files changed

Lines changed: 119 additions & 1 deletion

File tree

.changeset/mcp-allowlist-docs.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@transloadit/mcp-server": patch
3+
---
4+
5+
Document MCP client tool allowlists and keep verification tooling aligned.

packages/mcp-server/README.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,16 @@ claude mcp add --transport stdio transloadit \
3232
-- npx -y @transloadit/mcp-server stdio
3333
```
3434

35+
For non-interactive runs (e.g. `claude -p`), explicitly allow MCP tools. Claude MCP
36+
tools are named `mcp__<server>__<tool>`, so `mcp__transloadit__*` allows all tools
37+
from this server.
38+
39+
```bash
40+
claude -p "List templates" \
41+
--allowedTools mcp__transloadit__* \
42+
--output-format json
43+
```
44+
3545
Codex CLI:
3646

3747
```bash
@@ -41,6 +51,15 @@ codex mcp add transloadit \
4151
-- npx -y @transloadit/mcp-server stdio
4252
```
4353

54+
To allowlist tools, add `enabled_tools` for the server in `~/.codex/config.toml`:
55+
56+
```toml
57+
[mcp_servers.transloadit]
58+
command = "npx"
59+
args = ["-y", "@transloadit/mcp-server", "stdio"]
60+
enabled_tools = ["transloadit_list_templates"]
61+
```
62+
4463
Gemini CLI:
4564

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

71+
To allowlist tools, set `includeTools` for the server in `~/.gemini/settings.json`:
72+
73+
```json
74+
{
75+
"mcpServers": {
76+
"transloadit": {
77+
"command": "npx",
78+
"args": ["-y", "@transloadit/mcp-server", "stdio"],
79+
"env": {
80+
"TRANSLOADIT_KEY": "...",
81+
"TRANSLOADIT_SECRET": "..."
82+
},
83+
"includeTools": ["transloadit_list_templates"]
84+
}
85+
}
86+
}
87+
```
88+
5289
Cursor (`~/.cursor/mcp.json`):
5390

5491
```json

scripts/verify-mcp-clients.ts

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ type CliCheck = {
88
command: string
99
add: () => void
1010
run: () => { ok: boolean; output: string }
11+
cleanup?: () => void
1112
}
1213

1314
const requiredEnv = ['TRANSLOADIT_KEY', 'TRANSLOADIT_SECRET']
@@ -21,6 +22,7 @@ if (envMissing.length > 0) {
2122
const endpoint = process.env.TRANSLOADIT_ENDPOINT ?? 'https://api2.transloadit.com'
2223
const commandTimeoutMs = Number(process.env.MCP_VERIFY_TIMEOUT_MS ?? 60_000)
2324
const serverName = process.env.MCP_SERVER_NAME ?? 'transloadit'
25+
const allowlistedTools = ['transloadit_list_templates']
2426
const serverCommand = [
2527
'npm',
2628
'exec',
@@ -105,14 +107,17 @@ const runClaude = (prompt: string, expectedTemplateId: string): CliCheck => ({
105107
throw new Error(`claude mcp add failed: ${result.stderr || result.stdout}`)
106108
}
107109
},
110+
cleanup: () => {
111+
runCommand('claude', ['mcp', 'remove', serverName])
112+
},
108113
run: () => {
109114
const result = runCommand('claude', [
110115
'-p',
111116
prompt,
112117
'--output-format',
113118
'json',
114119
'--allowedTools',
115-
'transloadit_list_templates',
120+
`mcp__${serverName}`,
116121
'--permission-mode',
117122
'acceptEdits',
118123
])
@@ -143,6 +148,10 @@ const runCodex = (prompt: string, expectedTemplateId: string): CliCheck => ({
143148
if (result.status !== 0) {
144149
throw new Error(`codex mcp add failed: ${result.stderr || result.stdout}`)
145150
}
151+
updateCodexEnabledTools()
152+
},
153+
cleanup: () => {
154+
runCommand('codex', ['mcp', 'remove', serverName])
146155
},
147156
run: () => {
148157
const result = runCommand('codex', ['exec', '--full-auto', '--json', prompt])
@@ -181,13 +190,74 @@ const ensureGeminiSettings = (): void => {
181190
TRANSLOADIT_SECRET: process.env.TRANSLOADIT_SECRET ?? '',
182191
TRANSLOADIT_ENDPOINT: endpoint,
183192
},
193+
includeTools: allowlistedTools,
184194
}
185195

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

201+
const cleanupGeminiSettings = (): void => {
202+
const cwd = process.cwd()
203+
const settingsPath = join(homedir(), '.gemini', 'settings.json')
204+
if (settingsPath.startsWith(`${cwd}/`)) {
205+
return
206+
}
207+
if (!existsSync(settingsPath)) {
208+
return
209+
}
210+
let settings: Record<string, unknown> = {}
211+
try {
212+
settings = JSON.parse(readFileSync(settingsPath, 'utf8')) as Record<string, unknown>
213+
} catch {
214+
return
215+
}
216+
const mcpServers = (settings.mcpServers as Record<string, unknown>) ?? {}
217+
if (!(serverName in mcpServers)) {
218+
return
219+
}
220+
delete mcpServers[serverName]
221+
settings.mcpServers = mcpServers
222+
writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}\n`)
223+
}
224+
225+
const updateCodexEnabledTools = (): void => {
226+
const configPath = join(homedir(), '.codex', 'config.toml')
227+
if (!existsSync(configPath)) {
228+
return
229+
}
230+
const content = readFileSync(configPath, 'utf8')
231+
const header = `[mcp_servers.${serverName}]`
232+
const lines = content.split('\n')
233+
const headerIndex = lines.findIndex((line) => line.trim() === header)
234+
if (headerIndex === -1) {
235+
return
236+
}
237+
let endIndex = lines.length
238+
for (let i = headerIndex + 1; i < lines.length; i += 1) {
239+
if (lines[i].startsWith('[')) {
240+
endIndex = i
241+
break
242+
}
243+
}
244+
245+
const enabledLine = `enabled_tools = [${allowlistedTools.map((tool) => `"${tool}"`).join(', ')}]`
246+
let replaced = false
247+
for (let i = headerIndex + 1; i < endIndex; i += 1) {
248+
if (lines[i].trim().startsWith('enabled_tools')) {
249+
lines[i] = enabledLine
250+
replaced = true
251+
break
252+
}
253+
}
254+
if (!replaced) {
255+
lines.splice(headerIndex + 1, 0, enabledLine)
256+
}
257+
258+
writeFileSync(configPath, `${lines.join('\n')}\n`)
259+
}
260+
191261
const runGemini = (prompt: string, expectedTemplateId: string): CliCheck => ({
192262
name: 'Gemini CLI',
193263
command: 'gemini',
@@ -217,6 +287,10 @@ const runGemini = (prompt: string, expectedTemplateId: string): CliCheck => ({
217287
return
218288
}
219289
},
290+
cleanup: () => {
291+
runCommand('gemini', ['mcp', 'remove', serverName])
292+
cleanupGeminiSettings()
293+
},
220294
run: () => {
221295
const result = runCommand('gemini', [
222296
'--prompt',
@@ -261,6 +335,8 @@ const main = async (): Promise<void> => {
261335
ok: false,
262336
output: error instanceof Error ? error.message : String(error),
263337
})
338+
} finally {
339+
check.cleanup?.()
264340
}
265341
}
266342

0 commit comments

Comments
 (0)