Skip to content

Commit 6152d0f

Browse files
committed
fix: wip
1 parent ffb816c commit 6152d0f

File tree

8 files changed

+615
-210
lines changed

8 files changed

+615
-210
lines changed

src/Fields/Field.php

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

55
use Binaryk\LaravelRestify\Fields\Concerns\HasAction;
66
use Binaryk\LaravelRestify\Http\Requests\RestifyRequest;
7+
use Binaryk\LaravelRestify\MCP\Concerns\FieldMcpSchemaDetection;
78
use Binaryk\LaravelRestify\Repositories\Repository;
89
use Binaryk\LaravelRestify\Traits\Make;
910
use Closure;
@@ -12,13 +13,13 @@
1213
use Illuminate\Support\Str;
1314
use Illuminate\Validation\Rules\Unique;
1415
use JsonSerializable;
15-
use Laravel\Mcp\Server\Tools\ToolInputSchema;
1616
use ReturnTypeWillChange;
1717

1818
class Field extends OrganicField implements JsonSerializable
1919
{
20-
use HasAction;
2120
use Make;
21+
use HasAction;
22+
use FieldMcpSchemaDetection;
2223

2324
/**
2425
* The resource associated with the field.
@@ -925,202 +926,4 @@ public function toolSchema(callable|Closure $callback): self
925926

926927
return $this;
927928
}
928-
929-
/**
930-
* Resolve the tool schema for this field to be used in MCP tools.
931-
*/
932-
public function resolveToolSchema(ToolInputSchema $schema, Repository $repository): self
933-
{
934-
// Check if there's a custom callback defined
935-
if (is_callable($this->toolInputSchemaCallback)) {
936-
call_user_func($this->toolInputSchemaCallback, $schema, $repository, $this);
937-
938-
return $this;
939-
}
940-
941-
// Skip computed fields for default implementation
942-
if ($this->computed()) {
943-
return $this;
944-
}
945-
946-
$attribute = $this->label ?? $this->attribute;
947-
$fieldType = $this->guessFieldType();
948-
949-
// Add the field to schema based on its type
950-
$schemaField = match ($fieldType) {
951-
'boolean' => $schema->boolean($attribute),
952-
'number' => $schema->number($attribute),
953-
'array' => $schema->string($attribute), // Arrays are typically sent as JSON strings
954-
default => $schema->string($attribute)
955-
};
956-
957-
// Add description
958-
$description = $this->generateFieldDescription($repository);
959-
$schemaField->description($description);
960-
961-
// Mark as required if field has required validation
962-
if ($this->isRequired()) {
963-
$schemaField->required();
964-
}
965-
966-
return $this;
967-
}
968-
969-
/**
970-
* Generate a comprehensive description for the field.
971-
*/
972-
protected function generateFieldDescription(Repository $repository): string
973-
{
974-
$attribute = $this->label ?? $this->attribute;
975-
$fieldType = $this->guessFieldType();
976-
977-
$description = "Field: {$attribute} (type: {$fieldType})";
978-
979-
// Add validation rules information
980-
$rules = $this->getStoringRules();
981-
if (! empty($rules)) {
982-
$ruleDescriptions = $this->formatValidationRules($rules);
983-
if (! empty($ruleDescriptions)) {
984-
$description .= '. Validation: '.implode(', ', $ruleDescriptions);
985-
}
986-
}
987-
988-
// Add relationship information for relationship fields
989-
if ($this->isRelationshipField()) {
990-
$description .= '. This is a relationship field';
991-
}
992-
993-
// Add file information for file fields
994-
if ($this instanceof File) {
995-
$description .= '. Upload a file';
996-
}
997-
998-
// Add examples based on field type and name
999-
$examples = $this->generateFieldExamples();
1000-
if (! empty($examples)) {
1001-
$description .= '. Examples: '.implode(', ', $examples);
1002-
}
1003-
1004-
return $description;
1005-
}
1006-
1007-
/**
1008-
* Check if field is required based on validation rules.
1009-
*/
1010-
protected function isRequired(): bool
1011-
{
1012-
$rules = $this->getStoringRules();
1013-
1014-
return in_array('required', $rules) ||
1015-
collect($rules)->contains(function ($rule) {
1016-
return is_string($rule) && str_starts_with($rule, 'required');
1017-
});
1018-
}
1019-
1020-
/**
1021-
* Check if field is a relationship field.
1022-
*/
1023-
protected function isRelationshipField(): bool
1024-
{
1025-
return $this instanceof \Binaryk\LaravelRestify\Fields\BelongsTo ||
1026-
$this instanceof \Binaryk\LaravelRestify\Fields\HasOne ||
1027-
$this instanceof \Binaryk\LaravelRestify\Fields\HasMany ||
1028-
$this instanceof \Binaryk\LaravelRestify\Fields\BelongsToMany;
1029-
}
1030-
1031-
/**
1032-
* Format validation rules for display.
1033-
*/
1034-
protected function formatValidationRules(array $rules): array
1035-
{
1036-
$formatted = [];
1037-
1038-
foreach ($rules as $rule) {
1039-
if (is_string($rule)) {
1040-
$formatted[] = match (true) {
1041-
$rule === 'required' => 'required',
1042-
str_starts_with($rule, 'min:') => 'minimum '.substr($rule, 4).' characters',
1043-
str_starts_with($rule, 'max:') => 'maximum '.substr($rule, 4).' characters',
1044-
str_starts_with($rule, 'between:') => 'between '.str_replace(',', ' and ', substr($rule, 8)),
1045-
$rule === 'email' => 'valid email format',
1046-
$rule === 'url' => 'valid URL format',
1047-
$rule === 'numeric' => 'numeric value',
1048-
$rule === 'integer' => 'integer value',
1049-
$rule === 'boolean' => 'boolean value (true/false)',
1050-
$rule === 'array' => 'array format',
1051-
str_starts_with($rule, 'in:') => 'allowed values: '.str_replace(',', ', ', substr($rule, 3)),
1052-
default => $rule
1053-
};
1054-
}
1055-
}
1056-
1057-
return array_filter($formatted);
1058-
}
1059-
1060-
/**
1061-
* Generate examples for the field.
1062-
*/
1063-
protected function generateFieldExamples(): array
1064-
{
1065-
$attribute = strtolower($this->attribute);
1066-
$fieldType = $this->guessFieldType();
1067-
1068-
return match ($fieldType) {
1069-
'boolean' => ['true', 'false'],
1070-
'number' => $this->getNumberExamples($attribute),
1071-
'array' => ['["item1", "item2"]', '{"key": "value"}'],
1072-
default => $this->getStringExamples($attribute)
1073-
};
1074-
}
1075-
1076-
/**
1077-
* Get number field examples.
1078-
*/
1079-
protected function getNumberExamples(string $attribute): array
1080-
{
1081-
if (str_contains($attribute, 'price') || str_contains($attribute, 'cost') || str_contains($attribute, 'amount')) {
1082-
return ['99.99', '29.95'];
1083-
}
1084-
if (str_contains($attribute, 'age')) {
1085-
return ['25', '30'];
1086-
}
1087-
if (str_contains($attribute, 'year')) {
1088-
return ['2024', '2023'];
1089-
}
1090-
if (str_ends_with($attribute, '_id')) {
1091-
return ['1', '42'];
1092-
}
1093-
1094-
return ['1', '100'];
1095-
}
1096-
1097-
/**
1098-
* Get string field examples.
1099-
*/
1100-
protected function getStringExamples(string $attribute): array
1101-
{
1102-
if (str_contains($attribute, 'email')) {
1103-
1104-
}
1105-
if (str_contains($attribute, 'name')) {
1106-
return ['John Doe', 'Sample Name'];
1107-
}
1108-
if (str_contains($attribute, 'title')) {
1109-
return ['Sample Title', 'My Title'];
1110-
}
1111-
if (str_contains($attribute, 'description')) {
1112-
return ['A detailed description...', 'Brief summary'];
1113-
}
1114-
if (str_contains($attribute, 'url') || str_contains($attribute, 'link')) {
1115-
return ['https://example.com', 'https://website.org/path'];
1116-
}
1117-
if (str_contains($attribute, 'phone')) {
1118-
return ['+1234567890', '(555) 123-4567'];
1119-
}
1120-
if (str_contains($attribute, 'password')) {
1121-
return ['SecurePassword123!', 'MyPassword456'];
1122-
}
1123-
1124-
return ['sample text', 'example value'];
1125-
}
1126929
}

src/Fields/FieldCollection.php

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

55
use Binaryk\LaravelRestify\Http\Requests\RestifyRequest;
6+
use Binaryk\LaravelRestify\MCP\Requests\McpRequest;
67
use Binaryk\LaravelRestify\Repositories\Repository;
78
use Illuminate\Http\Request;
89
use Illuminate\Support\Collection;
@@ -66,6 +67,27 @@ public function forIndex(RestifyRequest $request, $repository): self
6667
})->values();
6768
}
6869

70+
public function forMcpIndex(RestifyRequest $request, $repository): self
71+
{
72+
// If this is an MCP request and repository has fieldsForMcpIndex method
73+
if ($request instanceof McpRequest && method_exists($repository, 'fieldsForMcpIndex')) {
74+
// Get the MCP-specific fields from the repository
75+
$mcpFields = $repository->fieldsForMcpIndex($request);
76+
$mcpFieldAttributes = collect($mcpFields)->map(fn($field) => $field->attribute)->toArray();
77+
78+
// Filter the current collection to only include MCP fields
79+
return $this
80+
->filter(fn (Field $field) => ! $field instanceof EagerField)
81+
->filter(fn (Field $field) => in_array($field->attribute, $mcpFieldAttributes))
82+
->filter(function (Field $field) use ($repository, $request) {
83+
return $field->isShownOnMcp($request, $repository);
84+
})->values();
85+
}
86+
87+
// Fallback to regular index filtering for non-MCP requests
88+
return $this->forIndex($request, $repository);
89+
}
90+
6991
public function forShow(RestifyRequest $request, $repository): self
7092
{
7193
return $this
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?php
2+
3+
namespace Binaryk\LaravelRestify\Filters;
4+
5+
use Illuminate\Support\Collection;
6+
7+
/**
8+
* @template TKey of array-key
9+
* @template TValue
10+
*
11+
* @extends \Illuminate\Support\Collection<TKey, TValue>
12+
*/
13+
class SearchablesCollection extends Collection
14+
{
15+
public function __construct($items = [])
16+
{
17+
$unified = [];
18+
19+
foreach ($items as $column => $searchable) {
20+
if ($searchable instanceof SearchableFilter) {
21+
// Extract column name from Filter object
22+
$unified[] = $searchable->column();
23+
} elseif (is_string($searchable)) {
24+
// Direct string field name
25+
$unified[] = $searchable;
26+
} elseif (is_string($column) && is_numeric($column) === false) {
27+
// Array key is the field name
28+
$unified[] = $column;
29+
} elseif (is_numeric($column) && is_string($searchable)) {
30+
// Numeric key with string value (e.g., ['name', 'email'])
31+
$unified[] = $searchable;
32+
}
33+
}
34+
35+
parent::__construct(array_unique($unified));
36+
}
37+
38+
/**
39+
* Get searchable field names only (no filter objects).
40+
*/
41+
public function fieldNames(): array
42+
{
43+
return $this->filter(fn($item) => is_string($item) && !empty($item))
44+
->unique()
45+
->values()
46+
->toArray();
47+
}
48+
49+
/**
50+
* Format searchable fields for documentation.
51+
*/
52+
public function formatForDocumentation(): string
53+
{
54+
$fields = $this->fieldNames();
55+
56+
if (empty($fields)) {
57+
return 'No searchable fields available';
58+
}
59+
60+
return implode(', ', $fields);
61+
}
62+
}

0 commit comments

Comments
 (0)