gopher-mcp-python 0.1.38__tar.gz → 0.1.39__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.38 → gopher_mcp_python-0.1.39}/PKG-INFO +1 -1
- {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python/__init__.py +1 -1
- {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python/agent.py +27 -2
- {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python/gateway_elicitation_preflight.py +56 -46
- {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python.egg-info/PKG-INFO +1 -1
- {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/pyproject.toml +1 -1
- {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/tests/test_agent_create_with_oauth.py +13 -10
- {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/tests/test_gateway_elicitation_preflight.py +79 -3
- {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/LICENSE +0 -0
- {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/README.md +0 -0
- {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python/auth/__init__.py +0 -0
- {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python/auth/errors.py +0 -0
- {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python/auth/gopher_auth.py +0 -0
- {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python/auth/scope_helpers.py +0 -0
- {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python/config.py +0 -0
- {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python/elicitation.py +0 -0
- {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python/elicitation_runtime.py +0 -0
- {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python/errors.py +0 -0
- {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python/ffi/__init__.py +0 -0
- {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python/ffi/auth/__init__.py +0 -0
- {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python/ffi/auth/auth_client.py +0 -0
- {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python/ffi/auth/auto_refresh.py +0 -0
- {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python/ffi/auth/config_loader.py +0 -0
- {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python/ffi/auth/loader.py +0 -0
- {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python/ffi/auth/oauth_client.py +0 -0
- {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python/ffi/auth/session_manager.py +0 -0
- {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python/ffi/auth/types.py +0 -0
- {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python/ffi/auth/validation_options.py +0 -0
- {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python/ffi/library.py +0 -0
- {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python/oauth_authorization_url.py +0 -0
- {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python/oauth_browser.py +0 -0
- {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python/oauth_discovery.py +0 -0
- {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python/oauth_loopback.py +0 -0
- {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python/oauth_pkce.py +0 -0
- {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python/oauth_registration.py +0 -0
- {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python/oauth_resolver.py +0 -0
- {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python/oauth_runtime_options.py +0 -0
- {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python/oauth_server_targets.py +0 -0
- {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python/oauth_token_exchange.py +0 -0
- {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python/oauth_token_store.py +0 -0
- {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python/result.py +0 -0
- {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python/runtime_options.py +0 -0
- {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python/server_config.py +0 -0
- {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python.egg-info/SOURCES.txt +0 -0
- {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python.egg-info/dependency_links.txt +0 -0
- {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python.egg-info/requires.txt +0 -0
- {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python.egg-info/top_level.txt +0 -0
- {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/setup.cfg +0 -0
- {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/setup.py +0 -0
- {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/tests/test_agent_create_by.py +0 -0
- {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/tests/test_agent_error_message.py +0 -0
- {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/tests/test_agent_lifecycle.py +0 -0
- {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/tests/test_agent_runtime_options.py +0 -0
- {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/tests/test_auth_exports.py +0 -0
- {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/tests/test_config.py +0 -0
- {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/tests/test_custom_oauth_test_idp.py +0 -0
- {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/tests/test_custom_protected_mcp_endpoints.py +0 -0
- {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/tests/test_dump_version.py +0 -0
- {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/tests/test_elicitation_runtime.py +0 -0
- {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/tests/test_ffi.py +0 -0
- {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/tests/test_ffi_runtime_options.py +0 -0
- {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/tests/test_gopher_auth.py +0 -0
- {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/tests/test_header_create_by_url_example.py +0 -0
- {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/tests/test_library_search_paths.py +0 -0
- {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/tests/test_linux_native_packaging.py +0 -0
- {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/tests/test_oauth_authorization_url.py +0 -0
- {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/tests/test_oauth_auto_custom_idp.py +0 -0
- {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/tests/test_oauth_auto_custom_idp_failures.py +0 -0
- {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/tests/test_oauth_browser.py +0 -0
- {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/tests/test_oauth_create_with_url_integration.py +0 -0
- {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/tests/test_oauth_discovery.py +0 -0
- {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/tests/test_oauth_loopback.py +0 -0
- {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/tests/test_oauth_pkce.py +0 -0
- {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/tests/test_oauth_registration.py +0 -0
- {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/tests/test_oauth_resolver.py +0 -0
- {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/tests/test_oauth_runtime_options.py +0 -0
- {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/tests/test_oauth_server_targets.py +0 -0
- {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/tests/test_oauth_test_token_helper.py +0 -0
- {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/tests/test_oauth_token_exchange.py +0 -0
- {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/tests/test_oauth_token_store.py +0 -0
- {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/tests/test_result.py +0 -0
- {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/tests/test_server_config.py +0 -0
|
@@ -53,7 +53,7 @@ from gopher_mcp_python.errors import (
|
|
|
53
53
|
from gopher_mcp_python.server_config import ServerConfig, ServerConfigRoute
|
|
54
54
|
from gopher_mcp_python.ffi import GopherOrchLibrary, GopherOrchHandle
|
|
55
55
|
|
|
56
|
-
__version__ = "0.1.
|
|
56
|
+
__version__ = "0.1.39"
|
|
57
57
|
|
|
58
58
|
_AUTH_EXPORTS = {
|
|
59
59
|
"GopherAuth",
|
|
@@ -32,7 +32,7 @@ import weakref
|
|
|
32
32
|
from typing import Callable, Optional
|
|
33
33
|
|
|
34
34
|
from gopher_mcp_python.gateway_elicitation_preflight import (
|
|
35
|
-
|
|
35
|
+
preflight_gateway_elicitation_with_status,
|
|
36
36
|
)
|
|
37
37
|
import gopher_mcp_python.oauth_resolver as oauth_resolver
|
|
38
38
|
from gopher_mcp_python.config import GopherAgentConfig
|
|
@@ -51,6 +51,8 @@ from gopher_mcp_python.ffi import GopherOrchLibrary, GopherOrchHandle
|
|
|
51
51
|
|
|
52
52
|
_initialized = False
|
|
53
53
|
_cleanup_handler_registered = False
|
|
54
|
+
_SKIP_DISCOVERY_ELICITATION_HEADER = "X-Gopher-Internal-Skip-Discovery-Elicitation"
|
|
55
|
+
_PREFLIGHT_MCP_SESSION_HEADER = "X-Gopher-Internal-Preflight-Mcp-Session-Id"
|
|
54
56
|
|
|
55
57
|
|
|
56
58
|
class GopherAgent:
|
|
@@ -432,11 +434,16 @@ class GopherAgent:
|
|
|
432
434
|
oauth=oauth,
|
|
433
435
|
)
|
|
434
436
|
)
|
|
435
|
-
|
|
437
|
+
preflight = preflight_gateway_elicitation_with_status(
|
|
436
438
|
url,
|
|
437
439
|
normalized_runtime_options,
|
|
438
440
|
create_options,
|
|
439
441
|
)
|
|
442
|
+
normalized_runtime_options = _mark_discovery_elicitation_preflighted(
|
|
443
|
+
preflight.runtime_options,
|
|
444
|
+
preflight.handled,
|
|
445
|
+
preflight.session,
|
|
446
|
+
)
|
|
440
447
|
return GopherAgent._create_from_ffi(
|
|
441
448
|
lambda lib: lib.agent_create_by_url(
|
|
442
449
|
provider, model, url, normalized_runtime_options
|
|
@@ -634,6 +641,24 @@ def _should_skip_oauth(
|
|
|
634
641
|
return any(name.lower() == "authorization" for name in runtime_options.headers)
|
|
635
642
|
|
|
636
643
|
|
|
644
|
+
def _mark_discovery_elicitation_preflighted(
|
|
645
|
+
runtime_options: Optional[GopherAgentRuntimeOptions],
|
|
646
|
+
handled: bool,
|
|
647
|
+
session: Optional[str],
|
|
648
|
+
) -> Optional[GopherAgentRuntimeOptions]:
|
|
649
|
+
if not handled:
|
|
650
|
+
return runtime_options
|
|
651
|
+
headers = dict(runtime_options.headers) if runtime_options is not None else {}
|
|
652
|
+
headers[_SKIP_DISCOVERY_ELICITATION_HEADER] = "1"
|
|
653
|
+
if session:
|
|
654
|
+
headers[_PREFLIGHT_MCP_SESSION_HEADER] = session
|
|
655
|
+
return GopherAgentRuntimeOptions(
|
|
656
|
+
access_token=runtime_options.access_token if runtime_options else None,
|
|
657
|
+
headers=headers,
|
|
658
|
+
elicitation=runtime_options.elicitation if runtime_options else None,
|
|
659
|
+
)
|
|
660
|
+
|
|
661
|
+
|
|
637
662
|
def _build_create_error_message() -> str:
|
|
638
663
|
"""
|
|
639
664
|
Build the AgentError message for a null native create*() result.
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""MCP URL elicitation preflight for direct create_with_url flows."""
|
|
2
2
|
|
|
3
|
+
from dataclasses import dataclass
|
|
3
4
|
import json
|
|
4
5
|
import os
|
|
5
|
-
import re
|
|
6
6
|
import socket
|
|
7
7
|
import time
|
|
8
8
|
from typing import Callable, Dict, Optional
|
|
@@ -18,28 +18,46 @@ from gopher_mcp_python.runtime_options import (
|
|
|
18
18
|
GopherAgentRuntimeOptions,
|
|
19
19
|
)
|
|
20
20
|
|
|
21
|
-
GATEWAY_PATH_RE = re.compile(r"^/v1/mcp/gateways/[^/]+/mcp/?$")
|
|
22
21
|
MCP_PROTOCOL_VERSION = "2025-11-25"
|
|
23
22
|
DEFAULT_PREFLIGHT_TIMEOUT_MS = 5000
|
|
24
23
|
|
|
25
24
|
UrlOpen = Callable[..., object]
|
|
26
25
|
|
|
27
26
|
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class GatewayElicitationPreflightResult:
|
|
29
|
+
runtime_options: Optional[GopherAgentRuntimeOptions]
|
|
30
|
+
handled: bool
|
|
31
|
+
session: Optional[str] = None
|
|
32
|
+
|
|
33
|
+
|
|
28
34
|
def preflight_gateway_elicitation(
|
|
29
35
|
url: str,
|
|
30
36
|
runtime_options: Optional[GopherAgentRuntimeOptions],
|
|
31
37
|
create_options: Optional[GopherAgentCreateOptions],
|
|
32
38
|
opener: UrlOpen = urlopen,
|
|
33
39
|
) -> Optional[GopherAgentRuntimeOptions]:
|
|
34
|
-
"""Handle
|
|
35
|
-
|
|
36
|
-
|
|
40
|
+
"""Handle URL elicitation before native tool calls."""
|
|
41
|
+
return preflight_gateway_elicitation_with_status(
|
|
42
|
+
url, runtime_options, create_options, opener
|
|
43
|
+
).runtime_options
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def preflight_gateway_elicitation_with_status(
|
|
47
|
+
url: str,
|
|
48
|
+
runtime_options: Optional[GopherAgentRuntimeOptions],
|
|
49
|
+
create_options: Optional[GopherAgentCreateOptions],
|
|
50
|
+
opener: UrlOpen = urlopen,
|
|
51
|
+
) -> GatewayElicitationPreflightResult:
|
|
52
|
+
"""Handle URL elicitation and report whether preflight accepted it."""
|
|
53
|
+
if not _is_http_mcp_url(url):
|
|
54
|
+
return GatewayElicitationPreflightResult(runtime_options, False)
|
|
37
55
|
if create_options is None or create_options.elicitation is None:
|
|
38
|
-
return runtime_options
|
|
56
|
+
return GatewayElicitationPreflightResult(runtime_options, False)
|
|
39
57
|
|
|
40
58
|
authorization = _authorization_header_value(runtime_options)
|
|
41
59
|
if authorization is None:
|
|
42
|
-
return runtime_options
|
|
60
|
+
return GatewayElicitationPreflightResult(runtime_options, False)
|
|
43
61
|
|
|
44
62
|
timeout_ms = (
|
|
45
63
|
create_options.elicitation.timeout_ms
|
|
@@ -48,30 +66,34 @@ def preflight_gateway_elicitation(
|
|
|
48
66
|
)
|
|
49
67
|
timeout_ms = max(0, int(timeout_ms))
|
|
50
68
|
if timeout_ms == 0:
|
|
51
|
-
return runtime_options
|
|
52
|
-
|
|
53
|
-
session = _initialize_gateway_session(url, authorization, opener)
|
|
54
|
-
if session is None:
|
|
55
|
-
return runtime_options
|
|
56
|
-
|
|
57
|
-
_notify_initialized(url, authorization, session, opener)
|
|
58
|
-
_list_tools(url, authorization, session, opener)
|
|
59
|
-
_handle_gateway_event_stream(
|
|
60
|
-
url, authorization, session, create_options, timeout_ms, opener
|
|
61
|
-
)
|
|
62
|
-
return _with_gateway_session_header(runtime_options, session)
|
|
63
|
-
|
|
69
|
+
return GatewayElicitationPreflightResult(runtime_options, False)
|
|
64
70
|
|
|
65
|
-
|
|
71
|
+
try:
|
|
72
|
+
session = _initialize_gateway_session(url, authorization, opener)
|
|
73
|
+
if session is None:
|
|
74
|
+
return GatewayElicitationPreflightResult(runtime_options, False)
|
|
75
|
+
|
|
76
|
+
_notify_initialized(url, authorization, session, opener)
|
|
77
|
+
_list_tools(url, authorization, session, opener)
|
|
78
|
+
handled = _handle_gateway_event_stream(
|
|
79
|
+
url, authorization, session, create_options, timeout_ms, opener
|
|
80
|
+
)
|
|
81
|
+
return GatewayElicitationPreflightResult(
|
|
82
|
+
runtime_options,
|
|
83
|
+
handled,
|
|
84
|
+
session if handled else None,
|
|
85
|
+
)
|
|
86
|
+
except Exception as exc:
|
|
87
|
+
_log_debug("elicitation preflight skipped", {"error": str(exc)})
|
|
88
|
+
return GatewayElicitationPreflightResult(runtime_options, False)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _is_http_mcp_url(value: str) -> bool:
|
|
66
92
|
try:
|
|
67
93
|
parsed = urlparse(value)
|
|
68
94
|
except Exception:
|
|
69
95
|
return False
|
|
70
|
-
return (
|
|
71
|
-
parsed.hostname is not None
|
|
72
|
-
and parsed.hostname.endswith(".gopher.security")
|
|
73
|
-
and GATEWAY_PATH_RE.match(parsed.path) is not None
|
|
74
|
-
)
|
|
96
|
+
return parsed.scheme in ("http", "https") and parsed.netloc != ""
|
|
75
97
|
|
|
76
98
|
|
|
77
99
|
def _authorization_header_value(
|
|
@@ -116,7 +138,7 @@ def _initialize_gateway_session(
|
|
|
116
138
|
)
|
|
117
139
|
session = _response_header(response, "mcp-session-id")
|
|
118
140
|
_log_debug(
|
|
119
|
-
"
|
|
141
|
+
"elicitation preflight initialized",
|
|
120
142
|
{"session_present": session is not None},
|
|
121
143
|
)
|
|
122
144
|
return session
|
|
@@ -159,7 +181,7 @@ def _handle_gateway_event_stream(
|
|
|
159
181
|
create_options: GopherAgentCreateOptions,
|
|
160
182
|
timeout_ms: int,
|
|
161
183
|
opener: UrlOpen,
|
|
162
|
-
) ->
|
|
184
|
+
) -> bool:
|
|
163
185
|
request = Request(
|
|
164
186
|
url,
|
|
165
187
|
method="GET",
|
|
@@ -174,12 +196,12 @@ def _handle_gateway_event_stream(
|
|
|
174
196
|
response = opener(request, timeout=timeout_ms / 1000.0)
|
|
175
197
|
event = _read_first_sse_event(response, timeout_ms)
|
|
176
198
|
except (OSError, TimeoutError, socket.timeout) as exc:
|
|
177
|
-
_log_debug("
|
|
178
|
-
return
|
|
199
|
+
_log_debug("elicitation preflight skipped", {"error": str(exc)})
|
|
200
|
+
return False
|
|
179
201
|
|
|
180
202
|
parsed = _parse_elicitation_event(event)
|
|
181
203
|
if parsed is None:
|
|
182
|
-
return
|
|
204
|
+
return False
|
|
183
205
|
|
|
184
206
|
action = resolve_elicitation_action_sync(
|
|
185
207
|
create_options.elicitation,
|
|
@@ -197,9 +219,10 @@ def _handle_gateway_event_stream(
|
|
|
197
219
|
opener,
|
|
198
220
|
)
|
|
199
221
|
_log_debug(
|
|
200
|
-
"
|
|
222
|
+
"elicitation preflight answered",
|
|
201
223
|
{"action": action, "session": session},
|
|
202
224
|
)
|
|
225
|
+
return action == "accept"
|
|
203
226
|
|
|
204
227
|
|
|
205
228
|
def _post_json(
|
|
@@ -289,19 +312,6 @@ def _response_header(response: object, name: str) -> Optional[str]:
|
|
|
289
312
|
return None
|
|
290
313
|
|
|
291
314
|
|
|
292
|
-
def _with_gateway_session_header(
|
|
293
|
-
options: Optional[GopherAgentRuntimeOptions],
|
|
294
|
-
session: str,
|
|
295
|
-
) -> GopherAgentRuntimeOptions:
|
|
296
|
-
headers = dict(options.headers) if options is not None else {}
|
|
297
|
-
headers["Mcp-Session-Id"] = session
|
|
298
|
-
return GopherAgentRuntimeOptions(
|
|
299
|
-
access_token=options.access_token if options is not None else None,
|
|
300
|
-
headers=headers,
|
|
301
|
-
elicitation=options.elicitation if options is not None else None,
|
|
302
|
-
)
|
|
303
|
-
|
|
304
|
-
|
|
305
315
|
def _string_field(value: Dict[str, object], field: str) -> Optional[str]:
|
|
306
316
|
field_value = value.get(field)
|
|
307
317
|
return field_value if isinstance(field_value, str) else None
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "gopher-mcp-python"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.39"
|
|
8
8
|
description = "Python SDK for Gopher MCP - AI Agent orchestration framework with native performance"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "Apache-2.0"}
|
|
@@ -4,6 +4,9 @@ import pytest
|
|
|
4
4
|
|
|
5
5
|
import gopher_mcp_python.agent as agent_module
|
|
6
6
|
from gopher_mcp_python import AgentError, GopherAgent
|
|
7
|
+
from gopher_mcp_python.gateway_elicitation_preflight import (
|
|
8
|
+
GatewayElicitationPreflightResult,
|
|
9
|
+
)
|
|
7
10
|
from gopher_mcp_python.runtime_options import GopherAgentRuntimeOptions
|
|
8
11
|
|
|
9
12
|
|
|
@@ -161,7 +164,7 @@ def test_create_with_url_explicit_token_skips_resolver(monkeypatch) -> None:
|
|
|
161
164
|
agent.dispose()
|
|
162
165
|
|
|
163
166
|
|
|
164
|
-
def
|
|
167
|
+
def test_create_with_url_passes_preflight_session_to_native(
|
|
165
168
|
monkeypatch,
|
|
166
169
|
) -> None:
|
|
167
170
|
fake = _install_fake_library(monkeypatch)
|
|
@@ -172,13 +175,10 @@ def test_create_with_url_passes_gateway_preflight_session_to_native(
|
|
|
172
175
|
|
|
173
176
|
def preflight(url, runtime_options, create_options):
|
|
174
177
|
preflight_calls.append((url, runtime_options, create_options))
|
|
175
|
-
return
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
"Mcp-Session-Id": "session-1",
|
|
180
|
-
},
|
|
181
|
-
elicitation=runtime_options.elicitation,
|
|
178
|
+
return GatewayElicitationPreflightResult(
|
|
179
|
+
runtime_options,
|
|
180
|
+
True,
|
|
181
|
+
"session-1",
|
|
182
182
|
)
|
|
183
183
|
|
|
184
184
|
monkeypatch.setattr(
|
|
@@ -186,7 +186,9 @@ def test_create_with_url_passes_gateway_preflight_session_to_native(
|
|
|
186
186
|
"resolve_url_runtime_options_with_oauth",
|
|
187
187
|
resolver,
|
|
188
188
|
)
|
|
189
|
-
monkeypatch.setattr(
|
|
189
|
+
monkeypatch.setattr(
|
|
190
|
+
agent_module, "preflight_gateway_elicitation_with_status", preflight
|
|
191
|
+
)
|
|
190
192
|
|
|
191
193
|
agent = GopherAgent.create_with_url(
|
|
192
194
|
"Provider",
|
|
@@ -204,7 +206,8 @@ def test_create_with_url_passes_gateway_preflight_session_to_native(
|
|
|
204
206
|
)
|
|
205
207
|
assert call[4].headers == {
|
|
206
208
|
"Authorization": "Bearer caller-token",
|
|
207
|
-
"Mcp-Session-Id": "session-1",
|
|
209
|
+
"X-Gopher-Internal-Preflight-Mcp-Session-Id": "session-1",
|
|
210
|
+
"X-Gopher-Internal-Skip-Discovery-Elicitation": "1",
|
|
208
211
|
}
|
|
209
212
|
assert preflight_calls[0][0] == (
|
|
210
213
|
"https://mcp-test.gopher.security/v1/mcp/gateways/gw-1/mcp"
|
{gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/tests/test_gateway_elicitation_preflight.py
RENAMED
|
@@ -5,6 +5,7 @@ import json
|
|
|
5
5
|
from gopher_mcp_python import elicitation_runtime
|
|
6
6
|
from gopher_mcp_python.gateway_elicitation_preflight import (
|
|
7
7
|
preflight_gateway_elicitation,
|
|
8
|
+
preflight_gateway_elicitation_with_status,
|
|
8
9
|
)
|
|
9
10
|
from gopher_mcp_python.runtime_options import (
|
|
10
11
|
GopherAgentCreateOptions,
|
|
@@ -52,7 +53,7 @@ def test_answers_gateway_url_elicitation_before_native_tool_calls() -> None:
|
|
|
52
53
|
assert result is not None
|
|
53
54
|
assert result.access_token == "gateway-token"
|
|
54
55
|
assert result.headers["Authorization"] == "Bearer gateway-token"
|
|
55
|
-
assert
|
|
56
|
+
assert "Mcp-Session-Id" not in result.headers
|
|
56
57
|
assert len(opener.requests) == 5
|
|
57
58
|
assert json.loads(opener.requests[0].data.decode("utf-8"))["method"] == (
|
|
58
59
|
"initialize"
|
|
@@ -72,12 +73,87 @@ def test_answers_gateway_url_elicitation_before_native_tool_calls() -> None:
|
|
|
72
73
|
}
|
|
73
74
|
|
|
74
75
|
|
|
75
|
-
def
|
|
76
|
+
def test_also_preflights_server_urls() -> None:
|
|
77
|
+
elicitation_runtime.set_elicitation_input_for_test(lambda timeout_ms: "\n")
|
|
78
|
+
opener = FakeOpener(
|
|
79
|
+
[
|
|
80
|
+
FakeResponse(headers={"mcp-session-id": "session-1"}),
|
|
81
|
+
FakeResponse(),
|
|
82
|
+
FakeResponse(body=b'{"tools":[]}'),
|
|
83
|
+
FakeResponse(
|
|
84
|
+
lines=[
|
|
85
|
+
b"event: message\n",
|
|
86
|
+
(
|
|
87
|
+
b'data: {"jsonrpc":"2.0","id":"server_elicitation_1",'
|
|
88
|
+
b'"method":"elicitation/create","params":'
|
|
89
|
+
b'{"elicitationId":"provider-auth","message":"Connect",'
|
|
90
|
+
b'"mode":"url","url":"https://accounts.google.com/o/oauth2'
|
|
91
|
+
b'/v2/auth?state=s"}}\n'
|
|
92
|
+
),
|
|
93
|
+
b"\n",
|
|
94
|
+
]
|
|
95
|
+
),
|
|
96
|
+
FakeResponse(),
|
|
97
|
+
]
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
result = preflight_gateway_elicitation(
|
|
101
|
+
"https://mcp.gopher.security/v1/mcp/servers/server-1/mcp",
|
|
102
|
+
GopherAgentRuntimeOptions(access_token="server-token"),
|
|
103
|
+
GopherAgentCreateOptions(elicitation={"open_browser": False}),
|
|
104
|
+
opener=opener,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
assert result is not None
|
|
108
|
+
assert result.access_token == "server-token"
|
|
109
|
+
assert result.headers["Authorization"] == "Bearer server-token"
|
|
110
|
+
assert "Mcp-Session-Id" not in result.headers
|
|
111
|
+
assert len(opener.requests) == 5
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def test_reports_accepted_preflight_session() -> None:
|
|
115
|
+
elicitation_runtime.set_elicitation_input_for_test(lambda timeout_ms: "\n")
|
|
116
|
+
opener = FakeOpener(
|
|
117
|
+
[
|
|
118
|
+
FakeResponse(headers={"mcp-session-id": "session-1"}),
|
|
119
|
+
FakeResponse(),
|
|
120
|
+
FakeResponse(body=b'{"tools":[]}'),
|
|
121
|
+
FakeResponse(
|
|
122
|
+
lines=[
|
|
123
|
+
b"event: message\n",
|
|
124
|
+
(
|
|
125
|
+
b'data: {"jsonrpc":"2.0","id":"server_elicitation_1",'
|
|
126
|
+
b'"method":"elicitation/create","params":'
|
|
127
|
+
b'{"elicitationId":"provider-auth","message":"Connect",'
|
|
128
|
+
b'"mode":"url","url":"https://accounts.google.com/o/oauth2'
|
|
129
|
+
b'/v2/auth?state=s"}}\n'
|
|
130
|
+
),
|
|
131
|
+
b"\n",
|
|
132
|
+
]
|
|
133
|
+
),
|
|
134
|
+
FakeResponse(),
|
|
135
|
+
]
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
result = preflight_gateway_elicitation_with_status(
|
|
139
|
+
"https://mcp.gopher.security/v1/mcp/servers/server-1/mcp",
|
|
140
|
+
GopherAgentRuntimeOptions(access_token="server-token"),
|
|
141
|
+
GopherAgentCreateOptions(elicitation={"open_browser": False}),
|
|
142
|
+
opener=opener,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
assert result.handled is True
|
|
146
|
+
assert result.session == "session-1"
|
|
147
|
+
assert result.runtime_options is not None
|
|
148
|
+
assert "Mcp-Session-Id" not in result.runtime_options.headers
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def test_does_nothing_for_non_http_urls() -> None:
|
|
76
152
|
opener = FakeOpener([])
|
|
77
153
|
runtime_options = GopherAgentRuntimeOptions(access_token="token")
|
|
78
154
|
|
|
79
155
|
result = preflight_gateway_elicitation(
|
|
80
|
-
"
|
|
156
|
+
"stdio://mcp.example.com/mcp",
|
|
81
157
|
runtime_options,
|
|
82
158
|
GopherAgentCreateOptions(),
|
|
83
159
|
opener=opener,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python/auth/scope_helpers.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python/elicitation_runtime.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python/ffi/auth/__init__.py
RENAMED
|
File without changes
|
{gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python/ffi/auth/auth_client.py
RENAMED
|
File without changes
|
{gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python/ffi/auth/auto_refresh.py
RENAMED
|
File without changes
|
{gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python/ffi/auth/config_loader.py
RENAMED
|
File without changes
|
|
File without changes
|
{gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python/ffi/auth/oauth_client.py
RENAMED
|
File without changes
|
{gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python/ffi/auth/session_manager.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python/oauth_authorization_url.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python/oauth_registration.py
RENAMED
|
File without changes
|
|
File without changes
|
{gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python/oauth_runtime_options.py
RENAMED
|
File without changes
|
{gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python/oauth_server_targets.py
RENAMED
|
File without changes
|
{gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python/oauth_token_exchange.py
RENAMED
|
File without changes
|
{gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python/oauth_token_store.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python.egg-info/requires.txt
RENAMED
|
File without changes
|
{gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/tests/test_custom_protected_mcp_endpoints.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/tests/test_header_create_by_url_example.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/tests/test_oauth_auto_custom_idp_failures.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|