Enforce an explicit read-only tool allowlist
Remove every tool that is not explicitly approved for the private Buzz agent. This keeps future mutating tools closed by default instead of relying on a denylist that must be updated manually. Co-authored-by: fredamn76 <fredrik.fallman@gmail.com> Signed-off-by: fredamn76 <fredrik.fallman@gmail.com>
This commit is contained in:
+15
-12
@@ -1,6 +1,7 @@
|
|||||||
"""Small, confirmation-friendly MCP facade over the Mealie API."""
|
"""Small, confirmation-friendly MCP facade over the Mealie API."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
import html
|
import html
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
@@ -38,16 +39,17 @@ RESOURCE_DIR = Path(__file__).parent / "resources"
|
|||||||
|
|
||||||
mcp = MCPServer("mealie", version="0.1.0")
|
mcp = MCPServer("mealie", version="0.1.0")
|
||||||
|
|
||||||
WRITE_TOOL_NAMES = frozenset(
|
READ_ONLY_TOOL_NAMES = frozenset(
|
||||||
{
|
{
|
||||||
"import_recipe_url",
|
"check_auth",
|
||||||
"import_recipe_text",
|
"find_by_source_url",
|
||||||
"import_recipe_image",
|
"get_recipe",
|
||||||
"patch_recipe",
|
"list_organizers",
|
||||||
"delete_recipe",
|
"resolve_foods",
|
||||||
"parse_ingredients",
|
"scale_ingredients",
|
||||||
"set_cover_image",
|
"search_recipes",
|
||||||
"shopping_list_add",
|
"suggest_recipes",
|
||||||
|
"verify_recipe",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -762,10 +764,11 @@ def main() -> None:
|
|||||||
|
|
||||||
def _configure_read_only(
|
def _configure_read_only(
|
||||||
server: MCPServer,
|
server: MCPServer,
|
||||||
write_tool_names: frozenset[str] = WRITE_TOOL_NAMES,
|
allowed_tool_names: frozenset[str] = READ_ONLY_TOOL_NAMES,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Remove mutating tools before exposing the server to a read-only client."""
|
"""Expose only explicitly approved tools to a read-only client."""
|
||||||
for name in sorted(write_tool_names):
|
registered_tool_names = {tool.name for tool in asyncio.run(server.list_tools())}
|
||||||
|
for name in sorted(registered_tool_names - allowed_tool_names):
|
||||||
server.remove_tool(name)
|
server.remove_tool(name)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -31,19 +31,35 @@ def test_read_only_server_removes_all_mutating_tools():
|
|||||||
def search_recipes(query: str) -> list[str]:
|
def search_recipes(query: str) -> list[str]:
|
||||||
return [query]
|
return [query]
|
||||||
|
|
||||||
def mutating_tool(value: str) -> str:
|
@test_server.tool()
|
||||||
|
def patch_recipe(value: str) -> str:
|
||||||
return value
|
return value
|
||||||
|
|
||||||
for name in server.WRITE_TOOL_NAMES:
|
@test_server.tool()
|
||||||
test_server.tool(name=name)(mutating_tool)
|
def future_mutating_tool(value: str) -> str:
|
||||||
|
return value
|
||||||
|
|
||||||
server._configure_read_only(test_server)
|
server._configure_read_only(test_server, frozenset({"search_recipes"}))
|
||||||
|
|
||||||
tool_names = {tool.name for tool in asyncio.run(test_server.list_tools())}
|
tool_names = {tool.name for tool in asyncio.run(test_server.list_tools())}
|
||||||
|
|
||||||
assert tool_names == {"search_recipes"}
|
assert tool_names == {"search_recipes"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_read_only_tool_allowlist_is_explicit():
|
||||||
|
assert server.READ_ONLY_TOOL_NAMES == {
|
||||||
|
"check_auth",
|
||||||
|
"find_by_source_url",
|
||||||
|
"get_recipe",
|
||||||
|
"list_organizers",
|
||||||
|
"resolve_foods",
|
||||||
|
"scale_ingredients",
|
||||||
|
"search_recipes",
|
||||||
|
"suggest_recipes",
|
||||||
|
"verify_recipe",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class TestUrlImportResponse:
|
class TestUrlImportResponse:
|
||||||
def test_slug_only_import_response_is_resolved_before_reporting(self, monkeypatch):
|
def test_slug_only_import_response_is_resolved_before_reporting(self, monkeypatch):
|
||||||
recipe = {"slug": "lax-med-citron", "name": "Lax med citron", "recipeIngredient": []}
|
recipe = {"slug": "lax-med-citron", "name": "Lax med citron", "recipeIngredient": []}
|
||||||
|
|||||||
Reference in New Issue
Block a user