Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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\SessionStoreManager;

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(SessionStoreManager::class, function ($app): Support\SessionStoreManager {
$sessionId = null;

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

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

$this->app->bind(LoggingManager::class, fn ($app): LoggingManager => new LoggingManager(
$app->make(SessionStoreManager::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 SessionStoreManager $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