mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-07-03 12:27:41 +08:00
feat(migration): add McpServerMigrator for v2 data migration (#13303)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
36
migrations/sqlite-drizzle/0002_tired_glorian.sql
Normal file
36
migrations/sqlite-drizzle/0002_tired_glorian.sql
Normal file
@@ -0,0 +1,36 @@
|
||||
CREATE TABLE `mcp_server` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`name` text NOT NULL,
|
||||
`type` text,
|
||||
`description` text,
|
||||
`base_url` text,
|
||||
`command` text,
|
||||
`registry_url` text,
|
||||
`args` text,
|
||||
`env` text,
|
||||
`headers` text,
|
||||
`provider` text,
|
||||
`provider_url` text,
|
||||
`logo_url` text,
|
||||
`tags` text,
|
||||
`long_running` integer,
|
||||
`timeout` integer,
|
||||
`dxt_version` text,
|
||||
`dxt_path` text,
|
||||
`reference` text,
|
||||
`search_key` text,
|
||||
`config_sample` text,
|
||||
`disabled_tools` text,
|
||||
`disabled_auto_approve_tools` text,
|
||||
`should_config` integer,
|
||||
`is_active` integer DEFAULT false NOT NULL,
|
||||
`install_source` text,
|
||||
`is_trusted` integer,
|
||||
`trusted_at` integer,
|
||||
`installed_at` integer,
|
||||
`created_at` integer,
|
||||
`updated_at` integer
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX `mcp_server_name_idx` ON `mcp_server` (`name`);--> statement-breakpoint
|
||||
CREATE INDEX `mcp_server_is_active_idx` ON `mcp_server` (`is_active`);
|
||||
851
migrations/sqlite-drizzle/meta/0002_snapshot.json
Normal file
851
migrations/sqlite-drizzle/meta/0002_snapshot.json
Normal file
@@ -0,0 +1,851 @@
|
||||
{
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"id": "f283d5d9-7eaa-40ff-8f9f-fbad46e536fd",
|
||||
"prevId": "a433b120-0ab8-4f3f-9d1d-766b48c216c8",
|
||||
"tables": {
|
||||
"app_state": {
|
||||
"name": "app_state",
|
||||
"columns": {
|
||||
"key": {
|
||||
"name": "key",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"value": {
|
||||
"name": "value",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"description": {
|
||||
"name": "description",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"group": {
|
||||
"name": "group",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"entity_type": {
|
||||
"name": "entity_type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"sort_order": {
|
||||
"name": "sort_order",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": 0
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"group_entity_sort_idx": {
|
||||
"name": "group_entity_sort_idx",
|
||||
"columns": ["entity_type", "sort_order"],
|
||||
"isUnique": false
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"mcp_server": {
|
||||
"name": "mcp_server",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"type": {
|
||||
"name": "type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"description": {
|
||||
"name": "description",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"base_url": {
|
||||
"name": "base_url",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"command": {
|
||||
"name": "command",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"registry_url": {
|
||||
"name": "registry_url",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"args": {
|
||||
"name": "args",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"env": {
|
||||
"name": "env",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"headers": {
|
||||
"name": "headers",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"provider": {
|
||||
"name": "provider",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"provider_url": {
|
||||
"name": "provider_url",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"logo_url": {
|
||||
"name": "logo_url",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"tags": {
|
||||
"name": "tags",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"long_running": {
|
||||
"name": "long_running",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"timeout": {
|
||||
"name": "timeout",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"dxt_version": {
|
||||
"name": "dxt_version",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"dxt_path": {
|
||||
"name": "dxt_path",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"reference": {
|
||||
"name": "reference",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"search_key": {
|
||||
"name": "search_key",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"config_sample": {
|
||||
"name": "config_sample",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"disabled_tools": {
|
||||
"name": "disabled_tools",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"disabled_auto_approve_tools": {
|
||||
"name": "disabled_auto_approve_tools",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"should_config": {
|
||||
"name": "should_config",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"is_active": {
|
||||
"name": "is_active",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": false
|
||||
},
|
||||
"install_source": {
|
||||
"name": "install_source",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"is_trusted": {
|
||||
"name": "is_trusted",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"trusted_at": {
|
||||
"name": "trusted_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"installed_at": {
|
||||
"name": "installed_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"mcp_server_name_idx": {
|
||||
"name": "mcp_server_name_idx",
|
||||
"columns": ["name"],
|
||||
"isUnique": false
|
||||
},
|
||||
"mcp_server_is_active_idx": {
|
||||
"name": "mcp_server_is_active_idx",
|
||||
"columns": ["is_active"],
|
||||
"isUnique": false
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"message": {
|
||||
"name": "message",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"parent_id": {
|
||||
"name": "parent_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"topic_id": {
|
||||
"name": "topic_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"role": {
|
||||
"name": "role",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"data": {
|
||||
"name": "data",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"searchable_text": {
|
||||
"name": "searchable_text",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"status": {
|
||||
"name": "status",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"siblings_group_id": {
|
||||
"name": "siblings_group_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": 0
|
||||
},
|
||||
"assistant_id": {
|
||||
"name": "assistant_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"assistant_meta": {
|
||||
"name": "assistant_meta",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"model_id": {
|
||||
"name": "model_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"model_meta": {
|
||||
"name": "model_meta",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"trace_id": {
|
||||
"name": "trace_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"stats": {
|
||||
"name": "stats",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"deleted_at": {
|
||||
"name": "deleted_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"message_parent_id_idx": {
|
||||
"name": "message_parent_id_idx",
|
||||
"columns": ["parent_id"],
|
||||
"isUnique": false
|
||||
},
|
||||
"message_topic_created_idx": {
|
||||
"name": "message_topic_created_idx",
|
||||
"columns": ["topic_id", "created_at"],
|
||||
"isUnique": false
|
||||
},
|
||||
"message_trace_id_idx": {
|
||||
"name": "message_trace_id_idx",
|
||||
"columns": ["trace_id"],
|
||||
"isUnique": false
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"message_topic_id_topic_id_fk": {
|
||||
"name": "message_topic_id_topic_id_fk",
|
||||
"tableFrom": "message",
|
||||
"tableTo": "topic",
|
||||
"columnsFrom": ["topic_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"message_parent_id_message_id_fk": {
|
||||
"name": "message_parent_id_message_id_fk",
|
||||
"tableFrom": "message",
|
||||
"tableTo": "message",
|
||||
"columnsFrom": ["parent_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "set null",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {
|
||||
"message_role_check": {
|
||||
"name": "message_role_check",
|
||||
"value": "\"message\".\"role\" IN ('user', 'assistant', 'system')"
|
||||
},
|
||||
"message_status_check": {
|
||||
"name": "message_status_check",
|
||||
"value": "\"message\".\"status\" IN ('pending', 'success', 'error', 'paused')"
|
||||
}
|
||||
}
|
||||
},
|
||||
"preference": {
|
||||
"name": "preference",
|
||||
"columns": {
|
||||
"scope": {
|
||||
"name": "scope",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "'default'"
|
||||
},
|
||||
"key": {
|
||||
"name": "key",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"value": {
|
||||
"name": "value",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"preference_scope_key_pk": {
|
||||
"columns": ["scope", "key"],
|
||||
"name": "preference_scope_key_pk"
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"entity_tag": {
|
||||
"name": "entity_tag",
|
||||
"columns": {
|
||||
"entity_type": {
|
||||
"name": "entity_type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"entity_id": {
|
||||
"name": "entity_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"tag_id": {
|
||||
"name": "tag_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"entity_tag_tag_id_idx": {
|
||||
"name": "entity_tag_tag_id_idx",
|
||||
"columns": ["tag_id"],
|
||||
"isUnique": false
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"entity_tag_tag_id_tag_id_fk": {
|
||||
"name": "entity_tag_tag_id_tag_id_fk",
|
||||
"tableFrom": "entity_tag",
|
||||
"tableTo": "tag",
|
||||
"columnsFrom": ["tag_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {
|
||||
"entity_tag_entity_type_entity_id_tag_id_pk": {
|
||||
"columns": ["entity_type", "entity_id", "tag_id"],
|
||||
"name": "entity_tag_entity_type_entity_id_tag_id_pk"
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"tag": {
|
||||
"name": "tag",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"color": {
|
||||
"name": "color",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"tag_name_unique": {
|
||||
"name": "tag_name_unique",
|
||||
"columns": ["name"],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"topic": {
|
||||
"name": "topic",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"is_name_manually_edited": {
|
||||
"name": "is_name_manually_edited",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": false
|
||||
},
|
||||
"assistant_id": {
|
||||
"name": "assistant_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"assistant_meta": {
|
||||
"name": "assistant_meta",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"prompt": {
|
||||
"name": "prompt",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"active_node_id": {
|
||||
"name": "active_node_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"group_id": {
|
||||
"name": "group_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"sort_order": {
|
||||
"name": "sort_order",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": 0
|
||||
},
|
||||
"is_pinned": {
|
||||
"name": "is_pinned",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": false
|
||||
},
|
||||
"pinned_order": {
|
||||
"name": "pinned_order",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": 0
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"deleted_at": {
|
||||
"name": "deleted_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"topic_group_updated_idx": {
|
||||
"name": "topic_group_updated_idx",
|
||||
"columns": ["group_id", "updated_at"],
|
||||
"isUnique": false
|
||||
},
|
||||
"topic_group_sort_idx": {
|
||||
"name": "topic_group_sort_idx",
|
||||
"columns": ["group_id", "sort_order"],
|
||||
"isUnique": false
|
||||
},
|
||||
"topic_updated_at_idx": {
|
||||
"name": "topic_updated_at_idx",
|
||||
"columns": ["updated_at"],
|
||||
"isUnique": false
|
||||
},
|
||||
"topic_is_pinned_idx": {
|
||||
"name": "topic_is_pinned_idx",
|
||||
"columns": ["is_pinned", "pinned_order"],
|
||||
"isUnique": false
|
||||
},
|
||||
"topic_assistant_id_idx": {
|
||||
"name": "topic_assistant_id_idx",
|
||||
"columns": ["assistant_id"],
|
||||
"isUnique": false
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"topic_group_id_group_id_fk": {
|
||||
"name": "topic_group_id_group_id_fk",
|
||||
"tableFrom": "topic",
|
||||
"tableTo": "group",
|
||||
"columnsFrom": ["group_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "set null",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
}
|
||||
},
|
||||
"views": {},
|
||||
"enums": {},
|
||||
"_meta": {
|
||||
"schemas": {},
|
||||
"tables": {},
|
||||
"columns": {}
|
||||
},
|
||||
"internal": {
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,13 @@
|
||||
"tag": "0001_futuristic_human_fly",
|
||||
"version": "6",
|
||||
"when": 1767455592181
|
||||
},
|
||||
{
|
||||
"idx": 2,
|
||||
"version": "6",
|
||||
"when": 1772954746790,
|
||||
"tag": "0002_tired_glorian",
|
||||
"breakpoints": true
|
||||
}
|
||||
],
|
||||
"version": "7"
|
||||
|
||||
6
packages/shared/data/cache/cacheSchemas.ts
vendored
6
packages/shared/data/cache/cacheSchemas.ts
vendored
@@ -225,10 +225,14 @@ export const DefaultSharedCache: SharedCacheSchema = {
|
||||
*/
|
||||
export type RendererPersistCacheSchema = {
|
||||
'ui.tab.state': CacheValueTypes.TabsState
|
||||
'feature.mcp.is_uv_installed': boolean
|
||||
'feature.mcp.is_bun_installed': boolean
|
||||
}
|
||||
|
||||
export const DefaultRendererPersistCache: RendererPersistCacheSchema = {
|
||||
'ui.tab.state': { tabs: [], activeTabId: '' }
|
||||
'ui.tab.state': { tabs: [], activeTabId: '' },
|
||||
'feature.mcp.is_uv_installed': false,
|
||||
'feature.mcp.is_bun_installed': false
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
64
src/main/data/db/schemas/mcpServer.ts
Normal file
64
src/main/data/db/schemas/mcpServer.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import type { MCPConfigSample } from '@types'
|
||||
import { sql } from 'drizzle-orm'
|
||||
import { check, index, integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'
|
||||
|
||||
import { createUpdateTimestamps, uuidPrimaryKey } from './_columnHelpers'
|
||||
|
||||
/**
|
||||
* MCP Server table - stores user-configured MCP server definitions
|
||||
*
|
||||
* Migrated from Redux state.mcp.servers.
|
||||
* Runtime flags (isUvInstalled, isBunInstalled) are NOT migrated - they are
|
||||
* re-detected at runtime and stored via usePersistCache.
|
||||
*/
|
||||
export const mcpServerTable = sqliteTable(
|
||||
'mcp_server',
|
||||
{
|
||||
id: uuidPrimaryKey(),
|
||||
name: text().notNull(),
|
||||
type: text(),
|
||||
description: text(),
|
||||
baseUrl: text(),
|
||||
command: text(),
|
||||
registryUrl: text(),
|
||||
args: text({ mode: 'json' }).$type<string[]>(),
|
||||
env: text({ mode: 'json' }).$type<Record<string, string>>(),
|
||||
headers: text({ mode: 'json' }).$type<Record<string, string>>(),
|
||||
provider: text(),
|
||||
providerUrl: text(),
|
||||
logoUrl: text(),
|
||||
tags: text({ mode: 'json' }).$type<string[]>(),
|
||||
longRunning: integer({ mode: 'boolean' }),
|
||||
timeout: integer(),
|
||||
dxtVersion: text(),
|
||||
dxtPath: text(),
|
||||
reference: text(),
|
||||
searchKey: text(),
|
||||
configSample: text({ mode: 'json' }).$type<MCPConfigSample>(),
|
||||
disabledTools: text({ mode: 'json' }).$type<string[]>(),
|
||||
disabledAutoApproveTools: text({ mode: 'json' }).$type<string[]>(),
|
||||
shouldConfig: integer({ mode: 'boolean' }),
|
||||
isActive: integer({ mode: 'boolean' }).notNull().default(false),
|
||||
installSource: text(),
|
||||
isTrusted: integer({ mode: 'boolean' }),
|
||||
trustedAt: integer(),
|
||||
installedAt: integer(),
|
||||
|
||||
...createUpdateTimestamps
|
||||
},
|
||||
(t) => [
|
||||
index('mcp_server_name_idx').on(t.name),
|
||||
index('mcp_server_is_active_idx').on(t.isActive),
|
||||
check(
|
||||
'mcp_server_type_check',
|
||||
sql`${t.type} IS NULL OR ${t.type} IN ('stdio', 'sse', 'streamableHttp', 'inMemory')`
|
||||
),
|
||||
check(
|
||||
'mcp_server_install_source_check',
|
||||
sql`${t.installSource} IS NULL OR ${t.installSource} IN ('builtin', 'manual', 'protocol', 'unknown')`
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
export type McpServerInsert = typeof mcpServerTable.$inferInsert
|
||||
export type McpServerSelect = typeof mcpServerTable.$inferSelect
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
import { dbService } from '@data/db/DbService'
|
||||
import { appStateTable } from '@data/db/schemas/appState'
|
||||
import { mcpServerTable } from '@data/db/schemas/mcpServer'
|
||||
import { messageTable } from '@data/db/schemas/message'
|
||||
import { preferenceTable } from '@data/db/schemas/preference'
|
||||
import { topicTable } from '@data/db/schemas/topic'
|
||||
@@ -210,6 +211,7 @@ export class MigrationEngine {
|
||||
const tables = [
|
||||
{ table: messageTable, name: 'message' }, // Must clear before topic (FK reference)
|
||||
{ table: topicTable, name: 'topic' },
|
||||
{ table: mcpServerTable, name: 'mcp_server' },
|
||||
{ table: preferenceTable, name: 'preference' }
|
||||
// TODO: Add these when tables are created
|
||||
// { table: assistantTable, name: 'assistant' },
|
||||
@@ -230,6 +232,7 @@ export class MigrationEngine {
|
||||
// Messages reference topics, so delete messages first
|
||||
await db.delete(messageTable)
|
||||
await db.delete(topicTable)
|
||||
await db.delete(mcpServerTable)
|
||||
await db.delete(preferenceTable)
|
||||
// TODO: Add these when tables are created (in correct order)
|
||||
// await db.delete(fileTable)
|
||||
|
||||
179
src/main/data/migration/v2/migrators/McpServerMigrator.ts
Normal file
179
src/main/data/migration/v2/migrators/McpServerMigrator.ts
Normal file
@@ -0,0 +1,179 @@
|
||||
/**
|
||||
* MCP Server migrator - migrates MCP servers from Redux to SQLite
|
||||
*
|
||||
* Data sources:
|
||||
* - Redux mcp slice (state.mcp.servers) -> mcp_server table
|
||||
*
|
||||
* Skipped fields (runtime/cache, re-detected when MCP settings are accessed):
|
||||
* - isUvInstalled, isBunInstalled -> usePersistCache
|
||||
*
|
||||
* Not migrated (regenerable cache, re-fetched from provider API):
|
||||
* - Dexie mcp:provider:*:servers (handled in separate PR)
|
||||
*/
|
||||
|
||||
import { type McpServerInsert, mcpServerTable } from '@data/db/schemas/mcpServer'
|
||||
import { loggerService } from '@logger'
|
||||
import type { ExecuteResult, PrepareResult, ValidateResult } from '@shared/data/migration/v2/types'
|
||||
import { sql } from 'drizzle-orm'
|
||||
|
||||
import type { MigrationContext } from '../core/MigrationContext'
|
||||
import { BaseMigrator } from './BaseMigrator'
|
||||
import { transformMcpServer } from './mappings/McpServerMappings'
|
||||
|
||||
const logger = loggerService.withContext('McpServerMigrator')
|
||||
|
||||
export class McpServerMigrator extends BaseMigrator {
|
||||
readonly id = 'mcp_server'
|
||||
readonly name = 'MCP Server'
|
||||
readonly description = 'Migrate MCP server configurations from Redux to SQLite'
|
||||
readonly order = 1.5
|
||||
|
||||
private preparedRows: McpServerInsert[] = []
|
||||
private skippedCount = 0
|
||||
|
||||
async prepare(ctx: MigrationContext): Promise<PrepareResult> {
|
||||
this.preparedRows = []
|
||||
this.skippedCount = 0
|
||||
|
||||
try {
|
||||
const warnings: string[] = []
|
||||
const servers = ctx.sources.reduxState.get<unknown[]>('mcp', 'servers') ?? []
|
||||
|
||||
if (!Array.isArray(servers)) {
|
||||
logger.warn('mcp.servers is not an array, skipping')
|
||||
warnings.push('mcp.servers is not an array')
|
||||
} else {
|
||||
const seenIds = new Set<string>()
|
||||
|
||||
for (const server of servers) {
|
||||
const s = server as Record<string, unknown>
|
||||
|
||||
if (!s.id || typeof s.id !== 'string') {
|
||||
this.skippedCount++
|
||||
warnings.push(`Skipped server without valid id: ${s.name ?? 'unknown'}`)
|
||||
continue
|
||||
}
|
||||
|
||||
if (seenIds.has(s.id)) {
|
||||
this.skippedCount++
|
||||
warnings.push(`Skipped duplicate server id: ${s.id}`)
|
||||
continue
|
||||
}
|
||||
seenIds.add(s.id)
|
||||
|
||||
try {
|
||||
this.preparedRows.push(transformMcpServer(s))
|
||||
} catch (err) {
|
||||
this.skippedCount++
|
||||
warnings.push(`Failed to transform server ${s.id}: ${(err as Error).message}`)
|
||||
logger.warn(`Skipping server ${s.id}`, err as Error)
|
||||
}
|
||||
}
|
||||
|
||||
if (this.skippedCount > 0 && this.preparedRows.length === 0 && servers.length > 0) {
|
||||
return {
|
||||
success: false,
|
||||
itemCount: 0,
|
||||
warnings
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.info('Preparation completed', {
|
||||
serverCount: this.preparedRows.length,
|
||||
skipped: this.skippedCount
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
itemCount: this.preparedRows.length,
|
||||
warnings: warnings.length > 0 ? warnings : undefined
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Preparation failed', error as Error)
|
||||
return {
|
||||
success: false,
|
||||
itemCount: 0,
|
||||
warnings: [error instanceof Error ? error.message : String(error)]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async execute(ctx: MigrationContext): Promise<ExecuteResult> {
|
||||
if (this.preparedRows.length === 0) {
|
||||
return { success: true, processedCount: 0 }
|
||||
}
|
||||
|
||||
try {
|
||||
let processed = 0
|
||||
|
||||
const BATCH_SIZE = 100
|
||||
await ctx.db.transaction(async (tx) => {
|
||||
for (let i = 0; i < this.preparedRows.length; i += BATCH_SIZE) {
|
||||
const batch = this.preparedRows.slice(i, i + BATCH_SIZE)
|
||||
await tx.insert(mcpServerTable).values(batch)
|
||||
processed += batch.length
|
||||
}
|
||||
})
|
||||
|
||||
this.reportProgress(100, `Migrated ${processed} items`, {
|
||||
key: 'migration.progress.migrated_mcp_servers',
|
||||
params: { processed, total: this.preparedRows.length }
|
||||
})
|
||||
|
||||
logger.info('Execute completed', { processedCount: processed })
|
||||
|
||||
return { success: true, processedCount: processed }
|
||||
} catch (error) {
|
||||
logger.error('Execute failed', error as Error)
|
||||
return {
|
||||
success: false,
|
||||
processedCount: 0,
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async validate(ctx: MigrationContext): Promise<ValidateResult> {
|
||||
try {
|
||||
const serverResult = await ctx.db.select({ count: sql<number>`count(*)` }).from(mcpServerTable).get()
|
||||
const serverCount = serverResult?.count ?? 0
|
||||
const errors: { key: string; message: string }[] = []
|
||||
|
||||
if (serverCount !== this.preparedRows.length) {
|
||||
errors.push({
|
||||
key: 'count_mismatch',
|
||||
message: `Expected ${this.preparedRows.length} servers but found ${serverCount}`
|
||||
})
|
||||
}
|
||||
|
||||
const sample = await ctx.db.select().from(mcpServerTable).limit(3).all()
|
||||
for (const server of sample) {
|
||||
if (!server.id || !server.name) {
|
||||
errors.push({ key: server.id ?? 'unknown', message: 'Missing required field (id or name)' })
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: errors.length === 0,
|
||||
errors,
|
||||
stats: {
|
||||
sourceCount: this.preparedRows.length,
|
||||
targetCount: serverCount,
|
||||
skippedCount: this.skippedCount
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Validation failed', error as Error)
|
||||
return {
|
||||
success: false,
|
||||
errors: [{ key: 'validation', message: error instanceof Error ? error.message : String(error) }],
|
||||
stats: {
|
||||
sourceCount: this.preparedRows.length,
|
||||
targetCount: 0,
|
||||
skippedCount: this.skippedCount
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
# McpServerMigrator
|
||||
|
||||
Migrates MCP server configurations from Redux to SQLite.
|
||||
|
||||
## Data Sources
|
||||
|
||||
| Source | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| Redux | `state.mcp.servers` | Array of MCPServer objects |
|
||||
|
||||
## Target Table
|
||||
|
||||
`mcp_server` (defined in `src/main/data/db/schemas/mcpServer.ts`)
|
||||
|
||||
## Skipped Fields (Runtime/Cache)
|
||||
|
||||
| Field | Reason | V2 Target |
|
||||
|-------|--------|-----------|
|
||||
| `isUvInstalled` | Re-detected when MCP settings are accessed | `usePersistCache('feature.mcp.is_uv_installed')` |
|
||||
| `isBunInstalled` | Re-detected when MCP settings are accessed | `usePersistCache('feature.mcp.is_bun_installed')` |
|
||||
|
||||
## Not Migrated (Regenerable Cache)
|
||||
|
||||
| Source | Reason | V2 Target |
|
||||
|--------|--------|-----------|
|
||||
| Dexie `mcp:provider:*:servers` | Re-fetched from provider API | Handled in separate PR |
|
||||
|
||||
## Field Mappings
|
||||
|
||||
All MCPServer fields are mapped 1:1 at the Drizzle ORM level (camelCase property names). The underlying SQLite columns use snake_case (e.g., `baseUrl` → `base_url`), handled automatically by Drizzle:
|
||||
|
||||
| Source Field | Target Column | Transform |
|
||||
|---|---|---|
|
||||
| `id` | `id` | Direct (PK) |
|
||||
| `name` | `name` | Direct (NOT NULL) |
|
||||
| `type` | `type` | Nullable passthrough |
|
||||
| `description` | `description` | Nullable passthrough |
|
||||
| `baseUrl` / `url` | `baseUrl` | Falls back from `url` if `baseUrl` is absent (legacy SSE servers) |
|
||||
| `command` | `command` | Nullable passthrough |
|
||||
| `registryUrl` | `registryUrl` | Nullable passthrough |
|
||||
| `args` | `args` | JSON array |
|
||||
| `env` | `env` | JSON object |
|
||||
| `headers` | `headers` | JSON object |
|
||||
| `provider` | `provider` | Nullable passthrough |
|
||||
| `providerUrl` | `providerUrl` | Nullable passthrough |
|
||||
| `logoUrl` | `logoUrl` | Nullable passthrough |
|
||||
| `tags` | `tags` | JSON array |
|
||||
| `longRunning` | `longRunning` | Nullable boolean |
|
||||
| `timeout` | `timeout` | Nullable integer |
|
||||
| `dxtVersion` | `dxtVersion` | Nullable passthrough |
|
||||
| `dxtPath` | `dxtPath` | Nullable passthrough |
|
||||
| `reference` | `reference` | Nullable passthrough |
|
||||
| `searchKey` | `searchKey` | Nullable passthrough |
|
||||
| `configSample` | `configSample` | JSON object |
|
||||
| `disabledTools` | `disabledTools` | JSON array |
|
||||
| `disabledAutoApproveTools` | `disabledAutoApproveTools` | JSON array |
|
||||
| `shouldConfig` | `shouldConfig` | Nullable boolean |
|
||||
| `isActive` | `isActive` | Boolean (NOT NULL, default false) |
|
||||
| `installSource` | `installSource` | Nullable passthrough |
|
||||
| `isTrusted` | `isTrusted` | Nullable boolean |
|
||||
| `trustedAt` | `trustedAt` | Nullable integer (timestamp) |
|
||||
| `installedAt` | `installedAt` | Nullable integer (timestamp) |
|
||||
|
||||
## Edge Cases
|
||||
|
||||
- **Missing `id`**: Server is skipped with warning
|
||||
- **Empty `id`**: Server is skipped with warning
|
||||
- **Duplicate `id`**: Second occurrence is skipped, first is kept
|
||||
- **Missing `isActive`**: Defaults to `false`
|
||||
- **`undefined`/`null` optional fields**: Stored as `null` in SQLite
|
||||
|
||||
## Execution Order
|
||||
|
||||
`order = 1.5` (after PreferencesMigrator=1, before AssistantMigrator=2)
|
||||
@@ -0,0 +1,277 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { ReduxStateReader } from '../../utils/ReduxStateReader'
|
||||
import { McpServerMigrator } from '../McpServerMigrator'
|
||||
|
||||
function createMockContext(reduxData: Record<string, unknown> = {}) {
|
||||
const reduxState = new ReduxStateReader(reduxData)
|
||||
|
||||
return {
|
||||
sources: {
|
||||
electronStore: { get: vi.fn() },
|
||||
reduxState,
|
||||
dexieExport: { readTable: vi.fn(), createStreamReader: vi.fn(), tableExists: vi.fn() },
|
||||
dexieSettings: { keys: vi.fn().mockReturnValue([]), get: vi.fn() }
|
||||
},
|
||||
db: {
|
||||
transaction: vi.fn(async (fn: (tx: any) => Promise<void>) => {
|
||||
const tx = {
|
||||
insert: vi.fn().mockReturnValue({
|
||||
values: vi.fn().mockResolvedValue(undefined)
|
||||
})
|
||||
}
|
||||
await fn(tx)
|
||||
return tx
|
||||
}),
|
||||
select: vi.fn().mockReturnValue({
|
||||
from: vi.fn().mockReturnValue({
|
||||
get: vi.fn().mockResolvedValue({ count: 0 })
|
||||
})
|
||||
})
|
||||
},
|
||||
sharedData: new Map(),
|
||||
logger: {
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
debug: vi.fn()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const SAMPLE_SERVERS = [
|
||||
{
|
||||
id: 'srv-1',
|
||||
name: '@cherry/fetch',
|
||||
type: 'inMemory',
|
||||
isActive: true,
|
||||
provider: 'CherryAI',
|
||||
installSource: 'builtin',
|
||||
isTrusted: true
|
||||
},
|
||||
{
|
||||
id: 'srv-2',
|
||||
name: 'custom-server',
|
||||
type: 'stdio',
|
||||
command: 'npx',
|
||||
args: ['-y', 'my-mcp-server'],
|
||||
env: { API_KEY: 'test' },
|
||||
isActive: false,
|
||||
installSource: 'manual'
|
||||
},
|
||||
{
|
||||
id: 'srv-3',
|
||||
name: 'sse-server',
|
||||
type: 'sse',
|
||||
baseUrl: 'http://localhost:8080',
|
||||
isActive: true,
|
||||
installSource: 'protocol'
|
||||
}
|
||||
]
|
||||
|
||||
describe('McpServerMigrator', () => {
|
||||
let migrator: McpServerMigrator
|
||||
|
||||
beforeEach(() => {
|
||||
migrator = new McpServerMigrator()
|
||||
migrator.setProgressCallback(vi.fn())
|
||||
})
|
||||
|
||||
it('should have correct metadata', () => {
|
||||
expect(migrator.id).toBe('mcp_server')
|
||||
expect(migrator.name).toBe('MCP Server')
|
||||
expect(migrator.order).toBe(1.5)
|
||||
})
|
||||
|
||||
describe('prepare', () => {
|
||||
it('should count source servers', async () => {
|
||||
const ctx = createMockContext({ mcp: { servers: SAMPLE_SERVERS } })
|
||||
const result = await migrator.prepare(ctx as any)
|
||||
expect(result).toStrictEqual({ success: true, itemCount: 3, warnings: undefined })
|
||||
})
|
||||
|
||||
it('should handle empty servers array', async () => {
|
||||
const ctx = createMockContext({ mcp: { servers: [] } })
|
||||
const result = await migrator.prepare(ctx as any)
|
||||
expect(result).toStrictEqual({ success: true, itemCount: 0, warnings: undefined })
|
||||
})
|
||||
|
||||
it('should handle missing mcp category', async () => {
|
||||
const ctx = createMockContext({})
|
||||
const result = await migrator.prepare(ctx as any)
|
||||
expect(result).toStrictEqual({ success: true, itemCount: 0, warnings: undefined })
|
||||
})
|
||||
|
||||
it('should handle missing servers key', async () => {
|
||||
const ctx = createMockContext({ mcp: {} })
|
||||
const result = await migrator.prepare(ctx as any)
|
||||
expect(result).toStrictEqual({ success: true, itemCount: 0, warnings: undefined })
|
||||
})
|
||||
|
||||
it('should handle non-array servers value', async () => {
|
||||
const ctx = createMockContext({ mcp: { servers: 'not-an-array' } })
|
||||
const result = await migrator.prepare(ctx as any)
|
||||
expect(result).toStrictEqual({
|
||||
success: true,
|
||||
itemCount: 0,
|
||||
warnings: ['mcp.servers is not an array']
|
||||
})
|
||||
})
|
||||
|
||||
it('should fail when all servers are skipped', async () => {
|
||||
const servers = [
|
||||
{ name: 'no-id-1', isActive: true },
|
||||
{ name: 'no-id-2', isActive: false }
|
||||
]
|
||||
const ctx = createMockContext({ mcp: { servers } })
|
||||
const result = await migrator.prepare(ctx as any)
|
||||
expect(result.success).toBe(false)
|
||||
expect(result.itemCount).toBe(0)
|
||||
})
|
||||
|
||||
it('should filter out servers without id', async () => {
|
||||
const servers = [
|
||||
{ id: 'srv-1', name: 'valid', isActive: true },
|
||||
{ name: 'no-id', isActive: false },
|
||||
{ id: '', name: 'empty-id', isActive: false }
|
||||
]
|
||||
const ctx = createMockContext({ mcp: { servers } })
|
||||
const result = await migrator.prepare(ctx as any)
|
||||
expect(result).toStrictEqual({
|
||||
success: true,
|
||||
itemCount: 1,
|
||||
warnings: ['Skipped server without valid id: no-id', 'Skipped server without valid id: empty-id']
|
||||
})
|
||||
})
|
||||
|
||||
it('should deduplicate servers by id', async () => {
|
||||
const servers = [
|
||||
{ id: 'dup-1', name: 'first', isActive: true },
|
||||
{ id: 'dup-1', name: 'duplicate', isActive: false },
|
||||
{ id: 'srv-2', name: 'unique', isActive: true }
|
||||
]
|
||||
const ctx = createMockContext({ mcp: { servers } })
|
||||
const result = await migrator.prepare(ctx as any)
|
||||
expect(result).toStrictEqual({
|
||||
success: true,
|
||||
itemCount: 2,
|
||||
warnings: ['Skipped duplicate server id: dup-1']
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('execute', () => {
|
||||
it('should insert servers into database', async () => {
|
||||
const ctx = createMockContext({ mcp: { servers: SAMPLE_SERVERS } })
|
||||
await migrator.prepare(ctx as any)
|
||||
const result = await migrator.execute(ctx as any)
|
||||
expect(result).toStrictEqual({ success: true, processedCount: 3 })
|
||||
expect(ctx.db.transaction).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle empty servers gracefully', async () => {
|
||||
const ctx = createMockContext({ mcp: { servers: [] } })
|
||||
await migrator.prepare(ctx as any)
|
||||
const result = await migrator.execute(ctx as any)
|
||||
expect(result).toStrictEqual({ success: true, processedCount: 0 })
|
||||
})
|
||||
|
||||
it('should return failure when transaction throws', async () => {
|
||||
const ctx = createMockContext({ mcp: { servers: SAMPLE_SERVERS } })
|
||||
ctx.db.transaction = vi.fn().mockRejectedValue(new Error('SQLITE_CONSTRAINT'))
|
||||
await migrator.prepare(ctx as any)
|
||||
const result = await migrator.execute(ctx as any)
|
||||
expect(result.success).toBe(false)
|
||||
expect(result.error).toContain('SQLITE_CONSTRAINT')
|
||||
expect(result.processedCount).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('validate', () => {
|
||||
function mockValidateDb(ctx: ReturnType<typeof createMockContext>, count: number, sample: any[] = []) {
|
||||
ctx.db.select = vi.fn().mockImplementation((arg) => {
|
||||
if (arg) {
|
||||
// count query: select({ count: ... }).from().get()
|
||||
return {
|
||||
from: vi.fn().mockReturnValue({
|
||||
get: vi.fn().mockResolvedValue({ count })
|
||||
})
|
||||
}
|
||||
}
|
||||
// sample query: select().from().limit().all()
|
||||
return {
|
||||
from: vi.fn().mockReturnValue({
|
||||
limit: vi.fn().mockReturnValue({
|
||||
all: vi.fn().mockResolvedValue(sample)
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
it('should pass when counts match and sample is valid', async () => {
|
||||
const ctx = createMockContext({ mcp: { servers: SAMPLE_SERVERS } })
|
||||
const sampleRows = SAMPLE_SERVERS.map((s) => ({ id: s.id, name: s.name }))
|
||||
mockValidateDb(ctx, 3, sampleRows)
|
||||
|
||||
await migrator.prepare(ctx as any)
|
||||
const result = await migrator.validate(ctx as any)
|
||||
expect(result).toStrictEqual({
|
||||
success: true,
|
||||
errors: [],
|
||||
stats: { sourceCount: 3, targetCount: 3, skippedCount: 0 }
|
||||
})
|
||||
})
|
||||
|
||||
it('should fail when sample has missing required fields', async () => {
|
||||
const ctx = createMockContext({ mcp: { servers: SAMPLE_SERVERS } })
|
||||
mockValidateDb(ctx, 3, [
|
||||
{ id: '', name: 'test' },
|
||||
{ id: 'srv-2', name: '' }
|
||||
])
|
||||
|
||||
await migrator.prepare(ctx as any)
|
||||
const result = await migrator.validate(ctx as any)
|
||||
expect(result.success).toBe(false)
|
||||
expect(result.errors).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('should pass with zero items', async () => {
|
||||
const ctx = createMockContext({})
|
||||
mockValidateDb(ctx, 0, [])
|
||||
|
||||
await migrator.prepare(ctx as any)
|
||||
const result = await migrator.validate(ctx as any)
|
||||
expect(result).toStrictEqual({
|
||||
success: true,
|
||||
errors: [],
|
||||
stats: { sourceCount: 0, targetCount: 0, skippedCount: 0 }
|
||||
})
|
||||
})
|
||||
|
||||
it('should fail on count mismatch', async () => {
|
||||
const ctx = createMockContext({ mcp: { servers: SAMPLE_SERVERS } })
|
||||
mockValidateDb(ctx, 2, [
|
||||
{ id: 'srv-1', name: 'test1' },
|
||||
{ id: 'srv-2', name: 'test2' }
|
||||
])
|
||||
|
||||
await migrator.prepare(ctx as any)
|
||||
const result = await migrator.validate(ctx as any)
|
||||
expect(result.success).toBe(false)
|
||||
expect(result.errors).toContainEqual(expect.objectContaining({ key: 'count_mismatch' }))
|
||||
})
|
||||
|
||||
it('should return failure when db throws', async () => {
|
||||
const ctx = createMockContext({ mcp: { servers: SAMPLE_SERVERS } })
|
||||
ctx.db.select = vi.fn().mockImplementation(() => {
|
||||
throw new Error('DB_CORRUPT')
|
||||
})
|
||||
|
||||
await migrator.prepare(ctx as any)
|
||||
const result = await migrator.validate(ctx as any)
|
||||
expect(result.success).toBe(false)
|
||||
expect(result.errors[0].message).toContain('DB_CORRUPT')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -8,14 +8,21 @@ export { BaseMigrator } from './BaseMigrator'
|
||||
import { AssistantMigrator } from './AssistantMigrator'
|
||||
import { ChatMigrator } from './ChatMigrator'
|
||||
import { KnowledgeMigrator } from './KnowledgeMigrator'
|
||||
import { McpServerMigrator } from './McpServerMigrator'
|
||||
import { PreferencesMigrator } from './PreferencesMigrator'
|
||||
|
||||
// Export migrator classes
|
||||
export { AssistantMigrator, ChatMigrator, KnowledgeMigrator, PreferencesMigrator }
|
||||
export { AssistantMigrator, ChatMigrator, KnowledgeMigrator, McpServerMigrator, PreferencesMigrator }
|
||||
|
||||
/**
|
||||
* Get all registered migrators in execution order
|
||||
*/
|
||||
export function getAllMigrators() {
|
||||
return [new PreferencesMigrator(), new AssistantMigrator(), new KnowledgeMigrator(), new ChatMigrator()]
|
||||
return [
|
||||
new PreferencesMigrator(),
|
||||
new McpServerMigrator(),
|
||||
new AssistantMigrator(),
|
||||
new KnowledgeMigrator(),
|
||||
new ChatMigrator()
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* MCP Server migration mappings and transform functions
|
||||
*
|
||||
* Transforms legacy Redux MCPServer objects to SQLite mcp_server table rows.
|
||||
*/
|
||||
|
||||
import type { McpServerInsert } from '@data/db/schemas/mcpServer'
|
||||
|
||||
function toNullable<T>(value: unknown): T | null {
|
||||
return (value ?? null) as T | null
|
||||
}
|
||||
|
||||
function toRequired<T>(value: unknown, fallback: T): T {
|
||||
return (value ?? fallback) as T
|
||||
}
|
||||
|
||||
export function transformMcpServer(source: Record<string, unknown>): McpServerInsert {
|
||||
return {
|
||||
id: toRequired<string>(source.id, ''),
|
||||
name: toRequired<string>(source.name, ''),
|
||||
type: toNullable(source.type),
|
||||
description: toNullable(source.description),
|
||||
baseUrl: toNullable(source.baseUrl ?? source.url),
|
||||
command: toNullable(source.command),
|
||||
registryUrl: toNullable(source.registryUrl),
|
||||
args: toNullable(source.args),
|
||||
env: toNullable(source.env),
|
||||
headers: toNullable(source.headers),
|
||||
provider: toNullable(source.provider),
|
||||
providerUrl: toNullable(source.providerUrl),
|
||||
logoUrl: toNullable(source.logoUrl),
|
||||
tags: toNullable(source.tags),
|
||||
longRunning: toNullable(source.longRunning),
|
||||
timeout: toNullable(source.timeout),
|
||||
dxtVersion: toNullable(source.dxtVersion),
|
||||
dxtPath: toNullable(source.dxtPath),
|
||||
reference: toNullable(source.reference),
|
||||
searchKey: toNullable(source.searchKey),
|
||||
configSample: toNullable(source.configSample),
|
||||
disabledTools: toNullable(source.disabledTools),
|
||||
disabledAutoApproveTools: toNullable(source.disabledAutoApproveTools),
|
||||
shouldConfig: toNullable(source.shouldConfig),
|
||||
isActive: toRequired(source.isActive, false),
|
||||
installSource: toNullable(source.installSource),
|
||||
isTrusted: toNullable(source.isTrusted),
|
||||
trustedAt: toNullable(source.trustedAt),
|
||||
installedAt: toNullable(source.installedAt)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { transformMcpServer } from '../McpServerMappings'
|
||||
|
||||
const NULL_FIELDS = {
|
||||
type: null,
|
||||
description: null,
|
||||
baseUrl: null,
|
||||
command: null,
|
||||
registryUrl: null,
|
||||
args: null,
|
||||
env: null,
|
||||
headers: null,
|
||||
provider: null,
|
||||
providerUrl: null,
|
||||
logoUrl: null,
|
||||
tags: null,
|
||||
longRunning: null,
|
||||
timeout: null,
|
||||
dxtVersion: null,
|
||||
dxtPath: null,
|
||||
reference: null,
|
||||
searchKey: null,
|
||||
configSample: null,
|
||||
disabledTools: null,
|
||||
disabledAutoApproveTools: null,
|
||||
shouldConfig: null,
|
||||
installSource: null,
|
||||
isTrusted: null,
|
||||
trustedAt: null,
|
||||
installedAt: null
|
||||
}
|
||||
|
||||
describe('McpServerMappings', () => {
|
||||
describe('transformMcpServer', () => {
|
||||
it('should transform a full MCPServer record', () => {
|
||||
const source = {
|
||||
id: 'srv-1',
|
||||
name: '@cherry/fetch',
|
||||
type: 'inMemory',
|
||||
description: 'Fetch tool',
|
||||
baseUrl: 'http://localhost:3000',
|
||||
command: 'npx',
|
||||
registryUrl: 'https://registry.example.com',
|
||||
args: ['-y', 'some-package'],
|
||||
env: { API_KEY: 'key123' },
|
||||
headers: { Authorization: 'Bearer token' },
|
||||
provider: 'CherryAI',
|
||||
providerUrl: 'https://cherry.ai',
|
||||
logoUrl: 'https://cherry.ai/logo.png',
|
||||
tags: ['search', 'web'],
|
||||
longRunning: true,
|
||||
timeout: 120,
|
||||
dxtVersion: '1.0.0',
|
||||
dxtPath: '/path/to/dxt',
|
||||
reference: 'https://docs.example.com',
|
||||
searchKey: 'fetch-tool',
|
||||
configSample: { command: 'npx', args: ['-y', 'some-package'], env: { API_KEY: 'key123' } },
|
||||
disabledTools: ['tool1'],
|
||||
disabledAutoApproveTools: ['tool2'],
|
||||
shouldConfig: true,
|
||||
isActive: true,
|
||||
installSource: 'builtin',
|
||||
isTrusted: true,
|
||||
trustedAt: 1700000000000,
|
||||
installedAt: 1699000000000
|
||||
}
|
||||
|
||||
expect(transformMcpServer(source)).toStrictEqual(source)
|
||||
})
|
||||
|
||||
it('should handle minimal MCPServer (only required fields)', () => {
|
||||
expect(transformMcpServer({ id: 'srv-2', name: 'my-server', isActive: false })).toStrictEqual({
|
||||
...NULL_FIELDS,
|
||||
id: 'srv-2',
|
||||
name: 'my-server',
|
||||
isActive: false
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle null and undefined optional fields', () => {
|
||||
const source = {
|
||||
id: 'srv-3',
|
||||
name: 'test',
|
||||
isActive: true,
|
||||
type: undefined,
|
||||
description: null,
|
||||
args: undefined,
|
||||
env: null
|
||||
}
|
||||
expect(transformMcpServer(source as any)).toStrictEqual({
|
||||
...NULL_FIELDS,
|
||||
id: 'srv-3',
|
||||
name: 'test',
|
||||
isActive: true
|
||||
})
|
||||
})
|
||||
|
||||
it('should default isActive to false when missing', () => {
|
||||
expect(transformMcpServer({ id: 'srv-4', name: 'no-active-field' } as any)).toStrictEqual({
|
||||
...NULL_FIELDS,
|
||||
id: 'srv-4',
|
||||
name: 'no-active-field',
|
||||
isActive: false
|
||||
})
|
||||
})
|
||||
|
||||
it('should preserve empty arrays', () => {
|
||||
expect(
|
||||
transformMcpServer({
|
||||
id: 'srv-5',
|
||||
name: 'empty-arrays',
|
||||
isActive: false,
|
||||
args: [],
|
||||
tags: [],
|
||||
disabledTools: []
|
||||
})
|
||||
).toStrictEqual({
|
||||
...NULL_FIELDS,
|
||||
id: 'srv-5',
|
||||
name: 'empty-arrays',
|
||||
isActive: false,
|
||||
args: [],
|
||||
tags: [],
|
||||
disabledTools: []
|
||||
})
|
||||
})
|
||||
|
||||
it('should fall back from url to baseUrl for SSE servers', () => {
|
||||
const result = transformMcpServer({
|
||||
id: 'sse-1',
|
||||
name: 'sse-server',
|
||||
isActive: true,
|
||||
url: 'http://localhost:8080/sse'
|
||||
})
|
||||
expect(result.baseUrl).toBe('http://localhost:8080/sse')
|
||||
})
|
||||
|
||||
it('should prefer baseUrl over url when both present', () => {
|
||||
const result = transformMcpServer({
|
||||
id: 'sse-2',
|
||||
name: 'sse-server',
|
||||
isActive: true,
|
||||
baseUrl: 'http://primary:8080',
|
||||
url: 'http://fallback:8080'
|
||||
})
|
||||
expect(result.baseUrl).toBe('http://primary:8080')
|
||||
})
|
||||
|
||||
it('should preserve empty objects', () => {
|
||||
expect(
|
||||
transformMcpServer({ id: 'srv-6', name: 'empty-objects', isActive: false, env: {}, headers: {} })
|
||||
).toStrictEqual({
|
||||
...NULL_FIELDS,
|
||||
id: 'srv-6',
|
||||
name: 'empty-objects',
|
||||
isActive: false,
|
||||
env: {},
|
||||
headers: {}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -5,14 +5,14 @@ import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
|
||||
import ModelSelector from '@renderer/components/ModelSelector'
|
||||
import { isMac, isWin } from '@renderer/config/constant'
|
||||
import { isEmbeddingModel, isRerankModel, isTextToImageModel } from '@renderer/config/models'
|
||||
import { usePersistCache } from '@renderer/data/hooks/useCache'
|
||||
import { useCodeTools } from '@renderer/hooks/useCodeTools'
|
||||
import { useProviders } from '@renderer/hooks/useProvider'
|
||||
import { useTimer } from '@renderer/hooks/useTimer'
|
||||
import { getAssistantSettings, getProviderByModel } from '@renderer/services/AssistantService'
|
||||
import { loggerService } from '@renderer/services/LoggerService'
|
||||
import { getModelUniqId } from '@renderer/services/ModelService'
|
||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import { setIsBunInstalled } from '@renderer/store/mcp'
|
||||
import { useAppSelector } from '@renderer/store'
|
||||
import type { EndpointType, Model } from '@renderer/types'
|
||||
import type { TerminalConfig } from '@shared/config/constant'
|
||||
import { codeTools, terminalApps } from '@shared/config/constant'
|
||||
@@ -38,8 +38,7 @@ const logger = loggerService.withContext('CodeToolsPage')
|
||||
const CodeToolsPage: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { providers } = useProviders()
|
||||
const dispatch = useAppDispatch()
|
||||
const isBunInstalled = useAppSelector((state) => state.mcp.isBunInstalled)
|
||||
const [isBunInstalled, setIsBunInstalled] = usePersistCache('feature.mcp.is_bun_installed')
|
||||
const {
|
||||
selectedCliTool,
|
||||
selectedModel,
|
||||
@@ -181,12 +180,12 @@ const CodeToolsPage: FC = () => {
|
||||
const checkBunInstallation = useCallback(async () => {
|
||||
try {
|
||||
const bunExists = await window.api.isBinaryExist('bun')
|
||||
dispatch(setIsBunInstalled(bunExists))
|
||||
setIsBunInstalled(bunExists)
|
||||
} catch (error) {
|
||||
logger.error('Failed to check bun installation status:', error as Error)
|
||||
dispatch(setIsBunInstalled(false))
|
||||
// IPC failure — leave previous value unchanged
|
||||
}
|
||||
}, [dispatch])
|
||||
}, [setIsBunInstalled])
|
||||
|
||||
// 获取可用终端
|
||||
const loadAvailableTerminals = useCallback(async () => {
|
||||
@@ -213,7 +212,7 @@ const CodeToolsPage: FC = () => {
|
||||
try {
|
||||
setIsInstallingBun(true)
|
||||
await window.api.installBunBinary()
|
||||
dispatch(setIsBunInstalled(true))
|
||||
setIsBunInstalled(true)
|
||||
window.toast.success(t('settings.mcp.installSuccess'))
|
||||
} catch (error: any) {
|
||||
logger.error('Failed to install bun:', error as Error)
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { CheckCircleOutlined, QuestionCircleOutlined, WarningOutlined } from '@ant-design/icons'
|
||||
import { Center, ColFlex } from '@cherrystudio/ui'
|
||||
import { Button } from '@cherrystudio/ui'
|
||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import { setIsBunInstalled, setIsUvInstalled } from '@renderer/store/mcp'
|
||||
import { usePersistCache } from '@renderer/data/hooks/useCache'
|
||||
import { useNavigate } from '@tanstack/react-router'
|
||||
import { Alert } from 'antd'
|
||||
import type { FC } from 'react'
|
||||
@@ -17,9 +16,8 @@ interface Props {
|
||||
}
|
||||
|
||||
const InstallNpxUv: FC<Props> = ({ mini = false }) => {
|
||||
const dispatch = useAppDispatch()
|
||||
const isUvInstalled = useAppSelector((state) => state.mcp.isUvInstalled)
|
||||
const isBunInstalled = useAppSelector((state) => state.mcp.isBunInstalled)
|
||||
const [isUvInstalled, setIsUvInstalled] = usePersistCache('feature.mcp.is_uv_installed')
|
||||
const [isBunInstalled, setIsBunInstalled] = usePersistCache('feature.mcp.is_bun_installed')
|
||||
|
||||
const [isInstallingUv, setIsInstallingUv] = useState(false)
|
||||
const [isInstallingBun, setIsInstallingBun] = useState(false)
|
||||
@@ -38,23 +36,27 @@ const InstallNpxUv: FC<Props> = ({ mini = false }) => {
|
||||
}, [])
|
||||
|
||||
const checkBinaries = useCallback(async () => {
|
||||
const uvExists = await window.api.isBinaryExist('uv')
|
||||
const bunExists = await window.api.isBinaryExist('bun')
|
||||
const { uvPath, bunPath, dir } = await window.api.mcp.getInstallInfo()
|
||||
try {
|
||||
const uvExists = await window.api.isBinaryExist('uv')
|
||||
const bunExists = await window.api.isBinaryExist('bun')
|
||||
const { uvPath, bunPath, dir } = await window.api.mcp.getInstallInfo()
|
||||
|
||||
dispatch(setIsUvInstalled(uvExists))
|
||||
dispatch(setIsBunInstalled(bunExists))
|
||||
setUvPath(uvPath)
|
||||
setBunPath(bunPath)
|
||||
setBinariesDir(dir)
|
||||
}, [dispatch])
|
||||
setIsUvInstalled(uvExists)
|
||||
setIsBunInstalled(bunExists)
|
||||
setUvPath(uvPath)
|
||||
setBunPath(bunPath)
|
||||
setBinariesDir(dir)
|
||||
} catch {
|
||||
// IPC failure — leave previous values unchanged
|
||||
}
|
||||
}, [setIsUvInstalled, setIsBunInstalled])
|
||||
|
||||
const installUV = async () => {
|
||||
try {
|
||||
setIsInstallingUv(true)
|
||||
await window.api.installUVBinary()
|
||||
setIsInstallingUv(false)
|
||||
dispatch(setIsUvInstalled(true))
|
||||
setIsUvInstalled(true)
|
||||
} catch (error: any) {
|
||||
window.toast.error(`${t('settings.mcp.installError')}: ${error.message}`)
|
||||
setIsInstallingUv(false)
|
||||
@@ -68,7 +70,7 @@ const InstallNpxUv: FC<Props> = ({ mini = false }) => {
|
||||
setIsInstallingBun(true)
|
||||
await window.api.installBunBinary()
|
||||
setIsInstallingBun(false)
|
||||
dispatch(setIsBunInstalled(true))
|
||||
setIsBunInstalled(true)
|
||||
} catch (error: any) {
|
||||
window.toast.error(`${t('settings.mcp.installError')}: ${error.message}`)
|
||||
setIsInstallingBun(false)
|
||||
|
||||
@@ -11,6 +11,7 @@ const SLICES_TO_EXPORT = [
|
||||
'assistants', // Assistant configurations
|
||||
'knowledge', // Knowledge base metadata
|
||||
'llm', // LLM provider and model configurations
|
||||
'mcp', // MCP server configurations
|
||||
'note', // Note-related settings
|
||||
'selectionStore', // Selection assistant settings
|
||||
'websearch' // Web search configurations
|
||||
|
||||
Reference in New Issue
Block a user