mirror of
https://github.com/googleapis/genai-toolbox.git
synced 2026-01-11 08:28:11 -05:00
Compare commits
17 Commits
v0.9.0
...
envvariabl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e2ec345e4a | ||
|
|
ed5ef4caea | ||
|
|
6f569bc950 | ||
|
|
208df0a428 | ||
|
|
313d3ca0d0 | ||
|
|
4dae5a6ed7 | ||
|
|
26bdba46ca | ||
|
|
a817b120ca | ||
|
|
8ce311f256 | ||
|
|
86227e3104 | ||
|
|
206bea4575 | ||
|
|
65843621c5 | ||
|
|
e681a7e36c | ||
|
|
0d1cadb245 | ||
|
|
6d27dabfb2 | ||
|
|
f312fc01b2 | ||
|
|
4998cae260 |
1
.github/release-please.yml
vendored
1
.github/release-please.yml
vendored
@@ -20,6 +20,7 @@ extraFiles: [
|
||||
"README.md",
|
||||
"docs/en/getting-started/introduction/_index.md",
|
||||
"docs/en/getting-started/local_quickstart.md",
|
||||
"docs/en/getting-started/local_quickstart_js.md",
|
||||
"docs/en/getting-started/mcp_quickstart/_index.md",
|
||||
"docs/en/samples/bigquery/local_quickstart.md",
|
||||
"docs/en/samples/bigquery/mcp_quickstart/_index.md",
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
# MCP Toolbox for Databases
|
||||
|
||||
[](https://discord.gg/Dmm69peqjh)
|
||||
[](https://googleapis.github.io/genai-toolbox/)
|
||||
[](https://discord.gg/Dmm69peqjh)
|
||||
[](https://medium.com/@mcp_toolbox)
|
||||
[](https://goreportcard.com/report/github.com/googleapis/genai-toolbox)
|
||||
|
||||
> [!NOTE]
|
||||
|
||||
@@ -64,6 +64,8 @@ import (
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/spanner/spannerexecutesql"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/spanner/spannersql"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/sqlitesql"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/utility/envvariable"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/utility/wait"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/valkey"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
@@ -190,6 +190,18 @@
|
||||
"!sudo lsof -i :5432"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Optional: Enable Vertex AI API for Google Cloud\n",
|
||||
"\n",
|
||||
"If you're using a model hosted on **Vertex AI**, run the following command to enable the API:\n",
|
||||
"\n",
|
||||
"```bash\n",
|
||||
"!gcloud services enable aiplatform.googleapis.com\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
|
||||
@@ -316,4 +316,273 @@ const tools = toolboxTools.map(getTool);
|
||||
{{< /tabpane >}}
|
||||
|
||||
For more detailed instructions on using the Toolbox Core SDK, see the
|
||||
[project's README](https://github.com/googleapis/mcp-toolbox-sdk-js/blob/main/packages/toolbox-core/README.md).
|
||||
[project's README](https://github.com/googleapis/mcp-toolbox-sdk-js/blob/main/packages/toolbox-core/README.md).
|
||||
|
||||
#### Go
|
||||
|
||||
Once you've installed the [Toolbox Go
|
||||
SDK](https://pkg.go.dev/github.com/googleapis/mcp-toolbox-sdk-go/core), you can load
|
||||
tools:
|
||||
|
||||
{{< tabpane text=true persist=header >}}
|
||||
{{% tab header="Core" lang="en" %}}
|
||||
|
||||
{{< highlight go >}}
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
|
||||
"github.com/googleapis/mcp-toolbox-sdk-go/core"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// update the url to point to your server
|
||||
URL := "http://127.0.0.1:5000"
|
||||
ctx := context.Background()
|
||||
|
||||
client, err := core.NewToolboxClient(URL)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create Toolbox client: %v", err)
|
||||
}
|
||||
|
||||
// Framework agnostic tools
|
||||
tools, err := client.LoadToolset("toolsetName", ctx)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to load tools: %v", err)
|
||||
}
|
||||
}
|
||||
{{< /highlight >}}
|
||||
|
||||
{{% /tab %}}
|
||||
{{% tab header="LangChain Go" lang="en" %}}
|
||||
|
||||
{{< highlight go >}}
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log"
|
||||
|
||||
"github.com/googleapis/mcp-toolbox-sdk-go/core"
|
||||
"github.com/tmc/langchaingo/llms"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Make sure to add the error checks
|
||||
// update the url to point to your server
|
||||
URL := "http://127.0.0.1:5000"
|
||||
ctx := context.Background()
|
||||
|
||||
client, err := core.NewToolboxClient(URL)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create Toolbox client: %v", err)
|
||||
}
|
||||
|
||||
// Framework agnostic tool
|
||||
tool, err := client.LoadTool("toolName", ctx)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to load tools: %v", err)
|
||||
}
|
||||
|
||||
// Fetch the tool's input schema
|
||||
inputschema, err := tool.InputSchema()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to fetch inputSchema: %v", err)
|
||||
}
|
||||
|
||||
var paramsSchema map[string]any
|
||||
_ = json.Unmarshal(inputschema, ¶msSchema)
|
||||
|
||||
// Use this tool with LangChainGo
|
||||
langChainTool := llms.Tool{
|
||||
Type: "function",
|
||||
Function: &llms.FunctionDefinition{
|
||||
Name: tool.Name(),
|
||||
Description: tool.Description(),
|
||||
Parameters: paramsSchema,
|
||||
},
|
||||
}
|
||||
}
|
||||
{{< /highlight >}}
|
||||
|
||||
{{% /tab %}}
|
||||
{{% tab header="Genkit Go" lang="en" %}}
|
||||
|
||||
{{< highlight go >}}
|
||||
package main
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log"
|
||||
|
||||
"github.com/firebase/genkit/go/ai"
|
||||
"github.com/firebase/genkit/go/genkit"
|
||||
"github.com/googleapis/mcp-toolbox-sdk-go/core"
|
||||
"github.com/invopop/jsonschema"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Make sure to add the error checks
|
||||
// Update the url to point to your server
|
||||
URL := "http://127.0.0.1:5000"
|
||||
ctx := context.Background()
|
||||
g, err := genkit.Init(ctx)
|
||||
|
||||
client, err := core.NewToolboxClient(URL)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create Toolbox client: %v", err)
|
||||
}
|
||||
|
||||
// Framework agnostic tool
|
||||
tool, err := client.LoadTool("toolName", ctx)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to load tools: %v", err)
|
||||
}
|
||||
|
||||
// Fetch the tool's input schema
|
||||
inputschema, err := tool.InputSchema()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to fetch inputSchema: %v", err)
|
||||
}
|
||||
|
||||
var schema *jsonschema.Schema
|
||||
_ = json.Unmarshal(inputschema, &schema)
|
||||
|
||||
executeFn := func(ctx *ai.ToolContext, input any) (string, error) {
|
||||
result, err := tool.Invoke(ctx, input.(map[string]any))
|
||||
if err != nil {
|
||||
// Propagate errors from the tool invocation.
|
||||
return "", err
|
||||
}
|
||||
|
||||
return result.(string), nil
|
||||
}
|
||||
|
||||
// Use this tool with Genkit Go
|
||||
genkitTool := genkit.DefineToolWithInputSchema(
|
||||
g,
|
||||
tool.Name(),
|
||||
tool.Description(),
|
||||
schema,
|
||||
executeFn,
|
||||
)
|
||||
}
|
||||
{{< /highlight >}}
|
||||
|
||||
{{% /tab %}}
|
||||
{{% tab header="Go GenAI" lang="en" %}}
|
||||
|
||||
{{< highlight go >}}
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log"
|
||||
|
||||
"github.com/googleapis/mcp-toolbox-sdk-go/core"
|
||||
"google.golang.org/genai"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Make sure to add the error checks
|
||||
// Update the url to point to your server
|
||||
URL := "http://127.0.0.1:5000"
|
||||
ctx := context.Background()
|
||||
|
||||
client, err := core.NewToolboxClient(URL)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create Toolbox client: %v", err)
|
||||
}
|
||||
|
||||
// Framework agnostic tool
|
||||
tool, err := client.LoadTool("toolName", ctx)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to load tools: %v", err)
|
||||
}
|
||||
|
||||
// Fetch the tool's input schema
|
||||
inputschema, err := tool.InputSchema()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to fetch inputSchema: %v", err)
|
||||
}
|
||||
|
||||
var schema *genai.Schema
|
||||
_ = json.Unmarshal(inputschema, &schema)
|
||||
|
||||
funcDeclaration := &genai.FunctionDeclaration{
|
||||
Name: tool.Name(),
|
||||
Description: tool.Description(),
|
||||
Parameters: schema,
|
||||
}
|
||||
|
||||
// Use this tool with Go GenAI
|
||||
genAITool := &genai.Tool{
|
||||
FunctionDeclarations: []*genai.FunctionDeclaration{funcDeclaration},
|
||||
}
|
||||
}
|
||||
{{< /highlight >}}
|
||||
|
||||
{{% /tab %}}
|
||||
|
||||
{{% tab header="OpenAI Go" lang="en" %}}
|
||||
|
||||
{{< highlight go >}}
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log"
|
||||
|
||||
"github.com/googleapis/mcp-toolbox-sdk-go/core"
|
||||
openai "github.com/openai/openai-go"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Make sure to add the error checks
|
||||
// Update the url to point to your server
|
||||
URL := "http://127.0.0.1:5000"
|
||||
ctx := context.Background()
|
||||
|
||||
client, err := core.NewToolboxClient(URL)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create Toolbox client: %v", err)
|
||||
}
|
||||
|
||||
// Framework agnostic tool
|
||||
tool, err := client.LoadTool("toolName", ctx)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to load tools: %v", err)
|
||||
}
|
||||
|
||||
// Fetch the tool's input schema
|
||||
inputschema, err := tool.InputSchema()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to fetch inputSchema: %v", err)
|
||||
}
|
||||
|
||||
var paramsSchema openai.FunctionParameters
|
||||
_ = json.Unmarshal(inputschema, ¶msSchema)
|
||||
|
||||
// Use this tool with OpenAI Go
|
||||
openAITool := openai.ChatCompletionToolParam{
|
||||
Function: openai.FunctionDefinitionParam{
|
||||
Name: tool.Name(),
|
||||
Description: openai.String(tool.Description()),
|
||||
Parameters: paramsSchema,
|
||||
},
|
||||
}
|
||||
}
|
||||
{{< /highlight >}}
|
||||
|
||||
{{% /tab %}}
|
||||
{{< /tabpane >}}
|
||||
|
||||
For more detailed instructions on using the Toolbox Go SDK, see the
|
||||
[project's README](https://github.com/googleapis/mcp-toolbox-sdk-go/blob/main/core/README.md).
|
||||
|
||||
For end-to-end samples on using the Toolbox Go SDK with orchestration frameworks, see the [project's samples](https://github.com/googleapis/mcp-toolbox-sdk-go/tree/main/core/samples)
|
||||
@@ -1,9 +1,9 @@
|
||||
---
|
||||
title: "Quickstart (Local)"
|
||||
title: "Python Quickstart (Local)"
|
||||
type: docs
|
||||
weight: 2
|
||||
description: >
|
||||
How to get started running Toolbox locally with Python, PostgreSQL, and [Agent Development Kit](https://google.github.io/adk-docs/),
|
||||
How to get started running Toolbox locally with [Python](https://github.com/googleapis/mcp-toolbox-sdk-python), PostgreSQL, and [Agent Development Kit](https://google.github.io/adk-docs/),
|
||||
[LangGraph](https://www.langchain.com/langgraph), [LlamaIndex](https://www.llamaindex.ai/) or [GoogleGenAI](https://pypi.org/project/google-genai/).
|
||||
---
|
||||
|
||||
@@ -14,8 +14,21 @@ description: >
|
||||
This guide assumes you have already done the following:
|
||||
|
||||
1. Installed [Python 3.9+][install-python] (including [pip][install-pip] and
|
||||
your preferred virtual environment tool for managing dependencies e.g. [venv][install-venv])
|
||||
1. Installed [PostgreSQL 16+ and the `psql` client][install-postgres]
|
||||
your preferred virtual environment tool for managing dependencies e.g. [venv][install-venv]).
|
||||
1. Installed [PostgreSQL 16+ and the `psql` client][install-postgres].
|
||||
|
||||
### Cloud Setup (Optional)
|
||||
|
||||
If you plan to use **Google Cloud’s Vertex AI** with your agent (e.g., using `vertexai=True` or a Google GenAI model), follow these one-time setup steps for local development:
|
||||
|
||||
1. [Install the Google Cloud CLI](https://cloud.google.com/sdk/docs/install)
|
||||
1. [Set up Application Default Credentials (ADC)](https://cloud.google.com/docs/authentication/set-up-adc-local-dev-environment)
|
||||
1. Set your project and enable Vertex AI
|
||||
|
||||
```bash
|
||||
gcloud config set project YOUR_PROJECT_ID
|
||||
gcloud services enable aiplatform.googleapis.com
|
||||
```
|
||||
|
||||
[install-python]: https://wiki.python.org/moin/BeginnersGuide/Download
|
||||
[install-pip]: https://pip.pypa.io/en/stable/installation/
|
||||
@@ -141,6 +154,7 @@ postgres` and a password next time.
|
||||
\q
|
||||
```
|
||||
|
||||
|
||||
## Step 2: Install and configure Toolbox
|
||||
|
||||
In this section, we will download Toolbox, configure our tools in a
|
||||
@@ -632,7 +646,7 @@ asyncio.run(run_application())
|
||||
{{< /tab >}}
|
||||
{{< /tabpane >}}
|
||||
|
||||
{{< tabpane text=true persist=header >}}
|
||||
{{< tabpane text=true persist=header >}}
|
||||
{{% tab header="ADK" lang="en" %}}
|
||||
To learn more about Agent Development Kit, check out the [ADK
|
||||
documentation.](https://google.github.io/adk-docs/)
|
||||
@@ -658,3 +672,7 @@ Documentation](https://github.com/googleapis/python-genai?tab=readme-ov-file#man
|
||||
```sh
|
||||
python hotel_agent.py
|
||||
```
|
||||
|
||||
{{< notice info >}}
|
||||
For more information, visit the [Python SDK repo](https://github.com/googleapis/mcp-toolbox-sdk-python).
|
||||
{{</ notice >}}
|
||||
|
||||
490
docs/en/getting-started/local_quickstart_js.md
Normal file
490
docs/en/getting-started/local_quickstart_js.md
Normal file
@@ -0,0 +1,490 @@
|
||||
---
|
||||
title: "JS Quickstart (Local)"
|
||||
type: docs
|
||||
weight: 3
|
||||
description: >
|
||||
How to get started running Toolbox locally with [JavaScript](https://github.com/googleapis/mcp-toolbox-sdk-python), PostgreSQL, and orchestration frameworks such as [LangChain](https://js.langchain.com/docs/introduction/) and [GenkitJS](https://genkit.dev/docs/get-started/).
|
||||
---
|
||||
|
||||
## Before you begin
|
||||
|
||||
This guide assumes you have already done the following:
|
||||
|
||||
1. Installed [Node.js (v18 or higher)].
|
||||
1. Installed [PostgreSQL 16+ and the `psql` client][install-postgres].
|
||||
|
||||
### Cloud Setup (Optional)
|
||||
|
||||
If you plan to use **Google Cloud’s Vertex AI** with your agent (e.g., using Gemini or PaLM models), follow these one-time setup steps:
|
||||
|
||||
1. [Install the Google Cloud CLI]
|
||||
1. [Set up Application Default Credentials (ADC)]
|
||||
1. Set your project and enable Vertex AI
|
||||
|
||||
```bash
|
||||
gcloud config set project YOUR_PROJECT_ID
|
||||
gcloud services enable aiplatform.googleapis.com
|
||||
```
|
||||
|
||||
[Node.js (v18 or higher)]: https://nodejs.org/
|
||||
[install-postgres]: https://www.postgresql.org/download/
|
||||
[Install the Google Cloud CLI]: https://cloud.google.com/sdk/docs/install
|
||||
[Set up Application Default Credentials (ADC)]: https://cloud.google.com/docs/authentication/set-up-adc-local-dev-environment
|
||||
|
||||
|
||||
## Step 1: Set up your database
|
||||
|
||||
In this section, we will create a database, insert some data that needs to be
|
||||
accessed by our agent, and create a database user for Toolbox to connect with.
|
||||
|
||||
1. Connect to postgres using the `psql` command:
|
||||
|
||||
```bash
|
||||
psql -h 127.0.0.1 -U postgres
|
||||
```
|
||||
|
||||
Here, `postgres` denotes the default postgres superuser.
|
||||
|
||||
{{< notice info >}}
|
||||
|
||||
#### **Having trouble connecting?**
|
||||
|
||||
* **Password Prompt:** If you are prompted for a password for the `postgres`
|
||||
user and do not know it (or a blank password doesn't work), your PostgreSQL
|
||||
installation might require a password or a different authentication method.
|
||||
* **`FATAL: role "postgres" does not exist`:** This error means the default
|
||||
`postgres` superuser role isn't available under that name on your system.
|
||||
* **`Connection refused`:** Ensure your PostgreSQL server is actually running.
|
||||
You can typically check with `sudo systemctl status postgresql` and start it
|
||||
with `sudo systemctl start postgresql` on Linux systems.
|
||||
|
||||
<br/>
|
||||
|
||||
#### **Common Solution**
|
||||
|
||||
For password issues or if the `postgres` role seems inaccessible directly, try
|
||||
switching to the `postgres` operating system user first. This user often has
|
||||
permission to connect without a password for local connections (this is called
|
||||
peer authentication).
|
||||
|
||||
```bash
|
||||
sudo -i -u postgres
|
||||
psql -h 127.0.0.1
|
||||
```
|
||||
|
||||
Once you are in the `psql` shell using this method, you can proceed with the
|
||||
database creation steps below. Afterwards, type `\q` to exit `psql`, and then
|
||||
`exit` to return to your normal user shell.
|
||||
|
||||
If desired, once connected to `psql` as the `postgres` OS user, you can set a
|
||||
password for the `postgres` *database* user using: `ALTER USER postgres WITH
|
||||
PASSWORD 'your_chosen_password';`. This would allow direct connection with `-U
|
||||
postgres` and a password next time.
|
||||
{{< /notice >}}
|
||||
|
||||
1. Create a new database and a new user:
|
||||
|
||||
{{< notice tip >}}
|
||||
For a real application, it's best to follow the principle of least permission
|
||||
and only grant the privileges your application needs.
|
||||
{{< /notice >}}
|
||||
|
||||
```sql
|
||||
CREATE USER toolbox_user WITH PASSWORD 'my-password';
|
||||
|
||||
CREATE DATABASE toolbox_db;
|
||||
GRANT ALL PRIVILEGES ON DATABASE toolbox_db TO toolbox_user;
|
||||
|
||||
ALTER DATABASE toolbox_db OWNER TO toolbox_user;
|
||||
```
|
||||
|
||||
1. End the database session:
|
||||
|
||||
```bash
|
||||
\q
|
||||
```
|
||||
|
||||
(If you used `sudo -i -u postgres` and then `psql`, remember you might also
|
||||
need to type `exit` after `\q` to leave the `postgres` user's shell
|
||||
session.)
|
||||
|
||||
1. Connect to your database with your new user:
|
||||
|
||||
```bash
|
||||
psql -h 127.0.0.1 -U toolbox_user -d toolbox_db
|
||||
```
|
||||
|
||||
1. Create a table using the following command:
|
||||
|
||||
```sql
|
||||
CREATE TABLE hotels(
|
||||
id INTEGER NOT NULL PRIMARY KEY,
|
||||
name VARCHAR NOT NULL,
|
||||
location VARCHAR NOT NULL,
|
||||
price_tier VARCHAR NOT NULL,
|
||||
checkin_date DATE NOT NULL,
|
||||
checkout_date DATE NOT NULL,
|
||||
booked BIT NOT NULL
|
||||
);
|
||||
```
|
||||
|
||||
1. Insert data into the table.
|
||||
|
||||
```sql
|
||||
INSERT INTO hotels(id, name, location, price_tier, checkin_date, checkout_date, booked)
|
||||
VALUES
|
||||
(1, 'Hilton Basel', 'Basel', 'Luxury', '2024-04-22', '2024-04-20', B'0'),
|
||||
(2, 'Marriott Zurich', 'Zurich', 'Upscale', '2024-04-14', '2024-04-21', B'0'),
|
||||
(3, 'Hyatt Regency Basel', 'Basel', 'Upper Upscale', '2024-04-02', '2024-04-20', B'0'),
|
||||
(4, 'Radisson Blu Lucerne', 'Lucerne', 'Midscale', '2024-04-24', '2024-04-05', B'0'),
|
||||
(5, 'Best Western Bern', 'Bern', 'Upper Midscale', '2024-04-23', '2024-04-01', B'0'),
|
||||
(6, 'InterContinental Geneva', 'Geneva', 'Luxury', '2024-04-23', '2024-04-28', B'0'),
|
||||
(7, 'Sheraton Zurich', 'Zurich', 'Upper Upscale', '2024-04-27', '2024-04-02', B'0'),
|
||||
(8, 'Holiday Inn Basel', 'Basel', 'Upper Midscale', '2024-04-24', '2024-04-09', B'0'),
|
||||
(9, 'Courtyard Zurich', 'Zurich', 'Upscale', '2024-04-03', '2024-04-13', B'0'),
|
||||
(10, 'Comfort Inn Bern', 'Bern', 'Midscale', '2024-04-04', '2024-04-16', B'0');
|
||||
```
|
||||
|
||||
1. End the database session:
|
||||
|
||||
```bash
|
||||
\q
|
||||
```
|
||||
|
||||
## Step 2: Install and configure Toolbox
|
||||
|
||||
In this section, we will download Toolbox, configure our tools in a
|
||||
`tools.yaml`, and then run the Toolbox server.
|
||||
|
||||
1. Download the latest version of Toolbox as a binary:
|
||||
|
||||
{{< notice tip >}}
|
||||
Select the
|
||||
[correct binary](https://github.com/googleapis/genai-toolbox/releases)
|
||||
corresponding to your OS and CPU architecture.
|
||||
{{< /notice >}}
|
||||
<!-- {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.9.0/$OS/toolbox
|
||||
```
|
||||
<!-- {x-release-please-end} -->
|
||||
|
||||
1. Make the binary executable:
|
||||
|
||||
```bash
|
||||
chmod +x toolbox
|
||||
```
|
||||
|
||||
1. Write the following into a `tools.yaml` file. Be sure to update any fields
|
||||
such as `user`, `password`, or `database` that you may have customized in the
|
||||
previous step.
|
||||
|
||||
{{< notice tip >}}
|
||||
In practice, use environment variable replacement with the format ${ENV_NAME}
|
||||
instead of hardcoding your secrets into the configuration file.
|
||||
{{< /notice >}}
|
||||
|
||||
```yaml
|
||||
sources:
|
||||
my-pg-source:
|
||||
kind: postgres
|
||||
host: 127.0.0.1
|
||||
port: 5432
|
||||
database: toolbox_db
|
||||
user: ${USER_NAME}
|
||||
password: ${PASSWORD}
|
||||
tools:
|
||||
search-hotels-by-name:
|
||||
kind: postgres-sql
|
||||
source: my-pg-source
|
||||
description: Search for hotels based on name.
|
||||
parameters:
|
||||
- name: name
|
||||
type: string
|
||||
description: The name of the hotel.
|
||||
statement: SELECT * FROM hotels WHERE name ILIKE '%' || $1 || '%';
|
||||
search-hotels-by-location:
|
||||
kind: postgres-sql
|
||||
source: my-pg-source
|
||||
description: Search for hotels based on location.
|
||||
parameters:
|
||||
- name: location
|
||||
type: string
|
||||
description: The location of the hotel.
|
||||
statement: SELECT * FROM hotels WHERE location ILIKE '%' || $1 || '%';
|
||||
book-hotel:
|
||||
kind: postgres-sql
|
||||
source: my-pg-source
|
||||
description: >-
|
||||
Book a hotel by its ID. If the hotel is successfully booked, returns a NULL, raises an error if not.
|
||||
parameters:
|
||||
- name: hotel_id
|
||||
type: string
|
||||
description: The ID of the hotel to book.
|
||||
statement: UPDATE hotels SET booked = B'1' WHERE id = $1;
|
||||
update-hotel:
|
||||
kind: postgres-sql
|
||||
source: my-pg-source
|
||||
description: >-
|
||||
Update a hotel's check-in and check-out dates by its ID. Returns a message
|
||||
indicating whether the hotel was successfully updated or not.
|
||||
parameters:
|
||||
- name: hotel_id
|
||||
type: string
|
||||
description: The ID of the hotel to update.
|
||||
- name: checkin_date
|
||||
type: string
|
||||
description: The new check-in date of the hotel.
|
||||
- name: checkout_date
|
||||
type: string
|
||||
description: The new check-out date of the hotel.
|
||||
statement: >-
|
||||
UPDATE hotels SET checkin_date = CAST($2 as date), checkout_date = CAST($3
|
||||
as date) WHERE id = $1;
|
||||
cancel-hotel:
|
||||
kind: postgres-sql
|
||||
source: my-pg-source
|
||||
description: Cancel a hotel by its ID.
|
||||
parameters:
|
||||
- name: hotel_id
|
||||
type: string
|
||||
description: The ID of the hotel to cancel.
|
||||
statement: UPDATE hotels SET booked = B'0' WHERE id = $1;
|
||||
toolsets:
|
||||
my-toolset:
|
||||
- search-hotels-by-name
|
||||
- search-hotels-by-location
|
||||
- book-hotel
|
||||
- update-hotel
|
||||
- cancel-hotel
|
||||
```
|
||||
|
||||
For more info on tools, check out the `Resources` section of the docs.
|
||||
|
||||
1. Run the Toolbox server, pointing to the `tools.yaml` file created earlier:
|
||||
|
||||
```bash
|
||||
./toolbox --tools-file "tools.yaml"
|
||||
```
|
||||
{{< notice note >}}
|
||||
Toolbox enables dynamic reloading by default. To disable, use the `--disable-reload` flag.
|
||||
{{< /notice >}}
|
||||
|
||||
## Step 3: Connect your agent to Toolbox
|
||||
|
||||
In this section, we will write and run an agent that will load the Tools
|
||||
from Toolbox.
|
||||
|
||||
1. (Optional) Initialize a Node.js project:
|
||||
|
||||
```bash
|
||||
npm init -y
|
||||
```
|
||||
|
||||
1. In a new terminal, install the [SDK](https://www.npmjs.com/package/@toolbox-sdk/core).
|
||||
|
||||
```bash
|
||||
npm install langchain @toolbox-sdk/core
|
||||
```
|
||||
|
||||
1. Install other required dependencies
|
||||
|
||||
{{< tabpane persist=header >}}
|
||||
{{< tab header="LangChain" lang="bash" >}}
|
||||
npm install langchain @langchain/google-vertexai
|
||||
{{< /tab >}}
|
||||
{{< tab header="GenkitJS" lang="bash" >}}
|
||||
npm install genkit @genkit-ai/vertexai
|
||||
{{< /tab >}}
|
||||
{{< /tabpane >}}
|
||||
|
||||
1. Create a new file named `hotelAgent.js` and copy the following code to create an agent:
|
||||
|
||||
{{< tabpane persist=header >}}
|
||||
{{< tab header="LangChain" lang="js" >}}
|
||||
|
||||
import { ChatVertexAI } from "@langchain/google-vertexai";
|
||||
import { ToolboxClient } from "@toolbox-sdk/core";
|
||||
import { tool } from "@langchain/core/tools";
|
||||
import { createReactAgent } from "@langchain/langgraph/prebuilt";
|
||||
import { MemorySaver } from "@langchain/langgraph";
|
||||
|
||||
// Replace it with your API key
|
||||
process.env.GOOGLE_API_KEY = 'your-api-key';
|
||||
|
||||
const prompt = `
|
||||
You're a helpful hotel assistant. You handle hotel searching, booking, and
|
||||
cancellations. When the user searches for a hotel, mention its name, id,
|
||||
location and price tier. Always mention hotel ids while performing any
|
||||
searches. This is very important for any operations. For any bookings or
|
||||
cancellations, please provide the appropriate confirmation. Be sure to
|
||||
update checkin or checkout dates if mentioned by the user.
|
||||
Don't ask for confirmations from the user.
|
||||
`;
|
||||
|
||||
const queries = [
|
||||
"Find hotels in Basel with Basel in its name.",
|
||||
"Can you book the Hilton Basel for me?",
|
||||
"Oh wait, this is too expensive. Please cancel it and book the Hyatt Regency instead.",
|
||||
"My check in dates would be from April 10, 2024 to April 19, 2024.",
|
||||
];
|
||||
|
||||
async function runApplication() {
|
||||
const model = new ChatVertexAI({
|
||||
model: "gemini-2.0-flash",
|
||||
});
|
||||
|
||||
|
||||
const client = new ToolboxClient("http://127.0.0.1:5000");
|
||||
const toolboxTools = await client.loadToolset("my-toolset");
|
||||
|
||||
// Define the basics of the tool: name, description, schema and core logic
|
||||
const getTool = (toolboxTool) => tool(toolboxTool, {
|
||||
name: toolboxTool.getName(),
|
||||
description: toolboxTool.getDescription(),
|
||||
schema: toolboxTool.getParamSchema()
|
||||
});
|
||||
const tools = toolboxTools.map(getTool);
|
||||
|
||||
const agent = createReactAgent({
|
||||
llm: model,
|
||||
tools: tools,
|
||||
checkpointer: new MemorySaver(),
|
||||
systemPrompt: prompt,
|
||||
});
|
||||
|
||||
const langGraphConfig = {
|
||||
configurable: {
|
||||
thread_id: "test-thread",
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
for (const query of queries) {
|
||||
const agentOutput = await agent.invoke(
|
||||
{
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: query,
|
||||
},
|
||||
],
|
||||
verbose: true,
|
||||
},
|
||||
langGraphConfig
|
||||
);
|
||||
const response = agentOutput.messages[agentOutput.messages.length - 1].content;
|
||||
console.log(response);
|
||||
}
|
||||
}
|
||||
|
||||
runApplication()
|
||||
.catch(console.error)
|
||||
.finally(() => console.log("\nApplication finished."));
|
||||
|
||||
{{< /tab >}}
|
||||
|
||||
{{< tab header="GenkitJS" lang="js" >}}
|
||||
|
||||
import { ToolboxClient } from "@toolbox-sdk/core";
|
||||
import { genkit } from "genkit";
|
||||
import { googleAI } from '@genkit-ai/googleai';
|
||||
|
||||
// Replace it with your API key
|
||||
process.env.GOOGLE_API_KEY = 'your-api-key';
|
||||
|
||||
const systemPrompt = `
|
||||
You're a helpful hotel assistant. You handle hotel searching, booking, and
|
||||
cancellations. When the user searches for a hotel, mention its name, id,
|
||||
location and price tier. Always mention hotel ids while performing any
|
||||
searches. This is very important for any operations. For any bookings or
|
||||
cancellations, please provide the appropriate confirmation. Be sure to
|
||||
update checkin or checkout dates if mentioned by the user.
|
||||
Don't ask for confirmations from the user.
|
||||
`;
|
||||
|
||||
const queries = [
|
||||
"Find hotels in Basel with Basel in its name.",
|
||||
"Can you book the Hilton Basel for me?",
|
||||
"Oh wait, this is too expensive. Please cancel it and book the Hyatt Regency instead.",
|
||||
"My check in dates would be from April 10, 2024 to April 19, 2024.",
|
||||
];
|
||||
|
||||
async function run() {
|
||||
const toolboxClient = new ToolboxClient("http://127.0.0.1:5000");
|
||||
|
||||
const ai = genkit({
|
||||
plugins: [
|
||||
googleAI({
|
||||
apiKey: process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY
|
||||
})
|
||||
],
|
||||
model: googleAI.model('gemini-2.0-flash'),
|
||||
});
|
||||
|
||||
const toolboxTools = await toolboxClient.loadToolset("my-toolset");
|
||||
const toolMap = Object.fromEntries(
|
||||
toolboxTools.map((tool) => {
|
||||
const definedTool = ai.defineTool(
|
||||
{
|
||||
name: tool.getName(),
|
||||
description: tool.getDescription(),
|
||||
inputSchema: tool.getParamSchema(),
|
||||
},
|
||||
tool
|
||||
);
|
||||
return [tool.getName(), definedTool];
|
||||
})
|
||||
);
|
||||
const tools = Object.values(toolMap);
|
||||
|
||||
let conversationHistory = [{ role: "system", content: [{ text: systemPrompt }] }];
|
||||
|
||||
for (const query of queries) {
|
||||
conversationHistory.push({ role: "user", content: [{ text: query }] });
|
||||
const response = await ai.generate({
|
||||
messages: conversationHistory,
|
||||
tools: tools,
|
||||
});
|
||||
conversationHistory.push(response.message);
|
||||
|
||||
const toolRequests = response.toolRequests;
|
||||
if (toolRequests?.length > 0) {
|
||||
// Execute tools concurrently and collect their responses.
|
||||
const toolResponses = await Promise.all(
|
||||
toolRequests.map(async (call) => {
|
||||
try {
|
||||
const toolOutput = await toolMap[call.name].invoke(call.input);
|
||||
return { role: "tool", content: [{ toolResponse: { name: call.name, output: toolOutput } }] };
|
||||
} catch (e) {
|
||||
console.error(`Error executing tool ${call.name}:`, e);
|
||||
return { role: "tool", content: [{ toolResponse: { name: call.name, output: { error: e.message } } }] };
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
conversationHistory.push(...toolResponses);
|
||||
|
||||
// Call the AI again with the tool results.
|
||||
response = await ai.generate({ messages: conversationHistory, tools });
|
||||
conversationHistory.push(response.message);
|
||||
}
|
||||
|
||||
console.log(response.text);
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
{{< /tab >}}
|
||||
{{< /tabpane >}}
|
||||
|
||||
1. Run your agent, and observe the results:
|
||||
|
||||
```sh
|
||||
node hotelAgent.js
|
||||
```
|
||||
|
||||
{{< notice info >}}
|
||||
For more information, visit the [JS SDK repo](https://github.com/googleapis/mcp-toolbox-sdk-js).
|
||||
{{</ notice >}}
|
||||
@@ -218,10 +218,16 @@ In this section, we will download Toolbox, configure our tools in a
|
||||
|
||||
1. Type `y` when it asks to install the inspector package.
|
||||
|
||||
1. It should show the following when the MCP Inspector is up and running:
|
||||
1. It should show the following when the MCP Inspector is up and running (please take note of <YOUR_SESSION_TOKEN>):
|
||||
|
||||
```bash
|
||||
🔍 MCP Inspector is up and running at http://127.0.0.1:5173 🚀
|
||||
Starting MCP inspector...
|
||||
⚙️ Proxy server listening on localhost:6277
|
||||
🔑 Session token: <YOUR_SESSION_TOKEN>
|
||||
Use this token to authenticate requests or set DANGEROUSLY_OMIT_AUTH=true to disable auth
|
||||
|
||||
🚀 MCP Inspector is up and running at:
|
||||
http://localhost:6274/?MCP_PROXY_AUTH_TOKEN=<YOUR_SESSION_TOKEN>
|
||||
```
|
||||
|
||||
1. Open the above link in your browser.
|
||||
@@ -230,6 +236,8 @@ In this section, we will download Toolbox, configure our tools in a
|
||||
|
||||
1. For `URL`, type in `http://127.0.0.1:5000/mcp/sse`.
|
||||
|
||||
1. For `Authentication` -> `Proxy Session Token`, make sure <YOUR_SESSION_TOKEN> is present.
|
||||
|
||||
1. Click Connect.
|
||||
|
||||

|
||||
|
||||
@@ -20,18 +20,16 @@ The native SDKs can be combined with MCP clients in many cases.
|
||||
|
||||
Toolbox currently supports the following versions of MCP specification:
|
||||
|
||||
* [2024-11-05](https://spec.modelcontextprotocol.io/specification/2024-11-05/)
|
||||
* [2025-06-18](https://modelcontextprotocol.io/specification/2025-06-18)
|
||||
* [2025-03-26](https://modelcontextprotocol.io/specification/2025-03-26)
|
||||
* [2024-11-05](https://modelcontextprotocol.io/specification/2024-11-05)
|
||||
|
||||
### Features Not Supported by MCP
|
||||
### Toolbox AuthZ/AuthN Not Supported by MCP
|
||||
|
||||
Toolbox has several features that are not yet supported in the MCP specification:
|
||||
|
||||
* **AuthZ/AuthN:** There are no auth implementation in the `2024-11-05`
|
||||
specification. This includes:
|
||||
The auth implementation in Toolbox is not supported in MCP's auth specification.
|
||||
This includes:
|
||||
* [Authenticated Parameters](../resources/tools/_index.md#authenticated-parameters)
|
||||
* [Authorized Invocations](../resources/tools/_index.md#authorized-invocations)
|
||||
* **Notifications:** Currently, editing Toolbox Tools requires a server restart.
|
||||
Clients should reload tools on disconnect to get the latest version.
|
||||
|
||||
## Connecting to Toolbox with an MCP client
|
||||
|
||||
@@ -87,11 +85,25 @@ Add the following configuration to your MCP client configuration:
|
||||
|
||||
If you would like to connect to a specific toolset, replace `url` with
|
||||
`"http://127.0.0.1:5000/mcp/{toolset_name}/sse"`.
|
||||
{{% /tab %}} {{% tab header="HTTP POST" lang="en" %}}
|
||||
Connect to Toolbox HTTP POST via `http://127.0.0.1:5000/mcp`.
|
||||
|
||||
If you would like to connect to a specific toolset, connect via
|
||||
`http://127.0.0.1:5000/mcp/{toolset_name}`.
|
||||
HTTP with SSE is only supported in version `2024-11-05` and is currently
|
||||
deprecated.
|
||||
{{% /tab %}} {{% tab header="Streamable HTTP" lang="en" %}}
|
||||
Add the following configuration to your MCP client configuration:
|
||||
|
||||
```bash
|
||||
{
|
||||
"mcpServers": {
|
||||
"toolbox": {
|
||||
"type": "http",
|
||||
"url": "http://127.0.0.1:5000/mcp",
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If you would like to connect to a specific toolset, replace `url` with
|
||||
`"http://127.0.0.1:5000/mcp/{toolset_name}"`.
|
||||
{{% /tab %}} {{< /tabpane >}}
|
||||
|
||||
### Using the MCP Inspector with Toolbox
|
||||
@@ -132,6 +144,23 @@ testing and debugging Toolbox server.
|
||||
1. For `URL`, type in `http://127.0.0.1:5000/mcp/sse` to use all tool or
|
||||
`http//127.0.0.1:5000/mcp/{toolset_name}/sse` to use a specific toolset.
|
||||
|
||||
1. Click the `Connect` button. Voila! You should be able to inspect your toolbox
|
||||
tools!
|
||||
{{% /tab %}}
|
||||
{{% tab header="Streamable HTTP" lang="en" %}}
|
||||
1. [Run Toolbox](../getting-started/introduction/_index.md#running-the-server).
|
||||
|
||||
1. In a separate terminal, run Inspector directly through `npx`:
|
||||
|
||||
```bash
|
||||
npx @modelcontextprotocol/inspector
|
||||
```
|
||||
|
||||
1. For `Transport Type` dropdown menu, select `Streamable HTTP`.
|
||||
|
||||
1. For `URL`, type in `http://127.0.0.1:5000/mcp` to use all tool or
|
||||
`http//127.0.0.1:5000/mcp/{toolset_name}` to use a specific toolset.
|
||||
|
||||
1. Click the `Connect` button. Voila! You should be able to inspect your toolbox
|
||||
tools!
|
||||
{{% /tab %}} {{< /tabpane >}}
|
||||
|
||||
@@ -20,8 +20,11 @@ instance. It's compatible with any of the following sources:
|
||||
|
||||
Bigtable supports SQL queries. The integration with Toolbox supports `googlesql`
|
||||
dialect, the specified SQL statement is executed as a [data manipulation
|
||||
language (DML)][bigtable-googlesql] statements, and specified parameters will
|
||||
inserted according to their name: e.g. `@name`.
|
||||
language (DML)][bigtable-googlesql] statements, and specified parameters will inserted according to their name: e.g. `@name`.
|
||||
|
||||
{{<notice note>}}
|
||||
Bigtable's GoogleSQL support for DML statements might be limited to certain query types. For detailed information on supported DML statements and use cases, refer to the [Bigtable GoogleSQL use cases](https://cloud.google.com/bigtable/docs/googlesql-overview#use-cases).
|
||||
{{</notice>}}
|
||||
|
||||
[bigtable-googlesql]: https://cloud.google.com/bigtable/docs/googlesql-overview
|
||||
|
||||
|
||||
22
docs/en/resources/tools/utility/update-mcp-settings.md
Normal file
22
docs/en/resources/tools/utility/update-mcp-settings.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# update-mcp-settings
|
||||
|
||||
## Description
|
||||
|
||||
The `update-mcp-settings` tool is a utility that updates the MCP (Model Context Protocol) settings file with the necessary environment variables for a given tool. This is particularly useful when you need to configure a tool with specific environment variables being set previously in chat for AlloyDB Control Plane.
|
||||
|
||||
## Configuration
|
||||
|
||||
To use the `update-mcp-settings` tool, you need to configure it in your `toolbox.yaml` file. Here is an example configuration:
|
||||
|
||||
```yaml
|
||||
tools:
|
||||
update-mcp-settings-tool:
|
||||
kind: update-mcp-settings
|
||||
description: "Run this tool to update mcp json file prebuilt tool for data plane with right parameters ALLOYDB_POSTGRES_PROJECT, ALLOYDB_POSTGRES_REGION, ALLOYDB_POSTGRES_CLUSTER, ALLOYDB_POSTGRES_INSTANCE, ALLOYDB_POSTGRES_DATABASE, ALLOYDB_POSTGRES_USER, ALLOYDB_POSTGRES_PASSWORD. Identify the mcp settings json file or ask user to share it's full path. Run this tool once cluster and instance creation is done."
|
||||
```
|
||||
|
||||
## Reference
|
||||
| **field** | **type** | **required** | **description** |
|
||||
|-------------|:------------------------------------------:|:------------:|--------------------------------------------------------------------------------------------------|
|
||||
| kind | string | true | Must be "update-mcp-settings". |
|
||||
| description | string | true | Description of the tool that is passed to the LLM. |
|
||||
37
docs/en/resources/tools/utility/wait.md
Normal file
37
docs/en/resources/tools/utility/wait.md
Normal file
@@ -0,0 +1,37 @@
|
||||
---
|
||||
title: "wait"
|
||||
type: docs
|
||||
weight: 1
|
||||
description: >
|
||||
A "wait" tool pauses execution for a specified duration.
|
||||
aliases:
|
||||
- /resources/tools/utility/wait
|
||||
---
|
||||
|
||||
## About
|
||||
|
||||
A `wait` tool pauses execution for a specified duration. This can be useful in workflows where a delay is needed between steps.
|
||||
|
||||
`wait` takes one input parameter `duration` which is a string representing the time to wait (e.g., "10s", "2m", "1h").
|
||||
|
||||
{{% notice info %}}
|
||||
This tool is intended for developer assistant workflows with human-in-the-loop and shouldn't be used for production agents.
|
||||
{{% /notice %}}
|
||||
|
||||
## Example
|
||||
|
||||
```yaml
|
||||
tools:
|
||||
wait_for_tool:
|
||||
kind: wait
|
||||
description: Use this tool to pause execution for a specified duration.
|
||||
timeout: 30s
|
||||
```
|
||||
|
||||
## Reference
|
||||
|
||||
| **field** | **type** | **required** | **description** |
|
||||
|-------------|:------------------------------------------:|:------------:|--------------------------------------------------------------------------------------------------|
|
||||
| kind | string | true | Must be "wait". |
|
||||
| description | string | true | Description of the tool that is passed to the LLM. |
|
||||
| timeout | string | true | The default duration the tool can wait for. |
|
||||
@@ -6,3 +6,10 @@ icon: fa-brands fa-golang
|
||||
manualLink: "https://github.com/googleapis/mcp-toolbox-sdk-go"
|
||||
manualLinkTarget: _blank
|
||||
---
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<link rel="canonical" href="https://github.com/googleapis/mcp-toolbox-sdk-go"/>
|
||||
<meta http-equiv="refresh" content="0;url=https://github.com/googleapis/mcp-toolbox-sdk-go"/>
|
||||
</head>
|
||||
</html>
|
||||
|
||||
@@ -5,4 +5,11 @@ description: Javascript client SDK
|
||||
icon: fa-brands fa-node-js
|
||||
manualLink: "https://github.com/googleapis/mcp-toolbox-sdk-js"
|
||||
manualLinkTarget: _blank
|
||||
---
|
||||
---
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<link rel="canonical" href="https://github.com/googleapis/mcp-toolbox-sdk-js"/>
|
||||
<meta http-equiv="refresh" content="0;url=https://github.com/googleapis/mcp-toolbox-sdk-js"/>
|
||||
</head>
|
||||
</html>
|
||||
|
||||
@@ -5,4 +5,11 @@ description: Python client SDK
|
||||
icon: fa-brands fa-python
|
||||
manualLink: "https://github.com/googleapis/mcp-toolbox-sdk-python"
|
||||
manualLinkTarget: _blank
|
||||
---
|
||||
---
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<link rel="canonical" href="https://github.com/googleapis/mcp-toolbox-sdk-python"/>
|
||||
<meta http-equiv="refresh" content="0;url=hhttps://github.com/googleapis/mcp-toolbox-sdk-python"/>
|
||||
</head>
|
||||
</html>
|
||||
|
||||
@@ -133,6 +133,7 @@ func toolGetHandler(s *Server, w http.ResponseWriter, r *http.Request) {
|
||||
func toolInvokeHandler(s *Server, w http.ResponseWriter, r *http.Request) {
|
||||
ctx, span := s.instrumentation.Tracer.Start(r.Context(), "toolbox/server/tool/invoke")
|
||||
r = r.WithContext(ctx)
|
||||
ctx = util.WithLogger(r.Context(), s.logger)
|
||||
|
||||
toolName := chi.URLParam(r, "toolName")
|
||||
s.logger.DebugContext(ctx, fmt.Sprintf("tool name: %s", toolName))
|
||||
|
||||
@@ -357,6 +357,17 @@ func httpHandler(s *Server, w http.ResponseWriter, r *http.Request) {
|
||||
protocolVersion = v20250326.PROTOCOL_VERSION
|
||||
}
|
||||
|
||||
// check if client have `MCP-Protocol-Version` header
|
||||
headerProtocolVersion := r.Header.Get("MCP-Protocol-Version")
|
||||
if headerProtocolVersion != "" {
|
||||
if !mcp.VerifyProtocolVersion(headerProtocolVersion) {
|
||||
err := fmt.Errorf("invalid protocol version: %s", headerProtocolVersion)
|
||||
_ = render.Render(w, r, newErrResponse(err, http.StatusBadRequest))
|
||||
return
|
||||
}
|
||||
protocolVersion = headerProtocolVersion
|
||||
}
|
||||
|
||||
toolsetName := chi.URLParam(r, "toolsetName")
|
||||
s.logger.DebugContext(ctx, fmt.Sprintf("toolset name: %s", toolsetName))
|
||||
span.SetAttributes(attribute.String("toolset_name", toolsetName))
|
||||
@@ -387,6 +398,7 @@ func httpHandler(s *Server, w http.ResponseWriter, r *http.Request) {
|
||||
id := uuid.New().String()
|
||||
s.logger.DebugContext(ctx, err.Error())
|
||||
render.JSON(w, r, jsonrpc.NewError(id, jsonrpc.PARSE_ERROR, err.Error(), nil))
|
||||
return
|
||||
}
|
||||
|
||||
v, res, err := processMcpMessage(ctx, body, s, protocolVersion, toolsetName)
|
||||
@@ -431,10 +443,6 @@ func processMcpMessage(ctx context.Context, body []byte, s *Server, protocolVers
|
||||
return "", jsonrpc.NewError("", jsonrpc.INTERNAL_ERROR, err.Error(), nil), err
|
||||
}
|
||||
|
||||
if protocolVersion == "" {
|
||||
protocolVersion = v20241105.PROTOCOL_VERSION
|
||||
}
|
||||
|
||||
// Generic baseMessage could either be a JSONRPCNotification or JSONRPCRequest
|
||||
var baseMessage jsonrpc.BaseMessage
|
||||
if err = util.DecodeJSON(bytes.NewBuffer(body), &baseMessage); err != nil {
|
||||
|
||||
@@ -24,15 +24,20 @@ 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"
|
||||
v20250618 "github.com/googleapis/genai-toolbox/internal/server/mcp/v20250618"
|
||||
"github.com/googleapis/genai-toolbox/internal/tools"
|
||||
)
|
||||
|
||||
// LATEST_PROTOCOL_VERSION is the latest version of the MCP protocol supported.
|
||||
// Update the version used in InitializeResponse when this value is updated.
|
||||
const LATEST_PROTOCOL_VERSION = v20250326.PROTOCOL_VERSION
|
||||
const LATEST_PROTOCOL_VERSION = v20250618.PROTOCOL_VERSION
|
||||
|
||||
// SUPPORTED_PROTOCOL_VERSIONS is the MCP protocol versions that are supported.
|
||||
var SUPPORTED_PROTOCOL_VERSIONS = []string{v20241105.PROTOCOL_VERSION, v20250326.PROTOCOL_VERSION}
|
||||
var SUPPORTED_PROTOCOL_VERSIONS = []string{
|
||||
v20241105.PROTOCOL_VERSION,
|
||||
v20250326.PROTOCOL_VERSION,
|
||||
v20250618.PROTOCOL_VERSION,
|
||||
}
|
||||
|
||||
// InitializeResponse runs capability negotiation and protocol version agreement.
|
||||
// This is the Initialization phase of the lifecycle for MCP client-server connections.
|
||||
@@ -61,7 +66,9 @@ func InitializeResponse(ctx context.Context, id jsonrpc.RequestId, body []byte,
|
||||
},
|
||||
},
|
||||
ServerInfo: mcputil.Implementation{
|
||||
Name: mcputil.SERVER_NAME,
|
||||
BaseMetadata: mcputil.BaseMetadata{
|
||||
Name: mcputil.SERVER_NAME,
|
||||
},
|
||||
Version: toolboxVersion,
|
||||
},
|
||||
}
|
||||
@@ -88,12 +95,16 @@ func NotificationHandler(ctx context.Context, body []byte) error {
|
||||
// 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, body []byte) (any, error) {
|
||||
switch mcpVersion {
|
||||
case v20250618.PROTOCOL_VERSION:
|
||||
return v20250618.ProcessMethod(ctx, id, method, toolset, tools, body)
|
||||
case v20250326.PROTOCOL_VERSION:
|
||||
return v20250326.ProcessMethod(ctx, id, method, toolset, tools, body)
|
||||
case v20241105.PROTOCOL_VERSION:
|
||||
return v20241105.ProcessMethod(ctx, id, method, toolset, tools, body)
|
||||
default:
|
||||
err := fmt.Errorf("invalid protocol version: %s", mcpVersion)
|
||||
return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err
|
||||
return v20241105.ProcessMethod(ctx, id, method, toolset, tools, body)
|
||||
}
|
||||
}
|
||||
|
||||
// VerifyProtocolVersion verifies if the version string is valid.
|
||||
func VerifyProtocolVersion(version string) bool {
|
||||
return slices.Contains(SUPPORTED_PROTOCOL_VERSIONS, version)
|
||||
}
|
||||
|
||||
@@ -89,8 +89,22 @@ type ServerCapabilities struct {
|
||||
Tools *ListChanged `json:"tools,omitempty"`
|
||||
}
|
||||
|
||||
// Base interface for metadata with name (identifier) and title (display name) properties.
|
||||
type BaseMetadata struct {
|
||||
// Intended for programmatic or logical use, but used as a display name in past specs
|
||||
// or fallback (if title isn't present).
|
||||
Name string `json:"name"`
|
||||
// Intended for UI and end-user contexts — optimized to be human-readable and easily understood,
|
||||
//even by those unfamiliar with domain-specific terminology.
|
||||
//
|
||||
// If not provided, the name should be used for display (except for Tool,
|
||||
// where `annotations.title` should be given precedence over using `name`,
|
||||
// if present).
|
||||
Title string `json:"title,omitempty"`
|
||||
}
|
||||
|
||||
// Implementation describes the name and version of an MCP implementation.
|
||||
type Implementation struct {
|
||||
Name string `json:"name"`
|
||||
BaseMetadata
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
141
internal/server/mcp/v20250618/method.go
Normal file
141
internal/server/mcp/v20250618/method.go
Normal file
@@ -0,0 +1,141 @@
|
||||
// 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 v20250618
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"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, body []byte) (any, error) {
|
||||
switch method {
|
||||
case TOOLS_LIST:
|
||||
return toolsListHandler(id, toolset, body)
|
||||
case TOOLS_CALL:
|
||||
return toolsCallHandler(ctx, id, tools, body)
|
||||
default:
|
||||
err := fmt.Errorf("invalid method %s", method)
|
||||
return jsonrpc.NewError(id, jsonrpc.METHOD_NOT_FOUND, err.Error(), nil), err
|
||||
}
|
||||
}
|
||||
|
||||
func toolsListHandler(id jsonrpc.RequestId, toolset tools.Toolset, body []byte) (any, error) {
|
||||
var req ListToolsRequest
|
||||
if err := json.Unmarshal(body, &req); err != nil {
|
||||
err = fmt.Errorf("invalid mcp tools list request: %w", err)
|
||||
return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err
|
||||
}
|
||||
|
||||
result := ListToolsResult{
|
||||
Tools: toolset.McpManifest,
|
||||
}
|
||||
return jsonrpc.JSONRPCResponse{
|
||||
Jsonrpc: jsonrpc.JSONRPC_VERSION,
|
||||
Id: id,
|
||||
Result: result,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// toolsCallHandler generate a response for tools call.
|
||||
func toolsCallHandler(ctx context.Context, id jsonrpc.RequestId, tools map[string]tools.Tool, 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
|
||||
}
|
||||
|
||||
var req CallToolRequest
|
||||
if err = json.Unmarshal(body, &req); err != nil {
|
||||
err = fmt.Errorf("invalid mcp tools call request: %w", err)
|
||||
return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err
|
||||
}
|
||||
|
||||
toolName := req.Params.Name
|
||||
toolArgument := req.Params.Arguments
|
||||
logger.DebugContext(ctx, fmt.Sprintf("tool name: %s", toolName))
|
||||
tool, ok := tools[toolName]
|
||||
if !ok {
|
||||
err = fmt.Errorf("invalid tool name: tool with name %q does not exist", toolName)
|
||||
return jsonrpc.NewError(id, jsonrpc.INVALID_PARAMS, err.Error(), nil), err
|
||||
}
|
||||
|
||||
// marshal arguments and decode it using decodeJSON instead to prevent loss between floats/int.
|
||||
aMarshal, err := json.Marshal(toolArgument)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("unable to marshal tools argument: %w", err)
|
||||
return jsonrpc.NewError(id, jsonrpc.INTERNAL_ERROR, err.Error(), nil), err
|
||||
}
|
||||
|
||||
var data map[string]any
|
||||
if err = util.DecodeJSON(bytes.NewBuffer(aMarshal), &data); err != nil {
|
||||
err = fmt.Errorf("unable to decode tools argument: %w", err)
|
||||
return jsonrpc.NewError(id, jsonrpc.INTERNAL_ERROR, err.Error(), nil), err
|
||||
}
|
||||
|
||||
// claimsFromAuth maps the name of the authservice to the claims retrieved from it.
|
||||
// Since MCP doesn't support auth, an empty map will be use every time.
|
||||
claimsFromAuth := make(map[string]map[string]any)
|
||||
|
||||
params, err := tool.ParseParams(data, claimsFromAuth)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("provided parameters were invalid: %w", err)
|
||||
return jsonrpc.NewError(id, jsonrpc.INVALID_PARAMS, err.Error(), nil), err
|
||||
}
|
||||
logger.DebugContext(ctx, fmt.Sprintf("invocation params: %s", params))
|
||||
|
||||
if !tool.Authorized([]string{}) {
|
||||
err = fmt.Errorf("unauthorized Tool call: `authRequired` is set for the target Tool")
|
||||
return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err
|
||||
}
|
||||
|
||||
// run tool invocation and generate response.
|
||||
results, err := tool.Invoke(ctx, params)
|
||||
if err != nil {
|
||||
text := TextContent{
|
||||
Type: "text",
|
||||
Text: err.Error(),
|
||||
}
|
||||
return jsonrpc.JSONRPCResponse{
|
||||
Jsonrpc: jsonrpc.JSONRPC_VERSION,
|
||||
Id: id,
|
||||
Result: CallToolResult{Content: []TextContent{text}, IsError: true},
|
||||
}, nil
|
||||
}
|
||||
|
||||
content := make([]TextContent, 0)
|
||||
for _, d := range results {
|
||||
text := TextContent{Type: "text"}
|
||||
dM, err := json.Marshal(d)
|
||||
if err != nil {
|
||||
text.Text = fmt.Sprintf("fail to marshal: %s, result: %s", err, d)
|
||||
} else {
|
||||
text.Text = string(dM)
|
||||
}
|
||||
content = append(content, text)
|
||||
}
|
||||
|
||||
return jsonrpc.JSONRPCResponse{
|
||||
Jsonrpc: jsonrpc.JSONRPC_VERSION,
|
||||
Id: id,
|
||||
Result: CallToolResult{Content: content},
|
||||
}, nil
|
||||
}
|
||||
180
internal/server/mcp/v20250618/types.go
Normal file
180
internal/server/mcp/v20250618/types.go
Normal file
@@ -0,0 +1,180 @@
|
||||
// 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 v20250618
|
||||
|
||||
import (
|
||||
"github.com/googleapis/genai-toolbox/internal/server/mcp/jsonrpc"
|
||||
"github.com/googleapis/genai-toolbox/internal/tools"
|
||||
)
|
||||
|
||||
// SERVER_NAME is the server name used in Implementation.
|
||||
const SERVER_NAME = "Toolbox"
|
||||
|
||||
// PROTOCOL_VERSION is the version of the MCP protocol in this package.
|
||||
const PROTOCOL_VERSION = "2025-06-18"
|
||||
|
||||
// methods that are supported.
|
||||
const (
|
||||
TOOLS_LIST = "tools/list"
|
||||
TOOLS_CALL = "tools/call"
|
||||
)
|
||||
|
||||
/* Empty result */
|
||||
|
||||
// EmptyResult represents a response that indicates success but carries no data.
|
||||
type EmptyResult jsonrpc.Result
|
||||
|
||||
/* Pagination */
|
||||
|
||||
// Cursor is an opaque token used to represent a cursor for pagination.
|
||||
type Cursor string
|
||||
|
||||
type PaginatedRequest struct {
|
||||
jsonrpc.Request
|
||||
Params struct {
|
||||
// An opaque token representing the current pagination position.
|
||||
// If provided, the server should return results starting after this cursor.
|
||||
Cursor Cursor `json:"cursor,omitempty"`
|
||||
} `json:"params,omitempty"`
|
||||
}
|
||||
|
||||
type PaginatedResult struct {
|
||||
jsonrpc.Result
|
||||
// An opaque token representing the pagination position after the last returned result.
|
||||
// If present, there may be more results available.
|
||||
NextCursor Cursor `json:"nextCursor,omitempty"`
|
||||
}
|
||||
|
||||
/* Tools */
|
||||
|
||||
// Sent from the client to request a list of tools the server has.
|
||||
type ListToolsRequest struct {
|
||||
PaginatedRequest
|
||||
}
|
||||
|
||||
// The server's response to a tools/list request from the client.
|
||||
type ListToolsResult struct {
|
||||
PaginatedResult
|
||||
Tools []tools.McpManifest `json:"tools"`
|
||||
}
|
||||
|
||||
// Used by the client to invoke a tool provided by the server.
|
||||
type CallToolRequest struct {
|
||||
jsonrpc.Request
|
||||
Params struct {
|
||||
Name string `json:"name"`
|
||||
Arguments map[string]any `json:"arguments,omitempty"`
|
||||
} `json:"params,omitempty"`
|
||||
}
|
||||
|
||||
// The sender or recipient of messages and data in a conversation.
|
||||
type Role string
|
||||
|
||||
const (
|
||||
RoleUser Role = "user"
|
||||
RoleAssistant Role = "assistant"
|
||||
)
|
||||
|
||||
// Base for objects that include optional annotations for the client.
|
||||
// The client can use annotations to inform how objects are used or displayed
|
||||
type Annotated struct {
|
||||
Annotations *struct {
|
||||
// Describes who the intended customer of this object or data is.
|
||||
// It can include multiple entries to indicate content useful for multiple
|
||||
// audiences (e.g., `["user", "assistant"]`).
|
||||
Audience []Role `json:"audience,omitempty"`
|
||||
// Describes how important this data is for operating the server.
|
||||
//
|
||||
// A value of 1 means "most important," and indicates that the data is
|
||||
// effectively required, while 0 means "least important," and indicates that
|
||||
// the data is entirely optional.
|
||||
//
|
||||
// @TJS-type number
|
||||
// @minimum 0
|
||||
// @maximum 1
|
||||
Priority float64 `json:"priority,omitempty"`
|
||||
} `json:"annotations,omitempty"`
|
||||
}
|
||||
|
||||
// TextContent represents text provided to or from an LLM.
|
||||
type TextContent struct {
|
||||
Annotated
|
||||
Type string `json:"type"`
|
||||
// The text content of the message.
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
// The server's response to a tool call.
|
||||
//
|
||||
// Any errors that originate from the tool SHOULD be reported inside the result
|
||||
// object, with `isError` set to true, _not_ as an MCP protocol-level error
|
||||
// response. Otherwise, the LLM would not be able to see that an error occurred
|
||||
// and self-correct.
|
||||
//
|
||||
// However, any errors in _finding_ the tool, an error indicating that the
|
||||
// server does not support tool calls, or any other exceptional conditions,
|
||||
// should be reported as an MCP error response.
|
||||
type CallToolResult struct {
|
||||
jsonrpc.Result
|
||||
// Could be either a TextContent, ImageContent, or EmbeddedResources
|
||||
// For Toolbox, we will only be sending TextContent
|
||||
Content []TextContent `json:"content"`
|
||||
// Whether the tool call ended in an error.
|
||||
// If not set, this is assumed to be false (the call was successful).
|
||||
//
|
||||
// Any errors that originate from the tool SHOULD be reported inside the result
|
||||
// object, with `isError` set to true, _not_ as an MCP protocol-level error
|
||||
// response. Otherwise, the LLM would not be able to see that an error occurred
|
||||
// and self-correct.
|
||||
//
|
||||
// However, any errors in _finding_ the tool, an error indicating that the
|
||||
// server does not support tool calls, or any other exceptional conditions,
|
||||
// should be reported as an MCP error response.
|
||||
IsError bool `json:"isError,omitempty"`
|
||||
// An optional JSON object that represents the structured result of the tool call.
|
||||
StructuredContent map[string]any `json:"structuredContent,omitempty"`
|
||||
}
|
||||
|
||||
// Additional properties describing a Tool to clients.
|
||||
//
|
||||
// NOTE: all properties in ToolAnnotations are **hints**.
|
||||
// They are not guaranteed to provide a faithful description of
|
||||
// tool behavior (including descriptive properties like `title`).
|
||||
//
|
||||
// Clients should never make tool use decisions based on ToolAnnotations
|
||||
// received from untrusted servers.
|
||||
type ToolAnnotations struct {
|
||||
// A human-readable title for the tool.
|
||||
Title string `json:"title,omitempty"`
|
||||
// If true, the tool does not modify its environment.
|
||||
// Default: false
|
||||
ReadOnlyHint bool `json:"readOnlyHint,omitempty"`
|
||||
// If true, the tool may perform destructive updates to its environment.
|
||||
// If false, the tool performs only additive updates.
|
||||
// (This property is meaningful only when `readOnlyHint == false`)
|
||||
// Default: true
|
||||
DestructiveHint bool `json:"destructiveHint,omitempty"`
|
||||
// If true, calling the tool repeatedly with the same arguments
|
||||
// will have no additional effect on the its environment.
|
||||
// (This property is meaningful only when `readOnlyHint == false`)
|
||||
// Default: false
|
||||
IdempotentHint bool `json:"idempotentHint,omitempty"`
|
||||
// If true, this tool may interact with an "open world" of external
|
||||
// entities. If false, the tool's domain of interaction is closed.
|
||||
// For example, the world of a web search tool is open, whereas that
|
||||
// of a memory tool is not.
|
||||
// Default: true
|
||||
OpenWorldHint bool `json:"openWorldHint,omitempty"`
|
||||
}
|
||||
@@ -36,6 +36,7 @@ import (
|
||||
const jsonrpcVersion = "2.0"
|
||||
const protocolVersion20241105 = "2024-11-05"
|
||||
const protocolVersion20250326 = "2025-03-26"
|
||||
const protocolVersion20250618 = "2025-06-18"
|
||||
const serverName = "Toolbox"
|
||||
|
||||
var tool1InputSchema = map[string]any{
|
||||
@@ -254,7 +255,7 @@ func TestMcpEndpoint(t *testing.T) {
|
||||
initWant map[string]any
|
||||
}{
|
||||
{
|
||||
name: "verson 2024-11-05",
|
||||
name: "version 2024-11-05",
|
||||
protocol: protocolVersion20241105,
|
||||
idHeader: false,
|
||||
initWant: map[string]any{
|
||||
@@ -270,7 +271,7 @@ func TestMcpEndpoint(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "verson 2025-03-26",
|
||||
name: "version 2025-03-26",
|
||||
protocol: protocolVersion20250326,
|
||||
idHeader: true,
|
||||
initWant: map[string]any{
|
||||
@@ -285,6 +286,22 @@ func TestMcpEndpoint(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "version 2025-06-18",
|
||||
protocol: protocolVersion20250618,
|
||||
idHeader: false,
|
||||
initWant: map[string]any{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "mcp-initialize",
|
||||
"result": map[string]any{
|
||||
"protocolVersion": "2025-06-18",
|
||||
"capabilities": map[string]any{
|
||||
"tools": map[string]any{"listChanged": false},
|
||||
},
|
||||
"serverInfo": map[string]any{"name": serverName, "version": fakeVersionString},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, vtc := range versTestCases {
|
||||
t.Run(vtc.name, func(t *testing.T) {
|
||||
@@ -295,6 +312,10 @@ func TestMcpEndpoint(t *testing.T) {
|
||||
header["Mcp-Session-Id"] = sessionId
|
||||
}
|
||||
|
||||
if vtc.protocol == protocolVersion20250618 {
|
||||
header["MCP-Protocol-Version"] = vtc.protocol
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
url string
|
||||
@@ -481,7 +502,7 @@ func TestMcpEndpoint(t *testing.T) {
|
||||
t.Fatalf("unexpected error during marshaling of body")
|
||||
}
|
||||
|
||||
if vtc.protocol == protocolVersion20250326 && len(header) == 0 {
|
||||
if vtc.protocol != protocolVersion20241105 && len(header) == 0 {
|
||||
t.Fatalf("header is missing")
|
||||
}
|
||||
|
||||
@@ -514,6 +535,33 @@ 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)
|
||||
defer shutdown()
|
||||
ts := runServer(r, false)
|
||||
defer ts.Close()
|
||||
|
||||
header := map[string]string{}
|
||||
header["MCP-Protocol-Version"] = "foo"
|
||||
|
||||
resp, body, err := runRequest(ts, http.MethodPost, "/", nil, header)
|
||||
if resp.Status != "400 Bad Request" {
|
||||
t.Fatalf("unexpected status: %s", resp.Status)
|
||||
}
|
||||
var got map[string]any
|
||||
if err := json.Unmarshal(body, &got); err != nil {
|
||||
t.Fatalf("unexpected error unmarshalling body: %s", err)
|
||||
}
|
||||
want := "invalid protocol version: foo"
|
||||
if got["error"] != want {
|
||||
t.Fatalf("unexpected error message: got %s, want %s", got["error"], want)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error during request: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteEndpoint(t *testing.T) {
|
||||
toolsMap, toolsets := map[string]tools.Tool{}, map[string]tools.Toolset{}
|
||||
r, shutdown := setUpServer(t, "mcp", toolsMap, toolsets)
|
||||
|
||||
@@ -109,11 +109,17 @@ func parseFromAuthService(paramAuthServices []ParamAuthService, claimsMap map[st
|
||||
return nil, fmt.Errorf("missing or invalid authentication header")
|
||||
}
|
||||
|
||||
// CheckParamRequired checks if a parameter is required based on the required and default field.
|
||||
func CheckParamRequired(required bool, defaultV any) bool {
|
||||
return required && defaultV == nil
|
||||
}
|
||||
|
||||
// ParseParams is a helper function for parsing Parameters from an arbitraryJSON object.
|
||||
func ParseParams(ps Parameters, data map[string]any, claimsMap map[string]map[string]any) (ParamValues, error) {
|
||||
params := make([]ParamValue, 0, len(ps))
|
||||
for _, p := range ps {
|
||||
var v any
|
||||
var v, newV any
|
||||
var err error
|
||||
paramAuthServices := p.GetAuthServices()
|
||||
name := p.GetName()
|
||||
if len(paramAuthServices) == 0 {
|
||||
@@ -122,21 +128,23 @@ func ParseParams(ps Parameters, data map[string]any, claimsMap map[string]map[st
|
||||
v, ok = data[name]
|
||||
if !ok {
|
||||
v = p.GetDefault()
|
||||
if v == nil {
|
||||
// if the parameter is required and no value given, throw an error
|
||||
if CheckParamRequired(p.GetRequired(), v) {
|
||||
return nil, fmt.Errorf("parameter %q is required", name)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// parse authenticated parameter
|
||||
var err error
|
||||
v, err = parseFromAuthService(paramAuthServices, claimsMap)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing authenticated parameter %q: %w", name, err)
|
||||
}
|
||||
}
|
||||
newV, err := p.Parse(v)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to parse value for %q: %w", name, err)
|
||||
if v != nil {
|
||||
newV, err = p.Parse(v)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to parse value for %q: %w", name, err)
|
||||
}
|
||||
}
|
||||
params = append(params, ParamValue{Name: name, Value: newV})
|
||||
}
|
||||
@@ -248,6 +256,7 @@ type Parameter interface {
|
||||
GetName() string
|
||||
GetType() string
|
||||
GetDefault() any
|
||||
GetRequired() bool
|
||||
GetAuthServices() []ParamAuthService
|
||||
Parse(any) (any, error)
|
||||
Manifest() ParameterManifest
|
||||
@@ -378,7 +387,7 @@ func (ps Parameters) McpManifest() McpToolsSchema {
|
||||
name := p.GetName()
|
||||
properties[name] = p.McpManifest()
|
||||
// parameters that doesn't have a default value are added to the required field
|
||||
if p.GetDefault() == nil {
|
||||
if CheckParamRequired(p.GetRequired(), p.GetDefault()) {
|
||||
required = append(required, name)
|
||||
}
|
||||
}
|
||||
@@ -412,6 +421,7 @@ type CommonParameter struct {
|
||||
Name string `yaml:"name" validate:"required"`
|
||||
Type string `yaml:"type" validate:"required"`
|
||||
Desc string `yaml:"description" validate:"required"`
|
||||
Required *bool `yaml:"required"`
|
||||
AuthServices []ParamAuthService `yaml:"authServices"`
|
||||
AuthSources []ParamAuthService `yaml:"authSources"` // Deprecated: Kept for compatibility.
|
||||
}
|
||||
@@ -426,6 +436,15 @@ func (p *CommonParameter) GetType() string {
|
||||
return p.Type
|
||||
}
|
||||
|
||||
// GetRequired returns the type specified for the Parameter.
|
||||
func (p *CommonParameter) GetRequired() bool {
|
||||
// parameters are defaulted to required
|
||||
if p.Required == nil {
|
||||
return true
|
||||
}
|
||||
return *p.Required
|
||||
}
|
||||
|
||||
// McpManifest returns the MCP manifest for the Parameter.
|
||||
func (p *CommonParameter) McpManifest() ParameterMcpManifest {
|
||||
return ParameterMcpManifest{
|
||||
@@ -475,6 +494,19 @@ func NewStringParameterWithDefault(name string, defaultV, desc string) *StringPa
|
||||
}
|
||||
}
|
||||
|
||||
// NewStringParameterWithRequired is a convenience function for initializing a StringParameter.
|
||||
func NewStringParameterWithRequired(name string, desc string, required bool) *StringParameter {
|
||||
return &StringParameter{
|
||||
CommonParameter: CommonParameter{
|
||||
Name: name,
|
||||
Type: typeString,
|
||||
Desc: desc,
|
||||
Required: &required,
|
||||
AuthServices: nil,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewStringParameterWithAuth is a convenience function for initializing a StringParameter with a list of ParamAuthService.
|
||||
func NewStringParameterWithAuth(name string, desc string, authServices []ParamAuthService) *StringParameter {
|
||||
return &StringParameter{
|
||||
@@ -522,11 +554,11 @@ func (p *StringParameter) Manifest() ParameterManifest {
|
||||
for i, a := range p.AuthServices {
|
||||
authNames[i] = a.Name
|
||||
}
|
||||
required := p.Default == nil
|
||||
r := CheckParamRequired(p.GetRequired(), p.GetDefault())
|
||||
return ParameterManifest{
|
||||
Name: p.Name,
|
||||
Type: p.Type,
|
||||
Required: required,
|
||||
Required: r,
|
||||
Description: p.Desc,
|
||||
AuthServices: authNames,
|
||||
}
|
||||
@@ -557,6 +589,19 @@ func NewIntParameterWithDefault(name string, defaultV int, desc string) *IntPara
|
||||
}
|
||||
}
|
||||
|
||||
// NewIntParameterWithRequired is a convenience function for initializing a IntParameter.
|
||||
func NewIntParameterWithRequired(name string, desc string, required bool) *IntParameter {
|
||||
return &IntParameter{
|
||||
CommonParameter: CommonParameter{
|
||||
Name: name,
|
||||
Type: typeInt,
|
||||
Desc: desc,
|
||||
Required: &required,
|
||||
AuthServices: nil,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewIntParameterWithAuth is a convenience function for initializing a IntParameter with a list of ParamAuthService.
|
||||
func NewIntParameterWithAuth(name string, desc string, authServices []ParamAuthService) *IntParameter {
|
||||
return &IntParameter{
|
||||
@@ -616,11 +661,11 @@ func (p *IntParameter) Manifest() ParameterManifest {
|
||||
for i, a := range p.AuthServices {
|
||||
authNames[i] = a.Name
|
||||
}
|
||||
required := p.Default == nil
|
||||
r := CheckParamRequired(p.GetRequired(), p.GetDefault())
|
||||
return ParameterManifest{
|
||||
Name: p.Name,
|
||||
Type: p.Type,
|
||||
Required: required,
|
||||
Required: r,
|
||||
Description: p.Desc,
|
||||
AuthServices: authNames,
|
||||
}
|
||||
@@ -651,6 +696,19 @@ func NewFloatParameterWithDefault(name string, defaultV float64, desc string) *F
|
||||
}
|
||||
}
|
||||
|
||||
// NewFloatParameterWithRequired is a convenience function for initializing a FloatParameter.
|
||||
func NewFloatParameterWithRequired(name string, desc string, required bool) *FloatParameter {
|
||||
return &FloatParameter{
|
||||
CommonParameter: CommonParameter{
|
||||
Name: name,
|
||||
Type: typeFloat,
|
||||
Desc: desc,
|
||||
Required: &required,
|
||||
AuthServices: nil,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewFloatParameterWithAuth is a convenience function for initializing a FloatParameter with a list of ParamAuthService.
|
||||
func NewFloatParameterWithAuth(name string, desc string, authServices []ParamAuthService) *FloatParameter {
|
||||
return &FloatParameter{
|
||||
@@ -708,11 +766,11 @@ func (p *FloatParameter) Manifest() ParameterManifest {
|
||||
for i, a := range p.AuthServices {
|
||||
authNames[i] = a.Name
|
||||
}
|
||||
required := p.Default == nil
|
||||
r := CheckParamRequired(p.GetRequired(), p.GetDefault())
|
||||
return ParameterManifest{
|
||||
Name: p.Name,
|
||||
Type: p.Type,
|
||||
Required: required,
|
||||
Required: r,
|
||||
Description: p.Desc,
|
||||
AuthServices: authNames,
|
||||
}
|
||||
@@ -743,6 +801,19 @@ func NewBooleanParameterWithDefault(name string, defaultV bool, desc string) *Bo
|
||||
}
|
||||
}
|
||||
|
||||
// NewBooleanParameterWithRequired is a convenience function for initializing a BooleanParameter.
|
||||
func NewBooleanParameterWithRequired(name string, desc string, required bool) *BooleanParameter {
|
||||
return &BooleanParameter{
|
||||
CommonParameter: CommonParameter{
|
||||
Name: name,
|
||||
Type: typeBool,
|
||||
Desc: desc,
|
||||
Required: &required,
|
||||
AuthServices: nil,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewBooleanParameterWithAuth is a convenience function for initializing a BooleanParameter with a list of ParamAuthService.
|
||||
func NewBooleanParameterWithAuth(name string, desc string, authServices []ParamAuthService) *BooleanParameter {
|
||||
return &BooleanParameter{
|
||||
@@ -789,11 +860,11 @@ func (p *BooleanParameter) Manifest() ParameterManifest {
|
||||
for i, a := range p.AuthServices {
|
||||
authNames[i] = a.Name
|
||||
}
|
||||
required := p.Default == nil
|
||||
r := CheckParamRequired(p.GetRequired(), p.GetDefault())
|
||||
return ParameterManifest{
|
||||
Name: p.Name,
|
||||
Type: p.Type,
|
||||
Required: required,
|
||||
Required: r,
|
||||
Description: p.Desc,
|
||||
AuthServices: authNames,
|
||||
}
|
||||
@@ -826,6 +897,20 @@ func NewArrayParameterWithDefault(name string, defaultV []any, desc string, item
|
||||
}
|
||||
}
|
||||
|
||||
// NewArrayParameterWithRequired is a convenience function for initializing a ArrayParameter with default value.
|
||||
func NewArrayParameterWithRequired(name string, desc string, required bool, items Parameter) *ArrayParameter {
|
||||
return &ArrayParameter{
|
||||
CommonParameter: CommonParameter{
|
||||
Name: name,
|
||||
Type: typeArray,
|
||||
Desc: desc,
|
||||
Required: &required,
|
||||
AuthServices: nil,
|
||||
},
|
||||
Items: items,
|
||||
}
|
||||
}
|
||||
|
||||
// NewArrayParameterWithAuth is a convenience function for initializing a ArrayParameter with a list of ParamAuthService.
|
||||
func NewArrayParameterWithAuth(name string, desc string, items Parameter, authServices []ParamAuthService) *ArrayParameter {
|
||||
return &ArrayParameter{
|
||||
@@ -910,12 +995,13 @@ func (p *ArrayParameter) Manifest() ParameterManifest {
|
||||
authNames[i] = a.Name
|
||||
}
|
||||
items := p.Items.Manifest()
|
||||
required := p.Default == nil
|
||||
items.Required = required
|
||||
// if required value is true, or there's no default value
|
||||
r := CheckParamRequired(p.GetRequired(), p.GetDefault())
|
||||
items.Required = r
|
||||
return ParameterManifest{
|
||||
Name: p.Name,
|
||||
Type: p.Type,
|
||||
Required: required,
|
||||
Required: r,
|
||||
Description: p.Desc,
|
||||
AuthServices: authNames,
|
||||
Items: &items,
|
||||
|
||||
@@ -50,6 +50,20 @@ func TestParametersMarshal(t *testing.T) {
|
||||
tools.NewStringParameter("my_string", "this param is a string"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "string not required",
|
||||
in: []map[string]any{
|
||||
{
|
||||
"name": "my_string",
|
||||
"type": "string",
|
||||
"description": "this param is a string",
|
||||
"required": false,
|
||||
},
|
||||
},
|
||||
want: tools.Parameters{
|
||||
tools.NewStringParameterWithRequired("my_string", "this param is a string", false),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "int",
|
||||
in: []map[string]any{
|
||||
@@ -63,6 +77,20 @@ func TestParametersMarshal(t *testing.T) {
|
||||
tools.NewIntParameter("my_integer", "this param is an int"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "int not required",
|
||||
in: []map[string]any{
|
||||
{
|
||||
"name": "my_integer",
|
||||
"type": "integer",
|
||||
"description": "this param is an int",
|
||||
"required": false,
|
||||
},
|
||||
},
|
||||
want: tools.Parameters{
|
||||
tools.NewIntParameterWithRequired("my_integer", "this param is an int", false),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "float",
|
||||
in: []map[string]any{
|
||||
@@ -76,6 +104,20 @@ func TestParametersMarshal(t *testing.T) {
|
||||
tools.NewFloatParameter("my_float", "my param is a float"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "float not required",
|
||||
in: []map[string]any{
|
||||
{
|
||||
"name": "my_float",
|
||||
"type": "float",
|
||||
"description": "my param is a float",
|
||||
"required": false,
|
||||
},
|
||||
},
|
||||
want: tools.Parameters{
|
||||
tools.NewFloatParameterWithRequired("my_float", "my param is a float", false),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "bool",
|
||||
in: []map[string]any{
|
||||
@@ -89,6 +131,20 @@ func TestParametersMarshal(t *testing.T) {
|
||||
tools.NewBooleanParameter("my_bool", "this param is a boolean"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "bool not required",
|
||||
in: []map[string]any{
|
||||
{
|
||||
"name": "my_bool",
|
||||
"type": "boolean",
|
||||
"description": "this param is a boolean",
|
||||
"required": false,
|
||||
},
|
||||
},
|
||||
want: tools.Parameters{
|
||||
tools.NewBooleanParameterWithRequired("my_bool", "this param is a boolean", false),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "string array",
|
||||
in: []map[string]any{
|
||||
@@ -107,6 +163,25 @@ func TestParametersMarshal(t *testing.T) {
|
||||
tools.NewArrayParameter("my_array", "this param is an array of strings", tools.NewStringParameter("my_string", "string item")),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "string array not required",
|
||||
in: []map[string]any{
|
||||
{
|
||||
"name": "my_array",
|
||||
"type": "array",
|
||||
"description": "this param is an array of strings",
|
||||
"required": false,
|
||||
"items": map[string]string{
|
||||
"name": "my_string",
|
||||
"type": "string",
|
||||
"description": "string item",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: tools.Parameters{
|
||||
tools.NewArrayParameterWithRequired("my_array", "this param is an array of strings", false, tools.NewStringParameter("my_string", "string item")),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "float array",
|
||||
in: []map[string]any{
|
||||
@@ -673,6 +748,38 @@ func TestParametersParse(t *testing.T) {
|
||||
in: map[string]any{},
|
||||
want: tools.ParamValues{tools.ParamValue{Name: "my_bool", Value: true}},
|
||||
},
|
||||
{
|
||||
name: "string not required",
|
||||
params: tools.Parameters{
|
||||
tools.NewStringParameterWithRequired("my_string", "this param is a string", false),
|
||||
},
|
||||
in: map[string]any{},
|
||||
want: tools.ParamValues{tools.ParamValue{Name: "my_string", Value: nil}},
|
||||
},
|
||||
{
|
||||
name: "int not required",
|
||||
params: tools.Parameters{
|
||||
tools.NewIntParameterWithRequired("my_int", "this param is an int", false),
|
||||
},
|
||||
in: map[string]any{},
|
||||
want: tools.ParamValues{tools.ParamValue{Name: "my_int", Value: nil}},
|
||||
},
|
||||
{
|
||||
name: "float not required",
|
||||
params: tools.Parameters{
|
||||
tools.NewFloatParameterWithRequired("my_float", "this param is a float", false),
|
||||
},
|
||||
in: map[string]any{},
|
||||
want: tools.ParamValues{tools.ParamValue{Name: "my_float", Value: nil}},
|
||||
},
|
||||
{
|
||||
name: "bool not required",
|
||||
params: tools.Parameters{
|
||||
tools.NewBooleanParameterWithRequired("my_bool", "this param is a bool", false),
|
||||
},
|
||||
in: map[string]any{},
|
||||
want: tools.ParamValues{tools.ParamValue{Name: "my_bool", Value: nil}},
|
||||
},
|
||||
}
|
||||
for _, tc := range tcs {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
@@ -1003,6 +1110,38 @@ func TestParamManifest(t *testing.T) {
|
||||
Items: &tools.ParameterManifest{Name: "foo-string", Type: "string", Required: false, Description: "bar", AuthServices: []string{}},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "string not required",
|
||||
in: tools.NewStringParameterWithRequired("foo-string", "bar", false),
|
||||
want: tools.ParameterManifest{Name: "foo-string", Type: "string", Required: false, Description: "bar", AuthServices: []string{}},
|
||||
},
|
||||
{
|
||||
name: "int not required",
|
||||
in: tools.NewIntParameterWithRequired("foo-int", "bar", false),
|
||||
want: tools.ParameterManifest{Name: "foo-int", Type: "integer", Required: false, Description: "bar", AuthServices: []string{}},
|
||||
},
|
||||
{
|
||||
name: "float not required",
|
||||
in: tools.NewFloatParameterWithRequired("foo-float", "bar", false),
|
||||
want: tools.ParameterManifest{Name: "foo-float", Type: "float", Required: false, Description: "bar", AuthServices: []string{}},
|
||||
},
|
||||
{
|
||||
name: "boolean not required",
|
||||
in: tools.NewBooleanParameterWithRequired("foo-bool", "bar", false),
|
||||
want: tools.ParameterManifest{Name: "foo-bool", Type: "boolean", Required: false, Description: "bar", AuthServices: []string{}},
|
||||
},
|
||||
{
|
||||
name: "array not required",
|
||||
in: tools.NewArrayParameterWithRequired("foo-array", "bar", false, tools.NewStringParameter("foo-string", "bar")),
|
||||
want: tools.ParameterManifest{
|
||||
Name: "foo-array",
|
||||
Type: "array",
|
||||
Required: false,
|
||||
Description: "bar",
|
||||
AuthServices: []string{},
|
||||
Items: &tools.ParameterManifest{Name: "foo-string", Type: "string", Required: false, Description: "bar", AuthServices: []string{}},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range tcs {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
@@ -1071,6 +1210,8 @@ func TestMcpManifest(t *testing.T) {
|
||||
in: tools.Parameters{
|
||||
tools.NewStringParameterWithDefault("foo-string", "foo", "bar"),
|
||||
tools.NewStringParameter("foo-string2", "bar"),
|
||||
tools.NewStringParameterWithRequired("foo-string-req", "bar", true),
|
||||
tools.NewStringParameterWithRequired("foo-string-not-req", "bar", false),
|
||||
tools.NewIntParameterWithDefault("foo-int", 1, "bar"),
|
||||
tools.NewIntParameter("foo-int2", "bar"),
|
||||
tools.NewArrayParameterWithDefault("foo-array", []any{"hello", "world"}, "bar", tools.NewStringParameter("foo-string", "bar")),
|
||||
@@ -1079,10 +1220,12 @@ func TestMcpManifest(t *testing.T) {
|
||||
want: tools.McpToolsSchema{
|
||||
Type: "object",
|
||||
Properties: map[string]tools.ParameterMcpManifest{
|
||||
"foo-string": tools.ParameterMcpManifest{Type: "string", Description: "bar"},
|
||||
"foo-string2": tools.ParameterMcpManifest{Type: "string", Description: "bar"},
|
||||
"foo-int": tools.ParameterMcpManifest{Type: "integer", Description: "bar"},
|
||||
"foo-int2": tools.ParameterMcpManifest{Type: "integer", Description: "bar"},
|
||||
"foo-string": tools.ParameterMcpManifest{Type: "string", Description: "bar"},
|
||||
"foo-string2": tools.ParameterMcpManifest{Type: "string", Description: "bar"},
|
||||
"foo-string-req": tools.ParameterMcpManifest{Type: "string", Description: "bar"},
|
||||
"foo-string-not-req": tools.ParameterMcpManifest{Type: "string", Description: "bar"},
|
||||
"foo-int": tools.ParameterMcpManifest{Type: "integer", Description: "bar"},
|
||||
"foo-int2": tools.ParameterMcpManifest{Type: "integer", Description: "bar"},
|
||||
"foo-array": tools.ParameterMcpManifest{
|
||||
Type: "array",
|
||||
Description: "bar",
|
||||
@@ -1094,7 +1237,7 @@ func TestMcpManifest(t *testing.T) {
|
||||
Items: &tools.ParameterMcpManifest{Type: "string", Description: "bar"},
|
||||
},
|
||||
},
|
||||
Required: []string{"foo-string2", "foo-int2", "foo-array2"},
|
||||
Required: []string{"foo-string2", "foo-string-req", "foo-int2", "foo-array2"},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1502,3 +1645,45 @@ func TestFailResolveTemplateParameters(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckParamRequired(t *testing.T) {
|
||||
tcs := []struct {
|
||||
name string
|
||||
required bool
|
||||
defaultV any
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "required and no default",
|
||||
required: true,
|
||||
defaultV: nil,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "required and default",
|
||||
required: true,
|
||||
defaultV: "foo",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "not required and no default",
|
||||
required: false,
|
||||
defaultV: nil,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "not required and default",
|
||||
required: false,
|
||||
defaultV: "foo",
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, tc := range tcs {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got := tools.CheckParamRequired(tc.required, tc.defaultV)
|
||||
if got != tc.want {
|
||||
t.Fatalf("got %v, want %v", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
209
internal/tools/utility/envvariable/setenvvariable.go
Normal file
209
internal/tools/utility/envvariable/setenvvariable.go
Normal file
@@ -0,0 +1,209 @@
|
||||
// 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 setenvvariable
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
yaml "github.com/goccy/go-yaml"
|
||||
"github.com/googleapis/genai-toolbox/internal/sources"
|
||||
"github.com/googleapis/genai-toolbox/internal/tools"
|
||||
)
|
||||
|
||||
const kind string = "update-mcp-settings"
|
||||
|
||||
func init() {
|
||||
if !tools.Register(kind, newConfig) {
|
||||
panic(fmt.Sprintf("tool kind %q already registered", kind))
|
||||
}
|
||||
}
|
||||
|
||||
func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.ToolConfig, error) {
|
||||
actual := Config{Name: name}
|
||||
if err := decoder.DecodeContext(ctx, &actual); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return actual, nil
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Name string `yaml:"name" validate:"required"`
|
||||
Kind string `yaml:"kind" validate:"required"`
|
||||
Description string `yaml:"description" validate:"required"`
|
||||
AuthRequired []string `yaml:"authRequired"`
|
||||
}
|
||||
|
||||
var _ tools.ToolConfig = Config{}
|
||||
|
||||
func (cfg Config) ToolConfigKind() string {
|
||||
return kind
|
||||
}
|
||||
|
||||
func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) {
|
||||
projectIDParam := tools.NewStringParameter("ALLOYDB_POSTGRES_PROJECT", "The Google Cloud project ID.")
|
||||
regionParam := tools.NewStringParameter("ALLOYDB_POSTGRES_REGION", "The region for AlloyDB.")
|
||||
clusterParam := tools.NewStringParameter("ALLOYDB_POSTGRES_CLUSTER", "The AlloyDB cluster name.")
|
||||
instanceParam := tools.NewStringParameter("ALLOYDB_POSTGRES_INSTANCE", "The AlloyDB instance name.")
|
||||
databaseParam := tools.NewStringParameter("ALLOYDB_POSTGRES_DATABASE", "The AlloyDB database name (defaults to 'postgres').")
|
||||
userParam := tools.NewStringParameter("ALLOYDB_POSTGRES_USER", "The database username.")
|
||||
passwordParam := tools.NewStringParameter("ALLOYDB_POSTGRES_PASSWORD", "The database password.")
|
||||
mcpSettingsFile := tools.NewStringParameter("mcpSettingsFile", "The MCP Settings json file which contains information about server to run for the IDE")
|
||||
|
||||
parameters := tools.Parameters{
|
||||
projectIDParam,
|
||||
regionParam,
|
||||
clusterParam,
|
||||
instanceParam,
|
||||
databaseParam,
|
||||
userParam,
|
||||
passwordParam,
|
||||
mcpSettingsFile,
|
||||
}
|
||||
|
||||
mcpManifest := tools.McpManifest{
|
||||
Name: cfg.Name,
|
||||
Description: cfg.Description,
|
||||
InputSchema: parameters.McpManifest(),
|
||||
}
|
||||
|
||||
t := Tool{
|
||||
Name: cfg.Name,
|
||||
Kind: kind,
|
||||
Parameters: parameters,
|
||||
manifest: tools.Manifest{Description: cfg.Description, Parameters: parameters.Manifest(), AuthRequired: cfg.AuthRequired},
|
||||
mcpManifest: mcpManifest,
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// validate interface
|
||||
var _ tools.Tool = Tool{}
|
||||
|
||||
type Tool struct {
|
||||
Name string
|
||||
Kind string
|
||||
Parameters tools.Parameters
|
||||
manifest tools.Manifest
|
||||
mcpManifest tools.McpManifest
|
||||
}
|
||||
|
||||
func (t Tool) Invoke(ctx context.Context, params tools.ParamValues) ([]any, error) {
|
||||
paramsMap := params.AsMap()
|
||||
mcpSettingsFile, ok := paramsMap["mcpSettingsFile"]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("mcpSettingsFile not found in params")
|
||||
}
|
||||
|
||||
mcpSettingsFileStr, ok := mcpSettingsFile.(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("mcpSettingsFile is not a string")
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(mcpSettingsFileStr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read mcp settings file: %w", err)
|
||||
}
|
||||
|
||||
var mcpSettings map[string]interface{}
|
||||
if err := json.Unmarshal(data, &mcpSettings); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal mcp settings file: %w", err)
|
||||
}
|
||||
|
||||
mcpServers, ok := mcpSettings["mcpServers"].(map[string]interface{})
|
||||
if !ok {
|
||||
if servers, found := mcpSettings["servers"].(map[string]interface{}); found {
|
||||
mcpServers = servers
|
||||
} else {
|
||||
mcpServers = make(map[string]interface{})
|
||||
mcpSettings["mcpServers"] = mcpServers
|
||||
}
|
||||
}
|
||||
|
||||
var targetServer map[string]interface{}
|
||||
var targetServerName string
|
||||
for serverName, server := range mcpServers {
|
||||
serverMap, ok := server.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
args, ok := serverMap["args"].([]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
for _, arg := range args {
|
||||
if argStr, ok := arg.(string); ok && argStr == "alloydb-postgres" {
|
||||
targetServer = serverMap
|
||||
targetServerName = serverName
|
||||
break
|
||||
}
|
||||
}
|
||||
if targetServer != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if targetServer == nil {
|
||||
targetServerName = "alloydb"
|
||||
targetServer = make(map[string]interface{})
|
||||
targetServer["args"] = []interface{}{"--prebuilt", "alloydb-postgres", "--stdio"}
|
||||
mcpServers[targetServerName] = targetServer
|
||||
}
|
||||
|
||||
if _, ok := targetServer["command"]; !ok {
|
||||
targetServer["command"] = "./PATH/TO/toolbox"
|
||||
}
|
||||
|
||||
env, ok := targetServer["env"].(map[string]interface{})
|
||||
if !ok {
|
||||
env = make(map[string]interface{})
|
||||
targetServer["env"] = env
|
||||
}
|
||||
|
||||
for key, value := range paramsMap {
|
||||
if key != "mcpSettingsFile" {
|
||||
env[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
updatedData, err := json.MarshalIndent(mcpSettings, "", " ")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal mcp settings file: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(mcpSettingsFileStr, updatedData, 0644); err != nil {
|
||||
return nil, fmt.Errorf("failed to write mcp settings file: %w", err)
|
||||
}
|
||||
|
||||
return []any{"Successfully updated MCP settings file"}, nil
|
||||
}
|
||||
|
||||
func (t Tool) ParseParams(data map[string]any, claims map[string]map[string]any) (tools.ParamValues, error) {
|
||||
return tools.ParseParams(t.Parameters, data, claims)
|
||||
}
|
||||
|
||||
func (t Tool) Manifest() tools.Manifest {
|
||||
return t.manifest
|
||||
}
|
||||
|
||||
func (t Tool) McpManifest() tools.McpManifest {
|
||||
return t.mcpManifest
|
||||
}
|
||||
|
||||
func (t Tool) Authorized(verifiedAuthServices []string) bool {
|
||||
return true
|
||||
}
|
||||
72
internal/tools/utility/envvariable/setenvvariable_test.go
Normal file
72
internal/tools/utility/envvariable/setenvvariable_test.go
Normal file
@@ -0,0 +1,72 @@
|
||||
// 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 setenvvariable_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/goccy/go-yaml"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/googleapis/genai-toolbox/internal/server"
|
||||
"github.com/googleapis/genai-toolbox/internal/testutils"
|
||||
setenvvariable "github.com/googleapis/genai-toolbox/internal/tools/utility/envvariable"
|
||||
)
|
||||
|
||||
func TestParseFromYamlSetEnvVariable(t *testing.T) {
|
||||
ctx, err := testutils.ContextWithNewLogger()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
tcs := []struct {
|
||||
desc string
|
||||
in string
|
||||
want server.ToolConfigs
|
||||
}{
|
||||
{
|
||||
desc: "basic example",
|
||||
in: `
|
||||
tools:
|
||||
example_tool:
|
||||
kind: update-mcp-settings
|
||||
description: some description
|
||||
authRequired:
|
||||
- my-google-auth-service
|
||||
`,
|
||||
want: server.ToolConfigs{
|
||||
"example_tool": setenvvariable.Config{
|
||||
Name: "example_tool",
|
||||
Kind: "update-mcp-settings",
|
||||
Description: "some description",
|
||||
AuthRequired: []string{"my-google-auth-service"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range tcs {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
got := struct {
|
||||
Tools server.ToolConfigs `yaml:"tools"`
|
||||
}{}
|
||||
// Parse contents
|
||||
err := yaml.UnmarshalContext(ctx, testutils.FormatYaml(tc.in), &got)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to unmarshal: %s", err)
|
||||
}
|
||||
if diff := cmp.Diff(tc.want, got.Tools); diff != "" {
|
||||
t.Fatalf("incorrect parse: diff %v", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
120
internal/tools/utility/wait/wait.go
Normal file
120
internal/tools/utility/wait/wait.go
Normal file
@@ -0,0 +1,120 @@
|
||||
// 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 wait
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
yaml "github.com/goccy/go-yaml"
|
||||
"github.com/googleapis/genai-toolbox/internal/sources"
|
||||
"github.com/googleapis/genai-toolbox/internal/tools"
|
||||
)
|
||||
|
||||
const kind string = "wait"
|
||||
|
||||
func init() {
|
||||
if !tools.Register(kind, newConfig) {
|
||||
panic(fmt.Sprintf("tool kind %q already registered", kind))
|
||||
}
|
||||
}
|
||||
|
||||
func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.ToolConfig, error) {
|
||||
actual := Config{Name: name}
|
||||
if err := decoder.DecodeContext(ctx, &actual); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return actual, nil
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Name string `yaml:"name" validate:"required"`
|
||||
Kind string `yaml:"kind" validate:"required"`
|
||||
Description string `yaml:"description" validate:"required"`
|
||||
Timeout string `yaml:"timeout" validate:"required"`
|
||||
AuthRequired []string `yaml:"authRequired"`
|
||||
}
|
||||
|
||||
var _ tools.ToolConfig = Config{}
|
||||
|
||||
func (cfg Config) ToolConfigKind() string {
|
||||
return kind
|
||||
}
|
||||
|
||||
func (cfg Config) Initialize(_ map[string]sources.Source) (tools.Tool, error) {
|
||||
durationParameter := tools.NewStringParameter("duration", "The duration to wait for, specified as a string (e.g., '10s', '2m', '1h').")
|
||||
parameters := tools.Parameters{durationParameter}
|
||||
|
||||
mcpManifest := tools.McpManifest{
|
||||
Name: cfg.Name,
|
||||
Description: cfg.Description,
|
||||
InputSchema: parameters.McpManifest(),
|
||||
}
|
||||
|
||||
t := Tool{
|
||||
Name: cfg.Name,
|
||||
Kind: kind,
|
||||
Parameters: parameters,
|
||||
manifest: tools.Manifest{Description: cfg.Description, Parameters: parameters.Manifest(), AuthRequired: cfg.AuthRequired},
|
||||
mcpManifest: mcpManifest,
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// validate interface
|
||||
var _ tools.Tool = Tool{}
|
||||
|
||||
type Tool struct {
|
||||
Name string
|
||||
Kind string
|
||||
Parameters tools.Parameters
|
||||
manifest tools.Manifest
|
||||
mcpManifest tools.McpManifest
|
||||
}
|
||||
|
||||
func (t Tool) Invoke(ctx context.Context, params tools.ParamValues) ([]any, error) {
|
||||
paramsMap := params.AsMap()
|
||||
|
||||
durationStr, ok := paramsMap["duration"].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("duration parameter is not a string")
|
||||
}
|
||||
|
||||
totalDuration, err := time.ParseDuration(durationStr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid duration format: %w", err)
|
||||
}
|
||||
|
||||
time.Sleep(totalDuration)
|
||||
|
||||
return []any{fmt.Sprintf("Wait for %v completed successfully.", totalDuration)}, nil
|
||||
}
|
||||
|
||||
func (t Tool) ParseParams(data map[string]any, claims map[string]map[string]any) (tools.ParamValues, error) {
|
||||
return tools.ParseParams(t.Parameters, data, claims)
|
||||
}
|
||||
|
||||
func (t Tool) Manifest() tools.Manifest {
|
||||
return t.manifest
|
||||
}
|
||||
|
||||
func (t Tool) McpManifest() tools.McpManifest {
|
||||
return t.mcpManifest
|
||||
}
|
||||
|
||||
func (t Tool) Authorized(verifiedAuthServices []string) bool {
|
||||
return true
|
||||
}
|
||||
75
internal/tools/utility/wait/wait_test.go
Normal file
75
internal/tools/utility/wait/wait_test.go
Normal file
@@ -0,0 +1,75 @@
|
||||
// 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 wait_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
yaml "github.com/goccy/go-yaml"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/googleapis/genai-toolbox/internal/server"
|
||||
"github.com/googleapis/genai-toolbox/internal/testutils"
|
||||
|
||||
wait "github.com/googleapis/genai-toolbox/internal/tools/utility/wait"
|
||||
)
|
||||
|
||||
func TestParseFromYamlWait(t *testing.T) {
|
||||
ctx, err := testutils.ContextWithNewLogger()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
tcs := []struct {
|
||||
desc string
|
||||
in string
|
||||
want server.ToolConfigs
|
||||
}{
|
||||
{
|
||||
desc: "basic example",
|
||||
in: `
|
||||
tools:
|
||||
example_tool:
|
||||
kind: wait
|
||||
description: some description
|
||||
timeout: 10s
|
||||
authRequired:
|
||||
- my-google-auth-service
|
||||
`,
|
||||
want: server.ToolConfigs{
|
||||
"example_tool": wait.Config{
|
||||
Name: "example_tool",
|
||||
Kind: "wait",
|
||||
Description: "some description",
|
||||
Timeout: "10s",
|
||||
AuthRequired: []string{"my-google-auth-service"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range tcs {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
got := struct {
|
||||
Tools server.ToolConfigs `yaml:"tools"`
|
||||
}{}
|
||||
// Parse contents
|
||||
err := yaml.UnmarshalContext(ctx, testutils.FormatYaml(tc.in), &got)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to unmarshal: %s", err)
|
||||
}
|
||||
if diff := cmp.Diff(tc.want, got.Tools); diff != "" {
|
||||
t.Fatalf("incorrect parse: diff %v", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -67,7 +67,7 @@ func StartCmd(ctx context.Context, toolsFile map[string]any, args ...string) (*C
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("unable to write tools file: %s", err)
|
||||
}
|
||||
args = append(args, "--tools_file", path)
|
||||
args = append(args, "--tools-file", path)
|
||||
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
// Open a pipe for tracking the output from the cmd
|
||||
|
||||
225
tests/utility/update_mcp_settings_test.go
Normal file
225
tests/utility/update_mcp_settings_test.go
Normal file
@@ -0,0 +1,225 @@
|
||||
// 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 utility_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/googleapis/genai-toolbox/internal/testutils"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/utility/envvariable"
|
||||
"github.com/googleapis/genai-toolbox/tests"
|
||||
)
|
||||
|
||||
func TestUpdateMCPSettings(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||
defer cancel()
|
||||
|
||||
toolName := "my-update-mcp-settings"
|
||||
mcpSettings := map[string]interface{}{
|
||||
"mcpServers": map[string]interface{}{},
|
||||
}
|
||||
mcpSettingsData, err := json.Marshal(mcpSettings)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to marshal mcp settings: %v", err)
|
||||
}
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
mcpSettingsFile := filepath.Join(tmpDir, "mcp.json")
|
||||
if err := os.WriteFile(mcpSettingsFile, mcpSettingsData, 0644); err != nil {
|
||||
t.Fatalf("failed to write mcp settings file: %v", err)
|
||||
}
|
||||
|
||||
toolsFile := map[string]any{
|
||||
"tools": map[string]any{
|
||||
toolName: map[string]any{
|
||||
"kind": "update-mcp-settings",
|
||||
"description": "Update MCP settings.",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
cmd, cleanup, err := tests.StartCmd(ctx, toolsFile)
|
||||
if err != nil {
|
||||
t.Fatalf("command initialization returned an error: %s", err)
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
waitCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer cancel()
|
||||
out, err := testutils.WaitForString(waitCtx, regexp.MustCompile(`Server ready to serve`), cmd.Out)
|
||||
if err != nil {
|
||||
t.Logf("toolbox command logs: \n%s", out)
|
||||
t.Fatalf("toolbox didn't start successfully: %s", err)
|
||||
}
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
params := map[string]interface{}{
|
||||
"mcpSettingsFile": mcpSettingsFile,
|
||||
"ALLOYDB_POSTGRES_PROJECT": "my-project",
|
||||
"ALLOYDB_POSTGRES_REGION": "my-region",
|
||||
"ALLOYDB_POSTGRES_CLUSTER": "my-cluster",
|
||||
"ALLOYDB_POSTGRES_INSTANCE": "my-instance",
|
||||
"ALLOYDB_POSTGRES_DATABASE": "my-db",
|
||||
"ALLOYDB_POSTGRES_USER": "my-user",
|
||||
"ALLOYDB_POSTGRES_PASSWORD": "my-password",
|
||||
}
|
||||
var result struct{ Result string }
|
||||
if err := invoke(toolName, params, &result); err != nil {
|
||||
t.Fatalf("tool invocation failed: %v", err)
|
||||
}
|
||||
|
||||
expectedResult := "[\"Successfully updated MCP settings file\"]"
|
||||
if !reflect.DeepEqual(result.Result, expectedResult) {
|
||||
t.Errorf("unexpected result: got %q, want %q", result.Result, expectedResult)
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(mcpSettingsFile)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read mcp settings file: %v", err)
|
||||
}
|
||||
|
||||
var updatedMCPSettings map[string]interface{}
|
||||
if err := json.Unmarshal(data, &updatedMCPSettings); err != nil {
|
||||
t.Fatalf("failed to unmarshal mcp settings file: %v", err)
|
||||
}
|
||||
|
||||
mcpServers, ok := updatedMCPSettings["mcpServers"].(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("mcpServers not found in updated settings")
|
||||
}
|
||||
|
||||
alloydbServer, ok := mcpServers["alloydb"].(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("alloydb server not found in updated settings")
|
||||
}
|
||||
|
||||
env, ok := alloydbServer["env"].(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("env not found in alloydb server settings")
|
||||
}
|
||||
|
||||
expectedEnv := map[string]interface{}{
|
||||
"ALLOYDB_POSTGRES_PROJECT": "my-project",
|
||||
"ALLOYDB_POSTGRES_REGION": "my-region",
|
||||
"ALLOYDB_POSTGRES_CLUSTER": "my-cluster",
|
||||
"ALLOYDB_POSTGRES_INSTANCE": "my-instance",
|
||||
"ALLOYDB_POSTGRES_DATABASE": "my-db",
|
||||
"ALLOYDB_POSTGRES_USER": "my-user",
|
||||
"ALLOYDB_POSTGRES_PASSWORD": "my-password",
|
||||
}
|
||||
if !reflect.DeepEqual(env, expectedEnv) {
|
||||
t.Errorf("unexpected env: got %v, want %v", env, expectedEnv)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("file not found", func(t *testing.T) {
|
||||
params := map[string]interface{}{
|
||||
"mcpSettingsFile": "non-existent-file.json",
|
||||
"ALLOYDB_POSTGRES_PROJECT": "my-project",
|
||||
"ALLOYDB_POSTGRES_REGION": "my-region",
|
||||
"ALLOYDB_POSTGRES_CLUSTER": "my-cluster",
|
||||
"ALLOYDB_POSTGRES_INSTANCE": "my-instance",
|
||||
"ALLOYDB_POSTGRES_DATABASE": "my-db",
|
||||
"ALLOYDB_POSTGRES_USER": "my-user",
|
||||
"ALLOYDB_POSTGRES_PASSWORD": "my-password",
|
||||
}
|
||||
var result struct{ Result string }
|
||||
err := invoke(toolName, params, &result)
|
||||
if err == nil {
|
||||
t.Fatal("expected an error but got none")
|
||||
}
|
||||
expectedError := "failed to read mcp settings file"
|
||||
if !strings.Contains(err.Error(), expectedError) {
|
||||
t.Errorf("unexpected error: got %v, want to contain %v", err, expectedError)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("invalid json", func(t *testing.T) {
|
||||
invalidJSONFile := filepath.Join(tmpDir, "invalid.json")
|
||||
if err := os.WriteFile(invalidJSONFile, []byte("{"), 0644); err != nil {
|
||||
t.Fatalf("failed to write invalid json file: %v", err)
|
||||
}
|
||||
params := map[string]interface{}{
|
||||
"mcpSettingsFile": invalidJSONFile,
|
||||
"ALLOYDB_POSTGRES_PROJECT": "my-project",
|
||||
"ALLOYDB_POSTGRES_REGION": "my-region",
|
||||
"ALLOYDB_POSTGRES_CLUSTER": "my-cluster",
|
||||
"ALLOYDB_POSTGRES_INSTANCE": "my-instance",
|
||||
"ALLOYDB_POSTGRES_DATABASE": "my-db",
|
||||
"ALLOYDB_POSTGRES_USER": "my-user",
|
||||
"ALLOYDB_POSTGRES_PASSWORD": "my-password",
|
||||
}
|
||||
var result struct{ Result string }
|
||||
err := invoke(toolName, params, &result)
|
||||
if err == nil {
|
||||
t.Fatal("expected an error but got none")
|
||||
}
|
||||
expectedError := "failed to unmarshal mcp settings file"
|
||||
if !strings.Contains(err.Error(), expectedError) {
|
||||
t.Errorf("unexpected error: got %v, want to contain %v", err, expectedError)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("missing mcpSettingsFile parameter", func(t *testing.T) {
|
||||
params := map[string]interface{}{
|
||||
"ALLOYDB_POSTGRES_PROJECT": "my-project",
|
||||
"ALLOYDB_POSTGRES_REGION": "my-region",
|
||||
"ALLOYDB_POSTGRES_CLUSTER": "my-cluster",
|
||||
"ALLOYDB_POSTGRES_INSTANCE": "my-instance",
|
||||
"ALLOYDB_POSTGRES_DATABASE": "my-db",
|
||||
"ALLOYDB_POSTGRES_USER": "my-user",
|
||||
"ALLOYDB_POSTGRES_PASSWORD": "my-password",
|
||||
}
|
||||
var result struct{ Result string }
|
||||
err := invoke(toolName, params, &result)
|
||||
if err == nil {
|
||||
t.Fatal("expected an error but got none")
|
||||
}
|
||||
expectedError := "parameter \\\"mcpSettingsFile\\\" is required"
|
||||
if !strings.Contains(err.Error(), expectedError) {
|
||||
t.Errorf("unexpected error: got %v, want to contain %v", err, expectedError)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func invoke(toolName string, params map[string]interface{}, result interface{}) error {
|
||||
url := fmt.Sprintf("http://127.0.0.1:5000/api/tool/%s/invoke", toolName)
|
||||
body, err := json.Marshal(params)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp, err := http.Post(url, "application/json", bytes.NewBuffer(body))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
respBody, _ := io.ReadAll(resp.Body)
|
||||
return fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, string(respBody))
|
||||
}
|
||||
return json.NewDecoder(resp.Body).Decode(result)
|
||||
}
|
||||
118
tests/utility/wait_integration_test.go
Normal file
118
tests/utility/wait_integration_test.go
Normal file
@@ -0,0 +1,118 @@
|
||||
// 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 utility
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/googleapis/genai-toolbox/internal/testutils"
|
||||
"github.com/googleapis/genai-toolbox/tests"
|
||||
)
|
||||
|
||||
func RunWaitTool(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||
defer cancel()
|
||||
|
||||
args := []string{"--port", "5001"}
|
||||
|
||||
toolsFile := map[string]any{
|
||||
"tools": map[string]any{
|
||||
"my-wait-for-tool": map[string]any{
|
||||
"kind": "wait",
|
||||
"description": "Wait for a specified duration.",
|
||||
"timeout": "30s",
|
||||
},
|
||||
},
|
||||
}
|
||||
cmd, cleanup, err := tests.StartCmd(ctx, toolsFile, args...)
|
||||
if err != nil {
|
||||
t.Fatalf("command initialization returned an error: %s", err)
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
waitCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer cancel()
|
||||
out, err := testutils.WaitForString(waitCtx, regexp.MustCompile(`Server ready to serve`), cmd.Out)
|
||||
if err != nil {
|
||||
t.Logf("toolbox command logs: \n%s", out)
|
||||
t.Fatalf("toolbox didn't start successfully: %s", err)
|
||||
}
|
||||
|
||||
invokeTcs := []struct {
|
||||
name string
|
||||
api string
|
||||
requestBody io.Reader
|
||||
want string
|
||||
isErr bool
|
||||
}{
|
||||
{
|
||||
name: "invoke my-wait-for-tool",
|
||||
api: "http://127.0.0.1:5001/api/tool/my-wait-for-tool/invoke",
|
||||
requestBody: bytes.NewBuffer([]byte(`{"duration": "1s"}`)),
|
||||
want: `["Wait for 1s completed successfully."]`,
|
||||
isErr: false,
|
||||
},
|
||||
{
|
||||
name: "invoke my-wait-for-tool with invalid duration",
|
||||
api: "http://127.0.0.1:5001/api/tool/my-wait-for-tool/invoke",
|
||||
requestBody: bytes.NewBuffer([]byte(`{"duration": "invalid"}`)),
|
||||
isErr: true,
|
||||
},
|
||||
}
|
||||
for _, tc := range invokeTcs {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
req, err := http.NewRequest(http.MethodPost, tc.api, tc.requestBody)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create request: %s", err)
|
||||
}
|
||||
req.Header.Add("Content-type", "application/json")
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to send request: %s", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
if tc.isErr {
|
||||
return
|
||||
}
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
t.Fatalf("response status code is not 200, got %d: %s", resp.StatusCode, string(bodyBytes))
|
||||
}
|
||||
|
||||
var body map[string]interface{}
|
||||
err = json.NewDecoder(resp.Body).Decode(&body)
|
||||
if err != nil {
|
||||
t.Fatalf("error parsing response body")
|
||||
}
|
||||
|
||||
got, ok := body["result"].(string)
|
||||
if !ok {
|
||||
t.Fatalf("unable to find result in response body")
|
||||
}
|
||||
|
||||
if got != tc.want {
|
||||
t.Fatalf("unexpected value: got %q, want %q", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user