Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions docs/en/documentation/configuration/tools/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,40 @@ parameters:
valueType: integer # This enforces the value type for all entries.
```


### Secure Parameters

Secure parameters are designed to handle sensitive information (such as API keys, passwords, and credentials) that should not be transmitted in plain text through standard client-server arguments or logs.

Secure parameters require negotiation of the draft protocol version (`DRAFT-2026-v1`) and the client capability `toolbox/secure-params` to be set to `true`.

To configure a parameter as secure, set the `secure` field to `true` in your tool's parameter definition:

```yaml
kind: tool
name: search_secure_data
type: postgres-sql
source: my-pg-instance
statement: |
SELECT * FROM sessions WHERE token = $1
parameters:
- name: sessionToken
type: string
description: Sensitive session token
secure: true
```

#### Protocol Constraints

When a parameter is marked as `secure: true`, the following strict rules are enforced at the transport level:

1. **Capability Requirement**: If a tool has one or more secure parameters, the client **must** negotiate the `"DRAFT-2026-v1"` protocol version during initialization and declare support for `toolbox/secure-params` in its capabilities. Otherwise, invoking the tool will return a `jsonrpc.INVALID_PARAMS` error.
2. **Mutual Exclusion (Configuration)**: A parameter **cannot** have both `secure: true` and `authServices` specified. An error will be thrown during configuration loading.
3. **Mutual Exclusion (Arguments)**:
- Parameters marked as `secure` **must** be passed in the request's `secureArguments` map, and **must not** be passed in the standard `arguments` map.
- Parameters **not** marked as `secure` **must** be passed in the standard `arguments` map, and **must not** be passed in the `secureArguments` map.
- Any violation of these rules will result in a `jsonrpc.INVALID_PARAMS` error and execution will be blocked.

### Authenticated Parameters

Authenticated parameters are automatically populated with user
Expand Down Expand Up @@ -193,6 +227,7 @@ parameters:
| name | string | true | Name of the [authServices](../authentication/_index.md) used to verify the OIDC auth token. |
| field | string | true | Claim field decoded from the OIDC token used to auto-populate this parameter. |


### Template Parameters

Template parameters types include `string`, `integer`, `float`, `boolean` types.
Expand Down
3 changes: 3 additions & 0 deletions internal/server/mcp/mcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
v20250326 "github.com/googleapis/mcp-toolbox/internal/server/mcp/v20250326"
v20250618 "github.com/googleapis/mcp-toolbox/internal/server/mcp/v20250618"
v20251125 "github.com/googleapis/mcp-toolbox/internal/server/mcp/v20251125"
vdraft "github.com/googleapis/mcp-toolbox/internal/server/mcp/vdraft"
"github.com/googleapis/mcp-toolbox/internal/server/resources"
"github.com/googleapis/mcp-toolbox/internal/tools"
)
Expand All @@ -46,6 +47,8 @@ func NotificationHandler(ctx context.Context, body []byte) error {
// 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, promptset prompts.Promptset, resourceMgr *resources.ResourceManager, body []byte, header http.Header) (any, error) {
switch mcpVersion {
case vdraft.PROTOCOL_VERSION:
return vdraft.ProcessMethod(ctx, id, method, toolset, promptset, resourceMgr, body, header)
case v20251125.PROTOCOL_VERSION:
return v20251125.ProcessMethod(ctx, id, method, toolset, promptset, resourceMgr, body, header)
case v20250618.PROTOCOL_VERSION:
Expand Down
28 changes: 26 additions & 2 deletions internal/server/mcp/vdraft/manifests.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,17 @@ import (

// generateToolManifest generates Tool for list tools result
func generateToolManifest(name, desc string, authInvoke []string, params parameters.Parameters, annotations *tools.ToolAnnotations) Tool {
inputSchema, authParams := generateParamManifest(params)
var standardParams parameters.Parameters
var secureParams parameters.Parameters
for _, p := range params {
if p.GetSecure() {
secureParams = append(secureParams, p)
} else {
standardParams = append(standardParams, p)
}
}
Comment thread
duwenxin99 marked this conversation as resolved.

inputSchema, authParams := generateParamManifest(standardParams)
var toolAnnotations *ToolAnnotations
if annotations != nil {
toolAnnotations = &ToolAnnotations{
Expand All @@ -42,6 +52,10 @@ func generateToolManifest(name, desc string, authInvoke []string, params paramet
ToolInputSchema: inputSchema,
Annotations: toolAnnotations,
}
if len(secureParams) > 0 {
secureInputSchema, _ := generateParamManifest(secureParams)
mcpManifest.SecureInputSchema = &secureInputSchema
}
Comment thread
duwenxin99 marked this conversation as resolved.
metadata := make(map[string]any)
if len(authInvoke) > 0 {
metadata["toolbox/authInvoke"] = authInvoke
Expand Down Expand Up @@ -90,13 +104,23 @@ func generateParamManifest(ps parameters.Parameters) (InputSchema, map[string][]
}

// GenerateListToolsResult generates tools/list method result according to mcp schema
func GenerateListToolsResult(t tools.Toolset, toolsMap map[string]tools.Tool) (ListToolsResult, error) {
func GenerateListToolsResult(t tools.Toolset, toolsMap map[string]tools.Tool, supportsSecureParams bool) (ListToolsResult, error) {
mcpManifest := make([]Tool, 0, len(t.ToolNames))
for _, toolName := range t.ToolNames {
tool, ok := toolsMap[toolName]
if !ok {
return ListToolsResult{}, fmt.Errorf("tool does not exist: %s", toolName)
}
var hasSecureParams bool
for _, p := range tool.GetParameters() {
if p.GetSecure() {
hasSecureParams = true
break
}
}
Comment thread
duwenxin99 marked this conversation as resolved.
if hasSecureParams && !supportsSecureParams {
continue
}
toolManifest := generateToolManifest(toolName, tool.GetDescription(), tool.GetAuthRequired(), tool.GetParameters(), tool.GetAnnotations())
mcpManifest = append(mcpManifest, toolManifest)
}
Expand Down
110 changes: 109 additions & 1 deletion internal/server/mcp/vdraft/manifests_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ func TestGenerateListToolsResult(t *testing.T) {
t.Fatalf("unable to initialize toolset %q: %s", "test-toolset", err)
}

got, err := GenerateListToolsResult(toolset, toolsMap)
got, err := GenerateListToolsResult(toolset, toolsMap, false)
if err != nil {
t.Fatalf("unable to generate list tools result: %s", err)
}
Expand Down Expand Up @@ -369,3 +369,111 @@ func TestGenerateListPromptsResult(t *testing.T) {
t.Fatalf("unexpected list tools result (-want +got):\n%s", diff)
}
}

func TestGenerateToolManifestWithSecureParams(t *testing.T) {
params := parameters.Parameters{
&parameters.StringParameter{
CommonParameter: parameters.CommonParameter{
Name: "standard_param",
Type: parameters.TypeString,
Desc: "A standard param",
},
},
&parameters.StringParameter{
CommonParameter: parameters.CommonParameter{
Name: "secure_param",
Type: parameters.TypeString,
Desc: "A secure param",
Secure: true,
},
},
}
got := generateToolManifest("test-tool", "desc", nil, params, nil)

// Validate standard schema
wantStandard := InputSchema{
Type: "object",
Properties: map[string]parameters.ParameterMcpManifest{
"standard_param": {
Type: "string",
Description: "A standard param",
},
},
Required: []string{"standard_param"},
}
if diff := cmp.Diff(wantStandard, got.ToolInputSchema); diff != "" {
t.Errorf("unexpected standard schema (-want +got):\n%s", diff)
}

// Validate secure schema
if got.SecureInputSchema == nil {
t.Fatal("expected secure input schema to be populated, got nil")
}
wantSecure := InputSchema{
Type: "object",
Properties: map[string]parameters.ParameterMcpManifest{
"secure_param": {
Type: "string",
Description: "A secure param",
},
},
Required: []string{"secure_param"},
}
if diff := cmp.Diff(wantSecure, *got.SecureInputSchema); diff != "" {
t.Errorf("unexpected secure schema (-want +got):\n%s", diff)
}
}

func TestGenerateListToolsResultWithSecureParamsFiltering(t *testing.T) {
paramsStandard := parameters.Parameters{
parameters.NewStringParameter("param1", "desc"),
}
paramsSecure := parameters.Parameters{
&parameters.StringParameter{
CommonParameter: parameters.CommonParameter{
Name: "param2",
Type: parameters.TypeString,
Desc: "desc",
Secure: true,
},
},
}

toolStandard := testutils.NewMockTool("standard_tool", "", paramsStandard, false, false)
toolSecure := testutils.NewMockTool("secure_tool", "", paramsSecure, false, false)

toolsMap := map[string]tools.Tool{
"standard_tool": toolStandard,
"secure_tool": toolSecure,
}

tc := tools.ToolsetConfig{
Name: "test-toolset",
ToolNames: []string{"standard_tool", "secure_tool"},
}
toolset, err := tc.Initialize("test-version", toolsMap)
if err != nil {
t.Fatalf("failed initializing toolset: %s", err)
}

// Case 1: Client does NOT support secure params
gotNoSupport, err := GenerateListToolsResult(toolset, toolsMap, false)
if err != nil {
t.Fatalf("failed GenerateListToolsResult: %s", err)
}
if len(gotNoSupport.Tools) != 1 {
t.Fatalf("expected only 1 tool (standard), got: %+v", gotNoSupport.Tools)
}
if gotNoSupport.Tools[0].Name != "standard_tool" {
t.Errorf("expected standard_tool, got: %s", gotNoSupport.Tools[0].Name)
}

// Case 2: Client DOES support secure params
gotSupport, err := GenerateListToolsResult(toolset, toolsMap, true)
if err != nil {
t.Fatalf("failed GenerateListToolsResult: %s", err)
}
if len(gotSupport.Tools) != 2 {
t.Fatalf("expected 2 tools, got: %+v", gotSupport.Tools)
}
}
75 changes: 72 additions & 3 deletions internal/server/mcp/vdraft/method.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"encoding/json"
"errors"
"fmt"
"maps"
"net/http"
"slices"
"strings"
Expand Down Expand Up @@ -117,8 +118,9 @@ func toolsListHandler(id jsonrpc.RequestId, resourceMgr *resources.ResourceManag
return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err
}

supportsSecureParams := parseSupportsSecureParams(body)
toolsMap := resourceMgr.GetToolsMap()
listToolsResult, err := GenerateListToolsResult(toolset, toolsMap)
listToolsResult, err := GenerateListToolsResult(toolset, toolsMap, supportsSecureParams)
if err != nil {
err = fmt.Errorf("error generating manifest: %w", err)
return jsonrpc.NewError(id, jsonrpc.INTERNAL_ERROR, err.Error(), nil), err
Expand Down Expand Up @@ -147,7 +149,6 @@ func toolsCallHandler(ctx context.Context, id jsonrpc.RequestId, toolset tools.T
}

toolName := req.Params.Name
toolArgument := req.Params.Arguments
logger.DebugContext(ctx, fmt.Sprintf("tool name: %s", toolName))

// Update span name and set gen_ai attributes
Expand All @@ -170,6 +171,11 @@ func toolsCallHandler(ctx context.Context, id jsonrpc.RequestId, toolset tools.T
return jsonrpc.NewError(id, jsonrpc.INVALID_PARAMS, err.Error(), nil), err
}

toolArguments, err := validateAndMergeSecureParams(tool, req, body)
if err != nil {
return jsonrpc.NewError(id, jsonrpc.INVALID_PARAMS, err.Error(), nil), err
}

// Populate gen_ai attributes for operation duration metric
if genAIAttrs := util.GenAIMetricAttrsFromContext(ctx); genAIAttrs != nil {
genAIAttrs.OperationName = "execute_tool"
Expand Down Expand Up @@ -202,7 +208,7 @@ func toolsCallHandler(ctx context.Context, id jsonrpc.RequestId, toolset tools.T
}

// marshal arguments and decode it using decodeJSON instead to prevent loss between floats/int.
aMarshal, err := json.Marshal(toolArgument)
aMarshal, err := json.Marshal(toolArguments)
if err != nil {
err = fmt.Errorf("unable to marshal tools argument: %w", err)
return jsonrpc.NewError(id, jsonrpc.INTERNAL_ERROR, err.Error(), nil), err
Expand Down Expand Up @@ -522,3 +528,66 @@ func promptsGetHandler(ctx context.Context, id jsonrpc.RequestId, promptset prom
Result: result,
}, nil
}

func parseSupportsSecureParams(body []byte) bool {
var meta struct {
Params struct {
Meta struct {
Capabilities map[string]any `json:"capabilities"`
} `json:"_meta"`
} `json:"params"`
}
if err := json.Unmarshal(body, &meta); err != nil {
return false
}
caps := meta.Params.Meta.Capabilities
if caps == nil {
return false
}
val, ok := caps["toolbox/secure-params"]
if !ok {
return false
}
supported, ok := val.(bool)
return ok && supported
}

// validateAndMergeSecureParams validates and merges standard and secure arguments based on the tool's parameter definitions.
func validateAndMergeSecureParams(tool tools.Tool, req CallToolRequest, body []byte) (map[string]any, error) {
// Validate capability and parameter routing
supportsSecureParams := parseSupportsSecureParams(body)

var hasSecureParams bool
secureParamMap := make(map[string]bool)
for _, p := range tool.GetParameters() {
if p.GetSecure() {
hasSecureParams = true
secureParamMap[p.GetName()] = true
}
}

if hasSecureParams && !supportsSecureParams {
return nil, fmt.Errorf("tool %q requires secure-params extension which is not supported by the client", req.Params.Name)
}

// Validate that secure parameters are only passed in secureArguments
for argName := range req.Params.Arguments {
if secureParamMap[argName] {
return nil, fmt.Errorf("parameter %q is secure and must not be passed in standard arguments", argName)
}
}

// Validate that non-secure parameters are not passed in secureArguments
for argName := range req.Params.SecureArguments {
if !secureParamMap[argName] {
return nil, fmt.Errorf("parameter %q is not secure and must not be passed in secureArguments", argName)
}
}

// Merge standard arguments and secure arguments using standard maps.Copy
toolArgument := make(map[string]any)
maps.Copy(toolArgument, req.Params.Arguments)
maps.Copy(toolArgument, req.Params.SecureArguments)

return toolArgument, nil
}
Loading
Loading