gopher-mcp-python 0.1.1__py3-none-any.whl

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.
@@ -0,0 +1,365 @@
1
+ """
2
+ ctypes interface to the gopher-mcp-python native library.
3
+ """
4
+
5
+ import ctypes
6
+ import os
7
+ import sys
8
+ from ctypes import c_char_p, c_void_p, c_int32, c_int64, POINTER, Structure
9
+ from pathlib import Path
10
+ from typing import Optional, Any
11
+
12
+ # Type alias for opaque handle
13
+ GopherOrchHandle = c_void_p
14
+
15
+
16
+ class GopherOrchErrorInfo(Structure):
17
+ """
18
+ Error info structure matching C:
19
+ typedef struct {
20
+ gopher_orch_error_t code;
21
+ const char* message;
22
+ const char* details;
23
+ const char* file;
24
+ int32_t line;
25
+ } gopher_orch_error_info_t;
26
+ """
27
+
28
+ _fields_ = [
29
+ ("code", c_int32),
30
+ ("message", c_char_p),
31
+ ("details", c_char_p),
32
+ ("file", c_char_p),
33
+ ("line", c_int32),
34
+ ]
35
+
36
+
37
+ class GopherOrchLibrary:
38
+ """
39
+ Wrapper for the gopher-mcp-python native library using ctypes.
40
+ """
41
+
42
+ _instance: Optional["GopherOrchLibrary"] = None
43
+ _lib: Optional[ctypes.CDLL] = None
44
+ _available: bool = False
45
+ _debug: bool = False
46
+
47
+ def __init__(self) -> None:
48
+ self._load_library()
49
+
50
+ @classmethod
51
+ def get_instance(cls) -> Optional["GopherOrchLibrary"]:
52
+ """
53
+ Get the library instance, loading it if necessary.
54
+ """
55
+ if cls._instance is None:
56
+ cls._instance = GopherOrchLibrary()
57
+ return cls._instance if cls._instance._available else None
58
+
59
+ @classmethod
60
+ def is_available(cls) -> bool:
61
+ """
62
+ Check if the library is available.
63
+ """
64
+ instance = cls.get_instance()
65
+ return instance is not None and instance._available
66
+
67
+ def _load_library(self) -> None:
68
+ self._debug = os.environ.get("DEBUG") is not None
69
+
70
+ library_name = self._get_library_name()
71
+ search_paths = self._get_search_paths()
72
+
73
+ # Try custom path from environment variable
74
+ env_path = os.environ.get("GOPHER_MCP_PYTHON_LIBRARY_PATH")
75
+ if env_path and os.path.exists(env_path):
76
+ try:
77
+ self._lib = ctypes.CDLL(env_path)
78
+ self._setup_functions()
79
+ self._available = True
80
+ return
81
+ except OSError as e:
82
+ if self._debug:
83
+ print(
84
+ f"Failed to load from GOPHER_MCP_PYTHON_LIBRARY_PATH: {e}",
85
+ file=sys.stderr,
86
+ )
87
+
88
+ # Try search paths
89
+ for search_path in search_paths:
90
+ lib_file = os.path.join(search_path, library_name)
91
+ if os.path.exists(lib_file):
92
+ try:
93
+ self._lib = ctypes.CDLL(lib_file)
94
+ self._setup_functions()
95
+ self._available = True
96
+ return
97
+ except OSError as e:
98
+ if self._debug:
99
+ print(f"Failed to load from {search_path}: {e}", file=sys.stderr)
100
+
101
+ # Try loading by name (system paths)
102
+ try:
103
+ self._lib = ctypes.CDLL(library_name)
104
+ self._setup_functions()
105
+ self._available = True
106
+ return
107
+ except OSError as e:
108
+ if self._debug:
109
+ print(f"Failed to load gopher-mcp-python library: {e}", file=sys.stderr)
110
+ print("Searched paths:", file=sys.stderr)
111
+ for p in search_paths:
112
+ print(f" - {p}", file=sys.stderr)
113
+
114
+ self._available = False
115
+
116
+ def _setup_functions(self) -> None:
117
+ if self._lib is None:
118
+ return
119
+
120
+ # Agent functions
121
+ self._lib.gopher_orch_agent_create_by_json.argtypes = [
122
+ c_char_p,
123
+ c_char_p,
124
+ c_char_p,
125
+ ]
126
+ self._lib.gopher_orch_agent_create_by_json.restype = c_void_p
127
+
128
+ self._lib.gopher_orch_agent_create_by_api_key.argtypes = [
129
+ c_char_p,
130
+ c_char_p,
131
+ c_char_p,
132
+ ]
133
+ self._lib.gopher_orch_agent_create_by_api_key.restype = c_void_p
134
+
135
+ self._lib.gopher_orch_agent_run.argtypes = [c_void_p, c_char_p, c_int64]
136
+ self._lib.gopher_orch_agent_run.restype = c_char_p
137
+
138
+ self._lib.gopher_orch_agent_add_ref.argtypes = [c_void_p]
139
+ self._lib.gopher_orch_agent_add_ref.restype = None
140
+
141
+ self._lib.gopher_orch_agent_release.argtypes = [c_void_p]
142
+ self._lib.gopher_orch_agent_release.restype = None
143
+
144
+ # API functions
145
+ self._lib.gopher_orch_api_fetch_servers.argtypes = [c_char_p]
146
+ self._lib.gopher_orch_api_fetch_servers.restype = c_char_p
147
+
148
+ # Error functions
149
+ self._lib.gopher_orch_last_error.argtypes = []
150
+ self._lib.gopher_orch_last_error.restype = POINTER(GopherOrchErrorInfo)
151
+
152
+ self._lib.gopher_orch_clear_error.argtypes = []
153
+ self._lib.gopher_orch_clear_error.restype = None
154
+
155
+ self._lib.gopher_orch_free.argtypes = [c_void_p]
156
+ self._lib.gopher_orch_free.restype = None
157
+
158
+ # Logging functions (optional - may not exist in all versions)
159
+ try:
160
+ self._lib.gopher_orch_set_log_level.argtypes = [c_int32]
161
+ self._lib.gopher_orch_set_log_level.restype = None
162
+ # Set default log level to Warning (3) for production use
163
+ # This suppresses debug and info logs that appear during normal operation
164
+ self._lib.gopher_orch_set_log_level(3)
165
+ except AttributeError:
166
+ # Function not available in this version of the library
167
+ pass
168
+
169
+ def _get_library_name(self) -> str:
170
+ if sys.platform == "darwin":
171
+ return "libgopher-orch.dylib"
172
+ elif sys.platform == "win32":
173
+ return "gopher-orch.dll"
174
+ else:
175
+ return "libgopher-orch.so"
176
+
177
+ def _get_platform_package_path(self) -> Optional[str]:
178
+ """
179
+ Get the path to the platform-specific native package.
180
+ These packages are published as gopher-mcp-python-native-{platform}-{arch}
181
+ and contain the native library for that specific platform.
182
+ """
183
+ import platform as plat
184
+
185
+ # Determine platform and architecture
186
+ system = sys.platform # 'darwin', 'linux', 'win32'
187
+ machine = plat.machine().lower() # 'arm64', 'x86_64', 'amd64'
188
+
189
+ # Map machine names to our arch names
190
+ arch_map = {
191
+ "arm64": "arm64",
192
+ "aarch64": "arm64",
193
+ "x86_64": "x64",
194
+ "amd64": "x64",
195
+ "x64": "x64",
196
+ }
197
+ arch = arch_map.get(machine)
198
+ if not arch:
199
+ if self._debug:
200
+ print(f"Unsupported architecture: {machine}", file=sys.stderr)
201
+ return None
202
+
203
+ # Map platform names
204
+ platform_map = {
205
+ "darwin": "darwin",
206
+ "linux": "linux",
207
+ "win32": "win32",
208
+ }
209
+ platform_name = platform_map.get(system)
210
+ if not platform_name:
211
+ if self._debug:
212
+ print(f"Unsupported platform: {system}", file=sys.stderr)
213
+ return None
214
+
215
+ # Construct the package name
216
+ package_name = f"gopher_mcp_python_native_{platform_name}_{arch}"
217
+
218
+ try:
219
+ # Try to import the platform-specific package
220
+ native_pkg = __import__(package_name)
221
+ lib_path = native_pkg.get_lib_path()
222
+ if lib_path.exists():
223
+ if self._debug:
224
+ print(f"Found platform package at: {lib_path}", file=sys.stderr)
225
+ return str(lib_path)
226
+ except ImportError:
227
+ # Package not installed - this is expected on platforms where
228
+ # the package wasn't installed
229
+ if self._debug:
230
+ print(f"Platform package {package_name} not found", file=sys.stderr)
231
+
232
+ return None
233
+
234
+ def _get_search_paths(self) -> list:
235
+ paths = []
236
+
237
+ # 1. Try platform-specific package first (pip distribution)
238
+ platform_path = self._get_platform_package_path()
239
+ if platform_path:
240
+ paths.append(platform_path)
241
+
242
+ # 2. Get the directory containing this module for development fallbacks
243
+ module_dir = Path(__file__).parent.parent.parent
244
+
245
+ # Development paths (native/lib in various locations)
246
+ paths.extend([
247
+ # Project root native/lib
248
+ os.path.join(os.getcwd(), "native", "lib"),
249
+ # Relative to module location
250
+ os.path.join(module_dir, "native", "lib"),
251
+ os.path.join(module_dir.parent, "native", "lib"),
252
+ ])
253
+
254
+ # 3. System paths as last resort
255
+ if sys.platform == "darwin":
256
+ paths.extend(["/usr/local/lib", "/opt/homebrew/lib"])
257
+ paths.append("/usr/lib")
258
+
259
+ return paths
260
+
261
+ # Agent functions
262
+ def agent_create_by_json(
263
+ self, provider: str, model: str, server_json: str
264
+ ) -> Optional[GopherOrchHandle]:
265
+ """Create an agent using JSON server configuration."""
266
+ if not self._available or self._lib is None:
267
+ return None
268
+ return self._lib.gopher_orch_agent_create_by_json(
269
+ provider.encode("utf-8"),
270
+ model.encode("utf-8"),
271
+ server_json.encode("utf-8"),
272
+ )
273
+
274
+ def agent_create_by_api_key(
275
+ self, provider: str, model: str, api_key: str
276
+ ) -> Optional[GopherOrchHandle]:
277
+ """Create an agent using API key."""
278
+ if not self._available or self._lib is None:
279
+ return None
280
+ return self._lib.gopher_orch_agent_create_by_api_key(
281
+ provider.encode("utf-8"),
282
+ model.encode("utf-8"),
283
+ api_key.encode("utf-8"),
284
+ )
285
+
286
+ def agent_run(
287
+ self, agent: GopherOrchHandle, query: str, timeout_ms: int
288
+ ) -> Optional[str]:
289
+ """Run a query against the agent."""
290
+ if not self._available or self._lib is None:
291
+ return None
292
+ result = self._lib.gopher_orch_agent_run(
293
+ agent, query.encode("utf-8"), timeout_ms
294
+ )
295
+ if result:
296
+ return result.decode("utf-8")
297
+ return None
298
+
299
+ def agent_add_ref(self, agent: GopherOrchHandle) -> None:
300
+ """Add a reference to the agent."""
301
+ if self._available and self._lib is not None:
302
+ self._lib.gopher_orch_agent_add_ref(agent)
303
+
304
+ def agent_release(self, agent: GopherOrchHandle) -> None:
305
+ """Release the agent."""
306
+ if self._available and self._lib is not None:
307
+ self._lib.gopher_orch_agent_release(agent)
308
+
309
+ # API functions
310
+ def api_fetch_servers(self, api_key: str) -> Optional[str]:
311
+ """Fetch server configuration from API."""
312
+ if not self._available or self._lib is None:
313
+ return None
314
+ result = self._lib.gopher_orch_api_fetch_servers(api_key.encode("utf-8"))
315
+ if result:
316
+ return result.decode("utf-8")
317
+ return None
318
+
319
+ # Error functions
320
+ def last_error(self) -> Optional[GopherOrchErrorInfo]:
321
+ """Get the last error info."""
322
+ if not self._available or self._lib is None:
323
+ return None
324
+ error_ptr = self._lib.gopher_orch_last_error()
325
+ if error_ptr and error_ptr.contents:
326
+ return error_ptr.contents
327
+ return None
328
+
329
+ def get_last_error_message(self) -> Optional[str]:
330
+ """Get the last error message."""
331
+ error_info = self.last_error()
332
+ if error_info and error_info.message:
333
+ return error_info.message.decode("utf-8")
334
+ return None
335
+
336
+ def clear_error(self) -> None:
337
+ """Clear the last error."""
338
+ if self._available and self._lib is not None:
339
+ self._lib.gopher_orch_clear_error()
340
+
341
+ def free(self, ptr: Any) -> None:
342
+ """Free memory allocated by the library."""
343
+ if self._available and self._lib is not None:
344
+ self._lib.gopher_orch_free(ptr)
345
+
346
+ def set_log_level(self, level: int) -> None:
347
+ """
348
+ Set the global log level for the native library.
349
+
350
+ Log levels:
351
+ 0 = Debug (most verbose)
352
+ 1 = Info
353
+ 2 = Notice
354
+ 3 = Warning (default for production)
355
+ 4 = Error
356
+ 5 = Critical
357
+ 6 = Alert
358
+ 7 = Emergency
359
+ 8 = Off (no logging)
360
+
361
+ Args:
362
+ level: Log level (0-8)
363
+ """
364
+ if self._available and self._lib is not None:
365
+ self._lib.gopher_orch_set_log_level(level)
@@ -0,0 +1,192 @@
1
+ """
2
+ Result classes for the Gopher Security MCP SDK.
3
+
4
+ Provides structured result objects with status, response, and metadata.
5
+ """
6
+
7
+ from enum import Enum
8
+ from typing import Optional
9
+
10
+
11
+ class AgentResultStatus(Enum):
12
+ """Status of an agent operation."""
13
+
14
+ SUCCESS = "success"
15
+ ERROR = "error"
16
+ TIMEOUT = "timeout"
17
+
18
+
19
+ class AgentResult:
20
+ """
21
+ Result of an agent operation.
22
+
23
+ Contains the response, status, and optional metadata.
24
+ """
25
+
26
+ def __init__(
27
+ self,
28
+ response: str,
29
+ status: AgentResultStatus,
30
+ iteration_count: int = 0,
31
+ tokens_used: int = 0,
32
+ error_message: Optional[str] = None,
33
+ ) -> None:
34
+ """
35
+ Initialize an AgentResult.
36
+
37
+ Args:
38
+ response: The agent's response text
39
+ status: The result status
40
+ iteration_count: Number of iterations performed
41
+ tokens_used: Number of tokens consumed
42
+ error_message: Error message if status is ERROR or TIMEOUT
43
+ """
44
+ self._response = response
45
+ self._status = status
46
+ self._iteration_count = iteration_count
47
+ self._tokens_used = tokens_used
48
+ self._error_message = error_message
49
+
50
+ @property
51
+ def response(self) -> str:
52
+ """Get the response text."""
53
+ return self._response
54
+
55
+ @property
56
+ def status(self) -> AgentResultStatus:
57
+ """Get the result status."""
58
+ return self._status
59
+
60
+ @property
61
+ def iteration_count(self) -> int:
62
+ """Get the number of iterations."""
63
+ return self._iteration_count
64
+
65
+ @property
66
+ def tokens_used(self) -> int:
67
+ """Get the number of tokens used."""
68
+ return self._tokens_used
69
+
70
+ @property
71
+ def error_message(self) -> Optional[str]:
72
+ """Get the error message if any."""
73
+ return self._error_message
74
+
75
+ def is_success(self) -> bool:
76
+ """Check if the result is successful."""
77
+ return self._status == AgentResultStatus.SUCCESS
78
+
79
+ def is_error(self) -> bool:
80
+ """Check if the result is an error."""
81
+ return self._status == AgentResultStatus.ERROR
82
+
83
+ def is_timeout(self) -> bool:
84
+ """Check if the result is a timeout."""
85
+ return self._status == AgentResultStatus.TIMEOUT
86
+
87
+ @staticmethod
88
+ def success(
89
+ response: str, iteration_count: int = 1, tokens_used: int = 0
90
+ ) -> "AgentResult":
91
+ """
92
+ Create a successful result.
93
+
94
+ Args:
95
+ response: The response text
96
+ iteration_count: Number of iterations
97
+ tokens_used: Number of tokens used
98
+
99
+ Returns:
100
+ AgentResult with SUCCESS status
101
+ """
102
+ return AgentResult(
103
+ response=response,
104
+ status=AgentResultStatus.SUCCESS,
105
+ iteration_count=iteration_count,
106
+ tokens_used=tokens_used,
107
+ )
108
+
109
+ @staticmethod
110
+ def error(message: str) -> "AgentResult":
111
+ """
112
+ Create an error result.
113
+
114
+ Args:
115
+ message: The error message
116
+
117
+ Returns:
118
+ AgentResult with ERROR status
119
+ """
120
+ return AgentResult(
121
+ response="",
122
+ status=AgentResultStatus.ERROR,
123
+ error_message=message,
124
+ )
125
+
126
+ @staticmethod
127
+ def timeout(message: str = "Operation timed out") -> "AgentResult":
128
+ """
129
+ Create a timeout result.
130
+
131
+ Args:
132
+ message: The timeout message
133
+
134
+ Returns:
135
+ AgentResult with TIMEOUT status
136
+ """
137
+ return AgentResult(
138
+ response="",
139
+ status=AgentResultStatus.TIMEOUT,
140
+ error_message=message,
141
+ )
142
+
143
+ @staticmethod
144
+ def builder() -> "AgentResultBuilder":
145
+ """Create a new result builder."""
146
+ return AgentResultBuilder()
147
+
148
+
149
+ class AgentResultBuilder:
150
+ """Builder for AgentResult."""
151
+
152
+ def __init__(self) -> None:
153
+ self._response: str = ""
154
+ self._status: AgentResultStatus = AgentResultStatus.SUCCESS
155
+ self._iteration_count: int = 0
156
+ self._tokens_used: int = 0
157
+ self._error_message: Optional[str] = None
158
+
159
+ def response(self, response: str) -> "AgentResultBuilder":
160
+ """Set the response text."""
161
+ self._response = response
162
+ return self
163
+
164
+ def status(self, status: AgentResultStatus) -> "AgentResultBuilder":
165
+ """Set the status."""
166
+ self._status = status
167
+ return self
168
+
169
+ def iteration_count(self, count: int) -> "AgentResultBuilder":
170
+ """Set the iteration count."""
171
+ self._iteration_count = count
172
+ return self
173
+
174
+ def tokens_used(self, tokens: int) -> "AgentResultBuilder":
175
+ """Set the tokens used."""
176
+ self._tokens_used = tokens
177
+ return self
178
+
179
+ def error_message(self, message: str) -> "AgentResultBuilder":
180
+ """Set the error message."""
181
+ self._error_message = message
182
+ return self
183
+
184
+ def build(self) -> AgentResult:
185
+ """Build the AgentResult."""
186
+ return AgentResult(
187
+ response=self._response,
188
+ status=self._status,
189
+ iteration_count=self._iteration_count,
190
+ tokens_used=self._tokens_used,
191
+ error_message=self._error_message,
192
+ )
@@ -0,0 +1,47 @@
1
+ """
2
+ Server configuration utilities for the Gopher Security MCP SDK.
3
+
4
+ Provides utilities for fetching and managing MCP server configurations.
5
+ """
6
+
7
+ from typing import Optional
8
+ from gopher_mcp_python.ffi import GopherOrchLibrary
9
+ from gopher_mcp_python.errors import ApiKeyError, ConnectionError
10
+
11
+
12
+ class ServerConfig:
13
+ """
14
+ Utility class for fetching server configurations.
15
+ """
16
+
17
+ @staticmethod
18
+ def fetch(api_key: str) -> str:
19
+ """
20
+ Fetch server configuration from the API.
21
+
22
+ Args:
23
+ api_key: API key for authentication
24
+
25
+ Returns:
26
+ JSON string containing server configuration
27
+
28
+ Raises:
29
+ ApiKeyError: If API key is invalid
30
+ ConnectionError: If fetch fails
31
+ """
32
+ if not api_key:
33
+ raise ApiKeyError("API key is required")
34
+
35
+ lib = GopherOrchLibrary.get_instance()
36
+ if lib is None:
37
+ raise ConnectionError("Native library not available")
38
+
39
+ result = lib.api_fetch_servers(api_key)
40
+ if result is None:
41
+ error_msg = lib.get_last_error_message()
42
+ lib.clear_error()
43
+ if error_msg and "api key" in error_msg.lower():
44
+ raise ApiKeyError(error_msg)
45
+ raise ConnectionError(error_msg or "Failed to fetch server configuration")
46
+
47
+ return result