mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-07-10 23:11:40 +08:00
✨ feat(ai-mcp): 补全结构探针并优化客户端接入体验
- 新增 get_indexes、get_foreign_keys、get_triggers 内置工具与 MCP Server 对应实现 - 拆分 AI 设置中的 MCP 接入面板和服务卡片,补充参数提示与客户端状态展示 - 补齐前后端测试与真实页面验证,降低 AI 设置区域的臃肿度
This commit is contained in:
@@ -19,6 +19,9 @@ type Backend interface {
|
||||
DBGetDatabases(config connection.ConnectionConfig) connection.QueryResult
|
||||
DBGetTables(config connection.ConnectionConfig, dbName string) connection.QueryResult
|
||||
DBGetColumns(config connection.ConnectionConfig, dbName string, tableName string) connection.QueryResult
|
||||
DBGetIndexes(config connection.ConnectionConfig, dbName string, tableName string) connection.QueryResult
|
||||
DBGetForeignKeys(config connection.ConnectionConfig, dbName string, tableName string) connection.QueryResult
|
||||
DBGetTriggers(config connection.ConnectionConfig, dbName string, tableName string) connection.QueryResult
|
||||
DBShowCreateTable(config connection.ConnectionConfig, dbName string, tableName string) connection.QueryResult
|
||||
DBQueryMulti(config connection.ConnectionConfig, dbName string, query string, queryID string) connection.QueryResult
|
||||
InspectSQL(dbType string, sql string) appcore.SQLInspection
|
||||
@@ -70,6 +73,18 @@ func (b *AppBackend) DBGetColumns(config connection.ConnectionConfig, dbName str
|
||||
return b.app.DBGetColumns(config, dbName, tableName)
|
||||
}
|
||||
|
||||
func (b *AppBackend) DBGetIndexes(config connection.ConnectionConfig, dbName string, tableName string) connection.QueryResult {
|
||||
return b.app.DBGetIndexes(config, dbName, tableName)
|
||||
}
|
||||
|
||||
func (b *AppBackend) DBGetForeignKeys(config connection.ConnectionConfig, dbName string, tableName string) connection.QueryResult {
|
||||
return b.app.DBGetForeignKeys(config, dbName, tableName)
|
||||
}
|
||||
|
||||
func (b *AppBackend) DBGetTriggers(config connection.ConnectionConfig, dbName string, tableName string) connection.QueryResult {
|
||||
return b.app.DBGetTriggers(config, dbName, tableName)
|
||||
}
|
||||
|
||||
func (b *AppBackend) DBShowCreateTable(config connection.ConnectionConfig, dbName string, tableName string) connection.QueryResult {
|
||||
return b.app.DBShowCreateTable(config, dbName, tableName)
|
||||
}
|
||||
|
||||
@@ -35,6 +35,21 @@ func NewServer(backend Backend) *mcp.Server {
|
||||
Description: "根据 connectionId、可选 dbName、tableName 获取字段定义。",
|
||||
}, service.GetColumns)
|
||||
|
||||
mcp.AddTool(server, &mcp.Tool{
|
||||
Name: "get_indexes",
|
||||
Description: "根据 connectionId、可选 dbName、tableName 获取索引定义。",
|
||||
}, service.GetIndexes)
|
||||
|
||||
mcp.AddTool(server, &mcp.Tool{
|
||||
Name: "get_foreign_keys",
|
||||
Description: "根据 connectionId、可选 dbName、tableName 获取外键关系。",
|
||||
}, service.GetForeignKeys)
|
||||
|
||||
mcp.AddTool(server, &mcp.Tool{
|
||||
Name: "get_triggers",
|
||||
Description: "根据 connectionId、可选 dbName、tableName 获取触发器定义。",
|
||||
}, service.GetTriggers)
|
||||
|
||||
mcp.AddTool(server, &mcp.Tool{
|
||||
Name: "get_table_ddl",
|
||||
Description: "根据 connectionId、可选 dbName、tableName 获取建表或建视图语句。",
|
||||
|
||||
@@ -88,6 +88,27 @@ type getColumnsResult struct {
|
||||
Columns []connection.ColumnDefinition `json:"columns"`
|
||||
}
|
||||
|
||||
type getIndexesResult struct {
|
||||
ConnectionID string `json:"connectionId"`
|
||||
DBName string `json:"dbName,omitempty"`
|
||||
TableName string `json:"tableName"`
|
||||
Indexes []connection.IndexDefinition `json:"indexes"`
|
||||
}
|
||||
|
||||
type getForeignKeysResult struct {
|
||||
ConnectionID string `json:"connectionId"`
|
||||
DBName string `json:"dbName,omitempty"`
|
||||
TableName string `json:"tableName"`
|
||||
ForeignKeys []connection.ForeignKeyDefinition `json:"foreignKeys"`
|
||||
}
|
||||
|
||||
type getTriggersResult struct {
|
||||
ConnectionID string `json:"connectionId"`
|
||||
DBName string `json:"dbName,omitempty"`
|
||||
TableName string `json:"tableName"`
|
||||
Triggers []connection.TriggerDefinition `json:"triggers"`
|
||||
}
|
||||
|
||||
type getTableDDLResult struct {
|
||||
ConnectionID string `json:"connectionId"`
|
||||
DBName string `json:"dbName,omitempty"`
|
||||
@@ -241,6 +262,105 @@ func (s *Service) GetColumns(ctx context.Context, req *mcp.CallToolRequest, args
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Service) GetIndexes(ctx context.Context, req *mcp.CallToolRequest, args tableArgs) (*mcp.CallToolResult, getIndexesResult, error) {
|
||||
_ = ctx
|
||||
_ = req
|
||||
|
||||
view, errResult := s.resolveConnection(args.ConnectionID)
|
||||
if errResult != nil {
|
||||
return errResult, getIndexesResult{}, nil
|
||||
}
|
||||
|
||||
tableName := strings.TrimSpace(args.TableName)
|
||||
if tableName == "" {
|
||||
return toolError("tableName 不能为空"), getIndexesResult{}, nil
|
||||
}
|
||||
|
||||
dbName := effectiveDBName(args.DBName, view.Config)
|
||||
queryResult := s.backend.DBGetIndexes(view.Config, dbName, tableName)
|
||||
if !queryResult.Success {
|
||||
return toolError("获取索引定义失败: %s", strings.TrimSpace(queryResult.Message)), getIndexesResult{}, nil
|
||||
}
|
||||
|
||||
indexes, err := decodeIndexes(queryResult.Data)
|
||||
if err != nil {
|
||||
return toolError("解析索引定义失败: %v", err), getIndexesResult{}, nil
|
||||
}
|
||||
|
||||
return successResult(), getIndexesResult{
|
||||
ConnectionID: view.ID,
|
||||
DBName: dbName,
|
||||
TableName: tableName,
|
||||
Indexes: ensureNonNilIndexes(indexes),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Service) GetForeignKeys(ctx context.Context, req *mcp.CallToolRequest, args tableArgs) (*mcp.CallToolResult, getForeignKeysResult, error) {
|
||||
_ = ctx
|
||||
_ = req
|
||||
|
||||
view, errResult := s.resolveConnection(args.ConnectionID)
|
||||
if errResult != nil {
|
||||
return errResult, getForeignKeysResult{}, nil
|
||||
}
|
||||
|
||||
tableName := strings.TrimSpace(args.TableName)
|
||||
if tableName == "" {
|
||||
return toolError("tableName 不能为空"), getForeignKeysResult{}, nil
|
||||
}
|
||||
|
||||
dbName := effectiveDBName(args.DBName, view.Config)
|
||||
queryResult := s.backend.DBGetForeignKeys(view.Config, dbName, tableName)
|
||||
if !queryResult.Success {
|
||||
return toolError("获取外键关系失败: %s", strings.TrimSpace(queryResult.Message)), getForeignKeysResult{}, nil
|
||||
}
|
||||
|
||||
foreignKeys, err := decodeForeignKeys(queryResult.Data)
|
||||
if err != nil {
|
||||
return toolError("解析外键关系失败: %v", err), getForeignKeysResult{}, nil
|
||||
}
|
||||
|
||||
return successResult(), getForeignKeysResult{
|
||||
ConnectionID: view.ID,
|
||||
DBName: dbName,
|
||||
TableName: tableName,
|
||||
ForeignKeys: ensureNonNilForeignKeys(foreignKeys),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Service) GetTriggers(ctx context.Context, req *mcp.CallToolRequest, args tableArgs) (*mcp.CallToolResult, getTriggersResult, error) {
|
||||
_ = ctx
|
||||
_ = req
|
||||
|
||||
view, errResult := s.resolveConnection(args.ConnectionID)
|
||||
if errResult != nil {
|
||||
return errResult, getTriggersResult{}, nil
|
||||
}
|
||||
|
||||
tableName := strings.TrimSpace(args.TableName)
|
||||
if tableName == "" {
|
||||
return toolError("tableName 不能为空"), getTriggersResult{}, nil
|
||||
}
|
||||
|
||||
dbName := effectiveDBName(args.DBName, view.Config)
|
||||
queryResult := s.backend.DBGetTriggers(view.Config, dbName, tableName)
|
||||
if !queryResult.Success {
|
||||
return toolError("获取触发器定义失败: %s", strings.TrimSpace(queryResult.Message)), getTriggersResult{}, nil
|
||||
}
|
||||
|
||||
triggers, err := decodeTriggers(queryResult.Data)
|
||||
if err != nil {
|
||||
return toolError("解析触发器定义失败: %v", err), getTriggersResult{}, nil
|
||||
}
|
||||
|
||||
return successResult(), getTriggersResult{
|
||||
ConnectionID: view.ID,
|
||||
DBName: dbName,
|
||||
TableName: tableName,
|
||||
Triggers: ensureNonNilTriggers(triggers),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Service) GetTableDDL(ctx context.Context, req *mcp.CallToolRequest, args tableArgs) (*mcp.CallToolResult, getTableDDLResult, error) {
|
||||
_ = ctx
|
||||
_ = req
|
||||
@@ -457,6 +577,51 @@ func decodeColumns(data interface{}) ([]connection.ColumnDefinition, error) {
|
||||
}
|
||||
}
|
||||
|
||||
func decodeIndexes(data interface{}) ([]connection.IndexDefinition, error) {
|
||||
switch indexes := data.(type) {
|
||||
case nil:
|
||||
return []connection.IndexDefinition{}, nil
|
||||
case []connection.IndexDefinition:
|
||||
return ensureNonNilIndexes(append([]connection.IndexDefinition(nil), indexes...)), nil
|
||||
default:
|
||||
var decoded []connection.IndexDefinition
|
||||
if err := remarshal(data, &decoded); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ensureNonNilIndexes(decoded), nil
|
||||
}
|
||||
}
|
||||
|
||||
func decodeForeignKeys(data interface{}) ([]connection.ForeignKeyDefinition, error) {
|
||||
switch foreignKeys := data.(type) {
|
||||
case nil:
|
||||
return []connection.ForeignKeyDefinition{}, nil
|
||||
case []connection.ForeignKeyDefinition:
|
||||
return ensureNonNilForeignKeys(append([]connection.ForeignKeyDefinition(nil), foreignKeys...)), nil
|
||||
default:
|
||||
var decoded []connection.ForeignKeyDefinition
|
||||
if err := remarshal(data, &decoded); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ensureNonNilForeignKeys(decoded), nil
|
||||
}
|
||||
}
|
||||
|
||||
func decodeTriggers(data interface{}) ([]connection.TriggerDefinition, error) {
|
||||
switch triggers := data.(type) {
|
||||
case nil:
|
||||
return []connection.TriggerDefinition{}, nil
|
||||
case []connection.TriggerDefinition:
|
||||
return ensureNonNilTriggers(append([]connection.TriggerDefinition(nil), triggers...)), nil
|
||||
default:
|
||||
var decoded []connection.TriggerDefinition
|
||||
if err := remarshal(data, &decoded); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ensureNonNilTriggers(decoded), nil
|
||||
}
|
||||
}
|
||||
|
||||
func decodeString(data interface{}) (string, error) {
|
||||
switch value := data.(type) {
|
||||
case nil:
|
||||
@@ -551,6 +716,27 @@ func ensureNonNilColumns(items []connection.ColumnDefinition) []connection.Colum
|
||||
return items
|
||||
}
|
||||
|
||||
func ensureNonNilIndexes(items []connection.IndexDefinition) []connection.IndexDefinition {
|
||||
if items == nil {
|
||||
return []connection.IndexDefinition{}
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
func ensureNonNilForeignKeys(items []connection.ForeignKeyDefinition) []connection.ForeignKeyDefinition {
|
||||
if items == nil {
|
||||
return []connection.ForeignKeyDefinition{}
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
func ensureNonNilTriggers(items []connection.TriggerDefinition) []connection.TriggerDefinition {
|
||||
if items == nil {
|
||||
return []connection.TriggerDefinition{}
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
func ensureNonNilRows(items []map[string]interface{}) []map[string]interface{} {
|
||||
if items == nil {
|
||||
return []map[string]interface{}{}
|
||||
|
||||
@@ -19,6 +19,9 @@ type fakeBackend struct {
|
||||
databasesResult connection.QueryResult
|
||||
tablesResult connection.QueryResult
|
||||
columnsResult connection.QueryResult
|
||||
indexesResult connection.QueryResult
|
||||
foreignKeysResult connection.QueryResult
|
||||
triggersResult connection.QueryResult
|
||||
ddlResult connection.QueryResult
|
||||
queryResult connection.QueryResult
|
||||
inspection appcore.SQLInspection
|
||||
@@ -50,6 +53,18 @@ func (f *fakeBackend) DBGetColumns(config connection.ConnectionConfig, dbName st
|
||||
return f.columnsResult
|
||||
}
|
||||
|
||||
func (f *fakeBackend) DBGetIndexes(config connection.ConnectionConfig, dbName string, tableName string) connection.QueryResult {
|
||||
return f.indexesResult
|
||||
}
|
||||
|
||||
func (f *fakeBackend) DBGetForeignKeys(config connection.ConnectionConfig, dbName string, tableName string) connection.QueryResult {
|
||||
return f.foreignKeysResult
|
||||
}
|
||||
|
||||
func (f *fakeBackend) DBGetTriggers(config connection.ConnectionConfig, dbName string, tableName string) connection.QueryResult {
|
||||
return f.triggersResult
|
||||
}
|
||||
|
||||
func (f *fakeBackend) DBShowCreateTable(config connection.ConnectionConfig, dbName string, tableName string) connection.QueryResult {
|
||||
return f.ddlResult
|
||||
}
|
||||
@@ -114,6 +129,108 @@ func TestGetConnectionsReturnsSavedConnectionSummaries(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetIndexesReturnsIndexDefinitions(t *testing.T) {
|
||||
backend := &fakeBackend{
|
||||
editableConnection: connection.SavedConnectionView{
|
||||
ID: "mysql-main",
|
||||
Config: connection.ConnectionConfig{
|
||||
Type: "mysql",
|
||||
Database: "app",
|
||||
},
|
||||
},
|
||||
indexesResult: connection.QueryResult{
|
||||
Success: true,
|
||||
Data: []connection.IndexDefinition{
|
||||
{Name: "idx_users_email", ColumnName: "email", NonUnique: 0, SeqInIndex: 1, IndexType: "BTREE"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
service := NewService(backend)
|
||||
result, out, err := service.GetIndexes(context.Background(), nil, tableArgs{
|
||||
ConnectionID: "mysql-main",
|
||||
DBName: "app",
|
||||
TableName: "users",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("GetIndexes returned error: %v", err)
|
||||
}
|
||||
if result == nil || result.IsError {
|
||||
t.Fatalf("expected success result, got %#v", result)
|
||||
}
|
||||
if len(out.Indexes) != 1 || out.Indexes[0].Name != "idx_users_email" {
|
||||
t.Fatalf("unexpected indexes output: %#v", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetForeignKeysReturnsForeignKeyDefinitions(t *testing.T) {
|
||||
backend := &fakeBackend{
|
||||
editableConnection: connection.SavedConnectionView{
|
||||
ID: "mysql-main",
|
||||
Config: connection.ConnectionConfig{
|
||||
Type: "mysql",
|
||||
Database: "app",
|
||||
},
|
||||
},
|
||||
foreignKeysResult: connection.QueryResult{
|
||||
Success: true,
|
||||
Data: []connection.ForeignKeyDefinition{
|
||||
{Name: "fk_orders_user_id", ColumnName: "user_id", RefTableName: "users", RefColumnName: "id", ConstraintName: "fk_orders_user_id"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
service := NewService(backend)
|
||||
result, out, err := service.GetForeignKeys(context.Background(), nil, tableArgs{
|
||||
ConnectionID: "mysql-main",
|
||||
DBName: "app",
|
||||
TableName: "orders",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("GetForeignKeys returned error: %v", err)
|
||||
}
|
||||
if result == nil || result.IsError {
|
||||
t.Fatalf("expected success result, got %#v", result)
|
||||
}
|
||||
if len(out.ForeignKeys) != 1 || out.ForeignKeys[0].RefTableName != "users" {
|
||||
t.Fatalf("unexpected foreign keys output: %#v", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetTriggersReturnsTriggerDefinitions(t *testing.T) {
|
||||
backend := &fakeBackend{
|
||||
editableConnection: connection.SavedConnectionView{
|
||||
ID: "mysql-main",
|
||||
Config: connection.ConnectionConfig{
|
||||
Type: "mysql",
|
||||
Database: "app",
|
||||
},
|
||||
},
|
||||
triggersResult: connection.QueryResult{
|
||||
Success: true,
|
||||
Data: []connection.TriggerDefinition{
|
||||
{Name: "trg_orders_audit", Timing: "AFTER", Event: "INSERT", Statement: "INSERT INTO audit_log ..."},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
service := NewService(backend)
|
||||
result, out, err := service.GetTriggers(context.Background(), nil, tableArgs{
|
||||
ConnectionID: "mysql-main",
|
||||
DBName: "app",
|
||||
TableName: "orders",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("GetTriggers returned error: %v", err)
|
||||
}
|
||||
if result == nil || result.IsError {
|
||||
t.Fatalf("expected success result, got %#v", result)
|
||||
}
|
||||
if len(out.Triggers) != 1 || out.Triggers[0].Name != "trg_orders_audit" {
|
||||
t.Fatalf("unexpected triggers output: %#v", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteSQLRejectsMutatingStatementsWithoutAllowMutating(t *testing.T) {
|
||||
backend := &fakeBackend{
|
||||
editableConnection: connection.SavedConnectionView{
|
||||
|
||||
Reference in New Issue
Block a user