code refactor

This commit is contained in:
MaysWind
2025-07-06 15:50:53 +08:00
parent a54275d307
commit 82b98eca95
9 changed files with 239 additions and 191 deletions
-12
View File
@@ -1,12 +0,0 @@
package mcp
var mcpTextContentTools = map[string]MCPToolHandler[MCPTextContent]{
"query_latest_exchange_rates": MCPQueryLatestExchangeRatesRequestToolHandler,
}
var mcpImageContentTools = map[string]MCPToolHandler[MCPImageContent]{}
var mcpAudioContentTools = map[string]MCPToolHandler[MCPAudioContent]{}
var mcpResourceLinkTools = map[string]MCPToolHandler[MCPResourceLink]{}
var mcpEmbeddedResourceTools = map[string]MCPToolHandler[MCPEmbeddedResource]{}
var AllMCPToolInfos = GetAllMCPToolInfos()
+4 -154
View File
@@ -3,7 +3,6 @@ package mcp
import (
"reflect"
"github.com/invopop/jsonschema"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/services"
@@ -21,6 +20,9 @@ type MCPAvailableServices interface {
// MCPToolHandler defines the MCP tool handler
type MCPToolHandler[T MCPTextContent | MCPImageContent | MCPAudioContent | MCPResourceLink | MCPEmbeddedResource] interface {
// Name returns the name of the MCP tool
Name() string
// Description returns the description of the MCP tool
Description() string
@@ -31,157 +33,5 @@ type MCPToolHandler[T MCPTextContent | MCPImageContent | MCPAudioContent | MCPRe
OutputType() reflect.Type
// Handle processes the MCP call tool request and returns the response
Handle(*core.WebContext, *MCPCallToolRequest, *settings.Config, MCPAvailableServices) ([]*T, *errs.Error)
}
// GetAllMCPToolInfos returns all available MCP tool information
func GetAllMCPToolInfos() []*MCPTool {
toolInfos := make([]*MCPTool, 0)
for name, handler := range mcpTextContentTools {
toolInfos = append(toolInfos, getMCPToolInfo(name, handler))
}
for name, handler := range mcpImageContentTools {
toolInfos = append(toolInfos, getMCPToolInfo(name, handler))
}
for name, handler := range mcpAudioContentTools {
toolInfos = append(toolInfos, getMCPToolInfo(name, handler))
}
for name, handler := range mcpResourceLinkTools {
toolInfos = append(toolInfos, getMCPToolInfo(name, handler))
}
for name, handler := range mcpEmbeddedResourceTools {
toolInfos = append(toolInfos, getMCPToolInfo(name, handler))
}
return toolInfos
}
// MCPToolHandle handles the MCP tool request based on the tool name
func MCPToolHandle(c *core.WebContext, callToolReq *MCPCallToolRequest, currentConfig *settings.Config, services MCPAvailableServices) (any, *errs.Error) {
if handler, exists := mcpTextContentTools[callToolReq.Name]; exists {
return mcpTextContentToolHandle(c, handler, currentConfig, services, callToolReq)
}
if handler, exists := mcpImageContentTools[callToolReq.Name]; exists {
return mcpImageContentToolHandle(c, handler, currentConfig, services, callToolReq)
}
if handler, exists := mcpAudioContentTools[callToolReq.Name]; exists {
return mcpAudioContentToolHandle(c, handler, currentConfig, services, callToolReq)
}
if handler, exists := mcpResourceLinkTools[callToolReq.Name]; exists {
return mcpResourceLinkToolHandle(c, handler, currentConfig, services, callToolReq)
}
if handler, exists := mcpEmbeddedResourceTools[callToolReq.Name]; exists {
return mcpEmbeddedResourceToolHandle(c, handler, currentConfig, services, callToolReq)
}
return nil, errs.ErrApiNotFound
}
func mcpTextContentToolHandle(c *core.WebContext, handler MCPToolHandler[MCPTextContent], currentConfig *settings.Config, services MCPAvailableServices, callToolReq *MCPCallToolRequest) (any, *errs.Error) {
result, err := handler.Handle(c, callToolReq, currentConfig, services)
if err != nil {
return nil, errs.Or(err, errs.ErrOperationFailed)
}
callToolResp := MCPCallToolResponse[MCPTextContent]{
Content: result,
IsError: false,
}
return callToolResp, nil
}
func mcpImageContentToolHandle(c *core.WebContext, handler MCPToolHandler[MCPImageContent], currentConfig *settings.Config, services MCPAvailableServices, callToolReq *MCPCallToolRequest) (any, *errs.Error) {
result, err := handler.Handle(c, callToolReq, currentConfig, services)
if err != nil {
return nil, errs.Or(err, errs.ErrOperationFailed)
}
callToolResp := MCPCallToolResponse[MCPImageContent]{
Content: result,
IsError: false,
}
return callToolResp, nil
}
func mcpAudioContentToolHandle(c *core.WebContext, handler MCPToolHandler[MCPAudioContent], currentConfig *settings.Config, services MCPAvailableServices, callToolReq *MCPCallToolRequest) (any, *errs.Error) {
result, err := handler.Handle(c, callToolReq, currentConfig, services)
if err != nil {
return nil, errs.Or(err, errs.ErrOperationFailed)
}
callToolResp := MCPCallToolResponse[MCPAudioContent]{
Content: result,
IsError: false,
}
return callToolResp, nil
}
func mcpResourceLinkToolHandle(c *core.WebContext, handler MCPToolHandler[MCPResourceLink], currentConfig *settings.Config, services MCPAvailableServices, callToolReq *MCPCallToolRequest) (any, *errs.Error) {
result, err := handler.Handle(c, callToolReq, currentConfig, services)
if err != nil {
return nil, errs.Or(err, errs.ErrOperationFailed)
}
callToolResp := MCPCallToolResponse[MCPResourceLink]{
Content: result,
IsError: false,
}
return callToolResp, nil
}
func mcpEmbeddedResourceToolHandle(c *core.WebContext, handler MCPToolHandler[MCPEmbeddedResource], currentConfig *settings.Config, services MCPAvailableServices, callToolReq *MCPCallToolRequest) (any, *errs.Error) {
result, err := handler.Handle(c, callToolReq, currentConfig, services)
if err != nil {
return nil, errs.Or(err, errs.ErrOperationFailed)
}
callToolResp := MCPCallToolResponse[MCPEmbeddedResource]{
Content: result,
IsError: false,
}
return callToolResp, nil
}
func getMCPToolInfo[T MCPTextContent | MCPImageContent | MCPAudioContent | MCPResourceLink | MCPEmbeddedResource](name string, handler MCPToolHandler[T]) *MCPTool {
mcpTool := &MCPTool{
Name: name,
Description: handler.Description(),
}
schemeGenerator := jsonschema.Reflector{
Anonymous: true,
DoNotReference: true,
ExpandedStruct: true,
}
if handler.InputType() != nil {
schema := schemeGenerator.ReflectFromType(handler.InputType())
mcpTool.InputSchema = schema
}
if handler.OutputType() != nil {
schema := schemeGenerator.ReflectFromType(handler.OutputType())
mcpTool.OutputSchema = schema
}
return mcpTool
Handle(*core.WebContext, *MCPCallToolRequest, *settings.Config, MCPAvailableServices) (any, []*T, *errs.Error)
}
+159
View File
@@ -0,0 +1,159 @@
package mcp
import (
"github.com/invopop/jsonschema"
orderedmap "github.com/wk8/go-ordered-map/v2"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/settings"
)
// MCPContainer contains the all mcp handlers
type MCPContainer struct {
mcpTextContentTools *orderedmap.OrderedMap[string, MCPToolHandler[MCPTextContent]]
mcpImageContentTools *orderedmap.OrderedMap[string, MCPToolHandler[MCPImageContent]]
mcpAudioContentTools *orderedmap.OrderedMap[string, MCPToolHandler[MCPAudioContent]]
mcpResourceLinkTools *orderedmap.OrderedMap[string, MCPToolHandler[MCPResourceLink]]
mcpEmbeddedResourceTools *orderedmap.OrderedMap[string, MCPToolHandler[MCPEmbeddedResource]]
mcpTools []*MCPTool
}
// Initialize a mcp handler container singleton instance
var (
Container = &MCPContainer{}
)
// GetMCPTools returns the registered MCP tools
func (c *MCPContainer) GetMCPTools() []*MCPTool {
if len(c.mcpTools) == 0 {
return nil
}
return c.mcpTools
}
// HandleTool returns the result of the MCP tool handler based on the tool name
func (c *MCPContainer) HandleTool(ctx *core.WebContext, callToolReq *MCPCallToolRequest, currentConfig *settings.Config, services MCPAvailableServices) (any, *errs.Error) {
if handler, exists := c.mcpTextContentTools.Get(callToolReq.Name); exists {
return handleTool(ctx, handler, currentConfig, services, callToolReq)
}
if handler, exists := c.mcpImageContentTools.Get(callToolReq.Name); exists {
return handleTool(ctx, handler, currentConfig, services, callToolReq)
}
if handler, exists := c.mcpAudioContentTools.Get(callToolReq.Name); exists {
return handleTool(ctx, handler, currentConfig, services, callToolReq)
}
if handler, exists := c.mcpResourceLinkTools.Get(callToolReq.Name); exists {
return handleTool(ctx, handler, currentConfig, services, callToolReq)
}
if handler, exists := c.mcpEmbeddedResourceTools.Get(callToolReq.Name); exists {
return handleTool(ctx, handler, currentConfig, services, callToolReq)
}
return nil, errs.ErrApiNotFound
}
// InitializeMCPHandlers initializes the all mcp handlers according to the config
func InitializeMCPHandlers(config *settings.Config) error {
container := &MCPContainer{
mcpTextContentTools: orderedmap.New[string, MCPToolHandler[MCPTextContent]](),
mcpImageContentTools: orderedmap.New[string, MCPToolHandler[MCPImageContent]](),
mcpAudioContentTools: orderedmap.New[string, MCPToolHandler[MCPAudioContent]](),
mcpResourceLinkTools: orderedmap.New[string, MCPToolHandler[MCPResourceLink]](),
mcpEmbeddedResourceTools: orderedmap.New[string, MCPToolHandler[MCPEmbeddedResource]](),
mcpTools: make([]*MCPTool, 0),
}
registerMCPTextContentToolHandler(container, MCPQueryLatestExchangeRatesToolHandler)
Container = container
return nil
}
func registerMCPTextContentToolHandler(c *MCPContainer, handler MCPToolHandler[MCPTextContent]) {
registerMCPToolHandler(c, c.mcpTextContentTools, handler)
}
func registerMCPImageContentToolHandler(c *MCPContainer, handler MCPToolHandler[MCPImageContent]) {
registerMCPToolHandler(c, c.mcpImageContentTools, handler)
}
func registerMCPAudioContentToolHandler(c *MCPContainer, handler MCPToolHandler[MCPAudioContent]) {
registerMCPToolHandler(c, c.mcpAudioContentTools, handler)
}
func registerMCPResourceLinkToolHandler(c *MCPContainer, handler MCPToolHandler[MCPResourceLink]) {
registerMCPToolHandler(c, c.mcpResourceLinkTools, handler)
}
func registerMCPEmbeddedResourceToolHandler(c *MCPContainer, handler MCPToolHandler[MCPEmbeddedResource]) {
registerMCPToolHandler(c, c.mcpEmbeddedResourceTools, handler)
}
func registerMCPToolHandler[T MCPTextContent | MCPImageContent | MCPAudioContent | MCPResourceLink | MCPEmbeddedResource](c *MCPContainer, mcpToolHandlerMap *orderedmap.OrderedMap[string, MCPToolHandler[T]], handler MCPToolHandler[T]) {
if _, exists := mcpToolHandlerMap.Get(handler.Name()); exists {
return
}
mcpToolHandlerMap.Set(handler.Name(), handler)
c.mcpTools = append(c.mcpTools, createNewMCPToolInfo(handler.Name(), handler))
}
func handleTool[T MCPTextContent | MCPImageContent | MCPAudioContent | MCPResourceLink | MCPEmbeddedResource](ctx *core.WebContext, handler MCPToolHandler[T], currentConfig *settings.Config, services MCPAvailableServices, callToolReq *MCPCallToolRequest) (any, *errs.Error) {
structuredResponse, result, err := handler.Handle(ctx, callToolReq, currentConfig, services)
if err != nil {
return nil, errs.Or(err, errs.ErrOperationFailed)
}
callToolResp := MCPCallToolResponse[T]{
Content: result,
IsError: false,
}
if ctx.GetHeader(MCPProtocolVersionHeaderName) > string(ToolResultStructuredContentMinVersion) {
callToolResp.StructuredContent = structuredResponse
}
return callToolResp, nil
}
func createNewMCPToolInfo[T MCPTextContent | MCPImageContent | MCPAudioContent | MCPResourceLink | MCPEmbeddedResource](name string, handler MCPToolHandler[T]) *MCPTool {
mcpTool := &MCPTool{
Name: name,
Description: handler.Description(),
}
schemeGenerator := jsonschema.Reflector{
Anonymous: true,
DoNotReference: true,
ExpandedStruct: true,
}
if handler.InputType() != nil {
schema := schemeGenerator.ReflectFromType(handler.InputType())
schema.Version = ""
mcpTool.InputSchema = schema
} else {
mcpTool.InputSchema = &jsonschema.Schema{
Type: "object",
}
}
if handler.OutputType() != nil {
schema := schemeGenerator.ReflectFromType(handler.OutputType())
schema.Version = ""
mcpTool.OutputSchema = schema
} else {
mcpTool.OutputSchema = &jsonschema.Schema{
Type: "object",
}
}
return mcpTool
}
+10 -3
View File
@@ -20,6 +20,12 @@ const (
// LatestSupportedMCPVersion defines the latest supported version of Model Context Protocol (MCP)
const LatestSupportedMCPVersion = MCPProtocolVersion20250618
// ToolResultStructuredContentMinVersion defines the minimum version of structured content supported in tool results
const ToolResultStructuredContentMinVersion = MCPProtocolVersion20250618
// MCPProtocolVersionHeaderName defines the HTTP header name for the MCP protocol version
const MCPProtocolVersionHeaderName = "MCP-Protocol-Version"
// SupportedMCPVersion defines a map of supported MCP versions
var SupportedMCPVersion = map[MCPProtocolVersion]bool{
MCPProtocolVersion20250618: true,
@@ -120,7 +126,7 @@ type MCPListToolsResponse struct {
type MCPTool struct {
Name string `json:"name"`
InputSchema *jsonschema.Schema `json:"inputSchema"`
OutputSchema *jsonschema.Schema `json:"outputSchema"`
OutputSchema *jsonschema.Schema `json:"outputSchema,omitempty"`
Title string `json:"title,omitempty"`
Description string `json:"description,omitempty"`
}
@@ -133,8 +139,9 @@ type MCPCallToolRequest struct {
// MCPCallToolResponse defines the response structure for calling a tool in the MCP
type MCPCallToolResponse[T MCPTextContent | MCPImageContent | MCPAudioContent | MCPResourceLink | MCPEmbeddedResource] struct {
Content []*T `json:"content"`
IsError bool `json:"isError,omitempty"`
Content []*T `json:"content"`
StructuredContent any `json:"structuredContent,omitempty"`
IsError bool `json:"isError,omitempty"`
}
// MCPTextContent defines the text content structure used in MCP
+13 -13
View File
@@ -29,7 +29,7 @@ type MCPQueryExchangeRatesResponse struct {
// MCPQueryExchangeRateInfo defines the structure of exchange rate information for a specific currency
type MCPQueryExchangeRateInfo struct {
Currency string `json:"currency" jsonschema_description:"Currency code (e.g. USD)"`
Rate string `json:"rate" jsonschema_description:"The amount of the base currency that can be exchanged for 1 of this currency"`
Rate string `json:"rate_to_base" jsonschema_description:"The amount of the base currency that can be obtained for 1 unit of this currency"`
}
type mcpQueryLatestExchangeRatesToolHandler struct{}
@@ -57,39 +57,39 @@ func (h *mcpQueryLatestExchangeRatesToolHandler) OutputType() reflect.Type {
}
// Handle processes the MCP call tool request and returns the response
func (h *mcpQueryLatestExchangeRatesToolHandler) Handle(c *core.WebContext, callToolReq *MCPCallToolRequest, currentConfig *settings.Config, services MCPAvailableServices) ([]*MCPTextContent, *errs.Error) {
func (h *mcpQueryLatestExchangeRatesToolHandler) Handle(c *core.WebContext, callToolReq *MCPCallToolRequest, currentConfig *settings.Config, services MCPAvailableServices) (any, []*MCPTextContent, *errs.Error) {
var exchangeRatesRequest MCPQueryExchangeRatesRequest
if callToolReq.Arguments != nil {
if err := json.Unmarshal(callToolReq.Arguments, &exchangeRatesRequest); err != nil {
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
return nil, nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
} else {
return nil, errs.ErrIncompleteOrIncorrectSubmission
return nil, nil, errs.ErrIncompleteOrIncorrectSubmission
}
dataSource := exchangerates.Container.Current
if dataSource == nil {
return nil, errs.ErrInvalidExchangeRatesDataSource
return nil, nil, errs.ErrInvalidExchangeRatesDataSource
}
exchangeRateResponse, err := dataSource.GetLatestExchangeRates(c, c.GetCurrentUid(), currentConfig)
if err != nil {
return nil, errs.Or(err, errs.ErrOperationFailed)
return nil, nil, errs.Or(err, errs.ErrOperationFailed)
}
response, err := h.createNewMCPQueryExchangeRatesResponse(exchangeRatesRequest.Currencies, exchangeRateResponse)
structuredResponse, response, err := h.createNewMCPQueryExchangeRatesResponse(exchangeRatesRequest.Currencies, exchangeRateResponse)
if err != nil {
return nil, errs.Or(err, errs.ErrOperationFailed)
return nil, nil, errs.Or(err, errs.ErrOperationFailed)
}
return response, nil
return structuredResponse, response, nil
}
func (h *mcpQueryLatestExchangeRatesToolHandler) createNewMCPQueryExchangeRatesResponse(currencies string, exchangeRatesResp *models.LatestExchangeRateResponse) ([]*MCPTextContent, error) {
func (h *mcpQueryLatestExchangeRatesToolHandler) createNewMCPQueryExchangeRatesResponse(currencies string, exchangeRatesResp *models.LatestExchangeRateResponse) (any, []*MCPTextContent, error) {
queryCurrencies := make(map[string]bool)
for _, currency := range strings.Split(currencies, ",") {
@@ -102,7 +102,7 @@ func (h *mcpQueryLatestExchangeRatesToolHandler) createNewMCPQueryExchangeRatesR
response := &MCPQueryExchangeRatesResponse{
BaseCurrency: exchangeRatesResp.BaseCurrency,
UpdateTime: utils.FormatUnixTimeToLongDateTimeWithTimezoneRFC3389Format(exchangeRatesResp.UpdateTime, time.UTC),
UpdateTime: utils.FormatUnixTimeToLongDateTimeWithTimezoneRFC3339Format(exchangeRatesResp.UpdateTime, time.UTC),
Rates: make([]*MCPQueryExchangeRateInfo, 0, len(exchangeRatesResp.ExchangeRates)),
}
@@ -120,10 +120,10 @@ func (h *mcpQueryLatestExchangeRatesToolHandler) createNewMCPQueryExchangeRatesR
content, err := json.Marshal(response)
if err != nil {
return nil, err
return nil, nil, err
}
return []*MCPTextContent{
return response, []*MCPTextContent{
NewMCPTextContent(string(content)),
}, nil
}