Skip to content

Commit 32f48df

Browse files
committed
Implement HTTP+SSE transport support
Add MCP HTTP+SSE transport as per MCP 2024-11-05: - Update README with SSE usage - Add `sse` config options in config/loop.php - Create McpSSEController and LegacySseEnabledMiddleware - Introduce SseService, SseSessionManager, SseDriverManager - Provide FileDriver and RedisDriver implementations - Extend routes/api.php for SSE endpoint - Add HTTP streaming docs (http-streaming.md, mcp-sse-transport.md)
1 parent 2658580 commit 32f48df

File tree

15 files changed

+1340
-58
lines changed

15 files changed

+1340
-58
lines changed

README.md

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ Loop::tool(
7070
);
7171
```
7272

73-
The available parameters types can be found in the [Prism Tool Documentation](https://prismphp.com/core-concepts/tools-function-calling.html#parameter-definition).
73+
The available parameters types can be found in the [Prism Tool Documentation](https://prismphp.com/core-concepts/tools-function-calling.html#parameter-definition).
7474

7575
### Custom Tool Objects
7676

@@ -183,6 +183,28 @@ See [HTTP Streaming Documentation](docs/http-streaming.md) for more details on c
183183

184184
If you are using the Streamable HTTP transport in any public endpoint, make sure you set the `streamable_http.middleware` config option to secure your endpoint. We recommend using something like Sanctum to protected the endpoint.
185185

186+
187+
### HTTP+SSE Transport
188+
189+
Laravel Loop also supports the [HTTP+SSE transport](https://modelcontextprotocol.io/specification/2024-11-05/basic/transports#http-with-sse) as specified in the MCP 2024-11-05 standard.
190+
191+
To enable the HTTP+SSE transport, update your `.env` file:
192+
193+
```bash
194+
LOOP_SSE_ENABLED=true
195+
```
196+
197+
This will expose an MCP endpoint at `/mcp/sse` that implements the HTTP+SSE transport protocol. The endpoint is protected by Laravel Sanctum by default.
198+
199+
The HTTP+SSE transport works as follows:
200+
201+
1. Client establishes an SSE connection to `GET /mcp/sse`
202+
2. Server sends an `endpoint` event with a URI for client messages
203+
3. Client sends JSON-RPC requests as POST requests to this endpoint
204+
4. Server replies with responses through the SSE connection
205+
206+
See [MCP SSE Transport Documentation](docs/mcp-sse-transport.md) for more details on configuration, implementation, and usage examples.
207+
186208
## Troubleshooting
187209

188210
**Connection failed: MCP error -32000: Connection closed**
@@ -208,4 +230,4 @@ Development of this package is sponsored by Kirschbaum Development Group, a deve
208230

209231
## License
210232

211-
The MIT License (MIT). Please see [License File](LICENSE) for more information.
233+
The MIT License (MIT). Please see [License File](LICENSE) for more information.

config/loop.php

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,70 @@
4040
*/
4141
'middleware' => ['auth:sanctum'],
4242
],
43+
44+
'sse' => [
45+
/*
46+
|--------------------------------------------------------------------------
47+
| HTTP + SSE Transport Enabled
48+
|--------------------------------------------------------------------------
49+
|
50+
| Determines whether SSE is enabled for the application.
51+
|
52+
*/
53+
'enabled' => env('LOOP_SSE_ENABLED', true),
54+
55+
/*
56+
|--------------------------------------------------------------------------
57+
| SSE Driver
58+
|--------------------------------------------------------------------------
59+
|
60+
| This option controls the default SSE driver that will be used for
61+
| maintaining server-sent events connections. Supported drivers: "file", "redis".
62+
|
63+
*/
64+
'driver' => env('LOOP_SSE_DRIVER', 'file'),
65+
66+
/*
67+
|--------------------------------------------------------------------------
68+
| SSE Path
69+
|--------------------------------------------------------------------------
70+
|
71+
| The base path for SSE routes.
72+
|
73+
*/
74+
'path' => env('LOOP_SSE_PATH', '/mcp/sse'),
75+
76+
/*
77+
|--------------------------------------------------------------------------
78+
| SSE Middleware
79+
|--------------------------------------------------------------------------
80+
|
81+
| The middleware used to authenticate MCP requests.
82+
| We recommend using something like Laravel Sanctum here.
83+
|
84+
| WARNING: DO NOT LEAVE THIS ENDPOINT ENABLED AND UNPROTECTED IN PRODUCTION.
85+
|
86+
*/
87+
'middleware' => [],
88+
89+
/*
90+
|--------------------------------------------------------------------------
91+
| SSE Drivers
92+
|--------------------------------------------------------------------------
93+
|
94+
| Configuration for each SSE driver.
95+
|
96+
*/
97+
'drivers' => [
98+
'file' => [
99+
'storage_dir' => storage_path('app/mcp_sse'),
100+
'session_ttl' => 86400, // 24 hours in seconds
101+
],
102+
'redis' => [
103+
'prefix' => 'sse',
104+
'session_ttl' => 86400, // 24 hours in seconds
105+
'connection' => 'default',
106+
],
107+
],
108+
],
43109
];

docs/http-streaming.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# Laravel Loop HTTP Streaming
2+
3+
Laravel Loop provides a streamable HTTP transport for the Model Context Protocol (MCP) that supports client-initiated requests (POST). The MCP HTTP transport is able to process JSON and SSE streaming responses on a single endpoint.
4+
5+
## Configuration
6+
7+
To enable and configure the HTTP MCP endpoint, update your `.env` file and/or `config/loop.php`:
8+
9+
```php
10+
# Enable the HTTP MCP endpoint
11+
LOOP_STREAMABLE_HTTP_ENABLED=true
12+
13+
# Set the endpoint path (default: /mcp)
14+
LOOP_STREAMABLE_HTTP_PATH=/mcp
15+
```
16+
17+
### Security Considerations
18+
19+
The MCP HTTP endpoint should be protected with proper authentication. By default, it uses Laravel Sanctum, but you can configure the middleware stack in `config/loop.php`:
20+
21+
```php
22+
'middleware' => ['auth:sanctum'],
23+
```
24+
25+
## Client-Initiated Messages (POST)
26+
27+
Clients can send JSON-RPC 2.0 messages to the MCP server by making HTTP POST requests to the configured endpoint.
28+
29+
### Single Request
30+
31+
```bash
32+
curl -X POST http://your-app.test/mcp \
33+
-H "Content-Type: application/json" \
34+
-H "Accept: application/json" \
35+
-H "Authorization: Bearer YOUR_TOKEN" \
36+
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"clientInfo":{"name":"Test Client"},"capabilities":{},"protocolVersion":"2024-11-05"}}'
37+
```
38+
39+
### Batch Requests
40+
41+
```bash
42+
curl -X POST http://your-app.test/mcp \
43+
-H "Content-Type: application/json" \
44+
-H "Accept: application/json" \
45+
-H "Authorization: Bearer YOUR_TOKEN" \
46+
-d '[{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"clientInfo":{"name":"Test Client"},"capabilities":{},"protocolVersion":"2024-11-05"}},{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}]'
47+
```
48+
49+
### SSE Streaming Responses
50+
51+
Clients can request SSE responses for batch processing by setting the `Accept` header to include `text/event-stream`:
52+
53+
```bash
54+
curl -X POST http://your-app.test/mcp \
55+
-H "Content-Type: application/json" \
56+
-H "Accept: text/event-stream" \
57+
-H "Authorization: Bearer YOUR_TOKEN" \
58+
-d '[{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"clientInfo":{"name":"Test Client"},"capabilities":{},"protocolVersion":"2024-11-05"}},{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}]'
59+
```
60+
61+
The server will respond with an SSE stream containing each response as a separate event.

docs/mcp-sse-transport.md

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# MCP HTTP+SSE Transport
2+
3+
## Overview
4+
5+
This implementation provides the HTTP+SSE transport option according to the MCP specification.
6+
7+
## How it Works
8+
9+
The SSE transport works as follows:
10+
11+
1. A client establishes a connection to the SSE endpoint (`GET /mcp/sse`)
12+
2. The server responds with an `endpoint` event containing a URI for the client to use for sending messages
13+
3. The client sends JSON-RPC messages as HTTP POST requests to this endpoint
14+
4. The server processes these messages and sends responses back through the SSE connection
15+
16+
## Configuration
17+
18+
To enable the MCP SSE transport in your Laravel application, add the following to your `.env` file:
19+
20+
```
21+
LOOP_SSE_ENABLED=true
22+
LOOP_SSE_PATH=/mcp/sse # This is the default, can be changed if needed
23+
LOOP_SSE_DRIVER=file # Default driver, options: "file" or "redis"
24+
```
25+
26+
## SSE Drivers
27+
28+
Laravel Loop supports two storage drivers for maintaining SSE connections:
29+
30+
### File Driver (Default)
31+
32+
The file driver stores session and message data in the local filesystem.
33+
34+
Configuration in `config/loop.php`:
35+
36+
```php
37+
'drivers' => [
38+
'file' => [
39+
'storage_dir' => storage_path('app/mcp_sse'),
40+
'session_ttl' => 86400, // 24 hours in seconds
41+
],
42+
],
43+
```
44+
45+
### Redis Driver
46+
47+
The Redis driver stores session and message data in Redis.
48+
49+
To use the Redis driver:
50+
51+
1. Set in your `.env` file:
52+
```
53+
LOOP_SSE_DRIVER=redis
54+
```
55+
56+
2. Configure Redis options in `config/loop.php`:
57+
```php
58+
'drivers' => [
59+
'redis' => [
60+
'prefix' => 'sse', // Redis key prefix for SSE data
61+
'session_ttl' => 86400, // Session TTL in seconds (24 hours)
62+
'connection' => 'default', // Redis connection from database config
63+
],
64+
],
65+
```
66+
67+
3. Ensure Redis is properly configured in your Laravel application's `config/database.php`.
68+
69+
Benefits of using the Redis driver:
70+
- **Performance**: In-memory storage provides faster access than filesystem
71+
- **Reliability**: Redis offers persistence options to prevent data loss
72+
- **Monitoring**: Redis provides tools to monitor usage and performance
73+
74+
## Security Considerations
75+
76+
By default, the SSE endpoint is protected with Laravel Sanctum authentication middleware. You can configure this in the `config/loop.php` file:
77+
78+
```php
79+
'sse' => [
80+
// ...
81+
'middleware' => ['auth:sanctum'], // Change this to your preferred authentication middleware
82+
],
83+
```
84+
85+
**WARNING:** Do not leave the SSE endpoint enabled and unprotected in production environments.
86+
87+
## Additional Resources
88+
89+
For more information on the MCP SSE transport specification, see the [official documentation](https://modelcontextprotocol.io/specification/2024-11-05/basic/transports#http-with-sse).

routes/api.php

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,41 @@
22

33
use Illuminate\Support\Facades\Route;
44
use Kirschbaum\Loop\Http\Controllers\McpController;
5+
use Kirschbaum\Loop\Http\Controllers\McpSSEController;
6+
use Kirschbaum\Loop\Http\Middleware\LegacySseEnabledMiddleware;
57
use Kirschbaum\Loop\Http\Middleware\StreamableHttpEnabledMiddleware;
68

9+
/*
10+
|--------------------------------------------------------------------------
11+
| Streamable Http Endpoint
12+
|--------------------------------------------------------------------------
13+
*/
714
$path = config('loop.streamable_http.path', '/mcp');
815
$streamableHttpEnabled = config('loop.streamable_http.enabled', false);
916

10-
if ($streamableHttpEnabled) {
11-
Route::prefix($path)
12-
->middleware([
13-
StreamableHttpEnabledMiddleware::class,
14-
...config('loop.streamable_http.middleware', []),
15-
])
16-
->group(function () {
17-
Route::post('/', McpController::class);
18-
});
19-
}
17+
Route::prefix($path)
18+
->middleware([
19+
StreamableHttpEnabledMiddleware::class,
20+
...config('loop.streamable_http.middleware', []),
21+
])
22+
->group(function () {
23+
Route::post('/', McpController::class);
24+
});
25+
26+
/*
27+
|--------------------------------------------------------------------------
28+
| HTTP + SSE Endpoint (Deprecated)
29+
|--------------------------------------------------------------------------
30+
*/
31+
$ssePath = config('loop.sse.path', '/mcp/sse');
32+
$sseEnabled = config('loop.sse.enabled', false);
33+
34+
Route::prefix($ssePath)
35+
->middleware([
36+
LegacySseEnabledMiddleware::class,
37+
...config('loop.sse.middleware', []),
38+
])
39+
->group(function () {
40+
Route::get('/', [McpSSEController::class, 'connect']);
41+
Route::post('message', [McpSSEController::class, 'message']);
42+
});
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<?php
2+
3+
namespace Kirschbaum\Loop\Contracts;
4+
5+
/**
6+
* Interface for SSE drivers.
7+
*/
8+
interface SseDriverInterface
9+
{
10+
/**
11+
* Register a new client session.
12+
*
13+
* @param string $sessionId Unique identifier for the client session
14+
* @return bool Whether registration was successful
15+
*/
16+
public function registerSession(string $sessionId): bool;
17+
18+
/**
19+
* Check if a session exists.
20+
*
21+
* @param string $sessionId The session identifier to check
22+
* @return bool Whether the session exists
23+
*/
24+
public function sessionExists(string $sessionId): bool;
25+
26+
/**
27+
* Send a message to a specific client.
28+
*
29+
* @param string $sessionId Session identifier
30+
* @param array<string, mixed> $data Message data to send
31+
* @return bool Whether the message was sent successfully
32+
*/
33+
public function sendMessage(string $sessionId, array $data): bool;
34+
35+
/**
36+
* Get messages for a specific client.
37+
*
38+
* @param string $sessionId Session identifier
39+
* @param int $lastMessageId ID of the last received message (for pagination)
40+
* @return array<array<string, mixed>> Array of messages
41+
*/
42+
public function getMessages(string $sessionId, int $lastMessageId = -1): array;
43+
44+
/**
45+
* Remove a client session.
46+
*
47+
* @param string $sessionId Session identifier
48+
* @return bool Whether removal was successful
49+
*/
50+
public function removeSession(string $sessionId): bool;
51+
52+
/**
53+
* Get all active session IDs.
54+
*
55+
* @return array<string> Array of active session IDs
56+
*/
57+
public function getActiveSessions(): array;
58+
}

0 commit comments

Comments
 (0)