@@ -86,6 +86,43 @@ export const DEFAULT_AGENT_FEEDBACK_PAYLOAD_SCHEMA: Record<string, unknown> = {
8686export const DOCS_MARKDOWN_SIGNATURE_AGENT_HEADER = "Signature-Agent" ;
8787const DOCS_LLMS_TXT_DIRECTIVE_LINE = "LLM index: /llms.txt" ;
8888
89+ const DOCS_MCP_SERVICE_SUBDOMAIN_LABELS = new Set ( [
90+ "api" ,
91+ "developer" ,
92+ "developers" ,
93+ "dev" ,
94+ "docs" ,
95+ "help" ,
96+ "mcp" ,
97+ "reference" ,
98+ ] ) ;
99+ const COMMON_SECOND_LEVEL_PUBLIC_SUFFIX_LABELS = new Set ( [
100+ "ac" ,
101+ "co" ,
102+ "com" ,
103+ "edu" ,
104+ "go" ,
105+ "gov" ,
106+ "mil" ,
107+ "ne" ,
108+ "net" ,
109+ "or" ,
110+ "org" ,
111+ ] ) ;
112+
113+ export interface DocsMcpEndpointCandidate {
114+ baseUrl : string ;
115+ route : string ;
116+ url : string ;
117+ label : string ;
118+ }
119+
120+ export interface DocsMcpEndpointCandidateOptions {
121+ includeOriginFallback ?: boolean ;
122+ includeRootDomainFallback ?: boolean ;
123+ includeMcpSubdomainFallback ?: boolean ;
124+ }
125+
89126export interface DocsAgentFeedbackResolvedConfig {
90127 enabled : boolean ;
91128 route : string ;
@@ -845,6 +882,142 @@ export function isDocsMcpRequest(url: URL): boolean {
845882 ) ;
846883}
847884
885+ export function buildDocsMcpEndpointCandidates (
886+ baseUrl : string ,
887+ routes : readonly string [ ] = [ DEFAULT_MCP_PUBLIC_ROUTE , DEFAULT_MCP_WELL_KNOWN_ROUTE ] ,
888+ options : DocsMcpEndpointCandidateOptions = { } ,
889+ ) : DocsMcpEndpointCandidate [ ] {
890+ const includeOriginFallback = options . includeOriginFallback !== false ;
891+ const includeRootDomainFallback = options . includeRootDomainFallback !== false ;
892+ const includeMcpSubdomainFallback = options . includeMcpSubdomainFallback !== false ;
893+ const base = new URL ( baseUrl ) ;
894+ const primaryOrigin = base . origin ;
895+ const seen = new Set < string > ( ) ;
896+ const candidates : DocsMcpEndpointCandidate [ ] = [ ] ;
897+
898+ const addCandidate = ( candidateBaseUrl : string , route : string ) => {
899+ const resolved = resolveDocsMcpCandidateUrl ( candidateBaseUrl , route ) ;
900+ if ( seen . has ( resolved . url ) ) return ;
901+ seen . add ( resolved . url ) ;
902+ candidates . push ( {
903+ ...resolved ,
904+ label : formatDocsMcpCandidateLabel ( resolved . url , primaryOrigin ) ,
905+ } ) ;
906+ } ;
907+
908+ for ( const route of routes ) {
909+ addCandidate ( baseUrl , route ) ;
910+ }
911+
912+ const originBaseUrl = primaryOrigin ;
913+ if ( includeOriginFallback && originBaseUrl !== baseUrl . replace ( / \/ + $ / , "" ) ) {
914+ for ( const route of routes ) {
915+ addCandidate ( originBaseUrl , route ) ;
916+ }
917+ }
918+
919+ if ( includeRootDomainFallback ) {
920+ for ( const rootDomainBaseUrl of toDocsRootDomainBaseUrls ( base ) ) {
921+ for ( const route of routes ) {
922+ addCandidate ( rootDomainBaseUrl , route ) ;
923+ }
924+ }
925+ }
926+
927+ if ( includeMcpSubdomainFallback ) {
928+ for ( const mcpBaseUrl of toDocsMcpSubdomainBaseUrls ( base ) ) {
929+ addCandidate ( mcpBaseUrl , DEFAULT_MCP_PUBLIC_ROUTE ) ;
930+ addCandidate ( mcpBaseUrl , "/" ) ;
931+ }
932+ }
933+
934+ return candidates ;
935+ }
936+
937+ function resolveDocsMcpCandidateUrl (
938+ baseUrl : string ,
939+ route : string ,
940+ ) : { baseUrl : string ; route : string ; url : string } {
941+ if ( / ^ h t t p s ? : \/ \/ / i. test ( route ) ) {
942+ const parsed = new URL ( route ) ;
943+ const path = `${ parsed . pathname || "/" } ${ parsed . search } ` ;
944+ return {
945+ baseUrl : parsed . origin ,
946+ route : path ,
947+ url : parsed . toString ( ) ,
948+ } ;
949+ }
950+
951+ const base = new URL ( baseUrl ) ;
952+ const basePath = base . pathname . replace ( / \/ + $ / , "" ) ;
953+ const routePath = route . startsWith ( "/" ) ? route : `/${ route } ` ;
954+ const parsed = new URL ( `${ basePath } ${ routePath } ` , base . origin ) ;
955+
956+ return {
957+ baseUrl : parsed . origin ,
958+ route : `${ parsed . pathname } ${ parsed . search } ` ,
959+ url : parsed . toString ( ) ,
960+ } ;
961+ }
962+
963+ function formatDocsMcpCandidateLabel ( url : string , primaryOrigin : string ) : string {
964+ const parsed = new URL ( url ) ;
965+ const path = `${ parsed . pathname } ${ parsed . search } ` ;
966+ return parsed . origin === primaryOrigin ? path : `${ parsed . origin } ${ path } ` ;
967+ }
968+
969+ function toDocsMcpSubdomainBaseUrls ( base : URL ) : string [ ] {
970+ return getDocsMcpRootDomainCandidates ( base . hostname ) . map (
971+ ( rootDomain ) => `${ base . protocol } //mcp.${ rootDomain } ${ base . port ? `:${ base . port } ` : "" } ` ,
972+ ) ;
973+ }
974+
975+ function toDocsRootDomainBaseUrls ( base : URL ) : string [ ] {
976+ return getDocsMcpRootDomainCandidates ( base . hostname ) . map (
977+ ( rootDomain ) => `${ base . protocol } //${ rootDomain } ${ base . port ? `:${ base . port } ` : "" } ` ,
978+ ) ;
979+ }
980+
981+ function getDocsMcpRootDomainCandidates ( hostname : string ) : string [ ] {
982+ const normalized = hostname
983+ . toLowerCase ( )
984+ . replace ( / ^ \[ | \] $ / g, "" )
985+ . replace ( / \. $ / , "" ) ;
986+ if ( ! normalized || ! normalized . includes ( "." ) || isDocsIpHostname ( normalized ) ) return [ ] ;
987+
988+ const labels = normalized . split ( "." ) . filter ( Boolean ) ;
989+ if ( labels . length < 2 ) return [ ] ;
990+
991+ const candidates : string [ ] = [ ] ;
992+ const addCandidate = ( candidateLabels : string [ ] ) => {
993+ if ( candidateLabels . length < 2 ) return ;
994+ const candidate = candidateLabels . join ( "." ) ;
995+ if ( ! candidates . includes ( candidate ) ) candidates . push ( candidate ) ;
996+ } ;
997+
998+ if ( labels . length >= 3 && DOCS_MCP_SERVICE_SUBDOMAIN_LABELS . has ( labels [ 0 ] ?? "" ) ) {
999+ addCandidate ( labels . slice ( 1 ) ) ;
1000+ }
1001+
1002+ const tld = labels . at ( - 1 ) ?? "" ;
1003+ const secondLevel = labels . at ( - 2 ) ?? "" ;
1004+ const shouldPreserveSecondLevelSuffix =
1005+ labels . length >= 3 &&
1006+ tld . length === 2 &&
1007+ COMMON_SECOND_LEVEL_PUBLIC_SUFFIX_LABELS . has ( secondLevel ) ;
1008+
1009+ if ( shouldPreserveSecondLevelSuffix ) {
1010+ addCandidate ( labels . slice ( - 3 ) ) ;
1011+ } else {
1012+ addCandidate ( labels . slice ( - 2 ) ) ;
1013+ }
1014+ return candidates ;
1015+ }
1016+
1017+ function isDocsIpHostname ( hostname : string ) : boolean {
1018+ return / ^ ( \d { 1 , 3 } \. ) { 3 } \d { 1 , 3 } $ / . test ( hostname ) || hostname . includes ( ":" ) ;
1019+ }
1020+
8481021export function isDocsSkillRequest ( url : URL ) : boolean {
8491022 const pathname = normalizeDocsUrlPath ( url . pathname ) ;
8501023 if ( pathname === DEFAULT_SKILL_MD_ROUTE || pathname === DEFAULT_SKILL_MD_WELL_KNOWN_ROUTE ) {
0 commit comments