From 7cdad305aa368723282087a38a9b5ed48ca3fa11 Mon Sep 17 00:00:00 2001 From: RahulHere Date: Wed, 17 Jun 2026 22:23:35 +0800 Subject: [PATCH 01/29] Track gopher-orch br_release branch and bump submodule to 0.1.23 (#9) Switch the gopher-orch submodule from main to br_release to match the JS SDK's release pipeline, and advance the recorded pointer from v0.1.13 to the br_release tip (Release 0.1.23, 05667fdf). --- .gitmodules | 2 +- third_party/gopher-orch | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitmodules b/.gitmodules index 435fb285..18a3014b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +1,4 @@ [submodule "third_party/gopher-orch"] path = third_party/gopher-orch url = https://github.com/GopherSecurity/gopher-orch.git - branch = main + branch = br_release diff --git a/third_party/gopher-orch b/third_party/gopher-orch index ff84c196..05667fdf 160000 --- a/third_party/gopher-orch +++ b/third_party/gopher-orch @@ -1 +1 @@ -Subproject commit ff84c1969e5ccd6250d0876e653db8c8f77667b0 +Subproject commit 05667fdf7c05d51ccd0aa6abc33545e0a022cf00 From d155c19500721de32b00941720d1e3188439f46a Mon Sep 17 00:00:00 2001 From: RahulHere Date: Wed, 17 Jun 2026 22:34:02 +0800 Subject: [PATCH 02/29] Bind five new ReActAgent factories in ctypes FFI layer fix (#9) Wire up the five gopher_orch_agent_create_by_* C entry points that landed in gopher-orch 0.1.23 (PR #116) into the Python SDK's ctypes binding layer, matching the JS koffi side commit ce787c99. Added in gopher_mcp_python/ffi/library.py: - argtypes / restype declarations for: gopher_orch_agent_create_by_server_id (4 char*) gopher_orch_agent_create_by_server_name (4 char*) gopher_orch_agent_create_by_gateway_id (4 char*) gopher_orch_agent_create_by_gateway_name (4 char*) gopher_orch_agent_create_by_url (3 char*) Wrapped in try / except AttributeError so the SDK keeps loading against older libgopher-orch builds (< 0.1.23) that lack these symbols; the matching set_log_level binding above uses the same pattern for forward-compatibility. - Five Python wrapper methods on GopherOrchLibrary: agent_create_by_server_id (provider, model, api_key, server_id) agent_create_by_server_name (provider, model, api_key, server_name) agent_create_by_gateway_id (provider, model, api_key, gateway_id) agent_create_by_gateway_name(provider, model, api_key, gateway_name) agent_create_by_url (provider, model, url) Each uses getattr(self._lib, symbol, None) so a missing C symbol yields a None handle instead of an AttributeError; the higher-level factory layer (A2) will translate the None into AgentError using the same last_error / clear_error pump the existing create() path uses. Verified with the existing tests/test_ffi.py suite (11 passed) and a local smoke test against the vendored 0.1.1 dylib confirming the new wrappers return None for missing symbols rather than crashing at import. --- gopher_mcp_python/ffi/library.py | 148 +++++++++++++++++++++++++++++++ 1 file changed, 148 insertions(+) diff --git a/gopher_mcp_python/ffi/library.py b/gopher_mcp_python/ffi/library.py index fa272d96..3aa0aee6 100644 --- a/gopher_mcp_python/ffi/library.py +++ b/gopher_mcp_python/ffi/library.py @@ -134,6 +134,54 @@ def _setup_functions(self) -> None: ] self._lib.gopher_orch_agent_create_by_api_key.restype = c_void_p + # Routing factories: scope the agent to a single MCP server or gateway + # selected by id / name, or to a known MCP URL. These C symbols landed + # in gopher-orch 0.1.23 -- wrapped in try / except so the SDK still + # loads against an older libgopher-orch, with the higher-level + # factories raising AgentError at call time if the symbol is missing. + try: + self._lib.gopher_orch_agent_create_by_server_id.argtypes = [ + c_char_p, + c_char_p, + c_char_p, + c_char_p, + ] + self._lib.gopher_orch_agent_create_by_server_id.restype = c_void_p + + self._lib.gopher_orch_agent_create_by_server_name.argtypes = [ + c_char_p, + c_char_p, + c_char_p, + c_char_p, + ] + self._lib.gopher_orch_agent_create_by_server_name.restype = c_void_p + + self._lib.gopher_orch_agent_create_by_gateway_id.argtypes = [ + c_char_p, + c_char_p, + c_char_p, + c_char_p, + ] + self._lib.gopher_orch_agent_create_by_gateway_id.restype = c_void_p + + self._lib.gopher_orch_agent_create_by_gateway_name.argtypes = [ + c_char_p, + c_char_p, + c_char_p, + c_char_p, + ] + self._lib.gopher_orch_agent_create_by_gateway_name.restype = c_void_p + + self._lib.gopher_orch_agent_create_by_url.argtypes = [ + c_char_p, + c_char_p, + c_char_p, + ] + self._lib.gopher_orch_agent_create_by_url.restype = c_void_p + except AttributeError: + # Older libgopher-orch builds (< 0.1.23) lack these symbols. + pass + self._lib.gopher_orch_agent_run.argtypes = [c_void_p, c_char_p, c_int64] self._lib.gopher_orch_agent_run.restype = c_char_p @@ -287,6 +335,106 @@ def agent_create_by_api_key( api_key.encode("utf-8"), ) + def agent_create_by_server_id( + self, provider: str, model: str, api_key: str, server_id: str + ) -> Optional[GopherOrchHandle]: + """Create an agent scoped to a single MCP server by id. + + The native side fetches server config from the Gopher API using the + Bearer api key, appending "?serverId={server_id}" so the response + carries only the matching MCP server entry. + """ + if not self._available or self._lib is None: + return None + fn = getattr(self._lib, "gopher_orch_agent_create_by_server_id", None) + if fn is None: + return None + return fn( + provider.encode("utf-8"), + model.encode("utf-8"), + api_key.encode("utf-8"), + server_id.encode("utf-8"), + ) + + def agent_create_by_server_name( + self, provider: str, model: str, api_key: str, server_name: str + ) -> Optional[GopherOrchHandle]: + """Create an agent scoped to a single MCP server by name. + + Mirrors agent_create_by_server_id but routes via "?serverName=". + """ + if not self._available or self._lib is None: + return None + fn = getattr(self._lib, "gopher_orch_agent_create_by_server_name", None) + if fn is None: + return None + return fn( + provider.encode("utf-8"), + model.encode("utf-8"), + api_key.encode("utf-8"), + server_name.encode("utf-8"), + ) + + def agent_create_by_gateway_id( + self, provider: str, model: str, api_key: str, gateway_id: str + ) -> Optional[GopherOrchHandle]: + """Create an agent scoped to a single MCP gateway by id. + + The native side appends "?gatewayId={gateway_id}" to the Gopher API + fetch so the response carries the backing MCP servers for that + gateway. + """ + if not self._available or self._lib is None: + return None + fn = getattr(self._lib, "gopher_orch_agent_create_by_gateway_id", None) + if fn is None: + return None + return fn( + provider.encode("utf-8"), + model.encode("utf-8"), + api_key.encode("utf-8"), + gateway_id.encode("utf-8"), + ) + + def agent_create_by_gateway_name( + self, provider: str, model: str, api_key: str, gateway_name: str + ) -> Optional[GopherOrchHandle]: + """Create an agent scoped to a single MCP gateway by name. + + Mirrors agent_create_by_gateway_id but routes via "?gatewayName=". + """ + if not self._available or self._lib is None: + return None + fn = getattr(self._lib, "gopher_orch_agent_create_by_gateway_name", None) + if fn is None: + return None + return fn( + provider.encode("utf-8"), + model.encode("utf-8"), + api_key.encode("utf-8"), + gateway_name.encode("utf-8"), + ) + + def agent_create_by_url( + self, provider: str, model: str, url: str + ) -> Optional[GopherOrchHandle]: + """Create an agent for a single MCP server reachable at a URL. + + Skips the remote config fetch: the native side synthesises an + http_sse server entry around the URL. Useful for local development + or one-off endpoints where the operator already knows the URL. + """ + if not self._available or self._lib is None: + return None + fn = getattr(self._lib, "gopher_orch_agent_create_by_url", None) + if fn is None: + return None + return fn( + provider.encode("utf-8"), + model.encode("utf-8"), + url.encode("utf-8"), + ) + def agent_run( self, agent: GopherOrchHandle, query: str, timeout_ms: int ) -> Optional[str]: From ee9325953f0f6878fa151ca54c8d0bcd47bb0dd8 Mon Sep 17 00:00:00 2001 From: RahulHere Date: Wed, 17 Jun 2026 22:37:23 +0800 Subject: [PATCH 03/29] Expose five new ReActAgent factories on GopherAgent fix (#9) Surface the five routing factories landed in gopher-orch 0.1.23 on the public GopherAgent class so Python SDK consumers can scope an agent to a single MCP server, MCP gateway, or a known MCP URL without hand-rolling a JSON server config. Added in gopher_mcp_python/agent.py: - Five @staticmethod factories mirroring create_with_api_key / create_with_server_config: create_with_server_id (provider, model, api_key, server_id) create_with_server_name (provider, model, api_key, server_name) create_with_gateway_id (provider, model, api_key, gateway_id) create_with_gateway_name(provider, model, api_key, gateway_name) create_with_url (provider, model, url) Each delegates to the matching FFI wrapper from the A1 binding layer via a small lambda passed to the new helper below. - _create_from_ffi(create_handle) helper that centralises the null handle pump shared by the five new factories: lazy library init, AgentError translation, and last_error / clear_error draining -- same pattern as the existing create() inline pump, extracted so the five new factories each stay around ten lines instead of duplicating the diagnostic plumbing. - Callable added to the typing imports for the helper signature. Note: the existing GopherAgent.create(config) path is unchanged -- its inline pump still flows through GopherAgentConfig, which only models the api_key / server_config XOR. The new factories take a fourth input (server_id / gateway_name / url) that does not fit that XOR and so bypass the builder; the next commit documents this decision on GopherAgentConfig itself. Verified with the full pytest suite (198 passed, 1 skipped) and a local smoke test confirming that calling create_with_url against the vendored 0.1.1 dylib (which lacks the new C symbols) raises AgentError via the null-handle pump rather than crashing. --- gopher_mcp_python/agent.py | 158 ++++++++++++++++++++++++++++++++++++- 1 file changed, 157 insertions(+), 1 deletion(-) diff --git a/gopher_mcp_python/agent.py b/gopher_mcp_python/agent.py index a1614e72..4939007d 100644 --- a/gopher_mcp_python/agent.py +++ b/gopher_mcp_python/agent.py @@ -27,7 +27,7 @@ """ import atexit -from typing import Optional +from typing import Callable, Optional from gopher_mcp_python.config import GopherAgentConfig from gopher_mcp_python.result import AgentResult, AgentResultStatus @@ -183,6 +183,162 @@ def create_with_server_config( .build() ) + @staticmethod + def create_with_server_id( + provider: str, model: str, api_key: str, server_id: str + ) -> "GopherAgent": + """ + Create a new GopherAgent scoped to a single MCP server by id. + + Fetches server config from the Gopher API using the Bearer api key, + appending "?serverId={server_id}" so the response carries only the + matching MCP server entry. + + Args: + provider: Provider name (e.g., "AnthropicProvider") + model: Model identifier accepted by the chosen provider + api_key: Gopher API key + server_id: MCP server id to scope the agent to + + Returns: + GopherAgent instance + """ + return GopherAgent._create_from_ffi( + lambda lib: lib.agent_create_by_server_id( + provider, model, api_key, server_id + ) + ) + + @staticmethod + def create_with_server_name( + provider: str, model: str, api_key: str, server_name: str + ) -> "GopherAgent": + """ + Create a new GopherAgent scoped to a single MCP server by name. + + Fetches server config from the Gopher API using the Bearer api key, + appending "?serverName={server_name}" so the response carries only + the matching MCP server entry. + + Args: + provider: Provider name (e.g., "AnthropicProvider") + model: Model identifier accepted by the chosen provider + api_key: Gopher API key + server_name: MCP server name to scope the agent to + + Returns: + GopherAgent instance + """ + return GopherAgent._create_from_ffi( + lambda lib: lib.agent_create_by_server_name( + provider, model, api_key, server_name + ) + ) + + @staticmethod + def create_with_gateway_id( + provider: str, model: str, api_key: str, gateway_id: str + ) -> "GopherAgent": + """ + Create a new GopherAgent scoped to a single MCP gateway by id. + + Fetches server config from the Gopher API using the Bearer api key, + appending "?gatewayId={gateway_id}" so the response carries the + backing MCP servers for that gateway. + + Args: + provider: Provider name (e.g., "AnthropicProvider") + model: Model identifier accepted by the chosen provider + api_key: Gopher API key + gateway_id: MCP gateway id to scope the agent to + + Returns: + GopherAgent instance + """ + return GopherAgent._create_from_ffi( + lambda lib: lib.agent_create_by_gateway_id( + provider, model, api_key, gateway_id + ) + ) + + @staticmethod + def create_with_gateway_name( + provider: str, model: str, api_key: str, gateway_name: str + ) -> "GopherAgent": + """ + Create a new GopherAgent scoped to a single MCP gateway by name. + + Fetches server config from the Gopher API using the Bearer api key, + appending "?gatewayName={gateway_name}" so the response carries the + backing MCP servers for that gateway. + + Args: + provider: Provider name (e.g., "AnthropicProvider") + model: Model identifier accepted by the chosen provider + api_key: Gopher API key + gateway_name: MCP gateway name to scope the agent to + + Returns: + GopherAgent instance + """ + return GopherAgent._create_from_ffi( + lambda lib: lib.agent_create_by_gateway_name( + provider, model, api_key, gateway_name + ) + ) + + @staticmethod + def create_with_url(provider: str, model: str, url: str) -> "GopherAgent": + """ + Create a new GopherAgent for a single MCP server reachable at a URL. + + Skips the remote config fetch entirely: synthesises an http_sse + server entry around the URL and delegates to create_by_json on the + native side. Useful for local development or one-off endpoints where + the operator already knows the URL. + + Args: + provider: Provider name (e.g., "AnthropicProvider") + model: Model identifier accepted by the chosen provider + url: Full URL of the MCP server (e.g., "http://127.0.0.1:8080/mcp") + + Returns: + GopherAgent instance + """ + return GopherAgent._create_from_ffi( + lambda lib: lib.agent_create_by_url(provider, model, url) + ) + + @staticmethod + def _create_from_ffi( + create_handle: Callable[[GopherOrchLibrary], Optional[GopherOrchHandle]], + ) -> "GopherAgent": + """ + Shared handle-creation pump for factories that bypass GopherAgentConfig. + + Ensures the native library is initialised, invokes the supplied FFI + callable, and translates a null handle return into AgentError using + the same last_error / clear_error contract as create(). + """ + if not _initialized: + GopherAgent.init() + + lib = GopherOrchLibrary.get_instance() + if lib is None: + raise AgentError("Native library not available") + + try: + handle = create_handle(lib) + except Exception as e: + raise AgentError(f"Failed to create agent: {e}") + + if handle is None: + error = lib.get_last_error_message() + lib.clear_error() + raise AgentError(error or "Failed to create agent") + + return GopherAgent(handle) + def run(self, query: str, timeout_ms: int = 60000) -> str: """ Run a query against the agent. From 4497ac3ca8566e3150f450b86ce170025035e6b1 Mon Sep 17 00:00:00 2001 From: RahulHere Date: Wed, 17 Jun 2026 22:38:32 +0800 Subject: [PATCH 04/29] Document the builder gap for the new routing factories fix (#9) Add a class-level docstring note to GopherAgentConfig explaining that the five new GopherAgent.create_with_* routing factories deliberately bypass the builder. Context: GopherAgentConfig has always modelled an api_key XOR server_config shape that maps onto the two original C entry points gopher_orch_agent_create_by_api_key and gopher_orch_agent_create_by_json. The five routing factories that landed in gopher-orch 0.1.23 -- create_with_server_id / create_with_server_name / create_with_gateway_id / create_with_gateway_name / create_with_url -- each take a fourth input (server / gateway identifier, or URL) that does not fit the XOR, so the previous commit added them as static methods on GopherAgent that dispatch into GopherOrchLibrary via the new _create_from_ffi helper rather than threading an extra optional field through GopherAgentConfig. This commit only updates the docstring -- runtime behaviour is unchanged. The note exists so a future reader looking at GopherAgentConfig is not surprised that four of the factory methods on GopherAgent never construct one. Mirrors the equivalent docstring landed on the JS-side GopherAgentConfig (gopher-mcp-js src/config.ts), keeping the two SDKs aligned on this architectural decision. Verified with the full pytest suite (198 passed, 1 skipped). --- gopher_mcp_python/config.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/gopher_mcp_python/config.py b/gopher_mcp_python/config.py index 8a4befe5..89e6a362 100644 --- a/gopher_mcp_python/config.py +++ b/gopher_mcp_python/config.py @@ -9,10 +9,20 @@ class GopherAgentConfig: """ - Immutable configuration for GopherAgent. + Immutable configuration for GopherAgent created via GopherAgent.create(). Use the builder() method to create configurations. + The builder accepts only the api_key / server_config XOR that maps to + the original gopher_orch_agent_create_by_api_key and + gopher_orch_agent_create_by_json C entry points. The five newer routing + factories (GopherAgent.create_with_server_id, create_with_server_name, + create_with_gateway_id, create_with_gateway_name, create_with_url) take + additional inputs (server / gateway identifier, or URL) that do not fit + that XOR shape and deliberately bypass this builder; they are exposed + as static methods on GopherAgent and dispatch into GopherOrchLibrary + directly via GopherAgent._create_from_ffi. + Example: >>> config = (GopherAgentConfig.builder() ... .provider("AnthropicProvider") From f8c8ac59bfca29145541a8a0d7c715363494b8a0 Mon Sep 17 00:00:00 2001 From: RahulHere Date: Wed, 17 Jun 2026 22:42:13 +0800 Subject: [PATCH 05/29] Surface the FFI handle type at the package root fix (#9) Verify the package-level export surface still covers the new GopherAgent.create_with_* routing factories, and close the only parity gap discovered against the JS SDK index.ts. Findings: - GopherAgent is already in gopher_mcp_python.__all__, and the five new @staticmethod factories ride along on it -- no new top-level export needed for the A2 surface. - The native FFI subpackage exports both GopherOrchLibrary and GopherOrchHandle (gopher_mcp_python/ffi/__init__.py), but only GopherOrchLibrary was being re-exported at the package root. The JS SDK index.ts exposes both, with GopherOrchHandle reserved for advanced FFI consumers that need to hand-craft the opaque pointer type. Change in gopher_mcp_python/__init__.py: - Import GopherOrchHandle next to GopherOrchLibrary - Add "GopherOrchHandle" to __all__ under the "# FFI" section No runtime behaviour change for existing consumers; this only widens the public re-export surface so advanced users no longer need to reach into gopher_mcp_python.ffi for the handle type. Verified by importing the new symbol at the top level and walking the five new GopherAgent factories from the package root. --- gopher_mcp_python/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gopher_mcp_python/__init__.py b/gopher_mcp_python/__init__.py index a31dd580..235e51e8 100644 --- a/gopher_mcp_python/__init__.py +++ b/gopher_mcp_python/__init__.py @@ -32,7 +32,7 @@ TimeoutError, ) from gopher_mcp_python.server_config import ServerConfig -from gopher_mcp_python.ffi import GopherOrchLibrary +from gopher_mcp_python.ffi import GopherOrchLibrary, GopherOrchHandle # Auth module re-exports from gopher_mcp_python.ffi.auth import ( @@ -68,6 +68,7 @@ "TimeoutError", # FFI "GopherOrchLibrary", + "GopherOrchHandle", # Auth "GopherAuthError", "ValidationResult", From 1261c8669498865aba81d152ec30e13033d1b3ac Mon Sep 17 00:00:00 2001 From: RahulHere Date: Wed, 17 Jun 2026 22:44:31 +0800 Subject: [PATCH 06/29] Add contract tests for the five new routing factories fix (#9) Add tests/test_agent_create_by.py covering the failure-path contract for the five GopherAgent.create_with_* factories that the previous commits added. Mirrors the JS-side suite at gopher-mcp-js/tests/agent-create-by.test.ts and the C++ failure-path cases in gopher-orch/tests/gopher/orch/agent_create_by_test.cc. Cases: - Empty api_key (one test per routing variant): create_with_server_id raises AgentError create_with_server_name raises AgentError create_with_gateway_id raises AgentError create_with_gateway_name raises AgentError Locks down the nullptr-on-failure contract that fetch_mcp_servers surfaces through the wrapper layer. - Empty url: create_with_url with url="" raises AgentError. Mirrors the CreateByUrlRejectsEmptyUrl C++ case -- validation fires before any FFI work happens. - Unknown provider: create_with_url(BAD_PROVIDER, MODEL, URL) raises AgentError. create_with_url synthesises a local http_sse config and reaches create_by_json, which rejects an unknown provider name; the factory must surface that as AgentError. - AgentError carries a non-empty message: A regression sentinel for the last_error / clear_error pump -- if a future change to _create_from_ffi swallows the C-side diagnostic, this case catches it immediately. Skip pattern: Module-level pytestmark uses GopherOrchLibrary.is_available() to skip the whole module when the native library is not loadable, matching the existing tests/test_ffi.py decorator approach. CI runs build.sh first so the suite executes; the contract tests also pass against the vendored 0.1.1 dylib because the FFI wrappers' getattr(_lib, symbol, None) pump returns None for missing symbols and the helper translates None into AgentError. Happy-path coverage for the four routing variants needs a stubbed HTTP listener capturing the /v1/mcp-servers query string with the new camelCase keys (serverId / serverName / gatewayId / gatewayName); that infrastructure is tracked separately, matching the same TODO comment on the C++ side. Verified locally: 7 tests pass, runs in ~0.05s. --- tests/test_agent_create_by.py | 90 +++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 tests/test_agent_create_by.py diff --git a/tests/test_agent_create_by.py b/tests/test_agent_create_by.py new file mode 100644 index 00000000..55a41854 --- /dev/null +++ b/tests/test_agent_create_by.py @@ -0,0 +1,90 @@ +""" +Contract tests for the five routing factories on GopherAgent. + +Mirrors the failure-path test set in +gopher-orch/tests/gopher/orch/agent_create_by_test.cc which locks down +the nullptr-on-failure contract that the C FFI surfaces here as +AgentError. Happy-path coverage needs a stubbed HTTP listener capturing +the /v1/mcp-servers query string with the camelCase routing keys +(serverId / serverName / gatewayId / gatewayName); that infrastructure +is tracked separately, same as on the C++ side. +""" + +import pytest + +from gopher_mcp_python import AgentError, GopherAgent +from gopher_mcp_python.ffi import GopherOrchLibrary + +PROVIDER = "AnthropicProvider" +MODEL = "test-model" +BAD_PROVIDER = "NotARealProvider" +URL = "http://127.0.0.1:8080/mcp" + + +pytestmark = pytest.mark.skipif( + not GopherOrchLibrary.is_available(), + reason="Native library not available -- run ./build.sh first", +) + + +class TestRoutingFactoryContracts: + """Failure-path contract for the five routing factories on GopherAgent.""" + + # ---------------------------------------------------------------- + # Empty api key. fetch_mcp_servers throws on the native side; the + # factory must surface that as AgentError rather than returning a + # partially-constructed agent or a null handle leaking through. + # ---------------------------------------------------------------- + + def test_create_with_server_id_rejects_empty_api_key(self) -> None: + with pytest.raises(AgentError): + GopherAgent.create_with_server_id(PROVIDER, MODEL, "", "srv-1") + + def test_create_with_server_name_rejects_empty_api_key(self) -> None: + with pytest.raises(AgentError): + GopherAgent.create_with_server_name( + PROVIDER, MODEL, "", "my-server" + ) + + def test_create_with_gateway_id_rejects_empty_api_key(self) -> None: + with pytest.raises(AgentError): + GopherAgent.create_with_gateway_id(PROVIDER, MODEL, "", "gw-1") + + def test_create_with_gateway_name_rejects_empty_api_key(self) -> None: + with pytest.raises(AgentError): + GopherAgent.create_with_gateway_name( + PROVIDER, MODEL, "", "my-gateway" + ) + + # ---------------------------------------------------------------- + # create_with_url rejects empty url before any FFI work happens. + # Mirrors the CreateByUrlRejectsEmptyUrl case in the C++ suite. + # ---------------------------------------------------------------- + + def test_create_with_url_rejects_empty_url(self) -> None: + with pytest.raises(AgentError): + GopherAgent.create_with_url(PROVIDER, MODEL, "") + + # ---------------------------------------------------------------- + # Unknown provider. create_with_url synthesises a local http_sse + # config and reaches create_by_json on the native side, which + # rejects an unknown provider name. The factory must surface that + # as AgentError. + # ---------------------------------------------------------------- + + def test_create_with_url_rejects_unknown_provider(self) -> None: + with pytest.raises(AgentError): + GopherAgent.create_with_url(BAD_PROVIDER, MODEL, URL) + + # ---------------------------------------------------------------- + # AgentError surfaces a non-empty message so SDK consumers can log + # a meaningful diagnostic; the C side fills last_error() and the + # wrapper pump should propagate it through. A future change to the + # error pump that swallows the underlying C diagnostic gets caught + # here immediately. + # ---------------------------------------------------------------- + + def test_create_with_server_id_surfaces_non_empty_message(self) -> None: + with pytest.raises(AgentError) as exc_info: + GopherAgent.create_with_server_id(PROVIDER, MODEL, "", "srv-1") + assert len(str(exc_info.value)) > 0 From aefecf381b53ca614394777d99efe586c411aa51 Mon Sep 17 00:00:00 2001 From: RahulHere Date: Wed, 17 Jun 2026 22:47:35 +0800 Subject: [PATCH 07/29] Add Python example for create_with_api_key fix (#9) Land examples/api/create_by_api_key.py, the smallest of the seven create_by_* examples and the first sanity check that the toolchain (pip install -e ., the right native lib, env vars) is wired correctly on the Python side. Direct port of gopher-mcp-js/examples/api/create_by_api_key.ts, which itself ports gopher-orch/examples/sdk/api/create_by_api_key.cc. Behaviour: - Uses a Gopher API key to fetch the caller's full MCP server inventory via GET /v1/mcp-servers and constructs an agent with every server the api key owns -- no routing parameter. - Provider defaults to AnthropicProvider; model is taken from the LLM_MODEL env var. Refuses to call the FFI when either the API key or the model is still the placeholder value, printing a clear error to stderr and exiting 1. This matches the env-var- required path the JS side picked, so the example never surfaces a stale or fictional model identifier even if the toolchain would otherwise auto-pick one. - Accepts queries as positional argv with a single canned fallback ("What time is it in Tokyo?") matching the JS sibling. - try / finally with agent.dispose() so the native handle is always released, even if a query fails partway through. This is the first file in examples/api/, a new self-contained subdirectory following the same convention as the existing examples/auth/ and examples/pip/ subdirs. The README, run scripts, and remaining six variants follow in subsequent commits. Verified locally: placeholder refusal path exits 1 with a clear diagnostic; with GOPHER_API_KEY / LLM_MODEL / ANTHROPIC_API_KEY set, the example completes end-to-end against a real provider. --- examples/api/create_by_api_key.py | 99 +++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 examples/api/create_by_api_key.py diff --git a/examples/api/create_by_api_key.py b/examples/api/create_by_api_key.py new file mode 100644 index 00000000..a2eb4bff --- /dev/null +++ b/examples/api/create_by_api_key.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +""" +SDK example for GopherAgent.create_with_api_key. + +Python port of gopher-mcp-js/examples/api/create_by_api_key.ts, which +itself ports gopher-orch/examples/sdk/api/create_by_api_key.cc. + +Uses a Gopher API key to fetch the caller's full MCP server inventory +via GET /v1/mcp-servers; the agent gets every server the api key owns +with no extra routing. Smallest of the seven create_by_* examples and +a good first sanity check that the toolchain (pip install -e ., the +right native lib, env vars) is wired correctly. + +Provider defaults to AnthropicProvider; the model is taken from +LLM_MODEL. Override either via env or by editing the constants in +main(). + +Configuration (env vars): + GOPHER_API_KEY Gopher API key for /v1/mcp-servers + LLM_PROVIDER Optional. Defaults to "AnthropicProvider". + LLM_MODEL Required. Model identifier the provider accepts. + DEBUG When set, ctypes prints library-resolution diagnostics. + +Usage: + python3 create_by_api_key.py # built-in query + python3 create_by_api_key.py "query one" "query two" ... # supplied queries +""" + +import os +import sys +import traceback + +from gopher_mcp_python import GopherAgent + +API_KEY_PLACEHOLDER = "{YOUR_GOPHER_API_KEY}" +MODEL_PLACEHOLDER = "{YOUR_LLM_MODEL}" + + +def env_or(name: str, fallback: str) -> str: + """Return os.environ[name] if non-empty, otherwise fallback.""" + value = os.environ.get(name, "") + return value if value else fallback + + +def main() -> None: + print("=== GopherAgent.create_with_api_key example ===") + print(f"Usage: python3 {sys.argv[0]} [query1] [query2] ...") + print("Env: GOPHER_API_KEY LLM_PROVIDER LLM_MODEL DEBUG") + print("") + + queries = sys.argv[1:] if len(sys.argv) > 1 else ["What time is it in Tokyo?"] + + provider = env_or("LLM_PROVIDER", "AnthropicProvider") + model = env_or("LLM_MODEL", MODEL_PLACEHOLDER) + api_key = env_or("GOPHER_API_KEY", API_KEY_PLACEHOLDER) + + print(f"Provider: {provider}") + model_label = ( + f"{model} (set LLM_MODEL)" if model == MODEL_PLACEHOLDER else model + ) + print(f"Model: {model_label}") + api_key_label = ( + f"{api_key} (set GOPHER_API_KEY)" + if api_key == API_KEY_PLACEHOLDER + else "" + ) + print(f"API key: {api_key_label}") + print(f"Queries: {len(queries)}") + + if model == MODEL_PLACEHOLDER or api_key == API_KEY_PLACEHOLDER: + print( + "\nError: LLM_MODEL and GOPHER_API_KEY must both be set.", + file=sys.stderr, + ) + sys.exit(1) + + print("\nCreating agent via GopherAgent.create_with_api_key...") + agent = GopherAgent.create_with_api_key(provider, model, api_key) + print("Agent created successfully!") + + try: + for i, query in enumerate(queries): + print(f"\nQuery {i + 1}: {query}") + answer = agent.run(query) + print(f"\nAgent Response {i + 1}:") + print("--------------------------------") + print(answer) + print("--------------------------------") + finally: + agent.dispose() + + +if __name__ == "__main__": + try: + main() + except Exception as e: + print(f"Error: {e}", file=sys.stderr) + traceback.print_exc(file=sys.stderr) + sys.exit(1) From c45466546ae76824bb497e5cc51485fd88728422 Mon Sep 17 00:00:00 2001 From: RahulHere Date: Wed, 17 Jun 2026 22:48:49 +0800 Subject: [PATCH 08/29] Add Python example for create_with_server_config fix (#9) Land examples/api/create_by_json.py, second of the seven create_by_* examples. Direct port of gopher-mcp-js/examples/api/create_by_json.ts, which itself ports gopher-orch/examples/sdk/api/create_by_json.cc. Behaviour: - Builds a GopherAgent from an inline server JSON document, skipping the remote /v1/mcp-servers fetch that create_with_api_key performs. - The inline SERVER_CONFIG follows the { succeeded, code, message, data: { servers: [...] } } envelope that ConfigLoader on the C++ side accepts. Default servers entry is a single http_sse stub pointing at 127.0.0.1:3001/rpc; edit the literal to point at a real MCP server before running. - Built with json.dumps(...) rather than a string literal so the inline payload stays valid JSON under any future edit and matches the JS sibling's JSON.stringify({...}) shape. - Provider defaults to AnthropicProvider; model is taken from the LLM_MODEL env var. Refuses to call the FFI when LLM_MODEL is still the placeholder, printing a clear error to stderr and exiting 1. Matches the env-var-required path the JS side picked so the example never surfaces a stale or fictional model identifier. - Accepts queries as positional argv with the canned "What time is it in Tokyo?" fallback. try / finally with agent.dispose() so the native handle is always released. Lands without depending on any of the five new routing factories, so this and create_by_api_key.py are the two variants that work today against either the vendored 0.1.1 dylib or a fresh gopher-orch 0.1.23 build. Verified locally: placeholder refusal path exits 1 with a clear diagnostic; syntax check clean; no forbidden tokens. --- examples/api/create_by_json.py | 112 +++++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 examples/api/create_by_json.py diff --git a/examples/api/create_by_json.py b/examples/api/create_by_json.py new file mode 100644 index 00000000..8d2341b0 --- /dev/null +++ b/examples/api/create_by_json.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 +""" +SDK example for GopherAgent.create_with_server_config. + +Python port of gopher-mcp-js/examples/api/create_by_json.ts, which +itself ports gopher-orch/examples/sdk/api/create_by_json.cc. + +Builds a GopherAgent from an inline server JSON document, skipping the +remote /v1/mcp-servers fetch that create_with_api_key performs. Useful +when the caller already knows which MCP servers to bind to and wants +to skip the round-trip; the inline payload follows the +{ succeeded, code, message, data: { servers: [...] } } shape that +ConfigLoader on the C++ side accepts. + +Provider defaults to AnthropicProvider; the model is taken from +LLM_MODEL. Edit SERVER_CONFIG below to point at your own MCP servers. + +Configuration (env vars): + LLM_PROVIDER Optional. Defaults to "AnthropicProvider". + LLM_MODEL Required. Model identifier the provider accepts. + DEBUG When set, ctypes prints library-resolution diagnostics. + +Usage: + python3 create_by_json.py # built-in query + python3 create_by_json.py "query one" "query two" ... # supplied queries +""" + +import json +import os +import sys +import traceback + +from gopher_mcp_python import GopherAgent + +MODEL_PLACEHOLDER = "{YOUR_LLM_MODEL}" + +SERVER_CONFIG = json.dumps( + { + "succeeded": True, + "code": 200000000, + "message": "success", + "data": { + "servers": [ + { + "version": "2025-01-09", + "serverId": "1877234567890123456", + "name": "gopher-auth-server", + "transport": "http_sse", + "config": { + "url": "http://127.0.0.1:3001/rpc", + "headers": {}, + }, + "connectTimeout": 5000, + "requestTimeout": 30000, + } + ] + }, + } +) + + +def env_or(name: str, fallback: str) -> str: + """Return os.environ[name] if non-empty, otherwise fallback.""" + value = os.environ.get(name, "") + return value if value else fallback + + +def main() -> None: + print("=== GopherAgent.create_with_server_config example ===") + print(f"Usage: python3 {sys.argv[0]} [query1] [query2] ...") + print("Env: LLM_PROVIDER LLM_MODEL DEBUG") + print("") + + queries = sys.argv[1:] if len(sys.argv) > 1 else ["What time is it in Tokyo?"] + + provider = env_or("LLM_PROVIDER", "AnthropicProvider") + model = env_or("LLM_MODEL", MODEL_PLACEHOLDER) + + print(f"Provider: {provider}") + model_label = ( + f"{model} (set LLM_MODEL)" if model == MODEL_PLACEHOLDER else model + ) + print(f"Model: {model_label}") + print(f"Queries: {len(queries)}") + + if model == MODEL_PLACEHOLDER: + print("\nError: LLM_MODEL must be set.", file=sys.stderr) + sys.exit(1) + + print("\nCreating agent via GopherAgent.create_with_server_config...") + agent = GopherAgent.create_with_server_config(provider, model, SERVER_CONFIG) + print("Agent created successfully!") + + try: + for i, query in enumerate(queries): + print(f"\nQuery {i + 1}: {query}") + answer = agent.run(query) + print(f"\nAgent Response {i + 1}:") + print("--------------------------------") + print(answer) + print("--------------------------------") + finally: + agent.dispose() + + +if __name__ == "__main__": + try: + main() + except Exception as e: + print(f"Error: {e}", file=sys.stderr) + traceback.print_exc(file=sys.stderr) + sys.exit(1) From da74f4b19d7e1316c2e7c5138aaa4ee6c824b8a2 Mon Sep 17 00:00:00 2001 From: RahulHere Date: Wed, 17 Jun 2026 22:51:02 +0800 Subject: [PATCH 09/29] Add Python example for create_with_server_id fix (#9) Land examples/api/create_by_server_id.py, third of the seven create_by_* examples and the first of the four routing variants added in gopher-orch 0.1.23. Direct port of gopher-mcp-js/examples/api/create_by_server_id.ts, which itself ports gopher-orch/examples/sdk/api/create_by_server_id.cc. Behaviour: - Scopes the agent to a single MCP server in the caller's workspace by id. Internally hits the same GET /v1/mcp-servers endpoint as create_with_api_key under the Bearer api key, but adds the "?serverId={id}" routing query so the response carries only the matching server entry. Use this when the api key owns several MCP servers but the agent should bind to exactly one. - Provider defaults to AnthropicProvider; the model comes from the LLM_MODEL env var. Refuses to call the FFI when any of the three required env vars (LLM_MODEL, GOPHER_API_KEY, GOPHER_MCP_SERVER_ID) is still the placeholder, printing a clear error to stderr and exiting 1. - Reads the routing parameter from GOPHER_MCP_SERVER_ID, matching the JS sibling's env-var name so a downstream user can share env across the JS and Python toolchains. Same pattern for the next three routing variants (server_name, gateway_id, gateway_name). - Accepts queries as positional argv with the canned "What time is it in Tokyo?" fallback. try / finally with agent.dispose() so the native handle is always released. Requires the create_with_server_id factory landed earlier in this PR series; against the vendored 0.1.1 dylib the example exits with an AgentError from the null-handle pump because the C symbol is missing. Against a fresh gopher-orch 0.1.23 build the example completes end-to-end. Verified locally: placeholder refusal path exits 1 with a clear diagnostic; syntax check clean; no forbidden tokens. --- examples/api/create_by_server_id.py | 116 ++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 examples/api/create_by_server_id.py diff --git a/examples/api/create_by_server_id.py b/examples/api/create_by_server_id.py new file mode 100644 index 00000000..2b1cd297 --- /dev/null +++ b/examples/api/create_by_server_id.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python3 +""" +SDK example for GopherAgent.create_with_server_id. + +Python port of gopher-mcp-js/examples/api/create_by_server_id.ts, which +itself ports gopher-orch/examples/sdk/api/create_by_server_id.cc. + +Scopes a GopherAgent to a single MCP server in the caller's workspace +by id. Internally this hits the same GET /v1/mcp-servers endpoint as +create_with_api_key under the Bearer api key, but adds the +"?serverId={id}" routing query so the response carries only the +matching server entry. Use this when the api key owns several MCP +servers but the agent should bind to exactly one. + +Provider defaults to AnthropicProvider; the model is taken from +LLM_MODEL. Override either via env or by editing the constants in +main(). + +Configuration (env vars): + GOPHER_API_KEY Gopher API key for /v1/mcp-servers + GOPHER_MCP_SERVER_ID MCP server id to scope the agent to + LLM_PROVIDER Optional. Defaults to "AnthropicProvider". + LLM_MODEL Required. Model identifier the provider accepts. + DEBUG When set, ctypes prints library-resolution diagnostics. + +Usage: + python3 create_by_server_id.py # built-in query + python3 create_by_server_id.py "query one" "query two" ... # supplied queries +""" + +import os +import sys +import traceback + +from gopher_mcp_python import GopherAgent + +API_KEY_PLACEHOLDER = "{YOUR_GOPHER_API_KEY}" +SERVER_ID_PLACEHOLDER = "{YOUR_MCP_SERVER_ID}" +MODEL_PLACEHOLDER = "{YOUR_LLM_MODEL}" + + +def env_or(name: str, fallback: str) -> str: + """Return os.environ[name] if non-empty, otherwise fallback.""" + value = os.environ.get(name, "") + return value if value else fallback + + +def main() -> None: + print("=== GopherAgent.create_with_server_id example ===") + print(f"Usage: python3 {sys.argv[0]} [query1] [query2] ...") + print( + "Env: GOPHER_API_KEY GOPHER_MCP_SERVER_ID LLM_PROVIDER LLM_MODEL DEBUG" + ) + print("") + + queries = sys.argv[1:] if len(sys.argv) > 1 else ["What time is it in Tokyo?"] + + provider = env_or("LLM_PROVIDER", "AnthropicProvider") + model = env_or("LLM_MODEL", MODEL_PLACEHOLDER) + api_key = env_or("GOPHER_API_KEY", API_KEY_PLACEHOLDER) + server_id = env_or("GOPHER_MCP_SERVER_ID", SERVER_ID_PLACEHOLDER) + + print(f"Provider: {provider}") + model_label = ( + f"{model} (set LLM_MODEL)" if model == MODEL_PLACEHOLDER else model + ) + print(f"Model: {model_label}") + api_key_label = ( + f"{api_key} (set GOPHER_API_KEY)" + if api_key == API_KEY_PLACEHOLDER + else "" + ) + print(f"API key: {api_key_label}") + server_id_label = ( + f"{server_id} (set GOPHER_MCP_SERVER_ID)" + if server_id == SERVER_ID_PLACEHOLDER + else server_id + ) + print(f"MCP server id: {server_id_label}") + print(f"Queries: {len(queries)}") + + if ( + model == MODEL_PLACEHOLDER + or api_key == API_KEY_PLACEHOLDER + or server_id == SERVER_ID_PLACEHOLDER + ): + print( + "\nError: LLM_MODEL, GOPHER_API_KEY, and GOPHER_MCP_SERVER_ID " + "must all be set.", + file=sys.stderr, + ) + sys.exit(1) + + print("\nCreating agent via GopherAgent.create_with_server_id...") + agent = GopherAgent.create_with_server_id(provider, model, api_key, server_id) + print("Agent created successfully!") + + try: + for i, query in enumerate(queries): + print(f"\nQuery {i + 1}: {query}") + answer = agent.run(query) + print(f"\nAgent Response {i + 1}:") + print("--------------------------------") + print(answer) + print("--------------------------------") + finally: + agent.dispose() + + +if __name__ == "__main__": + try: + main() + except Exception as e: + print(f"Error: {e}", file=sys.stderr) + traceback.print_exc(file=sys.stderr) + sys.exit(1) From 54a42f5e8827fcd1b3cec68fa654ca029a2a345c Mon Sep 17 00:00:00 2001 From: RahulHere Date: Wed, 17 Jun 2026 22:53:21 +0800 Subject: [PATCH 10/29] Add Python example for create_with_server_name fix (#9) Land examples/api/create_by_server_name.py, fourth of the seven create_by_* examples and the second routing variant added in gopher-orch 0.1.23. Direct port of gopher-mcp-js/examples/api/create_by_server_name.ts, which itself ports gopher-orch/examples/sdk/api/create_by_server_name.cc. Behaviour: - Scopes the agent to a single MCP server in the caller's workspace by human-readable name. Internally hits the same GET /v1/mcp-servers endpoint as create_with_api_key under the Bearer api key, but adds the "?serverName={name}" routing query so the response carries only the matching server entry. Use this when the api key owns several MCP servers and the agent should bind to exactly one identified by name rather than id. - Provider defaults to AnthropicProvider; the model comes from the LLM_MODEL env var. Refuses to call the FFI when any of the three required env vars (LLM_MODEL, GOPHER_API_KEY, GOPHER_MCP_SERVER_NAME) is still the placeholder, printing a clear error to stderr and exiting 1. - Reads the routing parameter from GOPHER_MCP_SERVER_NAME, matching the JS sibling env-var name. The sibling create_by_ server_id.py example uses GOPHER_MCP_SERVER_ID; the two coexist so a downstream user can flip between routing variants by swapping the example file without changing the env block. - Accepts queries as positional argv with the canned "What time is it in Tokyo?" fallback. try / finally with agent.dispose() so the native handle is always released. Requires the create_with_server_name factory landed earlier in this PR series. Verified locally: placeholder refusal path exits 1 with a clear diagnostic; syntax check clean; no forbidden tokens. --- examples/api/create_by_server_name.py | 120 ++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 examples/api/create_by_server_name.py diff --git a/examples/api/create_by_server_name.py b/examples/api/create_by_server_name.py new file mode 100644 index 00000000..d0f4732f --- /dev/null +++ b/examples/api/create_by_server_name.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +""" +SDK example for GopherAgent.create_with_server_name. + +Python port of gopher-mcp-js/examples/api/create_by_server_name.ts, +which itself ports +gopher-orch/examples/sdk/api/create_by_server_name.cc. + +Scopes a GopherAgent to a single MCP server in the caller's workspace +by human-readable name. Internally this hits the same GET +/v1/mcp-servers endpoint as create_with_api_key under the Bearer api +key, but adds the "?serverName={name}" routing query so the response +carries only the matching server entry. Use this when the api key +owns several MCP servers and the agent should bind to exactly one +identified by name rather than id. + +Provider defaults to AnthropicProvider; the model is taken from +LLM_MODEL. Override either via env or by editing the constants in +main(). + +Configuration (env vars): + GOPHER_API_KEY Gopher API key for /v1/mcp-servers + GOPHER_MCP_SERVER_NAME MCP server name to scope the agent to + LLM_PROVIDER Optional. Defaults to "AnthropicProvider". + LLM_MODEL Required. Model identifier the provider accepts. + DEBUG When set, ctypes prints library-resolution diagnostics. + +Usage: + python3 create_by_server_name.py # built-in query + python3 create_by_server_name.py "query one" "query two" ... # supplied queries +""" + +import os +import sys +import traceback + +from gopher_mcp_python import GopherAgent + +API_KEY_PLACEHOLDER = "{YOUR_GOPHER_API_KEY}" +SERVER_NAME_PLACEHOLDER = "{YOUR_MCP_SERVER_NAME}" +MODEL_PLACEHOLDER = "{YOUR_LLM_MODEL}" + + +def env_or(name: str, fallback: str) -> str: + """Return os.environ[name] if non-empty, otherwise fallback.""" + value = os.environ.get(name, "") + return value if value else fallback + + +def main() -> None: + print("=== GopherAgent.create_with_server_name example ===") + print(f"Usage: python3 {sys.argv[0]} [query1] [query2] ...") + print( + "Env: GOPHER_API_KEY GOPHER_MCP_SERVER_NAME LLM_PROVIDER LLM_MODEL DEBUG" + ) + print("") + + queries = sys.argv[1:] if len(sys.argv) > 1 else ["What time is it in Tokyo?"] + + provider = env_or("LLM_PROVIDER", "AnthropicProvider") + model = env_or("LLM_MODEL", MODEL_PLACEHOLDER) + api_key = env_or("GOPHER_API_KEY", API_KEY_PLACEHOLDER) + server_name = env_or("GOPHER_MCP_SERVER_NAME", SERVER_NAME_PLACEHOLDER) + + print(f"Provider: {provider}") + model_label = ( + f"{model} (set LLM_MODEL)" if model == MODEL_PLACEHOLDER else model + ) + print(f"Model: {model_label}") + api_key_label = ( + f"{api_key} (set GOPHER_API_KEY)" + if api_key == API_KEY_PLACEHOLDER + else "" + ) + print(f"API key: {api_key_label}") + server_name_label = ( + f"{server_name} (set GOPHER_MCP_SERVER_NAME)" + if server_name == SERVER_NAME_PLACEHOLDER + else server_name + ) + print(f"MCP server name: {server_name_label}") + print(f"Queries: {len(queries)}") + + if ( + model == MODEL_PLACEHOLDER + or api_key == API_KEY_PLACEHOLDER + or server_name == SERVER_NAME_PLACEHOLDER + ): + print( + "\nError: LLM_MODEL, GOPHER_API_KEY, and GOPHER_MCP_SERVER_NAME " + "must all be set.", + file=sys.stderr, + ) + sys.exit(1) + + print("\nCreating agent via GopherAgent.create_with_server_name...") + agent = GopherAgent.create_with_server_name( + provider, model, api_key, server_name + ) + print("Agent created successfully!") + + try: + for i, query in enumerate(queries): + print(f"\nQuery {i + 1}: {query}") + answer = agent.run(query) + print(f"\nAgent Response {i + 1}:") + print("--------------------------------") + print(answer) + print("--------------------------------") + finally: + agent.dispose() + + +if __name__ == "__main__": + try: + main() + except Exception as e: + print(f"Error: {e}", file=sys.stderr) + traceback.print_exc(file=sys.stderr) + sys.exit(1) From 9d4857c0e25b00c2bb3c99aa3e91f6951104b105 Mon Sep 17 00:00:00 2001 From: RahulHere Date: Wed, 17 Jun 2026 22:55:17 +0800 Subject: [PATCH 11/29] Add Python example for create_with_gateway_id fix (#9) Land examples/api/create_by_gateway_id.py, fifth of the seven create_by_* examples and the third routing variant added in gopher-orch 0.1.23. Direct port of gopher-mcp-js/examples/api/create_by_gateway_id.ts, which itself ports gopher-orch/examples/sdk/api/create_by_gateway_id.cc. Behaviour: - Scopes the agent to a single MCP gateway in the caller's workspace. Internally hits the same GET /v1/mcp-servers endpoint as create_with_api_key under the Bearer api key, but adds the "?gatewayId={id}" routing query so the response carries the backing MCP servers for that gateway. Use this when the api key owns several gateways and the agent should bind to exactly one. - Gateway routing is the natural choice when the workspace fronts several MCP servers behind a single mcp_gateway process; binding by gateway id reaches every backing server in one shot rather than picking one by server id / name. The variant landed at the same time as the per-server routing factories in gopher-orch PR #116 so the examples come in matched pairs. - Provider defaults to AnthropicProvider; model from LLM_MODEL. Refuses to call the FFI when LLM_MODEL, GOPHER_API_KEY, or GOPHER_MCP_GATEWAY_ID is still the placeholder, printing a clear error to stderr and exiting 1. Matches the env-var-required path the JS side picked so no specific model identifier is baked into source. - Accepts queries as positional argv with the canned "What time is it in Tokyo?" fallback. try / finally with agent.dispose() so the native handle is always released. Requires the create_with_gateway_id factory landed earlier in this PR series. Verified locally: placeholder refusal path exits 1; full pytest suite still passes (205 tests, including the seven contract tests from earlier in the series). --- examples/api/create_by_gateway_id.py | 120 +++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 examples/api/create_by_gateway_id.py diff --git a/examples/api/create_by_gateway_id.py b/examples/api/create_by_gateway_id.py new file mode 100644 index 00000000..640d25e9 --- /dev/null +++ b/examples/api/create_by_gateway_id.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +""" +SDK example for GopherAgent.create_with_gateway_id. + +Python port of gopher-mcp-js/examples/api/create_by_gateway_id.ts, +which itself ports +gopher-orch/examples/sdk/api/create_by_gateway_id.cc. + +Scopes a GopherAgent to a single MCP gateway in the caller's +workspace. Internally this hits the same GET /v1/mcp-servers endpoint +as create_with_api_key under the Bearer api key, but adds the +"?gatewayId={id}" routing query so the response carries the backing +MCP servers for that gateway. Use this when the api key owns several +gateways and the agent should bind to exactly one. + +Provider defaults to AnthropicProvider and the model is taken from +LLM_MODEL so the example stays runnable against any Anthropic-served +model without hardcoding a specific identifier in source. Override +either via env or by editing the constants in main(). + +Configuration (env vars): + GOPHER_API_KEY Gopher API key for /v1/mcp-servers + GOPHER_MCP_GATEWAY_ID MCP gateway id to scope the agent to + LLM_PROVIDER Optional. Defaults to "AnthropicProvider". + LLM_MODEL Required. Model identifier the provider accepts. + DEBUG When set, ctypes prints library-resolution diagnostics. + +Usage: + python3 create_by_gateway_id.py # built-in query + python3 create_by_gateway_id.py "query one" "query two" ... # supplied queries +""" + +import os +import sys +import traceback + +from gopher_mcp_python import GopherAgent + +API_KEY_PLACEHOLDER = "{YOUR_GOPHER_API_KEY}" +GATEWAY_ID_PLACEHOLDER = "{YOUR_MCP_GATEWAY_ID}" +MODEL_PLACEHOLDER = "{YOUR_LLM_MODEL}" + + +def env_or(name: str, fallback: str) -> str: + """Return os.environ[name] if non-empty, otherwise fallback.""" + value = os.environ.get(name, "") + return value if value else fallback + + +def main() -> None: + print("=== GopherAgent.create_with_gateway_id example ===") + print(f"Usage: python3 {sys.argv[0]} [query1] [query2] ...") + print( + "Env: GOPHER_API_KEY GOPHER_MCP_GATEWAY_ID LLM_PROVIDER LLM_MODEL DEBUG" + ) + print("") + + queries = sys.argv[1:] if len(sys.argv) > 1 else ["What time is it in Tokyo?"] + + provider = env_or("LLM_PROVIDER", "AnthropicProvider") + model = env_or("LLM_MODEL", MODEL_PLACEHOLDER) + api_key = env_or("GOPHER_API_KEY", API_KEY_PLACEHOLDER) + gateway_id = env_or("GOPHER_MCP_GATEWAY_ID", GATEWAY_ID_PLACEHOLDER) + + print(f"Provider: {provider}") + model_label = ( + f"{model} (set LLM_MODEL)" if model == MODEL_PLACEHOLDER else model + ) + print(f"Model: {model_label}") + api_key_label = ( + f"{api_key} (set GOPHER_API_KEY)" + if api_key == API_KEY_PLACEHOLDER + else "" + ) + print(f"API key: {api_key_label}") + gateway_id_label = ( + f"{gateway_id} (set GOPHER_MCP_GATEWAY_ID)" + if gateway_id == GATEWAY_ID_PLACEHOLDER + else gateway_id + ) + print(f"MCP gateway id: {gateway_id_label}") + print(f"Queries: {len(queries)}") + + if ( + model == MODEL_PLACEHOLDER + or api_key == API_KEY_PLACEHOLDER + or gateway_id == GATEWAY_ID_PLACEHOLDER + ): + print( + "\nError: LLM_MODEL, GOPHER_API_KEY, and GOPHER_MCP_GATEWAY_ID " + "must all be set.", + file=sys.stderr, + ) + sys.exit(1) + + print("\nCreating agent via GopherAgent.create_with_gateway_id...") + agent = GopherAgent.create_with_gateway_id( + provider, model, api_key, gateway_id + ) + print("Agent created successfully!") + + try: + for i, query in enumerate(queries): + print(f"\nQuery {i + 1}: {query}") + answer = agent.run(query) + print(f"\nAgent Response {i + 1}:") + print("--------------------------------") + print(answer) + print("--------------------------------") + finally: + agent.dispose() + + +if __name__ == "__main__": + try: + main() + except Exception as e: + print(f"Error: {e}", file=sys.stderr) + traceback.print_exc(file=sys.stderr) + sys.exit(1) From 40e628e701cce69a7a348d096bb2bb7c555d997b Mon Sep 17 00:00:00 2001 From: RahulHere Date: Wed, 17 Jun 2026 23:02:28 +0800 Subject: [PATCH 12/29] Add Python example for create_with_gateway_name fix (#9) Land examples/api/create_by_gateway_name.py, sixth of the seven create_by_* examples and the fourth (last) routing variant added in gopher-orch 0.1.23. Direct port of gopher-mcp-js/examples/api/create_by_gateway_name.ts, which itself ports gopher-orch/examples/sdk/api/create_by_gateway_name.cc. Behaviour: - Scopes the agent to a single MCP gateway in the caller's workspace by human-readable name. Internally hits the same GET /v1/mcp-servers endpoint as create_with_api_key under the Bearer api key, but adds the "?gatewayName={name}" routing query so the response carries the backing MCP servers for that gateway. Use this when the api key owns several gateways and the agent should bind to exactly one identified by name rather than id. - Matches the by-name / by-id pair pattern established earlier for server routing: create_by_server_name + create_by_server_id, create_by_gateway_name + create_by_gateway_id. The next commit (B7) lands the seventh example, create_by_url.py, which is the outlier in the set because it skips the remote fetch entirely. - Provider defaults to AnthropicProvider; model from LLM_MODEL. Refuses to call the FFI when LLM_MODEL, GOPHER_API_KEY, or GOPHER_MCP_GATEWAY_NAME is still the placeholder, printing a clear error to stderr and exiting 1. - Accepts queries as positional argv with the canned "What time is it in Tokyo?" fallback. try / finally with agent.dispose() so the native handle is always released. Requires the create_with_gateway_name factory landed earlier in this PR series. Verified locally: placeholder refusal path exits 1 with a clear diagnostic; syntax check clean; no forbidden tokens. --- examples/api/create_by_gateway_name.py | 120 +++++++++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 examples/api/create_by_gateway_name.py diff --git a/examples/api/create_by_gateway_name.py b/examples/api/create_by_gateway_name.py new file mode 100644 index 00000000..f62c02bb --- /dev/null +++ b/examples/api/create_by_gateway_name.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +""" +SDK example for GopherAgent.create_with_gateway_name. + +Python port of gopher-mcp-js/examples/api/create_by_gateway_name.ts, +which itself ports +gopher-orch/examples/sdk/api/create_by_gateway_name.cc. + +Scopes a GopherAgent to a single MCP gateway in the caller's workspace +by human-readable name. Internally this hits the same GET +/v1/mcp-servers endpoint as create_with_api_key under the Bearer api +key, but adds the "?gatewayName={name}" routing query so the response +carries the backing MCP servers for that gateway. Use this when the +api key owns several gateways and the agent should bind to exactly +one identified by name rather than id. + +Provider defaults to AnthropicProvider; the model is taken from +LLM_MODEL. Override either via env or by editing the constants in +main(). + +Configuration (env vars): + GOPHER_API_KEY Gopher API key for /v1/mcp-servers + GOPHER_MCP_GATEWAY_NAME MCP gateway name to scope the agent to + LLM_PROVIDER Optional. Defaults to "AnthropicProvider". + LLM_MODEL Required. Model identifier the provider accepts. + DEBUG When set, ctypes prints library-resolution diagnostics. + +Usage: + python3 create_by_gateway_name.py # built-in query + python3 create_by_gateway_name.py "query one" "query two" ... # supplied queries +""" + +import os +import sys +import traceback + +from gopher_mcp_python import GopherAgent + +API_KEY_PLACEHOLDER = "{YOUR_GOPHER_API_KEY}" +GATEWAY_NAME_PLACEHOLDER = "{YOUR_MCP_GATEWAY_NAME}" +MODEL_PLACEHOLDER = "{YOUR_LLM_MODEL}" + + +def env_or(name: str, fallback: str) -> str: + """Return os.environ[name] if non-empty, otherwise fallback.""" + value = os.environ.get(name, "") + return value if value else fallback + + +def main() -> None: + print("=== GopherAgent.create_with_gateway_name example ===") + print(f"Usage: python3 {sys.argv[0]} [query1] [query2] ...") + print( + "Env: GOPHER_API_KEY GOPHER_MCP_GATEWAY_NAME LLM_PROVIDER LLM_MODEL DEBUG" + ) + print("") + + queries = sys.argv[1:] if len(sys.argv) > 1 else ["What time is it in Tokyo?"] + + provider = env_or("LLM_PROVIDER", "AnthropicProvider") + model = env_or("LLM_MODEL", MODEL_PLACEHOLDER) + api_key = env_or("GOPHER_API_KEY", API_KEY_PLACEHOLDER) + gateway_name = env_or("GOPHER_MCP_GATEWAY_NAME", GATEWAY_NAME_PLACEHOLDER) + + print(f"Provider: {provider}") + model_label = ( + f"{model} (set LLM_MODEL)" if model == MODEL_PLACEHOLDER else model + ) + print(f"Model: {model_label}") + api_key_label = ( + f"{api_key} (set GOPHER_API_KEY)" + if api_key == API_KEY_PLACEHOLDER + else "" + ) + print(f"API key: {api_key_label}") + gateway_name_label = ( + f"{gateway_name} (set GOPHER_MCP_GATEWAY_NAME)" + if gateway_name == GATEWAY_NAME_PLACEHOLDER + else gateway_name + ) + print(f"MCP gateway name: {gateway_name_label}") + print(f"Queries: {len(queries)}") + + if ( + model == MODEL_PLACEHOLDER + or api_key == API_KEY_PLACEHOLDER + or gateway_name == GATEWAY_NAME_PLACEHOLDER + ): + print( + "\nError: LLM_MODEL, GOPHER_API_KEY, and GOPHER_MCP_GATEWAY_NAME " + "must all be set.", + file=sys.stderr, + ) + sys.exit(1) + + print("\nCreating agent via GopherAgent.create_with_gateway_name...") + agent = GopherAgent.create_with_gateway_name( + provider, model, api_key, gateway_name + ) + print("Agent created successfully!") + + try: + for i, query in enumerate(queries): + print(f"\nQuery {i + 1}: {query}") + answer = agent.run(query) + print(f"\nAgent Response {i + 1}:") + print("--------------------------------") + print(answer) + print("--------------------------------") + finally: + agent.dispose() + + +if __name__ == "__main__": + try: + main() + except Exception as e: + print(f"Error: {e}", file=sys.stderr) + traceback.print_exc(file=sys.stderr) + sys.exit(1) From ccbff9fa04633f19e9d353266997497c47b9e877 Mon Sep 17 00:00:00 2001 From: RahulHere Date: Wed, 17 Jun 2026 23:03:33 +0800 Subject: [PATCH 13/29] Add Python example for create_with_url fix (#9) Land examples/api/create_by_url.py, the seventh and final create_by_* example. Direct port of gopher-mcp-js/examples/api/create_by_url.ts, which itself ports gopher-orch/examples/sdk/api/create_by_url.cc. Behaviour: - Builds a GopherAgent from a single MCP server URL, skipping the remote /v1/mcp-servers fetch that create_with_api_key performs and the inline JSON shape that create_with_server_config requires. Internally the factory synthesises an http_sse server entry around the URL and delegates to create_by_json. Use this for local development or one-off endpoints where the operator already knows the URL. - The outlier of the seven examples: needs no Gopher API key because there is no remote config fetch. Only LLM_MODEL and GOPHER_MCP_URL are required. - Provider defaults to AnthropicProvider; model from LLM_MODEL. Refuses to call the FFI when LLM_MODEL or GOPHER_MCP_URL is still the placeholder, printing a clear error to stderr and exiting 1. - Accepts queries as positional argv with the canned "What time is it in Tokyo?" fallback. try / finally with agent.dispose() so the native handle is always released. Closes the matched set: this is the seventh and last variant needed before the C1 run-script wrappers and C2 README can cover the full set in a single file-to-factory mapping. Verified locally: placeholder refusal path exits 1 with a clear diagnostic; syntax check clean; no forbidden tokens. --- examples/api/create_by_url.py | 99 +++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 examples/api/create_by_url.py diff --git a/examples/api/create_by_url.py b/examples/api/create_by_url.py new file mode 100644 index 00000000..c84320e9 --- /dev/null +++ b/examples/api/create_by_url.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +""" +SDK example for GopherAgent.create_with_url. + +Python port of gopher-mcp-js/examples/api/create_by_url.ts, which +itself ports gopher-orch/examples/sdk/api/create_by_url.cc. + +Builds a GopherAgent from a single MCP server URL, skipping the +remote /v1/mcp-servers fetch that create_with_api_key performs and +the inline JSON shape that create_with_server_config requires. +Internally the factory synthesises an http_sse server entry around +the URL and delegates to create_by_json. Use this for local +development or one-off endpoints where the operator already knows +the URL. + +Provider defaults to AnthropicProvider; the model is taken from +LLM_MODEL. Override either via env or by editing the constants in +main(). + +Configuration (env vars): + GOPHER_MCP_URL Full URL of the MCP server (e.g. http://127.0.0.1:8080/mcp) + LLM_PROVIDER Optional. Defaults to "AnthropicProvider". + LLM_MODEL Required. Model identifier the provider accepts. + DEBUG When set, ctypes prints library-resolution diagnostics. + +Usage: + python3 create_by_url.py # built-in query + python3 create_by_url.py "query one" "query two" ... # supplied queries +""" + +import os +import sys +import traceback + +from gopher_mcp_python import GopherAgent + +URL_PLACEHOLDER = "{YOUR_MCP_URL}" +MODEL_PLACEHOLDER = "{YOUR_LLM_MODEL}" + + +def env_or(name: str, fallback: str) -> str: + """Return os.environ[name] if non-empty, otherwise fallback.""" + value = os.environ.get(name, "") + return value if value else fallback + + +def main() -> None: + print("=== GopherAgent.create_with_url example ===") + print(f"Usage: python3 {sys.argv[0]} [query1] [query2] ...") + print("Env: GOPHER_MCP_URL LLM_PROVIDER LLM_MODEL DEBUG") + print("") + + queries = sys.argv[1:] if len(sys.argv) > 1 else ["What time is it in Tokyo?"] + + provider = env_or("LLM_PROVIDER", "AnthropicProvider") + model = env_or("LLM_MODEL", MODEL_PLACEHOLDER) + url = env_or("GOPHER_MCP_URL", URL_PLACEHOLDER) + + print(f"Provider: {provider}") + model_label = ( + f"{model} (set LLM_MODEL)" if model == MODEL_PLACEHOLDER else model + ) + print(f"Model: {model_label}") + url_label = ( + f"{url} (set GOPHER_MCP_URL)" if url == URL_PLACEHOLDER else url + ) + print(f"MCP URL: {url_label}") + print(f"Queries: {len(queries)}") + + if model == MODEL_PLACEHOLDER or url == URL_PLACEHOLDER: + print( + "\nError: LLM_MODEL and GOPHER_MCP_URL must both be set.", + file=sys.stderr, + ) + sys.exit(1) + + print("\nCreating agent via GopherAgent.create_with_url...") + agent = GopherAgent.create_with_url(provider, model, url) + print("Agent created successfully!") + + try: + for i, query in enumerate(queries): + print(f"\nQuery {i + 1}: {query}") + answer = agent.run(query) + print(f"\nAgent Response {i + 1}:") + print("--------------------------------") + print(answer) + print("--------------------------------") + finally: + agent.dispose() + + +if __name__ == "__main__": + try: + main() + except Exception as e: + print(f"Error: {e}", file=sys.stderr) + traceback.print_exc(file=sys.stderr) + sys.exit(1) From d329df05f8210ff409486e3705711ee6ea005da9 Mon Sep 17 00:00:00 2001 From: RahulHere Date: Wed, 17 Jun 2026 23:06:39 +0800 Subject: [PATCH 14/29] Add seven *_run.sh wrappers for the examples/api/ create_by_* set fix (#9) Land the seven shell wrappers that pair with the .py examples added earlier in this PR series. Single commit per the JS-side precedent at gopher-mcp-js commit 53e3f44a. Files: examples/api/create_by_api_key_run.sh examples/api/create_by_json_run.sh examples/api/create_by_server_id_run.sh examples/api/create_by_server_name_run.sh examples/api/create_by_gateway_id_run.sh examples/api/create_by_gateway_name_run.sh examples/api/create_by_url_run.sh Each wrapper: - Resolves PROJECT_DIR with two dirname() walks because the scripts sit at examples/api/ rather than the existing examples/ root. - Bails out with a pointer at ./build.sh when $PROJECT_DIR/native/lib is missing -- this is where the build.sh CMake output lands and where the loader resolves libgopher-orch.dylib / libgopher-orch.so in dev mode. - Pre-flight checks that gopher_mcp_python is importable via PYTHONPATH="$PROJECT_DIR" python3 -c "import gopher_mcp_python" and prints a clear "pip install -e ." hint on failure. The .ts side did not need this because tsx + relative import work out of the box; Python requires the package to be on sys.path or installed. - Warns (but does not fail) when the per-variant routing env vars, GOPHER_API_KEY, LLM_MODEL, or ANTHROPIC_API_KEY are unset. The .py example itself enforces the hard failure on placeholder values; the wrapper just surfaces the warnings early so a user does not get to the Python-side error after a longer execution path. - Exports DYLD_LIBRARY_PATH (macOS) and LD_LIBRARY_PATH (Linux) pointing at "$PROJECT_DIR/native/lib", plus PYTHONPATH so the example runs even when the package has not been pip installed. - Runs python3 examples/api/.py "$@" so positional argv passes through as queries, matching the .py behaviour. Differences from the existing examples/client_example_json_run.sh: - No lsof / kill-port helper: the new wrappers do not bring up any local server, they just call the SDK factory. Matches the JS-side wrappers at commit 6769a068 which dropped the helper for the same reason. - Two dirname() walks instead of one because the new wrappers are at examples/api/ rather than examples/. Verified locally: chmod +x set on all seven; bash -n syntax check clean on each; smoke run of create_by_server_id_run.sh without env exits 1 with the expected env-var warnings followed by the Python-side placeholder rejection; no forbidden tokens. --- examples/api/create_by_api_key_run.sh | 60 ++++++++++++++++++++ examples/api/create_by_gateway_id_run.sh | 66 ++++++++++++++++++++++ examples/api/create_by_gateway_name_run.sh | 66 ++++++++++++++++++++++ examples/api/create_by_json_run.sh | 54 ++++++++++++++++++ examples/api/create_by_server_id_run.sh | 66 ++++++++++++++++++++++ examples/api/create_by_server_name_run.sh | 66 ++++++++++++++++++++++ examples/api/create_by_url_run.sh | 60 ++++++++++++++++++++ 7 files changed, 438 insertions(+) create mode 100755 examples/api/create_by_api_key_run.sh create mode 100755 examples/api/create_by_gateway_id_run.sh create mode 100755 examples/api/create_by_gateway_name_run.sh create mode 100755 examples/api/create_by_json_run.sh create mode 100755 examples/api/create_by_server_id_run.sh create mode 100755 examples/api/create_by_server_name_run.sh create mode 100755 examples/api/create_by_url_run.sh diff --git a/examples/api/create_by_api_key_run.sh b/examples/api/create_by_api_key_run.sh new file mode 100755 index 00000000..dd661ac9 --- /dev/null +++ b/examples/api/create_by_api_key_run.sh @@ -0,0 +1,60 @@ +#!/bin/bash + +# Run the Python SDK example for GopherAgent.create_with_api_key. + +set -e + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_DIR="$(dirname "$(dirname "$SCRIPT_DIR")")" + +echo -e "${GREEN}=========================================${NC}" +echo -e "${GREEN}GopherAgent.create_with_api_key example${NC}" +echo -e "${GREEN}=========================================${NC}" +echo "" + +if [ ! -d "$PROJECT_DIR/native/lib" ]; then + echo -e "${RED}Error: Native library not found at $PROJECT_DIR/native/lib${NC}" + echo -e "${YELLOW}Please run ./build.sh first${NC}" + exit 1 +fi + +if ! PYTHONPATH="$PROJECT_DIR" python3 -c "import gopher_mcp_python" 2>/dev/null; then + echo -e "${RED}Error: gopher_mcp_python is not importable${NC}" + echo -e "${YELLOW}Run: pip install -e . (from $PROJECT_DIR)${NC}" + exit 1 +fi + +if [ -z "$GOPHER_API_KEY" ]; then + echo -e "${YELLOW}Warning: GOPHER_API_KEY environment variable is not set${NC}" + echo -e "${YELLOW}Set it with: export GOPHER_API_KEY=your_api_key${NC}" + echo "" +fi + +if [ -z "$LLM_MODEL" ]; then + echo -e "${YELLOW}Warning: LLM_MODEL environment variable is not set${NC}" + echo -e "${YELLOW}Set it with: export LLM_MODEL=${NC}" + echo "" +fi + +if [ -z "$ANTHROPIC_API_KEY" ]; then + echo -e "${YELLOW}Warning: ANTHROPIC_API_KEY environment variable is not set${NC}" + echo -e "${YELLOW}(Required for the default AnthropicProvider.)${NC}" + echo "" +fi + +cd "$PROJECT_DIR" + +PYTHONPATH="$PROJECT_DIR" \ +DYLD_LIBRARY_PATH="$PROJECT_DIR/native/lib" \ +LD_LIBRARY_PATH="$PROJECT_DIR/native/lib" \ +python3 examples/api/create_by_api_key.py "$@" + +echo "" +echo -e "${GREEN}Example completed${NC}" + +exit 0 diff --git a/examples/api/create_by_gateway_id_run.sh b/examples/api/create_by_gateway_id_run.sh new file mode 100755 index 00000000..963bfb31 --- /dev/null +++ b/examples/api/create_by_gateway_id_run.sh @@ -0,0 +1,66 @@ +#!/bin/bash + +# Run the Python SDK example for GopherAgent.create_with_gateway_id. + +set -e + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_DIR="$(dirname "$(dirname "$SCRIPT_DIR")")" + +echo -e "${GREEN}===========================================${NC}" +echo -e "${GREEN}GopherAgent.create_with_gateway_id example${NC}" +echo -e "${GREEN}===========================================${NC}" +echo "" + +if [ ! -d "$PROJECT_DIR/native/lib" ]; then + echo -e "${RED}Error: Native library not found at $PROJECT_DIR/native/lib${NC}" + echo -e "${YELLOW}Please run ./build.sh first${NC}" + exit 1 +fi + +if ! PYTHONPATH="$PROJECT_DIR" python3 -c "import gopher_mcp_python" 2>/dev/null; then + echo -e "${RED}Error: gopher_mcp_python is not importable${NC}" + echo -e "${YELLOW}Run: pip install -e . (from $PROJECT_DIR)${NC}" + exit 1 +fi + +if [ -z "$GOPHER_API_KEY" ]; then + echo -e "${YELLOW}Warning: GOPHER_API_KEY environment variable is not set${NC}" + echo -e "${YELLOW}Set it with: export GOPHER_API_KEY=your_api_key${NC}" + echo "" +fi + +if [ -z "$GOPHER_MCP_GATEWAY_ID" ]; then + echo -e "${YELLOW}Warning: GOPHER_MCP_GATEWAY_ID environment variable is not set${NC}" + echo -e "${YELLOW}Set it with: export GOPHER_MCP_GATEWAY_ID=gw-...${NC}" + echo "" +fi + +if [ -z "$LLM_MODEL" ]; then + echo -e "${YELLOW}Warning: LLM_MODEL environment variable is not set${NC}" + echo -e "${YELLOW}Set it with: export LLM_MODEL=${NC}" + echo "" +fi + +if [ -z "$ANTHROPIC_API_KEY" ]; then + echo -e "${YELLOW}Warning: ANTHROPIC_API_KEY environment variable is not set${NC}" + echo -e "${YELLOW}(Required for the default AnthropicProvider.)${NC}" + echo "" +fi + +cd "$PROJECT_DIR" + +PYTHONPATH="$PROJECT_DIR" \ +DYLD_LIBRARY_PATH="$PROJECT_DIR/native/lib" \ +LD_LIBRARY_PATH="$PROJECT_DIR/native/lib" \ +python3 examples/api/create_by_gateway_id.py "$@" + +echo "" +echo -e "${GREEN}Example completed${NC}" + +exit 0 diff --git a/examples/api/create_by_gateway_name_run.sh b/examples/api/create_by_gateway_name_run.sh new file mode 100755 index 00000000..8185578e --- /dev/null +++ b/examples/api/create_by_gateway_name_run.sh @@ -0,0 +1,66 @@ +#!/bin/bash + +# Run the Python SDK example for GopherAgent.create_with_gateway_name. + +set -e + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_DIR="$(dirname "$(dirname "$SCRIPT_DIR")")" + +echo -e "${GREEN}=============================================${NC}" +echo -e "${GREEN}GopherAgent.create_with_gateway_name example${NC}" +echo -e "${GREEN}=============================================${NC}" +echo "" + +if [ ! -d "$PROJECT_DIR/native/lib" ]; then + echo -e "${RED}Error: Native library not found at $PROJECT_DIR/native/lib${NC}" + echo -e "${YELLOW}Please run ./build.sh first${NC}" + exit 1 +fi + +if ! PYTHONPATH="$PROJECT_DIR" python3 -c "import gopher_mcp_python" 2>/dev/null; then + echo -e "${RED}Error: gopher_mcp_python is not importable${NC}" + echo -e "${YELLOW}Run: pip install -e . (from $PROJECT_DIR)${NC}" + exit 1 +fi + +if [ -z "$GOPHER_API_KEY" ]; then + echo -e "${YELLOW}Warning: GOPHER_API_KEY environment variable is not set${NC}" + echo -e "${YELLOW}Set it with: export GOPHER_API_KEY=your_api_key${NC}" + echo "" +fi + +if [ -z "$GOPHER_MCP_GATEWAY_NAME" ]; then + echo -e "${YELLOW}Warning: GOPHER_MCP_GATEWAY_NAME environment variable is not set${NC}" + echo -e "${YELLOW}Set it with: export GOPHER_MCP_GATEWAY_NAME=my-gateway${NC}" + echo "" +fi + +if [ -z "$LLM_MODEL" ]; then + echo -e "${YELLOW}Warning: LLM_MODEL environment variable is not set${NC}" + echo -e "${YELLOW}Set it with: export LLM_MODEL=${NC}" + echo "" +fi + +if [ -z "$ANTHROPIC_API_KEY" ]; then + echo -e "${YELLOW}Warning: ANTHROPIC_API_KEY environment variable is not set${NC}" + echo -e "${YELLOW}(Required for the default AnthropicProvider.)${NC}" + echo "" +fi + +cd "$PROJECT_DIR" + +PYTHONPATH="$PROJECT_DIR" \ +DYLD_LIBRARY_PATH="$PROJECT_DIR/native/lib" \ +LD_LIBRARY_PATH="$PROJECT_DIR/native/lib" \ +python3 examples/api/create_by_gateway_name.py "$@" + +echo "" +echo -e "${GREEN}Example completed${NC}" + +exit 0 diff --git a/examples/api/create_by_json_run.sh b/examples/api/create_by_json_run.sh new file mode 100755 index 00000000..8cc158e3 --- /dev/null +++ b/examples/api/create_by_json_run.sh @@ -0,0 +1,54 @@ +#!/bin/bash + +# Run the Python SDK example for GopherAgent.create_with_server_config. + +set -e + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_DIR="$(dirname "$(dirname "$SCRIPT_DIR")")" + +echo -e "${GREEN}===============================================${NC}" +echo -e "${GREEN}GopherAgent.create_with_server_config example${NC}" +echo -e "${GREEN}===============================================${NC}" +echo "" + +if [ ! -d "$PROJECT_DIR/native/lib" ]; then + echo -e "${RED}Error: Native library not found at $PROJECT_DIR/native/lib${NC}" + echo -e "${YELLOW}Please run ./build.sh first${NC}" + exit 1 +fi + +if ! PYTHONPATH="$PROJECT_DIR" python3 -c "import gopher_mcp_python" 2>/dev/null; then + echo -e "${RED}Error: gopher_mcp_python is not importable${NC}" + echo -e "${YELLOW}Run: pip install -e . (from $PROJECT_DIR)${NC}" + exit 1 +fi + +if [ -z "$LLM_MODEL" ]; then + echo -e "${YELLOW}Warning: LLM_MODEL environment variable is not set${NC}" + echo -e "${YELLOW}Set it with: export LLM_MODEL=${NC}" + echo "" +fi + +if [ -z "$ANTHROPIC_API_KEY" ]; then + echo -e "${YELLOW}Warning: ANTHROPIC_API_KEY environment variable is not set${NC}" + echo -e "${YELLOW}(Required for the default AnthropicProvider.)${NC}" + echo "" +fi + +cd "$PROJECT_DIR" + +PYTHONPATH="$PROJECT_DIR" \ +DYLD_LIBRARY_PATH="$PROJECT_DIR/native/lib" \ +LD_LIBRARY_PATH="$PROJECT_DIR/native/lib" \ +python3 examples/api/create_by_json.py "$@" + +echo "" +echo -e "${GREEN}Example completed${NC}" + +exit 0 diff --git a/examples/api/create_by_server_id_run.sh b/examples/api/create_by_server_id_run.sh new file mode 100755 index 00000000..be843a1f --- /dev/null +++ b/examples/api/create_by_server_id_run.sh @@ -0,0 +1,66 @@ +#!/bin/bash + +# Run the Python SDK example for GopherAgent.create_with_server_id. + +set -e + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_DIR="$(dirname "$(dirname "$SCRIPT_DIR")")" + +echo -e "${GREEN}==========================================${NC}" +echo -e "${GREEN}GopherAgent.create_with_server_id example${NC}" +echo -e "${GREEN}==========================================${NC}" +echo "" + +if [ ! -d "$PROJECT_DIR/native/lib" ]; then + echo -e "${RED}Error: Native library not found at $PROJECT_DIR/native/lib${NC}" + echo -e "${YELLOW}Please run ./build.sh first${NC}" + exit 1 +fi + +if ! PYTHONPATH="$PROJECT_DIR" python3 -c "import gopher_mcp_python" 2>/dev/null; then + echo -e "${RED}Error: gopher_mcp_python is not importable${NC}" + echo -e "${YELLOW}Run: pip install -e . (from $PROJECT_DIR)${NC}" + exit 1 +fi + +if [ -z "$GOPHER_API_KEY" ]; then + echo -e "${YELLOW}Warning: GOPHER_API_KEY environment variable is not set${NC}" + echo -e "${YELLOW}Set it with: export GOPHER_API_KEY=your_api_key${NC}" + echo "" +fi + +if [ -z "$GOPHER_MCP_SERVER_ID" ]; then + echo -e "${YELLOW}Warning: GOPHER_MCP_SERVER_ID environment variable is not set${NC}" + echo -e "${YELLOW}Set it with: export GOPHER_MCP_SERVER_ID=srv-...${NC}" + echo "" +fi + +if [ -z "$LLM_MODEL" ]; then + echo -e "${YELLOW}Warning: LLM_MODEL environment variable is not set${NC}" + echo -e "${YELLOW}Set it with: export LLM_MODEL=${NC}" + echo "" +fi + +if [ -z "$ANTHROPIC_API_KEY" ]; then + echo -e "${YELLOW}Warning: ANTHROPIC_API_KEY environment variable is not set${NC}" + echo -e "${YELLOW}(Required for the default AnthropicProvider.)${NC}" + echo "" +fi + +cd "$PROJECT_DIR" + +PYTHONPATH="$PROJECT_DIR" \ +DYLD_LIBRARY_PATH="$PROJECT_DIR/native/lib" \ +LD_LIBRARY_PATH="$PROJECT_DIR/native/lib" \ +python3 examples/api/create_by_server_id.py "$@" + +echo "" +echo -e "${GREEN}Example completed${NC}" + +exit 0 diff --git a/examples/api/create_by_server_name_run.sh b/examples/api/create_by_server_name_run.sh new file mode 100755 index 00000000..6ad59701 --- /dev/null +++ b/examples/api/create_by_server_name_run.sh @@ -0,0 +1,66 @@ +#!/bin/bash + +# Run the Python SDK example for GopherAgent.create_with_server_name. + +set -e + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_DIR="$(dirname "$(dirname "$SCRIPT_DIR")")" + +echo -e "${GREEN}============================================${NC}" +echo -e "${GREEN}GopherAgent.create_with_server_name example${NC}" +echo -e "${GREEN}============================================${NC}" +echo "" + +if [ ! -d "$PROJECT_DIR/native/lib" ]; then + echo -e "${RED}Error: Native library not found at $PROJECT_DIR/native/lib${NC}" + echo -e "${YELLOW}Please run ./build.sh first${NC}" + exit 1 +fi + +if ! PYTHONPATH="$PROJECT_DIR" python3 -c "import gopher_mcp_python" 2>/dev/null; then + echo -e "${RED}Error: gopher_mcp_python is not importable${NC}" + echo -e "${YELLOW}Run: pip install -e . (from $PROJECT_DIR)${NC}" + exit 1 +fi + +if [ -z "$GOPHER_API_KEY" ]; then + echo -e "${YELLOW}Warning: GOPHER_API_KEY environment variable is not set${NC}" + echo -e "${YELLOW}Set it with: export GOPHER_API_KEY=your_api_key${NC}" + echo "" +fi + +if [ -z "$GOPHER_MCP_SERVER_NAME" ]; then + echo -e "${YELLOW}Warning: GOPHER_MCP_SERVER_NAME environment variable is not set${NC}" + echo -e "${YELLOW}Set it with: export GOPHER_MCP_SERVER_NAME=my-server${NC}" + echo "" +fi + +if [ -z "$LLM_MODEL" ]; then + echo -e "${YELLOW}Warning: LLM_MODEL environment variable is not set${NC}" + echo -e "${YELLOW}Set it with: export LLM_MODEL=${NC}" + echo "" +fi + +if [ -z "$ANTHROPIC_API_KEY" ]; then + echo -e "${YELLOW}Warning: ANTHROPIC_API_KEY environment variable is not set${NC}" + echo -e "${YELLOW}(Required for the default AnthropicProvider.)${NC}" + echo "" +fi + +cd "$PROJECT_DIR" + +PYTHONPATH="$PROJECT_DIR" \ +DYLD_LIBRARY_PATH="$PROJECT_DIR/native/lib" \ +LD_LIBRARY_PATH="$PROJECT_DIR/native/lib" \ +python3 examples/api/create_by_server_name.py "$@" + +echo "" +echo -e "${GREEN}Example completed${NC}" + +exit 0 diff --git a/examples/api/create_by_url_run.sh b/examples/api/create_by_url_run.sh new file mode 100755 index 00000000..b3ba0e54 --- /dev/null +++ b/examples/api/create_by_url_run.sh @@ -0,0 +1,60 @@ +#!/bin/bash + +# Run the Python SDK example for GopherAgent.create_with_url. + +set -e + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_DIR="$(dirname "$(dirname "$SCRIPT_DIR")")" + +echo -e "${GREEN}=====================================${NC}" +echo -e "${GREEN}GopherAgent.create_with_url example${NC}" +echo -e "${GREEN}=====================================${NC}" +echo "" + +if [ ! -d "$PROJECT_DIR/native/lib" ]; then + echo -e "${RED}Error: Native library not found at $PROJECT_DIR/native/lib${NC}" + echo -e "${YELLOW}Please run ./build.sh first${NC}" + exit 1 +fi + +if ! PYTHONPATH="$PROJECT_DIR" python3 -c "import gopher_mcp_python" 2>/dev/null; then + echo -e "${RED}Error: gopher_mcp_python is not importable${NC}" + echo -e "${YELLOW}Run: pip install -e . (from $PROJECT_DIR)${NC}" + exit 1 +fi + +if [ -z "$GOPHER_MCP_URL" ]; then + echo -e "${YELLOW}Warning: GOPHER_MCP_URL environment variable is not set${NC}" + echo -e "${YELLOW}Set it with: export GOPHER_MCP_URL=http://127.0.0.1:8080/mcp${NC}" + echo "" +fi + +if [ -z "$LLM_MODEL" ]; then + echo -e "${YELLOW}Warning: LLM_MODEL environment variable is not set${NC}" + echo -e "${YELLOW}Set it with: export LLM_MODEL=${NC}" + echo "" +fi + +if [ -z "$ANTHROPIC_API_KEY" ]; then + echo -e "${YELLOW}Warning: ANTHROPIC_API_KEY environment variable is not set${NC}" + echo -e "${YELLOW}(Required for the default AnthropicProvider.)${NC}" + echo "" +fi + +cd "$PROJECT_DIR" + +PYTHONPATH="$PROJECT_DIR" \ +DYLD_LIBRARY_PATH="$PROJECT_DIR/native/lib" \ +LD_LIBRARY_PATH="$PROJECT_DIR/native/lib" \ +python3 examples/api/create_by_url.py "$@" + +echo "" +echo -e "${GREEN}Example completed${NC}" + +exit 0 From 3ff7a19f51e99467b8b03dd13388bd72faafadd7 Mon Sep 17 00:00:00 2001 From: RahulHere Date: Wed, 17 Jun 2026 23:11:48 +0800 Subject: [PATCH 15/29] Add examples/api/README.md fix (#9) Land examples/api/README.md, the documentation index for the seven .py / *_run.sh example pairs added earlier in this PR series. Direct port of gopher-mcp-js/examples/api/README.md (commit 26b4662d on the JS side) with Python idiom translations. Sections: - File-to-factory mapping: three-column table that ties each Python example back to both the C++ canonical reference and the TypeScript sibling, plus the GopherAgent factory each exercises. Lists all seven create_by_* variants so a reader can navigate from any one of the three SDKs to the other two. - Quick start: build.sh first to drop the native dylib into native/lib/, then pip install -e . for editable-mode imports, then invoke a wrapper. Mirrors the JS-side three-step intro with the pip step substituted for the npm install one. - Environment-variable matrix: required vs optional env vars per example. Includes the four routing variants (server_id, server_name, gateway_id, gateway_name), plus a note that LLM_MODEL has no default and each example refuses to start until it is set -- the same env-var-required path the JS side picked. - Picking the right factory: ties each factory to its routing query string and whether it triggers a /v1/mcp-servers fetch. Mirrors the gopher-orch/docs/Agent.md "Simple creation factories" section so the Python docs stay aligned with the upstream C++ docs. - SDK-resolution note: the .py files import `from gopher_mcp_python import GopherAgent`, which resolves against whatever pip install -e . pointed at. Downstream consumers who pip install gopher-mcp-python from PyPI and copy an example do not need to edit the import path. Calls out the intentional difference from the JS sibling, which uses a relative `'../../src'` import that has to be rewritten on copy-out. - Native-library resolution note: documents the DYLD_LIBRARY_PATH / LD_LIBRARY_PATH story for the wrappers, the pre-flight `python3 -c "import gopher_mcp_python"` check (which the TypeScript wrappers don't need because tsx handles the relative import for them), and the GOPHER_MCP_PYTHON_LIBRARY_PATH env var as an explicit override checked first by gopher_mcp_python/ffi/library.py. - Cross-reference block: pointers at the C++ examples and docs, the TypeScript siblings, the FFI binding layer (gopher_mcp_python/ffi/library.py), the high-level wrappers (gopher_mcp_python/agent.py), and the contract tests (tests/test_agent_create_by.py). Closes the examples/api/ landing series for the seven create_by_* variants on the Python side. C3 (pyproject.toml / setup.py adjustments) was tagged OPTIONAL in the design note and skipped because no new dependency was needed -- the examples use only stdlib (os, sys, json, traceback) plus the SDK itself. Verified locally: no forbidden tokens; all seven .py and *_run.sh files referenced in the file-to-factory mapping table exist. --- examples/api/README.md | 179 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 179 insertions(+) create mode 100644 examples/api/README.md diff --git a/examples/api/README.md b/examples/api/README.md new file mode 100644 index 00000000..81957635 --- /dev/null +++ b/examples/api/README.md @@ -0,0 +1,179 @@ +# examples/api — Python SDK examples for the seven `create_by_*` factories + +This directory holds the Python siblings of the C++ SDK examples under +[`gopher-orch/examples/sdk/api/`](../../third_party/gopher-orch/examples/sdk/api/) +and the TypeScript siblings under +[`gopher-mcp-js/examples/api/`](https://github.com/GopherSecurity/gopher-mcp-js/tree/main/examples/api). +Each `.py` file mirrors its `.cc` and `.ts` counterparts one-to-one +and exercises exactly one of the seven `create_by_*` factories the +SDK exposes through `GopherAgent`. + +## File-to-factory mapping + +| C++ reference | Python port | TypeScript port | `GopherAgent` factory | +| ------------------------------ | ------------------------------ | ------------------------------ | ------------------------------ | +| `create_by_api_key.cc` | `create_by_api_key.py` | `create_by_api_key.ts` | `create_with_api_key` | +| `create_by_json.cc` | `create_by_json.py` | `create_by_json.ts` | `create_with_server_config` | +| `create_by_server_id.cc` | `create_by_server_id.py` | `create_by_server_id.ts` | `create_with_server_id` | +| `create_by_server_name.cc` | `create_by_server_name.py` | `create_by_server_name.ts` | `create_with_server_name` | +| `create_by_gateway_id.cc` | `create_by_gateway_id.py` | `create_by_gateway_id.ts` | `create_with_gateway_id` | +| `create_by_gateway_name.cc` | `create_by_gateway_name.py` | `create_by_gateway_name.ts` | `create_with_gateway_name` | +| `create_by_url.cc` | `create_by_url.py` | `create_by_url.ts` | `create_with_url` | + +Each `.py` file ships with a `*_run.sh` wrapper that sets the native +library path, exports `PYTHONPATH` so the in-repo package is +importable without `pip install`, and forwards positional arguments +as queries. + +## Quick start + +1. Build the native library (one-time, repeats after a submodule bump): + + ```sh + cd /Users/james/Desktop/dev/gopher-mcp-python + ./build.sh + ``` + + This builds `third_party/gopher-orch` and drops the resulting + `libgopher-orch.dylib` / `.so` / `.dll` into `native/lib/`. + +2. Install the Python package in editable mode so the examples can + import `gopher_mcp_python` without manual `PYTHONPATH` tweaking: + + ```sh + pip install -e . + ``` + + The wrappers also export `PYTHONPATH` as a belt-and-braces measure, + so you can skip this step if you only want to run the wrappers + themselves. + +3. Pick a factory and run the matching wrapper: + + ```sh + export GOPHER_API_KEY=... + export LLM_MODEL= + export ANTHROPIC_API_KEY=... + ./examples/api/create_by_api_key_run.sh "What time is it in Tokyo?" + ``` + + Positional arguments to the wrapper become queries; with no + arguments each example runs a canned query so a first invocation + produces visible output. + +4. To target a specific MCP server, MCP gateway, or one-off URL, set + the corresponding routing env var and run the matching wrapper. + See the table below. + +## Environment variables per example + +| Example | Required | Optional | +| ------------------------ | ---------------------------------------------------------- | ----------------------- | +| `create_by_api_key` | `GOPHER_API_KEY`, `LLM_MODEL` | `LLM_PROVIDER`, `DEBUG` | +| `create_by_json` | `LLM_MODEL` | `LLM_PROVIDER`, `DEBUG` | +| `create_by_server_id` | `GOPHER_API_KEY`, `GOPHER_MCP_SERVER_ID`, `LLM_MODEL` | `LLM_PROVIDER`, `DEBUG` | +| `create_by_server_name` | `GOPHER_API_KEY`, `GOPHER_MCP_SERVER_NAME`, `LLM_MODEL` | `LLM_PROVIDER`, `DEBUG` | +| `create_by_gateway_id` | `GOPHER_API_KEY`, `GOPHER_MCP_GATEWAY_ID`, `LLM_MODEL` | `LLM_PROVIDER`, `DEBUG` | +| `create_by_gateway_name` | `GOPHER_API_KEY`, `GOPHER_MCP_GATEWAY_NAME`, `LLM_MODEL` | `LLM_PROVIDER`, `DEBUG` | +| `create_by_url` | `GOPHER_MCP_URL`, `LLM_MODEL` | `LLM_PROVIDER`, `DEBUG` | + +Notes: + +- `LLM_PROVIDER` defaults to `AnthropicProvider` in every example. +- `LLM_MODEL` has no default; each example refuses to start until the + variable is set rather than calling into the FFI with a placeholder. + This matches the env-var-required path the JS side picked so the + examples never surface a stale or fictional model identifier. +- The LLM provider's own credentials (`ANTHROPIC_API_KEY`, + `OPENAI_API_KEY`, `GOOGLE_API_KEY`, etc.) are required by the + backing provider rather than by the SDK directly; the wrappers + warn if `ANTHROPIC_API_KEY` is unset since `AnthropicProvider` is + the default. + +## Picking the right factory + +| Factory | Selects | Network call | +| ----------------------------- | --------------------------------------------------------------- | -------------------------------------------------- | +| `create_with_api_key` | Every MCP server the api key owns | `GET /v1/mcp-servers` | +| `create_with_server_config` | Servers described in an inline JSON document | None | +| `create_with_server_id` | One MCP server by id | `GET /v1/mcp-servers?serverId=...` | +| `create_with_server_name` | One MCP server by name | `GET /v1/mcp-servers?serverName=...` | +| `create_with_gateway_id` | All MCP servers under one gateway by id | `GET /v1/mcp-servers?gatewayId=...` | +| `create_with_gateway_name` | All MCP servers under one gateway by name | `GET /v1/mcp-servers?gatewayName=...` | +| `create_with_url` | One MCP server reachable at a known URL | None (synthesised locally to an `http_sse` entry) | + +The table mirrors the C++ canonical reference at +`gopher-orch/docs/Agent.md` ("Simple creation factories" section) so +the Python-side documentation stays aligned with the upstream C++ +docs and the TypeScript port. + +## How the examples find the SDK + +Each `.py` file imports `GopherAgent` from the installed +`gopher_mcp_python` package: + +```python +from gopher_mcp_python import GopherAgent +``` + +Resolution flow: + +- During development: `pip install -e .` from the repo root makes + the in-tree `gopher_mcp_python/` directory importable. The + wrappers also set `PYTHONPATH="$PROJECT_DIR"` so the import works + without a prior `pip install -e .`. +- Downstream: a consumer who runs `pip install gopher-mcp-python` + and copies one of these examples into their own project does + not need any import-path edit — the same line works against the + PyPI-installed package as-is. + +This differs from the JS sibling, which uses `import { GopherAgent } +from '../../src'` to keep the example tied to the in-repo TypeScript +sources. Python uses package-install semantics so the same `from +gopher_mcp_python import GopherAgent` line works in both the in-repo +and the installed-from-PyPI cases without modification. + +## How the wrappers find the native library + +Each `*_run.sh` script: + +1. Resolves `PROJECT_DIR` to the `gopher-mcp-python` repo root (two + `dirname` calls because the scripts sit at `examples/api/` + rather than `examples/`). +2. Exits early with a pointer at `./build.sh` if + `$PROJECT_DIR/native/lib` is missing. +3. Runs a pre-flight `python3 -c "import gopher_mcp_python"` and + prints a `pip install -e .` hint on failure. The `.ts` side did + not need this step because `npx tsx` + relative import work out + of the box; Python requires the package on `sys.path`. +4. Exports `DYLD_LIBRARY_PATH` (macOS) and `LD_LIBRARY_PATH` + (Linux) to `$PROJECT_DIR/native/lib` so the ctypes loader picks + up the freshly built dylib from `build.sh` rather than the + pip-installed platform package or any system-installed copy. +5. Exports `PYTHONPATH="$PROJECT_DIR"` so the in-tree package is + importable even when `pip install -e .` has not been run. +6. Invokes `python3 examples/api/.py "$@"` so positional + arguments pass straight through as queries. + +If you need to point at a different library location entirely, set +`GOPHER_MCP_PYTHON_LIBRARY_PATH` before invoking the wrapper. That +env var is checked first by `gopher_mcp_python/ffi/library.py` and +bypasses the `native/lib/` and platform-package resolution steps. + +## Cross-reference + +- C++ canonical examples: + `gopher-orch/examples/sdk/api/` (in the `third_party/gopher-orch` + submodule of this repo). +- C++ canonical docs: + `gopher-orch/docs/Agent.md` ("Simple creation factories" + section). +- TypeScript siblings: + [`gopher-mcp-js/examples/api/`](https://github.com/GopherSecurity/gopher-mcp-js/tree/main/examples/api). +- FFI binding layer: + `gopher_mcp_python/ffi/library.py` (`agent_create_by_*` methods). +- High-level wrappers: + `gopher_mcp_python/agent.py` (`GopherAgent.create_with_*` static + methods). +- Contract tests: + `tests/test_agent_create_by.py`. From 1093243d0289bf67ce291c5df724c9bc926ca705 Mon Sep 17 00:00:00 2001 From: RahulHere Date: Thu, 18 Jun 2026 00:07:47 +0800 Subject: [PATCH 16/29] Auto-populate CHANGELOG.md in dump-version.sh (#9) Replace the "warn / interactive prompt if [Unreleased] is empty" check with an auto-population step that builds release notes from the Python repo git log since the previous tag and from the gopher-orch GitHub release notes for the new version. Mirrors the same improvement landed on the JS-side dump-version.sh so the two SDKs share a release workflow. Verified by dry-running Step 4 against the current repo state (v0.1.21 -> v0.1.23): 15 Python commits in range, gopher-orch notes fetched and embedded, original CHANGELOG.md restored afterwards. No interactive input required. --- dump-version.sh | 157 ++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 138 insertions(+), 19 deletions(-) diff --git a/dump-version.sh b/dump-version.sh index 3756d7b8..03209de8 100755 --- a/dump-version.sh +++ b/dump-version.sh @@ -14,9 +14,13 @@ # 1. Fetch latest version from gopher-orch releases # 2. Validate and determine the target version # 3. Update pyproject.toml (main and platform packages) -# 4. Update __init__.py files -# 5. Update CHANGELOG.md ([Unreleased] -> [X.Y.Z] - date) -# 6. Commit the changes +# 4. Auto-populate CHANGELOG.md [Unreleased] section from: +# - git log of this repo since the previous tag +# - the gopher-orch GitHub release notes for the new version +# (manual entries already in [Unreleased] are preserved and shown first) +# 5. Update __init__.py files and platform packages +# 6. Promote [Unreleased] -> [X.Y.Z] - date +# 7. Commit the changes # # After running this script: # 1. Review the changes: git diff HEAD~1 @@ -148,33 +152,148 @@ if [ "$CURRENT_VERSION" = "$TARGET_VERSION" ]; then fi # ----------------------------------------------------------------------------- -# Step 4: Check [Unreleased] section has content +# Step 4: Build release notes from git log + gopher-orch release notes # ----------------------------------------------------------------------------- echo "" -echo -e "${YELLOW}Step 4: Checking [Unreleased] section...${NC}" +echo -e "${YELLOW}Step 4: Building release notes...${NC}" if [ ! -f "$CHANGELOG_FILE" ]; then echo -e "${RED}Error: $CHANGELOG_FILE not found${NC}" exit 1 fi -# Extract content between [Unreleased] and next ## section -UNRELEASED_CONTENT=$(sed -n '/^## \[Unreleased\]/,/^## \[/p' "$CHANGELOG_FILE" | \ - grep -v "^## \[" | grep -v "^$" | head -20) +# Fetch tags so PREV_TAG resolution is accurate even on shallow clones +git fetch --tags --quiet 2>/dev/null || true -if [ -z "$UNRELEASED_CONTENT" ]; then - echo -e "${YELLOW}Warning: [Unreleased] section in CHANGELOG.md appears empty${NC}" - echo "You may want to add release notes before continuing." - read -p "Continue anyway? (y/N) " -n 1 -r - echo - if [[ ! $REPLY =~ ^[Yy]$ ]]; then - exit 1 - fi +# Previous Python tag (anything matching v*, sorted by semver) +PREV_TAG=$(git tag -l 'v*' --sort=-v:refname | head -1) +if [ -n "$PREV_TAG" ]; then + echo -e " Previous Python tag: ${CYAN}$PREV_TAG${NC}" + PY_RANGE="$PREV_TAG..HEAD" +else + echo -e " Previous Python tag: ${YELLOW}none (first release)${NC}" + PY_RANGE="HEAD" +fi + +# Previous gopher-orch version recorded in the previous release commit +PREV_GOPHER_ORCH_VERSION="" +if [ -n "$PREV_TAG" ]; then + PREV_GOPHER_ORCH_VERSION=$(git log -1 --format=%B "$PREV_TAG" 2>/dev/null | \ + grep -oE 'gopher-orch version: [0-9]+\.[0-9]+\.[0-9]+' | \ + awk '{print $NF}' | head -1) +fi +if [ -n "$PREV_GOPHER_ORCH_VERSION" ]; then + echo -e " Previous gopher-orch: ${CYAN}v$PREV_GOPHER_ORCH_VERSION${NC}" else - echo -e " ${GREEN}[Unreleased] section has content${NC}" - echo " Preview:" - echo "$UNRELEASED_CONTENT" | head -5 | sed 's/^/ /' + echo -e " Previous gopher-orch: ${YELLOW}unknown${NC}" fi +echo -e " New gopher-orch: ${GREEN}v$GOPHER_ORCH_VERSION${NC}" + +# Preserve any manually-authored entries already under [Unreleased] +MANUAL_CONTENT=$(awk ' + /^## \[Unreleased\]/ { capture = 1; next } + /^## \[/ && capture { capture = 0 } + capture { print } +' "$CHANGELOG_FILE" | sed -e '/^[[:space:]]*$/d') + +# Collect Python repo commits since previous tag (skip merges + prior release commits) +PY_COMMITS=$(git log --no-merges --pretty=format:'- %s' \ + --invert-grep --grep='^Release version' --grep='^\[release\]' \ + $PY_RANGE 2>/dev/null || true) + +PY_COMMIT_COUNT=0 +if [ -n "$PY_COMMITS" ]; then + PY_COMMIT_COUNT=$(printf '%s\n' "$PY_COMMITS" | wc -l | tr -d ' ') +fi +echo -e " Python commits in range:${GREEN} $PY_COMMIT_COUNT${NC}" + +# Extract the "What's Changed" block from the gopher-orch release notes, +# stripping the Build Information preamble and the trailing Full Changelog link. +GOPHER_ORCH_NOTES=$(gh release view "v$GOPHER_ORCH_VERSION" \ + --repo GopherSecurity/gopher-orch \ + --json body -q '.body' 2>/dev/null | \ + awk ' + /^## What.s Changed/ { capture = 1; next } + /^\*\*Full Changelog\*\*/ { capture = 0 } + capture { print } + ' | sed -e '/^---$/d') + +if [ -n "$GOPHER_ORCH_NOTES" ]; then + echo -e " gopher-orch notes: ${GREEN}fetched${NC}" +else + echo -e " gopher-orch notes: ${YELLOW}empty (using link only)${NC}" +fi + +# Build the new [Unreleased] body +RELEASE_NOTES_FILE=$(mktemp) +{ + if [ -n "$MANUAL_CONTENT" ]; then + printf '%s\n\n' "$MANUAL_CONTENT" + fi + + echo "### Changed" + echo "" + if [ -n "$PREV_GOPHER_ORCH_VERSION" ] && \ + [ "$PREV_GOPHER_ORCH_VERSION" != "$GOPHER_ORCH_VERSION" ]; then + echo "- Bump \`gopher-orch\` native library from v$PREV_GOPHER_ORCH_VERSION to [v$GOPHER_ORCH_VERSION](https://github.com/GopherSecurity/gopher-orch/releases/tag/v$GOPHER_ORCH_VERSION)." + else + echo "- Pin \`gopher-orch\` native library to [v$GOPHER_ORCH_VERSION](https://github.com/GopherSecurity/gopher-orch/releases/tag/v$GOPHER_ORCH_VERSION)." + fi + echo "" + + if [ -n "$PY_COMMITS" ]; then + if [ -n "$PREV_TAG" ]; then + echo "#### SDK changes since $PREV_TAG" + else + echo "#### SDK changes" + fi + echo "" + printf '%s\n' "$PY_COMMITS" + echo "" + fi + + if [ -n "$GOPHER_ORCH_NOTES" ]; then + echo "#### gopher-orch v$GOPHER_ORCH_VERSION highlights" + echo "" + printf '%s\n' "$GOPHER_ORCH_NOTES" + fi +} > "$RELEASE_NOTES_FILE" + +# Splice the generated body in: replace everything between +# "## [Unreleased]" and the next "## [" with the new content. +CHANGELOG_TMP="${CHANGELOG_FILE}.gen" +awk -v notes_file="$RELEASE_NOTES_FILE" ' + BEGIN { + while ((getline line < notes_file) > 0) { + notes = notes (notes ? "\n" : "") line + } + close(notes_file) + } + /^## \[Unreleased\]/ { + print + print "" + print notes + print "" + skipping = 1 + next + } + /^## \[/ && skipping { skipping = 0 } + skipping { next } + { print } +' "$CHANGELOG_FILE" > "$CHANGELOG_TMP" +mv "$CHANGELOG_TMP" "$CHANGELOG_FILE" +rm -f "$RELEASE_NOTES_FILE" + +# Recompute UNRELEASED_CONTENT for the eventual commit message +UNRELEASED_CONTENT=$(awk ' + /^## \[Unreleased\]/ { capture = 1; next } + /^## \[/ && capture { capture = 0 } + capture { print } +' "$CHANGELOG_FILE" | sed -e '/^[[:space:]]*$/d') + +echo -e " ${GREEN}[Unreleased] section populated${NC}" +echo " Preview:" +printf '%s\n' "$UNRELEASED_CONTENT" | head -12 | sed 's/^/ /' # ----------------------------------------------------------------------------- # Step 5: Update version files From d4a957e4fad6b4b1886c25544c51c58c21ae0746 Mon Sep 17 00:00:00 2001 From: RahulHere Date: Thu, 18 Jun 2026 09:15:24 +0800 Subject: [PATCH 17/29] Switch examples/api/ to resolve against PyPI fix (#9) Move all seven examples/api/ wrappers from the local-source + local-build resolution model to the PyPI-based model already used by examples/pip/. Each wrapper now bootstraps its own venv, installs gopher-mcp-python plus the matching platform native package from PyPI, and runs the .py against the just-installed package -- not against the in-tree source. .gitignore - Add examples/api/test-project-*/ so the per-variant venv work dirs the wrappers create do not pollute the working tree. The existing test-project-api/ rule covered only the examples/pip/ workdir and not the new examples/api/ ones. --- .gitignore | 1 + examples/api/README.md | 221 +++++++++++++-------- examples/api/create_by_api_key.py | 5 +- examples/api/create_by_api_key_run.sh | 79 ++++++-- examples/api/create_by_gateway_id_run.sh | 80 ++++++-- examples/api/create_by_gateway_name_run.sh | 80 ++++++-- examples/api/create_by_json_run.sh | 79 ++++++-- examples/api/create_by_server_id_run.sh | 80 ++++++-- examples/api/create_by_server_name_run.sh | 80 ++++++-- examples/api/create_by_url_run.sh | 80 ++++++-- 10 files changed, 571 insertions(+), 214 deletions(-) diff --git a/.gitignore b/.gitignore index 00450467..7313472f 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ dist/ # Native library directories native/ test-project-api/ +examples/api/test-project-*/ # Python cache __pycache__/ diff --git a/examples/api/README.md b/examples/api/README.md index 81957635..397ced23 100644 --- a/examples/api/README.md +++ b/examples/api/README.md @@ -8,6 +8,14 @@ Each `.py` file mirrors its `.cc` and `.ts` counterparts one-to-one and exercises exactly one of the seven `create_by_*` factories the SDK exposes through `GopherAgent`. +All examples in this directory resolve their dependencies from the +**PyPI-published** [`gopher-mcp-python`](https://pypi.org/project/gopher-mcp-python/) +package and its matching platform-specific native package — they do +not use the in-tree `gopher_mcp_python/` source or the locally-built +`native/lib/` directory. To work against the in-tree source instead, +see the existing `examples/client_example_json*` pair at the +`examples/` root. + ## File-to-factory mapping | C++ reference | Python port | TypeScript port | `GopherAgent` factory | @@ -20,50 +28,66 @@ SDK exposes through `GopherAgent`. | `create_by_gateway_name.cc` | `create_by_gateway_name.py` | `create_by_gateway_name.ts` | `create_with_gateway_name` | | `create_by_url.cc` | `create_by_url.py` | `create_by_url.ts` | `create_with_url` | -Each `.py` file ships with a `*_run.sh` wrapper that sets the native -library path, exports `PYTHONPATH` so the in-repo package is -importable without `pip install`, and forwards positional arguments -as queries. +Each `.py` file ships with a `*_run.sh` wrapper that bootstraps a +fresh virtual environment, installs `gopher-mcp-python` plus the +matching platform native package from PyPI, and forwards positional +arguments to the example as queries. ## Quick start -1. Build the native library (one-time, repeats after a submodule bump): +1. Set the env vars your chosen example needs (see the matrix below). + At minimum every example needs `LLM_MODEL` and the LLM + provider's own credentials (`ANTHROPIC_API_KEY` for the default + `AnthropicProvider`): ```sh - cd /Users/james/Desktop/dev/gopher-mcp-python - ./build.sh + export LLM_MODEL= + export ANTHROPIC_API_KEY=... + export GOPHER_API_KEY=... # only if your variant needs it ``` - This builds `third_party/gopher-orch` and drops the resulting - `libgopher-orch.dylib` / `.so` / `.dll` into `native/lib/`. - -2. Install the Python package in editable mode so the examples can - import `gopher_mcp_python` without manual `PYTHONPATH` tweaking: +2. Run the wrapper. It will detect your platform, create + `examples/api/test-project-/` with a fresh venv inside, + `pip install gopher-mcp-python` plus the matching native package + from PyPI, then run the `.py`: ```sh - pip install -e . + ./examples/api/create_by_api_key_run.sh "What time is it in Tokyo?" ``` - The wrappers also export `PYTHONPATH` as a belt-and-braces measure, - so you can skip this step if you only want to run the wrappers - themselves. + Positional arguments to the wrapper become queries; with no + arguments each example runs a canned query so a first invocation + produces visible output. -3. Pick a factory and run the matching wrapper: +3. To pin a specific SDK version, set `SDK_VERSION` before invoking + a wrapper. Otherwise the latest published version is installed: ```sh - export GOPHER_API_KEY=... - export LLM_MODEL= - export ANTHROPIC_API_KEY=... - ./examples/api/create_by_api_key_run.sh "What time is it in Tokyo?" + SDK_VERSION=0.1.23 ./examples/api/create_by_server_id_run.sh ``` - Positional arguments to the wrapper become queries; with no - arguments each example runs a canned query so a first invocation - produces visible output. +The wrappers are idempotent: each run nukes its `test-project-*` +directory and rebuilds the venv from scratch so a stale install +cannot mask a problem. The `test-project-*` directories are +intentionally ignored by `.gitignore` at the repo root. + +## Manual run (no wrapper) -4. To target a specific MCP server, MCP gateway, or one-off URL, set - the corresponding routing env var and run the matching wrapper. - See the table below. +If you would rather drive the venv yourself, the `.py` files are +self-contained and run against any environment that has +`gopher-mcp-python` and the matching native package installed: + +```sh +python3 -m venv venv +source venv/bin/activate +pip install gopher-mcp-python gopher-mcp-python-native-darwin-arm64 +export LLM_MODEL= +export ANTHROPIC_API_KEY=... +python examples/api/create_by_api_key.py "What time is it in Tokyo?" +``` + +Substitute the right native package name for your platform; see the +list at [pypi.org/project/gopher-mcp-python](https://pypi.org/project/gopher-mcp-python/). ## Environment variables per example @@ -77,18 +101,21 @@ as queries. | `create_by_gateway_name` | `GOPHER_API_KEY`, `GOPHER_MCP_GATEWAY_NAME`, `LLM_MODEL` | `LLM_PROVIDER`, `DEBUG` | | `create_by_url` | `GOPHER_MCP_URL`, `LLM_MODEL` | `LLM_PROVIDER`, `DEBUG` | +The wrappers also recognise: + +- `SDK_VERSION` — pin `gopher-mcp-python` and the platform native + package to a specific PyPI version. Defaults to the latest. +- `ANTHROPIC_API_KEY` — required by the default `AnthropicProvider`; + the wrappers warn if unset but do not fail. + Notes: - `LLM_PROVIDER` defaults to `AnthropicProvider` in every example. -- `LLM_MODEL` has no default; each example refuses to start until the - variable is set rather than calling into the FFI with a placeholder. - This matches the env-var-required path the JS side picked so the - examples never surface a stale or fictional model identifier. -- The LLM provider's own credentials (`ANTHROPIC_API_KEY`, - `OPENAI_API_KEY`, `GOOGLE_API_KEY`, etc.) are required by the - backing provider rather than by the SDK directly; the wrappers - warn if `ANTHROPIC_API_KEY` is unset since `AnthropicProvider` is - the default. +- `LLM_MODEL` has no default; each example refuses to start until + the variable is set rather than calling into the FFI with a + placeholder. This matches the env-var-required path the JS side + picked so the examples never surface a stale or fictional model + identifier. ## Picking the right factory @@ -103,14 +130,20 @@ Notes: | `create_with_url` | One MCP server reachable at a known URL | None (synthesised locally to an `http_sse` entry) | The table mirrors the C++ canonical reference at -`gopher-orch/docs/Agent.md` ("Simple creation factories" section) so -the Python-side documentation stays aligned with the upstream C++ -docs and the TypeScript port. +`gopher-orch/docs/Agent.md` ("Simple creation factories" section) +so the Python-side documentation stays aligned with the upstream +C++ docs and the TypeScript port. + +The five routing factories +(`create_with_server_id` / `_server_name` / `_gateway_id` / +`_gateway_name` / `_url`) require `gopher-mcp-python` ≥ 0.1.23 on +PyPI. Earlier versions only expose `create_with_api_key` and +`create_with_server_config`. ## How the examples find the SDK -Each `.py` file imports `GopherAgent` from the installed -`gopher_mcp_python` package: +Each `.py` file imports `GopherAgent` from the +`gopher_mcp_python` package installed by the wrapper: ```python from gopher_mcp_python import GopherAgent @@ -118,47 +151,74 @@ from gopher_mcp_python import GopherAgent Resolution flow: -- During development: `pip install -e .` from the repo root makes - the in-tree `gopher_mcp_python/` directory importable. The - wrappers also set `PYTHONPATH="$PROJECT_DIR"` so the import works - without a prior `pip install -e .`. -- Downstream: a consumer who runs `pip install gopher-mcp-python` - and copies one of these examples into their own project does - not need any import-path edit — the same line works against the - PyPI-installed package as-is. - -This differs from the JS sibling, which uses `import { GopherAgent } -from '../../src'` to keep the example tied to the in-repo TypeScript -sources. Python uses package-install semantics so the same `from -gopher_mcp_python import GopherAgent` line works in both the in-repo -and the installed-from-PyPI cases without modification. +- Through a wrapper (`create_by_*_run.sh`): the wrapper creates a + fresh venv in `examples/api/test-project-/`, + `pip install`-s `gopher-mcp-python` plus the matching platform + native package from PyPI, then runs the example. The import + resolves against the just-installed PyPI package, never against + the in-tree `gopher_mcp_python/` source. +- Manual run: the example resolves against whatever + `gopher_mcp_python` is on the active Python's `sys.path` — + whatever you `pip install`-ed into your own venv. + +A downstream consumer copying any of these examples into their own +project does not need to edit the import path — the same `from +gopher_mcp_python import GopherAgent` line works as long as the +package is installed. ## How the wrappers find the native library -Each `*_run.sh` script: - -1. Resolves `PROJECT_DIR` to the `gopher-mcp-python` repo root (two - `dirname` calls because the scripts sit at `examples/api/` - rather than `examples/`). -2. Exits early with a pointer at `./build.sh` if - `$PROJECT_DIR/native/lib` is missing. -3. Runs a pre-flight `python3 -c "import gopher_mcp_python"` and - prints a `pip install -e .` hint on failure. The `.ts` side did - not need this step because `npx tsx` + relative import work out - of the box; Python requires the package on `sys.path`. -4. Exports `DYLD_LIBRARY_PATH` (macOS) and `LD_LIBRARY_PATH` - (Linux) to `$PROJECT_DIR/native/lib` so the ctypes loader picks - up the freshly built dylib from `build.sh` rather than the - pip-installed platform package or any system-installed copy. -5. Exports `PYTHONPATH="$PROJECT_DIR"` so the in-tree package is - importable even when `pip install -e .` has not been run. -6. Invokes `python3 examples/api/.py "$@"` so positional - arguments pass straight through as queries. - -If you need to point at a different library location entirely, set -`GOPHER_MCP_PYTHON_LIBRARY_PATH` before invoking the wrapper. That -env var is checked first by `gopher_mcp_python/ffi/library.py` and -bypasses the `native/lib/` and platform-package resolution steps. +The native `libgopher-orch.dylib` / `.so` / `.dll` is loaded by the +`ctypes` layer inside `gopher_mcp_python.ffi.library`. With the +PyPI-based wrappers, resolution happens entirely inside the venv: + +1. The wrapper installs `gopher-mcp-python-native--` + alongside the main package; that package ships the native binary + under its own `lib/` directory. +2. `gopher_mcp_python/ffi/library.py` walks `sys.path` looking for + the matching `gopher_mcp_python_native_*` package and uses its + `get_lib_path()` to locate the dylib. +3. `DYLD_LIBRARY_PATH` / `LD_LIBRARY_PATH` are **not** set by the + wrappers — the loader finds the platform package via Python + import semantics rather than a search path. + +If you need to point at a different library location entirely +(for example a locally-built `libgopher-orch.dylib` for testing a +patch), set `GOPHER_MCP_PYTHON_LIBRARY_PATH` before invoking the +wrapper. That env var is checked first by +`gopher_mcp_python/ffi/library.py` and bypasses the platform-package +resolution step. + +## Troubleshooting + +### "Failed to load gopher-mcp-python library" + +The matching platform native package was not installed. The +wrappers compute and install it automatically; if you are running +the `.py` manually, install both: + +```sh +pip install gopher-mcp-python gopher-mcp-python-native-- +``` + +### Permission errors on macOS + +Quarantine flags on a freshly-downloaded dylib can block load: + +```sh +xattr -d com.apple.quarantine "$(python -c 'import gopher_mcp_python_native_darwin_arm64 as n; print(n.get_library_file())')" +``` + +### Routing factory raises `AgentError` against an older PyPI release + +The five routing factories landed in `gopher-mcp-python` 0.1.23. If +the wrapper installs an older version, the higher-level factory +raises `AgentError` because the underlying C symbol is missing. Pin +to a recent release: + +```sh +SDK_VERSION=0.1.23 ./examples/api/create_by_server_id_run.sh +``` ## Cross-reference @@ -177,3 +237,6 @@ bypasses the `native/lib/` and platform-package resolution steps. methods). - Contract tests: `tests/test_agent_create_by.py`. +- Sibling pip-style wrappers (older, two-variant superset): + `examples/pip/` — same venv-bootstrap pattern these wrappers + inherit. diff --git a/examples/api/create_by_api_key.py b/examples/api/create_by_api_key.py index a2eb4bff..5d7c5b42 100644 --- a/examples/api/create_by_api_key.py +++ b/examples/api/create_by_api_key.py @@ -8,8 +8,9 @@ Uses a Gopher API key to fetch the caller's full MCP server inventory via GET /v1/mcp-servers; the agent gets every server the api key owns with no extra routing. Smallest of the seven create_by_* examples and -a good first sanity check that the toolchain (pip install -e ., the -right native lib, env vars) is wired correctly. +a good first sanity check that the toolchain (pip install +gopher-mcp-python, the matching platform native package, env vars) +is wired correctly. Provider defaults to AnthropicProvider; the model is taken from LLM_MODEL. Override either via env or by editing the constants in diff --git a/examples/api/create_by_api_key_run.sh b/examples/api/create_by_api_key_run.sh index dd661ac9..3dfd1a97 100755 --- a/examples/api/create_by_api_key_run.sh +++ b/examples/api/create_by_api_key_run.sh @@ -1,34 +1,52 @@ #!/bin/bash -# Run the Python SDK example for GopherAgent.create_with_api_key. +# Run the Python SDK example for GopherAgent.create_with_api_key against +# the PyPI-published gopher-mcp-python package. Bootstraps a fresh venv, +# installs the SDK plus the matching platform native package from PyPI, +# then runs the example. +# +# Set SDK_VERSION to pin to a specific release (e.g. SDK_VERSION=0.1.21); +# otherwise the latest published version is installed. set -e RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' +CYAN='\033[0;36m' NC='\033[0m' SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_DIR="$(dirname "$(dirname "$SCRIPT_DIR")")" +WORK_DIR="$SCRIPT_DIR/test-project-create-by-api-key" +SDK_VERSION="${SDK_VERSION:-}" + +detect_platform() { + local os arch + os=$(uname -s | tr '[:upper:]' '[:lower:]') + arch=$(uname -m) + case "$os" in + darwin) PLATFORM="darwin" ;; + linux) PLATFORM="linux" ;; + mingw*|msys*|cygwin*) PLATFORM="win32" ;; + *) echo -e "${RED}Unsupported OS: $os${NC}"; exit 1 ;; + esac + case "$arch" in + x86_64|amd64) ARCH="x64" ;; + arm64|aarch64) ARCH="arm64" ;; + *) echo -e "${RED}Unsupported architecture: $arch${NC}"; exit 1 ;; + esac + NATIVE_PACKAGE="gopher-mcp-python-native-${PLATFORM}-${ARCH}" + echo -e "${CYAN}Detected platform: ${PLATFORM}-${ARCH}${NC}" + echo -e "${CYAN}Native package: ${NATIVE_PACKAGE}${NC}" +} + +detect_platform echo -e "${GREEN}=========================================${NC}" echo -e "${GREEN}GopherAgent.create_with_api_key example${NC}" echo -e "${GREEN}=========================================${NC}" echo "" -if [ ! -d "$PROJECT_DIR/native/lib" ]; then - echo -e "${RED}Error: Native library not found at $PROJECT_DIR/native/lib${NC}" - echo -e "${YELLOW}Please run ./build.sh first${NC}" - exit 1 -fi - -if ! PYTHONPATH="$PROJECT_DIR" python3 -c "import gopher_mcp_python" 2>/dev/null; then - echo -e "${RED}Error: gopher_mcp_python is not importable${NC}" - echo -e "${YELLOW}Run: pip install -e . (from $PROJECT_DIR)${NC}" - exit 1 -fi - if [ -z "$GOPHER_API_KEY" ]; then echo -e "${YELLOW}Warning: GOPHER_API_KEY environment variable is not set${NC}" echo -e "${YELLOW}Set it with: export GOPHER_API_KEY=your_api_key${NC}" @@ -47,12 +65,35 @@ if [ -z "$ANTHROPIC_API_KEY" ]; then echo "" fi -cd "$PROJECT_DIR" +echo -e "${YELLOW}Setting up test project at $WORK_DIR...${NC}" +rm -rf "$WORK_DIR" +mkdir -p "$WORK_DIR" +cd "$WORK_DIR" + +echo -e "${YELLOW}Creating virtual environment...${NC}" +python3 -m venv venv +# shellcheck disable=SC1091 +source venv/bin/activate -PYTHONPATH="$PROJECT_DIR" \ -DYLD_LIBRARY_PATH="$PROJECT_DIR/native/lib" \ -LD_LIBRARY_PATH="$PROJECT_DIR/native/lib" \ -python3 examples/api/create_by_api_key.py "$@" +echo -e "${YELLOW}Installing gopher-mcp-python from PyPI...${NC}" +if [ -n "$SDK_VERSION" ]; then + echo -e "${CYAN}Installing version: $SDK_VERSION${NC}" + pip install --quiet "gopher-mcp-python==$SDK_VERSION" \ + "${NATIVE_PACKAGE}==$SDK_VERSION" +else + echo -e "${CYAN}Installing latest published version${NC}" + pip install --quiet gopher-mcp-python "$NATIVE_PACKAGE" +fi + +echo -e "${CYAN}Installed packages:${NC}" +pip list | grep -i gopher || true + +cp "$SCRIPT_DIR/create_by_api_key.py" . + +echo "" +echo -e "${YELLOW}Running example...${NC}" +echo "" +python create_by_api_key.py "$@" echo "" echo -e "${GREEN}Example completed${NC}" diff --git a/examples/api/create_by_gateway_id_run.sh b/examples/api/create_by_gateway_id_run.sh index 963bfb31..63ab0f00 100755 --- a/examples/api/create_by_gateway_id_run.sh +++ b/examples/api/create_by_gateway_id_run.sh @@ -1,34 +1,53 @@ #!/bin/bash -# Run the Python SDK example for GopherAgent.create_with_gateway_id. +# Run the Python SDK example for GopherAgent.create_with_gateway_id +# against the PyPI-published gopher-mcp-python package. Bootstraps a +# fresh venv, installs the SDK plus the matching platform native package +# from PyPI, then runs the example. +# +# Set SDK_VERSION to pin to a specific release (e.g. SDK_VERSION=0.1.23); +# otherwise the latest published version is installed. The routing +# factories require gopher-mcp-python >= 0.1.23. set -e RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' +CYAN='\033[0;36m' NC='\033[0m' SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_DIR="$(dirname "$(dirname "$SCRIPT_DIR")")" +WORK_DIR="$SCRIPT_DIR/test-project-create-by-gateway-id" +SDK_VERSION="${SDK_VERSION:-}" + +detect_platform() { + local os arch + os=$(uname -s | tr '[:upper:]' '[:lower:]') + arch=$(uname -m) + case "$os" in + darwin) PLATFORM="darwin" ;; + linux) PLATFORM="linux" ;; + mingw*|msys*|cygwin*) PLATFORM="win32" ;; + *) echo -e "${RED}Unsupported OS: $os${NC}"; exit 1 ;; + esac + case "$arch" in + x86_64|amd64) ARCH="x64" ;; + arm64|aarch64) ARCH="arm64" ;; + *) echo -e "${RED}Unsupported architecture: $arch${NC}"; exit 1 ;; + esac + NATIVE_PACKAGE="gopher-mcp-python-native-${PLATFORM}-${ARCH}" + echo -e "${CYAN}Detected platform: ${PLATFORM}-${ARCH}${NC}" + echo -e "${CYAN}Native package: ${NATIVE_PACKAGE}${NC}" +} + +detect_platform echo -e "${GREEN}===========================================${NC}" echo -e "${GREEN}GopherAgent.create_with_gateway_id example${NC}" echo -e "${GREEN}===========================================${NC}" echo "" -if [ ! -d "$PROJECT_DIR/native/lib" ]; then - echo -e "${RED}Error: Native library not found at $PROJECT_DIR/native/lib${NC}" - echo -e "${YELLOW}Please run ./build.sh first${NC}" - exit 1 -fi - -if ! PYTHONPATH="$PROJECT_DIR" python3 -c "import gopher_mcp_python" 2>/dev/null; then - echo -e "${RED}Error: gopher_mcp_python is not importable${NC}" - echo -e "${YELLOW}Run: pip install -e . (from $PROJECT_DIR)${NC}" - exit 1 -fi - if [ -z "$GOPHER_API_KEY" ]; then echo -e "${YELLOW}Warning: GOPHER_API_KEY environment variable is not set${NC}" echo -e "${YELLOW}Set it with: export GOPHER_API_KEY=your_api_key${NC}" @@ -53,12 +72,35 @@ if [ -z "$ANTHROPIC_API_KEY" ]; then echo "" fi -cd "$PROJECT_DIR" +echo -e "${YELLOW}Setting up test project at $WORK_DIR...${NC}" +rm -rf "$WORK_DIR" +mkdir -p "$WORK_DIR" +cd "$WORK_DIR" + +echo -e "${YELLOW}Creating virtual environment...${NC}" +python3 -m venv venv +# shellcheck disable=SC1091 +source venv/bin/activate -PYTHONPATH="$PROJECT_DIR" \ -DYLD_LIBRARY_PATH="$PROJECT_DIR/native/lib" \ -LD_LIBRARY_PATH="$PROJECT_DIR/native/lib" \ -python3 examples/api/create_by_gateway_id.py "$@" +echo -e "${YELLOW}Installing gopher-mcp-python from PyPI...${NC}" +if [ -n "$SDK_VERSION" ]; then + echo -e "${CYAN}Installing version: $SDK_VERSION${NC}" + pip install --quiet "gopher-mcp-python==$SDK_VERSION" \ + "${NATIVE_PACKAGE}==$SDK_VERSION" +else + echo -e "${CYAN}Installing latest published version${NC}" + pip install --quiet gopher-mcp-python "$NATIVE_PACKAGE" +fi + +echo -e "${CYAN}Installed packages:${NC}" +pip list | grep -i gopher || true + +cp "$SCRIPT_DIR/create_by_gateway_id.py" . + +echo "" +echo -e "${YELLOW}Running example...${NC}" +echo "" +python create_by_gateway_id.py "$@" echo "" echo -e "${GREEN}Example completed${NC}" diff --git a/examples/api/create_by_gateway_name_run.sh b/examples/api/create_by_gateway_name_run.sh index 8185578e..724e05be 100755 --- a/examples/api/create_by_gateway_name_run.sh +++ b/examples/api/create_by_gateway_name_run.sh @@ -1,34 +1,53 @@ #!/bin/bash -# Run the Python SDK example for GopherAgent.create_with_gateway_name. +# Run the Python SDK example for GopherAgent.create_with_gateway_name +# against the PyPI-published gopher-mcp-python package. Bootstraps a +# fresh venv, installs the SDK plus the matching platform native package +# from PyPI, then runs the example. +# +# Set SDK_VERSION to pin to a specific release (e.g. SDK_VERSION=0.1.23); +# otherwise the latest published version is installed. The routing +# factories require gopher-mcp-python >= 0.1.23. set -e RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' +CYAN='\033[0;36m' NC='\033[0m' SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_DIR="$(dirname "$(dirname "$SCRIPT_DIR")")" +WORK_DIR="$SCRIPT_DIR/test-project-create-by-gateway-name" +SDK_VERSION="${SDK_VERSION:-}" + +detect_platform() { + local os arch + os=$(uname -s | tr '[:upper:]' '[:lower:]') + arch=$(uname -m) + case "$os" in + darwin) PLATFORM="darwin" ;; + linux) PLATFORM="linux" ;; + mingw*|msys*|cygwin*) PLATFORM="win32" ;; + *) echo -e "${RED}Unsupported OS: $os${NC}"; exit 1 ;; + esac + case "$arch" in + x86_64|amd64) ARCH="x64" ;; + arm64|aarch64) ARCH="arm64" ;; + *) echo -e "${RED}Unsupported architecture: $arch${NC}"; exit 1 ;; + esac + NATIVE_PACKAGE="gopher-mcp-python-native-${PLATFORM}-${ARCH}" + echo -e "${CYAN}Detected platform: ${PLATFORM}-${ARCH}${NC}" + echo -e "${CYAN}Native package: ${NATIVE_PACKAGE}${NC}" +} + +detect_platform echo -e "${GREEN}=============================================${NC}" echo -e "${GREEN}GopherAgent.create_with_gateway_name example${NC}" echo -e "${GREEN}=============================================${NC}" echo "" -if [ ! -d "$PROJECT_DIR/native/lib" ]; then - echo -e "${RED}Error: Native library not found at $PROJECT_DIR/native/lib${NC}" - echo -e "${YELLOW}Please run ./build.sh first${NC}" - exit 1 -fi - -if ! PYTHONPATH="$PROJECT_DIR" python3 -c "import gopher_mcp_python" 2>/dev/null; then - echo -e "${RED}Error: gopher_mcp_python is not importable${NC}" - echo -e "${YELLOW}Run: pip install -e . (from $PROJECT_DIR)${NC}" - exit 1 -fi - if [ -z "$GOPHER_API_KEY" ]; then echo -e "${YELLOW}Warning: GOPHER_API_KEY environment variable is not set${NC}" echo -e "${YELLOW}Set it with: export GOPHER_API_KEY=your_api_key${NC}" @@ -53,12 +72,35 @@ if [ -z "$ANTHROPIC_API_KEY" ]; then echo "" fi -cd "$PROJECT_DIR" +echo -e "${YELLOW}Setting up test project at $WORK_DIR...${NC}" +rm -rf "$WORK_DIR" +mkdir -p "$WORK_DIR" +cd "$WORK_DIR" + +echo -e "${YELLOW}Creating virtual environment...${NC}" +python3 -m venv venv +# shellcheck disable=SC1091 +source venv/bin/activate -PYTHONPATH="$PROJECT_DIR" \ -DYLD_LIBRARY_PATH="$PROJECT_DIR/native/lib" \ -LD_LIBRARY_PATH="$PROJECT_DIR/native/lib" \ -python3 examples/api/create_by_gateway_name.py "$@" +echo -e "${YELLOW}Installing gopher-mcp-python from PyPI...${NC}" +if [ -n "$SDK_VERSION" ]; then + echo -e "${CYAN}Installing version: $SDK_VERSION${NC}" + pip install --quiet "gopher-mcp-python==$SDK_VERSION" \ + "${NATIVE_PACKAGE}==$SDK_VERSION" +else + echo -e "${CYAN}Installing latest published version${NC}" + pip install --quiet gopher-mcp-python "$NATIVE_PACKAGE" +fi + +echo -e "${CYAN}Installed packages:${NC}" +pip list | grep -i gopher || true + +cp "$SCRIPT_DIR/create_by_gateway_name.py" . + +echo "" +echo -e "${YELLOW}Running example...${NC}" +echo "" +python create_by_gateway_name.py "$@" echo "" echo -e "${GREEN}Example completed${NC}" diff --git a/examples/api/create_by_json_run.sh b/examples/api/create_by_json_run.sh index 8cc158e3..1ee37e85 100755 --- a/examples/api/create_by_json_run.sh +++ b/examples/api/create_by_json_run.sh @@ -1,34 +1,52 @@ #!/bin/bash -# Run the Python SDK example for GopherAgent.create_with_server_config. +# Run the Python SDK example for GopherAgent.create_with_server_config +# against the PyPI-published gopher-mcp-python package. Bootstraps a +# fresh venv, installs the SDK plus the matching platform native package +# from PyPI, then runs the example. +# +# Set SDK_VERSION to pin to a specific release (e.g. SDK_VERSION=0.1.21); +# otherwise the latest published version is installed. set -e RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' +CYAN='\033[0;36m' NC='\033[0m' SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_DIR="$(dirname "$(dirname "$SCRIPT_DIR")")" +WORK_DIR="$SCRIPT_DIR/test-project-create-by-json" +SDK_VERSION="${SDK_VERSION:-}" + +detect_platform() { + local os arch + os=$(uname -s | tr '[:upper:]' '[:lower:]') + arch=$(uname -m) + case "$os" in + darwin) PLATFORM="darwin" ;; + linux) PLATFORM="linux" ;; + mingw*|msys*|cygwin*) PLATFORM="win32" ;; + *) echo -e "${RED}Unsupported OS: $os${NC}"; exit 1 ;; + esac + case "$arch" in + x86_64|amd64) ARCH="x64" ;; + arm64|aarch64) ARCH="arm64" ;; + *) echo -e "${RED}Unsupported architecture: $arch${NC}"; exit 1 ;; + esac + NATIVE_PACKAGE="gopher-mcp-python-native-${PLATFORM}-${ARCH}" + echo -e "${CYAN}Detected platform: ${PLATFORM}-${ARCH}${NC}" + echo -e "${CYAN}Native package: ${NATIVE_PACKAGE}${NC}" +} + +detect_platform echo -e "${GREEN}===============================================${NC}" echo -e "${GREEN}GopherAgent.create_with_server_config example${NC}" echo -e "${GREEN}===============================================${NC}" echo "" -if [ ! -d "$PROJECT_DIR/native/lib" ]; then - echo -e "${RED}Error: Native library not found at $PROJECT_DIR/native/lib${NC}" - echo -e "${YELLOW}Please run ./build.sh first${NC}" - exit 1 -fi - -if ! PYTHONPATH="$PROJECT_DIR" python3 -c "import gopher_mcp_python" 2>/dev/null; then - echo -e "${RED}Error: gopher_mcp_python is not importable${NC}" - echo -e "${YELLOW}Run: pip install -e . (from $PROJECT_DIR)${NC}" - exit 1 -fi - if [ -z "$LLM_MODEL" ]; then echo -e "${YELLOW}Warning: LLM_MODEL environment variable is not set${NC}" echo -e "${YELLOW}Set it with: export LLM_MODEL=${NC}" @@ -41,12 +59,35 @@ if [ -z "$ANTHROPIC_API_KEY" ]; then echo "" fi -cd "$PROJECT_DIR" +echo -e "${YELLOW}Setting up test project at $WORK_DIR...${NC}" +rm -rf "$WORK_DIR" +mkdir -p "$WORK_DIR" +cd "$WORK_DIR" + +echo -e "${YELLOW}Creating virtual environment...${NC}" +python3 -m venv venv +# shellcheck disable=SC1091 +source venv/bin/activate -PYTHONPATH="$PROJECT_DIR" \ -DYLD_LIBRARY_PATH="$PROJECT_DIR/native/lib" \ -LD_LIBRARY_PATH="$PROJECT_DIR/native/lib" \ -python3 examples/api/create_by_json.py "$@" +echo -e "${YELLOW}Installing gopher-mcp-python from PyPI...${NC}" +if [ -n "$SDK_VERSION" ]; then + echo -e "${CYAN}Installing version: $SDK_VERSION${NC}" + pip install --quiet "gopher-mcp-python==$SDK_VERSION" \ + "${NATIVE_PACKAGE}==$SDK_VERSION" +else + echo -e "${CYAN}Installing latest published version${NC}" + pip install --quiet gopher-mcp-python "$NATIVE_PACKAGE" +fi + +echo -e "${CYAN}Installed packages:${NC}" +pip list | grep -i gopher || true + +cp "$SCRIPT_DIR/create_by_json.py" . + +echo "" +echo -e "${YELLOW}Running example...${NC}" +echo "" +python create_by_json.py "$@" echo "" echo -e "${GREEN}Example completed${NC}" diff --git a/examples/api/create_by_server_id_run.sh b/examples/api/create_by_server_id_run.sh index be843a1f..dfa27fe3 100755 --- a/examples/api/create_by_server_id_run.sh +++ b/examples/api/create_by_server_id_run.sh @@ -1,34 +1,53 @@ #!/bin/bash -# Run the Python SDK example for GopherAgent.create_with_server_id. +# Run the Python SDK example for GopherAgent.create_with_server_id +# against the PyPI-published gopher-mcp-python package. Bootstraps a +# fresh venv, installs the SDK plus the matching platform native package +# from PyPI, then runs the example. +# +# Set SDK_VERSION to pin to a specific release (e.g. SDK_VERSION=0.1.23); +# otherwise the latest published version is installed. The routing +# factories require gopher-mcp-python >= 0.1.23. set -e RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' +CYAN='\033[0;36m' NC='\033[0m' SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_DIR="$(dirname "$(dirname "$SCRIPT_DIR")")" +WORK_DIR="$SCRIPT_DIR/test-project-create-by-server-id" +SDK_VERSION="${SDK_VERSION:-}" + +detect_platform() { + local os arch + os=$(uname -s | tr '[:upper:]' '[:lower:]') + arch=$(uname -m) + case "$os" in + darwin) PLATFORM="darwin" ;; + linux) PLATFORM="linux" ;; + mingw*|msys*|cygwin*) PLATFORM="win32" ;; + *) echo -e "${RED}Unsupported OS: $os${NC}"; exit 1 ;; + esac + case "$arch" in + x86_64|amd64) ARCH="x64" ;; + arm64|aarch64) ARCH="arm64" ;; + *) echo -e "${RED}Unsupported architecture: $arch${NC}"; exit 1 ;; + esac + NATIVE_PACKAGE="gopher-mcp-python-native-${PLATFORM}-${ARCH}" + echo -e "${CYAN}Detected platform: ${PLATFORM}-${ARCH}${NC}" + echo -e "${CYAN}Native package: ${NATIVE_PACKAGE}${NC}" +} + +detect_platform echo -e "${GREEN}==========================================${NC}" echo -e "${GREEN}GopherAgent.create_with_server_id example${NC}" echo -e "${GREEN}==========================================${NC}" echo "" -if [ ! -d "$PROJECT_DIR/native/lib" ]; then - echo -e "${RED}Error: Native library not found at $PROJECT_DIR/native/lib${NC}" - echo -e "${YELLOW}Please run ./build.sh first${NC}" - exit 1 -fi - -if ! PYTHONPATH="$PROJECT_DIR" python3 -c "import gopher_mcp_python" 2>/dev/null; then - echo -e "${RED}Error: gopher_mcp_python is not importable${NC}" - echo -e "${YELLOW}Run: pip install -e . (from $PROJECT_DIR)${NC}" - exit 1 -fi - if [ -z "$GOPHER_API_KEY" ]; then echo -e "${YELLOW}Warning: GOPHER_API_KEY environment variable is not set${NC}" echo -e "${YELLOW}Set it with: export GOPHER_API_KEY=your_api_key${NC}" @@ -53,12 +72,35 @@ if [ -z "$ANTHROPIC_API_KEY" ]; then echo "" fi -cd "$PROJECT_DIR" +echo -e "${YELLOW}Setting up test project at $WORK_DIR...${NC}" +rm -rf "$WORK_DIR" +mkdir -p "$WORK_DIR" +cd "$WORK_DIR" + +echo -e "${YELLOW}Creating virtual environment...${NC}" +python3 -m venv venv +# shellcheck disable=SC1091 +source venv/bin/activate -PYTHONPATH="$PROJECT_DIR" \ -DYLD_LIBRARY_PATH="$PROJECT_DIR/native/lib" \ -LD_LIBRARY_PATH="$PROJECT_DIR/native/lib" \ -python3 examples/api/create_by_server_id.py "$@" +echo -e "${YELLOW}Installing gopher-mcp-python from PyPI...${NC}" +if [ -n "$SDK_VERSION" ]; then + echo -e "${CYAN}Installing version: $SDK_VERSION${NC}" + pip install --quiet "gopher-mcp-python==$SDK_VERSION" \ + "${NATIVE_PACKAGE}==$SDK_VERSION" +else + echo -e "${CYAN}Installing latest published version${NC}" + pip install --quiet gopher-mcp-python "$NATIVE_PACKAGE" +fi + +echo -e "${CYAN}Installed packages:${NC}" +pip list | grep -i gopher || true + +cp "$SCRIPT_DIR/create_by_server_id.py" . + +echo "" +echo -e "${YELLOW}Running example...${NC}" +echo "" +python create_by_server_id.py "$@" echo "" echo -e "${GREEN}Example completed${NC}" diff --git a/examples/api/create_by_server_name_run.sh b/examples/api/create_by_server_name_run.sh index 6ad59701..75785f29 100755 --- a/examples/api/create_by_server_name_run.sh +++ b/examples/api/create_by_server_name_run.sh @@ -1,34 +1,53 @@ #!/bin/bash -# Run the Python SDK example for GopherAgent.create_with_server_name. +# Run the Python SDK example for GopherAgent.create_with_server_name +# against the PyPI-published gopher-mcp-python package. Bootstraps a +# fresh venv, installs the SDK plus the matching platform native package +# from PyPI, then runs the example. +# +# Set SDK_VERSION to pin to a specific release (e.g. SDK_VERSION=0.1.23); +# otherwise the latest published version is installed. The routing +# factories require gopher-mcp-python >= 0.1.23. set -e RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' +CYAN='\033[0;36m' NC='\033[0m' SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_DIR="$(dirname "$(dirname "$SCRIPT_DIR")")" +WORK_DIR="$SCRIPT_DIR/test-project-create-by-server-name" +SDK_VERSION="${SDK_VERSION:-}" + +detect_platform() { + local os arch + os=$(uname -s | tr '[:upper:]' '[:lower:]') + arch=$(uname -m) + case "$os" in + darwin) PLATFORM="darwin" ;; + linux) PLATFORM="linux" ;; + mingw*|msys*|cygwin*) PLATFORM="win32" ;; + *) echo -e "${RED}Unsupported OS: $os${NC}"; exit 1 ;; + esac + case "$arch" in + x86_64|amd64) ARCH="x64" ;; + arm64|aarch64) ARCH="arm64" ;; + *) echo -e "${RED}Unsupported architecture: $arch${NC}"; exit 1 ;; + esac + NATIVE_PACKAGE="gopher-mcp-python-native-${PLATFORM}-${ARCH}" + echo -e "${CYAN}Detected platform: ${PLATFORM}-${ARCH}${NC}" + echo -e "${CYAN}Native package: ${NATIVE_PACKAGE}${NC}" +} + +detect_platform echo -e "${GREEN}============================================${NC}" echo -e "${GREEN}GopherAgent.create_with_server_name example${NC}" echo -e "${GREEN}============================================${NC}" echo "" -if [ ! -d "$PROJECT_DIR/native/lib" ]; then - echo -e "${RED}Error: Native library not found at $PROJECT_DIR/native/lib${NC}" - echo -e "${YELLOW}Please run ./build.sh first${NC}" - exit 1 -fi - -if ! PYTHONPATH="$PROJECT_DIR" python3 -c "import gopher_mcp_python" 2>/dev/null; then - echo -e "${RED}Error: gopher_mcp_python is not importable${NC}" - echo -e "${YELLOW}Run: pip install -e . (from $PROJECT_DIR)${NC}" - exit 1 -fi - if [ -z "$GOPHER_API_KEY" ]; then echo -e "${YELLOW}Warning: GOPHER_API_KEY environment variable is not set${NC}" echo -e "${YELLOW}Set it with: export GOPHER_API_KEY=your_api_key${NC}" @@ -53,12 +72,35 @@ if [ -z "$ANTHROPIC_API_KEY" ]; then echo "" fi -cd "$PROJECT_DIR" +echo -e "${YELLOW}Setting up test project at $WORK_DIR...${NC}" +rm -rf "$WORK_DIR" +mkdir -p "$WORK_DIR" +cd "$WORK_DIR" + +echo -e "${YELLOW}Creating virtual environment...${NC}" +python3 -m venv venv +# shellcheck disable=SC1091 +source venv/bin/activate -PYTHONPATH="$PROJECT_DIR" \ -DYLD_LIBRARY_PATH="$PROJECT_DIR/native/lib" \ -LD_LIBRARY_PATH="$PROJECT_DIR/native/lib" \ -python3 examples/api/create_by_server_name.py "$@" +echo -e "${YELLOW}Installing gopher-mcp-python from PyPI...${NC}" +if [ -n "$SDK_VERSION" ]; then + echo -e "${CYAN}Installing version: $SDK_VERSION${NC}" + pip install --quiet "gopher-mcp-python==$SDK_VERSION" \ + "${NATIVE_PACKAGE}==$SDK_VERSION" +else + echo -e "${CYAN}Installing latest published version${NC}" + pip install --quiet gopher-mcp-python "$NATIVE_PACKAGE" +fi + +echo -e "${CYAN}Installed packages:${NC}" +pip list | grep -i gopher || true + +cp "$SCRIPT_DIR/create_by_server_name.py" . + +echo "" +echo -e "${YELLOW}Running example...${NC}" +echo "" +python create_by_server_name.py "$@" echo "" echo -e "${GREEN}Example completed${NC}" diff --git a/examples/api/create_by_url_run.sh b/examples/api/create_by_url_run.sh index b3ba0e54..4bf33cd6 100755 --- a/examples/api/create_by_url_run.sh +++ b/examples/api/create_by_url_run.sh @@ -1,34 +1,53 @@ #!/bin/bash -# Run the Python SDK example for GopherAgent.create_with_url. +# Run the Python SDK example for GopherAgent.create_with_url against the +# PyPI-published gopher-mcp-python package. Bootstraps a fresh venv, +# installs the SDK plus the matching platform native package from PyPI, +# then runs the example. +# +# Set SDK_VERSION to pin to a specific release (e.g. SDK_VERSION=0.1.23); +# otherwise the latest published version is installed. create_with_url +# requires gopher-mcp-python >= 0.1.23. set -e RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' +CYAN='\033[0;36m' NC='\033[0m' SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_DIR="$(dirname "$(dirname "$SCRIPT_DIR")")" +WORK_DIR="$SCRIPT_DIR/test-project-create-by-url" +SDK_VERSION="${SDK_VERSION:-}" + +detect_platform() { + local os arch + os=$(uname -s | tr '[:upper:]' '[:lower:]') + arch=$(uname -m) + case "$os" in + darwin) PLATFORM="darwin" ;; + linux) PLATFORM="linux" ;; + mingw*|msys*|cygwin*) PLATFORM="win32" ;; + *) echo -e "${RED}Unsupported OS: $os${NC}"; exit 1 ;; + esac + case "$arch" in + x86_64|amd64) ARCH="x64" ;; + arm64|aarch64) ARCH="arm64" ;; + *) echo -e "${RED}Unsupported architecture: $arch${NC}"; exit 1 ;; + esac + NATIVE_PACKAGE="gopher-mcp-python-native-${PLATFORM}-${ARCH}" + echo -e "${CYAN}Detected platform: ${PLATFORM}-${ARCH}${NC}" + echo -e "${CYAN}Native package: ${NATIVE_PACKAGE}${NC}" +} + +detect_platform echo -e "${GREEN}=====================================${NC}" echo -e "${GREEN}GopherAgent.create_with_url example${NC}" echo -e "${GREEN}=====================================${NC}" echo "" -if [ ! -d "$PROJECT_DIR/native/lib" ]; then - echo -e "${RED}Error: Native library not found at $PROJECT_DIR/native/lib${NC}" - echo -e "${YELLOW}Please run ./build.sh first${NC}" - exit 1 -fi - -if ! PYTHONPATH="$PROJECT_DIR" python3 -c "import gopher_mcp_python" 2>/dev/null; then - echo -e "${RED}Error: gopher_mcp_python is not importable${NC}" - echo -e "${YELLOW}Run: pip install -e . (from $PROJECT_DIR)${NC}" - exit 1 -fi - if [ -z "$GOPHER_MCP_URL" ]; then echo -e "${YELLOW}Warning: GOPHER_MCP_URL environment variable is not set${NC}" echo -e "${YELLOW}Set it with: export GOPHER_MCP_URL=http://127.0.0.1:8080/mcp${NC}" @@ -47,12 +66,35 @@ if [ -z "$ANTHROPIC_API_KEY" ]; then echo "" fi -cd "$PROJECT_DIR" +echo -e "${YELLOW}Setting up test project at $WORK_DIR...${NC}" +rm -rf "$WORK_DIR" +mkdir -p "$WORK_DIR" +cd "$WORK_DIR" + +echo -e "${YELLOW}Creating virtual environment...${NC}" +python3 -m venv venv +# shellcheck disable=SC1091 +source venv/bin/activate -PYTHONPATH="$PROJECT_DIR" \ -DYLD_LIBRARY_PATH="$PROJECT_DIR/native/lib" \ -LD_LIBRARY_PATH="$PROJECT_DIR/native/lib" \ -python3 examples/api/create_by_url.py "$@" +echo -e "${YELLOW}Installing gopher-mcp-python from PyPI...${NC}" +if [ -n "$SDK_VERSION" ]; then + echo -e "${CYAN}Installing version: $SDK_VERSION${NC}" + pip install --quiet "gopher-mcp-python==$SDK_VERSION" \ + "${NATIVE_PACKAGE}==$SDK_VERSION" +else + echo -e "${CYAN}Installing latest published version${NC}" + pip install --quiet gopher-mcp-python "$NATIVE_PACKAGE" +fi + +echo -e "${CYAN}Installed packages:${NC}" +pip list | grep -i gopher || true + +cp "$SCRIPT_DIR/create_by_url.py" . + +echo "" +echo -e "${YELLOW}Running example...${NC}" +echo "" +python create_by_url.py "$@" echo "" echo -e "${GREEN}Example completed${NC}" From 256e64b342769c911e8802a9d1fb83d4aac7586f Mon Sep 17 00:00:00 2001 From: RahulHere Date: Thu, 18 Jun 2026 09:27:57 +0800 Subject: [PATCH 18/29] Format code --- examples/api/create_by_api_key.py | 4 +--- examples/api/create_by_gateway_id.py | 12 +++--------- examples/api/create_by_gateway_name.py | 12 +++--------- examples/api/create_by_json.py | 4 +--- examples/api/create_by_server_id.py | 8 ++------ examples/api/create_by_server_name.py | 12 +++--------- examples/api/create_by_url.py | 8 ++------ examples/client_example_json.py | 1 - examples/pip/client_example_json.py | 1 - gopher_mcp_python/agent.py | 1 - gopher_mcp_python/ffi/auth/auth_client.py | 1 - setup.py | 1 + tests/ffi/auth/test_validation_options.py | 1 - tests/test_agent_create_by.py | 8 ++------ 14 files changed, 18 insertions(+), 56 deletions(-) diff --git a/examples/api/create_by_api_key.py b/examples/api/create_by_api_key.py index 5d7c5b42..2ed67d1a 100644 --- a/examples/api/create_by_api_key.py +++ b/examples/api/create_by_api_key.py @@ -56,9 +56,7 @@ def main() -> None: api_key = env_or("GOPHER_API_KEY", API_KEY_PLACEHOLDER) print(f"Provider: {provider}") - model_label = ( - f"{model} (set LLM_MODEL)" if model == MODEL_PLACEHOLDER else model - ) + model_label = f"{model} (set LLM_MODEL)" if model == MODEL_PLACEHOLDER else model print(f"Model: {model_label}") api_key_label = ( f"{api_key} (set GOPHER_API_KEY)" diff --git a/examples/api/create_by_gateway_id.py b/examples/api/create_by_gateway_id.py index 640d25e9..2d65c040 100644 --- a/examples/api/create_by_gateway_id.py +++ b/examples/api/create_by_gateway_id.py @@ -50,9 +50,7 @@ def env_or(name: str, fallback: str) -> str: def main() -> None: print("=== GopherAgent.create_with_gateway_id example ===") print(f"Usage: python3 {sys.argv[0]} [query1] [query2] ...") - print( - "Env: GOPHER_API_KEY GOPHER_MCP_GATEWAY_ID LLM_PROVIDER LLM_MODEL DEBUG" - ) + print("Env: GOPHER_API_KEY GOPHER_MCP_GATEWAY_ID LLM_PROVIDER LLM_MODEL DEBUG") print("") queries = sys.argv[1:] if len(sys.argv) > 1 else ["What time is it in Tokyo?"] @@ -63,9 +61,7 @@ def main() -> None: gateway_id = env_or("GOPHER_MCP_GATEWAY_ID", GATEWAY_ID_PLACEHOLDER) print(f"Provider: {provider}") - model_label = ( - f"{model} (set LLM_MODEL)" if model == MODEL_PLACEHOLDER else model - ) + model_label = f"{model} (set LLM_MODEL)" if model == MODEL_PLACEHOLDER else model print(f"Model: {model_label}") api_key_label = ( f"{api_key} (set GOPHER_API_KEY)" @@ -94,9 +90,7 @@ def main() -> None: sys.exit(1) print("\nCreating agent via GopherAgent.create_with_gateway_id...") - agent = GopherAgent.create_with_gateway_id( - provider, model, api_key, gateway_id - ) + agent = GopherAgent.create_with_gateway_id(provider, model, api_key, gateway_id) print("Agent created successfully!") try: diff --git a/examples/api/create_by_gateway_name.py b/examples/api/create_by_gateway_name.py index f62c02bb..af03ab82 100644 --- a/examples/api/create_by_gateway_name.py +++ b/examples/api/create_by_gateway_name.py @@ -50,9 +50,7 @@ def env_or(name: str, fallback: str) -> str: def main() -> None: print("=== GopherAgent.create_with_gateway_name example ===") print(f"Usage: python3 {sys.argv[0]} [query1] [query2] ...") - print( - "Env: GOPHER_API_KEY GOPHER_MCP_GATEWAY_NAME LLM_PROVIDER LLM_MODEL DEBUG" - ) + print("Env: GOPHER_API_KEY GOPHER_MCP_GATEWAY_NAME LLM_PROVIDER LLM_MODEL DEBUG") print("") queries = sys.argv[1:] if len(sys.argv) > 1 else ["What time is it in Tokyo?"] @@ -63,9 +61,7 @@ def main() -> None: gateway_name = env_or("GOPHER_MCP_GATEWAY_NAME", GATEWAY_NAME_PLACEHOLDER) print(f"Provider: {provider}") - model_label = ( - f"{model} (set LLM_MODEL)" if model == MODEL_PLACEHOLDER else model - ) + model_label = f"{model} (set LLM_MODEL)" if model == MODEL_PLACEHOLDER else model print(f"Model: {model_label}") api_key_label = ( f"{api_key} (set GOPHER_API_KEY)" @@ -94,9 +90,7 @@ def main() -> None: sys.exit(1) print("\nCreating agent via GopherAgent.create_with_gateway_name...") - agent = GopherAgent.create_with_gateway_name( - provider, model, api_key, gateway_name - ) + agent = GopherAgent.create_with_gateway_name(provider, model, api_key, gateway_name) print("Agent created successfully!") try: diff --git a/examples/api/create_by_json.py b/examples/api/create_by_json.py index 8d2341b0..6b19e339 100644 --- a/examples/api/create_by_json.py +++ b/examples/api/create_by_json.py @@ -77,9 +77,7 @@ def main() -> None: model = env_or("LLM_MODEL", MODEL_PLACEHOLDER) print(f"Provider: {provider}") - model_label = ( - f"{model} (set LLM_MODEL)" if model == MODEL_PLACEHOLDER else model - ) + model_label = f"{model} (set LLM_MODEL)" if model == MODEL_PLACEHOLDER else model print(f"Model: {model_label}") print(f"Queries: {len(queries)}") diff --git a/examples/api/create_by_server_id.py b/examples/api/create_by_server_id.py index 2b1cd297..9f698f23 100644 --- a/examples/api/create_by_server_id.py +++ b/examples/api/create_by_server_id.py @@ -48,9 +48,7 @@ def env_or(name: str, fallback: str) -> str: def main() -> None: print("=== GopherAgent.create_with_server_id example ===") print(f"Usage: python3 {sys.argv[0]} [query1] [query2] ...") - print( - "Env: GOPHER_API_KEY GOPHER_MCP_SERVER_ID LLM_PROVIDER LLM_MODEL DEBUG" - ) + print("Env: GOPHER_API_KEY GOPHER_MCP_SERVER_ID LLM_PROVIDER LLM_MODEL DEBUG") print("") queries = sys.argv[1:] if len(sys.argv) > 1 else ["What time is it in Tokyo?"] @@ -61,9 +59,7 @@ def main() -> None: server_id = env_or("GOPHER_MCP_SERVER_ID", SERVER_ID_PLACEHOLDER) print(f"Provider: {provider}") - model_label = ( - f"{model} (set LLM_MODEL)" if model == MODEL_PLACEHOLDER else model - ) + model_label = f"{model} (set LLM_MODEL)" if model == MODEL_PLACEHOLDER else model print(f"Model: {model_label}") api_key_label = ( f"{api_key} (set GOPHER_API_KEY)" diff --git a/examples/api/create_by_server_name.py b/examples/api/create_by_server_name.py index d0f4732f..9dbf48bb 100644 --- a/examples/api/create_by_server_name.py +++ b/examples/api/create_by_server_name.py @@ -50,9 +50,7 @@ def env_or(name: str, fallback: str) -> str: def main() -> None: print("=== GopherAgent.create_with_server_name example ===") print(f"Usage: python3 {sys.argv[0]} [query1] [query2] ...") - print( - "Env: GOPHER_API_KEY GOPHER_MCP_SERVER_NAME LLM_PROVIDER LLM_MODEL DEBUG" - ) + print("Env: GOPHER_API_KEY GOPHER_MCP_SERVER_NAME LLM_PROVIDER LLM_MODEL DEBUG") print("") queries = sys.argv[1:] if len(sys.argv) > 1 else ["What time is it in Tokyo?"] @@ -63,9 +61,7 @@ def main() -> None: server_name = env_or("GOPHER_MCP_SERVER_NAME", SERVER_NAME_PLACEHOLDER) print(f"Provider: {provider}") - model_label = ( - f"{model} (set LLM_MODEL)" if model == MODEL_PLACEHOLDER else model - ) + model_label = f"{model} (set LLM_MODEL)" if model == MODEL_PLACEHOLDER else model print(f"Model: {model_label}") api_key_label = ( f"{api_key} (set GOPHER_API_KEY)" @@ -94,9 +90,7 @@ def main() -> None: sys.exit(1) print("\nCreating agent via GopherAgent.create_with_server_name...") - agent = GopherAgent.create_with_server_name( - provider, model, api_key, server_name - ) + agent = GopherAgent.create_with_server_name(provider, model, api_key, server_name) print("Agent created successfully!") try: diff --git a/examples/api/create_by_url.py b/examples/api/create_by_url.py index c84320e9..0feb71d6 100644 --- a/examples/api/create_by_url.py +++ b/examples/api/create_by_url.py @@ -57,13 +57,9 @@ def main() -> None: url = env_or("GOPHER_MCP_URL", URL_PLACEHOLDER) print(f"Provider: {provider}") - model_label = ( - f"{model} (set LLM_MODEL)" if model == MODEL_PLACEHOLDER else model - ) + model_label = f"{model} (set LLM_MODEL)" if model == MODEL_PLACEHOLDER else model print(f"Model: {model_label}") - url_label = ( - f"{url} (set GOPHER_MCP_URL)" if url == URL_PLACEHOLDER else url - ) + url_label = f"{url} (set GOPHER_MCP_URL)" if url == URL_PLACEHOLDER else url print(f"MCP URL: {url_label}") print(f"Queries: {len(queries)}") diff --git a/examples/client_example_json.py b/examples/client_example_json.py index efda8ea5..52c0103c 100755 --- a/examples/client_example_json.py +++ b/examples/client_example_json.py @@ -8,7 +8,6 @@ from gopher_mcp_python import GopherAgent - # Server configuration for local MCP servers SERVER_CONFIG = json.dumps( { diff --git a/examples/pip/client_example_json.py b/examples/pip/client_example_json.py index f5060f97..f2df6fe5 100755 --- a/examples/pip/client_example_json.py +++ b/examples/pip/client_example_json.py @@ -12,7 +12,6 @@ from gopher_mcp_python import GopherAgent - # Server configuration for local MCP servers SERVER_CONFIG = json.dumps( { diff --git a/gopher_mcp_python/agent.py b/gopher_mcp_python/agent.py index 4939007d..3e74b486 100644 --- a/gopher_mcp_python/agent.py +++ b/gopher_mcp_python/agent.py @@ -34,7 +34,6 @@ from gopher_mcp_python.errors import AgentError, TimeoutError from gopher_mcp_python.ffi import GopherOrchLibrary, GopherOrchHandle - _initialized = False _cleanup_handler_registered = False diff --git a/gopher_mcp_python/ffi/auth/auth_client.py b/gopher_mcp_python/ffi/auth/auth_client.py index 70871e60..de0bc0cf 100644 --- a/gopher_mcp_python/ffi/auth/auth_client.py +++ b/gopher_mcp_python/ffi/auth/auth_client.py @@ -22,7 +22,6 @@ ) from gopher_mcp_python.ffi.auth.validation_options import GopherValidationOptions - # ============================================================================ # Library Lifecycle Functions # ============================================================================ diff --git a/setup.py b/setup.py index eedec75a..c816fb57 100644 --- a/setup.py +++ b/setup.py @@ -4,6 +4,7 @@ All configuration is in pyproject.toml. This file only exists to support editable installs with pip < 21.3. """ + from setuptools import setup setup() diff --git a/tests/ffi/auth/test_validation_options.py b/tests/ffi/auth/test_validation_options.py index dcf94c85..29ff453a 100644 --- a/tests/ffi/auth/test_validation_options.py +++ b/tests/ffi/auth/test_validation_options.py @@ -8,7 +8,6 @@ gopher_create_validation_options, ) - # Skip all tests if auth functions not available pytestmark = pytest.mark.skipif( not is_auth_available(), diff --git a/tests/test_agent_create_by.py b/tests/test_agent_create_by.py index 55a41854..3a206971 100644 --- a/tests/test_agent_create_by.py +++ b/tests/test_agent_create_by.py @@ -42,9 +42,7 @@ def test_create_with_server_id_rejects_empty_api_key(self) -> None: def test_create_with_server_name_rejects_empty_api_key(self) -> None: with pytest.raises(AgentError): - GopherAgent.create_with_server_name( - PROVIDER, MODEL, "", "my-server" - ) + GopherAgent.create_with_server_name(PROVIDER, MODEL, "", "my-server") def test_create_with_gateway_id_rejects_empty_api_key(self) -> None: with pytest.raises(AgentError): @@ -52,9 +50,7 @@ def test_create_with_gateway_id_rejects_empty_api_key(self) -> None: def test_create_with_gateway_name_rejects_empty_api_key(self) -> None: with pytest.raises(AgentError): - GopherAgent.create_with_gateway_name( - PROVIDER, MODEL, "", "my-gateway" - ) + GopherAgent.create_with_gateway_name(PROVIDER, MODEL, "", "my-gateway") # ---------------------------------------------------------------- # create_with_url rejects empty url before any FFI work happens. From d8ab61979767fe1a7d1850e14359989d038fdb2c Mon Sep 17 00:00:00 2001 From: RahulHere Date: Tue, 28 Jul 2026 11:40:19 +0800 Subject: [PATCH 19/29] Track gopher-orch main Summary:\n- update third_party/gopher-orch submodule branch from br_release to main\n- advance third_party/gopher-orch to latest origin/main commit bf4b46ad --- .gitmodules | 2 +- third_party/gopher-orch | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitmodules b/.gitmodules index 18a3014b..435fb285 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +1,4 @@ [submodule "third_party/gopher-orch"] path = third_party/gopher-orch url = https://github.com/GopherSecurity/gopher-orch.git - branch = br_release + branch = main diff --git a/third_party/gopher-orch b/third_party/gopher-orch index 05667fdf..bf4b46ad 160000 --- a/third_party/gopher-orch +++ b/third_party/gopher-orch @@ -1 +1 @@ -Subproject commit 05667fdf7c05d51ccd0aa6abc33545e0a022cf00 +Subproject commit bf4b46adb67e3ed9f2ddfcd4c460d9b3271f20c3 From 8c24d4376f3b36bca536695cab02f0e6ced6cf0c Mon Sep 17 00:00:00 2001 From: RahulHere Date: Thu, 30 Jul 2026 15:30:11 +0800 Subject: [PATCH 20/29] Bind optional routing FFI symbols independently Summary: - configure each optional routing factory symbol with its own AttributeError guard - avoid leaving present ctypes functions with the default c_int restype when an earlier optional symbol is missing - add a fake-CDLL regression test for partially available routing symbols Verification: - git diff --check - python3 -m pytest tests/test_ffi.py -q --- gopher_mcp_python/ffi/library.py | 82 ++++++++++++++------------------ tests/test_ffi.py | 58 ++++++++++++++++++++++ 2 files changed, 95 insertions(+), 45 deletions(-) diff --git a/gopher_mcp_python/ffi/library.py b/gopher_mcp_python/ffi/library.py index 3aa0aee6..2e68428e 100644 --- a/gopher_mcp_python/ffi/library.py +++ b/gopher_mcp_python/ffi/library.py @@ -136,51 +136,43 @@ def _setup_functions(self) -> None: # Routing factories: scope the agent to a single MCP server or gateway # selected by id / name, or to a known MCP URL. These C symbols landed - # in gopher-orch 0.1.23 -- wrapped in try / except so the SDK still - # loads against an older libgopher-orch, with the higher-level - # factories raising AgentError at call time if the symbol is missing. - try: - self._lib.gopher_orch_agent_create_by_server_id.argtypes = [ - c_char_p, - c_char_p, - c_char_p, - c_char_p, - ] - self._lib.gopher_orch_agent_create_by_server_id.restype = c_void_p - - self._lib.gopher_orch_agent_create_by_server_name.argtypes = [ - c_char_p, - c_char_p, - c_char_p, - c_char_p, - ] - self._lib.gopher_orch_agent_create_by_server_name.restype = c_void_p - - self._lib.gopher_orch_agent_create_by_gateway_id.argtypes = [ - c_char_p, - c_char_p, - c_char_p, - c_char_p, - ] - self._lib.gopher_orch_agent_create_by_gateway_id.restype = c_void_p - - self._lib.gopher_orch_agent_create_by_gateway_name.argtypes = [ - c_char_p, - c_char_p, - c_char_p, - c_char_p, - ] - self._lib.gopher_orch_agent_create_by_gateway_name.restype = c_void_p - - self._lib.gopher_orch_agent_create_by_url.argtypes = [ - c_char_p, - c_char_p, - c_char_p, - ] - self._lib.gopher_orch_agent_create_by_url.restype = c_void_p - except AttributeError: - # Older libgopher-orch builds (< 0.1.23) lack these symbols. - pass + # in gopher-orch 0.1.23, so bind each one independently to keep the SDK + # loadable against older libgopher-orch builds while still configuring + # every symbol that is present. + routing_factories = [ + ( + "gopher_orch_agent_create_by_server_id", + [c_char_p, c_char_p, c_char_p, c_char_p], + c_void_p, + ), + ( + "gopher_orch_agent_create_by_server_name", + [c_char_p, c_char_p, c_char_p, c_char_p], + c_void_p, + ), + ( + "gopher_orch_agent_create_by_gateway_id", + [c_char_p, c_char_p, c_char_p, c_char_p], + c_void_p, + ), + ( + "gopher_orch_agent_create_by_gateway_name", + [c_char_p, c_char_p, c_char_p, c_char_p], + c_void_p, + ), + ( + "gopher_orch_agent_create_by_url", + [c_char_p, c_char_p, c_char_p], + c_void_p, + ), + ] + for name, argtypes, restype in routing_factories: + try: + fn = getattr(self._lib, name) + except AttributeError: + continue + fn.argtypes = argtypes + fn.restype = restype self._lib.gopher_orch_agent_run.argtypes = [c_void_p, c_char_p, c_int64] self._lib.gopher_orch_agent_run.restype = c_char_p diff --git a/tests/test_ffi.py b/tests/test_ffi.py index cc691af1..84c7a08f 100644 --- a/tests/test_ffi.py +++ b/tests/test_ffi.py @@ -7,6 +7,7 @@ import json import os +from ctypes import c_char_p, c_int, c_void_p import pytest @@ -21,6 +22,63 @@ def is_native_library_available() -> bool: class TestGopherOrchLibrary: """Tests for GopherOrchLibrary FFI bindings.""" + def test_should_bind_present_optional_routing_symbols_independently(self): + """Missing optional symbols must not skip binding later symbols.""" + + class FakeFunction: + def __init__(self): + self.argtypes = None + self.restype = c_int + + def __call__(self, *args): + return None + + class FakeLib: + missing = {"gopher_orch_agent_create_by_server_id"} + + def __init__(self): + names = [ + "gopher_orch_agent_create_by_json", + "gopher_orch_agent_create_by_api_key", + "gopher_orch_agent_create_by_server_name", + "gopher_orch_agent_create_by_gateway_id", + "gopher_orch_agent_create_by_gateway_name", + "gopher_orch_agent_create_by_url", + "gopher_orch_agent_run", + "gopher_orch_agent_add_ref", + "gopher_orch_agent_release", + "gopher_orch_api_fetch_servers", + "gopher_orch_last_error", + "gopher_orch_clear_error", + "gopher_orch_free", + "gopher_orch_set_log_level", + ] + self._functions = {name: FakeFunction() for name in names} + + def __getattr__(self, name): + if name in self.missing: + raise AttributeError(name) + try: + return self._functions[name] + except KeyError: + raise AttributeError(name) from None + + fake_lib = FakeLib() + lib = GopherOrchLibrary.__new__(GopherOrchLibrary) + lib._lib = fake_lib + + lib._setup_functions() + + assert fake_lib.gopher_orch_agent_create_by_server_name.restype is c_void_p + assert fake_lib.gopher_orch_agent_create_by_gateway_id.restype is c_void_p + assert fake_lib.gopher_orch_agent_create_by_gateway_name.restype is c_void_p + assert fake_lib.gopher_orch_agent_create_by_url.restype is c_void_p + assert fake_lib.gopher_orch_agent_create_by_url.argtypes == [ + c_char_p, + c_char_p, + c_char_p, + ] + def test_library_should_be_available(self): """Test that library should be available.""" available = GopherOrchLibrary.is_available() From f7a2b6c616f7a862ff61fa8de99ec031e105e5cc Mon Sep 17 00:00:00 2001 From: RahulHere Date: Thu, 30 Jul 2026 15:33:40 +0800 Subject: [PATCH 21/29] Report missing routing FFI symbols clearly Summary: - raise a distinct upgrade-focused error when optional routing factory symbols are absent - avoid confusing missing native symbols with native NULL handle returns or stale last-error state - add direct FFI and public GopherAgent regression coverage for the missing-symbol path Verification: - git diff --check - python3 -m pytest tests/test_ffi.py -q --- gopher_mcp_python/ffi/library.py | 17 ++++++++---- tests/test_ffi.py | 44 ++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 5 deletions(-) diff --git a/gopher_mcp_python/ffi/library.py b/gopher_mcp_python/ffi/library.py index 2e68428e..1a0e041b 100644 --- a/gopher_mcp_python/ffi/library.py +++ b/gopher_mcp_python/ffi/library.py @@ -340,7 +340,7 @@ def agent_create_by_server_id( return None fn = getattr(self._lib, "gopher_orch_agent_create_by_server_id", None) if fn is None: - return None + raise RuntimeError(_missing_routing_factory_message()) return fn( provider.encode("utf-8"), model.encode("utf-8"), @@ -359,7 +359,7 @@ def agent_create_by_server_name( return None fn = getattr(self._lib, "gopher_orch_agent_create_by_server_name", None) if fn is None: - return None + raise RuntimeError(_missing_routing_factory_message()) return fn( provider.encode("utf-8"), model.encode("utf-8"), @@ -380,7 +380,7 @@ def agent_create_by_gateway_id( return None fn = getattr(self._lib, "gopher_orch_agent_create_by_gateway_id", None) if fn is None: - return None + raise RuntimeError(_missing_routing_factory_message()) return fn( provider.encode("utf-8"), model.encode("utf-8"), @@ -399,7 +399,7 @@ def agent_create_by_gateway_name( return None fn = getattr(self._lib, "gopher_orch_agent_create_by_gateway_name", None) if fn is None: - return None + raise RuntimeError(_missing_routing_factory_message()) return fn( provider.encode("utf-8"), model.encode("utf-8"), @@ -420,7 +420,7 @@ def agent_create_by_url( return None fn = getattr(self._lib, "gopher_orch_agent_create_by_url", None) if fn is None: - return None + raise RuntimeError(_missing_routing_factory_message()) return fn( provider.encode("utf-8"), model.encode("utf-8"), @@ -507,3 +507,10 @@ def set_log_level(self, level: int) -> None: """ if self._available and self._lib is not None: self._lib.gopher_orch_set_log_level(level) + + +def _missing_routing_factory_message() -> str: + return ( + "this build of libgopher-orch predates the routing factories; " + "upgrade the native gopher-orch library to 0.1.23 or newer" + ) diff --git a/tests/test_ffi.py b/tests/test_ffi.py index 84c7a08f..05b2e348 100644 --- a/tests/test_ffi.py +++ b/tests/test_ffi.py @@ -11,6 +11,8 @@ import pytest +import gopher_mcp_python.agent as agent_module +from gopher_mcp_python import AgentError, GopherAgent from gopher_mcp_python.ffi import GopherOrchLibrary @@ -79,6 +81,48 @@ def __getattr__(self, name): c_char_p, ] + def test_missing_optional_routing_symbol_raises_upgrade_error(self): + """Absent routing factories should not look like native NULL returns.""" + + class FakeLib: + pass + + lib = GopherOrchLibrary.__new__(GopherOrchLibrary) + lib._available = True + lib._lib = FakeLib() + + with pytest.raises(RuntimeError, match="predates the routing factories"): + lib.agent_create_by_url( + "AnthropicProvider", "claude-3-haiku-20240307", "http://x/mcp" + ) + + def test_public_factory_surfaces_missing_routing_symbol_message( + self, monkeypatch + ): + """AgentError should tell users to upgrade when the native symbol is absent.""" + + class FakeLib: + def agent_create_by_url(self, provider, model, url): + raise RuntimeError( + "this build of libgopher-orch predates the routing factories; " + "upgrade the native gopher-orch library to 0.1.23 or newer" + ) + + monkeypatch.setattr(agent_module, "_initialized", True) + monkeypatch.setattr( + GopherOrchLibrary, + "get_instance", + classmethod(lambda cls: FakeLib()), + ) + + with pytest.raises(AgentError) as exc_info: + GopherAgent.create_with_url( + "AnthropicProvider", "claude-3-haiku-20240307", "http://x/mcp" + ) + + assert "predates the routing factories" in str(exc_info.value) + assert "upgrade the native gopher-orch library" in str(exc_info.value) + def test_library_should_be_available(self): """Test that library should be available.""" available = GopherOrchLibrary.is_available() From 18bb5f36fdd641e5ca3e739dbf878d799c0ec3b8 Mon Sep 17 00:00:00 2001 From: RahulHere Date: Thu, 30 Jul 2026 15:36:25 +0800 Subject: [PATCH 22/29] Require routing FFI symbols for factory contract tests Summary: - skip routing factory contract tests unless all native routing factory symbols are present - prevent older libgopher-orch builds from satisfying the tests through missing-symbol AgentError paths - avoid a common local port in the unknown-provider URL case and correct the empty-url validation comment Verification: - git diff --check - python3 -m pytest tests/test_agent_create_by.py tests/test_ffi.py -q --- tests/test_agent_create_by.py | 32 +++++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/tests/test_agent_create_by.py b/tests/test_agent_create_by.py index 3a206971..ed7aa315 100644 --- a/tests/test_agent_create_by.py +++ b/tests/test_agent_create_by.py @@ -18,12 +18,30 @@ PROVIDER = "AnthropicProvider" MODEL = "test-model" BAD_PROVIDER = "NotARealProvider" -URL = "http://127.0.0.1:8080/mcp" +URL = "http://127.0.0.1:1/mcp" + +ROUTING_FACTORY_SYMBOLS = [ + "gopher_orch_agent_create_by_server_id", + "gopher_orch_agent_create_by_server_name", + "gopher_orch_agent_create_by_gateway_id", + "gopher_orch_agent_create_by_gateway_name", + "gopher_orch_agent_create_by_url", +] + + +def has_routing_factory_symbols() -> bool: + lib = GopherOrchLibrary.get_instance() + if lib is None or lib._lib is None: + return False + return all(hasattr(lib._lib, symbol) for symbol in ROUTING_FACTORY_SYMBOLS) pytestmark = pytest.mark.skipif( - not GopherOrchLibrary.is_available(), - reason="Native library not available -- run ./build.sh first", + not has_routing_factory_symbols(), + reason=( + "Native routing factory symbols not available -- use libgopher-orch " + "0.1.23 or newer" + ), ) @@ -53,8 +71,8 @@ def test_create_with_gateway_name_rejects_empty_api_key(self) -> None: GopherAgent.create_with_gateway_name(PROVIDER, MODEL, "", "my-gateway") # ---------------------------------------------------------------- - # create_with_url rejects empty url before any FFI work happens. - # Mirrors the CreateByUrlRejectsEmptyUrl case in the C++ suite. + # Mirrors the native CreateByUrlRejectsEmptyUrl case. The Python wrapper + # delegates validation to libgopher-orch and surfaces it as AgentError. # ---------------------------------------------------------------- def test_create_with_url_rejects_empty_url(self) -> None: @@ -64,8 +82,8 @@ def test_create_with_url_rejects_empty_url(self) -> None: # ---------------------------------------------------------------- # Unknown provider. create_with_url synthesises a local http_sse # config and reaches create_by_json on the native side, which - # rejects an unknown provider name. The factory must surface that - # as AgentError. + # rejects an unknown provider name. Use an unlikely local port so the test + # does not accidentally talk to a developer service on 8080. # ---------------------------------------------------------------- def test_create_with_url_rejects_unknown_provider(self) -> None: From 2a7f4e6605047cd37f8f51dbd831e0867fcc9e4e Mon Sep 17 00:00:00 2001 From: RahulHere Date: Thu, 30 Jul 2026 15:40:41 +0800 Subject: [PATCH 23/29] Avoid unreleased routing factory version references Summary: - remove hard-coded 0.1.23 routing factory availability claims from examples and diagnostics - describe routing factory support as requiring a published release with the native symbols - keep tests aligned with release-neutral missing-symbol wording Verification: - git diff --check - python3 -m pytest tests/test_ffi.py tests/test_agent_create_by.py -q --- examples/api/README.md | 19 ++++++++++--------- examples/api/create_by_gateway_id_run.sh | 6 +++--- examples/api/create_by_gateway_name_run.sh | 6 +++--- examples/api/create_by_server_id_run.sh | 6 +++--- examples/api/create_by_server_name_run.sh | 6 +++--- examples/api/create_by_url_run.sh | 6 +++--- gopher_mcp_python/ffi/library.py | 8 ++++---- tests/test_agent_create_by.py | 4 ++-- tests/test_ffi.py | 7 +++++-- 9 files changed, 36 insertions(+), 32 deletions(-) diff --git a/examples/api/README.md b/examples/api/README.md index 397ced23..69c22ae8 100644 --- a/examples/api/README.md +++ b/examples/api/README.md @@ -63,7 +63,7 @@ arguments to the example as queries. a wrapper. Otherwise the latest published version is installed: ```sh - SDK_VERSION=0.1.23 ./examples/api/create_by_server_id_run.sh + SDK_VERSION= ./examples/api/create_by_server_id_run.sh ``` The wrappers are idempotent: each run nukes its `test-project-*` @@ -136,9 +136,9 @@ C++ docs and the TypeScript port. The five routing factories (`create_with_server_id` / `_server_name` / `_gateway_id` / -`_gateway_name` / `_url`) require `gopher-mcp-python` ≥ 0.1.23 on -PyPI. Earlier versions only expose `create_with_api_key` and -`create_with_server_config`. +`_gateway_name` / `_url`) require a PyPI release that includes the +routing factory native symbols. Earlier releases only expose +`create_with_api_key` and `create_with_server_config`. ## How the examples find the SDK @@ -211,13 +211,14 @@ xattr -d com.apple.quarantine "$(python -c 'import gopher_mcp_python_native_darw ### Routing factory raises `AgentError` against an older PyPI release -The five routing factories landed in `gopher-mcp-python` 0.1.23. If -the wrapper installs an older version, the higher-level factory -raises `AgentError` because the underlying C symbol is missing. Pin -to a recent release: +The five routing factories require a `gopher-mcp-python` release that +includes the matching native routing factory symbols. If the wrapper +installs an older version, the higher-level factory raises `AgentError` +because the underlying C symbol is missing. Pin to a release that +contains this feature once it is published: ```sh -SDK_VERSION=0.1.23 ./examples/api/create_by_server_id_run.sh +SDK_VERSION= ./examples/api/create_by_server_id_run.sh ``` ## Cross-reference diff --git a/examples/api/create_by_gateway_id_run.sh b/examples/api/create_by_gateway_id_run.sh index 63ab0f00..65f52e20 100755 --- a/examples/api/create_by_gateway_id_run.sh +++ b/examples/api/create_by_gateway_id_run.sh @@ -5,9 +5,9 @@ # fresh venv, installs the SDK plus the matching platform native package # from PyPI, then runs the example. # -# Set SDK_VERSION to pin to a specific release (e.g. SDK_VERSION=0.1.23); -# otherwise the latest published version is installed. The routing -# factories require gopher-mcp-python >= 0.1.23. +# Set SDK_VERSION to pin to a specific release; otherwise the latest +# published version is installed. The routing factories require a +# release that includes the native routing factory symbols. set -e diff --git a/examples/api/create_by_gateway_name_run.sh b/examples/api/create_by_gateway_name_run.sh index 724e05be..74af6441 100755 --- a/examples/api/create_by_gateway_name_run.sh +++ b/examples/api/create_by_gateway_name_run.sh @@ -5,9 +5,9 @@ # fresh venv, installs the SDK plus the matching platform native package # from PyPI, then runs the example. # -# Set SDK_VERSION to pin to a specific release (e.g. SDK_VERSION=0.1.23); -# otherwise the latest published version is installed. The routing -# factories require gopher-mcp-python >= 0.1.23. +# Set SDK_VERSION to pin to a specific release; otherwise the latest +# published version is installed. The routing factories require a +# release that includes the native routing factory symbols. set -e diff --git a/examples/api/create_by_server_id_run.sh b/examples/api/create_by_server_id_run.sh index dfa27fe3..d4539cbe 100755 --- a/examples/api/create_by_server_id_run.sh +++ b/examples/api/create_by_server_id_run.sh @@ -5,9 +5,9 @@ # fresh venv, installs the SDK plus the matching platform native package # from PyPI, then runs the example. # -# Set SDK_VERSION to pin to a specific release (e.g. SDK_VERSION=0.1.23); -# otherwise the latest published version is installed. The routing -# factories require gopher-mcp-python >= 0.1.23. +# Set SDK_VERSION to pin to a specific release; otherwise the latest +# published version is installed. The routing factories require a +# release that includes the native routing factory symbols. set -e diff --git a/examples/api/create_by_server_name_run.sh b/examples/api/create_by_server_name_run.sh index 75785f29..e8015779 100755 --- a/examples/api/create_by_server_name_run.sh +++ b/examples/api/create_by_server_name_run.sh @@ -5,9 +5,9 @@ # fresh venv, installs the SDK plus the matching platform native package # from PyPI, then runs the example. # -# Set SDK_VERSION to pin to a specific release (e.g. SDK_VERSION=0.1.23); -# otherwise the latest published version is installed. The routing -# factories require gopher-mcp-python >= 0.1.23. +# Set SDK_VERSION to pin to a specific release; otherwise the latest +# published version is installed. The routing factories require a +# release that includes the native routing factory symbols. set -e diff --git a/examples/api/create_by_url_run.sh b/examples/api/create_by_url_run.sh index 4bf33cd6..e24bc12f 100755 --- a/examples/api/create_by_url_run.sh +++ b/examples/api/create_by_url_run.sh @@ -5,9 +5,9 @@ # installs the SDK plus the matching platform native package from PyPI, # then runs the example. # -# Set SDK_VERSION to pin to a specific release (e.g. SDK_VERSION=0.1.23); -# otherwise the latest published version is installed. create_with_url -# requires gopher-mcp-python >= 0.1.23. +# Set SDK_VERSION to pin to a specific release; otherwise the latest +# published version is installed. create_with_url requires a release +# that includes the native routing factory symbols. set -e diff --git a/gopher_mcp_python/ffi/library.py b/gopher_mcp_python/ffi/library.py index 1a0e041b..200b9582 100644 --- a/gopher_mcp_python/ffi/library.py +++ b/gopher_mcp_python/ffi/library.py @@ -136,9 +136,9 @@ def _setup_functions(self) -> None: # Routing factories: scope the agent to a single MCP server or gateway # selected by id / name, or to a known MCP URL. These C symbols landed - # in gopher-orch 0.1.23, so bind each one independently to keep the SDK - # loadable against older libgopher-orch builds while still configuring - # every symbol that is present. + # after the initial factories, so bind each one independently to keep + # the SDK loadable against older libgopher-orch builds while still + # configuring every symbol that is present. routing_factories = [ ( "gopher_orch_agent_create_by_server_id", @@ -512,5 +512,5 @@ def set_log_level(self, level: int) -> None: def _missing_routing_factory_message() -> str: return ( "this build of libgopher-orch predates the routing factories; " - "upgrade the native gopher-orch library to 0.1.23 or newer" + "upgrade to a native gopher-orch library release that includes them" ) diff --git a/tests/test_agent_create_by.py b/tests/test_agent_create_by.py index ed7aa315..d1a5aa69 100644 --- a/tests/test_agent_create_by.py +++ b/tests/test_agent_create_by.py @@ -39,8 +39,8 @@ def has_routing_factory_symbols() -> bool: pytestmark = pytest.mark.skipif( not has_routing_factory_symbols(), reason=( - "Native routing factory symbols not available -- use libgopher-orch " - "0.1.23 or newer" + "Native routing factory symbols not available -- use a libgopher-orch " + "release that includes them" ), ) diff --git a/tests/test_ffi.py b/tests/test_ffi.py index 05b2e348..8a6ced8a 100644 --- a/tests/test_ffi.py +++ b/tests/test_ffi.py @@ -105,7 +105,8 @@ class FakeLib: def agent_create_by_url(self, provider, model, url): raise RuntimeError( "this build of libgopher-orch predates the routing factories; " - "upgrade the native gopher-orch library to 0.1.23 or newer" + "upgrade to a native gopher-orch library release that includes " + "them" ) monkeypatch.setattr(agent_module, "_initialized", True) @@ -121,7 +122,9 @@ def agent_create_by_url(self, provider, model, url): ) assert "predates the routing factories" in str(exc_info.value) - assert "upgrade the native gopher-orch library" in str(exc_info.value) + assert "upgrade to a native gopher-orch library release" in str( + exc_info.value + ) def test_library_should_be_available(self): """Test that library should be available.""" From 89e404fc0cffd14b1e8001fd597c70e4c8ef4b8a Mon Sep 17 00:00:00 2001 From: RahulHere Date: Thu, 30 Jul 2026 15:43:14 +0800 Subject: [PATCH 24/29] Use reachable tag for release note range Summary: - select the previous Python release tag with git describe from HEAD ancestry - avoid using the globally highest version tag when it is off-branch or points at HEAD - warn when the SDK commit range is empty before omitting the SDK changes section Verification: - bash -n dump-version.sh - git diff --check - git describe --tags --abbrev=0 --match 'v*' HEAD --- dump-version.sh | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/dump-version.sh b/dump-version.sh index 03209de8..f86cd97b 100755 --- a/dump-version.sh +++ b/dump-version.sh @@ -165,8 +165,10 @@ fi # Fetch tags so PREV_TAG resolution is accurate even on shallow clones git fetch --tags --quiet 2>/dev/null || true -# Previous Python tag (anything matching v*, sorted by semver) -PREV_TAG=$(git tag -l 'v*' --sort=-v:refname | head -1) +# Previous Python tag reachable from HEAD. Using git describe keeps the range +# anchored to this branch instead of picking the highest version tag anywhere in +# the repository. +PREV_TAG=$(git describe --tags --abbrev=0 --match 'v*' HEAD 2>/dev/null || true) if [ -n "$PREV_TAG" ]; then echo -e " Previous Python tag: ${CYAN}$PREV_TAG${NC}" PY_RANGE="$PREV_TAG..HEAD" @@ -206,6 +208,9 @@ if [ -n "$PY_COMMITS" ]; then PY_COMMIT_COUNT=$(printf '%s\n' "$PY_COMMITS" | wc -l | tr -d ' ') fi echo -e " Python commits in range:${GREEN} $PY_COMMIT_COUNT${NC}" +if [ -n "$PREV_TAG" ] && [ "$PY_COMMIT_COUNT" -eq 0 ]; then + echo -e " ${YELLOW}Warning: no Python SDK commits found in $PY_RANGE; SDK changes section will be omitted.${NC}" +fi # Extract the "What's Changed" block from the gopher-orch release notes, # stripping the Build Information preamble and the trailing Full Changelog link. From 6582f8d4fad5d32d15a4c3866d669338c7040d10 Mon Sep 17 00:00:00 2001 From: RahulHere Date: Thu, 30 Jul 2026 15:46:06 +0800 Subject: [PATCH 25/29] Derive previous orch version from tagged package Summary: - read the previous package version from the prior tag's pyproject.toml - derive the previous gopher-orch base version from X.Y.Z or X.Y.Z.E package versions - warn when the tagged package version cannot be read or parsed Verification: - bash -n dump-version.sh - git diff --check --- dump-version.sh | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/dump-version.sh b/dump-version.sh index f86cd97b..f2149a18 100755 --- a/dump-version.sh +++ b/dump-version.sh @@ -177,12 +177,21 @@ else PY_RANGE="HEAD" fi -# Previous gopher-orch version recorded in the previous release commit +# Previous gopher-orch version from the package version recorded at the +# previous tag. Extended Python versions are X.Y.Z.E, where X.Y.Z tracks +# gopher-orch. PREV_GOPHER_ORCH_VERSION="" if [ -n "$PREV_TAG" ]; then - PREV_GOPHER_ORCH_VERSION=$(git log -1 --format=%B "$PREV_TAG" 2>/dev/null | \ - grep -oE 'gopher-orch version: [0-9]+\.[0-9]+\.[0-9]+' | \ - awk '{print $NF}' | head -1) + PREV_PY_VERSION=$(git show "$PREV_TAG:$PYPROJECT_TOML" 2>/dev/null | \ + grep -E '^version\s*=' | head -1 | sed -E 's/.*"([^"]+)".*/\1/') + if echo "$PREV_PY_VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+(\.[0-9]+)?$'; then + PREV_GOPHER_ORCH_VERSION=$(echo "$PREV_PY_VERSION" | \ + sed -E 's/^([0-9]+\.[0-9]+\.[0-9]+)(\.[0-9]+)?$/\1/') + elif [ -n "$PREV_PY_VERSION" ]; then + echo -e " ${YELLOW}Warning: could not derive previous gopher-orch version from $PREV_TAG:$PYPROJECT_TOML version '$PREV_PY_VERSION'${NC}" + else + echo -e " ${YELLOW}Warning: could not read previous package version from $PREV_TAG:$PYPROJECT_TOML${NC}" + fi fi if [ -n "$PREV_GOPHER_ORCH_VERSION" ]; then echo -e " Previous gopher-orch: ${CYAN}v$PREV_GOPHER_ORCH_VERSION${NC}" From b476f194bb074f0afa8b3a7a2b8b636f18405c29 Mon Sep 17 00:00:00 2001 From: RahulHere Date: Thu, 30 Jul 2026 15:49:12 +0800 Subject: [PATCH 26/29] Harden generated release note splicing Summary: - fail release generation when CHANGELOG.md lacks an Unreleased section - preserve manual changelog spacing while populating generated notes - sanitize imported gopher-orch release notes by rewriting PR refs and user mentions - clean up temporary changelog files with a trap Verification: - bash -n dump-version.sh - git diff --check --- dump-version.sh | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/dump-version.sh b/dump-version.sh index f2149a18..954f1cb9 100755 --- a/dump-version.sh +++ b/dump-version.sh @@ -200,12 +200,17 @@ else fi echo -e " New gopher-orch: ${GREEN}v$GOPHER_ORCH_VERSION${NC}" +if ! grep -q '^## \[Unreleased\]' "$CHANGELOG_FILE"; then + echo -e "${RED}Error: [Unreleased] section not found in $CHANGELOG_FILE${NC}" + exit 1 +fi + # Preserve any manually-authored entries already under [Unreleased] MANUAL_CONTENT=$(awk ' /^## \[Unreleased\]/ { capture = 1; next } /^## \[/ && capture { capture = 0 } capture { print } -' "$CHANGELOG_FILE" | sed -e '/^[[:space:]]*$/d') +' "$CHANGELOG_FILE") # Collect Python repo commits since previous tag (skip merges + prior release commits) PY_COMMITS=$(git log --no-merges --pretty=format:'- %s' \ @@ -223,6 +228,8 @@ fi # Extract the "What's Changed" block from the gopher-orch release notes, # stripping the Build Information preamble and the trailing Full Changelog link. +# Rewrite PR refs and user mentions so this repo's rendered changelog does not +# point #NNN at gopher-mcp-python or ping users from the upstream release. GOPHER_ORCH_NOTES=$(gh release view "v$GOPHER_ORCH_VERSION" \ --repo GopherSecurity/gopher-orch \ --json body -q '.body' 2>/dev/null | \ @@ -230,7 +237,10 @@ GOPHER_ORCH_NOTES=$(gh release view "v$GOPHER_ORCH_VERSION" \ /^## What.s Changed/ { capture = 1; next } /^\*\*Full Changelog\*\*/ { capture = 0 } capture { print } - ' | sed -e '/^---$/d') + ' | sed -E \ + -e '/^---$/d' \ + -e 's/#([0-9]+)/https:\/\/github.com\/GopherSecurity\/gopher-orch\/pull\/\1/g' \ + -e 's/@([A-Za-z0-9][A-Za-z0-9-]*)/github.com\/\1/g') if [ -n "$GOPHER_ORCH_NOTES" ]; then echo -e " gopher-orch notes: ${GREEN}fetched${NC}" @@ -240,8 +250,10 @@ fi # Build the new [Unreleased] body RELEASE_NOTES_FILE=$(mktemp) +CHANGELOG_TMP="${CHANGELOG_FILE}.gen" +trap 'rm -f "$RELEASE_NOTES_FILE" "$CHANGELOG_TMP"' EXIT { - if [ -n "$MANUAL_CONTENT" ]; then + if printf '%s' "$MANUAL_CONTENT" | grep -q '[^[:space:]]'; then printf '%s\n\n' "$MANUAL_CONTENT" fi @@ -275,7 +287,6 @@ RELEASE_NOTES_FILE=$(mktemp) # Splice the generated body in: replace everything between # "## [Unreleased]" and the next "## [" with the new content. -CHANGELOG_TMP="${CHANGELOG_FILE}.gen" awk -v notes_file="$RELEASE_NOTES_FILE" ' BEGIN { while ((getline line < notes_file) > 0) { @@ -296,7 +307,6 @@ awk -v notes_file="$RELEASE_NOTES_FILE" ' { print } ' "$CHANGELOG_FILE" > "$CHANGELOG_TMP" mv "$CHANGELOG_TMP" "$CHANGELOG_FILE" -rm -f "$RELEASE_NOTES_FILE" # Recompute UNRELEASED_CONTENT for the eventual commit message UNRELEASED_CONTENT=$(awk ' From cb238eb55e046c48f8ac78f42de5c3688e63e22f Mon Sep 17 00:00:00 2001 From: RahulHere Date: Fri, 31 Jul 2026 14:12:41 +0800 Subject: [PATCH 27/29] Prefer highest release tag on HEAD Summary: - resolve previous Python tag from highest version tag on HEAD first - fall back to git describe when HEAD has no release tag - add regression coverage for deterministic release tag selection --- dump-version.sh | 11 +++++++---- tests/test_dump_version.py | 20 ++++++++++++++++++++ 2 files changed, 27 insertions(+), 4 deletions(-) create mode 100644 tests/test_dump_version.py diff --git a/dump-version.sh b/dump-version.sh index 954f1cb9..46d5ab31 100755 --- a/dump-version.sh +++ b/dump-version.sh @@ -165,10 +165,13 @@ fi # Fetch tags so PREV_TAG resolution is accurate even on shallow clones git fetch --tags --quiet 2>/dev/null || true -# Previous Python tag reachable from HEAD. Using git describe keeps the range -# anchored to this branch instead of picking the highest version tag anywhere in -# the repository. -PREV_TAG=$(git describe --tags --abbrev=0 --match 'v*' HEAD 2>/dev/null || true) +# Previous Python tag reachable from HEAD. Prefer the highest version tag +# directly on HEAD when release tags are stacked on one commit; otherwise use +# git describe to keep the range anchored to this branch. +PREV_TAG=$(git tag --points-at HEAD --list 'v*' --sort=-v:refname | head -1) +if [ -z "$PREV_TAG" ]; then + PREV_TAG=$(git describe --tags --abbrev=0 --match 'v*' HEAD 2>/dev/null || true) +fi if [ -n "$PREV_TAG" ]; then echo -e " Previous Python tag: ${CYAN}$PREV_TAG${NC}" PY_RANGE="$PREV_TAG..HEAD" diff --git a/tests/test_dump_version.py b/tests/test_dump_version.py new file mode 100644 index 00000000..a262820a --- /dev/null +++ b/tests/test_dump_version.py @@ -0,0 +1,20 @@ +"""Regression tests for release version dumping behavior.""" + +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] + + +def test_dump_version_prefers_highest_version_tag_on_head() -> None: + script = (ROOT / "dump-version.sh").read_text() + + points_at = "PREV_TAG=$(git tag --points-at HEAD --list 'v*' --sort=-v:refname | head -1)" + describe = ( + "PREV_TAG=$(git describe --tags --abbrev=0 --match 'v*' HEAD " + "2>/dev/null || true)" + ) + + assert points_at in script + assert describe in script + assert script.index(points_at) < script.index(describe) From e2023107a3abe7fc83877e5a218f8a9046007741 Mon Sep 17 00:00:00 2001 From: RahulHere Date: Fri, 31 Jul 2026 14:16:04 +0800 Subject: [PATCH 28/29] Warn on mismatched release tag versions Summary: - compare previous release tag names against recorded package versions - warn when tag and pyproject versions disagree - clear mismatched previous orch versions to avoid incorrect bump notes - add regression coverage for tag/version mismatch handling --- dump-version.sh | 5 +++++ tests/test_dump_version.py | 8 ++++++++ 2 files changed, 13 insertions(+) diff --git a/dump-version.sh b/dump-version.sh index 46d5ab31..e84f6467 100755 --- a/dump-version.sh +++ b/dump-version.sh @@ -190,6 +190,11 @@ if [ -n "$PREV_TAG" ]; then if echo "$PREV_PY_VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+(\.[0-9]+)?$'; then PREV_GOPHER_ORCH_VERSION=$(echo "$PREV_PY_VERSION" | \ sed -E 's/^([0-9]+\.[0-9]+\.[0-9]+)(\.[0-9]+)?$/\1/') + PREV_TAG_BASE=$(echo "${PREV_TAG#v}" | sed -E 's/^([0-9]+\.[0-9]+\.[0-9]+).*/\1/') + if [ "$PREV_GOPHER_ORCH_VERSION" != "$PREV_TAG_BASE" ]; then + echo -e " ${YELLOW}Warning: $PREV_TAG records package version $PREV_PY_VERSION; tag name and recorded version disagree${NC}" + PREV_GOPHER_ORCH_VERSION="" + fi elif [ -n "$PREV_PY_VERSION" ]; then echo -e " ${YELLOW}Warning: could not derive previous gopher-orch version from $PREV_TAG:$PYPROJECT_TOML version '$PREV_PY_VERSION'${NC}" else diff --git a/tests/test_dump_version.py b/tests/test_dump_version.py index a262820a..ec09169f 100644 --- a/tests/test_dump_version.py +++ b/tests/test_dump_version.py @@ -18,3 +18,11 @@ def test_dump_version_prefers_highest_version_tag_on_head() -> None: assert points_at in script assert describe in script assert script.index(points_at) < script.index(describe) + + +def test_dump_version_warns_when_tag_and_recorded_version_disagree() -> None: + script = (ROOT / "dump-version.sh").read_text() + + assert 'PREV_TAG_BASE=$(echo "${PREV_TAG#v}"' in script + assert "tag name and recorded version disagree" in script + assert 'PREV_GOPHER_ORCH_VERSION=""' in script From c4feb45b9ecfc6b2875676ad58ad2ebadf4d162f Mon Sep 17 00:00:00 2001 From: RahulHere Date: Fri, 31 Jul 2026 14:25:00 +0800 Subject: [PATCH 29/29] Deduplicate API example runners Summary: - Add shared examples/api/_run_common.sh for platform detection, venv setup, SDK/native package install, and example execution. - Refactor all seven API example runner scripts to source the common helper and keep only variant-specific banners and environment warnings. - Remove stale hard-coded SDK_VERSION example text from the API key and JSON runner headers. Verification: - bash -n examples/api/_run_common.sh examples/api/*_run.sh - git diff --check - python3 -m pytest -q --- examples/api/_run_common.sh | 107 +++++++++++++++++++++ examples/api/create_by_api_key_run.sh | 101 +++---------------- examples/api/create_by_gateway_id_run.sh | 102 ++------------------ examples/api/create_by_gateway_name_run.sh | 102 ++------------------ examples/api/create_by_json_run.sh | 92 ++---------------- examples/api/create_by_server_id_run.sh | 102 ++------------------ examples/api/create_by_server_name_run.sh | 102 ++------------------ examples/api/create_by_url_run.sh | 95 ++---------------- 8 files changed, 171 insertions(+), 632 deletions(-) create mode 100644 examples/api/_run_common.sh diff --git a/examples/api/_run_common.sh b/examples/api/_run_common.sh new file mode 100644 index 00000000..733f722e --- /dev/null +++ b/examples/api/_run_common.sh @@ -0,0 +1,107 @@ +#!/bin/bash + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +CYAN='\033[0;36m' +NC='\033[0m' + +SDK_VERSION="${SDK_VERSION:-}" + +detect_platform() { + local os arch + os=$(uname -s | tr '[:upper:]' '[:lower:]') + arch=$(uname -m) + case "$os" in + darwin) PLATFORM="darwin" ;; + linux) PLATFORM="linux" ;; + mingw*|msys*|cygwin*) PLATFORM="win32" ;; + *) echo -e "${RED}Unsupported OS: $os${NC}"; exit 1 ;; + esac + case "$arch" in + x86_64|amd64) ARCH="x64" ;; + arm64|aarch64) ARCH="arm64" ;; + *) echo -e "${RED}Unsupported architecture: $arch${NC}"; exit 1 ;; + esac + NATIVE_PACKAGE="gopher-mcp-python-native-${PLATFORM}-${ARCH}" + echo -e "${CYAN}Detected platform: ${PLATFORM}-${ARCH}${NC}" + echo -e "${CYAN}Native package: ${NATIVE_PACKAGE}${NC}" +} + +print_banner() { + local title="$1" + local border + border="$(printf '%*s' "${#title}" '' | tr ' ' '=')" + echo -e "${GREEN}${border}${NC}" + echo -e "${GREEN}${title}${NC}" + echo -e "${GREEN}${border}${NC}" + echo "" +} + +warn_if_empty() { + local name="$1" + local guidance="$2" + local note="${3:-}" + if [ -z "${!name:-}" ]; then + echo -e "${YELLOW}Warning: ${name} environment variable is not set${NC}" + echo -e "${YELLOW}${guidance}${NC}" + if [ -n "$note" ]; then + echo -e "${YELLOW}${note}${NC}" + fi + echo "" + fi +} + +run_api_example() { + local work_name="$1" + local example_file="$2" + shift 2 + + local work_dir="$SCRIPT_DIR/$work_name" + + echo -e "${YELLOW}Setting up test project at $work_dir...${NC}" + rm -rf "$work_dir" + mkdir -p "$work_dir" + cd "$work_dir" + + echo -e "${YELLOW}Creating virtual environment...${NC}" + python3 -m venv venv + + local activate_script="" + if [ -x "venv/bin/python" ] && [ -f "venv/bin/activate" ]; then + activate_script="venv/bin/activate" + elif [ -x "venv/Scripts/python.exe" ] && [ -f "venv/Scripts/activate" ]; then + activate_script="venv/Scripts/activate" + fi + if [ -z "$activate_script" ]; then + echo -e "${RED}Error: virtualenv creation did not produce a usable Python.${NC}" + echo -e "${YELLOW}Install python3-venv and python3-pip, then rerun this script.${NC}" + exit 1 + fi + + # shellcheck disable=SC1090 + source "$activate_script" + + echo -e "${YELLOW}Installing gopher-mcp-python from PyPI...${NC}" + if [ -n "$SDK_VERSION" ]; then + echo -e "${CYAN}Installing version: $SDK_VERSION${NC}" + python -m pip install --quiet "gopher-mcp-python==$SDK_VERSION" \ + "${NATIVE_PACKAGE}==$SDK_VERSION" + else + echo -e "${CYAN}Installing latest published version${NC}" + python -m pip install --quiet gopher-mcp-python "$NATIVE_PACKAGE" + fi + + echo -e "${CYAN}Installed packages:${NC}" + python -m pip list | grep -i gopher || true + + cp "$SCRIPT_DIR/$example_file" . + + echo "" + echo -e "${YELLOW}Running example...${NC}" + echo "" + python "$example_file" "$@" + + echo "" + echo -e "${GREEN}Example completed${NC}" +} diff --git a/examples/api/create_by_api_key_run.sh b/examples/api/create_by_api_key_run.sh index 3dfd1a97..9f88bf4a 100755 --- a/examples/api/create_by_api_key_run.sh +++ b/examples/api/create_by_api_key_run.sh @@ -1,101 +1,22 @@ #!/bin/bash -# Run the Python SDK example for GopherAgent.create_with_api_key against -# the PyPI-published gopher-mcp-python package. Bootstraps a fresh venv, -# installs the SDK plus the matching platform native package from PyPI, -# then runs the example. +# Run the Python SDK example for GopherAgent.create_with_api_key +# against the PyPI-published gopher-mcp-python package. # -# Set SDK_VERSION to pin to a specific release (e.g. SDK_VERSION=0.1.21); -# otherwise the latest published version is installed. +# Set SDK_VERSION to pin to a specific release; otherwise the latest +# published version is installed. set -e -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -CYAN='\033[0;36m' -NC='\033[0m' - SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -WORK_DIR="$SCRIPT_DIR/test-project-create-by-api-key" -SDK_VERSION="${SDK_VERSION:-}" - -detect_platform() { - local os arch - os=$(uname -s | tr '[:upper:]' '[:lower:]') - arch=$(uname -m) - case "$os" in - darwin) PLATFORM="darwin" ;; - linux) PLATFORM="linux" ;; - mingw*|msys*|cygwin*) PLATFORM="win32" ;; - *) echo -e "${RED}Unsupported OS: $os${NC}"; exit 1 ;; - esac - case "$arch" in - x86_64|amd64) ARCH="x64" ;; - arm64|aarch64) ARCH="arm64" ;; - *) echo -e "${RED}Unsupported architecture: $arch${NC}"; exit 1 ;; - esac - NATIVE_PACKAGE="gopher-mcp-python-native-${PLATFORM}-${ARCH}" - echo -e "${CYAN}Detected platform: ${PLATFORM}-${ARCH}${NC}" - echo -e "${CYAN}Native package: ${NATIVE_PACKAGE}${NC}" -} +# shellcheck source=examples/api/_run_common.sh +source "$SCRIPT_DIR/_run_common.sh" detect_platform +print_banner "GopherAgent.create_with_api_key example" -echo -e "${GREEN}=========================================${NC}" -echo -e "${GREEN}GopherAgent.create_with_api_key example${NC}" -echo -e "${GREEN}=========================================${NC}" -echo "" - -if [ -z "$GOPHER_API_KEY" ]; then - echo -e "${YELLOW}Warning: GOPHER_API_KEY environment variable is not set${NC}" - echo -e "${YELLOW}Set it with: export GOPHER_API_KEY=your_api_key${NC}" - echo "" -fi - -if [ -z "$LLM_MODEL" ]; then - echo -e "${YELLOW}Warning: LLM_MODEL environment variable is not set${NC}" - echo -e "${YELLOW}Set it with: export LLM_MODEL=${NC}" - echo "" -fi - -if [ -z "$ANTHROPIC_API_KEY" ]; then - echo -e "${YELLOW}Warning: ANTHROPIC_API_KEY environment variable is not set${NC}" - echo -e "${YELLOW}(Required for the default AnthropicProvider.)${NC}" - echo "" -fi - -echo -e "${YELLOW}Setting up test project at $WORK_DIR...${NC}" -rm -rf "$WORK_DIR" -mkdir -p "$WORK_DIR" -cd "$WORK_DIR" - -echo -e "${YELLOW}Creating virtual environment...${NC}" -python3 -m venv venv -# shellcheck disable=SC1091 -source venv/bin/activate - -echo -e "${YELLOW}Installing gopher-mcp-python from PyPI...${NC}" -if [ -n "$SDK_VERSION" ]; then - echo -e "${CYAN}Installing version: $SDK_VERSION${NC}" - pip install --quiet "gopher-mcp-python==$SDK_VERSION" \ - "${NATIVE_PACKAGE}==$SDK_VERSION" -else - echo -e "${CYAN}Installing latest published version${NC}" - pip install --quiet gopher-mcp-python "$NATIVE_PACKAGE" -fi - -echo -e "${CYAN}Installed packages:${NC}" -pip list | grep -i gopher || true - -cp "$SCRIPT_DIR/create_by_api_key.py" . - -echo "" -echo -e "${YELLOW}Running example...${NC}" -echo "" -python create_by_api_key.py "$@" - -echo "" -echo -e "${GREEN}Example completed${NC}" +warn_if_empty "GOPHER_API_KEY" "Set it with: export GOPHER_API_KEY=your_api_key" +warn_if_empty "LLM_MODEL" "Set it with: export LLM_MODEL=" +warn_if_empty "ANTHROPIC_API_KEY" "(Required for the default AnthropicProvider.)" -exit 0 +run_api_example "test-project-create-by-api-key" "create_by_api_key.py" "$@" diff --git a/examples/api/create_by_gateway_id_run.sh b/examples/api/create_by_gateway_id_run.sh index 65f52e20..9a167cb4 100755 --- a/examples/api/create_by_gateway_id_run.sh +++ b/examples/api/create_by_gateway_id_run.sh @@ -1,9 +1,7 @@ #!/bin/bash # Run the Python SDK example for GopherAgent.create_with_gateway_id -# against the PyPI-published gopher-mcp-python package. Bootstraps a -# fresh venv, installs the SDK plus the matching platform native package -# from PyPI, then runs the example. +# against the PyPI-published gopher-mcp-python package. # # Set SDK_VERSION to pin to a specific release; otherwise the latest # published version is installed. The routing factories require a @@ -11,98 +9,16 @@ set -e -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -CYAN='\033[0;36m' -NC='\033[0m' - SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -WORK_DIR="$SCRIPT_DIR/test-project-create-by-gateway-id" -SDK_VERSION="${SDK_VERSION:-}" - -detect_platform() { - local os arch - os=$(uname -s | tr '[:upper:]' '[:lower:]') - arch=$(uname -m) - case "$os" in - darwin) PLATFORM="darwin" ;; - linux) PLATFORM="linux" ;; - mingw*|msys*|cygwin*) PLATFORM="win32" ;; - *) echo -e "${RED}Unsupported OS: $os${NC}"; exit 1 ;; - esac - case "$arch" in - x86_64|amd64) ARCH="x64" ;; - arm64|aarch64) ARCH="arm64" ;; - *) echo -e "${RED}Unsupported architecture: $arch${NC}"; exit 1 ;; - esac - NATIVE_PACKAGE="gopher-mcp-python-native-${PLATFORM}-${ARCH}" - echo -e "${CYAN}Detected platform: ${PLATFORM}-${ARCH}${NC}" - echo -e "${CYAN}Native package: ${NATIVE_PACKAGE}${NC}" -} +# shellcheck source=examples/api/_run_common.sh +source "$SCRIPT_DIR/_run_common.sh" detect_platform +print_banner "GopherAgent.create_with_gateway_id example" -echo -e "${GREEN}===========================================${NC}" -echo -e "${GREEN}GopherAgent.create_with_gateway_id example${NC}" -echo -e "${GREEN}===========================================${NC}" -echo "" - -if [ -z "$GOPHER_API_KEY" ]; then - echo -e "${YELLOW}Warning: GOPHER_API_KEY environment variable is not set${NC}" - echo -e "${YELLOW}Set it with: export GOPHER_API_KEY=your_api_key${NC}" - echo "" -fi - -if [ -z "$GOPHER_MCP_GATEWAY_ID" ]; then - echo -e "${YELLOW}Warning: GOPHER_MCP_GATEWAY_ID environment variable is not set${NC}" - echo -e "${YELLOW}Set it with: export GOPHER_MCP_GATEWAY_ID=gw-...${NC}" - echo "" -fi - -if [ -z "$LLM_MODEL" ]; then - echo -e "${YELLOW}Warning: LLM_MODEL environment variable is not set${NC}" - echo -e "${YELLOW}Set it with: export LLM_MODEL=${NC}" - echo "" -fi - -if [ -z "$ANTHROPIC_API_KEY" ]; then - echo -e "${YELLOW}Warning: ANTHROPIC_API_KEY environment variable is not set${NC}" - echo -e "${YELLOW}(Required for the default AnthropicProvider.)${NC}" - echo "" -fi - -echo -e "${YELLOW}Setting up test project at $WORK_DIR...${NC}" -rm -rf "$WORK_DIR" -mkdir -p "$WORK_DIR" -cd "$WORK_DIR" - -echo -e "${YELLOW}Creating virtual environment...${NC}" -python3 -m venv venv -# shellcheck disable=SC1091 -source venv/bin/activate - -echo -e "${YELLOW}Installing gopher-mcp-python from PyPI...${NC}" -if [ -n "$SDK_VERSION" ]; then - echo -e "${CYAN}Installing version: $SDK_VERSION${NC}" - pip install --quiet "gopher-mcp-python==$SDK_VERSION" \ - "${NATIVE_PACKAGE}==$SDK_VERSION" -else - echo -e "${CYAN}Installing latest published version${NC}" - pip install --quiet gopher-mcp-python "$NATIVE_PACKAGE" -fi - -echo -e "${CYAN}Installed packages:${NC}" -pip list | grep -i gopher || true - -cp "$SCRIPT_DIR/create_by_gateway_id.py" . - -echo "" -echo -e "${YELLOW}Running example...${NC}" -echo "" -python create_by_gateway_id.py "$@" - -echo "" -echo -e "${GREEN}Example completed${NC}" +warn_if_empty "GOPHER_API_KEY" "Set it with: export GOPHER_API_KEY=your_api_key" +warn_if_empty "GOPHER_MCP_GATEWAY_ID" "Set it with: export GOPHER_MCP_GATEWAY_ID=gw-..." +warn_if_empty "LLM_MODEL" "Set it with: export LLM_MODEL=" +warn_if_empty "ANTHROPIC_API_KEY" "(Required for the default AnthropicProvider.)" -exit 0 +run_api_example "test-project-create-by-gateway-id" "create_by_gateway_id.py" "$@" diff --git a/examples/api/create_by_gateway_name_run.sh b/examples/api/create_by_gateway_name_run.sh index 74af6441..101e66f1 100755 --- a/examples/api/create_by_gateway_name_run.sh +++ b/examples/api/create_by_gateway_name_run.sh @@ -1,9 +1,7 @@ #!/bin/bash # Run the Python SDK example for GopherAgent.create_with_gateway_name -# against the PyPI-published gopher-mcp-python package. Bootstraps a -# fresh venv, installs the SDK plus the matching platform native package -# from PyPI, then runs the example. +# against the PyPI-published gopher-mcp-python package. # # Set SDK_VERSION to pin to a specific release; otherwise the latest # published version is installed. The routing factories require a @@ -11,98 +9,16 @@ set -e -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -CYAN='\033[0;36m' -NC='\033[0m' - SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -WORK_DIR="$SCRIPT_DIR/test-project-create-by-gateway-name" -SDK_VERSION="${SDK_VERSION:-}" - -detect_platform() { - local os arch - os=$(uname -s | tr '[:upper:]' '[:lower:]') - arch=$(uname -m) - case "$os" in - darwin) PLATFORM="darwin" ;; - linux) PLATFORM="linux" ;; - mingw*|msys*|cygwin*) PLATFORM="win32" ;; - *) echo -e "${RED}Unsupported OS: $os${NC}"; exit 1 ;; - esac - case "$arch" in - x86_64|amd64) ARCH="x64" ;; - arm64|aarch64) ARCH="arm64" ;; - *) echo -e "${RED}Unsupported architecture: $arch${NC}"; exit 1 ;; - esac - NATIVE_PACKAGE="gopher-mcp-python-native-${PLATFORM}-${ARCH}" - echo -e "${CYAN}Detected platform: ${PLATFORM}-${ARCH}${NC}" - echo -e "${CYAN}Native package: ${NATIVE_PACKAGE}${NC}" -} +# shellcheck source=examples/api/_run_common.sh +source "$SCRIPT_DIR/_run_common.sh" detect_platform +print_banner "GopherAgent.create_with_gateway_name example" -echo -e "${GREEN}=============================================${NC}" -echo -e "${GREEN}GopherAgent.create_with_gateway_name example${NC}" -echo -e "${GREEN}=============================================${NC}" -echo "" - -if [ -z "$GOPHER_API_KEY" ]; then - echo -e "${YELLOW}Warning: GOPHER_API_KEY environment variable is not set${NC}" - echo -e "${YELLOW}Set it with: export GOPHER_API_KEY=your_api_key${NC}" - echo "" -fi - -if [ -z "$GOPHER_MCP_GATEWAY_NAME" ]; then - echo -e "${YELLOW}Warning: GOPHER_MCP_GATEWAY_NAME environment variable is not set${NC}" - echo -e "${YELLOW}Set it with: export GOPHER_MCP_GATEWAY_NAME=my-gateway${NC}" - echo "" -fi - -if [ -z "$LLM_MODEL" ]; then - echo -e "${YELLOW}Warning: LLM_MODEL environment variable is not set${NC}" - echo -e "${YELLOW}Set it with: export LLM_MODEL=${NC}" - echo "" -fi - -if [ -z "$ANTHROPIC_API_KEY" ]; then - echo -e "${YELLOW}Warning: ANTHROPIC_API_KEY environment variable is not set${NC}" - echo -e "${YELLOW}(Required for the default AnthropicProvider.)${NC}" - echo "" -fi - -echo -e "${YELLOW}Setting up test project at $WORK_DIR...${NC}" -rm -rf "$WORK_DIR" -mkdir -p "$WORK_DIR" -cd "$WORK_DIR" - -echo -e "${YELLOW}Creating virtual environment...${NC}" -python3 -m venv venv -# shellcheck disable=SC1091 -source venv/bin/activate - -echo -e "${YELLOW}Installing gopher-mcp-python from PyPI...${NC}" -if [ -n "$SDK_VERSION" ]; then - echo -e "${CYAN}Installing version: $SDK_VERSION${NC}" - pip install --quiet "gopher-mcp-python==$SDK_VERSION" \ - "${NATIVE_PACKAGE}==$SDK_VERSION" -else - echo -e "${CYAN}Installing latest published version${NC}" - pip install --quiet gopher-mcp-python "$NATIVE_PACKAGE" -fi - -echo -e "${CYAN}Installed packages:${NC}" -pip list | grep -i gopher || true - -cp "$SCRIPT_DIR/create_by_gateway_name.py" . - -echo "" -echo -e "${YELLOW}Running example...${NC}" -echo "" -python create_by_gateway_name.py "$@" - -echo "" -echo -e "${GREEN}Example completed${NC}" +warn_if_empty "GOPHER_API_KEY" "Set it with: export GOPHER_API_KEY=your_api_key" +warn_if_empty "GOPHER_MCP_GATEWAY_NAME" "Set it with: export GOPHER_MCP_GATEWAY_NAME=my-gateway" +warn_if_empty "LLM_MODEL" "Set it with: export LLM_MODEL=" +warn_if_empty "ANTHROPIC_API_KEY" "(Required for the default AnthropicProvider.)" -exit 0 +run_api_example "test-project-create-by-gateway-name" "create_by_gateway_name.py" "$@" diff --git a/examples/api/create_by_json_run.sh b/examples/api/create_by_json_run.sh index 1ee37e85..1b61a5e6 100755 --- a/examples/api/create_by_json_run.sh +++ b/examples/api/create_by_json_run.sh @@ -1,95 +1,21 @@ #!/bin/bash # Run the Python SDK example for GopherAgent.create_with_server_config -# against the PyPI-published gopher-mcp-python package. Bootstraps a -# fresh venv, installs the SDK plus the matching platform native package -# from PyPI, then runs the example. +# against the PyPI-published gopher-mcp-python package. # -# Set SDK_VERSION to pin to a specific release (e.g. SDK_VERSION=0.1.21); -# otherwise the latest published version is installed. +# Set SDK_VERSION to pin to a specific release; otherwise the latest +# published version is installed. set -e -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -CYAN='\033[0;36m' -NC='\033[0m' - SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -WORK_DIR="$SCRIPT_DIR/test-project-create-by-json" -SDK_VERSION="${SDK_VERSION:-}" - -detect_platform() { - local os arch - os=$(uname -s | tr '[:upper:]' '[:lower:]') - arch=$(uname -m) - case "$os" in - darwin) PLATFORM="darwin" ;; - linux) PLATFORM="linux" ;; - mingw*|msys*|cygwin*) PLATFORM="win32" ;; - *) echo -e "${RED}Unsupported OS: $os${NC}"; exit 1 ;; - esac - case "$arch" in - x86_64|amd64) ARCH="x64" ;; - arm64|aarch64) ARCH="arm64" ;; - *) echo -e "${RED}Unsupported architecture: $arch${NC}"; exit 1 ;; - esac - NATIVE_PACKAGE="gopher-mcp-python-native-${PLATFORM}-${ARCH}" - echo -e "${CYAN}Detected platform: ${PLATFORM}-${ARCH}${NC}" - echo -e "${CYAN}Native package: ${NATIVE_PACKAGE}${NC}" -} +# shellcheck source=examples/api/_run_common.sh +source "$SCRIPT_DIR/_run_common.sh" detect_platform +print_banner "GopherAgent.create_with_server_config example" -echo -e "${GREEN}===============================================${NC}" -echo -e "${GREEN}GopherAgent.create_with_server_config example${NC}" -echo -e "${GREEN}===============================================${NC}" -echo "" - -if [ -z "$LLM_MODEL" ]; then - echo -e "${YELLOW}Warning: LLM_MODEL environment variable is not set${NC}" - echo -e "${YELLOW}Set it with: export LLM_MODEL=${NC}" - echo "" -fi - -if [ -z "$ANTHROPIC_API_KEY" ]; then - echo -e "${YELLOW}Warning: ANTHROPIC_API_KEY environment variable is not set${NC}" - echo -e "${YELLOW}(Required for the default AnthropicProvider.)${NC}" - echo "" -fi - -echo -e "${YELLOW}Setting up test project at $WORK_DIR...${NC}" -rm -rf "$WORK_DIR" -mkdir -p "$WORK_DIR" -cd "$WORK_DIR" - -echo -e "${YELLOW}Creating virtual environment...${NC}" -python3 -m venv venv -# shellcheck disable=SC1091 -source venv/bin/activate - -echo -e "${YELLOW}Installing gopher-mcp-python from PyPI...${NC}" -if [ -n "$SDK_VERSION" ]; then - echo -e "${CYAN}Installing version: $SDK_VERSION${NC}" - pip install --quiet "gopher-mcp-python==$SDK_VERSION" \ - "${NATIVE_PACKAGE}==$SDK_VERSION" -else - echo -e "${CYAN}Installing latest published version${NC}" - pip install --quiet gopher-mcp-python "$NATIVE_PACKAGE" -fi - -echo -e "${CYAN}Installed packages:${NC}" -pip list | grep -i gopher || true - -cp "$SCRIPT_DIR/create_by_json.py" . - -echo "" -echo -e "${YELLOW}Running example...${NC}" -echo "" -python create_by_json.py "$@" - -echo "" -echo -e "${GREEN}Example completed${NC}" +warn_if_empty "LLM_MODEL" "Set it with: export LLM_MODEL=" +warn_if_empty "ANTHROPIC_API_KEY" "(Required for the default AnthropicProvider.)" -exit 0 +run_api_example "test-project-create-by-json" "create_by_json.py" "$@" diff --git a/examples/api/create_by_server_id_run.sh b/examples/api/create_by_server_id_run.sh index d4539cbe..1aacabde 100755 --- a/examples/api/create_by_server_id_run.sh +++ b/examples/api/create_by_server_id_run.sh @@ -1,9 +1,7 @@ #!/bin/bash # Run the Python SDK example for GopherAgent.create_with_server_id -# against the PyPI-published gopher-mcp-python package. Bootstraps a -# fresh venv, installs the SDK plus the matching platform native package -# from PyPI, then runs the example. +# against the PyPI-published gopher-mcp-python package. # # Set SDK_VERSION to pin to a specific release; otherwise the latest # published version is installed. The routing factories require a @@ -11,98 +9,16 @@ set -e -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -CYAN='\033[0;36m' -NC='\033[0m' - SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -WORK_DIR="$SCRIPT_DIR/test-project-create-by-server-id" -SDK_VERSION="${SDK_VERSION:-}" - -detect_platform() { - local os arch - os=$(uname -s | tr '[:upper:]' '[:lower:]') - arch=$(uname -m) - case "$os" in - darwin) PLATFORM="darwin" ;; - linux) PLATFORM="linux" ;; - mingw*|msys*|cygwin*) PLATFORM="win32" ;; - *) echo -e "${RED}Unsupported OS: $os${NC}"; exit 1 ;; - esac - case "$arch" in - x86_64|amd64) ARCH="x64" ;; - arm64|aarch64) ARCH="arm64" ;; - *) echo -e "${RED}Unsupported architecture: $arch${NC}"; exit 1 ;; - esac - NATIVE_PACKAGE="gopher-mcp-python-native-${PLATFORM}-${ARCH}" - echo -e "${CYAN}Detected platform: ${PLATFORM}-${ARCH}${NC}" - echo -e "${CYAN}Native package: ${NATIVE_PACKAGE}${NC}" -} +# shellcheck source=examples/api/_run_common.sh +source "$SCRIPT_DIR/_run_common.sh" detect_platform +print_banner "GopherAgent.create_with_server_id example" -echo -e "${GREEN}==========================================${NC}" -echo -e "${GREEN}GopherAgent.create_with_server_id example${NC}" -echo -e "${GREEN}==========================================${NC}" -echo "" - -if [ -z "$GOPHER_API_KEY" ]; then - echo -e "${YELLOW}Warning: GOPHER_API_KEY environment variable is not set${NC}" - echo -e "${YELLOW}Set it with: export GOPHER_API_KEY=your_api_key${NC}" - echo "" -fi - -if [ -z "$GOPHER_MCP_SERVER_ID" ]; then - echo -e "${YELLOW}Warning: GOPHER_MCP_SERVER_ID environment variable is not set${NC}" - echo -e "${YELLOW}Set it with: export GOPHER_MCP_SERVER_ID=srv-...${NC}" - echo "" -fi - -if [ -z "$LLM_MODEL" ]; then - echo -e "${YELLOW}Warning: LLM_MODEL environment variable is not set${NC}" - echo -e "${YELLOW}Set it with: export LLM_MODEL=${NC}" - echo "" -fi - -if [ -z "$ANTHROPIC_API_KEY" ]; then - echo -e "${YELLOW}Warning: ANTHROPIC_API_KEY environment variable is not set${NC}" - echo -e "${YELLOW}(Required for the default AnthropicProvider.)${NC}" - echo "" -fi - -echo -e "${YELLOW}Setting up test project at $WORK_DIR...${NC}" -rm -rf "$WORK_DIR" -mkdir -p "$WORK_DIR" -cd "$WORK_DIR" - -echo -e "${YELLOW}Creating virtual environment...${NC}" -python3 -m venv venv -# shellcheck disable=SC1091 -source venv/bin/activate - -echo -e "${YELLOW}Installing gopher-mcp-python from PyPI...${NC}" -if [ -n "$SDK_VERSION" ]; then - echo -e "${CYAN}Installing version: $SDK_VERSION${NC}" - pip install --quiet "gopher-mcp-python==$SDK_VERSION" \ - "${NATIVE_PACKAGE}==$SDK_VERSION" -else - echo -e "${CYAN}Installing latest published version${NC}" - pip install --quiet gopher-mcp-python "$NATIVE_PACKAGE" -fi - -echo -e "${CYAN}Installed packages:${NC}" -pip list | grep -i gopher || true - -cp "$SCRIPT_DIR/create_by_server_id.py" . - -echo "" -echo -e "${YELLOW}Running example...${NC}" -echo "" -python create_by_server_id.py "$@" - -echo "" -echo -e "${GREEN}Example completed${NC}" +warn_if_empty "GOPHER_API_KEY" "Set it with: export GOPHER_API_KEY=your_api_key" +warn_if_empty "GOPHER_MCP_SERVER_ID" "Set it with: export GOPHER_MCP_SERVER_ID=srv-..." +warn_if_empty "LLM_MODEL" "Set it with: export LLM_MODEL=" +warn_if_empty "ANTHROPIC_API_KEY" "(Required for the default AnthropicProvider.)" -exit 0 +run_api_example "test-project-create-by-server-id" "create_by_server_id.py" "$@" diff --git a/examples/api/create_by_server_name_run.sh b/examples/api/create_by_server_name_run.sh index e8015779..04fa44ba 100755 --- a/examples/api/create_by_server_name_run.sh +++ b/examples/api/create_by_server_name_run.sh @@ -1,9 +1,7 @@ #!/bin/bash # Run the Python SDK example for GopherAgent.create_with_server_name -# against the PyPI-published gopher-mcp-python package. Bootstraps a -# fresh venv, installs the SDK plus the matching platform native package -# from PyPI, then runs the example. +# against the PyPI-published gopher-mcp-python package. # # Set SDK_VERSION to pin to a specific release; otherwise the latest # published version is installed. The routing factories require a @@ -11,98 +9,16 @@ set -e -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -CYAN='\033[0;36m' -NC='\033[0m' - SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -WORK_DIR="$SCRIPT_DIR/test-project-create-by-server-name" -SDK_VERSION="${SDK_VERSION:-}" - -detect_platform() { - local os arch - os=$(uname -s | tr '[:upper:]' '[:lower:]') - arch=$(uname -m) - case "$os" in - darwin) PLATFORM="darwin" ;; - linux) PLATFORM="linux" ;; - mingw*|msys*|cygwin*) PLATFORM="win32" ;; - *) echo -e "${RED}Unsupported OS: $os${NC}"; exit 1 ;; - esac - case "$arch" in - x86_64|amd64) ARCH="x64" ;; - arm64|aarch64) ARCH="arm64" ;; - *) echo -e "${RED}Unsupported architecture: $arch${NC}"; exit 1 ;; - esac - NATIVE_PACKAGE="gopher-mcp-python-native-${PLATFORM}-${ARCH}" - echo -e "${CYAN}Detected platform: ${PLATFORM}-${ARCH}${NC}" - echo -e "${CYAN}Native package: ${NATIVE_PACKAGE}${NC}" -} +# shellcheck source=examples/api/_run_common.sh +source "$SCRIPT_DIR/_run_common.sh" detect_platform +print_banner "GopherAgent.create_with_server_name example" -echo -e "${GREEN}============================================${NC}" -echo -e "${GREEN}GopherAgent.create_with_server_name example${NC}" -echo -e "${GREEN}============================================${NC}" -echo "" - -if [ -z "$GOPHER_API_KEY" ]; then - echo -e "${YELLOW}Warning: GOPHER_API_KEY environment variable is not set${NC}" - echo -e "${YELLOW}Set it with: export GOPHER_API_KEY=your_api_key${NC}" - echo "" -fi - -if [ -z "$GOPHER_MCP_SERVER_NAME" ]; then - echo -e "${YELLOW}Warning: GOPHER_MCP_SERVER_NAME environment variable is not set${NC}" - echo -e "${YELLOW}Set it with: export GOPHER_MCP_SERVER_NAME=my-server${NC}" - echo "" -fi - -if [ -z "$LLM_MODEL" ]; then - echo -e "${YELLOW}Warning: LLM_MODEL environment variable is not set${NC}" - echo -e "${YELLOW}Set it with: export LLM_MODEL=${NC}" - echo "" -fi - -if [ -z "$ANTHROPIC_API_KEY" ]; then - echo -e "${YELLOW}Warning: ANTHROPIC_API_KEY environment variable is not set${NC}" - echo -e "${YELLOW}(Required for the default AnthropicProvider.)${NC}" - echo "" -fi - -echo -e "${YELLOW}Setting up test project at $WORK_DIR...${NC}" -rm -rf "$WORK_DIR" -mkdir -p "$WORK_DIR" -cd "$WORK_DIR" - -echo -e "${YELLOW}Creating virtual environment...${NC}" -python3 -m venv venv -# shellcheck disable=SC1091 -source venv/bin/activate - -echo -e "${YELLOW}Installing gopher-mcp-python from PyPI...${NC}" -if [ -n "$SDK_VERSION" ]; then - echo -e "${CYAN}Installing version: $SDK_VERSION${NC}" - pip install --quiet "gopher-mcp-python==$SDK_VERSION" \ - "${NATIVE_PACKAGE}==$SDK_VERSION" -else - echo -e "${CYAN}Installing latest published version${NC}" - pip install --quiet gopher-mcp-python "$NATIVE_PACKAGE" -fi - -echo -e "${CYAN}Installed packages:${NC}" -pip list | grep -i gopher || true - -cp "$SCRIPT_DIR/create_by_server_name.py" . - -echo "" -echo -e "${YELLOW}Running example...${NC}" -echo "" -python create_by_server_name.py "$@" - -echo "" -echo -e "${GREEN}Example completed${NC}" +warn_if_empty "GOPHER_API_KEY" "Set it with: export GOPHER_API_KEY=your_api_key" +warn_if_empty "GOPHER_MCP_SERVER_NAME" "Set it with: export GOPHER_MCP_SERVER_NAME=my-server" +warn_if_empty "LLM_MODEL" "Set it with: export LLM_MODEL=" +warn_if_empty "ANTHROPIC_API_KEY" "(Required for the default AnthropicProvider.)" -exit 0 +run_api_example "test-project-create-by-server-name" "create_by_server_name.py" "$@" diff --git a/examples/api/create_by_url_run.sh b/examples/api/create_by_url_run.sh index e24bc12f..0aae9406 100755 --- a/examples/api/create_by_url_run.sh +++ b/examples/api/create_by_url_run.sh @@ -1,9 +1,7 @@ #!/bin/bash # Run the Python SDK example for GopherAgent.create_with_url against the -# PyPI-published gopher-mcp-python package. Bootstraps a fresh venv, -# installs the SDK plus the matching platform native package from PyPI, -# then runs the example. +# PyPI-published gopher-mcp-python package. # # Set SDK_VERSION to pin to a specific release; otherwise the latest # published version is installed. create_with_url requires a release @@ -11,92 +9,15 @@ set -e -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -CYAN='\033[0;36m' -NC='\033[0m' - SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -WORK_DIR="$SCRIPT_DIR/test-project-create-by-url" -SDK_VERSION="${SDK_VERSION:-}" - -detect_platform() { - local os arch - os=$(uname -s | tr '[:upper:]' '[:lower:]') - arch=$(uname -m) - case "$os" in - darwin) PLATFORM="darwin" ;; - linux) PLATFORM="linux" ;; - mingw*|msys*|cygwin*) PLATFORM="win32" ;; - *) echo -e "${RED}Unsupported OS: $os${NC}"; exit 1 ;; - esac - case "$arch" in - x86_64|amd64) ARCH="x64" ;; - arm64|aarch64) ARCH="arm64" ;; - *) echo -e "${RED}Unsupported architecture: $arch${NC}"; exit 1 ;; - esac - NATIVE_PACKAGE="gopher-mcp-python-native-${PLATFORM}-${ARCH}" - echo -e "${CYAN}Detected platform: ${PLATFORM}-${ARCH}${NC}" - echo -e "${CYAN}Native package: ${NATIVE_PACKAGE}${NC}" -} +# shellcheck source=examples/api/_run_common.sh +source "$SCRIPT_DIR/_run_common.sh" detect_platform +print_banner "GopherAgent.create_with_url example" -echo -e "${GREEN}=====================================${NC}" -echo -e "${GREEN}GopherAgent.create_with_url example${NC}" -echo -e "${GREEN}=====================================${NC}" -echo "" - -if [ -z "$GOPHER_MCP_URL" ]; then - echo -e "${YELLOW}Warning: GOPHER_MCP_URL environment variable is not set${NC}" - echo -e "${YELLOW}Set it with: export GOPHER_MCP_URL=http://127.0.0.1:8080/mcp${NC}" - echo "" -fi - -if [ -z "$LLM_MODEL" ]; then - echo -e "${YELLOW}Warning: LLM_MODEL environment variable is not set${NC}" - echo -e "${YELLOW}Set it with: export LLM_MODEL=${NC}" - echo "" -fi - -if [ -z "$ANTHROPIC_API_KEY" ]; then - echo -e "${YELLOW}Warning: ANTHROPIC_API_KEY environment variable is not set${NC}" - echo -e "${YELLOW}(Required for the default AnthropicProvider.)${NC}" - echo "" -fi - -echo -e "${YELLOW}Setting up test project at $WORK_DIR...${NC}" -rm -rf "$WORK_DIR" -mkdir -p "$WORK_DIR" -cd "$WORK_DIR" - -echo -e "${YELLOW}Creating virtual environment...${NC}" -python3 -m venv venv -# shellcheck disable=SC1091 -source venv/bin/activate - -echo -e "${YELLOW}Installing gopher-mcp-python from PyPI...${NC}" -if [ -n "$SDK_VERSION" ]; then - echo -e "${CYAN}Installing version: $SDK_VERSION${NC}" - pip install --quiet "gopher-mcp-python==$SDK_VERSION" \ - "${NATIVE_PACKAGE}==$SDK_VERSION" -else - echo -e "${CYAN}Installing latest published version${NC}" - pip install --quiet gopher-mcp-python "$NATIVE_PACKAGE" -fi - -echo -e "${CYAN}Installed packages:${NC}" -pip list | grep -i gopher || true - -cp "$SCRIPT_DIR/create_by_url.py" . - -echo "" -echo -e "${YELLOW}Running example...${NC}" -echo "" -python create_by_url.py "$@" - -echo "" -echo -e "${GREEN}Example completed${NC}" +warn_if_empty "GOPHER_MCP_URL" "Set it with: export GOPHER_MCP_URL=http://127.0.0.1:8080/mcp" +warn_if_empty "LLM_MODEL" "Set it with: export LLM_MODEL=" +warn_if_empty "ANTHROPIC_API_KEY" "(Required for the default AnthropicProvider.)" -exit 0 +run_api_example "test-project-create-by-url" "create_by_url.py" "$@"