diff --git a/cmd/plugin-backend.go b/cmd/plugin-backend.go index d3700788a..4a2fbcb91 100644 --- a/cmd/plugin-backend.go +++ b/cmd/plugin-backend.go @@ -2,6 +2,7 @@ package main import ( "context" + "errors" "flag" "fmt" "os" @@ -42,7 +43,7 @@ func main() { } logrus.SetLevel(lvl) log.Infof("Starting %s at log level %s", appVersion, *logLevel) - + log.Infof("Loading config from %s", *configPath) cfg, err := config.ReadFile(buildVersion, buildDate, *configPath) if err != nil { log.WithError(err).Fatal("error reading config file") @@ -62,11 +63,18 @@ func main() { model.AddFlowLineMapping(decoders.NetworkEventsToString) } - go server.StartMetrics(&server.MetricsConfig{ - Port: cfg.Server.MetricsPort, - CertPath: cfg.Server.CertPath, - KeyPath: cfg.Server.KeyPath, - }) + ctx := context.Background() + if cfg.Server.MCP.Enable { + if err := server.StartMCPServer(ctx, cfg); err != nil && !errors.Is(err, context.Canceled) { + panic(err) + } + } else { + go server.StartMetrics(&server.MetricsConfig{ + Port: cfg.Server.MetricsPort, + CertPath: cfg.Server.CertPath, + KeyPath: cfg.Server.KeyPath, + }) - server.Start(context.Background(), cfg, checker) + server.Start(ctx, cfg, checker) + } } diff --git a/config/sample-config.yaml b/config/sample-config.yaml index 62d946d75..fa6024533 100644 --- a/config/sample-config.yaml +++ b/config/sample-config.yaml @@ -2,6 +2,11 @@ server: port: 9001 metricsPort: 9002 authCheck: none + mcp: + enable: true + transport: stdio + sseAddress: localhost + ssePort: 3001 loki: url: http://localhost:3100 labels: @@ -18,7 +23,7 @@ loki: # - SrcK8S_Zone # - DstK8S_Zone tenantID: netobserv - useMocks: false + useMocks: true prometheus: timeout: 30s metrics: diff --git a/go.mod b/go.mod index 7c2315103..1dd943ff9 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ toolchain go1.23.5 require ( github.com/gorilla/mux v1.8.1 github.com/json-iterator/go v1.1.12 + github.com/mark3labs/mcp-go v0.16.0 github.com/mitchellh/mapstructure v1.5.0 github.com/netobserv/netobserv-ebpf-agent v1.9.0-crc0 github.com/prometheus/client_golang v1.22.0 @@ -51,6 +52,7 @@ require ( github.com/spf13/pflag v1.0.6 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/x448/float16 v0.8.4 // indirect + github.com/yosida95/uritemplate/v3 v3.0.2 // indirect golang.org/x/net v0.39.0 // indirect golang.org/x/oauth2 v0.29.0 // indirect golang.org/x/sys v0.32.0 // indirect diff --git a/go.sum b/go.sum index 0869f6ac4..fb3fc3886 100644 --- a/go.sum +++ b/go.sum @@ -76,6 +76,8 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 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.16.0 h1:hNOr0EqhSUra5jm1Wv6+BOynzIa+bMtfP3zgde70MvY= +github.com/mark3labs/mcp-go v0.16.0/go.mod h1:KmJndYv7GIgcPVwEKJjNcbhVQ+hJGJhrCCB/9xITzpE= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -153,6 +155,8 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= +github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= +github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= diff --git a/pkg/config/config.go b/pkg/config/config.go index 0708afeb9..0a4797905 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -19,6 +19,13 @@ var ( log = logrus.WithField("module", "config") ) +type MCP struct { + Enable bool `yaml:"enable,omitempty" json:"enable,omitempty"` + Transport string `yaml:"transport,omitempty" json:"transport,omitempty"` + SSEAddress string `yaml:"sseAddress,omitempty" json:"sseAddress,omitempty"` + SSEPort int `yaml:"ssePort,omitempty" json:"ssePort,omitempty"` +} + type Server struct { Port int `yaml:"port,omitempty" json:"port,omitempty"` MetricsPort int `yaml:"metricsPort,omitempty" json:"metricsPort,omitempty"` @@ -29,6 +36,7 @@ type Server struct { CORSHeaders string `yaml:"corsHeaders,omitempty" json:"corsHeaders,omitempty"` CORSMaxAge string `yaml:"corsMaxAge,omitempty" json:"corsMaxAge,omitempty"` AuthCheck string `yaml:"authCheck,omitempty" json:"authCheck,omitempty"` + MCP MCP `yaml:"mcp,omitempty" json:"mcp,omitempty"` } type Prometheus struct { diff --git a/pkg/handler/clients.go b/pkg/handler/clients.go index 1d5684480..605f31a6e 100644 --- a/pkg/handler/clients.go +++ b/pkg/handler/clients.go @@ -22,7 +22,7 @@ type clients struct { promDev api.Client } -func newClients(cfg *config.Config, requestHeader http.Header, useLokiStatus bool, namespace string) (clients, error) { +func NewClients(cfg *config.Config, requestHeader http.Header, useLokiStatus bool, namespace string) (clients, error) { lokiClients := newLokiClients(cfg, requestHeader, useLokiStatus) promClients, err := newPromClients(cfg, requestHeader, namespace) return clients{loki: lokiClients.loki, promAdmin: promClients.promAdmin, promDev: promClients.promDev}, err @@ -49,7 +49,7 @@ func newPromClients(cfg *config.Config, requestHeader http.Header, namespace str func newLokiClients(cfg *config.Config, requestHeader http.Header, useLokiStatus bool) clients { var lokiClient httpclient.Caller if cfg.IsLokiEnabled() { - lokiClient = newLokiClient(&cfg.Loki, requestHeader, useLokiStatus) + lokiClient = NewLokiClient(&cfg.Loki, requestHeader, useLokiStatus) } return clients{loki: lokiClient} } diff --git a/pkg/handler/export.go b/pkg/handler/export.go index 7eb64f2c8..bcaeb3bff 100644 --- a/pkg/handler/export.go +++ b/pkg/handler/export.go @@ -22,7 +22,7 @@ func (h *Handlers) ExportFlows(ctx context.Context) func(w http.ResponseWriter, writeError(w, http.StatusBadRequest, "Cannot perform flows query with disabled Loki") return } - cl := newLokiClient(&h.Cfg.Loki, r.Header, false) + cl := NewLokiClient(&h.Cfg.Loki, r.Header, false) var code int startTime := time.Now() defer func() { @@ -32,7 +32,7 @@ func (h *Handlers) ExportFlows(ctx context.Context) func(w http.ResponseWriter, params := r.URL.Query() hlog.Debugf("ExportFlows query params: %s", params) - flows, code, err := h.getFlows(ctx, cl, params) + flows, code, err := h.QueryFlows(ctx, cl, params) if err != nil { writeError(w, code, err.Error()) return diff --git a/pkg/handler/flows.go b/pkg/handler/flows.go index 2132b6471..3479a2107 100644 --- a/pkg/handler/flows.go +++ b/pkg/handler/flows.go @@ -34,7 +34,7 @@ func (h *Handlers) GetFlows(ctx context.Context) func(w http.ResponseWriter, r * return } - cl := newLokiClient(&h.Cfg.Loki, r.Header, false) + cl := NewLokiClient(&h.Cfg.Loki, r.Header, false) var code int startTime := time.Now() defer func() { @@ -44,7 +44,7 @@ func (h *Handlers) GetFlows(ctx context.Context) func(w http.ResponseWriter, r * params := r.URL.Query() hlog.Debugf("GetFlows query params: %s", params) - flows, code, err := h.getFlows(ctx, cl, params) + flows, code, err := h.QueryFlows(ctx, cl, params) if err != nil { writeError(w, code, err.Error()) return @@ -55,7 +55,7 @@ func (h *Handlers) GetFlows(ctx context.Context) func(w http.ResponseWriter, r * } } -func (h *Handlers) getFlows(ctx context.Context, lokiClient httpclient.Caller, params url.Values) (*model.AggregatedQueryResponse, int, error) { +func (h *Handlers) QueryFlows(ctx context.Context, lokiClient httpclient.Caller, params url.Values) (*model.AggregatedQueryResponse, int, error) { start, _, err := getStartTime(params) if err != nil { return nil, http.StatusBadRequest, err diff --git a/pkg/handler/frontend-config.go b/pkg/handler/frontend-config.go index 3dd146627..6191b1130 100644 --- a/pkg/handler/frontend-config.go +++ b/pkg/handler/frontend-config.go @@ -20,7 +20,7 @@ func (h *Handlers) GetFrontendConfig() func(w http.ResponseWriter, r *http.Reque } if h.Cfg.IsLokiEnabled() { // (Re)load Loki max chunk age - lokiClient := newLokiClient(&h.Cfg.Loki, r.Header, true) + lokiClient := NewLokiClient(&h.Cfg.Loki, r.Header, true) if maxChunkAge, err := h.fetchIngesterMaxChunkAge(lokiClient); err != nil { // Log the error, but keep returning known config hlog.Errorf("Could not get max chunk age: %v", err) diff --git a/pkg/handler/loki.go b/pkg/handler/loki.go index d36adaca7..16080cebd 100644 --- a/pkg/handler/loki.go +++ b/pkg/handler/loki.go @@ -39,7 +39,7 @@ const ( lokiOrgIDHeader = "X-Scope-OrgID" ) -func newLokiClient(cfg *config.Loki, requestHeader http.Header, useStatusConfig bool) httpclient.Caller { +func NewLokiClient(cfg *config.Loki, requestHeader http.Header, useStatusConfig bool) httpclient.Caller { headers := map[string][]string{} if cfg.TenantID != "" { headers[lokiOrgIDHeader] = []string{cfg.TenantID} @@ -198,7 +198,7 @@ func getLokiNamesForPrefix(cfg *config.Loki, lokiClient httpclient.Caller, filts } func (h *Handlers) getLokiStatus(r *http.Request) ([]byte, int, error) { - lokiClient := newLokiClient(&h.Cfg.Loki, r.Header, true) + lokiClient := NewLokiClient(&h.Cfg.Loki, r.Header, true) baseURL := strings.TrimRight(h.Cfg.Loki.GetStatusURL(), "/") return executeLokiQuery(fmt.Sprintf("%s/%s", baseURL, "ready"), lokiClient) } @@ -231,7 +231,7 @@ func (h *Handlers) LokiMetrics() func(w http.ResponseWriter, r *http.Request) { writeError(w, http.StatusBadRequest, "Loki is disabled") return } - lokiClient := newLokiClient(&h.Cfg.Loki, r.Header, true) + lokiClient := NewLokiClient(&h.Cfg.Loki, r.Header, true) baseURL := strings.TrimRight(h.Cfg.Loki.GetStatusURL(), "/") resp, code, err := executeLokiQuery(fmt.Sprintf("%s/%s", baseURL, "metrics"), lokiClient) @@ -250,7 +250,7 @@ func (h *Handlers) LokiBuildInfos() func(w http.ResponseWriter, r *http.Request) writeError(w, http.StatusBadRequest, "Loki is disabled") return } - lokiClient := newLokiClient(&h.Cfg.Loki, r.Header, true) + lokiClient := NewLokiClient(&h.Cfg.Loki, r.Header, true) baseURL := strings.TrimRight(h.Cfg.Loki.GetStatusURL(), "/") resp, code, err := executeLokiQuery(fmt.Sprintf("%s/%s", baseURL, "loki/api/v1/status/buildinfo"), lokiClient) @@ -293,7 +293,7 @@ func (h *Handlers) LokiLimits() func(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusNoContent, "Loki is disabled") return } - lokiClient := newLokiClient(&h.Cfg.Loki, r.Header, true) + lokiClient := NewLokiClient(&h.Cfg.Loki, r.Header, true) limits, err := h.fetchLokiLimits(lokiClient) if err != nil { hlog.WithError(err).Error("cannot fetch Loki limits") diff --git a/pkg/handler/lokiclientmock/loki_client_mock.go b/pkg/handler/lokiclientmock/loki_client_mock.go index 61b5c39bd..7d8b12ee8 100644 --- a/pkg/handler/lokiclientmock/loki_client_mock.go +++ b/pkg/handler/lokiclientmock/loki_client_mock.go @@ -12,6 +12,8 @@ import ( var mlog = logrus.WithField("module", "lokiclientmock") +var folderPath = "/home/julien/dev/me/network-observability-console-plugin/" + type LokiClientMock struct { } @@ -23,10 +25,10 @@ func (o *LokiClientMock) Get(url string) ([]byte, int, error) { isLabel := strings.Contains(url, "/label/") if isLabel { - path = "mocks/loki/namespaces.json" + path = folderPath + "mocks/loki/namespaces.json" } else { if strings.Contains(url, "query=topk") { - path = "mocks/loki/flow_metrics" + path = folderPath + "mocks/loki/flow_metrics" if strings.Contains(url, "|unwrap%20PktDrop") { path += "_dropped" @@ -55,7 +57,7 @@ func (o *LokiClientMock) Get(url string) ([]byte, int, error) { path += "_resource.json" } } else { - path = "mocks/loki/flow_records" + path = folderPath + "mocks/loki/flow_records" //nolint:gocritic // if-else is ok if strings.Contains(url, "|~`\"Packets\":0[,}]|~`\"PktDropPackets\":[1-9][0-9]*[,}]") { path += "_dropped.json" diff --git a/pkg/handler/resources.go b/pkg/handler/resources.go index 8bf0d9295..623cac567 100644 --- a/pkg/handler/resources.go +++ b/pkg/handler/resources.go @@ -21,7 +21,7 @@ func (h *Handlers) GetClusters(ctx context.Context) func(w http.ResponseWriter, namespace := params.Get(namespaceKey) isDev := namespace != "" - clients, err := newClients(h.Cfg, r.Header, false, namespace) + clients, err := NewClients(h.Cfg, r.Header, false, namespace) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return @@ -50,7 +50,7 @@ func (h *Handlers) GetUDNs(ctx context.Context) func(w http.ResponseWriter, r *h namespace := params.Get(namespaceKey) isDev := namespace != "" - clients, err := newClients(h.Cfg, r.Header, false, namespace) + clients, err := NewClients(h.Cfg, r.Header, false, namespace) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return @@ -79,7 +79,7 @@ func (h *Handlers) GetZones(ctx context.Context) func(w http.ResponseWriter, r * namespace := params.Get(namespaceKey) isDev := namespace != "" - clients, err := newClients(h.Cfg, r.Header, false, namespace) + clients, err := NewClients(h.Cfg, r.Header, false, namespace) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return @@ -139,7 +139,7 @@ func (h *Handlers) GetNamespaces(ctx context.Context) func(w http.ResponseWriter namespace := params.Get(namespaceKey) isDev := namespace != "" - clients, err := newClients(h.Cfg, r.Header, false, namespace) + clients, err := NewClients(h.Cfg, r.Header, false, namespace) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return @@ -196,7 +196,7 @@ func (h *Handlers) GetNames(ctx context.Context) func(w http.ResponseWriter, r * namespace := params.Get(namespaceKey) kind := params.Get("kind") - clients, err := newClients(h.Cfg, r.Header, false, namespace) + clients, err := NewClients(h.Cfg, r.Header, false, namespace) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return diff --git a/pkg/handler/topology.go b/pkg/handler/topology.go index bf5b17ea8..d5e7b6afd 100644 --- a/pkg/handler/topology.go +++ b/pkg/handler/topology.go @@ -38,27 +38,26 @@ const ( func (h *Handlers) GetTopology(ctx context.Context) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { params := r.URL.Query() - namespace := params.Get(namespaceKey) - - clients, err := newClients(h.Cfg, r.Header, false, namespace) - if err != nil { - writeError(w, http.StatusInternalServerError, err.Error()) - return - } - var code int startTime := time.Now() defer func() { metrics.ObserveHTTPCall("GetTopology", code, startTime) }() + namespace := params.Get(namespaceKey) + clients, err := NewClients(h.Cfg, r.Header, false, namespace) + if err != nil { + writeError(w, http.StatusInternalServerError, err.Error()) + return + } + ds, err := getDatasource(params) if err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } - flows, code, err := h.getTopologyFlows(ctx, clients, params, ds) + flows, code, err := h.QueryMetrics(ctx, clients, params, ds) var dsErr *datasourceError if err != nil && ds == constants.DataSourceAuto && @@ -70,7 +69,7 @@ func (h *Handlers) GetTopology(ctx context.Context) func(w http.ResponseWriter, // This is because multi-tenancy is currently not managed for prom datasource, hence such queries have to go with Loki // Unfortunately we don't know a safe and generic way to pre-flight check if the user will be authorized hlog.Info("Retrying with Loki...") - flows, code, err = h.getTopologyFlows(ctx, clients, params, constants.DataSourceLoki) + flows, code, err = h.QueryMetrics(ctx, clients, params, constants.DataSourceLoki) } if err != nil { writeError(w, code, err.Error()) @@ -148,7 +147,7 @@ func (h *Handlers) extractTopologyQueryParams(params url.Values, ds constants.Da return &in, filterGroups, qr, reqLimit, err } -func (h *Handlers) getTopologyFlows(ctx context.Context, cl clients, params url.Values, ds constants.DataSource) (*model.AggregatedQueryResponse, int, error) { +func (h *Handlers) QueryMetrics(ctx context.Context, cl clients, params url.Values, ds constants.DataSource) (*model.AggregatedQueryResponse, int, error) { hlog.Debugf("GetTopology query params: %s", params) dataSources := make(map[constants.DataSource]bool) diff --git a/pkg/server/mcp.go b/pkg/server/mcp.go new file mode 100644 index 000000000..63e744039 --- /dev/null +++ b/pkg/server/mcp.go @@ -0,0 +1,157 @@ +package server + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/url" + + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" + "github.com/netobserv/network-observability-console-plugin/pkg/config" + "github.com/netobserv/network-observability-console-plugin/pkg/handler" + "github.com/netobserv/network-observability-console-plugin/pkg/model" + "github.com/netobserv/network-observability-console-plugin/pkg/prometheus" + "github.com/netobserv/network-observability-console-plugin/pkg/utils/constants" +) + +type MCPServer struct { + context context.Context + server *server.MCPServer + handler handler.Handlers +} + +func StartMCPServer(ctx context.Context, cfg *config.Config) error { + var promInventory *prometheus.Inventory + if cfg.IsPromEnabled() { + promInventory = prometheus.NewInventory(&cfg.Prometheus) + } + + s := &MCPServer{ + context: ctx, + server: server.NewMCPServer( + "mcp-netobserv", + cfg.Frontend.BuildVersion, + server.WithResourceCapabilities(true, true), + server.WithPromptCapabilities(true), + server.WithToolCapabilities(true), + server.WithLogging(), + ), + handler: handler.Handlers{Cfg: cfg, PromInventory: promInventory}, + } + + s.server.SetTools( + server.ServerTool{ + Tool: mcp.NewTool("flows", + getToolOptions("Get netobserv flow logs in the current cluster")..., + ), + Handler: s.GetFlows, + }, + server.ServerTool{ + Tool: mcp.NewTool("metrics", + getToolOptions("Get netobserv flow metrics in the current cluster")..., + ), + Handler: s.GetMetrics, + }, + ) + + switch cfg.Server.MCP.Transport { + case "stdio": + return server.ServeStdio(s.server) + case "sse": + options := make([]server.SSEOption, 0) + if cfg.Server.MCP.SSEAddress != "" { + options = append(options, server.WithBaseURL(cfg.Server.MCP.SSEAddress)) + } + return server.NewSSEServer(s.server, options...).Start(fmt.Sprintf(":%d", cfg.Server.MCP.SSEPort)) + default: + return fmt.Errorf("unknown MCP transport: %s", cfg.Server.MCP.Transport) + } +} + +func getToolOptions(description string) []mcp.ToolOption { + return []mcp.ToolOption{ + mcp.WithDescription(description), + //mcp.WithString("timeRange", mcp.Description("Time range to capture")), + //mcp.WithString("startTime", mcp.Description("Start time to capture")), + //mcp.WithString("endTime", mcp.Description("End time to capture")), + mcp.WithString("namespace", mcp.Description("Source or Destnation namespace of the flow")), + mcp.WithString("limit", mcp.Description("Limit results")), + //mcp.WithString("filters", mcp.Description("Filters to apply (url-encoded)")), + } +} + +func parseRequest(ctr mcp.CallToolRequest) (http.Header, url.Values) { + header := http.Header{} + params := url.Values{} + + for key, value := range ctr.Params.Arguments { + params.Add(key, fmt.Sprintf("%v", value)) + } + return header, params +} + +func parseResults(response *model.AggregatedQueryResponse, code int, err error) (*mcp.CallToolResult, error) { + if code != http.StatusOK || err != nil { + return &mcp.CallToolResult{ + IsError: true, + Content: []mcp.Content{ + mcp.TextContent{ + Type: "text", + Text: fmt.Sprintf("query returned status %d error %v", code, err), + }, + }, + }, nil + } + + content, err := json.Marshal(response) + if code != http.StatusOK || err != nil { + return &mcp.CallToolResult{ + IsError: true, + Content: []mcp.Content{ + mcp.TextContent{ + Type: "text", + Text: fmt.Sprintf("json.Marshal returned error %v", err), + }, + }, + }, nil + } + result := &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.TextContent{ + Type: "text", + Text: string(content), + }, + }, + } + + return result, nil +} + +func (s *MCPServer) GetFlows(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) { + header, params := parseRequest(ctr) + clients := handler.NewLokiClient(&s.handler.Cfg.Loki, header, false) + flows, code, err := s.handler.QueryFlows(ctx, clients, params) + return parseResults(flows, code, err) +} + +func (s *MCPServer) GetMetrics(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) { + header, params := parseRequest(ctr) + namespace := params.Get("namespace") + clients, err := handler.NewClients(s.handler.Cfg, header, false, namespace) + if err != nil { + return &mcp.CallToolResult{ + IsError: true, + Content: []mcp.Content{ + mcp.TextContent{ + Type: "text", + Text: err.Error(), + }, + }, + }, nil + } + + metrics, code, err := s.handler.QueryMetrics(ctx, clients, params, constants.DataSourceLoki) + return parseResults(metrics, code, err) +} diff --git a/vendor/github.com/mark3labs/mcp-go/LICENSE b/vendor/github.com/mark3labs/mcp-go/LICENSE new file mode 100644 index 000000000..3d4843545 --- /dev/null +++ b/vendor/github.com/mark3labs/mcp-go/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Anthropic, PBC + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/mark3labs/mcp-go/mcp/prompts.go b/vendor/github.com/mark3labs/mcp-go/mcp/prompts.go new file mode 100644 index 000000000..bc12a7297 --- /dev/null +++ b/vendor/github.com/mark3labs/mcp-go/mcp/prompts.go @@ -0,0 +1,163 @@ +package mcp + +/* Prompts */ + +// ListPromptsRequest is sent from the client to request a list of prompts and +// prompt templates the server has. +type ListPromptsRequest struct { + PaginatedRequest +} + +// ListPromptsResult is the server's response to a prompts/list request from +// the client. +type ListPromptsResult struct { + PaginatedResult + Prompts []Prompt `json:"prompts"` +} + +// GetPromptRequest is used by the client to get a prompt provided by the +// server. +type GetPromptRequest struct { + Request + Params struct { + // The name of the prompt or prompt template. + Name string `json:"name"` + // Arguments to use for templating the prompt. + Arguments map[string]string `json:"arguments,omitempty"` + } `json:"params"` +} + +// GetPromptResult is the server's response to a prompts/get request from the +// client. +type GetPromptResult struct { + Result + // An optional description for the prompt. + Description string `json:"description,omitempty"` + Messages []PromptMessage `json:"messages"` +} + +// Prompt represents a prompt or prompt template that the server offers. +// If Arguments is non-nil and non-empty, this indicates the prompt is a template +// that requires argument values to be provided when calling prompts/get. +// If Arguments is nil or empty, this is a static prompt that takes no arguments. +type Prompt struct { + // The name of the prompt or prompt template. + Name string `json:"name"` + // An optional description of what this prompt provides + Description string `json:"description,omitempty"` + // A list of arguments to use for templating the prompt. + // The presence of arguments indicates this is a template prompt. + Arguments []PromptArgument `json:"arguments,omitempty"` +} + +// PromptArgument describes an argument that a prompt template can accept. +// When a prompt includes arguments, clients must provide values for all +// required arguments when making a prompts/get request. +type PromptArgument struct { + // The name of the argument. + Name string `json:"name"` + // A human-readable description of the argument. + Description string `json:"description,omitempty"` + // Whether this argument must be provided. + // If true, clients must include this argument when calling prompts/get. + Required bool `json:"required,omitempty"` +} + +// Role represents the sender or recipient of messages and data in a +// conversation. +type Role string + +const ( + RoleUser Role = "user" + RoleAssistant Role = "assistant" +) + +// PromptMessage describes a message returned as part of a prompt. +// +// This is similar to `SamplingMessage`, but also supports the embedding of +// resources from the MCP server. +type PromptMessage struct { + Role Role `json:"role"` + Content Content `json:"content"` // Can be TextContent, ImageContent, or EmbeddedResource +} + +// PromptListChangedNotification is an optional notification from the server +// to the client, informing it that the list of prompts it offers has changed. This +// may be issued by servers without any previous subscription from the client. +type PromptListChangedNotification struct { + Notification +} + +// PromptOption is a function that configures a Prompt. +// It provides a flexible way to set various properties of a Prompt using the functional options pattern. +type PromptOption func(*Prompt) + +// ArgumentOption is a function that configures a PromptArgument. +// It allows for flexible configuration of prompt arguments using the functional options pattern. +type ArgumentOption func(*PromptArgument) + +// +// Core Prompt Functions +// + +// NewPrompt creates a new Prompt with the given name and options. +// The prompt will be configured based on the provided options. +// Options are applied in order, allowing for flexible prompt configuration. +func NewPrompt(name string, opts ...PromptOption) Prompt { + prompt := Prompt{ + Name: name, + } + + for _, opt := range opts { + opt(&prompt) + } + + return prompt +} + +// WithPromptDescription adds a description to the Prompt. +// The description should provide a clear, human-readable explanation of what the prompt does. +func WithPromptDescription(description string) PromptOption { + return func(p *Prompt) { + p.Description = description + } +} + +// WithArgument adds an argument to the prompt's argument list. +// The argument will be configured based on the provided options. +func WithArgument(name string, opts ...ArgumentOption) PromptOption { + return func(p *Prompt) { + arg := PromptArgument{ + Name: name, + } + + for _, opt := range opts { + opt(&arg) + } + + if p.Arguments == nil { + p.Arguments = make([]PromptArgument, 0) + } + p.Arguments = append(p.Arguments, arg) + } +} + +// +// Argument Options +// + +// ArgumentDescription adds a description to a prompt argument. +// The description should explain the purpose and expected values of the argument. +func ArgumentDescription(desc string) ArgumentOption { + return func(arg *PromptArgument) { + arg.Description = desc + } +} + +// RequiredArgument marks an argument as required in the prompt. +// Required arguments must be provided when getting the prompt. +func RequiredArgument() ArgumentOption { + return func(arg *PromptArgument) { + arg.Required = true + } +} diff --git a/vendor/github.com/mark3labs/mcp-go/mcp/resources.go b/vendor/github.com/mark3labs/mcp-go/mcp/resources.go new file mode 100644 index 000000000..51cdd25dd --- /dev/null +++ b/vendor/github.com/mark3labs/mcp-go/mcp/resources.go @@ -0,0 +1,105 @@ +package mcp + +import "github.com/yosida95/uritemplate/v3" + +// ResourceOption is a function that configures a Resource. +// It provides a flexible way to set various properties of a Resource using the functional options pattern. +type ResourceOption func(*Resource) + +// NewResource creates a new Resource with the given URI, name and options. +// The resource will be configured based on the provided options. +// Options are applied in order, allowing for flexible resource configuration. +func NewResource(uri string, name string, opts ...ResourceOption) Resource { + resource := Resource{ + URI: uri, + Name: name, + } + + for _, opt := range opts { + opt(&resource) + } + + return resource +} + +// WithResourceDescription adds a description to the Resource. +// The description should provide a clear, human-readable explanation of what the resource represents. +func WithResourceDescription(description string) ResourceOption { + return func(r *Resource) { + r.Description = description + } +} + +// WithMIMEType sets the MIME type for the Resource. +// This should indicate the format of the resource's contents. +func WithMIMEType(mimeType string) ResourceOption { + return func(r *Resource) { + r.MIMEType = mimeType + } +} + +// WithAnnotations adds annotations to the Resource. +// Annotations can provide additional metadata about the resource's intended use. +func WithAnnotations(audience []Role, priority float64) ResourceOption { + return func(r *Resource) { + if r.Annotations == nil { + r.Annotations = &struct { + Audience []Role `json:"audience,omitempty"` + Priority float64 `json:"priority,omitempty"` + }{} + } + r.Annotations.Audience = audience + r.Annotations.Priority = priority + } +} + +// ResourceTemplateOption is a function that configures a ResourceTemplate. +// It provides a flexible way to set various properties of a ResourceTemplate using the functional options pattern. +type ResourceTemplateOption func(*ResourceTemplate) + +// NewResourceTemplate creates a new ResourceTemplate with the given URI template, name and options. +// The template will be configured based on the provided options. +// Options are applied in order, allowing for flexible template configuration. +func NewResourceTemplate(uriTemplate string, name string, opts ...ResourceTemplateOption) ResourceTemplate { + template := ResourceTemplate{ + URITemplate: &URITemplate{Template: uritemplate.MustNew(uriTemplate)}, + Name: name, + } + + for _, opt := range opts { + opt(&template) + } + + return template +} + +// WithTemplateDescription adds a description to the ResourceTemplate. +// The description should provide a clear, human-readable explanation of what resources this template represents. +func WithTemplateDescription(description string) ResourceTemplateOption { + return func(t *ResourceTemplate) { + t.Description = description + } +} + +// WithTemplateMIMEType sets the MIME type for the ResourceTemplate. +// This should only be set if all resources matching this template will have the same type. +func WithTemplateMIMEType(mimeType string) ResourceTemplateOption { + return func(t *ResourceTemplate) { + t.MIMEType = mimeType + } +} + +// WithTemplateAnnotations adds annotations to the ResourceTemplate. +// Annotations can provide additional metadata about the template's intended use. +func WithTemplateAnnotations(audience []Role, priority float64) ResourceTemplateOption { + return func(t *ResourceTemplate) { + if t.Annotations == nil { + t.Annotations = &struct { + Audience []Role `json:"audience,omitempty"` + Priority float64 `json:"priority,omitempty"` + }{} + } + t.Annotations.Audience = audience + t.Annotations.Priority = priority + } +} diff --git a/vendor/github.com/mark3labs/mcp-go/mcp/tools.go b/vendor/github.com/mark3labs/mcp-go/mcp/tools.go new file mode 100644 index 000000000..ca93985cc --- /dev/null +++ b/vendor/github.com/mark3labs/mcp-go/mcp/tools.go @@ -0,0 +1,486 @@ +package mcp + +import ( + "encoding/json" + "errors" + "fmt" +) + +var errToolSchemaConflict = errors.New("provide either InputSchema or RawInputSchema, not both") + +// ListToolsRequest is sent from the client to request a list of tools the +// server has. +type ListToolsRequest struct { + PaginatedRequest +} + +// ListToolsResult is the server's response to a tools/list request from the +// client. +type ListToolsResult struct { + PaginatedResult + Tools []Tool `json:"tools"` +} + +// CallToolResult is the server's response to a tool call. +// +// Any errors that originate from the tool SHOULD be reported inside the result +// object, with `isError` set to true, _not_ as an MCP protocol-level error +// response. Otherwise, the LLM would not be able to see that an error occurred +// and self-correct. +// +// However, any errors in _finding_ the tool, an error indicating that the +// server does not support tool calls, or any other exceptional conditions, +// should be reported as an MCP error response. +type CallToolResult struct { + Result + Content []Content `json:"content"` // Can be TextContent, ImageContent, or EmbeddedResource + // Whether the tool call ended in an error. + // + // If not set, this is assumed to be false (the call was successful). + IsError bool `json:"isError,omitempty"` +} + +// CallToolRequest is used by the client to invoke a tool provided by the server. +type CallToolRequest struct { + Request + Params struct { + Name string `json:"name"` + Arguments map[string]interface{} `json:"arguments,omitempty"` + Meta *struct { + // If specified, the caller is requesting out-of-band progress + // notifications for this request (as represented by + // notifications/progress). The value of this parameter is an + // opaque token that will be attached to any subsequent + // notifications. The receiver is not obligated to provide these + // notifications. + ProgressToken ProgressToken `json:"progressToken,omitempty"` + } `json:"_meta,omitempty"` + } `json:"params"` +} + +// ToolListChangedNotification is an optional notification from the server to +// the client, informing it that the list of tools it offers has changed. This may +// be issued by servers without any previous subscription from the client. +type ToolListChangedNotification struct { + Notification +} + +// Tool represents the definition for a tool the client can call. +type Tool struct { + // The name of the tool. + Name string `json:"name"` + // A human-readable description of the tool. + Description string `json:"description,omitempty"` + // A JSON Schema object defining the expected parameters for the tool. + InputSchema ToolInputSchema `json:"inputSchema"` + // Alternative to InputSchema - allows arbitrary JSON Schema to be provided + RawInputSchema json.RawMessage `json:"-"` // Hide this from JSON marshaling +} + +// MarshalJSON implements the json.Marshaler interface for Tool. +// It handles marshaling either InputSchema or RawInputSchema based on which is set. +func (t Tool) MarshalJSON() ([]byte, error) { + // Create a map to build the JSON structure + m := make(map[string]interface{}, 3) + + // Add the name and description + m["name"] = t.Name + if t.Description != "" { + m["description"] = t.Description + } + + // Determine which schema to use + if t.RawInputSchema != nil { + if t.InputSchema.Type != "" { + return nil, fmt.Errorf("tool %s has both InputSchema and RawInputSchema set: %w", t.Name, errToolSchemaConflict) + } + m["inputSchema"] = t.RawInputSchema + } else { + // Use the structured InputSchema + m["inputSchema"] = t.InputSchema + } + + return json.Marshal(m) +} + +type ToolInputSchema struct { + Type string `json:"type"` + Properties map[string]interface{} `json:"properties,omitempty"` + Required []string `json:"required,omitempty"` +} + +// ToolOption is a function that configures a Tool. +// It provides a flexible way to set various properties of a Tool using the functional options pattern. +type ToolOption func(*Tool) + +// PropertyOption is a function that configures a property in a Tool's input schema. +// It allows for flexible configuration of JSON Schema properties using the functional options pattern. +type PropertyOption func(map[string]interface{}) + +// +// Core Tool Functions +// + +// NewTool creates a new Tool with the given name and options. +// The tool will have an object-type input schema with configurable properties. +// Options are applied in order, allowing for flexible tool configuration. +func NewTool(name string, opts ...ToolOption) Tool { + tool := Tool{ + Name: name, + InputSchema: ToolInputSchema{ + Type: "object", + Properties: make(map[string]interface{}), + Required: nil, // Will be omitted from JSON if empty + }, + } + + for _, opt := range opts { + opt(&tool) + } + + return tool +} + +// NewToolWithRawSchema creates a new Tool with the given name and a raw JSON +// Schema. This allows for arbitrary JSON Schema to be used for the tool's input +// schema. +// +// NOTE a [Tool] built in such a way is incompatible with the [ToolOption] and +// runtime errors will result from supplying a [ToolOption] to a [Tool] built +// with this function. +func NewToolWithRawSchema(name, description string, schema json.RawMessage) Tool { + tool := Tool{ + Name: name, + Description: description, + RawInputSchema: schema, + } + + return tool +} + +// WithDescription adds a description to the Tool. +// The description should provide a clear, human-readable explanation of what the tool does. +func WithDescription(description string) ToolOption { + return func(t *Tool) { + t.Description = description + } +} + +// +// Common Property Options +// + +// Description adds a description to a property in the JSON Schema. +// The description should explain the purpose and expected values of the property. +func Description(desc string) PropertyOption { + return func(schema map[string]interface{}) { + schema["description"] = desc + } +} + +// Required marks a property as required in the tool's input schema. +// Required properties must be provided when using the tool. +func Required() PropertyOption { + return func(schema map[string]interface{}) { + schema["required"] = true + } +} + +// Title adds a display-friendly title to a property in the JSON Schema. +// This title can be used by UI components to show a more readable property name. +func Title(title string) PropertyOption { + return func(schema map[string]interface{}) { + schema["title"] = title + } +} + +// +// String Property Options +// + +// DefaultString sets the default value for a string property. +// This value will be used if the property is not explicitly provided. +func DefaultString(value string) PropertyOption { + return func(schema map[string]interface{}) { + schema["default"] = value + } +} + +// Enum specifies a list of allowed values for a string property. +// The property value must be one of the specified enum values. +func Enum(values ...string) PropertyOption { + return func(schema map[string]interface{}) { + schema["enum"] = values + } +} + +// MaxLength sets the maximum length for a string property. +// The string value must not exceed this length. +func MaxLength(max int) PropertyOption { + return func(schema map[string]interface{}) { + schema["maxLength"] = max + } +} + +// MinLength sets the minimum length for a string property. +// The string value must be at least this length. +func MinLength(min int) PropertyOption { + return func(schema map[string]interface{}) { + schema["minLength"] = min + } +} + +// Pattern sets a regex pattern that a string property must match. +// The string value must conform to the specified regular expression. +func Pattern(pattern string) PropertyOption { + return func(schema map[string]interface{}) { + schema["pattern"] = pattern + } +} + +// +// Number Property Options +// + +// DefaultNumber sets the default value for a number property. +// This value will be used if the property is not explicitly provided. +func DefaultNumber(value float64) PropertyOption { + return func(schema map[string]interface{}) { + schema["default"] = value + } +} + +// Max sets the maximum value for a number property. +// The number value must not exceed this maximum. +func Max(max float64) PropertyOption { + return func(schema map[string]interface{}) { + schema["maximum"] = max + } +} + +// Min sets the minimum value for a number property. +// The number value must not be less than this minimum. +func Min(min float64) PropertyOption { + return func(schema map[string]interface{}) { + schema["minimum"] = min + } +} + +// MultipleOf specifies that a number must be a multiple of the given value. +// The number value must be divisible by this value. +func MultipleOf(value float64) PropertyOption { + return func(schema map[string]interface{}) { + schema["multipleOf"] = value + } +} + +// +// Boolean Property Options +// + +// DefaultBool sets the default value for a boolean property. +// This value will be used if the property is not explicitly provided. +func DefaultBool(value bool) PropertyOption { + return func(schema map[string]interface{}) { + schema["default"] = value + } +} + +// +// Property Type Helpers +// + +// WithBoolean adds a boolean property to the tool schema. +// It accepts property options to configure the boolean property's behavior and constraints. +func WithBoolean(name string, opts ...PropertyOption) ToolOption { + return func(t *Tool) { + schema := map[string]interface{}{ + "type": "boolean", + } + + for _, opt := range opts { + opt(schema) + } + + // Remove required from property schema and add to InputSchema.required + if required, ok := schema["required"].(bool); ok && required { + delete(schema, "required") + if t.InputSchema.Required == nil { + t.InputSchema.Required = []string{name} + } else { + t.InputSchema.Required = append(t.InputSchema.Required, name) + } + } + + t.InputSchema.Properties[name] = schema + } +} + +// WithNumber adds a number property to the tool schema. +// It accepts property options to configure the number property's behavior and constraints. +func WithNumber(name string, opts ...PropertyOption) ToolOption { + return func(t *Tool) { + schema := map[string]interface{}{ + "type": "number", + } + + for _, opt := range opts { + opt(schema) + } + + // Remove required from property schema and add to InputSchema.required + if required, ok := schema["required"].(bool); ok && required { + delete(schema, "required") + if t.InputSchema.Required == nil { + t.InputSchema.Required = []string{name} + } else { + t.InputSchema.Required = append(t.InputSchema.Required, name) + } + } + + t.InputSchema.Properties[name] = schema + } +} + +// WithString adds a string property to the tool schema. +// It accepts property options to configure the string property's behavior and constraints. +func WithString(name string, opts ...PropertyOption) ToolOption { + return func(t *Tool) { + schema := map[string]interface{}{ + "type": "string", + } + + for _, opt := range opts { + opt(schema) + } + + // Remove required from property schema and add to InputSchema.required + if required, ok := schema["required"].(bool); ok && required { + delete(schema, "required") + if t.InputSchema.Required == nil { + t.InputSchema.Required = []string{name} + } else { + t.InputSchema.Required = append(t.InputSchema.Required, name) + } + } + + t.InputSchema.Properties[name] = schema + } +} + +// WithObject adds an object property to the tool schema. +// It accepts property options to configure the object property's behavior and constraints. +func WithObject(name string, opts ...PropertyOption) ToolOption { + return func(t *Tool) { + schema := map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{}, + } + + for _, opt := range opts { + opt(schema) + } + + // Remove required from property schema and add to InputSchema.required + if required, ok := schema["required"].(bool); ok && required { + delete(schema, "required") + if t.InputSchema.Required == nil { + t.InputSchema.Required = []string{name} + } else { + t.InputSchema.Required = append(t.InputSchema.Required, name) + } + } + + t.InputSchema.Properties[name] = schema + } +} + +// WithArray adds an array property to the tool schema. +// It accepts property options to configure the array property's behavior and constraints. +func WithArray(name string, opts ...PropertyOption) ToolOption { + return func(t *Tool) { + schema := map[string]interface{}{ + "type": "array", + } + + for _, opt := range opts { + opt(schema) + } + + // Remove required from property schema and add to InputSchema.required + if required, ok := schema["required"].(bool); ok && required { + delete(schema, "required") + if t.InputSchema.Required == nil { + t.InputSchema.Required = []string{name} + } else { + t.InputSchema.Required = append(t.InputSchema.Required, name) + } + } + + t.InputSchema.Properties[name] = schema + } +} + +// Properties defines the properties for an object schema +func Properties(props map[string]interface{}) PropertyOption { + return func(schema map[string]interface{}) { + schema["properties"] = props + } +} + +// AdditionalProperties specifies whether additional properties are allowed in the object +// or defines a schema for additional properties +func AdditionalProperties(schema interface{}) PropertyOption { + return func(schemaMap map[string]interface{}) { + schemaMap["additionalProperties"] = schema + } +} + +// MinProperties sets the minimum number of properties for an object +func MinProperties(min int) PropertyOption { + return func(schema map[string]interface{}) { + schema["minProperties"] = min + } +} + +// MaxProperties sets the maximum number of properties for an object +func MaxProperties(max int) PropertyOption { + return func(schema map[string]interface{}) { + schema["maxProperties"] = max + } +} + +// PropertyNames defines a schema for property names in an object +func PropertyNames(schema map[string]interface{}) PropertyOption { + return func(schemaMap map[string]interface{}) { + schemaMap["propertyNames"] = schema + } +} + +// Items defines the schema for array items +func Items(schema interface{}) PropertyOption { + return func(schemaMap map[string]interface{}) { + schemaMap["items"] = schema + } +} + +// MinItems sets the minimum number of items for an array +func MinItems(min int) PropertyOption { + return func(schema map[string]interface{}) { + schema["minItems"] = min + } +} + +// MaxItems sets the maximum number of items for an array +func MaxItems(max int) PropertyOption { + return func(schema map[string]interface{}) { + schema["maxItems"] = max + } +} + +// UniqueItems specifies whether array items must be unique +func UniqueItems(unique bool) PropertyOption { + return func(schema map[string]interface{}) { + schema["uniqueItems"] = unique + } +} diff --git a/vendor/github.com/mark3labs/mcp-go/mcp/types.go b/vendor/github.com/mark3labs/mcp-go/mcp/types.go new file mode 100644 index 000000000..a3ad8174e --- /dev/null +++ b/vendor/github.com/mark3labs/mcp-go/mcp/types.go @@ -0,0 +1,860 @@ +// Package mcp defines the core types and interfaces for the Model Control Protocol (MCP). +// MCP is a protocol for communication between LLM-powered applications and their supporting services. +package mcp + +import ( + "encoding/json" + + "github.com/yosida95/uritemplate/v3" +) + +type MCPMethod string + +const ( + // Initiates connection and negotiates protocol capabilities. + // https://spec.modelcontextprotocol.io/specification/2024-11-05/basic/lifecycle/#initialization + MethodInitialize MCPMethod = "initialize" + + // Verifies connection liveness between client and server. + // https://spec.modelcontextprotocol.io/specification/2024-11-05/basic/utilities/ping/ + MethodPing MCPMethod = "ping" + + // Lists all available server resources. + // https://spec.modelcontextprotocol.io/specification/2024-11-05/server/resources/ + MethodResourcesList MCPMethod = "resources/list" + + // Provides URI templates for constructing resource URIs. + // https://spec.modelcontextprotocol.io/specification/2024-11-05/server/resources/ + MethodResourcesTemplatesList MCPMethod = "resources/templates/list" + + // Retrieves content of a specific resource by URI. + // https://spec.modelcontextprotocol.io/specification/2024-11-05/server/resources/ + MethodResourcesRead MCPMethod = "resources/read" + + // Lists all available prompt templates. + // https://spec.modelcontextprotocol.io/specification/2024-11-05/server/prompts/ + MethodPromptsList MCPMethod = "prompts/list" + + // Retrieves a specific prompt template with filled parameters. + // https://spec.modelcontextprotocol.io/specification/2024-11-05/server/prompts/ + MethodPromptsGet MCPMethod = "prompts/get" + + // Lists all available executable tools. + // https://spec.modelcontextprotocol.io/specification/2024-11-05/server/tools/ + MethodToolsList MCPMethod = "tools/list" + + // Invokes a specific tool with provided parameters. + // https://spec.modelcontextprotocol.io/specification/2024-11-05/server/tools/ + MethodToolsCall MCPMethod = "tools/call" +) + +type URITemplate struct { + *uritemplate.Template +} + +func (t *URITemplate) MarshalJSON() ([]byte, error) { + return json.Marshal(t.Template.Raw()) +} + +func (t *URITemplate) UnmarshalJSON(data []byte) error { + var raw string + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + template, err := uritemplate.New(raw) + if err != nil { + return err + } + t.Template = template + return nil +} + +/* JSON-RPC types */ + +// JSONRPCMessage represents either a JSONRPCRequest, JSONRPCNotification, JSONRPCResponse, or JSONRPCError +type JSONRPCMessage interface{} + +// LATEST_PROTOCOL_VERSION is the most recent version of the MCP protocol. +const LATEST_PROTOCOL_VERSION = "2024-11-05" + +// JSONRPC_VERSION is the version of JSON-RPC used by MCP. +const JSONRPC_VERSION = "2.0" + +// ProgressToken is used to associate progress notifications with the original request. +type ProgressToken interface{} + +// Cursor is an opaque token used to represent a cursor for pagination. +type Cursor string + +type Request struct { + Method string `json:"method"` + Params struct { + Meta *struct { + // If specified, the caller is requesting out-of-band progress + // notifications for this request (as represented by + // notifications/progress). The value of this parameter is an + // opaque token that will be attached to any subsequent + // notifications. The receiver is not obligated to provide these + // notifications. + ProgressToken ProgressToken `json:"progressToken,omitempty"` + } `json:"_meta,omitempty"` + } `json:"params,omitempty"` +} + +type Params map[string]interface{} + +type Notification struct { + Method string `json:"method"` + Params NotificationParams `json:"params,omitempty"` +} + +type NotificationParams struct { + // This parameter name is reserved by MCP to allow clients and + // servers to attach additional metadata to their notifications. + Meta map[string]interface{} `json:"_meta,omitempty"` + + // Additional fields can be added to this map + AdditionalFields map[string]interface{} `json:"-"` +} + +// MarshalJSON implements custom JSON marshaling +func (p NotificationParams) MarshalJSON() ([]byte, error) { + // Create a map to hold all fields + m := make(map[string]interface{}) + + // Add Meta if it exists + if p.Meta != nil { + m["_meta"] = p.Meta + } + + // Add all additional fields + for k, v := range p.AdditionalFields { + // Ensure we don't override the _meta field + if k != "_meta" { + m[k] = v + } + } + + return json.Marshal(m) +} + +// UnmarshalJSON implements custom JSON unmarshaling +func (p *NotificationParams) UnmarshalJSON(data []byte) error { + // Create a map to hold all fields + var m map[string]interface{} + if err := json.Unmarshal(data, &m); err != nil { + return err + } + + // Initialize maps if they're nil + if p.Meta == nil { + p.Meta = make(map[string]interface{}) + } + if p.AdditionalFields == nil { + p.AdditionalFields = make(map[string]interface{}) + } + + // Process all fields + for k, v := range m { + if k == "_meta" { + // Handle Meta field + if meta, ok := v.(map[string]interface{}); ok { + p.Meta = meta + } + } else { + // Handle additional fields + p.AdditionalFields[k] = v + } + } + + return nil +} + +type Result struct { + // This result property is reserved by the protocol to allow clients and + // servers to attach additional metadata to their responses. + Meta map[string]interface{} `json:"_meta,omitempty"` +} + +// RequestId is a uniquely identifying ID for a request in JSON-RPC. +// It can be any JSON-serializable value, typically a number or string. +type RequestId interface{} + +// JSONRPCRequest represents a request that expects a response. +type JSONRPCRequest struct { + JSONRPC string `json:"jsonrpc"` + ID RequestId `json:"id"` + Params interface{} `json:"params,omitempty"` + Request +} + +// JSONRPCNotification represents a notification which does not expect a response. +type JSONRPCNotification struct { + JSONRPC string `json:"jsonrpc"` + Notification +} + +// JSONRPCResponse represents a successful (non-error) response to a request. +type JSONRPCResponse struct { + JSONRPC string `json:"jsonrpc"` + ID RequestId `json:"id"` + Result interface{} `json:"result"` +} + +// JSONRPCError represents a non-successful (error) response to a request. +type JSONRPCError struct { + JSONRPC string `json:"jsonrpc"` + ID RequestId `json:"id"` + Error struct { + // The error type that occurred. + Code int `json:"code"` + // A short description of the error. The message SHOULD be limited + // to a concise single sentence. + Message string `json:"message"` + // Additional information about the error. The value of this member + // is defined by the sender (e.g. detailed error information, nested errors etc.). + Data interface{} `json:"data,omitempty"` + } `json:"error"` +} + +// Standard JSON-RPC error codes +const ( + PARSE_ERROR = -32700 + INVALID_REQUEST = -32600 + METHOD_NOT_FOUND = -32601 + INVALID_PARAMS = -32602 + INTERNAL_ERROR = -32603 +) + +/* Empty result */ + +// EmptyResult represents a response that indicates success but carries no data. +type EmptyResult Result + +/* Cancellation */ + +// CancelledNotification can be sent by either side to indicate that it is +// cancelling a previously-issued request. +// +// The request SHOULD still be in-flight, but due to communication latency, it +// is always possible that this notification MAY arrive after the request has +// already finished. +// +// This notification indicates that the result will be unused, so any +// associated processing SHOULD cease. +// +// A client MUST NOT attempt to cancel its `initialize` request. +type CancelledNotification struct { + Notification + Params struct { + // The ID of the request to cancel. + // + // This MUST correspond to the ID of a request previously issued + // in the same direction. + RequestId RequestId `json:"requestId"` + + // An optional string describing the reason for the cancellation. This MAY + // be logged or presented to the user. + Reason string `json:"reason,omitempty"` + } `json:"params"` +} + +/* Initialization */ + +// InitializeRequest is sent from the client to the server when it first +// connects, asking it to begin initialization. +type InitializeRequest struct { + Request + Params struct { + // The latest version of the Model Context Protocol that the client supports. + // The client MAY decide to support older versions as well. + ProtocolVersion string `json:"protocolVersion"` + Capabilities ClientCapabilities `json:"capabilities"` + ClientInfo Implementation `json:"clientInfo"` + } `json:"params"` +} + +// InitializeResult is sent after receiving an initialize request from the +// client. +type InitializeResult struct { + Result + // The version of the Model Context Protocol that the server wants to use. + // This may not match the version that the client requested. If the client cannot + // support this version, it MUST disconnect. + ProtocolVersion string `json:"protocolVersion"` + Capabilities ServerCapabilities `json:"capabilities"` + ServerInfo Implementation `json:"serverInfo"` + // Instructions describing how to use the server and its features. + // + // This can be used by clients to improve the LLM's understanding of + // available tools, resources, etc. It can be thought of like a "hint" to the model. + // For example, this information MAY be added to the system prompt. + Instructions string `json:"instructions,omitempty"` +} + +// InitializedNotification is sent from the client to the server after +// initialization has finished. +type InitializedNotification struct { + Notification +} + +// ClientCapabilities represents capabilities a client may support. Known +// capabilities are defined here, in this schema, but this is not a closed set: any +// client can define its own, additional capabilities. +type ClientCapabilities struct { + // Experimental, non-standard capabilities that the client supports. + Experimental map[string]interface{} `json:"experimental,omitempty"` + // Present if the client supports listing roots. + Roots *struct { + // Whether the client supports notifications for changes to the roots list. + ListChanged bool `json:"listChanged,omitempty"` + } `json:"roots,omitempty"` + // Present if the client supports sampling from an LLM. + Sampling *struct{} `json:"sampling,omitempty"` +} + +// ServerCapabilities represents capabilities that a server may support. Known +// capabilities are defined here, in this schema, but this is not a closed set: any +// server can define its own, additional capabilities. +type ServerCapabilities struct { + // Experimental, non-standard capabilities that the server supports. + Experimental map[string]interface{} `json:"experimental,omitempty"` + // Present if the server supports sending log messages to the client. + Logging *struct{} `json:"logging,omitempty"` + // Present if the server offers any prompt templates. + Prompts *struct { + // Whether this server supports notifications for changes to the prompt list. + ListChanged bool `json:"listChanged,omitempty"` + } `json:"prompts,omitempty"` + // Present if the server offers any resources to read. + Resources *struct { + // Whether this server supports subscribing to resource updates. + Subscribe bool `json:"subscribe,omitempty"` + // Whether this server supports notifications for changes to the resource + // list. + ListChanged bool `json:"listChanged,omitempty"` + } `json:"resources,omitempty"` + // Present if the server offers any tools to call. + Tools *struct { + // Whether this server supports notifications for changes to the tool list. + ListChanged bool `json:"listChanged,omitempty"` + } `json:"tools,omitempty"` +} + +// Implementation describes the name and version of an MCP implementation. +type Implementation struct { + Name string `json:"name"` + Version string `json:"version"` +} + +/* Ping */ + +// PingRequest represents a ping, issued by either the server or the client, +// to check that the other party is still alive. The receiver must promptly respond, +// or else may be disconnected. +type PingRequest struct { + Request +} + +/* Progress notifications */ + +// ProgressNotification is an out-of-band notification used to inform the +// receiver of a progress update for a long-running request. +type ProgressNotification struct { + Notification + Params struct { + // The progress token which was given in the initial request, used to + // associate this notification with the request that is proceeding. + ProgressToken ProgressToken `json:"progressToken"` + // The progress thus far. This should increase every time progress is made, + // even if the total is unknown. + Progress float64 `json:"progress"` + // Total number of items to process (or total progress required), if known. + Total float64 `json:"total,omitempty"` + } `json:"params"` +} + +/* Pagination */ + +type PaginatedRequest struct { + Request + Params struct { + // An opaque token representing the current pagination position. + // If provided, the server should return results starting after this cursor. + Cursor Cursor `json:"cursor,omitempty"` + } `json:"params,omitempty"` +} + +type PaginatedResult struct { + Result + // An opaque token representing the pagination position after the last + // returned result. + // If present, there may be more results available. + NextCursor Cursor `json:"nextCursor,omitempty"` +} + +/* Resources */ + +// ListResourcesRequest is sent from the client to request a list of resources +// the server has. +type ListResourcesRequest struct { + PaginatedRequest +} + +// ListResourcesResult is the server's response to a resources/list request +// from the client. +type ListResourcesResult struct { + PaginatedResult + Resources []Resource `json:"resources"` +} + +// ListResourceTemplatesRequest is sent from the client to request a list of +// resource templates the server has. +type ListResourceTemplatesRequest struct { + PaginatedRequest +} + +// ListResourceTemplatesResult is the server's response to a +// resources/templates/list request from the client. +type ListResourceTemplatesResult struct { + PaginatedResult + ResourceTemplates []ResourceTemplate `json:"resourceTemplates"` +} + +// ReadResourceRequest is sent from the client to the server, to read a +// specific resource URI. +type ReadResourceRequest struct { + Request + Params struct { + // The URI of the resource to read. The URI can use any protocol; it is up + // to the server how to interpret it. + URI string `json:"uri"` + // Arguments to pass to the resource handler + Arguments map[string]interface{} `json:"arguments,omitempty"` + } `json:"params"` +} + +// ReadResourceResult is the server's response to a resources/read request +// from the client. +type ReadResourceResult struct { + Result + Contents []ResourceContents `json:"contents"` // Can be TextResourceContents or BlobResourceContents +} + +// ResourceListChangedNotification is an optional notification from the server +// to the client, informing it that the list of resources it can read from has +// changed. This may be issued by servers without any previous subscription from +// the client. +type ResourceListChangedNotification struct { + Notification +} + +// SubscribeRequest is sent from the client to request resources/updated +// notifications from the server whenever a particular resource changes. +type SubscribeRequest struct { + Request + Params struct { + // The URI of the resource to subscribe to. The URI can use any protocol; it + // is up to the server how to interpret it. + URI string `json:"uri"` + } `json:"params"` +} + +// UnsubscribeRequest is sent from the client to request cancellation of +// resources/updated notifications from the server. This should follow a previous +// resources/subscribe request. +type UnsubscribeRequest struct { + Request + Params struct { + // The URI of the resource to unsubscribe from. + URI string `json:"uri"` + } `json:"params"` +} + +// ResourceUpdatedNotification is a notification from the server to the client, +// informing it that a resource has changed and may need to be read again. This +// should only be sent if the client previously sent a resources/subscribe request. +type ResourceUpdatedNotification struct { + Notification + Params struct { + // The URI of the resource that has been updated. This might be a sub- + // resource of the one that the client actually subscribed to. + URI string `json:"uri"` + } `json:"params"` +} + +// Resource represents a known resource that the server is capable of reading. +type Resource struct { + Annotated + // The URI of this resource. + URI string `json:"uri"` + // A human-readable name for this resource. + // + // This can be used by clients to populate UI elements. + Name string `json:"name"` + // A description of what this resource represents. + // + // This can be used by clients to improve the LLM's understanding of + // available resources. It can be thought of like a "hint" to the model. + Description string `json:"description,omitempty"` + // The MIME type of this resource, if known. + MIMEType string `json:"mimeType,omitempty"` +} + +// ResourceTemplate represents a template description for resources available +// on the server. +type ResourceTemplate struct { + Annotated + // A URI template (according to RFC 6570) that can be used to construct + // resource URIs. + URITemplate *URITemplate `json:"uriTemplate"` + // A human-readable name for the type of resource this template refers to. + // + // This can be used by clients to populate UI elements. + Name string `json:"name"` + // A description of what this template is for. + // + // This can be used by clients to improve the LLM's understanding of + // available resources. It can be thought of like a "hint" to the model. + Description string `json:"description,omitempty"` + // The MIME type for all resources that match this template. This should only + // be included if all resources matching this template have the same type. + MIMEType string `json:"mimeType,omitempty"` +} + +// ResourceContents represents the contents of a specific resource or sub- +// resource. +type ResourceContents interface { + isResourceContents() +} + +type TextResourceContents struct { + // The URI of this resource. + URI string `json:"uri"` + // The MIME type of this resource, if known. + MIMEType string `json:"mimeType,omitempty"` + // The text of the item. This must only be set if the item can actually be + // represented as text (not binary data). + Text string `json:"text"` +} + +func (TextResourceContents) isResourceContents() {} + +type BlobResourceContents struct { + // The URI of this resource. + URI string `json:"uri"` + // The MIME type of this resource, if known. + MIMEType string `json:"mimeType,omitempty"` + // A base64-encoded string representing the binary data of the item. + Blob string `json:"blob"` +} + +func (BlobResourceContents) isResourceContents() {} + +/* Logging */ + +// SetLevelRequest is a request from the client to the server, to enable or +// adjust logging. +type SetLevelRequest struct { + Request + Params struct { + // The level of logging that the client wants to receive from the server. + // The server should send all logs at this level and higher (i.e., more severe) to + // the client as notifications/logging/message. + Level LoggingLevel `json:"level"` + } `json:"params"` +} + +// LoggingMessageNotification is a notification of a log message passed from +// server to client. If no logging/setLevel request has been sent from the client, +// the server MAY decide which messages to send automatically. +type LoggingMessageNotification struct { + Notification + Params struct { + // The severity of this log message. + Level LoggingLevel `json:"level"` + // An optional name of the logger issuing this message. + Logger string `json:"logger,omitempty"` + // The data to be logged, such as a string message or an object. Any JSON + // serializable type is allowed here. + Data interface{} `json:"data"` + } `json:"params"` +} + +// LoggingLevel represents the severity of a log message. +// +// These map to syslog message severities, as specified in RFC-5424: +// https://datatracker.ietf.org/doc/html/rfc5424#section-6.2.1 +type LoggingLevel string + +const ( + LoggingLevelDebug LoggingLevel = "debug" + LoggingLevelInfo LoggingLevel = "info" + LoggingLevelNotice LoggingLevel = "notice" + LoggingLevelWarning LoggingLevel = "warning" + LoggingLevelError LoggingLevel = "error" + LoggingLevelCritical LoggingLevel = "critical" + LoggingLevelAlert LoggingLevel = "alert" + LoggingLevelEmergency LoggingLevel = "emergency" +) + +/* Sampling */ + +// CreateMessageRequest is a request from the server to sample an LLM via the +// client. The client has full discretion over which model to select. The client +// should also inform the user before beginning sampling, to allow them to inspect +// the request (human in the loop) and decide whether to approve it. +type CreateMessageRequest struct { + Request + Params struct { + Messages []SamplingMessage `json:"messages"` + ModelPreferences *ModelPreferences `json:"modelPreferences,omitempty"` + SystemPrompt string `json:"systemPrompt,omitempty"` + IncludeContext string `json:"includeContext,omitempty"` + Temperature float64 `json:"temperature,omitempty"` + MaxTokens int `json:"maxTokens"` + StopSequences []string `json:"stopSequences,omitempty"` + Metadata interface{} `json:"metadata,omitempty"` + } `json:"params"` +} + +// CreateMessageResult is the client's response to a sampling/create_message +// request from the server. The client should inform the user before returning the +// sampled message, to allow them to inspect the response (human in the loop) and +// decide whether to allow the server to see it. +type CreateMessageResult struct { + Result + SamplingMessage + // The name of the model that generated the message. + Model string `json:"model"` + // The reason why sampling stopped, if known. + StopReason string `json:"stopReason,omitempty"` +} + +// SamplingMessage describes a message issued to or received from an LLM API. +type SamplingMessage struct { + Role Role `json:"role"` + Content interface{} `json:"content"` // Can be TextContent or ImageContent +} + +// Annotated is the base for objects that include optional annotations for the +// client. The client can use annotations to inform how objects are used or +// displayed +type Annotated struct { + Annotations *struct { + // Describes who the intended customer of this object or data is. + // + // It can include multiple entries to indicate content useful for multiple + // audiences (e.g., `["user", "assistant"]`). + Audience []Role `json:"audience,omitempty"` + + // Describes how important this data is for operating the server. + // + // A value of 1 means "most important," and indicates that the data is + // effectively required, while 0 means "least important," and indicates that + // the data is entirely optional. + Priority float64 `json:"priority,omitempty"` + } `json:"annotations,omitempty"` +} + +type Content interface { + isContent() +} + +// TextContent represents text provided to or from an LLM. +// It must have Type set to "text". +type TextContent struct { + Annotated + Type string `json:"type"` // Must be "text" + // The text content of the message. + Text string `json:"text"` +} + +func (TextContent) isContent() {} + +// ImageContent represents an image provided to or from an LLM. +// It must have Type set to "image". +type ImageContent struct { + Annotated + Type string `json:"type"` // Must be "image" + // The base64-encoded image data. + Data string `json:"data"` + // The MIME type of the image. Different providers may support different image types. + MIMEType string `json:"mimeType"` +} + +func (ImageContent) isContent() {} + +// EmbeddedResource represents the contents of a resource, embedded into a prompt or tool call result. +// +// It is up to the client how best to render embedded resources for the +// benefit of the LLM and/or the user. +type EmbeddedResource struct { + Annotated + Type string `json:"type"` + Resource ResourceContents `json:"resource"` +} + +func (EmbeddedResource) isContent() {} + +// ModelPreferences represents the server's preferences for model selection, +// requested of the client during sampling. +// +// Because LLMs can vary along multiple dimensions, choosing the "best" modelis +// rarely straightforward. Different models excel in different areas—some are +// faster but less capable, others are more capable but more expensive, and so +// on. This interface allows servers to express their priorities across multiple +// dimensions to help clients make an appropriate selection for their use case. +// +// These preferences are always advisory. The client MAY ignore them. It is also +// up to the client to decide how to interpret these preferences and how to +// balance them against other considerations. +type ModelPreferences struct { + // Optional hints to use for model selection. + // + // If multiple hints are specified, the client MUST evaluate them in order + // (such that the first match is taken). + // + // The client SHOULD prioritize these hints over the numeric priorities, but + // MAY still use the priorities to select from ambiguous matches. + Hints []ModelHint `json:"hints,omitempty"` + + // How much to prioritize cost when selecting a model. A value of 0 means cost + // is not important, while a value of 1 means cost is the most important + // factor. + CostPriority float64 `json:"costPriority,omitempty"` + + // How much to prioritize sampling speed (latency) when selecting a model. A + // value of 0 means speed is not important, while a value of 1 means speed is + // the most important factor. + SpeedPriority float64 `json:"speedPriority,omitempty"` + + // How much to prioritize intelligence and capabilities when selecting a + // model. A value of 0 means intelligence is not important, while a value of 1 + // means intelligence is the most important factor. + IntelligencePriority float64 `json:"intelligencePriority,omitempty"` +} + +// ModelHint represents hints to use for model selection. +// +// Keys not declared here are currently left unspecified by the spec and are up +// to the client to interpret. +type ModelHint struct { + // A hint for a model name. + // + // The client SHOULD treat this as a substring of a model name; for example: + // - `claude-3-5-sonnet` should match `claude-3-5-sonnet-20241022` + // - `sonnet` should match `claude-3-5-sonnet-20241022`, `claude-3-sonnet-20240229`, etc. + // - `claude` should match any Claude model + // + // The client MAY also map the string to a different provider's model name or + // a different model family, as long as it fills a similar niche; for example: + // - `gemini-1.5-flash` could match `claude-3-haiku-20240307` + Name string `json:"name,omitempty"` +} + +/* Autocomplete */ + +// CompleteRequest is a request from the client to the server, to ask for completion options. +type CompleteRequest struct { + Request + Params struct { + Ref interface{} `json:"ref"` // Can be PromptReference or ResourceReference + Argument struct { + // The name of the argument + Name string `json:"name"` + // The value of the argument to use for completion matching. + Value string `json:"value"` + } `json:"argument"` + } `json:"params"` +} + +// CompleteResult is the server's response to a completion/complete request +type CompleteResult struct { + Result + Completion struct { + // An array of completion values. Must not exceed 100 items. + Values []string `json:"values"` + // The total number of completion options available. This can exceed the + // number of values actually sent in the response. + Total int `json:"total,omitempty"` + // Indicates whether there are additional completion options beyond those + // provided in the current response, even if the exact total is unknown. + HasMore bool `json:"hasMore,omitempty"` + } `json:"completion"` +} + +// ResourceReference is a reference to a resource or resource template definition. +type ResourceReference struct { + Type string `json:"type"` + // The URI or URI template of the resource. + URI string `json:"uri"` +} + +// PromptReference identifies a prompt. +type PromptReference struct { + Type string `json:"type"` + // The name of the prompt or prompt template + Name string `json:"name"` +} + +/* Roots */ + +// ListRootsRequest is sent from the server to request a list of root URIs from the client. Roots allow +// servers to ask for specific directories or files to operate on. A common example +// for roots is providing a set of repositories or directories a server should operate +// on. +// +// This request is typically used when the server needs to understand the file system +// structure or access specific locations that the client has permission to read from. +type ListRootsRequest struct { + Request +} + +// ListRootsResult is the client's response to a roots/list request from the server. +// This result contains an array of Root objects, each representing a root directory +// or file that the server can operate on. +type ListRootsResult struct { + Result + Roots []Root `json:"roots"` +} + +// Root represents a root directory or file that the server can operate on. +type Root struct { + // The URI identifying the root. This *must* start with file:// for now. + // This restriction may be relaxed in future versions of the protocol to allow + // other URI schemes. + URI string `json:"uri"` + // An optional name for the root. This can be used to provide a human-readable + // identifier for the root, which may be useful for display purposes or for + // referencing the root in other parts of the application. + Name string `json:"name,omitempty"` +} + +// RootsListChangedNotification is a notification from the client to the +// server, informing it that the list of roots has changed. +// This notification should be sent whenever the client adds, removes, or modifies any root. +// The server should then request an updated list of roots using the ListRootsRequest. +type RootsListChangedNotification struct { + Notification +} + +/* Client messages */ +// ClientRequest represents any request that can be sent from client to server. +type ClientRequest interface{} + +// ClientNotification represents any notification that can be sent from client to server. +type ClientNotification interface{} + +// ClientResult represents any result that can be sent from client to server. +type ClientResult interface{} + +/* Server messages */ +// ServerRequest represents any request that can be sent from server to client. +type ServerRequest interface{} + +// ServerNotification represents any notification that can be sent from server to client. +type ServerNotification interface{} + +// ServerResult represents any result that can be sent from server to client. +type ServerResult interface{} diff --git a/vendor/github.com/mark3labs/mcp-go/mcp/utils.go b/vendor/github.com/mark3labs/mcp-go/mcp/utils.go new file mode 100644 index 000000000..ddaca1695 --- /dev/null +++ b/vendor/github.com/mark3labs/mcp-go/mcp/utils.go @@ -0,0 +1,582 @@ +package mcp + +import ( + "encoding/json" + "fmt" +) + +// ClientRequest types +var _ ClientRequest = &PingRequest{} +var _ ClientRequest = &InitializeRequest{} +var _ ClientRequest = &CompleteRequest{} +var _ ClientRequest = &SetLevelRequest{} +var _ ClientRequest = &GetPromptRequest{} +var _ ClientRequest = &ListPromptsRequest{} +var _ ClientRequest = &ListResourcesRequest{} +var _ ClientRequest = &ReadResourceRequest{} +var _ ClientRequest = &SubscribeRequest{} +var _ ClientRequest = &UnsubscribeRequest{} +var _ ClientRequest = &CallToolRequest{} +var _ ClientRequest = &ListToolsRequest{} + +// ClientNotification types +var _ ClientNotification = &CancelledNotification{} +var _ ClientNotification = &ProgressNotification{} +var _ ClientNotification = &InitializedNotification{} +var _ ClientNotification = &RootsListChangedNotification{} + +// ClientResult types +var _ ClientResult = &EmptyResult{} +var _ ClientResult = &CreateMessageResult{} +var _ ClientResult = &ListRootsResult{} + +// ServerRequest types +var _ ServerRequest = &PingRequest{} +var _ ServerRequest = &CreateMessageRequest{} +var _ ServerRequest = &ListRootsRequest{} + +// ServerNotification types +var _ ServerNotification = &CancelledNotification{} +var _ ServerNotification = &ProgressNotification{} +var _ ServerNotification = &LoggingMessageNotification{} +var _ ServerNotification = &ResourceUpdatedNotification{} +var _ ServerNotification = &ResourceListChangedNotification{} +var _ ServerNotification = &ToolListChangedNotification{} +var _ ServerNotification = &PromptListChangedNotification{} + +// ServerResult types +var _ ServerResult = &EmptyResult{} +var _ ServerResult = &InitializeResult{} +var _ ServerResult = &CompleteResult{} +var _ ServerResult = &GetPromptResult{} +var _ ServerResult = &ListPromptsResult{} +var _ ServerResult = &ListResourcesResult{} +var _ ServerResult = &ReadResourceResult{} +var _ ServerResult = &CallToolResult{} +var _ ServerResult = &ListToolsResult{} + +// Helper functions for type assertions + +// asType attempts to cast the given interface to the given type +func asType[T any](content interface{}) (*T, bool) { + tc, ok := content.(T) + if !ok { + return nil, false + } + return &tc, true +} + +// AsTextContent attempts to cast the given interface to TextContent +func AsTextContent(content interface{}) (*TextContent, bool) { + return asType[TextContent](content) +} + +// AsImageContent attempts to cast the given interface to ImageContent +func AsImageContent(content interface{}) (*ImageContent, bool) { + return asType[ImageContent](content) +} + +// AsEmbeddedResource attempts to cast the given interface to EmbeddedResource +func AsEmbeddedResource(content interface{}) (*EmbeddedResource, bool) { + return asType[EmbeddedResource](content) +} + +// AsTextResourceContents attempts to cast the given interface to TextResourceContents +func AsTextResourceContents(content interface{}) (*TextResourceContents, bool) { + return asType[TextResourceContents](content) +} + +// AsBlobResourceContents attempts to cast the given interface to BlobResourceContents +func AsBlobResourceContents(content interface{}) (*BlobResourceContents, bool) { + return asType[BlobResourceContents](content) +} + +// Helper function for JSON-RPC + +// NewJSONRPCResponse creates a new JSONRPCResponse with the given id and result +func NewJSONRPCResponse(id RequestId, result Result) JSONRPCResponse { + return JSONRPCResponse{ + JSONRPC: JSONRPC_VERSION, + ID: id, + Result: result, + } +} + +// NewJSONRPCError creates a new JSONRPCResponse with the given id, code, and message +func NewJSONRPCError( + id RequestId, + code int, + message string, + data interface{}, +) JSONRPCError { + return JSONRPCError{ + JSONRPC: JSONRPC_VERSION, + ID: id, + Error: struct { + Code int `json:"code"` + Message string `json:"message"` + Data interface{} `json:"data,omitempty"` + }{ + Code: code, + Message: message, + Data: data, + }, + } +} + +// Helper function for creating a progress notification +func NewProgressNotification( + token ProgressToken, + progress float64, + total *float64, +) ProgressNotification { + notification := ProgressNotification{ + Notification: Notification{ + Method: "notifications/progress", + }, + Params: struct { + ProgressToken ProgressToken `json:"progressToken"` + Progress float64 `json:"progress"` + Total float64 `json:"total,omitempty"` + }{ + ProgressToken: token, + Progress: progress, + }, + } + if total != nil { + notification.Params.Total = *total + } + return notification +} + +// Helper function for creating a logging message notification +func NewLoggingMessageNotification( + level LoggingLevel, + logger string, + data interface{}, +) LoggingMessageNotification { + return LoggingMessageNotification{ + Notification: Notification{ + Method: "notifications/message", + }, + Params: struct { + Level LoggingLevel `json:"level"` + Logger string `json:"logger,omitempty"` + Data interface{} `json:"data"` + }{ + Level: level, + Logger: logger, + Data: data, + }, + } +} + +// Helper function to create a new PromptMessage +func NewPromptMessage(role Role, content Content) PromptMessage { + return PromptMessage{ + Role: role, + Content: content, + } +} + +// Helper function to create a new TextContent +func NewTextContent(text string) TextContent { + return TextContent{ + Type: "text", + Text: text, + } +} + +// Helper function to create a new ImageContent +func NewImageContent(data, mimeType string) ImageContent { + return ImageContent{ + Type: "image", + Data: data, + MIMEType: mimeType, + } +} + +// Helper function to create a new EmbeddedResource +func NewEmbeddedResource(resource ResourceContents) EmbeddedResource { + return EmbeddedResource{ + Type: "resource", + Resource: resource, + } +} + +// NewToolResultText creates a new CallToolResult with a text content +func NewToolResultText(text string) *CallToolResult { + return &CallToolResult{ + Content: []Content{ + TextContent{ + Type: "text", + Text: text, + }, + }, + } +} + +// NewToolResultImage creates a new CallToolResult with both text and image content +func NewToolResultImage(text, imageData, mimeType string) *CallToolResult { + return &CallToolResult{ + Content: []Content{ + TextContent{ + Type: "text", + Text: text, + }, + ImageContent{ + Type: "image", + Data: imageData, + MIMEType: mimeType, + }, + }, + } +} + +// NewToolResultResource creates a new CallToolResult with an embedded resource +func NewToolResultResource( + text string, + resource ResourceContents, +) *CallToolResult { + return &CallToolResult{ + Content: []Content{ + TextContent{ + Type: "text", + Text: text, + }, + EmbeddedResource{ + Type: "resource", + Resource: resource, + }, + }, + } +} + +// NewListResourcesResult creates a new ListResourcesResult +func NewListResourcesResult( + resources []Resource, + nextCursor Cursor, +) *ListResourcesResult { + return &ListResourcesResult{ + PaginatedResult: PaginatedResult{ + NextCursor: nextCursor, + }, + Resources: resources, + } +} + +// NewListResourceTemplatesResult creates a new ListResourceTemplatesResult +func NewListResourceTemplatesResult( + templates []ResourceTemplate, + nextCursor Cursor, +) *ListResourceTemplatesResult { + return &ListResourceTemplatesResult{ + PaginatedResult: PaginatedResult{ + NextCursor: nextCursor, + }, + ResourceTemplates: templates, + } +} + +// NewReadResourceResult creates a new ReadResourceResult with text content +func NewReadResourceResult(text string) *ReadResourceResult { + return &ReadResourceResult{ + Contents: []ResourceContents{ + TextResourceContents{ + Text: text, + }, + }, + } +} + +// NewListPromptsResult creates a new ListPromptsResult +func NewListPromptsResult( + prompts []Prompt, + nextCursor Cursor, +) *ListPromptsResult { + return &ListPromptsResult{ + PaginatedResult: PaginatedResult{ + NextCursor: nextCursor, + }, + Prompts: prompts, + } +} + +// NewGetPromptResult creates a new GetPromptResult +func NewGetPromptResult( + description string, + messages []PromptMessage, +) *GetPromptResult { + return &GetPromptResult{ + Description: description, + Messages: messages, + } +} + +// NewListToolsResult creates a new ListToolsResult +func NewListToolsResult(tools []Tool, nextCursor Cursor) *ListToolsResult { + return &ListToolsResult{ + PaginatedResult: PaginatedResult{ + NextCursor: nextCursor, + }, + Tools: tools, + } +} + +// NewInitializeResult creates a new InitializeResult +func NewInitializeResult( + protocolVersion string, + capabilities ServerCapabilities, + serverInfo Implementation, + instructions string, +) *InitializeResult { + return &InitializeResult{ + ProtocolVersion: protocolVersion, + Capabilities: capabilities, + ServerInfo: serverInfo, + Instructions: instructions, + } +} + +// Helper for formatting numbers in tool results +func FormatNumberResult(value float64) *CallToolResult { + return NewToolResultText(fmt.Sprintf("%.2f", value)) +} + +func ExtractString(data map[string]any, key string) string { + if value, ok := data[key]; ok { + if str, ok := value.(string); ok { + return str + } + } + return "" +} + +func ExtractMap(data map[string]any, key string) map[string]any { + if value, ok := data[key]; ok { + if m, ok := value.(map[string]any); ok { + return m + } + } + return nil +} + +func ParseContent(contentMap map[string]any) (Content, error) { + contentType := ExtractString(contentMap, "type") + + switch contentType { + case "text": + text := ExtractString(contentMap, "text") + if text == "" { + return nil, fmt.Errorf("text is missing") + } + return NewTextContent(text), nil + + case "image": + data := ExtractString(contentMap, "data") + mimeType := ExtractString(contentMap, "mimeType") + if data == "" || mimeType == "" { + return nil, fmt.Errorf("image data or mimeType is missing") + } + return NewImageContent(data, mimeType), nil + + case "resource": + resourceMap := ExtractMap(contentMap, "resource") + if resourceMap == nil { + return nil, fmt.Errorf("resource is missing") + } + + resourceContents, err := ParseResourceContents(resourceMap) + if err != nil { + return nil, err + } + + return NewEmbeddedResource(resourceContents), nil + } + + return nil, fmt.Errorf("unsupported content type: %s", contentType) +} + +func ParseGetPromptResult(rawMessage *json.RawMessage) (*GetPromptResult, error) { + var jsonContent map[string]any + if err := json.Unmarshal(*rawMessage, &jsonContent); err != nil { + return nil, fmt.Errorf("failed to unmarshal response: %w", err) + } + + result := GetPromptResult{} + + meta, ok := jsonContent["_meta"] + if ok { + if metaMap, ok := meta.(map[string]any); ok { + result.Meta = metaMap + } + } + + description, ok := jsonContent["description"] + if ok { + if descriptionStr, ok := description.(string); ok { + result.Description = descriptionStr + } + } + + messages, ok := jsonContent["messages"] + if ok { + messagesArr, ok := messages.([]any) + if !ok { + return nil, fmt.Errorf("messages is not an array") + } + + for _, message := range messagesArr { + messageMap, ok := message.(map[string]any) + if !ok { + return nil, fmt.Errorf("message is not an object") + } + + // Extract role + roleStr := ExtractString(messageMap, "role") + if roleStr == "" || (roleStr != string(RoleAssistant) && roleStr != string(RoleUser)) { + return nil, fmt.Errorf("unsupported role: %s", roleStr) + } + + // Extract content + contentMap, ok := messageMap["content"].(map[string]any) + if !ok { + return nil, fmt.Errorf("content is not an object") + } + + // Process content + content, err := ParseContent(contentMap) + if err != nil { + return nil, err + } + + // Append processed message + result.Messages = append(result.Messages, NewPromptMessage(Role(roleStr), content)) + + } + } + + return &result, nil +} + +func ParseCallToolResult(rawMessage *json.RawMessage) (*CallToolResult, error) { + var jsonContent map[string]any + if err := json.Unmarshal(*rawMessage, &jsonContent); err != nil { + return nil, fmt.Errorf("failed to unmarshal response: %w", err) + } + + var result CallToolResult + + meta, ok := jsonContent["_meta"] + if ok { + if metaMap, ok := meta.(map[string]any); ok { + result.Meta = metaMap + } + } + + isError, ok := jsonContent["isError"] + if ok { + if isErrorBool, ok := isError.(bool); ok { + result.IsError = isErrorBool + } + } + + contents, ok := jsonContent["content"] + if !ok { + return nil, fmt.Errorf("content is missing") + } + + contentArr, ok := contents.([]any) + if !ok { + return nil, fmt.Errorf("content is not an array") + } + + for _, content := range contentArr { + // Extract content + contentMap, ok := content.(map[string]any) + if !ok { + return nil, fmt.Errorf("content is not an object") + } + + // Process content + content, err := ParseContent(contentMap) + if err != nil { + return nil, err + } + + result.Content = append(result.Content, content) + } + + return &result, nil +} + +func ParseResourceContents(contentMap map[string]any) (ResourceContents, error) { + uri := ExtractString(contentMap, "uri") + if uri == "" { + return nil, fmt.Errorf("resource uri is missing") + } + + mimeType := ExtractString(contentMap, "mimeType") + + if text := ExtractString(contentMap, "text"); text != "" { + return TextResourceContents{ + URI: uri, + MIMEType: mimeType, + Text: text, + }, nil + } + + if blob := ExtractString(contentMap, "blob"); blob != "" { + return BlobResourceContents{ + URI: uri, + MIMEType: mimeType, + Blob: blob, + }, nil + } + + return nil, fmt.Errorf("unsupported resource type") +} + +func ParseReadResourceResult(rawMessage *json.RawMessage) (*ReadResourceResult, error) { + var jsonContent map[string]any + if err := json.Unmarshal(*rawMessage, &jsonContent); err != nil { + return nil, fmt.Errorf("failed to unmarshal response: %w", err) + } + + var result ReadResourceResult + + meta, ok := jsonContent["_meta"] + if ok { + if metaMap, ok := meta.(map[string]any); ok { + result.Meta = metaMap + } + } + + contents, ok := jsonContent["contents"] + if !ok { + return nil, fmt.Errorf("contents is missing") + } + + contentArr, ok := contents.([]any) + if !ok { + return nil, fmt.Errorf("contents is not an array") + } + + for _, content := range contentArr { + // Extract content + contentMap, ok := content.(map[string]any) + if !ok { + return nil, fmt.Errorf("content is not an object") + } + + // Process content + content, err := ParseResourceContents(contentMap) + if err != nil { + return nil, err + } + + result.Contents = append(result.Contents, content) + } + + return &result, nil +} diff --git a/vendor/github.com/mark3labs/mcp-go/server/hooks.go b/vendor/github.com/mark3labs/mcp-go/server/hooks.go new file mode 100644 index 000000000..075fe9f8f --- /dev/null +++ b/vendor/github.com/mark3labs/mcp-go/server/hooks.go @@ -0,0 +1,442 @@ +// Code generated by `go generate`. DO NOT EDIT. +// source: server/internal/gen/hooks.go.tmpl +package server + +import ( + "github.com/mark3labs/mcp-go/mcp" +) + +// BeforeAnyHookFunc is a function that is called after the request is +// parsed but before the method is called. +type BeforeAnyHookFunc func(id any, method mcp.MCPMethod, message any) + +// OnSuccessHookFunc is a hook that will be called after the request +// successfully generates a result, but before the result is sent to the client. +type OnSuccessHookFunc func(id any, method mcp.MCPMethod, message any, result any) + +// OnErrorHookFunc is a hook that will be called when an error occurs, +// either during the request parsing or the method execution. +// +// Example usage: +// ``` +// +// hooks.AddOnError(func(id any, method mcp.MCPMethod, message any, err error) { +// // Check for specific error types using errors.Is +// if errors.Is(err, ErrUnsupported) { +// // Handle capability not supported errors +// log.Printf("Capability not supported: %v", err) +// } +// +// // Use errors.As to get specific error types +// var parseErr = &UnparseableMessageError{} +// if errors.As(err, &parseErr) { +// // Access specific methods/fields of the error type +// log.Printf("Failed to parse message for method %s: %v", +// parseErr.GetMethod(), parseErr.Unwrap()) +// // Access the raw message that failed to parse +// rawMsg := parseErr.GetMessage() +// } +// +// // Check for specific resource/prompt/tool errors +// switch { +// case errors.Is(err, ErrResourceNotFound): +// log.Printf("Resource not found: %v", err) +// case errors.Is(err, ErrPromptNotFound): +// log.Printf("Prompt not found: %v", err) +// case errors.Is(err, ErrToolNotFound): +// log.Printf("Tool not found: %v", err) +// } +// }) +type OnErrorHookFunc func(id any, method mcp.MCPMethod, message any, err error) + +type OnBeforeInitializeFunc func(id any, message *mcp.InitializeRequest) +type OnAfterInitializeFunc func(id any, message *mcp.InitializeRequest, result *mcp.InitializeResult) + +type OnBeforePingFunc func(id any, message *mcp.PingRequest) +type OnAfterPingFunc func(id any, message *mcp.PingRequest, result *mcp.EmptyResult) + +type OnBeforeListResourcesFunc func(id any, message *mcp.ListResourcesRequest) +type OnAfterListResourcesFunc func(id any, message *mcp.ListResourcesRequest, result *mcp.ListResourcesResult) + +type OnBeforeListResourceTemplatesFunc func(id any, message *mcp.ListResourceTemplatesRequest) +type OnAfterListResourceTemplatesFunc func(id any, message *mcp.ListResourceTemplatesRequest, result *mcp.ListResourceTemplatesResult) + +type OnBeforeReadResourceFunc func(id any, message *mcp.ReadResourceRequest) +type OnAfterReadResourceFunc func(id any, message *mcp.ReadResourceRequest, result *mcp.ReadResourceResult) + +type OnBeforeListPromptsFunc func(id any, message *mcp.ListPromptsRequest) +type OnAfterListPromptsFunc func(id any, message *mcp.ListPromptsRequest, result *mcp.ListPromptsResult) + +type OnBeforeGetPromptFunc func(id any, message *mcp.GetPromptRequest) +type OnAfterGetPromptFunc func(id any, message *mcp.GetPromptRequest, result *mcp.GetPromptResult) + +type OnBeforeListToolsFunc func(id any, message *mcp.ListToolsRequest) +type OnAfterListToolsFunc func(id any, message *mcp.ListToolsRequest, result *mcp.ListToolsResult) + +type OnBeforeCallToolFunc func(id any, message *mcp.CallToolRequest) +type OnAfterCallToolFunc func(id any, message *mcp.CallToolRequest, result *mcp.CallToolResult) + +type Hooks struct { + OnBeforeAny []BeforeAnyHookFunc + OnSuccess []OnSuccessHookFunc + OnError []OnErrorHookFunc + OnBeforeInitialize []OnBeforeInitializeFunc + OnAfterInitialize []OnAfterInitializeFunc + OnBeforePing []OnBeforePingFunc + OnAfterPing []OnAfterPingFunc + OnBeforeListResources []OnBeforeListResourcesFunc + OnAfterListResources []OnAfterListResourcesFunc + OnBeforeListResourceTemplates []OnBeforeListResourceTemplatesFunc + OnAfterListResourceTemplates []OnAfterListResourceTemplatesFunc + OnBeforeReadResource []OnBeforeReadResourceFunc + OnAfterReadResource []OnAfterReadResourceFunc + OnBeforeListPrompts []OnBeforeListPromptsFunc + OnAfterListPrompts []OnAfterListPromptsFunc + OnBeforeGetPrompt []OnBeforeGetPromptFunc + OnAfterGetPrompt []OnAfterGetPromptFunc + OnBeforeListTools []OnBeforeListToolsFunc + OnAfterListTools []OnAfterListToolsFunc + OnBeforeCallTool []OnBeforeCallToolFunc + OnAfterCallTool []OnAfterCallToolFunc +} + +func (c *Hooks) AddBeforeAny(hook BeforeAnyHookFunc) { + c.OnBeforeAny = append(c.OnBeforeAny, hook) +} + +func (c *Hooks) AddOnSuccess(hook OnSuccessHookFunc) { + c.OnSuccess = append(c.OnSuccess, hook) +} + +// AddOnError registers a hook function that will be called when an error occurs. +// The error parameter contains the actual error object, which can be interrogated +// using Go's error handling patterns like errors.Is and errors.As. +// +// Example: +// ``` +// // Create a channel to receive errors for testing +// errChan := make(chan error, 1) +// +// // Register hook to capture and inspect errors +// hooks := &Hooks{} +// +// hooks.AddOnError(func(id any, method mcp.MCPMethod, message any, err error) { +// // For capability-related errors +// if errors.Is(err, ErrUnsupported) { +// // Handle capability not supported +// errChan <- err +// return +// } +// +// // For parsing errors +// var parseErr = &UnparseableMessageError{} +// if errors.As(err, &parseErr) { +// // Handle unparseable message errors +// fmt.Printf("Failed to parse %s request: %v\n", +// parseErr.GetMethod(), parseErr.Unwrap()) +// errChan <- parseErr +// return +// } +// +// // For resource/prompt/tool not found errors +// if errors.Is(err, ErrResourceNotFound) || +// errors.Is(err, ErrPromptNotFound) || +// errors.Is(err, ErrToolNotFound) { +// // Handle not found errors +// errChan <- err +// return +// } +// +// // For other errors +// errChan <- err +// }) +// +// server := NewMCPServer("test-server", "1.0.0", WithHooks(hooks)) +// ``` +func (c *Hooks) AddOnError(hook OnErrorHookFunc) { + c.OnError = append(c.OnError, hook) +} + +func (c *Hooks) beforeAny(id any, method mcp.MCPMethod, message any) { + if c == nil { + return + } + for _, hook := range c.OnBeforeAny { + hook(id, method, message) + } +} + +func (c *Hooks) onSuccess(id any, method mcp.MCPMethod, message any, result any) { + if c == nil { + return + } + for _, hook := range c.OnSuccess { + hook(id, method, message, result) + } +} + +// onError calls all registered error hooks with the error object. +// The err parameter contains the actual error that occurred, which implements +// the standard error interface and may be a wrapped error or custom error type. +// +// This allows consumer code to use Go's error handling patterns: +// - errors.Is(err, ErrUnsupported) to check for specific sentinel errors +// - errors.As(err, &customErr) to extract custom error types +// +// Common error types include: +// - ErrUnsupported: When a capability is not enabled +// - UnparseableMessageError: When request parsing fails +// - ErrResourceNotFound: When a resource is not found +// - ErrPromptNotFound: When a prompt is not found +// - ErrToolNotFound: When a tool is not found +func (c *Hooks) onError(id any, method mcp.MCPMethod, message any, err error) { + if c == nil { + return + } + for _, hook := range c.OnError { + hook(id, method, message, err) + } +} +func (c *Hooks) AddBeforeInitialize(hook OnBeforeInitializeFunc) { + c.OnBeforeInitialize = append(c.OnBeforeInitialize, hook) +} + +func (c *Hooks) AddAfterInitialize(hook OnAfterInitializeFunc) { + c.OnAfterInitialize = append(c.OnAfterInitialize, hook) +} + +func (c *Hooks) beforeInitialize(id any, message *mcp.InitializeRequest) { + c.beforeAny(id, mcp.MethodInitialize, message) + if c == nil { + return + } + for _, hook := range c.OnBeforeInitialize { + hook(id, message) + } +} + +func (c *Hooks) afterInitialize(id any, message *mcp.InitializeRequest, result *mcp.InitializeResult) { + c.onSuccess(id, mcp.MethodInitialize, message, result) + if c == nil { + return + } + for _, hook := range c.OnAfterInitialize { + hook(id, message, result) + } +} +func (c *Hooks) AddBeforePing(hook OnBeforePingFunc) { + c.OnBeforePing = append(c.OnBeforePing, hook) +} + +func (c *Hooks) AddAfterPing(hook OnAfterPingFunc) { + c.OnAfterPing = append(c.OnAfterPing, hook) +} + +func (c *Hooks) beforePing(id any, message *mcp.PingRequest) { + c.beforeAny(id, mcp.MethodPing, message) + if c == nil { + return + } + for _, hook := range c.OnBeforePing { + hook(id, message) + } +} + +func (c *Hooks) afterPing(id any, message *mcp.PingRequest, result *mcp.EmptyResult) { + c.onSuccess(id, mcp.MethodPing, message, result) + if c == nil { + return + } + for _, hook := range c.OnAfterPing { + hook(id, message, result) + } +} +func (c *Hooks) AddBeforeListResources(hook OnBeforeListResourcesFunc) { + c.OnBeforeListResources = append(c.OnBeforeListResources, hook) +} + +func (c *Hooks) AddAfterListResources(hook OnAfterListResourcesFunc) { + c.OnAfterListResources = append(c.OnAfterListResources, hook) +} + +func (c *Hooks) beforeListResources(id any, message *mcp.ListResourcesRequest) { + c.beforeAny(id, mcp.MethodResourcesList, message) + if c == nil { + return + } + for _, hook := range c.OnBeforeListResources { + hook(id, message) + } +} + +func (c *Hooks) afterListResources(id any, message *mcp.ListResourcesRequest, result *mcp.ListResourcesResult) { + c.onSuccess(id, mcp.MethodResourcesList, message, result) + if c == nil { + return + } + for _, hook := range c.OnAfterListResources { + hook(id, message, result) + } +} +func (c *Hooks) AddBeforeListResourceTemplates(hook OnBeforeListResourceTemplatesFunc) { + c.OnBeforeListResourceTemplates = append(c.OnBeforeListResourceTemplates, hook) +} + +func (c *Hooks) AddAfterListResourceTemplates(hook OnAfterListResourceTemplatesFunc) { + c.OnAfterListResourceTemplates = append(c.OnAfterListResourceTemplates, hook) +} + +func (c *Hooks) beforeListResourceTemplates(id any, message *mcp.ListResourceTemplatesRequest) { + c.beforeAny(id, mcp.MethodResourcesTemplatesList, message) + if c == nil { + return + } + for _, hook := range c.OnBeforeListResourceTemplates { + hook(id, message) + } +} + +func (c *Hooks) afterListResourceTemplates(id any, message *mcp.ListResourceTemplatesRequest, result *mcp.ListResourceTemplatesResult) { + c.onSuccess(id, mcp.MethodResourcesTemplatesList, message, result) + if c == nil { + return + } + for _, hook := range c.OnAfterListResourceTemplates { + hook(id, message, result) + } +} +func (c *Hooks) AddBeforeReadResource(hook OnBeforeReadResourceFunc) { + c.OnBeforeReadResource = append(c.OnBeforeReadResource, hook) +} + +func (c *Hooks) AddAfterReadResource(hook OnAfterReadResourceFunc) { + c.OnAfterReadResource = append(c.OnAfterReadResource, hook) +} + +func (c *Hooks) beforeReadResource(id any, message *mcp.ReadResourceRequest) { + c.beforeAny(id, mcp.MethodResourcesRead, message) + if c == nil { + return + } + for _, hook := range c.OnBeforeReadResource { + hook(id, message) + } +} + +func (c *Hooks) afterReadResource(id any, message *mcp.ReadResourceRequest, result *mcp.ReadResourceResult) { + c.onSuccess(id, mcp.MethodResourcesRead, message, result) + if c == nil { + return + } + for _, hook := range c.OnAfterReadResource { + hook(id, message, result) + } +} +func (c *Hooks) AddBeforeListPrompts(hook OnBeforeListPromptsFunc) { + c.OnBeforeListPrompts = append(c.OnBeforeListPrompts, hook) +} + +func (c *Hooks) AddAfterListPrompts(hook OnAfterListPromptsFunc) { + c.OnAfterListPrompts = append(c.OnAfterListPrompts, hook) +} + +func (c *Hooks) beforeListPrompts(id any, message *mcp.ListPromptsRequest) { + c.beforeAny(id, mcp.MethodPromptsList, message) + if c == nil { + return + } + for _, hook := range c.OnBeforeListPrompts { + hook(id, message) + } +} + +func (c *Hooks) afterListPrompts(id any, message *mcp.ListPromptsRequest, result *mcp.ListPromptsResult) { + c.onSuccess(id, mcp.MethodPromptsList, message, result) + if c == nil { + return + } + for _, hook := range c.OnAfterListPrompts { + hook(id, message, result) + } +} +func (c *Hooks) AddBeforeGetPrompt(hook OnBeforeGetPromptFunc) { + c.OnBeforeGetPrompt = append(c.OnBeforeGetPrompt, hook) +} + +func (c *Hooks) AddAfterGetPrompt(hook OnAfterGetPromptFunc) { + c.OnAfterGetPrompt = append(c.OnAfterGetPrompt, hook) +} + +func (c *Hooks) beforeGetPrompt(id any, message *mcp.GetPromptRequest) { + c.beforeAny(id, mcp.MethodPromptsGet, message) + if c == nil { + return + } + for _, hook := range c.OnBeforeGetPrompt { + hook(id, message) + } +} + +func (c *Hooks) afterGetPrompt(id any, message *mcp.GetPromptRequest, result *mcp.GetPromptResult) { + c.onSuccess(id, mcp.MethodPromptsGet, message, result) + if c == nil { + return + } + for _, hook := range c.OnAfterGetPrompt { + hook(id, message, result) + } +} +func (c *Hooks) AddBeforeListTools(hook OnBeforeListToolsFunc) { + c.OnBeforeListTools = append(c.OnBeforeListTools, hook) +} + +func (c *Hooks) AddAfterListTools(hook OnAfterListToolsFunc) { + c.OnAfterListTools = append(c.OnAfterListTools, hook) +} + +func (c *Hooks) beforeListTools(id any, message *mcp.ListToolsRequest) { + c.beforeAny(id, mcp.MethodToolsList, message) + if c == nil { + return + } + for _, hook := range c.OnBeforeListTools { + hook(id, message) + } +} + +func (c *Hooks) afterListTools(id any, message *mcp.ListToolsRequest, result *mcp.ListToolsResult) { + c.onSuccess(id, mcp.MethodToolsList, message, result) + if c == nil { + return + } + for _, hook := range c.OnAfterListTools { + hook(id, message, result) + } +} +func (c *Hooks) AddBeforeCallTool(hook OnBeforeCallToolFunc) { + c.OnBeforeCallTool = append(c.OnBeforeCallTool, hook) +} + +func (c *Hooks) AddAfterCallTool(hook OnAfterCallToolFunc) { + c.OnAfterCallTool = append(c.OnAfterCallTool, hook) +} + +func (c *Hooks) beforeCallTool(id any, message *mcp.CallToolRequest) { + c.beforeAny(id, mcp.MethodToolsCall, message) + if c == nil { + return + } + for _, hook := range c.OnBeforeCallTool { + hook(id, message) + } +} + +func (c *Hooks) afterCallTool(id any, message *mcp.CallToolRequest, result *mcp.CallToolResult) { + c.onSuccess(id, mcp.MethodToolsCall, message, result) + if c == nil { + return + } + for _, hook := range c.OnAfterCallTool { + hook(id, message, result) + } +} diff --git a/vendor/github.com/mark3labs/mcp-go/server/request_handler.go b/vendor/github.com/mark3labs/mcp-go/server/request_handler.go new file mode 100644 index 000000000..e1167b22d --- /dev/null +++ b/vendor/github.com/mark3labs/mcp-go/server/request_handler.go @@ -0,0 +1,279 @@ +// Code generated by `go generate`. DO NOT EDIT. +// source: server/internal/gen/request_handler.go.tmpl +package server + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/mark3labs/mcp-go/mcp" +) + +// HandleMessage processes an incoming JSON-RPC message and returns an appropriate response +func (s *MCPServer) HandleMessage( + ctx context.Context, + message json.RawMessage, +) mcp.JSONRPCMessage { + // Add server to context + ctx = context.WithValue(ctx, serverKey{}, s) + var err *requestError + + var baseMessage struct { + JSONRPC string `json:"jsonrpc"` + Method mcp.MCPMethod `json:"method"` + ID any `json:"id,omitempty"` + } + + if err := json.Unmarshal(message, &baseMessage); err != nil { + return createErrorResponse( + nil, + mcp.PARSE_ERROR, + "Failed to parse message", + ) + } + + // Check for valid JSONRPC version + if baseMessage.JSONRPC != mcp.JSONRPC_VERSION { + return createErrorResponse( + baseMessage.ID, + mcp.INVALID_REQUEST, + "Invalid JSON-RPC version", + ) + } + + if baseMessage.ID == nil { + var notification mcp.JSONRPCNotification + if err := json.Unmarshal(message, ¬ification); err != nil { + return createErrorResponse( + nil, + mcp.PARSE_ERROR, + "Failed to parse notification", + ) + } + s.handleNotification(ctx, notification) + return nil // Return nil for notifications + } + + switch baseMessage.Method { + case mcp.MethodInitialize: + var request mcp.InitializeRequest + var result *mcp.InitializeResult + if unmarshalErr := json.Unmarshal(message, &request); unmarshalErr != nil { + err = &requestError{ + id: baseMessage.ID, + code: mcp.INVALID_REQUEST, + err: &UnparseableMessageError{message: message, err: unmarshalErr, method: baseMessage.Method}, + } + } else { + s.hooks.beforeInitialize(baseMessage.ID, &request) + result, err = s.handleInitialize(ctx, baseMessage.ID, request) + } + if err != nil { + s.hooks.onError(baseMessage.ID, baseMessage.Method, &request, err) + return err.ToJSONRPCError() + } + s.hooks.afterInitialize(baseMessage.ID, &request, result) + return createResponse(baseMessage.ID, *result) + case mcp.MethodPing: + var request mcp.PingRequest + var result *mcp.EmptyResult + if unmarshalErr := json.Unmarshal(message, &request); unmarshalErr != nil { + err = &requestError{ + id: baseMessage.ID, + code: mcp.INVALID_REQUEST, + err: &UnparseableMessageError{message: message, err: unmarshalErr, method: baseMessage.Method}, + } + } else { + s.hooks.beforePing(baseMessage.ID, &request) + result, err = s.handlePing(ctx, baseMessage.ID, request) + } + if err != nil { + s.hooks.onError(baseMessage.ID, baseMessage.Method, &request, err) + return err.ToJSONRPCError() + } + s.hooks.afterPing(baseMessage.ID, &request, result) + return createResponse(baseMessage.ID, *result) + case mcp.MethodResourcesList: + var request mcp.ListResourcesRequest + var result *mcp.ListResourcesResult + if s.capabilities.resources == nil { + err = &requestError{ + id: baseMessage.ID, + code: mcp.METHOD_NOT_FOUND, + err: fmt.Errorf("resources %w", ErrUnsupported), + } + } else if unmarshalErr := json.Unmarshal(message, &request); unmarshalErr != nil { + err = &requestError{ + id: baseMessage.ID, + code: mcp.INVALID_REQUEST, + err: &UnparseableMessageError{message: message, err: unmarshalErr, method: baseMessage.Method}, + } + } else { + s.hooks.beforeListResources(baseMessage.ID, &request) + result, err = s.handleListResources(ctx, baseMessage.ID, request) + } + if err != nil { + s.hooks.onError(baseMessage.ID, baseMessage.Method, &request, err) + return err.ToJSONRPCError() + } + s.hooks.afterListResources(baseMessage.ID, &request, result) + return createResponse(baseMessage.ID, *result) + case mcp.MethodResourcesTemplatesList: + var request mcp.ListResourceTemplatesRequest + var result *mcp.ListResourceTemplatesResult + if s.capabilities.resources == nil { + err = &requestError{ + id: baseMessage.ID, + code: mcp.METHOD_NOT_FOUND, + err: fmt.Errorf("resources %w", ErrUnsupported), + } + } else if unmarshalErr := json.Unmarshal(message, &request); unmarshalErr != nil { + err = &requestError{ + id: baseMessage.ID, + code: mcp.INVALID_REQUEST, + err: &UnparseableMessageError{message: message, err: unmarshalErr, method: baseMessage.Method}, + } + } else { + s.hooks.beforeListResourceTemplates(baseMessage.ID, &request) + result, err = s.handleListResourceTemplates(ctx, baseMessage.ID, request) + } + if err != nil { + s.hooks.onError(baseMessage.ID, baseMessage.Method, &request, err) + return err.ToJSONRPCError() + } + s.hooks.afterListResourceTemplates(baseMessage.ID, &request, result) + return createResponse(baseMessage.ID, *result) + case mcp.MethodResourcesRead: + var request mcp.ReadResourceRequest + var result *mcp.ReadResourceResult + if s.capabilities.resources == nil { + err = &requestError{ + id: baseMessage.ID, + code: mcp.METHOD_NOT_FOUND, + err: fmt.Errorf("resources %w", ErrUnsupported), + } + } else if unmarshalErr := json.Unmarshal(message, &request); unmarshalErr != nil { + err = &requestError{ + id: baseMessage.ID, + code: mcp.INVALID_REQUEST, + err: &UnparseableMessageError{message: message, err: unmarshalErr, method: baseMessage.Method}, + } + } else { + s.hooks.beforeReadResource(baseMessage.ID, &request) + result, err = s.handleReadResource(ctx, baseMessage.ID, request) + } + if err != nil { + s.hooks.onError(baseMessage.ID, baseMessage.Method, &request, err) + return err.ToJSONRPCError() + } + s.hooks.afterReadResource(baseMessage.ID, &request, result) + return createResponse(baseMessage.ID, *result) + case mcp.MethodPromptsList: + var request mcp.ListPromptsRequest + var result *mcp.ListPromptsResult + if s.capabilities.prompts == nil { + err = &requestError{ + id: baseMessage.ID, + code: mcp.METHOD_NOT_FOUND, + err: fmt.Errorf("prompts %w", ErrUnsupported), + } + } else if unmarshalErr := json.Unmarshal(message, &request); unmarshalErr != nil { + err = &requestError{ + id: baseMessage.ID, + code: mcp.INVALID_REQUEST, + err: &UnparseableMessageError{message: message, err: unmarshalErr, method: baseMessage.Method}, + } + } else { + s.hooks.beforeListPrompts(baseMessage.ID, &request) + result, err = s.handleListPrompts(ctx, baseMessage.ID, request) + } + if err != nil { + s.hooks.onError(baseMessage.ID, baseMessage.Method, &request, err) + return err.ToJSONRPCError() + } + s.hooks.afterListPrompts(baseMessage.ID, &request, result) + return createResponse(baseMessage.ID, *result) + case mcp.MethodPromptsGet: + var request mcp.GetPromptRequest + var result *mcp.GetPromptResult + if s.capabilities.prompts == nil { + err = &requestError{ + id: baseMessage.ID, + code: mcp.METHOD_NOT_FOUND, + err: fmt.Errorf("prompts %w", ErrUnsupported), + } + } else if unmarshalErr := json.Unmarshal(message, &request); unmarshalErr != nil { + err = &requestError{ + id: baseMessage.ID, + code: mcp.INVALID_REQUEST, + err: &UnparseableMessageError{message: message, err: unmarshalErr, method: baseMessage.Method}, + } + } else { + s.hooks.beforeGetPrompt(baseMessage.ID, &request) + result, err = s.handleGetPrompt(ctx, baseMessage.ID, request) + } + if err != nil { + s.hooks.onError(baseMessage.ID, baseMessage.Method, &request, err) + return err.ToJSONRPCError() + } + s.hooks.afterGetPrompt(baseMessage.ID, &request, result) + return createResponse(baseMessage.ID, *result) + case mcp.MethodToolsList: + var request mcp.ListToolsRequest + var result *mcp.ListToolsResult + if s.capabilities.tools == nil { + err = &requestError{ + id: baseMessage.ID, + code: mcp.METHOD_NOT_FOUND, + err: fmt.Errorf("tools %w", ErrUnsupported), + } + } else if unmarshalErr := json.Unmarshal(message, &request); unmarshalErr != nil { + err = &requestError{ + id: baseMessage.ID, + code: mcp.INVALID_REQUEST, + err: &UnparseableMessageError{message: message, err: unmarshalErr, method: baseMessage.Method}, + } + } else { + s.hooks.beforeListTools(baseMessage.ID, &request) + result, err = s.handleListTools(ctx, baseMessage.ID, request) + } + if err != nil { + s.hooks.onError(baseMessage.ID, baseMessage.Method, &request, err) + return err.ToJSONRPCError() + } + s.hooks.afterListTools(baseMessage.ID, &request, result) + return createResponse(baseMessage.ID, *result) + case mcp.MethodToolsCall: + var request mcp.CallToolRequest + var result *mcp.CallToolResult + if s.capabilities.tools == nil { + err = &requestError{ + id: baseMessage.ID, + code: mcp.METHOD_NOT_FOUND, + err: fmt.Errorf("tools %w", ErrUnsupported), + } + } else if unmarshalErr := json.Unmarshal(message, &request); unmarshalErr != nil { + err = &requestError{ + id: baseMessage.ID, + code: mcp.INVALID_REQUEST, + err: &UnparseableMessageError{message: message, err: unmarshalErr, method: baseMessage.Method}, + } + } else { + s.hooks.beforeCallTool(baseMessage.ID, &request) + result, err = s.handleToolCall(ctx, baseMessage.ID, request) + } + if err != nil { + s.hooks.onError(baseMessage.ID, baseMessage.Method, &request, err) + return err.ToJSONRPCError() + } + s.hooks.afterCallTool(baseMessage.ID, &request, result) + return createResponse(baseMessage.ID, *result) + default: + return createErrorResponse( + baseMessage.ID, + mcp.METHOD_NOT_FOUND, + fmt.Sprintf("Method %s not found", baseMessage.Method), + ) + } +} diff --git a/vendor/github.com/mark3labs/mcp-go/server/server.go b/vendor/github.com/mark3labs/mcp-go/server/server.go new file mode 100644 index 000000000..64b516234 --- /dev/null +++ b/vendor/github.com/mark3labs/mcp-go/server/server.go @@ -0,0 +1,768 @@ +// Package server provides MCP (Model Control Protocol) server implementations. +package server + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "sort" + "sync" + "sync/atomic" + + "github.com/mark3labs/mcp-go/mcp" +) + +// resourceEntry holds both a resource and its handler +type resourceEntry struct { + resource mcp.Resource + handler ResourceHandlerFunc +} + +// resourceTemplateEntry holds both a template and its handler +type resourceTemplateEntry struct { + template mcp.ResourceTemplate + handler ResourceTemplateHandlerFunc +} + +// ServerOption is a function that configures an MCPServer. +type ServerOption func(*MCPServer) + +// ResourceHandlerFunc is a function that returns resource contents. +type ResourceHandlerFunc func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) + +// ResourceTemplateHandlerFunc is a function that returns a resource template. +type ResourceTemplateHandlerFunc func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) + +// PromptHandlerFunc handles prompt requests with given arguments. +type PromptHandlerFunc func(ctx context.Context, request mcp.GetPromptRequest) (*mcp.GetPromptResult, error) + +// ToolHandlerFunc handles tool calls with given arguments. +type ToolHandlerFunc func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) + +// ServerTool combines a Tool with its ToolHandlerFunc. +type ServerTool struct { + Tool mcp.Tool + Handler ToolHandlerFunc +} + +// ClientSession represents an active session that can be used by MCPServer to interact with client. +type ClientSession interface { + // NotificationChannel provides a channel suitable for sending notifications to client. + NotificationChannel() chan<- mcp.JSONRPCNotification + // SessionID is a unique identifier used to track user session. + SessionID() string +} + +// clientSessionKey is the context key for storing current client notification channel. +type clientSessionKey struct{} + +// ClientSessionFromContext retrieves current client notification context from context. +func ClientSessionFromContext(ctx context.Context) ClientSession { + if session, ok := ctx.Value(clientSessionKey{}).(ClientSession); ok { + return session + } + return nil +} + +// UnparseableMessageError is attached to the RequestError when json.Unmarshal +// fails on the request. +type UnparseableMessageError struct { + message json.RawMessage + method mcp.MCPMethod + err error +} + +func (e *UnparseableMessageError) Error() string { + return fmt.Sprintf("unparseable %s request: %s", e.method, e.err) +} + +func (e *UnparseableMessageError) Unwrap() error { + return e.err +} + +func (e *UnparseableMessageError) GetMessage() json.RawMessage { + return e.message +} + +func (e *UnparseableMessageError) GetMethod() mcp.MCPMethod { + return e.method +} + +// RequestError is an error that can be converted to a JSON-RPC error. +// Implements Unwrap() to allow inspecting the error chain. +type requestError struct { + id any + code int + err error +} + +func (e *requestError) Error() string { + return fmt.Sprintf("request error: %s", e.err) +} + +func (e *requestError) ToJSONRPCError() mcp.JSONRPCError { + return mcp.JSONRPCError{ + JSONRPC: mcp.JSONRPC_VERSION, + ID: e.id, + Error: struct { + Code int `json:"code"` + Message string `json:"message"` + Data any `json:"data,omitempty"` + }{ + Code: e.code, + Message: e.err.Error(), + }, + } +} + +func (e *requestError) Unwrap() error { + return e.err +} + +var ( + ErrUnsupported = errors.New("not supported") + ErrResourceNotFound = errors.New("resource not found") + ErrPromptNotFound = errors.New("prompt not found") + ErrToolNotFound = errors.New("tool not found") +) + +// NotificationHandlerFunc handles incoming notifications. +type NotificationHandlerFunc func(ctx context.Context, notification mcp.JSONRPCNotification) + +// MCPServer implements a Model Control Protocol server that can handle various types of requests +// including resources, prompts, and tools. +type MCPServer struct { + mu sync.RWMutex // Add mutex for protecting shared resources + name string + version string + instructions string + resources map[string]resourceEntry + resourceTemplates map[string]resourceTemplateEntry + prompts map[string]mcp.Prompt + promptHandlers map[string]PromptHandlerFunc + tools map[string]ServerTool + notificationHandlers map[string]NotificationHandlerFunc + capabilities serverCapabilities + sessions sync.Map + initialized atomic.Bool // Use atomic for the initialized flag + hooks *Hooks +} + +// serverKey is the context key for storing the server instance +type serverKey struct{} + +// ServerFromContext retrieves the MCPServer instance from a context +func ServerFromContext(ctx context.Context) *MCPServer { + if srv, ok := ctx.Value(serverKey{}).(*MCPServer); ok { + return srv + } + return nil +} + +// WithContext sets the current client session and returns the provided context +func (s *MCPServer) WithContext( + ctx context.Context, + session ClientSession, +) context.Context { + return context.WithValue(ctx, clientSessionKey{}, session) +} + +// RegisterSession saves session that should be notified in case if some server attributes changed. +func (s *MCPServer) RegisterSession( + session ClientSession, +) error { + sessionID := session.SessionID() + if _, exists := s.sessions.LoadOrStore(sessionID, session); exists { + return fmt.Errorf("session %s is already registered", sessionID) + } + return nil +} + +// UnregisterSession removes from storage session that is shut down. +func (s *MCPServer) UnregisterSession( + sessionID string, +) { + s.sessions.Delete(sessionID) +} + +// sendNotificationToAllClients sends a notification to all the currently active clients. +func (s *MCPServer) sendNotificationToAllClients( + method string, + params map[string]any, +) { + notification := mcp.JSONRPCNotification{ + JSONRPC: mcp.JSONRPC_VERSION, + Notification: mcp.Notification{ + Method: method, + Params: mcp.NotificationParams{ + AdditionalFields: params, + }, + }, + } + + s.sessions.Range(func(k, v any) bool { + if session, ok := v.(ClientSession); ok { + select { + case session.NotificationChannel() <- notification: + default: + // TODO: log blocked channel in the future versions + } + } + return true + }) +} + +// SendNotificationToClient sends a notification to the current client +func (s *MCPServer) SendNotificationToClient( + ctx context.Context, + method string, + params map[string]any, +) error { + session := ClientSessionFromContext(ctx) + if session == nil { + return fmt.Errorf("notification channel not initialized") + } + + notification := mcp.JSONRPCNotification{ + JSONRPC: mcp.JSONRPC_VERSION, + Notification: mcp.Notification{ + Method: method, + Params: mcp.NotificationParams{ + AdditionalFields: params, + }, + }, + } + + select { + case session.NotificationChannel() <- notification: + return nil + default: + return fmt.Errorf("notification channel full or blocked") + } +} + +// serverCapabilities defines the supported features of the MCP server +type serverCapabilities struct { + tools *toolCapabilities + resources *resourceCapabilities + prompts *promptCapabilities + logging bool +} + +// resourceCapabilities defines the supported resource-related features +type resourceCapabilities struct { + subscribe bool + listChanged bool +} + +// promptCapabilities defines the supported prompt-related features +type promptCapabilities struct { + listChanged bool +} + +// toolCapabilities defines the supported tool-related features +type toolCapabilities struct { + listChanged bool +} + +// WithResourceCapabilities configures resource-related server capabilities +func WithResourceCapabilities(subscribe, listChanged bool) ServerOption { + return func(s *MCPServer) { + // Always create a non-nil capability object + s.capabilities.resources = &resourceCapabilities{ + subscribe: subscribe, + listChanged: listChanged, + } + } +} + +// WithHooks allows adding hooks that will be called before or after +// either [all] requests or before / after specific request methods, or else +// prior to returning an error to the client. +func WithHooks(hooks *Hooks) ServerOption { + return func(s *MCPServer) { + s.hooks = hooks + } +} + +// WithPromptCapabilities configures prompt-related server capabilities +func WithPromptCapabilities(listChanged bool) ServerOption { + return func(s *MCPServer) { + // Always create a non-nil capability object + s.capabilities.prompts = &promptCapabilities{ + listChanged: listChanged, + } + } +} + +// WithToolCapabilities configures tool-related server capabilities +func WithToolCapabilities(listChanged bool) ServerOption { + return func(s *MCPServer) { + // Always create a non-nil capability object + s.capabilities.tools = &toolCapabilities{ + listChanged: listChanged, + } + } +} + +// WithLogging enables logging capabilities for the server +func WithLogging() ServerOption { + return func(s *MCPServer) { + s.capabilities.logging = true + } +} + +// WithInstructions sets the server instructions for the client returned in the initialize response +func WithInstructions(instructions string) ServerOption { + return func(s *MCPServer) { + s.instructions = instructions + } +} + +// NewMCPServer creates a new MCP server instance with the given name, version and options +func NewMCPServer( + name, version string, + opts ...ServerOption, +) *MCPServer { + s := &MCPServer{ + resources: make(map[string]resourceEntry), + resourceTemplates: make(map[string]resourceTemplateEntry), + prompts: make(map[string]mcp.Prompt), + promptHandlers: make(map[string]PromptHandlerFunc), + tools: make(map[string]ServerTool), + name: name, + version: version, + notificationHandlers: make(map[string]NotificationHandlerFunc), + capabilities: serverCapabilities{ + tools: nil, + resources: nil, + prompts: nil, + logging: false, + }, + } + + for _, opt := range opts { + opt(s) + } + + return s +} + +// AddResource registers a new resource and its handler +func (s *MCPServer) AddResource( + resource mcp.Resource, + handler ResourceHandlerFunc, +) { + if s.capabilities.resources == nil { + s.capabilities.resources = &resourceCapabilities{} + } + s.mu.Lock() + defer s.mu.Unlock() + s.resources[resource.URI] = resourceEntry{ + resource: resource, + handler: handler, + } +} + +// AddResourceTemplate registers a new resource template and its handler +func (s *MCPServer) AddResourceTemplate( + template mcp.ResourceTemplate, + handler ResourceTemplateHandlerFunc, +) { + if s.capabilities.resources == nil { + s.capabilities.resources = &resourceCapabilities{} + } + s.mu.Lock() + defer s.mu.Unlock() + s.resourceTemplates[template.URITemplate.Raw()] = resourceTemplateEntry{ + template: template, + handler: handler, + } +} + +// AddPrompt registers a new prompt handler with the given name +func (s *MCPServer) AddPrompt(prompt mcp.Prompt, handler PromptHandlerFunc) { + if s.capabilities.prompts == nil { + s.capabilities.prompts = &promptCapabilities{} + } + s.mu.Lock() + defer s.mu.Unlock() + s.prompts[prompt.Name] = prompt + s.promptHandlers[prompt.Name] = handler +} + +// AddTool registers a new tool and its handler +func (s *MCPServer) AddTool(tool mcp.Tool, handler ToolHandlerFunc) { + s.AddTools(ServerTool{Tool: tool, Handler: handler}) +} + +// AddTools registers multiple tools at once +func (s *MCPServer) AddTools(tools ...ServerTool) { + if s.capabilities.tools == nil { + s.capabilities.tools = &toolCapabilities{} + } + s.mu.Lock() + for _, entry := range tools { + s.tools[entry.Tool.Name] = entry + } + initialized := s.initialized.Load() + s.mu.Unlock() + + // Send notification if server is already initialized + if initialized { + s.sendNotificationToAllClients("notifications/tools/list_changed", nil) + } +} + +// SetTools replaces all existing tools with the provided list +func (s *MCPServer) SetTools(tools ...ServerTool) { + s.mu.Lock() + s.tools = make(map[string]ServerTool) + s.mu.Unlock() + s.AddTools(tools...) +} + +// DeleteTools removes a tool from the server +func (s *MCPServer) DeleteTools(names ...string) { + s.mu.Lock() + for _, name := range names { + delete(s.tools, name) + } + initialized := s.initialized.Load() + s.mu.Unlock() + + // Send notification if server is already initialized + if initialized { + s.sendNotificationToAllClients("notifications/tools/list_changed", nil) + } +} + +// AddNotificationHandler registers a new handler for incoming notifications +func (s *MCPServer) AddNotificationHandler( + method string, + handler NotificationHandlerFunc, +) { + s.mu.Lock() + defer s.mu.Unlock() + s.notificationHandlers[method] = handler +} + +func (s *MCPServer) handleInitialize( + ctx context.Context, + id interface{}, + request mcp.InitializeRequest, +) (*mcp.InitializeResult, *requestError) { + capabilities := mcp.ServerCapabilities{} + + // Only add resource capabilities if they're configured + if s.capabilities.resources != nil { + capabilities.Resources = &struct { + Subscribe bool `json:"subscribe,omitempty"` + ListChanged bool `json:"listChanged,omitempty"` + }{ + Subscribe: s.capabilities.resources.subscribe, + ListChanged: s.capabilities.resources.listChanged, + } + } + + // Only add prompt capabilities if they're configured + if s.capabilities.prompts != nil { + capabilities.Prompts = &struct { + ListChanged bool `json:"listChanged,omitempty"` + }{ + ListChanged: s.capabilities.prompts.listChanged, + } + } + + // Only add tool capabilities if they're configured + if s.capabilities.tools != nil { + capabilities.Tools = &struct { + ListChanged bool `json:"listChanged,omitempty"` + }{ + ListChanged: s.capabilities.tools.listChanged, + } + } + + if s.capabilities.logging { + capabilities.Logging = &struct{}{} + } + + result := mcp.InitializeResult{ + ProtocolVersion: mcp.LATEST_PROTOCOL_VERSION, + ServerInfo: mcp.Implementation{ + Name: s.name, + Version: s.version, + }, + Capabilities: capabilities, + Instructions: s.instructions, + } + + s.initialized.Store(true) + return &result, nil +} + +func (s *MCPServer) handlePing( + ctx context.Context, + id interface{}, + request mcp.PingRequest, +) (*mcp.EmptyResult, *requestError) { + return &mcp.EmptyResult{}, nil +} + +func (s *MCPServer) handleListResources( + ctx context.Context, + id interface{}, + request mcp.ListResourcesRequest, +) (*mcp.ListResourcesResult, *requestError) { + s.mu.RLock() + resources := make([]mcp.Resource, 0, len(s.resources)) + for _, entry := range s.resources { + resources = append(resources, entry.resource) + } + s.mu.RUnlock() + + result := mcp.ListResourcesResult{ + Resources: resources, + } + if request.Params.Cursor != "" { + result.NextCursor = "" // Handle pagination if needed + } + return &result, nil +} + +func (s *MCPServer) handleListResourceTemplates( + ctx context.Context, + id interface{}, + request mcp.ListResourceTemplatesRequest, +) (*mcp.ListResourceTemplatesResult, *requestError) { + s.mu.RLock() + templates := make([]mcp.ResourceTemplate, 0, len(s.resourceTemplates)) + for _, entry := range s.resourceTemplates { + templates = append(templates, entry.template) + } + s.mu.RUnlock() + + result := mcp.ListResourceTemplatesResult{ + ResourceTemplates: templates, + } + if request.Params.Cursor != "" { + result.NextCursor = "" // Handle pagination if needed + } + return &result, nil +} + +func (s *MCPServer) handleReadResource( + ctx context.Context, + id interface{}, + request mcp.ReadResourceRequest, +) (*mcp.ReadResourceResult, *requestError) { + s.mu.RLock() + // First try direct resource handlers + if entry, ok := s.resources[request.Params.URI]; ok { + handler := entry.handler + s.mu.RUnlock() + contents, err := handler(ctx, request) + if err != nil { + return nil, &requestError{ + id: id, + code: mcp.INTERNAL_ERROR, + err: err, + } + } + return &mcp.ReadResourceResult{Contents: contents}, nil + } + + // If no direct handler found, try matching against templates + var matchedHandler ResourceTemplateHandlerFunc + var matched bool + for _, entry := range s.resourceTemplates { + template := entry.template + if matchesTemplate(request.Params.URI, template.URITemplate) { + matchedHandler = entry.handler + matched = true + matchedVars := template.URITemplate.Match(request.Params.URI) + // Convert matched variables to a map + request.Params.Arguments = make(map[string]interface{}) + for name, value := range matchedVars { + request.Params.Arguments[name] = value.V + } + break + } + } + s.mu.RUnlock() + + if matched { + contents, err := matchedHandler(ctx, request) + if err != nil { + return nil, &requestError{ + id: id, + code: mcp.INTERNAL_ERROR, + err: err, + } + } + return &mcp.ReadResourceResult{Contents: contents}, nil + } + + return nil, &requestError{ + id: id, + code: mcp.INVALID_PARAMS, + err: fmt.Errorf("handler not found for resource URI '%s': %w", request.Params.URI, ErrResourceNotFound), + } +} + +// matchesTemplate checks if a URI matches a URI template pattern +func matchesTemplate(uri string, template *mcp.URITemplate) bool { + return template.Regexp().MatchString(uri) +} + +func (s *MCPServer) handleListPrompts( + ctx context.Context, + id interface{}, + request mcp.ListPromptsRequest, +) (*mcp.ListPromptsResult, *requestError) { + s.mu.RLock() + prompts := make([]mcp.Prompt, 0, len(s.prompts)) + for _, prompt := range s.prompts { + prompts = append(prompts, prompt) + } + s.mu.RUnlock() + + result := mcp.ListPromptsResult{ + Prompts: prompts, + } + if request.Params.Cursor != "" { + result.NextCursor = "" // Handle pagination if needed + } + return &result, nil +} + +func (s *MCPServer) handleGetPrompt( + ctx context.Context, + id interface{}, + request mcp.GetPromptRequest, +) (*mcp.GetPromptResult, *requestError) { + s.mu.RLock() + handler, ok := s.promptHandlers[request.Params.Name] + s.mu.RUnlock() + + if !ok { + return nil, &requestError{ + id: id, + code: mcp.INVALID_PARAMS, + err: fmt.Errorf("prompt '%s' not found: %w", request.Params.Name, ErrPromptNotFound), + } + } + + result, err := handler(ctx, request) + if err != nil { + return nil, &requestError{ + id: id, + code: mcp.INTERNAL_ERROR, + err: err, + } + } + + return result, nil +} + +func (s *MCPServer) handleListTools( + ctx context.Context, + id interface{}, + request mcp.ListToolsRequest, +) (*mcp.ListToolsResult, *requestError) { + s.mu.RLock() + tools := make([]mcp.Tool, 0, len(s.tools)) + + // Get all tool names for consistent ordering + toolNames := make([]string, 0, len(s.tools)) + for name := range s.tools { + toolNames = append(toolNames, name) + } + + // Sort the tool names for consistent ordering + sort.Strings(toolNames) + + // Add tools in sorted order + for _, name := range toolNames { + tools = append(tools, s.tools[name].Tool) + } + s.mu.RUnlock() + + result := mcp.ListToolsResult{ + Tools: tools, + } + if request.Params.Cursor != "" { + result.NextCursor = "" // Handle pagination if needed + } + return &result, nil +} +func (s *MCPServer) handleToolCall( + ctx context.Context, + id interface{}, + request mcp.CallToolRequest, +) (*mcp.CallToolResult, *requestError) { + s.mu.RLock() + tool, ok := s.tools[request.Params.Name] + s.mu.RUnlock() + + if !ok { + return nil, &requestError{ + id: id, + code: mcp.INVALID_PARAMS, + err: fmt.Errorf("tool '%s' not found: %w", request.Params.Name, ErrToolNotFound), + } + } + + result, err := tool.Handler(ctx, request) + if err != nil { + return nil, &requestError{ + id: id, + code: mcp.INTERNAL_ERROR, + err: err, + } + } + + return result, nil +} + +func (s *MCPServer) handleNotification( + ctx context.Context, + notification mcp.JSONRPCNotification, +) mcp.JSONRPCMessage { + s.mu.RLock() + handler, ok := s.notificationHandlers[notification.Method] + s.mu.RUnlock() + + if ok { + handler(ctx, notification) + } + return nil +} + +func createResponse(id interface{}, result interface{}) mcp.JSONRPCMessage { + return mcp.JSONRPCResponse{ + JSONRPC: mcp.JSONRPC_VERSION, + ID: id, + Result: result, + } +} + +func createErrorResponse( + id interface{}, + code int, + message string, +) mcp.JSONRPCMessage { + return mcp.JSONRPCError{ + JSONRPC: mcp.JSONRPC_VERSION, + ID: id, + Error: struct { + Code int `json:"code"` + Message string `json:"message"` + Data interface{} `json:"data,omitempty"` + }{ + Code: code, + Message: message, + }, + } +} diff --git a/vendor/github.com/mark3labs/mcp-go/server/sse.go b/vendor/github.com/mark3labs/mcp-go/server/sse.go new file mode 100644 index 000000000..c3c070e6c --- /dev/null +++ b/vendor/github.com/mark3labs/mcp-go/server/sse.go @@ -0,0 +1,404 @@ +package server + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "sync" + + "github.com/google/uuid" + "github.com/mark3labs/mcp-go/mcp" +) + +// sseSession represents an active SSE connection. +type sseSession struct { + writer http.ResponseWriter + flusher http.Flusher + done chan struct{} + eventQueue chan string // Channel for queuing events + sessionID string + notificationChannel chan mcp.JSONRPCNotification +} + +// SSEContextFunc is a function that takes an existing context and the current +// request and returns a potentially modified context based on the request +// content. This can be used to inject context values from headers, for example. +type SSEContextFunc func(ctx context.Context, r *http.Request) context.Context + +func (s *sseSession) SessionID() string { + return s.sessionID +} + +func (s *sseSession) NotificationChannel() chan<- mcp.JSONRPCNotification { + return s.notificationChannel +} + +var _ ClientSession = (*sseSession)(nil) + +// SSEServer implements a Server-Sent Events (SSE) based MCP server. +// It provides real-time communication capabilities over HTTP using the SSE protocol. +type SSEServer struct { + server *MCPServer + baseURL string + basePath string + messageEndpoint string + sseEndpoint string + sessions sync.Map + srv *http.Server + contextFunc SSEContextFunc +} + +// SSEOption defines a function type for configuring SSEServer +type SSEOption func(*SSEServer) + +// WithBaseURL sets the base URL for the SSE server +func WithBaseURL(baseURL string) SSEOption { + return func(s *SSEServer) { + if baseURL != "" { + u, err := url.Parse(baseURL) + if err != nil { + return + } + if u.Scheme != "http" && u.Scheme != "https" { + return + } + // Check if the host is empty or only contains a port + if u.Host == "" || strings.HasPrefix(u.Host, ":") { + return + } + if len(u.Query()) > 0 { + return + } + } + s.baseURL = strings.TrimSuffix(baseURL, "/") + } +} + +// Add a new option for setting base path +func WithBasePath(basePath string) SSEOption { + return func(s *SSEServer) { + // Ensure the path starts with / and doesn't end with / + if !strings.HasPrefix(basePath, "/") { + basePath = "/" + basePath + } + s.basePath = strings.TrimSuffix(basePath, "/") + } +} + +// WithMessageEndpoint sets the message endpoint path +func WithMessageEndpoint(endpoint string) SSEOption { + return func(s *SSEServer) { + s.messageEndpoint = endpoint + } +} + +// WithSSEEndpoint sets the SSE endpoint path +func WithSSEEndpoint(endpoint string) SSEOption { + return func(s *SSEServer) { + s.sseEndpoint = endpoint + } +} + +// WithHTTPServer sets the HTTP server instance +func WithHTTPServer(srv *http.Server) SSEOption { + return func(s *SSEServer) { + s.srv = srv + } +} + +// WithContextFunc sets a function that will be called to customise the context +// to the server using the incoming request. +func WithSSEContextFunc(fn SSEContextFunc) SSEOption { + return func(s *SSEServer) { + s.contextFunc = fn + } +} + +// NewSSEServer creates a new SSE server instance with the given MCP server and options. +func NewSSEServer(server *MCPServer, opts ...SSEOption) *SSEServer { + s := &SSEServer{ + server: server, + sseEndpoint: "/sse", + messageEndpoint: "/message", + } + + // Apply all options + for _, opt := range opts { + opt(s) + } + + return s +} + +// NewTestServer creates a test server for testing purposes +func NewTestServer(server *MCPServer, opts ...SSEOption) *httptest.Server { + sseServer := NewSSEServer(server) + for _, opt := range opts { + opt(sseServer) + } + + testServer := httptest.NewServer(sseServer) + sseServer.baseURL = testServer.URL + return testServer +} + +// Start begins serving SSE connections on the specified address. +// It sets up HTTP handlers for SSE and message endpoints. +func (s *SSEServer) Start(addr string) error { + s.srv = &http.Server{ + Addr: addr, + Handler: s, + } + + return s.srv.ListenAndServe() +} + +// Shutdown gracefully stops the SSE server, closing all active sessions +// and shutting down the HTTP server. +func (s *SSEServer) Shutdown(ctx context.Context) error { + if s.srv != nil { + s.sessions.Range(func(key, value interface{}) bool { + if session, ok := value.(*sseSession); ok { + close(session.done) + } + s.sessions.Delete(key) + return true + }) + + return s.srv.Shutdown(ctx) + } + return nil +} + +// handleSSE handles incoming SSE connection requests. +// It sets up appropriate headers and creates a new session for the client. +func (s *SSEServer) handleSSE(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + w.Header().Set("Content-Type", "text/event-stream") + w.Header().Set("Cache-Control", "no-cache") + w.Header().Set("Connection", "keep-alive") + w.Header().Set("Access-Control-Allow-Origin", "*") + + flusher, ok := w.(http.Flusher) + if !ok { + http.Error(w, "Streaming unsupported", http.StatusInternalServerError) + return + } + + sessionID := uuid.New().String() + session := &sseSession{ + writer: w, + flusher: flusher, + done: make(chan struct{}), + eventQueue: make(chan string, 100), // Buffer for events + sessionID: sessionID, + notificationChannel: make(chan mcp.JSONRPCNotification, 100), + } + + s.sessions.Store(sessionID, session) + defer s.sessions.Delete(sessionID) + + if err := s.server.RegisterSession(session); err != nil { + http.Error(w, fmt.Sprintf("Session registration failed: %v", err), http.StatusInternalServerError) + return + } + defer s.server.UnregisterSession(sessionID) + + // Start notification handler for this session + go func() { + for { + select { + case notification := <-session.notificationChannel: + eventData, err := json.Marshal(notification) + if err == nil { + select { + case session.eventQueue <- fmt.Sprintf("event: message\ndata: %s\n\n", eventData): + // Event queued successfully + case <-session.done: + return + } + } + case <-session.done: + return + case <-r.Context().Done(): + return + } + } + }() + + messageEndpoint := fmt.Sprintf("%s?sessionId=%s", s.CompleteMessageEndpoint(), sessionID) + + // Send the initial endpoint event + fmt.Fprintf(w, "event: endpoint\ndata: %s\r\n\r\n", messageEndpoint) + flusher.Flush() + + // Main event loop - this runs in the HTTP handler goroutine + for { + select { + case event := <-session.eventQueue: + // Write the event to the response + fmt.Fprint(w, event) + flusher.Flush() + case <-r.Context().Done(): + close(session.done) + return + } + } +} + +// handleMessage processes incoming JSON-RPC messages from clients and sends responses +// back through both the SSE connection and HTTP response. +func (s *SSEServer) handleMessage(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + s.writeJSONRPCError(w, nil, mcp.INVALID_REQUEST, "Method not allowed") + return + } + + sessionID := r.URL.Query().Get("sessionId") + if sessionID == "" { + s.writeJSONRPCError(w, nil, mcp.INVALID_PARAMS, "Missing sessionId") + return + } + + sessionI, ok := s.sessions.Load(sessionID) + if !ok { + s.writeJSONRPCError(w, nil, mcp.INVALID_PARAMS, "Invalid session ID") + return + } + session := sessionI.(*sseSession) + + // Set the client context before handling the message + ctx := s.server.WithContext(r.Context(), session) + if s.contextFunc != nil { + ctx = s.contextFunc(ctx, r) + } + + // Parse message as raw JSON + var rawMessage json.RawMessage + if err := json.NewDecoder(r.Body).Decode(&rawMessage); err != nil { + s.writeJSONRPCError(w, nil, mcp.PARSE_ERROR, "Parse error") + return + } + + // Process message through MCPServer + response := s.server.HandleMessage(ctx, rawMessage) + + // Only send response if there is one (not for notifications) + if response != nil { + eventData, _ := json.Marshal(response) + + // Queue the event for sending via SSE + select { + case session.eventQueue <- fmt.Sprintf("event: message\ndata: %s\n\n", eventData): + // Event queued successfully + case <-session.done: + // Session is closed, don't try to queue + default: + // Queue is full, could log this + } + + // Send HTTP response + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) + json.NewEncoder(w).Encode(response) + } else { + // For notifications, just send 202 Accepted with no body + w.WriteHeader(http.StatusAccepted) + } +} + +// writeJSONRPCError writes a JSON-RPC error response with the given error details. +func (s *SSEServer) writeJSONRPCError( + w http.ResponseWriter, + id interface{}, + code int, + message string, +) { + response := createErrorResponse(id, code, message) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(response) +} + +// SendEventToSession sends an event to a specific SSE session identified by sessionID. +// Returns an error if the session is not found or closed. +func (s *SSEServer) SendEventToSession( + sessionID string, + event interface{}, +) error { + sessionI, ok := s.sessions.Load(sessionID) + if !ok { + return fmt.Errorf("session not found: %s", sessionID) + } + session := sessionI.(*sseSession) + + eventData, err := json.Marshal(event) + if err != nil { + return err + } + + // Queue the event for sending via SSE + select { + case session.eventQueue <- fmt.Sprintf("event: message\ndata: %s\n\n", eventData): + return nil + case <-session.done: + return fmt.Errorf("session closed") + default: + return fmt.Errorf("event queue full") + } +} +func (s *SSEServer) GetUrlPath(input string) (string, error) { + parse, err := url.Parse(input) + if err != nil { + return "", fmt.Errorf("failed to parse URL %s: %w", input, err) + } + return parse.Path, nil +} + +func (s *SSEServer) CompleteSseEndpoint() string { + return s.baseURL + s.basePath + s.sseEndpoint +} +func (s *SSEServer) CompleteSsePath() string { + path, err := s.GetUrlPath(s.CompleteSseEndpoint()) + if err != nil { + return s.basePath + s.sseEndpoint + } + return path +} + +func (s *SSEServer) CompleteMessageEndpoint() string { + return s.baseURL + s.basePath + s.messageEndpoint +} +func (s *SSEServer) CompleteMessagePath() string { + path, err := s.GetUrlPath(s.CompleteMessageEndpoint()) + if err != nil { + return s.basePath + s.messageEndpoint + } + return path +} + +// ServeHTTP implements the http.Handler interface. +func (s *SSEServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { + path := r.URL.Path + // Use exact path matching rather than Contains + ssePath := s.CompleteSsePath() + if ssePath != "" && path == ssePath { + s.handleSSE(w, r) + return + } + messagePath := s.CompleteMessagePath() + if messagePath != "" && path == messagePath { + s.handleMessage(w, r) + return + } + + http.NotFound(w, r) +} diff --git a/vendor/github.com/mark3labs/mcp-go/server/stdio.go b/vendor/github.com/mark3labs/mcp-go/server/stdio.go new file mode 100644 index 000000000..441c50b99 --- /dev/null +++ b/vendor/github.com/mark3labs/mcp-go/server/stdio.go @@ -0,0 +1,250 @@ +package server + +import ( + "bufio" + "context" + "encoding/json" + "fmt" + "io" + "log" + "os" + "os/signal" + "syscall" + + "github.com/mark3labs/mcp-go/mcp" +) + +// StdioContextFunc is a function that takes an existing context and returns +// a potentially modified context. +// This can be used to inject context values from environment variables, +// for example. +type StdioContextFunc func(ctx context.Context) context.Context + +// StdioServer wraps a MCPServer and handles stdio communication. +// It provides a simple way to create command-line MCP servers that +// communicate via standard input/output streams using JSON-RPC messages. +type StdioServer struct { + server *MCPServer + errLogger *log.Logger + contextFunc StdioContextFunc +} + +// StdioOption defines a function type for configuring StdioServer +type StdioOption func(*StdioServer) + +// WithErrorLogger sets the error logger for the server +func WithErrorLogger(logger *log.Logger) StdioOption { + return func(s *StdioServer) { + s.errLogger = logger + } +} + +// WithContextFunc sets a function that will be called to customise the context +// to the server. Note that the stdio server uses the same context for all requests, +// so this function will only be called once per server instance. +func WithStdioContextFunc(fn StdioContextFunc) StdioOption { + return func(s *StdioServer) { + s.contextFunc = fn + } +} + +// stdioSession is a static client session, since stdio has only one client. +type stdioSession struct { + notifications chan mcp.JSONRPCNotification +} + +func (s *stdioSession) SessionID() string { + return "stdio" +} + +func (s *stdioSession) NotificationChannel() chan<- mcp.JSONRPCNotification { + return s.notifications +} + +var _ ClientSession = (*stdioSession)(nil) + +var stdioSessionInstance = stdioSession{ + notifications: make(chan mcp.JSONRPCNotification, 100), +} + +// NewStdioServer creates a new stdio server wrapper around an MCPServer. +// It initializes the server with a default error logger that discards all output. +func NewStdioServer(server *MCPServer) *StdioServer { + return &StdioServer{ + server: server, + errLogger: log.New( + os.Stderr, + "", + log.LstdFlags, + ), // Default to discarding logs + } +} + +// SetErrorLogger configures where error messages from the StdioServer are logged. +// The provided logger will receive all error messages generated during server operation. +func (s *StdioServer) SetErrorLogger(logger *log.Logger) { + s.errLogger = logger +} + +// SetContextFunc sets a function that will be called to customise the context +// to the server. Note that the stdio server uses the same context for all requests, +// so this function will only be called once per server instance. +func (s *StdioServer) SetContextFunc(fn StdioContextFunc) { + s.contextFunc = fn +} + +// Listen starts listening for JSON-RPC messages on the provided input and writes responses to the provided output. +// It runs until the context is cancelled or an error occurs. +// Returns an error if there are issues with reading input or writing output. +func (s *StdioServer) Listen( + ctx context.Context, + stdin io.Reader, + stdout io.Writer, +) error { + // Set a static client context since stdio only has one client + if err := s.server.RegisterSession(&stdioSessionInstance); err != nil { + return fmt.Errorf("register session: %w", err) + } + defer s.server.UnregisterSession(stdioSessionInstance.SessionID()) + ctx = s.server.WithContext(ctx, &stdioSessionInstance) + + // Add in any custom context. + if s.contextFunc != nil { + ctx = s.contextFunc(ctx) + } + + reader := bufio.NewReader(stdin) + + // Start notification handler + go func() { + for { + select { + case notification := <-stdioSessionInstance.notifications: + err := s.writeResponse( + notification, + stdout, + ) + if err != nil { + s.errLogger.Printf( + "Error writing notification: %v", + err, + ) + } + case <-ctx.Done(): + return + } + } + }() + + for { + select { + case <-ctx.Done(): + return ctx.Err() + default: + // Use a goroutine to make the read cancellable + readChan := make(chan string, 1) + errChan := make(chan error, 1) + + go func() { + line, err := reader.ReadString('\n') + if err != nil { + errChan <- err + return + } + readChan <- line + }() + + select { + case <-ctx.Done(): + return ctx.Err() + case err := <-errChan: + if err == io.EOF { + return nil + } + s.errLogger.Printf("Error reading input: %v", err) + return err + case line := <-readChan: + if err := s.processMessage(ctx, line, stdout); err != nil { + if err == io.EOF { + return nil + } + s.errLogger.Printf("Error handling message: %v", err) + return err + } + } + } + } +} + +// processMessage handles a single JSON-RPC message and writes the response. +// It parses the message, processes it through the wrapped MCPServer, and writes any response. +// Returns an error if there are issues with message processing or response writing. +func (s *StdioServer) processMessage( + ctx context.Context, + line string, + writer io.Writer, +) error { + // Parse the message as raw JSON + var rawMessage json.RawMessage + if err := json.Unmarshal([]byte(line), &rawMessage); err != nil { + response := createErrorResponse(nil, mcp.PARSE_ERROR, "Parse error") + return s.writeResponse(response, writer) + } + + // Handle the message using the wrapped server + response := s.server.HandleMessage(ctx, rawMessage) + + // Only write response if there is one (not for notifications) + if response != nil { + if err := s.writeResponse(response, writer); err != nil { + return fmt.Errorf("failed to write response: %w", err) + } + } + + return nil +} + +// writeResponse marshals and writes a JSON-RPC response message followed by a newline. +// Returns an error if marshaling or writing fails. +func (s *StdioServer) writeResponse( + response mcp.JSONRPCMessage, + writer io.Writer, +) error { + responseBytes, err := json.Marshal(response) + if err != nil { + return err + } + + // Write response followed by newline + if _, err := fmt.Fprintf(writer, "%s\n", responseBytes); err != nil { + return err + } + + return nil +} + +// ServeStdio is a convenience function that creates and starts a StdioServer with os.Stdin and os.Stdout. +// It sets up signal handling for graceful shutdown on SIGTERM and SIGINT. +// Returns an error if the server encounters any issues during operation. +func ServeStdio(server *MCPServer, opts ...StdioOption) error { + s := NewStdioServer(server) + s.SetErrorLogger(log.New(os.Stderr, "", log.LstdFlags)) + + for _, opt := range opts { + opt(s) + } + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // Set up signal handling + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, syscall.SIGTERM, syscall.SIGINT) + + go func() { + <-sigChan + cancel() + }() + + return s.Listen(ctx, os.Stdin, os.Stdout) +} diff --git a/vendor/github.com/yosida95/uritemplate/v3/LICENSE b/vendor/github.com/yosida95/uritemplate/v3/LICENSE new file mode 100644 index 000000000..79e8f8757 --- /dev/null +++ b/vendor/github.com/yosida95/uritemplate/v3/LICENSE @@ -0,0 +1,25 @@ +Copyright (C) 2016, Kohei YOSHIDA . All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/yosida95/uritemplate/v3/README.rst b/vendor/github.com/yosida95/uritemplate/v3/README.rst new file mode 100644 index 000000000..6815d0a46 --- /dev/null +++ b/vendor/github.com/yosida95/uritemplate/v3/README.rst @@ -0,0 +1,46 @@ +uritemplate +=========== + +`uritemplate`_ is a Go implementation of `URI Template`_ [RFC6570] with +full functionality of URI Template Level 4. + +uritemplate can also generate a regexp that matches expansion of the +URI Template from a URI Template. + +Getting Started +--------------- + +Installation +~~~~~~~~~~~~ + +.. code-block:: sh + + $ go get -u github.com/yosida95/uritemplate/v3 + +Documentation +~~~~~~~~~~~~~ + +The documentation is available on GoDoc_. + +Examples +-------- + +See `examples on GoDoc`_. + +License +------- + +`uritemplate`_ is distributed under the BSD 3-Clause license. +PLEASE READ ./LICENSE carefully and follow its clauses to use this software. + +Author +------ + +yosida95_ + + +.. _`URI Template`: https://tools.ietf.org/html/rfc6570 +.. _Godoc: https://godoc.org/github.com/yosida95/uritemplate +.. _`examples on GoDoc`: https://godoc.org/github.com/yosida95/uritemplate#pkg-examples +.. _yosida95: https://yosida95.com/ +.. _uritemplate: https://github.com/yosida95/uritemplate diff --git a/vendor/github.com/yosida95/uritemplate/v3/compile.go b/vendor/github.com/yosida95/uritemplate/v3/compile.go new file mode 100644 index 000000000..bd774d15d --- /dev/null +++ b/vendor/github.com/yosida95/uritemplate/v3/compile.go @@ -0,0 +1,224 @@ +// Copyright (C) 2016 Kohei YOSHIDA. All rights reserved. +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of The BSD 3-Clause License +// that can be found in the LICENSE file. + +package uritemplate + +import ( + "fmt" + "unicode/utf8" +) + +type compiler struct { + prog *prog +} + +func (c *compiler) init() { + c.prog = &prog{} +} + +func (c *compiler) op(opcode progOpcode) uint32 { + i := len(c.prog.op) + c.prog.op = append(c.prog.op, progOp{code: opcode}) + return uint32(i) +} + +func (c *compiler) opWithRune(opcode progOpcode, r rune) uint32 { + addr := c.op(opcode) + (&c.prog.op[addr]).r = r + return addr +} + +func (c *compiler) opWithRuneClass(opcode progOpcode, rc runeClass) uint32 { + addr := c.op(opcode) + (&c.prog.op[addr]).rc = rc + return addr +} + +func (c *compiler) opWithAddr(opcode progOpcode, absaddr uint32) uint32 { + addr := c.op(opcode) + (&c.prog.op[addr]).i = absaddr + return addr +} + +func (c *compiler) opWithAddrDelta(opcode progOpcode, delta uint32) uint32 { + return c.opWithAddr(opcode, uint32(len(c.prog.op))+delta) +} + +func (c *compiler) opWithName(opcode progOpcode, name string) uint32 { + addr := c.op(opcode) + (&c.prog.op[addr]).name = name + return addr +} + +func (c *compiler) compileString(str string) { + for i := 0; i < len(str); { + // NOTE(yosida95): It is confirmed at parse time that literals + // consist of only valid-UTF8 runes. + r, size := utf8.DecodeRuneInString(str[i:]) + c.opWithRune(opRune, r) + i += size + } +} + +func (c *compiler) compileRuneClass(rc runeClass, maxlen int) { + for i := 0; i < maxlen; i++ { + if i > 0 { + c.opWithAddrDelta(opSplit, 7) + } + c.opWithAddrDelta(opSplit, 3) // raw rune or pct-encoded + c.opWithRuneClass(opRuneClass, rc) // raw rune + c.opWithAddrDelta(opJmp, 4) // + c.opWithRune(opRune, '%') // pct-encoded + c.opWithRuneClass(opRuneClass, runeClassPctE) // + c.opWithRuneClass(opRuneClass, runeClassPctE) // + } +} + +func (c *compiler) compileRuneClassInfinite(rc runeClass) { + start := c.opWithAddrDelta(opSplit, 3) // raw rune or pct-encoded + c.opWithRuneClass(opRuneClass, rc) // raw rune + c.opWithAddrDelta(opJmp, 4) // + c.opWithRune(opRune, '%') // pct-encoded + c.opWithRuneClass(opRuneClass, runeClassPctE) // + c.opWithRuneClass(opRuneClass, runeClassPctE) // + c.opWithAddrDelta(opSplit, 2) // loop + c.opWithAddr(opJmp, start) // +} + +func (c *compiler) compileVarspecValue(spec varspec, expr *expression) { + var specname string + if spec.maxlen > 0 { + specname = fmt.Sprintf("%s:%d", spec.name, spec.maxlen) + } else { + specname = spec.name + } + + c.prog.numCap++ + + c.opWithName(opCapStart, specname) + + split := c.op(opSplit) + if spec.maxlen > 0 { + c.compileRuneClass(expr.allow, spec.maxlen) + } else { + c.compileRuneClassInfinite(expr.allow) + } + + capEnd := c.opWithName(opCapEnd, specname) + c.prog.op[split].i = capEnd +} + +func (c *compiler) compileVarspec(spec varspec, expr *expression) { + switch { + case expr.named && spec.explode: + split1 := c.op(opSplit) + noop := c.op(opNoop) + c.compileString(spec.name) + + split2 := c.op(opSplit) + c.opWithRune(opRune, '=') + c.compileVarspecValue(spec, expr) + + split3 := c.op(opSplit) + c.compileString(expr.sep) + c.opWithAddr(opJmp, noop) + + c.prog.op[split2].i = uint32(len(c.prog.op)) + c.compileString(expr.ifemp) + c.opWithAddr(opJmp, split3) + + c.prog.op[split1].i = uint32(len(c.prog.op)) + c.prog.op[split3].i = uint32(len(c.prog.op)) + + case expr.named && !spec.explode: + c.compileString(spec.name) + + split2 := c.op(opSplit) + c.opWithRune(opRune, '=') + + split3 := c.op(opSplit) + + split4 := c.op(opSplit) + c.compileVarspecValue(spec, expr) + + split5 := c.op(opSplit) + c.prog.op[split4].i = split5 + c.compileString(",") + c.opWithAddr(opJmp, split4) + + c.prog.op[split3].i = uint32(len(c.prog.op)) + c.compileString(",") + jmp1 := c.op(opJmp) + + c.prog.op[split2].i = uint32(len(c.prog.op)) + c.compileString(expr.ifemp) + + c.prog.op[split5].i = uint32(len(c.prog.op)) + c.prog.op[jmp1].i = uint32(len(c.prog.op)) + + case !expr.named: + start := uint32(len(c.prog.op)) + c.compileVarspecValue(spec, expr) + + split1 := c.op(opSplit) + jmp := c.op(opJmp) + + c.prog.op[split1].i = uint32(len(c.prog.op)) + if spec.explode { + c.compileString(expr.sep) + } else { + c.opWithRune(opRune, ',') + } + c.opWithAddr(opJmp, start) + + c.prog.op[jmp].i = uint32(len(c.prog.op)) + } +} + +func (c *compiler) compileExpression(expr *expression) { + if len(expr.vars) < 1 { + return + } + + split1 := c.op(opSplit) + c.compileString(expr.first) + + for i, size := 0, len(expr.vars); i < size; i++ { + spec := expr.vars[i] + + split2 := c.op(opSplit) + if i > 0 { + split3 := c.op(opSplit) + c.compileString(expr.sep) + c.prog.op[split3].i = uint32(len(c.prog.op)) + } + c.compileVarspec(spec, expr) + c.prog.op[split2].i = uint32(len(c.prog.op)) + } + + c.prog.op[split1].i = uint32(len(c.prog.op)) +} + +func (c *compiler) compileLiterals(lt literals) { + c.compileString(string(lt)) +} + +func (c *compiler) compile(tmpl *Template) { + c.op(opLineBegin) + for i := range tmpl.exprs { + expr := tmpl.exprs[i] + switch expr := expr.(type) { + default: + panic("unhandled expression") + case *expression: + c.compileExpression(expr) + case literals: + c.compileLiterals(expr) + } + } + c.op(opLineEnd) + c.op(opEnd) +} diff --git a/vendor/github.com/yosida95/uritemplate/v3/equals.go b/vendor/github.com/yosida95/uritemplate/v3/equals.go new file mode 100644 index 000000000..aa59a5c03 --- /dev/null +++ b/vendor/github.com/yosida95/uritemplate/v3/equals.go @@ -0,0 +1,53 @@ +// Copyright (C) 2016 Kohei YOSHIDA. All rights reserved. +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of The BSD 3-Clause License +// that can be found in the LICENSE file. + +package uritemplate + +type CompareFlags uint8 + +const ( + CompareVarname CompareFlags = 1 << iota +) + +// Equals reports whether or not two URI Templates t1 and t2 are equivalent. +func Equals(t1 *Template, t2 *Template, flags CompareFlags) bool { + if len(t1.exprs) != len(t2.exprs) { + return false + } + for i := 0; i < len(t1.exprs); i++ { + switch t1 := t1.exprs[i].(type) { + case literals: + t2, ok := t2.exprs[i].(literals) + if !ok { + return false + } + if t1 != t2 { + return false + } + case *expression: + t2, ok := t2.exprs[i].(*expression) + if !ok { + return false + } + if t1.op != t2.op || len(t1.vars) != len(t2.vars) { + return false + } + for n := 0; n < len(t1.vars); n++ { + v1 := t1.vars[n] + v2 := t2.vars[n] + if flags&CompareVarname == CompareVarname && v1.name != v2.name { + return false + } + if v1.maxlen != v2.maxlen || v1.explode != v2.explode { + return false + } + } + default: + panic("unhandled case") + } + } + return true +} diff --git a/vendor/github.com/yosida95/uritemplate/v3/error.go b/vendor/github.com/yosida95/uritemplate/v3/error.go new file mode 100644 index 000000000..2fd34a808 --- /dev/null +++ b/vendor/github.com/yosida95/uritemplate/v3/error.go @@ -0,0 +1,16 @@ +// Copyright (C) 2016 Kohei YOSHIDA. All rights reserved. +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of The BSD 3-Clause License +// that can be found in the LICENSE file. + +package uritemplate + +import ( + "fmt" +) + +func errorf(pos int, format string, a ...interface{}) error { + msg := fmt.Sprintf(format, a...) + return fmt.Errorf("uritemplate:%d:%s", pos, msg) +} diff --git a/vendor/github.com/yosida95/uritemplate/v3/escape.go b/vendor/github.com/yosida95/uritemplate/v3/escape.go new file mode 100644 index 000000000..6d27e693a --- /dev/null +++ b/vendor/github.com/yosida95/uritemplate/v3/escape.go @@ -0,0 +1,190 @@ +// Copyright (C) 2016 Kohei YOSHIDA. All rights reserved. +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of The BSD 3-Clause License +// that can be found in the LICENSE file. + +package uritemplate + +import ( + "strings" + "unicode" + "unicode/utf8" +) + +var ( + hex = []byte("0123456789ABCDEF") + // reserved = gen-delims / sub-delims + // gen-delims = ":" / "/" / "?" / "#" / "[" / "]" / "@" + // sub-delims = "!" / "$" / "&" / "’" / "(" / ")" + // / "*" / "+" / "," / ";" / "=" + rangeReserved = &unicode.RangeTable{ + R16: []unicode.Range16{ + {Lo: 0x21, Hi: 0x21, Stride: 1}, // '!' + {Lo: 0x23, Hi: 0x24, Stride: 1}, // '#' - '$' + {Lo: 0x26, Hi: 0x2C, Stride: 1}, // '&' - ',' + {Lo: 0x2F, Hi: 0x2F, Stride: 1}, // '/' + {Lo: 0x3A, Hi: 0x3B, Stride: 1}, // ':' - ';' + {Lo: 0x3D, Hi: 0x3D, Stride: 1}, // '=' + {Lo: 0x3F, Hi: 0x40, Stride: 1}, // '?' - '@' + {Lo: 0x5B, Hi: 0x5B, Stride: 1}, // '[' + {Lo: 0x5D, Hi: 0x5D, Stride: 1}, // ']' + }, + LatinOffset: 9, + } + reReserved = `\x21\x23\x24\x26-\x2c\x2f\x3a\x3b\x3d\x3f\x40\x5b\x5d` + // ALPHA = %x41-5A / %x61-7A + // DIGIT = %x30-39 + // unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" + rangeUnreserved = &unicode.RangeTable{ + R16: []unicode.Range16{ + {Lo: 0x2D, Hi: 0x2E, Stride: 1}, // '-' - '.' + {Lo: 0x30, Hi: 0x39, Stride: 1}, // '0' - '9' + {Lo: 0x41, Hi: 0x5A, Stride: 1}, // 'A' - 'Z' + {Lo: 0x5F, Hi: 0x5F, Stride: 1}, // '_' + {Lo: 0x61, Hi: 0x7A, Stride: 1}, // 'a' - 'z' + {Lo: 0x7E, Hi: 0x7E, Stride: 1}, // '~' + }, + } + reUnreserved = `\x2d\x2e\x30-\x39\x41-\x5a\x5f\x61-\x7a\x7e` +) + +type runeClass uint8 + +const ( + runeClassU runeClass = 1 << iota + runeClassR + runeClassPctE + runeClassLast + + runeClassUR = runeClassU | runeClassR +) + +var runeClassNames = []string{ + "U", + "R", + "pct-encoded", +} + +func (rc runeClass) String() string { + ret := make([]string, 0, len(runeClassNames)) + for i, j := 0, runeClass(1); j < runeClassLast; j <<= 1 { + if rc&j == j { + ret = append(ret, runeClassNames[i]) + } + i++ + } + return strings.Join(ret, "+") +} + +func pctEncode(w *strings.Builder, r rune) { + if s := r >> 24 & 0xff; s > 0 { + w.Write([]byte{'%', hex[s/16], hex[s%16]}) + } + if s := r >> 16 & 0xff; s > 0 { + w.Write([]byte{'%', hex[s/16], hex[s%16]}) + } + if s := r >> 8 & 0xff; s > 0 { + w.Write([]byte{'%', hex[s/16], hex[s%16]}) + } + if s := r & 0xff; s > 0 { + w.Write([]byte{'%', hex[s/16], hex[s%16]}) + } +} + +func unhex(c byte) byte { + switch { + case '0' <= c && c <= '9': + return c - '0' + case 'a' <= c && c <= 'f': + return c - 'a' + 10 + case 'A' <= c && c <= 'F': + return c - 'A' + 10 + } + return 0 +} + +func ishex(c byte) bool { + switch { + case '0' <= c && c <= '9': + return true + case 'a' <= c && c <= 'f': + return true + case 'A' <= c && c <= 'F': + return true + default: + return false + } +} + +func pctDecode(s string) string { + size := len(s) + for i := 0; i < len(s); { + switch s[i] { + case '%': + size -= 2 + i += 3 + default: + i++ + } + } + if size == len(s) { + return s + } + + buf := make([]byte, size) + j := 0 + for i := 0; i < len(s); { + switch c := s[i]; c { + case '%': + buf[j] = unhex(s[i+1])<<4 | unhex(s[i+2]) + i += 3 + j++ + default: + buf[j] = c + i++ + j++ + } + } + return string(buf) +} + +type escapeFunc func(*strings.Builder, string) error + +func escapeLiteral(w *strings.Builder, v string) error { + w.WriteString(v) + return nil +} + +func escapeExceptU(w *strings.Builder, v string) error { + for i := 0; i < len(v); { + r, size := utf8.DecodeRuneInString(v[i:]) + if r == utf8.RuneError { + return errorf(i, "invalid encoding") + } + if unicode.Is(rangeUnreserved, r) { + w.WriteRune(r) + } else { + pctEncode(w, r) + } + i += size + } + return nil +} + +func escapeExceptUR(w *strings.Builder, v string) error { + for i := 0; i < len(v); { + r, size := utf8.DecodeRuneInString(v[i:]) + if r == utf8.RuneError { + return errorf(i, "invalid encoding") + } + // TODO(yosida95): is pct-encoded triplets allowed here? + if unicode.In(r, rangeUnreserved, rangeReserved) { + w.WriteRune(r) + } else { + pctEncode(w, r) + } + i += size + } + return nil +} diff --git a/vendor/github.com/yosida95/uritemplate/v3/expression.go b/vendor/github.com/yosida95/uritemplate/v3/expression.go new file mode 100644 index 000000000..4858c2dde --- /dev/null +++ b/vendor/github.com/yosida95/uritemplate/v3/expression.go @@ -0,0 +1,173 @@ +// Copyright (C) 2016 Kohei YOSHIDA. All rights reserved. +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of The BSD 3-Clause License +// that can be found in the LICENSE file. + +package uritemplate + +import ( + "regexp" + "strconv" + "strings" +) + +type template interface { + expand(*strings.Builder, Values) error + regexp(*strings.Builder) +} + +type literals string + +func (l literals) expand(b *strings.Builder, _ Values) error { + b.WriteString(string(l)) + return nil +} + +func (l literals) regexp(b *strings.Builder) { + b.WriteString("(?:") + b.WriteString(regexp.QuoteMeta(string(l))) + b.WriteByte(')') +} + +type varspec struct { + name string + maxlen int + explode bool +} + +type expression struct { + vars []varspec + op parseOp + first string + sep string + named bool + ifemp string + escape escapeFunc + allow runeClass +} + +func (e *expression) init() { + switch e.op { + case parseOpSimple: + e.sep = "," + e.escape = escapeExceptU + e.allow = runeClassU + case parseOpPlus: + e.sep = "," + e.escape = escapeExceptUR + e.allow = runeClassUR + case parseOpCrosshatch: + e.first = "#" + e.sep = "," + e.escape = escapeExceptUR + e.allow = runeClassUR + case parseOpDot: + e.first = "." + e.sep = "." + e.escape = escapeExceptU + e.allow = runeClassU + case parseOpSlash: + e.first = "/" + e.sep = "/" + e.escape = escapeExceptU + e.allow = runeClassU + case parseOpSemicolon: + e.first = ";" + e.sep = ";" + e.named = true + e.escape = escapeExceptU + e.allow = runeClassU + case parseOpQuestion: + e.first = "?" + e.sep = "&" + e.named = true + e.ifemp = "=" + e.escape = escapeExceptU + e.allow = runeClassU + case parseOpAmpersand: + e.first = "&" + e.sep = "&" + e.named = true + e.ifemp = "=" + e.escape = escapeExceptU + e.allow = runeClassU + } +} + +func (e *expression) expand(w *strings.Builder, values Values) error { + first := true + for _, varspec := range e.vars { + value := values.Get(varspec.name) + if !value.Valid() { + continue + } + + if first { + w.WriteString(e.first) + first = false + } else { + w.WriteString(e.sep) + } + + if err := value.expand(w, varspec, e); err != nil { + return err + } + + } + return nil +} + +func (e *expression) regexp(b *strings.Builder) { + if e.first != "" { + b.WriteString("(?:") // $1 + b.WriteString(regexp.QuoteMeta(e.first)) + } + b.WriteByte('(') // $2 + runeClassToRegexp(b, e.allow, e.named || e.vars[0].explode) + if len(e.vars) > 1 || e.vars[0].explode { + max := len(e.vars) - 1 + for i := 0; i < len(e.vars); i++ { + if e.vars[i].explode { + max = -1 + break + } + } + + b.WriteString("(?:") // $3 + b.WriteString(regexp.QuoteMeta(e.sep)) + runeClassToRegexp(b, e.allow, e.named || max < 0) + b.WriteByte(')') // $3 + if max > 0 { + b.WriteString("{0,") + b.WriteString(strconv.Itoa(max)) + b.WriteByte('}') + } else { + b.WriteByte('*') + } + } + b.WriteByte(')') // $2 + if e.first != "" { + b.WriteByte(')') // $1 + } + b.WriteByte('?') +} + +func runeClassToRegexp(b *strings.Builder, class runeClass, named bool) { + b.WriteString("(?:(?:[") + if class&runeClassR == 0 { + b.WriteString(`\x2c`) + if named { + b.WriteString(`\x3d`) + } + } + if class&runeClassU == runeClassU { + b.WriteString(reUnreserved) + } + if class&runeClassR == runeClassR { + b.WriteString(reReserved) + } + b.WriteString("]") + b.WriteString("|%[[:xdigit:]][[:xdigit:]]") + b.WriteString(")*)") +} diff --git a/vendor/github.com/yosida95/uritemplate/v3/machine.go b/vendor/github.com/yosida95/uritemplate/v3/machine.go new file mode 100644 index 000000000..7b1d0b518 --- /dev/null +++ b/vendor/github.com/yosida95/uritemplate/v3/machine.go @@ -0,0 +1,23 @@ +// Copyright (C) 2016 Kohei YOSHIDA. All rights reserved. +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of The BSD 3-Clause License +// that can be found in the LICENSE file. + +package uritemplate + +// threadList implements https://research.swtch.com/sparse. +type threadList struct { + dense []threadEntry + sparse []uint32 +} + +type threadEntry struct { + pc uint32 + t *thread +} + +type thread struct { + op *progOp + cap map[string][]int +} diff --git a/vendor/github.com/yosida95/uritemplate/v3/match.go b/vendor/github.com/yosida95/uritemplate/v3/match.go new file mode 100644 index 000000000..02fe6385a --- /dev/null +++ b/vendor/github.com/yosida95/uritemplate/v3/match.go @@ -0,0 +1,213 @@ +// Copyright (C) 2016 Kohei YOSHIDA. All rights reserved. +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of The BSD 3-Clause License +// that can be found in the LICENSE file. + +package uritemplate + +import ( + "bytes" + "unicode" + "unicode/utf8" +) + +type matcher struct { + prog *prog + + list1 threadList + list2 threadList + matched bool + cap map[string][]int + + input string +} + +func (m *matcher) at(pos int) (rune, int, bool) { + if l := len(m.input); pos < l { + c := m.input[pos] + if c < utf8.RuneSelf { + return rune(c), 1, pos+1 < l + } + r, size := utf8.DecodeRuneInString(m.input[pos:]) + return r, size, pos+size < l + } + return -1, 0, false +} + +func (m *matcher) add(list *threadList, pc uint32, pos int, next bool, cap map[string][]int) { + if i := list.sparse[pc]; i < uint32(len(list.dense)) && list.dense[i].pc == pc { + return + } + + n := len(list.dense) + list.dense = list.dense[:n+1] + list.sparse[pc] = uint32(n) + + e := &list.dense[n] + e.pc = pc + e.t = nil + + op := &m.prog.op[pc] + switch op.code { + default: + panic("unhandled opcode") + case opRune, opRuneClass, opEnd: + e.t = &thread{ + op: &m.prog.op[pc], + cap: make(map[string][]int, len(m.cap)), + } + for k, v := range cap { + e.t.cap[k] = make([]int, len(v)) + copy(e.t.cap[k], v) + } + case opLineBegin: + if pos == 0 { + m.add(list, pc+1, pos, next, cap) + } + case opLineEnd: + if !next { + m.add(list, pc+1, pos, next, cap) + } + case opCapStart, opCapEnd: + ocap := make(map[string][]int, len(m.cap)) + for k, v := range cap { + ocap[k] = make([]int, len(v)) + copy(ocap[k], v) + } + ocap[op.name] = append(ocap[op.name], pos) + m.add(list, pc+1, pos, next, ocap) + case opSplit: + m.add(list, pc+1, pos, next, cap) + m.add(list, op.i, pos, next, cap) + case opJmp: + m.add(list, op.i, pos, next, cap) + case opJmpIfNotDefined: + m.add(list, pc+1, pos, next, cap) + m.add(list, op.i, pos, next, cap) + case opJmpIfNotFirst: + m.add(list, pc+1, pos, next, cap) + m.add(list, op.i, pos, next, cap) + case opJmpIfNotEmpty: + m.add(list, op.i, pos, next, cap) + m.add(list, pc+1, pos, next, cap) + case opNoop: + m.add(list, pc+1, pos, next, cap) + } +} + +func (m *matcher) step(clist *threadList, nlist *threadList, r rune, pos int, nextPos int, next bool) { + debug.Printf("===== %q =====", string(r)) + for i := 0; i < len(clist.dense); i++ { + e := clist.dense[i] + if debug { + var buf bytes.Buffer + dumpProg(&buf, m.prog, e.pc) + debug.Printf("\n%s", buf.String()) + } + if e.t == nil { + continue + } + + t := e.t + op := t.op + switch op.code { + default: + panic("unhandled opcode") + case opRune: + if op.r == r { + m.add(nlist, e.pc+1, nextPos, next, t.cap) + } + case opRuneClass: + ret := false + if !ret && op.rc&runeClassU == runeClassU { + ret = ret || unicode.Is(rangeUnreserved, r) + } + if !ret && op.rc&runeClassR == runeClassR { + ret = ret || unicode.Is(rangeReserved, r) + } + if !ret && op.rc&runeClassPctE == runeClassPctE { + ret = ret || unicode.Is(unicode.ASCII_Hex_Digit, r) + } + if ret { + m.add(nlist, e.pc+1, nextPos, next, t.cap) + } + case opEnd: + m.matched = true + for k, v := range t.cap { + m.cap[k] = make([]int, len(v)) + copy(m.cap[k], v) + } + clist.dense = clist.dense[:0] + } + } + clist.dense = clist.dense[:0] +} + +func (m *matcher) match() bool { + pos := 0 + clist, nlist := &m.list1, &m.list2 + for { + if len(clist.dense) == 0 && m.matched { + break + } + r, width, next := m.at(pos) + if !m.matched { + m.add(clist, 0, pos, next, m.cap) + } + m.step(clist, nlist, r, pos, pos+width, next) + + if width < 1 { + break + } + pos += width + + clist, nlist = nlist, clist + } + return m.matched +} + +func (tmpl *Template) Match(expansion string) Values { + tmpl.mu.Lock() + if tmpl.prog == nil { + c := compiler{} + c.init() + c.compile(tmpl) + tmpl.prog = c.prog + } + prog := tmpl.prog + tmpl.mu.Unlock() + + n := len(prog.op) + m := matcher{ + prog: prog, + list1: threadList{ + dense: make([]threadEntry, 0, n), + sparse: make([]uint32, n), + }, + list2: threadList{ + dense: make([]threadEntry, 0, n), + sparse: make([]uint32, n), + }, + cap: make(map[string][]int, prog.numCap), + input: expansion, + } + if !m.match() { + return nil + } + + match := make(Values, len(m.cap)) + for name, indices := range m.cap { + v := Value{V: make([]string, len(indices)/2)} + for i := range v.V { + v.V[i] = pctDecode(expansion[indices[2*i]:indices[2*i+1]]) + } + if len(v.V) == 1 { + v.T = ValueTypeString + } else { + v.T = ValueTypeList + } + match[name] = v + } + return match +} diff --git a/vendor/github.com/yosida95/uritemplate/v3/parse.go b/vendor/github.com/yosida95/uritemplate/v3/parse.go new file mode 100644 index 000000000..fd38a682f --- /dev/null +++ b/vendor/github.com/yosida95/uritemplate/v3/parse.go @@ -0,0 +1,277 @@ +// Copyright (C) 2016 Kohei YOSHIDA. All rights reserved. +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of The BSD 3-Clause License +// that can be found in the LICENSE file. + +package uritemplate + +import ( + "fmt" + "unicode" + "unicode/utf8" +) + +type parseOp int + +const ( + parseOpSimple parseOp = iota + parseOpPlus + parseOpCrosshatch + parseOpDot + parseOpSlash + parseOpSemicolon + parseOpQuestion + parseOpAmpersand +) + +var ( + rangeVarchar = &unicode.RangeTable{ + R16: []unicode.Range16{ + {Lo: 0x0030, Hi: 0x0039, Stride: 1}, // '0' - '9' + {Lo: 0x0041, Hi: 0x005A, Stride: 1}, // 'A' - 'Z' + {Lo: 0x005F, Hi: 0x005F, Stride: 1}, // '_' + {Lo: 0x0061, Hi: 0x007A, Stride: 1}, // 'a' - 'z' + }, + LatinOffset: 4, + } + rangeLiterals = &unicode.RangeTable{ + R16: []unicode.Range16{ + {Lo: 0x0021, Hi: 0x0021, Stride: 1}, // '!' + {Lo: 0x0023, Hi: 0x0024, Stride: 1}, // '#' - '$' + {Lo: 0x0026, Hi: 0x003B, Stride: 1}, // '&' ''' '(' - ';'. '''/27 used to be excluded but an errata is in the review process https://www.rfc-editor.org/errata/eid6937 + {Lo: 0x003D, Hi: 0x003D, Stride: 1}, // '=' + {Lo: 0x003F, Hi: 0x005B, Stride: 1}, // '?' - '[' + {Lo: 0x005D, Hi: 0x005D, Stride: 1}, // ']' + {Lo: 0x005F, Hi: 0x005F, Stride: 1}, // '_' + {Lo: 0x0061, Hi: 0x007A, Stride: 1}, // 'a' - 'z' + {Lo: 0x007E, Hi: 0x007E, Stride: 1}, // '~' + {Lo: 0x00A0, Hi: 0xD7FF, Stride: 1}, // ucschar + {Lo: 0xE000, Hi: 0xF8FF, Stride: 1}, // iprivate + {Lo: 0xF900, Hi: 0xFDCF, Stride: 1}, // ucschar + {Lo: 0xFDF0, Hi: 0xFFEF, Stride: 1}, // ucschar + }, + R32: []unicode.Range32{ + {Lo: 0x00010000, Hi: 0x0001FFFD, Stride: 1}, // ucschar + {Lo: 0x00020000, Hi: 0x0002FFFD, Stride: 1}, // ucschar + {Lo: 0x00030000, Hi: 0x0003FFFD, Stride: 1}, // ucschar + {Lo: 0x00040000, Hi: 0x0004FFFD, Stride: 1}, // ucschar + {Lo: 0x00050000, Hi: 0x0005FFFD, Stride: 1}, // ucschar + {Lo: 0x00060000, Hi: 0x0006FFFD, Stride: 1}, // ucschar + {Lo: 0x00070000, Hi: 0x0007FFFD, Stride: 1}, // ucschar + {Lo: 0x00080000, Hi: 0x0008FFFD, Stride: 1}, // ucschar + {Lo: 0x00090000, Hi: 0x0009FFFD, Stride: 1}, // ucschar + {Lo: 0x000A0000, Hi: 0x000AFFFD, Stride: 1}, // ucschar + {Lo: 0x000B0000, Hi: 0x000BFFFD, Stride: 1}, // ucschar + {Lo: 0x000C0000, Hi: 0x000CFFFD, Stride: 1}, // ucschar + {Lo: 0x000D0000, Hi: 0x000DFFFD, Stride: 1}, // ucschar + {Lo: 0x000E1000, Hi: 0x000EFFFD, Stride: 1}, // ucschar + {Lo: 0x000F0000, Hi: 0x000FFFFD, Stride: 1}, // iprivate + {Lo: 0x00100000, Hi: 0x0010FFFD, Stride: 1}, // iprivate + }, + LatinOffset: 10, + } +) + +type parser struct { + r string + start int + stop int + state parseState +} + +func (p *parser) errorf(i rune, format string, a ...interface{}) error { + return fmt.Errorf("%s: %s%s", fmt.Sprintf(format, a...), p.r[0:p.stop], string(i)) +} + +func (p *parser) rune() (rune, int) { + r, size := utf8.DecodeRuneInString(p.r[p.stop:]) + if r != utf8.RuneError { + p.stop += size + } + return r, size +} + +func (p *parser) unread(r rune) { + p.stop -= utf8.RuneLen(r) +} + +type parseState int + +const ( + parseStateDefault = parseState(iota) + parseStateOperator + parseStateVarList + parseStateVarName + parseStatePrefix +) + +func (p *parser) setState(state parseState) { + p.state = state + p.start = p.stop +} + +func (p *parser) parseURITemplate() (*Template, error) { + tmpl := Template{ + raw: p.r, + exprs: []template{}, + } + + var exp *expression + for { + r, size := p.rune() + if r == utf8.RuneError { + if size == 0 { + if p.state != parseStateDefault { + return nil, p.errorf('_', "incomplete expression") + } + if p.start < p.stop { + tmpl.exprs = append(tmpl.exprs, literals(p.r[p.start:p.stop])) + } + return &tmpl, nil + } + return nil, p.errorf('_', "invalid UTF-8 sequence") + } + + switch p.state { + case parseStateDefault: + switch r { + case '{': + if stop := p.stop - size; stop > p.start { + tmpl.exprs = append(tmpl.exprs, literals(p.r[p.start:stop])) + } + exp = &expression{} + tmpl.exprs = append(tmpl.exprs, exp) + p.setState(parseStateOperator) + case '%': + p.unread(r) + if err := p.consumeTriplet(); err != nil { + return nil, err + } + default: + if !unicode.Is(rangeLiterals, r) { + p.unread(r) + return nil, p.errorf('_', "unacceptable character (hint: use %%XX encoding)") + } + } + case parseStateOperator: + switch r { + default: + p.unread(r) + exp.op = parseOpSimple + case '+': + exp.op = parseOpPlus + case '#': + exp.op = parseOpCrosshatch + case '.': + exp.op = parseOpDot + case '/': + exp.op = parseOpSlash + case ';': + exp.op = parseOpSemicolon + case '?': + exp.op = parseOpQuestion + case '&': + exp.op = parseOpAmpersand + case '=', ',', '!', '@', '|': // op-reserved + return nil, p.errorf('|', "unimplemented operator (op-reserved)") + } + p.setState(parseStateVarName) + case parseStateVarList: + switch r { + case ',': + p.setState(parseStateVarName) + case '}': + exp.init() + p.setState(parseStateDefault) + default: + p.unread(r) + return nil, p.errorf('_', "unrecognized value modifier") + } + case parseStateVarName: + switch r { + case ':', '*': + name := p.r[p.start : p.stop-size] + if !isValidVarname(name) { + return nil, p.errorf('|', "unacceptable variable name") + } + explode := r == '*' + exp.vars = append(exp.vars, varspec{ + name: name, + explode: explode, + }) + if explode { + p.setState(parseStateVarList) + } else { + p.setState(parseStatePrefix) + } + case ',', '}': + p.unread(r) + name := p.r[p.start:p.stop] + if !isValidVarname(name) { + return nil, p.errorf('|', "unacceptable variable name") + } + exp.vars = append(exp.vars, varspec{ + name: name, + }) + p.setState(parseStateVarList) + case '%': + p.unread(r) + if err := p.consumeTriplet(); err != nil { + return nil, err + } + case '.': + if dot := p.stop - size; dot == p.start || p.r[dot-1] == '.' { + return nil, p.errorf('|', "unacceptable variable name") + } + default: + if !unicode.Is(rangeVarchar, r) { + p.unread(r) + return nil, p.errorf('_', "unacceptable variable name") + } + } + case parseStatePrefix: + spec := &(exp.vars[len(exp.vars)-1]) + switch { + case '0' <= r && r <= '9': + spec.maxlen *= 10 + spec.maxlen += int(r - '0') + if spec.maxlen == 0 || spec.maxlen > 9999 { + return nil, p.errorf('|', "max-length must be (0, 9999]") + } + default: + p.unread(r) + if spec.maxlen == 0 { + return nil, p.errorf('_', "max-length must be (0, 9999]") + } + p.setState(parseStateVarList) + } + default: + p.unread(r) + panic(p.errorf('_', "unhandled parseState(%d)", p.state)) + } + } +} + +func isValidVarname(name string) bool { + if l := len(name); l == 0 || name[0] == '.' || name[l-1] == '.' { + return false + } + for i := 1; i < len(name)-1; i++ { + switch c := name[i]; c { + case '.': + if name[i-1] == '.' { + return false + } + } + } + return true +} + +func (p *parser) consumeTriplet() error { + if len(p.r)-p.stop < 3 || p.r[p.stop] != '%' || !ishex(p.r[p.stop+1]) || !ishex(p.r[p.stop+2]) { + return p.errorf('_', "incomplete pct-encodeed") + } + p.stop += 3 + return nil +} diff --git a/vendor/github.com/yosida95/uritemplate/v3/prog.go b/vendor/github.com/yosida95/uritemplate/v3/prog.go new file mode 100644 index 000000000..97af4f0ea --- /dev/null +++ b/vendor/github.com/yosida95/uritemplate/v3/prog.go @@ -0,0 +1,130 @@ +// Copyright (C) 2016 Kohei YOSHIDA. All rights reserved. +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of The BSD 3-Clause License +// that can be found in the LICENSE file. + +package uritemplate + +import ( + "bytes" + "strconv" +) + +type progOpcode uint16 + +const ( + // match + opRune progOpcode = iota + opRuneClass + opLineBegin + opLineEnd + // capture + opCapStart + opCapEnd + // stack + opSplit + opJmp + opJmpIfNotDefined + opJmpIfNotEmpty + opJmpIfNotFirst + // result + opEnd + // fake + opNoop + opcodeMax +) + +var opcodeNames = []string{ + // match + "opRune", + "opRuneClass", + "opLineBegin", + "opLineEnd", + // capture + "opCapStart", + "opCapEnd", + // stack + "opSplit", + "opJmp", + "opJmpIfNotDefined", + "opJmpIfNotEmpty", + "opJmpIfNotFirst", + // result + "opEnd", +} + +func (code progOpcode) String() string { + if code >= opcodeMax { + return "" + } + return opcodeNames[code] +} + +type progOp struct { + code progOpcode + r rune + rc runeClass + i uint32 + + name string +} + +func dumpProgOp(b *bytes.Buffer, op *progOp) { + b.WriteString(op.code.String()) + switch op.code { + case opRune: + b.WriteString("(") + b.WriteString(strconv.QuoteToASCII(string(op.r))) + b.WriteString(")") + case opRuneClass: + b.WriteString("(") + b.WriteString(op.rc.String()) + b.WriteString(")") + case opCapStart, opCapEnd: + b.WriteString("(") + b.WriteString(strconv.QuoteToASCII(op.name)) + b.WriteString(")") + case opSplit: + b.WriteString(" -> ") + b.WriteString(strconv.FormatInt(int64(op.i), 10)) + case opJmp, opJmpIfNotFirst: + b.WriteString(" -> ") + b.WriteString(strconv.FormatInt(int64(op.i), 10)) + case opJmpIfNotDefined, opJmpIfNotEmpty: + b.WriteString("(") + b.WriteString(strconv.QuoteToASCII(op.name)) + b.WriteString(")") + b.WriteString(" -> ") + b.WriteString(strconv.FormatInt(int64(op.i), 10)) + } +} + +type prog struct { + op []progOp + numCap int +} + +func dumpProg(b *bytes.Buffer, prog *prog, pc uint32) { + for i := range prog.op { + op := prog.op[i] + + pos := strconv.Itoa(i) + if uint32(i) == pc { + pos = "*" + pos + } + b.WriteString(" "[len(pos):]) + b.WriteString(pos) + + b.WriteByte('\t') + dumpProgOp(b, &op) + + b.WriteByte('\n') + } +} + +func (p *prog) String() string { + b := bytes.Buffer{} + dumpProg(&b, p, 0) + return b.String() +} diff --git a/vendor/github.com/yosida95/uritemplate/v3/uritemplate.go b/vendor/github.com/yosida95/uritemplate/v3/uritemplate.go new file mode 100644 index 000000000..dbd267375 --- /dev/null +++ b/vendor/github.com/yosida95/uritemplate/v3/uritemplate.go @@ -0,0 +1,116 @@ +// Copyright (C) 2016 Kohei YOSHIDA. All rights reserved. +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of The BSD 3-Clause License +// that can be found in the LICENSE file. + +package uritemplate + +import ( + "log" + "regexp" + "strings" + "sync" +) + +var ( + debug = debugT(false) +) + +type debugT bool + +func (t debugT) Printf(format string, v ...interface{}) { + if t { + log.Printf(format, v...) + } +} + +// Template represents a URI Template. +type Template struct { + raw string + exprs []template + + // protects the rest of fields + mu sync.Mutex + varnames []string + re *regexp.Regexp + prog *prog +} + +// New parses and constructs a new Template instance based on the template. +// New returns an error if the template cannot be recognized. +func New(template string) (*Template, error) { + return (&parser{r: template}).parseURITemplate() +} + +// MustNew panics if the template cannot be recognized. +func MustNew(template string) *Template { + ret, err := New(template) + if err != nil { + panic(err) + } + return ret +} + +// Raw returns a raw URI template passed to New in string. +func (t *Template) Raw() string { + return t.raw +} + +// Varnames returns variable names used in the template. +func (t *Template) Varnames() []string { + t.mu.Lock() + defer t.mu.Unlock() + if t.varnames != nil { + return t.varnames + } + + reg := map[string]struct{}{} + t.varnames = []string{} + for i := range t.exprs { + expr, ok := t.exprs[i].(*expression) + if !ok { + continue + } + for _, spec := range expr.vars { + if _, ok := reg[spec.name]; ok { + continue + } + reg[spec.name] = struct{}{} + t.varnames = append(t.varnames, spec.name) + } + } + + return t.varnames +} + +// Expand returns a URI reference corresponding to the template expanded using the passed variables. +func (t *Template) Expand(vars Values) (string, error) { + var w strings.Builder + for i := range t.exprs { + expr := t.exprs[i] + if err := expr.expand(&w, vars); err != nil { + return w.String(), err + } + } + return w.String(), nil +} + +// Regexp converts the template to regexp and returns compiled *regexp.Regexp. +func (t *Template) Regexp() *regexp.Regexp { + t.mu.Lock() + defer t.mu.Unlock() + if t.re != nil { + return t.re + } + + var b strings.Builder + b.WriteByte('^') + for _, expr := range t.exprs { + expr.regexp(&b) + } + b.WriteByte('$') + t.re = regexp.MustCompile(b.String()) + + return t.re +} diff --git a/vendor/github.com/yosida95/uritemplate/v3/value.go b/vendor/github.com/yosida95/uritemplate/v3/value.go new file mode 100644 index 000000000..0550eabdb --- /dev/null +++ b/vendor/github.com/yosida95/uritemplate/v3/value.go @@ -0,0 +1,216 @@ +// Copyright (C) 2016 Kohei YOSHIDA. All rights reserved. +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of The BSD 3-Clause License +// that can be found in the LICENSE file. + +package uritemplate + +import "strings" + +// A varname containing pct-encoded characters is not the same variable as +// a varname with those same characters decoded. +// +// -- https://tools.ietf.org/html/rfc6570#section-2.3 +type Values map[string]Value + +func (v Values) Set(name string, value Value) { + v[name] = value +} + +func (v Values) Get(name string) Value { + if v == nil { + return Value{} + } + return v[name] +} + +type ValueType uint8 + +const ( + ValueTypeString = iota + ValueTypeList + ValueTypeKV + valueTypeLast +) + +var valueTypeNames = []string{ + "String", + "List", + "KV", +} + +func (vt ValueType) String() string { + if vt < valueTypeLast { + return valueTypeNames[vt] + } + return "" +} + +type Value struct { + T ValueType + V []string +} + +func (v Value) String() string { + if v.Valid() && v.T == ValueTypeString { + return v.V[0] + } + return "" +} + +func (v Value) List() []string { + if v.Valid() && v.T == ValueTypeList { + return v.V + } + return nil +} + +func (v Value) KV() []string { + if v.Valid() && v.T == ValueTypeKV { + return v.V + } + return nil +} + +func (v Value) Valid() bool { + switch v.T { + default: + return false + case ValueTypeString: + return len(v.V) > 0 + case ValueTypeList: + return len(v.V) > 0 + case ValueTypeKV: + return len(v.V) > 0 && len(v.V)%2 == 0 + } +} + +func (v Value) expand(w *strings.Builder, spec varspec, exp *expression) error { + switch v.T { + case ValueTypeString: + val := v.V[0] + var maxlen int + if max := len(val); spec.maxlen < 1 || spec.maxlen > max { + maxlen = max + } else { + maxlen = spec.maxlen + } + + if exp.named { + w.WriteString(spec.name) + if val == "" { + w.WriteString(exp.ifemp) + return nil + } + w.WriteByte('=') + } + return exp.escape(w, val[:maxlen]) + case ValueTypeList: + var sep string + if spec.explode { + sep = exp.sep + } else { + sep = "," + } + + var pre string + var preifemp string + if spec.explode && exp.named { + pre = spec.name + "=" + preifemp = spec.name + exp.ifemp + } + + if !spec.explode && exp.named { + w.WriteString(spec.name) + w.WriteByte('=') + } + for i := range v.V { + val := v.V[i] + if i > 0 { + w.WriteString(sep) + } + if val == "" { + w.WriteString(preifemp) + continue + } + w.WriteString(pre) + + if err := exp.escape(w, val); err != nil { + return err + } + } + case ValueTypeKV: + var sep string + var kvsep string + if spec.explode { + sep = exp.sep + kvsep = "=" + } else { + sep = "," + kvsep = "," + } + + var ifemp string + var kescape escapeFunc + if spec.explode && exp.named { + ifemp = exp.ifemp + kescape = escapeLiteral + } else { + ifemp = "," + kescape = exp.escape + } + + if !spec.explode && exp.named { + w.WriteString(spec.name) + w.WriteByte('=') + } + + for i := 0; i < len(v.V); i += 2 { + if i > 0 { + w.WriteString(sep) + } + if err := kescape(w, v.V[i]); err != nil { + return err + } + if v.V[i+1] == "" { + w.WriteString(ifemp) + continue + } + w.WriteString(kvsep) + + if err := exp.escape(w, v.V[i+1]); err != nil { + return err + } + } + } + return nil +} + +// String returns Value that represents string. +func String(v string) Value { + return Value{ + T: ValueTypeString, + V: []string{v}, + } +} + +// List returns Value that represents list. +func List(v ...string) Value { + return Value{ + T: ValueTypeList, + V: v, + } +} + +// KV returns Value that represents associative list. +// KV panics if len(kv) is not even. +func KV(kv ...string) Value { + if len(kv)%2 != 0 { + panic("uritemplate.go: count of the kv must be even number") + } + return Value{ + T: ValueTypeKV, + V: kv, + } +} diff --git a/vendor/modules.txt b/vendor/modules.txt index c99ed1adb..0c77b066a 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -72,6 +72,10 @@ github.com/json-iterator/go github.com/mailru/easyjson/buffer github.com/mailru/easyjson/jlexer github.com/mailru/easyjson/jwriter +# github.com/mark3labs/mcp-go v0.16.0 +## explicit; go 1.23 +github.com/mark3labs/mcp-go/mcp +github.com/mark3labs/mcp-go/server # github.com/mitchellh/mapstructure v1.5.0 ## explicit; go 1.14 github.com/mitchellh/mapstructure @@ -146,6 +150,9 @@ github.com/stretchr/testify/require # github.com/x448/float16 v0.8.4 ## explicit; go 1.11 github.com/x448/float16 +# github.com/yosida95/uritemplate/v3 v3.0.2 +## explicit; go 1.14 +github.com/yosida95/uritemplate/v3 # golang.org/x/net v0.39.0 ## explicit; go 1.23.0 golang.org/x/net/http/httpguts