clone

a self-hosted multitenant nostr relay with sites, file storage, git hosting, and inbox and outbox delivery

feat/ui-updates
tinyrelay / internal / mcp / mcp.go
mcp.go 4.6K
1package mcp23import (4 "encoding/base64"5 "encoding/json"6 "fmt"7 "strings"8)910// Version is the protocol revision this package implements.11const Version = "2026-07-28"1213// SupportedVersions lists the protocol revisions the server accepts.14var SupportedVersions = []string{Version}1516// Header names mirrored from the JSON-RPC body so intermediaries can route17// without parsing it. Comparison of names is case-insensitive; values are18// case-sensitive.19const (20 HeaderProtocolVersion = "MCP-Protocol-Version"21 HeaderMethod = "Mcp-Method"22 HeaderName = "Mcp-Name"23)2425// Reserved _meta keys carried on every request and result.26const (27 MetaProtocolVersion = "io.modelcontextprotocol/protocolVersion"28 MetaClientCapabilities = "io.modelcontextprotocol/clientCapabilities"29 MetaClientInfo = "io.modelcontextprotocol/clientInfo"30 MetaServerInfo = "io.modelcontextprotocol/serverInfo"31)3233// JSON-RPC error codes. The -32020 to -32099 range is reserved for the MCP34// specification and only the defined codes may be emitted.35const (36 CodeParse = -3270037 CodeInvalidRequest = -3260038 CodeMethodNotFound = -3260139 CodeInvalidParams = -3260240 CodeInternal = -3260341 CodeHeaderMismatch = -3202042 CodeMissingClientCapability = -3202143 CodeUnsupportedProtocolVersion = -3202244)4546// Error is a JSON-RPC error object. Status is the HTTP status the transport47// answers with and is never serialized.48type Error struct {49 Code int `json:"code"`50 Message string `json:"message"`51 Data any `json:"data,omitempty"`52 Status int `json:"-"`53}5455func (e *Error) Error() string { return fmt.Sprintf("mcp: %d %s", e.Code, e.Message) }5657// Request is an incoming JSON-RPC request or notification. ID is nil for a58// notification.59type Request struct {60 JSONRPC string `json:"jsonrpc"`61 ID json.RawMessage `json:"id,omitempty"`62 Method string `json:"method"`63 Params json.RawMessage `json:"params,omitempty"`64}6566// Notification reports whether the message carries no id and therefore67// expects no response.68func (r Request) Notification() bool { return len(r.ID) == 0 }6970// Response is an outgoing JSON-RPC result or error.71type Response struct {72 JSONRPC string `json:"jsonrpc"`73 ID json.RawMessage `json:"id,omitempty"`74 Result any `json:"result,omitempty"`75 Error *Error `json:"error,omitempty"`76}7778// Implementation names a client or server.79type Implementation struct {80 Name string `json:"name"`81 Version string `json:"version,omitempty"`82}8384// params is the subset of request parameters the transport inspects.85type params struct {86 Meta map[string]json.RawMessage `json:"_meta"`87 Name string `json:"name"`88 URI string `json:"uri"`89 Cursor string `json:"cursor"`90 Arguments json.RawMessage `json:"arguments"`91}9293const (94 sentinelPrefix = "=?base64?"95 sentinelSuffix = "?="96)9798// EncodeHeaderValue renders a body value for the Mcp-Name or Mcp-Param99// headers. Values outside the safe ASCII set, with surrounding whitespace, or100// that already look like the sentinel are Base64 encoded.101func EncodeHeaderValue(value string) string {102 if headerSafe(value) && !sentinel(value) {103 return value104 }105 return sentinelPrefix + base64.StdEncoding.EncodeToString([]byte(value)) + sentinelSuffix106}107108// DecodeHeaderValue reverses EncodeHeaderValue. Plain values must consist of109// visible ASCII, space and horizontal tab.110func DecodeHeaderValue(value string) (string, error) {111 if sentinel(value) {112 raw, err := base64.StdEncoding.DecodeString(strings.TrimSuffix(strings.TrimPrefix(value, sentinelPrefix), sentinelSuffix))113 if err != nil {114 return "", fmt.Errorf("invalid Base64 header value")115 }116 return string(raw), nil117 }118 if !headerSafe(value) {119 return "", fmt.Errorf("header value contains invalid characters")120 }121 return value, nil122}123124func sentinel(value string) bool {125 return strings.HasPrefix(value, sentinelPrefix) && strings.HasSuffix(value, sentinelSuffix) && len(value) >= len(sentinelPrefix)+len(sentinelSuffix)126}127128func headerSafe(value string) bool {129 if value != strings.TrimSpace(value) {130 return false131 }132 for i := 0; i < len(value); i++ {133 c := value[i]134 if c == ' ' || c == '\t' || (c >= 0x21 && c <= 0x7e) {135 continue136 }137 return false138 }139 return true140}141142func errorf(code, status int, format string, args ...any) *Error {143 return &Error{Code: code, Status: status, Message: fmt.Sprintf(format, args...)}144}145146func supported(version string) bool {147 for _, v := range SupportedVersions {148 if v == version {149 return true150 }151 }152 return false153}