mirror of
https://github.com/googleapis/genai-toolbox.git
synced 2026-02-03 11:45:10 -05:00
Compare commits
11 Commits
refactor-q
...
err-api
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e884f52ea | ||
|
|
8a0f179f15 | ||
|
|
87ae5ae816 | ||
|
|
0c5285c5c8 | ||
|
|
ac544d0878 | ||
|
|
54f9a3d312 | ||
|
|
62d96a662d | ||
|
|
46244458c4 | ||
|
|
b6fa798610 | ||
|
|
bb58baff70 | ||
|
|
32b2c9366d |
47
.ci/quickstart_test/go.integration.cloudbuild.yaml
Normal file
47
.ci/quickstart_test/go.integration.cloudbuild.yaml
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
# Copyright 2025 Google LLC
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: 'golang:1.25.1'
|
||||||
|
id: 'go-quickstart-test'
|
||||||
|
entrypoint: 'bash'
|
||||||
|
args:
|
||||||
|
# The '-c' flag tells bash to execute the following string as a command.
|
||||||
|
# The 'set -ex' enables debug output and exits on error for easier troubleshooting.
|
||||||
|
- -c
|
||||||
|
- |
|
||||||
|
set -ex
|
||||||
|
export VERSION=$(cat ./cmd/version.txt)
|
||||||
|
chmod +x .ci/quickstart_test/run_go_tests.sh
|
||||||
|
.ci/quickstart_test/run_go_tests.sh
|
||||||
|
env:
|
||||||
|
- 'CLOUD_SQL_INSTANCE=${_CLOUD_SQL_INSTANCE}'
|
||||||
|
- 'GCP_PROJECT=${_GCP_PROJECT}'
|
||||||
|
- 'DATABASE_NAME=${_DATABASE_NAME}'
|
||||||
|
- 'DB_USER=${_DB_USER}'
|
||||||
|
secretEnv: ['TOOLS_YAML_CONTENT', 'GOOGLE_API_KEY', 'DB_PASSWORD']
|
||||||
|
|
||||||
|
availableSecrets:
|
||||||
|
secretManager:
|
||||||
|
- versionName: projects/${_GCP_PROJECT}/secrets/${_TOOLS_YAML_SECRET}/versions/7
|
||||||
|
env: 'TOOLS_YAML_CONTENT'
|
||||||
|
- versionName: projects/${_GCP_PROJECT_NUMBER}/secrets/${_API_KEY_SECRET}/versions/latest
|
||||||
|
env: 'GOOGLE_API_KEY'
|
||||||
|
- versionName: projects/${_GCP_PROJECT}/secrets/${_DB_PASS_SECRET}/versions/latest
|
||||||
|
env: 'DB_PASSWORD'
|
||||||
|
|
||||||
|
timeout: 1000s
|
||||||
|
|
||||||
|
options:
|
||||||
|
logging: CLOUD_LOGGING_ONLY
|
||||||
47
.ci/quickstart_test/js.integration.cloudbuild.yaml
Normal file
47
.ci/quickstart_test/js.integration.cloudbuild.yaml
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
# Copyright 2025 Google LLC
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: 'node:22'
|
||||||
|
id: 'js-quickstart-test'
|
||||||
|
entrypoint: 'bash'
|
||||||
|
args:
|
||||||
|
# The '-c' flag tells bash to execute the following string as a command.
|
||||||
|
# The 'set -ex' enables debug output and exits on error for easier troubleshooting.
|
||||||
|
- -c
|
||||||
|
- |
|
||||||
|
set -ex
|
||||||
|
export VERSION=$(cat ./cmd/version.txt)
|
||||||
|
chmod +x .ci/quickstart_test/run_js_tests.sh
|
||||||
|
.ci/quickstart_test/run_js_tests.sh
|
||||||
|
env:
|
||||||
|
- 'CLOUD_SQL_INSTANCE=${_CLOUD_SQL_INSTANCE}'
|
||||||
|
- 'GCP_PROJECT=${_GCP_PROJECT}'
|
||||||
|
- 'DATABASE_NAME=${_DATABASE_NAME}'
|
||||||
|
- 'DB_USER=${_DB_USER}'
|
||||||
|
secretEnv: ['TOOLS_YAML_CONTENT', 'GOOGLE_API_KEY', 'DB_PASSWORD']
|
||||||
|
|
||||||
|
availableSecrets:
|
||||||
|
secretManager:
|
||||||
|
- versionName: projects/${_GCP_PROJECT}/secrets/${_TOOLS_YAML_SECRET}/versions/6
|
||||||
|
env: 'TOOLS_YAML_CONTENT'
|
||||||
|
- versionName: projects/${_GCP_PROJECT_NUMBER}/secrets/${_API_KEY_SECRET}/versions/latest
|
||||||
|
env: 'GOOGLE_API_KEY'
|
||||||
|
- versionName: projects/${_GCP_PROJECT}/secrets/${_DB_PASS_SECRET}/versions/latest
|
||||||
|
env: 'DB_PASSWORD'
|
||||||
|
|
||||||
|
timeout: 1000s
|
||||||
|
|
||||||
|
options:
|
||||||
|
logging: CLOUD_LOGGING_ONLY
|
||||||
47
.ci/quickstart_test/py.integration.cloudbuild.yaml
Normal file
47
.ci/quickstart_test/py.integration.cloudbuild.yaml
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
# Copyright 2025 Google LLC
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: 'gcr.io/google.com/cloudsdktool/cloud-sdk:537.0.0'
|
||||||
|
id: 'python-quickstart-test'
|
||||||
|
entrypoint: 'bash'
|
||||||
|
args:
|
||||||
|
# The '-c' flag tells bash to execute the following string as a command.
|
||||||
|
# The 'set -ex' enables debug output and exits on error for easier troubleshooting.
|
||||||
|
- -c
|
||||||
|
- |
|
||||||
|
set -ex
|
||||||
|
export VERSION=$(cat ./cmd/version.txt)
|
||||||
|
chmod +x .ci/quickstart_test/run_py_tests.sh
|
||||||
|
.ci/quickstart_test/run_py_tests.sh
|
||||||
|
env:
|
||||||
|
- 'CLOUD_SQL_INSTANCE=${_CLOUD_SQL_INSTANCE}'
|
||||||
|
- 'GCP_PROJECT=${_GCP_PROJECT}'
|
||||||
|
- 'DATABASE_NAME=${_DATABASE_NAME}'
|
||||||
|
- 'DB_USER=${_DB_USER}'
|
||||||
|
secretEnv: ['TOOLS_YAML_CONTENT', 'GOOGLE_API_KEY', 'DB_PASSWORD']
|
||||||
|
|
||||||
|
availableSecrets:
|
||||||
|
secretManager:
|
||||||
|
- versionName: projects/${_GCP_PROJECT}/secrets/${_TOOLS_YAML_SECRET}/versions/5
|
||||||
|
env: 'TOOLS_YAML_CONTENT'
|
||||||
|
- versionName: projects/${_GCP_PROJECT_NUMBER}/secrets/${_API_KEY_SECRET}/versions/latest
|
||||||
|
env: 'GOOGLE_API_KEY'
|
||||||
|
- versionName: projects/${_GCP_PROJECT}/secrets/${_DB_PASS_SECRET}/versions/latest
|
||||||
|
env: 'DB_PASSWORD'
|
||||||
|
|
||||||
|
timeout: 1000s
|
||||||
|
|
||||||
|
options:
|
||||||
|
logging: CLOUD_LOGGING_ONLY
|
||||||
125
.ci/quickstart_test/run_go_tests.sh
Normal file
125
.ci/quickstart_test/run_go_tests.sh
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
# Copyright 2025 Google LLC
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
TABLE_NAME="hotels_go"
|
||||||
|
QUICKSTART_GO_DIR="docs/en/getting-started/quickstart/go"
|
||||||
|
SQL_FILE=".ci/quickstart_test/setup_hotels_sample.sql"
|
||||||
|
|
||||||
|
PROXY_PID=""
|
||||||
|
TOOLBOX_PID=""
|
||||||
|
|
||||||
|
install_system_packages() {
|
||||||
|
apt-get update && apt-get install -y \
|
||||||
|
postgresql-client \
|
||||||
|
wget \
|
||||||
|
gettext-base \
|
||||||
|
netcat-openbsd
|
||||||
|
}
|
||||||
|
|
||||||
|
start_cloud_sql_proxy() {
|
||||||
|
wget "https://storage.googleapis.com/cloud-sql-connectors/cloud-sql-proxy/v2.10.0/cloud-sql-proxy.linux.amd64" -O /usr/local/bin/cloud-sql-proxy
|
||||||
|
chmod +x /usr/local/bin/cloud-sql-proxy
|
||||||
|
cloud-sql-proxy "${CLOUD_SQL_INSTANCE}" &
|
||||||
|
PROXY_PID=$!
|
||||||
|
|
||||||
|
for i in {1..30}; do
|
||||||
|
if nc -z 127.0.0.1 5432; then
|
||||||
|
echo "Cloud SQL Proxy is up and running."
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Cloud SQL Proxy failed to start within the timeout period."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
setup_toolbox() {
|
||||||
|
TOOLBOX_YAML="/tools.yaml"
|
||||||
|
echo "${TOOLS_YAML_CONTENT}" > "$TOOLBOX_YAML"
|
||||||
|
if [ ! -f "$TOOLBOX_YAML" ]; then echo "Failed to create tools.yaml"; exit 1; fi
|
||||||
|
wget "https://storage.googleapis.com/genai-toolbox/v${VERSION}/linux/amd64/toolbox" -O "/toolbox"
|
||||||
|
chmod +x "/toolbox"
|
||||||
|
/toolbox --tools-file "$TOOLBOX_YAML" &
|
||||||
|
TOOLBOX_PID=$!
|
||||||
|
sleep 2
|
||||||
|
}
|
||||||
|
|
||||||
|
setup_orch_table() {
|
||||||
|
export TABLE_NAME
|
||||||
|
envsubst < "$SQL_FILE" | psql -h "$PGHOST" -p "$PGPORT" -U "$DB_USER" -d "$DATABASE_NAME"
|
||||||
|
}
|
||||||
|
|
||||||
|
run_orch_test() {
|
||||||
|
local orch_dir="$1"
|
||||||
|
local orch_name
|
||||||
|
orch_name=$(basename "$orch_dir")
|
||||||
|
|
||||||
|
if [ "$orch_name" == "openAI" ]; then
|
||||||
|
echo -e "\nSkipping framework '${orch_name}': Temporarily excluded."
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
(
|
||||||
|
set -e
|
||||||
|
setup_orch_table
|
||||||
|
|
||||||
|
echo "--- Preparing module for $orch_name ---"
|
||||||
|
cd "$orch_dir"
|
||||||
|
|
||||||
|
if [ -f "go.mod" ]; then
|
||||||
|
go mod tidy
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
export ORCH_NAME="$orch_name"
|
||||||
|
|
||||||
|
echo "--- Running tests for $orch_name ---"
|
||||||
|
go test -v ./...
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup_all() {
|
||||||
|
echo "--- Final cleanup: Shutting down processes and dropping table ---"
|
||||||
|
if [ -n "$TOOLBOX_PID" ]; then
|
||||||
|
kill $TOOLBOX_PID || true
|
||||||
|
fi
|
||||||
|
if [ -n "$PROXY_PID" ]; then
|
||||||
|
kill $PROXY_PID || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
trap cleanup_all EXIT
|
||||||
|
|
||||||
|
# Main script execution
|
||||||
|
install_system_packages
|
||||||
|
start_cloud_sql_proxy
|
||||||
|
|
||||||
|
export PGHOST=127.0.0.1
|
||||||
|
export PGPORT=5432
|
||||||
|
export PGPASSWORD="$DB_PASSWORD"
|
||||||
|
export GOOGLE_API_KEY="$GOOGLE_API_KEY"
|
||||||
|
|
||||||
|
setup_toolbox
|
||||||
|
|
||||||
|
for ORCH_DIR in "$QUICKSTART_GO_DIR"/*/; do
|
||||||
|
if [ ! -d "$ORCH_DIR" ]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
run_orch_test "$ORCH_DIR"
|
||||||
|
done
|
||||||
125
.ci/quickstart_test/run_js_tests.sh
Normal file
125
.ci/quickstart_test/run_js_tests.sh
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
# Copyright 2025 Google LLC
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
TABLE_NAME="hotels_js"
|
||||||
|
QUICKSTART_JS_DIR="docs/en/getting-started/quickstart/js"
|
||||||
|
SQL_FILE=".ci/quickstart_test/setup_hotels_sample.sql"
|
||||||
|
|
||||||
|
# Initialize process IDs to empty at the top of the script
|
||||||
|
PROXY_PID=""
|
||||||
|
TOOLBOX_PID=""
|
||||||
|
|
||||||
|
install_system_packages() {
|
||||||
|
apt-get update && apt-get install -y \
|
||||||
|
postgresql-client \
|
||||||
|
wget \
|
||||||
|
gettext-base \
|
||||||
|
netcat-openbsd
|
||||||
|
}
|
||||||
|
|
||||||
|
start_cloud_sql_proxy() {
|
||||||
|
wget "https://storage.googleapis.com/cloud-sql-connectors/cloud-sql-proxy/v2.10.0/cloud-sql-proxy.linux.amd64" -O /usr/local/bin/cloud-sql-proxy
|
||||||
|
chmod +x /usr/local/bin/cloud-sql-proxy
|
||||||
|
cloud-sql-proxy "${CLOUD_SQL_INSTANCE}" &
|
||||||
|
PROXY_PID=$!
|
||||||
|
|
||||||
|
for i in {1..30}; do
|
||||||
|
if nc -z 127.0.0.1 5432; then
|
||||||
|
echo "Cloud SQL Proxy is up and running."
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Cloud SQL Proxy failed to start within the timeout period."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
setup_toolbox() {
|
||||||
|
TOOLBOX_YAML="/tools.yaml"
|
||||||
|
echo "${TOOLS_YAML_CONTENT}" > "$TOOLBOX_YAML"
|
||||||
|
if [ ! -f "$TOOLBOX_YAML" ]; then echo "Failed to create tools.yaml"; exit 1; fi
|
||||||
|
wget "https://storage.googleapis.com/genai-toolbox/v${VERSION}/linux/amd64/toolbox" -O "/toolbox"
|
||||||
|
chmod +x "/toolbox"
|
||||||
|
/toolbox --tools-file "$TOOLBOX_YAML" &
|
||||||
|
TOOLBOX_PID=$!
|
||||||
|
sleep 2
|
||||||
|
}
|
||||||
|
|
||||||
|
setup_orch_table() {
|
||||||
|
export TABLE_NAME
|
||||||
|
envsubst < "$SQL_FILE" | psql -h "$PGHOST" -p "$PGPORT" -U "$DB_USER" -d "$DATABASE_NAME"
|
||||||
|
}
|
||||||
|
|
||||||
|
run_orch_test() {
|
||||||
|
local orch_dir="$1"
|
||||||
|
local orch_name
|
||||||
|
orch_name=$(basename "$orch_dir")
|
||||||
|
|
||||||
|
(
|
||||||
|
set -e
|
||||||
|
echo "--- Preparing environment for $orch_name ---"
|
||||||
|
setup_orch_table
|
||||||
|
|
||||||
|
cd "$orch_dir"
|
||||||
|
echo "Installing dependencies for $orch_name..."
|
||||||
|
if [ -f "package-lock.json" ]; then
|
||||||
|
npm ci
|
||||||
|
else
|
||||||
|
npm install
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
echo "--- Running tests for $orch_name ---"
|
||||||
|
export ORCH_NAME="$orch_name"
|
||||||
|
node --test quickstart.test.js
|
||||||
|
|
||||||
|
echo "--- Cleaning environment for $orch_name ---"
|
||||||
|
rm -rf "${orch_name}/node_modules"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup_all() {
|
||||||
|
echo "--- Final cleanup: Shutting down processes and dropping table ---"
|
||||||
|
if [ -n "$TOOLBOX_PID" ]; then
|
||||||
|
kill $TOOLBOX_PID || true
|
||||||
|
fi
|
||||||
|
if [ -n "$PROXY_PID" ]; then
|
||||||
|
kill $PROXY_PID || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
trap cleanup_all EXIT
|
||||||
|
|
||||||
|
# Main script execution
|
||||||
|
install_system_packages
|
||||||
|
start_cloud_sql_proxy
|
||||||
|
|
||||||
|
export PGHOST=127.0.0.1
|
||||||
|
export PGPORT=5432
|
||||||
|
export PGPASSWORD="$DB_PASSWORD"
|
||||||
|
export GOOGLE_API_KEY="$GOOGLE_API_KEY"
|
||||||
|
|
||||||
|
setup_toolbox
|
||||||
|
|
||||||
|
for ORCH_DIR in "$QUICKSTART_JS_DIR"/*/; do
|
||||||
|
if [ ! -d "$ORCH_DIR" ]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
run_orch_test "$ORCH_DIR"
|
||||||
|
done
|
||||||
115
.ci/quickstart_test/run_py_tests.sh
Normal file
115
.ci/quickstart_test/run_py_tests.sh
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
# Copyright 2025 Google LLC
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
TABLE_NAME="hotels_python"
|
||||||
|
QUICKSTART_PYTHON_DIR="docs/en/getting-started/quickstart/python"
|
||||||
|
SQL_FILE=".ci/quickstart_test/setup_hotels_sample.sql"
|
||||||
|
|
||||||
|
PROXY_PID=""
|
||||||
|
TOOLBOX_PID=""
|
||||||
|
|
||||||
|
install_system_packages() {
|
||||||
|
apt-get update && apt-get install -y \
|
||||||
|
postgresql-client \
|
||||||
|
python3-venv \
|
||||||
|
wget \
|
||||||
|
gettext-base \
|
||||||
|
netcat-openbsd
|
||||||
|
}
|
||||||
|
|
||||||
|
start_cloud_sql_proxy() {
|
||||||
|
wget "https://storage.googleapis.com/cloud-sql-connectors/cloud-sql-proxy/v2.10.0/cloud-sql-proxy.linux.amd64" -O /usr/local/bin/cloud-sql-proxy
|
||||||
|
chmod +x /usr/local/bin/cloud-sql-proxy
|
||||||
|
cloud-sql-proxy "${CLOUD_SQL_INSTANCE}" &
|
||||||
|
PROXY_PID=$!
|
||||||
|
|
||||||
|
for i in {1..30}; do
|
||||||
|
if nc -z 127.0.0.1 5432; then
|
||||||
|
echo "Cloud SQL Proxy is up and running."
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Cloud SQL Proxy failed to start within the timeout period."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
setup_toolbox() {
|
||||||
|
TOOLBOX_YAML="/tools.yaml"
|
||||||
|
echo "${TOOLS_YAML_CONTENT}" > "$TOOLBOX_YAML"
|
||||||
|
if [ ! -f "$TOOLBOX_YAML" ]; then echo "Failed to create tools.yaml"; exit 1; fi
|
||||||
|
wget "https://storage.googleapis.com/genai-toolbox/v${VERSION}/linux/amd64/toolbox" -O "/toolbox"
|
||||||
|
chmod +x "/toolbox"
|
||||||
|
/toolbox --tools-file "$TOOLBOX_YAML" &
|
||||||
|
TOOLBOX_PID=$!
|
||||||
|
sleep 2
|
||||||
|
}
|
||||||
|
|
||||||
|
setup_orch_table() {
|
||||||
|
export TABLE_NAME
|
||||||
|
envsubst < "$SQL_FILE" | psql -h "$PGHOST" -p "$PGPORT" -U "$DB_USER" -d "$DATABASE_NAME"
|
||||||
|
}
|
||||||
|
|
||||||
|
run_orch_test() {
|
||||||
|
local orch_dir="$1"
|
||||||
|
local orch_name
|
||||||
|
orch_name=$(basename "$orch_dir")
|
||||||
|
(
|
||||||
|
set -e
|
||||||
|
setup_orch_table
|
||||||
|
cd "$orch_dir"
|
||||||
|
local VENV_DIR=".venv"
|
||||||
|
python3 -m venv "$VENV_DIR"
|
||||||
|
source "$VENV_DIR/bin/activate"
|
||||||
|
pip install -r requirements.txt
|
||||||
|
echo "--- Running tests for $orch_name ---"
|
||||||
|
cd ..
|
||||||
|
ORCH_NAME="$orch_name" pytest
|
||||||
|
rm -rf "$VENV_DIR"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup_all() {
|
||||||
|
echo "--- Final cleanup: Shutting down processes and dropping table ---"
|
||||||
|
if [ -n "$TOOLBOX_PID" ]; then
|
||||||
|
kill $TOOLBOX_PID || true
|
||||||
|
fi
|
||||||
|
if [ -n "$PROXY_PID" ]; then
|
||||||
|
kill $PROXY_PID || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
trap cleanup_all EXIT
|
||||||
|
|
||||||
|
# Main script execution
|
||||||
|
install_system_packages
|
||||||
|
start_cloud_sql_proxy
|
||||||
|
|
||||||
|
export PGHOST=127.0.0.1
|
||||||
|
export PGPORT=5432
|
||||||
|
export PGPASSWORD="$DB_PASSWORD"
|
||||||
|
export GOOGLE_API_KEY="$GOOGLE_API_KEY"
|
||||||
|
|
||||||
|
setup_toolbox
|
||||||
|
|
||||||
|
for ORCH_DIR in "$QUICKSTART_PYTHON_DIR"/*/; do
|
||||||
|
if [ ! -d "$ORCH_DIR" ]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
run_orch_test "$ORCH_DIR"
|
||||||
|
done
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
# Copyright 2026 Google LLC
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: "${_IMAGE}"
|
|
||||||
id: "universal-test"
|
|
||||||
entrypoint: "bash"
|
|
||||||
args:
|
|
||||||
- -c
|
|
||||||
- |
|
|
||||||
set -ex
|
|
||||||
chmod +x .ci/universal/run_tests.sh
|
|
||||||
.ci/universal/run_tests.sh
|
|
||||||
env:
|
|
||||||
- "CLOUD_SQL_INSTANCE=${_CLOUD_SQL_INSTANCE}"
|
|
||||||
- "GCP_PROJECT=${_GCP_PROJECT}"
|
|
||||||
- "DATABASE_NAME=${_DATABASE_NAME}"
|
|
||||||
- "DB_USER=${_DB_USER}"
|
|
||||||
- "TARGET_ROOT=${_TARGET_ROOT}"
|
|
||||||
- "TARGET_LANG=${_TARGET_LANG}"
|
|
||||||
- "TABLE_NAME=${_TABLE_NAME}"
|
|
||||||
- "SQL_FILE=${_SQL_FILE}"
|
|
||||||
- "AGENT_FILE_PATTERN=${_AGENT_FILE_PATTERN}"
|
|
||||||
secretEnv: ["TOOLS_YAML_CONTENT", "GOOGLE_API_KEY", "DB_PASSWORD"]
|
|
||||||
|
|
||||||
availableSecrets:
|
|
||||||
secretManager:
|
|
||||||
- versionName: projects/${_GCP_PROJECT}/secrets/${_TOOLS_YAML_SECRET}/versions/5
|
|
||||||
env: "TOOLS_YAML_CONTENT"
|
|
||||||
- versionName: projects/${_GCP_PROJECT_NUMBER}/secrets/${_API_KEY_SECRET}/versions/latest
|
|
||||||
env: "GOOGLE_API_KEY"
|
|
||||||
- versionName: projects/${_GCP_PROJECT}/secrets/${_DB_PASS_SECRET}/versions/latest
|
|
||||||
env: "DB_PASSWORD"
|
|
||||||
|
|
||||||
|
|
||||||
timeout: 1200s
|
|
||||||
|
|
||||||
substitutions:
|
|
||||||
_TARGET_LANG: "python"
|
|
||||||
_IMAGE: "python:3.11"
|
|
||||||
_TARGET_ROOT: "docs/en/getting-started/quickstart/python"
|
|
||||||
_TABLE_NAME: "hotels_python"
|
|
||||||
_SQL_FILE: ".ci/universal/setup_hotels.sql"
|
|
||||||
_AGENT_FILE_PATTERN: "quickstart.py"
|
|
||||||
_LOG_BUCKET: "toolbox-test-logs"
|
|
||||||
|
|
||||||
options:
|
|
||||||
logging: CLOUD_LOGGING_ONLY
|
|
||||||
@@ -1,173 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# Copyright 2026 Google LLC
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# --- Configuration (from Environment Variables) ---
|
|
||||||
# TARGET_ROOT: The directory to search for tests (e.g., docs/en/getting-started/quickstart/js)
|
|
||||||
# TARGET_LANG: python, js, go
|
|
||||||
# TABLE_NAME: Database table name to use
|
|
||||||
# SQL_FILE: Path to the SQL setup file
|
|
||||||
# AGENT_FILE_PATTERN: Filename to look for (e.g., quickstart.js or agent.py)
|
|
||||||
|
|
||||||
VERSION=$(cat ./cmd/version.txt)
|
|
||||||
|
|
||||||
# Process IDs & Logs
|
|
||||||
PROXY_PID=""
|
|
||||||
TOOLBOX_PID=""
|
|
||||||
PROXY_LOG="cloud_sql_proxy.log"
|
|
||||||
TOOLBOX_LOG="toolbox_server.log"
|
|
||||||
|
|
||||||
install_system_packages() {
|
|
||||||
echo "Installing system packages..."
|
|
||||||
apt-get update && apt-get install -y \
|
|
||||||
postgresql-client \
|
|
||||||
wget \
|
|
||||||
gettext-base \
|
|
||||||
netcat-openbsd
|
|
||||||
|
|
||||||
if [[ "$TARGET_LANG" == "python" ]]; then
|
|
||||||
apt-get install -y python3-venv
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
start_cloud_sql_proxy() {
|
|
||||||
echo "Starting Cloud SQL Proxy..."
|
|
||||||
wget -q "https://storage.googleapis.com/cloud-sql-connectors/cloud-sql-proxy/v2.10.0/cloud-sql-proxy.linux.amd64" -O /usr/local/bin/cloud-sql-proxy
|
|
||||||
chmod +x /usr/local/bin/cloud-sql-proxy
|
|
||||||
cloud-sql-proxy "${CLOUD_SQL_INSTANCE}" > "$PROXY_LOG" 2>&1 &
|
|
||||||
PROXY_PID=$!
|
|
||||||
|
|
||||||
# Health Check
|
|
||||||
for i in {1..30}; do
|
|
||||||
if nc -z 127.0.0.1 5432; then
|
|
||||||
echo "Cloud SQL Proxy is up and running."
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
sleep 1
|
|
||||||
done
|
|
||||||
echo "ERROR: Cloud SQL Proxy failed to start. Logs:"
|
|
||||||
cat "$PROXY_LOG"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
setup_toolbox() {
|
|
||||||
echo "Setting up Toolbox server..."
|
|
||||||
TOOLBOX_YAML="/tools.yaml"
|
|
||||||
echo "${TOOLS_YAML_CONTENT}" > "$TOOLBOX_YAML"
|
|
||||||
wget -q "https://storage.googleapis.com/genai-toolbox/v${VERSION}/linux/amd64/toolbox" -O "/toolbox"
|
|
||||||
chmod +x "/toolbox"
|
|
||||||
/toolbox --tools-file "$TOOLBOX_YAML" > "$TOOLBOX_LOG" 2>&1 &
|
|
||||||
TOOLBOX_PID=$!
|
|
||||||
|
|
||||||
# Health Check
|
|
||||||
for i in {1..15}; do
|
|
||||||
if nc -z 127.0.0.1 5000; then
|
|
||||||
echo "Toolbox server is up and running."
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
sleep 1
|
|
||||||
done
|
|
||||||
echo "ERROR: Toolbox server failed to start. Logs:"
|
|
||||||
cat "$TOOLBOX_LOG"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
setup_db_table() {
|
|
||||||
echo "Setting up database table $TABLE_NAME using $SQL_FILE..."
|
|
||||||
export TABLE_NAME
|
|
||||||
envsubst < "$SQL_FILE" | psql -h 127.0.0.1 -p 5432 -U "$DB_USER" -d "$DATABASE_NAME"
|
|
||||||
}
|
|
||||||
|
|
||||||
run_python_test() {
|
|
||||||
local dir=$1
|
|
||||||
local name=$(basename "$dir")
|
|
||||||
echo "--- Running Python Test: $name ---"
|
|
||||||
(
|
|
||||||
cd "$dir"
|
|
||||||
python3 -m venv .venv
|
|
||||||
source .venv/bin/activate
|
|
||||||
pip install -q -r requirements.txt pytest
|
|
||||||
|
|
||||||
cd ..
|
|
||||||
# If there is a pytest file in the parent directory (like agent_test.py or quickstart_test.py)
|
|
||||||
# we use it. Otherwise we just run the agent.
|
|
||||||
local test_file=$(find . -maxdepth 1 -name "*test.py" | head -n 1)
|
|
||||||
if [ -n "$test_file" ]; then
|
|
||||||
echo "Found native test: $test_file. Running pytest..."
|
|
||||||
export ORCH_NAME="$name"
|
|
||||||
export PYTHONPATH="../"
|
|
||||||
pytest "$test_file"
|
|
||||||
else
|
|
||||||
echo "No native test found. running agent directly..."
|
|
||||||
export PYTHONPATH="../"
|
|
||||||
python3 "${name}/${AGENT_FILE_PATTERN}"
|
|
||||||
fi
|
|
||||||
rm -rf "${name}/.venv"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
run_js_test() {
|
|
||||||
local dir=$1
|
|
||||||
local name=$(basename "$dir")
|
|
||||||
echo "--- Running JS Test: $name ---"
|
|
||||||
(
|
|
||||||
cd "$dir"
|
|
||||||
if [ -f "package-lock.json" ]; then npm ci -q; else npm install -q; fi
|
|
||||||
|
|
||||||
cd ..
|
|
||||||
# Looking for a JS test file in the parent directory
|
|
||||||
local test_file=$(find . -maxdepth 1 -name "*test.js" | head -n 1)
|
|
||||||
if [ -n "$test_file" ]; then
|
|
||||||
echo "Found native test: $test_file. Running node --test..."
|
|
||||||
export ORCH_NAME="$name"
|
|
||||||
node --test "$test_file"
|
|
||||||
else
|
|
||||||
echo "No native test found. running agent directly..."
|
|
||||||
node "${name}/${AGENT_FILE_PATTERN}"
|
|
||||||
fi
|
|
||||||
rm -rf "${name}/node_modules"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
cleanup() {
|
|
||||||
echo "Cleaning up background processes..."
|
|
||||||
[ -n "$TOOLBOX_PID" ] && kill "$TOOLBOX_PID" || true
|
|
||||||
[ -n "$PROXY_PID" ] && kill "$PROXY_PID" || true
|
|
||||||
}
|
|
||||||
trap cleanup EXIT
|
|
||||||
|
|
||||||
# --- Execution ---
|
|
||||||
install_system_packages
|
|
||||||
start_cloud_sql_proxy
|
|
||||||
|
|
||||||
export PGHOST=127.0.0.1
|
|
||||||
export PGPORT=5432
|
|
||||||
export PGPASSWORD="$DB_PASSWORD"
|
|
||||||
export GOOGLE_API_KEY="$GOOGLE_API_KEY"
|
|
||||||
|
|
||||||
setup_toolbox
|
|
||||||
setup_db_table
|
|
||||||
|
|
||||||
echo "Scanning $TARGET_ROOT for tests with pattern $AGENT_FILE_PATTERN..."
|
|
||||||
|
|
||||||
find "$TARGET_ROOT" -name "$AGENT_FILE_PATTERN" | while read -r agent_file; do
|
|
||||||
sample_dir=$(dirname "$agent_file")
|
|
||||||
if [[ "$TARGET_LANG" == "python" ]]; then
|
|
||||||
run_python_test "$sample_dir"
|
|
||||||
elif [[ "$TARGET_LANG" == "js" ]]; then
|
|
||||||
run_js_test "$sample_dir"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
@@ -53,7 +53,7 @@ export async function main() {
|
|||||||
|
|
||||||
for (const query of queries) {
|
for (const query of queries) {
|
||||||
conversationHistory.push({ role: "user", content: [{ text: query }] });
|
conversationHistory.push({ role: "user", content: [{ text: query }] });
|
||||||
let response = await ai.generate({
|
const response = await ai.generate({
|
||||||
messages: conversationHistory,
|
messages: conversationHistory,
|
||||||
tools: tools,
|
tools: tools,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,12 +13,12 @@ The `invoke` command allows you to invoke tools defined in your configuration di
|
|||||||
|
|
||||||
{{< notice tip >}}
|
{{< notice tip >}}
|
||||||
**Keep configurations minimal:** The `invoke` command initializes *all* resources (sources, tools, etc.) defined in your configuration files during execution. To ensure fast response times, consider using a minimal configuration file containing only the tools you need for the specific invocation.
|
**Keep configurations minimal:** The `invoke` command initializes *all* resources (sources, tools, etc.) defined in your configuration files during execution. To ensure fast response times, consider using a minimal configuration file containing only the tools you need for the specific invocation.
|
||||||
{{< /notice >}}
|
{{< notice tip >}}
|
||||||
|
|
||||||
## Before you begin
|
## Prerequisites
|
||||||
|
|
||||||
1. Make sure you have the `toolbox` binary installed or built.
|
- You have the `toolbox` binary installed or built.
|
||||||
2. Make sure you have a valid tool configuration file (e.g., `tools.yaml`).
|
- You have a valid tool configuration file (e.g., `tools.yaml`).
|
||||||
|
|
||||||
## Basic Usage
|
## Basic Usage
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
"github.com/go-chi/chi/v5/middleware"
|
"github.com/go-chi/chi/v5/middleware"
|
||||||
@@ -235,8 +234,10 @@ func toolInvokeHandler(s *Server, w http.ResponseWriter, r *http.Request) {
|
|||||||
params, err := parameters.ParseParams(tool.GetParameters(), data, claimsFromAuth)
|
params, err := parameters.ParseParams(tool.GetParameters(), data, claimsFromAuth)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// If auth error, return 401
|
// If auth error, return 401
|
||||||
if errors.Is(err, util.ErrUnauthorized) {
|
errMsg := fmt.Sprintf("error parsing authenticated parameters from ID token: %w", err)
|
||||||
s.logger.DebugContext(ctx, fmt.Sprintf("error parsing authenticated parameters from ID token: %s", err))
|
var clientServerErr *util.ClientServerError
|
||||||
|
if errors.As(err, &clientServerErr) && clientServerErr.Code == http.StatusUnauthorized {
|
||||||
|
s.logger.DebugContext(ctx, errMsg)
|
||||||
_ = render.Render(w, r, newErrResponse(err, http.StatusUnauthorized))
|
_ = render.Render(w, r, newErrResponse(err, http.StatusUnauthorized))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -259,35 +260,50 @@ func toolInvokeHandler(s *Server, w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// Determine what error to return to the users.
|
// Determine what error to return to the users.
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errStr := err.Error()
|
var tbErr util.ToolboxError
|
||||||
var statusCode int
|
|
||||||
|
|
||||||
// Upstream API auth error propagation
|
if errors.As(err, &tbErr) {
|
||||||
switch {
|
switch tbErr.Category() {
|
||||||
case strings.Contains(errStr, "Error 401"):
|
case util.CategoryAgent:
|
||||||
statusCode = http.StatusUnauthorized
|
// Agent Errors -> 200 OK
|
||||||
case strings.Contains(errStr, "Error 403"):
|
s.logger.DebugContext(ctx, fmt.Sprintf("Tool invocation agent error: %v", err))
|
||||||
statusCode = http.StatusForbidden
|
_ = render.Render(w, r, newErrResponse(err, http.StatusOK))
|
||||||
|
return
|
||||||
|
|
||||||
|
case util.CategoryServer:
|
||||||
|
// Server Errors -> Check the specific code inside
|
||||||
|
var clientServerErr *util.ClientServerError
|
||||||
|
statusCode := http.StatusInternalServerError // Default to 500
|
||||||
|
|
||||||
|
if errors.As(err, &clientServerErr) {
|
||||||
|
if clientServerErr.Code != 0 {
|
||||||
|
statusCode = clientServerErr.Code
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Process auth error
|
||||||
if statusCode == http.StatusUnauthorized || statusCode == http.StatusForbidden {
|
if statusCode == http.StatusUnauthorized || statusCode == http.StatusForbidden {
|
||||||
if clientAuth {
|
if clientAuth {
|
||||||
// Propagate the original 401/403 error.
|
// Token error, pass through 401/403
|
||||||
s.logger.DebugContext(ctx, fmt.Sprintf("error invoking tool. Client credentials lack authorization to the source: %v", err))
|
s.logger.DebugContext(ctx, fmt.Sprintf("Client credentials lack authorization: %v", err))
|
||||||
_ = render.Render(w, r, newErrResponse(err, statusCode))
|
_ = render.Render(w, r, newErrResponse(err, statusCode))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// ADC lacking permission or credentials configuration error.
|
// ADC/Config error, return 500
|
||||||
internalErr := fmt.Errorf("unexpected auth error occured during Tool invocation: %w", err)
|
statusCode = http.StatusInternalServerError
|
||||||
s.logger.ErrorContext(ctx, internalErr.Error())
|
}
|
||||||
_ = render.Render(w, r, newErrResponse(internalErr, http.StatusInternalServerError))
|
|
||||||
|
s.logger.ErrorContext(ctx, fmt.Sprintf("Tool invocation server error: %v", err))
|
||||||
|
_ = render.Render(w, r, newErrResponse(err, statusCode))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err = fmt.Errorf("error while invoking tool: %w", err)
|
} else {
|
||||||
s.logger.DebugContext(ctx, err.Error())
|
// Unknown error -> 500
|
||||||
_ = render.Render(w, r, newErrResponse(err, http.StatusBadRequest))
|
s.logger.ErrorContext(ctx, fmt.Sprintf("Tool invocation unknown error: %v", err))
|
||||||
|
_ = render.Render(w, r, newErrResponse(err, http.StatusInternalServerError))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
resMarshal, err := json.Marshal(res)
|
resMarshal, err := json.Marshal(res)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -444,18 +443,20 @@ func httpHandler(s *Server, w http.ResponseWriter, r *http.Request) {
|
|||||||
code := rpcResponse.Error.Code
|
code := rpcResponse.Error.Code
|
||||||
switch code {
|
switch code {
|
||||||
case jsonrpc.INTERNAL_ERROR:
|
case jsonrpc.INTERNAL_ERROR:
|
||||||
|
// Map Internal RPC Error (-32603) to HTTP 500
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
case jsonrpc.INVALID_REQUEST:
|
case jsonrpc.INVALID_REQUEST:
|
||||||
errStr := err.Error()
|
var clientServerErr *util.ClientServerError
|
||||||
if errors.Is(err, util.ErrUnauthorized) {
|
if errors.As(err, &clientServerErr) {
|
||||||
|
switch clientServerErr.Code {
|
||||||
|
case http.StatusUnauthorized:
|
||||||
w.WriteHeader(http.StatusUnauthorized)
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
} else if strings.Contains(errStr, "Error 401") {
|
case http.StatusForbidden:
|
||||||
w.WriteHeader(http.StatusUnauthorized)
|
|
||||||
} else if strings.Contains(errStr, "Error 403") {
|
|
||||||
w.WriteHeader(http.StatusForbidden)
|
w.WriteHeader(http.StatusForbidden)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// send HTTP response
|
// send HTTP response
|
||||||
render.JSON(w, r, res)
|
render.JSON(w, r, res)
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/googleapis/genai-toolbox/internal/prompts"
|
"github.com/googleapis/genai-toolbox/internal/prompts"
|
||||||
"github.com/googleapis/genai-toolbox/internal/server/mcp/jsonrpc"
|
"github.com/googleapis/genai-toolbox/internal/server/mcp/jsonrpc"
|
||||||
@@ -124,7 +123,11 @@ func toolsCallHandler(ctx context.Context, id jsonrpc.RequestId, resourceMgr *re
|
|||||||
}
|
}
|
||||||
if clientAuth {
|
if clientAuth {
|
||||||
if accessToken == "" {
|
if accessToken == "" {
|
||||||
return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, "missing access token in the 'Authorization' header", nil), util.ErrUnauthorized
|
return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, "missing access token in the 'Authorization' header", nil), util.NewClientServerError(
|
||||||
|
"missing access token in the 'Authorization' header",
|
||||||
|
http.StatusUnauthorized,
|
||||||
|
nil,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,7 +175,11 @@ func toolsCallHandler(ctx context.Context, id jsonrpc.RequestId, resourceMgr *re
|
|||||||
// Check if any of the specified auth services is verified
|
// Check if any of the specified auth services is verified
|
||||||
isAuthorized := tool.Authorized(verifiedAuthServices)
|
isAuthorized := tool.Authorized(verifiedAuthServices)
|
||||||
if !isAuthorized {
|
if !isAuthorized {
|
||||||
err = fmt.Errorf("unauthorized Tool call: Please make sure your specify correct auth headers: %w", util.ErrUnauthorized)
|
err = util.NewClientServerError(
|
||||||
|
"unauthorized Tool call: Please make sure you specify correct auth headers",
|
||||||
|
http.StatusUnauthorized,
|
||||||
|
nil,
|
||||||
|
)
|
||||||
return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err
|
return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err
|
||||||
}
|
}
|
||||||
logger.DebugContext(ctx, "tool invocation authorized")
|
logger.DebugContext(ctx, "tool invocation authorized")
|
||||||
@@ -194,21 +201,13 @@ func toolsCallHandler(ctx context.Context, id jsonrpc.RequestId, resourceMgr *re
|
|||||||
// run tool invocation and generate response.
|
// run tool invocation and generate response.
|
||||||
results, err := tool.Invoke(ctx, resourceMgr, params, accessToken)
|
results, err := tool.Invoke(ctx, resourceMgr, params, accessToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errStr := err.Error()
|
var tbErr util.ToolboxError
|
||||||
// Missing authService tokens.
|
|
||||||
if errors.Is(err, util.ErrUnauthorized) {
|
|
||||||
return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err
|
|
||||||
}
|
|
||||||
// Upstream auth error
|
|
||||||
if strings.Contains(errStr, "Error 401") || strings.Contains(errStr, "Error 403") {
|
|
||||||
if clientAuth {
|
|
||||||
// Error with client credentials should pass down to the client
|
|
||||||
return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err
|
|
||||||
}
|
|
||||||
// Auth error with ADC should raise internal 500 error
|
|
||||||
return jsonrpc.NewError(id, jsonrpc.INTERNAL_ERROR, err.Error(), nil), err
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if errors.As(err, &tbErr) {
|
||||||
|
switch tbErr.Category() {
|
||||||
|
case util.CategoryAgent:
|
||||||
|
// MCP - Tool execution error
|
||||||
|
// Return SUCCESS but with IsError: true
|
||||||
text := TextContent{
|
text := TextContent{
|
||||||
Type: "text",
|
Type: "text",
|
||||||
Text: err.Error(),
|
Text: err.Error(),
|
||||||
@@ -218,6 +217,28 @@ func toolsCallHandler(ctx context.Context, id jsonrpc.RequestId, resourceMgr *re
|
|||||||
Id: id,
|
Id: id,
|
||||||
Result: CallToolResult{Content: []TextContent{text}, IsError: true},
|
Result: CallToolResult{Content: []TextContent{text}, IsError: true},
|
||||||
}, nil
|
}, nil
|
||||||
|
|
||||||
|
case util.CategoryServer:
|
||||||
|
// MCP Spec - Protocol error
|
||||||
|
// Return JSON-RPC ERROR
|
||||||
|
var clientServerErr *util.ClientServerError
|
||||||
|
rpcCode := jsonrpc.INTERNAL_ERROR // Default to Internal Error (-32603)
|
||||||
|
|
||||||
|
if errors.As(err, &clientServerErr) {
|
||||||
|
if clientServerErr.Code == http.StatusUnauthorized || clientServerErr.Code == http.StatusForbidden {
|
||||||
|
if clientAuth {
|
||||||
|
rpcCode = jsonrpc.INVALID_REQUEST
|
||||||
|
} else {
|
||||||
|
rpcCode = jsonrpc.INTERNAL_ERROR
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return jsonrpc.NewError(id, rpcCode, err.Error(), nil), err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Unknown error -> 500
|
||||||
|
return jsonrpc.NewError(id, jsonrpc.INTERNAL_ERROR, err.Error(), nil), err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
content := make([]TextContent, 0)
|
content := make([]TextContent, 0)
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/googleapis/genai-toolbox/internal/prompts"
|
"github.com/googleapis/genai-toolbox/internal/prompts"
|
||||||
"github.com/googleapis/genai-toolbox/internal/server/mcp/jsonrpc"
|
"github.com/googleapis/genai-toolbox/internal/server/mcp/jsonrpc"
|
||||||
@@ -124,7 +123,11 @@ func toolsCallHandler(ctx context.Context, id jsonrpc.RequestId, resourceMgr *re
|
|||||||
}
|
}
|
||||||
if clientAuth {
|
if clientAuth {
|
||||||
if accessToken == "" {
|
if accessToken == "" {
|
||||||
return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, "missing access token in the 'Authorization' header", nil), util.ErrUnauthorized
|
return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, "missing access token in the 'Authorization' header", nil), util.NewClientServerError(
|
||||||
|
"missing access token in the 'Authorization' header",
|
||||||
|
http.StatusUnauthorized,
|
||||||
|
nil,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,7 +175,11 @@ func toolsCallHandler(ctx context.Context, id jsonrpc.RequestId, resourceMgr *re
|
|||||||
// Check if any of the specified auth services is verified
|
// Check if any of the specified auth services is verified
|
||||||
isAuthorized := tool.Authorized(verifiedAuthServices)
|
isAuthorized := tool.Authorized(verifiedAuthServices)
|
||||||
if !isAuthorized {
|
if !isAuthorized {
|
||||||
err = fmt.Errorf("unauthorized Tool call: Please make sure your specify correct auth headers: %w", util.ErrUnauthorized)
|
err = util.NewClientServerError(
|
||||||
|
"unauthorized Tool call: Please make sure you specify correct auth headers",
|
||||||
|
http.StatusUnauthorized,
|
||||||
|
nil,
|
||||||
|
)
|
||||||
return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err
|
return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err
|
||||||
}
|
}
|
||||||
logger.DebugContext(ctx, "tool invocation authorized")
|
logger.DebugContext(ctx, "tool invocation authorized")
|
||||||
@@ -194,20 +201,13 @@ func toolsCallHandler(ctx context.Context, id jsonrpc.RequestId, resourceMgr *re
|
|||||||
// run tool invocation and generate response.
|
// run tool invocation and generate response.
|
||||||
results, err := tool.Invoke(ctx, resourceMgr, params, accessToken)
|
results, err := tool.Invoke(ctx, resourceMgr, params, accessToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errStr := err.Error()
|
var tbErr util.ToolboxError
|
||||||
// Missing authService tokens.
|
|
||||||
if errors.Is(err, util.ErrUnauthorized) {
|
if errors.As(err, &tbErr) {
|
||||||
return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err
|
switch tbErr.Category() {
|
||||||
}
|
case util.CategoryAgent:
|
||||||
// Upstream auth error
|
// MCP - Tool execution error
|
||||||
if strings.Contains(errStr, "Error 401") || strings.Contains(errStr, "Error 403") {
|
// Return SUCCESS but with IsError: true
|
||||||
if clientAuth {
|
|
||||||
// Error with client credentials should pass down to the client
|
|
||||||
return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err
|
|
||||||
}
|
|
||||||
// Auth error with ADC should raise internal 500 error
|
|
||||||
return jsonrpc.NewError(id, jsonrpc.INTERNAL_ERROR, err.Error(), nil), err
|
|
||||||
}
|
|
||||||
text := TextContent{
|
text := TextContent{
|
||||||
Type: "text",
|
Type: "text",
|
||||||
Text: err.Error(),
|
Text: err.Error(),
|
||||||
@@ -217,8 +217,29 @@ func toolsCallHandler(ctx context.Context, id jsonrpc.RequestId, resourceMgr *re
|
|||||||
Id: id,
|
Id: id,
|
||||||
Result: CallToolResult{Content: []TextContent{text}, IsError: true},
|
Result: CallToolResult{Content: []TextContent{text}, IsError: true},
|
||||||
}, nil
|
}, nil
|
||||||
}
|
|
||||||
|
|
||||||
|
case util.CategoryServer:
|
||||||
|
// MCP Spec - Protocol error
|
||||||
|
// Return JSON-RPC ERROR
|
||||||
|
var clientServerErr *util.ClientServerError
|
||||||
|
rpcCode := jsonrpc.INTERNAL_ERROR // Default to Internal Error (-32603)
|
||||||
|
|
||||||
|
if errors.As(err, &clientServerErr) {
|
||||||
|
if clientServerErr.Code == http.StatusUnauthorized || clientServerErr.Code == http.StatusForbidden {
|
||||||
|
if clientAuth {
|
||||||
|
rpcCode = jsonrpc.INVALID_REQUEST
|
||||||
|
} else {
|
||||||
|
rpcCode = jsonrpc.INTERNAL_ERROR
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return jsonrpc.NewError(id, rpcCode, err.Error(), nil), err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Unknown error -> 500
|
||||||
|
return jsonrpc.NewError(id, jsonrpc.INTERNAL_ERROR, err.Error(), nil), err
|
||||||
|
}
|
||||||
|
}
|
||||||
content := make([]TextContent, 0)
|
content := make([]TextContent, 0)
|
||||||
|
|
||||||
sliceRes, ok := results.([]any)
|
sliceRes, ok := results.([]any)
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/googleapis/genai-toolbox/internal/prompts"
|
"github.com/googleapis/genai-toolbox/internal/prompts"
|
||||||
"github.com/googleapis/genai-toolbox/internal/server/mcp/jsonrpc"
|
"github.com/googleapis/genai-toolbox/internal/server/mcp/jsonrpc"
|
||||||
@@ -117,7 +116,12 @@ func toolsCallHandler(ctx context.Context, id jsonrpc.RequestId, resourceMgr *re
|
|||||||
}
|
}
|
||||||
if clientAuth {
|
if clientAuth {
|
||||||
if accessToken == "" {
|
if accessToken == "" {
|
||||||
return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, "missing access token in the 'Authorization' header", nil), util.ErrUnauthorized
|
errMsg := "missing access token in the 'Authorization' header"
|
||||||
|
return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, errMsg, nil), util.NewClientServerError(
|
||||||
|
errMsg,
|
||||||
|
http.StatusUnauthorized,
|
||||||
|
nil,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,7 +169,11 @@ func toolsCallHandler(ctx context.Context, id jsonrpc.RequestId, resourceMgr *re
|
|||||||
// Check if any of the specified auth services is verified
|
// Check if any of the specified auth services is verified
|
||||||
isAuthorized := tool.Authorized(verifiedAuthServices)
|
isAuthorized := tool.Authorized(verifiedAuthServices)
|
||||||
if !isAuthorized {
|
if !isAuthorized {
|
||||||
err = fmt.Errorf("unauthorized Tool call: Please make sure your specify correct auth headers: %w", util.ErrUnauthorized)
|
err = util.NewClientServerError(
|
||||||
|
"unauthorized Tool call: Please make sure you specify correct auth headers",
|
||||||
|
http.StatusUnauthorized,
|
||||||
|
nil,
|
||||||
|
)
|
||||||
return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err
|
return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err
|
||||||
}
|
}
|
||||||
logger.DebugContext(ctx, "tool invocation authorized")
|
logger.DebugContext(ctx, "tool invocation authorized")
|
||||||
@@ -187,20 +195,13 @@ func toolsCallHandler(ctx context.Context, id jsonrpc.RequestId, resourceMgr *re
|
|||||||
// run tool invocation and generate response.
|
// run tool invocation and generate response.
|
||||||
results, err := tool.Invoke(ctx, resourceMgr, params, accessToken)
|
results, err := tool.Invoke(ctx, resourceMgr, params, accessToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errStr := err.Error()
|
var tbErr util.ToolboxError
|
||||||
// Missing authService tokens.
|
|
||||||
if errors.Is(err, util.ErrUnauthorized) {
|
if errors.As(err, &tbErr) {
|
||||||
return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err
|
switch tbErr.Category() {
|
||||||
}
|
case util.CategoryAgent:
|
||||||
// Upstream auth error
|
// MCP - Tool execution error
|
||||||
if strings.Contains(errStr, "Error 401") || strings.Contains(errStr, "Error 403") {
|
// Return SUCCESS but with IsError: true
|
||||||
if clientAuth {
|
|
||||||
// Error with client credentials should pass down to the client
|
|
||||||
return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err
|
|
||||||
}
|
|
||||||
// Auth error with ADC should raise internal 500 error
|
|
||||||
return jsonrpc.NewError(id, jsonrpc.INTERNAL_ERROR, err.Error(), nil), err
|
|
||||||
}
|
|
||||||
text := TextContent{
|
text := TextContent{
|
||||||
Type: "text",
|
Type: "text",
|
||||||
Text: err.Error(),
|
Text: err.Error(),
|
||||||
@@ -210,6 +211,28 @@ func toolsCallHandler(ctx context.Context, id jsonrpc.RequestId, resourceMgr *re
|
|||||||
Id: id,
|
Id: id,
|
||||||
Result: CallToolResult{Content: []TextContent{text}, IsError: true},
|
Result: CallToolResult{Content: []TextContent{text}, IsError: true},
|
||||||
}, nil
|
}, nil
|
||||||
|
|
||||||
|
case util.CategoryServer:
|
||||||
|
// MCP Spec - Protocol error
|
||||||
|
// Return JSON-RPC ERROR
|
||||||
|
var clientServerErr *util.ClientServerError
|
||||||
|
rpcCode := jsonrpc.INTERNAL_ERROR // Default to Internal Error (-32603)
|
||||||
|
|
||||||
|
if errors.As(err, &clientServerErr) {
|
||||||
|
if clientServerErr.Code == http.StatusUnauthorized || clientServerErr.Code == http.StatusForbidden {
|
||||||
|
if clientAuth {
|
||||||
|
rpcCode = jsonrpc.INVALID_REQUEST
|
||||||
|
} else {
|
||||||
|
rpcCode = jsonrpc.INTERNAL_ERROR
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return jsonrpc.NewError(id, rpcCode, err.Error(), nil), err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Unknown error -> 500
|
||||||
|
return jsonrpc.NewError(id, jsonrpc.INTERNAL_ERROR, err.Error(), nil), err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
content := make([]TextContent, 0)
|
content := make([]TextContent, 0)
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/googleapis/genai-toolbox/internal/prompts"
|
"github.com/googleapis/genai-toolbox/internal/prompts"
|
||||||
"github.com/googleapis/genai-toolbox/internal/server/mcp/jsonrpc"
|
"github.com/googleapis/genai-toolbox/internal/server/mcp/jsonrpc"
|
||||||
@@ -117,7 +116,11 @@ func toolsCallHandler(ctx context.Context, id jsonrpc.RequestId, resourceMgr *re
|
|||||||
}
|
}
|
||||||
if clientAuth {
|
if clientAuth {
|
||||||
if accessToken == "" {
|
if accessToken == "" {
|
||||||
return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, "missing access token in the 'Authorization' header", nil), util.ErrUnauthorized
|
return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, "missing access token in the 'Authorization' header", nil), util.NewClientServerError(
|
||||||
|
"missing access token in the 'Authorization' header",
|
||||||
|
http.StatusUnauthorized,
|
||||||
|
nil,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,7 +168,11 @@ func toolsCallHandler(ctx context.Context, id jsonrpc.RequestId, resourceMgr *re
|
|||||||
// Check if any of the specified auth services is verified
|
// Check if any of the specified auth services is verified
|
||||||
isAuthorized := tool.Authorized(verifiedAuthServices)
|
isAuthorized := tool.Authorized(verifiedAuthServices)
|
||||||
if !isAuthorized {
|
if !isAuthorized {
|
||||||
err = fmt.Errorf("unauthorized Tool call: Please make sure your specify correct auth headers: %w", util.ErrUnauthorized)
|
err = util.NewClientServerError(
|
||||||
|
"unauthorized Tool call: Please make sure you specify correct auth headers",
|
||||||
|
http.StatusUnauthorized,
|
||||||
|
nil,
|
||||||
|
)
|
||||||
return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err
|
return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err
|
||||||
}
|
}
|
||||||
logger.DebugContext(ctx, "tool invocation authorized")
|
logger.DebugContext(ctx, "tool invocation authorized")
|
||||||
@@ -187,20 +194,13 @@ func toolsCallHandler(ctx context.Context, id jsonrpc.RequestId, resourceMgr *re
|
|||||||
// run tool invocation and generate response.
|
// run tool invocation and generate response.
|
||||||
results, err := tool.Invoke(ctx, resourceMgr, params, accessToken)
|
results, err := tool.Invoke(ctx, resourceMgr, params, accessToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errStr := err.Error()
|
var tbErr util.ToolboxError
|
||||||
// Missing authService tokens.
|
|
||||||
if errors.Is(err, util.ErrUnauthorized) {
|
if errors.As(err, &tbErr) {
|
||||||
return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err
|
switch tbErr.Category() {
|
||||||
}
|
case util.CategoryAgent:
|
||||||
// Upstream auth error
|
// MCP - Tool execution error
|
||||||
if strings.Contains(errStr, "Error 401") || strings.Contains(errStr, "Error 403") {
|
// Return SUCCESS but with IsError: true
|
||||||
if clientAuth {
|
|
||||||
// Error with client credentials should pass down to the client
|
|
||||||
return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err
|
|
||||||
}
|
|
||||||
// Auth error with ADC should raise internal 500 error
|
|
||||||
return jsonrpc.NewError(id, jsonrpc.INTERNAL_ERROR, err.Error(), nil), err
|
|
||||||
}
|
|
||||||
text := TextContent{
|
text := TextContent{
|
||||||
Type: "text",
|
Type: "text",
|
||||||
Text: err.Error(),
|
Text: err.Error(),
|
||||||
@@ -210,6 +210,28 @@ func toolsCallHandler(ctx context.Context, id jsonrpc.RequestId, resourceMgr *re
|
|||||||
Id: id,
|
Id: id,
|
||||||
Result: CallToolResult{Content: []TextContent{text}, IsError: true},
|
Result: CallToolResult{Content: []TextContent{text}, IsError: true},
|
||||||
}, nil
|
}, nil
|
||||||
|
|
||||||
|
case util.CategoryServer:
|
||||||
|
// MCP Spec - Protocol error
|
||||||
|
// Return JSON-RPC ERROR
|
||||||
|
var clientServerErr *util.ClientServerError
|
||||||
|
rpcCode := jsonrpc.INTERNAL_ERROR // Default to Internal Error (-32603)
|
||||||
|
|
||||||
|
if errors.As(err, &clientServerErr) {
|
||||||
|
if clientServerErr.Code == http.StatusUnauthorized || clientServerErr.Code == http.StatusForbidden {
|
||||||
|
if clientAuth {
|
||||||
|
rpcCode = jsonrpc.INVALID_REQUEST
|
||||||
|
} else {
|
||||||
|
rpcCode = jsonrpc.INTERNAL_ERROR
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return jsonrpc.NewError(id, rpcCode, err.Error(), nil), err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Unknown error -> 500
|
||||||
|
return jsonrpc.NewError(id, jsonrpc.INTERNAL_ERROR, err.Error(), nil), err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
content := make([]TextContent, 0)
|
content := make([]TextContent, 0)
|
||||||
|
|||||||
@@ -184,7 +184,7 @@ func (t Tool) Invoke(ctx context.Context, resourceMgr tools.SourceProvider, para
|
|||||||
if source.UseClientAuthorization() {
|
if source.UseClientAuthorization() {
|
||||||
// Use client-side access token
|
// Use client-side access token
|
||||||
if accessToken == "" {
|
if accessToken == "" {
|
||||||
return nil, fmt.Errorf("tool is configured for client OAuth but no token was provided in the request header: %w", util.ErrUnauthorized)
|
return nil, util.NewClientServerError("tool is configured for client OAuth but no token was provided in the request header", http.StatusUnauthorized, nil)
|
||||||
}
|
}
|
||||||
tokenStr, err = accessToken.ParseBearerToken()
|
tokenStr, err = accessToken.ParseBearerToken()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ package tools
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -80,7 +81,7 @@ type AccessToken string
|
|||||||
func (token AccessToken) ParseBearerToken() (string, error) {
|
func (token AccessToken) ParseBearerToken() (string, error) {
|
||||||
headerParts := strings.Split(string(token), " ")
|
headerParts := strings.Split(string(token), " ")
|
||||||
if len(headerParts) != 2 || strings.ToLower(headerParts[0]) != "bearer" {
|
if len(headerParts) != 2 || strings.ToLower(headerParts[0]) != "bearer" {
|
||||||
return "", fmt.Errorf("authorization header must be in the format 'Bearer <token>': %w", util.ErrUnauthorized)
|
return "", util.NewClientServerError("authorization header must be in the format 'Bearer <token>'", http.StatusUnauthorized, nil)
|
||||||
}
|
}
|
||||||
return headerParts[1], nil
|
return headerParts[1], nil
|
||||||
}
|
}
|
||||||
|
|||||||
61
internal/util/errors.go
Normal file
61
internal/util/errors.go
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
// Copyright 2026 Google LLC
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
package util
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
type ErrorCategory string
|
||||||
|
|
||||||
|
const (
|
||||||
|
CategoryAgent ErrorCategory = "AGENT_ERROR"
|
||||||
|
CategoryServer ErrorCategory = "SERVER_ERROR"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ToolboxError is the interface all custom errors must satisfy
|
||||||
|
type ToolboxError interface {
|
||||||
|
error
|
||||||
|
Category() ErrorCategory
|
||||||
|
}
|
||||||
|
|
||||||
|
// Agent Errors return 200 to the sender
|
||||||
|
type AgentError struct {
|
||||||
|
Msg string
|
||||||
|
Cause error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *AgentError) Error() string { return e.Msg }
|
||||||
|
|
||||||
|
func (e *AgentError) Category() ErrorCategory { return CategoryAgent }
|
||||||
|
|
||||||
|
func (e *AgentError) Unwrap() error { return e.Cause }
|
||||||
|
|
||||||
|
func NewAgentError(msg string, cause error) *AgentError {
|
||||||
|
return &AgentError{Msg: msg, Cause: cause}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClientServerError returns 4XX/5XX error code
|
||||||
|
type ClientServerError struct {
|
||||||
|
Msg string
|
||||||
|
Code int
|
||||||
|
Cause error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ClientServerError) Error() string { return fmt.Sprintf("%s: %v", e.Msg, e.Cause) }
|
||||||
|
|
||||||
|
func (e *ClientServerError) Category() ErrorCategory { return CategoryServer }
|
||||||
|
|
||||||
|
func (e *ClientServerError) Unwrap() error { return e.Cause }
|
||||||
|
|
||||||
|
func NewClientServerError(msg string, code int, cause error) *ClientServerError {
|
||||||
|
return &ClientServerError{Msg: msg, Code: code, Cause: cause}
|
||||||
|
}
|
||||||
@@ -19,6 +19,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
"reflect"
|
"reflect"
|
||||||
"regexp"
|
"regexp"
|
||||||
"slices"
|
"slices"
|
||||||
@@ -118,7 +119,7 @@ func parseFromAuthService(paramAuthServices []ParamAuthService, claimsMap map[st
|
|||||||
}
|
}
|
||||||
return v, nil
|
return v, nil
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("missing or invalid authentication header: %w", util.ErrUnauthorized)
|
return nil, util.NewClientServerError("missing or invalid authentication header", http.StatusUnauthorized, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CheckParamRequired checks if a parameter is required based on the required and default field.
|
// CheckParamRequired checks if a parameter is required based on the required and default field.
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -188,5 +187,3 @@ func InstrumentationFromContext(ctx context.Context) (*telemetry.Instrumentation
|
|||||||
}
|
}
|
||||||
return nil, fmt.Errorf("unable to retrieve instrumentation")
|
return nil, fmt.Errorf("unable to retrieve instrumentation")
|
||||||
}
|
}
|
||||||
|
|
||||||
var ErrUnauthorized = errors.New("unauthorized")
|
|
||||||
|
|||||||
Reference in New Issue
Block a user