From 19f2d1d11e42f5c171f03f38e29ea1621f5d0ead Mon Sep 17 00:00:00 2001
From: Alexis Rico
Date: Tue, 20 May 2025 10:16:42 +0200
Subject: [PATCH 1/4] Add migration
Signed-off-by: Alexis Rico
---
apps/dbagent/migrations/0009_remote_mcp.sql | 5 +
.../migrations/meta/0009_snapshot.json | 2074 +++++++++++++++++
apps/dbagent/migrations/meta/_journal.json | 7 +
3 files changed, 2086 insertions(+)
create mode 100644 apps/dbagent/migrations/0009_remote_mcp.sql
create mode 100644 apps/dbagent/migrations/meta/0009_snapshot.json
diff --git a/apps/dbagent/migrations/0009_remote_mcp.sql b/apps/dbagent/migrations/0009_remote_mcp.sql
new file mode 100644
index 00000000..8069cd98
--- /dev/null
+++ b/apps/dbagent/migrations/0009_remote_mcp.sql
@@ -0,0 +1,5 @@
+ALTER TABLE "mcp_servers" DROP CONSTRAINT "uq_mcp_servers_server_name";--> statement-breakpoint
+ALTER TABLE "mcp_servers" ADD COLUMN "config" jsonb NOT NULL;--> statement-breakpoint
+ALTER TABLE "mcp_servers" DROP COLUMN "server_name";--> statement-breakpoint
+ALTER TABLE "mcp_servers" DROP COLUMN "file_path";--> statement-breakpoint
+ALTER TABLE "mcp_servers" DROP COLUMN "env_vars";
\ No newline at end of file
diff --git a/apps/dbagent/migrations/meta/0009_snapshot.json b/apps/dbagent/migrations/meta/0009_snapshot.json
new file mode 100644
index 00000000..cebaa29a
--- /dev/null
+++ b/apps/dbagent/migrations/meta/0009_snapshot.json
@@ -0,0 +1,2074 @@
+{
+ "id": "03f75f0b-c694-4a4c-aa66-4f7fd21d157b",
+ "prevId": "e79cf7af-0cbc-43c4-b7e5-939e09229989",
+ "version": "7",
+ "dialect": "postgresql",
+ "tables": {
+ "public.artifact_documents": {
+ "name": "artifact_documents",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "project_id": {
+ "name": "project_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "content": {
+ "name": "content",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "kind": {
+ "name": "kind",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'text'"
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "idx_artifact_documents_project_id": {
+ "name": "idx_artifact_documents_project_id",
+ "columns": [
+ {
+ "expression": "project_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_artifact_documents_user_id": {
+ "name": "idx_artifact_documents_user_id",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "fk_artifact_documents_project": {
+ "name": "fk_artifact_documents_project",
+ "tableFrom": "artifact_documents",
+ "tableTo": "projects",
+ "columnsFrom": ["project_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "artifact_documents_id_pk": {
+ "name": "artifact_documents_id_pk",
+ "columns": ["id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {
+ "artifact_documents_policy": {
+ "name": "artifact_documents_policy",
+ "as": "PERMISSIVE",
+ "for": "ALL",
+ "to": ["authenticated_user"],
+ "using": "EXISTS (\n SELECT 1 FROM project_members\n WHERE project_id = artifact_documents.project_id AND user_id = current_setting('app.current_user', true)::TEXT\n )"
+ }
+ },
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.artifact_suggestions": {
+ "name": "artifact_suggestions",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "project_id": {
+ "name": "project_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "document_id": {
+ "name": "document_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "document_created_at": {
+ "name": "document_created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "original_text": {
+ "name": "original_text",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "suggested_text": {
+ "name": "suggested_text",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "is_resolved": {
+ "name": "is_resolved",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "idx_artifact_suggestions_document_id": {
+ "name": "idx_artifact_suggestions_document_id",
+ "columns": [
+ {
+ "expression": "document_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_artifact_suggestions_project_id": {
+ "name": "idx_artifact_suggestions_project_id",
+ "columns": [
+ {
+ "expression": "project_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_artifact_suggestions_user_id": {
+ "name": "idx_artifact_suggestions_user_id",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "fk_artifact_suggestions_project": {
+ "name": "fk_artifact_suggestions_project",
+ "tableFrom": "artifact_suggestions",
+ "tableTo": "projects",
+ "columnsFrom": ["project_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "fk_artifact_suggestions_document": {
+ "name": "fk_artifact_suggestions_document",
+ "tableFrom": "artifact_suggestions",
+ "tableTo": "artifact_documents",
+ "columnsFrom": ["document_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "artifact_suggestions_id_pk": {
+ "name": "artifact_suggestions_id_pk",
+ "columns": ["id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {
+ "artifact_suggestions_policy": {
+ "name": "artifact_suggestions_policy",
+ "as": "PERMISSIVE",
+ "for": "ALL",
+ "to": ["authenticated_user"],
+ "using": "EXISTS (\n SELECT 1 FROM project_members\n WHERE project_id = artifact_suggestions.project_id AND user_id = current_setting('app.current_user', true)::TEXT\n )"
+ }
+ },
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.aws_cluster_connections": {
+ "name": "aws_cluster_connections",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "project_id": {
+ "name": "project_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "cluster_id": {
+ "name": "cluster_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "connection_id": {
+ "name": "connection_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "idx_aws_cluster_connections_cluster_id": {
+ "name": "idx_aws_cluster_connections_cluster_id",
+ "columns": [
+ {
+ "expression": "cluster_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_aws_cluster_connections_connection_id": {
+ "name": "idx_aws_cluster_connections_connection_id",
+ "columns": [
+ {
+ "expression": "connection_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_aws_cluster_connections_project_id": {
+ "name": "idx_aws_cluster_connections_project_id",
+ "columns": [
+ {
+ "expression": "project_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "fk_aws_cluster_connections_project": {
+ "name": "fk_aws_cluster_connections_project",
+ "tableFrom": "aws_cluster_connections",
+ "tableTo": "projects",
+ "columnsFrom": ["project_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "fk_aws_cluster_connections_cluster": {
+ "name": "fk_aws_cluster_connections_cluster",
+ "tableFrom": "aws_cluster_connections",
+ "tableTo": "aws_clusters",
+ "columnsFrom": ["cluster_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "fk_aws_cluster_connections_connection": {
+ "name": "fk_aws_cluster_connections_connection",
+ "tableFrom": "aws_cluster_connections",
+ "tableTo": "connections",
+ "columnsFrom": ["connection_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {
+ "aws_cluster_connections_policy": {
+ "name": "aws_cluster_connections_policy",
+ "as": "PERMISSIVE",
+ "for": "ALL",
+ "to": ["authenticated_user"],
+ "using": "EXISTS (\n SELECT 1 FROM project_members\n WHERE project_id = aws_cluster_connections.project_id AND user_id = current_setting('app.current_user', true)::TEXT\n )"
+ }
+ },
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.aws_clusters": {
+ "name": "aws_clusters",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "project_id": {
+ "name": "project_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "cluster_identifier": {
+ "name": "cluster_identifier",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "region": {
+ "name": "region",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'us-east-1'"
+ },
+ "data": {
+ "name": "data",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "idx_aws_clusters_project_id": {
+ "name": "idx_aws_clusters_project_id",
+ "columns": [
+ {
+ "expression": "project_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "fk_aws_clusters_project": {
+ "name": "fk_aws_clusters_project",
+ "tableFrom": "aws_clusters",
+ "tableTo": "projects",
+ "columnsFrom": ["project_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "uq_aws_clusters_integration_identifier": {
+ "name": "uq_aws_clusters_integration_identifier",
+ "nullsNotDistinct": false,
+ "columns": ["cluster_identifier"]
+ }
+ },
+ "policies": {
+ "aws_clusters_policy": {
+ "name": "aws_clusters_policy",
+ "as": "PERMISSIVE",
+ "for": "ALL",
+ "to": ["authenticated_user"],
+ "using": "EXISTS (\n SELECT 1 FROM project_members\n WHERE project_id = aws_clusters.project_id AND user_id = current_setting('app.current_user', true)::TEXT\n )"
+ }
+ },
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.chats": {
+ "name": "chats",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "project_id": {
+ "name": "project_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "model": {
+ "name": "model",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "idx_chats_project_id": {
+ "name": "idx_chats_project_id",
+ "columns": [
+ {
+ "expression": "project_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_chats_user_id": {
+ "name": "idx_chats_user_id",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "fk_chats_project": {
+ "name": "fk_chats_project",
+ "tableFrom": "chats",
+ "tableTo": "projects",
+ "columnsFrom": ["project_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {
+ "chats_policy": {
+ "name": "chats_policy",
+ "as": "PERMISSIVE",
+ "for": "ALL",
+ "to": ["authenticated_user"],
+ "using": "EXISTS (\n SELECT 1 FROM project_members\n WHERE project_id = chats.project_id AND user_id = current_setting('app.current_user', true)::TEXT\n )"
+ }
+ },
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.connection_info": {
+ "name": "connection_info",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "project_id": {
+ "name": "project_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "connection_id": {
+ "name": "connection_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "data": {
+ "name": "data",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "idx_connection_info_connection_id": {
+ "name": "idx_connection_info_connection_id",
+ "columns": [
+ {
+ "expression": "connection_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_connection_info_project_id": {
+ "name": "idx_connection_info_project_id",
+ "columns": [
+ {
+ "expression": "project_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "fk_connection_info_project": {
+ "name": "fk_connection_info_project",
+ "tableFrom": "connection_info",
+ "tableTo": "projects",
+ "columnsFrom": ["project_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "fk_connection_info_connection": {
+ "name": "fk_connection_info_connection",
+ "tableFrom": "connection_info",
+ "tableTo": "connections",
+ "columnsFrom": ["connection_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "uq_connection_info": {
+ "name": "uq_connection_info",
+ "nullsNotDistinct": false,
+ "columns": ["connection_id", "type"]
+ }
+ },
+ "policies": {
+ "connection_info_policy": {
+ "name": "connection_info_policy",
+ "as": "PERMISSIVE",
+ "for": "ALL",
+ "to": ["authenticated_user"],
+ "using": "EXISTS (\n SELECT 1 FROM project_members\n WHERE project_id = connection_info.project_id AND user_id = current_setting('app.current_user', true)::TEXT\n )"
+ }
+ },
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.connections": {
+ "name": "connections",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "project_id": {
+ "name": "project_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "is_default": {
+ "name": "is_default",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "connection_string": {
+ "name": "connection_string",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "idx_connections_project_id": {
+ "name": "idx_connections_project_id",
+ "columns": [
+ {
+ "expression": "project_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "fk_connections_project": {
+ "name": "fk_connections_project",
+ "tableFrom": "connections",
+ "tableTo": "projects",
+ "columnsFrom": ["project_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "uq_connections_name": {
+ "name": "uq_connections_name",
+ "nullsNotDistinct": false,
+ "columns": ["project_id", "name"]
+ },
+ "uq_connections_connection_string": {
+ "name": "uq_connections_connection_string",
+ "nullsNotDistinct": false,
+ "columns": ["project_id", "connection_string"]
+ }
+ },
+ "policies": {
+ "connections_policy": {
+ "name": "connections_policy",
+ "as": "PERMISSIVE",
+ "for": "ALL",
+ "to": ["authenticated_user"],
+ "using": "EXISTS (\n SELECT 1 FROM project_members\n WHERE project_id = connections.project_id AND user_id = current_setting('app.current_user', true)::TEXT\n )"
+ }
+ },
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.gcp_instance_connections": {
+ "name": "gcp_instance_connections",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "project_id": {
+ "name": "project_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "instance_id": {
+ "name": "instance_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "connection_id": {
+ "name": "connection_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "idx_gcp_instance_connections_instance_id": {
+ "name": "idx_gcp_instance_connections_instance_id",
+ "columns": [
+ {
+ "expression": "instance_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_gcp_instance_connections_connection_id": {
+ "name": "idx_gcp_instance_connections_connection_id",
+ "columns": [
+ {
+ "expression": "connection_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_gcp_instance_connections_project_id": {
+ "name": "idx_gcp_instance_connections_project_id",
+ "columns": [
+ {
+ "expression": "project_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "fk_gcp_instance_connections_project": {
+ "name": "fk_gcp_instance_connections_project",
+ "tableFrom": "gcp_instance_connections",
+ "tableTo": "projects",
+ "columnsFrom": ["project_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "fk_gcp_instance_connections_instance": {
+ "name": "fk_gcp_instance_connections_instance",
+ "tableFrom": "gcp_instance_connections",
+ "tableTo": "gcp_instances",
+ "columnsFrom": ["instance_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "fk_gcp_instance_connections_connection": {
+ "name": "fk_gcp_instance_connections_connection",
+ "tableFrom": "gcp_instance_connections",
+ "tableTo": "connections",
+ "columnsFrom": ["connection_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {
+ "gcp_instance_connections_policy": {
+ "name": "gcp_instance_connections_policy",
+ "as": "PERMISSIVE",
+ "for": "ALL",
+ "to": ["authenticated_user"],
+ "using": "EXISTS (\n SELECT 1 FROM project_members\n WHERE project_id = gcp_instance_connections.project_id AND user_id = current_setting('app.current_user', true)::TEXT\n )"
+ }
+ },
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.gcp_instances": {
+ "name": "gcp_instances",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "project_id": {
+ "name": "project_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "instance_name": {
+ "name": "instance_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "gcp_project_id": {
+ "name": "gcp_project_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "data": {
+ "name": "data",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "idx_gcp_instances_project_id": {
+ "name": "idx_gcp_instances_project_id",
+ "columns": [
+ {
+ "expression": "project_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "fk_gcp_instances_project": {
+ "name": "fk_gcp_instances_project",
+ "tableFrom": "gcp_instances",
+ "tableTo": "projects",
+ "columnsFrom": ["project_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "uq_gcp_instances_instance_name": {
+ "name": "uq_gcp_instances_instance_name",
+ "nullsNotDistinct": false,
+ "columns": ["project_id", "gcp_project_id", "instance_name"]
+ }
+ },
+ "policies": {
+ "gcp_instances_policy": {
+ "name": "gcp_instances_policy",
+ "as": "PERMISSIVE",
+ "for": "ALL",
+ "to": ["authenticated_user"],
+ "using": "EXISTS (\n SELECT 1 FROM project_members\n WHERE project_id = gcp_instances.project_id AND user_id = current_setting('app.current_user', true)::TEXT\n )"
+ }
+ },
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.integrations": {
+ "name": "integrations",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "project_id": {
+ "name": "project_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "data": {
+ "name": "data",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "idx_integrations_project_id": {
+ "name": "idx_integrations_project_id",
+ "columns": [
+ {
+ "expression": "project_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "fk_integrations_project": {
+ "name": "fk_integrations_project",
+ "tableFrom": "integrations",
+ "tableTo": "projects",
+ "columnsFrom": ["project_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "uq_integrations_name": {
+ "name": "uq_integrations_name",
+ "nullsNotDistinct": false,
+ "columns": ["project_id", "name"]
+ }
+ },
+ "policies": {
+ "integrations_policy": {
+ "name": "integrations_policy",
+ "as": "PERMISSIVE",
+ "for": "ALL",
+ "to": ["authenticated_user"],
+ "using": "EXISTS (\n SELECT 1 FROM project_members\n WHERE project_id = integrations.project_id AND user_id = current_setting('app.current_user', true)::TEXT\n )"
+ }
+ },
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.mcp_servers": {
+ "name": "mcp_servers",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "config": {
+ "name": "config",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "version": {
+ "name": "version",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "uq_mcp_servers_name": {
+ "name": "uq_mcp_servers_name",
+ "nullsNotDistinct": false,
+ "columns": ["name"]
+ }
+ },
+ "policies": {
+ "mcp_servers_policy": {
+ "name": "mcp_servers_policy",
+ "as": "PERMISSIVE",
+ "for": "ALL",
+ "to": ["authenticated_user"],
+ "using": "true"
+ }
+ },
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.message_votes": {
+ "name": "message_votes",
+ "schema": "",
+ "columns": {
+ "project_id": {
+ "name": "project_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "chat_id": {
+ "name": "chat_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "message_id": {
+ "name": "message_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "is_upvoted": {
+ "name": "is_upvoted",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "idx_message_votes_chat_id": {
+ "name": "idx_message_votes_chat_id",
+ "columns": [
+ {
+ "expression": "chat_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_message_votes_message_id": {
+ "name": "idx_message_votes_message_id",
+ "columns": [
+ {
+ "expression": "message_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_message_votes_project_id": {
+ "name": "idx_message_votes_project_id",
+ "columns": [
+ {
+ "expression": "project_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_message_votes_user_id": {
+ "name": "idx_message_votes_user_id",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "fk_votes_chat": {
+ "name": "fk_votes_chat",
+ "tableFrom": "message_votes",
+ "tableTo": "chats",
+ "columnsFrom": ["chat_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "fk_message_votes_message": {
+ "name": "fk_message_votes_message",
+ "tableFrom": "message_votes",
+ "tableTo": "messages",
+ "columnsFrom": ["message_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "fk_message_votes_project": {
+ "name": "fk_message_votes_project",
+ "tableFrom": "message_votes",
+ "tableTo": "projects",
+ "columnsFrom": ["project_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "message_votes_chat_id_message_id_user_id_pk": {
+ "name": "message_votes_chat_id_message_id_user_id_pk",
+ "columns": ["chat_id", "message_id", "user_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {
+ "message_votes_policy": {
+ "name": "message_votes_policy",
+ "as": "PERMISSIVE",
+ "for": "ALL",
+ "to": ["authenticated_user"],
+ "using": "EXISTS (\n SELECT 1 FROM project_members\n WHERE project_id = message_votes.project_id AND user_id = current_setting('app.current_user', true)::TEXT\n )"
+ }
+ },
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.messages": {
+ "name": "messages",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "project_id": {
+ "name": "project_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "chat_id": {
+ "name": "chat_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "role": {
+ "name": "role",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "parts": {
+ "name": "parts",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "idx_messages_project_id": {
+ "name": "idx_messages_project_id",
+ "columns": [
+ {
+ "expression": "project_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_messages_chat_id": {
+ "name": "idx_messages_chat_id",
+ "columns": [
+ {
+ "expression": "chat_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "fk_messages_project": {
+ "name": "fk_messages_project",
+ "tableFrom": "messages",
+ "tableTo": "projects",
+ "columnsFrom": ["project_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "fk_messages_chat": {
+ "name": "fk_messages_chat",
+ "tableFrom": "messages",
+ "tableTo": "chats",
+ "columnsFrom": ["chat_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {
+ "messages_policy": {
+ "name": "messages_policy",
+ "as": "PERMISSIVE",
+ "for": "ALL",
+ "to": ["authenticated_user"],
+ "using": "EXISTS (\n SELECT 1 FROM project_members\n WHERE project_id = messages.project_id AND user_id = current_setting('app.current_user', true)::TEXT\n )"
+ }
+ },
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.playbooks": {
+ "name": "playbooks",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "project_id": {
+ "name": "project_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "content": {
+ "name": "content",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "idx_playbooks_project_id": {
+ "name": "idx_playbooks_project_id",
+ "columns": [
+ {
+ "expression": "project_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "fk_playbooks_project": {
+ "name": "fk_playbooks_project",
+ "tableFrom": "playbooks",
+ "tableTo": "projects",
+ "columnsFrom": ["project_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "uq_playbooks_name": {
+ "name": "uq_playbooks_name",
+ "nullsNotDistinct": false,
+ "columns": ["project_id", "name"]
+ }
+ },
+ "policies": {
+ "playbooks_policy": {
+ "name": "playbooks_policy",
+ "as": "PERMISSIVE",
+ "for": "ALL",
+ "to": ["authenticated_user"],
+ "using": "EXISTS (\n SELECT 1 FROM project_members\n WHERE project_id = playbooks.project_id AND user_id = current_setting('app.current_user', true)::TEXT\n )"
+ }
+ },
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.project_members": {
+ "name": "project_members",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "project_id": {
+ "name": "project_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "role": {
+ "name": "role",
+ "type": "member_role",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'member'"
+ },
+ "added_at": {
+ "name": "added_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "idx_project_members_project_id": {
+ "name": "idx_project_members_project_id",
+ "columns": [
+ {
+ "expression": "project_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "fk_project_members_project": {
+ "name": "fk_project_members_project",
+ "tableFrom": "project_members",
+ "tableTo": "projects",
+ "columnsFrom": ["project_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "uq_project_members_user_project": {
+ "name": "uq_project_members_user_project",
+ "nullsNotDistinct": false,
+ "columns": ["project_id", "user_id"]
+ }
+ },
+ "policies": {
+ "project_members_policy": {
+ "name": "project_members_policy",
+ "as": "PERMISSIVE",
+ "for": "ALL",
+ "to": ["authenticated_user"],
+ "using": "true"
+ }
+ },
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.projects": {
+ "name": "projects",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "cloud_provider": {
+ "name": "cloud_provider",
+ "type": "cloud_provider",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {
+ "projects_view_policy": {
+ "name": "projects_view_policy",
+ "as": "PERMISSIVE",
+ "for": "SELECT",
+ "to": ["authenticated_user"],
+ "using": "EXISTS (\n SELECT 1 FROM project_members\n WHERE project_id = projects.id AND user_id = current_setting('app.current_user', true)::TEXT\n )"
+ },
+ "projects_create_policy": {
+ "name": "projects_create_policy",
+ "as": "PERMISSIVE",
+ "for": "INSERT",
+ "to": ["authenticated_user"],
+ "withCheck": "true"
+ },
+ "projects_update_policy": {
+ "name": "projects_update_policy",
+ "as": "PERMISSIVE",
+ "for": "UPDATE",
+ "to": ["authenticated_user"],
+ "using": "EXISTS (\n SELECT 1 FROM project_members\n WHERE project_id = projects.id AND user_id = current_setting('app.current_user', true)::TEXT AND role = 'owner'\n )"
+ },
+ "projects_delete_policy": {
+ "name": "projects_delete_policy",
+ "as": "PERMISSIVE",
+ "for": "DELETE",
+ "to": ["authenticated_user"],
+ "using": "EXISTS (\n SELECT 1 FROM project_members\n WHERE project_id = projects.id AND user_id = current_setting('app.current_user', true)::TEXT AND role = 'owner'\n )"
+ }
+ },
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.schedule_runs": {
+ "name": "schedule_runs",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "project_id": {
+ "name": "project_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "schedule_id": {
+ "name": "schedule_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "result": {
+ "name": "result",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "summary": {
+ "name": "summary",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "notification_level": {
+ "name": "notification_level",
+ "type": "notification_level",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'info'"
+ },
+ "messages": {
+ "name": "messages",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "idx_schedule_runs_created_at": {
+ "name": "idx_schedule_runs_created_at",
+ "columns": [
+ {
+ "expression": "schedule_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_schedule_runs_schedule_id": {
+ "name": "idx_schedule_runs_schedule_id",
+ "columns": [
+ {
+ "expression": "schedule_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_schedule_runs_project_id": {
+ "name": "idx_schedule_runs_project_id",
+ "columns": [
+ {
+ "expression": "project_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_schedule_runs_notification_level": {
+ "name": "idx_schedule_runs_notification_level",
+ "columns": [
+ {
+ "expression": "notification_level",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "fk_schedule_runs_project": {
+ "name": "fk_schedule_runs_project",
+ "tableFrom": "schedule_runs",
+ "tableTo": "projects",
+ "columnsFrom": ["project_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "fk_schedule_runs_schedule": {
+ "name": "fk_schedule_runs_schedule",
+ "tableFrom": "schedule_runs",
+ "tableTo": "schedules",
+ "columnsFrom": ["schedule_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {
+ "schedule_runs_policy": {
+ "name": "schedule_runs_policy",
+ "as": "PERMISSIVE",
+ "for": "ALL",
+ "to": ["authenticated_user"],
+ "using": "EXISTS (\n SELECT 1 FROM project_members\n WHERE project_id = schedule_runs.project_id AND user_id = current_setting('app.current_user', true)::TEXT\n )"
+ }
+ },
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.schedules": {
+ "name": "schedules",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "project_id": {
+ "name": "project_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "connection_id": {
+ "name": "connection_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "playbook": {
+ "name": "playbook",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "schedule_type": {
+ "name": "schedule_type",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "cron_expression": {
+ "name": "cron_expression",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "additional_instructions": {
+ "name": "additional_instructions",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "min_interval": {
+ "name": "min_interval",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "max_interval": {
+ "name": "max_interval",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "last_run": {
+ "name": "last_run",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "next_run": {
+ "name": "next_run",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "schedule_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'disabled'"
+ },
+ "failures": {
+ "name": "failures",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "default": 0
+ },
+ "keep_history": {
+ "name": "keep_history",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 300
+ },
+ "model": {
+ "name": "model",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "max_steps": {
+ "name": "max_steps",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "notify_level": {
+ "name": "notify_level",
+ "type": "notification_level",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'alert'"
+ },
+ "extra_notification_text": {
+ "name": "extra_notification_text",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "idx_schedules_project_id": {
+ "name": "idx_schedules_project_id",
+ "columns": [
+ {
+ "expression": "project_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_schedules_connection_id": {
+ "name": "idx_schedules_connection_id",
+ "columns": [
+ {
+ "expression": "connection_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_schedules_status": {
+ "name": "idx_schedules_status",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_schedules_next_run": {
+ "name": "idx_schedules_next_run",
+ "columns": [
+ {
+ "expression": "next_run",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_schedules_enabled": {
+ "name": "idx_schedules_enabled",
+ "columns": [
+ {
+ "expression": "enabled",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "fk_schedules_project": {
+ "name": "fk_schedules_project",
+ "tableFrom": "schedules",
+ "tableTo": "projects",
+ "columnsFrom": ["project_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "fk_schedules_connection": {
+ "name": "fk_schedules_connection",
+ "tableFrom": "schedules",
+ "tableTo": "connections",
+ "columnsFrom": ["connection_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {
+ "schedules_policy": {
+ "name": "schedules_policy",
+ "as": "PERMISSIVE",
+ "for": "ALL",
+ "to": ["authenticated_user"],
+ "using": "EXISTS (\n SELECT 1 FROM project_members\n WHERE project_id = schedules.project_id AND user_id = current_setting('app.current_user', true)::TEXT\n )"
+ }
+ },
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ }
+ },
+ "enums": {
+ "public.cloud_provider": {
+ "name": "cloud_provider",
+ "schema": "public",
+ "values": ["aws", "gcp", "other"]
+ },
+ "public.member_role": {
+ "name": "member_role",
+ "schema": "public",
+ "values": ["owner", "member"]
+ },
+ "public.notification_level": {
+ "name": "notification_level",
+ "schema": "public",
+ "values": ["info", "warning", "alert"]
+ },
+ "public.schedule_status": {
+ "name": "schedule_status",
+ "schema": "public",
+ "values": ["disabled", "scheduled", "running"]
+ }
+ },
+ "schemas": {},
+ "sequences": {},
+ "roles": {
+ "authenticated_user": {
+ "name": "authenticated_user",
+ "createDb": false,
+ "createRole": false,
+ "inherit": true
+ }
+ },
+ "policies": {},
+ "views": {},
+ "_meta": {
+ "columns": {},
+ "schemas": {},
+ "tables": {}
+ }
+}
diff --git a/apps/dbagent/migrations/meta/_journal.json b/apps/dbagent/migrations/meta/_journal.json
index 0c045aa2..1ec8d43b 100644
--- a/apps/dbagent/migrations/meta/_journal.json
+++ b/apps/dbagent/migrations/meta/_journal.json
@@ -64,6 +64,13 @@
"when": 1746433645178,
"tag": "0008_mcp_servers",
"breakpoints": true
+ },
+ {
+ "idx": 9,
+ "version": "7",
+ "when": 1747728975039,
+ "tag": "0009_remote_mcp",
+ "breakpoints": true
}
]
}
From 9202719588dd4596469635f35e82b9ad740a4dc3 Mon Sep 17 00:00:00 2001
From: Alexis Rico
Date: Tue, 20 May 2025 10:17:28 +0200
Subject: [PATCH 2/4] Update schema
Signed-off-by: Alexis Rico
---
apps/dbagent/src/lib/db/schema.ts | 21 +++++++++++++++++----
1 file changed, 17 insertions(+), 4 deletions(-)
diff --git a/apps/dbagent/src/lib/db/schema.ts b/apps/dbagent/src/lib/db/schema.ts
index dc97f5d0..8c988bf9 100644
--- a/apps/dbagent/src/lib/db/schema.ts
+++ b/apps/dbagent/src/lib/db/schema.ts
@@ -674,21 +674,34 @@ export const playbooks = pgTable(
export type Playbook = InferSelectModel;
export type PlaybookInsert = InferInsertModel;
+export type McpServerConfig =
+ | {
+ type: 'local';
+ filePath: string;
+ envVars?: Record;
+ }
+ | {
+ type: 'sse';
+ url: string;
+ headers?: Record;
+ }
+ | {
+ type: 'streamable-http';
+ url: string;
+ };
+
export const mcpServers = pgTable(
'mcp_servers',
{
id: uuid('id').primaryKey().defaultRandom().notNull(),
name: text('name').notNull(),
- serverName: text('server_name').notNull(),
- filePath: text('file_path').notNull(),
+ config: jsonb('config').$type().notNull(),
version: text('version').notNull(),
enabled: boolean('enabled').default(true).notNull(),
- envVars: jsonb('env_vars').$type>().default({}).notNull(),
createdAt: timestamp('created_at', { mode: 'date' }).defaultNow().notNull()
},
(table) => [
unique('uq_mcp_servers_name').on(table.name),
- unique('uq_mcp_servers_server_name').on(table.serverName),
pgPolicy('mcp_servers_policy', {
to: authenticatedUser,
for: 'all',
From c7fe16212e4512a7a1092b0417d6220b6a6e963a Mon Sep 17 00:00:00 2001
From: "google-labs-jules[bot]"
<161369871+google-labs-jules[bot]@users.noreply.github.com>
Date: Tue, 20 May 2025 19:25:21 +0000
Subject: [PATCH 3/4] feat: Add support for remote MCP server types
This commit introduces support for different types of MCP (Managed Component Process) servers, allowing for remote server configurations in addition to the existing stdio-based ones.
Key changes:
1. **Schema Updates:**
* Added a new PostgreSQL enum `mcp_server_type` with values: `stdio`, `sse` (Server-Sent Events), and `streamable-http`.
* The `mcp_servers` table now includes:
* A `type` column (of `mcp_server_type`, defaulting to `stdio`).
* A nullable `url` column (text) to store the endpoint for remote servers.
* A database migration (`0009_mcp_server_type_and_url.sql`) has been added.
* `MCPServer` and `MCPServerInsert` types in `schema.ts` are updated accordingly.
2. **Backend Logic:**
* CRUD functions in `apps/dbagent/src/lib/db/mcp-servers.ts` (`addUserMcpServerToDB`, `updateUserMcpServer`) now handle the `type` and `url` fields.
* API routes under `apps/dbagent/src/app/api/mcp/servers/` have been updated:
* Request bodies and responses now include `type` and `url`.
* Zod validation schemas are in place for these new fields, including logic to require `url` for `sse` and `streamable-http` types.
3. **UI Enhancements:**
* The MCP server management table (`mcp-table.tsx`) now displays the `type` and `url` of each server.
* The MCP server creation/editing form (`mcp-view.tsx`):
* Includes a dropdown to select the server `type`.
* Conditionally displays a `url` input field only when `type` is `sse` or `streamable-http`.
* Handles client-side logic to clear/nullify `url` when `type` is `stdio`.
* The `name` field is now editable only during server creation.
* Server actions in `action.ts` have been updated to support the new fields.
These changes enable you to configure and manage MCP servers that operate via stdio, SSE, or streamable HTTP, providing greater flexibility in deploying and integrating managed components.
---
.../0009_mcp_server_type_and_url.sql | 12 ++
.../src/app/api/mcp/servers/[server]/route.ts | 136 +++++++++++++-----
apps/dbagent/src/app/api/mcp/servers/route.ts | 75 ++++++----
apps/dbagent/src/components/mcp/action.ts | 2 +-
apps/dbagent/src/components/mcp/mcp-table.tsx | 39 +++++
apps/dbagent/src/components/mcp/mcp-view.tsx | 134 +++++++++++++++++
apps/dbagent/src/lib/db/mcp-servers.ts | 89 +++++++++---
apps/dbagent/src/lib/db/schema.ts | 7 +-
8 files changed, 418 insertions(+), 76 deletions(-)
create mode 100644 apps/dbagent/migrations/0009_mcp_server_type_and_url.sql
diff --git a/apps/dbagent/migrations/0009_mcp_server_type_and_url.sql b/apps/dbagent/migrations/0009_mcp_server_type_and_url.sql
new file mode 100644
index 00000000..8aec5949
--- /dev/null
+++ b/apps/dbagent/migrations/0009_mcp_server_type_and_url.sql
@@ -0,0 +1,12 @@
+-- Add mcp_server_type enum type if it doesn't exist
+DO $$
+BEGIN
+ IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'mcp_server_type') THEN
+ CREATE TYPE mcp_server_type AS ENUM ('stdio', 'sse', 'streamable-http');
+ END IF;
+END$$;
+
+-- Add the new columns to the mcp_servers table
+ALTER TABLE mcp_servers
+ADD COLUMN type mcp_server_type DEFAULT 'stdio',
+ADD COLUMN url TEXT;
diff --git a/apps/dbagent/src/app/api/mcp/servers/[server]/route.ts b/apps/dbagent/src/app/api/mcp/servers/[server]/route.ts
index f753b798..e6f6215a 100644
--- a/apps/dbagent/src/app/api/mcp/servers/[server]/route.ts
+++ b/apps/dbagent/src/app/api/mcp/servers/[server]/route.ts
@@ -1,42 +1,112 @@
-import { promises as fs } from 'fs';
import { NextResponse } from 'next/server';
-import path from 'path';
-import { getMCPSourceDir } from '~/lib/ai/tools/user-mcp';
+import { z } from 'zod';
+import { dbAccess } from '~/lib/db/db'; // Assuming dbAccess is available
+import { getUserMcpServer, updateUserMcpServer, deleteUserMcpServer } from '~/lib/db/mcp-servers';
+import { MCPServerType } from '~/lib/db/schema'; // Import the enum type
-const mcpSourceDir = getMCPSourceDir();
+// Zod schema for updating an MCP Server (all fields optional)
+const mcpServerUpdateSchema = z.object({
+ serverName: z.string().min(1).optional(),
+ filePath: z.string().min(1).optional(),
+ version: z.string().min(1).optional(),
+ type: z.enum(['stdio', 'sse', 'streamable-http']).optional(),
+ url: z.string().url('Invalid URL format').nullable().optional(),
+ enabled: z.boolean().optional(),
+ envVars: z.record(z.string()).optional(),
+});
-export async function GET(_: Request, { params }: { params: Promise<{ server: string }> }) {
+export async function GET(_: Request, { params }: { params: { server: string } }) {
try {
- const { server } = await params;
- const filePath = path.join(mcpSourceDir, `${server}.ts`);
-
- // Check if file exists
- try {
- await fs.access(filePath);
- } catch (error) {
- return NextResponse.json({ error: 'Server file not found' }, { status: 404 });
- }
-
- // Read file content
- const content = await fs.readFile(filePath, 'utf-8');
-
- // Extract metadata from file content
- const nameMatch = content.match(/name:\s*['"]([^'"]+)['"]/);
- const versionMatch = content.match(/version:\s*['"]([^'"]+)['"]/);
- const descriptionMatch = content.match(/description:\s*['"]([^'"]+)['"]/);
-
- const metadata = {
- name: server,
- serverName: nameMatch ? nameMatch[1] : server,
- version: versionMatch ? versionMatch[1] : '1.0.0',
- description: descriptionMatch ? descriptionMatch[1] : '',
- filePath: `${server}.ts`,
- enabled: false
+ const serverName = params.server; // This is the 'name' field of the server
+ if (!serverName) {
+ return NextResponse.json({ error: 'Server name parameter is missing' }, { status: 400 });
+ }
+
+ const server = await getUserMcpServer(dbAccess, serverName);
+
+ if (!server) {
+ return NextResponse.json({ error: 'Server not found' }, { status: 404 });
+ }
+ return NextResponse.json(server);
+ } catch (error) {
+ console.error('Error fetching MCP server:', error);
+ const errorMessage = error instanceof Error ? error.message : 'An unknown error occurred';
+ return NextResponse.json({ error: 'Failed to fetch MCP server', details: errorMessage }, { status: 500 });
+ }
+}
+
+export async function PUT(request: Request, { params }: { params: { server: string } }) {
+ try {
+ const serverNameFromParams = params.server; // This is the 'name' field to identify the server
+ if (!serverNameFromParams) {
+ return NextResponse.json({ error: 'Server name parameter is missing' }, { status: 400 });
+ }
+
+ const body = await request.json();
+ const validatedData = mcpServerUpdateSchema.safeParse(body);
+
+ if (!validatedData.success) {
+ return NextResponse.json({ error: 'Invalid input', details: validatedData.error.flatten() }, { status: 400 });
+ }
+
+ const updatePayload = { ...validatedData.data };
+
+ // Logic for type and url:
+ // If type is explicitly set to 'stdio', url should be null.
+ if (updatePayload.type === 'stdio') {
+ updatePayload.url = null;
+ } else if ((updatePayload.type === 'sse' || updatePayload.type === 'streamable-http') &&
+ (updatePayload.url === undefined || updatePayload.url === null)) {
+ // If type is sse/streamable-http, URL is expected.
+ // Check if the existing record already has a URL if type is not in payload
+ if (updatePayload.type !== undefined) { // type is being explicitly set
+ return NextResponse.json({ error: `URL is required when type is '${updatePayload.type}'` }, { status: 400 });
+ }
+ // If type is not in payload, we might need to fetch the server to check its current type
+ // and see if the URL becomes implicitly required.
+ // For simplicity now, this check is only if 'type' is in the payload.
+ }
+
+
+ // The updateUserMcpServer function expects an MCPServerInsert-like object,
+ // where 'name' is the identifier for the WHERE clause.
+ const serverToUpdate = {
+ name: serverNameFromParams, // Critical: This is the key for the update operation
+ ...updatePayload
};
- return NextResponse.json(metadata);
+ const updatedServer = await updateUserMcpServer(dbAccess, serverToUpdate);
+ return NextResponse.json(updatedServer);
+
} catch (error) {
- console.error('Error reading server file:', error);
- return NextResponse.json({ error: 'Failed to read server file' }, { status: 500 });
+ console.error('Error updating MCP server:', error);
+ const errorMessage = error instanceof Error ? error.message : 'An unknown error occurred';
+ if (errorMessage.includes('not found or no values changed') || errorMessage.includes('not found or update failed')) {
+ return NextResponse.json({ error: errorMessage }, { status: 404 });
+ }
+ if (errorMessage.includes('already exists')) {
+ return NextResponse.json({ error: errorMessage }, { status: 409 }); // Conflict
+ }
+ return NextResponse.json({ error: 'Failed to update MCP server', details: errorMessage }, { status: 500 });
+ }
+}
+
+export async function DELETE(_: Request, { params }: { params: { server: string } }) {
+ try {
+ const serverName = params.server; // This is the 'name' field of the server
+ if (!serverName) {
+ return NextResponse.json({ error: 'Server name parameter is missing' }, { status: 400 });
+ }
+
+ await deleteUserMcpServer(dbAccess, serverName);
+ return NextResponse.json({ message: `Server "${serverName}" deleted successfully` }, { status: 200 }); // Or 204 No Content
+
+ } catch (error) {
+ console.error('Error deleting MCP server:', error);
+ const errorMessage = error instanceof Error ? error.message : 'An unknown error occurred';
+ if (errorMessage.includes('not found')) {
+ return NextResponse.json({ error: errorMessage }, { status: 404 });
+ }
+ return NextResponse.json({ error: 'Failed to delete MCP server', details: errorMessage }, { status: 500 });
}
}
diff --git a/apps/dbagent/src/app/api/mcp/servers/route.ts b/apps/dbagent/src/app/api/mcp/servers/route.ts
index 10f50d24..b966e7be 100644
--- a/apps/dbagent/src/app/api/mcp/servers/route.ts
+++ b/apps/dbagent/src/app/api/mcp/servers/route.ts
@@ -1,37 +1,62 @@
-import { promises as fs } from 'fs';
import { NextResponse } from 'next/server';
-import path from 'path';
-import { getMCPSourceDir } from '~/lib/ai/tools/user-mcp';
+import { z } from 'zod';
+import { addUserMcpServerToDB, getUserMcpServers } from '~/lib/db/mcp-servers';
+import { dbAccess } from '~/lib/db/db'; // Assuming dbAccess is available like this
+import { MCPServerType } from '~/lib/db/schema'; // Import the enum type for Zod
-const mcpSourceDir = getMCPSourceDir();
+// Zod schema for MCPServerInsert
+const mcpServerInsertSchema = z.object({
+ name: z.string().min(1, 'Name is required'),
+ serverName: z.string().min(1, 'Server name is required'),
+ filePath: z.string().min(1, 'File path is required'),
+ version: z.string().min(1, 'Version is required'),
+ type: z.enum(['stdio', 'sse', 'streamable-http']),
+ url: z.string().url('Invalid URL format').nullable().optional(),
+ enabled: z.boolean().optional(),
+ envVars: z.record(z.string()).optional(),
+});
export async function GET() {
try {
- const files = await fs.readdir(mcpSourceDir);
- const serverFiles = files.filter((file) => file.endsWith('.ts') && !file.endsWith('.d.ts'));
+ // Fetch servers from the database
+ const servers = await getUserMcpServers(dbAccess);
+ return NextResponse.json(servers);
+ } catch (error) {
+ console.error('Error fetching MCP servers from database:', error);
+ const errorMessage = error instanceof Error ? error.message : 'An unknown error occurred';
+ return NextResponse.json({ error: 'Failed to fetch MCP servers from database', details: errorMessage }, { status: 500 });
+ }
+}
- const servers = await Promise.all(
- serverFiles.map(async (file) => {
- const filePath = path.join(mcpSourceDir, file);
- const content = await fs.readFile(filePath, 'utf-8');
+export async function POST(request: Request) {
+ try {
+ const body = await request.json();
+ const validatedData = mcpServerInsertSchema.safeParse(body);
- // Extract server name and version from the file content
- const nameMatch = content.match(/name:\s*['"]([^'"]+)['"]/);
- const versionMatch = content.match(/version:\s*['"]([^'"]+)['"]/);
+ if (!validatedData.success) {
+ return NextResponse.json({ error: 'Invalid input', details: validatedData.error.flatten() }, { status: 400 });
+ }
- return {
- name: path.basename(file, '.ts'),
- serverName: nameMatch ? nameMatch[1] : path.basename(file, '.ts'),
- version: versionMatch ? versionMatch[1] : '1.0.0',
- filePath: file,
- enabled: false
- };
- })
- );
+ // Logic for type and url:
+ // If type is 'stdio', url should ideally be null.
+ if (validatedData.data.type === 'stdio' && validatedData.data.url) {
+ // Optionally, force URL to null for stdio, or return a specific error
+ // For now, let's allow it but it might be refined based on strictness requirements
+ // validatedData.data.url = null;
+ } else if ((validatedData.data.type === 'sse' || validatedData.data.type === 'streamable-http') && !validatedData.data.url) {
+ return NextResponse.json({ error: `URL is required for type '${validatedData.data.type}'` }, { status: 400 });
+ }
+
+ const newServer = await addUserMcpServerToDB(dbAccess, validatedData.data);
+ return NextResponse.json(newServer, { status: 201 });
- return NextResponse.json(servers);
} catch (error) {
- console.error('Error reading MCP servers:', error);
- return NextResponse.json({ error: 'Failed to read MCP servers' }, { status: 500 });
+ console.error('Error creating MCP server:', error);
+ const errorMessage = error instanceof Error ? error.message : 'An unknown error occurred';
+ // Check for specific error messages from addUserMcpServerToDB (e.g., unique constraints)
+ if (errorMessage.includes('already exists')) {
+ return NextResponse.json({ error: errorMessage }, { status: 409 }); // Conflict
+ }
+ return NextResponse.json({ error: 'Failed to create MCP server', details: errorMessage }, { status: 500 });
}
}
diff --git a/apps/dbagent/src/components/mcp/action.ts b/apps/dbagent/src/components/mcp/action.ts
index dbab1add..d847c52b 100644
--- a/apps/dbagent/src/components/mcp/action.ts
+++ b/apps/dbagent/src/components/mcp/action.ts
@@ -8,7 +8,7 @@ import { addUserMcpServerToDB, deleteUserMcpServer, getUserMcpServer, updateUser
import { MCPServer, MCPServerInsert } from '~/lib/db/schema';
//playbook db insert
-export async function actionAddUserMcpServerToDB(input: MCPServer): Promise {
+export async function actionAddUserMcpServerToDB(input: MCPServerInsert): Promise {
const dbAccess = await getUserSessionDBAccess();
return await addUserMcpServerToDB(dbAccess, input);
}
diff --git a/apps/dbagent/src/components/mcp/mcp-table.tsx b/apps/dbagent/src/components/mcp/mcp-table.tsx
index e0dd9d16..74189db1 100644
--- a/apps/dbagent/src/components/mcp/mcp-table.tsx
+++ b/apps/dbagent/src/components/mcp/mcp-table.tsx
@@ -108,6 +108,18 @@ export function McpTable() {
+
+
+
+
+
+
+
+
+
+
+
+
);
@@ -133,6 +145,8 @@ export function McpTable() {
Sever Name
File
+ Type
+ URL
Enabled
Actions
@@ -165,6 +179,31 @@ export function McpTable() {
{server.filePath}
+
+
+ {server.type}
+
+
+
+ {server.url ? (
+
+
+
+ {server.url}
+
+
+ {server.url}
+
+ ) : (
+ N/A
+ )}
+
handleToggleEnabled(server)} />
diff --git a/apps/dbagent/src/components/mcp/mcp-view.tsx b/apps/dbagent/src/components/mcp/mcp-view.tsx
index 4f9c7dc7..7fd96616 100644
--- a/apps/dbagent/src/components/mcp/mcp-view.tsx
+++ b/apps/dbagent/src/components/mcp/mcp-view.tsx
@@ -170,6 +170,78 @@ export function McpView({ server: initialServer }: { server: MCPServerInsert })
+ {/* Editable Fields Start */}
+ {/* Add Input for 'name' (identifier), crucial for creation */}
+ {!initialServer.id && ( // Only show 'name' input if it's a new server (no id yet)
+
+
Server Identifier (Name)
+
setServer({ ...server, name: e.target.value })}
+ placeholder="my-unique-server-name"
+ disabled={!!initialServer.id} // Disable if editing existing
+ />
+
Unique identifier for the server. Cannot be changed after creation.
+
+ )}
+
+ Server Display Name
+ setServer({ ...server, serverName: e.target.value })}
+ placeholder="My Awesome Server"
+ />
+
+
+ File Path
+ setServer({ ...server, filePath: e.target.value })}
+ placeholder="src/my-server.ts"
+ />
+
+
+ Version
+ setServer({ ...server, version: e.target.value })}
+ placeholder="1.0.0"
+ />
+
+
+ Type
+ {/* Assuming Select components are available from @xata.io/components or using native HTML */}
+ setServer({ ...server, type: e.target.value as MCPServerInsert['type'], url: e.target.value === 'stdio' ? null : server.url })}
+ className="w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
+ >
+ stdio
+ sse
+ streamable-http
+
+
+ {(server.type === 'sse' || server.type === 'streamable-http') && (
+
+ URL
+ setServer({ ...server, url: e.target.value })}
+ placeholder="http://localhost:8000/stream"
+ />
+
+ )}
+ {/* Editable Fields End */}
+
+ {/* Save Server Changes Button - to be added near CardFooter or repurpose existing save */}
+
+
{isInDb && (
Environment Variables
@@ -215,6 +287,68 @@ export function McpView({ server: initialServer }: { server: MCPServerInsert })
)}
+
+ {/* Placeholder for a general Save Server button */}
+
+ {
+ try {
+ let payload = { ...server };
+ if (server.type === 'stdio') {
+ payload.url = null; // Ensure URL is null for stdio
+ } else if ((server.type === 'sse' || server.type === 'streamable-http') && !server.url) {
+ toast.error(`URL is required for type '${server.type}'.`);
+ return;
+ }
+
+ // Validate required fields
+ if (!payload.name || !payload.serverName || !payload.filePath || !payload.version || !payload.type) {
+ toast.error('Name, Server Name, File Path, Version, and Type are required.');
+ return;
+ }
+
+ if (!initialServer.id && !isInDb) { // CREATE MODE
+ // Check if server with this name already exists (client-side check before calling action)
+ // The actionAddUserMcpServerToDB also checks, but this is a quicker feedback
+ const exists = await actionCheckUserMcpServerExists(payload.name);
+ if (exists) {
+ toast.error(`Server with identifier (name) "${payload.name}" already exists.`);
+ return;
+ }
+ const newServer = await actionAddUserMcpServerToDB(payload);
+ toast.success('Server created successfully!');
+ // Update isInDb and potentially initialServer to reflect creation
+ setIsInDb(true);
+ // It's better to navigate to the edit view of the new server or refresh.
+ // For now, just update local state and initialServer to allow further edits as if it's an edit page.
+ // This might need router.push(`/projects/${project}/mcp/${newServer.name}`) for a full SPA feel.
+ setServer(newServer); // Update local state with returned server (includes ID, defaults, etc.)
+ // To prevent re-triggering create mode if user clicks save again:
+ // This depends on how `initialServer` is managed by the parent of McpView
+ // A robust solution would involve navigating or the parent component refreshing `initialServer`.
+ // For now, we assume `setServer` with the new server (which has an ID) is enough for subsequent saves to be updates.
+ // A proper "create" page would likely redirect.
+ if (newServer.id) { // Simulate that it's now an existing server for subsequent saves
+ initialServer.id = newServer.id;
+ initialServer.name = newServer.name; // ensure name is also updated if it was somehow different
+ }
+
+ } else { // UPDATE MODE
+ await actionUpdateUserMcpServer(payload);
+ toast.success('Server updated successfully!');
+ }
+ // Optionally re-fetch or update local state if needed for parent components
+ } catch (error) {
+ console.error('Error saving server:', error);
+ toast.error(`Failed to save server: ${error instanceof Error ? error.message : 'Unknown error'}`);
+ }
+ }}
+ disabled={isCheckingDb || isSavingEnvVars}
+ >
+ {(!initialServer.id && !isInDb) ? 'Create Server' : 'Save Server Changes'}
+
+
+
Available Tools
{isLoading ? (
diff --git a/apps/dbagent/src/lib/db/mcp-servers.ts b/apps/dbagent/src/lib/db/mcp-servers.ts
index 5ca61373..b57069d0 100644
--- a/apps/dbagent/src/lib/db/mcp-servers.ts
+++ b/apps/dbagent/src/lib/db/mcp-servers.ts
@@ -23,41 +23,98 @@ export async function getUserMcpServer(dbAccess: DBAccess, serverName: string) {
//might need to update this for version and filepath aswell
export async function updateUserMcpServer(dbAccess: DBAccess, input: MCPServerInsert) {
return await dbAccess.query(async ({ db }) => {
+ const updateData: Partial
= {};
+ // Check and add fields to updateData if they are provided in input
+ if (input.enabled !== undefined) {
+ updateData.enabled = input.enabled;
+ }
+ if (input.envVars !== undefined) {
+ updateData.envVars = input.envVars;
+ }
+ if (input.type !== undefined) {
+ updateData.type = input.type;
+ }
+ // Explicitly check for undefined to allow setting url to null
+ if (input.url !== undefined) {
+ updateData.url = input.url;
+ }
+ if (input.version !== undefined) {
+ updateData.version = input.version;
+ }
+ if (input.filePath !== undefined) {
+ updateData.filePath = input.filePath;
+ }
+ // serverName can also be updated
+ if (input.serverName !== undefined) {
+ updateData.serverName = input.serverName;
+ }
+
+ // Ensure input.name is present, as it's used in the WHERE clause
+ if (!input.name) {
+ throw new Error('Server name (input.name) is required for update.');
+ }
+
+ // If no updatable fields were provided, fetch and return the existing server
+ if (Object.keys(updateData).length === 0) {
+ const currentServer = await db.select().from(mcpServers).where(eq(mcpServers.name, input.name)).limit(1);
+ if (currentServer.length === 0) {
+ throw new Error(`[UPDATE]Server with name "${input.name}" not found.`);
+ }
+ return currentServer[0];
+ }
+
+ // Note: If input.serverName is being updated, and it's unique,
+ // this could fail if another record already has the new serverName.
+ // The DB unique constraint will handle this.
const result = await db
.update(mcpServers)
- .set({
- enabled: input.enabled,
- envVars: input.envVars
- })
- .where(eq(mcpServers.name, input.name))
+ .set(updateData)
+ .where(eq(mcpServers.name, input.name))
.returning();
if (result.length === 0) {
- throw new Error(`[UPDATE]Server with name "${input.name}" not found`);
+ // This might happen if the server name in input.name doesn't exist
+ // or if the unique constraint on serverName failed during an update of serverName.
+ throw new Error(`[UPDATE]Server with name "${input.name}" not found, or update failed due to constraints (e.g., serverName already exists).`);
}
return result[0];
});
}
-export async function addUserMcpServerToDB(dbAccess: DBAccess, input: MCPServer): Promise {
+export async function addUserMcpServerToDB(dbAccess: DBAccess, input: MCPServerInsert): Promise {
return await dbAccess.query(async ({ db }) => {
- // Check if server with same name exists
- const existingServer = await db.select().from(mcpServers).where(eq(mcpServers.name, input.serverName)).limit(1);
+ // Validate required fields from MCPServerInsert
+ if (!input.name || !input.serverName || !input.filePath || !input.version) {
+ throw new Error('Required fields (name, serverName, filePath, version) are missing from input.');
+ }
+
+ // Check if server with same name or serverName exists
+ const existingServerByName = await db.select().from(mcpServers).where(eq(mcpServers.name, input.name)).limit(1);
+ if (existingServerByName.length > 0) {
+ throw new Error(`Server with name "${input.name}" already exists`);
+ }
- if (existingServer.length > 0) {
- throw new Error(`Server with name "${input.serverName}" already exists`);
+ // serverName is required by the check above, so no need for "if (input.serverName)"
+ const existingServerByServerName = await db.select().from(mcpServers).where(eq(mcpServers.serverName, input.serverName)).limit(1);
+ if (existingServerByServerName.length > 0) {
+ throw new Error(`Server with serverName "${input.serverName}" already exists`);
}
// Create new server
+ // All fields in MCPServerInsert should be passed.
+ // Drizzle handles undefined for optional fields (inserts NULL or uses default)
const result = await db
.insert(mcpServers)
.values({
- name: input.name,
- serverName: input.serverName,
- version: input.version,
- filePath: input.filePath,
- enabled: input.enabled
+ name: input.name, // required
+ serverName: input.serverName, // required
+ filePath: input.filePath, // required
+ version: input.version, // required
+ enabled: input.enabled, // has default
+ envVars: input.envVars, // has default
+ type: input.type, // has default
+ url: input.url // optional (nullable)
})
.returning();
diff --git a/apps/dbagent/src/lib/db/schema.ts b/apps/dbagent/src/lib/db/schema.ts
index dc97f5d0..f8708a92 100644
--- a/apps/dbagent/src/lib/db/schema.ts
+++ b/apps/dbagent/src/lib/db/schema.ts
@@ -37,6 +37,9 @@ export type MemberRole = InferEnumType;
export const cloudProvider = pgEnum('cloud_provider', ['aws', 'gcp', 'other']);
export type CloudProvider = InferEnumType;
+export const mcpServerType = pgEnum('mcp_server_type', ['stdio', 'sse', 'streamable-http']);
+export type MCPServerType = InferEnumType;
+
export const awsClusters = pgTable(
'aws_clusters',
{
@@ -684,7 +687,9 @@ export const mcpServers = pgTable(
version: text('version').notNull(),
enabled: boolean('enabled').default(true).notNull(),
envVars: jsonb('env_vars').$type>().default({}).notNull(),
- createdAt: timestamp('created_at', { mode: 'date' }).defaultNow().notNull()
+ createdAt: timestamp('created_at', { mode: 'date' }).defaultNow().notNull(),
+ type: mcpServerType('type').default('stdio'),
+ url: text('url')
},
(table) => [
unique('uq_mcp_servers_name').on(table.name),
From 9135df18cf85c069abfbb6a48c387bf5c9aae8d5 Mon Sep 17 00:00:00 2001
From: Alexis Rico
Date: Wed, 21 May 2025 07:34:26 +0200
Subject: [PATCH 4/4] Work in progress
Signed-off-by: Alexis Rico
---
.../projects/[project]/mcp/[server]/page.tsx | 18 +++++----
apps/dbagent/src/components/mcp/mcp-view.tsx | 40 +++++++++++--------
apps/dbagent/src/lib/ai/tools/user-mcp.ts | 31 ++++++++++----
apps/dbagent/src/lib/db/mcp-servers.ts | 11 +++--
apps/dbagent/src/lib/db/schema.ts | 2 +-
5 files changed, 65 insertions(+), 37 deletions(-)
diff --git a/apps/dbagent/src/app/(main)/projects/[project]/mcp/[server]/page.tsx b/apps/dbagent/src/app/(main)/projects/[project]/mcp/[server]/page.tsx
index cf726f58..690fbd88 100644
--- a/apps/dbagent/src/app/(main)/projects/[project]/mcp/[server]/page.tsx
+++ b/apps/dbagent/src/app/(main)/projects/[project]/mcp/[server]/page.tsx
@@ -29,11 +29,13 @@ export default async function McpServerPage({ params }: { params: Promise(false);
const [isCheckingDb, setIsCheckingDb] = useState(true);
- const [envVars, setEnvVars] = useState>(initialServer.envVars || {});
+ const [config, setConfig] = useState(initialServer.config);
const [isSavingEnvVars, setIsSavingEnvVars] = useState(false);
useEffect(() => {
setServer(initialServer);
- setEnvVars(initialServer.envVars || {});
+ setConfig(initialServer.config);
}, [initialServer]);
useEffect(() => {
@@ -93,24 +93,32 @@ export function McpView({ server: initialServer }: { server: MCPServerInsert })
};
const handleAddEnvVar = () => {
- setEnvVars({ ...envVars, '': '' });
+ if (config.type !== 'local') return;
+
+ setConfig({ ...config, env: { ...config.env, '': '' } });
};
const handleEnvVarChange = (index: number, key: string, value: string) => {
- const entries = Object.entries(envVars);
+ if (config.type !== 'local') return;
+
+ const entries = Object.entries(config.env ?? {});
const oldKey = entries[index]?.[0];
const newEntries = entries.filter(([k]) => k !== oldKey);
newEntries.splice(index, 0, [key, value]);
- setEnvVars(Object.fromEntries(newEntries));
+ setConfig({ ...config, env: Object.fromEntries(newEntries) });
};
const handleRemoveEnvVar = (keyToRemove: string) => {
- const newEnvVars = { ...envVars };
- delete newEnvVars[keyToRemove];
- setEnvVars(newEnvVars);
+ if (config.type !== 'local') return;
+
+ const newConfig = { ...config };
+ delete newConfig.env?.[keyToRemove];
+ setConfig(newConfig);
};
const handleSaveEnvVars = async () => {
+ if (config.type !== 'local') return;
+
setIsSavingEnvVars(true);
try {
if (!isInDb) {
@@ -119,11 +127,11 @@ export function McpView({ server: initialServer }: { server: MCPServerInsert })
return;
}
- const varsToSave = Object.fromEntries(Object.entries(envVars).filter(([key]) => key.trim() !== ''));
+ const varsToSave = Object.fromEntries(Object.entries(config.env ?? {}).filter(([key]) => key.trim() !== ''));
const updatedServerData = { ...server, envVars: varsToSave };
await actionUpdateUserMcpServer(updatedServerData);
setServer(updatedServerData);
- setEnvVars(varsToSave);
+ setConfig({ ...config, env: varsToSave });
toast.success('Environment variables saved successfully.');
} catch (error) {
console.error('Error saving environment variables:', error);
@@ -145,15 +153,15 @@ export function McpView({ server: initialServer }: { server: MCPServerInsert })
- MCP Server: {server.serverName}
+ MCP Server: {server.name}
Version: {server.version}
-
File Path
-
{server.filePath}
+
Type
+
{server.config.type}
Status
@@ -170,14 +178,14 @@ export function McpView({ server: initialServer }: { server: MCPServerInsert })
- {isInDb && (
+ {isInDb && config.type === 'local' && (
Environment Variables
These variables will be passed to the MCP server process.
- {Object.entries(envVars).map(([key, value], index) => (
+ {Object.entries(config.env ?? {}).map(([key, value], index) => (
{
}
}
+function createTransport(config: McpServerConfig) {
+ switch (config.type) {
+ case 'local':
+ return new Experimental_StdioMCPTransport({
+ command: 'node',
+ args: [config.filePath],
+ env: config.env
+ });
+ case 'sse':
+ return {
+ type: 'sse',
+ url: config.url,
+ headers: config.headers
+ } as const;
+ case 'streamable-http':
+ return new StreamableHTTPClientTransport(new URL(config.url), {
+ sessionId: undefined
+ });
+ }
+}
+
async function loadToolsFromFile(filePath: string): Promise {
try {
const serverName = path.basename(filePath, '.js');
@@ -69,14 +92,8 @@ async function loadToolsFromFile(filePath: string): Promise {
return {};
}
- const transport = new Experimental_StdioMCPTransport({
- command: 'node',
- args: [filePath],
- env: serverDetails?.envVars
- });
-
const client = await experimental_createMCPClient({
- transport
+ transport: createTransport(serverDetails.config)
});
return await client.tools();
diff --git a/apps/dbagent/src/lib/db/mcp-servers.ts b/apps/dbagent/src/lib/db/mcp-servers.ts
index 5ca61373..0294ea6a 100644
--- a/apps/dbagent/src/lib/db/mcp-servers.ts
+++ b/apps/dbagent/src/lib/db/mcp-servers.ts
@@ -27,7 +27,7 @@ export async function updateUserMcpServer(dbAccess: DBAccess, input: MCPServerIn
.update(mcpServers)
.set({
enabled: input.enabled,
- envVars: input.envVars
+ config: input.config
})
.where(eq(mcpServers.name, input.name))
.returning();
@@ -43,10 +43,10 @@ export async function updateUserMcpServer(dbAccess: DBAccess, input: MCPServerIn
export async function addUserMcpServerToDB(dbAccess: DBAccess, input: MCPServer): Promise {
return await dbAccess.query(async ({ db }) => {
// Check if server with same name exists
- const existingServer = await db.select().from(mcpServers).where(eq(mcpServers.name, input.serverName)).limit(1);
+ const existingServer = await db.select().from(mcpServers).where(eq(mcpServers.name, input.name)).limit(1);
if (existingServer.length > 0) {
- throw new Error(`Server with name "${input.serverName}" already exists`);
+ throw new Error(`Server with name "${input.name}" already exists`);
}
// Create new server
@@ -54,10 +54,9 @@ export async function addUserMcpServerToDB(dbAccess: DBAccess, input: MCPServer)
.insert(mcpServers)
.values({
name: input.name,
- serverName: input.serverName,
version: input.version,
- filePath: input.filePath,
- enabled: input.enabled
+ enabled: input.enabled,
+ config: input.config
})
.returning();
diff --git a/apps/dbagent/src/lib/db/schema.ts b/apps/dbagent/src/lib/db/schema.ts
index 8c988bf9..7d33c997 100644
--- a/apps/dbagent/src/lib/db/schema.ts
+++ b/apps/dbagent/src/lib/db/schema.ts
@@ -678,7 +678,7 @@ export type McpServerConfig =
| {
type: 'local';
filePath: string;
- envVars?: Record;
+ env?: Record;
}
| {
type: 'sse';