Pass session id's to MCP endpoints. (#466)

* Pass session id's to MCP endpoints.

* Update src/ModelContextProtocol/Client/McpClient.cs

Co-authored-by: Stephen Halter <halter73@gmail.com>

* Update src/ModelContextProtocol.AspNetCore/StreamableHttpHandler.cs

Co-authored-by: Stephen Halter <halter73@gmail.com>

* Make init callback asynchronous.

* Address feedback.

---------

Co-authored-by: Stephen Halter <halter73@gmail.com>
This commit is contained in:
Eirik Tsarpalis 2025-06-12 16:56:57 +02:00 • committed by GitHub
commit d7d9eab3f8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 166 additions and 21 deletions

View file

@ -16,6 +16,8 @@ public class TestServerTransport : ITransport
public Action<JsonRpcMessage>? OnMessageSent { get; set; }
public string? SessionId => null;
public TestServerTransport()
{
_messageChannel = Channel.CreateUnbounded<JsonRpcMessage>(new UnboundedChannelOptions

View file

@ -52,6 +52,15 @@ public abstract class HttpServerIntegrationTests : LoggedTest, IClassFixture<Sse
// Assert
Assert.NotNull(client.ServerCapabilities);
Assert.NotNull(client.ServerInfo);
if (ClientTransportOptions.Endpoint.AbsolutePath.EndsWith("/sse"))
{
Assert.Null(client.SessionId);
}
else
{
Assert.NotNull(client.SessionId);
}
}
[Fact]
@ -90,6 +99,35 @@ public abstract class HttpServerIntegrationTests : LoggedTest, IClassFixture<Sse
Assert.Equal("Echo: Hello MCP!", textContent.Text);
}
[Fact]
public async Task CallTool_EchoSessionId_ReturnsTheSameSessionId()
{
// arrange
// act
await using var client = await GetClientAsync();
var result1 = await client.CallToolAsync("echoSessionId", cancellationToken: TestContext.Current.CancellationToken);
var result2 = await client.CallToolAsync("echoSessionId", cancellationToken: TestContext.Current.CancellationToken);
var result3 = await client.CallToolAsync("echoSessionId", cancellationToken: TestContext.Current.CancellationToken);
// assert
Assert.NotNull(result1);
Assert.NotNull(result2);
Assert.NotNull(result3);
Assert.False(result1.IsError);
Assert.False(result2.IsError);
Assert.False(result3.IsError);
var textContent1 = Assert.Single(result1.Content);
var textContent2 = Assert.Single(result2.Content);
var textContent3 = Assert.Single(result3.Content);
Assert.NotNull(textContent1.Text);
Assert.Equal(textContent1.Text, textContent2.Text);
Assert.Equal(textContent1.Text, textContent3.Text);
}
[Fact]
public async Task ListResources_Sse_TestServer()
{

View file

@ -133,6 +133,16 @@ internal static class Program
"""),
},
new Tool()
{
Name = "echoSessionId",
Description = "Echoes the session id back to the client.",
InputSchema = JsonSerializer.Deserialize<JsonElement>("""
{
"type": "object"
}
""", McpJsonUtilities.DefaultOptions),
},
new Tool()
{
Name = "sampleLLM",
Description = "Samples from an LLM using MCP's sampling feature.",
@ -170,6 +180,13 @@ internal static class Program
Content = [new Content() { Text = "Echo: " + message.ToString(), Type = "text" }]
};
}
else if (request.Params?.Name == "echoSessionId")
{
return new CallToolResponse()
{
Content = [new Content() { Text = request.Server.SessionId, Type = "text" }]
};
}
else if (request.Params?.Name == "sampleLLM")
{
if (request.Params?.Arguments is null ||

View file

@ -128,6 +128,16 @@ public class Program
""", McpJsonUtilities.DefaultOptions),
},
new Tool()
{
Name = "echoSessionId",
Description = "Echoes the session id back to the client.",
InputSchema = JsonSerializer.Deserialize<JsonElement>("""
{
"type": "object"
}
""", McpJsonUtilities.DefaultOptions),
},
new Tool()
{
Name = "sampleLLM",
Description = "Samples from an LLM using MCP's sampling feature.",
@ -168,6 +178,13 @@ public class Program
Content = [new Content() { Text = "Echo: " + message.ToString(), Type = "text" }]
};
}
else if (request.Params.Name == "echoSessionId")
{
return new CallToolResponse()
{
Content = [new Content() { Text = request.Server.SessionId, Type = "text" }]
};
}
else if (request.Params.Name == "sampleLLM")
{
if (request.Params.Arguments is null ||

View file

@ -109,6 +109,7 @@ public class McpClientFactoryTests
private readonly Channel<JsonRpcMessage> _channel = Channel.CreateUnbounded<JsonRpcMessage>();
public bool IsConnected => true;
public string? SessionId => null;
public ChannelReader<JsonRpcMessage> MessageReader => _channel.Reader;

View file

@ -54,6 +54,8 @@ public partial class ClientIntegrationTests : LoggedTest, IClassFixture<ClientIn
Assert.NotNull(client.ServerInfo);
if (clientId != "everything") // Note: Comment the below assertion back when the everything server is updated to provide instructions
Assert.NotNull(client.ServerInstructions);
Assert.Null(client.SessionId);
}
[Theory]
@ -94,6 +96,22 @@ public partial class ClientIntegrationTests : LoggedTest, IClassFixture<ClientIn
Assert.Equal("Echo: Hello MCP!", textContent.Text);
}
[Fact]
public async Task CallTool_Stdio_EchoSessionId_ReturnsNull()
{
// arrange
// act
await using var client = await _fixture.CreateClientAsync("test_server");
var result = await client.CallToolAsync("echoSessionId", cancellationToken: TestContext.Current.CancellationToken);
// assert
Assert.NotNull(result);
Assert.False(result.IsError);
var textContent = Assert.Single(result.Content, c => c.Type == "text");
Assert.Null(textContent.Text);
}
[Theory]
[MemberData(nameof(GetClients))]
public async Task CallTool_Stdio_ViaAIFunction_EchoServer(string clientId)

View file

@ -650,6 +650,7 @@ public class McpServerTests : LoggedTest
public ValueTask DisposeAsync() => default;
public string? SessionId => throw new NotImplementedException();
public Implementation? ClientInfo => throw new NotImplementedException();
public IServiceProvider? Services => throw new NotImplementedException();
public LoggingLevel? LoggingLevel => throw new NotImplementedException();