Skip to content

Commit ef6c6dd

Browse files
committed
fix attempt #2
1 parent ec377c1 commit ef6c6dd

3 files changed

Lines changed: 116 additions & 20 deletions

File tree

src/services/api-extractor.ts

Lines changed: 83 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,19 +58,47 @@ function hasEmbeddedMcpConfiguration(apiJson: Record<string, unknown>): boolean
5858
return true;
5959
}
6060

61+
// MCP APIs created from an existing MCP server are wired purely via
62+
// backendId + (optionally absent) mcpProperties. A non-null backendId on a
63+
// type='mcp' API is itself an MCP server configuration we must capture.
64+
if (typeof properties.backendId === 'string' && properties.backendId.length > 0) {
65+
return true;
66+
}
67+
6168
return false;
6269
}
6370

64-
function buildEmbeddedMcpServerResource(apiJson: Record<string, unknown>): Record<string, unknown> {
71+
function buildEmbeddedMcpServerResource(
72+
apiJson: Record<string, unknown>,
73+
backendUrl?: string
74+
): Record<string, unknown> {
6575
const properties = getApiProperties(apiJson) ?? {};
6676
const resourceProperties: Record<string, unknown> = {};
6777

68-
if (properties.mcpProperties !== undefined) {
69-
resourceProperties.mcpProperties = properties.mcpProperties;
78+
// Clone mcpProperties so we can augment it with serverUrl without mutating
79+
// the caller's apiJson.
80+
let mcpProperties: Record<string, unknown> | undefined;
81+
if (properties.mcpProperties && typeof properties.mcpProperties === 'object') {
82+
mcpProperties = { ...(properties.mcpProperties as Record<string, unknown>) };
83+
} else if (backendUrl) {
84+
mcpProperties = {};
85+
}
86+
87+
if (mcpProperties && backendUrl && mcpProperties.serverUrl === undefined) {
88+
mcpProperties.serverUrl = backendUrl;
89+
}
90+
91+
if (mcpProperties !== undefined) {
92+
resourceProperties.mcpProperties = mcpProperties;
7093
}
7194
if (properties.mcpTools !== undefined) {
7295
resourceProperties.mcpTools = properties.mcpTools;
7396
}
97+
// Preserve the link to the upstream backend so the MCP server sidecar is
98+
// self-describing for MCP-from-existing-MCP-server APIs.
99+
if (typeof properties.backendId === 'string' && properties.backendId.length > 0) {
100+
resourceProperties.backendId = properties.backendId;
101+
}
74102

75103
return {
76104
name: 'default',
@@ -79,6 +107,43 @@ function buildEmbeddedMcpServerResource(apiJson: Record<string, unknown>): Recor
79107

80108
}
81109

110+
/**
111+
* For an MCP API wired to a backend via `backendId`, fetch that backend and
112+
* return its `properties.url` so the MCP sidecar can carry the actual upstream
113+
* server URL. Returns undefined when the API has no backendId, the backend
114+
* cannot be fetched, or the backend has no url.
115+
*/
116+
async function resolveLinkedBackendUrl(
117+
client: IApimClient,
118+
context: ApimServiceContext,
119+
apiJson: Record<string, unknown>,
120+
workspace?: string
121+
): Promise<string | undefined> {
122+
const properties = getApiProperties(apiJson);
123+
const backendId = properties?.backendId;
124+
if (typeof backendId !== 'string' || backendId.length === 0) {
125+
return undefined;
126+
}
127+
128+
// backendId may be either a bare resource name or a full ARM resource id.
129+
// We only consume the trailing name segment for descriptor lookup.
130+
const backendName = backendId.includes('/') ? backendId.split('/').pop()! : backendId;
131+
132+
try {
133+
const backendJson = await client.getResource(context, {
134+
type: ResourceType.Backend,
135+
nameParts: [backendName],
136+
workspace,
137+
});
138+
const url = (backendJson?.properties as Record<string, unknown> | undefined)?.url;
139+
return typeof url === 'string' && url.length > 0 ? url : undefined;
140+
} catch (error) {
141+
const errorMessage = error instanceof Error ? error.message : String(error);
142+
logger.debug(`Could not resolve backend "${backendName}" for MCP server URL: ${errorMessage}`);
143+
return undefined;
144+
}
145+
}
146+
82147
/**
83148
* Extract all API-specific resources for a single API.
84149
* This includes revisions, specifications, operations, policies, etc.
@@ -556,11 +621,16 @@ async function extractGraphQLResolvers(
556621
* `apis/{id}/mcpServers/default` endpoint returns 404 even on working MCP APIs,
557622
* and `apis/{id}/mcpServers` returns 500 (no such collection). All MCP data
558623
* therefore comes from the API JSON itself.
624+
*
625+
* For MCP APIs created from an existing MCP server (the `backendId` pattern),
626+
* the upstream URL lives on the linked backend, not on the API. To make the
627+
* extracted `mcpServerInformation.json` self-describing, the extractor
628+
* resolves that backend and surfaces its URL as `mcpProperties.serverUrl`.
559629
*/
560630
async function extractApiMcpServer(
561-
_client: IApimClient,
631+
client: IApimClient,
562632
store: IArtifactStore,
563-
_context: ApimServiceContext,
633+
context: ApimServiceContext,
564634
apiDescriptor: ResourceDescriptor,
565635
apiJson: Record<string, unknown>,
566636
outputDir: string
@@ -582,7 +652,14 @@ async function extractApiMcpServer(
582652
workspace: apiDescriptor.workspace,
583653
};
584654

585-
await store.writeResource(outputDir, mcpDescriptor, buildEmbeddedMcpServerResource(apiJson));
655+
// Resolve the upstream backend URL so the MCP sidecar carries the actual
656+
// server URL (not just a backendId reference + uri template).
657+
const backendUrl = await resolveLinkedBackendUrl(client, context, apiJson, apiDescriptor.workspace);
658+
await store.writeResource(
659+
outputDir,
660+
mcpDescriptor,
661+
buildEmbeddedMcpServerResource(apiJson, backendUrl)
662+
);
586663
logger.info(`Extracted ${buildResourceLabel(mcpDescriptor)} from API metadata`);
587664
return true;
588665
}

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

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@
120120
]
121121
},
122122
"backends": {
123-
"minCount": 5,
123+
"minCount": 6,
124124
"expected": [
125125
{
126126
"name": "src-backend-http",
@@ -172,6 +172,17 @@
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"
175186
}
176187
]
177188
},
@@ -582,30 +593,26 @@
582593
"properties.mcpTools": "exists"
583594
}
584595
},
585-
"children": {
586-
"operations": {
587-
"minCount": 1,
588-
"notes": "Explicit operation for ApiOperation BVT coverage"
589-
}
590-
},
591-
"notes": "MCP API exposing REST backend operations as MCP tools; has explicit operation and mcpServerInformation.json"
596+
"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)"
592597
},
593598
{
594599
"name": "src-mcp-from-external",
595600
"files": ["apiInformation.json", "mcpServerInformation.json"],
596601
"spotChecks": {
597602
"apiInformation.json": {
598603
"properties.displayName": "KS MCP from External Server",
599-
"properties.path": "ks/mcp-external",
604+
"properties.path": "",
600605
"properties.type": "mcp",
601-
"properties.subscriptionRequired": false
606+
"properties.subscriptionRequired": false,
607+
"properties.backendId": "src-backend-mcp-external"
602608
},
603609
"mcpServerInformation.json": {
604610
"properties.mcpProperties": "exists",
605-
"properties.mcpProperties.serverUrl": "https://api.githubcopilot.com/mcp"
611+
"properties.mcpProperties.endpoints.mcp.uriTemplate": "/mcp",
612+
"properties.backendId": "src-backend-mcp-external"
606613
}
607614
},
608-
"notes": "MCP API repackaging a public external MCP server; has mcpServerInformation.json with mcpProperties.serverUrl"
615+
"notes": "MCP API repackaging an external MCP server: empty path on the API, 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"
609616
}
610617
]
611618
},

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

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -930,6 +930,18 @@ resource apiMcpFromApi 'Microsoft.ApiManagement/service/apis@2025-09-01-preview'
930930
})
931931
}
932932

933+
// Explicit operation on the MCP-from-API surface for ApiOperation BVT coverage
934+
resource apiMcpFromApiPing 'Microsoft.ApiManagement/service/apis/operations@2025-09-01-preview' = {
935+
parent: apiMcpFromApi
936+
name: 'mcpPing'
937+
properties: {
938+
displayName: 'MCP Ping'
939+
method: 'GET'
940+
urlTemplate: '/ping'
941+
description: 'Lightweight health probe for MCP-from-API'
942+
}
943+
}
944+
933945
// Backend for external MCP server (backendId pattern requires a backend resource)
934946
resource backendMcpExternal 'Microsoft.ApiManagement/service/backends@2025-09-01-preview' = {
935947
parent: apim
@@ -942,15 +954,15 @@ resource backendMcpExternal 'Microsoft.ApiManagement/service/backends@2025-09-01
942954
}
943955

944956
// 9. MCP API created from an existing (external) public MCP server
945-
// External MCP uses backendId + mcpProperties (path must be empty)
957+
// External MCP uses backendId + mcpProperties; APIM accepts a non-empty path here.
946958
// any() used because backendId and mcpProperties.endpoints are valid at runtime but absent from Bicep type definitions (BCP037/BCP036)
947959
resource apiMcpFromExternal 'Microsoft.ApiManagement/service/apis@2025-09-01-preview' = {
948960
parent: apim
949961
name: 'src-mcp-from-external'
950962
properties: any({
951963
displayName: 'KS MCP from External Server'
952964
description: 'MCP server repackaging a public external MCP server via APIM'
953-
path: ''
965+
path: 'ks/mcp-external'
954966
protocols: ['https']
955967
subscriptionRequired: false
956968
type: 'mcp'

0 commit comments

Comments
 (0)