# Integrating GitHub with Microsoft Agent Framework with OAuth Authentication and Octokit

My blogs in the past were focused on Microsoft Agent Framework(MAF) agent integration with Microsoft services like Microsoft Azure, Microsoft Fabric and other M365 service like Microsoft Purview. The approach in them were pretty straightforward as all these services lie within the Microsoft ecosystem and the permissions can be easily managed through delegated permissions in Microsoft Entra.

But the challenge comes in when you have a service that sits outside of Microsoft ecosystem for example **GitHub**.

The authentication and authorization model is fundamentally different from Microsoft services and it becomes even more challenging when the MAF agent needs to access this service that has their own identity and authorization model.

For instance, you can generally establish a Microsoft Entra identity context and use Microsoft-native authorization mechanisms when accessing Microsoft services as the identity and permissions flow through the Microsoft security model. But for external service like GitHub the permission model does not flow through Microsoft authorization model and has its own security limitation.

In this article we will look into the approach on how to overcome this limitation. We wont use any third party authorization providers and will try to leverage native techniques that can be available at our disposal.

So, the intended flow is straightforward : a user sends a GitHub request through an MAF agent to create an issue in a repository. The agent should perform the operation only if the user has the required permissions on that repository. If the user does not have the necessary permissions the operation should fail and the issue should not be created.

We will use **Azure Functions for MCP** for MCP endpoint and [Octokit](https://github.com/octokit) to perform GitHub operations.

> The biggest challenge is that, unlike MSAL which provides a redirect flow for authentication in windows, GitHub requires the application to manage the OAuth flow itself. But it does provide redirects to the callback URL with a temporary authorization code and through this code we can derive the GitHub access token .

> We will use HttpListener to act as a temporary local callback server to call URL once GitHub authenticates the user credentials.

Our MCP endpoint will be an Azure Function and the client will be a console application that calls the MCP endpoint .

We will implement an OAuth based authentication model for GitHub. Think of it as a delegated authority where the application acts on behalf of the user. The OAuth will return an access token for the logged in user.

A token for the logged in user and GitHub uses the delegated authorization in combination with the user's existing permissions.

### GitHub Settings

Repo **GithubActions** has two users : **Sachin- Nand** and **SachinNandanwar**

**Sachin-Nand** has write access while **SachinNandanwar** only has read access.

![](https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/a7ec0f20-fe4a-4292-ba4e-d9c1718cf989.png align="center")

We first have to set OAuth Apps to enable GitHub OAuth flow.

Under **Settings** >> **Profile** >> **Developer settings**

![](https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/91ef713c-5855-477b-a867-9976e104e287.png align="center")

Click on **OAuth Apps** and **New OAuth apps**

![](https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/7d32502f-d416-46a2-9795-504eb69d51d3.png align="center")

Enter App name and create a new client secret.

Note down the Client Id and Client Secret

![](https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/e87b4758-f0cf-4a78-926a-1a9dfecbf68b.png align="center")

The app created above is named **GHub API.**

Scroll down to the bottom of the page and for Redirect URI enter value

**http://127.0.0.1:50505/callback.** This is our loopback url.

![](https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/05f18c1a-66c9-4ee6-be9f-d3c79e25d98d.png align="center")

You can use any port of your choice.

### Azure Function

Create a new **Azure Function for MCP** project and add the following packages

```csharp
dotnet add package Microsoft.Azure.Functions.Worker.Extensions.Mcp
dotnet add package Microsoft.Extensions.Logging
dotnet add package Octokit
```

Create a MCP function that accepts the repo name and the owner name.

```csharp
[Function(nameof(GetGitHubRepository))]
public async Task<string> GetGitHubRepository(
  [McpToolTrigger("get_github_repository", "Gets information about a GitHub repository.")] ToolInvocationContext context,
  [McpToolProperty("owner", "GitHub repository owner.", isRequired: true)] string owner,
  [McpToolProperty("repository", "GitHub repository name.", isRequired: true)] string repository)
{
    var client = new GitHubClient(new ProductHeaderValue("MyGitHubApp"));
  
    if (context.TryGetHttpTransport(out var authHeaders))
    {
        var authenticatedClient = new GitHubClient(new ProductHeaderValue("MyGitHubApp"));

        authenticatedClient.Credentials = new Credentials(authHeaders.Headers["Authorization"]);
        var newIssue = new NewIssue("Bug: App crashes on launch")
        {
            Body = "The application immediately crashes when on start up."
        };
        var issue = await authenticatedClient.Issue.Create(owner, repository, newIssue);
        return "Issue created !!!";
    }
    else
    {
        return "Error !!!";
    }
}
```

Nothing fancy or complicated. Just a simple function that accepts the user inputs and authenticates the calls through **ToolInvocationContext** by checking the Authorization value in the request header.

Once authenticated and permissions are validated, an GitHub issue is logged through the **GitHubClient** object of **Octokit**.

> Ideally the issue title and body should also come through the user input but for brevity I have mentioned it in the MCP function.

**MCP Client**

As mentioned earlier, the client will be a Console Application.

Add the following references

```csharp
dotnet add package Azure;
dotnet add package Azure.AI.OpenAI;
dotnet add package Microsoft.Agents.AI;
dotnet add package Microsoft.Extensions.AI;
dotnet add package ModelContextProtocol.Server;
dotnet add package System.Text.Json.Serialization;
dotnet add package Microsoft.Extensions.Configuration;
dotnet add package Microsoft.Extensions.DependencyInjection;
dotnet add package Microsoft.Extensions.Logging;
dotnet add package ModelContextProtocol.Client;
```

Add **appsetting.json** to the project

```csharp
"AppSettings": { 
    "Chat_DeploymentName": "Deployment Name",
    "EndPoint": "Azure OpenAI endpoint",
    "ApiKey": "Azure OpenAI API key"
}
```

### **Code**

Once all the artifacts in place, add the following code to **Program.cs**

**Program.cs >>**

**Read from the Config file**

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

**Declare HttpClient**

```csharp
private static readonly HttpClient client = new HttpClient();
public static HttpClient httpClient => client;
```

**Read credentials and register** `Chatclient`

```csharp
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()
    )
);
```

**Create a DI container**

```csharp
 ServiceCollection servicecollection = new ServiceCollection();
```

**Register ChatClientAgent in the DI container and build the Service Provider**

```csharp
servicecollection.AddSingleton<ChatClientAgent>();
ServiceProvider serviceprovider = servicecollection.BuildServiceProvider();
```

**Fetch keyed IChatclient from DI container**

```csharp
var chatclient = new ChatClientAgent(serviceprovider.GetKeyedService<IChatClient>("ChatClient")).ChatClient;
```

**Create ChatClientAgentOptions**

```csharp
  var options = new ChatClientAgentOptions()
  {
      ChatOptions = new ChatOptions()
      {
          Instructions = "You perform github operations",
          ToolMode = AutoChatToolMode.Auto,
          Tools = [.. await ConnectGitHubMCP()],
      },
      Name = "GitHub Agent"
  };
```

**ConnectGitHubMCP**

```csharp
 public static async Task<List<McpClientTool>> ConnectGitHubMCP()
 {
     string clientId = "XXXXXXXXXXXXXX";
     string redirectUri = "http://127.0.0.1:50505/callback";
     using var listener = new HttpListener();
     listener.Prefixes.Add("http://127.0.0.1:50505/");
     listener.Start();

     var authUrl =
         "https://github.com/login/oauth/authorize" +
         $"?client_id={Uri.EscapeDataString(clientId)}" +
         $"&redirect_uri={Uri.EscapeDataString(redirectUri)}";

     Process.Start(
     new ProcessStartInfo
     {
         FileName = authUrl,
         UseShellExecute = true
     });

     var context = await listener.GetContextAsync();

     var tokenRequest = new Dictionary<string, string>
     {
         ["client_id"] = clientId,
         ["client_secret"] = "XXXXXXXXXXXXXX",
         ["code"] = context.Request.QueryString["code"],
         ["redirect_uri"] = redirectUri
     };
    
     var content = new FormUrlEncodedContent(tokenRequest);
     var response = await httpClient.PostAsync(
         "https://github.com/login/oauth/access_token",
         content);
     var accesstoken = response.Content.ReadAsStringAsync().Result.Split("=")[1].Replace("&expires_in", "");

     const string html = """
         <!DOCTYPE html>
         <html>
         <head>
             <title>Authentication Complete</title>
         </head>
         <body>
             <p>Authentication successful. You can close this window.</p>
             <script>
                 window.close();
             </script>
         </body>
         </html>
         """;

     var buffer = Encoding.UTF8.GetBytes(html);
     context.Response.ContentType = "text/html";
     context.Response.ContentLength64 = buffer.Length;
     await context.Response.OutputStream.WriteAsync(buffer);
     context.Response.OutputStream.Close();

     listener.Stop();
     httpClient.Timeout = Timeout.InfiniteTimeSpan;

     var transport = new ModelContextProtocol.Client.HttpClientTransport(
         new HttpClientTransportOptions
         {
             Endpoint = new Uri("http://localhost:7287/runtime/webhooks/mcp"),
             Name = "Custom GitHub MCP",
             TransportMode = HttpTransportMode.AutoDetect,
             EnableStandaloneGetStream = false,
             AdditionalHeaders = new Dictionary<string, string>
             {
                 { "Authorization" , accesstoken }
             }
         },
             httpClient
        );

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

Lets look at the important aspects of the above code.

We first create and start a **HttpListener** with the **redirectUri** pointed to the GitHub callback URI that we set on the GitHub settings page of the account.

```csharp
 string clientId = "XXXXXXXXXXXXXX";
 string redirectUri = "http://127.0.0.1:50505/callback";
 using var listener = new HttpListener();
 listener.Prefixes.Add("http://127.0.0.1:50505/");
 listener.Start();
```

We then build a GitHub OAuth authorization URL with two parameters ,**client\_id** and **redirect\_uri.**

Client id is the GitHub Client Id of the OAuth App called **GHub API** that we had set earlier and the redirect uri is the callback url.

```csharp
var authUrl =
    "https://github.com/login/oauth/authorize" +
    $"?client_id={Uri.EscapeDataString(clientId)}" +
    $"&redirect_uri={Uri.EscapeDataString(redirectUri)}";
```

Start a process to trigger the opening of the GitHub authorization URL in the browser.

```csharp
Process.Start(
new ProcessStartInfo
{
    FileName = authUrl,
    UseShellExecute = true
});
```

Create a token request with the following default parameters, the most important one being **context.Request.QueryString\["code"\]** that extracts the authorization code that GitHub sends back in the callback URL. Its a temporary authorization code required only for token generation.

```csharp
var context = await listener.GetContextAsync();

var tokenRequest = new Dictionary<string, string>
{
    ["client_id"] = clientId,
    ["client_secret"] = clientSecret,
    ["code"] = context.Request.QueryString["code"],
    ["redirect_uri"] = redirectUri
};
```

Exchanging the code for our access token and then extract the **accesstoken** from the response.

```csharp
var content = new FormUrlEncodedContent(tokenRequest);

var response = await httpClient.PostAsync(
            "https://github.com/login/oauth/access_token",
            content);

var accesstoken = response.Content.ReadAsStringAsync().Result.Split("=")[1].Replace("&expires_in", "");
```

The part below is optional. The reason for this code is to notify the user that the authentication is successful and the user can then close the browser window.

Since GitHub does not support a native browser client redirects, I have injected my own custom HTML to the client

```csharp
 const string html = """
     <!DOCTYPE html>
     <html>
     <head>
         <title>Authentication Complete</title>
     </head>
     <body>
         <p>Authentication successful. You can close this window.</p>
         <script>
             window.close();
         </script>
     </body>
     </html>
     """;

 var buffer = Encoding.UTF8.GetBytes(html);
 context.Response.ContentType = "text/html";
 context.Response.ContentLength64 = buffer.Length;
 await context.Response.OutputStream.WriteAsync(buffer);
 context.Response.OutputStream.Close();
 listener.Stop();
```

Create a client function that connects to the MCP endpoints and returns the MCP Server tool list.

MCP endpoint used here is : [**http://localhost:7287/runtime/webhooks/mcp**](http://localhost:7287/runtime/webhooks/mcp)

```csharp
var transport = new ModelContextProtocol.Client.HttpClientTransport(
    new HttpClientTransportOptions
    {
        Endpoint = new Uri("http://localhost:7287/runtime/webhooks/mcp"),
        Name = "Custom GitHub MCP",
        TransportMode = HttpTransportMode.AutoDetect,
        EnableStandaloneGetStream = false,
        AdditionalHeaders = new Dictionary<string, string>
        {
            { "Authorization" , accesstoken }
        }
    }
  );

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

Send the following prompt to the agent

```yaml
Create an issue on the repo GitHubActions owned by Sachin-Nand
```

as user **Sachin-Nand** and the GitHub issue is created.

![](https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/5cb93826-c04d-4ec2-bab9-1983349d2a12.png align="center")

![](https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/56fa54e1-7299-4bf8-978d-f3c19392e43b.png align="center")

while doing so as user **SachinNandanwar** the creation fails.

![](https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/33f3c374-792a-4142-ba9c-a2cc99d23f3a.png align="center")

### **Execution**

![](https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/d2849dd0-28b4-4133-ad82-361c13f0ed61.gif align="center")

### Conclusion

With this approach the possibilities of leveraging MAF agents to external services that exist out of Microsoft ecosystem becomes significantly broader with out the need for any third party OAuth providers.

More importantly, this approach allows MAF agents to combine capabilities across multiple ecosystems without being restricted to Microsoft native services.

Thanks for reading !!!
