@caidazi/mcp 0.2.0

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.
@@ -0,0 +1,137 @@
1
+ import { isAllowedTool } from "./allowed-tools.js";
2
+
3
+ const DEFAULT_REGISTRY_PATH = "/api/tools/registered";
4
+ const DEFAULT_CALL_PATH = "/api/tools/call";
5
+
6
+ export class CaidaziRestClient {
7
+ constructor({
8
+ baseUrl,
9
+ apiKey,
10
+ fetchImpl = globalThis.fetch,
11
+ timeoutMs = 30000,
12
+ registryPath = DEFAULT_REGISTRY_PATH,
13
+ callPath = DEFAULT_CALL_PATH,
14
+ }) {
15
+ if (!baseUrl) {
16
+ throw new Error("baseUrl is required");
17
+ }
18
+ if (!apiKey) {
19
+ throw new Error("apiKey is required");
20
+ }
21
+ if (typeof fetchImpl !== "function") {
22
+ throw new Error("fetch implementation is required");
23
+ }
24
+
25
+ this.baseUrl = baseUrl.replace(/\/+$/, "");
26
+ this.apiKey = apiKey;
27
+ this.fetchImpl = fetchImpl;
28
+ this.timeoutMs = timeoutMs;
29
+ this.registryPath = registryPath;
30
+ this.callPath = callPath;
31
+ }
32
+
33
+ async listTools() {
34
+ const body = await this.request(this.registryPath, {
35
+ method: "GET",
36
+ headers: this.headers(),
37
+ });
38
+
39
+ if (!Array.isArray(body.tools)) {
40
+ throw new Error("Tool registry response did not include a tools array");
41
+ }
42
+
43
+ return body.tools.filter((tool) => isAllowedTool(tool.name));
44
+ }
45
+
46
+ async callTool(toolName, parameters = {}) {
47
+ if (!isAllowedTool(toolName)) {
48
+ throw new Error(`Tool ${toolName} is not exposed by @caidazi/mcp`);
49
+ }
50
+
51
+ const body = await this.request(this.callPath, {
52
+ method: "POST",
53
+ headers: this.headers({ "Content-Type": "application/json" }),
54
+ body: JSON.stringify({
55
+ tool_name: toolName,
56
+ parameters,
57
+ }),
58
+ });
59
+
60
+ if (body.success === false) {
61
+ throw new Error(body.error || `Tool ${toolName} failed`);
62
+ }
63
+
64
+ if (Object.prototype.hasOwnProperty.call(body, "result")) {
65
+ return body.result;
66
+ }
67
+
68
+ return body;
69
+ }
70
+
71
+ headers(extra = {}) {
72
+ return {
73
+ Accept: "application/json",
74
+ Authorization: `Bearer ${this.apiKey}`,
75
+ ...extra,
76
+ };
77
+ }
78
+
79
+ async request(path, init) {
80
+ const controller = new AbortController();
81
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
82
+
83
+ try {
84
+ const response = await this.fetchImpl(this.url(path), {
85
+ ...init,
86
+ signal: controller.signal,
87
+ });
88
+ const text = await response.text();
89
+ const body = parseResponseBody(text);
90
+
91
+ if (!response.ok) {
92
+ throw new Error(`Caidazi API request failed with HTTP ${response.status}: ${stringifyBody(body)}`);
93
+ }
94
+
95
+ return body;
96
+ } catch (error) {
97
+ const message = error instanceof Error ? error.message : String(error);
98
+ throw new Error(redactSecret(message, this.apiKey));
99
+ } finally {
100
+ clearTimeout(timer);
101
+ }
102
+ }
103
+
104
+ url(path) {
105
+ return `${this.baseUrl}${path.startsWith("/") ? path : `/${path}`}`;
106
+ }
107
+ }
108
+
109
+ export function redactSecret(text, apiKey) {
110
+ let redacted = String(text);
111
+
112
+ if (apiKey) {
113
+ redacted = redacted.split(apiKey).join("[REDACTED]");
114
+ }
115
+
116
+ return redacted.replace(/cdz_live_[A-Za-z0-9_-]+/g, "[REDACTED]");
117
+ }
118
+
119
+ function parseResponseBody(text) {
120
+ if (!text) {
121
+ return {};
122
+ }
123
+
124
+ try {
125
+ return JSON.parse(text);
126
+ } catch {
127
+ return text;
128
+ }
129
+ }
130
+
131
+ function stringifyBody(body) {
132
+ if (typeof body === "string") {
133
+ return body;
134
+ }
135
+
136
+ return JSON.stringify(body);
137
+ }
@@ -0,0 +1,109 @@
1
+ export function createMcpTool(toolInfo) {
2
+ return {
3
+ name: toolInfo.name,
4
+ description: toolInfo.description || toolInfo.name,
5
+ inputSchema: parseInputDetailToSchema(toolInfo.input_detail),
6
+ };
7
+ }
8
+
9
+ export function parseInputDetailToSchema(inputDetail) {
10
+ const properties = {};
11
+ const required = [];
12
+
13
+ for (const row of parseMarkdownRows(inputDetail)) {
14
+ const [rawName, rawType, rawRequired, rawDescription] = row;
15
+ const name = cleanCell(rawName).replace(/^`|`$/g, "");
16
+
17
+ if (!name || name === "参数" || /^-+$/.test(name)) {
18
+ continue;
19
+ }
20
+
21
+ properties[name] = {
22
+ ...typeToSchema(cleanCell(rawType)),
23
+ description: cleanCell(rawDescription),
24
+ };
25
+
26
+ if (isRequired(cleanCell(rawRequired))) {
27
+ required.push(name);
28
+ }
29
+ }
30
+
31
+ return {
32
+ type: "object",
33
+ properties,
34
+ required,
35
+ additionalProperties: true,
36
+ };
37
+ }
38
+
39
+ export function resultToContent(result) {
40
+ if (typeof result === "string") {
41
+ return [{ type: "text", text: result }];
42
+ }
43
+
44
+ return [
45
+ {
46
+ type: "text",
47
+ text: JSON.stringify(result, null, 2),
48
+ },
49
+ ];
50
+ }
51
+
52
+ function parseMarkdownRows(inputDetail = "") {
53
+ return String(inputDetail)
54
+ .split(/\r?\n/)
55
+ .map((line) => line.trim())
56
+ .filter((line) => line.startsWith("|") && line.endsWith("|"))
57
+ .map((line) =>
58
+ line
59
+ .slice(1, -1)
60
+ .split("|")
61
+ .map((cell) => cell.trim()),
62
+ )
63
+ .filter((cells) => cells.length >= 4 && !cells.every((cell) => /^-+$/.test(cell)));
64
+ }
65
+
66
+ function cleanCell(value = "") {
67
+ return String(value)
68
+ .replace(/<br\s*\/?>/gi, " ")
69
+ .replace(/\s+/g, " ")
70
+ .trim();
71
+ }
72
+
73
+ function isRequired(value) {
74
+ return /^(是|必填|required|true|yes)$/i.test(value);
75
+ }
76
+
77
+ function typeToSchema(type) {
78
+ const normalized = type.toLowerCase().replace(/\s+/g, "");
79
+
80
+ if (/^array\[(.+)\]$/.test(normalized)) {
81
+ const itemType = normalized.match(/^array\[(.+)\]$/)[1];
82
+ return {
83
+ type: "array",
84
+ items: typeToSchema(itemType),
85
+ };
86
+ }
87
+
88
+ if (["array", "list"].includes(normalized)) {
89
+ return { type: "array" };
90
+ }
91
+
92
+ if (["int", "integer"].includes(normalized)) {
93
+ return { type: "integer" };
94
+ }
95
+
96
+ if (["number", "float", "double"].includes(normalized)) {
97
+ return { type: "number" };
98
+ }
99
+
100
+ if (["bool", "boolean"].includes(normalized)) {
101
+ return { type: "boolean" };
102
+ }
103
+
104
+ if (["object", "dict", "map"].includes(normalized)) {
105
+ return { type: "object" };
106
+ }
107
+
108
+ return { type: "string" };
109
+ }