Skip to content
Open
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
223 changes: 215 additions & 8 deletions shared/McpSamples.Shared/OpenApi/McpDocumentTransformer.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Net.Mime;
using System.Text.Json.Nodes;

using McpSamples.Shared.Configurations;
Expand All @@ -6,17 +7,19 @@
using Microsoft.AspNetCore.OpenApi;
using Microsoft.OpenApi;

using ModelContextProtocol.Protocol;

namespace McpSamples.Shared.OpenApi;

/// <summary>
/// This represents a transformer entity that defines the OpenAPI document for the MCP server.
/// </summary>
/// <param name="appsettings"><see cref="AppSettings"/> instance.</param>
/// <param name="accessor"><see cref="IHttpContextAccessor"/> instance.</param>
public class McpDocumentTransformer<T>(T appsettings, IHttpContextAccessor accessor) : IOpenApiDocumentTransformer where T : AppSettings, new()
public sealed class McpDocumentTransformer<T>(T appsettings, IHttpContextAccessor accessor) : IOpenApiDocumentTransformer where T : AppSettings, new()
{
/// <inheritdoc />
public Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerContext context, CancellationToken cancellationToken)
public async Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerContext context, CancellationToken cancellationToken)
{
document.Info = new OpenApiInfo
{
Expand All @@ -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<string, IOpenApiExtension>
{
["x-ms-agentic-protocol"] = new JsonNodeExtension(JsonValue.Create("mcp-streamable-1.0"))
Expand All @@ -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<string, OpenApiMediaType>
{
[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<string, OpenApiMediaType>
{
[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<string, OpenApiMediaType>
{
[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<string, OpenApiMediaType>
{
[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<string, OpenApiMediaType>
{
[MediaTypeNames.Application.Json] = new()
{
Schema = new OpenApiSchemaReference(nameof(JsonRpcError), document),
},
},
},
},
RequestBody = new OpenApiRequestBody
{
Required = true,
Content = new Dictionary<string, OpenApiMediaType>
{
[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<string, OpenApiMediaType>
{
[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<string, OpenApiMediaType>
{
[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<string, OpenApiMediaType>
{
[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<string, OpenApiMediaType>
{
[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<string, OpenApiMediaType>
{
[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<string, OpenApiMediaType>
{
[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;
}
}
}