Improvements to MCP Server Sample#518
Open
MehakBindra wants to merge 2 commits into
Open
Conversation
Contributor
There was a problem hiding this comment.
Pull request overview
This PR updates the core/samples/McpServer sample to support tenant user lookup via Microsoft Graph and to reduce MCP-side polling by adding “wait” style tools that complete as soon as an Adaptive Card action arrives.
Changes:
- Add
find_user(Graph-backed) and switch the sample’s “userId” concept to AAD object id. - Change
askto use an Adaptive Card reply box + addwait_for_reply/wait_for_approvaltools backed byTaskCompletionSource. - Add a lightweight app-only
GraphClientand related models, plus update documentation.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| core/samples/McpServer/State.cs | Removes superseded status and adds waiter dictionaries for reply/approval completion signaling. |
| core/samples/McpServer/README.md | Documents new tools, AAD object id usage, and Graph permissions required for find_user. |
| core/samples/McpServer/Program.cs | Switches to AAD object id for conversation cache key; routes adaptive card actions for approvals and ask replies. |
| core/samples/McpServer/Models.cs | Adds output models for find_user. |
| core/samples/McpServer/McpTools.cs | Adds find_user, wait_for_reply, wait_for_approval, and migrates ask to Adaptive Cards. |
| core/samples/McpServer/McpServer.csproj | Adds Azure.Identity dependency for Graph token acquisition. |
| core/samples/McpServer/GraphClient.cs | Implements app-only Graph /users $search lookup. |
| core/samples/McpServer/Cards.cs | Adds an Adaptive Card template for “ask with reply input”. |
Comments suppressed due to low confidence (2)
core/samples/McpServer/McpTools.cs:106
- If
PendingAskstransitions to answered betweenGetOrAddand the re-check,WaitForReplyreturns but leaves aTaskCompletionSourceinReplyWaitersforever. Similarly, the timeout path never removes the waiter. Consider removing the waiter (or using a per-call TCS) when returning due tolatest.Status != pendingand on timeout/cancellation to avoid unbounded growth in the in-memory dictionaries.
TaskCompletionSource<PendingAsk> waiter = state.ReplyWaiters.GetOrAdd(
requestId,
_ => new TaskCompletionSource<PendingAsk>(TaskCreationOptions.RunContinuationsAsynchronously));
// Re-check after registering the waiter so we don't miss a signal that
// fired between the initial read and GetOrAdd.
if (state.PendingAsks.TryGetValue(requestId, out PendingAsk? latest)
&& latest.Status != AskStatus.Pending)
{
return new ReplyResult(latest.Status, latest.Reply);
}
core/samples/McpServer/McpTools.cs:202
- Same waiter-leak pattern as
WaitForReply: if the approval status flips afterGetOrAddbut before the re-check returns, theApprovalWaitersentry remains. The timeout path also leaves the waiter in the dictionary. Consider removing the waiter when returning early / timing out to prevent unbounded growth.
TaskCompletionSource<string> waiter = state.ApprovalWaiters.GetOrAdd(
approvalId,
_ => new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously));
if (state.Approvals.TryGetValue(approvalId, out string? latest) && latest != ApprovalStatus.Pending)
{
return new ApprovalResult(approvalId, latest);
}
try
{
string result = await waiter.Task.WaitAsync(
TimeSpan.FromSeconds(timeoutSeconds), cancellationToken);
return new ApprovalResult(approvalId, result);
}
catch (TimeoutException)
{
state.Approvals.TryGetValue(approvalId, out string? current);
return new ApprovalResult(approvalId, current ?? ApprovalStatus.Pending);
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
52
to
+60
| AdaptiveCardAction? action = context.Activity.Value?.Action; | ||
| if (action?.Verb != "approval_response") | ||
|
|
||
| switch (action?.Verb) | ||
| { | ||
| return AdaptiveCardResponse.CreateMessageResponse("Unknown action"); | ||
| case "approval_response": | ||
| return HandleApprovalResponse(action, state); | ||
| case "ask_reply": | ||
| return HandleAskReply(action, state); | ||
| default: |
Comment on lines
71
to
78
| if (approvalId is not null | ||
| && (decision == ApprovalStatus.Approved || decision == ApprovalStatus.Rejected) | ||
| && state.Approvals.TryGetValue(approvalId, out string? currentDecision) | ||
| && state.Approvals.TryUpdate(approvalId, decision, currentDecision)) | ||
| { | ||
| if (state.ApprovalWaiters.TryRemove(approvalId, out TaskCompletionSource<string>? waiter)) | ||
| waiter.TrySetResult(decision); | ||
| return AdaptiveCardResponse.CreateMessageResponse("Response recorded"); |
Comment on lines
+90
to
+99
| if (requestId is not null | ||
| && state.PendingAsks.TryGetValue(requestId, out PendingAsk? entry) | ||
| && entry.Status == AskStatus.Pending) | ||
| { | ||
| PendingAsk answered = entry with { Status = AskStatus.Answered, Reply = reply ?? string.Empty }; | ||
| if (state.PendingAsks.TryUpdate(requestId, answered, entry)) | ||
| { | ||
| if (state.ReplyWaiters.TryRemove(requestId, out TaskCompletionSource<PendingAsk>? waiter)) | ||
| waiter.TrySetResult(answered); | ||
| return AdaptiveCardResponse.CreateMessageResponse("Thanks for your reply!"); |
| public ConcurrentDictionary<string, string> Approvals { get; } = new(); | ||
|
|
||
| /// <summary> | ||
| /// requestId -> TCS completed when the user replies (or the ask is superseded). |
Comment on lines
+154
to
+155
| "Snapshot the current status of an approval request. this exists for manual polling." + | ||
| "Returns 'pending', 'approved', or 'rejected'.")] |
| [Description("The request_id returned from ask.")] string requestId, | ||
| [Description("Max seconds to wait before returning (default 30).")] int timeoutSeconds = 30, | ||
| CancellationToken cancellationToken = default) | ||
| { |
Comment on lines
+52
to
+58
| GraphUsersResponse? body = await resp.Content.ReadFromJsonAsync<GraphUsersResponse>( | ||
| cancellationToken: cancellationToken); | ||
|
|
||
| return body?.Value | ||
| .Select(u => new UserMatch(u.Id, u.DisplayName, u.UserPrincipalName)) | ||
| .ToArray() ?? []; | ||
| } |
Comment on lines
+39
to
+43
| string search = $"\"displayName:{query}\" OR \"userPrincipalName:{query}\""; | ||
| string url = "https://graph.microsoft.com/v1.0/users" | ||
| + $"?$search={Uri.EscapeDataString(search)}" | ||
| + "&$select=id,displayName,userPrincipalName" | ||
| + $"&$top={top}"; |
Comment on lines
19
to
23
| [McpServerTool(Name = "notify"), Description("Send a notification to a Teams user. No response expected.")] | ||
| public async Task<NotifyResult> Notify( | ||
| [Description("The Teams user id (e.g. 29:...) to notify.")] string userId, | ||
| [Description("The AAD object id of the Teams user to notify.")] string userId, | ||
| [Description("The message text to send.")] string message, | ||
| CancellationToken cancellationToken = default) |
heyitsaamir
approved these changes
May 22, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This pull request introduces several significant improvements to the MCP server sample, focusing on user search, adaptive card-based asks, and enhanced approval/reply handling. The most important changes include adding a
find_usertool to search for users by name or email, switching the "ask" flow to use adaptive cards (allowing multiple outstanding asks per user), and introducing efficient wait-based APIs for replies and approvals. The changes also update the handling of user IDs to always use Azure AD object IDs, and update the documentation accordingly.User Search and Identity Handling
GraphClientclass and dependency to enable searching for users in the tenant by partial name, email, or UPN, returning their Azure AD object IDs. This is exposed via the newfind_usertool and associated models (UserMatch,FindUserResult).userId) for identifying users, and the documentation clarifies this.Adaptive Card-based Ask Flow
asktool now sends an adaptive card with a reply box to the user, allowing multiple outstanding asks per user (previously only one was allowed). The reply is handled via adaptive card action, and the old single-ask logic is removed.Wait-based APIs for Replies and Approvals
wait_for_replyandwait_for_approvalAPIs that block (with timeout) until a reply or approval decision is received, usingTaskCompletionSourcefor efficient waiting. Manual polling APIs (get_reply,get_approval) remain for compatibilityOther Improvements
These changes modernize the MCP sample, improve user experience, and enable more robust automation scenarios.