MCP Tools

June 1, 2026 · View on GitHub

This project is a TypeScript Azure Function app that exposes multiple MCP (Model Context Protocol) tools as a remote MCP server. It includes a hello world tool, snippet save/get backed by Azure Blob Storage bindings, and tools demonstrating structured content responses (images, QR codes, badges, resource links, structured data).

Note: MCP prompts are in the mcp-prompts project, and the MCP App weather sample is in mcp-weather-app.

Tools included

ToolFileDescription
hellosrc/functions/helloMcpTool.tsSimple hello world tool
helloMcpWithAuthsrc/functions/helloMcpWithAuth.tsGreets the signed-in user by name via Microsoft Graph (OBO flow)
savesnippetsrc/functions/snippetsMcpTool.tsSaves a code snippet to blob storage
getsnippetssrc/functions/snippetsMcpTool.tsRetrieves a snippet from blob storage
getImageWithTextsrc/functions/structured-content/imageContentSample.tsReturns multi-block content (text + image)
getImageWithMetadatasrc/functions/structured-content/imageContentSample.tsReturns McpToolResponse with structuredContent
generateQrCodesrc/functions/structured-content/imageContentSample.tsGenerates a QR code PNG from text
generateBadgesrc/functions/structured-content/imageContentSample.tsGenerates an SVG status badge
resourceLinksrc/functions/structured-content/resourceLinkSample.tsReturns a resource link content block
getCodeSnippetsrc/functions/structured-content/snippetDataSample.tsRetrieves a snippet with structured metadata

Prerequisites

Prepare your local environment

Start Azurite, the Azure Storage emulator required by the Functions host:

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 from the command palette.

Run locally

From this directory (mcp-tools/), start the Functions host:

func start

The MCP endpoint will be available at http://localhost:7071/runtime/webhooks/mcp.

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.ts
    
  3. When prompted to run a tool, consent by clicking Continue.

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 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.

Verify local blob storage

After testing snippet save, you can verify blobs were stored in Azurite:

az storage blob list \
    --container-name snippets \
    --connection-string "DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;"

Or open Azure Storage ExplorerEmulator & AttachedBlob Containerssnippets.

Deploy to Azure

Step 1: Sign in

az login
azd auth login

Step 2: Create an environment

azd env new <environment-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.

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 helloMcpWithAuth 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-mcp-function. 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

Blob bindings for snippet storage

This sample uses Azure Functions blob bindings. No Azure SDK code needed:

const blobInputBinding = input.storageBlob({
  connection: "AzureWebJobsStorage",
  path: `snippets/{mcptoolargs.snippetname}.json`,
});

app.mcpTool("getSnippet", {
  toolName: "getsnippets",
  description: "Gets code snippets from your snippet collection.",
  toolProperties: {
    snippetname: arg.string().describe("The name of the snippet.")
  },
  extraInputs: [blobInputBinding],
  handler: getSnippet,
});

On-Behalf-Of (OBO) auth with helloMcpWithAuth

The helloMcpWithAuth tool (in src/functions/helloMcpWithAuth.ts) 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, azd, etc.):

if (isDevelopment) {
    credential = new ChainedTokenCredential(
        new AzureCliCredential(),
        new AzureDeveloperCliCredential()
    );
} else {
    credential = buildOnBehalfOfCredential(context);
}

In production, the buildOnBehalfOfCredential function 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
const managedIdentity = new ManagedIdentityCredential(federatedMiClientId);

const getAssertion = async (): Promise<string> => {
    const result = await managedIdentity.getToken(
        `${tokenExchangeAudience}/.default`
    );
    return result.token;
};

return new OnBehalfOfCredential({
    tenantId,
    clientId,
    getAssertion,
    userAssertionToken: userToken,
});

The resulting credential is then used to call Microsoft Graph /me and greet the user by name:

const tokenResponse = await credential.getToken(["https://graph.microsoft.com/.default"]);
const response = await fetch("https://graph.microsoft.com/v1.0/me", {
    headers: { Authorization: `Bearer ${tokenResponse.token}` },
});
const me = await response.json();
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.

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)
Snippet save/get returns errorsVerify Azurite is reachable and local.settings.json has "AzureWebJobsStorage": "UseDevelopmentStorage=true"
helloMcpWithAuth fails locallyEnsure you're signed in with az login or the VS Code Azure account extension
OBO errors in deployed serverVerify that consent has been granted (see Step 4) and that the Entra app registration is configured correctly
An error occurred invoking 'helloMcpWithAuth' right after azd upRestart the function app: az functionapp restart -g <resource-group> -n <function-app-name>. The OBO flow signs a client assertion with the user-assigned managed identity via a federated identity credential (FIC). 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 | where timestamp > ago(1h) | project timestamp, outerMessage, innermostMessage to find the actual error.
403 error or "Cannot force new registration for a non-dynamic authentication provider"Ensure host.json has extensions.mcp.system.webhookAuthorizationLevel set to "Anonymous". Without this, EasyAuth validates the token but the Functions host still requires a system key — resulting in 403. Also verify that VS Code's client ID (aebc6443-996d-45c2-90f0-388ff96faa56) is in the allowedApplications list in EasyAuth config.

Next Steps