diff --git a/.changeset/mcp-allowlist-docs.md b/.changeset/mcp-allowlist-docs.md new file mode 100644 index 00000000..192df915 --- /dev/null +++ b/.changeset/mcp-allowlist-docs.md @@ -0,0 +1,5 @@ +--- +"@transloadit/mcp-server": patch +--- + +Document MCP client tool allowlists and keep verification tooling aligned. diff --git a/packages/mcp-server/README.md b/packages/mcp-server/README.md index b3829334..5529154b 100644 --- a/packages/mcp-server/README.md +++ b/packages/mcp-server/README.md @@ -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____`, so `mcp__transloadit__*` allows all tools +from this server. + +```bash +claude -p "List templates" \ + --allowedTools mcp__transloadit__* \ + --output-format json +``` + Codex CLI: ```bash @@ -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 @@ -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 diff --git a/scripts/verify-mcp-clients.ts b/scripts/verify-mcp-clients.ts index 635beb42..7f0c7ea1 100644 --- a/scripts/verify-mcp-clients.ts +++ b/scripts/verify-mcp-clients.ts @@ -8,6 +8,7 @@ type CliCheck = { command: string add: () => void run: () => { ok: boolean; output: string } + cleanup?: () => void } const requiredEnv = ['TRANSLOADIT_KEY', 'TRANSLOADIT_SECRET'] @@ -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', @@ -105,6 +107,9 @@ 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', @@ -112,7 +117,7 @@ const runClaude = (prompt: string, expectedTemplateId: string): CliCheck => ({ '--output-format', 'json', '--allowedTools', - 'transloadit_list_templates', + `mcp__${serverName}`, '--permission-mode', 'acceptEdits', ]) @@ -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]) @@ -181,6 +190,7 @@ const ensureGeminiSettings = (): void => { TRANSLOADIT_SECRET: process.env.TRANSLOADIT_SECRET ?? '', TRANSLOADIT_ENDPOINT: endpoint, }, + includeTools: allowlistedTools, } settings.mcpServers = mcpServers @@ -188,6 +198,66 @@ const ensureGeminiSettings = (): void => { 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 = {} + try { + settings = JSON.parse(readFileSync(settingsPath, 'utf8')) as Record + } catch { + return + } + const mcpServers = (settings.mcpServers as Record) ?? {} + 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', @@ -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', @@ -261,6 +335,8 @@ const main = async (): Promise => { ok: false, output: error instanceof Error ? error.message : String(error), }) + } finally { + check.cleanup?.() } }