@@ -61,47 +61,32 @@ def test_advertised_scopes_use_cache_without_network(self):
6161 scopes = asyncio .run (auth .protected_resource_metadata ())["scopes_supported" ]
6262 finally :
6363 auth ._discovery_cache .pop (pid , None )
64- # None of the preferred scopes exist, so the full discovered list is
65- # mirrored (custom scope catalogs on self-hosted projects).
64+ # No curated list configured, so the discovered catalog is mirrored.
6665 self .assertEqual (scopes , ["rows.read" , "rows.write" ])
6766
68- def test_advertised_scopes_prefer_curated_subset (self ):
69- # The Cloud console authorization server advertises a very large
70- # fine-grained scope catalog; the MCP must advertise only the compact
71- # preferred set (clients request every advertised scope, and the
72- # authorize endpoint caps the scope parameter length).
67+ def test_advertised_scopes_mirror_full_catalog_by_default (self ):
68+ # The MCP mirrors the authorization server's full scope catalog so
69+ # clients request everything and consent-time narrowing is the control
70+ # point (granular MCP scopes design).
71+ catalog = [
72+ "openid" ,
73+ "profile" ,
74+ "email" ,
75+ "phone" ,
76+ "all" ,
77+ "project:all" ,
78+ "organization:all" ,
79+ "project:users.read" ,
80+ "project:users.write" ,
81+ "organization:projects.read" ,
82+ ]
7383 pid = auth .configured_project_id ()
74- auth ._store_discovery (
75- pid ,
76- discovery_doc (
77- [
78- "openid" ,
79- "profile" ,
80- "email" ,
81- "phone" ,
82- "all" ,
83- "project:all" ,
84- "organization:all" ,
85- "project:users.read" ,
86- "project:users.write" ,
87- "organization:projects.read" ,
88- ]
89- ),
90- )
91- try :
92- scopes = asyncio .run (auth .protected_resource_metadata ())["scopes_supported" ]
93- finally :
94- auth ._discovery_cache .pop (pid , None )
95- self .assertEqual (scopes , ["openid" , "profile" , "email" , "all" ])
96-
97- def test_advertised_scopes_drop_preferred_scopes_missing_from_discovery (self ):
98- pid = auth .configured_project_id ()
99- auth ._store_discovery (pid , discovery_doc (["openid" , "email" , "all" ]))
84+ auth ._store_discovery (pid , discovery_doc (catalog ))
10085 try :
10186 scopes = asyncio .run (auth .protected_resource_metadata ())["scopes_supported" ]
10287 finally :
10388 auth ._discovery_cache .pop (pid , None )
104- self .assertEqual (scopes , [ "openid" , "email" , "all" ] )
89+ self .assertEqual (scopes , catalog )
10590
10691 def test_advertised_scopes_env_override (self ):
10792 pid = auth .configured_project_id ()
@@ -119,6 +104,46 @@ def test_advertised_scopes_env_override(self):
119104 auth ._discovery_cache .pop (pid , None )
120105 self .assertEqual (scopes , ["openid" , "project:all" ])
121106
107+ def test_advertised_scopes_env_override_drops_undiscovered_scopes (self ):
108+ # A curated scope missing from the live catalog is never advertised.
109+ pid = auth .configured_project_id ()
110+ auth ._store_discovery (pid , discovery_doc (["openid" , "email" , "all" ]))
111+ try :
112+ with mock .patch .dict (
113+ os .environ , {"MCP_OAUTH_SCOPES" : "openid email all phone" }
114+ ):
115+ scopes = asyncio .run (auth .protected_resource_metadata ())[
116+ "scopes_supported"
117+ ]
118+ finally :
119+ auth ._discovery_cache .pop (pid , None )
120+ self .assertEqual (scopes , ["openid" , "email" , "all" ])
121+
122+ def test_advertised_scopes_env_override_falls_back_to_full_catalog (self ):
123+ # When none of the curated scopes exist in the live catalog, the full
124+ # discovered list is mirrored instead of advertising nothing.
125+ pid = auth .configured_project_id ()
126+ auth ._store_discovery (pid , discovery_doc (["rows.read" , "rows.write" ]))
127+ try :
128+ with mock .patch .dict (os .environ , {"MCP_OAUTH_SCOPES" : "openid all" }):
129+ scopes = asyncio .run (auth .protected_resource_metadata ())[
130+ "scopes_supported"
131+ ]
132+ finally :
133+ auth ._discovery_cache .pop (pid , None )
134+ self .assertEqual (scopes , ["rows.read" , "rows.write" ])
135+
136+ def test_advertised_scopes_raise_when_discovery_missing_scopes_supported (self ):
137+ pid = auth .configured_project_id ()
138+ doc = discovery_doc ([])
139+ del doc ["scopes_supported" ]
140+ auth ._store_discovery (pid , doc )
141+ try :
142+ with self .assertRaises (ValueError ):
143+ asyncio .run (auth .protected_resource_metadata ())
144+ finally :
145+ auth ._discovery_cache .pop (pid , None )
146+
122147 def test_discovery_cache_expires_after_ttl (self ):
123148 pid = auth .configured_project_id ()
124149 auth ._store_discovery (pid , {"issuer" : "x" , "jwks_uri" : "y" })
0 commit comments