diff --git a/docs/en/how-to/connect_via_mcp.md b/docs/en/how-to/connect_via_mcp.md index d994a84be5e1..f04d8f75eb24 100644 --- a/docs/en/how-to/connect_via_mcp.md +++ b/docs/en/how-to/connect_via_mcp.md @@ -20,6 +20,7 @@ The native SDKs can be combined with MCP clients in many cases. Toolbox currently supports the following versions of MCP specification: +* [2025-11-25](https://modelcontextprotocol.io/specification/2025-11-25) * [2025-06-18](https://modelcontextprotocol.io/specification/2025-06-18) * [2025-03-26](https://modelcontextprotocol.io/specification/2025-03-26) * [2024-11-05](https://modelcontextprotocol.io/specification/2024-11-05) diff --git a/internal/server/mcp/mcp.go b/internal/server/mcp/mcp.go index 74ff1bee59c6..befd3d7fb996 100644 --- a/internal/server/mcp/mcp.go +++ b/internal/server/mcp/mcp.go @@ -27,19 +27,21 @@ import ( v20241105 "github.com/googleapis/genai-toolbox/internal/server/mcp/v20241105" v20250326 "github.com/googleapis/genai-toolbox/internal/server/mcp/v20250326" v20250618 "github.com/googleapis/genai-toolbox/internal/server/mcp/v20250618" + v20251125 "github.com/googleapis/genai-toolbox/internal/server/mcp/v20251125" "github.com/googleapis/genai-toolbox/internal/server/resources" "github.com/googleapis/genai-toolbox/internal/tools" ) // LATEST_PROTOCOL_VERSION is the latest version of the MCP protocol supported. // Update the version used in InitializeResponse when this value is updated. -const LATEST_PROTOCOL_VERSION = v20250618.PROTOCOL_VERSION +const LATEST_PROTOCOL_VERSION = v20251125.PROTOCOL_VERSION // SUPPORTED_PROTOCOL_VERSIONS is the MCP protocol versions that are supported. var SUPPORTED_PROTOCOL_VERSIONS = []string{ v20241105.PROTOCOL_VERSION, v20250326.PROTOCOL_VERSION, v20250618.PROTOCOL_VERSION, + v20251125.PROTOCOL_VERSION, } // InitializeResponse runs capability negotiation and protocol version agreement. @@ -102,6 +104,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 v20251125.PROTOCOL_VERSION: + return v20251125.ProcessMethod(ctx, id, method, toolset, promptset, resourceMgr, body, header) case v20250618.PROTOCOL_VERSION: return v20250618.ProcessMethod(ctx, id, method, toolset, promptset, resourceMgr, body, header) case v20250326.PROTOCOL_VERSION: diff --git a/internal/server/mcp/v20251125/method.go b/internal/server/mcp/v20251125/method.go new file mode 100644 index 000000000000..8d2ae77587fa --- /dev/null +++ b/internal/server/mcp/v20251125/method.go @@ -0,0 +1,326 @@ +// 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 v20251125 + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "strings" + + "github.com/googleapis/genai-toolbox/internal/prompts" + "github.com/googleapis/genai-toolbox/internal/server/mcp/jsonrpc" + "github.com/googleapis/genai-toolbox/internal/server/resources" + "github.com/googleapis/genai-toolbox/internal/tools" + "github.com/googleapis/genai-toolbox/internal/util" +) + +// ProcessMethod returns a response for the request. +func ProcessMethod(ctx context.Context, id jsonrpc.RequestId, method string, toolset tools.Toolset, promptset prompts.Promptset, resourceMgr *resources.ResourceManager, body []byte, header http.Header) (any, error) { + switch method { + case PING: + return pingHandler(id) + case TOOLS_LIST: + return toolsListHandler(id, toolset, body) + case TOOLS_CALL: + return toolsCallHandler(ctx, id, resourceMgr, body, header) + case PROMPTS_LIST: + return promptsListHandler(ctx, id, promptset, body) + case PROMPTS_GET: + return promptsGetHandler(ctx, id, resourceMgr, body) + default: + err := fmt.Errorf("invalid method %s", method) + return jsonrpc.NewError(id, jsonrpc.METHOD_NOT_FOUND, err.Error(), nil), err + } +} + +// pingHandler handles the "ping" method by returning an empty response. +func pingHandler(id jsonrpc.RequestId) (any, error) { + return jsonrpc.JSONRPCResponse{ + Jsonrpc: jsonrpc.JSONRPC_VERSION, + Id: id, + Result: struct{}{}, + }, nil +} + +func toolsListHandler(id jsonrpc.RequestId, toolset tools.Toolset, body []byte) (any, error) { + var req ListToolsRequest + if err := json.Unmarshal(body, &req); err != nil { + err = fmt.Errorf("invalid mcp tools list request: %w", err) + return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err + } + + result := ListToolsResult{ + Tools: toolset.McpManifest, + } + return jsonrpc.JSONRPCResponse{ + Jsonrpc: jsonrpc.JSONRPC_VERSION, + Id: id, + Result: result, + }, nil +} + +// toolsCallHandler generate a response for tools call. +func toolsCallHandler(ctx context.Context, id jsonrpc.RequestId, resourceMgr *resources.ResourceManager, body []byte, header http.Header) (any, error) { + authServices := resourceMgr.GetAuthServiceMap() + + // retrieve logger from context + logger, err := util.LoggerFromContext(ctx) + if err != nil { + return jsonrpc.NewError(id, jsonrpc.INTERNAL_ERROR, err.Error(), nil), err + } + + var req CallToolRequest + if err = json.Unmarshal(body, &req); err != nil { + err = fmt.Errorf("invalid mcp tools call request: %w", err) + return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err + } + + toolName := req.Params.Name + toolArgument := req.Params.Arguments + logger.DebugContext(ctx, fmt.Sprintf("tool name: %s", toolName)) + tool, ok := resourceMgr.GetTool(toolName) + if !ok { + err = fmt.Errorf("invalid tool name: tool with name %q does not exist", toolName) + return jsonrpc.NewError(id, jsonrpc.INVALID_PARAMS, err.Error(), nil), err + } + + // Get access token + authTokenHeadername, err := tool.GetAuthTokenHeaderName(resourceMgr) + if err != nil { + errMsg := fmt.Errorf("error during invocation: %w", err) + return jsonrpc.NewError(id, jsonrpc.INTERNAL_ERROR, errMsg.Error(), nil), errMsg + } + accessToken := tools.AccessToken(header.Get(authTokenHeadername)) + + // Check if this specific tool requires the standard authorization header + clientAuth, err := tool.RequiresClientAuthorization(resourceMgr) + if err != nil { + errMsg := fmt.Errorf("error during invocation: %w", err) + return jsonrpc.NewError(id, jsonrpc.INTERNAL_ERROR, errMsg.Error(), nil), errMsg + } + if clientAuth { + if accessToken == "" { + return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, "missing access token in the 'Authorization' header", nil), util.ErrUnauthorized + } + } + + // marshal arguments and decode it using decodeJSON instead to prevent loss between floats/int. + aMarshal, err := json.Marshal(toolArgument) + if err != nil { + err = fmt.Errorf("unable to marshal tools argument: %w", err) + return jsonrpc.NewError(id, jsonrpc.INTERNAL_ERROR, err.Error(), nil), err + } + + var data map[string]any + if err = util.DecodeJSON(bytes.NewBuffer(aMarshal), &data); err != nil { + err = fmt.Errorf("unable to decode tools argument: %w", err) + return jsonrpc.NewError(id, jsonrpc.INTERNAL_ERROR, err.Error(), nil), err + } + + // Tool authentication + // claimsFromAuth maps the name of the authservice to the claims retrieved from it. + claimsFromAuth := make(map[string]map[string]any) + + // if using stdio, header will be nil and auth will not be supported + if header != nil { + for _, aS := range authServices { + claims, err := aS.GetClaimsFromHeader(ctx, header) + if err != nil { + logger.DebugContext(ctx, err.Error()) + continue + } + if claims == nil { + // authService not present in header + continue + } + claimsFromAuth[aS.GetName()] = claims + } + } + + // Tool authorization check + verifiedAuthServices := make([]string, len(claimsFromAuth)) + i := 0 + for k := range claimsFromAuth { + verifiedAuthServices[i] = k + i++ + } + + // Check if any of the specified auth services is verified + isAuthorized := tool.Authorized(verifiedAuthServices) + if !isAuthorized { + err = fmt.Errorf("unauthorized Tool call: Please make sure your specify correct auth headers: %w", util.ErrUnauthorized) + return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err + } + logger.DebugContext(ctx, "tool invocation authorized") + + params, err := tool.ParseParams(data, claimsFromAuth) + if err != nil { + err = fmt.Errorf("provided parameters were invalid: %w", err) + return jsonrpc.NewError(id, jsonrpc.INVALID_PARAMS, err.Error(), nil), err + } + logger.DebugContext(ctx, fmt.Sprintf("invocation params: %s", params)) + + // run tool invocation and generate response. + results, err := tool.Invoke(ctx, resourceMgr, params, accessToken) + if err != nil { + errStr := err.Error() + // Missing authService tokens. + if errors.Is(err, util.ErrUnauthorized) { + return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err + } + // Upstream auth error + if strings.Contains(errStr, "Error 401") || strings.Contains(errStr, "Error 403") { + if clientAuth { + // Error with client credentials should pass down to the client + return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err + } + // Auth error with ADC should raise internal 500 error + 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) + + sliceRes, ok := results.([]any) + if !ok { + sliceRes = []any{results} + } + + for _, d := range sliceRes { + text := TextContent{Type: "text"} + dM, err := json.Marshal(d) + if err != nil { + text.Text = fmt.Sprintf("fail to marshal: %s, result: %s", err, d) + } else { + text.Text = string(dM) + } + content = append(content, text) + } + + return jsonrpc.JSONRPCResponse{ + Jsonrpc: jsonrpc.JSONRPC_VERSION, + Id: id, + Result: CallToolResult{Content: content}, + }, nil +} + +// promptsListHandler handles the "prompts/list" method. +func promptsListHandler(ctx context.Context, id jsonrpc.RequestId, promptset prompts.Promptset, body []byte) (any, error) { + // retrieve logger from context + logger, err := util.LoggerFromContext(ctx) + if err != nil { + return jsonrpc.NewError(id, jsonrpc.INTERNAL_ERROR, err.Error(), nil), err + } + logger.DebugContext(ctx, "handling prompts/list request") + + var req ListPromptsRequest + if err := json.Unmarshal(body, &req); err != nil { + err = fmt.Errorf("invalid mcp prompts list request: %w", err) + return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err + } + + result := ListPromptsResult{ + Prompts: promptset.McpManifest, + } + logger.DebugContext(ctx, fmt.Sprintf("returning %d prompts", len(promptset.McpManifest))) + return jsonrpc.JSONRPCResponse{ + Jsonrpc: jsonrpc.JSONRPC_VERSION, + Id: id, + Result: result, + }, nil +} + +// promptsGetHandler handles the "prompts/get" method. +func promptsGetHandler(ctx context.Context, id jsonrpc.RequestId, resourceMgr *resources.ResourceManager, body []byte) (any, error) { + // retrieve logger from context + logger, err := util.LoggerFromContext(ctx) + if err != nil { + return jsonrpc.NewError(id, jsonrpc.INTERNAL_ERROR, err.Error(), nil), err + } + logger.DebugContext(ctx, "handling prompts/get request") + + var req GetPromptRequest + if err := json.Unmarshal(body, &req); err != nil { + err = fmt.Errorf("invalid mcp prompts/get request: %w", err) + return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err + } + + promptName := req.Params.Name + logger.DebugContext(ctx, fmt.Sprintf("prompt name: %s", promptName)) + prompt, ok := resourceMgr.GetPrompt(promptName) + if !ok { + err := fmt.Errorf("prompt with name %q does not exist", promptName) + return jsonrpc.NewError(id, jsonrpc.INVALID_PARAMS, err.Error(), nil), err + } + + // Parse the arguments provided in the request. + argValues, err := prompt.ParseArgs(req.Params.Arguments, nil) + if err != nil { + err = fmt.Errorf("invalid arguments for prompt %q: %w", promptName, err) + return jsonrpc.NewError(id, jsonrpc.INVALID_PARAMS, err.Error(), nil), err + } + logger.DebugContext(ctx, fmt.Sprintf("parsed args: %v", argValues)) + + // Substitute the argument values into the prompt's messages. + substituted, err := prompt.SubstituteParams(argValues) + if err != nil { + err = fmt.Errorf("error substituting params for prompt %q: %w", promptName, err) + return jsonrpc.NewError(id, jsonrpc.INTERNAL_ERROR, err.Error(), nil), err + } + + // Cast the result to the expected []prompts.Message type. + substitutedMessages, ok := substituted.([]prompts.Message) + if !ok { + err = fmt.Errorf("internal error: SubstituteParams returned unexpected type") + return jsonrpc.NewError(id, jsonrpc.INTERNAL_ERROR, err.Error(), nil), err + } + logger.DebugContext(ctx, "substituted params successfully") + + // Format the response messages into the required structure. + promptMessages := make([]PromptMessage, len(substitutedMessages)) + for i, msg := range substitutedMessages { + promptMessages[i] = PromptMessage{ + Role: msg.Role, + Content: TextContent{ + Type: "text", + Text: msg.Content, + }, + } + } + + result := GetPromptResult{ + Description: prompt.Manifest().Description, + Messages: promptMessages, + } + + return jsonrpc.JSONRPCResponse{ + Jsonrpc: jsonrpc.JSONRPC_VERSION, + Id: id, + Result: result, + }, nil +} diff --git a/internal/server/mcp/v20251125/types.go b/internal/server/mcp/v20251125/types.go new file mode 100644 index 000000000000..c45326382e26 --- /dev/null +++ b/internal/server/mcp/v20251125/types.go @@ -0,0 +1,219 @@ +// 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 v20251125 + +import ( + "github.com/googleapis/genai-toolbox/internal/prompts" + "github.com/googleapis/genai-toolbox/internal/server/mcp/jsonrpc" + "github.com/googleapis/genai-toolbox/internal/tools" +) + +// SERVER_NAME is the server name used in Implementation. +const SERVER_NAME = "Toolbox" + +// PROTOCOL_VERSION is the version of the MCP protocol in this package. +const PROTOCOL_VERSION = "2025-11-25" + +// methods that are supported. +const ( + PING = "ping" + TOOLS_LIST = "tools/list" + TOOLS_CALL = "tools/call" + PROMPTS_LIST = "prompts/list" + PROMPTS_GET = "prompts/get" +) + +/* Empty result */ + +// EmptyResult represents a response that indicates success but carries no data. +type EmptyResult jsonrpc.Result + +/* Pagination */ + +// Cursor is an opaque token used to represent a cursor for pagination. +type Cursor string + +type PaginatedRequest struct { + jsonrpc.Request + Params struct { + // An opaque token representing the current pagination position. + // If provided, the server should return results starting after this cursor. + Cursor Cursor `json:"cursor,omitempty"` + } `json:"params,omitempty"` +} + +type PaginatedResult struct { + jsonrpc.Result + // An opaque token representing the pagination position after the last returned result. + // If present, there may be more results available. + NextCursor Cursor `json:"nextCursor,omitempty"` +} + +/* Tools */ + +// Sent from the client to request a list of tools the server has. +type ListToolsRequest struct { + PaginatedRequest +} + +// The server's response to a tools/list request from the client. +type ListToolsResult struct { + PaginatedResult + Tools []tools.McpManifest `json:"tools"` +} + +// Used by the client to invoke a tool provided by the server. +type CallToolRequest struct { + jsonrpc.Request + Params struct { + Name string `json:"name"` + Arguments map[string]any `json:"arguments,omitempty"` + } `json:"params,omitempty"` +} + +// The sender or recipient of messages and data in a conversation. +type Role string + +const ( + RoleUser Role = "user" + RoleAssistant Role = "assistant" +) + +// Base for objects that include optional annotations for the client. +// The client can use annotations to inform how objects are used or displayed +type Annotated struct { + Annotations *struct { + // Describes who the intended customer of this object or data is. + // It can include multiple entries to indicate content useful for multiple + // audiences (e.g., `["user", "assistant"]`). + Audience []Role `json:"audience,omitempty"` + // Describes how important this data is for operating the server. + // + // A value of 1 means "most important," and indicates that the data is + // effectively required, while 0 means "least important," and indicates that + // the data is entirely optional. + // + // @TJS-type number + // @minimum 0 + // @maximum 1 + Priority float64 `json:"priority,omitempty"` + } `json:"annotations,omitempty"` +} + +// TextContent represents text provided to or from an LLM. +type TextContent struct { + Annotated + Type string `json:"type"` + // The text content of the message. + Text string `json:"text"` +} + +// The server's response to a tool call. +// +// Any errors that originate from the tool SHOULD be reported inside the result +// object, with `isError` set to true, _not_ as an MCP protocol-level error +// response. Otherwise, the LLM would not be able to see that an error occurred +// and self-correct. +// +// However, any errors in _finding_ the tool, an error indicating that the +// server does not support tool calls, or any other exceptional conditions, +// should be reported as an MCP error response. +type CallToolResult struct { + jsonrpc.Result + // Could be either a TextContent, ImageContent, or EmbeddedResources + // For Toolbox, we will only be sending TextContent + Content []TextContent `json:"content"` + // Whether the tool call ended in an error. + // If not set, this is assumed to be false (the call was successful). + // + // Any errors that originate from the tool SHOULD be reported inside the result + // object, with `isError` set to true, _not_ as an MCP protocol-level error + // response. Otherwise, the LLM would not be able to see that an error occurred + // and self-correct. + // + // However, any errors in _finding_ the tool, an error indicating that the + // server does not support tool calls, or any other exceptional conditions, + // should be reported as an MCP error response. + IsError bool `json:"isError,omitempty"` + // An optional JSON object that represents the structured result of the tool call. + StructuredContent map[string]any `json:"structuredContent,omitempty"` +} + +// Additional properties describing a Tool to clients. +// +// NOTE: all properties in ToolAnnotations are **hints**. +// They are not guaranteed to provide a faithful description of +// tool behavior (including descriptive properties like `title`). +// +// Clients should never make tool use decisions based on ToolAnnotations +// received from untrusted servers. +type ToolAnnotations struct { + // A human-readable title for the tool. + Title string `json:"title,omitempty"` + // If true, the tool does not modify its environment. + // Default: false + ReadOnlyHint bool `json:"readOnlyHint,omitempty"` + // If true, the tool may perform destructive updates to its environment. + // If false, the tool performs only additive updates. + // (This property is meaningful only when `readOnlyHint == false`) + // Default: true + DestructiveHint bool `json:"destructiveHint,omitempty"` + // If true, calling the tool repeatedly with the same arguments + // will have no additional effect on the its environment. + // (This property is meaningful only when `readOnlyHint == false`) + // Default: false + IdempotentHint bool `json:"idempotentHint,omitempty"` + // If true, this tool may interact with an "open world" of external + // entities. If false, the tool's domain of interaction is closed. + // For example, the world of a web search tool is open, whereas that + // of a memory tool is not. + // Default: true + OpenWorldHint bool `json:"openWorldHint,omitempty"` +} + +/* Prompts */ + +// Sent from the client to request a list of prompts the server has. +type ListPromptsRequest struct { + PaginatedRequest +} + +// The server's response to a prompts/list request from the client. +type ListPromptsResult struct { + PaginatedResult + Prompts []prompts.McpManifest `json:"prompts"` +} + +// Used by the client to get a prompt provided by the server. +type GetPromptRequest struct { + jsonrpc.Request + Params struct { + Name string `json:"name"` + Arguments map[string]any `json:"arguments,omitempty"` + } `json:"params"` +} + +// The server's response to a prompts/get request from the client. +type GetPromptResult struct { + jsonrpc.Result + Description string `json:"description,omitempty"` + Messages []PromptMessage `json:"messages"` +} + +// Describes a message returned as part of a prompt. +type PromptMessage struct { + Role string `json:"role"` + Content TextContent `json:"content"` +} diff --git a/internal/server/mcp_test.go b/internal/server/mcp_test.go index ff6ffffe845e..0d50af2b249b 100644 --- a/internal/server/mcp_test.go +++ b/internal/server/mcp_test.go @@ -37,6 +37,7 @@ const jsonrpcVersion = "2.0" const protocolVersion20241105 = "2024-11-05" const protocolVersion20250326 = "2025-03-26" const protocolVersion20250618 = "2025-06-18" +const protocolVersion20251125 = "2025-11-25" const serverName = "Toolbox" var basicInputSchema = map[string]any{ @@ -485,6 +486,23 @@ func TestMcpEndpoint(t *testing.T) { }, }, }, + { + name: "version 2025-11-25", + protocol: protocolVersion20251125, + idHeader: false, + initWant: map[string]any{ + "jsonrpc": "2.0", + "id": "mcp-initialize", + "result": map[string]any{ + "protocolVersion": "2025-11-25", + "capabilities": map[string]any{ + "tools": map[string]any{"listChanged": false}, + "prompts": map[string]any{"listChanged": false}, + }, + "serverInfo": map[string]any{"name": serverName, "version": fakeVersionString}, + }, + }, + }, } for _, vtc := range versTestCases { t.Run(vtc.name, func(t *testing.T) { @@ -494,8 +512,7 @@ func TestMcpEndpoint(t *testing.T) { if sessionId != "" { header["Mcp-Session-Id"] = sessionId } - - if vtc.protocol == protocolVersion20250618 { + if vtc.protocol != protocolVersion20241105 && vtc.protocol != protocolVersion20250326 { header["MCP-Protocol-Version"] = vtc.protocol } diff --git a/internal/server/server.go b/internal/server/server.go index 1db4fcf1dab2..5f63ba24301c 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -304,10 +304,14 @@ func hostCheck(allowedHosts map[string]struct{}) func(http.Handler) http.Handler return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, hasWildcard := allowedHosts["*"] - _, hostIsAllowed := allowedHosts[r.Host] + hostname := r.Host + if host, _, err := net.SplitHostPort(r.Host); err == nil { + hostname = host + } + _, hostIsAllowed := allowedHosts[hostname] if !hasWildcard && !hostIsAllowed { - // Return 400 Bad Request or 403 Forbidden to block the attack - http.Error(w, "Invalid Host header", http.StatusBadRequest) + // Return 403 Forbidden to block the attack + http.Error(w, "Invalid Host header", http.StatusForbidden) return } next.ServeHTTP(w, r) @@ -406,7 +410,11 @@ func NewServer(ctx context.Context, cfg ServerConfig) (*Server, error) { } allowedHostsMap := make(map[string]struct{}, len(cfg.AllowedHosts)) for _, h := range cfg.AllowedHosts { - allowedHostsMap[h] = struct{}{} + hostname := h + if host, _, err := net.SplitHostPort(h); err == nil { + hostname = host + } + allowedHostsMap[hostname] = struct{}{} } r.Use(hostCheck(allowedHostsMap)) diff --git a/server.json b/server.json index b72200be3a38..123e0ede75fc 100644 --- a/server.json +++ b/server.json @@ -31,6 +31,18 @@ "default": "tools.yaml", "isRequired": false }, + { + "type": "named", + "name": "--tools-files", + "description": "Multiple file paths specifying tool configurations. Files will be merged. Cannot be used with –-tools-file or –-tools-folder.", + "isRequired": false + }, + { + "type": "named", + "name": "--tools-folder", + "description": "Directory path containing YAML tool configuration files. All .yaml and .yml files in the directory will be loaded and merged. Cannot be used with –-tools-file or –-tools-files.", + "isRequired": false + }, { "type": "named", "name": "--address", @@ -70,6 +82,82 @@ "warn", "error" ] + }, + { + "type": "named", + "name": "--logging-format", + "description": "Specify logging format to use.", + "default": "standard", + "choices": [ + "standard", + "json" + ] + }, + { + "type": "named", + "name": "--disable-reload", + "description": "Disables dynamic reloading of tools file.", + "format": "boolean", + "isRequired": false + }, + { + "type": "named", + "name": "--prebuilt", + "description": "Use a prebuilt tool configuration by source type.", + "isRequired": false + }, + { + "type": "named", + "name": "--stdio", + "description": "Listens via MCP STDIO instead of acting as a remote HTTP server.", + "format": "boolean", + "isRequired": false + }, + { + "type": "named", + "name": "--telemetry-gcp", + "description": "Enable exporting directly to Google Cloud Monitoring.", + "format": "boolean", + "isRequired": false + }, + { + "type": "named", + "name": "--telemetry-otlp", + "description": "Enable exporting using OpenTelemetry Protocol (OTLP) to the specified endpoint (e.g. 'http://127.0.0.1:4318').", + "isRequired": false + }, + { + "type": "named", + "name": "--telemetry-service-name", + "description": "Sets the value of the service.name resource attribute for telemetry data.", + "default": "toolbox", + "isRequired": false + }, + { + "type": "named", + "name": "--ui", + "description": "Launches the Toolbox UI web server.", + "format": "boolean", + "isRequired": false + }, + { + "type": "named", + "name": "--allowed-origins", + "description": "Specifies a list of origins permitted to access this server.", + "default": "*", + "isRequired": false + }, + { + "type": "named", + "name": "--help", + "description": "Show help for toolbox", + "isRequired": false + }, + { + "type": "named", + "name": "--version", + "description": "Show version for toolbox", + "isRequired": false } ] }