diff --git a/core/api/mcp.go b/core/api/mcp.go new file mode 100644 index 00000000..a19a42e4 --- /dev/null +++ b/core/api/mcp.go @@ -0,0 +1,399 @@ +package api + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/httptest" + "net/url" + "regexp" + "strings" + "sync" + + log "github.com/cihub/seelog" + "github.com/mark3labs/mcp-go/mcp" + mcpserver "github.com/mark3labs/mcp-go/server" + httprouter "infini.sh/framework/core/api/router" + "infini.sh/framework/core/config" + "infini.sh/framework/core/global" + "infini.sh/framework/core/util" +) + +var ( + mcpAutoMutex sync.Mutex + mcpAutoServer *mcpserver.MCPServer +) + +type mcpRouteArguments struct { + PathParams map[string]interface{} `json:"path_params"` + Query map[string]interface{} `json:"query"` + Headers map[string]interface{} `json:"headers"` + Body interface{} `json:"body"` + RawBody string `json:"raw_body"` +} + +type mcpAuthContextKey struct{} + +type mcpToolMetadata struct { + Options *HandlerOptions +} + +var ( + mcpAutoToolMetadata = map[string]mcpToolMetadata{} + + mcpToolAuthorizerMutex sync.RWMutex + mcpToolAuthorizer func(headers http.Header, options *HandlerOptions) bool +) + +// RegisterMCPToolAuthorizer registers a callback used to authorize MCP tool visibility and invocation. +// Returning false from authorizer blocks both listing and calling the tool for the current request headers. +func RegisterMCPToolAuthorizer(authorizer func(headers http.Header, options *HandlerOptions) bool) { + mcpToolAuthorizerMutex.Lock() + mcpToolAuthorizer = authorizer + mcpToolAuthorizerMutex.Unlock() +} + +func registerMCPAutoUIHandler(cfg config.WebAppConfig) { + if !cfg.MCP.Enabled { + return + } + + name := strings.TrimSpace(cfg.MCP.Name) + if name == "" { + name = global.Env().GetAppCapitalName() + " UI" + } + version := strings.TrimSpace(cfg.MCP.Version) + if version == "" { + version = global.Env().GetVersion() + } + + server := mcpserver.NewMCPServer(name, version, mcpserver.WithToolFilter(filterMCPAutoToolsByPermission)) + mcpAutoMutex.Lock() + mcpAutoServer = server + mcpAutoToolMetadata = map[string]mcpToolMetadata{} + mcpAutoMutex.Unlock() + + WalkMCPAutoUIMethodRoutes(func(route RegisteredUIMethodRoute) { + addMCPAutoUIMethodTool(server, route.Route.Method, route.Route.Path, RegisteredAPIHandler{Handler: route.Handler, Options: route.Options}) + }) + + opts := []mcpserver.StreamableHTTPOption{} + if cfg.MCP.Stateless { + opts = append(opts, mcpserver.WithStateLess(true)) + } + if cfg.MCP.DisableStreaming { + opts = append(opts, mcpserver.WithDisableStreaming(true)) + } + + path := strings.TrimSpace(cfg.MCP.BasePath) + if path == "" { + path = "/mcp" + } + if !strings.HasPrefix(path, "/") { + path = "/" + path + } + path = strings.TrimRight(path, "/") + if path == "" { + path = "/mcp" + } + + log.Infof("register MCP auto UI handler: %s", path) + opts = append(opts, mcpserver.WithHTTPContextFunc(func(ctx context.Context, r *http.Request) context.Context { + return context.WithValue(ctx, mcpAuthContextKey{}, r.Header.Clone()) + })) + uiServeMux.Handle(path, mcpserver.NewStreamableHTTPServer(server, opts...)) +} + +func registerMCPAutoUIMethodTool(method Method, pattern string, handler RegisteredAPIHandler) { + if handler.Options == nil || !handler.Options.Feature(FeatureMCPAuto) { + return + } + + mcpAutoMutex.Lock() + server := mcpAutoServer + mcpAutoMutex.Unlock() + if server == nil { + return + } + + addMCPAutoUIMethodTool(server, method, pattern, handler) +} + +func addMCPAutoUIMethodTool(server *mcpserver.MCPServer, method Method, pattern string, handler RegisteredAPIHandler) { + if server == nil || handler.Handler == nil || handler.Options == nil || !handler.Options.Feature(FeatureMCPAuto) { + return + } + + toolName := getMCPAutoToolName(method, pattern, handler.Options) + description := getMCPAutoToolDescription(method, pattern, handler.Options) + mcpAutoMutex.Lock() + mcpAutoToolMetadata[toolName] = mcpToolMetadata{Options: cloneHandlerOptions(handler.Options)} + mcpAutoMutex.Unlock() + server.AddTool(newMCPAutoTool(toolName, description, handler.Options), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + return callMCPAutoUIRoute(method, pattern, handler, request) + }) +} + +func newMCPAutoTool(name, description string, options *HandlerOptions) mcp.Tool { + var tool mcp.Tool + if rawSchema := getRawJSONLabel(options, MCPToolInputSchema); len(rawSchema) > 0 { + tool = mcp.NewToolWithRawSchema(name, description, rawSchema) + } else { + tool = mcp.NewToolWithRawSchema(name, description, json.RawMessage(`{ + "type":"object", + "properties":{ + "path_params":{"type":"object","additionalProperties":true,"description":"Values for route parameters such as :id."}, + "query":{"type":"object","additionalProperties":true,"description":"Query string parameters."}, + "headers":{"type":"object","additionalProperties":true,"description":"Additional request headers."}, + "body":{"description":"JSON request body for POST, PUT, PATCH, or DELETE handlers."}, + "raw_body":{"type":"string","description":"Raw request body. Used when body is not set."} + }, + "additionalProperties":false + }`)) + } + + if rawOutputSchema := getRawJSONLabel(options, MCPToolOutputSchema); len(rawOutputSchema) > 0 { + tool.RawOutputSchema = rawOutputSchema + } + return tool +} + +func callMCPAutoUIRoute(method Method, pattern string, handler RegisteredAPIHandler, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + if !authorizeMCPToolRequest(request.Header, handler.Options) { + return mcp.NewToolResultError("permission denied"), nil + } + + arguments, err := decodeMCPRouteArguments(request) + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + path, params, err := buildMCPAutoRequestPath(pattern, arguments.PathParams) + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + body, err := encodeMCPAutoRequestBody(arguments) + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + requestURL := &url.URL{Path: path, RawQuery: encodeMCPAutoQuery(arguments.Query)} + req := httptest.NewRequest(string(method), requestURL.String(), bytes.NewReader(body)) + copyMCPAutoHeaders(req.Header, request.Header) + copyMCPAutoHeaderMap(req.Header, arguments.Headers) + if len(body) > 0 && req.Header.Get("Content-Type") == "" { + req.Header.Set("Content-Type", "application/json") + } + + recorder := httptest.NewRecorder() + getWrappedHandler(string(method), pattern, handler)(recorder, req, params) + response := recorder.Result() + defer response.Body.Close() + + responseBody, err := io.ReadAll(response.Body) + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + result := util.MapStr{ + "status_code": response.StatusCode, + "headers": response.Header, + "body": string(responseBody), + } + var jsonBody interface{} + if len(responseBody) > 0 && json.Unmarshal(responseBody, &jsonBody) == nil { + result["json"] = jsonBody + } + + if response.StatusCode >= http.StatusBadRequest { + toolResult := mcp.NewToolResultStructured(result, string(responseBody)) + toolResult.IsError = true + return toolResult, nil + } + return mcp.NewToolResultStructured(result, string(responseBody)), nil +} + +func decodeMCPRouteArguments(request mcp.CallToolRequest) (mcpRouteArguments, error) { + var arguments mcpRouteArguments + raw := request.GetRawArguments() + if raw == nil { + return arguments, nil + } + data, err := json.Marshal(raw) + if err != nil { + return arguments, fmt.Errorf("failed to marshal MCP tool arguments: %w", err) + } + if err := json.Unmarshal(data, &arguments); err != nil { + return arguments, fmt.Errorf("failed to decode MCP tool arguments: %w", err) + } + return arguments, nil +} + +func buildMCPAutoRequestPath(pattern string, pathParams map[string]interface{}) (string, httprouter.Params, error) { + path := pattern + matches := routeParamPattern.FindAllStringSubmatch(pattern, -1) + params := make(httprouter.Params, 0, len(matches)) + for _, match := range matches { + name := match[1] + value, ok := pathParams[name] + if !ok { + return "", nil, fmt.Errorf("missing required path parameter %q", name) + } + valueString := fmt.Sprint(value) + params = append(params, httprouter.Param{Key: name, Value: valueString}) + path = strings.ReplaceAll(path, ":"+name, url.PathEscape(valueString)) + } + return path, params, nil +} + +func encodeMCPAutoRequestBody(arguments mcpRouteArguments) ([]byte, error) { + if arguments.Body != nil { + return json.Marshal(arguments.Body) + } + if arguments.RawBody != "" { + return []byte(arguments.RawBody), nil + } + return nil, nil +} + +func encodeMCPAutoQuery(query map[string]interface{}) string { + values := url.Values{} + for key, value := range query { + switch typed := value.(type) { + case []interface{}: + for _, item := range typed { + values.Add(key, fmt.Sprint(item)) + } + default: + values.Set(key, fmt.Sprint(value)) + } + } + return values.Encode() +} + +func copyMCPAutoHeaders(target http.Header, source http.Header) { + for key, values := range source { + for _, value := range values { + target.Add(key, value) + } + } +} + +func copyMCPAutoHeaderMap(target http.Header, headers map[string]interface{}) { + for key, value := range headers { + switch typed := value.(type) { + case []interface{}: + for _, item := range typed { + target.Add(key, fmt.Sprint(item)) + } + default: + target.Set(key, fmt.Sprint(value)) + } + } +} + +func getMCPAutoToolName(method Method, pattern string, options *HandlerOptions) string { + if options != nil && options.Labels != nil { + if value, ok := options.Labels[MCPToolName]; ok { + if name := strings.TrimSpace(fmt.Sprint(value)); name != "" { + return sanitizeMCPToolName(name) + } + } + } + return sanitizeMCPToolName(strings.ToLower(string(method)) + "_" + strings.Trim(pattern, "/")) +} + +func getMCPAutoToolDescription(method Method, pattern string, options *HandlerOptions) string { + if options != nil && options.Labels != nil { + if value, ok := options.Labels[MCPToolDescription]; ok { + if description := strings.TrimSpace(fmt.Sprint(value)); description != "" { + return description + } + } + } + return fmt.Sprintf("Call %s %s", method, pattern) +} + +func getRawJSONLabel(options *HandlerOptions, label string) json.RawMessage { + if options == nil || options.Labels == nil { + return nil + } + value, ok := options.Labels[label] + if !ok || value == nil { + return nil + } + switch typed := value.(type) { + case json.RawMessage: + return typed + case []byte: + return json.RawMessage(typed) + case string: + return json.RawMessage(typed) + default: + data, err := json.Marshal(typed) + if err != nil { + return nil + } + return data + } +} + +func sanitizeMCPToolName(name string) string { + name = strings.TrimSpace(name) + name = strings.ReplaceAll(name, ":", "_") + name = mcpToolNamePattern.ReplaceAllString(name, "_") + name = strings.Trim(name, "_") + if name == "" { + return "ui_handler" + } + return name +} + +func filterMCPAutoToolsByPermission(ctx context.Context, tools []mcp.Tool) []mcp.Tool { + headers, _ := ctx.Value(mcpAuthContextKey{}).(http.Header) + if headers == nil { + headers = http.Header{} + } + + mcpAutoMutex.Lock() + metadata := make(map[string]mcpToolMetadata, len(mcpAutoToolMetadata)) + for name, entry := range mcpAutoToolMetadata { + metadata[name] = entry + } + mcpAutoMutex.Unlock() + + filtered := make([]mcp.Tool, 0, len(tools)) + for _, tool := range tools { + entry, ok := metadata[tool.Name] + if !ok { + filtered = append(filtered, tool) + continue + } + if authorizeMCPToolRequest(headers, entry.Options) { + filtered = append(filtered, tool) + } + } + return filtered +} + +func authorizeMCPToolRequest(headers http.Header, options *HandlerOptions) bool { + mcpToolAuthorizerMutex.RLock() + authorizer := mcpToolAuthorizer + mcpToolAuthorizerMutex.RUnlock() + if authorizer == nil { + return true + } + if headers == nil { + headers = http.Header{} + } + return authorizer(headers, options) +} + +var ( + routeParamPattern = regexp.MustCompile(`:([A-Za-z0-9_]+)`) + mcpToolNamePattern = regexp.MustCompile(`[^A-Za-z0-9_]+`) +) diff --git a/core/api/mcp_registry.go b/core/api/mcp_registry.go new file mode 100644 index 00000000..19af8ac4 --- /dev/null +++ b/core/api/mcp_registry.go @@ -0,0 +1,90 @@ +package api + +import ( + "sort" + + httprouter "infini.sh/framework/core/api/router" + "infini.sh/framework/core/util" +) + +type RegisteredUIMethodRoute struct { + Route MCPAutoRoute + Options *HandlerOptions + Handler httprouter.Handle +} + +type MCPAutoRoute struct { + Method Method + Path string +} + +func WalkRegisteredUIMethodRoutes(walk func(route RegisteredUIMethodRoute)) { + if walk == nil { + return + } + + uiMutex.Lock() + routes := make([]RegisteredUIMethodRoute, 0) + for method, handlers := range registeredUIMethodHandler { + for path, handler := range handlers { + routes = append(routes, RegisteredUIMethodRoute{ + Route: MCPAutoRoute{Method: method, Path: path}, + Options: cloneHandlerOptions(handler.Options), + Handler: handler.Handler, + }) + } + } + uiMutex.Unlock() + + sort.Slice(routes, func(i, j int) bool { + if routes[i].Route.Method == routes[j].Route.Method { + return routes[i].Route.Path < routes[j].Route.Path + } + return routes[i].Route.Method < routes[j].Route.Method + }) + + for _, route := range routes { + walk(route) + } +} + +func WalkMCPAutoUIMethodRoutes(walk func(route RegisteredUIMethodRoute)) { + if walk == nil { + return + } + + WalkRegisteredUIMethodRoutes(func(route RegisteredUIMethodRoute) { + if route.Options == nil || !route.Options.Feature(FeatureMCPAuto) { + return + } + walk(route) + }) +} + +func cloneHandlerOptions(options *HandlerOptions) *HandlerOptions { + if options == nil { + return nil + } + + cloned := *options + if options.RequirePermission != nil { + cloned.RequirePermission = append([]PermissionKey(nil), options.RequirePermission...) + } + if options.Tags != nil { + cloned.Tags = append([]string(nil), options.Tags...) + } + if options.Features != nil { + cloned.Features = map[string]bool{} + for key, value := range options.Features { + cloned.Features[key] = value + } + } + if options.Labels != nil { + cloned.Labels = util.MapStr{} + for key, value := range options.Labels { + cloned.Labels[key] = value + } + } + + return &cloned +} diff --git a/core/api/mcp_test.go b/core/api/mcp_test.go new file mode 100644 index 00000000..a702caaf --- /dev/null +++ b/core/api/mcp_test.go @@ -0,0 +1,99 @@ +package api + +import ( + "encoding/json" + "net/http" + "testing" + + "github.com/mark3labs/mcp-go/mcp" + httprouter "infini.sh/framework/core/api/router" + "infini.sh/framework/core/util" +) + +func TestAuthorizeMCPToolRequest(t *testing.T) { + RegisterMCPToolAuthorizer(nil) + t.Cleanup(func() { + RegisterMCPToolAuthorizer(nil) + }) + + if !authorizeMCPToolRequest(nil, &HandlerOptions{}) { + t.Fatal("expected allow when no authorizer is registered") + } + + RegisterMCPToolAuthorizer(func(headers http.Header, options *HandlerOptions) bool { + return headers.Get("X-Allow") == "1" + }) + + if authorizeMCPToolRequest(http.Header{}, &HandlerOptions{}) { + t.Fatal("expected deny when authorizer rejects headers") + } + + if !authorizeMCPToolRequest(http.Header{"X-Allow": []string{"1"}}, &HandlerOptions{}) { + t.Fatal("expected allow when authorizer accepts headers") + } +} + +func TestMCPAutoUIRouteCallsRegisteredHandler(t *testing.T) { + handler := RegisteredAPIHandler{ + Options: &HandlerOptions{}, + Handler: func(w http.ResponseWriter, req *http.Request, ps httprouter.Params) { + var body map[string]interface{} + if err := json.NewDecoder(req.Body).Decode(&body); err != nil { + t.Fatalf("decode body: %v", err) + } + WriteJSON(w, util.MapStr{ + "id": ps.MustGetParameter("id"), + "q": req.URL.Query().Get("q"), + "authorization": req.Header.Get("Authorization"), + "name": body["name"], + }, http.StatusCreated) + }, + } + + result, err := callMCPAutoUIRoute(POST, "/things/:id", handler, mcp.CallToolRequest{ + Header: http.Header{"Authorization": []string{"Bearer token"}}, + Params: mcp.CallToolParams{ + Arguments: map[string]interface{}{ + "path_params": map[string]interface{}{"id": "thing-1"}, + "query": map[string]interface{}{"q": "search"}, + "body": map[string]interface{}{"name": "test"}, + }, + }, + }) + if err != nil { + t.Fatalf("call MCP auto UI route: %v", err) + } + if result.IsError { + t.Fatalf("expected successful MCP result: %#v", result) + } + + structured, ok := result.StructuredContent.(util.MapStr) + if !ok { + t.Fatalf("expected structured map result, got %T", result.StructuredContent) + } + if structured["status_code"] != http.StatusCreated { + t.Fatalf("unexpected status code: %#v", structured["status_code"]) + } + + jsonBody, ok := structured["json"].(map[string]interface{}) + if !ok { + t.Fatalf("expected JSON response body, got %#v", structured["json"]) + } + if jsonBody["id"] != "thing-1" || jsonBody["q"] != "search" || jsonBody["authorization"] != "Bearer token" || jsonBody["name"] != "test" { + t.Fatalf("unexpected JSON response: %#v", jsonBody) + } +} + +func TestNewMCPAutoToolUsesCustomSchemas(t *testing.T) { + tool := newMCPAutoTool("custom_tool", "Custom tool", &HandlerOptions{Labels: util.MapStr{ + MCPToolInputSchema: json.RawMessage(`{"type":"object","properties":{"id":{"type":"string"}},"required":["id"]}`), + MCPToolOutputSchema: json.RawMessage(`{"type":"object","properties":{"ok":{"type":"boolean"}}}`), + }}) + + if string(tool.RawInputSchema) == "" { + t.Fatal("expected custom input schema") + } + if string(tool.RawOutputSchema) == "" { + t.Fatal("expected custom output schema") + } +} diff --git a/core/api/options.go b/core/api/options.go index cddeb879..da0a9e83 100644 --- a/core/api/options.go +++ b/core/api/options.go @@ -27,6 +27,14 @@ import ( "infini.sh/framework/core/util" ) +const ( + FeatureMCPAuto = "feature_mcp_auto" + MCPToolName = "mcp_tool_name" + MCPToolDescription = "mcp_tool_description" + MCPToolInputSchema = "mcp_tool_input_schema" + MCPToolOutputSchema = "mcp_tool_output_schema" +) + // Define the Option type (function that modifies HandlerOptions) type Option func(*HandlerOptions) @@ -161,6 +169,31 @@ func Feature(feature string) Option { } } +func MCPAuto() Option { + return Feature(FeatureMCPAuto) +} + +func MCPTool(name, description string) Option { + return func(o *HandlerOptions) { + MCPAuto()(o) + if o.Labels == nil { + o.Labels = util.MapStr{} + } + o.Labels[MCPToolName] = name + o.Labels[MCPToolDescription] = description + } +} + +func WithHandlerOptions(options *HandlerOptions) Option { + return func(o *HandlerOptions) { + if options == nil { + return + } + cloned := cloneHandlerOptions(options) + *o = *cloned + } +} + func (option *HandlerOptions) Feature(feature string) bool { if option.Features != nil { v, ok := option.Features[feature] diff --git a/core/api/web.go b/core/api/web.go index 76406ff0..62cfff5e 100755 --- a/core/api/web.go +++ b/core/api/web.go @@ -106,6 +106,8 @@ func StartWeb(cfg config.WebAppConfig) { } } + registerMCPAutoUIHandler(cfg) + if cfg.EmbeddingAPI { if registeredAPIMethodHandler != nil { for k, v := range registeredAPIMethodHandler { @@ -453,12 +455,14 @@ func HandleUIMethod(method Method, pattern string, handler func(w http.ResponseW myHandler := RegisteredAPIHandler{Handler: handler, Options: opts} registeredUIMethodHandler[method][pattern] = myHandler + registerMCPAutoUIMethodTool(method, pattern, myHandler) if opts.AllowOPTIONS { m := registeredUIMethodHandler[OPTIONS] if m == nil { registeredUIMethodHandler[OPTIONS] = map[string]RegisteredAPIHandler{} } registeredUIMethodHandler[OPTIONS][pattern] = myHandler + registerMCPAutoUIMethodTool(OPTIONS, pattern, myHandler) } } diff --git a/core/config/system.go b/core/config/system.go index b2f3bbc0..2424da87 100755 --- a/core/config/system.go +++ b/core/config/system.go @@ -392,12 +392,22 @@ type WebAppConfig struct { BasePath string `config:"base_path"` Domain string `config:"domain"` EmbeddingAPI bool `config:"embedding_api"` + MCP MCPConfig `config:"mcp"` Gzip GzipConfig `config:"gzip"` S3Config S3BucketConfig `config:"s3"` Cookie CookieConfig `config:"cookie"` } +type MCPConfig struct { + Enabled bool `config:"enabled"` + BasePath string `config:"base_path"` + Name string `config:"name"` + Version string `config:"version"` + Stateless bool `config:"stateless"` + DisableStreaming bool `config:"disable_streaming"` +} + type S3Config struct { Endpoint string `config:"endpoint" json:"endpoint,omitempty"` AccessKey string `config:"access_key" json:"access_key,omitempty"` diff --git a/core/security/filters/mcp_authorizer.go b/core/security/filters/mcp_authorizer.go new file mode 100644 index 00000000..5b7d3ada --- /dev/null +++ b/core/security/filters/mcp_authorizer.go @@ -0,0 +1,48 @@ +/* Copyright © INFINI LTD. All rights reserved. + * Web: https://infinilabs.com + * Email: hello#infini.ltd */ + +package filters + +import ( + "net/http" + + "infini.sh/framework/core/api" + common "infini.sh/framework/core/api/common" + "infini.sh/framework/core/security" + "infini.sh/framework/core/util" +) + +func init() { + api.RegisterMCPToolAuthorizer(func(headers http.Header, options *api.HandlerOptions) bool { + if options == nil || !common.IsAuthEnable() { + return true + } + if !options.RequireLogin && !options.OptionLogin && len(options.RequirePermission) == 0 { + return true + } + + req := &http.Request{Header: headers} + user, err := security.ValidateLogin(nil, req) + if err != nil || user == nil || !user.IsValid() { + // optional-login routes can still be called anonymously unless explicit permissions are required + return options.OptionLogin && len(options.RequirePermission) == 0 + } + + if len(options.RequirePermission) == 0 { + return true + } + + if user.Roles != nil && util.AnyInArrayEquals(user.Roles, security.RoleAdmin) { + return true + } + + if user.UserAssignedPermission == nil || user.UserAssignedPermission.NeedRefresh() { + user.UserAssignedPermission = security.GetUserPermissions(user) + } + if user.UserAssignedPermission == nil { + return false + } + return user.UserAssignedPermission.Validate(security.MustRegisterPermissionByKeys(options.RequirePermission)) + }) +} diff --git a/go.mod b/go.mod index 917e3a71..b57bce6e 100644 --- a/go.mod +++ b/go.mod @@ -36,6 +36,7 @@ require ( github.com/libdns/libdns v1.1.1 github.com/magiconair/properties v1.8.10 github.com/mailru/easyjson v0.9.0 + github.com/mark3labs/mcp-go v0.43.1 github.com/minio/minio-go/v7 v7.0.90 github.com/mitchellh/mapstructure v1.5.0 github.com/ncruces/go-sqlite3 v0.25.0 diff --git a/go.sum b/go.sum index 4b3c9378..ff0bf4ad 100644 --- a/go.sum +++ b/go.sum @@ -213,6 +213,8 @@ github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3 github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +github.com/mark3labs/mcp-go v0.43.1 h1:WXNVd+bRM/7mOzCM9zulSwn/s9YEdAxbmeh9LoRHEXY= +github.com/mark3labs/mcp-go v0.43.1/go.mod h1:YnJfOL382MIWDx1kMY+2zsRHU/q78dBg9aFb8W6Thdw= github.com/mattn/go-colorable v0.0.10-0.20170816031813-ad5389df28cd/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=