Skip to content

Commit 3404993

Browse files
CopilotEMaher
andauthored
Add A2A API round-trip support in extract/publish and all-types integration coverage (#89)
* Add A2A API round-trip support in extract/publish * Update round-trip test for working mcp server * Updating round-trip to use real back-ends for demo --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Elizabeth Maher <enewman@microsoft.com>
1 parent bba4cb4 commit 3404993

11 files changed

Lines changed: 774 additions & 56 deletions

File tree

docs/reference/artifact-format.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,7 @@ Example policy XML:
241241
- **Info file names** follow the pattern `{resourceType}Information.json`
242242
- **Policy files** are always named `policy.xml`
243243
- **API specifications** are named `specification.{ext}` (e.g., `specification.yaml`, `specification.json`, `specification.wsdl`)
244+
- Specification files are not exported for API types that don't use OpenAPI/WSDL artifacts (for example: WebSocket, MCP, A2A).
244245
- **Wiki files** are named `wiki.md`
245246

246247
---

src/clients/apim-client.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -586,7 +586,7 @@ export class ApimClient implements IApimClient {
586586
* graphql-link – GraphQL SDL blob (type=graphql)
587587
* wsdl-link – WSDL blob (type=soap)
588588
* openapi-link – OpenAPI 3.0 YAML (type=http, default)
589-
* undefined – (type=websocket — no spec; callers should skip)
589+
* undefined – (type=websocket|a2a — no spec; callers should skip)
590590
*
591591
* SOAP APIs use wsdl-link so the exported specification can be re-imported
592592
* faithfully on publish (matches the Azure/apiops reference tool). APIM's
@@ -612,6 +612,7 @@ export class ApimClient implements IApimClient {
612612
case 'graphql': return 'graphql-link';
613613
case 'soap': return 'wsdl-link';
614614
case 'websocket': return undefined;
615+
case 'a2a': return undefined;
615616
default: return 'openapi-link';
616617
}
617618
}
@@ -743,4 +744,3 @@ export class ApimClient implements IApimClient {
743744
}
744745

745746
}
746-

src/services/api-extractor.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,11 @@ async function extractApiSpecification(
358358
return false;
359359
}
360360

361+
if (apiType?.toLowerCase() === 'a2a') {
362+
logger.debug(`Skipping spec export for A2A API "${getNamePart(apiDescriptor.nameParts, 0)}" — A2A APIs use JSON-RPC + agent card endpoints, not OpenAPI`);
363+
return false;
364+
}
365+
361366
if (apiType?.toLowerCase() === 'graphql' && hasGraphQLSchema(extractedSchemas)) {
362367
logger.debug(
363368
`Skipping spec export for synthetic GraphQL API "${getNamePart(apiDescriptor.nameParts, 0)}" — schema is captured via ApiSchema`

src/services/api-publisher.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,11 @@ export async function publishApi(
9797
* Maps spec file format to APIM ContentFormat for inline import.
9898
*/
9999
function getImportFormat(specFormat: string, _apiType?: string): string | undefined {
100+
const apiType = _apiType?.toLowerCase();
101+
if (apiType === 'a2a') {
102+
return undefined;
103+
}
104+
100105
switch (specFormat) {
101106
case 'yaml':
102107
return 'openapi';
@@ -178,7 +183,7 @@ async function publishRootApi(
178183

179184
// The `type` property from GET is read-only and ignored on PUT.
180185
// APIM uses `apiType` to determine the API kind during spec import.
181-
// Always set it explicitly (valid values: http, soap, websocket, graphql, odata, grpc, mcp).
186+
// Always set it explicitly (valid values include http, soap, websocket, graphql, odata, grpc, mcp, a2a).
182187
if (apiType) {
183188
cleanProps.apiType = apiType;
184189
}

tests/integration/all-resource-types/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ The kitchen sink APIM instance includes **every resource type and API protocol v
1717
| `src-rest-versioned-v1` | REST (versioned) | OpenAPI |
1818
| `src-rest-revisioned` | REST (revisioned) | OpenAPI |
1919
| `src-mcp-from-api` | MCP (from existing API) | None |
20-
| `src-mcp-from-external` | MCP (from external MCP server) | None |
20+
| `src-mcp-existing-server` | MCP (working existing-server demo via Learn) | None |
21+
| `src-a2a-weather-agent` | A2A (JSON-RPC + agent card) | None |
2122

2223
### Backend Variations
2324
| Backend | Type |

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

Lines changed: 26 additions & 20 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
},
@@ -355,7 +344,7 @@
355344
]
356345
},
357346
"apis": {
358-
"minCount": 9,
347+
"minCount": 10,
359348
"expected": [
360349
{
361350
"name": "src-rest-openapi",
@@ -579,23 +568,40 @@
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"
584+
}
585+
},
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."
587+
},
588+
{
589+
"name": "src-a2a-weather-agent",
590+
"files": ["apiInformation.json"],
591+
"spotChecks": {
592+
"apiInformation.json": {
593+
"properties.displayName": "KS A2A Weather Agent",
594+
"properties.path": "ks/a2a-managed",
595+
"properties.type": "a2a",
596+
"properties.agent.id": "src-a2a-weather-agent",
597+
"properties.a2aProperties.agentCardPath": "/.well-known/agent-card.json",
598+
"properties.jsonRpcProperties.path": "/ks/a2a-weather",
599+
"properties.subscriptionRequired": true,
600+
"properties.subscriptionKeyParameterNames.header": "Ocp-Apim-Subscription-Key",
601+
"properties.subscriptionKeyParameterNames.query": "subscription-key"
596602
}
597603
},
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"
604+
"notes": "A2A API with JSON-RPC runtime mediation and agent card settings."
599605
}
600606
]
601607
},

0 commit comments

Comments
 (0)