gopher-mcp-python 0.1.30__tar.gz → 0.1.34__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 (73) hide show
  1. {gopher_mcp_python-0.1.30 → gopher_mcp_python-0.1.34}/PKG-INFO +4 -1
  2. {gopher_mcp_python-0.1.30 → gopher_mcp_python-0.1.34}/README.md +3 -0
  3. {gopher_mcp_python-0.1.30 → gopher_mcp_python-0.1.34}/gopher_mcp_python/__init__.py +90 -58
  4. {gopher_mcp_python-0.1.30 → gopher_mcp_python-0.1.34}/gopher_mcp_python/agent.py +216 -76
  5. {gopher_mcp_python-0.1.30 → gopher_mcp_python-0.1.34}/gopher_mcp_python/config.py +38 -90
  6. {gopher_mcp_python-0.1.30 → gopher_mcp_python-0.1.34}/gopher_mcp_python/ffi/auth/loader.py +9 -9
  7. {gopher_mcp_python-0.1.30 → gopher_mcp_python-0.1.34}/gopher_mcp_python/ffi/library.py +140 -96
  8. gopher_mcp_python-0.1.34/gopher_mcp_python/oauth_authorization_url.py +49 -0
  9. gopher_mcp_python-0.1.34/gopher_mcp_python/oauth_browser.py +33 -0
  10. gopher_mcp_python-0.1.34/gopher_mcp_python/oauth_discovery.py +341 -0
  11. gopher_mcp_python-0.1.34/gopher_mcp_python/oauth_loopback.py +148 -0
  12. gopher_mcp_python-0.1.34/gopher_mcp_python/oauth_pkce.py +21 -0
  13. gopher_mcp_python-0.1.34/gopher_mcp_python/oauth_registration.py +89 -0
  14. gopher_mcp_python-0.1.34/gopher_mcp_python/oauth_resolver.py +362 -0
  15. gopher_mcp_python-0.1.34/gopher_mcp_python/oauth_runtime_options.py +29 -0
  16. gopher_mcp_python-0.1.34/gopher_mcp_python/oauth_server_targets.py +76 -0
  17. gopher_mcp_python-0.1.34/gopher_mcp_python/oauth_token_exchange.py +104 -0
  18. gopher_mcp_python-0.1.34/gopher_mcp_python/oauth_token_store.py +75 -0
  19. gopher_mcp_python-0.1.34/gopher_mcp_python/runtime_options.py +286 -0
  20. gopher_mcp_python-0.1.34/gopher_mcp_python/server_config.py +112 -0
  21. {gopher_mcp_python-0.1.30 → gopher_mcp_python-0.1.34}/gopher_mcp_python.egg-info/PKG-INFO +4 -1
  22. {gopher_mcp_python-0.1.30 → gopher_mcp_python-0.1.34}/gopher_mcp_python.egg-info/SOURCES.txt +31 -1
  23. {gopher_mcp_python-0.1.30 → gopher_mcp_python-0.1.34}/pyproject.toml +1 -1
  24. {gopher_mcp_python-0.1.30 → gopher_mcp_python-0.1.34}/tests/test_agent_create_by.py +31 -8
  25. gopher_mcp_python-0.1.34/tests/test_agent_create_with_oauth.py +376 -0
  26. {gopher_mcp_python-0.1.30 → gopher_mcp_python-0.1.34}/tests/test_agent_error_message.py +9 -3
  27. gopher_mcp_python-0.1.34/tests/test_agent_lifecycle.py +77 -0
  28. {gopher_mcp_python-0.1.30 → gopher_mcp_python-0.1.34}/tests/test_agent_runtime_options.py +29 -2
  29. {gopher_mcp_python-0.1.30 → gopher_mcp_python-0.1.34}/tests/test_auth_exports.py +9 -6
  30. {gopher_mcp_python-0.1.30 → gopher_mcp_python-0.1.34}/tests/test_config.py +109 -1
  31. gopher_mcp_python-0.1.34/tests/test_dump_version.py +28 -0
  32. {gopher_mcp_python-0.1.30 → gopher_mcp_python-0.1.34}/tests/test_ffi.py +175 -0
  33. {gopher_mcp_python-0.1.30 → gopher_mcp_python-0.1.34}/tests/test_ffi_runtime_options.py +27 -1
  34. gopher_mcp_python-0.1.34/tests/test_header_create_by_url_example.py +54 -0
  35. {gopher_mcp_python-0.1.30 → gopher_mcp_python-0.1.34}/tests/test_library_search_paths.py +41 -4
  36. gopher_mcp_python-0.1.34/tests/test_linux_native_packaging.py +196 -0
  37. gopher_mcp_python-0.1.34/tests/test_oauth_authorization_url.py +112 -0
  38. gopher_mcp_python-0.1.34/tests/test_oauth_browser.py +53 -0
  39. gopher_mcp_python-0.1.34/tests/test_oauth_create_with_url_integration.py +210 -0
  40. gopher_mcp_python-0.1.34/tests/test_oauth_discovery.py +240 -0
  41. gopher_mcp_python-0.1.34/tests/test_oauth_loopback.py +80 -0
  42. gopher_mcp_python-0.1.34/tests/test_oauth_pkce.py +23 -0
  43. gopher_mcp_python-0.1.34/tests/test_oauth_registration.py +56 -0
  44. gopher_mcp_python-0.1.34/tests/test_oauth_resolver.py +136 -0
  45. gopher_mcp_python-0.1.34/tests/test_oauth_runtime_options.py +48 -0
  46. gopher_mcp_python-0.1.34/tests/test_oauth_server_targets.py +58 -0
  47. gopher_mcp_python-0.1.34/tests/test_oauth_token_exchange.py +91 -0
  48. gopher_mcp_python-0.1.34/tests/test_oauth_token_store.py +74 -0
  49. gopher_mcp_python-0.1.34/tests/test_server_config.py +74 -0
  50. gopher_mcp_python-0.1.30/gopher_mcp_python/server_config.py +0 -47
  51. {gopher_mcp_python-0.1.30 → gopher_mcp_python-0.1.34}/LICENSE +0 -0
  52. {gopher_mcp_python-0.1.30 → gopher_mcp_python-0.1.34}/gopher_mcp_python/auth/__init__.py +0 -0
  53. {gopher_mcp_python-0.1.30 → gopher_mcp_python-0.1.34}/gopher_mcp_python/auth/errors.py +0 -0
  54. {gopher_mcp_python-0.1.30 → gopher_mcp_python-0.1.34}/gopher_mcp_python/auth/gopher_auth.py +0 -0
  55. {gopher_mcp_python-0.1.30 → gopher_mcp_python-0.1.34}/gopher_mcp_python/auth/scope_helpers.py +0 -0
  56. {gopher_mcp_python-0.1.30 → gopher_mcp_python-0.1.34}/gopher_mcp_python/errors.py +0 -0
  57. {gopher_mcp_python-0.1.30 → gopher_mcp_python-0.1.34}/gopher_mcp_python/ffi/__init__.py +0 -0
  58. {gopher_mcp_python-0.1.30 → gopher_mcp_python-0.1.34}/gopher_mcp_python/ffi/auth/__init__.py +0 -0
  59. {gopher_mcp_python-0.1.30 → gopher_mcp_python-0.1.34}/gopher_mcp_python/ffi/auth/auth_client.py +0 -0
  60. {gopher_mcp_python-0.1.30 → gopher_mcp_python-0.1.34}/gopher_mcp_python/ffi/auth/auto_refresh.py +0 -0
  61. {gopher_mcp_python-0.1.30 → gopher_mcp_python-0.1.34}/gopher_mcp_python/ffi/auth/config_loader.py +0 -0
  62. {gopher_mcp_python-0.1.30 → gopher_mcp_python-0.1.34}/gopher_mcp_python/ffi/auth/oauth_client.py +0 -0
  63. {gopher_mcp_python-0.1.30 → gopher_mcp_python-0.1.34}/gopher_mcp_python/ffi/auth/session_manager.py +0 -0
  64. {gopher_mcp_python-0.1.30 → gopher_mcp_python-0.1.34}/gopher_mcp_python/ffi/auth/types.py +0 -0
  65. {gopher_mcp_python-0.1.30 → gopher_mcp_python-0.1.34}/gopher_mcp_python/ffi/auth/validation_options.py +0 -0
  66. {gopher_mcp_python-0.1.30 → gopher_mcp_python-0.1.34}/gopher_mcp_python/result.py +0 -0
  67. {gopher_mcp_python-0.1.30 → gopher_mcp_python-0.1.34}/gopher_mcp_python.egg-info/dependency_links.txt +0 -0
  68. {gopher_mcp_python-0.1.30 → gopher_mcp_python-0.1.34}/gopher_mcp_python.egg-info/requires.txt +0 -0
  69. {gopher_mcp_python-0.1.30 → gopher_mcp_python-0.1.34}/gopher_mcp_python.egg-info/top_level.txt +0 -0
  70. {gopher_mcp_python-0.1.30 → gopher_mcp_python-0.1.34}/setup.cfg +0 -0
  71. {gopher_mcp_python-0.1.30 → gopher_mcp_python-0.1.34}/setup.py +0 -0
  72. {gopher_mcp_python-0.1.30 → gopher_mcp_python-0.1.34}/tests/test_gopher_auth.py +0 -0
  73. {gopher_mcp_python-0.1.30 → gopher_mcp_python-0.1.34}/tests/test_result.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gopher-mcp-python
3
- Version: 0.1.30
3
+ Version: 0.1.34
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
@@ -61,6 +61,9 @@ Python SDK for gopher-mcp-python, providing AI agent orchestration with native C
61
61
  - Python 3.8 or higher
62
62
  - `venv` and `pip` for PyPI installation, examples, and development workflows
63
63
  - Native gopher-mcp-python library (built from source)
64
+ - On Linux, system OpenSSL runtime libraries (`libssl` / `libcrypto`) from
65
+ your distribution. Native wheels do not bundle OpenSSL, so OS security
66
+ updates remain effective.
64
67
 
65
68
  On Debian/Ubuntu, install the Python venv and pip packages before using the
66
69
  PyPI install path, running examples, or setting up development dependencies:
@@ -17,6 +17,9 @@ Python SDK for gopher-mcp-python, providing AI agent orchestration with native C
17
17
  - Python 3.8 or higher
18
18
  - `venv` and `pip` for PyPI installation, examples, and development workflows
19
19
  - Native gopher-mcp-python library (built from source)
20
+ - On Linux, system OpenSSL runtime libraries (`libssl` / `libcrypto`) from
21
+ your distribution. Native wheels do not bundle OpenSSL, so OS security
22
+ updates remain effective.
20
23
 
21
24
  On Debian/Ubuntu, install the Python venv and pip packages before using the
22
25
  PyPI install path, running examples, or setting up development dependencies:
@@ -22,11 +22,19 @@ Example:
22
22
  >>> agent.dispose()
23
23
  """
24
24
 
25
+ from importlib import import_module
26
+
25
27
  from gopher_mcp_python.agent import GopherAgent
26
28
  from gopher_mcp_python.config import (
27
29
  GopherAgentConfig,
28
30
  GopherAgentConfigBuilder,
31
+ )
32
+ from gopher_mcp_python.runtime_options import (
33
+ GopherAgentCreateOptions,
34
+ GopherAgentOAuthOptions,
29
35
  GopherAgentRuntimeOptions,
36
+ GopherAgentTokenRecord,
37
+ GopherAgentTokenStore,
30
38
  )
31
39
  from gopher_mcp_python.result import AgentResult, AgentResultStatus, AgentResultBuilder
32
40
  from gopher_mcp_python.errors import (
@@ -35,76 +43,100 @@ from gopher_mcp_python.errors import (
35
43
  ConnectionError,
36
44
  TimeoutError,
37
45
  )
38
- from gopher_mcp_python.server_config import ServerConfig
46
+ from gopher_mcp_python.server_config import ServerConfig, ServerConfigRoute
39
47
  from gopher_mcp_python.ffi import GopherOrchLibrary, GopherOrchHandle
40
48
 
41
- # Auth module re-exports
42
- from gopher_mcp_python.ffi.auth import (
43
- # Types
44
- GopherAuthError,
45
- AutoRefreshResult,
46
- RegistrationResponse,
47
- TokenResponse,
48
- ValidationResult,
49
- TokenPayload,
50
- GopherAuthContext,
51
- ERROR_DESCRIPTIONS,
52
- get_error_description,
53
- gopher_create_empty_auth_context,
54
- is_gopher_auth_error,
55
- # Classes
56
- GopherAuthClient,
57
- GopherAuthConfig,
58
- GopherOAuthClient,
59
- GopherSessionManager,
60
- GopherValidationOptions,
61
- # Functions
62
- gopher_auth_auto_refresh,
63
- gopher_auth_build_oidc_discovery_metadata,
64
- gopher_auth_build_oauth_server_metadata,
65
- gopher_auth_build_protected_resource_metadata,
66
- gopher_auth_extract_bearer_token,
67
- gopher_auth_extract_method,
68
- gopher_auth_extract_path,
69
- gopher_auth_url_decode,
70
- gopher_auth_url_encode,
71
- gopher_auth_validate_all_scopes,
72
- gopher_auth_validate_any_scopes,
73
- gopher_auth_validate_idp,
74
- gopher_create_validation_options,
75
- gopher_generate_www_authenticate_header,
76
- gopher_generate_www_authenticate_header_v2,
77
- gopher_get_auth_library_version,
78
- gopher_init_auth_library,
79
- gopher_is_auth_library_initialized,
80
- gopher_shutdown_auth_library,
81
- is_auth_available,
82
- )
83
- from gopher_mcp_python.auth import (
84
- GopherAuth,
85
- GopherAuthError as GopherAuthBaseError,
86
- ConfigurationError,
87
- InsufficientScopesError,
88
- JwksError,
89
- TokenExchangeError,
90
- TokenValidationError,
91
- has_all_scopes,
92
- has_any_scope,
93
- has_scope,
94
- )
49
+ __version__ = "0.1.34"
50
+
51
+ _AUTH_EXPORTS = {
52
+ "GopherAuth",
53
+ "GopherAuthError",
54
+ "ConfigurationError",
55
+ "InsufficientScopesError",
56
+ "JwksError",
57
+ "TokenExchangeError",
58
+ "TokenValidationError",
59
+ "has_all_scopes",
60
+ "has_any_scope",
61
+ "has_scope",
62
+ }
63
+
64
+ _AUTH_FFI_EXPORTS = {
65
+ "AutoRefreshResult",
66
+ "RegistrationResponse",
67
+ "TokenResponse",
68
+ "ValidationResult",
69
+ "TokenPayload",
70
+ "GopherAuthContext",
71
+ "ERROR_DESCRIPTIONS",
72
+ "get_error_description",
73
+ "gopher_create_empty_auth_context",
74
+ "is_gopher_auth_error",
75
+ "GopherAuthClient",
76
+ "GopherAuthConfig",
77
+ "GopherOAuthClient",
78
+ "GopherSessionManager",
79
+ "GopherValidationOptions",
80
+ "gopher_auth_auto_refresh",
81
+ "gopher_auth_build_oidc_discovery_metadata",
82
+ "gopher_auth_build_oauth_server_metadata",
83
+ "gopher_auth_build_protected_resource_metadata",
84
+ "gopher_auth_extract_bearer_token",
85
+ "gopher_auth_extract_method",
86
+ "gopher_auth_extract_path",
87
+ "gopher_auth_url_decode",
88
+ "gopher_auth_url_encode",
89
+ "gopher_auth_validate_all_scopes",
90
+ "gopher_auth_validate_any_scopes",
91
+ "gopher_auth_validate_idp",
92
+ "gopher_create_validation_options",
93
+ "gopher_generate_www_authenticate_header",
94
+ "gopher_generate_www_authenticate_header_v2",
95
+ "gopher_get_auth_library_version",
96
+ "gopher_init_auth_library",
97
+ "gopher_is_auth_library_initialized",
98
+ "gopher_shutdown_auth_library",
99
+ "is_auth_available",
100
+ }
101
+
102
+
103
+ def __getattr__(name: str):
104
+ if name in _AUTH_EXPORTS:
105
+ auth = import_module("gopher_mcp_python.auth")
106
+ value = getattr(auth, name)
107
+ globals()[name] = value
108
+ return value
109
+
110
+ if name == "GopherAuthFfiError":
111
+ from gopher_mcp_python.ffi.auth import GopherAuthError as GopherAuthFfiError
112
+
113
+ globals()[name] = GopherAuthFfiError
114
+ return GopherAuthFfiError
115
+
116
+ if name in _AUTH_FFI_EXPORTS:
117
+ ffi_auth = import_module("gopher_mcp_python.ffi.auth")
118
+ value = getattr(ffi_auth, name)
119
+ globals()[name] = value
120
+ return value
121
+
122
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
95
123
 
96
- __version__ = "0.1.30"
97
124
 
98
125
  __all__ = [
99
126
  # Main classes
100
127
  "GopherAgent",
101
128
  "GopherAgentConfig",
102
129
  "GopherAgentConfigBuilder",
130
+ "GopherAgentCreateOptions",
131
+ "GopherAgentOAuthOptions",
103
132
  "GopherAgentRuntimeOptions",
133
+ "GopherAgentTokenRecord",
134
+ "GopherAgentTokenStore",
104
135
  "AgentResult",
105
136
  "AgentResultStatus",
106
137
  "AgentResultBuilder",
107
138
  "ServerConfig",
139
+ "ServerConfigRoute",
108
140
  # Errors
109
141
  "AgentError",
110
142
  "ApiKeyError",
@@ -114,7 +146,9 @@ __all__ = [
114
146
  "GopherOrchLibrary",
115
147
  "GopherOrchHandle",
116
148
  # Auth
149
+ "GopherAuth",
117
150
  "GopherAuthError",
151
+ "GopherAuthFfiError",
118
152
  "AutoRefreshResult",
119
153
  "RegistrationResponse",
120
154
  "TokenResponse",
@@ -150,8 +184,6 @@ __all__ = [
150
184
  "gopher_is_auth_library_initialized",
151
185
  "gopher_shutdown_auth_library",
152
186
  "is_auth_available",
153
- "GopherAuth",
154
- "GopherAuthBaseError",
155
187
  "ConfigurationError",
156
188
  "InsufficientScopesError",
157
189
  "JwksError",
@@ -27,10 +27,21 @@ Example with context manager:
27
27
  """
28
28
 
29
29
  import atexit
30
+ import asyncio
31
+ import weakref
30
32
  from typing import Callable, Optional
31
33
 
34
+ import gopher_mcp_python.oauth_resolver as oauth_resolver
32
35
  from gopher_mcp_python.config import GopherAgentConfig
36
+ from gopher_mcp_python.runtime_options import (
37
+ GopherAgentOAuthOptions,
38
+ GopherAgentRuntimeOptions,
39
+ RuntimeOptionsInput,
40
+ normalize_create_options,
41
+ normalize_runtime_options,
42
+ )
33
43
  from gopher_mcp_python.result import AgentResult, AgentResultStatus
44
+ from gopher_mcp_python.server_config import ServerConfig, ServerConfigRoute
34
45
  from gopher_mcp_python.errors import AgentError, TimeoutError
35
46
  from gopher_mcp_python.ffi import GopherOrchLibrary, GopherOrchHandle
36
47
 
@@ -51,6 +62,7 @@ class GopherAgent:
51
62
  """
52
63
  self._handle = handle
53
64
  self._disposed = False
65
+ self._finalizer = weakref.finalize(self, _release_handle_best_effort, handle)
54
66
 
55
67
  def __enter__(self) -> "GopherAgent":
56
68
  """Context manager entry."""
@@ -103,7 +115,7 @@ class GopherAgent:
103
115
  @staticmethod
104
116
  def create(config: GopherAgentConfig) -> "GopherAgent":
105
117
  """
106
- Create a new GopherAgent instance.
118
+ Create a new GopherAgent instance, resolving SDK OAuth credentials if needed.
107
119
 
108
120
  Args:
109
121
  config: Agent configuration
@@ -114,46 +126,26 @@ class GopherAgent:
114
126
  Raises:
115
127
  AgentError: if agent creation fails
116
128
  """
117
- if not _initialized:
118
- GopherAgent.init()
119
-
120
- lib = GopherOrchLibrary.get_instance()
121
- if lib is None:
122
- load_error = GopherOrchLibrary.get_load_error_message()
123
- raise AgentError(f"Native library not available.\n{load_error}")
124
-
125
- handle: Optional[GopherOrchHandle] = None
126
- try:
127
- if config.has_api_key():
128
- handle = lib.agent_create_by_api_key(
129
- config.provider,
130
- config.model,
131
- config.api_key,
132
- config.runtime_options,
133
- )
134
- else:
135
- handle = lib.agent_create_by_json(
136
- config.provider,
137
- config.model,
138
- config.server_config,
139
- config.runtime_options,
140
- )
141
- except Exception as e:
142
- raise AgentError(f"Failed to create agent: {e}")
143
-
144
- if handle is None:
145
- error = lib.get_last_error_message()
146
- lib.clear_error()
147
- raise AgentError(error or _build_create_error_message())
148
-
149
- return GopherAgent(handle)
129
+ if config.has_api_key():
130
+ return GopherAgent.create_with_api_key(
131
+ config.provider,
132
+ config.model,
133
+ config.api_key,
134
+ config.runtime_options,
135
+ )
136
+ return GopherAgent.create_with_server_config(
137
+ config.provider,
138
+ config.model,
139
+ config.server_config,
140
+ config.runtime_options,
141
+ )
150
142
 
151
143
  @staticmethod
152
144
  def create_with_api_key(
153
145
  provider: str,
154
146
  model: str,
155
147
  api_key: str,
156
- runtime_options: Optional[object] = None,
148
+ runtime_options: RuntimeOptionsInput = None,
157
149
  ) -> "GopherAgent":
158
150
  """
159
151
  Create a new GopherAgent with API key.
@@ -167,22 +159,31 @@ class GopherAgent:
167
159
  Returns:
168
160
  GopherAgent instance
169
161
  """
170
- builder = (
171
- GopherAgentConfig.builder()
172
- .provider(provider)
173
- .model(model)
174
- .api_key(api_key)
162
+ create_options = normalize_create_options(runtime_options)
163
+ runtime_options = normalize_runtime_options(create_options)
164
+ oauth = create_options.oauth if create_options is not None else None
165
+ if _should_skip_oauth(runtime_options, oauth):
166
+ return GopherAgent._create_from_ffi(
167
+ lambda lib: lib.agent_create_by_api_key(
168
+ provider, model, api_key, runtime_options
169
+ )
170
+ )
171
+
172
+ return _create_from_api_config(
173
+ provider,
174
+ model,
175
+ api_key,
176
+ route=None,
177
+ runtime_options=runtime_options,
178
+ oauth=oauth,
175
179
  )
176
- if runtime_options is not None:
177
- builder.runtime_options(runtime_options)
178
- return GopherAgent.create(builder.build())
179
180
 
180
181
  @staticmethod
181
182
  def create_with_server_config(
182
183
  provider: str,
183
184
  model: str,
184
185
  server_config: str,
185
- runtime_options: Optional[object] = None,
186
+ runtime_options: RuntimeOptionsInput = None,
186
187
  ) -> "GopherAgent":
187
188
  """
188
189
  Create a new GopherAgent with JSON server config.
@@ -196,15 +197,29 @@ class GopherAgent:
196
197
  Returns:
197
198
  GopherAgent instance
198
199
  """
199
- builder = (
200
- GopherAgentConfig.builder()
201
- .provider(provider)
202
- .model(model)
203
- .server_config(server_config)
200
+ create_options = normalize_create_options(runtime_options)
201
+ runtime_options = normalize_runtime_options(create_options)
202
+ oauth = create_options.oauth if create_options is not None else None
203
+ if _should_skip_oauth(runtime_options, oauth):
204
+ return GopherAgent._create_from_ffi(
205
+ lambda lib: lib.agent_create_by_json(
206
+ provider, model, server_config, runtime_options
207
+ )
208
+ )
209
+
210
+ resolved_runtime_options = _run_oauth_coroutine(
211
+ lambda: oauth_resolver.resolve_runtime_options_with_oauth(
212
+ urls=[],
213
+ server_config=server_config,
214
+ runtime_options=runtime_options,
215
+ oauth=oauth,
216
+ )
217
+ )
218
+ return GopherAgent._create_from_ffi(
219
+ lambda lib: lib.agent_create_by_json(
220
+ provider, model, server_config, resolved_runtime_options
221
+ )
204
222
  )
205
- if runtime_options is not None:
206
- builder.runtime_options(runtime_options)
207
- return GopherAgent.create(builder.build())
208
223
 
209
224
  @staticmethod
210
225
  def create_with_server_id(
@@ -212,7 +227,7 @@ class GopherAgent:
212
227
  model: str,
213
228
  api_key: str,
214
229
  server_id: str,
215
- runtime_options: Optional[object] = None,
230
+ runtime_options: RuntimeOptionsInput = None,
216
231
  ) -> "GopherAgent":
217
232
  """
218
233
  Create a new GopherAgent scoped to a single MCP server by id.
@@ -231,10 +246,22 @@ class GopherAgent:
231
246
  Returns:
232
247
  GopherAgent instance
233
248
  """
234
- return GopherAgent._create_from_ffi(
235
- lambda lib: lib.agent_create_by_server_id(
236
- provider, model, api_key, server_id, runtime_options
249
+ create_options = normalize_create_options(runtime_options)
250
+ normalized_runtime_options = normalize_runtime_options(create_options)
251
+ oauth = create_options.oauth if create_options is not None else None
252
+ if _should_skip_oauth(normalized_runtime_options, oauth):
253
+ return GopherAgent._create_from_ffi(
254
+ lambda lib: lib.agent_create_by_server_id(
255
+ provider, model, api_key, server_id, normalized_runtime_options
256
+ )
237
257
  )
258
+ return _create_from_api_config(
259
+ provider,
260
+ model,
261
+ api_key,
262
+ route=ServerConfigRoute("serverId", server_id),
263
+ runtime_options=normalized_runtime_options,
264
+ oauth=oauth,
238
265
  )
239
266
 
240
267
  @staticmethod
@@ -243,7 +270,7 @@ class GopherAgent:
243
270
  model: str,
244
271
  api_key: str,
245
272
  server_name: str,
246
- runtime_options: Optional[object] = None,
273
+ runtime_options: RuntimeOptionsInput = None,
247
274
  ) -> "GopherAgent":
248
275
  """
249
276
  Create a new GopherAgent scoped to a single MCP server by name.
@@ -262,10 +289,22 @@ class GopherAgent:
262
289
  Returns:
263
290
  GopherAgent instance
264
291
  """
265
- return GopherAgent._create_from_ffi(
266
- lambda lib: lib.agent_create_by_server_name(
267
- provider, model, api_key, server_name, runtime_options
292
+ create_options = normalize_create_options(runtime_options)
293
+ normalized_runtime_options = normalize_runtime_options(create_options)
294
+ oauth = create_options.oauth if create_options is not None else None
295
+ if _should_skip_oauth(normalized_runtime_options, oauth):
296
+ return GopherAgent._create_from_ffi(
297
+ lambda lib: lib.agent_create_by_server_name(
298
+ provider, model, api_key, server_name, normalized_runtime_options
299
+ )
268
300
  )
301
+ return _create_from_api_config(
302
+ provider,
303
+ model,
304
+ api_key,
305
+ route=ServerConfigRoute("serverName", server_name),
306
+ runtime_options=normalized_runtime_options,
307
+ oauth=oauth,
269
308
  )
270
309
 
271
310
  @staticmethod
@@ -274,7 +313,7 @@ class GopherAgent:
274
313
  model: str,
275
314
  api_key: str,
276
315
  gateway_id: str,
277
- runtime_options: Optional[object] = None,
316
+ runtime_options: RuntimeOptionsInput = None,
278
317
  ) -> "GopherAgent":
279
318
  """
280
319
  Create a new GopherAgent scoped to a single MCP gateway by id.
@@ -293,10 +332,22 @@ class GopherAgent:
293
332
  Returns:
294
333
  GopherAgent instance
295
334
  """
296
- return GopherAgent._create_from_ffi(
297
- lambda lib: lib.agent_create_by_gateway_id(
298
- provider, model, api_key, gateway_id, runtime_options
335
+ create_options = normalize_create_options(runtime_options)
336
+ normalized_runtime_options = normalize_runtime_options(create_options)
337
+ oauth = create_options.oauth if create_options is not None else None
338
+ if _should_skip_oauth(normalized_runtime_options, oauth):
339
+ return GopherAgent._create_from_ffi(
340
+ lambda lib: lib.agent_create_by_gateway_id(
341
+ provider, model, api_key, gateway_id, normalized_runtime_options
342
+ )
299
343
  )
344
+ return _create_from_api_config(
345
+ provider,
346
+ model,
347
+ api_key,
348
+ route=ServerConfigRoute("gatewayId", gateway_id),
349
+ runtime_options=normalized_runtime_options,
350
+ oauth=oauth,
300
351
  )
301
352
 
302
353
  @staticmethod
@@ -305,7 +356,7 @@ class GopherAgent:
305
356
  model: str,
306
357
  api_key: str,
307
358
  gateway_name: str,
308
- runtime_options: Optional[object] = None,
359
+ runtime_options: RuntimeOptionsInput = None,
309
360
  ) -> "GopherAgent":
310
361
  """
311
362
  Create a new GopherAgent scoped to a single MCP gateway by name.
@@ -324,10 +375,22 @@ class GopherAgent:
324
375
  Returns:
325
376
  GopherAgent instance
326
377
  """
327
- return GopherAgent._create_from_ffi(
328
- lambda lib: lib.agent_create_by_gateway_name(
329
- provider, model, api_key, gateway_name, runtime_options
378
+ create_options = normalize_create_options(runtime_options)
379
+ normalized_runtime_options = normalize_runtime_options(create_options)
380
+ oauth = create_options.oauth if create_options is not None else None
381
+ if _should_skip_oauth(normalized_runtime_options, oauth):
382
+ return GopherAgent._create_from_ffi(
383
+ lambda lib: lib.agent_create_by_gateway_name(
384
+ provider, model, api_key, gateway_name, normalized_runtime_options
385
+ )
330
386
  )
387
+ return _create_from_api_config(
388
+ provider,
389
+ model,
390
+ api_key,
391
+ route=ServerConfigRoute("gatewayName", gateway_name),
392
+ runtime_options=normalized_runtime_options,
393
+ oauth=oauth,
331
394
  )
332
395
 
333
396
  @staticmethod
@@ -335,7 +398,7 @@ class GopherAgent:
335
398
  provider: str,
336
399
  model: str,
337
400
  url: str,
338
- runtime_options: Optional[object] = None,
401
+ runtime_options: RuntimeOptionsInput = None,
339
402
  ) -> "GopherAgent":
340
403
  """
341
404
  Create a new GopherAgent for a single MCP server reachable at a URL.
@@ -354,8 +417,21 @@ class GopherAgent:
354
417
  Returns:
355
418
  GopherAgent instance
356
419
  """
420
+ create_options = normalize_create_options(runtime_options)
421
+ normalized_runtime_options = normalize_runtime_options(create_options)
422
+ oauth = create_options.oauth if create_options is not None else None
423
+ if url != "" and not _should_skip_oauth(normalized_runtime_options, oauth):
424
+ normalized_runtime_options = _run_oauth_coroutine(
425
+ lambda: oauth_resolver.resolve_url_runtime_options_with_oauth(
426
+ url,
427
+ runtime_options=normalized_runtime_options,
428
+ oauth=oauth,
429
+ )
430
+ )
357
431
  return GopherAgent._create_from_ffi(
358
- lambda lib: lib.agent_create_by_url(provider, model, url, runtime_options)
432
+ lambda lib: lib.agent_create_by_url(
433
+ provider, model, url, normalized_runtime_options
434
+ )
359
435
  )
360
436
 
361
437
  @staticmethod
@@ -379,6 +455,8 @@ class GopherAgent:
379
455
 
380
456
  try:
381
457
  handle = create_handle(lib)
458
+ except AgentError:
459
+ raise
382
460
  except Exception as e:
383
461
  raise AgentError(f"Failed to create agent: {e}")
384
462
 
@@ -449,14 +527,18 @@ class GopherAgent:
449
527
  return AgentResult.error(str(e))
450
528
 
451
529
  def dispose(self) -> None:
452
- """Dispose of the agent and free resources."""
530
+ """
531
+ Dispose of the agent and free native resources deterministically.
532
+
533
+ A best-effort finalizer also releases forgotten agents during garbage
534
+ collection, but callers should prefer dispose() or a with block.
535
+ """
453
536
  if self._disposed:
454
537
  return
455
538
 
456
539
  self._disposed = True
457
- lib = GopherOrchLibrary.get_instance()
458
- if lib is not None and self._handle is not None:
459
- lib.agent_release(self._handle)
540
+ if self._finalizer.alive:
541
+ self._finalizer()
460
542
 
461
543
  def is_disposed(self) -> bool:
462
544
  """Check if agent is disposed."""
@@ -478,6 +560,64 @@ def _setup_cleanup_handler() -> None:
478
560
  atexit.register(GopherAgent.shutdown)
479
561
 
480
562
 
563
+ def _release_handle_best_effort(handle: GopherOrchHandle) -> None:
564
+ try:
565
+ lib = GopherOrchLibrary.get_instance()
566
+ if lib is not None and handle is not None:
567
+ lib.agent_release(handle)
568
+ except Exception:
569
+ return
570
+
571
+
572
+ def _create_from_api_config(
573
+ provider: str,
574
+ model: str,
575
+ api_key: str,
576
+ route: Optional[ServerConfigRoute],
577
+ runtime_options: Optional[GopherAgentRuntimeOptions],
578
+ oauth: Optional[GopherAgentOAuthOptions],
579
+ ) -> GopherAgent:
580
+ server_config = ServerConfig.fetch(api_key, route=route)
581
+ resolved_runtime_options = _run_oauth_coroutine(
582
+ lambda: oauth_resolver.resolve_runtime_options_with_oauth(
583
+ urls=[],
584
+ server_config=server_config,
585
+ runtime_options=runtime_options,
586
+ oauth=oauth,
587
+ )
588
+ )
589
+ return GopherAgent._create_from_ffi(
590
+ lambda lib: lib.agent_create_by_json(
591
+ provider, model, server_config, resolved_runtime_options
592
+ )
593
+ )
594
+
595
+
596
+ def _run_oauth_coroutine(create_coroutine):
597
+ try:
598
+ asyncio.get_running_loop()
599
+ except RuntimeError:
600
+ return asyncio.run(create_coroutine())
601
+ raise AgentError(
602
+ "SDK OAuth auto-flow cannot run inside an active asyncio event loop. "
603
+ "Provide runtime_options with access_token/Authorization, or set "
604
+ 'oauth.mode to "disabled".'
605
+ )
606
+
607
+
608
+ def _should_skip_oauth(
609
+ runtime_options: Optional[GopherAgentRuntimeOptions],
610
+ oauth: Optional[GopherAgentOAuthOptions],
611
+ ) -> bool:
612
+ if oauth is not None and oauth.mode == "disabled":
613
+ return True
614
+ if runtime_options is None:
615
+ return False
616
+ if runtime_options.access_token is not None:
617
+ return True
618
+ return any(name.lower() == "authorization" for name in runtime_options.headers)
619
+
620
+
481
621
  def _build_create_error_message() -> str:
482
622
  """
483
623
  Build the AgentError message for a null native create*() result.
@@ -490,6 +630,6 @@ def _build_create_error_message() -> str:
490
630
  "Failed to create agent: native library returned null without a "
491
631
  "specific error. Most often this means every configured MCP server "
492
632
  "failed to connect or returned no tools (TLS / network / bad URL), "
493
- "or the LLM provider could not be initialized. Set GOPHER_DEBUG=1 to "
494
- "see native-side logs."
633
+ "or the LLM provider could not be initialized. Set DEBUG=1 to see "
634
+ "Python-side native library load diagnostics and native-side logs."
495
635
  )