Use MCP Resources and Dynamic Prompts in Agent Workflows
The Model Context Protocol offers more than tool calling. MCP servers can expose resources (structured data your application can read), prompts (reusable message templates with arguments), sampling (server-initiated LLM requests), elicitation (server-initiated user input requests), and diagnostic features such as progress tracking and logging. This guide walks through every MCP capability beyond tools, showing how to integrate each one into your LM-Kit.NET application.
Why This Matters
Two enterprise problems that advanced MCP capabilities solve:
- Centralize configuration and data access. Resources let MCP servers publish files, database records, and live configuration as first-class data that agents can read on demand. Instead of hard-coding data-fetching logic in every tool, agents discover and consume resources through a standard protocol.
- Standardize prompt engineering across teams. Prompt templates stored on an MCP server can be versioned, shared, and rendered with arguments at runtime. Teams reuse the same prompts without copy-pasting system instructions into every agent, reducing drift and making updates instant.
Prerequisites
| Requirement | Minimum |
|---|---|
| .NET SDK | 8.0+ |
| VRAM | 6+ GB (for sampling step; not needed for resource/prompt only workflows) |
| Disk | ~5 GB free for model download (sampling step only) |
| MCP server | A running MCP server that exposes resources, prompts, or both |
Step 1: Create the Project
dotnet new console -n McpResourcesDemo
cd McpResourcesDemo
dotnet add package LM-Kit.NET
Step 2: Understand MCP Capabilities Beyond Tools
The first MCP guide covered Tools: functions the agent can call. MCP servers advertise additional capabilities that unlock richer integrations.
┌─────────────────────────────────────────────────────────────────┐
│ MCP Server Capabilities │
├───────────────┬─────────────────────────────────────────────────┤
│ │ │
│ Tools │ Resources Prompts Logging Completions │
│ (covered in │ (this guide) │
│ prior │ │
│ guide) │ Sampling Elicitation Progress │
│ │ (server requests handled by client) │
└───────────────┴─────────────────────────────────────────────────┘
Each capability is advertised by the server during initialization and represented as a flag in the McpServerCapabilities enum:
| Flag | Description |
|---|---|
Tools |
Server exposes callable functions |
Resources |
Server publishes readable data (files, records, live config) |
Prompts |
Server offers reusable prompt templates with arguments |
Logging |
Server can stream log messages to the client |
Completions |
Server supports argument autocompletion suggestions |
Sampling and elicitation are client capabilities: the server requests LLM completions or user input, and your client responds.
Step 3: Connect to an MCP Server and Check Capabilities
using System.Text;
using LMKit.Mcp.Client;
using LMKit.Mcp.Abstractions;
// Set your license key (leave empty for trial)
LMKit.Licensing.LicenseManager.SetLicenseKey("");
Console.InputEncoding = Encoding.UTF8;
Console.OutputEncoding = Encoding.UTF8;
// ──────────────────────────────────────
// 1. Build the client
// ──────────────────────────────────────
using var client = McpClientBuilder
.ForHttp("https://mcp.example.com/api")
.WithBearerToken(Environment.GetEnvironmentVariable("MCP_API_KEY"))
.WithClientInfo("MyApp", "My Application", "1.0.0")
.Build();
await client.InitializeAsync();
Console.WriteLine($"Protocol: {client.McpProtocolVersion}");
Console.WriteLine($"Session: {client.SessionId}");
Console.WriteLine();
// ──────────────────────────────────────
// 2. Check which capabilities are available
// ──────────────────────────────────────
Console.WriteLine("Server capabilities:");
if (client.HasCapability(McpServerCapabilities.Resources))
Console.WriteLine(" Resources: supported");
if (client.HasCapability(McpServerCapabilities.Prompts))
Console.WriteLine(" Prompts: supported");
if (client.HasCapability(McpServerCapabilities.Logging))
Console.WriteLine(" Logging: supported");
if (client.HasCapability(McpServerCapabilities.Completions))
Console.WriteLine(" Completions: supported");
Console.WriteLine();
HasCapability performs a bitwise check against the flags the server returned during initialization. Always verify a capability before calling methods that depend on it.
Step 4: List and Read Resources
Resources are server-managed data items identified by URIs. Each resource has a name, MIME type, and optional description.
// ──────────────────────────────────────
// 1. List all resources
// ──────────────────────────────────────
var resources = await client.GetResourcesAsync();
foreach (var resource in resources)
{
Console.WriteLine($"Resource: {resource.Name}");
Console.WriteLine($" URI: {resource.Uri}");
Console.WriteLine($" MIME type: {resource.MimeType}");
Console.WriteLine($" Description: {resource.Description}");
Console.WriteLine();
}
// ──────────────────────────────────────
// 2. Read a specific resource by URI
// ──────────────────────────────────────
var contents = await client.ReadResourceAsync("file:///project/config.json");
foreach (var item in contents)
{
Console.WriteLine($" URI: {item.Uri}");
Console.WriteLine($" MIME: {item.MimeType}");
if (item.Text != null)
Console.WriteLine($" Text content ({item.Text.Length} chars)");
else if (item.Blob != null)
Console.WriteLine($" Binary content ({item.Blob.Length} bytes, base64-encoded)");
}
GetResourcesAsync returns IReadOnlyCollection<McpResource> and caches the result after the first call. ReadResourceAsync returns IReadOnlyList<McpResourceContent>, where each entry contains either Text (for textual data) or Blob (for base64-encoded binary data).
Step 5: Use Resource Templates for Dynamic Content
Resource templates define parameterized URI patterns (RFC 6570). The client fills in the template variables to request dynamic data.
// ──────────────────────────────────────
// 1. Discover available templates
// ──────────────────────────────────────
var templates = await client.GetResourceTemplatesAsync();
foreach (var template in templates)
{
Console.WriteLine($"Template: {template.Name}");
Console.WriteLine($" URI pattern: {template.UriTemplate}"); // e.g. "db://users/{userId}"
Console.WriteLine($" Description: {template.Description}");
Console.WriteLine($" MIME type: {template.MimeType}");
Console.WriteLine();
}
// ──────────────────────────────────────
// 2. Read a dynamic resource by filling in the template
// ──────────────────────────────────────
var userData = await client.ReadResourceAsync("db://users/12345");
foreach (var item in userData)
{
if (item.Text != null)
Console.WriteLine($"User data: {item.Text}");
}
The client does not expand templates automatically. You construct the final URI yourself by replacing the {placeholder} segments with actual values, then pass it to ReadResourceAsync.
Step 6: Subscribe to Resource Updates
When the server supports resource subscriptions, your application can receive push notifications when a resource changes.
// ──────────────────────────────────────
// 1. Attach a handler for updates
// ──────────────────────────────────────
client.ResourceUpdated += (sender, args) =>
{
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine($"[{args.Timestamp:HH:mm:ss}] Resource updated: {args.Uri}");
Console.ResetColor();
// Re-read the resource to get fresh content
var fresh = client.ReadResource(args.Uri);
foreach (var item in fresh)
{
if (item.Text != null)
Console.WriteLine($" New content: {item.Text.Substring(0, Math.Min(200, item.Text.Length))}...");
}
};
// ──────────────────────────────────────
// 2. Subscribe to a resource
// ──────────────────────────────────────
await client.SubscribeToResourceAsync("file:///project/config.json");
Console.WriteLine("Subscribed. Waiting for updates...");
// Check active subscriptions
var subscribed = client.SubscribedResources;
Console.WriteLine($"Active subscriptions: {subscribed.Count}");
// ──────────────────────────────────────
// 3. Unsubscribe when done
// ──────────────────────────────────────
await client.UnsubscribeFromResourceAsync("file:///project/config.json");
Console.WriteLine("Unsubscribed.");
The ResourceUpdated event fires when the server pushes a notifications/resources/updated message. Subscription support depends on the server; SubscribeToResourceAsync throws InvalidOperationException if the server does not advertise the subscribe capability.
Step 7: Discover and Render Dynamic Prompts
MCP prompts are reusable message templates stored on the server. Each prompt declares arguments that the client fills in before rendering.
// ──────────────────────────────────────
// 1. List available prompts
// ──────────────────────────────────────
var prompts = await client.GetPromptsAsync();
foreach (var prompt in prompts)
{
Console.WriteLine($"Prompt: {prompt.Name}");
Console.WriteLine($" Title: {prompt.Title}");
Console.WriteLine($" Description: {prompt.Description}");
foreach (var arg in prompt.Arguments)
{
string required = arg.Required == true ? "required" : "optional";
Console.WriteLine($" Arg: {arg.Name} ({required}) {arg.Description}");
}
Console.WriteLine();
}
// ──────────────────────────────────────
// 2. Render a prompt with arguments
// ──────────────────────────────────────
var result = await client.GetPromptAsync("draft_email", new Dictionary<string, object>
{
{ "topic", "Quarterly review meeting" },
{ "recipient", "Engineering Team" }
});
Console.WriteLine($"Description: {result.Description}");
Console.WriteLine($"Messages: {result.Messages.Count}");
foreach (var message in result.Messages)
{
Console.WriteLine($"\n[{message.Role}]");
foreach (var part in message.Content)
{
if (part.Type == "text")
Console.WriteLine(part.Text);
else if (part.Type == "resource")
Console.WriteLine($" Embedded resource: {part.Resource.Uri}");
else if (part.Type == "image")
Console.WriteLine($" Image: {part.MimeType} ({part.Data?.Length ?? 0} chars base64)");
}
}
GetPromptAsync sends a prompts/get request with the argument map and returns an McpPromptResult containing pre-formatted messages. Each message has a Role (User or Assistant) and a list of McpPromptContent parts that may be text, embedded resources, images, or audio.
Step 8: Handle Sampling Requests from the Server
Sampling is a server-to-client request: the MCP server asks your application to run an LLM completion. This enables agentic behaviors where the server delegates reasoning to the client's local model.
using LMKit.Model;
using LMKit.TextGeneration;
// ──────────────────────────────────────
// 1. Load a model for sampling
// ──────────────────────────────────────
Console.WriteLine("Loading model for sampling...");
using LM model = LM.LoadFromModelID("qwen3:8b",
downloadingProgress: (_, len, read) =>
{
if (len.HasValue) Console.Write($"\r Downloading: {(double)read / len.Value * 100:F1}% ");
return true;
},
loadingProgress: p => { Console.Write($"\r Loading: {p * 100:F0}% "); return true; });
Console.WriteLine("\n");
// ──────────────────────────────────────
// 2. Option A: Register a handler function
// ──────────────────────────────────────
client.SetSamplingHandler(async (request, cancellationToken) =>
{
var conversation = new SingleTurnConversation(model);
if (request.SystemPrompt != null)
conversation.SystemPrompt = request.SystemPrompt;
if (request.MaxTokens > 0)
conversation.MaximumCompletionTokens = request.MaxTokens;
string prompt = request.Messages[0].Content.Text;
var completionResult = conversation.Submit(prompt);
return McpSamplingResponse.FromText(
completionResult.Completion,
model: "qwen3:8b",
stopReason: McpStopReason.EndTurn);
});
// ──────────────────────────────────────
// 3. Option B: Use the event (alternative)
// ──────────────────────────────────────
// client.SamplingRequested += (sender, args) =>
// {
// // Accept with a quick text response
// args.Accept("Here is the generated response.", model: "qwen3:8b");
//
// // Or reject the request
// // args.Reject("Sampling not allowed for this request type");
// };
The handler function takes precedence. If the handler returns null, the SamplingRequested event fires as a fallback. If neither produces a response, the client sends an error back to the server.
Step 9: Handle Elicitation Requests (User Input)
Elicitation is the server asking your application to collect input from the user. The server describes what it needs using a JSON schema, and your code gathers the data and responds.
// ──────────────────────────────────────
// 1. Attach the elicitation handler
// ──────────────────────────────────────
client.ElicitationRequested += (sender, args) =>
{
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine($"Server requests input: {args.Request.Message}");
Console.ResetColor();
if (!string.IsNullOrEmpty(args.Request.RequestedSchema))
Console.WriteLine($"Schema: {args.Request.RequestedSchema}");
// Collect user input (in a real application, render a form)
Console.Write("Enter value: ");
string userInput = Console.ReadLine();
if (string.IsNullOrWhiteSpace(userInput))
{
// User chose not to provide input
args.Decline();
return;
}
args.Accept(new Dictionary<string, object>
{
{ "response", userInput }
});
};
// ──────────────────────────────────────
// 2. Or use a handler function
// ──────────────────────────────────────
// client.SetElicitationHandler((request, cancellationToken) =>
// {
// Console.WriteLine($"Elicitation: {request.Message}");
// // Return acceptance, decline, or cancellation
// return Task.FromResult(McpElicitationResponse.Accept(
// new Dictionary<string, object> { { "response", "auto-filled value" } }
// ));
// });
Three responses are available: Accept (with a content dictionary matching the schema), Decline (user refused), and Cancel (operation cancelled). If no handler or event is registered, the client automatically sends a Cancel response.
Step 10: Track Progress and Monitor Logs
MCP servers can push progress notifications for long-running operations and stream log messages for diagnostics.
// ──────────────────────────────────────
// 1. Progress tracking
// ──────────────────────────────────────
client.ProgressReceived += (sender, args) =>
{
string pct = args.Percentage.HasValue ? $" ({args.Percentage:F1}%)" : "";
Console.ForegroundColor = ConsoleColor.DarkGray;
Console.WriteLine($" Progress: {args.Progress}/{args.Total}{pct} {args.Message}");
Console.ResetColor();
};
// ──────────────────────────────────────
// 2. Set log level and listen for messages
// ──────────────────────────────────────
if (client.HasCapability(McpServerCapabilities.Logging))
{
await client.SetLogLevelAsync(McpLogLevel.Info);
client.LogMessageReceived += (sender, args) =>
{
Console.ForegroundColor = ConsoleColor.DarkGray;
Console.WriteLine($" [{args.Level}] {args.Logger}: {args.Data}");
Console.ResetColor();
};
}
// ──────────────────────────────────────
// 3. Protocol-level diagnostics
// ──────────────────────────────────────
client.Sending += (sender, args) =>
{
Console.ForegroundColor = ConsoleColor.DarkGray;
Console.WriteLine($" >>> {args.Method}");
Console.ResetColor();
};
client.Received += (sender, args) =>
{
Console.ForegroundColor = ConsoleColor.DarkGray;
Console.WriteLine($" <<< {args.Method} (HTTP {args.StatusCode})");
Console.ResetColor();
};
// ──────────────────────────────────────
// 4. React to catalog changes
// ──────────────────────────────────────
client.ResourcesChanged += (sender, _) =>
{
Console.WriteLine(" Resource catalog changed. Refreshing...");
};
client.PromptsChanged += (sender, _) =>
{
Console.WriteLine(" Prompt catalog changed. Refreshing...");
};
SetLogLevelAsync sends a logging/setLevel request to the server, telling it to filter messages below the specified severity. The McpLogLevel enum follows syslog levels: Debug, Info, Notice, Warning, Error, Critical, Alert, Emergency.
Progress events include Progress (current value), Total (expected total, if known), Percentage (computed automatically), and Message (optional description).
Common Issues
| Problem | Cause | Fix |
|---|---|---|
InvalidOperationException on SubscribeToResourceAsync |
Server does not support resource subscriptions | Check the server's capabilities before subscribing. Not all servers that support Resources also support subscriptions. |
GetResourcesAsync returns empty |
Server has no resources published | Verify the server exposes resources. Check HasCapability(McpServerCapabilities.Resources) first. |
| Sampling handler never called | Server did not request sampling | Sampling is server-initiated. Your handler only runs when the server sends a sampling/createMessage request. |
SetLogLevelAsync throws |
Server does not advertise logging | Guard with HasCapability(McpServerCapabilities.Logging) before calling. |
| Prompt arguments ignored | Wrong argument names | Check McpPrompt.Arguments for the exact names and required status the server expects. |
ResourceUpdated event not firing |
Not subscribed to the resource | Call SubscribeToResourceAsync for each URI you want to monitor. The event only fires for subscribed resources. |
Next Steps
- Connect to MCP Servers from Your Application: covers MCP basics, tool discovery, authentication, and multi-server setups.
- Create an AI Agent with Tools: build agents with built-in and custom tools, then combine with MCP capabilities.