diff --git a/cmd/internal/imports.go b/cmd/internal/imports.go index 6b34dfa5938c..478f21a51f3b 100644 --- a/cmd/internal/imports.go +++ b/cmd/internal/imports.go @@ -119,11 +119,13 @@ import ( _ "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookeradddashboardfilter" _ "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookerconversationalanalytics" _ "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookercreateagent" + _ "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookercreateconversation" _ "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookercreategitbranch" _ "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookercreateprojectdirectory" _ "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookercreateprojectfile" _ "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookercreateviewfromtable" _ "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookerdeleteagent" + _ "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookerdeleteconversation" _ "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookerdeletegitbranch" _ "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookerdeleteprojectdirectory" _ "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookerdeleteprojectfile" @@ -135,6 +137,7 @@ import ( _ "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookergetconnectionschemas" _ "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookergetconnectiontablecolumns" _ "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookergetconnectiontables" + _ "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookergetconversation" _ "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookergetdashboards" _ "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookergetdimensions" _ "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookergetexplores" @@ -153,6 +156,7 @@ import ( _ "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookerhealthpulse" _ "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookerhealthvacuum" _ "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookerlistagents" + _ "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookerlistconversations" _ "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookerlistgitbranches" _ "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookermakedashboard" _ "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookermakelook" @@ -164,6 +168,7 @@ import ( _ "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookerrunlookmltests" _ "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookerswitchgitbranch" _ "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookerupdateagent" + _ "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookerupdateconversation" _ "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookerupdateprojectfile" _ "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookervalidateproject" _ "github.com/googleapis/mcp-toolbox/internal/tools/mindsdb/mindsdbexecutesql" diff --git a/docs/en/integrations/looker/tools/looker-create-conversation.md b/docs/en/integrations/looker/tools/looker-create-conversation.md new file mode 100644 index 000000000000..805ec97aab18 --- /dev/null +++ b/docs/en/integrations/looker/tools/looker-create-conversation.md @@ -0,0 +1,49 @@ +--- +title: "looker-create-conversation Tool" +type: docs +weight: 1 +description: > + "looker-create-conversation" tool starts a new conversation session with an AI agent. + +--- + +## About + +The `looker-create-conversation` tool initiates a new conversation session +associated with a specific Looker AI agent. + +Creating a conversation is the first step in interacting with an AI agent. +The tool returns a conversation object, including a `conversation_id` which +is required for subsequent messages and chat interactions. + +## Compatible Sources + +{{< compatible-sources >}} + +## Parameters + +- `agent_id` (required): The unique ID of the AI agent to associate with the conversation. +- `name` (optional): A descriptive name for the conversation. + +## Example + +```yaml +kind: tool +name: create_conversation +type: looker-create-conversation +source: looker-source +description: | + Start a new conversation session with an AI agent. + Required Parameter: + - agent_id: The ID of the agent to use. + Optional Parameter: + - name: A name to identify this conversation. +``` + +## Reference + +| **field** | **type** | **required** | **description** | +|-------------|:--------:|:------------:|----------------------------------------------------| +| type | string | true | Must be "looker-create-conversation" | +| source | string | true | Name of the Looker source to use. | +| description | string | true | Description of the tool that is passed to the LLM. | diff --git a/docs/en/integrations/looker/tools/looker-delete-conversation.md b/docs/en/integrations/looker/tools/looker-delete-conversation.md new file mode 100644 index 000000000000..98780b1fe813 --- /dev/null +++ b/docs/en/integrations/looker/tools/looker-delete-conversation.md @@ -0,0 +1,44 @@ +--- +title: "looker-delete-conversation Tool" +type: docs +weight: 1 +description: > + "looker-delete-conversation" tool deletes a conversation session. + +--- + +## About + +The `looker-delete-conversation` tool removes a conversation session. + +Once deleted, the conversation and its message history are no longer +accessible via the API. + +## Compatible Sources + +{{< compatible-sources >}} + +## Parameters + +- `conversation_id` (required): The unique ID of the conversation to delete. + +## Example + +```yaml +kind: tool +name: delete_conversation +type: looker-delete-conversation +source: looker-source +description: | + Delete a conversation and its history. + Required Parameter: + - conversation_id: The ID of the conversation. +``` + +## Reference + +| **field** | **type** | **required** | **description** | +|-------------|:--------:|:------------:|----------------------------------------------------| +| type | string | true | Must be "looker-delete-conversation" | +| source | string | true | Name of the Looker source to use. | +| description | string | true | Description of the tool that is passed to the LLM. | diff --git a/docs/en/integrations/looker/tools/looker-get-conversation.md b/docs/en/integrations/looker/tools/looker-get-conversation.md new file mode 100644 index 000000000000..f6d181eb1a1a --- /dev/null +++ b/docs/en/integrations/looker/tools/looker-get-conversation.md @@ -0,0 +1,45 @@ +--- +title: "looker-get-conversation Tool" +type: docs +weight: 1 +description: > + "looker-get-conversation" tool retrieves detailed information about a specific conversation. + +--- + +## About + +The `looker-get-conversation` tool retrieves details for a single conversation +by its ID. + +This is useful for checking the current status or metadata of a +conversation before interacting with it. + +## Compatible Sources + +{{< compatible-sources >}} + +## Parameters + +- `conversation_id` (required): The unique ID of the conversation to retrieve. + +## Example + +```yaml +kind: tool +name: get_conversation +type: looker-get-conversation +source: looker-source +description: | + Retrieve detailed information about a single conversation. + Required Parameter: + - conversation_id: The ID of the conversation to get. +``` + +## Reference + +| **field** | **type** | **required** | **description** | +|-------------|:--------:|:------------:|----------------------------------------------------| +| type | string | true | Must be "looker-get-conversation" | +| source | string | true | Name of the Looker source to use. | +| description | string | true | Description of the tool that is passed to the LLM. | diff --git a/docs/en/integrations/looker/tools/looker-list-conversations.md b/docs/en/integrations/looker/tools/looker-list-conversations.md new file mode 100644 index 000000000000..6c406f42c32c --- /dev/null +++ b/docs/en/integrations/looker/tools/looker-list-conversations.md @@ -0,0 +1,49 @@ +--- +title: "looker-list-conversations Tool" +type: docs +weight: 1 +description: > + "looker-list-conversations" tool lists existing conversation sessions. + +--- + +## About + +The `looker-list-conversations` tool retrieves a list of previously +created conversation sessions. + +Users can optionally filter the results by `agent_id` or use `limit` and +`offset` for pagination. + +## Compatible Sources + +{{< compatible-sources >}} + +## Parameters + +- `agent_id` (optional): Filter conversations by a specific AI agent ID. +- `limit` (optional): The maximum number of conversations to fetch. Default is 100. +- `offset` (optional): The number of conversations to skip before fetching. Default is 0. + +## Example + +```yaml +kind: tool +name: list_conversations +type: looker-list-conversations +source: looker-source +description: | + Search for existing conversation sessions. + Optional Parameters: + - agent_id: Filter by agent ID. + - limit: Pagination limit. + - offset: Pagination offset. +``` + +## Reference + +| **field** | **type** | **required** | **description** | +|-------------|:--------:|:------------:|----------------------------------------------------| +| type | string | true | Must be "looker-list-conversations" | +| source | string | true | Name of the Looker source to use. | +| description | string | true | Description of the tool that is passed to the LLM. | diff --git a/docs/en/integrations/looker/tools/looker-update-conversation.md b/docs/en/integrations/looker/tools/looker-update-conversation.md new file mode 100644 index 000000000000..e3cabf9f69b0 --- /dev/null +++ b/docs/en/integrations/looker/tools/looker-update-conversation.md @@ -0,0 +1,46 @@ +--- +title: "looker-update-conversation Tool" +type: docs +weight: 1 +description: > + "looker-update-conversation" tool updates an existing conversation's attributes. + +--- + +## About + +The `looker-update-conversation` tool modifies attributes of an +existing conversation. + +Common uses include renaming a conversation to better reflect its content. + +## Compatible Sources + +{{< compatible-sources >}} + +## Parameters + +- `conversation_id` (required): The unique ID of the conversation to update. +- `name` (required): The new name for the conversation. + +## Example + +```yaml +kind: tool +name: update_conversation +type: looker-update-conversation +source: looker-source +description: | + Update an existing conversation's name. + Required Parameters: + - conversation_id: The ID of the conversation. + - name: The new name. +``` + +## Reference + +| **field** | **type** | **required** | **description** | +|-------------|:--------:|:------------:|----------------------------------------------------| +| type | string | true | Must be "looker-update-conversation" | +| source | string | true | Name of the Looker source to use. | +| description | string | true | Description of the tool that is passed to the LLM. | diff --git a/internal/tools/looker/lookercreateconversation/lookercreateconversation.go b/internal/tools/looker/lookercreateconversation/lookercreateconversation.go new file mode 100644 index 000000000000..607face5d3ad --- /dev/null +++ b/internal/tools/looker/lookercreateconversation/lookercreateconversation.go @@ -0,0 +1,185 @@ +// 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 lookercreateconversation + +import ( + "context" + "fmt" + "net/http" + + yaml "github.com/goccy/go-yaml" + "github.com/googleapis/mcp-toolbox/internal/embeddingmodels" + "github.com/googleapis/mcp-toolbox/internal/sources" + "github.com/googleapis/mcp-toolbox/internal/tools" + "github.com/googleapis/mcp-toolbox/internal/util" + "github.com/googleapis/mcp-toolbox/internal/util/parameters" + + "github.com/looker-open-source/sdk-codegen/go/rtl" + v4 "github.com/looker-open-source/sdk-codegen/go/sdk/v4" +) + +const resourceType string = "looker-create-conversation" + +func init() { + if !tools.Register(resourceType, newConfig) { + panic(fmt.Sprintf("tool type %q already registered", resourceType)) + } +} + +func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.ToolConfig, error) { + actual := Config{Name: name} + if err := decoder.DecodeContext(ctx, &actual); err != nil { + return nil, err + } + return actual, nil +} + +type compatibleSource interface { + UseClientAuthorization() bool + GetAuthTokenHeaderName() string + LookerApiSettings() *rtl.ApiSettings + GetLookerSDK(string) (*v4.LookerSDK, error) +} + +type Config struct { + Name string `yaml:"name" validate:"required"` + Type string `yaml:"type" validate:"required"` + Source string `yaml:"source" validate:"required"` + Description string `yaml:"description" validate:"required"` + AuthRequired []string `yaml:"authRequired"` + Annotations *tools.ToolAnnotations `yaml:"annotations,omitempty"` +} + +// validate interface +var _ tools.ToolConfig = Config{} + +func (cfg Config) ToolConfigType() string { + return resourceType +} + +func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) { + agentIdParameter := parameters.NewStringParameter("agent_id", "The ID of the agent to associate with the conversation.") + nameParameter := parameters.NewStringParameterWithDefault("name", "", "The name of the conversation.") + params := parameters.Parameters{ + agentIdParameter, + nameParameter, + } + + readOnlyHint := false + destructiveHint := true + annotations := &tools.ToolAnnotations{ + ReadOnlyHint: &readOnlyHint, + DestructiveHint: &destructiveHint, + } + + mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, params, annotations) + + // finish tool setup + return Tool{ + Config: cfg, + Parameters: params, + manifest: tools.Manifest{ + Description: cfg.Description, + Parameters: params.Manifest(), + AuthRequired: cfg.AuthRequired, + }, + mcpManifest: mcpManifest, + }, nil +} + +// validate interface +var _ tools.Tool = Tool{} + +type Tool struct { + Config + Parameters parameters.Parameters `yaml:"parameters"` + manifest tools.Manifest + mcpManifest tools.McpManifest +} + +func (t Tool) ToConfig() tools.ToolConfig { + return t.Config +} + +func (t Tool) Invoke(ctx context.Context, resourceMgr tools.SourceProvider, params parameters.ParamValues, accessToken tools.AccessToken) (any, util.ToolboxError) { + source, err := tools.GetCompatibleSource[compatibleSource](resourceMgr, t.Source, t.Name, t.Type) + if err != nil { + return nil, util.NewClientServerError("source used is not compatible with the tool", http.StatusInternalServerError, err) + } + + logger, err := util.LoggerFromContext(ctx) + if err != nil { + return nil, util.NewClientServerError("unable to get logger from ctx", http.StatusInternalServerError, err) + } + paramsMap := params.AsMap() + agentId := paramsMap["agent_id"].(string) + name := paramsMap["name"].(string) + + sdk, err := source.GetLookerSDK(string(accessToken)) + if err != nil { + return nil, util.NewClientServerError("error getting sdk", http.StatusInternalServerError, err) + } + + body := v4.WriteConversation{ + AgentId: &agentId, + } + if name != "" { + body.Name = &name + } + + logger.InfoContext(ctx, "Making CreateConversation request", "agent_id", agentId) + resp, err := sdk.CreateConversation(body, "", source.LookerApiSettings()) + if err != nil { + return nil, util.ProcessGeneralError(err) + } + logger.InfoContext(ctx, "Got response", "response", resp) + + return resp, nil +} + +func (t Tool) EmbedParams(ctx context.Context, paramValues parameters.ParamValues, embeddingModelsMap map[string]embeddingmodels.EmbeddingModel) (parameters.ParamValues, error) { + return parameters.EmbedParams(ctx, t.Parameters, paramValues, embeddingModelsMap, nil) +} + +func (t Tool) Manifest() tools.Manifest { + return t.manifest +} + +func (t Tool) McpManifest() tools.McpManifest { + return t.mcpManifest +} + +func (t Tool) RequiresClientAuthorization(resourceMgr tools.SourceProvider) (bool, error) { + source, err := tools.GetCompatibleSource[compatibleSource](resourceMgr, t.Source, t.Name, t.Type) + if err != nil { + return false, err + } + return source.UseClientAuthorization(), nil +} + +func (t Tool) Authorized(verifiedAuthServices []string) bool { + return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices) +} + +func (t Tool) GetAuthTokenHeaderName(resourceMgr tools.SourceProvider) (string, error) { + source, err := tools.GetCompatibleSource[compatibleSource](resourceMgr, t.Source, t.Name, t.Type) + if err != nil { + return "", err + } + return source.GetAuthTokenHeaderName(), nil +} + +func (t Tool) GetParameters() parameters.Parameters { + return t.Parameters +} diff --git a/internal/tools/looker/lookercreateconversation/lookercreateconversation_test.go b/internal/tools/looker/lookercreateconversation/lookercreateconversation_test.go new file mode 100644 index 000000000000..1fb814deaa5c --- /dev/null +++ b/internal/tools/looker/lookercreateconversation/lookercreateconversation_test.go @@ -0,0 +1,68 @@ +// 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 lookercreateconversation_test + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/googleapis/mcp-toolbox/internal/server" + "github.com/googleapis/mcp-toolbox/internal/testutils" + lkr "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookercreateconversation" +) + +func TestParseFromYamlLookerCreateConversation(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: ` + kind: tool + name: example_tool + type: looker-create-conversation + source: my-instance + description: some description + `, + want: server.ToolConfigs{ + "example_tool": lkr.Config{ + Name: "example_tool", + Type: "looker-create-conversation", + Source: "my-instance", + Description: "some description", + AuthRequired: []string{}, + }, + }, + }, + } + for _, tc := range tcs { + t.Run(tc.desc, func(t *testing.T) { + // Parse contents + _, _, _, got, _, _, err := server.UnmarshalResourceConfig(ctx, testutils.FormatYaml(tc.in)) + if err != nil { + t.Fatalf("unable to unmarshal: %s", err) + } + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Fatalf("incorrect parse: diff %v", diff) + } + }) + } +} diff --git a/internal/tools/looker/lookerdeleteconversation/lookerdeleteconversation.go b/internal/tools/looker/lookerdeleteconversation/lookerdeleteconversation.go new file mode 100644 index 000000000000..07e2e5d3090c --- /dev/null +++ b/internal/tools/looker/lookerdeleteconversation/lookerdeleteconversation.go @@ -0,0 +1,175 @@ +// 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 lookerdeleteconversation + +import ( + "context" + "fmt" + "net/http" + + yaml "github.com/goccy/go-yaml" + "github.com/googleapis/mcp-toolbox/internal/embeddingmodels" + "github.com/googleapis/mcp-toolbox/internal/sources" + "github.com/googleapis/mcp-toolbox/internal/tools" + "github.com/googleapis/mcp-toolbox/internal/util" + "github.com/googleapis/mcp-toolbox/internal/util/parameters" + + "github.com/looker-open-source/sdk-codegen/go/rtl" + v4 "github.com/looker-open-source/sdk-codegen/go/sdk/v4" +) + +const resourceType string = "looker-delete-conversation" + +func init() { + if !tools.Register(resourceType, newConfig) { + panic(fmt.Sprintf("tool type %q already registered", resourceType)) + } +} + +func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.ToolConfig, error) { + actual := Config{Name: name} + if err := decoder.DecodeContext(ctx, &actual); err != nil { + return nil, err + } + return actual, nil +} + +type compatibleSource interface { + UseClientAuthorization() bool + GetAuthTokenHeaderName() string + LookerApiSettings() *rtl.ApiSettings + GetLookerSDK(string) (*v4.LookerSDK, error) +} + +type Config struct { + Name string `yaml:"name" validate:"required"` + Type string `yaml:"type" validate:"required"` + Source string `yaml:"source" validate:"required"` + Description string `yaml:"description" validate:"required"` + AuthRequired []string `yaml:"authRequired"` + Annotations *tools.ToolAnnotations `yaml:"annotations,omitempty"` +} + +// validate interface +var _ tools.ToolConfig = Config{} + +func (cfg Config) ToolConfigType() string { + return resourceType +} + +func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) { + conversationIdParameter := parameters.NewStringParameter("conversation_id", "The ID of the conversation to delete.") + params := parameters.Parameters{ + conversationIdParameter, + } + + readOnlyHint := false + destructiveHint := true + annotations := &tools.ToolAnnotations{ + ReadOnlyHint: &readOnlyHint, + DestructiveHint: &destructiveHint, + } + + mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, params, annotations) + + // finish tool setup + return Tool{ + Config: cfg, + Parameters: params, + manifest: tools.Manifest{ + Description: cfg.Description, + Parameters: params.Manifest(), + AuthRequired: cfg.AuthRequired, + }, + mcpManifest: mcpManifest, + }, nil +} + +// validate interface +var _ tools.Tool = Tool{} + +type Tool struct { + Config + Parameters parameters.Parameters `yaml:"parameters"` + manifest tools.Manifest + mcpManifest tools.McpManifest +} + +func (t Tool) ToConfig() tools.ToolConfig { + return t.Config +} + +func (t Tool) Invoke(ctx context.Context, resourceMgr tools.SourceProvider, params parameters.ParamValues, accessToken tools.AccessToken) (any, util.ToolboxError) { + source, err := tools.GetCompatibleSource[compatibleSource](resourceMgr, t.Source, t.Name, t.Type) + if err != nil { + return nil, util.NewClientServerError("source used is not compatible with the tool", http.StatusInternalServerError, err) + } + + logger, err := util.LoggerFromContext(ctx) + if err != nil { + return nil, util.NewClientServerError("unable to get logger from ctx", http.StatusInternalServerError, err) + } + paramsMap := params.AsMap() + conversationId := paramsMap["conversation_id"].(string) + + sdk, err := source.GetLookerSDK(string(accessToken)) + if err != nil { + return nil, util.NewClientServerError("error getting sdk", http.StatusInternalServerError, err) + } + + logger.InfoContext(ctx, "Making DeleteConversation request", "conversation_id", conversationId) + resp, err := sdk.DeleteConversation(conversationId, "", source.LookerApiSettings()) + if err != nil { + return nil, util.ProcessGeneralError(err) + } + logger.InfoContext(ctx, "Got response", "response", resp) + + return resp, nil +} + +func (t Tool) EmbedParams(ctx context.Context, paramValues parameters.ParamValues, embeddingModelsMap map[string]embeddingmodels.EmbeddingModel) (parameters.ParamValues, error) { + return parameters.EmbedParams(ctx, t.Parameters, paramValues, embeddingModelsMap, nil) +} + +func (t Tool) Manifest() tools.Manifest { + return t.manifest +} + +func (t Tool) McpManifest() tools.McpManifest { + return t.mcpManifest +} + +func (t Tool) RequiresClientAuthorization(resourceMgr tools.SourceProvider) (bool, error) { + source, err := tools.GetCompatibleSource[compatibleSource](resourceMgr, t.Source, t.Name, t.Type) + if err != nil { + return false, err + } + return source.UseClientAuthorization(), nil +} + +func (t Tool) Authorized(verifiedAuthServices []string) bool { + return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices) +} + +func (t Tool) GetAuthTokenHeaderName(resourceMgr tools.SourceProvider) (string, error) { + source, err := tools.GetCompatibleSource[compatibleSource](resourceMgr, t.Source, t.Name, t.Type) + if err != nil { + return "", err + } + return source.GetAuthTokenHeaderName(), nil +} + +func (t Tool) GetParameters() parameters.Parameters { + return t.Parameters +} diff --git a/internal/tools/looker/lookerdeleteconversation/lookerdeleteconversation_test.go b/internal/tools/looker/lookerdeleteconversation/lookerdeleteconversation_test.go new file mode 100644 index 000000000000..76f7ecb75b2f --- /dev/null +++ b/internal/tools/looker/lookerdeleteconversation/lookerdeleteconversation_test.go @@ -0,0 +1,68 @@ +// 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 lookerdeleteconversation_test + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/googleapis/mcp-toolbox/internal/server" + "github.com/googleapis/mcp-toolbox/internal/testutils" + lkr "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookerdeleteconversation" +) + +func TestParseFromYamlLookerDeleteConversation(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: ` + kind: tool + name: example_tool + type: looker-delete-conversation + source: my-instance + description: some description + `, + want: server.ToolConfigs{ + "example_tool": lkr.Config{ + Name: "example_tool", + Type: "looker-delete-conversation", + Source: "my-instance", + Description: "some description", + AuthRequired: []string{}, + }, + }, + }, + } + for _, tc := range tcs { + t.Run(tc.desc, func(t *testing.T) { + // Parse contents + _, _, _, got, _, _, err := server.UnmarshalResourceConfig(ctx, testutils.FormatYaml(tc.in)) + if err != nil { + t.Fatalf("unable to unmarshal: %s", err) + } + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Fatalf("incorrect parse: diff %v", diff) + } + }) + } +} diff --git a/internal/tools/looker/lookergetconversation/lookergetconversation.go b/internal/tools/looker/lookergetconversation/lookergetconversation.go new file mode 100644 index 000000000000..825b221b96bb --- /dev/null +++ b/internal/tools/looker/lookergetconversation/lookergetconversation.go @@ -0,0 +1,173 @@ +// 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 lookergetconversation + +import ( + "context" + "fmt" + "net/http" + + yaml "github.com/goccy/go-yaml" + "github.com/googleapis/mcp-toolbox/internal/embeddingmodels" + "github.com/googleapis/mcp-toolbox/internal/sources" + "github.com/googleapis/mcp-toolbox/internal/tools" + "github.com/googleapis/mcp-toolbox/internal/util" + "github.com/googleapis/mcp-toolbox/internal/util/parameters" + + "github.com/looker-open-source/sdk-codegen/go/rtl" + v4 "github.com/looker-open-source/sdk-codegen/go/sdk/v4" +) + +const resourceType string = "looker-get-conversation" + +func init() { + if !tools.Register(resourceType, newConfig) { + panic(fmt.Sprintf("tool type %q already registered", resourceType)) + } +} + +func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.ToolConfig, error) { + actual := Config{Name: name} + if err := decoder.DecodeContext(ctx, &actual); err != nil { + return nil, err + } + return actual, nil +} + +type compatibleSource interface { + UseClientAuthorization() bool + GetAuthTokenHeaderName() string + LookerApiSettings() *rtl.ApiSettings + GetLookerSDK(string) (*v4.LookerSDK, error) +} + +type Config struct { + Name string `yaml:"name" validate:"required"` + Type string `yaml:"type" validate:"required"` + Source string `yaml:"source" validate:"required"` + Description string `yaml:"description" validate:"required"` + AuthRequired []string `yaml:"authRequired"` + Annotations *tools.ToolAnnotations `yaml:"annotations,omitempty"` +} + +// validate interface +var _ tools.ToolConfig = Config{} + +func (cfg Config) ToolConfigType() string { + return resourceType +} + +func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) { + conversationIdParameter := parameters.NewStringParameter("conversation_id", "The ID of the conversation to retrieve.") + params := parameters.Parameters{ + conversationIdParameter, + } + + readOnlyHint := true + annotations := &tools.ToolAnnotations{ + ReadOnlyHint: &readOnlyHint, + } + + mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, params, annotations) + + // finish tool setup + return Tool{ + Config: cfg, + Parameters: params, + manifest: tools.Manifest{ + Description: cfg.Description, + Parameters: params.Manifest(), + AuthRequired: cfg.AuthRequired, + }, + mcpManifest: mcpManifest, + }, nil +} + +// validate interface +var _ tools.Tool = Tool{} + +type Tool struct { + Config + Parameters parameters.Parameters `yaml:"parameters"` + manifest tools.Manifest + mcpManifest tools.McpManifest +} + +func (t Tool) ToConfig() tools.ToolConfig { + return t.Config +} + +func (t Tool) Invoke(ctx context.Context, resourceMgr tools.SourceProvider, params parameters.ParamValues, accessToken tools.AccessToken) (any, util.ToolboxError) { + source, err := tools.GetCompatibleSource[compatibleSource](resourceMgr, t.Source, t.Name, t.Type) + if err != nil { + return nil, util.NewClientServerError("source used is not compatible with the tool", http.StatusInternalServerError, err) + } + + logger, err := util.LoggerFromContext(ctx) + if err != nil { + return nil, util.NewClientServerError("unable to get logger from ctx", http.StatusInternalServerError, err) + } + paramsMap := params.AsMap() + conversationId := paramsMap["conversation_id"].(string) + + sdk, err := source.GetLookerSDK(string(accessToken)) + if err != nil { + return nil, util.NewClientServerError("error getting sdk", http.StatusInternalServerError, err) + } + + logger.InfoContext(ctx, "Making GetConversation request", "conversation_id", conversationId) + resp, err := sdk.GetConversation(conversationId, "", source.LookerApiSettings()) + if err != nil { + return nil, util.ProcessGeneralError(err) + } + logger.InfoContext(ctx, "Got response", "response", resp) + + return resp, nil +} + +func (t Tool) EmbedParams(ctx context.Context, paramValues parameters.ParamValues, embeddingModelsMap map[string]embeddingmodels.EmbeddingModel) (parameters.ParamValues, error) { + return parameters.EmbedParams(ctx, t.Parameters, paramValues, embeddingModelsMap, nil) +} + +func (t Tool) Manifest() tools.Manifest { + return t.manifest +} + +func (t Tool) McpManifest() tools.McpManifest { + return t.mcpManifest +} + +func (t Tool) RequiresClientAuthorization(resourceMgr tools.SourceProvider) (bool, error) { + source, err := tools.GetCompatibleSource[compatibleSource](resourceMgr, t.Source, t.Name, t.Type) + if err != nil { + return false, err + } + return source.UseClientAuthorization(), nil +} + +func (t Tool) Authorized(verifiedAuthServices []string) bool { + return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices) +} + +func (t Tool) GetAuthTokenHeaderName(resourceMgr tools.SourceProvider) (string, error) { + source, err := tools.GetCompatibleSource[compatibleSource](resourceMgr, t.Source, t.Name, t.Type) + if err != nil { + return "", err + } + return source.GetAuthTokenHeaderName(), nil +} + +func (t Tool) GetParameters() parameters.Parameters { + return t.Parameters +} diff --git a/internal/tools/looker/lookergetconversation/lookergetconversation_test.go b/internal/tools/looker/lookergetconversation/lookergetconversation_test.go new file mode 100644 index 000000000000..f3a76100a093 --- /dev/null +++ b/internal/tools/looker/lookergetconversation/lookergetconversation_test.go @@ -0,0 +1,68 @@ +// 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 lookergetconversation_test + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/googleapis/mcp-toolbox/internal/server" + "github.com/googleapis/mcp-toolbox/internal/testutils" + lkr "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookergetconversation" +) + +func TestParseFromYamlLookerGetConversation(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: ` + kind: tool + name: example_tool + type: looker-get-conversation + source: my-instance + description: some description + `, + want: server.ToolConfigs{ + "example_tool": lkr.Config{ + Name: "example_tool", + Type: "looker-get-conversation", + Source: "my-instance", + Description: "some description", + AuthRequired: []string{}, + }, + }, + }, + } + for _, tc := range tcs { + t.Run(tc.desc, func(t *testing.T) { + // Parse contents + _, _, _, got, _, _, err := server.UnmarshalResourceConfig(ctx, testutils.FormatYaml(tc.in)) + if err != nil { + t.Fatalf("unable to unmarshal: %s", err) + } + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Fatalf("incorrect parse: diff %v", diff) + } + }) + } +} diff --git a/internal/tools/looker/lookerlistconversations/lookerlistconversations.go b/internal/tools/looker/lookerlistconversations/lookerlistconversations.go new file mode 100644 index 000000000000..6694758d2e4e --- /dev/null +++ b/internal/tools/looker/lookerlistconversations/lookerlistconversations.go @@ -0,0 +1,187 @@ +// 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 lookerlistconversations + +import ( + "context" + "fmt" + "net/http" + + yaml "github.com/goccy/go-yaml" + "github.com/googleapis/mcp-toolbox/internal/embeddingmodels" + "github.com/googleapis/mcp-toolbox/internal/sources" + "github.com/googleapis/mcp-toolbox/internal/tools" + "github.com/googleapis/mcp-toolbox/internal/util" + "github.com/googleapis/mcp-toolbox/internal/util/parameters" + + "github.com/looker-open-source/sdk-codegen/go/rtl" + v4 "github.com/looker-open-source/sdk-codegen/go/sdk/v4" +) + +const resourceType string = "looker-list-conversations" + +func init() { + if !tools.Register(resourceType, newConfig) { + panic(fmt.Sprintf("tool type %q already registered", resourceType)) + } +} + +func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.ToolConfig, error) { + actual := Config{Name: name} + if err := decoder.DecodeContext(ctx, &actual); err != nil { + return nil, err + } + return actual, nil +} + +type compatibleSource interface { + UseClientAuthorization() bool + GetAuthTokenHeaderName() string + LookerApiSettings() *rtl.ApiSettings + GetLookerSDK(string) (*v4.LookerSDK, error) +} + +type Config struct { + Name string `yaml:"name" validate:"required"` + Type string `yaml:"type" validate:"required"` + Source string `yaml:"source" validate:"required"` + Description string `yaml:"description" validate:"required"` + AuthRequired []string `yaml:"authRequired"` + Annotations *tools.ToolAnnotations `yaml:"annotations,omitempty"` +} + +// validate interface +var _ tools.ToolConfig = Config{} + +func (cfg Config) ToolConfigType() string { + return resourceType +} + +func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) { + agentIdParameter := parameters.NewStringParameterWithDefault("agent_id", "", "Filter conversations by agent ID.") + limitParameter := parameters.NewIntParameterWithDefault("limit", 100, "The number of conversations to fetch. Default 100") + offsetParameter := parameters.NewIntParameterWithDefault("offset", 0, "The number of conversations to skip before fetching. Default 0") + params := parameters.Parameters{ + agentIdParameter, + limitParameter, + offsetParameter, + } + + readOnlyHint := true + annotations := &tools.ToolAnnotations{ + ReadOnlyHint: &readOnlyHint, + } + + mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, params, annotations) + + // finish tool setup + return Tool{ + Config: cfg, + Parameters: params, + manifest: tools.Manifest{ + Description: cfg.Description, + Parameters: params.Manifest(), + AuthRequired: cfg.AuthRequired, + }, + mcpManifest: mcpManifest, + }, nil +} + +// validate interface +var _ tools.Tool = Tool{} + +type Tool struct { + Config + Parameters parameters.Parameters `yaml:"parameters"` + manifest tools.Manifest + mcpManifest tools.McpManifest +} + +func (t Tool) ToConfig() tools.ToolConfig { + return t.Config +} + +func (t Tool) Invoke(ctx context.Context, resourceMgr tools.SourceProvider, params parameters.ParamValues, accessToken tools.AccessToken) (any, util.ToolboxError) { + source, err := tools.GetCompatibleSource[compatibleSource](resourceMgr, t.Source, t.Name, t.Type) + if err != nil { + return nil, util.NewClientServerError("source used is not compatible with the tool", http.StatusInternalServerError, err) + } + + logger, err := util.LoggerFromContext(ctx) + if err != nil { + return nil, util.NewClientServerError("unable to get logger from ctx", http.StatusInternalServerError, err) + } + paramsMap := params.AsMap() + agentId := paramsMap["agent_id"].(string) + limit := int64(paramsMap["limit"].(int)) + offset := int64(paramsMap["offset"].(int)) + + sdk, err := source.GetLookerSDK(string(accessToken)) + if err != nil { + return nil, util.NewClientServerError("error getting sdk", http.StatusInternalServerError, err) + } + + req := v4.RequestSearchConversations{ + Limit: &limit, + Offset: &offset, + } + if agentId != "" { + req.AgentId = &agentId + } + + logger.InfoContext(ctx, "Making SearchConversations request", "request", req) + resp, err := sdk.SearchConversations(req, source.LookerApiSettings()) + if err != nil { + return nil, util.ProcessGeneralError(err) + } + logger.InfoContext(ctx, "Got response", "response", resp) + + return resp, nil +} + +func (t Tool) EmbedParams(ctx context.Context, paramValues parameters.ParamValues, embeddingModelsMap map[string]embeddingmodels.EmbeddingModel) (parameters.ParamValues, error) { + return parameters.EmbedParams(ctx, t.Parameters, paramValues, embeddingModelsMap, nil) +} + +func (t Tool) Manifest() tools.Manifest { + return t.manifest +} + +func (t Tool) McpManifest() tools.McpManifest { + return t.mcpManifest +} + +func (t Tool) RequiresClientAuthorization(resourceMgr tools.SourceProvider) (bool, error) { + source, err := tools.GetCompatibleSource[compatibleSource](resourceMgr, t.Source, t.Name, t.Type) + if err != nil { + return false, err + } + return source.UseClientAuthorization(), nil +} + +func (t Tool) Authorized(verifiedAuthServices []string) bool { + return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices) +} + +func (t Tool) GetAuthTokenHeaderName(resourceMgr tools.SourceProvider) (string, error) { + source, err := tools.GetCompatibleSource[compatibleSource](resourceMgr, t.Source, t.Name, t.Type) + if err != nil { + return "", err + } + return source.GetAuthTokenHeaderName(), nil +} + +func (t Tool) GetParameters() parameters.Parameters { + return t.Parameters +} diff --git a/internal/tools/looker/lookerlistconversations/lookerlistconversations_test.go b/internal/tools/looker/lookerlistconversations/lookerlistconversations_test.go new file mode 100644 index 000000000000..7d7c4737c38c --- /dev/null +++ b/internal/tools/looker/lookerlistconversations/lookerlistconversations_test.go @@ -0,0 +1,68 @@ +// 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 lookerlistconversations_test + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/googleapis/mcp-toolbox/internal/server" + "github.com/googleapis/mcp-toolbox/internal/testutils" + lkr "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookerlistconversations" +) + +func TestParseFromYamlLookerListConversations(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: ` + kind: tool + name: example_tool + type: looker-list-conversations + source: my-instance + description: some description + `, + want: server.ToolConfigs{ + "example_tool": lkr.Config{ + Name: "example_tool", + Type: "looker-list-conversations", + Source: "my-instance", + Description: "some description", + AuthRequired: []string{}, + }, + }, + }, + } + for _, tc := range tcs { + t.Run(tc.desc, func(t *testing.T) { + // Parse contents + _, _, _, got, _, _, err := server.UnmarshalResourceConfig(ctx, testutils.FormatYaml(tc.in)) + if err != nil { + t.Fatalf("unable to unmarshal: %s", err) + } + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Fatalf("incorrect parse: diff %v", diff) + } + }) + } +} diff --git a/internal/tools/looker/lookerupdateconversation/lookerupdateconversation.go b/internal/tools/looker/lookerupdateconversation/lookerupdateconversation.go new file mode 100644 index 000000000000..5453bf1eac1f --- /dev/null +++ b/internal/tools/looker/lookerupdateconversation/lookerupdateconversation.go @@ -0,0 +1,182 @@ +// 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 lookerupdateconversation + +import ( + "context" + "fmt" + "net/http" + + yaml "github.com/goccy/go-yaml" + "github.com/googleapis/mcp-toolbox/internal/embeddingmodels" + "github.com/googleapis/mcp-toolbox/internal/sources" + "github.com/googleapis/mcp-toolbox/internal/tools" + "github.com/googleapis/mcp-toolbox/internal/util" + "github.com/googleapis/mcp-toolbox/internal/util/parameters" + + "github.com/looker-open-source/sdk-codegen/go/rtl" + v4 "github.com/looker-open-source/sdk-codegen/go/sdk/v4" +) + +const resourceType string = "looker-update-conversation" + +func init() { + if !tools.Register(resourceType, newConfig) { + panic(fmt.Sprintf("tool type %q already registered", resourceType)) + } +} + +func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.ToolConfig, error) { + actual := Config{Name: name} + if err := decoder.DecodeContext(ctx, &actual); err != nil { + return nil, err + } + return actual, nil +} + +type compatibleSource interface { + UseClientAuthorization() bool + GetAuthTokenHeaderName() string + LookerApiSettings() *rtl.ApiSettings + GetLookerSDK(string) (*v4.LookerSDK, error) +} + +type Config struct { + Name string `yaml:"name" validate:"required"` + Type string `yaml:"type" validate:"required"` + Source string `yaml:"source" validate:"required"` + Description string `yaml:"description" validate:"required"` + AuthRequired []string `yaml:"authRequired"` + Annotations *tools.ToolAnnotations `yaml:"annotations,omitempty"` +} + +// validate interface +var _ tools.ToolConfig = Config{} + +func (cfg Config) ToolConfigType() string { + return resourceType +} + +func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) { + conversationIdParameter := parameters.NewStringParameter("conversation_id", "The ID of the conversation to update.") + nameParameter := parameters.NewStringParameter("name", "The new name for the conversation.") + params := parameters.Parameters{ + conversationIdParameter, + nameParameter, + } + + readOnlyHint := false + destructiveHint := true + annotations := &tools.ToolAnnotations{ + ReadOnlyHint: &readOnlyHint, + DestructiveHint: &destructiveHint, + } + + mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, params, annotations) + + // finish tool setup + return Tool{ + Config: cfg, + Parameters: params, + manifest: tools.Manifest{ + Description: cfg.Description, + Parameters: params.Manifest(), + AuthRequired: cfg.AuthRequired, + }, + mcpManifest: mcpManifest, + }, nil +} + +// validate interface +var _ tools.Tool = Tool{} + +type Tool struct { + Config + Parameters parameters.Parameters `yaml:"parameters"` + manifest tools.Manifest + mcpManifest tools.McpManifest +} + +func (t Tool) ToConfig() tools.ToolConfig { + return t.Config +} + +func (t Tool) Invoke(ctx context.Context, resourceMgr tools.SourceProvider, params parameters.ParamValues, accessToken tools.AccessToken) (any, util.ToolboxError) { + source, err := tools.GetCompatibleSource[compatibleSource](resourceMgr, t.Source, t.Name, t.Type) + if err != nil { + return nil, util.NewClientServerError("source used is not compatible with the tool", http.StatusInternalServerError, err) + } + + logger, err := util.LoggerFromContext(ctx) + if err != nil { + return nil, util.NewClientServerError("unable to get logger from ctx", http.StatusInternalServerError, err) + } + paramsMap := params.AsMap() + conversationId := paramsMap["conversation_id"].(string) + name := paramsMap["name"].(string) + + sdk, err := source.GetLookerSDK(string(accessToken)) + if err != nil { + return nil, util.NewClientServerError("error getting sdk", http.StatusInternalServerError, err) + } + + body := v4.WriteConversation{ + Name: &name, + } + + logger.InfoContext(ctx, "Making UpdateConversation request", "conversation_id", conversationId) + resp, err := sdk.UpdateConversation(conversationId, body, "", source.LookerApiSettings()) + if err != nil { + return nil, util.ProcessGeneralError(err) + } + logger.InfoContext(ctx, "Got response", "response", resp) + + return resp, nil +} + +func (t Tool) EmbedParams(ctx context.Context, paramValues parameters.ParamValues, embeddingModelsMap map[string]embeddingmodels.EmbeddingModel) (parameters.ParamValues, error) { + return parameters.EmbedParams(ctx, t.Parameters, paramValues, embeddingModelsMap, nil) +} + +func (t Tool) Manifest() tools.Manifest { + return t.manifest +} + +func (t Tool) McpManifest() tools.McpManifest { + return t.mcpManifest +} + +func (t Tool) RequiresClientAuthorization(resourceMgr tools.SourceProvider) (bool, error) { + source, err := tools.GetCompatibleSource[compatibleSource](resourceMgr, t.Source, t.Name, t.Type) + if err != nil { + return false, err + } + return source.UseClientAuthorization(), nil +} + +func (t Tool) Authorized(verifiedAuthServices []string) bool { + return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices) +} + +func (t Tool) GetAuthTokenHeaderName(resourceMgr tools.SourceProvider) (string, error) { + source, err := tools.GetCompatibleSource[compatibleSource](resourceMgr, t.Source, t.Name, t.Type) + if err != nil { + return "", err + } + return source.GetAuthTokenHeaderName(), nil +} + +func (t Tool) GetParameters() parameters.Parameters { + return t.Parameters +} diff --git a/internal/tools/looker/lookerupdateconversation/lookerupdateconversation_test.go b/internal/tools/looker/lookerupdateconversation/lookerupdateconversation_test.go new file mode 100644 index 000000000000..c7a8f22d28ae --- /dev/null +++ b/internal/tools/looker/lookerupdateconversation/lookerupdateconversation_test.go @@ -0,0 +1,68 @@ +// 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 lookerupdateconversation_test + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/googleapis/mcp-toolbox/internal/server" + "github.com/googleapis/mcp-toolbox/internal/testutils" + lkr "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookerupdateconversation" +) + +func TestParseFromYamlLookerUpdateConversation(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: ` + kind: tool + name: example_tool + type: looker-update-conversation + source: my-instance + description: some description + `, + want: server.ToolConfigs{ + "example_tool": lkr.Config{ + Name: "example_tool", + Type: "looker-update-conversation", + Source: "my-instance", + Description: "some description", + AuthRequired: []string{}, + }, + }, + }, + } + for _, tc := range tcs { + t.Run(tc.desc, func(t *testing.T) { + // Parse contents + _, _, _, got, _, _, err := server.UnmarshalResourceConfig(ctx, testutils.FormatYaml(tc.in)) + if err != nil { + t.Fatalf("unable to unmarshal: %s", err) + } + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Fatalf("incorrect parse: diff %v", diff) + } + }) + } +}