Skip to content

Commit 45826c4

Browse files
authored
feat: update type definition of _meta field to text/blob resources. (#591)
* feat: update type definition of _meta field to text/blob resources. - Parses optional _meta field on the text/blob resource - Add TestResourceContentsMetaField to verify _meta field handling - Ensure backward compatibility with resources without _meta * chore: apply coderabbit nits.
1 parent 9d1f447 commit 45826c4

File tree

3 files changed

+253
-4
lines changed

3 files changed

+253
-4
lines changed

mcp/types.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -739,8 +739,9 @@ type ResourceContents interface {
739739
}
740740

741741
type TextResourceContents struct {
742-
// Meta is a metadata object that is reserved by MCP for storing additional information.
743-
Meta *Meta `json:"_meta,omitempty"`
742+
// Raw per‑resource metadata; pass‑through as defined by MCP. Not the same as mcp.Meta.
743+
// Allows _meta to be used for MCP-UI features for example. Does not assume any specific format.
744+
Meta map[string]any `json:"_meta,omitempty"`
744745
// The URI of this resource.
745746
URI string `json:"uri"`
746747
// The MIME type of this resource, if known.
@@ -753,8 +754,9 @@ type TextResourceContents struct {
753754
func (TextResourceContents) isResourceContents() {}
754755

755756
type BlobResourceContents struct {
756-
// Meta is a metadata object that is reserved by MCP for storing additional information.
757-
Meta *Meta `json:"_meta,omitempty"`
757+
// Raw per‑resource metadata; pass‑through as defined by MCP. Not the same as mcp.Meta.
758+
// Allows _meta to be used for MCP-UI features for example. Does not assume any specific format.
759+
Meta map[string]any `json:"_meta,omitempty"`
758760
// The URI of this resource.
759761
URI string `json:"uri"`
760762
// The MIME type of this resource, if known.

mcp/types_test.go

Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,3 +138,242 @@ func TestCallToolResultWithResourceLink(t *testing.T) {
138138
assert.Equal(t, "A test document", resourceLink.Description)
139139
assert.Equal(t, "application/pdf", resourceLink.MIMEType)
140140
}
141+
142+
func TestResourceContentsMetaField(t *testing.T) {
143+
tests := []struct {
144+
name string
145+
inputJSON string
146+
expectedType string
147+
expectedMeta map[string]any
148+
}{
149+
{
150+
name: "TextResourceContents with empty _meta",
151+
inputJSON: `{
152+
"uri":"file://empty-meta.txt",
153+
"mimeType":"text/plain",
154+
"text":"x",
155+
"_meta": {}
156+
}`,
157+
expectedType: "text",
158+
expectedMeta: map[string]any{},
159+
},
160+
{
161+
name: "TextResourceContents with _meta field",
162+
inputJSON: `{
163+
"uri": "file://test.txt",
164+
"mimeType": "text/plain",
165+
"text": "Hello World",
166+
"_meta": {
167+
"mcpui.dev/ui-preferred-frame-size": ["800px", "600px"],
168+
"mcpui.dev/ui-initial-render-data": {
169+
"test": "value"
170+
}
171+
}
172+
}`,
173+
expectedType: "text",
174+
expectedMeta: map[string]any{
175+
"mcpui.dev/ui-preferred-frame-size": []interface{}{"800px", "600px"},
176+
"mcpui.dev/ui-initial-render-data": map[string]any{
177+
"test": "value",
178+
},
179+
},
180+
},
181+
{
182+
name: "BlobResourceContents with _meta field",
183+
inputJSON: `{
184+
"uri": "file://image.png",
185+
"mimeType": "image/png",
186+
"blob": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==",
187+
"_meta": {
188+
"width": 100,
189+
"height": 100,
190+
"format": "PNG"
191+
}
192+
}`,
193+
expectedType: "blob",
194+
expectedMeta: map[string]any{
195+
"width": float64(100), // JSON numbers are always float64
196+
"height": float64(100),
197+
"format": "PNG",
198+
},
199+
},
200+
{
201+
name: "TextResourceContents without _meta field",
202+
inputJSON: `{
203+
"uri": "file://simple.txt",
204+
"mimeType": "text/plain",
205+
"text": "Simple content"
206+
}`,
207+
expectedType: "text",
208+
expectedMeta: nil,
209+
},
210+
{
211+
name: "BlobResourceContents without _meta field",
212+
inputJSON: `{
213+
"uri": "file://simple.png",
214+
"mimeType": "image/png",
215+
"blob": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg=="
216+
}`,
217+
expectedType: "blob",
218+
expectedMeta: nil,
219+
},
220+
}
221+
222+
for _, tc := range tests {
223+
t.Run(tc.name, func(t *testing.T) {
224+
// Parse the JSON as a generic map first
225+
var contentMap map[string]any
226+
err := json.Unmarshal([]byte(tc.inputJSON), &contentMap)
227+
require.NoError(t, err)
228+
229+
// Use ParseResourceContents to convert to ResourceContents
230+
resourceContent, err := ParseResourceContents(contentMap)
231+
require.NoError(t, err)
232+
require.NotNil(t, resourceContent)
233+
234+
// Test based on expected type
235+
if tc.expectedType == "text" {
236+
textContent, ok := resourceContent.(TextResourceContents)
237+
require.True(t, ok, "Expected TextResourceContents")
238+
239+
// Verify standard fields
240+
assert.Equal(t, contentMap["uri"], textContent.URI)
241+
assert.Equal(t, contentMap["mimeType"], textContent.MIMEType)
242+
assert.Equal(t, contentMap["text"], textContent.Text)
243+
244+
// Verify _meta field
245+
assert.Equal(t, tc.expectedMeta, textContent.Meta)
246+
247+
} else if tc.expectedType == "blob" {
248+
blobContent, ok := resourceContent.(BlobResourceContents)
249+
require.True(t, ok, "Expected BlobResourceContents")
250+
251+
// Verify standard fields
252+
assert.Equal(t, contentMap["uri"], blobContent.URI)
253+
assert.Equal(t, contentMap["mimeType"], blobContent.MIMEType)
254+
assert.Equal(t, contentMap["blob"], blobContent.Blob)
255+
256+
// Verify _meta field
257+
assert.Equal(t, tc.expectedMeta, blobContent.Meta)
258+
}
259+
260+
// Test round-trip marshaling to ensure _meta is preserved
261+
marshaledJSON, err := json.Marshal(resourceContent)
262+
require.NoError(t, err)
263+
264+
var marshaledMap map[string]any
265+
err = json.Unmarshal(marshaledJSON, &marshaledMap)
266+
require.NoError(t, err)
267+
268+
// Verify _meta field is preserved in marshaled output
269+
v, ok := marshaledMap["_meta"]
270+
if tc.expectedMeta != nil {
271+
// Special case: empty maps are omitted due to omitempty tag
272+
if len(tc.expectedMeta) == 0 {
273+
assert.False(t, ok, "_meta should be omitted when empty due to omitempty")
274+
} else {
275+
require.True(t, ok, "_meta should be present")
276+
assert.Equal(t, tc.expectedMeta, v)
277+
}
278+
} else {
279+
assert.False(t, ok, "_meta should be omitted when nil")
280+
}
281+
})
282+
}
283+
}
284+
285+
func TestParseResourceContentsInvalidMeta(t *testing.T) {
286+
tests := []struct {
287+
name string
288+
inputJSON string
289+
expectedErr string
290+
}{
291+
{
292+
name: "TextResourceContents with invalid _meta (string)",
293+
inputJSON: `{
294+
"uri": "file://test.txt",
295+
"mimeType": "text/plain",
296+
"text": "Hello World",
297+
"_meta": "invalid_meta_string"
298+
}`,
299+
expectedErr: "_meta must be an object",
300+
},
301+
{
302+
name: "TextResourceContents with invalid _meta (number)",
303+
inputJSON: `{
304+
"uri": "file://test.txt",
305+
"mimeType": "text/plain",
306+
"text": "Hello World",
307+
"_meta": 123
308+
}`,
309+
expectedErr: "_meta must be an object",
310+
},
311+
{
312+
name: "TextResourceContents with invalid _meta (array)",
313+
inputJSON: `{
314+
"uri": "file://test.txt",
315+
"mimeType": "text/plain",
316+
"text": "Hello World",
317+
"_meta": ["invalid", "array"]
318+
}`,
319+
expectedErr: "_meta must be an object",
320+
},
321+
{
322+
name: "TextResourceContents with invalid _meta (boolean)",
323+
inputJSON: `{
324+
"uri": "file://test.txt",
325+
"mimeType": "text/plain",
326+
"text": "Hello World",
327+
"_meta": true
328+
}`,
329+
expectedErr: "_meta must be an object",
330+
},
331+
{
332+
name: "TextResourceContents with invalid _meta (null)",
333+
inputJSON: `{
334+
"uri": "file://test.txt",
335+
"mimeType": "text/plain",
336+
"text": "Hello World",
337+
"_meta": null
338+
}`,
339+
expectedErr: "_meta must be an object",
340+
},
341+
{
342+
name: "BlobResourceContents with invalid _meta (string)",
343+
inputJSON: `{
344+
"uri": "file://image.png",
345+
"mimeType": "image/png",
346+
"blob": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==",
347+
"_meta": "invalid_meta_string"
348+
}`,
349+
expectedErr: "_meta must be an object",
350+
},
351+
{
352+
name: "BlobResourceContents with invalid _meta (number)",
353+
inputJSON: `{
354+
"uri": "file://image.png",
355+
"mimeType": "image/png",
356+
"blob": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==",
357+
"_meta": 456
358+
}`,
359+
expectedErr: "_meta must be an object",
360+
},
361+
}
362+
363+
for _, tc := range tests {
364+
t.Run(tc.name, func(t *testing.T) {
365+
// Parse the JSON as a generic map first
366+
var contentMap map[string]any
367+
err := json.Unmarshal([]byte(tc.inputJSON), &contentMap)
368+
require.NoError(t, err)
369+
370+
// Use ParseResourceContents to convert to ResourceContents
371+
resourceContent, err := ParseResourceContents(contentMap)
372+
373+
// Expect an error
374+
require.Error(t, err)
375+
assert.Contains(t, err.Error(), tc.expectedErr)
376+
assert.Nil(t, resourceContent)
377+
})
378+
}
379+
}

mcp/utils.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -767,8 +767,15 @@ func ParseResourceContents(contentMap map[string]any) (ResourceContents, error)
767767

768768
mimeType := ExtractString(contentMap, "mimeType")
769769

770+
meta := ExtractMap(contentMap, "_meta")
771+
772+
if _, present := contentMap["_meta"]; present && meta == nil {
773+
return nil, fmt.Errorf("_meta must be an object")
774+
}
775+
770776
if text := ExtractString(contentMap, "text"); text != "" {
771777
return TextResourceContents{
778+
Meta: meta,
772779
URI: uri,
773780
MIMEType: mimeType,
774781
Text: text,
@@ -777,6 +784,7 @@ func ParseResourceContents(contentMap map[string]any) (ResourceContents, error)
777784

778785
if blob := ExtractString(contentMap, "blob"); blob != "" {
779786
return BlobResourceContents{
787+
Meta: meta,
780788
URI: uri,
781789
MIMEType: mimeType,
782790
Blob: blob,

0 commit comments

Comments
 (0)