Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 47 additions & 22 deletions build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ NC='\033[0m' # No Color
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
NATIVE_DIR="${SCRIPT_DIR}/third_party/gopher-orch"
BUILD_DIR="${NATIVE_DIR}/build"
SOURCE_STAMP_FILE="${SCRIPT_DIR}/native/.gopher-orch-source"

# Handle --clean flag (cleans CMake cache but preserves _deps)
if [ "$1" = "--clean" ]; then
Expand Down Expand Up @@ -47,10 +48,12 @@ fi

# Configure SSH URL rewrite for GopherSecurity repos
# Clear any existing rewrites first to avoid conflicts
git config --local --unset-all url."[email protected]:GopherSecurity/".insteadOf 2>/dev/null || true
git config --local --unset-all url."git@${SSH_HOST}:GopherSecurity/".insteadOf 2>/dev/null || true
# Set up URL rewrites - both https and default [email protected] should map to custom SSH host
git config --local --add url."git@${SSH_HOST}:GopherSecurity/".insteadOf "https://github.com/GopherSecurity/"
git config --local --add url."git@${SSH_HOST}:GopherSecurity/".insteadOf "[email protected]:GopherSecurity/"
git submodule sync -- third_party/gopher-orch
git config --local submodule.third_party/gopher-orch.url "git@${SSH_HOST}:GopherSecurity/gopher-orch.git"

# Check if submodule directory exists but is empty/broken (missing CMakeLists.txt)
Expand All @@ -62,30 +65,37 @@ if [ -d "${NATIVE_DIR}" ] && [ ! -f "${NATIVE_DIR}/CMakeLists.txt" ]; then
rm -rf .git/modules/third_party/gopher-orch 2>/dev/null || true
fi

# Update main submodule
# First try with recorded commit, if that fails (commit doesn't exist), use --remote to get latest
if ! git submodule update --init third_party/gopher-orch 2>/dev/null; then
echo -e "${YELLOW} Recorded commit not found, fetching latest from remote...${NC}"
if ! git submodule update --init --remote third_party/gopher-orch 2>/dev/null; then
echo -e "${RED}Error: Failed to clone gopher-orch submodule${NC}"
echo -e "${YELLOW}If you have multiple GitHub accounts, use:${NC}"
echo -e " GITHUB_SSH_HOST=your-ssh-alias ./build.sh"
exit 1
fi
# Update main submodule to the latest commit on the branch configured in
# .gitmodules (currently br_release), not just the parent-recorded SHA.
if ! git submodule update --init --remote --checkout third_party/gopher-orch; then
echo -e "${RED}Error: Failed to update gopher-orch submodule to latest remote branch${NC}"
echo -e "${YELLOW}If you have multiple GitHub accounts, use:${NC}"
echo -e " GITHUB_SSH_HOST=your-ssh-alias ./build.sh"
exit 1
fi

# Update nested submodule (gopher-mcp inside gopher-orch)
# Note: gopher-orch/.gitmodules has 'update = none' so we must explicitly update
if [ -d "${NATIVE_DIR}" ]; then
cd "${NATIVE_DIR}"
git config --local --unset-all url."[email protected]:GopherSecurity/".insteadOf 2>/dev/null || true
git config --local --unset-all url."git@${SSH_HOST}:GopherSecurity/".insteadOf 2>/dev/null || true
git config --local url."git@${SSH_HOST}:GopherSecurity/".insteadOf "https://github.com/GopherSecurity/"
# Override 'update = none' by using --checkout
git submodule update --init --checkout third_party/gopher-mcp 2>/dev/null || true
git submodule sync -- third_party/gopher-mcp
git config --local submodule.third_party/gopher-mcp.url "git@${SSH_HOST}:GopherSecurity/gopher-mcp.git"
# Keep gopher-mcp on the latest commit from its configured release branch.
if ! git submodule update --init --remote --checkout third_party/gopher-mcp; then
echo -e "${RED}Error: Failed to update gopher-mcp submodule to latest remote branch${NC}"
echo -e "${YELLOW}If you have multiple GitHub accounts, use:${NC}"
echo -e " GITHUB_SSH_HOST=your-ssh-alias ./build.sh"
exit 1
fi
# Also update gopher-mcp's nested submodules recursively
if [ -d "third_party/gopher-mcp" ]; then
cd third_party/gopher-mcp
git config --local --unset-all url."[email protected]:GopherSecurity/".insteadOf 2>/dev/null || true
git config --local --unset-all url."git@${SSH_HOST}:GopherSecurity/".insteadOf 2>/dev/null || true
git config --local url."git@${SSH_HOST}:GopherSecurity/".insteadOf "https://github.com/GopherSecurity/"
git submodule update --init --recursive 2>/dev/null || true
git submodule update --init --recursive
fi
cd "${SCRIPT_DIR}"
fi
Expand All @@ -101,18 +111,29 @@ if [ ! -d "${NATIVE_DIR}" ]; then
fi

# Step 3: Build gopher-orch native library
# Skip build if native lib already has the required auth symbols
# Skip build only when the installed native lib matches the current submodule
# revisions and has the required auth symbols.
SKIP_NATIVE_BUILD=false
EXISTING_LIB="${SCRIPT_DIR}/native/lib/libgopher-orch.dylib"
if [ ! -f "${EXISTING_LIB}" ]; then
EXISTING_LIB="${SCRIPT_DIR}/native/lib/libgopher-orch.so"
fi

SOURCE_STAMP="gopher-orch=$(git -C "${NATIVE_DIR}" rev-parse HEAD)"
if [ -d "${NATIVE_DIR}/third_party/gopher-mcp" ]; then
SOURCE_STAMP="${SOURCE_STAMP}
gopher-mcp=$(git -C "${NATIVE_DIR}/third_party/gopher-mcp" rev-parse HEAD)"
fi

if [ -f "${EXISTING_LIB}" ]; then
if nm -gU "${EXISTING_LIB}" 2>/dev/null | grep -q "gopher_auth_config_create"; then
echo -e "${GREEN}✓ Native library already has auth symbols — skipping rebuild${NC}"
echo -e " (To force rebuild, delete native/lib/ first)"
SKIP_NATIVE_BUILD=true
if [ -f "${SOURCE_STAMP_FILE}" ] && [ "$(cat "${SOURCE_STAMP_FILE}")" = "${SOURCE_STAMP}" ]; then
if nm -gU "${EXISTING_LIB}" 2>/dev/null | grep -q "gopher_auth_config_create"; then
echo -e "${GREEN}✓ Native library already matches latest submodule revisions — skipping rebuild${NC}"
echo -e " (To force rebuild, delete native/lib/ first)"
SKIP_NATIVE_BUILD=true
fi
else
echo -e "${YELLOW} Native source revision changed; rebuilding libgopher-orch${NC}"
fi
fi

Expand Down Expand Up @@ -167,6 +188,7 @@ cp -P "${BUILD_DIR}"/_deps/fmt-build/libfmt*.a "${NATIVE_LIB}/" 2>/dev/null || t
cd "${SCRIPT_DIR}"

echo -e "${GREEN}✓ Native library built successfully${NC}"
printf "%s\n" "${SOURCE_STAMP}" > "${SOURCE_STAMP_FILE}"
echo ""

fi # end SKIP_NATIVE_BUILD
Expand Down Expand Up @@ -238,11 +260,14 @@ echo -e "${YELLOW}Step 5: Running tests...${NC}"
export PYTHONPATH="${SCRIPT_DIR}:${PYTHONPATH}"

# Use the freshly built native library (not a stale pip-installed version)
export GOPHER_ORCH_LIBRARY_PATH="${NATIVE_LIB_DIR}/libgopher-orch.dylib"
if [ ! -f "${GOPHER_ORCH_LIBRARY_PATH}" ]; then
export GOPHER_MCP_PYTHON_LIBRARY_PATH="${NATIVE_LIB_DIR}/libgopher-orch.dylib"
if [ ! -f "${GOPHER_MCP_PYTHON_LIBRARY_PATH}" ]; then
# Try .so for Linux
export GOPHER_ORCH_LIBRARY_PATH="${NATIVE_LIB_DIR}/libgopher-orch.so"
export GOPHER_MCP_PYTHON_LIBRARY_PATH="${NATIVE_LIB_DIR}/libgopher-orch.so"
fi
export GOPHER_ORCH_LIBRARY_PATH="${GOPHER_MCP_PYTHON_LIBRARY_PATH}"
export DYLD_LIBRARY_PATH="${NATIVE_LIB_DIR}:${DYLD_LIBRARY_PATH}"
export LD_LIBRARY_PATH="${NATIVE_LIB_DIR}:${LD_LIBRARY_PATH}"

# Try to run pytest, handling different installation scenarios
if python3 -c "import pytest" 2>/dev/null; then
Expand Down
138 changes: 138 additions & 0 deletions examples/header/create_by_url.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
#!/usr/bin/env python3
"""
SDK example for GopherAgent.create_with_url with dynamic MCP headers.

Python port of gopher-mcp-js/examples/header/create_by_url.ts, which
itself ports gopher-orch/examples/sdk/header/access_token_create_by_url.cc.

Shows the runtime options object:

GopherAgent.create_with_url(
provider,
model,
mcp_url,
runtime_options={"access_token": user_access_token},
)

or:

GopherAgent.create_with_url(
provider,
model,
mcp_url,
runtime_options={"headers": {"x-trace-id": "..."}},
)

access_token is a convenience alias for Authorization: Bearer <token>
on MCP runtime traffic only. Explicit headers["Authorization"] takes
precedence on the native side.
"""

import os
import sys
import traceback

from gopher_mcp_python import GopherAgent


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 queries_from_args() -> list[str]:
"""Return command-line queries or a default query."""
return sys.argv[1:] if len(sys.argv) > 1 else ["What is the weather in Tokyo?"]


def runtime_options_from_env() -> dict[str, object]:
"""
Build dynamic MCP runtime options for the example.

Set GOPHER_HEADER_MODE=headers to demonstrate the headers-only form.
The default token mode demonstrates the access_token convenience form.
"""
mode = env_or("GOPHER_HEADER_MODE", "token")
access_token = env_or("GOPHER_ACCESS_TOKEN", "")
trace_id = env_or("GOPHER_TRACE_ID", "python-header-create-by-url")

if mode == "headers":
headers = {
"x-gopher-example": "header-create-by-url",
"x-trace-id": trace_id,
}
if access_token:
headers["Authorization"] = f"Bearer {access_token}"
return {"headers": headers}

return {
"access_token": access_token,
"headers": {
"x-gopher-example": "header-create-by-url",
"x-trace-id": trace_id,
},
}


def main() -> None:
print("=== GopherAgent.create_with_url dynamic header example ===")
print(f"Usage: python3 {sys.argv[0]} [query1] [query2] ...")
print(
"Env: GOPHER_MCP_URL GOPHER_ACCESS_TOKEN GOPHER_HEADER_MODE "
"GOPHER_TRACE_ID LLM_PROVIDER LLM_MODEL"
)
print("")

provider = env_or("LLM_PROVIDER", "AnthropicProvider")
model = env_or("LLM_MODEL", "{YOUR_LLM_MODEL}")
mcp_url = env_or("GOPHER_MCP_URL", "http://127.0.0.1:5001/mcp")
access_token = env_or("GOPHER_ACCESS_TOKEN", "")
queries = queries_from_args()
runtime_options = runtime_options_from_env()

print(f"Provider: {provider}")
model_label = f"{model} (set LLM_MODEL)" if model == "{YOUR_LLM_MODEL}" else model
print(f"Model: {model_label}")
print(f"MCP URL: {mcp_url}")
token_label = (
"<empty; set GOPHER_ACCESS_TOKEN for protected MCP>"
if not access_token
else "<set via GOPHER_ACCESS_TOKEN>"
)
print(f"Access token: {token_label}")
print(f"Header mode: {env_or('GOPHER_HEADER_MODE', 'token')}")
print(f"Queries: {len(queries)}")

if model == "{YOUR_LLM_MODEL}":
print("\nError: LLM_MODEL must be set.", file=sys.stderr)
sys.exit(1)

print("\nCreating agent via GopherAgent.create_with_url(..., runtime_options=...)")
agent = GopherAgent.create_with_url(
provider,
model,
mcp_url,
runtime_options=runtime_options,
)
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()
sys.exit(1)
Binary file added examples/header/create_by_url_gateway
Binary file not shown.
Loading