Add framework for conceptual docs (#708)
Add framework for conceptual docs plus article on elicitation Co-authored-by: Stephen Halter <halter73@gmail.com>
This commit is contained in:
parent
b067261ca7
commit
15f8e89ac3
12 changed files with 441 additions and 3 deletions
51
docs/concepts/elicitation/elicitation.md
Normal file
51
docs/concepts/elicitation/elicitation.md
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
---
|
||||
title: Elicitation
|
||||
author: mikekistler
|
||||
description: Learn about the telemetry collected by the HttpRepl.
|
||||
uid: elicitation
|
||||
---
|
||||
|
||||
The **elicitation** feature allows servers to request additional information from users during interactions. This enables more dynamic and interactive AI experiences, making it easier to gather necessary context before executing tasks.
|
||||
|
||||
## Server Support for Elicitation
|
||||
|
||||
Servers request structured data from users with the [ElicitAsync] extension method on [IMcpServer].
|
||||
The C# SDK registers an instance of [IMcpServer] with the dependency injection container,
|
||||
so tools can simply add a parameter of type [IMcpServer] to their method signature to access it.
|
||||
|
||||
[ElicitAsync]: https://modelcontextprotocol.github.io/csharp-sdk/api/ModelContextProtocol.Server.McpServerExtensions.html#ModelContextProtocol_Server_McpServerExtensions_ElicitAsync_ModelContextProtocol_Server_IMcpServer_ModelContextProtocol_Protocol_ElicitRequestParams_System_Threading_CancellationToken_
|
||||
[IMcpServer]: https://modelcontextprotocol.github.io/csharp-sdk/api/ModelContextProtocol.Server.IMcpServer.html
|
||||
|
||||
The MCP Server must specify the schema of each input value it is requesting from the user.
|
||||
Only primitive types (string, number, boolean) are supported for elicitation requests.
|
||||
The schema may include a description to help the user understand what is being requested.
|
||||
|
||||
The server can request a single input or multiple inputs at once.
|
||||
To help distinguish multiple inputs, each input has a unique name.
|
||||
|
||||
The following example demonstrates how a server could request a boolean response from the user.
|
||||
|
||||
[!code-csharp[](samples/server/Tools/InteractiveTools.cs?name=snippet_GuessTheNumber)]
|
||||
|
||||
## Client Support for Elicitation
|
||||
|
||||
Elicitation is an optional feature so clients declare their support for it in their capabilities as part of the `initialize` request. In the MCP C# SDK, this is done by configuring an [ElicitationHandler] in the [McpClientOptions]:
|
||||
|
||||
[ElicitationHandler]: https://modelcontextprotocol.github.io/csharp-sdk/api/ModelContextProtocol.Protocol.ElicitationCapability.html#ModelContextProtocol_Protocol_ElicitationCapability_ElicitationHandler
|
||||
[McpClientOptions]: https://modelcontextprotocol.github.io/csharp-sdk/api/ModelContextProtocol.Client.McpClientOptions.html
|
||||
|
||||
[!code-csharp[](samples/client/Program.cs?name=snippet_McpInitialize)]
|
||||
|
||||
The ElicitationHandler is an asynchronous method that will be called when the server requests additional information.
|
||||
The ElicitationHandler must request input from the user and return the data in a format that matches the requested schema.
|
||||
This will be highly dependent on the client application and how it interacts with the user.
|
||||
|
||||
If the user provides the requested information, the ElicitationHandler should return an [ElicitResult] with the action set to "accept" and the content containing the user's input.
|
||||
If the user does not provide the requested information, the ElicitationHandler should return an [ElicitResult] with the action set to "reject" and no content.
|
||||
|
||||
[ElicitResult]: https://modelcontextprotocol.github.io/csharp-sdk/api/ModelContextProtocol.Protocol.ElicitResult.html
|
||||
|
||||
Below is an example of how a console application might handle elicitation requests.
|
||||
Here's an example implementation:
|
||||
|
||||
[!code-csharp[](samples/client/Program.cs?name=snippet_ElicitationHandler)]
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ModelContextProtocol.Core" Version="0.3.0-preview.3" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
121
docs/concepts/elicitation/samples/client/Program.cs
Normal file
121
docs/concepts/elicitation/samples/client/Program.cs
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
using System.Text.Json;
|
||||
using ModelContextProtocol.Client;
|
||||
using ModelContextProtocol.Protocol;
|
||||
|
||||
var endpoint = Environment.GetEnvironmentVariable("ENDPOINT") ?? "http://localhost:3001";
|
||||
|
||||
var clientTransport = new SseClientTransport(new()
|
||||
{
|
||||
Endpoint = new Uri(endpoint),
|
||||
TransportMode = HttpTransportMode.StreamableHttp,
|
||||
});
|
||||
|
||||
// <snippet_McpInitialize>
|
||||
McpClientOptions options = new()
|
||||
{
|
||||
ClientInfo = new()
|
||||
{
|
||||
Name = "ElicitationClient",
|
||||
Version = "1.0.0"
|
||||
},
|
||||
Capabilities = new()
|
||||
{
|
||||
Elicitation = new()
|
||||
{
|
||||
ElicitationHandler = HandleElicitationAsync
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
await using var mcpClient = await McpClientFactory.CreateAsync(clientTransport, options);
|
||||
// </snippet_McpInitialize>
|
||||
|
||||
var tools = await mcpClient.ListToolsAsync();
|
||||
foreach (var tool in tools)
|
||||
{
|
||||
Console.WriteLine($"Connected to server with tools: {tool.Name}");
|
||||
}
|
||||
|
||||
Console.WriteLine($"Calling tool: {tools.First().Name}");
|
||||
|
||||
var result = await mcpClient.CallToolAsync(toolName: tools.First().Name);
|
||||
|
||||
foreach (var block in result.Content)
|
||||
{
|
||||
if (block is TextContentBlock textBlock)
|
||||
{
|
||||
Console.WriteLine(textBlock.Text);
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($"Received unexpected result content of type {block.GetType()}");
|
||||
}
|
||||
}
|
||||
|
||||
// <snippet_ElicitationHandler>
|
||||
async ValueTask<ElicitResult> HandleElicitationAsync(ElicitRequestParams? requestParams, CancellationToken token)
|
||||
{
|
||||
// Bail out if the requestParams is null or if the requested schema has no properties
|
||||
if (requestParams?.RequestedSchema?.Properties == null)
|
||||
{
|
||||
return new ElicitResult();
|
||||
}
|
||||
|
||||
// Process the elicitation request
|
||||
if (requestParams?.Message is not null)
|
||||
{
|
||||
Console.WriteLine(requestParams.Message);
|
||||
}
|
||||
|
||||
var content = new Dictionary<string, JsonElement>();
|
||||
|
||||
// Loop through requestParams.requestSchema.Properties dictionary requesting values for each property
|
||||
foreach (var property in requestParams.RequestedSchema.Properties)
|
||||
{
|
||||
if (property.Value is ElicitRequestParams.BooleanSchema booleanSchema)
|
||||
{
|
||||
Console.Write($"{booleanSchema.Description}: ");
|
||||
var clientInput = Console.ReadLine();
|
||||
bool parsedBool;
|
||||
|
||||
// Try standard boolean parsing first
|
||||
if (bool.TryParse(clientInput, out parsedBool))
|
||||
{
|
||||
content[property.Key] = JsonSerializer.Deserialize<JsonElement>(JsonSerializer.Serialize(parsedBool));
|
||||
}
|
||||
// Also accept "yes"/"no" as valid boolean inputs
|
||||
else if (string.Equals(clientInput?.Trim(), "yes", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
content[property.Key] = JsonSerializer.Deserialize<JsonElement>(JsonSerializer.Serialize(true));
|
||||
}
|
||||
else if (string.Equals(clientInput?.Trim(), "no", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
content[property.Key] = JsonSerializer.Deserialize<JsonElement>(JsonSerializer.Serialize(false));
|
||||
}
|
||||
}
|
||||
else if (property.Value is ElicitRequestParams.NumberSchema numberSchema)
|
||||
{
|
||||
Console.Write($"{numberSchema.Description}: ");
|
||||
var clientInput = Console.ReadLine();
|
||||
double parsedNumber;
|
||||
if (double.TryParse(clientInput, out parsedNumber))
|
||||
{
|
||||
content[property.Key] = JsonSerializer.Deserialize<JsonElement>(JsonSerializer.Serialize(parsedNumber));
|
||||
}
|
||||
}
|
||||
else if (property.Value is ElicitRequestParams.StringSchema stringSchema)
|
||||
{
|
||||
Console.Write($"{stringSchema.Description}: ");
|
||||
var clientInput = Console.ReadLine();
|
||||
content[property.Key] = JsonSerializer.Deserialize<JsonElement>(JsonSerializer.Serialize(clientInput));
|
||||
}
|
||||
}
|
||||
|
||||
// Return the user's input
|
||||
return new ElicitResult
|
||||
{
|
||||
Action = "accept",
|
||||
Content = content
|
||||
};
|
||||
}
|
||||
// </snippet_ElicitationHandler>
|
||||
13
docs/concepts/elicitation/samples/server/Elicitation.csproj
Normal file
13
docs/concepts/elicitation/samples/server/Elicitation.csproj
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ModelContextProtocol.AspNetCore" Version="0.3.0-preview.3" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
58
docs/concepts/elicitation/samples/server/Elicitation.http
Normal file
58
docs/concepts/elicitation/samples/server/Elicitation.http
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
@HostAddress = http://localhost:3001
|
||||
|
||||
# No session ID, so elicitation capabilities not declared.
|
||||
|
||||
POST {{HostAddress}}/
|
||||
Accept: application/json, text/event-stream
|
||||
Content-Type: application/json
|
||||
MCP-Protocol-Version: 2025-06-18
|
||||
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 2,
|
||||
"method": "tools/call",
|
||||
"params": {
|
||||
"name": "guess_the_number"
|
||||
}
|
||||
}
|
||||
|
||||
###
|
||||
|
||||
POST {{HostAddress}}/
|
||||
Accept: application/json, text/event-stream
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": "initialize",
|
||||
"params": {
|
||||
"clientInfo": {
|
||||
"name": "RestClient",
|
||||
"version": "0.1.0"
|
||||
},
|
||||
"capabilities": {
|
||||
"elicitation": {}
|
||||
},
|
||||
"protocolVersion": "2025-06-18"
|
||||
}
|
||||
}
|
||||
|
||||
###
|
||||
|
||||
@SessionId = lgEu87uKTy8kLffZayO5rQ
|
||||
|
||||
POST {{HostAddress}}/
|
||||
Accept: application/json, text/event-stream
|
||||
Content-Type: application/json
|
||||
Mcp-Session-Id: {{SessionId}}
|
||||
MCP-Protocol-Version: 2025-06-18
|
||||
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 2,
|
||||
"method": "tools/call",
|
||||
"params": {
|
||||
"name": "guess_the_number"
|
||||
}
|
||||
}
|
||||
24
docs/concepts/elicitation/samples/server/Program.cs
Normal file
24
docs/concepts/elicitation/samples/server/Program.cs
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
using Elicitation.Tools;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Add services to the container.
|
||||
|
||||
builder.Services.AddMcpServer()
|
||||
.WithHttpTransport(options =>
|
||||
options.IdleTimeout = Timeout.InfiniteTimeSpan // Never timeout
|
||||
)
|
||||
.WithTools<InteractiveTools>();
|
||||
|
||||
builder.Logging.AddConsole(options =>
|
||||
{
|
||||
options.LogToStandardErrorThreshold = LogLevel.Information;
|
||||
});
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
|
||||
app.MapMcp();
|
||||
|
||||
app.Run();
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"applicationUrl": "http://localhost:3001",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
}
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"applicationUrl": "https://localhost:7133;http://localhost:3001",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,126 @@
|
|||
using System.ComponentModel;
|
||||
using System.Text.Json;
|
||||
using ModelContextProtocol;
|
||||
using ModelContextProtocol.Protocol;
|
||||
using ModelContextProtocol.Server;
|
||||
using static ModelContextProtocol.Protocol.ElicitRequestParams;
|
||||
|
||||
namespace Elicitation.Tools;
|
||||
|
||||
[McpServerToolType]
|
||||
public sealed class InteractiveTools
|
||||
{
|
||||
// <snippet_GuessTheNumber>
|
||||
[McpServerTool, Description("A simple game where the user has to guess a number between 1 and 10.")]
|
||||
public async Task<string> GuessTheNumber(
|
||||
IMcpServer server, // Get the McpServer from DI container
|
||||
CancellationToken token
|
||||
)
|
||||
{
|
||||
// Check if the client supports elicitation
|
||||
if (server.ClientCapabilities?.Elicitation == null)
|
||||
{
|
||||
// fail the tool call
|
||||
throw new McpException("Client does not support elicitation");
|
||||
}
|
||||
|
||||
// First ask the user if they want to play
|
||||
var playSchema = new RequestSchema
|
||||
{
|
||||
Properties =
|
||||
{
|
||||
["Answer"] = new BooleanSchema()
|
||||
}
|
||||
};
|
||||
|
||||
var playResponse = await server.ElicitAsync(new ElicitRequestParams
|
||||
{
|
||||
Message = "Do you want to play a game?",
|
||||
RequestedSchema = playSchema
|
||||
}, token);
|
||||
|
||||
// Check if user wants to play
|
||||
if (playResponse.Action != "accept" || playResponse.Content?["Answer"].ValueKind != JsonValueKind.True)
|
||||
{
|
||||
return "Maybe next time!";
|
||||
}
|
||||
// </snippet_GuessTheNumber>
|
||||
|
||||
// Now ask the user to enter their name
|
||||
var nameSchema = new RequestSchema
|
||||
{
|
||||
Properties =
|
||||
{
|
||||
["Name"] = new StringSchema()
|
||||
{
|
||||
Description = "Name of the player",
|
||||
MinLength = 2,
|
||||
MaxLength = 50,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var nameResponse = await server.ElicitAsync(new ElicitRequestParams
|
||||
{
|
||||
Message = "What is your name?",
|
||||
RequestedSchema = nameSchema
|
||||
}, token);
|
||||
|
||||
if (nameResponse.Action != "accept")
|
||||
{
|
||||
return "Maybe next time!";
|
||||
}
|
||||
string? playerName = nameResponse.Content?["Name"].GetString();
|
||||
|
||||
// Generate a random number between 1 and 10
|
||||
Random random = new Random();
|
||||
int targetNumber = random.Next(1, 11); // 1 to 10 inclusive
|
||||
int attempts = 0;
|
||||
|
||||
var message = "Guess a number between 1 and 10";
|
||||
|
||||
while (true)
|
||||
{
|
||||
attempts++;
|
||||
|
||||
var guessSchema = new RequestSchema
|
||||
{
|
||||
Properties =
|
||||
{
|
||||
["Guess"] = new NumberSchema()
|
||||
{
|
||||
Type = "integer",
|
||||
Minimum = 1,
|
||||
Maximum = 10,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var guessResponse = await server.ElicitAsync(new ElicitRequestParams
|
||||
{
|
||||
Message = message,
|
||||
RequestedSchema = guessSchema
|
||||
}, token);
|
||||
|
||||
if (guessResponse.Action != "accept")
|
||||
{
|
||||
return "Maybe next time!";
|
||||
}
|
||||
int guess = (int)(guessResponse.Content?["Guess"].GetInt32())!;
|
||||
|
||||
// Check if the guess is correct
|
||||
if (guess == targetNumber)
|
||||
{
|
||||
return $"Congratulations {playerName}! You guessed the number {targetNumber} in {attempts} attempts!";
|
||||
}
|
||||
else if (guess < targetNumber)
|
||||
{
|
||||
message = $"Your guess is too low! Try again (Attempt #{attempts}):";
|
||||
}
|
||||
else
|
||||
{
|
||||
message = $"Your guess is too high! Try again (Attempt #{attempts}):";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
docs/concepts/index.md
Normal file
2
docs/concepts/index.md
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
|
||||
Welcome to the conceptual documentation for the Model Context Protocol SDK. Here you'll find high-level overviews, explanations, and guides to help you understand how the SDK implements the Model Context Protocol.
|
||||
5
docs/concepts/toc.yml
Normal file
5
docs/concepts/toc.yml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
items:
|
||||
- name: Overview
|
||||
href: index.md
|
||||
- name: Elicitation
|
||||
uid: elicitation
|
||||
|
|
@ -42,6 +42,7 @@
|
|||
"_appLogoPath": "images/mcp.svg",
|
||||
"_appFaviconPath": "images/favicon.ico",
|
||||
"_enableSearch": true,
|
||||
"_disableNextArticle": true,
|
||||
"pdf": false
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
items:
|
||||
- name: API Docs
|
||||
- name: Documentation
|
||||
href: concepts/index.md
|
||||
- name: API Reference
|
||||
href: api/ModelContextProtocol.yml
|
||||
- name: Github
|
||||
href: https://github.com/ModelContextProtocol/csharp-sdk
|
||||
Loading…
Reference in a new issue