FunctionsMcpTool

May 7, 2026 · View on GitHub

This project is a .NET 10 Azure Function app that exposes multiple MCP (Model Context Protocol) tools as a remote MCP server. It includes tools for snippets (including a search_snippets sample that declares explicit input/output JSON schemas), QR code generation, badges, echo, hello, and a hello with auth tool that demonstrates the On-Behalf-Of (OBO) flow to call Microsoft Graph as the signed-in user.

Note: MCP prompts and resource templates are in separate projects — see FunctionsMcpPrompts and FunctionsMcpResources.

Prerequisites

Prepare your local environment

An Azure Storage Emulator is needed because the snippet tools save and retrieve blobs from storage. Start Azurite:

docker run -d -p 10000:10000 -p 10001:10001 -p 10002:10002 \
    mcr.microsoft.com/azure-storage/azurite

If you use the Azurite VS Code extension instead, run Azurite: Start now.

Run locally

From this directory (src/FunctionsMcpTool), start the Functions host:

func start

Connect to the MCP server

Option A: VS Code with GitHub Copilot

  1. Open .vscode/mcp.json in the workspace root. Find the server called local-mcp-function and click Start above the name. It points to:

    http://localhost:7071/runtime/webhooks/mcp
    
  2. In Copilot chat agent mode, try prompts like:

    Say Hello
    
    Save this snippet as snippet1
    
    Retrieve snippet1 and apply to NewFile.cs
    
  3. When prompted to run a tool, consent by clicking Continue.

  4. Press Ctrl+C in the terminal to stop the function host when done.

Option B: MCP Inspector

  1. In a new terminal, install and run MCP Inspector:

    npx @modelcontextprotocol/inspector
    
  2. Open the Inspector URL (e.g. http://0.0.0.0:5173/#resources).

  3. Set the transport type to Streamable HTTP.

  4. Set the URL to http://0.0.0.0:7071/runtime/webhooks/mcp and click Connect.

  5. Click List Tools, select a tool, and Run Tool.

Deploy to Azure

Step 1: Sign in

az login
azd auth login

Step 2: Create an environment

azd env new <environment-name>

This also becomes the resource group name.

Step 3: Provision and deploy

This project requires OAuth-based authentication through the built-in MCP auth feature with Microsoft Entra as the identity provider, and it is enabled by default. Do not disable authentication for this project.

Configure VS Code as an allowed client application for Microsoft Entra:

azd env set PRE_AUTHORIZED_CLIENT_IDS aebc6443-996d-45c2-90f0-388ff96faa56

Optionally enable VNet isolation:

azd env set VNET_ENABLED true

Deploy the project. When prompted, pick your subscription and an Azure region.

azd up

The HelloToolWithAuth tool requires consent for delegated permission to access Microsoft Graph. For testing, you can grant consent just for yourself by logging into the application in a browser. See Consent authoring for how you would handle this for production scenarios.

Navigate to the /.auth/login/aad endpoint of your deployed function app. For example, if your function app is at https://my-mcp-function-app.azurewebsites.net, navigate to:

https://my-mcp-function-app.azurewebsites.net/.auth/login/aad

Sign in with your Azure subscription email and accept the permissions prompt. This completes the consent flow for you.

Step 5: Connect to the remote MCP server

Open .vscode/mcp.json and click Start above remote-functions-mcp-tool. You'll be prompted for functionapp-name (find it in your azd command output or the /.azure/<env>/.env file). You'll also be prompted to authenticate with Microsoft—click Allow and sign in.

Tip: A successful connection shows the number of tools the server exposes. Click More... → Show Output above the server name to see request/response details.

If you run into issues, see the Troubleshooting section below.

Redeploy and clean up

  • Redeploy: azd deploy
  • Clean up all resources: azd down

Examining the code

MCP tool basics

Each tool is a C# method with a [Function] attribute and an [McpToolTrigger] binding that exposes it as an MCP tool. For example, the snippet tools in SnippetsTool.cs use [BlobInput] and [BlobOutput] bindings to read/write Azure Blob Storage directly:

[Function(nameof(GetSnippet))]
public Snippet? GetSnippet(
    [McpToolTrigger(GetSnippetToolName, GetSnippetToolDescription)]
        ToolInvocationContext context,
    [McpToolProperty(SnippetNamePropertyName, SnippetNamePropertyDescription, true)]
        string name,
    [BlobInput(BlobPath)] string? snippetContent)
{
    // ...
}

[Function(nameof(SaveSnippet))]
[BlobOutput(BlobPath)]
public string SaveSnippet(
    [McpToolTrigger(SaveSnippetToolName, SaveSnippetToolDescription)]
        Snippet snippet,
    // ...
)
{
    // ...
}

Calling Microsoft Graph with the On-Behalf-Of flow (HelloToolWithAuth)

The HelloToolWithAuth tool demonstrates how to call a downstream API (Microsoft Graph) as the signed-in user using the On-Behalf-Of (OBO) flow.

Local development falls back to your local developer identity (Azure CLI, VS Code, etc.):

if (hostEnvironment.IsDevelopment())
{
    credential = new ChainedTokenCredential(
        new AzureCliCredential(),
        new VisualStudioCodeCredential(),
        new VisualStudioCredential(),
        new AzureDeveloperCliCredential());
}
else
{
    credential = BuildOnBehalfOfCredential(context);
}

In production, the BuildOnBehalfOfCredential method exchanges the user's auth token for a Microsoft Graph token using three pieces of information:

  1. The user's bearer token — extracted from the X-MS-TOKEN-AAD-ACCESS-TOKEN header (or Authorization fallback)
  2. The user's tenant ID — decoded from the X-MS-CLIENT-PRINCIPAL header
  3. A client assertion — obtained from a managed identity with a federated credential, proving the app's identity without a client secret
private static TokenCredential BuildOnBehalfOfCredential(ToolInvocationContext context)
{
    if (!context.TryGetHttpTransport(out var transport))
        throw new InvalidOperationException(
            "No HTTP transport available. Is built-in auth (App Service Authentication) enabled?");

    var userToken = GetUserToken(transport!);
    var tenantId = GetTenantId(transport!);
    var clientAssertionCallback = BuildClientAssertionCallback();

    string clientId = Environment.GetEnvironmentVariable("WEBSITE_AUTH_CLIENT_ID")
        ?? throw new InvalidOperationException("WEBSITE_AUTH_CLIENT_ID is not set.");

    return new OnBehalfOfCredential(tenantId, clientId, clientAssertionCallback, userToken);
}

The resulting credential is then used to create a GraphServiceClient and call Me.GetAsync() to greet the user by name:

using var graphClient = new GraphServiceClient(credential, GraphScopes);
var me = await graphClient.Me.GetAsync().ConfigureAwait(false);
return $"Hello, {me?.DisplayName} ({me?.Mail})!";

In the steps described for this example, you consented to the application by signing into it in a browser. This allowed the application to request delegated permissions to the Microsoft Graph. There are two main ways that consent can be handled:

  • User consent — This is the approach used in the example above. Each user signs into the application and consents to the permissions requested. They can only do this for themselves, unless they are a tenant administrator with the ability to consent on behalf of others. In this sample, user consent is appropriate because it allows you to quickly test things without impacting other users. However, the way user consent is authored in this sample does not reflect how you would typically do it in a production scenario. This is described in more detail below.

  • Admin consent — A tenant administrator can consent to the application on behalf of all users when they sign in and review the permissions. Once this is done, individual users can sign in without needing to consent themselves. This approach is more scalable and ensures that all users can access the application without running into consent issues. For the purposes of a sample, admin consent is not appropriate, but it is a great choice for production scenarios.

The user consent approach for this sample is a separate login because the sample uses Visual Studio Code as the client. Although Visual Studio Code is pre-authorized to our application, that only creates consent for the user to call the MCP server. It doesn't create consent for the MCP server to call the Microsoft Graph on behalf of the user. When we log into the application directly, we request Microsoft Graph permissions as part of a combined consent experience.

The main difference is that because Visual Studio Code is using a single sign-on flow, it only requests a token for the MCP server. It does not present an opportunity for the user to interactively consent to any permissions needed for or by the MCP server. If you built a client that used an interactive login of some kind, you could have it all handled entirely by that client. It would not be necessary to have a separate browser login.

See Overview of permissions and consent in the Microsoft identity platform for additional information on how Entra ID handles consent.

Tools included

ToolDescription
hello_toolSimple hello world tool
hello_tool_with_authGreets the signed-in user by name via Microsoft Graph (OBO flow)
echo_messageEchoes back a provided message (properties defined in Program.cs)
save_snippetSaves a code snippet to blob storage
get_snippetRetrieves a code snippet from blob storage
get_snippet_with_metadataRetrieves a snippet with structured metadata
batch_save_snippetsSaves multiple snippets at once
generate_qr_codeGenerates a QR code image from text
generate_badgeGenerates an SVG status badge
get_website_previewFetches a website preview

Troubleshooting

ProblemSolution
Connection refused locallyEnsure Azurite is running (docker run -p 10000:10000 -p 10001:10001 -p 10002:10002 mcr.microsoft.com/azure-storage/azurite)
API version not supported by AzuritePull the latest image (docker pull mcr.microsoft.com/azure-storage/azurite) and restart
hello_tool_with_auth fails locallyEnsure you're signed in with az login or the VS Code Azure account extension
OBO errors in productionVerify that consent has been granted (see Step 4) and that the Entra app registration is configured correctly
An error occurred invoking 'hello_tool_with_auth' right after azd upRestart the function app to refresh it: az functionapp restart -g <resource-group> -n <function-app-name>. The hello_tool_with_auth tool uses the On-Behalf-Of (OBO) flow, which signs a client assertion with the user-assigned managed identity via a federated identity credential (FIC) on the app registration. Right after provisioning, the auth runtime can hold a stale signing credential while the FIC propagates in Entra. Check Application Insights > Logs for AADSTS50013: Assertion failed signature validation to confirm.
Generic "An error occurred invoking" with no detailsCheck Application Insights > Logs and query `exceptions
azd up provision succeeded but deploy immediately failed: unable to find a resource tagged with 'azd-service-name: mcp'The tag was provisioned but not propagated yet when azd deploy looked it up — run azd deploy again
azd deploy fails with Kudu restart error: deployment was partially successful: [KuduSpecializer] Kudu has been restarted after package deployedTransient error — run azd deploy again

Next Steps