2525 */
2626class StreamableHttpTransport implements TransportInterface
2727{
28- private $ messageHandler = null ;
29- private $ outgoingMessages = [];
28+ /** @var array<string, array<callable>> */
29+ private array $ listeners = [];
30+
31+ /** @var string[] */
32+ private array $ outgoingMessages = [];
33+
34+ private array $ corsHeaders = [
35+ 'Access-Control-Allow-Origin ' => '* ' ,
36+ 'Access-Control-Allow-Methods ' => 'GET, POST, DELETE, OPTIONS ' ,
37+ 'Access-Control-Allow-Headers ' => 'Content-Type, Mcp-Session-Id, Last-Event-ID, Authorization, Accept ' ,
38+ ];
3039
3140 public function __construct (
3241 private readonly ServerRequestInterface $ request ,
@@ -37,9 +46,23 @@ public function __construct(
3746
3847 public function initialize (): void {}
3948
40- public function setMessageHandler (callable $ handler ): void
49+ public function on (string $ event , callable $ listener ): void
50+ {
51+ if (!isset ($ this ->listeners [$ event ])) {
52+ $ this ->listeners [$ event ] = [];
53+ }
54+ $ this ->listeners [$ event ][] = $ listener ;
55+ }
56+
57+ public function emit (string $ event , mixed ...$ args ): void
4158 {
42- $ this ->messageHandler = $ handler ;
59+ if (!isset ($ this ->listeners [$ event ])) {
60+ return ;
61+ }
62+
63+ foreach ($ this ->listeners [$ event ] as $ listener ) {
64+ $ listener (...$ args );
65+ }
4366 }
4467
4568 public function send (string $ data ): void
@@ -49,58 +72,95 @@ public function send(string $data): void
4972
5073 public function listen (): mixed
5174 {
52- if ($ this ->messageHandler === null ) {
53- $ this ->logger ->error ('Cannot listen without a message handler. Did you forget to call Server::connect()? ' );
54- return $ this ->createErrorResponse (Error::forInternalError ('Internal Server Error: Transport not configured. ' ), 500 );
55- }
5675
57- switch ($ this ->request ->getMethod ()) {
58- case 'POST ' :
59- $ body = $ this ->request ->getBody ()->getContents ();
60- if (empty ($ body )) {
61- return $ this ->createErrorResponse (Error::forInvalidRequest ('Bad Request: Empty request body. ' ), 400 );
62- }
76+ return match ($ this ->request ->getMethod ()) {
77+ 'OPTIONS ' => $ this ->handleOptionsRequest (),
78+ 'GET ' => $ this ->handleGetRequest (),
79+ 'POST ' => $ this ->handlePostRequest (),
80+ 'DELETE ' => $ this ->handleDeleteRequest (),
81+ default => $ this ->handleUnsupportedRequest (),
82+ };
83+ }
6384
64- call_user_func ($ this ->messageHandler , $ body );
65- break ;
85+ protected function handleOptionsRequest (): ResponseInterface
86+ {
87+ return $ this ->withCorsHeaders ($ this ->responseFactory ->createResponse (204 ));
88+ }
6689
67- case 'GET ' :
68- case 'DELETE ' :
69- return $ this ->createErrorResponse (Error::forInvalidRequest ('Method Not Allowed ' ), 405 );
90+ protected function handlePostRequest (): ResponseInterface
91+ {
92+ $ acceptHeader = $ this ->request ->getHeaderLine ('Accept ' );
93+ if (!str_contains ($ acceptHeader , 'application/json ' ) || !str_contains ($ acceptHeader , 'text/event-stream ' )) {
94+ $ error = Error::forInvalidRequest ('Not Acceptable: Client must accept both application/json and text/event-stream. ' );
95+ return $ this ->createErrorResponse ($ error , 406 );
96+ }
7097
71- default :
72- return $ this -> createErrorResponse ( Error::forInvalidRequest ('Method Not Allowed ' ), 405 )
73- -> withHeader ( ' Allow ' , ' POST ' );
98+ if (! str_contains ( $ this -> request -> getHeaderLine ( ' Content-Type ' ), ' application/json ' )) {
99+ $ error = Error::forInvalidRequest ('Unsupported Media Type: Content-Type must be application/json. ' );
100+ return $ this -> createErrorResponse ( $ error , 415 );
74101 }
75102
76- return $ this ->buildResponse ();
77- }
103+ $ body = $ this ->request ->getBody ()->getContents ();
104+ if (empty ($ body )) {
105+ $ error = Error::forInvalidRequest ('Bad Request: Empty request body. ' );
106+ return $ this ->createErrorResponse ($ error , 400 );
107+ }
78108
79- public function close (): void {}
109+ $ this -> emit ( ' message ' , $ body );
80110
81- private function buildResponse (): ResponseInterface
82- {
83- $ hasRequestsInInput = !empty ($ this ->request ->getBody ()->getContents ());
111+ $ hasRequestsInInput = str_contains ($ body , '"id" ' );
84112 $ hasResponsesInOutput = !empty ($ this ->outgoingMessages );
85113
86114 if ($ hasRequestsInInput && !$ hasResponsesInOutput ) {
87- return $ this ->responseFactory ->createResponse (202 );
115+ return $ this ->withCorsHeaders ( $ this -> responseFactory ->createResponse (202 ) );
88116 }
89117
90118 $ responseBody = count ($ this ->outgoingMessages ) === 1
91119 ? $ this ->outgoingMessages [0 ]
92120 : '[ ' . implode (', ' , $ this ->outgoingMessages ) . '] ' ;
93121
94- return $ this ->responseFactory ->createResponse (200 )
122+ $ response = $ this ->responseFactory ->createResponse (200 )
95123 ->withHeader ('Content-Type ' , 'application/json ' )
96124 ->withBody ($ this ->streamFactory ->createStream ($ responseBody ));
125+
126+ return $ this ->withCorsHeaders ($ response );
127+ }
128+
129+ protected function handleGetRequest (): ResponseInterface
130+ {
131+ $ response = $ this ->createErrorResponse (Error::forInvalidRequest ('Not Yet Implemented ' ), 501 );
132+ return $ this ->withCorsHeaders ($ response );
133+ }
134+
135+ protected function handleDeleteRequest (): ResponseInterface
136+ {
137+ $ response = $ this ->createErrorResponse (Error::forInvalidRequest ('Not Yet Implemented ' ), 501 );
138+ return $ this ->withCorsHeaders ($ response );
139+ }
140+
141+ protected function handleUnsupportedRequest (): ResponseInterface
142+ {
143+ $ response = $ this ->createErrorResponse (Error::forInvalidRequest ('Method Not Allowed ' ), 405 );
144+ return $ this ->withCorsHeaders ($ response );
97145 }
98146
99- private function createErrorResponse ( Error $ jsonRpcErrpr , int $ statusCode ): ResponseInterface
147+ protected function withCorsHeaders ( ResponseInterface $ response ): ResponseInterface
100148 {
101- $ errorPayload = json_encode ($ jsonRpcErrpr , \JSON_THROW_ON_ERROR );
149+ foreach ($ this ->corsHeaders as $ name => $ value ) {
150+ $ response = $ response ->withHeader ($ name , $ value );
151+ }
152+
153+ return $ response ;
154+ }
155+
156+ protected function createErrorResponse (Error $ jsonRpcError , int $ statusCode ): ResponseInterface
157+ {
158+ $ errorPayload = json_encode ($ jsonRpcError , \JSON_THROW_ON_ERROR );
159+
102160 return $ this ->responseFactory ->createResponse ($ statusCode )
103161 ->withHeader ('Content-Type ' , 'application/json ' )
104- ->withBody ($ this ->streamFactory ->createStream (json_encode ( $ errorPayload) ));
162+ ->withBody ($ this ->streamFactory ->createStream ($ errorPayload ));
105163 }
164+
165+ public function close (): void {}
106166}
0 commit comments