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:
parent
6e2f285827
commit
314bdb2f49
1 changed files with 178 additions and 63 deletions
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Reference in a new issue