mirror of
https://github.com/googleapis/genai-toolbox.git
synced 2026-01-11 08:28:11 -05:00
Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
170b54b5b9 | ||
|
|
2cad82e510 | ||
|
|
3ae2526e0f | ||
|
|
8755e3db34 | ||
|
|
ade9a2515b | ||
|
|
d2978d5395 | ||
|
|
14c224939a | ||
|
|
7430f318a2 | ||
|
|
21085ef422 | ||
|
|
d16f89fbb6 | ||
|
|
4da2fcc055 | ||
|
|
27381ed9c0 | ||
|
|
670da6e451 | ||
|
|
2f1ed3aaf5 | ||
|
|
8d20a48f13 | ||
|
|
89af3a4ca3 | ||
|
|
6029e129bc | ||
|
|
8628f7190b | ||
|
|
f544e676ea | ||
|
|
a21e68022b | ||
|
|
08397cf398 | ||
|
|
75a04a55dd | ||
|
|
a5e74af104 | ||
|
|
58d7b3e5f8 | ||
|
|
ca353e0b66 | ||
|
|
fc707fb561 | ||
|
|
03aa9fabac |
@@ -152,25 +152,25 @@ steps:
|
||||
bigquery \
|
||||
bigquery
|
||||
|
||||
# - id: "dataplex"
|
||||
# name: golang:1
|
||||
# waitFor: ["compile-test-binary"]
|
||||
# entrypoint: /bin/bash
|
||||
# env:
|
||||
# - "GOPATH=/gopath"
|
||||
# - "DATAPLEX_PROJECT=$PROJECT_ID"
|
||||
# - "SERVICE_ACCOUNT_EMAIL=$SERVICE_ACCOUNT_EMAIL"
|
||||
# secretEnv: ["CLIENT_ID"]
|
||||
# volumes:
|
||||
# - name: "go"
|
||||
# path: "/gopath"
|
||||
# args:
|
||||
# - -c
|
||||
# - |
|
||||
# .ci/test_with_coverage.sh \
|
||||
# "Dataplex" \
|
||||
# dataplex \
|
||||
# dataplex
|
||||
- id: "dataplex"
|
||||
name: golang:1
|
||||
waitFor: ["compile-test-binary"]
|
||||
entrypoint: /bin/bash
|
||||
env:
|
||||
- "GOPATH=/gopath"
|
||||
- "DATAPLEX_PROJECT=$PROJECT_ID"
|
||||
- "SERVICE_ACCOUNT_EMAIL=$SERVICE_ACCOUNT_EMAIL"
|
||||
secretEnv: ["CLIENT_ID"]
|
||||
volumes:
|
||||
- name: "go"
|
||||
path: "/gopath"
|
||||
args:
|
||||
- -c
|
||||
- |
|
||||
.ci/test_with_coverage.sh \
|
||||
"Dataplex" \
|
||||
dataplex \
|
||||
dataplex
|
||||
|
||||
- id: "postgres"
|
||||
name: golang:1
|
||||
@@ -464,7 +464,7 @@ steps:
|
||||
"OceanBase" \
|
||||
oceanbase \
|
||||
oceanbase
|
||||
|
||||
|
||||
- id: "firestore"
|
||||
name: golang:1
|
||||
waitFor: ["compile-test-binary"]
|
||||
@@ -575,6 +575,28 @@ steps:
|
||||
firebird \
|
||||
firebirdsql firebirdexecutesql
|
||||
|
||||
- id: "clickhouse"
|
||||
name : golang:1
|
||||
waitFor: ["compile-test-binary"]
|
||||
entrypoint: /bin/bash
|
||||
env:
|
||||
- "GOPATH=/gopath"
|
||||
- "CLICKHOUSE_DATABASE=$_CLICKHOUSE_DATABASE"
|
||||
- "CLICKHOUSE_PORT=$_CLICKHOUSE_PORT"
|
||||
- "CLICKHOUSE_PROTOCOL=$_CLICKHOUSE_PROTOCOL"
|
||||
- "SERVICE_ACCOUNT_EMAIL=$SERVICE_ACCOUNT_EMAIL"
|
||||
secretEnv: ["CLICKHOUSE_HOST", "CLICKHOUSE_USER", "CLIENT_ID"]
|
||||
volumes:
|
||||
- name: "go"
|
||||
path: "/gopath"
|
||||
args:
|
||||
- -c
|
||||
- |
|
||||
.ci/test_with_coverage.sh \
|
||||
"ClickHouse" \
|
||||
clickhouse \
|
||||
clickhouse
|
||||
|
||||
- id: "trino"
|
||||
name: golang:1
|
||||
waitFor: ["compile-test-binary"]
|
||||
@@ -660,6 +682,10 @@ availableSecrets:
|
||||
env: TIDB_USER
|
||||
- versionName: projects/$PROJECT_ID/secrets/tidb_pass/versions/latest
|
||||
env: TIDB_PASS
|
||||
- versionName: projects/$PROJECT_ID/secrets/clickhouse_host/versions/latest
|
||||
env: CLICKHOUSE_HOST
|
||||
- versionName: projects/$PROJECT_ID/secrets/clickhouse_user/versions/latest
|
||||
env: CLICKHOUSE_USER
|
||||
- versionName: projects/$PROJECT_ID/secrets/firebird_user/versions/latest
|
||||
env: FIREBIRD_USER
|
||||
- versionName: projects/$PROJECT_ID/secrets/firebird_pass/versions/latest
|
||||
@@ -707,6 +733,9 @@ substitutions:
|
||||
_LOOKER_VERIFY_SSL: "true"
|
||||
_TIDB_HOST: 127.0.0.1
|
||||
_TIDB_PORT: "4000"
|
||||
_CLICKHOUSE_DATABASE: "default"
|
||||
_CLICKHOUSE_PORT: "8123"
|
||||
_CLICKHOUSE_PROTOCOL: "http"
|
||||
_FIREBIRD_HOST: 127.0.0.1
|
||||
_FIREBIRD_PORT: "3050"
|
||||
_TRINO_HOST: 127.0.0.1
|
||||
|
||||
@@ -20,3 +20,5 @@ sourceFileExtensions:
|
||||
- 'go'
|
||||
- 'yaml'
|
||||
- 'yml'
|
||||
ignoreFiles:
|
||||
- 'docs/en/getting-started/quickstart/**'
|
||||
19
.github/workflows/docs_preview_deploy.yaml
vendored
19
.github/workflows/docs_preview_deploy.yaml
vendored
@@ -49,6 +49,23 @@ jobs:
|
||||
group: "preview-${{ github.event.number }}"
|
||||
cancel-in-progress: true
|
||||
steps:
|
||||
- name: Remove PR label
|
||||
if: "${{ github.event.action == 'labeled' && github.event.label.name == 'docs: deploy-preview' }}"
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
try {
|
||||
await github.rest.issues.removeLabel({
|
||||
name: 'docs: deploy-preview',
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.payload.pull_request.number
|
||||
});
|
||||
} catch (e) {
|
||||
console.log('Failed to remove label. Another job may have already removed it!');
|
||||
}
|
||||
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
with:
|
||||
# Checkout the PR's HEAD commit (supports forks).
|
||||
@@ -98,4 +115,4 @@ jobs:
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: "🔎 Preview at https://${{ github.repository_owner }}.github.io/${{ github.event.repository.name }}/previews/PR-${{ github.event.number }}/"
|
||||
})
|
||||
})
|
||||
@@ -52,6 +52,8 @@ import (
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/bigquery/bigquerylisttableids"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/bigquery/bigquerysql"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/bigtable"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/clickhouse/clickhouseexecutesql"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/clickhouse/clickhousesql"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/couchbase"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/dataplex/dataplexlookupentry"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/dataplex/dataplexsearchaspecttypes"
|
||||
@@ -120,6 +122,7 @@ import (
|
||||
_ "github.com/googleapis/genai-toolbox/internal/sources/alloydbpg"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/sources/bigquery"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/sources/bigtable"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/sources/clickhouse"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/sources/cloudsqlmssql"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/sources/cloudsqlmysql"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/sources/cloudsqlpg"
|
||||
@@ -267,7 +270,7 @@ type ToolsFile struct {
|
||||
// parseEnv replaces environment variables ${ENV_NAME} with their values.
|
||||
// also support ${ENV_NAME:default_value}.
|
||||
func parseEnv(input string) (string, error) {
|
||||
re := regexp.MustCompile(`\$\{(\w+)(:(\w+))?\}`)
|
||||
re := regexp.MustCompile(`\$\{(\w+)(:(\w*))?\}`)
|
||||
|
||||
var err error
|
||||
output := re.ReplaceAllStringFunc(input, func(match string) string {
|
||||
@@ -278,7 +281,7 @@ func parseEnv(input string) (string, error) {
|
||||
if value, found := os.LookupEnv(variableName); found {
|
||||
return value
|
||||
}
|
||||
if len(parts) == 4 {
|
||||
if parts[2] != "" {
|
||||
return parts[3]
|
||||
}
|
||||
err = fmt.Errorf("environment variable not found: %q", variableName)
|
||||
@@ -834,7 +837,7 @@ func run(cmd *Command) error {
|
||||
}
|
||||
cmd.logger.InfoContext(ctx, "Server ready to serve!")
|
||||
if cmd.cfg.UI {
|
||||
cmd.logger.InfoContext(ctx, fmt.Sprintf("Toolbox UI is up and running at: http://localhost:%d/ui", cmd.cfg.Port))
|
||||
cmd.logger.InfoContext(ctx, fmt.Sprintf("Toolbox UI is up and running at: http://%s:%d/ui", cmd.cfg.Address, cmd.cfg.Port))
|
||||
}
|
||||
|
||||
go func() {
|
||||
|
||||
@@ -206,6 +206,72 @@ func TestServerConfigFlags(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseEnv(t *testing.T) {
|
||||
tcs := []struct {
|
||||
desc string
|
||||
env map[string]string
|
||||
in string
|
||||
want string
|
||||
err bool
|
||||
errString string
|
||||
}{
|
||||
{
|
||||
desc: "without default without env",
|
||||
in: "${FOO}",
|
||||
want: "",
|
||||
err: true,
|
||||
errString: `environment variable not found: "FOO"`,
|
||||
},
|
||||
{
|
||||
desc: "without default with env",
|
||||
env: map[string]string{
|
||||
"FOO": "bar",
|
||||
},
|
||||
in: "${FOO}",
|
||||
want: "bar",
|
||||
},
|
||||
{
|
||||
desc: "with empty default",
|
||||
in: "${FOO:}",
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
desc: "with default",
|
||||
in: "${FOO:bar}",
|
||||
want: "bar",
|
||||
},
|
||||
{
|
||||
desc: "with default with env",
|
||||
env: map[string]string{
|
||||
"FOO": "hello",
|
||||
},
|
||||
in: "${FOO:bar}",
|
||||
want: "hello",
|
||||
},
|
||||
}
|
||||
for _, tc := range tcs {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
if tc.env != nil {
|
||||
for k, v := range tc.env {
|
||||
t.Setenv(k, v)
|
||||
}
|
||||
}
|
||||
got, err := parseEnv(tc.in)
|
||||
if tc.err {
|
||||
if err == nil {
|
||||
t.Fatalf("expected error not found")
|
||||
}
|
||||
if tc.errString != err.Error() {
|
||||
t.Fatalf("incorrect error string: got %s, want %s", err, tc.errString)
|
||||
}
|
||||
}
|
||||
if tc.want != got {
|
||||
t.Fatalf("unexpected want: got %s, want %s", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestToolFileFlag(t *testing.T) {
|
||||
tcs := []struct {
|
||||
desc string
|
||||
@@ -1166,6 +1232,7 @@ func TestPrebuiltTools(t *testing.T) {
|
||||
alloydb_admin_config, _ := prebuiltconfigs.Get("alloydb-postgres-admin")
|
||||
alloydb_config, _ := prebuiltconfigs.Get("alloydb-postgres")
|
||||
bigquery_config, _ := prebuiltconfigs.Get("bigquery")
|
||||
clickhouse_config, _ := prebuiltconfigs.Get("clickhouse")
|
||||
cloudsqlpg_config, _ := prebuiltconfigs.Get("cloud-sql-postgres")
|
||||
cloudsqlmysql_config, _ := prebuiltconfigs.Get("cloud-sql-mysql")
|
||||
cloudsqlmssql_config, _ := prebuiltconfigs.Get("cloud-sql-mssql")
|
||||
@@ -1198,6 +1265,13 @@ func TestPrebuiltTools(t *testing.T) {
|
||||
t.Setenv("ALLOYDB_POSTGRES_USER", "your_alloydb_user")
|
||||
t.Setenv("ALLOYDB_POSTGRES_PASSWORD", "your_alloydb_password")
|
||||
|
||||
t.Setenv("CLICKHOUSE_PROTOCOL", "your_clickhouse_protocol")
|
||||
t.Setenv("CLICKHOUSE_DATABASE", "your_clickhouse_database")
|
||||
t.Setenv("CLICKHOUSE_PASSWORD", "your_clickhouse_password")
|
||||
t.Setenv("CLICKHOUSE_USER", "your_clickhouse_user")
|
||||
t.Setenv("CLICKHOUSE_HOST", "your_clickhosue_host")
|
||||
t.Setenv("CLICKHOUSE_PORT", "8123")
|
||||
|
||||
t.Setenv("CLOUD_SQL_POSTGRES_PROJECT", "your_pg_project")
|
||||
t.Setenv("CLOUD_SQL_POSTGRES_INSTANCE", "your_pg_instance")
|
||||
t.Setenv("CLOUD_SQL_POSTGRES_DATABASE", "your_pg_db")
|
||||
@@ -1283,6 +1357,16 @@ func TestPrebuiltTools(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "clickhouse prebuilt tools",
|
||||
in: clickhouse_config,
|
||||
wantToolset: server.ToolsetConfigs{
|
||||
"clickhouse-database-tools": tools.ToolsetConfig{
|
||||
Name: "clickhouse-database-tools",
|
||||
ToolNames: []string{"execute_sql"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "cloudsqlpg prebuilt tools",
|
||||
in: cloudsqlpg_config,
|
||||
|
||||
@@ -94,305 +94,23 @@ pip install google-genai
|
||||
code to create an agent:
|
||||
{{< tabpane persist=header >}}
|
||||
{{< tab header="ADK" lang="python" >}}
|
||||
from google.adk.agents import Agent
|
||||
from google.adk.runners import Runner
|
||||
from google.adk.sessions import InMemorySessionService
|
||||
from google.adk.artifacts.in_memory_artifact_service import InMemoryArtifactService
|
||||
from google.genai import types
|
||||
from toolbox_core import ToolboxSyncClient
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
{{< include "quickstart/python/adk/quickstart.py" >}}
|
||||
|
||||
# TODO(developer): replace this with your Google API key
|
||||
|
||||
os.environ['GOOGLE_API_KEY'] = 'your-api-key'
|
||||
|
||||
async def main():
|
||||
with ToolboxSyncClient("<http://127.0.0.1:5000>") as toolbox_client:
|
||||
|
||||
prompt = """
|
||||
You're a helpful hotel assistant. You handle hotel searching, booking and
|
||||
cancellations. When the user searches for a hotel, mention it's name, id,
|
||||
location and price tier. Always mention hotel ids while performing any
|
||||
searches. This is very important for any operations. For any bookings or
|
||||
cancellations, please provide the appropriate confirmation. Be sure to
|
||||
update checkin or checkout dates if mentioned by the user.
|
||||
Don't ask for confirmations from the user.
|
||||
"""
|
||||
|
||||
root_agent = Agent(
|
||||
model='gemini-2.0-flash-001',
|
||||
name='hotel_agent',
|
||||
description='A helpful AI assistant.',
|
||||
instruction=prompt,
|
||||
tools=toolbox_client.load_toolset("my-toolset"),
|
||||
)
|
||||
|
||||
session_service = InMemorySessionService()
|
||||
artifacts_service = InMemoryArtifactService()
|
||||
session = await session_service.create_session(
|
||||
state={}, app_name='hotel_agent', user_id='123'
|
||||
)
|
||||
runner = Runner(
|
||||
app_name='hotel_agent',
|
||||
agent=root_agent,
|
||||
artifact_service=artifacts_service,
|
||||
session_service=session_service,
|
||||
)
|
||||
|
||||
queries = [
|
||||
"Find hotels in Basel with Basel in its name.",
|
||||
"Can you book the Hilton Basel for me?",
|
||||
"Oh wait, this is too expensive. Please cancel it and book the Hyatt Regency instead.",
|
||||
"My check in dates would be from April 10, 2024 to April 19, 2024.",
|
||||
]
|
||||
|
||||
for query in queries:
|
||||
content = types.Content(role='user', parts=[types.Part(text=query)])
|
||||
events = runner.run(session_id=session.id,
|
||||
user_id='123', new_message=content)
|
||||
|
||||
responses = (
|
||||
part.text
|
||||
for event in events
|
||||
for part in event.content.parts
|
||||
if part.text is not None
|
||||
)
|
||||
|
||||
for text in responses:
|
||||
print(text)
|
||||
|
||||
asyncio.run(main())
|
||||
{{< /tab >}}
|
||||
{{< tab header="LangChain" lang="python" >}}
|
||||
import asyncio
|
||||
|
||||
from langgraph.prebuilt import create_react_agent
|
||||
{{< include "quickstart/python/langchain/quickstart.py" >}}
|
||||
|
||||
# TODO(developer): replace this with another import if needed
|
||||
|
||||
from langchain_google_vertexai import ChatVertexAI
|
||||
|
||||
# from langchain_google_genai import ChatGoogleGenerativeAI
|
||||
|
||||
# from langchain_anthropic import ChatAnthropic
|
||||
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
|
||||
from toolbox_langchain import ToolboxClient
|
||||
|
||||
prompt = """
|
||||
You're a helpful hotel assistant. You handle hotel searching, booking and
|
||||
cancellations. When the user searches for a hotel, mention it's name, id,
|
||||
location and price tier. Always mention hotel ids while performing any
|
||||
searches. This is very important for any operations. For any bookings or
|
||||
cancellations, please provide the appropriate confirmation. Be sure to
|
||||
update checkin or checkout dates if mentioned by the user.
|
||||
Don't ask for confirmations from the user.
|
||||
"""
|
||||
|
||||
queries = [
|
||||
"Find hotels in Basel with Basel in its name.",
|
||||
"Can you book the Hilton Basel for me?",
|
||||
"Oh wait, this is too expensive. Please cancel it and book the Hyatt Regency instead.",
|
||||
"My check in dates would be from April 10, 2024 to April 19, 2024.",
|
||||
]
|
||||
|
||||
async def run_application():
|
||||
# TODO(developer): replace this with another model if needed
|
||||
model = ChatVertexAI(model_name="gemini-2.0-flash-001")
|
||||
# model = ChatGoogleGenerativeAI(model="gemini-2.0-flash-001")
|
||||
# model = ChatAnthropic(model="claude-3-5-sonnet-20240620")
|
||||
|
||||
# Load the tools from the Toolbox server
|
||||
async with ToolboxClient("http://127.0.0.1:5000") as client:
|
||||
tools = await client.aload_toolset()
|
||||
|
||||
agent = create_react_agent(model, tools, checkpointer=MemorySaver())
|
||||
|
||||
config = {"configurable": {"thread_id": "thread-1"}}
|
||||
for query in queries:
|
||||
inputs = {"messages": [("user", prompt + query)]}
|
||||
response = agent.invoke(inputs, stream_mode="values", config=config)
|
||||
print(response["messages"][-1].content)
|
||||
|
||||
asyncio.run(run_application())
|
||||
{{< /tab >}}
|
||||
{{< tab header="LlamaIndex" lang="python" >}}
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
from llama_index.core.agent.workflow import AgentWorkflow
|
||||
{{< include "quickstart/python/llamaindex/quickstart.py" >}}
|
||||
|
||||
from llama_index.core.workflow import Context
|
||||
|
||||
# TODO(developer): replace this with another import if needed
|
||||
|
||||
from llama_index.llms.google_genai import GoogleGenAI
|
||||
|
||||
# from llama_index.llms.anthropic import Anthropic
|
||||
|
||||
from toolbox_llamaindex import ToolboxClient
|
||||
|
||||
prompt = """
|
||||
You're a helpful hotel assistant. You handle hotel searching, booking and
|
||||
cancellations. When the user searches for a hotel, mention it's name, id,
|
||||
location and price tier. Always mention hotel ids while performing any
|
||||
searches. This is very important for any operations. For any bookings or
|
||||
cancellations, please provide the appropriate confirmation. Be sure to
|
||||
update checkin or checkout dates if mentioned by the user.
|
||||
Don't ask for confirmations from the user.
|
||||
"""
|
||||
|
||||
queries = [
|
||||
"Find hotels in Basel with Basel in its name.",
|
||||
"Can you book the Hilton Basel for me?",
|
||||
"Oh wait, this is too expensive. Please cancel it and book the Hyatt Regency instead.",
|
||||
"My check in dates would be from April 10, 2024 to April 19, 2024.",
|
||||
]
|
||||
|
||||
async def run_application():
|
||||
# TODO(developer): replace this with another model if needed
|
||||
llm = GoogleGenAI(
|
||||
model="gemini-2.0-flash-001",
|
||||
vertexai_config={"project": "project-id", "location": "us-central1"},
|
||||
)
|
||||
# llm = GoogleGenAI(
|
||||
# api_key=os.getenv("GOOGLE_API_KEY"),
|
||||
# model="gemini-2.0-flash-001",
|
||||
# )
|
||||
# llm = Anthropic(
|
||||
# model="claude-3-7-sonnet-latest",
|
||||
# api_key=os.getenv("ANTHROPIC_API_KEY")
|
||||
# )
|
||||
|
||||
# Load the tools from the Toolbox server
|
||||
async with ToolboxClient("http://127.0.0.1:5000") as client:
|
||||
tools = await client.aload_toolset()
|
||||
|
||||
agent = AgentWorkflow.from_tools_or_functions(
|
||||
tools,
|
||||
llm=llm,
|
||||
system_prompt=prompt,
|
||||
)
|
||||
ctx = Context(agent)
|
||||
for query in queries:
|
||||
response = await agent.run(user_msg=query, ctx=ctx)
|
||||
print(f"---- {query} ----")
|
||||
print(str(response))
|
||||
|
||||
asyncio.run(run_application())
|
||||
{{< /tab >}}
|
||||
{{< tab header="Core" lang="python" >}}
|
||||
import asyncio
|
||||
|
||||
from google import genai
|
||||
from google.genai.types import (
|
||||
Content,
|
||||
FunctionDeclaration,
|
||||
GenerateContentConfig,
|
||||
Part,
|
||||
Tool,
|
||||
)
|
||||
|
||||
from toolbox_core import ToolboxClient
|
||||
|
||||
prompt = """
|
||||
You're a helpful hotel assistant. You handle hotel searching, booking and
|
||||
cancellations. When the user searches for a hotel, mention it's name, id,
|
||||
location and price tier. Always mention hotel id while performing any
|
||||
searches. This is very important for any operations. For any bookings or
|
||||
cancellations, please provide the appropriate confirmation. Be sure to
|
||||
update checkin or checkout dates if mentioned by the user.
|
||||
Don't ask for confirmations from the user.
|
||||
"""
|
||||
|
||||
queries = [
|
||||
"Find hotels in Basel with Basel in its name.",
|
||||
"Please book the hotel Hilton Basel for me.",
|
||||
"This is too expensive. Please cancel it.",
|
||||
"Please book Hyatt Regency for me",
|
||||
"My check in dates for my booking would be from April 10, 2024 to April 19, 2024.",
|
||||
]
|
||||
|
||||
async def run_application():
|
||||
async with ToolboxClient("<http://127.0.0.1:5000>") as toolbox_client:
|
||||
|
||||
# The toolbox_tools list contains Python callables (functions/methods) designed for LLM tool-use
|
||||
# integration. While this example uses Google's genai client, these callables can be adapted for
|
||||
# various function-calling or agent frameworks. For easier integration with supported frameworks
|
||||
# (https://github.com/googleapis/mcp-toolbox-python-sdk/tree/main/packages), use the
|
||||
# provided wrapper packages, which handle framework-specific boilerplate.
|
||||
toolbox_tools = await toolbox_client.load_toolset("my-toolset")
|
||||
genai_client = genai.Client(
|
||||
vertexai=True, project="project-id", location="us-central1"
|
||||
)
|
||||
|
||||
genai_tools = [
|
||||
Tool(
|
||||
function_declarations=[
|
||||
FunctionDeclaration.from_callable_with_api_option(callable=tool)
|
||||
]
|
||||
)
|
||||
for tool in toolbox_tools
|
||||
]
|
||||
history = []
|
||||
for query in queries:
|
||||
user_prompt_content = Content(
|
||||
role="user",
|
||||
parts=[Part.from_text(text=query)],
|
||||
)
|
||||
history.append(user_prompt_content)
|
||||
|
||||
response = genai_client.models.generate_content(
|
||||
model="gemini-2.0-flash-001",
|
||||
contents=history,
|
||||
config=GenerateContentConfig(
|
||||
system_instruction=prompt,
|
||||
tools=genai_tools,
|
||||
),
|
||||
)
|
||||
history.append(response.candidates[0].content)
|
||||
function_response_parts = []
|
||||
for function_call in response.function_calls:
|
||||
fn_name = function_call.name
|
||||
# The tools are sorted alphabetically
|
||||
if fn_name == "search-hotels-by-name":
|
||||
function_result = await toolbox_tools[3](**function_call.args)
|
||||
elif fn_name == "search-hotels-by-location":
|
||||
function_result = await toolbox_tools[2](**function_call.args)
|
||||
elif fn_name == "book-hotel":
|
||||
function_result = await toolbox_tools[0](**function_call.args)
|
||||
elif fn_name == "update-hotel":
|
||||
function_result = await toolbox_tools[4](**function_call.args)
|
||||
elif fn_name == "cancel-hotel":
|
||||
function_result = await toolbox_tools[1](**function_call.args)
|
||||
else:
|
||||
raise ValueError("Function name not present.")
|
||||
function_response = {"result": function_result}
|
||||
function_response_part = Part.from_function_response(
|
||||
name=function_call.name,
|
||||
response=function_response,
|
||||
)
|
||||
function_response_parts.append(function_response_part)
|
||||
|
||||
if function_response_parts:
|
||||
tool_response_content = Content(role="tool", parts=function_response_parts)
|
||||
history.append(tool_response_content)
|
||||
|
||||
response2 = genai_client.models.generate_content(
|
||||
model="gemini-2.0-flash-001",
|
||||
contents=history,
|
||||
config=GenerateContentConfig(
|
||||
tools=genai_tools,
|
||||
),
|
||||
)
|
||||
final_model_response_content = response2.candidates[0].content
|
||||
history.append(final_model_response_content)
|
||||
print(response2.text)
|
||||
|
||||
asyncio.run(run_application())
|
||||
{{< include "quickstart/python/core/quickstart.py" >}}
|
||||
|
||||
{{< /tab >}}
|
||||
{{< /tabpane >}}
|
||||
|
||||
@@ -90,7 +90,7 @@ const queries = [
|
||||
"My check in dates would be from April 10, 2024 to April 19, 2024.",
|
||||
];
|
||||
|
||||
async function runApplication() {
|
||||
async function main() {
|
||||
const model = new ChatGoogleGenerativeAI({
|
||||
model: "gemini-2.0-flash",
|
||||
});
|
||||
@@ -137,9 +137,7 @@ async function runApplication() {
|
||||
}
|
||||
}
|
||||
|
||||
runApplication()
|
||||
.catch(console.error)
|
||||
.finally(() => console.log("\nApplication finished."));
|
||||
main();
|
||||
|
||||
{{< /tab >}}
|
||||
|
||||
@@ -169,7 +167,7 @@ const queries = [
|
||||
"My check in dates would be from April 10, 2024 to April 19, 2024.",
|
||||
];
|
||||
|
||||
async function run() {
|
||||
async function main() {
|
||||
const toolboxClient = new ToolboxClient("http://127.0.0.1:5000");
|
||||
|
||||
const ai = genkit({
|
||||
@@ -233,7 +231,7 @@ async function run() {
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
main();
|
||||
{{< /tab >}}
|
||||
|
||||
{{< tab header="LlamaIndex" lang="js" >}}
|
||||
@@ -358,7 +356,7 @@ function mapZodTypeToOpenAPIType(zodTypeName) {
|
||||
return typeMap[zodTypeName] || 'string';
|
||||
}
|
||||
|
||||
async function runApplication() {
|
||||
async function main() {
|
||||
|
||||
const toolboxClient = new ToolboxClient(TOOLBOX_URL);
|
||||
const toolboxTools = await toolboxClient.loadToolset("my-toolset");
|
||||
@@ -439,9 +437,7 @@ async function runApplication() {
|
||||
}
|
||||
}
|
||||
|
||||
runApplication()
|
||||
.catch(console.error)
|
||||
.finally(() => console.log("\nApplication finished."));
|
||||
main();
|
||||
{{< /tab >}}
|
||||
|
||||
{{< /tabpane >}}
|
||||
|
||||
70
docs/en/getting-started/quickstart/python/adk/quickstart.py
Normal file
70
docs/en/getting-started/quickstart/python/adk/quickstart.py
Normal file
@@ -0,0 +1,70 @@
|
||||
from google.adk.agents import Agent
|
||||
from google.adk.runners import Runner
|
||||
from google.adk.sessions import InMemorySessionService
|
||||
from google.adk.artifacts.in_memory_artifact_service import InMemoryArtifactService
|
||||
from google.genai import types
|
||||
from toolbox_core import ToolboxSyncClient
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
# TODO(developer): replace this with your Google API key
|
||||
|
||||
os.environ['GOOGLE_API_KEY'] = 'your-api-key'
|
||||
|
||||
async def main():
|
||||
with ToolboxSyncClient("http://127.0.0.1:5000") as toolbox_client:
|
||||
|
||||
prompt = """
|
||||
You're a helpful hotel assistant. You handle hotel searching, booking and
|
||||
cancellations. When the user searches for a hotel, mention it's name, id,
|
||||
location and price tier. Always mention hotel ids while performing any
|
||||
searches. This is very important for any operations. For any bookings or
|
||||
cancellations, please provide the appropriate confirmation. Be sure to
|
||||
update checkin or checkout dates if mentioned by the user.
|
||||
Don't ask for confirmations from the user.
|
||||
"""
|
||||
|
||||
root_agent = Agent(
|
||||
model='gemini-2.0-flash-001',
|
||||
name='hotel_agent',
|
||||
description='A helpful AI assistant.',
|
||||
instruction=prompt,
|
||||
tools=toolbox_client.load_toolset("my-toolset"),
|
||||
)
|
||||
|
||||
session_service = InMemorySessionService()
|
||||
artifacts_service = InMemoryArtifactService()
|
||||
session = await session_service.create_session(
|
||||
state={}, app_name='hotel_agent', user_id='123'
|
||||
)
|
||||
runner = Runner(
|
||||
app_name='hotel_agent',
|
||||
agent=root_agent,
|
||||
artifact_service=artifacts_service,
|
||||
session_service=session_service,
|
||||
)
|
||||
|
||||
queries = [
|
||||
"Find hotels in Basel with Basel in its name.",
|
||||
"Can you book the Hilton Basel for me?",
|
||||
"Oh wait, this is too expensive. Please cancel it and book the Hyatt Regency instead.",
|
||||
"My check in dates would be from April 10, 2024 to April 19, 2024.",
|
||||
]
|
||||
|
||||
for query in queries:
|
||||
content = types.Content(role='user', parts=[types.Part(text=query)])
|
||||
events = runner.run(session_id=session.id,
|
||||
user_id='123', new_message=content)
|
||||
|
||||
responses = (
|
||||
part.text
|
||||
for event in events
|
||||
for part in event.content.parts
|
||||
if part.text is not None
|
||||
)
|
||||
|
||||
for text in responses:
|
||||
print(text)
|
||||
|
||||
asyncio.run(main())
|
||||
108
docs/en/getting-started/quickstart/python/core/quickstart.py
Normal file
108
docs/en/getting-started/quickstart/python/core/quickstart.py
Normal file
@@ -0,0 +1,108 @@
|
||||
import asyncio
|
||||
|
||||
from google import genai
|
||||
from google.genai.types import (
|
||||
Content,
|
||||
FunctionDeclaration,
|
||||
GenerateContentConfig,
|
||||
Part,
|
||||
Tool,
|
||||
)
|
||||
|
||||
from toolbox_core import ToolboxClient
|
||||
|
||||
prompt = """
|
||||
You're a helpful hotel assistant. You handle hotel searching, booking and
|
||||
cancellations. When the user searches for a hotel, mention it's name, id,
|
||||
location and price tier. Always mention hotel id while performing any
|
||||
searches. This is very important for any operations. For any bookings or
|
||||
cancellations, please provide the appropriate confirmation. Be sure to
|
||||
update checkin or checkout dates if mentioned by the user.
|
||||
Don't ask for confirmations from the user.
|
||||
"""
|
||||
|
||||
queries = [
|
||||
"Find hotels in Basel with Basel in its name.",
|
||||
"Please book the hotel Hilton Basel for me.",
|
||||
"This is too expensive. Please cancel it.",
|
||||
"Please book Hyatt Regency for me",
|
||||
"My check in dates for my booking would be from April 10, 2024 to April 19, 2024.",
|
||||
]
|
||||
|
||||
async def main():
|
||||
async with ToolboxClient("http://127.0.0.1:5000") as toolbox_client:
|
||||
|
||||
# The toolbox_tools list contains Python callables (functions/methods) designed for LLM tool-use
|
||||
# integration. While this example uses Google's genai client, these callables can be adapted for
|
||||
# various function-calling or agent frameworks. For easier integration with supported frameworks
|
||||
# (https://github.com/googleapis/mcp-toolbox-python-sdk/tree/main/packages), use the
|
||||
# provided wrapper packages, which handle framework-specific boilerplate.
|
||||
toolbox_tools = await toolbox_client.load_toolset("my-toolset")
|
||||
genai_client = genai.Client(
|
||||
vertexai=True, project="project-id", location="us-central1"
|
||||
)
|
||||
|
||||
genai_tools = [
|
||||
Tool(
|
||||
function_declarations=[
|
||||
FunctionDeclaration.from_callable_with_api_option(callable=tool)
|
||||
]
|
||||
)
|
||||
for tool in toolbox_tools
|
||||
]
|
||||
history = []
|
||||
for query in queries:
|
||||
user_prompt_content = Content(
|
||||
role="user",
|
||||
parts=[Part.from_text(text=query)],
|
||||
)
|
||||
history.append(user_prompt_content)
|
||||
|
||||
response = genai_client.models.generate_content(
|
||||
model="gemini-2.0-flash-001",
|
||||
contents=history,
|
||||
config=GenerateContentConfig(
|
||||
system_instruction=prompt,
|
||||
tools=genai_tools,
|
||||
),
|
||||
)
|
||||
history.append(response.candidates[0].content)
|
||||
function_response_parts = []
|
||||
for function_call in response.function_calls:
|
||||
fn_name = function_call.name
|
||||
# The tools are sorted alphabetically
|
||||
if fn_name == "search-hotels-by-name":
|
||||
function_result = await toolbox_tools[3](**function_call.args)
|
||||
elif fn_name == "search-hotels-by-location":
|
||||
function_result = await toolbox_tools[2](**function_call.args)
|
||||
elif fn_name == "book-hotel":
|
||||
function_result = await toolbox_tools[0](**function_call.args)
|
||||
elif fn_name == "update-hotel":
|
||||
function_result = await toolbox_tools[4](**function_call.args)
|
||||
elif fn_name == "cancel-hotel":
|
||||
function_result = await toolbox_tools[1](**function_call.args)
|
||||
else:
|
||||
raise ValueError("Function name not present.")
|
||||
function_response = {"result": function_result}
|
||||
function_response_part = Part.from_function_response(
|
||||
name=function_call.name,
|
||||
response=function_response,
|
||||
)
|
||||
function_response_parts.append(function_response_part)
|
||||
|
||||
if function_response_parts:
|
||||
tool_response_content = Content(role="tool", parts=function_response_parts)
|
||||
history.append(tool_response_content)
|
||||
|
||||
response2 = genai_client.models.generate_content(
|
||||
model="gemini-2.0-flash-001",
|
||||
contents=history,
|
||||
config=GenerateContentConfig(
|
||||
tools=genai_tools,
|
||||
),
|
||||
)
|
||||
final_model_response_content = response2.candidates[0].content
|
||||
history.append(final_model_response_content)
|
||||
print(response2.text)
|
||||
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,52 @@
|
||||
import asyncio
|
||||
|
||||
from langgraph.prebuilt import create_react_agent
|
||||
|
||||
# TODO(developer): replace this with another import if needed
|
||||
|
||||
from langchain_google_vertexai import ChatVertexAI
|
||||
|
||||
# from langchain_google_genai import ChatGoogleGenerativeAI
|
||||
|
||||
# from langchain_anthropic import ChatAnthropic
|
||||
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
|
||||
from toolbox_langchain import ToolboxClient
|
||||
|
||||
prompt = """
|
||||
You're a helpful hotel assistant. You handle hotel searching, booking and
|
||||
cancellations. When the user searches for a hotel, mention it's name, id,
|
||||
location and price tier. Always mention hotel ids while performing any
|
||||
searches. This is very important for any operations. For any bookings or
|
||||
cancellations, please provide the appropriate confirmation. Be sure to
|
||||
update checkin or checkout dates if mentioned by the user.
|
||||
Don't ask for confirmations from the user.
|
||||
"""
|
||||
|
||||
queries = [
|
||||
"Find hotels in Basel with Basel in its name.",
|
||||
"Can you book the Hilton Basel for me?",
|
||||
"Oh wait, this is too expensive. Please cancel it and book the Hyatt Regency instead.",
|
||||
"My check in dates would be from April 10, 2024 to April 19, 2024.",
|
||||
]
|
||||
|
||||
async def main():
|
||||
# TODO(developer): replace this with another model if needed
|
||||
model = ChatVertexAI(model_name="gemini-2.0-flash-001")
|
||||
# model = ChatGoogleGenerativeAI(model="gemini-2.0-flash-001")
|
||||
# model = ChatAnthropic(model="claude-3-5-sonnet-20240620")
|
||||
|
||||
# Load the tools from the Toolbox server
|
||||
async with ToolboxClient("http://127.0.0.1:5000") as client:
|
||||
tools = await client.aload_toolset()
|
||||
|
||||
agent = create_react_agent(model, tools, checkpointer=MemorySaver())
|
||||
|
||||
config = {"configurable": {"thread_id": "thread-1"}}
|
||||
for query in queries:
|
||||
inputs = {"messages": [("user", prompt + query)]}
|
||||
response = agent.invoke(inputs, stream_mode="values", config=config)
|
||||
print(response["messages"][-1].content)
|
||||
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,63 @@
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
from llama_index.core.agent.workflow import AgentWorkflow
|
||||
|
||||
from llama_index.core.workflow import Context
|
||||
|
||||
# TODO(developer): replace this with another import if needed
|
||||
|
||||
from llama_index.llms.google_genai import GoogleGenAI
|
||||
|
||||
# from llama_index.llms.anthropic import Anthropic
|
||||
|
||||
from toolbox_llamaindex import ToolboxClient
|
||||
|
||||
prompt = """
|
||||
You're a helpful hotel assistant. You handle hotel searching, booking and
|
||||
cancellations. When the user searches for a hotel, mention it's name, id,
|
||||
location and price tier. Always mention hotel ids while performing any
|
||||
searches. This is very important for any operations. For any bookings or
|
||||
cancellations, please provide the appropriate confirmation. Be sure to
|
||||
update checkin or checkout dates if mentioned by the user.
|
||||
Don't ask for confirmations from the user.
|
||||
"""
|
||||
|
||||
queries = [
|
||||
"Find hotels in Basel with Basel in its name.",
|
||||
"Can you book the Hilton Basel for me?",
|
||||
"Oh wait, this is too expensive. Please cancel it and book the Hyatt Regency instead.",
|
||||
"My check in dates would be from April 10, 2024 to April 19, 2024.",
|
||||
]
|
||||
|
||||
async def main():
|
||||
# TODO(developer): replace this with another model if needed
|
||||
llm = GoogleGenAI(
|
||||
model="gemini-2.0-flash-001",
|
||||
vertexai_config={"project": "project-id", "location": "us-central1"},
|
||||
)
|
||||
# llm = GoogleGenAI(
|
||||
# api_key=os.getenv("GOOGLE_API_KEY"),
|
||||
# model="gemini-2.0-flash-001",
|
||||
# )
|
||||
# llm = Anthropic(
|
||||
# model="claude-3-7-sonnet-latest",
|
||||
# api_key=os.getenv("ANTHROPIC_API_KEY")
|
||||
# )
|
||||
|
||||
# Load the tools from the Toolbox server
|
||||
async with ToolboxClient("http://127.0.0.1:5000") as client:
|
||||
tools = await client.aload_toolset()
|
||||
|
||||
agent = AgentWorkflow.from_tools_or_functions(
|
||||
tools,
|
||||
llm=llm,
|
||||
system_prompt=prompt,
|
||||
)
|
||||
ctx = Context(agent)
|
||||
for query in queries:
|
||||
response = await agent.run(user_msg=query, ctx=ctx)
|
||||
print(f"---- {query} ----")
|
||||
print(str(response))
|
||||
|
||||
asyncio.run(main())
|
||||
@@ -5,329 +5,9 @@ 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.13.0/linux/amd64/toolbox
|
||||
{{< /tab >}}
|
||||
|
||||
{{< tab header="darwin/arm64" lang="bash" >}}
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.13.0/darwin/arm64/toolbox
|
||||
{{< /tab >}}
|
||||
|
||||
{{< tab header="darwin/amd64" lang="bash" >}}
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.13.0/darwin/amd64/toolbox
|
||||
{{< /tab >}}
|
||||
|
||||
{{< tab header="windows/amd64" lang="bash" >}}
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.13.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 >}}
|
||||
<html>
|
||||
<head>
|
||||
<link rel="canonical" href="https://cloud.google.com/firestore/native/docs/connect-ide-using-mcp-toolbox"/>
|
||||
<meta http-equiv="refresh" content="0;url=https://cloud.google.com/firestore/native/docs/connect-ide-using-mcp-toolbox"/>
|
||||
</head>
|
||||
</html>
|
||||
@@ -36,12 +36,15 @@ avoiding full table scans or complex filters.
|
||||
|
||||
## Available Tools
|
||||
|
||||
- [`bigquery-sql`](../tools/bigquery/bigquery-sql.md)
|
||||
Run SQL queries directly against BigQuery datasets.
|
||||
- [`bigquery-conversational-analytics`](../tools/bigquery/bigquery-conversational-analytics.md)
|
||||
Allows conversational interaction with a BigQuery source.
|
||||
|
||||
- [`bigquery-execute-sql`](../tools/bigquery/bigquery-execute-sql.md)
|
||||
Execute structured queries using parameters.
|
||||
|
||||
- [`bigquery-forecast`](../tools/bigquery/bigquery-forecast.md)
|
||||
Forecasts time series data in BigQuery.
|
||||
|
||||
- [`bigquery-get-dataset-info`](../tools/bigquery/bigquery-get-dataset-info.md)
|
||||
Retrieve metadata for a specific dataset.
|
||||
|
||||
@@ -54,6 +57,9 @@ avoiding full table scans or complex filters.
|
||||
- [`bigquery-list-table-ids`](../tools/bigquery/bigquery-list-table-ids.md)
|
||||
List tables in a given dataset.
|
||||
|
||||
- [`bigquery-sql`](../tools/bigquery/bigquery-sql.md)
|
||||
Run SQL queries directly against BigQuery datasets.
|
||||
|
||||
### Pre-built Configurations
|
||||
|
||||
- [BigQuery using MCP](https://googleapis.github.io/genai-toolbox/how-to/connect-ide/bigquery_mcp/)
|
||||
@@ -104,6 +110,7 @@ sources:
|
||||
my-bigquery-source:
|
||||
kind: "bigquery"
|
||||
project: "my-project-id"
|
||||
# location: "US" # Optional: Specifies the location for query jobs.
|
||||
```
|
||||
|
||||
Initialize a BigQuery source that uses the client's access token:
|
||||
@@ -114,6 +121,7 @@ sources:
|
||||
kind: "bigquery"
|
||||
project: "my-project-id"
|
||||
useClientOAuth: true
|
||||
# location: "US" # Optional: Specifies the location for query jobs.
|
||||
```
|
||||
|
||||
## Reference
|
||||
@@ -121,6 +129,6 @@ sources:
|
||||
| **field** | **type** | **required** | **description** |
|
||||
|----------------|:--------:|:------------:|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| kind | string | true | Must be "bigquery". |
|
||||
| project | string | true | Id of the GCP project that the cluster was created in (e.g. "my-project-id"). |
|
||||
| location | string | false | Specifies the location (e.g., 'us', 'asia-northeast1') in which to run the query job. This location must match the location of any tables referenced in the query. The default behavior is for it to be executed in the US multi-region |
|
||||
| project | string | true | Id of the Google Cloud project to use for billing and as the default project for BigQuery resources. |
|
||||
| location | string | false | Specifies the location (e.g., 'us', 'asia-northeast1') in which to run the query job. This location must match the location of any tables referenced in the query. Defaults to the table's location or 'US' if the location cannot be determined. [Learn More](https://cloud.google.com/bigquery/docs/locations) |
|
||||
| useClientOAuth | bool | false | If true, forwards the client's OAuth access token from the "Authorization" header to downstream queries. |
|
||||
|
||||
91
docs/en/resources/sources/clickhouse.md
Normal file
91
docs/en/resources/sources/clickhouse.md
Normal file
@@ -0,0 +1,91 @@
|
||||
---
|
||||
title: "ClickHouse"
|
||||
type: docs
|
||||
weight: 1
|
||||
description: >
|
||||
ClickHouse is an open-source, OLTP database.
|
||||
|
||||
---
|
||||
|
||||
## About
|
||||
|
||||
[ClickHouse][clickhouse-docs] is a fast, open-source, column-oriented database
|
||||
|
||||
[clickhouse-docs]: https://clickhouse.com/docs
|
||||
|
||||
## Available Tools
|
||||
|
||||
- [`clickhouse-execute-sql`](../tools/clickhouse/clickhouse-execute-sql.md)
|
||||
Execute parameterized SQL queries in ClickHouse with query logging.
|
||||
|
||||
- [`clickhouse-sql`](../tools/clickhouse/clickhouse-sql.md)
|
||||
Execute SQL queries as prepared statements in ClickHouse.
|
||||
|
||||
|
||||
## Requirements
|
||||
|
||||
### Database User
|
||||
|
||||
This source uses standard ClickHouse authentication. You will need to [create a
|
||||
ClickHouse user][clickhouse-users] (or with [ClickHouse Cloud][clickhouse-cloud]) to connect to the database with. The user
|
||||
should have appropriate permissions for the operations you plan to perform.
|
||||
|
||||
[clickhouse-cloud]: https://clickhouse.com/docs/getting-started/quick-start/cloud#connect-with-your-app
|
||||
[clickhouse-users]: https://clickhouse.com/docs/en/sql-reference/statements/create/user
|
||||
|
||||
### Network Access
|
||||
|
||||
ClickHouse supports multiple protocols:
|
||||
|
||||
- **HTTPS protocol** (default port 8443) - Secure HTTP access (default)
|
||||
- **HTTP protocol** (default port 8123) - Good for web-based access
|
||||
|
||||
## Example
|
||||
|
||||
### Secure Connection Example
|
||||
|
||||
```yaml
|
||||
sources:
|
||||
secure-clickhouse-source:
|
||||
kind: clickhouse
|
||||
host: clickhouse.example.com
|
||||
port: "8443"
|
||||
database: analytics
|
||||
user: ${CLICKHOUSE_USER}
|
||||
password: ${CLICKHOUSE_PASSWORD}
|
||||
protocol: https
|
||||
secure: true
|
||||
```
|
||||
|
||||
### HTTP Protocol Example
|
||||
|
||||
```yaml
|
||||
sources:
|
||||
http-clickhouse-source:
|
||||
kind: clickhouse
|
||||
host: localhost
|
||||
port: "8123"
|
||||
database: logs
|
||||
user: ${CLICKHOUSE_USER}
|
||||
password: ${CLICKHOUSE_PASSWORD}
|
||||
protocol: http
|
||||
secure: false
|
||||
```
|
||||
|
||||
{{< notice tip >}}
|
||||
Use environment variable replacement with the format ${ENV_NAME}
|
||||
instead of hardcoding your secrets into the configuration file.
|
||||
{{< /notice >}}
|
||||
|
||||
## Reference
|
||||
|
||||
| **field** | **type** | **required** | **description** |
|
||||
|-------------|:--------:|:------------:|------------------------------------------------------------------------------------|
|
||||
| kind | string | true | Must be "clickhouse". |
|
||||
| host | string | true | IP address or hostname to connect to (e.g. "127.0.0.1" or "clickhouse.example.com") |
|
||||
| port | string | true | Port to connect to (e.g. "8443" for HTTPS, "8123" for HTTP) |
|
||||
| database | string | true | Name of the ClickHouse database to connect to (e.g. "my_database"). |
|
||||
| user | string | true | Name of the ClickHouse user to connect as (e.g. "analytics_user"). |
|
||||
| password | string | false | Password of the ClickHouse user (e.g. "my-password"). |
|
||||
| protocol | string | false | Connection protocol: "https" (default) or "http". |
|
||||
| secure | boolean | false | Whether to use a secure connection (TLS). Default: false. |
|
||||
@@ -47,7 +47,8 @@ a self-signed ssl certificate for the Looker server. Anything other than "true"
|
||||
will be interpreted as false.
|
||||
|
||||
The client id and client secret are seemingly random character sequences
|
||||
assigned by the looker server.
|
||||
assigned by the looker server. If you are using Looker OAuth you don't need
|
||||
these settings
|
||||
|
||||
{{< notice tip >}}
|
||||
Use environment variable replacement with the format ${ENV_NAME}
|
||||
@@ -60,10 +61,11 @@ instead of hardcoding your secrets into the configuration file.
|
||||
| -------------------- | :------: | :----------: | ----------------------------------------------------------------------------------------- |
|
||||
| kind | string | true | Must be "looker". |
|
||||
| base_url | string | true | The URL of your Looker server with no trailing /). |
|
||||
| client_id | string | true | The client id assigned by Looker. |
|
||||
| client_secret | string | true | The client secret assigned by Looker. |
|
||||
| verify_ssl | string | true | Whether to check the ssl certificate of the server. |
|
||||
| client_id | string | false | The client id assigned by Looker. |
|
||||
| client_secret | string | false | The client secret assigned by Looker. |
|
||||
| verify_ssl | string | false | Whether to check the ssl certificate of the server. |
|
||||
| timeout | string | false | Maximum time to wait for query execution (e.g. "30s", "2m"). By default, 120s is applied. |
|
||||
| use_client_oauth | string | false | Use OAuth tokens instead of client_id and client_secret. (default: false) |
|
||||
| show_hidden_models | string | false | Show or hide hidden models. (default: true) |
|
||||
| show_hidden_explores | string | false | Show or hide hidden explores. (default: true) |
|
||||
| show_hidden_fields | string | false | Show or hide hidden fields. (default: true) |
|
||||
|
||||
@@ -42,6 +42,9 @@ sources:
|
||||
database: my_db
|
||||
user: ${USER_NAME}
|
||||
password: ${PASSWORD}
|
||||
# Optional TLS and other driver parameters. For example, enable preferred TLS:
|
||||
# queryParams:
|
||||
# tls: preferred
|
||||
queryTimeout: 30s # Optional: query timeout duration
|
||||
```
|
||||
|
||||
@@ -61,3 +64,4 @@ instead of hardcoding your secrets into the configuration file.
|
||||
| user | string | true | Name of the MySQL user to connect as (e.g. "my-mysql-user"). |
|
||||
| password | string | true | Password of the MySQL user (e.g. "my-password"). |
|
||||
| queryTimeout | string | false | Maximum time to wait for query execution (e.g. "30s", "2m"). By default, no timeout is applied. |
|
||||
| queryParams | map<string,string> | false | Arbitrary DSN parameters passed to the driver (e.g. `tls: preferred`, `charset: utf8mb4`). Useful for enabling TLS or other connection options. |
|
||||
|
||||
7
docs/en/resources/tools/clickhouse/_index.md
Normal file
7
docs/en/resources/tools/clickhouse/_index.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
title: "ClickHouse"
|
||||
type: docs
|
||||
weight: 1
|
||||
description: >
|
||||
Tools for interacting with ClickHouse databases and tables.
|
||||
---
|
||||
46
docs/en/resources/tools/clickhouse/clickhouse-execute-sql.md
Normal file
46
docs/en/resources/tools/clickhouse/clickhouse-execute-sql.md
Normal file
@@ -0,0 +1,46 @@
|
||||
---
|
||||
title: "clickhouse-execute-sql"
|
||||
type: docs
|
||||
weight: 1
|
||||
description: >
|
||||
A "clickhouse-execute-sql" tool executes a SQL statement against a ClickHouse
|
||||
database.
|
||||
aliases:
|
||||
- /resources/tools/clickhouse-execute-sql
|
||||
---
|
||||
|
||||
## About
|
||||
|
||||
A `clickhouse-execute-sql` tool executes a SQL statement against a ClickHouse
|
||||
database. It's compatible with the [clickhouse](../../sources/clickhouse.md) source.
|
||||
|
||||
`clickhouse-execute-sql` takes one input parameter `sql` and runs the SQL
|
||||
statement against the specified `source`. This tool includes query logging
|
||||
capabilities for monitoring and debugging purposes.
|
||||
|
||||
> **Note:** This tool is intended for developer assistant workflows with
|
||||
> human-in-the-loop and shouldn't be used for production agents.
|
||||
|
||||
## Example
|
||||
|
||||
```yaml
|
||||
tools:
|
||||
execute_sql_tool:
|
||||
kind: clickhouse-execute-sql
|
||||
source: my-clickhouse-instance
|
||||
description: Use this tool to execute SQL statements against ClickHouse.
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
| **parameter** | **type** | **required** | **description** |
|
||||
|---------------|:--------:|:------------:|----------------------------------------------------|
|
||||
| sql | string | true | The SQL statement to execute against the database |
|
||||
|
||||
## Reference
|
||||
|
||||
| **field** | **type** | **required** | **description** |
|
||||
|-------------|:--------:|:------------:|---------------------------------------------------------|
|
||||
| kind | string | true | Must be "clickhouse-execute-sql". |
|
||||
| source | string | true | Name of the ClickHouse source to execute SQL against. |
|
||||
| description | string | true | Description of the tool that is passed to the LLM. |
|
||||
81
docs/en/resources/tools/clickhouse/clickhouse-sql.md
Normal file
81
docs/en/resources/tools/clickhouse/clickhouse-sql.md
Normal file
@@ -0,0 +1,81 @@
|
||||
---
|
||||
title: "clickhouse-sql"
|
||||
type: docs
|
||||
weight: 2
|
||||
description: >
|
||||
A "clickhouse-sql" tool executes SQL queries as prepared statements in ClickHouse.
|
||||
aliases:
|
||||
- /resources/tools/clickhouse-sql
|
||||
---
|
||||
|
||||
## About
|
||||
|
||||
A `clickhouse-sql` tool executes SQL queries as prepared statements against a
|
||||
ClickHouse database. It's compatible with the [clickhouse](../../sources/clickhouse.md) source.
|
||||
|
||||
This tool supports both template parameters (for SQL statement customization)
|
||||
and regular parameters (for prepared statement values), providing flexible
|
||||
query execution capabilities.
|
||||
|
||||
## Example
|
||||
|
||||
```yaml
|
||||
tools:
|
||||
my_analytics_query:
|
||||
kind: clickhouse-sql
|
||||
source: my-clickhouse-instance
|
||||
description: Get user analytics for a specific date range
|
||||
statement: |
|
||||
SELECT
|
||||
user_id,
|
||||
count(*) as event_count,
|
||||
max(timestamp) as last_event
|
||||
FROM events
|
||||
WHERE date >= ? AND date <= ?
|
||||
GROUP BY user_id
|
||||
ORDER BY event_count DESC
|
||||
LIMIT ?
|
||||
parameters:
|
||||
- name: start_date
|
||||
description: Start date for the query (YYYY-MM-DD format)
|
||||
- name: end_date
|
||||
description: End date for the query (YYYY-MM-DD format)
|
||||
- name: limit
|
||||
description: Maximum number of results to return
|
||||
```
|
||||
|
||||
## Template Parameters Example
|
||||
|
||||
```yaml
|
||||
tools:
|
||||
flexible_table_query:
|
||||
kind: clickhouse-sql
|
||||
source: my-clickhouse-instance
|
||||
description: Query any table with flexible columns
|
||||
statement: |
|
||||
SELECT {{columns}}
|
||||
FROM {{table_name}}
|
||||
WHERE created_date >= ?
|
||||
LIMIT ?
|
||||
templateParameters:
|
||||
- name: columns
|
||||
description: Comma-separated list of columns to select
|
||||
- name: table_name
|
||||
description: Name of the table to query
|
||||
parameters:
|
||||
- name: start_date
|
||||
description: Start date filter
|
||||
- name: limit
|
||||
description: Maximum number of results
|
||||
```
|
||||
|
||||
## Reference
|
||||
|
||||
| **field** | **type** | **required** | **description** |
|
||||
|--------------------|:------------------:|:------------:|-----------------------------------------------------------|
|
||||
| kind | string | true | Must be "clickhouse-sql". |
|
||||
| source | string | true | Name of the ClickHouse source to execute SQL against. |
|
||||
| description | string | true | Description of the tool that is passed to the LLM. |
|
||||
| statement | string | true | The SQL statement template to execute. |
|
||||
| parameters | array of Parameter | false | Parameters for prepared statement values. |
|
||||
| templateParameters | array of Parameter | false | Parameters for SQL statement template customization. |
|
||||
@@ -33,6 +33,12 @@ tools:
|
||||
|
||||
It takes two parameters, the model_name looked up from get_models and the
|
||||
explore_name looked up from get_explores.
|
||||
|
||||
If this returns a suggestions field for a dimension, the contents of suggestions
|
||||
can be used as filters for this field. If this returns a suggest_explore and
|
||||
suggest_dimension, a query against that explore and dimension can be used to find
|
||||
valid filters for this field.
|
||||
|
||||
```
|
||||
|
||||
The response is a json array with the following elements:
|
||||
@@ -45,7 +51,10 @@ The response is a json array with the following elements:
|
||||
"label": "field label",
|
||||
"label_short": "field short label",
|
||||
"tags": ["tags", ...],
|
||||
"synonyms": ["synonyms", ...]
|
||||
"synonyms": ["synonyms", ...],
|
||||
"suggestions": ["suggestion", ...],
|
||||
"suggest_explore": "explore",
|
||||
"suggest_dimension": "dimension"
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -21,6 +21,16 @@ It's compatible with the following sources:
|
||||
`looker-get-explores` accepts one parameter, the
|
||||
`model` id.
|
||||
|
||||
The return type is an array of maps, each map is formatted like:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "explore name",
|
||||
"description": "explore description",
|
||||
"label": "explore label",
|
||||
"group_label": "group label"
|
||||
}
|
||||
```
|
||||
## Example
|
||||
|
||||
```yaml
|
||||
|
||||
@@ -45,7 +45,10 @@ The response is a json array with the following elements:
|
||||
"label": "field label",
|
||||
"label_short": "field short label",
|
||||
"tags": ["tags", ...],
|
||||
"synonyms": ["synonyms", ...]
|
||||
"synonyms": ["synonyms", ...],
|
||||
"suggestions": ["suggestion", ...],
|
||||
"suggest_explore": "explore",
|
||||
"suggest_dimension": "dimension"
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -33,6 +33,12 @@ tools:
|
||||
|
||||
It takes two parameters, the model_name looked up from get_models and the
|
||||
explore_name looked up from get_explores.
|
||||
|
||||
If this returns a suggestions field for a measure, the contents of suggestions
|
||||
can be used as filters for this field. If this returns a suggest_explore and
|
||||
suggest_dimension, a query against that explore and dimension can be used to find
|
||||
valid filters for this field.
|
||||
|
||||
```
|
||||
|
||||
The response is a json array with the following elements:
|
||||
@@ -45,7 +51,10 @@ The response is a json array with the following elements:
|
||||
"label": "field label",
|
||||
"label_short": "field short label",
|
||||
"tags": ["tags", ...],
|
||||
"synonyms": ["synonyms", ...]
|
||||
"synonyms": ["synonyms", ...],
|
||||
"suggestions": ["suggestion", ...],
|
||||
"suggest_explore": "explore",
|
||||
"suggest_dimension": "dimension"
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -45,7 +45,10 @@ The response is a json array with the following elements:
|
||||
"label": "field label",
|
||||
"label_short": "field short label",
|
||||
"tags": ["tags", ...],
|
||||
"synonyms": ["synonyms", ...]
|
||||
"synonyms": ["synonyms", ...],
|
||||
"suggestions": ["suggestion", ...],
|
||||
"suggest_explore": "explore",
|
||||
"suggest_dimension": "dimension"
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
140
docs/en/samples/looker/looker_gemini_oauth/_index.md
Normal file
140
docs/en/samples/looker/looker_gemini_oauth/_index.md
Normal file
@@ -0,0 +1,140 @@
|
||||
---
|
||||
title: "Gemini-CLI and OAuth"
|
||||
type: docs
|
||||
weight: 2
|
||||
description: >
|
||||
How to connect to Looker from Gemini-CLI with end-user credentials
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Gemini-CLI can be configured to get an OAuth token from Looker, then send this
|
||||
token to MCP Toolbox as part of the request. MCP Toolbox can then use this token
|
||||
to authentincate with Looker. This means that there is no need to get a Looker
|
||||
Client ID and Client Secret. This also means that MCP Toolbox can be set up as a
|
||||
shared resource.
|
||||
|
||||
This configuration requires Toolbox v0.14.0 or later.
|
||||
|
||||
## Step 1: Register the OAuth App in Looker
|
||||
|
||||
You first need to register the OAuth application. Refer to the documentation
|
||||
[here](https://cloud.google.com/looker/docs/api-cors#registering_an_oauth_client_application).
|
||||
You may need to ask an administrator to do this for you.
|
||||
|
||||
1. Go to the API Explorer application, locate "Register OAuth App", and press
|
||||
the "Run It" button.
|
||||
1. Set the `client_guid` to "gemini-cli".
|
||||
1. Set the `redirect_uri` to "http://localhost:7777/oauth/callback".
|
||||
1. The `display_name` and `description` can be "Gemini-CLI" or anything
|
||||
meaningful.
|
||||
1. Set `enabled` to "true".
|
||||
1. Check the box confirming that you understand this API will change data.
|
||||
1. Click the "Run" button.
|
||||
|
||||

|
||||
|
||||
## Step 2: Install and configure Toolbox
|
||||
|
||||
In this section, we will download Toolbox and run the Toolbox server.
|
||||
|
||||
1. Download the latest version of Toolbox as a binary:
|
||||
|
||||
{{< notice tip >}}
|
||||
Select the
|
||||
[correct binary](https://github.com/googleapis/genai-toolbox/releases)
|
||||
corresponding to your OS and CPU architecture.
|
||||
{{< /notice >}}
|
||||
<!-- {x-release-please-start-version} -->
|
||||
```bash
|
||||
export OS="linux/amd64" # one of linux/amd64, darwin/arm64, darwin/amd64, or windows/amd64
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.14.0/$OS/toolbox
|
||||
```
|
||||
<!-- {x-release-please-end} -->
|
||||
|
||||
1. Make the binary executable:
|
||||
|
||||
```bash
|
||||
chmod +x toolbox
|
||||
```
|
||||
|
||||
1. Create a file `looker_env` with the settings for your
|
||||
Looker instance.
|
||||
|
||||
```bash
|
||||
export LOOKER_BASE_URL=https://looker.example.com
|
||||
export LOOKER_VERIFY_SSL=true
|
||||
```
|
||||
|
||||
In some instances you may need to append `:19999` to
|
||||
the LOOKER_BASE_URL.
|
||||
|
||||
1. Load the looker_env file into your environment.
|
||||
|
||||
```bash
|
||||
source looker_env
|
||||
```
|
||||
|
||||
1. Run the Toolbox server using the prebuilt Looker tools.
|
||||
|
||||
```bash
|
||||
./toolbox --prebuilt looker
|
||||
```
|
||||
|
||||
The toolbox server will begin listening on localhost port 5000. Leave it
|
||||
running and continue in another terminal.
|
||||
|
||||
Later, when it is time to shut everything down, you can quit the toolbox
|
||||
server with Ctrl-C in this terminal window.
|
||||
|
||||
## Step 3: Configure Gemini-CLI
|
||||
|
||||
1. Edit the file `~/.gemini/settings.json`. Add the following, substituting your
|
||||
Looker server host name for `looker.example.com`.
|
||||
|
||||
```json
|
||||
"mcpServers": {
|
||||
"looker": {
|
||||
"httpUrl": "http://localhost:5000/mcp",
|
||||
"oauth": {
|
||||
"enabled": true,
|
||||
"clientId": "gemini-cli",
|
||||
"authorizationUrl": "https://looker.example.com/auth",
|
||||
"tokenUrl": "https://looker.example.com/api/token",
|
||||
"scopes": ["cors_api"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `authorizationUrl` should point to the URL you use to access Looker via the
|
||||
web UI. The `tokenUrl` should point to the URL you use to access Looker via
|
||||
the API. In some cases you will need to use the port number `:19999` after
|
||||
the host name but before the `/api/token` part.
|
||||
|
||||
1. Start Gemini-CLI.
|
||||
|
||||
1. Authenticate with the command `/mcp auth looker`. Gemini-CLI will open up a
|
||||
browser where you will confirm that you want to access Looker with your
|
||||
account.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
1. Use Gemini-CLI with your tools.
|
||||
|
||||
## Using Toolbox as a Shared Service
|
||||
|
||||
Toolbox can be run on another server as a shared service accessed by multiple
|
||||
users. We strongly recommend running toolbox behind a web proxy such as `nginx`
|
||||
which will provide SSL encryption. Google Cloud Run is another good way to run
|
||||
toolbox. You will connect to a service like `https://toolbox.example.com/mcp`.
|
||||
The proxy server will handle the SSL encryption and certificates. Then it will
|
||||
foward the requests to `http://localhost:5000/mcp` running in that environment.
|
||||
The details of the config are beyond the scope of this document, but will be
|
||||
familiar to your system administrators.
|
||||
|
||||
To use the shared service, just change the `localhost:5000` in the `httpUrl` in
|
||||
`~/.gemini/settings.json` to the host name and possibly the port of the shared
|
||||
service.
|
||||
BIN
docs/en/samples/looker/looker_gemini_oauth/authenticated.png
Normal file
BIN
docs/en/samples/looker/looker_gemini_oauth/authenticated.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
BIN
docs/en/samples/looker/looker_gemini_oauth/authorize.png
Normal file
BIN
docs/en/samples/looker/looker_gemini_oauth/authorize.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 75 KiB |
BIN
docs/en/samples/looker/looker_gemini_oauth/registration.png
Normal file
BIN
docs/en/samples/looker/looker_gemini_oauth/registration.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 81 KiB |
28
go.mod
28
go.mod
@@ -1,24 +1,25 @@
|
||||
module github.com/googleapis/genai-toolbox
|
||||
|
||||
go 1.24
|
||||
go 1.24.0
|
||||
|
||||
toolchain go1.25.0
|
||||
|
||||
require (
|
||||
cloud.google.com/go/alloydbconn v1.15.5
|
||||
cloud.google.com/go/bigquery v1.69.0
|
||||
cloud.google.com/go/bigtable v1.38.0
|
||||
cloud.google.com/go/cloudsqlconn v1.18.0
|
||||
cloud.google.com/go/bigtable v1.39.0
|
||||
cloud.google.com/go/cloudsqlconn v1.18.1
|
||||
cloud.google.com/go/dataplex v1.26.0
|
||||
cloud.google.com/go/firestore v1.18.0
|
||||
cloud.google.com/go/spanner v1.84.1
|
||||
github.com/ClickHouse/clickhouse-go/v2 v2.40.1
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace v1.29.0
|
||||
github.com/cenkalti/backoff/v5 v5.0.3
|
||||
github.com/couchbase/gocb/v2 v2.11.0
|
||||
github.com/couchbase/tools-common/http v1.0.9
|
||||
github.com/fsnotify/fsnotify v1.9.0
|
||||
github.com/go-chi/chi/v5 v5.2.2
|
||||
github.com/go-chi/chi/v5 v5.2.3
|
||||
github.com/go-chi/httplog/v2 v2.1.1
|
||||
github.com/go-chi/render v1.0.3
|
||||
github.com/go-goquery/goquery v1.0.1
|
||||
@@ -32,7 +33,7 @@ require (
|
||||
github.com/looker-open-source/sdk-codegen/go v0.25.10
|
||||
github.com/microsoft/go-mssqldb v1.9.3
|
||||
github.com/nakagami/firebirdsql v0.9.15
|
||||
github.com/neo4j/neo4j-go-driver/v5 v5.28.2
|
||||
github.com/neo4j/neo4j-go-driver/v5 v5.28.3
|
||||
github.com/redis/go-redis/v9 v9.12.1
|
||||
github.com/spf13/cobra v1.9.1
|
||||
github.com/thlib/go-timezone-local v0.0.7
|
||||
@@ -49,19 +50,27 @@ require (
|
||||
go.opentelemetry.io/otel/trace v1.37.0
|
||||
golang.org/x/oauth2 v0.30.0
|
||||
google.golang.org/api v0.248.0
|
||||
google.golang.org/genproto v0.0.0-20250818200422-3122310a409c
|
||||
google.golang.org/genproto v0.0.0-20250826171959-ef028d996bc1
|
||||
modernc.org/sqlite v1.38.2
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/ClickHouse/ch-go v0.67.0 // indirect
|
||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||
github.com/andybalholm/cascadia v1.3.3 // indirect
|
||||
github.com/go-faster/city v1.0.1 // indirect
|
||||
github.com/go-faster/errors v0.7.1 // indirect
|
||||
github.com/paulmach/orb v0.11.1 // indirect
|
||||
github.com/segmentio/asm v1.2.0 // indirect
|
||||
github.com/shopspring/decimal v1.4.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
|
||||
gonum.org/v1/gonum v0.16.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
cel.dev/expr v0.24.0 // indirect
|
||||
cloud.google.com/go v0.121.4 // indirect
|
||||
cloud.google.com/go v0.121.6 // indirect
|
||||
cloud.google.com/go/alloydb v1.18.0 // indirect
|
||||
cloud.google.com/go/auth v0.16.5 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||
@@ -132,7 +141,6 @@ require (
|
||||
github.com/pierrec/lz4/v4 v4.1.22 // indirect
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/shopspring/decimal v1.2.0 // indirect
|
||||
github.com/spf13/pflag v1.0.6 // indirect
|
||||
github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect
|
||||
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
||||
@@ -164,10 +172,10 @@ require (
|
||||
golang.org/x/time v0.12.0 // indirect
|
||||
golang.org/x/tools v0.35.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250811230008-5f3141c8851a // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250818200422-3122310a409c // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c // indirect
|
||||
google.golang.org/grpc v1.74.2 // indirect
|
||||
google.golang.org/protobuf v1.36.7 // indirect
|
||||
google.golang.org/protobuf v1.36.8 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
modernc.org/libc v1.66.3 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
|
||||
80
go.sum
80
go.sum
@@ -38,8 +38,8 @@ cloud.google.com/go v0.104.0/go.mod h1:OO6xxXdJyvuJPcEPBLN9BJPD+jep5G1+2U5B5gkRY
|
||||
cloud.google.com/go v0.105.0/go.mod h1:PrLgOJNe5nfE9UMxKxgXj4mD3voiP+YQ6gdt6KMFOKM=
|
||||
cloud.google.com/go v0.107.0/go.mod h1:wpc2eNrD7hXUTy8EKS10jkxpZBjASrORK7goS+3YX2I=
|
||||
cloud.google.com/go v0.110.0/go.mod h1:SJnCLqQ0FCFGSZMUNUf84MV3Aia54kn7pi8st7tMzaY=
|
||||
cloud.google.com/go v0.121.4 h1:cVvUiY0sX0xwyxPwdSU2KsF9knOVmtRyAMt8xou0iTs=
|
||||
cloud.google.com/go v0.121.4/go.mod h1:XEBchUiHFJbz4lKBZwYBDHV/rSyfFktk737TLDU089s=
|
||||
cloud.google.com/go v0.121.6 h1:waZiuajrI28iAf40cWgycWNgaXPO06dupuS+sgibK6c=
|
||||
cloud.google.com/go v0.121.6/go.mod h1:coChdst4Ea5vUpiALcYKXEpR1S9ZgXbhEzzMcMR66vI=
|
||||
cloud.google.com/go/accessapproval v1.4.0/go.mod h1:zybIuC3KpDOvotz59lFe5qxRZx6C75OtwbisN56xYB4=
|
||||
cloud.google.com/go/accessapproval v1.5.0/go.mod h1:HFy3tuiGvMdcd/u+Cu5b9NkO1pEICJ46IR82PoUdplw=
|
||||
cloud.google.com/go/accessapproval v1.6.0/go.mod h1:R0EiYnwV5fsRFiKZkPHr6mwyk2wxUJ30nL4j2pcFY2E=
|
||||
@@ -139,8 +139,8 @@ cloud.google.com/go/bigquery v1.49.0/go.mod h1:Sv8hMmTFFYBlt/ftw2uN6dFdQPzBlREY9
|
||||
cloud.google.com/go/bigquery v1.50.0/go.mod h1:YrleYEh2pSEbgTBZYMJ5SuSr0ML3ypjRB1zgf7pvQLU=
|
||||
cloud.google.com/go/bigquery v1.69.0 h1:rZvHnjSUs5sHK3F9awiuFk2PeOaB8suqNuim21GbaTc=
|
||||
cloud.google.com/go/bigquery v1.69.0/go.mod h1:TdGLquA3h/mGg+McX+GsqG9afAzTAcldMjqhdjHTLew=
|
||||
cloud.google.com/go/bigtable v1.38.0 h1:L/PnUXRtAzFfa7qMULJHt4cXa/O2dqPJEkzYNGA4hfo=
|
||||
cloud.google.com/go/bigtable v1.38.0/go.mod h1:o/lntJarF3Y5C0XYLMJLjLYwxaRbcrtM0BiV57ymXbI=
|
||||
cloud.google.com/go/bigtable v1.39.0 h1:NF0aaSend+Z5CKND2vWY9fgDwaeZ4bDgzUdgw8rk75Y=
|
||||
cloud.google.com/go/bigtable v1.39.0/go.mod h1:zgL2Vxux9Bx+TcARDJDUxVyE+BCUfP2u4Zm9qeHF+g0=
|
||||
cloud.google.com/go/billing v1.4.0/go.mod h1:g9IdKBEFlItS8bTtlrZdVLWSSdSyFUZKXNS02zKMOZY=
|
||||
cloud.google.com/go/billing v1.5.0/go.mod h1:mztb1tBc3QekhjSgmpf/CV4LzWXLzCArwpLmP2Gm88s=
|
||||
cloud.google.com/go/billing v1.6.0/go.mod h1:WoXzguj+BeHXPbKfNWkqVtDdzORazmCjraY+vrxcyvI=
|
||||
@@ -167,8 +167,8 @@ cloud.google.com/go/cloudbuild v1.9.0/go.mod h1:qK1d7s4QlO0VwfYn5YuClDGg2hfmLZEb
|
||||
cloud.google.com/go/clouddms v1.3.0/go.mod h1:oK6XsCDdW4Ib3jCCBugx+gVjevp2TMXFtgxvPSee3OM=
|
||||
cloud.google.com/go/clouddms v1.4.0/go.mod h1:Eh7sUGCC+aKry14O1NRljhjyrr0NFC0G2cjwX0cByRk=
|
||||
cloud.google.com/go/clouddms v1.5.0/go.mod h1:QSxQnhikCLUw13iAbffF2CZxAER3xDGNHjsTAkQJcQA=
|
||||
cloud.google.com/go/cloudsqlconn v1.18.0 h1:mP6TY/7I+nrnIh6vmbWCRJPxpFBZSL6AZhW6HaYC/OI=
|
||||
cloud.google.com/go/cloudsqlconn v1.18.0/go.mod h1:58bxZZ17Mz5D83ddMT8x6w56yKpcmVXyaOwGWkzGcMw=
|
||||
cloud.google.com/go/cloudsqlconn v1.18.1 h1:IIvs7QJ8eqKUUHSon13Joie9oH7/i7MJwNzBLG+FrhM=
|
||||
cloud.google.com/go/cloudsqlconn v1.18.1/go.mod h1:58bxZZ17Mz5D83ddMT8x6w56yKpcmVXyaOwGWkzGcMw=
|
||||
cloud.google.com/go/cloudtasks v1.5.0/go.mod h1:fD92REy1x5woxkKEkLdvavGnPJGEn8Uic9nWuLzqCpY=
|
||||
cloud.google.com/go/cloudtasks v1.6.0/go.mod h1:C6Io+sxuke9/KNRkbQpihnW93SWDU3uXt92nu85HkYI=
|
||||
cloud.google.com/go/cloudtasks v1.7.0/go.mod h1:ImsfdYWwlWNJbdgPIIGJWC+gemEGTBK/SunNQQNCAb4=
|
||||
@@ -563,8 +563,8 @@ cloud.google.com/go/storage v1.23.0/go.mod h1:vOEEDNFnciUMhBeT6hsJIn3ieU5cFRmzeL
|
||||
cloud.google.com/go/storage v1.27.0/go.mod h1:x9DOL8TK/ygDUMieqwfhdpQryTeEkhGKMi80i/iqR2s=
|
||||
cloud.google.com/go/storage v1.28.1/go.mod h1:Qnisd4CqDdo6BGs2AD5LLnEsmSQ80wQ5ogcBBKhU86Y=
|
||||
cloud.google.com/go/storage v1.29.0/go.mod h1:4puEjyTKnku6gfKoTfNOU/W+a9JyuVNxjpS5GBrB8h4=
|
||||
cloud.google.com/go/storage v1.55.0 h1:NESjdAToN9u1tmhVqhXCaCwYBuvEhZLLv0gBr+2znf0=
|
||||
cloud.google.com/go/storage v1.55.0/go.mod h1:ztSmTTwzsdXe5syLVS0YsbFxXuvEmEyZj7v7zChEmuY=
|
||||
cloud.google.com/go/storage v1.56.0 h1:iixmq2Fse2tqxMbWhLWC9HfBj1qdxqAmiK8/eqtsLxI=
|
||||
cloud.google.com/go/storage v1.56.0/go.mod h1:Tpuj6t4NweCLzlNbw9Z9iwxEkrSem20AetIeH/shgVU=
|
||||
cloud.google.com/go/storagetransfer v1.5.0/go.mod h1:dxNzUopWy7RQevYFHewchb29POFv3/AaBgnhqzqiK0w=
|
||||
cloud.google.com/go/storagetransfer v1.6.0/go.mod h1:y77xm4CQV/ZhFZH75PLEXY0ROiS7Gh6pSKrM8dJyg6I=
|
||||
cloud.google.com/go/storagetransfer v1.7.0/go.mod h1:8Giuj1QNb1kfLAiWM1bN6dHzfdlDAVC9rv9abHot2W4=
|
||||
@@ -632,8 +632,8 @@ cloud.google.com/go/workflows v1.7.0/go.mod h1:JhSrZuVZWuiDfKEFxU0/F1PQjmpnpcoIS
|
||||
cloud.google.com/go/workflows v1.8.0/go.mod h1:ysGhmEajwZxGn1OhGOGKsTXc5PyxOc0vfKf5Af+to4M=
|
||||
cloud.google.com/go/workflows v1.9.0/go.mod h1:ZGkj1aFIOd9c8Gerkjjq7OW7I5+l6cSvT3ujaO/WwSA=
|
||||
cloud.google.com/go/workflows v1.10.0/go.mod h1:fZ8LmRmZQWacon9UCX1r/g/DfAXx5VcPALq2CxzdePw=
|
||||
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
|
||||
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
|
||||
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
@@ -655,6 +655,10 @@ github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJ
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/ClickHouse/ch-go v0.67.0 h1:18MQF6vZHj+4/hTRaK7JbS/TIzn4I55wC+QzO24uiqc=
|
||||
github.com/ClickHouse/ch-go v0.67.0/go.mod h1:2MSAeyVmgt+9a2k2SQPPG1b4qbTPzdGDpf1+bcHh+18=
|
||||
github.com/ClickHouse/clickhouse-go/v2 v2.40.1 h1:PbwsHBgqXRydU7jKULD1C8CHmifczffvQqmFvltM2W4=
|
||||
github.com/ClickHouse/clickhouse-go/v2 v2.40.1/go.mod h1:GDzSBLVhladVm8V01aEB36IoBOVLLICfyeuiIp/8Ezc=
|
||||
github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.3 h1:2afWGsMzkIcN8Qm4mgPJKZWyroE5QBszMiDMYEBrnfw=
|
||||
github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.3/go.mod h1:dppbR7CwXD4pgtV9t3wD1812RaLDcBjtblcDF5f1vI0=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 h1:ErKg/3iS1AKcTkf3yixlZ54f9U1rljCkQyEXWUnIUxc=
|
||||
@@ -684,6 +688,8 @@ github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T
|
||||
github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
|
||||
github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM=
|
||||
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
||||
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
|
||||
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
|
||||
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
||||
@@ -796,8 +802,8 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/r
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/docker/cli v26.1.4+incompatible h1:I8PHdc0MtxEADqYJZvhBrW9bo8gawKwwenxRM7/rLu8=
|
||||
github.com/docker/cli v26.1.4+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/docker v27.1.1+incompatible h1:hO/M4MtV36kzKldqnA37IWhebRA+LnqqcqDja6kVaKY=
|
||||
github.com/docker/docker v27.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI=
|
||||
github.com/docker/docker v28.3.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
|
||||
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
@@ -840,12 +846,16 @@ github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618=
|
||||
github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
|
||||
github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=
|
||||
github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
|
||||
github.com/go-chi/httplog/v2 v2.1.1 h1:ojojiu4PIaoeJ/qAO4GWUxJqvYUTobeo7zmuHQJAxRk=
|
||||
github.com/go-chi/httplog/v2 v2.1.1/go.mod h1:/XXdxicJsp4BA5fapgIC3VuTD+z0Z/VzukoB3VDc1YE=
|
||||
github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=
|
||||
github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=
|
||||
github.com/go-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw=
|
||||
github.com/go-faster/city v1.0.1/go.mod h1:jKcUJId49qdW3L1qKHH/3wPeUstCVpVSXTM6vO3VcTw=
|
||||
github.com/go-faster/errors v0.7.1 h1:MkJTnDoEdi9pDabt1dpWf7AA8/BaSYZqibYyhZ20AYg=
|
||||
github.com/go-faster/errors v0.7.1/go.mod h1:5ySTjWFiphBs07IKuiL69nxdfd5+fzh1u7FPGZP2quo=
|
||||
github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g=
|
||||
github.com/go-fonts/latin-modern v0.2.0/go.mod h1:rQVLdDMK+mK1xscDwsqM5J8U2jrRa3T0ecnM9pNujks=
|
||||
github.com/go-fonts/liberation v0.1.1/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY=
|
||||
@@ -933,6 +943,7 @@ github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiu
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
|
||||
@@ -1088,6 +1099,7 @@ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:C
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE=
|
||||
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
|
||||
github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
@@ -1134,6 +1146,7 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
|
||||
github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE=
|
||||
github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
|
||||
github.com/nakagami/chacha20 v0.1.0 h1:2fbf5KeVUw7oRpAe6/A7DqvBJLYYu0ka5WstFbnkEVo=
|
||||
@@ -1142,19 +1155,22 @@ github.com/nakagami/firebirdsql v0.9.15 h1:Mf05jaFI8+kjy6sBstsAu76zOkJ44AGd6cpAp
|
||||
github.com/nakagami/firebirdsql v0.9.15/go.mod h1:bZKRs3rpHAjJgXAoc9YiPobTz3R22i41Zjo+llIS2B0=
|
||||
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/neo4j/neo4j-go-driver/v5 v5.28.2 h1:uG7nMK0zS/a/iSWMZgCIY40SfYzWBc6uSrMONhiIS0U=
|
||||
github.com/neo4j/neo4j-go-driver/v5 v5.28.2/go.mod h1:Vff8OwT7QpLm7L2yYr85XNWe9Rbqlbeb9asNXJTHO4k=
|
||||
github.com/neo4j/neo4j-go-driver/v5 v5.28.3 h1:OHP/vzX0oZ2YUY5DnGUp7QY21BIpOzw+Pp+Dga8zYl4=
|
||||
github.com/neo4j/neo4j-go-driver/v5 v5.28.3/go.mod h1:Vff8OwT7QpLm7L2yYr85XNWe9Rbqlbeb9asNXJTHO4k=
|
||||
github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8=
|
||||
github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
|
||||
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
|
||||
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
||||
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
||||
github.com/opencontainers/runc v1.1.13 h1:98S2srgG9vw0zWcDpFMn5TRrh8kLxa/5OFUstuUhmRs=
|
||||
github.com/opencontainers/runc v1.1.13/go.mod h1:R016aXacfp/gwQBYw2FDGa9m+n6atbLWrYY8hNMT/sA=
|
||||
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
|
||||
github.com/ory/dockertest/v3 v3.11.0 h1:OiHcxKAvSDUwsEVh2BjxQQc/5EHz9n0va9awCtNGuyA=
|
||||
github.com/ory/dockertest/v3 v3.11.0/go.mod h1:VIPxS1gwT9NpPOrfD3rACs8Y9Z7yhzO4SB194iUDnUI=
|
||||
github.com/paulmach/orb v0.11.1 h1:3koVegMC4X/WeiXYz9iswopaTwMem53NzTJuTF20JzU=
|
||||
github.com/paulmach/orb v0.11.1/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU=
|
||||
github.com/paulmach/protoscan v0.2.1/go.mod h1:SpcSwydNLrxUGSDvXvO0P7g7AuhJ7lcKfDlhJCDw2gY=
|
||||
github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY=
|
||||
github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=
|
||||
github.com/phpdave11/gofpdi v1.0.13/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=
|
||||
@@ -1193,8 +1209,10 @@ github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWN
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w=
|
||||
github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245/go.mod h1:pQAZKsJ8yyVxGRWYNEm9oFB8ieLgKFnamEyDmSA0BRk=
|
||||
github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
|
||||
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
|
||||
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
|
||||
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
||||
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
@@ -1228,14 +1246,17 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/thlib/go-timezone-local v0.0.7 h1:fX8zd3aJydqLlTs/TrROrIIdztzsdFV23OzOQx31jII=
|
||||
github.com/thlib/go-timezone-local v0.0.7/go.mod h1:/Tnicc6m/lsJE0irFMA0LfIwTBo4QP7A8IfyIv4zZKI=
|
||||
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
|
||||
github.com/trinodb/trino-go-client v0.328.0 h1:X6hrGGysA3nvyVcz8kJbBS98srLNTNsnNYwRkMC1atA=
|
||||
github.com/trinodb/trino-go-client v0.328.0/go.mod h1:e/nck9W6hy+9bbyZEpXKFlNsufn3lQGpUgDL1d5f1FI=
|
||||
github.com/valkey-io/valkey-go v1.0.64 h1:3u4+b6D6zs9JQs254TLy4LqitCMHHr9XorP9GGk7XY4=
|
||||
github.com/valkey-io/valkey-go v1.0.64/go.mod h1:bHmwjIEOrGq/ubOJfh5uMRs7Xj6mV3mQ/ZXUbmqpjqY=
|
||||
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
|
||||
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
||||
github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g=
|
||||
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
|
||||
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
|
||||
github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8=
|
||||
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
|
||||
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
|
||||
@@ -1244,6 +1265,9 @@ github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHo
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
|
||||
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
|
||||
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
|
||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
@@ -1261,6 +1285,7 @@ github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
|
||||
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
|
||||
gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b h1:7gd+rd8P3bqcn/96gOZa3F5dpJr/vEiDQYlNb/y2uNs=
|
||||
gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b/go.mod h1:T3BPAOm2cqquPa0MKWeNkmOM5RQsRhkrwMWonFMN7fE=
|
||||
go.mongodb.org/mongo-driver v1.11.4/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g=
|
||||
go.mongodb.org/mongo-driver v1.17.4 h1:jUorfmVzljjr0FLzYQsGP8cgN/qzzxlY9Vh0C9KFXVw=
|
||||
go.mongodb.org/mongo-driver v1.17.4/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
@@ -1330,6 +1355,7 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
|
||||
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
@@ -1439,6 +1465,7 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b
|
||||
golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
@@ -1936,10 +1963,10 @@ google.golang.org/genproto v0.0.0-20230323212658-478b75c54725/go.mod h1:UUQDJDOl
|
||||
google.golang.org/genproto v0.0.0-20230330154414-c0448cd141ea/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak=
|
||||
google.golang.org/genproto v0.0.0-20230331144136-dcfb400f0633/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak=
|
||||
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=
|
||||
google.golang.org/genproto v0.0.0-20250818200422-3122310a409c h1:ZERoum3uuqL0PRSc6SXielu26FN96T4BUGaaW0oL+c8=
|
||||
google.golang.org/genproto v0.0.0-20250818200422-3122310a409c/go.mod h1:Q8kep885BJnK3Jt6QZXIFeLHSzoAQtlI1CCloQigiyU=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250811230008-5f3141c8851a h1:DMCgtIAIQGZqJXMVzJF4MV8BlWoJh2ZuFiRdAleyr58=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250811230008-5f3141c8851a/go.mod h1:y2yVLIE/CSMCPXaHnSKXxu1spLPnglFLegmgdY23uuE=
|
||||
google.golang.org/genproto v0.0.0-20250826171959-ef028d996bc1 h1:Nm5SEGIguOIBDXs5rhfz2aKwEVWlgwC58UcmEnLDc8Y=
|
||||
google.golang.org/genproto v0.0.0-20250826171959-ef028d996bc1/go.mod h1:Jz9LrroM7Mcm+a0QrLh4UpZ1B/WhjIbqwEcUf4y08nQ=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250818200422-3122310a409c h1:AtEkQdl5b6zsybXcbz00j1LwNodDuH6hVifIaNqk7NQ=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250818200422-3122310a409c/go.mod h1:ea2MjsO70ssTfCjiwHgI0ZFqcw45Ksuk2ckf9G468GA=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c h1:qXWI/sQtv5UKboZ/zUk7h+mrf/lXORyI+n9DKDAusdg=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c/go.mod h1:gw1tLEfykwDz2ET4a12jcXt4couGAm7IwsVaTy0Sflo=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
@@ -2003,10 +2030,11 @@ google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqw
|
||||
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.29.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A=
|
||||
google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
|
||||
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/ini.v1 v1.61.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
|
||||
@@ -24,6 +24,7 @@ var expectedToolSources = []string{
|
||||
"alloydb-postgres-admin",
|
||||
"alloydb-postgres",
|
||||
"bigquery",
|
||||
"clickhouse",
|
||||
"cloud-sql-mssql",
|
||||
"cloud-sql-mysql",
|
||||
"cloud-sql-postgres",
|
||||
@@ -85,6 +86,7 @@ func TestGetPrebuiltTool(t *testing.T) {
|
||||
alloydb_admin_config, _ := Get("alloydb-postgres-admin")
|
||||
alloydb_config, _ := Get("alloydb-postgres")
|
||||
bigquery_config, _ := Get("bigquery")
|
||||
clickhouse_config, _ := Get("clickhouse")
|
||||
cloudsqlpg_config, _ := Get("cloud-sql-postgres")
|
||||
cloudsqlmysql_config, _ := Get("cloud-sql-mysql")
|
||||
cloudsqlmssql_config, _ := Get("cloud-sql-mssql")
|
||||
@@ -105,6 +107,9 @@ func TestGetPrebuiltTool(t *testing.T) {
|
||||
if len(bigquery_config) <= 0 {
|
||||
t.Fatalf("unexpected error: could not fetch bigquery prebuilt tools yaml")
|
||||
}
|
||||
if len(clickhouse_config) <= 0 {
|
||||
t.Fatalf("unexpected error: could not fetch clickhouse prebuilt tools yaml")
|
||||
}
|
||||
if len(cloudsqlpg_config) <= 0 {
|
||||
t.Fatalf("unexpected error: could not fetch cloud sql pg prebuilt tools yaml")
|
||||
}
|
||||
|
||||
19
internal/prebuiltconfigs/tools/clickhouse.yaml
Normal file
19
internal/prebuiltconfigs/tools/clickhouse.yaml
Normal file
@@ -0,0 +1,19 @@
|
||||
sources:
|
||||
clickhouse-source:
|
||||
kind: clickhouse
|
||||
host: ${CLICKHOUSE_HOST}
|
||||
port: ${CLICKHOUSE_PORT}
|
||||
user: ${CLICKHOUSE_USER}
|
||||
password: ${CLICKHOUSE_PASSWORD}
|
||||
database: ${CLICKHOUSE_DATABASE}
|
||||
protocol: ${CLICKHOUSE_PROTOCOL}
|
||||
|
||||
tools:
|
||||
execute_sql:
|
||||
kind: clickhouse-execute-sql
|
||||
source: clickhouse-source
|
||||
description: Use this tool to execute SQL.
|
||||
|
||||
toolsets:
|
||||
clickhouse-database-tools:
|
||||
- execute_sql
|
||||
@@ -1,11 +1,26 @@
|
||||
# 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.
|
||||
|
||||
sources:
|
||||
looker-source:
|
||||
kind: looker
|
||||
base_url: ${LOOKER_BASE_URL}
|
||||
client_id: ${LOOKER_CLIENT_ID}
|
||||
client_secret: ${LOOKER_CLIENT_SECRET}
|
||||
client_id: ${LOOKER_CLIENT_ID:}
|
||||
client_secret: ${LOOKER_CLIENT_SECRET:}
|
||||
verify_ssl: ${LOOKER_VERIFY_SSL:true}
|
||||
timeout: 600s
|
||||
use_client_oauth: ${LOOKER_USE_CLIENT_OAUTH:false}
|
||||
show_hidden_models: ${LOOKER_SHOW_HIDDEN_MODELS:true}
|
||||
show_hidden_explores: ${LOOKER_SHOW_HIDDEN_EXPLORES:true}
|
||||
show_hidden_fields: ${LOOKER_SHOW_HIDDEN_FIELDS:true}
|
||||
@@ -38,6 +53,11 @@ tools:
|
||||
It takes two parameters, the model_name looked up from get_models and the
|
||||
explore_name looked up from get_explores.
|
||||
|
||||
If this returns a suggestions field for a dimension, the contents of suggestions
|
||||
can be used as filters for this field. If this returns a suggest_explore and
|
||||
suggest_dimension, a query against that explore and dimension can be used to find
|
||||
valid filters for this field.
|
||||
|
||||
get_measures:
|
||||
kind: looker-get-measures
|
||||
source: looker-source
|
||||
@@ -48,6 +68,11 @@ tools:
|
||||
It takes two parameters, the model_name looked up from get_models and the
|
||||
explore_name looked up from get_explores.
|
||||
|
||||
If this returns a suggestions field for a measure, the contents of suggestions
|
||||
can be used as filters for this field. If this returns a suggest_explore and
|
||||
suggest_dimension, a query against that explore and dimension can be used to find
|
||||
valid filters for this field.
|
||||
|
||||
get_filters:
|
||||
kind: looker-get-filters
|
||||
source: looker-source
|
||||
|
||||
@@ -212,7 +212,7 @@ func TestToolGetEndpoint(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestToolInvokeEndpoint(t *testing.T) {
|
||||
mockTools := []MockTool{tool1, tool2}
|
||||
mockTools := []MockTool{tool1, tool2, tool4, tool5}
|
||||
toolsMap, toolsets := setUpResources(t, mockTools)
|
||||
r, shutdown := setUpServer(t, "api", toolsMap, toolsets)
|
||||
defer shutdown()
|
||||
@@ -247,6 +247,20 @@ func TestToolInvokeEndpoint(t *testing.T) {
|
||||
want: "",
|
||||
isErr: true,
|
||||
},
|
||||
{
|
||||
name: "tool4",
|
||||
toolName: tool4.Name,
|
||||
requestBody: bytes.NewBuffer([]byte(`{}`)),
|
||||
want: "",
|
||||
isErr: true,
|
||||
},
|
||||
{
|
||||
name: "tool5",
|
||||
toolName: tool5.Name,
|
||||
requestBody: bytes.NewBuffer([]byte(`{}`)),
|
||||
want: "",
|
||||
isErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
|
||||
@@ -36,10 +36,12 @@ var _ tools.Tool = &MockTool{}
|
||||
|
||||
// MockTool is used to mock tools in tests
|
||||
type MockTool struct {
|
||||
Name string
|
||||
Description string
|
||||
Params []tools.Parameter
|
||||
manifest tools.Manifest
|
||||
Name string
|
||||
Description string
|
||||
Params []tools.Parameter
|
||||
manifest tools.Manifest
|
||||
unauthorized bool
|
||||
requiresClientAuthrorization bool
|
||||
}
|
||||
|
||||
func (t MockTool) Invoke(context.Context, tools.ParamValues, tools.AccessToken) (any, error) {
|
||||
@@ -59,12 +61,15 @@ func (t MockTool) Manifest() tools.Manifest {
|
||||
}
|
||||
return tools.Manifest{Description: t.Description, Parameters: pMs}
|
||||
}
|
||||
|
||||
func (t MockTool) Authorized(verifiedAuthServices []string) bool {
|
||||
return true
|
||||
// defaulted to true
|
||||
return !t.unauthorized
|
||||
}
|
||||
|
||||
func (t MockTool) RequiresClientAuthorization() bool {
|
||||
return false
|
||||
// defaulted to false
|
||||
return t.requiresClientAuthrorization
|
||||
}
|
||||
|
||||
func (t MockTool) McpManifest() tools.McpManifest {
|
||||
@@ -111,6 +116,18 @@ var tool3 = MockTool{
|
||||
},
|
||||
}
|
||||
|
||||
var tool4 = MockTool{
|
||||
Name: "unauthorized_tool",
|
||||
Params: []tools.Parameter{},
|
||||
unauthorized: true,
|
||||
}
|
||||
|
||||
var tool5 = MockTool{
|
||||
Name: "require_client_auth_tool",
|
||||
Params: []tools.Parameter{},
|
||||
requiresClientAuthrorization: true,
|
||||
}
|
||||
|
||||
// setUpResources setups resources to test against
|
||||
func setUpResources(t *testing.T, mockTools []MockTool) (map[string]tools.Tool, map[string]tools.Toolset) {
|
||||
toolsMap := make(map[string]tools.Tool)
|
||||
|
||||
@@ -144,7 +144,7 @@ func (s *stdioSession) readInputStream(ctx context.Context) error {
|
||||
}
|
||||
return err
|
||||
}
|
||||
v, res, err := processMcpMessage(ctx, []byte(line), s.server, s.protocol, "", "")
|
||||
v, res, err := processMcpMessage(ctx, []byte(line), s.server, s.protocol, "", nil)
|
||||
if err != nil {
|
||||
// errors during the processing of message will generate a valid MCP Error response.
|
||||
// server can continue to run.
|
||||
@@ -332,6 +332,8 @@ func methodNotAllowed(s *Server, w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// httpHandler handles all mcp messages.
|
||||
func httpHandler(s *Server, w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
ctx, span := s.instrumentation.Tracer.Start(r.Context(), "toolbox/server/mcp")
|
||||
r = r.WithContext(ctx)
|
||||
ctx = util.WithLogger(r.Context(), s.logger)
|
||||
@@ -404,14 +406,9 @@ func httpHandler(s *Server, w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Extract OAuth access token from the "Authorization" header (currently for
|
||||
// BigQuery end-user credentials usage only)
|
||||
accessToken := tools.AccessToken(r.Header.Get("Authorization"))
|
||||
|
||||
v, res, err := processMcpMessage(ctx, body, s, protocolVersion, toolsetName, accessToken)
|
||||
|
||||
v, res, err := processMcpMessage(ctx, body, s, protocolVersion, toolsetName, r.Header)
|
||||
if err != nil {
|
||||
s.logger.DebugContext(ctx, fmt.Errorf("error invoking tool: %w", err).Error())
|
||||
s.logger.DebugContext(ctx, fmt.Errorf("error processing message: %w", err).Error())
|
||||
}
|
||||
|
||||
// notifications will return empty string
|
||||
@@ -462,7 +459,7 @@ func httpHandler(s *Server, w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// processMcpMessage process the messages received from clients
|
||||
func processMcpMessage(ctx context.Context, body []byte, s *Server, protocolVersion string, toolsetName string, accessToken tools.AccessToken) (string, any, error) {
|
||||
func processMcpMessage(ctx context.Context, body []byte, s *Server, protocolVersion string, toolsetName string, header http.Header) (string, any, error) {
|
||||
logger, err := util.LoggerFromContext(ctx)
|
||||
if err != nil {
|
||||
return "", jsonrpc.NewError("", jsonrpc.INTERNAL_ERROR, err.Error(), nil), err
|
||||
@@ -517,8 +514,7 @@ func processMcpMessage(ctx context.Context, body []byte, s *Server, protocolVers
|
||||
err = fmt.Errorf("toolset does not exist")
|
||||
return "", jsonrpc.NewError(baseMessage.Id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err
|
||||
}
|
||||
|
||||
res, err := mcp.ProcessMethod(ctx, protocolVersion, baseMessage.Id, baseMessage.Method, toolset, s.ResourceMgr.GetToolsMap(), body, accessToken)
|
||||
res, err := mcp.ProcessMethod(ctx, protocolVersion, baseMessage.Id, baseMessage.Method, toolset, s.ResourceMgr.GetToolsMap(), s.ResourceMgr.GetAuthServiceMap(), body, header)
|
||||
return "", res, err
|
||||
}
|
||||
}
|
||||
|
||||
39
internal/server/mcp/jsonrpc/jsonrpc_test.go
Normal file
39
internal/server/mcp/jsonrpc/jsonrpc_test.go
Normal file
@@ -0,0 +1,39 @@
|
||||
// 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 jsonrpc
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewError(t *testing.T) {
|
||||
var id interface{} = "foo"
|
||||
code := 111
|
||||
message := "foo bar"
|
||||
want := JSONRPCError{
|
||||
Jsonrpc: "2.0",
|
||||
Id: "foo",
|
||||
Error: Error{
|
||||
Code: 111,
|
||||
Message: "foo bar",
|
||||
},
|
||||
}
|
||||
|
||||
got := NewError(id, code, message, nil)
|
||||
if !reflect.DeepEqual(want, got) {
|
||||
t.Fatalf("unexpected error: got %+v, want %+v", got, want)
|
||||
}
|
||||
}
|
||||
@@ -18,8 +18,10 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"slices"
|
||||
|
||||
"github.com/googleapis/genai-toolbox/internal/auth"
|
||||
"github.com/googleapis/genai-toolbox/internal/server/mcp/jsonrpc"
|
||||
mcputil "github.com/googleapis/genai-toolbox/internal/server/mcp/util"
|
||||
v20241105 "github.com/googleapis/genai-toolbox/internal/server/mcp/v20241105"
|
||||
@@ -93,14 +95,14 @@ func NotificationHandler(ctx context.Context, body []byte) error {
|
||||
|
||||
// ProcessMethod returns a response for the request.
|
||||
// This is the Operation phase of the lifecycle for MCP client-server connections.
|
||||
func ProcessMethod(ctx context.Context, mcpVersion string, id jsonrpc.RequestId, method string, toolset tools.Toolset, tools map[string]tools.Tool, body []byte, accessToken tools.AccessToken) (any, error) {
|
||||
func ProcessMethod(ctx context.Context, mcpVersion string, id jsonrpc.RequestId, method string, toolset tools.Toolset, tools map[string]tools.Tool, authServices map[string]auth.AuthService, body []byte, header http.Header) (any, error) {
|
||||
switch mcpVersion {
|
||||
case v20250618.PROTOCOL_VERSION:
|
||||
return v20250618.ProcessMethod(ctx, id, method, toolset, tools, body, accessToken)
|
||||
return v20250618.ProcessMethod(ctx, id, method, toolset, tools, authServices, body, header)
|
||||
case v20250326.PROTOCOL_VERSION:
|
||||
return v20250326.ProcessMethod(ctx, id, method, toolset, tools, body, accessToken)
|
||||
return v20250326.ProcessMethod(ctx, id, method, toolset, tools, authServices, body, header)
|
||||
default:
|
||||
return v20241105.ProcessMethod(ctx, id, method, toolset, tools, body, accessToken)
|
||||
return v20241105.ProcessMethod(ctx, id, method, toolset, tools, authServices, body, header)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,22 +20,24 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/googleapis/genai-toolbox/internal/auth"
|
||||
"github.com/googleapis/genai-toolbox/internal/server/mcp/jsonrpc"
|
||||
"github.com/googleapis/genai-toolbox/internal/tools"
|
||||
"github.com/googleapis/genai-toolbox/internal/util"
|
||||
)
|
||||
|
||||
// ProcessMethod returns a response for the request.
|
||||
func ProcessMethod(ctx context.Context, id jsonrpc.RequestId, method string, toolset tools.Toolset, tools map[string]tools.Tool, body []byte, accessToken tools.AccessToken) (any, error) {
|
||||
func ProcessMethod(ctx context.Context, id jsonrpc.RequestId, method string, toolset tools.Toolset, tools map[string]tools.Tool, authServices map[string]auth.AuthService, body []byte, header http.Header) (any, error) {
|
||||
switch method {
|
||||
case PING:
|
||||
return pingHandler(id)
|
||||
case TOOLS_LIST:
|
||||
return toolsListHandler(id, toolset, body)
|
||||
case TOOLS_CALL:
|
||||
return toolsCallHandler(ctx, id, tools, body, accessToken)
|
||||
return toolsCallHandler(ctx, id, tools, authServices, body, header)
|
||||
default:
|
||||
err := fmt.Errorf("invalid method %s", method)
|
||||
return jsonrpc.NewError(id, jsonrpc.METHOD_NOT_FOUND, err.Error(), nil), err
|
||||
@@ -69,7 +71,7 @@ func toolsListHandler(id jsonrpc.RequestId, toolset tools.Toolset, body []byte)
|
||||
}
|
||||
|
||||
// toolsCallHandler generate a response for tools call.
|
||||
func toolsCallHandler(ctx context.Context, id jsonrpc.RequestId, toolsMap map[string]tools.Tool, body []byte, accessToken tools.AccessToken) (any, error) {
|
||||
func toolsCallHandler(ctx context.Context, id jsonrpc.RequestId, toolsMap map[string]tools.Tool, authServices map[string]auth.AuthService, body []byte, header http.Header) (any, error) {
|
||||
// retrieve logger from context
|
||||
logger, err := util.LoggerFromContext(ctx)
|
||||
if err != nil {
|
||||
@@ -91,6 +93,9 @@ func toolsCallHandler(ctx context.Context, id jsonrpc.RequestId, toolsMap map[st
|
||||
return jsonrpc.NewError(id, jsonrpc.INVALID_PARAMS, err.Error(), nil), err
|
||||
}
|
||||
|
||||
// Get access token
|
||||
accessToken := tools.AccessToken(header.Get("Authorization"))
|
||||
|
||||
// Check if this specific tool requires the standard authorization header
|
||||
if tool.RequiresClientAuthorization() {
|
||||
if accessToken == "" {
|
||||
@@ -111,10 +116,42 @@ func toolsCallHandler(ctx context.Context, id jsonrpc.RequestId, toolsMap map[st
|
||||
return jsonrpc.NewError(id, jsonrpc.INTERNAL_ERROR, err.Error(), nil), err
|
||||
}
|
||||
|
||||
// Tool authentication
|
||||
// claimsFromAuth maps the name of the authservice to the claims retrieved from it.
|
||||
// Since MCP doesn't support auth, an empty map will be use every time.
|
||||
claimsFromAuth := make(map[string]map[string]any)
|
||||
|
||||
// if using stdio, header will be nil and auth will not be supported
|
||||
if header != nil {
|
||||
for _, aS := range authServices {
|
||||
claims, err := aS.GetClaimsFromHeader(ctx, header)
|
||||
if err != nil {
|
||||
logger.DebugContext(ctx, err.Error())
|
||||
continue
|
||||
}
|
||||
if claims == nil {
|
||||
// authService not present in header
|
||||
continue
|
||||
}
|
||||
claimsFromAuth[aS.GetName()] = claims
|
||||
}
|
||||
}
|
||||
|
||||
// Tool authorization check
|
||||
verifiedAuthServices := make([]string, len(claimsFromAuth))
|
||||
i := 0
|
||||
for k := range claimsFromAuth {
|
||||
verifiedAuthServices[i] = k
|
||||
i++
|
||||
}
|
||||
|
||||
// Check if any of the specified auth services is verified
|
||||
isAuthorized := tool.Authorized(verifiedAuthServices)
|
||||
if !isAuthorized {
|
||||
err = fmt.Errorf("unauthorized Tool call: Please make sure your specify correct auth headers: %w", tools.ErrUnauthorized)
|
||||
return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err
|
||||
}
|
||||
logger.DebugContext(ctx, "tool invocation authorized")
|
||||
|
||||
params, err := tool.ParseParams(data, claimsFromAuth)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("provided parameters were invalid: %w", err)
|
||||
@@ -122,11 +159,6 @@ func toolsCallHandler(ctx context.Context, id jsonrpc.RequestId, toolsMap map[st
|
||||
}
|
||||
logger.DebugContext(ctx, fmt.Sprintf("invocation params: %s", params))
|
||||
|
||||
if !tool.Authorized([]string{}) {
|
||||
err = fmt.Errorf("unauthorized Tool call: `authRequired` is set for the target Tool but isn't supported through MCP Tool call: %w", tools.ErrUnauthorized)
|
||||
return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err
|
||||
}
|
||||
|
||||
// run tool invocation and generate response.
|
||||
results, err := tool.Invoke(ctx, params, accessToken)
|
||||
if err != nil {
|
||||
|
||||
@@ -20,22 +20,24 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/googleapis/genai-toolbox/internal/auth"
|
||||
"github.com/googleapis/genai-toolbox/internal/server/mcp/jsonrpc"
|
||||
"github.com/googleapis/genai-toolbox/internal/tools"
|
||||
"github.com/googleapis/genai-toolbox/internal/util"
|
||||
)
|
||||
|
||||
// ProcessMethod returns a response for the request.
|
||||
func ProcessMethod(ctx context.Context, id jsonrpc.RequestId, method string, toolset tools.Toolset, tools map[string]tools.Tool, body []byte, accessToken tools.AccessToken) (any, error) {
|
||||
func ProcessMethod(ctx context.Context, id jsonrpc.RequestId, method string, toolset tools.Toolset, tools map[string]tools.Tool, authServices map[string]auth.AuthService, body []byte, header http.Header) (any, error) {
|
||||
switch method {
|
||||
case PING:
|
||||
return pingHandler(id)
|
||||
case TOOLS_LIST:
|
||||
return toolsListHandler(id, toolset, body)
|
||||
case TOOLS_CALL:
|
||||
return toolsCallHandler(ctx, id, tools, body, accessToken)
|
||||
return toolsCallHandler(ctx, id, tools, authServices, body, header)
|
||||
default:
|
||||
err := fmt.Errorf("invalid method %s", method)
|
||||
return jsonrpc.NewError(id, jsonrpc.METHOD_NOT_FOUND, err.Error(), nil), err
|
||||
@@ -69,7 +71,7 @@ func toolsListHandler(id jsonrpc.RequestId, toolset tools.Toolset, body []byte)
|
||||
}
|
||||
|
||||
// toolsCallHandler generate a response for tools call.
|
||||
func toolsCallHandler(ctx context.Context, id jsonrpc.RequestId, toolsMap map[string]tools.Tool, body []byte, accessToken tools.AccessToken) (any, error) {
|
||||
func toolsCallHandler(ctx context.Context, id jsonrpc.RequestId, toolsMap map[string]tools.Tool, authServices map[string]auth.AuthService, body []byte, header http.Header) (any, error) {
|
||||
// retrieve logger from context
|
||||
logger, err := util.LoggerFromContext(ctx)
|
||||
if err != nil {
|
||||
@@ -91,6 +93,9 @@ func toolsCallHandler(ctx context.Context, id jsonrpc.RequestId, toolsMap map[st
|
||||
return jsonrpc.NewError(id, jsonrpc.INVALID_PARAMS, err.Error(), nil), err
|
||||
}
|
||||
|
||||
// Get access token
|
||||
accessToken := tools.AccessToken(header.Get("Authorization"))
|
||||
|
||||
// Check if this specific tool requires the standard authorization header
|
||||
if tool.RequiresClientAuthorization() {
|
||||
if accessToken == "" {
|
||||
@@ -111,10 +116,42 @@ func toolsCallHandler(ctx context.Context, id jsonrpc.RequestId, toolsMap map[st
|
||||
return jsonrpc.NewError(id, jsonrpc.INTERNAL_ERROR, err.Error(), nil), err
|
||||
}
|
||||
|
||||
// Tool authentication
|
||||
// claimsFromAuth maps the name of the authservice to the claims retrieved from it.
|
||||
// Since MCP doesn't support auth, an empty map will be use every time.
|
||||
claimsFromAuth := make(map[string]map[string]any)
|
||||
|
||||
// if using stdio, header will be nil and auth will not be supported
|
||||
if header != nil {
|
||||
for _, aS := range authServices {
|
||||
claims, err := aS.GetClaimsFromHeader(ctx, header)
|
||||
if err != nil {
|
||||
logger.DebugContext(ctx, err.Error())
|
||||
continue
|
||||
}
|
||||
if claims == nil {
|
||||
// authService not present in header
|
||||
continue
|
||||
}
|
||||
claimsFromAuth[aS.GetName()] = claims
|
||||
}
|
||||
}
|
||||
|
||||
// Tool authorization check
|
||||
verifiedAuthServices := make([]string, len(claimsFromAuth))
|
||||
i := 0
|
||||
for k := range claimsFromAuth {
|
||||
verifiedAuthServices[i] = k
|
||||
i++
|
||||
}
|
||||
|
||||
// Check if any of the specified auth services is verified
|
||||
isAuthorized := tool.Authorized(verifiedAuthServices)
|
||||
if !isAuthorized {
|
||||
err = fmt.Errorf("unauthorized Tool call: Please make sure your specify correct auth headers: %w", tools.ErrUnauthorized)
|
||||
return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err
|
||||
}
|
||||
logger.DebugContext(ctx, "tool invocation authorized")
|
||||
|
||||
params, err := tool.ParseParams(data, claimsFromAuth)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("provided parameters were invalid: %w", err)
|
||||
@@ -122,11 +159,6 @@ func toolsCallHandler(ctx context.Context, id jsonrpc.RequestId, toolsMap map[st
|
||||
}
|
||||
logger.DebugContext(ctx, fmt.Sprintf("invocation params: %s", params))
|
||||
|
||||
if !tool.Authorized([]string{}) {
|
||||
err = fmt.Errorf("unauthorized Tool call: `authRequired` is set for the target Tool but isn't supported through MCP Tool call: %w", tools.ErrUnauthorized)
|
||||
return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err
|
||||
}
|
||||
|
||||
// run tool invocation and generate response.
|
||||
results, err := tool.Invoke(ctx, params, accessToken)
|
||||
if err != nil {
|
||||
|
||||
@@ -20,22 +20,24 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/googleapis/genai-toolbox/internal/auth"
|
||||
"github.com/googleapis/genai-toolbox/internal/server/mcp/jsonrpc"
|
||||
"github.com/googleapis/genai-toolbox/internal/tools"
|
||||
"github.com/googleapis/genai-toolbox/internal/util"
|
||||
)
|
||||
|
||||
// ProcessMethod returns a response for the request.
|
||||
func ProcessMethod(ctx context.Context, id jsonrpc.RequestId, method string, toolset tools.Toolset, tools map[string]tools.Tool, body []byte, accessToken tools.AccessToken) (any, error) {
|
||||
func ProcessMethod(ctx context.Context, id jsonrpc.RequestId, method string, toolset tools.Toolset, tools map[string]tools.Tool, authServices map[string]auth.AuthService, body []byte, header http.Header) (any, error) {
|
||||
switch method {
|
||||
case PING:
|
||||
return pingHandler(id)
|
||||
case TOOLS_LIST:
|
||||
return toolsListHandler(id, toolset, body)
|
||||
case TOOLS_CALL:
|
||||
return toolsCallHandler(ctx, id, tools, body, accessToken)
|
||||
return toolsCallHandler(ctx, id, tools, authServices, body, header)
|
||||
default:
|
||||
err := fmt.Errorf("invalid method %s", method)
|
||||
return jsonrpc.NewError(id, jsonrpc.METHOD_NOT_FOUND, err.Error(), nil), err
|
||||
@@ -69,7 +71,7 @@ func toolsListHandler(id jsonrpc.RequestId, toolset tools.Toolset, body []byte)
|
||||
}
|
||||
|
||||
// toolsCallHandler generate a response for tools call.
|
||||
func toolsCallHandler(ctx context.Context, id jsonrpc.RequestId, toolsMap map[string]tools.Tool, body []byte, accessToken tools.AccessToken) (any, error) {
|
||||
func toolsCallHandler(ctx context.Context, id jsonrpc.RequestId, toolsMap map[string]tools.Tool, authServices map[string]auth.AuthService, body []byte, header http.Header) (any, error) {
|
||||
// retrieve logger from context
|
||||
logger, err := util.LoggerFromContext(ctx)
|
||||
if err != nil {
|
||||
@@ -91,6 +93,9 @@ func toolsCallHandler(ctx context.Context, id jsonrpc.RequestId, toolsMap map[st
|
||||
return jsonrpc.NewError(id, jsonrpc.INVALID_PARAMS, err.Error(), nil), err
|
||||
}
|
||||
|
||||
// Get access token
|
||||
accessToken := tools.AccessToken(header.Get("Authorization"))
|
||||
|
||||
// Check if this specific tool requires the standard authorization header
|
||||
if tool.RequiresClientAuthorization() {
|
||||
if accessToken == "" {
|
||||
@@ -111,10 +116,42 @@ func toolsCallHandler(ctx context.Context, id jsonrpc.RequestId, toolsMap map[st
|
||||
return jsonrpc.NewError(id, jsonrpc.INTERNAL_ERROR, err.Error(), nil), err
|
||||
}
|
||||
|
||||
// Tool authentication
|
||||
// claimsFromAuth maps the name of the authservice to the claims retrieved from it.
|
||||
// Since MCP doesn't support auth, an empty map will be use every time.
|
||||
claimsFromAuth := make(map[string]map[string]any)
|
||||
|
||||
// if using stdio, header will be nil and auth will not be supported
|
||||
if header != nil {
|
||||
for _, aS := range authServices {
|
||||
claims, err := aS.GetClaimsFromHeader(ctx, header)
|
||||
if err != nil {
|
||||
logger.DebugContext(ctx, err.Error())
|
||||
continue
|
||||
}
|
||||
if claims == nil {
|
||||
// authService not present in header
|
||||
continue
|
||||
}
|
||||
claimsFromAuth[aS.GetName()] = claims
|
||||
}
|
||||
}
|
||||
|
||||
// Tool authorization check
|
||||
verifiedAuthServices := make([]string, len(claimsFromAuth))
|
||||
i := 0
|
||||
for k := range claimsFromAuth {
|
||||
verifiedAuthServices[i] = k
|
||||
i++
|
||||
}
|
||||
|
||||
// Check if any of the specified auth services is verified
|
||||
isAuthorized := tool.Authorized(verifiedAuthServices)
|
||||
if !isAuthorized {
|
||||
err = fmt.Errorf("unauthorized Tool call: Please make sure your specify correct auth headers: %w", tools.ErrUnauthorized)
|
||||
return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err
|
||||
}
|
||||
logger.DebugContext(ctx, "tool invocation authorized")
|
||||
|
||||
params, err := tool.ParseParams(data, claimsFromAuth)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("provided parameters were invalid: %w", err)
|
||||
@@ -122,11 +159,6 @@ func toolsCallHandler(ctx context.Context, id jsonrpc.RequestId, toolsMap map[st
|
||||
}
|
||||
logger.DebugContext(ctx, fmt.Sprintf("invocation params: %s", params))
|
||||
|
||||
if !tool.Authorized([]string{}) {
|
||||
err = fmt.Errorf("unauthorized Tool call: `authRequired` is set for the target Tool but isn't supported through MCP Tool call: %w", tools.ErrUnauthorized)
|
||||
return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err
|
||||
}
|
||||
|
||||
// run tool invocation and generate response.
|
||||
results, err := tool.Invoke(ctx, params, accessToken)
|
||||
if err != nil {
|
||||
|
||||
@@ -39,7 +39,7 @@ const protocolVersion20250326 = "2025-03-26"
|
||||
const protocolVersion20250618 = "2025-06-18"
|
||||
const serverName = "Toolbox"
|
||||
|
||||
var tool1InputSchema = map[string]any{
|
||||
var basicInputSchema = map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{},
|
||||
"required": []any{},
|
||||
@@ -67,7 +67,7 @@ var tool3InputSchema = map[string]any{
|
||||
}
|
||||
|
||||
func TestMcpEndpointWithoutInitialized(t *testing.T) {
|
||||
mockTools := []MockTool{tool1, tool2, tool3}
|
||||
mockTools := []MockTool{tool1, tool2, tool3, tool4, tool5}
|
||||
toolsMap, toolsets := setUpResources(t, mockTools)
|
||||
r, shutdown := setUpServer(t, "mcp", toolsMap, toolsets)
|
||||
defer shutdown()
|
||||
@@ -116,7 +116,7 @@ func TestMcpEndpointWithoutInitialized(t *testing.T) {
|
||||
"tools": []any{
|
||||
map[string]any{
|
||||
"name": "no_params",
|
||||
"inputSchema": tool1InputSchema,
|
||||
"inputSchema": basicInputSchema,
|
||||
},
|
||||
map[string]any{
|
||||
"name": "some_params",
|
||||
@@ -127,6 +127,14 @@ func TestMcpEndpointWithoutInitialized(t *testing.T) {
|
||||
"description": "some description",
|
||||
"inputSchema": tool3InputSchema,
|
||||
},
|
||||
map[string]any{
|
||||
"name": "unauthorized_tool",
|
||||
"inputSchema": basicInputSchema,
|
||||
},
|
||||
map[string]any{
|
||||
"name": "require_client_auth_tool",
|
||||
"inputSchema": basicInputSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -169,6 +177,76 @@ func TestMcpEndpointWithoutInitialized(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "call tool1 unauthorized tool",
|
||||
url: "/",
|
||||
body: jsonrpc.JSONRPCRequest{
|
||||
Jsonrpc: jsonrpcVersion,
|
||||
Id: "tools-call-tool1",
|
||||
Request: jsonrpc.Request{
|
||||
Method: "tools/call",
|
||||
},
|
||||
Params: map[string]any{
|
||||
"name": "no_params",
|
||||
},
|
||||
},
|
||||
want: map[string]any{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "tools-call-tool1",
|
||||
"result": map[string]any{
|
||||
"content": []any{
|
||||
map[string]any{
|
||||
"type": "text",
|
||||
"text": `"no_params"`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "call tool4 unauthorized tool",
|
||||
url: "/",
|
||||
body: jsonrpc.JSONRPCRequest{
|
||||
Jsonrpc: jsonrpcVersion,
|
||||
Id: "tools-call-tool4",
|
||||
Request: jsonrpc.Request{
|
||||
Method: "tools/call",
|
||||
},
|
||||
Params: map[string]any{
|
||||
"name": "unauthorized_tool",
|
||||
},
|
||||
},
|
||||
want: map[string]any{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "tools-call-tool4",
|
||||
"error": map[string]any{
|
||||
"code": -32600.0,
|
||||
"message": "unauthorized Tool call: Please make sure your specify correct auth headers: unauthorized",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "call tool5 unauthorized tool",
|
||||
url: "/",
|
||||
body: jsonrpc.JSONRPCRequest{
|
||||
Jsonrpc: jsonrpcVersion,
|
||||
Id: "tools-call-tool5",
|
||||
Request: jsonrpc.Request{
|
||||
Method: "tools/call",
|
||||
},
|
||||
Params: map[string]any{
|
||||
"name": "require_client_auth_tool",
|
||||
},
|
||||
},
|
||||
want: map[string]any{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "tools-call-tool5",
|
||||
"error": map[string]any{
|
||||
"code": -32600.0,
|
||||
"message": "missing access token in the 'Authorization' header",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
@@ -258,7 +336,7 @@ func runInitializeLifecycle(t *testing.T, ts *httptest.Server, protocolVersion s
|
||||
}
|
||||
|
||||
func TestMcpEndpoint(t *testing.T) {
|
||||
mockTools := []MockTool{tool1, tool2, tool3}
|
||||
mockTools := []MockTool{tool1, tool2, tool3, tool4, tool5}
|
||||
toolsMap, toolsets := setUpResources(t, mockTools)
|
||||
r, shutdown := setUpServer(t, "mcp", toolsMap, toolsets)
|
||||
defer shutdown()
|
||||
@@ -334,11 +412,12 @@ func TestMcpEndpoint(t *testing.T) {
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
url string
|
||||
isErr bool
|
||||
body any
|
||||
want map[string]any
|
||||
name string
|
||||
url string
|
||||
isErr bool
|
||||
body any
|
||||
wantStatusCode int
|
||||
want map[string]any
|
||||
}{
|
||||
{
|
||||
name: "basic notification",
|
||||
@@ -349,6 +428,7 @@ func TestMcpEndpoint(t *testing.T) {
|
||||
Method: "notification",
|
||||
},
|
||||
},
|
||||
wantStatusCode: http.StatusAccepted,
|
||||
},
|
||||
{
|
||||
name: "ping",
|
||||
@@ -360,6 +440,7 @@ func TestMcpEndpoint(t *testing.T) {
|
||||
Method: "ping",
|
||||
},
|
||||
},
|
||||
wantStatusCode: http.StatusOK,
|
||||
want: map[string]any{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "ping-test-123",
|
||||
@@ -376,6 +457,7 @@ func TestMcpEndpoint(t *testing.T) {
|
||||
Method: "tools/list",
|
||||
},
|
||||
},
|
||||
wantStatusCode: http.StatusOK,
|
||||
want: map[string]any{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "tools-list",
|
||||
@@ -383,7 +465,7 @@ func TestMcpEndpoint(t *testing.T) {
|
||||
"tools": []any{
|
||||
map[string]any{
|
||||
"name": "no_params",
|
||||
"inputSchema": tool1InputSchema,
|
||||
"inputSchema": basicInputSchema,
|
||||
},
|
||||
map[string]any{
|
||||
"name": "some_params",
|
||||
@@ -394,6 +476,14 @@ func TestMcpEndpoint(t *testing.T) {
|
||||
"description": "some description",
|
||||
"inputSchema": tool3InputSchema,
|
||||
},
|
||||
map[string]any{
|
||||
"name": "unauthorized_tool",
|
||||
"inputSchema": basicInputSchema,
|
||||
},
|
||||
map[string]any{
|
||||
"name": "require_client_auth_tool",
|
||||
"inputSchema": basicInputSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -408,6 +498,7 @@ func TestMcpEndpoint(t *testing.T) {
|
||||
Method: "tools/list",
|
||||
},
|
||||
},
|
||||
wantStatusCode: http.StatusOK,
|
||||
want: map[string]any{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "tools-list-tool1",
|
||||
@@ -415,7 +506,7 @@ func TestMcpEndpoint(t *testing.T) {
|
||||
"tools": []any{
|
||||
map[string]any{
|
||||
"name": "no_params",
|
||||
"inputSchema": tool1InputSchema,
|
||||
"inputSchema": basicInputSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -432,6 +523,7 @@ func TestMcpEndpoint(t *testing.T) {
|
||||
Method: "tools/list",
|
||||
},
|
||||
},
|
||||
wantStatusCode: http.StatusOK,
|
||||
want: map[string]any{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "tools-list-invalid-toolset",
|
||||
@@ -450,6 +542,7 @@ func TestMcpEndpoint(t *testing.T) {
|
||||
Id: "missing-method",
|
||||
Request: jsonrpc.Request{},
|
||||
},
|
||||
wantStatusCode: http.StatusOK,
|
||||
want: map[string]any{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "missing-method",
|
||||
@@ -470,6 +563,7 @@ func TestMcpEndpoint(t *testing.T) {
|
||||
Method: "foo",
|
||||
},
|
||||
},
|
||||
wantStatusCode: http.StatusOK,
|
||||
want: map[string]any{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "invalid-method",
|
||||
@@ -490,6 +584,7 @@ func TestMcpEndpoint(t *testing.T) {
|
||||
Method: "foo",
|
||||
},
|
||||
},
|
||||
wantStatusCode: http.StatusOK,
|
||||
want: map[string]any{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "invalid-jsonrpc-version",
|
||||
@@ -519,6 +614,7 @@ func TestMcpEndpoint(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
wantStatusCode: http.StatusOK,
|
||||
want: map[string]any{
|
||||
"jsonrpc": "2.0",
|
||||
"error": map[string]any{
|
||||
@@ -527,6 +623,79 @@ func TestMcpEndpoint(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "call tool1 unauthorized tool",
|
||||
url: "/",
|
||||
body: jsonrpc.JSONRPCRequest{
|
||||
Jsonrpc: jsonrpcVersion,
|
||||
Id: "tools-call-tool1",
|
||||
Request: jsonrpc.Request{
|
||||
Method: "tools/call",
|
||||
},
|
||||
Params: map[string]any{
|
||||
"name": "no_params",
|
||||
},
|
||||
},
|
||||
wantStatusCode: http.StatusOK,
|
||||
want: map[string]any{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "tools-call-tool1",
|
||||
"result": map[string]any{
|
||||
"content": []any{
|
||||
map[string]any{
|
||||
"type": "text",
|
||||
"text": `"no_params"`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "call tool4 unauthorized tool",
|
||||
url: "/",
|
||||
body: jsonrpc.JSONRPCRequest{
|
||||
Jsonrpc: jsonrpcVersion,
|
||||
Id: "tools-call-tool4",
|
||||
Request: jsonrpc.Request{
|
||||
Method: "tools/call",
|
||||
},
|
||||
Params: map[string]any{
|
||||
"name": "unauthorized_tool",
|
||||
},
|
||||
},
|
||||
wantStatusCode: http.StatusUnauthorized,
|
||||
want: map[string]any{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "tools-call-tool4",
|
||||
"error": map[string]any{
|
||||
"code": -32600.0,
|
||||
"message": "unauthorized Tool call: Please make sure your specify correct auth headers: unauthorized",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "call tool5 unauthorized tool",
|
||||
url: "/",
|
||||
body: jsonrpc.JSONRPCRequest{
|
||||
Jsonrpc: jsonrpcVersion,
|
||||
Id: "tools-call-tool5",
|
||||
Request: jsonrpc.Request{
|
||||
Method: "tools/call",
|
||||
},
|
||||
Params: map[string]any{
|
||||
"name": "require_client_auth_tool",
|
||||
},
|
||||
},
|
||||
wantStatusCode: http.StatusUnauthorized,
|
||||
want: map[string]any{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "tools-call-tool5",
|
||||
"error": map[string]any{
|
||||
"code": -32600.0,
|
||||
"message": "missing access token in the 'Authorization' header",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
@@ -540,10 +709,15 @@ func TestMcpEndpoint(t *testing.T) {
|
||||
}
|
||||
|
||||
resp, body, err := runRequest(ts, http.MethodPost, tc.url, bytes.NewBuffer(reqMarshal), header)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error during request: %s", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != tc.wantStatusCode {
|
||||
t.Errorf("StatusCode mismatch: got %d, want %d", resp.StatusCode, tc.wantStatusCode)
|
||||
}
|
||||
|
||||
// Notifications don't expect a response.
|
||||
if tc.want != nil {
|
||||
if contentType := resp.Header.Get("Content-type"); contentType != "application/json" {
|
||||
|
||||
@@ -53,10 +53,11 @@ func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (sources
|
||||
|
||||
type Config struct {
|
||||
// BigQuery configs
|
||||
Name string `yaml:"name" validate:"required"`
|
||||
Kind string `yaml:"kind" validate:"required"`
|
||||
Project string `yaml:"project" validate:"required"`
|
||||
Location string `yaml:"location"`
|
||||
Name string `yaml:"name" validate:"required"`
|
||||
Kind string `yaml:"kind" validate:"required"`
|
||||
Project string `yaml:"project" validate:"required"`
|
||||
Location string `yaml:"location"`
|
||||
UseClientOAuth bool `yaml:"useClientOAuth"`
|
||||
}
|
||||
|
||||
func (r Config) SourceConfigKind() string {
|
||||
@@ -65,10 +66,23 @@ func (r Config) SourceConfigKind() string {
|
||||
}
|
||||
|
||||
func (r Config) Initialize(ctx context.Context, tracer trace.Tracer) (sources.Source, error) {
|
||||
// Initializes a BigQuery Google SQL source
|
||||
client, restService, tokenSource, clientCreator, err := initBigQueryConnection(ctx, tracer, r.Name, r.Project, r.Location)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
var client *bigqueryapi.Client
|
||||
var restService *bigqueryrestapi.Service
|
||||
var tokenSource oauth2.TokenSource
|
||||
var clientCreator BigqueryClientCreator
|
||||
var err error
|
||||
|
||||
if r.UseClientOAuth {
|
||||
clientCreator, err = newBigQueryClientCreator(ctx, tracer, r.Project, r.Location, r.Name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error constructing client creator: %w", err)
|
||||
}
|
||||
} else {
|
||||
// Initializes a BigQuery Google SQL source
|
||||
client, restService, tokenSource, err = initBigQueryConnection(ctx, tracer, r.Name, r.Project, r.Location)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating client from ADC: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
s := &Source{
|
||||
@@ -79,6 +93,7 @@ func (r Config) Initialize(ctx context.Context, tracer trace.Tracer) (sources.So
|
||||
TokenSource: tokenSource,
|
||||
MaxQueryResultRows: 50,
|
||||
ClientCreator: clientCreator,
|
||||
UseClientOAuth: r.UseClientOAuth,
|
||||
}
|
||||
return s, nil
|
||||
|
||||
@@ -95,6 +110,7 @@ type Source struct {
|
||||
TokenSource oauth2.TokenSource
|
||||
MaxQueryResultRows int
|
||||
ClientCreator BigqueryClientCreator
|
||||
UseClientOAuth bool
|
||||
}
|
||||
|
||||
func (s *Source) SourceKind() string {
|
||||
@@ -110,6 +126,10 @@ func (s *Source) BigQueryRestService() *bigqueryrestapi.Service {
|
||||
return s.RestService
|
||||
}
|
||||
|
||||
func (s *Source) UseClientAuthorization() bool {
|
||||
return s.UseClientOAuth
|
||||
}
|
||||
|
||||
func (s *Source) BigQueryTokenSource() oauth2.TokenSource {
|
||||
return s.TokenSource
|
||||
}
|
||||
@@ -128,46 +148,49 @@ func initBigQueryConnection(
|
||||
name string,
|
||||
project string,
|
||||
location string,
|
||||
) (*bigqueryapi.Client, *bigqueryrestapi.Service, oauth2.TokenSource, BigqueryClientCreator, error) {
|
||||
) (*bigqueryapi.Client, *bigqueryrestapi.Service, oauth2.TokenSource, error) {
|
||||
ctx, span := sources.InitConnectionSpan(ctx, tracer, SourceKind, name)
|
||||
defer span.End()
|
||||
|
||||
cred, err := google.FindDefaultCredentials(ctx, bigqueryapi.Scope)
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, fmt.Errorf("failed to find default Google Cloud credentials with scope %q: %w", bigqueryapi.Scope, err)
|
||||
return nil, nil, nil, fmt.Errorf("failed to find default Google Cloud credentials with scope %q: %w", bigqueryapi.Scope, err)
|
||||
}
|
||||
|
||||
userAgent, err := util.UserAgentFromContext(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, err
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
// Initialize the high-level BigQuery client
|
||||
client, err := bigqueryapi.NewClient(ctx, project, option.WithUserAgent(userAgent), option.WithCredentials(cred))
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, fmt.Errorf("failed to create BigQuery client for project %q: %w", project, err)
|
||||
return nil, nil, nil, fmt.Errorf("failed to create BigQuery client for project %q: %w", project, err)
|
||||
}
|
||||
client.Location = location
|
||||
|
||||
// Initialize the low-level BigQuery REST service using the same credentials
|
||||
restService, err := bigqueryrestapi.NewService(ctx, option.WithUserAgent(userAgent), option.WithCredentials(cred))
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, fmt.Errorf("failed to create BigQuery v2 service: %w", err)
|
||||
return nil, nil, nil, fmt.Errorf("failed to create BigQuery v2 service: %w", err)
|
||||
}
|
||||
|
||||
clientCreator := newBigQueryClientCreator(ctx, project, location, userAgent)
|
||||
return client, restService, cred.TokenSource, clientCreator, nil
|
||||
return client, restService, cred.TokenSource, nil
|
||||
}
|
||||
|
||||
// initBigQueryConnectionWithOAuthToken initialize a BigQuery client with an
|
||||
// OAuth access token.
|
||||
func initBigQueryConnectionWithOAuthToken(
|
||||
ctx context.Context,
|
||||
tracer trace.Tracer,
|
||||
project string,
|
||||
location string,
|
||||
name string,
|
||||
userAgent string,
|
||||
tokenString tools.AccessToken,
|
||||
) (*bigqueryapi.Client, *bigqueryrestapi.Service, error) {
|
||||
ctx, span := sources.InitConnectionSpan(ctx, tracer, SourceKind, name)
|
||||
defer span.End()
|
||||
// Construct token source
|
||||
token := &oauth2.Token{
|
||||
AccessToken: string(tokenString),
|
||||
@@ -195,11 +218,17 @@ func initBigQueryConnectionWithOAuthToken(
|
||||
// create a BQ client.
|
||||
func newBigQueryClientCreator(
|
||||
ctx context.Context,
|
||||
tracer trace.Tracer,
|
||||
project string,
|
||||
location string,
|
||||
userAgent string,
|
||||
) func(tools.AccessToken) (*bigqueryapi.Client, *bigqueryrestapi.Service, error) {
|
||||
return func(tokenString tools.AccessToken) (*bigqueryapi.Client, *bigqueryrestapi.Service, error) {
|
||||
return initBigQueryConnectionWithOAuthToken(ctx, project, location, userAgent, tokenString)
|
||||
name string,
|
||||
) (func(tools.AccessToken) (*bigqueryapi.Client, *bigqueryrestapi.Service, error), error) {
|
||||
userAgent, err := util.UserAgentFromContext(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return func(tokenString tools.AccessToken) (*bigqueryapi.Client, *bigqueryrestapi.Service, error) {
|
||||
return initBigQueryConnectionWithOAuthToken(ctx, tracer, project, location, name, userAgent, tokenString)
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -41,10 +41,31 @@ func TestParseFromYamlBigQuery(t *testing.T) {
|
||||
`,
|
||||
want: server.SourceConfigs{
|
||||
"my-instance": bigquery.Config{
|
||||
Name: "my-instance",
|
||||
Kind: bigquery.SourceKind,
|
||||
Project: "my-project",
|
||||
Location: "us",
|
||||
Name: "my-instance",
|
||||
Kind: bigquery.SourceKind,
|
||||
Project: "my-project",
|
||||
Location: "us",
|
||||
UseClientOAuth: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "use client auth example",
|
||||
in: `
|
||||
sources:
|
||||
my-instance:
|
||||
kind: bigquery
|
||||
project: my-project
|
||||
location: us
|
||||
useClientOAuth: true
|
||||
`,
|
||||
want: server.SourceConfigs{
|
||||
"my-instance": bigquery.Config{
|
||||
Name: "my-instance",
|
||||
Kind: bigquery.SourceKind,
|
||||
Project: "my-project",
|
||||
Location: "us",
|
||||
UseClientOAuth: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
145
internal/sources/clickhouse/clickhouse.go
Normal file
145
internal/sources/clickhouse/clickhouse.go
Normal file
@@ -0,0 +1,145 @@
|
||||
// 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 clickhouse
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
_ "github.com/ClickHouse/clickhouse-go/v2"
|
||||
"github.com/goccy/go-yaml"
|
||||
"github.com/googleapis/genai-toolbox/internal/sources"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
const SourceKind string = "clickhouse"
|
||||
|
||||
// validate interface
|
||||
var _ sources.SourceConfig = Config{}
|
||||
|
||||
func init() {
|
||||
if !sources.Register(SourceKind, newConfig) {
|
||||
panic(fmt.Sprintf("source kind %q already registered", SourceKind))
|
||||
}
|
||||
}
|
||||
|
||||
func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (sources.SourceConfig, 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"`
|
||||
Host string `yaml:"host" validate:"required"`
|
||||
Port string `yaml:"port" validate:"required"`
|
||||
Database string `yaml:"database" validate:"required"`
|
||||
User string `yaml:"user" validate:"required"`
|
||||
Password string `yaml:"password"`
|
||||
Protocol string `yaml:"protocol"`
|
||||
Secure bool `yaml:"secure"`
|
||||
}
|
||||
|
||||
func (r Config) SourceConfigKind() string {
|
||||
return SourceKind
|
||||
}
|
||||
|
||||
func (r Config) Initialize(ctx context.Context, tracer trace.Tracer) (sources.Source, error) {
|
||||
pool, err := initClickHouseConnectionPool(ctx, tracer, r.Name, r.Host, r.Port, r.User, r.Password, r.Database, r.Protocol, r.Secure)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to create pool: %w", err)
|
||||
}
|
||||
|
||||
err = pool.PingContext(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to connect successfully: %w", err)
|
||||
}
|
||||
|
||||
s := &Source{
|
||||
Name: r.Name,
|
||||
Kind: SourceKind,
|
||||
Pool: pool,
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
var _ sources.Source = &Source{}
|
||||
|
||||
type Source struct {
|
||||
Name string `yaml:"name"`
|
||||
Kind string `yaml:"kind"`
|
||||
Pool *sql.DB
|
||||
}
|
||||
|
||||
func (s *Source) SourceKind() string {
|
||||
return SourceKind
|
||||
}
|
||||
|
||||
func (s *Source) ClickHousePool() *sql.DB {
|
||||
return s.Pool
|
||||
}
|
||||
|
||||
func validateConfig(protocol string) error {
|
||||
validProtocols := map[string]bool{"http": true, "https": true}
|
||||
|
||||
if protocol != "" && !validProtocols[protocol] {
|
||||
return fmt.Errorf("invalid protocol: %s, must be one of: http, https", protocol)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func initClickHouseConnectionPool(ctx context.Context, tracer trace.Tracer, name, host, port, user, pass, dbname, protocol string, secure bool) (*sql.DB, error) {
|
||||
//nolint:all // Reassigned ctx
|
||||
ctx, span := sources.InitConnectionSpan(ctx, tracer, SourceKind, name)
|
||||
defer span.End()
|
||||
|
||||
if protocol == "" {
|
||||
protocol = "https"
|
||||
}
|
||||
|
||||
if err := validateConfig(protocol); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
encodedUser := url.QueryEscape(user)
|
||||
encodedPass := url.QueryEscape(pass)
|
||||
|
||||
var dsn string
|
||||
scheme := protocol
|
||||
if protocol == "http" && secure {
|
||||
scheme = "https"
|
||||
}
|
||||
dsn = fmt.Sprintf("%s://%s:%s@%s:%s/%s", scheme, encodedUser, encodedPass, host, port, dbname)
|
||||
if scheme == "https" {
|
||||
dsn += "?secure=true&skip_verify=false"
|
||||
}
|
||||
|
||||
pool, err := sql.Open("clickhouse", dsn)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("sql.Open: %w", err)
|
||||
}
|
||||
|
||||
pool.SetMaxOpenConns(25)
|
||||
pool.SetMaxIdleConns(5)
|
||||
pool.SetConnMaxLifetime(5 * time.Minute)
|
||||
|
||||
return pool, nil
|
||||
}
|
||||
348
internal/sources/clickhouse/clickhouse_test.go
Normal file
348
internal/sources/clickhouse/clickhouse_test.go
Normal file
@@ -0,0 +1,348 @@
|
||||
// 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 clickhouse
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/goccy/go-yaml"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/googleapis/genai-toolbox/internal/testutils"
|
||||
"go.opentelemetry.io/otel"
|
||||
)
|
||||
|
||||
func TestConfigSourceConfigKind(t *testing.T) {
|
||||
config := Config{}
|
||||
if config.SourceConfigKind() != SourceKind {
|
||||
t.Errorf("Expected %s, got %s", SourceKind, config.SourceConfigKind())
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewConfig(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
yaml string
|
||||
expected Config
|
||||
}{
|
||||
{
|
||||
name: "all fields specified",
|
||||
yaml: `
|
||||
name: test-clickhouse
|
||||
kind: clickhouse
|
||||
host: localhost
|
||||
port: "8443"
|
||||
user: default
|
||||
password: "mypass"
|
||||
database: mydb
|
||||
protocol: https
|
||||
secure: true
|
||||
`,
|
||||
expected: Config{
|
||||
Name: "test-clickhouse",
|
||||
Kind: "clickhouse",
|
||||
Host: "localhost",
|
||||
Port: "8443",
|
||||
User: "default",
|
||||
Password: "mypass",
|
||||
Database: "mydb",
|
||||
Protocol: "https",
|
||||
Secure: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "minimal configuration with defaults",
|
||||
yaml: `
|
||||
name: minimal-clickhouse
|
||||
kind: clickhouse
|
||||
host: 127.0.0.1
|
||||
port: "8123"
|
||||
user: testuser
|
||||
database: testdb
|
||||
`,
|
||||
expected: Config{
|
||||
Name: "minimal-clickhouse",
|
||||
Kind: "clickhouse",
|
||||
Host: "127.0.0.1",
|
||||
Port: "8123",
|
||||
User: "testuser",
|
||||
Password: "",
|
||||
Database: "testdb",
|
||||
Protocol: "",
|
||||
Secure: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "http protocol",
|
||||
yaml: `
|
||||
name: http-clickhouse
|
||||
kind: clickhouse
|
||||
host: clickhouse.example.com
|
||||
port: "8123"
|
||||
user: analytics
|
||||
password: "securepass"
|
||||
database: analytics_db
|
||||
protocol: http
|
||||
secure: false
|
||||
`,
|
||||
expected: Config{
|
||||
Name: "http-clickhouse",
|
||||
Kind: "clickhouse",
|
||||
Host: "clickhouse.example.com",
|
||||
Port: "8123",
|
||||
User: "analytics",
|
||||
Password: "securepass",
|
||||
Database: "analytics_db",
|
||||
Protocol: "http",
|
||||
Secure: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "https with secure connection",
|
||||
yaml: `
|
||||
name: secure-clickhouse
|
||||
kind: clickhouse
|
||||
host: secure.clickhouse.io
|
||||
port: "8443"
|
||||
user: secureuser
|
||||
password: "verysecure"
|
||||
database: production
|
||||
protocol: https
|
||||
secure: true
|
||||
`,
|
||||
expected: Config{
|
||||
Name: "secure-clickhouse",
|
||||
Kind: "clickhouse",
|
||||
Host: "secure.clickhouse.io",
|
||||
Port: "8443",
|
||||
User: "secureuser",
|
||||
Password: "verysecure",
|
||||
Database: "production",
|
||||
Protocol: "https",
|
||||
Secure: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
decoder := yaml.NewDecoder(strings.NewReader(string(testutils.FormatYaml(tt.yaml))))
|
||||
config, err := newConfig(context.Background(), tt.expected.Name, decoder)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create config: %v", err)
|
||||
}
|
||||
|
||||
clickhouseConfig, ok := config.(Config)
|
||||
if !ok {
|
||||
t.Fatalf("Expected Config type, got %T", config)
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(tt.expected, clickhouseConfig); diff != "" {
|
||||
t.Errorf("Config mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewConfigInvalidYAML(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
yaml string
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "invalid yaml syntax",
|
||||
yaml: `
|
||||
name: test-clickhouse
|
||||
kind: clickhouse
|
||||
host: [invalid
|
||||
`,
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "missing required fields",
|
||||
yaml: `
|
||||
name: test-clickhouse
|
||||
kind: clickhouse
|
||||
`,
|
||||
expectError: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
decoder := yaml.NewDecoder(strings.NewReader(string(testutils.FormatYaml(tt.yaml))))
|
||||
_, err := newConfig(context.Background(), "test-clickhouse", decoder)
|
||||
if tt.expectError && err == nil {
|
||||
t.Errorf("Expected error but got none")
|
||||
}
|
||||
if !tt.expectError && err != nil {
|
||||
t.Errorf("Expected no error but got: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSource_SourceKind(t *testing.T) {
|
||||
source := &Source{}
|
||||
if source.SourceKind() != SourceKind {
|
||||
t.Errorf("Expected %s, got %s", SourceKind, source.SourceKind())
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateConfig(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
protocol string
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "valid https protocol",
|
||||
protocol: "https",
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "valid http protocol",
|
||||
protocol: "http",
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "invalid protocol",
|
||||
protocol: "invalid",
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "invalid protocol - native not supported",
|
||||
protocol: "native",
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "empty values use defaults",
|
||||
protocol: "",
|
||||
expectError: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := validateConfig(tt.protocol)
|
||||
if tt.expectError && err == nil {
|
||||
t.Errorf("Expected error but got none")
|
||||
}
|
||||
if !tt.expectError && err != nil {
|
||||
t.Errorf("Expected no error but got: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitClickHouseConnectionPoolDSNGeneration(t *testing.T) {
|
||||
tracer := otel.Tracer("test")
|
||||
ctx := context.Background()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
host string
|
||||
port string
|
||||
user string
|
||||
pass string
|
||||
dbname string
|
||||
protocol string
|
||||
secure bool
|
||||
shouldErr bool
|
||||
}{
|
||||
{
|
||||
name: "http protocol with defaults",
|
||||
host: "localhost",
|
||||
port: "8123",
|
||||
user: "default",
|
||||
pass: "",
|
||||
dbname: "default",
|
||||
protocol: "http",
|
||||
secure: false,
|
||||
shouldErr: true,
|
||||
},
|
||||
{
|
||||
name: "https protocol with secure",
|
||||
host: "localhost",
|
||||
port: "8443",
|
||||
user: "default",
|
||||
pass: "",
|
||||
dbname: "default",
|
||||
protocol: "https",
|
||||
secure: true,
|
||||
shouldErr: true,
|
||||
},
|
||||
{
|
||||
name: "special characters in password",
|
||||
host: "localhost",
|
||||
port: "8443",
|
||||
user: "test@user",
|
||||
pass: "pass@word:with/special&chars",
|
||||
dbname: "default",
|
||||
protocol: "https",
|
||||
secure: true,
|
||||
shouldErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid protocol should fail",
|
||||
host: "localhost",
|
||||
port: "9000",
|
||||
user: "default",
|
||||
pass: "",
|
||||
dbname: "default",
|
||||
protocol: "invalid",
|
||||
secure: false,
|
||||
shouldErr: true,
|
||||
},
|
||||
{
|
||||
name: "empty protocol defaults to https",
|
||||
host: "localhost",
|
||||
port: "8443",
|
||||
user: "user",
|
||||
pass: "pass",
|
||||
dbname: "testdb",
|
||||
protocol: "",
|
||||
secure: true,
|
||||
shouldErr: true,
|
||||
},
|
||||
{
|
||||
name: "http with secure flag should upgrade to https",
|
||||
host: "example.com",
|
||||
port: "8443",
|
||||
user: "user",
|
||||
pass: "pass",
|
||||
dbname: "db",
|
||||
protocol: "http",
|
||||
secure: true,
|
||||
shouldErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
pool, err := initClickHouseConnectionPool(ctx, tracer, "test", tt.host, tt.port, tt.user, tt.pass, tt.dbname, tt.protocol, tt.secure)
|
||||
|
||||
if !tt.shouldErr && err != nil {
|
||||
t.Errorf("Expected no error, got: %v", err)
|
||||
}
|
||||
|
||||
if pool != nil {
|
||||
pool.Close()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -43,6 +43,7 @@ func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (sources
|
||||
Name: name,
|
||||
SslVerification: true,
|
||||
Timeout: "600s",
|
||||
UseClientOAuth: false,
|
||||
ShowHiddenModels: true,
|
||||
ShowHiddenExplores: true,
|
||||
ShowHiddenFields: true,
|
||||
@@ -60,6 +61,7 @@ type Config struct {
|
||||
ClientId string `yaml:"client_id" validate:"required"`
|
||||
ClientSecret string `yaml:"client_secret" validate:"required"`
|
||||
SslVerification bool `yaml:"verify_ssl"`
|
||||
UseClientOAuth bool `yaml:"use_client_oauth"`
|
||||
Timeout string `yaml:"timeout"`
|
||||
ShowHiddenModels bool `yaml:"show_hidden_models"`
|
||||
ShowHiddenExplores bool `yaml:"show_hidden_explores"`
|
||||
@@ -100,23 +102,29 @@ func (r Config) Initialize(ctx context.Context, tracer trace.Tracer) (sources.So
|
||||
ClientSecret: r.ClientSecret,
|
||||
}
|
||||
|
||||
sdk := v4.NewLookerSDK(rtl.NewAuthSession(cfg))
|
||||
me, err := sdk.Me("", &cfg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to log in: %s", err)
|
||||
}
|
||||
logger.DebugContext(ctx, fmt.Sprintf("logged in as user %v %v.\n", *me.FirstName, *me.LastName))
|
||||
|
||||
s := &Source{
|
||||
Name: r.Name,
|
||||
Kind: SourceKind,
|
||||
Timeout: r.Timeout,
|
||||
Client: sdk,
|
||||
UseClientOAuth: r.UseClientOAuth,
|
||||
ApiSettings: &cfg,
|
||||
ShowHiddenModels: r.ShowHiddenModels,
|
||||
ShowHiddenExplores: r.ShowHiddenExplores,
|
||||
ShowHiddenFields: r.ShowHiddenFields,
|
||||
}
|
||||
|
||||
if !r.UseClientOAuth {
|
||||
if r.ClientId == "" || r.ClientSecret == "" {
|
||||
return nil, fmt.Errorf("client_id and client_secret need to be specified")
|
||||
}
|
||||
s.Client = v4.NewLookerSDK(rtl.NewAuthSession(cfg))
|
||||
resp, err := s.Client.Me("", s.ApiSettings)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("incorrect settings: %w", err)
|
||||
}
|
||||
logger.DebugContext(ctx, fmt.Sprintf("logged in as %s %s", *resp.FirstName, *resp.LastName))
|
||||
}
|
||||
|
||||
return s, nil
|
||||
|
||||
}
|
||||
@@ -129,6 +137,7 @@ type Source struct {
|
||||
Timeout string `yaml:"timeout"`
|
||||
Client *v4.LookerSDK
|
||||
ApiSettings *rtl.ApiSettings
|
||||
UseClientOAuth bool `yaml:"use_client_oauth"`
|
||||
ShowHiddenModels bool `yaml:"show_hidden_models"`
|
||||
ShowHiddenExplores bool `yaml:"show_hidden_explores"`
|
||||
ShowHiddenFields bool `yaml:"show_hidden_fields"`
|
||||
|
||||
@@ -50,6 +50,7 @@ func TestParseFromYamlLooker(t *testing.T) {
|
||||
ClientSecret: "sdakl;jgflkasdfkfg",
|
||||
Timeout: "600s",
|
||||
SslVerification: true,
|
||||
UseClientOAuth: false,
|
||||
ShowHiddenModels: true,
|
||||
ShowHiddenExplores: true,
|
||||
ShowHiddenFields: true,
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
@@ -46,14 +47,15 @@ func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (sources
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Name string `yaml:"name" validate:"required"`
|
||||
Kind string `yaml:"kind" validate:"required"`
|
||||
Host string `yaml:"host" validate:"required"`
|
||||
Port string `yaml:"port" validate:"required"`
|
||||
User string `yaml:"user" validate:"required"`
|
||||
Password string `yaml:"password" validate:"required"`
|
||||
Database string `yaml:"database" validate:"required"`
|
||||
QueryTimeout string `yaml:"queryTimeout"`
|
||||
Name string `yaml:"name" validate:"required"`
|
||||
Kind string `yaml:"kind" validate:"required"`
|
||||
Host string `yaml:"host" validate:"required"`
|
||||
Port string `yaml:"port" validate:"required"`
|
||||
User string `yaml:"user" validate:"required"`
|
||||
Password string `yaml:"password" validate:"required"`
|
||||
Database string `yaml:"database" validate:"required"`
|
||||
QueryTimeout string `yaml:"queryTimeout"`
|
||||
QueryParams map[string]string `yaml:"queryParams"`
|
||||
}
|
||||
|
||||
func (r Config) SourceConfigKind() string {
|
||||
@@ -61,7 +63,7 @@ func (r Config) SourceConfigKind() string {
|
||||
}
|
||||
|
||||
func (r Config) Initialize(ctx context.Context, tracer trace.Tracer) (sources.Source, error) {
|
||||
pool, err := initMySQLConnectionPool(ctx, tracer, r.Name, r.Host, r.Port, r.User, r.Password, r.Database, r.QueryTimeout)
|
||||
pool, err := initMySQLConnectionPool(ctx, tracer, r.Name, r.Host, r.Port, r.User, r.Password, r.Database, r.QueryTimeout, r.QueryParams)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to create pool: %w", err)
|
||||
}
|
||||
@@ -95,21 +97,34 @@ func (s *Source) MySQLPool() *sql.DB {
|
||||
return s.Pool
|
||||
}
|
||||
|
||||
func initMySQLConnectionPool(ctx context.Context, tracer trace.Tracer, name, host, port, user, pass, dbname, queryTimeout string) (*sql.DB, error) {
|
||||
func initMySQLConnectionPool(ctx context.Context, tracer trace.Tracer, name, host, port, user, pass, dbname, queryTimeout string, queryParams map[string]string) (*sql.DB, error) {
|
||||
//nolint:all // Reassigned ctx
|
||||
ctx, span := sources.InitConnectionSpan(ctx, tracer, SourceKind, name)
|
||||
defer span.End()
|
||||
|
||||
// Configure the driver to connect to the database
|
||||
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?parseTime=true", user, pass, host, port, dbname)
|
||||
// Build query parameters via url.Values for deterministic order and proper escaping.
|
||||
values := url.Values{}
|
||||
|
||||
// Add query timeout to DSN if specified
|
||||
// Derive readTimeout from queryTimeout when provided.
|
||||
if queryTimeout != "" {
|
||||
timeout, err := time.ParseDuration(queryTimeout)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid queryTimeout %q: %w", queryTimeout, err)
|
||||
}
|
||||
dsn += "&readTimeout=" + timeout.String()
|
||||
values.Set("readTimeout", timeout.String())
|
||||
}
|
||||
|
||||
// Custom user parameters
|
||||
for k, v := range queryParams {
|
||||
if v == "" {
|
||||
continue // skip empty values
|
||||
}
|
||||
values.Set(k, v)
|
||||
}
|
||||
|
||||
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?parseTime=true", user, pass, host, port, dbname)
|
||||
if enc := values.Encode(); enc != "" {
|
||||
dsn += "&" + enc
|
||||
}
|
||||
|
||||
// Interact with the driver directly as you normally would
|
||||
|
||||
@@ -15,10 +15,15 @@
|
||||
package mysql_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
yaml "github.com/goccy/go-yaml"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
"go.opentelemetry.io/otel/trace/noop"
|
||||
|
||||
"github.com/googleapis/genai-toolbox/internal/server"
|
||||
"github.com/googleapis/genai-toolbox/internal/sources/mysql"
|
||||
"github.com/googleapis/genai-toolbox/internal/testutils"
|
||||
@@ -80,9 +85,41 @@ func TestParseFromYamlCloudSQLMySQL(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "with query params",
|
||||
in: `
|
||||
sources:
|
||||
my-mysql-instance:
|
||||
kind: mysql
|
||||
host: 0.0.0.0
|
||||
port: my-port
|
||||
database: my_db
|
||||
user: my_user
|
||||
password: my_pass
|
||||
queryParams:
|
||||
tls: preferred
|
||||
charset: utf8mb4
|
||||
`,
|
||||
want: server.SourceConfigs{
|
||||
"my-mysql-instance": mysql.Config{
|
||||
Name: "my-mysql-instance",
|
||||
Kind: mysql.SourceKind,
|
||||
Host: "0.0.0.0",
|
||||
Port: "my-port",
|
||||
Database: "my_db",
|
||||
User: "my_user",
|
||||
Password: "my_pass",
|
||||
QueryParams: map[string]string{
|
||||
"tls": "preferred",
|
||||
"charset": "utf8mb4",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range tcs {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
got := struct {
|
||||
Sources server.SourceConfigs `yaml:"sources"`
|
||||
}{}
|
||||
@@ -91,8 +128,8 @@ func TestParseFromYamlCloudSQLMySQL(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("unable to unmarshal: %s", err)
|
||||
}
|
||||
if !cmp.Equal(tc.want, got.Sources) {
|
||||
t.Fatalf("incorrect parse: want %v, got %v", tc.want, got.Sources)
|
||||
if diff := cmp.Diff(tc.want, got.Sources, cmpopts.EquateEmpty()); diff != "" {
|
||||
t.Fatalf("mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -118,7 +155,7 @@ func TestFailParseFromYaml(t *testing.T) {
|
||||
password: my_pass
|
||||
foo: bar
|
||||
`,
|
||||
err: "unable to parse source \"my-mysql-instance\" as \"mysql\": [2:1] unknown field \"foo\"\n 1 | database: my_db\n> 2 | foo: bar\n ^\n 3 | host: 0.0.0.0\n 4 | kind: mysql\n 5 | password: my_pass\n 6 | ",
|
||||
err: "unknown field \"foo\"",
|
||||
},
|
||||
{
|
||||
desc: "missing required field",
|
||||
@@ -131,11 +168,27 @@ func TestFailParseFromYaml(t *testing.T) {
|
||||
user: my_user
|
||||
password: my_pass
|
||||
`,
|
||||
err: "unable to parse source \"my-mysql-instance\" as \"mysql\": Key: 'Config.Host' Error:Field validation for 'Host' failed on the 'required' tag",
|
||||
err: "Field validation for 'Host' failed",
|
||||
},
|
||||
{
|
||||
desc: "invalid query params type",
|
||||
in: `
|
||||
sources:
|
||||
my-mysql-instance:
|
||||
kind: mysql
|
||||
host: 0.0.0.0
|
||||
port: 3306
|
||||
database: my_db
|
||||
user: my_user
|
||||
password: my_pass
|
||||
queryParams: not-a-map
|
||||
`,
|
||||
err: "string was used where mapping is expected",
|
||||
},
|
||||
}
|
||||
for _, tc := range tcs {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
got := struct {
|
||||
Sources server.SourceConfigs `yaml:"sources"`
|
||||
}{}
|
||||
@@ -145,9 +198,32 @@ func TestFailParseFromYaml(t *testing.T) {
|
||||
t.Fatalf("expect parsing to fail")
|
||||
}
|
||||
errStr := err.Error()
|
||||
if errStr != tc.err {
|
||||
t.Fatalf("unexpected error: got %q, want %q", errStr, tc.err)
|
||||
if !strings.Contains(errStr, tc.err) {
|
||||
t.Fatalf("unexpected error: got %q, want substring %q", errStr, tc.err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestFailInitialization test error during initialization without attempting a DB connection.
|
||||
func TestFailInitialization(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cfg := mysql.Config{
|
||||
Name: "instance",
|
||||
Kind: "mysql",
|
||||
Host: "localhost",
|
||||
Port: "3306",
|
||||
Database: "db",
|
||||
User: "user",
|
||||
Password: "pass",
|
||||
QueryTimeout: "abc", // invalid duration
|
||||
}
|
||||
_, err := cfg.Initialize(context.Background(), noop.NewTracerProvider().Tracer("test"))
|
||||
if err == nil {
|
||||
t.Fatalf("expected error for invalid queryTimeout, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "invalid queryTimeout") {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +50,7 @@ type compatibleSource interface {
|
||||
BigQueryClient() *bigqueryapi.Client
|
||||
BigQueryRestService() *bigqueryrestapi.Service
|
||||
BigQueryClientCreator() bigqueryds.BigqueryClientCreator
|
||||
UseClientAuthorization() bool
|
||||
}
|
||||
|
||||
// validate compatible sources are still compatible
|
||||
@@ -64,7 +65,6 @@ type Config struct {
|
||||
Description string `yaml:"description" validate:"required"`
|
||||
Statement string `yaml:"statement" validate:"required"`
|
||||
AuthRequired []string `yaml:"authRequired"`
|
||||
UseClientOAuth bool `yaml:"useClientOAuth"`
|
||||
Parameters tools.Parameters `yaml:"parameters"`
|
||||
TemplateParameters tools.Parameters `yaml:"templateParameters"`
|
||||
}
|
||||
@@ -105,17 +105,17 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
Name: cfg.Name,
|
||||
Kind: kind,
|
||||
AuthRequired: cfg.AuthRequired,
|
||||
UseClientOAuth: cfg.UseClientOAuth,
|
||||
Parameters: cfg.Parameters,
|
||||
TemplateParameters: cfg.TemplateParameters,
|
||||
AllParams: allParameters,
|
||||
|
||||
Statement: cfg.Statement,
|
||||
Client: s.BigQueryClient(),
|
||||
RestService: s.BigQueryRestService(),
|
||||
ClientCreator: s.BigQueryClientCreator(),
|
||||
manifest: tools.Manifest{Description: cfg.Description, Parameters: paramManifest, AuthRequired: cfg.AuthRequired},
|
||||
mcpManifest: mcpManifest,
|
||||
Statement: cfg.Statement,
|
||||
UseClientOAuth: s.UseClientAuthorization(),
|
||||
Client: s.BigQueryClient(),
|
||||
RestService: s.BigQueryRestService(),
|
||||
ClientCreator: s.BigQueryClientCreator(),
|
||||
manifest: tools.Manifest{Description: cfg.Description, Parameters: paramManifest, AuthRequired: cfg.AuthRequired},
|
||||
mcpManifest: mcpManifest,
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
@@ -231,9 +231,9 @@ func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken
|
||||
|
||||
query = bqClient.Query(newStatement)
|
||||
query.Parameters = highLevelParams
|
||||
query.Location = t.Client.Location
|
||||
query.Location = bqClient.Location
|
||||
|
||||
dryRunJob, err := dryRunQuery(ctx, restService, t.Client.Project(), t.Client.Location, newStatement, lowLevelParams, query.ConnectionProperties)
|
||||
dryRunJob, err := dryRunQuery(ctx, restService, bqClient.Project(), bqClient.Location, newStatement, lowLevelParams, query.ConnectionProperties)
|
||||
if err != nil {
|
||||
// This is a fallback check in case the switch logic was bypassed.
|
||||
return nil, fmt.Errorf("final query validation failed: %w", err)
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
// 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 clickhouse
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
yaml "github.com/goccy/go-yaml"
|
||||
"github.com/googleapis/genai-toolbox/internal/sources"
|
||||
"github.com/googleapis/genai-toolbox/internal/tools"
|
||||
)
|
||||
|
||||
type compatibleSource interface {
|
||||
ClickHousePool() *sql.DB
|
||||
}
|
||||
|
||||
var compatibleSources = []string{"clickhouse"}
|
||||
|
||||
const executeSQLKind string = "clickhouse-execute-sql"
|
||||
|
||||
func init() {
|
||||
if !tools.Register(executeSQLKind, newExecuteSQLConfig) {
|
||||
panic(fmt.Sprintf("tool kind %q already registered", executeSQLKind))
|
||||
}
|
||||
}
|
||||
|
||||
func newExecuteSQLConfig(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"`
|
||||
}
|
||||
|
||||
var _ tools.ToolConfig = Config{}
|
||||
|
||||
func (cfg Config) ToolConfigKind() string {
|
||||
return executeSQLKind
|
||||
}
|
||||
|
||||
func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) {
|
||||
rawS, ok := srcs[cfg.Source]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("no source named %q configured", cfg.Source)
|
||||
}
|
||||
|
||||
s, ok := rawS.(compatibleSource)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid source for %q tool: source kind must be one of %q", executeSQLKind, compatibleSources)
|
||||
}
|
||||
|
||||
sqlParameter := tools.NewStringParameter("sql", "The SQL statement to execute.")
|
||||
parameters := tools.Parameters{sqlParameter}
|
||||
|
||||
mcpManifest := tools.McpManifest{
|
||||
Name: cfg.Name,
|
||||
Description: cfg.Description,
|
||||
InputSchema: parameters.McpManifest(),
|
||||
}
|
||||
|
||||
t := ExecuteSQLTool{
|
||||
Name: cfg.Name,
|
||||
Kind: executeSQLKind,
|
||||
Parameters: parameters,
|
||||
AuthRequired: cfg.AuthRequired,
|
||||
Pool: s.ClickHousePool(),
|
||||
manifest: tools.Manifest{Description: cfg.Description, Parameters: parameters.Manifest(), AuthRequired: cfg.AuthRequired},
|
||||
mcpManifest: mcpManifest,
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
var _ tools.Tool = ExecuteSQLTool{}
|
||||
|
||||
type ExecuteSQLTool struct {
|
||||
Name string `yaml:"name"`
|
||||
Kind string `yaml:"kind"`
|
||||
AuthRequired []string `yaml:"authRequired"`
|
||||
Parameters tools.Parameters `yaml:"parameters"`
|
||||
|
||||
Pool *sql.DB
|
||||
manifest tools.Manifest
|
||||
mcpManifest tools.McpManifest
|
||||
}
|
||||
|
||||
func (t ExecuteSQLTool) Invoke(ctx context.Context, params tools.ParamValues, token tools.AccessToken) (any, error) {
|
||||
paramsMap := params.AsMap()
|
||||
sql, ok := paramsMap["sql"].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unable to cast sql parameter %s", paramsMap["sql"])
|
||||
}
|
||||
|
||||
results, err := t.Pool.QueryContext(ctx, sql)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to execute query: %w", err)
|
||||
}
|
||||
defer results.Close()
|
||||
|
||||
cols, err := results.Columns()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to retrieve rows column name: %w", err)
|
||||
}
|
||||
|
||||
// create an array of values for each column, which can be re-used to scan each row
|
||||
rawValues := make([]any, len(cols))
|
||||
values := make([]any, len(cols))
|
||||
for i := range rawValues {
|
||||
values[i] = &rawValues[i]
|
||||
}
|
||||
|
||||
colTypes, err := results.ColumnTypes()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to get column types: %w", err)
|
||||
}
|
||||
|
||||
var out []any
|
||||
for results.Next() {
|
||||
err := results.Scan(values...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to parse row: %w", err)
|
||||
}
|
||||
vMap := make(map[string]any)
|
||||
for i, name := range cols {
|
||||
// ClickHouse driver may return specific types that need handling
|
||||
switch colTypes[i].DatabaseTypeName() {
|
||||
case "String", "FixedString":
|
||||
if rawValues[i] != nil {
|
||||
// Handle potential []byte to string conversion if needed
|
||||
if b, ok := rawValues[i].([]byte); ok {
|
||||
vMap[name] = string(b)
|
||||
} else {
|
||||
vMap[name] = rawValues[i]
|
||||
}
|
||||
} else {
|
||||
vMap[name] = nil
|
||||
}
|
||||
default:
|
||||
vMap[name] = rawValues[i]
|
||||
}
|
||||
}
|
||||
out = append(out, vMap)
|
||||
}
|
||||
|
||||
if err := results.Err(); err != nil {
|
||||
return nil, fmt.Errorf("errors encountered by results.Scan: %w", err)
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (t ExecuteSQLTool) ParseParams(data map[string]any, claims map[string]map[string]any) (tools.ParamValues, error) {
|
||||
return tools.ParseParams(t.Parameters, data, claims)
|
||||
}
|
||||
|
||||
func (t ExecuteSQLTool) Manifest() tools.Manifest {
|
||||
return t.manifest
|
||||
}
|
||||
|
||||
func (t ExecuteSQLTool) McpManifest() tools.McpManifest {
|
||||
return t.mcpManifest
|
||||
}
|
||||
|
||||
func (t ExecuteSQLTool) Authorized(verifiedAuthServices []string) bool {
|
||||
return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices)
|
||||
}
|
||||
|
||||
func (t ExecuteSQLTool) RequiresClientAuthorization() bool {
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
// 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 clickhouse
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
yaml "github.com/goccy/go-yaml"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/googleapis/genai-toolbox/internal/server"
|
||||
"github.com/googleapis/genai-toolbox/internal/testutils"
|
||||
)
|
||||
|
||||
func TestParseFromYamlClickHouseExecuteSQL(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: clickhouse-execute-sql
|
||||
source: my-instance
|
||||
description: some description
|
||||
`,
|
||||
want: server.ToolConfigs{
|
||||
"example_tool": Config{
|
||||
Name: "example_tool",
|
||||
Kind: "clickhouse-execute-sql",
|
||||
Source: "my-instance",
|
||||
Description: "some description",
|
||||
AuthRequired: []string{},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range tcs {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
got := struct {
|
||||
Tools server.ToolConfigs `yaml:"tools"`
|
||||
}{}
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
207
internal/tools/clickhouse/clickhousesql/clickhousesql.go
Normal file
207
internal/tools/clickhouse/clickhousesql/clickhousesql.go
Normal file
@@ -0,0 +1,207 @@
|
||||
// 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 clickhouse
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
yaml "github.com/goccy/go-yaml"
|
||||
"github.com/googleapis/genai-toolbox/internal/sources"
|
||||
"github.com/googleapis/genai-toolbox/internal/tools"
|
||||
)
|
||||
|
||||
type compatibleSource interface {
|
||||
ClickHousePool() *sql.DB
|
||||
}
|
||||
|
||||
var compatibleSources = []string{"clickhouse"}
|
||||
|
||||
const sqlKind string = "clickhouse-sql"
|
||||
|
||||
func init() {
|
||||
if !tools.Register(sqlKind, newSQLConfig) {
|
||||
panic(fmt.Sprintf("tool kind %q already registered", sqlKind))
|
||||
}
|
||||
}
|
||||
|
||||
func newSQLConfig(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"`
|
||||
Statement string `yaml:"statement" validate:"required"`
|
||||
AuthRequired []string `yaml:"authRequired"`
|
||||
Parameters tools.Parameters `yaml:"parameters"`
|
||||
TemplateParameters tools.Parameters `yaml:"templateParameters"`
|
||||
}
|
||||
|
||||
var _ tools.ToolConfig = Config{}
|
||||
|
||||
func (cfg Config) ToolConfigKind() string {
|
||||
return sqlKind
|
||||
}
|
||||
|
||||
func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) {
|
||||
rawS, ok := srcs[cfg.Source]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("no source named %q configured", cfg.Source)
|
||||
}
|
||||
|
||||
s, ok := rawS.(compatibleSource)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid source for %q tool: source kind must be one of %q", sqlKind, compatibleSources)
|
||||
}
|
||||
|
||||
allParameters, paramManifest, paramMcpManifest, _ := tools.ProcessParameters(cfg.TemplateParameters, cfg.Parameters)
|
||||
|
||||
mcpManifest := tools.McpManifest{
|
||||
Name: cfg.Name,
|
||||
Description: cfg.Description,
|
||||
InputSchema: paramMcpManifest,
|
||||
}
|
||||
|
||||
t := Tool{
|
||||
Name: cfg.Name,
|
||||
Kind: sqlKind,
|
||||
Parameters: cfg.Parameters,
|
||||
TemplateParameters: cfg.TemplateParameters,
|
||||
AllParams: allParameters,
|
||||
Statement: cfg.Statement,
|
||||
AuthRequired: cfg.AuthRequired,
|
||||
Pool: s.ClickHousePool(),
|
||||
manifest: tools.Manifest{Description: cfg.Description, Parameters: paramManifest, AuthRequired: cfg.AuthRequired},
|
||||
mcpManifest: mcpManifest,
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
var _ tools.Tool = Tool{}
|
||||
|
||||
type Tool struct {
|
||||
Name string `yaml:"name"`
|
||||
Kind string `yaml:"kind"`
|
||||
AuthRequired []string `yaml:"authRequired"`
|
||||
Parameters tools.Parameters `yaml:"parameters"`
|
||||
TemplateParameters tools.Parameters `yaml:"templateParameters"`
|
||||
AllParams tools.Parameters `yaml:"allParams"`
|
||||
|
||||
Pool *sql.DB
|
||||
Statement string
|
||||
manifest tools.Manifest
|
||||
mcpManifest tools.McpManifest
|
||||
}
|
||||
|
||||
func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, token tools.AccessToken) (any, error) {
|
||||
paramsMap := params.AsMap()
|
||||
newStatement, err := tools.ResolveTemplateParams(t.TemplateParameters, t.Statement, paramsMap)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to extract template params: %w", err)
|
||||
}
|
||||
|
||||
newParams, err := tools.GetParams(t.Parameters, paramsMap)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to extract standard params: %w", err)
|
||||
}
|
||||
|
||||
sliceParams := newParams.AsSlice()
|
||||
results, err := t.Pool.QueryContext(ctx, newStatement, sliceParams...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to execute query: %w", err)
|
||||
}
|
||||
|
||||
cols, err := results.Columns()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to retrieve rows column name: %w", err)
|
||||
}
|
||||
|
||||
rawValues := make([]any, len(cols))
|
||||
values := make([]any, len(cols))
|
||||
for i := range rawValues {
|
||||
values[i] = &rawValues[i]
|
||||
}
|
||||
|
||||
colTypes, err := results.ColumnTypes()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to get column types: %w", err)
|
||||
}
|
||||
|
||||
var out []any
|
||||
for results.Next() {
|
||||
err := results.Scan(values...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to parse row: %w", err)
|
||||
}
|
||||
vMap := make(map[string]any)
|
||||
for i, name := range cols {
|
||||
switch colTypes[i].DatabaseTypeName() {
|
||||
case "String", "FixedString":
|
||||
if rawValues[i] != nil {
|
||||
// Handle potential []byte to string conversion if needed
|
||||
if b, ok := rawValues[i].([]byte); ok {
|
||||
vMap[name] = string(b)
|
||||
} else {
|
||||
vMap[name] = rawValues[i]
|
||||
}
|
||||
} else {
|
||||
vMap[name] = nil
|
||||
}
|
||||
default:
|
||||
vMap[name] = rawValues[i]
|
||||
}
|
||||
}
|
||||
out = append(out, vMap)
|
||||
}
|
||||
|
||||
err = results.Close()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to close rows: %w", err)
|
||||
}
|
||||
|
||||
if err := results.Err(); err != nil {
|
||||
return nil, fmt.Errorf("errors encountered by results.Scan: %w", err)
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
func (t Tool) RequiresClientAuthorization() bool {
|
||||
return false
|
||||
}
|
||||
276
internal/tools/clickhouse/clickhousesql/clickhousesql_test.go
Normal file
276
internal/tools/clickhouse/clickhousesql/clickhousesql_test.go
Normal file
@@ -0,0 +1,276 @@
|
||||
// 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 clickhouse
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/goccy/go-yaml"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/googleapis/genai-toolbox/internal/server"
|
||||
"github.com/googleapis/genai-toolbox/internal/sources"
|
||||
"github.com/googleapis/genai-toolbox/internal/sources/clickhouse"
|
||||
"github.com/googleapis/genai-toolbox/internal/testutils"
|
||||
"github.com/googleapis/genai-toolbox/internal/tools"
|
||||
)
|
||||
|
||||
func TestConfigToolConfigKind(t *testing.T) {
|
||||
config := Config{}
|
||||
if config.ToolConfigKind() != sqlKind {
|
||||
t.Errorf("Expected %s, got %s", sqlKind, config.ToolConfigKind())
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseFromYamlClickHouseSQL(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: clickhouse-sql
|
||||
source: my-instance
|
||||
description: some description
|
||||
statement: SELECT 1
|
||||
`,
|
||||
want: server.ToolConfigs{
|
||||
"example_tool": Config{
|
||||
Name: "example_tool",
|
||||
Kind: "clickhouse-sql",
|
||||
Source: "my-instance",
|
||||
Description: "some description",
|
||||
Statement: "SELECT 1",
|
||||
AuthRequired: []string{},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "with parameters",
|
||||
in: `
|
||||
tools:
|
||||
param_tool:
|
||||
kind: clickhouse-sql
|
||||
source: test-source
|
||||
description: Test ClickHouse tool
|
||||
statement: SELECT * FROM test_table WHERE id = $1
|
||||
parameters:
|
||||
- name: id
|
||||
type: string
|
||||
description: Test ID
|
||||
`,
|
||||
want: server.ToolConfigs{
|
||||
"param_tool": Config{
|
||||
Name: "param_tool",
|
||||
Kind: "clickhouse-sql",
|
||||
Source: "test-source",
|
||||
Description: "Test ClickHouse tool",
|
||||
Statement: "SELECT * FROM test_table WHERE id = $1",
|
||||
Parameters: tools.Parameters{
|
||||
tools.NewStringParameter("id", "Test ID"),
|
||||
},
|
||||
AuthRequired: []string{},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range tcs {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
got := struct {
|
||||
Tools server.ToolConfigs `yaml:"tools"`
|
||||
}{}
|
||||
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 TestSQLConfigInitializeValidSource(t *testing.T) {
|
||||
config := Config{
|
||||
Name: "test-tool",
|
||||
Kind: sqlKind,
|
||||
Source: "test-clickhouse",
|
||||
Description: "Test tool",
|
||||
Statement: "SELECT 1",
|
||||
Parameters: tools.Parameters{},
|
||||
}
|
||||
|
||||
// Create a mock ClickHouse source
|
||||
mockSource := &clickhouse.Source{}
|
||||
|
||||
sources := map[string]sources.Source{
|
||||
"test-clickhouse": mockSource,
|
||||
}
|
||||
|
||||
tool, err := config.Initialize(sources)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got: %v", err)
|
||||
}
|
||||
|
||||
clickhouseTool, ok := tool.(Tool)
|
||||
if !ok {
|
||||
t.Fatalf("Expected Tool type, got %T", tool)
|
||||
}
|
||||
|
||||
if clickhouseTool.Name != "test-tool" {
|
||||
t.Errorf("Expected name 'test-tool', got %s", clickhouseTool.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSQLConfigInitializeMissingSource(t *testing.T) {
|
||||
config := Config{
|
||||
Name: "test-tool",
|
||||
Kind: sqlKind,
|
||||
Source: "missing-source",
|
||||
Description: "Test tool",
|
||||
Statement: "SELECT 1",
|
||||
Parameters: tools.Parameters{},
|
||||
}
|
||||
|
||||
sources := map[string]sources.Source{}
|
||||
|
||||
_, err := config.Initialize(sources)
|
||||
if err == nil {
|
||||
t.Fatal("Expected error for missing source, got nil")
|
||||
}
|
||||
|
||||
expectedErr := `no source named "missing-source" configured`
|
||||
if err.Error() != expectedErr {
|
||||
t.Errorf("Expected error %q, got %q", expectedErr, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// mockIncompatibleSource is a mock source that doesn't implement the compatibleSource interface
|
||||
type mockIncompatibleSource struct{}
|
||||
|
||||
func (m *mockIncompatibleSource) SourceKind() string {
|
||||
return "mock"
|
||||
}
|
||||
|
||||
func TestSQLConfigInitializeIncompatibleSource(t *testing.T) {
|
||||
config := Config{
|
||||
Name: "test-tool",
|
||||
Kind: sqlKind,
|
||||
Source: "incompatible-source",
|
||||
Description: "Test tool",
|
||||
Statement: "SELECT 1",
|
||||
Parameters: tools.Parameters{},
|
||||
}
|
||||
|
||||
mockSource := &mockIncompatibleSource{}
|
||||
|
||||
sources := map[string]sources.Source{
|
||||
"incompatible-source": mockSource,
|
||||
}
|
||||
|
||||
_, err := config.Initialize(sources)
|
||||
if err == nil {
|
||||
t.Fatal("Expected error for incompatible source, got nil")
|
||||
}
|
||||
|
||||
if err.Error() == "" {
|
||||
t.Error("Expected non-empty error message")
|
||||
}
|
||||
}
|
||||
|
||||
func TestToolManifest(t *testing.T) {
|
||||
tool := Tool{
|
||||
manifest: tools.Manifest{
|
||||
Description: "Test description",
|
||||
Parameters: []tools.ParameterManifest{},
|
||||
},
|
||||
}
|
||||
|
||||
manifest := tool.Manifest()
|
||||
if manifest.Description != "Test description" {
|
||||
t.Errorf("Expected description 'Test description', got %s", manifest.Description)
|
||||
}
|
||||
}
|
||||
|
||||
func TestToolMcpManifest(t *testing.T) {
|
||||
tool := Tool{
|
||||
mcpManifest: tools.McpManifest{
|
||||
Name: "test-tool",
|
||||
Description: "Test description",
|
||||
},
|
||||
}
|
||||
|
||||
manifest := tool.McpManifest()
|
||||
if manifest.Name != "test-tool" {
|
||||
t.Errorf("Expected name 'test-tool', got %s", manifest.Name)
|
||||
}
|
||||
if manifest.Description != "Test description" {
|
||||
t.Errorf("Expected description 'Test description', got %s", manifest.Description)
|
||||
}
|
||||
}
|
||||
|
||||
func TestToolAuthorized(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
authRequired []string
|
||||
verifiedAuthServices []string
|
||||
expectedAuthorized bool
|
||||
}{
|
||||
{
|
||||
name: "no auth required",
|
||||
authRequired: []string{},
|
||||
verifiedAuthServices: []string{},
|
||||
expectedAuthorized: true,
|
||||
},
|
||||
{
|
||||
name: "auth required and verified",
|
||||
authRequired: []string{"google"},
|
||||
verifiedAuthServices: []string{"google"},
|
||||
expectedAuthorized: true,
|
||||
},
|
||||
{
|
||||
name: "auth required but not verified",
|
||||
authRequired: []string{"google"},
|
||||
verifiedAuthServices: []string{},
|
||||
expectedAuthorized: false,
|
||||
},
|
||||
{
|
||||
name: "auth required but different service verified",
|
||||
authRequired: []string{"google"},
|
||||
verifiedAuthServices: []string{"aws"},
|
||||
expectedAuthorized: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tool := Tool{
|
||||
AuthRequired: tt.authRequired,
|
||||
}
|
||||
|
||||
authorized := tool.Authorized(tt.verifiedAuthServices)
|
||||
if authorized != tt.expectedAuthorized {
|
||||
t.Errorf("Expected authorized %t, got %t", tt.expectedAuthorized, authorized)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -85,7 +85,7 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
// Create parameters
|
||||
collectionPathParameter := tools.NewStringParameter(
|
||||
collectionPathKey,
|
||||
"The path of the collection where the document will be added to",
|
||||
"The relative path of the collection where the document will be added to (e.g., 'users' or 'users/userId/posts'). Note: This is a relative path, NOT an absolute path like 'projects/{project_id}/databases/{database_id}/documents/...'",
|
||||
)
|
||||
|
||||
documentDataParameter := tools.NewMapParameter(
|
||||
@@ -159,6 +159,11 @@ func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken
|
||||
return nil, fmt.Errorf("invalid or missing '%s' parameter", collectionPathKey)
|
||||
}
|
||||
|
||||
// Validate collection path
|
||||
if err := util.ValidateCollectionPath(collectionPath); err != nil {
|
||||
return nil, fmt.Errorf("invalid collection path: %w", err)
|
||||
}
|
||||
|
||||
// Get document data
|
||||
documentDataRaw, ok := mapParams[documentDataKey]
|
||||
if !ok {
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"github.com/googleapis/genai-toolbox/internal/sources"
|
||||
firestoreds "github.com/googleapis/genai-toolbox/internal/sources/firestore"
|
||||
"github.com/googleapis/genai-toolbox/internal/tools"
|
||||
"github.com/googleapis/genai-toolbox/internal/tools/firestore/util"
|
||||
)
|
||||
|
||||
const kind string = "firestore-delete-documents"
|
||||
@@ -79,7 +80,7 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
return nil, fmt.Errorf("invalid source for %q tool: source kind must be one of %q", kind, compatibleSources)
|
||||
}
|
||||
|
||||
documentPathsParameter := tools.NewArrayParameter(documentPathsKey, "Array of document paths to delete from Firestore.", tools.NewStringParameter("item", "Document path"))
|
||||
documentPathsParameter := tools.NewArrayParameter(documentPathsKey, "Array of relative document paths to delete from Firestore (e.g., 'users/userId' or 'users/userId/posts/postId'). Note: These are relative paths, NOT absolute paths like 'projects/{project_id}/databases/{database_id}/documents/...'", tools.NewStringParameter("item", "Relative document path"))
|
||||
parameters := tools.Parameters{documentPathsParameter}
|
||||
|
||||
mcpManifest := tools.McpManifest{
|
||||
@@ -137,6 +138,13 @@ func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken
|
||||
return nil, fmt.Errorf("unexpected type conversion error for document paths")
|
||||
}
|
||||
|
||||
// Validate each document path
|
||||
for i, path := range documentPaths {
|
||||
if err := util.ValidateDocumentPath(path); err != nil {
|
||||
return nil, fmt.Errorf("invalid document path at index %d: %w", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Create a BulkWriter to handle multiple deletions efficiently
|
||||
bulkWriter := t.Client.BulkWriter(ctx)
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"github.com/googleapis/genai-toolbox/internal/sources"
|
||||
firestoreds "github.com/googleapis/genai-toolbox/internal/sources/firestore"
|
||||
"github.com/googleapis/genai-toolbox/internal/tools"
|
||||
"github.com/googleapis/genai-toolbox/internal/tools/firestore/util"
|
||||
)
|
||||
|
||||
const kind string = "firestore-get-documents"
|
||||
@@ -79,7 +80,7 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
return nil, fmt.Errorf("invalid source for %q tool: source kind must be one of %q", kind, compatibleSources)
|
||||
}
|
||||
|
||||
documentPathsParameter := tools.NewArrayParameter(documentPathsKey, "Array of document paths to retrieve from Firestore.", tools.NewStringParameter("item", "Document path"))
|
||||
documentPathsParameter := tools.NewArrayParameter(documentPathsKey, "Array of relative document paths to retrieve from Firestore (e.g., 'users/userId' or 'users/userId/posts/postId'). Note: These are relative paths, NOT absolute paths like 'projects/{project_id}/databases/{database_id}/documents/...'", tools.NewStringParameter("item", "Relative document path"))
|
||||
parameters := tools.Parameters{documentPathsParameter}
|
||||
|
||||
mcpManifest := tools.McpManifest{
|
||||
@@ -137,6 +138,13 @@ func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken
|
||||
return nil, fmt.Errorf("unexpected type conversion error for document paths")
|
||||
}
|
||||
|
||||
// Validate each document path
|
||||
for i, path := range documentPaths {
|
||||
if err := util.ValidateDocumentPath(path); err != nil {
|
||||
return nil, fmt.Errorf("invalid document path at index %d: %w", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Create document references from paths
|
||||
docRefs := make([]*firestoreapi.DocumentRef, len(documentPaths))
|
||||
for i, path := range documentPaths {
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"github.com/googleapis/genai-toolbox/internal/sources"
|
||||
firestoreds "github.com/googleapis/genai-toolbox/internal/sources/firestore"
|
||||
"github.com/googleapis/genai-toolbox/internal/tools"
|
||||
"github.com/googleapis/genai-toolbox/internal/tools/firestore/util"
|
||||
)
|
||||
|
||||
const kind string = "firestore-list-collections"
|
||||
@@ -80,7 +81,7 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
}
|
||||
|
||||
emptyString := ""
|
||||
parentPathParameter := tools.NewStringParameterWithDefault(parentPathKey, emptyString, "Parent document path to list subcollections from. If not provided, lists root collections.")
|
||||
parentPathParameter := tools.NewStringParameterWithDefault(parentPathKey, emptyString, "Relative parent document path to list subcollections from (e.g., 'users/userId'). If not provided, lists root collections. Note: This is a relative path, NOT an absolute path like 'projects/{project_id}/databases/{database_id}/documents/...'")
|
||||
parameters := tools.Parameters{parentPathParameter}
|
||||
|
||||
mcpManifest := tools.McpManifest{
|
||||
@@ -126,6 +127,11 @@ func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken
|
||||
parentPath, hasParent := mapParams[parentPathKey].(string)
|
||||
|
||||
if hasParent && parentPath != "" {
|
||||
// Validate parent document path
|
||||
if err := util.ValidateDocumentPath(parentPath); err != nil {
|
||||
return nil, fmt.Errorf("invalid parent document path: %w", err)
|
||||
}
|
||||
|
||||
// List subcollections of the specified document
|
||||
docRef := t.Client.Doc(parentPath)
|
||||
collectionRefs, err = docRef.Collections(ctx).GetAll()
|
||||
|
||||
@@ -25,6 +25,7 @@ import (
|
||||
"github.com/googleapis/genai-toolbox/internal/sources"
|
||||
firestoreds "github.com/googleapis/genai-toolbox/internal/sources/firestore"
|
||||
"github.com/googleapis/genai-toolbox/internal/tools"
|
||||
"github.com/googleapis/genai-toolbox/internal/tools/firestore/util"
|
||||
)
|
||||
|
||||
// Constants for tool configuration
|
||||
@@ -152,7 +153,7 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
func createParameters() tools.Parameters {
|
||||
collectionPathParameter := tools.NewStringParameter(
|
||||
collectionPathKey,
|
||||
"The path to the Firestore collection to query",
|
||||
"The relative path to the Firestore collection to query (e.g., 'users' or 'users/userId/posts'). Note: This is a relative path, NOT an absolute path like 'projects/{project_id}/databases/{database_id}/documents/...'",
|
||||
)
|
||||
|
||||
filtersDescription := `Array of filter objects to apply to the query. Each filter is a JSON string with:
|
||||
@@ -303,6 +304,11 @@ func (t Tool) parseQueryParameters(params tools.ParamValues) (*queryParameters,
|
||||
return nil, fmt.Errorf(errMissingCollectionPath, collectionPathKey)
|
||||
}
|
||||
|
||||
// Validate collection path
|
||||
if err := util.ValidateCollectionPath(collectionPath); err != nil {
|
||||
return nil, fmt.Errorf("invalid collection path: %w", err)
|
||||
}
|
||||
|
||||
result := &queryParameters{
|
||||
CollectionPath: collectionPath,
|
||||
Limit: defaultLimit,
|
||||
|
||||
@@ -87,7 +87,7 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
// Create parameters
|
||||
documentPathParameter := tools.NewStringParameter(
|
||||
documentPathKey,
|
||||
"The path of the document which needs to be updated",
|
||||
"The relative path of the document which needs to be updated (e.g., 'users/userId' or 'users/userId/posts/postId'). Note: This is a relative path, NOT an absolute path like 'projects/{project_id}/databases/{database_id}/documents/...'",
|
||||
)
|
||||
|
||||
documentDataParameter := tools.NewMapParameter(
|
||||
@@ -169,6 +169,11 @@ func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken
|
||||
return nil, fmt.Errorf("invalid or missing '%s' parameter", documentPathKey)
|
||||
}
|
||||
|
||||
// Validate document path
|
||||
if err := util.ValidateDocumentPath(documentPath); err != nil {
|
||||
return nil, fmt.Errorf("invalid document path: %w", err)
|
||||
}
|
||||
|
||||
// Get document data
|
||||
documentDataRaw, ok := mapParams[documentDataKey]
|
||||
if !ok {
|
||||
|
||||
137
internal/tools/firestore/util/validator.go
Normal file
137
internal/tools/firestore/util/validator.go
Normal file
@@ -0,0 +1,137 @@
|
||||
// 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 util
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Regular expressions for validating Firestore paths
|
||||
var (
|
||||
// Pattern to detect absolute paths (those starting with "projects/")
|
||||
absolutePathRegex = regexp.MustCompile(`^projects/[^/]+/databases/[^/]+/documents/`)
|
||||
)
|
||||
|
||||
// PathType represents the type of Firestore path
|
||||
type PathType int
|
||||
|
||||
const (
|
||||
CollectionPath PathType = iota
|
||||
DocumentPath
|
||||
)
|
||||
|
||||
// ValidateCollectionPath validates that a path is a valid Firestore collection path.
|
||||
// Collection paths must have an odd number of segments (collection/doc/collection)
|
||||
func ValidateCollectionPath(path string) error {
|
||||
return validatePath(path, CollectionPath)
|
||||
}
|
||||
|
||||
// ValidateDocumentPath validates that a path is a valid Firestore document path.
|
||||
// Document paths must have an even number of segments (collection/doc or collection/doc/collection/doc)
|
||||
func ValidateDocumentPath(path string) error {
|
||||
return validatePath(path, DocumentPath)
|
||||
}
|
||||
|
||||
// validatePath is the common validation function for both collection and document paths
|
||||
func validatePath(path string, pathType PathType) error {
|
||||
pathTypeName := "document"
|
||||
if pathType == CollectionPath {
|
||||
pathTypeName = "collection"
|
||||
}
|
||||
|
||||
// Check for empty path
|
||||
if path == "" {
|
||||
return fmt.Errorf("%s path cannot be empty", pathTypeName)
|
||||
}
|
||||
|
||||
// Check if it's an absolute path
|
||||
if absolutePathRegex.MatchString(path) {
|
||||
example := "users/userId"
|
||||
if pathType == CollectionPath {
|
||||
example = "users"
|
||||
}
|
||||
return fmt.Errorf("path must be relative (e.g., '%s'), not absolute (matching pattern: ^projects/[^/]+/databases/[^/]+/documents/)", example)
|
||||
}
|
||||
|
||||
// Split the path using strings.Split to preserve empty segments
|
||||
segments := strings.Split(path, "/")
|
||||
|
||||
// Check for empty result
|
||||
if len(segments) == 0 {
|
||||
return fmt.Errorf("%s path cannot be empty or contain only slashes", pathTypeName)
|
||||
}
|
||||
|
||||
// Check segment count based on path type
|
||||
segmentCount := len(segments)
|
||||
if pathType == CollectionPath && segmentCount%2 == 0 {
|
||||
// Collection paths must have an odd number of segments
|
||||
return fmt.Errorf("invalid collection path: must have an odd number of segments (e.g., 'collection' or 'collection/doc/subcollection'), got %d segments", segmentCount)
|
||||
} else if pathType == DocumentPath && segmentCount%2 != 0 {
|
||||
// Document paths must have an even number of segments
|
||||
return fmt.Errorf("invalid document path: must have an even number of segments (e.g., 'collection/doc'), got %d segments", segmentCount)
|
||||
}
|
||||
|
||||
// Validate each segment
|
||||
for i, segment := range segments {
|
||||
isCollectionSegment := (i % 2) == 0
|
||||
if err := validateSegment(segment, isCollectionSegment); err != nil {
|
||||
return fmt.Errorf("invalid segment at position %d (%s): %w", i+1, segment, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateSegment validates a single path segment
|
||||
func validateSegment(segment string, isCollection bool) error {
|
||||
segmentType := "document ID"
|
||||
if isCollection {
|
||||
segmentType = "collection ID"
|
||||
}
|
||||
|
||||
// Check for empty segment
|
||||
if segment == "" {
|
||||
return fmt.Errorf("segment cannot be empty")
|
||||
}
|
||||
|
||||
// Check for whitespace-only segment
|
||||
if strings.TrimSpace(segment) == "" {
|
||||
return fmt.Errorf("segment cannot be only whitespace")
|
||||
}
|
||||
|
||||
// Check for single or double period
|
||||
if segment == "." || segment == ".." {
|
||||
return fmt.Errorf("segment cannot be '.' or '..'")
|
||||
}
|
||||
|
||||
// Check for reserved prefix
|
||||
if strings.HasPrefix(segment, "__") {
|
||||
return fmt.Errorf("%s cannot start with '__' (reserved prefix)", segmentType)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsAbsolutePath checks if a path is an absolute Firestore path
|
||||
func IsAbsolutePath(path string) bool {
|
||||
return absolutePathRegex.MatchString(path)
|
||||
}
|
||||
|
||||
// IsRelativePath checks if a path is a relative Firestore path
|
||||
func IsRelativePath(path string) bool {
|
||||
return path != "" && !IsAbsolutePath(path)
|
||||
}
|
||||
218
internal/tools/firestore/util/validator_test.go
Normal file
218
internal/tools/firestore/util/validator_test.go
Normal file
@@ -0,0 +1,218 @@
|
||||
// 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 util
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestValidateCollectionPath(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
path string
|
||||
wantErr bool
|
||||
errMsg string
|
||||
}{
|
||||
// Valid cases
|
||||
{
|
||||
name: "valid root collection",
|
||||
path: "users",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid subcollection",
|
||||
path: "users/user123/posts",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid deeply nested",
|
||||
path: "users/user123/posts/post456/comments",
|
||||
wantErr: false,
|
||||
},
|
||||
|
||||
// Invalid cases
|
||||
{
|
||||
name: "empty path",
|
||||
path: "",
|
||||
wantErr: true,
|
||||
errMsg: "collection path cannot be empty",
|
||||
},
|
||||
{
|
||||
name: "even segments (document path)",
|
||||
path: "users/user123",
|
||||
wantErr: true,
|
||||
errMsg: "must have an odd number of segments",
|
||||
},
|
||||
{
|
||||
name: "absolute path",
|
||||
path: "projects/my-project/databases/(default)/documents/users",
|
||||
wantErr: true,
|
||||
errMsg: "path must be relative",
|
||||
},
|
||||
{
|
||||
name: "reserved prefix __",
|
||||
path: "__users",
|
||||
wantErr: true,
|
||||
errMsg: "collection ID cannot start with '__'",
|
||||
},
|
||||
{
|
||||
name: "dot segment",
|
||||
path: "users/./posts",
|
||||
wantErr: true,
|
||||
errMsg: "segment cannot be '.'",
|
||||
},
|
||||
{
|
||||
name: "double slashes",
|
||||
path: "users//posts",
|
||||
wantErr: true,
|
||||
errMsg: "segment cannot be empty",
|
||||
},
|
||||
{
|
||||
name: "trailing slash",
|
||||
path: "users/",
|
||||
wantErr: true,
|
||||
errMsg: "must have an odd number of segments",
|
||||
},
|
||||
{
|
||||
name: "whitespace only segment",
|
||||
path: "users/ /posts",
|
||||
wantErr: true,
|
||||
errMsg: "segment cannot be only whitespace",
|
||||
},
|
||||
{
|
||||
name: "tab whitespace segment",
|
||||
path: "users/\t/posts",
|
||||
wantErr: true,
|
||||
errMsg: "segment cannot be only whitespace",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := ValidateCollectionPath(tt.path)
|
||||
if tt.wantErr {
|
||||
if err == nil {
|
||||
t.Errorf("ValidateCollectionPath(%q) expected error but got none", tt.path)
|
||||
} else if tt.errMsg != "" && !strings.Contains(err.Error(), tt.errMsg) {
|
||||
t.Errorf("ValidateCollectionPath(%q) error = %v, want error containing %q", tt.path, err, tt.errMsg)
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Errorf("ValidateCollectionPath(%q) unexpected error: %v", tt.path, err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateDocumentPath(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
path string
|
||||
wantErr bool
|
||||
errMsg string
|
||||
}{
|
||||
// Valid cases
|
||||
{
|
||||
name: "valid root document",
|
||||
path: "users/user123",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid nested document",
|
||||
path: "users/user123/posts/post456",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid deeply nested",
|
||||
path: "users/user123/posts/post456/comments/comment789",
|
||||
wantErr: false,
|
||||
},
|
||||
|
||||
// Invalid cases
|
||||
{
|
||||
name: "empty path",
|
||||
path: "",
|
||||
wantErr: true,
|
||||
errMsg: "document path cannot be empty",
|
||||
},
|
||||
{
|
||||
name: "odd segments (collection path)",
|
||||
path: "users",
|
||||
wantErr: true,
|
||||
errMsg: "must have an even number of segments",
|
||||
},
|
||||
{
|
||||
name: "absolute path",
|
||||
path: "projects/my-project/databases/(default)/documents/users/user123",
|
||||
wantErr: true,
|
||||
errMsg: "path must be relative",
|
||||
},
|
||||
{
|
||||
name: "reserved prefix __",
|
||||
path: "users/__user123",
|
||||
wantErr: true,
|
||||
errMsg: "document ID cannot start with '__'",
|
||||
},
|
||||
{
|
||||
name: "double dot segment",
|
||||
path: "users/..",
|
||||
wantErr: true,
|
||||
errMsg: "segment cannot be '.'",
|
||||
},
|
||||
{
|
||||
name: "double slashes in document path",
|
||||
path: "users//user123",
|
||||
wantErr: true,
|
||||
errMsg: "must have an even number of segments",
|
||||
},
|
||||
{
|
||||
name: "trailing slash document",
|
||||
path: "users/user123/",
|
||||
wantErr: true,
|
||||
errMsg: "must have an even number of segments",
|
||||
},
|
||||
{
|
||||
name: "whitespace only document ID",
|
||||
path: "users/ ",
|
||||
wantErr: true,
|
||||
errMsg: "segment cannot be only whitespace",
|
||||
},
|
||||
{
|
||||
name: "whitespace in middle segment",
|
||||
path: "users/user123/posts/ \t ",
|
||||
wantErr: true,
|
||||
errMsg: "segment cannot be only whitespace",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := ValidateDocumentPath(tt.path)
|
||||
if tt.wantErr {
|
||||
if err == nil {
|
||||
t.Errorf("ValidateDocumentPath(%q) expected error but got none", tt.path)
|
||||
} else if tt.errMsg != "" && !strings.Contains(err.Error(), tt.errMsg) {
|
||||
t.Errorf("ValidateDocumentPath(%q) error = %v, want error containing %q", tt.path, err, tt.errMsg)
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Errorf("ValidateDocumentPath(%q) unexpected error: %v", tt.path, err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -93,12 +93,13 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
|
||||
// finish tool setup
|
||||
return Tool{
|
||||
Name: cfg.Name,
|
||||
Kind: kind,
|
||||
Parameters: parameters,
|
||||
AuthRequired: cfg.AuthRequired,
|
||||
Client: s.Client,
|
||||
ApiSettings: s.ApiSettings,
|
||||
Name: cfg.Name,
|
||||
Kind: kind,
|
||||
Parameters: parameters,
|
||||
AuthRequired: cfg.AuthRequired,
|
||||
UseClientOAuth: s.UseClientOAuth,
|
||||
Client: s.Client,
|
||||
ApiSettings: s.ApiSettings,
|
||||
manifest: tools.Manifest{
|
||||
Description: cfg.Description,
|
||||
Parameters: parameters.Manifest(),
|
||||
@@ -112,14 +113,15 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
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
|
||||
Name string `yaml:"name"`
|
||||
Kind string `yaml:"kind"`
|
||||
UseClientOAuth bool
|
||||
Client *v4.LookerSDK
|
||||
ApiSettings *rtl.ApiSettings
|
||||
AuthRequired []string `yaml:"authRequired"`
|
||||
Parameters tools.Parameters `yaml:"parameters"`
|
||||
manifest tools.Manifest
|
||||
mcpManifest tools.McpManifest
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -146,9 +148,15 @@ func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken
|
||||
wq.VisConfig = &visConfig
|
||||
|
||||
qrespFields := "id"
|
||||
qresp, err := t.Client.CreateQuery(*wq, qrespFields, t.ApiSettings)
|
||||
|
||||
sdk, err := lookercommon.GetLookerSDK(t.UseClientOAuth, t.ApiSettings, t.Client, accessToken)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error making create query request: %s", err)
|
||||
return nil, fmt.Errorf("error getting sdk: %w", err)
|
||||
}
|
||||
|
||||
qresp, err := sdk.CreateQuery(*wq, qrespFields, t.ApiSettings)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error making create query request: %w", err)
|
||||
}
|
||||
|
||||
wde := v4.WriteDashboardElement{
|
||||
@@ -170,9 +178,9 @@ func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken
|
||||
Fields: &fields,
|
||||
}
|
||||
|
||||
resp, err := t.Client.CreateDashboardElement(req, t.ApiSettings)
|
||||
resp, err := sdk.CreateDashboardElement(req, t.ApiSettings)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error making create dashboard element request: %s", err)
|
||||
return nil, fmt.Errorf("error making create dashboard element request: %w", err)
|
||||
}
|
||||
logger.DebugContext(ctx, "resp = %v", resp)
|
||||
|
||||
@@ -200,5 +208,5 @@ func (t Tool) Authorized(verifiedAuthServices []string) bool {
|
||||
}
|
||||
|
||||
func (t Tool) RequiresClientAuthorization() bool {
|
||||
return false
|
||||
return t.UseClientOAuth
|
||||
}
|
||||
|
||||
@@ -15,20 +15,67 @@ package lookercommon
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/googleapis/genai-toolbox/internal/tools"
|
||||
"github.com/googleapis/genai-toolbox/internal/util"
|
||||
rtl "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"
|
||||
)
|
||||
|
||||
// Make types for RoundTripper
|
||||
type transportWithAuthHeader struct {
|
||||
Base http.RoundTripper
|
||||
AuthToken tools.AccessToken
|
||||
}
|
||||
|
||||
func (t *transportWithAuthHeader) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
req.Header.Set("x-looker-appid", "go-sdk")
|
||||
req.Header.Set("Authorization", string(t.AuthToken))
|
||||
return t.Base.RoundTrip(req)
|
||||
}
|
||||
|
||||
func GetLookerSDK(useClientOAuth bool, config *rtl.ApiSettings, client *v4.LookerSDK, accessToken tools.AccessToken) (*v4.LookerSDK, error) {
|
||||
|
||||
if useClientOAuth {
|
||||
if accessToken == "" {
|
||||
return nil, fmt.Errorf("no access token supplied with request")
|
||||
}
|
||||
// Configure base transport with TLS
|
||||
transport := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: !config.VerifySsl,
|
||||
},
|
||||
}
|
||||
|
||||
// Build transport for end user token
|
||||
newTransport := &transportWithAuthHeader{
|
||||
Base: transport,
|
||||
AuthToken: accessToken,
|
||||
}
|
||||
|
||||
// return SDK with new Transport
|
||||
return v4.NewLookerSDK(&rtl.AuthSession{
|
||||
Config: *config,
|
||||
Client: http.Client{Transport: newTransport},
|
||||
}), nil
|
||||
}
|
||||
|
||||
if client == nil {
|
||||
return nil, fmt.Errorf("client id or client secret not valid")
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
|
||||
const (
|
||||
DimensionsFields = "fields(dimensions(name,type,label,label_short,description,synonyms,tags,hidden))"
|
||||
FiltersFields = "fields(filters(name,type,label,label_short,description,synonyms,tags,hidden))"
|
||||
MeasuresFields = "fields(measures(name,type,label,label_short,description,synonyms,tags,hidden))"
|
||||
ParametersFields = "fields(parameters(name,type,label,label_short,description,synonyms,tags,hidden))"
|
||||
DimensionsFields = "fields(dimensions(name,type,label,label_short,description,synonyms,tags,hidden,suggestable,suggestions,suggest_dimension,suggest_explore))"
|
||||
FiltersFields = "fields(filters(name,type,label,label_short,description,synonyms,tags,hidden,suggestable,suggestions,suggest_dimension,suggest_explore))"
|
||||
MeasuresFields = "fields(measures(name,type,label,label_short,description,synonyms,tags,hidden,suggestable,suggestions,suggest_dimension,suggest_explore))"
|
||||
ParametersFields = "fields(parameters(name,type,label,label_short,description,synonyms,tags,hidden,suggestable,suggestions,suggest_dimension,suggest_explore))"
|
||||
)
|
||||
|
||||
// ExtractLookerFieldProperties extracts common properties from Looker field objects.
|
||||
@@ -71,12 +118,21 @@ func ExtractLookerFieldProperties(ctx context.Context, fields *[]v4.LookmlModelE
|
||||
if v.Description != nil {
|
||||
vMap["description"] = *v.Description
|
||||
}
|
||||
if v.Tags != nil {
|
||||
if v.Tags != nil && len(*v.Tags) > 0 {
|
||||
vMap["tags"] = *v.Tags
|
||||
}
|
||||
if v.Synonyms != nil {
|
||||
if v.Synonyms != nil && len(*v.Synonyms) > 0 {
|
||||
vMap["synonyms"] = *v.Synonyms
|
||||
}
|
||||
if v.Suggestable != nil && *v.Suggestable {
|
||||
if v.Suggestions != nil && len(*v.Suggestions) > 0 {
|
||||
vMap["suggestions"] = *v.Suggestions
|
||||
}
|
||||
if v.SuggestExplore != nil && v.SuggestDimension != nil {
|
||||
vMap["suggest_explore"] = *v.SuggestExplore
|
||||
vMap["suggest_dimension"] = *v.SuggestDimension
|
||||
}
|
||||
}
|
||||
logger.DebugContext(ctx, "Converted to %v\n", vMap)
|
||||
data = append(data, vMap)
|
||||
}
|
||||
|
||||
@@ -31,6 +31,8 @@ func TestExtractLookerFieldProperties(t *testing.T) {
|
||||
|
||||
// Helper function to create string pointers
|
||||
stringPtr := func(s string) *string { return &s }
|
||||
stringArrayPtr := func(s []string) *[]string { return &s }
|
||||
boolPtr := func(b bool) *bool { return &b }
|
||||
|
||||
tcs := []struct {
|
||||
desc string
|
||||
@@ -41,20 +43,27 @@ func TestExtractLookerFieldProperties(t *testing.T) {
|
||||
desc: "field with all properties including description",
|
||||
fields: []v4.LookmlModelExploreField{
|
||||
{
|
||||
Name: stringPtr("dimension_name"),
|
||||
Type: stringPtr("string"),
|
||||
Label: stringPtr("Dimension Label"),
|
||||
LabelShort: stringPtr("Dim Label"),
|
||||
Description: stringPtr("This is a dimension description"),
|
||||
Name: stringPtr("dimension_name"),
|
||||
Type: stringPtr("string"),
|
||||
Label: stringPtr("Dimension Label"),
|
||||
LabelShort: stringPtr("Dim Label"),
|
||||
Description: stringPtr("This is a dimension description"),
|
||||
Suggestable: boolPtr(true),
|
||||
SuggestExplore: stringPtr("explore"),
|
||||
SuggestDimension: stringPtr("dimension"),
|
||||
Suggestions: stringArrayPtr([]string{"foo", "bar", "baz"}),
|
||||
},
|
||||
},
|
||||
want: []any{
|
||||
map[string]any{
|
||||
"name": "dimension_name",
|
||||
"type": "string",
|
||||
"label": "Dimension Label",
|
||||
"label_short": "Dim Label",
|
||||
"description": "This is a dimension description",
|
||||
"name": "dimension_name",
|
||||
"type": "string",
|
||||
"label": "Dimension Label",
|
||||
"label_short": "Dim Label",
|
||||
"description": "This is a dimension description",
|
||||
"suggest_explore": "explore",
|
||||
"suggest_dimension": "dimension",
|
||||
"suggestions": []string{"foo", "bar", "baz"},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"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/tools/looker/lookercommon"
|
||||
"github.com/googleapis/genai-toolbox/internal/util"
|
||||
|
||||
"github.com/looker-open-source/sdk-codegen/go/rtl"
|
||||
@@ -90,12 +91,13 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
|
||||
// finish tool setup
|
||||
return Tool{
|
||||
Name: cfg.Name,
|
||||
Kind: kind,
|
||||
Parameters: parameters,
|
||||
AuthRequired: cfg.AuthRequired,
|
||||
Client: s.Client,
|
||||
ApiSettings: s.ApiSettings,
|
||||
Name: cfg.Name,
|
||||
Kind: kind,
|
||||
Parameters: parameters,
|
||||
AuthRequired: cfg.AuthRequired,
|
||||
UseClientOAuth: s.UseClientOAuth,
|
||||
Client: s.Client,
|
||||
ApiSettings: s.ApiSettings,
|
||||
manifest: tools.Manifest{
|
||||
Description: cfg.Description,
|
||||
Parameters: parameters.Manifest(),
|
||||
@@ -109,14 +111,15 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
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
|
||||
Name string `yaml:"name"`
|
||||
Kind string `yaml:"kind"`
|
||||
UseClientOAuth bool
|
||||
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, accessToken tools.AccessToken) (any, error) {
|
||||
@@ -138,6 +141,10 @@ func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken
|
||||
limit := int64(paramsMap["limit"].(int))
|
||||
offset := int64(paramsMap["offset"].(int))
|
||||
|
||||
sdk, err := lookercommon.GetLookerSDK(t.UseClientOAuth, t.ApiSettings, t.Client, accessToken)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting sdk: %w", err)
|
||||
}
|
||||
req := v4.RequestSearchDashboards{
|
||||
Title: title_ptr,
|
||||
Description: desc_ptr,
|
||||
@@ -145,7 +152,7 @@ func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken
|
||||
Offset: &offset,
|
||||
}
|
||||
logger.ErrorContext(ctx, "Making request %v", req)
|
||||
resp, err := t.Client.SearchDashboards(req, t.ApiSettings)
|
||||
resp, err := sdk.SearchDashboards(req, t.ApiSettings)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error making get_dashboards request: %s", err)
|
||||
}
|
||||
|
||||
@@ -82,12 +82,13 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
|
||||
// finish tool setup
|
||||
return Tool{
|
||||
Name: cfg.Name,
|
||||
Kind: kind,
|
||||
Parameters: parameters,
|
||||
AuthRequired: cfg.AuthRequired,
|
||||
Client: s.Client,
|
||||
ApiSettings: s.ApiSettings,
|
||||
Name: cfg.Name,
|
||||
Kind: kind,
|
||||
Parameters: parameters,
|
||||
UseClientOAuth: s.UseClientOAuth,
|
||||
Client: s.Client,
|
||||
AuthRequired: cfg.AuthRequired,
|
||||
ApiSettings: s.ApiSettings,
|
||||
manifest: tools.Manifest{
|
||||
Description: cfg.Description,
|
||||
Parameters: parameters.Manifest(),
|
||||
@@ -104,6 +105,7 @@ var _ tools.Tool = Tool{}
|
||||
type Tool struct {
|
||||
Name string `yaml:"name"`
|
||||
Kind string `yaml:"kind"`
|
||||
UseClientOAuth bool
|
||||
Client *v4.LookerSDK
|
||||
ApiSettings *rtl.ApiSettings
|
||||
AuthRequired []string `yaml:"authRequired"`
|
||||
@@ -123,13 +125,17 @@ func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken
|
||||
return nil, fmt.Errorf("error processing model or explore: %w", err)
|
||||
}
|
||||
|
||||
sdk, err := lookercommon.GetLookerSDK(t.UseClientOAuth, t.ApiSettings, t.Client, accessToken)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting sdk: %w", err)
|
||||
}
|
||||
fields := lookercommon.DimensionsFields
|
||||
req := v4.RequestLookmlModelExplore{
|
||||
LookmlModelName: *model,
|
||||
ExploreName: *explore,
|
||||
Fields: &fields,
|
||||
}
|
||||
resp, err := t.Client.LookmlModelExplore(req, t.ApiSettings)
|
||||
resp, err := sdk.LookmlModelExplore(req, t.ApiSettings)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error making get_dimensions request: %w", err)
|
||||
}
|
||||
@@ -164,5 +170,5 @@ func (t Tool) Authorized(verifiedAuthServices []string) bool {
|
||||
}
|
||||
|
||||
func (t Tool) RequiresClientAuthorization() bool {
|
||||
return false
|
||||
return t.UseClientOAuth
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"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/tools/looker/lookercommon"
|
||||
"github.com/googleapis/genai-toolbox/internal/util"
|
||||
|
||||
"github.com/looker-open-source/sdk-codegen/go/rtl"
|
||||
@@ -82,12 +83,13 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
|
||||
// finish tool setup
|
||||
return Tool{
|
||||
Name: cfg.Name,
|
||||
Kind: kind,
|
||||
Parameters: parameters,
|
||||
AuthRequired: cfg.AuthRequired,
|
||||
Client: s.Client,
|
||||
ApiSettings: s.ApiSettings,
|
||||
Name: cfg.Name,
|
||||
Kind: kind,
|
||||
Parameters: parameters,
|
||||
AuthRequired: cfg.AuthRequired,
|
||||
UseClientOAuth: s.UseClientOAuth,
|
||||
Client: s.Client,
|
||||
ApiSettings: s.ApiSettings,
|
||||
manifest: tools.Manifest{
|
||||
Description: cfg.Description,
|
||||
Parameters: parameters.Manifest(),
|
||||
@@ -104,6 +106,7 @@ var _ tools.Tool = Tool{}
|
||||
type Tool struct {
|
||||
Name string `yaml:"name"`
|
||||
Kind string `yaml:"kind"`
|
||||
UseClientOAuth bool
|
||||
Client *v4.LookerSDK
|
||||
ApiSettings *rtl.ApiSettings
|
||||
AuthRequired []string `yaml:"authRequired"`
|
||||
@@ -124,7 +127,11 @@ func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken
|
||||
return nil, fmt.Errorf("'model' must be a string, got %T", mapParams["model"])
|
||||
}
|
||||
|
||||
resp, err := t.Client.LookmlModel(model, "explores(name,description,label,group_label,hidden)", t.ApiSettings)
|
||||
sdk, err := lookercommon.GetLookerSDK(t.UseClientOAuth, t.ApiSettings, t.Client, accessToken)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting sdk: %w", err)
|
||||
}
|
||||
resp, err := sdk.LookmlModel(model, "explores(name,description,label,group_label,hidden)", t.ApiSettings)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error making get_explores request: %s", err)
|
||||
}
|
||||
@@ -139,6 +146,9 @@ func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken
|
||||
if v.Name != nil {
|
||||
vMap["name"] = *v.Name
|
||||
}
|
||||
if v.Description != nil {
|
||||
vMap["description"] = *v.Description
|
||||
}
|
||||
if v.Label != nil {
|
||||
vMap["label"] = *v.Label
|
||||
}
|
||||
@@ -170,5 +180,5 @@ func (t Tool) Authorized(verifiedAuthServices []string) bool {
|
||||
}
|
||||
|
||||
func (t Tool) RequiresClientAuthorization() bool {
|
||||
return false
|
||||
return t.UseClientOAuth
|
||||
}
|
||||
|
||||
@@ -82,12 +82,13 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
|
||||
// finish tool setup
|
||||
return Tool{
|
||||
Name: cfg.Name,
|
||||
Kind: kind,
|
||||
Parameters: parameters,
|
||||
AuthRequired: cfg.AuthRequired,
|
||||
Client: s.Client,
|
||||
ApiSettings: s.ApiSettings,
|
||||
Name: cfg.Name,
|
||||
Kind: kind,
|
||||
Parameters: parameters,
|
||||
AuthRequired: cfg.AuthRequired,
|
||||
UseClientOAuth: s.UseClientOAuth,
|
||||
Client: s.Client,
|
||||
ApiSettings: s.ApiSettings,
|
||||
manifest: tools.Manifest{
|
||||
Description: cfg.Description,
|
||||
Parameters: parameters.Manifest(),
|
||||
@@ -104,6 +105,7 @@ var _ tools.Tool = Tool{}
|
||||
type Tool struct {
|
||||
Name string `yaml:"name"`
|
||||
Kind string `yaml:"kind"`
|
||||
UseClientOAuth bool
|
||||
Client *v4.LookerSDK
|
||||
ApiSettings *rtl.ApiSettings
|
||||
AuthRequired []string `yaml:"authRequired"`
|
||||
@@ -124,12 +126,16 @@ func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken
|
||||
}
|
||||
|
||||
fields := lookercommon.FiltersFields
|
||||
sdk, err := lookercommon.GetLookerSDK(t.UseClientOAuth, t.ApiSettings, t.Client, accessToken)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting sdk: %w", err)
|
||||
}
|
||||
req := v4.RequestLookmlModelExplore{
|
||||
LookmlModelName: *model,
|
||||
ExploreName: *explore,
|
||||
Fields: &fields,
|
||||
}
|
||||
resp, err := t.Client.LookmlModelExplore(req, t.ApiSettings)
|
||||
resp, err := sdk.LookmlModelExplore(req, t.ApiSettings)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error making get_filters request: %w", err)
|
||||
}
|
||||
@@ -164,5 +170,5 @@ func (t Tool) Authorized(verifiedAuthServices []string) bool {
|
||||
}
|
||||
|
||||
func (t Tool) RequiresClientAuthorization() bool {
|
||||
return false
|
||||
return t.UseClientOAuth
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"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/tools/looker/lookercommon"
|
||||
"github.com/googleapis/genai-toolbox/internal/util"
|
||||
|
||||
"github.com/looker-open-source/sdk-codegen/go/rtl"
|
||||
@@ -90,12 +91,13 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
|
||||
// finish tool setup
|
||||
return Tool{
|
||||
Name: cfg.Name,
|
||||
Kind: kind,
|
||||
Parameters: parameters,
|
||||
AuthRequired: cfg.AuthRequired,
|
||||
Client: s.Client,
|
||||
ApiSettings: s.ApiSettings,
|
||||
Name: cfg.Name,
|
||||
Kind: kind,
|
||||
Parameters: parameters,
|
||||
AuthRequired: cfg.AuthRequired,
|
||||
UseClientOAuth: s.UseClientOAuth,
|
||||
Client: s.Client,
|
||||
ApiSettings: s.ApiSettings,
|
||||
manifest: tools.Manifest{
|
||||
Description: cfg.Description,
|
||||
Parameters: parameters.Manifest(),
|
||||
@@ -109,14 +111,15 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
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
|
||||
Name string `yaml:"name"`
|
||||
Kind string `yaml:"kind"`
|
||||
UseClientOAuth bool
|
||||
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, accessToken tools.AccessToken) (any, error) {
|
||||
@@ -138,13 +141,17 @@ func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken
|
||||
limit := int64(paramsMap["limit"].(int))
|
||||
offset := int64(paramsMap["offset"].(int))
|
||||
|
||||
sdk, err := lookercommon.GetLookerSDK(t.UseClientOAuth, t.ApiSettings, t.Client, accessToken)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting sdk: %w", err)
|
||||
}
|
||||
req := v4.RequestSearchLooks{
|
||||
Title: title_ptr,
|
||||
Description: desc_ptr,
|
||||
Limit: &limit,
|
||||
Offset: &offset,
|
||||
}
|
||||
resp, err := t.Client.SearchLooks(req, t.ApiSettings)
|
||||
resp, err := sdk.SearchLooks(req, t.ApiSettings)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error making get_looks request: %s", err)
|
||||
}
|
||||
@@ -188,5 +195,5 @@ func (t Tool) Authorized(verifiedAuthServices []string) bool {
|
||||
}
|
||||
|
||||
func (t Tool) RequiresClientAuthorization() bool {
|
||||
return false
|
||||
return t.UseClientOAuth
|
||||
}
|
||||
|
||||
@@ -82,12 +82,13 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
|
||||
// finish tool setup
|
||||
return Tool{
|
||||
Name: cfg.Name,
|
||||
Kind: kind,
|
||||
Parameters: parameters,
|
||||
AuthRequired: cfg.AuthRequired,
|
||||
Client: s.Client,
|
||||
ApiSettings: s.ApiSettings,
|
||||
Name: cfg.Name,
|
||||
Kind: kind,
|
||||
Parameters: parameters,
|
||||
AuthRequired: cfg.AuthRequired,
|
||||
UseClientOAuth: s.UseClientOAuth,
|
||||
Client: s.Client,
|
||||
ApiSettings: s.ApiSettings,
|
||||
manifest: tools.Manifest{
|
||||
Description: cfg.Description,
|
||||
Parameters: parameters.Manifest(),
|
||||
@@ -104,6 +105,7 @@ var _ tools.Tool = Tool{}
|
||||
type Tool struct {
|
||||
Name string `yaml:"name"`
|
||||
Kind string `yaml:"kind"`
|
||||
UseClientOAuth bool
|
||||
Client *v4.LookerSDK
|
||||
ApiSettings *rtl.ApiSettings
|
||||
AuthRequired []string `yaml:"authRequired"`
|
||||
@@ -124,12 +126,16 @@ func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken
|
||||
}
|
||||
|
||||
fields := lookercommon.MeasuresFields
|
||||
sdk, err := lookercommon.GetLookerSDK(t.UseClientOAuth, t.ApiSettings, t.Client, accessToken)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting sdk: %w", err)
|
||||
}
|
||||
req := v4.RequestLookmlModelExplore{
|
||||
LookmlModelName: *model,
|
||||
ExploreName: *explore,
|
||||
Fields: &fields,
|
||||
}
|
||||
resp, err := t.Client.LookmlModelExplore(req, t.ApiSettings)
|
||||
resp, err := sdk.LookmlModelExplore(req, t.ApiSettings)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error making get_measures request: %w", err)
|
||||
}
|
||||
@@ -164,5 +170,5 @@ func (t Tool) Authorized(verifiedAuthServices []string) bool {
|
||||
}
|
||||
|
||||
func (t Tool) RequiresClientAuthorization() bool {
|
||||
return false
|
||||
return t.UseClientOAuth
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"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/tools/looker/lookercommon"
|
||||
"github.com/googleapis/genai-toolbox/internal/util"
|
||||
|
||||
"github.com/looker-open-source/sdk-codegen/go/rtl"
|
||||
@@ -81,12 +82,13 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
|
||||
// finish tool setup
|
||||
return Tool{
|
||||
Name: cfg.Name,
|
||||
Kind: kind,
|
||||
Parameters: parameters,
|
||||
AuthRequired: cfg.AuthRequired,
|
||||
Client: s.Client,
|
||||
ApiSettings: s.ApiSettings,
|
||||
Name: cfg.Name,
|
||||
Kind: kind,
|
||||
Parameters: parameters,
|
||||
AuthRequired: cfg.AuthRequired,
|
||||
UseClientOAuth: s.UseClientOAuth,
|
||||
Client: s.Client,
|
||||
ApiSettings: s.ApiSettings,
|
||||
manifest: tools.Manifest{
|
||||
Description: cfg.Description,
|
||||
Parameters: parameters.Manifest(),
|
||||
@@ -103,6 +105,7 @@ var _ tools.Tool = Tool{}
|
||||
type Tool struct {
|
||||
Name string `yaml:"name"`
|
||||
Kind string `yaml:"kind"`
|
||||
UseClientOAuth bool
|
||||
Client *v4.LookerSDK
|
||||
ApiSettings *rtl.ApiSettings
|
||||
AuthRequired []string `yaml:"authRequired"`
|
||||
@@ -122,12 +125,16 @@ func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken
|
||||
excludeHidden := !t.ShowHiddenModels
|
||||
includeInternal := true
|
||||
|
||||
sdk, err := lookercommon.GetLookerSDK(t.UseClientOAuth, t.ApiSettings, t.Client, accessToken)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting sdk: %w", err)
|
||||
}
|
||||
req := v4.RequestAllLookmlModels{
|
||||
ExcludeEmpty: &excludeEmpty,
|
||||
ExcludeHidden: &excludeHidden,
|
||||
IncludeInternal: &includeInternal,
|
||||
}
|
||||
resp, err := t.Client.AllLookmlModels(req, t.ApiSettings)
|
||||
resp, err := sdk.AllLookmlModels(req, t.ApiSettings)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error making get_models request: %s", err)
|
||||
}
|
||||
@@ -164,5 +171,5 @@ func (t Tool) Authorized(verifiedAuthServices []string) bool {
|
||||
}
|
||||
|
||||
func (t Tool) RequiresClientAuthorization() bool {
|
||||
return false
|
||||
return t.UseClientOAuth
|
||||
}
|
||||
|
||||
@@ -82,12 +82,13 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
|
||||
// finish tool setup
|
||||
return Tool{
|
||||
Name: cfg.Name,
|
||||
Kind: kind,
|
||||
Parameters: parameters,
|
||||
AuthRequired: cfg.AuthRequired,
|
||||
Client: s.Client,
|
||||
ApiSettings: s.ApiSettings,
|
||||
Name: cfg.Name,
|
||||
Kind: kind,
|
||||
Parameters: parameters,
|
||||
AuthRequired: cfg.AuthRequired,
|
||||
UseClientOAuth: s.UseClientOAuth,
|
||||
Client: s.Client,
|
||||
ApiSettings: s.ApiSettings,
|
||||
manifest: tools.Manifest{
|
||||
Description: cfg.Description,
|
||||
Parameters: parameters.Manifest(),
|
||||
@@ -104,6 +105,7 @@ var _ tools.Tool = Tool{}
|
||||
type Tool struct {
|
||||
Name string `yaml:"name"`
|
||||
Kind string `yaml:"kind"`
|
||||
UseClientOAuth bool
|
||||
Client *v4.LookerSDK
|
||||
ApiSettings *rtl.ApiSettings
|
||||
AuthRequired []string `yaml:"authRequired"`
|
||||
@@ -124,12 +126,16 @@ func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken
|
||||
}
|
||||
|
||||
fields := lookercommon.ParametersFields
|
||||
sdk, err := lookercommon.GetLookerSDK(t.UseClientOAuth, t.ApiSettings, t.Client, accessToken)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting sdk: %w", err)
|
||||
}
|
||||
req := v4.RequestLookmlModelExplore{
|
||||
LookmlModelName: *model,
|
||||
ExploreName: *explore,
|
||||
Fields: &fields,
|
||||
}
|
||||
resp, err := t.Client.LookmlModelExplore(req, t.ApiSettings)
|
||||
resp, err := sdk.LookmlModelExplore(req, t.ApiSettings)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error making get_parameters request: %w", err)
|
||||
}
|
||||
@@ -164,5 +170,5 @@ func (t Tool) Authorized(verifiedAuthServices []string) bool {
|
||||
}
|
||||
|
||||
func (t Tool) RequiresClientAuthorization() bool {
|
||||
return false
|
||||
return t.UseClientOAuth
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"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/tools/looker/lookercommon"
|
||||
"github.com/googleapis/genai-toolbox/internal/util"
|
||||
|
||||
"github.com/looker-open-source/sdk-codegen/go/rtl"
|
||||
@@ -88,12 +89,13 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
|
||||
// finish tool setup
|
||||
return Tool{
|
||||
Name: cfg.Name,
|
||||
Kind: kind,
|
||||
Parameters: parameters,
|
||||
AuthRequired: cfg.AuthRequired,
|
||||
Client: s.Client,
|
||||
ApiSettings: s.ApiSettings,
|
||||
Name: cfg.Name,
|
||||
Kind: kind,
|
||||
Parameters: parameters,
|
||||
AuthRequired: cfg.AuthRequired,
|
||||
UseClientOAuth: s.UseClientOAuth,
|
||||
Client: s.Client,
|
||||
ApiSettings: s.ApiSettings,
|
||||
manifest: tools.Manifest{
|
||||
Description: cfg.Description,
|
||||
Parameters: parameters.Manifest(),
|
||||
@@ -107,14 +109,15 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
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
|
||||
Name string `yaml:"name"`
|
||||
Kind string `yaml:"kind"`
|
||||
UseClientOAuth bool
|
||||
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, accessToken tools.AccessToken) (any, error) {
|
||||
@@ -124,8 +127,12 @@ func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken
|
||||
}
|
||||
logger.DebugContext(ctx, "params = ", params)
|
||||
|
||||
sdk, err := lookercommon.GetLookerSDK(t.UseClientOAuth, t.ApiSettings, t.Client, accessToken)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting sdk: %w", err)
|
||||
}
|
||||
mrespFields := "id,personal_folder_id"
|
||||
mresp, err := t.Client.Me(mrespFields, t.ApiSettings)
|
||||
mresp, err := sdk.Me(mrespFields, t.ApiSettings)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error making me request: %s", err)
|
||||
}
|
||||
@@ -138,7 +145,7 @@ func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken
|
||||
return nil, fmt.Errorf("user does not have a personal folder. cannot continue")
|
||||
}
|
||||
|
||||
dashs, err := t.Client.FolderDashboards(*mresp.PersonalFolderId, "title", t.ApiSettings)
|
||||
dashs, err := sdk.FolderDashboards(*mresp.PersonalFolderId, "title", t.ApiSettings)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting existing dashboards in folder: %s", err)
|
||||
}
|
||||
@@ -157,13 +164,13 @@ func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken
|
||||
Description: &description,
|
||||
FolderId: mresp.PersonalFolderId,
|
||||
}
|
||||
resp, err := t.Client.CreateDashboard(wd, t.ApiSettings)
|
||||
resp, err := sdk.CreateDashboard(wd, t.ApiSettings)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error making create dashboard request: %s", err)
|
||||
}
|
||||
logger.DebugContext(ctx, "resp = %v", resp)
|
||||
|
||||
setting, err := t.Client.GetSetting("host_url", t.ApiSettings)
|
||||
setting, err := sdk.GetSetting("host_url", t.ApiSettings)
|
||||
if err != nil {
|
||||
logger.ErrorContext(ctx, "error getting settings: %s", err)
|
||||
}
|
||||
@@ -201,5 +208,5 @@ func (t Tool) Authorized(verifiedAuthServices []string) bool {
|
||||
}
|
||||
|
||||
func (t Tool) RequiresClientAuthorization() bool {
|
||||
return false
|
||||
return t.UseClientOAuth
|
||||
}
|
||||
|
||||
@@ -95,12 +95,13 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
|
||||
// finish tool setup
|
||||
return Tool{
|
||||
Name: cfg.Name,
|
||||
Kind: kind,
|
||||
Parameters: parameters,
|
||||
AuthRequired: cfg.AuthRequired,
|
||||
Client: s.Client,
|
||||
ApiSettings: s.ApiSettings,
|
||||
Name: cfg.Name,
|
||||
Kind: kind,
|
||||
Parameters: parameters,
|
||||
AuthRequired: cfg.AuthRequired,
|
||||
UseClientOAuth: s.UseClientOAuth,
|
||||
Client: s.Client,
|
||||
ApiSettings: s.ApiSettings,
|
||||
manifest: tools.Manifest{
|
||||
Description: cfg.Description,
|
||||
Parameters: parameters.Manifest(),
|
||||
@@ -114,14 +115,15 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
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
|
||||
Name string `yaml:"name"`
|
||||
Kind string `yaml:"kind"`
|
||||
UseClientOAuth bool
|
||||
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, accessToken tools.AccessToken) (any, error) {
|
||||
@@ -135,8 +137,12 @@ func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken
|
||||
return nil, fmt.Errorf("error building query request: %w", err)
|
||||
}
|
||||
|
||||
sdk, err := lookercommon.GetLookerSDK(t.UseClientOAuth, t.ApiSettings, t.Client, accessToken)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting sdk: %w", err)
|
||||
}
|
||||
mrespFields := "id,personal_folder_id"
|
||||
mresp, err := t.Client.Me(mrespFields, t.ApiSettings)
|
||||
mresp, err := sdk.Me(mrespFields, t.ApiSettings)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error making me request: %s", err)
|
||||
}
|
||||
@@ -145,7 +151,7 @@ func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken
|
||||
title := paramsMap["title"].(string)
|
||||
description := paramsMap["description"].(string)
|
||||
|
||||
looks, err := t.Client.FolderLooks(*mresp.PersonalFolderId, "title", t.ApiSettings)
|
||||
looks, err := sdk.FolderLooks(*mresp.PersonalFolderId, "title", t.ApiSettings)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting existing looks in folder: %s", err)
|
||||
}
|
||||
@@ -163,7 +169,7 @@ func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken
|
||||
wq.VisConfig = &visConfig
|
||||
|
||||
qrespFields := "id"
|
||||
qresp, err := t.Client.CreateQuery(*wq, qrespFields, t.ApiSettings)
|
||||
qresp, err := sdk.CreateQuery(*wq, qrespFields, t.ApiSettings)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error making create query request: %s", err)
|
||||
}
|
||||
@@ -175,13 +181,13 @@ func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken
|
||||
QueryId: qresp.Id,
|
||||
FolderId: mresp.PersonalFolderId,
|
||||
}
|
||||
resp, err := t.Client.CreateLook(wlwq, "", t.ApiSettings)
|
||||
resp, err := sdk.CreateLook(wlwq, "", t.ApiSettings)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error making create look request: %s", err)
|
||||
}
|
||||
logger.DebugContext(ctx, "resp = %v", resp)
|
||||
|
||||
setting, err := t.Client.GetSetting("host_url", t.ApiSettings)
|
||||
setting, err := sdk.GetSetting("host_url", t.ApiSettings)
|
||||
if err != nil {
|
||||
logger.ErrorContext(ctx, "error getting settings: %s", err)
|
||||
}
|
||||
@@ -219,5 +225,5 @@ func (t Tool) Authorized(verifiedAuthServices []string) bool {
|
||||
}
|
||||
|
||||
func (t Tool) RequiresClientAuthorization() bool {
|
||||
return false
|
||||
return t.UseClientOAuth
|
||||
}
|
||||
|
||||
@@ -83,12 +83,13 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
|
||||
// finish tool setup
|
||||
return Tool{
|
||||
Name: cfg.Name,
|
||||
Kind: kind,
|
||||
Parameters: parameters,
|
||||
AuthRequired: cfg.AuthRequired,
|
||||
Client: s.Client,
|
||||
ApiSettings: s.ApiSettings,
|
||||
Name: cfg.Name,
|
||||
Kind: kind,
|
||||
Parameters: parameters,
|
||||
AuthRequired: cfg.AuthRequired,
|
||||
UseClientOAuth: s.UseClientOAuth,
|
||||
Client: s.Client,
|
||||
ApiSettings: s.ApiSettings,
|
||||
manifest: tools.Manifest{
|
||||
Description: cfg.Description,
|
||||
Parameters: parameters.Manifest(),
|
||||
@@ -102,14 +103,15 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
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
|
||||
Name string `yaml:"name"`
|
||||
Kind string `yaml:"kind"`
|
||||
UseClientOAuth bool
|
||||
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, accessToken tools.AccessToken) (any, error) {
|
||||
@@ -121,11 +123,15 @@ func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error building WriteQuery request: %w", err)
|
||||
}
|
||||
sdk, err := lookercommon.GetLookerSDK(t.UseClientOAuth, t.ApiSettings, t.Client, accessToken)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting sdk: %w", err)
|
||||
}
|
||||
req := v4.RequestRunInlineQuery{
|
||||
Body: *wq,
|
||||
ResultFormat: "json",
|
||||
}
|
||||
resp, err := t.Client.RunInlineQuery(req, t.ApiSettings)
|
||||
resp, err := sdk.RunInlineQuery(req, t.ApiSettings)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error making query request: %s", err)
|
||||
}
|
||||
@@ -159,5 +165,5 @@ func (t Tool) Authorized(verifiedAuthServices []string) bool {
|
||||
}
|
||||
|
||||
func (t Tool) RequiresClientAuthorization() bool {
|
||||
return false
|
||||
return t.UseClientOAuth
|
||||
}
|
||||
|
||||
@@ -82,12 +82,13 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
|
||||
// finish tool setup
|
||||
return Tool{
|
||||
Name: cfg.Name,
|
||||
Kind: kind,
|
||||
Parameters: parameters,
|
||||
AuthRequired: cfg.AuthRequired,
|
||||
Client: s.Client,
|
||||
ApiSettings: s.ApiSettings,
|
||||
Name: cfg.Name,
|
||||
Kind: kind,
|
||||
Parameters: parameters,
|
||||
AuthRequired: cfg.AuthRequired,
|
||||
UseClientOAuth: s.UseClientOAuth,
|
||||
Client: s.Client,
|
||||
ApiSettings: s.ApiSettings,
|
||||
manifest: tools.Manifest{
|
||||
Description: cfg.Description,
|
||||
Parameters: parameters.Manifest(),
|
||||
@@ -101,14 +102,15 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
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
|
||||
Name string `yaml:"name"`
|
||||
Kind string `yaml:"kind"`
|
||||
UseClientOAuth bool
|
||||
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, accessToken tools.AccessToken) (any, error) {
|
||||
@@ -120,11 +122,15 @@ func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error building query request: %w", err)
|
||||
}
|
||||
sdk, err := lookercommon.GetLookerSDK(t.UseClientOAuth, t.ApiSettings, t.Client, accessToken)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting sdk: %w", err)
|
||||
}
|
||||
req := v4.RequestRunInlineQuery{
|
||||
Body: *wq,
|
||||
ResultFormat: "sql",
|
||||
}
|
||||
resp, err := t.Client.RunInlineQuery(req, t.ApiSettings)
|
||||
resp, err := sdk.RunInlineQuery(req, t.ApiSettings)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error making query_sql request: %s", err)
|
||||
}
|
||||
@@ -150,5 +156,5 @@ func (t Tool) Authorized(verifiedAuthServices []string) bool {
|
||||
}
|
||||
|
||||
func (t Tool) RequiresClientAuthorization() bool {
|
||||
return false
|
||||
return t.UseClientOAuth
|
||||
}
|
||||
|
||||
@@ -89,12 +89,13 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
|
||||
// finish tool setup
|
||||
return Tool{
|
||||
Name: cfg.Name,
|
||||
Kind: kind,
|
||||
Parameters: parameters,
|
||||
AuthRequired: cfg.AuthRequired,
|
||||
Client: s.Client,
|
||||
ApiSettings: s.ApiSettings,
|
||||
Name: cfg.Name,
|
||||
Kind: kind,
|
||||
Parameters: parameters,
|
||||
AuthRequired: cfg.AuthRequired,
|
||||
UseClientOAuth: s.UseClientOAuth,
|
||||
Client: s.Client,
|
||||
ApiSettings: s.ApiSettings,
|
||||
manifest: tools.Manifest{
|
||||
Description: cfg.Description,
|
||||
Parameters: parameters.Manifest(),
|
||||
@@ -108,14 +109,15 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
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
|
||||
Name string `yaml:"name"`
|
||||
Kind string `yaml:"kind"`
|
||||
UseClientOAuth bool
|
||||
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, accessToken tools.AccessToken) (any, error) {
|
||||
@@ -133,8 +135,12 @@ func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken
|
||||
visConfig := paramsMap["vis_config"].(map[string]any)
|
||||
wq.VisConfig = &visConfig
|
||||
|
||||
sdk, err := lookercommon.GetLookerSDK(t.UseClientOAuth, t.ApiSettings, t.Client, accessToken)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting sdk: %w", err)
|
||||
}
|
||||
respFields := "id,slug,share_url,expanded_share_url"
|
||||
resp, err := t.Client.CreateQuery(*wq, respFields, t.ApiSettings)
|
||||
resp, err := sdk.CreateQuery(*wq, respFields, t.ApiSettings)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error making query request: %s", err)
|
||||
}
|
||||
@@ -175,5 +181,5 @@ func (t Tool) Authorized(verifiedAuthServices []string) bool {
|
||||
}
|
||||
|
||||
func (t Tool) RequiresClientAuthorization() bool {
|
||||
return false
|
||||
return t.UseClientOAuth
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"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/tools/looker/lookercommon"
|
||||
"github.com/googleapis/genai-toolbox/internal/util"
|
||||
|
||||
"github.com/looker-open-source/sdk-codegen/go/rtl"
|
||||
@@ -88,12 +89,13 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
|
||||
// finish tool setup
|
||||
return Tool{
|
||||
Name: cfg.Name,
|
||||
Kind: kind,
|
||||
Parameters: parameters,
|
||||
AuthRequired: cfg.AuthRequired,
|
||||
Client: s.Client,
|
||||
ApiSettings: s.ApiSettings,
|
||||
Name: cfg.Name,
|
||||
Kind: kind,
|
||||
Parameters: parameters,
|
||||
AuthRequired: cfg.AuthRequired,
|
||||
UseClientOAuth: s.UseClientOAuth,
|
||||
Client: s.Client,
|
||||
ApiSettings: s.ApiSettings,
|
||||
manifest: tools.Manifest{
|
||||
Description: cfg.Description,
|
||||
Parameters: parameters.Manifest(),
|
||||
@@ -107,14 +109,15 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
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
|
||||
Name string `yaml:"name"`
|
||||
Kind string `yaml:"kind"`
|
||||
UseClientOAuth bool
|
||||
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, accessToken tools.AccessToken) (any, error) {
|
||||
@@ -128,12 +131,16 @@ func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken
|
||||
look_id := paramsMap["look_id"].(string)
|
||||
limit := int64(paramsMap["limit"].(int))
|
||||
|
||||
sdk, err := lookercommon.GetLookerSDK(t.UseClientOAuth, t.ApiSettings, t.Client, accessToken)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting sdk: %w", err)
|
||||
}
|
||||
req := v4.RequestRunLook{
|
||||
LookId: look_id,
|
||||
ResultFormat: "json",
|
||||
Limit: &limit,
|
||||
}
|
||||
resp, err := t.Client.RunLook(req, t.ApiSettings)
|
||||
resp, err := sdk.RunLook(req, t.ApiSettings)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error making run_look request: %s", err)
|
||||
}
|
||||
@@ -167,5 +174,5 @@ func (t Tool) Authorized(verifiedAuthServices []string) bool {
|
||||
}
|
||||
|
||||
func (t Tool) RequiresClientAuthorization() bool {
|
||||
return false
|
||||
return t.UseClientOAuth
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"github.com/googleapis/genai-toolbox/internal/sources"
|
||||
"github.com/googleapis/genai-toolbox/internal/sources/oceanbase"
|
||||
"github.com/googleapis/genai-toolbox/internal/tools"
|
||||
"github.com/googleapis/genai-toolbox/internal/tools/mysql/mysqlcommon"
|
||||
)
|
||||
|
||||
const kind string = "oceanbase-execute-sql"
|
||||
@@ -159,13 +160,10 @@ func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken
|
||||
continue
|
||||
}
|
||||
|
||||
// oceanbase driver returns []uint8 type for "TEXT", "VARCHAR", and "NVARCHAR"
|
||||
// we'll need to cast it back to string
|
||||
switch colTypes[i].DatabaseTypeName() {
|
||||
case "TEXT", "VARCHAR", "NVARCHAR":
|
||||
vMap[name] = string(val.([]byte))
|
||||
default:
|
||||
vMap[name] = val
|
||||
// oceanbase uses mysql driver
|
||||
vMap[name], err = mysqlcommon.ConvertToType(colTypes[i], val)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("errors encountered when converting values: %w", err)
|
||||
}
|
||||
}
|
||||
out = append(out, vMap)
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"github.com/googleapis/genai-toolbox/internal/sources"
|
||||
"github.com/googleapis/genai-toolbox/internal/sources/oceanbase"
|
||||
"github.com/googleapis/genai-toolbox/internal/tools"
|
||||
"github.com/googleapis/genai-toolbox/internal/tools/mysql/mysqlcommon"
|
||||
)
|
||||
|
||||
const kind string = "oceanbase-sql"
|
||||
@@ -176,13 +177,10 @@ func (t Tool) Invoke(ctx context.Context, params tools.ParamValues, accessToken
|
||||
continue
|
||||
}
|
||||
|
||||
// oceanbase driver returns []uint8 type for "TEXT", "VARCHAR", and "NVARCHAR"
|
||||
// we'll need to cast it back to string
|
||||
switch colTypes[i].DatabaseTypeName() {
|
||||
case "TEXT", "VARCHAR", "NVARCHAR":
|
||||
vMap[name] = string(val.([]byte))
|
||||
default:
|
||||
vMap[name] = val
|
||||
// oceanbase uses mysql driver
|
||||
vMap[name], err = mysqlcommon.ConvertToType(colTypes[i], val)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("errors encountered when converting values: %w", err)
|
||||
}
|
||||
}
|
||||
out = append(out, vMap)
|
||||
|
||||
@@ -165,12 +165,12 @@ func TestAlloyDBPgToolEndpoints(t *testing.T) {
|
||||
}
|
||||
|
||||
// Get configs for tests
|
||||
select1Want, failInvocationWant, createTableStatement := tests.GetPostgresWants()
|
||||
select1Want, failInvocationWant, createTableStatement, mcpSelect1Want := tests.GetPostgresWants()
|
||||
|
||||
// Run tests
|
||||
tests.RunToolGetTest(t)
|
||||
tests.RunToolInvokeTest(t, select1Want)
|
||||
tests.RunMCPToolCallMethod(t, failInvocationWant)
|
||||
tests.RunMCPToolCallMethod(t, failInvocationWant, mcpSelect1Want)
|
||||
tests.RunExecuteSqlToolInvokeTest(t, createTableStatement, select1Want)
|
||||
tests.RunToolInvokeWithTemplateParameters(t, tableNameTemplateParam)
|
||||
}
|
||||
|
||||
@@ -133,6 +133,7 @@ func TestBigQueryToolEndpoints(t *testing.T) {
|
||||
|
||||
// Write config into a file and pass it to command
|
||||
toolsFile := tests.GetToolsConfig(sourceConfig, BigqueryToolKind, paramToolStmt, idParamToolStmt, nameParamToolStmt, arrayToolStmt, authToolStmt)
|
||||
toolsFile = addClientAuthSourceConfig(t, toolsFile)
|
||||
toolsFile = addBigQuerySqlToolConfig(t, toolsFile, dataTypeToolStmt, arrayDataTypeToolStmt)
|
||||
toolsFile = addBigQueryPrebuiltToolsConfig(t, toolsFile)
|
||||
tmplSelectCombined, tmplSelectFilterCombined := getBigQueryTmplToolStatement()
|
||||
@@ -161,13 +162,14 @@ func TestBigQueryToolEndpoints(t *testing.T) {
|
||||
dataInsightsWant := `(?s)Schema Resolved.*Retrieval Query.*SQL Generated.*Answer`
|
||||
// Partial message; the full error message is too long.
|
||||
mcpMyFailToolWant := `{"jsonrpc":"2.0","id":"invoke-fail-tool","result":{"content":[{"type":"text","text":"final query validation failed: failed to insert dry run job: googleapi: Error 400: Syntax error: Unexpected identifier \"SELEC\" at [1:1]`
|
||||
mcpSelect1Want := `{"jsonrpc":"2.0","id":"invoke my-auth-required-tool","result":{"content":[{"type":"text","text":"{\"f0_\":1}"}]}}`
|
||||
createColArray := `["id INT64", "name STRING", "age INT64"]`
|
||||
selectEmptyWant := `"The query returned 0 rows."`
|
||||
|
||||
// Run tests
|
||||
tests.RunToolGetTest(t)
|
||||
tests.RunToolInvokeTest(t, select1Want, tests.DisableOptionalNullParamTest(), tests.EnableClientAuthTest())
|
||||
tests.RunMCPToolCallMethod(t, mcpMyFailToolWant, tests.EnableMcpClientAuthTest())
|
||||
tests.RunMCPToolCallMethod(t, mcpMyFailToolWant, mcpSelect1Want, tests.EnableMcpClientAuthTest())
|
||||
tests.RunToolInvokeWithTemplateParameters(t, tableNameTemplateParam,
|
||||
tests.WithCreateColArray(createColArray),
|
||||
tests.WithDdlWant(ddlWant),
|
||||
@@ -439,6 +441,20 @@ func addBigQueryPrebuiltToolsConfig(t *testing.T, config map[string]any) map[str
|
||||
return config
|
||||
}
|
||||
|
||||
func addClientAuthSourceConfig(t *testing.T, config map[string]any) map[string]any {
|
||||
sources, ok := config["sources"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("unable to get sources from config")
|
||||
}
|
||||
sources["my-client-auth-source"] = map[string]any{
|
||||
"kind": BigquerySourceKind,
|
||||
"project": BigqueryProject,
|
||||
"useClientOAuth": true,
|
||||
}
|
||||
config["sources"] = sources
|
||||
return config
|
||||
}
|
||||
|
||||
func addBigQuerySqlToolConfig(t *testing.T, config map[string]any, toolStatement, arrayToolStatement string) map[string]any {
|
||||
tools, ok := config["tools"].(map[string]any)
|
||||
if !ok {
|
||||
@@ -469,11 +485,10 @@ func addBigQuerySqlToolConfig(t *testing.T, config map[string]any, toolStatement
|
||||
},
|
||||
}
|
||||
tools["my-client-auth-tool"] = map[string]any{
|
||||
"kind": "bigquery-sql",
|
||||
"source": "my-instance",
|
||||
"description": "Tool to test client authorization.",
|
||||
"useClientOAuth": true,
|
||||
"statement": "SELECT 1",
|
||||
"kind": "bigquery-sql",
|
||||
"source": "my-client-auth-source",
|
||||
"description": "Tool to test client authorization.",
|
||||
"statement": "SELECT 1",
|
||||
}
|
||||
config["tools"] = tools
|
||||
return config
|
||||
|
||||
@@ -121,6 +121,7 @@ func TestBigtableToolEndpoints(t *testing.T) {
|
||||
select1Want := "[{\"$col1\":1}]"
|
||||
myToolById4Want := `[{"id":4,"name":""}]`
|
||||
mcpMyFailToolWant := `{"jsonrpc":"2.0","id":"invoke-fail-tool","result":{"content":[{"type":"text","text":"unable to prepare statement: rpc error: code = InvalidArgument desc = Syntax error: Unexpected identifier \"SELEC\" [at 1:1]"}],"isError":true}}`
|
||||
mcpSelect1Want := `{"jsonrpc":"2.0","id":"invoke my-auth-required-tool","result":{"content":[{"type":"text","text":"{\"$col1\":1}"}]}}`
|
||||
nameFieldArray := `["CAST(cf['name'] AS string) as name"]`
|
||||
nameColFilter := "CAST(cf['name'] AS string)"
|
||||
|
||||
@@ -129,7 +130,7 @@ func TestBigtableToolEndpoints(t *testing.T) {
|
||||
tests.RunToolInvokeTest(t, select1Want,
|
||||
tests.WithMyToolById4Want(myToolById4Want),
|
||||
)
|
||||
tests.RunMCPToolCallMethod(t, mcpMyFailToolWant)
|
||||
tests.RunMCPToolCallMethod(t, mcpMyFailToolWant, mcpSelect1Want)
|
||||
tests.RunToolInvokeWithTemplateParameters(t, tableNameTemplateParam,
|
||||
tests.WithNameFieldArray(nameFieldArray),
|
||||
tests.WithNameColFilter(nameColFilter),
|
||||
|
||||
1014
tests/clickhouse/clickhouse_integration_test.go
Normal file
1014
tests/clickhouse/clickhouse_integration_test.go
Normal file
File diff suppressed because it is too large
Load Diff
@@ -159,12 +159,12 @@ func TestCloudSQLMSSQLToolEndpoints(t *testing.T) {
|
||||
}
|
||||
|
||||
// Get configs for tests
|
||||
select1Want, mcpMyFailToolWant, createTableStatement := tests.GetMSSQLWants()
|
||||
select1Want, mcpMyFailToolWant, createTableStatement, mcpSelect1Want := tests.GetMSSQLWants()
|
||||
|
||||
// Run tests
|
||||
tests.RunToolGetTest(t)
|
||||
tests.RunToolInvokeTest(t, select1Want, tests.DisableArrayTest())
|
||||
tests.RunMCPToolCallMethod(t, mcpMyFailToolWant)
|
||||
tests.RunMCPToolCallMethod(t, mcpMyFailToolWant, mcpSelect1Want)
|
||||
tests.RunExecuteSqlToolInvokeTest(t, createTableStatement, select1Want)
|
||||
tests.RunToolInvokeWithTemplateParameters(t, tableNameTemplateParam)
|
||||
}
|
||||
|
||||
@@ -146,12 +146,12 @@ func TestCloudSQLMySQLToolEndpoints(t *testing.T) {
|
||||
}
|
||||
|
||||
// Get configs for tests
|
||||
select1Want, mcpMyFailToolWant, createTableStatement := tests.GetMySQLWants()
|
||||
select1Want, mcpMyFailToolWant, createTableStatement, mcpSelect1Want := tests.GetMySQLWants()
|
||||
|
||||
// Run tests
|
||||
tests.RunToolGetTest(t)
|
||||
tests.RunToolInvokeTest(t, select1Want, tests.DisableArrayTest())
|
||||
tests.RunMCPToolCallMethod(t, mcpMyFailToolWant)
|
||||
tests.RunMCPToolCallMethod(t, mcpMyFailToolWant, mcpSelect1Want)
|
||||
tests.RunExecuteSqlToolInvokeTest(t, createTableStatement, select1Want)
|
||||
tests.RunToolInvokeWithTemplateParameters(t, tableNameTemplateParam)
|
||||
}
|
||||
|
||||
@@ -150,12 +150,12 @@ func TestCloudSQLPgSimpleToolEndpoints(t *testing.T) {
|
||||
}
|
||||
|
||||
// Get configs for tests
|
||||
select1Want, mcpMyFailToolWant, createTableStatement := tests.GetPostgresWants()
|
||||
select1Want, mcpMyFailToolWant, createTableStatement, mcpSelect1Want := tests.GetPostgresWants()
|
||||
|
||||
// Run tests
|
||||
tests.RunToolGetTest(t)
|
||||
tests.RunToolInvokeTest(t, select1Want)
|
||||
tests.RunMCPToolCallMethod(t, mcpMyFailToolWant)
|
||||
tests.RunMCPToolCallMethod(t, mcpMyFailToolWant, mcpSelect1Want)
|
||||
tests.RunExecuteSqlToolInvokeTest(t, createTableStatement, select1Want)
|
||||
tests.RunToolInvokeWithTemplateParameters(t, tableNameTemplateParam)
|
||||
}
|
||||
|
||||
@@ -400,27 +400,30 @@ func GetMySQLTmplToolStatement() (string, string) {
|
||||
}
|
||||
|
||||
// GetPostgresWants return the expected wants for postgres
|
||||
func GetPostgresWants() (string, string, string) {
|
||||
func GetPostgresWants() (string, string, string, string) {
|
||||
select1Want := "[{\"?column?\":1}]"
|
||||
mcpMyFailToolWant := `{"jsonrpc":"2.0","id":"invoke-fail-tool","result":{"content":[{"type":"text","text":"unable to execute query: ERROR: syntax error at or near \"SELEC\" (SQLSTATE 42601)"}],"isError":true}}`
|
||||
createTableStatement := `"CREATE TABLE t (id SERIAL PRIMARY KEY, name TEXT)"`
|
||||
return select1Want, mcpMyFailToolWant, createTableStatement
|
||||
mcpSelect1Want := `{"jsonrpc":"2.0","id":"invoke my-auth-required-tool","result":{"content":[{"type":"text","text":"{\"?column?\":1}"}]}}`
|
||||
return select1Want, mcpMyFailToolWant, createTableStatement, mcpSelect1Want
|
||||
}
|
||||
|
||||
// GetMSSQLWants return the expected wants for mssql
|
||||
func GetMSSQLWants() (string, string, string) {
|
||||
func GetMSSQLWants() (string, string, string, string) {
|
||||
select1Want := "[{\"\":1}]"
|
||||
mcpMyFailToolWant := `{"jsonrpc":"2.0","id":"invoke-fail-tool","result":{"content":[{"type":"text","text":"unable to execute query: mssql: Could not find stored procedure 'SELEC'."}],"isError":true}}`
|
||||
createTableStatement := `"CREATE TABLE t (id INT IDENTITY(1,1) PRIMARY KEY, name NVARCHAR(MAX))"`
|
||||
return select1Want, mcpMyFailToolWant, createTableStatement
|
||||
mcpSelect1Want := `{"jsonrpc":"2.0","id":"invoke my-auth-required-tool","result":{"content":[{"type":"text","text":"{\"\":1}"}]}}`
|
||||
return select1Want, mcpMyFailToolWant, createTableStatement, mcpSelect1Want
|
||||
}
|
||||
|
||||
// GetMySQLWants return the expected wants for mysql
|
||||
func GetMySQLWants() (string, string, string) {
|
||||
func GetMySQLWants() (string, string, string, string) {
|
||||
select1Want := "[{\"1\":1}]"
|
||||
mcpMyFailToolWant := `{"jsonrpc":"2.0","id":"invoke-fail-tool","result":{"content":[{"type":"text","text":"unable to execute query: Error 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'SELEC 1' at line 1"}],"isError":true}}`
|
||||
createTableStatement := `"CREATE TABLE t (id SERIAL PRIMARY KEY, name TEXT)"`
|
||||
return select1Want, mcpMyFailToolWant, createTableStatement
|
||||
mcpSelect1Want := `{"jsonrpc":"2.0","id":"invoke my-auth-required-tool","result":{"content":[{"type":"text","text":"{\"1\":1}"}]}}`
|
||||
return select1Want, mcpMyFailToolWant, createTableStatement, mcpSelect1Want
|
||||
}
|
||||
|
||||
// SetupPostgresSQLTable creates and inserts data into a table of tool
|
||||
@@ -511,14 +514,15 @@ func SetupMySQLTable(t *testing.T, ctx context.Context, pool *sql.DB, createStat
|
||||
}
|
||||
|
||||
// GetRedisWants return the expected wants for redis
|
||||
func GetRedisValkeyWants() (string, string, string, string, string, string) {
|
||||
func GetRedisValkeyWants() (string, string, string, string, string, string, string) {
|
||||
select1Want := "[\"PONG\"]"
|
||||
mcpMyFailToolWant := `unknown command 'SELEC 1;', with args beginning with: \""}]}}`
|
||||
invokeParamWant := "[{\"id\":\"1\",\"name\":\"Alice\"},{\"id\":\"3\",\"name\":\"Sid\"}]"
|
||||
invokeIdNullWant := `[{"id":"4","name":""}]`
|
||||
nullWant := `["null"]`
|
||||
mcpSelect1Want := `{"jsonrpc":"2.0","id":"invoke my-auth-required-tool","result":{"content":[{"type":"text","text":"\"PONG\""}]}}`
|
||||
mcpInvokeParamWant := `{"jsonrpc":"2.0","id":"my-tool","result":{"content":[{"type":"text","text":"{\"id\":\"1\",\"name\":\"Alice\"}"},{"type":"text","text":"{\"id\":\"3\",\"name\":\"Sid\"}"}]}}`
|
||||
return select1Want, mcpMyFailToolWant, invokeParamWant, invokeIdNullWant, nullWant, mcpInvokeParamWant
|
||||
return select1Want, mcpMyFailToolWant, invokeParamWant, invokeIdNullWant, nullWant, mcpSelect1Want, mcpInvokeParamWant
|
||||
}
|
||||
|
||||
func GetRedisValkeyToolsConfig(sourceConfig map[string]any, toolKind string) map[string]any {
|
||||
|
||||
@@ -138,13 +138,14 @@ func TestCouchbaseToolEndpoints(t *testing.T) {
|
||||
// Get configs for tests
|
||||
select1Want := "[{\"$1\":1}]"
|
||||
mcpMyFailToolWant := `{"jsonrpc":"2.0","id":"invoke-fail-tool","result":{"content":[{"type":"text","text":"unable to execute query: parsing failure | {\"statement\":\"SELEC 1;\"`
|
||||
mcpSelect1Want := `{"jsonrpc":"2.0","id":"invoke my-auth-required-tool","result":{"content":[{"type":"text","text":"{\"$1\":1}"}]}}`
|
||||
tmplSelectId1Want := "[{\"age\":21,\"id\":1,\"name\":\"Alex\"}]"
|
||||
selectAllWant := "[{\"age\":21,\"id\":1,\"name\":\"Alex\"},{\"age\":100,\"id\":2,\"name\":\"Alice\"}]"
|
||||
|
||||
// Run tests
|
||||
tests.RunToolGetTest(t)
|
||||
tests.RunToolInvokeTest(t, select1Want)
|
||||
tests.RunMCPToolCallMethod(t, mcpMyFailToolWant)
|
||||
tests.RunMCPToolCallMethod(t, mcpMyFailToolWant, mcpSelect1Want)
|
||||
tests.RunToolInvokeWithTemplateParameters(t, collectionNameTemplateParam,
|
||||
tests.WithTmplSelectId1Want(tmplSelectId1Want),
|
||||
tests.WithSelectAllWant(selectAllWant),
|
||||
|
||||
@@ -367,7 +367,7 @@ func runDataplexSearchEntriesToolInvokeTest(t *testing.T, tableName string, data
|
||||
name: "Success - Entry Found",
|
||||
api: "http://127.0.0.1:5000/api/tool/my-dataplex-search-entries-tool/invoke",
|
||||
requestHeader: map[string]string{},
|
||||
requestBody: bytes.NewBuffer([]byte(fmt.Sprintf("{\"query\":\"displayname=%s system=bigquery parent=%s\"}", tableName, datasetName))),
|
||||
requestBody: bytes.NewBuffer([]byte(fmt.Sprintf("{\"query\":\"displayname=%s system=bigquery parent:%s\"}", tableName, datasetName))),
|
||||
wantStatusCode: 200,
|
||||
expectResult: true,
|
||||
wantContentKey: "dataplex_entry",
|
||||
@@ -376,7 +376,7 @@ func runDataplexSearchEntriesToolInvokeTest(t *testing.T, tableName string, data
|
||||
name: "Success with Authorization - Entry Found",
|
||||
api: "http://127.0.0.1:5000/api/tool/my-auth-dataplex-search-entries-tool/invoke",
|
||||
requestHeader: map[string]string{"my-google-auth_token": idToken},
|
||||
requestBody: bytes.NewBuffer([]byte(fmt.Sprintf("{\"query\":\"displayname=%s system=bigquery parent=%s\"}", tableName, datasetName))),
|
||||
requestBody: bytes.NewBuffer([]byte(fmt.Sprintf("{\"query\":\"displayname=%s system=bigquery parent:%s\"}", tableName, datasetName))),
|
||||
wantStatusCode: 200,
|
||||
expectResult: true,
|
||||
wantContentKey: "dataplex_entry",
|
||||
@@ -385,7 +385,7 @@ func runDataplexSearchEntriesToolInvokeTest(t *testing.T, tableName string, data
|
||||
name: "Failure - Invalid Authorization Token",
|
||||
api: "http://127.0.0.1:5000/api/tool/my-auth-dataplex-search-entries-tool/invoke",
|
||||
requestHeader: map[string]string{"my-google-auth_token": "invalid_token"},
|
||||
requestBody: bytes.NewBuffer([]byte(fmt.Sprintf("{\"query\":\"displayname=%s system=bigquery parent=%s\"}", tableName, datasetName))),
|
||||
requestBody: bytes.NewBuffer([]byte(fmt.Sprintf("{\"query\":\"displayname=%s system=bigquery parent:%s\"}", tableName, datasetName))),
|
||||
wantStatusCode: 401,
|
||||
expectResult: false,
|
||||
wantContentKey: "dataplex_entry",
|
||||
@@ -394,7 +394,7 @@ func runDataplexSearchEntriesToolInvokeTest(t *testing.T, tableName string, data
|
||||
name: "Failure - Without Authorization Token",
|
||||
api: "http://127.0.0.1:5000/api/tool/my-auth-dataplex-search-entries-tool/invoke",
|
||||
requestHeader: map[string]string{},
|
||||
requestBody: bytes.NewBuffer([]byte(fmt.Sprintf("{\"query\":\"displayname=%s system=bigquery parent=%s\"}", tableName, datasetName))),
|
||||
requestBody: bytes.NewBuffer([]byte(fmt.Sprintf("{\"query\":\"displayname=%s system=bigquery parent:%s\"}", tableName, datasetName))),
|
||||
wantStatusCode: 401,
|
||||
expectResult: false,
|
||||
wantContentKey: "dataplex_entry",
|
||||
@@ -403,7 +403,7 @@ func runDataplexSearchEntriesToolInvokeTest(t *testing.T, tableName string, data
|
||||
name: "Failure - Entry Not Found",
|
||||
api: "http://127.0.0.1:5000/api/tool/my-dataplex-search-entries-tool/invoke",
|
||||
requestHeader: map[string]string{},
|
||||
requestBody: bytes.NewBuffer([]byte(`{"query":"displayname=\"\" system=bigquery parent=\"\""}`)),
|
||||
requestBody: bytes.NewBuffer([]byte(`{"query":"displayname=\"\" system=bigquery parent:\"\""}`)),
|
||||
wantStatusCode: 200,
|
||||
expectResult: false,
|
||||
wantContentKey: "",
|
||||
@@ -648,7 +648,7 @@ func runDataplexSearchAspectTypesToolInvokeTest(t *testing.T, aspectTypeId strin
|
||||
name: "Success - Aspect Type Found",
|
||||
api: "http://127.0.0.1:5000/api/tool/my-dataplex-search-aspect-types-tool/invoke",
|
||||
requestHeader: map[string]string{},
|
||||
requestBody: bytes.NewBuffer([]byte(fmt.Sprintf("{\"query\":\"name=%s_aspectType\"}", aspectTypeId))),
|
||||
requestBody: bytes.NewBuffer([]byte(fmt.Sprintf("{\"query\":\"name:%s_aspectType\"}", aspectTypeId))),
|
||||
wantStatusCode: 200,
|
||||
expectResult: true,
|
||||
wantContentKey: "metadata_template",
|
||||
@@ -657,7 +657,7 @@ func runDataplexSearchAspectTypesToolInvokeTest(t *testing.T, aspectTypeId strin
|
||||
name: "Success - Aspect Type Found with Authorization",
|
||||
api: "http://127.0.0.1:5000/api/tool/my-auth-dataplex-search-aspect-types-tool/invoke",
|
||||
requestHeader: map[string]string{"my-google-auth_token": idToken},
|
||||
requestBody: bytes.NewBuffer([]byte(fmt.Sprintf("{\"query\":\"name=%s_aspectType\"}", aspectTypeId))),
|
||||
requestBody: bytes.NewBuffer([]byte(fmt.Sprintf("{\"query\":\"name:%s_aspectType\"}", aspectTypeId))),
|
||||
wantStatusCode: 200,
|
||||
expectResult: true,
|
||||
wantContentKey: "metadata_template",
|
||||
@@ -666,7 +666,7 @@ func runDataplexSearchAspectTypesToolInvokeTest(t *testing.T, aspectTypeId strin
|
||||
name: "Failure - Aspect Type Not Found",
|
||||
api: "http://127.0.0.1:5000/api/tool/my-dataplex-search-aspect-types-tool/invoke",
|
||||
requestHeader: map[string]string{},
|
||||
requestBody: bytes.NewBuffer([]byte(`"{\"query\":\"name=_aspectType\"}"`)),
|
||||
requestBody: bytes.NewBuffer([]byte(`"{\"query\":\"name:_aspectType\"}"`)),
|
||||
wantStatusCode: 400,
|
||||
expectResult: false,
|
||||
},
|
||||
@@ -674,7 +674,7 @@ func runDataplexSearchAspectTypesToolInvokeTest(t *testing.T, aspectTypeId strin
|
||||
name: "Failure - Invalid Authorization Token",
|
||||
api: "http://127.0.0.1:5000/api/tool/my-auth-dataplex-search-aspect-types-tool/invoke",
|
||||
requestHeader: map[string]string{"my-google-auth_token": "invalid_token"},
|
||||
requestBody: bytes.NewBuffer([]byte(fmt.Sprintf("{\"query\":\"name=%s_aspectType\"}", aspectTypeId))),
|
||||
requestBody: bytes.NewBuffer([]byte(fmt.Sprintf("{\"query\":\"name:%s_aspectType\"}", aspectTypeId))),
|
||||
wantStatusCode: 401,
|
||||
expectResult: false,
|
||||
},
|
||||
@@ -682,7 +682,7 @@ func runDataplexSearchAspectTypesToolInvokeTest(t *testing.T, aspectTypeId strin
|
||||
name: "Failure - No Authorization Token",
|
||||
api: "http://127.0.0.1:5000/api/tool/my-auth-dataplex-search-aspect-types-tool/invoke",
|
||||
requestHeader: map[string]string{},
|
||||
requestBody: bytes.NewBuffer([]byte(fmt.Sprintf("{\"query\":\"name=%s_aspectType\"}", aspectTypeId))),
|
||||
requestBody: bytes.NewBuffer([]byte(fmt.Sprintf("{\"query\":\"name:%s_aspectType\"}", aspectTypeId))),
|
||||
wantStatusCode: 401,
|
||||
expectResult: false,
|
||||
},
|
||||
|
||||
@@ -127,7 +127,7 @@ func TestFirebirdToolEndpoints(t *testing.T) {
|
||||
}
|
||||
|
||||
// Get configs for tests
|
||||
select1Want, failInvocationWant, createTableStatement := getFirebirdWants()
|
||||
select1Want, mcpMyFailToolWant, createTableStatement, mcpSelect1Want := getFirebirdWants()
|
||||
nullWant := `[{"id":4,"name":null}]`
|
||||
select1Statement := `"SELECT 1 AS \"constant\" FROM RDB$DATABASE;"`
|
||||
templateParamCreateColArray := `["id INTEGER","name VARCHAR(255)","age INTEGER"]`
|
||||
@@ -137,7 +137,7 @@ func TestFirebirdToolEndpoints(t *testing.T) {
|
||||
tests.RunToolInvokeTest(t, select1Want,
|
||||
tests.WithNullWant(nullWant),
|
||||
tests.DisableArrayTest())
|
||||
tests.RunMCPToolCallMethod(t, failInvocationWant)
|
||||
tests.RunMCPToolCallMethod(t, mcpMyFailToolWant, mcpSelect1Want)
|
||||
tests.RunExecuteSqlToolInvokeTest(t, createTableStatement, select1Want, tests.WithSelect1Statement(select1Statement))
|
||||
tests.RunToolInvokeWithTemplateParameters(t, tableNameTemplateParam,
|
||||
tests.WithCreateColArray(templateParamCreateColArray))
|
||||
@@ -303,11 +303,12 @@ func getFirebirdAuthToolInfo(tableName string) ([]string, string, string, []any)
|
||||
return createStatements, insertStatement, toolStatement, params
|
||||
}
|
||||
|
||||
func getFirebirdWants() (string, string, string) {
|
||||
func getFirebirdWants() (string, string, string, string) {
|
||||
select1Want := `[{"constant":1}]`
|
||||
failInvocationWant := `{"jsonrpc":"2.0","id":"invoke-fail-tool","result":{"content":[{"type":"text","text":"unable to execute query: Dynamic SQL Error\nSQL error code = -104\nToken unknown - line 1, column 1\nSELEC\n"}],"isError":true}}`
|
||||
mcpMyFailToolWant := `{"jsonrpc":"2.0","id":"invoke-fail-tool","result":{"content":[{"type":"text","text":"unable to execute query: Dynamic SQL Error\nSQL error code = -104\nToken unknown - line 1, column 1\nSELEC\n"}],"isError":true}}`
|
||||
createTableStatement := `"CREATE TABLE t (id INTEGER PRIMARY KEY, name VARCHAR(50))"`
|
||||
return select1Want, failInvocationWant, createTableStatement
|
||||
mcpSelect1Want := `{"jsonrpc":"2.0","id":"invoke my-auth-required-tool","result":{"content":[{"type":"text","text":"{\"constant\":1}"}]}}`
|
||||
return select1Want, mcpMyFailToolWant, createTableStatement, mcpSelect1Want
|
||||
}
|
||||
|
||||
func getFirebirdToolsConfig(sourceConfig map[string]any, toolKind, paramToolStatement, idParamToolStmt, nameParamToolStmt, arrayToolStatement, authToolStatement string) map[string]any {
|
||||
|
||||
@@ -621,13 +621,13 @@ func TestLooker(t *testing.T) {
|
||||
wantResult := "{\"label\":\"System Activity\",\"name\":\"system__activity\",\"project_name\":\"system__activity\"}"
|
||||
tests.RunToolInvokeSimpleTest(t, "get_models", wantResult)
|
||||
|
||||
wantResult = "{\"group_label\":\"System Activity\",\"label\":\"Content Usage\",\"name\":\"content_usage\"}"
|
||||
wantResult = "{\"description\":\"Data about Look and dashboard usage, including frequency of views, favoriting, scheduling, embedding, and access via the API. Also includes details about individual Looks and dashboards.\",\"group_label\":\"System Activity\",\"label\":\"Content Usage\",\"name\":\"content_usage\"}"
|
||||
tests.RunToolInvokeParametersTest(t, "get_explores", []byte(`{"model": "system__activity"}`), wantResult)
|
||||
|
||||
wantResult = "{\"description\":\"Number of times this content has been viewed via the Looker API\",\"label\":\"Content Usage API Count\",\"label_short\":\"API Count\",\"name\":\"content_usage.api_count\",\"synonyms\":[],\"tags\":[],\"type\":\"number\"}"
|
||||
wantResult = "{\"description\":\"Number of times this content has been viewed via the Looker API\",\"label\":\"Content Usage API Count\",\"label_short\":\"API Count\",\"name\":\"content_usage.api_count\",\"type\":\"number\"}"
|
||||
tests.RunToolInvokeParametersTest(t, "get_dimensions", []byte(`{"model": "system__activity", "explore": "content_usage"}`), wantResult)
|
||||
|
||||
wantResult = "{\"description\":\"The total number of views via the Looker API\",\"label\":\"Content Usage API Total\",\"label_short\":\"API Total\",\"name\":\"content_usage.api_total\",\"synonyms\":[],\"tags\":[],\"type\":\"sum\"}"
|
||||
wantResult = "{\"description\":\"The total number of views via the Looker API\",\"label\":\"Content Usage API Total\",\"label_short\":\"API Total\",\"name\":\"content_usage.api_total\",\"type\":\"sum\"}"
|
||||
tests.RunToolInvokeParametersTest(t, "get_measures", []byte(`{"model": "system__activity", "explore": "content_usage"}`), wantResult)
|
||||
|
||||
wantResult = "[]"
|
||||
|
||||
@@ -112,7 +112,7 @@ func TestMongoDBToolEndpoints(t *testing.T) {
|
||||
tests.WithMyToolId3NameAliceWant(myToolId3NameAliceWant),
|
||||
tests.WithMyToolById4Want(myToolById4Want),
|
||||
)
|
||||
tests.RunMCPToolCallMethod(t, mcpMyFailToolWant,
|
||||
tests.RunMCPToolCallMethod(t, mcpMyFailToolWant, select1Want,
|
||||
tests.WithMcpMyToolId3NameAliceWant(mcpMyToolId3NameAliceWant),
|
||||
)
|
||||
|
||||
|
||||
@@ -132,12 +132,12 @@ func TestMSSQLToolEndpoints(t *testing.T) {
|
||||
}
|
||||
|
||||
// Get configs for tests
|
||||
select1Want, mcpMyFailToolWant, createTableStatement := tests.GetMSSQLWants()
|
||||
select1Want, mcpMyFailToolWant, createTableStatement, mcpSelect1Want := tests.GetMSSQLWants()
|
||||
|
||||
// Run tests
|
||||
tests.RunToolGetTest(t)
|
||||
tests.RunToolInvokeTest(t, select1Want, tests.DisableArrayTest())
|
||||
tests.RunMCPToolCallMethod(t, mcpMyFailToolWant)
|
||||
tests.RunMCPToolCallMethod(t, mcpMyFailToolWant, mcpSelect1Want)
|
||||
tests.RunExecuteSqlToolInvokeTest(t, createTableStatement, select1Want)
|
||||
tests.RunToolInvokeWithTemplateParameters(t, tableNameTemplateParam)
|
||||
}
|
||||
|
||||
@@ -123,12 +123,12 @@ func TestMySQLToolEndpoints(t *testing.T) {
|
||||
}
|
||||
|
||||
// Get configs for tests
|
||||
select1Want, mcpMyFailToolWant, createTableStatement := tests.GetMySQLWants()
|
||||
select1Want, mcpMyFailToolWant, createTableStatement, mcpSelect1Want := tests.GetMySQLWants()
|
||||
|
||||
// Run tests
|
||||
tests.RunToolGetTest(t)
|
||||
tests.RunToolInvokeTest(t, select1Want, tests.DisableArrayTest())
|
||||
tests.RunMCPToolCallMethod(t, mcpMyFailToolWant)
|
||||
tests.RunMCPToolCallMethod(t, mcpMyFailToolWant, mcpSelect1Want)
|
||||
tests.RunExecuteSqlToolInvokeTest(t, createTableStatement, select1Want)
|
||||
tests.RunToolInvokeWithTemplateParameters(t, tableNameTemplateParam)
|
||||
}
|
||||
|
||||
@@ -125,12 +125,12 @@ func TestOceanBaseToolEndpoints(t *testing.T) {
|
||||
}
|
||||
|
||||
// Get configs for tests
|
||||
select1Want, mcpMyFailToolWant, createTableStatement := getOceanBaseWants()
|
||||
select1Want, mcpMyFailToolWant, createTableStatement, mcpSelect1Want := getOceanBaseWants()
|
||||
|
||||
// Run tests
|
||||
tests.RunToolGetTest(t)
|
||||
tests.RunToolInvokeTest(t, select1Want, tests.DisableArrayTest())
|
||||
tests.RunMCPToolCallMethod(t, mcpMyFailToolWant)
|
||||
tests.RunMCPToolCallMethod(t, mcpMyFailToolWant, mcpSelect1Want)
|
||||
tests.RunExecuteSqlToolInvokeTest(t, createTableStatement, select1Want)
|
||||
tests.RunToolInvokeWithTemplateParameters(t, tableNameTemplateParam)
|
||||
}
|
||||
@@ -164,11 +164,12 @@ func getOceanBaseTmplToolStatement() (string, string) {
|
||||
}
|
||||
|
||||
// OceanBase specific expected results
|
||||
func getOceanBaseWants() (string, string, string) {
|
||||
func getOceanBaseWants() (string, string, string, string) {
|
||||
select1Want := "[{\"1\":1}]"
|
||||
mcpMyFailToolWant := `{"jsonrpc":"2.0","id":"invoke-fail-tool","result":{"content":[{"type":"text","text":"unable to execute query: Error 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your OceanBase version for the right syntax to use near 'SELEC 1;' at line 1"}],"isError":true}}`
|
||||
createTableStatement := `"CREATE TABLE t (id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255))"`
|
||||
return select1Want, mcpMyFailToolWant, createTableStatement
|
||||
mcpSelect1Want := `{"jsonrpc":"2.0","id":"invoke my-auth-required-tool","result":{"content":[{"type":"text","text":"{\"1\":1}"}]}}`
|
||||
return select1Want, mcpMyFailToolWant, createTableStatement, mcpSelect1Want
|
||||
}
|
||||
|
||||
// Add OceanBase Execute SQL configuration
|
||||
|
||||
@@ -129,12 +129,12 @@ func TestPostgres(t *testing.T) {
|
||||
}
|
||||
|
||||
// Get configs for tests
|
||||
select1Want, mcpMyFailToolWant, createTableStatement := tests.GetPostgresWants()
|
||||
select1Want, mcpMyFailToolWant, createTableStatement, mcpSelect1Want := tests.GetPostgresWants()
|
||||
|
||||
// Run tests
|
||||
tests.RunToolGetTest(t)
|
||||
tests.RunToolInvokeTest(t, select1Want)
|
||||
tests.RunMCPToolCallMethod(t, mcpMyFailToolWant)
|
||||
tests.RunMCPToolCallMethod(t, mcpMyFailToolWant, mcpSelect1Want)
|
||||
tests.RunExecuteSqlToolInvokeTest(t, createTableStatement, select1Want)
|
||||
tests.RunToolInvokeWithTemplateParameters(t, tableNameTemplateParam)
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@ func TestRedisToolEndpoints(t *testing.T) {
|
||||
}
|
||||
|
||||
// Get configs for tests
|
||||
select1Want, mcpMyFailToolWant, invokeParamWant, invokeIdNullWant, nullWant, mcpInvokeParamWant := tests.GetRedisValkeyWants()
|
||||
select1Want, mcpMyFailToolWant, invokeParamWant, invokeIdNullWant, nullWant, mcpSelect1Want, mcpInvokeParamWant := tests.GetRedisValkeyWants()
|
||||
|
||||
// Run tests
|
||||
tests.RunToolGetTest(t)
|
||||
@@ -107,7 +107,7 @@ func TestRedisToolEndpoints(t *testing.T) {
|
||||
tests.WithMyToolById4Want(invokeIdNullWant),
|
||||
tests.WithNullWant(nullWant),
|
||||
)
|
||||
tests.RunMCPToolCallMethod(t, mcpMyFailToolWant,
|
||||
tests.RunMCPToolCallMethod(t, mcpMyFailToolWant, mcpSelect1Want,
|
||||
tests.WithMcpMyToolId3NameAliceWant(mcpInvokeParamWant),
|
||||
)
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user