mirror of
https://github.com/googleapis/genai-toolbox.git
synced 2026-01-11 16:38:15 -05:00
Compare commits
9 Commits
mongodb-in
...
mongodb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4ff819386a | ||
|
|
6119d401f2 | ||
|
|
967cd98cb0 | ||
|
|
2a94a33d63 | ||
|
|
f93693c92d | ||
|
|
9903df0715 | ||
|
|
ed6d6b8e4a | ||
|
|
b261be23a1 | ||
|
|
61b08a345e |
@@ -425,7 +425,7 @@ steps:
|
||||
"Valkey" \
|
||||
valkey \
|
||||
valkey
|
||||
|
||||
|
||||
- id: "firestore"
|
||||
name: golang:1
|
||||
waitFor: ["compile-test-binary"]
|
||||
@@ -466,8 +466,26 @@ steps:
|
||||
"Looker" \
|
||||
looker \
|
||||
looker
|
||||
|
||||
|
||||
- id: "mongodb"
|
||||
name : golang:1
|
||||
waitFor: ["compile-test-binary"]
|
||||
entrypoint: /bin/bash
|
||||
env:
|
||||
- "GOPATH=/gopath"
|
||||
- "SERVICE_ACCOUNT_EMAIL=$SERVICE_ACCOUNT_EMAIL"
|
||||
- "MONGODB_DATABASE=$_MONGODB_DATABASE"
|
||||
secretEnv: ["CLIENT_ID", "MONGODB_URI"]
|
||||
volumes:
|
||||
- name: "go"
|
||||
path: "/gopath"
|
||||
args:
|
||||
- -c
|
||||
- |
|
||||
.ci/test_with_coverage.sh \
|
||||
"MongoDB" \
|
||||
mongodb \
|
||||
mongodb
|
||||
|
||||
- id: "alloydbwaitforoperation"
|
||||
name: golang:1
|
||||
@@ -546,6 +564,8 @@ availableSecrets:
|
||||
env: LOOKER_CLIENT_ID
|
||||
- versionName: projects/107716898620/secrets/looker_client_secret/versions/latest
|
||||
env: LOOKER_CLIENT_SECRET
|
||||
- versionName: projects/$PROJECT_ID/secrets/mongodb_uri/versions/latest
|
||||
env: MONGODB_URI
|
||||
|
||||
|
||||
options:
|
||||
@@ -578,4 +598,5 @@ substitutions:
|
||||
_DGRAPHURL: "https://play.dgraph.io"
|
||||
_COUCHBASE_BUCKET: "couchbase-bucket"
|
||||
_COUCHBASE_SCOPE: "couchbase-scope"
|
||||
_LOOKER_VERIFY_SSL: "true"
|
||||
_LOOKER_VERIFY_SSL: "true"
|
||||
_MONGODB_DATABASE: "test"
|
||||
|
||||
@@ -69,6 +69,7 @@ import (
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookerquery"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookerquerysql"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookerrunlook"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/mongodb/mongodbaggregate"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/mongodb/mongodbdeletemany"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/mongodb/mongodbdeleteone"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/mongodb/mongodbfind"
|
||||
@@ -212,7 +213,7 @@ func NewCommand(opts ...Option) *Command {
|
||||
flags.BoolVar(&cmd.cfg.TelemetryGCP, "telemetry-gcp", false, "Enable exporting directly to Google Cloud Monitoring.")
|
||||
flags.StringVar(&cmd.cfg.TelemetryOTLP, "telemetry-otlp", "", "Enable exporting using OpenTelemetry Protocol (OTLP) to the specified endpoint (e.g. 'http://127.0.0.1:4318')")
|
||||
flags.StringVar(&cmd.cfg.TelemetryServiceName, "telemetry-service-name", "toolbox", "Sets the value of the service.name resource attribute for telemetry data.")
|
||||
flags.StringVar(&cmd.prebuiltConfig, "prebuilt", "", "Use a prebuilt tool configuration by source type. Cannot be used with --tools-file. Allowed: 'alloydb-postgres-admin', alloydb-postgres', 'bigquery', 'cloud-sql-mysql', 'cloud-sql-postgres', 'cloud-sql-mssql', 'firestore', 'mssql', 'mysql', 'postgres', 'spanner', 'spanner-postgres'.")
|
||||
flags.StringVar(&cmd.prebuiltConfig, "prebuilt", "", "Use a prebuilt tool configuration by source type. Cannot be used with --tools-file. Allowed: 'alloydb-postgres-admin', alloydb-postgres', 'bigquery', 'cloud-sql-mysql', 'cloud-sql-postgres', 'cloud-sql-mssql', 'firestore', 'postgres', 'spanner', 'spanner-postgres'.")
|
||||
flags.BoolVar(&cmd.cfg.Stdio, "stdio", false, "Listens via MCP STDIO instead of acting as a remote HTTP server.")
|
||||
flags.BoolVar(&cmd.cfg.DisableReload, "disable-reload", false, "Disables dynamic reloading of tools file.")
|
||||
|
||||
|
||||
@@ -1168,8 +1168,6 @@ func TestPrebuiltTools(t *testing.T) {
|
||||
cloudsqlmysql_config, _ := prebuiltconfigs.Get("cloud-sql-mysql")
|
||||
cloudsqlmssql_config, _ := prebuiltconfigs.Get("cloud-sql-mssql")
|
||||
firestoreconfig, _ := prebuiltconfigs.Get("firestore")
|
||||
mysql_config, _ := prebuiltconfigs.Get("mysql")
|
||||
mssql_config, _ := prebuiltconfigs.Get("mssql")
|
||||
looker_config, _ := prebuiltconfigs.Get("looker")
|
||||
postgresconfig, _ := prebuiltconfigs.Get("postgres")
|
||||
spanner_config, _ := prebuiltconfigs.Get("spanner")
|
||||
@@ -1253,26 +1251,6 @@ func TestPrebuiltTools(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "mysql prebuilt tools",
|
||||
in: mysql_config,
|
||||
wantToolset: server.ToolsetConfigs{
|
||||
"mysql-database-tools": tools.ToolsetConfig{
|
||||
Name: "mysql-database-tools",
|
||||
ToolNames: []string{"execute_sql", "list_tables"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "mssql prebuilt tools",
|
||||
in: mssql_config,
|
||||
wantToolset: server.ToolsetConfigs{
|
||||
"mssql-database-tools": tools.ToolsetConfig{
|
||||
Name: "mssql-database-tools",
|
||||
ToolNames: []string{"execute_sql", "list_tables"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "looker prebuilt tools",
|
||||
in: looker_config,
|
||||
|
||||
@@ -1,306 +0,0 @@
|
||||
---
|
||||
title: "AlloyDB Admin API using MCP"
|
||||
type: docs
|
||||
weight: 2
|
||||
description: >
|
||||
Create your AlloyDB database with MCP Toolbox.
|
||||
---
|
||||
|
||||
This guide covers how to use [MCP Toolbox for Databases][toolbox] to create AlloyDB clusters and instances from IDE enabling their E2E journey.
|
||||
|
||||
- [Cursor][cursor]
|
||||
- [Windsurf][windsurf] (Codium)
|
||||
- [Visual Studio Code ][vscode] (Copilot)
|
||||
- [Cline][cline] (VS Code extension)
|
||||
- [Claude desktop][claudedesktop]
|
||||
- [Claude code][claudecode]
|
||||
- [Gemini CLI][geminicli]
|
||||
- [Gemini Code Assist][geminicodeassist]
|
||||
|
||||
[toolbox]: https://github.com/googleapis/genai-toolbox
|
||||
[cursor]: #configure-your-mcp-client
|
||||
[windsurf]: #configure-your-mcp-client
|
||||
[vscode]: #configure-your-mcp-client
|
||||
[cline]: #configure-your-mcp-client
|
||||
[claudedesktop]: #configure-your-mcp-client
|
||||
[claudecode]: #configure-your-mcp-client
|
||||
[geminicli]: #configure-your-mcp-client
|
||||
[geminicodeassist]: #configure-your-mcp-client
|
||||
|
||||
## Before you begin
|
||||
|
||||
1. In the Google Cloud console, on the [project selector page](https://console.cloud.google.com/projectselector2/home/dashboard), select or create a Google Cloud project.
|
||||
|
||||
1. [Make sure that billing is enabled for your Google Cloud project](https://cloud.google.com/billing/docs/how-to/verify-billing-enabled#confirm_billing_is_enabled_on_a_project).
|
||||
|
||||
## Install MCP Toolbox
|
||||
|
||||
1. Download the latest version of Toolbox as a binary. Select the [correct binary](https://github.com/googleapis/genai-toolbox/releases) corresponding to your OS and CPU architecture. You are required to use Toolbox version V0.10.0+:
|
||||
|
||||
<!-- {x-release-please-start-version} -->
|
||||
|
||||
{{< tabpane persist=header >}}
|
||||
{{< tab header="linux/amd64" lang="bash" >}}
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.10.0/linux/amd64/toolbox
|
||||
{{< /tab >}}
|
||||
|
||||
{{< tab header="darwin/arm64" lang="bash" >}}
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.10.0/darwin/arm64/toolbox
|
||||
{{< /tab >}}
|
||||
|
||||
{{< tab header="darwin/amd64" lang="bash" >}}
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.10.0/darwin/amd64/toolbox
|
||||
{{< /tab >}}
|
||||
|
||||
{{< tab header="windows/amd64" lang="bash" >}}
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.10.0/windows/amd64/toolbox
|
||||
{{< /tab >}}
|
||||
{{< /tabpane >}}
|
||||
|
||||
<!-- {x-release-please-end} -->
|
||||
|
||||
1. Make the binary executable:
|
||||
|
||||
```bash
|
||||
chmod +x toolbox
|
||||
```
|
||||
|
||||
1. Verify the installation:
|
||||
|
||||
```bash
|
||||
./toolbox --version
|
||||
```
|
||||
|
||||
## Configure your MCP Client
|
||||
|
||||
{{< tabpane text=true >}}
|
||||
{{% tab header="Claude code" lang="en" %}}
|
||||
|
||||
1. Install [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/overview).
|
||||
1. Create a `.mcp.json` file in your project root if it doesn't exist.
|
||||
1. Generate Access token to be used as API_KEY using `gcloud auth print-access-token`.
|
||||
|
||||
> **Note:** The lifetime of token is 1 hour.
|
||||
|
||||
1. Add the following configuration, replace the environment variables with your values, and save:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"alloydb-admin": {
|
||||
"command": "./PATH/TO/toolbox",
|
||||
"args": ["--prebuilt", "alloydb-postgres-admin", "--stdio"],
|
||||
"env": {
|
||||
"API_KEY": "your-api-key"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
1. Restart Claude code to apply the new configuration.
|
||||
{{% /tab %}}
|
||||
|
||||
{{% tab header="Claude desktop" lang="en" %}}
|
||||
|
||||
1. Open [Claude desktop](https://claude.ai/download) and navigate to Settings.
|
||||
1. Under the Developer tab, tap Edit Config to open the configuration file.
|
||||
1. Generate Access token to be used as API_KEY using `gcloud auth print-access-token`.
|
||||
|
||||
> **Note:** The lifetime of token is 1 hour.
|
||||
|
||||
1. Add the following configuration, replace the environment variables with your values, and save:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"alloydb-admin": {
|
||||
"command": "./PATH/TO/toolbox",
|
||||
"args": ["--prebuilt", "alloydb-postgres-admin", "--stdio"],
|
||||
"env": {
|
||||
"API_KEY": "your-api-key"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
1. Restart Claude desktop.
|
||||
1. From the new chat screen, you should see a hammer (MCP) icon appear with the new MCP server available.
|
||||
{{% /tab %}}
|
||||
|
||||
{{% tab header="Cline" lang="en" %}}
|
||||
|
||||
1. Open the [Cline](https://github.com/cline/cline) extension in VS Code and tap the **MCP Servers** icon.
|
||||
1. Tap Configure MCP Servers to open the configuration file.
|
||||
1. Generate Access token to be used as API_KEY using `gcloud auth print-access-token`.
|
||||
|
||||
> **Note:** The lifetime of token is 1 hour.
|
||||
|
||||
1. Add the following configuration, replace the environment variables with your values, and save:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"alloydb-admin": {
|
||||
"command": "./PATH/TO/toolbox",
|
||||
"args": ["--prebuilt", "alloydb-postgres-admin", "--stdio"],
|
||||
"env": {
|
||||
"API_KEY": "your-api-key"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
1. You should see a green active status after the server is successfully connected.
|
||||
{{% /tab %}}
|
||||
|
||||
{{% tab header="Cursor" lang="en" %}}
|
||||
|
||||
1. Create a `.cursor` directory in your project root if it doesn't exist.
|
||||
1. Create a `.cursor/mcp.json` file if it doesn't exist and open it.
|
||||
1. Generate Access token to be used as API_KEY using `gcloud auth print-access-token`.
|
||||
|
||||
> **Note:** The lifetime of token is 1 hour.
|
||||
|
||||
1. Add the following configuration, replace the environment variables with your values, and save:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"alloydb-admin": {
|
||||
"command": "./PATH/TO/toolbox",
|
||||
"args": ["--prebuilt", "alloydb-postgres-admin", "--stdio"],
|
||||
"env": {
|
||||
"API_KEY": "your-api-key"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
1. [Cursor](https://www.cursor.com/) and navigate to **Settings > Cursor Settings > MCP**. You should see a green active status after the server is successfully connected.
|
||||
{{% /tab %}}
|
||||
|
||||
{{% tab header="Visual Studio Code (Copilot)" lang="en" %}}
|
||||
|
||||
1. Open [VS Code](https://code.visualstudio.com/docs/copilot/overview) and create a `.vscode` directory in your project root if it doesn't exist.
|
||||
1. Create a `.vscode/mcp.json` file if it doesn't exist and open it.
|
||||
1. Generate Access token to be used as API_KEY using `gcloud auth print-access-token`.
|
||||
|
||||
> **Note:** The lifetime of token is 1 hour.
|
||||
|
||||
1. Add the following configuration, replace the environment variables with your values, and save:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"alloydb-admin": {
|
||||
"command": "./PATH/TO/toolbox",
|
||||
"args": ["--prebuilt", "alloydb-postgres-admin", "--stdio"],
|
||||
"env": {
|
||||
"API_KEY": "your-api-key"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
{{% /tab %}}
|
||||
|
||||
{{% tab header="Windsurf" lang="en" %}}
|
||||
|
||||
1. Open [Windsurf](https://docs.codeium.com/windsurf) and navigate to the Cascade assistant.
|
||||
1. Tap on the hammer (MCP) icon, then Configure to open the configuration file.
|
||||
1. Generate Access token to be used as API_KEY using `gcloud auth print-access-token`.
|
||||
|
||||
> **Note:** The lifetime of token is 1 hour.
|
||||
|
||||
1. Add the following configuration, replace the environment variables with your values, and save:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"alloydb-admin": {
|
||||
"command": "./PATH/TO/toolbox",
|
||||
"args": ["--prebuilt", "alloydb-postgres-admin", "--stdio"],
|
||||
"env": {
|
||||
"API_KEY": "your-api-key"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
{{% /tab %}}
|
||||
{{% tab header="Gemini CLI" lang="en" %}}
|
||||
|
||||
1. Install the [Gemini CLI](https://github.com/google-gemini/gemini-cli?tab=readme-ov-file#quickstart).
|
||||
1. In your working directory, create a folder named `.gemini`. Within it, create a `settings.json` file.
|
||||
1. Generate Access token to be used as API_KEY using `gcloud auth print-access-token`.
|
||||
|
||||
> **Note:** The lifetime of token is 1 hour.
|
||||
|
||||
1. Add the following configuration, replace the environment variables with your values, and save:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"alloydb-admin": {
|
||||
"command": "./PATH/TO/toolbox",
|
||||
"args": ["--prebuilt", "alloydb-postgres-admin", "--stdio"],
|
||||
"env": {
|
||||
"API_KEY": "your-api-key"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
{{% /tab %}}
|
||||
{{% tab header="Gemini Code Assist" lang="en" %}}
|
||||
|
||||
1. Install the [Gemini Code Assist](https://marketplace.visualstudio.com/items?itemName=Google.geminicodeassist) extension in Visual Studio Code.
|
||||
1. Enable Agent Mode in Gemini Code Assist chat.
|
||||
1. In your working directory, create a folder named `.gemini`. Within it, create a `settings.json` file.
|
||||
1. Generate Access token to be used as API_KEY using `gcloud auth print-access-token`.
|
||||
|
||||
> **Note:** The lifetime of token is 1 hour.
|
||||
|
||||
1. Add the following configuration, replace the environment variables with your values, and save:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"alloydb-admin": {
|
||||
"command": "./PATH/TO/toolbox",
|
||||
"args": ["--prebuilt", "alloydb-postgres-admin", "--stdio"],
|
||||
"env": {
|
||||
"API_KEY": "your-api-key"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
{{% /tab %}}
|
||||
{{< /tabpane >}}
|
||||
|
||||
## Use Tools
|
||||
|
||||
Your AI tool is now connected to AlloyDB using MCP. Try asking your AI assistant to create a database, cluster or instance.
|
||||
|
||||
The following tools are available to the LLM:
|
||||
|
||||
1. **alloydb-create-cluster**: creates alloydb cluster
|
||||
1. **alloydb-create-instance**: creates alloydb instance (PRIMARY, READ_POOL or SECONDARY)
|
||||
1. **alloydb-get-operation**: polls on operations API until the operation is done.
|
||||
|
||||
{{< notice note >}}
|
||||
Prebuilt tools are pre-1.0, so expect some tool changes between versions. LLMs will adapt to the tools available, so this shouldn't affect most users.
|
||||
{{< /notice >}}
|
||||
|
||||
## Connect to your Data
|
||||
|
||||
After setting up an AlloyDB cluster and instance, you can [connect your IDE to the database](https://cloud.google.com/alloydb/docs/pre-built-tools-with-mcp-toolbox).
|
||||
@@ -1,313 +0,0 @@
|
||||
---
|
||||
title: "Firestore using MCP"
|
||||
type: docs
|
||||
weight: 2
|
||||
description: >
|
||||
Connect your IDE to Firestore using Toolbox.
|
||||
---
|
||||
|
||||
[Model Context Protocol (MCP)](https://modelcontextprotocol.io/introduction) is
|
||||
an open protocol for connecting Large Language Models (LLMs) to data sources
|
||||
like Firestore. This guide covers how to use [MCP Toolbox for Databases][toolbox]
|
||||
to expose your developer assistant tools to a Firestore instance:
|
||||
|
||||
* [Cursor][cursor]
|
||||
* [Windsurf][windsurf] (Codium)
|
||||
* [Visual Studio Code][vscode] (Copilot)
|
||||
* [Cline][cline] (VS Code extension)
|
||||
* [Claude desktop][claudedesktop]
|
||||
* [Claude code][claudecode]
|
||||
* [Gemini CLI][geminicli]
|
||||
* [Gemini Code Assist][geminicodeassist]
|
||||
|
||||
[toolbox]: https://github.com/googleapis/genai-toolbox
|
||||
[cursor]: #configure-your-mcp-client
|
||||
[windsurf]: #configure-your-mcp-client
|
||||
[vscode]: #configure-your-mcp-client
|
||||
[cline]: #configure-your-mcp-client
|
||||
[claudedesktop]: #configure-your-mcp-client
|
||||
[claudecode]: #configure-your-mcp-client
|
||||
[geminicli]: #configure-your-mcp-client
|
||||
[geminicodeassist]: #configure-your-mcp-client]
|
||||
|
||||
## Set up Firestore
|
||||
|
||||
1. Create or select a Google Cloud project.
|
||||
|
||||
* [Create a new project](https://cloud.google.com/resource-manager/docs/creating-managing-projects)
|
||||
* [Select an existing project](https://cloud.google.com/resource-manager/docs/creating-managing-projects#identifying_projects)
|
||||
|
||||
1. [Enable the Firestore API](https://console.cloud.google.com/apis/library/firestore.googleapis.com) for your project.
|
||||
|
||||
1. [Create a Firestore database](https://cloud.google.com/firestore/docs/create-database-web-mobile-client-library) if you haven't already.
|
||||
|
||||
1. Set up authentication for your local environment.
|
||||
|
||||
* [Install gcloud CLI](https://cloud.google.com/sdk/docs/install)
|
||||
* Run `gcloud auth application-default login` to authenticate
|
||||
|
||||
## Install MCP Toolbox
|
||||
|
||||
1. Download the latest version of Toolbox as a binary. Select the [correct
|
||||
binary](https://github.com/googleapis/genai-toolbox/releases) corresponding
|
||||
to your OS and CPU architecture. You are required to use Toolbox version
|
||||
V0.10.0+:
|
||||
|
||||
<!-- {x-release-please-start-version} -->
|
||||
{{< tabpane persist=header >}}
|
||||
{{< tab header="linux/amd64" lang="bash" >}}
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.10.0/linux/amd64/toolbox
|
||||
{{< /tab >}}
|
||||
|
||||
{{< tab header="darwin/arm64" lang="bash" >}}
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.10.0/darwin/arm64/toolbox
|
||||
{{< /tab >}}
|
||||
|
||||
{{< tab header="darwin/amd64" lang="bash" >}}
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.10.0/darwin/amd64/toolbox
|
||||
{{< /tab >}}
|
||||
|
||||
{{< tab header="windows/amd64" lang="bash" >}}
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.10.0/windows/amd64/toolbox
|
||||
{{< /tab >}}
|
||||
{{< /tabpane >}}
|
||||
<!-- {x-release-please-end} -->
|
||||
|
||||
1. Make the binary executable:
|
||||
|
||||
```bash
|
||||
chmod +x toolbox
|
||||
```
|
||||
|
||||
1. Verify the installation:
|
||||
|
||||
```bash
|
||||
./toolbox --version
|
||||
```
|
||||
|
||||
## Configure your MCP Client
|
||||
|
||||
{{< tabpane text=true >}}
|
||||
{{% tab header="Claude code" lang="en" %}}
|
||||
|
||||
1. Install [Claude
|
||||
Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/overview).
|
||||
1. Create a `.mcp.json` file in your project root if it doesn't exist.
|
||||
1. Add the following configuration, replace the environment variables with your
|
||||
values, and save:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"firestore": {
|
||||
"command": "./PATH/TO/toolbox",
|
||||
"args": ["--prebuilt","firestore","--stdio"],
|
||||
"env": {
|
||||
"FIRESTORE_PROJECT": "your-project-id",
|
||||
"FIRESTORE_DATABASE": "(default)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
1. Restart Claude code to apply the new configuration.
|
||||
{{% /tab %}}
|
||||
|
||||
{{% tab header="Claude desktop" lang="en" %}}
|
||||
|
||||
1. Open [Claude desktop](https://claude.ai/download) and navigate to Settings.
|
||||
1. Under the Developer tab, tap Edit Config to open the configuration file.
|
||||
1. Add the following configuration, replace the environment variables with your
|
||||
values, and save:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"firestore": {
|
||||
"command": "./PATH/TO/toolbox",
|
||||
"args": ["--prebuilt","firestore","--stdio"],
|
||||
"env": {
|
||||
"FIRESTORE_PROJECT": "your-project-id",
|
||||
"FIRESTORE_DATABASE": "(default)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
1. Restart Claude desktop.
|
||||
1. From the new chat screen, you should see a hammer (MCP) icon appear with the
|
||||
new MCP server available.
|
||||
{{% /tab %}}
|
||||
|
||||
{{% tab header="Cline" lang="en" %}}
|
||||
|
||||
1. Open the [Cline](https://github.com/cline/cline) extension in VS Code and tap
|
||||
the **MCP Servers** icon.
|
||||
1. Tap Configure MCP Servers to open the configuration file.
|
||||
1. Add the following configuration, replace the environment variables with your
|
||||
values, and save:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"firestore": {
|
||||
"command": "./PATH/TO/toolbox",
|
||||
"args": ["--prebuilt","firestore","--stdio"],
|
||||
"env": {
|
||||
"FIRESTORE_PROJECT": "your-project-id",
|
||||
"FIRESTORE_DATABASE": "(default)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
1. You should see a green active status after the server is successfully
|
||||
connected.
|
||||
{{% /tab %}}
|
||||
|
||||
{{% tab header="Cursor" lang="en" %}}
|
||||
|
||||
1. Create a `.cursor` directory in your project root if it doesn't exist.
|
||||
1. Create a `.cursor/mcp.json` file if it doesn't exist and open it.
|
||||
1. Add the following configuration, replace the environment variables with your
|
||||
values, and save:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"firestore": {
|
||||
"command": "./PATH/TO/toolbox",
|
||||
"args": ["--prebuilt","firestore","--stdio"],
|
||||
"env": {
|
||||
"FIRESTORE_PROJECT": "your-project-id",
|
||||
"FIRESTORE_DATABASE": "(default)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
1. [Cursor](https://www.cursor.com/) and navigate to **Settings > Cursor
|
||||
Settings > MCP**. You should see a green active status after the server is
|
||||
successfully connected.
|
||||
{{% /tab %}}
|
||||
|
||||
{{% tab header="Visual Studio Code (Copilot)" lang="en" %}}
|
||||
|
||||
1. Open [VS Code](https://code.visualstudio.com/docs/copilot/overview) and
|
||||
create a `.vscode` directory in your project root if it doesn't exist.
|
||||
1. Create a `.vscode/mcp.json` file if it doesn't exist and open it.
|
||||
1. Add the following configuration, replace the environment variables with your
|
||||
values, and save:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"firestore": {
|
||||
"command": "./PATH/TO/toolbox",
|
||||
"args": ["--prebuilt","firestore","--stdio"],
|
||||
"env": {
|
||||
"FIRESTORE_PROJECT": "your-project-id",
|
||||
"FIRESTORE_DATABASE": "(default)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
{{% /tab %}}
|
||||
|
||||
{{% tab header="Windsurf" lang="en" %}}
|
||||
|
||||
1. Open [Windsurf](https://docs.codeium.com/windsurf) and navigate to the
|
||||
Cascade assistant.
|
||||
1. Tap on the hammer (MCP) icon, then Configure to open the configuration file.
|
||||
1. Add the following configuration, replace the environment variables with your
|
||||
values, and save:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"firestore": {
|
||||
"command": "./PATH/TO/toolbox",
|
||||
"args": ["--prebuilt","firestore","--stdio"],
|
||||
"env": {
|
||||
"FIRESTORE_PROJECT": "your-project-id",
|
||||
"FIRESTORE_DATABASE": "(default)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
{{% /tab %}}
|
||||
{{% tab header="Gemini CLI" lang="en" %}}
|
||||
|
||||
1. Install the [Gemini CLI](https://github.com/google-gemini/gemini-cli?tab=readme-ov-file#quickstart).
|
||||
1. In your working directory, create a folder named `.gemini`. Within it, create a `settings.json` file.
|
||||
1. Add the following configuration, replace the environment variables with your values, and then save:
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"firestore": {
|
||||
"command": "./PATH/TO/toolbox",
|
||||
"args": ["--prebuilt","firestore","--stdio"],
|
||||
"env": {
|
||||
"FIRESTORE_PROJECT": "your-project-id",
|
||||
"FIRESTORE_DATABASE": "(default)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
{{% /tab %}}
|
||||
{{% tab header="Gemini Code Assist" lang="en" %}}
|
||||
|
||||
1. Install the [Gemini Code Assist](https://marketplace.visualstudio.com/items?itemName=Google.geminicodeassist) extension in Visual Studio Code.
|
||||
1. Enable Agent Mode in Gemini Code Assist chat.
|
||||
1. In your working directory, create a folder named `.gemini`. Within it, create a `settings.json` file.
|
||||
1. Add the following configuration, replace the environment variables with your values, and then save:
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"firestore": {
|
||||
"command": "./PATH/TO/toolbox",
|
||||
"args": ["--prebuilt","firestore","--stdio"],
|
||||
"env": {
|
||||
"FIRESTORE_PROJECT": "your-project-id",
|
||||
"FIRESTORE_DATABASE": "(default)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
{{% /tab %}}
|
||||
{{< /tabpane >}}
|
||||
|
||||
## Use Tools
|
||||
|
||||
Your AI tool is now connected to Firestore using MCP. Try asking your AI
|
||||
assistant to list collections, get documents, query collections, or manage
|
||||
security rules.
|
||||
|
||||
The following tools are available to the LLM:
|
||||
|
||||
1. **firestore-get-documents**: Gets multiple documents from Firestore by their paths
|
||||
1. **firestore-list-collections**: List Firestore collections for a given parent path
|
||||
1. **firestore-delete-documents**: Delete multiple documents from Firestore
|
||||
1. **firestore-query-collection**: Query documents from a collection with filtering, ordering, and limit options
|
||||
1. **firestore-get-rules**: Retrieves the active Firestore security rules for the current project
|
||||
1. **firestore-validate-rules**: Validates Firestore security rules syntax and errors
|
||||
|
||||
{{< notice note >}}
|
||||
Prebuilt tools are pre-1.0, so expect some tool changes between versions. LLMs
|
||||
will adapt to the tools available, so this shouldn't affect most users.
|
||||
{{< /notice >}}
|
||||
@@ -35,7 +35,6 @@ the IAM identity has been given the correct IAM permissions for accessing
|
||||
Firestore. Common roles include:
|
||||
- `roles/datastore.user` - Read and write access to Firestore
|
||||
- `roles/datastore.viewer` - Read-only access to Firestore
|
||||
- `roles/firebaserules.admin` - Full management of Firebase Security Rules for Firestore. This role is required for operations that involve creating, updating, or managing Firestore security rules (see [Firebase Security Rules roles][firebaserules-roles])
|
||||
|
||||
See [Firestore access control][firestore-iam] for more information on
|
||||
applying IAM permissions and roles to an identity.
|
||||
@@ -44,7 +43,6 @@ applying IAM permissions and roles to an identity.
|
||||
[adc]: https://cloud.google.com/docs/authentication#adc
|
||||
[set-adc]: https://cloud.google.com/docs/authentication/provide-credentials-adc
|
||||
[firestore-iam]: https://cloud.google.com/firestore/docs/security/iam
|
||||
[firebaserules-roles]: https://cloud.google.com/iam/docs/roles-permissions/firebaserules
|
||||
|
||||
### Database Selection
|
||||
|
||||
|
||||
@@ -9,9 +9,9 @@ description: >
|
||||
|
||||
## About
|
||||
|
||||
[MongoDB][mongodb-docs] is a popular NoSQL database that stores data in flexible, JSON-like documents, making it easy to develop and scale applications.
|
||||
[MongoDB][mongodb-docs] is a leading nosql database that can not only cater your operational data needs but also perform vector search.
|
||||
|
||||
[mongodb-docs]: https://www.mongodb.com/docs/atlas/getting-started/
|
||||
[mongodb-docs]: https://www.mongodb.com/docs/atlas/atlas-vector-search/vector-search-overview/
|
||||
|
||||
## Example
|
||||
|
||||
@@ -24,10 +24,11 @@ sources:
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Reference
|
||||
|
||||
| **field** | **type** | **required** | **description** |
|
||||
|-----------|:--------:|:------------:|-------------------------------------------------------------------|
|
||||
| kind | string | true | Must be "mongodb". |
|
||||
| uri | string | true | connection string to connect to MongoDB |
|
||||
| database | string | true | Name of the mongodb database to connect to (e.g. "sample_mflix"). |
|
||||
| uri | string | true | connection string to connect to MongoDB | |
|
||||
| database | string | true | Name of the mongodb database to connect to (e.g. "sample_mflix"). |
|
||||
@@ -1,11 +1,7 @@
|
||||
---
|
||||
title: "firestore-validate-rules"
|
||||
type: docs
|
||||
weight: 1
|
||||
description: >
|
||||
A "firestore-validate-rules" tool validates Firestore security rules syntax and semantic correctness without deploying them. It provides detailed error reporting with source positions and code snippets.
|
||||
aliases:
|
||||
- /resources/tools/firestore-validate-rules
|
||||
title: firestore-validate-rules
|
||||
weight: 6
|
||||
date: 2025-01-07
|
||||
---
|
||||
|
||||
## Overview
|
||||
@@ -38,20 +34,20 @@ The tool returns a `ValidationResult` object containing:
|
||||
|
||||
```json
|
||||
{
|
||||
"valid": "boolean",
|
||||
"issueCount": "number",
|
||||
"formattedIssues": "string",
|
||||
"rawIssues": [
|
||||
"valid": boolean, // Whether the rules are valid
|
||||
"issueCount": number, // Number of issues found
|
||||
"formattedIssues": string, // Human-readable formatted issues
|
||||
"rawIssues": [ // Array of raw issue objects
|
||||
{
|
||||
"sourcePosition": {
|
||||
"fileName": "string",
|
||||
"line": "number",
|
||||
"column": "number",
|
||||
"currentOffset": "number",
|
||||
"endOffset": "number"
|
||||
"fileName": string,
|
||||
"line": number,
|
||||
"column": number,
|
||||
"currentOffset": number,
|
||||
"endOffset": number
|
||||
},
|
||||
"description": "string",
|
||||
"severity": "string"
|
||||
"description": string,
|
||||
"severity": string // e.g., "ERROR", "WARNING"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
title: "MongoDB"
|
||||
type: docs
|
||||
weight: 1
|
||||
description: >
|
||||
Tools that work with the MongoDB Source.
|
||||
---
|
||||
@@ -1,53 +0,0 @@
|
||||
---
|
||||
title: "mongodb-delete-many"
|
||||
type: docs
|
||||
weight: 1
|
||||
description: >
|
||||
A "mongodb-delete-many" tool deletes all documents from a MongoDB collection that match a filter.
|
||||
aliases:
|
||||
- /resources/tools/mongodb-delete-many
|
||||
---
|
||||
|
||||
## About
|
||||
|
||||
The `mongodb-delete-many` tool performs a **bulk destructive operation**, deleting **ALL** documents from a collection that match a specified filter.
|
||||
|
||||
The tool returns the total count of documents that were deleted. If the filter does not match any documents (i.e., the deleted count is 0), the tool will return an error.
|
||||
|
||||
This tool is compatible with the following source kind:
|
||||
|
||||
* [`mongodb`](../../sources/mongodb.md)
|
||||
|
||||
---
|
||||
|
||||
## Example
|
||||
|
||||
Here is an example that performs a cleanup task by deleting all products from the `inventory` collection that belong to a discontinued brand.
|
||||
|
||||
```yaml
|
||||
tools:
|
||||
retire_brand_products:
|
||||
kind: mongodb-delete-many
|
||||
source: my-mongo-source
|
||||
description: Deletes all products from a specified discontinued brand.
|
||||
database: ecommerce
|
||||
collection: inventory
|
||||
filterPayload: |
|
||||
{ "brand_name": {{json .brand_to_delete}} }
|
||||
filterParams:
|
||||
- name: brand_to_delete
|
||||
type: string
|
||||
description: The name of the discontinued brand whose products should be deleted.
|
||||
```
|
||||
|
||||
## Reference
|
||||
|
||||
| **field** | **type** | **required** | **description** |
|
||||
|:--------------|:---------|:-------------|:--------------------------------------------------------------------------------------------------------------------|
|
||||
| kind | string | true | Must be `mongodb-delete-many`. |
|
||||
| source | string | true | The name of the `mongodb` source to use. |
|
||||
| description | string | true | A description of the tool that is passed to the LLM. |
|
||||
| database | string | true | The name of the MongoDB database containing the collection. |
|
||||
| collection | string | true | The name of the MongoDB collection from which to delete documents. |
|
||||
| filterPayload | string | true | The MongoDB query filter document to select the documents for deletion. Uses `{{json .param_name}}` for templating. |
|
||||
| filterParams | list | true | A list of parameter objects that define the variables used in the `filterPayload`. |
|
||||
@@ -1,55 +0,0 @@
|
||||
---
|
||||
title: "mongodb-delete-one"
|
||||
type: docs
|
||||
weight: 1
|
||||
description: >
|
||||
A "mongodb-delete-one" tool deletes a single document from a MongoDB collection.
|
||||
aliases:
|
||||
- /resources/tools/mongodb-delete-one
|
||||
---
|
||||
|
||||
## About
|
||||
|
||||
The `mongodb-delete-one` tool performs a destructive operation, deleting the **first single document** that matches a specified filter from a MongoDB collection.
|
||||
|
||||
If the filter matches multiple documents, only the first one found by the database will be deleted. This tool is useful for removing specific entries, such as a user account or a single item from an inventory based on a unique ID.
|
||||
|
||||
The tool returns the number of documents deleted, which will be either `1` if a document was found and deleted, or `0` if no matching document was found.
|
||||
|
||||
This tool is compatible with the following source kind:
|
||||
|
||||
* [`mongodb`](../../sources/mongodb.md)
|
||||
|
||||
---
|
||||
|
||||
## Example
|
||||
|
||||
Here is an example that deletes a specific user account from the `users` collection by matching their unique email address. This is a permanent action.
|
||||
|
||||
```yaml
|
||||
tools:
|
||||
delete_user_account:
|
||||
kind: mongodb-delete-one
|
||||
source: my-mongo-source
|
||||
description: Permanently deletes a user account by their email address.
|
||||
database: user_data
|
||||
collection: users
|
||||
filterPayload: |
|
||||
{ "email": {{json .email_address}} }
|
||||
filterParams:
|
||||
- name: email_address
|
||||
type: string
|
||||
description: The email of the user account to delete.
|
||||
```
|
||||
|
||||
## Reference
|
||||
|
||||
| **field** | **type** | **required** | **description** |
|
||||
|:--------------|:---------|:-------------|:-------------------------------------------------------------------------------------------------------------------|
|
||||
| kind | string | true | Must be `mongodb-delete-one`. |
|
||||
| source | string | true | The name of the `mongodb` source to use. |
|
||||
| description | string | true | A description of the tool that is passed to the LLM. |
|
||||
| database | string | true | The name of the MongoDB database containing the collection. |
|
||||
| collection | string | true | The name of the MongoDB collection from which to delete a document. |
|
||||
| filterPayload | string | true | The MongoDB query filter document to select the document for deletion. Uses `{{json .param_name}}` for templating. |
|
||||
| filterParams | list | true | A list of parameter objects that define the variables used in the `filterPayload`. |
|
||||
@@ -1,62 +0,0 @@
|
||||
---
|
||||
title: "mongodb-find-one"
|
||||
type: docs
|
||||
weight: 1
|
||||
description: >
|
||||
A "mongodb-find-one" tool finds and retrieves a single document from a MongoDB collection.
|
||||
aliases:
|
||||
- /resources/tools/mongodb-find-one
|
||||
---
|
||||
|
||||
## About
|
||||
|
||||
A `mongodb-find-one` tool is used to retrieve the **first single document** that matches a specified filter from a MongoDB collection. If multiple documents match the filter, you can use `sort` options to control which document is returned. Otherwise, the selection is not guaranteed.
|
||||
|
||||
The tool returns a single JSON object representing the document, wrapped in a JSON array.
|
||||
|
||||
This tool is compatible with the following source kind:
|
||||
|
||||
* [`mongodb`](../../sources/mongodb.md)
|
||||
|
||||
---
|
||||
|
||||
## Example
|
||||
|
||||
Here's a common use case: finding a specific user by their unique email address and returning their profile information, while excluding sensitive fields like the password hash.
|
||||
|
||||
```yaml
|
||||
tools:
|
||||
get_user_profile:
|
||||
kind: mongodb-find-one
|
||||
source: my-mongo-source
|
||||
description: Retrieves a user's profile by their email address.
|
||||
database: user_data
|
||||
collection: profiles
|
||||
filterPayload: |
|
||||
{ "email": {{json .email}} }
|
||||
filterParams:
|
||||
- name: email
|
||||
type: string
|
||||
description: The email address of the user to find.
|
||||
projectPayload: |
|
||||
{
|
||||
"password_hash": 0,
|
||||
"login_history": 0
|
||||
}
|
||||
```
|
||||
|
||||
## Reference
|
||||
|
||||
| **field** | **type** | **required** | **description** |
|
||||
|:---------------|:---------|:-------------|:---------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| kind | string | true | Must be `mongodb-find-one`. |
|
||||
| source | string | true | The name of the `mongodb` source to use. |
|
||||
| description | string | true | A description of the tool that is passed to the LLM. |
|
||||
| database | string | true | The name of the MongoDB database to query. |
|
||||
| collection | string | true | The name of the MongoDB collection to query. |
|
||||
| filterPayload | string | true | The MongoDB query filter document to select the document. Uses `{{json .param_name}}` for templating. |
|
||||
| filterParams | list | true | A list of parameter objects that define the variables used in the `filterPayload`. |
|
||||
| projectPayload | string | false | An optional MongoDB projection document to specify which fields to include (1) or exclude (0) in the result. |
|
||||
| projectParams | list | false | A list of parameter objects for the `projectPayload`. |
|
||||
| sortPayload | string | false | An optional MongoDB sort document. Useful for selecting which document to return if the filter matches multiple (e.g., get the most recent). |
|
||||
| sortParams | list | false | A list of parameter objects for the `sortPayload`. |
|
||||
@@ -1,70 +0,0 @@
|
||||
---
|
||||
title: "mongodb-find"
|
||||
type: docs
|
||||
weight: 1
|
||||
description: >
|
||||
A "mongodb-find" tool finds and retrieves documents from a MongoDB collection.
|
||||
aliases:
|
||||
- /resources/tools/mongodb-find
|
||||
---
|
||||
|
||||
## About
|
||||
|
||||
A `mongodb-find` tool is used to query a MongoDB collection and retrieve documents that match a specified filter. It's a flexible tool that allows you to shape the output by selecting specific fields (**projection**), ordering the results (**sorting**), and restricting the number of documents returned (**limiting**).
|
||||
|
||||
The tool returns a JSON array of the documents found.
|
||||
|
||||
This tool is compatible with the following source kind:
|
||||
|
||||
* [`mongodb`](../../sources/mongodb.md)
|
||||
|
||||
## Example
|
||||
|
||||
Here's an example that finds up to 10 users from the `customers` collection who live in a specific city. The results are sorted by their last name, and only their first name, last name, and email are returned.
|
||||
|
||||
```yaml
|
||||
tools:
|
||||
find_local_customers:
|
||||
kind: mongodb-find
|
||||
source: my-mongo-source
|
||||
description: Finds customers by city, sorted by last name.
|
||||
database: crm
|
||||
collection: customers
|
||||
limit: 10
|
||||
filterPayload: |
|
||||
{ "address.city": {{json .city}} }
|
||||
filterParams:
|
||||
- name: city
|
||||
type: string
|
||||
description: The city to search for customers in.
|
||||
projectPayload: |
|
||||
{
|
||||
"first_name": 1,
|
||||
"last_name": 1,
|
||||
"email": 1,
|
||||
"_id": 0
|
||||
}
|
||||
sortPayload: |
|
||||
{ "last_name": {{json .sort_order}} }
|
||||
sortParams:
|
||||
- name: sort_order
|
||||
type: integer
|
||||
description: The sort order (1 for ascending, -1 for descending).
|
||||
```
|
||||
|
||||
## Reference
|
||||
|
||||
| **field** | **type** | **required** | **description** |
|
||||
|:---------------|:---------|:-------------|:----------------------------------------------------------------------------------------------------------------------------|
|
||||
| kind | string | true | Must be `mongodb-find`. |
|
||||
| source | string | true | The name of the `mongodb` source to use. |
|
||||
| description | string | true | A description of the tool that is passed to the LLM. |
|
||||
| database | string | true | The name of the MongoDB database to query. |
|
||||
| collection | string | true | The name of the MongoDB collection to query. |
|
||||
| filterPayload | string | true | The MongoDB query filter document to select which documents to return. Uses `{{json .param_name}}` for templating. |
|
||||
| filterParams | list | true | A list of parameter objects that define the variables used in the `filterPayload`. |
|
||||
| projectPayload | string | false | An optional MongoDB projection document to specify which fields to include (1) or exclude (0) in the results. |
|
||||
| projectParams | list | false | A list of parameter objects for the `projectPayload`. |
|
||||
| sortPayload | string | false | An optional MongoDB sort document to define the order of the returned documents. Use 1 for ascending and -1 for descending. |
|
||||
| sortParams | list | false | A list of parameter objects for the `sortPayload`. |
|
||||
| limit | integer | false | An optional integer specifying the maximum number of documents to return. |
|
||||
@@ -1,52 +0,0 @@
|
||||
---
|
||||
title: "mongodb-insert-many"
|
||||
type: docs
|
||||
weight: 1
|
||||
description: >
|
||||
A "mongodb-insert-many" tool inserts multiple new documents into a MongoDB collection.
|
||||
aliases:
|
||||
- /resources/tools/mongodb-insert-many
|
||||
---
|
||||
|
||||
## About
|
||||
|
||||
The `mongodb-insert-many` tool inserts **multiple new documents** into a specified MongoDB collection in a single bulk operation. This is highly efficient for adding large amounts of data at once.
|
||||
|
||||
This tool takes one required parameter named `data`. This `data` parameter must be a string containing a **JSON array of document objects**. Upon successful insertion, the tool returns a JSON array containing the unique `_id` of **each** new document that was created.
|
||||
|
||||
This tool is compatible with the following source kind:
|
||||
|
||||
* [`mongodb`](../../sources/mongodb.md)
|
||||
|
||||
---
|
||||
|
||||
## Example
|
||||
|
||||
Here is an example configuration for a tool that logs multiple events at once.
|
||||
|
||||
```yaml
|
||||
tools:
|
||||
log_batch_events:
|
||||
kind: mongodb-insert-many
|
||||
source: my-mongo-source
|
||||
description: Inserts a batch of event logs into the database.
|
||||
database: logging
|
||||
collection: events
|
||||
canonical: true
|
||||
```
|
||||
|
||||
An LLM would call this tool by providing an array of documents as a JSON string in the `data` parameter, like this:
|
||||
`tool_code: log_batch_events(data='[{"event": "login", "user": "user1"}, {"event": "click", "user": "user2"}, {"event": "logout", "user": "user1"}]')`
|
||||
|
||||
---
|
||||
|
||||
## Reference
|
||||
|
||||
| **field** | **type** | **required** | **description** |
|
||||
|:------------|:---------|:-------------|:---------------------------------------------------------------------------------------------------|
|
||||
| kind | string | true | Must be `mongodb-insert-many`. |
|
||||
| source | string | true | The name of the `mongodb` source to use. |
|
||||
| description | string | true | A description of the tool that is passed to the LLM. |
|
||||
| database | string | true | The name of the MongoDB database containing the collection. |
|
||||
| collection | string | true | The name of the MongoDB collection into which the documents will be inserted. |
|
||||
| canonical | bool | true | Determines if the data string is parsed using MongoDB's Canonical or Relaxed Extended JSON format. |
|
||||
@@ -1,48 +0,0 @@
|
||||
---
|
||||
title: "mongodb-insert-one"
|
||||
type: docs
|
||||
weight: 1
|
||||
description: >
|
||||
A "mongodb-insert-one" tool inserts a single new document into a MongoDB collection.
|
||||
aliases:
|
||||
- /resources/tools/mongodb-insert-one
|
||||
---
|
||||
|
||||
## About
|
||||
|
||||
The `mongodb-insert-one` tool inserts a **single new document** into a specified MongoDB collection.
|
||||
|
||||
This tool takes one required parameter named `data`, which must be a string containing the JSON object you want to insert. Upon successful insertion, the tool returns the unique `_id` of the newly created document.
|
||||
|
||||
This tool is compatible with the following source kind:
|
||||
|
||||
* [`mongodb`](../../sources/mongodb.md)
|
||||
|
||||
## Example
|
||||
|
||||
Here is an example configuration for a tool that adds a new user to a `users` collection.
|
||||
|
||||
```yaml
|
||||
tools:
|
||||
create_new_user:
|
||||
kind: mongodb-insert-one
|
||||
source: my-mongo-source
|
||||
description: Creates a new user record in the database.
|
||||
database: user_data
|
||||
collection: users
|
||||
canonical: false
|
||||
```
|
||||
|
||||
An LLM would call this tool by providing the document as a JSON string in the `data` parameter, like this:
|
||||
`tool_code: create_new_user(data='{"email": "new.user@example.com", "name": "Jane Doe", "status": "active"}')`
|
||||
|
||||
## Reference
|
||||
|
||||
| **field** | **type** | **required** | **description** |
|
||||
|:------------|:---------|:-------------|:---------------------------------------------------------------------------------------------------|
|
||||
| kind | string | true | Must be `mongodb-insert-one`. |
|
||||
| source | string | true | The name of the `mongodb` source to use. |
|
||||
| description | string | true | A description of the tool that is passed to the LLM. |
|
||||
| database | string | true | The name of the MongoDB database containing the collection. |
|
||||
| collection | string | true | The name of the MongoDB collection into which the document will be inserted. |
|
||||
| canonical | bool | true | Determines if the data string is parsed using MongoDB's Canonical or Relaxed Extended JSON format. |
|
||||
@@ -1,68 +0,0 @@
|
||||
---
|
||||
title: "mongodb-update-many"
|
||||
type: docs
|
||||
weight: 1
|
||||
description: >
|
||||
A "mongodb-update-many" tool updates all documents in a MongoDB collection that match a filter.
|
||||
aliases:
|
||||
- /resources/tools/mongodb-update-many
|
||||
---
|
||||
|
||||
## About
|
||||
|
||||
A `mongodb-update-many` tool updates **all** documents within a specified MongoDB collection that match a given filter. It locates the documents using a `filterPayload` and applies the modifications defined in an `updatePayload`.
|
||||
|
||||
The tool returns an array of three integers: `[ModifiedCount, UpsertedCount, MatchedCount]`.
|
||||
|
||||
This tool is compatible with the following source kind:
|
||||
|
||||
* [`mongodb`](../../sources/mongodb.md)
|
||||
|
||||
---
|
||||
|
||||
## Example
|
||||
|
||||
Here's an example configuration. This tool applies a discount to all items within a specific category and also marks them as being on sale.
|
||||
|
||||
```yaml
|
||||
tools:
|
||||
apply_category_discount:
|
||||
kind: mongodb-update-many
|
||||
source: my-mongo-source
|
||||
description: Use this tool to apply a discount to all items in a given category.
|
||||
database: products
|
||||
collection: inventory
|
||||
filterPayload: |
|
||||
{ "category": {{json .category_name}} }
|
||||
filterParams:
|
||||
- name: category_name
|
||||
type: string
|
||||
description: The category of items to update.
|
||||
updatePayload: |
|
||||
{
|
||||
"$mul": { "price": {{json .discount_multiplier}} },
|
||||
"$set": { "on_sale": true }
|
||||
}
|
||||
updateParams:
|
||||
- name: discount_multiplier
|
||||
type: number
|
||||
description: The multiplier to apply to the price (e.g., 0.8 for a 20% discount).
|
||||
canonical: false
|
||||
upsert: false
|
||||
```
|
||||
|
||||
## Reference
|
||||
|
||||
| **field** | **type** | **required** | **description** |
|
||||
|:--------------|:---------|:-------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| kind | string | true | Must be `mongodb-update-many`. |
|
||||
| source | string | true | The name of the `mongodb` source to use. |
|
||||
| description | string | true | A description of the tool that is passed to the LLM. |
|
||||
| database | string | true | The name of the MongoDB database containing the collection. |
|
||||
| collection | string | true | The name of the MongoDB collection in which to update documents. |
|
||||
| filterPayload | string | true | The MongoDB query filter document to select the documents for updating. It's written as a Go template, using `{{json .param_name}}` to insert parameters. |
|
||||
| filterParams | list | true | A list of parameter objects that define the variables used in the `filterPayload`. |
|
||||
| updatePayload | string | true | The MongoDB update document, It's written as a Go template, using `{{json .param_name}}` to insert parameters. |
|
||||
| updateParams | list | true | A list of parameter objects that define the variables used in the `updatePayload`. |
|
||||
| canonical | bool | true | Determines if the `filterPayload` and `updatePayload` strings are parsed using MongoDB's Canonical or Relaxed Extended JSON format. **Canonical** is stricter about type representation, while **Relaxed** is more lenient. |
|
||||
| upsert | bool | false | If `true`, a new document is created if no document matches the `filterPayload`. Defaults to `false`. |
|
||||
@@ -1,66 +0,0 @@
|
||||
---
|
||||
title: "mongodb-update-one"
|
||||
type: docs
|
||||
weight: 1
|
||||
description: >
|
||||
A "mongodb-update-one" tool updates a single document in a MongoDB collection.
|
||||
aliases:
|
||||
- /resources/tools/mongodb-update-one
|
||||
---
|
||||
|
||||
## About
|
||||
|
||||
A `mongodb-update-one` tool updates a single document within a specified MongoDB collection. It locates the document to be updated using a `filterPayload` and applies modifications defined in an `updatePayload`. If the filter matches multiple documents, only the first one found will be updated.
|
||||
|
||||
This tool is compatible with the following source kind:
|
||||
|
||||
* [`mongodb`](../../sources/mongodb.md)
|
||||
|
||||
---
|
||||
|
||||
## Example
|
||||
|
||||
Here's an example of a `mongodb-update-one` tool configuration. This tool updates the `stock` and `status` fields of a document in the `inventory` collection where the `item` field matches a provided value. If no matching document is found, the `upsert: true` option will create a new one.
|
||||
|
||||
```yaml
|
||||
tools:
|
||||
update_inventory_item:
|
||||
kind: mongodb-update-one
|
||||
source: my-mongo-source
|
||||
description: Use this tool to update an item's stock and status in the inventory.
|
||||
database: products
|
||||
collection: inventory
|
||||
filterPayload: |
|
||||
{ "item": {{json .item_name}} }
|
||||
filterParams:
|
||||
- name: item_name
|
||||
type: string
|
||||
description: The name of the item to update.
|
||||
updatePayload: |
|
||||
{ "$set": { "stock": {{json .new_stock}}, "status": {{json .new_status}} } }
|
||||
updateParams:
|
||||
- name: new_stock
|
||||
type: integer
|
||||
description: The new stock quantity.
|
||||
- name: new_status
|
||||
type: string
|
||||
description: The new status of the item (e.g., "In Stock", "Backordered").
|
||||
canonical: false
|
||||
upsert: true
|
||||
```
|
||||
|
||||
## Reference
|
||||
|
||||
| **field** | **type** | **required** | **description** |
|
||||
|:--------------|:---------|:-------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| kind | string | true | Must be `mongodb-update-one`. |
|
||||
| source | string | true | The name of the `mongodb` source to use. |
|
||||
| description | string | true | A description of the tool that is passed to the LLM. |
|
||||
| database | string | true | The name of the MongoDB database containing the collection. |
|
||||
| collection | string | true | The name of the MongoDB collection to update a document in. |
|
||||
| filterPayload | string | true | The MongoDB query filter document to select the document for updating. It's written as a Go template, using `{{json .param_name}}` to insert parameters. |
|
||||
| filterParams | list | true | A list of parameter objects that define the variables used in the `filterPayload`. |
|
||||
| updatePayload | string | true | The MongoDB update document, which specifies the modifications. This often uses update operators like `$set`. It's written as a Go template, using `{{json .param_name}}` to insert parameters. |
|
||||
| updateParams | list | true | A list of parameter objects that define the variables used in the `updatePayload`. |
|
||||
| canonical | bool | true | Determines if the `updatePayload` string is parsed using MongoDB's Canonical or Relaxed Extended JSON format. **Canonical** is stricter about type representation (e.g., `{"$numberInt": "42"}`), while **Relaxed** is more lenient (e.g., `42`). |
|
||||
| upsert | bool | false | If `true`, a new document is created if no document matches the `filterPayload`. Defaults to `false`. |
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
title: "Utility tools"
|
||||
type: docs
|
||||
weight: 1
|
||||
description: >
|
||||
Tools that provide utility.
|
||||
---
|
||||
@@ -14,9 +14,9 @@ A `wait` tool pauses execution for a specified duration. This can be useful in w
|
||||
|
||||
`wait` takes one input parameter `duration` which is a string representing the time to wait (e.g., "10s", "2m", "1h").
|
||||
|
||||
{{< notice info >}}
|
||||
{{% notice info %}}
|
||||
This tool is intended for developer assistant workflows with human-in-the-loop and shouldn't be used for production agents.
|
||||
{{< /notice >}}
|
||||
{{% /notice %}}
|
||||
|
||||
## Example
|
||||
|
||||
|
||||
@@ -31,8 +31,6 @@ func TestLoadPrebuiltToolYAMLs(t *testing.T) {
|
||||
"cloud-sql-postgres",
|
||||
"firestore",
|
||||
"looker",
|
||||
"mssql",
|
||||
"mysql",
|
||||
"postgres",
|
||||
"spanner-postgres",
|
||||
"spanner",
|
||||
@@ -75,8 +73,6 @@ func TestGetPrebuiltTool(t *testing.T) {
|
||||
cloudsqlmysql_config, _ := Get("cloud-sql-mysql")
|
||||
cloudsqlmssql_config, _ := Get("cloud-sql-mssql")
|
||||
firestoreconfig, _ := Get("firestore")
|
||||
mysql_config, _ := Get("mysql")
|
||||
mssql_config, _ := Get("mssql")
|
||||
postgresconfig, _ := Get("postgres")
|
||||
spanner_config, _ := Get("spanner")
|
||||
spannerpg_config, _ := Get("spanner-postgres")
|
||||
@@ -101,12 +97,6 @@ func TestGetPrebuiltTool(t *testing.T) {
|
||||
if len(firestoreconfig) <= 0 {
|
||||
t.Fatalf("unexpected error: could not fetch firestore prebuilt tools yaml")
|
||||
}
|
||||
if len(mysql_config) <= 0 {
|
||||
t.Fatalf("unexpected error: could not fetch mysql prebuilt tools yaml")
|
||||
}
|
||||
if len(mssql_config) <= 0 {
|
||||
t.Fatalf("unexpected error: could not fetch mssql prebuilt tools yaml")
|
||||
}
|
||||
if len(postgresconfig) <= 0 {
|
||||
t.Fatalf("unexpected error: could not fetch postgres prebuilt tools yaml")
|
||||
}
|
||||
|
||||
@@ -1,269 +0,0 @@
|
||||
sources:
|
||||
mssql-source:
|
||||
kind: mssql
|
||||
host: ${MSSQL_HOST}
|
||||
port: ${MSSQL_PORT}
|
||||
database: ${MSSQL_DATABASE}
|
||||
user: ${MSSQL_USER}
|
||||
password: ${MSSQL_PASSWORD}
|
||||
tools:
|
||||
execute_sql:
|
||||
kind: mssql-execute-sql
|
||||
source: mssql-source
|
||||
description: Use this tool to execute SQL.
|
||||
|
||||
list_tables:
|
||||
kind: mssql-sql
|
||||
source: mssql-source
|
||||
description: "Lists detailed schema information (object type, columns, constraints, indexes, triggers, comment) as JSON for user-created tables (ordinary or partitioned). Filters by a comma-separated list of names. If names are omitted, lists all tables in user schemas."
|
||||
statement: |
|
||||
WITH table_info AS (
|
||||
SELECT
|
||||
t.object_id AS table_oid,
|
||||
s.name AS schema_name,
|
||||
t.name AS table_name,
|
||||
dp.name AS table_owner, -- Schema's owner principal name
|
||||
CAST(ep.value AS NVARCHAR(MAX)) AS table_comment, -- Cast for JSON compatibility
|
||||
CASE
|
||||
WHEN EXISTS ( -- Check if the table has more than one partition for any of its indexes or heap
|
||||
SELECT 1 FROM sys.partitions p
|
||||
WHERE p.object_id = t.object_id AND p.partition_number > 1
|
||||
) THEN 'PARTITIONED TABLE'
|
||||
ELSE 'TABLE'
|
||||
END AS object_type_detail
|
||||
FROM
|
||||
sys.tables t
|
||||
INNER JOIN
|
||||
sys.schemas s ON t.schema_id = s.schema_id
|
||||
LEFT JOIN
|
||||
sys.database_principals dp ON s.principal_id = dp.principal_id
|
||||
LEFT JOIN
|
||||
sys.extended_properties ep ON ep.major_id = t.object_id AND ep.minor_id = 0 AND ep.class = 1 AND ep.name = 'MS_Description'
|
||||
WHERE
|
||||
t.type = 'U' -- User tables
|
||||
AND s.name NOT IN ('sys', 'INFORMATION_SCHEMA', 'guest', 'db_owner', 'db_accessadmin', 'db_backupoperator', 'db_datareader', 'db_datawriter', 'db_ddladmin', 'db_denydatareader', 'db_denydatawriter', 'db_securityadmin')
|
||||
AND (@table_names IS NULL OR LTRIM(RTRIM(@table_names)) = '' OR t.name IN (SELECT LTRIM(RTRIM(value)) FROM STRING_SPLIT(@table_names, ',')))
|
||||
),
|
||||
columns_info AS (
|
||||
SELECT
|
||||
c.object_id AS table_oid,
|
||||
c.name AS column_name,
|
||||
CONCAT(
|
||||
UPPER(TY.name), -- Base type name
|
||||
CASE
|
||||
WHEN TY.name IN ('char', 'varchar', 'nchar', 'nvarchar', 'binary', 'varbinary') THEN
|
||||
CONCAT('(', IIF(c.max_length = -1, 'MAX', CAST(c.max_length / CASE WHEN TY.name IN ('nchar', 'nvarchar') THEN 2 ELSE 1 END AS VARCHAR(10))), ')')
|
||||
WHEN TY.name IN ('decimal', 'numeric') THEN
|
||||
CONCAT('(', c.precision, ',', c.scale, ')')
|
||||
WHEN TY.name IN ('datetime2', 'datetimeoffset', 'time') THEN
|
||||
CONCAT('(', c.scale, ')')
|
||||
ELSE ''
|
||||
END
|
||||
) AS data_type,
|
||||
c.column_id AS column_ordinal_position,
|
||||
IIF(c.is_nullable = 0, CAST(1 AS BIT), CAST(0 AS BIT)) AS is_not_nullable,
|
||||
dc.definition AS column_default,
|
||||
CAST(epc.value AS NVARCHAR(MAX)) AS column_comment
|
||||
FROM
|
||||
sys.columns c
|
||||
JOIN
|
||||
table_info ti ON c.object_id = ti.table_oid
|
||||
JOIN
|
||||
sys.types TY ON c.user_type_id = TY.user_type_id AND TY.is_user_defined = 0 -- Ensure we get base types
|
||||
LEFT JOIN
|
||||
sys.default_constraints dc ON c.object_id = dc.parent_object_id AND c.column_id = dc.parent_column_id
|
||||
LEFT JOIN
|
||||
sys.extended_properties epc ON epc.major_id = c.object_id AND epc.minor_id = c.column_id AND epc.class = 1 AND epc.name = 'MS_Description'
|
||||
),
|
||||
constraints_info AS (
|
||||
-- Primary Keys & Unique Constraints
|
||||
SELECT
|
||||
kc.parent_object_id AS table_oid,
|
||||
kc.name AS constraint_name,
|
||||
REPLACE(kc.type_desc, '_CONSTRAINT', '') AS constraint_type, -- 'PRIMARY_KEY', 'UNIQUE'
|
||||
STUFF((SELECT ', ' + col.name
|
||||
FROM sys.index_columns ic
|
||||
JOIN sys.columns col ON ic.object_id = col.object_id AND ic.column_id = col.column_id
|
||||
WHERE ic.object_id = kc.parent_object_id AND ic.index_id = kc.unique_index_id
|
||||
ORDER BY ic.key_ordinal
|
||||
FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)'), 1, 2, '') AS constraint_columns,
|
||||
NULL AS foreign_key_referenced_table,
|
||||
NULL AS foreign_key_referenced_columns,
|
||||
CASE kc.type
|
||||
WHEN 'PK' THEN 'PRIMARY KEY (' + STUFF((SELECT ', ' + col.name FROM sys.index_columns ic JOIN sys.columns col ON ic.object_id = col.object_id AND ic.column_id = col.column_id WHERE ic.object_id = kc.parent_object_id AND ic.index_id = kc.unique_index_id ORDER BY ic.key_ordinal FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)'), 1, 2, '') + ')'
|
||||
WHEN 'UQ' THEN 'UNIQUE (' + STUFF((SELECT ', ' + col.name FROM sys.index_columns ic JOIN sys.columns col ON ic.object_id = col.object_id AND ic.column_id = col.column_id WHERE ic.object_id = kc.parent_object_id AND ic.index_id = kc.unique_index_id ORDER BY ic.key_ordinal FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)'), 1, 2, '') + ')'
|
||||
END AS constraint_definition
|
||||
FROM sys.key_constraints kc
|
||||
JOIN table_info ti ON kc.parent_object_id = ti.table_oid
|
||||
UNION ALL
|
||||
-- Foreign Keys
|
||||
SELECT
|
||||
fk.parent_object_id AS table_oid,
|
||||
fk.name AS constraint_name,
|
||||
'FOREIGN KEY' AS constraint_type,
|
||||
STUFF((SELECT ', ' + pc.name
|
||||
FROM sys.foreign_key_columns fkc
|
||||
JOIN sys.columns pc ON fkc.parent_object_id = pc.object_id AND fkc.parent_column_id = pc.column_id
|
||||
WHERE fkc.constraint_object_id = fk.object_id
|
||||
ORDER BY fkc.constraint_column_id
|
||||
FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)'), 1, 2, '') AS constraint_columns,
|
||||
SCHEMA_NAME(rt.schema_id) + '.' + OBJECT_NAME(fk.referenced_object_id) AS foreign_key_referenced_table,
|
||||
STUFF((SELECT ', ' + rc.name
|
||||
FROM sys.foreign_key_columns fkc
|
||||
JOIN sys.columns rc ON fkc.referenced_object_id = rc.object_id AND fkc.referenced_column_id = rc.column_id
|
||||
WHERE fkc.constraint_object_id = fk.object_id
|
||||
ORDER BY fkc.constraint_column_id
|
||||
FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)'), 1, 2, '') AS foreign_key_referenced_columns,
|
||||
OBJECT_DEFINITION(fk.object_id) AS constraint_definition
|
||||
FROM sys.foreign_keys fk
|
||||
JOIN sys.tables rt ON fk.referenced_object_id = rt.object_id
|
||||
JOIN table_info ti ON fk.parent_object_id = ti.table_oid
|
||||
UNION ALL
|
||||
-- Check Constraints
|
||||
SELECT
|
||||
cc.parent_object_id AS table_oid,
|
||||
cc.name AS constraint_name,
|
||||
'CHECK' AS constraint_type,
|
||||
NULL AS constraint_columns, -- Definition includes column context
|
||||
NULL AS foreign_key_referenced_table,
|
||||
NULL AS foreign_key_referenced_columns,
|
||||
cc.definition AS constraint_definition
|
||||
FROM sys.check_constraints cc
|
||||
JOIN table_info ti ON cc.parent_object_id = ti.table_oid
|
||||
),
|
||||
indexes_info AS (
|
||||
SELECT
|
||||
i.object_id AS table_oid,
|
||||
i.name AS index_name,
|
||||
i.type_desc AS index_method, -- CLUSTERED, NONCLUSTERED, XML, etc.
|
||||
i.is_unique,
|
||||
i.is_primary_key AS is_primary,
|
||||
STUFF((SELECT ', ' + c.name
|
||||
FROM sys.index_columns ic
|
||||
JOIN sys.columns c ON i.object_id = c.object_id AND ic.column_id = c.column_id
|
||||
WHERE ic.object_id = i.object_id AND ic.index_id = i.index_id AND ic.is_included_column = 0
|
||||
ORDER BY ic.key_ordinal
|
||||
FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)'), 1, 2, '') AS index_columns,
|
||||
(
|
||||
'COLUMNS: (' + ISNULL(STUFF((SELECT ', ' + c.name + CASE WHEN ic.is_descending_key = 1 THEN ' DESC' ELSE '' END
|
||||
FROM sys.index_columns ic
|
||||
JOIN sys.columns c ON i.object_id = c.object_id AND ic.column_id = c.column_id
|
||||
WHERE ic.object_id = i.object_id AND ic.index_id = i.index_id AND ic.is_included_column = 0
|
||||
ORDER BY ic.key_ordinal FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)'), 1, 2, ''), 'N/A') + ')' +
|
||||
ISNULL(CHAR(13)+CHAR(10) + 'INCLUDE: (' + STUFF((SELECT ', ' + c.name
|
||||
FROM sys.index_columns ic
|
||||
JOIN sys.columns c ON i.object_id = c.object_id AND ic.column_id = c.column_id
|
||||
WHERE ic.object_id = i.object_id AND ic.index_id = i.index_id AND ic.is_included_column = 1
|
||||
ORDER BY ic.index_column_id FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)'), 1, 2, '') + ')', '') +
|
||||
ISNULL(CHAR(13)+CHAR(10) + 'FILTER: (' + i.filter_definition + ')', '')
|
||||
) AS index_definition_details
|
||||
FROM
|
||||
sys.indexes i
|
||||
JOIN
|
||||
table_info ti ON i.object_id = ti.table_oid
|
||||
WHERE i.type <> 0 -- Exclude Heaps
|
||||
AND i.name IS NOT NULL -- Exclude unnamed heap indexes; named indexes (PKs are often named) are preferred.
|
||||
),
|
||||
triggers_info AS (
|
||||
SELECT
|
||||
tr.parent_id AS table_oid,
|
||||
tr.name AS trigger_name,
|
||||
OBJECT_DEFINITION(tr.object_id) AS trigger_definition,
|
||||
CASE tr.is_disabled WHEN 0 THEN 'ENABLED' ELSE 'DISABLED' END AS trigger_enabled_state
|
||||
FROM
|
||||
sys.triggers tr
|
||||
JOIN
|
||||
table_info ti ON tr.parent_id = ti.table_oid
|
||||
WHERE
|
||||
tr.is_ms_shipped = 0
|
||||
AND tr.parent_class_desc = 'OBJECT_OR_COLUMN' -- DML Triggers on tables/views
|
||||
)
|
||||
SELECT
|
||||
ti.schema_name,
|
||||
ti.table_name AS object_name,
|
||||
(
|
||||
SELECT
|
||||
ti.schema_name AS schema_name,
|
||||
ti.table_name AS object_name,
|
||||
ti.object_type_detail AS object_type,
|
||||
ti.table_owner AS owner,
|
||||
ti.table_comment AS comment,
|
||||
JSON_QUERY(ISNULL((
|
||||
SELECT
|
||||
ci.column_name,
|
||||
ci.data_type,
|
||||
ci.column_ordinal_position,
|
||||
ci.is_not_nullable,
|
||||
ci.column_default,
|
||||
ci.column_comment
|
||||
FROM columns_info ci
|
||||
WHERE ci.table_oid = ti.table_oid
|
||||
ORDER BY ci.column_ordinal_position
|
||||
FOR JSON PATH
|
||||
), '[]')) AS columns,
|
||||
JSON_QUERY(ISNULL((
|
||||
SELECT
|
||||
cons.constraint_name,
|
||||
cons.constraint_type,
|
||||
cons.constraint_definition,
|
||||
JSON_QUERY(
|
||||
CASE
|
||||
WHEN cons.constraint_columns IS NOT NULL AND LTRIM(RTRIM(cons.constraint_columns)) <> ''
|
||||
THEN '[' + (SELECT STRING_AGG('"' + LTRIM(RTRIM(value)) + '"', ',') FROM STRING_SPLIT(cons.constraint_columns, ',')) + ']'
|
||||
ELSE '[]'
|
||||
END
|
||||
) AS constraint_columns,
|
||||
cons.foreign_key_referenced_table,
|
||||
JSON_QUERY(
|
||||
CASE
|
||||
WHEN cons.foreign_key_referenced_columns IS NOT NULL AND LTRIM(RTRIM(cons.foreign_key_referenced_columns)) <> ''
|
||||
THEN '[' + (SELECT STRING_AGG('"' + LTRIM(RTRIM(value)) + '"', ',') FROM STRING_SPLIT(cons.foreign_key_referenced_columns, ',')) + ']'
|
||||
ELSE '[]'
|
||||
END
|
||||
) AS foreign_key_referenced_columns
|
||||
FROM constraints_info cons
|
||||
WHERE cons.table_oid = ti.table_oid
|
||||
FOR JSON PATH
|
||||
), '[]')) AS constraints,
|
||||
JSON_QUERY(ISNULL((
|
||||
SELECT
|
||||
ii.index_name,
|
||||
ii.index_definition_details AS index_definition,
|
||||
ii.is_unique,
|
||||
ii.is_primary,
|
||||
ii.index_method,
|
||||
JSON_QUERY(
|
||||
CASE
|
||||
WHEN ii.index_columns IS NOT NULL AND LTRIM(RTRIM(ii.index_columns)) <> ''
|
||||
THEN '[' + (SELECT STRING_AGG('"' + LTRIM(RTRIM(value)) + '"', ',') FROM STRING_SPLIT(ii.index_columns, ',')) + ']'
|
||||
ELSE '[]'
|
||||
END
|
||||
) AS index_columns
|
||||
FROM indexes_info ii
|
||||
WHERE ii.table_oid = ti.table_oid
|
||||
FOR JSON PATH
|
||||
), '[]')) AS indexes,
|
||||
JSON_QUERY(ISNULL((
|
||||
SELECT
|
||||
tri.trigger_name,
|
||||
tri.trigger_definition,
|
||||
tri.trigger_enabled_state
|
||||
FROM triggers_info tri
|
||||
WHERE tri.table_oid = ti.table_oid
|
||||
FOR JSON PATH
|
||||
), '[]')) AS triggers
|
||||
FOR JSON PATH, WITHOUT_ARRAY_WRAPPER -- Creates a single JSON object for this table's details
|
||||
) AS object_details
|
||||
FROM
|
||||
table_info ti
|
||||
ORDER BY
|
||||
ti.schema_name, ti.table_name;
|
||||
parameters:
|
||||
- name: table_names
|
||||
type: string
|
||||
description: "Optional: A comma-separated list of table names. If empty, details for all tables in user-accessible schemas will be listed."
|
||||
|
||||
toolsets:
|
||||
mssql-database-tools:
|
||||
- execute_sql
|
||||
- list_tables
|
||||
@@ -1,172 +0,0 @@
|
||||
sources:
|
||||
mysql-source:
|
||||
kind: mysql
|
||||
host: ${MYSQL_HOST}
|
||||
port: ${MYSQL_PORT}
|
||||
database: ${MYSQL_DATABASE}
|
||||
user: ${MYSQL_USER}
|
||||
password: ${MYSQL_PASSWORD}
|
||||
queryTimeout: 30s # Optional
|
||||
tools:
|
||||
execute_sql:
|
||||
kind: mysql-execute-sql
|
||||
source: mysql-source
|
||||
description: Use this tool to execute SQL.
|
||||
list_tables:
|
||||
kind: mysql-sql
|
||||
source: mysql-source
|
||||
description: "Lists detailed schema information (object type, columns, constraints, indexes, triggers, comment) as JSON for user-created tables (ordinary or partitioned). Filters by a comma-separated list of names. If names are omitted, lists all tables in user schemas."
|
||||
statement: |
|
||||
SELECT
|
||||
T.TABLE_SCHEMA AS schema_name,
|
||||
T.TABLE_NAME AS object_name,
|
||||
CONVERT( JSON_OBJECT(
|
||||
'schema_name', T.TABLE_SCHEMA,
|
||||
'object_name', T.TABLE_NAME,
|
||||
'object_type', 'TABLE',
|
||||
'owner', (
|
||||
SELECT
|
||||
IFNULL(U.GRANTEE, 'N/A')
|
||||
FROM
|
||||
INFORMATION_SCHEMA.SCHEMA_PRIVILEGES U
|
||||
WHERE
|
||||
U.TABLE_SCHEMA = T.TABLE_SCHEMA
|
||||
LIMIT 1
|
||||
),
|
||||
'comment', IFNULL(T.TABLE_COMMENT, ''),
|
||||
'columns', (
|
||||
SELECT
|
||||
IFNULL(
|
||||
JSON_ARRAYAGG(
|
||||
JSON_OBJECT(
|
||||
'column_name', C.COLUMN_NAME,
|
||||
'data_type', C.COLUMN_TYPE,
|
||||
'ordinal_position', C.ORDINAL_POSITION,
|
||||
'is_not_nullable', IF(C.IS_NULLABLE = 'NO', TRUE, FALSE),
|
||||
'column_default', C.COLUMN_DEFAULT,
|
||||
'column_comment', IFNULL(C.COLUMN_COMMENT, '')
|
||||
)
|
||||
),
|
||||
JSON_ARRAY()
|
||||
)
|
||||
FROM
|
||||
INFORMATION_SCHEMA.COLUMNS C
|
||||
WHERE
|
||||
C.TABLE_SCHEMA = T.TABLE_SCHEMA AND C.TABLE_NAME = T.TABLE_NAME
|
||||
ORDER BY C.ORDINAL_POSITION
|
||||
),
|
||||
'constraints', (
|
||||
SELECT
|
||||
IFNULL(
|
||||
JSON_ARRAYAGG(
|
||||
JSON_OBJECT(
|
||||
'constraint_name', TC.CONSTRAINT_NAME,
|
||||
'constraint_type',
|
||||
CASE TC.CONSTRAINT_TYPE
|
||||
WHEN 'PRIMARY KEY' THEN 'PRIMARY KEY'
|
||||
WHEN 'FOREIGN KEY' THEN 'FOREIGN KEY'
|
||||
WHEN 'UNIQUE' THEN 'UNIQUE'
|
||||
ELSE TC.CONSTRAINT_TYPE
|
||||
END,
|
||||
'constraint_definition', '',
|
||||
'constraint_columns', (
|
||||
SELECT
|
||||
IFNULL(JSON_ARRAYAGG(KCU.COLUMN_NAME), JSON_ARRAY())
|
||||
FROM
|
||||
INFORMATION_SCHEMA.KEY_COLUMN_USAGE KCU
|
||||
WHERE
|
||||
KCU.CONSTRAINT_SCHEMA = TC.CONSTRAINT_SCHEMA
|
||||
AND KCU.CONSTRAINT_NAME = TC.CONSTRAINT_NAME
|
||||
AND KCU.TABLE_NAME = TC.TABLE_NAME
|
||||
ORDER BY KCU.ORDINAL_POSITION
|
||||
),
|
||||
'foreign_key_referenced_table', IF(TC.CONSTRAINT_TYPE = 'FOREIGN KEY', RC.REFERENCED_TABLE_NAME, NULL),
|
||||
'foreign_key_referenced_columns', IF(TC.CONSTRAINT_TYPE = 'FOREIGN KEY',
|
||||
(SELECT IFNULL(JSON_ARRAYAGG(FKCU.REFERENCED_COLUMN_NAME), JSON_ARRAY())
|
||||
FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE FKCU
|
||||
WHERE FKCU.CONSTRAINT_SCHEMA = TC.CONSTRAINT_SCHEMA
|
||||
AND FKCU.CONSTRAINT_NAME = TC.CONSTRAINT_NAME
|
||||
AND FKCU.TABLE_NAME = TC.TABLE_NAME
|
||||
AND FKCU.REFERENCED_TABLE_NAME IS NOT NULL
|
||||
ORDER BY FKCU.ORDINAL_POSITION),
|
||||
NULL
|
||||
)
|
||||
)
|
||||
),
|
||||
JSON_ARRAY()
|
||||
)
|
||||
FROM
|
||||
INFORMATION_SCHEMA.TABLE_CONSTRAINTS TC
|
||||
LEFT JOIN
|
||||
INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS RC
|
||||
ON TC.CONSTRAINT_SCHEMA = RC.CONSTRAINT_SCHEMA
|
||||
AND TC.CONSTRAINT_NAME = RC.CONSTRAINT_NAME
|
||||
AND TC.TABLE_NAME = RC.TABLE_NAME
|
||||
WHERE
|
||||
TC.TABLE_SCHEMA = T.TABLE_SCHEMA AND TC.TABLE_NAME = T.TABLE_NAME
|
||||
),
|
||||
'indexes', (
|
||||
SELECT
|
||||
IFNULL(
|
||||
JSON_ARRAYAGG(
|
||||
JSON_OBJECT(
|
||||
'index_name', IndexData.INDEX_NAME,
|
||||
'is_unique', IF(IndexData.NON_UNIQUE = 0, TRUE, FALSE),
|
||||
'is_primary', IF(IndexData.INDEX_NAME = 'PRIMARY', TRUE, FALSE),
|
||||
'index_columns', IFNULL(IndexData.INDEX_COLUMNS_ARRAY, JSON_ARRAY())
|
||||
)
|
||||
),
|
||||
JSON_ARRAY()
|
||||
)
|
||||
FROM (
|
||||
SELECT
|
||||
S.TABLE_SCHEMA,
|
||||
S.TABLE_NAME,
|
||||
S.INDEX_NAME,
|
||||
MIN(S.NON_UNIQUE) AS NON_UNIQUE, -- Aggregate NON_UNIQUE here to get unique status for the index
|
||||
JSON_ARRAYAGG(S.COLUMN_NAME) AS INDEX_COLUMNS_ARRAY -- Aggregate columns into an array for this index
|
||||
FROM
|
||||
INFORMATION_SCHEMA.STATISTICS S
|
||||
WHERE
|
||||
S.TABLE_SCHEMA = T.TABLE_SCHEMA AND S.TABLE_NAME = T.TABLE_NAME
|
||||
GROUP BY
|
||||
S.TABLE_SCHEMA, S.TABLE_NAME, S.INDEX_NAME
|
||||
) AS IndexData
|
||||
ORDER BY IndexData.INDEX_NAME
|
||||
),
|
||||
'triggers', (
|
||||
SELECT
|
||||
IFNULL(
|
||||
JSON_ARRAYAGG(
|
||||
JSON_OBJECT(
|
||||
'trigger_name', TR.TRIGGER_NAME,
|
||||
'trigger_definition', TR.ACTION_STATEMENT
|
||||
)
|
||||
),
|
||||
JSON_ARRAY()
|
||||
)
|
||||
FROM
|
||||
INFORMATION_SCHEMA.TRIGGERS TR
|
||||
WHERE
|
||||
TR.EVENT_OBJECT_SCHEMA = T.TABLE_SCHEMA AND TR.EVENT_OBJECT_TABLE = T.TABLE_NAME
|
||||
ORDER BY TR.TRIGGER_NAME
|
||||
)
|
||||
) USING utf8mb4) AS object_details
|
||||
FROM
|
||||
INFORMATION_SCHEMA.TABLES T
|
||||
CROSS JOIN (SELECT @table_names := ?) AS variables
|
||||
WHERE
|
||||
T.TABLE_SCHEMA NOT IN ('mysql', 'information_schema', 'performance_schema', 'sys')
|
||||
AND (NULLIF(TRIM(@table_names), '') IS NULL OR FIND_IN_SET(T.TABLE_NAME, @table_names))
|
||||
AND T.TABLE_TYPE = 'BASE TABLE'
|
||||
ORDER BY
|
||||
T.TABLE_SCHEMA, T.TABLE_NAME;
|
||||
parameters:
|
||||
- name: table_names
|
||||
type: string
|
||||
description: "Optional: A comma-separated list of table names. If empty, details for all tables in user-accessible schemas will be listed."
|
||||
default: ""
|
||||
toolsets:
|
||||
mysql-database-tools:
|
||||
- execute_sql
|
||||
- list_tables
|
||||
@@ -67,11 +67,6 @@ func (r Config) Initialize(ctx context.Context, tracer trace.Tracer) (sources.So
|
||||
return nil, fmt.Errorf("unable to get logger from ctx: %s", err)
|
||||
}
|
||||
|
||||
userAgent, err := util.UserAgentFromContext(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
duration, err := time.ParseDuration(r.Timeout)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to parse Timeout string as time.Duration: %s", err)
|
||||
@@ -81,7 +76,6 @@ func (r Config) Initialize(ctx context.Context, tracer trace.Tracer) (sources.So
|
||||
logger.WarnContext(ctx, "Insecure HTTP is enabled for Looker source %s. TLS certificate verification is skipped.\n", r.Name)
|
||||
}
|
||||
cfg := rtl.ApiSettings{
|
||||
AgentTag: userAgent,
|
||||
BaseUrl: r.BaseURL,
|
||||
ApiVersion: "4.0",
|
||||
VerifySsl: (r.SslVerification == "true"),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2025 Google LLC
|
||||
// Copyright 2024 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -20,7 +20,6 @@ import (
|
||||
|
||||
"github.com/goccy/go-yaml"
|
||||
"github.com/googleapis/genai-toolbox/internal/sources"
|
||||
"github.com/googleapis/genai-toolbox/internal/util"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
@@ -96,13 +95,8 @@ func initMongoDBClient(ctx context.Context, tracer trace.Tracer, name, uri strin
|
||||
ctx, span := sources.InitConnectionSpan(ctx, tracer, SourceKind, name)
|
||||
defer span.End()
|
||||
|
||||
userAgent, err := util.UserAgentFromContext(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create a new MongoDB client
|
||||
clientOpts := options.Client().ApplyURI(uri).SetAppName(userAgent)
|
||||
clientOpts := options.Client().ApplyURI(uri)
|
||||
client, err := mongo.Connect(ctx, clientOpts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to create MongoDB client: %w", err)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2025 Google LLC
|
||||
// Copyright 2024 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
|
||||
@@ -15,11 +15,8 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
var validName = regexp.MustCompile(`^[a-zA-Z0-9_-]*$`)
|
||||
@@ -28,7 +25,6 @@ func IsValidName(s string) bool {
|
||||
return validName.MatchString(s)
|
||||
}
|
||||
|
||||
// ConvertAnySliceToTyped a []any to typed slice ([]string, []int, []float etc.)
|
||||
func ConvertAnySliceToTyped(s []any, itemType string) (any, error) {
|
||||
var typedSlice any
|
||||
switch itemType {
|
||||
@@ -75,44 +71,3 @@ func ConvertAnySliceToTyped(s []any, itemType string) (any, error) {
|
||||
}
|
||||
return typedSlice, nil
|
||||
}
|
||||
|
||||
// convertParamToJSON is a Go template helper function to convert a parameter to JSON formatted string.
|
||||
func convertParamToJSON(param any) (string, error) {
|
||||
jsonData, err := json.Marshal(param)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to marshal param to JSON: %w", err)
|
||||
}
|
||||
return string(jsonData), nil
|
||||
}
|
||||
|
||||
// PopulateTemplateWithJSON populate a Go template with a custom `json` array formatter
|
||||
func PopulateTemplateWithJSON(templateName, templateString string, data map[string]any) (string, error) {
|
||||
funcMap := template.FuncMap{
|
||||
"json": convertParamToJSON,
|
||||
}
|
||||
|
||||
tmpl, err := template.New(templateName).Funcs(funcMap).Parse(templateString)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error parsing template '%s': %w", templateName, err)
|
||||
}
|
||||
|
||||
var result bytes.Buffer
|
||||
err = tmpl.Execute(&result, data)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error executing template '%s': %w", templateName, err)
|
||||
}
|
||||
return result.String(), nil
|
||||
}
|
||||
|
||||
// CheckDuplicateParameters verify there are no duplicate parameter names
|
||||
func CheckDuplicateParameters(ps Parameters) error {
|
||||
seenNames := make(map[string]bool)
|
||||
for _, p := range ps {
|
||||
pName := p.GetName()
|
||||
if _, exists := seenNames[pName]; exists {
|
||||
return fmt.Errorf("parameter name must be unique across all parameter fields. Duplicate parameter: %s", pName)
|
||||
}
|
||||
seenNames[pName] = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -105,16 +105,6 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
if paramManifest == nil {
|
||||
paramManifest = make([]tools.ParameterManifest, 0)
|
||||
}
|
||||
|
||||
// Verify there are no duplicate parameter names
|
||||
seenNames := make(map[string]bool)
|
||||
for _, param := range paramManifest {
|
||||
if _, exists := seenNames[param.Name]; exists {
|
||||
return nil, fmt.Errorf("parameter name must be unique across queryParams, bodyParams, and headerParams. Duplicate parameter: %s", param.Name)
|
||||
}
|
||||
seenNames[param.Name] = true
|
||||
}
|
||||
|
||||
pathMcpManifest := cfg.PathParams.McpManifest()
|
||||
queryMcpManifest := cfg.QueryParams.McpManifest()
|
||||
bodyMcpManifest := cfg.BodyParams.McpManifest()
|
||||
@@ -153,6 +143,15 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
Required: concatRequiredManifest,
|
||||
}
|
||||
|
||||
// Verify there are no duplicate parameter names
|
||||
seenNames := make(map[string]bool)
|
||||
for _, param := range paramManifest {
|
||||
if _, exists := seenNames[param.Name]; exists {
|
||||
return nil, fmt.Errorf("parameter name must be unique across queryParams, bodyParams, and headerParams. Duplicate parameter: %s", param.Name)
|
||||
}
|
||||
seenNames[param.Name] = true
|
||||
}
|
||||
|
||||
mcpManifest := tools.McpManifest{
|
||||
Name: cfg.Name,
|
||||
Description: cfg.Description,
|
||||
@@ -208,6 +207,15 @@ type Tool struct {
|
||||
mcpManifest tools.McpManifest
|
||||
}
|
||||
|
||||
// helper function to convert a parameter to JSON formatted string.
|
||||
func convertParamToJSON(param any) (string, error) {
|
||||
jsonData, err := json.Marshal(param)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to marshal param to JSON: %w", err)
|
||||
}
|
||||
return string(jsonData), nil
|
||||
}
|
||||
|
||||
// Helper function to generate the HTTP request body upon Tool invocation.
|
||||
func getRequestBody(bodyParams tools.Parameters, requestBodyPayload string, paramsMap map[string]any) (string, error) {
|
||||
bodyParamValues, err := tools.GetParams(bodyParams, paramsMap)
|
||||
@@ -216,11 +224,20 @@ func getRequestBody(bodyParams tools.Parameters, requestBodyPayload string, para
|
||||
}
|
||||
bodyParamsMap := bodyParamValues.AsMap()
|
||||
|
||||
requestBodyStr, err := tools.PopulateTemplateWithJSON("HTTPToolRequestBody", requestBodyPayload, bodyParamsMap)
|
||||
if err != nil {
|
||||
return "", err
|
||||
// Create a FuncMap to format array parameters
|
||||
funcMap := template.FuncMap{
|
||||
"json": convertParamToJSON,
|
||||
}
|
||||
return requestBodyStr, nil
|
||||
templ, err := template.New("body").Funcs(funcMap).Parse(requestBodyPayload)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error parsing request body: %s", err)
|
||||
}
|
||||
var result bytes.Buffer
|
||||
err = templ.Execute(&result, bodyParamsMap)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error replacing body payload: %s", err)
|
||||
}
|
||||
return result.String(), nil
|
||||
}
|
||||
|
||||
// Helper function to generate the HTTP request URL upon Tool invocation.
|
||||
|
||||
75
internal/tools/mongodb/common/common.go
Normal file
75
internal/tools/mongodb/common/common.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"text/template"
|
||||
|
||||
"github.com/googleapis/genai-toolbox/internal/tools"
|
||||
)
|
||||
|
||||
// helper function to convert a parameter to JSON formatted string.
|
||||
func ConvertParamToJSON(param any) (string, error) {
|
||||
jsonData, err := json.Marshal(param)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to marshal param to JSON: %w", err)
|
||||
}
|
||||
return string(jsonData), nil
|
||||
}
|
||||
|
||||
func ParsePayloadTemplate(params tools.Parameters, payload string, paramsMap map[string]any) (string, error) {
|
||||
// Create a map for request body parameters
|
||||
cleanParamsMap := make(map[string]any)
|
||||
for _, p := range params {
|
||||
k := p.GetName()
|
||||
v, ok := paramsMap[k]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("missing parameter %s", k)
|
||||
}
|
||||
cleanParamsMap[k] = v
|
||||
}
|
||||
|
||||
// Create a FuncMap to format array parameters
|
||||
funcMap := template.FuncMap{
|
||||
"json": ConvertParamToJSON,
|
||||
}
|
||||
templ, err := template.New("template").Funcs(funcMap).Parse(payload)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error parsing: %s", err)
|
||||
}
|
||||
var result bytes.Buffer
|
||||
err = templ.Execute(&result, cleanParamsMap)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error replacing payload: %s", err)
|
||||
}
|
||||
return result.String(), nil
|
||||
}
|
||||
|
||||
func GetUpdate(params tools.Parameters, payload string, paramsMap map[string]any) (string, error) {
|
||||
// Create a map for request body parameters
|
||||
cleanParamsMap := make(map[string]any)
|
||||
for _, p := range params {
|
||||
k := p.GetName()
|
||||
v, ok := paramsMap[k]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("missing update parameter %s", k)
|
||||
}
|
||||
cleanParamsMap[k] = v
|
||||
}
|
||||
|
||||
// Create a FuncMap to format array parameters
|
||||
funcMap := template.FuncMap{
|
||||
"json": ConvertParamToJSON,
|
||||
}
|
||||
templ, err := template.New("filter").Funcs(funcMap).Parse(payload)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error parsing filter: %s", err)
|
||||
}
|
||||
var result bytes.Buffer
|
||||
err = templ.Execute(&result, cleanParamsMap)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error replacing filter payload: %s", err)
|
||||
}
|
||||
return result.String(), nil
|
||||
}
|
||||
237
internal/tools/mongodb/mongodbaggregate/mongodbaggregate.go
Normal file
237
internal/tools/mongodb/mongodbaggregate/mongodbaggregate.go
Normal file
@@ -0,0 +1,237 @@
|
||||
// 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 mongodbaggregate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
"github.com/goccy/go-yaml"
|
||||
mongosrc "github.com/googleapis/genai-toolbox/internal/sources/mongodb"
|
||||
"github.com/googleapis/genai-toolbox/internal/tools/mongodb/common"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
|
||||
"github.com/googleapis/genai-toolbox/internal/sources"
|
||||
"github.com/googleapis/genai-toolbox/internal/tools"
|
||||
)
|
||||
|
||||
const kind string = "mongodb-aggregate"
|
||||
|
||||
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"`
|
||||
Source string `yaml:"source" validate:"required"`
|
||||
AuthRequired []string `yaml:"authRequired" validate:"required"`
|
||||
Description string `yaml:"description" validate:"required"`
|
||||
Database string `yaml:"database" validate:"required"`
|
||||
Collection string `yaml:"collection" validate:"required"`
|
||||
PipelinePayload string `yaml:"pipelinePayload" validate:"required"`
|
||||
PipelineParams tools.Parameters `yaml:"pipelineParams" validate:"required"`
|
||||
Canonical bool `yaml:"canonical"`
|
||||
ReadOnly bool `yaml:"readOnly"`
|
||||
}
|
||||
|
||||
// validate interface
|
||||
var _ tools.ToolConfig = Config{}
|
||||
|
||||
func (cfg Config) ToolConfigKind() string {
|
||||
return kind
|
||||
}
|
||||
|
||||
func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) {
|
||||
// verify source exists
|
||||
rawS, ok := srcs[cfg.Source]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("no source named %q configured", cfg.Source)
|
||||
}
|
||||
|
||||
// verify the source is compatible
|
||||
s, ok := rawS.(*mongosrc.Source)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid source for %q tool: source kind must be `mongodb`", kind)
|
||||
}
|
||||
|
||||
// Create a slice for all parameters
|
||||
allParameters := slices.Concat(cfg.PipelineParams)
|
||||
|
||||
// Create parameter MCP manifest
|
||||
paramManifest := slices.Concat(
|
||||
cfg.PipelineParams.Manifest(),
|
||||
)
|
||||
if paramManifest == nil {
|
||||
paramManifest = make([]tools.ParameterManifest, 0)
|
||||
}
|
||||
|
||||
pipelineMcpManifest := cfg.PipelineParams.McpManifest()
|
||||
|
||||
// Concatenate parameters for MCP `required` field
|
||||
concatRequiredManifest := slices.Concat(
|
||||
pipelineMcpManifest.Required,
|
||||
)
|
||||
if concatRequiredManifest == nil {
|
||||
concatRequiredManifest = []string{}
|
||||
}
|
||||
|
||||
// Concatenate parameters for MCP `properties` field
|
||||
concatPropertiesManifest := make(map[string]tools.ParameterMcpManifest)
|
||||
for name, p := range pipelineMcpManifest.Properties {
|
||||
concatPropertiesManifest[name] = p
|
||||
}
|
||||
|
||||
// Create a new McpToolsSchema with all parameters
|
||||
paramMcpManifest := tools.McpToolsSchema{
|
||||
Type: "object",
|
||||
Properties: concatPropertiesManifest,
|
||||
Required: concatRequiredManifest,
|
||||
}
|
||||
|
||||
// Verify there are no duplicate parameter names
|
||||
seenNames := make(map[string]bool)
|
||||
for _, param := range paramManifest {
|
||||
if _, exists := seenNames[param.Name]; exists {
|
||||
return nil, fmt.Errorf("parameter name must be unique across pipelineParams, and sortParams. Duplicate parameter: %s", param.Name)
|
||||
}
|
||||
seenNames[param.Name] = true
|
||||
}
|
||||
|
||||
mcpManifest := tools.McpManifest{
|
||||
Name: cfg.Name,
|
||||
Description: cfg.Description,
|
||||
InputSchema: paramMcpManifest,
|
||||
}
|
||||
|
||||
// finish tool setup
|
||||
return Tool{
|
||||
Name: cfg.Name,
|
||||
Kind: kind,
|
||||
AuthRequired: cfg.AuthRequired,
|
||||
Collection: cfg.Collection,
|
||||
PipelinePayload: cfg.PipelinePayload,
|
||||
PipelineParams: cfg.PipelineParams,
|
||||
Canonical: cfg.Canonical,
|
||||
ReadOnly: cfg.ReadOnly,
|
||||
AllParams: allParameters,
|
||||
database: s.Client.Database(cfg.Database),
|
||||
manifest: tools.Manifest{Description: cfg.Description, Parameters: paramManifest, AuthRequired: cfg.AuthRequired},
|
||||
mcpManifest: mcpManifest,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// validate interface
|
||||
var _ tools.Tool = Tool{}
|
||||
|
||||
type Tool struct {
|
||||
Name string `yaml:"name"`
|
||||
Kind string `yaml:"kind"`
|
||||
Description string `yaml:"description"`
|
||||
AuthRequired []string `yaml:"authRequired"`
|
||||
Collection string `yaml:"collection"`
|
||||
PipelinePayload string `yaml:"pipelinePayload"`
|
||||
PipelineParams tools.Parameters `yaml:"pipelineParams"`
|
||||
Canonical bool `yaml:"canonical"`
|
||||
ReadOnly bool `yaml:"readOnly"`
|
||||
AllParams tools.Parameters `yaml:"allParams"`
|
||||
|
||||
database *mongo.Database
|
||||
manifest tools.Manifest
|
||||
mcpManifest tools.McpManifest
|
||||
}
|
||||
|
||||
func (t Tool) Invoke(ctx context.Context, params tools.ParamValues) (any, error) {
|
||||
paramsMap := params.AsMap()
|
||||
|
||||
pipelineString, err := common.ParsePayloadTemplate(t.PipelineParams, t.PipelinePayload, paramsMap)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error populating pipeline: %s", err)
|
||||
}
|
||||
|
||||
var pipeline = []bson.M{}
|
||||
err = bson.UnmarshalExtJSON([]byte(pipelineString), t.Canonical, &pipeline)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if t.ReadOnly {
|
||||
//fail if we do a merge or an out
|
||||
for _, stage := range pipeline {
|
||||
for key := range stage {
|
||||
if key == "$merge" || key == "$out" {
|
||||
return nil, fmt.Errorf("this is not a read-only pipeline: %+v", stage)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cur, err := t.database.Collection(t.Collection).Aggregate(ctx, pipeline)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer cur.Close(ctx)
|
||||
|
||||
var data = []any{}
|
||||
err = cur.All(ctx, &data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(data) == 0 {
|
||||
return []any{}, nil
|
||||
}
|
||||
|
||||
var final []any
|
||||
for _, item := range data {
|
||||
tmp, _ := bson.MarshalExtJSON(item, false, false)
|
||||
var tmp2 any
|
||||
err = json.Unmarshal(tmp, &tmp2)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
final = append(final, tmp2)
|
||||
}
|
||||
|
||||
return final, err
|
||||
}
|
||||
|
||||
func (t Tool) ParseParams(data map[string]any, claims map[string]map[string]any) (tools.ParamValues, error) {
|
||||
return tools.ParseParams(t.AllParams, 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 tools.IsAuthorized(t.AuthRequired, verifiedAuthServices)
|
||||
}
|
||||
141
internal/tools/mongodb/mongodbaggregate/mongodbaggregate_test.go
Normal file
141
internal/tools/mongodb/mongodbaggregate/mongodbaggregate_test.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 mongodbaggregate_test
|
||||
|
||||
import (
|
||||
"github.com/googleapis/genai-toolbox/internal/tools/mongodb/mongodbaggregate"
|
||||
"strings"
|
||||
"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"
|
||||
"github.com/googleapis/genai-toolbox/internal/tools"
|
||||
)
|
||||
|
||||
func TestParseFromYamlMongoQuery(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: mongodb-aggregate
|
||||
source: my-instance
|
||||
description: some description
|
||||
database: test_db
|
||||
collection: test_coll
|
||||
readOnly: true
|
||||
pipelinePayload: |
|
||||
[{ $match: { name: {{json .name}} }}]
|
||||
pipelineParams:
|
||||
- name: name
|
||||
type: string
|
||||
description: small description
|
||||
`,
|
||||
want: server.ToolConfigs{
|
||||
"example_tool": mongodbaggregate.Config{
|
||||
Name: "example_tool",
|
||||
Kind: "mongodb-aggregate",
|
||||
Source: "my-instance",
|
||||
AuthRequired: []string{},
|
||||
Database: "test_db",
|
||||
Collection: "test_coll",
|
||||
Description: "some description",
|
||||
PipelinePayload: "[{ $match: { name: {{json .name}} }}]\n",
|
||||
PipelineParams: tools.Parameters{
|
||||
&tools.StringParameter{
|
||||
CommonParameter: tools.CommonParameter{
|
||||
Name: "name",
|
||||
Type: "string",
|
||||
Desc: "small description",
|
||||
},
|
||||
},
|
||||
},
|
||||
ReadOnly: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestFailParseFromYamlMongoQuery(t *testing.T) {
|
||||
ctx, err := testutils.ContextWithNewLogger()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
tcs := []struct {
|
||||
desc string
|
||||
in string
|
||||
err string
|
||||
}{
|
||||
{
|
||||
desc: "Invalid method",
|
||||
in: `
|
||||
tools:
|
||||
example_tool:
|
||||
kind: mongodb-aggregate
|
||||
source: my-instance
|
||||
description: some description
|
||||
collection: test_coll
|
||||
pipelinePayload: |
|
||||
[{ $match: { name : {{json .name}} }}]
|
||||
`,
|
||||
err: `unable to parse tool "example_tool" as kind "mongodb-aggregate"`,
|
||||
},
|
||||
}
|
||||
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("expect parsing to fail")
|
||||
}
|
||||
errStr := err.Error()
|
||||
if !strings.Contains(errStr, tc.err) {
|
||||
t.Fatalf("unexpected error string: got %q, want substring %q", errStr, tc.err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
|
||||
"github.com/goccy/go-yaml"
|
||||
mongosrc "github.com/googleapis/genai-toolbox/internal/sources/mongodb"
|
||||
"github.com/googleapis/genai-toolbox/internal/tools/mongodb/common"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
@@ -80,24 +81,51 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
// Create a slice for all parameters
|
||||
allParameters := slices.Concat(cfg.FilterParams)
|
||||
|
||||
// Verify no duplicate parameter names
|
||||
err := tools.CheckDuplicateParameters(allParameters)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create Toolbox manifest
|
||||
paramManifest := allParameters.Manifest()
|
||||
|
||||
// Create parameter MCP manifest
|
||||
paramManifest := slices.Concat(
|
||||
cfg.FilterParams.Manifest(),
|
||||
)
|
||||
if paramManifest == nil {
|
||||
paramManifest = make([]tools.ParameterManifest, 0)
|
||||
}
|
||||
|
||||
// Create MCP manifest
|
||||
filterMcpManifest := cfg.FilterParams.McpManifest()
|
||||
|
||||
// Concatenate parameters for MCP `required` field
|
||||
concatRequiredManifest := slices.Concat(
|
||||
filterMcpManifest.Required,
|
||||
)
|
||||
|
||||
if concatRequiredManifest == nil {
|
||||
concatRequiredManifest = []string{}
|
||||
}
|
||||
|
||||
// Concatenate parameters for MCP `properties` field
|
||||
concatPropertiesManifest := make(map[string]tools.ParameterMcpManifest)
|
||||
for name, p := range filterMcpManifest.Properties {
|
||||
concatPropertiesManifest[name] = p
|
||||
}
|
||||
|
||||
// Create a new McpToolsSchema with all parameters
|
||||
paramMcpManifest := tools.McpToolsSchema{
|
||||
Type: "object",
|
||||
Properties: concatPropertiesManifest,
|
||||
Required: concatRequiredManifest,
|
||||
}
|
||||
|
||||
// Verify there are no duplicate parameter names
|
||||
seenNames := make(map[string]bool)
|
||||
for _, param := range paramManifest {
|
||||
if _, exists := seenNames[param.Name]; exists {
|
||||
return nil, fmt.Errorf("parameter name must be unique across filterParams, projectParams, and sortParams. Duplicate parameter: %s", param.Name)
|
||||
}
|
||||
seenNames[param.Name] = true
|
||||
}
|
||||
|
||||
mcpManifest := tools.McpManifest{
|
||||
Name: cfg.Name,
|
||||
Description: cfg.Description,
|
||||
InputSchema: allParameters.McpManifest(),
|
||||
InputSchema: paramMcpManifest,
|
||||
}
|
||||
|
||||
// finish tool setup
|
||||
@@ -136,7 +164,7 @@ type Tool struct {
|
||||
func (t Tool) Invoke(ctx context.Context, params tools.ParamValues) (any, error) {
|
||||
paramsMap := params.AsMap()
|
||||
|
||||
filterString, err := tools.PopulateTemplateWithJSON("MongoDBDeleteManyFilter", t.FilterPayload, paramsMap)
|
||||
filterString, err := common.ParsePayloadTemplate(t.FilterParams, t.FilterPayload, paramsMap)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error populating filter: %s", err)
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
|
||||
"github.com/goccy/go-yaml"
|
||||
mongosrc "github.com/googleapis/genai-toolbox/internal/sources/mongodb"
|
||||
"github.com/googleapis/genai-toolbox/internal/tools/mongodb/common"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
@@ -79,24 +80,51 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
// Create a slice for all parameters
|
||||
allParameters := slices.Concat(cfg.FilterParams)
|
||||
|
||||
// Verify no duplicate parameter names
|
||||
err := tools.CheckDuplicateParameters(allParameters)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create Toolbox manifest
|
||||
paramManifest := allParameters.Manifest()
|
||||
|
||||
// Create parameter MCP manifest
|
||||
paramManifest := slices.Concat(
|
||||
cfg.FilterParams.Manifest(),
|
||||
)
|
||||
if paramManifest == nil {
|
||||
paramManifest = make([]tools.ParameterManifest, 0)
|
||||
}
|
||||
|
||||
// Create MCP manifest
|
||||
filterMcpManifest := cfg.FilterParams.McpManifest()
|
||||
|
||||
// Concatenate parameters for MCP `required` field
|
||||
concatRequiredManifest := slices.Concat(
|
||||
filterMcpManifest.Required,
|
||||
)
|
||||
|
||||
if concatRequiredManifest == nil {
|
||||
concatRequiredManifest = []string{}
|
||||
}
|
||||
|
||||
// Concatenate parameters for MCP `properties` field
|
||||
concatPropertiesManifest := make(map[string]tools.ParameterMcpManifest)
|
||||
for name, p := range filterMcpManifest.Properties {
|
||||
concatPropertiesManifest[name] = p
|
||||
}
|
||||
|
||||
// Create a new McpToolsSchema with all parameters
|
||||
paramMcpManifest := tools.McpToolsSchema{
|
||||
Type: "object",
|
||||
Properties: concatPropertiesManifest,
|
||||
Required: concatRequiredManifest,
|
||||
}
|
||||
|
||||
// Verify there are no duplicate parameter names
|
||||
seenNames := make(map[string]bool)
|
||||
for _, param := range paramManifest {
|
||||
if _, exists := seenNames[param.Name]; exists {
|
||||
return nil, fmt.Errorf("parameter name must be unique across filterParams, projectParams, and sortParams. Duplicate parameter: %s", param.Name)
|
||||
}
|
||||
seenNames[param.Name] = true
|
||||
}
|
||||
|
||||
mcpManifest := tools.McpManifest{
|
||||
Name: cfg.Name,
|
||||
Description: cfg.Description,
|
||||
InputSchema: allParameters.McpManifest(),
|
||||
InputSchema: paramMcpManifest,
|
||||
}
|
||||
|
||||
// finish tool setup
|
||||
@@ -135,7 +163,7 @@ type Tool struct {
|
||||
func (t Tool) Invoke(ctx context.Context, params tools.ParamValues) (any, error) {
|
||||
paramsMap := params.AsMap()
|
||||
|
||||
filterString, err := tools.PopulateTemplateWithJSON("MongoDBDeleteOneFilter", t.FilterPayload, paramsMap)
|
||||
filterString, err := common.ParsePayloadTemplate(t.FilterParams, t.FilterPayload, paramsMap)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error populating filter: %s", err)
|
||||
}
|
||||
|
||||
@@ -14,13 +14,16 @@
|
||||
package mongodbfind
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"slices"
|
||||
"text/template"
|
||||
|
||||
"github.com/goccy/go-yaml"
|
||||
mongosrc "github.com/googleapis/genai-toolbox/internal/sources/mongodb"
|
||||
"github.com/googleapis/genai-toolbox/internal/tools/mongodb/common"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
@@ -85,23 +88,62 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
// Create a slice for all parameters
|
||||
allParameters := slices.Concat(cfg.FilterParams, cfg.ProjectParams, cfg.SortParams)
|
||||
|
||||
// Verify no duplicate parameter names
|
||||
err := tools.CheckDuplicateParameters(allParameters)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create Toolbox manifest
|
||||
paramManifest := allParameters.Manifest()
|
||||
// Create parameter MCP manifest
|
||||
paramManifest := slices.Concat(
|
||||
cfg.FilterParams.Manifest(),
|
||||
cfg.ProjectParams.Manifest(),
|
||||
cfg.SortParams.Manifest(),
|
||||
)
|
||||
if paramManifest == nil {
|
||||
paramManifest = make([]tools.ParameterManifest, 0)
|
||||
}
|
||||
|
||||
// Create MCP manifest
|
||||
filterMcpManifest := cfg.FilterParams.McpManifest()
|
||||
projectMcpManifest := cfg.ProjectParams.McpManifest()
|
||||
sortMcpManifest := cfg.SortParams.McpManifest()
|
||||
|
||||
// Concatenate parameters for MCP `required` field
|
||||
concatRequiredManifest := slices.Concat(
|
||||
filterMcpManifest.Required,
|
||||
projectMcpManifest.Required,
|
||||
sortMcpManifest.Required,
|
||||
)
|
||||
if concatRequiredManifest == nil {
|
||||
concatRequiredManifest = []string{}
|
||||
}
|
||||
|
||||
// Concatenate parameters for MCP `properties` field
|
||||
concatPropertiesManifest := make(map[string]tools.ParameterMcpManifest)
|
||||
for name, p := range filterMcpManifest.Properties {
|
||||
concatPropertiesManifest[name] = p
|
||||
}
|
||||
for name, p := range projectMcpManifest.Properties {
|
||||
concatPropertiesManifest[name] = p
|
||||
}
|
||||
for name, p := range sortMcpManifest.Properties {
|
||||
concatPropertiesManifest[name] = p
|
||||
}
|
||||
|
||||
// Create a new McpToolsSchema with all parameters
|
||||
paramMcpManifest := tools.McpToolsSchema{
|
||||
Type: "object",
|
||||
Properties: concatPropertiesManifest,
|
||||
Required: concatRequiredManifest,
|
||||
}
|
||||
|
||||
// Verify there are no duplicate parameter names
|
||||
seenNames := make(map[string]bool)
|
||||
for _, param := range paramManifest {
|
||||
if _, exists := seenNames[param.Name]; exists {
|
||||
return nil, fmt.Errorf("parameter name must be unique across filterParams, projectParams, and sortParams. Duplicate parameter: %s", param.Name)
|
||||
}
|
||||
seenNames[param.Name] = true
|
||||
}
|
||||
|
||||
mcpManifest := tools.McpManifest{
|
||||
Name: cfg.Name,
|
||||
Description: cfg.Description,
|
||||
InputSchema: allParameters.McpManifest(),
|
||||
InputSchema: paramMcpManifest,
|
||||
}
|
||||
|
||||
// finish tool setup
|
||||
@@ -147,7 +189,7 @@ type Tool struct {
|
||||
mcpManifest tools.McpManifest
|
||||
}
|
||||
|
||||
func getOptions(sortParameters tools.Parameters, projectPayload string, limit int64, paramsMap map[string]any) (*options.FindOptions, error) {
|
||||
func getOptions(sortParameters tools.Parameters, projectParams tools.Parameters, projectPayload string, limit int64, paramsMap map[string]any) (*options.FindOptions, error) {
|
||||
opts := options.Find()
|
||||
|
||||
sort := bson.M{}
|
||||
@@ -160,14 +202,29 @@ func getOptions(sortParameters tools.Parameters, projectPayload string, limit in
|
||||
return opts, nil
|
||||
}
|
||||
|
||||
result, err := tools.PopulateTemplateWithJSON("MongoDBFindProjectString", projectPayload, paramsMap)
|
||||
project := bson.M{}
|
||||
|
||||
for _, p := range projectParams {
|
||||
project[p.GetName()] = paramsMap[p.GetName()]
|
||||
}
|
||||
|
||||
// Create a FuncMap to format array parameters
|
||||
funcMap := template.FuncMap{
|
||||
"json": common.ConvertParamToJSON,
|
||||
}
|
||||
templ, err := template.New("project").Funcs(funcMap).Parse(projectPayload)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error populating project payload: %s", err)
|
||||
return nil, fmt.Errorf("error parsing project: %s", err)
|
||||
}
|
||||
|
||||
var result bytes.Buffer
|
||||
err = templ.Execute(&result, project)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error replacing projection payload: %s", err)
|
||||
}
|
||||
|
||||
var projection any
|
||||
err = bson.UnmarshalExtJSON([]byte(result), false, &projection)
|
||||
err = bson.UnmarshalExtJSON(result.Bytes(), false, &projection)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error unmarshalling projection: %s", err)
|
||||
}
|
||||
@@ -184,13 +241,12 @@ func getOptions(sortParameters tools.Parameters, projectPayload string, limit in
|
||||
func (t Tool) Invoke(ctx context.Context, params tools.ParamValues) (any, error) {
|
||||
paramsMap := params.AsMap()
|
||||
|
||||
filterString, err := tools.PopulateTemplateWithJSON("MongoDBFindFilterString", t.FilterPayload, paramsMap)
|
||||
|
||||
filterString, err := common.ParsePayloadTemplate(t.FilterParams, t.FilterPayload, paramsMap)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error populating filter: %s", err)
|
||||
}
|
||||
|
||||
opts, err := getOptions(t.SortParams, t.ProjectPayload, t.Limit, paramsMap)
|
||||
opts, err := getOptions(t.SortParams, t.ProjectParams, t.ProjectPayload, t.Limit, paramsMap)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error populating options: %s", err)
|
||||
}
|
||||
|
||||
@@ -14,13 +14,16 @@
|
||||
package mongodbfindone
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"slices"
|
||||
"text/template"
|
||||
|
||||
"github.com/goccy/go-yaml"
|
||||
mongosrc "github.com/googleapis/genai-toolbox/internal/sources/mongodb"
|
||||
"github.com/googleapis/genai-toolbox/internal/tools/mongodb/common"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
@@ -84,24 +87,62 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
// Create a slice for all parameters
|
||||
allParameters := slices.Concat(cfg.FilterParams, cfg.ProjectParams, cfg.SortParams)
|
||||
|
||||
// Verify no duplicate parameter names
|
||||
err := tools.CheckDuplicateParameters(allParameters)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create Toolbox manifest
|
||||
paramManifest := allParameters.Manifest()
|
||||
|
||||
// Create parameter MCP manifest
|
||||
paramManifest := slices.Concat(
|
||||
cfg.FilterParams.Manifest(),
|
||||
cfg.ProjectParams.Manifest(),
|
||||
cfg.SortParams.Manifest(),
|
||||
)
|
||||
if paramManifest == nil {
|
||||
paramManifest = make([]tools.ParameterManifest, 0)
|
||||
}
|
||||
|
||||
// Create MCP manifest
|
||||
filterMcpManifest := cfg.FilterParams.McpManifest()
|
||||
projectMcpManifest := cfg.ProjectParams.McpManifest()
|
||||
sortMcpManifest := cfg.SortParams.McpManifest()
|
||||
|
||||
// Concatenate parameters for MCP `required` field
|
||||
concatRequiredManifest := slices.Concat(
|
||||
filterMcpManifest.Required,
|
||||
projectMcpManifest.Required,
|
||||
sortMcpManifest.Required,
|
||||
)
|
||||
if concatRequiredManifest == nil {
|
||||
concatRequiredManifest = []string{}
|
||||
}
|
||||
|
||||
// Concatenate parameters for MCP `properties` field
|
||||
concatPropertiesManifest := make(map[string]tools.ParameterMcpManifest)
|
||||
for name, p := range filterMcpManifest.Properties {
|
||||
concatPropertiesManifest[name] = p
|
||||
}
|
||||
for name, p := range projectMcpManifest.Properties {
|
||||
concatPropertiesManifest[name] = p
|
||||
}
|
||||
for name, p := range sortMcpManifest.Properties {
|
||||
concatPropertiesManifest[name] = p
|
||||
}
|
||||
|
||||
// Create a new McpToolsSchema with all parameters
|
||||
paramMcpManifest := tools.McpToolsSchema{
|
||||
Type: "object",
|
||||
Properties: concatPropertiesManifest,
|
||||
Required: concatRequiredManifest,
|
||||
}
|
||||
|
||||
// Verify there are no duplicate parameter names
|
||||
seenNames := make(map[string]bool)
|
||||
for _, param := range paramManifest {
|
||||
if _, exists := seenNames[param.Name]; exists {
|
||||
return nil, fmt.Errorf("parameter name must be unique across filterParams, projectParams, and sortParams. Duplicate parameter: %s", param.Name)
|
||||
}
|
||||
seenNames[param.Name] = true
|
||||
}
|
||||
|
||||
mcpManifest := tools.McpManifest{
|
||||
Name: cfg.Name,
|
||||
Description: cfg.Description,
|
||||
InputSchema: allParameters.McpManifest(),
|
||||
InputSchema: paramMcpManifest,
|
||||
}
|
||||
|
||||
// finish tool setup
|
||||
@@ -145,7 +186,7 @@ type Tool struct {
|
||||
mcpManifest tools.McpManifest
|
||||
}
|
||||
|
||||
func getOptions(sortParameters tools.Parameters, projectPayload string, paramsMap map[string]any) (*options.FindOneOptions, error) {
|
||||
func getOptions(sortParameters tools.Parameters, projectParams tools.Parameters, projectPayload string, paramsMap map[string]any) (*options.FindOneOptions, error) {
|
||||
opts := options.FindOne()
|
||||
|
||||
sort := bson.M{}
|
||||
@@ -158,14 +199,28 @@ func getOptions(sortParameters tools.Parameters, projectPayload string, paramsMa
|
||||
return opts, nil
|
||||
}
|
||||
|
||||
result, err := tools.PopulateTemplateWithJSON("MongoDBFindOneProjectString", projectPayload, paramsMap)
|
||||
project := bson.M{}
|
||||
for _, p := range projectParams {
|
||||
project[p.GetName()] = paramsMap[p.GetName()]
|
||||
}
|
||||
|
||||
// Create a FuncMap to format array parameters
|
||||
funcMap := template.FuncMap{
|
||||
"json": common.ConvertParamToJSON,
|
||||
}
|
||||
templ, err := template.New("project").Funcs(funcMap).Parse(projectPayload)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error populating project payload: %s", err)
|
||||
return nil, fmt.Errorf("error parsing project: %s", err)
|
||||
}
|
||||
|
||||
var result bytes.Buffer
|
||||
err = templ.Execute(&result, project)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error replacing project payload: %s", err)
|
||||
}
|
||||
|
||||
var projection any
|
||||
err = bson.Unmarshal([]byte(result), &projection)
|
||||
err = bson.Unmarshal(result.Bytes(), &projection)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error unmarshalling projection: %s", err)
|
||||
}
|
||||
@@ -177,13 +232,12 @@ func getOptions(sortParameters tools.Parameters, projectPayload string, paramsMa
|
||||
func (t Tool) Invoke(ctx context.Context, params tools.ParamValues) (any, error) {
|
||||
paramsMap := params.AsMap()
|
||||
|
||||
filterString, err := tools.PopulateTemplateWithJSON("MongoDBFindOneFilterString", t.FilterPayload, paramsMap)
|
||||
|
||||
filterString, err := common.ParsePayloadTemplate(t.FilterParams, t.FilterPayload, paramsMap)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error populating filter: %s", err)
|
||||
}
|
||||
|
||||
opts, err := getOptions(t.SortParams, t.ProjectPayload, paramsMap)
|
||||
opts, err := getOptions(t.SortParams, t.ProjectParams, t.ProjectPayload, paramsMap)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error populating options: %s", err)
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
"github.com/goccy/go-yaml"
|
||||
"github.com/googleapis/genai-toolbox/internal/sources"
|
||||
@@ -77,22 +78,37 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
}
|
||||
|
||||
dataParam := tools.NewStringParameterWithRequired(paramDataKey, "the JSON payload to insert, should be a JSON array of documents", true)
|
||||
parameters := tools.Parameters{dataParam}
|
||||
|
||||
allParameters := tools.Parameters{dataParam}
|
||||
|
||||
// Create Toolbox manifest
|
||||
paramManifest := allParameters.Manifest()
|
||||
// Create parameter MCP manifest
|
||||
paramManifest := slices.Concat(
|
||||
parameters.Manifest(),
|
||||
)
|
||||
|
||||
if paramManifest == nil {
|
||||
paramManifest = make([]tools.ParameterManifest, 0)
|
||||
}
|
||||
|
||||
// Create MCP manifest
|
||||
payloadMcpManifest := dataParam.McpManifest()
|
||||
|
||||
// Concatenate parameters for MCP `properties` field
|
||||
concatPropertiesManifest := map[string]tools.ParameterMcpManifest{
|
||||
paramDataKey: payloadMcpManifest,
|
||||
}
|
||||
|
||||
// Create a new McpToolsSchema with all parameters
|
||||
paramMcpManifest := tools.McpToolsSchema{
|
||||
Type: "object",
|
||||
Properties: concatPropertiesManifest,
|
||||
Required: []string{paramDataKey},
|
||||
}
|
||||
|
||||
mcpManifest := tools.McpManifest{
|
||||
Name: cfg.Name,
|
||||
Description: cfg.Description,
|
||||
InputSchema: allParameters.McpManifest(),
|
||||
InputSchema: paramMcpManifest,
|
||||
}
|
||||
|
||||
// finish tool setup
|
||||
return Tool{
|
||||
Name: cfg.Name,
|
||||
@@ -100,7 +116,7 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
AuthRequired: cfg.AuthRequired,
|
||||
Collection: cfg.Collection,
|
||||
Canonical: cfg.Canonical,
|
||||
PayloadParams: allParameters,
|
||||
PayloadParams: parameters,
|
||||
database: s.Client.Database(cfg.Database),
|
||||
manifest: tools.Manifest{Description: cfg.Description, Parameters: paramManifest, AuthRequired: cfg.AuthRequired},
|
||||
mcpManifest: mcpManifest,
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
"github.com/goccy/go-yaml"
|
||||
"github.com/googleapis/genai-toolbox/internal/sources"
|
||||
@@ -77,21 +78,35 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
}
|
||||
|
||||
payloadParams := tools.NewStringParameterWithRequired(dataParamsKey, "the JSON payload to insert, should be a JSON object", true)
|
||||
parameters := tools.Parameters{payloadParams}
|
||||
|
||||
allParameters := tools.Parameters{payloadParams}
|
||||
|
||||
// Create Toolbox manifest
|
||||
paramManifest := allParameters.Manifest()
|
||||
// Create parameter MCP manifest
|
||||
paramManifest := slices.Concat(
|
||||
parameters.Manifest(),
|
||||
)
|
||||
|
||||
if paramManifest == nil {
|
||||
paramManifest = make([]tools.ParameterManifest, 0)
|
||||
}
|
||||
|
||||
// Create MCP manifest
|
||||
payloadMcpManifest := payloadParams.McpManifest()
|
||||
|
||||
// Concatenate parameters for MCP `properties` field
|
||||
concatPropertiesManifest := map[string]tools.ParameterMcpManifest{
|
||||
dataParamsKey: payloadMcpManifest,
|
||||
}
|
||||
|
||||
// Create a new McpToolsSchema with all parameters
|
||||
paramMcpManifest := tools.McpToolsSchema{
|
||||
Type: "object",
|
||||
Properties: concatPropertiesManifest,
|
||||
Required: []string{dataParamsKey},
|
||||
}
|
||||
|
||||
mcpManifest := tools.McpManifest{
|
||||
Name: cfg.Name,
|
||||
Description: cfg.Description,
|
||||
InputSchema: allParameters.McpManifest(),
|
||||
InputSchema: paramMcpManifest,
|
||||
}
|
||||
|
||||
// finish tool setup
|
||||
@@ -101,7 +116,7 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
AuthRequired: cfg.AuthRequired,
|
||||
Collection: cfg.Collection,
|
||||
Canonical: cfg.Canonical,
|
||||
PayloadParams: allParameters,
|
||||
PayloadParams: parameters,
|
||||
database: s.Client.Database(cfg.Database),
|
||||
manifest: tools.Manifest{Description: cfg.Description, Parameters: paramManifest, AuthRequired: cfg.AuthRequired},
|
||||
mcpManifest: mcpManifest,
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"github.com/googleapis/genai-toolbox/internal/sources"
|
||||
mongosrc "github.com/googleapis/genai-toolbox/internal/sources/mongodb"
|
||||
"github.com/googleapis/genai-toolbox/internal/tools"
|
||||
"github.com/googleapis/genai-toolbox/internal/tools/mongodb/common"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
@@ -55,7 +56,7 @@ type Config struct {
|
||||
FilterParams tools.Parameters `yaml:"filterParams" validate:"required"`
|
||||
UpdatePayload string `yaml:"updatePayload" validate:"required"`
|
||||
UpdateParams tools.Parameters `yaml:"updateParams" validate:"required"`
|
||||
Canonical bool `yaml:"canonical" validate:"required"`
|
||||
Canonical bool `yaml:"canonical" validate:"required"` //i want to force the user to choose
|
||||
Upsert bool `yaml:"upsert"`
|
||||
}
|
||||
|
||||
@@ -82,24 +83,55 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
// Create a slice for all parameters
|
||||
allParameters := slices.Concat(cfg.FilterParams, cfg.FilterParams, cfg.UpdateParams)
|
||||
|
||||
// Verify no duplicate parameter names
|
||||
err := tools.CheckDuplicateParameters(allParameters)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create Toolbox manifest
|
||||
paramManifest := allParameters.Manifest()
|
||||
|
||||
// Create parameter MCP manifest
|
||||
paramManifest := slices.Concat(
|
||||
cfg.FilterParams.Manifest(),
|
||||
)
|
||||
if paramManifest == nil {
|
||||
paramManifest = make([]tools.ParameterManifest, 0)
|
||||
}
|
||||
|
||||
// Create MCP manifest
|
||||
filterMcpManifest := cfg.FilterParams.McpManifest()
|
||||
updateMcpManifest := cfg.UpdateParams.McpManifest()
|
||||
|
||||
// Concatenate parameters for MCP `required` field
|
||||
concatRequiredManifest := slices.Concat(
|
||||
filterMcpManifest.Required,
|
||||
updateMcpManifest.Required,
|
||||
)
|
||||
if concatRequiredManifest == nil {
|
||||
concatRequiredManifest = []string{}
|
||||
}
|
||||
|
||||
// Concatenate parameters for MCP `properties` field
|
||||
concatPropertiesManifest := make(map[string]tools.ParameterMcpManifest)
|
||||
for name, p := range filterMcpManifest.Properties {
|
||||
concatPropertiesManifest[name] = p
|
||||
}
|
||||
for name, p := range updateMcpManifest.Properties {
|
||||
concatPropertiesManifest[name] = p
|
||||
}
|
||||
|
||||
// Verify there are no duplicate parameter names
|
||||
seenNames := make(map[string]bool)
|
||||
for _, param := range paramManifest {
|
||||
if _, exists := seenNames[param.Name]; exists {
|
||||
return nil, fmt.Errorf("parameter name must be unique across filterParams, projectParams, and sortParams. Duplicate parameter: %s", param.Name)
|
||||
}
|
||||
seenNames[param.Name] = true
|
||||
}
|
||||
|
||||
// Create a new McpToolsSchema with all parameters
|
||||
paramMcpManifest := tools.McpToolsSchema{
|
||||
Type: "object",
|
||||
Properties: concatPropertiesManifest,
|
||||
Required: concatRequiredManifest,
|
||||
}
|
||||
|
||||
mcpManifest := tools.McpManifest{
|
||||
Name: cfg.Name,
|
||||
Description: cfg.Description,
|
||||
InputSchema: allParameters.McpManifest(),
|
||||
InputSchema: paramMcpManifest,
|
||||
}
|
||||
|
||||
// finish tool setup
|
||||
@@ -135,7 +167,7 @@ type Tool struct {
|
||||
UpdatePayload string `yaml:"updatePayload" validate:"required"`
|
||||
UpdateParams tools.Parameters `yaml:"updateParams" validate:"required"`
|
||||
AllParams tools.Parameters `yaml:"allParams"`
|
||||
Canonical bool `yaml:"canonical" validation:"required"`
|
||||
Canonical bool `yaml:"canonical" validation:"required"` //i want to force the user to choose
|
||||
Upsert bool `yaml:"upsert"`
|
||||
|
||||
database *mongo.Database
|
||||
@@ -146,7 +178,7 @@ type Tool struct {
|
||||
func (t Tool) Invoke(ctx context.Context, params tools.ParamValues) (any, error) {
|
||||
paramsMap := params.AsMap()
|
||||
|
||||
filterString, err := tools.PopulateTemplateWithJSON("MongoDBUpdateManyFilter", t.FilterPayload, paramsMap)
|
||||
filterString, err := common.ParsePayloadTemplate(t.FilterParams, t.FilterPayload, paramsMap)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error populating filter: %s", err)
|
||||
}
|
||||
@@ -157,7 +189,7 @@ func (t Tool) Invoke(ctx context.Context, params tools.ParamValues) (any, error)
|
||||
return nil, fmt.Errorf("unable to unmarshal filter string: %w", err)
|
||||
}
|
||||
|
||||
updateString, err := tools.PopulateTemplateWithJSON("MongoDBUpdateMany", t.UpdatePayload, paramsMap)
|
||||
updateString, err := common.GetUpdate(t.UpdateParams, t.UpdatePayload, paramsMap)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to get update: %w", err)
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"github.com/googleapis/genai-toolbox/internal/sources"
|
||||
mongosrc "github.com/googleapis/genai-toolbox/internal/sources/mongodb"
|
||||
"github.com/googleapis/genai-toolbox/internal/tools"
|
||||
"github.com/googleapis/genai-toolbox/internal/tools/mongodb/common"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
@@ -56,7 +57,7 @@ type Config struct {
|
||||
UpdatePayload string `yaml:"updatePayload" validate:"required"`
|
||||
UpdateParams tools.Parameters `yaml:"updateParams" validate:"required"`
|
||||
|
||||
Canonical bool `yaml:"canonical" validate:"required"`
|
||||
Canonical bool `yaml:"canonical" validate:"required"` //i want to force the user to choose
|
||||
Upsert bool `yaml:"upsert"`
|
||||
}
|
||||
|
||||
@@ -83,24 +84,50 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
// Create a slice for all parameters
|
||||
allParameters := slices.Concat(cfg.FilterParams, cfg.FilterParams, cfg.UpdateParams)
|
||||
|
||||
// Verify no duplicate parameter names
|
||||
err := tools.CheckDuplicateParameters(allParameters)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create Toolbox manifest
|
||||
paramManifest := allParameters.Manifest()
|
||||
|
||||
// Create parameter MCP manifest
|
||||
paramManifest := slices.Concat(
|
||||
cfg.FilterParams.Manifest(),
|
||||
)
|
||||
if paramManifest == nil {
|
||||
paramManifest = make([]tools.ParameterManifest, 0)
|
||||
}
|
||||
|
||||
// Create MCP manifest
|
||||
// Verify there are no duplicate parameter names
|
||||
seenNames := make(map[string]bool)
|
||||
for _, param := range paramManifest {
|
||||
if _, exists := seenNames[param.Name]; exists {
|
||||
return nil, fmt.Errorf("parameter name must be unique across filterParams, projectParams, and sortParams. Duplicate parameter: %s", param.Name)
|
||||
}
|
||||
seenNames[param.Name] = true
|
||||
}
|
||||
|
||||
filterMcpManifest := cfg.FilterParams.McpManifest()
|
||||
|
||||
// Concatenate parameters for MCP `required` field
|
||||
concatRequiredManifest := slices.Concat(
|
||||
filterMcpManifest.Required,
|
||||
)
|
||||
if concatRequiredManifest == nil {
|
||||
concatRequiredManifest = []string{}
|
||||
}
|
||||
|
||||
// Concatenate parameters for MCP `properties` field
|
||||
concatPropertiesManifest := make(map[string]tools.ParameterMcpManifest)
|
||||
for name, p := range filterMcpManifest.Properties {
|
||||
concatPropertiesManifest[name] = p
|
||||
}
|
||||
|
||||
// Create a new McpToolsSchema with all parameters
|
||||
paramMcpManifest := tools.McpToolsSchema{
|
||||
Type: "object",
|
||||
Properties: concatPropertiesManifest,
|
||||
Required: concatRequiredManifest,
|
||||
}
|
||||
|
||||
mcpManifest := tools.McpManifest{
|
||||
Name: cfg.Name,
|
||||
Description: cfg.Description,
|
||||
InputSchema: allParameters.McpManifest(),
|
||||
InputSchema: paramMcpManifest,
|
||||
}
|
||||
|
||||
// finish tool setup
|
||||
@@ -147,7 +174,7 @@ type Tool struct {
|
||||
func (t Tool) Invoke(ctx context.Context, params tools.ParamValues) (any, error) {
|
||||
paramsMap := params.AsMap()
|
||||
|
||||
filterString, err := tools.PopulateTemplateWithJSON("MongoDBUpdateOneFilter", t.FilterPayload, paramsMap)
|
||||
filterString, err := common.ParsePayloadTemplate(t.FilterParams, t.FilterPayload, paramsMap)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error populating filter: %s", err)
|
||||
}
|
||||
@@ -158,7 +185,7 @@ func (t Tool) Invoke(ctx context.Context, params tools.ParamValues) (any, error)
|
||||
return nil, fmt.Errorf("unable to unmarshal filter string: %w", err)
|
||||
}
|
||||
|
||||
updateString, err := tools.PopulateTemplateWithJSON("MongoDBUpdateOne", t.UpdatePayload, paramsMap)
|
||||
updateString, err := common.GetUpdate(t.UpdateParams, t.UpdatePayload, paramsMap)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to get update: %w", err)
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ package mysqlsql
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
yaml "github.com/goccy/go-yaml"
|
||||
@@ -178,14 +177,6 @@ func (t Tool) Invoke(ctx context.Context, params tools.ParamValues) (any, error)
|
||||
// mysql driver return []uint8 type for "TEXT", "VARCHAR", and "NVARCHAR"
|
||||
// we'll need to cast it back to string
|
||||
switch colTypes[i].DatabaseTypeName() {
|
||||
case "JSON":
|
||||
// unmarshal JSON data before storing to prevent double marshaling
|
||||
var unmarshaledData any
|
||||
err := json.Unmarshal(val.([]byte), &unmarshaledData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to unmarshal json data %s", val)
|
||||
}
|
||||
vMap[name] = unmarshaledData
|
||||
case "TEXT", "VARCHAR", "NVARCHAR":
|
||||
vMap[name] = string(val.([]byte))
|
||||
default:
|
||||
|
||||
@@ -790,15 +790,6 @@ func (p *FloatParameter) Manifest() ParameterManifest {
|
||||
}
|
||||
}
|
||||
|
||||
// McpManifest returns the MCP manifest for the FloatParameter.
|
||||
// json schema only allow numeric types of 'integer' and 'number'.
|
||||
func (p *FloatParameter) McpManifest() ParameterMcpManifest {
|
||||
return ParameterMcpManifest{
|
||||
Type: "number",
|
||||
Description: p.Desc,
|
||||
}
|
||||
}
|
||||
|
||||
// NewBooleanParameter is a convenience function for initializing a BooleanParameter.
|
||||
func NewBooleanParameter(name string, desc string) *BooleanParameter {
|
||||
return &BooleanParameter{
|
||||
|
||||
@@ -1327,7 +1327,7 @@ func TestParamMcpManifest(t *testing.T) {
|
||||
{
|
||||
name: "float",
|
||||
in: tools.NewFloatParameter("foo-float", "bar"),
|
||||
want: tools.ParameterMcpManifest{Type: "number", Description: "bar"},
|
||||
want: tools.ParameterMcpManifest{Type: "float", Description: "bar"},
|
||||
},
|
||||
{
|
||||
name: "boolean",
|
||||
@@ -1385,7 +1385,6 @@ func TestMcpManifest(t *testing.T) {
|
||||
tools.NewStringParameterWithDefault("foo-string", "foo", "bar"),
|
||||
tools.NewStringParameter("foo-string2", "bar"),
|
||||
tools.NewIntParameter("foo-int2", "bar"),
|
||||
tools.NewFloatParameter("foo-float", "bar"),
|
||||
tools.NewArrayParameter("foo-array2", "bar", tools.NewStringParameter("foo-string", "bar")),
|
||||
tools.NewMapParameter("foo-map-int", "a map of ints", "integer"),
|
||||
tools.NewMapParameter("foo-map-any", "a map of any", ""),
|
||||
@@ -1396,7 +1395,6 @@ func TestMcpManifest(t *testing.T) {
|
||||
"foo-string": {Type: "string", Description: "bar"},
|
||||
"foo-string2": {Type: "string", Description: "bar"},
|
||||
"foo-int2": {Type: "integer", Description: "bar"},
|
||||
"foo-float": {Type: "number", Description: "bar"},
|
||||
"foo-array2": {
|
||||
Type: "array",
|
||||
Description: "bar",
|
||||
@@ -1413,7 +1411,7 @@ func TestMcpManifest(t *testing.T) {
|
||||
AdditionalProperties: true,
|
||||
},
|
||||
},
|
||||
Required: []string{"foo-string2", "foo-int2", "foo-float", "foo-array2", "foo-map-int", "foo-map-any"},
|
||||
Required: []string{"foo-string2", "foo-int2", "foo-array2", "foo-map-int", "foo-map-any"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
760
tests/mongodb/mongodb_integration_test.go
Normal file
760
tests/mongodb/mongodb_integration_test.go
Normal file
@@ -0,0 +1,760 @@
|
||||
// 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 mongodb
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"regexp"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/googleapis/genai-toolbox/internal/testutils"
|
||||
"github.com/googleapis/genai-toolbox/tests"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
var (
|
||||
MongoDbSourceKind = "mongodb"
|
||||
MongoDbToolKind = "mongodb-find"
|
||||
MongoDbUri = os.Getenv("MONGODB_URI")
|
||||
MongoDbDatabase = os.Getenv("MONGODB_DATABASE")
|
||||
ServiceAccountEmail = os.Getenv("SERVICE_ACCOUNT_EMAIL")
|
||||
)
|
||||
|
||||
func getMongoDBVars(t *testing.T) map[string]any {
|
||||
switch "" {
|
||||
case MongoDbUri:
|
||||
t.Fatal("'MongoDbUri' not set")
|
||||
case MongoDbDatabase:
|
||||
t.Fatal("'MongoDbDatabase' not set")
|
||||
}
|
||||
return map[string]any{
|
||||
"kind": MongoDbSourceKind,
|
||||
"uri": MongoDbUri,
|
||||
}
|
||||
}
|
||||
|
||||
func initMongoDbDatabase(ctx context.Context, uri, database string) (*mongo.Database, error) {
|
||||
// Create a new mongodb Database
|
||||
client, err := mongo.Connect(ctx, options.Client().ApplyURI(uri))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to connect to mongodb: %s", err)
|
||||
}
|
||||
err = client.Ping(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to connect to mongodb: %s", err)
|
||||
}
|
||||
return client.Database(database), nil
|
||||
}
|
||||
|
||||
func TestMongoDBToolEndpoints(t *testing.T) {
|
||||
sourceConfig := getMongoDBVars(t)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||
defer cancel()
|
||||
|
||||
var args []string
|
||||
|
||||
database, err := initMongoDbDatabase(ctx, MongoDbUri, MongoDbDatabase)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create MongoDB connection: %s", err)
|
||||
}
|
||||
|
||||
// set up data for param tool
|
||||
//setupMongoDB(t, ctx, database)
|
||||
teardownDB := setupMongoDB(t, ctx, database)
|
||||
defer teardownDB(t)
|
||||
|
||||
// Write config into a file and pass it to command
|
||||
toolsFile := getMongoDBToolsConfig(sourceConfig, MongoDbToolKind)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
tests.RunToolGetTest(t)
|
||||
|
||||
select1Want, failInvocationWant, invokeParamWant, invokeIdNullWant, nullString, mcpInvokeParamWant := getMongoDBWants()
|
||||
tests.RunToolInvokeTest(t, select1Want, invokeParamWant, invokeIdNullWant, nullString, true, true)
|
||||
tests.RunMCPToolCallMethod(t, mcpInvokeParamWant, failInvocationWant)
|
||||
|
||||
delete1Want := "1"
|
||||
deleteManyWant := "2"
|
||||
RunToolDeleteInvokeTest(t, delete1Want, deleteManyWant)
|
||||
insert1Want := `["68666e1035bb36bf1b4d47fb"]`
|
||||
insertManyWant := `["68667a6436ec7d0363668db7","68667a6436ec7d0363668db8","68667a6436ec7d0363668db9"]`
|
||||
RunToolInsertInvokeTest(t, insert1Want, insertManyWant)
|
||||
update1Want := "1"
|
||||
updateManyWant := "[2,0,2]"
|
||||
RunToolUpdateInvokeTest(t, update1Want, updateManyWant)
|
||||
aggregate1Want := `[{"id":2}]`
|
||||
aggregateManyWant := `[{"id":500},{"id":501}]`
|
||||
RunToolAggregateInvokeTest(t, aggregate1Want, aggregateManyWant)
|
||||
|
||||
}
|
||||
|
||||
func RunToolDeleteInvokeTest(t *testing.T, delete1Want, deleteManyWant string) {
|
||||
// Test tool invoke endpoint
|
||||
invokeTcs := []struct {
|
||||
name string
|
||||
api string
|
||||
requestHeader map[string]string
|
||||
requestBody io.Reader
|
||||
want string
|
||||
isErr bool
|
||||
}{
|
||||
{
|
||||
name: "invoke my-delete-one-tool",
|
||||
api: "http://127.0.0.1:5000/api/tool/my-delete-one-tool/invoke",
|
||||
requestHeader: map[string]string{},
|
||||
requestBody: bytes.NewBuffer([]byte(`{ "id" : 100 }`)),
|
||||
want: delete1Want,
|
||||
isErr: false,
|
||||
},
|
||||
{
|
||||
name: "invoke my-delete-many-tool",
|
||||
api: "http://127.0.0.1:5000/api/tool/my-delete-many-tool/invoke",
|
||||
requestHeader: map[string]string{},
|
||||
requestBody: bytes.NewBuffer([]byte(`{ "id" : 101 }`)),
|
||||
want: deleteManyWant,
|
||||
isErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range invokeTcs {
|
||||
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// Send Tool invocation request
|
||||
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")
|
||||
for k, v := range tc.requestHeader {
|
||||
req.Header.Add(k, v)
|
||||
}
|
||||
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))
|
||||
}
|
||||
|
||||
// Check response body
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func RunToolInsertInvokeTest(t *testing.T, insert1Want, insertManyWant string) {
|
||||
// Test tool invoke endpoint
|
||||
invokeTcs := []struct {
|
||||
name string
|
||||
api string
|
||||
requestHeader map[string]string
|
||||
requestBody io.Reader
|
||||
want string
|
||||
isErr bool
|
||||
}{
|
||||
{
|
||||
name: "invoke my-insert-one-tool",
|
||||
api: "http://127.0.0.1:5000/api/tool/my-insert-one-tool/invoke",
|
||||
requestHeader: map[string]string{},
|
||||
requestBody: bytes.NewBuffer([]byte(`{ "data" : "{ \"_id\": { \"$oid\": \"68666e1035bb36bf1b4d47fb\" }, \"id\" : 200 }" }"`)),
|
||||
want: insert1Want,
|
||||
isErr: false,
|
||||
},
|
||||
{
|
||||
name: "invoke my-insert-many-tool",
|
||||
api: "http://127.0.0.1:5000/api/tool/my-insert-many-tool/invoke",
|
||||
requestHeader: map[string]string{},
|
||||
requestBody: bytes.NewBuffer([]byte(`{ "data" : "[{ \"_id\": { \"$oid\": \"68667a6436ec7d0363668db7\"} , \"id\" : 201 }, { \"_id\" : { \"$oid\": \"68667a6436ec7d0363668db8\"}, \"id\" : 202 }, { \"_id\": { \"$oid\": \"68667a6436ec7d0363668db9\"}, \"id\": 203 }]" }`)),
|
||||
want: insertManyWant,
|
||||
isErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range invokeTcs {
|
||||
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// Send Tool invocation request
|
||||
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")
|
||||
for k, v := range tc.requestHeader {
|
||||
req.Header.Add(k, v)
|
||||
}
|
||||
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))
|
||||
}
|
||||
|
||||
// Check response body
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func RunToolUpdateInvokeTest(t *testing.T, update1Want, updateManyWant string) {
|
||||
// Test tool invoke endpoint
|
||||
invokeTcs := []struct {
|
||||
name string
|
||||
api string
|
||||
requestHeader map[string]string
|
||||
requestBody io.Reader
|
||||
want string
|
||||
isErr bool
|
||||
}{
|
||||
{
|
||||
name: "invoke my-update-one-tool",
|
||||
api: "http://127.0.0.1:5000/api/tool/my-update-one-tool/invoke",
|
||||
requestHeader: map[string]string{},
|
||||
requestBody: bytes.NewBuffer([]byte(`{ "id": 300, "name": "Bob" }`)),
|
||||
want: update1Want,
|
||||
isErr: false,
|
||||
},
|
||||
{
|
||||
name: "invoke my-update-many-tool",
|
||||
api: "http://127.0.0.1:5000/api/tool/my-update-many-tool/invoke",
|
||||
requestHeader: map[string]string{},
|
||||
requestBody: bytes.NewBuffer([]byte(`{ "id": 400, "name" : "Alice" }`)),
|
||||
want: updateManyWant,
|
||||
isErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range invokeTcs {
|
||||
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// Send Tool invocation request
|
||||
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")
|
||||
for k, v := range tc.requestHeader {
|
||||
req.Header.Add(k, v)
|
||||
}
|
||||
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))
|
||||
}
|
||||
|
||||
// Check response body
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
func RunToolAggregateInvokeTest(t *testing.T, aggregate1Want string, aggregateManyWant string) {
|
||||
// Test tool invoke endpoint
|
||||
invokeTcs := []struct {
|
||||
name string
|
||||
api string
|
||||
requestHeader map[string]string
|
||||
requestBody io.Reader
|
||||
want string
|
||||
isErr bool
|
||||
}{
|
||||
{
|
||||
name: "invoke my-aggregate-tool",
|
||||
api: "http://127.0.0.1:5000/api/tool/my-aggregate-tool/invoke",
|
||||
requestHeader: map[string]string{},
|
||||
requestBody: bytes.NewBuffer([]byte(`{ "name": "Jane" }`)),
|
||||
want: aggregate1Want,
|
||||
isErr: false,
|
||||
},
|
||||
{
|
||||
name: "invoke my-aggregate-tool",
|
||||
api: "http://127.0.0.1:5000/api/tool/my-aggregate-tool/invoke",
|
||||
requestHeader: map[string]string{},
|
||||
requestBody: bytes.NewBuffer([]byte(`{ "name" : "ToBeAggregated" }`)),
|
||||
want: aggregateManyWant,
|
||||
isErr: false,
|
||||
},
|
||||
{
|
||||
name: "invoke my-read-only-aggregate-tool",
|
||||
api: "http://127.0.0.1:5000/api/tool/my-read-only-aggregate-tool/invoke",
|
||||
requestHeader: map[string]string{},
|
||||
requestBody: bytes.NewBuffer([]byte(`{ "name" : "ToBeAggregated" }`)),
|
||||
want: "",
|
||||
isErr: true,
|
||||
},
|
||||
{
|
||||
name: "invoke my-read-write-aggregate-tool",
|
||||
api: "http://127.0.0.1:5000/api/tool/my-read-write-aggregate-tool/invoke",
|
||||
requestHeader: map[string]string{},
|
||||
requestBody: bytes.NewBuffer([]byte(`{ "name" : "ToBeAggregated" }`)),
|
||||
want: "[]",
|
||||
isErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range invokeTcs {
|
||||
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// Send Tool invocation request
|
||||
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")
|
||||
for k, v := range tc.requestHeader {
|
||||
req.Header.Add(k, v)
|
||||
}
|
||||
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))
|
||||
}
|
||||
|
||||
// Check response body
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func setupMongoDB(t *testing.T, ctx context.Context, database *mongo.Database) func(*testing.T) {
|
||||
collectionName := "test_collection"
|
||||
|
||||
documents := []map[string]any{
|
||||
{"_id": 1, "id": 1, "name": "Alice", "email": ServiceAccountEmail},
|
||||
{"_id": 2, "id": 2, "name": "Jane"},
|
||||
{"_id": 3, "id": 3, "name": "Sid"},
|
||||
{"_id": 4, "id": 4, "name": nil},
|
||||
{"_id": 5, "id": 3, "name": "Alice", "email": "alice@gmail.com"},
|
||||
{"_id": 6, "id": 100, "name": "ToBeDeleted", "email": "bob@gmail.com"},
|
||||
{"_id": 7, "id": 101, "name": "ToBeDeleted", "email": "bob1@gmail.com"},
|
||||
{"_id": 8, "id": 101, "name": "ToBeDeleted", "email": "bob2@gmail.com"},
|
||||
{"_id": 9, "id": 300, "name": "ToBeUpdatedToBob", "email": "bob@gmail.com"},
|
||||
{"_id": 10, "id": 400, "name": "ToBeUpdatedToAlice", "email": "alice@gmail.com"},
|
||||
{"_id": 11, "id": 400, "name": "ToBeUpdatedToAlice", "email": "alice@gmail.com"},
|
||||
{"_id": 12, "id": 500, "name": "ToBeAggregated", "email": "agatha@gmail.com"},
|
||||
{"_id": 13, "id": 501, "name": "ToBeAggregated", "email": "agatha@gmail.com"},
|
||||
}
|
||||
for _, doc := range documents {
|
||||
_, err := database.Collection(collectionName).InsertOne(ctx, doc)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to insert test data: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
return func(t *testing.T) {
|
||||
// tear down test
|
||||
err := database.Collection(collectionName).Drop(ctx)
|
||||
if err != nil {
|
||||
t.Errorf("Teardown failed: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func getMongoDBToolsConfig(sourceConfig map[string]any, toolKind string) map[string]any {
|
||||
toolsFile := map[string]any{
|
||||
"sources": map[string]any{
|
||||
"my-instance": sourceConfig,
|
||||
},
|
||||
"authServices": map[string]any{
|
||||
"my-google-auth": map[string]any{
|
||||
"kind": "google",
|
||||
"clientId": tests.ClientId,
|
||||
},
|
||||
},
|
||||
"tools": map[string]any{
|
||||
"my-simple-tool": map[string]any{
|
||||
"kind": "mongodb-find-one",
|
||||
"source": "my-instance",
|
||||
"description": "Simple tool to test end to end functionality.",
|
||||
"collection": "test_collection",
|
||||
"filterPayload": `{ "_id" : 3 }`,
|
||||
"filterParams": []any{},
|
||||
"database": MongoDbDatabase,
|
||||
},
|
||||
"my-tool": map[string]any{
|
||||
"kind": toolKind,
|
||||
"source": "my-instance",
|
||||
"description": "Tool to test invocation with params.",
|
||||
"authRequired": []string{},
|
||||
"collection": "test_collection",
|
||||
"filterPayload": `{ "id" : {{ .id }}, "name" : {{json .name }} }`,
|
||||
"filterParams": []map[string]any{
|
||||
{
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"description": "user id",
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"type": "string",
|
||||
"description": "user name",
|
||||
},
|
||||
},
|
||||
"projectPayload": `{ "_id": 1, "id": 1, "name" : 1 }`,
|
||||
"database": MongoDbDatabase,
|
||||
},
|
||||
"my-tool-by-id": map[string]any{
|
||||
"kind": toolKind,
|
||||
"source": "my-instance",
|
||||
"description": "Tool to test invocation with params.",
|
||||
"authRequired": []string{},
|
||||
"collection": "test_collection",
|
||||
"filterPayload": `{ "id" : {{ .id }} }`,
|
||||
"filterParams": []map[string]any{
|
||||
{
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"description": "user id",
|
||||
},
|
||||
},
|
||||
"projectPayload": `{ "_id": 1, "id": 1, "name" : 1 }`,
|
||||
"database": MongoDbDatabase,
|
||||
},
|
||||
"my-tool-by-name": map[string]any{
|
||||
"kind": toolKind,
|
||||
"source": "my-instance",
|
||||
"description": "Tool to test invocation with params.",
|
||||
"authRequired": []string{},
|
||||
"collection": "test_collection",
|
||||
"filterPayload": `{ "name" : {{ .name }} }`,
|
||||
"filterParams": []map[string]any{
|
||||
{
|
||||
"name": "name",
|
||||
"type": "string",
|
||||
"description": "user name",
|
||||
"required": false,
|
||||
},
|
||||
},
|
||||
"projectPayload": `{ "_id": 1, "id": 1, "name" : 1 }`,
|
||||
"database": MongoDbDatabase,
|
||||
},
|
||||
"my-array-tool": map[string]any{
|
||||
"kind": toolKind,
|
||||
"source": "my-instance",
|
||||
"description": "Tool to test invocation with array.",
|
||||
"authRequired": []string{},
|
||||
"collection": "test_collection",
|
||||
"filterPayload": `{ "name": { "$in": {{json .nameArray}} }, "_id": 5 })`,
|
||||
"filterParams": []map[string]any{
|
||||
{
|
||||
"name": "nameArray",
|
||||
"type": "array",
|
||||
"description": "user names",
|
||||
"items": map[string]any{
|
||||
"name": "username",
|
||||
"type": "string",
|
||||
"description": "string item"},
|
||||
},
|
||||
},
|
||||
"projectPayload": `{ "_id": 1, "id": 1, "name" : 1 }`,
|
||||
"database": MongoDbDatabase,
|
||||
},
|
||||
"my-auth-tool": map[string]any{
|
||||
"kind": toolKind,
|
||||
"source": "my-instance",
|
||||
"description": "Tool to test authenticated parameters.",
|
||||
"authRequired": []string{},
|
||||
"collection": "test_collection",
|
||||
"filterPayload": `{ "email" : {{json .email }} }`,
|
||||
"filterParams": []map[string]any{
|
||||
{
|
||||
"name": "email",
|
||||
"type": "string",
|
||||
"description": "user email",
|
||||
"authServices": []map[string]string{
|
||||
{
|
||||
"name": "my-google-auth",
|
||||
"field": "email",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"projectPayload": `{ "_id": 0, "name" : 1 }`,
|
||||
"database": MongoDbDatabase,
|
||||
},
|
||||
"my-auth-required-tool": map[string]any{
|
||||
"kind": toolKind,
|
||||
"source": "my-instance",
|
||||
"description": "Tool to test auth required invocation.",
|
||||
"authRequired": []string{
|
||||
"my-google-auth",
|
||||
},
|
||||
"collection": "test_collection",
|
||||
"filterPayload": `{ "_id": 3, "id": 3 }`,
|
||||
"filterParams": []any{},
|
||||
"database": MongoDbDatabase,
|
||||
},
|
||||
"my-fail-tool": map[string]any{
|
||||
"kind": toolKind,
|
||||
"source": "my-instance",
|
||||
"description": "Tool to test statement with incorrect syntax.",
|
||||
"authRequired": []string{},
|
||||
"collection": "test_collection",
|
||||
"filterPayload": `{ "id" ; 1 }"}`,
|
||||
"filterParams": []any{},
|
||||
"database": MongoDbDatabase,
|
||||
},
|
||||
"my-delete-one-tool": map[string]any{
|
||||
"kind": "mongodb-delete-one",
|
||||
"source": "my-instance",
|
||||
"description": "Tool to test deleting an entry.",
|
||||
"authRequired": []string{},
|
||||
"collection": "test_collection",
|
||||
"filterPayload": `{ "id" : 100 }"}`,
|
||||
"filterParams": []any{},
|
||||
"database": MongoDbDatabase,
|
||||
},
|
||||
"my-delete-many-tool": map[string]any{
|
||||
"kind": "mongodb-delete-many",
|
||||
"source": "my-instance",
|
||||
"description": "Tool to test deleting multiple entries.",
|
||||
"authRequired": []string{},
|
||||
"collection": "test_collection",
|
||||
"filterPayload": `{ "id" : 101 }"}`,
|
||||
"filterParams": []any{},
|
||||
"database": MongoDbDatabase,
|
||||
},
|
||||
"my-insert-one-tool": map[string]any{
|
||||
"kind": "mongodb-insert-one",
|
||||
"source": "my-instance",
|
||||
"description": "Tool to test inserting an entry.",
|
||||
"authRequired": []string{},
|
||||
"collection": "test_collection",
|
||||
"canonical": true,
|
||||
"database": MongoDbDatabase,
|
||||
},
|
||||
"my-insert-many-tool": map[string]any{
|
||||
"kind": "mongodb-insert-many",
|
||||
"source": "my-instance",
|
||||
"description": "Tool to test inserting multiple entries.",
|
||||
"authRequired": []string{},
|
||||
"collection": "test_collection",
|
||||
"canonical": true,
|
||||
"database": MongoDbDatabase,
|
||||
},
|
||||
"my-update-one-tool": map[string]any{
|
||||
"kind": "mongodb-update-one",
|
||||
"source": "my-instance",
|
||||
"description": "Tool to test updating an entry.",
|
||||
"authRequired": []string{},
|
||||
"collection": "test_collection",
|
||||
"canonical": true,
|
||||
"filterPayload": `{ "id" : 300 }`,
|
||||
"filterParams": []any{},
|
||||
"updatePayload": `{ "$set" : { "name": {{json .name}} } }`,
|
||||
"updateParams": []map[string]any{
|
||||
{
|
||||
"name": "name",
|
||||
"type": "string",
|
||||
"description": "user name",
|
||||
},
|
||||
},
|
||||
"database": MongoDbDatabase,
|
||||
},
|
||||
"my-update-many-tool": map[string]any{
|
||||
"kind": "mongodb-update-many",
|
||||
"source": "my-instance",
|
||||
"description": "Tool to test updating multiple entries.",
|
||||
"authRequired": []string{},
|
||||
"collection": "test_collection",
|
||||
"canonical": true,
|
||||
"filterPayload": `{ "id" : {{ .id }} }`,
|
||||
"filterParams": []map[string]any{
|
||||
{
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"description": "id",
|
||||
},
|
||||
},
|
||||
"updatePayload": `{ "$set" : { "name": {{json .name}} } }`,
|
||||
"updateParams": []map[string]any{
|
||||
{
|
||||
"name": "name",
|
||||
"type": "string",
|
||||
"description": "user name",
|
||||
},
|
||||
},
|
||||
"database": MongoDbDatabase,
|
||||
},
|
||||
"my-aggregate-tool": map[string]any{
|
||||
"kind": "mongodb-aggregate",
|
||||
"source": "my-instance",
|
||||
"description": "Tool to test an aggregation.",
|
||||
"authRequired": []string{},
|
||||
"collection": "test_collection",
|
||||
"canonical": true,
|
||||
"pipelinePayload": `[{ "$match" : { "name": {{json .name}} } }, { "$project" : { "id" : 1, "_id" : 0 }}]`,
|
||||
"pipelineParams": []map[string]any{
|
||||
{
|
||||
"name": "name",
|
||||
"type": "string",
|
||||
"description": "user name",
|
||||
},
|
||||
},
|
||||
"database": MongoDbDatabase,
|
||||
},
|
||||
"my-read-only-aggregate-tool": map[string]any{
|
||||
"kind": "mongodb-aggregate",
|
||||
"source": "my-instance",
|
||||
"description": "Tool to test an aggregation.",
|
||||
"authRequired": []string{},
|
||||
"collection": "test_collection",
|
||||
"canonical": true,
|
||||
"readOnly": true,
|
||||
"pipelinePayload": `[{ "$match" : { "name": {{json .name}} } }, { "$out" : "target_collection" }]`,
|
||||
"pipelineParams": []map[string]any{
|
||||
{
|
||||
"name": "name",
|
||||
"type": "string",
|
||||
"description": "user name",
|
||||
},
|
||||
},
|
||||
"database": MongoDbDatabase,
|
||||
},
|
||||
"my-read-write-aggregate-tool": map[string]any{
|
||||
"kind": "mongodb-aggregate",
|
||||
"source": "my-instance",
|
||||
"description": "Tool to test an aggregation.",
|
||||
"authRequired": []string{},
|
||||
"collection": "test_collection",
|
||||
"canonical": true,
|
||||
"readOnly": false,
|
||||
"pipelinePayload": `[{ "$match" : { "name": {{json .name}} } }, { "$out" : "target_collection" }]`,
|
||||
"pipelineParams": []map[string]any{
|
||||
{
|
||||
"name": "name",
|
||||
"type": "string",
|
||||
"description": "user name",
|
||||
},
|
||||
},
|
||||
"database": MongoDbDatabase,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return toolsFile
|
||||
|
||||
}
|
||||
|
||||
func getMongoDBWants() (string, string, string, string, string, string) {
|
||||
select1Want := `[{"_id":3,"id":3,"name":"Sid"}]`
|
||||
failInvocationWant := `invalid JSON input: missing colon after key `
|
||||
invokeParamWant := `[{"_id":5,"id":3,"name":"Alice"}]`
|
||||
invokeParamWantNull := `[{"_id":4,"id":4,"name":null}]`
|
||||
mcpInvokeParamWant := `{"jsonrpc":"2.0","id":"my-tool","result":{"content":[{"type":"text","text":"{\"_id\":5,\"id\":3,\"name\":\"Alice\"}"}]}}`
|
||||
nullString := "null"
|
||||
return select1Want, failInvocationWant, invokeParamWant, invokeParamWantNull, nullString, mcpInvokeParamWant
|
||||
}
|
||||
Reference in New Issue
Block a user