Skip to content

Commit 29e146b

Browse files
authored
Merge pull request #21 from JuliaSMLM/fix/protocol-version-negotiation-v2
fix: Correct protocol version negotiation per MCP spec
2 parents e35733b + 13db571 commit 29e146b

4 files changed

Lines changed: 45 additions & 99 deletions

File tree

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name = "ModelContextProtocol"
22
uuid = "c58f755f-f2a7-4f48-bf29-4e9659b78499"
33
authors = ["klidke@unm.edu"]
4-
version = "0.3.0"
4+
version = "0.3.1"
55

66
[deps]
77
Base64 = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f"

src/protocol/handlers.jl

Lines changed: 10 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -126,28 +126,15 @@ Handle MCP protocol initialization requests by setting up the server and returni
126126
- `HandlerResult`: Contains the server's capabilities and configuration
127127
"""
128128
function handle_initialize(ctx::RequestContext, params::InitializeParams)::HandlerResult
129-
# Currently we only support MCP protocol version 2025-06-18
130-
# In the future, this could be extended to support multiple versions
131-
SUPPORTED_PROTOCOL_VERSIONS = ["2025-06-18"]
132-
supported_version = SUPPORTED_PROTOCOL_VERSIONS[1] # Use the only supported version for now
133-
134-
# Check if client requested a specific version
135-
if !isnothing(params.protocolVersion) && params.protocolVersion != supported_version
136-
# Return error for unsupported versions
137-
error_info = ErrorInfo(
138-
code = ErrorCodes.INVALID_PARAMS,
139-
message = "Unsupported protocol version",
140-
data = LittleDict{String,Any}(
141-
"supported" => [supported_version],
142-
"requested" => params.protocolVersion
143-
)
144-
)
145-
return HandlerResult(
146-
response=JSONRPCError(
147-
id=ctx.request_id,
148-
error=error_info
149-
)
150-
)
129+
# MCP protocol version we support
130+
# Per spec: if client requests unsupported version, server MUST respond with
131+
# a version it supports (not error). Client then decides if it can work with that.
132+
SERVER_PROTOCOL_VERSION = "2025-06-18"
133+
134+
# Log version negotiation for debugging
135+
client_version = params.protocolVersion
136+
if !isnothing(client_version) && client_version != SERVER_PROTOCOL_VERSION
137+
@debug "Version negotiation" client_requested=client_version server_supports=SERVER_PROTOCOL_VERSION
151138
end
152139

153140
# Get full capabilities including available tools and resources
@@ -163,7 +150,7 @@ function handle_initialize(ctx::RequestContext, params::InitializeParams)::Handl
163150
"version" => ctx.server.config.version
164151
),
165152
capabilities=current_capabilities,
166-
protocolVersion=supported_version,
153+
protocolVersion=SERVER_PROTOCOL_VERSION,
167154
instructions=ctx.server.config.instructions
168155
)
169156

src/transports/http.jl

Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -302,28 +302,13 @@ function handle_request(transport::HttpTransport, stream::HTTP.Stream)
302302
return nothing
303303
end
304304

305-
# Check MCP-Protocol-Version header - required for 2025-06-18 spec
305+
# MCP-Protocol-Version header check
306+
# Per spec: version negotiation happens at JSON-RPC level (initialize request/response).
307+
# The header is for subsequent requests after negotiation. We log mismatches but don't
308+
# error - the JSON-RPC layer handles version negotiation properly.
306309
client_protocol_version = HTTP.header(request, "MCP-Protocol-Version", "")
307310
if !isempty(client_protocol_version) && client_protocol_version != transport.protocol_version
308-
@debug "Unsupported protocol version" client=client_protocol_version server=transport.protocol_version
309-
HTTP.setstatus(stream, 400)
310-
HTTP.setheader(stream, "Content-Type" => "application/json")
311-
error_response = JSON3.write(Dict(
312-
"jsonrpc" => "2.0",
313-
"error" => Dict(
314-
"code" => -32602,
315-
"message" => "Unsupported protocol version",
316-
"data" => Dict(
317-
"supported" => [transport.protocol_version],
318-
"requested" => client_protocol_version
319-
)
320-
),
321-
"id" => nothing
322-
))
323-
HTTP.setheader(stream, "Content-Length" => string(length(error_response)))
324-
HTTP.startwrite(stream) # Ensure headers are sent
325-
write(stream, error_response)
326-
return nothing
311+
@debug "Client protocol version differs from server" client=client_protocol_version server=transport.protocol_version
327312
end
328313

329314
# Security: Validate Origin header if configured

test/transports/test_streamable_http.jl

Lines changed: 29 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -211,104 +211,78 @@
211211
Base.close(timer)
212212
end
213213

214-
@testset "Protocol Version Validation" begin
214+
@testset "Protocol Version Negotiation" begin
215+
# Per MCP spec: version negotiation happens at JSON-RPC level.
216+
# Server MUST respond with a version it supports (not error).
217+
# Client then decides if it can work with that version.
218+
215219
port = 15090 + rand(1:1000)
216-
220+
217221
transport = HttpTransport(port=port, protocol_version="2025-06-18")
218-
222+
219223
server = mcp_server(
220224
name = "version-test",
221225
version = "1.0.0"
222226
)
223-
227+
224228
server.transport = transport
225229
ModelContextProtocol.connect(transport)
226-
230+
227231
server_task = @async start!(server)
228232
sleep(2)
229-
230-
# First, properly initialize the server
233+
234+
# Test 1: Client requests newer version, server responds with its version
231235
init_response = HTTP.post(
232236
"http://127.0.0.1:$port/",
233237
["Content-Type" => "application/json",
234-
"MCP-Protocol-Version" => "2025-06-18",
238+
"MCP-Protocol-Version" => "2025-11-25", # Client sends newer version
235239
"Accept" => "application/json, text/event-stream"],
236240
JSON3.write(Dict(
237241
"jsonrpc" => "2.0",
238242
"method" => "initialize",
239243
"params" => Dict(
240-
"protocolVersion" => "2025-06-18",
244+
"protocolVersion" => "2025-11-25", # Client requests newer version
241245
"capabilities" => Dict(),
242246
"clientInfo" => Dict("name" => "test", "version" => "1.0")
243247
),
244-
"id" => 0
248+
"id" => 1
245249
))
246250
)
247251
@test init_response.status == 200
252+
result = JSON3.read(String(init_response.body))
253+
@test haskey(result, "result")
254+
# Server responds with its supported version (not error)
255+
@test result["result"]["protocolVersion"] == "2025-06-18"
256+
248257
session_id = HTTP.header(init_response, "Mcp-Session-Id", "")
249-
250-
# Request with wrong protocol version header - should fail
251-
try
252-
response = HTTP.post(
253-
"http://127.0.0.1:$port/",
254-
["Content-Type" => "application/json",
255-
"MCP-Protocol-Version" => "2024-11-05", # Wrong version
256-
"Accept" => "application/json, text/event-stream"],
257-
JSON3.write(Dict(
258-
"jsonrpc" => "2.0",
259-
"method" => "initialize",
260-
"params" => Dict(
261-
"protocolVersion" => "2025-06-18",
262-
"capabilities" => Dict(),
263-
"clientInfo" => Dict("name" => "test", "version" => "1.0")
264-
),
265-
"id" => 1
266-
))
267-
)
268-
@test false # Should not succeed
269-
catch e
270-
if e isa HTTP.ExceptionRequest.StatusError
271-
@test e.response.status == 400 # Should return 400 for wrong protocol version
272-
# Verify the error message
273-
body = String(e.response.body)
274-
msg = JSON3.read(body)
275-
@test msg["error"]["code"] == -32602
276-
@test contains(msg["error"]["message"], "protocol version")
277-
else
278-
rethrow(e)
279-
end
280-
end
281-
282-
# Request with wrong protocol version in params (but correct header)
283-
# This is a different case - the transport accepts it but the server should reject
258+
259+
# Test 2: Client requests older version, server still responds with its version
284260
response = HTTP.post(
285261
"http://127.0.0.1:$port/",
286262
["Content-Type" => "application/json",
287-
"MCP-Protocol-Version" => "2025-06-18",
288-
"Mcp-Session-Id" => session_id,
263+
"MCP-Protocol-Version" => "2024-11-05", # Older version header
289264
"Accept" => "application/json, text/event-stream"],
290265
JSON3.write(Dict(
291266
"jsonrpc" => "2.0",
292267
"method" => "initialize",
293268
"params" => Dict(
294-
"protocolVersion" => "2024-11-05", # Wrong version
269+
"protocolVersion" => "2024-11-05", # Older version
295270
"capabilities" => Dict(),
296271
"clientInfo" => Dict("name" => "test", "version" => "1.0")
297272
),
298273
"id" => 2
299274
))
300275
)
301-
302-
# Should return error
303-
@test response.status == 200 # Still 200 for JSON-RPC errors
276+
@test response.status == 200
304277
result = JSON3.read(String(response.body))
305-
@test haskey(result, "error")
306-
@test result["error"]["code"] == -32602 # Invalid params
307-
278+
@test haskey(result, "result")
279+
# Server responds with its version, client decides compatibility
280+
@test result["result"]["protocolVersion"] == "2025-06-18"
281+
308282
# Clean up
309283
server.active = false
310284
ModelContextProtocol.close(transport)
311-
285+
312286
timer = Timer(2)
313287
while !istaskdone(server_task) && isopen(timer)
314288
sleep(0.1)

0 commit comments

Comments
 (0)