diff --git a/shared/McpSamples.Shared/OpenApi/McpDocumentTransformer.cs b/shared/McpSamples.Shared/OpenApi/McpDocumentTransformer.cs index 8beb9b07..4a12a7a0 100644 --- a/shared/McpSamples.Shared/OpenApi/McpDocumentTransformer.cs +++ b/shared/McpSamples.Shared/OpenApi/McpDocumentTransformer.cs @@ -1,3 +1,4 @@ +using System.Net.Mime; using System.Text.Json.Nodes; using McpSamples.Shared.Configurations; @@ -6,6 +7,8 @@ using Microsoft.AspNetCore.OpenApi; using Microsoft.OpenApi; +using ModelContextProtocol.Protocol; + namespace McpSamples.Shared.OpenApi; /// @@ -13,10 +16,10 @@ namespace McpSamples.Shared.OpenApi; /// /// instance. /// instance. -public class McpDocumentTransformer(T appsettings, IHttpContextAccessor accessor) : IOpenApiDocumentTransformer where T : AppSettings, new() +public sealed class McpDocumentTransformer(T appsettings, IHttpContextAccessor accessor) : IOpenApiDocumentTransformer where T : AppSettings, new() { /// - public Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerContext context, CancellationToken cancellationToken) + public async Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerContext context, CancellationToken cancellationToken) { document.Info = new OpenApiInfo { @@ -33,10 +36,38 @@ public Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerC : "http://localhost:8080/" } ]; + + // Register JSON-RPC schemas as components + var jsonRpcRequest = await context.GetOrCreateSchemaAsync(typeof(JsonRpcRequest), cancellationToken: cancellationToken); + var jsonRpcNotification = await context.GetOrCreateSchemaAsync(typeof(JsonRpcNotification), cancellationToken: cancellationToken); + var jsonRpcResponse = await context.GetOrCreateSchemaAsync(typeof(JsonRpcResponse), cancellationToken: cancellationToken); + var jsonRpcError = await context.GetOrCreateSchemaAsync(typeof(JsonRpcError), cancellationToken: cancellationToken); + + document.AddComponent(nameof(JsonRpcRequest), jsonRpcRequest); + document.AddComponent(nameof(JsonRpcNotification), jsonRpcNotification); + document.AddComponent(nameof(JsonRpcResponse), jsonRpcResponse); + document.AddComponent(nameof(JsonRpcError), jsonRpcError); + + // Build oneOf schema for request body per MCP Streamable HTTP spec: + // "The body of the POST request MUST be a single JSON-RPC request, notification, or response." + var jsonRpcMessage = new OpenApiSchema + { + OneOf = + [ + new OpenApiSchemaReference(nameof(JsonRpcRequest), document), + new OpenApiSchemaReference(nameof(JsonRpcNotification), document), + new OpenApiSchemaReference(nameof(JsonRpcResponse), document), + ] + }; + document.AddComponent("JsonRpcMessage", jsonRpcMessage); + var pathItem = new OpenApiPathItem(); + + // POST /mcp - Send a JSON-RPC request, notification, or response pathItem.AddOperation(HttpMethod.Post, new OpenApiOperation { Summary = "Invoke operation", + Description = "Send a JSON-RPC request, notification, or response to the MCP server.", Extensions = new Dictionary { ["x-ms-agentic-protocol"] = new JsonNodeExtension(JsonValue.Create("mcp-streamable-1.0")) @@ -46,14 +77,190 @@ public Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerC { ["200"] = new OpenApiResponse { - Description = "Success", - } - } + Description = "Success - returned when the input is a JSON-RPC request", + Content = new Dictionary + { + [MediaTypeNames.Application.Json] = new() + { + Schema = new OpenApiSchemaReference(nameof(JsonRpcResponse), document), + }, + [MediaTypeNames.Text.EventStream] = new() + { + Schema = new OpenApiSchema + { + Type = JsonSchemaType.String, + Description = "Server-Sent Events stream containing JSON-RPC responses", + }, + }, + }, + }, + ["202"] = new OpenApiResponse + { + Description = "Accepted - returned when the input is a JSON-RPC response or notification", + }, + ["400"] = new OpenApiResponse + { + Description = "Bad Request - invalid JSON-RPC message, unsupported protocol version, or missing/invalid session ID", + Content = new Dictionary + { + [MediaTypeNames.Application.Json] = new() + { + Schema = new OpenApiSchemaReference(nameof(JsonRpcError), document), + }, + }, + }, + ["403"] = new OpenApiResponse + { + Description = "Forbidden - invalid Origin header, or the authenticated user does not match the user who initiated the session", + Content = new Dictionary + { + [MediaTypeNames.Application.Json] = new() + { + Schema = new OpenApiSchemaReference(nameof(JsonRpcError), document), + }, + }, + }, + ["404"] = new OpenApiResponse + { + Description = "Not Found - the specified session ID was not found", + Content = new Dictionary + { + [MediaTypeNames.Application.Json] = new() + { + Schema = new OpenApiSchemaReference(nameof(JsonRpcError), document), + }, + }, + }, + ["406"] = new OpenApiResponse + { + Description = "Not Acceptable - client must accept both application/json and text/event-stream", + Content = new Dictionary + { + [MediaTypeNames.Application.Json] = new() + { + Schema = new OpenApiSchemaReference(nameof(JsonRpcError), document), + }, + }, + }, + }, + RequestBody = new OpenApiRequestBody + { + Required = true, + Content = new Dictionary + { + [MediaTypeNames.Application.Json] = new() + { + Schema = new OpenApiSchemaReference("JsonRpcMessage", document), + }, + }, + }, + }); + + // GET /mcp - Open SSE stream for server-initiated messages (stateful mode only) + pathItem.AddOperation(HttpMethod.Get, new OpenApiOperation + { + Summary = "Open SSE stream", + Description = "Open a Server-Sent Events stream to receive server-initiated JSON-RPC messages. Only available in stateful mode.", + OperationId = "OpenMCPStream", + Responses = new OpenApiResponses + { + ["200"] = new OpenApiResponse + { + Description = "SSE stream opened successfully", + Content = new Dictionary + { + [MediaTypeNames.Text.EventStream] = new() + { + Schema = new OpenApiSchema + { + Type = JsonSchemaType.String, + Description = "Server-Sent Events stream containing JSON-RPC messages", + }, + }, + }, + }, + ["400"] = new OpenApiResponse + { + Description = "Bad Request - missing session ID, unsupported protocol version, or invalid Last-Event-ID", + Content = new Dictionary + { + [MediaTypeNames.Application.Json] = new() + { + Schema = new OpenApiSchemaReference(nameof(JsonRpcError), document), + }, + }, + }, + ["404"] = new OpenApiResponse + { + Description = "Not Found - the specified session ID was not found", + Content = new Dictionary + { + [MediaTypeNames.Application.Json] = new() + { + Schema = new OpenApiSchemaReference(nameof(JsonRpcError), document), + }, + }, + }, + ["405"] = new OpenApiResponse + { + Description = "Method Not Allowed - server does not offer an SSE stream at this endpoint", + }, + ["406"] = new OpenApiResponse + { + Description = "Not Acceptable - client must accept text/event-stream", + Content = new Dictionary + { + [MediaTypeNames.Application.Json] = new() + { + Schema = new OpenApiSchemaReference(nameof(JsonRpcError), document), + }, + }, + }, + }, + }); + + // DELETE /mcp - Terminate a session (stateful mode only) + pathItem.AddOperation(HttpMethod.Delete, new OpenApiOperation + { + Summary = "Terminate session", + Description = "Terminate an active MCP session and clean up server-side resources. Only available in stateful mode.", + OperationId = "TerminateMCPSession", + Responses = new OpenApiResponses + { + ["200"] = new OpenApiResponse + { + Description = "Session terminated successfully", + }, + ["400"] = new OpenApiResponse + { + Description = "Bad Request - missing session ID or unsupported protocol version", + Content = new Dictionary + { + [MediaTypeNames.Application.Json] = new() + { + Schema = new OpenApiSchemaReference(nameof(JsonRpcError), document), + }, + }, + }, + ["404"] = new OpenApiResponse + { + Description = "Not Found - the specified session ID was not found", + Content = new Dictionary + { + [MediaTypeNames.Application.Json] = new() + { + Schema = new OpenApiSchemaReference(nameof(JsonRpcError), document), + }, + }, + }, + ["405"] = new OpenApiResponse + { + Description = "Method Not Allowed - server does not allow clients to terminate sessions", + }, + }, }); document.Paths ??= []; document.Paths.Add("/mcp", pathItem); - - return Task.CompletedTask; } -} \ No newline at end of file +}