Skip to content

Commit 989f36e

Browse files
committed
Fix MCP Apps elicitation helpers
Require hierarchical ui:// resource URIs and merge elicitation into existing typed and JSON MCP Apps server capabilities without losing existing settings. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 19b7c141-8708-451b-b194-0c029b8e79a1
1 parent ea6d1ee commit 989f36e

4 files changed

Lines changed: 319 additions & 159 deletions

File tree

src/ModelContextProtocol.Extensions.Apps/Server/McpAppElicitation.cs

Lines changed: 51 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
using ModelContextProtocol.Protocol;
2-
using ModelContextProtocol.Server;
31
using System.Diagnostics.CodeAnalysis;
42
using System.Text.Json;
53
using System.Text.Json.Nodes;
4+
using ModelContextProtocol.Protocol;
5+
using ModelContextProtocol.Server;
66

77
namespace ModelContextProtocol.Extensions.Apps;
88

@@ -16,7 +16,8 @@ public static ClientCapabilities AddClientCapabilities(ClientCapabilities capabi
1616
#if NET
1717
ArgumentNullException.ThrowIfNull(capabilities);
1818
#else
19-
if (capabilities is null) throw new ArgumentNullException(nameof(capabilities));
19+
if (capabilities is null)
20+
throw new ArgumentNullException(nameof(capabilities));
2021
#endif
2122

2223
capabilities.Elicitation ??= new ElicitationCapability();
@@ -28,12 +29,13 @@ public static ClientCapabilities AddClientCapabilities(ClientCapabilities capabi
2829
{
2930
uiCapabilities = existing switch
3031
{
31-
McpUiClientCapabilities typed => JsonSerializer.SerializeToNode(
32-
typed,
33-
McpAppsJsonContext.Default.McpUiClientCapabilities)!.AsObject(),
32+
McpUiClientCapabilities typed => JsonSerializer
33+
.SerializeToNode(typed, McpAppsJsonContext.Default.McpUiClientCapabilities)!
34+
.AsObject(),
3435
JsonObject jsonObject => jsonObject,
35-
JsonElement { ValueKind: JsonValueKind.Object } element =>
36-
JsonNode.Parse(element.GetRawText())!.AsObject(),
36+
JsonElement { ValueKind: JsonValueKind.Object } element => JsonNode
37+
.Parse(element.GetRawText())!
38+
.AsObject(),
3739
_ => [],
3840
};
3941
}
@@ -48,10 +50,13 @@ public static ClientCapabilities AddClientCapabilities(ClientCapabilities capabi
4850
uiCapabilities["mimeTypes"] = mimeTypes;
4951
}
5052

51-
if (!mimeTypes.Any(node =>
52-
node is JsonValue value &&
53-
value.TryGetValue<string>(out var mimeType) &&
54-
string.Equals(mimeType, McpApps.HtmlMimeType, StringComparison.OrdinalIgnoreCase)))
53+
if (
54+
!mimeTypes.Any(node =>
55+
node is JsonValue value
56+
&& value.TryGetValue<string>(out var mimeType)
57+
&& string.Equals(mimeType, McpApps.HtmlMimeType, StringComparison.OrdinalIgnoreCase)
58+
)
59+
)
5560
{
5661
mimeTypes.Add((JsonNode?)JsonValue.Create(McpApps.HtmlMimeType));
5762
}
@@ -67,12 +72,12 @@ node is JsonValue value &&
6772
/// </summary>
6873
public static bool IsSupported(
6974
ClientCapabilities? clientCapabilities,
70-
ServerCapabilities? serverCapabilities)
75+
ServerCapabilities? serverCapabilities
76+
)
7177
{
7278
var serverUi = McpApps.GetUiServerCapability(serverCapabilities);
7379

74-
return IsClientSupported(clientCapabilities) &&
75-
serverUi?.Elicitation is not null;
80+
return IsClientSupported(clientCapabilities) && serverUi?.Elicitation is not null;
7681
}
7782

7883
/// <summary>
@@ -87,9 +92,10 @@ public static bool IsSupported(
8792
public static bool IsClientSupported(ClientCapabilities? clientCapabilities)
8893
{
8994
var clientUi = McpApps.GetUiCapability(clientCapabilities);
90-
return clientCapabilities?.Elicitation?.Form is not null &&
91-
clientUi?.Elicitation is not null &&
92-
clientUi.MimeTypes?.Contains(McpApps.HtmlMimeType, StringComparer.OrdinalIgnoreCase) == true;
95+
return clientCapabilities?.Elicitation?.Form is not null
96+
&& clientUi?.Elicitation is not null
97+
&& clientUi.MimeTypes?.Contains(McpApps.HtmlMimeType, StringComparer.OrdinalIgnoreCase)
98+
== true;
9399
}
94100

95101
/// <summary>Associates a form elicitation request with an MCP App UI resource.</summary>
@@ -100,7 +106,8 @@ public static ElicitRequestParams SetAppUi(ElicitRequestParams request, string r
100106
request.Meta ??= [];
101107
request.Meta["ui"] = JsonSerializer.SerializeToNode(
102108
new McpAppElicitationMeta { ResourceUri = resourceUri },
103-
McpAppsJsonContext.Default.McpAppElicitationMeta);
109+
McpAppsJsonContext.Default.McpAppElicitationMeta
110+
);
104111
return request;
105112
}
106113

@@ -112,7 +119,8 @@ public static ElicitRequestParams SetAppUiIfSupported(
112119
ElicitRequestParams request,
113120
ClientCapabilities? clientCapabilities,
114121
ServerCapabilities? serverCapabilities,
115-
string resourceUri)
122+
string resourceUri
123+
)
116124
{
117125
ValidateArguments(request, resourceUri);
118126
return IsSupported(clientCapabilities, serverCapabilities)
@@ -128,12 +136,11 @@ public static ElicitRequestParams SetAppUiIfSupported(
128136
public static ElicitRequestParams SetAppUiIfSupported(
129137
ElicitRequestParams request,
130138
ClientCapabilities? clientCapabilities,
131-
string resourceUri)
139+
string resourceUri
140+
)
132141
{
133142
ValidateArguments(request, resourceUri);
134-
return IsClientSupported(clientCapabilities)
135-
? SetAppUi(request, resourceUri)
136-
: request;
143+
return IsClientSupported(clientCapabilities) ? SetAppUi(request, resourceUri) : request;
137144
}
138145

139146
/// <summary>
@@ -145,17 +152,18 @@ public static ElicitRequestParams SetAppUiIfSupported(
145152
public static ElicitRequestParams SetAppUiIfSupported<TParams>(
146153
ElicitRequestParams request,
147154
RequestContext<TParams> context,
148-
string resourceUri)
155+
string resourceUri
156+
)
149157
{
150158
#if NET
151159
ArgumentNullException.ThrowIfNull(context);
152160
#else
153-
if (context is null) throw new ArgumentNullException(nameof(context));
161+
if (context is null)
162+
throw new ArgumentNullException(nameof(context));
154163
#endif
155164

156165
var clientCapabilities =
157-
context.JsonRpcRequest.Context?.ClientCapabilities ??
158-
context.Server.ClientCapabilities;
166+
context.JsonRpcRequest.Context?.ClientCapabilities ?? context.Server.ClientCapabilities;
159167
return SetAppUiIfSupported(request, clientCapabilities, resourceUri);
160168
}
161169

@@ -165,7 +173,8 @@ public static ElicitRequestParams SetAppUiIfSupported<TParams>(
165173
#if NET
166174
ArgumentNullException.ThrowIfNull(request);
167175
#else
168-
if (request is null) throw new ArgumentNullException(nameof(request));
176+
if (request is null)
177+
throw new ArgumentNullException(nameof(request));
169178
#endif
170179

171180
if (request.Meta?["ui"] is not JsonNode node)
@@ -190,24 +199,31 @@ private static void ValidateArguments(ElicitRequestParams request, string resour
190199
ArgumentNullException.ThrowIfNull(request);
191200
ArgumentException.ThrowIfNullOrWhiteSpace(resourceUri);
192201
#else
193-
if (request is null) throw new ArgumentNullException(nameof(request));
194-
if (string.IsNullOrWhiteSpace(resourceUri)) throw new ArgumentException("The resource URI is required.", nameof(resourceUri));
202+
if (request is null)
203+
throw new ArgumentNullException(nameof(request));
204+
if (string.IsNullOrWhiteSpace(resourceUri))
205+
throw new ArgumentException("The resource URI is required.", nameof(resourceUri));
195206
#endif
196207

197208
if (!string.Equals(request.Mode, "form", StringComparison.Ordinal))
198209
{
199-
throw new ArgumentException("MCP Apps only support form-mode elicitations.", nameof(request));
210+
throw new ArgumentException(
211+
"MCP Apps only support form-mode elicitations.",
212+
nameof(request)
213+
);
200214
}
201215

202216
if (!IsAbsoluteUiUri(resourceUri))
203217
{
204218
throw new ArgumentException(
205219
"MCP App elicitation resources must be absolute ui:// URIs.",
206-
nameof(resourceUri));
220+
nameof(resourceUri)
221+
);
207222
}
208223
}
209224

210225
private static bool IsAbsoluteUiUri(string resourceUri) =>
211-
Uri.TryCreate(resourceUri, UriKind.Absolute, out var uri) &&
212-
string.Equals(uri.Scheme, "ui", StringComparison.OrdinalIgnoreCase);
226+
Uri.TryCreate(resourceUri, UriKind.Absolute, out var uri)
227+
&& string.Equals(uri.Scheme, "ui", StringComparison.OrdinalIgnoreCase)
228+
&& !string.IsNullOrEmpty(uri.Host);
213229
}

src/ModelContextProtocol.Extensions.Apps/Server/McpAppsBuilderExtensions.cs

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
using System.Diagnostics.CodeAnalysis;
2+
using System.Text.Json;
3+
using System.Text.Json.Nodes;
14
using Microsoft.Extensions.DependencyInjection;
25
using Microsoft.Extensions.Options;
36
using ModelContextProtocol.Protocol;
47
using ModelContextProtocol.Server;
5-
using System.Diagnostics.CodeAnalysis;
68

79
namespace ModelContextProtocol.Extensions.Apps;
810

@@ -40,10 +42,14 @@ public static IMcpServerBuilder WithMcpApps(this IMcpServerBuilder builder)
4042
#if NET
4143
ArgumentNullException.ThrowIfNull(builder);
4244
#else
43-
if (builder is null) throw new ArgumentNullException(nameof(builder));
45+
if (builder is null)
46+
throw new ArgumentNullException(nameof(builder));
4447
#endif
4548

46-
builder.Services.AddSingleton<IPostConfigureOptions<McpServerOptions>, McpAppsPostConfigureOptions>();
49+
builder.Services.AddSingleton<
50+
IPostConfigureOptions<McpServerOptions>,
51+
McpAppsPostConfigureOptions
52+
>();
4753
return builder;
4854
}
4955

@@ -54,11 +60,28 @@ public void PostConfigure(string? name, McpServerOptions options)
5460
// Advertise server-side MCP Apps support in capabilities.
5561
options.Capabilities ??= new ServerCapabilities();
5662
options.Capabilities.Extensions ??= new Dictionary<string, object>();
57-
if (!options.Capabilities.Extensions.ContainsKey(McpApps.ExtensionId))
63+
if (options.Capabilities.Extensions.TryGetValue(McpApps.ExtensionId, out var existing))
64+
{
65+
switch (existing)
66+
{
67+
case McpUiServerCapabilities typed:
68+
typed.Elicitation ??= new McpUiElicitationCapability();
69+
break;
70+
case JsonObject jsonObject:
71+
jsonObject["elicitation"] ??= new JsonObject();
72+
break;
73+
case JsonElement { ValueKind: JsonValueKind.Object } element:
74+
var merged = JsonNode.Parse(element.GetRawText())!.AsObject();
75+
merged["elicitation"] ??= new JsonObject();
76+
options.Capabilities.Extensions[McpApps.ExtensionId] = merged;
77+
break;
78+
}
79+
}
80+
else
5881
{
59-
options.Capabilities.Extensions[McpApps.ExtensionId] = new System.Text.Json.Nodes.JsonObject
82+
options.Capabilities.Extensions[McpApps.ExtensionId] = new JsonObject
6083
{
61-
["elicitation"] = new System.Text.Json.Nodes.JsonObject(),
84+
["elicitation"] = new JsonObject(),
6285
};
6386
}
6487

0 commit comments

Comments
 (0)