Skip to content
13 changes: 13 additions & 0 deletions config/mcp.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,17 @@
// 'https://example.com',
],

/*
|--------------------------------------------------------------------------
| Session Time To Live (TTL)
|--------------------------------------------------------------------------
|
| This value determines how long (in seconds) MCP session data will be
| cached. Session data includes log level preferences and another
| per-session state. The default is 86,400 seconds (24 hours).
|
*/

'session_ttl' => env('MCP_SESSION_TTL', 86400),

];
41 changes: 41 additions & 0 deletions src/Enums/LogLevel.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

declare(strict_types=1);

namespace Laravel\Mcp\Enums;

enum LogLevel: string
{
case Emergency = 'emergency';
case Alert = 'alert';
case Critical = 'critical';
case Error = 'error';
case Warning = 'warning';
case Notice = 'notice';
case Info = 'info';
case Debug = 'debug';

public function severity(): int
{
return match ($this) {
self::Emergency => 0,
self::Alert => 1,
self::Critical => 2,
self::Error => 3,
self::Warning => 4,
self::Notice => 5,
self::Info => 6,
self::Debug => 7,
};
}

public function shouldLog(LogLevel $configuredLevel): bool
{
return $this->severity() <= $configuredLevel->severity();
}

public static function fromString(string $level): self
{
return self::from(strtolower($level));
}
}
13 changes: 13 additions & 0 deletions src/Response.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@
use Illuminate\Support\Traits\Macroable;
use InvalidArgumentException;
use JsonException;
use Laravel\Mcp\Enums\LogLevel;
use Laravel\Mcp\Enums\Role;
use Laravel\Mcp\Exceptions\NotImplementedException;
use Laravel\Mcp\Server\Content\Blob;
use Laravel\Mcp\Server\Content\Log;
use Laravel\Mcp\Server\Content\Notification;
use Laravel\Mcp\Server\Content\Text;
use Laravel\Mcp\Server\Contracts\Content;
Expand All @@ -36,6 +38,17 @@ public static function notification(string $method, array $params = []): static
return new static(new Notification($method, $params));
}

public static function log(LogLevel $level, mixed $data, ?string $logger = null): static
{
try {
json_encode($data, JSON_THROW_ON_ERROR);
} catch (JsonException $jsonException) {
throw new InvalidArgumentException("Invalid log data: {$jsonException->getMessage()}", 0, $jsonException);
}

return new static(new Log($level, $data, $logger));
}

public static function text(string $text): static
{
return new static(new Text($text));
Expand Down
38 changes: 19 additions & 19 deletions src/Server.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
use Laravel\Mcp\Server\Methods\ListTools;
use Laravel\Mcp\Server\Methods\Ping;
use Laravel\Mcp\Server\Methods\ReadResource;
use Laravel\Mcp\Server\Methods\SetLogLevel;
use Laravel\Mcp\Server\Prompt;
use Laravel\Mcp\Server\Resource;
use Laravel\Mcp\Server\ServerContext;
Expand Down Expand Up @@ -98,6 +99,7 @@ abstract class Server
'prompts/list' => ListPrompts::class,
'prompts/get' => GetPrompt::class,
'ping' => Ping::class,
'logging/setLevel' => SetLogLevel::class,
];

public function __construct(
Expand Down Expand Up @@ -230,19 +232,23 @@ public function createContext(): ServerContext
*/
protected function handleMessage(JsonRpcRequest $request, ServerContext $context): void
{
$response = $this->runMethodHandle($request, $context);

if (! is_iterable($response)) {
$this->transport->send($response->toJson());
try {
$response = $this->runMethodHandle($request, $context);

return;
}
if (! is_iterable($response)) {
$this->transport->send($response->toJson());

$this->transport->stream(function () use ($response): void {
foreach ($response as $message) {
$this->transport->send($message->toJson());
return;
}
});

$this->transport->stream(function () use ($response): void {
foreach ($response as $message) {
$this->transport->send($message->toJson());
}
});
} finally {
Container::getInstance()->forgetInstance('mcp.request');
}
}

/**
Expand All @@ -254,20 +260,14 @@ protected function runMethodHandle(JsonRpcRequest $request, ServerContext $conte
{
$container = Container::getInstance();

$container->instance('mcp.request', $request->toRequest());

/** @var Method $methodClass */
$methodClass = $container->make(
$this->methods[$request->method],
);

$container->instance('mcp.request', $request->toRequest());

try {
$response = $methodClass->handle($request, $context);
} finally {
$container->forgetInstance('mcp.request');
}

return $response;
return $methodClass->handle($request, $context);
}

protected function handleInitializeMessage(JsonRpcRequest $request, ServerContext $context): void
Expand Down
50 changes: 50 additions & 0 deletions src/Server/Content/Log.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

declare(strict_types=1);

namespace Laravel\Mcp\Server\Content;

use Laravel\Mcp\Enums\LogLevel;

class Log extends Notification
{
public function __construct(
protected LogLevel $level,
protected mixed $data,
protected ?string $logger = null,
) {
parent::__construct('notifications/message', $this->buildParams());
}

public function level(): LogLevel
{
return $this->level;
}

public function data(): mixed
{
return $this->data;
}

public function logger(): ?string
{
return $this->logger;
}

/**
* @return array<string, mixed>
*/
protected function buildParams(): array
{
$params = [
'level' => $this->level->value,
'data' => $this->data,
];

if ($this->logger !== null) {
$params['logger'] = $this->logger;
}

return $params;
}
}
27 changes: 27 additions & 0 deletions src/Server/McpServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Laravel\Mcp\Server;

use Illuminate\Contracts\Cache\Repository;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\ServiceProvider;
use Laravel\Mcp\Console\Commands\InspectorCommand;
Expand All @@ -13,6 +14,8 @@
use Laravel\Mcp\Console\Commands\MakeToolCommand;
use Laravel\Mcp\Console\Commands\StartCommand;
use Laravel\Mcp\Request;
use Laravel\Mcp\Server\Support\LoggingManager;
use Laravel\Mcp\Server\Support\SessionStore;

class McpServiceProvider extends ServiceProvider
{
Expand All @@ -21,6 +24,8 @@ public function register(): void
$this->app->singleton(Registrar::class, fn (): Registrar => new Registrar);

$this->mergeConfigFrom(__DIR__.'/../../config/mcp.php', 'mcp');

$this->registerSessionBindings();
}

public function boot(): void
Expand Down Expand Up @@ -85,6 +90,28 @@ protected function registerContainerCallbacks(): void
});
}

protected function registerSessionBindings(): void
{
$this->app->bind(SessionStore::class, function ($app): Support\SessionStore {
$sessionId = null;

if ($app->bound('mcp.request')) {
/** @var Request $request */
$request = $app->make('mcp.request');
$sessionId = $request->sessionId();
}

return new SessionStore(
$app->make(Repository::class),
$sessionId
);
});

$this->app->bind(LoggingManager::class, fn ($app): LoggingManager => new LoggingManager(
$app->make(SessionStore::class)
));
}

protected function registerCommands(): void
{
$this->commands([
Expand Down
11 changes: 11 additions & 0 deletions src/Server/Methods/Concerns/InteractsWithResponses.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@
use Illuminate\Validation\ValidationException;
use Laravel\Mcp\Response;
use Laravel\Mcp\ResponseFactory;
use Laravel\Mcp\Server\Content\Log;
use Laravel\Mcp\Server\Content\Notification;
use Laravel\Mcp\Server\Contracts\Errable;
use Laravel\Mcp\Server\Exceptions\JsonRpcException;
use Laravel\Mcp\Server\Support\LoggingManager;
use Laravel\Mcp\Server\Transport\JsonRpcRequest;
use Laravel\Mcp\Server\Transport\JsonRpcResponse;

Expand Down Expand Up @@ -45,13 +47,22 @@ protected function toJsonRpcStreamedResponse(JsonRpcRequest $request, iterable $
{
/** @var array<int, Response|ResponseFactory|string> $pendingResponses */
$pendingResponses = [];
$loggingManager = null;

try {
foreach ($responses as $response) {
if ($response instanceof Response && $response->isNotification()) {
/** @var Notification $content */
$content = $response->content();

if ($content instanceof Log) {
$loggingManager ??= app(LoggingManager::class);

if (! $loggingManager->shouldLog($content->level())) {
continue;
}
}

yield JsonRpcResponse::notification(
...$content->toArray(),
);
Expand Down
49 changes: 49 additions & 0 deletions src/Server/Methods/SetLogLevel.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

declare(strict_types=1);

namespace Laravel\Mcp\Server\Methods;

use Laravel\Mcp\Enums\LogLevel;
use Laravel\Mcp\Server\Contracts\Method;
use Laravel\Mcp\Server\Exceptions\JsonRpcException;
use Laravel\Mcp\Server\ServerContext;
use Laravel\Mcp\Server\Support\LoggingManager;
use Laravel\Mcp\Server\Transport\JsonRpcRequest;
use Laravel\Mcp\Server\Transport\JsonRpcResponse;
use ValueError;

class SetLogLevel implements Method
{
public function __construct(protected LoggingManager $loggingManager)
{
//
}

public function handle(JsonRpcRequest $request, ServerContext $context): JsonRpcResponse
{
$levelString = $request->get('level');

if (! is_string($levelString)) {
throw new JsonRpcException(
'Invalid Request: The [level] parameter is required and must be a string.',
-32602,
$request->id,
);
}

try {
$level = LogLevel::fromString($levelString);
} catch (ValueError) {
throw new JsonRpcException(
"Invalid log level [{$levelString}]. Must be one of: emergency, alert, critical, error, warning, notice, info, debug.",
-32602,
$request->id,
);
}

$this->loggingManager->setLevel($level);

return JsonRpcResponse::result($request->id, []);
}
}
38 changes: 38 additions & 0 deletions src/Server/Support/LoggingManager.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

declare(strict_types=1);

namespace Laravel\Mcp\Server\Support;

use Laravel\Mcp\Enums\LogLevel;

class LoggingManager
{
protected const LOG_LEVEL_KEY = 'log_level';

private const DEFAULT_LEVEL = LogLevel::Info;

public function __construct(protected SessionStore $session)
{
//
}

public function setLevel(LogLevel $level): void
{
$this->session->set(self::LOG_LEVEL_KEY, $level);
}

public function getLevel(): LogLevel
{
if (is_null($this->session->sessionId())) {
return self::DEFAULT_LEVEL;
}

return $this->session->get(self::LOG_LEVEL_KEY, self::DEFAULT_LEVEL);
}

public function shouldLog(LogLevel $messageLevel): bool
{
return $messageLevel->shouldLog($this->getLevel());
}
}
Loading