Skip to content

Commit 8582b6c

Browse files
committed
fix: adding support for getters and actions
1 parent 030bbbb commit 8582b6c

23 files changed

+930
-319
lines changed

src/Filters/Filter.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@ public function dd(): self
261261

262262
protected function booted() {}
263263

264-
protected function getType(): string
264+
public function getType(): string
265265
{
266266
return $this->type;
267267
}

src/Filters/MatchFilter.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use Binaryk\LaravelRestify\Contracts\RestifySearchable;
66
use Binaryk\LaravelRestify\Http\Requests\RestifyRequest;
7+
use Binaryk\LaravelRestify\MCP\Concerns\FieldMcpSchemaDetection;
78
use Closure;
89
use Illuminate\Database\Eloquent\Builder;
910
use Illuminate\Database\Eloquent\Relations\Relation;
@@ -129,4 +130,27 @@ public function usingClosure(Closure $closure): self
129130

130131
return $this;
131132
}
133+
134+
public function description(): string
135+
{
136+
$description = "This is a exact match for $this->column (e.g., $this->column=some_value). It accepts negation by prefixing the column with a hyphen (e.g., -$this->column=some_value). The filter type is string.";
137+
138+
if ($this->getType() === RestifySearchable::MATCH_BETWEEN) {
139+
$description = "This is a range match for $this->column (e.g., $this->column=value1,value2). It accepts negation by prefixing the column with a hyphen (e.g., -$this->column=value1,value2). Accepted values can be any.";
140+
}
141+
142+
if ($this->getType() === RestifySearchable::MATCH_DATETIME) {
143+
$description = "This is a date match for $this->column (e.g., $this->column=YYYY-MM-DD or $this->column=value1,value2 for range). It accepts negation by prefixing the column with a hyphen (e.g., -$this->column=YYYY-MM-DD or -$this->column=value1,value2 for range). Accepted values are date.";
144+
}
145+
146+
if ($this->getType() === RestifySearchable::MATCH_BOOL) {
147+
$description = "This is a boolean match for $this->column (e.g., $this->column=true or $this->column=false). It accepts negation by prefixing the column with a hyphen (e.g., -$this->column=true or -$this->column=false). Accepted values are boolean.";
148+
}
149+
150+
if ($this->getType() === RestifySearchable::MATCH_ARRAY) {
151+
$description = "This is an array match for $this->column (e.g., $this->column=value1,value2). It accepts negation by prefixing the column with a hyphen (e.g., -$this->column=value1,value2). The values acccepted can be any.";
152+
}
153+
154+
return $description;
155+
}
132156
}

src/Filters/SearchablesCollection.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
namespace Binaryk\LaravelRestify\Filters;
44

5+
use Binaryk\LaravelRestify\Http\Requests\RestifyRequest;
6+
use Binaryk\LaravelRestify\Repositories\Repository;
7+
use Illuminate\Database\Eloquent\Model;
58
use Illuminate\Support\Collection;
69

710
/**
@@ -59,4 +62,24 @@ public function formatForDocumentation(): string
5962

6063
return implode(', ', $fields);
6164
}
65+
66+
/**
67+
* Process searchables for search functionality, creating SearchableFilter instances.
68+
*/
69+
public function forSearch(Model $model, Repository $repository): Collection
70+
{
71+
return $this->map(function ($searchable, $key) use ($model, $repository) {
72+
// If it's already a Filter instance, set repository and return
73+
if ($searchable instanceof Filter) {
74+
return $searchable->setRepository($repository);
75+
}
76+
77+
// Create SearchableFilter for string fields
78+
$columnName = is_numeric($key) ? $searchable : $key;
79+
80+
return SearchableFilter::make()
81+
->setColumn($model->qualifyColumn($columnName))
82+
->setRepository($repository);
83+
});
84+
}
6285
}

src/Http/Requests/ActionRequest.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace Binaryk\LaravelRestify\Http\Requests;
44

55
use Binaryk\LaravelRestify\Actions\Action;
6+
use Binaryk\LaravelRestify\MCP\Requests\McpActionRequest;
67
use Binaryk\LaravelRestify\Services\Search\RepositorySearchService;
78
use Closure;
89
use Illuminate\Database\Eloquent\Builder;
@@ -64,6 +65,10 @@ public function collectRepositories(Action $action, $count, Closure $callback):
6465

6566
public function isForRepositoryRequest(): bool
6667
{
68+
if ($this instanceof McpActionRequest) {
69+
return $this->input('id') != null;
70+
}
71+
6772
return $this instanceof RepositoryActionRequest;
6873
}
6974
}

src/MCP/Concerns/McpActionTool.php

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
<?php
2+
3+
namespace Binaryk\LaravelRestify\MCP\Concerns;
4+
5+
use Binaryk\LaravelRestify\Actions\Action;
6+
use Binaryk\LaravelRestify\Http\Requests\ActionRequest;
7+
use Binaryk\LaravelRestify\MCP\Requests\McpActionRequest;
8+
use Binaryk\LaravelRestify\MCP\Requests\McpRequest;
9+
use Laravel\Mcp\Server\Tools\ToolInputSchema;
10+
11+
/**
12+
* @mixin \Binaryk\LaravelRestify\Repositories\Repository
13+
*/
14+
trait McpActionTool
15+
{
16+
public function actionTool(Action $action, array $arguments, McpActionRequest $actionRequest): array
17+
{
18+
$actionRequest->merge($arguments);
19+
20+
$this->sanitizeToolRequest($actionRequest, $arguments);
21+
22+
if ($id = $actionRequest->input('id')) {
23+
if (!$action->authorizedToRun($actionRequest, $actionRequest->findModelOrFail($id))) {
24+
return [
25+
'error' => 'Not authorized to run this action',
26+
'getter' => $action->uriKey(),
27+
];
28+
}
29+
}
30+
31+
32+
// Set up the action request context based on action type
33+
if (!$action->isStandalone()) {
34+
if (isset($arguments['id'])) {
35+
// Single model action (show context)
36+
$actionRequest->merge(['id' => $arguments['id']]);
37+
} elseif (isset($arguments['repositories'])) {
38+
// Multiple models action (index context)
39+
$actionRequest->merge(['repositories' => $arguments['repositories']]);
40+
}
41+
}
42+
43+
// Check authorization
44+
if (!$action->authorizedToSee($actionRequest)) {
45+
return [
46+
'error' => 'Not authorized to see this action',
47+
'action' => $action->uriKey(),
48+
];
49+
}
50+
51+
try {
52+
$result = $action->handleRequest($actionRequest);
53+
54+
return [
55+
'success' => true,
56+
'action' => $action->uriKey(),
57+
'result' => $result->getData(),
58+
];
59+
} catch (\Exception $e) {
60+
return [
61+
'error' => $e->getMessage(),
62+
'action' => $action->uriKey(),
63+
];
64+
}
65+
}
66+
67+
public static function actionToolSchema(Action $action, ToolInputSchema $schema, McpActionRequest $mcpRequest): void
68+
{
69+
$modelName = class_basename(static::$model);
70+
71+
// Add action-specific validation rules
72+
$actionRules = $action->rules();
73+
foreach ($actionRules as $field => $rules) {
74+
$rulesArray = is_array($rules) ? $rules : explode('|', $rules);
75+
$isRequired = in_array('required', $rulesArray);
76+
77+
// Determine field type based on rules
78+
if (in_array('boolean', $rulesArray)) {
79+
$fieldSchema = $schema->boolean($field);
80+
} elseif (in_array('integer', $rulesArray) || in_array('numeric', $rulesArray)) {
81+
$fieldSchema = $schema->number($field);
82+
} elseif (in_array('array', $rulesArray)) {
83+
$fieldSchema = $schema->array($field);
84+
} else {
85+
$fieldSchema = $schema->string($field);
86+
}
87+
88+
if ($isRequired) {
89+
$fieldSchema->required();
90+
}
91+
92+
$fieldSchema->description("Action parameter: {$field}");
93+
}
94+
95+
// Add context-specific fields based on action type
96+
if ($action->isStandalone()) {
97+
// Standalone actions don't need ID or repositories
98+
$schema->string('include')
99+
->description('Comma-separated list of relationships to include in response');
100+
} else {
101+
// Check if it's primarily a show action or index action
102+
$shownOnShow = $action->isShownOnShow($mcpRequest, app(static::class));
103+
$shownOnIndex = $action->isShownOnIndex($mcpRequest, app(static::class));
104+
105+
if ($shownOnShow && !$shownOnIndex) {
106+
// Show action - requires single ID
107+
$schema->string('id')
108+
->description("The ID of the {$modelName} to perform the action on")
109+
->required();
110+
111+
$schema->string('include')
112+
->description('Comma-separated list of relationships to include');
113+
} else {
114+
// Index action - requires repositories array
115+
$schema->string('repositories')
116+
->description("Array of {$modelName} IDs to perform the action on. e.g. repositories=[1,2,3]")
117+
->required();
118+
119+
$schema->string('include')
120+
->description('Comma-separated list of relationships to include');
121+
}
122+
}
123+
}
124+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
namespace Binaryk\LaravelRestify\MCP\Concerns;
4+
5+
use Binaryk\LaravelRestify\MCP\Requests\McpRequest;
6+
use Laravel\Mcp\Server\Tools\ToolInputSchema;
7+
8+
/**
9+
* @mixin \Binaryk\LaravelRestify\Repositories\Repository
10+
*/
11+
trait McpDestroyTool
12+
{
13+
public function deleteTool(array $arguments, McpRequest $request): array
14+
{
15+
$id = $arguments['id'] ?? null;
16+
unset($arguments['id']);
17+
$request->merge($arguments);
18+
$this->sanitizeToolRequest($request, $arguments);
19+
20+
$model = static::query($request)->findOrFail($id);
21+
22+
return static::resolveWith($model)->destroy($request, $id);
23+
}
24+
25+
public static function destroyToolSchema(ToolInputSchema $schema): void
26+
{
27+
$key = static::uriKey();
28+
$modelName = class_basename(static::$model);
29+
30+
$schema->string('id')
31+
->description("The ID of the $modelName to delete")
32+
->required();
33+
}
34+
}

src/MCP/Concerns/McpGetterTool.php

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
<?php
2+
3+
namespace Binaryk\LaravelRestify\MCP\Concerns;
4+
5+
use Binaryk\LaravelRestify\Getters\Getter;
6+
use Binaryk\LaravelRestify\Http\Requests\GetterRequest;
7+
use Binaryk\LaravelRestify\MCP\Requests\McpGetterRequest;
8+
use Binaryk\LaravelRestify\MCP\Requests\McpRequest;
9+
use Illuminate\Http\JsonResponse;
10+
use Laravel\Mcp\Server\Tools\ToolInputSchema;
11+
12+
/**
13+
* @mixin \Binaryk\LaravelRestify\Repositories\Repository
14+
*/
15+
trait McpGetterTool
16+
{
17+
public function getterTool(Getter $getter, array $arguments, McpGetterRequest $getterRequest): array
18+
{
19+
$getterRequest->merge($arguments);
20+
21+
$this->sanitizeToolRequest($getterRequest, $arguments);
22+
23+
if ($id = $getterRequest->input('id')) {
24+
if (!$getter->authorizedToRun($getterRequest, $getterRequest->findModelOrFail($id))) {
25+
return [
26+
'error' => 'Not authorized to run this getter',
27+
'getter' => $getter->uriKey(),
28+
];
29+
}
30+
}
31+
32+
try {
33+
$result = $getter->handleRequest($getterRequest);
34+
35+
// Handle different response types
36+
$responseData = $result instanceof JsonResponse
37+
? $result->getData()
38+
: $result->getContent();
39+
40+
return [
41+
'success' => true,
42+
'getter' => $getter->uriKey(),
43+
'result' => $responseData,
44+
];
45+
} catch (\Exception $e) {
46+
return [
47+
'error' => $e->getMessage(),
48+
'getter' => $getter->uriKey(),
49+
];
50+
}
51+
}
52+
53+
public static function getterToolSchema(Getter $getter, ToolInputSchema $schema, McpGetterRequest $mcpRequest): void
54+
{
55+
$modelName = class_basename(static::$model);
56+
57+
// Add getter-specific validation rules if the getter has a rules method
58+
if (method_exists($getter, 'rules')) {
59+
$getterRules = $getter->rules();
60+
foreach ($getterRules as $field => $rules) {
61+
$rulesArray = is_array($rules) ? $rules : explode('|', $rules);
62+
$isRequired = in_array('required', $rulesArray);
63+
64+
// Determine field type based on rules
65+
if (in_array('boolean', $rulesArray)) {
66+
$fieldSchema = $schema->boolean($field);
67+
} elseif (in_array('integer', $rulesArray) || in_array('numeric', $rulesArray)) {
68+
$fieldSchema = $schema->number($field);
69+
} elseif (in_array('array', $rulesArray)) {
70+
$fieldSchema = $schema->array($field);
71+
} else {
72+
$fieldSchema = $schema->string($field);
73+
}
74+
75+
if ($isRequired) {
76+
$fieldSchema->required();
77+
}
78+
79+
$fieldSchema->description("Getter parameter: {$field}");
80+
}
81+
}
82+
83+
// Check if it's primarily a show getter or index getter
84+
$shownOnShow = $getter->isShownOnShow($mcpRequest, app(static::class));
85+
$shownOnIndex = $getter->isShownOnIndex($mcpRequest, app(static::class));
86+
87+
if ($shownOnShow && !$shownOnIndex) {
88+
// Show getter - requires single ID
89+
$schema->string('id')
90+
->description("The ID of the {$modelName} to execute the getter on")
91+
->required();
92+
93+
$schema->string('include')
94+
->description('Comma-separated list of relationships to include in response');
95+
} else {
96+
// Index getters typically don't require specific IDs as they work on collections/aggregates
97+
$schema->string('include')
98+
->description('Comma-separated list of relationships to include in response');
99+
}
100+
}
101+
}

0 commit comments

Comments
 (0)