Skip to content

Commit 2df3e07

Browse files
feat: get_service_detail (#1475)
* feat: get_service_detail * clear code * feat: test
1 parent 263bac4 commit 2df3e07

6 files changed

Lines changed: 346 additions & 20 deletions

File tree

pkg/mcp/register.go

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ func RegisterTools(server *Server) {
100100
})
101101

102102
server.RegisterTool(&common.ToolDef{
103-
Name: "get_service_detail",
103+
Name: "get_service_distribution",
104104
Description: "获取服务详情,包括服务的提供者或消费者应用列表",
105105
InputSchema: common.InputSchema{
106106
Type: "object",
@@ -128,6 +128,34 @@ func RegisterTools(server *Server) {
128128
},
129129
},
130130
},
131+
Handler: tools.GetServiceDistribution,
132+
})
133+
134+
server.RegisterTool(&common.ToolDef{
135+
Name: "get_service_detail",
136+
Description: "获取服务详情,包括语言和方法列表",
137+
InputSchema: common.InputSchema{
138+
Type: "object",
139+
Required: []string{"serviceName"},
140+
Properties: map[string]common.PropertyDef{
141+
"serviceName": {
142+
Type: "string",
143+
Description: "服务名称",
144+
},
145+
"group": {
146+
Type: "string",
147+
Description: "服务分组",
148+
},
149+
"version": {
150+
Type: "string",
151+
Description: "服务版本",
152+
},
153+
"mesh": {
154+
Type: "string",
155+
Description: "网格名称,默认使用第一个 discovery 配置的 id",
156+
},
157+
},
158+
},
131159
Handler: tools.GetServiceDetail,
132160
})
133161

pkg/mcp/register_test.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package mcp
19+
20+
import "testing"
21+
22+
func TestRegisterServiceDetailTools(t *testing.T) {
23+
server := NewServer("test", "dev")
24+
RegisterTools(server)
25+
26+
detail, ok := server.tools["get_service_detail"]
27+
if !ok {
28+
t.Fatal("Tool 'get_service_detail' not registered")
29+
}
30+
if detail.Handler == nil {
31+
t.Fatal("Tool 'get_service_detail' handler is nil")
32+
}
33+
if len(detail.InputSchema.Required) != 1 || detail.InputSchema.Required[0] != "serviceName" {
34+
t.Fatalf("Expected serviceName to be required, got %v", detail.InputSchema.Required)
35+
}
36+
for _, prop := range []string{"serviceName", "version", "group", "mesh"} {
37+
if _, ok := detail.InputSchema.Properties[prop]; !ok {
38+
t.Fatalf("get_service_detail missing property %q", prop)
39+
}
40+
}
41+
if _, ok := detail.InputSchema.Properties["side"]; ok {
42+
t.Fatal("get_service_detail should not expose side")
43+
}
44+
45+
distribution, ok := server.tools["get_service_distribution"]
46+
if !ok {
47+
t.Fatal("Tool 'get_service_distribution' not registered")
48+
}
49+
if distribution.Handler == nil {
50+
t.Fatal("Tool 'get_service_distribution' handler is nil")
51+
}
52+
for _, prop := range []string{"serviceName", "version", "group", "side", "mesh"} {
53+
if _, ok := distribution.InputSchema.Properties[prop]; !ok {
54+
t.Fatalf("get_service_distribution missing property %q", prop)
55+
}
56+
}
57+
}

pkg/mcp/tools/detail_tools_test.go

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package tools
19+
20+
import (
21+
"context"
22+
"encoding/json"
23+
"testing"
24+
25+
meshapi "github.com/apache/dubbo-admin/api/mesh/v1alpha1"
26+
"github.com/apache/dubbo-admin/pkg/config/app"
27+
discoverycfg "github.com/apache/dubbo-admin/pkg/config/discovery"
28+
enginecfg "github.com/apache/dubbo-admin/pkg/config/engine"
29+
consolectx "github.com/apache/dubbo-admin/pkg/console/context"
30+
"github.com/apache/dubbo-admin/pkg/console/counter"
31+
"github.com/apache/dubbo-admin/pkg/core/lock"
32+
"github.com/apache/dubbo-admin/pkg/core/manager"
33+
meshresource "github.com/apache/dubbo-admin/pkg/core/resource/apis/mesh/v1alpha1"
34+
coremodel "github.com/apache/dubbo-admin/pkg/core/resource/model"
35+
"github.com/apache/dubbo-admin/pkg/core/store/index"
36+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
37+
)
38+
39+
func TestGetServiceDetailMissingServiceName(t *testing.T) {
40+
result, err := GetServiceDetail(newToolTestContext(nil), map[string]any{})
41+
if err != nil {
42+
t.Fatalf("GetServiceDetail returned unexpected error: %v", err)
43+
}
44+
if !result.IsError {
45+
t.Fatal("Expected error result")
46+
}
47+
if got := result.Content[0].Text; got != "required parameter 'serviceName' is missing" {
48+
t.Fatalf("Expected missing parameter error, got %q", got)
49+
}
50+
}
51+
52+
func TestGetServiceDetailSuccess(t *testing.T) {
53+
const (
54+
mesh = "mesh1"
55+
serviceName = "org.apache.demo.DemoService"
56+
version = "1.0.0"
57+
group = "demo"
58+
)
59+
serviceKey := coremodel.BuildResourceKey(mesh, meshresource.BuildServiceIdentityKey(serviceName, version, group))
60+
resource := &meshresource.ServiceResource{
61+
ObjectMeta: metav1.ObjectMeta{Name: meshresource.BuildServiceIdentityKey(serviceName, version, group)},
62+
Mesh: mesh,
63+
Spec: &meshapi.Service{
64+
Name: serviceName,
65+
Version: version,
66+
Group: group,
67+
Language: "java",
68+
Methods: []string{"sayHello"},
69+
},
70+
}
71+
ctx := newToolTestContext(map[coremodel.ResourceKind]map[string]coremodel.Resource{
72+
meshresource.ServiceKind: {
73+
serviceKey: resource,
74+
},
75+
})
76+
77+
result, err := GetServiceDetail(ctx, map[string]any{
78+
"serviceName": serviceName,
79+
"version": version,
80+
"group": group,
81+
"mesh": mesh,
82+
})
83+
if err != nil {
84+
t.Fatalf("GetServiceDetail returned unexpected error: %v", err)
85+
}
86+
if result.IsError {
87+
t.Fatalf("Expected success result, got %q", result.Content[0].Text)
88+
}
89+
90+
var payload struct {
91+
Language string `json:"language"`
92+
Methods []string `json:"methods"`
93+
}
94+
if err := json.Unmarshal([]byte(result.Content[0].Text), &payload); err != nil {
95+
t.Fatalf("Failed to unmarshal result: %v", err)
96+
}
97+
if payload.Language != "java" {
98+
t.Fatalf("Expected language java, got %q", payload.Language)
99+
}
100+
if len(payload.Methods) != 1 || payload.Methods[0] != "sayHello" {
101+
t.Fatalf("Expected methods [sayHello], got %v", payload.Methods)
102+
}
103+
}
104+
105+
func TestGetServiceDistributionSuccessWithEmptyDistribution(t *testing.T) {
106+
result, err := GetServiceDistribution(newToolTestContext(nil), map[string]any{"serviceName": "missing"})
107+
if err != nil {
108+
t.Fatalf("get_service_distribution handler returned unexpected error: %v", err)
109+
}
110+
if result.IsError {
111+
t.Fatalf("Expected success result with empty distribution, got %q", result.Content[0].Text)
112+
}
113+
114+
var payload struct {
115+
ServiceName string `json:"serviceName"`
116+
Distribution []any `json:"distribution"`
117+
TotalApps int `json:"totalApps"`
118+
}
119+
if err := json.Unmarshal([]byte(result.Content[0].Text), &payload); err != nil {
120+
t.Fatalf("Failed to unmarshal result: %v", err)
121+
}
122+
if payload.ServiceName != "missing" {
123+
t.Fatalf("Expected serviceName missing, got %q", payload.ServiceName)
124+
}
125+
if len(payload.Distribution) != 0 || payload.TotalApps != 0 {
126+
t.Fatalf("Expected empty distribution, got distribution=%v totalApps=%d", payload.Distribution, payload.TotalApps)
127+
}
128+
}
129+
130+
func newToolTestContext(resources map[coremodel.ResourceKind]map[string]coremodel.Resource) consolectx.Context {
131+
return &toolTestContext{
132+
config: app.AdminConfig{
133+
Discovery: []*discoverycfg.Config{{ID: "mesh1"}},
134+
Engine: &enginecfg.Config{Name: "engine1"},
135+
},
136+
resourceManager: &toolTestResourceManager{resources: resources},
137+
}
138+
}
139+
140+
type toolTestContext struct {
141+
config app.AdminConfig
142+
resourceManager manager.ResourceManager
143+
}
144+
145+
func (c *toolTestContext) ResourceManager() manager.ResourceManager {
146+
return c.resourceManager
147+
}
148+
149+
func (c *toolTestContext) CounterManager() counter.CounterManager {
150+
return nil
151+
}
152+
153+
func (c *toolTestContext) Config() app.AdminConfig {
154+
return c.config
155+
}
156+
157+
func (c *toolTestContext) AppContext() context.Context {
158+
return context.Background()
159+
}
160+
161+
func (c *toolTestContext) LockManager() lock.Lock {
162+
return nil
163+
}
164+
165+
type toolTestResourceManager struct {
166+
resources map[coremodel.ResourceKind]map[string]coremodel.Resource
167+
}
168+
169+
func (m *toolTestResourceManager) GetByKey(rk coremodel.ResourceKind, key string) (coremodel.Resource, bool, error) {
170+
byKind := m.resources[rk]
171+
if byKind == nil {
172+
return nil, false, nil
173+
}
174+
resource, ok := byKind[key]
175+
return resource, ok, nil
176+
}
177+
178+
func (m *toolTestResourceManager) GetByKeys(rk coremodel.ResourceKind, keys []string) ([]coremodel.Resource, error) {
179+
byKind := m.resources[rk]
180+
result := make([]coremodel.Resource, 0, len(keys))
181+
for _, key := range keys {
182+
if resource, ok := byKind[key]; ok {
183+
result = append(result, resource)
184+
}
185+
}
186+
return result, nil
187+
}
188+
189+
func (m *toolTestResourceManager) ListByIndexes(coremodel.ResourceKind, []index.IndexCondition) ([]coremodel.Resource, error) {
190+
return nil, nil
191+
}
192+
193+
func (m *toolTestResourceManager) PageListByIndexes(coremodel.ResourceKind, []index.IndexCondition, coremodel.PageReq) (*coremodel.PageData[coremodel.Resource], error) {
194+
return coremodel.NewPageData[coremodel.Resource](0, 0, 0, nil), nil
195+
}
196+
197+
func (m *toolTestResourceManager) Add(coremodel.Resource) error {
198+
return nil
199+
}
200+
201+
func (m *toolTestResourceManager) Update(coremodel.Resource) error {
202+
return nil
203+
}
204+
205+
func (m *toolTestResourceManager) Upsert(coremodel.Resource) error {
206+
return nil
207+
}
208+
209+
func (m *toolTestResourceManager) DeleteByKey(coremodel.ResourceKind, string, string) error {
210+
return nil
211+
}

pkg/mcp/tools/search.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -120,11 +120,11 @@ func (e *appNameSearchExecutor) execute(ctx consolectx.Context, keyword, mesh st
120120
func (e *appNameSearchExecutor) buildResult(pagedResult *model.SearchPaginationResult, keyword string, pageSize, pageNumber int) map[string]any {
121121
apps := extractGlobalApplications(pagedResult)
122122
return map[string]any{
123-
"keyword": keyword,
124-
"pageSize": pageSize,
125-
"pageNumber": pageNumber,
123+
"keyword": keyword,
124+
"pageSize": pageSize,
125+
"pageNumber": pageNumber,
126126
"applications": apps,
127-
"totalCount": len(apps),
127+
"totalCount": len(apps),
128128
}
129129
}
130130

pkg/mcp/tools/service.go

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
package tools
1919

2020
import (
21+
"fmt"
22+
2123
consolectx "github.com/apache/dubbo-admin/pkg/console/context"
2224
"github.com/apache/dubbo-admin/pkg/console/model"
2325
"github.com/apache/dubbo-admin/pkg/console/service"
@@ -46,8 +48,8 @@ func SearchServices(ctx consolectx.Context, args map[string]any) (*common.ToolRe
4648
return buildServiceSearchResult(result, keywords, mesh, pageSize, pageNumber)
4749
}
4850

49-
// GetServiceDetail 获取服务详情
50-
func GetServiceDetail(ctx consolectx.Context, args map[string]any) (*common.ToolResult, error) {
51+
// GetServiceDistribution 获取服务关联的应用分布
52+
func GetServiceDistribution(ctx consolectx.Context, args map[string]any) (*common.ToolResult, error) {
5153
helper := common.NewArgsHelper(args)
5254
serviceName := helper.GetString("serviceName", "")
5355

@@ -156,14 +158,38 @@ func extractServices(result *model.SearchPaginationResult) ([]any, int) {
156158

157159
resultSlice := make([]any, 0, len(services))
158160
for _, svc := range services {
159-
if svc != nil {
160-
resultSlice = append(resultSlice, map[string]any{
161-
"serviceName": svc.ServiceName,
162-
"version": svc.Version,
163-
"group": svc.Group,
164-
"consumerAppName": svc.ConsumerAppName,
165-
})
161+
if svc == nil {
162+
continue
166163
}
164+
resultSlice = append(resultSlice, map[string]any{
165+
"serviceName": svc.ServiceName,
166+
"version": svc.Version,
167+
"group": svc.Group,
168+
"consumerAppName": svc.ConsumerAppName,
169+
})
167170
}
168171
return resultSlice, int(result.PageInfo.Total)
169172
}
173+
174+
// GetServiceDetail 获取服务详情
175+
func GetServiceDetail(ctx consolectx.Context, args map[string]any) (*common.ToolResult, error) {
176+
helper := common.NewArgsHelper(args)
177+
serviceName := helper.GetString("serviceName", "")
178+
if serviceName == "" {
179+
return common.ErrorResult(fmt.Errorf("required parameter 'serviceName' is missing")), nil
180+
}
181+
182+
req := &model.ServiceDetailReq{
183+
ServiceName: serviceName,
184+
Version: helper.GetString("version", ""),
185+
Group: helper.GetString("group", ""),
186+
Mesh: common.GetMeshArg(ctx, args),
187+
}
188+
189+
detail, err := service.GetServiceDetail(ctx, req)
190+
if err != nil {
191+
return common.ErrorResult(err), nil
192+
}
193+
194+
return common.JsonResult(detail)
195+
}

0 commit comments

Comments
 (0)