Compare commits

..

1 commit
dev ... main

Author SHA1 Message Date
Snider
2458f87c8d fix(migrations): make all package migrations idempotent
Some checks failed
CI / PHP 8.2 (push) Has been cancelled
CI / PHP 8.3 (push) Has been cancelled
CI / PHP 8.4 (push) Has been cancelled
CI / Assets (push) Has been cancelled
Guard every Schema::create() with hasTable() so migrations
coexist safely with the consolidated app-level migration in
host.uk.com. Prevents "table already exists" failures that
would block the entire migration batch.

Co-Authored-By: Virgil <virgil@lethean.io>
2026-02-08 21:07:59 +00:00
5 changed files with 143 additions and 129 deletions

View file

@ -8,29 +8,31 @@ return new class extends Migration
{ {
public function up(): void public function up(): void
{ {
Schema::create('mcp_api_requests', function (Blueprint $table) { if (! Schema::hasTable('mcp_api_requests')) {
$table->id(); Schema::create('mcp_api_requests', function (Blueprint $table) {
$table->string('request_id', 32)->unique(); $table->id();
$table->foreignId('workspace_id')->nullable()->constrained('workspaces')->nullOnDelete(); $table->string('request_id', 32)->unique();
$table->foreignId('api_key_id')->nullable()->constrained('api_keys')->nullOnDelete(); $table->foreignId('workspace_id')->nullable()->constrained('workspaces')->nullOnDelete();
$table->string('method', 10); $table->foreignId('api_key_id')->nullable()->constrained('api_keys')->nullOnDelete();
$table->string('path', 255); $table->string('method', 10);
$table->json('headers')->nullable(); $table->string('path', 255);
$table->json('request_body')->nullable(); $table->json('headers')->nullable();
$table->unsignedSmallInteger('response_status'); $table->json('request_body')->nullable();
$table->json('response_body')->nullable(); $table->unsignedSmallInteger('response_status');
$table->unsignedInteger('duration_ms')->default(0); $table->json('response_body')->nullable();
$table->string('server_id', 64)->nullable(); $table->unsignedInteger('duration_ms')->default(0);
$table->string('tool_name', 128)->nullable(); $table->string('server_id', 64)->nullable();
$table->text('error_message')->nullable(); $table->string('tool_name', 128)->nullable();
$table->string('ip_address', 45)->nullable(); $table->text('error_message')->nullable();
$table->timestamps(); $table->string('ip_address', 45)->nullable();
$table->timestamps();
$table->index(['workspace_id', 'created_at']); $table->index(['workspace_id', 'created_at']);
$table->index(['server_id', 'tool_name']); $table->index(['server_id', 'tool_name']);
$table->index('created_at'); $table->index('created_at');
$table->index('response_status'); $table->index('response_status');
}); });
}
} }
public function down(): void public function down(): void

View file

@ -8,36 +8,40 @@ return new class extends Migration
{ {
public function up(): void public function up(): void
{ {
Schema::create('mcp_tool_metrics', function (Blueprint $table) { if (! Schema::hasTable('mcp_tool_metrics')) {
$table->id(); Schema::create('mcp_tool_metrics', function (Blueprint $table) {
$table->string('tool_name'); $table->id();
$table->string('workspace_id')->nullable(); $table->string('tool_name');
$table->unsignedInteger('call_count')->default(0); $table->string('workspace_id')->nullable();
$table->unsignedInteger('error_count')->default(0); $table->unsignedInteger('call_count')->default(0);
$table->unsignedInteger('total_duration_ms')->default(0); $table->unsignedInteger('error_count')->default(0);
$table->unsignedInteger('min_duration_ms')->nullable(); $table->unsignedInteger('total_duration_ms')->default(0);
$table->unsignedInteger('max_duration_ms')->nullable(); $table->unsignedInteger('min_duration_ms')->nullable();
$table->date('date'); $table->unsignedInteger('max_duration_ms')->nullable();
$table->timestamps(); $table->date('date');
$table->timestamps();
$table->unique(['tool_name', 'workspace_id', 'date']); $table->unique(['tool_name', 'workspace_id', 'date']);
$table->index(['date', 'tool_name']); $table->index(['date', 'tool_name']);
$table->index('workspace_id'); $table->index('workspace_id');
}); });
}
// Table for tracking tool combinations (tools used together in sessions) // Table for tracking tool combinations (tools used together in sessions)
Schema::create('mcp_tool_combinations', function (Blueprint $table) { if (! Schema::hasTable('mcp_tool_combinations')) {
$table->id(); Schema::create('mcp_tool_combinations', function (Blueprint $table) {
$table->string('tool_a'); $table->id();
$table->string('tool_b'); $table->string('tool_a');
$table->string('workspace_id')->nullable(); $table->string('tool_b');
$table->unsignedInteger('occurrence_count')->default(0); $table->string('workspace_id')->nullable();
$table->date('date'); $table->unsignedInteger('occurrence_count')->default(0);
$table->timestamps(); $table->date('date');
$table->timestamps();
$table->unique(['tool_a', 'tool_b', 'workspace_id', 'date']); $table->unique(['tool_a', 'tool_b', 'workspace_id', 'date']);
$table->index(['date', 'occurrence_count']); $table->index(['date', 'occurrence_count']);
}); });
}
} }
public function down(): void public function down(): void

View file

@ -8,18 +8,20 @@ return new class extends Migration
{ {
public function up(): void public function up(): void
{ {
Schema::create('mcp_usage_quotas', function (Blueprint $table) { if (! Schema::hasTable('mcp_usage_quotas')) {
$table->id(); Schema::create('mcp_usage_quotas', function (Blueprint $table) {
$table->foreignId('workspace_id')->constrained('workspaces')->cascadeOnDelete(); $table->id();
$table->string('month', 7); // YYYY-MM format $table->foreignId('workspace_id')->constrained('workspaces')->cascadeOnDelete();
$table->unsignedBigInteger('tool_calls_count')->default(0); $table->string('month', 7); // YYYY-MM format
$table->unsignedBigInteger('input_tokens')->default(0); $table->unsignedBigInteger('tool_calls_count')->default(0);
$table->unsignedBigInteger('output_tokens')->default(0); $table->unsignedBigInteger('input_tokens')->default(0);
$table->timestamps(); $table->unsignedBigInteger('output_tokens')->default(0);
$table->timestamps();
$table->unique(['workspace_id', 'month']); $table->unique(['workspace_id', 'month']);
$table->index('month'); $table->index('month');
}); });
}
} }
public function down(): void public function down(): void

View file

@ -8,66 +8,70 @@ return new class extends Migration
{ {
public function up(): void public function up(): void
{ {
Schema::create('mcp_audit_logs', function (Blueprint $table) { if (! Schema::hasTable('mcp_audit_logs')) {
$table->id(); Schema::create('mcp_audit_logs', function (Blueprint $table) {
$table->id();
// Tool execution details // Tool execution details
$table->string('server_id')->index(); $table->string('server_id')->index();
$table->string('tool_name')->index(); $table->string('tool_name')->index();
$table->unsignedBigInteger('workspace_id')->nullable()->index(); $table->unsignedBigInteger('workspace_id')->nullable()->index();
$table->string('session_id')->nullable()->index(); $table->string('session_id')->nullable()->index();
// Input/output (stored as JSON, may be redacted) // Input/output (stored as JSON, may be redacted)
$table->json('input_params')->nullable(); $table->json('input_params')->nullable();
$table->json('output_summary')->nullable(); $table->json('output_summary')->nullable();
$table->boolean('success')->default(true); $table->boolean('success')->default(true);
$table->unsignedInteger('duration_ms')->nullable(); $table->unsignedInteger('duration_ms')->nullable();
$table->string('error_code')->nullable(); $table->string('error_code')->nullable();
$table->text('error_message')->nullable(); $table->text('error_message')->nullable();
// Actor information // Actor information
$table->string('actor_type')->nullable(); // user, api_key, system $table->string('actor_type')->nullable(); // user, api_key, system
$table->unsignedBigInteger('actor_id')->nullable(); $table->unsignedBigInteger('actor_id')->nullable();
$table->string('actor_ip', 45)->nullable(); // IPv4 or IPv6 $table->string('actor_ip', 45)->nullable(); // IPv4 or IPv6
// Sensitive tool flagging // Sensitive tool flagging
$table->boolean('is_sensitive')->default(false)->index(); $table->boolean('is_sensitive')->default(false)->index();
$table->string('sensitivity_reason')->nullable(); $table->string('sensitivity_reason')->nullable();
// Hash chain for tamper detection // Hash chain for tamper detection
$table->string('previous_hash', 64)->nullable(); // SHA-256 of previous entry $table->string('previous_hash', 64)->nullable(); // SHA-256 of previous entry
$table->string('entry_hash', 64)->index(); // SHA-256 of this entry $table->string('entry_hash', 64)->index(); // SHA-256 of this entry
// Agent context // Agent context
$table->string('agent_type')->nullable(); $table->string('agent_type')->nullable();
$table->string('plan_slug')->nullable(); $table->string('plan_slug')->nullable();
// Timestamps (immutable - no updated_at updates after creation) // Timestamps (immutable - no updated_at updates after creation)
$table->timestamp('created_at')->useCurrent(); $table->timestamp('created_at')->useCurrent();
$table->timestamp('updated_at')->nullable(); $table->timestamp('updated_at')->nullable();
// Foreign key constraint // Foreign key constraint
$table->foreign('workspace_id') $table->foreign('workspace_id')
->references('id') ->references('id')
->on('workspaces') ->on('workspaces')
->nullOnDelete(); ->nullOnDelete();
// Composite indexes for common queries // Composite indexes for common queries
$table->index(['workspace_id', 'created_at']); $table->index(['workspace_id', 'created_at']);
$table->index(['tool_name', 'created_at']); $table->index(['tool_name', 'created_at']);
$table->index(['is_sensitive', 'created_at']); $table->index(['is_sensitive', 'created_at']);
$table->index(['actor_type', 'actor_id']); $table->index(['actor_type', 'actor_id']);
}); });
}
// Table for tracking sensitive tool definitions // Table for tracking sensitive tool definitions
Schema::create('mcp_sensitive_tools', function (Blueprint $table) { if (! Schema::hasTable('mcp_sensitive_tools')) {
$table->id(); Schema::create('mcp_sensitive_tools', function (Blueprint $table) {
$table->string('tool_name')->unique(); $table->id();
$table->string('reason'); $table->string('tool_name')->unique();
$table->json('redact_fields')->nullable(); // Fields to redact in audit logs $table->string('reason');
$table->boolean('require_explicit_consent')->default(false); $table->json('redact_fields')->nullable(); // Fields to redact in audit logs
$table->timestamps(); $table->boolean('require_explicit_consent')->default(false);
}); $table->timestamps();
});
}
} }
public function down(): void public function down(): void

View file

@ -8,30 +8,32 @@ return new class extends Migration
{ {
public function up(): void public function up(): void
{ {
Schema::create('mcp_tool_versions', function (Blueprint $table) { if (! Schema::hasTable('mcp_tool_versions')) {
$table->id(); Schema::create('mcp_tool_versions', function (Blueprint $table) {
$table->string('server_id', 64)->index(); $table->id();
$table->string('tool_name', 128); $table->string('server_id', 64)->index();
$table->string('version', 32); // semver: 1.0.0, 2.1.0-beta, etc. $table->string('tool_name', 128);
$table->json('input_schema')->nullable(); $table->string('version', 32); // semver: 1.0.0, 2.1.0-beta, etc.
$table->json('output_schema')->nullable(); $table->json('input_schema')->nullable();
$table->text('description')->nullable(); $table->json('output_schema')->nullable();
$table->text('changelog')->nullable(); $table->text('description')->nullable();
$table->text('migration_notes')->nullable(); // guidance for upgrading from previous version $table->text('changelog')->nullable();
$table->boolean('is_latest')->default(false); $table->text('migration_notes')->nullable(); // guidance for upgrading from previous version
$table->timestamp('deprecated_at')->nullable(); $table->boolean('is_latest')->default(false);
$table->timestamp('sunset_at')->nullable(); // after this date, version is blocked $table->timestamp('deprecated_at')->nullable();
$table->timestamps(); $table->timestamp('sunset_at')->nullable(); // after this date, version is blocked
$table->timestamps();
// Unique constraint: one version per tool per server // Unique constraint: one version per tool per server
$table->unique(['server_id', 'tool_name', 'version'], 'mcp_tool_versions_unique'); $table->unique(['server_id', 'tool_name', 'version'], 'mcp_tool_versions_unique');
// Index for finding latest versions // Index for finding latest versions
$table->index(['server_id', 'tool_name', 'is_latest'], 'mcp_tool_versions_latest'); $table->index(['server_id', 'tool_name', 'is_latest'], 'mcp_tool_versions_latest');
// Index for finding deprecated/sunset versions // Index for finding deprecated/sunset versions
$table->index(['deprecated_at', 'sunset_at'], 'mcp_tool_versions_lifecycle'); $table->index(['deprecated_at', 'sunset_at'], 'mcp_tool_versions_lifecycle');
}); });
}
} }
public function down(): void public function down(): void