Skip to content

Improvements to MCP Server Sample#518

Open
MehakBindra wants to merge 2 commits into
mainfrom
mehak/mcp-use-graph
Open

Improvements to MCP Server Sample#518
MehakBindra wants to merge 2 commits into
mainfrom
mehak/mcp-use-graph

Conversation

@MehakBindra
Copy link
Copy Markdown
Collaborator

@MehakBindra MehakBindra commented May 21, 2026

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_user tool 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

  • Added a new GraphClient class 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 new find_user tool and associated models (UserMatch, FindUserResult).
  • All tools now use the Azure AD object ID (userId) for identifying users, and the documentation clarifies this.

Adaptive Card-based Ask Flow

  • The ask tool 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.
  • Added a handler for the new "ask_reply" adaptive card action, updating the pending ask and unblocking any waiters.

Wait-based APIs for Replies and Approvals

  • Added wait_for_reply and wait_for_approval APIs that block (with timeout) until a reply or approval decision is received, using TaskCompletionSource for efficient waiting. Manual polling APIs (get_reply, get_approval) remain for compatibility

Other Improvements

  • Updated the README to document the new tools and clarify user ID usage.
  • Refactored internal state handling to support the new flows and concurrency model.

These changes modernize the MCP sample, improve user experience, and enable more robust automation scenarios.

@MehakBindra MehakBindra marked this pull request as ready for review May 21, 2026 13:47
Copilot AI review requested due to automatic review settings May 21, 2026 13:47
@MehakBindra MehakBindra changed the title Mehak/mcp use graph Improvements to MCP Server Sample May 21, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ask to use an Adaptive Card reply box + add wait_for_reply / wait_for_approval tools backed by TaskCompletionSource.
  • Add a lightweight app-only GraphClient and 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 PendingAsks transitions to answered between GetOrAdd and the re-check, WaitForReply returns but leaves a TaskCompletionSource in ReplyWaiters forever. Similarly, the timeout path never removes the waiter. Consider removing the waiter (or using a per-call TCS) when returning due to latest.Status != pending and 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 after GetOrAdd but before the re-check returns, the ApprovalWaiters entry 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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants