From 89d717cf16eaa83c1d00345d051f0ce64a48cc62 Mon Sep 17 00:00:00 2001 From: nagisa-kun <1434936049@qq.com> Date: Fri, 22 May 2026 00:42:10 +0800 Subject: [PATCH 1/3] feat: get_service_detail --- pkg/mcp/tools/detail_tools_test.go | 240 +++++++++++++++++++++++++++++ pkg/mcp/tools/search.go | 11 +- pkg/mcp/tools/service.go | 176 +++++++++++++++++++-- pkg/mcp/transport/http/sse.go | 17 +- 4 files changed, 421 insertions(+), 23 deletions(-) create mode 100644 pkg/mcp/tools/detail_tools_test.go diff --git a/pkg/mcp/tools/detail_tools_test.go b/pkg/mcp/tools/detail_tools_test.go new file mode 100644 index 000000000..8de4767ab --- /dev/null +++ b/pkg/mcp/tools/detail_tools_test.go @@ -0,0 +1,240 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package tools + +import ( + "context" + "encoding/json" + "testing" + + meshapi "github.com/apache/dubbo-admin/api/mesh/v1alpha1" + "github.com/apache/dubbo-admin/pkg/config/app" + discoverycfg "github.com/apache/dubbo-admin/pkg/config/discovery" + enginecfg "github.com/apache/dubbo-admin/pkg/config/engine" + consolectx "github.com/apache/dubbo-admin/pkg/console/context" + "github.com/apache/dubbo-admin/pkg/console/counter" + "github.com/apache/dubbo-admin/pkg/core/lock" + "github.com/apache/dubbo-admin/pkg/core/manager" + meshresource "github.com/apache/dubbo-admin/pkg/core/resource/apis/mesh/v1alpha1" + coremodel "github.com/apache/dubbo-admin/pkg/core/resource/model" + "github.com/apache/dubbo-admin/pkg/core/store/index" + "github.com/apache/dubbo-admin/pkg/mcp/registry" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestGetServiceDetailSchema(t *testing.T) { + reg := registry.NewRegistry() + (&DetailRegistrar{}).RegisterTools(reg) + + tool, ok := reg.Get("get_service_detail") + if !ok { + t.Fatal("Tool 'get_service_detail' not registered") + } + if len(tool.InputSchema.Required) != 1 || tool.InputSchema.Required[0] != "serviceName" { + t.Errorf("Expected serviceName to be required, got %v", tool.InputSchema.Required) + } + for _, prop := range []string{"serviceName", "version", "group", "mesh"} { + if _, ok := tool.InputSchema.Properties[prop]; !ok { + t.Errorf("Missing property: %s", prop) + } + } +} + +func TestGetServiceDetailMissingServiceName(t *testing.T) { + result, err := GetServiceDetail(newToolTestContext(nil), map[string]any{}) + if err != nil { + t.Fatalf("GetServiceDetail returned unexpected error: %v", err) + } + if !result.IsError { + t.Fatal("Expected error result") + } + if got := result.Content[0].Text; got != "required parameter 'serviceName' is missing" { + t.Fatalf("Expected missing parameter error, got %q", got) + } +} + +func TestGetServiceDetailSuccess(t *testing.T) { + const ( + mesh = "mesh1" + serviceName = "org.apache.demo.DemoService" + version = "1.0.0" + group = "demo" + ) + serviceKey := coremodel.BuildResourceKey(mesh, meshresource.BuildServiceIdentityKey(serviceName, version, group)) + resource := &meshresource.ServiceResource{ + ObjectMeta: metav1.ObjectMeta{Name: meshresource.BuildServiceIdentityKey(serviceName, version, group)}, + Mesh: mesh, + Spec: &meshapi.Service{ + Name: serviceName, + Version: version, + Group: group, + Language: "java", + Methods: []string{"sayHello"}, + }, + } + ctx := newToolTestContext(map[coremodel.ResourceKind]map[string]coremodel.Resource{ + meshresource.ServiceKind: { + serviceKey: resource, + }, + }) + + result, err := GetServiceDetail(ctx, map[string]any{ + "serviceName": serviceName, + "version": version, + "group": group, + "mesh": mesh, + }) + if err != nil { + t.Fatalf("GetServiceDetail returned unexpected error: %v", err) + } + if result.IsError { + t.Fatalf("Expected success result, got %q", result.Content[0].Text) + } + + var payload struct { + Language string `json:"language"` + Methods []string `json:"methods"` + } + if err := json.Unmarshal([]byte(result.Content[0].Text), &payload); err != nil { + t.Fatalf("Failed to unmarshal result: %v", err) + } + if payload.Language != "java" { + t.Fatalf("Expected language java, got %q", payload.Language) + } + if len(payload.Methods) != 1 || payload.Methods[0] != "sayHello" { + t.Fatalf("Expected methods [sayHello], got %v", payload.Methods) + } +} + +func TestGetServiceDistributionRegisteredHandler(t *testing.T) { + reg := registry.NewRegistry() + (&ServiceRegistrar{}).RegisterTools(reg) + + tool, ok := reg.Get("get_service_distribution") + if !ok { + t.Fatal("Tool 'get_service_distribution' not registered") + } + if tool.Handler == nil { + t.Fatal("Tool 'get_service_distribution' handler is nil") + } + result, err := tool.Handler(newToolTestContext(nil), map[string]any{"serviceName": "missing"}) + if err != nil { + t.Fatalf("get_service_distribution handler returned unexpected error: %v", err) + } + if result.IsError { + t.Fatalf("Expected success result with empty distribution, got %q", result.Content[0].Text) + } + + var payload struct { + ServiceName string `json:"serviceName"` + Distribution []any `json:"distribution"` + TotalApps int `json:"totalApps"` + } + if err := json.Unmarshal([]byte(result.Content[0].Text), &payload); err != nil { + t.Fatalf("Failed to unmarshal result: %v", err) + } + if payload.ServiceName != "missing" { + t.Fatalf("Expected serviceName missing, got %q", payload.ServiceName) + } + if len(payload.Distribution) != 0 || payload.TotalApps != 0 { + t.Fatalf("Expected empty distribution, got distribution=%v totalApps=%d", payload.Distribution, payload.TotalApps) + } +} + +func newToolTestContext(resources map[coremodel.ResourceKind]map[string]coremodel.Resource) consolectx.Context { + return &toolTestContext{ + config: app.AdminConfig{ + Discovery: []*discoverycfg.Config{{ID: "mesh1"}}, + Engine: &enginecfg.Config{Name: "engine1"}, + }, + resourceManager: &toolTestResourceManager{resources: resources}, + } +} + +type toolTestContext struct { + config app.AdminConfig + resourceManager manager.ResourceManager +} + +func (c *toolTestContext) ResourceManager() manager.ResourceManager { + return c.resourceManager +} + +func (c *toolTestContext) CounterManager() counter.CounterManager { + return nil +} + +func (c *toolTestContext) Config() app.AdminConfig { + return c.config +} + +func (c *toolTestContext) AppContext() context.Context { + return context.Background() +} + +func (c *toolTestContext) LockManager() lock.Lock { + return nil +} + +type toolTestResourceManager struct { + resources map[coremodel.ResourceKind]map[string]coremodel.Resource +} + +func (m *toolTestResourceManager) GetByKey(rk coremodel.ResourceKind, key string) (coremodel.Resource, bool, error) { + byKind := m.resources[rk] + if byKind == nil { + return nil, false, nil + } + resource, ok := byKind[key] + return resource, ok, nil +} + +func (m *toolTestResourceManager) GetByKeys(rk coremodel.ResourceKind, keys []string) ([]coremodel.Resource, error) { + byKind := m.resources[rk] + result := make([]coremodel.Resource, 0, len(keys)) + for _, key := range keys { + if resource, ok := byKind[key]; ok { + result = append(result, resource) + } + } + return result, nil +} + +func (m *toolTestResourceManager) ListByIndexes(coremodel.ResourceKind, []index.IndexCondition) ([]coremodel.Resource, error) { + return nil, nil +} + +func (m *toolTestResourceManager) PageListByIndexes(coremodel.ResourceKind, []index.IndexCondition, coremodel.PageReq) (*coremodel.PageData[coremodel.Resource], error) { + return coremodel.NewPageData[coremodel.Resource](0, 0, 0, nil), nil +} + +func (m *toolTestResourceManager) Add(coremodel.Resource) error { + return nil +} + +func (m *toolTestResourceManager) Update(coremodel.Resource) error { + return nil +} + +func (m *toolTestResourceManager) Upsert(coremodel.Resource) error { + return nil +} + +func (m *toolTestResourceManager) DeleteByKey(coremodel.ResourceKind, string, string) error { + return nil +} diff --git a/pkg/mcp/tools/search.go b/pkg/mcp/tools/search.go index f8093945f..5a46183a3 100644 --- a/pkg/mcp/tools/search.go +++ b/pkg/mcp/tools/search.go @@ -21,7 +21,8 @@ import ( consolectx "github.com/apache/dubbo-admin/pkg/console/context" "github.com/apache/dubbo-admin/pkg/console/model" "github.com/apache/dubbo-admin/pkg/console/service" - "github.com/apache/dubbo-admin/pkg/mcp/common" + "github.com/apache/dubbo-admin/pkg/mcp/registry" + "github.com/apache/dubbo-admin/pkg/mcp/types" ) // searchExecutor 搜索执行器接口 @@ -120,11 +121,11 @@ func (e *appNameSearchExecutor) execute(ctx consolectx.Context, keyword, mesh st func (e *appNameSearchExecutor) buildResult(pagedResult *model.SearchPaginationResult, keyword string, pageSize, pageNumber int) map[string]any { apps := extractGlobalApplications(pagedResult) return map[string]any{ - "keyword": keyword, - "pageSize": pageSize, - "pageNumber": pageNumber, + "keyword": keyword, + "pageSize": pageSize, + "pageNumber": pageNumber, "applications": apps, - "totalCount": len(apps), + "totalCount": len(apps), } } diff --git a/pkg/mcp/tools/service.go b/pkg/mcp/tools/service.go index 7bca8b220..64f75e53b 100644 --- a/pkg/mcp/tools/service.go +++ b/pkg/mcp/tools/service.go @@ -21,9 +21,139 @@ import ( consolectx "github.com/apache/dubbo-admin/pkg/console/context" "github.com/apache/dubbo-admin/pkg/console/model" "github.com/apache/dubbo-admin/pkg/console/service" - "github.com/apache/dubbo-admin/pkg/mcp/common" + "github.com/apache/dubbo-admin/pkg/mcp/registry" + "github.com/apache/dubbo-admin/pkg/mcp/types" ) +// ServiceRegistrar 服务工具注册器 +type ServiceRegistrar struct{} + +// RegisterTools 实现 ToolRegistrar 接口 +func (r *ServiceRegistrar) RegisterTools(reg *registry.Registry) { + reg.Register(types.ToolDef{ + Name: "search_services", + Description: "搜索 Dubbo 服务,支持按服务名过滤和分页", + InputSchema: types.InputSchema{ + Type: "object", + Properties: map[string]types.PropertyDef{ + "keywords": { + Type: "string", + Description: "服务名搜索关键字,支持模糊匹配", + }, + "mesh": { + Type: "string", + Description: "Mesh 名称,默认使用配置中的默认 mesh", + }, + "pageSize": { + Type: "integer", + Description: "每页数量", + Default: DefaultPageSize, + }, + "pageNumber": { + Type: "integer", + Description: "页码,从 1 开始", + Default: DefaultPageNumber, + }, + }, + }, + Handler: SearchServices, + }) + + reg.Register(types.ToolDef{ + Name: "get_service_distribution", + Description: "获取服务关联的 provider/consumer 应用分布", + InputSchema: types.InputSchema{ + Type: "object", + Required: []string{"serviceName"}, + Properties: map[string]types.PropertyDef{ + "serviceName": { + Type: "string", + Description: "服务名称", + }, + "group": { + Type: "string", + Description: "服务分组", + Default: "", + }, + "version": { + Type: "string", + Description: "服务版本", + Default: "", + }, + "side": { + Type: "string", + Description: "服务端或消费者 (provider/consumer)", + Default: string(ServiceSideProvider), + Enum: []string{string(ServiceSideProvider), string(ServiceSideConsumer)}, + }, + "mesh": { + Type: "string", + Description: "Mesh 名称,默认使用配置中的默认 mesh", + }, + }, + }, + Handler: GetServiceDistribution, + }) + + // 获取服务详情 + reg.Register(types.ToolDef{ + Name: "get_service_detail", + Description: "获取服务详情,包括语言和方法列表", + InputSchema: types.InputSchema{ + Type: "object", + Required: []string{"serviceName"}, + Properties: map[string]types.PropertyDef{ + "serviceName": { + Type: "string", + Description: "服务名称", + }, + "version": { + Type: "string", + Description: "服务版本", + }, + "group": { + Type: "string", + Description: "服务分组", + }, + "mesh": { + Type: "string", + Description: "Mesh 名称,默认使用配置中的默认 mesh", + }, + }, + }, + Handler: GetServiceDetail, + }) + + // 获取服务详情 + reg.Register(types.ToolDef{ + Name: "get_service_detail", + Description: "获取服务详情,包括语言和方法列表", + InputSchema: types.InputSchema{ + Type: "object", + Required: []string{"serviceName"}, + Properties: map[string]types.PropertyDef{ + "serviceName": { + Type: "string", + Description: "服务名称", + }, + "version": { + Type: "string", + Description: "服务版本", + }, + "group": { + Type: "string", + Description: "服务分组", + }, + "mesh": { + Type: "string", + Description: "Mesh 名称,默认使用配置中的默认 mesh", + }, + }, + }, + Handler: GetServiceDetail, + }) +} + // SearchServices 搜索服务 func SearchServices(ctx consolectx.Context, args map[string]any) (*common.ToolResult, error) { helper := common.NewArgsHelper(args) @@ -46,9 +176,9 @@ func SearchServices(ctx consolectx.Context, args map[string]any) (*common.ToolRe return buildServiceSearchResult(result, keywords, mesh, pageSize, pageNumber) } -// GetServiceDetail 获取服务详情 -func GetServiceDetail(ctx consolectx.Context, args map[string]any) (*common.ToolResult, error) { - helper := common.NewArgsHelper(args) +// GetServiceDistribution 获取服务关联的应用分布 +func GetServiceDistribution(ctx consolectx.Context, args map[string]any) (*types.ToolResult, error) { + helper := NewArgsHelper(args) serviceName := helper.GetString("serviceName", "") params := serviceDetailParams{ @@ -156,14 +286,36 @@ func extractServices(result *model.SearchPaginationResult) ([]any, int) { resultSlice := make([]any, 0, len(services)) for _, svc := range services { - if svc != nil { - resultSlice = append(resultSlice, map[string]any{ - "serviceName": svc.ServiceName, - "version": svc.Version, - "group": svc.Group, - "consumerAppName": svc.ConsumerAppName, - }) - } + resultSlice = append(resultSlice, map[string]any{ + "serviceName": svc.ServiceName, + "version": svc.Version, + "group": svc.Group, + "providerAppName": svc.ProviderAppName, + "consumerAppName": svc.ConsumerAppName, + }) } return resultSlice, int(result.PageInfo.Total) } + +// GetServiceDetail 获取服务详情 +func GetServiceDetail(ctx consolectx.Context, args map[string]any) (*types.ToolResult, error) { + helper := NewArgsHelper(args) + serviceName := helper.GetString("serviceName", "") + if serviceName == "" { + return ErrorResult(fmt.Errorf("required parameter 'serviceName' is missing")), nil + } + + req := &model.ServiceDetailReq{ + ServiceName: serviceName, + Version: helper.GetString("version", ""), + Group: helper.GetString("group", ""), + Mesh: GetMeshArg(ctx, args), + } + + detail, err := service.GetServiceDetail(ctx, req) + if err != nil { + return ErrorResult(err), nil + } + + return JsonResult(detail) +} \ No newline at end of file diff --git a/pkg/mcp/transport/http/sse.go b/pkg/mcp/transport/http/sse.go index 84a0687f9..137633ed4 100644 --- a/pkg/mcp/transport/http/sse.go +++ b/pkg/mcp/transport/http/sse.go @@ -26,13 +26,14 @@ import ( "github.com/apache/dubbo-admin/pkg/mcp" "github.com/apache/dubbo-admin/pkg/mcp/common" + "github.com/llm-d/llm-d-workload-variant-autoscaler/pkg/core" ) // SSETransport Server-Sent Events传输层 type SSETransport struct { - server *mcp.Server - clients map[*SSEClient]bool - mu sync.RWMutex + server *core.Server + clients map[*SSEClient]bool + mu sync.RWMutex broadcast chan []byte } @@ -58,8 +59,8 @@ func NewSSEClient(id string) *SSEClient { // NewSSETransport 创建SSE传输层 func NewSSETransport(server *mcp.Server) *SSETransport { return &SSETransport{ - server: server, - clients: make(map[*SSEClient]bool), + server: server, + clients: make(map[*SSEClient]bool), broadcast: make(chan []byte, 256), } } @@ -83,7 +84,11 @@ func (t *SSETransport) HandleSSE(w http.ResponseWriter, r *http.Request) { t.sendToClient(client, t.sseEvent("connected", "SSE connection established")) // 等待断开连接 - <-client.ctx.Done() + select { + case <-client.ctx.Done(): + case <-r.Context().Done(): + client.done() + } t.mu.Lock() delete(t.clients, client) From 4c19f0e2dd02356c89d33a7b4755ebb667159981 Mon Sep 17 00:00:00 2001 From: nagisa-kun <1434936049@qq.com> Date: Tue, 9 Jun 2026 00:00:17 +0800 Subject: [PATCH 2/3] clear code --- pkg/mcp/register.go | 30 +++++- pkg/mcp/tools/detail_tools_test.go | 33 +----- pkg/mcp/tools/search.go | 3 +- pkg/mcp/tools/service.go | 156 +++-------------------------- pkg/mcp/transport/http/sse.go | 3 +- 5 files changed, 48 insertions(+), 177 deletions(-) diff --git a/pkg/mcp/register.go b/pkg/mcp/register.go index 641d567ff..cec3acb4c 100644 --- a/pkg/mcp/register.go +++ b/pkg/mcp/register.go @@ -100,7 +100,7 @@ func RegisterTools(server *Server) { }) server.RegisterTool(&common.ToolDef{ - Name: "get_service_detail", + Name: "get_service_distribution", Description: "获取服务详情,包括服务的提供者或消费者应用列表", InputSchema: common.InputSchema{ Type: "object", @@ -128,6 +128,34 @@ func RegisterTools(server *Server) { }, }, }, + Handler: tools.GetServiceDistribution, + }) + + server.RegisterTool(&common.ToolDef{ + Name: "get_service_detail", + Description: "获取服务详情,包括语言和方法列表", + InputSchema: common.InputSchema{ + Type: "object", + Required: []string{"serviceName"}, + Properties: map[string]common.PropertyDef{ + "serviceName": { + Type: "string", + Description: "服务名称", + }, + "group": { + Type: "string", + Description: "服务分组", + }, + "version": { + Type: "string", + Description: "服务版本", + }, + "mesh": { + Type: "string", + Description: "网格名称,默认使用第一个 discovery 配置的 id", + }, + }, + }, Handler: tools.GetServiceDetail, }) diff --git a/pkg/mcp/tools/detail_tools_test.go b/pkg/mcp/tools/detail_tools_test.go index 8de4767ab..8aeb1411b 100644 --- a/pkg/mcp/tools/detail_tools_test.go +++ b/pkg/mcp/tools/detail_tools_test.go @@ -33,28 +33,9 @@ import ( meshresource "github.com/apache/dubbo-admin/pkg/core/resource/apis/mesh/v1alpha1" coremodel "github.com/apache/dubbo-admin/pkg/core/resource/model" "github.com/apache/dubbo-admin/pkg/core/store/index" - "github.com/apache/dubbo-admin/pkg/mcp/registry" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -func TestGetServiceDetailSchema(t *testing.T) { - reg := registry.NewRegistry() - (&DetailRegistrar{}).RegisterTools(reg) - - tool, ok := reg.Get("get_service_detail") - if !ok { - t.Fatal("Tool 'get_service_detail' not registered") - } - if len(tool.InputSchema.Required) != 1 || tool.InputSchema.Required[0] != "serviceName" { - t.Errorf("Expected serviceName to be required, got %v", tool.InputSchema.Required) - } - for _, prop := range []string{"serviceName", "version", "group", "mesh"} { - if _, ok := tool.InputSchema.Properties[prop]; !ok { - t.Errorf("Missing property: %s", prop) - } - } -} - func TestGetServiceDetailMissingServiceName(t *testing.T) { result, err := GetServiceDetail(newToolTestContext(nil), map[string]any{}) if err != nil { @@ -121,18 +102,8 @@ func TestGetServiceDetailSuccess(t *testing.T) { } } -func TestGetServiceDistributionRegisteredHandler(t *testing.T) { - reg := registry.NewRegistry() - (&ServiceRegistrar{}).RegisterTools(reg) - - tool, ok := reg.Get("get_service_distribution") - if !ok { - t.Fatal("Tool 'get_service_distribution' not registered") - } - if tool.Handler == nil { - t.Fatal("Tool 'get_service_distribution' handler is nil") - } - result, err := tool.Handler(newToolTestContext(nil), map[string]any{"serviceName": "missing"}) +func TestGetServiceDistributionSuccessWithEmptyDistribution(t *testing.T) { + result, err := GetServiceDistribution(newToolTestContext(nil), map[string]any{"serviceName": "missing"}) if err != nil { t.Fatalf("get_service_distribution handler returned unexpected error: %v", err) } diff --git a/pkg/mcp/tools/search.go b/pkg/mcp/tools/search.go index 5a46183a3..2b4e27862 100644 --- a/pkg/mcp/tools/search.go +++ b/pkg/mcp/tools/search.go @@ -21,8 +21,7 @@ import ( consolectx "github.com/apache/dubbo-admin/pkg/console/context" "github.com/apache/dubbo-admin/pkg/console/model" "github.com/apache/dubbo-admin/pkg/console/service" - "github.com/apache/dubbo-admin/pkg/mcp/registry" - "github.com/apache/dubbo-admin/pkg/mcp/types" + "github.com/apache/dubbo-admin/pkg/mcp/common" ) // searchExecutor 搜索执行器接口 diff --git a/pkg/mcp/tools/service.go b/pkg/mcp/tools/service.go index 64f75e53b..354656ebd 100644 --- a/pkg/mcp/tools/service.go +++ b/pkg/mcp/tools/service.go @@ -18,142 +18,14 @@ package tools import ( + "fmt" + consolectx "github.com/apache/dubbo-admin/pkg/console/context" "github.com/apache/dubbo-admin/pkg/console/model" "github.com/apache/dubbo-admin/pkg/console/service" - "github.com/apache/dubbo-admin/pkg/mcp/registry" - "github.com/apache/dubbo-admin/pkg/mcp/types" + "github.com/apache/dubbo-admin/pkg/mcp/common" ) -// ServiceRegistrar 服务工具注册器 -type ServiceRegistrar struct{} - -// RegisterTools 实现 ToolRegistrar 接口 -func (r *ServiceRegistrar) RegisterTools(reg *registry.Registry) { - reg.Register(types.ToolDef{ - Name: "search_services", - Description: "搜索 Dubbo 服务,支持按服务名过滤和分页", - InputSchema: types.InputSchema{ - Type: "object", - Properties: map[string]types.PropertyDef{ - "keywords": { - Type: "string", - Description: "服务名搜索关键字,支持模糊匹配", - }, - "mesh": { - Type: "string", - Description: "Mesh 名称,默认使用配置中的默认 mesh", - }, - "pageSize": { - Type: "integer", - Description: "每页数量", - Default: DefaultPageSize, - }, - "pageNumber": { - Type: "integer", - Description: "页码,从 1 开始", - Default: DefaultPageNumber, - }, - }, - }, - Handler: SearchServices, - }) - - reg.Register(types.ToolDef{ - Name: "get_service_distribution", - Description: "获取服务关联的 provider/consumer 应用分布", - InputSchema: types.InputSchema{ - Type: "object", - Required: []string{"serviceName"}, - Properties: map[string]types.PropertyDef{ - "serviceName": { - Type: "string", - Description: "服务名称", - }, - "group": { - Type: "string", - Description: "服务分组", - Default: "", - }, - "version": { - Type: "string", - Description: "服务版本", - Default: "", - }, - "side": { - Type: "string", - Description: "服务端或消费者 (provider/consumer)", - Default: string(ServiceSideProvider), - Enum: []string{string(ServiceSideProvider), string(ServiceSideConsumer)}, - }, - "mesh": { - Type: "string", - Description: "Mesh 名称,默认使用配置中的默认 mesh", - }, - }, - }, - Handler: GetServiceDistribution, - }) - - // 获取服务详情 - reg.Register(types.ToolDef{ - Name: "get_service_detail", - Description: "获取服务详情,包括语言和方法列表", - InputSchema: types.InputSchema{ - Type: "object", - Required: []string{"serviceName"}, - Properties: map[string]types.PropertyDef{ - "serviceName": { - Type: "string", - Description: "服务名称", - }, - "version": { - Type: "string", - Description: "服务版本", - }, - "group": { - Type: "string", - Description: "服务分组", - }, - "mesh": { - Type: "string", - Description: "Mesh 名称,默认使用配置中的默认 mesh", - }, - }, - }, - Handler: GetServiceDetail, - }) - - // 获取服务详情 - reg.Register(types.ToolDef{ - Name: "get_service_detail", - Description: "获取服务详情,包括语言和方法列表", - InputSchema: types.InputSchema{ - Type: "object", - Required: []string{"serviceName"}, - Properties: map[string]types.PropertyDef{ - "serviceName": { - Type: "string", - Description: "服务名称", - }, - "version": { - Type: "string", - Description: "服务版本", - }, - "group": { - Type: "string", - Description: "服务分组", - }, - "mesh": { - Type: "string", - Description: "Mesh 名称,默认使用配置中的默认 mesh", - }, - }, - }, - Handler: GetServiceDetail, - }) -} - // SearchServices 搜索服务 func SearchServices(ctx consolectx.Context, args map[string]any) (*common.ToolResult, error) { helper := common.NewArgsHelper(args) @@ -177,8 +49,8 @@ func SearchServices(ctx consolectx.Context, args map[string]any) (*common.ToolRe } // GetServiceDistribution 获取服务关联的应用分布 -func GetServiceDistribution(ctx consolectx.Context, args map[string]any) (*types.ToolResult, error) { - helper := NewArgsHelper(args) +func GetServiceDistribution(ctx consolectx.Context, args map[string]any) (*common.ToolResult, error) { + helper := common.NewArgsHelper(args) serviceName := helper.GetString("serviceName", "") params := serviceDetailParams{ @@ -286,11 +158,13 @@ func extractServices(result *model.SearchPaginationResult) ([]any, int) { resultSlice := make([]any, 0, len(services)) for _, svc := range services { + if svc == nil { + continue + } resultSlice = append(resultSlice, map[string]any{ "serviceName": svc.ServiceName, "version": svc.Version, "group": svc.Group, - "providerAppName": svc.ProviderAppName, "consumerAppName": svc.ConsumerAppName, }) } @@ -298,24 +172,24 @@ func extractServices(result *model.SearchPaginationResult) ([]any, int) { } // GetServiceDetail 获取服务详情 -func GetServiceDetail(ctx consolectx.Context, args map[string]any) (*types.ToolResult, error) { - helper := NewArgsHelper(args) +func GetServiceDetail(ctx consolectx.Context, args map[string]any) (*common.ToolResult, error) { + helper := common.NewArgsHelper(args) serviceName := helper.GetString("serviceName", "") if serviceName == "" { - return ErrorResult(fmt.Errorf("required parameter 'serviceName' is missing")), nil + return common.ErrorResult(fmt.Errorf("required parameter 'serviceName' is missing")), nil } req := &model.ServiceDetailReq{ ServiceName: serviceName, Version: helper.GetString("version", ""), Group: helper.GetString("group", ""), - Mesh: GetMeshArg(ctx, args), + Mesh: common.GetMeshArg(ctx, args), } detail, err := service.GetServiceDetail(ctx, req) if err != nil { - return ErrorResult(err), nil + return common.ErrorResult(err), nil } - return JsonResult(detail) -} \ No newline at end of file + return common.JsonResult(detail) +} diff --git a/pkg/mcp/transport/http/sse.go b/pkg/mcp/transport/http/sse.go index 137633ed4..1c836cd3f 100644 --- a/pkg/mcp/transport/http/sse.go +++ b/pkg/mcp/transport/http/sse.go @@ -26,12 +26,11 @@ import ( "github.com/apache/dubbo-admin/pkg/mcp" "github.com/apache/dubbo-admin/pkg/mcp/common" - "github.com/llm-d/llm-d-workload-variant-autoscaler/pkg/core" ) // SSETransport Server-Sent Events传输层 type SSETransport struct { - server *core.Server + server *mcp.Server clients map[*SSEClient]bool mu sync.RWMutex broadcast chan []byte From a3e7ad80c158b9517943737ec6d55990768a8e4f Mon Sep 17 00:00:00 2001 From: nagisa-kun <1434936049@qq.com> Date: Tue, 9 Jun 2026 00:00:32 +0800 Subject: [PATCH 3/3] feat: test --- pkg/mcp/register_test.go | 57 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 pkg/mcp/register_test.go diff --git a/pkg/mcp/register_test.go b/pkg/mcp/register_test.go new file mode 100644 index 000000000..c9806f737 --- /dev/null +++ b/pkg/mcp/register_test.go @@ -0,0 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package mcp + +import "testing" + +func TestRegisterServiceDetailTools(t *testing.T) { + server := NewServer("test", "dev") + RegisterTools(server) + + detail, ok := server.tools["get_service_detail"] + if !ok { + t.Fatal("Tool 'get_service_detail' not registered") + } + if detail.Handler == nil { + t.Fatal("Tool 'get_service_detail' handler is nil") + } + if len(detail.InputSchema.Required) != 1 || detail.InputSchema.Required[0] != "serviceName" { + t.Fatalf("Expected serviceName to be required, got %v", detail.InputSchema.Required) + } + for _, prop := range []string{"serviceName", "version", "group", "mesh"} { + if _, ok := detail.InputSchema.Properties[prop]; !ok { + t.Fatalf("get_service_detail missing property %q", prop) + } + } + if _, ok := detail.InputSchema.Properties["side"]; ok { + t.Fatal("get_service_detail should not expose side") + } + + distribution, ok := server.tools["get_service_distribution"] + if !ok { + t.Fatal("Tool 'get_service_distribution' not registered") + } + if distribution.Handler == nil { + t.Fatal("Tool 'get_service_distribution' handler is nil") + } + for _, prop := range []string{"serviceName", "version", "group", "side", "mesh"} { + if _, ok := distribution.InputSchema.Properties[prop]; !ok { + t.Fatalf("get_service_distribution missing property %q", prop) + } + } +}