mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-07-09 17:29:12 +00:00
feat: add plugin manager and official plugins (#119)
* feat: add plugin manager and official plugins * fix(agent-core): honor plugin capability overrides * fix: restrict plugin zip root detection * Update apps/kimi-code/src/constant/app.ts Co-authored-by: liruifengv <liruifeng1024@gmail.com> Signed-off-by: qer <wbxl2000@outlook.com> --------- Signed-off-by: qer <wbxl2000@outlook.com> Co-authored-by: liruifengv <liruifeng1024@gmail.com>
This commit is contained in:
parent
2c7a8cc010
commit
ebf6e8181e
75 changed files with 7327 additions and 23 deletions
24
plugins/marketplace.json
Normal file
24
plugins/marketplace.json
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"version": "1",
|
||||
"plugins": [
|
||||
{
|
||||
"id": "kimi-datasource",
|
||||
"tier": "official",
|
||||
"displayName": "Kimi Datasource",
|
||||
"version": "3.0.0",
|
||||
"description": "Official datasource workflows.",
|
||||
"keywords": ["data", "mcp"],
|
||||
"source": "./official/kimi-datasource"
|
||||
},
|
||||
{
|
||||
"id": "superpowers",
|
||||
"tier": "curated",
|
||||
"displayName": "Superpowers",
|
||||
"version": "5.1.0",
|
||||
"description": "Planning, TDD, debugging, and delivery workflows for coding agents.",
|
||||
"homepage": "https://github.com/obra/superpowers",
|
||||
"keywords": ["skills", "planning", "tdd", "debugging", "code-review"],
|
||||
"source": "./curated/superpowers"
|
||||
}
|
||||
]
|
||||
}
|
||||
6
plugins/official/kimi-datasource/.gitignore
vendored
Normal file
6
plugins/official/kimi-datasource/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
# 编辑器
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
150
plugins/official/kimi-datasource/SKILL.md
Normal file
150
plugins/official/kimi-datasource/SKILL.md
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
---
|
||||
name: kimi-datasource
|
||||
description: |
|
||||
通用数据源助手。当用户要查股票/财报/技术指标/全球宏观经济/中国企业工商/学术论文这类外部数据时,使用这个 skill。
|
||||
本 plugin 通过 MCP server `plugin-kimi-datasource-data` 提供工具;优先调用 `mcp__plugin-kimi-datasource-data__query_stock`、`mcp__plugin-kimi-datasource-data__get_data_source_desc`、`mcp__plugin-kimi-datasource-data__call_data_source_tool`。
|
||||
---
|
||||
|
||||
# kimi-datasource — 通用数据源助手
|
||||
|
||||
## 0. 调用方式
|
||||
|
||||
本 skill 使用 datasource MCP server 注册的三个工具,不要通过 Bash 手动执行脚本:
|
||||
|
||||
- `mcp__plugin-kimi-datasource-data__query_stock`
|
||||
- `mcp__plugin-kimi-datasource-data__get_data_source_desc`
|
||||
- `mcp__plugin-kimi-datasource-data__call_data_source_tool`
|
||||
|
||||
这三个工具由 Kimi Code 托管执行,参数直接按 tool schema 传 JSON。
|
||||
|
||||
工具会读取 `$KIMI_CODE_HOME/credentials/kimi-code.json`。如果没有登录凭据,让用户先在 Kimi Code 里执行 `/login`。
|
||||
|
||||
## 1. 这个 skill 提供什么能力
|
||||
|
||||
本 plugin 后面挂了 6 个外部数据源。每一行的"数据源名"就是传给 `get_data_source_desc` 的 `name`。
|
||||
|
||||
| 能力域 | 数据源名 | 典型问题 |
|
||||
|---|---|---|
|
||||
| **A股 / 港股 / 美股 行情和财务** | `stock_finance_data` | "茅台现在多少钱"、"宁德时代 2024 年财报"、"腾讯股东"、"杭州的人工智能股票" |
|
||||
| **Yahoo Finance 全球金融** | `yahoo_finance` | "苹果分析师评级"、"AAPL 期权链"、"标普 500 历年价格" |
|
||||
| **世界银行宏观经济** | `world_bank_open_data` | "中国历年 GDP"、"印度通胀率"、"各国人口增长对比" |
|
||||
| **中国企业工商信息** | `tianyancha` | "字节跳动股东"、"比亚迪司法风险"、"宁德时代专利" |
|
||||
| **arXiv 论文预印本** | `arxiv` | "找 RAG 综述"、"下载 2406.xxxxx" |
|
||||
| **Google Scholar 学术搜索** | `scholar` | "Hinton 最新论文"、"transformer 综述高引文献" |
|
||||
|
||||
**不支持的能力**:通用 Web 搜索 / 实时新闻。问到这类问题,告诉用户当前数据源不覆盖。
|
||||
|
||||
## 2. 标准工作流:`get_data_source_desc` → `call_data_source_tool`
|
||||
|
||||
后端可用 API 经常会调整,**这份 skill 故意不抄具体的 API 名和参数表**。每次调用前你都应当现场问数据源:"你都有什么接口?"
|
||||
|
||||
```
|
||||
1. 根据用户问题,从上表挑出一个 data_source_name
|
||||
2. 执行 get_data_source_desc,读取该数据源的 Markdown 文档
|
||||
3. 仔细读返回的 Markdown,里面列了:
|
||||
- 该数据源整体说明(含 ticker 格式、全局约束)
|
||||
- 每个 API 的描述 / 必填参数 / 可选参数 / 默认值 / 取值范围
|
||||
4. 选最匹配的 API,按文档拼 params
|
||||
5. 执行 call_data_source_tool
|
||||
6. 读返回结果给用户
|
||||
```
|
||||
|
||||
### 例 1:用户问"茅台最近一年走势"
|
||||
|
||||
1. 股票走势 → `stock_finance_data`
|
||||
2. 调用 `mcp__plugin-kimi-datasource-data__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"}}`
|
||||
|
||||
### 例 2:用户问"找几篇 retrieval augmented generation 的综述"
|
||||
|
||||
1. 论文搜索 → `arxiv`(或 `scholar`,arxiv 更适合预印本,scholar 引用更全)
|
||||
2. 调用 `mcp__plugin-kimi-datasource-data__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"}`
|
||||
|
||||
3. 注意:tianyancha 的 API 是动态注册的,文档会指引你**先用搜索类接口找到合适的 API 名,再调用**
|
||||
4. **必须使用企业全称**("北京字节跳动科技有限公司"),不要用简称。不知道全称就先用 tianyancha 文档里的"公司搜索"接口查
|
||||
|
||||
## 3. 调用前的几条铁律
|
||||
|
||||
### 3.1 股票代码必须核对,不能凭记忆猜
|
||||
|
||||
A 股 `.SH/.SZ/.BJ`,港股 `.HK`,美股 `.US` 等。用户通常只说中文名("茅台"、"宁德时代"、"腾讯"),不会给代码。
|
||||
|
||||
**调任何股票相关 API 前**,先用 `web_search` / `WebSearch` 一类联网工具确认正确代码 + 后缀。
|
||||
|
||||
如果当前环境没有任何联网工具,**让用户亲口确认代码**,不要硬猜。错了的话接口会静默返回错数据或空数据。
|
||||
|
||||
### 3.2 企业相关查询必须用全称
|
||||
|
||||
`tianyancha` 拒收"特斯拉"、"网易"、"腾讯"这种简称,必须给"北京特斯拉销售有限公司"这种全名。不知道全名时,先调它的公司搜索 API。
|
||||
|
||||
### 3.3 多数 API 需要 `file_path`
|
||||
|
||||
绝大部分数据源 API 把完整结果以 CSV 形式写到 `file_path`。漏传会报 `Missing required parameters: file_path`。不知道传啥时,给一个 `/tmp/<场景>_<时间戳>.csv` 即可。
|
||||
|
||||
### 3.4 一次调用不要堆太多 ticker
|
||||
|
||||
`stock_finance_data` 的实时接口最多 3 个 ticker,历史接口最多 10 个。超过会被截断或报错。多了就分批调。
|
||||
|
||||
## 4. 怎么读返回结果
|
||||
|
||||
`call_data_source_tool` 和 `query_stock` 的 stdout 一般含两段:
|
||||
|
||||
1. **`data_preview`**:CSV 头 + 前几行(通常 1~3 行),方便你直接答简单问题
|
||||
2. **`CSV 数据已写入:/tmp/xxx.csv`**:完整数据落盘路径
|
||||
|
||||
策略:
|
||||
- 用户只问"XX 现在多少钱"、"中国 2023 GDP 多少"这种单值 → `data_preview` 一般够,直接答
|
||||
- 用户要画图、对比、算盈亏、列清单 → 用 `Read` 工具把 CSV 读出来再处理
|
||||
- 混合 A+港股查询时服务端会自动把 CSV 拆成 `_a.csv` / `_hk.csv` 两份,原 `file_path` 那个文件不存在
|
||||
|
||||
如果接口返回失败,提示文字一般会写明原因(参数不对 / 不支持 / 数据空等)。把人话原因反馈给用户,不要硬走第二次。
|
||||
|
||||
## 5. `query_stock` — 实时行情快捷命令
|
||||
|
||||
对**实时**类的常见股票查询,可以**不走** `get_data_source_desc → call_data_source_tool` 这套流程,直接用 `query_stock`,省一次调用。它等价于 `stock_finance_data` 的实时接口子集。
|
||||
|
||||
调用 `mcp__plugin-kimi-datasource-data__query_stock`,参数形如 `{"ticker":"600519.SH","type":"realtime_price","file_path":"/tmp/stock_600519.csv"}`。
|
||||
|
||||
适用场景(且仅限):
|
||||
- 看当前价 / 当日分钟 K 线(`type=realtime_price`)
|
||||
- 看实时技术指标 MA/MACD/KDJ/RSI/BOLL 等(`type=realtime_tech`,**仅 A 股**,港股/美股会报错)
|
||||
- 开盘摘要(`type=open_summary`)
|
||||
- 收盘摘要(`type=close_summary`,**港股盘后看当天数据必须用这个**,`realtime_price` 拿不到)
|
||||
|
||||
涉及**历史日 K / 财报 / 公告 / 股东 / 财务指标 / 选股 / 业务分部 / 预测**等任何"非实时"的股票查询,走标准 `get_data_source_desc → call_data_source_tool` 流程,不要在 `query_stock` 上硬凑。
|
||||
|
||||
## 6. `watchlist.json` — 用户自选股
|
||||
|
||||
`${KIMI_SKILL_DIR}/watchlist.json` 是用户的自选股列表。用户问"看一下我的自选股"时,读这个文件然后按每 3 只一组分批执行 `query_stock`。
|
||||
|
||||
格式:
|
||||
|
||||
```json
|
||||
[
|
||||
{"code": "600519.SH", "name": "贵州茅台"},
|
||||
{"code": "0700.HK", "name": "腾讯控股", "hold_cost": 350.5, "hold_quantity": 100}
|
||||
]
|
||||
```
|
||||
|
||||
- `code` 和 `name` 必填;`hold_cost` 和 `hold_quantity` 可选
|
||||
- 两者都有时顺便算盈亏:`(当前价 - hold_cost) * hold_quantity`
|
||||
- 用户说"帮我加 XX 到自选股"时:先 web_search 核对代码,再追加到 JSON 数组
|
||||
|
||||
## 7. 注意事项
|
||||
|
||||
- **不要凭记忆猜股票代码 / 企业全称**。错代码会让接口静默返回错数据,用户察觉不到
|
||||
- **不要在没读 desc 的情况下硬传 `api_name`**。后端会报 `API_NOT_FOUND`。除非这次会话里你已经读过该数据源的 desc 并记得参数
|
||||
- **不要给投资建议**。给完数据加一句"AI 生成,不构成投资建议"即可
|
||||
- **不要在 `query_stock` 上塞历史/财报查询**。它只覆盖实时类,别的会失败
|
||||
- 如果某个数据源接口返回的报错明显是后端 bug(参数 schema 自相矛盾、内部 Python 报错等),**汇报错误给用户,不要硬试**——这种 bug 我们这边修不了,要后端服务侧改
|
||||
446
plugins/official/kimi-datasource/bin/kimi-datasource.mjs
Executable file
446
plugins/official/kimi-datasource/bin/kimi-datasource.mjs
Executable file
|
|
@ -0,0 +1,446 @@
|
|||
#!/usr/bin/env node
|
||||
// Stdio MCP server for kimi-datasource.
|
||||
//
|
||||
// Speaks newline-delimited JSON-RPC 2.0 on stdin/stdout per the MCP "stdio"
|
||||
// transport. Implements the minimal surface the Kimi Code host calls:
|
||||
// - initialize
|
||||
// - notifications/initialized
|
||||
// - tools/list
|
||||
// - tools/call
|
||||
// - ping
|
||||
//
|
||||
// Business logic (API call, credentials, headers) is unchanged from the
|
||||
// previous one-shot CLI; only the transport changed.
|
||||
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
||||
import { arch, homedir, hostname, release, type } from 'node:os';
|
||||
import path from 'node:path';
|
||||
import readline from 'node:readline';
|
||||
|
||||
const VERSION = '3.0.0';
|
||||
const API_URL = process.env.KIMI_DATASOURCE_API_URL ?? 'https://api.kimi.com/coding/v1/tools';
|
||||
const REQUEST_TIMEOUT_MS = 30_000;
|
||||
const PROTOCOL_VERSION = '2025-06-18';
|
||||
const VALID_STOCK_QUERY_TYPES = new Set([
|
||||
'realtime_price',
|
||||
'realtime_tech',
|
||||
'open_summary',
|
||||
'close_summary',
|
||||
]);
|
||||
|
||||
const TOOLS = [
|
||||
{
|
||||
name: 'query_stock',
|
||||
description:
|
||||
'Query realtime stock price, realtime technical indicators, open summaries, or close summaries for up to 3 tickers.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
ticker: {
|
||||
type: 'string',
|
||||
description: 'Ticker code list separated by commas, for example 600519.SH or 0700.HK.',
|
||||
},
|
||||
type: {
|
||||
type: 'string',
|
||||
enum: ['realtime_price', 'realtime_tech', 'open_summary', 'close_summary'],
|
||||
description: 'Realtime stock query type.',
|
||||
},
|
||||
time: {
|
||||
type: 'string',
|
||||
description: 'Optional time parameter for supported realtime endpoints.',
|
||||
},
|
||||
file_path: {
|
||||
type: 'string',
|
||||
description: 'Optional CSV output path. When omitted, the tool chooses a temporary path.',
|
||||
},
|
||||
},
|
||||
required: ['ticker'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'get_data_source_desc',
|
||||
description:
|
||||
'Get the current API documentation for one Kimi data source before calling a specific API.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
enum: [
|
||||
'stock_finance_data',
|
||||
'yahoo_finance',
|
||||
'world_bank_open_data',
|
||||
'tianyancha',
|
||||
'arxiv',
|
||||
'scholar',
|
||||
],
|
||||
description: 'Data source name.',
|
||||
},
|
||||
},
|
||||
required: ['name'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'call_data_source_tool',
|
||||
description: 'Call one API from a Kimi data source after reading get_data_source_desc.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
data_source_name: {
|
||||
type: 'string',
|
||||
description: 'Data source name returned or documented by get_data_source_desc.',
|
||||
},
|
||||
api_name: {
|
||||
type: 'string',
|
||||
description: 'API name from the data source description.',
|
||||
},
|
||||
params: {
|
||||
type: 'object',
|
||||
description: 'API parameters that match the data source description.',
|
||||
},
|
||||
},
|
||||
required: ['data_source_name', 'api_name', 'params'],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const HANDLERS = {
|
||||
query_stock: {
|
||||
method: 'get_stock_realtime_price',
|
||||
buildParams(args) {
|
||||
const ticker = requiredString(args, 'ticker');
|
||||
const tickerList = ticker
|
||||
.split(',')
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean);
|
||||
if (tickerList.length === 0) throw new Error('Missing required argument: ticker.');
|
||||
if (tickerList.length > 3) {
|
||||
throw new Error('ticker accepts at most 3 values separated by commas.');
|
||||
}
|
||||
|
||||
const queryType = optionalString(args, 'type') ?? 'realtime_price';
|
||||
if (!VALID_STOCK_QUERY_TYPES.has(queryType)) {
|
||||
throw new Error(
|
||||
`type must be one of ${JSON.stringify([...VALID_STOCK_QUERY_TYPES])}; received: ${queryType}`,
|
||||
);
|
||||
}
|
||||
|
||||
const params = {
|
||||
ticker,
|
||||
type: queryType,
|
||||
file_path: optionalString(args, 'file_path') ?? defaultStockFilePath(ticker, queryType),
|
||||
};
|
||||
const time = optionalString(args, 'time');
|
||||
if (time !== undefined) params.time = time;
|
||||
return params;
|
||||
},
|
||||
format(text, params) {
|
||||
return `${text}\n\nCSV data written to: ${params.file_path}`;
|
||||
},
|
||||
},
|
||||
get_data_source_desc: {
|
||||
method: 'get_data_source_desc',
|
||||
buildParams(args) {
|
||||
return { name: requiredString(args, 'name') };
|
||||
},
|
||||
},
|
||||
call_data_source_tool: {
|
||||
method: 'call_data_source_tool',
|
||||
buildParams(args) {
|
||||
return {
|
||||
data_source_name: requiredString(args, 'data_source_name'),
|
||||
api_name: requiredString(args, 'api_name'),
|
||||
params: requiredObject(args, 'params'),
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
async function handleRequest(message) {
|
||||
const { method, id, params } = message;
|
||||
switch (method) {
|
||||
case 'initialize':
|
||||
return {
|
||||
protocolVersion: PROTOCOL_VERSION,
|
||||
capabilities: { tools: {} },
|
||||
serverInfo: { name: 'kimi-datasource', version: VERSION },
|
||||
};
|
||||
case 'ping':
|
||||
return {};
|
||||
case 'tools/list':
|
||||
return { tools: TOOLS };
|
||||
case 'tools/call':
|
||||
return runTool(params);
|
||||
default:
|
||||
throw jsonRpcError(-32601, `Method not found: ${method}`, { id });
|
||||
}
|
||||
}
|
||||
|
||||
async function runTool(params) {
|
||||
const name = params?.name;
|
||||
const args = params?.arguments ?? {};
|
||||
const handler = HANDLERS[name];
|
||||
if (handler === undefined) {
|
||||
return {
|
||||
content: [{ type: 'text', text: `Unknown tool: ${String(name)}` }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
try {
|
||||
const built = handler.buildParams(args);
|
||||
const response = await callKimiTool(handler.method, built);
|
||||
const text = extractText(response);
|
||||
const formatted = (handler.format?.(text, built) ?? text).trim();
|
||||
return { content: [{ type: 'text', text: formatted }] };
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
return {
|
||||
content: [{ type: 'text', text: message }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function resolveKimiHome() {
|
||||
const explicit = process.env.KIMI_CODE_HOME?.trim();
|
||||
return explicit && explicit.length > 0 ? explicit : path.join(homedir(), '.kimi-code');
|
||||
}
|
||||
|
||||
async function loadAccessToken() {
|
||||
const kimiHome = resolveKimiHome();
|
||||
const credentialsFile = path.join(kimiHome, 'credentials', 'kimi-code.json');
|
||||
let parsed;
|
||||
try {
|
||||
parsed = JSON.parse(await readFile(credentialsFile, 'utf8'));
|
||||
} catch (err) {
|
||||
if (isNotFound(err)) {
|
||||
throw new Error(
|
||||
`Kimi Code credentials file not found: ${credentialsFile}\nRun /login in Kimi Code first.`,
|
||||
);
|
||||
}
|
||||
if (err instanceof SyntaxError) {
|
||||
throw new Error(`Failed to parse Kimi Code credentials file: ${err.message}`);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (!isRecord(parsed)) {
|
||||
throw new Error(`Invalid Kimi Code credentials file: ${credentialsFile}`);
|
||||
}
|
||||
const token = typeof parsed.access_token === 'string' ? parsed.access_token : '';
|
||||
if (token.length === 0) {
|
||||
throw new Error('Kimi Code credentials do not contain access_token. Run /login again.');
|
||||
}
|
||||
const expiresAt = typeof parsed.expires_at === 'number' ? parsed.expires_at : 0;
|
||||
if (expiresAt > 0 && expiresAt <= Math.floor(Date.now() / 1000)) {
|
||||
throw new Error('Kimi Code access_token has expired. Run /login again and retry.');
|
||||
}
|
||||
return { kimiHome, token };
|
||||
}
|
||||
|
||||
async function callKimiTool(method, params) {
|
||||
const { kimiHome, token } = await loadAccessToken();
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => {
|
||||
controller.abort();
|
||||
}, REQUEST_TIMEOUT_MS);
|
||||
try {
|
||||
const response = await fetch(API_URL, {
|
||||
method: 'POST',
|
||||
headers: await buildHeaders(kimiHome, token),
|
||||
body: JSON.stringify({ method, params }),
|
||||
signal: controller.signal,
|
||||
});
|
||||
const text = await response.text();
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status} error: ${text}`);
|
||||
}
|
||||
try {
|
||||
return JSON.parse(text);
|
||||
} catch {
|
||||
return text;
|
||||
}
|
||||
} catch (err) {
|
||||
if (err instanceof DOMException && err.name === 'AbortError') {
|
||||
throw new Error(`Request timed out after ${REQUEST_TIMEOUT_MS / 1000} seconds.`);
|
||||
}
|
||||
throw err;
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
}
|
||||
|
||||
async function buildHeaders(kimiHome, token) {
|
||||
return {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
'X-Msh-Tool-Call-Id': randomUUID(),
|
||||
'X-Msh-Platform': asciiHeader(process.env.KIMI_MSH_PLATFORM ?? 'kimi-code-cli'),
|
||||
'X-Msh-Version': asciiHeader(process.env.KIMI_MSH_VERSION ?? VERSION),
|
||||
'X-Msh-Device-Name': asciiHeader(process.env.KIMI_MSH_DEVICE_NAME ?? hostname()),
|
||||
'X-Msh-Device-Model': asciiHeader(process.env.KIMI_MSH_DEVICE_MODEL ?? deviceModel()),
|
||||
'X-Msh-Os-Version': asciiHeader(process.env.KIMI_MSH_OS_VERSION ?? release()),
|
||||
'X-Msh-Device-Id': asciiHeader(process.env.KIMI_MSH_DEVICE_ID ?? (await createDeviceId(kimiHome))),
|
||||
'User-Agent': `kimi-datasource/${VERSION}`,
|
||||
};
|
||||
}
|
||||
|
||||
async function createDeviceId(kimiHome) {
|
||||
const deviceIdPath = path.join(kimiHome, 'device_id');
|
||||
try {
|
||||
const existing = (await readFile(deviceIdPath, 'utf8')).trim();
|
||||
if (existing.length > 0) return existing;
|
||||
} catch {
|
||||
// Fall through to create a best-effort local device id.
|
||||
}
|
||||
|
||||
const id = randomUUID();
|
||||
try {
|
||||
await mkdir(kimiHome, { recursive: true, mode: 0o700 });
|
||||
await writeFile(deviceIdPath, `${id}\n`, { encoding: 'utf8', mode: 0o600 });
|
||||
} catch {
|
||||
// Headers can still use the in-memory id if the file cannot be written.
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
function deviceModel() {
|
||||
const os = type();
|
||||
const osVersion = release();
|
||||
const osArch = arch();
|
||||
if (os === 'Darwin') return `macOS ${osVersion} ${osArch}`;
|
||||
if (os === 'Windows_NT') return `Windows ${osVersion} ${osArch}`;
|
||||
return `${os} ${osVersion} ${osArch}`.trim();
|
||||
}
|
||||
|
||||
function extractText(response) {
|
||||
if (typeof response === 'string') return response;
|
||||
if (!isRecord(response)) return String(response);
|
||||
|
||||
if (response.is_success === false) {
|
||||
const message = extractUserText(response.error) ?? JSON.stringify(response);
|
||||
throw new Error(`Tool API returned an error: ${message}`);
|
||||
}
|
||||
|
||||
const text = extractUserText(response.result);
|
||||
if (text !== undefined) return text;
|
||||
return `Tool API succeeded but did not return user text. Raw response: ${JSON.stringify(response)}`;
|
||||
}
|
||||
|
||||
function extractUserText(value) {
|
||||
if (!isRecord(value) || !Array.isArray(value.user)) return undefined;
|
||||
const text = value.user
|
||||
.filter((item) => isRecord(item) && item.type === 'text' && typeof item.text === 'string')
|
||||
.map((item) => item.text)
|
||||
.filter(Boolean)
|
||||
.join('\n\n');
|
||||
return text.length > 0 ? text : undefined;
|
||||
}
|
||||
|
||||
function defaultStockFilePath(ticker, queryType) {
|
||||
const safeTicker = ticker.replaceAll(',', '_').replaceAll('.', '_');
|
||||
return `/tmp/stock_${safeTicker}_${queryType}.csv`;
|
||||
}
|
||||
|
||||
function requiredString(args, field) {
|
||||
const value = optionalString(args, field);
|
||||
if (value === undefined) throw new Error(`Missing required argument: ${field}.`);
|
||||
return value;
|
||||
}
|
||||
|
||||
function optionalString(args, field) {
|
||||
if (!isRecord(args)) return undefined;
|
||||
const value = args[field];
|
||||
if (value === undefined || value === null) return undefined;
|
||||
if (typeof value !== 'string') throw new Error(`${field} must be a string.`);
|
||||
const trimmed = value.trim();
|
||||
return trimmed.length > 0 ? trimmed : undefined;
|
||||
}
|
||||
|
||||
function requiredObject(args, field) {
|
||||
if (!isRecord(args)) throw new Error(`Missing required argument: ${field}.`);
|
||||
const value = args[field];
|
||||
if (!isRecord(value)) throw new Error(`${field} must be an object.`);
|
||||
return value;
|
||||
}
|
||||
|
||||
function isRecord(value) {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function isNotFound(err) {
|
||||
return isRecord(err) && err.code === 'ENOENT';
|
||||
}
|
||||
|
||||
function asciiHeader(value, fallback = 'unknown') {
|
||||
const cleaned = String(value).replaceAll(/[^ -~]/g, '').trim();
|
||||
return cleaned.length > 0 ? cleaned : fallback;
|
||||
}
|
||||
|
||||
function jsonRpcError(code, message, data) {
|
||||
const err = new Error(message);
|
||||
err.jsonRpc = { code, message, data };
|
||||
return err;
|
||||
}
|
||||
|
||||
function send(message) {
|
||||
process.stdout.write(`${JSON.stringify(message)}\n`);
|
||||
}
|
||||
|
||||
function sendResult(id, result) {
|
||||
send({ jsonrpc: '2.0', id, result });
|
||||
}
|
||||
|
||||
function sendError(id, error) {
|
||||
send({ jsonrpc: '2.0', id, error });
|
||||
}
|
||||
|
||||
async function dispatch(message) {
|
||||
if (message?.jsonrpc !== '2.0') return;
|
||||
// Notifications carry no id and never expect a response.
|
||||
if (message.id === undefined || message.id === null) {
|
||||
if (message.method === 'notifications/initialized' || message.method === 'notifications/cancelled') {
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
const id = message.id;
|
||||
try {
|
||||
const result = await handleRequest(message);
|
||||
sendResult(id, result ?? {});
|
||||
} catch (err) {
|
||||
if (err && typeof err === 'object' && err.jsonRpc !== undefined) {
|
||||
sendError(id, err.jsonRpc);
|
||||
return;
|
||||
}
|
||||
sendError(id, {
|
||||
code: -32603,
|
||||
message: err instanceof Error ? err.message : String(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function start() {
|
||||
const rl = readline.createInterface({ input: process.stdin });
|
||||
rl.on('line', (line) => {
|
||||
const trimmed = line.trim();
|
||||
if (trimmed.length === 0) return;
|
||||
let message;
|
||||
try {
|
||||
message = JSON.parse(trimmed);
|
||||
} catch (err) {
|
||||
sendError(null, {
|
||||
code: -32700,
|
||||
message: `Parse error: ${err instanceof Error ? err.message : String(err)}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
void dispatch(message);
|
||||
});
|
||||
rl.on('close', () => {
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
|
||||
start();
|
||||
18
plugins/official/kimi-datasource/kimi.plugin.json
Normal file
18
plugins/official/kimi-datasource/kimi.plugin.json
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"name": "kimi-datasource",
|
||||
"version": "3.0.0",
|
||||
"description": "Finance, macro, enterprise, and academic data tools for Kimi Code.",
|
||||
"keywords": ["finance", "data-source", "mcp"],
|
||||
"mcpServers": {
|
||||
"data": {
|
||||
"command": "node",
|
||||
"args": ["./bin/kimi-datasource.mjs"],
|
||||
"cwd": "./"
|
||||
}
|
||||
},
|
||||
"interface": {
|
||||
"displayName": "Kimi Datasource",
|
||||
"shortDescription": "Finance, macro, enterprise, and academic data tools",
|
||||
"developerName": "Moonshot AI"
|
||||
}
|
||||
}
|
||||
14
plugins/official/kimi-datasource/watchlist.json
Normal file
14
plugins/official/kimi-datasource/watchlist.json
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
[
|
||||
{
|
||||
"code": "600519.SH",
|
||||
"name": "贵州茅台"
|
||||
},
|
||||
{
|
||||
"code": "000001.SZ",
|
||||
"name": "平安银行"
|
||||
},
|
||||
{
|
||||
"code": "0700.HK",
|
||||
"name": "腾讯控股"
|
||||
}
|
||||
]
|
||||
Loading…
Add table
Add a link
Reference in a new issue