@verifik/mcp 0.1.0 → 0.1.2
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/README.md +443 -130
- package/data/field-enrichment.json +452 -0
- package/docs/field-formats.md +148 -0
- package/lib/catalog.js +19 -2
- package/lib/client-features.js +64 -0
- package/lib/config.js +2 -0
- package/lib/description.js +67 -0
- package/lib/error-response.js +210 -0
- package/lib/field-enrichment.js +103 -0
- package/lib/field-normalize.js +65 -0
- package/lib/filters.js +10 -3
- package/lib/meta-tools.js +3 -0
- package/lib/proxy.js +143 -23
- package/lib/tool-naming.js +51 -0
- package/lib/tool-schema.js +127 -46
- package/lib/validate-args.js +230 -0
- package/package.json +22 -6
- package/server.js +40 -12
package/lib/proxy.js
CHANGED
|
@@ -6,6 +6,45 @@ const {
|
|
|
6
6
|
sanitizeRequestParams,
|
|
7
7
|
splitRequestPayload,
|
|
8
8
|
} = require("./url-builder");
|
|
9
|
+
const { getRequestOverride, getParameterAliases } = require("./field-enrichment");
|
|
10
|
+
const { normalizeRequestParams } = require("./field-normalize");
|
|
11
|
+
const {
|
|
12
|
+
extractCreditsCharged,
|
|
13
|
+
buildProxyErrorPayload,
|
|
14
|
+
sanitizeResponseData,
|
|
15
|
+
classifyUpstreamError,
|
|
16
|
+
SERVICE_UNAVAILABLE_MESSAGE,
|
|
17
|
+
} = require("./error-response");
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param {import("./types").AppFeature} feature
|
|
21
|
+
*/
|
|
22
|
+
const resolveFeatureRequest = (feature) => {
|
|
23
|
+
const override = getRequestOverride(feature.code);
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
method: override?.method || feature.method,
|
|
27
|
+
url: override?.url || feature.url,
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @param {string} featureCode
|
|
33
|
+
* @param {Record<string, unknown>} params
|
|
34
|
+
*/
|
|
35
|
+
const applyParameterAliases = (featureCode, params) => {
|
|
36
|
+
const aliases = getParameterAliases(featureCode);
|
|
37
|
+
const mapped = { ...params };
|
|
38
|
+
|
|
39
|
+
for (const [from, to] of Object.entries(aliases)) {
|
|
40
|
+
if (mapped[from] !== undefined) {
|
|
41
|
+
mapped[to] = mapped[from];
|
|
42
|
+
delete mapped[from];
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return mapped;
|
|
47
|
+
};
|
|
9
48
|
|
|
10
49
|
/**
|
|
11
50
|
* @param {import("./types").AppFeature} feature
|
|
@@ -13,9 +52,13 @@ const {
|
|
|
13
52
|
* @param {import("./types").McpConfig} config
|
|
14
53
|
*/
|
|
15
54
|
const invokeFeature = async (feature, args, config) => {
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
const
|
|
55
|
+
const requestMeta = resolveFeatureRequest(feature);
|
|
56
|
+
const method = normalizeHttpMethod(requestMeta.method);
|
|
57
|
+
const url = buildFeatureRequestUrl(config.apiBase, requestMeta.url);
|
|
58
|
+
const params = applyParameterAliases(
|
|
59
|
+
feature.code,
|
|
60
|
+
normalizeRequestParams(sanitizeRequestParams(args))
|
|
61
|
+
);
|
|
19
62
|
const { query, body } = splitRequestPayload(method, params);
|
|
20
63
|
|
|
21
64
|
const requestInit = {
|
|
@@ -24,30 +67,53 @@ const invokeFeature = async (feature, args, config) => {
|
|
|
24
67
|
Authorization: `Bearer ${config.token}`,
|
|
25
68
|
Accept: "application/json",
|
|
26
69
|
},
|
|
70
|
+
signal: AbortSignal.timeout(config.requestTimeoutMs),
|
|
27
71
|
};
|
|
28
72
|
|
|
29
73
|
if (body !== undefined) {
|
|
30
74
|
requestInit.headers["Content-Type"] = "application/json";
|
|
31
|
-
requestInit.body = JSON.stringify(body);
|
|
75
|
+
requestInit.body = JSON.stringify({ ...body, includeCost: true });
|
|
32
76
|
}
|
|
33
77
|
|
|
34
|
-
const requestUrl = query ? appendQueryParams(url, query) : url;
|
|
78
|
+
const requestUrl = query ? appendQueryParams(url, { ...query, includeCost: "true" }) : url;
|
|
35
79
|
const startedAt = Date.now();
|
|
36
|
-
const response = await fetch(requestUrl, requestInit);
|
|
37
|
-
const responseBody = await readResponseBody(response);
|
|
38
|
-
const durationMs = Date.now() - startedAt;
|
|
39
80
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
81
|
+
try {
|
|
82
|
+
const response = await fetch(requestUrl, requestInit);
|
|
83
|
+
const responseBody = await readResponseBody(response);
|
|
84
|
+
const durationMs = Date.now() - startedAt;
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
ok: response.ok,
|
|
88
|
+
status: response.status,
|
|
89
|
+
statusText: response.statusText,
|
|
90
|
+
body: responseBody,
|
|
91
|
+
durationMs,
|
|
92
|
+
request: {
|
|
93
|
+
method,
|
|
94
|
+
url: requestUrl,
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
} catch (error) {
|
|
98
|
+
const durationMs = Date.now() - startedAt;
|
|
99
|
+
|
|
100
|
+
if (error?.name === "TimeoutError" || error?.name === "AbortError") {
|
|
101
|
+
return {
|
|
102
|
+
ok: false,
|
|
103
|
+
status: 504,
|
|
104
|
+
statusText: "Gateway Timeout",
|
|
105
|
+
body: null,
|
|
106
|
+
durationMs,
|
|
107
|
+
timedOut: true,
|
|
108
|
+
request: {
|
|
109
|
+
method,
|
|
110
|
+
url: requestUrl,
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
51
117
|
};
|
|
52
118
|
|
|
53
119
|
/**
|
|
@@ -83,22 +149,76 @@ const readResponseBody = async (response) => {
|
|
|
83
149
|
};
|
|
84
150
|
|
|
85
151
|
/**
|
|
86
|
-
* @param {{ status: number, statusText?: string, body: unknown, durationMs?: number, request?: { method: string, url: string } }} result
|
|
152
|
+
* @param {{ status: number, statusText?: string, body: unknown, durationMs?: number, request?: { method: string, url: string }, timedOut?: boolean }} result
|
|
87
153
|
*/
|
|
88
154
|
const formatProxyResult = (result) => {
|
|
155
|
+
if (result.timedOut) {
|
|
156
|
+
const payload = {
|
|
157
|
+
ok: false,
|
|
158
|
+
httpStatus: result.status,
|
|
159
|
+
statusText: result.statusText,
|
|
160
|
+
durationMs: result.durationMs,
|
|
161
|
+
error: {
|
|
162
|
+
code: "ServiceUnavailable",
|
|
163
|
+
message: SERVICE_UNAVAILABLE_MESSAGE,
|
|
164
|
+
retryable: true,
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
text: JSON.stringify(payload, null, 2),
|
|
170
|
+
structuredContent: payload,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const creditsCharged = extractCreditsCharged(result.body);
|
|
175
|
+
const data = result.ok ? sanitizeResponseData(result.body) : undefined;
|
|
176
|
+
|
|
89
177
|
const payload = {
|
|
178
|
+
ok: result.ok,
|
|
90
179
|
httpStatus: result.status,
|
|
91
180
|
statusText: result.statusText,
|
|
92
181
|
durationMs: result.durationMs,
|
|
93
|
-
|
|
94
|
-
|
|
182
|
+
...(creditsCharged != null ? { creditsCharged } : {}),
|
|
183
|
+
...(result.ok ? { data } : {}),
|
|
95
184
|
};
|
|
96
185
|
|
|
97
|
-
|
|
186
|
+
if (!result.ok) {
|
|
187
|
+
Object.assign(payload, buildProxyErrorPayload(result));
|
|
188
|
+
delete payload.data;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
text: JSON.stringify(payload, null, 2),
|
|
193
|
+
structuredContent: payload,
|
|
194
|
+
};
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* @param {string} message
|
|
199
|
+
*/
|
|
200
|
+
const formatValidationError = (message) => {
|
|
201
|
+
const payload = {
|
|
202
|
+
ok: false,
|
|
203
|
+
error: {
|
|
204
|
+
code: "ValidationError",
|
|
205
|
+
message,
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
text: JSON.stringify(payload, null, 2),
|
|
211
|
+
structuredContent: payload,
|
|
212
|
+
};
|
|
98
213
|
};
|
|
99
214
|
|
|
100
215
|
module.exports = {
|
|
101
216
|
invokeFeature,
|
|
102
217
|
formatProxyResult,
|
|
218
|
+
formatValidationError,
|
|
103
219
|
appendQueryParams,
|
|
220
|
+
applyParameterAliases,
|
|
221
|
+
resolveFeatureRequest,
|
|
222
|
+
extractCreditsCharged,
|
|
223
|
+
classifyUpstreamError,
|
|
104
224
|
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { createHash } = require("node:crypto");
|
|
4
|
+
const { getToolNameOverride } = require("./field-enrichment");
|
|
5
|
+
|
|
6
|
+
const TOOL_NAME_REGEX = /^[a-z][a-z0-9_]{0,127}$/;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Normalize a catalog feature code into a stable MCP tool name.
|
|
10
|
+
* @param {string} code
|
|
11
|
+
*/
|
|
12
|
+
const normalizeToolName = (code) => {
|
|
13
|
+
const override = getToolNameOverride(code);
|
|
14
|
+
if (override) return override;
|
|
15
|
+
|
|
16
|
+
const raw = String(code || "")
|
|
17
|
+
.trim()
|
|
18
|
+
.toLowerCase()
|
|
19
|
+
.replace(/-/g, "_")
|
|
20
|
+
.replace(/[^a-z0-9_]+/g, "_")
|
|
21
|
+
.replace(/_+/g, "_")
|
|
22
|
+
.replace(/^_+|_+$/g, "");
|
|
23
|
+
|
|
24
|
+
if (!raw) return "verifik_feature";
|
|
25
|
+
|
|
26
|
+
if (raw.length <= 128 && TOOL_NAME_REGEX.test(raw)) return raw;
|
|
27
|
+
|
|
28
|
+
const hash = createHash("sha1").update(String(code)).digest("hex").slice(0, 8);
|
|
29
|
+
const prefix = raw.slice(0, 119).replace(/_+$/g, "");
|
|
30
|
+
return `${prefix}_${hash}`;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @param {import("./types").AppFeature[]} features
|
|
35
|
+
*/
|
|
36
|
+
const buildToolNameMapping = (features) => {
|
|
37
|
+
const mapping = new Map();
|
|
38
|
+
|
|
39
|
+
for (const feature of features) {
|
|
40
|
+
if (!feature?.code) continue;
|
|
41
|
+
mapping.set(feature.code, normalizeToolName(feature.code));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return mapping;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
module.exports = {
|
|
48
|
+
normalizeToolName,
|
|
49
|
+
buildToolNameMapping,
|
|
50
|
+
TOOL_NAME_REGEX,
|
|
51
|
+
};
|
package/lib/tool-schema.js
CHANGED
|
@@ -1,29 +1,18 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
const { createHash } = require("node:crypto");
|
|
4
|
+
const {
|
|
5
|
+
resolveFieldEnrichment,
|
|
6
|
+
isHiddenField,
|
|
7
|
+
getValidationRule,
|
|
8
|
+
buildFieldDescription,
|
|
9
|
+
getFeatureEnrichment,
|
|
10
|
+
} = require("./field-enrichment");
|
|
11
|
+
const { buildToolDescription } = require("./description");
|
|
12
|
+
const { normalizeToolName } = require("./tool-naming");
|
|
4
13
|
|
|
5
|
-
const TOOL_NAME_REGEX = /^[A-Za-z0-9._-]{1,128}$/;
|
|
6
14
|
const BINARY_TYPE_PATTERN = /^(buffer|file|binary|multipart|image|blob|base64)$/i;
|
|
7
|
-
const BINARY_FIELD_PATTERN =
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* @param {string} code
|
|
11
|
-
*/
|
|
12
|
-
const sanitizeToolName = (code) => {
|
|
13
|
-
const raw = String(code || "")
|
|
14
|
-
.trim()
|
|
15
|
-
.replace(/[^A-Za-z0-9._-]+/g, "_")
|
|
16
|
-
.replace(/_+/g, "_")
|
|
17
|
-
.replace(/^_+|_+$/g, "");
|
|
18
|
-
|
|
19
|
-
if (!raw) return "verifik_feature";
|
|
20
|
-
|
|
21
|
-
if (raw.length <= 128 && TOOL_NAME_REGEX.test(raw)) return raw;
|
|
22
|
-
|
|
23
|
-
const hash = createHash("sha1").update(String(code)).digest("hex").slice(0, 8);
|
|
24
|
-
const prefix = raw.slice(0, 119).replace(/[._-]+$/g, "");
|
|
25
|
-
return `${prefix}_${hash}`;
|
|
26
|
-
};
|
|
15
|
+
const BINARY_FIELD_PATTERN = /^(image|photo|selfie|file|pdf|attachment|biometric)$/i;
|
|
27
16
|
|
|
28
17
|
/**
|
|
29
18
|
* @param {import("./types").AppFeatureDependency} dependency
|
|
@@ -52,53 +41,145 @@ const isBinaryDependency = (dependency) => {
|
|
|
52
41
|
* @param {import("./types").AppFeature} feature
|
|
53
42
|
*/
|
|
54
43
|
const hasUnsupportedDependencies = (feature) => {
|
|
44
|
+
if (getValidationRule(feature.code) === "document_liveness") return false;
|
|
45
|
+
|
|
55
46
|
const dependencies = Array.isArray(feature?.dependencies) ? feature.dependencies : [];
|
|
56
47
|
return dependencies.some((dependency) => isBinaryDependency(dependency));
|
|
57
48
|
};
|
|
58
49
|
|
|
59
50
|
/**
|
|
60
|
-
* @param {
|
|
61
|
-
*/
|
|
62
|
-
const buildToolDescription = (feature) => {
|
|
63
|
-
const parts = [
|
|
64
|
-
feature.name || feature.code,
|
|
65
|
-
feature.country ? `(${feature.country})` : null,
|
|
66
|
-
feature.description ? `- ${feature.description}` : null,
|
|
67
|
-
"Charges Verifik credits via the REST API.",
|
|
68
|
-
].filter(Boolean);
|
|
69
|
-
|
|
70
|
-
return parts.join(" ");
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
/**
|
|
51
|
+
* @param {string} featureCode
|
|
74
52
|
* @param {import("./types").AppFeatureDependency} dependency
|
|
75
53
|
*/
|
|
76
|
-
const buildDependencyProperty = (dependency) => {
|
|
54
|
+
const buildDependencyProperty = (featureCode, dependency) => {
|
|
55
|
+
const meta = resolveFieldEnrichment(featureCode, dependency.field);
|
|
77
56
|
const property = {
|
|
78
|
-
type: mapDependencyType(dependency),
|
|
57
|
+
type: meta.type || mapDependencyType(dependency),
|
|
79
58
|
};
|
|
80
59
|
|
|
81
|
-
|
|
60
|
+
const description = buildFieldDescription(meta) || dependency.description;
|
|
61
|
+
if (description) property.description = description;
|
|
62
|
+
|
|
82
63
|
if (dependency.default != null) property.default = dependency.default;
|
|
64
|
+
if (meta.default != null) property.default = meta.default;
|
|
83
65
|
if (Array.isArray(dependency.enum) && dependency.enum.length) property.enum = dependency.enum;
|
|
84
|
-
|
|
85
|
-
|
|
66
|
+
|
|
67
|
+
const minLength = meta.minLength ?? parseLength(dependency.min);
|
|
68
|
+
const maxLength = meta.maxLength ?? parseLength(dependency.max);
|
|
69
|
+
|
|
70
|
+
if (minLength != null) property.minLength = minLength;
|
|
71
|
+
if (maxLength != null) property.maxLength = maxLength;
|
|
72
|
+
if (meta.pattern) property.pattern = meta.pattern;
|
|
86
73
|
|
|
87
74
|
return property;
|
|
88
75
|
};
|
|
89
76
|
|
|
77
|
+
/**
|
|
78
|
+
* @param {unknown} value
|
|
79
|
+
*/
|
|
80
|
+
const parseLength = (value) => {
|
|
81
|
+
if (value == null || value === "") return undefined;
|
|
82
|
+
const parsed = Number(value);
|
|
83
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* @param {import("./types").AppFeature} feature
|
|
88
|
+
*/
|
|
89
|
+
const buildDocumentLivenessSchema = (feature) => {
|
|
90
|
+
const fields = getFeatureEnrichment(feature.code).fields || {};
|
|
91
|
+
const properties = {};
|
|
92
|
+
|
|
93
|
+
for (const [field, meta] of Object.entries(fields)) {
|
|
94
|
+
const property = {
|
|
95
|
+
type: meta.type || "string",
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const description = buildFieldDescription(meta);
|
|
99
|
+
if (description) property.description = description;
|
|
100
|
+
if (meta.default != null) property.default = meta.default;
|
|
101
|
+
if (meta.enum) property.enum = meta.enum;
|
|
102
|
+
|
|
103
|
+
properties[field] = property;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
type: "object",
|
|
108
|
+
properties,
|
|
109
|
+
required: ["image"],
|
|
110
|
+
additionalProperties: false,
|
|
111
|
+
};
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* @param {import("./types").AppFeature} feature
|
|
116
|
+
*/
|
|
117
|
+
const buildCriminalSearchSchema = (feature) => {
|
|
118
|
+
const dependencies = Array.isArray(feature?.dependencies) ? feature.dependencies : [];
|
|
119
|
+
const properties = {};
|
|
120
|
+
|
|
121
|
+
for (const dependency of dependencies) {
|
|
122
|
+
if (!dependency?.field || isHiddenField(dependency.field)) continue;
|
|
123
|
+
properties[dependency.field] = buildDependencyProperty(feature.code, dependency);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
type: "object",
|
|
128
|
+
properties,
|
|
129
|
+
anyOf: [
|
|
130
|
+
{ required: ["fullName"] },
|
|
131
|
+
{ required: ["documentType", "documentNumber"] },
|
|
132
|
+
],
|
|
133
|
+
additionalProperties: false,
|
|
134
|
+
};
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* @param {import("./types").AppFeature} feature
|
|
139
|
+
*/
|
|
140
|
+
const buildMatrimonioSearchSchema = (feature) => {
|
|
141
|
+
const dependencies = Array.isArray(feature?.dependencies) ? feature.dependencies : [];
|
|
142
|
+
const properties = {};
|
|
143
|
+
|
|
144
|
+
for (const dependency of dependencies) {
|
|
145
|
+
if (!dependency?.field || isHiddenField(dependency.field)) continue;
|
|
146
|
+
properties[dependency.field] = buildDependencyProperty(feature.code, dependency);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
type: "object",
|
|
151
|
+
properties,
|
|
152
|
+
description:
|
|
153
|
+
"Search by one of: (1) serial alone, (2) documentNumber + sexo, or (3) primerNombre + primerApellido + sexo + fecha.",
|
|
154
|
+
anyOf: [
|
|
155
|
+
{ required: ["serial"] },
|
|
156
|
+
{ required: ["documentNumber", "sexo"] },
|
|
157
|
+
{ required: ["primerNombre", "primerApellido", "sexo", "fecha"] },
|
|
158
|
+
],
|
|
159
|
+
additionalProperties: false,
|
|
160
|
+
};
|
|
161
|
+
};
|
|
162
|
+
|
|
90
163
|
/**
|
|
91
164
|
* @param {import("./types").AppFeature} feature
|
|
92
165
|
*/
|
|
93
166
|
const buildInputSchema = (feature) => {
|
|
167
|
+
const validationRule = getValidationRule(feature.code);
|
|
168
|
+
|
|
169
|
+
if (validationRule === "document_liveness") return buildDocumentLivenessSchema(feature);
|
|
170
|
+
if (validationRule === "criminal_search") return buildCriminalSearchSchema(feature);
|
|
171
|
+
if (validationRule === "matrimonio_search") return buildMatrimonioSearchSchema(feature);
|
|
172
|
+
|
|
94
173
|
const dependencies = Array.isArray(feature?.dependencies) ? feature.dependencies : [];
|
|
95
174
|
const properties = {};
|
|
96
175
|
const required = [];
|
|
97
176
|
|
|
98
177
|
for (const dependency of dependencies) {
|
|
99
|
-
if (!dependency?.field || isBinaryDependency(dependency))
|
|
178
|
+
if (!dependency?.field || isHiddenField(dependency.field) || isBinaryDependency(dependency)) {
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
100
181
|
|
|
101
|
-
properties[dependency.field] = buildDependencyProperty(dependency);
|
|
182
|
+
properties[dependency.field] = buildDependencyProperty(feature.code, dependency);
|
|
102
183
|
|
|
103
184
|
if (dependency.required) required.push(dependency.field);
|
|
104
185
|
}
|
|
@@ -125,11 +206,11 @@ const buildToolRegistry = (features) => {
|
|
|
125
206
|
for (const feature of features) {
|
|
126
207
|
if (!feature?.code || !feature?.url || hasUnsupportedDependencies(feature)) continue;
|
|
127
208
|
|
|
128
|
-
let toolName =
|
|
209
|
+
let toolName = normalizeToolName(feature.code);
|
|
129
210
|
|
|
130
211
|
if (usedNames.has(toolName)) {
|
|
131
212
|
const suffix = createHash("sha1").update(feature.code).digest("hex").slice(0, 6);
|
|
132
|
-
toolName =
|
|
213
|
+
toolName = `${toolName.slice(0, 121)}_${suffix}`;
|
|
133
214
|
}
|
|
134
215
|
|
|
135
216
|
usedNames.add(toolName);
|
|
@@ -145,6 +226,7 @@ const buildToolRegistry = (features) => {
|
|
|
145
226
|
|
|
146
227
|
/**
|
|
147
228
|
* @param {import("./types").AppFeature} feature
|
|
229
|
+
* @param {string} toolName
|
|
148
230
|
*/
|
|
149
231
|
const toMcpToolDefinition = (feature, toolName) => ({
|
|
150
232
|
name: toolName,
|
|
@@ -153,12 +235,11 @@ const toMcpToolDefinition = (feature, toolName) => ({
|
|
|
153
235
|
});
|
|
154
236
|
|
|
155
237
|
module.exports = {
|
|
156
|
-
sanitizeToolName,
|
|
157
238
|
mapDependencyType,
|
|
158
239
|
isBinaryDependency,
|
|
159
240
|
hasUnsupportedDependencies,
|
|
160
|
-
buildToolDescription,
|
|
161
241
|
buildInputSchema,
|
|
162
242
|
buildToolRegistry,
|
|
163
243
|
toMcpToolDefinition,
|
|
244
|
+
normalizeToolName,
|
|
164
245
|
};
|