Skip to content
Merged
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ dist/
native/
test-project-api/
examples/api/test-project-*/
examples/header/create_by_url_server
examples/header/create_by_url_gateway

# Python cache
__pycache__/
Expand Down
77 changes: 55 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."git@github.com: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 git@github.com 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 "git@github.com: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,45 @@ 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 the main submodule to the parent-recorded SHA by default so builds are
# reproducible. Developers can opt into branch-tip tracking when refreshing the
# pinned native dependency.
SUBMODULE_UPDATE_ARGS=(--init --checkout)
if [ "${GOPHER_ORCH_TRACK_REMOTE:-}" = "1" ]; then
echo -e "${YELLOW} GOPHER_ORCH_TRACK_REMOTE=1: tracking configured remote branch tips${NC}"
SUBMODULE_UPDATE_ARGS+=(--remote)
fi

if ! git submodule update "${SUBMODULE_UPDATE_ARGS[@]}" third_party/gopher-orch; then
echo -e "${RED}Error: Failed to update 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 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."git@github.com: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 the nested submodule pinned unless GOPHER_ORCH_TRACK_REMOTE=1 was
# requested above.
if ! git submodule update "${SUBMODULE_UPDATE_ARGS[@]}" third_party/gopher-mcp; then
echo -e "${RED}Error: Failed to update gopher-mcp 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
# 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."git@github.com: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 +119,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 +196,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 +268,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
140 changes: 140 additions & 0 deletions examples/header/create_by_url.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
#!/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}

options: dict[str, object] = {
"headers": {
"x-gopher-example": "header-create-by-url",
"x-trace-id": trace_id,
},
}
if access_token:
options["access_token"] = access_token
return options


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)
Loading