1: <?php
2:
3: declare(strict_types=1);
4:
5: /**
6: * This file is part of the Nexus MCP SDK package.
7: *
8: * (c) 2026 John Paul E. Balandan, CPA <paulbalandan@gmail.com>
9: *
10: * For the full copyright and license information, please view
11: * the LICENSE file that was distributed with this source code.
12: */
13:
14: namespace Nexus\Mcp\Server\Dispatch;
15:
16: use Nexus\Mcp\Core\Dispatch\InitializationState;
17: use Nexus\Mcp\Core\Schema\Request\InitializeRequest;
18: use Nexus\Mcp\Core\Schema\Request\PingRequest;
19:
20: /**
21: * Tracks the client handshake lifecycle and decides which inbound request
22: * methods may run before it completes.
23: */
24: final class ServerInitializationGate
25: {
26: private InitializationState $state = InitializationState::AwaitingInitialize;
27: private bool $pendingInitializedNotification = false;
28:
29: public function isInitialized(): bool
30: {
31: return InitializationState::Initialized === $this->state;
32: }
33:
34: /**
35: * @param non-empty-string $requestMethod
36: */
37: public function allowsRequest(string $requestMethod): bool
38: {
39: if (InitializeRequest::getMethod() === $requestMethod) {
40: return InitializationState::AwaitingInitialize === $this->state;
41: }
42:
43: return $this->isInitialized() || PingRequest::getMethod() === $requestMethod;
44: }
45:
46: /**
47: * Transitions `AwaitingInitialize` -> `InitializeInFlight`. Returns `true` if the transition fired.
48: */
49: public function markInitializeInFlight(): bool
50: {
51: if (InitializationState::AwaitingInitialize !== $this->state) {
52: return false;
53: }
54:
55: $this->state = InitializationState::InitializeInFlight;
56:
57: return true;
58: }
59:
60: /**
61: * From `InitializeInFlight`: if a `notifications/initialized` was buffered while the handler was
62: * still running, transition straight to `Initialized` (consuming the pending flag). Otherwise
63: * transition to `InitializeCompleted` to wait for the notification. Returns `true` if either
64: * transition fired.
65: */
66: public function markInitializeCompleted(): bool
67: {
68: if (InitializationState::InitializeInFlight !== $this->state) {
69: return false;
70: }
71:
72: $this->state = $this->pendingInitializedNotification
73: ? InitializationState::Initialized
74: : InitializationState::InitializeCompleted;
75:
76: return true;
77: }
78:
79: /**
80: * From `InitializeCompleted`: transition to `Initialized`. From `InitializeInFlight`: buffer the
81: * notification (the handler has not finished yet). `markInitializeCompleted` will consume the
82: * buffered flag on success. Returns `true` if the notification was accepted (flipped the gate or
83: * got buffered). Returns `false` when there is no in-flight handshake to apply the notification
84: * to, or when a notification was already buffered (duplicate).
85: */
86: public function markInitialized(): bool
87: {
88: if (InitializationState::InitializeCompleted === $this->state) {
89: $this->state = InitializationState::Initialized;
90:
91: return true;
92: }
93:
94: if (InitializationState::InitializeInFlight === $this->state && ! $this->pendingInitializedNotification) {
95: $this->pendingInitializedNotification = true;
96:
97: return true;
98: }
99:
100: return false;
101: }
102:
103: /**
104: * Reverts `InitializeInFlight` -> `AwaitingInitialize`. Also clears any buffered notification so a
105: * retry handshake starts fresh. Returns `true` if the transition fired.
106: */
107: public function revertInitializeInFlight(): bool
108: {
109: if (InitializationState::InitializeInFlight !== $this->state) {
110: return false;
111: }
112:
113: $this->state = InitializationState::AwaitingInitialize;
114: $this->pendingInitializedNotification = false;
115:
116: return true;
117: }
118: }
119: