Skip to content

Commit ae49fb7

Browse files
authored
feat(tools/elasticsearch-execute-esql): add Tool to execute arbitrary ES/QL queries (googleapis#3013)
Add `elasticsearch-execute-esql` tool.
1 parent 862c396 commit ae49fb7

File tree

6 files changed

+343
-11
lines changed

6 files changed

+343
-11
lines changed

cmd/internal/imports.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ import (
9898
_ "github.com/googleapis/mcp-toolbox/internal/tools/dataproc/dataproclistjobs"
9999
_ "github.com/googleapis/mcp-toolbox/internal/tools/dgraph"
100100
_ "github.com/googleapis/mcp-toolbox/internal/tools/elasticsearch/elasticsearchesql"
101+
_ "github.com/googleapis/mcp-toolbox/internal/tools/elasticsearch/elasticsearchexecuteesql"
101102
_ "github.com/googleapis/mcp-toolbox/internal/tools/firebird/firebirdexecutesql"
102103
_ "github.com/googleapis/mcp-toolbox/internal/tools/firebird/firebirdsql"
103104
_ "github.com/googleapis/mcp-toolbox/internal/tools/firestore/firestoreadddocuments"

docs/en/integrations/elasticsearch/tools/elasticsearch-esql.md

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,23 +13,14 @@ Execute ES|QL queries.
1313
This tool allows you to execute ES|QL queries against your Elasticsearch
1414
cluster. You can use this to perform complex searches and aggregations.
1515

16-
See the [official
17-
documentation](https://www.elastic.co/docs/reference/query-languages/esql/esql-getting-started)
16+
See the
17+
[official documentation](https://www.elastic.co/docs/reference/query-languages/esql/esql-getting-started)
1818
for more information.
1919

2020
## Compatible Sources
2121

2222
{{< compatible-sources >}}
2323

24-
## Parameters
25-
26-
| **name** | **type** | **required** | **description** |
27-
|------------|:---------------------------------------:|:------------:|-----------------------------------------------------------------------------------------------------------------------------------------------------|
28-
| query | string | false | The ES\|QL query to run. Can also be passed by parameters. |
29-
| format | string | false | The format of the query. Default is json. Valid values are csv, json, tsv, txt, yaml, cbor, smile, or arrow. |
30-
| timeout | integer | false | The timeout for the query in seconds. Default is 60 (1 minute). |
31-
| parameters | [parameters](../../../documentation/configuration/tools/_index.md#specifying-parameters) | false | List of [parameters](../../../documentation/configuration/tools/_index.md#specifying-parameters) that will be used with the ES\|QL query.<br/>Only supports “string”, “integer”, “float”, “boolean”. |
32-
3324
## Example
3425

3526
```yaml
@@ -48,3 +39,12 @@ parameters:
4839
description: Limit the number of results.
4940
required: true
5041
```
42+
43+
## Reference
44+
45+
| **field** | **type** | **required** | **description** |
46+
| ---------- | :-------------------------------------: | :----------: | --------------------------------------------------------------------------------------------------------------------------------------------------- |
47+
| query | string | false | The ES\|QL query to run. Can also be passed by parameters. |
48+
| format | string | false | The format of the query. Default is json. Valid values are `csv`, `json`, `tsv`, `txt`, `yaml`, `cbor`, `smile`, or `arrow`. |
49+
| timeout | integer | false | The timeout for the query in seconds. Default is 60 (1 minute). |
50+
| parameters | [parameters](../#specifying-parameters) | false | List of [parameters](../#specifying-parameters) that will be used with the ES\|QL query.<br/>Only supports “string”, “integer”, “float”, “boolean”. |
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
---
2+
title: "elasticsearch-execute-esql"
3+
type: docs
4+
weight: 3
5+
description: >
6+
Execute arbitrary ES|QL statements.
7+
---
8+
9+
## About
10+
11+
Execute arbitrary ES|QL statements.
12+
13+
This tool allows you to execute arbitrary ES|QL statements against your
14+
Elasticsearch cluster at runtime. This is useful for ad-hoc queries where the
15+
statement is not known beforehand.
16+
17+
See the
18+
[official documentation](https://www.elastic.co/docs/reference/query-languages/esql/esql-getting-started)
19+
for more information.
20+
21+
## Compatible Sources
22+
23+
{{< compatible-sources >}}
24+
25+
## Parameters
26+
27+
| **name** | **type** | **required** | **description** |
28+
| -------- | :------: | :----------: | -------------------------------- |
29+
| query | string | true | The ES|QL statement to execute. |
30+
31+
## Example
32+
33+
```yaml
34+
kind: tool
35+
name: execute_ad_hoc_esql
36+
type: elasticsearch-execute-esql
37+
source: elasticsearch-source
38+
description: Use this tool to execute arbitrary ES|QL statements.
39+
format: json
40+
```
41+
42+
## Reference
43+
44+
| **field** | **type** | **required** | **description** |
45+
|-------------|:------------------------------------------:|:------------:|--------------------------------------------------------------------------------------------------|
46+
| type | string | true | Must be "elasticsearch-execute-esql". |
47+
| source | string | true | Name of the source the ES|QL should execute on. |
48+
| description | string | true | Description of the tool that is passed to the LLM. |
49+
| format | string | false | The format of the query. Default is json. Valid values are `csv`, `json`, `tsv`, `txt`, `yaml`, `cbor`, `smile`, or `arrow`. |
50+
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
// Copyright 2026 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package elasticsearchexecuteesql
16+
17+
import (
18+
"context"
19+
"fmt"
20+
"net/http"
21+
22+
yaml "github.com/goccy/go-yaml"
23+
"github.com/googleapis/mcp-toolbox/internal/embeddingmodels"
24+
"github.com/googleapis/mcp-toolbox/internal/sources"
25+
es "github.com/googleapis/mcp-toolbox/internal/sources/elasticsearch"
26+
"github.com/googleapis/mcp-toolbox/internal/tools"
27+
"github.com/googleapis/mcp-toolbox/internal/util"
28+
"github.com/googleapis/mcp-toolbox/internal/util/parameters"
29+
)
30+
31+
const resourceType string = "elasticsearch-execute-esql"
32+
33+
func init() {
34+
if !tools.Register(resourceType, newConfig) {
35+
panic(fmt.Sprintf("tool type %q already registered", resourceType))
36+
}
37+
}
38+
39+
func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.ToolConfig, error) {
40+
actual := Config{Name: name}
41+
if err := decoder.DecodeContext(ctx, &actual); err != nil {
42+
return nil, err
43+
}
44+
return actual, nil
45+
}
46+
47+
type compatibleSource interface {
48+
ElasticsearchClient() es.EsClient
49+
RunSQL(ctx context.Context, format, query string, params []map[string]any) (any, error)
50+
}
51+
52+
type Config struct {
53+
Name string `yaml:"name" validate:"required"`
54+
Type string `yaml:"type" validate:"required"`
55+
Source string `yaml:"source" validate:"required"`
56+
Description string `yaml:"description" validate:"required"`
57+
AuthRequired []string `yaml:"authRequired"`
58+
Format string `yaml:"format"`
59+
Annotations *tools.ToolAnnotations `yaml:"annotations,omitempty"`
60+
}
61+
62+
var _ tools.ToolConfig = Config{}
63+
64+
func (cfg Config) ToolConfigType() string {
65+
return resourceType
66+
}
67+
68+
func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) {
69+
queryParameter := parameters.NewStringParameter("query", "The ES|QL statement to execute.")
70+
params := parameters.Parameters{queryParameter}
71+
72+
annotations := tools.GetAnnotationsOrDefault(cfg.Annotations, tools.NewDestructiveAnnotations)
73+
mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, params, annotations)
74+
75+
t := Tool{
76+
Config: cfg,
77+
Parameters: params,
78+
manifest: tools.Manifest{Description: cfg.Description, Parameters: params.Manifest(), AuthRequired: cfg.AuthRequired},
79+
mcpManifest: mcpManifest,
80+
}
81+
return t, nil
82+
}
83+
84+
var _ tools.Tool = Tool{}
85+
86+
type Tool struct {
87+
Config
88+
Parameters parameters.Parameters `yaml:"parameters"`
89+
manifest tools.Manifest
90+
mcpManifest tools.McpManifest
91+
}
92+
93+
func (t Tool) Invoke(ctx context.Context, resourceMgr tools.SourceProvider, params parameters.ParamValues, accessToken tools.AccessToken) (any, util.ToolboxError) {
94+
source, err := tools.GetCompatibleSource[compatibleSource](resourceMgr, t.Source, t.Name, t.Type)
95+
if err != nil {
96+
return nil, util.NewClientServerError("source used is not compatible with the tool", http.StatusInternalServerError, err)
97+
}
98+
99+
paramsMap := params.AsMap()
100+
query, ok := paramsMap["query"].(string)
101+
if !ok {
102+
return nil, util.NewAgentError(fmt.Sprintf("unable to get cast %s", paramsMap["query"]), nil)
103+
}
104+
105+
// Default to json format
106+
format := t.Format
107+
if format == "" {
108+
format = "json"
109+
}
110+
111+
// Get logger
112+
logger, err := util.LoggerFromContext(ctx)
113+
if err != nil {
114+
return nil, util.NewClientServerError("error getting logger", http.StatusInternalServerError, err)
115+
}
116+
logger.DebugContext(ctx, fmt.Sprintf("executing `%s` tool query: %s with format: %s", resourceType, query, format))
117+
118+
resp, err := source.RunSQL(ctx, format, query, nil)
119+
if err != nil {
120+
return nil, util.ProcessGeneralError(err)
121+
}
122+
return resp, nil
123+
}
124+
125+
func (t Tool) EmbedParams(ctx context.Context, paramValues parameters.ParamValues, embeddingModelsMap map[string]embeddingmodels.EmbeddingModel) (parameters.ParamValues, error) {
126+
return parameters.EmbedParams(ctx, t.Parameters, paramValues, embeddingModelsMap, nil)
127+
}
128+
129+
func (t Tool) Manifest() tools.Manifest {
130+
return t.manifest
131+
}
132+
133+
func (t Tool) McpManifest() tools.McpManifest {
134+
return t.mcpManifest
135+
}
136+
137+
func (t Tool) Authorized(verifiedAuthServices []string) bool {
138+
return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices)
139+
}
140+
141+
func (t Tool) RequiresClientAuthorization(resourceMgr tools.SourceProvider) (bool, error) {
142+
return false, nil
143+
}
144+
145+
func (t Tool) ToConfig() tools.ToolConfig {
146+
return t.Config
147+
}
148+
149+
func (t Tool) GetAuthTokenHeaderName(resourceMgr tools.SourceProvider) (string, error) {
150+
return "Authorization", nil
151+
}
152+
153+
func (t Tool) GetParameters() parameters.Parameters {
154+
return t.Parameters
155+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
// Copyright 2026 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package elasticsearchexecuteesql
16+
17+
import (
18+
"testing"
19+
20+
"github.com/google/go-cmp/cmp"
21+
"github.com/googleapis/mcp-toolbox/internal/server"
22+
"github.com/googleapis/mcp-toolbox/internal/testutils"
23+
)
24+
25+
func TestParseFromYamlElasticsearchExecuteEsql(t *testing.T) {
26+
ctx, err := testutils.ContextWithNewLogger()
27+
if err != nil {
28+
t.Fatalf("unexpected error: %s", err)
29+
}
30+
tcs := []struct {
31+
desc string
32+
in string
33+
want server.ToolConfigs
34+
}{
35+
{
36+
desc: "basic execute tool example",
37+
in: `
38+
kind: tool
39+
name: example_tool
40+
type: elasticsearch-execute-esql
41+
source: my-elasticsearch-instance
42+
description: Elasticsearch execute ES|QL tool
43+
`,
44+
want: server.ToolConfigs{
45+
"example_tool": Config{
46+
Name: "example_tool",
47+
Type: "elasticsearch-execute-esql",
48+
Source: "my-elasticsearch-instance",
49+
Description: "Elasticsearch execute ES|QL tool",
50+
AuthRequired: []string{},
51+
},
52+
},
53+
},
54+
{
55+
desc: "execute tool with format",
56+
in: `
57+
kind: tool
58+
name: example_tool_csv
59+
type: elasticsearch-execute-esql
60+
source: my-elasticsearch-instance
61+
description: Elasticsearch execute ES|QL tool in CSV
62+
format: csv
63+
`,
64+
want: server.ToolConfigs{
65+
"example_tool_csv": Config{
66+
Name: "example_tool_csv",
67+
Type: "elasticsearch-execute-esql",
68+
Source: "my-elasticsearch-instance",
69+
Description: "Elasticsearch execute ES|QL tool in CSV",
70+
AuthRequired: []string{},
71+
Format: "csv",
72+
},
73+
},
74+
},
75+
}
76+
77+
for _, tc := range tcs {
78+
t.Run(tc.desc, func(t *testing.T) {
79+
_, _, _, got, _, _, err := server.UnmarshalResourceConfig(ctx, testutils.FormatYaml(tc.in))
80+
if err != nil {
81+
t.Fatalf("unable to unmarshal: %s", err)
82+
}
83+
if diff := cmp.Diff(tc.want, got); diff != "" {
84+
t.Fatalf("incorrect parse: diff %v", diff)
85+
}
86+
})
87+
}
88+
}

0 commit comments

Comments
 (0)