001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.juneau.rest.server.mcp.v20260728; 018 019import static org.apache.juneau.commons.utils.Shorts.*; 020 021import java.net.*; 022import java.util.*; 023 024/** 025 * Pure, side-effect-free construction of the RFC 9728 / SEP-2351 well-known Protected Resource Metadata (PRM) URIs for a 026 * given canonical resource identifier. 027 * 028 * <p> 029 * Per <a class="doclink" href="https://datatracker.ietf.org/doc/html/rfc9728#section-3.1">RFC 9728 §3.1</a> and 030 * SEP-2351, the well-known segment {@code /.well-known/oauth-protected-resource} is inserted between the host and the 031 * resource's path component: 032 * 033 * <ul> 034 * <li>Resource {@code https://host/mcp} → PRM at {@code https://host/.well-known/oauth-protected-resource/mcp} 035 * (path-insertion). 036 * <li>Resource {@code https://host} (no path) → PRM at {@code https://host/.well-known/oauth-protected-resource} 037 * (root form). 038 * </ul> 039 * 040 * <p> 041 * {@link #candidates(URI)} returns the ordered client lookup sequence — the path-inserted URI first, then the root 042 * fallback — matching the ordered fallback SEP-2351 specifies. The server hosts the path-inserted form (and, for 043 * a rooted resource, that <i>is</i> the root form). 044 * 045 * @since 10.0.0 046 */ 047public final class McpWellKnownRouting { 048 049 /** The RFC 9728 well-known path segment. */ 050 public static final String WELL_KNOWN_PATH = "/.well-known/oauth-protected-resource"; 051 052 private McpWellKnownRouting() {} 053 054 /** 055 * Returns the path-inserted PRM URI for the supplied resource identifier (RFC 9728 §3.1). 056 * 057 * @param resource The canonical resource identifier. Must not be <jk>null</jk> and must be absolute (scheme + 058 * authority). 059 * @return The PRM document URI. 060 * @throws IllegalArgumentException If {@code resource} is <jk>null</jk> or is missing a scheme/authority. 061 */ 062 public static URI metadataUri(URI resource) { 063 var base = baseOf(resource); 064 return URI.create(base + WELL_KNOWN_PATH + normalizePath(resource.getRawPath())); 065 } 066 067 /** 068 * Returns the root-form PRM URI for the supplied resource identifier — the ordered fallback location 069 * (SEP-2351). 070 * 071 * @param resource The canonical resource identifier. Must not be <jk>null</jk> and must be absolute (scheme + 072 * authority). 073 * @return The root PRM document URI. 074 * @throws IllegalArgumentException If {@code resource} is <jk>null</jk> or is missing a scheme/authority. 075 */ 076 public static URI rootMetadataUri(URI resource) { 077 return URI.create(baseOf(resource) + WELL_KNOWN_PATH); 078 } 079 080 /** 081 * Returns the ordered PRM lookup candidates — the path-inserted URI first, then the root fallback (SEP-2351). 082 * 083 * <p> 084 * When the resource has no path component the two coincide and a single-element list is returned. 085 * 086 * @param resource The canonical resource identifier. Must not be <jk>null</jk> and must be absolute (scheme + 087 * authority). 088 * @return The ordered candidate list (1 or 2 entries). Never <jk>null</jk>. 089 * @throws IllegalArgumentException If {@code resource} is <jk>null</jk> or is missing a scheme/authority. 090 */ 091 public static List<URI> candidates(URI resource) { 092 var pathInserted = metadataUri(resource); 093 var root = rootMetadataUri(resource); 094 return pathInserted.equals(root) ? List.of(pathInserted) : List.of(pathInserted, root); 095 } 096 097 /** 098 * Returns the servlet-relative request path at which this server hosts its PRM document for the supplied resource 099 * identifier — i.e. {@link #metadataUri(URI)}'s path component. 100 * 101 * @param resource The canonical resource identifier. Must not be <jk>null</jk>. 102 * @return The request path (e.g. {@code /.well-known/oauth-protected-resource/mcp}). 103 * @throws IllegalArgumentException If {@code resource} is <jk>null</jk>. 104 */ 105 public static String wellKnownRequestPath(URI resource) { 106 reqnn("resource", resource); 107 return WELL_KNOWN_PATH + normalizePath(resource.getRawPath()); 108 } 109 110 private static String baseOf(URI resource) { 111 reqnn("resource", resource); 112 var scheme = resource.getScheme(); 113 var authority = resource.getRawAuthority(); 114 if (scheme == null || authority == null) 115 throw iaex("resource %s must be an absolute URI with a scheme and authority", resource); 116 return scheme + "://" + authority; 117 } 118 119 private static String normalizePath(String rawPath) { 120 if (ie(rawPath)) 121 return ""; 122 var p = rawPath; 123 while (p.endsWith("/")) 124 p = p.substring(0, p.length() - 1); 125 if (p.isEmpty()) 126 return ""; 127 return p.startsWith("/") ? p : "/" + p; 128 } 129}