gopher-mcp-python 0.1.34__tar.gz → 0.1.38__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {gopher_mcp_python-0.1.34 → gopher_mcp_python-0.1.38}/PKG-INFO +41 -1
- {gopher_mcp_python-0.1.34 → gopher_mcp_python-0.1.38}/README.md +40 -0
- {gopher_mcp_python-0.1.34 → gopher_mcp_python-0.1.38}/gopher_mcp_python/__init__.py +13 -1
- {gopher_mcp_python-0.1.34 → gopher_mcp_python-0.1.38}/gopher_mcp_python/agent.py +23 -7
- {gopher_mcp_python-0.1.34 → gopher_mcp_python-0.1.38}/gopher_mcp_python/config.py +24 -12
- gopher_mcp_python-0.1.38/gopher_mcp_python/elicitation.py +120 -0
- gopher_mcp_python-0.1.38/gopher_mcp_python/elicitation_runtime.py +229 -0
- {gopher_mcp_python-0.1.34 → gopher_mcp_python-0.1.38}/gopher_mcp_python/ffi/library.py +289 -24
- gopher_mcp_python-0.1.38/gopher_mcp_python/gateway_elicitation_preflight.py +316 -0
- {gopher_mcp_python-0.1.34 → gopher_mcp_python-0.1.38}/gopher_mcp_python/oauth_resolver.py +1 -0
- {gopher_mcp_python-0.1.34 → gopher_mcp_python-0.1.38}/gopher_mcp_python/oauth_runtime_options.py +1 -0
- {gopher_mcp_python-0.1.34 → gopher_mcp_python-0.1.38}/gopher_mcp_python/runtime_options.py +55 -8
- {gopher_mcp_python-0.1.34 → gopher_mcp_python-0.1.38}/gopher_mcp_python.egg-info/PKG-INFO +41 -1
- {gopher_mcp_python-0.1.34 → gopher_mcp_python-0.1.38}/gopher_mcp_python.egg-info/SOURCES.txt +10 -0
- {gopher_mcp_python-0.1.34 → gopher_mcp_python-0.1.38}/pyproject.toml +1 -1
- {gopher_mcp_python-0.1.34 → gopher_mcp_python-0.1.38}/tests/test_agent_create_with_oauth.py +67 -9
- {gopher_mcp_python-0.1.34 → gopher_mcp_python-0.1.38}/tests/test_agent_runtime_options.py +10 -1
- {gopher_mcp_python-0.1.34 → gopher_mcp_python-0.1.38}/tests/test_config.py +21 -0
- gopher_mcp_python-0.1.38/tests/test_custom_oauth_test_idp.py +186 -0
- gopher_mcp_python-0.1.38/tests/test_custom_protected_mcp_endpoints.py +173 -0
- gopher_mcp_python-0.1.38/tests/test_elicitation_runtime.py +157 -0
- {gopher_mcp_python-0.1.34 → gopher_mcp_python-0.1.38}/tests/test_ffi_runtime_options.py +106 -0
- gopher_mcp_python-0.1.38/tests/test_gateway_elicitation_preflight.py +117 -0
- {gopher_mcp_python-0.1.34 → gopher_mcp_python-0.1.38}/tests/test_library_search_paths.py +15 -2
- {gopher_mcp_python-0.1.34 → gopher_mcp_python-0.1.38}/tests/test_linux_native_packaging.py +34 -12
- gopher_mcp_python-0.1.38/tests/test_oauth_auto_custom_idp.py +155 -0
- gopher_mcp_python-0.1.38/tests/test_oauth_auto_custom_idp_failures.py +285 -0
- {gopher_mcp_python-0.1.34 → gopher_mcp_python-0.1.38}/tests/test_oauth_resolver.py +5 -0
- {gopher_mcp_python-0.1.34 → gopher_mcp_python-0.1.38}/tests/test_oauth_runtime_options.py +6 -1
- gopher_mcp_python-0.1.38/tests/test_oauth_test_token_helper.py +184 -0
- {gopher_mcp_python-0.1.34 → gopher_mcp_python-0.1.38}/LICENSE +0 -0
- {gopher_mcp_python-0.1.34 → gopher_mcp_python-0.1.38}/gopher_mcp_python/auth/__init__.py +0 -0
- {gopher_mcp_python-0.1.34 → gopher_mcp_python-0.1.38}/gopher_mcp_python/auth/errors.py +0 -0
- {gopher_mcp_python-0.1.34 → gopher_mcp_python-0.1.38}/gopher_mcp_python/auth/gopher_auth.py +0 -0
- {gopher_mcp_python-0.1.34 → gopher_mcp_python-0.1.38}/gopher_mcp_python/auth/scope_helpers.py +0 -0
- {gopher_mcp_python-0.1.34 → gopher_mcp_python-0.1.38}/gopher_mcp_python/errors.py +0 -0
- {gopher_mcp_python-0.1.34 → gopher_mcp_python-0.1.38}/gopher_mcp_python/ffi/__init__.py +0 -0
- {gopher_mcp_python-0.1.34 → gopher_mcp_python-0.1.38}/gopher_mcp_python/ffi/auth/__init__.py +0 -0
- {gopher_mcp_python-0.1.34 → gopher_mcp_python-0.1.38}/gopher_mcp_python/ffi/auth/auth_client.py +0 -0
- {gopher_mcp_python-0.1.34 → gopher_mcp_python-0.1.38}/gopher_mcp_python/ffi/auth/auto_refresh.py +0 -0
- {gopher_mcp_python-0.1.34 → gopher_mcp_python-0.1.38}/gopher_mcp_python/ffi/auth/config_loader.py +0 -0
- {gopher_mcp_python-0.1.34 → gopher_mcp_python-0.1.38}/gopher_mcp_python/ffi/auth/loader.py +0 -0
- {gopher_mcp_python-0.1.34 → gopher_mcp_python-0.1.38}/gopher_mcp_python/ffi/auth/oauth_client.py +0 -0
- {gopher_mcp_python-0.1.34 → gopher_mcp_python-0.1.38}/gopher_mcp_python/ffi/auth/session_manager.py +0 -0
- {gopher_mcp_python-0.1.34 → gopher_mcp_python-0.1.38}/gopher_mcp_python/ffi/auth/types.py +0 -0
- {gopher_mcp_python-0.1.34 → gopher_mcp_python-0.1.38}/gopher_mcp_python/ffi/auth/validation_options.py +0 -0
- {gopher_mcp_python-0.1.34 → gopher_mcp_python-0.1.38}/gopher_mcp_python/oauth_authorization_url.py +0 -0
- {gopher_mcp_python-0.1.34 → gopher_mcp_python-0.1.38}/gopher_mcp_python/oauth_browser.py +0 -0
- {gopher_mcp_python-0.1.34 → gopher_mcp_python-0.1.38}/gopher_mcp_python/oauth_discovery.py +0 -0
- {gopher_mcp_python-0.1.34 → gopher_mcp_python-0.1.38}/gopher_mcp_python/oauth_loopback.py +0 -0
- {gopher_mcp_python-0.1.34 → gopher_mcp_python-0.1.38}/gopher_mcp_python/oauth_pkce.py +0 -0
- {gopher_mcp_python-0.1.34 → gopher_mcp_python-0.1.38}/gopher_mcp_python/oauth_registration.py +0 -0
- {gopher_mcp_python-0.1.34 → gopher_mcp_python-0.1.38}/gopher_mcp_python/oauth_server_targets.py +0 -0
- {gopher_mcp_python-0.1.34 → gopher_mcp_python-0.1.38}/gopher_mcp_python/oauth_token_exchange.py +0 -0
- {gopher_mcp_python-0.1.34 → gopher_mcp_python-0.1.38}/gopher_mcp_python/oauth_token_store.py +0 -0
- {gopher_mcp_python-0.1.34 → gopher_mcp_python-0.1.38}/gopher_mcp_python/result.py +0 -0
- {gopher_mcp_python-0.1.34 → gopher_mcp_python-0.1.38}/gopher_mcp_python/server_config.py +0 -0
- {gopher_mcp_python-0.1.34 → gopher_mcp_python-0.1.38}/gopher_mcp_python.egg-info/dependency_links.txt +0 -0
- {gopher_mcp_python-0.1.34 → gopher_mcp_python-0.1.38}/gopher_mcp_python.egg-info/requires.txt +0 -0
- {gopher_mcp_python-0.1.34 → gopher_mcp_python-0.1.38}/gopher_mcp_python.egg-info/top_level.txt +0 -0
- {gopher_mcp_python-0.1.34 → gopher_mcp_python-0.1.38}/setup.cfg +0 -0
- {gopher_mcp_python-0.1.34 → gopher_mcp_python-0.1.38}/setup.py +0 -0
- {gopher_mcp_python-0.1.34 → gopher_mcp_python-0.1.38}/tests/test_agent_create_by.py +0 -0
- {gopher_mcp_python-0.1.34 → gopher_mcp_python-0.1.38}/tests/test_agent_error_message.py +0 -0
- {gopher_mcp_python-0.1.34 → gopher_mcp_python-0.1.38}/tests/test_agent_lifecycle.py +0 -0
- {gopher_mcp_python-0.1.34 → gopher_mcp_python-0.1.38}/tests/test_auth_exports.py +0 -0
- {gopher_mcp_python-0.1.34 → gopher_mcp_python-0.1.38}/tests/test_dump_version.py +0 -0
- {gopher_mcp_python-0.1.34 → gopher_mcp_python-0.1.38}/tests/test_ffi.py +0 -0
- {gopher_mcp_python-0.1.34 → gopher_mcp_python-0.1.38}/tests/test_gopher_auth.py +0 -0
- {gopher_mcp_python-0.1.34 → gopher_mcp_python-0.1.38}/tests/test_header_create_by_url_example.py +0 -0
- {gopher_mcp_python-0.1.34 → gopher_mcp_python-0.1.38}/tests/test_oauth_authorization_url.py +0 -0
- {gopher_mcp_python-0.1.34 → gopher_mcp_python-0.1.38}/tests/test_oauth_browser.py +0 -0
- {gopher_mcp_python-0.1.34 → gopher_mcp_python-0.1.38}/tests/test_oauth_create_with_url_integration.py +0 -0
- {gopher_mcp_python-0.1.34 → gopher_mcp_python-0.1.38}/tests/test_oauth_discovery.py +0 -0
- {gopher_mcp_python-0.1.34 → gopher_mcp_python-0.1.38}/tests/test_oauth_loopback.py +0 -0
- {gopher_mcp_python-0.1.34 → gopher_mcp_python-0.1.38}/tests/test_oauth_pkce.py +0 -0
- {gopher_mcp_python-0.1.34 → gopher_mcp_python-0.1.38}/tests/test_oauth_registration.py +0 -0
- {gopher_mcp_python-0.1.34 → gopher_mcp_python-0.1.38}/tests/test_oauth_server_targets.py +0 -0
- {gopher_mcp_python-0.1.34 → gopher_mcp_python-0.1.38}/tests/test_oauth_token_exchange.py +0 -0
- {gopher_mcp_python-0.1.34 → gopher_mcp_python-0.1.38}/tests/test_oauth_token_store.py +0 -0
- {gopher_mcp_python-0.1.34 → gopher_mcp_python-0.1.38}/tests/test_result.py +0 -0
- {gopher_mcp_python-0.1.34 → gopher_mcp_python-0.1.38}/tests/test_server_config.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gopher-mcp-python
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.38
|
|
4
4
|
Summary: Python SDK for Gopher MCP - AI Agent orchestration framework with native performance
|
|
5
5
|
Author-email: Gopher Security <dev@gophersecurity.com>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -219,6 +219,42 @@ Result class with status and metadata.
|
|
|
219
219
|
- `result.is_error()` - Check if error
|
|
220
220
|
- `result.is_timeout()` - Check if timeout
|
|
221
221
|
|
|
222
|
+
### Provider OAuth Elicitation
|
|
223
|
+
|
|
224
|
+
Provider OAuth requested during a tool call is handled automatically by default.
|
|
225
|
+
For URL-mode elicitation, the SDK opens the authorization URL when possible,
|
|
226
|
+
prints it as a fallback, waits for the user to finish the browser flow, and then
|
|
227
|
+
lets the native agent retry the tool call.
|
|
228
|
+
|
|
229
|
+
No example-level elicitation configuration is required:
|
|
230
|
+
|
|
231
|
+
```python
|
|
232
|
+
agent = GopherAgent.create_with_url(provider, model, mcp_url)
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Use `GopherAgentCreateOptions` only when you need a custom synchronous handler
|
|
236
|
+
or want to disable browser opening while still printing the URL:
|
|
237
|
+
|
|
238
|
+
```python
|
|
239
|
+
from gopher_mcp_python import GopherAgentCreateOptions
|
|
240
|
+
|
|
241
|
+
agent = GopherAgent.create_with_url(
|
|
242
|
+
provider,
|
|
243
|
+
model,
|
|
244
|
+
mcp_url,
|
|
245
|
+
GopherAgentCreateOptions(
|
|
246
|
+
elicitation={
|
|
247
|
+
"open_browser": False,
|
|
248
|
+
"handler": lambda request: "accept",
|
|
249
|
+
},
|
|
250
|
+
),
|
|
251
|
+
)
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
First-step MCP server OAuth is controlled separately by the `oauth` create
|
|
255
|
+
option. Setting `oauth={"mode": "disabled"}` skips SDK OAuth discovery for the
|
|
256
|
+
MCP endpoint but still leaves provider elicitation enabled.
|
|
257
|
+
|
|
222
258
|
### Exceptions
|
|
223
259
|
|
|
224
260
|
- `AgentError` - Base exception for agent errors
|
|
@@ -234,6 +270,10 @@ Result class with status and metadata.
|
|
|
234
270
|
pytest
|
|
235
271
|
```
|
|
236
272
|
|
|
273
|
+
For deterministic OAuth auto verification with a local custom IdP and local MCP
|
|
274
|
+
server/gateway endpoints, see
|
|
275
|
+
[`docs/oauth-auto-custom-idp.md`](docs/oauth-auto-custom-idp.md).
|
|
276
|
+
|
|
237
277
|
### Code Formatting
|
|
238
278
|
|
|
239
279
|
This project uses Black for code formatting and Ruff for linting.
|
|
@@ -175,6 +175,42 @@ Result class with status and metadata.
|
|
|
175
175
|
- `result.is_error()` - Check if error
|
|
176
176
|
- `result.is_timeout()` - Check if timeout
|
|
177
177
|
|
|
178
|
+
### Provider OAuth Elicitation
|
|
179
|
+
|
|
180
|
+
Provider OAuth requested during a tool call is handled automatically by default.
|
|
181
|
+
For URL-mode elicitation, the SDK opens the authorization URL when possible,
|
|
182
|
+
prints it as a fallback, waits for the user to finish the browser flow, and then
|
|
183
|
+
lets the native agent retry the tool call.
|
|
184
|
+
|
|
185
|
+
No example-level elicitation configuration is required:
|
|
186
|
+
|
|
187
|
+
```python
|
|
188
|
+
agent = GopherAgent.create_with_url(provider, model, mcp_url)
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Use `GopherAgentCreateOptions` only when you need a custom synchronous handler
|
|
192
|
+
or want to disable browser opening while still printing the URL:
|
|
193
|
+
|
|
194
|
+
```python
|
|
195
|
+
from gopher_mcp_python import GopherAgentCreateOptions
|
|
196
|
+
|
|
197
|
+
agent = GopherAgent.create_with_url(
|
|
198
|
+
provider,
|
|
199
|
+
model,
|
|
200
|
+
mcp_url,
|
|
201
|
+
GopherAgentCreateOptions(
|
|
202
|
+
elicitation={
|
|
203
|
+
"open_browser": False,
|
|
204
|
+
"handler": lambda request: "accept",
|
|
205
|
+
},
|
|
206
|
+
),
|
|
207
|
+
)
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
First-step MCP server OAuth is controlled separately by the `oauth` create
|
|
211
|
+
option. Setting `oauth={"mode": "disabled"}` skips SDK OAuth discovery for the
|
|
212
|
+
MCP endpoint but still leaves provider elicitation enabled.
|
|
213
|
+
|
|
178
214
|
### Exceptions
|
|
179
215
|
|
|
180
216
|
- `AgentError` - Base exception for agent errors
|
|
@@ -190,6 +226,10 @@ Result class with status and metadata.
|
|
|
190
226
|
pytest
|
|
191
227
|
```
|
|
192
228
|
|
|
229
|
+
For deterministic OAuth auto verification with a local custom IdP and local MCP
|
|
230
|
+
server/gateway endpoints, see
|
|
231
|
+
[`docs/oauth-auto-custom-idp.md`](docs/oauth-auto-custom-idp.md).
|
|
232
|
+
|
|
193
233
|
### Code Formatting
|
|
194
234
|
|
|
195
235
|
This project uses Black for code formatting and Ruff for linting.
|
|
@@ -29,6 +29,13 @@ from gopher_mcp_python.config import (
|
|
|
29
29
|
GopherAgentConfig,
|
|
30
30
|
GopherAgentConfigBuilder,
|
|
31
31
|
)
|
|
32
|
+
from gopher_mcp_python.elicitation import (
|
|
33
|
+
GopherAgentElicitationAction,
|
|
34
|
+
GopherAgentElicitationHandler,
|
|
35
|
+
GopherAgentElicitationOptions,
|
|
36
|
+
GopherAgentElicitationRequest,
|
|
37
|
+
GopherAgentElicitationResponse,
|
|
38
|
+
)
|
|
32
39
|
from gopher_mcp_python.runtime_options import (
|
|
33
40
|
GopherAgentCreateOptions,
|
|
34
41
|
GopherAgentOAuthOptions,
|
|
@@ -46,7 +53,7 @@ from gopher_mcp_python.errors import (
|
|
|
46
53
|
from gopher_mcp_python.server_config import ServerConfig, ServerConfigRoute
|
|
47
54
|
from gopher_mcp_python.ffi import GopherOrchLibrary, GopherOrchHandle
|
|
48
55
|
|
|
49
|
-
__version__ = "0.1.
|
|
56
|
+
__version__ = "0.1.38"
|
|
50
57
|
|
|
51
58
|
_AUTH_EXPORTS = {
|
|
52
59
|
"GopherAuth",
|
|
@@ -128,6 +135,11 @@ __all__ = [
|
|
|
128
135
|
"GopherAgentConfig",
|
|
129
136
|
"GopherAgentConfigBuilder",
|
|
130
137
|
"GopherAgentCreateOptions",
|
|
138
|
+
"GopherAgentElicitationAction",
|
|
139
|
+
"GopherAgentElicitationHandler",
|
|
140
|
+
"GopherAgentElicitationOptions",
|
|
141
|
+
"GopherAgentElicitationRequest",
|
|
142
|
+
"GopherAgentElicitationResponse",
|
|
131
143
|
"GopherAgentOAuthOptions",
|
|
132
144
|
"GopherAgentRuntimeOptions",
|
|
133
145
|
"GopherAgentTokenRecord",
|
|
@@ -31,9 +31,13 @@ import asyncio
|
|
|
31
31
|
import weakref
|
|
32
32
|
from typing import Callable, Optional
|
|
33
33
|
|
|
34
|
+
from gopher_mcp_python.gateway_elicitation_preflight import (
|
|
35
|
+
preflight_gateway_elicitation,
|
|
36
|
+
)
|
|
34
37
|
import gopher_mcp_python.oauth_resolver as oauth_resolver
|
|
35
38
|
from gopher_mcp_python.config import GopherAgentConfig
|
|
36
39
|
from gopher_mcp_python.runtime_options import (
|
|
40
|
+
GopherAgentCreateOptions,
|
|
37
41
|
GopherAgentOAuthOptions,
|
|
38
42
|
GopherAgentRuntimeOptions,
|
|
39
43
|
RuntimeOptionsInput,
|
|
@@ -159,7 +163,7 @@ class GopherAgent:
|
|
|
159
163
|
Returns:
|
|
160
164
|
GopherAgent instance
|
|
161
165
|
"""
|
|
162
|
-
create_options =
|
|
166
|
+
create_options = _normalize_create_options_for_agent(runtime_options)
|
|
163
167
|
runtime_options = normalize_runtime_options(create_options)
|
|
164
168
|
oauth = create_options.oauth if create_options is not None else None
|
|
165
169
|
if _should_skip_oauth(runtime_options, oauth):
|
|
@@ -197,7 +201,7 @@ class GopherAgent:
|
|
|
197
201
|
Returns:
|
|
198
202
|
GopherAgent instance
|
|
199
203
|
"""
|
|
200
|
-
create_options =
|
|
204
|
+
create_options = _normalize_create_options_for_agent(runtime_options)
|
|
201
205
|
runtime_options = normalize_runtime_options(create_options)
|
|
202
206
|
oauth = create_options.oauth if create_options is not None else None
|
|
203
207
|
if _should_skip_oauth(runtime_options, oauth):
|
|
@@ -246,7 +250,7 @@ class GopherAgent:
|
|
|
246
250
|
Returns:
|
|
247
251
|
GopherAgent instance
|
|
248
252
|
"""
|
|
249
|
-
create_options =
|
|
253
|
+
create_options = _normalize_create_options_for_agent(runtime_options)
|
|
250
254
|
normalized_runtime_options = normalize_runtime_options(create_options)
|
|
251
255
|
oauth = create_options.oauth if create_options is not None else None
|
|
252
256
|
if _should_skip_oauth(normalized_runtime_options, oauth):
|
|
@@ -289,7 +293,7 @@ class GopherAgent:
|
|
|
289
293
|
Returns:
|
|
290
294
|
GopherAgent instance
|
|
291
295
|
"""
|
|
292
|
-
create_options =
|
|
296
|
+
create_options = _normalize_create_options_for_agent(runtime_options)
|
|
293
297
|
normalized_runtime_options = normalize_runtime_options(create_options)
|
|
294
298
|
oauth = create_options.oauth if create_options is not None else None
|
|
295
299
|
if _should_skip_oauth(normalized_runtime_options, oauth):
|
|
@@ -332,7 +336,7 @@ class GopherAgent:
|
|
|
332
336
|
Returns:
|
|
333
337
|
GopherAgent instance
|
|
334
338
|
"""
|
|
335
|
-
create_options =
|
|
339
|
+
create_options = _normalize_create_options_for_agent(runtime_options)
|
|
336
340
|
normalized_runtime_options = normalize_runtime_options(create_options)
|
|
337
341
|
oauth = create_options.oauth if create_options is not None else None
|
|
338
342
|
if _should_skip_oauth(normalized_runtime_options, oauth):
|
|
@@ -375,7 +379,7 @@ class GopherAgent:
|
|
|
375
379
|
Returns:
|
|
376
380
|
GopherAgent instance
|
|
377
381
|
"""
|
|
378
|
-
create_options =
|
|
382
|
+
create_options = _normalize_create_options_for_agent(runtime_options)
|
|
379
383
|
normalized_runtime_options = normalize_runtime_options(create_options)
|
|
380
384
|
oauth = create_options.oauth if create_options is not None else None
|
|
381
385
|
if _should_skip_oauth(normalized_runtime_options, oauth):
|
|
@@ -417,7 +421,7 @@ class GopherAgent:
|
|
|
417
421
|
Returns:
|
|
418
422
|
GopherAgent instance
|
|
419
423
|
"""
|
|
420
|
-
create_options =
|
|
424
|
+
create_options = _normalize_create_options_for_agent(runtime_options)
|
|
421
425
|
normalized_runtime_options = normalize_runtime_options(create_options)
|
|
422
426
|
oauth = create_options.oauth if create_options is not None else None
|
|
423
427
|
if url != "" and not _should_skip_oauth(normalized_runtime_options, oauth):
|
|
@@ -428,6 +432,11 @@ class GopherAgent:
|
|
|
428
432
|
oauth=oauth,
|
|
429
433
|
)
|
|
430
434
|
)
|
|
435
|
+
normalized_runtime_options = preflight_gateway_elicitation(
|
|
436
|
+
url,
|
|
437
|
+
normalized_runtime_options,
|
|
438
|
+
create_options,
|
|
439
|
+
)
|
|
431
440
|
return GopherAgent._create_from_ffi(
|
|
432
441
|
lambda lib: lib.agent_create_by_url(
|
|
433
442
|
provider, model, url, normalized_runtime_options
|
|
@@ -605,6 +614,13 @@ def _run_oauth_coroutine(create_coroutine):
|
|
|
605
614
|
)
|
|
606
615
|
|
|
607
616
|
|
|
617
|
+
def _normalize_create_options_for_agent(
|
|
618
|
+
runtime_options: RuntimeOptionsInput,
|
|
619
|
+
) -> GopherAgentCreateOptions:
|
|
620
|
+
create_options = normalize_create_options(runtime_options)
|
|
621
|
+
return create_options if create_options is not None else GopherAgentCreateOptions()
|
|
622
|
+
|
|
623
|
+
|
|
608
624
|
def _should_skip_oauth(
|
|
609
625
|
runtime_options: Optional[GopherAgentRuntimeOptions],
|
|
610
626
|
oauth: Optional[GopherAgentOAuthOptions],
|
|
@@ -206,13 +206,19 @@ class GopherAgentConfigBuilder:
|
|
|
206
206
|
current_oauth = (
|
|
207
207
|
self._runtime_options.oauth if self._runtime_options is not None else None
|
|
208
208
|
)
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
"oauth": current_oauth,
|
|
214
|
-
}
|
|
209
|
+
current_elicitation = (
|
|
210
|
+
self._runtime_options.elicitation
|
|
211
|
+
if self._runtime_options is not None
|
|
212
|
+
else None
|
|
215
213
|
)
|
|
214
|
+
next_options = {
|
|
215
|
+
"access_token": access_token,
|
|
216
|
+
"headers": current_headers,
|
|
217
|
+
"oauth": current_oauth,
|
|
218
|
+
}
|
|
219
|
+
if current_elicitation is not None:
|
|
220
|
+
next_options["elicitation"] = current_elicitation
|
|
221
|
+
self._runtime_options = normalize_create_options(next_options)
|
|
216
222
|
return self
|
|
217
223
|
|
|
218
224
|
def headers(self, headers: Mapping[str, str]) -> "GopherAgentConfigBuilder":
|
|
@@ -233,13 +239,19 @@ class GopherAgentConfigBuilder:
|
|
|
233
239
|
current_oauth = (
|
|
234
240
|
self._runtime_options.oauth if self._runtime_options is not None else None
|
|
235
241
|
)
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
"oauth": current_oauth,
|
|
241
|
-
}
|
|
242
|
+
current_elicitation = (
|
|
243
|
+
self._runtime_options.elicitation
|
|
244
|
+
if self._runtime_options is not None
|
|
245
|
+
else None
|
|
242
246
|
)
|
|
247
|
+
next_options = {
|
|
248
|
+
"access_token": current_token,
|
|
249
|
+
"headers": headers,
|
|
250
|
+
"oauth": current_oauth,
|
|
251
|
+
}
|
|
252
|
+
if current_elicitation is not None:
|
|
253
|
+
next_options["elicitation"] = current_elicitation
|
|
254
|
+
self._runtime_options = normalize_create_options(next_options)
|
|
243
255
|
return self
|
|
244
256
|
|
|
245
257
|
def build(self) -> GopherAgentConfig:
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""MCP server-to-client elicitation support."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Callable, Mapping, Optional, Union
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
GopherAgentElicitationAction = str
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class GopherAgentElicitationRequest:
|
|
10
|
+
"""Server-provided MCP elicitation/create request."""
|
|
11
|
+
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
mode: str,
|
|
15
|
+
elicitation_id: Optional[str] = None,
|
|
16
|
+
message: Optional[str] = None,
|
|
17
|
+
url: Optional[str] = None,
|
|
18
|
+
request_id_json: Optional[str] = None,
|
|
19
|
+
raw_json: Optional[str] = None,
|
|
20
|
+
raw_params_json: Optional[str] = None,
|
|
21
|
+
) -> None:
|
|
22
|
+
if not isinstance(mode, str):
|
|
23
|
+
raise ValueError("elicitation mode must be a string")
|
|
24
|
+
self.mode = mode
|
|
25
|
+
self.elicitation_id = _optional_string(elicitation_id, "elicitation_id")
|
|
26
|
+
self.message = _optional_string(message, "message")
|
|
27
|
+
self.url = _optional_string(url, "url")
|
|
28
|
+
self.request_id_json = _optional_string(request_id_json, "request_id_json")
|
|
29
|
+
self.raw_json = _optional_string(raw_json, "raw_json")
|
|
30
|
+
self.raw_params_json = _optional_string(raw_params_json, "raw_params_json")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class GopherAgentElicitationResponse:
|
|
34
|
+
"""Application response to an MCP elicitation request."""
|
|
35
|
+
|
|
36
|
+
def __init__(self, action: GopherAgentElicitationAction) -> None:
|
|
37
|
+
self.action = normalize_elicitation_action(action)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
GopherAgentElicitationHandler = Callable[
|
|
41
|
+
[GopherAgentElicitationRequest],
|
|
42
|
+
Union[GopherAgentElicitationResponse, GopherAgentElicitationAction],
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class GopherAgentElicitationOptions:
|
|
47
|
+
"""Options for MCP server-to-client elicitation handling."""
|
|
48
|
+
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
handler: Optional[GopherAgentElicitationHandler] = None,
|
|
52
|
+
timeout_ms: Optional[int] = None,
|
|
53
|
+
open_browser: Optional[bool] = None,
|
|
54
|
+
) -> None:
|
|
55
|
+
if handler is not None and not callable(handler):
|
|
56
|
+
raise ValueError("elicitation handler must be callable")
|
|
57
|
+
if timeout_ms is not None:
|
|
58
|
+
if not isinstance(timeout_ms, (int, float)) or not _is_finite(timeout_ms):
|
|
59
|
+
raise ValueError("elicitation timeout_ms must be a finite number")
|
|
60
|
+
timeout_ms = max(0, int(timeout_ms))
|
|
61
|
+
if open_browser is not None and not isinstance(open_browser, bool):
|
|
62
|
+
raise ValueError("elicitation open_browser must be a boolean")
|
|
63
|
+
self.handler = handler
|
|
64
|
+
self.timeout_ms = timeout_ms
|
|
65
|
+
self.open_browser = open_browser
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def normalize_elicitation_options(
|
|
69
|
+
options: Optional[
|
|
70
|
+
Union[GopherAgentElicitationOptions, Mapping[str, Any]]
|
|
71
|
+
] = None,
|
|
72
|
+
) -> GopherAgentElicitationOptions:
|
|
73
|
+
"""Normalize elicitation options, defaulting omitted options to enabled."""
|
|
74
|
+
if options is None:
|
|
75
|
+
return GopherAgentElicitationOptions()
|
|
76
|
+
|
|
77
|
+
if isinstance(options, GopherAgentElicitationOptions):
|
|
78
|
+
return options
|
|
79
|
+
|
|
80
|
+
if isinstance(options, Mapping):
|
|
81
|
+
timeout_ms = (
|
|
82
|
+
options.get("timeout_ms")
|
|
83
|
+
if "timeout_ms" in options
|
|
84
|
+
else options.get("timeoutMs")
|
|
85
|
+
)
|
|
86
|
+
open_browser = (
|
|
87
|
+
options.get("open_browser")
|
|
88
|
+
if "open_browser" in options
|
|
89
|
+
else options.get("openBrowser")
|
|
90
|
+
)
|
|
91
|
+
return GopherAgentElicitationOptions(
|
|
92
|
+
handler=options.get("handler"),
|
|
93
|
+
timeout_ms=timeout_ms,
|
|
94
|
+
open_browser=open_browser,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
raise ValueError(
|
|
98
|
+
"elicitation options must be a GopherAgentElicitationOptions instance "
|
|
99
|
+
"or mapping"
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def normalize_elicitation_action(
|
|
104
|
+
response: Union[GopherAgentElicitationResponse, GopherAgentElicitationAction]
|
|
105
|
+
) -> GopherAgentElicitationAction:
|
|
106
|
+
"""Normalize and validate an MCP elicitation action."""
|
|
107
|
+
action = response.action if isinstance(response, GopherAgentElicitationResponse) else response
|
|
108
|
+
if action in ("accept", "decline", "cancel"):
|
|
109
|
+
return action
|
|
110
|
+
raise ValueError(f"unsupported MCP elicitation action: {action}")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _optional_string(value: Optional[str], name: str) -> Optional[str]:
|
|
114
|
+
if value is not None and not isinstance(value, str):
|
|
115
|
+
raise ValueError(f"elicitation {name} must be a string")
|
|
116
|
+
return value
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _is_finite(value: Union[int, float]) -> bool:
|
|
120
|
+
return value == value and value not in (float("inf"), float("-inf"))
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
"""Runtime helpers for MCP elicitation callbacks."""
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
import os
|
|
5
|
+
import selectors
|
|
6
|
+
import sys
|
|
7
|
+
from typing import Any, Callable, Dict, Mapping, Optional
|
|
8
|
+
from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit
|
|
9
|
+
|
|
10
|
+
from gopher_mcp_python.elicitation import (
|
|
11
|
+
GopherAgentElicitationAction,
|
|
12
|
+
GopherAgentElicitationHandler,
|
|
13
|
+
GopherAgentElicitationOptions,
|
|
14
|
+
GopherAgentElicitationRequest,
|
|
15
|
+
GopherAgentElicitationResponse,
|
|
16
|
+
normalize_elicitation_action,
|
|
17
|
+
)
|
|
18
|
+
from gopher_mcp_python.oauth_browser import open_authorization_url
|
|
19
|
+
|
|
20
|
+
ELICITATION_ACTION_ACCEPT = 1
|
|
21
|
+
ELICITATION_ACTION_DECLINE = 2
|
|
22
|
+
ELICITATION_ACTION_CANCEL = 3
|
|
23
|
+
|
|
24
|
+
_read_input: Callable[[Optional[int]], Optional[str]]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def to_elicitation_request(
|
|
28
|
+
request: Mapping[str, Optional[str]]
|
|
29
|
+
) -> GopherAgentElicitationRequest:
|
|
30
|
+
"""Convert native request fields into the public Python request object."""
|
|
31
|
+
return GopherAgentElicitationRequest(
|
|
32
|
+
mode=request.get("mode") or "",
|
|
33
|
+
elicitation_id=request.get("elicitation_id"),
|
|
34
|
+
message=request.get("message"),
|
|
35
|
+
url=request.get("url"),
|
|
36
|
+
request_id_json=request.get("request_id_json"),
|
|
37
|
+
raw_json=request.get("raw_json"),
|
|
38
|
+
raw_params_json=request.get("raw_params_json"),
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def resolve_elicitation_action_sync(
|
|
43
|
+
options: GopherAgentElicitationOptions,
|
|
44
|
+
request: GopherAgentElicitationRequest,
|
|
45
|
+
) -> GopherAgentElicitationAction:
|
|
46
|
+
"""Resolve an elicitation request using a synchronous handler."""
|
|
47
|
+
handler = options.handler or default_url_elicitation_handler(options)
|
|
48
|
+
_log_elicitation_debug("request", _summarize_elicitation_request(request))
|
|
49
|
+
response = handler(request)
|
|
50
|
+
if inspect.isawaitable(response):
|
|
51
|
+
if inspect.iscoroutine(response):
|
|
52
|
+
response.close()
|
|
53
|
+
raise RuntimeError(
|
|
54
|
+
"Async MCP elicitation handlers are not supported by the native "
|
|
55
|
+
"FFI bridge yet"
|
|
56
|
+
)
|
|
57
|
+
action = normalize_elicitation_action(response)
|
|
58
|
+
_log_elicitation_debug(
|
|
59
|
+
"response",
|
|
60
|
+
{
|
|
61
|
+
"elicitation_id": request.elicitation_id,
|
|
62
|
+
"mode": request.mode,
|
|
63
|
+
"action": action,
|
|
64
|
+
},
|
|
65
|
+
)
|
|
66
|
+
return action
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def default_url_elicitation_handler(
|
|
70
|
+
options: Optional[GopherAgentElicitationOptions] = None,
|
|
71
|
+
) -> GopherAgentElicitationHandler:
|
|
72
|
+
"""Return the SDK's default URL-mode provider OAuth handler."""
|
|
73
|
+
resolved_options = options or GopherAgentElicitationOptions()
|
|
74
|
+
|
|
75
|
+
def handle(
|
|
76
|
+
request: GopherAgentElicitationRequest,
|
|
77
|
+
) -> GopherAgentElicitationResponse:
|
|
78
|
+
if request.mode != "url" or not request.url:
|
|
79
|
+
return GopherAgentElicitationResponse("decline")
|
|
80
|
+
result = open_authorization_url(
|
|
81
|
+
request.url,
|
|
82
|
+
open_browser=resolved_options.open_browser,
|
|
83
|
+
)
|
|
84
|
+
if not result.get("opened"):
|
|
85
|
+
print(
|
|
86
|
+
"Open this OAuth authorization URL to continue:\n"
|
|
87
|
+
f"{request.url}",
|
|
88
|
+
file=sys.stderr,
|
|
89
|
+
)
|
|
90
|
+
return GopherAgentElicitationResponse(
|
|
91
|
+
wait_for_oauth_completion_sync(resolved_options.timeout_ms)
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
return handle
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def wait_for_oauth_completion_sync(
|
|
98
|
+
timeout_ms: Optional[int] = None,
|
|
99
|
+
) -> GopherAgentElicitationAction:
|
|
100
|
+
"""Wait synchronously for the user to finish browser OAuth."""
|
|
101
|
+
print(
|
|
102
|
+
'Complete the OAuth flow in the browser, then press Enter to continue. '
|
|
103
|
+
'Type "cancel" and press Enter to cancel.',
|
|
104
|
+
file=sys.stderr,
|
|
105
|
+
)
|
|
106
|
+
value = _read_input(timeout_ms)
|
|
107
|
+
if value is None:
|
|
108
|
+
print(
|
|
109
|
+
"Cannot access an interactive terminal; canceling provider "
|
|
110
|
+
"authorization.",
|
|
111
|
+
file=sys.stderr,
|
|
112
|
+
)
|
|
113
|
+
return "cancel"
|
|
114
|
+
return "cancel" if value.strip().lower() == "cancel" else "accept"
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def native_action_from_elicitation_action(
|
|
118
|
+
action: GopherAgentElicitationAction,
|
|
119
|
+
) -> int:
|
|
120
|
+
"""Map public action strings to native integer constants."""
|
|
121
|
+
if action == "accept":
|
|
122
|
+
return ELICITATION_ACTION_ACCEPT
|
|
123
|
+
if action == "decline":
|
|
124
|
+
return ELICITATION_ACTION_DECLINE
|
|
125
|
+
if action == "cancel":
|
|
126
|
+
return ELICITATION_ACTION_CANCEL
|
|
127
|
+
raise ValueError(f"unsupported MCP elicitation action: {action}")
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def set_elicitation_input_for_test(
|
|
131
|
+
read_input: Optional[Callable[[Optional[int]], Optional[str]]],
|
|
132
|
+
) -> None:
|
|
133
|
+
"""Override terminal input for tests."""
|
|
134
|
+
global _read_input
|
|
135
|
+
_read_input = read_input or _read_terminal_input
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def redact_elicitation_url(url: str) -> str:
|
|
139
|
+
"""Redact sensitive OAuth query fields for diagnostic logging."""
|
|
140
|
+
try:
|
|
141
|
+
parsed = urlsplit(url)
|
|
142
|
+
query = urlencode(
|
|
143
|
+
[
|
|
144
|
+
(
|
|
145
|
+
name,
|
|
146
|
+
"<redacted>" if _is_sensitive_query_name(name) else "<present>",
|
|
147
|
+
)
|
|
148
|
+
for name, _value in parse_qsl(parsed.query, keep_blank_values=True)
|
|
149
|
+
]
|
|
150
|
+
)
|
|
151
|
+
fragment = "<redacted>" if parsed.fragment else ""
|
|
152
|
+
return urlunsplit(
|
|
153
|
+
(parsed.scheme, parsed.netloc, parsed.path, query, fragment)
|
|
154
|
+
)
|
|
155
|
+
except Exception:
|
|
156
|
+
return "<invalid-url>"
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _read_terminal_input(timeout_ms: Optional[int]) -> Optional[str]:
|
|
160
|
+
if os.environ.get("PYTEST_CURRENT_TEST"):
|
|
161
|
+
return None
|
|
162
|
+
if timeout_ms is not None and timeout_ms > 0:
|
|
163
|
+
return _read_terminal_input_with_timeout(timeout_ms)
|
|
164
|
+
if not sys.stdin or not sys.stdin.isatty():
|
|
165
|
+
return None
|
|
166
|
+
return sys.stdin.readline()
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _read_terminal_input_with_timeout(timeout_ms: int) -> Optional[str]:
|
|
170
|
+
if not sys.stdin or not sys.stdin.isatty():
|
|
171
|
+
return None
|
|
172
|
+
selector = None
|
|
173
|
+
try:
|
|
174
|
+
selector = selectors.DefaultSelector()
|
|
175
|
+
selector.register(sys.stdin, selectors.EVENT_READ)
|
|
176
|
+
events = selector.select(timeout_ms / 1000.0)
|
|
177
|
+
except Exception:
|
|
178
|
+
return None
|
|
179
|
+
finally:
|
|
180
|
+
if selector is not None:
|
|
181
|
+
try:
|
|
182
|
+
selector.close()
|
|
183
|
+
except Exception:
|
|
184
|
+
pass
|
|
185
|
+
if not events:
|
|
186
|
+
print(
|
|
187
|
+
"Timed out waiting for OAuth completion; canceling provider "
|
|
188
|
+
"authorization.",
|
|
189
|
+
file=sys.stderr,
|
|
190
|
+
)
|
|
191
|
+
return None
|
|
192
|
+
return sys.stdin.readline()
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _summarize_elicitation_request(
|
|
196
|
+
request: GopherAgentElicitationRequest,
|
|
197
|
+
) -> Dict[str, Optional[str]]:
|
|
198
|
+
host = None
|
|
199
|
+
if request.url:
|
|
200
|
+
try:
|
|
201
|
+
host = urlsplit(request.url).netloc
|
|
202
|
+
except Exception:
|
|
203
|
+
host = None
|
|
204
|
+
return {
|
|
205
|
+
"elicitation_id": request.elicitation_id,
|
|
206
|
+
"mode": request.mode,
|
|
207
|
+
"host": host,
|
|
208
|
+
"url": redact_elicitation_url(request.url) if request.url else None,
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _log_elicitation_debug(label: str, values: Any) -> None:
|
|
213
|
+
if os.environ.get("GOPHER_MCP_OAUTH_DEBUG") != "1" and os.environ.get("DEBUG") != "1":
|
|
214
|
+
return
|
|
215
|
+
print(f"[gopher-mcp-python elicitation] {label}: {values}", file=sys.stderr)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _is_sensitive_query_name(name: str) -> bool:
|
|
219
|
+
normalized = name.lower()
|
|
220
|
+
return (
|
|
221
|
+
normalized == "state"
|
|
222
|
+
or normalized == "code"
|
|
223
|
+
or normalized == "client_secret"
|
|
224
|
+
or "token" in normalized
|
|
225
|
+
or "secret" in normalized
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
_read_input = _read_terminal_input
|