ms-vite-plugin 1.1.20 → 1.1.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/mcp/httpapi-docs-service.d.ts +73 -0
- package/dist/mcp/httpapi-docs-service.js +188 -0
- package/dist/mcp/httpapi-tools.d.ts +9 -0
- package/dist/mcp/httpapi-tools.js +345 -0
- package/dist/mcp/runtime-tools.js +2 -87
- package/dist/mcp/tool-utils.d.ts +0 -30
- package/dist/mcp/tool-utils.js +0 -74
- package/dist/mcp/tools.js +2 -0
- package/docs/httpapi/api.md +2411 -0
- package/package.json +1 -1
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP API 请求方法
|
|
3
|
+
*/
|
|
4
|
+
export type HttpApiMethod = "GET" | "POST";
|
|
5
|
+
/**
|
|
6
|
+
* HTTP API 文档条目
|
|
7
|
+
*/
|
|
8
|
+
export type HttpApiDocEntry = {
|
|
9
|
+
/** 稳定 slug,可用于读取单个接口文档 */
|
|
10
|
+
slug: string;
|
|
11
|
+
/** 接口请求方法 */
|
|
12
|
+
method: HttpApiMethod;
|
|
13
|
+
/** 接口路径 */
|
|
14
|
+
path: string;
|
|
15
|
+
/** 接口标题 */
|
|
16
|
+
title: string;
|
|
17
|
+
/** 所属一级章节 */
|
|
18
|
+
section: string;
|
|
19
|
+
/** 文档片段起始行号(1-based) */
|
|
20
|
+
startLine: number;
|
|
21
|
+
/** 文档片段结束行号(1-based) */
|
|
22
|
+
endLine: number;
|
|
23
|
+
/** 接口完整 markdown 片段 */
|
|
24
|
+
content: string;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* 获取 HTTP API 文档路径
|
|
28
|
+
* @returns 返回 docs/httpapi/api.md 的绝对路径
|
|
29
|
+
* @example
|
|
30
|
+
* const filePath = getHttpApiDocPath()
|
|
31
|
+
*/
|
|
32
|
+
export declare function getHttpApiDocPath(): string;
|
|
33
|
+
/**
|
|
34
|
+
* 读取 HTTP API markdown 文档
|
|
35
|
+
* @returns 返回完整 markdown 文本
|
|
36
|
+
* @example
|
|
37
|
+
* const markdown = await readHttpApiMarkdown()
|
|
38
|
+
*/
|
|
39
|
+
export declare function readHttpApiMarkdown(): Promise<string>;
|
|
40
|
+
/**
|
|
41
|
+
* 根据 method/path 生成稳定 slug
|
|
42
|
+
* @param method HTTP 请求方法
|
|
43
|
+
* @param apiPath HTTP API 路径
|
|
44
|
+
* @returns 返回可用于 MCP 资源路径的 slug
|
|
45
|
+
* @example
|
|
46
|
+
* createHttpApiSlug("GET", "/api/control/click")
|
|
47
|
+
*/
|
|
48
|
+
export declare function createHttpApiSlug(method: HttpApiMethod, apiPath: string): string;
|
|
49
|
+
/**
|
|
50
|
+
* 解析 HTTP API markdown 为接口条目
|
|
51
|
+
* @returns 返回按文档顺序排列的接口条目
|
|
52
|
+
* @example
|
|
53
|
+
* const entries = await getHttpApiDocEntries()
|
|
54
|
+
*/
|
|
55
|
+
export declare function getHttpApiDocEntries(): Promise<HttpApiDocEntry[]>;
|
|
56
|
+
/**
|
|
57
|
+
* 计算 HTTP API 文档条目搜索分数
|
|
58
|
+
* @param entry 文档条目
|
|
59
|
+
* @param normalizedQuery 已转小写的搜索词
|
|
60
|
+
* @returns 返回匹配分数,0 表示不匹配
|
|
61
|
+
* @example
|
|
62
|
+
* scoreHttpApiDocEntry(entry, "click")
|
|
63
|
+
*/
|
|
64
|
+
export declare function scoreHttpApiDocEntry(entry: HttpApiDocEntry, normalizedQuery: string): number;
|
|
65
|
+
/**
|
|
66
|
+
* 根据 slug 或 path 查找 HTTP API 文档条目
|
|
67
|
+
* @param identifier slug 或 API 路径
|
|
68
|
+
* @param method 可选 HTTP 方法,用于同路径多方法时消歧
|
|
69
|
+
* @returns 找到时返回条目,否则返回 undefined
|
|
70
|
+
* @example
|
|
71
|
+
* const entry = await findHttpApiDocEntry("/api/control/click", "GET")
|
|
72
|
+
*/
|
|
73
|
+
export declare function findHttpApiDocEntry(identifier: string, method?: HttpApiMethod): Promise<HttpApiDocEntry | undefined>;
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.getHttpApiDocPath = getHttpApiDocPath;
|
|
37
|
+
exports.readHttpApiMarkdown = readHttpApiMarkdown;
|
|
38
|
+
exports.createHttpApiSlug = createHttpApiSlug;
|
|
39
|
+
exports.getHttpApiDocEntries = getHttpApiDocEntries;
|
|
40
|
+
exports.scoreHttpApiDocEntry = scoreHttpApiDocEntry;
|
|
41
|
+
exports.findHttpApiDocEntry = findHttpApiDocEntry;
|
|
42
|
+
const fsExtra = __importStar(require("fs-extra"));
|
|
43
|
+
const path = __importStar(require("path"));
|
|
44
|
+
/**
|
|
45
|
+
* 获取 HTTP API 文档路径
|
|
46
|
+
* @returns 返回 docs/httpapi/api.md 的绝对路径
|
|
47
|
+
* @example
|
|
48
|
+
* const filePath = getHttpApiDocPath()
|
|
49
|
+
*/
|
|
50
|
+
function getHttpApiDocPath() {
|
|
51
|
+
return path.resolve(__dirname, "..", "..", "docs", "httpapi", "api.md");
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* 读取 HTTP API markdown 文档
|
|
55
|
+
* @returns 返回完整 markdown 文本
|
|
56
|
+
* @example
|
|
57
|
+
* const markdown = await readHttpApiMarkdown()
|
|
58
|
+
*/
|
|
59
|
+
async function readHttpApiMarkdown() {
|
|
60
|
+
const filePath = getHttpApiDocPath();
|
|
61
|
+
if (!(await fsExtra.pathExists(filePath))) {
|
|
62
|
+
throw new Error(`HTTP API 文档不存在: ${filePath}`);
|
|
63
|
+
}
|
|
64
|
+
return fsExtra.readFile(filePath, "utf8");
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* 根据 method/path 生成稳定 slug
|
|
68
|
+
* @param method HTTP 请求方法
|
|
69
|
+
* @param apiPath HTTP API 路径
|
|
70
|
+
* @returns 返回可用于 MCP 资源路径的 slug
|
|
71
|
+
* @example
|
|
72
|
+
* createHttpApiSlug("GET", "/api/control/click")
|
|
73
|
+
*/
|
|
74
|
+
function createHttpApiSlug(method, apiPath) {
|
|
75
|
+
return `${method.toLowerCase()}-${apiPath}`
|
|
76
|
+
.replace(/^get-\//, "get-")
|
|
77
|
+
.replace(/^post-\//, "post-")
|
|
78
|
+
.replace(/[^a-zA-Z0-9]+/g, "-")
|
|
79
|
+
.replace(/^-+|-+$/g, "")
|
|
80
|
+
.toLowerCase();
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* 解析 HTTP API markdown 为接口条目
|
|
84
|
+
* @returns 返回按文档顺序排列的接口条目
|
|
85
|
+
* @example
|
|
86
|
+
* const entries = await getHttpApiDocEntries()
|
|
87
|
+
*/
|
|
88
|
+
async function getHttpApiDocEntries() {
|
|
89
|
+
const markdown = await readHttpApiMarkdown();
|
|
90
|
+
const lines = markdown.split(/\r?\n/);
|
|
91
|
+
const entries = [];
|
|
92
|
+
let currentSection = "";
|
|
93
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
94
|
+
const line = lines[index] ?? "";
|
|
95
|
+
const sectionMatch = line.match(/^#\s+(.+)$/);
|
|
96
|
+
if (sectionMatch) {
|
|
97
|
+
currentSection = sectionMatch[1]?.trim() ?? "";
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const titleMatch = line.match(/^##\s+(.+)$/);
|
|
101
|
+
if (!titleMatch) {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
const title = titleMatch[1]?.trim() ?? "";
|
|
105
|
+
let method = null;
|
|
106
|
+
let apiPath = "";
|
|
107
|
+
for (let cursor = index + 1; cursor < Math.min(lines.length, index + 8); cursor += 1) {
|
|
108
|
+
const endpointMatch = (lines[cursor] ?? "").match(/^(GET|POST)\s+(\/\S+)/);
|
|
109
|
+
if (endpointMatch) {
|
|
110
|
+
method = endpointMatch[1];
|
|
111
|
+
apiPath = endpointMatch[2] ?? "";
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (!method || !apiPath) {
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
let endIndex = lines.length;
|
|
119
|
+
for (let cursor = index + 1; cursor < lines.length; cursor += 1) {
|
|
120
|
+
if (/^##\s+/.test(lines[cursor] ?? "")) {
|
|
121
|
+
endIndex = cursor;
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
entries.push({
|
|
126
|
+
slug: createHttpApiSlug(method, apiPath),
|
|
127
|
+
method,
|
|
128
|
+
path: apiPath,
|
|
129
|
+
title,
|
|
130
|
+
section: currentSection,
|
|
131
|
+
startLine: index + 1,
|
|
132
|
+
endLine: endIndex,
|
|
133
|
+
content: lines.slice(index, endIndex).join("\n").trim(),
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
return entries;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* 计算 HTTP API 文档条目搜索分数
|
|
140
|
+
* @param entry 文档条目
|
|
141
|
+
* @param normalizedQuery 已转小写的搜索词
|
|
142
|
+
* @returns 返回匹配分数,0 表示不匹配
|
|
143
|
+
* @example
|
|
144
|
+
* scoreHttpApiDocEntry(entry, "click")
|
|
145
|
+
*/
|
|
146
|
+
function scoreHttpApiDocEntry(entry, normalizedQuery) {
|
|
147
|
+
const query = normalizedQuery.trim();
|
|
148
|
+
if (!query) {
|
|
149
|
+
return 0;
|
|
150
|
+
}
|
|
151
|
+
let score = 0;
|
|
152
|
+
if (entry.path.toLowerCase() === query) {
|
|
153
|
+
score += 100;
|
|
154
|
+
}
|
|
155
|
+
if (`${entry.method} ${entry.path}`.toLowerCase() === query) {
|
|
156
|
+
score += 120;
|
|
157
|
+
}
|
|
158
|
+
if (entry.path.toLowerCase().includes(query)) {
|
|
159
|
+
score += 60;
|
|
160
|
+
}
|
|
161
|
+
if (entry.title.toLowerCase().includes(query)) {
|
|
162
|
+
score += 50;
|
|
163
|
+
}
|
|
164
|
+
if (entry.section.toLowerCase().includes(query)) {
|
|
165
|
+
score += 20;
|
|
166
|
+
}
|
|
167
|
+
if (entry.content.toLowerCase().includes(query)) {
|
|
168
|
+
score += 10;
|
|
169
|
+
}
|
|
170
|
+
return score;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* 根据 slug 或 path 查找 HTTP API 文档条目
|
|
174
|
+
* @param identifier slug 或 API 路径
|
|
175
|
+
* @param method 可选 HTTP 方法,用于同路径多方法时消歧
|
|
176
|
+
* @returns 找到时返回条目,否则返回 undefined
|
|
177
|
+
* @example
|
|
178
|
+
* const entry = await findHttpApiDocEntry("/api/control/click", "GET")
|
|
179
|
+
*/
|
|
180
|
+
async function findHttpApiDocEntry(identifier, method) {
|
|
181
|
+
const normalizedIdentifier = identifier.trim();
|
|
182
|
+
const entries = await getHttpApiDocEntries();
|
|
183
|
+
return entries.find((entry) => {
|
|
184
|
+
const methodMatches = method ? entry.method === method : true;
|
|
185
|
+
return (methodMatches &&
|
|
186
|
+
(entry.slug === normalizedIdentifier || entry.path === normalizedIdentifier));
|
|
187
|
+
});
|
|
188
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
/**
|
|
3
|
+
* 注册 HTTP API 文档与通用调用工具
|
|
4
|
+
* @param server MCP 服务实例
|
|
5
|
+
* @returns 无返回值
|
|
6
|
+
* @example
|
|
7
|
+
* registerHttpApiTools(server)
|
|
8
|
+
*/
|
|
9
|
+
export declare function registerHttpApiTools(server: McpServer): void;
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.registerHttpApiTools = registerHttpApiTools;
|
|
37
|
+
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
38
|
+
const z = __importStar(require("zod/v4"));
|
|
39
|
+
const httpapi_docs_service_1 = require("./httpapi-docs-service");
|
|
40
|
+
const tool_utils_1 = require("./tool-utils");
|
|
41
|
+
/**
|
|
42
|
+
* HTTP API 文档完整资源 URI
|
|
43
|
+
*/
|
|
44
|
+
const HTTP_API_DOC_RESOURCE_URI = "kuaijs://httpapi/api";
|
|
45
|
+
/**
|
|
46
|
+
* 格式化 HTTP API 文档搜索摘要
|
|
47
|
+
* @param entry HTTP API 文档条目
|
|
48
|
+
* @param index 列表序号
|
|
49
|
+
* @returns 返回摘要文本
|
|
50
|
+
* @example
|
|
51
|
+
* formatHttpApiDocSummary(entry, 0)
|
|
52
|
+
*/
|
|
53
|
+
function formatHttpApiDocSummary(entry, index) {
|
|
54
|
+
return [
|
|
55
|
+
`${index + 1}. ${entry.title}`,
|
|
56
|
+
`method: ${entry.method}`,
|
|
57
|
+
`path: ${entry.path}`,
|
|
58
|
+
`slug: ${entry.slug}`,
|
|
59
|
+
`section: ${entry.section || "未分组"}`,
|
|
60
|
+
`lines: ${entry.startLine}-${entry.endLine}`,
|
|
61
|
+
`uri: kuaijs://httpapi/endpoints/${entry.slug}`,
|
|
62
|
+
].join("\n");
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* 标准化并校验 HTTP API 路径输入
|
|
66
|
+
* @param apiPath 用户传入路径
|
|
67
|
+
* @returns 返回标准化后的路径
|
|
68
|
+
* @example
|
|
69
|
+
* normalizeHttpApiPath("/api/status")
|
|
70
|
+
*/
|
|
71
|
+
function normalizeHttpApiPath(apiPath) {
|
|
72
|
+
const normalizedPath = apiPath.trim();
|
|
73
|
+
if (/^https?:\/\//i.test(normalizedPath)) {
|
|
74
|
+
throw new Error("path 只能传 HTTP API 相对路径,不能传完整 URL。");
|
|
75
|
+
}
|
|
76
|
+
if (!normalizedPath.startsWith("/")) {
|
|
77
|
+
throw new Error("path 必须以 / 开头,例如 /api/status。");
|
|
78
|
+
}
|
|
79
|
+
if (normalizedPath.includes("?")) {
|
|
80
|
+
throw new Error("path 不能包含 query string,请使用 query 参数传递查询参数。");
|
|
81
|
+
}
|
|
82
|
+
return normalizedPath;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* 将 query 参数追加到 URL
|
|
86
|
+
* @param url 目标 URL
|
|
87
|
+
* @param query 可选 query 参数
|
|
88
|
+
* @returns 无返回值
|
|
89
|
+
* @example
|
|
90
|
+
* appendQueryParams(url, { x: 1 })
|
|
91
|
+
*/
|
|
92
|
+
function appendQueryParams(url, query) {
|
|
93
|
+
if (!query) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
for (const [key, value] of Object.entries(query)) {
|
|
97
|
+
if (value === undefined || value === null) {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
url.searchParams.set(key, String(value));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* 读取响应体并按要求格式化
|
|
105
|
+
* @param response fetch 响应对象
|
|
106
|
+
* @param responseFormat 响应格式
|
|
107
|
+
* @returns 返回格式化文本
|
|
108
|
+
* @example
|
|
109
|
+
* const text = await readHttpApiResponseText(response, "json")
|
|
110
|
+
*/
|
|
111
|
+
async function readHttpApiResponseText(response, responseFormat) {
|
|
112
|
+
const responseText = await response.text();
|
|
113
|
+
if (responseFormat === "text") {
|
|
114
|
+
return responseText;
|
|
115
|
+
}
|
|
116
|
+
try {
|
|
117
|
+
return (0, tool_utils_1.formatRuntimeJsonText)(JSON.parse(responseText));
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
throw new Error("响应不是有效 JSON;如需读取文本响应,请设置 responseFormat=text。");
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* 注册 HTTP API 文档与通用调用工具
|
|
125
|
+
* @param server MCP 服务实例
|
|
126
|
+
* @returns 无返回值
|
|
127
|
+
* @example
|
|
128
|
+
* registerHttpApiTools(server)
|
|
129
|
+
*/
|
|
130
|
+
function registerHttpApiTools(server) {
|
|
131
|
+
server.registerResource("http-api-doc", HTTP_API_DOC_RESOURCE_URI, {
|
|
132
|
+
title: "KuaiJS HTTP API document",
|
|
133
|
+
mimeType: "text/markdown",
|
|
134
|
+
}, async () => ({
|
|
135
|
+
contents: [
|
|
136
|
+
{
|
|
137
|
+
uri: HTTP_API_DOC_RESOURCE_URI,
|
|
138
|
+
mimeType: "text/markdown",
|
|
139
|
+
text: await (0, httpapi_docs_service_1.readHttpApiMarkdown)(),
|
|
140
|
+
},
|
|
141
|
+
],
|
|
142
|
+
}));
|
|
143
|
+
server.registerResource("http-api-endpoint", new mcp_js_1.ResourceTemplate("kuaijs://httpapi/endpoints/{slug}", {
|
|
144
|
+
list: async () => {
|
|
145
|
+
const entries = await (0, httpapi_docs_service_1.getHttpApiDocEntries)();
|
|
146
|
+
return {
|
|
147
|
+
resources: entries.map((entry) => ({
|
|
148
|
+
uri: `kuaijs://httpapi/endpoints/${entry.slug}`,
|
|
149
|
+
name: `${entry.method} ${entry.path}`,
|
|
150
|
+
description: `${entry.title} [${entry.section || "未分组"}]`,
|
|
151
|
+
mimeType: "text/markdown",
|
|
152
|
+
})),
|
|
153
|
+
};
|
|
154
|
+
},
|
|
155
|
+
}), {
|
|
156
|
+
title: "KuaiJS HTTP API endpoint documents",
|
|
157
|
+
mimeType: "text/markdown",
|
|
158
|
+
}, async (_uri, variables) => {
|
|
159
|
+
const slug = String(variables.slug || "");
|
|
160
|
+
const entry = await (0, httpapi_docs_service_1.findHttpApiDocEntry)(slug);
|
|
161
|
+
if (!entry) {
|
|
162
|
+
throw new Error(`HTTP API 文档不存在: ${slug}`);
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
contents: [
|
|
166
|
+
{
|
|
167
|
+
uri: `kuaijs://httpapi/endpoints/${entry.slug}`,
|
|
168
|
+
mimeType: "text/markdown",
|
|
169
|
+
text: entry.content,
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
};
|
|
173
|
+
});
|
|
174
|
+
server.registerTool("search_http_api_docs", {
|
|
175
|
+
title: "Search HTTP API Docs",
|
|
176
|
+
description: "搜索 docs/httpapi/api.md 中声明的 HTTP API。调用 http_api_call 前应先用本工具确认接口 method、path、参数和 slug。",
|
|
177
|
+
inputSchema: {
|
|
178
|
+
query: z
|
|
179
|
+
.string()
|
|
180
|
+
.min(1)
|
|
181
|
+
.describe("搜索关键词,例如 click、/api/control/click、截图"),
|
|
182
|
+
limit: z
|
|
183
|
+
.number()
|
|
184
|
+
.int()
|
|
185
|
+
.min(1)
|
|
186
|
+
.max(20)
|
|
187
|
+
.optional()
|
|
188
|
+
.default(8)
|
|
189
|
+
.describe("返回条数上限,默认 8"),
|
|
190
|
+
},
|
|
191
|
+
}, async ({ query, limit }) => {
|
|
192
|
+
const normalizedQuery = query.toLowerCase();
|
|
193
|
+
const entries = await (0, httpapi_docs_service_1.getHttpApiDocEntries)();
|
|
194
|
+
const matches = entries
|
|
195
|
+
.map((entry) => ({
|
|
196
|
+
entry,
|
|
197
|
+
score: (0, httpapi_docs_service_1.scoreHttpApiDocEntry)(entry, normalizedQuery),
|
|
198
|
+
}))
|
|
199
|
+
.filter((item) => item.score > 0)
|
|
200
|
+
.sort((a, b) => b.score - a.score ||
|
|
201
|
+
a.entry.path.localeCompare(b.entry.path) ||
|
|
202
|
+
a.entry.method.localeCompare(b.entry.method))
|
|
203
|
+
.slice(0, limit)
|
|
204
|
+
.map((item) => item.entry);
|
|
205
|
+
return (0, tool_utils_1.createTextToolResult)(matches.length === 0
|
|
206
|
+
? `未找到与 "${query}" 匹配的 HTTP API 文档。\n文档: ${(0, httpapi_docs_service_1.getHttpApiDocPath)()}`
|
|
207
|
+
: [
|
|
208
|
+
`HTTP API 文档: ${(0, httpapi_docs_service_1.getHttpApiDocPath)()}`,
|
|
209
|
+
"",
|
|
210
|
+
...matches.map((entry, index) => formatHttpApiDocSummary(entry, index)),
|
|
211
|
+
].join("\n"));
|
|
212
|
+
});
|
|
213
|
+
server.registerTool("read_http_api_doc", {
|
|
214
|
+
title: "Read HTTP API Doc",
|
|
215
|
+
description: "按 slug 或 path 读取 docs/httpapi/api.md 中某个接口的完整片段。调用 http_api_call 前应读取目标接口片段,确认参数位置、类型和返回结构。",
|
|
216
|
+
inputSchema: {
|
|
217
|
+
identifier: z
|
|
218
|
+
.string()
|
|
219
|
+
.min(1)
|
|
220
|
+
.describe("接口 slug 或 path,例如 get-api-control-click 或 /api/control/click"),
|
|
221
|
+
method: z
|
|
222
|
+
.enum(["GET", "POST"])
|
|
223
|
+
.optional()
|
|
224
|
+
.describe("可选 HTTP 方法,用于同路径多方法时消歧"),
|
|
225
|
+
},
|
|
226
|
+
}, async ({ identifier, method }) => {
|
|
227
|
+
const entry = await (0, httpapi_docs_service_1.findHttpApiDocEntry)(identifier, method);
|
|
228
|
+
if (!entry) {
|
|
229
|
+
return (0, tool_utils_1.createTextToolResult)(`未找到 HTTP API 文档: ${identifier}\n请先调用 search_http_api_docs 搜索可用接口。`, true);
|
|
230
|
+
}
|
|
231
|
+
return (0, tool_utils_1.createTextToolResult)([
|
|
232
|
+
`title: ${entry.title}`,
|
|
233
|
+
`method: ${entry.method}`,
|
|
234
|
+
`path: ${entry.path}`,
|
|
235
|
+
`slug: ${entry.slug}`,
|
|
236
|
+
`section: ${entry.section || "未分组"}`,
|
|
237
|
+
`lines: ${entry.startLine}-${entry.endLine}`,
|
|
238
|
+
"",
|
|
239
|
+
entry.content,
|
|
240
|
+
].join("\n"));
|
|
241
|
+
});
|
|
242
|
+
server.registerTool("http_api_call", {
|
|
243
|
+
title: "HTTP API Call",
|
|
244
|
+
description: "调用 docs/httpapi/api.md 中已声明的 HTTP API。使用前应先调用 search_http_api_docs/read_http_api_doc 获取 docSlug,并确认 method、path、query/body 参数;本工具会拒绝 docSlug、method、path 不匹配的调用。",
|
|
245
|
+
inputSchema: {
|
|
246
|
+
method: z
|
|
247
|
+
.enum(["GET", "POST"])
|
|
248
|
+
.describe("HTTP 方法,必须与 HTTP API 文档一致"),
|
|
249
|
+
path: z
|
|
250
|
+
.string()
|
|
251
|
+
.min(1)
|
|
252
|
+
.describe("HTTP API 相对路径,例如 /api/control/click;不能包含 query string"),
|
|
253
|
+
docSlug: z
|
|
254
|
+
.string()
|
|
255
|
+
.min(1)
|
|
256
|
+
.describe("search_http_api_docs/read_http_api_doc 返回的接口 slug"),
|
|
257
|
+
query: z
|
|
258
|
+
.record(z.string(), z.union([z.string(), z.number(), z.boolean(), z.null()]))
|
|
259
|
+
.optional()
|
|
260
|
+
.describe("query 参数;GET 接口通常使用 query 传参"),
|
|
261
|
+
body: z
|
|
262
|
+
.unknown()
|
|
263
|
+
.optional()
|
|
264
|
+
.describe("POST 请求体;对象会按 JSON 提交"),
|
|
265
|
+
responseFormat: z
|
|
266
|
+
.enum(["json", "text"])
|
|
267
|
+
.optional()
|
|
268
|
+
.default("json")
|
|
269
|
+
.describe("响应解析格式,默认 json"),
|
|
270
|
+
timeoutMs: z
|
|
271
|
+
.number()
|
|
272
|
+
.int()
|
|
273
|
+
.min(1000)
|
|
274
|
+
.max(600000)
|
|
275
|
+
.optional()
|
|
276
|
+
.default(30000)
|
|
277
|
+
.describe("请求超时时间,默认 30000 毫秒"),
|
|
278
|
+
},
|
|
279
|
+
}, async ({ method, path, docSlug, query, body, responseFormat, timeoutMs, }) => {
|
|
280
|
+
const apiPath = normalizeHttpApiPath(path);
|
|
281
|
+
const entry = await (0, httpapi_docs_service_1.findHttpApiDocEntry)(docSlug);
|
|
282
|
+
if (!entry) {
|
|
283
|
+
return (0, tool_utils_1.createTextToolResult)([
|
|
284
|
+
`HTTP API 文档中不存在该 docSlug: ${docSlug}`,
|
|
285
|
+
"请先调用 search_http_api_docs 搜索可用接口,再调用 read_http_api_doc 确认参数和 slug。",
|
|
286
|
+
].join("\n"), true);
|
|
287
|
+
}
|
|
288
|
+
if (entry.method !== method || entry.path !== apiPath) {
|
|
289
|
+
return (0, tool_utils_1.createTextToolResult)([
|
|
290
|
+
"HTTP API 调用与文档条目不匹配。",
|
|
291
|
+
`传入: ${method} ${apiPath} (${docSlug})`,
|
|
292
|
+
`文档: ${entry.method} ${entry.path} (${entry.slug})`,
|
|
293
|
+
"请重新调用 read_http_api_doc 确认目标接口。",
|
|
294
|
+
].join("\n"), true);
|
|
295
|
+
}
|
|
296
|
+
if (method === "GET" && body !== undefined) {
|
|
297
|
+
return (0, tool_utils_1.createTextToolResult)("GET 接口不能传 body,请使用 query 参数。", true);
|
|
298
|
+
}
|
|
299
|
+
const target = await (0, tool_utils_1.resolveRuntimeHttpTarget)();
|
|
300
|
+
const url = new URL(`http://${target.ip}:${target.port}${apiPath}`);
|
|
301
|
+
appendQueryParams(url, query);
|
|
302
|
+
const controller = new AbortController();
|
|
303
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
304
|
+
try {
|
|
305
|
+
const headers = {};
|
|
306
|
+
let requestBody;
|
|
307
|
+
if (body !== undefined) {
|
|
308
|
+
if (typeof body === "string") {
|
|
309
|
+
requestBody = body;
|
|
310
|
+
headers["Content-Type"] = "text/plain; charset=utf-8";
|
|
311
|
+
}
|
|
312
|
+
else {
|
|
313
|
+
requestBody = JSON.stringify(body);
|
|
314
|
+
headers["Content-Type"] = "application/json";
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
const response = await fetch(url.toString(), {
|
|
318
|
+
method,
|
|
319
|
+
signal: controller.signal,
|
|
320
|
+
headers: Object.keys(headers).length > 0 ? headers : undefined,
|
|
321
|
+
body: requestBody,
|
|
322
|
+
});
|
|
323
|
+
const responseText = await readHttpApiResponseText(response, responseFormat);
|
|
324
|
+
return (0, tool_utils_1.createTextToolResult)([
|
|
325
|
+
`HTTP API 调用${response.ok ? "成功" : "失败"}: ${method} ${apiPath}`,
|
|
326
|
+
`设备: ${target.label}`,
|
|
327
|
+
`文档: ${entry.title} (${entry.slug}, lines ${entry.startLine}-${entry.endLine})`,
|
|
328
|
+
`状态码: ${response.status}`,
|
|
329
|
+
"",
|
|
330
|
+
responseText,
|
|
331
|
+
].join("\n"), !response.ok);
|
|
332
|
+
}
|
|
333
|
+
catch (error) {
|
|
334
|
+
const message = error instanceof Error && error.name === "AbortError"
|
|
335
|
+
? `请求超时: ${timeoutMs}ms`
|
|
336
|
+
: error instanceof Error
|
|
337
|
+
? error.message
|
|
338
|
+
: String(error);
|
|
339
|
+
return (0, tool_utils_1.createTextToolResult)(`HTTP API 调用失败: ${method} ${apiPath}\n${message}`, true);
|
|
340
|
+
}
|
|
341
|
+
finally {
|
|
342
|
+
clearTimeout(timeout);
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
}
|