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.
Files changed (82) hide show
  1. {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/PKG-INFO +1 -1
  2. {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python/__init__.py +1 -1
  3. {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python/agent.py +27 -2
  4. {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python/gateway_elicitation_preflight.py +56 -46
  5. {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python.egg-info/PKG-INFO +1 -1
  6. {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/pyproject.toml +1 -1
  7. {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/tests/test_agent_create_with_oauth.py +13 -10
  8. {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/tests/test_gateway_elicitation_preflight.py +79 -3
  9. {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/LICENSE +0 -0
  10. {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/README.md +0 -0
  11. {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python/auth/__init__.py +0 -0
  12. {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python/auth/errors.py +0 -0
  13. {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python/auth/gopher_auth.py +0 -0
  14. {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python/auth/scope_helpers.py +0 -0
  15. {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python/config.py +0 -0
  16. {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python/elicitation.py +0 -0
  17. {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python/elicitation_runtime.py +0 -0
  18. {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python/errors.py +0 -0
  19. {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python/ffi/__init__.py +0 -0
  20. {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python/ffi/auth/__init__.py +0 -0
  21. {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python/ffi/auth/auth_client.py +0 -0
  22. {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python/ffi/auth/auto_refresh.py +0 -0
  23. {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python/ffi/auth/config_loader.py +0 -0
  24. {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python/ffi/auth/loader.py +0 -0
  25. {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python/ffi/auth/oauth_client.py +0 -0
  26. {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python/ffi/auth/session_manager.py +0 -0
  27. {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python/ffi/auth/types.py +0 -0
  28. {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python/ffi/auth/validation_options.py +0 -0
  29. {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python/ffi/library.py +0 -0
  30. {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python/oauth_authorization_url.py +0 -0
  31. {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python/oauth_browser.py +0 -0
  32. {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python/oauth_discovery.py +0 -0
  33. {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python/oauth_loopback.py +0 -0
  34. {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python/oauth_pkce.py +0 -0
  35. {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python/oauth_registration.py +0 -0
  36. {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python/oauth_resolver.py +0 -0
  37. {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python/oauth_runtime_options.py +0 -0
  38. {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python/oauth_server_targets.py +0 -0
  39. {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python/oauth_token_exchange.py +0 -0
  40. {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python/oauth_token_store.py +0 -0
  41. {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python/result.py +0 -0
  42. {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python/runtime_options.py +0 -0
  43. {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python/server_config.py +0 -0
  44. {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python.egg-info/SOURCES.txt +0 -0
  45. {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python.egg-info/dependency_links.txt +0 -0
  46. {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python.egg-info/requires.txt +0 -0
  47. {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/gopher_mcp_python.egg-info/top_level.txt +0 -0
  48. {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/setup.cfg +0 -0
  49. {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/setup.py +0 -0
  50. {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/tests/test_agent_create_by.py +0 -0
  51. {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/tests/test_agent_error_message.py +0 -0
  52. {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/tests/test_agent_lifecycle.py +0 -0
  53. {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/tests/test_agent_runtime_options.py +0 -0
  54. {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/tests/test_auth_exports.py +0 -0
  55. {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/tests/test_config.py +0 -0
  56. {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/tests/test_custom_oauth_test_idp.py +0 -0
  57. {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/tests/test_custom_protected_mcp_endpoints.py +0 -0
  58. {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/tests/test_dump_version.py +0 -0
  59. {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/tests/test_elicitation_runtime.py +0 -0
  60. {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/tests/test_ffi.py +0 -0
  61. {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/tests/test_ffi_runtime_options.py +0 -0
  62. {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/tests/test_gopher_auth.py +0 -0
  63. {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/tests/test_header_create_by_url_example.py +0 -0
  64. {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/tests/test_library_search_paths.py +0 -0
  65. {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/tests/test_linux_native_packaging.py +0 -0
  66. {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/tests/test_oauth_authorization_url.py +0 -0
  67. {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/tests/test_oauth_auto_custom_idp.py +0 -0
  68. {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/tests/test_oauth_auto_custom_idp_failures.py +0 -0
  69. {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/tests/test_oauth_browser.py +0 -0
  70. {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/tests/test_oauth_create_with_url_integration.py +0 -0
  71. {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/tests/test_oauth_discovery.py +0 -0
  72. {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/tests/test_oauth_loopback.py +0 -0
  73. {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/tests/test_oauth_pkce.py +0 -0
  74. {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/tests/test_oauth_registration.py +0 -0
  75. {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/tests/test_oauth_resolver.py +0 -0
  76. {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/tests/test_oauth_runtime_options.py +0 -0
  77. {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/tests/test_oauth_server_targets.py +0 -0
  78. {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/tests/test_oauth_test_token_helper.py +0 -0
  79. {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/tests/test_oauth_token_exchange.py +0 -0
  80. {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/tests/test_oauth_token_store.py +0 -0
  81. {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/tests/test_result.py +0 -0
  82. {gopher_mcp_python-0.1.38 → gopher_mcp_python-0.1.39}/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.38
3
+ Version: 0.1.39
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
@@ -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.38"
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
- preflight_gateway_elicitation,
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
- normalized_runtime_options = preflight_gateway_elicitation(
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
- """Gateway URL elicitation preflight for direct create_with_url flows."""
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 gateway second-step URL elicitation before native tool calls."""
35
- if not _is_gopher_gateway_mcp_url(url):
36
- return runtime_options
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
- def _is_gopher_gateway_mcp_url(value: str) -> bool:
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
- "gateway elicitation preflight initialized",
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
- ) -> None:
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("gateway elicitation preflight skipped", {"error": str(exc)})
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
- "gateway elicitation preflight answered",
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gopher-mcp-python
3
- Version: 0.1.38
3
+ Version: 0.1.39
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "gopher-mcp-python"
7
- version = "0.1.38"
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 test_create_with_url_passes_gateway_preflight_session_to_native(
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 GopherAgentRuntimeOptions(
176
- access_token=runtime_options.access_token,
177
- headers={
178
- **runtime_options.headers,
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(agent_module, "preflight_gateway_elicitation", preflight)
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"
@@ -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 result.headers["Mcp-Session-Id"] == "session-1"
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 test_does_nothing_for_non_gateway_urls() -> None:
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
- "https://mcp.example.com/mcp",
156
+ "stdio://mcp.example.com/mcp",
81
157
  runtime_options,
82
158
  GopherAgentCreateOptions(),
83
159
  opener=opener,