Skip to main content

Command Palette

Search for a command to run...

I created a Fabric MCP endpoint with Fabric REST API, Graph API, Azure Functions and Microsoft Agent Framework

Updated
16 min readView as Markdown
I created a Fabric MCP endpoint with Fabric REST API, Graph API, Azure Functions and Microsoft Agent Framework
S
From Synapse Analytics, Power BI, Spark, Microsoft Fabric,ASP.NET Core and recently Agentic AI on .NET I try to explore, learn and share all aspects of Microsoft Data Stack in this blog.

Disclaimer : If you think that I used a coding assistant to develop this MCP endpoint, then sorry I have to disappoint you :) because the core of this approach is ingrained into the application logic that I developed in Jan-2025. Check out the date of that article .

That implementation was limited in a sense that the entire operation was done through a console application. In this article I tried to take that logic a step ahead by integrating it with MAF and Azure Functions and expose it through MCP.

Introduction

Recently, Fabric Core MCP Server was released and is currently under preview. Its integration with other applications is quite seamless and it exposes a lot of useful tools that can significantly simplify and enhance external tool interactions with Microsoft Fabric.

The screenshot below lists the complete set of tools available through the Fabric MCP server.

I published a detailed article a couple of days ago explaining on how to integrate your external client application with Fabric Core MCP Server. You can go through that article here .

Before we get started, following is a list of key tools and technologies used in the implementation of this MCP tool.

It would be helpful if you have some understanding of the above tools and technologies. Of the above list, Microsoft Agent Framework (MAF) is the least used as its implementation is limited for setting up the instructions, tool bindings and passing the user prompts to the MCP Server (Azure Function).

What's the need for this MCP tool ?

Imagine an employee leaving an organization and you wanna know which Fabric items does the user has access to. Wouldn't it be great if you could just send a prompt like

Give me list of items user ABC has access to.
Or
Give me list of items user ABC has access to, which are of type Reports

I couldn't find a tool function in the Fabric Core MCP Server that would return item access of an user.

This is exactly why this MCP is designed for. As mentioned at the very start, I created a similar utility about an year and half ago and I blogged it here.

In that implementation I extensively used the Users - List Access Entities Fabric REST API's in a combination with Microsoft Graph API's.

If you want to know more about Microsoft Graph API's, I have a detailed article on that topic as well.

Delegated Permissions and Scopes

We need to assign the following Delegated permissions to the service principal we will be using.

We will require two scopes. One for Graph API and second for Fabric REST API's

So the authentication header will required two access tokens. One for Microsoft Graph and second for Microsoft Fabric.

Thank fully MCP SDK supports passing multiple tokens through a type Dictionary.

TokenCredentials

GraphServiceClient authentication requires TokenCredentials for authentication, so just using the access token will not be useful. For that we will have to convert bearer token to TokenCredentials.

I have an detailed article on how that can be implemented.

The example in the above article is specific for OneLake authentication but can be used for other implementations as well. OneLake authentication also requires the bearer access token to be converted into a TokenCredential before it can be used with the Azure SDK.

Along with that, we will use PublicApplicationBuilder from the MSAL(Microsoft Authentication Library) to create the bearer access token so that we don't have maintain client secret of the underlying service principal.

Set Up

We will create a Azure Function for MCP endpoints and a console application as the client. The client will send prompts to the MCP Azure functions through an AIAgent . The agent tools are exposed through the MCP C# SDK.

Lets first set up the Azure Function

The Azure function type is MCP Tool trigger as our Azure Function execution should be tool driven instead of prompt driven.

Add the following references to the Azure Function project.

dotnet add package Microsoft.Azure.Functions.Worker;
dotnet add package Microsoft.Azure.Functions.Worker.Extensions.Mcp
dotnet add package Microsoft.Extensions.Logging;
dotnet add package Microsoft.Graph;
dotnet add package Newtonsoft.Json.Linq;

The Fabric REST API output is in JSON format and is very dynamic.

So we will use Json.NET to traverse the response object.

To get started, declare a bunch of variables and define the constructor of class in the Azure function that is assigned to the _logger variable.

The fabric endpoint used is https://api.fabric.microsoft.com/v1

 private static Dictionary<string, string> itemlist = new();
 private static string endpoint = "https://api.fabric.microsoft.com/v1";
 private static readonly HttpClient client = new HttpClient();
 private static GraphServiceClient graph_Service_Client;
 private ILogger<Function1> _logger;

 public Function1(ILogger<Function1> logger)
 {
     _logger = logger;
 }

GetAsync >>

This function returns HTTP response received from the endpoint using an HttpClient.

  public async static Task<string> GetAsync(string url, string token)
  {
      client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
      HttpResponseMessage response = await client.GetAsync(url);
      try
      {
          response.EnsureSuccessStatusCode();
          return await response.Content.ReadAsStringAsync();
      }
      catch (HttpRequestException httpRequestException)
      {
          return null;
      }

  }

GetUserDetails >>

This function is used to parse the JSON response and extract the relevant data

 [Function(nameof(GetUserDetails))]
 public async Task <IDictionary<string,string>> GetUserDetails
(
   [McpToolTrigger(nameof(GetUserDetails), "Gets the user access details")] ToolInvocationContext context,
   [McpToolProperty(nameof(username), "The name of the user for whom access details are sought")] string ? username) {
   List <string> tokens = new();

   if (context.TryGetHttpTransport(out var authHeaders)) 
   {     tokens.Add(authHeaders.Headers["Authorization_graph"].Replace("Bearer ", ""));
     tokens.Add(authHeaders.Headers["Authorization_fabric"].Replace("Bearer ", ""));
   }

   AccessTokenCredential tokenCredential = new AccessTokenCredential(tokens[0]);
   graph_Service_Client = new GraphServiceClient(tokenCredential);

   Microsoft.Graph.Models.UserCollectionResponse result = await graph_Service_Client.Users.GetAsync((requestConfiguration) => requestConfiguration.QueryParameters.Top = 999);

   IDictionary < string, object > userdetails = new Dictionary < string, object > ();

   int i = 0;

   if (username == "All") 
{
     foreach(var str in result.Value) {
       userdetails.Add(result.Value[i].DisplayName.ToString(), result.Value[i]);
       i++;
     }
   } 
else 
{
     foreach(var str in result.Value.Select(i => i.DisplayName == username)) {
       userdetails.Add(result.Value[i].DisplayName.ToString(), result.Value[i]);
       i++;
     }
   }

   i = 0;
   itemlist.Clear();

   foreach(var user in userdetails) 
 {
     string userId = ((Microsoft.Graph.Models.Entity) userdetails.Where(a => a.Key == user.Key).ToList()[0].Value).Id.ToString();
     string response = await GetAsync(endpoint + "/admin/Users/" + userId + "/access", tokens[1]);

     if (response == null) 
     {
       continue;
     }
     JObject j_response = JObject.Parse(response);
     JArray j_array = (JArray) j_response["accessEntities"];
     foreach(JObject path in j_array) {

       JToken itemid = path["id"];
       JToken item = path["itemAccessDetails"]["type"];
       JToken name = path["displayName"];

       JToken permissions = path["itemAccessDetails"]["permissions"];
       JArray permissions_arr = (JArray) permissions;
       string permission = "";
       foreach(JToken p in permissions_arr) 
       {
         permission = permission + ("/" + p.ToString());
         p.ToString();
       }

       JToken additionalPermissions = path["itemAccessDetails"]["additionalPermissions"];
       if (additionalPermissions != null) 
       {
         JArray permissions_ad_arr = (JArray) additionalPermissions;
         string permission_a = "";
         foreach(JToken p in permissions_ad_arr) {
           permission_a = permission_a + ("/" + p.ToString());
           p.ToString();
         }
         itemlist.Add(i + "`" + item, "Item Type" + "~" + item + "~" + "Item Name" + "~" + name + "Permissions" + "~" + permissions + "~" + "Additional Permissions" + "~" + permission_a);
       } 
     else 
       {
         itemlist.Add(i + "`" + item, "Item Type" + "~" + item + "~" + "Item Name" + "~" + name + "Permissions" + "~" + permissions + "~" + "Additional Permissions" + "~" + "N/A");

       }
       i++;
     }
     return itemlist;
   }
   return null;
 }

Lets look at the important aspects of the above function.

We have McpToolTrigger of type GetUserDetails that accepts username as McpToolProperty. It is required for MCP tool to process a prompt, something like the below

Give me list of items user ABC has access to.

Then we have the ToolInvocationContext that exposes the underlying tools, HTTP headers and arguments of the tool call.

With ToolInvocationContext, it becomes quite easy to read the HTTP headers invoked by the client. Recall that earlier I mentioned that we will require two bearer tokens, one for Fabric REST API and second for Graph API.

List <string> tokens = new();

if (context.TryGetHttpTransport(out var authHeaders)) 
{
tokens.Add(authHeaders.Headers["Authorization_graph"].Replace("Bearer ", ""));
  tokens.Add(authHeaders.Headers["Authorization_fabric"].Replace("Bearer ", ""));
}

Also earlier I mentioned that Graph API requires bearer token to be converted to TokenCredentials. The following line of code does that.

AccessTokenCredential tokenCredential = new AccessTokenCredential(tokens[0]);
graph_Service_Client = new GraphServiceClient(tokenCredential);

tokens[0] above is the token for Graph API and AccessTokenCredential is the custom class that does the conversion. I will post the code for that class later in the article.

GraphServiceClient gets a list of users and add it to UserCollectionResponse object of the Graph Model object.

Microsoft.Graph.Models.UserCollectionResponse result = await graph_Service_Client.Users.GetAsync((requestConfiguration) => requestConfiguration.QueryParameters.Top = 999);

We then add the usernames to a dictionary object called userdetails .

IDictionary<string,object> userdetails = new Dictionary<string, object>();
int i = 0;

if (username == "All")
{
    foreach (var str in result.Value)
    {
        userdetails.Add(result.Value[i].DisplayName.ToString(), result.Value[i]);
        i++;
    }
}
else
{
    foreach (var str in result.Value.Select(i => i.DisplayName == username))
    {
        userdetails.Add(result.Value[i].DisplayName.ToString(), result.Value[i]);
        i++;
    }
}

and then filter the dictionary based on filters passed through the prompt.

If the prompt contains the keyword "All" we save all the usernames to userdetails dictionary and if not, then just store the filtered username to the userdetails dictionary.

The code below calls the Users - List Access Entities API for each user object in the userdetails dictionary and the response is traversed with JSON.Net library.

foreach(var user in userdetails) 
{
    string userId = ((Microsoft.Graph.Models.Entity) userdetails.Where(a => a.Key == user.Key).ToList()[0].Value).Id.ToString();
    string response = await GetAsync(endpoint + "/admin/Users/" + userId + "/access", tokens[1]);
    if (response == null) 
    {
      continue;
    }
    JObject j_response = JObject.Parse(response);
    JArray j_array = (JArray) j_response["accessEntities"];
    foreach(JObject path in j_array) 
   {

      JToken itemid = path["id"];
      JToken item = path["itemAccessDetails"]["type"];
      JToken name = path["displayName"];

      JToken permissions = path["itemAccessDetails"]["permissions"];
      JArray permissions_arr = (JArray) permissions;
      string permission = "";
      foreach(JToken p in permissions_arr) 
      {
        permission = permission + ("/" + p.ToString());
        p.ToString();
      }

      JToken additionalPermissions = path["itemAccessDetails"]["additionalPermissions"];
      if (additionalPermissions != null) {
        JArray permissions_ad_arr = (JArray) additionalPermissions;
        string permission_a = "";
        foreach(JToken p in permissions_ad_arr) 
        {
          permission_a = permission_a + ("/" + p.ToString());
          p.ToString();
        }
        itemlist.Add(i + "`" + item, "Item Type" + "~" + item + "~" + "Item Name" + "~" + name + "Permissions" + "~" + permissions + "~" + "Additional Permissions" + "~" + permission_a);
      } 
     else 
     {
        itemlist.Add(i + "`" + item, "Item Type" + "~" + item + "~" + "Item Name" + "~" + name + "Permissions" + "~" + permissions + "~" + "Additional Permissions" + "~" + "N/A");
      }
      i++;
    }
return itemlist;

API call is : https://api.fabric.microsoft.com/v1/admin/users/{userId}/access that fetches the userId from the Graph API.

API Response >>

The access details are finally stored in a dictionary called itemlist with "~" used as the separator.

itemlist Dictionary >>

the user access details are stored in the dictionary object in the following format which is sent back the agent for the display.

GetUserDetailsForItemType >>

This function is similar to the GetUserDetails function with the only difference being that it is used to get access details for a specific item type.

[Function(nameof(GetUserDetailsForItemType))]
public async Task <IDictionary <string,string>> GetUserDetailsForItemType
(
  [McpToolTrigger(nameof(GetUserDetailsForItemType), "Gets the user access detail for a given item type")] ToolInvocationContext context,
  [McpToolProperty(nameof(username), "The name of the user for whom access details are sought")] string ? username,
  [McpToolProperty(nameof(itemtype), "The name of the item type for whom access details are sought")] string ? itemtype) {

  List <string> tokens = new();

  if (context.TryGetHttpTransport(out var authHeaders)) 
{
    tokens.Add(authHeaders.Headers["Authorization_graph"].Replace("Bearer ", ""));
    tokens.Add(authHeaders.Headers["Authorization_fabric"].Replace("Bearer ", ""));

  }
  AccessTokenCredential tokenCredential = new AccessTokenCredential(tokens[0]);
  graph_Service_Client = new GraphServiceClient(tokenCredential);

  Microsoft.Graph.Models.UserCollectionResponse result =
  await graph_Service_Client.Users.GetAsync((requestConfiguration) => requestConfiguration.QueryParameters.Top = 999);

  IDictionary <string,object> userdetails = new Dictionary< string,object> ();
  int i = 0;
  if (username == "All") 
   {
    foreach(var str in result.Value) 
   {
      userdetails.Add(result.Value[i].DisplayName.ToString(), result.Value[i]);
      i++;
    }
  } 
  else 
   {
    foreach(var str in result.Value.Select(i => i.DisplayName == username)) 
    {
      userdetails.Add(result.Value[i].DisplayName.ToString(), result.Value[i]);
      i++;
    }
  }
  itemlist.Clear();

  foreach(var user in userdetails) 
  {
    string userId = ((Microsoft.Graph.Models.Entity) userdetails.Where(a => a.Key == user.Key).ToList()[0].Value).Id.ToString();
    string response = await GetAsync(endpoint + "/admin/Users/" + userId + "/access?type=" + itemtype, tokens[1]);

    if (response == null) 
    {
      continue;
    }

    JObject j_response = JObject.Parse(response);

    JArray j_array = (JArray) j_response["accessEntities"];
    foreach(JObject path in j_array) {

      JToken itemid = path["id"];
      JToken item = path["itemAccessDetails"]["type"];
      JToken name = path["displayName"];

      JToken permissions = path["itemAccessDetails"]["permissions"];
      JArray permissions_arr = (JArray) permissions;
      string permission = "";
      foreach(JToken p in permissions_arr) 
      {
        permission = permission + ("/" + p.ToString());
        p.ToString();
      }

      JToken additionalPermissions = path["itemAccessDetails"]["additionalPermissions"];

      if (additionalPermissions.HasValues == true) 
      {
        JArray permissions_ad_arr = (JArray) additionalPermissions;
        string permission_a = "";
        foreach(JToken p in permissions_ad_arr) 
        {
          permission_a = permission_a + ("/" + p.ToString());
          p.ToString();
        }
        itemlist.Add(i + "`" + item, "Item Type" + "~" + item + "~" + "Item Name" + "~" + name + "Permissions" + "~" + permissions + "~" + "Additional Permissions" + "~" + permission_a);
      } 
     else 
      {
        itemlist.Add(i + "`" + item, "Item Type" + "~" + item + "~" + "Item Name" + "~" + name + "Permissions" + "~" + permissions + "~" + "Additional Permissions" + "~" + "N/A");
      }

      i++;

    }
    return itemlist;

  }
  return null;
}

Unlike GetUserDetails that accepts only username as McpToolProperty, the function GetUserDetailsForItemType has two McpToolProperty: username and itemtype.

This is required for MCP tool to process a prompt something like the below

Get access details for user ABC for item type Notebooks

The API call is now made to the following endpoint :

https://api.fabric.microsoft.com/v1/admin/users/{userId}/access/access?type={itemtype}

Client Console Application >>

This console application is used to call the MCP endpoint. You can use any other client tool like VSCode Copilot or through a Minimal API incase you want to expose the output through API's

using Azure;
using Azure.AI.OpenAI;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Identity.Client;
using ModelContextProtocol.Client;

public static class Program 
{
  private static string RedirectURI = "http://localhost";
  private static string clientId = "Service Principal Client Id";
  private static string tenantId = "Tenant Id";
  private static readonly HttpClient client = new HttpClient();
  private static string[] scopes_g = new string[] {
    "https://graph.microsoft.com/.default"};
  private static string[] scopes_f = new string[] {
    "https://api.fabric.microsoft.com/.default"};
  private static string Authority = $"https://login.microsoftonline.com/{tenantId}";
  public static HttpClient Client => client;

  private static async Task Main() 
{

    ServiceCollection servicecollection = new();

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

    var credential = new AzureKeyCredential(configuration["AppSettings:ApiKey"]);

    servicecollection.AddKeyedChatClient("ChatClient", 
   (sp => new AzureOpenAIClient(new Uri(configuration["AppSettings:EndPoint"]), credential)      .GetChatClient(configuration["AppSettings:Chat_DeploymentName"])
      .AsIChatClient()));

    servicecollection.AddSingleton <ChatClientAgent> (sp => 
   {
      return new ChatClientAgent(sp.GetKeyedService <IChatClient > ("ChatClient"));
    });

    ServiceProvider serviceprovider = servicecollection.BuildServiceProvider();

    var chatlient = serviceprovider.GetServices < ChatClientAgent > ();
    List < ChatClientAgent > lstchatclient = new(chatlient);

    var options = new ChatClientAgentOptions() {

      ChatOptions = new ChatOptions() {
          Instructions = "You are a Fabric Agent and you execute the appropriate tools. " +
            "You will display the results in table format with columns Item Type, Item Name, Access Details. Please singularize the itemtype if use prompt contains them." +
            "For example, if the prompt contains reports then singularize to report and if Notebooks then to Notebook and so and so forth",
            ToolMode = AutoChatToolMode.Auto,
            Tools = [..await ConnectMCP()]
        },
        Name = "Fabric Agent"
    };

    var agent = lstchatclient[0].ChatClient.AsAIAgent(options);

    AgentResponse agentresponse = await agent.RunAsync("Get access details for user ABC that he has access to");

    Console.Write(agentresponse.Text);

  }

  public async static Task <AuthenticationResult> ReturnAuthenticationResult(string[] scopes) {
    string AccessToken;
    PublicClientApplicationBuilder PublicClientAppBuilder =
      PublicClientApplicationBuilder.Create(clientId)
      .WithAuthority(Authority)
      .WithCacheOptions(CacheOptions.EnableSharedCacheOptions)
      .WithRedirectUri(RedirectURI);

    IPublicClientApplication PublicClientApplication = PublicClientAppBuilder.Build();
    var accounts = await PublicClientApplication.GetAccountsAsync();
    AuthenticationResult result;
    try 
    {
      result = await PublicClientApplication.AcquireTokenSilent(scopes, accounts.First())
        .ExecuteAsync()
        .ConfigureAwait(false);
    } 
  catch 
   {
      result = await PublicClientApplication.AcquireTokenInteractive(scopes)
        .ExecuteAsync()
        .ConfigureAwait(false);
    }
    return result;
  }

  public static async Task <List<McpClientTool>> ConnectMCP() 
{
    AuthenticationResult result_graph = await ReturnAuthenticationResult(scopes_g);
    AuthenticationResult result_fabric = await ReturnAuthenticationResult(scopes_f);

    var httpClient = new HttpClient 
    {
      Timeout = Timeout.InfiniteTimeSpan
    };
    var transport = new ModelContextProtocol.Client.HttpClientTransport(
      new HttpClientTransportOptions {
        Endpoint = new Uri("http://{Azure Function URL }/runtime/webhooks/mcp"),
          Name = "Custom Fabric MCP",
          TransportMode = HttpTransportMode.AutoDetect,
          EnableStandaloneGetStream = false,
          AdditionalHeaders = new Dictionary < string, string > {
            {
              "Authorization_graph",
              $ "Bearer {result_graph.AccessToken}"
            },
            {
              "Authorization_fabric",
              $ "Bearer {result_fabric.AccessToken}"
            }
          }
      },
      httpClient
    );

    var mcpClient = await McpClient.CreateAsync(transport);
    var tools = await mcpClient.ListToolsAsync();
    return tools.ToList();
  }
}

The above code is similar to the one used in my earlier article on Fabric MCP. Only difference being that I am passing two tokens through the header

AdditionalHeaders = new Dictionary <string,string> 
         {
            {
              "Authorization_graph",
              $ "Bearer {result_graph.AccessToken}"
            },
            {
              "Authorization_fabric",
              $ "Bearer {result_fabric.AccessToken}"
            }
          }

which is used to call GraphServiceClient & Users - List Access Entities API .

Also notice the agent instructions

You are a Fabric Agent and you execute the appropriate tools.
You will display the results in table format with columns Item Type, Item Name, Access Details. Please singularize the itemtype if use prompt contains them. For example, if the prompt contains reports then singularize to report and if Notebooks then to Notebook and so and so forth.

AccessTokenCredential.cs >>

This class converts bearer token generated by ReturnAuthenticationResult in the client code above to TokenCredentials for reasons explained earlier

using Azure.Core;
using System.IdentityModel.Tokens.Jwt;

namespace AccesTokenCredentials
{
    public class AccessTokenCredential : Azure.Identity.ClientSecretCredential
    {
        public AccessTokenCredential(string accessToken)
        {
            AccessToken = accessToken;
        }

        private string AccessToken;

        public AccessToken FetchAccessToken()
        {
            JwtSecurityToken token = new JwtSecurityToken(AccessToken);
            return new AccessToken(AccessToken, token.ValidTo);
        }

        public override ValueTask<AccessToken> GetTokenAsync(
            TokenRequestContext requestContext,
            CancellationToken cancellationToken
        )
        {
            return new ValueTask<AccessToken>(FetchAccessToken());
        }

        public override AccessToken GetToken(
            TokenRequestContext requestContext,
            CancellationToken cancellationToken
        )
        {
            JwtSecurityToken token = new JwtSecurityToken(AccessToken);
            return new AccessToken(AccessToken, token.ValidTo);
        }
    }
}

Limitations >>

The output does not lists the workspace under which a particular item exists and also it does not lists access details of a service principal. This can be a major shortcoming but I have a solution for that as well :) which I will post in my upcoming blog.

Execution

Conclusion

The intention of this article is not to make a production level agent but to provide a starting point incase you do decide to create a custom agent that interacts with the Fabric services.

The possibilities and quite endless and when coupled with Microsoft Agent Framework(MAF) you can leverage the fantastic features and properties of MAF that could interact with Fabric services and ecosystem.

Thanks for reading !!!

More from this blog