Implement new SDK APIs#10
Open
bettercallsaulj wants to merge 18 commits into
Open
Conversation
added 18 commits
June 17, 2026 22:25
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).
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.
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.
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).
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.
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.
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.
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.
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.
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.
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).
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.
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.
…#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/<file>.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.
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.
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.
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.