@@ -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 */
560630async 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}
0 commit comments