feat(database): create mcp_tool_versions table and model for versioned tool management
This commit is contained in:
parent
7631afb12e
commit
537f01672b
2 changed files with 400 additions and 0 deletions
|
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('mcp_tool_versions', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('server_id', 64)->index();
|
||||
$table->string('tool_name', 128);
|
||||
$table->string('version', 32); // semver: 1.0.0, 2.1.0-beta, etc.
|
||||
$table->json('input_schema')->nullable();
|
||||
$table->json('output_schema')->nullable();
|
||||
$table->text('description')->nullable();
|
||||
$table->text('changelog')->nullable();
|
||||
$table->text('migration_notes')->nullable(); // guidance for upgrading from previous version
|
||||
$table->boolean('is_latest')->default(false);
|
||||
$table->timestamp('deprecated_at')->nullable();
|
||||
$table->timestamp('sunset_at')->nullable(); // after this date, version is blocked
|
||||
$table->timestamps();
|
||||
|
||||
// Unique constraint: one version per tool per server
|
||||
$table->unique(['server_id', 'tool_name', 'version'], 'mcp_tool_versions_unique');
|
||||
|
||||
// Index for finding latest versions
|
||||
$table->index(['server_id', 'tool_name', 'is_latest'], 'mcp_tool_versions_latest');
|
||||
|
||||
// Index for finding deprecated/sunset versions
|
||||
$table->index(['deprecated_at', 'sunset_at'], 'mcp_tool_versions_lifecycle');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('mcp_tool_versions');
|
||||
}
|
||||
};
|
||||
359
packages/core-mcp/src/Mod/Mcp/Models/McpToolVersion.php
Normal file
359
packages/core-mcp/src/Mod/Mcp/Models/McpToolVersion.php
Normal file
|
|
@ -0,0 +1,359 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core\Mod\Mcp\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
/**
|
||||
* MCP Tool Version - tracks versioned tool schemas for backwards compatibility.
|
||||
*
|
||||
* Enables running agents to continue using older tool versions while
|
||||
* newer versions are deployed. Supports deprecation lifecycle with
|
||||
* warnings and eventual sunset blocking.
|
||||
*
|
||||
* @property int $id
|
||||
* @property string $server_id
|
||||
* @property string $tool_name
|
||||
* @property string $version
|
||||
* @property array|null $input_schema
|
||||
* @property array|null $output_schema
|
||||
* @property string|null $description
|
||||
* @property string|null $changelog
|
||||
* @property string|null $migration_notes
|
||||
* @property bool $is_latest
|
||||
* @property \Carbon\Carbon|null $deprecated_at
|
||||
* @property \Carbon\Carbon|null $sunset_at
|
||||
* @property \Carbon\Carbon|null $created_at
|
||||
* @property \Carbon\Carbon|null $updated_at
|
||||
* @property-read bool $is_deprecated
|
||||
* @property-read bool $is_sunset
|
||||
* @property-read string $status
|
||||
* @property-read string $full_name
|
||||
*/
|
||||
class McpToolVersion extends Model
|
||||
{
|
||||
protected $table = 'mcp_tool_versions';
|
||||
|
||||
protected $fillable = [
|
||||
'server_id',
|
||||
'tool_name',
|
||||
'version',
|
||||
'input_schema',
|
||||
'output_schema',
|
||||
'description',
|
||||
'changelog',
|
||||
'migration_notes',
|
||||
'is_latest',
|
||||
'deprecated_at',
|
||||
'sunset_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'input_schema' => 'array',
|
||||
'output_schema' => 'array',
|
||||
'is_latest' => 'boolean',
|
||||
'deprecated_at' => 'datetime',
|
||||
'sunset_at' => 'datetime',
|
||||
];
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Scopes
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Filter by server.
|
||||
*/
|
||||
public function scopeForServer(Builder $query, string $serverId): Builder
|
||||
{
|
||||
return $query->where('server_id', $serverId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter by tool name.
|
||||
*/
|
||||
public function scopeForTool(Builder $query, string $toolName): Builder
|
||||
{
|
||||
return $query->where('tool_name', $toolName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter by specific version.
|
||||
*/
|
||||
public function scopeForVersion(Builder $query, string $version): Builder
|
||||
{
|
||||
return $query->where('version', $version);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get only latest versions.
|
||||
*/
|
||||
public function scopeLatest(Builder $query): Builder
|
||||
{
|
||||
return $query->where('is_latest', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get deprecated versions.
|
||||
*/
|
||||
public function scopeDeprecated(Builder $query): Builder
|
||||
{
|
||||
return $query->whereNotNull('deprecated_at')
|
||||
->where('deprecated_at', '<=', now());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get sunset versions (blocked).
|
||||
*/
|
||||
public function scopeSunset(Builder $query): Builder
|
||||
{
|
||||
return $query->whereNotNull('sunset_at')
|
||||
->where('sunset_at', '<=', now());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get active versions (not sunset).
|
||||
*/
|
||||
public function scopeActive(Builder $query): Builder
|
||||
{
|
||||
return $query->where(function ($q) {
|
||||
$q->whereNull('sunset_at')
|
||||
->orWhere('sunset_at', '>', now());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Order by version (newest first using semver sort).
|
||||
*/
|
||||
public function scopeOrderByVersion(Builder $query, string $direction = 'desc'): Builder
|
||||
{
|
||||
// Basic version ordering - splits on dots and orders numerically
|
||||
// For production use, consider a more robust semver sorting approach
|
||||
return $query->orderByRaw(
|
||||
"CAST(SUBSTRING_INDEX(version, '.', 1) AS UNSIGNED) {$direction}, ".
|
||||
"CAST(SUBSTRING_INDEX(SUBSTRING_INDEX(version, '.', 2), '.', -1) AS UNSIGNED) {$direction}, ".
|
||||
"CAST(SUBSTRING_INDEX(SUBSTRING_INDEX(version, '.', 3), '.', -1) AS UNSIGNED) {$direction}"
|
||||
);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Accessors
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Check if this version is deprecated.
|
||||
*/
|
||||
public function getIsDeprecatedAttribute(): bool
|
||||
{
|
||||
return $this->deprecated_at !== null && $this->deprecated_at->isPast();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this version is sunset (blocked).
|
||||
*/
|
||||
public function getIsSunsetAttribute(): bool
|
||||
{
|
||||
return $this->sunset_at !== null && $this->sunset_at->isPast();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the lifecycle status of this version.
|
||||
*/
|
||||
public function getStatusAttribute(): string
|
||||
{
|
||||
if ($this->is_sunset) {
|
||||
return 'sunset';
|
||||
}
|
||||
|
||||
if ($this->is_deprecated) {
|
||||
return 'deprecated';
|
||||
}
|
||||
|
||||
if ($this->is_latest) {
|
||||
return 'latest';
|
||||
}
|
||||
|
||||
return 'active';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get full tool identifier (server:tool).
|
||||
*/
|
||||
public function getFullNameAttribute(): string
|
||||
{
|
||||
return "{$this->server_id}:{$this->tool_name}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get full versioned identifier (server:tool@version).
|
||||
*/
|
||||
public function getVersionedNameAttribute(): string
|
||||
{
|
||||
return "{$this->server_id}:{$this->tool_name}@{$this->version}";
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Methods
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Get deprecation warning message if deprecated but not sunset.
|
||||
*/
|
||||
public function getDeprecationWarning(): ?array
|
||||
{
|
||||
if (! $this->is_deprecated || $this->is_sunset) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$warning = [
|
||||
'code' => 'TOOL_VERSION_DEPRECATED',
|
||||
'message' => "Tool version {$this->version} is deprecated.",
|
||||
'current_version' => $this->version,
|
||||
];
|
||||
|
||||
// Find the latest version to suggest
|
||||
$latest = static::forServer($this->server_id)
|
||||
->forTool($this->tool_name)
|
||||
->latest()
|
||||
->first();
|
||||
|
||||
if ($latest && $latest->version !== $this->version) {
|
||||
$warning['latest_version'] = $latest->version;
|
||||
$warning['message'] .= " Please upgrade to version {$latest->version}.";
|
||||
}
|
||||
|
||||
if ($this->sunset_at) {
|
||||
$warning['sunset_at'] = $this->sunset_at->toIso8601String();
|
||||
$warning['message'] .= " This version will be blocked after {$this->sunset_at->format('Y-m-d')}.";
|
||||
}
|
||||
|
||||
if ($this->migration_notes) {
|
||||
$warning['migration_notes'] = $this->migration_notes;
|
||||
}
|
||||
|
||||
return $warning;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get sunset error if this version is blocked.
|
||||
*/
|
||||
public function getSunsetError(): ?array
|
||||
{
|
||||
if (! $this->is_sunset) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$error = [
|
||||
'code' => 'TOOL_VERSION_SUNSET',
|
||||
'message' => "Tool version {$this->version} is no longer available as of {$this->sunset_at->format('Y-m-d')}.",
|
||||
'sunset_version' => $this->version,
|
||||
'sunset_at' => $this->sunset_at->toIso8601String(),
|
||||
];
|
||||
|
||||
// Find the latest version to suggest
|
||||
$latest = static::forServer($this->server_id)
|
||||
->forTool($this->tool_name)
|
||||
->latest()
|
||||
->first();
|
||||
|
||||
if ($latest && $latest->version !== $this->version) {
|
||||
$error['latest_version'] = $latest->version;
|
||||
$error['message'] .= " Please use version {$latest->version} instead.";
|
||||
}
|
||||
|
||||
if ($this->migration_notes) {
|
||||
$error['migration_notes'] = $this->migration_notes;
|
||||
}
|
||||
|
||||
return $error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare schemas between this version and another.
|
||||
*
|
||||
* @return array{added: array, removed: array, changed: array}
|
||||
*/
|
||||
public function compareSchemaWith(self $other): array
|
||||
{
|
||||
$thisProps = $this->input_schema['properties'] ?? [];
|
||||
$otherProps = $other->input_schema['properties'] ?? [];
|
||||
|
||||
$added = array_diff_key($otherProps, $thisProps);
|
||||
$removed = array_diff_key($thisProps, $otherProps);
|
||||
|
||||
$changed = [];
|
||||
foreach (array_intersect_key($thisProps, $otherProps) as $key => $thisProp) {
|
||||
$otherProp = $otherProps[$key];
|
||||
if (json_encode($thisProp) !== json_encode($otherProp)) {
|
||||
$changed[$key] = [
|
||||
'from' => $thisProp,
|
||||
'to' => $otherProp,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'added' => array_keys($added),
|
||||
'removed' => array_keys($removed),
|
||||
'changed' => $changed,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark this version as deprecated.
|
||||
*/
|
||||
public function deprecate(?Carbon $sunsetAt = null): self
|
||||
{
|
||||
$this->deprecated_at = now();
|
||||
|
||||
if ($sunsetAt) {
|
||||
$this->sunset_at = $sunsetAt;
|
||||
}
|
||||
|
||||
$this->save();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark this version as the latest (and unmark others).
|
||||
*/
|
||||
public function markAsLatest(): self
|
||||
{
|
||||
// Unmark all other versions for this tool
|
||||
static::forServer($this->server_id)
|
||||
->forTool($this->tool_name)
|
||||
->where('id', '!=', $this->id)
|
||||
->update(['is_latest' => false]);
|
||||
|
||||
$this->is_latest = true;
|
||||
$this->save();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Export version info for API responses.
|
||||
*/
|
||||
public function toApiArray(): array
|
||||
{
|
||||
return [
|
||||
'server_id' => $this->server_id,
|
||||
'tool_name' => $this->tool_name,
|
||||
'version' => $this->version,
|
||||
'is_latest' => $this->is_latest,
|
||||
'status' => $this->status,
|
||||
'description' => $this->description,
|
||||
'input_schema' => $this->input_schema,
|
||||
'output_schema' => $this->output_schema,
|
||||
'deprecated_at' => $this->deprecated_at?->toIso8601String(),
|
||||
'sunset_at' => $this->sunset_at?->toIso8601String(),
|
||||
'migration_notes' => $this->migration_notes,
|
||||
'changelog' => $this->changelog,
|
||||
'created_at' => $this->created_at?->toIso8601String(),
|
||||
];
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue