@@ -21,24 +21,34 @@ def _fake_mcp(monkeypatch):
2121 class FakeMCP :
2222 def __init__ (self , * _args , ** _kwargs ):
2323 self .tools = {}
24+ self .tool_annotations = {}
2425
25- def tool (self ):
26+ def tool (self , ** kwargs ):
2627 def register (fn ):
2728 self .tools [fn .__name__ ] = fn
29+ self .tool_annotations [fn .__name__ ] = kwargs .get ("annotations" )
2830 return fn
2931
3032 return register
3133
34+ class FakeToolAnnotations :
35+ def __init__ (self , ** kwargs ):
36+ self .__dict__ .update (kwargs )
37+
3238 fastmcp_module .FastMCP = FakeMCP
39+ types_module = ModuleType ("mcp.types" )
40+ types_module .ToolAnnotations = FakeToolAnnotations
3341 mcp_module = ModuleType ("mcp" )
3442 mcp_module .__path__ = []
3543 server_module = ModuleType ("mcp.server" )
3644 server_module .__path__ = []
3745 mcp_module .server = server_module
46+ mcp_module .types = types_module
3847 server_module .fastmcp = fastmcp_module
3948 monkeypatch .setitem (sys .modules , "mcp" , mcp_module )
4049 monkeypatch .setitem (sys .modules , "mcp.server" , server_module )
4150 monkeypatch .setitem (sys .modules , "mcp.server.fastmcp" , fastmcp_module )
51+ monkeypatch .setitem (sys .modules , "mcp.types" , types_module )
4252
4353 class FakeCore :
4454 def __init__ (self ):
@@ -67,6 +77,31 @@ def get_status(self, run_id):
6777 return build_mcp (core = core ), core
6878
6979
80+ def test_tool_annotations_mark_the_read_only_surface (monkeypatch ):
81+ """Approval-gating hosts (codex) read these hints; keep them truthful.
82+
83+ Unannotated tools trigger a per-call approval dialog in codex and are
84+ auto-cancelled in its headless runs (#86), so every tool declares its
85+ hints: `review` is the only tool that mutates state (reserves a Strict
86+ run) or reaches the network; everything else only reads.
87+ """
88+ mcp , _core = _fake_mcp (monkeypatch )
89+
90+ assert set (mcp .tool_annotations ) == set (mcp .tools )
91+ review = mcp .tool_annotations ["review" ]
92+ assert review is not None
93+ assert review .readOnlyHint is False
94+ assert review .destructiveHint is False
95+ assert review .openWorldHint is True
96+ for name in ("validate_direct_review" , "get_review_result" ,
97+ "get_review_status" , "update_knowledge" , "doc_search" ,
98+ "doc_read" ):
99+ hints = mcp .tool_annotations [name ]
100+ assert hints is not None , name
101+ assert hints .readOnlyHint is True , name
102+ assert hints .openWorldHint is False , name
103+
104+
70105def test_direct_entrypoints_do_not_resolve_repo (monkeypatch ):
71106 mcp , core = _fake_mcp (monkeypatch )
72107 assert set (mcp .tools ) == {
0 commit comments