feat(ext-data): add unified Data gateway; mcp: derive roles claim from OIDC
CI / Build & Test (push) Canceled after 1m28s
CI / Build & Test (pull_request) Canceled after 16s

Data/RepositoryFactory/HibernateData give applications one cached,
stateless entry point for repositories per entity type instead of
per-request instantiation, with write()/afterCommit() replacing manual
transaction+reload choreography.

McpConfig.rolesClaimPath is removed - MCP now derives the claim path
from OidcMiddleware.rolesClaimPath() so applications never duplicate
the roles-claim config between OIDC and MCP. McpPackageScanner is
rebuilt on the shared PackageScanner.discover(packageName) primitive,
doing only McpTool/McpResource/McpPrompt classification itself.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Zakaria El Orche
2026-08-12 18:37:30 +00:00
co-authored by Claude Sonnet 5
parent 891ef99b8e
commit 9e045287c1
12 changed files with 161 additions and 107 deletions
@@ -25,7 +25,7 @@ import java.util.jar.JarFile;
* class cannot be loaded, an {@link InitializationException} is thrown immediately.
* A clear crash at boot is always preferable to a server that starts "empty".
*/
final class PackageScanner {
public final class PackageScanner {
private PackageScanner() {}
@@ -41,6 +41,60 @@ final class PackageScanner {
return scan(packageName).httpHandlers();
}
/**
* Shared deterministic classpath discovery primitive for Flash extensions. It owns the
* directory/JAR traversal, anonymous-class exclusion and load diagnostics; extensions only
* classify the returned classes according to their own component contract.
*/
public static List<Class<?>> discover(String packageName) {
if (packageName == null || packageName.isBlank())
throw new InitializationException("scan() called with null or blank package name");
String resourcePath = packageName.replace('.', '/');
ClassLoader cl = Thread.currentThread().getContextClassLoader();
List<Class<?>> result = new ArrayList<>();
List<String> errors = new ArrayList<>();
boolean found = false;
try {
Enumeration<URL> resources = cl.getResources(resourcePath);
while (resources.hasMoreElements()) {
found = true;
URL url = resources.nextElement();
if ("file".equals(url.getProtocol())) discoverDirectory(new File(url.toURI()), packageName, cl, result, errors);
else if ("jar".equals(url.getProtocol())) {
String path = url.getPath();
try (JarFile jar = new JarFile(path.substring(path.indexOf("file:") + 5, path.indexOf('!')))) {
discoverJar(jar, resourcePath, cl, result, errors);
}
}
}
} catch (Exception e) { throw new InitializationException("Failed to scan package: " + packageName, e); }
if (!found) throw new InitializationException("scan(\"" + packageName + "\") — package not found on classpath");
if (!errors.isEmpty()) throw new InitializationException("scan(\"" + packageName + "\") — failed to load classes:\n • " + String.join("\n • ", errors));
return List.copyOf(result);
}
private static void discoverDirectory(File dir, String packageName, ClassLoader cl, List<Class<?>> result, List<String> errors) {
File[] files = dir.listFiles(); if (files == null) return;
for (File file : files) {
if (file.isDirectory()) discoverDirectory(file, packageName + '.' + file.getName(), cl, result, errors);
else if (file.getName().endsWith(".class") && !isAnonymous(file.getName())) discoverClass(packageName + '.' + file.getName().replace(".class", ""), cl, result, errors);
}
}
private static void discoverJar(JarFile jar, String resourcePath, ClassLoader cl, List<Class<?>> result, List<String> errors) {
String prefix = resourcePath + "/";
Enumeration<JarEntry> entries = jar.entries();
while (entries.hasMoreElements()) {
String name = entries.nextElement().getName();
if (name.startsWith(prefix) && name.endsWith(".class") && !isAnonymous(name)) discoverClass(name.replace('/', '.').replace(".class", ""), cl, result, errors);
}
}
private static void discoverClass(String name, ClassLoader cl, List<Class<?>> result, List<String> errors) {
try { result.add(cl.loadClass(name)); }
catch (ClassNotFoundException | LinkageError e) { errors.add(name + "" + e.getMessage()); }
}
static ScanResult scan(String packageName) {
if (packageName == null || packageName.isBlank())
throw new InitializationException("scan() called with null or blank package name");