Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions mcp/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -716,8 +716,9 @@ type ResourceContents interface {
}

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

type BlobResourceContents struct {
// Meta is a metadata object that is reserved by MCP for storing additional information.
Meta *Meta `json:"_meta,omitempty"`
// Raw per‑resource metadata; pass‑through as defined by MCP. Not the same as mcp.Meta.
// Allows _meta to be used for MCP-UI features for example. Does not assume any specific format.
Meta map[string]any `json:"_meta,omitempty"`
// The URI of this resource.
URI string `json:"uri"`
// The MIME type of this resource, if known.
Expand Down
239 changes: 239 additions & 0 deletions mcp/types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,3 +138,242 @@
assert.Equal(t, "A test document", resourceLink.Description)
assert.Equal(t, "application/pdf", resourceLink.MIMEType)
}

func TestResourceContentsMetaField(t *testing.T) {
tests := []struct {
name string
inputJSON string
expectedType string
expectedMeta map[string]any
}{
{
name: "TextResourceContents with empty _meta",
inputJSON: `{
"uri":"file://empty-meta.txt",
"mimeType":"text/plain",
"text":"x",
"_meta": {}
}`,
expectedType: "text",
expectedMeta: map[string]any{},
},
{
name: "TextResourceContents with _meta field",
inputJSON: `{
"uri": "file://test.txt",
"mimeType": "text/plain",
"text": "Hello World",
"_meta": {
"mcpui.dev/ui-preferred-frame-size": ["800px", "600px"],
"mcpui.dev/ui-initial-render-data": {
"test": "value"
}
}
}`,
expectedType: "text",
expectedMeta: map[string]any{
"mcpui.dev/ui-preferred-frame-size": []interface{}{"800px", "600px"},
"mcpui.dev/ui-initial-render-data": map[string]any{
"test": "value",
},
},
},
{
name: "BlobResourceContents with _meta field",
inputJSON: `{
"uri": "file://image.png",
"mimeType": "image/png",
"blob": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==",
"_meta": {
"width": 100,
"height": 100,
"format": "PNG"
}
}`,
expectedType: "blob",
expectedMeta: map[string]any{
"width": float64(100), // JSON numbers are always float64
"height": float64(100),
"format": "PNG",
},
},
{
name: "TextResourceContents without _meta field",
inputJSON: `{
"uri": "file://simple.txt",
"mimeType": "text/plain",
"text": "Simple content"
}`,
expectedType: "text",
expectedMeta: nil,
},
{
name: "BlobResourceContents without _meta field",
inputJSON: `{
"uri": "file://simple.png",
"mimeType": "image/png",
"blob": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg=="
}`,
expectedType: "blob",
expectedMeta: nil,
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Parse the JSON as a generic map first
var contentMap map[string]any
err := json.Unmarshal([]byte(tc.inputJSON), &contentMap)
require.NoError(t, err)

// Use ParseResourceContents to convert to ResourceContents
resourceContent, err := ParseResourceContents(contentMap)
require.NoError(t, err)
require.NotNil(t, resourceContent)

// Test based on expected type
if tc.expectedType == "text" {

Check failure on line 235 in mcp/types_test.go

View workflow job for this annotation

GitHub Actions / lint

QF1003: could use tagged switch on tc.expectedType (staticcheck)
textContent, ok := resourceContent.(TextResourceContents)
require.True(t, ok, "Expected TextResourceContents")

// Verify standard fields
assert.Equal(t, contentMap["uri"], textContent.URI)
assert.Equal(t, contentMap["mimeType"], textContent.MIMEType)
assert.Equal(t, contentMap["text"], textContent.Text)

// Verify _meta field
assert.Equal(t, tc.expectedMeta, textContent.Meta)

} else if tc.expectedType == "blob" {
blobContent, ok := resourceContent.(BlobResourceContents)
require.True(t, ok, "Expected BlobResourceContents")

// Verify standard fields
assert.Equal(t, contentMap["uri"], blobContent.URI)
assert.Equal(t, contentMap["mimeType"], blobContent.MIMEType)
assert.Equal(t, contentMap["blob"], blobContent.Blob)

// Verify _meta field
assert.Equal(t, tc.expectedMeta, blobContent.Meta)
}

// Test round-trip marshaling to ensure _meta is preserved
marshaledJSON, err := json.Marshal(resourceContent)
require.NoError(t, err)

var marshaledMap map[string]any
err = json.Unmarshal(marshaledJSON, &marshaledMap)
require.NoError(t, err)

// Verify _meta field is preserved in marshaled output
v, ok := marshaledMap["_meta"]
if tc.expectedMeta != nil {
// Special case: empty maps are omitted due to omitempty tag
if len(tc.expectedMeta) == 0 {
assert.False(t, ok, "_meta should be omitted when empty due to omitempty")
} else {
require.True(t, ok, "_meta should be present")
assert.Equal(t, tc.expectedMeta, v)
}
} else {
assert.False(t, ok, "_meta should be omitted when nil")
}
})
}
}

func TestParseResourceContentsInvalidMeta(t *testing.T) {
tests := []struct {
name string
inputJSON string
expectedErr string
}{
{
name: "TextResourceContents with invalid _meta (string)",
inputJSON: `{
"uri": "file://test.txt",
"mimeType": "text/plain",
"text": "Hello World",
"_meta": "invalid_meta_string"
}`,
expectedErr: "_meta must be an object",
},
{
name: "TextResourceContents with invalid _meta (number)",
inputJSON: `{
"uri": "file://test.txt",
"mimeType": "text/plain",
"text": "Hello World",
"_meta": 123
}`,
expectedErr: "_meta must be an object",
},
{
name: "TextResourceContents with invalid _meta (array)",
inputJSON: `{
"uri": "file://test.txt",
"mimeType": "text/plain",
"text": "Hello World",
"_meta": ["invalid", "array"]
}`,
expectedErr: "_meta must be an object",
},
{
name: "TextResourceContents with invalid _meta (boolean)",
inputJSON: `{
"uri": "file://test.txt",
"mimeType": "text/plain",
"text": "Hello World",
"_meta": true
}`,
expectedErr: "_meta must be an object",
},
{
name: "TextResourceContents with invalid _meta (null)",
inputJSON: `{
"uri": "file://test.txt",
"mimeType": "text/plain",
"text": "Hello World",
"_meta": null
}`,
expectedErr: "_meta must be an object",
},
{
name: "BlobResourceContents with invalid _meta (string)",
inputJSON: `{
"uri": "file://image.png",
"mimeType": "image/png",
"blob": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==",
"_meta": "invalid_meta_string"
}`,
expectedErr: "_meta must be an object",
},
{
name: "BlobResourceContents with invalid _meta (number)",
inputJSON: `{
"uri": "file://image.png",
"mimeType": "image/png",
"blob": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==",
"_meta": 456
}`,
expectedErr: "_meta must be an object",
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Parse the JSON as a generic map first
var contentMap map[string]any
err := json.Unmarshal([]byte(tc.inputJSON), &contentMap)
require.NoError(t, err)

// Use ParseResourceContents to convert to ResourceContents
resourceContent, err := ParseResourceContents(contentMap)

// Expect an error
require.Error(t, err)
assert.Contains(t, err.Error(), tc.expectedErr)
assert.Nil(t, resourceContent)
})
}
}
8 changes: 8 additions & 0 deletions mcp/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -705,8 +705,15 @@ func ParseResourceContents(contentMap map[string]any) (ResourceContents, error)

mimeType := ExtractString(contentMap, "mimeType")

meta := ExtractMap(contentMap, "_meta")

if _, present := contentMap["_meta"]; present && meta == nil {
return nil, fmt.Errorf("_meta must be an object")
}

if text := ExtractString(contentMap, "text"); text != "" {
return TextResourceContents{
Meta: meta,
URI: uri,
MIMEType: mimeType,
Text: text,
Expand All @@ -715,6 +722,7 @@ func ParseResourceContents(contentMap map[string]any) (ResourceContents, error)

if blob := ExtractString(contentMap, "blob"); blob != "" {
return BlobResourceContents{
Meta: meta,
URI: uri,
MIMEType: mimeType,
Blob: blob,
Expand Down
Loading