code refactor
This commit is contained in:
@@ -19,6 +19,7 @@ import (
|
|||||||
"github.com/mayswind/ezbookkeeping/pkg/cron"
|
"github.com/mayswind/ezbookkeeping/pkg/cron"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/log"
|
"github.com/mayswind/ezbookkeeping/pkg/log"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/mcp"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/middlewares"
|
"github.com/mayswind/ezbookkeeping/pkg/middlewares"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/requestid"
|
"github.com/mayswind/ezbookkeeping/pkg/requestid"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/settings"
|
"github.com/mayswind/ezbookkeeping/pkg/settings"
|
||||||
@@ -64,6 +65,13 @@ func startWebServer(c *core.CliContext) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = mcp.InitializeMCPHandlers(config)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.BootErrorf(c, "[webserver.startWebServer] initializes mcp handlers failed, because %s", err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
err = cron.InitializeCronJobSchedulerContainer(c, config, true)
|
err = cron.InitializeCronJobSchedulerContainer(c, config, true)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -102,8 +102,25 @@ func (a *ModelContextProtocolAPI) ReadResourceHandler(c *core.WebContext, jsonRP
|
|||||||
|
|
||||||
// ListToolsHandler returns the list of tools for model context protocol
|
// ListToolsHandler returns the list of tools for model context protocol
|
||||||
func (a *ModelContextProtocolAPI) ListToolsHandler(c *core.WebContext, jsonRPCRequest *core.JSONRPCRequest) (any, *errs.Error) {
|
func (a *ModelContextProtocolAPI) ListToolsHandler(c *core.WebContext, jsonRPCRequest *core.JSONRPCRequest) (any, *errs.Error) {
|
||||||
|
mcpVersion := a.getMCPVersion(c)
|
||||||
|
toolsInfo := mcp.Container.GetMCPTools()
|
||||||
|
finalToolsInfos := make([]*mcp.MCPTool, len(toolsInfo))
|
||||||
|
|
||||||
|
for i := 0; i < len(toolsInfo); i++ {
|
||||||
|
finalToolsInfos[i] = &mcp.MCPTool{
|
||||||
|
Name: toolsInfo[i].Name,
|
||||||
|
InputSchema: toolsInfo[i].InputSchema,
|
||||||
|
Title: toolsInfo[i].Title,
|
||||||
|
Description: toolsInfo[i].Description,
|
||||||
|
}
|
||||||
|
|
||||||
|
if mcpVersion >= string(mcp.ToolResultStructuredContentMinVersion) {
|
||||||
|
finalToolsInfos[i].OutputSchema = toolsInfo[i].OutputSchema
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
listToolsResp := mcp.MCPListToolsResponse{
|
listToolsResp := mcp.MCPListToolsResponse{
|
||||||
Tools: mcp.AllMCPToolInfos,
|
Tools: finalToolsInfos,
|
||||||
}
|
}
|
||||||
|
|
||||||
return listToolsResp, nil
|
return listToolsResp, nil
|
||||||
@@ -121,7 +138,7 @@ func (a *ModelContextProtocolAPI) CallToolHandler(c *core.WebContext, jsonRPCReq
|
|||||||
return nil, errs.ErrIncompleteOrIncorrectSubmission
|
return nil, errs.ErrIncompleteOrIncorrectSubmission
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := mcp.MCPToolHandle(c, &callToolReq, a.CurrentConfig(), a)
|
result, err := mcp.Container.HandleTool(c, &callToolReq, a.CurrentConfig(), a)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -159,3 +176,8 @@ func (a *ModelContextProtocolAPI) GetAccountService() *services.AccountService {
|
|||||||
func (a *ModelContextProtocolAPI) GetUserService() *services.UserService {
|
func (a *ModelContextProtocolAPI) GetUserService() *services.UserService {
|
||||||
return a.users
|
return a.users
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getMCPVersion returns the MCP protocol version from the request header
|
||||||
|
func (a *ModelContextProtocolAPI) getMCPVersion(c *core.WebContext) string {
|
||||||
|
return c.GetHeader(mcp.MCPProtocolVersionHeaderName)
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
@@ -3,7 +3,6 @@ package mcp
|
|||||||
import (
|
import (
|
||||||
"reflect"
|
"reflect"
|
||||||
|
|
||||||
"github.com/invopop/jsonschema"
|
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/core"
|
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/services"
|
"github.com/mayswind/ezbookkeeping/pkg/services"
|
||||||
@@ -21,6 +20,9 @@ type MCPAvailableServices interface {
|
|||||||
|
|
||||||
// MCPToolHandler defines the MCP tool handler
|
// MCPToolHandler defines the MCP tool handler
|
||||||
type MCPToolHandler[T MCPTextContent | MCPImageContent | MCPAudioContent | MCPResourceLink | MCPEmbeddedResource] interface {
|
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 returns the description of the MCP tool
|
||||||
Description() string
|
Description() string
|
||||||
|
|
||||||
@@ -31,157 +33,5 @@ type MCPToolHandler[T MCPTextContent | MCPImageContent | MCPAudioContent | MCPRe
|
|||||||
OutputType() reflect.Type
|
OutputType() reflect.Type
|
||||||
|
|
||||||
// Handle processes the MCP call tool request and returns the response
|
// Handle processes the MCP call tool request and returns the response
|
||||||
Handle(*core.WebContext, *MCPCallToolRequest, *settings.Config, MCPAvailableServices) ([]*T, *errs.Error)
|
Handle(*core.WebContext, *MCPCallToolRequest, *settings.Config, MCPAvailableServices) (any, []*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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -20,6 +20,12 @@ const (
|
|||||||
// LatestSupportedMCPVersion defines the latest supported version of Model Context Protocol (MCP)
|
// LatestSupportedMCPVersion defines the latest supported version of Model Context Protocol (MCP)
|
||||||
const LatestSupportedMCPVersion = MCPProtocolVersion20250618
|
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
|
// SupportedMCPVersion defines a map of supported MCP versions
|
||||||
var SupportedMCPVersion = map[MCPProtocolVersion]bool{
|
var SupportedMCPVersion = map[MCPProtocolVersion]bool{
|
||||||
MCPProtocolVersion20250618: true,
|
MCPProtocolVersion20250618: true,
|
||||||
@@ -120,7 +126,7 @@ type MCPListToolsResponse struct {
|
|||||||
type MCPTool struct {
|
type MCPTool struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
InputSchema *jsonschema.Schema `json:"inputSchema"`
|
InputSchema *jsonschema.Schema `json:"inputSchema"`
|
||||||
OutputSchema *jsonschema.Schema `json:"outputSchema"`
|
OutputSchema *jsonschema.Schema `json:"outputSchema,omitempty"`
|
||||||
Title string `json:"title,omitempty"`
|
Title string `json:"title,omitempty"`
|
||||||
Description string `json:"description,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
|
// MCPCallToolResponse defines the response structure for calling a tool in the MCP
|
||||||
type MCPCallToolResponse[T MCPTextContent | MCPImageContent | MCPAudioContent | MCPResourceLink | MCPEmbeddedResource] struct {
|
type MCPCallToolResponse[T MCPTextContent | MCPImageContent | MCPAudioContent | MCPResourceLink | MCPEmbeddedResource] struct {
|
||||||
Content []*T `json:"content"`
|
Content []*T `json:"content"`
|
||||||
IsError bool `json:"isError,omitempty"`
|
StructuredContent any `json:"structuredContent,omitempty"`
|
||||||
|
IsError bool `json:"isError,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// MCPTextContent defines the text content structure used in MCP
|
// MCPTextContent defines the text content structure used in MCP
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ type MCPQueryExchangeRatesResponse struct {
|
|||||||
// MCPQueryExchangeRateInfo defines the structure of exchange rate information for a specific currency
|
// MCPQueryExchangeRateInfo defines the structure of exchange rate information for a specific currency
|
||||||
type MCPQueryExchangeRateInfo struct {
|
type MCPQueryExchangeRateInfo struct {
|
||||||
Currency string `json:"currency" jsonschema_description:"Currency code (e.g. USD)"`
|
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{}
|
type mcpQueryLatestExchangeRatesToolHandler struct{}
|
||||||
@@ -57,39 +57,39 @@ func (h *mcpQueryLatestExchangeRatesToolHandler) OutputType() reflect.Type {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handle processes the MCP call tool request and returns the response
|
// 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
|
var exchangeRatesRequest MCPQueryExchangeRatesRequest
|
||||||
|
|
||||||
if callToolReq.Arguments != nil {
|
if callToolReq.Arguments != nil {
|
||||||
if err := json.Unmarshal(callToolReq.Arguments, &exchangeRatesRequest); err != nil {
|
if err := json.Unmarshal(callToolReq.Arguments, &exchangeRatesRequest); err != nil {
|
||||||
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
return nil, nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return nil, errs.ErrIncompleteOrIncorrectSubmission
|
return nil, nil, errs.ErrIncompleteOrIncorrectSubmission
|
||||||
}
|
}
|
||||||
|
|
||||||
dataSource := exchangerates.Container.Current
|
dataSource := exchangerates.Container.Current
|
||||||
|
|
||||||
if dataSource == nil {
|
if dataSource == nil {
|
||||||
return nil, errs.ErrInvalidExchangeRatesDataSource
|
return nil, nil, errs.ErrInvalidExchangeRatesDataSource
|
||||||
}
|
}
|
||||||
|
|
||||||
exchangeRateResponse, err := dataSource.GetLatestExchangeRates(c, c.GetCurrentUid(), currentConfig)
|
exchangeRateResponse, err := dataSource.GetLatestExchangeRates(c, c.GetCurrentUid(), currentConfig)
|
||||||
|
|
||||||
if err != nil {
|
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 {
|
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)
|
queryCurrencies := make(map[string]bool)
|
||||||
|
|
||||||
for _, currency := range strings.Split(currencies, ",") {
|
for _, currency := range strings.Split(currencies, ",") {
|
||||||
@@ -102,7 +102,7 @@ func (h *mcpQueryLatestExchangeRatesToolHandler) createNewMCPQueryExchangeRatesR
|
|||||||
|
|
||||||
response := &MCPQueryExchangeRatesResponse{
|
response := &MCPQueryExchangeRatesResponse{
|
||||||
BaseCurrency: exchangeRatesResp.BaseCurrency,
|
BaseCurrency: exchangeRatesResp.BaseCurrency,
|
||||||
UpdateTime: utils.FormatUnixTimeToLongDateTimeWithTimezoneRFC3389Format(exchangeRatesResp.UpdateTime, time.UTC),
|
UpdateTime: utils.FormatUnixTimeToLongDateTimeWithTimezoneRFC3339Format(exchangeRatesResp.UpdateTime, time.UTC),
|
||||||
Rates: make([]*MCPQueryExchangeRateInfo, 0, len(exchangeRatesResp.ExchangeRates)),
|
Rates: make([]*MCPQueryExchangeRateInfo, 0, len(exchangeRatesResp.ExchangeRates)),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,10 +120,10 @@ func (h *mcpQueryLatestExchangeRatesToolHandler) createNewMCPQueryExchangeRatesR
|
|||||||
content, err := json.Marshal(response)
|
content, err := json.Marshal(response)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return []*MCPTextContent{
|
return response, []*MCPTextContent{
|
||||||
NewMCPTextContent(string(content)),
|
NewMCPTextContent(string(content)),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ const (
|
|||||||
longDateTimeFormat = "2006-01-02 15:04:05"
|
longDateTimeFormat = "2006-01-02 15:04:05"
|
||||||
longDateTimeWithTimezoneFormat = "2006-01-02 15:04:05Z07:00"
|
longDateTimeWithTimezoneFormat = "2006-01-02 15:04:05Z07:00"
|
||||||
longDateTimeWithTimezoneFormat2 = "2006-01-02 15:04:05 Z0700"
|
longDateTimeWithTimezoneFormat2 = "2006-01-02 15:04:05 Z0700"
|
||||||
longDateTimeWithTimezoneRFC3389Format = "2006-01-02T15:04:05Z07:00"
|
longDateTimeWithTimezoneRFC3339Format = "2006-01-02T15:04:05Z07:00"
|
||||||
longDateTimeWithoutSecondFormat = "2006-01-02 15:04"
|
longDateTimeWithoutSecondFormat = "2006-01-02 15:04"
|
||||||
shortDateTimeFormat = "2006-1-2 15:4:5"
|
shortDateTimeFormat = "2006-1-2 15:4:5"
|
||||||
yearMonthDateTimeFormat = "2006-01"
|
yearMonthDateTimeFormat = "2006-01"
|
||||||
@@ -77,15 +77,15 @@ func FormatUnixTimeToLongDateTimeWithTimezone(unixTime int64, timezone *time.Loc
|
|||||||
return t.Format(longDateTimeWithTimezoneFormat)
|
return t.Format(longDateTimeWithTimezoneFormat)
|
||||||
}
|
}
|
||||||
|
|
||||||
// FormatUnixTimeToLongDateTimeWithTimezoneRFC3389Format returns a textual representation of the unix time formatted by long date time with timezone RFC 3389 format
|
// FormatUnixTimeToLongDateTimeWithTimezoneRFC3339Format returns a textual representation of the unix time formatted by long date time with timezone RFC 3339 format
|
||||||
func FormatUnixTimeToLongDateTimeWithTimezoneRFC3389Format(unixTime int64, timezone *time.Location) string {
|
func FormatUnixTimeToLongDateTimeWithTimezoneRFC3339Format(unixTime int64, timezone *time.Location) string {
|
||||||
t := parseFromUnixTime(unixTime)
|
t := parseFromUnixTime(unixTime)
|
||||||
|
|
||||||
if timezone != nil {
|
if timezone != nil {
|
||||||
t = t.In(timezone)
|
t = t.In(timezone)
|
||||||
}
|
}
|
||||||
|
|
||||||
return t.Format(longDateTimeWithTimezoneRFC3389Format)
|
return t.Format(longDateTimeWithTimezoneRFC3339Format)
|
||||||
}
|
}
|
||||||
|
|
||||||
func FormatYearMonthDayToLongDateTime(year string, month string, day string) (string, error) {
|
func FormatYearMonthDayToLongDateTime(year string, month string, day string) (string, error) {
|
||||||
@@ -229,6 +229,11 @@ func ParseFromLongDateTimeWithTimezone2(t string) (time.Time, error) {
|
|||||||
return time.Parse(longDateTimeWithTimezoneFormat2, t)
|
return time.Parse(longDateTimeWithTimezoneFormat2, t)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ParseFromLongDateTimeWithTimezoneRFC3339Format parses a formatted string in long date time RFC 3378 format
|
||||||
|
func ParseFromLongDateTimeWithTimezoneRFC3339Format(t string) (time.Time, error) {
|
||||||
|
return time.Parse(longDateTimeWithTimezoneRFC3339Format, t)
|
||||||
|
}
|
||||||
|
|
||||||
// ParseFromLongDateTimeWithoutSecond parses a formatted string in long date time format (no second)
|
// ParseFromLongDateTimeWithoutSecond parses a formatted string in long date time format (no second)
|
||||||
func ParseFromLongDateTimeWithoutSecond(t string, utcOffset int16) (time.Time, error) {
|
func ParseFromLongDateTimeWithoutSecond(t string, utcOffset int16) (time.Time, error) {
|
||||||
timezone := time.FixedZone("Timezone", int(utcOffset)*60)
|
timezone := time.FixedZone("Timezone", int(utcOffset)*60)
|
||||||
|
|||||||
@@ -46,17 +46,17 @@ func TestFormatUnixTimeToLongDateTimeWithTimezone(t *testing.T) {
|
|||||||
assert.Equal(t, expectedValue, actualValue)
|
assert.Equal(t, expectedValue, actualValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFormatUnixTimeToLongDateTimeWithTimezoneRFC3389Format(t *testing.T) {
|
func TestFormatUnixTimeToLongDateTimeWithTimezoneRFC3339Format(t *testing.T) {
|
||||||
unixTime := int64(1617228083)
|
unixTime := int64(1617228083)
|
||||||
utcTimezone := time.FixedZone("Test Timezone", 0) // UTC
|
utcTimezone := time.FixedZone("Test Timezone", 0) // UTC
|
||||||
utc8Timezone := time.FixedZone("Test Timezone", 28800) // UTC+8
|
utc8Timezone := time.FixedZone("Test Timezone", 28800) // UTC+8
|
||||||
|
|
||||||
expectedValue := "2021-03-31T22:01:23Z"
|
expectedValue := "2021-03-31T22:01:23Z"
|
||||||
actualValue := FormatUnixTimeToLongDateTimeWithTimezoneRFC3389Format(unixTime, utcTimezone)
|
actualValue := FormatUnixTimeToLongDateTimeWithTimezoneRFC3339Format(unixTime, utcTimezone)
|
||||||
assert.Equal(t, expectedValue, actualValue)
|
assert.Equal(t, expectedValue, actualValue)
|
||||||
|
|
||||||
expectedValue = "2021-04-01T06:01:23+08:00"
|
expectedValue = "2021-04-01T06:01:23+08:00"
|
||||||
actualValue = FormatUnixTimeToLongDateTimeWithTimezoneRFC3389Format(unixTime, utc8Timezone)
|
actualValue = FormatUnixTimeToLongDateTimeWithTimezoneRFC3339Format(unixTime, utc8Timezone)
|
||||||
assert.Equal(t, expectedValue, actualValue)
|
assert.Equal(t, expectedValue, actualValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,6 +228,15 @@ func TestParseFromLongDateTimeWithTimezone2(t *testing.T) {
|
|||||||
assert.Equal(t, expectedValue, actualValue)
|
assert.Equal(t, expectedValue, actualValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParseFromLongDateTimeWithTimezoneRFC3339Format(t *testing.T) {
|
||||||
|
expectedValue := int64(1617238883)
|
||||||
|
actualTime, err := ParseFromLongDateTimeWithTimezoneRFC3339Format("2021-04-01T06:01:23+05:00")
|
||||||
|
assert.Equal(t, nil, err)
|
||||||
|
|
||||||
|
actualValue := actualTime.Unix()
|
||||||
|
assert.Equal(t, expectedValue, actualValue)
|
||||||
|
}
|
||||||
|
|
||||||
func TestParseFromLongDateTimeWithoutSecond(t *testing.T) {
|
func TestParseFromLongDateTimeWithoutSecond(t *testing.T) {
|
||||||
expectedValue := int64(1691947440)
|
expectedValue := int64(1691947440)
|
||||||
actualTime, err := ParseFromLongDateTimeWithoutSecond("2023-08-13 17:24", 0)
|
actualTime, err := ParseFromLongDateTimeWithoutSecond("2023-08-13 17:24", 0)
|
||||||
|
|||||||
Reference in New Issue
Block a user