Skip to content

Commit e93def0

Browse files
committed
Support custom categories for plugin-contributed MCP tools
1 parent 718f906 commit e93def0

5 files changed

Lines changed: 77 additions & 1 deletion

File tree

‎api/src/main/java/run/halo/mcpserver/api/McpToolDefinition.java‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ public final class McpToolDefinition {
1212
private final String description;
1313
private final String displayTitle;
1414
private final String displayDescription;
15+
private final String category;
1516
private final Map<String, Object> inputSchema;
1617
private final Map<String, Object> outputSchema;
1718
private final McpToolAnnotations annotations;
@@ -32,6 +33,8 @@ private McpToolDefinition(Builder builder) {
3233
this.description = Objects.requireNonNullElse(builder.description, "");
3334
this.displayTitle = builder.displayTitle == null ? this.title : builder.displayTitle;
3435
this.displayDescription = Objects.requireNonNullElse(builder.displayDescription, this.description);
36+
this.category = builder.category == null || builder.category.isBlank()
37+
? "PLUGIN" : builder.category.trim();
3538
this.inputSchema = copySchema(builder.inputSchema);
3639
this.outputSchema = builder.outputSchema == null ? null : copySchema(builder.outputSchema);
3740
this.annotations = Objects.requireNonNullElse(
@@ -70,6 +73,11 @@ public String displayDescription() {
7073
return displayDescription;
7174
}
7275

76+
/** Business category within the plugin in Halo's management UI. Defaults to PLUGIN. */
77+
public String category() {
78+
return category;
79+
}
80+
7381
public Map<String, Object> inputSchema() {
7482
return inputSchema;
7583
}
@@ -100,6 +108,7 @@ public static final class Builder {
100108
private String description;
101109
private String displayTitle;
102110
private String displayDescription;
111+
private String category;
103112
private Map<String, Object> inputSchema;
104113
private Map<String, Object> outputSchema;
105114
private McpToolAnnotations annotations;
@@ -131,6 +140,11 @@ public Builder displayDescription(String displayDescription) {
131140
return this;
132141
}
133142

143+
public Builder category(String category) {
144+
this.category = category;
145+
return this;
146+
}
147+
134148
public Builder inputSchema(Map<String, Object> inputSchema) {
135149
this.inputSchema = inputSchema;
136150
return this;

‎dev/dev.md‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ public final class ExportToolProvider implements McpToolProvider {
107107
.description("Export plugin data.")
108108
.displayTitle("导出数据")
109109
.displayDescription("导出插件数据。")
110+
.category("数据管理")
110111
.inputSchema(Map.of(
111112
"type", "object",
112113
"properties", Map.of(),
@@ -134,6 +135,17 @@ Provider 只声明本地 `snake_case` 名称。MCP Server 会根据 Provider 的
134135
和 `displayDescription` 只用于 Halo 的 MCP 管理界面,因此插件可以提供中文管理文案,而不
135136
改变协议描述;省略时,管理界面会回退到协议字段。
136137

138+
### 插件内工具分类
139+
140+
可通过 `.category("数据管理")` 设置工具在插件内的业务分类。管理页面与访问密钥的工具选择器
141+
均按「所属插件 → 分类」展示;自定义分类字符串直接作为显示名称,可使用中文。
142+
省略、空字符串或纯空白值使用默认分类 `PLUGIN`(界面显示「插件工具」),自定义值会去除首尾空白。
143+
同一插件的多个 Provider 可以使用相同分类;仅拆分 Provider 不会自动生成分类。
144+
145+
分类只影响管理界面,不改变协议工具名、输入输出 Schema 或访问密钥的逐工具授权。
146+
此字段属于当前开发分支新增 API,已发布的 `api:1.0.0` 不包含它;使用分类的 Provider
147+
需要搭配包含该字段的 MCP Server,并使用本地 API JAR 或包含该改动的 SNAPSHOT 编译。
148+
137149
## 按条件注册 Provider
138150

139151
通过配置类注册 Provider Bean,并用 API 类名作为启用条件。必须使用字符串形式,因为

‎src/main/java/run/halo/mcpserver/McpToolCatalog.java‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ private static ToolDescriptor descriptor(RegisteredTool tool, ToolSource source)
128128
definition.displayDescription(),
129129
definition.inputSchema(),
130130
definition.outputSchema(),
131-
"PLUGIN",
131+
definition.category(),
132132
definition.annotations().readOnlyHint(),
133133
definition.annotations().destructiveHint(),
134134
true,

‎src/test/java/run/halo/mcpserver/McpToolCatalogTest.java‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ void usesProviderDisplayCopyOnlyForTheConsoleCatalog() {
7878
.description("Export plugin data.")
7979
.displayTitle("导出数据")
8080
.displayDescription("导出插件数据。")
81+
.category("数据管理")
8182
.inputSchema(Map.of("type", "object", "properties", Map.of()))
8283
.outputSchema(Map.of(
8384
"type", "object",
@@ -97,6 +98,7 @@ void usesProviderDisplayCopyOnlyForTheConsoleCatalog() {
9798
assertThat(tool.name()).isEqualTo("demo__export");
9899
assertThat(tool.title()).isEqualTo("导出数据");
99100
assertThat(tool.description()).isEqualTo("导出插件数据。");
101+
assertThat(tool.category()).isEqualTo("数据管理");
100102
assertThat(tool.inputSchema()).containsEntry("type", "object");
101103
assertThat(tool.outputSchema()).containsEntry("type", "object");
102104
});
@@ -107,6 +109,32 @@ void usesProviderDisplayCopyOnlyForTheConsoleCatalog() {
107109
});
108110
}
109111

112+
@org.junit.jupiter.params.ParameterizedTest
113+
@org.junit.jupiter.params.provider.NullAndEmptySource
114+
@org.junit.jupiter.params.provider.ValueSource(strings = {" "})
115+
void defaultsUnspecifiedCategoriesToPlugin(String category) {
116+
var definition = McpToolDefinition.builder()
117+
.name("export")
118+
.category(category)
119+
.inputSchema(Map.of("type", "object", "properties", Map.of()))
120+
.permission(invocation -> Mono.just(true))
121+
.handler(invocation -> Mono.empty())
122+
.build();
123+
assertThat(definition.category()).isEqualTo("PLUGIN");
124+
}
125+
126+
@Test
127+
void trimsCustomCategory() {
128+
var definition = McpToolDefinition.builder()
129+
.name("export")
130+
.category(" 数据管理 ")
131+
.inputSchema(Map.of("type", "object", "properties", Map.of()))
132+
.permission(invocation -> Mono.just(true))
133+
.handler(invocation -> Mono.empty())
134+
.build();
135+
assertThat(definition.category()).isEqualTo("数据管理");
136+
}
137+
110138
@Test
111139
void keepsBuiltInToolsWhenContributedToolDiscoveryFails() {
112140
var protocolTool = McpSchema.Tool.builder(

‎ui/src/utils/__tests__/tool.test.ts‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,28 @@ function tool(name: string, category: string, pluginName = 'PluginMcpServer'): M
1919
}
2020

2121
describe('groupTools', () => {
22+
it('groups contributed tools by their custom categories within each plugin', () => {
23+
const groups = groupTools([
24+
tool('PluginLinks__list_links', '友链管理', 'PluginLinks'),
25+
tool('PluginLinks__create_link', '友链管理', 'PluginLinks'),
26+
tool('PluginLinks__list_feed_items', '订阅阅读', 'PluginLinks'),
27+
tool('other__list_links', '友链管理', 'OtherPlugin'),
28+
])
29+
30+
const links = groups.find(({ source }) => source.pluginName === 'PluginLinks')
31+
expect(links?.toolCount).toBe(3)
32+
expect(links?.categories).toHaveLength(2)
33+
expect(links?.categories.find(({ category }) => category === '友链管理')).toMatchObject({
34+
label: '友链管理',
35+
tools: [{ name: 'PluginLinks__list_links' }, { name: 'PluginLinks__create_link' }],
36+
})
37+
expect(links?.categories.find(({ category }) => category === '订阅阅读')).toMatchObject({
38+
label: '订阅阅读',
39+
tools: [{ name: 'PluginLinks__list_feed_items' }],
40+
})
41+
expect(groups.find(({ source }) => source.pluginName === 'OtherPlugin')?.toolCount).toBe(1)
42+
})
43+
2244
it('groups tools by source and business category', () => {
2345
const groups = groupTools([
2446
tool('halo_list_posts', 'POST'),

0 commit comments

Comments
 (0)