fix(mcp): let the tool allowlist express plugin tools with wildcards

This commit is contained in:
Jinpy
2026-09-29 14:00:58 +08:00
committed by GitHub
parent e7883a6466
commit 6f5511a732
15 changed files with 208 additions and 3 deletions
@@ -233,7 +233,18 @@ import {
} from "@/lib/mcp/mcpConfigTemplates";
import { beginMcpStatusRequest, mcpUpdateAvailability } from "@/lib/mcp/mcpUpdateStatus";
import { notifyComponentUpdatesChanged } from "@/lib/updates/componentUpdateEvents";
import { isMcpPolicyMutationBlocked, MCP_CAPABILITY_ROWS, MCP_EXECUTION_MODE_COLUMNS, MCP_TOOL_OPTIONS, mcpExecutionModeFromPolicy, mcpPolicyFieldsForExecutionMode, toggleMcpAllowedToolName, type McpExecutionMode } from "@/lib/mcp/mcpPolicySelection";
import {
addMcpAllowedToolName,
customMcpAllowedToolNames,
isMcpPolicyMutationBlocked,
MCP_CAPABILITY_ROWS,
MCP_EXECUTION_MODE_COLUMNS,
MCP_TOOL_OPTIONS,
mcpExecutionModeFromPolicy,
mcpPolicyFieldsForExecutionMode,
toggleMcpAllowedToolName,
type McpExecutionMode,
} from "@/lib/mcp/mcpPolicySelection";
import { isMacOS, isWindows } from "@/lib/backend/platform";
import { combineDataTypeForDatabase, dataTypeLengthInputValue, getDataTypeOptions, getDefaultLengthForType, isDataTypeLengthDisabled, splitDataType } from "@/lib/table/tableStructureEditorState";
import { useToast } from "@/composables/useToast";
@@ -3710,6 +3721,24 @@ function onMcpToolAllowedChange(name: string, allowed: boolean) {
void saveMcpPolicy({ allowedToolNames: toggleMcpAllowedToolName(mcpAllowedToolNames.value, name, allowed) });
}
// Entries beyond the static options: discovered plugin tool names and the
// `dbx_<prefix>__*` / `dbx_*__*` wildcards the backend matcher expands. They
// are invisible in the checkbox grid, so they render as removable chips.
const mcpCustomToolEntries = computed(() => customMcpAllowedToolNames(mcpAllowedToolNames.value));
const mcpCustomToolInput = ref("");
function onMcpCustomToolAdd() {
const rawName = mcpCustomToolInput.value;
if (!rawName.trim()) return;
const { names, added } = addMcpAllowedToolName(mcpAllowedToolNames.value, rawName);
mcpCustomToolInput.value = "";
if (added) void saveMcpPolicy({ allowedToolNames: names });
}
function onMcpCustomToolRemove(name: string) {
void saveMcpPolicy({ allowedToolNames: toggleMcpAllowedToolName(mcpAllowedToolNames.value, name, false) });
}
const mcpConnectionPolicyConnections = computed(() => {
const allowed = mcpEffectiveAllowedConnectionIds.value;
return allowed === null ? mcpSelectableConnections.value : mcpSelectableConnections.value.filter((connection) => allowed.includes(connection.id));
@@ -10598,6 +10627,21 @@ LIMIT 100;</pre
<span>{{ t(tool.labelKey) }}</span>
</label>
</div>
<div class="space-y-2">
<div class="flex gap-2">
<Input v-model="mcpCustomToolInput" class="h-8 flex-1 font-mono text-xs" :placeholder="t('settings.mcpToolCustomPlaceholder')" :disabled="mcpPolicyControlsDisabled" spellcheck="false" @keydown.enter.prevent="onMcpCustomToolAdd" />
<Button variant="outline" size="sm" class="h-8 shrink-0" :disabled="mcpPolicyControlsDisabled || !mcpCustomToolInput.trim()" @click="onMcpCustomToolAdd">{{ t("settings.mcpToolCustomAdd") }}</Button>
</div>
<p class="text-xs text-muted-foreground">{{ t("settings.mcpToolCustomHint") }}</p>
<div v-if="mcpCustomToolEntries.length" class="flex flex-wrap gap-1.5">
<span v-for="name in mcpCustomToolEntries" :key="name" class="flex items-center gap-1 rounded border bg-background px-2 py-1 font-mono text-xs">
{{ name }}
<button type="button" class="text-muted-foreground transition-colors hover:text-destructive" :disabled="mcpPolicyControlsDisabled" :aria-label="t('settings.mcpToolCustomRemove')" @click="onMcpCustomToolRemove(name)">
<X class="size-3" />
</button>
</span>
</div>
</div>
</section>
</div>
</template>
+4
View File
@@ -8262,6 +8262,10 @@ export default withEnglishFallback({
mcpConnectionPolicyAllowSalesforceDmlReadOnlyBlocked: "Bu bağlantı yalnız oxumaq üçün olduğundan Salesforce DML bağlı qalır. Əvvəlcə yazmağa icazə verən icra rejimini seçin.",
mcpToolPermissionsTitle: "MCP alət icazələri",
mcpToolPermissionsDescription: "Yalnız ehtiyacınız olan MCP alətlərini təqdim edin. Seçim ləğv edildikdə, müştərilər alət təriflərini keşdə saxlamış olsalar belə, server onların müraciətlərini rədd edir.",
mcpToolCustomPlaceholder: "Alət adı və ya joker əlavə edin, məs. dbx_ssh__*",
mcpToolCustomAdd: "Əlavə et",
mcpToolCustomRemove: "Girişi sil",
mcpToolCustomHint: "Pləgin alətləri iş vaxtı aşkarlanır, burada sadala bilməz: bir pləgin üçün dbx_<prefiks>__*, bütün pləgin alətləri üçün dbx_*__* istifadə edin. Yuxarıdakı qutuların xaricində saxlanan girişlər aşağıda görünür və silinə bilər.",
mcpToolListConnections: "Əlaqələri siyahıla",
mcpToolListDatabases: "Verilənlər bazalarını siyahıla",
mcpToolListTables: "Cədvəlləri siyahıla",
+4
View File
@@ -9005,6 +9005,10 @@ export default {
mcpConnectionPolicyAllowSalesforceDmlReadOnlyBlocked: "This connection is read-only, so Salesforce DML stays disabled. Choose a write-capable execution mode first.",
mcpToolPermissionsTitle: "MCP tool permissions",
mcpToolPermissionsDescription: "Expose only the MCP tools you need. Once deselected, clients are rejected by the server even if they cached the tool definitions.",
mcpToolCustomPlaceholder: "Add a tool name or wildcard, e.g. dbx_ssh__*",
mcpToolCustomAdd: "Add",
mcpToolCustomRemove: "Remove entry",
mcpToolCustomHint: "Plugin tools are discovered at runtime, so they cannot be listed here: allow one plugin with dbx_<prefix>__*, or every plugin tool with dbx_*__*. Saved entries beyond the checkboxes above stay visible and removable below.",
mcpToolListConnections: "List connections",
mcpToolListDatabases: "List databases",
mcpToolListTables: "List tables",
+5
View File
@@ -8292,6 +8292,11 @@ export default withEnglishFallback({
mcpConnectionPolicyAllowSalesforceDmlReadOnlyBlocked: "Esta conexión es de solo lectura, por lo que DML de Salesforce permanece desactivado. Elija primero un modo de ejecución que permita escrituras.",
mcpToolPermissionsTitle: "Permisos de herramientas MCP",
mcpToolPermissionsDescription: "Expón solo las herramientas MCP que necesitas. Al desmarcarlas, el servidor rechaza las solicitudes aunque el cliente haya cacheado las definiciones.",
mcpToolCustomPlaceholder: "Añade un nombre de herramienta o comodín, p. ej. dbx_ssh__*",
mcpToolCustomAdd: "Añadir",
mcpToolCustomRemove: "Eliminar entrada",
mcpToolCustomHint:
"Las herramientas de plugins se descubren en tiempo de ejecución, por lo que no se pueden listar aquí: permite un plugin con dbx_<prefijo>__* o todas las herramientas de plugins con dbx_*__*. Las entradas guardadas fuera de las casillas anteriores se muestran aquí y pueden eliminarse.",
mcpToolListConnections: "Listar conexiones",
mcpToolListDatabases: "Listar bases de datos",
mcpToolListTables: "Listar tablas",
+4
View File
@@ -8216,6 +8216,10 @@ export default withEnglishFallback({
mcpConnectionPolicyAllowSalesforceDmlReadOnlyBlocked: "Questa connessione è in sola lettura, quindi DML Salesforce resta disattivato. Scegli prima una modalità di esecuzione che consenta le scritture.",
mcpToolPermissionsTitle: "Permessi degli strumenti MCP",
mcpToolPermissionsDescription: "Esporre solo gli strumenti MCP necessari. Dopo la deselezione, le richieste vengono rifiutate dal server anche se il client ha le definizioni in cache.",
mcpToolCustomPlaceholder: "Aggiungi un nome strumento o un carattere jolly, es. dbx_ssh__*",
mcpToolCustomAdd: "Aggiungi",
mcpToolCustomRemove: "Rimuovi voce",
mcpToolCustomHint: "Gli strumenti dei plugin vengono scoperti a runtime e non possono essere elencati qui: consenti un plugin con dbx_<prefisso>__* o tutti gli strumenti dei plugin con dbx_*__*. Le voci salvate oltre le caselle sopra restano visibili e rimovibili qui sotto.",
mcpToolListConnections: "Elenca connessioni",
mcpToolListDatabases: "Elenca database",
mcpToolListTables: "Elenca tabelle",
+4
View File
@@ -8212,6 +8212,10 @@ export default withEnglishFallback({
mcpConnectionPolicyAllowSalesforceDmlReadOnlyBlocked: "この接続は読み取り専用のため、Salesforce DML は無効のままです。先に書き込みを許可する実行モードを選択してください。",
mcpToolPermissionsTitle: "MCP ツール権限",
mcpToolPermissionsDescription: "必要な MCP ツールのみ公開します。選択を解除すると、ツール定義をキャッシュしたクライアントもサーバー側で拒否されます。",
mcpToolCustomPlaceholder: "ツール名またはワイルドカードを追加(例: dbx_ssh__*)",
mcpToolCustomAdd: "追加",
mcpToolCustomRemove: "エントリを削除",
mcpToolCustomHint: "プラグインツールは実行時に検出されるため、ここには列挙できません:dbx_<接頭辞>__* で特定プラグインを、dbx_*__* ですべてのプラグインツールを許可できます。上のチェックボックス以外に保存されたエントリは下に表示され、いつでも削除できます。",
mcpToolListConnections: "接続を一覧",
mcpToolListDatabases: "データベースを一覧",
mcpToolListTables: "テーブルを一覧",
+4
View File
@@ -7995,6 +7995,10 @@ export default withEnglishFallback({
mcpConnectionPolicyAllowSalesforceDmlReadOnlyBlocked: "이 연결은 읽기 전용이므로 Salesforce DML은 비활성 상태로 유지됩니다. 먼저 쓰기를 허용하는 실행 모드를 선택하세요.",
mcpToolPermissionsTitle: "MCP 도구 권한",
mcpToolPermissionsDescription: "필요한 MCP 도구만 노출하세요. 선택 해제하면 도구 정의를 캐시한 클라이언트도 서버에서 거부됩니다.",
mcpToolCustomPlaceholder: "도구 이름 또는 와일드카드 추가(예: dbx_ssh__*)",
mcpToolCustomAdd: "추가",
mcpToolCustomRemove: "항목 제거",
mcpToolCustomHint: "플러그인 도구는 런타임에 발견되므로 여기에 나열할 수 없습니다: dbx_<접두사>__*로 특정 플러그인을, dbx_*__*로 모든 플러그인 도구를 허용하세요. 위 체크박스 외에 저장된 항목은 아래에 표시되며 언제든 제거할 수 있습니다.",
mcpToolListConnections: "연결 나열",
mcpToolListDatabases: "데이터베이스 나열",
mcpToolListTables: "테이블 나열",
+4
View File
@@ -8218,6 +8218,10 @@ export default withEnglishFallback({
mcpConnectionPolicyAllowSalesforceDmlReadOnlyBlocked: "Esta conexão é somente leitura, então o DML do Salesforce permanece desativado. Escolha primeiro um modo de execução que permita gravações.",
mcpToolPermissionsTitle: "Permissões de ferramentas MCP",
mcpToolPermissionsDescription: "Exponha apenas as ferramentas MCP necessárias. Ao desmarcá-las, o servidor recusa as solicitações mesmo que o cliente tenha as definições em cache.",
mcpToolCustomPlaceholder: "Adicione um nome de ferramenta ou curinga, ex.: dbx_ssh__*",
mcpToolCustomAdd: "Adicionar",
mcpToolCustomRemove: "Remover entrada",
mcpToolCustomHint: "As ferramentas de plugins são descobertas em tempo de execução e não podem ser listadas aqui: permita um plugin com dbx_<prefixo>__* ou todas as ferramentas de plugins com dbx_*__*. Entradas salvas fora das caixas acima permanecem visíveis e removíveis abaixo.",
mcpToolListConnections: "Listar conexões",
mcpToolListDatabases: "Listar bancos de dados",
mcpToolListTables: "Listar tabelas",
+4
View File
@@ -9493,6 +9493,10 @@ export default withEnglishFallback({
mcpConnectionPolicyHighRiskWrite: "Разрешить операции высокого риска",
mcpToolPermissionsTitle: "Разрешения инструментов MCP",
mcpToolPermissionsDescription: "Предоставляйте только нужные инструменты MCP. После снятия выбора клиенты отклоняются сервером, даже если они кэшировали определения инструментов.",
mcpToolCustomPlaceholder: "Добавьте имя инструмента или подстановку, напр. dbx_ssh__*",
mcpToolCustomAdd: "Добавить",
mcpToolCustomRemove: "Удалить запись",
mcpToolCustomHint: "Инструменты плагинов обнаруживаются во время работы, поэтому их нельзя перечислить здесь: разрешите один плагин через dbx_<префикс>__* или все инструменты плагинов через dbx_*__*. Сохранённые записи за пределами списка выше остаются видимыми и удаляемыми ниже.",
mcpToolListConnections: "Список подключений",
mcpToolListDatabases: "Список баз данных",
mcpToolListTables: "Список таблиц",
+4
View File
@@ -8159,6 +8159,10 @@ export default withEnglishFallback({
mcpConnectionPolicyAllowSalesforceDmlReadOnlyBlocked: "Bu bağlantı salt okunur olduğundan Salesforce DML devre dışı kalır. Önce yazmaya izin veren bir çalıştırma modu seçin.",
mcpToolPermissionsTitle: "MCP araç yetkileri",
mcpToolPermissionsDescription: "Yalnızca ihtiyacınız olan MCP araçlarını açın. Seçim kaldırıldığında, araç tanımlarını önbelleğe almış olsalar bile istemciler sunucu tarafından reddedilir.",
mcpToolCustomPlaceholder: "Araç adı veya joker ekleyin, örn. dbx_ssh__*",
mcpToolCustomAdd: "Ekle",
mcpToolCustomRemove: "Girdiyi kaldır",
mcpToolCustomHint: "Eklenti araçları çalışma zamanında keşfedilir, burada listelenemez: tek bir eklenti için dbx_<önek>__*, tüm eklenti araçları için dbx_*__* kullanın. Yukarıdaki onay kutularının dışında kayıtlı girdiler aşağıda görünür ve kaldırılabilir.",
mcpToolListConnections: "Bağlantıları listele",
mcpToolListDatabases: "Veritabanlarını listele",
mcpToolListTables: "Tabloları listele",
+4
View File
@@ -8971,6 +8971,10 @@ export default withEnglishFallback({
mcpConnectionPolicyAllowSalesforceDmlReadOnlyBlocked: "该连接为只读,Salesforce DML 保持关闭。请先选择允许写入的执行模式。",
mcpToolPermissionsTitle: "MCP 工具权限",
mcpToolPermissionsDescription: "只暴露当前需要的 MCP 工具。取消选择后,客户端即使缓存了工具定义也会被服务端拒绝。",
mcpToolCustomPlaceholder: "添加工具名或通配符,如 dbx_ssh__*",
mcpToolCustomAdd: "添加",
mcpToolCustomRemove: "移除该条目",
mcpToolCustomHint: "插件工具在运行时发现,无法在此逐个列出:可用 dbx_<前缀>__* 放行单个插件,或用 dbx_*__* 放行所有插件工具。保存在上方复选框之外的条目会显示在下方,可随时移除。",
mcpToolListConnections: "列出连接",
mcpToolListDatabases: "列出数据库",
mcpToolListTables: "列出表",
+4
View File
@@ -7581,6 +7581,10 @@ export default withEnglishFallback({
mcpConnectionPolicyAllowSalesforceDmlReadOnlyBlocked: "該連線為唯讀,Salesforce DML 保持關閉。請先選擇允許寫入的執行模式。",
mcpToolPermissionsTitle: "MCP 工具權限",
mcpToolPermissionsDescription: "只暴露目前需要的 MCP 工具。取消選擇後,用戶端即使快取了工具定義也會被服務端拒絕。",
mcpToolCustomPlaceholder: "新增工具名稱或萬用字元,如 dbx_ssh__*",
mcpToolCustomAdd: "新增",
mcpToolCustomRemove: "移除此項目",
mcpToolCustomHint: "外掛工具在執行時發現,無法在此逐項列出:可用 dbx_<前綴>__* 放行單一外掛,或用 dbx_*__* 放行所有外掛工具。儲存在上方核取方塊之外的項目會顯示在下方,可隨時移除。",
mcpToolListConnections: "列出連線",
mcpToolListDatabases: "列出資料庫",
mcpToolListTables: "列出資料表",
+44
View File
@@ -0,0 +1,44 @@
import { describe, expect, it } from "vitest";
import { MCP_PLUGIN_TOOLS_WILDCARD, MCP_TOOL_OPTIONS, addMcpAllowedToolName, customMcpAllowedToolNames, toggleMcpAllowedToolName } from "./mcpPolicySelection";
describe("mcpPolicySelection tool allowlist", () => {
it("seeds the plugin wildcard when the null allowlist becomes explicit", () => {
// A null allowlist lets every discovered plugin tool through. The first
// checkbox toggle materializes the list, and without the wildcard seed it
// would silently revoke all of them.
const names = toggleMcpAllowedToolName(null, "dbx_list_tables", false);
expect(names).toContain(MCP_PLUGIN_TOOLS_WILDCARD);
expect(names).not.toContain("dbx_list_tables");
expect(names).toEqual(expect.arrayContaining(MCP_TOOL_OPTIONS.map((tool) => tool.name).filter((name) => name !== "dbx_list_tables")));
});
it("keeps the explicit list exact once materialized", () => {
const seeded = toggleMcpAllowedToolName(null, "dbx_list_tables", true);
const withoutWildcard = toggleMcpAllowedToolName(seeded, MCP_PLUGIN_TOOLS_WILDCARD, false);
expect(withoutWildcard).not.toContain(MCP_PLUGIN_TOOLS_WILDCARD);
expect(withoutWildcard).not.toContain("dbx_kafka__kafka_topics_delete");
expect(withoutWildcard).toContain("dbx_list_tables");
});
it("adds trimmed custom entries without duplicates", () => {
const seeded = toggleMcpAllowedToolName(null, "dbx_list_tables", true);
const first = addMcpAllowedToolName(seeded, " dbx_ssh__* ");
expect(first.added).toBe(true);
expect(first.names).toContain("dbx_ssh__*");
const second = addMcpAllowedToolName(first.names, "dbx_ssh__*");
expect(second.added).toBe(false);
expect(second.names).toEqual(first.names);
});
it("ignores empty custom entries", () => {
const result = addMcpAllowedToolName(["dbx_ssh__*"], " ");
expect(result.added).toBe(false);
expect(result.names).toEqual(["dbx_ssh__*"]);
});
it("reports custom entries beyond the static options", () => {
const names = toggleMcpAllowedToolName(null, "dbx_list_tables", true);
expect(customMcpAllowedToolNames(names)).toEqual([MCP_PLUGIN_TOOLS_WILDCARD]);
expect(customMcpAllowedToolNames(null)).toEqual([]);
});
});
+29 -1
View File
@@ -121,13 +121,41 @@ export function toggleMcpAllowedConnectionId(current: readonly string[] | null,
return updateMcpAllowedConnectionIds(current, availableConnectionIds, [connectionId], allowed);
}
/**
* Allowlist entry that exposes every discovered plugin tool (`dbx_<prefix>__*`
* scopes to one plugin). Plugin tool names come from sidecars at runtime, so
* the static options above cannot name them; the backend matcher expands the
* wildcard, and static tool names never contain the `__` separator.
*/
export const MCP_PLUGIN_TOOLS_WILDCARD = "dbx_*__*";
export function toggleMcpAllowedToolName(current: readonly string[] | null, toolName: string, allowed: boolean): string[] {
const selected = new Set(current === null ? MCP_TOOL_OPTIONS.map((tool) => tool.name) : current);
// A null allowlist means "everything", including every discovered plugin
// tool. The first toggle materializes the list: seed the plugin wildcard so
// checking one static tool does not silently strip every dynamic plugin
// tool — removing the wildcard is an explicit settings action.
const selected = new Set<string>(current === null ? [...MCP_TOOL_OPTIONS.map((tool) => tool.name), MCP_PLUGIN_TOOLS_WILDCARD] : current);
if (allowed) selected.add(toolName);
else selected.delete(toolName);
return [...selected];
}
/** Adds a free-form allowlist entry (a plugin tool name or a `dbx_<prefix>__*` wildcard). */
export function addMcpAllowedToolName(current: readonly string[] | null, rawName: string): { names: string[]; added: boolean } {
const name = rawName.trim();
if (!name) return { names: [...(current ?? MCP_TOOL_OPTIONS.map((tool) => tool.name))], added: false };
const selected = new Set<string>(current === null ? MCP_TOOL_OPTIONS.map((tool) => tool.name) : current);
const added = !selected.has(name);
if (added) selected.add(name);
return { names: [...selected], added };
}
/** Saved allowlist entries beyond the static options: plugin tool names and wildcards. */
export function customMcpAllowedToolNames(current: readonly string[] | null): string[] {
const staticNames = new Set<string>(MCP_TOOL_OPTIONS.map((tool) => tool.name));
return (current ?? []).filter((name) => !staticNames.has(name));
}
export function updateMcpAllowedConnectionIds(current: readonly string[] | null, availableConnectionIds: readonly string[], connectionIds: readonly string[], allowed: boolean): string[] {
const selected = new Set(current === null ? availableConnectionIds : current);
for (const connectionId of connectionIds) {
+45 -1
View File
@@ -4073,7 +4073,19 @@ fn policy_allows_connection(
}
fn policy_allows_tool(policy: &McpGlobalPolicy, tool_name: &str) -> bool {
policy.allowed_tool_names.as_ref().is_none_or(|allowed| allowed.iter().any(|name| name == tool_name))
let Some(allowed) = policy.allowed_tool_names.as_ref() else { return true };
if allowed.iter().any(|name| name == tool_name) {
return true;
}
// Plugin wildcard entries: plugin tool names are discovered from sidecars
// at runtime, so a static settings list cannot name them individually —
// `dbx_<prefix>__*` exposes every tool of one plugin, `dbx_*__*` every
// plugin. Static tools never contain the `__` separator (guarded by
// `is_plugin_tool_name`), so a wildcard cannot widen their access.
let Some((prefix, _)) = tool_name.strip_prefix("dbx_").and_then(|rest| rest.split_once("__")) else {
return false;
};
allowed.iter().any(|name| name == "dbx_*__*" || name == &format!("dbx_{prefix}__*"))
}
fn database_scope_for_connection(
@@ -5723,6 +5735,38 @@ mod tests {
assert!(result_text(&unknown).contains("does not contribute MCP tools"), "{}", result_text(&unknown));
}
#[test]
fn plugin_tool_wildcards_match_only_the_plugin_namespace() {
let per_plugin =
McpGlobalPolicy { allowed_tool_names: Some(vec!["dbx_ssh__*".to_string()]), ..Default::default() };
assert!(policy_allows_tool(&per_plugin, "dbx_ssh__sftp_list_dir"));
assert!(policy_allows_tool(&per_plugin, "dbx_ssh__sftp_write_file"));
assert!(!policy_allows_tool(&per_plugin, "dbx_kafka__kafka_topics_list"));
assert!(
!policy_allows_tool(&per_plugin, "dbx_execute_query"),
"a plugin wildcard must never widen a static tool"
);
let every_plugin = McpGlobalPolicy {
allowed_tool_names: Some(vec!["dbx_plugin_list".to_string(), "dbx_*__*".to_string()]),
..Default::default()
};
assert!(policy_allows_tool(&every_plugin, "dbx_kafka__kafka_topics_delete"));
assert!(policy_allows_tool(&every_plugin, "dbx_ssh__sftp_list_dir"));
assert!(policy_allows_tool(&every_plugin, "dbx_plugin_list"), "exact entries keep working beside the wildcard");
assert!(!policy_allows_tool(&every_plugin, "dbx_execute_query"));
let explicit = McpGlobalPolicy {
allowed_tool_names: Some(vec!["dbx_ssh__sftp_list_dir".to_string()]),
..Default::default()
};
assert!(policy_allows_tool(&explicit, "dbx_ssh__sftp_list_dir"));
assert!(
!policy_allows_tool(&explicit, "dbx_ssh__sftp_write_file"),
"no wildcard means per-tool gating stays exact"
);
}
#[tokio::test]
async fn plugin_call_is_gated_by_the_flat_tool_allowlist() {
// The meta tool itself is allowed, but the flat name of the targeted