Skip to content

Azure-Samples/remote-mcp-functions-typescript

Repository files navigation

Getting Started with Remote MCP Servers using Azure Functions (Node.js/TypeScript)

This is a quickstart template to easily build and deploy a custom remote MCP server to the cloud using Azure functions. You can clone/restore/run on your local machine with debugging, and azd up to have it in the cloud in a couple minutes.

The MCP server is configured with built-in authentication using Microsoft Entra as the identity provider.

You can also use API Management to secure the server, as well as network isolation using VNET.

Watch the video overview

Watch the video

If you're looking for this sample in more languages check out the .NET/C# and Python versions.

Open in GitHub Codespaces

Below is the architecture diagram for the Remote MCP Server using Azure Functions:

Architecture Diagram

Repository layout

This repository now includes two independently deployable Function Apps:

  1. mcp-tools contains MCP Tool triggers, rich content samples, and snippet storage samples.
  2. mcp-weather-app contains the MCP App weather sample (resource + weather tool).

Each app has its own azure.yaml, package.json, and README.md. Run azd commands from inside each app folder to provision and deploy it independently.

Repository layout

This repository now includes two independently deployable Function Apps:

  1. mcp-tools contains MCP Tool triggers, rich content samples, and snippet storage samples.
  2. mcp-weather-app contains the MCP App weather sample (resource + weather tool).

Each app has its own azure.yaml, package.json, and README.md. Run azd commands from inside each app folder to provision and deploy it independently.

Prerequisites

Prepare your local environment

An Azure Storage Emulator is needed for this particular sample because we will save and get snippets from blob storage.

  1. Start Azurite

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

Note if you use Azurite coming from VS Code extension you need to run Azurite: Start now or you will see errors.

Run your MCP Server locally from the terminal

This repository contains multiple independently runnable Function Apps. Each app exposes its own MCP endpoint on the default Functions port (7071). To run more than one app at the same time, start each from a separate terminal and pass --port to avoid collisions.

Root snippets/tools app (this folder)

  1. Install dependencies

    npm install
  2. Build the project

    npm run build
  3. Start the Functions host locally:

    func start

mcp-tools app

Contains MCP Tool triggers for rich content samples (image, resource links, structured content) and snippet samples (save/get snippets via blob bindings). See mcp-tools/README.md for details.

cd mcp-tools
npm install
npm run build
func start

mcp-weather-app

Contains the MCP App sample: an MCP Resource trigger serving an interactive weather widget plus an MCP Tool trigger that calls Open-Meteo. See mcp-weather-app/README.md for details.

cd mcp-weather-app
npm install
npm run build:app   # bundle the widget UI
npm run build
func start

mcp-prompts app

Contains MCP Prompt triggers (code review checklist, summarize content, generate documentation). See mcp-prompts for details.

cd mcp-prompts
npm install
npm run build
func start

Note by default this will use the webhooks route: /runtime/webhooks/mcp. Later we will use this in Azure to set the key on client/host calls: /runtime/webhooks/mcp?code=<system_key>

If you want to run multiple apps simultaneously, give each its own port, e.g. func start --port 7072, and use that port when configuring the client.

Use the local MCP server from within a client/host

The example URL below points to the root app on port 7071. To connect to the mcp-tools app, mcp-weather-app, or mcp-prompts app, use the same /runtime/webhooks/mcp path against the port that app is listening on (see mcp-tools/README.md, mcp-weather-app/README.md, and the mcp-prompts folder for the tools, weather widget, and prompts each app exposes).

VS Code - Copilot Edits

  1. Add MCP Server from command palette and add URL to your running Function app's mcp endpoint:

    http://0.0.0.0:7071/runtime/webhooks/mcp
  2. Select HTTP (Server-Sent-Events) for the type of MCP server to add.

  3. Enter the URL to your running function app's mcp endpoint

  4. Enter the server ID. (This can be any name you want)

  5. Choose if you want to run this in your User settings (available to all apps for you) or to your Workspace settings (available to this app, only)

  6. List MCP Servers from command palette and start the server. The previous step may have already started your local server. If so, you can skip this step.

  7. In Copilot chat agent mode enter a prompt to trigger the tool, e.g., select some code and enter this prompt

    Say Hello
    
    Save this snippet as snippet1 
    
    Retrieve snippet1 and apply to newFile.ts
    
  8. When prompted to run the tool, consent by clicking Continue

  9. When you're done, press Ctrl+C in the terminal window to stop the func.exe host process, and List MCP Servers from command palette and stop the local server.

MCP Inspector

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

    npx @modelcontextprotocol/inspector node build/index.js
  2. If you stopped your function app previously, start the Functions host locally:

    func start
  3. CTRL click to load the MCP Inspector web app from the URL displayed by the app (e.g. http://0.0.0.0:5173/#resources)

  4. Set the transport type to http

  5. Set the URL to your running Function app's mcp endpoint and Connect:

    http://0.0.0.0:7071/runtime/webhooks/mcp
  6. List Tools. Click on a tool and Run Tool.

  7. When you're done, press Ctrl+C in the terminal window to stop the func.exe host process, and press Ctrl+C in the terminal window to stop the @modelcontextprotocol/inspector host process.

Verify local blob storage in Azurite

After testing the snippet save functionality locally, you can verify that blobs are being stored correctly in your local Azurite storage emulator.

Using Azure Storage Explorer

  1. Open Azure Storage Explorer
  2. In the left panel, expand Emulator & AttachedStorage Accounts(Emulator - Default Ports) (Key)
  3. Navigate to Blob Containerssnippets
  4. You should see any saved snippets as blob files in this container
  5. Double-click on any blob to view its contents and verify the snippet data was saved correctly

Using Azure CLI (Alternative)

If you prefer using the command line, you can also verify blobs using Azure CLI with the storage emulator:

# List blobs in the snippets container
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;"
# Download a specific blob to view its contents
az storage blob download --container-name snippets --name <blob-name> --file <local-file-path> --connection-string "DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;"

This verification step ensures your MCP server is correctly interacting with the local storage emulator and that the blob storage functionality is working as expected before deploying to Azure.

Deploy to Azure for Remote MCP

azd env new <environment-name>

Configure VS Code as an allowed client application to request access tokens from Microsoft Entra:

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

Optionally, you can opt-in to a VNet being used in the sample. (If you choose this, do this before azd up)

azd env set VNET_ENABLED true

Run these azd commands to provision the function app, with any required Azure resources, and deploy your code:

azd provision

Wait a few minutes for access permissions to take effect, then run

azd deploy

Note API Management can be used for improved security and policies over your MCP Server.

Deploy the mcp-tools or mcp-weather-app independently

The mcp-tools, mcp-weather-app, and mcp-prompts folders each contain their own azure.yaml and can be provisioned and deployed as a separate Function App. Run the same azd flow from inside the app folder you want to deploy:

cd mcp-tools          # or: cd mcp-weather-app  |  cd mcp-prompts
azd init              # first time only
azd env new           # first time only
azd provision
azd deploy

Each app reuses the shared infra under infra/ but is deployed as its own Function App. See mcp-tools/README.md and mcp-weather-app/README.md for app-specific notes.

Note API Management can be used for improved security and policies over your MCP Server, and App Service built-in authentication can be used to set up your favorite OAuth provider including Entra.

Connect to remote MCP server in VS Code - GitHub Copilot

For GitHub Copilot within VS Code, you use https://<funcappname>.azurewebsites.net/runtime/webhooks/mcp for the URL. The following example is from the mcp.json file included in this repository and uses an input to prompt you to provide the function name when you start the server from VS Code. The server is configured with buit-in MCP auth, so you'll be asked to login as well. Your mcp.json file looks like this:

Connect to your remote MCP server function app from a client

Your client will need a key in order to invoke the new hosted SSE endpoint, which will be of the form https://<funcappname>.azurewebsites.net/runtime/webhooks/mcp. The hosted function requires a system key by default which can be obtained from the portal or the CLI (az functionapp keys list --resource-group <resource_group> --name <function_app_name>). Obtain the system key named mcp_extension.

Connect to remote MCP server in MCP Inspector

For MCP Inspector, you can include the key in the URL:

https://<funcappname>.azurewebsites.net/runtime/webhooks/mcp?code=<your-mcp-extension-system-key>

Connect to remote MCP server in VS Code - GitHub Copilot

For GitHub Copilot within VS Code, you should set the key as the x-functions-key header in mcp.json, and you would use https://<funcappname>.azurewebsites.net/runtime/webhooks/mcp for the URL. The following example is from the mcp.json file included in this repository and uses an input to prompt you to provide the key when you start the server from VS Code. Your mcp.json file looks like this:

{
    "inputs": [
        {
            "type": "promptString",
            "id": "functions-mcp-extension-system-key",
            "description": "Azure Functions MCP Extension System Key",
            "password": true
        },
        {
            "type": "promptString",
            "id": "functionapp-name",
            "description": "Azure Functions App Name"
        }
    ],
    "servers": {
        "remote-mcp-function": {
            "type": "http",
            "url": "https://${input:functionapp-name}.azurewebsites.net/runtime/webhooks/mcp",
            "headers": {
                "x-functions-key": "${input:functions-mcp-extension-system-key}"
            }
        },
        "local-mcp-function": {
            "type": "http",
            "url": "http://0.0.0.0:7071/runtime/webhooks/mcp"
        }
    }
}
  1. Click Start on the server remote-mcp-function, inside the mcp.json file:

  2. Enter the name of the function app that you created in the Azure Portal, when prompted by VS Code.

  3. Enter the Azure Functions MCP Extension System Key into the prompt. You can copy this from the Azure portal for your function app by going to the Functions menu item, then App Keys, and copying the mcp_extension key from the System Keys.

  4. In Copilot chat agent mode enter a prompt to trigger the tool, e.g., select some code and enter this prompt

    Say Hello
    
    Save this snippet as snippet1 
    
    Retrieve snippet1 and apply to newFile.ts
    

Redeploy your code

You can run the azd deploy command as many times as you need to deploy code updates to your function app.

Note

Deployed code files are always overwritten by the latest deployment package.

Clean up resources

When you're done working with your function app and related resources, you can use this command to delete the function app and its related resources from Azure and avoid incurring any further costs:

azd down

Source Code

The function code for the getSnippet and saveSnippet endpoints are defined in the TypeScript files in the src directory. The MCP function annotations expose these functions as MCP Server tools.

This shows the code for a few MCP server examples (get string, get object, save object):

// Hello function - responds with hello message
export async function mcpToolHello(context: InvocationContext): Promise<string> {
    return "Hello I am MCP Tool!";
}

// Register the hello tool
app.mcpTool('hello', {
    toolName: 'hello',
    description: 'Simple hello world MCP Tool that responses with a hello message.',
    handler: mcpToolHello
});

// GetSnippet function - retrieves a snippet by name
export async function getSnippet(_message: unknown, context: InvocationContext): Promise<string> {
    console.info('Getting snippet');
    
    // Get snippet name from the tool arguments
    const mcptoolargs = context.triggerMetadata.mcptoolargs as { snippetname?: string };
    const snippetName = mcptoolargs?.snippetname;

    console.info(`Snippet name: ${snippetName}`);
    
    if (!snippetName) {
        return "No snippet name provided";
    }
    
    // Get the content from blob binding - properly retrieving from extraInputs
    const snippetContent = context.extraInputs.get(blobInputBinding);
    
    if (!snippetContent) {
        return `Snippet '${snippetName}' not found`;
    }
    
    console.info(`Retrieved snippet: ${snippetName}`);
    return snippetContent as string;
}


// Register the GetSnippet tool
app.mcpTool('getsnippet', {
    toolName: GET_SNIPPET_TOOL_NAME,
    description: GET_SNIPPET_TOOL_DESCRIPTION,
    toolProperties: [
        {
            propertyName: SNIPPET_NAME_PROPERTY_NAME,
            propertyValue: PROPERTY_TYPE,
            description: SNIPPET_NAME_PROPERTY_DESCRIPTION,
        }
    ],
    extraInputs: [blobInputBinding],
    handler: getSnippet
});

// SaveSnippet function - saves a snippet with a name
export async function saveSnippet(_message: unknown, context: InvocationContext): Promise<string> {
    console.info('Saving snippet');
    
    // Get snippet name and content from the tool arguments
    const mcptoolargs = context.triggerMetadata.mcptoolargs as { 
        snippetname?: string;
        snippet?: string;
    };
    
    const snippetName = mcptoolargs?.snippetname;
    const snippet = mcptoolargs?.snippet;
    
    if (!snippetName) {
        return "No snippet name provided";
    }
    
    if (!snippet) {
        return "No snippet content provided";
    }
    
    // Save the snippet to blob storage using the output binding
    context.extraOutputs.set(blobOutputBinding, snippet);
    
    console.info(`Saved snippet: ${snippetName}`);
    return snippet;
}

// Register the SaveSnippet tool
app.mcpTool('savesnippet', {
    toolName: SAVE_SNIPPET_TOOL_NAME,
    description: SAVE_SNIPPET_TOOL_DESCRIPTION,
    toolProperties: [
        {
            propertyName: SNIPPET_NAME_PROPERTY_NAME,
            propertyValue: PROPERTY_TYPE,
            description: SNIPPET_NAME_PROPERTY_DESCRIPTION,
        },
        {
            propertyName: SNIPPET_PROPERTY_NAME,
            propertyValue: PROPERTY_TYPE,
            description: SNIPPET_PROPERTY_DESCRIPTION,
        }
    ],
    extraOutputs: [blobOutputBinding],
    handler: saveSnippet
});

Result Schema (UseResultSchema)

The TypeScript MCP tool trigger now supports explicitly defining resultSchema on app.mcpTool(...) options. When present and valid JSON, it is sent with useResultSchema: true so the host can use your declared result shape.

const imageInfoSchema = JSON.stringify({
    type: "object",
    properties: {
        imageId: { type: "string" },
        format: { type: "string" },
        tags: { type: "array", items: { type: "string" } }
    },
    required: ["imageId", "format", "tags"],
    additionalProperties: false
});

app.mcpTool("GetImageInfo", {
    toolName: "GetImageInfo",
    description: "Get image information",
    toolProperties: {
        imageId: arg.string().describe("Optional image identifier").optional()
    },
    resultSchema: imageInfoSchema,
    handler: getImageInfo
});

Note that the host.json file also includes a reference to the extension bundle, which is required for apps using this feature:

"extensionBundle": {
  "id": "Microsoft.Azure.Functions.ExtensionBundle",
  "version": "[4.*, 5.0.0)"
}

Weather App Sample

A sample MCP App that displays weather information with an interactive UI.

What Are MCP Apps?

MCP Apps let tools return interactive interfaces instead of plain text. When a tool declares a UI resource, the host renders it in a sandboxed iframe where users can interact directly.

MCP Apps = Tool + UI Resource

The architecture relies on two MCP primitives:

  1. Tools with UI metadata pointing to a resource URI
  2. Resources containing bundled HTML/JavaScript served via the ui:// scheme

Azure Functions makes it easy to build both.

Prerequisites for Weather App

Getting Started with Weather App

All commands in this section must be run from the mcp-weather-app/ folder. Navigate there first:

cd mcp-weather-app

1. Build the UI

The UI must be bundled before running the function app:

```shell
cd src/app
npm install
npm run build
```

This creates a bundled src/app/dist/index.html file that the function serves.

2. Build and run the Function App

  1. Install dependencies for the Function app:

    cd ../../
    npm install
  2. Build the project:

    npm run build
  3. Start the Functions host locally:

    func start

3. Connect from VS Code

Open .vscode/mcp.json. Find the server called local-mcp-function and click Start above the name. The server is already set up with the running Function app's MCP endpoint:

```shell
http://0.0.0.0:7071/runtime/webhooks/mcp
```

4. Prompt the Agent

Ask Copilot: "What's the weather in Seattle?"

How It Works Together

  1. User asks: "What's the weather in Seattle?"
  2. Agent calls the getWeather tool
  3. Tool returns weather data (JSON) and the host sees the ui.resourceUri metadata
  4. Host fetches the UI resource from ui://weather/index.html
  5. Host renders the HTML in a sandboxed iframe, passing the tool result as context
  6. User sees an interactive weather widget instead of plain text

The UI (TypeScript)

The frontend in src/app/src/weatherMcpApp.ts receives the tool result and renders the weather display. It's bundled with Vite into a single index.html that the resource serves.

Next Steps

About

This is a quickstart template to easily build and deploy a custom remote MCP server to the cloud using Azure functions. You can clone/restore/run on your local machine with debugging, and azd up to have it in the cloud in a couple minutes. The MCP server is secured by design.

Resources

License

Code of conduct

Stars

Watchers

Forks

Packages

 
 
 

Contributors