Skip to content

Commit c7381fc

Browse files
committed
Enhance McpDocumentTransformer to support JSON-RPC error handling and additional operations
1 parent e5ca78e commit c7381fc

1 file changed

Lines changed: 187 additions & 8 deletions

File tree

shared/McpSamples.Shared/OpenApi/McpDocumentTransformer.cs

Lines changed: 187 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)