├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── config.yml │ ├── feature_request.yml │ └── question.yml ├── PULL_REQUEST_TEMPLATE │ └── PULL_REQUEST_TEMPLATE.md ├── auto-label.yaml ├── blunderbuss.yml ├── header-checker-lint.yml ├── labels.yaml ├── release-please.yml ├── release-trigger.yml ├── renovate.json5 ├── sync-repo-settings.yaml └── workflows │ ├── cloud_build_failure_reporter.yml │ ├── lint-toolbox-core.yaml │ ├── lint-toolbox-langchain.yaml │ ├── lint-toolbox-llamaindex.yaml │ ├── schedule_reporter.yml │ └── sync-labels.yaml ├── .gitignore ├── .kokoro ├── build.sh ├── populate-secrets.sh ├── trampoline.sh └── trampoline_v2.sh ├── .release-please-manifest.json ├── .trampolinerc ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── DEVELOPER.md ├── LICENSE ├── README.md ├── SECURITY.md ├── packages ├── toolbox-core │ ├── CHANGELOG.md │ ├── DEVELOPER.md │ ├── README.md │ ├── integration.cloudbuild.yaml │ ├── pyproject.toml │ ├── requirements.txt │ ├── src │ │ └── toolbox_core │ │ │ ├── __init__.py │ │ │ ├── auth_methods.py │ │ │ ├── client.py │ │ │ ├── protocol.py │ │ │ ├── py.typed │ │ │ ├── sync_client.py │ │ │ ├── sync_tool.py │ │ │ ├── tool.py │ │ │ ├── utils.py │ │ │ └── version.py │ └── tests │ │ ├── conftest.py │ │ ├── test_auth_methods.py │ │ ├── test_client.py │ │ ├── test_e2e.py │ │ ├── test_protocol.py │ │ ├── test_sync_client.py │ │ ├── test_sync_e2e.py │ │ ├── test_sync_tool.py │ │ ├── test_tool.py │ │ └── test_utils.py ├── toolbox-langchain │ ├── CHANGELOG.md │ ├── DEVELOPER.md │ ├── README.md │ ├── integration.cloudbuild.yaml │ ├── pyproject.toml │ ├── requirements.txt │ ├── src │ │ └── toolbox_langchain │ │ │ ├── __init__.py │ │ │ ├── async_client.py │ │ │ ├── async_tools.py │ │ │ ├── client.py │ │ │ ├── py.typed │ │ │ ├── tools.py │ │ │ └── version.py │ └── tests │ │ ├── conftest.py │ │ ├── test_async_client.py │ │ ├── test_async_tools.py │ │ ├── test_client.py │ │ ├── test_e2e.py │ │ └── test_tools.py └── toolbox-llamaindex │ ├── CHANGELOG.md │ ├── DEVELOPER.md │ ├── README.md │ ├── integration.cloudbuild.yaml │ ├── pyproject.toml │ ├── requirements.txt │ ├── src │ └── toolbox_llamaindex │ │ ├── __init__.py │ │ ├── async_client.py │ │ ├── async_tools.py │ │ ├── client.py │ │ ├── py.typed │ │ ├── tools.py │ │ └── version.py │ └── tests │ ├── conftest.py │ ├── test_async_client.py │ ├── test_async_tools.py │ ├── test_client.py │ ├── test_e2e.py │ └── test_tools.py └── release-please-config.json /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # This file controls who is tagged for review for any given pull request. 2 | # 3 | # For syntax help see: 4 | # https://help.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners#codeowners-syntax 5 | 6 | * @googleapis/senseai-eco -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | name: 🐞 Bug Report 16 | description: File a report for unexpected or undesired behavior. 17 | title: "" 18 | labels: ["type: bug"] 19 | 20 | body: 21 | - type: markdown 22 | attributes: 23 | value: | 24 | Thanks for helping us improve! 🙏 Please answer these questions and provide as much information as possible about your problem. 25 | 26 | - id: preamble 27 | type: checkboxes 28 | attributes: 29 | label: Prerequisites 30 | description: | 31 | Please run through the following list and make sure you've tried the usual "quick fixes": 32 | - Search the [current open issues](https://github.com/googleapis/mcp-toolbox-sdk-python/issues) 33 | - Update to the [latest version of 34 | Toolbox](https://github.com/googleapis/genai-toolbox/releases) 35 | - Update to the [latest version of the SDK](https://github.com/googleapis/mcp-toolbox-sdk-python/CHANGELOG.md). 36 | options: 37 | - label: "I've searched the current open issues" 38 | required: true 39 | - label: "I've updated to the latest version of Toolbox" 40 | - label: "I've updated to the latest version of the SDK" 41 | 42 | - type: input 43 | id: version 44 | attributes: 45 | label: Toolbox version 46 | description: | 47 | What version of Toolbox are you using (`toolbox --version`)? e.g. 48 | - toolbox version 0.3.0 49 | - us-central1-docker.pkg.dev/database-toolbox/toolbox/toolbox:0.3.0 50 | placeholder: ex. toolbox version 0.3.0 51 | validations: 52 | required: true 53 | 54 | - type: textarea 55 | id: environment 56 | attributes: 57 | label: Environment 58 | description: "Let us know some details about the environment in which you are seeing the bug!" 59 | value: | 60 | 1. OS type and version: (output of `uname -a`) 61 | 2. How are you running Toolbox: 62 | - As a downloaded binary (e.g. from `curl -O https://storage.googleapis.com/genai-toolbox/v$VERSION/linux/amd64/toolbox`) 63 | - As a container (e.g. from `us-central1-docker.pkg.dev/database-toolbox/toolbox/toolbox:$VERSION`) 64 | - Compiled from source (include the command used to build) 65 | 3. Python version (output of `python --version`) 66 | 4. pip version (output of `pip --version`) 67 | 68 | - type: textarea 69 | id: client 70 | attributes: 71 | label: Client 72 | description: "How are you connecting to Toolbox?" 73 | value: | 74 | 1. Client: . 75 | 2. Version: (`pip show `)? e.g. 76 | - toolbox-core version 0.1.0 77 | 3. Example: If possible, please include your code of configuration: 78 | 79 | ```python 80 | # Code goes here! 81 | ``` 82 | 83 | - id: expected-behavior 84 | type: textarea 85 | attributes: 86 | label: Expected Behavior 87 | description: | 88 | Please enter a detailed description of the behavior you expected, and any information about what behavior you 89 | noticed and why it is defective or unintentional. 90 | validations: 91 | required: true 92 | 93 | - id: current-behavior 94 | type: textarea 95 | attributes: 96 | label: Current Behavior 97 | description: "Please enter a detailed description of the behavior you encountered instead." 98 | validations: 99 | required: true 100 | 101 | - type: textarea 102 | id: repro 103 | attributes: 104 | label: Steps to reproduce? 105 | description: | 106 | How can we reproduce this bug? Please walk us through it step by step, 107 | with as much relevant detail as possible. A 'minimal' reproduction is 108 | preferred, which means removing as much of the examples as possible so 109 | only the minimum required to run and reproduce the bug is left. 110 | value: | 111 | 1. ? 112 | 2. ? 113 | 3. ? 114 | ... 115 | validations: 116 | required: true 117 | 118 | - type: textarea 119 | id: additional-details 120 | attributes: 121 | label: Additional Details 122 | description: | 123 | Any other information you want us to know? Things such as tools config, 124 | server logs, etc. can be included here. -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | blank_issues_enabled: false 16 | contact_links: 17 | - name: Google Cloud Support 18 | url: https://cloud.google.com/support/ 19 | about: If you have a support contract with Google, please both open an issue here and open Google Cloud Support portal with a link to the issue. -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | name: ✨ Feature Request 16 | description: Suggest an idea for new or improved behavior. 17 | title: "" 18 | labels: ["type: feature request"] 19 | 20 | body: 21 | - type: markdown 22 | attributes: 23 | value: | 24 | Thanks for helping us improve! 🙏 Please answer these questions and provide as much information as possible about your feature request. 25 | 26 | - id: preamble 27 | type: checkboxes 28 | attributes: 29 | label: Prerequisites 30 | description: | 31 | Please run through the following list and make sure you've tried the usual "quick fixes": 32 | options: 33 | - label: "Search the [current open issues](https://github.com/googleapis/mcp-toolbox-sdk-python/issues)" 34 | required: true 35 | 36 | - type: textarea 37 | id: use-case 38 | attributes: 39 | label: What are you trying to do that currently feels hard or impossible? 40 | description: "A clear and concise description of what the end goal for the feature should be -- avoid generalizing and try to provide a specific use-case." 41 | validations: 42 | required: true 43 | 44 | - type: textarea 45 | id: suggested-solution 46 | attributes: 47 | label: Suggested Solution(s) 48 | description: "If you have a suggestion for how this use-case can be solved, please feel free to include it." 49 | 50 | - type: textarea 51 | id: alternatives-considered 52 | attributes: 53 | label: Alternatives Considered 54 | description: "Are there any workaround or third party tools to replicate this behavior? Why would adding this feature be preferred over them?" 55 | 56 | - type: textarea 57 | id: additional-details 58 | attributes: 59 | label: Additional Details 60 | description: "Any additional information we should know? Please reference it here (issues, PRs, descriptions, or screenshots)" -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | name: 💬 Question 16 | description: Questions on how something works or the best way to do something? 17 | title: "" 18 | labels: ["type: question"] 19 | 20 | body: 21 | - type: markdown 22 | attributes: 23 | value: | 24 | Thanks for helping us improve! 🙏 Please provide as much information as possible about your question. 25 | 26 | - id: preamble 27 | type: checkboxes 28 | attributes: 29 | label: Prerequisites 30 | description: | 31 | Please run through the following list and make sure you've tried the usual "quick fixes": 32 | options: 33 | - label: "Search the [current open issues](https://github.com/googleapis/mcp-toolbox-sdk-python/issues)" 34 | required: true 35 | 36 | - type: textarea 37 | id: question 38 | attributes: 39 | label: Question 40 | description: "What's your question? Please provide as much relevant information as possible to reduce turnaround time. Include information like what environment, language, or framework you are using." 41 | placeholder: "Example: How do I use Toolbox SDKs with my own orchestration framework?" 42 | validations: 43 | required: true 44 | 45 | - type: textarea 46 | id: code 47 | attributes: 48 | label: Code 49 | description: "Please paste any useful application code that might be relevant to your question. (if your code is in a public repo, feel free to paste a link!)" 50 | 51 | - type: textarea 52 | id: additional-details 53 | attributes: 54 | label: Additional Details 55 | description: "Any other information you want us to know that might be helpful in answering your question? (link issues, PRs, descriptions, or screenshots)." -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: 2 | - [ ] Make sure to open an issue before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea 3 | - [ ] Ensure the tests and linter pass 4 | - [ ] Communicate test infrastructure changes, i.e. API enablement, secrets 5 | - [ ] Appropriate docs were updated (if necessary) 6 | 7 | 🛠️ Fixes # -------------------------------------------------------------------------------- /.github/auto-label.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | enabled: false 16 | -------------------------------------------------------------------------------- /.github/blunderbuss.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | assign_issues: 16 | - kurtisvg 17 | - anubhav756 18 | - twishabansal 19 | assign_prs: 20 | - kurtisvg 21 | - anubhav756 22 | - twishabansal 23 | -------------------------------------------------------------------------------- /.github/header-checker-lint.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | # Presubmit test that ensures that source files contain valid license headers 17 | # https://github.com/googleapis/repo-automation-bots/tree/main/packages/header-checker-lint 18 | # Install: https://github.com/apps/license-header-lint-gcf 19 | 20 | allowedCopyrightHolders: 21 | - "Google LLC" 22 | allowedLicenses: 23 | - "Apache-2.0" 24 | sourceFileExtensions: 25 | - "yaml" 26 | - "yml" 27 | - "sh" 28 | - "proto" 29 | - "Dockerfile" 30 | - "py" 31 | - "text" -------------------------------------------------------------------------------- /.github/labels.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | - name: duplicate 16 | color: ededed 17 | description: "" 18 | 19 | - name: 'type: bug' 20 | color: db4437 21 | description: Error or flaw in code with unintended results or allowing sub-optimal 22 | usage patterns. 23 | - name: 'type: cleanup' 24 | color: c5def5 25 | description: An internal cleanup or hygiene concern. 26 | - name: 'type: docs' 27 | color: 0000A0 28 | description: Improvement to the documentation for an API. 29 | - name: 'type: feature request' 30 | color: c5def5 31 | description: ‘Nice-to-have’ improvement, new feature or different behavior or design. 32 | - name: 'type: process' 33 | color: c5def5 34 | description: A process-related concern. May include testing, release, or the like. 35 | - name: 'type: question' 36 | color: c5def5 37 | description: Request for information or clarification. 38 | 39 | - name: 'priority: p0' 40 | color: b60205 41 | description: Highest priority. Critical issue. P0 implies highest priority. 42 | - name: 'priority: p1' 43 | color: ffa03e 44 | description: Important issue which blocks shipping the next release. Will be fixed 45 | prior to next release. 46 | - name: 'priority: p2' 47 | color: fef2c0 48 | description: Moderately-important priority. Fix may not be included in next release. 49 | - name: 'priority: p3' 50 | color: ffffc7 51 | description: Desirable enhancement or fix. May not be included in next release. 52 | 53 | - name: do not merge 54 | color: d93f0b 55 | description: Indicates a pull request not ready for merge, due to either quality 56 | or timing. 57 | 58 | - name: 'autorelease: pending' 59 | color: ededed 60 | description: Release please needs to do its work on this. 61 | - name: 'autorelease: triggered' 62 | color: ededed 63 | description: Release please has triggered a release for this. 64 | - name: 'autorelease: tagged' 65 | color: ededed 66 | description: Release please has completed a release for this. 67 | 68 | - name: 'tests: run' 69 | color: 3DED97 70 | description: Label to trigger Github Action tests. 71 | -------------------------------------------------------------------------------- /.github/release-please.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | handleGHRelease: true 16 | manifest: true 17 | tagPullRequestNumber: true 18 | -------------------------------------------------------------------------------- /.github/release-trigger.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | enabled: true 16 | multiScmName: mcp-toolbox-sdk-python 17 | -------------------------------------------------------------------------------- /.github/renovate.json5: -------------------------------------------------------------------------------- 1 | { 2 | extends: [ 3 | 'config:recommended', 4 | ':semanticCommits', 5 | ':ignoreUnstable', 6 | 'group:allNonMajor', 7 | ':separateMajorReleases', 8 | ':prConcurrentLimitNone', 9 | ':prHourlyLimitNone', 10 | ':preserveSemverRanges', 11 | ], 12 | minimumReleaseAge: '3', 13 | rebaseWhen: 'conflicted', 14 | dependencyDashboardLabels: [ 15 | 'type: process', 16 | ], 17 | packageRules: [ 18 | { 19 | groupName: 'GitHub Actions', 20 | matchManagers: [ 21 | 'github-actions', 22 | ], 23 | pinDigests: true, 24 | }, 25 | { 26 | matchPackageNames: [ 27 | 'pytest', 28 | ], 29 | matchUpdateTypes: [ 30 | 'minor', 31 | 'major', 32 | ], 33 | }, 34 | { 35 | groupName: 'python-nonmajor', 36 | matchCategories: [ 37 | 'python', 38 | ], 39 | matchUpdateTypes: [ 40 | 'minor', 41 | 'patch', 42 | ], 43 | }, 44 | { 45 | groupName: 'kokoro dependencies', 46 | matchFileNames: [ 47 | '.kokoro/**', 48 | ], 49 | }, 50 | ], 51 | } 52 | -------------------------------------------------------------------------------- /.github/sync-repo-settings.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # Synchronize repository settings from a centralized config 16 | # https://github.com/googleapis/repo-automation-bots/tree/main/packages/sync-repo-settings 17 | # Install: https://github.com/apps/sync-repo-settings 18 | 19 | # Disable merge commits 20 | rebaseMergeAllowed: true 21 | squashMergeAllowed: true 22 | mergeCommitAllowed: false 23 | # Enable branch protection 24 | branchProtectionRules: 25 | - pattern: main 26 | isAdminEnforced: true 27 | requiredStatusCheckContexts: 28 | - "cla/google" 29 | - "lint" 30 | - "conventionalcommits.org" 31 | - "header-check" 32 | # Add required status checks like presubmit tests 33 | - "core-python-sdk-pr-py313 (toolbox-testing-438616)" 34 | - "core-python-sdk-pr-py312 (toolbox-testing-438616)" 35 | - "core-python-sdk-pr-py311 (toolbox-testing-438616)" 36 | - "core-python-sdk-pr-py310 (toolbox-testing-438616)" 37 | - "core-python-sdk-pr-py39 (toolbox-testing-438616)" 38 | - "langchain-python-sdk-pr-py313 (toolbox-testing-438616)" 39 | - "langchain-python-sdk-pr-py312 (toolbox-testing-438616)" 40 | - "langchain-python-sdk-pr-py311 (toolbox-testing-438616)" 41 | - "langchain-python-sdk-pr-py310 (toolbox-testing-438616)" 42 | - "langchain-python-sdk-pr-py39 (toolbox-testing-438616)" 43 | - "llamaindex-python-sdk-pr-py313 (toolbox-testing-438616)" 44 | - "llamaindex-python-sdk-pr-py312 (toolbox-testing-438616)" 45 | - "llamaindex-python-sdk-pr-py311 (toolbox-testing-438616)" 46 | - "llamaindex-python-sdk-pr-py310 (toolbox-testing-438616)" 47 | - "llamaindex-python-sdk-pr-py39 (toolbox-testing-438616)" 48 | requiredApprovingReviewCount: 1 49 | requiresCodeOwnerReviews: true 50 | requiresStrictStatusChecks: true 51 | 52 | # Set team access 53 | permissionRules: 54 | - team: senseai-eco 55 | permission: admin 56 | -------------------------------------------------------------------------------- /.github/workflows/cloud_build_failure_reporter.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | name: Cloud Build Failure Reporter 16 | 17 | on: 18 | workflow_call: 19 | inputs: 20 | trigger_names: 21 | required: true 22 | type: string 23 | workflow_dispatch: 24 | inputs: 25 | trigger_names: 26 | description: 'Cloud Build trigger names separated by comma.' 27 | required: true 28 | default: '' 29 | 30 | jobs: 31 | report: 32 | 33 | permissions: 34 | issues: 'write' 35 | checks: 'read' 36 | 37 | runs-on: 'ubuntu-latest' 38 | 39 | steps: 40 | - uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea' # v7 41 | with: 42 | script: |- 43 | // parse test names 44 | const testNameSubstring = '${{ inputs.trigger_names }}'; 45 | const testNameFound = new Map(); //keeps track of whether each test is found 46 | testNameSubstring.split(',').forEach(testName => { 47 | testNameFound.set(testName, false); 48 | }); 49 | 50 | // label for all issues opened by reporter 51 | const periodicLabel = 'periodic-failure'; 52 | 53 | // check if any reporter opened any issues previously 54 | const prevIssues = await github.paginate(github.rest.issues.listForRepo, { 55 | ...context.repo, 56 | state: 'open', 57 | creator: 'github-actions[bot]', 58 | labels: [periodicLabel] 59 | }); 60 | 61 | // createOrCommentIssue creates a new issue or comments on an existing issue. 62 | const createOrCommentIssue = async function (title, txt) { 63 | if (prevIssues.length < 1) { 64 | console.log('no previous issues found, creating one'); 65 | await github.rest.issues.create({ 66 | ...context.repo, 67 | title: title, 68 | body: txt, 69 | labels: [periodicLabel] 70 | }); 71 | return; 72 | } 73 | // only comment on issue related to the current test 74 | for (const prevIssue of prevIssues) { 75 | if (prevIssue.title.includes(title)){ 76 | console.log( 77 | `found previous issue ${prevIssue.html_url}, adding comment` 78 | ); 79 | 80 | await github.rest.issues.createComment({ 81 | ...context.repo, 82 | issue_number: prevIssue.number, 83 | body: txt 84 | }); 85 | return; 86 | } 87 | } 88 | }; 89 | 90 | // updateIssues comments on any existing issues. No-op if no issue exists. 91 | const updateIssues = async function (checkName, txt) { 92 | if (prevIssues.length < 1) { 93 | console.log('no previous issues found.'); 94 | return; 95 | } 96 | // only comment on issue related to the current test 97 | for (const prevIssue of prevIssues) { 98 | if (prevIssue.title.includes(checkName)){ 99 | console.log(`found previous issue ${prevIssue.html_url}, adding comment`); 100 | await github.rest.issues.createComment({ 101 | ...context.repo, 102 | issue_number: prevIssue.number, 103 | body: txt 104 | }); 105 | } 106 | } 107 | }; 108 | 109 | // Find status of check runs. 110 | // We will find check runs for each commit and then filter for the periodic. 111 | // Checks API only allows for ref and if we use main there could be edge cases where 112 | // the check run happened on a SHA that is different from head. 113 | const commits = await github.paginate(github.rest.repos.listCommits, { 114 | ...context.repo 115 | }); 116 | 117 | const relevantChecks = new Map(); 118 | for (const commit of commits) { 119 | console.log( 120 | `checking runs at ${commit.html_url}: ${commit.commit.message}` 121 | ); 122 | const checks = await github.rest.checks.listForRef({ 123 | ...context.repo, 124 | ref: commit.sha 125 | }); 126 | 127 | // Iterate through each check and find matching names 128 | for (const check of checks.data.check_runs) { 129 | console.log(`Handling test name ${check.name}`); 130 | for (const testName of testNameFound.keys()) { 131 | if (testNameFound.get(testName) === true){ 132 | //skip if a check is already found for this name 133 | continue; 134 | } 135 | if (check.name.includes(testName)) { 136 | relevantChecks.set(check, commit); 137 | testNameFound.set(testName, true); 138 | } 139 | } 140 | } 141 | // Break out of the loop early if all tests are found 142 | const allTestsFound = Array.from(testNameFound.values()).every(value => value === true); 143 | if (allTestsFound){ 144 | break; 145 | } 146 | } 147 | 148 | // Handle each relevant check 149 | relevantChecks.forEach((commit, check) => { 150 | if ( 151 | check.status === 'completed' && 152 | check.conclusion === 'success' 153 | ) { 154 | updateIssues( 155 | check.name, 156 | `[Tests are passing](${check.html_url}) for commit [${commit.sha}](${commit.html_url}).` 157 | ); 158 | } else if (check.status === 'in_progress') { 159 | console.log( 160 | `Check is pending ${check.html_url} for ${commit.html_url}. Retry again later.` 161 | ); 162 | } else { 163 | createOrCommentIssue( 164 | `Cloud Build Failure Reporter: ${check.name} failed`, 165 | `Cloud Build Failure Reporter found test failure for [**${check.name}** ](${check.html_url}) at [${commit.sha}](${commit.html_url}). Please fix the error and then close the issue after the **${check.name}** test passes.` 166 | ); 167 | } 168 | }); 169 | 170 | // no periodic checks found across all commits, report it 171 | const noTestFound = Array.from(testNameFound.values()).every(value => value === false); 172 | if (noTestFound){ 173 | createOrCommentIssue( 174 | 'Missing periodic tests: ${{ inputs.trigger_names }}', 175 | `No periodic test is found for triggers: ${{ inputs.trigger_names }}. Last checked from ${ 176 | commits[0].html_url 177 | } to ${commits[commits.length - 1].html_url}.` 178 | ); 179 | } 180 | -------------------------------------------------------------------------------- /.github/workflows/lint-toolbox-core.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | name: core 16 | on: 17 | pull_request: 18 | paths: 19 | - 'packages/toolbox-core/**' 20 | - '!packages/toolbox-core/**/*.md' 21 | pull_request_target: 22 | types: [labeled] 23 | 24 | # Declare default permissions as read only. 25 | permissions: read-all 26 | 27 | jobs: 28 | lint: 29 | if: "${{ github.event.action != 'labeled' || github.event.label.name == 'tests: run' }}" 30 | name: lint 31 | runs-on: ubuntu-latest 32 | concurrency: 33 | group: ${{ github.workflow }}-${{ github.ref }} 34 | cancel-in-progress: true 35 | defaults: 36 | run: 37 | working-directory: ./packages/toolbox-core 38 | permissions: 39 | contents: 'read' 40 | issues: 'write' 41 | pull-requests: 'write' 42 | steps: 43 | - name: Remove PR Label 44 | if: "${{ github.event.action == 'labeled' && github.event.label.name == 'tests: run' }}" 45 | uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 46 | with: 47 | github-token: ${{ secrets.GITHUB_TOKEN }} 48 | script: | 49 | try { 50 | await github.rest.issues.removeLabel({ 51 | name: 'tests: run', 52 | owner: context.repo.owner, 53 | repo: context.repo.repo, 54 | issue_number: context.payload.pull_request.number 55 | }); 56 | } catch (e) { 57 | console.log('Failed to remove label. Another job may have already removed it!'); 58 | } 59 | - name: Checkout code 60 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 61 | with: 62 | ref: ${{ github.event.pull_request.head.sha }} 63 | repository: ${{ github.event.pull_request.head.repo.full_name }} 64 | token: ${{ secrets.GITHUB_TOKEN }} 65 | - name: Setup Python 66 | uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 67 | with: 68 | python-version: "3.13" 69 | 70 | - name: Install library requirements 71 | run: pip install -r requirements.txt 72 | 73 | - name: Install test requirements 74 | run: pip install .[test] 75 | 76 | - name: Run linters 77 | run: | 78 | black --check . 79 | isort --check . 80 | 81 | - name: Run type-check 82 | env: 83 | MYPYPATH: './src' 84 | run: mypy --install-types --non-interactive --cache-dir=.mypy_cache/ -p toolbox_core -------------------------------------------------------------------------------- /.github/workflows/lint-toolbox-langchain.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | name: langchain 16 | on: 17 | pull_request: 18 | paths: 19 | - 'packages/toolbox-langchain/**' 20 | - '!packages/toolbox-langchain/**/*.md' 21 | pull_request_target: 22 | types: [labeled] 23 | 24 | # Declare default permissions as read only. 25 | permissions: read-all 26 | 27 | jobs: 28 | lint: 29 | if: "${{ github.event.action != 'labeled' || github.event.label.name == 'tests: run' }}" 30 | name: lint 31 | runs-on: ubuntu-latest 32 | concurrency: 33 | group: ${{ github.workflow }}-${{ github.ref }} 34 | cancel-in-progress: true 35 | defaults: 36 | run: 37 | working-directory: ./packages/toolbox-langchain 38 | permissions: 39 | contents: 'read' 40 | issues: 'write' 41 | pull-requests: 'write' 42 | steps: 43 | - name: Remove PR Label 44 | if: "${{ github.event.action == 'labeled' && github.event.label.name == 'tests: run' }}" 45 | uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 46 | with: 47 | github-token: ${{ secrets.GITHUB_TOKEN }} 48 | script: | 49 | try { 50 | await github.rest.issues.removeLabel({ 51 | name: 'tests: run', 52 | owner: context.repo.owner, 53 | repo: context.repo.repo, 54 | issue_number: context.payload.pull_request.number 55 | }); 56 | } catch (e) { 57 | console.log('Failed to remove label. Another job may have already removed it!'); 58 | } 59 | - name: Checkout code 60 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 61 | with: 62 | ref: ${{ github.event.pull_request.head.sha }} 63 | repository: ${{ github.event.pull_request.head.repo.full_name }} 64 | token: ${{ secrets.GITHUB_TOKEN }} 65 | - name: Setup Python 66 | uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 67 | with: 68 | python-version: "3.13" 69 | 70 | - name: Install library requirements 71 | run: pip install -r requirements.txt 72 | 73 | - name: Install test requirements 74 | run: pip install .[test] 75 | 76 | - name: Run linters 77 | run: | 78 | black --check . 79 | isort --check . 80 | 81 | - name: Run type-check 82 | env: 83 | MYPYPATH: './src' 84 | run: mypy --install-types --non-interactive --cache-dir=.mypy_cache/ -p toolbox_langchain -------------------------------------------------------------------------------- /.github/workflows/lint-toolbox-llamaindex.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | name: llamaindex 16 | on: 17 | pull_request: 18 | paths: 19 | - 'packages/toolbox-llamaindex/**' 20 | - '!packages/toolbox-llamaindex/**/*.md' 21 | pull_request_target: 22 | types: [labeled] 23 | 24 | # Declare default permissions as read only. 25 | permissions: read-all 26 | 27 | jobs: 28 | lint: 29 | if: "${{ github.event.action != 'labeled' || github.event.label.name == 'tests: run' }}" 30 | name: lint 31 | runs-on: ubuntu-latest 32 | concurrency: 33 | group: ${{ github.workflow }}-${{ github.ref }} 34 | cancel-in-progress: true 35 | defaults: 36 | run: 37 | working-directory: ./packages/toolbox-llamaindex 38 | permissions: 39 | contents: 'read' 40 | issues: 'write' 41 | pull-requests: 'write' 42 | steps: 43 | - name: Remove PR Label 44 | if: "${{ github.event.action == 'labeled' && github.event.label.name == 'tests: run' }}" 45 | uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 46 | with: 47 | github-token: ${{ secrets.GITHUB_TOKEN }} 48 | script: | 49 | try { 50 | await github.rest.issues.removeLabel({ 51 | name: 'tests: run', 52 | owner: context.repo.owner, 53 | repo: context.repo.repo, 54 | issue_number: context.payload.pull_request.number 55 | }); 56 | } catch (e) { 57 | console.log('Failed to remove label. Another job may have already removed it!'); 58 | } 59 | - name: Checkout code 60 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 61 | with: 62 | ref: ${{ github.event.pull_request.head.sha }} 63 | repository: ${{ github.event.pull_request.head.repo.full_name }} 64 | token: ${{ secrets.GITHUB_TOKEN }} 65 | - name: Setup Python 66 | uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 67 | with: 68 | python-version: "3.13" 69 | 70 | - name: Install library requirements 71 | run: pip install -r requirements.txt 72 | 73 | - name: Install test requirements 74 | run: pip install .[test] 75 | 76 | - name: Run linters 77 | run: | 78 | black --check . 79 | isort --check . 80 | 81 | - name: Run type-check 82 | env: 83 | MYPYPATH: './src' 84 | run: mypy --install-types --non-interactive --cache-dir=.mypy_cache/ -p toolbox_llamaindex -------------------------------------------------------------------------------- /.github/workflows/schedule_reporter.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | name: Schedule Reporter 16 | 17 | on: 18 | schedule: 19 | - cron: '0 6 * * *' # Runs at 6 AM every morning 20 | 21 | jobs: 22 | run_reporter: 23 | permissions: 24 | issues: 'write' 25 | checks: 'read' 26 | contents: 'read' 27 | uses: ./.github/workflows/cloud_build_failure_reporter.yml 28 | with: 29 | trigger_names: "core-python-sdk-test-nightly,core-python-sdk-test-on-merge,langchain-python-sdk-test-nightly,langchain-python-sdk-test-on-merge,llamaindex-python-sdk-test-nightly,llamaindex-python-sdk-test-on-merge" 30 | -------------------------------------------------------------------------------- /.github/workflows/sync-labels.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | name: Sync Labels 16 | on: 17 | push: 18 | branches: 19 | - main 20 | 21 | # Declare default permissions as read only. 22 | permissions: read-all 23 | 24 | jobs: 25 | build: 26 | runs-on: ubuntu-latest 27 | permissions: 28 | contents: 'read' 29 | issues: 'write' 30 | pull-requests: 'write' 31 | steps: 32 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 33 | - uses: micnncim/action-label-syncer@3abd5ab72fda571e69fffd97bd4e0033dd5f495c # v1.3.0 34 | env: 35 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 36 | with: 37 | manifest: .github/labels.yaml 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # direnv 2 | .envrc 3 | 4 | # vscode 5 | .vscode/ 6 | 7 | # python 8 | env 9 | venv 10 | *.pyc 11 | .python-version 12 | **.egg-info/ 13 | __pycache__/** 14 | 15 | -------------------------------------------------------------------------------- /.kokoro/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright 2025 Google LLC 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # https://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | set -eo pipefail 17 | 18 | if [[ -z "${PROJECT_ROOT:-}" ]]; then 19 | PROJECT_ROOT="github/mcp-toolbox-sdk-python" 20 | fi 21 | 22 | cd "${PROJECT_ROOT}" 23 | 24 | # Disable buffering, so that the logs stream through. 25 | export PYTHONUNBUFFERED=1 26 | 27 | # Debug: show build environment 28 | env | grep KOKORO 29 | 30 | # Setup service account credentials. 31 | export GOOGLE_APPLICATION_CREDENTIALS=${KOKORO_GFILE_DIR}/service-account.json 32 | 33 | # Setup project id. 34 | export PROJECT_ID=$(cat "${KOKORO_GFILE_DIR}/project-id.json") 35 | 36 | # Remove old nox 37 | python3 -m pip uninstall --yes --quiet nox-automation 38 | 39 | # Install nox 40 | python3 -m pip install --upgrade --quiet nox 41 | python3 -m nox --version 42 | 43 | # If this is a continuous build, send the test log to the FlakyBot. 44 | # See https://github.com/googleapis/repo-automation-bots/tree/main/packages/flakybot. 45 | if [[ $KOKORO_BUILD_ARTIFACTS_SUBDIR = *"continuous"* ]]; then 46 | cleanup() { 47 | chmod +x $KOKORO_GFILE_DIR/linux_amd64/flakybot 48 | $KOKORO_GFILE_DIR/linux_amd64/flakybot 49 | } 50 | trap cleanup EXIT HUP 51 | fi 52 | 53 | # If NOX_SESSION is set, it only runs the specified session, 54 | # otherwise run all the sessions. 55 | if [[ -n "${NOX_SESSION:-}" ]]; then 56 | python3 -m nox -s ${NOX_SESSION:-} 57 | else 58 | python3 -m nox 59 | fi 60 | -------------------------------------------------------------------------------- /.kokoro/populate-secrets.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright 2025 Google LLC. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | set -eo pipefail 17 | 18 | function now { date +"%Y-%m-%d %H:%M:%S" | tr -d '\n' ;} 19 | function msg { println "$*" >&2 ;} 20 | function println { printf '%s\n' "$(now) $*" ;} 21 | 22 | 23 | # Populates requested secrets set in SECRET_MANAGER_KEYS from service account: 24 | # kokoro-trampoline@cloud-devrel-kokoro-resources.iam.gserviceaccount.com 25 | SECRET_LOCATION="${KOKORO_GFILE_DIR}/secret_manager" 26 | msg "Creating folder on disk for secrets: ${SECRET_LOCATION}" 27 | mkdir -p ${SECRET_LOCATION} 28 | for key in $(echo ${SECRET_MANAGER_KEYS} | sed "s/,/ /g") 29 | do 30 | msg "Retrieving secret ${key}" 31 | docker run --entrypoint=gcloud \ 32 | --volume=${KOKORO_GFILE_DIR}:${KOKORO_GFILE_DIR} \ 33 | gcr.io/google.com/cloudsdktool/cloud-sdk \ 34 | secrets versions access latest \ 35 | --project cloud-devrel-kokoro-resources \ 36 | --secret ${key} > \ 37 | "${SECRET_LOCATION}/${key}" 38 | if [[ $? == 0 ]]; then 39 | msg "Secret written to ${SECRET_LOCATION}/${key}" 40 | else 41 | msg "Error retrieving secret ${key}" 42 | fi 43 | done 44 | -------------------------------------------------------------------------------- /.kokoro/trampoline.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright 2025 Google LLC 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | set -eo pipefail 17 | 18 | # Always run the cleanup script, regardless of the success of bouncing into 19 | # the container. 20 | function cleanup() { 21 | chmod +x ${KOKORO_GFILE_DIR}/trampoline_cleanup.sh 22 | ${KOKORO_GFILE_DIR}/trampoline_cleanup.sh 23 | echo "cleanup"; 24 | } 25 | trap cleanup EXIT 26 | 27 | $(dirname $0)/populate-secrets.sh # Secret Manager secrets. 28 | python3 "${KOKORO_GFILE_DIR}/trampoline_v1.py" -------------------------------------------------------------------------------- /.release-please-manifest.json: -------------------------------------------------------------------------------- 1 | {"packages/toolbox-langchain":"0.2.0","packages/toolbox-core":"0.2.0","packages/toolbox-llamaindex":"0.2.0"} 2 | -------------------------------------------------------------------------------- /.trampolinerc: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # Add required env vars here. 16 | required_envvars+=( 17 | ) 18 | 19 | # Add env vars which are passed down into the container here. 20 | pass_down_envvars+=( 21 | "NOX_SESSION" 22 | ############### 23 | # Docs builds 24 | ############### 25 | "STAGING_BUCKET" 26 | "V2_STAGING_BUCKET" 27 | ################## 28 | # Samples builds 29 | ################## 30 | "INSTALL_LIBRARY_FROM_SOURCE" 31 | "RUN_TESTS_SESSION" 32 | "BUILD_SPECIFIC_GCLOUD_PROJECT" 33 | # Target directories. 34 | "RUN_TESTS_DIRS" 35 | # The nox session to run. 36 | "RUN_TESTS_SESSION" 37 | ) 38 | 39 | # Prevent unintentional override on the default image. 40 | if [[ "${TRAMPOLINE_IMAGE_UPLOAD:-false}" == "true" ]] && \ 41 | [[ -z "${TRAMPOLINE_IMAGE:-}" ]]; then 42 | echo "Please set TRAMPOLINE_IMAGE if you want to upload the Docker image." 43 | exit 1 44 | fi 45 | 46 | # Define the default value if it makes sense. 47 | if [[ -z "${TRAMPOLINE_IMAGE_UPLOAD:-}" ]]; then 48 | TRAMPOLINE_IMAGE_UPLOAD="" 49 | fi 50 | 51 | if [[ -z "${TRAMPOLINE_IMAGE:-}" ]]; then 52 | TRAMPOLINE_IMAGE="" 53 | fi 54 | 55 | if [[ -z "${TRAMPOLINE_DOCKERFILE:-}" ]]; then 56 | TRAMPOLINE_DOCKERFILE="" 57 | fi 58 | 59 | if [[ -z "${TRAMPOLINE_BUILD_FILE:-}" ]]; then 60 | TRAMPOLINE_BUILD_FILE="" 61 | fi -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Please refer to each API's `CHANGELOG.md` file under the `packages/` directory 2 | 3 | Changelogs 4 | ----- 5 | | Package | Version | 6 | | -------- | ------- | 7 | | [toolbox-core](https://github.com/googleapis/mcp-toolbox-sdk-python/tree/main/packages/toolbox-core/CHANGELOG.md) | ![toolbox-core version](https://img.shields.io/pypi/v/toolbox-core.svg) | 8 | | [toolbox-langchain](https://github.com/googleapis/mcp-toolbox-sdk-python/tree/main/packages/toolbox-langchain/CHANGELOG.md) | ![toolbox-langchain version](https://img.shields.io/pypi/v/toolbox-langchain.svg) | 9 | | [toolbox-llamaindex](https://github.com/googleapis/mcp-toolbox-sdk-python/tree/main/packages/toolbox-llamaindex/CHANGELOG.md) | ![toolbox-llamaindex version](https://img.shields.io/pypi/v/toolbox-llamaindex.svg) | 10 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 2 | # Code of Conduct 3 | 4 | ## Our Pledge 5 | 6 | In the interest of fostering an open and welcoming environment, we as 7 | contributors and maintainers pledge to making participation in our project and 8 | our community a harassment-free experience for everyone, regardless of age, body 9 | size, disability, ethnicity, gender identity and expression, level of 10 | experience, education, socio-economic status, nationality, personal appearance, 11 | race, religion, or sexual identity and orientation. 12 | 13 | ## Our Standards 14 | 15 | Examples of behavior that contributes to creating a positive environment 16 | include: 17 | 18 | * Using welcoming and inclusive language 19 | * Being respectful of differing viewpoints and experiences 20 | * Gracefully accepting constructive criticism 21 | * Focusing on what is best for the community 22 | * Showing empathy towards other community members 23 | 24 | Examples of unacceptable behavior by participants include: 25 | 26 | * The use of sexualized language or imagery and unwelcome sexual attention or 27 | advances 28 | * Trolling, insulting/derogatory comments, and personal or political attacks 29 | * Public or private harassment 30 | * Publishing others' private information, such as a physical or electronic 31 | address, without explicit permission 32 | * Other conduct which could reasonably be considered inappropriate in a 33 | professional setting 34 | 35 | ## Our Responsibilities 36 | 37 | Project maintainers are responsible for clarifying the standards of acceptable 38 | behavior and are expected to take appropriate and fair corrective action in 39 | response to any instances of unacceptable behavior. 40 | 41 | Project maintainers have the right and responsibility to remove, edit, or reject 42 | comments, commits, code, wiki edits, issues, and other contributions that are 43 | not aligned to this Code of Conduct, or to ban temporarily or permanently any 44 | contributor for other behaviors that they deem inappropriate, threatening, 45 | offensive, or harmful. 46 | 47 | ## Scope 48 | 49 | This Code of Conduct applies both within project spaces and in public spaces 50 | when an individual is representing the project or its community. Examples of 51 | representing a project or community include using an official project e-mail 52 | address, posting via an official social media account, or acting as an appointed 53 | representative at an online or offline event. Representation of a project may be 54 | further defined and clarified by project maintainers. 55 | 56 | This Code of Conduct also applies outside the project spaces when the Project 57 | Steward has a reasonable belief that an individual's behavior may have a 58 | negative impact on the project or its community. 59 | 60 | ## Conflict Resolution 61 | 62 | We do not believe that all conflict is bad; healthy debate and disagreement 63 | often yield positive results. However, it is never okay to be disrespectful or 64 | to engage in behavior that violates the project’s code of conduct. 65 | 66 | If you see someone violating the code of conduct, you are encouraged to address 67 | the behavior directly with those involved. Many issues can be resolved quickly 68 | and easily, and this gives people more control over the outcome of their 69 | dispute. If you are unable to resolve the matter for any reason, or if the 70 | behavior is threatening or harassing, report it. We are dedicated to providing 71 | an environment where participants feel welcome and safe. 72 | 73 | Reports should be directed to *googleapis-stewards@google.com*, the 74 | Project Steward(s) for *Google Cloud Client Libraries*. It is the Project Steward’s duty to 75 | receive and address reported violations of the code of conduct. They will then 76 | work with a committee consisting of representatives from the Open Source 77 | Programs Office and the Google Open Source Strategy team. If for any reason you 78 | are uncomfortable reaching out to the Project Steward, please email 79 | opensource@google.com. 80 | 81 | We will investigate every complaint, but you may not receive a direct response. 82 | We will use our discretion in determining when and how to follow up on reported 83 | incidents, which may range from not taking action to permanent expulsion from 84 | the project and project-sponsored spaces. We will notify the accused of the 85 | report and provide them an opportunity to discuss it before any action is taken. 86 | The identity of the reporter will be omitted from the details of the report 87 | supplied to the accused. In potentially harmful situations, such as ongoing 88 | harassment or threats to anyone's safety, we may take action without notice. 89 | 90 | ## Attribution 91 | 92 | This Code of Conduct is adapted from the Contributor Covenant, version 1.4, 93 | available at 94 | https://www.contributor-covenant.org/version/1/4/code-of-conduct.html -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | We'd love to accept your patches and contributions to this project. 4 | 5 | ## Before you begin 6 | 7 | ### Sign our Contributor License Agreement 8 | 9 | Contributions to this project must be accompanied by a 10 | [Contributor License Agreement](https://cla.developers.google.com/about) (CLA). 11 | You (or your employer) retain the copyright to your contribution; this simply 12 | gives us permission to use and redistribute your contributions as part of the 13 | project. 14 | 15 | If you or your current employer have already signed the Google CLA (even if it 16 | was for a different project), you probably don't need to do it again. 17 | 18 | Visit to see your current agreements or to 19 | sign a new one. 20 | 21 | ### Review our community guidelines 22 | 23 | This project follows 24 | [Google's Open Source Community Guidelines](https://opensource.google/conduct/). 25 | 26 | ## Contribution process 27 | 28 | ### Code reviews 29 | 30 | All submissions, including submissions by project members, require review. We 31 | use GitHub pull requests for this purpose. Consult 32 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 33 | information on using pull requests. -------------------------------------------------------------------------------- /DEVELOPER.md: -------------------------------------------------------------------------------- 1 | # Development 2 | 3 | Below are the details to set up a development environment and run tests. 4 | 5 | ## Install 6 | 1. Clone the repository: 7 | ```bash 8 | git clone https://github.com/googleapis/mcp-toolbox-sdk-python 9 | ``` 10 | 1. Navigate to the package directory: 11 | ```bash 12 | cd mcp-toolbox-sdk-python/packages/ 13 | ``` 14 | 1. Install the package in editable mode, so changes are reflected without 15 | reinstall: 16 | ```bash 17 | pip install -e . 18 | ``` 19 | 1. Make code changes and contribute to the SDK's development. 20 | > [!TIP] 21 | > Using `-e` option allows you to make changes to the SDK code and have 22 | > those changes reflected immediately without reinstalling the package. 23 | 24 | ## Test 25 | 1. Navigate to the package directory if needed: 26 | ```bash 27 | cd mcp-toolbox-sdk-python/packages/ 28 | ``` 29 | 1. Install the SDK and test dependencies: 30 | ```bash 31 | pip install -e .[test] 32 | ``` 33 | 1. Run tests and/or contribute to the SDK's development. 34 | 35 | ```bash 36 | pytest 37 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![MCP Toolbox 2 | Logo](https://raw.githubusercontent.com/googleapis/genai-toolbox/main/logo.png) 3 | # MCP Toolbox SDKs for Python 4 | 5 | [![License: Apache 6 | 2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) 7 | [![PyPI - Python 8 | Version](https://img.shields.io/pypi/pyversions/toolbox-core)](https://pypi.org/project/toolbox-core/) 9 | 10 | This repository contains Python SDKs designed to seamlessly integrate the 11 | functionalities of the [MCP 12 | Toolbox](https://github.com/googleapis/genai-toolbox) into your Gen AI 13 | applications. These SDKs allow you to load tools defined in Toolbox and use them 14 | as standard Python functions or objects within popular orchestration frameworks 15 | or your custom code. 16 | 17 | This simplifies the process of incorporating external functionalities (like 18 | Databases or APIs) managed by Toolbox into your GenAI applications. 19 | 20 | 21 | - [Overview](#overview) 22 | - [Which Package Should I Use?](#which-package-should-i-use) 23 | - [Available Packages](#available-packages) 24 | - [Getting Started](#getting-started) 25 | - [Contributing](#contributing) 26 | - [License](#license) 27 | - [Support](#support) 28 | 29 | 30 | 31 | ## Overview 32 | 33 | The MCP Toolbox service provides a centralized way to manage and expose tools 34 | (like API connectors, database query tools, etc.) for use by GenAI applications. 35 | 36 | These Python SDKs act as clients for that service. They handle the communication needed to: 37 | 38 | * Fetch tool definitions from your running Toolbox instance. 39 | * Provide convenient Python objects or functions representing those tools. 40 | * Invoke the tools (calling the underlying APIs/services configured in Toolbox). 41 | * Handle authentication and parameter binding as needed. 42 | 43 | By using these SDKs, you can easily leverage your Toolbox-managed tools directly 44 | within your Python applications or AI orchestration frameworks. 45 | 46 | ## Which Package Should I Use? 47 | 48 | Choosing the right package depends on how you are building your application: 49 | 50 | * [`toolbox-langchain`](https://github.com/googleapis/mcp-toolbox-sdk-python/tree/main/packages/toolbox-langchain): 51 | Use this package if you are building your application using the LangChain or 52 | LangGraph frameworks. It provides tools that are directly compatible with the 53 | LangChain ecosystem (`BaseTool` interface), simplifying integration. 54 | * [`toolbox-llamaindex`](https://github.com/googleapis/mcp-toolbox-sdk-python/tree/main/packages/toolbox-llamaindex): 55 | Use this package if you are building your application using the LlamaIndex framework. 56 | It provides tools that are directly compatible with the 57 | LlamaIndex ecosystem (`BaseTool` interface), simplifying integration. 58 | * [`toolbox-core`](https://github.com/googleapis/mcp-toolbox-sdk-python/tree/main/packages/toolbox-core): 59 | Use this package if you are not using LangChain/LangGraph or any other 60 | orchestration framework, or if you need a framework-agnostic way to interact 61 | with Toolbox tools (e.g., for custom orchestration logic or direct use in 62 | Python scripts). 63 | 64 | ## Available Packages 65 | 66 | This repository hosts the following Python packages. See the package-specific 67 | README for detailed installation and usage instructions: 68 | 69 | | Package | Target Use Case | Integration | Path | Details (README) | PyPI Status | 70 | | :------ | :---------- | :---------- | :---------------------- | :---------- | :--------- 71 | | `toolbox-core` | Framework-agnostic / Custom applications | Use directly / Custom | `packages/toolbox-core/` | 📄 [View README](https://github.com/googleapis/mcp-toolbox-sdk-python/blob/main/packages/toolbox-core/README.md) | ![pypi version](https://img.shields.io/pypi/v/toolbox-core.svg) | 72 | | `toolbox-langchain` | LangChain / LangGraph applications | LangChain / LangGraph | `packages/toolbox-langchain/` | 📄 [View README](https://github.com/googleapis/mcp-toolbox-sdk-python/blob/main/packages/toolbox-langchain/README.md) | ![pypi version](https://img.shields.io/pypi/v/toolbox-langchain.svg) | 73 | | `toolbox-llamaindex` | LlamaIndex applications | LlamaIndex | `packages/toolbox-llamaindex/` | 📄 [View README](https://github.com/googleapis/mcp-toolbox-sdk-python/blob/main/packages/toolbox-llamaindex/README.md) | ![pypi version](https://img.shields.io/pypi/v/toolbox-llamaindex.svg) | 74 | 75 | ## Getting Started 76 | 77 | To get started using Toolbox tools with an application, follow these general steps: 78 | 79 | 1. **Set up and Run the Toolbox Service:** 80 | 81 | Before using the SDKs, you need the main MCP Toolbox service running. Follow 82 | the instructions here: [**Toolbox Getting Started 83 | Guide**](https://github.com/googleapis/genai-toolbox?tab=readme-ov-file#getting-started) 84 | 85 | 2. **Install the Appropriate SDK:** 86 | 87 | Choose the package based on your needs (see "[Which Package Should I Use?](#which-package-should-i-use)" above) and install it: 88 | 89 | ```bash 90 | # For the core, framework-agnostic SDK 91 | pip install toolbox-core 92 | 93 | # OR 94 | 95 | # For LangChain/LangGraph integration 96 | pip install toolbox-langchain 97 | 98 | # For the LlamaIndex integration 99 | pip install toolbox-llamaindex 100 | ``` 101 | 102 | 3. **Use the SDK:** 103 | 104 | Consult the README for your chosen package (linked in the "[Available 105 | Packages](#available-packages)" section above) for detailed instructions on 106 | how to connect the client, load tool definitions, invoke tools, configure 107 | authentication/binding, and integrate them into your application or 108 | framework. 109 | 110 | > [!TIP] 111 | > For a complete, end-to-end example including setting up the service and using 112 | > an SDK, see the full tutorial: [**Toolbox Quickstart 113 | > Tutorial**](https://googleapis.github.io/genai-toolbox/getting-started/local_quickstart) 114 | 115 | ## Contributing 116 | 117 | Contributions are welcome! Please refer to the 118 | [`CONTRIBUTING.md`](https://github.com/googleapis/mcp-toolbox-sdk-python/blob/main/CONTRIBUTING.md) 119 | to get started. 120 | 121 | ## License 122 | 123 | This project is licensed under the Apache License 2.0. See the 124 | [LICENSE](https://github.com/googleapis/genai-toolbox/blob/main/LICENSE) file 125 | for details. 126 | 127 | ## Support 128 | 129 | If you encounter issues or have questions, please check the existing [GitHub 130 | Issues](https://github.com/googleapis/genai-toolbox/issues) for the main Toolbox 131 | project. If your issue is specific to one of the SDKs, please look for existing 132 | issues [here](https://github.com/googleapis/mcp-toolbox-sdk-python/issues) or 133 | open a new issue in this repository. 134 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | To report a security issue, please use [g.co/vulnz](https://g.co/vulnz). 4 | 5 | The Google Security Team will respond within 5 working days of your report on g.co/vulnz. 6 | 7 | We use g.co/vulnz for our intake, and do coordination and disclosure here using GitHub Security Advisory to privately discuss and fix the issue. 8 | -------------------------------------------------------------------------------- /packages/toolbox-core/DEVELOPER.md: -------------------------------------------------------------------------------- 1 | # Development 2 | 3 | Below are the details to set up a development environment and run tests. 4 | 5 | ## Install 6 | 1. Clone the repository: 7 | ```bash 8 | git clone https://github.com/googleapis/mcp-toolbox-sdk-python 9 | ``` 10 | 1. Navigate to the package directory: 11 | ```bash 12 | cd mcp-toolbox-sdk-python/packages/toolbox-core 13 | ``` 14 | 1. Install the package in editable mode, so changes are reflected without 15 | reinstall: 16 | ```bash 17 | pip install -e . 18 | ``` 19 | 1. Make code changes and contribute to the SDK's development. 20 | > [!TIP] 21 | > Using `-e` option allows you to make changes to the SDK code and have 22 | > those changes reflected immediately without reinstalling the package. 23 | 24 | ## Test 25 | 1. Navigate to the package directory if needed: 26 | ```bash 27 | cd mcp-toolbox-sdk-python/packages/toolbox-core 28 | ``` 29 | 1. Install the SDK and test dependencies: 30 | ```bash 31 | pip install -e .[test] 32 | ``` 33 | 1. Run tests and/or contribute to the SDK's development. 34 | 35 | ```bash 36 | pytest 37 | ``` 38 | -------------------------------------------------------------------------------- /packages/toolbox-core/integration.cloudbuild.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | steps: 16 | - id: Install library requirements 17 | name: 'python:${_VERSION}' 18 | args: 19 | - install 20 | - '-r' 21 | - 'packages/toolbox-core/requirements.txt' 22 | - '--user' 23 | entrypoint: pip 24 | - id: Install test requirements 25 | name: 'python:${_VERSION}' 26 | args: 27 | - install 28 | - 'packages/toolbox-core[test]' 29 | - '--user' 30 | entrypoint: pip 31 | - id: Run integration tests 32 | name: 'python:${_VERSION}' 33 | env: 34 | - TOOLBOX_URL=$_TOOLBOX_URL 35 | - TOOLBOX_VERSION=$_TOOLBOX_VERSION 36 | - GOOGLE_CLOUD_PROJECT=$PROJECT_ID 37 | args: 38 | - '-c' 39 | - >- 40 | python -m pytest packages/toolbox-core/tests/ 41 | entrypoint: /bin/bash 42 | options: 43 | logging: CLOUD_LOGGING_ONLY 44 | substitutions: 45 | _VERSION: '3.13' 46 | _TOOLBOX_VERSION: '0.5.0' 47 | -------------------------------------------------------------------------------- /packages/toolbox-core/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "toolbox-core" 3 | dynamic = ["version"] 4 | readme = "README.md" 5 | description = "Python Base SDK for interacting with the Toolbox service" 6 | license = {file = "LICENSE"} 7 | requires-python = ">=3.9" 8 | authors = [ 9 | {name = "Google LLC", email = "googleapis-packages@google.com"} 10 | ] 11 | 12 | dependencies = [ 13 | "pydantic>=2.7.0,<3.0.0", 14 | "aiohttp>=3.8.6,<4.0.0", 15 | ] 16 | 17 | classifiers = [ 18 | "Intended Audience :: Developers", 19 | "License :: OSI Approved :: Apache Software License", 20 | "Programming Language :: Python", 21 | "Programming Language :: Python :: 3", 22 | "Programming Language :: Python :: 3.9", 23 | "Programming Language :: Python :: 3.10", 24 | "Programming Language :: Python :: 3.11", 25 | "Programming Language :: Python :: 3.12", 26 | "Programming Language :: Python :: 3.13", 27 | ] 28 | 29 | # Tells setuptools that packages are under the 'src' directory 30 | [tool.setuptools] 31 | package-dir = {"" = "src"} 32 | 33 | [tool.setuptools.dynamic] 34 | version = {attr = "toolbox_core.version.__version__"} 35 | 36 | [project.urls] 37 | Homepage = "https://github.com/googleapis/mcp-toolbox-sdk-python/blob/main/packages/toolbox-core" 38 | Repository = "https://github.com/googleapis/mcp-toolbox-sdk-python.git" 39 | "Bug Tracker" = "https://github.com/googleapis/mcp-toolbox-sdk-python/issues" 40 | Changelog = "https://github.com/googleapis/mcp-toolbox-sdk-python/blob/main/packages/toolbox-core/CHANGELOG.md" 41 | 42 | [project.optional-dependencies] 43 | test = [ 44 | "black[jupyter]==25.1.0", 45 | "isort==6.0.1", 46 | "mypy==1.16.0", 47 | "pytest==8.4.0", 48 | "pytest-aioresponses==0.3.0", 49 | "pytest-asyncio==1.0.0", 50 | "pytest-cov==6.1.1", 51 | "pytest-mock==3.14.1", 52 | "google-cloud-secret-manager==2.23.3", 53 | "google-cloud-storage==3.1.0", 54 | ] 55 | [build-system] 56 | requires = ["setuptools"] 57 | build-backend = "setuptools.build_meta" 58 | 59 | [tool.black] 60 | target-version = ['py39'] 61 | 62 | [tool.isort] 63 | profile = "black" 64 | 65 | [tool.mypy] 66 | python_version = "3.9" 67 | warn_unused_configs = true 68 | disallow_incomplete_defs = true 69 | -------------------------------------------------------------------------------- /packages/toolbox-core/requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp==3.12.9 2 | pydantic==2.11.5 3 | -------------------------------------------------------------------------------- /packages/toolbox-core/src/toolbox_core/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from .client import ToolboxClient 16 | from .sync_client import ToolboxSyncClient 17 | 18 | __all__ = ["ToolboxClient", "ToolboxSyncClient"] 19 | -------------------------------------------------------------------------------- /packages/toolbox-core/src/toolbox_core/auth_methods.py: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """ 16 | This module provides functions to obtain Google ID tokens, formatted as "Bearer" tokens, 17 | for use in the "Authorization" header of HTTP requests. 18 | 19 | Example User Experience: 20 | from toolbox_core import auth_methods 21 | 22 | auth_token_provider = auth_methods.aget_google_id_token 23 | toolbox = ToolboxClient( 24 | URL, 25 | client_headers={"Authorization": auth_token_provider}, 26 | ) 27 | tools = await toolbox.load_toolset() 28 | """ 29 | 30 | from datetime import datetime, timedelta, timezone 31 | from functools import partial 32 | from typing import Any, Dict, Optional 33 | 34 | import google.auth 35 | from google.auth._credentials_async import Credentials 36 | from google.auth._default_async import default_async 37 | from google.auth.transport import _aiohttp_requests 38 | from google.auth.transport.requests import AuthorizedSession, Request 39 | 40 | # --- Constants and Configuration --- 41 | # Prefix for Authorization header tokens 42 | BEARER_TOKEN_PREFIX = "Bearer " 43 | # Margin in seconds to refresh token before its actual expiry 44 | CACHE_REFRESH_MARGIN_SECONDS = 60 45 | 46 | 47 | # --- Global Cache Storage --- 48 | # Stores the cached Google ID token and its expiry timestamp 49 | _cached_google_id_token: Dict[str, Any] = {"token": None, "expires_at": 0} 50 | 51 | 52 | # --- Helper Functions --- 53 | def _is_cached_token_valid( 54 | cache: Dict[str, Any], margin_seconds: int = CACHE_REFRESH_MARGIN_SECONDS 55 | ) -> bool: 56 | """ 57 | Checks if a token in the cache is valid (exists and not expired). 58 | 59 | Args: 60 | cache: The dictionary containing 'token' and 'expires_at'. 61 | margin_seconds: The time in seconds before expiry to consider the token invalid. 62 | 63 | Returns: 64 | True if the token is valid, False otherwise. 65 | """ 66 | if not cache.get("token"): 67 | return False 68 | 69 | expires_at_value = cache.get("expires_at") 70 | if not isinstance(expires_at_value, datetime): 71 | return False 72 | 73 | # Ensure expires_at_value is timezone-aware (UTC). 74 | if ( 75 | expires_at_value.tzinfo is None 76 | or expires_at_value.tzinfo.utcoffset(expires_at_value) is None 77 | ): 78 | expires_at_value = expires_at_value.replace(tzinfo=timezone.utc) 79 | 80 | current_time_utc = datetime.now(timezone.utc) 81 | if current_time_utc + timedelta(seconds=margin_seconds) < expires_at_value: 82 | return True 83 | 84 | return False 85 | 86 | 87 | def _update_token_cache( 88 | cache: Dict[str, Any], new_id_token: Optional[str], expiry: Optional[datetime] 89 | ) -> None: 90 | """ 91 | Updates the global token cache with a new token and its expiry. 92 | 93 | Args: 94 | cache: The dictionary containing 'token' and 'expires_at'. 95 | new_id_token: The new ID token string to cache. 96 | """ 97 | if new_id_token: 98 | cache["token"] = new_id_token 99 | expiry_timestamp = expiry 100 | if expiry_timestamp: 101 | cache["expires_at"] = expiry_timestamp 102 | else: 103 | # If expiry can't be determined, treat as immediately expired to force refresh 104 | cache["expires_at"] = 0 105 | else: 106 | # Clear cache if no new token is provided 107 | cache["token"] = None 108 | cache["expires_at"] = 0 109 | 110 | 111 | # --- Public API Functions --- 112 | def get_google_id_token() -> str: 113 | """ 114 | Synchronously fetches a Google ID token. 115 | 116 | The token is formatted as a 'Bearer' token string and is suitable for use 117 | in an HTTP Authorization header. This function uses Application Default 118 | Credentials. 119 | 120 | Returns: 121 | A string in the format "Bearer ". 122 | 123 | Raises: 124 | Exception: If fetching the Google ID token fails. 125 | """ 126 | if _is_cached_token_valid(_cached_google_id_token): 127 | return BEARER_TOKEN_PREFIX + _cached_google_id_token["token"] 128 | 129 | credentials, _ = google.auth.default() 130 | session = AuthorizedSession(credentials) 131 | request = Request(session) 132 | credentials.refresh(request) 133 | new_id_token = getattr(credentials, "id_token", None) 134 | expiry = getattr(credentials, "expiry") 135 | 136 | _update_token_cache(_cached_google_id_token, new_id_token, expiry) 137 | if new_id_token: 138 | return BEARER_TOKEN_PREFIX + new_id_token 139 | else: 140 | raise Exception("Failed to fetch Google ID token.") 141 | 142 | 143 | async def aget_google_id_token() -> str: 144 | """ 145 | Asynchronously fetches a Google ID token. 146 | 147 | The token is formatted as a 'Bearer' token string and is suitable for use 148 | in an HTTP Authorization header. This function uses Application Default 149 | Credentials. 150 | 151 | Returns: 152 | A string in the format "Bearer ". 153 | 154 | Raises: 155 | Exception: If fetching the Google ID token fails. 156 | """ 157 | if _is_cached_token_valid(_cached_google_id_token): 158 | return BEARER_TOKEN_PREFIX + _cached_google_id_token["token"] 159 | 160 | credentials, _ = default_async() 161 | await credentials.refresh(_aiohttp_requests.Request()) 162 | credentials.before_request = partial(Credentials.before_request, credentials) 163 | new_id_token = getattr(credentials, "id_token", None) 164 | expiry = getattr(credentials, "expiry") 165 | 166 | _update_token_cache(_cached_google_id_token, new_id_token, expiry) 167 | 168 | if new_id_token: 169 | return BEARER_TOKEN_PREFIX + new_id_token 170 | else: 171 | raise Exception("Failed to fetch async Google ID token.") 172 | -------------------------------------------------------------------------------- /packages/toolbox-core/src/toolbox_core/protocol.py: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from inspect import Parameter 16 | from typing import Optional, Type 17 | 18 | from pydantic import BaseModel 19 | 20 | 21 | class ParameterSchema(BaseModel): 22 | """ 23 | Schema for a tool parameter. 24 | """ 25 | 26 | name: str 27 | type: str 28 | description: str 29 | authSources: Optional[list[str]] = None 30 | items: Optional["ParameterSchema"] = None 31 | 32 | def __get_type(self) -> Type: 33 | if self.type == "string": 34 | return str 35 | elif self.type == "integer": 36 | return int 37 | elif self.type == "float": 38 | return float 39 | elif self.type == "boolean": 40 | return bool 41 | elif self.type == "array": 42 | if self.items is None: 43 | raise Exception("Unexpected value: type is 'list' but items is None") 44 | return list[self.items.__get_type()] # type: ignore 45 | 46 | raise ValueError(f"Unsupported schema type: {self.type}") 47 | 48 | def to_param(self) -> Parameter: 49 | return Parameter( 50 | self.name, 51 | Parameter.POSITIONAL_OR_KEYWORD, 52 | annotation=self.__get_type(), 53 | ) 54 | 55 | 56 | class ToolSchema(BaseModel): 57 | """ 58 | Schema for a tool. 59 | """ 60 | 61 | description: str 62 | parameters: list[ParameterSchema] 63 | authRequired: list[str] = [] 64 | 65 | 66 | class ManifestSchema(BaseModel): 67 | """ 68 | Schema for the Toolbox manifest. 69 | """ 70 | 71 | serverVersion: str 72 | tools: dict[str, ToolSchema] 73 | -------------------------------------------------------------------------------- /packages/toolbox-core/src/toolbox_core/py.typed: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | -------------------------------------------------------------------------------- /packages/toolbox-core/src/toolbox_core/sync_client.py: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | from asyncio import AbstractEventLoop, new_event_loop, run_coroutine_threadsafe 17 | from threading import Thread 18 | from typing import Any, Awaitable, Callable, Mapping, Optional, Union 19 | 20 | from .client import ToolboxClient 21 | from .sync_tool import ToolboxSyncTool 22 | 23 | 24 | class ToolboxSyncClient: 25 | """ 26 | A synchronous client for interacting with a Toolbox service. 27 | 28 | Provides methods to discover and load tools defined by a remote Toolbox 29 | service endpoint. 30 | """ 31 | 32 | __loop: Optional[AbstractEventLoop] = None 33 | __thread: Optional[Thread] = None 34 | 35 | def __init__( 36 | self, 37 | url: str, 38 | client_headers: Optional[ 39 | Mapping[str, Union[Callable[[], str], Callable[[], Awaitable[str]], str]] 40 | ] = None, 41 | ): 42 | """ 43 | Initializes the ToolboxSyncClient. 44 | 45 | Args: 46 | url: The base URL for the Toolbox service API (e.g., "http://localhost:5000"). 47 | client_headers: Headers to include in each request sent through this client. 48 | """ 49 | # Running a loop in a background thread allows us to support async 50 | # methods from non-async environments. 51 | if self.__class__.__loop is None: 52 | loop = new_event_loop() 53 | thread = Thread(target=loop.run_forever, daemon=True) 54 | thread.start() 55 | self.__class__.__thread = thread 56 | self.__class__.__loop = loop 57 | 58 | async def create_client(): 59 | return ToolboxClient(url, client_headers=client_headers) 60 | 61 | self.__async_client = run_coroutine_threadsafe( 62 | create_client(), self.__class__.__loop 63 | ).result() 64 | 65 | def close(self): 66 | """ 67 | Synchronously closes the underlying client session. Doing so will cause 68 | any tools created by this Client to cease to function. 69 | """ 70 | coro = self.__async_client.close() 71 | run_coroutine_threadsafe(coro, self.__loop).result() 72 | 73 | def load_tool( 74 | self, 75 | name: str, 76 | auth_token_getters: Mapping[ 77 | str, Union[Callable[[], str], Callable[[], Awaitable[str]]] 78 | ] = {}, 79 | bound_params: Mapping[ 80 | str, Union[Callable[[], Any], Callable[[], Awaitable[Any]], Any] 81 | ] = {}, 82 | ) -> ToolboxSyncTool: 83 | """ 84 | Synchronously loads a tool from the server. 85 | 86 | Retrieves the schema for the specified tool from the Toolbox server and 87 | returns a callable object (`ToolboxSyncTool`) that can be used to invoke the 88 | tool remotely. 89 | 90 | Args: 91 | name: The unique name or identifier of the tool to load. 92 | auth_token_getters: A mapping of authentication service names to 93 | callables that return the corresponding authentication token. 94 | bound_params: A mapping of parameter names to bind to specific values or 95 | callables that are called to produce values as needed. 96 | 97 | Returns: 98 | ToolboxSyncTool: A callable object representing the loaded tool, ready 99 | for execution. The specific arguments and behavior of the callable 100 | depend on the tool itself. 101 | """ 102 | coro = self.__async_client.load_tool(name, auth_token_getters, bound_params) 103 | 104 | if not self.__loop or not self.__thread: 105 | raise ValueError("Background loop or thread cannot be None.") 106 | 107 | async_tool = run_coroutine_threadsafe(coro, self.__loop).result() 108 | return ToolboxSyncTool(async_tool, self.__loop, self.__thread) 109 | 110 | def load_toolset( 111 | self, 112 | name: Optional[str] = None, 113 | auth_token_getters: Mapping[ 114 | str, Union[Callable[[], str], Callable[[], Awaitable[str]]] 115 | ] = {}, 116 | bound_params: Mapping[ 117 | str, Union[Callable[[], Any], Callable[[], Awaitable[Any]], Any] 118 | ] = {}, 119 | strict: bool = False, 120 | ) -> list[ToolboxSyncTool]: 121 | """ 122 | Synchronously fetches a toolset and loads all tools defined within it. 123 | 124 | Args: 125 | name: Name of the toolset to load. If None, loads the default toolset. 126 | auth_token_getters: A mapping of authentication service names to 127 | callables that return the corresponding authentication token. 128 | bound_params: A mapping of parameter names to bind to specific values or 129 | callables that are called to produce values as needed. 130 | strict: If True, raises an error if *any* loaded tool instance fails 131 | to utilize all of the given parameters or auth tokens. (if any 132 | provided). If False (default), raises an error only if a 133 | user-provided parameter or auth token cannot be applied to *any* 134 | loaded tool across the set. 135 | 136 | Returns: 137 | list[ToolboxSyncTool]: A list of callables, one for each tool defined 138 | in the toolset. 139 | 140 | Raises: 141 | ValueError: If validation fails based on the `strict` flag. 142 | """ 143 | coro = self.__async_client.load_toolset( 144 | name, auth_token_getters, bound_params, strict 145 | ) 146 | 147 | if not self.__loop or not self.__thread: 148 | raise ValueError("Background loop or thread cannot be None.") 149 | 150 | async_tools = run_coroutine_threadsafe(coro, self.__loop).result() 151 | return [ 152 | ToolboxSyncTool(async_tool, self.__loop, self.__thread) 153 | for async_tool in async_tools 154 | ] 155 | 156 | def add_headers( 157 | self, 158 | headers: Mapping[ 159 | str, Union[Callable[[], str], Callable[[], Awaitable[str]], str] 160 | ], 161 | ) -> None: 162 | """ 163 | Add headers to be included in each request sent through this client. 164 | 165 | Args: 166 | headers: Headers to include in each request sent through this client. 167 | 168 | Raises: 169 | ValueError: If any of the headers are already registered in the client. 170 | """ 171 | self.__async_client.add_headers(headers) 172 | 173 | def __enter__(self): 174 | """Enter the runtime context related to this client instance.""" 175 | return self 176 | 177 | def __exit__(self, exc_type, exc_val, exc_tb): 178 | """Exit the runtime context and close the client session.""" 179 | self.close() 180 | -------------------------------------------------------------------------------- /packages/toolbox-core/src/toolbox_core/sync_tool.py: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | import asyncio 17 | from asyncio import AbstractEventLoop 18 | from inspect import Signature 19 | from threading import Thread 20 | from typing import Any, Awaitable, Callable, Mapping, Sequence, Union 21 | 22 | from .protocol import ParameterSchema 23 | from .tool import ToolboxTool 24 | 25 | 26 | class ToolboxSyncTool: 27 | """ 28 | A callable proxy object representing a specific tool on a remote Toolbox server. 29 | 30 | Instances of this class behave like synchronous functions. When called, they 31 | send a request to the corresponding tool's endpoint on the Toolbox server with 32 | the provided arguments. 33 | 34 | It utilizes Python's introspection features (`__name__`, `__doc__`, 35 | `__signature__`, `__annotations__`) so that standard tools like `help()` 36 | and `inspect` work as expected. 37 | """ 38 | 39 | def __init__( 40 | self, async_tool: ToolboxTool, loop: AbstractEventLoop, thread: Thread 41 | ): 42 | """ 43 | Initializes a callable that will trigger the tool invocation through the 44 | Toolbox server. 45 | 46 | Args: 47 | async_tool: An instance of the asynchronous ToolboxTool. 48 | loop: The event loop used to run asynchronous tasks. 49 | thread: The thread to run blocking operations in. 50 | """ 51 | 52 | if not isinstance(async_tool, ToolboxTool): 53 | raise TypeError("async_tool must be an instance of ToolboxTool") 54 | 55 | self.__async_tool = async_tool 56 | self.__loop = loop 57 | self.__thread = thread 58 | 59 | # NOTE: We cannot define __qualname__ as a @property here. 60 | # Properties are designed to compute values dynamically when accessed on an *instance* (using 'self'). 61 | # However, Python needs the class's __qualname__ attribute to be a plain string 62 | # *before* any instances exist, specifically when the 'class ToolboxSyncTool:' statement 63 | # itself is being processed during module import or class definition. 64 | # Defining __qualname__ as a property leads to a TypeError because the class object needs 65 | # a string value immediately, not a descriptor that evaluates later. 66 | self.__qualname__ = ( 67 | f"{self.__class__.__qualname__}.{self.__async_tool.__name__}" 68 | ) 69 | 70 | @property 71 | def __name__(self) -> str: 72 | return self.__async_tool.__name__ 73 | 74 | @property 75 | def __doc__(self) -> Union[str, None]: # type: ignore[override] 76 | # Standard Python object attributes like __doc__ are technically "writable". 77 | # But not defining a setter function makes this a read-only property. 78 | # Mypy flags this issue in the type checks. 79 | return self.__async_tool.__doc__ 80 | 81 | @property 82 | def __signature__(self) -> Signature: 83 | return self.__async_tool.__signature__ 84 | 85 | @property 86 | def __annotations__(self) -> dict[str, Any]: # type: ignore[override] 87 | # Standard Python object attributes like __doc__ are technically "writable". 88 | # But not defining a setter function makes this a read-only property. 89 | # Mypy flags this issue in the type checks. 90 | return self.__async_tool.__annotations__ 91 | 92 | @property 93 | def _name(self) -> str: 94 | return self.__async_tool._name 95 | 96 | @property 97 | def _description(self) -> str: 98 | return self.__async_tool._description 99 | 100 | @property 101 | def _params(self) -> Sequence[ParameterSchema]: 102 | return self.__async_tool._params 103 | 104 | @property 105 | def _bound_params( 106 | self, 107 | ) -> Mapping[str, Union[Callable[[], Any], Callable[[], Awaitable[Any]], Any]]: 108 | return self.__async_tool._bound_params 109 | 110 | @property 111 | def _required_auth_params(self) -> Mapping[str, list[str]]: 112 | return self.__async_tool._required_auth_params 113 | 114 | @property 115 | def _auth_service_token_getters( 116 | self, 117 | ) -> Mapping[str, Union[Callable[[], str], Callable[[], Awaitable[str]]]]: 118 | return self.__async_tool._auth_service_token_getters 119 | 120 | @property 121 | def _client_headers( 122 | self, 123 | ) -> Mapping[str, Union[Callable[[], str], Callable[[], Awaitable[str]], str]]: 124 | return self.__async_tool._client_headers 125 | 126 | def __call__(self, *args: Any, **kwargs: Any) -> str: 127 | """ 128 | Synchronously calls the remote tool with the provided arguments. 129 | 130 | Validates arguments against the tool's signature, then sends them 131 | as a JSON payload in a POST request to the tool's invoke URL. 132 | 133 | Args: 134 | *args: Positional arguments for the tool. 135 | **kwargs: Keyword arguments for the tool. 136 | 137 | Returns: 138 | The string result returned by the remote tool execution. 139 | """ 140 | coro = self.__async_tool(*args, **kwargs) 141 | return asyncio.run_coroutine_threadsafe(coro, self.__loop).result() 142 | 143 | def add_auth_token_getters( 144 | self, 145 | auth_token_getters: Mapping[ 146 | str, Union[Callable[[], str], Callable[[], Awaitable[str]]] 147 | ], 148 | ) -> "ToolboxSyncTool": 149 | """ 150 | Registers auth token getter functions that are used for AuthServices 151 | when tools are invoked. 152 | 153 | Args: 154 | auth_token_getters: A mapping of authentication service names to 155 | callables that return the corresponding authentication token. 156 | 157 | Returns: 158 | A new ToolboxSyncTool instance with the specified authentication 159 | token getters registered. 160 | 161 | Raises: 162 | ValueError: If an auth source has already been registered either to 163 | the tool or to the corresponding client. 164 | 165 | """ 166 | new_async_tool = self.__async_tool.add_auth_token_getters(auth_token_getters) 167 | return ToolboxSyncTool(new_async_tool, self.__loop, self.__thread) 168 | 169 | def add_auth_token_getter( 170 | self, 171 | auth_source: str, 172 | get_id_token: Union[Callable[[], str], Callable[[], Awaitable[str]]], 173 | ) -> "ToolboxSyncTool": 174 | """ 175 | Registers an auth token getter function that is used for AuthService 176 | when tools are invoked. 177 | 178 | Args: 179 | auth_source: The name of the authentication source. 180 | get_id_token: A function that returns the ID token. 181 | 182 | Returns: 183 | A new ToolboxSyncTool instance with the specified authentication 184 | token getter registered. 185 | 186 | Raises: 187 | ValueError: If the auth source has already been registered either to 188 | the tool or to the corresponding client. 189 | 190 | """ 191 | return self.add_auth_token_getters({auth_source: get_id_token}) 192 | 193 | def bind_params( 194 | self, 195 | bound_params: Mapping[ 196 | str, Union[Callable[[], Any], Callable[[], Awaitable[Any]], Any] 197 | ], 198 | ) -> "ToolboxSyncTool": 199 | """ 200 | Binds parameters to values or callables that produce values. 201 | 202 | Args: 203 | bound_params: A mapping of parameter names to values or callables 204 | that produce values. 205 | 206 | Returns: 207 | A new ToolboxSyncTool instance with the specified parameters bound. 208 | 209 | Raises: 210 | ValueError: If a parameter is already bound or is not defined by the 211 | tool's definition. 212 | 213 | """ 214 | new_async_tool = self.__async_tool.bind_params(bound_params) 215 | return ToolboxSyncTool(new_async_tool, self.__loop, self.__thread) 216 | 217 | def bind_param( 218 | self, 219 | param_name: str, 220 | param_value: Union[Callable[[], Any], Callable[[], Awaitable[Any]], Any], 221 | ) -> "ToolboxSyncTool": 222 | """ 223 | Binds a parameter to the value or callable that produce the value. 224 | 225 | Args: 226 | param_name: The name of the bound parameter. 227 | param_value: The value of the bound parameter, or a callable that 228 | returns the value. 229 | 230 | Returns: 231 | A new ToolboxSyncTool instance with the specified parameter bound. 232 | 233 | Raises: 234 | ValueError: If the parameter is already bound or is not defined by 235 | the tool's definition. 236 | 237 | """ 238 | return self.bind_params({param_name: param_value}) 239 | -------------------------------------------------------------------------------- /packages/toolbox-core/src/toolbox_core/utils.py: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | import asyncio 17 | from typing import ( 18 | Any, 19 | Awaitable, 20 | Callable, 21 | Iterable, 22 | Mapping, 23 | Sequence, 24 | Type, 25 | Union, 26 | cast, 27 | ) 28 | 29 | from pydantic import BaseModel, Field, create_model 30 | 31 | from toolbox_core.protocol import ParameterSchema 32 | 33 | 34 | def create_func_docstring(description: str, params: Sequence[ParameterSchema]) -> str: 35 | """Convert tool description and params into its function docstring""" 36 | docstring = description 37 | if not params: 38 | return docstring 39 | docstring += "\n\nArgs:" 40 | for p in params: 41 | docstring += ( 42 | f"\n {p.name} ({p.to_param().annotation.__name__}): {p.description}" 43 | ) 44 | return docstring 45 | 46 | 47 | def identify_auth_requirements( 48 | req_authn_params: Mapping[str, list[str]], 49 | req_authz_tokens: Sequence[str], 50 | auth_service_names: Iterable[str], 51 | ) -> tuple[dict[str, list[str]], list[str], set[str]]: 52 | """ 53 | Identifies authentication parameters and authorization tokens that are still 54 | required because they are not covered by the provided `auth_service_names`. 55 | Also returns a set of all authentication/authorization services from 56 | `auth_service_names` that were found to be matching. 57 | 58 | Args: 59 | req_authn_params: A mapping of parameter names to lists of required 60 | authentication services for those parameters. 61 | req_authz_tokens: A list of strings representing all authorization 62 | tokens that are required to invoke the current tool. 63 | auth_service_names: An iterable of authentication/authorization service 64 | names for which token getters are available. 65 | 66 | Returns: 67 | A tuple containing: 68 | - required_authn_params: A new dictionary representing the subset of 69 | required authentication parameters that are not covered by the 70 | provided `auth_service_names`. 71 | - required_authz_tokens: A list of required authorization tokens if 72 | no service name in `auth_service_names` matches any token in 73 | `req_authz_tokens`. If any match is found, this list is empty. 74 | - used_services: A set of service names from `auth_service_names` 75 | that were found to satisfy at least one authentication parameter's 76 | requirements or matched one of the `req_authz_tokens`. 77 | """ 78 | required_authn_params: dict[str, list[str]] = {} 79 | used_services: set[str] = set() 80 | 81 | # find which of the required authn params are covered by available services. 82 | for param, services in req_authn_params.items(): 83 | 84 | # if we don't have a token_getter for any of the services required by the param, 85 | # the param is still required 86 | matched_authn_services = [s for s in services if s in auth_service_names] 87 | 88 | if matched_authn_services: 89 | used_services.update(matched_authn_services) 90 | else: 91 | required_authn_params[param] = services 92 | 93 | # find which of the required authz tokens are covered by available services. 94 | matched_authz_services = [s for s in auth_service_names if s in req_authz_tokens] 95 | required_authz_tokens: list[str] = [] 96 | 97 | # If a match is found, authorization is met (no remaining required tokens). 98 | # Otherwise, all `req_authz_tokens` are still required. (Handles empty 99 | # `req_authz_tokens` correctly, resulting in no required tokens). 100 | if matched_authz_services: 101 | used_services.update(matched_authz_services) 102 | else: 103 | required_authz_tokens = list(req_authz_tokens) 104 | 105 | return required_authn_params, required_authz_tokens, used_services 106 | 107 | 108 | def params_to_pydantic_model( 109 | tool_name: str, params: Sequence[ParameterSchema] 110 | ) -> Type[BaseModel]: 111 | """Converts the given parameters to a Pydantic BaseModel class.""" 112 | field_definitions = {} 113 | for field in params: 114 | field_definitions[field.name] = cast( 115 | Any, 116 | ( 117 | field.to_param().annotation, 118 | Field(description=field.description), 119 | ), 120 | ) 121 | return create_model(tool_name, **field_definitions) 122 | 123 | 124 | async def resolve_value( 125 | source: Union[Callable[[], Any], Callable[[], Awaitable[Any]], Any], 126 | ) -> Any: 127 | """ 128 | Asynchronously or synchronously resolves a given source to its value. 129 | 130 | If the `source` is a coroutine function, it will be awaited. 131 | If the `source` is a regular callable, it will be called. 132 | Otherwise (if it's not a callable), the `source` itself is returned directly. 133 | 134 | Args: 135 | source: The value, a callable returning a value, or a callable 136 | returning an awaitable value. 137 | 138 | Returns: 139 | The resolved value. 140 | """ 141 | 142 | if asyncio.iscoroutinefunction(source): 143 | return await source() 144 | elif callable(source): 145 | return source() 146 | return source 147 | -------------------------------------------------------------------------------- /packages/toolbox-core/src/toolbox_core/version.py: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | __version__ = "0.2.0" 16 | -------------------------------------------------------------------------------- /packages/toolbox-core/tests/conftest.py: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """Contains pytest fixtures that are accessible from all 16 | files present in the same directory.""" 17 | 18 | from __future__ import annotations 19 | 20 | import os 21 | import platform 22 | import subprocess 23 | import tempfile 24 | import time 25 | from typing import Generator 26 | 27 | import google 28 | import pytest_asyncio 29 | from google.auth import compute_engine 30 | from google.cloud import secretmanager, storage 31 | 32 | 33 | #### Define Utility Functions 34 | def get_env_var(key: str) -> str: 35 | """Gets environment variables.""" 36 | value = os.environ.get(key) 37 | if value is None: 38 | raise ValueError(f"Must set env var {key}") 39 | return value 40 | 41 | 42 | def access_secret_version( 43 | project_id: str, secret_id: str, version_id: str = "latest" 44 | ) -> str: 45 | """Accesses the payload of a given secret version from Secret Manager.""" 46 | client = secretmanager.SecretManagerServiceClient() 47 | name = f"projects/{project_id}/secrets/{secret_id}/versions/{version_id}" 48 | response = client.access_secret_version(request={"name": name}) 49 | return response.payload.data.decode("UTF-8") 50 | 51 | 52 | def create_tmpfile(content: str) -> str: 53 | """Creates a temporary file with the given content.""" 54 | with tempfile.NamedTemporaryFile(delete=False, mode="w") as tmpfile: 55 | tmpfile.write(content) 56 | return tmpfile.name 57 | 58 | 59 | def download_blob( 60 | bucket_name: str, source_blob_name: str, destination_file_name: str 61 | ) -> None: 62 | """Downloads a blob from a GCS bucket.""" 63 | storage_client = storage.Client() 64 | 65 | bucket = storage_client.bucket(bucket_name) 66 | blob = bucket.blob(source_blob_name) 67 | blob.download_to_filename(destination_file_name) 68 | 69 | print(f"Blob {source_blob_name} downloaded to {destination_file_name}.") 70 | 71 | 72 | def get_toolbox_binary_url(toolbox_version: str) -> str: 73 | """Constructs the GCS path to the toolbox binary.""" 74 | os_system = platform.system().lower() 75 | arch = ( 76 | "arm64" if os_system == "darwin" and platform.machine() == "arm64" else "amd64" 77 | ) 78 | return f"v{toolbox_version}/{os_system}/{arch}/toolbox" 79 | 80 | 81 | def get_auth_token(client_id: str) -> str: 82 | """Retrieves an authentication token""" 83 | request = google.auth.transport.requests.Request() 84 | credentials = compute_engine.IDTokenCredentials( 85 | request=request, 86 | target_audience=client_id, 87 | use_metadata_identity_endpoint=True, 88 | ) 89 | if not credentials.valid: 90 | credentials.refresh(request) 91 | return credentials.token 92 | 93 | 94 | #### Define Fixtures 95 | @pytest_asyncio.fixture(scope="session") 96 | def project_id() -> str: 97 | return get_env_var("GOOGLE_CLOUD_PROJECT") 98 | 99 | 100 | @pytest_asyncio.fixture(scope="session") 101 | def toolbox_version() -> str: 102 | return get_env_var("TOOLBOX_VERSION") 103 | 104 | 105 | @pytest_asyncio.fixture(scope="session") 106 | def tools_file_path(project_id: str) -> Generator[str]: 107 | """Provides a temporary file path containing the tools manifest.""" 108 | tools_manifest = access_secret_version( 109 | project_id=project_id, secret_id="sdk_testing_tools" 110 | ) 111 | tools_file_path = create_tmpfile(tools_manifest) 112 | yield tools_file_path 113 | os.remove(tools_file_path) 114 | 115 | 116 | @pytest_asyncio.fixture(scope="session") 117 | def auth_token1(project_id: str) -> str: 118 | client_id = access_secret_version( 119 | project_id=project_id, secret_id="sdk_testing_client1" 120 | ) 121 | return get_auth_token(client_id) 122 | 123 | 124 | @pytest_asyncio.fixture(scope="session") 125 | def auth_token2(project_id: str) -> str: 126 | client_id = access_secret_version( 127 | project_id=project_id, secret_id="sdk_testing_client2" 128 | ) 129 | return get_auth_token(client_id) 130 | 131 | 132 | @pytest_asyncio.fixture(scope="session") 133 | def toolbox_server(toolbox_version: str, tools_file_path: str) -> Generator[None]: 134 | """Starts the toolbox server as a subprocess.""" 135 | print("Downloading toolbox binary from gcs bucket...") 136 | source_blob_name = get_toolbox_binary_url(toolbox_version) 137 | download_blob("genai-toolbox", source_blob_name, "toolbox") 138 | print("Toolbox binary downloaded successfully.") 139 | try: 140 | print("Opening toolbox server process...") 141 | # Make toolbox executable 142 | os.chmod("toolbox", 0o700) 143 | # Run toolbox binary 144 | toolbox_server = subprocess.Popen( 145 | ["./toolbox", "--tools_file", tools_file_path] 146 | ) 147 | 148 | # Wait for server to start 149 | # Retry logic with a timeout 150 | for _ in range(5): # retries 151 | time.sleep(2) 152 | print("Checking if toolbox is successfully started...") 153 | if toolbox_server.poll() is None: 154 | print("Toolbox server started successfully.") 155 | break 156 | else: 157 | raise RuntimeError("Toolbox server failed to start after 5 retries.") 158 | except subprocess.CalledProcessError as e: 159 | print(e.stderr.decode("utf-8")) 160 | print(e.stdout.decode("utf-8")) 161 | raise RuntimeError(f"{e}\n\n{e.stderr.decode('utf-8')}") from e 162 | yield 163 | 164 | # Clean up toolbox server 165 | toolbox_server.terminate() 166 | toolbox_server.wait(timeout=5) 167 | -------------------------------------------------------------------------------- /packages/toolbox-core/tests/test_e2e.py: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | import pytest 15 | import pytest_asyncio 16 | from pydantic import ValidationError 17 | 18 | from toolbox_core.client import ToolboxClient 19 | from toolbox_core.tool import ToolboxTool 20 | 21 | 22 | # --- Shared Fixtures Defined at Module Level --- 23 | @pytest_asyncio.fixture(scope="function") 24 | async def toolbox(): 25 | """Creates a ToolboxClient instance shared by all tests in this module.""" 26 | toolbox = ToolboxClient("http://localhost:5000") 27 | try: 28 | yield toolbox 29 | finally: 30 | await toolbox.close() 31 | 32 | 33 | @pytest_asyncio.fixture(scope="function") 34 | async def get_n_rows_tool(toolbox: ToolboxClient) -> ToolboxTool: 35 | """Load the 'get-n-rows' tool using the shared toolbox client.""" 36 | tool = await toolbox.load_tool("get-n-rows") 37 | assert tool.__name__ == "get-n-rows" 38 | return tool 39 | 40 | 41 | @pytest.mark.asyncio 42 | @pytest.mark.usefixtures("toolbox_server") 43 | class TestBasicE2E: 44 | @pytest.mark.parametrize( 45 | "toolset_name, expected_length, expected_tools", 46 | [ 47 | ("my-toolset", 1, ["get-row-by-id"]), 48 | ("my-toolset-2", 2, ["get-n-rows", "get-row-by-id"]), 49 | ], 50 | ) 51 | async def test_load_toolset_specific( 52 | self, 53 | toolbox: ToolboxClient, 54 | toolset_name: str, 55 | expected_length: int, 56 | expected_tools: list[str], 57 | ): 58 | """Load a specific toolset""" 59 | toolset = await toolbox.load_toolset(toolset_name) 60 | assert len(toolset) == expected_length 61 | tool_names = {tool.__name__ for tool in toolset} 62 | assert tool_names == set(expected_tools) 63 | 64 | async def test_load_toolset_default(self, toolbox: ToolboxClient): 65 | """Load the default toolset, i.e. all tools.""" 66 | toolset = await toolbox.load_toolset() 67 | assert len(toolset) == 5 68 | tool_names = {tool.__name__ for tool in toolset} 69 | expected_tools = [ 70 | "get-row-by-content-auth", 71 | "get-row-by-email-auth", 72 | "get-row-by-id-auth", 73 | "get-row-by-id", 74 | "get-n-rows", 75 | ] 76 | assert tool_names == set(expected_tools) 77 | 78 | async def test_run_tool(self, get_n_rows_tool: ToolboxTool): 79 | """Invoke a tool.""" 80 | response = await get_n_rows_tool(num_rows="2") 81 | 82 | assert isinstance(response, str) 83 | assert "row1" in response 84 | assert "row2" in response 85 | assert "row3" not in response 86 | 87 | async def test_run_tool_missing_params(self, get_n_rows_tool: ToolboxTool): 88 | """Invoke a tool with missing params.""" 89 | with pytest.raises(TypeError, match="missing a required argument: 'num_rows'"): 90 | await get_n_rows_tool() 91 | 92 | async def test_run_tool_wrong_param_type(self, get_n_rows_tool: ToolboxTool): 93 | """Invoke a tool with wrong param type.""" 94 | with pytest.raises( 95 | ValidationError, 96 | match=r"num_rows\s+Input should be a valid string\s+\[type=string_type,\s+input_value=2,\s+input_type=int\]", 97 | ): 98 | await get_n_rows_tool(num_rows=2) 99 | 100 | 101 | @pytest.mark.asyncio 102 | @pytest.mark.usefixtures("toolbox_server") 103 | class TestBindParams: 104 | async def test_bind_params( 105 | self, toolbox: ToolboxClient, get_n_rows_tool: ToolboxTool 106 | ): 107 | """Bind a param to an existing tool.""" 108 | new_tool = get_n_rows_tool.bind_params({"num_rows": "3"}) 109 | response = await new_tool() 110 | assert isinstance(response, str) 111 | assert "row1" in response 112 | assert "row2" in response 113 | assert "row3" in response 114 | assert "row4" not in response 115 | 116 | async def test_bind_params_callable( 117 | self, toolbox: ToolboxClient, get_n_rows_tool: ToolboxTool 118 | ): 119 | """Bind a callable param to an existing tool.""" 120 | new_tool = get_n_rows_tool.bind_params({"num_rows": lambda: "3"}) 121 | response = await new_tool() 122 | assert isinstance(response, str) 123 | assert "row1" in response 124 | assert "row2" in response 125 | assert "row3" in response 126 | assert "row4" not in response 127 | 128 | 129 | @pytest.mark.asyncio 130 | @pytest.mark.usefixtures("toolbox_server") 131 | class TestAuth: 132 | async def test_run_tool_unauth_with_auth( 133 | self, toolbox: ToolboxClient, auth_token2: str 134 | ): 135 | """Tests running a tool that doesn't require auth, with auth provided.""" 136 | 137 | with pytest.raises( 138 | ValueError, 139 | match=rf"Validation failed for tool 'get-row-by-id': unused auth tokens: my-test-auth", 140 | ): 141 | await toolbox.load_tool( 142 | "get-row-by-id", 143 | auth_token_getters={"my-test-auth": lambda: auth_token2}, 144 | ) 145 | 146 | async def test_run_tool_no_auth(self, toolbox: ToolboxClient): 147 | """Tests running a tool requiring auth without providing auth.""" 148 | tool = await toolbox.load_tool("get-row-by-id-auth") 149 | with pytest.raises( 150 | PermissionError, 151 | match="One or more of the following authn services are required to invoke this tool: my-test-auth", 152 | ): 153 | await tool(id="2") 154 | 155 | async def test_run_tool_wrong_auth(self, toolbox: ToolboxClient, auth_token2: str): 156 | """Tests running a tool with incorrect auth. The tool 157 | requires a different authentication than the one provided.""" 158 | tool = await toolbox.load_tool("get-row-by-id-auth") 159 | auth_tool = tool.add_auth_token_getters({"my-test-auth": lambda: auth_token2}) 160 | with pytest.raises( 161 | Exception, 162 | match="tool invocation not authorized", 163 | ): 164 | await auth_tool(id="2") 165 | 166 | async def test_run_tool_auth(self, toolbox: ToolboxClient, auth_token1: str): 167 | """Tests running a tool with correct auth.""" 168 | tool = await toolbox.load_tool("get-row-by-id-auth") 169 | auth_tool = tool.add_auth_token_getters({"my-test-auth": lambda: auth_token1}) 170 | response = await auth_tool(id="2") 171 | assert "row2" in response 172 | 173 | @pytest.mark.asyncio 174 | async def test_run_tool_async_auth(self, toolbox: ToolboxClient, auth_token1: str): 175 | """Tests running a tool with correct auth using an async token getter.""" 176 | tool = await toolbox.load_tool("get-row-by-id-auth") 177 | 178 | async def get_token_asynchronously(): 179 | return auth_token1 180 | 181 | auth_tool = tool.add_auth_token_getters( 182 | {"my-test-auth": get_token_asynchronously} 183 | ) 184 | response = await auth_tool(id="2") 185 | assert "row2" in response 186 | 187 | async def test_run_tool_param_auth_no_auth(self, toolbox: ToolboxClient): 188 | """Tests running a tool with a param requiring auth, without auth.""" 189 | tool = await toolbox.load_tool("get-row-by-email-auth") 190 | with pytest.raises( 191 | PermissionError, 192 | match="One or more of the following authn services are required to invoke this tool: my-test-auth", 193 | ): 194 | await tool() 195 | 196 | async def test_run_tool_param_auth(self, toolbox: ToolboxClient, auth_token1: str): 197 | """Tests running a tool with a param requiring auth, with correct auth.""" 198 | tool = await toolbox.load_tool( 199 | "get-row-by-email-auth", 200 | auth_token_getters={"my-test-auth": lambda: auth_token1}, 201 | ) 202 | response = await tool() 203 | assert "row4" in response 204 | assert "row5" in response 205 | assert "row6" in response 206 | 207 | async def test_run_tool_param_auth_no_field( 208 | self, toolbox: ToolboxClient, auth_token1: str 209 | ): 210 | """Tests running a tool with a param requiring auth, with insufficient auth.""" 211 | tool = await toolbox.load_tool( 212 | "get-row-by-content-auth", 213 | auth_token_getters={"my-test-auth": lambda: auth_token1}, 214 | ) 215 | with pytest.raises( 216 | Exception, 217 | match="no field named row_data in claims", 218 | ): 219 | await tool() 220 | -------------------------------------------------------------------------------- /packages/toolbox-core/tests/test_protocol.py: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | from inspect import Parameter 17 | 18 | import pytest 19 | 20 | from toolbox_core.protocol import ParameterSchema 21 | 22 | 23 | def test_parameter_schema_float(): 24 | """Tests ParameterSchema with type 'float'.""" 25 | schema = ParameterSchema(name="price", type="float", description="The item price") 26 | expected_type = float 27 | assert schema._ParameterSchema__get_type() == expected_type 28 | 29 | param = schema.to_param() 30 | assert isinstance(param, Parameter) 31 | assert param.name == "price" 32 | assert param.annotation == expected_type 33 | assert param.kind == Parameter.POSITIONAL_OR_KEYWORD 34 | assert param.default == Parameter.empty 35 | 36 | 37 | def test_parameter_schema_boolean(): 38 | """Tests ParameterSchema with type 'boolean'.""" 39 | schema = ParameterSchema( 40 | name="is_active", type="boolean", description="Activity status" 41 | ) 42 | expected_type = bool 43 | assert schema._ParameterSchema__get_type() == expected_type 44 | 45 | param = schema.to_param() 46 | assert isinstance(param, Parameter) 47 | assert param.name == "is_active" 48 | assert param.annotation == expected_type 49 | assert param.kind == Parameter.POSITIONAL_OR_KEYWORD 50 | 51 | 52 | def test_parameter_schema_array_string(): 53 | """Tests ParameterSchema with type 'array' containing strings.""" 54 | item_schema = ParameterSchema(name="", type="string", description="") 55 | schema = ParameterSchema( 56 | name="tags", type="array", description="List of tags", items=item_schema 57 | ) 58 | 59 | assert schema._ParameterSchema__get_type() == list[str] 60 | 61 | param = schema.to_param() 62 | assert isinstance(param, Parameter) 63 | assert param.name == "tags" 64 | assert param.annotation == list[str] 65 | assert param.kind == Parameter.POSITIONAL_OR_KEYWORD 66 | 67 | 68 | def test_parameter_schema_array_integer(): 69 | """Tests ParameterSchema with type 'array' containing integers.""" 70 | item_schema = ParameterSchema(name="", type="integer", description="") 71 | schema = ParameterSchema( 72 | name="scores", type="array", description="List of scores", items=item_schema 73 | ) 74 | 75 | param = schema.to_param() 76 | assert isinstance(param, Parameter) 77 | assert param.name == "scores" 78 | assert param.annotation == list[int] 79 | assert param.kind == Parameter.POSITIONAL_OR_KEYWORD 80 | 81 | 82 | def test_parameter_schema_array_no_items_error(): 83 | """Tests that 'array' type raises error if 'items' is None.""" 84 | schema = ParameterSchema( 85 | name="bad_list", type="array", description="List without item type" 86 | ) 87 | 88 | expected_error_msg = "Unexpected value: type is 'list' but items is None" 89 | with pytest.raises(Exception, match=expected_error_msg): 90 | schema._ParameterSchema__get_type() 91 | 92 | with pytest.raises(Exception, match=expected_error_msg): 93 | schema.to_param() 94 | 95 | 96 | def test_parameter_schema_unsupported_type_error(): 97 | """Tests that an unsupported type raises ValueError.""" 98 | unsupported_type = "datetime" 99 | schema = ParameterSchema( 100 | name="event_time", type=unsupported_type, description="When it happened" 101 | ) 102 | 103 | expected_error_msg = f"Unsupported schema type: {unsupported_type}" 104 | with pytest.raises(ValueError, match=expected_error_msg): 105 | schema._ParameterSchema__get_type() 106 | 107 | with pytest.raises(ValueError, match=expected_error_msg): 108 | schema.to_param() 109 | -------------------------------------------------------------------------------- /packages/toolbox-core/tests/test_sync_e2e.py: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import pytest 16 | 17 | from toolbox_core.sync_client import ToolboxSyncClient 18 | from toolbox_core.sync_tool import ToolboxSyncTool 19 | 20 | 21 | # --- Shared Fixtures Defined at Module Level --- 22 | @pytest.fixture(scope="module") 23 | def toolbox(): 24 | """Creates a ToolboxSyncClient instance shared by all tests in this module.""" 25 | toolbox = ToolboxSyncClient("http://localhost:5000") 26 | try: 27 | yield toolbox 28 | finally: 29 | toolbox.close() 30 | 31 | 32 | @pytest.fixture(scope="function") 33 | def get_n_rows_tool(toolbox: ToolboxSyncClient) -> ToolboxSyncTool: 34 | """Load the 'get-n-rows' tool using the shared toolbox client.""" 35 | tool = toolbox.load_tool("get-n-rows") 36 | assert tool.__name__ == "get-n-rows" 37 | return tool 38 | 39 | 40 | @pytest.mark.usefixtures("toolbox_server") 41 | class TestBasicE2E: 42 | @pytest.mark.parametrize( 43 | "toolset_name, expected_length, expected_tools", 44 | [ 45 | ("my-toolset", 1, ["get-row-by-id"]), 46 | ("my-toolset-2", 2, ["get-n-rows", "get-row-by-id"]), 47 | ], 48 | ) 49 | def test_load_toolset_specific( 50 | self, 51 | toolbox: ToolboxSyncClient, 52 | toolset_name: str, 53 | expected_length: int, 54 | expected_tools: list[str], 55 | ): 56 | """Load a specific toolset""" 57 | toolset = toolbox.load_toolset(toolset_name) 58 | assert len(toolset) == expected_length 59 | tool_names = {tool.__name__ for tool in toolset} 60 | assert tool_names == set(expected_tools) 61 | 62 | def test_run_tool(self, get_n_rows_tool: ToolboxSyncTool): 63 | """Invoke a tool.""" 64 | response = get_n_rows_tool(num_rows="2") 65 | 66 | assert isinstance(response, str) 67 | assert "row1" in response 68 | assert "row2" in response 69 | assert "row3" not in response 70 | 71 | def test_run_tool_missing_params(self, get_n_rows_tool: ToolboxSyncTool): 72 | """Invoke a tool with missing params.""" 73 | with pytest.raises(TypeError, match="missing a required argument: 'num_rows'"): 74 | get_n_rows_tool() 75 | 76 | def test_run_tool_wrong_param_type(self, get_n_rows_tool: ToolboxSyncTool): 77 | """Invoke a tool with wrong param type.""" 78 | with pytest.raises( 79 | Exception, 80 | match=r"num_rows\s+Input should be a valid string\s+\[type=string_type,\s+input_value=2,\s+input_type=int\]", 81 | ): 82 | get_n_rows_tool(num_rows=2) 83 | 84 | 85 | @pytest.mark.usefixtures("toolbox_server") 86 | class TestBindParams: 87 | def test_bind_params( 88 | self, toolbox: ToolboxSyncClient, get_n_rows_tool: ToolboxSyncTool 89 | ): 90 | """Bind a param to an existing tool.""" 91 | new_tool = get_n_rows_tool.bind_params({"num_rows": "3"}) 92 | response = new_tool() 93 | assert isinstance(response, str) 94 | assert "row1" in response 95 | assert "row2" in response 96 | assert "row3" in response 97 | assert "row4" not in response 98 | 99 | def test_bind_params_callable( 100 | self, toolbox: ToolboxSyncClient, get_n_rows_tool: ToolboxSyncTool 101 | ): 102 | """Bind a callable param to an existing tool.""" 103 | new_tool = get_n_rows_tool.bind_params({"num_rows": lambda: "3"}) 104 | response = new_tool() 105 | assert isinstance(response, str) 106 | assert "row1" in response 107 | assert "row2" in response 108 | assert "row3" in response 109 | assert "row4" not in response 110 | 111 | 112 | @pytest.mark.usefixtures("toolbox_server") 113 | class TestAuth: 114 | def test_run_tool_unauth_with_auth( 115 | self, toolbox: ToolboxSyncClient, auth_token2: str 116 | ): 117 | """Tests running a tool that doesn't require auth, with auth provided.""" 118 | 119 | with pytest.raises( 120 | ValueError, 121 | match=rf"Validation failed for tool 'get-row-by-id': unused auth tokens: my-test-auth", 122 | ): 123 | toolbox.load_tool( 124 | "get-row-by-id", 125 | auth_token_getters={"my-test-auth": lambda: auth_token2}, 126 | ) 127 | 128 | def test_run_tool_no_auth(self, toolbox: ToolboxSyncClient): 129 | """Tests running a tool requiring auth without providing auth.""" 130 | tool = toolbox.load_tool("get-row-by-id-auth") 131 | with pytest.raises( 132 | PermissionError, 133 | match="One or more of the following authn services are required to invoke this tool: my-test-auth", 134 | ): 135 | tool(id="2") 136 | 137 | def test_run_tool_wrong_auth(self, toolbox: ToolboxSyncClient, auth_token2: str): 138 | """Tests running a tool with incorrect auth. The tool 139 | requires a different authentication than the one provided.""" 140 | tool = toolbox.load_tool("get-row-by-id-auth") 141 | auth_tool = tool.add_auth_token_getters({"my-test-auth": lambda: auth_token2}) 142 | with pytest.raises( 143 | Exception, 144 | match="tool invocation not authorized", 145 | ): 146 | auth_tool(id="2") 147 | 148 | def test_run_tool_auth(self, toolbox: ToolboxSyncClient, auth_token1: str): 149 | """Tests running a tool with correct auth.""" 150 | tool = toolbox.load_tool("get-row-by-id-auth") 151 | auth_tool = tool.add_auth_token_getters({"my-test-auth": lambda: auth_token1}) 152 | response = auth_tool(id="2") 153 | assert "row2" in response 154 | 155 | def test_run_tool_param_auth_no_auth(self, toolbox: ToolboxSyncClient): 156 | """Tests running a tool with a param requiring auth, without auth.""" 157 | tool = toolbox.load_tool("get-row-by-email-auth") 158 | with pytest.raises( 159 | PermissionError, 160 | match="One or more of the following authn services are required to invoke this tool: my-test-auth", 161 | ): 162 | tool() 163 | 164 | def test_run_tool_param_auth(self, toolbox: ToolboxSyncClient, auth_token1: str): 165 | """Tests running a tool with a param requiring auth, with correct auth.""" 166 | tool = toolbox.load_tool( 167 | "get-row-by-email-auth", 168 | auth_token_getters={"my-test-auth": lambda: auth_token1}, 169 | ) 170 | response = tool() 171 | assert "row4" in response 172 | assert "row5" in response 173 | assert "row6" in response 174 | 175 | def test_run_tool_param_auth_no_field( 176 | self, toolbox: ToolboxSyncClient, auth_token1: str 177 | ): 178 | """Tests running a tool with a param requiring auth, with insufficient auth.""" 179 | tool = toolbox.load_tool( 180 | "get-row-by-content-auth", 181 | auth_token_getters={"my-test-auth": lambda: auth_token1}, 182 | ) 183 | with pytest.raises( 184 | Exception, 185 | match="no field named row_data in claims", 186 | ): 187 | tool() 188 | -------------------------------------------------------------------------------- /packages/toolbox-langchain/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.2.0](https://github.com/googleapis/mcp-toolbox-sdk-python/compare/toolbox-langchain-v0.1.0...toolbox-langchain-v0.2.0) (2025-05-20) 4 | 5 | 6 | ### ⚠ BREAKING CHANGES 7 | 8 | * **toolbox-langchain:** Base toolbox-langchain over toolbox-core ([#229](https://github.com/googleapis/mcp-toolbox-sdk-python/issues/229)) 9 | 10 | ### Features 11 | 12 | * **toolbox-langchain:** Base toolbox-langchain over toolbox-core ([#229](https://github.com/googleapis/mcp-toolbox-sdk-python/issues/229)) ([03d1b16](https://github.com/googleapis/mcp-toolbox-sdk-python/commit/03d1b160db602f7aeb1c25bc77014ff440ea7504)) 13 | 14 | 15 | ### Bug Fixes 16 | 17 | * **deps:** update python-nonmajor ([#148](https://github.com/googleapis/mcp-toolbox-sdk-python/issues/148)) ([bc894e1](https://github.com/googleapis/mcp-toolbox-sdk-python/commit/bc894e1d14823e88a3c0f24ab05b8839a3d653e0)) 18 | * **deps:** update python-nonmajor ([#175](https://github.com/googleapis/mcp-toolbox-sdk-python/issues/175)) ([73e5a4a](https://github.com/googleapis/mcp-toolbox-sdk-python/commit/73e5a4ac63ee39486529952351c06179ee268c7c)) 19 | * **deps:** update python-nonmajor ([#180](https://github.com/googleapis/mcp-toolbox-sdk-python/issues/180)) ([8d909a9](https://github.com/googleapis/mcp-toolbox-sdk-python/commit/8d909a9e19abed4a02e30a4dfc48e06afdbb01ea)) 20 | * **deps:** update python-nonmajor ([#98](https://github.com/googleapis/mcp-toolbox-sdk-python/issues/98)) ([f03e7ec](https://github.com/googleapis/mcp-toolbox-sdk-python/commit/f03e7ec986eddfb1e0adc81b8be8e9140dcbd530)) 21 | 22 | 23 | ### Miscellaneous Chores 24 | 25 | * Auto-update core package dependency version ([#251](https://github.com/googleapis/mcp-toolbox-sdk-python/issues/251)) ([1c49d2c](https://github.com/googleapis/mcp-toolbox-sdk-python/commit/1c49d2c6e717adc8ec5f08c0d0464e343f9ce4f2)) 26 | * change port number to default toolbox port ([#135](https://github.com/googleapis/mcp-toolbox-sdk-python/issues/135)) ([6164b09](https://github.com/googleapis/mcp-toolbox-sdk-python/commit/6164b09d60412a0e3faf95c1b2e8df13b5ab7782)) 27 | * Define precedence for deprecated 'auth_tokens' vs. 'auth_headers' ([#237](https://github.com/googleapis/mcp-toolbox-sdk-python/issues/237)) ([e9c428b](https://github.com/googleapis/mcp-toolbox-sdk-python/commit/e9c428bfe48cedf67ef984ed2d1769e3b8042cc6)) 28 | * **deps:** update dependency pydantic to v2.11.3 ([#163](https://github.com/googleapis/mcp-toolbox-sdk-python/issues/163)) ([6a78495](https://github.com/googleapis/mcp-toolbox-sdk-python/commit/6a78495ecfe8b51992f55518ab0e7dca1bd6f849)) 29 | * **deps:** update dependency pydantic to v2.11.4 ([#200](https://github.com/googleapis/mcp-toolbox-sdk-python/issues/200)) ([758f620](https://github.com/googleapis/mcp-toolbox-sdk-python/commit/758f620e25427396b52d257722d7f71312421ad1)) 30 | * **deps:** update python-nonmajor ([#207](https://github.com/googleapis/mcp-toolbox-sdk-python/issues/207)) ([83ba029](https://github.com/googleapis/mcp-toolbox-sdk-python/commit/83ba029280089d1c0d4974e5910830048586fa49)) 31 | * **deps:** update python-nonmajor ([#250](https://github.com/googleapis/mcp-toolbox-sdk-python/issues/250)) ([8fb9762](https://github.com/googleapis/mcp-toolbox-sdk-python/commit/8fb976258dda5549218f9f4e75257983866790f0)) 32 | * fix supported python versions ([#191](https://github.com/googleapis/mcp-toolbox-sdk-python/issues/191)) ([f308b5f](https://github.com/googleapis/mcp-toolbox-sdk-python/commit/f308b5f7d7019635798000d0921cf3f549075fd8)) 33 | * fix urls in pyproject.toml ([#101](https://github.com/googleapis/mcp-toolbox-sdk-python/issues/101)) ([6e5875e](https://github.com/googleapis/mcp-toolbox-sdk-python/commit/6e5875eb5c3dbce9c9efb00418716577f90e4d05)) 34 | * **main:** release toolbox-langchain 0.1.1 ([#54](https://github.com/googleapis/mcp-toolbox-sdk-python/issues/54)) ([3e4edf1](https://github.com/googleapis/mcp-toolbox-sdk-python/commit/3e4edf12841ed880073ac0e4e905a51c4cc7c9a9)) 35 | * move to correct readme ([#198](https://github.com/googleapis/mcp-toolbox-sdk-python/issues/198)) ([99d0ad0](https://github.com/googleapis/mcp-toolbox-sdk-python/commit/99d0ad043071b89a937ee90bffb3f24ecc03a2e7)) 36 | * move toolbox-llamaindex package ([#192](https://github.com/googleapis/mcp-toolbox-sdk-python/issues/192)) ([293854f](https://github.com/googleapis/mcp-toolbox-sdk-python/commit/293854ff514c015968d205ab731dcd040a143df6)) 37 | * Pin toolbox-core version ([#248](https://github.com/googleapis/mcp-toolbox-sdk-python/issues/248)) ([ec423ea](https://github.com/googleapis/mcp-toolbox-sdk-python/commit/ec423eaec2adae5272997a0727238ec1ea494ca2)) 38 | * rebrand as MCP Toolbox ([#156](https://github.com/googleapis/mcp-toolbox-sdk-python/issues/156)) ([d090a3f](https://github.com/googleapis/mcp-toolbox-sdk-python/commit/d090a3f2af35a2e3e1e5d59b3176b026af510b7b)) 39 | * refactor layout for multiple packages ([#99](https://github.com/googleapis/mcp-toolbox-sdk-python/issues/99)) ([ac43090](https://github.com/googleapis/mcp-toolbox-sdk-python/commit/ac43090822fbf19a8920732e2ec3aa8b9c3130c1)) 40 | * release 0.1.0 ([#24](https://github.com/googleapis/mcp-toolbox-sdk-python/issues/24)) ([6fff8e2](https://github.com/googleapis/mcp-toolbox-sdk-python/commit/6fff8e2ea18bd6df9f30d7790b6076cf0b32cc75)) 41 | * rename repo ([#165](https://github.com/googleapis/mcp-toolbox-sdk-python/issues/165)) ([70a476a](https://github.com/googleapis/mcp-toolbox-sdk-python/commit/70a476a4fed46a905fe77101c3c1077fd6d5bd21)) 42 | * Restore add_auth_token(s) as deprecated for backward compatibility ([#236](https://github.com/googleapis/mcp-toolbox-sdk-python/issues/236)) ([fcdfdae](https://github.com/googleapis/mcp-toolbox-sdk-python/commit/fcdfdae29dc11e623897b6e412ecd97b9e078758)) 43 | * Update auth_token(s) as auth_token_getter(s) and add_auth_token(s) as add_auth_token_getter(s) ([#182](https://github.com/googleapis/mcp-toolbox-sdk-python/issues/182)) ([48fd28c](https://github.com/googleapis/mcp-toolbox-sdk-python/commit/48fd28c63421429e7bf287194769dab26ede2d10)) 44 | * update toolbox version ([#188](https://github.com/googleapis/mcp-toolbox-sdk-python/issues/188)) ([58d8f7d](https://github.com/googleapis/mcp-toolbox-sdk-python/commit/58d8f7d4601495faf2a33a48cc26bb0a599622ed)) 45 | * update toolbox version ([#190](https://github.com/googleapis/mcp-toolbox-sdk-python/issues/190)) ([87e21ed](https://github.com/googleapis/mcp-toolbox-sdk-python/commit/87e21ed07035ec96fb7b6c730585061d17d727c7)) 46 | * update toolbox version ([#226](https://github.com/googleapis/mcp-toolbox-sdk-python/issues/226)) ([2a92def](https://github.com/googleapis/mcp-toolbox-sdk-python/commit/2a92def08825417b32faa523a3355eba34351955)) 47 | 48 | 49 | ### Documentation 50 | 51 | * Update docstring for strict flag to make it unambiguous ([#247](https://github.com/googleapis/mcp-toolbox-sdk-python/issues/247)) ([59f0634](https://github.com/googleapis/mcp-toolbox-sdk-python/commit/59f063446b98965c1fa8818d8ab93d5cd5d0b2ec)) 52 | 53 | ## 0.1.0 (2025-02-05) 54 | 55 | 56 | ### ⚠ BREAKING CHANGES 57 | 58 | * Improve PyPI package name 59 | * Migrate existing state and APIs to a tools level class 60 | * deprecate 'add_auth_headers' in favor of 'add_auth_tokens' 61 | 62 | ### Features 63 | 64 | * Add support for sync operations ([9885469](https://github.com/googleapis/genai-toolbox-langchain-python/commit/9885469703d88afc7c7aed10c85e97c099d7e532)) 65 | *Add features for binding parameters to ToolboxTool class. ([4fcfc35](https://github.com/googleapis/genai-toolbox-langchain-python/commit/4fcfc3549038c52c495d452f36037817a30eed2e)) 66 | *Add Toolbox SDK for LangChain ([d4a24e6](https://github.com/googleapis/genai-toolbox-langchain-python/commit/d4a24e66139cb985d7457d9162766ce564c36656)) 67 | * Correctly parse Manifest API response as JSON ([86bcf1c](https://github.com/googleapis/genai-toolbox-langchain-python/commit/86bcf1c4db65aa5214f4db280d55cfc23edac361)) 68 | * Migrate existing state and APIs to a tools level class. ([6fe2e39](https://github.com/googleapis/genai-toolbox-langchain-python/commit/6fe2e39eb16eeeeaedea0a31fc2125b105d633b4)) 69 | * Support authentication in LangChain Toolbox SDK. ([b28bdb5](https://github.com/googleapis/genai-toolbox-langchain-python/commit/b28bdb5b12cdfe3fe6768345c00a65a65d91b81b)) 70 | * Implement OAuth support for LlamaIndex. ([dc47da9](https://github.com/googleapis/genai-toolbox-langchain-python/commit/dc47da9282af876939f60d6b24e5a9cf3bf75dfd)) 71 | * Make ClientSession optional when initializing ToolboxClient ([956591d](https://github.com/googleapis/genai-toolbox-langchain-python/commit/956591d1da69495df3f602fd9e5fd967bd7ea5ca)) 72 | 73 | 74 | ### Bug Fixes 75 | 76 | * Deprecate 'add_auth_headers' in favor of 'add_auth_tokens' ([c5c699c](https://github.com/googleapis/genai-toolbox-langchain-python/commit/c5c699cc29bcc0708a31bff90e8cec489982fe2a)) 77 | -------------------------------------------------------------------------------- /packages/toolbox-langchain/DEVELOPER.md: -------------------------------------------------------------------------------- 1 | # Development 2 | 3 | Below are the details to set up a development environment and run tests. 4 | 5 | ## Install 6 | 1. Clone the repository: 7 | ```bash 8 | git clone https://github.com/googleapis/mcp-toolbox-sdk-python 9 | ``` 10 | 1. Navigate to the package directory: 11 | ```bash 12 | cd mcp-toolbox-sdk-python/packages/toolbox-langchain 13 | ``` 14 | 1. Install the package in editable mode, so changes are reflected without 15 | reinstall: 16 | ```bash 17 | pip install -e . 18 | ``` 19 | 1. Make code changes and contribute to the SDK's development. 20 | > [!TIP] 21 | > Using `-e` option allows you to make changes to the SDK code and have 22 | > those changes reflected immediately without reinstalling the package. 23 | 24 | ## Test 25 | 1. Navigate to the package directory if needed: 26 | ```bash 27 | cd mcp-toolbox-sdk-python/packages/toolbox-langchain 28 | ``` 29 | 1. Install the SDK and test dependencies: 30 | ```bash 31 | pip install -e .[test] 32 | ``` 33 | 1. Run tests and/or contribute to the SDK's development. 34 | 35 | ```bash 36 | pytest 37 | ``` 38 | -------------------------------------------------------------------------------- /packages/toolbox-langchain/integration.cloudbuild.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | steps: 16 | - id: Install library requirements 17 | name: 'python:${_VERSION}' 18 | dir: 'packages/toolbox-langchain' 19 | args: 20 | - install 21 | - '-r' 22 | - 'requirements.txt' 23 | - '--user' 24 | entrypoint: pip 25 | - id: Install test requirements 26 | name: 'python:${_VERSION}' 27 | args: 28 | - install 29 | - 'packages/toolbox-langchain[test]' 30 | - '--user' 31 | entrypoint: pip 32 | - id: Run integration tests 33 | name: 'python:${_VERSION}' 34 | env: 35 | - TOOLBOX_URL=$_TOOLBOX_URL 36 | - TOOLBOX_VERSION=$_TOOLBOX_VERSION 37 | - GOOGLE_CLOUD_PROJECT=$PROJECT_ID 38 | args: 39 | - '-c' 40 | - >- 41 | python -m pytest packages/toolbox-langchain/tests/ 42 | entrypoint: /bin/bash 43 | options: 44 | logging: CLOUD_LOGGING_ONLY 45 | substitutions: 46 | _VERSION: '3.13' 47 | _TOOLBOX_VERSION: '0.5.0' 48 | -------------------------------------------------------------------------------- /packages/toolbox-langchain/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "toolbox-langchain" 3 | dynamic = ["version"] 4 | readme = "README.md" 5 | description = "Python SDK for interacting with the Toolbox service with LangChain" 6 | license = {file = "LICENSE"} 7 | requires-python = ">=3.9" 8 | authors = [ 9 | {name = "Google LLC", email = "googleapis-packages@google.com"} 10 | ] 11 | dependencies = [ 12 | "toolbox-core==0.2.0", # x-release-please-version 13 | "langchain-core>=0.2.23,<1.0.0", 14 | "PyYAML>=6.0.1,<7.0.0", 15 | "pydantic>=2.7.0,<3.0.0", 16 | "aiohttp>=3.8.6,<4.0.0", 17 | "deprecated>=1.1.0,<2.0.0", 18 | ] 19 | 20 | classifiers = [ 21 | "Intended Audience :: Developers", 22 | "License :: OSI Approved :: Apache Software License", 23 | "Programming Language :: Python", 24 | "Programming Language :: Python :: 3", 25 | "Programming Language :: Python :: 3.9", 26 | "Programming Language :: Python :: 3.10", 27 | "Programming Language :: Python :: 3.11", 28 | "Programming Language :: Python :: 3.12", 29 | "Programming Language :: Python :: 3.13", 30 | ] 31 | 32 | # Tells setuptools that packages are under the 'src' directory 33 | [tool.setuptools] 34 | package-dir = {"" = "src"} 35 | 36 | [tool.setuptools.dynamic] 37 | version = {attr = "toolbox_langchain.version.__version__"} 38 | 39 | [project.urls] 40 | Homepage = "https://github.com/googleapis/mcp-toolbox-sdk-python/blob/main/packages/toolbox-langchain" 41 | Repository = "https://github.com/googleapis/mcp-toolbox-sdk-python.git" 42 | "Bug Tracker" = "https://github.com/googleapis/mcp-toolbox-sdk-python/issues" 43 | Changelog = "https://github.com/googleapis/mcp-toolbox-sdk-python/blob/main/packages/toolbox-langchain/CHANGELOG.md" 44 | 45 | [project.optional-dependencies] 46 | test = [ 47 | "black[jupyter]==25.1.0", 48 | "isort==6.0.1", 49 | "mypy==1.16.0", 50 | "pytest-asyncio==1.0.0", 51 | "pytest==8.4.0", 52 | "pytest-cov==6.1.1", 53 | "Pillow==11.2.1", 54 | "google-cloud-secret-manager==2.23.3", 55 | "google-cloud-storage==3.1.0", 56 | ] 57 | 58 | [build-system] 59 | requires = ["setuptools"] 60 | build-backend = "setuptools.build_meta" 61 | 62 | [tool.black] 63 | target-version = ['py39'] 64 | 65 | [tool.isort] 66 | profile = "black" 67 | 68 | [tool.mypy] 69 | python_version = "3.9" 70 | warn_unused_configs = true 71 | disallow_incomplete_defs = true 72 | -------------------------------------------------------------------------------- /packages/toolbox-langchain/requirements.txt: -------------------------------------------------------------------------------- 1 | -e ../toolbox-core 2 | langchain-core==0.3.63 3 | PyYAML==6.0.2 4 | pydantic==2.11.5 5 | aiohttp==3.12.9 6 | deprecated==1.2.18 -------------------------------------------------------------------------------- /packages/toolbox-langchain/src/toolbox_langchain/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from .client import ToolboxClient 16 | from .tools import ToolboxTool 17 | 18 | __all__ = ["ToolboxClient", "ToolboxTool"] 19 | -------------------------------------------------------------------------------- /packages/toolbox-langchain/src/toolbox_langchain/async_client.py: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from typing import Any, Awaitable, Callable, Mapping, Optional, Union 16 | from warnings import warn 17 | 18 | from aiohttp import ClientSession 19 | from toolbox_core.client import ToolboxClient as ToolboxCoreClient 20 | 21 | from .async_tools import AsyncToolboxTool 22 | 23 | 24 | # This class is an internal implementation detail and is not exposed to the 25 | # end-user. It should not be used directly by external code. Changes to this 26 | # class will not be considered breaking changes to the public API. 27 | class AsyncToolboxClient: 28 | 29 | def __init__( 30 | self, 31 | url: str, 32 | session: ClientSession, 33 | client_headers: Optional[ 34 | Mapping[str, Union[Callable[[], str], Callable[[], Awaitable[str]], str]] 35 | ] = None, 36 | ): 37 | """ 38 | Initializes the AsyncToolboxClient for the Toolbox service at the given URL. 39 | 40 | Args: 41 | url: The base URL of the Toolbox service. 42 | session: An HTTP client session. 43 | """ 44 | self.__core_client = ToolboxCoreClient( 45 | url=url, session=session, client_headers=client_headers 46 | ) 47 | 48 | async def aload_tool( 49 | self, 50 | tool_name: str, 51 | auth_token_getters: dict[str, Callable[[], str]] = {}, 52 | auth_tokens: Optional[dict[str, Callable[[], str]]] = None, 53 | auth_headers: Optional[dict[str, Callable[[], str]]] = None, 54 | bound_params: dict[str, Union[Any, Callable[[], Any]]] = {}, 55 | ) -> AsyncToolboxTool: 56 | """ 57 | Loads the tool with the given tool name from the Toolbox service. 58 | 59 | Args: 60 | tool_name: The name of the tool to load. 61 | auth_token_getters: An optional mapping of authentication source 62 | names to functions that retrieve ID tokens. 63 | auth_tokens: Deprecated. Use `auth_token_getters` instead. 64 | auth_headers: Deprecated. Use `auth_token_getters` instead. 65 | bound_params: An optional mapping of parameter names to their 66 | bound values. 67 | 68 | Returns: 69 | A tool loaded from the Toolbox. 70 | """ 71 | if auth_tokens: 72 | if auth_token_getters: 73 | warn( 74 | "Both `auth_token_getters` and `auth_tokens` are provided. `auth_tokens` is deprecated, and `auth_token_getters` will be used.", 75 | DeprecationWarning, 76 | ) 77 | else: 78 | warn( 79 | "Argument `auth_tokens` is deprecated. Use `auth_token_getters` instead.", 80 | DeprecationWarning, 81 | ) 82 | auth_token_getters = auth_tokens 83 | 84 | if auth_headers: 85 | if auth_token_getters: 86 | warn( 87 | "Both `auth_token_getters` and `auth_headers` are provided. `auth_headers` is deprecated, and `auth_token_getters` will be used.", 88 | DeprecationWarning, 89 | ) 90 | else: 91 | warn( 92 | "Argument `auth_headers` is deprecated. Use `auth_token_getters` instead.", 93 | DeprecationWarning, 94 | ) 95 | auth_token_getters = auth_headers 96 | 97 | core_tool = await self.__core_client.load_tool( 98 | name=tool_name, 99 | auth_token_getters=auth_token_getters, 100 | bound_params=bound_params, 101 | ) 102 | return AsyncToolboxTool(core_tool=core_tool) 103 | 104 | async def aload_toolset( 105 | self, 106 | toolset_name: Optional[str] = None, 107 | auth_token_getters: dict[str, Callable[[], str]] = {}, 108 | auth_tokens: Optional[dict[str, Callable[[], str]]] = None, 109 | auth_headers: Optional[dict[str, Callable[[], str]]] = None, 110 | bound_params: dict[str, Union[Any, Callable[[], Any]]] = {}, 111 | strict: bool = False, 112 | ) -> list[AsyncToolboxTool]: 113 | """ 114 | Loads tools from the Toolbox service, optionally filtered by toolset 115 | name. 116 | 117 | Args: 118 | toolset_name: The name of the toolset to load. If not provided, 119 | all tools are loaded. 120 | auth_token_getters: An optional mapping of authentication source 121 | names to functions that retrieve ID tokens. 122 | auth_tokens: Deprecated. Use `auth_token_getters` instead. 123 | auth_headers: Deprecated. Use `auth_token_getters` instead. 124 | bound_params: An optional mapping of parameter names to their 125 | bound values. 126 | strict: If True, raises an error if *any* loaded tool instance fails 127 | to utilize all of the given parameters or auth tokens. (if any 128 | provided). If False (default), raises an error only if a 129 | user-provided parameter or auth token cannot be applied to *any* 130 | loaded tool across the set. 131 | 132 | Returns: 133 | A list of all tools loaded from the Toolbox. 134 | """ 135 | if auth_tokens: 136 | if auth_token_getters: 137 | warn( 138 | "Both `auth_token_getters` and `auth_tokens` are provided. `auth_tokens` is deprecated, and `auth_token_getters` will be used.", 139 | DeprecationWarning, 140 | ) 141 | else: 142 | warn( 143 | "Argument `auth_tokens` is deprecated. Use `auth_token_getters` instead.", 144 | DeprecationWarning, 145 | ) 146 | auth_token_getters = auth_tokens 147 | 148 | if auth_headers: 149 | if auth_token_getters: 150 | warn( 151 | "Both `auth_token_getters` and `auth_headers` are provided. `auth_headers` is deprecated, and `auth_token_getters` will be used.", 152 | DeprecationWarning, 153 | ) 154 | else: 155 | warn( 156 | "Argument `auth_headers` is deprecated. Use `auth_token_getters` instead.", 157 | DeprecationWarning, 158 | ) 159 | auth_token_getters = auth_headers 160 | 161 | core_tools = await self.__core_client.load_toolset( 162 | name=toolset_name, 163 | auth_token_getters=auth_token_getters, 164 | bound_params=bound_params, 165 | strict=strict, 166 | ) 167 | 168 | tools = [] 169 | for core_tool in core_tools: 170 | tools.append(AsyncToolboxTool(core_tool=core_tool)) 171 | return tools 172 | 173 | def load_tool( 174 | self, 175 | tool_name: str, 176 | auth_token_getters: dict[str, Callable[[], str]] = {}, 177 | auth_tokens: Optional[dict[str, Callable[[], str]]] = None, 178 | auth_headers: Optional[dict[str, Callable[[], str]]] = None, 179 | bound_params: dict[str, Union[Any, Callable[[], Any]]] = {}, 180 | ) -> AsyncToolboxTool: 181 | raise NotImplementedError("Synchronous methods not supported by async client.") 182 | 183 | def load_toolset( 184 | self, 185 | toolset_name: Optional[str] = None, 186 | auth_token_getters: dict[str, Callable[[], str]] = {}, 187 | auth_tokens: Optional[dict[str, Callable[[], str]]] = None, 188 | auth_headers: Optional[dict[str, Callable[[], str]]] = None, 189 | bound_params: dict[str, Union[Any, Callable[[], Any]]] = {}, 190 | strict: bool = False, 191 | ) -> list[AsyncToolboxTool]: 192 | raise NotImplementedError("Synchronous methods not supported by async client.") 193 | 194 | def add_headers( 195 | self, 196 | headers: Mapping[str, Union[Callable[[], str], Callable[[], Awaitable[str]]]], 197 | ) -> None: 198 | """ 199 | Add headers to be included in each request sent through this client. 200 | 201 | Args: 202 | headers: Headers to include in each request sent through this client. 203 | 204 | Raises: 205 | ValueError: If any of the headers are already registered in the client. 206 | """ 207 | self.__core_client.add_headers(headers) 208 | -------------------------------------------------------------------------------- /packages/toolbox-langchain/src/toolbox_langchain/async_tools.py: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from typing import Any, Callable, Union 16 | 17 | from deprecated import deprecated 18 | from langchain_core.tools import BaseTool 19 | from toolbox_core.tool import ToolboxTool as ToolboxCoreTool 20 | from toolbox_core.utils import params_to_pydantic_model 21 | 22 | 23 | # This class is an internal implementation detail and is not exposed to the 24 | # end-user. It should not be used directly by external code. Changes to this 25 | # class will not be considered breaking changes to the public API. 26 | class AsyncToolboxTool(BaseTool): 27 | """ 28 | A subclass of LangChain's BaseTool that supports features specific to 29 | Toolbox, like bound parameters and authenticated tools. 30 | """ 31 | 32 | def __init__( 33 | self, 34 | core_tool: ToolboxCoreTool, 35 | ) -> None: 36 | """ 37 | Initializes an AsyncToolboxTool instance. 38 | 39 | Args: 40 | core_tool: The underlying core async ToolboxTool instance. 41 | """ 42 | 43 | # Due to how pydantic works, we must initialize the underlying 44 | # BaseTool class before assigning values to member variables. 45 | super().__init__( 46 | name=core_tool.__name__, 47 | description=core_tool.__doc__, 48 | args_schema=params_to_pydantic_model(core_tool._name, core_tool._params), 49 | ) 50 | self.__core_tool = core_tool 51 | 52 | def _run(self, **kwargs: Any) -> str: 53 | raise NotImplementedError("Synchronous methods not supported by async tools.") 54 | 55 | async def _arun(self, **kwargs: Any) -> str: 56 | """ 57 | The coroutine that invokes the tool with the given arguments. 58 | 59 | Args: 60 | **kwargs: The arguments to the tool. 61 | 62 | Returns: 63 | A dictionary containing the parsed JSON response from the tool 64 | invocation. 65 | """ 66 | return await self.__core_tool(**kwargs) 67 | 68 | def add_auth_token_getters( 69 | self, auth_token_getters: dict[str, Callable[[], str]] 70 | ) -> "AsyncToolboxTool": 71 | """ 72 | Registers functions to retrieve ID tokens for the corresponding 73 | authentication sources. 74 | 75 | Args: 76 | auth_token_getters: A dictionary of authentication source names to 77 | the functions that return corresponding ID token getters. 78 | 79 | Returns: 80 | A new AsyncToolboxTool instance that is a deep copy of the current 81 | instance, with added auth token getters. 82 | 83 | Raises: 84 | ValueError: If any of the provided auth parameters is already 85 | registered. 86 | 87 | """ 88 | new_core_tool = self.__core_tool.add_auth_token_getters(auth_token_getters) 89 | return AsyncToolboxTool(core_tool=new_core_tool) 90 | 91 | def add_auth_token_getter( 92 | self, auth_source: str, get_id_token: Callable[[], str] 93 | ) -> "AsyncToolboxTool": 94 | """ 95 | Registers a function to retrieve an ID token for a given authentication 96 | source. 97 | 98 | Args: 99 | auth_source: The name of the authentication source. 100 | get_id_token: A function that returns the ID token. 101 | 102 | Returns: 103 | A new ToolboxTool instance that is a deep copy of the current 104 | instance, with added auth token getter. 105 | 106 | Raises: 107 | ValueError: If the provided auth parameter is already registered. 108 | 109 | """ 110 | return self.add_auth_token_getters({auth_source: get_id_token}) 111 | 112 | @deprecated("Please use `add_auth_token_getters` instead.") 113 | def add_auth_tokens( 114 | self, auth_tokens: dict[str, Callable[[], str]], strict: bool = True 115 | ) -> "AsyncToolboxTool": 116 | return self.add_auth_token_getters(auth_tokens) 117 | 118 | @deprecated("Please use `add_auth_token_getter` instead.") 119 | def add_auth_token( 120 | self, auth_source: str, get_id_token: Callable[[], str], strict: bool = True 121 | ) -> "AsyncToolboxTool": 122 | return self.add_auth_token_getter(auth_source, get_id_token) 123 | 124 | def bind_params( 125 | self, 126 | bound_params: dict[str, Union[Any, Callable[[], Any]]], 127 | ) -> "AsyncToolboxTool": 128 | """ 129 | Registers values or functions to retrieve the value for the 130 | corresponding bound parameters. 131 | 132 | Args: 133 | bound_params: A dictionary of the bound parameter name to the 134 | value or function of the bound value. 135 | 136 | Returns: 137 | A new AsyncToolboxTool instance that is a deep copy of the current 138 | instance, with added bound params. 139 | 140 | Raises: 141 | ValueError: If any of the provided bound params is already bound. 142 | """ 143 | new_core_tool = self.__core_tool.bind_params(bound_params) 144 | return AsyncToolboxTool(core_tool=new_core_tool) 145 | 146 | def bind_param( 147 | self, 148 | param_name: str, 149 | param_value: Union[Any, Callable[[], Any]], 150 | ) -> "AsyncToolboxTool": 151 | """ 152 | Registers a value or a function to retrieve the value for a given bound 153 | parameter. 154 | 155 | Args: 156 | param_name: The name of the bound parameter. 157 | param_value: The value of the bound parameter, or a callable that 158 | returns the value. 159 | 160 | Returns: 161 | A new ToolboxTool instance that is a deep copy of the current 162 | instance, with added bound param. 163 | 164 | Raises: 165 | ValueError: If the provided bound param is already bound. 166 | """ 167 | return self.bind_params({param_name: param_value}) 168 | -------------------------------------------------------------------------------- /packages/toolbox-langchain/src/toolbox_langchain/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googleapis/mcp-toolbox-sdk-python/813d60e40f036faa2bf7d1c72457ceb39c1c37d1/packages/toolbox-langchain/src/toolbox_langchain/py.typed -------------------------------------------------------------------------------- /packages/toolbox-langchain/src/toolbox_langchain/tools.py: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from asyncio import to_thread 16 | from typing import Any, Callable, Union 17 | 18 | from deprecated import deprecated 19 | from langchain_core.tools import BaseTool 20 | from toolbox_core.sync_tool import ToolboxSyncTool as ToolboxCoreSyncTool 21 | from toolbox_core.utils import params_to_pydantic_model 22 | 23 | 24 | class ToolboxTool(BaseTool): 25 | """ 26 | A subclass of LangChain's BaseTool that supports features specific to 27 | Toolbox, like bound parameters and authenticated tools. 28 | """ 29 | 30 | def __init__( 31 | self, 32 | core_tool: ToolboxCoreSyncTool, 33 | ) -> None: 34 | """ 35 | Initializes a ToolboxTool instance. 36 | 37 | Args: 38 | core_tool: The underlying core sync ToolboxTool instance. 39 | """ 40 | 41 | # Due to how pydantic works, we must initialize the underlying 42 | # BaseTool class before assigning values to member variables. 43 | super().__init__( 44 | name=core_tool.__name__, 45 | description=core_tool.__doc__, 46 | args_schema=params_to_pydantic_model(core_tool._name, core_tool._params), 47 | ) 48 | self.__core_tool = core_tool 49 | 50 | def _run(self, **kwargs: Any) -> str: 51 | return self.__core_tool(**kwargs) 52 | 53 | async def _arun(self, **kwargs: Any) -> str: 54 | return await to_thread(self.__core_tool, **kwargs) 55 | 56 | def add_auth_token_getters( 57 | self, auth_token_getters: dict[str, Callable[[], str]] 58 | ) -> "ToolboxTool": 59 | """ 60 | Registers functions to retrieve ID tokens for the corresponding 61 | authentication sources. 62 | 63 | Args: 64 | auth_token_getters: A dictionary of authentication source names to 65 | the functions that return corresponding ID token. 66 | 67 | Returns: 68 | A new ToolboxTool instance that is a deep copy of the current 69 | instance, with added auth token getters. 70 | 71 | Raises: 72 | ValueError: If any of the provided auth parameters is already 73 | registered. 74 | """ 75 | new_core_tool = self.__core_tool.add_auth_token_getters(auth_token_getters) 76 | return ToolboxTool(core_tool=new_core_tool) 77 | 78 | def add_auth_token_getter( 79 | self, auth_source: str, get_id_token: Callable[[], str] 80 | ) -> "ToolboxTool": 81 | """ 82 | Registers a function to retrieve an ID token for a given authentication 83 | source. 84 | 85 | Args: 86 | auth_source: The name of the authentication source. 87 | get_id_token: A function that returns the ID token. 88 | 89 | Returns: 90 | A new ToolboxTool instance that is a deep copy of the current 91 | instance, with added auth token getter. 92 | 93 | Raises: 94 | ValueError: If the provided auth parameter is already registered. 95 | """ 96 | return self.add_auth_token_getters({auth_source: get_id_token}) 97 | 98 | @deprecated("Please use `add_auth_token_getters` instead.") 99 | def add_auth_tokens( 100 | self, auth_tokens: dict[str, Callable[[], str]], strict: bool = True 101 | ) -> "ToolboxTool": 102 | return self.add_auth_token_getters(auth_tokens) 103 | 104 | @deprecated("Please use `add_auth_token_getter` instead.") 105 | def add_auth_token( 106 | self, auth_source: str, get_id_token: Callable[[], str], strict: bool = True 107 | ) -> "ToolboxTool": 108 | return self.add_auth_token_getter(auth_source, get_id_token) 109 | 110 | def bind_params( 111 | self, 112 | bound_params: dict[str, Union[Any, Callable[[], Any]]], 113 | ) -> "ToolboxTool": 114 | """ 115 | Registers values or functions to retrieve the value for the 116 | corresponding bound parameters. 117 | 118 | Args: 119 | bound_params: A dictionary of the bound parameter name to the 120 | value or function of the bound value. 121 | 122 | Returns: 123 | A new ToolboxTool instance that is a deep copy of the current 124 | instance, with added bound params. 125 | 126 | Raises: 127 | ValueError: If any of the provided bound params is already bound. 128 | """ 129 | new_core_tool = self.__core_tool.bind_params(bound_params) 130 | return ToolboxTool(core_tool=new_core_tool) 131 | 132 | def bind_param( 133 | self, 134 | param_name: str, 135 | param_value: Union[Any, Callable[[], Any]], 136 | ) -> "ToolboxTool": 137 | """ 138 | Registers a value or a function to retrieve the value for a given bound 139 | parameter. 140 | 141 | Args: 142 | param_name: The name of the bound parameter. 143 | param_value: The value of the bound parameter, or a callable that 144 | returns the value. 145 | 146 | Returns: 147 | A new ToolboxTool instance that is a deep copy of the current 148 | instance, with added bound param. 149 | 150 | Raises: 151 | ValueError: If the provided bound param is already bound. 152 | """ 153 | return self.bind_params({param_name: param_value}) 154 | -------------------------------------------------------------------------------- /packages/toolbox-langchain/src/toolbox_langchain/version.py: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | __version__ = "0.2.0" 16 | -------------------------------------------------------------------------------- /packages/toolbox-langchain/tests/conftest.py: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """Contains pytest fixtures that are accessible from all 16 | files present in the same directory.""" 17 | 18 | from __future__ import annotations 19 | 20 | import os 21 | import platform 22 | import subprocess 23 | import tempfile 24 | import time 25 | from typing import Generator 26 | 27 | import google 28 | import pytest_asyncio 29 | from google.auth import compute_engine 30 | from google.cloud import secretmanager, storage 31 | 32 | 33 | #### Define Utility Functions 34 | def get_env_var(key: str) -> str: 35 | """Gets environment variables.""" 36 | value = os.environ.get(key) 37 | if value is None: 38 | raise ValueError(f"Must set env var {key}") 39 | return value 40 | 41 | 42 | def access_secret_version( 43 | project_id: str, secret_id: str, version_id: str = "latest" 44 | ) -> str: 45 | """Accesses the payload of a given secret version from Secret Manager.""" 46 | client = secretmanager.SecretManagerServiceClient() 47 | name = f"projects/{project_id}/secrets/{secret_id}/versions/{version_id}" 48 | response = client.access_secret_version(request={"name": name}) 49 | return response.payload.data.decode("UTF-8") 50 | 51 | 52 | def create_tmpfile(content: str) -> str: 53 | """Creates a temporary file with the given content.""" 54 | with tempfile.NamedTemporaryFile(delete=False, mode="w") as tmpfile: 55 | tmpfile.write(content) 56 | return tmpfile.name 57 | 58 | 59 | def download_blob( 60 | bucket_name: str, source_blob_name: str, destination_file_name: str 61 | ) -> None: 62 | """Downloads a blob from a GCS bucket.""" 63 | storage_client = storage.Client() 64 | 65 | bucket = storage_client.bucket(bucket_name) 66 | blob = bucket.blob(source_blob_name) 67 | blob.download_to_filename(destination_file_name) 68 | 69 | print(f"Blob {source_blob_name} downloaded to {destination_file_name}.") 70 | 71 | 72 | def get_toolbox_binary_url(toolbox_version: str) -> str: 73 | """Constructs the GCS path to the toolbox binary.""" 74 | os_system = platform.system().lower() 75 | arch = ( 76 | "arm64" if os_system == "darwin" and platform.machine() == "arm64" else "amd64" 77 | ) 78 | return f"v{toolbox_version}/{os_system}/{arch}/toolbox" 79 | 80 | 81 | def get_auth_token(client_id: str) -> str: 82 | """Retrieves an authentication token""" 83 | request = google.auth.transport.requests.Request() 84 | credentials = compute_engine.IDTokenCredentials( 85 | request=request, 86 | target_audience=client_id, 87 | use_metadata_identity_endpoint=True, 88 | ) 89 | if not credentials.valid: 90 | credentials.refresh(request) 91 | return credentials.token 92 | 93 | 94 | #### Define Fixtures 95 | @pytest_asyncio.fixture(scope="session") 96 | def project_id() -> str: 97 | return get_env_var("GOOGLE_CLOUD_PROJECT") 98 | 99 | 100 | @pytest_asyncio.fixture(scope="session") 101 | def toolbox_version() -> str: 102 | return get_env_var("TOOLBOX_VERSION") 103 | 104 | 105 | @pytest_asyncio.fixture(scope="session") 106 | def tools_file_path(project_id: str) -> Generator[str]: 107 | """Provides a temporary file path containing the tools manifest.""" 108 | tools_manifest = access_secret_version( 109 | project_id=project_id, secret_id="sdk_testing_tools" 110 | ) 111 | tools_file_path = create_tmpfile(tools_manifest) 112 | yield tools_file_path 113 | os.remove(tools_file_path) 114 | 115 | 116 | @pytest_asyncio.fixture(scope="session") 117 | def auth_token1(project_id: str) -> str: 118 | client_id = access_secret_version( 119 | project_id=project_id, secret_id="sdk_testing_client1" 120 | ) 121 | return get_auth_token(client_id) 122 | 123 | 124 | @pytest_asyncio.fixture(scope="session") 125 | def auth_token2(project_id: str) -> str: 126 | client_id = access_secret_version( 127 | project_id=project_id, secret_id="sdk_testing_client2" 128 | ) 129 | return get_auth_token(client_id) 130 | 131 | 132 | @pytest_asyncio.fixture(scope="session") 133 | def toolbox_server(toolbox_version: str, tools_file_path: str) -> Generator[None]: 134 | """Starts the toolbox server as a subprocess.""" 135 | print("Downloading toolbox binary from gcs bucket...") 136 | source_blob_name = get_toolbox_binary_url(toolbox_version) 137 | download_blob("genai-toolbox", source_blob_name, "toolbox") 138 | print("Toolbox binary downloaded successfully.") 139 | try: 140 | print("Opening toolbox server process...") 141 | # Make toolbox executable 142 | os.chmod("toolbox", 0o700) 143 | # Run toolbox binary 144 | toolbox_server = subprocess.Popen( 145 | ["./toolbox", "--tools_file", tools_file_path] 146 | ) 147 | 148 | # Wait for server to start 149 | # Retry logic with a timeout 150 | for _ in range(5): # retries 151 | time.sleep(2) 152 | print("Checking if toolbox is successfully started...") 153 | if toolbox_server.poll() is None: 154 | print("Toolbox server started successfully.") 155 | break 156 | else: 157 | raise RuntimeError("Toolbox server failed to start after 5 retries.") 158 | except subprocess.CalledProcessError as e: 159 | print(e.stderr.decode("utf-8")) 160 | print(e.stdout.decode("utf-8")) 161 | raise RuntimeError(f"{e}\n\n{e.stderr.decode('utf-8')}") from e 162 | yield 163 | 164 | # Clean up toolbox server 165 | toolbox_server.terminate() 166 | toolbox_server.wait() 167 | -------------------------------------------------------------------------------- /packages/toolbox-llamaindex/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.2.0](https://github.com/googleapis/mcp-toolbox-sdk-python/compare/toolbox-llamaindex-v0.1.1...toolbox-llamaindex-v0.2.0) (2025-05-20) 4 | 5 | 6 | ### ⚠ BREAKING CHANGES 7 | 8 | * **toolbox-llamaindex:** Base toolbox-llamaindex over toolbox-core ([#244](https://github.com/googleapis/mcp-toolbox-sdk-python/issues/244)) 9 | 10 | ### Features 11 | 12 | * **toolbox-llamaindex:** Base toolbox-llamaindex over toolbox-core ([#244](https://github.com/googleapis/mcp-toolbox-sdk-python/issues/244)) ([64aa5a8](https://github.com/googleapis/mcp-toolbox-sdk-python/commit/64aa5a89d299ff1e0be4899d90b22df8cbcecb23)) 13 | 14 | 15 | ### Bug Fixes 16 | 17 | * **deps:** update python-nonmajor ([#180](https://github.com/googleapis/mcp-toolbox-sdk-python/issues/180)) ([8d909a9](https://github.com/googleapis/mcp-toolbox-sdk-python/commit/8d909a9e19abed4a02e30a4dfc48e06afdbb01ea)) 18 | 19 | 20 | ### Miscellaneous Chores 21 | 22 | * Auto-update core package dependency version ([#251](https://github.com/googleapis/mcp-toolbox-sdk-python/issues/251)) ([1c49d2c](https://github.com/googleapis/mcp-toolbox-sdk-python/commit/1c49d2c6e717adc8ec5f08c0d0464e343f9ce4f2)) 23 | * **deps:** update dependency pydantic to v2.11.4 ([#200](https://github.com/googleapis/mcp-toolbox-sdk-python/issues/200)) ([758f620](https://github.com/googleapis/mcp-toolbox-sdk-python/commit/758f620e25427396b52d257722d7f71312421ad1)) 24 | * **deps:** update python-nonmajor ([#207](https://github.com/googleapis/mcp-toolbox-sdk-python/issues/207)) ([83ba029](https://github.com/googleapis/mcp-toolbox-sdk-python/commit/83ba029280089d1c0d4974e5910830048586fa49)) 25 | * **deps:** update python-nonmajor ([#250](https://github.com/googleapis/mcp-toolbox-sdk-python/issues/250)) ([8fb9762](https://github.com/googleapis/mcp-toolbox-sdk-python/commit/8fb976258dda5549218f9f4e75257983866790f0)) 26 | * move to correct readme ([#198](https://github.com/googleapis/mcp-toolbox-sdk-python/issues/198)) ([99d0ad0](https://github.com/googleapis/mcp-toolbox-sdk-python/commit/99d0ad043071b89a937ee90bffb3f24ecc03a2e7)) 27 | * move toolbox-llamaindex package ([#192](https://github.com/googleapis/mcp-toolbox-sdk-python/issues/192)) ([293854f](https://github.com/googleapis/mcp-toolbox-sdk-python/commit/293854ff514c015968d205ab731dcd040a143df6)) 28 | * release 0.1.0 ([#24](https://github.com/googleapis/mcp-toolbox-sdk-python/issues/24)) ([6fff8e2](https://github.com/googleapis/mcp-toolbox-sdk-python/commit/6fff8e2ea18bd6df9f30d7790b6076cf0b32cc75)) 29 | * update toolbox version ([#194](https://github.com/googleapis/mcp-toolbox-sdk-python/issues/194)) ([1ea3179](https://github.com/googleapis/mcp-toolbox-sdk-python/commit/1ea31794bb90eed27a121fdc902ea4a09feb2ca6)) 30 | * update toolbox version ([#226](https://github.com/googleapis/mcp-toolbox-sdk-python/issues/226)) ([2a92def](https://github.com/googleapis/mcp-toolbox-sdk-python/commit/2a92def08825417b32faa523a3355eba34351955)) 31 | 32 | ## [0.1.1](https://github.com/googleapis/genai-toolbox-llamaindex-python/compare/v0.1.0...v0.1.1) (2025-04-04) 33 | 34 | 35 | ### Bug Fixes 36 | 37 | * **deps:** Update dependency black to v25 ([#46](https://github.com/googleapis/genai-toolbox-llamaindex-python/issues/46)) ([ddb60af](https://github.com/googleapis/genai-toolbox-llamaindex-python/commit/ddb60afaa78c4e57b01e87a649963df449f3ac6a)) 38 | * **deps:** Update dependency google-cloud-storage to v3 ([#47](https://github.com/googleapis/genai-toolbox-llamaindex-python/issues/47)) ([d10d779](https://github.com/googleapis/genai-toolbox-llamaindex-python/commit/d10d779ea22c02f04b26825e686ad519b4eec56f)) 39 | * **deps:** Update dependency isort to v6 ([#48](https://github.com/googleapis/genai-toolbox-llamaindex-python/issues/48)) ([e27a249](https://github.com/googleapis/genai-toolbox-llamaindex-python/commit/e27a249afb52bd0a0aff8a0ddb5b6cc8e1c535ec)) 40 | * **deps:** Update dependency pillow to v11 ([#49](https://github.com/googleapis/genai-toolbox-llamaindex-python/issues/49)) ([a467b68](https://github.com/googleapis/genai-toolbox-llamaindex-python/commit/a467b680201e796d80d0699fe7b1de711a99be74)) 41 | * **deps:** Update python-nonmajor ([#44](https://github.com/googleapis/genai-toolbox-llamaindex-python/issues/44)) ([4c1b88d](https://github.com/googleapis/genai-toolbox-llamaindex-python/commit/4c1b88d23d1c0a0b78f6b29200fa32044152c550)) 42 | * **deps:** Update python-nonmajor ([#68](https://github.com/googleapis/genai-toolbox-llamaindex-python/issues/68)) ([7595657](https://github.com/googleapis/genai-toolbox-llamaindex-python/commit/7595657b2dd5cf7974d751649120a08ba3f7853d)) 43 | 44 | ## 0.1.0 (2025-03-17) 45 | 46 | 47 | ### Features 48 | 49 | * Add support for sync operations ([#20](https://github.com/googleapis/genai-toolbox-llamaindex-python/issues/20)) ([1fa45af](https://github.com/googleapis/genai-toolbox-llamaindex-python/commit/1fa45afed49db863bf17641fb5984bf8ceb5a4c6)) 50 | * Add support for Bound Params. ([#10](https://github.com/googleapis/genai-toolbox-llamaindex-python/issues/10)) ([1d484a8](https://github.com/googleapis/genai-toolbox-llamaindex-python/commit/1d484a8daee5567d5a32d20ea492dbc125daf332)) 51 | 52 | ### Bug Fixes 53 | 54 | * Add items to parameter schema ([#9](https://github.com/googleapis/genai-toolbox-llamaindex-python/issues/9)) ([769b7f1](https://github.com/googleapis/genai-toolbox-llamaindex-python/commit/769b7f1c86dd83c9cd5e19c8bd28890da6f6a6ae)) 55 | * Rename package to 'toolbox_llamaindex' ([#8](https://github.com/googleapis/genai-toolbox-llamaindex-python/issues/8)) ([9b71c72](https://github.com/googleapis/genai-toolbox-llamaindex-python/commit/9b71c728a7887d783a027fc54367584e0ddd4489)) 56 | * Throw tool errors correctly. ([#35](https://github.com/googleapis/genai-toolbox-llamaindex-python/issues/35)) ([11159c6](https://github.com/googleapis/genai-toolbox-llamaindex-python/commit/11159c6ac9813d8da21888c70a8550518f64f3ce)) 57 | 58 | ### Documentation 59 | 60 | * Update README for new features ([#22](https://github.com/googleapis/genai-toolbox-llamaindex-python/issues/22)) ([f5060b9](https://github.com/googleapis/genai-toolbox-llamaindex-python/commit/f5060b9057329809073553c88ebd2e677db7b902)) 61 | * Update the README to recommend AgentWorkflow for using LlamaIndex. ([#34](https://github.com/googleapis/genai-toolbox-llamaindex-python/issues/34)) ([fe8e74f](https://github.com/googleapis/genai-toolbox-llamaindex-python/commit/fe8e74fb2c76af6598e6054914b03731c85a2741)) 62 | -------------------------------------------------------------------------------- /packages/toolbox-llamaindex/DEVELOPER.md: -------------------------------------------------------------------------------- 1 | # Development 2 | 3 | Below are the details to set up a development environment and run tests. 4 | 5 | ## Install 6 | 1. Clone the repository: 7 | ```bash 8 | git clone https://github.com/googleapis/mcp-toolbox-sdk-python 9 | ``` 10 | 1. Navigate to the package directory: 11 | ```bash 12 | cd mcp-toolbox-sdk-python/packages/toolbox-llamaindex 13 | ``` 14 | 1. Install the package in editable mode, so changes are reflected without 15 | reinstall: 16 | ```bash 17 | pip install -e . 18 | ``` 19 | 1. Make code changes and contribute to the SDK's development. 20 | > [!TIP] 21 | > Using `-e` option allows you to make changes to the SDK code and have 22 | > those changes reflected immediately without reinstalling the package. 23 | 24 | ## Test 25 | 1. Navigate to the package directory if needed: 26 | ```bash 27 | cd mcp-toolbox-sdk-python/packages/toolbox-llamaindex 28 | ``` 29 | 1. Install the SDK and test dependencies: 30 | ```bash 31 | pip install -e .[test] 32 | ``` 33 | 1. Run tests and/or contribute to the SDK's development. 34 | 35 | ```bash 36 | pytest 37 | ``` 38 | -------------------------------------------------------------------------------- /packages/toolbox-llamaindex/integration.cloudbuild.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | steps: 16 | - id: Install library requirements 17 | name: 'python:${_VERSION}' 18 | dir: 'packages/toolbox-llamaindex' 19 | args: 20 | - install 21 | - '-r' 22 | - 'requirements.txt' 23 | - '--user' 24 | entrypoint: pip 25 | - id: Install test requirements 26 | name: 'python:${_VERSION}' 27 | args: 28 | - install 29 | - 'packages/toolbox-llamaindex[test]' 30 | - '--user' 31 | entrypoint: pip 32 | - id: Run integration tests 33 | name: 'python:${_VERSION}' 34 | env: 35 | - TOOLBOX_URL=$_TOOLBOX_URL 36 | - TOOLBOX_VERSION=$_TOOLBOX_VERSION 37 | - GOOGLE_CLOUD_PROJECT=$PROJECT_ID 38 | args: 39 | - '-c' 40 | - >- 41 | python -m pytest packages/toolbox-llamaindex/tests/ 42 | entrypoint: /bin/bash 43 | options: 44 | logging: CLOUD_LOGGING_ONLY 45 | substitutions: 46 | _VERSION: '3.13' 47 | _TOOLBOX_VERSION: '0.5.0' 48 | -------------------------------------------------------------------------------- /packages/toolbox-llamaindex/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "toolbox-llamaindex" 3 | dynamic = ["version"] 4 | readme = "README.md" 5 | description = "Python SDK for interacting with the Toolbox service with LlamaIndex" 6 | license = {file = "LICENSE"} 7 | requires-python = ">=3.9" 8 | authors = [ 9 | {name = "Google LLC", email = "googleapis-packages@google.com"} 10 | ] 11 | dependencies = [ 12 | "toolbox-core==0.2.0", # x-release-please-version 13 | "llama-index>=0.12.0,<1.0.0", 14 | "PyYAML>=6.0.1,<7.0.0", 15 | "pydantic>=2.8.0,<3.0.0", 16 | "aiohttp>=3.8.6,<4.0.0", 17 | "deprecated>=1.2.10,<2.0.0", 18 | ] 19 | 20 | classifiers = [ 21 | "Intended Audience :: Developers", 22 | "License :: OSI Approved :: Apache Software License", 23 | "Programming Language :: Python", 24 | "Programming Language :: Python :: 3", 25 | "Programming Language :: Python :: 3.9", 26 | "Programming Language :: Python :: 3.10", 27 | "Programming Language :: Python :: 3.11", 28 | "Programming Language :: Python :: 3.12", 29 | "Programming Language :: Python :: 3.13", 30 | ] 31 | 32 | # Tells setuptools that packages are under the 'src' directory 33 | [tool.setuptools] 34 | package-dir = {"" = "src"} 35 | 36 | [tool.setuptools.dynamic] 37 | version = {attr = "toolbox_llamaindex.version.__version__"} 38 | 39 | [project.urls] 40 | Homepage = "https://github.com/googleapis/mcp-toolbox-sdk-python/blob/main/packages/toolbox-llamaindex" 41 | Repository = "https://github.com/googleapis/mcp-toolbox-sdk-python.git" 42 | "Bug Tracker" = "https://github.com/googleapis/mcp-toolbox-sdk-python/issues" 43 | Changelog = "https://github.com/googleapis/mcp-toolbox-sdk-python/blob/main/packages/toolbox-llamaindex/CHANGELOG.md" 44 | 45 | [project.optional-dependencies] 46 | test = [ 47 | "black[jupyter]==25.1.0", 48 | "isort==6.0.1", 49 | "mypy==1.16.0", 50 | "pytest-asyncio==1.0.0", 51 | "pytest==8.4.0", 52 | "pytest-cov==6.1.1", 53 | "Pillow==11.2.1", 54 | "google-cloud-secret-manager==2.23.3", 55 | "google-cloud-storage==3.1.0", 56 | ] 57 | 58 | [build-system] 59 | requires = ["setuptools"] 60 | build-backend = "setuptools.build_meta" 61 | 62 | [tool.black] 63 | target-version = ['py39'] 64 | 65 | [tool.isort] 66 | profile = "black" 67 | 68 | [tool.mypy] 69 | python_version = "3.9" 70 | warn_unused_configs = true 71 | disallow_incomplete_defs = true 72 | -------------------------------------------------------------------------------- /packages/toolbox-llamaindex/requirements.txt: -------------------------------------------------------------------------------- 1 | -e ../toolbox-core 2 | llama-index==0.12.40 3 | PyYAML==6.0.2 4 | pydantic==2.11.5 5 | aiohttp==3.12.9 6 | deprecated==1.2.18 -------------------------------------------------------------------------------- /packages/toolbox-llamaindex/src/toolbox_llamaindex/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from .client import ToolboxClient 16 | from .tools import ToolboxTool 17 | 18 | __all__ = ["ToolboxClient", "ToolboxTool"] 19 | -------------------------------------------------------------------------------- /packages/toolbox-llamaindex/src/toolbox_llamaindex/async_client.py: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from typing import Any, Awaitable, Callable, Mapping, Optional, Union 16 | from warnings import warn 17 | 18 | from aiohttp import ClientSession 19 | from toolbox_core.client import ToolboxClient as ToolboxCoreClient 20 | 21 | from .async_tools import AsyncToolboxTool 22 | 23 | 24 | # This class is an internal implementation detail and is not exposed to the 25 | # end-user. It should not be used directly by external code. Changes to this 26 | # class will not be considered breaking changes to the public API. 27 | class AsyncToolboxClient: 28 | 29 | def __init__( 30 | self, 31 | url: str, 32 | session: ClientSession, 33 | client_headers: Optional[ 34 | Mapping[str, Union[Callable[[], str], Callable[[], Awaitable[str]], str]] 35 | ] = None, 36 | ): 37 | """ 38 | Initializes the AsyncToolboxClient for the Toolbox service at the given URL. 39 | 40 | Args: 41 | url: The base URL of the Toolbox service. 42 | session: An HTTP client session. 43 | """ 44 | self.__core_client = ToolboxCoreClient( 45 | url=url, session=session, client_headers=client_headers 46 | ) 47 | 48 | async def aload_tool( 49 | self, 50 | tool_name: str, 51 | auth_token_getters: dict[str, Callable[[], str]] = {}, 52 | auth_tokens: Optional[dict[str, Callable[[], str]]] = None, 53 | auth_headers: Optional[dict[str, Callable[[], str]]] = None, 54 | bound_params: dict[str, Union[Any, Callable[[], Any]]] = {}, 55 | ) -> AsyncToolboxTool: 56 | """ 57 | Loads the tool with the given tool name from the Toolbox service. 58 | 59 | Args: 60 | tool_name: The name of the tool to load. 61 | auth_token_getters: An optional mapping of authentication source 62 | names to functions that retrieve ID tokens. 63 | auth_tokens: Deprecated. Use `auth_token_getters` instead. 64 | auth_headers: Deprecated. Use `auth_token_getters` instead. 65 | bound_params: An optional mapping of parameter names to their 66 | bound values. 67 | 68 | Returns: 69 | A tool loaded from the Toolbox. 70 | """ 71 | if auth_tokens: 72 | if auth_token_getters: 73 | warn( 74 | "Both `auth_token_getters` and `auth_tokens` are provided. `auth_tokens` is deprecated, and `auth_token_getters` will be used.", 75 | DeprecationWarning, 76 | ) 77 | else: 78 | warn( 79 | "Argument `auth_tokens` is deprecated. Use `auth_token_getters` instead.", 80 | DeprecationWarning, 81 | ) 82 | auth_token_getters = auth_tokens 83 | 84 | if auth_headers: 85 | if auth_token_getters: 86 | warn( 87 | "Both `auth_token_getters` and `auth_headers` are provided. `auth_headers` is deprecated, and `auth_token_getters` will be used.", 88 | DeprecationWarning, 89 | ) 90 | else: 91 | warn( 92 | "Argument `auth_headers` is deprecated. Use `auth_token_getters` instead.", 93 | DeprecationWarning, 94 | ) 95 | auth_token_getters = auth_headers 96 | 97 | core_tool = await self.__core_client.load_tool( 98 | name=tool_name, 99 | auth_token_getters=auth_token_getters, 100 | bound_params=bound_params, 101 | ) 102 | return AsyncToolboxTool(core_tool=core_tool) 103 | 104 | async def aload_toolset( 105 | self, 106 | toolset_name: Optional[str] = None, 107 | auth_token_getters: dict[str, Callable[[], str]] = {}, 108 | auth_tokens: Optional[dict[str, Callable[[], str]]] = None, 109 | auth_headers: Optional[dict[str, Callable[[], str]]] = None, 110 | bound_params: dict[str, Union[Any, Callable[[], Any]]] = {}, 111 | strict: bool = False, 112 | ) -> list[AsyncToolboxTool]: 113 | """ 114 | Loads tools from the Toolbox service, optionally filtered by toolset 115 | name. 116 | 117 | Args: 118 | toolset_name: The name of the toolset to load. If not provided, 119 | all tools are loaded. 120 | auth_token_getters: An optional mapping of authentication source 121 | names to functions that retrieve ID tokens. 122 | auth_tokens: Deprecated. Use `auth_token_getters` instead. 123 | auth_headers: Deprecated. Use `auth_token_getters` instead. 124 | bound_params: An optional mapping of parameter names to their 125 | bound values. 126 | strict: If True, raises an error if *any* loaded tool instance fails 127 | to utilize at least one provided parameter or auth token (if any 128 | provided). If False (default), raises an error only if a 129 | user-provided parameter or auth token cannot be applied to *any* 130 | loaded tool across the set. 131 | 132 | Returns: 133 | A list of all tools loaded from the Toolbox. 134 | """ 135 | if auth_tokens: 136 | if auth_token_getters: 137 | warn( 138 | "Both `auth_token_getters` and `auth_tokens` are provided. `auth_tokens` is deprecated, and `auth_token_getters` will be used.", 139 | DeprecationWarning, 140 | ) 141 | else: 142 | warn( 143 | "Argument `auth_tokens` is deprecated. Use `auth_token_getters` instead.", 144 | DeprecationWarning, 145 | ) 146 | auth_token_getters = auth_tokens 147 | 148 | if auth_headers: 149 | if auth_token_getters: 150 | warn( 151 | "Both `auth_token_getters` and `auth_headers` are provided. `auth_headers` is deprecated, and `auth_token_getters` will be used.", 152 | DeprecationWarning, 153 | ) 154 | else: 155 | warn( 156 | "Argument `auth_headers` is deprecated. Use `auth_token_getters` instead.", 157 | DeprecationWarning, 158 | ) 159 | auth_token_getters = auth_headers 160 | 161 | core_tools = await self.__core_client.load_toolset( 162 | name=toolset_name, 163 | auth_token_getters=auth_token_getters, 164 | bound_params=bound_params, 165 | strict=strict, 166 | ) 167 | 168 | tools = [] 169 | for core_tool in core_tools: 170 | tools.append(AsyncToolboxTool(core_tool=core_tool)) 171 | return tools 172 | 173 | def load_tool( 174 | self, 175 | tool_name: str, 176 | auth_token_getters: dict[str, Callable[[], str]] = {}, 177 | auth_tokens: Optional[dict[str, Callable[[], str]]] = None, 178 | auth_headers: Optional[dict[str, Callable[[], str]]] = None, 179 | bound_params: dict[str, Union[Any, Callable[[], Any]]] = {}, 180 | ) -> AsyncToolboxTool: 181 | raise NotImplementedError("Synchronous methods not supported by async client.") 182 | 183 | def load_toolset( 184 | self, 185 | toolset_name: Optional[str] = None, 186 | auth_token_getters: dict[str, Callable[[], str]] = {}, 187 | auth_tokens: Optional[dict[str, Callable[[], str]]] = None, 188 | auth_headers: Optional[dict[str, Callable[[], str]]] = None, 189 | bound_params: dict[str, Union[Any, Callable[[], Any]]] = {}, 190 | strict: bool = False, 191 | ) -> list[AsyncToolboxTool]: 192 | raise NotImplementedError("Synchronous methods not supported by async client.") 193 | 194 | def add_headers( 195 | self, 196 | headers: Mapping[str, Union[Callable[[], str], Callable[[], Awaitable[str]]]], 197 | ) -> None: 198 | """ 199 | Add headers to be included in each request sent through this client. 200 | Args: 201 | headers: Headers to include in each request sent through this client. 202 | Raises: 203 | ValueError: If any of the headers are already registered in the client. 204 | """ 205 | self.__core_client.add_headers(headers) 206 | -------------------------------------------------------------------------------- /packages/toolbox-llamaindex/src/toolbox_llamaindex/async_tools.py: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from typing import Any, Callable, Union 16 | 17 | from deprecated import deprecated 18 | from llama_index.core.tools import ToolMetadata 19 | from llama_index.core.tools.types import AsyncBaseTool, ToolOutput 20 | from toolbox_core.tool import ToolboxTool as ToolboxCoreTool 21 | from toolbox_core.utils import params_to_pydantic_model 22 | 23 | 24 | # This class is an internal implementation detail and is not exposed to the 25 | # end-user. It should not be used directly by external code. Changes to this 26 | # class will not be considered breaking changes to the public API. 27 | class AsyncToolboxTool(AsyncBaseTool): 28 | """ 29 | A subclass of LlamaIndex's AsyncBaseTool that supports features specific to 30 | Toolbox, like bound parameters and authenticated tools. 31 | """ 32 | 33 | def __init__( 34 | self, 35 | core_tool: ToolboxCoreTool, 36 | ) -> None: 37 | """ 38 | Initializes an AsyncToolboxTool instance. 39 | 40 | Args: 41 | core_tool: The underlying core async ToolboxTool instance. 42 | """ 43 | 44 | # Due to how pydantic works, we must initialize the underlying 45 | # AsyncBaseTool class before assigning values to member variables. 46 | super().__init__() 47 | self.__core_tool = core_tool 48 | 49 | @property 50 | def metadata(self) -> ToolMetadata: 51 | if self.__core_tool.__doc__ is None: 52 | raise ValueError("No description found for the tool.") 53 | 54 | return ToolMetadata( 55 | name=self.__core_tool.__name__, 56 | description=self.__core_tool.__doc__, 57 | fn_schema=params_to_pydantic_model( 58 | self.__core_tool._name, self.__core_tool._params 59 | ), 60 | ) 61 | 62 | def call(self, *args: Any, **kwargs: Any) -> ToolOutput: # type: ignore 63 | raise NotImplementedError("Synchronous methods not supported by async tools.") 64 | 65 | async def acall(self, **kwargs: Any) -> ToolOutput: # type: ignore 66 | """ 67 | The coroutine that invokes the tool with the given arguments. 68 | 69 | Args: 70 | **kwargs: The arguments to the tool. 71 | 72 | Returns: 73 | A dictionary containing the parsed JSON response from the tool 74 | invocation. 75 | """ 76 | output_content = await self.__core_tool(**kwargs) 77 | return ToolOutput( 78 | content=output_content, 79 | tool_name=self.__core_tool.__name__, 80 | raw_input=kwargs, 81 | raw_output=output_content, 82 | ) 83 | 84 | def add_auth_token_getters( 85 | self, auth_token_getters: dict[str, Callable[[], str]] 86 | ) -> "AsyncToolboxTool": 87 | """ 88 | Registers functions to retrieve ID tokens for the corresponding 89 | authentication sources. 90 | 91 | Args: 92 | auth_token_getters: A dictionary of authentication source names to 93 | the functions that return corresponding ID token getters. 94 | 95 | Returns: 96 | A new AsyncToolboxTool instance that is a deep copy of the current 97 | instance, with added auth token getters. 98 | 99 | Raises: 100 | ValueError: If any of the provided auth parameters is already 101 | registered. 102 | 103 | """ 104 | new_core_tool = self.__core_tool.add_auth_token_getters(auth_token_getters) 105 | return AsyncToolboxTool(core_tool=new_core_tool) 106 | 107 | def add_auth_token_getter( 108 | self, auth_source: str, get_id_token: Callable[[], str] 109 | ) -> "AsyncToolboxTool": 110 | """ 111 | Registers a function to retrieve an ID token for a given authentication 112 | source. 113 | 114 | Args: 115 | auth_source: The name of the authentication source. 116 | get_id_token: A function that returns the ID token. 117 | 118 | Returns: 119 | A new ToolboxTool instance that is a deep copy of the current 120 | instance, with added auth token getter. 121 | 122 | Raises: 123 | ValueError: If the provided auth parameter is already registered. 124 | 125 | """ 126 | return self.add_auth_token_getters({auth_source: get_id_token}) 127 | 128 | @deprecated("Please use `add_auth_token_getters` instead.") 129 | def add_auth_tokens( 130 | self, auth_tokens: dict[str, Callable[[], str]], strict: bool = True 131 | ) -> "AsyncToolboxTool": 132 | return self.add_auth_token_getters(auth_tokens) 133 | 134 | @deprecated("Please use `add_auth_token_getter` instead.") 135 | def add_auth_token( 136 | self, auth_source: str, get_id_token: Callable[[], str], strict: bool = True 137 | ) -> "AsyncToolboxTool": 138 | return self.add_auth_token_getter(auth_source, get_id_token) 139 | 140 | def bind_params( 141 | self, 142 | bound_params: dict[str, Union[Any, Callable[[], Any]]], 143 | ) -> "AsyncToolboxTool": 144 | """ 145 | Registers values or functions to retrieve the value for the 146 | corresponding bound parameters. 147 | 148 | Args: 149 | bound_params: A dictionary of the bound parameter name to the 150 | value or function of the bound value. 151 | 152 | Returns: 153 | A new AsyncToolboxTool instance that is a deep copy of the current 154 | instance, with added bound params. 155 | 156 | Raises: 157 | ValueError: If any of the provided bound params is already bound. 158 | """ 159 | new_core_tool = self.__core_tool.bind_params(bound_params) 160 | return AsyncToolboxTool(core_tool=new_core_tool) 161 | 162 | def bind_param( 163 | self, 164 | param_name: str, 165 | param_value: Union[Any, Callable[[], Any]], 166 | ) -> "AsyncToolboxTool": 167 | """ 168 | Registers a value or a function to retrieve the value for a given bound 169 | parameter. 170 | 171 | Args: 172 | param_name: The name of the bound parameter. 173 | param_value: The value of the bound parameter, or a callable that 174 | returns the value. 175 | 176 | Returns: 177 | A new ToolboxTool instance that is a deep copy of the current 178 | instance, with added bound param. 179 | 180 | Raises: 181 | ValueError: If the provided bound param is already bound. 182 | """ 183 | return self.bind_params({param_name: param_value}) 184 | -------------------------------------------------------------------------------- /packages/toolbox-llamaindex/src/toolbox_llamaindex/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googleapis/mcp-toolbox-sdk-python/813d60e40f036faa2bf7d1c72457ceb39c1c37d1/packages/toolbox-llamaindex/src/toolbox_llamaindex/py.typed -------------------------------------------------------------------------------- /packages/toolbox-llamaindex/src/toolbox_llamaindex/tools.py: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from asyncio import to_thread 16 | from typing import Any, Callable, Union 17 | 18 | from deprecated import deprecated 19 | from llama_index.core.tools import ToolMetadata 20 | from llama_index.core.tools.types import AsyncBaseTool, ToolOutput 21 | from toolbox_core.sync_tool import ToolboxSyncTool as ToolboxCoreSyncTool 22 | from toolbox_core.utils import params_to_pydantic_model 23 | 24 | 25 | class ToolboxTool(AsyncBaseTool): 26 | """ 27 | A subclass of LlamaIndex's AsyncBaseTool that supports features specific to 28 | Toolbox, like bound parameters and authenticated tools. 29 | """ 30 | 31 | def __init__( 32 | self, 33 | core_tool: ToolboxCoreSyncTool, 34 | ) -> None: 35 | """ 36 | Initializes a ToolboxTool instance. 37 | 38 | Args: 39 | core_tool: The underlying core sync ToolboxTool instance. 40 | """ 41 | # Due to how pydantic works, we must initialize the underlying 42 | # AsyncBaseTool class before assigning values to member variables. 43 | super().__init__() 44 | 45 | self.__core_tool = core_tool 46 | 47 | @property 48 | def metadata(self) -> ToolMetadata: 49 | if self.__core_tool.__doc__ is None: 50 | raise ValueError("No description found for the tool.") 51 | 52 | return ToolMetadata( 53 | name=self.__core_tool.__name__, 54 | description=self.__core_tool.__doc__, 55 | fn_schema=params_to_pydantic_model( 56 | self.__core_tool._name, self.__core_tool._params 57 | ), 58 | ) 59 | 60 | def call(self, **kwargs: Any) -> ToolOutput: # type: ignore 61 | output_content = self.__core_tool(**kwargs) 62 | return ToolOutput( 63 | content=output_content, 64 | tool_name=self.__core_tool.__name__, 65 | raw_input=kwargs, 66 | raw_output=output_content, 67 | ) 68 | 69 | async def acall(self, **kwargs: Any) -> ToolOutput: # type: ignore 70 | output_content = await to_thread(self.__core_tool, **kwargs) 71 | return ToolOutput( 72 | content=output_content, 73 | tool_name=self.__core_tool.__name__, 74 | raw_input=kwargs, 75 | raw_output=output_content, 76 | ) 77 | 78 | def add_auth_token_getters( 79 | self, auth_token_getters: dict[str, Callable[[], str]] 80 | ) -> "ToolboxTool": 81 | """ 82 | Registers functions to retrieve ID tokens for the corresponding 83 | authentication sources. 84 | 85 | Args: 86 | auth_token_getters: A dictionary of authentication source names to 87 | the functions that return corresponding ID token. 88 | 89 | Returns: 90 | A new ToolboxTool instance that is a deep copy of the current 91 | instance, with added auth token getters. 92 | 93 | Raises: 94 | ValueError: If any of the provided auth parameters is already 95 | registered. 96 | """ 97 | new_core_tool = self.__core_tool.add_auth_token_getters(auth_token_getters) 98 | return ToolboxTool(core_tool=new_core_tool) 99 | 100 | def add_auth_token_getter( 101 | self, auth_source: str, get_id_token: Callable[[], str] 102 | ) -> "ToolboxTool": 103 | """ 104 | Registers a function to retrieve an ID token for a given authentication 105 | source. 106 | 107 | Args: 108 | auth_source: The name of the authentication source. 109 | get_id_token: A function that returns the ID token. 110 | 111 | Returns: 112 | A new ToolboxTool instance that is a deep copy of the current 113 | instance, with added auth token getter. 114 | 115 | Raises: 116 | ValueError: If the provided auth parameter is already registered. 117 | """ 118 | return self.add_auth_token_getters({auth_source: get_id_token}) 119 | 120 | @deprecated("Please use `add_auth_token_getters` instead.") 121 | def add_auth_tokens( 122 | self, auth_tokens: dict[str, Callable[[], str]], strict: bool = True 123 | ) -> "ToolboxTool": 124 | return self.add_auth_token_getters(auth_tokens) 125 | 126 | @deprecated("Please use `add_auth_token_getter` instead.") 127 | def add_auth_token( 128 | self, auth_source: str, get_id_token: Callable[[], str], strict: bool = True 129 | ) -> "ToolboxTool": 130 | return self.add_auth_token_getter(auth_source, get_id_token) 131 | 132 | def bind_params( 133 | self, 134 | bound_params: dict[str, Union[Any, Callable[[], Any]]], 135 | ) -> "ToolboxTool": 136 | """ 137 | Registers values or functions to retrieve the value for the 138 | corresponding bound parameters. 139 | 140 | Args: 141 | bound_params: A dictionary of the bound parameter name to the 142 | value or function of the bound value. 143 | 144 | Returns: 145 | A new ToolboxTool instance that is a deep copy of the current 146 | instance, with added bound params. 147 | 148 | Raises: 149 | ValueError: If any of the provided bound params is already bound. 150 | """ 151 | new_core_tool = self.__core_tool.bind_params(bound_params) 152 | return ToolboxTool(core_tool=new_core_tool) 153 | 154 | def bind_param( 155 | self, 156 | param_name: str, 157 | param_value: Union[Any, Callable[[], Any]], 158 | ) -> "ToolboxTool": 159 | """ 160 | Registers a value or a function to retrieve the value for a given bound 161 | parameter. 162 | 163 | Args: 164 | param_name: The name of the bound parameter. 165 | param_value: The value of the bound parameter, or a callable that 166 | returns the value. 167 | 168 | Returns: 169 | A new ToolboxTool instance that is a deep copy of the current 170 | instance, with added bound param. 171 | 172 | Raises: 173 | ValueError: If the provided bound param is already bound. 174 | """ 175 | return self.bind_params({param_name: param_value}) 176 | -------------------------------------------------------------------------------- /packages/toolbox-llamaindex/src/toolbox_llamaindex/version.py: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | __version__ = "0.2.0" 16 | -------------------------------------------------------------------------------- /packages/toolbox-llamaindex/tests/conftest.py: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """Contains pytest fixtures that are accessible from all 16 | files present in the same directory.""" 17 | 18 | from __future__ import annotations 19 | 20 | import os 21 | import platform 22 | import subprocess 23 | import tempfile 24 | import time 25 | from typing import Generator 26 | 27 | import google 28 | import pytest_asyncio 29 | from google.auth import compute_engine 30 | from google.cloud import secretmanager, storage 31 | 32 | 33 | #### Define Utility Functions 34 | def get_env_var(key: str) -> str: 35 | """Gets environment variables.""" 36 | value = os.environ.get(key) 37 | if value is None: 38 | raise ValueError(f"Must set env var {key}") 39 | return value 40 | 41 | 42 | def access_secret_version( 43 | project_id: str, secret_id: str, version_id: str = "latest" 44 | ) -> str: 45 | """Accesses the payload of a given secret version from Secret Manager.""" 46 | client = secretmanager.SecretManagerServiceClient() 47 | name = f"projects/{project_id}/secrets/{secret_id}/versions/{version_id}" 48 | response = client.access_secret_version(request={"name": name}) 49 | return response.payload.data.decode("UTF-8") 50 | 51 | 52 | def create_tmpfile(content: str) -> str: 53 | """Creates a temporary file with the given content.""" 54 | with tempfile.NamedTemporaryFile(delete=False, mode="w") as tmpfile: 55 | tmpfile.write(content) 56 | return tmpfile.name 57 | 58 | 59 | def download_blob( 60 | bucket_name: str, source_blob_name: str, destination_file_name: str 61 | ) -> None: 62 | """Downloads a blob from a GCS bucket.""" 63 | storage_client = storage.Client() 64 | 65 | bucket = storage_client.bucket(bucket_name) 66 | blob = bucket.blob(source_blob_name) 67 | blob.download_to_filename(destination_file_name) 68 | 69 | print(f"Blob {source_blob_name} downloaded to {destination_file_name}.") 70 | 71 | 72 | def get_toolbox_binary_url(toolbox_version: str) -> str: 73 | """Constructs the GCS path to the toolbox binary.""" 74 | os_system = platform.system().lower() 75 | arch = ( 76 | "arm64" if os_system == "darwin" and platform.machine() == "arm64" else "amd64" 77 | ) 78 | return f"v{toolbox_version}/{os_system}/{arch}/toolbox" 79 | 80 | 81 | def get_auth_token(client_id: str) -> str: 82 | """Retrieves an authentication token""" 83 | request = google.auth.transport.requests.Request() 84 | credentials = compute_engine.IDTokenCredentials( 85 | request=request, 86 | target_audience=client_id, 87 | use_metadata_identity_endpoint=True, 88 | ) 89 | if not credentials.valid: 90 | credentials.refresh(request) 91 | return credentials.token 92 | 93 | 94 | #### Define Fixtures 95 | @pytest_asyncio.fixture(scope="session") 96 | def project_id() -> str: 97 | return get_env_var("GOOGLE_CLOUD_PROJECT") 98 | 99 | 100 | @pytest_asyncio.fixture(scope="session") 101 | def toolbox_version() -> str: 102 | return get_env_var("TOOLBOX_VERSION") 103 | 104 | 105 | @pytest_asyncio.fixture(scope="session") 106 | def tools_file_path(project_id: str) -> Generator[str]: 107 | """Provides a temporary file path containing the tools manifest.""" 108 | tools_manifest = access_secret_version( 109 | project_id=project_id, secret_id="sdk_testing_tools" 110 | ) 111 | tools_file_path = create_tmpfile(tools_manifest) 112 | yield tools_file_path 113 | os.remove(tools_file_path) 114 | 115 | 116 | @pytest_asyncio.fixture(scope="session") 117 | def auth_token1(project_id: str) -> str: 118 | client_id = access_secret_version( 119 | project_id=project_id, secret_id="sdk_testing_client1" 120 | ) 121 | return get_auth_token(client_id) 122 | 123 | 124 | @pytest_asyncio.fixture(scope="session") 125 | def auth_token2(project_id: str) -> str: 126 | client_id = access_secret_version( 127 | project_id=project_id, secret_id="sdk_testing_client2" 128 | ) 129 | return get_auth_token(client_id) 130 | 131 | 132 | @pytest_asyncio.fixture(scope="session") 133 | def toolbox_server(toolbox_version: str, tools_file_path: str) -> Generator[None]: 134 | """Starts the toolbox server as a subprocess.""" 135 | print("Downloading toolbox binary from gcs bucket...") 136 | source_blob_name = get_toolbox_binary_url(toolbox_version) 137 | download_blob("genai-toolbox", source_blob_name, "toolbox") 138 | print("Toolbox binary downloaded successfully.") 139 | try: 140 | print("Opening toolbox server process...") 141 | # Make toolbox executable 142 | os.chmod("toolbox", 0o700) 143 | # Run toolbox binary 144 | toolbox_server = subprocess.Popen( 145 | ["./toolbox", "--tools_file", tools_file_path] 146 | ) 147 | 148 | # Wait for server to start 149 | # Retry logic with a timeout 150 | for _ in range(5): # retries 151 | time.sleep(4) 152 | print("Checking if toolbox is successfully started...") 153 | if toolbox_server.poll() is None: 154 | print("Toolbox server started successfully.") 155 | break 156 | else: 157 | raise RuntimeError("Toolbox server failed to start after 5 retries.") 158 | except subprocess.CalledProcessError as e: 159 | print(e.stderr.decode("utf-8")) 160 | print(e.stdout.decode("utf-8")) 161 | raise RuntimeError(f"{e}\n\n{e.stderr.decode('utf-8')}") from e 162 | yield 163 | 164 | # Clean up toolbox server 165 | toolbox_server.terminate() 166 | toolbox_server.wait() 167 | -------------------------------------------------------------------------------- /release-please-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "release-type": "python", 3 | "bump-minor-pre-major": true, 4 | "bump-patch-for-minor-pre-major": true, 5 | "include-v-in-tag": true, 6 | "changelog-sections": [ 7 | { "type": "feat", "section": "Features" }, 8 | { "type": "fix", "section": "Bug Fixes" }, 9 | { "type": "chore", "section": "Miscellaneous Chores", "hidden": false }, 10 | { "type": "docs", "section": "Documentation", "hidden": false } 11 | ], 12 | "packages": { 13 | "packages/toolbox-core": { 14 | "component": "toolbox-core", 15 | "extra-files": [ 16 | "src/toolbox_core/version.py" 17 | ] 18 | }, 19 | "packages/toolbox-langchain": { 20 | "component": "toolbox-langchain", 21 | "extra-files": [ 22 | "pyproject.toml", 23 | "src/toolbox_langchain/version.py" 24 | ] 25 | }, 26 | "packages/toolbox-llamaindex": { 27 | "component": "toolbox-llamaindex", 28 | "extra-files": [ 29 | "pyproject.toml", 30 | "src/toolbox_llamaindex/version.py" 31 | ] 32 | } 33 | }, 34 | "plugins": [ 35 | { 36 | "type": "linked-versions", 37 | "groupName": "toolbox-python-sdks", 38 | "components": [ 39 | "toolbox-core", "toolbox-langchain", "toolbox-llamaindex" 40 | ] 41 | } 42 | ] 43 | } 44 | --------------------------------------------------------------------------------