Skip to content
Open
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
50 changes: 50 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ of the MCP specification.
- [Progress Tracking](#progress-tracking-1)
- [Initialize Hook](#initialize-hook)
- [HTTP Request Context in Handlers](#http-request-context-in-handlers)
- [Exact JSON in Method Handlers](#exact-json-in-method-handlers)
- [Graceful Shutdown](#graceful-shutdown)
- [Transports](#transports)
- [Authentication](#authentication)
Expand Down Expand Up @@ -1214,6 +1215,55 @@ Task.detached { await doWork(with: ctx?.httpContext) }
Custom HTTP transports can opt in by conforming to `HTTPContextProviding` and returning the
`HTTPRequest` for a given JSON-RPC id while it is in flight.

### Exact JSON in Method Handlers

`Value` intentionally provides convenient semantic JSON values, including automatic data-URL
decoding and dictionary-backed objects. If a handler must instead inspect source member order,
duplicate names, Unicode-scalar-exact keys, or a data URL as a string, register the additive
raw-aware method-handler overload:

```swift
await server.withMethodHandler(CallTool.self) { request, rawContext in
// `request` remains the usual validated, typed request.
guard let parameters = rawContext.uniqueParameters?.objectValue,
let arguments = parameters.uniqueValue(forExactKey: "arguments")?.objectValue
else {
throw MCPError.invalidParams("Expected one object-valued params member")
}

// Every duplicate is retained in source order. Lookup compares Unicode
// scalar sequences exactly and never normalizes object names.
let payloads = arguments.values(forExactKey: "payload")
if case .string(let dataURL)? = payloads.first {
// A string remains a string even when it is a valid data URL.
print(dataURL)
}

return .init(content: [.text("Handled \(request.params.name)")], isError: false)
}
```

`RawJSONValue` uses a dedicated parser and serializer rather than `Codable`, because keyed
decoding cannot retain duplicate object members. Raw-aware request parsing and encoding are
bounded by `RawJSONLimits`; pass custom limits when constructing the server if the defaults are
not suitable. These limits do not affect legacy handlers or inbound responses:

```swift
let server = Server(
name: "MyModelServer",
version: "1.0.0",
rawJSONLimits: .init(
maximumDocumentBytes: 8 * 1024 * 1024,
maximumStringBytes: 6 * 1024 * 1024,
maximumDepth: 64,
maximumContainerEntries: 50_000
)
)
```

Inbound JSON strings always decode as `RawJSONValue.string`. The `data` case is reserved for
programmatically constructed raw values and is size-checked before base64 storage is allocated.

### Graceful Shutdown

We recommend using
Expand Down
65 changes: 60 additions & 5 deletions Sources/MCP/Base/Messages.swift
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,35 @@ extension Request {
/// A type-erased request for request/response handling
typealias AnyRequest = Request<AnyMethod>

/// The exact JSON representation retained alongside a typed inbound request.
///
/// Register a raw-aware server method handler to receive this context when
/// object member order, duplicate keys, or data-URL strings must be preserved.
/// The ordinary typed request remains the source of the method's validated
/// parameters.
public struct RawRequestContext: Hashable, Sendable {
/// The complete inbound JSON-RPC request object.
public let request: ExactJSONObject

/// Every exact `params` member in source order.
///
/// Valid JSON-RPC requests normally contain at most one. Returning an
/// array keeps duplicate-member handling explicit instead of silently
/// selecting a first or last value.
public var parameters: [RawJSONValue] {
request.values(forExactKey: "params")
}

/// The exact `params` value when it occurs once, otherwise `nil`.
public var uniqueParameters: RawJSONValue? {
request.uniqueValue(forExactKey: "params")
}

public init(request: ExactJSONObject) {
self.request = request
}
}

extension AnyRequest {
init<T: Method>(_ request: Request<T>) throws {
let encoder = JSONEncoder()
Expand All @@ -166,21 +195,47 @@ extension AnyRequest {

/// A box for request handlers that can be type-erased
class RequestHandlerBox: @unchecked Sendable {
func callAsFunction(_ request: AnyRequest) async throws -> AnyResponse {
var requiresRawContext: Bool { false }

func callAsFunction(
_ request: AnyRequest,
rawContext: RawRequestContext? = nil
) async throws -> AnyResponse {
fatalError("Must override")
}
}

/// A typed request handler that can be used to handle requests of a specific type
final class TypedRequestHandler<M: Method>: RequestHandlerBox, @unchecked Sendable {
private let _handle: @Sendable (Request<M>) async throws -> Response<M>
private let _handle: @Sendable (Request<M>, RawRequestContext?) async throws -> Response<M>
private let isRawAware: Bool

override var requiresRawContext: Bool { isRawAware }

init(_ handler: @escaping @Sendable (Request<M>) async throws -> Response<M>) {
self._handle = handler
self._handle = { request, _ in try await handler(request) }
self.isRawAware = false
super.init()
}

init(
rawAware handler:
@escaping @Sendable (Request<M>, RawRequestContext) async throws -> Response<M>
) {
self._handle = { request, rawContext in
guard let rawContext else {
throw MCPError.internalError("Raw request context is unavailable")
}
return try await handler(request, rawContext)
}
self.isRawAware = true
super.init()
}

override func callAsFunction(_ request: AnyRequest) async throws -> AnyResponse {
override func callAsFunction(
_ request: AnyRequest,
rawContext: RawRequestContext? = nil
) async throws -> AnyResponse {
let encoder = JSONEncoder()
let decoder = JSONDecoder()

Expand All @@ -189,7 +244,7 @@ final class TypedRequestHandler<M: Method>: RequestHandlerBox, @unchecked Sendab
let request = try decoder.decode(Request<M>.self, from: data)

// Handle with concrete type
let response = try await _handle(request)
let response = try await _handle(request, rawContext)

// Convert result to AnyMethod response
switch response.result {
Expand Down
Loading