@@ -16,7 +16,7 @@ namespace McpSamples.Shared.OpenApi;
1616/// </summary>
1717/// <param name="appsettings"><see cref="AppSettings"/> instance.</param>
1818/// <param name="accessor"><see cref="IHttpContextAccessor"/> instance.</param>
19- public class McpDocumentTransformer < T > ( T appsettings , IHttpContextAccessor accessor ) : IOpenApiDocumentTransformer where T : AppSettings , new ( )
19+ public sealed class McpDocumentTransformer < T > ( T appsettings , IHttpContextAccessor accessor ) : IOpenApiDocumentTransformer where T : AppSettings , new ( )
2020{
2121 /// <inheritdoc />
2222 public async Task TransformAsync ( OpenApiDocument document , OpenApiDocumentTransformerContext context , CancellationToken cancellationToken )
@@ -37,18 +37,37 @@ public async Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransf
3737 }
3838 ] ;
3939
40- var jsonRpcResponse = await context . GetOrCreateSchemaAsync ( typeof ( JsonRpcResponse ) , cancellationToken : cancellationToken ) ;
41-
40+ // Register JSON-RPC schemas as components
4241 var jsonRpcRequest = await context . GetOrCreateSchemaAsync ( typeof ( JsonRpcRequest ) , cancellationToken : cancellationToken ) ;
42+ var jsonRpcNotification = await context . GetOrCreateSchemaAsync ( typeof ( JsonRpcNotification ) , cancellationToken : cancellationToken ) ;
43+ var jsonRpcResponse = await context . GetOrCreateSchemaAsync ( typeof ( JsonRpcResponse ) , cancellationToken : cancellationToken ) ;
44+ var jsonRpcError = await context . GetOrCreateSchemaAsync ( typeof ( JsonRpcError ) , cancellationToken : cancellationToken ) ;
4345
46+ document . AddComponent ( nameof ( JsonRpcRequest ) , jsonRpcRequest ) ;
47+ document . AddComponent ( nameof ( JsonRpcNotification ) , jsonRpcNotification ) ;
4448 document . AddComponent ( nameof ( JsonRpcResponse ) , jsonRpcResponse ) ;
49+ document . AddComponent ( nameof ( JsonRpcError ) , jsonRpcError ) ;
4550
46- document . AddComponent ( nameof ( JsonRpcRequest ) , jsonRpcRequest ) ;
51+ // Build oneOf schema for request body per MCP Streamable HTTP spec:
52+ // "The body of the POST request MUST be a single JSON-RPC request, notification, or response."
53+ var jsonRpcMessage = new OpenApiSchema
54+ {
55+ OneOf =
56+ [
57+ new OpenApiSchemaReference ( nameof ( JsonRpcRequest ) , document ) ,
58+ new OpenApiSchemaReference ( nameof ( JsonRpcNotification ) , document ) ,
59+ new OpenApiSchemaReference ( nameof ( JsonRpcResponse ) , document ) ,
60+ ]
61+ } ;
62+ document . AddComponent ( "JsonRpcMessage" , jsonRpcMessage ) ;
4763
4864 var pathItem = new OpenApiPathItem ( ) ;
65+
66+ // POST /mcp - Send a JSON-RPC request, notification, or response
4967 pathItem . AddOperation ( HttpMethod . Post , new OpenApiOperation
5068 {
5169 Summary = "Invoke operation" ,
70+ Description = "Send a JSON-RPC request, notification, or response to the MCP server." ,
5271 Extensions = new Dictionary < string , IOpenApiExtension >
5372 {
5473 [ "x-ms-agentic-protocol" ] = new JsonNodeExtension ( JsonValue . Create ( "mcp-streamable-1.0" ) )
@@ -58,15 +77,71 @@ public async Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransf
5877 {
5978 [ "200" ] = new OpenApiResponse
6079 {
61- Description = "Success" ,
80+ Description = "Success - returned when the input is a JSON-RPC request " ,
6281 Content = new Dictionary < string , OpenApiMediaType >
6382 {
6483 [ MediaTypeNames . Application . Json ] = new ( )
6584 {
6685 Schema = new OpenApiSchemaReference ( nameof ( JsonRpcResponse ) , document ) ,
6786 } ,
87+ [ MediaTypeNames . Text . EventStream ] = new ( )
88+ {
89+ Schema = new OpenApiSchema
90+ {
91+ Type = JsonSchemaType . String ,
92+ Description = "Server-Sent Events stream containing JSON-RPC responses" ,
93+ } ,
94+ } ,
95+ } ,
96+ } ,
97+ [ "202" ] = new OpenApiResponse
98+ {
99+ Description = "Accepted - returned when the input is a JSON-RPC response or notification" ,
100+ } ,
101+ [ "400" ] = new OpenApiResponse
102+ {
103+ Description = "Bad Request - invalid JSON-RPC message, unsupported protocol version, or missing/invalid session ID" ,
104+ Content = new Dictionary < string , OpenApiMediaType >
105+ {
106+ [ MediaTypeNames . Application . Json ] = new ( )
107+ {
108+ Schema = new OpenApiSchemaReference ( nameof ( JsonRpcError ) , document ) ,
109+ } ,
110+ } ,
111+ } ,
112+ [ "403" ] = new OpenApiResponse
113+ {
114+ Description = "Forbidden - invalid Origin header, or the authenticated user does not match the user who initiated the session" ,
115+ Content = new Dictionary < string , OpenApiMediaType >
116+ {
117+ [ MediaTypeNames . Application . Json ] = new ( )
118+ {
119+ Schema = new OpenApiSchemaReference ( nameof ( JsonRpcError ) , document ) ,
120+ } ,
68121 } ,
69- }
122+ } ,
123+ [ "404" ] = new OpenApiResponse
124+ {
125+ Description = "Not Found - the specified session ID was not found" ,
126+ Content = new Dictionary < string , OpenApiMediaType >
127+ {
128+ [ MediaTypeNames . Application . Json ] = new ( )
129+ {
130+ Schema = new OpenApiSchemaReference ( nameof ( JsonRpcError ) , document ) ,
131+ } ,
132+ } ,
133+ } ,
134+ [ "406" ] = new OpenApiResponse
135+ {
136+ Description = "Not Acceptable - client must accept both application/json and text/event-stream" ,
137+ Content = new Dictionary < string , OpenApiMediaType >
138+ {
139+ [ MediaTypeNames . Application . Json ] = new ( )
140+ {
141+ Schema = new OpenApiSchemaReference ( nameof ( JsonRpcError ) , document ) ,
142+ } ,
143+ } ,
144+ } ,
70145 } ,
71146 RequestBody = new OpenApiRequestBody
72147 {
@@ -75,13 +150,117 @@ public async Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransf
75150 {
76151 [ MediaTypeNames . Application . Json ] = new ( )
77152 {
78- Schema = new OpenApiSchemaReference ( nameof ( JsonRpcRequest ) , document ) ,
153+ Schema = new OpenApiSchemaReference ( "JsonRpcMessage" , document ) ,
154+ } ,
155+ } ,
156+ } ,
157+ } ) ;
158+
159+ // GET /mcp - Open SSE stream for server-initiated messages (stateful mode only)
160+ pathItem . AddOperation ( HttpMethod . Get , new OpenApiOperation
161+ {
162+ Summary = "Open SSE stream" ,
163+ Description = "Open a Server-Sent Events stream to receive server-initiated JSON-RPC messages. Only available in stateful mode." ,
164+ OperationId = "OpenMCPStream" ,
165+ Responses = new OpenApiResponses
166+ {
167+ [ "200" ] = new OpenApiResponse
168+ {
169+ Description = "SSE stream opened successfully" ,
170+ Content = new Dictionary < string , OpenApiMediaType >
171+ {
172+ [ MediaTypeNames . Text . EventStream ] = new ( )
173+ {
174+ Schema = new OpenApiSchema
175+ {
176+ Type = JsonSchemaType . String ,
177+ Description = "Server-Sent Events stream containing JSON-RPC messages" ,
178+ } ,
179+ } ,
180+ } ,
181+ } ,
182+ [ "400" ] = new OpenApiResponse
183+ {
184+ Description = "Bad Request - missing session ID, unsupported protocol version, or invalid Last-Event-ID" ,
185+ Content = new Dictionary < string , OpenApiMediaType >
186+ {
187+ [ MediaTypeNames . Application . Json ] = new ( )
188+ {
189+ Schema = new OpenApiSchemaReference ( nameof ( JsonRpcError ) , document ) ,
190+ } ,
191+ } ,
192+ } ,
193+ [ "404" ] = new OpenApiResponse
194+ {
195+ Description = "Not Found - the specified session ID was not found" ,
196+ Content = new Dictionary < string , OpenApiMediaType >
197+ {
198+ [ MediaTypeNames . Application . Json ] = new ( )
199+ {
200+ Schema = new OpenApiSchemaReference ( nameof ( JsonRpcError ) , document ) ,
201+ } ,
202+ } ,
203+ } ,
204+ [ "405" ] = new OpenApiResponse
205+ {
206+ Description = "Method Not Allowed - server does not offer an SSE stream at this endpoint" ,
207+ } ,
208+ [ "406" ] = new OpenApiResponse
209+ {
210+ Description = "Not Acceptable - client must accept text/event-stream" ,
211+ Content = new Dictionary < string , OpenApiMediaType >
212+ {
213+ [ MediaTypeNames . Application . Json ] = new ( )
214+ {
215+ Schema = new OpenApiSchemaReference ( nameof ( JsonRpcError ) , document ) ,
216+ } ,
217+ } ,
218+ } ,
219+ } ,
220+ } ) ;
221+
222+ // DELETE /mcp - Terminate a session (stateful mode only)
223+ pathItem . AddOperation ( HttpMethod . Delete , new OpenApiOperation
224+ {
225+ Summary = "Terminate session" ,
226+ Description = "Terminate an active MCP session and clean up server-side resources. Only available in stateful mode." ,
227+ OperationId = "TerminateMCPSession" ,
228+ Responses = new OpenApiResponses
229+ {
230+ [ "200" ] = new OpenApiResponse
231+ {
232+ Description = "Session terminated successfully" ,
233+ } ,
234+ [ "400" ] = new OpenApiResponse
235+ {
236+ Description = "Bad Request - missing session ID or unsupported protocol version" ,
237+ Content = new Dictionary < string , OpenApiMediaType >
238+ {
239+ [ MediaTypeNames . Application . Json ] = new ( )
240+ {
241+ Schema = new OpenApiSchemaReference ( nameof ( JsonRpcError ) , document ) ,
242+ } ,
243+ } ,
244+ } ,
245+ [ "404" ] = new OpenApiResponse
246+ {
247+ Description = "Not Found - the specified session ID was not found" ,
248+ Content = new Dictionary < string , OpenApiMediaType >
249+ {
250+ [ MediaTypeNames . Application . Json ] = new ( )
251+ {
252+ Schema = new OpenApiSchemaReference ( nameof ( JsonRpcError ) , document ) ,
253+ } ,
79254 } ,
80255 } ,
256+ [ "405" ] = new OpenApiResponse
257+ {
258+ Description = "Method Not Allowed - server does not allow clients to terminate sessions" ,
259+ } ,
81260 } ,
82261 } ) ;
83262
84263 document . Paths ??= [ ] ;
85264 document . Paths . Add ( "/mcp" , pathItem ) ;
86265 }
87- }
266+ }
0 commit comments