001/*
002 * Copyright 2022-2026 Revetware LLC.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package com.soklet;
018
019import com.soklet.Soklet.MockHttpServer;
020import com.soklet.Soklet.MockSseServer;
021import com.soklet.converter.ValueConverterRegistry;
022import org.jspecify.annotations.NonNull;
023import org.jspecify.annotations.Nullable;
024
025import javax.annotation.concurrent.NotThreadSafe;
026import javax.annotation.concurrent.ThreadSafe;
027import java.util.Collection;
028import java.util.concurrent.atomic.AtomicBoolean;
029import java.util.function.Consumer;
030
031import static com.soklet.internal.ObjectIdentity.sameInstance;
032import static java.util.Objects.requireNonNull;
033
034/**
035 * Opaque, single-use execution configuration consumed by one
036 * {@link SokletSimulator} run attempt.
037 * <p>
038 * Each {@link #builder()} and
039 * {@link #withSokletConfig(SokletConfig)} owns a fresh set of off-network
040 * transports. A successful {@link Builder#build()} seals that transport graph, and
041 * {@link SokletSimulator#run(SimulatorConfig, SokletSimulator.Simulation)}
042 * atomically claims the completed configuration before lifecycle work begins.
043 * Sequential or concurrent reuse is rejected. Create a new configuration for
044 * each simulation run.
045 * <p>
046 * Instances are thread-safe, but deliberately stateful: claiming a run is an
047 * irreversible operation. The configuration intentionally exposes no public
048 * accessors; configured transports are available from the run's
049 * {@link Simulator}.
050 */
051@ThreadSafe
052public final class SimulatorConfig {
053        @NonNull
054        private final SokletConfig sokletConfig;
055        @NonNull
056        private final SimulatorOptions simulatorOptions;
057        @NonNull
058        private final ConfigurationGraph configurationGraph;
059        @NonNull
060        private final AtomicBoolean runClaimed;
061
062        private SimulatorConfig(@NonNull Builder builder) {
063                Builder exactBuilder = requireNonNull(builder);
064                this.sokletConfig = exactBuilder.sokletConfigBuilder.build();
065                this.simulatorOptions = exactBuilder.simulatorOptions == null
066                                ? SimulatorOptions.defaultInstance()
067                                : exactBuilder.simulatorOptions;
068                this.configurationGraph = exactBuilder.configurationGraph;
069                this.runClaimed = new AtomicBoolean();
070        }
071
072        /**
073         * Creates a builder backed by a fresh off-network transport graph.
074         *
075         * @return a fresh simulator-configuration builder
076         */
077        @NonNull
078        public static Builder builder() {
079                return new Builder();
080        }
081
082        /**
083         * Creates a builder by importing the application settings and transport
084         * shape of an existing Soklet configuration.
085         * <p>
086         * The returned builder owns a fresh off-network counterpart for each HTTP,
087         * SSE, or MCP transport present in the source; it never reuses or changes the
088         * source configuration's transport instances.
089         * Every transport present in the source remains present in the imported
090         * shape. Use {@link #builder()} instead when a test must omit one.
091         * Explicitly configured application collaborators are reused by identity,
092         * while unset defaults that depend on the completed configuration are
093         * derived again for the simulator configuration. MCP collaborators such as
094 * admission controllers, interceptors, localizers, rate limiters, the task
095 * manager, and its task-event publisher are also reused by identity; stateful
096 * ones can be replaced through
097         * {@link Builder#configureMcpServer(Consumer)}. Later calls on the returned
098         * builder override imported settings.
099         * <p>
100         * Derivation is transport-isolated, not a deep copy of the application object
101         * graph. A dependency-injection provider or other collaborator that captured
102         * a source transport is not inspected or rebound and continues to reference
103         * that source object. Supply test-scoped collaborators explicitly when that
104         * distinction matters.
105         * <p>
106         * An imported MCP server is reconstructed from its original build settings.
107         * Runtime control-plane changes made after that server was built, such as key
108         * rotation or localization invalidation, are deliberately not copied.
109         * HTTP and SSE streaming lifecycle capacity, callback concurrency, and cleanup
110         * timeout are copied from Soklet's built-in servers. The SSE connection queue
111         * capacity is also copied. Custom transports use the simulator's defaults.
112         *
113         * @param sokletConfig source application configuration
114         * @return a fresh, transport-isolated simulator-configuration builder
115         */
116        @NonNull
117        public static Builder withSokletConfig(
118                        @NonNull SokletConfig sokletConfig) {
119                return new Builder(requireNonNull(sokletConfig));
120        }
121
122        /**
123         * Creates a completed, single-use simulator configuration from an existing
124         * Soklet configuration.
125         * <p>
126         * This is equivalent to {@code withSokletConfig(sokletConfig).build()}.
127         * Each invocation creates a fresh off-network transport graph, so the source
128         * configuration may be used to derive any number of transport-isolated
129         * simulator configurations. Application and MCP collaborators are still
130         * shared by identity as described by
131         * {@link #withSokletConfig(SokletConfig)}.
132         *
133         * @param sokletConfig source application configuration
134         * @return a fresh, completed, transport-isolated simulator configuration
135         */
136        @NonNull
137        public static SimulatorConfig fromSokletConfig(
138                        @NonNull SokletConfig sokletConfig) {
139                return withSokletConfig(sokletConfig).build();
140        }
141
142        @NonNull
143        SokletConfig getSokletConfig() {
144                return this.sokletConfig;
145        }
146
147        @NonNull
148        SimulatorOptions getSimulatorOptions() {
149                return this.simulatorOptions;
150        }
151
152        void claimForRun() {
153                if (!this.runClaimed.compareAndSet(false, true))
154                        throw new IllegalStateException(
155                                        "The simulator configuration has already been claimed by a run");
156        }
157
158        @NonNull
159        Object configurationIdentity() {
160                return this.configurationGraph.configurationIdentity();
161        }
162
163        boolean belongsTo(@NonNull Object configurationIdentity) {
164                return sameInstance(this.configurationGraph.configurationIdentity(),
165                                requireNonNull(configurationIdentity));
166        }
167
168        @NonNull
169        MockHttpServer simulatedHttpServer() {
170                return this.configurationGraph.httpServer();
171        }
172
173        @NonNull
174        MockSseServer simulatedSseServer() {
175                return this.configurationGraph.sseServer();
176        }
177
178        @Nullable
179        DefaultMcpServer simulatedMcpServer() {
180                return this.configurationGraph.mcpServer();
181        }
182
183        /**
184         * Builds a single-use configuration against fresh off-network transports.
185         * <p>
186         * A builder is sealed after a successful {@link #build()}. Builders are not
187         * thread-safe and must not be retained after building.
188         */
189        @NotThreadSafe
190        public static final class Builder {
191                @NonNull
192                private final ConfigurationGraph configurationGraph;
193                private final SokletConfig.@NonNull Builder sokletConfigBuilder;
194                @Nullable
195                private SimulatorOptions simulatorOptions;
196                @Nullable
197                private DefaultMcpServer importedMcpServer;
198                private boolean built;
199                private int activeTransportConfigurers;
200
201                private Builder() {
202                        this.configurationGraph = new ConfigurationGraph(null, null);
203                        this.sokletConfigBuilder = new SokletConfig.Builder();
204                }
205
206                private Builder(@NonNull SokletConfig sokletConfig) {
207                        SokletConfig exactConfig = requireNonNull(sokletConfig);
208                        this.configurationGraph = new ConfigurationGraph(exactConfig.getHttpServer().orElse(null),
209                                        exactConfig.getSseServer().orElse(null));
210                        this.sokletConfigBuilder = new SokletConfig.Builder();
211                        exactConfig.applyApplicationSettingsTo(this.sokletConfigBuilder);
212                        if (exactConfig.getHttpServer().isPresent())
213                                httpServer();
214                        if (exactConfig.getSseServer().isPresent())
215                                sseServer();
216                        this.importedMcpServer = exactConfig.getMcpServer()
217                                        .map(server -> (DefaultMcpServer) server)
218                                        .orElse(null);
219                }
220
221                /**
222                 * Adds this run's fresh simulated HTTP server.
223                 *
224                 * @return this builder
225                 */
226                @NonNull
227                public Builder httpServer() {
228                        requireMutable();
229                        this.sokletConfigBuilder.httpServer(
230                                        this.configurationGraph.httpServer());
231                        return this;
232                }
233
234                /**
235                 * Adds this run's fresh simulated HTTP server and supplies that exact
236                 * server for configuration-time application dependency wiring. For access
237                 * from the simulation body, prefer {@link Simulator#getHttpServer()}.
238                 * The consumer runs synchronously during this call and must not retain the
239                 * server for another configuration.
240                 *
241                 * @param httpServerConsumer receives this run's HTTP server
242                 * @return this builder
243                 */
244                @NonNull
245                public Builder httpServer(
246                                @NonNull Consumer<@NonNull HttpServer> httpServerConsumer) {
247                        Consumer<@NonNull HttpServer> exactConsumer =
248                                        requireNonNull(httpServerConsumer);
249                        requireMutable();
250                        beginTransportConfigurer();
251                        try {
252                                exactConsumer.accept(this.configurationGraph.httpServer());
253                        } finally {
254                                endTransportConfigurer();
255                        }
256                        requireMutable();
257                        this.sokletConfigBuilder.httpServer(
258                                        this.configurationGraph.httpServer());
259                        return this;
260                }
261
262                /**
263                 * Adds this run's fresh simulated Server-Sent Events server.
264                 *
265                 * @return this builder
266                 */
267                @NonNull
268                public Builder sseServer() {
269                        requireMutable();
270                        this.sokletConfigBuilder.sseServer(
271                                        this.configurationGraph.sseServer());
272                        return this;
273                }
274
275                /**
276                 * Adds this run's fresh simulated Server-Sent Events server and supplies
277                 * that exact server for configuration-time application dependency wiring.
278                 * For access from the simulation body, prefer
279                 * {@link Simulator#getSseServer()}. The consumer runs synchronously during
280                 * this call and must not retain the server for another configuration.
281                 *
282                 * @param sseServerConsumer receives this run's SSE server
283                 * @return this builder
284                 */
285                @NonNull
286                public Builder sseServer(
287                                @NonNull Consumer<@NonNull SseServer> sseServerConsumer) {
288                        Consumer<@NonNull SseServer> exactConsumer =
289                                        requireNonNull(sseServerConsumer);
290                        requireMutable();
291                        beginTransportConfigurer();
292                        try {
293                                exactConsumer.accept(this.configurationGraph.sseServer());
294                        } finally {
295                                endTransportConfigurer();
296                        }
297                        requireMutable();
298                        this.sokletConfigBuilder.sseServer(
299                                        this.configurationGraph.sseServer());
300                        return this;
301                }
302
303                /**
304                 * Configures, builds, and adds one fresh simulated MCP server.
305                 * If this builder was created by
306                 * {@link SimulatorConfig#withSokletConfig(SokletConfig)} and the source
307                 * configuration contains an MCP server, configuration begins with that
308                 * server's construction settings. Otherwise, configuration begins with
309                 * {@link McpServer#withPort(Integer)} defaults and logical port {@code 0}.
310                 * The logical port may be changed with
311                 * {@link McpServer.Builder#port(Integer)}.
312                 * <p>
313                 * The consumer runs synchronously and must only customize the supplied
314                 * builder. This outer builder owns the call to
315                 * {@link McpServer.Builder#build()}; calling it from the consumer or
316                 * retaining the builder for later use is rejected.
317                 *
318                 * @param mcpServerConfigurer customizes this configuration's MCP builder
319                 * @return this builder
320                 */
321                @NonNull
322                public Builder configureMcpServer(
323                                @NonNull Consumer<McpServer.@NonNull Builder>
324                                                mcpServerConfigurer) {
325                        Consumer<McpServer.@NonNull Builder> exactConfigurer =
326                                        requireNonNull(mcpServerConfigurer);
327                        requireMutable();
328                        DefaultMcpServer exactImportedMcpServer = this.importedMcpServer;
329                        McpBuilderLease lease = exactImportedMcpServer == null
330                                        ? this.configurationGraph.openMcpBuilder(0)
331                                        : this.configurationGraph.openMcpBuilder(
332                                                        exactImportedMcpServer);
333                        return configureAndBuildMcpServer(lease, exactConfigurer);
334                }
335
336                @NonNull
337                private Builder configureAndBuildMcpServer(
338                                @NonNull McpBuilderLease lease,
339                                @NonNull Consumer<McpServer.@NonNull Builder> configurer) {
340                        requireMutable();
341                        try {
342                                McpServer.Builder mcpServerBuilder = lease.builder();
343                                beginTransportConfigurer();
344                                try {
345                                        requireNonNull(configurer).accept(mcpServerBuilder);
346                                } finally {
347                                        endTransportConfigurer();
348                                }
349                                requireMutable();
350                                lease.finishConfiguration();
351                                McpServer mcpServer = mcpServerBuilder.build();
352                                requireMutable();
353                                this.sokletConfigBuilder.mcpServer(mcpServer);
354                                this.importedMcpServer = null;
355                                return this;
356                        } finally {
357                                lease.close();
358                        }
359                }
360
361                /**
362                 * Sets the startup and shutdown deadline policy shared by every configured
363                 * lifecycle component. Passing {@code null} restores the built-in default.
364                 *
365                 * @param lifecyclePolicy lifecycle policy, or {@code null} to use the default
366                 * @return this builder
367                 */
368                @NonNull
369                public Builder lifecyclePolicy(
370                                @Nullable LifecyclePolicy lifecyclePolicy) {
371                        requireMutable();
372                        this.sokletConfigBuilder.lifecyclePolicy(lifecyclePolicy);
373                        return this;
374                }
375
376                @NonNull
377                Builder internalLifecyclePolicy(
378                                @NonNull InternalLifecyclePolicy internalLifecyclePolicy) {
379                        requireMutable();
380                        this.sokletConfigBuilder.internalLifecyclePolicy(
381                                        requireNonNull(internalLifecyclePolicy));
382                        return this;
383                }
384
385                /**
386                 * Sets how Soklet creates application instances for HTTP, SSE, and MCP
387                 * handlers and application parameter values. The provider may be called
388                 * concurrently.
389                 *
390                 * @param instanceProvider instance provider, or {@code null} for the default
391                 * @return this builder
392                 */
393                @NonNull
394                public Builder instanceProvider(
395                                @Nullable InstanceProvider instanceProvider) {
396                        requireMutable();
397                        this.sokletConfigBuilder.instanceProvider(instanceProvider);
398                        return this;
399                }
400
401                /**
402                 * Sets the registry used for Java value conversions.
403                 *
404                 * @param valueConverterRegistry registry, or {@code null} for the default
405                 * @return this builder
406                 */
407                @NonNull
408                public Builder valueConverterRegistry(
409                                @Nullable ValueConverterRegistry valueConverterRegistry) {
410                        requireMutable();
411                        this.sokletConfigBuilder.valueConverterRegistry(valueConverterRegistry);
412                        return this;
413                }
414
415                /**
416                 * Sets how request bodies are converted to application values.
417                 *
418                 * @param requestBodyMarshaler marshaler, or {@code null} to derive one
419                 * @return this builder
420                 */
421                @NonNull
422                public Builder requestBodyMarshaler(
423                                @Nullable RequestBodyMarshaler requestBodyMarshaler) {
424                        requireMutable();
425                        this.sokletConfigBuilder.requestBodyMarshaler(requestBodyMarshaler);
426                        return this;
427                }
428
429                /**
430                 * Sets how Soklet discovers and resolves Resource Methods.
431                 *
432                 * @param resourceMethodResolver resolver, or {@code null} for the default
433                 * @return this builder
434                 */
435                @NonNull
436                public Builder resourceMethodResolver(
437                                @Nullable ResourceMethodResolver resourceMethodResolver) {
438                        requireMutable();
439                        this.sokletConfigBuilder.resourceMethodResolver(resourceMethodResolver);
440                        return this;
441                }
442
443                /**
444                 * Sets how parameters are supplied for Resource Method invocation.
445                 *
446                 * @param resourceMethodParameterProvider provider, or {@code null} for the
447                 * default
448                 * @return this builder
449                 */
450                @NonNull
451                public Builder resourceMethodParameterProvider(
452                                @Nullable ResourceMethodParameterProvider
453                                                resourceMethodParameterProvider) {
454                        requireMutable();
455                        this.sokletConfigBuilder.resourceMethodParameterProvider(
456                                        resourceMethodParameterProvider);
457                        return this;
458                }
459
460                /**
461                 * Sets how application response values are converted for transmission.
462                 *
463                 * @param responseMarshaler marshaler, or {@code null} for the default
464                 * @return this builder
465                 */
466                @NonNull
467                public Builder responseMarshaler(
468                                @Nullable ResponseMarshaler responseMarshaler) {
469                        requireMutable();
470                        this.sokletConfigBuilder.responseMarshaler(responseMarshaler);
471                        return this;
472                }
473
474                /**
475                 * Sets the interceptor around application request handling.
476                 *
477                 * @param requestInterceptor interceptor, or {@code null} for the default
478                 * @return this builder
479                 */
480                @NonNull
481                public Builder requestInterceptor(
482                                @Nullable RequestInterceptor requestInterceptor) {
483                        requireMutable();
484                        this.sokletConfigBuilder.requestInterceptor(requestInterceptor);
485                        return this;
486                }
487
488                /**
489                 * Replaces the configured lifecycle observers with one observer.
490                 *
491                 * @param lifecycleObserver observer, or {@code null} for no observers
492                 * @return this builder
493                 */
494                @NonNull
495                public Builder lifecycleObserver(
496                                @Nullable LifecycleObserver lifecycleObserver) {
497                        requireMutable();
498                        this.sokletConfigBuilder.lifecycleObserver(lifecycleObserver);
499                        return this;
500                }
501
502                /**
503                 * Replaces the configured lifecycle observers in iteration order.
504                 *
505                 * @param lifecycleObservers non-null observers, or {@code null} for no
506                 * observers; elements must be non-null
507                 * @return this builder
508                 */
509                @NonNull
510                public Builder lifecycleObservers(
511                                @Nullable Collection<? extends @NonNull LifecycleObserver>
512                                                lifecycleObservers) {
513                        requireMutable();
514                        this.sokletConfigBuilder.lifecycleObservers(lifecycleObservers);
515                        return this;
516                }
517
518                /**
519                 * Sets how Soklet records operational metrics.
520                 *
521                 * @param metricsCollector collector, or {@code null} for the default
522                 * @return this builder
523                 */
524                @NonNull
525                public Builder metricsCollector(
526                                @Nullable MetricsCollector metricsCollector) {
527                        requireMutable();
528                        this.sokletConfigBuilder.metricsCollector(metricsCollector);
529                        return this;
530                }
531
532                /**
533                 * Sets the authorizer for ordinary HTTP and SSE CORS processing.
534                 *
535                 * @param corsAuthorizer authorizer, or {@code null} to reject all
536                 * cross-origin requests
537                 * @return this builder
538                 */
539                @NonNull
540                public Builder corsAuthorizer(
541                                @Nullable CorsAuthorizer corsAuthorizer) {
542                        requireMutable();
543                        this.sokletConfigBuilder.corsAuthorizer(corsAuthorizer);
544                        return this;
545                }
546
547                /**
548                 * Sets simulator request and response behavior options.
549                 *
550                 * @param simulatorOptions options, or {@code null} for defaults
551                 * @return this builder
552                 */
553                @NonNull
554                public Builder simulatorOptions(
555                                @Nullable SimulatorOptions simulatorOptions) {
556                        requireMutable();
557                        this.simulatorOptions = simulatorOptions;
558                        return this;
559                }
560
561                /**
562                 * Builds the single-use configuration for one simulator run attempt.
563                 *
564                 * @return completed simulator configuration
565                 * @throws IllegalStateException if the builder is sealed, was already built,
566                 * or has no simulated server
567                 */
568                @NonNull
569                public SimulatorConfig build() {
570                        requireMutable();
571                        if (this.activeTransportConfigurers != 0)
572                                throw new IllegalStateException(
573                                                "A simulator configuration cannot be built from a transport configurer");
574                        if (this.importedMcpServer != null)
575                                configureMcpServer(ignored -> {
576                                });
577                        SimulatorConfig config = new SimulatorConfig(this);
578                        this.built = true;
579                        this.configurationGraph.seal();
580                        return config;
581                }
582
583                private void requireMutable() {
584                        if (this.built)
585                                throw new IllegalStateException(
586                                                "The simulator configuration has already been built");
587                        this.configurationGraph.requireOpen();
588                }
589
590                private void beginTransportConfigurer() {
591                        this.activeTransportConfigurers++;
592                }
593
594                private void endTransportConfigurer() {
595                        this.activeTransportConfigurers--;
596                }
597        }
598
599        @ThreadSafe
600        private static final class ConfigurationGraph {
601                @NonNull
602                private final Object configurationIdentity;
603                @NonNull
604                private final MockHttpServer httpServer;
605                @NonNull
606                private final MockSseServer sseServer;
607                private boolean open;
608                private @Nullable DefaultMcpServer mcpServer;
609                private @Nullable McpBuilderLease activeMcpBuilderLease;
610
611                private ConfigurationGraph(@Nullable HttpServer sourceHttpServer, @Nullable SseServer sourceSseServer) {
612                        this.configurationIdentity = new Object();
613                        this.httpServer = new MockHttpServer(sourceHttpServer);
614                        this.sseServer = new MockSseServer(sourceSseServer);
615                        this.open = true;
616                }
617
618                @NonNull
619                private Object configurationIdentity() {
620                        return this.configurationIdentity;
621                }
622
623                @NonNull
624                private MockHttpServer httpServer() {
625                        return this.httpServer;
626                }
627
628                @NonNull
629                private MockSseServer sseServer() {
630                        return this.sseServer;
631                }
632
633                @Nullable
634                private synchronized DefaultMcpServer mcpServer() {
635                        return this.mcpServer;
636                }
637
638                @NonNull
639                private synchronized McpBuilderLease openMcpBuilder(
640                                @NonNull Integer port) {
641                        return openMcpBuilder(new McpBuilderLease(this,
642                                        requireNonNull(port)));
643                }
644
645                @NonNull
646                private synchronized McpBuilderLease openMcpBuilder(
647                                @NonNull DefaultMcpServer sourceMcpServer) {
648                        return openMcpBuilder(new McpBuilderLease(this,
649                                        requireNonNull(sourceMcpServer)));
650                }
651
652                @NonNull
653                private McpBuilderLease openMcpBuilder(
654                                @NonNull McpBuilderLease lease) {
655                        requireOpen();
656                        if (this.mcpServer != null)
657                                throw new IllegalStateException(
658                                                "A simulator configuration may build at most one MCP server");
659                        if (this.activeMcpBuilderLease != null)
660                                throw new IllegalStateException(
661                                                "A simulator MCP builder is already active");
662                        this.activeMcpBuilderLease = requireNonNull(lease);
663                        return this.activeMcpBuilderLease;
664                }
665
666                private synchronized void verifyBuildAllowed(
667                                @NonNull McpBuilderLease lease) {
668                        if (this.activeMcpBuilderLease != requireNonNull(lease))
669                                throw new IllegalStateException(
670                                                "The simulator MCP builder is no longer active");
671                        requireOpen();
672                        if (!lease.buildAllowed())
673                                throw new IllegalStateException(
674                                                "Only SimulatorConfig.Builder may build the simulator MCP server");
675                        if (this.mcpServer != null)
676                                throw new IllegalStateException(
677                                                "A simulator configuration may build at most one MCP server");
678                }
679
680                private synchronized void register(@NonNull McpBuilderLease lease,
681                                @NonNull DefaultMcpServer server) {
682                        verifyBuildAllowed(lease);
683                        DefaultMcpServer registered = requireNonNull(server);
684                        registered.claimSimulatorScope(this);
685                        this.mcpServer = registered;
686                }
687
688                private synchronized void closeLease(@NonNull McpBuilderLease lease) {
689                        if (this.activeMcpBuilderLease == requireNonNull(lease))
690                                this.activeMcpBuilderLease = null;
691                }
692
693                private synchronized void seal() {
694                        this.open = false;
695                }
696
697                private synchronized void requireOpen() {
698                        if (!this.open)
699                                throw new IllegalStateException(
700                                                "The simulator configuration has been sealed");
701                }
702        }
703
704        @ThreadSafe
705        private static final class McpBuilderLease
706                        implements SimulatorMcpBuildRegistrar, AutoCloseable {
707                @NonNull
708                private final ConfigurationGraph configurationGraph;
709                private final McpServer.@NonNull Builder builder;
710                private volatile @Nullable Thread buildThread;
711
712                private McpBuilderLease(@NonNull ConfigurationGraph configurationGraph,
713                                @NonNull Integer port) {
714                        this.configurationGraph = requireNonNull(configurationGraph);
715                        this.builder = McpServer.withPort(requireNonNull(port))
716                                        .simulatorBuildRegistrar(this);
717                }
718
719                private McpBuilderLease(@NonNull ConfigurationGraph configurationGraph,
720                                @NonNull DefaultMcpServer sourceMcpServer) {
721                        this.configurationGraph = requireNonNull(configurationGraph);
722                        this.builder = requireNonNull(sourceMcpServer)
723                                        .copyBuilderForSimulator(this);
724                }
725
726                private McpServer.@NonNull Builder builder() {
727                        return this.builder;
728                }
729
730                private void finishConfiguration() {
731                        this.buildThread = Thread.currentThread();
732                }
733
734                private boolean buildAllowed() {
735                        return sameInstance(this.buildThread, Thread.currentThread());
736                }
737
738                @Override
739                public void verifyBuildAllowed() {
740                        this.configurationGraph.verifyBuildAllowed(this);
741                }
742
743                @Override
744                public void register(@NonNull DefaultMcpServer server) {
745                        this.configurationGraph.register(this, requireNonNull(server));
746                }
747
748                @Override
749                public void close() {
750                        this.buildThread = null;
751                        this.configurationGraph.closeLease(this);
752                }
753        }
754}