Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 .github/workflows/psalm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
php-version: '8.3'
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick
coverage: none

Expand Down
9 changes: 5 additions & 4 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@ on: [push, pull_request]
jobs:
test:
runs-on: ${{ matrix.os }}
timeout-minutes: 30
strategy:
fail-fast: true
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
php: [8.2, 8.3, 8.4]
php: [8.3, 8.4]
laravel: [11.*, 12.*]
stability: [prefer-lowest, prefer-stable]
stability: [prefer-stable]
include:
- laravel: 11.*
testbench: 9.*
Expand Down Expand Up @@ -48,4 +49,4 @@ jobs:
run: sleep 5

- name: Execute tests
run: ./vendor/bin/testbench package:test --no-coverage
run: ./vendor/bin/testbench package:test --parallel --no-coverage
7 changes: 4 additions & 3 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,11 @@
}
],
"require": {
"php": "^8.2|^8.3",
"php": "^8.3",
"illuminate/contracts": "^11.0|^12.0",
"laravel/pint": "^1.0",
"laravel/mcp": "^0.1.0",
"laravel/framework": "^11.0|^12.0",
"laravel/mcp": "^0.2.0",
"laravel/pint": "^1.25.1",
"spatie/laravel-data": "^4.4",
"spatie/laravel-package-tools": "^1.12",
"spatie/once": "^3.0"
Expand Down
26 changes: 14 additions & 12 deletions src/Commands/stubs/mcp-tool.stub
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@

namespace DummyNamespace;

use Generator;
use Illuminate\JsonSchema\JsonSchema;
use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\Server\Tool;
use Laravel\Mcp\Server\Tools\ToolInputSchema;
use Laravel\Mcp\Server\Tools\ToolResult;

class DummyClass extends Tool
{
Expand All @@ -19,23 +19,25 @@ class DummyClass extends Tool
return 'Description of what this tool does';
}

public function schema(ToolInputSchema $schema): ToolInputSchema
public function schema(JsonSchema $schema): array
{
// Define your tool's input parameters here
// Example:
// $schema->string('input')
// ->description('The input parameter')
// ->required();
// return [
// 'input' => $schema->string()
// ->description('The input parameter')
// ->required(),
// ];

return $schema;
return [];
}

public function handle(array $arguments): ToolResult|Generator
public function handle(Request $request): Response
{
// Implement your tool's logic here
// Access input parameters via $arguments array
return ToolResult::json([
// Access input parameters via $request->input('parameter_name')

return Response::json([
'success' => true,
'message' => 'Tool executed successfully',
]);
Expand Down
31 changes: 1 addition & 30 deletions src/Fields/Field.php
Original file line number Diff line number Diff line change
Expand Up @@ -774,13 +774,6 @@ public function image(): Image
*/
public function guessFieldType(): string
{
// Check field class type first
$fieldType = $this->guessTypeFromFieldClass();
if ($fieldType) {
return $fieldType;
}

// Check validation rules
$ruleType = $this->guessTypeFromValidationRules();
if ($ruleType) {
return $ruleType;
Expand All @@ -796,28 +789,6 @@ public function guessFieldType(): string
return 'string';
}

/**
* Guess type from field class name.
*/
protected function guessTypeFromFieldClass(): ?string
{
$className = class_basename(static::class);

return match ($className) {
'Boolean', 'BooleanField' => 'boolean',
'Number', 'Integer', 'Decimal', 'Float' => 'number',
'Email' => 'string',
'Password' => 'string',
'Textarea' => 'string',
'Text', 'TextField' => 'string',
'Date', 'DateTime' => 'string',
'File', 'Image' => 'string',
'Select', 'MultiSelect' => 'array',
'BelongsTo', 'HasOne', 'HasMany', 'BelongsToMany' => 'object',
default => null
};
}

/**
* Guess type from validation rules.
*/
Expand All @@ -842,7 +813,7 @@ protected function guessTypeFromValidationRules(): ?string
return 'boolean';
}

if ($this->hasAnyRule($ruleStrings, ['email', 'url', 'ip', 'uuid', 'string', 'regex', 'in', 'array'])) {
if ($this->hasAnyRule($ruleStrings, ['email', 'url', 'ip', 'uuid', 'string', 'regex', 'array'])) {
return 'string';
}

Expand Down
35 changes: 19 additions & 16 deletions src/MCP/Concerns/FieldMcpSchemaDetection.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,36 +4,39 @@

use Binaryk\LaravelRestify\Fields\File;
use Binaryk\LaravelRestify\Repositories\Repository;
use Laravel\Mcp\Server\Tools\ToolInputSchema;
use Illuminate\JsonSchema\JsonSchema;
use Illuminate\JsonSchema\Types\Type;

trait FieldMcpSchemaDetection
{
/**
* Resolve the tool schema for this field to be used in MCP tools.
* Resolve the JSON schema for this field to be used in MCP tools.
*/
public function resolveToolSchema(ToolInputSchema $schema, Repository $repository): self
public function resolveJsonSchema(JsonSchema $schema, Repository $repository): ?Type
{
// Check if there's a custom callback defined
if (is_callable($this->toolInputSchemaCallback)) {
call_user_func($this->toolInputSchemaCallback, $schema, $repository, $this);

return $this;
$result = call_user_func($this->toolInputSchemaCallback, $schema, $repository, $this);
if ($result instanceof Type) {
return $result;
}
}

// Skip computed fields for default implementation
if ($this->computed()) {
return $this;
// For MCP tools, we include computed fields that have resolve callbacks
// since they represent storable fields in MCP contexts
// Only skip truly computed fields without resolve callbacks
if ($this->computed() && ! $this->resolveCallback) {
return null;
}

$attribute = $this->label ?? $this->attribute;
$fieldType = $this->guessFieldType();

// Add the field to schema based on its type
// Create the field schema based on its type
$schemaField = match ($fieldType) {
'boolean' => $schema->boolean($attribute),
'number' => $schema->number($attribute),
'array' => $schema->string($attribute), // Arrays are typically sent as JSON strings
default => $schema->string($attribute)
'boolean' => $schema->boolean(),
'number' => $schema->number(),
'array' => $schema->string(), // Arrays are typically sent as JSON strings
default => $schema->string()
};

// Add description
Expand All @@ -45,7 +48,7 @@ public function resolveToolSchema(ToolInputSchema $schema, Repository $repositor
$schemaField->required();
}

return $this;
return $schemaField;
}

/**
Expand Down
43 changes: 16 additions & 27 deletions src/MCP/Concerns/McpActionTool.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,15 @@

use Binaryk\LaravelRestify\Actions\Action;
use Binaryk\LaravelRestify\MCP\Requests\McpActionRequest;
use Laravel\Mcp\Server\Tools\ToolInputSchema;
use Illuminate\JsonSchema\JsonSchema;

/**
* @mixin \Binaryk\LaravelRestify\Repositories\Repository
*/
trait McpActionTool
{
public function actionTool(Action $action, array $arguments, McpActionRequest $actionRequest): array
public function actionTool(Action $action, McpActionRequest $actionRequest): array
{
$actionRequest->merge($arguments);

$this->sanitizeToolRequest($actionRequest, $arguments);

if ($id = $actionRequest->input('id')) {
if (! $action->authorizedToRun($actionRequest, $actionRequest->findModelOrFail($id))) {
return [
Expand All @@ -26,17 +22,6 @@ public function actionTool(Action $action, array $arguments, McpActionRequest $a
}
}

// Set up the action request context based on action type
if (! $action->isStandalone()) {
if (isset($arguments['id'])) {
// Single model action (show context)
$actionRequest->merge(['id' => $arguments['id']]);
} elseif (isset($arguments['repositories'])) {
// Multiple models action (index context)
$actionRequest->merge(['repositories' => $arguments['repositories']]);
}
}

// Check authorization
if (! $action->authorizedToSee($actionRequest)) {
return [
Expand All @@ -61,9 +46,10 @@ public function actionTool(Action $action, array $arguments, McpActionRequest $a
}
}

public static function actionToolSchema(Action $action, ToolInputSchema $schema, McpActionRequest $mcpRequest): void
public static function actionToolSchema(Action $action, JsonSchema $schema, McpActionRequest $mcpRequest): array
{
$modelName = class_basename(static::guessModelClassName());
$properties = [];

// Add action-specific validation rules
$actionRules = $action->rules();
Expand All @@ -73,26 +59,27 @@ public static function actionToolSchema(Action $action, ToolInputSchema $schema,

// Determine field type based on rules
if (in_array('boolean', $rulesArray)) {
$fieldSchema = $schema->boolean($field);
$fieldSchema = $schema->boolean();
} elseif (in_array('integer', $rulesArray) || in_array('numeric', $rulesArray)) {
$fieldSchema = $schema->number($field);
$fieldSchema = $schema->number();
} elseif (in_array('array', $rulesArray)) {
$fieldSchema = $schema->string($field);
$fieldSchema = $schema->string();
} else {
$fieldSchema = $schema->string($field);
$fieldSchema = $schema->string();
}

if ($isRequired) {
$fieldSchema->required();
}

$fieldSchema->description("Action parameter: {$field}");
$properties[$field] = $fieldSchema;
}

// Add context-specific fields based on action type
if ($action->isStandalone()) {
// Standalone actions don't need ID or repositories
$schema->string('include')
$properties['include'] = $schema->string()
->description('Comma-separated list of relationships to include in response');
} else {
// Check if it's primarily a show action or index action
Expand All @@ -101,21 +88,23 @@ public static function actionToolSchema(Action $action, ToolInputSchema $schema,

if ($shownOnShow && ! $shownOnIndex) {
// Show action - requires single ID
$schema->string('id')
$properties['id'] = $schema->string()
->description("The ID of the {$modelName} to perform the action on")
->required();

$schema->string('include')
$properties['include'] = $schema->string()
->description('Comma-separated list of relationships to include');
} else {
// Index action - requires repositories array
$schema->string('repositories')
$properties['repositories'] = $schema->string()
->description("Array of {$modelName} IDs to perform the action on. e.g. repositories=[1,2,3]")
->required();

$schema->string('include')
$properties['include'] = $schema->string()
->description('Comma-separated list of relationships to include');
}
}

return $properties;
}
}
19 changes: 9 additions & 10 deletions src/MCP/Concerns/McpDestroyTool.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,31 @@
namespace Binaryk\LaravelRestify\MCP\Concerns;

use Binaryk\LaravelRestify\MCP\Requests\McpDestroyRequest;
use Laravel\Mcp\Server\Tools\ToolInputSchema;
use Illuminate\JsonSchema\JsonSchema;

/**
* @mixin \Binaryk\LaravelRestify\Repositories\Repository
*/
trait McpDestroyTool
{
public function deleteTool(array $arguments, McpDestroyRequest $request): array
public function deleteTool(McpDestroyRequest $request): array
{
$id = $arguments['id'] ?? null;
unset($arguments['id']);
$request->merge($arguments);
$this->sanitizeToolRequest($request, $arguments);
$id = $request->input('id');

$model = static::query($request)->findOrFail($id);

return static::resolveWith($model)->destroy($request, $id);
}

public static function destroyToolSchema(ToolInputSchema $schema): void
public static function destroyToolSchema(JsonSchema $schema): array
{
$key = static::uriKey();
$modelName = class_basename(static::guessModelClassName());

$schema->string('id')
->description("The ID of the $modelName to delete")
->required();
return [
'id' => $schema->string()
->description("The ID of the $modelName to delete")
->required(),
];
}
}
Loading
Loading