Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
c62a1c7
Add Resource Template
pushpak1300 Nov 18, 2025
a4c8c8d
Fix code styling
pushpak1300 Nov 24, 2025
2940d1f
Add _meta support (#106)
pushpak1300 Nov 16, 2025
b631f69
Remove non-spec fields from resource content responses (#110)
pushpak1300 Nov 18, 2025
a099fb9
Update CHANGELOG
taylorotwell Nov 18, 2025
a2b9ec3
Merge branch 'main' into add_support_for_resorce_templatees
pushpak1300 Nov 24, 2025
077a8d7
Merge branch 'main' into add_support_for_resorce_templatees
pushpak1300 Nov 24, 2025
092bef8
Fix code styling
pushpak1300 Nov 24, 2025
5490626
Fix test
pushpak1300 Nov 25, 2025
baf00e0
Refactor
pushpak1300 Nov 25, 2025
e55dda6
Refactor
pushpak1300 Nov 25, 2025
e5f152a
Add Test
pushpak1300 Nov 25, 2025
c9d9028
Refactor
pushpak1300 Nov 25, 2025
2ad7723
Add Test
pushpak1300 Nov 25, 2025
a9d3a75
Refactor UriTemplate methods
pushpak1300 Nov 25, 2025
56c1a40
Refactor
pushpak1300 Nov 25, 2025
34f0e57
Refactor
pushpak1300 Nov 25, 2025
941dc2b
Refactor
pushpak1300 Nov 25, 2025
39e618e
Refactor
pushpak1300 Nov 25, 2025
6350fbf
Add More Test
pushpak1300 Nov 25, 2025
d8b221a
Merge branch 'main' into add_support_for_resorce_templatees
pushpak1300 Nov 26, 2025
cd0c4a9
Add make:: method
pushpak1300 Nov 26, 2025
781f3e4
Refactor Test
pushpak1300 Nov 26, 2025
bc66373
Refactor
pushpak1300 Nov 26, 2025
0070857
Refactor
pushpak1300 Nov 26, 2025
bed6009
Fix Test
pushpak1300 Nov 26, 2025
2fa4692
Refactor tools variable name
pushpak1300 Nov 26, 2025
bf35228
Formatting
pushpak1300 Nov 26, 2025
f7d747c
SupportUriTemplate -> SupportsUriTemplate
pushpak1300 Nov 26, 2025
9c32483
improve numeric readability
pushpak1300 Nov 26, 2025
6a999b4
Update the test coverage threshold
pushpak1300 Nov 26, 2025
407cae7
Merge branch 'main' into add_support_for_resorce_templatees
pushpak1300 Nov 26, 2025
de08e11
Remove redundant test
pushpak1300 Nov 26, 2025
8ed0c25
formatting
taylorotwell Nov 28, 2025
bc7e704
add files
taylorotwell Nov 28, 2025
5c3c900
formatting
taylorotwell Nov 28, 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
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@
"pint --test",
"rector --dry-run"
],
"test:unit": "pest --ci --coverage --min=92",
"test:unit": "pest --ci --coverage --min=92.5",
"test:types": "phpstan",
"test": [
"@test:lint",
Expand Down
21 changes: 21 additions & 0 deletions src/Request.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public function __construct(
protected array $arguments = [],
protected ?string $sessionId = null,
protected ?array $meta = null,
protected ?string $uri = null,
) {
//
}
Expand Down Expand Up @@ -61,6 +62,16 @@ public function get(string $key, mixed $default = null): mixed
return $this->data($key, $default);
}

/**
* @param array<string,mixed> $data
*/
public function merge(array $data): static
{
$this->arguments = array_merge($this->arguments, $data);

return $this;
}

/**
* @return array<string, mixed>
*/
Expand Down Expand Up @@ -102,6 +113,11 @@ public function meta(): ?array
return $this->meta;
}

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

/**
* @param array<string, mixed> $arguments
*/
Expand All @@ -122,4 +138,9 @@ public function setMeta(?array $meta): void
{
$this->meta = $meta;
}

public function setUri(?string $uri): void
{
$this->uri = $uri;
}
}
2 changes: 2 additions & 0 deletions src/Server.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
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;
Expand Down Expand Up @@ -93,6 +94,7 @@ abstract class Server
'tools/call' => CallTool::class,
'resources/list' => ListResources::class,
'resources/read' => ReadResource::class,
'resources/templates/list' => ListResourceTemplates::class,
'prompts/list' => ListPrompts::class,
'prompts/get' => GetPrompt::class,
'ping' => Ping::class,
Expand Down
15 changes: 15 additions & 0 deletions src/Server/Contracts/HasUriTemplate.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace Laravel\Mcp\Server\Contracts;

use Laravel\Mcp\Support\UriTemplate;

interface HasUriTemplate
{
/**
* Get the URI pattern for the resource template.
*/
public function uriTemplate(): UriTemplate;
}
1 change: 1 addition & 0 deletions src/Server/McpServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,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
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'));
}
}
61 changes: 47 additions & 14 deletions src/Server/Methods/ReadResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@

use Generator;
use Illuminate\Container\Container;
use Illuminate\Contracts\Container\BindingResolutionException;
use Illuminate\Validation\ValidationException;
use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\ResponseFactory;
use Laravel\Mcp\Server\Contracts\HasUriTemplate;
use Laravel\Mcp\Server\Contracts\Method;
use Laravel\Mcp\Server\Exceptions\JsonRpcException;
use Laravel\Mcp\Server\Methods\Concerns\InteractsWithResponses;
Expand All @@ -26,6 +29,7 @@ class ReadResource implements Method
* @return Generator<JsonRpcResponse>|JsonRpcResponse
*
* @throws JsonRpcException
* @throws BindingResolutionException
*/
public function handle(JsonRpcRequest $request, ServerContext $context): Generator|JsonRpcResponse
{
Expand All @@ -37,31 +41,60 @@ public function handle(JsonRpcRequest $request, ServerContext $context): Generat
);
}

$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,
));
$uri = $request->get('uri');

/** @var Resource|null $resource */
$resource = $context->resources()->first(fn (Resource $resource): bool => $resource->uri() === $uri) ??
$context->resourceTemplates()->first(fn (HasUriTemplate $template): bool => ! is_null($template->uriTemplate()->match($uri)));

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

try {
// @phpstan-ignore-next-line
$response = Container::getInstance()->call([$resource, 'handle']);
$response = $this->invokeResource($resource, $uri);
} catch (ValidationException $validationException) {
$response = Response::error('Invalid params: '.ValidationMessages::from($validationException));
}

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));
}

/**
* @throws BindingResolutionException
* @throws ValidationException
*/
protected function invokeResource(Resource $resource, string $uri): mixed
{
$container = Container::getInstance();

$request = $container->make(Request::class);
$request->setUri($uri);

if ($resource instanceof HasUriTemplate) {
$variables = $resource->uriTemplate()->match($uri) ?? [];
$request->merge($variables);
}

$container->instance(Request::class, $request);

try {
// @phpstan-ignore-next-line
return $container->call([$resource, 'handle']);
} finally {
$container->forgetInstance(Request::class);
}
}

protected function serializable(Resource $resource): callable
protected function serializable(Resource $resource, string $uri): callable
{
return fn (ResponseFactory $factory): array => $factory->mergeMeta([
'contents' => $factory->responses()->map(fn (Response $response): array => $response->content()->toResource($resource))->all(),
'contents' => $factory->responses()->map(fn (Response $response): array => [
...$response->content()->toResource($resource),
'uri' => $uri,
])->all(),
]);
}
}
19 changes: 14 additions & 5 deletions src/Server/Resource.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Illuminate\Support\Str;
use Laravel\Mcp\Server\Annotations\Annotation;
use Laravel\Mcp\Server\Concerns\HasAnnotations;
use Laravel\Mcp\Server\Contracts\HasUriTemplate;

abstract class Resource extends Primitive
{
Expand All @@ -18,9 +19,11 @@ abstract class Resource extends Primitive

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

return $this->uri !== '' ? $this->uri : 'file://resources/'.Str::kebab(class_basename($this));
}

public function mimeType(): string
Expand All @@ -43,7 +46,8 @@ public function toMethodCall(): array
* name: string,
* title: string,
* description: string,
* uri: string,
* uri?: string,
* uriTemplate?: string,
* mimeType: string,
* _meta?: array<string, mixed>
* }
Expand All @@ -56,14 +60,19 @@ public function toArray(): array
'name' => $this->name(),
'title' => $this->title(),
'description' => $this->description(),
'uri' => $this->uri(),
'mimeType' => $this->mimeType(),
];

if ($annotations !== []) {
$data['annotations'] = $annotations;
}

if ($this instanceof HasUriTemplate) {
$data['uriTemplate'] = (string) $this->uriTemplate();
} else {
$data['uri'] = $this->uri();
}

// @phpstan-ignore return.type
return $this->mergeMeta($data);
}
Expand Down
59 changes: 45 additions & 14 deletions src/Server/ServerContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use Illuminate\Container\Container;
use Illuminate\Support\Collection;
use Laravel\Mcp\Server\Contracts\HasUriTemplate;

class ServerContext
{
Expand Down Expand Up @@ -36,38 +37,68 @@ public function __construct(
*/
public function tools(): Collection
{
return collect($this->tools)->map(fn (Tool|string $toolClass) => is_string($toolClass)
? Container::getInstance()->make($toolClass)
: $toolClass
)->filter(fn (Tool $tool): bool => $tool->eligibleForRegistration());
/** @var Collection<int,Tool> $tools */
$tools = collect($this->tools);

return $this->resolvePrimitives($tools);
}

/**
* @return Collection<int, Resource>
*/
public function resources(): Collection
{
return collect($this->resources)->map(
fn (Resource|string $resourceClass) => is_string($resourceClass)
? Container::getInstance()->make($resourceClass)
: $resourceClass
)->filter(fn (Resource $resource): bool => $resource->eligibleForRegistration());
/** @var Collection<int,Resource> $resourceTemplates */
$resourceTemplates = collect($this->resources)
->filter(fn (Resource|string $resource): bool => ! $this->isResourceTemplate($resource));

return $this->resolvePrimitives($resourceTemplates);
}

/**
* @return Collection<int, HasUriTemplate&Resource>
*/
public function resourceTemplates(): Collection
{
/** @var Collection<int,HasUriTemplate&Resource> $resourceTemplates */
$resourceTemplates = collect($this->resources)
->filter(fn (Resource|string $resource): bool => $this->isResourceTemplate($resource));

return $this->resolvePrimitives($resourceTemplates);
}

/**
* @return Collection<int, Prompt>
*/
public function prompts(): Collection
{
return collect($this->prompts)->map(
fn ($promptClass) => is_string($promptClass)
? Container::getInstance()->make($promptClass)
: $promptClass
)->filter(fn (Prompt $prompt): bool => $prompt->eligibleForRegistration());
/** @var Collection<int,Prompt> $prompts */
$prompts = collect($this->prompts);

return $this->resolvePrimitives($prompts);
}

public function perPage(?int $requestedPerPage = null): int
{
return min($requestedPerPage ?? $this->defaultPaginationLength, $this->maxPaginationLength);
}

/**
* @template T of Primitive
*
* @param Collection<int, T|string> $primitive
* @return Collection<int, T>
*/
private function resolvePrimitives(Collection $primitive): Collection
{
return $primitive->map(fn (Primitive|string $primitiveClass) => is_string($primitiveClass)
? Container::getInstance()->make($primitiveClass)
: $primitiveClass)
->filter(fn (Primitive $primitive): bool => $primitive->eligibleForRegistration());
}

private function isResourceTemplate(Resource|string $resource): bool
{
return $resource instanceof HasUriTemplate || (is_string($resource) && is_subclass_of($resource, HasUriTemplate::class));
}
}
Loading
Loading