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
123 changes: 123 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
name: Create Release

on:
push:
branches:
- 10.x

jobs:
release:
runs-on: ubuntu-latest
if: github.event_name == 'push' && github.ref == 'refs/heads/10.x'

steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: 8.3
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, fileinfo
coverage: none

- name: Install dependencies
run: composer install --no-dev --prefer-dist --no-interaction --optimize-autoloader

- name: Run tests
run: |
composer require "laravel/framework:^11.0" "orchestra/testbench:^9.0" --no-interaction --no-update
composer update --prefer-stable --prefer-dist --no-interaction
vendor/bin/pest --ci

- name: Get next version
id: get_version
run: |
# Get the latest tag (handle both v-prefixed and non-prefixed)
LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "0.0.0")
echo "Latest tag: $LATEST_TAG"

# Extract version numbers (remove 'v' prefix if present)
VERSION_NUM=${LATEST_TAG#v}

# Split version into parts
IFS='.' read -r MAJOR MINOR PATCH <<< "$VERSION_NUM"

# Default to 0 if parts are empty
MAJOR=${MAJOR:-0}
MINOR=${MINOR:-0}
PATCH=${PATCH:-0}

# Get the previous tag for commit analysis
PREVIOUS_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "")

# Analyze commits since last tag to determine version bump
if [ -z "$PREVIOUS_TAG" ]; then
# If no previous tags, analyze all commits
COMMITS=$(git log --pretty=format:"%s" --no-merges)
else
# Get commits since last tag
COMMITS=$(git log ${PREVIOUS_TAG}..HEAD --pretty=format:"%s" --no-merges)
fi

echo "Analyzing commits:"
echo "$COMMITS"

# Check for breaking changes (MAJOR version bump)
if echo "$COMMITS" | grep -E "^(BREAKING|BREAKING CHANGE|feat!|fix!):" > /dev/null; then
echo "Found breaking changes, incrementing MAJOR version"
MAJOR=$((MAJOR + 1))
MINOR=0
PATCH=0
# Check for features (MINOR version bump)
elif echo "$COMMITS" | grep -E "^feat(\(.+\))?:" > /dev/null; then
echo "Found features, incrementing MINOR version"
MINOR=$((MINOR + 1))
PATCH=0
# Default to patch version bump
else
echo "No features or breaking changes found, incrementing PATCH version"
PATCH=$((PATCH + 1))
fi

# Create new version (no v prefix)
NEW_VERSION="$MAJOR.$MINOR.$PATCH"

echo "New version: $NEW_VERSION"
echo "version=$NEW_VERSION" >> $GITHUB_OUTPUT
echo "tag_name=$NEW_VERSION" >> $GITHUB_OUTPUT

- name: Generate changelog
id: changelog
run: |
# Get the previous tag for changelog
PREVIOUS_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "")

if [ -z "$PREVIOUS_TAG" ]; then
# If no previous tags, get all commits
COMMITS=$(git log --pretty=format:"* %s (%an)" --no-merges)
else
# Get commits since last tag
COMMITS=$(git log ${PREVIOUS_TAG}..HEAD --pretty=format:"* %s (%an)" --no-merges)
fi

# Create changelog
CHANGELOG="## What's Changed\n\n$COMMITS"

# Handle multiline output for GitHub Actions
echo "changelog<<EOF" >> $GITHUB_OUTPUT
echo -e "$CHANGELOG" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT

- name: Create Release
uses: softprops/action-gh-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ steps.get_version.outputs.tag_name }}
name: Release ${{ steps.get_version.outputs.tag_name }}
body: ${{ steps.changelog.outputs.changelog }}
draft: false
prerelease: false
38 changes: 38 additions & 0 deletions docs-v2/content/en/api/fields.md
Original file line number Diff line number Diff line change
Expand Up @@ -1065,6 +1065,44 @@ The MCP visibility system automatically detects when a request is coming from an

This allows you to have different field visibility for your regular API consumers versus AI agents accessing your data through MCP tools.

### Field Descriptions

Fields can have custom descriptions that are used when generating schema documentation, particularly useful for MCP tools and API documentation:

```php
public function fields(RestifyRequest $request)
{
return [
field('status')
->description('The current status of the item')
->rules(['required', 'string']),

field('feedbackable_id')
->description('This is the id of the employee.')
->rules(['required', 'string', 'max:26']),

field('priority')
->description(function($generatedDescription, $field, $repository) {
return $generatedDescription . ' - Values range from 1 (low) to 5 (high)';
}),
];
}
```

The `description()` method accepts either:
- **String**: A static description text
- **Closure**: A callback that receives the auto-generated description, field instance, and repository for dynamic modifications

When using a closure, you can:
- Modify the automatically generated description
- Add context-specific information
- Access field and repository data for dynamic descriptions

The description callback receives three parameters:
- `$generatedDescription` - The automatically generated description based on field type and validation rules
- `$field` - The field instance
- `$repository` - The repository context

### Custom Tool Schema

When using MCP, you can define custom schema definitions for individual fields using the `toolSchema()` method:
Expand Down
23 changes: 23 additions & 0 deletions src/Fields/Field.php
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,11 @@ class Field extends OrganicField implements JsonSerializable, Matchable, Sortabl

public $toolInputSchemaCallback = null;

/**
* Closure to modify the generated field description.
*/
public $descriptionCallback = null;

/**
* Create a new field.
*
Expand Down Expand Up @@ -937,4 +942,22 @@ public function toolSchema(callable|Closure $callback): self

return $this;
}

/**
* Set a callback to modify the generated field description.
*
* @return $this
*/
public function description(string|callable|Closure $callback): self
{
if (is_string($callback)) {
$this->descriptionCallback = fn () => $callback;

return $this;
}

$this->descriptionCallback = $callback;

return $this;
}
}
5 changes: 5 additions & 0 deletions src/MCP/Concerns/FieldMcpSchemaDetection.php
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,11 @@ protected function generateFieldDescription(Repository $repository): string
$description .= '. Examples: '.implode(', ', $examples);
}

// Apply custom description callback if provided
if (is_callable($this->descriptionCallback)) {
$description = call_user_func($this->descriptionCallback, $description, $this, $repository);
}

return $description;
}

Expand Down
38 changes: 38 additions & 0 deletions tests/Fields/FieldMcpSchemaDetectionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,40 @@ public function test_resolve_tool_schema_with_custom_callback(): void
$this->assertSame($field, $result);
}

public function test_resolve_tool_schema_with_string_description(): void
{
$schema = Mockery::mock(ToolInputSchema::class);
$repository = new PostRepository;

$schema->shouldReceive('string')->with('title')->once()->andReturnSelf();
$schema->shouldReceive('description')->with('Custom description for the title field')->once()->andReturnSelf();

$field = $this->createTestField('title');
$field->description('Custom description for the title field');

$result = $field->resolveToolSchema($schema, $repository);

$this->assertSame($field, $result);
}

public function test_resolve_tool_schema_with_closure_description(): void
{
$schema = Mockery::mock(ToolInputSchema::class);
$repository = new PostRepository;

$schema->shouldReceive('string')->with('title')->once()->andReturnSelf();
$schema->shouldReceive('description')->with('Field: title (type: string). Examples: Sample Title, My Title - Custom addition')->once()->andReturnSelf();

$field = $this->createTestField('title');
$field->description(function ($generatedDescription, $field, $repository) {
return $generatedDescription.' - Custom addition';
});

$result = $field->resolveToolSchema($schema, $repository);

$this->assertSame($field, $result);
}

public function test_get_string_examples_for_different_contexts(): void
{
$field = $this->createTestField('email');
Expand Down Expand Up @@ -107,6 +141,10 @@ protected function createTestField(string $attribute, array $rules = []): Field
$field->shouldReceive('generateFieldExamples')->passthru();
$field->shouldReceive('getNumberExamples')->passthru();
$field->shouldReceive('getStringExamples')->passthru();
$field->shouldReceive('description')->passthru();

// Initialize the descriptionCallback property
$field->descriptionCallback = null;

return $field;
}
Expand Down