Wednesday, 23 September 2026

Use Microsoft Graph from a Microsoft Agent Framework Agent

Some time ago, I wrote about using the Microsoft Search API to query SharePoint content. At the time, the API and the .NET SDK support were still in preview.

More recently, I wrote about letting a Microsoft Agent Framework agent run C# functions as tools. In this post, we will combine the two approaches by using Microsoft Graph to search Microsoft 365 and exposing that search as a function tool the agent can run.

Microsoft Search is now available through the Microsoft Graph v1.0 endpoint, and it is a useful capability to put behind an agent tool. It already searches content indexed by Microsoft 365, understands SharePoint and OneDrive permissions, and returns results the signed-in user can access.

In this post, we will give a Microsoft Agent Framework agent a tool that searches files across SharePoint and OneDrive. The user can ask in natural language, the model can turn that request into a search query, and our .NET function will execute the query through Microsoft Graph.

Search before retrieval infrastructure

The requirement is simple: find Microsoft 365 files related to a topic and return useful links. We do not need to copy documents into a separate vector database to do that. Microsoft Search already indexes the content and gives us keyword search, KQL filters, relevance ranking, and permission-aware results.

The request will follow this path:

User
  -> Microsoft Agent Framework agent
      -> .NET function tool
          -> Microsoft Graph Search
              -> SharePoint and OneDrive

This is still a normal Agent Framework function tool. Microsoft Graph is an application integration, so our application owns the Graph client, authentication, query, and result shaping. The model only sees the tool description and the structured result we return.

This sample uses separate credentials: DefaultAzureCredential for Microsoft Foundry and DeviceCodeCredential for delegated Microsoft Graph access. Azure CLI sign-in does not provide the Graph token, so the user signs in separately when the first Graph request runs.

Prepare the Microsoft Entra app registration

Create an app registration for the console application:

  1. Open Microsoft Entra admin center > App registrations.
  2. Create a new single-tenant application.
  3. Copy the Application (client) ID and Directory (tenant) ID.
  4. Open Authentication > Advanced settings and enable Allow public client flows.
  5. Under API permissions, add the delegated Microsoft Graph permission Files.Read.All.

Files.Read.All allows the application to read files the signed-in user can access. It does not make private files visible to a user who could not already access them. The permission is read-only and, according to the current Microsoft Graph permissions reference, delegated Files.Read.All does not require administrator consent. Your tenant's user-consent policy can still require an administrator to approve it.

Create the console application

The project uses .NET 10, Microsoft Agent Framework, Azure Identity, and the Microsoft Graph .NET SDK:

dotnet new console -n AgentWithGraph --framework net10.0
cd AgentWithGraph

dotnet add package Microsoft.Agents.AI.Foundry
dotnet add package Azure.Identity
dotnet add package Microsoft.Graph

I tested this sample with Microsoft.Agents.AI.Foundry 1.5.0, Azure.Identity 1.21.0, and Microsoft.Graph 6.7.0.

Sign in to Microsoft Graph as the user

Read the tenant and client IDs from environment variables, then create a DeviceCodeCredential:

DeviceCodeCredential graphCredential = new(new DeviceCodeCredentialOptions
{
    AuthorityHost = AzureAuthorityHosts.AzurePublicCloud,
    TenantId = tenantId,
    ClientId = clientId,
    DeviceCodeCallback = (code, cancellationToken) =>
    {
        Console.WriteLine(code.Message);
        return Task.CompletedTask;
    }
});

GraphServiceClient graphClient = new(graphCredential, ["Files.Read.All"]);

The Graph SDK asks the credential for a token when the first Graph request is made. The callback prints a short code and the URL where the user should sign in. Azure Identity handles token acquisition and caching; we do not need to put a client secret in this desktop-style application.

Turn Microsoft Search into a function tool

The tool accepts one string. It can be plain keywords such as Project Northstar, or a KQL query such as Project Northstar filetype:docx.

[Description("Search files in SharePoint and OneDrive that the signed-in user can access. The query can contain keywords or Microsoft Search KQL.")]
async Task<Microsoft365FileSearchResult> SearchMicrosoft365Files(
    [Description("Keywords or a Microsoft Search KQL query, for example: project northstar filetype:docx")] string query)
{
    Console.WriteLine($"[Tool] Searching Microsoft 365 for: {query}");

    QueryPostRequestBody requestBody = new()
    {
        Requests =
        [
            new SearchRequest
            {
                EntityTypes = [EntityType.DriveItem],
                Query = new SearchQuery { QueryString = query },
                From = 0,
                Size = 5
            }
        ]
    };

    QueryPostResponse? response = await graphClient.Search.Query
        .PostAsQueryPostResponseAsync(requestBody);

    // Result mapping continues below.
}

Setting EntityType.DriveItem scopes the search to files and folders in SharePoint and OneDrive. The API returns results in relevance order by default. We ask for five results because every tool result becomes part of the model's context; returning hundreds of search hits would make the answer slower and less focused.

The model is allowed to supply the query, but the application still controls the endpoint, entity type, page size, delegated permission, and fields returned to the model.

Return facts, not a prewritten answer

Microsoft Graph returns each match as a SearchHit. For a driveItem search, its resource is a DriveItem. We reduce that response to the values the agent needs:

List<Microsoft365File> files = [];

foreach (SearchResponse searchResponse in response?.Value ?? [])
{
    foreach (SearchHitsContainer container in searchResponse.HitsContainers ?? [])
    {
        foreach (SearchHit hit in container.Hits ?? [])
        {
            if (hit.Resource is not DriveItem driveItem)
            {
                continue;
            }

            files.Add(new Microsoft365File(
                driveItem.Name ?? "Untitled",
                driveItem.WebUrl ?? string.Empty,
                CleanSummary(hit.Summary),
                driveItem.LastModifiedDateTime));
        }
    }
}

return new Microsoft365FileSearchResult(query, files);

Search summaries contain markup such as <c0> to identify highlighted terms. The sample removes that markup before returning the summary to the model.

The structured result contains the search query, file name, URL, search snippet, and last modified date. This keeps Graph data separate from the final response. The model can explain why a result looks useful, but it cannot invent another file and present it as a search result.

Give the agent a narrow contract

The instructions are deliberately explicit about what the agent has and has not seen:

const string instructions = """
    You help employees find files in Microsoft 365.
    Always use the Microsoft 365 file search tool before answering a file search question.
    Only describe files returned by the tool. Do not claim to have read a document when only a search snippet is available.
    Include a clickable source link for every file you recommend.
    """;

AIAgent agent = projectClient.AsAIAgent(
    model: modelDeployment,
    instructions: instructions,
    name: "Microsoft365SearchAssistant",
    tools: [AIFunctionFactory.Create(SearchMicrosoft365Files)]);

AIFunctionFactory.Create turns the C# method into an Agent Framework tool. The method and parameter descriptions become part of the tool definition sent to the model. When the user asks for files, the model chooses the tool and supplies a query.

The complete sample

Replace Program.cs with the following code:

using System.ComponentModel;
using System.Net;
using System.Text.RegularExpressions;
using Azure.AI.Projects;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using Microsoft.Graph;
using Microsoft.Graph.Models;
using Microsoft.Graph.Search.Query;

string foundryEndpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT")
    ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
string modelDeployment = Environment.GetEnvironmentVariable("FOUNDRY_MODEL")
    ?? throw new InvalidOperationException("FOUNDRY_MODEL is not set.");
string tenantId = Environment.GetEnvironmentVariable("GRAPH_TENANT_ID")
    ?? throw new InvalidOperationException("GRAPH_TENANT_ID is not set.");
string clientId = Environment.GetEnvironmentVariable("GRAPH_CLIENT_ID")
    ?? throw new InvalidOperationException("GRAPH_CLIENT_ID is not set.");

DeviceCodeCredential graphCredential = new(new DeviceCodeCredentialOptions
{
    AuthorityHost = AzureAuthorityHosts.AzurePublicCloud,
    TenantId = tenantId,
    ClientId = clientId,
    DeviceCodeCallback = (code, cancellationToken) =>
    {
        Console.WriteLine(code.Message);
        return Task.CompletedTask;
    }
});

GraphServiceClient graphClient = new(graphCredential, ["Files.Read.All"]);

[Description("Search files in SharePoint and OneDrive that the signed-in user can access. The query can contain keywords or Microsoft Search KQL.")]
async Task<Microsoft365FileSearchResult> SearchMicrosoft365Files(
    [Description("Keywords or a Microsoft Search KQL query, for example: project northstar filetype:docx")] string query)
{
    Console.WriteLine($"[Tool] Searching Microsoft 365 for: {query}");

    QueryPostRequestBody requestBody = new()
    {
        Requests =
        [
            new SearchRequest
            {
                EntityTypes = [EntityType.DriveItem],
                Query = new SearchQuery { QueryString = query },
                From = 0,
                Size = 5
            }
        ]
    };

    QueryPostResponse? response = await graphClient.Search.Query
        .PostAsQueryPostResponseAsync(requestBody);

    List<Microsoft365File> files = [];

    foreach (SearchResponse searchResponse in response?.Value ?? [])
    {
        foreach (SearchHitsContainer container in searchResponse.HitsContainers ?? [])
        {
            foreach (SearchHit hit in container.Hits ?? [])
            {
                if (hit.Resource is not DriveItem driveItem)
                {
                    continue;
                }

                files.Add(new Microsoft365File(
                    driveItem.Name ?? "Untitled",
                    driveItem.WebUrl ?? string.Empty,
                    CleanSummary(hit.Summary),
                    driveItem.LastModifiedDateTime));
            }
        }
    }

    return new Microsoft365FileSearchResult(query, files);
}

DefaultAzureCredential foundryCredential = new(new DefaultAzureCredentialOptions
{
    ExcludeManagedIdentityCredential = true
});
AIProjectClient projectClient = new(new Uri(foundryEndpoint), foundryCredential);

const string instructions = """
    You help employees find files in Microsoft 365.
    Always use the Microsoft 365 file search tool before answering a file search question.
    Only describe files returned by the tool. Do not claim to have read a document when only a search snippet is available.
    Include a clickable source link for every file you recommend.
    """;

AIAgent agent = projectClient.AsAIAgent(
    model: modelDeployment,
    instructions: instructions,
    name: "Microsoft365SearchAssistant",
    tools: [AIFunctionFactory.Create(SearchMicrosoft365Files)]);

const string prompt = "Find documents about Project Northstar that I can access and tell me which ones look most useful. Include links.";

Console.WriteLine($"\nUser: {prompt}\n");
Console.WriteLine($"Agent: {await agent.RunAsync(prompt)}");

static string CleanSummary(string? summary)
{
    string withoutTags = Regex.Replace(summary ?? string.Empty, "<[^>]+>", " ");
    return Regex.Replace(WebUtility.HtmlDecode(withoutTags), @"\s+", " ").Trim();
}

record Microsoft365File(
    string Name,
    string WebUrl,
    string Summary,
    DateTimeOffset? LastModifiedDateTime);

record Microsoft365FileSearchResult(
    string Query,
    IReadOnlyList<Microsoft365File> Files);

Run it against your tenant

Set the Foundry project endpoint, model deployment, and the two values copied from the app registration. In PowerShell:

$env:FOUNDRY_PROJECT_ENDPOINT="YOUR_FOUNDRY_PROJECT_ENDPOINT"
$env:FOUNDRY_MODEL="YOUR_MODEL_DEPLOYMENT_NAME"
$env:GRAPH_TENANT_ID="YOUR_TENANT_ID"
$env:GRAPH_CLIENT_ID="YOUR_APP_CLIENT_ID"

Sign in to Azure for the Foundry connection, then run the application:

az login
dotnet run

The first Graph request prints a device sign-in message. Open the displayed URL, enter the code, and sign in with a work or school account from the tenant. The console will then show the query selected by the model:

User: Find documents about Project Northstar that I can access and tell me which ones look most useful. Include links.

[Tool] Searching Microsoft 365 for: "Project Northstar" isDocument=true

Agent: I found the following files...

The exact query and final wording can vary by model. The file names, URLs, snippets, and dates in the answer come from Microsoft Graph.

What the agent can actually know

This tool returns search metadata and a highlighted snippet. It does not download the complete file. The agent can identify likely useful documents and explain the evidence in the search result, but it should not claim to have read or summarized the full document.

If the requirement changes to answering questions from document contents, add a separate, tightly scoped tool that retrieves the selected file content. Keep search and content retrieval as separate operations so that the application can validate the selected file, enforce size limits, and audit access before sending content to the model.

Microsoft Graph controls which files the user can access. Our application still controls which Graph operations are exposed to the agent and how much Microsoft 365 data is returned to the model.

Wrapping up

We connected a Microsoft Agent Framework agent to Microsoft Graph through a focused function tool. The model translates a natural-language request into a Microsoft Search query, Graph returns permission-aware SharePoint and OneDrive results, and the tool gives the agent a small structured response containing file names, snippets, dates, and links.

For finding Microsoft 365 content, this is a useful place to start. It uses the search index and permissions already present in Microsoft 365 without introducing a separate ingestion pipeline or vector database.

Hope this helps!

No comments: