Compare commits

..

1 Commits

Author SHA1 Message Date
akangsha7
686eb644b3 Add instructions to setup API key 2026-02-04 11:59:36 -08:00
20 changed files with 151 additions and 323 deletions

View File

@@ -40,7 +40,7 @@ jobs:
group: docs-deployment group: docs-deployment
cancel-in-progress: false cancel-in-progress: false
steps: steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
with: with:
fetch-depth: 0 # Fetch all history for .GitInfo and .Lastmod fetch-depth: 0 # Fetch all history for .GitInfo and .Lastmod
@@ -56,7 +56,7 @@ jobs:
node-version: "22" node-version: "22"
- name: Cache dependencies - name: Cache dependencies
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5 uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5
with: with:
path: ~/.npm path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}

View File

@@ -30,14 +30,14 @@ jobs:
steps: steps:
- name: Checkout main branch (for latest templates and theme) - name: Checkout main branch (for latest templates and theme)
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
with: with:
ref: 'main' ref: 'main'
submodules: 'recursive' submodules: 'recursive'
fetch-depth: 0 fetch-depth: 0
- name: Checkout old content from tag into a temporary directory - name: Checkout old content from tag into a temporary directory
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
with: with:
ref: ${{ github.event.inputs.version_tag }} ref: ${{ github.event.inputs.version_tag }}
path: 'old_version_source' # Checkout into a temp subdir path: 'old_version_source' # Checkout into a temp subdir

View File

@@ -30,7 +30,7 @@ jobs:
cancel-in-progress: false cancel-in-progress: false
steps: steps:
- name: Checkout Code at Tag - name: Checkout Code at Tag
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
with: with:
ref: ${{ github.event.release.tag_name }} ref: ${{ github.event.release.tag_name }}

View File

@@ -34,7 +34,7 @@ jobs:
group: "preview-${{ github.event.number }}" group: "preview-${{ github.event.number }}"
cancel-in-progress: true cancel-in-progress: true
steps: steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
with: with:
ref: versioned-gh-pages ref: versioned-gh-pages

View File

@@ -49,7 +49,7 @@ jobs:
group: "preview-${{ github.event.number }}" group: "preview-${{ github.event.number }}"
cancel-in-progress: true cancel-in-progress: true
steps: steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
with: with:
# Checkout the PR's HEAD commit (supports forks). # Checkout the PR's HEAD commit (supports forks).
ref: ${{ github.event.pull_request.head.sha }} ref: ${{ github.event.pull_request.head.sha }}
@@ -67,7 +67,7 @@ jobs:
node-version: "22" node-version: "22"
- name: Cache dependencies - name: Cache dependencies
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5 uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5
with: with:
path: ~/.npm path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}

View File

@@ -22,10 +22,10 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout Repository - name: Checkout Repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
- name: Restore lychee cache - name: Restore lychee cache
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5 uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5
with: with:
path: .lycheecache path: .lycheecache
key: cache-lychee-${{ github.sha }} key: cache-lychee-${{ github.sha }}

View File

@@ -29,7 +29,7 @@ jobs:
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
- name: Wait for image in Artifact Registry - name: Wait for image in Artifact Registry
shell: bash shell: bash

View File

@@ -46,6 +46,11 @@ from Toolbox.
go get github.com/googleapis/mcp-toolbox-sdk-go go get github.com/googleapis/mcp-toolbox-sdk-go
``` ```
1. Set your API key (If you don't have an API key yet, you can get one from [Google AI Studio](https://aistudio.google.com/app/apikey).)
```bash
export GOOGLE_API_KEY="YOUR_ACTUAL_GEMINI_API_KEY"
```
1. Create a new file named `hotelagent.go` and copy the following code to create 1. Create a new file named `hotelagent.go` and copy the following code to create
an agent: an agent:

View File

@@ -19,6 +19,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
"strings"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware" "github.com/go-chi/chi/v5/middleware"
@@ -233,11 +234,10 @@ func toolInvokeHandler(s *Server, w http.ResponseWriter, r *http.Request) {
params, err := parameters.ParseParams(tool.GetParameters(), data, claimsFromAuth) params, err := parameters.ParseParams(tool.GetParameters(), data, claimsFromAuth)
if err != nil { if err != nil {
// If auth error, return 401 or 403 // If auth error, return 401
var clientServerErr *util.ClientServerError if errors.Is(err, util.ErrUnauthorized) {
if errors.As(err, &clientServerErr) && (clientServerErr.Code == http.StatusUnauthorized || clientServerErr.Code == http.StatusForbidden) {
s.logger.DebugContext(ctx, fmt.Sprintf("error parsing authenticated parameters from ID token: %s", err)) s.logger.DebugContext(ctx, fmt.Sprintf("error parsing authenticated parameters from ID token: %s", err))
_ = render.Render(w, r, newErrResponse(err, clientServerErr.Code)) _ = render.Render(w, r, newErrResponse(err, http.StatusUnauthorized))
return return
} }
err = fmt.Errorf("provided parameters were invalid: %w", err) err = fmt.Errorf("provided parameters were invalid: %w", err)
@@ -259,49 +259,34 @@ func toolInvokeHandler(s *Server, w http.ResponseWriter, r *http.Request) {
// Determine what error to return to the users. // Determine what error to return to the users.
if err != nil { if err != nil {
var tbErr util.ToolboxError errStr := err.Error()
var statusCode int
if errors.As(err, &tbErr) { // Upstream API auth error propagation
switch tbErr.Category() { switch {
case util.CategoryAgent: case strings.Contains(errStr, "Error 401"):
// Agent Errors -> 200 OK statusCode = http.StatusUnauthorized
s.logger.DebugContext(ctx, fmt.Sprintf("Tool invocation agent error: %v", err)) case strings.Contains(errStr, "Error 403"):
_ = render.Render(w, r, newErrResponse(err, http.StatusOK)) statusCode = http.StatusForbidden
return }
case util.CategoryServer: if statusCode == http.StatusUnauthorized || statusCode == http.StatusForbidden {
// Server Errors -> Check the specific code inside if clientAuth {
var clientServerErr *util.ClientServerError // Propagate the original 401/403 error.
statusCode := http.StatusInternalServerError // Default to 500 s.logger.DebugContext(ctx, fmt.Sprintf("error invoking tool. Client credentials lack authorization to the source: %v", err))
if errors.As(err, &clientServerErr) {
if clientServerErr.Code != 0 {
statusCode = clientServerErr.Code
}
}
// Process auth error
if statusCode == http.StatusUnauthorized || statusCode == http.StatusForbidden {
if clientAuth {
// Token error, pass through 401/403
s.logger.DebugContext(ctx, fmt.Sprintf("Client credentials lack authorization: %v", err))
_ = render.Render(w, r, newErrResponse(err, statusCode))
return
}
// ADC/Config error, return 500
statusCode = http.StatusInternalServerError
}
s.logger.ErrorContext(ctx, fmt.Sprintf("Tool invocation server error: %v", err))
_ = render.Render(w, r, newErrResponse(err, statusCode)) _ = render.Render(w, r, newErrResponse(err, statusCode))
return return
} }
} else { // ADC lacking permission or credentials configuration error.
// Unknown error -> 500 internalErr := fmt.Errorf("unexpected auth error occured during Tool invocation: %w", err)
s.logger.ErrorContext(ctx, fmt.Sprintf("Tool invocation unknown error: %v", err)) s.logger.ErrorContext(ctx, internalErr.Error())
_ = render.Render(w, r, newErrResponse(err, http.StatusInternalServerError)) _ = render.Render(w, r, newErrResponse(internalErr, http.StatusInternalServerError))
return return
} }
err = fmt.Errorf("error while invoking tool: %w", err)
s.logger.DebugContext(ctx, err.Error())
_ = render.Render(w, r, newErrResponse(err, http.StatusBadRequest))
return
} }
resMarshal, err := json.Marshal(res) resMarshal, err := json.Marshal(res)

View File

@@ -23,6 +23,7 @@ import (
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
"strings"
"sync" "sync"
"time" "time"
@@ -443,12 +444,15 @@ func httpHandler(s *Server, w http.ResponseWriter, r *http.Request) {
code := rpcResponse.Error.Code code := rpcResponse.Error.Code
switch code { switch code {
case jsonrpc.INTERNAL_ERROR: case jsonrpc.INTERNAL_ERROR:
// Map Internal RPC Error (-32603) to HTTP 500
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
case jsonrpc.INVALID_REQUEST: case jsonrpc.INVALID_REQUEST:
var clientServerErr *util.ClientServerError errStr := err.Error()
if errors.As(err, &clientServerErr) { if errors.Is(err, util.ErrUnauthorized) {
w.WriteHeader(clientServerErr.Code) w.WriteHeader(http.StatusUnauthorized)
} else if strings.Contains(errStr, "Error 401") {
w.WriteHeader(http.StatusUnauthorized)
} else if strings.Contains(errStr, "Error 403") {
w.WriteHeader(http.StatusForbidden)
} }
} }
} }

View File

@@ -21,6 +21,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
"strings"
"github.com/googleapis/genai-toolbox/internal/prompts" "github.com/googleapis/genai-toolbox/internal/prompts"
"github.com/googleapis/genai-toolbox/internal/server/mcp/jsonrpc" "github.com/googleapis/genai-toolbox/internal/server/mcp/jsonrpc"
@@ -123,12 +124,7 @@ func toolsCallHandler(ctx context.Context, id jsonrpc.RequestId, resourceMgr *re
} }
if clientAuth { if clientAuth {
if accessToken == "" { if accessToken == "" {
errMsg := "missing access token in the 'Authorization' header" return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, "missing access token in the 'Authorization' header", nil), util.ErrUnauthorized
return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, errMsg, nil), util.NewClientServerError(
errMsg,
http.StatusUnauthorized,
nil,
)
} }
} }
@@ -176,11 +172,7 @@ func toolsCallHandler(ctx context.Context, id jsonrpc.RequestId, resourceMgr *re
// Check if any of the specified auth services is verified // Check if any of the specified auth services is verified
isAuthorized := tool.Authorized(verifiedAuthServices) isAuthorized := tool.Authorized(verifiedAuthServices)
if !isAuthorized { if !isAuthorized {
err = util.NewClientServerError( err = fmt.Errorf("unauthorized Tool call: Please make sure your specify correct auth headers: %w", util.ErrUnauthorized)
"unauthorized Tool call: Please make sure you specify correct auth headers",
http.StatusUnauthorized,
nil,
)
return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err
} }
logger.DebugContext(ctx, "tool invocation authorized") logger.DebugContext(ctx, "tool invocation authorized")
@@ -202,44 +194,30 @@ func toolsCallHandler(ctx context.Context, id jsonrpc.RequestId, resourceMgr *re
// run tool invocation and generate response. // run tool invocation and generate response.
results, err := tool.Invoke(ctx, resourceMgr, params, accessToken) results, err := tool.Invoke(ctx, resourceMgr, params, accessToken)
if err != nil { if err != nil {
var tbErr util.ToolboxError errStr := err.Error()
// Missing authService tokens.
if errors.As(err, &tbErr) { if errors.Is(err, util.ErrUnauthorized) {
switch tbErr.Category() { return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err
case util.CategoryAgent: }
// MCP - Tool execution error // Upstream auth error
// Return SUCCESS but with IsError: true if strings.Contains(errStr, "Error 401") || strings.Contains(errStr, "Error 403") {
text := TextContent{ if clientAuth {
Type: "text", // Error with client credentials should pass down to the client
Text: err.Error(), return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err
}
return jsonrpc.JSONRPCResponse{
Jsonrpc: jsonrpc.JSONRPC_VERSION,
Id: id,
Result: CallToolResult{Content: []TextContent{text}, IsError: true},
}, nil
case util.CategoryServer:
// MCP Spec - Protocol error
// Return JSON-RPC ERROR
var clientServerErr *util.ClientServerError
rpcCode := jsonrpc.INTERNAL_ERROR // Default to Internal Error (-32603)
if errors.As(err, &clientServerErr) {
if clientServerErr.Code == http.StatusUnauthorized || clientServerErr.Code == http.StatusForbidden {
if clientAuth {
rpcCode = jsonrpc.INVALID_REQUEST
} else {
rpcCode = jsonrpc.INTERNAL_ERROR
}
}
}
return jsonrpc.NewError(id, rpcCode, err.Error(), nil), err
} }
} else { // Auth error with ADC should raise internal 500 error
// Unknown error -> 500
return jsonrpc.NewError(id, jsonrpc.INTERNAL_ERROR, err.Error(), nil), err return jsonrpc.NewError(id, jsonrpc.INTERNAL_ERROR, err.Error(), nil), err
} }
text := TextContent{
Type: "text",
Text: err.Error(),
}
return jsonrpc.JSONRPCResponse{
Jsonrpc: jsonrpc.JSONRPC_VERSION,
Id: id,
Result: CallToolResult{Content: []TextContent{text}, IsError: true},
}, nil
} }
content := make([]TextContent, 0) content := make([]TextContent, 0)

View File

@@ -21,6 +21,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
"strings"
"github.com/googleapis/genai-toolbox/internal/prompts" "github.com/googleapis/genai-toolbox/internal/prompts"
"github.com/googleapis/genai-toolbox/internal/server/mcp/jsonrpc" "github.com/googleapis/genai-toolbox/internal/server/mcp/jsonrpc"
@@ -123,12 +124,7 @@ func toolsCallHandler(ctx context.Context, id jsonrpc.RequestId, resourceMgr *re
} }
if clientAuth { if clientAuth {
if accessToken == "" { if accessToken == "" {
errMsg := "missing access token in the 'Authorization' header" return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, "missing access token in the 'Authorization' header", nil), util.ErrUnauthorized
return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, errMsg, nil), util.NewClientServerError(
errMsg,
http.StatusUnauthorized,
nil,
)
} }
} }
@@ -176,11 +172,7 @@ func toolsCallHandler(ctx context.Context, id jsonrpc.RequestId, resourceMgr *re
// Check if any of the specified auth services is verified // Check if any of the specified auth services is verified
isAuthorized := tool.Authorized(verifiedAuthServices) isAuthorized := tool.Authorized(verifiedAuthServices)
if !isAuthorized { if !isAuthorized {
err = util.NewClientServerError( err = fmt.Errorf("unauthorized Tool call: Please make sure your specify correct auth headers: %w", util.ErrUnauthorized)
"unauthorized Tool call: Please make sure you specify correct auth headers",
http.StatusUnauthorized,
nil,
)
return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err
} }
logger.DebugContext(ctx, "tool invocation authorized") logger.DebugContext(ctx, "tool invocation authorized")
@@ -202,45 +194,31 @@ func toolsCallHandler(ctx context.Context, id jsonrpc.RequestId, resourceMgr *re
// run tool invocation and generate response. // run tool invocation and generate response.
results, err := tool.Invoke(ctx, resourceMgr, params, accessToken) results, err := tool.Invoke(ctx, resourceMgr, params, accessToken)
if err != nil { if err != nil {
var tbErr util.ToolboxError errStr := err.Error()
// Missing authService tokens.
if errors.As(err, &tbErr) { if errors.Is(err, util.ErrUnauthorized) {
switch tbErr.Category() { return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err
case util.CategoryAgent: }
// MCP - Tool execution error // Upstream auth error
// Return SUCCESS but with IsError: true if strings.Contains(errStr, "Error 401") || strings.Contains(errStr, "Error 403") {
text := TextContent{ if clientAuth {
Type: "text", // Error with client credentials should pass down to the client
Text: err.Error(), return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err
}
return jsonrpc.JSONRPCResponse{
Jsonrpc: jsonrpc.JSONRPC_VERSION,
Id: id,
Result: CallToolResult{Content: []TextContent{text}, IsError: true},
}, nil
case util.CategoryServer:
// MCP Spec - Protocol error
// Return JSON-RPC ERROR
var clientServerErr *util.ClientServerError
rpcCode := jsonrpc.INTERNAL_ERROR // Default to Internal Error (-32603)
if errors.As(err, &clientServerErr) {
if clientServerErr.Code == http.StatusUnauthorized || clientServerErr.Code == http.StatusForbidden {
if clientAuth {
rpcCode = jsonrpc.INVALID_REQUEST
} else {
rpcCode = jsonrpc.INTERNAL_ERROR
}
}
}
return jsonrpc.NewError(id, rpcCode, err.Error(), nil), err
} }
} else { // Auth error with ADC should raise internal 500 error
// Unknown error -> 500
return jsonrpc.NewError(id, jsonrpc.INTERNAL_ERROR, err.Error(), nil), err return jsonrpc.NewError(id, jsonrpc.INTERNAL_ERROR, err.Error(), nil), err
} }
text := TextContent{
Type: "text",
Text: err.Error(),
}
return jsonrpc.JSONRPCResponse{
Jsonrpc: jsonrpc.JSONRPC_VERSION,
Id: id,
Result: CallToolResult{Content: []TextContent{text}, IsError: true},
}, nil
} }
content := make([]TextContent, 0) content := make([]TextContent, 0)
sliceRes, ok := results.([]any) sliceRes, ok := results.([]any)

View File

@@ -21,6 +21,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
"strings"
"github.com/googleapis/genai-toolbox/internal/prompts" "github.com/googleapis/genai-toolbox/internal/prompts"
"github.com/googleapis/genai-toolbox/internal/server/mcp/jsonrpc" "github.com/googleapis/genai-toolbox/internal/server/mcp/jsonrpc"
@@ -116,12 +117,7 @@ func toolsCallHandler(ctx context.Context, id jsonrpc.RequestId, resourceMgr *re
} }
if clientAuth { if clientAuth {
if accessToken == "" { if accessToken == "" {
errMsg := "missing access token in the 'Authorization' header" return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, "missing access token in the 'Authorization' header", nil), util.ErrUnauthorized
return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, errMsg, nil), util.NewClientServerError(
errMsg,
http.StatusUnauthorized,
nil,
)
} }
} }
@@ -169,11 +165,7 @@ func toolsCallHandler(ctx context.Context, id jsonrpc.RequestId, resourceMgr *re
// Check if any of the specified auth services is verified // Check if any of the specified auth services is verified
isAuthorized := tool.Authorized(verifiedAuthServices) isAuthorized := tool.Authorized(verifiedAuthServices)
if !isAuthorized { if !isAuthorized {
err = util.NewClientServerError( err = fmt.Errorf("unauthorized Tool call: Please make sure your specify correct auth headers: %w", util.ErrUnauthorized)
"unauthorized Tool call: Please make sure you specify correct auth headers",
http.StatusUnauthorized,
nil,
)
return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err
} }
logger.DebugContext(ctx, "tool invocation authorized") logger.DebugContext(ctx, "tool invocation authorized")
@@ -195,44 +187,29 @@ func toolsCallHandler(ctx context.Context, id jsonrpc.RequestId, resourceMgr *re
// run tool invocation and generate response. // run tool invocation and generate response.
results, err := tool.Invoke(ctx, resourceMgr, params, accessToken) results, err := tool.Invoke(ctx, resourceMgr, params, accessToken)
if err != nil { if err != nil {
var tbErr util.ToolboxError errStr := err.Error()
// Missing authService tokens.
if errors.As(err, &tbErr) { if errors.Is(err, util.ErrUnauthorized) {
switch tbErr.Category() { return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err
case util.CategoryAgent: }
// MCP - Tool execution error // Upstream auth error
// Return SUCCESS but with IsError: true if strings.Contains(errStr, "Error 401") || strings.Contains(errStr, "Error 403") {
text := TextContent{ if clientAuth {
Type: "text", // Error with client credentials should pass down to the client
Text: err.Error(), return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err
}
return jsonrpc.JSONRPCResponse{
Jsonrpc: jsonrpc.JSONRPC_VERSION,
Id: id,
Result: CallToolResult{Content: []TextContent{text}, IsError: true},
}, nil
case util.CategoryServer:
// MCP Spec - Protocol error
// Return JSON-RPC ERROR
var clientServerErr *util.ClientServerError
rpcCode := jsonrpc.INTERNAL_ERROR // Default to Internal Error (-32603)
if errors.As(err, &clientServerErr) {
if clientServerErr.Code == http.StatusUnauthorized || clientServerErr.Code == http.StatusForbidden {
if clientAuth {
rpcCode = jsonrpc.INVALID_REQUEST
} else {
rpcCode = jsonrpc.INTERNAL_ERROR
}
}
}
return jsonrpc.NewError(id, rpcCode, err.Error(), nil), err
} }
} else { // Auth error with ADC should raise internal 500 error
// Unknown error -> 500
return jsonrpc.NewError(id, jsonrpc.INTERNAL_ERROR, err.Error(), nil), err return jsonrpc.NewError(id, jsonrpc.INTERNAL_ERROR, err.Error(), nil), err
} }
text := TextContent{
Type: "text",
Text: err.Error(),
}
return jsonrpc.JSONRPCResponse{
Jsonrpc: jsonrpc.JSONRPC_VERSION,
Id: id,
Result: CallToolResult{Content: []TextContent{text}, IsError: true},
}, nil
} }
content := make([]TextContent, 0) content := make([]TextContent, 0)

View File

@@ -21,6 +21,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
"strings"
"github.com/googleapis/genai-toolbox/internal/prompts" "github.com/googleapis/genai-toolbox/internal/prompts"
"github.com/googleapis/genai-toolbox/internal/server/mcp/jsonrpc" "github.com/googleapis/genai-toolbox/internal/server/mcp/jsonrpc"
@@ -116,12 +117,7 @@ func toolsCallHandler(ctx context.Context, id jsonrpc.RequestId, resourceMgr *re
} }
if clientAuth { if clientAuth {
if accessToken == "" { if accessToken == "" {
errMsg := "missing access token in the 'Authorization' header" return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, "missing access token in the 'Authorization' header", nil), util.ErrUnauthorized
return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, errMsg, nil), util.NewClientServerError(
errMsg,
http.StatusUnauthorized,
nil,
)
} }
} }
@@ -169,11 +165,7 @@ func toolsCallHandler(ctx context.Context, id jsonrpc.RequestId, resourceMgr *re
// Check if any of the specified auth services is verified // Check if any of the specified auth services is verified
isAuthorized := tool.Authorized(verifiedAuthServices) isAuthorized := tool.Authorized(verifiedAuthServices)
if !isAuthorized { if !isAuthorized {
err = util.NewClientServerError( err = fmt.Errorf("unauthorized Tool call: Please make sure your specify correct auth headers: %w", util.ErrUnauthorized)
"unauthorized Tool call: Please make sure you specify correct auth headers",
http.StatusUnauthorized,
nil,
)
return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err
} }
logger.DebugContext(ctx, "tool invocation authorized") logger.DebugContext(ctx, "tool invocation authorized")
@@ -195,44 +187,29 @@ func toolsCallHandler(ctx context.Context, id jsonrpc.RequestId, resourceMgr *re
// run tool invocation and generate response. // run tool invocation and generate response.
results, err := tool.Invoke(ctx, resourceMgr, params, accessToken) results, err := tool.Invoke(ctx, resourceMgr, params, accessToken)
if err != nil { if err != nil {
var tbErr util.ToolboxError errStr := err.Error()
// Missing authService tokens.
if errors.As(err, &tbErr) { if errors.Is(err, util.ErrUnauthorized) {
switch tbErr.Category() { return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err
case util.CategoryAgent: }
// MCP - Tool execution error // Upstream auth error
// Return SUCCESS but with IsError: true if strings.Contains(errStr, "Error 401") || strings.Contains(errStr, "Error 403") {
text := TextContent{ if clientAuth {
Type: "text", // Error with client credentials should pass down to the client
Text: err.Error(), return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err
}
return jsonrpc.JSONRPCResponse{
Jsonrpc: jsonrpc.JSONRPC_VERSION,
Id: id,
Result: CallToolResult{Content: []TextContent{text}, IsError: true},
}, nil
case util.CategoryServer:
// MCP Spec - Protocol error
// Return JSON-RPC ERROR
var clientServerErr *util.ClientServerError
rpcCode := jsonrpc.INTERNAL_ERROR // Default to Internal Error (-32603)
if errors.As(err, &clientServerErr) {
if clientServerErr.Code == http.StatusUnauthorized || clientServerErr.Code == http.StatusForbidden {
if clientAuth {
rpcCode = jsonrpc.INVALID_REQUEST
} else {
rpcCode = jsonrpc.INTERNAL_ERROR
}
}
}
return jsonrpc.NewError(id, rpcCode, err.Error(), nil), err
} }
} else { // Auth error with ADC should raise internal 500 error
// Unknown error -> 500
return jsonrpc.NewError(id, jsonrpc.INTERNAL_ERROR, err.Error(), nil), err return jsonrpc.NewError(id, jsonrpc.INTERNAL_ERROR, err.Error(), nil), err
} }
text := TextContent{
Type: "text",
Text: err.Error(),
}
return jsonrpc.JSONRPCResponse{
Jsonrpc: jsonrpc.JSONRPC_VERSION,
Id: id,
Result: CallToolResult{Content: []TextContent{text}, IsError: true},
}, nil
} }
content := make([]TextContent, 0) content := make([]TextContent, 0)

View File

@@ -231,7 +231,7 @@ func TestMcpEndpointWithoutInitialized(t *testing.T) {
"id": "tools-call-tool4", "id": "tools-call-tool4",
"error": map[string]any{ "error": map[string]any{
"code": -32600.0, "code": -32600.0,
"message": "unauthorized Tool call: Please make sure you specify correct auth headers: <nil>", "message": "unauthorized Tool call: Please make sure your specify correct auth headers: unauthorized",
}, },
}, },
}, },
@@ -834,7 +834,7 @@ func TestMcpEndpoint(t *testing.T) {
"id": "tools-call-tool4", "id": "tools-call-tool4",
"error": map[string]any{ "error": map[string]any{
"code": -32600.0, "code": -32600.0,
"message": "unauthorized Tool call: Please make sure you specify correct auth headers: <nil>", "message": "unauthorized Tool call: Please make sure your specify correct auth headers: unauthorized",
}, },
}, },
}, },

View File

@@ -184,7 +184,7 @@ func (t Tool) Invoke(ctx context.Context, resourceMgr tools.SourceProvider, para
if source.UseClientAuthorization() { if source.UseClientAuthorization() {
// Use client-side access token // Use client-side access token
if accessToken == "" { if accessToken == "" {
return nil, util.NewClientServerError("tool is configured for client OAuth but no token was provided in the request header", http.StatusUnauthorized, nil) return nil, fmt.Errorf("tool is configured for client OAuth but no token was provided in the request header: %w", util.ErrUnauthorized)
} }
tokenStr, err = accessToken.ParseBearerToken() tokenStr, err = accessToken.ParseBearerToken()
if err != nil { if err != nil {

View File

@@ -17,7 +17,6 @@ package tools
import ( import (
"context" "context"
"fmt" "fmt"
"net/http"
"slices" "slices"
"strings" "strings"
@@ -81,7 +80,7 @@ type AccessToken string
func (token AccessToken) ParseBearerToken() (string, error) { func (token AccessToken) ParseBearerToken() (string, error) {
headerParts := strings.Split(string(token), " ") headerParts := strings.Split(string(token), " ")
if len(headerParts) != 2 || strings.ToLower(headerParts[0]) != "bearer" { if len(headerParts) != 2 || strings.ToLower(headerParts[0]) != "bearer" {
return "", util.NewClientServerError("authorization header must be in the format 'Bearer <token>'", http.StatusUnauthorized, nil) return "", fmt.Errorf("authorization header must be in the format 'Bearer <token>': %w", util.ErrUnauthorized)
} }
return headerParts[1], nil return headerParts[1], nil
} }

View File

@@ -1,77 +0,0 @@
// 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 util
import "fmt"
type ErrorCategory string
const (
CategoryAgent ErrorCategory = "AGENT_ERROR"
CategoryServer ErrorCategory = "SERVER_ERROR"
)
// ToolboxError is the interface all custom errors must satisfy
type ToolboxError interface {
error
Category() ErrorCategory
Error() string
Unwrap() error
}
// Agent Errors return 200 to the sender
type AgentError struct {
Msg string
Cause error
}
var _ ToolboxError = &AgentError{}
func (e *AgentError) Error() string {
if e.Cause != nil {
return fmt.Sprintf("%s: %v", e.Msg, e.Cause)
}
return e.Msg
}
func (e *AgentError) Category() ErrorCategory { return CategoryAgent }
func (e *AgentError) Unwrap() error { return e.Cause }
func NewAgentError(msg string, cause error) *AgentError {
return &AgentError{Msg: msg, Cause: cause}
}
// ClientServerError returns 4XX/5XX error code
type ClientServerError struct {
Msg string
Code int
Cause error
}
var _ ToolboxError = &ClientServerError{}
func (e *ClientServerError) Error() string {
if e.Cause != nil {
return fmt.Sprintf("%s: %v", e.Msg, e.Cause)
}
return e.Msg
}
func (e *ClientServerError) Category() ErrorCategory { return CategoryServer }
func (e *ClientServerError) Unwrap() error { return e.Cause }
func NewClientServerError(msg string, code int, cause error) *ClientServerError {
return &ClientServerError{Msg: msg, Code: code, Cause: cause}
}

View File

@@ -19,7 +19,6 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/http"
"reflect" "reflect"
"regexp" "regexp"
"slices" "slices"
@@ -119,7 +118,7 @@ func parseFromAuthService(paramAuthServices []ParamAuthService, claimsMap map[st
} }
return v, nil return v, nil
} }
return nil, util.NewClientServerError("missing or invalid authentication header", http.StatusUnauthorized, nil) return nil, fmt.Errorf("missing or invalid authentication header: %w", util.ErrUnauthorized)
} }
// CheckParamRequired checks if a parameter is required based on the required and default field. // CheckParamRequired checks if a parameter is required based on the required and default field.

View File

@@ -17,6 +17,7 @@ import (
"bytes" "bytes"
"context" "context"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
@@ -187,3 +188,5 @@ func InstrumentationFromContext(ctx context.Context) (*telemetry.Instrumentation
} }
return nil, fmt.Errorf("unable to retrieve instrumentation") return nil, fmt.Errorf("unable to retrieve instrumentation")
} }
var ErrUnauthorized = errors.New("unauthorized")