Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ import (
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookergetparameters"
_ "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/lookerqueryurl"
_ "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"
Expand Down Expand Up @@ -218,7 +219,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', 'dataplex', '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', 'dataplex', 'firestore', 'looker', 'mssql', 'mysql', '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.")

Expand Down
2 changes: 1 addition & 1 deletion cmd/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1290,7 +1290,7 @@ func TestPrebuiltTools(t *testing.T) {
wantToolset: server.ToolsetConfigs{
"looker-tools": tools.ToolsetConfig{
Name: "looker-tools",
ToolNames: []string{"get_models", "get_explores", "get_dimensions", "get_measures", "get_filters", "get_parameters", "query", "query_sql", "get_looks", "run_look"},
ToolNames: []string{"get_models", "get_explores", "get_dimensions", "get_measures", "get_filters", "get_parameters", "query", "query_sql", "query_url", "get_looks", "run_look"},
},
},
},
Expand Down
3 changes: 2 additions & 1 deletion docs/en/how-to/connect-ide/looker_mcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ to expose your developer assistant tools to a Looker instance:
v0.10.0+:

<!-- {x-release-please-start-version} -->
{{< tabpane persist=header >}}
{{< tabpane persist=header >}}
{{< tab header="linux/amd64" lang="bash" >}}
curl -O https://storage.googleapis.com/genai-toolbox/v0.10.0/linux/amd64/toolbox
{{< /tab >}}
Expand Down Expand Up @@ -265,6 +265,7 @@ The following tools are available to the LLM:
1. **get_parameters**: list the parameters in a given explore
1. **query**: Run a query
1. **query_sql**: Return the SQL generated by Looker for a query
1. **query_url**: Return a link to the query in Looker for further exploration
1. **get_looks**: Return the saved Looks that match a title or description
1. **run_look**: Run a saved Look and return the data

Expand Down
53 changes: 53 additions & 0 deletions docs/en/resources/tools/looker/looker_query_url.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
---
title: "looker-query-url"
type: docs
weight: 1
description: >
"looker-query-url" generates a url link to a Looker explore.
aliases:
- /resources/tools/looker-query-url
---

## About

The `looker-query-url` generates a url link to an explore in
Looker so the query can be investigated further.

It's compatible with the following sources:

- [looker](../../sources/looker.md)

`looker-query-url` takes eight parameters:

1. the `model`
2. the `explore`
3. the `fields` list
4. an optional set of `filters`
5. an optional set of `pivots`
6. an optional set of `sorts`
7. an optional `limit`
8. an optional `tz`

## Example

```yaml
tools:
query_url:
kind: looker-query-url
source: looker-source
description: |
Query URL Tool

This tool is used to generate the URL of a query in Looker.
The user can then explore the query further inside Looker.
The tool also returns the query_id and slug. The parameters
are the same as the `looker-query` tool.
```

## Reference

| **field** | **type** | **required** | **description** |
|-------------|:------------------------------------------:|:------------:|--------------------------------------------------------------------------------------------------|
| kind | string | true | Must be "looker-query-url" |
| source | string | true | Name of the source the SQL should execute on. |
| description | string | true | Description of the tool that is passed to the LLM. |
7 changes: 5 additions & 2 deletions docs/en/samples/looker/looker_gemini.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ In this section, we will download Toolbox and run the Toolbox server.

1. Edit the file `~/.gemini/settings.json` and add the following
to the list of mcpServers. Use the Client Id and Client Secret
you obtained earlier.
you obtained earlier. The name of the server - here
`looker-toolbox` - can be anything meaningful to you.

```json
"mcpServers": {
Expand Down Expand Up @@ -99,11 +100,13 @@ In this section, we will download Toolbox and run the Toolbox server.
- looker-toolbox__query_sql
- looker-toolbox__get_dimensions
- looker-toolbox__run_look
- looker-toolbox__query_url
```

1. Start exploring your Looker instance with commands like
`Find an explore to see orders` or `show me my current
inventory broken down by item category`.

1. Gemini will prompt you for your approval before using
a tool. You can approve all the tools.
a tool. You can approve all the tools at once or
one at a time.
15 changes: 15 additions & 0 deletions internal/prebuiltconfigs/tools/looker.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,20 @@ tools:

The result of the query sql tool is SQL text.

query_url:
kind: looker-query-url
source: looker-source
description: |
Query URL Tool

This tool is used to generate the URL of a query in Looker.
The user can then explore the query further inside Looker.
The tool also returns the query_id and slug. The parameters
are the same as the query tool.

The result is a JSON object with the id, slug, the url, and
the long_url.

get_looks:
kind: looker-get-looks
source: looker-source
Expand Down Expand Up @@ -154,5 +168,6 @@ toolsets:
- get_parameters
- query
- query_sql
- query_url
- get_looks
- run_look
243 changes: 243 additions & 0 deletions internal/tools/looker/lookerqueryurl/lookerqueryurl.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
// 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 lookerqueryurl

import (
"context"
"fmt"

yaml "github.com/goccy/go-yaml"
"github.com/googleapis/genai-toolbox/internal/sources"
lookersrc "github.com/googleapis/genai-toolbox/internal/sources/looker"
"github.com/googleapis/genai-toolbox/internal/tools"
"github.com/googleapis/genai-toolbox/internal/util"

"github.com/looker-open-source/sdk-codegen/go/rtl"
v4 "github.com/looker-open-source/sdk-codegen/go/sdk/v4"

"github.com/thlib/go-timezone-local/tzlocal"
)

const kind string = "looker-query-url"

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"`
Description string `yaml:"description" validate:"required"`
AuthRequired []string `yaml:"authRequired"`
}

// 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.(*lookersrc.Source)
if !ok {
return nil, fmt.Errorf("invalid source for %q tool: source kind must be `looker`", kind)
}

modelParameter := tools.NewStringParameter("model", "The model containing the explore.")
exploreParameter := tools.NewStringParameter("explore", "The explore to be queried.")
fieldsParameter := tools.NewArrayParameter("fields",
"The fields to be retrieved.",
tools.NewStringParameter("field", "A field to be returned in the query"),
)
filtersParameter := tools.NewMapParameterWithDefault("filters",
map[string]any{},
"The filters for the query",
"",
)
pivotsParameter := tools.NewArrayParameterWithDefault("pivots",
[]any{},
"The query pivots (must be included in fields as well).",
tools.NewStringParameter("pivot_field", "A field to be used as a pivot in the query"),
)
sortsParameter := tools.NewArrayParameterWithDefault("sorts",
[]any{},
"The sorts like \"field.id desc 0\".",
tools.NewStringParameter("sort_field", "A field to be used as a sort in the query"),
)
limitParameter := tools.NewIntParameterWithDefault("limit", 500, "The row limit.")
tzParameter := tools.NewStringParameterWithRequired("tz", "The query timezone.", false)

parameters := tools.Parameters{
modelParameter,
exploreParameter,
fieldsParameter,
filtersParameter,
pivotsParameter,
sortsParameter,
limitParameter,
tzParameter,
}

mcpManifest := tools.McpManifest{
Name: cfg.Name,
Description: cfg.Description,
InputSchema: parameters.McpManifest(),
}

// finish tool setup
return Tool{
Name: cfg.Name,
Kind: kind,
Parameters: parameters,
AuthRequired: cfg.AuthRequired,
Client: s.Client,
ApiSettings: s.ApiSettings,
manifest: tools.Manifest{
Description: cfg.Description,
Parameters: parameters.Manifest(),
AuthRequired: cfg.AuthRequired,
},
mcpManifest: mcpManifest,
}, nil
}

// validate interface
var _ tools.Tool = Tool{}

type Tool struct {
Name string `yaml:"name"`
Kind string `yaml:"kind"`
Client *v4.LookerSDK
ApiSettings *rtl.ApiSettings
AuthRequired []string `yaml:"authRequired"`
Parameters tools.Parameters `yaml:"parameters"`
manifest tools.Manifest
mcpManifest tools.McpManifest
}

func (t Tool) Invoke(ctx context.Context, params tools.ParamValues) (any, error) {
logger, err := util.LoggerFromContext(ctx)
if err != nil {
return nil, fmt.Errorf("unable to get logger from ctx: %s", err)
}
logger.DebugContext(ctx, "params = ", params)
paramsMap := params.AsMap()

f, err := tools.ConvertAnySliceToTyped(paramsMap["fields"].([]any), "string")
if err != nil {
return nil, fmt.Errorf("can't convert fields to array of strings: %s", err)
}
fields := f.([]string)
filters := paramsMap["filters"].(map[string]any)
// Sometimes filters come as "'field.id'": "expression" so strip extra ''
for k, v := range filters {
if len(k) > 0 && k[0] == '\'' && k[len(k)-1] == '\'' {
delete(filters, k)
filters[k[1:len(k)-1]] = v
}
}
p, err := tools.ConvertAnySliceToTyped(paramsMap["pivots"].([]any), "string")
if err != nil {
return nil, fmt.Errorf("can't convert pivots to array of strings: %s", err)
}
pivots := p.([]string)
s, err := tools.ConvertAnySliceToTyped(paramsMap["sorts"].([]any), "string")
if err != nil {
return nil, fmt.Errorf("can't convert sorts to array of strings: %s", err)
}
sorts := s.([]string)
limit := fmt.Sprintf("%d", paramsMap["limit"].(int))

var tz string
if paramsMap["tz"] != nil {
tz = paramsMap["tz"].(string)
} else {
tzname, err := tzlocal.RuntimeTZ()
if err != nil {
logger.ErrorContext(ctx, fmt.Sprintf("Error getting local timezone: %s", err))
tzname = "Etc/UTC"
}
tz = tzname
}

wq := v4.WriteQuery{
Model: paramsMap["model"].(string),
View: paramsMap["explore"].(string),
Fields: &fields,
Pivots: &pivots,
Filters: &filters,
Sorts: &sorts,
Limit: &limit,
QueryTimezone: &tz,
}

respFields := "id,slug,share_url,expanded_share_url"
resp, err := t.Client.CreateQuery(wq, respFields, t.ApiSettings)
if err != nil {
return nil, fmt.Errorf("error making query request: %s", err)
}
logger.DebugContext(ctx, "resp = ", resp)

data := make(map[string]any)
if resp.Id != nil {
data["id"] = *resp.Id
}
if resp.Slug != nil {
data["slug"] = *resp.Slug
}
if resp.ShareUrl != nil {
data["url"] = *resp.ShareUrl
}
if resp.ExpandedShareUrl != nil {
data["long_url"] = *resp.ExpandedShareUrl
}
logger.DebugContext(ctx, "data = %v", data)

return data, nil
}

func (t Tool) ParseParams(data map[string]any, claims map[string]map[string]any) (tools.ParamValues, error) {
return tools.ParseParams(t.Parameters, data, claims)
}

func (t Tool) Manifest() tools.Manifest {
return t.manifest
}

func (t Tool) McpManifest() tools.McpManifest {
return t.mcpManifest
}

func (t Tool) Authorized(verifiedAuthServices []string) bool {
return true
}
Loading
Loading