Compare commits

...

30 Commits

Author SHA1 Message Date
release-please[bot]
5156db2621 chore(main): release 0.20.0 (#1921)
🤖 I have created a release *beep* *boop*
---


##
[0.20.0](https://github.com/googleapis/genai-toolbox/compare/v0.19.1...v0.20.0)
(2025-11-14)


### Features

* Added prompt support for toolbox
([#1798](https://github.com/googleapis/genai-toolbox/issues/1798))
([cd56ea4](cd56ea44fb))
* **source/alloydb,
source/cloud-sql-postgres,source/cloud-sql-mysql,source/cloud-sql-mssql:**
Use project from env for alloydb and cloud sql control plane tools
([#1588](https://github.com/googleapis/genai-toolbox/issues/1588))
([12bdd95](12bdd95459))
* **source/mysql:** Set default host and port for MySQL source
([#1922](https://github.com/googleapis/genai-toolbox/issues/1922))
([2c228ef](2c228ef4f2))
* **source/Postgresql:** Set default host and port for Postgresql source
([#1927](https://github.com/googleapis/genai-toolbox/issues/1927))
([7e6e88a](7e6e88a21f))
* **tool/looker-generate-embed-url:** Adding generate embed url tool
([#1877](https://github.com/googleapis/genai-toolbox/issues/1877))
([ef63860](ef63860559))
* **tools/postgres:** Add `list_triggers`, `database_overview` tools for
postgres
([#1912](https://github.com/googleapis/genai-toolbox/issues/1912))
([a4c9287](a4c9287aec))
* **tools/postgres:** Add list_indexes, list_sequences tools for
postgres
([#1765](https://github.com/googleapis/genai-toolbox/issues/1765))
([897c63d](897c63dcea))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

---------

Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com>
Co-authored-by: Yuan Teoh <45984206+Yuan325@users.noreply.github.com>
2025-11-13 17:37:01 -08:00
Yuan Teoh
7c67bcc810 chore: update hugo version (#1950)
update hugo version
2025-11-14 01:15:51 +00:00
Yuan Teoh
f6804420b9 chore: embed ToolsetConfig into Toolset (#1885)
To keep a persistent backend storage for configuration, we will have to
keep a single source of truth. This involves supporting bi-directional
conversion between ToolsetConfig and Toolset.


This PR make the following changes:
* Embed ToolsetConfig in Toolset
* Add `ToConfig()` to extract ToolsetConfig from Toolset.
2025-11-13 16:53:35 -08:00
Yuan Teoh
927881ffb9 chore: embed Config into Auth (#1865)
To keep a persistent backend storage for configuration, we will have to
keep a single source of truth. This involves supporting bi-directional
conversion between Config and AuthService.


This PR make the following changes:
* Embed Config in AuthService
* Add `ToConfig()` to extract Config from AuthService.
2025-11-13 16:37:23 -08:00
google-labs-jules[bot]
42c8dd7ddd chore: embed Config into Tool (#1875)
To keep a persistent backend storage for configuration, we will have to
keep a single source of truth. This involves supporting bi-directional
conversion between Config and Tool.


This PR make the following changes:
* Embed Config in Tool
* Add `ToConfig()` to extract Config from Tool.


Jules PR

---
*PR created automatically by Jules for task
[11947649751737965380](https://jules.google.com/task/11947649751737965380)*

---------

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
Co-authored-by: Yuan Teoh <yuanteoh@google.com>
2025-11-14 00:21:15 +00:00
Yuan Teoh
ae0c29254a chore: embed Config into Source (#1864)
To keep a persistent backend storage for configuration, we will have to
keep a single source of truth. This involves supporting bi-directional
conversion between Config and Source.


This PR make the following changes:
* Embed Config in Source
* Add `ToConfig()` to extract Config from Source.
2025-11-13 14:11:25 -08:00
Ajaykumar Yadav
46b072c3f4 docs(alloydb): fix redirection link of the cgc doc for alloydb (#1936)
## Description
In the docs `Connect from you IDE > AlloyDB Admin API with MCP` 

The previous link was `pointing to wrong documentation`, corrected the
link now it will be pointing to the correct documentation for the `how
to connect you ide with alloydb-pg-admin`
- [x] removed the wrong link:
https://cloud.google.com/alloydb/docs/quickstart/create-and-connect
- [x] added the correct link:
https://cloud.google.com/alloydb/docs/connect-ide-using-mcp-toolbox

## PR Checklist
- [x] Make sure you reviewed

[CONTRIBUTING.md](https://github.com/googleapis/genai-toolbox/blob/main/CONTRIBUTING.md)
- [ ] Make sure to open an issue as a

[bug/issue](https://github.com/googleapis/genai-toolbox/issues/new/choose)
  before writing your code! That way we can discuss the change, evaluate
  designs, and agree on the general idea
- [x] Ensure the tests and linter pass
- [x] Code coverage does not decrease (if any source code was changed)
- [x] Appropriate docs were updated (if necessary)
- [ ] Make sure to add `!` if this involves a breaking change
2025-11-13 21:54:05 +00:00
Yuan Teoh
4aabb4aaca chore: move parameters to internal/util (#1907)
To facilitate the transition of moving invocation implementation to
Source, we will have to move parameter to `internal/util`. This approach
is crucial because certain parameters may not be fully resolvable
pre-implementation. Since both `internal/sources` and `internal/tools`
will need access to `parameters`, it will be more relevant to move
parameters implementation to utils.
2025-11-13 21:37:12 +00:00
Srividya Reddy
897c63dcea feat(tools): add list_indexes, list_sequences tools for postgres (#1765)
## Description

Adds the following tools for Postgres:
(1) list_indexes:  Lists available user indexes in a given database. 
(2) list_sequences: Lists all the sequences in the database ordered by
sequence name.

list_indexes:
<img width="1708" height="816" alt="Screenshot 2025-10-21 at 10 24
30 PM"
src="https://github.com/user-attachments/assets/d1888f53-6013-4beb-b0dc-b94f3d66135e"
/>

<img width="1914" height="1017" alt="Screenshot 2025-10-22 at 8 23
16 AM"
src="https://github.com/user-attachments/assets/7c185ef4-4550-4bb2-8051-0eca8f40dc7b"
/>

list_sequences:

<img width="2539" height="902" alt="Screenshot 2025-10-22 at 8 04 25 AM"
src="https://github.com/user-attachments/assets/f22cfaad-412b-4df2-99f3-ee813f538ff7"
/>

<img width="1082" height="610" alt="Screenshot 2025-10-22 at 8 06 07 AM"
src="https://github.com/user-attachments/assets/102ed3ca-33f6-409f-9ba1-363c9e15d5d3"
/>



> Should include a concise description of the changes (bug or feature),
it's
> impact, along with a summary of the solution

## PR Checklist

> 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:

- [x] Make sure you reviewed

[CONTRIBUTING.md](https://github.com/googleapis/genai-toolbox/blob/main/CONTRIBUTING.md)
- [x] Make sure to open an issue as a

[bug/issue](https://github.com/googleapis/genai-toolbox/issues/new/choose)
  before writing your code! That way we can discuss the change, evaluate
  designs, and agree on the general idea
- [x] Ensure the tests and linter pass
- [x] Code coverage does not decrease (if any source code was changed)
- [x] Appropriate docs were updated (if necessary)
- [x] Make sure to add `!` if this involve a breaking change

🛠️ Fixes #1738
2025-11-13 21:17:03 +00:00
Averi Kitsch
22b5aca395 ci: add product specific automation (#1906)
## Description

Set up product specific automation

## PR Checklist

> 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:

- [ ] Make sure you reviewed

[CONTRIBUTING.md](https://github.com/googleapis/genai-toolbox/blob/main/CONTRIBUTING.md)
- [ ] Make sure to open an issue as a

[bug/issue](https://github.com/googleapis/genai-toolbox/issues/new/choose)
  before writing your code! That way we can discuss the change, evaluate
  designs, and agree on the general idea
- [ ] Ensure the tests and linter pass
- [ ] Code coverage does not decrease (if any source code was changed)
- [ ] Appropriate docs were updated (if necessary)
- [ ] Make sure to add `!` if this involve a breaking change

🛠️ Fixes #<issue_number_goes_here>
2025-11-13 20:19:06 +00:00
Mend Renovate
57f6220b9e chore(deps): update module google.golang.org/genai to v1.35.0 (#1941)
This PR contains the following updates:

| Package | Change | Age | Confidence |
|---|---|---|---|
|
[google.golang.org/genai](https://redirect.github.com/googleapis/go-genai)
| `v1.34.0` -> `v1.35.0` |
[![age](https://developer.mend.io/api/mc/badges/age/go/google.golang.org%2fgenai/v1.35.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/go/google.golang.org%2fgenai/v1.34.0/v1.35.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Release Notes

<details>
<summary>googleapis/go-genai (google.golang.org/genai)</summary>

###
[`v1.35.0`](https://redirect.github.com/googleapis/go-genai/releases/tag/v1.35.0)

[Compare
Source](https://redirect.github.com/googleapis/go-genai/compare/v1.34.0...v1.35.0)

##### Features

- Add `ToGenerationConfig` method to `GenerateContentConfig`. fixes
[#&#8203;506](https://redirect.github.com/googleapis/go-genai/issues/506)
([bd53df4](bd53df47bb))

##### Bug Fixes

- Add missing fields to the model types
([9e1b329](9e1b329097))
- Fix base\_steps parameter for recontext\_image
([4f90d03](4f90d03d57))
- Fix models.list() filter parameter
([f5859fd](f5859fd6b3))

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/googleapis/genai-toolbox).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS4xNzMuMSIsInVwZGF0ZWRJblZlciI6IjQxLjE3My4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: Yuan Teoh <45984206+Yuan325@users.noreply.github.com>
2025-11-13 20:00:39 +00:00
Mend Renovate
c451015509 chore(deps): update module github.com/trinodb/trino-go-client to v0.330.0 (#1940)
This PR contains the following updates:

| Package | Change | Age | Confidence |
|---|---|---|---|
|
[github.com/trinodb/trino-go-client](https://redirect.github.com/trinodb/trino-go-client)
| `v0.329.0` -> `v0.330.0` |
[![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2ftrinodb%2ftrino-go-client/v0.330.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2ftrinodb%2ftrino-go-client/v0.329.0/v0.330.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Release Notes

<details>
<summary>trinodb/trino-go-client
(github.com/trinodb/trino-go-client)</summary>

###
[`v0.330.0`](https://redirect.github.com/trinodb/trino-go-client/releases/tag/v0.330.0)

[Compare
Source](https://redirect.github.com/trinodb/trino-go-client/compare/v0.329.0...v0.330.0)

<!-- Release notes generated using configuration in .github/release.yml
at v0.330.0 -->

#### What's Changed

##### Other changes

- Fix tests for Trino>=477 by
[@&#8203;nineinchnick](https://redirect.github.com/nineinchnick) in
[#&#8203;161](https://redirect.github.com/trinodb/trino-go-client/pull/161)
- Add support to disable explicitPrepare by using Config.FormatDSN by
[@&#8203;Flgado](https://redirect.github.com/Flgado) in
[#&#8203;160](https://redirect.github.com/trinodb/trino-go-client/pull/160)
- Add Client Tags Support by
[@&#8203;Flgado](https://redirect.github.com/Flgado) in
[#&#8203;155](https://redirect.github.com/trinodb/trino-go-client/pull/155)

**Full Changelog**:
<https://github.com/trinodb/trino-go-client/compare/v0.329.0...v0.330.0>

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/googleapis/genai-toolbox).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS4xNzMuMSIsInVwZGF0ZWRJblZlciI6IjQxLjE3My4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: Yuan Teoh <45984206+Yuan325@users.noreply.github.com>
2025-11-13 11:45:18 -08:00
Luka Fontanilla
ef63860559 feat: adding generate embed url functionality (#1877)
## Description

> Should include a concise description of the changes (bug or feature),
it's
> impact, along with a summary of the solution

This PR adds a new tool to the Looker MCP Toolbox, that enables the user
authenticated to the Looker Source to generate authenticated embed urls
for dashboards, looks and queries. When combined with other tools that
already exist in the Looker toolbox, this enables searching existing
content and providing authenticated urls to them OR creating
authenticated urls from queries generated via Natural Language. The
embed urls will create an embed session for the user and can be opened
directly or added to an iframe `src` attribute to provide a smooth
Embedded Analytics setup.

Additionally this PR adds a new optional parameter to the Looker source
called `SessionLength` which an admin setting up the Looker source can
set to determine how long the Embed sessions last for.

## PR Checklist

> 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:

- [ X] Make sure you reviewed

[CONTRIBUTING.md](https://github.com/googleapis/genai-toolbox/blob/main/CONTRIBUTING.md)
- [ X] Make sure to open an issue as a

[bug/issue](https://github.com/googleapis/genai-toolbox/issues/new/choose)
  before writing your code! That way we can discuss the change, evaluate
  designs, and agree on the general idea
- [ ] Ensure the tests and linter pass
- [ X] Code coverage does not decrease (if any source code was changed)
- [X ] Appropriate docs were updated (if necessary)
- [ X] Make sure to add `!` if this involve a breaking change

🛠️ Fixes #1876 https://github.com/googleapis/genai-toolbox/issues/1876

---------

Co-authored-by: Luka Fontanilla <maluka@google.com>
Co-authored-by: Dr. Strangelove <drstrangelove@google.com>
2025-11-13 19:15:39 +00:00
Dr. Strangelove
a89191d8bb ci: increase time for lint to run (#1947)
## Description

As the codebase gets bigger, the linter takes longer to run. Currently
it is running in an average of 3 minutes and 30 seconds or so, but
occasionally goes to 4 minutes, which causes a failure since it hits the
timeout.

The unit tests all take 10 to 15 minutes to run, so setting the timeout
to 10m will give plenty of time for the linter to run.
2025-11-13 18:26:53 +00:00
Yuan Teoh
13a682f407 ci(mcp-registry): fix path to retrieve version string (#1946)
## Description

Fix path to retrieve Version string from server.json
2025-11-13 18:07:09 +00:00
Twisha Bansal
dc7c62c951 test: improve root test coverage (#1929)
## Description

> Should include a concise description of the changes (bug or feature),
it's
> impact, along with a summary of the solution

## PR Checklist

> 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:

- [x] Make sure you reviewed

[CONTRIBUTING.md](https://github.com/googleapis/genai-toolbox/blob/main/CONTRIBUTING.md)
- [ ] Make sure to open an issue as a

[bug/issue](https://github.com/googleapis/genai-toolbox/issues/new/choose)
  before writing your code! That way we can discuss the change, evaluate
  designs, and agree on the general idea
- [x] Ensure the tests and linter pass
- [x] Code coverage does not decrease (if any source code was changed)
- [ ] Appropriate docs were updated (if necessary)
- [ ] Make sure to add `!` if this involve a breaking change

🛠️ Fixes #<issue_number_goes_here>
2025-11-13 08:30:03 +05:30
Yuan Teoh
aec8897805 feat(mcp-registry): publish custom tools server to mcp-registry (#1834)
## Description

This PR adds a new [github
action](https://github.com/modelcontextprotocol/registry/blob/main/docs/guides/publishing/github-actions.md)
workflow to publish Toolbox to mcp-registry. This only publishes the
custom tools server, prebuilt-tools will be added in a different PR once
this is successful.

The workflow will be triggered on (1) any new release tag (v*) or (2)
manually via GHA workflow dispatch. It will grab the version number from
the `server.json` file.

The server.json file will be updated by release please during every
release.


Note: server that are published in the mcp-registry is immutable. In
cases where we successfully published a server and would like to make an
update, we will have to update the `version` field in
`.registry/server.json` and manually publish another server with a new
version.

## PR Checklist

> 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:

- [x] Make sure you reviewed

[CONTRIBUTING.md](https://github.com/googleapis/genai-toolbox/blob/main/CONTRIBUTING.md)
- [x] Make sure to open an issue as a

[bug/issue](https://github.com/googleapis/genai-toolbox/issues/new/choose)
  before writing your code! That way we can discuss the change, evaluate
  designs, and agree on the general idea
- [x] Ensure the tests and linter pass
- [x] Code coverage does not decrease (if any source code was changed)
- [x] Appropriate docs were updated (if necessary)
- [x] Make sure to add `!` if this involve a breaking change

🛠️ Fixes #1659

Issue at mcp-registry to support GAR
([#427](https://github.com/modelcontextprotocol/registry/issues/427))
2025-11-12 15:47:02 -08:00
Srividya Reddy
a4c9287aec feat(tools/postgres): add list_triggers, database_overview tools for postgres (#1912)
## Description

Adds the following tools for Postgres:
(1) list_triggers: Lists triggers in the database. .
(2) database_overview: Fetches the current state of the PostgreSQL
server.

list_triggers:
<img width="1712" height="703" alt="Screenshot 2025-11-09 at 8 16 53 PM"
src="https://github.com/user-attachments/assets/1974e303-b559-4efc-b129-444ba97c7715"
/>

<img width="874" height="513" alt="Screenshot 2025-11-09 at 8 19 43 PM"
src="https://github.com/user-attachments/assets/59ddcd15-224b-4e9a-906d-ec2645835873"
/>

database_overview:

<img width="1521" height="683" alt="Screenshot 2025-11-09 at 8 53 03 PM"
src="https://github.com/user-attachments/assets/4ae86e74-aa78-410c-a9cc-f33ae3268fb6"
/>

<img width="850" height="241" alt="Screenshot 2025-11-09 at 8 49 53 PM"
src="https://github.com/user-attachments/assets/abae2c7a-5f3e-4433-86de-3606e3298ec5"
/>


> Should include a concise description of the changes (bug or feature),
it's
> impact, along with a summary of the solution

## PR Checklist

> 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:

- [x] Make sure you reviewed

[CONTRIBUTING.md](https://github.com/googleapis/genai-toolbox/blob/main/CONTRIBUTING.md)
- [x] Make sure to open an issue as a

[bug/issue](https://github.com/googleapis/genai-toolbox/issues/new/choose)
  before writing your code! That way we can discuss the change, evaluate
  designs, and agree on the general idea
- [x] Ensure the tests and linter pass
- [x] Code coverage does not decrease (if any source code was changed)
- [x] Appropriate docs were updated (if necessary)
- [x] Make sure to add `!` if this involve a breaking change

🛠️ Fixes #1738
2025-11-12 14:35:23 -08:00
Niraj Nandre
2c228ef4f2 feat(tool/mysql): Set default host and port for MySQL source (#1922)
## Description

This pull request addresses issue
[#29](https://github.com/gemini-cli-extensions/mysql/issues/29) by
implementing default connection values for the MySQL data source. When a
user does not specify a host or port, the connection will now
automatically default to localhost:3306.

### Detailed Changes

1. **internal/prebuiltconfigs/tools/mysql.yaml**
- Added the default values to host and port.Specifically, they are now
host: ${MYSQL_HOST:localhost} and port: ${MYSQL_PORT:3306}.This allows
the configuration loader to directly use "localhost" or "3306" as a
fallback if the MYSQL_HOST or MYSQL_PORT environment variables are not
set.

**Testing and Validation**
To ensure the changes work correctly and are compatible with MySQL-like
databases, the feature was manually tested against a MariaDB instance
running in a Docker container.

The testing process involved the following steps:

1. A local testdb database with a products table was created in the
MariaDB container.
2. For Gemini CLI integration, the ~/.gemini/settings.json file was
configured to point to a local build of the toolbox executable.
3. The toolbox was launched in UI mode using the **_./toolbox
--prebuilt=mysql --ui_** command.
4. Two connection scenarios were tested to validate the new logic.

**Test 1: Default Connection (Host/Port Unset)**

The MYSQL_HOST and MYSQL_PORT environment variables were unset. The
application correctly defaulted to localhost:3306 and successfully
connected to the testdb database.
<img width="2087" height="1102" alt="Screenshot 2025-11-11 11 31 45 PM"
src="https://github.com/user-attachments/assets/a8ef5f92-eaca-472f-a0df-e2b3c0c027da"
/>

**Test 2: Explicit Connection (Host/Port Set)**

The MYSQL_HOST and MYSQL_PORT environment variables were explicitly set.
The application correctly used these values, overriding the defaults and
establishing a successful connection.
<img width="2073" height="958" alt="Screenshot 2025-11-11 6 12 44 PM"
src="https://github.com/user-attachments/assets/4b9b8838-091f-4c78-9e3b-97768323693c"
/>

**Result:**

In both scenarios, the list_tables prebuilt tool was executed via the
Toolbox UI, which successfully returned the products table from the
testdb database, confirming the changes work as expected.

Screenshot of list_tables execution from the Toolbox UI:
<img width="2251" height="1240" alt="result"
src="https://github.com/user-attachments/assets/f1c5372d-acc0-4551-af2d-fa1ee4b228d7"
/>

## PR Checklist

> 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:

- [x] Make sure you reviewed

[CONTRIBUTING.md](https://github.com/googleapis/genai-toolbox/blob/main/CONTRIBUTING.md)
- [x] Make sure to open an issue as a

[bug/issue](https://github.com/googleapis/genai-toolbox/issues/new/choose)
  before writing your code! That way we can discuss the change, evaluate
  designs, and agree on the general idea
- [ ] Ensure the tests and linter pass
- [ ] Code coverage does not decrease (if any source code was changed)
- [ ] Appropriate docs were updated (if necessary)
- [ ] Make sure to add `!` if this involve a breaking change

🛠️ Fixes [#29](https://github.com/gemini-cli-extensions/mysql/issues/29)

---------

Co-authored-by: prernakakkar-google <158031829+prernakakkar-google@users.noreply.github.com>
2025-11-12 21:22:06 +00:00
Mend Renovate
1e9c4762a5 chore(deps): update module github.com/valkey-io/valkey-go to v1.0.68 (#1920)
This PR contains the following updates:

| Package | Change | Age | Confidence |
|---|---|---|---|
|
[github.com/valkey-io/valkey-go](https://redirect.github.com/valkey-io/valkey-go)
| `v1.0.67` -> `v1.0.68` |
[![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2fvalkey-io%2fvalkey-go/v1.0.68?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2fvalkey-io%2fvalkey-go/v1.0.67/v1.0.68?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Release Notes

<details>
<summary>valkey-io/valkey-go (github.com/valkey-io/valkey-go)</summary>

###
[`v1.0.68`](https://redirect.github.com/valkey-io/valkey-go/releases/tag/v1.0.68):
1.0.68

[Compare
Source](https://redirect.github.com/valkey-io/valkey-go/compare/v1.0.67...v1.0.68)

### Changes

- feat: add new DELEX, DIGEST, MSETEX commands; extend SET and
XREADGROUP
- feat: FIPS-compliant SHA-1 loading option for Lua scripts without
performance penalty
- feat: add EPSILON option to VSIM

#### Contributors

We'd like to thank all the contributors who worked on this release!

[@&#8203;FZambia](https://redirect.github.com/FZambia) and
[@&#8203;rueian](https://redirect.github.com/rueian)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/googleapis/genai-toolbox).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS4xNzMuMSIsInVwZGF0ZWRJblZlciI6IjQxLjE3My4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->
2025-11-12 21:03:58 +00:00
Valipishetty Sathwik
7e6e88a21f feat(tool/Postgresql): Set default host and port for Postgresql source (#1927)
### Description

This pull request addresses issue #41 by implementing default connection
values for the Postgresql data source. When a user does not specify a
host or port, the connection will now automatically default to
`localhost:5432`.

---

### Detailed Changes

- **docs/en/reference/prebuilt-tools.md**
   -updated the env variables host and port to optional in the doc

- **internal/prebuiltconfigs/tools/postgres.yaml**
- Added the default values to host and port. Specifically, they are now
`host: ${POSTGRES_HOST:localhost}` and `port: ${POSTGRES_PORT:5432}`.
This allows the configuration loader to directly use `"localhost"` or
`"5432"` as a fallback if the `POSTGRES_HOST` or `POSTGRES_PORT`
environment variables are not set.

---

### PR Checklist

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:

- [ ] Make sure you reviewed  
  CONTRIBUTING.md
- [ ] Make sure to open an issue as a  
  bug/issue  
before writing your code! That way we can discuss the change, evaluate
designs, and agree on the general idea
- [ ] Ensure the tests and linter pass
- [ ] Code coverage does not decrease (if any source code was changed)
- [ ] Appropriate docs were updated (if necessary)
- [ ] Make sure to add ! if this involve a breaking change



🛠️ **Fixes** #41

---------

Co-authored-by: Averi Kitsch <akitsch@google.com>
2025-11-12 19:08:06 +00:00
Mend Renovate
b2ea4b7b8f chore(deps): update dependency pytest to v9.0.1 (#1938)
This PR contains the following updates:

| Package | Change | Age | Confidence |
|---|---|---|---|
| [pytest](https://redirect.github.com/pytest-dev/pytest)
([changelog](https://docs.pytest.org/en/stable/changelog.html)) |
`==9.0.0` -> `==9.0.1` |
[![age](https://developer.mend.io/api/mc/badges/age/pypi/pytest/9.0.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/pypi/pytest/9.0.0/9.0.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Release Notes

<details>
<summary>pytest-dev/pytest (pytest)</summary>

###
[`v9.0.1`](https://redirect.github.com/pytest-dev/pytest/releases/tag/9.0.1)

[Compare
Source](https://redirect.github.com/pytest-dev/pytest/compare/9.0.0...9.0.1)

### pytest 9.0.1 (2025-11-12)

#### Bug fixes

-
[#&#8203;13895](https://redirect.github.com/pytest-dev/pytest/issues/13895):
Restore support for skipping tests via `raise unittest.SkipTest`.
-
[#&#8203;13896](https://redirect.github.com/pytest-dev/pytest/issues/13896):
The terminal progress plugin added in pytest 9.0 is now automatically
disabled when iTerm2 is detected, it generated desktop notifications
instead of the desired functionality.
-
[#&#8203;13904](https://redirect.github.com/pytest-dev/pytest/issues/13904):
Fixed the TOML type of the verbosity settings in the API reference from
number to string.
-
[#&#8203;13910](https://redirect.github.com/pytest-dev/pytest/issues/13910):
Fixed <span class="title-ref">UserWarning: Do not expect
file\_or\_dir</span> on some earlier Python 3.12 and 3.13 point
versions.

#### Packaging updates and notes for downstreams

-
[#&#8203;13933](https://redirect.github.com/pytest-dev/pytest/issues/13933):
The tox configuration has been adjusted to make sure the desired
  version string can be passed into its `package_env` through
  the `SETUPTOOLS_SCM_PRETEND_VERSION_FOR_PYTEST` environment
  variable as a part of the release process -- by `webknjaz`.

#### Contributor-facing changes

-
[#&#8203;13891](https://redirect.github.com/pytest-dev/pytest/issues/13891),
[#&#8203;13942](https://redirect.github.com/pytest-dev/pytest/issues/13942):
The CI/CD part of the release automation is now capable of
  creating GitHub Releases without having a Git checkout on
  disk -- by `bluetech` and `webknjaz`.
-
[#&#8203;13933](https://redirect.github.com/pytest-dev/pytest/issues/13933):
The tox configuration has been adjusted to make sure the desired
  version string can be passed into its `package_env` through
  the `SETUPTOOLS_SCM_PRETEND_VERSION_FOR_PYTEST` environment
  variable as a part of the release process -- by `webknjaz`.

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/googleapis/genai-toolbox).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS4xNzMuMSIsInVwZGF0ZWRJblZlciI6IjQxLjE3My4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->
2025-11-12 10:08:41 -08:00
Twisha Bansal
cfd4b18dee docs: fix prompt message (#1930)
## Description

> Should include a concise description of the changes (bug or feature),
it's
> impact, along with a summary of the solution

## PR Checklist

> 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:

- [x] Make sure you reviewed

[CONTRIBUTING.md](https://github.com/googleapis/genai-toolbox/blob/main/CONTRIBUTING.md)
- [ ] Make sure to open an issue as a

[bug/issue](https://github.com/googleapis/genai-toolbox/issues/new/choose)
  before writing your code! That way we can discuss the change, evaluate
  designs, and agree on the general idea
- [x] Ensure the tests and linter pass
- [ ] Code coverage does not decrease (if any source code was changed)
- [ ] Appropriate docs were updated (if necessary)
- [ ] Make sure to add `!` if this involve a breaking change

🛠️ Fixes #<issue_number_goes_here>
2025-11-12 13:57:55 +05:30
Twisha Bansal
d2576cbc38 docs: add links (#1931)
## Description

> Should include a concise description of the changes (bug or feature),
it's
> impact, along with a summary of the solution

## PR Checklist

> 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:

- [x] Make sure you reviewed

[CONTRIBUTING.md](https://github.com/googleapis/genai-toolbox/blob/main/CONTRIBUTING.md)
- [ ] Make sure to open an issue as a

[bug/issue](https://github.com/googleapis/genai-toolbox/issues/new/choose)
  before writing your code! That way we can discuss the change, evaluate
  designs, and agree on the general idea
- [x] Ensure the tests and linter pass
- [ ] Code coverage does not decrease (if any source code was changed)
- [ ] Appropriate docs were updated (if necessary)
- [ ] Make sure to add `!` if this involve a breaking change

🛠️ Fixes #<issue_number_goes_here>
2025-11-12 13:04:54 +05:30
Twisha Bansal
cd56ea44fb feat: Added prompt support for toolbox (#1798)
## Description

Added MCP prompt support in the toolbox server.

- No updates needed corresponding to
https://github.com/googleapis/genai-toolbox/pull/1828/files.

## PR Checklist

> 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:

- [x] Make sure you reviewed
[CONTRIBUTING.md](https://github.com/googleapis/genai-toolbox/blob/main/CONTRIBUTING.md)
- [x] Make sure to open an issue as a
[bug/issue](https://github.com/googleapis/genai-toolbox/issues/new/choose)
before writing your code! That way we can discuss the change, evaluate
designs, and agree on the general idea
- [x] Ensure the tests and linter pass
- [x] Code coverage does not decrease (if any source code was changed)
- [x] Appropriate docs were updated (if necessary)
- [x] Make sure to add `!` if this involve a breaking change

🛠️ Fixes https://github.com/googleapis/genai-toolbox/issues/1040

---------

Co-authored-by: Yuan Teoh <45984206+Yuan325@users.noreply.github.com>
Co-authored-by: dishaprakash <57954147+dishaprakash@users.noreply.github.com>
Co-authored-by: Mend Renovate <bot@renovateapp.com>
Co-authored-by: Averi Kitsch <akitsch@google.com>
Co-authored-by: Anmol Shukla <shuklaanmol@google.com>
Co-authored-by: Harsh Jha <83023263+rapid-killer-9@users.noreply.github.com>
Co-authored-by: Wenxin Du <117315983+duwenxin99@users.noreply.github.com>
Co-authored-by: Kurtis Van Gent <31518063+kurtisvg@users.noreply.github.com>
Co-authored-by: Dr. Strangelove <drstrangelove@google.com>
Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com>
Co-authored-by: Dave Borowitz <dborowitz@google.com>
2025-11-11 23:07:51 +05:30
Sri Varshitha
12bdd95459 feat(source/alloydb, source/cloud-sql-postgres,source/cloud-sql-mysql,source/cloud-sql-mssql): Use project from env for alloydb and cloud sql control plane tools (#1588)
## Description

---
This change introduces the `DefaultProject` field for the
`alloydb-admin` and `cloud-sql-admin` sources. This field allows the
alloydb and cloud sql control plane tools to use the project value from
the environment variables (Ex: `ALLOYDB_POSTGRES_PROJECT`) if it is
already set instead of asking the user.

## PR Checklist

---
> 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:

- [x] Make sure you reviewed

[CONTRIBUTING.md](https://github.com/googleapis/genai-toolbox/blob/main/CONTRIBUTING.md)
- [x] Make sure to open an issue as a

[bug/issue](https://github.com/googleapis/genai-toolbox/issues/new/choose)
before writing your code! That way we can discuss the change, evaluate
  designs, and agree on the general idea
- [x] Ensure the tests and linter pass
- [x] Code coverage does not decrease (if any source code was changed)
- [x] Appropriate docs were updated (if necessary)
- [x] Make sure to add `!` if this involve a breaking change

🛠️ Fixes: https://github.com/gemini-cli-extensions/alloydb/issues/47

---------

Co-authored-by: Averi Kitsch <akitsch@google.com>
2025-11-11 04:14:47 +00:00
Mend Renovate
61739300be chore(deps): update dependency llama-index-llms-google-genai to v0.7.3 (#1886)
This PR contains the following updates:

| Package | Change | Age | Confidence |
|---|---|---|---|
| llama-index-llms-google-genai | `==0.7.1` -> `==0.7.3` |
[![age](https://developer.mend.io/api/mc/badges/age/pypi/llama-index-llms-google-genai/0.7.3?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/pypi/llama-index-llms-google-genai/0.7.1/0.7.3?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/googleapis/genai-toolbox).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS4xNTkuNCIsInVwZGF0ZWRJblZlciI6IjQxLjE1OS40IiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->
2025-11-10 14:01:45 -08:00
Mend Renovate
3b140f5006 chore(deps): update module google.golang.org/adk to v0.1.0 (#1887)
This PR contains the following updates:

| Package | Change | Age | Confidence |
|---|---|---|---|
| [google.golang.org/adk](https://redirect.github.com/google/adk-go) |
`v0.0.0-20251105212711-ccd61aa4a1b6` -> `v0.1.0` |
[![age](https://developer.mend.io/api/mc/badges/age/go/google.golang.org%2fadk/v0.1.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/go/google.golang.org%2fadk/v0.0.0-20251105212711-ccd61aa4a1b6/v0.1.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/googleapis/genai-toolbox).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS4xNTkuNCIsInVwZGF0ZWRJblZlciI6IjQxLjE1OS40IiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->
2025-11-10 13:29:13 -08:00
Mend Renovate
84e826a93e chore(deps): update module golang.org/x/oauth2 to v0.33.0 (#1909)
This PR contains the following updates:

| Package | Change | Age | Confidence |
|---|---|---|---|
| golang.org/x/oauth2 | `v0.32.0` -> `v0.33.0` |
[![age](https://developer.mend.io/api/mc/badges/age/go/golang.org%2fx%2foauth2/v0.33.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/go/golang.org%2fx%2foauth2/v0.32.0/v0.33.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/googleapis/genai-toolbox).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS4xNTkuNCIsInVwZGF0ZWRJblZlciI6IjQxLjE1OS40IiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: Averi Kitsch <akitsch@google.com>
2025-11-10 12:23:34 -08:00
Mend Renovate
edd739c490 chore(deps): update dependency pytest to v9 (#1911)
This PR contains the following updates:

| Package | Change | Age | Confidence |
|---|---|---|---|
| [pytest](https://redirect.github.com/pytest-dev/pytest)
([changelog](https://docs.pytest.org/en/stable/changelog.html)) |
`==8.4.2` -> `==9.0.0` |
[![age](https://developer.mend.io/api/mc/badges/age/pypi/pytest/9.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/pypi/pytest/8.4.2/9.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Release Notes

<details>
<summary>pytest-dev/pytest (pytest)</summary>

###
[`v9.0.0`](https://redirect.github.com/pytest-dev/pytest/releases/tag/9.0.0)

[Compare
Source](https://redirect.github.com/pytest-dev/pytest/compare/8.4.2...9.0.0)

### pytest 9.0.0 (2025-11-05)

#### New features

-
[#&#8203;1367](https://redirect.github.com/pytest-dev/pytest/issues/1367):
**Support for subtests** has been added.

`subtests <subtests>` are an alternative to parametrization, useful in
situations where the parametrization values are not all known at
collection time.

  Example:

  ```python
  def contains_docstring(p: Path) -> bool:
"""Return True if the given Python file contains a top-level
docstring."""
      ...

def test_py_files_contain_docstring(subtests: pytest.Subtests) -> None:
      for path in Path.cwd().glob("*.py"):
          with subtests.test(path=str(path)):
              assert contains_docstring(path)
  ```

Each assert failure or error is caught by the context manager and
reported individually, giving a clear picture of all files that are
missing a docstring.

  In addition, `unittest.TestCase.subTest` is now also supported.

This feature was originally implemented as a separate plugin in
[pytest-subtests](https://redirect.github.com/pytest-dev/pytest-subtests),
but since then has been merged into the core.

  > \[!NOTE]
> This feature is experimental and will likely evolve in future
releases. By that we mean that we might change how subtests are reported
on failure, but the functionality and how to use it are stable.

-
[#&#8203;13743](https://redirect.github.com/pytest-dev/pytest/issues/13743):
Added support for **native TOML configuration files**.

While pytest, since version 6, supports configuration in
`pyproject.toml` files under `[tool.pytest.ini_options]`,
it does so in an "INI compatibility mode", where all configuration
values are treated as strings or list of strings.
  Now, pytest supports the native TOML data model.

In `pyproject.toml`, the native TOML configuration is under the
`[tool.pytest]` table.

  ```toml
  # pyproject.toml
  [tool.pytest]
  minversion = "9.0"
  addopts = ["-ra", "-q"]
  testpaths = [
      "tests",
      "integration",
  ]
  ```

The `[tool.pytest.ini_options]` table remains supported, but both tables
cannot be used at the same time.

If you prefer to use a separate configuration file, or don't use
`pyproject.toml`, you can use `pytest.toml` or `.pytest.toml`:

  ```toml
  # pytest.toml or .pytest.toml
  [pytest]
  minversion = "9.0"
  addopts = ["-ra", "-q"]
  testpaths = [
      "tests",
      "integration",
  ]
  ```

The documentation now (sometimes) shows configuration snippets in both
TOML and INI formats, in a tabbed interface.

  See `config file formats` for full details.

-
[#&#8203;13823](https://redirect.github.com/pytest-dev/pytest/issues/13823):
Added a **"strict mode"** enabled by the `strict` configuration option.

  When set to `true`, the `strict` option currently enables

  - `strict_config`
  - `strict_markers`
  - `strict_parametrization_ids`
  - `strict_xfail`

The individual strictness options can be explicitly set to override the
global `strict` setting.

The previously-deprecated `--strict` command-line flag now enables
strict mode.

If pytest adds new strictness options in the future, they will also be
enabled in strict mode.
Therefore, you should only enable strict mode if you use a pinned/locked
version of pytest,
or if you want to proactively adopt new strictness options as they are
added.

  See `strict mode` for more details.

-
[#&#8203;13737](https://redirect.github.com/pytest-dev/pytest/issues/13737):
Added the `strict_parametrization_ids` configuration option.

When set, pytest emits an error if it detects non-unique parameter set
IDs,
rather than automatically making the IDs unique by adding <span
class="title-ref">0</span>, <span class="title-ref">1</span>, ... to
them.
  This can be particularly useful for catching unintended duplicates.

-
[#&#8203;13072](https://redirect.github.com/pytest-dev/pytest/issues/13072):
Added support for displaying test session **progress in the terminal
tab** using the [OSC
9;4;](https://conemu.github.io/en/AnsiEscapeCodes.html#ConEmu_specific_OSC)
ANSI sequence.
When pytest runs in a supported terminal emulator like ConEmu, Gnome
Terminal, Ptyxis, Windows Terminal, Kitty or Ghostty,
  you'll see the progress in the terminal tab or window,
  allowing you to monitor pytest's progress at a glance.

This feature is automatically enabled when running in a TTY. It is
implemented as an internal plugin. If needed, it can be disabled as
follows:

- On a user level, using `-p no:terminalprogress` on the command line or
via an environment variable `PYTEST_ADDOPTS='-p no:terminalprogress'`.
- On a project configuration level, using `addopts = "-p
no:terminalprogress"`.

-
[#&#8203;478](https://redirect.github.com/pytest-dev/pytest/issues/478):
Support PEP420 (implicit namespace packages) as <span
class="title-ref">--pyargs</span> target when
`consider_namespace_packages` is <span class="title-ref">true</span> in
the config.

Previously, this option only impacted package imports, now it also
impacts tests discovery.

-
[#&#8203;13678](https://redirect.github.com/pytest-dev/pytest/issues/13678):
Added a new `faulthandler_exit_on_timeout` configuration option set to
"false" by default to let <span class="title-ref">faulthandler</span>
interrupt the <span class="title-ref">pytest</span> process after a
timeout in case of deadlock.

Previously, a <span class="title-ref">faulthandler</span> timeout would
only dump the traceback of all threads to stderr, but would not
interrupt the <span class="title-ref">pytest</span> process.

  \-- by `ogrisel`.

-
[#&#8203;13829](https://redirect.github.com/pytest-dev/pytest/issues/13829):
Added support for configuration option aliases via the `aliases`
parameter in `Parser.addini() <pytest.Parser.addini>`.

  Plugins can now register alternative names for configuration options,
allowing for more flexibility in configuration naming and supporting
backward compatibility when renaming options.
The canonical name always takes precedence if both the canonical name
and an alias are specified in the configuration file.

#### Improvements in existing functionality

-
[#&#8203;13330](https://redirect.github.com/pytest-dev/pytest/issues/13330):
Having pytest configuration spread over more than one file (for example
having both a `pytest.ini` file and `pyproject.toml` with a
`[tool.pytest.ini_options]` table) will now print a warning to make it
clearer to the user that only one of them is actually used.

  \-- by `sgaist`

-
[#&#8203;13574](https://redirect.github.com/pytest-dev/pytest/issues/13574):
The single argument `--version` no longer loads the entire plugin
infrastructure, making it faster and more reliable when displaying only
the pytest version.

Passing `--version` twice (e.g., `pytest --version --version`) retains
the original behavior, showing both the pytest version and plugin
information.

  > \[!NOTE]
> Since `--version` is now processed early, it only takes effect when
passed directly via the command line. It will not work if set through
other mechanisms, such as `PYTEST_ADDOPTS` or `addopts`.

-
[#&#8203;13823](https://redirect.github.com/pytest-dev/pytest/issues/13823):
Added `strict_xfail` as an alias to the `xfail_strict` option,
  `strict_config` as an alias to the `--strict-config` flag,
  and `strict_markers` as an alias to the `--strict-markers` flag.
This makes all strictness options consistently have configuration
options with the prefix `strict_`.

-
[#&#8203;13700](https://redirect.github.com/pytest-dev/pytest/issues/13700):
<span class="title-ref">--junitxml</span> no longer prints the <span
class="title-ref">generated xml file</span> summary at the end of the
pytest session when <span class="title-ref">--quiet</span> is given.

-
[#&#8203;13732](https://redirect.github.com/pytest-dev/pytest/issues/13732):
Previously, when filtering warnings, pytest would fail if the filter
referenced a class that could not be imported. Now, this only outputs a
message indicating the problem.

-
[#&#8203;13859](https://redirect.github.com/pytest-dev/pytest/issues/13859):
Clarify the error message for <span
class="title-ref">pytest.raises()</span> when a regex <span
class="title-ref">match</span> fails.

-
[#&#8203;13861](https://redirect.github.com/pytest-dev/pytest/issues/13861):
Better sentence structure in a test's expected error message.
Previously, the error message would be "expected exception must be
\<expected>, but got \<actual>". Now, it is "Expected \<expected>, but
got \<actual>".

#### Removals and backward incompatible breaking changes

-
[#&#8203;12083](https://redirect.github.com/pytest-dev/pytest/issues/12083):
Fixed a bug where an invocation such as <span class="title-ref">pytest
a/ a/b</span> would cause only tests from <span
class="title-ref">a/b</span> to run, and not other tests under <span
class="title-ref">a/</span>.

The fix entails a few breaking changes to how such overlapping arguments
and duplicates are handled:

1. <span class="title-ref">pytest a/b a/</span> or <span
class="title-ref">pytest a/ a/b</span> are equivalent to <span
class="title-ref">pytest a</span>; if an argument overlaps another
arguments, only the prefix remains.
2. <span class="title-ref">pytest x.py x.py</span> is equivalent to
<span class="title-ref">pytest x.py</span>; previously such an
invocation was taken as an explicit request to run the tests from the
file twice.

If you rely on these behaviors, consider using `--keep-duplicates
<duplicate-paths>`, which retains its existing behavior (including the
bug).

-
[#&#8203;13719](https://redirect.github.com/pytest-dev/pytest/issues/13719):
Support for Python 3.9 is dropped following its end of life.

-
[#&#8203;13766](https://redirect.github.com/pytest-dev/pytest/issues/13766):
Previously, pytest would assume it was running in a CI/CD environment if
either of the environment variables <span class="title-ref">$CI</span>
or <span class="title-ref">$BUILD\_NUMBER</span> was defined;
now, CI mode is only activated if at least one of those variables is
defined and set to a *non-empty* value.

-
[#&#8203;13779](https://redirect.github.com/pytest-dev/pytest/issues/13779):
**PytestRemovedIn9Warning deprecation warnings are now errors by
default.**

Following our plan to remove deprecated features with as little
disruption as
possible, all warnings of type `PytestRemovedIn9Warning` now generate
errors
  instead of warning messages by default.

**The affected features will be effectively removed in pytest 9.1**, so
please consult the
`deprecations` section in the docs for directions on how to update
existing code.

In the pytest `9.0.X` series, it is possible to change the errors back
into warnings as a
  stopgap measure by adding this to your `pytest.ini` file:

  ```ini
  [pytest]
  filterwarnings =
      ignore::pytest.PytestRemovedIn9Warning
  ```

  But this will stop working when pytest `9.1` is released.

**If you have concerns** about the removal of a specific feature, please
add a
  comment to `13779`.

#### Deprecations (removal in next major release)

-
[#&#8203;13807](https://redirect.github.com/pytest-dev/pytest/issues/13807):
`monkeypatch.syspath_prepend() <pytest.MonkeyPatch.syspath_prepend>` now
issues a deprecation warning when the prepended path contains legacy
namespace packages (those using `pkg_resources.declare_namespace()`).
  Users should migrate to native namespace packages (`420`).
  See `monkeypatch-fixup-namespace-packages` for details.

#### Bug fixes

-
[#&#8203;13445](https://redirect.github.com/pytest-dev/pytest/issues/13445):
Made the type annotations of `pytest.skip` and friends more
spec-complaint to have them work across more type checkers.

-
[#&#8203;13537](https://redirect.github.com/pytest-dev/pytest/issues/13537):
Fixed a bug in which `ExceptionGroup` with only `Skipped` exceptions in
teardown was not handled correctly and showed as error.

-
[#&#8203;13598](https://redirect.github.com/pytest-dev/pytest/issues/13598):
Fixed possible collection confusion on Windows when short paths and
symlinks are involved.

-
[#&#8203;13716](https://redirect.github.com/pytest-dev/pytest/issues/13716):
Fixed a bug where a nonsensical invocation like `pytest x.py[a]` (a file
cannot be parametrized) was silently treated as `pytest x.py`. This is
now a usage error.

-
[#&#8203;13722](https://redirect.github.com/pytest-dev/pytest/issues/13722):
Fixed a misleading assertion failure message when using `pytest.approx`
on mappings with differing lengths.

-
[#&#8203;13773](https://redirect.github.com/pytest-dev/pytest/issues/13773):
Fixed the static fixture closure calculation to properly consider
transitive dependencies requested by overridden fixtures.

-
[#&#8203;13816](https://redirect.github.com/pytest-dev/pytest/issues/13816):
Fixed `pytest.approx` which now returns a clearer error message when
comparing mappings with different keys.

-
[#&#8203;13849](https://redirect.github.com/pytest-dev/pytest/issues/13849):
Hidden `.pytest.ini` files are now picked up as the config file even if
empty.
  This was an inconsistency with non-hidden `pytest.ini`.

-
[#&#8203;13865](https://redirect.github.com/pytest-dev/pytest/issues/13865):
Fixed <span class="title-ref">--show-capture</span> with <span
class="title-ref">--tb=line</span>.

-
[#&#8203;13522](https://redirect.github.com/pytest-dev/pytest/issues/13522):
Fixed `pytester` in subprocess mode ignored all :attr\`pytester.plugins
\<pytest.Pytester.plugins>\` except the first.

Fixed `pytester` in subprocess mode silently ignored non-str
`pytester.plugins <pytest.Pytester.plugins>`.
  Now it errors instead.
If you are affected by this, specify the plugin by name, or switch the
affected tests to use `pytester.runpytest_inprocess
<pytest.Pytester.runpytest_inprocess>` explicitly instead.

#### Packaging updates and notes for downstreams

-
[#&#8203;13791](https://redirect.github.com/pytest-dev/pytest/issues/13791):
Minimum requirements on `iniconfig` and `packaging` were bumped to
`1.0.1` and `22.0.0`, respectively.

#### Contributor-facing changes

-
[#&#8203;12244](https://redirect.github.com/pytest-dev/pytest/issues/12244):
Fixed self-test failures when <span class="title-ref">TERM=dumb</span>.
-
[#&#8203;12474](https://redirect.github.com/pytest-dev/pytest/issues/12474):
Added scheduled GitHub Action Workflow to run Sphinx linkchecks in repo
documentation.
-
[#&#8203;13621](https://redirect.github.com/pytest-dev/pytest/issues/13621):
pytest's own testsuite now handles the `lsof` command hanging (e.g. due
to unreachable network filesystems), with the affected selftests being
skipped after 10 seconds.
-
[#&#8203;13638](https://redirect.github.com/pytest-dev/pytest/issues/13638):
Fixed deprecated `gh pr new` command in `scripts/prepare-release-pr.py`.
The script now uses `gh pr create` which is compatible with GitHub CLI
v2.0+.
-
[#&#8203;13695](https://redirect.github.com/pytest-dev/pytest/issues/13695):
Flush <span class="title-ref">stdout</span> and <span
class="title-ref">stderr</span> in <span
class="title-ref">Pytester.run</span> to avoid truncated outputs in
<span class="title-ref">test\_faulthandler.py::test\_timeout</span> on
CI -- by `ogrisel`.
-
[#&#8203;13771](https://redirect.github.com/pytest-dev/pytest/issues/13771):
Skip <span
class="title-ref">test\_do\_not\_collect\_symlink\_siblings</span> on
Windows environments without symlink support to avoid false negatives.
-
[#&#8203;13841](https://redirect.github.com/pytest-dev/pytest/issues/13841):
`tox>=4` is now required when contributing to pytest.
-
[#&#8203;13625](https://redirect.github.com/pytest-dev/pytest/issues/13625):
Added missing docstrings to `pytest_addoption()`, `pytest_configure()`,
and `cacheshow()` functions in `cacheprovider.py`.

#### Miscellaneous internal changes

-
[#&#8203;13830](https://redirect.github.com/pytest-dev/pytest/issues/13830):
Configuration overrides (`-o`/`--override-ini`) are now processed during
startup rather than during `config.getini() <pytest.Config.getini>`.

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/googleapis/genai-toolbox).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS4xNTkuNCIsInVwZGF0ZWRJblZlciI6IjQxLjE1OS40IiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->
2025-11-10 18:59:34 +00:00
363 changed files with 10601 additions and 4878 deletions

View File

@@ -34,9 +34,47 @@ steps:
path: "/gopath"
script: |
go test -c -race -cover \
-coverpkg=./internal/sources/...,./internal/tools/... ./tests/...
-coverpkg=./internal/sources/...,./internal/tools/... \
$(go list ./tests/... | grep -v '/tests/prompts')
chmod +x .ci/test_with_coverage.sh
- id: "compile-prompt-test-binary"
name: golang:1
waitFor: ["install-dependencies"]
env:
- "GOPATH=/gopath"
volumes:
- name: "go"
path: "/gopath"
script: |
for dir in ./tests/prompts/*; do
if [ -d "$dir" ]; then
PROMPT_TYPE=$(basename "$dir")
echo "--- Compiling prompt test for ${PROMPT_TYPE} with targeted coverage ---"
go test -c -race -cover \
-coverpkg=./internal/prompts/... \
-o "prompt.${PROMPT_TYPE}.test" \
"${dir}"
fi
done
chmod +x .ci/test_prompts_with_coverage.sh
- id: "prompts-custom"
name: golang:1
waitFor: ["compile-prompt-test-binary"]
entrypoint: /bin/bash
env:
- "GOPATH=/gopath"
volumes:
- name: "go"
path: "/gopath"
args:
- -c
- |
.ci/test_prompts_with_coverage.sh "custom"
- id: "cloud-sql-pg"
name: golang:1
waitFor: ["compile-test-binary"]

View File

@@ -0,0 +1,75 @@
#!/bin/bash
# .ci/test_prompts_with_coverage.sh
#
# This script runs a specific prompt integration test, calculates its
# code coverage, and checks if it meets a minimum threshold.
#
# It is called with one argument: the type of the prompt.
# Example usage: .ci/test_prompts_with_coverage.sh "custom"
# Exit immediately if a command fails.
set -e
# --- 1. Define Variables ---
# The first argument is the prompt type (e.g., "custom").
PROMPT_TYPE=$1
COVERAGE_THRESHOLD=80 # Minimum coverage percentage required.
if [ -z "$PROMPT_TYPE" ]; then
echo "Error: No prompt type provided. Please call this script with an argument."
echo "Usage: .ci/test_prompts_with_coverage.sh <prompt_type>"
exit 1
fi
# Construct names based on the prompt type.
TEST_BINARY="./prompt.${PROMPT_TYPE}.test"
TEST_NAME="$(tr '[:lower:]' '[:upper:]' <<< ${PROMPT_TYPE:0:1})${PROMPT_TYPE:1} Prompts"
COVERAGE_FILE="coverage.prompts-${PROMPT_TYPE}.out"
# --- 2. Run Integration Tests ---
echo "--- Running integration tests for ${TEST_NAME} ---"
# Safety check for the binary's existence.
if [ ! -f "$TEST_BINARY" ]; then
echo "Error: Test binary not found at ${TEST_BINARY}. Aborting."
exit 1
fi
# Execute the test binary and generate the coverage file.
# If the tests fail, the 'set -e' command will cause the script to exit here.
if ! ./"${TEST_BINARY}" -test.v -test.coverprofile="${COVERAGE_FILE}"; then
echo "Error: Tests for ${TEST_NAME} failed. Exiting."
exit 1
fi
echo "--- Tests for ${TEST_NAME} passed successfully ---"
# --- 3. Calculate and Check Coverage ---
echo "Calculating coverage for ${TEST_NAME}..."
# Calculate the total coverage percentage from the generated file.
# The '2>/dev/null' suppresses warnings if the coverage file is empty.
total_coverage=$(go tool cover -func="${COVERAGE_FILE}" 2>/dev/null | grep "total:" | awk '{print $3}')
if [ -z "$total_coverage" ]; then
echo "Warning: Could not calculate coverage for ${TEST_NAME}. The coverage report might be empty."
total_coverage="0%"
fi
echo "${TEST_NAME} total coverage: $total_coverage"
# Remove the '%' sign for numerical comparison.
coverage_numeric=$(echo "$total_coverage" | sed 's/%//')
# Check if the coverage is below the defined threshold.
if awk -v coverage="$coverage_numeric" -v threshold="$COVERAGE_THRESHOLD" 'BEGIN {exit !(coverage < threshold)}'; then
echo "Coverage failure: ${TEST_NAME} total coverage (${total_coverage}) is below the ${COVERAGE_THRESHOLD}% threshold."
exit 1
else
echo "Coverage for ${TEST_NAME} is sufficient."
fi

11
.github/CODEOWNERS vendored
View File

@@ -4,3 +4,14 @@
# https://help.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners#codeowners-syntax
* @googleapis/senseai-eco
# Code & Tests
**/alloydb*/ @googleapis/toolbox-alloydb
**/bigquery/ @googleapis/toolbox-bigquery
**/bigtable/ @googleapis/toolbox-bigtable
**/cloudsqlmssql/ @googleapis/toolbox-cloud-sql-mssql
**/cloudsqlmysql/ @googleapis/toolbox-cloud-sql-mysql
**/cloudsqlpg/ @googleapis/toolbox-cloud-sql-postgres
**/dataplex/ @googleapis/toolbox-dataplex
**/firestore/ @googleapis/toolbox-firestore
**/looker/ @googleapis/toolbox-looker
**/spanner/ @googleapis/toolbox-spanner

View File

@@ -19,17 +19,89 @@ assign_issues:
- anubhav756
- twishabansal
assign_issues_by:
- labels:
- 'product: alloydb'
to:
- 'googleapis/toolbox-alloydb'
- labels:
- 'product: bigquery'
to:
- Genesis929
- shobsi
- jiaxunwu
- 'googleapis/toolbox-bigquery'
- labels:
- 'product: bigtable'
to:
- 'googleapis/toolbox-bigtable'
- labels:
- 'product: mssql'
to:
- 'googleapis/toolbox-cloud-sql-mssql'
- labels:
- 'product: mysql'
to:
- 'googleapis/toolbox-cloud-sql-mysql'
- labels:
- 'product: postgres'
to:
- 'googleapis/toolbox-cloud-sql-postgres'
- labels:
- 'product: dataplex'
to:
- 'googleapis/toolbox-dataplex'
- labels:
- 'product: firestore'
to:
- 'googleapis/toolbox-firestore'
- labels:
- 'product: looker'
to:
- drstrangelooker
- 'googleapis/toolbox-looker'
- labels:
- 'product: spanner'
to:
- 'googleapis/toolbox-spanner'
assign_prs:
- Yuan325
- duwenxin99
- averikitsch
- duwenxin99
- averikitsch
assign_prs_by:
- labels:
- 'product: alloydb'
to:
- 'googleapis/toolbox-alloydb'
- labels:
- 'product: bigquery'
to:
- 'googleapis/toolbox-bigquery'
- labels:
- 'product: bigtable'
to:
- 'googleapis/toolbox-bigtable'
- labels:
- 'product: mssql'
to:
- 'googleapis/toolbox-cloud-sql-mssql'
- labels:
- 'product: mysql'
to:
- 'googleapis/toolbox-cloud-sql-mysql'
- labels:
- 'product: postgres'
to:
- 'googleapis/toolbox-cloud-sql-postgres'
- labels:
- 'product: dataplex'
to:
- 'googleapis/toolbox-dataplex'
- labels:
- 'product: firestore'
to:
- 'googleapis/toolbox-firestore'
- labels:
- 'product: looker'
to:
- 'googleapis/toolbox-looker'
- labels:
- 'product: spanner'
to:
- 'googleapis/toolbox-spanner'

86
.github/labels.yaml vendored
View File

@@ -93,10 +93,90 @@
description: 'Use label to signal PR should be included in the next release.'
# Product Labels
- name: 'product: alloydb'
color: 5065c7
description: 'AlloyDB'
- name: 'product: bigquery'
color: 5065c7
description: 'Product: Assigned to the BigQuery team.'
# Product Labels
description: 'BigQuery'
- name: 'product: bigtable'
color: 5065c7
description: 'Bigtable'
- name: 'product: cassandra'
color: 5065c7
description: 'Cassandra'
- name: 'product: clickhouse'
color: 5065c7
description: 'ClickHouse'
- name: 'product: mssql'
color: 5065c7
description: 'SQL Server'
- name: 'product: mysql'
color: 5065c7
description: 'MySQL'
- name: 'product: postgres'
color: 5065c7
description: 'PostgreSQL'
- name: 'product: couchbase'
color: 5065c7
description: 'Couchbase'
- name: 'product: dataplex'
color: 5065c7
description: 'Dataplex'
- name: 'product: dgraph'
color: 5065c7
description: 'Dgraph'
- name: 'product: elasticsearch'
color: 5065c7
description: 'Elasticsearch'
- name: 'product: firebird'
color: 5065c7
description: 'Firebird'
- name: 'product: firestore'
color: 5065c7
description: 'Firestore'
- name: 'product: looker'
color: 5065c7
description: 'Product: Assigned to the Looker team.'
description: 'Looker'
- name: 'product: mindsdb'
color: 5065c7
description: 'MindsDB'
- name: 'product: mongodb'
color: 5065c7
description: 'MongoDB'
- name: 'product: neo4j'
color: 5065c7
description: 'Neo4j'
- name: 'product: oceanbase'
color: 5065c7
description: 'OceanBase'
- name: 'product: oracle'
color: 5065c7
description: 'Oracle'
- name: 'product: redis'
color: 5065c7
description: 'Redis'
- name: 'product: serverlessspark'
color: 5065c7
description: 'Serverless Spark'
- name: 'product: singlestore'
color: 5065c7
description: 'SingleStore'
- name: 'product: spanner'
color: 5065c7
description: 'Spanner'
- name: 'product: sqlite'
color: 5065c7
description: 'SQLite'
- name: 'product: tidb'
color: 5065c7
description: 'TiDB'
- name: 'product: trino'
color: 5065c7
description: 'Trino'
- name: 'product: valkey'
color: 5065c7
description: 'Valkey'
- name: 'product: yugabytedb'
color: 5065c7
description: 'YugabyteDB'

View File

@@ -38,4 +38,14 @@ extraFiles: [
"docs/en/how-to/connect-ide/neo4j_mcp.md",
"docs/en/how-to/connect-ide/sqlite_mcp.md",
"gemini-extension.json",
]
{
"type": "json",
"path": ".registry/server.json",
"jsonpath": "$.version"
},
{
"type": "json",
"path": ".registry/server.json",
"jsonpath": "$.packages[0].identifier"
},
]

View File

@@ -69,4 +69,4 @@ jobs:
uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8.0.0
with:
version: latest
args: --timeout 4m
args: --timeout 10m

73
.github/workflows/publish-mcp.yml vendored Normal file
View File

@@ -0,0 +1,73 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
name: Publish to MCP Registry
on:
push:
tags: ["v*"] # Triggers on version tags like v1.0.0
# allow manual triggering with no inputs required
workflow_dispatch:
jobs:
publish:
runs-on: ubuntu-latest
permissions:
id-token: write # Required for OIDC authentication
contents: read
steps:
- name: Checkout code
uses: actions/checkout@v5
- name: Wait for image in Artifact Registry
shell: bash
run: |
MAX_ATTEMPTS=10
VERSION=$(jq -r '.version' .registry/server.json)
REGISTRY_URL="https://us-central1-docker.pkg.dev/v2/database-toolbox/toolbox/toolbox/manifests/${VERSION}"
# initially sleep time to wait for the version release
sleep 3m
for i in $(seq 1 ${MAX_ATTEMPTS}); do
echo "Attempt $i: Checking for image ${REGISTRY_URL}..."
# Use curl to check the manifest header
# Using -I to fetch headers only, -s silent, -f fail fast on errors.
curl -Isf "${REGISTRY_URL}" > /dev/null
if [ $? -eq 0 ]; then
echo "✅ Image found! Continuing to next steps."
exit 0
else
echo "❌ Image not found (likely 404 error) on attempt $i."
if [ $i -lt ${MAX_ATTEMPTS} ]; then
echo "Sleeping for 5 minutes before next attempt..."
sleep 2m
else
echo "Maximum attempts reached. Image not found."
exit 1
fi
fi
done
- name: Install MCP Publisher
run: |
curl -L "https://github.com/modelcontextprotocol/registry/releases/latest/download/mcp-publisher_$(uname -s | tr '[:upper:]' '[:lower:]')_$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/').tar.gz" | tar xz mcp-publisher
- name: Login to MCP Registry
run: ./mcp-publisher login github-oidc
- name: Publish to MCP Registry
run: ./mcp-publisher publish --file=.registry/server.json

View File

@@ -81,11 +81,12 @@ jobs:
run: |
source_dir="./internal/sources/*"
tool_dir="./internal/tools/*"
prompt_dir="./internal/prompts/*"
auth_dir="./internal/auth/*"
int_test_dir="./tests/*"
included_packages=$(go list ./... | grep -v -e "$source_dir" -e "$tool_dir" -e "$auth_dir" -e "$int_test_dir")
included_packages=$(go list ./... | grep -v -e "$source_dir" -e "$tool_dir" -e "$prompt_dir" -e "$auth_dir" -e "$int_test_dir")
go test -race -cover -coverprofile=coverage.out -v $included_packages
go test -race -v ./internal/sources/... ./internal/tools/... ./internal/auth/...
go test -race -v ./internal/sources/... ./internal/tools/... ./internal/prompts/... ./internal/auth/...
- name: Run tests without coverage
if: ${{ runner.os != 'Linux' }}

View File

@@ -51,6 +51,10 @@ ignoreFiles = ["quickstart/shared", "quickstart/python", "quickstart/js", "quick
# Add a new version block here before every release
# The order of versions in this file is mirrored into the dropdown
[[params.versions]]
version = "v0.20.0"
url = "https://googleapis.github.io/genai-toolbox/v0.20.0/"
[[params.versions]]
version = "v0.19.1"
url = "https://googleapis.github.io/genai-toolbox/v0.19.1/"

76
.registry/server.json Normal file
View File

@@ -0,0 +1,76 @@
{
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-10-17/server.schema.json",
"name": "io.github.googleapis/genai-toolbox",
"description": "MCP Toolbox for Databases enables your agent to connect to your database.",
"title": "MCP Toolbox",
"websiteUrl": "https://github.com/googleapis/genai-toolbox",
"icons": [
{
"src": "https://github.com/googleapis/genai-toolbox/blob/main/.hugo/assets/icons/logo.svg",
"mimeType": "image/svg+xml"
}
],
"repository": {
"url": "https://github.com/googleapis/genai-toolbox",
"source": "github"
},
"version": "0.20.0",
"packages": [
{
"registryType": "oci",
"registryBaseUrl": "https://artifactregistry.googleapis.com",
"identifier": "us-central1-docker.pkg.dev/database-toolbox/toolbox/toolbox:0.20.0",
"transport": {
"type": "streamable-http",
"url": "http://{host}:{port}/mcp"
},
"runtimeArguments": [
{
"type": "named",
"name": "--tools-file",
"description": "File path specifying the tool configuration.",
"default": "tools.yaml",
"isRequired": false
},
{
"type": "named",
"name": "--address",
"description": "Address of the interface the server will listen on.",
"value": "{host}",
"variables": {
"host": {
"description": "ip address",
"isRequired": true,
"default": "127.0.0.1"
}
}
},
{
"type": "named",
"name": "--port",
"description": "Port the server will listen on.",
"value": "{port}",
"variables": {
"port": {
"description": "port",
"isRequired": true,
"default": "5000"
}
}
},
{
"type": "named",
"name": "--log-level",
"description": "Specify the minimum level logged.",
"default": "info",
"choices": [
"debug",
"info",
"warn",
"error"
]
}
]
}
]
}

View File

@@ -1,5 +1,18 @@
# Changelog
## [0.20.0](https://github.com/googleapis/genai-toolbox/compare/v0.19.1...v0.20.0) (2025-11-14)
### Features
* Added prompt support for toolbox ([#1798](https://github.com/googleapis/genai-toolbox/issues/1798)) ([cd56ea4](https://github.com/googleapis/genai-toolbox/commit/cd56ea44fbdd149fcb92324e70ee36ac747635db))
* **source/alloydb, source/cloud-sql-postgres,source/cloud-sql-mysql,source/cloud-sql-mssql:** Use project from env for alloydb and cloud sql control plane tools ([#1588](https://github.com/googleapis/genai-toolbox/issues/1588)) ([12bdd95](https://github.com/googleapis/genai-toolbox/commit/12bdd954597e49d3ec6b247cc104584c5a4d1943))
* **source/mysql:** Set default host and port for MySQL source ([#1922](https://github.com/googleapis/genai-toolbox/issues/1922)) ([2c228ef](https://github.com/googleapis/genai-toolbox/commit/2c228ef4f2d4cb8dfc41e845466bfe3566d141a1))
* **source/Postgresql:** Set default host and port for Postgresql source ([#1927](https://github.com/googleapis/genai-toolbox/issues/1927)) ([7e6e88a](https://github.com/googleapis/genai-toolbox/commit/7e6e88a21f2b9b60e0d645cdde33a95892d31a04))
* **tool/looker-generate-embed-url:** Adding generate embed url tool ([#1877](https://github.com/googleapis/genai-toolbox/issues/1877)) ([ef63860](https://github.com/googleapis/genai-toolbox/commit/ef63860559798fbad54c1051d9f53bce42d66464))
* **tools/postgres:** Add `list_triggers`, `database_overview` tools for postgres ([#1912](https://github.com/googleapis/genai-toolbox/issues/1912)) ([a4c9287](https://github.com/googleapis/genai-toolbox/commit/a4c9287aecf848faa98d973a9ce5b13fa309a58e))
* **tools/postgres:** Add list_indexes, list_sequences tools for postgres ([#1765](https://github.com/googleapis/genai-toolbox/issues/1765)) ([897c63d](https://github.com/googleapis/genai-toolbox/commit/897c63dcea43226262d2062088c59f2d1068fca7))
## [0.19.1](https://github.com/googleapis/genai-toolbox/compare/v0.18.0...v0.19.1) (2025-11-07)

View File

@@ -337,7 +337,7 @@ for instructions on developing Toolbox SDKs.
Team `@googleapis/senseai-eco` has been set as
[CODEOWNERS](.github/CODEOWNERS). The GitHub TeamSync tool is used to create
this team from MDB Group, `senseai-eco`.
this team from MDB Group, `senseai-eco`. Additionally, database-specific GitHub teams (e.g., `@googleapis/toolbox-alloydb`) have been created from MDB groups to manage code ownership and review for individual database products.
Team `@googleapis/toolbox-contributors` has write access to this repo. They
can create branches and approve test runs. But they do not have the ability
@@ -441,7 +441,7 @@ Trigger pull request tests for external contributors by:
## Repo Setup & Automation
* .github/blunderbuss.yml - Auto-assign issues and PRs from GitHub teams
* .github/blunderbuss.yml - Auto-assign issues and PRs from GitHub teams. Use a product label to assign to a product-specific team member.
* .github/renovate.json5 - Tooling for dependency updates. Dependabot is built
into the GitHub repo for GitHub security warnings
* go/github-issue-mirror - GitHub issues are automatically mirrored into buganizer

View File

@@ -39,6 +39,7 @@ documentation](https://googleapis.github.io/genai-toolbox/).
- [Sources](#sources)
- [Tools](#tools)
- [Toolsets](#toolsets)
- [Prompts](#prompts)
- [Versioning](#versioning)
- [Pre-1.0.0 Versioning](#pre-100-versioning)
- [Post-1.0.0 Versioning](#post-100-versioning)
@@ -124,7 +125,7 @@ To install Toolbox as a binary:
>
> ```sh
> # see releases page for other versions
> export VERSION=0.19.1
> export VERSION=0.20.0
> curl -L -o toolbox https://storage.googleapis.com/genai-toolbox/v$VERSION/linux/amd64/toolbox
> chmod +x toolbox
> ```
@@ -137,7 +138,7 @@ To install Toolbox as a binary:
>
> ```sh
> # see releases page for other versions
> export VERSION=0.19.1
> export VERSION=0.20.0
> curl -L -o toolbox https://storage.googleapis.com/genai-toolbox/v$VERSION/darwin/arm64/toolbox
> chmod +x toolbox
> ```
@@ -150,7 +151,7 @@ To install Toolbox as a binary:
>
> ```sh
> # see releases page for other versions
> export VERSION=0.19.1
> export VERSION=0.20.0
> curl -L -o toolbox https://storage.googleapis.com/genai-toolbox/v$VERSION/darwin/amd64/toolbox
> chmod +x toolbox
> ```
@@ -163,7 +164,7 @@ To install Toolbox as a binary:
>
> ```powershell
> # see releases page for other versions
> $VERSION = "0.19.1"
> $VERSION = "0.20.0"
> Invoke-WebRequest -Uri "https://storage.googleapis.com/genai-toolbox/v$VERSION/windows/amd64/toolbox.exe" -OutFile "toolbox.exe"
> ```
>
@@ -176,7 +177,7 @@ You can also install Toolbox as a container:
```sh
# see releases page for other versions
export VERSION=0.19.1
export VERSION=0.20.0
docker pull us-central1-docker.pkg.dev/database-toolbox/toolbox/toolbox:$VERSION
```
@@ -200,7 +201,7 @@ To install from source, ensure you have the latest version of
[Go installed](https://go.dev/doc/install), and then run the following command:
```sh
go install github.com/googleapis/genai-toolbox@v0.19.1
go install github.com/googleapis/genai-toolbox@v0.20.0
```
<!-- {x-release-please-end} -->
@@ -932,6 +933,25 @@ all_tools = client.load_toolset()
my_second_toolset = client.load_toolset("my_second_toolset")
```
### Prompts
The `prompts` section of a `tools.yaml` defines prompts that can be used for
interactions with LLMs.
```yaml
prompts:
code_review:
description: "Asks the LLM to analyze code quality and suggest improvements."
messages:
- content: "Please review the following code for quality, correctness, and potential improvements: \n\n{{.code}}"
arguments:
- name: "code"
description: "The code to review"
```
For more details on configuring prompts, see the
[Prompts](https://googleapis.github.io/genai-toolbox/resources/prompts).
## Versioning
This project uses [semantic versioning](https://semver.org/) (`MAJOR.MINOR.PATCH`).

View File

@@ -35,12 +35,16 @@ import (
"github.com/googleapis/genai-toolbox/internal/auth"
"github.com/googleapis/genai-toolbox/internal/log"
"github.com/googleapis/genai-toolbox/internal/prebuiltconfigs"
"github.com/googleapis/genai-toolbox/internal/prompts"
"github.com/googleapis/genai-toolbox/internal/server"
"github.com/googleapis/genai-toolbox/internal/sources"
"github.com/googleapis/genai-toolbox/internal/telemetry"
"github.com/googleapis/genai-toolbox/internal/tools"
"github.com/googleapis/genai-toolbox/internal/util"
// Import prompt packages for side effect of registration
_ "github.com/googleapis/genai-toolbox/internal/prompts/custom"
// Import tool packages for side effect of registration
_ "github.com/googleapis/genai-toolbox/internal/tools/alloydb/alloydbcreatecluster"
_ "github.com/googleapis/genai-toolbox/internal/tools/alloydb/alloydbcreateinstance"
@@ -118,6 +122,7 @@ import (
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookercreateprojectfile"
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookerdeleteprojectfile"
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookerdevmode"
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookergenerateembedurl"
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookergetconnectiondatabases"
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookergetconnections"
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookergetconnectionschemas"
@@ -172,12 +177,16 @@ import (
_ "github.com/googleapis/genai-toolbox/internal/tools/oceanbase/oceanbasesql"
_ "github.com/googleapis/genai-toolbox/internal/tools/oracle/oracleexecutesql"
_ "github.com/googleapis/genai-toolbox/internal/tools/oracle/oraclesql"
_ "github.com/googleapis/genai-toolbox/internal/tools/postgres/postgresdatabaseoverview"
_ "github.com/googleapis/genai-toolbox/internal/tools/postgres/postgresexecutesql"
_ "github.com/googleapis/genai-toolbox/internal/tools/postgres/postgreslistactivequeries"
_ "github.com/googleapis/genai-toolbox/internal/tools/postgres/postgreslistavailableextensions"
_ "github.com/googleapis/genai-toolbox/internal/tools/postgres/postgreslistindexes"
_ "github.com/googleapis/genai-toolbox/internal/tools/postgres/postgreslistinstalledextensions"
_ "github.com/googleapis/genai-toolbox/internal/tools/postgres/postgreslistschemas"
_ "github.com/googleapis/genai-toolbox/internal/tools/postgres/postgreslistsequences"
_ "github.com/googleapis/genai-toolbox/internal/tools/postgres/postgreslisttables"
_ "github.com/googleapis/genai-toolbox/internal/tools/postgres/postgreslisttriggers"
_ "github.com/googleapis/genai-toolbox/internal/tools/postgres/postgreslistviews"
_ "github.com/googleapis/genai-toolbox/internal/tools/postgres/postgressql"
_ "github.com/googleapis/genai-toolbox/internal/tools/redis"
@@ -360,12 +369,13 @@ type ToolsFile struct {
AuthServices server.AuthServiceConfigs `yaml:"authServices"`
Tools server.ToolConfigs `yaml:"tools"`
Toolsets server.ToolsetConfigs `yaml:"toolsets"`
Prompts server.PromptConfigs `yaml:"prompts"`
}
// parseEnv replaces environment variables ${ENV_NAME} with their values.
// also support ${ENV_NAME:default_value}.
func parseEnv(input string) (string, error) {
re := regexp.MustCompile(`\$\{(\w+)(:(\w*))?\}`)
re := regexp.MustCompile(`\$\{(\w+)(:([^}]*))?\}`)
var err error
output := re.ReplaceAllStringFunc(input, func(match string) string {
@@ -376,7 +386,7 @@ func parseEnv(input string) (string, error) {
if value, found := os.LookupEnv(variableName); found {
return value
}
if parts[2] != "" {
if len(parts) >= 4 && parts[2] != "" {
return parts[3]
}
err = fmt.Errorf("environment variable not found: %q", variableName)
@@ -412,6 +422,7 @@ func mergeToolsFiles(files ...ToolsFile) (ToolsFile, error) {
AuthServices: make(server.AuthServiceConfigs),
Tools: make(server.ToolConfigs),
Toolsets: make(server.ToolsetConfigs),
Prompts: make(server.PromptConfigs),
}
var conflicts []string
@@ -461,11 +472,20 @@ func mergeToolsFiles(files ...ToolsFile) (ToolsFile, error) {
merged.Toolsets[name] = toolset
}
}
// Check for conflicts and merge prompts
for name, prompt := range file.Prompts {
if _, exists := merged.Prompts[name]; exists {
conflicts = append(conflicts, fmt.Sprintf("prompt '%s' (file #%d)", name, fileIndex+1))
} else {
merged.Prompts[name] = prompt
}
}
}
// If conflicts were detected, return an error
if len(conflicts) > 0 {
return ToolsFile{}, fmt.Errorf("resource conflicts detected:\n - %s\n\nPlease ensure each source, authService, tool, and toolset has a unique name across all files", strings.Join(conflicts, "\n - "))
return ToolsFile{}, fmt.Errorf("resource conflicts detected:\n - %s\n\nPlease ensure each source, authService, tool, toolset and prompt has a unique name across all files", strings.Join(conflicts, "\n - "))
}
return merged, nil
@@ -539,14 +559,14 @@ func handleDynamicReload(ctx context.Context, toolsFile ToolsFile, s *server.Ser
panic(err)
}
sourcesMap, authServicesMap, toolsMap, toolsetsMap, err := validateReloadEdits(ctx, toolsFile)
sourcesMap, authServicesMap, toolsMap, toolsetsMap, promptsMap, promptsetsMap, err := validateReloadEdits(ctx, toolsFile)
if err != nil {
errMsg := fmt.Errorf("unable to validate reloaded edits: %w", err)
logger.WarnContext(ctx, errMsg.Error())
return err
}
s.ResourceMgr.SetResources(sourcesMap, authServicesMap, toolsMap, toolsetsMap)
s.ResourceMgr.SetResources(sourcesMap, authServicesMap, toolsMap, toolsetsMap, promptsMap, promptsetsMap)
return nil
}
@@ -554,7 +574,7 @@ func handleDynamicReload(ctx context.Context, toolsFile ToolsFile, s *server.Ser
// validateReloadEdits checks that the reloaded tools file configs can initialized without failing
func validateReloadEdits(
ctx context.Context, toolsFile ToolsFile,
) (map[string]sources.Source, map[string]auth.AuthService, map[string]tools.Tool, map[string]tools.Toolset, error,
) (map[string]sources.Source, map[string]auth.AuthService, map[string]tools.Tool, map[string]tools.Toolset, map[string]prompts.Prompt, map[string]prompts.Promptset, error,
) {
logger, err := util.LoggerFromContext(ctx)
if err != nil {
@@ -577,16 +597,17 @@ func validateReloadEdits(
AuthServiceConfigs: toolsFile.AuthServices,
ToolConfigs: toolsFile.Tools,
ToolsetConfigs: toolsFile.Toolsets,
PromptConfigs: toolsFile.Prompts,
}
sourcesMap, authServicesMap, toolsMap, toolsetsMap, err := server.InitializeConfigs(ctx, reloadedConfig)
sourcesMap, authServicesMap, toolsMap, toolsetsMap, promptsMap, promptsetsMap, err := server.InitializeConfigs(ctx, reloadedConfig)
if err != nil {
errMsg := fmt.Errorf("unable to initialize reloaded configs: %w", err)
logger.WarnContext(ctx, errMsg.Error())
return nil, nil, nil, nil, err
return nil, nil, nil, nil, nil, nil, err
}
return sourcesMap, authServicesMap, toolsMap, toolsetsMap, nil
return sourcesMap, authServicesMap, toolsMap, toolsetsMap, promptsMap, promptsetsMap, nil
}
// watchChanges checks for changes in the provided yaml tools file(s) or folder.
@@ -877,7 +898,8 @@ func run(cmd *Command) error {
}
}
cmd.cfg.SourceConfigs, cmd.cfg.AuthServiceConfigs, cmd.cfg.ToolConfigs, cmd.cfg.ToolsetConfigs = toolsFile.Sources, toolsFile.AuthServices, toolsFile.Tools, toolsFile.Toolsets
cmd.cfg.SourceConfigs, cmd.cfg.AuthServiceConfigs, cmd.cfg.ToolConfigs, cmd.cfg.ToolsetConfigs, cmd.cfg.PromptConfigs = toolsFile.Sources, toolsFile.AuthServices, toolsFile.Tools, toolsFile.Toolsets, toolsFile.Prompts
authSourceConfigs := toolsFile.AuthSources
if authSourceConfigs != nil {
cmd.logger.WarnContext(ctx, "`authSources` is deprecated, use `authServices` instead")

View File

@@ -34,6 +34,8 @@ import (
"github.com/googleapis/genai-toolbox/internal/auth/google"
"github.com/googleapis/genai-toolbox/internal/log"
"github.com/googleapis/genai-toolbox/internal/prebuiltconfigs"
"github.com/googleapis/genai-toolbox/internal/prompts"
"github.com/googleapis/genai-toolbox/internal/prompts/custom"
"github.com/googleapis/genai-toolbox/internal/server"
cloudsqlpgsrc "github.com/googleapis/genai-toolbox/internal/sources/cloudsqlpg"
httpsrc "github.com/googleapis/genai-toolbox/internal/sources/http"
@@ -43,6 +45,7 @@ import (
"github.com/googleapis/genai-toolbox/internal/tools/http"
"github.com/googleapis/genai-toolbox/internal/tools/postgres/postgressql"
"github.com/googleapis/genai-toolbox/internal/util"
"github.com/googleapis/genai-toolbox/internal/util/parameters"
"github.com/spf13/cobra"
)
@@ -513,8 +516,8 @@ func TestParseToolFile(t *testing.T) {
Source: "my-pg-instance",
Description: "some description",
Statement: "SELECT * FROM SQL_STATEMENT;\n",
Parameters: []tools.Parameter{
tools.NewStringParameter("country", "some description"),
Parameters: []parameters.Parameter{
parameters.NewStringParameter("country", "some description"),
},
AuthRequired: []string{},
},
@@ -525,6 +528,38 @@ func TestParseToolFile(t *testing.T) {
ToolNames: []string{"example_tool"},
},
},
Prompts: nil,
},
},
{
description: "with prompts example",
in: `
prompts:
my-prompt:
description: A prompt template for data analysis.
arguments:
- name: country
description: The country to analyze.
messages:
- content: Analyze the data for {{.country}}.
`,
wantToolsFile: ToolsFile{
Sources: nil,
AuthServices: nil,
Tools: nil,
Toolsets: nil,
Prompts: server.PromptConfigs{
"my-prompt": &custom.Config{
Name: "my-prompt",
Description: "A prompt template for data analysis.",
Arguments: prompts.Arguments{
{Parameter: parameters.NewStringParameter("country", "The country to analyze.")},
},
Messages: []prompts.Message{
{Role: "user", Content: "Analyze the data for {{.country}}."},
},
},
},
},
},
}
@@ -544,7 +579,10 @@ func TestParseToolFile(t *testing.T) {
t.Fatalf("incorrect tools parse: diff %v", diff)
}
if diff := cmp.Diff(tc.wantToolsFile.Toolsets, toolsFile.Toolsets); diff != "" {
t.Fatalf("incorrect tools parse: diff %v", diff)
t.Fatalf("incorrect toolsets parse: diff %v", diff)
}
if diff := cmp.Diff(tc.wantToolsFile.Prompts, toolsFile.Prompts); diff != "" {
t.Fatalf("incorrect prompts parse: diff %v", diff)
}
})
}
@@ -645,10 +683,10 @@ func TestParseToolFileWithAuth(t *testing.T) {
Description: "some description",
Statement: "SELECT * FROM SQL_STATEMENT;\n",
AuthRequired: []string{},
Parameters: []tools.Parameter{
tools.NewStringParameter("country", "some description"),
tools.NewIntParameterWithAuth("id", "user id", []tools.ParamAuthService{{Name: "my-google-service", Field: "user_id"}}),
tools.NewStringParameterWithAuth("email", "user email", []tools.ParamAuthService{{Name: "my-google-service", Field: "email"}, {Name: "other-google-service", Field: "other_email"}}),
Parameters: []parameters.Parameter{
parameters.NewStringParameter("country", "some description"),
parameters.NewIntParameterWithAuth("id", "user id", []parameters.ParamAuthService{{Name: "my-google-service", Field: "user_id"}}),
parameters.NewStringParameterWithAuth("email", "user email", []parameters.ParamAuthService{{Name: "my-google-service", Field: "email"}, {Name: "other-google-service", Field: "other_email"}}),
},
},
},
@@ -658,6 +696,7 @@ func TestParseToolFileWithAuth(t *testing.T) {
ToolNames: []string{"example_tool"},
},
},
Prompts: nil,
},
},
{
@@ -744,10 +783,10 @@ func TestParseToolFileWithAuth(t *testing.T) {
Description: "some description",
Statement: "SELECT * FROM SQL_STATEMENT;\n",
AuthRequired: []string{},
Parameters: []tools.Parameter{
tools.NewStringParameter("country", "some description"),
tools.NewIntParameterWithAuth("id", "user id", []tools.ParamAuthService{{Name: "my-google-service", Field: "user_id"}}),
tools.NewStringParameterWithAuth("email", "user email", []tools.ParamAuthService{{Name: "my-google-service", Field: "email"}, {Name: "other-google-service", Field: "other_email"}}),
Parameters: []parameters.Parameter{
parameters.NewStringParameter("country", "some description"),
parameters.NewIntParameterWithAuth("id", "user id", []parameters.ParamAuthService{{Name: "my-google-service", Field: "user_id"}}),
parameters.NewStringParameterWithAuth("email", "user email", []parameters.ParamAuthService{{Name: "my-google-service", Field: "email"}, {Name: "other-google-service", Field: "other_email"}}),
},
},
},
@@ -757,6 +796,7 @@ func TestParseToolFileWithAuth(t *testing.T) {
ToolNames: []string{"example_tool"},
},
},
Prompts: nil,
},
},
{
@@ -845,10 +885,10 @@ func TestParseToolFileWithAuth(t *testing.T) {
Description: "some description",
Statement: "SELECT * FROM SQL_STATEMENT;\n",
AuthRequired: []string{"my-google-service"},
Parameters: []tools.Parameter{
tools.NewStringParameter("country", "some description"),
tools.NewIntParameterWithAuth("id", "user id", []tools.ParamAuthService{{Name: "my-google-service", Field: "user_id"}}),
tools.NewStringParameterWithAuth("email", "user email", []tools.ParamAuthService{{Name: "my-google-service", Field: "email"}, {Name: "other-google-service", Field: "other_email"}}),
Parameters: []parameters.Parameter{
parameters.NewStringParameter("country", "some description"),
parameters.NewIntParameterWithAuth("id", "user id", []parameters.ParamAuthService{{Name: "my-google-service", Field: "user_id"}}),
parameters.NewStringParameterWithAuth("email", "user email", []parameters.ParamAuthService{{Name: "my-google-service", Field: "email"}, {Name: "other-google-service", Field: "other_email"}}),
},
},
},
@@ -858,6 +898,7 @@ func TestParseToolFileWithAuth(t *testing.T) {
ToolNames: []string{"example_tool"},
},
},
Prompts: nil,
},
},
}
@@ -877,7 +918,10 @@ func TestParseToolFileWithAuth(t *testing.T) {
t.Fatalf("incorrect tools parse: diff %v", diff)
}
if diff := cmp.Diff(tc.wantToolsFile.Toolsets, toolsFile.Toolsets); diff != "" {
t.Fatalf("incorrect tools parse: diff %v", diff)
t.Fatalf("incorrect toolsets parse: diff %v", diff)
}
if diff := cmp.Diff(tc.wantToolsFile.Prompts, toolsFile.Prompts); diff != "" {
t.Fatalf("incorrect prompts parse: diff %v", diff)
}
})
}
@@ -894,6 +938,8 @@ func TestEnvVarReplacement(t *testing.T) {
t.Setenv("cat_string", "cat")
t.Setenv("food_string", "food")
t.Setenv("TestHeader", "ACTUAL_HEADER")
t.Setenv("prompt_name", "ACTUAL_PROMPT_NAME")
t.Setenv("prompt_content", "ACTUAL_CONTENT")
if err != nil {
t.Fatalf("unexpected error: %s", err)
@@ -967,6 +1013,14 @@ func TestEnvVarReplacement(t *testing.T) {
toolsets:
${toolset_name}:
- example_tool
prompts:
${prompt_name}:
description: A test prompt for {{.name}}.
messages:
- role: user
content: ${prompt_content}
`,
wantToolsFile: ToolsFile{
Sources: server.SourceConfigs{
@@ -1000,9 +1054,9 @@ func TestEnvVarReplacement(t *testing.T) {
Path: "search?name=alice&pet=cat",
Description: "some description",
AuthRequired: []string{"my-google-auth-service", "other-auth-service"},
QueryParams: []tools.Parameter{
tools.NewStringParameterWithAuth("country", "some description",
[]tools.ParamAuthService{{Name: "my-google-auth-service", Field: "user_id"},
QueryParams: []parameters.Parameter{
parameters.NewStringParameterWithAuth("country", "some description",
[]parameters.ParamAuthService{{Name: "my-google-auth-service", Field: "user_id"},
{Name: "other-auth-service", Field: "user_id"}}),
},
RequestBody: `{
@@ -1012,9 +1066,9 @@ func TestEnvVarReplacement(t *testing.T) {
"other": "$OTHER"
}
`,
BodyParams: []tools.Parameter{tools.NewIntParameter("age", "age num"), tools.NewStringParameter("city", "city string")},
BodyParams: []parameters.Parameter{parameters.NewIntParameter("age", "age num"), parameters.NewStringParameter("city", "city string")},
Headers: map[string]string{"Authorization": "API_KEY", "Content-Type": "application/json"},
HeaderParams: []tools.Parameter{tools.NewStringParameter("Language", "language string")},
HeaderParams: []parameters.Parameter{parameters.NewStringParameter("Language", "language string")},
},
},
Toolsets: server.ToolsetConfigs{
@@ -1023,6 +1077,19 @@ func TestEnvVarReplacement(t *testing.T) {
ToolNames: []string{"example_tool"},
},
},
Prompts: server.PromptConfigs{
"ACTUAL_PROMPT_NAME": &custom.Config{
Name: "ACTUAL_PROMPT_NAME",
Description: "A test prompt for {{.name}}.",
Messages: []prompts.Message{
{
Role: "user",
Content: "ACTUAL_CONTENT",
},
},
Arguments: nil,
},
},
},
},
}
@@ -1042,11 +1109,13 @@ func TestEnvVarReplacement(t *testing.T) {
t.Fatalf("incorrect tools parse: diff %v", diff)
}
if diff := cmp.Diff(tc.wantToolsFile.Toolsets, toolsFile.Toolsets); diff != "" {
t.Fatalf("incorrect tools parse: diff %v", diff)
t.Fatalf("incorrect toolsets parse: diff %v", diff)
}
if diff := cmp.Diff(tc.wantToolsFile.Prompts, toolsFile.Prompts); diff != "" {
t.Fatalf("incorrect prompts parse: diff %v", diff)
}
})
}
}
// normalizeFilepaths is a helper function to allow same filepath formats for Mac and Windows.
@@ -1409,7 +1478,7 @@ func TestPrebuiltTools(t *testing.T) {
wantToolset: server.ToolsetConfigs{
"alloydb_postgres_database_tools": tools.ToolsetConfig{
Name: "alloydb_postgres_database_tools",
ToolNames: []string{"execute_sql", "list_tables", "list_active_queries", "list_available_extensions", "list_installed_extensions", "list_autovacuum_configurations", "list_memory_configurations", "list_top_bloated_tables", "list_replication_slots", "list_invalid_indexes", "get_query_plan", "list_views", "list_schemas"},
ToolNames: []string{"execute_sql", "list_tables", "list_active_queries", "list_available_extensions", "list_installed_extensions", "list_autovacuum_configurations", "list_memory_configurations", "list_top_bloated_tables", "list_replication_slots", "list_invalid_indexes", "get_query_plan", "list_views", "list_schemas", "database_overview", "list_triggers", "list_indexes", "list_sequences"},
},
},
},
@@ -1439,7 +1508,7 @@ func TestPrebuiltTools(t *testing.T) {
wantToolset: server.ToolsetConfigs{
"cloud_sql_postgres_database_tools": tools.ToolsetConfig{
Name: "cloud_sql_postgres_database_tools",
ToolNames: []string{"execute_sql", "list_tables", "list_active_queries", "list_available_extensions", "list_installed_extensions", "list_autovacuum_configurations", "list_memory_configurations", "list_top_bloated_tables", "list_replication_slots", "list_invalid_indexes", "get_query_plan", "list_views", "list_schemas"},
ToolNames: []string{"execute_sql", "list_tables", "list_active_queries", "list_available_extensions", "list_installed_extensions", "list_autovacuum_configurations", "list_memory_configurations", "list_top_bloated_tables", "list_replication_slots", "list_invalid_indexes", "get_query_plan", "list_views", "list_schemas", "database_overview", "list_triggers", "list_indexes", "list_sequences"},
},
},
},
@@ -1539,7 +1608,7 @@ func TestPrebuiltTools(t *testing.T) {
wantToolset: server.ToolsetConfigs{
"postgres_database_tools": tools.ToolsetConfig{
Name: "postgres_database_tools",
ToolNames: []string{"execute_sql", "list_tables", "list_active_queries", "list_available_extensions", "list_installed_extensions", "list_autovacuum_configurations", "list_memory_configurations", "list_top_bloated_tables", "list_replication_slots", "list_invalid_indexes", "get_query_plan", "list_views", "list_schemas"},
ToolNames: []string{"execute_sql", "list_tables", "list_active_queries", "list_available_extensions", "list_installed_extensions", "list_autovacuum_configurations", "list_memory_configurations", "list_top_bloated_tables", "list_replication_slots", "list_invalid_indexes", "get_query_plan", "list_views", "list_schemas", "database_overview", "list_triggers", "list_indexes", "list_sequences"},
},
},
},
@@ -1662,6 +1731,10 @@ func TestPrebuiltTools(t *testing.T) {
if diff := cmp.Diff(tc.wantToolset, toolsFile.Toolsets); diff != "" {
t.Fatalf("incorrect tools parse: diff %v", diff)
}
// Prebuilt configs do not have prompts, so assert empty maps.
if len(toolsFile.Prompts) != 0 {
t.Fatalf("expected empty prompts map for prebuilt config, got: %v", toolsFile.Prompts)
}
})
}
}
@@ -1734,3 +1807,88 @@ func TestFileLoadingErrors(t *testing.T) {
}
})
}
func TestMergeToolsFiles(t *testing.T) {
file1 := ToolsFile{
Sources: server.SourceConfigs{"source1": httpsrc.Config{Name: "source1"}},
Tools: server.ToolConfigs{"tool1": http.Config{Name: "tool1"}},
Toolsets: server.ToolsetConfigs{"set1": tools.ToolsetConfig{Name: "set1"}},
}
file2 := ToolsFile{
AuthServices: server.AuthServiceConfigs{"auth1": google.Config{Name: "auth1"}},
Tools: server.ToolConfigs{"tool2": http.Config{Name: "tool2"}},
Toolsets: server.ToolsetConfigs{"set2": tools.ToolsetConfig{Name: "set2"}},
}
fileWithConflicts := ToolsFile{
Sources: server.SourceConfigs{"source1": httpsrc.Config{Name: "source1"}},
Tools: server.ToolConfigs{"tool2": http.Config{Name: "tool2"}},
}
testCases := []struct {
name string
files []ToolsFile
want ToolsFile
wantErr bool
}{
{
name: "merge two distinct files",
files: []ToolsFile{file1, file2},
want: ToolsFile{
Sources: server.SourceConfigs{"source1": httpsrc.Config{Name: "source1"}},
AuthServices: server.AuthServiceConfigs{"auth1": google.Config{Name: "auth1"}},
Tools: server.ToolConfigs{"tool1": http.Config{Name: "tool1"}, "tool2": http.Config{Name: "tool2"}},
Toolsets: server.ToolsetConfigs{"set1": tools.ToolsetConfig{Name: "set1"}, "set2": tools.ToolsetConfig{Name: "set2"}},
Prompts: server.PromptConfigs{},
},
wantErr: false,
},
{
name: "merge with conflicts",
files: []ToolsFile{file1, file2, fileWithConflicts},
wantErr: true,
},
{
name: "merge single file",
files: []ToolsFile{file1},
want: ToolsFile{
Sources: file1.Sources,
AuthServices: make(server.AuthServiceConfigs),
Tools: file1.Tools,
Toolsets: file1.Toolsets,
Prompts: server.PromptConfigs{},
},
},
{
name: "merge empty list",
files: []ToolsFile{},
want: ToolsFile{
Sources: make(server.SourceConfigs),
AuthServices: make(server.AuthServiceConfigs),
Tools: make(server.ToolConfigs),
Toolsets: make(server.ToolsetConfigs),
Prompts: server.PromptConfigs{},
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
got, err := mergeToolsFiles(tc.files...)
if (err != nil) != tc.wantErr {
t.Fatalf("mergeToolsFiles() error = %v, wantErr %v", err, tc.wantErr)
}
if !tc.wantErr {
if diff := cmp.Diff(tc.want, got); diff != "" {
t.Errorf("mergeToolsFiles() mismatch (-want +got):\n%s", diff)
}
} else {
if err == nil {
t.Fatal("expected an error for conflicting files but got none")
}
if !strings.Contains(err.Error(), "resource conflicts detected") {
t.Errorf("expected conflict error, but got: %v", err)
}
}
})
}
}

View File

@@ -1 +1 @@
0.19.1
0.20.0

View File

@@ -234,7 +234,7 @@
},
"outputs": [],
"source": [
"version = \"0.19.1\" # x-release-please-version\n",
"version = \"0.20.0\" # x-release-please-version\n",
"! curl -O https://storage.googleapis.com/genai-toolbox/v{version}/linux/amd64/toolbox\n",
"\n",
"# Make the binary executable\n",

View File

@@ -96,3 +96,21 @@ all_tools = client.load_toolset()
# This will only load the tools listed in 'my_second_toolset'
my_second_toolset = client.load_toolset("my_second_toolset")
```
### Prompts
The `prompts` section of your `tools.yaml` defines the templates containing structured messages and instructions for interacting with language models.
```yaml
prompts:
code_review:
description: "Asks the LLM to analyze code quality and suggest improvements."
messages:
- content: "Please review the following code for quality, correctness, and potential improvements: \n\n{{.code}}"
arguments:
- name: "code"
description: "The code to review"
```
For more details on configuring different types of prompts, see the
[Prompts](../resources/prompts/).

View File

@@ -86,7 +86,7 @@ following instructions for your OS and CPU architecture.
To install Toolbox as a binary on Linux (AMD64):
```sh
# see releases page for other versions
export VERSION=0.19.1
export VERSION=0.20.0
curl -L -o toolbox https://storage.googleapis.com/genai-toolbox/v$VERSION/linux/amd64/toolbox
chmod +x toolbox
```
@@ -95,7 +95,7 @@ chmod +x toolbox
To install Toolbox as a binary on macOS (Apple Silicon):
```sh
# see releases page for other versions
export VERSION=0.19.1
export VERSION=0.20.0
curl -L -o toolbox https://storage.googleapis.com/genai-toolbox/v$VERSION/darwin/arm64/toolbox
chmod +x toolbox
```
@@ -104,7 +104,7 @@ chmod +x toolbox
To install Toolbox as a binary on macOS (Intel):
```sh
# see releases page for other versions
export VERSION=0.19.1
export VERSION=0.20.0
curl -L -o toolbox https://storage.googleapis.com/genai-toolbox/v$VERSION/darwin/amd64/toolbox
chmod +x toolbox
```
@@ -113,7 +113,7 @@ chmod +x toolbox
To install Toolbox as a binary on Windows (AMD64):
```powershell
# see releases page for other versions
$VERSION = "0.19.1"
$VERSION = "0.20.0"
Invoke-WebRequest -Uri "https://storage.googleapis.com/genai-toolbox/v$VERSION/windows/amd64/toolbox.exe" -OutFile "toolbox.exe"
```
{{% /tab %}}
@@ -124,7 +124,7 @@ You can also install Toolbox as a container:
```sh
# see releases page for other versions
export VERSION=0.19.1
export VERSION=0.20.0
docker pull us-central1-docker.pkg.dev/database-toolbox/toolbox/toolbox:$VERSION
```
@@ -143,7 +143,7 @@ To install from source, ensure you have the latest version of
[Go installed](https://go.dev/doc/install), and then run the following command:
```sh
go install github.com/googleapis/genai-toolbox@v0.19.1
go install github.com/googleapis/genai-toolbox@v0.20.0
```
{{% /tab %}}

View File

@@ -105,7 +105,7 @@ In this section, we will download Toolbox, configure our tools in a
<!-- {x-release-please-start-version} -->
```bash
export OS="linux/amd64" # one of linux/amd64, darwin/arm64, darwin/amd64, or windows/amd64
curl -O https://storage.googleapis.com/genai-toolbox/v0.19.1/$OS/toolbox
curl -O https://storage.googleapis.com/genai-toolbox/v0.20.0/$OS/toolbox
```
<!-- {x-release-please-end} -->

View File

@@ -4,8 +4,8 @@ go 1.24.4
require (
github.com/googleapis/mcp-toolbox-sdk-go v0.4.0
google.golang.org/adk v0.0.0-20251105212711-ccd61aa4a1b6
google.golang.org/genai v1.34.0
google.golang.org/adk v0.1.0
google.golang.org/genai v1.35.0
)
require (

View File

@@ -104,12 +104,12 @@ golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/adk v0.0.0-20251105212711-ccd61aa4a1b6 h1:LiCwUK/a39m3ZJYOBfJX0WAZLaHZjgU0DsJJsekPxLU=
google.golang.org/adk v0.0.0-20251105212711-ccd61aa4a1b6/go.mod h1:NvtSLoNx7UzZIiUAI1KoJQLMmt9sG3oCgiCx1TLqKFw=
google.golang.org/adk v0.1.0 h1:+w/fHuqRVolotOATlujRA+2DKUuDrFH2poRdEX2QjB8=
google.golang.org/adk v0.1.0/go.mod h1:NvtSLoNx7UzZIiUAI1KoJQLMmt9sG3oCgiCx1TLqKFw=
google.golang.org/api v0.255.0 h1:OaF+IbRwOottVCYV2wZan7KUq7UeNUQn1BcPc4K7lE4=
google.golang.org/api v0.255.0/go.mod h1:d1/EtvCLdtiWEV4rAEHDHGh2bCnqsWhw+M8y2ECN4a8=
google.golang.org/genai v1.34.0 h1:lPRJRO+HqRX1SwFo1Xb/22nZ5MBEPUbXDl61OoDxlbY=
google.golang.org/genai v1.34.0/go.mod h1:7pAilaICJlQBonjKKJNhftDFv3SREhZcTe9F6nRcjbg=
google.golang.org/genai v1.35.0 h1:Jo6g25CzVqFzGrX5mhWyBgQqXAUzxcx5jeK7U74zv9c=
google.golang.org/genai v1.35.0/go.mod h1:A3kkl0nyBjyFlNjgxIwKq70julKbIxpSxqKO5gw/gmk=
google.golang.org/genproto v0.0.0-20251014184007-4626949a642f h1:vLd1CJuJOUgV6qijD7KT5Y2ZtC97ll4dxjTUappMnbo=
google.golang.org/genproto v0.0.0-20251014184007-4626949a642f/go.mod h1:PI3KrSadr00yqfv6UDvgZGFsmLqeRIwt8x4p5Oo7CdM=
google.golang.org/genproto/googleapis/api v0.0.0-20251014184007-4626949a642f h1:OiFuztEyBivVKDvguQJYWq1yDcfAHIID/FVrPR4oiI0=

View File

@@ -4,7 +4,7 @@ go 1.24.6
require (
github.com/googleapis/mcp-toolbox-sdk-go v0.4.0
google.golang.org/genai v1.34.0
google.golang.org/genai v1.35.0
)
require (

View File

@@ -102,8 +102,8 @@ gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/api v0.255.0 h1:OaF+IbRwOottVCYV2wZan7KUq7UeNUQn1BcPc4K7lE4=
google.golang.org/api v0.255.0/go.mod h1:d1/EtvCLdtiWEV4rAEHDHGh2bCnqsWhw+M8y2ECN4a8=
google.golang.org/genai v1.34.0 h1:lPRJRO+HqRX1SwFo1Xb/22nZ5MBEPUbXDl61OoDxlbY=
google.golang.org/genai v1.34.0/go.mod h1:7pAilaICJlQBonjKKJNhftDFv3SREhZcTe9F6nRcjbg=
google.golang.org/genai v1.35.0 h1:Jo6g25CzVqFzGrX5mhWyBgQqXAUzxcx5jeK7U74zv9c=
google.golang.org/genai v1.35.0/go.mod h1:A3kkl0nyBjyFlNjgxIwKq70julKbIxpSxqKO5gw/gmk=
google.golang.org/genproto v0.0.0-20251014184007-4626949a642f h1:vLd1CJuJOUgV6qijD7KT5Y2ZtC97ll4dxjTUappMnbo=
google.golang.org/genproto v0.0.0-20251014184007-4626949a642f/go.mod h1:PI3KrSadr00yqfv6UDvgZGFsmLqeRIwt8x4p5Oo7CdM=
google.golang.org/genproto/googleapis/api v0.0.0-20251014184007-4626949a642f h1:OiFuztEyBivVKDvguQJYWq1yDcfAHIID/FVrPR4oiI0=

View File

@@ -1,3 +1,3 @@
google-adk==1.18.0
toolbox-core==0.5.2
pytest==8.4.2
pytest==9.0.1

View File

@@ -1,3 +1,3 @@
google-genai==1.47.0
toolbox-core==0.5.2
pytest==8.4.2
pytest==9.0.1

View File

@@ -2,4 +2,4 @@ langchain==0.3.27
langchain-google-vertexai==2.1.2
langgraph==1.0.1
toolbox-langchain==0.5.2
pytest==8.4.2
pytest==9.0.1

View File

@@ -1,4 +1,4 @@
llama-index==0.14.6
llama-index-llms-google-genai==0.7.1
llama-index-llms-google-genai==0.7.3
toolbox-llamaindex==0.5.2
pytest==8.4.2
pytest==9.0.1

View File

@@ -13,7 +13,7 @@ In this section, we will download Toolbox, configure our tools in a
<!-- {x-release-please-start-version} -->
```bash
export OS="linux/amd64" # one of linux/amd64, darwin/arm64, darwin/amd64, or windows/amd64
curl -O https://storage.googleapis.com/genai-toolbox/v0.19.1/$OS/toolbox
curl -O https://storage.googleapis.com/genai-toolbox/v0.20.0/$OS/toolbox
```
<!-- {x-release-please-end} -->

View File

@@ -8,7 +8,7 @@ description: >
<html>
<head>
<link rel="canonical" href="https://cloud.google.com/alloydb/docs/quickstart/create-and-connect"/>
<meta http-equiv="refresh" content="0;url=https://cloud.google.com/alloydb/docs/quickstart/create-and-connect"/>
<link rel="canonical" href="https://cloud.google.com/alloydb/docs/connect-ide-using-mcp-toolbox"/>
<meta http-equiv="refresh" content="0;url=https://cloud.google.com/alloydb/docs/connect-ide-using-mcp-toolbox"/>
</head>
</html>

View File

@@ -48,19 +48,19 @@ to expose your developer assistant tools to a Looker instance:
<!-- {x-release-please-start-version} -->
{{< tabpane persist=header >}}
{{< tab header="linux/amd64" lang="bash" >}}
curl -O https://storage.googleapis.com/genai-toolbox/v0.19.1/linux/amd64/toolbox
curl -O https://storage.googleapis.com/genai-toolbox/v0.20.0/linux/amd64/toolbox
{{< /tab >}}
{{< tab header="darwin/arm64" lang="bash" >}}
curl -O https://storage.googleapis.com/genai-toolbox/v0.19.1/darwin/arm64/toolbox
curl -O https://storage.googleapis.com/genai-toolbox/v0.20.0/darwin/arm64/toolbox
{{< /tab >}}
{{< tab header="darwin/amd64" lang="bash" >}}
curl -O https://storage.googleapis.com/genai-toolbox/v0.19.1/darwin/amd64/toolbox
curl -O https://storage.googleapis.com/genai-toolbox/v0.20.0/darwin/amd64/toolbox
{{< /tab >}}
{{< tab header="windows/amd64" lang="bash" >}}
curl -O https://storage.googleapis.com/genai-toolbox/v0.19.1/windows/amd64/toolbox.exe
curl -O https://storage.googleapis.com/genai-toolbox/v0.20.0/windows/amd64/toolbox.exe
{{< /tab >}}
{{< /tabpane >}}
<!-- {x-release-please-end} -->

View File

@@ -45,19 +45,19 @@ instance:
<!-- {x-release-please-start-version} -->
{{< tabpane persist=header >}}
{{< tab header="linux/amd64" lang="bash" >}}
curl -O https://storage.googleapis.com/genai-toolbox/v0.19.1/linux/amd64/toolbox
curl -O https://storage.googleapis.com/genai-toolbox/v0.20.0/linux/amd64/toolbox
{{< /tab >}}
{{< tab header="darwin/arm64" lang="bash" >}}
curl -O https://storage.googleapis.com/genai-toolbox/v0.19.1/darwin/arm64/toolbox
curl -O https://storage.googleapis.com/genai-toolbox/v0.20.0/darwin/arm64/toolbox
{{< /tab >}}
{{< tab header="darwin/amd64" lang="bash" >}}
curl -O https://storage.googleapis.com/genai-toolbox/v0.19.1/darwin/amd64/toolbox
curl -O https://storage.googleapis.com/genai-toolbox/v0.20.0/darwin/amd64/toolbox
{{< /tab >}}
{{< tab header="windows/amd64" lang="bash" >}}
curl -O https://storage.googleapis.com/genai-toolbox/v0.19.1/windows/amd64/toolbox.exe
curl -O https://storage.googleapis.com/genai-toolbox/v0.20.0/windows/amd64/toolbox.exe
{{< /tab >}}
{{< /tabpane >}}
<!-- {x-release-please-end} -->

View File

@@ -43,19 +43,19 @@ expose your developer assistant tools to a MySQL instance:
<!-- {x-release-please-start-version} -->
{{< tabpane persist=header >}}
{{< tab header="linux/amd64" lang="bash" >}}
curl -O https://storage.googleapis.com/genai-toolbox/v0.19.1/linux/amd64/toolbox
curl -O https://storage.googleapis.com/genai-toolbox/v0.20.0/linux/amd64/toolbox
{{< /tab >}}
{{< tab header="darwin/arm64" lang="bash" >}}
curl -O https://storage.googleapis.com/genai-toolbox/v0.19.1/darwin/arm64/toolbox
curl -O https://storage.googleapis.com/genai-toolbox/v0.20.0/darwin/arm64/toolbox
{{< /tab >}}
{{< tab header="darwin/amd64" lang="bash" >}}
curl -O https://storage.googleapis.com/genai-toolbox/v0.19.1/darwin/amd64/toolbox
curl -O https://storage.googleapis.com/genai-toolbox/v0.20.0/darwin/amd64/toolbox
{{< /tab >}}
{{< tab header="windows/amd64" lang="bash" >}}
curl -O https://storage.googleapis.com/genai-toolbox/v0.19.1/windows/amd64/toolbox.exe
curl -O https://storage.googleapis.com/genai-toolbox/v0.20.0/windows/amd64/toolbox.exe
{{< /tab >}}
{{< /tabpane >}}
<!-- {x-release-please-end} -->

View File

@@ -44,19 +44,19 @@ expose your developer assistant tools to a Neo4j instance:
<!-- {x-release-please-start-version} -->
{{< tabpane persist=header >}}
{{< tab header="linux/amd64" lang="bash" >}}
curl -O https://storage.googleapis.com/genai-toolbox/v0.19.1/linux/amd64/toolbox
curl -O https://storage.googleapis.com/genai-toolbox/v0.20.0/linux/amd64/toolbox
{{< /tab >}}
{{< tab header="darwin/arm64" lang="bash" >}}
curl -O https://storage.googleapis.com/genai-toolbox/v0.19.1/darwin/arm64/toolbox
curl -O https://storage.googleapis.com/genai-toolbox/v0.20.0/darwin/arm64/toolbox
{{< /tab >}}
{{< tab header="darwin/amd64" lang="bash" >}}
curl -O https://storage.googleapis.com/genai-toolbox/v0.19.1/darwin/amd64/toolbox
curl -O https://storage.googleapis.com/genai-toolbox/v0.20.0/darwin/amd64/toolbox
{{< /tab >}}
{{< tab header="windows/amd64" lang="bash" >}}
curl -O https://storage.googleapis.com/genai-toolbox/v0.19.1/windows/amd64/toolbox.exe
curl -O https://storage.googleapis.com/genai-toolbox/v0.20.0/windows/amd64/toolbox.exe
{{< /tab >}}
{{< /tabpane >}}
<!-- {x-release-please-end} -->

View File

@@ -56,19 +56,19 @@ Omni](https://cloud.google.com/alloydb/omni/current/docs/overview).
<!-- {x-release-please-start-version} -->
{{< tabpane persist=header >}}
{{< tab header="linux/amd64" lang="bash" >}}
curl -O https://storage.googleapis.com/genai-toolbox/v0.19.1/linux/amd64/toolbox
curl -O https://storage.googleapis.com/genai-toolbox/v0.20.0/linux/amd64/toolbox
{{< /tab >}}
{{< tab header="darwin/arm64" lang="bash" >}}
curl -O https://storage.googleapis.com/genai-toolbox/v0.19.1/darwin/arm64/toolbox
curl -O https://storage.googleapis.com/genai-toolbox/v0.20.0/darwin/arm64/toolbox
{{< /tab >}}
{{< tab header="darwin/amd64" lang="bash" >}}
curl -O https://storage.googleapis.com/genai-toolbox/v0.19.1/darwin/amd64/toolbox
curl -O https://storage.googleapis.com/genai-toolbox/v0.20.0/darwin/amd64/toolbox
{{< /tab >}}
{{< tab header="windows/amd64" lang="bash" >}}
curl -O https://storage.googleapis.com/genai-toolbox/v0.19.1/windows/amd64/toolbox.exe
curl -O https://storage.googleapis.com/genai-toolbox/v0.20.0/windows/amd64/toolbox.exe
{{< /tab >}}
{{< /tabpane >}}
<!-- {x-release-please-end} -->

View File

@@ -43,19 +43,19 @@ to expose your developer assistant tools to a SQLite instance:
<!-- {x-release-please-start-version} -->
{{< tabpane persist=header >}}
{{< tab header="linux/amd64" lang="bash" >}}
curl -O https://storage.googleapis.com/genai-toolbox/v0.19.1/linux/amd64/toolbox
curl -O https://storage.googleapis.com/genai-toolbox/v0.20.0/linux/amd64/toolbox
{{< /tab >}}
{{< tab header="darwin/arm64" lang="bash" >}}
curl -O https://storage.googleapis.com/genai-toolbox/v0.19.1/darwin/arm64/toolbox
curl -O https://storage.googleapis.com/genai-toolbox/v0.20.0/darwin/arm64/toolbox
{{< /tab >}}
{{< tab header="darwin/amd64" lang="bash" >}}
curl -O https://storage.googleapis.com/genai-toolbox/v0.19.1/darwin/amd64/toolbox
curl -O https://storage.googleapis.com/genai-toolbox/v0.20.0/darwin/amd64/toolbox
{{< /tab >}}
{{< tab header="windows/amd64" lang="bash" >}}
curl -O https://storage.googleapis.com/genai-toolbox/v0.19.1/windows/amd64/toolbox.exe
curl -O https://storage.googleapis.com/genai-toolbox/v0.20.0/windows/amd64/toolbox.exe
{{< /tab >}}
{{< /tabpane >}}
<!-- {x-release-please-end} -->

View File

@@ -46,6 +46,10 @@ details on how to connect your AI tools (IDEs) to databases via Toolbox and MCP.
* `list_views`: Lists views in the database from pg_views with a default
limit of 50 rows. Returns schemaname, viewname and the ownername.
* `list_schemas`: Lists schemas in the database.
* `database_overview`: Fetches the current state of the PostgreSQL server.
* `list_triggers`: Lists triggers in the database.
* `list_indexes`: List available user indexes in a PostgreSQL database.
* `list_sequences`: List sequences in a PostgreSQL database.
## AlloyDB Postgres Admin
@@ -216,6 +220,10 @@ details on how to connect your AI tools (IDEs) to databases via Toolbox and MCP.
* `list_views`: Lists views in the database from pg_views with a default
limit of 50 rows. Returns schemaname, viewname and the ownername.
* `list_schemas`: Lists schemas in the database.
* `database_overview`: Fetches the current state of the PostgreSQL server.
* `list_triggers`: Lists triggers in the database.
* `list_indexes`: List available user indexes in a PostgreSQL database.
* `list_sequences`: List sequences in a PostgreSQL database.
## Cloud SQL for PostgreSQL Observability
@@ -489,8 +497,8 @@ details on how to connect your AI tools (IDEs) to databases via Toolbox and MCP.
* `--prebuilt` value: `postgres`
* **Environment Variables:**
* `POSTGRES_HOST`: The hostname or IP address of the PostgreSQL server.
* `POSTGRES_PORT`: The port number for the PostgreSQL server.
* `POSTGRES_HOST`: (Optional) The hostname or IP address of the PostgreSQL server.
* `POSTGRES_PORT`: (Optional) The port number for the PostgreSQL server.
* `POSTGRES_DATABASE`: The name of the database to connect to.
* `POSTGRES_USER`: The database username.
* `POSTGRES_PASSWORD`: The password for the database user.
@@ -513,6 +521,10 @@ details on how to connect your AI tools (IDEs) to databases via Toolbox and MCP.
* `list_views`: Lists views in the database from pg_views with a default
limit of 50 rows. Returns schemaname, viewname and the ownername.
* `list_schemas`: Lists schemas in the database.
* `database_overview`: Fetches the current state of the PostgreSQL server.
* `list_triggers`: Lists triggers in the database.
* `list_indexes`: List available user indexes in a PostgreSQL database.
* `list_sequences`: List sequences in a PostgreSQL database.
## Google Cloud Serverless for Apache Spark

View File

@@ -0,0 +1,70 @@
---
title: "Prompts"
type: docs
weight: 3
description: >
Prompts allow servers to provide structured messages and instructions for interacting with language models.
---
A `prompt` represents a reusable prompt template that can be retrieved and used
by MCP clients.
A Prompt is essentially a template for a message or a series of messages that can be sent to a Large Language Model (LLM). The Toolbox server implements the `prompts/list` and `prompts/get` methods from the [Model Context Protocol (MCP)](https://modelcontextprotocol.io/docs/getting-started/intro) specification, allowing clients to discover and retrieve these prompts.
```yaml
prompts:
code_review:
description: "Asks the LLM to analyze code quality and suggest improvements."
messages:
- content: "Please review the following code for quality, correctness, and potential improvements: \n\n{{.code}}"
arguments:
- name: "code"
description: "The code to review"
```
## Prompt Schema
| **field** | **type** | **required** | **description** |
| --- | --- | --- | --- |
| description | string | No | A brief explanation of what the prompt does. |
| kind | string | No | The kind of prompt. Defaults to `"custom"`. |
| messages | [][Message](#message-schema) | Yes | A list of one or more message objects that make up the prompt's content. |
| arguments | [][Argument](#argument-schema) | No | A list of arguments that can be interpolated into the prompt's content.|
## Message Schema
| **field** | **type** | **required** | **description** |
| --- | --- | --- | --- |
| role | string | No | The role of the sender. Can be `"user"` or `"assistant"`. Defaults to `"user"`. |
| content | string | Yes | The text of the message. You can include placeholders for arguments using `{{.argument_name}}` syntax. |
## Argument Schema
An argument can be any [Parameter](../tools/_index.md#specifying-parameters)
type. If the `type` field is not specified, it will default to `string`.
## Usage with Gemini CLI
Prompts defined in your `tools.yaml` can be seamlessly integrated with the Gemini CLI to create [custom slash commands](https://github.com/google-gemini/gemini-cli/blob/main/docs/tools/mcp-server.md#mcp-prompts-as-slash-commands). The workflow is as follows:
1. **Discovery:** When the Gemini CLI connects to your Toolbox server, it automatically calls `prompts/list` to discover all available prompts.
2. **Conversion:** Each discovered prompt is converted into a corresponding slash command. For example, a prompt named `code_review` becomes the `/code_review` command in the CLI.
3. **Execution:** You can execute the command as follows:
```bash
/code_review --code="def hello():\n print('world')"
```
4. **Interpolation:** Once all arguments are collected, the CLI calls prompts/get
with your provided values to retrieve the final, interpolated prompt.
Eg.
```bash
Please review the following code for quality, correctness, and potential improvements: \ndef hello():\n print('world')
```
5. **Response:** This completed prompt is then sent to the Gemini model, and the model's response is displayed back to you in the CLI.
## Kinds of prompts

View File

@@ -0,0 +1,69 @@
---
title: "Custom"
type: docs
weight: 1
description: >
Custom prompts defined by the user.
---
Custom prompts are defined by the user to be exposed through their MCP server.
They are the default type for prompts.
## Examples
### Basic Prompt
Here is an example of a simple prompt that takes a single argument, code, and
asks an LLM to review it.
```yaml
prompts:
code_review:
description: "Asks the LLM to analyze code quality and suggest improvements."
messages:
- content: "Please review the following code for quality, correctness, and potential improvements: \n\n{{.code}}"
arguments:
- name: "code"
description: "The code to review"
```
### Multi-message prompt
You can define prompts with multiple messages to set up more complex
conversational contexts, like a role-playing scenario.
```yaml
prompts:
roleplay_scenario:
description: "Sets up a roleplaying scenario with initial messages."
arguments:
- name: "character"
description: "The character the AI should embody."
- name: "situation"
description: "The initial situation for the roleplay."
messages:
- role: "user"
content: "Let's roleplay. You are {{.character}}. The situation is: {{.situation}}"
- role: "assistant"
content: "Okay, I understand. I am ready. What happens next?"
```
## Reference
### Prompt Schema
| **field** | **type** | **required** | **description** |
| --- | --- | --- | --- |
| kind | string | No | The kind of prompt. Must be `"custom"`. |
| description | string | No | A brief explanation of what the prompt does. |
| messages | [][Message](#message-schema) | Yes | A list of one or more message objects that make up the prompt's content. |
| arguments | [][Argument](#argument-schema) | No | A list of arguments that can be interpolated into the prompt's content.|
### Message Schema
Refer to the default prompt [Message Schema](../_index.md#message-schema).
### Argument Schema
Refer to the default prompt [Argument Schema](../_index.md#argument-schema).

View File

@@ -1,12 +1,10 @@
---
title: "AlloyDB Admin"
linkTitle: "AlloyDB Admin"
title: AlloyDB Admin
linkTitle: AlloyDB Admin
type: docs
weight: 2
description: >
The "alloydb-admin" source provides a client for the AlloyDB API.
aliases:
- /resources/sources/alloydb-admin
weight: 1
description: "The \"alloydb-admin\" source provides a client for the AlloyDB API.\n"
aliases: [/resources/sources/alloydb-admin]
---
## About
@@ -17,6 +15,7 @@ tools to perform administrative tasks on AlloyDB resources, such as managing
clusters, instances, and users.
Authentication can be handled in two ways:
1. **Application Default Credentials (ADC):** By default, the source uses ADC
to authenticate with the API.
2. **Client-side OAuth:** If `useClientOAuth` is set to `true`, the source will
@@ -36,7 +35,9 @@ sources:
```
## Reference
| **field** | **type** | **required** | **description** |
|----------------|:--------:|:------------:|------------------------------------------------------------------------------------------------------------------------------------------------|
| -------------- | :------: | :----------: | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| kind | string | true | Must be "alloydb-admin". |
| useClientOAuth | boolean | false | If true, the source will use client-side OAuth for authorization. Otherwise, it will use Application Default Credentials. Defaults to `false`. |
| defaultProject | string | false | The Google Cloud project ID to use for AlloyDB infrastructure tools. |
| useClientOAuth | boolean | false | If true, the source will use client-side OAuth for authorization. Otherwise, it will use Application Default Credentials. Defaults to `false`. |

View File

@@ -51,6 +51,18 @@ cluster][alloydb-free-trial].
- [`postgres-list-schemas`](../tools/postgres/postgres-list-schemas.md)
List schemas in an AlloyDB for PostgreSQL database.
- [`postgres-database-overview`](../tools/postgres/postgres-database-overview.md)
Fetches the current state of the PostgreSQL server.
- [`postgres-list-triggers`](../tools/postgres/postgres-list-triggers.md)
List triggers in an AlloyDB for PostgreSQL database.
- [`postgres-list-indexes`](../tools/postgres/postgres-list-indexes.md)
List available user indexes in a PostgreSQL database.
- [`postgres-list-sequences`](../tools/postgres/postgres-list-sequences.md)
List sequences in a PostgreSQL database.
### Pre-built Configurations
- [AlloyDB using MCP](https://googleapis.github.io/genai-toolbox/how-to/connect-ide/alloydb_pg_mcp/)

View File

@@ -1,11 +1,9 @@
---
title: "Cloud SQL Admin"
title: Cloud SQL Admin
type: docs
weight: 1
description: >
A "cloud-sql-admin" source provides a client for the Cloud SQL Admin API.
aliases:
- /resources/sources/cloud-sql-admin
description: "A \"cloud-sql-admin\" source provides a client for the Cloud SQL Admin API.\n"
aliases: [/resources/sources/cloud-sql-admin]
---
## About
@@ -16,6 +14,7 @@ allows tools to perform administrative tasks on Cloud SQL instances, such as
creating users and databases.
Authentication can be handled in two ways:
1. **Application Default Credentials (ADC):** By default, the source uses ADC
to authenticate with the API.
2. **Client-side OAuth:** If `useClientOAuth` is set to `true`, the source will
@@ -37,6 +36,7 @@ sources:
## Reference
| **field** | **type** | **required** | **description** |
|----------------|:--------:|:------------:|------------------------------------------------------------------------------------------------------------------------------------------------|
| -------------- | :------: | :----------: | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| kind | string | true | Must be "cloud-sql-admin". |
| useClientOAuth | boolean | false | If true, the source will use client-side OAuth for authorization. Otherwise, it will use Application Default Credentials. Defaults to `false`. |
| defaultProject | string | false | The Google Cloud project ID to use for Cloud SQL infrastructure tools. |
| useClientOAuth | boolean | false | If true, the source will use client-side OAuth for authorization. Otherwise, it will use Application Default Credentials. Defaults to `false`. |

View File

@@ -47,6 +47,18 @@ to a database by following these instructions][csql-pg-quickstart].
- [`postgres-list-schemas`](../tools/postgres/postgres-list-schemas.md)
List schemas in a PostgreSQL database.
- [`postgres-database-overview`](../tools/postgres/postgres-database-overview.md)
Fetches the current state of the PostgreSQL server.
- [`postgres-list-triggers`](../tools/postgres/postgres-list-triggers.md)
List triggers in a PostgreSQL database.
- [`postgres-list-indexes`](../tools/postgres/postgres-list-indexes.md)
List available user indexes in a PostgreSQL database.
- [`postgres-list-sequences`](../tools/postgres/postgres-list-sequences.md)
List sequences in a PostgreSQL database.
### Pre-built Configurations
- [Cloud SQL for Postgres using

View File

@@ -41,6 +41,18 @@ reputation for reliability, feature robustness, and performance.
- [`postgres-list-schemas`](../tools/postgres/postgres-list-views.md)
List schemas in a PostgreSQL database.
- [`postgres-database-overview`](../tools/postgres/postgres-database-overview.md)
Fetches the current state of the PostgreSQL server.
- [`postgres-list-triggers`](../tools/postgres/postgres-list-triggers.md)
List triggers in a PostgreSQL database.
- [`postgres-list-indexes`](../tools/postgres/postgres-list-indexes.md)
List available user indexes in a PostgreSQL database.
- [`postgres-list-sequences`](../tools/postgres/postgres-list-sequences.md)
List sequences in a PostgreSQL database.
### Pre-built Configurations
- [PostgreSQL using MCP](https://googleapis.github.io/genai-toolbox/how-to/connect-ide/postgres_mcp/)

View File

@@ -0,0 +1,47 @@
---
title: "looker-generate-embed-url"
type: docs
weight: 1
description: >
"looker-generate-embed-url" generates an embeddable URL for Looker content.
aliases:
- /resources/tools/looker-generate-embed-url
---
## About
The `looker-generate-embed-url` tool generates an embeddable URL for a given piece of Looker content. The url generated is created for the user authenticated to the Looker source. When opened in the browser it will create a Looker Embed session.
It's compatible with the following sources:
- [looker](../../sources/looker.md)
`looker-generate-embed-url` takes two parameters:
1. the `type` of content (e.g., "dashboards", "looks", "query-visualization")
2. the `id` of the content
It's recommended to use other tools from the Looker MCP toolbox with this tool to do things like fetch dashboard id's, generate a query, etc that can be supplied to this tool.
## Example
```yaml
tools:
generate_embed_url:
kind: looker-generate-embed-url
source: looker-source
description: |
generate_embed_url Tool
This tool generates an embeddable URL for Looker content.
You need to provide the type of content (e.g., 'dashboards', 'looks', 'query-visualization')
and the ID of the content.
```
## Reference
| **field** | **type** | **required** | **description** |
|-------------|:--------:|:------------:|----------------------------------------------------|
| kind | string | true | Must be "looker-generate-embed-url" |
| source | string | true | Name of the source the SQL should execute on. |
| description | string | true | Description of the tool that is passed to the LLM. |

View File

@@ -0,0 +1,51 @@
---
title: "postgres-database-overview"
type: docs
weight: 1
description: >
The "postgres-database-overview" fetches the current state of the PostgreSQL server.
aliases:
- /resources/tools/postgres-database-overview
---
## About
The `postgres-database-overview` fetches the current state of the PostgreSQL server. It's compatible with any of the following sources:
- [alloydb-postgres](../../sources/alloydb-pg.md)
- [cloud-sql-postgres](../../sources/cloud-sql-pg.md)
- [postgres](../../sources/postgres.md)
`postgres-database-overview` fetches the current state of the PostgreSQL server This tool does not take any input parameters.
## Example
```yaml
tools:
database_overview:
kind: postgres-database-overview
source: cloudsql-pg-source
description: |
fetches the current state of the PostgreSQL server. It returns the postgres version, whether it's a replica, uptime duration, maximum connection limit, number of current connections, number of active connections and the percentage of connections in use.
```
The response is a JSON object with the following elements:
```json
{
"pg_version": "PostgreSQL server version string",
"is_replica": "boolean indicating if the instance is in recovery mode",
"uptime": "interval string representing the total server uptime",
"max_connections": "integer maximum number of allowed connections",
"current_connections": "integer number of current connections",
"active_connections": "integer number of currently active connections",
"pct_connections_used": "float percentage of max_connections currently in use"
}
```
## Reference
| **field** | **type** | **required** | **description** |
|-------------|:--------:|:-------------:|------------------------------------------------------|
| kind | string | true | Must be "postgres-database-overview". |
| source | string | true | Name of the source the SQL should execute on. |
| description | string | false | Description of the tool that is passed to the agent. |

View File

@@ -0,0 +1,67 @@
---
title: "postgres-list-indexes"
type: docs
weight: 1
description: >
The "postgres-list-indexes" tool lists indexes in a Postgres database.
aliases:
- /resources/tools/postgres-list-indexes
---
## About
The `postgres-list-indexes` tool lists available user indexes in the database excluding those in `pg_catalog` and `information_schema`. It's compatible with any of the following sources:
- [alloydb-postgres](../../sources/alloydb-pg.md)
- [cloud-sql-postgres](../../sources/cloud-sql-pg.md)
- [postgres](../../sources/postgres.md)
`postgres-list-indexes` lists detailed information as JSON for indexes. The tool takes the following input parameters:
- `table_name` (optional): A text to filter results by table name. The input is used within a LIKE clause. Default: `""`
- `index_name` (optional): A text to filter results by index name. The input is used within a LIKE clause. Default: `""`
- `schema_name` (optional): A text to filter results by schema name. The input is used within a LIKE clause. Default: `""`
- `limit` (optional): The maximum number of rows to return. Default: `50`.
## Example
```yaml
tools:
list_indexes:
kind: postgres-list-indexes
source: postgres-source
description: |
Lists available user indexes in the database, excluding system schemas (pg_catalog,
information_schema). For each index, the following properties are returned:
schema name, table name, index name, index type (access method), a boolean
indicating if it's a unique index, a boolean indicating if it's for a primary key,
the index definition, index size in bytes, the number of index scans, the number of
index tuples read, the number of table tuples fetched via index scans, and a boolean
indicating if the index has been used at least once.
```
The response is a json array with the following elements:
```json
{
"schema_name": "schema name",
"table_name": "table name",
"index_name": "index name",
"index_type": "index access method (e.g btree, hash, gin)",
"is_unique": "boolean indicating if the index is unique",
"is_primary": "boolean indicating if the index is for a primary key",
"index_definition": "index definition statement",
"index_size_bytes": "index size in bytes",
"index_scans": "Number of index scans initiated on this index",
"tuples_read": "Number of index entries returned by scans on this index",
"tuples_fetched": "Number of live table rows fetched by simple index scans using this index",
"is_used": "boolean indicating if the index has been scanned at least once"
}
```
## Reference
| **field** | **type** | **required** | **description** |
|-------------|:--------:|:-------------:|------------------------------------------------------|
| kind | string | true | Must be "postgres-list-indexes". |
| source | string | true | Name of the source the SQL should execute on. |
| description | string | false | Description of the tool that is passed to the agent. |

View File

@@ -0,0 +1,61 @@
---
title: "postgres-list-sequences"
type: docs
weight: 1
description: >
The "postgres-list-sequences" tool lists sequences in a Postgres database.
aliases:
- /resources/tools/postgres-list-sequences
---
## About
The `postgres-list-sequences` tool retrieves information about sequences in a Postgres database. It's compatible with any of the following sources:
- [alloydb-postgres](../../sources/alloydb-pg.md)
- [cloud-sql-postgres](../../sources/cloud-sql-pg.md)
- [postgres](../../sources/postgres.md)
`postgres-list-sequences` lists detailed information as JSON for all sequences. The tool takes the following input parameters:
- `sequencename` (optional): A text to filter results by sequence name. The input is used within a LIKE clause. Default: `""`
- `schemaname` (optional): A text to filter results by schema name. The input is used within a LIKE clause. Default: `""`
- `limit` (optional): The maximum number of rows to return. Default: `50`.
## Example
```yaml
tools:
list_indexes:
kind: postgres-list-sequences
source: postgres-source
description: |
Lists all the sequences in the database ordered by sequence name.
Returns sequence name, schema name, sequence owner, data type of the
sequence, starting value, minimum value, maximum value of the sequence,
the value by which the sequence is incremented, and the last value
generated by generated by the sequence in the current session.
```
The response is a json array with the following elements:
```json
{
"sequencename": "sequence name",
"schemaname": "schema name",
"sequenceowner": "owner of the sequence",
"data_type": "data type of the sequence",
"start_value": "starting value of the sequence",
"min_value": "minimum value of the sequence",
"max_value": "maximum value of the sequence",
"increment_by": "increment value of the sequence",
"last_value": "last value of the sequence"
}
```
## Reference
| **field** | **type** | **required** | **description** |
|-------------|:--------:|:-------------:|------------------------------------------------------|
| kind | string | true | Must be "postgres-list-sequences". |
| source | string | true | Name of the source the SQL should execute on. |
| description | string | false | Description of the tool that is passed to the agent. |

View File

@@ -0,0 +1,60 @@
---
title: "postgres-list-triggers"
type: docs
weight: 1
description: >
The "postgres-list-triggers" tool lists triggers in a Postgres database.
aliases:
- /resources/tools/postgres-list-triggers
---
## About
The `postgres-list-triggers` tool lists available non-internal triggers in the database. It's compatible with any of the following sources:
- [alloydb-postgres](../../sources/alloydb-pg.md)
- [cloud-sql-postgres](../../sources/cloud-sql-pg.md)
- [postgres](../../sources/postgres.md)
`postgres-list-triggers` lists detailed information as JSON for triggers. The tool takes the following input parameters:
- `trigger_name` (optional): A text to filter results by trigger name. The input is used within a LIKE clause. Default: `""`
- `schema_name` (optional): A text to filter results by schema name. The input is used within a LIKE clause. Default: `""`
- `table_name` (optional): A text to filter results by table name. The input is used within a LIKE clause. Default: `""`
- `limit` (optional): The maximum number of triggers to return. Default: `50`
## Example
```yaml
```yaml
tools:
list_triggers:
kind: postgres-list-triggers
source: postgres-source
description: |
Lists all non-internal triggers in a database. Returns trigger name, schema name, table name, wether its enabled or disabled, timing (e.g BEFORE/AFTER of the event), the events that cause the trigger to fire such as INSERT, UPDATE, or DELETE, whether the trigger activates per ROW or per STATEMENT, the handler function executed by the trigger and full definition.
```
The response is a json array with the following elements:
```json
{
"trigger_name": "trigger name",
"schema_name": "schema name",
"table_name": "table name",
"status": "Whether the trigger is currently active (ENABLED, DISABLED, REPLICA, ALWAYS).",
"timing": "When it runs relative to the event (BEFORE, AFTER, INSTEAD OF).",
"events": "The specific operations that fire it (INSERT, UPDATE, DELETE, TRUNCATE)",
"activation_level": "Granularity of execution (ROW vs STATEMENT).",
"function_name": "The function it executes",
"definition": "Full SQL definition of the trigger"
}
```
## Reference
| **field** | **type** | **required** | **description** |
|-------------|:--------:|:-------------:|------------------------------------------------------|
| kind | string | true | Must be "postgres-list-triggers". |
| source | string | true | Name of the source the SQL should execute on. |
| description | string | false | Description of the tool that is passed to the agent. |

View File

@@ -771,7 +771,7 @@
},
"outputs": [],
"source": [
"version = \"0.19.1\" # x-release-please-version\n",
"version = \"0.20.0\" # x-release-please-version\n",
"! curl -L -o /content/toolbox https://storage.googleapis.com/genai-toolbox/v{version}/linux/amd64/toolbox\n",
"\n",
"# Make the binary executable\n",

View File

@@ -123,7 +123,7 @@ In this section, we will download and install the Toolbox binary.
<!-- {x-release-please-start-version} -->
```bash
export OS="linux/amd64" # one of linux/amd64, darwin/arm64, darwin/amd64, or windows/amd64
export VERSION="0.19.1"
export VERSION="0.20.0"
curl -O https://storage.googleapis.com/genai-toolbox/v$VERSION/$OS/toolbox
```
<!-- {x-release-please-end} -->

View File

@@ -220,7 +220,7 @@
},
"outputs": [],
"source": [
"version = \"0.19.1\" # x-release-please-version\n",
"version = \"0.20.0\" # x-release-please-version\n",
"! curl -O https://storage.googleapis.com/genai-toolbox/v{version}/linux/amd64/toolbox\n",
"\n",
"# Make the binary executable\n",

View File

@@ -179,7 +179,7 @@ to use BigQuery, and then run the Toolbox server.
<!-- {x-release-please-start-version} -->
```bash
export OS="linux/amd64" # one of linux/amd64, darwin/arm64, darwin/amd64, or windows/amd64
curl -O https://storage.googleapis.com/genai-toolbox/v0.19.1/$OS/toolbox
curl -O https://storage.googleapis.com/genai-toolbox/v0.20.0/$OS/toolbox
```
<!-- {x-release-please-end} -->

View File

@@ -98,7 +98,7 @@ In this section, we will download Toolbox, configure our tools in a
<!-- {x-release-please-start-version} -->
```bash
export OS="linux/amd64" # one of linux/amd64, darwin/arm64, darwin/amd64, or windows/amd64
curl -O https://storage.googleapis.com/genai-toolbox/v0.19.1/$OS/toolbox
curl -O https://storage.googleapis.com/genai-toolbox/v0.20.0/$OS/toolbox
```
<!-- {x-release-please-end} -->

View File

@@ -34,7 +34,7 @@ In this section, we will download Toolbox and run the Toolbox server.
<!-- {x-release-please-start-version} -->
```bash
export OS="linux/amd64" # one of linux/amd64, darwin/arm64, darwin/amd64, or windows/amd64
curl -O https://storage.googleapis.com/genai-toolbox/v0.19.1/$OS/toolbox
curl -O https://storage.googleapis.com/genai-toolbox/v0.20.0/$OS/toolbox
```
<!-- {x-release-please-end} -->

View File

@@ -48,7 +48,7 @@ In this section, we will download Toolbox and run the Toolbox server.
<!-- {x-release-please-start-version} -->
```bash
export OS="linux/amd64" # one of linux/amd64, darwin/arm64, darwin/amd64, or windows/amd64
curl -O https://storage.googleapis.com/genai-toolbox/v0.19.1/$OS/toolbox
curl -O https://storage.googleapis.com/genai-toolbox/v0.20.0/$OS/toolbox
```
<!-- {x-release-please-end} -->

View File

@@ -34,7 +34,7 @@ In this section, we will download Toolbox and run the Toolbox server.
<!-- {x-release-please-start-version} -->
```bash
export OS="linux/amd64" # one of linux/amd64, darwin/arm64, darwin/amd64, or windows/amd64
curl -O https://storage.googleapis.com/genai-toolbox/v0.19.1/$OS/toolbox
curl -O https://storage.googleapis.com/genai-toolbox/v0.20.0/$OS/toolbox
```
<!-- {x-release-please-end} -->

View File

@@ -1,6 +1,6 @@
{
"name": "mcp-toolbox-for-databases",
"version": "0.19.1",
"version": "0.20.0",
"description": "MCP Toolbox for Databases is an open-source MCP server for more than 30 different datasources.",
"contextFileName": "MCP-TOOLBOX-EXTENSION.md"
}

6
go.mod
View File

@@ -44,8 +44,8 @@ require (
github.com/sijms/go-ora/v2 v2.9.0
github.com/spf13/cobra v1.10.1
github.com/thlib/go-timezone-local v0.0.7
github.com/trinodb/trino-go-client v0.329.0
github.com/valkey-io/valkey-go v1.0.67
github.com/trinodb/trino-go-client v0.330.0
github.com/valkey-io/valkey-go v1.0.68
github.com/yugabyte/pgx/v5 v5.5.3-yb-5
go.mongodb.org/mongo-driver v1.17.4
go.opentelemetry.io/contrib/propagators/autoprop v0.62.0
@@ -56,7 +56,7 @@ require (
go.opentelemetry.io/otel/sdk v1.37.0
go.opentelemetry.io/otel/sdk/metric v1.37.0
go.opentelemetry.io/otel/trace v1.38.0
golang.org/x/oauth2 v0.32.0
golang.org/x/oauth2 v0.33.0
google.golang.org/api v0.251.0
google.golang.org/genproto v0.0.0-20251022142026-3a174f9686a8
google.golang.org/protobuf v1.36.10

12
go.sum
View File

@@ -1266,10 +1266,10 @@ github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD
github.com/thlib/go-timezone-local v0.0.7 h1:fX8zd3aJydqLlTs/TrROrIIdztzsdFV23OzOQx31jII=
github.com/thlib/go-timezone-local v0.0.7/go.mod h1:/Tnicc6m/lsJE0irFMA0LfIwTBo4QP7A8IfyIv4zZKI=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/trinodb/trino-go-client v0.329.0 h1:tAQR5oXsW81C+lA0xiZsyoOcD7qYLv6Rtdw7SqH5Cy0=
github.com/trinodb/trino-go-client v0.329.0/go.mod h1:BXj9QNy6pA4Gn8eIu9dVdRhetABCjFAOZ6xxsVsOZJE=
github.com/valkey-io/valkey-go v1.0.67 h1:QPaRcuBmazhyoWTxk7I2XcSALhoL7UhAReR5o/rh1Po=
github.com/valkey-io/valkey-go v1.0.67/go.mod h1:bHmwjIEOrGq/ubOJfh5uMRs7Xj6mV3mQ/ZXUbmqpjqY=
github.com/trinodb/trino-go-client v0.330.0 h1:TBbHjFBuRjYbGtkNyRAJfzLOcwvz8ECihtMtxSzXqOc=
github.com/trinodb/trino-go-client v0.330.0/go.mod h1:BXj9QNy6pA4Gn8eIu9dVdRhetABCjFAOZ6xxsVsOZJE=
github.com/valkey-io/valkey-go v1.0.68 h1:bTbfonp49b41DqrF30q+y2JL3gcbjd2IiacFAtO4JBA=
github.com/valkey-io/valkey-go v1.0.68/go.mod h1:bHmwjIEOrGq/ubOJfh5uMRs7Xj6mV3mQ/ZXUbmqpjqY=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g=
@@ -1544,8 +1544,8 @@ golang.org/x/oauth2 v0.4.0/go.mod h1:RznEsdpjGAINPTOF0UH/t+xJ75L18YO3Ho6Pyn+uRec
golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I=
golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw=
golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4=
golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY=
golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo=
golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=

View File

@@ -30,4 +30,5 @@ type AuthService interface {
AuthServiceKind() string
GetName() string
GetClaimsFromHeader(context.Context, http.Header) (map[string]any, error)
ToConfig() AuthServiceConfig
}

View File

@@ -43,9 +43,7 @@ func (cfg Config) AuthServiceConfigKind() string {
// Initialize a Google auth service
func (cfg Config) Initialize() (auth.AuthService, error) {
a := &AuthService{
Name: cfg.Name,
Kind: AuthServiceKind,
ClientID: cfg.ClientID,
Config: cfg,
}
return a, nil
}
@@ -54,9 +52,7 @@ var _ auth.AuthService = AuthService{}
// struct used to store auth service info
type AuthService struct {
Name string `yaml:"name"`
Kind string `yaml:"kind"`
ClientID string `yaml:"clientId"`
Config
}
// Returns the auth service kind
@@ -64,6 +60,10 @@ func (a AuthService) AuthServiceKind() string {
return AuthServiceKind
}
func (a AuthService) ToConfig() auth.AuthServiceConfig {
return a.Config
}
// Returns the name of the auth service
func (a AuthService) GetName() string {
return a.Name

View File

@@ -15,6 +15,7 @@
sources:
alloydb-admin-source:
kind: alloydb-admin
defaultProject: ${ALLOYDB_POSTGRES_PROJECT:}
tools:
create_cluster:
kind: alloydb-create-cluster
@@ -30,8 +31,8 @@ tools:
kind: alloydb-create-instance
source: alloydb-admin-source
list_clusters:
kind: alloydb-list-clusters
source: alloydb-admin-source
kind: alloydb-list-clusters
source: alloydb-admin-source
list_instances:
kind: alloydb-list-instances
source: alloydb-admin-source

View File

@@ -163,6 +163,22 @@ tools:
list_schemas:
kind: postgres-list-schemas
source: alloydb-pg-source
list_indexes:
kind: postgres-list-indexes
source: alloydb-pg-source
list_sequences:
kind: postgres-list-sequences
source: alloydb-pg-source
database_overview:
kind: postgres-database-overview
source: alloydb-pg-source
list_triggers:
kind: postgres-list-triggers
source: alloydb-pg-source
toolsets:
alloydb_postgres_database_tools:
@@ -179,3 +195,7 @@ toolsets:
- get_query_plan
- list_views
- list_schemas
- database_overview
- list_triggers
- list_indexes
- list_sequences

View File

@@ -15,6 +15,7 @@
sources:
cloud-sql-admin-source:
kind: cloud-sql-admin
defaultProject: ${CLOUD_SQL_MSSQL_PROJECT:}
tools:
create_instance:

View File

@@ -15,6 +15,7 @@
sources:
cloud-sql-admin-source:
kind: cloud-sql-admin
defaultProject: ${CLOUD_SQL_MYSQL_PROJECT:}
tools:
create_instance:

View File

@@ -15,6 +15,7 @@
sources:
cloud-sql-admin-source:
kind: cloud-sql-admin
defaultProject: ${CLOUD_SQL_POSTGRES_PROJECT:}
tools:
create_instance:

View File

@@ -163,6 +163,22 @@ tools:
kind: postgres-list-schemas
source: cloudsql-pg-source
database_overview:
kind: postgres-database-overview
source: cloudsql-pg-source
list_triggers:
kind: postgres-list-triggers
source: cloudsql-pg-source
list_indexes:
kind: postgres-list-indexes
source: cloudsql-pg-source
list_sequences:
kind: postgres-list-sequences
source: cloudsql-pg-source
toolsets:
cloud_sql_postgres_database_tools:
- execute_sql
@@ -178,3 +194,7 @@ toolsets:
- get_query_plan
- list_views
- list_schemas
- database_overview
- list_triggers
- list_indexes
- list_sequences

View File

@@ -15,8 +15,8 @@
sources:
mysql-source:
kind: mysql
host: ${MYSQL_HOST}
port: ${MYSQL_PORT}
host: ${MYSQL_HOST:localhost}
port: ${MYSQL_PORT:3306}
database: ${MYSQL_DATABASE}
user: ${MYSQL_USER}
password: ${MYSQL_PASSWORD}

View File

@@ -15,8 +15,8 @@
sources:
postgresql-source:
kind: postgres
host: ${POSTGRES_HOST}
port: ${POSTGRES_PORT}
host: ${POSTGRES_HOST:localhost}
port: ${POSTGRES_PORT:5432}
database: ${POSTGRES_DATABASE}
user: ${POSTGRES_USER}
password: ${POSTGRES_PASSWORD}
@@ -162,6 +162,22 @@ tools:
kind: postgres-list-schemas
source: postgresql-source
database_overview:
kind: postgres-database-overview
source: postgresql-source
list_triggers:
kind: postgres-list-triggers
source: postgresql-source
list_indexes:
kind: postgres-list-indexes
source: postgresql-source
list_sequences:
kind: postgres-list-sequences
source: postgresql-source
toolsets:
postgres_database_tools:
- execute_sql
@@ -177,3 +193,7 @@ toolsets:
- get_query_plan
- list_views
- list_schemas
- database_overview
- list_triggers
- list_indexes
- list_sequences

View File

@@ -0,0 +1,89 @@
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package prompts
import (
"context"
"fmt"
"github.com/googleapis/genai-toolbox/internal/util"
"github.com/googleapis/genai-toolbox/internal/util/parameters"
)
// ArgMcpManifest is the simplified manifest structure for an argument required for prompts.
type ArgMcpManifest struct {
Name string `json:"name"`
Description string `json:"description"`
Required bool `json:"required"`
}
// Argument is a wrapper around a parameters.Parameter that provides prompt-specific functionality.
// If the 'type' field is not specified in a YAML definition, it defaults to 'string'.
type Argument struct {
parameters.Parameter
}
// McpManifest returns the simplified manifest structure required for prompts.
func (a Argument) McpManifest() ArgMcpManifest {
return ArgMcpManifest{
Name: a.GetName(),
Description: a.Manifest().Description,
Required: parameters.CheckParamRequired(a.GetRequired(), a.GetDefault()),
}
}
// Arguments is a slice of Argument.
type Arguments []Argument
// UnmarshalYAML provides custom unmarshaling logic for Arguments.
func (args *Arguments) UnmarshalYAML(ctx context.Context, unmarshal func(interface{}) error) error {
*args = make(Arguments, 0)
var rawList []util.DelayedUnmarshaler
if err := unmarshal(&rawList); err != nil {
return err
}
for _, u := range rawList {
var p map[string]any
if err := u.Unmarshal(&p); err != nil {
return fmt.Errorf("error parsing argument: %w", err)
}
// If 'type' is missing, default it to string.
paramType, ok := p["type"]
if !ok {
p["type"] = parameters.TypeString
paramType = parameters.TypeString
}
// Call the clean, exported parser from the tools package. No more duplicated logic!
param, err := parameters.ParseParameter(ctx, p, paramType.(string))
if err != nil {
return err
}
*args = append(*args, Argument{Parameter: param})
}
return nil
}
// ParseArguments validates and processes the user-provided arguments against the prompt's requirements.
func ParseArguments(arguments Arguments, args map[string]any, data map[string]map[string]any) (parameters.ParamValues, error) {
var params parameters.Parameters
for _, arg := range arguments {
params = append(params, arg.Parameter)
}
return parameters.ParseParams(params, args, data)
}

View File

@@ -0,0 +1,249 @@
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package prompts_test
import (
"fmt"
"strings"
"testing"
yaml "github.com/goccy/go-yaml"
"github.com/google/go-cmp/cmp"
"github.com/googleapis/genai-toolbox/internal/prompts"
"github.com/googleapis/genai-toolbox/internal/testutils"
"github.com/googleapis/genai-toolbox/internal/util/parameters"
)
// Test type aliases for convenience.
type (
Argument = prompts.Argument
ArgMcpManifest = prompts.ArgMcpManifest
Arguments = prompts.Arguments
)
// Ptr is a helper function to create a pointer to a value.
func Ptr[T any](v T) *T {
return &v
}
func makeArrayArg(name, desc string, items parameters.Parameter) Argument {
return Argument{Parameter: parameters.NewArrayParameter(name, desc, items)}
}
func TestArgMcpManifest(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
arg Argument
expected ArgMcpManifest
}{
{
name: "Required with no default",
arg: Argument{Parameter: parameters.NewStringParameterWithRequired("name1", "desc1", true)},
expected: ArgMcpManifest{
Name: "name1", Description: "desc1", Required: true,
},
},
{
name: "Not required with no default",
arg: Argument{Parameter: parameters.NewStringParameterWithRequired("name2", "desc2", false)},
expected: ArgMcpManifest{
Name: "name2", Description: "desc2", Required: false,
},
},
{
name: "Implicitly required with default",
arg: Argument{Parameter: parameters.NewStringParameterWithDefault("name3", "defaultVal", "desc3")},
expected: ArgMcpManifest{
Name: "name3", Description: "desc3", Required: false,
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
got := tc.arg.McpManifest()
if diff := cmp.Diff(tc.expected, got); diff != "" {
t.Errorf("McpManifest() mismatch (-want +got):\n%s", diff)
}
})
}
}
// TestArguments_UnmarshalYAML tests all unmarshaling logic for the Arguments type.
func TestArgumentsUnmarshalYAML(t *testing.T) {
t.Parallel()
// paramComparer allows cmp.Diff to intelligently compare the parsed results.
var transformFunc func(parameters.Parameter) any
transformFunc = func(p parameters.Parameter) any {
s := struct{ Name, Type, Desc string }{
Name: p.GetName(),
Type: p.GetType(),
Desc: p.Manifest().Description,
}
if arr, ok := p.(*parameters.ArrayParameter); ok {
s.Desc = fmt.Sprintf("%s items:%v", s.Desc, transformFunc(arr.GetItems()))
}
return s
}
paramComparer := cmp.Transformer("Parameter", transformFunc)
testCases := []struct {
name string
yamlInput []map[string]any
expectedArgs Arguments
wantErr string
}{
{
name: "Defaults type to string when omitted",
yamlInput: []map[string]any{
{"name": "p1", "description": "d1"},
},
expectedArgs: Arguments{
{Parameter: parameters.NewStringParameter("p1", "d1")},
},
},
{
name: "Respects type when present",
yamlInput: []map[string]any{
{"name": "p1", "description": "d1", "type": "integer"},
},
expectedArgs: Arguments{
{Parameter: parameters.NewIntParameter("p1", "d1")},
},
},
{
name: "Parses complex types like arrays correctly",
yamlInput: []map[string]any{
{
"name": "param_array",
"description": "an array",
"type": "array",
"items": map[string]any{
"name": "item_name",
"type": "string",
"description": "an item",
},
},
},
expectedArgs: Arguments{
makeArrayArg("param_array", "an array", parameters.NewStringParameter("item_name", "an item")),
},
},
{
name: "Propagates parsing error for unsupported type",
yamlInput: []map[string]any{
{"name": "p1", "description": "d1", "type": "unsupported"},
},
wantErr: `"unsupported" is not valid type for a parameter`,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
yamlBytes, err := yaml.Marshal(tc.yamlInput)
if err != nil {
t.Fatalf("Test setup failure: could not marshal test input to YAML: %v", err)
}
var got Arguments
ctx, err := testutils.ContextWithNewLogger()
if err != nil {
t.Fatalf("Failed to create logger using testutils: %v", err)
}
err = yaml.UnmarshalContext(ctx, yamlBytes, &got)
if tc.wantErr != "" {
if err == nil {
t.Fatalf("UnmarshalContext() expected error but got nil")
}
if !strings.Contains(err.Error(), tc.wantErr) {
t.Errorf("UnmarshalContext() error mismatch:\nwant to contain: %q\ngot: %q", tc.wantErr, err.Error())
}
} else {
if err != nil {
t.Fatalf("UnmarshalContext() returned unexpected error: %v", err)
}
if diff := cmp.Diff(tc.expectedArgs, got, paramComparer); diff != "" {
t.Errorf("UnmarshalContext() result mismatch (-want +got):\n%s", diff)
}
}
})
}
}
func TestParseArguments(t *testing.T) {
t.Parallel()
testArguments := prompts.Arguments{
{Parameter: parameters.NewStringParameter("name", "A required name.")},
{Parameter: parameters.NewIntParameterWithRequired("count", "An optional count.", false)},
}
testCases := []struct {
name string
argsIn map[string]any
want parameters.ParamValues
wantErr string
}{
{
name: "Success with all parameters provided",
argsIn: map[string]any{
"name": "test-name",
"count": 42,
},
want: parameters.ParamValues{
{Name: "name", Value: "test-name"},
{Name: "count", Value: 42},
},
},
{
name: "Success with only required parameters",
argsIn: map[string]any{
"name": "another-name",
},
want: parameters.ParamValues{
{Name: "name", Value: "another-name"},
{Name: "count", Value: nil},
},
},
{
name: "Failure with missing required parameter",
argsIn: map[string]any{
"count": 123,
},
wantErr: `parameter "name" is required`,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
got, err := prompts.ParseArguments(testArguments, tc.argsIn, nil)
if tc.wantErr != "" {
if err == nil {
t.Fatalf("expected an error but got nil")
}
if !strings.Contains(err.Error(), tc.wantErr) {
t.Errorf("error mismatch:\n want to contain: %q\n got: %q", tc.wantErr, err.Error())
}
} else {
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if diff := cmp.Diff(tc.want, got); diff != "" {
t.Errorf("ParseArguments() result mismatch (-want +got):\n%s", diff)
}
}
})
}
}

View File

@@ -0,0 +1,81 @@
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package custom
import (
"context"
"fmt"
yaml "github.com/goccy/go-yaml"
"github.com/googleapis/genai-toolbox/internal/prompts"
"github.com/googleapis/genai-toolbox/internal/util/parameters"
)
type Message = prompts.Message
const kind = "custom"
// init registers this prompt kind with the prompt framework.
func init() {
if !prompts.Register(kind, newConfig) {
panic(fmt.Sprintf("prompt kind %q already registered", kind))
}
}
// newConfig is the factory function for creating a custom prompt configuration.
func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (prompts.PromptConfig, error) {
cfg := &Config{Name: name}
if err := decoder.DecodeContext(ctx, cfg); err != nil {
return nil, err
}
return cfg, nil
}
// Config is the configuration for a custom prompt.
// It implements both the prompts.PromptConfig and prompts.Prompt interfaces.
type Config struct {
Name string `yaml:"name"`
Description string `yaml:"description,omitempty"`
Messages []Message `yaml:"messages"`
Arguments prompts.Arguments `yaml:"arguments,omitempty"`
}
// Interface compliance checks.
var _ prompts.PromptConfig = (*Config)(nil)
var _ prompts.Prompt = (*Config)(nil)
func (c *Config) PromptConfigKind() string {
return kind
}
func (c *Config) Initialize() (prompts.Prompt, error) {
return c, nil
}
func (c *Config) Manifest() prompts.Manifest {
return prompts.GetManifest(c.Description, c.Arguments)
}
func (c *Config) McpManifest() prompts.McpManifest {
return prompts.GetMcpManifest(c.Name, c.Description, c.Arguments)
}
func (c *Config) SubstituteParams(argValues parameters.ParamValues) (any, error) {
return prompts.SubstituteMessages(c.Messages, c.Arguments, argValues)
}
func (c *Config) ParseArgs(args map[string]any, data map[string]map[string]any) (parameters.ParamValues, error) {
return prompts.ParseArguments(c.Arguments, args, data)
}

View File

@@ -0,0 +1,143 @@
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package custom_test
import (
"strings"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/googleapis/genai-toolbox/internal/prompts"
"github.com/googleapis/genai-toolbox/internal/prompts/custom"
"github.com/googleapis/genai-toolbox/internal/util/parameters"
)
func TestConfig(t *testing.T) {
t.Parallel()
// Setup a shared config for testing its methods
testArgs := prompts.Arguments{
{Parameter: parameters.NewStringParameter("name", "The name to use.")},
{Parameter: parameters.NewStringParameterWithRequired("location", "The location.", false)},
}
cfg := &custom.Config{
Name: "TestConfig",
Description: "A test config.",
Messages: []custom.Message{
{Role: "user", Content: "Hello, my name is {{.name}} and I am in {{.location}}."},
},
Arguments: testArgs,
}
t.Run("Initialize and Kind", func(t *testing.T) {
p, err := cfg.Initialize()
if err != nil {
t.Fatalf("Initialize() failed: %v", err)
}
if p == nil {
t.Fatal("Initialize() returned a nil prompt")
}
if cfg.PromptConfigKind() != "custom" {
t.Errorf("PromptConfigKind() = %q, want %q", cfg.PromptConfigKind(), "custom")
}
})
t.Run("Manifest", func(t *testing.T) {
want := prompts.Manifest{
Description: "A test config.",
Arguments: []parameters.ParameterManifest{
{Name: "name", Type: "string", Required: true, Description: "The name to use.", AuthServices: []string{}},
{Name: "location", Type: "string", Required: false, Description: "The location.", AuthServices: []string{}},
},
}
got := cfg.Manifest()
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("Manifest() mismatch (-want +got):\n%s", diff)
}
})
t.Run("McpManifest", func(t *testing.T) {
want := prompts.McpManifest{
Name: "TestConfig",
Description: "A test config.",
Arguments: []prompts.ArgMcpManifest{
{Name: "name", Description: "The name to use.", Required: true},
{Name: "location", Description: "The location.", Required: false},
},
}
got := cfg.McpManifest()
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("McpManifest() mismatch (-want +got):\n%s", diff)
}
})
t.Run("SubstituteParams", func(t *testing.T) {
argValues := parameters.ParamValues{
{Name: "name", Value: "Alice"},
{Name: "location", Value: "Wonderland"},
}
want := []prompts.Message{
{Role: "user", Content: "Hello, my name is Alice and I am in Wonderland."},
}
got, err := cfg.SubstituteParams(argValues)
if err != nil {
t.Fatalf("SubstituteParams() failed: %v", err)
}
gotMessages, ok := got.([]prompts.Message)
if !ok {
t.Fatalf("expected result to be of type []prompts.Message, but got %T", got)
}
if diff := cmp.Diff(want, gotMessages); diff != "" {
t.Errorf("SubstituteParams() mismatch (-want +got):\n%s", diff)
}
})
t.Run("ParseArgs", func(t *testing.T) {
t.Run("Success", func(t *testing.T) {
argsIn := map[string]any{
"name": "Bob",
"location": "the Builder",
}
want := parameters.ParamValues{
{Name: "name", Value: "Bob"},
{Name: "location", Value: "the Builder"},
}
got, err := cfg.ParseArgs(argsIn, nil)
if err != nil {
t.Fatalf("ParseArgs() failed: %v", err)
}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("ParseArgs() mismatch (-want +got):\n%s", diff)
}
})
t.Run("FailureMissingRequired", func(t *testing.T) {
argsIn := map[string]any{
"location": "missing name",
}
_, err := cfg.ParseArgs(argsIn, nil)
if err == nil {
t.Fatal("expected an error for missing required arg, but got nil")
}
if !strings.Contains(err.Error(), `parameter "name" is required`) {
t.Errorf("expected error to be about missing parameter, but got: %v", err)
}
})
})
}

View File

@@ -0,0 +1,77 @@
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package prompts
import (
"fmt"
"github.com/googleapis/genai-toolbox/internal/util/parameters"
)
// Message represents a single message in a prompt, with a role and content.
type Message struct {
Role string `yaml:"role,omitempty"`
Content string `yaml:"content"`
}
const (
userRole = "user"
assistantRole = "assistant"
)
func (m *Message) UnmarshalYAML(unmarshal func(interface{}) error) error {
// Use a type alias to prevent an infinite recursion loop. The alias
// has the same fields but lacks the UnmarshalYAML method.
type messageAlias Message
var alias messageAlias
if err := unmarshal(&alias); err != nil {
return err
}
*m = Message(alias)
if m.Role == "" {
m.Role = userRole
}
if m.Role != userRole && m.Role != assistantRole {
return fmt.Errorf("invalid role %q: must be 'user' or 'assistant'", m.Role)
}
return nil
}
// SubstituteMessages takes a slice of Messages and a set of parameter values,
// and returns a new slice with all template variables resolved.
func SubstituteMessages(messages []Message, arguments Arguments, argValues parameters.ParamValues) ([]Message, error) {
substitutedMessages := make([]Message, 0, len(messages))
argsMap := argValues.AsMap()
var params parameters.Parameters
for _, arg := range arguments {
params = append(params, arg.Parameter)
}
for _, msg := range messages {
substitutedContent, err := parameters.ResolveTemplateParams(params, msg.Content, argsMap)
if err != nil {
return nil, fmt.Errorf("error substituting params for message: %w", err)
}
substitutedMessages = append(substitutedMessages, Message{
Role: msg.Role,
Content: substitutedContent,
})
}
return substitutedMessages, nil
}

View File

@@ -0,0 +1,133 @@
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package prompts_test
import (
"strings"
"testing"
yaml "github.com/goccy/go-yaml"
"github.com/google/go-cmp/cmp"
"github.com/googleapis/genai-toolbox/internal/prompts"
"github.com/googleapis/genai-toolbox/internal/util/parameters"
)
func TestMessageUnmarshalYAML(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
yamlInput map[string]any
want prompts.Message
wantErr string
}{
{
name: "Valid role: user",
yamlInput: map[string]any{"role": "user", "content": "Hello"},
want: prompts.Message{Role: "user", Content: "Hello"},
},
{
name: "Valid role: assistant",
yamlInput: map[string]any{"role": "assistant", "content": "Hi there"},
want: prompts.Message{Role: "assistant", Content: "Hi there"},
},
{
name: "Role is omitted, defaults to user",
yamlInput: map[string]any{"content": "A message with no role"},
want: prompts.Message{Role: "user", Content: "A message with no role"},
},
{
name: "Invalid role: other",
yamlInput: map[string]any{"role": "other", "content": "Some other role"},
wantErr: `invalid role "other": must be 'user' or 'assistant'`,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
yamlBytes, err := yaml.Marshal(tc.yamlInput)
if err != nil {
t.Fatalf("Test setup failure: could not marshal test input: %v", err)
}
var got prompts.Message
err = yaml.Unmarshal(yamlBytes, &got)
if tc.wantErr != "" {
if err == nil {
t.Fatalf("expected an error but got nil")
}
if !strings.Contains(err.Error(), tc.wantErr) {
t.Errorf("error mismatch:\n want to contain: %q\n got: %q", tc.wantErr, err.Error())
}
} else {
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if diff := cmp.Diff(tc.want, got); diff != "" {
t.Errorf("unmarshal mismatch (-want +got):\n%s", diff)
}
}
})
}
}
func TestSubstituteMessages(t *testing.T) {
t.Parallel()
t.Run("Success", func(t *testing.T) {
arguments := prompts.Arguments{
{Parameter: parameters.NewStringParameter("name", "The name to use.")},
{Parameter: parameters.NewStringParameterWithRequired("location", "The location.", false)},
}
messages := []prompts.Message{
{Role: "user", Content: "Hello, my name is {{.name}} and I am in {{.location}}."},
{Role: "assistant", Content: "Nice to meet you, {{.name}}!"},
}
argValues := parameters.ParamValues{
{Name: "name", Value: "Alice"},
{Name: "location", Value: "Wonderland"},
}
want := []prompts.Message{
{Role: "user", Content: "Hello, my name is Alice and I am in Wonderland."},
{Role: "assistant", Content: "Nice to meet you, Alice!"},
}
got, err := prompts.SubstituteMessages(messages, arguments, argValues)
if err != nil {
t.Fatalf("SubstituteMessages() failed: %v", err)
}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("SubstituteMessages() mismatch (-want +got):\n%s", diff)
}
})
t.Run("FailureInvalidTemplate", func(t *testing.T) {
arguments := prompts.Arguments{}
messages := []prompts.Message{
{Content: "This has an {{.unclosed template"},
}
argValues := parameters.ParamValues{}
_, err := prompts.SubstituteMessages(messages, arguments, argValues)
if err == nil {
t.Fatal("expected an error for invalid template, but got nil")
}
wantErr := "unexpected <template> in operand"
if !strings.Contains(err.Error(), wantErr) {
t.Errorf("error mismatch:\n want to contain: %q\n got: %q", wantErr, err.Error())
}
})
}

111
internal/prompts/prompts.go Normal file
View File

@@ -0,0 +1,111 @@
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package prompts
import (
"context"
"fmt"
yaml "github.com/goccy/go-yaml"
"github.com/googleapis/genai-toolbox/internal/util/parameters"
)
// PromptConfigFactory defines the signature for a function that creates and
// decodes a specific prompt's configuration.
type PromptConfigFactory func(ctx context.Context, name string, decoder *yaml.Decoder) (PromptConfig, error)
var promptRegistry = make(map[string]PromptConfigFactory)
// Register allows individual prompt packages to register their configuration
// factory function. This is typically called from an init() function in the
// prompt's package. It associates a 'kind' string with a function that can
// produce the specific PromptConfig type. It returns true if the registration was
// successful, and false if a prompt with the same kind was already registered.
func Register(kind string, factory PromptConfigFactory) bool {
if _, exists := promptRegistry[kind]; exists {
// Prompt with this kind already exists, do not overwrite.
return false
}
promptRegistry[kind] = factory
return true
}
// DecodeConfig looks up the registered factory for the given kind and uses it
// to decode the prompt configuration.
func DecodeConfig(ctx context.Context, kind, name string, decoder *yaml.Decoder) (PromptConfig, error) {
factory, found := promptRegistry[kind]
if !found && kind == "" {
kind = "custom"
factory, found = promptRegistry[kind]
}
if !found {
return nil, fmt.Errorf("unknown prompt kind: %q", kind)
}
promptConfig, err := factory(ctx, name, decoder)
if err != nil {
return nil, fmt.Errorf("unable to parse prompt %q as kind %q: %w", name, kind, err)
}
return promptConfig, nil
}
type PromptConfig interface {
PromptConfigKind() string
Initialize() (Prompt, error)
}
type Prompt interface {
SubstituteParams(parameters.ParamValues) (any, error)
ParseArgs(map[string]any, map[string]map[string]any) (parameters.ParamValues, error)
Manifest() Manifest
McpManifest() McpManifest
}
// Manifest is the representation of prompts sent to Client SDKs.
type Manifest struct {
Description string `json:"description"`
Arguments []parameters.ParameterManifest `json:"arguments"`
}
// McpManifest is the definition for a prompt the MCP client can get.
type McpManifest struct {
Name string `json:"name"`
Description string `json:"description,omitempty"`
Arguments []ArgMcpManifest `json:"arguments,omitempty"`
}
func GetMcpManifest(name, desc string, args Arguments) McpManifest {
mcpArgs := make([]ArgMcpManifest, 0, len(args))
for _, arg := range args {
mcpArgs = append(mcpArgs, arg.McpManifest())
}
return McpManifest{
Name: name,
Description: desc,
Arguments: mcpArgs,
}
}
func GetManifest(desc string, args Arguments) Manifest {
paramManifests := make([]parameters.ParameterManifest, 0, len(args))
for _, arg := range args {
paramManifests = append(paramManifests, arg.Manifest())
}
return Manifest{
Description: desc,
Arguments: paramManifests,
}
}

View File

@@ -0,0 +1,203 @@
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package prompts_test
import (
"context"
"errors"
"strings"
"testing"
yaml "github.com/goccy/go-yaml"
"github.com/google/go-cmp/cmp"
"github.com/googleapis/genai-toolbox/internal/prompts"
_ "github.com/googleapis/genai-toolbox/internal/prompts/custom"
"github.com/googleapis/genai-toolbox/internal/util/parameters"
)
type mockPromptConfig struct {
name string
kind string
}
func (m *mockPromptConfig) PromptConfigKind() string { return m.kind }
func (m *mockPromptConfig) Initialize() (prompts.Prompt, error) { return nil, nil }
var errMockFactory = errors.New("mock factory error")
func mockFactory(ctx context.Context, name string, decoder *yaml.Decoder) (prompts.PromptConfig, error) {
return &mockPromptConfig{name: name, kind: "mockKind"}, nil
}
func mockErrorFactory(ctx context.Context, name string, decoder *yaml.Decoder) (prompts.PromptConfig, error) {
return nil, errMockFactory
}
func TestRegistry(t *testing.T) {
t.Parallel()
ctx := context.Background()
t.Run("RegisterAndDecodeSuccess", func(t *testing.T) {
kind := "testKindSuccess"
if !prompts.Register(kind, mockFactory) {
t.Fatal("expected registration to succeed")
}
// This should fail because we are registering a duplicate
if prompts.Register(kind, mockFactory) {
t.Fatal("expected duplicate registration to fail")
}
decoder := yaml.NewDecoder(strings.NewReader(""))
config, err := prompts.DecodeConfig(ctx, kind, "testPrompt", decoder)
if err != nil {
t.Fatalf("expected DecodeConfig to succeed, but got error: %v", err)
}
if config == nil {
t.Fatal("expected a non-nil config")
}
})
t.Run("DecodeUnknownKind", func(t *testing.T) {
decoder := yaml.NewDecoder(strings.NewReader(""))
_, err := prompts.DecodeConfig(ctx, "unregisteredKind", "testPrompt", decoder)
if err == nil {
t.Fatal("expected an error for unknown kind, but got nil")
}
if !strings.Contains(err.Error(), "unknown prompt kind") {
t.Errorf("expected error to contain 'unknown prompt kind', but got: %v", err)
}
})
t.Run("FactoryReturnsError", func(t *testing.T) {
kind := "testKindError"
if !prompts.Register(kind, mockErrorFactory) {
t.Fatal("expected registration to succeed")
}
decoder := yaml.NewDecoder(strings.NewReader(""))
_, err := prompts.DecodeConfig(ctx, kind, "testPrompt", decoder)
if err == nil {
t.Fatal("expected an error from the factory, but got nil")
}
if !errors.Is(err, errMockFactory) {
t.Errorf("expected error to wrap mock factory error, but it didn't")
}
})
t.Run("DecodeDefaultsToCustom", func(t *testing.T) {
decoder := yaml.NewDecoder(strings.NewReader("description: A test prompt"))
config, err := prompts.DecodeConfig(ctx, "", "testDefaultPrompt", decoder)
if err != nil {
t.Fatalf("expected DecodeConfig with empty kind to succeed, but got error: %v", err)
}
if config == nil {
t.Fatal("expected a non-nil config for default kind")
}
if config.PromptConfigKind() != "custom" {
t.Errorf("expected default kind to be 'custom', but got %q", config.PromptConfigKind())
}
})
}
func TestGetMcpManifest(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
promptName string
description string
args prompts.Arguments
want prompts.McpManifest
}{
{
name: "No arguments",
promptName: "test-prompt",
description: "A test prompt.",
args: prompts.Arguments{},
want: prompts.McpManifest{
Name: "test-prompt",
Description: "A test prompt.",
Arguments: []prompts.ArgMcpManifest{},
},
},
{
name: "With arguments",
promptName: "arg-prompt",
description: "Prompt with args.",
args: prompts.Arguments{
{Parameter: parameters.NewStringParameter("param1", "First param")},
{Parameter: parameters.NewIntParameterWithRequired("param2", "Second param", false)},
},
want: prompts.McpManifest{
Name: "arg-prompt",
Description: "Prompt with args.",
Arguments: []prompts.ArgMcpManifest{
{Name: "param1", Description: "First param", Required: true},
{Name: "param2", Description: "Second param", Required: false},
},
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
got := prompts.GetMcpManifest(tc.promptName, tc.description, tc.args)
if diff := cmp.Diff(tc.want, got); diff != "" {
t.Errorf("GetMcpManifest() mismatch (-want +got):\n%s", diff)
}
})
}
}
func TestGetManifest(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
description string
args prompts.Arguments
want prompts.Manifest
}{
{
name: "No arguments",
description: "A simple prompt.",
args: prompts.Arguments{},
want: prompts.Manifest{
Description: "A simple prompt.",
Arguments: []parameters.ParameterManifest{},
},
},
{
name: "With arguments",
description: "Prompt with arguments.",
args: prompts.Arguments{
{Parameter: parameters.NewStringParameter("param1", "First param")},
{Parameter: parameters.NewBooleanParameterWithRequired("param2", "Second param", false)},
},
want: prompts.Manifest{
Description: "Prompt with arguments.",
Arguments: []parameters.ParameterManifest{
{Name: "param1", Type: "string", Required: true, Description: "First param", AuthServices: []string{}},
{Name: "param2", Type: "boolean", Required: false, Description: "Second param", AuthServices: []string{}},
},
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
got := prompts.GetManifest(tc.description, tc.args)
if diff := cmp.Diff(tc.want, got); diff != "" {
t.Errorf("GetManifest() mismatch (-want +got):\n%s", diff)
}
})
}
}

View File

@@ -0,0 +1,64 @@
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package prompts
import (
"fmt"
"github.com/googleapis/genai-toolbox/internal/tools"
)
type PromptsetConfig struct {
Name string `yaml:"name"`
PromptNames []string `yaml:",inline"`
}
type Promptset struct {
Name string `yaml:"name"`
Prompts []*Prompt `yaml:",inline"`
Manifest PromptsetManifest `yaml:",inline"`
McpManifest []McpManifest `yaml:",inline"`
}
type PromptsetManifest struct {
ServerVersion string `json:"serverVersion"`
PromptsManifest map[string]Manifest `json:"prompts"`
}
func (t PromptsetConfig) Initialize(serverVersion string, promptsMap map[string]Prompt) (Promptset, error) {
// Check each declared prompt name exists
var promptset Promptset
promptset.Name = t.Name
if !tools.IsValidName(promptset.Name) {
return promptset, fmt.Errorf("invalid promptset name: %s", t)
}
promptset.Prompts = make([]*Prompt, 0, len(t.PromptNames))
promptset.McpManifest = make([]McpManifest, 0, len(t.PromptNames))
promptset.Manifest = PromptsetManifest{
ServerVersion: serverVersion,
PromptsManifest: make(map[string]Manifest, len(t.PromptNames)),
}
for _, promptName := range t.PromptNames {
prompt, ok := promptsMap[promptName]
if !ok {
return promptset, fmt.Errorf("prompt does not exist: %s", t)
}
promptset.Prompts = append(promptset.Prompts, &prompt)
promptset.Manifest.PromptsManifest[promptName] = prompt.Manifest()
promptset.McpManifest = append(promptset.McpManifest, prompt.McpManifest())
}
return promptset, nil
}

View File

@@ -0,0 +1,215 @@
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package prompts_test
import (
"strings"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/googleapis/genai-toolbox/internal/prompts"
"github.com/googleapis/genai-toolbox/internal/util/parameters"
)
// mockPrompt is a simple mock implementation of prompts.Prompt for testing.
type mockPrompt struct {
name string
desc string
args prompts.Arguments
manifest prompts.Manifest
mcpManifest prompts.McpManifest
}
func (m *mockPrompt) SubstituteParams(parameters.ParamValues) (any, error) { return nil, nil }
func (m *mockPrompt) ParseArgs(map[string]any, map[string]map[string]any) (parameters.ParamValues, error) {
return nil, nil
}
func (m *mockPrompt) Manifest() prompts.Manifest { return m.manifest }
func (m *mockPrompt) McpManifest() prompts.McpManifest { return m.mcpManifest }
// newMockPrompt creates a new mock prompt for testing.
func newMockPrompt(name, desc string) prompts.Prompt {
args := prompts.Arguments{
{Parameter: parameters.NewStringParameter("arg1", "Test argument")},
}
return &mockPrompt{
name: name,
desc: desc,
args: args,
manifest: prompts.Manifest{
Description: desc,
Arguments: []parameters.ParameterManifest{
{Name: "arg1", Type: "string", Required: true, Description: "Test argument", AuthServices: []string{}},
},
},
mcpManifest: prompts.McpManifest{
Name: name,
Description: desc,
Arguments: []prompts.ArgMcpManifest{
{Name: "arg1", Description: "Test argument", Required: true},
},
},
}
}
func TestPromptsetConfig_Initialize(t *testing.T) {
t.Parallel()
promptsMap := map[string]prompts.Prompt{
"prompt1": newMockPrompt("prompt1", "First test prompt"),
"prompt2": newMockPrompt("prompt2", "Second test prompt"),
}
serverVersion := "v1.0.0"
p1 := promptsMap["prompt1"]
p2 := promptsMap["prompt2"]
prompt1Ptr := &p1
prompt2Ptr := &p2
testCases := []struct {
name string
config prompts.PromptsetConfig
want prompts.Promptset
wantErr string
}{
{
name: "Success case",
config: prompts.PromptsetConfig{
Name: "default",
PromptNames: []string{"prompt1", "prompt2"},
},
want: prompts.Promptset{
Name: "default",
Prompts: []*prompts.Prompt{
prompt1Ptr,
prompt2Ptr,
},
Manifest: prompts.PromptsetManifest{
ServerVersion: serverVersion,
PromptsManifest: map[string]prompts.Manifest{
"prompt1": promptsMap["prompt1"].Manifest(),
"prompt2": promptsMap["prompt2"].Manifest(),
},
},
McpManifest: []prompts.McpManifest{
promptsMap["prompt1"].McpManifest(),
promptsMap["prompt2"].McpManifest(),
},
},
wantErr: "",
},
{
name: "Success case with one prompt",
config: prompts.PromptsetConfig{
Name: "single",
PromptNames: []string{"prompt1"},
},
want: prompts.Promptset{
Name: "single",
Prompts: []*prompts.Prompt{
prompt1Ptr,
},
Manifest: prompts.PromptsetManifest{
ServerVersion: serverVersion,
PromptsManifest: map[string]prompts.Manifest{
"prompt1": promptsMap["prompt1"].Manifest(),
},
},
McpManifest: []prompts.McpManifest{
promptsMap["prompt1"].McpManifest(),
},
},
wantErr: "",
},
{
name: "Failure case - invalid promptset name",
config: prompts.PromptsetConfig{
Name: "invalid name", // Contains a space
PromptNames: []string{"prompt1"},
},
want: prompts.Promptset{Name: "invalid name"}, // Expect partial struct
wantErr: "invalid promptset name",
},
{
name: "Failure case - prompt not found",
config: prompts.PromptsetConfig{
Name: "missing_prompt",
PromptNames: []string{"prompt1", "prompt_does_not_exist"},
},
// Expect partial struct with fields populated up to the error
want: prompts.Promptset{
Name: "missing_prompt",
Prompts: []*prompts.Prompt{
prompt1Ptr,
},
Manifest: prompts.PromptsetManifest{
ServerVersion: serverVersion,
PromptsManifest: map[string]prompts.Manifest{
"prompt1": promptsMap["prompt1"].Manifest(),
},
},
McpManifest: []prompts.McpManifest{
promptsMap["prompt1"].McpManifest(),
},
},
wantErr: "prompt does not exist",
},
{
name: "Success case - empty prompt list",
config: prompts.PromptsetConfig{
Name: "empty",
PromptNames: []string{},
},
want: prompts.Promptset{
Name: "empty",
Prompts: []*prompts.Prompt{},
Manifest: prompts.PromptsetManifest{
ServerVersion: serverVersion,
PromptsManifest: map[string]prompts.Manifest{},
},
McpManifest: []prompts.McpManifest{},
},
wantErr: "",
},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
got, err := tc.config.Initialize(serverVersion, promptsMap)
if tc.wantErr != "" {
if err == nil {
t.Fatalf("Initialize() expected error but got nil")
}
if !strings.Contains(err.Error(), tc.wantErr) {
t.Errorf("Initialize() error mismatch:\n want to contain: %q\n got: %q", tc.wantErr, err.Error())
}
// Also check that the partially populated struct matches
if diff := cmp.Diff(tc.want, got, cmp.AllowUnexported(mockPrompt{})); diff != "" {
t.Errorf("Initialize() partial result on error mismatch (-want +got):\n%s", diff)
}
} else {
if err != nil {
t.Fatalf("Initialize() returned unexpected error: %v", err)
}
// Using cmp.AllowUnexported because mockPrompt is unexported
if diff := cmp.Diff(tc.want, got, cmp.AllowUnexported(mockPrompt{})); diff != "" {
t.Errorf("Initialize() result mismatch (-want +got):\n%s", diff)
}
}
})
}
}

View File

@@ -227,7 +227,7 @@ func toolInvokeHandler(s *Server, w http.ResponseWriter, r *http.Request) {
params, err := tool.ParseParams(data, claimsFromAuth)
if err != nil {
// If auth error, return 401
if errors.Is(err, tools.ErrUnauthorized) {
if errors.Is(err, util.ErrUnauthorized) {
s.logger.DebugContext(ctx, fmt.Sprintf("error parsing authenticated parameters from ID token: %s", err))
_ = render.Render(w, r, newErrResponse(err, http.StatusUnauthorized))
return

View File

@@ -28,8 +28,8 @@ import (
func TestToolsetEndpoint(t *testing.T) {
mockTools := []MockTool{tool1, tool2}
toolsMap, toolsets := setUpResources(t, mockTools)
r, shutdown := setUpServer(t, "api", toolsMap, toolsets)
toolsMap, toolsets, _, _ := setUpResources(t, mockTools, nil)
r, shutdown := setUpServer(t, "api", toolsMap, toolsets, nil, nil)
defer shutdown()
ts := runServer(r, false)
defer ts.Close()
@@ -125,8 +125,8 @@ func TestToolsetEndpoint(t *testing.T) {
func TestToolGetEndpoint(t *testing.T) {
mockTools := []MockTool{tool1, tool2}
toolsMap, toolsets := setUpResources(t, mockTools)
r, shutdown := setUpServer(t, "api", toolsMap, toolsets)
toolsMap, toolsets, _, _ := setUpResources(t, mockTools, nil)
r, shutdown := setUpServer(t, "api", toolsMap, toolsets, nil, nil)
defer shutdown()
ts := runServer(r, false)
defer ts.Close()
@@ -213,8 +213,8 @@ func TestToolGetEndpoint(t *testing.T) {
func TestToolInvokeEndpoint(t *testing.T) {
mockTools := []MockTool{tool1, tool2, tool4, tool5}
toolsMap, toolsets := setUpResources(t, mockTools)
r, shutdown := setUpServer(t, "api", toolsMap, toolsets)
toolsMap, toolsets, _, _ := setUpResources(t, mockTools, nil)
r, shutdown := setUpServer(t, "api", toolsMap, toolsets, nil, nil)
defer shutdown()
ts := runServer(r, false)
defer ts.Close()

View File

@@ -25,37 +25,46 @@ import (
"github.com/go-chi/chi/v5"
"github.com/googleapis/genai-toolbox/internal/log"
"github.com/googleapis/genai-toolbox/internal/prompts"
"github.com/googleapis/genai-toolbox/internal/telemetry"
"github.com/googleapis/genai-toolbox/internal/tools"
"github.com/googleapis/genai-toolbox/internal/util/parameters"
)
// fakeVersionString is used as a temporary version string in tests
const fakeVersionString = "0.0.0"
var _ tools.Tool = &MockTool{}
var (
_ tools.Tool = &MockTool{}
_ prompts.Prompt = &MockPrompt{}
)
// MockTool is used to mock tools in tests
type MockTool struct {
Name string
Description string
Params []tools.Parameter
Params []parameters.Parameter
manifest tools.Manifest
unauthorized bool
requiresClientAuthrorization bool
}
func (t MockTool) Invoke(context.Context, tools.ParamValues, tools.AccessToken) (any, error) {
func (t MockTool) Invoke(context.Context, parameters.ParamValues, tools.AccessToken) (any, error) {
mock := []any{t.Name}
return mock, nil
}
func (t MockTool) ToConfig() tools.ToolConfig {
return nil
}
// claims is a map of user info decoded from an auth token
func (t MockTool) ParseParams(data map[string]any, claimsMap map[string]map[string]any) (tools.ParamValues, error) {
return tools.ParseParams(t.Params, data, claimsMap)
func (t MockTool) ParseParams(data map[string]any, claimsMap map[string]map[string]any) (parameters.ParamValues, error) {
return parameters.ParseParams(t.Params, data, claimsMap)
}
func (t MockTool) Manifest() tools.Manifest {
pMs := make([]tools.ParameterManifest, 0, len(t.Params))
pMs := make([]parameters.ParameterManifest, 0, len(t.Params))
for _, p := range t.Params {
pMs = append(pMs, p.Manifest())
}
@@ -73,7 +82,7 @@ func (t MockTool) RequiresClientAuthorization() bool {
}
func (t MockTool) McpManifest() tools.McpManifest {
properties := make(map[string]tools.ParameterMcpManifest)
properties := make(map[string]parameters.ParameterMcpManifest)
required := make([]string, 0)
authParams := make(map[string][]string)
@@ -88,7 +97,7 @@ func (t MockTool) McpManifest() tools.McpManifest {
}
}
toolsSchema := tools.McpToolsSchema{
toolsSchema := parameters.McpToolsSchema{
Type: "object",
Properties: properties,
Required: required,
@@ -109,41 +118,92 @@ func (t MockTool) McpManifest() tools.McpManifest {
return mcpManifest
}
// MockPrompt is used to mock prompts in tests
type MockPrompt struct {
Name string
Description string
Args prompts.Arguments
}
func (p MockPrompt) SubstituteParams(vals parameters.ParamValues) (any, error) {
return []prompts.Message{
{
Role: "user",
Content: fmt.Sprintf("substituted %s", p.Name),
},
}, nil
}
func (p MockPrompt) ParseArgs(data map[string]any, claimsMap map[string]map[string]any) (parameters.ParamValues, error) {
var params parameters.Parameters
for _, arg := range p.Args {
params = append(params, arg.Parameter)
}
return parameters.ParseParams(params, data, claimsMap)
}
func (p MockPrompt) Manifest() prompts.Manifest {
var argManifests []parameters.ParameterManifest
for _, arg := range p.Args {
argManifests = append(argManifests, arg.Manifest())
}
return prompts.Manifest{
Description: p.Description,
Arguments: argManifests,
}
}
func (p MockPrompt) McpManifest() prompts.McpManifest {
return prompts.GetMcpManifest(p.Name, p.Description, p.Args)
}
var tool1 = MockTool{
Name: "no_params",
Params: []tools.Parameter{},
Params: []parameters.Parameter{},
}
var tool2 = MockTool{
Name: "some_params",
Params: tools.Parameters{
tools.NewIntParameter("param1", "This is the first parameter."),
tools.NewIntParameter("param2", "This is the second parameter."),
Params: parameters.Parameters{
parameters.NewIntParameter("param1", "This is the first parameter."),
parameters.NewIntParameter("param2", "This is the second parameter."),
},
}
var tool3 = MockTool{
Name: "array_param",
Description: "some description",
Params: tools.Parameters{
tools.NewArrayParameter("my_array", "this param is an array of strings", tools.NewStringParameter("my_string", "string item")),
Params: parameters.Parameters{
parameters.NewArrayParameter("my_array", "this param is an array of strings", parameters.NewStringParameter("my_string", "string item")),
},
}
var tool4 = MockTool{
Name: "unauthorized_tool",
Params: []tools.Parameter{},
Params: []parameters.Parameter{},
unauthorized: true,
}
var tool5 = MockTool{
Name: "require_client_auth_tool",
Params: []tools.Parameter{},
Params: []parameters.Parameter{},
requiresClientAuthrorization: true,
}
var prompt1 = MockPrompt{
Name: "prompt1",
Args: prompts.Arguments{},
}
var prompt2 = MockPrompt{
Name: "prompt2",
Args: prompts.Arguments{
{Parameter: parameters.NewStringParameter("arg1", "This is the first argument.")},
},
}
// setUpResources setups resources to test against
func setUpResources(t *testing.T, mockTools []MockTool) (map[string]tools.Tool, map[string]tools.Toolset) {
func setUpResources(t *testing.T, mockTools []MockTool, mockPrompts []MockPrompt) (map[string]tools.Tool, map[string]tools.Toolset, map[string]prompts.Prompt, map[string]prompts.Promptset) {
toolsMap := make(map[string]tools.Tool)
var allTools []string
for _, tool := range mockTools {
@@ -165,11 +225,29 @@ func setUpResources(t *testing.T, mockTools []MockTool) (map[string]tools.Tool,
}
toolsets[name] = m
}
return toolsMap, toolsets
promptsMap := make(map[string]prompts.Prompt)
var allPrompts []string
for _, prompt := range mockPrompts {
promptsMap[prompt.Name] = prompt
allPrompts = append(allPrompts, prompt.Name)
}
promptsets := make(map[string]prompts.Promptset)
if len(allPrompts) > 0 {
psc := prompts.PromptsetConfig{Name: "", PromptNames: allPrompts}
ps, err := psc.Initialize(fakeVersionString, promptsMap)
if err != nil {
t.Fatalf("unable to initialize default promptset: %s", err)
}
promptsets[""] = ps
}
return toolsMap, toolsets, promptsMap, promptsets
}
// setUpServer create a new server with tools and toolsets that are given
func setUpServer(t *testing.T, router string, tools map[string]tools.Tool, toolsets map[string]tools.Toolset) (chi.Router, func()) {
// setUpServer create a new server with tools, toolsets, prompts, and promptsets.
func setUpServer(t *testing.T, router string, tools map[string]tools.Tool, toolsets map[string]tools.Toolset, prompts map[string]prompts.Prompt, promptsets map[string]prompts.Promptset) (chi.Router, func()) {
ctx, cancel := context.WithCancel(context.Background())
testLogger, err := log.NewStdLogger(os.Stdout, os.Stderr, "info")
@@ -189,7 +267,7 @@ func setUpServer(t *testing.T, router string, tools map[string]tools.Tool, tools
sseManager := newSseManager(ctx)
resourceManager := NewResourceManager(nil, nil, tools, toolsets)
resourceManager := NewResourceManager(nil, nil, tools, toolsets, prompts, promptsets)
server := Server{
version: fakeVersionString,

View File

@@ -21,6 +21,7 @@ import (
yaml "github.com/goccy/go-yaml"
"github.com/googleapis/genai-toolbox/internal/auth"
"github.com/googleapis/genai-toolbox/internal/auth/google"
"github.com/googleapis/genai-toolbox/internal/prompts"
"github.com/googleapis/genai-toolbox/internal/sources"
"github.com/googleapis/genai-toolbox/internal/tools"
"github.com/googleapis/genai-toolbox/internal/util"
@@ -41,6 +42,10 @@ type ServerConfig struct {
ToolConfigs ToolConfigs
// ToolsetConfigs defines what tools are available.
ToolsetConfigs ToolsetConfigs
// PromptConfigs defines what prompts are available
PromptConfigs PromptConfigs
// PromptsetConfigs defines what prompts are available
PromptsetConfigs PromptsetConfigs
// LoggingFormat defines whether structured loggings are used.
LoggingFormat logFormat
// LogLevel defines the levels to log.
@@ -251,7 +256,7 @@ func (c *ToolConfigs) UnmarshalYAML(ctx context.Context, unmarshal func(interfac
return nil
}
// ToolConfigs is a type used to allow unmarshal of the toolset configs
// ToolsetConfigs is a type used to allow unmarshal of the toolset configs
type ToolsetConfigs map[string]tools.ToolsetConfig
// validate interface
@@ -270,3 +275,69 @@ func (c *ToolsetConfigs) UnmarshalYAML(ctx context.Context, unmarshal func(inter
}
return nil
}
// PromptConfigs is a type used to allow unmarshal of the prompt configs
type PromptConfigs map[string]prompts.PromptConfig
// validate interface
var _ yaml.InterfaceUnmarshalerContext = &PromptConfigs{}
func (c *PromptConfigs) UnmarshalYAML(ctx context.Context, unmarshal func(interface{}) error) error {
*c = make(PromptConfigs)
var raw map[string]util.DelayedUnmarshaler
if err := unmarshal(&raw); err != nil {
return err
}
for name, u := range raw {
var v map[string]any
if err := u.Unmarshal(&v); err != nil {
return fmt.Errorf("unable to unmarshal prompt %q: %w", name, err)
}
// Look for the 'kind' field. If it's not present, kindStr will be an
// empty string, which prompts.DecodeConfig will correctly default to "custom".
var kindStr string
if kindVal, ok := v["kind"]; ok {
var isString bool
kindStr, isString = kindVal.(string)
if !isString {
return fmt.Errorf("invalid 'kind' field for prompt %q (must be a string)", name)
}
}
// Create a new, strict decoder for this specific prompt's data.
yamlDecoder, err := util.NewStrictDecoder(v)
if err != nil {
return fmt.Errorf("error creating YAML decoder for prompt %q: %w", name, err)
}
// Use the central registry to decode the prompt based on its kind.
promptCfg, err := prompts.DecodeConfig(ctx, kindStr, name, yamlDecoder)
if err != nil {
return err
}
(*c)[name] = promptCfg
}
return nil
}
// PromptsetConfigs is a type used to allow unmarshal of the PromptsetConfigs configs
type PromptsetConfigs map[string]prompts.PromptsetConfig
// validate interface
var _ yaml.InterfaceUnmarshalerContext = &PromptsetConfigs{}
func (c *PromptsetConfigs) UnmarshalYAML(ctx context.Context, unmarshal func(interface{}) error) error {
*c = make(PromptsetConfigs)
var raw map[string][]string
if err := unmarshal(&raw); err != nil {
return err
}
for name, promptList := range raw {
(*c)[name] = prompts.PromptsetConfig{Name: name, PromptNames: promptList}
}
return nil
}

View File

@@ -36,7 +36,6 @@ import (
mcputil "github.com/googleapis/genai-toolbox/internal/server/mcp/util"
v20241105 "github.com/googleapis/genai-toolbox/internal/server/mcp/v20241105"
v20250326 "github.com/googleapis/genai-toolbox/internal/server/mcp/v20250326"
"github.com/googleapis/genai-toolbox/internal/tools"
"github.com/googleapis/genai-toolbox/internal/util"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
@@ -144,7 +143,7 @@ func (s *stdioSession) readInputStream(ctx context.Context) error {
}
return err
}
v, res, err := processMcpMessage(ctx, []byte(line), s.server, s.protocol, "", nil)
v, res, err := processMcpMessage(ctx, []byte(line), s.server, s.protocol, "", "", nil)
if err != nil {
// errors during the processing of message will generate a valid MCP Error response.
// server can continue to run.
@@ -374,6 +373,7 @@ func httpHandler(s *Server, w http.ResponseWriter, r *http.Request) {
}
toolsetName := chi.URLParam(r, "toolsetName")
promptsetName := chi.URLParam(r, "promptsetName")
s.logger.DebugContext(ctx, fmt.Sprintf("toolset name: %s", toolsetName))
span.SetAttributes(attribute.String("toolset_name", toolsetName))
@@ -406,7 +406,7 @@ func httpHandler(s *Server, w http.ResponseWriter, r *http.Request) {
return
}
v, res, err := processMcpMessage(ctx, body, s, protocolVersion, toolsetName, r.Header)
v, res, err := processMcpMessage(ctx, body, s, protocolVersion, toolsetName, promptsetName, r.Header)
if err != nil {
s.logger.DebugContext(ctx, fmt.Errorf("error processing message: %w", err).Error())
}
@@ -444,7 +444,7 @@ func httpHandler(s *Server, w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
case jsonrpc.INVALID_REQUEST:
errStr := err.Error()
if errors.Is(err, tools.ErrUnauthorized) {
if errors.Is(err, util.ErrUnauthorized) {
w.WriteHeader(http.StatusUnauthorized)
} else if strings.Contains(errStr, "Error 401") {
w.WriteHeader(http.StatusUnauthorized)
@@ -459,7 +459,7 @@ func httpHandler(s *Server, w http.ResponseWriter, r *http.Request) {
}
// processMcpMessage process the messages received from clients
func processMcpMessage(ctx context.Context, body []byte, s *Server, protocolVersion string, toolsetName string, header http.Header) (string, any, error) {
func processMcpMessage(ctx context.Context, body []byte, s *Server, protocolVersion string, toolsetName string, promptsetName string, header http.Header) (string, any, error) {
logger, err := util.LoggerFromContext(ctx)
if err != nil {
return "", jsonrpc.NewError("", jsonrpc.INTERNAL_ERROR, err.Error(), nil), err
@@ -514,7 +514,12 @@ func processMcpMessage(ctx context.Context, body []byte, s *Server, protocolVers
err = fmt.Errorf("toolset does not exist")
return "", jsonrpc.NewError(baseMessage.Id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err
}
res, err := mcp.ProcessMethod(ctx, protocolVersion, baseMessage.Id, baseMessage.Method, toolset, s.ResourceMgr.GetToolsMap(), s.ResourceMgr.GetAuthServiceMap(), body, header)
promptset, ok := s.ResourceMgr.GetPromptset(promptsetName)
if !ok {
err = fmt.Errorf("promptset does not exist")
return "", jsonrpc.NewError(baseMessage.Id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err
}
res, err := mcp.ProcessMethod(ctx, protocolVersion, baseMessage.Id, baseMessage.Method, toolset, s.ResourceMgr.GetToolsMap(), promptset, s.ResourceMgr.GetPromptsMap(), s.ResourceMgr.GetAuthServiceMap(), body, header)
return "", res, err
}
}

View File

@@ -22,6 +22,7 @@ import (
"slices"
"github.com/googleapis/genai-toolbox/internal/auth"
"github.com/googleapis/genai-toolbox/internal/prompts"
"github.com/googleapis/genai-toolbox/internal/server/mcp/jsonrpc"
mcputil "github.com/googleapis/genai-toolbox/internal/server/mcp/util"
v20241105 "github.com/googleapis/genai-toolbox/internal/server/mcp/v20241105"
@@ -60,12 +61,16 @@ func InitializeResponse(ctx context.Context, id jsonrpc.RequestId, body []byte,
}
toolsListChanged := false
promptsListChanged := false
result := mcputil.InitializeResult{
ProtocolVersion: protocolVersion,
Capabilities: mcputil.ServerCapabilities{
Tools: &mcputil.ListChanged{
ListChanged: &toolsListChanged,
},
Prompts: &mcputil.ListChanged{
ListChanged: &promptsListChanged,
},
},
ServerInfo: mcputil.Implementation{
BaseMetadata: mcputil.BaseMetadata{
@@ -95,14 +100,14 @@ func NotificationHandler(ctx context.Context, body []byte) error {
// ProcessMethod returns a response for the request.
// This is the Operation phase of the lifecycle for MCP client-server connections.
func ProcessMethod(ctx context.Context, mcpVersion string, id jsonrpc.RequestId, method string, toolset tools.Toolset, tools map[string]tools.Tool, authServices map[string]auth.AuthService, body []byte, header http.Header) (any, error) {
func ProcessMethod(ctx context.Context, mcpVersion string, id jsonrpc.RequestId, method string, toolset tools.Toolset, tools map[string]tools.Tool, promptset prompts.Promptset, prompts map[string]prompts.Prompt, authServices map[string]auth.AuthService, body []byte, header http.Header) (any, error) {
switch mcpVersion {
case v20250618.PROTOCOL_VERSION:
return v20250618.ProcessMethod(ctx, id, method, toolset, tools, authServices, body, header)
return v20250618.ProcessMethod(ctx, id, method, toolset, tools, promptset, prompts, authServices, body, header)
case v20250326.PROTOCOL_VERSION:
return v20250326.ProcessMethod(ctx, id, method, toolset, tools, authServices, body, header)
return v20250326.ProcessMethod(ctx, id, method, toolset, tools, promptset, prompts, authServices, body, header)
default:
return v20241105.ProcessMethod(ctx, id, method, toolset, tools, authServices, body, header)
return v20241105.ProcessMethod(ctx, id, method, toolset, tools, promptset, prompts, authServices, body, header)
}
}

View File

@@ -88,7 +88,8 @@ type ClientCapabilities struct {
// capabilities are defined here, in this schema, but this is not a closed set: any
// server can define its own, additional capabilities.
type ServerCapabilities struct {
Tools *ListChanged `json:"tools,omitempty"`
Tools *ListChanged `json:"tools,omitempty"`
Prompts *ListChanged `json:"prompts,omitempty"`
}
// Base interface for metadata with name (identifier) and title (display name) properties.

View File

@@ -24,13 +24,14 @@ import (
"strings"
"github.com/googleapis/genai-toolbox/internal/auth"
"github.com/googleapis/genai-toolbox/internal/prompts"
"github.com/googleapis/genai-toolbox/internal/server/mcp/jsonrpc"
"github.com/googleapis/genai-toolbox/internal/tools"
"github.com/googleapis/genai-toolbox/internal/util"
)
// ProcessMethod returns a response for the request.
func ProcessMethod(ctx context.Context, id jsonrpc.RequestId, method string, toolset tools.Toolset, tools map[string]tools.Tool, authServices map[string]auth.AuthService, body []byte, header http.Header) (any, error) {
func ProcessMethod(ctx context.Context, id jsonrpc.RequestId, method string, toolset tools.Toolset, tools map[string]tools.Tool, promptset prompts.Promptset, prompts map[string]prompts.Prompt, authServices map[string]auth.AuthService, body []byte, header http.Header) (any, error) {
switch method {
case PING:
return pingHandler(id)
@@ -38,6 +39,10 @@ func ProcessMethod(ctx context.Context, id jsonrpc.RequestId, method string, too
return toolsListHandler(id, toolset, body)
case TOOLS_CALL:
return toolsCallHandler(ctx, id, tools, authServices, body, header)
case PROMPTS_LIST:
return promptsListHandler(ctx, id, promptset, body)
case PROMPTS_GET:
return promptsGetHandler(ctx, id, prompts, body)
default:
err := fmt.Errorf("invalid method %s", method)
return jsonrpc.NewError(id, jsonrpc.METHOD_NOT_FOUND, err.Error(), nil), err
@@ -99,7 +104,7 @@ func toolsCallHandler(ctx context.Context, id jsonrpc.RequestId, toolsMap map[st
// Check if this specific tool requires the standard authorization header
if tool.RequiresClientAuthorization() {
if accessToken == "" {
return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, "missing access token in the 'Authorization' header", nil), tools.ErrUnauthorized
return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, "missing access token in the 'Authorization' header", nil), util.ErrUnauthorized
}
}
@@ -147,7 +152,7 @@ func toolsCallHandler(ctx context.Context, id jsonrpc.RequestId, toolsMap map[st
// Check if any of the specified auth services is verified
isAuthorized := tool.Authorized(verifiedAuthServices)
if !isAuthorized {
err = fmt.Errorf("unauthorized Tool call: Please make sure your specify correct auth headers: %w", tools.ErrUnauthorized)
err = fmt.Errorf("unauthorized Tool call: Please make sure your specify correct auth headers: %w", util.ErrUnauthorized)
return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err
}
logger.DebugContext(ctx, "tool invocation authorized")
@@ -164,7 +169,7 @@ func toolsCallHandler(ctx context.Context, id jsonrpc.RequestId, toolsMap map[st
if err != nil {
errStr := err.Error()
// Missing authService tokens.
if errors.Is(err, tools.ErrUnauthorized) {
if errors.Is(err, util.ErrUnauthorized) {
return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err
}
// Upstream auth error
@@ -212,3 +217,99 @@ func toolsCallHandler(ctx context.Context, id jsonrpc.RequestId, toolsMap map[st
Result: CallToolResult{Content: content},
}, nil
}
// promptsListHandler handles the "prompts/list" method.
func promptsListHandler(ctx context.Context, id jsonrpc.RequestId, promptset prompts.Promptset, body []byte) (any, error) {
// retrieve logger from context
logger, err := util.LoggerFromContext(ctx)
if err != nil {
return jsonrpc.NewError(id, jsonrpc.INTERNAL_ERROR, err.Error(), nil), err
}
logger.DebugContext(ctx, "handling prompts/list request")
var req ListPromptsRequest
if err := json.Unmarshal(body, &req); err != nil {
err = fmt.Errorf("invalid mcp prompts list request: %w", err)
return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err
}
result := ListPromptsResult{
Prompts: promptset.McpManifest,
}
logger.DebugContext(ctx, fmt.Sprintf("returning %d prompts", len(promptset.McpManifest)))
return jsonrpc.JSONRPCResponse{
Jsonrpc: jsonrpc.JSONRPC_VERSION,
Id: id,
Result: result,
}, nil
}
// promptsGetHandler handles the "prompts/get" method.
func promptsGetHandler(ctx context.Context, id jsonrpc.RequestId, promptsMap map[string]prompts.Prompt, body []byte) (any, error) {
// retrieve logger from context
logger, err := util.LoggerFromContext(ctx)
if err != nil {
return jsonrpc.NewError(id, jsonrpc.INTERNAL_ERROR, err.Error(), nil), err
}
logger.DebugContext(ctx, "handling prompts/get request")
var req GetPromptRequest
if err := json.Unmarshal(body, &req); err != nil {
err = fmt.Errorf("invalid mcp prompts/get request: %w", err)
return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err
}
promptName := req.Params.Name
logger.DebugContext(ctx, fmt.Sprintf("prompt name: %s", promptName))
prompt, ok := promptsMap[promptName]
if !ok {
err := fmt.Errorf("prompt with name %q does not exist", promptName)
return jsonrpc.NewError(id, jsonrpc.INVALID_PARAMS, err.Error(), nil), err
}
// Parse the arguments provided in the request.
argValues, err := prompt.ParseArgs(req.Params.Arguments, nil)
if err != nil {
err = fmt.Errorf("invalid arguments for prompt %q: %w", promptName, err)
return jsonrpc.NewError(id, jsonrpc.INVALID_PARAMS, err.Error(), nil), err
}
logger.DebugContext(ctx, fmt.Sprintf("parsed args: %v", argValues))
// Substitute the argument values into the prompt's messages.
substituted, err := prompt.SubstituteParams(argValues)
if err != nil {
err = fmt.Errorf("error substituting params for prompt %q: %w", promptName, err)
return jsonrpc.NewError(id, jsonrpc.INTERNAL_ERROR, err.Error(), nil), err
}
logger.DebugContext(ctx, "substituted params successfully")
// Cast the result to the expected []prompts.Message type.
substitutedMessages, ok := substituted.([]prompts.Message)
if !ok {
err = fmt.Errorf("internal error: SubstituteParams returned unexpected type")
return jsonrpc.NewError(id, jsonrpc.INTERNAL_ERROR, err.Error(), nil), err
}
// Format the response messages into the required structure.
promptMessages := make([]PromptMessage, len(substitutedMessages))
for i, msg := range substitutedMessages {
promptMessages[i] = PromptMessage{
Role: msg.Role,
Content: TextContent{
Type: "text",
Text: msg.Content,
},
}
}
result := GetPromptResult{
Description: prompt.Manifest().Description,
Messages: promptMessages,
}
return jsonrpc.JSONRPCResponse{
Jsonrpc: jsonrpc.JSONRPC_VERSION,
Id: id,
Result: result,
}, nil
}

View File

@@ -15,6 +15,7 @@
package v20241105
import (
"github.com/googleapis/genai-toolbox/internal/prompts"
"github.com/googleapis/genai-toolbox/internal/server/mcp/jsonrpc"
"github.com/googleapis/genai-toolbox/internal/tools"
)
@@ -27,9 +28,11 @@ const PROTOCOL_VERSION = "2024-11-05"
// methods that are supported.
const (
PING = "ping"
TOOLS_LIST = "tools/list"
TOOLS_CALL = "tools/call"
PING = "ping"
TOOLS_LIST = "tools/list"
TOOLS_CALL = "tools/call"
PROMPTS_LIST = "prompts/list"
PROMPTS_GET = "prompts/get"
)
/* Empty result */
@@ -136,3 +139,38 @@ type CallToolResult struct {
// If not set, this is assumed to be false (the call was successful).
IsError bool `json:"isError,omitempty"`
}
/* Prompts */
// Sent from the client to request a list of prompts the server has.
type ListPromptsRequest struct {
PaginatedRequest
}
// The server's response to a prompts/list request from the client.
type ListPromptsResult struct {
PaginatedResult
Prompts []prompts.McpManifest `json:"prompts"`
}
// Used by the client to get a prompt provided by the server.
type GetPromptRequest struct {
jsonrpc.Request
Params struct {
Name string `json:"name"`
Arguments map[string]any `json:"arguments,omitempty"`
} `json:"params"`
}
// The server's response to a prompts/get request from the client.
type GetPromptResult struct {
jsonrpc.Result
Description string `json:"description,omitempty"`
Messages []PromptMessage `json:"messages"`
}
// Describes a message returned as part of a prompt.
type PromptMessage struct {
Role string `json:"role"`
Content TextContent `json:"content"`
}

View File

@@ -24,13 +24,14 @@ import (
"strings"
"github.com/googleapis/genai-toolbox/internal/auth"
"github.com/googleapis/genai-toolbox/internal/prompts"
"github.com/googleapis/genai-toolbox/internal/server/mcp/jsonrpc"
"github.com/googleapis/genai-toolbox/internal/tools"
"github.com/googleapis/genai-toolbox/internal/util"
)
// ProcessMethod returns a response for the request.
func ProcessMethod(ctx context.Context, id jsonrpc.RequestId, method string, toolset tools.Toolset, tools map[string]tools.Tool, authServices map[string]auth.AuthService, body []byte, header http.Header) (any, error) {
func ProcessMethod(ctx context.Context, id jsonrpc.RequestId, method string, toolset tools.Toolset, tools map[string]tools.Tool, promptset prompts.Promptset, prompts map[string]prompts.Prompt, authServices map[string]auth.AuthService, body []byte, header http.Header) (any, error) {
switch method {
case PING:
return pingHandler(id)
@@ -38,6 +39,10 @@ func ProcessMethod(ctx context.Context, id jsonrpc.RequestId, method string, too
return toolsListHandler(id, toolset, body)
case TOOLS_CALL:
return toolsCallHandler(ctx, id, tools, authServices, body, header)
case PROMPTS_LIST:
return promptsListHandler(ctx, id, promptset, body)
case PROMPTS_GET:
return promptsGetHandler(ctx, id, prompts, body)
default:
err := fmt.Errorf("invalid method %s", method)
return jsonrpc.NewError(id, jsonrpc.METHOD_NOT_FOUND, err.Error(), nil), err
@@ -99,7 +104,7 @@ func toolsCallHandler(ctx context.Context, id jsonrpc.RequestId, toolsMap map[st
// Check if this specific tool requires the standard authorization header
if tool.RequiresClientAuthorization() {
if accessToken == "" {
return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, "missing access token in the 'Authorization' header", nil), tools.ErrUnauthorized
return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, "missing access token in the 'Authorization' header", nil), util.ErrUnauthorized
}
}
@@ -147,7 +152,7 @@ func toolsCallHandler(ctx context.Context, id jsonrpc.RequestId, toolsMap map[st
// Check if any of the specified auth services is verified
isAuthorized := tool.Authorized(verifiedAuthServices)
if !isAuthorized {
err = fmt.Errorf("unauthorized Tool call: Please make sure your specify correct auth headers: %w", tools.ErrUnauthorized)
err = fmt.Errorf("unauthorized Tool call: Please make sure your specify correct auth headers: %w", util.ErrUnauthorized)
return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err
}
logger.DebugContext(ctx, "tool invocation authorized")
@@ -164,7 +169,7 @@ func toolsCallHandler(ctx context.Context, id jsonrpc.RequestId, toolsMap map[st
if err != nil {
errStr := err.Error()
// Missing authService tokens.
if errors.Is(err, tools.ErrUnauthorized) {
if errors.Is(err, util.ErrUnauthorized) {
return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err
}
// Upstream auth error
@@ -211,3 +216,99 @@ func toolsCallHandler(ctx context.Context, id jsonrpc.RequestId, toolsMap map[st
Result: CallToolResult{Content: content},
}, nil
}
// promptsListHandler handles the "prompts/list" method.
func promptsListHandler(ctx context.Context, id jsonrpc.RequestId, promptset prompts.Promptset, body []byte) (any, error) {
// retrieve logger from context
logger, err := util.LoggerFromContext(ctx)
if err != nil {
return jsonrpc.NewError(id, jsonrpc.INTERNAL_ERROR, err.Error(), nil), err
}
logger.DebugContext(ctx, "handling prompts/list request")
var req ListPromptsRequest
if err := json.Unmarshal(body, &req); err != nil {
err = fmt.Errorf("invalid mcp prompts list request: %w", err)
return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err
}
result := ListPromptsResult{
Prompts: promptset.McpManifest,
}
logger.DebugContext(ctx, fmt.Sprintf("returning %d prompts", len(promptset.McpManifest)))
return jsonrpc.JSONRPCResponse{
Jsonrpc: jsonrpc.JSONRPC_VERSION,
Id: id,
Result: result,
}, nil
}
// promptsGetHandler handles the "prompts/get" method.
func promptsGetHandler(ctx context.Context, id jsonrpc.RequestId, promptsMap map[string]prompts.Prompt, body []byte) (any, error) {
// retrieve logger from context
logger, err := util.LoggerFromContext(ctx)
if err != nil {
return jsonrpc.NewError(id, jsonrpc.INTERNAL_ERROR, err.Error(), nil), err
}
logger.DebugContext(ctx, "handling prompts/get request")
var req GetPromptRequest
if err := json.Unmarshal(body, &req); err != nil {
err = fmt.Errorf("invalid mcp prompts/get request: %w", err)
return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err
}
promptName := req.Params.Name
logger.DebugContext(ctx, fmt.Sprintf("prompt name: %s", promptName))
prompt, ok := promptsMap[promptName]
if !ok {
err := fmt.Errorf("prompt with name %q does not exist", promptName)
return jsonrpc.NewError(id, jsonrpc.INVALID_PARAMS, err.Error(), nil), err
}
// Parse the arguments provided in the request.
argValues, err := prompt.ParseArgs(req.Params.Arguments, nil)
if err != nil {
err = fmt.Errorf("invalid arguments for prompt %q: %w", promptName, err)
return jsonrpc.NewError(id, jsonrpc.INVALID_PARAMS, err.Error(), nil), err
}
logger.DebugContext(ctx, fmt.Sprintf("parsed args: %v", argValues))
// Substitute the argument values into the prompt's messages.
substituted, err := prompt.SubstituteParams(argValues)
if err != nil {
err = fmt.Errorf("error substituting params for prompt %q: %w", promptName, err)
return jsonrpc.NewError(id, jsonrpc.INTERNAL_ERROR, err.Error(), nil), err
}
logger.DebugContext(ctx, "substituted params successfully")
// Cast the result to the expected []prompts.Message type.
substitutedMessages, ok := substituted.([]prompts.Message)
if !ok {
err = fmt.Errorf("internal error: SubstituteParams returned unexpected type")
return jsonrpc.NewError(id, jsonrpc.INTERNAL_ERROR, err.Error(), nil), err
}
// Format the response messages into the required structure.
promptMessages := make([]PromptMessage, len(substitutedMessages))
for i, msg := range substitutedMessages {
promptMessages[i] = PromptMessage{
Role: msg.Role,
Content: TextContent{
Type: "text",
Text: msg.Content,
},
}
}
result := GetPromptResult{
Description: prompt.Manifest().Description,
Messages: promptMessages,
}
return jsonrpc.JSONRPCResponse{
Jsonrpc: jsonrpc.JSONRPC_VERSION,
Id: id,
Result: result,
}, nil
}

View File

@@ -15,6 +15,7 @@
package v20250326
import (
"github.com/googleapis/genai-toolbox/internal/prompts"
"github.com/googleapis/genai-toolbox/internal/server/mcp/jsonrpc"
"github.com/googleapis/genai-toolbox/internal/tools"
)
@@ -27,9 +28,11 @@ const PROTOCOL_VERSION = "2025-03-26"
// methods that are supported.
const (
PING = "ping"
TOOLS_LIST = "tools/list"
TOOLS_CALL = "tools/call"
PING = "ping"
TOOLS_LIST = "tools/list"
TOOLS_CALL = "tools/call"
PROMPTS_LIST = "prompts/list"
PROMPTS_GET = "prompts/get"
)
/* Empty result */
@@ -168,3 +171,38 @@ type ToolAnnotations struct {
// Default: true
OpenWorldHint bool `json:"openWorldHint,omitempty"`
}
/* Prompts */
// Sent from the client to request a list of prompts the server has.
type ListPromptsRequest struct {
PaginatedRequest
}
// The server's response to a prompts/list request from the client.
type ListPromptsResult struct {
PaginatedResult
Prompts []prompts.McpManifest `json:"prompts"`
}
// Used by the client to get a prompt provided by the server.
type GetPromptRequest struct {
jsonrpc.Request
Params struct {
Name string `json:"name"`
Arguments map[string]any `json:"arguments,omitempty"`
} `json:"params"`
}
// The server's response to a prompts/get request from the client.
type GetPromptResult struct {
jsonrpc.Result
Description string `json:"description,omitempty"`
Messages []PromptMessage `json:"messages"`
}
// Describes a message returned as part of a prompt.
type PromptMessage struct {
Role string `json:"role"`
Content TextContent `json:"content"`
}

View File

@@ -24,13 +24,14 @@ import (
"strings"
"github.com/googleapis/genai-toolbox/internal/auth"
"github.com/googleapis/genai-toolbox/internal/prompts"
"github.com/googleapis/genai-toolbox/internal/server/mcp/jsonrpc"
"github.com/googleapis/genai-toolbox/internal/tools"
"github.com/googleapis/genai-toolbox/internal/util"
)
// ProcessMethod returns a response for the request.
func ProcessMethod(ctx context.Context, id jsonrpc.RequestId, method string, toolset tools.Toolset, tools map[string]tools.Tool, authServices map[string]auth.AuthService, body []byte, header http.Header) (any, error) {
func ProcessMethod(ctx context.Context, id jsonrpc.RequestId, method string, toolset tools.Toolset, tools map[string]tools.Tool, promptset prompts.Promptset, prompts map[string]prompts.Prompt, authServices map[string]auth.AuthService, body []byte, header http.Header) (any, error) {
switch method {
case PING:
return pingHandler(id)
@@ -38,6 +39,10 @@ func ProcessMethod(ctx context.Context, id jsonrpc.RequestId, method string, too
return toolsListHandler(id, toolset, body)
case TOOLS_CALL:
return toolsCallHandler(ctx, id, tools, authServices, body, header)
case PROMPTS_LIST:
return promptsListHandler(ctx, id, promptset, body)
case PROMPTS_GET:
return promptsGetHandler(ctx, id, prompts, body)
default:
err := fmt.Errorf("invalid method %s", method)
return jsonrpc.NewError(id, jsonrpc.METHOD_NOT_FOUND, err.Error(), nil), err
@@ -99,7 +104,7 @@ func toolsCallHandler(ctx context.Context, id jsonrpc.RequestId, toolsMap map[st
// Check if this specific tool requires the standard authorization header
if tool.RequiresClientAuthorization() {
if accessToken == "" {
return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, "missing access token in the 'Authorization' header", nil), tools.ErrUnauthorized
return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, "missing access token in the 'Authorization' header", nil), util.ErrUnauthorized
}
}
@@ -147,7 +152,7 @@ func toolsCallHandler(ctx context.Context, id jsonrpc.RequestId, toolsMap map[st
// Check if any of the specified auth services is verified
isAuthorized := tool.Authorized(verifiedAuthServices)
if !isAuthorized {
err = fmt.Errorf("unauthorized Tool call: Please make sure your specify correct auth headers: %w", tools.ErrUnauthorized)
err = fmt.Errorf("unauthorized Tool call: Please make sure your specify correct auth headers: %w", util.ErrUnauthorized)
return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err
}
logger.DebugContext(ctx, "tool invocation authorized")
@@ -164,7 +169,7 @@ func toolsCallHandler(ctx context.Context, id jsonrpc.RequestId, toolsMap map[st
if err != nil {
errStr := err.Error()
// Missing authService tokens.
if errors.Is(err, tools.ErrUnauthorized) {
if errors.Is(err, util.ErrUnauthorized) {
return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err
}
// Upstream auth error
@@ -211,3 +216,100 @@ func toolsCallHandler(ctx context.Context, id jsonrpc.RequestId, toolsMap map[st
Result: CallToolResult{Content: content},
}, nil
}
// promptsListHandler handles the "prompts/list" method.
func promptsListHandler(ctx context.Context, id jsonrpc.RequestId, promptset prompts.Promptset, body []byte) (any, error) {
// retrieve logger from context
logger, err := util.LoggerFromContext(ctx)
if err != nil {
return jsonrpc.NewError(id, jsonrpc.INTERNAL_ERROR, err.Error(), nil), err
}
logger.DebugContext(ctx, "handling prompts/list request")
var req ListPromptsRequest
if err := json.Unmarshal(body, &req); err != nil {
err = fmt.Errorf("invalid mcp prompts list request: %w", err)
return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err
}
result := ListPromptsResult{
Prompts: promptset.McpManifest,
}
logger.DebugContext(ctx, fmt.Sprintf("returning %d prompts", len(promptset.McpManifest)))
return jsonrpc.JSONRPCResponse{
Jsonrpc: jsonrpc.JSONRPC_VERSION,
Id: id,
Result: result,
}, nil
}
// promptsGetHandler handles the "prompts/get" method.
func promptsGetHandler(ctx context.Context, id jsonrpc.RequestId, promptsMap map[string]prompts.Prompt, body []byte) (any, error) {
// retrieve logger from context
logger, err := util.LoggerFromContext(ctx)
if err != nil {
return jsonrpc.NewError(id, jsonrpc.INTERNAL_ERROR, err.Error(), nil), err
}
logger.DebugContext(ctx, "handling prompts/get request")
var req GetPromptRequest
if err := json.Unmarshal(body, &req); err != nil {
err = fmt.Errorf("invalid mcp prompts/get request: %w", err)
return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err
}
promptName := req.Params.Name
logger.DebugContext(ctx, fmt.Sprintf("prompt name: %s", promptName))
prompt, ok := promptsMap[promptName]
if !ok {
err := fmt.Errorf("prompt with name %q does not exist", promptName)
return jsonrpc.NewError(id, jsonrpc.INVALID_PARAMS, err.Error(), nil), err
}
// Parse the arguments provided in the request.
argValues, err := prompt.ParseArgs(req.Params.Arguments, nil)
if err != nil {
err = fmt.Errorf("invalid arguments for prompt %q: %w", promptName, err)
return jsonrpc.NewError(id, jsonrpc.INVALID_PARAMS, err.Error(), nil), err
}
logger.DebugContext(ctx, fmt.Sprintf("parsed args: %v", argValues))
// Substitute the argument values into the prompt's messages.
substituted, err := prompt.SubstituteParams(argValues)
if err != nil {
err = fmt.Errorf("error substituting params for prompt %q: %w", promptName, err)
return jsonrpc.NewError(id, jsonrpc.INTERNAL_ERROR, err.Error(), nil), err
}
// Cast the result to the expected []prompts.Message type.
substitutedMessages, ok := substituted.([]prompts.Message)
if !ok {
err = fmt.Errorf("internal error: SubstituteParams returned unexpected type")
return jsonrpc.NewError(id, jsonrpc.INTERNAL_ERROR, err.Error(), nil), err
}
logger.DebugContext(ctx, "substituted params successfully")
// Format the response messages into the required structure.
promptMessages := make([]PromptMessage, len(substitutedMessages))
for i, msg := range substitutedMessages {
promptMessages[i] = PromptMessage{
Role: msg.Role,
Content: TextContent{
Type: "text",
Text: msg.Content,
},
}
}
result := GetPromptResult{
Description: prompt.Manifest().Description,
Messages: promptMessages,
}
return jsonrpc.JSONRPCResponse{
Jsonrpc: jsonrpc.JSONRPC_VERSION,
Id: id,
Result: result,
}, nil
}

View File

@@ -15,6 +15,7 @@
package v20250618
import (
"github.com/googleapis/genai-toolbox/internal/prompts"
"github.com/googleapis/genai-toolbox/internal/server/mcp/jsonrpc"
"github.com/googleapis/genai-toolbox/internal/tools"
)
@@ -27,9 +28,11 @@ const PROTOCOL_VERSION = "2025-06-18"
// methods that are supported.
const (
PING = "ping"
TOOLS_LIST = "tools/list"
TOOLS_CALL = "tools/call"
PING = "ping"
TOOLS_LIST = "tools/list"
TOOLS_CALL = "tools/call"
PROMPTS_LIST = "prompts/list"
PROMPTS_GET = "prompts/get"
)
/* Empty result */
@@ -179,3 +182,38 @@ type ToolAnnotations struct {
// Default: true
OpenWorldHint bool `json:"openWorldHint,omitempty"`
}
/* Prompts */
// Sent from the client to request a list of prompts the server has.
type ListPromptsRequest struct {
PaginatedRequest
}
// The server's response to a prompts/list request from the client.
type ListPromptsResult struct {
PaginatedResult
Prompts []prompts.McpManifest `json:"prompts"`
}
// Used by the client to get a prompt provided by the server.
type GetPromptRequest struct {
jsonrpc.Request
Params struct {
Name string `json:"name"`
Arguments map[string]any `json:"arguments,omitempty"`
} `json:"params"`
}
// The server's response to a prompts/get request from the client.
type GetPromptResult struct {
jsonrpc.Result
Description string `json:"description,omitempty"`
Messages []PromptMessage `json:"messages"`
}
// Describes a message returned as part of a prompt.
type PromptMessage struct {
Role string `json:"role"`
Content TextContent `json:"content"`
}

View File

@@ -30,7 +30,6 @@ import (
"github.com/googleapis/genai-toolbox/internal/log"
"github.com/googleapis/genai-toolbox/internal/server/mcp/jsonrpc"
"github.com/googleapis/genai-toolbox/internal/telemetry"
"github.com/googleapis/genai-toolbox/internal/tools"
)
const jsonrpcVersion = "2.0"
@@ -66,10 +65,19 @@ var tool3InputSchema = map[string]any{
"required": []any{"my_array"},
}
var prompt2Args = []any{
map[string]any{
"name": "arg1",
"description": "This is the first argument.",
"required": true,
},
}
func TestMcpEndpointWithoutInitialized(t *testing.T) {
mockTools := []MockTool{tool1, tool2, tool3, tool4, tool5}
toolsMap, toolsets := setUpResources(t, mockTools)
r, shutdown := setUpServer(t, "mcp", toolsMap, toolsets)
mockPrompts := []MockPrompt{prompt1, prompt2}
toolsMap, toolsets, promptsMap, promptsets := setUpResources(t, mockTools, mockPrompts)
r, shutdown := setUpServer(t, "mcp", toolsMap, toolsets, promptsMap, promptsets)
defer shutdown()
ts := runServer(r, false)
defer ts.Close()
@@ -247,6 +255,82 @@ func TestMcpEndpointWithoutInitialized(t *testing.T) {
},
},
},
{
name: "prompts/list",
url: "/",
body: jsonrpc.JSONRPCRequest{
Jsonrpc: jsonrpcVersion,
Id: "prompts-list-uninitialized",
Request: jsonrpc.Request{
Method: "prompts/list",
},
},
isErr: false,
want: map[string]any{
"jsonrpc": "2.0",
"id": "prompts-list-uninitialized",
"result": map[string]any{
"prompts": []any{
map[string]any{
"name": "prompt1",
},
map[string]any{
"name": "prompt2",
"arguments": prompt2Args,
},
},
},
},
},
{
name: "prompts/get non-existent prompt",
url: "/",
isErr: true,
body: jsonrpc.JSONRPCRequest{
Jsonrpc: jsonrpcVersion,
Id: "prompts-get-non-existent",
Request: jsonrpc.Request{
Method: "prompts/get",
},
Params: map[string]any{
"name": "non_existent_prompt",
},
},
want: map[string]any{
"jsonrpc": "2.0",
"id": "prompts-get-non-existent",
"error": map[string]any{
"code": -32602.0,
"message": `prompt with name "non_existent_prompt" does not exist`,
},
},
},
{
name: "prompts/get with invalid arguments",
url: "/",
isErr: true,
body: jsonrpc.JSONRPCRequest{
Jsonrpc: jsonrpcVersion,
Id: "prompts-get-invalid-args",
Request: jsonrpc.Request{
Method: "prompts/get",
},
Params: map[string]any{
"name": "prompt2",
"arguments": map[string]any{
"arg1": 42, // prompt2 expects a string, we send a number
},
},
},
want: map[string]any{
"jsonrpc": "2.0",
"id": "prompts-get-invalid-args",
"error": map[string]any{
"code": -32602.0,
"message": `invalid arguments for prompt "prompt2": unable to parse value for "arg1": %!q(float64=42) not type "string"`,
},
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
@@ -254,7 +338,6 @@ func TestMcpEndpointWithoutInitialized(t *testing.T) {
if err != nil {
t.Fatalf("unexpected error during marshaling of body")
}
resp, body, err := runRequest(ts, http.MethodPost, tc.url, bytes.NewBuffer(reqMarshal), nil)
if err != nil {
t.Fatalf("unexpected error during request: %s", err)
@@ -337,8 +420,9 @@ func runInitializeLifecycle(t *testing.T, ts *httptest.Server, protocolVersion s
func TestMcpEndpoint(t *testing.T) {
mockTools := []MockTool{tool1, tool2, tool3, tool4, tool5}
toolsMap, toolsets := setUpResources(t, mockTools)
r, shutdown := setUpServer(t, "mcp", toolsMap, toolsets)
mockPrompts := []MockPrompt{prompt1, prompt2}
toolsMap, toolsets, promptsMap, promptsets := setUpResources(t, mockTools, mockPrompts)
r, shutdown := setUpServer(t, "mcp", toolsMap, toolsets, promptsMap, promptsets)
defer shutdown()
ts := runServer(r, false)
defer ts.Close()
@@ -359,7 +443,8 @@ func TestMcpEndpoint(t *testing.T) {
"result": map[string]any{
"protocolVersion": "2024-11-05",
"capabilities": map[string]any{
"tools": map[string]any{"listChanged": false},
"tools": map[string]any{"listChanged": false},
"prompts": map[string]any{"listChanged": false},
},
"serverInfo": map[string]any{"name": serverName, "version": fakeVersionString},
},
@@ -375,7 +460,8 @@ func TestMcpEndpoint(t *testing.T) {
"result": map[string]any{
"protocolVersion": "2025-03-26",
"capabilities": map[string]any{
"tools": map[string]any{"listChanged": false},
"tools": map[string]any{"listChanged": false},
"prompts": map[string]any{"listChanged": false},
},
"serverInfo": map[string]any{"name": serverName, "version": fakeVersionString},
},
@@ -391,7 +477,8 @@ func TestMcpEndpoint(t *testing.T) {
"result": map[string]any{
"protocolVersion": "2025-06-18",
"capabilities": map[string]any{
"tools": map[string]any{"listChanged": false},
"tools": map[string]any{"listChanged": false},
"prompts": map[string]any{"listChanged": false},
},
"serverInfo": map[string]any{"name": serverName, "version": fakeVersionString},
},
@@ -488,6 +575,66 @@ func TestMcpEndpoint(t *testing.T) {
},
},
},
{
name: "prompts/list",
url: "/",
body: jsonrpc.JSONRPCRequest{
Jsonrpc: jsonrpcVersion,
Id: "prompts-list",
Request: jsonrpc.Request{
Method: "prompts/list",
},
},
wantStatusCode: http.StatusOK,
want: map[string]any{
"jsonrpc": "2.0",
"id": "prompts-list",
"result": map[string]any{
"prompts": []any{
map[string]any{
"name": "prompt1",
},
map[string]any{
"name": "prompt2",
"arguments": prompt2Args,
},
},
},
},
},
{
name: "prompts/get",
url: "/",
body: jsonrpc.JSONRPCRequest{
Jsonrpc: jsonrpcVersion,
Id: "prompts-get-prompt2",
Request: jsonrpc.Request{
Method: "prompts/get",
},
Params: map[string]any{
"name": "prompt2",
"arguments": map[string]any{
"arg1": "value1",
},
},
},
wantStatusCode: http.StatusOK,
want: map[string]any{
"jsonrpc": "2.0",
"id": "prompts-get-prompt2",
"result": map[string]any{
"messages": []any{
map[string]any{
"role": "user",
"content": map[string]any{
"type": "text",
"text": "substituted prompt2",
},
},
},
},
},
},
{
name: "tools/list on tool1_only",
url: "/tool1_only",
@@ -743,8 +890,7 @@ func TestMcpEndpoint(t *testing.T) {
}
func TestInvalidProtocolVersionHeader(t *testing.T) {
toolsMap, toolsets := map[string]tools.Tool{}, map[string]tools.Toolset{}
r, shutdown := setUpServer(t, "mcp", toolsMap, toolsets)
r, shutdown := setUpServer(t, "mcp", nil, nil, nil, nil)
defer shutdown()
ts := runServer(r, false)
defer ts.Close()
@@ -770,8 +916,7 @@ func TestInvalidProtocolVersionHeader(t *testing.T) {
}
func TestDeleteEndpoint(t *testing.T) {
toolsMap, toolsets := map[string]tools.Tool{}, map[string]tools.Toolset{}
r, shutdown := setUpServer(t, "mcp", toolsMap, toolsets)
r, shutdown := setUpServer(t, "mcp", nil, nil, nil, nil)
defer shutdown()
ts := runServer(r, false)
defer ts.Close()
@@ -786,8 +931,7 @@ func TestDeleteEndpoint(t *testing.T) {
}
func TestGetEndpoint(t *testing.T) {
toolsMap, toolsets := map[string]tools.Tool{}, map[string]tools.Toolset{}
r, shutdown := setUpServer(t, "mcp", toolsMap, toolsets)
r, shutdown := setUpServer(t, "mcp", nil, nil, nil, nil)
defer shutdown()
ts := runServer(r, false)
defer ts.Close()
@@ -810,7 +954,7 @@ func TestGetEndpoint(t *testing.T) {
}
func TestSseEndpoint(t *testing.T) {
r, shutdown := setUpServer(t, "mcp", nil, nil)
r, shutdown := setUpServer(t, "mcp", nil, nil, nil, nil)
defer shutdown()
ts := runServer(r, false)
defer ts.Close()
@@ -849,6 +993,12 @@ func TestSseEndpoint(t *testing.T) {
path: "/tool1_only/sse",
event: fmt.Sprintf("event: endpoint\ndata: http://127.0.0.1:%s/mcp/tool1_only?sessionId=", tsPort),
},
{
name: "promptset1",
server: ts,
path: "/prompt1_only/sse",
event: fmt.Sprintf("event: endpoint\ndata: http://127.0.0.1:%s/mcp/prompt1_only?sessionId=", tsPort),
},
{
name: "basic with http proto",
server: ts,
@@ -925,7 +1075,8 @@ func TestStdioSession(t *testing.T) {
defer cancel()
mockTools := []MockTool{tool1, tool2, tool3}
toolsMap, toolsets := setUpResources(t, mockTools)
mockPrompts := []MockPrompt{prompt1, prompt2}
toolsMap, toolsets, promptsMap, promptsets := setUpResources(t, mockTools, mockPrompts)
pr, pw, err := os.Pipe()
if err != nil {
@@ -955,7 +1106,7 @@ func TestStdioSession(t *testing.T) {
sseManager := newSseManager(ctx)
resourceManager := NewResourceManager(nil, nil, toolsMap, toolsets)
resourceManager := NewResourceManager(nil, nil, toolsMap, toolsets, promptsMap, promptsets)
server := &Server{
version: fakeVersionString,

View File

@@ -30,6 +30,7 @@ import (
"github.com/go-chi/httplog/v2"
"github.com/googleapis/genai-toolbox/internal/auth"
"github.com/googleapis/genai-toolbox/internal/log"
"github.com/googleapis/genai-toolbox/internal/prompts"
"github.com/googleapis/genai-toolbox/internal/sources"
"github.com/googleapis/genai-toolbox/internal/telemetry"
"github.com/googleapis/genai-toolbox/internal/tools"
@@ -57,12 +58,16 @@ type ResourceManager struct {
authServices map[string]auth.AuthService
tools map[string]tools.Tool
toolsets map[string]tools.Toolset
prompts map[string]prompts.Prompt
promptsets map[string]prompts.Promptset
}
func NewResourceManager(
sourcesMap map[string]sources.Source,
authServicesMap map[string]auth.AuthService,
toolsMap map[string]tools.Tool, toolsetsMap map[string]tools.Toolset,
promptsMap map[string]prompts.Prompt, promptsetsMap map[string]prompts.Promptset,
) *ResourceManager {
resourceMgr := &ResourceManager{
mu: sync.RWMutex{},
@@ -70,6 +75,8 @@ func NewResourceManager(
authServices: authServicesMap,
tools: toolsMap,
toolsets: toolsetsMap,
prompts: promptsMap,
promptsets: promptsetsMap,
}
return resourceMgr
@@ -103,13 +110,29 @@ func (r *ResourceManager) GetToolset(toolsetName string) (tools.Toolset, bool) {
return toolset, ok
}
func (r *ResourceManager) SetResources(sourcesMap map[string]sources.Source, authServicesMap map[string]auth.AuthService, toolsMap map[string]tools.Tool, toolsetsMap map[string]tools.Toolset) {
func (r *ResourceManager) GetPrompt(promptName string) (prompts.Prompt, bool) {
r.mu.RLock()
defer r.mu.RUnlock()
prompt, ok := r.prompts[promptName]
return prompt, ok
}
func (r *ResourceManager) GetPromptset(promptsetName string) (prompts.Promptset, bool) {
r.mu.RLock()
defer r.mu.RUnlock()
promptset, ok := r.promptsets[promptsetName]
return promptset, ok
}
func (r *ResourceManager) SetResources(sourcesMap map[string]sources.Source, authServicesMap map[string]auth.AuthService, toolsMap map[string]tools.Tool, toolsetsMap map[string]tools.Toolset, promptsMap map[string]prompts.Prompt, promptsetsMap map[string]prompts.Promptset) {
r.mu.Lock()
defer r.mu.Unlock()
r.sources = sourcesMap
r.authServices = authServicesMap
r.tools = toolsMap
r.toolsets = toolsetsMap
r.prompts = promptsMap
r.promptsets = promptsetsMap
}
func (r *ResourceManager) GetAuthServiceMap() map[string]auth.AuthService {
@@ -124,11 +147,19 @@ func (r *ResourceManager) GetToolsMap() map[string]tools.Tool {
return r.tools
}
func (r *ResourceManager) GetPromptsMap() map[string]prompts.Prompt {
r.mu.RLock()
defer r.mu.RUnlock()
return r.prompts
}
func InitializeConfigs(ctx context.Context, cfg ServerConfig) (
map[string]sources.Source,
map[string]auth.AuthService,
map[string]tools.Tool,
map[string]tools.Toolset,
map[string]prompts.Prompt,
map[string]prompts.Promptset,
error,
) {
ctx = util.WithUserAgent(ctx, cfg.Version)
@@ -160,7 +191,7 @@ func InitializeConfigs(ctx context.Context, cfg ServerConfig) (
return s, nil
}()
if err != nil {
return nil, nil, nil, nil, err
return nil, nil, nil, nil, nil, nil, err
}
sourcesMap[name] = s
}
@@ -188,7 +219,7 @@ func InitializeConfigs(ctx context.Context, cfg ServerConfig) (
return a, nil
}()
if err != nil {
return nil, nil, nil, nil, err
return nil, nil, nil, nil, nil, nil, err
}
authServicesMap[name] = a
}
@@ -216,7 +247,7 @@ func InitializeConfigs(ctx context.Context, cfg ServerConfig) (
return t, nil
}()
if err != nil {
return nil, nil, nil, nil, err
return nil, nil, nil, nil, nil, nil, err
}
toolsMap[name] = t
}
@@ -253,7 +284,7 @@ func InitializeConfigs(ctx context.Context, cfg ServerConfig) (
return t, err
}()
if err != nil {
return nil, nil, nil, nil, err
return nil, nil, nil, nil, nil, nil, err
}
toolsetsMap[name] = t
}
@@ -267,7 +298,76 @@ func InitializeConfigs(ctx context.Context, cfg ServerConfig) (
}
l.InfoContext(ctx, fmt.Sprintf("Initialized %d toolsets: %s", len(toolsetsMap), strings.Join(toolsetNames, ", ")))
return sourcesMap, authServicesMap, toolsMap, toolsetsMap, nil
// initialize and validate the prompts from configs
promptsMap := make(map[string]prompts.Prompt)
for name, pc := range cfg.PromptConfigs {
p, err := func() (prompts.Prompt, error) {
_, span := instrumentation.Tracer.Start(
ctx,
"toolbox/server/prompt/init",
trace.WithAttributes(attribute.String("prompt_kind", pc.PromptConfigKind())),
trace.WithAttributes(attribute.String("prompt_name", name)),
)
defer span.End()
p, err := pc.Initialize()
if err != nil {
return nil, fmt.Errorf("unable to initialize prompt %q: %w", name, err)
}
return p, nil
}()
if err != nil {
return nil, nil, nil, nil, nil, nil, err
}
promptsMap[name] = p
}
promptNames := make([]string, 0, len(promptsMap))
for name := range promptsMap {
promptNames = append(promptNames, name)
}
l.InfoContext(ctx, fmt.Sprintf("Initialized %d prompts: %s", len(promptsMap), strings.Join(promptNames, ", ")))
// create a default promptset that contains all prompts
allPromptNames := make([]string, 0, len(promptsMap))
for name := range promptsMap {
allPromptNames = append(allPromptNames, name)
}
if cfg.PromptsetConfigs == nil {
cfg.PromptsetConfigs = make(PromptsetConfigs)
}
cfg.PromptsetConfigs[""] = prompts.PromptsetConfig{Name: "", PromptNames: allPromptNames}
// initialize and validate the promptsets from configs
promptsetsMap := make(map[string]prompts.Promptset)
for name, pc := range cfg.PromptsetConfigs {
p, err := func() (prompts.Promptset, error) {
_, span := instrumentation.Tracer.Start(
ctx,
"toolbox/server/prompset/init",
trace.WithAttributes(attribute.String("prompset_name", name)),
)
defer span.End()
p, err := pc.Initialize(cfg.Version, promptsMap)
if err != nil {
return prompts.Promptset{}, fmt.Errorf("unable to initialize promptset %q: %w", name, err)
}
return p, err
}()
if err != nil {
return nil, nil, nil, nil, nil, nil, err
}
promptsetsMap[name] = p
}
promptsetNames := make([]string, 0, len(promptsetsMap))
for name := range promptsetsMap {
if name == "" {
promptsetNames = append(promptsetNames, "default")
} else {
promptsetNames = append(promptsetNames, name)
}
}
l.InfoContext(ctx, fmt.Sprintf("Initialized %d promptsets: %s", len(promptsetsMap), strings.Join(promptsetNames, ", ")))
return sourcesMap, authServicesMap, toolsMap, toolsetsMap, promptsMap, promptsetsMap, nil
}
// NewServer returns a Server object based on provided Config.
@@ -319,7 +419,7 @@ func NewServer(ctx context.Context, cfg ServerConfig) (*Server, error) {
httpLogger := httplog.NewLogger("httplog", httpOpts)
r.Use(httplog.RequestLogger(httpLogger))
sourcesMap, authServicesMap, toolsMap, toolsetsMap, err := InitializeConfigs(ctx, cfg)
sourcesMap, authServicesMap, toolsMap, toolsetsMap, promptsMap, promptsetsMap, err := InitializeConfigs(ctx, cfg)
if err != nil {
return nil, fmt.Errorf("unable to initialize configs: %w", err)
}
@@ -329,7 +429,7 @@ func NewServer(ctx context.Context, cfg ServerConfig) (*Server, error) {
sseManager := newSseManager(ctx)
resourceManager := NewResourceManager(sourcesMap, authServicesMap, toolsMap, toolsetsMap)
resourceManager := NewResourceManager(sourcesMap, authServicesMap, toolsMap, toolsetsMap, promptsMap, promptsetsMap)
s := &Server{
version: cfg.Version,

View File

@@ -26,6 +26,7 @@ import (
"github.com/google/go-cmp/cmp"
"github.com/googleapis/genai-toolbox/internal/auth"
"github.com/googleapis/genai-toolbox/internal/log"
"github.com/googleapis/genai-toolbox/internal/prompts"
"github.com/googleapis/genai-toolbox/internal/server"
"github.com/googleapis/genai-toolbox/internal/sources"
"github.com/googleapis/genai-toolbox/internal/sources/alloydbpg"
@@ -136,18 +137,29 @@ func TestUpdateServer(t *testing.T) {
newSources := map[string]sources.Source{
"example-source": &alloydbpg.Source{
Name: "example-alloydb-source",
Kind: "alloydb-postgres",
Config: alloydbpg.Config{
Name: "example-alloydb-source",
Kind: "alloydb-postgres",
},
},
}
newAuth := map[string]auth.AuthService{"example-auth": nil}
newTools := map[string]tools.Tool{"example-tool": nil}
newToolsets := map[string]tools.Toolset{
"example-toolset": {
Name: "example-toolset", Tools: []*tools.Tool{},
ToolsetConfig: tools.ToolsetConfig{
Name: "example-toolset",
},
Tools: []*tools.Tool{},
},
}
s.ResourceMgr.SetResources(newSources, newAuth, newTools, newToolsets)
newPrompts := map[string]prompts.Prompt{"example-prompt": nil}
newPromptsets := map[string]prompts.Promptset{
"example-promptset": {
Name: "example-promptset", Prompts: []*prompts.Prompt{},
},
}
s.ResourceMgr.SetResources(newSources, newAuth, newTools, newToolsets, newPrompts, newPromptsets)
if err != nil {
t.Errorf("error updating server: %s", err)
}
@@ -171,4 +183,14 @@ func TestUpdateServer(t *testing.T) {
if diff := cmp.Diff(gotToolset, newToolsets["example-toolset"]); diff != "" {
t.Errorf("error updating server, toolset (-want +got):\n%s", diff)
}
gotPrompt, _ := s.ResourceMgr.GetPrompt("example-prompt")
if diff := cmp.Diff(gotPrompt, newPrompts["example-prompt"]); diff != "" {
t.Errorf("error updating server, prompts (-want +got):\n%s", diff)
}
gotPromptset, _ := s.ResourceMgr.GetPromptset("example-promptset")
if diff := cmp.Diff(gotPromptset, newPromptsets["example-promptset"]); diff != "" {
t.Errorf("error updating server, promptset (-want +got):\n%s", diff)
}
}

View File

@@ -70,6 +70,7 @@ func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (sources
type Config struct {
Name string `yaml:"name" validate:"required"`
Kind string `yaml:"kind" validate:"required"`
DefaultProject string `yaml:"defaultProject"`
UseClientOAuth bool `yaml:"useClientOAuth"`
}
@@ -111,11 +112,9 @@ func (r Config) Initialize(ctx context.Context, tracer trace.Tracer) (sources.So
}
s := &Source{
Name: r.Name,
Kind: SourceKind,
BaseURL: "https://alloydb.googleapis.com",
Service: service,
UseClientOAuth: r.UseClientOAuth,
Config: r,
BaseURL: "https://alloydb.googleapis.com",
Service: service,
}
return s, nil
@@ -124,17 +123,19 @@ func (r Config) Initialize(ctx context.Context, tracer trace.Tracer) (sources.So
var _ sources.Source = &Source{}
type Source struct {
Name string `yaml:"name"`
Kind string `yaml:"kind"`
BaseURL string
Service *alloydbrestapi.Service
UseClientOAuth bool
Config
BaseURL string
Service *alloydbrestapi.Service
}
func (s *Source) SourceKind() string {
return SourceKind
}
func (s *Source) ToConfig() sources.SourceConfig {
return s.Config
}
func (s *Source) GetService(ctx context.Context, accessToken string) (*alloydbrestapi.Service, error) {
if s.UseClientOAuth {
token := &oauth2.Token{AccessToken: accessToken}

View File

@@ -76,9 +76,8 @@ func (r Config) Initialize(ctx context.Context, tracer trace.Tracer) (sources.So
}
s := &Source{
Name: r.Name,
Kind: SourceKind,
Pool: pool,
Config: r,
Pool: pool,
}
return s, nil
}
@@ -86,8 +85,7 @@ func (r Config) Initialize(ctx context.Context, tracer trace.Tracer) (sources.So
var _ sources.Source = &Source{}
type Source struct {
Name string `yaml:"name"`
Kind string `yaml:"kind"`
Config
Pool *pgxpool.Pool
}
@@ -95,6 +93,10 @@ func (s *Source) SourceKind() string {
return SourceKind
}
func (s *Source) ToConfig() sources.SourceConfig {
return s.Config
}
func (s *Source) PostgresPool() *pgxpool.Pool {
return s.Pool
}

View File

@@ -110,18 +110,12 @@ func (r Config) Initialize(ctx context.Context, tracer trace.Tracer) (sources.So
var err error
s := &Source{
Name: r.Name,
Kind: SourceKind,
Project: r.Project,
Location: r.Location,
Client: client,
RestService: restService,
TokenSource: tokenSource,
MaxQueryResultRows: 50,
WriteMode: r.WriteMode,
UseClientOAuth: r.UseClientOAuth,
ClientCreator: clientCreator,
ImpersonateServiceAccount: r.ImpersonateServiceAccount,
Config: r,
Client: client,
RestService: restService,
TokenSource: tokenSource,
MaxQueryResultRows: 50,
ClientCreator: clientCreator,
}
if r.UseClientOAuth {
@@ -241,20 +235,13 @@ func setupClientCaching(s *Source, baseCreator BigqueryClientCreator) {
var _ sources.Source = &Source{}
type Source struct {
// BigQuery Google SQL struct with client
Name string `yaml:"name"`
Kind string `yaml:"kind"`
Project string
Location string
Config
Client *bigqueryapi.Client
RestService *bigqueryrestapi.Service
TokenSource oauth2.TokenSource
MaxQueryResultRows int
ClientCreator BigqueryClientCreator
AllowedDatasets map[string]struct{}
UseClientOAuth bool
ImpersonateServiceAccount string
WriteMode string
sessionMutex sync.Mutex
makeDataplexCatalogClient func() (*dataplexapi.CatalogClient, DataplexClientCreator, error)
SessionProvider BigQuerySessionProvider
@@ -279,6 +266,10 @@ func (s *Source) SourceKind() string {
return SourceKind
}
func (s *Source) ToConfig() sources.SourceConfig {
return s.Config
}
func (s *Source) BigQueryClient() *bigqueryapi.Client {
return s.Client
}

Some files were not shown because too many files have changed in this diff Show More