Skip to content

Commit 1162a14

Browse files
committed
Supporting new and legacy default paths for agent cards
1 parent df90543 commit 1162a14

2 files changed

Lines changed: 144 additions & 58 deletions

File tree

tests/integration/all-resource-types/expected-structure.json

Lines changed: 7 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -172,17 +172,6 @@
172172
"properties.pool.services": "exists"
173173
}
174174
}
175-
},
176-
{
177-
"name": "src-backend-mcp-external",
178-
"files": ["backendInformation.json"],
179-
"spotChecks": {
180-
"backendInformation.json": {
181-
"properties.url": "https://api.githubcopilot.com/mcp",
182-
"properties.protocol": "http"
183-
}
184-
},
185-
"notes": "Upstream URL for the MCP-from-external API; the API resource references this backend via backendId"
186175
}
187176
]
188177
},
@@ -579,23 +568,22 @@
579568
"notes": "MCP API exposing operations of an existing REST API as MCP tools via mcpTools (each tool's operationId references the backing REST API; this MCP API has no operations of its own)"
580569
},
581570
{
582-
"name": "src-mcp-from-external",
583-
"files": ["apiInformation.json", "mcpServerInformation.json"],
571+
"name": "src-mcp-existing-server",
572+
"files": ["apiInformation.json", "mcpServerInformation.json", "policy.xml"],
584573
"spotChecks": {
585574
"apiInformation.json": {
586-
"properties.displayName": "KS MCP from External Server",
587-
"properties.path": "ks/mcp-external",
575+
"properties.displayName": "KS MCP Existing Server Demo",
576+
"properties.path": "ks/mcp-existing",
588577
"properties.type": "mcp",
589578
"properties.subscriptionRequired": false,
590-
"properties.backendId": "src-backend-mcp-external"
579+
"properties.backendId": "src-backend-mcp-learn"
591580
},
592581
"mcpServerInformation.json": {
593-
"properties.mcpProperties": "exists",
594582
"properties.mcpProperties.endpoints.mcp.uriTemplate": "/mcp",
595-
"properties.backendId": "src-backend-mcp-external"
583+
"properties.backendId": "src-backend-mcp-learn"
596584
}
597585
},
598-
"notes": "MCP API repackaging an external MCP server: backendId points to the backend that holds the upstream URL (https://api.githubcopilot.com/mcp), and mcpProperties.endpoints.mcp.uriTemplate addresses the MCP endpoint exposed by that backend"
586+
"notes": "Working existing-server MCP demo. APIM exposes a public Microsoft Learn MCP server through a policy-based MCP proxy so the API is extractable and demoable end to end."
599587
},
600588
{
601589
"name": "src-a2a-weather-agent",

tests/integration/all-resource-types/source-apim.bicep

Lines changed: 137 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -923,15 +923,13 @@ resource apiMcpExistingServer 'Microsoft.ApiManagement/service/apis@2025-09-01-p
923923
})
924924
}
925925

926-
// Mock runtime API used by the A2A API backend settings.
927-
// This keeps the demo self-contained and avoids external dependencies.
928926
resource apiA2aRuntimeMock 'Microsoft.ApiManagement/service/apis@2025-09-01-preview' = {
929927
parent: apim
930928
name: 'src-a2a-runtime-mock'
931929
properties: {
932930
displayName: 'KS A2A Runtime Mock'
933931
description: 'Mock runtime API used as the backend for the A2A demo API'
934-
path: 'ks/a2a-runtime'
932+
path: 'ks/a2a-weather'
935933
protocols: ['https']
936934
serviceUrl: 'https://httpbin.org'
937935
subscriptionRequired: false
@@ -945,7 +943,7 @@ resource apiA2aRuntimeCardOperation 'Microsoft.ApiManagement/service/apis/operat
945943
properties: {
946944
displayName: 'Get agent card'
947945
method: 'GET'
948-
urlTemplate: '/.well-known/agent.json'
946+
urlTemplate: '/.well-known/agent-card.json'
949947
responses: [
950948
{
951949
statusCode: 200
@@ -965,7 +963,37 @@ resource apiA2aRuntimeCardPolicy 'Microsoft.ApiManagement/service/apis/operation
965963
name: 'policy'
966964
properties: {
967965
format: 'rawxml'
968-
value: '<policies><inbound><base /><return-response><set-status code="200" reason="OK" /><set-header name="Content-Type" exists-action="override"><value>application/json</value></set-header><set-body>{"protocolVersion":"0.3.0","name":"KS A2A Weather Agent","description":"Demo A2A weather agent served entirely by APIM policies","url":"https://${apim.name}.azure-api.net/ks/a2a-weather","preferredTransport":"JSONRPC","version":"1.0.0","capabilities":{"streaming":false,"pushNotifications":false,"stateTransitionHistory":false},"defaultInputModes":["text/plain"],"defaultOutputModes":["text/plain"],"skills":[{"id":"get_weather","name":"Get weather","description":"Returns current weather conditions for a city","tags":["weather","demo"],"examples":["What is the weather in Seattle?","weather in Paris"],"inputModes":["text/plain"],"outputModes":["text/plain"]}]}</set-body></return-response></inbound><backend><base /></backend><outbound><base /></outbound><on-error><base /></on-error></policies>'
966+
value: '''<policies><inbound><base /><return-response><set-status code="200" reason="OK" /><set-header name="Content-Type" exists-action="override"><value>application/json</value></set-header><set-body>@("{\"protocolVersion\":\"0.3.0\",\"name\":\"KS A2A Weather Agent\",\"description\":\"Demo A2A weather agent served entirely by APIM policies\",\"url\":\"https://" + context.Request.OriginalUrl.Host + "/ks/a2a-weather\",\"preferredTransport\":\"JSONRPC\",\"version\":\"1.0.0\",\"capabilities\":{\"streaming\":false,\"pushNotifications\":false,\"stateTransitionHistory\":false},\"defaultInputModes\":[\"text/plain\"],\"defaultOutputModes\":[\"text/plain\"],\"skills\":[{\"id\":\"get_weather\",\"name\":\"Get weather\",\"description\":\"Returns current weather conditions for a city\",\"tags\":[\"weather\",\"demo\"],\"examples\":[\"What is the weather in Seattle?\",\"weather in Paris\"],\"inputModes\":[\"text/plain\"],\"outputModes\":[\"text/plain\"]}]}")</set-body></return-response></inbound><backend><base /></backend><outbound><base /></outbound><on-error><base /></on-error></policies>'''
967+
}
968+
}
969+
970+
resource apiA2aRuntimeCardLegacyOperation 'Microsoft.ApiManagement/service/apis/operations@2025-09-01-preview' = {
971+
parent: apiA2aRuntimeMock
972+
name: 'get-agent-card-legacy'
973+
properties: {
974+
displayName: 'Get agent card (legacy path)'
975+
method: 'GET'
976+
urlTemplate: '/.well-known/agent.json'
977+
responses: [
978+
{
979+
statusCode: 200
980+
description: 'OK'
981+
representations: [
982+
{
983+
contentType: 'application/json'
984+
}
985+
]
986+
}
987+
]
988+
}
989+
}
990+
991+
resource apiA2aRuntimeCardLegacyPolicy 'Microsoft.ApiManagement/service/apis/operations/policies@2025-09-01-preview' = {
992+
parent: apiA2aRuntimeCardLegacyOperation
993+
name: 'policy'
994+
properties: {
995+
format: 'rawxml'
996+
value: '''<policies><inbound><base /><return-response><set-status code="200" reason="OK" /><set-header name="Content-Type" exists-action="override"><value>application/json</value></set-header><set-body>@("{\"protocolVersion\":\"0.3.0\",\"name\":\"KS A2A Weather Agent\",\"description\":\"Demo A2A weather agent served entirely by APIM policies\",\"url\":\"https://" + context.Request.OriginalUrl.Host + "/ks/a2a-weather\",\"preferredTransport\":\"JSONRPC\",\"version\":\"1.0.0\",\"capabilities\":{\"streaming\":false,\"pushNotifications\":false,\"stateTransitionHistory\":false},\"defaultInputModes\":[\"text/plain\"],\"defaultOutputModes\":[\"text/plain\"],\"skills\":[{\"id\":\"get_weather\",\"name\":\"Get weather\",\"description\":\"Returns current weather conditions for a city\",\"tags\":[\"weather\",\"demo\"],\"examples\":[\"What is the weather in Seattle?\",\"weather in Paris\"],\"inputModes\":[\"text/plain\"],\"outputModes\":[\"text/plain\"]}]}")</set-body></return-response></inbound><backend><base /></backend><outbound><base /></outbound><on-error><base /></on-error></policies>'''
969997
}
970998
}
971999

@@ -1002,31 +1030,105 @@ resource apiA2aRuntimeJsonRpcPolicy 'Microsoft.ApiManagement/service/apis/operat
10021030
name: 'policy'
10031031
properties: {
10041032
format: 'rawxml'
1005-
value: '''<policies><inbound><base /><return-response><set-status code="200" reason="OK" /><set-header name="Content-Type" exists-action="override"><value>application/json</value></set-header><set-body>@{
1006-
var reqBody = context.Request.Body.As<JObject>(preserveContent: true);
1007-
var idToken = reqBody["id"];
1008-
string idJson = idToken != null ? idToken.ToString(Newtonsoft.Json.Formatting.None) : "1";
1009-
string method = (string)reqBody["method"] ?? "";
1010-
if (method != "message/send") {
1011-
return "{\"jsonrpc\":\"2.0\",\"id\":" + idJson + ",\"error\":{\"code\":-32601,\"message\":\"Method not found: " + method + "\"}}";
1012-
}
1013-
var parts = reqBody.SelectToken("params.message.parts") as JArray;
1014-
string text = "";
1015-
if (parts != null) {
1016-
foreach (var p in parts) { if ((string)p["kind"] == "text") { text = (string)p["text"] ?? ""; break; } }
1017-
}
1018-
string city = "your location";
1019-
int idx = text.ToLowerInvariant().IndexOf(" in ");
1020-
if (idx >= 0) { city = text.Substring(idx + 4).Trim().TrimEnd(new[] { '?', '.', '!', ',' }); }
1021-
string reply = "Weather in " + city + ": 62°F, partly cloudy (demo response from APIM A2A policy).";
1022-
string replyJson = Newtonsoft.Json.JsonConvert.SerializeObject(reply);
1023-
string taskId = System.Guid.NewGuid().ToString();
1024-
string contextId = System.Guid.NewGuid().ToString();
1025-
string artifactId = System.Guid.NewGuid().ToString();
1026-
string msgId = System.Guid.NewGuid().ToString();
1027-
string ts = System.DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffZ");
1028-
return "{\"jsonrpc\":\"2.0\",\"id\":" + idJson + ",\"result\":{\"kind\":\"task\",\"id\":\"" + taskId + "\",\"contextId\":\"" + contextId + "\",\"status\":{\"state\":\"completed\",\"timestamp\":\"" + ts + "\"},\"artifacts\":[{\"artifactId\":\"" + artifactId + "\",\"name\":\"weather-reply\",\"parts\":[{\"kind\":\"text\",\"text\":" + replyJson + "}]}],\"history\":[{\"kind\":\"message\",\"role\":\"agent\",\"messageId\":\"" + msgId + "\",\"parts\":[{\"kind\":\"text\",\"text\":" + replyJson + "}]}]}}";
1029-
}</set-body></return-response></inbound><backend><base /></backend><outbound><base /></outbound><on-error><base /></on-error></policies>'''
1033+
value: '''<policies>
1034+
<inbound>
1035+
<base />
1036+
<set-variable name="reqBody" value='@(context.Request.Body.As<Newtonsoft.Json.Linq.JObject>(preserveContent: true))' />
1037+
<set-variable name="rpcId" value='@{ var t = ((Newtonsoft.Json.Linq.JObject)context.Variables["reqBody"])["id"]; return t != null ? t.ToString(Newtonsoft.Json.Formatting.None) : "1"; }' />
1038+
<set-variable name="rpcMethod" value='@((string)((Newtonsoft.Json.Linq.JObject)context.Variables["reqBody"])["method"] ?? "")' />
1039+
<choose>
1040+
<when condition='@((string)context.Variables["rpcMethod"] != "message/send")'>
1041+
<return-response>
1042+
<set-status code="200" reason="OK" />
1043+
<set-header name="Content-Type" exists-action="override"><value>application/json</value></set-header>
1044+
<set-body>@("{\"jsonrpc\":\"2.0\",\"id\":" + (string)context.Variables["rpcId"] + ",\"error\":{\"code\":-32601,\"message\":\"Method not found: " + (string)context.Variables["rpcMethod"] + "\"}}")</set-body>
1045+
</return-response>
1046+
</when>
1047+
</choose>
1048+
<set-variable name="city" value='@{
1049+
var parts = ((Newtonsoft.Json.Linq.JObject)context.Variables["reqBody"]).SelectToken("params.message.parts") as Newtonsoft.Json.Linq.JArray;
1050+
string text = "";
1051+
if (parts != null) { foreach (var p in parts) { if ((string)p["kind"] == "text") { text = (string)p["text"] ?? ""; break; } } }
1052+
string city = text.Trim();
1053+
int idx = text.ToLowerInvariant().IndexOf(" in ");
1054+
if (idx >= 0) { city = text.Substring(idx + 4).Trim(); }
1055+
city = city.TrimEnd("?.!,".ToCharArray()).Trim();
1056+
if (string.IsNullOrWhiteSpace(city)) { city = "Seattle"; }
1057+
return city;
1058+
}' />
1059+
<send-request mode="new" response-variable-name="geoResp" timeout="10" ignore-error="true">
1060+
<set-url>@($"https://geocoding-api.open-meteo.com/v1/search?count=1&amp;name={System.Uri.EscapeDataString((string)context.Variables["city"])}")</set-url>
1061+
<set-method>GET</set-method>
1062+
</send-request>
1063+
<set-variable name="latlon" value='@{
1064+
var r = (IResponse)context.Variables["geoResp"];
1065+
if (r == null || r.StatusCode != 200) { return (string)null; }
1066+
var body = r.Body.As<Newtonsoft.Json.Linq.JObject>();
1067+
var arr = body["results"] as Newtonsoft.Json.Linq.JArray;
1068+
if (arr == null || arr.Count == 0) { return (string)null; }
1069+
string lat = arr[0]["latitude"].ToString(Newtonsoft.Json.Formatting.None);
1070+
string lon = arr[0]["longitude"].ToString(Newtonsoft.Json.Formatting.None);
1071+
string resolved = (string)arr[0]["name"];
1072+
string country = (string)arr[0]["country"];
1073+
return lat + "|" + lon + "|" + resolved + "|" + (country ?? "");
1074+
}' />
1075+
<choose>
1076+
<when condition='@(context.Variables["latlon"] == null)'>
1077+
<set-variable name="reply" value='@("Sorry, I could not find a location named " + (string)context.Variables["city"] + ".")' />
1078+
</when>
1079+
<otherwise>
1080+
<send-request mode="new" response-variable-name="wxResp" timeout="10" ignore-error="true">
1081+
<set-url>@{
1082+
var ll = ((string)context.Variables["latlon"]).Split('|');
1083+
return "https://api.open-meteo.com/v1/forecast?latitude=" + ll[0] + "&amp;longitude=" + ll[1] + "&amp;current=temperature_2m,weather_code,wind_speed_10m&amp;temperature_unit=fahrenheit&amp;wind_speed_unit=mph";
1084+
}</set-url>
1085+
<set-method>GET</set-method>
1086+
</send-request>
1087+
<set-variable name="reply" value='@{
1088+
var ll = ((string)context.Variables["latlon"]).Split('|');
1089+
string place = ll.Length >= 4 && !string.IsNullOrEmpty(ll[3]) ? (ll[2] + ", " + ll[3]) : ll[2];
1090+
var r = (IResponse)context.Variables["wxResp"];
1091+
if (r == null || r.StatusCode != 200) { return "Weather for " + place + " is currently unavailable."; }
1092+
var cur = r.Body.As<Newtonsoft.Json.Linq.JObject>()["current"] as Newtonsoft.Json.Linq.JObject;
1093+
if (cur == null) { return "Weather for " + place + " is currently unavailable."; }
1094+
double tempF = (double)cur["temperature_2m"];
1095+
int code = cur["weather_code"] != null ? (int)cur["weather_code"] : -1;
1096+
double wind = cur["wind_speed_10m"] != null ? (double)cur["wind_speed_10m"] : 0.0;
1097+
var codes = new System.Collections.Generic.Dictionary<int, string> {
1098+
{0,"clear sky"},{1,"mainly clear"},{2,"partly cloudy"},{3,"overcast"},
1099+
{45,"fog"},{48,"depositing rime fog"},
1100+
{51,"light drizzle"},{53,"moderate drizzle"},{55,"dense drizzle"},
1101+
{56,"light freezing drizzle"},{57,"dense freezing drizzle"},
1102+
{61,"light rain"},{63,"moderate rain"},{65,"heavy rain"},
1103+
{66,"light freezing rain"},{67,"heavy freezing rain"},
1104+
{71,"light snow"},{73,"moderate snow"},{75,"heavy snow"},{77,"snow grains"},
1105+
{80,"rain showers"},{81,"moderate rain showers"},{82,"violent rain showers"},
1106+
{85,"light snow showers"},{86,"heavy snow showers"},
1107+
{95,"thunderstorm"},{96,"thunderstorm with light hail"},{99,"thunderstorm with heavy hail"}
1108+
};
1109+
string cond = codes.ContainsKey(code) ? codes[code] : ("weather code " + code);
1110+
return "Weather in " + place + ": " + tempF.ToString("F0") + "°F, " + cond + ", wind " + wind.ToString("F0") + " mph (live data from Open-Meteo).";
1111+
}' />
1112+
</otherwise>
1113+
</choose>
1114+
<return-response>
1115+
<set-status code="200" reason="OK" />
1116+
<set-header name="Content-Type" exists-action="override"><value>application/json</value></set-header>
1117+
<set-body>@{
1118+
string replyJson = Newtonsoft.Json.JsonConvert.SerializeObject((string)context.Variables["reply"]);
1119+
string taskId = System.Guid.NewGuid().ToString();
1120+
string contextId = System.Guid.NewGuid().ToString();
1121+
string artifactId = System.Guid.NewGuid().ToString();
1122+
string msgId = System.Guid.NewGuid().ToString();
1123+
string ts = System.DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffZ");
1124+
return "{\"jsonrpc\":\"2.0\",\"id\":" + (string)context.Variables["rpcId"] + ",\"result\":{\"kind\":\"task\",\"id\":\"" + taskId + "\",\"contextId\":\"" + contextId + "\",\"status\":{\"state\":\"completed\",\"timestamp\":\"" + ts + "\"},\"artifacts\":[{\"artifactId\":\"" + artifactId + "\",\"name\":\"weather-reply\",\"parts\":[{\"kind\":\"text\",\"text\":" + replyJson + "}]}],\"history\":[{\"kind\":\"message\",\"role\":\"agent\",\"messageId\":\"" + msgId + "\",\"parts\":[{\"kind\":\"text\",\"text\":" + replyJson + "}]}]}}";
1125+
}</set-body>
1126+
</return-response>
1127+
</inbound>
1128+
<backend><base /></backend>
1129+
<outbound><base /></outbound>
1130+
<on-error><base /></on-error>
1131+
</policies>'''
10301132
}
10311133
}
10321134

@@ -1039,26 +1141,22 @@ resource apiA2a 'Microsoft.ApiManagement/service/apis@2025-09-01-preview' = {
10391141
properties: any({
10401142
displayName: 'KS A2A Weather Agent'
10411143
description: 'A2A API exposing JSON-RPC runtime and an APIM-mediated agent card'
1042-
path: 'ks/a2a-weather'
1144+
path: 'ks/a2a-managed'
10431145
protocols: ['https']
10441146
type: 'a2a'
10451147
isAgent: true
10461148
agent: {
10471149
id: 'src-a2a-weather-agent'
10481150
}
10491151
a2aProperties: {
1050-
agentCardPath: '/.well-known/agent.json'
1051-
agentCardBackendUrl: 'https://${apim.name}.azure-api.net/ks/a2a-runtime/.well-known/agent.json'
1152+
agentCardPath: '/.well-known/agent-card.json'
1153+
agentCardBackendUrl: 'https://${apim.name}.azure-api.net/ks/a2a-weather/.well-known/agent-card.json'
10521154
}
10531155
jsonRpcProperties: {
10541156
backendUrl: 'https://${apim.name}.azure-api.net'
1055-
path: '/ks/a2a-runtime'
1056-
}
1057-
subscriptionRequired: true
1058-
subscriptionKeyParameterNames: {
1059-
header: 'Ocp-Apim-Subscription-Key'
1060-
query: 'subscription-key'
1157+
path: '/ks/a2a-weather'
10611158
}
1159+
subscriptionRequired: false
10621160
})
10631161
}
10641162

0 commit comments

Comments
 (0)