Skip to content

Value decoding changes data-URL-looking JSON strings into Value.data #277

Description

@bitbemol

Describe the bug

This is my first contribution to this repository, so I may be missing some intended design context. I have tried to verify the behavior carefully and would appreciate correction if this conversion is intentional.

Value.init(from:) currently interprets a JSON string as Value.data whenever the string looks like a data URL and can be parsed successfully.

For example, this valid JSON string:

"data:text/plain,Hello%20World"

is decoded as binary data rather than:

Value.string("data:text/plain,Hello%20World")

Re-encoding the decoded value changes the string to:

data:text/plain;base64,SGVsbG8gV29ybGQ=

This changes both the Value case and the original string’s spelling.

The same behavior can affect explicitly typed MCP text content. A CallTool.Result containing:

.text(
    text: "data:text/plain,Hello%20World",
    annotations: nil,
    _meta: nil
)

is rewritten when it passes through the SDK’s generic Value conversion. The recovered content is still tagged as text, but its text payload has changed.

To Reproduce

import Foundation
import MCP

let json = Data(#""data:text/plain,Hello%20World""#.utf8)
let decoded = try JSONDecoder().decode(Value.self, from: json)

print(decoded.stringValue as Any)

let reencoded = try JSONEncoder().encode(decoded)
let recoveredString = try JSONDecoder().decode(String.self, from: reencoded)
print(recoveredString)

Current behavior:

nil
data:text/plain;base64,SGVsbG8gV29ybGQ=

The behavior comes from the string branch of Value.init(from:), which calls Data.isDataURL and Data.parseDataURL before deciding between .data and .string.

Strings that do not form parseable data URLs remain .string. For example, Hello%20World by itself is unaffected. The conversion is triggered by complete data-URL-looking strings such as:

data:text/plain,Hello%20World
data:text/plain,Hello World
data:text/plain;base64,SGVsbG8=
data:,

Expected behavior

I believe generic JSON decoding should follow the JSON wire type:

  • Every JSON string decoded through Value remains Value.string.
  • Data-URL interpretation remains explicitly available through Data.parseDataURL.
  • Explicitly constructed Value.data values continue to encode using the existing data-URL representation.
  • Typed image and audio content remains unchanged because those MCP content types have explicit data and mimeType fields.

A generic JSON string does not carry enough information to distinguish:

Value.string("data:text/plain;base64,SGVsbG8=")

from an encoded:

Value.data(mimeType: "text/plain", Data("Hello".utf8))

because both have the same JSON representation.

Logs

A focused typed-text regression currently fails with:

text → "data:text/plain;base64,SGVsbG8gV29ybGQ="
original → "data:text/plain,Hello%20World"

With the original decoder implementation, 6 of 8 focused regression and control tests fail. The two unaffected controls verify explicit Value.data encoding and typed image/audio serialization.

Changing the generic string branch to assign .string(value) makes all 8 focused tests pass. The complete SDK test plan also passes: 559 tests, 0 failures.

Additional context

I encountered this through a downstream MCP server that needs to preserve literal text exactly:

https://github.com/bitbemol/second-brain-mcp

The downstream project currently vendors the Swift SDK solely to avoid this transformation.

I have a minimal patch and regression tests ready. The explicit data-URL parsing helpers are preserved.

There is a compatibility consideration: callers that rely on generic Value decoding to recognize data URLs would instead need to call Data.parseDataURL explicitly. If the current implicit decoding is intentional API behavior, I would appreciate guidance on how ordinary JSON strings with the same spelling should be represented without being transformed.

Tested against Swift SDK main at a0ae212 and release 0.12.1.

AI assistance disclosure: An LLM initially identified this behavior while I was diagnosing the downstream issue. Codex assisted with repository analysis, regression-test development, and drafting this report. I reviewed the affected code, reproduced the behavior with the original implementation in Xcode, and verified the proposed behavior with the focused and complete test suites.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions