Skip to content

Commit 0069321

Browse files
committed
Register MCP components at compile time via McpToolModelProcessor instead of runtime reflection and classpath scanning, improving startup and simplifying the server bootstrap path.
1 parent 6ba7b20 commit 0069321

52 files changed

Lines changed: 2600 additions & 2679 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

pom.xml

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,6 @@
6262
<logback.version>1.5.33</logback.version>
6363
<mcp-sdk.version>2.0.0-M3</mcp-sdk.version>
6464
<mockito.version>5.23.0</mockito.version>
65-
<reflections.version>0.10.2</reflections.version>
6665
</properties>
6766

6867
<dependencyManagement>
@@ -111,11 +110,6 @@
111110
<version>${mockito.version}</version>
112111
<scope>test</scope>
113112
</dependency>
114-
<dependency>
115-
<groupId>org.reflections</groupId>
116-
<artifactId>reflections</artifactId>
117-
<version>${reflections.version}</version>
118-
</dependency>
119113
</dependencies>
120114

121115
<build>
@@ -184,6 +178,19 @@
184178
<arg>-Xlint:deprecation</arg>
185179
</compilerArgs>
186180
</configuration>
181+
<executions>
182+
<execution>
183+
<id>default-compile</id>
184+
<configuration>
185+
<!--
186+
This module ships annotation processors for downstream consumers.
187+
Disable processor execution while compiling this module itself to avoid
188+
service-loader bootstrap failures before processor classes are compiled.
189+
-->
190+
<proc>none</proc>
191+
</configuration>
192+
</execution>
193+
</executions>
187194
</plugin>
188195
<plugin>
189196
<groupId>org.apache.maven.plugins</groupId>
Lines changed: 108 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,77 +1,144 @@
11
package com.github.thought2code.mcp.annotated;
22

3-
import com.github.thought2code.mcp.annotated.reflect.ReflectionsProvider;
4-
import com.github.thought2code.mcp.annotated.server.component.DefaultMcpComponentInstanceFactory;
5-
import com.github.thought2code.mcp.annotated.server.component.McpComponentInstanceFactory;
6-
import java.lang.annotation.Annotation;
7-
import java.lang.reflect.Field;
8-
import java.lang.reflect.Method;
3+
import com.github.thought2code.mcp.annotated.annotation.McpServerApplication;
4+
import com.github.thought2code.mcp.annotated.enums.McpServerError;
5+
import com.github.thought2code.mcp.annotated.exception.McpServerException;
6+
import com.github.thought2code.mcp.annotated.util.StringHelper;
97
import java.util.Objects;
10-
import java.util.Set;
8+
import java.util.concurrent.ConcurrentHashMap;
9+
import java.util.concurrent.ConcurrentMap;
1110

1211
/**
1312
* Runtime context for one annotated MCP application.
1413
*
15-
* <p>The context owns application-scoped services such as classpath scanning. This keeps
16-
* independent MCP applications from sharing global mutable state when they run in the same JVM.
14+
* <p>In the build-time compiled architecture, this context no longer performs classpath scanning.
15+
* Instead, it centralizes runtime concerns that compiled invokers still need:
16+
*
17+
* <ul>
18+
* <li>Component instance lifecycle (lazy creation with one cached instance per component class)
19+
* <li>Scope filtering for compiled model definitions based on the resolved application base
20+
* package
21+
* </ul>
22+
*
23+
* <p>Each application gets its own context instance so multiple MCP applications can run in the
24+
* same JVM without sharing component instances or scope rules.
1725
*/
1826
public final class McpApplicationContext {
19-
/** The reflections provider used to scan the application's classpath for annotated components. */
20-
private final ReflectionsProvider reflectionsProvider;
27+
/** Base package resolved from {@link McpServerApplication}. */
28+
private final String basePackage;
2129

22-
/** The factory used to create or locate annotated component instances. */
23-
private final McpComponentInstanceFactory componentInstanceFactory;
30+
/** Component instance cache; one instance per component class. */
31+
private final ConcurrentMap<Class<?>, Object> componentInstances = new ConcurrentHashMap<>();
2432

25-
private McpApplicationContext(
26-
ReflectionsProvider reflectionsProvider,
27-
McpComponentInstanceFactory componentInstanceFactory) {
28-
this.reflectionsProvider =
29-
Objects.requireNonNull(reflectionsProvider, "reflectionsProvider must not be null");
30-
this.componentInstanceFactory =
31-
Objects.requireNonNull(
32-
componentInstanceFactory, "componentInstanceFactory must not be null");
33+
private McpApplicationContext(String basePackage) {
34+
this.basePackage = Objects.requireNonNull(basePackage, "basePackage must not be null");
3335
}
3436

3537
/**
3638
* Creates a new context for the specified MCP application class.
3739
*
38-
* @param mainClass the application entry class used for package scanning
40+
* <p>The resolved base package follows this priority:
41+
*
42+
* <ol>
43+
* <li>{@link McpServerApplication#basePackageClass()} when explicitly set
44+
* <li>{@link McpServerApplication#basePackage()} when non-blank
45+
* <li>the package of {@code mainClass} as fallback
46+
* </ol>
47+
*
48+
* @param mainClass the application entry class used to resolve base-package scope
3949
* @return a new application-scoped context
4050
*/
4151
public static McpApplicationContext from(Class<?> mainClass) {
42-
ReflectionsProvider reflectionsProvider =
43-
ReflectionsProvider.initializeReflectionsInstance(mainClass);
44-
return new McpApplicationContext(
45-
reflectionsProvider, DefaultMcpComponentInstanceFactory.create());
52+
Objects.requireNonNull(mainClass, "mainClass must not be null");
53+
return new McpApplicationContext(resolveBasePackage(mainClass));
4654
}
4755

4856
/**
49-
* Returns all methods annotated with the specified annotation within this context's scan scope.
57+
* Returns a component instance for the specified component class.
58+
*
59+
* <p>Instances are created lazily via the no-argument constructor and cached per declaring class.
60+
* Repeated calls for the same class return the same instance.
5061
*
51-
* @param annotation the annotation class to search for
52-
* @return methods annotated with the given annotation
62+
* @param componentClass the class declaring annotated MCP component methods
63+
* @return the component instance to invoke
5364
*/
54-
public Set<Method> getMethodsAnnotatedWith(Class<? extends Annotation> annotation) {
55-
return reflectionsProvider.getMethodsAnnotatedWith(annotation);
65+
public Object getComponentInstance(Class<?> componentClass) {
66+
Objects.requireNonNull(componentClass, "componentClass must not be null");
67+
return componentInstances.computeIfAbsent(
68+
componentClass, McpApplicationContext::createInstance);
5669
}
5770

5871
/**
59-
* Returns all fields annotated with the specified annotation within this context's scan scope.
72+
* Returns whether a compiled source method belongs to this application's base package scope.
73+
*
74+
* <p>This method is used while loading compiled definitions to prevent out-of-scope components
75+
* from being registered (for example, fixtures from other test packages or neighboring modules).
6076
*
61-
* @param annotation the annotation class to search for
62-
* @return fields annotated with the given annotation
77+
* @param sourceMethod compiled source method descriptor in the form {@code fqcn#method(...)}
78+
* @return {@code true} when the source method declaring class is within the configured base
79+
* package
6380
*/
64-
public Set<Field> getFieldsAnnotatedWith(Class<? extends Annotation> annotation) {
65-
return reflectionsProvider.getFieldsAnnotatedWith(annotation);
81+
public boolean isInScope(String sourceMethod) {
82+
if (StringHelper.isBlank(sourceMethod)) {
83+
return false;
84+
}
85+
86+
final int hashIndex = sourceMethod.indexOf(StringHelper.HASH);
87+
final String declaringClassName =
88+
(hashIndex >= 0 ? sourceMethod.substring(0, hashIndex) : sourceMethod).trim();
89+
if (declaringClassName.isEmpty()) {
90+
return false;
91+
}
92+
93+
if (basePackage.isEmpty()) {
94+
return !declaringClassName.contains(StringHelper.DOT);
95+
}
96+
return declaringClassName.startsWith(basePackage + StringHelper.DOT);
6697
}
6798

6899
/**
69-
* Returns a component instance for the specified component class.
100+
* Resolves the base package used for compiled-definition scope filtering.
70101
*
71-
* @param componentClass the class declaring annotated MCP component methods
72-
* @return the component instance to invoke
102+
* <p>Resolution priority:
103+
*
104+
* <ol>
105+
* <li>{@link McpServerApplication#basePackageClass()} when explicitly configured
106+
* <li>{@link McpServerApplication#basePackage()} when non-blank
107+
* <li>{@code mainClass.getPackageName()} as fallback
108+
* </ol>
109+
*
110+
* @param mainClass application entry class
111+
* @return resolved base package (never {@code null})
73112
*/
74-
public Object getComponentInstance(Class<?> componentClass) {
75-
return componentInstanceFactory.getInstance(componentClass);
113+
private static String resolveBasePackage(Class<?> mainClass) {
114+
McpServerApplication application = mainClass.getAnnotation(McpServerApplication.class);
115+
if (application == null) {
116+
return mainClass.getPackageName();
117+
}
118+
119+
if (application.basePackageClass() != Object.class) {
120+
return application.basePackageClass().getPackageName();
121+
}
122+
123+
final String configuredBasePackage = application.basePackage().trim();
124+
return configuredBasePackage.isBlank() ? mainClass.getPackageName() : configuredBasePackage;
125+
}
126+
127+
/**
128+
* Creates a component instance using the class no-argument constructor.
129+
*
130+
* <p>This method is used by the context cache on first access for each component class.
131+
*
132+
* @param clazz component class declaring MCP annotated methods
133+
* @return created component instance
134+
* @throws McpServerException when instance creation fails
135+
*/
136+
private static Object createInstance(Class<?> clazz) {
137+
try {
138+
return clazz.getDeclaredConstructor().newInstance();
139+
} catch (Exception e) {
140+
throw new McpServerException(
141+
McpServerError.COMPONENT_INSTANCE_CREATE_ERROR.withDetail(clazz.getName()), e);
142+
}
76143
}
77144
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package com.github.thought2code.mcp.annotated.compiled.completion;
2+
3+
import io.modelcontextprotocol.spec.McpSchema;
4+
5+
/**
6+
* Build-time compiled MCP completion definition.
7+
*
8+
* @param sourceMethod source method descriptor used for diagnostics
9+
* @param reference MCP completion reference
10+
* @param invoker generated completion invoker
11+
*/
12+
public record CompiledCompletionDefinition(
13+
String sourceMethod, McpSchema.CompleteReference reference, CompiledCompletionInvoker invoker) {
14+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package com.github.thought2code.mcp.annotated.compiled.completion;
2+
3+
import com.github.thought2code.mcp.annotated.McpApplicationContext;
4+
import com.github.thought2code.mcp.annotated.reflect.Invocation;
5+
import io.modelcontextprotocol.spec.McpSchema;
6+
7+
/** Strongly-typed invocation contract generated at build time for one completion method. */
8+
@FunctionalInterface
9+
public interface CompiledCompletionInvoker {
10+
/**
11+
* Invokes one compiled completion method.
12+
*
13+
* @param context application context
14+
* @param argument completion argument from request
15+
* @return invocation result
16+
*/
17+
Invocation invoke(
18+
McpApplicationContext context, McpSchema.CompleteRequest.CompleteArgument argument);
19+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package com.github.thought2code.mcp.annotated.compiled.completion;
2+
3+
import com.github.thought2code.mcp.annotated.McpApplicationContext;
4+
import com.github.thought2code.mcp.annotated.compiled.spi.McpCompiledModelProvider;
5+
import com.github.thought2code.mcp.annotated.exception.McpServerComponentRegistrationException;
6+
import com.github.thought2code.mcp.annotated.server.component.McpCompleteCompletion;
7+
import io.modelcontextprotocol.server.McpServerFeatures;
8+
import io.modelcontextprotocol.spec.McpSchema;
9+
import java.util.ArrayList;
10+
import java.util.List;
11+
import java.util.ServiceLoader;
12+
import reactor.core.publisher.Mono;
13+
14+
/** Builds completion specifications from build-time compiled completion models. */
15+
public final class CompiledCompletionSupport {
16+
17+
private CompiledCompletionSupport() {}
18+
19+
public static List<McpServerFeatures.SyncCompletionSpecification> allSync(
20+
McpApplicationContext context) {
21+
return allSync(context, ServiceLoader.load(McpCompiledModelProvider.class));
22+
}
23+
24+
static List<McpServerFeatures.SyncCompletionSpecification> allSync(
25+
McpApplicationContext context, Iterable<McpCompiledModelProvider> providers) {
26+
List<CompiledCompletionDefinition> definitions = loadDefinitions(providers, context);
27+
List<McpServerFeatures.SyncCompletionSpecification> completions = new ArrayList<>();
28+
for (CompiledCompletionDefinition definition : definitions) {
29+
completions.add(
30+
new McpServerFeatures.SyncCompletionSpecification(
31+
definition.reference(), (exchange, request) -> invoke(definition, context, request)));
32+
}
33+
return completions;
34+
}
35+
36+
public static List<McpServerFeatures.AsyncCompletionSpecification> allAsync(
37+
McpApplicationContext context) {
38+
return allAsync(context, ServiceLoader.load(McpCompiledModelProvider.class));
39+
}
40+
41+
static List<McpServerFeatures.AsyncCompletionSpecification> allAsync(
42+
McpApplicationContext context, Iterable<McpCompiledModelProvider> providers) {
43+
List<CompiledCompletionDefinition> definitions = loadDefinitions(providers, context);
44+
List<McpServerFeatures.AsyncCompletionSpecification> completions = new ArrayList<>();
45+
for (CompiledCompletionDefinition definition : definitions) {
46+
completions.add(
47+
new McpServerFeatures.AsyncCompletionSpecification(
48+
definition.reference(),
49+
(exchange, request) ->
50+
Mono.fromCallable(() -> invoke(definition, context, request))));
51+
}
52+
return completions;
53+
}
54+
55+
private static List<CompiledCompletionDefinition> loadDefinitions(
56+
Iterable<McpCompiledModelProvider> providers, McpApplicationContext context) {
57+
List<CompiledCompletionDefinition> definitions = new ArrayList<>();
58+
for (McpCompiledModelProvider provider : providers) {
59+
for (CompiledCompletionDefinition definition : provider.completions()) {
60+
if (context.isInScope(definition.sourceMethod())) {
61+
definitions.add(definition);
62+
}
63+
}
64+
}
65+
return definitions;
66+
}
67+
68+
private static McpSchema.CompleteResult invoke(
69+
CompiledCompletionDefinition definition,
70+
McpApplicationContext context,
71+
McpSchema.CompleteRequest request) {
72+
var invocation = definition.invoker().invoke(context, request.argument());
73+
if (invocation.isError()) {
74+
throw new McpServerComponentRegistrationException(
75+
"Completion invocation failed for " + definition.sourceMethod());
76+
}
77+
Object raw = invocation.result();
78+
if (!(raw instanceof McpCompleteCompletion completion)) {
79+
throw new McpServerComponentRegistrationException(
80+
"Completion method must return McpCompleteCompletion: " + definition.sourceMethod());
81+
}
82+
return new McpSchema.CompleteResult(
83+
new McpSchema.CompleteResult.CompleteCompletion(
84+
completion.values(), completion.total(), completion.hasMore()));
85+
}
86+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package com.github.thought2code.mcp.annotated.compiled.prompt;
2+
3+
import io.modelcontextprotocol.spec.McpSchema;
4+
5+
/**
6+
* Build-time compiled MCP prompt definition.
7+
*
8+
* @param sourceMethod source method descriptor used for diagnostics
9+
* @param prompt MCP prompt metadata
10+
* @param description resolved prompt description
11+
* @param invoker generated prompt invoker
12+
*/
13+
public record CompiledPromptDefinition(
14+
String sourceMethod,
15+
McpSchema.Prompt prompt,
16+
String description,
17+
CompiledPromptInvoker invoker) {
18+
19+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package com.github.thought2code.mcp.annotated.compiled.prompt;
2+
3+
import com.github.thought2code.mcp.annotated.McpApplicationContext;
4+
import com.github.thought2code.mcp.annotated.reflect.Invocation;
5+
import java.util.Map;
6+
7+
/** Strongly-typed invocation contract generated at build time for one {@code @McpPrompt} method. */
8+
@FunctionalInterface
9+
public interface CompiledPromptInvoker {
10+
/**
11+
* Invokes one compiled prompt method using request arguments.
12+
*
13+
* @param context application context
14+
* @param arguments request arguments
15+
* @return invocation result
16+
*/
17+
Invocation invoke(McpApplicationContext context, Map<String, Object> arguments);
18+
}

0 commit comments

Comments
 (0)