Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
6385829
[Add] Resource Template Implementation : Core ResourceTemplate class …
keshav-k3 Oct 9, 2025
3d6bb09
[Add] Resource Template Implementation : Server and ServerContext int…
keshav-k3 Oct 9, 2025
d02aff3
[Add] Resource Template Implementation : Register MakeResourceTemplat…
keshav-k3 Oct 9, 2025
95cff8f
[Add] Resource Template Implementation : Artisan command for generati…
keshav-k3 Oct 9, 2025
d1464f8
[Add] Resource Template Implementation : Unit tests for ListResourceT…
keshav-k3 Oct 9, 2025
6acd016
[Add] Resource Template Implementation : Update TestCase and ServerCo…
keshav-k3 Oct 9, 2025
6f1f224
[Add] Resource Template Implementation : Update method handler tests …
keshav-k3 Oct 9, 2025
48c0a82
[Add] Resource Template Implementation : Update method handler tests …
keshav-k3 Oct 9, 2025
98b33d2
[Add] Resource Template Implementation : Update resource tests for Ca…
keshav-k3 Oct 9, 2025
4932271
[Add] Resource Template Implementation : Update resource tests for Li…
keshav-k3 Oct 9, 2025
90ed961
[Add] Resource Template Implementation : URI pattern matching utility…
keshav-k3 Oct 9, 2025
521a234
[Add] Resource Template Implementation : Add merge() method to Reques…
keshav-k3 Oct 9, 2025
681f82e
[Add] Resource Template Implementation : Enable dynamic resource read…
keshav-k3 Oct 9, 2025
bca6879
[Add] Resource Template Implementation : Update Content interface and…
keshav-k3 Oct 9, 2025
73b5e5b
[Add] Resource Template Implementation : Add comprehensive tests for …
keshav-k3 Oct 9, 2025
447b49a
[Fix] Resource Template Implementation : Static analysis fixes for PH…
keshav-k3 Oct 9, 2025
01d4c23
[Add] Resource Template Implementation : Add feature tests for MakeRe…
keshav-k3 Oct 9, 2025
c325b4f
[Fix] Resource Template Implementation : Use afterResolving instead o…
keshav-k3 Oct 9, 2025
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
55 changes: 55 additions & 0 deletions src/Console/Commands/MakeResourceTemplateCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

declare(strict_types=1);

namespace Laravel\Mcp\Console\Commands;

use Illuminate\Console\GeneratorCommand;
use Illuminate\Support\Str;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputOption;

#[AsCommand(
name: 'make:mcp-resource-template',
description: 'Create a new MCP resource template class'
)]
class MakeResourceTemplateCommand extends GeneratorCommand
{
/**
* @var string
*/
protected $type = 'ResourceTemplate';

protected function getStub(): string
{
return file_exists($customPath = $this->laravel->basePath('stubs/resource-template.stub'))
? $customPath
: __DIR__.'/../../../stubs/resource-template.stub';
}

protected function getDefaultNamespace($rootNamespace): string
{
return "{$rootNamespace}\\Mcp\\ResourceTemplates";
}

/**
* @return array<int, array<int, string|int>>
*/
protected function getOptions(): array
{
return [
['force', 'f', InputOption::VALUE_NONE, 'Create the class even if the resource template already exists'],
];
}

protected function buildClass($name): string
{
$stub = parent::buildClass($name);

return str_replace(
'{{ classKebab }}',
Str::kebab(class_basename($name)),
$stub
);
}
}
13 changes: 13 additions & 0 deletions src/Request.php
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,19 @@ public function setArguments(array $arguments): void
$this->arguments = $arguments;
}

/**
* Merge new data into the current request data.
*
* @param array<string, mixed> $data
* @return $this
*/
public function merge(array $data): static
{
$this->arguments = array_merge($this->arguments, $data);

return $this;
}

public function setSessionId(?string $sessionId): void
{
$this->sessionId = $sessionId;
Expand Down
9 changes: 9 additions & 0 deletions src/Server.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@
use Laravel\Mcp\Server\Methods\Initialize;
use Laravel\Mcp\Server\Methods\ListPrompts;
use Laravel\Mcp\Server\Methods\ListResources;
use Laravel\Mcp\Server\Methods\ListResourceTemplates;
use Laravel\Mcp\Server\Methods\ListTools;
use Laravel\Mcp\Server\Methods\Ping;
use Laravel\Mcp\Server\Methods\ReadResource;
use Laravel\Mcp\Server\Prompt;
use Laravel\Mcp\Server\Resource;
use Laravel\Mcp\Server\ResourceTemplate;
use Laravel\Mcp\Server\ServerContext;
use Laravel\Mcp\Server\Testing\PendingTestResponse;
use Laravel\Mcp\Server\Testing\TestResponse;
Expand Down Expand Up @@ -76,6 +78,11 @@ abstract class Server
*/
protected array $resources = [];

/**
* @var array<int, ResourceTemplate|class-string<ResourceTemplate>>
*/
protected array $resourceTemplates = [];

/**
* @var array<int, Prompt|class-string<Prompt>>
*/
Expand All @@ -92,6 +99,7 @@ abstract class Server
'tools/list' => ListTools::class,
'tools/call' => CallTool::class,
'resources/list' => ListResources::class,
'resources/templates/list' => ListResourceTemplates::class,
'resources/read' => ReadResource::class,
'prompts/list' => ListPrompts::class,
'prompts/get' => GetPrompt::class,
Expand Down Expand Up @@ -219,6 +227,7 @@ public function createContext(): ServerContext
defaultPaginationLength: $this->defaultPaginationLength,
tools: $this->tools,
resources: $this->resources,
resourceTemplates: $this->resourceTemplates,
prompts: $this->prompts,
);
}
Expand Down
5 changes: 3 additions & 2 deletions src/Server/Content/Blob.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Laravel\Mcp\Server\Contracts\Content;
use Laravel\Mcp\Server\Prompt;
use Laravel\Mcp\Server\Resource;
use Laravel\Mcp\Server\ResourceTemplate;
use Laravel\Mcp\Server\Tool;

class Blob implements Content
Expand Down Expand Up @@ -40,11 +41,11 @@ public function toPrompt(Prompt $prompt): array
/**
* @return array<string, mixed>
*/
public function toResource(Resource $resource): array
public function toResource(Resource|ResourceTemplate $resource): array
{
return [
'blob' => base64_encode($this->content),
'uri' => $resource->uri(),
'uri' => $resource instanceof ResourceTemplate ? $resource->uriTemplate() : $resource->uri(),
'name' => $resource->name(),
'title' => $resource->title(),
'mimeType' => $resource->mimeType(),
Expand Down
3 changes: 2 additions & 1 deletion src/Server/Content/Notification.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Laravel\Mcp\Server\Contracts\Content;
use Laravel\Mcp\Server\Prompt;
use Laravel\Mcp\Server\Resource;
use Laravel\Mcp\Server\ResourceTemplate;
use Laravel\Mcp\Server\Tool;

class Notification implements Content
Expand Down Expand Up @@ -38,7 +39,7 @@ public function toPrompt(Prompt $prompt): array
/**
* @return array<string, mixed>
*/
public function toResource(Resource $resource): array
public function toResource(Resource|ResourceTemplate $resource): array
{
return $this->toArray();
}
Expand Down
5 changes: 3 additions & 2 deletions src/Server/Content/Text.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Laravel\Mcp\Server\Contracts\Content;
use Laravel\Mcp\Server\Prompt;
use Laravel\Mcp\Server\Resource;
use Laravel\Mcp\Server\ResourceTemplate;
use Laravel\Mcp\Server\Tool;

class Text implements Content
Expand Down Expand Up @@ -35,11 +36,11 @@ public function toPrompt(Prompt $prompt): array
/**
* @return array<string, mixed>
*/
public function toResource(Resource $resource): array
public function toResource(Resource|ResourceTemplate $resource): array
{
return [
'text' => $this->text,
'uri' => $resource->uri(),
'uri' => $resource instanceof ResourceTemplate ? $resource->uriTemplate() : $resource->uri(),
'name' => $resource->name(),
'title' => $resource->title(),
'mimeType' => $resource->mimeType(),
Expand Down
3 changes: 2 additions & 1 deletion src/Server/Contracts/Content.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Illuminate\Contracts\Support\Arrayable;
use Laravel\Mcp\Server\Prompt;
use Laravel\Mcp\Server\Resource;
use Laravel\Mcp\Server\ResourceTemplate;
use Laravel\Mcp\Server\Tool;
use Stringable;

Expand All @@ -28,7 +29,7 @@ public function toPrompt(Prompt $prompt): array;
/**
* @return array<string, mixed>
*/
public function toResource(Resource $resource): array;
public function toResource(Resource|ResourceTemplate $resource): array;

public function __toString(): string;
}
3 changes: 3 additions & 0 deletions src/Server/McpServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use Laravel\Mcp\Console\Commands\InspectorCommand;
use Laravel\Mcp\Console\Commands\MakePromptCommand;
use Laravel\Mcp\Console\Commands\MakeResourceCommand;
use Laravel\Mcp\Console\Commands\MakeResourceTemplateCommand;
use Laravel\Mcp\Console\Commands\MakeServerCommand;
use Laravel\Mcp\Console\Commands\MakeToolCommand;
use Laravel\Mcp\Console\Commands\StartCommand;
Expand Down Expand Up @@ -45,6 +46,7 @@ protected function registerPublishing(): void
$this->publishes([
__DIR__.'/../../stubs/prompt.stub' => base_path('stubs/prompt.stub'),
__DIR__.'/../../stubs/resource.stub' => base_path('stubs/resource.stub'),
__DIR__.'/../../stubs/resource-template.stub' => base_path('stubs/resource-template.stub'),
__DIR__.'/../../stubs/server.stub' => base_path('stubs/server.stub'),
__DIR__.'/../../stubs/tool.stub' => base_path('stubs/tool.stub'),
], 'mcp-stubs');
Expand Down Expand Up @@ -86,6 +88,7 @@ protected function registerCommands(): void
MakeToolCommand::class,
MakePromptCommand::class,
MakeResourceCommand::class,
MakeResourceTemplateCommand::class,
InspectorCommand::class,
]);
}
Expand Down
25 changes: 25 additions & 0 deletions src/Server/Methods/ListResourceTemplates.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

declare(strict_types=1);

namespace Laravel\Mcp\Server\Methods;

use Laravel\Mcp\Server\Contracts\Method;
use Laravel\Mcp\Server\Pagination\CursorPaginator;
use Laravel\Mcp\Server\ServerContext;
use Laravel\Mcp\Server\Transport\JsonRpcRequest;
use Laravel\Mcp\Server\Transport\JsonRpcResponse;

class ListResourceTemplates implements Method
{
public function handle(JsonRpcRequest $request, ServerContext $context): JsonRpcResponse
{
$paginator = new CursorPaginator(
items: $context->resourceTemplates(),
perPage: $context->perPage($request->get('per_page')),
cursor: $request->cursor(),
);

return JsonRpcResponse::result($request->id, $paginator->paginate('resourceTemplates'));
}
}
50 changes: 39 additions & 11 deletions src/Server/Methods/ReadResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,15 @@
use Illuminate\Container\Container;
use Illuminate\Support\Collection;
use Illuminate\Validation\ValidationException;
use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\Server\Contracts\Method;
use Laravel\Mcp\Server\Exceptions\JsonRpcException;
use Laravel\Mcp\Server\Methods\Concerns\InteractsWithResponses;
use Laravel\Mcp\Server\Resource;
use Laravel\Mcp\Server\ResourceTemplate;
use Laravel\Mcp\Server\ServerContext;
use Laravel\Mcp\Server\Support\UriTemplateMatcher;
use Laravel\Mcp\Server\Transport\JsonRpcRequest;
use Laravel\Mcp\Server\Transport\JsonRpcResponse;
use Laravel\Mcp\Support\ValidationMessages;
Expand All @@ -37,14 +40,33 @@ public function handle(JsonRpcRequest $request, ServerContext $context): Generat
);
}

$uri = $request->get('uri');

$resource = $context->resources()
->first(
fn (Resource $resource): bool => $resource->uri() === $request->get('uri'),
fn () => throw new JsonRpcException(
"Resource [{$request->get('uri')}] not found.",
-32002,
$request->id,
));
->first(fn (Resource $resource): bool => $resource->uri() === $uri);

if (! $resource) {
$resource = $context->resourceTemplates()
->first(fn (ResourceTemplate $template): bool => UriTemplateMatcher::matches($template->uriTemplate(), $uri));

if ($resource instanceof ResourceTemplate) {
$variables = UriTemplateMatcher::extract($resource->uriTemplate(), $uri);

Container::getInstance()->afterResolving(Request::class, function (Request $mcpRequest) use ($variables): void {
foreach ($variables as $key => $value) {
$mcpRequest->merge([$key => $value]);
}
});
}
}

if (! $resource) {
throw new JsonRpcException(
"Resource [{$uri}] not found.",
-32002,
$request->id,
);
}

try {
// @phpstan-ignore-next-line
Expand All @@ -54,14 +76,20 @@ public function handle(JsonRpcRequest $request, ServerContext $context): Generat
}

return is_iterable($response)
? $this->toJsonRpcStreamedResponse($request, $response, $this->serializable($resource))
: $this->toJsonRpcResponse($request, $response, $this->serializable($resource));
? $this->toJsonRpcStreamedResponse($request, $response, $this->serializable($resource, $uri))
: $this->toJsonRpcResponse($request, $response, $this->serializable($resource, $uri));
}

protected function serializable(Resource $resource): callable
protected function serializable(Resource|ResourceTemplate $resource, string $requestedUri): callable
{
return fn (Collection $responses): array => [
'contents' => $responses->map(fn (Response $response): array => $response->content()->toResource($resource))->all(),
'contents' => $responses->map(function (Response $response) use ($resource, $requestedUri): array {
$content = $response->content()->toResource($resource);

$content['uri'] = $requestedUri;

return $content;
})->all(),
];
}
}
47 changes: 47 additions & 0 deletions src/Server/ResourceTemplate.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

declare(strict_types=1);

namespace Laravel\Mcp\Server;

use Illuminate\Support\Str;

abstract class ResourceTemplate extends Primitive
{
protected string $uriTemplate = '';

protected string $mimeType = '';

public function uriTemplate(): string
{
return $this->uriTemplate !== ''
? $this->uriTemplate
: 'file://resources/'.Str::kebab(class_basename($this)).'/{id}';
}

public function mimeType(): string
{
return $this->mimeType !== ''
? $this->mimeType
: 'text/plain';
}

/**
* @return array<string, mixed>
*/
public function toMethodCall(): array
{
return ['uriTemplate' => $this->uriTemplate()];
}

public function toArray(): array
{
return [
'name' => $this->name(),
'title' => $this->title(),
'description' => $this->description(),
'uriTemplate' => $this->uriTemplate(),
'mimeType' => $this->mimeType(),
];
}
}
Loading