Skip to content

Conversation

@jalingo
Copy link

@jalingo jalingo commented Aug 29, 2025

URL was formatting incorrectly, preventing endpoint from being recognized after sse connection.

Motivation and Context

Wanted SSE connections to mcp services to work.

How Has This Been Tested?

It works with multiple sse mcp services now.

Breaking Changes

Don't think so.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

Pretty straightforward

endpoint: URL(string: "http://localhost:8080/sse")! // Ensure endpoint is SSE-specific if needed
)
try await client.connect(transport: sseTransport)
try await client.initialize()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is no longer required, as of #100

Suggested change
try await client.initialize()

Comment on lines +421 to +430
if let pathComponents = URLComponents(string: pathToUse) {
components.path = pathComponents.path
// Preserve any query parameters from the endpoint path
if let queryItems = pathComponents.queryItems {
components.queryItems = queryItems
}
} else {
components.path = pathToUse
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Slightly more comprehensive version:

Suggested change
if let pathComponents = URLComponents(string: pathToUse) {
components.path = pathComponents.path
// Preserve any query parameters from the endpoint path
if let queryItems = pathComponents.queryItems {
components.queryItems = queryItems
}
} else {
components.path = pathToUse
}
// For relative paths, preserve the scheme, host, and port.
let pathToUse = path.starts(with: "/") ? path : "/\(path)"
if let relativeComponents = URLComponents(string: pathToUse) {
// Set path (prefer percentEncodedPath if present to avoid double-encoding)
if let percentEncodedPath = relativeComponents.percentEncodedPath.isEmpty
? nil : relativeComponents.percentEncodedPath
{
components.percentEncodedPath = percentEncodedPath
} else {
components.path = relativeComponents.path
}
// Set query via queryItems to ensure correct percent-encoding
if let queryItems = relativeComponents.queryItems, !queryItems.isEmpty {
components.queryItems = queryItems
} else {
components.queryItems = nil
}
// Preserve fragment if provided
components.fragment = relativeComponents.fragment
} else {
// Fallback: set path directly if components parsing fails
components.path = pathToUse
}

@mattt mattt changed the title URL formatting endpoints returned from sse connection Preserve query in endpoint URL sent by SSE transport Aug 29, 2025
@mattt
Copy link
Contributor

mattt commented Aug 29, 2025

Hi @jalingo. Thanks for your contribution. Making sure I correctly understand the motivation behind this PR: You're using query parameters (e.g. ?session={id}) to distinguish between messages from different contexts. Is that right?

@mattt
Copy link
Contributor

mattt commented Aug 29, 2025

We should give this new behavior some test coverage. Here are some tests that pass with the code I suggested above:

        @Test("Query parameter encoding in relative endpoint URL", .sseTransportSetup)
        func testQueryEncodingInRelativeEndpoint() async throws {
            let configuration = URLSessionConfiguration.ephemeral
            configuration.protocolClasses = [MockSSEURLProtocol.self]

            let transport = SSEClientTransport(
                endpoint: testEndpoint,
                configuration: configuration
            )

            let rawEndpoint =
                "/messages/with space?q=hello world&sym=%&plus=+&slash=a/b&emoji=😀#frag"
            let capturedRequest = CapturedRequest()

            await MockSSEURLProtocol.setHandler { request in
                if request.httpMethod == "GET" {
                    let response = HTTPURLResponse(
                        url: testEndpoint,
                        statusCode: 200,
                        httpVersion: "HTTP/1.1",
                        headerFields: ["Content-Type": "text/event-stream"]
                    )!
                    return (
                        response,
                        Data(
                            """
                            event: endpoint
                            data: \(rawEndpoint)

                            """.utf8)
                    )
                } else if request.httpMethod == "POST" {
                    await capturedRequest.setValue(request)
                    let response = HTTPURLResponse(
                        url: request.url!,
                        statusCode: 200,
                        httpVersion: "HTTP/1.1",
                        headerFields: ["Content-Type": "application/json"]
                    )!
                    return (response, Data())
                } else {
                    throw MCPError.internalError("Unexpected request method")
                }
            }

            try await transport.connect()
            try await transport.send(Data())

            let postReq = await capturedRequest.getValue()
            #expect(postReq != nil)
            guard let url = postReq?.url,
                let comps = URLComponents(url: url, resolvingAgainstBaseURL: false)
            else {
                Issue.record("Invalid URL in captured POST request")
                return
            }

            #expect(comps.scheme == testEndpoint.scheme)
            #expect(comps.host == testEndpoint.host)
            #expect(comps.path == "/messages/with space")

            let expectedItems = [
                URLQueryItem(name: "q", value: "hello world"),
                URLQueryItem(name: "sym", value: "%"),
                URLQueryItem(name: "plus", value: "+"),
                URLQueryItem(name: "slash", value: "a/b"),
                URLQueryItem(name: "emoji", value: "😀"),
            ]
            #expect(comps.queryItems?.count == expectedItems.count)
            if let items = comps.queryItems {
                for (idx, item) in items.enumerated() {
                    #expect(item.name == expectedItems[idx].name)
                    #expect(item.value == expectedItems[idx].value)
                }
            }

            #expect(comps.fragment == "frag")

            await transport.disconnect()
        }

        @Test("No double-encoding for already percent-encoded inputs", .sseTransportSetup)
        func testNoDoubleEncoding() async throws {
            let configuration = URLSessionConfiguration.ephemeral
            configuration.protocolClasses = [MockSSEURLProtocol.self]

            let transport = SSEClientTransport(
                endpoint: testEndpoint,
                configuration: configuration
            )

            let rawEndpoint = "/messages/%E2%9C%93?q=a%2Bb"
            let capturedRequest = CapturedRequest()

            await MockSSEURLProtocol.setHandler { request in
                if request.httpMethod == "GET" {
                    let response = HTTPURLResponse(
                        url: testEndpoint,
                        statusCode: 200,
                        httpVersion: "HTTP/1.1",
                        headerFields: ["Content-Type": "text/event-stream"]
                    )!
                    return (
                        response,
                        Data(
                            """
                            event: endpoint
                            data: \(rawEndpoint)

                            """.utf8)
                    )
                } else if request.httpMethod == "POST" {
                    await capturedRequest.setValue(request)
                    let response = HTTPURLResponse(
                        url: request.url!,
                        statusCode: 200,
                        httpVersion: "HTTP/1.1",
                        headerFields: ["Content-Type": "application/json"]
                    )!
                    return (response, Data())
                } else {
                    throw MCPError.internalError("Unexpected request method")
                }
            }

            try await transport.connect()
            try await transport.send(Data())

            guard let url = await capturedRequest.getValue()?.url,
                let comps = URLComponents(url: url, resolvingAgainstBaseURL: false)
            else {
                Issue.record("Invalid URL in captured POST request")
                return
            }

            #expect(comps.path == "/messages/✓")
            #expect(comps.queryItems?.first(where: { $0.name == "q" })?.value == "a+b")

            await transport.disconnect()
        }
    }

@jalingo
Copy link
Author

jalingo commented Aug 29, 2025

Actually, just found out I'm not allowed to contribute to open source anymore; but when the endpoint was delivered for all future posts it was getting malformed.

Also not my experience with the init, but maybe that was added to the branch recently.

@jalingo jalingo closed this Aug 29, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants