<!--
MCP Server Policy for Azure API Management
This policy transforms APIM into an MCP (Model Context Protocol) server by:
1. Handling JSON-RPC 2.0 protocol messages
2. Routing MCP methods (initialize, tools/list, tools/call) appropriately
3. Transforming Azure Functions responses to MCP format
MCP Protocol Version: 2024-11-05
-->
<policies>
<inbound>
<base />
<!-- Set CORS headers for browser-based MCP clients -->
<cors allow-credentials="true">
<allowed-origins>
<origin>*</origin>
</allowed-origins>
<allowed-methods>
<method>POST</method>
<method>OPTIONS</method>
</allowed-methods>
<allowed-headers>
<header>*</header>
</allowed-headers>
</cors>
<!-- Parse and validate MCP request -->
<set-variable name="mcpRequest" value="@(context.Request.Body.As<JObject>(preserveContent: true))" />
<set-variable name="jsonrpcVersion" value="@(((JObject)context.Variables["mcpRequest"])["jsonrpc"]?.ToString() ?? "")" />
<set-variable name="method" value="@(((JObject)context.Variables["mcpRequest"])["method"]?.ToString() ?? "")" />
<set-variable name="requestId" value="@(((JObject)context.Variables["mcpRequest"])["id"]?.ToString())" />
<set-variable name="params" value="@(((JObject)context.Variables["mcpRequest"])["params"] as JObject)" />
<!-- Validate JSON-RPC 2.0 version -->
<choose>
<when condition="@((string)context.Variables["jsonrpcVersion"] != "2.0")">
<return-response>
<set-status code="200" />
<set-header name="Content-Type" exists-action="override">
<value>application/json</value>
</set-header>
<set-body>@{
return new JObject(
new JProperty("jsonrpc", "2.0"),
new JProperty("error", new JObject(
new JProperty("code", -32600),
new JProperty("message", "Invalid Request: jsonrpc must be '2.0'")
)),
new JProperty("id", context.Variables["requestId"])
).ToString();
}</set-body>
</return-response>
</when>
</choose>
<!-- Route based on MCP method -->
<choose>
<!-- ============================================ -->
<!-- MCP: initialize -->
<!-- ============================================ -->
<when condition="@((string)context.Variables["method"] == "initialize")">
<return-response>
<set-status code="200" />
<set-header name="Content-Type" exists-action="override">
<value>application/json</value>
</set-header>
<set-body>@{
var requestId = context.Variables["requestId"];
return new JObject(
new JProperty("jsonrpc", "2.0"),
new JProperty("result", new JObject(
new JProperty("protocolVersion", "2024-11-05"),
new JProperty("capabilities", new JObject(
new JProperty("tools", new JObject(
new JProperty("listChanged", false)
))
)),
new JProperty("serverInfo", new JObject(
new JProperty("name", "Azure APIM MCP Server"),
new JProperty("version", "1.0.0")
))
)),
new JProperty("id", requestId)
).ToString();
}</set-body>
</return-response>
</when>
<!-- ============================================ -->
<!-- MCP: notifications/initialized -->
<!-- ============================================ -->
<when condition="@((string)context.Variables["method"] == "notifications/initialized")">
<!-- This is a notification, no response required but we acknowledge -->
<return-response>
<set-status code="200" />
<set-header name="Content-Type" exists-action="override">
<value>application/json</value>
</set-header>
<set-body>{}</set-body>
</return-response>
</when>
<!-- ============================================ -->
<!-- MCP: tools/list -->
<!-- ============================================ -->
<when condition="@((string)context.Variables["method"] == "tools/list")">
<return-response>
<set-status code="200" />
<set-header name="Content-Type" exists-action="override">
<value>application/json</value>
</set-header>
<set-body>@{
var requestId = context.Variables["requestId"];
return new JObject(
new JProperty("jsonrpc", "2.0"),
new JProperty("result", new JObject(
new JProperty("tools", new JArray(
// Tool: get_products
new JObject(
new JProperty("name", "get_products"),
new JProperty("description", "Retrieve products from the catalog. Can filter by category and price range."),
new JProperty("inputSchema", new JObject(
new JProperty("type", "object"),
new JProperty("properties", new JObject(
new JProperty("category", new JObject(
new JProperty("type", "string"),
new JProperty("description", "Filter by product category (e.g., 'electronics', 'tools', 'safety')")
)),
new JProperty("minPrice", new JObject(
new JProperty("type", "number"),
new JProperty("description", "Minimum price filter")
)),
new JProperty("maxPrice", new JObject(
new JProperty("type", "number"),
new JProperty("description", "Maximum price filter")
))
)),
new JProperty("required", new JArray())
))
),
// Tool: get_product
new JObject(
new JProperty("name", "get_product"),
new JProperty("description", "Get detailed information about a specific product by its ID."),
new JProperty("inputSchema", new JObject(
new JProperty("type", "object"),
new JProperty("properties", new JObject(
new JProperty("productId", new JObject(
new JProperty("type", "string"),
new JProperty("description", "The unique product identifier (e.g., 'prod-001')")
))
)),
new JProperty("required", new JArray("productId"))
))
),
// Tool: search_products
new JObject(
new JProperty("name", "search_products"),
new JProperty("description", "Search for products by query string. Returns products matching the search terms with relevance scores."),
new JProperty("inputSchema", new JObject(
new JProperty("type", "object"),
new JProperty("properties", new JObject(
new JProperty("query", new JObject(
new JProperty("type", "string"),
new JProperty("description", "Search query to find products")
)),
new JProperty("maxResults", new JObject(
new JProperty("type", "integer"),
new JProperty("description", "Maximum number of results to return (default: 10)")
)),
new JProperty("category", new JObject(
new JProperty("type", "string"),
new JProperty("description", "Optional category filter")
))
)),
new JProperty("required", new JArray("query"))
))
),
// Tool: create_order
new JObject(
new JProperty("name", "create_order"),
new JProperty("description", "Create a new order for a product. Validates stock availability before creating."),
new JProperty("inputSchema", new JObject(
new JProperty("type", "object"),
new JProperty("properties", new JObject(
new JProperty("productId", new JObject(
new JProperty("type", "string"),
new JProperty("description", "The ID of the product to order")
)),
new JProperty("quantity", new JObject(
new JProperty("type", "integer"),
new JProperty("description", "Number of items to order (must be positive)")
))
)),
new JProperty("required", new JArray("productId", "quantity"))
))
),
// Tool: get_orders
new JObject(
new JProperty("name", "get_orders"),
new JProperty("description", "Retrieve all orders from the system."),
new JProperty("inputSchema", new JObject(
new JProperty("type", "object"),
new JProperty("properties", new JObject()),
new JProperty("required", new JArray())
))
),
// Tool: check_health
new JObject(
new JProperty("name", "check_health"),
new JProperty("description", "Check the health status of the API and get system statistics."),
new JProperty("inputSchema", new JObject(
new JProperty("type", "object"),
new JProperty("properties", new JObject()),
new JProperty("required", new JArray())
))
)
))
)),
new JProperty("id", requestId)
).ToString();
}</set-body>
</return-response>
</when>
<!-- ============================================ -->
<!-- MCP: tools/call -->
<!-- ============================================ -->
<when condition="@((string)context.Variables["method"] == "tools/call")">
<set-variable name="toolName" value="@{
var p = (JObject)context.Variables["params"];
return p?["name"]?.ToString() ?? "";
}" />
<set-variable name="toolArgs" value="@{
var p = (JObject)context.Variables["params"];
return p?["arguments"] as JObject ?? new JObject();
}" />
</when>
<!-- Unknown method -->
<otherwise>
<return-response>
<set-status code="200" />
<set-header name="Content-Type" exists-action="override">
<value>application/json</value>
</set-header>
<set-body>@{
var method = context.Variables["method"];
var requestId = context.Variables["requestId"];
return new JObject(
new JProperty("jsonrpc", "2.0"),
new JProperty("error", new JObject(
new JProperty("code", -32601),
new JProperty("message", $"Method not found: {method}")
)),
new JProperty("id", requestId)
).ToString();
}</set-body>
</return-response>
</otherwise>
</choose>
<!-- Route tool calls to appropriate Azure Function endpoints -->
<choose>
<!-- get_products tool -->
<when condition="@((string)context.Variables["toolName"] == "get_products")">
<set-backend-service base-url="{{BackendFunctionUrl}}" />
<rewrite-uri template="/api/products" />
<set-method>GET</set-method>
<set-query-parameter name="category" exists-action="override">
<value>@{
var args = (JObject)context.Variables["toolArgs"];
return args?["category"]?.ToString() ?? "";
}</value>
</set-query-parameter>
<set-query-parameter name="minPrice" exists-action="override">
<value>@{
var args = (JObject)context.Variables["toolArgs"];
return args?["minPrice"]?.ToString() ?? "";
}</value>
</set-query-parameter>
<set-query-parameter name="maxPrice" exists-action="override">
<value>@{
var args = (JObject)context.Variables["toolArgs"];
return args?["maxPrice"]?.ToString() ?? "";
}</value>
</set-query-parameter>
</when>
<!-- get_product tool -->
<when condition="@((string)context.Variables["toolName"] == "get_product")">
<set-backend-service base-url="{{BackendFunctionUrl}}" />
<rewrite-uri template="@{
var args = (JObject)context.Variables["toolArgs"];
var productId = args?["productId"]?.ToString() ?? "";
return $"/api/products/{productId}";
}" />
<set-method>GET</set-method>
</when>
<!-- search_products tool -->
<when condition="@((string)context.Variables["toolName"] == "search_products")">
<set-backend-service base-url="{{BackendFunctionUrl}}" />
<rewrite-uri template="/api/products/search" />
<set-method>POST</set-method>
<set-header name="Content-Type" exists-action="override">
<value>application/json</value>
</set-header>
<set-body>@{
var args = (JObject)context.Variables["toolArgs"];
return args.ToString();
}</set-body>
</when>
<!-- create_order tool -->
<when condition="@((string)context.Variables["toolName"] == "create_order")">
<set-backend-service base-url="{{BackendFunctionUrl}}" />
<rewrite-uri template="/api/orders" />
<set-method>POST</set-method>
<set-header name="Content-Type" exists-action="override">
<value>application/json</value>
</set-header>
<set-body>@{
var args = (JObject)context.Variables["toolArgs"];
return args.ToString();
}</set-body>
</when>
<!-- get_orders tool -->
<when condition="@((string)context.Variables["toolName"] == "get_orders")">
<set-backend-service base-url="{{BackendFunctionUrl}}" />
<rewrite-uri template="/api/orders" />
<set-method>GET</set-method>
</when>
<!-- check_health tool -->
<when condition="@((string)context.Variables["toolName"] == "check_health")">
<set-backend-service base-url="{{BackendFunctionUrl}}" />
<rewrite-uri template="/api/health" />
<set-method>GET</set-method>
</when>
<!-- Unknown tool -->
<when condition="@(!string.IsNullOrEmpty((string)context.Variables["toolName"]))">
<return-response>
<set-status code="200" />
<set-header name="Content-Type" exists-action="override">
<value>application/json</value>
</set-header>
<set-body>@{
var toolName = context.Variables["toolName"];
var requestId = context.Variables["requestId"];
return new JObject(
new JProperty("jsonrpc", "2.0"),
new JProperty("error", new JObject(
new JProperty("code", -32602),
new JProperty("message", $"Unknown tool: {toolName}")
)),
new JProperty("id", requestId)
).ToString();
}</set-body>
</return-response>
</when>
</choose>
<!-- Add function key for Azure Functions authentication -->
<set-header name="x-functions-key" exists-action="override">
<value>{{FunctionAppKey}}</value>
</set-header>
</inbound>
<backend>
<base />
</backend>
<outbound>
<base />
<!-- Transform Azure Function response to MCP tool result format -->
<choose>
<when condition="@(context.Variables.ContainsKey("toolName") && !string.IsNullOrEmpty((string)context.Variables["toolName"]))">
<set-body>@{
var requestId = context.Variables["requestId"];
var responseBody = context.Response.Body.As<string>(preserveContent: true);
// Try to parse as JSON, fallback to plain text
JToken content;
try {
content = JToken.Parse(responseBody);
} catch {
content = responseBody;
}
return new JObject(
new JProperty("jsonrpc", "2.0"),
new JProperty("result", new JObject(
new JProperty("content", new JArray(
new JObject(
new JProperty("type", "text"),
new JProperty("text", content.ToString(Newtonsoft.Json.Formatting.Indented))
)
))
)),
new JProperty("id", requestId)
).ToString();
}</set-body>
</when>
</choose>
<set-header name="Content-Type" exists-action="override">
<value>application/json</value>
</set-header>
</outbound>
<on-error>
<base />
<return-response>
<set-status code="200" />
<set-header name="Content-Type" exists-action="override">
<value>application/json</value>
</set-header>
<set-body>@{
var requestId = context.Variables.ContainsKey("requestId") ? context.Variables["requestId"] : null;
var errorMessage = context.LastError?.Message ?? "Internal server error";
var errorReason = context.LastError?.Reason ?? "";
return new JObject(
new JProperty("jsonrpc", "2.0"),
new JProperty("error", new JObject(
new JProperty("code", -32603),
new JProperty("message", $"{errorMessage}. {errorReason}".Trim())
)),
new JProperty("id", requestId)
).ToString();
}</set-body>
</return-response>
</on-error>
</policies>