e04ed9c211
CF: Deploy Dev Docs / deploy (push) Has been cancelled
Sync Labels / build (push) Has been cancelled
tests / unit tests (macos-latest) (push) Has been cancelled
tests / unit tests (windows-latest) (push) Has been cancelled
tests / unit tests (ubuntu-latest) (push) Has been cancelled
602 lines
21 KiB
Go
602 lines
21 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 vdraft
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/googleapis/mcp-toolbox/internal/auth"
|
|
"github.com/googleapis/mcp-toolbox/internal/prompts"
|
|
"github.com/googleapis/mcp-toolbox/internal/server/mcp/jsonrpc"
|
|
mcputil "github.com/googleapis/mcp-toolbox/internal/server/mcp/util"
|
|
"github.com/googleapis/mcp-toolbox/internal/server/resources"
|
|
"github.com/googleapis/mcp-toolbox/internal/tools"
|
|
"github.com/googleapis/mcp-toolbox/internal/util"
|
|
"github.com/googleapis/mcp-toolbox/internal/util/parameters"
|
|
"go.opentelemetry.io/otel/attribute"
|
|
"go.opentelemetry.io/otel/metric"
|
|
"go.opentelemetry.io/otel/trace"
|
|
)
|
|
|
|
// 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 SERVER_DISCOVER:
|
|
return serverDiscoverHandler(ctx, id, body, header)
|
|
case TOOLS_LIST:
|
|
return toolsListHandler(ctx, id, resourceMgr, toolset, body, header)
|
|
case TOOLS_CALL:
|
|
return toolsCallHandler(ctx, id, toolset, resourceMgr, body, header)
|
|
case PROMPTS_LIST:
|
|
return promptsListHandler(ctx, id, resourceMgr, promptset, body, header)
|
|
case PROMPTS_GET:
|
|
return promptsGetHandler(ctx, id, promptset, resourceMgr, body, header)
|
|
default:
|
|
err := fmt.Errorf("invalid method %s", method)
|
|
return jsonrpc.NewError(id, jsonrpc.METHOD_NOT_FOUND, err.Error(), nil), err
|
|
}
|
|
}
|
|
|
|
// validateMetadata checks the metadata of every requests
|
|
func validateMetadata(id jsonrpc.RequestId, params RequestParams, stdio bool) (any, error) {
|
|
// Check _meta field for protocol version
|
|
if params.Meta != nil {
|
|
// check for protocol version metadata
|
|
v := params.Meta.ProtocolVersion
|
|
if v == "" {
|
|
metaErr := fmt.Errorf("_meta error: missing io.modelcontextprotocol/protocolVersion")
|
|
return jsonrpc.NewError(id, jsonrpc.INVALID_PARAMS, metaErr.Error(), nil), metaErr
|
|
}
|
|
// do not have to verify header for stdio; already verified during stdio
|
|
// message processing
|
|
if !stdio {
|
|
if PROTOCOL_VERSION != v {
|
|
metaErr := fmt.Errorf("_meta error: header mismatch: MCP-Protocol-Version header value '%s' does not match body value '%s'", PROTOCOL_VERSION, v)
|
|
return jsonrpc.NewHeaderMismatchedError(id, metaErr), metaErr
|
|
}
|
|
}
|
|
|
|
// check for clientInfo
|
|
clientInfo := params.Meta.ClientInfo
|
|
if clientInfo.Version == "" || clientInfo.Name == "" {
|
|
metaErr := fmt.Errorf("_meta error: missing field from io.modelcontextprotocol/clientInfo")
|
|
return jsonrpc.NewError(id, jsonrpc.INVALID_PARAMS, metaErr.Error(), nil), metaErr
|
|
}
|
|
// check for clientCapabilities
|
|
clientCapabilities := params.Meta.MetaClientCapabilities
|
|
if clientCapabilities == nil {
|
|
metaErr := fmt.Errorf("_meta error: missing field from io.modelcontextprotocol/clientCapabilities")
|
|
return jsonrpc.NewError(id, jsonrpc.INVALID_PARAMS, metaErr.Error(), nil), metaErr
|
|
}
|
|
// skip checking clientCapabilities since Toolbox do not utilize any of those
|
|
} else {
|
|
metaErr := fmt.Errorf("_meta error: missing required fields in request metadata")
|
|
return jsonrpc.NewError(id, jsonrpc.INVALID_PARAMS, metaErr.Error(), nil), metaErr
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
// validateHeader checks the header of every requests
|
|
// Toolbox do not check for `Mcp-Param-{Name}` header since we are not
|
|
// implementing custom headers from parameters
|
|
// Do not need to check for Base64-encoding or invalid characters since we are
|
|
// only checking `mcp-method` and `mcp-name`
|
|
func validateHeader(id jsonrpc.RequestId, header http.Header, method, name string) (any, error) {
|
|
// stdio transport will not have header
|
|
if header == nil {
|
|
return nil, nil
|
|
}
|
|
headerMethod := header.Get("mcp-method")
|
|
if headerMethod != method {
|
|
err := fmt.Errorf("Mcp-Method header value '%s' does not match body value '%s'", headerMethod, method)
|
|
return jsonrpc.NewHeaderMismatchedError(id, err), err
|
|
}
|
|
headerName := header.Get("mcp-name")
|
|
if headerName != name {
|
|
err := fmt.Errorf("Mcp-Name header value '%s' does not match body value '%s'", headerName, name)
|
|
return jsonrpc.NewHeaderMismatchedError(id, err), err
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
func serverDiscoverHandler(ctx context.Context, id jsonrpc.RequestId, body []byte, header http.Header) (any, error) {
|
|
v, err := util.ToolboxVersionFromContext(ctx)
|
|
if err != nil {
|
|
return jsonrpc.NewError(id, jsonrpc.INTERNAL_ERROR, err.Error(), nil), err
|
|
}
|
|
enableDraft, ok := util.EnableDraftSpecsFromContext(ctx)
|
|
if !ok {
|
|
err = fmt.Errorf("unable to retrieve enableDraftSpecs from context")
|
|
return jsonrpc.NewError(id, jsonrpc.INTERNAL_ERROR, err.Error(), nil), err
|
|
}
|
|
|
|
var req DiscoverRequest
|
|
if err := json.Unmarshal(body, &req); err != nil {
|
|
err = fmt.Errorf("invalid server discover request: %w", err)
|
|
return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err
|
|
}
|
|
validateHeaderErr, err := validateHeader(id, header, SERVER_DISCOVER, "")
|
|
if err != nil {
|
|
return validateHeaderErr, err
|
|
}
|
|
validateErr, err := validateMetadata(id, req.Params, header == nil)
|
|
if err != nil {
|
|
return validateErr, err
|
|
}
|
|
|
|
toolsListChanged := false
|
|
promptsListChanged := false
|
|
result := DiscoverResult{
|
|
SupportedVersions: mcputil.GetSupportedVersions(enableDraft),
|
|
Capabilities: ServerCapabilities{
|
|
Tools: &ListChanged{
|
|
ListChanged: &toolsListChanged,
|
|
},
|
|
Prompts: &ListChanged{
|
|
ListChanged: &promptsListChanged,
|
|
},
|
|
},
|
|
ServerInfo: Implementation{
|
|
BaseMetadata: BaseMetadata{
|
|
Name: SERVER_NAME,
|
|
},
|
|
Version: v,
|
|
},
|
|
}
|
|
res := jsonrpc.JSONRPCResponse{
|
|
Jsonrpc: jsonrpc.JSONRPC_VERSION,
|
|
Id: id,
|
|
Result: result,
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
func toolsListHandler(ctx context.Context, id jsonrpc.RequestId, resourceMgr *resources.ResourceManager, toolset tools.Toolset, body []byte, header http.Header) (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
|
|
}
|
|
validateHeaderErr, err := validateHeader(id, header, TOOLS_LIST, "")
|
|
if err != nil {
|
|
return validateHeaderErr, err
|
|
}
|
|
validateErr, err := validateMetadata(id, req.Params.RequestParams, header == nil)
|
|
if err != nil {
|
|
return validateErr, err
|
|
}
|
|
|
|
urlParams, _ := util.UrlParamsFromContext(ctx)
|
|
toolsMap := resourceMgr.GetToolsMap()
|
|
listToolsResult, err := GenerateListToolsResult(resourceMgr.GetSourcesMap(), toolset, toolsMap, urlParams)
|
|
if err != nil {
|
|
err = fmt.Errorf("error generating manifest: %w", err)
|
|
return jsonrpc.NewError(id, jsonrpc.INTERNAL_ERROR, err.Error(), nil), err
|
|
}
|
|
return jsonrpc.JSONRPCResponse{
|
|
Jsonrpc: jsonrpc.JSONRPC_VERSION,
|
|
Id: id,
|
|
Result: listToolsResult,
|
|
}, nil
|
|
}
|
|
|
|
// toolsCallHandler generate a response for tools call.
|
|
func toolsCallHandler(ctx context.Context, id jsonrpc.RequestId, toolset tools.Toolset, 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
|
|
}
|
|
validateHeaderErr, err := validateHeader(id, header, TOOLS_CALL, req.Params.Name)
|
|
if err != nil {
|
|
return validateHeaderErr, err
|
|
}
|
|
validateErr, err := validateMetadata(id, req.Params.RequestParams, header == nil)
|
|
if err != nil {
|
|
return validateErr, err
|
|
}
|
|
|
|
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
|
|
span := trace.SpanFromContext(ctx)
|
|
span.SetName(fmt.Sprintf("%s %s", TOOLS_CALL, toolName))
|
|
span.SetAttributes(
|
|
attribute.String("gen_ai.tool.name", toolName),
|
|
attribute.String("gen_ai.operation.name", "execute_tool"),
|
|
)
|
|
|
|
// Verify tool belongs to the current toolset before resolving globally.
|
|
if !toolset.ContainsTool(toolName) {
|
|
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
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// Populate gen_ai attributes for operation duration metric
|
|
if genAIAttrs := util.GenAIMetricAttrsFromContext(ctx); genAIAttrs != nil {
|
|
genAIAttrs.OperationName = "execute_tool"
|
|
genAIAttrs.ToolName = toolName
|
|
}
|
|
|
|
// 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
|
|
}
|
|
var accessToken tools.AccessToken
|
|
if header != nil {
|
|
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 == "" {
|
|
err := util.NewClientServerError(
|
|
"missing access token in the 'Authorization' header",
|
|
http.StatusUnauthorized,
|
|
nil,
|
|
)
|
|
return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err
|
|
}
|
|
}
|
|
|
|
// marshal arguments and decode it using decodeJSON instead to prevent loss between floats/int.
|
|
var data map[string]any
|
|
if toolArgument != nil {
|
|
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
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
if data == nil {
|
|
data = make(map[string]any)
|
|
}
|
|
|
|
// 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 {
|
|
var claims map[string]any
|
|
var err error
|
|
|
|
if mSvc, ok := aS.(auth.MCPAuthService); ok && mSvc.IsMCPEnabled() {
|
|
claims = util.AuthTokenClaimsFromContext(ctx)
|
|
} else {
|
|
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 = util.NewClientServerError(
|
|
"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
|
|
}
|
|
logger.DebugContext(ctx, "tool invocation authorized")
|
|
|
|
if err := mcputil.ValidateScopes(ctx, tool.GetScopesRequired(), authServices); err != nil {
|
|
return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err
|
|
}
|
|
|
|
toolParams, err := tool.GetParameters(resourceMgr.GetSourcesMap())
|
|
if err != nil {
|
|
err = fmt.Errorf("error getting parameters for tool: %w", err)
|
|
return jsonrpc.NewError(id, jsonrpc.INTERNAL_ERROR, err.Error(), nil), err
|
|
}
|
|
|
|
// Auto-populate arguments from URL parameters
|
|
data = mcputil.PopulateUrlParams(ctx, data, toolParams)
|
|
|
|
params, err := parameters.ParseParams(toolParams, 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))
|
|
|
|
embeddingModels := resourceMgr.GetEmbeddingModelMap()
|
|
params, err = tool.EmbedParams(ctx, params, embeddingModels)
|
|
if err != nil {
|
|
err = fmt.Errorf("error embedding parameters: %w", err)
|
|
return jsonrpc.NewError(id, jsonrpc.INVALID_PARAMS, err.Error(), nil), err
|
|
}
|
|
|
|
// Get instrumentation for recording tool execution duration
|
|
instrumentation, instrumentationErr := util.InstrumentationFromContext(ctx)
|
|
|
|
// run tool invocation and generate response.
|
|
executionStart := time.Now()
|
|
results, err := tool.Invoke(ctx, resourceMgr, params, accessToken)
|
|
executionDuration := time.Since(executionStart).Seconds()
|
|
|
|
// Record tool execution duration metric
|
|
if instrumentationErr == nil {
|
|
execAttrs := []attribute.KeyValue{
|
|
attribute.String("gen_ai.tool.name", toolName),
|
|
}
|
|
// Add network protocol attributes from context
|
|
if genAIAttrs := util.GenAIMetricAttrsFromContext(ctx); genAIAttrs != nil {
|
|
if genAIAttrs.NetworkProtocolName != "" {
|
|
execAttrs = append(execAttrs, attribute.String("network.protocol.name", genAIAttrs.NetworkProtocolName))
|
|
}
|
|
if genAIAttrs.NetworkProtocolVersion != "" {
|
|
execAttrs = append(execAttrs, attribute.String("network.protocol.version", genAIAttrs.NetworkProtocolVersion))
|
|
}
|
|
}
|
|
if err != nil {
|
|
execAttrs = append(execAttrs, attribute.String("error.type", err.Error()))
|
|
}
|
|
instrumentation.ToolExecutionDuration.Record(ctx, executionDuration, metric.WithAttributes(execAttrs...))
|
|
}
|
|
|
|
if err != nil {
|
|
var tbErr util.ToolboxError
|
|
|
|
if errors.As(err, &tbErr) {
|
|
switch tbErr.Category() {
|
|
case util.CategoryAgent:
|
|
// MCP - Tool execution error
|
|
// Return SUCCESS but with IsError: true
|
|
text := TextContent{
|
|
Type: "text",
|
|
Text: err.Error(),
|
|
}
|
|
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 {
|
|
// Unknown error -> 500
|
|
return jsonrpc.NewError(id, jsonrpc.INTERNAL_ERROR, err.Error(), nil), err
|
|
}
|
|
}
|
|
|
|
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, resourceMgr *resources.ResourceManager, promptset prompts.Promptset, body []byte, header http.Header) (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
|
|
}
|
|
validateHeaderErr, err := validateHeader(id, header, PROMPTS_LIST, "")
|
|
if err != nil {
|
|
return validateHeaderErr, err
|
|
}
|
|
validateErr, err := validateMetadata(id, req.Params.RequestParams, header == nil)
|
|
if err != nil {
|
|
return validateErr, err
|
|
}
|
|
|
|
promptsMap := resourceMgr.GetPromptsMap()
|
|
listPromptsResult, err := GenerateListPromptsResult(promptset, promptsMap)
|
|
if err != nil {
|
|
err = fmt.Errorf("error generating manifest: %w", err)
|
|
return jsonrpc.NewError(id, jsonrpc.INTERNAL_ERROR, err.Error(), nil), err
|
|
}
|
|
logger.DebugContext(ctx, fmt.Sprintf("returning %d prompts", len(listPromptsResult.Prompts)))
|
|
return jsonrpc.JSONRPCResponse{
|
|
Jsonrpc: jsonrpc.JSONRPC_VERSION,
|
|
Id: id,
|
|
Result: listPromptsResult,
|
|
}, nil
|
|
}
|
|
|
|
// promptsGetHandler handles the "prompts/get" method.
|
|
func promptsGetHandler(ctx context.Context, id jsonrpc.RequestId, promptset prompts.Promptset, resourceMgr *resources.ResourceManager, body []byte, header http.Header) (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
|
|
}
|
|
validateHeaderErr, err := validateHeader(id, header, PROMPTS_GET, req.Params.Name)
|
|
if err != nil {
|
|
return validateHeaderErr, err
|
|
}
|
|
validateErr, err := validateMetadata(id, req.Params.RequestParams, header == nil)
|
|
if err != nil {
|
|
return validateErr, err
|
|
}
|
|
|
|
promptName := req.Params.Name
|
|
logger.DebugContext(ctx, fmt.Sprintf("prompt name: %s", promptName))
|
|
|
|
// Update span name and set gen_ai attributes
|
|
span := trace.SpanFromContext(ctx)
|
|
span.SetName(fmt.Sprintf("%s %s", PROMPTS_GET, promptName))
|
|
span.SetAttributes(attribute.String("gen_ai.prompt.name", promptName))
|
|
|
|
// Verify prompt belongs to the current promptset before resolving globally.
|
|
if !promptset.ContainsPrompt(promptName) {
|
|
err := fmt.Errorf("prompt with name %q does not exist", promptName)
|
|
return jsonrpc.NewError(id, jsonrpc.INVALID_PARAMS, err.Error(), nil), err
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// Populate gen_ai attributes for operation duration metric
|
|
if genAIAttrs := util.GenAIMetricAttrsFromContext(ctx); genAIAttrs != nil {
|
|
genAIAttrs.OperationName = "get_prompt"
|
|
genAIAttrs.PromptName = promptName
|
|
}
|
|
|
|
// 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
|
|
}
|