Skip to content

Commit 6e49cfa

Browse files
Add _meta support (#106)
* Add meta and security scheme to tools * Fix phpstan definition * WIP 🚧 * WIP * Update the meta $property addition * Introduce `ResponseFactory` for handling responses with result-level metadata. * Replace `UnexpectedValueException` with `InvalidArgumentException` in `ResponseFactory` and improve type validation logic. * Formatting * Fix test and change the API * Refactor * Refactor * Update method signatures * Update Test * Improve testing --------- Co-authored-by: zacksmash <[email protected]>
1 parent 3599ce8 commit 6e49cfa

37 files changed

+1257
-48
lines changed

src/Request.php

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,12 @@ class Request implements Arrayable
2424

2525
/**
2626
* @param array<string, mixed> $arguments
27+
* @param array<string, mixed>|null $meta
2728
*/
2829
public function __construct(
2930
protected array $arguments = [],
30-
protected ?string $sessionId = null
31+
protected ?string $sessionId = null,
32+
protected ?array $meta = null,
3133
) {
3234
//
3335
}
@@ -92,6 +94,14 @@ public function sessionId(): ?string
9294
return $this->sessionId;
9395
}
9496

97+
/**
98+
* @return array<string, mixed>|null
99+
*/
100+
public function meta(): ?array
101+
{
102+
return $this->meta;
103+
}
104+
95105
/**
96106
* @param array<string, mixed> $arguments
97107
*/
@@ -104,4 +114,12 @@ public function setSessionId(?string $sessionId): void
104114
{
105115
$this->sessionId = $sessionId;
106116
}
117+
118+
/**
119+
* @param array<string, mixed>|null $meta
120+
*/
121+
public function setMeta(?array $meta): void
122+
{
123+
$this->meta = $meta;
124+
}
107125
}

src/Response.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,24 @@ public function content(): Content
6868
return $this->content;
6969
}
7070

71+
/**
72+
* @param Response|array<int, Response> $responses
73+
*/
74+
public static function make(Response|array $responses): ResponseFactory
75+
{
76+
return new ResponseFactory($responses);
77+
}
78+
79+
/**
80+
* @param array<string, mixed>|string $meta
81+
*/
82+
public function withMeta(array|string $meta, mixed $value = null): static
83+
{
84+
$this->content->setMeta($meta, $value);
85+
86+
return $this;
87+
}
88+
7189
/**
7290
* @throws NotImplementedException
7391
*/

src/ResponseFactory.php

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Laravel\Mcp;
6+
7+
use Illuminate\Support\Arr;
8+
use Illuminate\Support\Collection;
9+
use Illuminate\Support\Traits\Conditionable;
10+
use Illuminate\Support\Traits\Macroable;
11+
use InvalidArgumentException;
12+
use Laravel\Mcp\Server\Concerns\HasMeta;
13+
14+
class ResponseFactory
15+
{
16+
use Conditionable;
17+
use HasMeta;
18+
use Macroable;
19+
20+
/**
21+
* @var Collection<int, Response>
22+
*/
23+
protected Collection $responses;
24+
25+
/**
26+
* @param Response|array<int, Response> $responses
27+
*/
28+
public function __construct(Response|array $responses)
29+
{
30+
$wrapped = Arr::wrap($responses);
31+
32+
foreach ($wrapped as $index => $response) {
33+
if (! $response instanceof Response) {
34+
throw new InvalidArgumentException(
35+
"Invalid response type at index {$index}: Expected ".Response::class.', but received '.get_debug_type($response).'.'
36+
);
37+
}
38+
}
39+
40+
$this->responses = collect($wrapped);
41+
}
42+
43+
/**
44+
* @param string|array<string, mixed> $meta
45+
*/
46+
public function withMeta(string|array $meta, mixed $value = null): static
47+
{
48+
$this->setMeta($meta, $value);
49+
50+
return $this;
51+
}
52+
53+
/**
54+
* @return Collection<int, Response>
55+
*/
56+
public function responses(): Collection
57+
{
58+
return $this->responses;
59+
}
60+
61+
/**
62+
* @return array<string, mixed>|null
63+
*/
64+
public function getMeta(): ?array
65+
{
66+
return $this->meta;
67+
}
68+
}

src/Server/Concerns/HasMeta.php

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Laravel\Mcp\Server\Concerns;
6+
7+
use InvalidArgumentException;
8+
9+
trait HasMeta
10+
{
11+
/**
12+
* @var array<string, mixed>|null
13+
*/
14+
protected ?array $meta = null;
15+
16+
/**
17+
* @param array<string, mixed>|string $meta
18+
*/
19+
public function setMeta(array|string $meta, mixed $value = null): void
20+
{
21+
$this->meta ??= [];
22+
23+
if (! is_array($meta)) {
24+
if (is_null($value)) {
25+
throw new InvalidArgumentException('Value is required when using key-value signature.');
26+
}
27+
28+
$this->meta[$meta] = $value;
29+
30+
return;
31+
}
32+
33+
$this->meta = array_merge($this->meta, $meta);
34+
}
35+
36+
/**
37+
* @template T of array<string, mixed>
38+
*
39+
* @param T $baseArray
40+
* @return T&array{_meta?: array<string, mixed>}
41+
*/
42+
public function mergeMeta(array $baseArray): array
43+
{
44+
return ($meta = $this->meta)
45+
? [...$baseArray, '_meta' => $meta]
46+
: $baseArray;
47+
}
48+
}

src/Server/Content/Blob.php

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,16 @@
55
namespace Laravel\Mcp\Server\Content;
66

77
use InvalidArgumentException;
8+
use Laravel\Mcp\Server\Concerns\HasMeta;
89
use Laravel\Mcp\Server\Contracts\Content;
910
use Laravel\Mcp\Server\Prompt;
1011
use Laravel\Mcp\Server\Resource;
1112
use Laravel\Mcp\Server\Tool;
1213

1314
class Blob implements Content
1415
{
16+
use HasMeta;
17+
1518
public function __construct(protected string $content)
1619
{
1720
//
@@ -42,13 +45,13 @@ public function toPrompt(Prompt $prompt): array
4245
*/
4346
public function toResource(Resource $resource): array
4447
{
45-
return [
48+
return $this->mergeMeta([
4649
'blob' => base64_encode($this->content),
4750
'uri' => $resource->uri(),
4851
'name' => $resource->name(),
4952
'title' => $resource->title(),
5053
'mimeType' => $resource->mimeType(),
51-
];
54+
]);
5255
}
5356

5457
public function __toString(): string
@@ -61,9 +64,9 @@ public function __toString(): string
6164
*/
6265
public function toArray(): array
6366
{
64-
return [
67+
return $this->mergeMeta([
6568
'type' => 'blob',
6669
'blob' => $this->content,
67-
];
70+
]);
6871
}
6972
}

src/Server/Content/Notification.php

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,16 @@
44

55
namespace Laravel\Mcp\Server\Content;
66

7+
use Laravel\Mcp\Server\Concerns\HasMeta;
78
use Laravel\Mcp\Server\Contracts\Content;
89
use Laravel\Mcp\Server\Prompt;
910
use Laravel\Mcp\Server\Resource;
1011
use Laravel\Mcp\Server\Tool;
1112

1213
class Notification implements Content
1314
{
15+
use HasMeta;
16+
1417
/**
1518
* @param array<string, mixed> $params
1619
*/
@@ -53,9 +56,15 @@ public function __toString(): string
5356
*/
5457
public function toArray(): array
5558
{
59+
$params = $this->params;
60+
61+
if ($this->meta !== null && $this->meta !== [] && ! isset($params['_meta'])) {
62+
$params['_meta'] = $this->meta;
63+
}
64+
5665
return [
5766
'method' => $this->method,
58-
'params' => $this->params,
67+
'params' => $params,
5968
];
6069
}
6170
}

src/Server/Content/Text.php

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,16 @@
44

55
namespace Laravel\Mcp\Server\Content;
66

7+
use Laravel\Mcp\Server\Concerns\HasMeta;
78
use Laravel\Mcp\Server\Contracts\Content;
89
use Laravel\Mcp\Server\Prompt;
910
use Laravel\Mcp\Server\Resource;
1011
use Laravel\Mcp\Server\Tool;
1112

1213
class Text implements Content
1314
{
15+
use HasMeta;
16+
1417
public function __construct(protected string $text)
1518
{
1619
//
@@ -37,13 +40,13 @@ public function toPrompt(Prompt $prompt): array
3740
*/
3841
public function toResource(Resource $resource): array
3942
{
40-
return [
43+
return $this->mergeMeta([
4144
'text' => $this->text,
4245
'uri' => $resource->uri(),
4346
'name' => $resource->name(),
4447
'title' => $resource->title(),
4548
'mimeType' => $resource->mimeType(),
46-
];
49+
]);
4750
}
4851

4952
public function __toString(): string
@@ -56,9 +59,9 @@ public function __toString(): string
5659
*/
5760
public function toArray(): array
5861
{
59-
return [
62+
return $this->mergeMeta([
6063
'type' => 'text',
6164
'text' => $this->text,
62-
];
65+
]);
6366
}
6467
}

src/Server/Contracts/Content.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,5 +30,10 @@ public function toPrompt(Prompt $prompt): array;
3030
*/
3131
public function toResource(Resource $resource): array;
3232

33+
/**
34+
* @param array<string, mixed>|string $meta
35+
*/
36+
public function setMeta(array|string $meta, mixed $value = null): void;
37+
3338
public function __toString(): string;
3439
}

src/Server/McpServiceProvider.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ protected function registerContainerCallbacks(): void
8080

8181
$request->setArguments($currentRequest->all());
8282
$request->setSessionId($currentRequest->sessionId());
83+
$request->setMeta($currentRequest->meta());
8384
}
8485
});
8586
}

src/Server/Methods/CallTool.php

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66

77
use Generator;
88
use Illuminate\Container\Container;
9-
use Illuminate\Support\Collection;
109
use Illuminate\Validation\ValidationException;
1110
use Laravel\Mcp\Response;
11+
use Laravel\Mcp\ResponseFactory;
1212
use Laravel\Mcp\Server\Contracts\Errable;
1313
use Laravel\Mcp\Server\Contracts\Method;
1414
use Laravel\Mcp\Server\Exceptions\JsonRpcException;
@@ -61,13 +61,13 @@ public function handle(JsonRpcRequest $request, ServerContext $context): Generat
6161
}
6262

6363
/**
64-
* @return callable(Collection<int, Response>): array{content: array<int, array<string, mixed>>, isError: bool}
64+
* @return callable(ResponseFactory): array<string, mixed>
6565
*/
6666
protected function serializable(Tool $tool): callable
6767
{
68-
return fn (Collection $responses): array => [
69-
'content' => $responses->map(fn (Response $response): array => $response->content()->toTool($tool))->all(),
70-
'isError' => $responses->contains(fn (Response $response): bool => $response->isError()),
71-
];
68+
return fn (ResponseFactory $factory): array => $factory->mergeMeta([
69+
'content' => $factory->responses()->map(fn (Response $response): array => $response->content()->toTool($tool))->all(),
70+
'isError' => $factory->responses()->contains(fn (Response $response): bool => $response->isError()),
71+
]);
7272
}
7373
}

0 commit comments

Comments
 (0)