55package servercard
66
77import (
8+ "crypto/sha256"
9+ "encoding/hex"
810 "encoding/json"
911 "errors"
1012 "fmt"
@@ -95,11 +97,12 @@ type ServerCard struct {
9597}
9698
9799type buildOptions struct {
98- name string
99- schema string
100- remotes []Remote
101- repository * Repository
102- meta map [string ]any
100+ name string
101+ description string
102+ schema string
103+ remotes []Remote
104+ repository * Repository
105+ meta map [string ]any
103106}
104107
105108// BuildOption configures [BuildServerCard].
@@ -112,6 +115,13 @@ func WithName(name string) BuildOption {
112115 }
113116}
114117
118+ // WithDescription sets the Server Card's short user-facing description.
119+ func WithDescription (description string ) BuildOption {
120+ return func (o * buildOptions ) {
121+ o .description = description
122+ }
123+ }
124+
115125// WithSchema sets the Server Card schema URL. If unset, [SchemaURL] is used.
116126func WithSchema (schema string ) BuildOption {
117127 return func (o * buildOptions ) {
@@ -143,10 +153,10 @@ func WithMeta(meta map[string]any) BuildOption {
143153// BuildServerCard builds a Server Card from MCP implementation identity
144154// metadata.
145155//
146- // The implementation provides the title, description, version, website URL, and
147- // icons. The card name is supplied with [WithName] because MCP implementation
148- // names are free-form while Server Card names must be reverse-DNS namespace/name
149- // identifiers.
156+ // The implementation provides the title, version, website URL, and icons. The
157+ // card name is supplied with [WithName] because MCP implementation names are
158+ // free-form while Server Card names must be reverse-DNS namespace/name
159+ // identifiers. The card description is supplied with [WithDescription].
150160func BuildServerCard (impl * mcp.Implementation , opts ... BuildOption ) (* ServerCard , error ) {
151161 if impl == nil {
152162 return nil , errors .New ("implementation must not be nil" )
@@ -163,14 +173,14 @@ func BuildServerCard(impl *mcp.Implementation, opts ...BuildOption) (*ServerCard
163173 if impl .Version == "" {
164174 return nil , errors .New ("implementation version must be set to build a Server Card" )
165175 }
166- if impl . Description == "" {
167- return nil , errors .New ("implementation description must be set to build a Server Card " )
176+ if cfg . description == "" {
177+ return nil , errors .New ("server card description must be set" )
168178 }
169179 card := & ServerCard {
170180 Schema : cfg .schema ,
171181 Name : cfg .name ,
172182 Title : impl .Title ,
173- Description : impl . Description ,
183+ Description : cfg . description ,
174184 Version : impl .Version ,
175185 WebsiteURL : impl .WebsiteURL ,
176186 Icons : append ([]Icon (nil ), impl .Icons ... ),
@@ -254,8 +264,8 @@ func (c *ServerCard) Validate() error {
254264func Handler (card * ServerCard ) http.Handler {
255265 return http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
256266 setDiscoveryHeaders (w .Header ())
257- if r .Method != http .MethodGet {
258- w .Header ().Set ("Allow" , http . MethodGet )
267+ if r .Method != http .MethodGet && r . Method != http . MethodHead {
268+ w .Header ().Set ("Allow" , "GET, HEAD" )
259269 http .Error (w , "method not allowed" , http .StatusMethodNotAllowed )
260270 return
261271 }
@@ -268,9 +278,18 @@ func Handler(card *ServerCard) http.Handler {
268278 http .Error (w , err .Error (), http .StatusInternalServerError )
269279 return
270280 }
281+ sum := sha256 .Sum256 (body )
282+ etag := `"` + hex .EncodeToString (sum [:]) + `"`
271283 w .Header ().Set ("Content-Type" , MediaType )
284+ w .Header ().Set ("ETag" , etag )
285+ if ifNoneMatchMatches (r .Header .Get ("If-None-Match" ), etag ) {
286+ w .WriteHeader (http .StatusNotModified )
287+ return
288+ }
272289 w .WriteHeader (http .StatusOK )
273- _ , _ = w .Write (body )
290+ if r .Method == http .MethodGet {
291+ _ , _ = w .Write (body )
292+ }
274293 })
275294}
276295
@@ -290,6 +309,25 @@ func setDiscoveryHeaders(h http.Header) {
290309 h .Set ("Cache-Control" , "public, max-age=3600" )
291310}
292311
312+ func ifNoneMatchMatches (header , etag string ) bool {
313+ if header == "" {
314+ return false
315+ }
316+ for _ , candidate := range strings .Split (header , "," ) {
317+ candidate = strings .TrimSpace (candidate )
318+ if candidate == "*" {
319+ return true
320+ }
321+ if strings .HasPrefix (candidate , "W/" ) || strings .HasPrefix (candidate , "w/" ) {
322+ candidate = strings .TrimSpace (candidate [2 :])
323+ }
324+ if candidate == etag {
325+ return true
326+ }
327+ }
328+ return false
329+ }
330+
293331func isVersionRange (version string ) bool {
294332 release , _ , _ := strings .Cut (version , "-" )
295333 return versionRangeOperatorRE .MatchString (version ) || versionWildcardSegmentRE .MatchString (release )
0 commit comments