Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/shorten-plugin-mcp-tool-names.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": minor
---

Shorten plugin MCP tool names by dropping the redundant `plugin-` prefix and the server-name segment for single-server plugins (e.g. `mcp__plugin-foo-mcp_s__bar` becomes `mcp__foo-mcp__bar`). Existing per-tool permission approvals for plugin MCP tools will need to be re-granted after upgrading.
26 changes: 16 additions & 10 deletions packages/agent-core/src/plugin/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,9 +230,10 @@ export class PluginManager {
const out: Record<string, McpServerConfig> = {};
for (const record of this.records.values()) {
if (!record.enabled || record.state !== 'ok' || record.manifest === undefined) continue;
for (const [name, config] of Object.entries(record.manifest.mcpServers ?? {})) {
const entries = Object.entries(record.manifest.mcpServers ?? {});
for (const [name, config] of entries) {
if (!isMcpServerEnabled(record, name, config)) continue;
out[pluginMcpRuntimeName(record.id, name)] = withPluginMcpRuntime(
out[pluginMcpRuntimeName(record.id, name, entries.length)] = withPluginMcpRuntime(
withMcpServerEnabled(config, true),
record.root,
this.kimiHomeDir,
Expand Down Expand Up @@ -443,20 +444,22 @@ function isMcpServerEnabled(
}

function pluginMcpServersInfo(record: PluginRecord): readonly PluginMcpServerInfo[] {
return Object.entries(record.manifest?.mcpServers ?? {})
.map(([name, config]) => pluginMcpServerInfo(record, name, config))
const entries = Object.entries(record.manifest?.mcpServers ?? {});
return entries
.map(([name, config]) => pluginMcpServerInfo(record, name, config, entries.length))
.toSorted((a, b) => a.name.localeCompare(b.name));
}

function pluginMcpServerInfo(
record: PluginRecord,
name: string,
config: McpServerConfig,
serverCount: number,
): PluginMcpServerInfo {
if (config.transport === 'http' || config.transport === 'sse') {
return {
name,
runtimeName: pluginMcpRuntimeName(record.id, name),
runtimeName: pluginMcpRuntimeName(record.id, name, serverCount),
enabled: isMcpServerEnabled(record, name, config),
transport: config.transport,
url: config.url,
Expand All @@ -465,7 +468,7 @@ function pluginMcpServerInfo(
}
return {
name,
runtimeName: pluginMcpRuntimeName(record.id, name),
runtimeName: pluginMcpRuntimeName(record.id, name, serverCount),
enabled: isMcpServerEnabled(record, name, config),
transport: 'stdio',
command: config.command,
Expand All @@ -479,10 +482,13 @@ function withMcpServerEnabled(config: McpServerConfig, enabled: boolean): McpSer
return { ...config, enabled };
}

function pluginMcpRuntimeName(pluginId: string, serverName: string): string {
// Plugin ids cannot contain ":", so this keeps plugin/server pairs unambiguous
// even when either side contains "-".
return `plugin-${pluginId}:${serverName}`;
function pluginMcpRuntimeName(pluginId: string, serverName: string, serverCount: number): string {
// Plugin ids are unique and cannot contain ":", so a bare pluginId is
// already unambiguous when there is nothing to disambiguate (the common
// single-server case). Only append ":serverName" when a plugin declares
// more than one MCP server — that keeps cross-plugin pairs unambiguous
// even when either half contains "-" (e.g. "a-b"+"c" vs "a"+"b-c").
return serverCount > 1 ? `${pluginId}:${serverName}` : pluginId;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve a namespace for plugin MCP servers

When a single-server plugin id matches an existing user/caller MCP server name (for example both are github), this now returns the bare plugin id, and mergePluginMcpConfig() spreads plugin servers after base?.servers, so the plugin silently replaces the user's configured server for that session. The previous plugin-<id>:<server> key avoided collisions with non-plugin MCP servers; this needs either a reserved plugin namespace or explicit collision handling before omitting the server segment.

Useful? React with 👍 / 👎.

}

function withPluginMcpRuntime(
Expand Down
47 changes: 34 additions & 13 deletions packages/agent-core/test/plugin/manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -366,15 +366,15 @@ describe('PluginManager', () => {
expect(manager.info('demo')?.mcpServers).toContainEqual(
expect.objectContaining({
name: 'finance',
runtimeName: 'plugin-demo:finance',
runtimeName: 'demo:finance',
enabled: true,
command: 'finance-mcp',
}),
);
expect(manager.info('demo')?.mcpServers).toContainEqual(
expect.objectContaining({
name: 'events',
runtimeName: 'plugin-demo:events',
runtimeName: 'demo:events',
transport: 'sse',
url: 'https://example.com/sse',
}),
Expand All @@ -388,15 +388,15 @@ describe('PluginManager', () => {

expect(manager.enabledMcpServers()).toEqual(
expect.objectContaining({
'plugin-demo:finance': expect.objectContaining({
'demo:finance': expect.objectContaining({
command: 'finance-mcp',
cwd: managedRoot,
env: expect.objectContaining({ KIMI_CODE_HOME: home, KIMI_PLUGIN_ROOT: managedRoot }),
}),
'plugin-demo:docs': expect.objectContaining({
'demo:docs': expect.objectContaining({
url: 'https://example.com/mcp',
}),
'plugin-demo:events': expect.objectContaining({
'demo:events': expect.objectContaining({
transport: 'sse',
url: 'https://example.com/sse',
}),
Expand All @@ -405,7 +405,7 @@ describe('PluginManager', () => {

await manager.setMcpServerEnabled('demo', 'finance', false);

expect(manager.enabledMcpServers()).not.toHaveProperty('plugin-demo:finance');
expect(manager.enabledMcpServers()).not.toHaveProperty('demo:finance');
expect(manager.summaries()[0]).toEqual(
expect.objectContaining({
mcpServerCount: 3,
Expand Down Expand Up @@ -449,7 +449,7 @@ describe('PluginManager', () => {
);
expect(manager.enabledMcpServers()).toEqual(
expect.objectContaining({
'plugin-demo:finance': expect.objectContaining({
demo: expect.objectContaining({
command: 'finance-mcp',
enabled: true,
}),
Expand All @@ -461,19 +461,40 @@ describe('PluginManager', () => {
expect(reloaded.info('demo')?.mcpServers).toContainEqual(
expect.objectContaining({ name: 'finance', enabled: true }),
);
expect(reloaded.enabledMcpServers()).toHaveProperty('plugin-demo:finance');
expect(reloaded.enabledMcpServers()).toHaveProperty('demo');
});

it('omits the server segment when a plugin declares a single MCP server', async () => {
const home = await makeKimiHome();
const root = await makePlugin('demo', {
mcpServers: {
finance: { command: 'finance-mcp' },
},
});
const manager = new PluginManager({ kimiHomeDir: home });
await manager.load();
await manager.install(root);

expect(manager.info('demo')?.mcpServers).toContainEqual(
expect.objectContaining({ name: 'finance', runtimeName: 'demo' }),
);
expect(manager.enabledMcpServers()).toEqual(
expect.objectContaining({ demo: expect.objectContaining({ command: 'finance-mcp' }) }),
);
});

it('uses unambiguous runtime names for plugin MCP servers', async () => {
const home = await makeKimiHome();
const first = await makePlugin('a-b', {
mcpServers: {
c: { command: 'first-mcp' },
d: { command: 'first-mcp-2' },
},
});
const second = await makePlugin('a', {
mcpServers: {
'b-c': { command: 'second-mcp' },
e: { command: 'second-mcp-2' },
},
});
const manager = new PluginManager({ kimiHomeDir: home });
Expand All @@ -482,20 +503,20 @@ describe('PluginManager', () => {
await manager.install(second);

expect(manager.info('a-b')?.mcpServers).toContainEqual(
expect.objectContaining({ name: 'c', runtimeName: 'plugin-a-b:c' }),
expect.objectContaining({ name: 'c', runtimeName: 'a-b:c' }),
);
expect(manager.info('a')?.mcpServers).toContainEqual(
expect.objectContaining({ name: 'b-c', runtimeName: 'plugin-a:b-c' }),
expect.objectContaining({ name: 'b-c', runtimeName: 'a:b-c' }),
);

const servers = manager.enabledMcpServers();
expect(servers).toEqual(
expect.objectContaining({
'plugin-a-b:c': expect.objectContaining({ command: 'first-mcp' }),
'plugin-a:b-c': expect.objectContaining({ command: 'second-mcp' }),
'a-b:c': expect.objectContaining({ command: 'first-mcp' }),
'a:b-c': expect.objectContaining({ command: 'second-mcp' }),
}),
);
expect(Object.keys(servers)).toHaveLength(2);
expect(Object.keys(servers)).toHaveLength(4);
});

it('enabledMcpServers() excludes disabled plugins', async () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/agent-core/test/rpc/plugins-rpc.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ oauth = { storage = "file", key = "oauth/kimi-code-env-1234", oauth_host = "http
}
).mergePluginMcpConfig(undefined);

expect(mcpConfig.servers['plugin-kimi-datasource:data']?.env).toEqual(
expect(mcpConfig.servers['kimi-datasource']?.env).toEqual(
expect.objectContaining({
KIMI_CODE_BASE_URL: 'https://api.dev.example.test/coding/v1',
KIMI_CODE_OAUTH_HOST: 'https://auth.dev.example.test',
Expand Down
2 changes: 1 addition & 1 deletion plugins/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"id": "kimi-datasource",
"tier": "official",
"displayName": "Kimi Datasource",
"version": "3.2.0",
"version": "3.2.1",
"description": "Official datasource workflows.",
"keywords": ["data", "mcp"],
"source": "./official/kimi-datasource"
Expand Down
4 changes: 4 additions & 0 deletions plugins/official/kimi-datasource/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## 3.2.1 - 2026-07-11

- Update the tool-name examples in this skill to match the shorter `mcp__kimi-datasource__*` naming (previously `mcp__plugin-kimi-datasource_data__*`).

## 3.2.0 - 2026-06-10

- Add the `yuandian_law` data source (元典法律数据库) for Chinese laws/regulations and judicial case search.
Expand Down
14 changes: 7 additions & 7 deletions plugins/official/kimi-datasource/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name: kimi-datasource
description: |
Universal data-source assistant. Use this skill when the user wants external structured data such as stocks, financial reports, technical indicators, A-share/HK/US markets, global macroeconomics, Chinese enterprise registry information, arXiv papers, Google Scholar results, or Chinese laws/regulations and judicial cases.
This plugin exposes tools via MCP server `plugin-kimi-datasource_data`; call them in the flow `mcp__plugin-kimi-datasource_data__get_data_source_desc` → `mcp__plugin-kimi-datasource_data__call_data_source_tool`.
This plugin exposes tools via MCP server `kimi-datasource`; call them in the flow `mcp__kimi-datasource__get_data_source_desc` → `mcp__kimi-datasource__call_data_source_tool`.
---

# kimi-datasource — 通用数据源助手
Expand All @@ -11,8 +11,8 @@ description: |

本 skill 使用 datasource MCP server 注册的两个工具,不要通过 Bash 手动执行脚本:

- `mcp__plugin-kimi-datasource_data__get_data_source_desc`
- `mcp__plugin-kimi-datasource_data__call_data_source_tool`
- `mcp__kimi-datasource__get_data_source_desc`
- `mcp__kimi-datasource__call_data_source_tool`

这两个工具由 Kimi Code 托管执行,参数直接按 tool schema 传 JSON。

Expand Down Expand Up @@ -52,24 +52,24 @@ description: |
### 例 1:用户问"茅台最近一年走势"

1. 股票走势 → `stock_finance_data`
2. 调用 `mcp__plugin-kimi-datasource_data__get_data_source_desc`,参数 `{"name":"stock_finance_data"}`
2. 调用 `mcp__kimi-datasource__get_data_source_desc`,参数 `{"name":"stock_finance_data"}`

3. 从文档里找到"获取历史价格"那个 API,看它要 `ticker / start_date / end_date / file_path` 等
4. 用 web_search 核对 → 茅台 = `600519.SH`
5. 调用 `mcp__plugin-kimi-datasource_data__call_data_source_tool`,参数形如 `{"data_source_name":"stock_finance_data","api_name":"<文档里写的 api>","params":{"ticker":"600519.SH","start_date":"...","end_date":"...","file_path":"/tmp/mao_1y.csv"}}`
5. 调用 `mcp__kimi-datasource__call_data_source_tool`,参数形如 `{"data_source_name":"stock_finance_data","api_name":"<文档里写的 api>","params":{"ticker":"600519.SH","start_date":"...","end_date":"...","file_path":"/tmp/mao_1y.csv"}}`

### 例 2:用户问"找几篇 retrieval augmented generation 的综述"

1. 论文搜索 → `arxiv`(或 `scholar`,arxiv 更适合预印本,scholar 引用更全)
2. 调用 `mcp__plugin-kimi-datasource_data__get_data_source_desc`,参数 `{"name":"arxiv"}`
2. 调用 `mcp__kimi-datasource__get_data_source_desc`,参数 `{"name":"arxiv"}`

3. 从文档里找到搜索类 API,看它要 `query / file_path / max_results` 等
4. 执行 `call_data_source_tool`

### 例 3:用户问"字节跳动有哪些股东"

1. 企业工商 → `tianyancha`
2. 调用 `mcp__plugin-kimi-datasource_data__get_data_source_desc`,参数 `{"name":"tianyancha"}`
2. 调用 `mcp__kimi-datasource__get_data_source_desc`,参数 `{"name":"tianyancha"}`

3. 注意:tianyancha 的 API 是动态注册的,文档会指引你**先用搜索类接口找到合适的 API 名,再调用**
4. **必须使用企业全称**("北京字节跳动科技有限公司"),不要用简称。不知道全称就先用 tianyancha 文档里的"公司搜索"接口查
Expand Down
2 changes: 1 addition & 1 deletion plugins/official/kimi-datasource/kimi.plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "kimi-datasource",
"version": "3.2.0",
"version": "3.2.1",
"description": "Finance, macro, enterprise, academic, and legal data tools for Kimi Code.",
"keywords": ["finance", "data-source", "mcp", "legal"],
"mcpServers": {
Expand Down