Building Intelligent SharePoint Agents – Part 2: Implementation

5
(1)

In Part 1, we explored the fundamental shift from manual SharePoint indexing to Azure AI Foundry’s SharePoint grounding tool. We covered the challenges of traditional approaches, the power of Microsoft 365 Copilot API, and the cost considerations for enterprise adoption.

This part is the build: connecting a SharePoint site, wiring the tool into a .NET agent, and testing it end to end.

Prerequisites

Before diving into implementation, ensure you have the following ready:

  • Azure subscription with access to Microsoft Foundry (the rebrand of Azure AI Foundry; both names still show up in the docs and SDKs)
  • Microsoft 365 Copilot license, $30/user/month, for every developer and end user who’ll query the agent. If your users don’t have Copilot licenses, Microsoft’s pay-as-you-go option (also preview) covers that instead
  • Foundry User RBAC role for all developers and end users (Microsoft renamed this role from Azure AI User; permissions are unchanged)
  • READ access to the target SharePoint sites, plus their site URLs
  • Admin access to configure connections, if your organization requires it
  • .NET 8 or later, Visual Studio 2022 or VS Code. .NET 8’s mainstream support ends November 10, 2026, so if you’re starting a new project, target .NET 10 instead

Set up the SharePoint connection

If you haven’t set up an Azure AI Foundry Project yet, please refer to steps 1 and 2 in my first blog post: “Migrating from Bing Search APIs to Azure OpenAI Agent with Grounding Bing Search

  1. Navigate to “Connected Resources” in your AI Foundry project
  2. Click “Add Connection
  3. Select “SharePoint” as the connection type
  4. Enter your target site URL and give it a Connection name
  5. Select “Add connection

Troubleshooting Tip: If the connection fails, confirm your Azure AI service has permission to reach SharePoint through your organization’s Microsoft 365 tenant.

Set up the .NET project

Now for the .NET side: wiring these agents into an actual application.

A note on the code below: it targets the Azure.AI.Agents.Persistent package, the preview SDK current when this series was first published in September 2025. Microsoft’s SharePoint tool documentation has since moved to a different package (Azure.AI.Projects, with SharepointPreviewTool and the Responses API).

The pattern here (look up a connection, wrap it in a tool definition) still maps directly. Check the current Microsoft Learn SharePoint tool page for exact class names before you drop this into a new project.

2.1 Configuration Setup

Update appsettings.json and add SharePoint to the agents section:

JSON
{
  "AzureAI": {
    "ConnectionString": "https://yourproject.services.ai.azure.com/api/projects/yourproject"
  },
  "AzureOpenAI": {
    "Endpoint": "https://your-ai-foundry-resource.openai.azure.com",
    "ApiKey": "your-api-key-or-use-managed-identity",
    "DeploymentName": "gpt-4o-deployment"
  },
  "Agents": [
    {
      "Name": "SharePoint Knowledge Agent",
      "Description": "Enterprise knowledge assistant with SharePoint access",
      "Instructions": "You are a knowledgeable assistant with access to enterprise SharePoint content. When users ask questions, search through connected SharePoint sites to provide accurate, current information based on official company documents. Always cite specific documents and respect user permissions.",
      "Deployment": "gpt-4o-deployment",
      "Tools": [
        {
          "ToolType": "SharePointGrounding",
          "ConnectionName": "MainSharePointConnection"
        }
      ]
    },
    {
      "Name": "Web Search Agent",
      "Description": "External information research assistant",
      "Instructions": "You are a research assistant that searches the web for current information. Use Bing search to find relevant, up-to-date information and provide accurate responses with proper citations.",
      "Deployment": "gpt-4o-deployment",
      "Tools": [
        {
          "ToolType": "BingGroundingSearch",
          "ConnectionName": "BingSearchConnection"
        }
      ]
    },
    {
      "Name": "Hybrid Research Agent",
      "Description": "Combined internal and external research specialist",
      "Instructions": "You are a comprehensive research assistant with access to both internal SharePoint content and external web information. Always check internal sources first for company-specific information, then supplement with external research as needed. Clearly distinguish between internal and external sources.",
      "Deployment": "gpt-4o-deployment",
      "Tools": [
        {
          "ToolType": "SharePointGrounding",
          "ConnectionName": "MainSharePointConnection"
        },
        {
          "ToolType": "BingGroundingSearch",
          "ConnectionName": "BingSearchConnection"
        }
      ]
    }
  ]
}

2.2 Agent Service Implementation

The agent service sits between your application and Azure AI Foundry. Here are the pieces that matter:

C#
private async Task<ToolDefinition?> CreateToolDefinitionAsync(ToolsOptions tool)
{
	try
	{
		return tool.ToolType switch
		{
			"SharePointGrounding" => await CreateSharePointToolAsync(tool),
			"BingGroundingSearch" => await CreateBingSearchToolAsync(tool),
			"CustomBingGroundingSearch" => await CreateCustomBingSearchToolAsync(tool),
			_ => throw new NotSupportedException($"Tool type '{tool.ToolType}' is not supported")
		};
	}
	catch (Exception ex)
	{
		_logger.LogError(ex, "Failed to create tool definition for {ToolType} with connection {ConnectionName}",
			tool.ToolType, tool.ConnectionName);
		return null;
	}
}

private async Task<ToolDefinition> CreateSharePointToolAsync(ToolsOptions tool)
{
	var connection = await GetConnectionByNameAsync(tool.ConnectionName);
	if (connection?.Id == null)
	{
		throw new InvalidOperationException($"SharePoint connection '{tool.ConnectionName}' not found or invalid");
	}

	_logger.LogInformation("Creating SharePoint grounding tool with connection: {ConnectionName}",
		tool.ConnectionName);
	
	return new SharepointToolDefinition(
		new SharepointGroundingToolParameters(connection.Id)
	);
}

private async Task<ToolDefinition> CreateBingSearchToolAsync(ToolsOptions tool)
{
	var connection = await GetConnectionByNameAsync(tool.ConnectionName);
	if (connection?.Id == null)
	{
		throw new InvalidOperationException($"Bing Search connection '{tool.ConnectionName}' not found or invalid");
	}

	return new BingGroundingToolDefinition(
		new BingGroundingSearchToolParameters(
			[new BingGroundingSearchConfiguration(connection.Id)]
		)
	);
}

private async Task<ToolDefinition> CreateCustomBingSearchToolAsync(ToolsOptions tool)
{
	var connection = await GetConnectionByNameAsync(tool.ConnectionName);
	if (connection?.Id == null)
	{
		throw new InvalidOperationException($"Custom Bing Search connection '{tool.ConnectionName}' not found or invalid");
	}

	var configuration = new BingCustomSearchConfiguration(
		connection.Id,
		tool.ConfigurationName ?? "default")
	{
		Count = 5,
		SetLang = "en",
		Market = "en-us"
	};

	return new BingCustomSearchToolDefinition(
		new BingCustomSearchToolParameters([configuration])
	);
}

CreateToolDefinitionAsync Method is the factory method. It pattern-matches on tool.ToolType to build the tool the config asks for.

CreateSharePointToolAsync Method handles the SharePoint-specific part. It looks up the connection by name and wraps it in a SharepointToolDefinition, which is what the AI service uses to reach your SharePoint content.

Both methods check the connection before creating anything and throw a specific exception if it’s missing. I’d rather fail at startup with a clear error than have an agent silently come up with no working tools.

Putting tool creation behind this service means adding a new tool type later is a new case in the switch statement, not a rewrite of the agent logic.

2.3 Agent wrapper class

The agent wrapper sits around the core agent and handles initialization, cleanup, and resource management. Here’s the method I added for connection validation:

C#
public async Task<bool> ValidateConnectionsAsync()
        {
            try
            {
                if (_agentOptions.Tools == null || _agentOptions.Tools.Count == 0)
                {
                    _logger.LogInformation("Agent '{AgentName}' has no tools to validate", _agentOptions.Name);
                    return true;
                }

                var allValid = true;
                foreach (var tool in _agentOptions.Tools)
                {
                    var isValid = await _agentService.ValidateConnectionAsync(tool.ConnectionName);
                    if (!isValid)
                    {
                        _logger.LogWarning("Invalid connection '{ConnectionName}' for tool '{ToolType}' in agent '{AgentName}'", 
                            tool.ConnectionName, tool.ToolType, _agentOptions.Name);
                        allValid = false;
                    }
                }

                return allValid;
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Failed to validate connections for agent '{AgentName}'", _agentOptions.Name);
                return false;
            }
        }

Connection Validation: Before an agent becomes active, it validates that all required connections are working properly. This prevents agents from failing during user interactions.

Error Handling: The wrapper includes comprehensive error handling and logging, making it easier to diagnose issues in production environments.

2.4 Main Application

The main application brings everything together:

C#
using Azure.AI.Agents.Persistent;
using BingGroundingAgent;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System.ComponentModel.DataAnnotations;

var builder = Host.CreateApplicationBuilder(args);

var configuration = new ConfigurationBuilder()
    .SetBasePath(Directory.GetCurrentDirectory())
    .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
    .AddEnvironmentVariables()
    .Build();

builder.Services.AddSingleton<IConfiguration>(configuration);
builder.Services.AddSingleton<AgentService>();

// Register agents from configuration
var agentsConfig = new AgentsConfiguration();
configuration.GetSection("Agents").Bind(agentsConfig.Agents);

if (agentsConfig.Agents.Count == 0)
{
	Console.WriteLine("No agents configured. Please check your appsettings.json file.");
	return;
}

// Validate agent configurations
var validAgents = new List<AgentOptions>();
foreach (var agentConfig in agentsConfig.Agents)
{
	var validationResults = new List<ValidationResult>();
	var validationContext = new ValidationContext(agentConfig);

	if (Validator.TryValidateObject(agentConfig, validationContext, validationResults, true))
	{
		validAgents.Add(agentConfig);
	}
	else
	{
		var errors = string.Join(", ", validationResults.Select(vr => vr.ErrorMessage));
		Console.WriteLine($"Invalid configuration for agent '{agentConfig.Name}': {errors}");
	}
}

if (validAgents.Count == 0)
{
	Console.WriteLine("No valid agents found. Please check your configuration.");
	return;
}

// Register each agent as a named service
foreach (var agentConfig in agentsConfig.Agents)
{
    var agentType = agentConfig.Name; // Fallback to name if type is not set
    builder.Services.AddKeyedSingleton<AzureAgent>(
        agentType,
        (provider, key) =>
            new AzureAgent(
                provider.GetRequiredService<ILogger<AzureAgent>>(),
                provider.GetRequiredService<AgentService>(),
                agentConfig
            )
    );
}

builder.Services.AddLogging(logging =>
{
    logging.AddConsole();
    logging.SetMinimumLevel(LogLevel.Information);
});

var host = builder.Build();
var logger = host.Services.GetRequiredService<ILogger<Program>>();

// Get available agents from configuration
var availableAgents = new AgentsConfiguration();
configuration.GetSection("Agents").Bind(availableAgents.Agents);

// Let user select which agent to use
Console.WriteLine("Available agents:");
for (int i = 0; i < availableAgents.Agents.Count; i++)
{
    var agent = availableAgents.Agents[i];
    Console.WriteLine($"{i + 1}. {agent.Name}");
}

Console.Write("Select an agent (1-{0}): ", availableAgents.Agents.Count);
var selection = Console.ReadLine();

if (
    !int.TryParse(selection, out int agentIndex)
    || agentIndex < 1
    || agentIndex > availableAgents.Agents.Count
)
{
    logger.LogError("Invalid agent selection");
    return;
}

var selectedAgentConfig = availableAgents.Agents[agentIndex - 1];
var agentKey = selectedAgentConfig.Name;
var selectedAgent = host.Services.GetRequiredKeyedService<AzureAgent>(agentKey);

try
{
    logger.LogInformation("Initializing {AgentName}...", selectedAgentConfig.Name);
    await selectedAgent.InitializeAsync();

    while (true)
    {
        Console.Write("\nYou: ");
        var input = Console.ReadLine();
        if (string.IsNullOrEmpty(input) || input.ToLower() == "exit")
            break;

        logger.LogInformation("Processing query: {Query}", input);

        Console.Write("Assistant: ");
        await foreach (var update in await selectedAgent.ProcessQueryAsync(input))
        {
            try
            {
                if (update.UpdateKind == StreamingUpdateReason.MessageUpdated)
                {
                    if (update is MessageContentUpdate messageContent)
                    {
                        // Filter out citation markers and special characters
                        var text = messageContent.Text;
                        if (!string.IsNullOrEmpty(text))
                        {
                            Console.Write(text);
                        }
                    }
                }
            }
            catch (Exception updateEx)
            {
                logger.LogWarning(updateEx, "Error processing streaming update");
            }
        }

        // Output references (citations)
        await selectedAgent.DisplayCitationsAsync();
    }
}
catch (Exception ex)
{
    logger.LogError(ex, "An error occurred: {Message}", ex.Message);
}

logger.LogInformation("Application completed");
await host.StopAsync();

The program reads agent configs from appsettings.json and validates each one with data annotations before anything runs, so a typo in the config file fails at startup instead of mid-conversation. It lets you pick which agent to talk to and streams the response as MessageContentUpdate events arrive.

Citations print after the response finishes, not inline, where they’d break up the text.

Test SharePoint agent

Run the application and select the SharePoint agent:

Try queries like:

  • What documents are available in SharePoint?
  • Find information about [specific topic in your SharePoint]
  • Show me the latest [document type] from [SharePoint site]
  • List key points from document [document name]

Security Considerations

Your SharePoint agents touch sensitive corporate information, so the security model is worth understanding upfront.

  1. Data in transit: all traffic between your application, Azure AI Foundry, and SharePoint uses TLS encryption.
  2. Permission inheritance: agents automatically respect SharePoint permissions. Users only see content they’re already authorized to access.
  3. No data storage: Azure AI Foundry doesn’t store your SharePoint content. It queries live, on every request.

SharePoint Site Optimization:

  • Limit scope: connect only the SharePoint sites that contain relevant information.
  • Organize content: well-structured sites with clear metadata improve search accuracy.
  • Regular cleanup: archive or delete outdated documents to keep results relevant.

Conclusion

You’ve successfully built a powerful SharePoint agent that transform how your organization accesses and utilizes knowledge. These agents eliminate the traditional barriers of manual indexing while maintaining security and permissions.

The impact on your organization:

  • Time Savings: Users find information in seconds instead of hours
  • Better Decisions: Access to comprehensive, current information
  • Knowledge Democratization: Everyone can access organizational knowledge easily
  • Reduced IT Overhead: No search infrastructure to maintain

Before you open this to real users, run a permission test: sign in as two users with different access to the same site, ask both the same question, and confirm the one without permission gets no citation back. Microsoft’s own SharePoint tool documentation recommends exactly this check before rollout, and it matters more than watching a demo query return an answer.

Source Code

The complete source code for this series is on GitHub: GitHub Repository Link

How useful was this post?

Click on a star to rate it!

Average rating 5 / 5. Vote count: 1

No votes so far! Be the first to rate this post.