@@ -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,25 +107,43 @@ function buildEmbeddedMcpServerResource(apiJson: Record<string, unknown>): Recor
79107
80108}
81109
82- function hasMeaningfulMcpContent ( mcpJson : Record < string , unknown > ) : boolean {
83- const properties = mcpJson . properties as Record < string , unknown > | undefined ;
84- if ( ! properties ) {
85- return false ;
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 ;
86126 }
87127
88- // Check if mcpTools has actual content (non-empty array), excluding null
89- const mcpTools = properties . mcpTools as unknown [ ] | undefined | null ;
90- if ( Array . isArray ( mcpTools ) && mcpTools . length > 0 ) {
91- return true ;
92- }
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 ;
93131
94- // Check if mcpProperties exists and is not null or undefined
95- if ( properties . mcpProperties != null ) {
96- return true ;
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 ;
97144 }
98-
99- return false ;
100145}
146+
101147/**
102148 * Extract all API-specific resources for a single API.
103149 * This includes revisions, specifications, operations, policies, etc.
@@ -568,8 +614,18 @@ async function extractGraphQLResolvers(
568614
569615/**
570616 * Extract MCP (Model Context Protocol) server configuration for an API.
571- * The MCP server is a singleton resource per API exposed at apis/{id}/mcpServers/default.
572- * Silently skips if the API does not have MCP enabled or the resource does not exist.
617+ *
618+ * MCP configuration is embedded directly on the API resource
619+ * (`properties.mcpTools`, `properties.mcpProperties`, `properties.backendId`).
620+ * There is no separate child resource served by ARM — the
621+ * `apis/{id}/mcpServers/default` endpoint returns 404 even on working MCP APIs,
622+ * and `apis/{id}/mcpServers` returns 500 (no such collection). All MCP data
623+ * 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`.
573629 */
574630async function extractApiMcpServer (
575631 client : IApimClient ,
@@ -586,30 +642,24 @@ async function extractApiMcpServer(
586642 return false ;
587643 }
588644
645+ if ( ! hasEmbeddedMcpConfiguration ( apiJson ) ) {
646+ return false ;
647+ }
648+
589649 const mcpDescriptor : ResourceDescriptor = {
590650 type : ResourceType . McpServer ,
591651 nameParts : [ ...apiDescriptor . nameParts ] ,
592652 workspace : apiDescriptor . workspace ,
593653 } ;
594654
595- if ( hasEmbeddedMcpConfiguration ( apiJson ) ) {
596- await store . writeResource ( outputDir , mcpDescriptor , buildEmbeddedMcpServerResource ( apiJson ) ) ;
597- logger . info ( `Extracted ${ buildResourceLabel ( mcpDescriptor ) } from API metadata` ) ;
598- return true ;
599- }
600-
601- try {
602- const mcpJson = await client . getResource ( context , mcpDescriptor ) ;
603- if ( ! mcpJson || ! hasMeaningfulMcpContent ( mcpJson ) ) {
604- return false ;
605- }
606-
607- await store . writeResource ( outputDir , mcpDescriptor , mcpJson ) ;
608- logger . info ( `Extracted ${ buildResourceLabel ( mcpDescriptor ) } ` ) ;
609- return true ;
610- } catch ( error ) {
611- const errorMessage = error instanceof Error ? error . message : String ( error ) ;
612- logger . debug ( `No MCP server configuration ${ buildResourceLabel ( mcpDescriptor ) } : ${ errorMessage } ` ) ;
613- return false ;
614- }
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+ ) ;
663+ logger . info ( `Extracted ${ buildResourceLabel ( mcpDescriptor ) } from API metadata` ) ;
664+ return true ;
615665}
0 commit comments