Refine 2.0.0 concept documentation (#1742)

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 782b5bad-4046-49ec-93a6-72768b5b7521
Copilot-Session: 4b938335-418c-4ee7-8287-6c70d9148b89
This commit is contained in:
Jeff Handley 2026-07-27 20:15:16 -07:00 • committed by GitHub
commit 314bdb2f49
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -247,20 +247,130 @@ public static string WizardTool(
}
```
### Providing custom error messages
### Supporting down-level clients
When MRTR is not supported, you can provide domain-specific guidance:
When <xref:ModelContextProtocol.Server.McpServer.IsMrtrSupported> is `false`, the tool
can't obtain client input through an input request — but that doesn't have to be a dead
end. Handle down-level and stateless callers along a spectrum, from a helpful message to a
fully functional non-interactive path.
#### Return actionable guidance
At minimum, tell the caller how to proceed instead of failing opaquely. Explain what kind
of session would enable the interactive path, and — when the tool exposes one — how to
invoke the non-interactive path described below:
```csharp
if (!server.IsMrtrSupported)
{
return "This tool requires interactive input. To use it:\n"
+ "1. Connect with a client that negotiates MCP protocol revision 2026-07-28, or\n"
+ "2. Use a stateful session using protocol revision 2025-11-25 so the server can resolve the input requests for you.\n"
+ "\nStateless sessions using protocol revision 2025-11-25 and earlier cannot resolve MRTR input requests.";
return "This tool needs interactive input. Connect with a client that negotiates MCP "
+ "protocol revision 2026-07-28, or use a stateful session using revision 2025-11-25 "
+ "so the server can resolve the input requests for you.";
}
```
#### Offer a non-interactive fallback
When a tool *can* complete without a prompt, expose an explicit argument that lets a
down-level or stateless caller opt in directly. The tool stays usable everywhere: it
elicits input when MRTR is available, and otherwise returns guidance the caller
(or its model) can act on by resending with the argument set.
The tool below is just an example that has a sensible non-interactive fallback. It also
shows how to branch on the elicitation **action** rather than assuming acceptance: the
deserialized <xref:ModelContextProtocol.Protocol.ElicitResult> carries an
<xref:ModelContextProtocol.Protocol.ElicitResult.Action> of `"accept"`, `"decline"`, or
`"cancel"`, surfaced through the
<xref:ModelContextProtocol.Protocol.ElicitResult.IsAccepted> shorthand.
```csharp
[McpServerTool, Description("Closes a support ticket, recording why it was closed.")]
public static string CloseSupportTicket(
McpServer server,
RequestContext<CallToolRequestParams> context,
[Description("The ID of the ticket to close")] long ticketId,
[Description("Why the ticket is being closed")] string? closeReason = null)
{
// Handles four client scenarios:
// 1. Provided up-front: client sends `closeReason` in the initial call
// 2. MRTR round-trip request: client confirms via `InputResponses["closeReason"]`
// 3. MRTR initial request: server proposes a default reason and asks for confirmation
// (with automatic SDK down-level bridge)
// 4. Session-less down-level: server returns a guidance message requesting the reason up-front
// The default reason proposed to the caller and used if none is provided.
string defaultCloseReason = "completed";
// (1) Provided up-front. Works on any client, including down-level session-less.
// These requests are typically sent after (4) returns a guidance message.
var confirmedReason = closeReason;
// (2) MRTR round-trip request. Works with native MRTR support or the automatic down-level
// SDK bridge after (3) throws an `InputRequiredException` to request a `closeReason`.
if (string.IsNullOrWhiteSpace(confirmedReason) &&
context.Params?.InputResponses?.TryGetValue("closeReason", out var reasonResponse) is true)
{
var reasonResult = reasonResponse.Deserialize(InputResponse.ElicitResultJsonTypeInfo);
// Branch on the elicitation action: `decline` or `cancel` leaves the ticket open.
if (reasonResult?.IsAccepted is not true) return "Ticket close cancelled";
// Accepted: use the reason the caller confirmed, falling back to the proposed default.
confirmedReason = reasonResult.Content?.TryGetValue("closeReason", out var reasonValue) is true
? reasonValue.GetString()
: null;
confirmedReason = string.IsNullOrWhiteSpace(confirmedReason) ? defaultCloseReason : confirmedReason;
}
// (1) or (2) A reason is in hand; proceed with closing the ticket.
if (!string.IsNullOrWhiteSpace(confirmedReason))
return $"Closed ticket {ticketId}: {confirmedReason}";
// (3) MRTR initial request: propose "completed" as the default reason and ask the caller
// to confirm (or adjust) it. This uses the 2026-07-28 MRTR input request, but the SDK
// provides an automatic bridge to a legacy elicitation on a down-level, stateful
// session. When the bridge can be provided, `server.IsMrtrSupported` is `true` and the
// exception leads to a legacy elicitation response automatically.
if (server.IsMrtrSupported)
{
throw new InputRequiredException(
inputRequests: new Dictionary<string, InputRequest>
{
["closeReason"] = InputRequest.ForElicitation(new ElicitRequestParams
{
Message = $"Close ticket '{ticketId}'? Accept the default reason or provide your own.",
RequestedSchema = new()
{
Properties =
{
["closeReason"] = new ElicitRequestParams.StringSchema
{
Title = "Close reason",
Description = "The reason for closing the ticket",
Default = defaultCloseReason,
},
},
},
})
},
requestState: ticketId.ToString()); // opaque; echoed back to us on the retry
}
// (4) Down-level and stateless: we can't prompt an elicitation through an MRTR
// round-trip request or an elicitation. Return a natural language response
// with guidance for providing the reason up-front.
return "Closing a ticket requires a reason. Resend with `closeReason`.";
}
```
> [!IMPORTANT]
> Form-mode elicitations must set `RequestedSchema`. This tool proposes a default, so its
> schema declares an optional `closeReason` string with a `Default` the caller can accept or
> override. A confirmation prompt that collects no fields still needs a schema — set
> `RequestedSchema = new()` (an empty object schema): it's accepted by native `2026-07-28` MRTR
> and **required** by the down-level stateful elicitation bridge under `2025-11-25`, which
> otherwise throws `ArgumentException: "Form mode elicitation requests require a requested schema."`.
## Compatibility
The SDK supports `InputRequiredException` across two protocol revisions and two session modes: