e04ed9c211
CF: Deploy Dev Docs / deploy (push) Waiting to run
Sync Labels / build (push) Waiting to run
tests / unit tests (macos-latest) (push) Waiting to run
tests / unit tests (ubuntu-latest) (push) Waiting to run
tests / unit tests (windows-latest) (push) Waiting to run
151 lines
5.8 KiB
Go
151 lines
5.8 KiB
Go
// Copyright 2026 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 cloudstoragecommon holds helpers shared across the Cloud Storage
|
|
// tool implementations, chiefly error classification.
|
|
package cloudstoragecommon
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/http"
|
|
"os"
|
|
|
|
"cloud.google.com/go/storage"
|
|
"github.com/googleapis/mcp-toolbox/internal/util"
|
|
"google.golang.org/api/googleapi"
|
|
)
|
|
|
|
// ErrReadSizeLimitExceeded is returned by the source when an object/range
|
|
// would exceed the source's configured byte limit. ProcessGCSError maps this
|
|
// to an Agent error because the LLM can fix the call by narrowing the 'range'
|
|
// parameter.
|
|
var ErrReadSizeLimitExceeded = errors.New("cloud storage read size limit exceeded")
|
|
|
|
// ErrBinaryContent is returned by the source when an object's bytes are not
|
|
// valid UTF-8. The MCP tool result channel only carries text today, so binary
|
|
// payloads cannot be faithfully round-tripped; ProcessGCSError maps this to an
|
|
// Agent error so the LLM knows to stop asking for this object.
|
|
//
|
|
// TODO: when the toolbox supports non-text MCP content (embedded resources,
|
|
// images, blobs), remove this guard and return binary payloads directly.
|
|
var ErrBinaryContent = errors.New("cloud storage object is not valid UTF-8 text")
|
|
|
|
// ErrDestinationExists is returned by the download_object source method when
|
|
// the local destination file already exists and overwrite is false.
|
|
// ProcessGCSError maps this to an Agent error so the LLM can retry the call
|
|
// with overwrite=true.
|
|
var ErrDestinationExists = errors.New("download destination already exists")
|
|
|
|
// ProcessGCSError classifies an error from the Cloud Storage Go client into
|
|
// either an Agent Error (the LLM can self-correct by changing its input — bad
|
|
// request, missing bucket/object, unsatisfiable range) or a Server Error
|
|
// (infrastructure failure — auth, IAM denial, quota, 5xx, network
|
|
// cancellation). See DEVELOPER.md "Tool Invocation & Error Handling" for the
|
|
// wider rationale.
|
|
func ProcessGCSError(err error) util.ToolboxError {
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
|
|
// Transport-level cancellation/timeout — treat as infrastructure. These
|
|
// checks come first because a wrapped googleapi.Error on top of a
|
|
// cancelled context should still surface as a server error.
|
|
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
|
|
return util.NewClientServerError(
|
|
"cloud storage request cancelled or timed out",
|
|
http.StatusGatewayTimeout, err)
|
|
}
|
|
|
|
// Read-size cap tripped — agent can narrow the 'range' parameter.
|
|
if errors.Is(err, ErrReadSizeLimitExceeded) {
|
|
return util.NewAgentError(
|
|
"object is too large to read in one call; narrow the 'range' parameter",
|
|
err)
|
|
}
|
|
|
|
// Non-UTF-8 object — MCP only carries text today, so the agent should
|
|
// stop trying to read this object.
|
|
if errors.Is(err, ErrBinaryContent) {
|
|
return util.NewAgentError(
|
|
"object contains non-text (binary) bytes and cannot be returned; only UTF-8 text objects are supported",
|
|
err)
|
|
}
|
|
|
|
// Download destination already exists — the agent can retry with
|
|
// overwrite=true.
|
|
if errors.Is(err, ErrDestinationExists) {
|
|
return util.NewAgentError(
|
|
"destination file exists; retry with overwrite=true to replace it",
|
|
err)
|
|
}
|
|
|
|
// Local-filesystem "not found" — an upload source path or a download
|
|
// destination's parent directory that doesn't exist. The agent can fix
|
|
// this by correcting the path, so classify as Agent error.
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
return util.NewAgentError("local file or directory not found", err)
|
|
}
|
|
|
|
// GCS sentinel errors — "not found" flavours are agent-fixable.
|
|
if errors.Is(err, storage.ErrBucketNotExist) {
|
|
return util.NewAgentError("cloud storage bucket does not exist", err)
|
|
}
|
|
if errors.Is(err, storage.ErrObjectNotExist) {
|
|
return util.NewAgentError("cloud storage object does not exist", err)
|
|
}
|
|
|
|
// HTTP-layer errors from googleapis.
|
|
var gErr *googleapi.Error
|
|
if errors.As(err, &gErr) {
|
|
switch gErr.Code {
|
|
case http.StatusBadRequest:
|
|
return util.NewAgentError("cloud storage rejected the request as invalid", err)
|
|
case http.StatusUnauthorized:
|
|
return util.NewClientServerError(
|
|
"cloud storage authentication failed",
|
|
http.StatusUnauthorized, err)
|
|
case http.StatusForbidden:
|
|
return util.NewClientServerError(
|
|
"cloud storage permission denied",
|
|
http.StatusForbidden, err)
|
|
case http.StatusNotFound:
|
|
return util.NewAgentError("cloud storage resource not found", err)
|
|
case http.StatusConflict:
|
|
return util.NewAgentError("cloud storage conflict", err)
|
|
case http.StatusPreconditionFailed:
|
|
return util.NewAgentError("cloud storage precondition failed", err)
|
|
case http.StatusRequestedRangeNotSatisfiable:
|
|
return util.NewAgentError("requested byte range is not satisfiable for this object", err)
|
|
case http.StatusTooManyRequests:
|
|
return util.NewClientServerError(
|
|
"cloud storage request rate limit exceeded",
|
|
http.StatusTooManyRequests, err)
|
|
}
|
|
if gErr.Code >= 500 {
|
|
return util.NewClientServerError(
|
|
"cloud storage server error",
|
|
http.StatusBadGateway, err)
|
|
}
|
|
return util.NewClientServerError(
|
|
"unexpected cloud storage error",
|
|
http.StatusInternalServerError, err)
|
|
}
|
|
|
|
// Fallback — unrecognized error (likely transport-level).
|
|
return util.NewClientServerError(
|
|
"cloud storage request failed",
|
|
http.StatusInternalServerError, err)
|
|
}
|