@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
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const CLIENT_FEATURES_PATH = "/v2/client-features";
|
|
4
|
+
const PAGE_LIMIT = 500;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {import("./types").McpConfig} config
|
|
8
|
+
*/
|
|
9
|
+
const fetchClientFeaturesPage = async (config, page = 1) => {
|
|
10
|
+
const url = new URL(`${config.apiBase}${CLIENT_FEATURES_PATH}`);
|
|
11
|
+
url.searchParams.set("page", String(page));
|
|
12
|
+
url.searchParams.set("limit", String(PAGE_LIMIT));
|
|
13
|
+
|
|
14
|
+
const response = await fetch(url, {
|
|
15
|
+
method: "GET",
|
|
16
|
+
headers: {
|
|
17
|
+
Authorization: `Bearer ${config.token}`,
|
|
18
|
+
Accept: "application/json",
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
if (!response.ok) {
|
|
23
|
+
throw new Error(`Client features request failed (${response.status})`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const body = await response.json();
|
|
27
|
+
const data = Array.isArray(body?.data)
|
|
28
|
+
? body.data
|
|
29
|
+
: Array.isArray(body?.data?.docs)
|
|
30
|
+
? body.data.docs
|
|
31
|
+
: [];
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
features: data,
|
|
35
|
+
page: Number(body?.page) || 1,
|
|
36
|
+
pages: Number(body?.pages) || 1,
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @param {import("./types").McpConfig} config
|
|
42
|
+
*/
|
|
43
|
+
const fetchGrantedFeatureCodes = async (config) => {
|
|
44
|
+
const firstPage = await fetchClientFeaturesPage(config, 1);
|
|
45
|
+
const codes = new Set();
|
|
46
|
+
|
|
47
|
+
for (const feature of firstPage.features) {
|
|
48
|
+
if (feature?.code) codes.add(feature.code);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
for (let page = firstPage.page + 1; page <= firstPage.pages; page += 1) {
|
|
52
|
+
const nextPage = await fetchClientFeaturesPage(config, page);
|
|
53
|
+
for (const feature of nextPage.features) {
|
|
54
|
+
if (feature?.code) codes.add(feature.code);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return codes;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
module.exports = {
|
|
62
|
+
CLIENT_FEATURES_PATH,
|
|
63
|
+
fetchGrantedFeatureCodes,
|
|
64
|
+
};
|
package/lib/config.js
CHANGED
|
@@ -18,6 +18,7 @@ const loadConfig = () => {
|
|
|
18
18
|
const baseCategoryFilter = parseListEnv(process.env.VERIFIK_MCP_BASE_CATEGORY);
|
|
19
19
|
const codeAllowlist = parseListEnv(process.env.VERIFIK_MCP_CODES);
|
|
20
20
|
const catalogRefreshMs = parsePositiveInt(process.env.VERIFIK_MCP_CATALOG_REFRESH_MS, 0);
|
|
21
|
+
const requestTimeoutMs = parsePositiveInt(process.env.VERIFIK_MCP_REQUEST_TIMEOUT_MS, 90_000);
|
|
21
22
|
|
|
22
23
|
return {
|
|
23
24
|
apiBase,
|
|
@@ -27,6 +28,7 @@ const loadConfig = () => {
|
|
|
27
28
|
baseCategoryFilter,
|
|
28
29
|
codeAllowlist,
|
|
29
30
|
catalogRefreshMs,
|
|
31
|
+
requestTimeoutMs,
|
|
30
32
|
};
|
|
31
33
|
};
|
|
32
34
|
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const ROUTE_PATTERN = /\b(?:GET|POST|PUT|PATCH|DELETE)\s+\/v?\d*\/[^\s.]+\b/gi;
|
|
4
|
+
const ALIAS_PATTERN = /\bAlias:\s*[^\n.]+\.?/gi;
|
|
5
|
+
const INTERNAL_JARGON = /\bNo name sources\.?\b/gi;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @param {string} text
|
|
9
|
+
*/
|
|
10
|
+
const stripCountryPrefix = (text) => {
|
|
11
|
+
return String(text || "")
|
|
12
|
+
.replace(/^[A-Za-zÀ-ÿ\s]+?\s*[-—]\s*/u, "")
|
|
13
|
+
.trim();
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {string} text
|
|
18
|
+
*/
|
|
19
|
+
const stripParentheticalCountry = (text) => {
|
|
20
|
+
return String(text || "").replace(/\s*\([A-Za-zÀ-ÿ\s]+\)\s*/gu, " ").trim();
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {string} text
|
|
25
|
+
*/
|
|
26
|
+
const cleanDescriptionText = (text) => {
|
|
27
|
+
return String(text || "")
|
|
28
|
+
.replace(ROUTE_PATTERN, "")
|
|
29
|
+
.replace(ALIAS_PATTERN, "")
|
|
30
|
+
.replace(INTERNAL_JARGON, "")
|
|
31
|
+
.replace(/\s+/g, " ")
|
|
32
|
+
.trim();
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @param {number | null | undefined} price
|
|
37
|
+
*/
|
|
38
|
+
const formatPrice = (price) => {
|
|
39
|
+
const value = Number(price);
|
|
40
|
+
if (!Number.isFinite(value)) return null;
|
|
41
|
+
return value.toFixed(value % 1 === 0 ? 0 : 2);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @param {import("./types").AppFeature} feature
|
|
46
|
+
*/
|
|
47
|
+
const buildToolDescription = (feature) => {
|
|
48
|
+
const rawDescription = cleanDescriptionText(feature.description || "");
|
|
49
|
+
const rawName = cleanDescriptionText(feature.name || feature.code || "Verifik feature");
|
|
50
|
+
const withoutCountryInName = stripParentheticalCountry(stripCountryPrefix(rawName));
|
|
51
|
+
const descriptionBody = rawDescription || withoutCountryInName;
|
|
52
|
+
const price = formatPrice(feature.smartCheckPrice ?? feature.price);
|
|
53
|
+
|
|
54
|
+
const parts = [descriptionBody];
|
|
55
|
+
|
|
56
|
+
if (price != null) {
|
|
57
|
+
parts.push(`Costs ${price} credit${price === "1" ? "" : "s"} per successful call.`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return parts.join(" ");
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
module.exports = {
|
|
64
|
+
cleanDescriptionText,
|
|
65
|
+
buildToolDescription,
|
|
66
|
+
formatPrice,
|
|
67
|
+
};
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const SERVICE_UNAVAILABLE_MESSAGE =
|
|
4
|
+
"The data source is temporarily unavailable; try again later.";
|
|
5
|
+
|
|
6
|
+
const FEATURE_NOT_ENABLED_MESSAGE =
|
|
7
|
+
"This feature is not enabled on your Verifik account. Contact support to request access.";
|
|
8
|
+
|
|
9
|
+
const FEATURE_UNAVAILABLE_MESSAGE =
|
|
10
|
+
"This feature is currently unavailable; contact support.";
|
|
11
|
+
|
|
12
|
+
const NOT_FOUND_MESSAGE = "No record was found for the provided data.";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param {unknown} body
|
|
16
|
+
*/
|
|
17
|
+
const isHtmlBody = (body) => {
|
|
18
|
+
if (typeof body !== "string") return false;
|
|
19
|
+
const trimmed = body.trim().toLowerCase();
|
|
20
|
+
return trimmed.startsWith("<!doctype") || trimmed.startsWith("<html");
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {unknown} body
|
|
25
|
+
*/
|
|
26
|
+
const isVerifikJsonBody = (body) => {
|
|
27
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) return false;
|
|
28
|
+
return typeof body.code === "string" || typeof body.message === "string";
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Verifik API error envelope (code + message, often with id).
|
|
33
|
+
* @param {unknown} body
|
|
34
|
+
*/
|
|
35
|
+
const isVerifikBusinessErrorBody = (body) => {
|
|
36
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) return false;
|
|
37
|
+
return typeof body.code === "string" && typeof body.message === "string";
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @param {unknown} code
|
|
42
|
+
* @param {unknown} message
|
|
43
|
+
*/
|
|
44
|
+
const isEndpointOutOfService = (code, message) => {
|
|
45
|
+
const haystack = `${String(code || "")} ${String(message || "")}`.toLowerCase();
|
|
46
|
+
return haystack.includes("endpoint_out_of_service") || haystack.includes("endpoint out of service");
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @param {unknown} body
|
|
51
|
+
*/
|
|
52
|
+
const extractRequestId = (body) => {
|
|
53
|
+
if (!body || typeof body !== "object") return undefined;
|
|
54
|
+
if (typeof body.id === "string" && body.id) return body.id;
|
|
55
|
+
return undefined;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @param {unknown} body
|
|
60
|
+
*/
|
|
61
|
+
const extractCreditsCharged = (body) => {
|
|
62
|
+
if (!body || typeof body !== "object") return null;
|
|
63
|
+
|
|
64
|
+
const creditsCharged = body.creditsCharged;
|
|
65
|
+
|
|
66
|
+
if (typeof creditsCharged === "number" && Number.isFinite(creditsCharged)) {
|
|
67
|
+
return creditsCharged;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return null;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* @param {unknown} message
|
|
75
|
+
*/
|
|
76
|
+
const isUpstreamGatewayMessage = (message) => {
|
|
77
|
+
if (typeof message !== "string") return false;
|
|
78
|
+
return /request failed with status code 50[234]/i.test(message) || /gateway timeout/i.test(message);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @param {number} status
|
|
83
|
+
* @param {unknown} body
|
|
84
|
+
*/
|
|
85
|
+
const classifyUpstreamError = (status, body) => {
|
|
86
|
+
const isJsonObject = isVerifikJsonBody(body);
|
|
87
|
+
const errorBody = isJsonObject ? body : {};
|
|
88
|
+
const apiCode = typeof errorBody.code === "string" ? errorBody.code : "";
|
|
89
|
+
const apiMessage = typeof errorBody.message === "string" ? errorBody.message : "";
|
|
90
|
+
const plainText = typeof body === "string" ? body.trim() : "";
|
|
91
|
+
|
|
92
|
+
if (status === 403 && apiMessage.toLowerCase().includes("no access to this feature")) {
|
|
93
|
+
return {
|
|
94
|
+
code: "FeatureNotEnabled",
|
|
95
|
+
message: FEATURE_NOT_ENABLED_MESSAGE,
|
|
96
|
+
retryable: false,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (
|
|
101
|
+
isEndpointOutOfService(apiCode, apiMessage) ||
|
|
102
|
+
isEndpointOutOfService("", plainText)
|
|
103
|
+
) {
|
|
104
|
+
return {
|
|
105
|
+
code: "ServiceUnavailable",
|
|
106
|
+
message: SERVICE_UNAVAILABLE_MESSAGE,
|
|
107
|
+
retryable: true,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (
|
|
112
|
+
status === 502 ||
|
|
113
|
+
status === 503 ||
|
|
114
|
+
status === 504 ||
|
|
115
|
+
status >= 500 ||
|
|
116
|
+
isUpstreamGatewayMessage(apiMessage) ||
|
|
117
|
+
isUpstreamGatewayMessage(plainText) ||
|
|
118
|
+
isHtmlBody(body)
|
|
119
|
+
) {
|
|
120
|
+
return {
|
|
121
|
+
code: "ServiceUnavailable",
|
|
122
|
+
message: SERVICE_UNAVAILABLE_MESSAGE,
|
|
123
|
+
retryable: true,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (status === 404) {
|
|
128
|
+
if (isVerifikBusinessErrorBody(body)) {
|
|
129
|
+
return {
|
|
130
|
+
code: "NotFound",
|
|
131
|
+
message: NOT_FOUND_MESSAGE,
|
|
132
|
+
retryable: false,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
code: "FeatureUnavailable",
|
|
138
|
+
message: FEATURE_UNAVAILABLE_MESSAGE,
|
|
139
|
+
retryable: false,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (apiCode || apiMessage) {
|
|
144
|
+
return {
|
|
145
|
+
code: apiCode || "RequestFailed",
|
|
146
|
+
message: apiMessage || "Request failed",
|
|
147
|
+
retryable: false,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (plainText) {
|
|
152
|
+
return {
|
|
153
|
+
code: "RequestFailed",
|
|
154
|
+
message: plainText,
|
|
155
|
+
retryable: false,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
code: "RequestFailed",
|
|
161
|
+
message: "Request failed",
|
|
162
|
+
retryable: false,
|
|
163
|
+
};
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* @param {{ status: number, statusText?: string, body: unknown, durationMs?: number }} result
|
|
168
|
+
*/
|
|
169
|
+
const buildProxyErrorPayload = (result) => {
|
|
170
|
+
const requestId = extractRequestId(result.body);
|
|
171
|
+
const classified = classifyUpstreamError(result.status, result.body);
|
|
172
|
+
|
|
173
|
+
const payload = {
|
|
174
|
+
ok: false,
|
|
175
|
+
httpStatus: result.status,
|
|
176
|
+
statusText: result.statusText,
|
|
177
|
+
durationMs: result.durationMs,
|
|
178
|
+
error: classified,
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
if (requestId) payload.requestId = requestId;
|
|
182
|
+
|
|
183
|
+
return payload;
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* @param {unknown} body
|
|
188
|
+
*/
|
|
189
|
+
const sanitizeResponseData = (body) => {
|
|
190
|
+
if (isHtmlBody(body)) return null;
|
|
191
|
+
if (!body || typeof body !== "object") return body;
|
|
192
|
+
if ("data" in body) return body.data;
|
|
193
|
+
return body;
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
module.exports = {
|
|
197
|
+
SERVICE_UNAVAILABLE_MESSAGE,
|
|
198
|
+
FEATURE_NOT_ENABLED_MESSAGE,
|
|
199
|
+
FEATURE_UNAVAILABLE_MESSAGE,
|
|
200
|
+
NOT_FOUND_MESSAGE,
|
|
201
|
+
isHtmlBody,
|
|
202
|
+
isVerifikJsonBody,
|
|
203
|
+
isVerifikBusinessErrorBody,
|
|
204
|
+
isEndpointOutOfService,
|
|
205
|
+
extractRequestId,
|
|
206
|
+
extractCreditsCharged,
|
|
207
|
+
classifyUpstreamError,
|
|
208
|
+
buildProxyErrorPayload,
|
|
209
|
+
sanitizeResponseData,
|
|
210
|
+
};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const path = require("node:path");
|
|
4
|
+
|
|
5
|
+
/** @type {import("./field-enrichment.types")} */
|
|
6
|
+
const enrichmentData = require(path.join(__dirname, "..", "data", "field-enrichment.json"));
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @param {string} featureCode
|
|
10
|
+
*/
|
|
11
|
+
const getFeatureEnrichment = (featureCode) => {
|
|
12
|
+
return enrichmentData.features[featureCode] || {};
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {string} fieldName
|
|
17
|
+
*/
|
|
18
|
+
const getDefaultFieldEnrichment = (fieldName) => {
|
|
19
|
+
return enrichmentData.defaultFields[fieldName] || {};
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {string} featureCode
|
|
24
|
+
* @param {string} fieldName
|
|
25
|
+
*/
|
|
26
|
+
const resolveFieldEnrichment = (featureCode, fieldName) => {
|
|
27
|
+
const featureFields = getFeatureEnrichment(featureCode).fields || {};
|
|
28
|
+
const featureField = featureFields[fieldName] || {};
|
|
29
|
+
const defaultField = getDefaultFieldEnrichment(fieldName);
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
...defaultField,
|
|
33
|
+
...featureField,
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @param {string} featureCode
|
|
39
|
+
*/
|
|
40
|
+
const getToolNameOverride = (featureCode) => {
|
|
41
|
+
return enrichmentData.toolNameOverrides[featureCode] || null;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @param {string} featureCode
|
|
46
|
+
*/
|
|
47
|
+
const getRequestOverride = (featureCode) => {
|
|
48
|
+
return enrichmentData.requestOverrides[featureCode] || null;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @param {string} featureCode
|
|
53
|
+
*/
|
|
54
|
+
const getParameterAliases = (featureCode) => {
|
|
55
|
+
return enrichmentData.parameterAliases[featureCode] || {};
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @param {string} fieldName
|
|
60
|
+
*/
|
|
61
|
+
const isHiddenField = (fieldName) => {
|
|
62
|
+
return enrichmentData.hiddenFields.includes(fieldName);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @param {string} featureCode
|
|
67
|
+
*/
|
|
68
|
+
const getValidationRule = (featureCode) => {
|
|
69
|
+
const feature = getFeatureEnrichment(featureCode);
|
|
70
|
+
return feature.validation || null;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* @param {string} fieldMeta
|
|
75
|
+
*/
|
|
76
|
+
const buildFieldDescription = (fieldMeta) => {
|
|
77
|
+
if (!fieldMeta?.description) return undefined;
|
|
78
|
+
|
|
79
|
+
const parts = [fieldMeta.description];
|
|
80
|
+
|
|
81
|
+
if (fieldMeta.format) {
|
|
82
|
+
parts.push(`Format: ${fieldMeta.format}.`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (fieldMeta.example) {
|
|
86
|
+
parts.push(`Example: ${fieldMeta.example}.`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return parts.join(" ");
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
module.exports = {
|
|
93
|
+
enrichmentData,
|
|
94
|
+
getFeatureEnrichment,
|
|
95
|
+
getDefaultFieldEnrichment,
|
|
96
|
+
resolveFieldEnrichment,
|
|
97
|
+
getToolNameOverride,
|
|
98
|
+
getRequestOverride,
|
|
99
|
+
getParameterAliases,
|
|
100
|
+
isHiddenField,
|
|
101
|
+
getValidationRule,
|
|
102
|
+
buildFieldDescription,
|
|
103
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/** Fields where common punctuation (dots, dashes, slashes, spaces) may appear but length limits apply to alphanumeric content. */
|
|
4
|
+
const PUNCTUATION_TOLERANT_FIELDS = new Set([
|
|
5
|
+
"documentNumber",
|
|
6
|
+
"plate",
|
|
7
|
+
"card",
|
|
8
|
+
"serial",
|
|
9
|
+
"citizenIdentifier",
|
|
10
|
+
"emissionNumber",
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Remove common formatting characters used in ID numbers and plates.
|
|
15
|
+
* @param {unknown} value
|
|
16
|
+
*/
|
|
17
|
+
const stripFormatting = (value) => {
|
|
18
|
+
return String(value).replace(/[.\-\s/]/g, "");
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param {string} fieldName
|
|
23
|
+
*/
|
|
24
|
+
const isPunctuationTolerantField = (fieldName) => {
|
|
25
|
+
return PUNCTUATION_TOLERANT_FIELDS.has(fieldName);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Value used for minLength/maxLength validation (punctuation ignored for tolerant fields).
|
|
30
|
+
* @param {string} fieldName
|
|
31
|
+
* @param {unknown} value
|
|
32
|
+
*/
|
|
33
|
+
const valueForLengthCheck = (fieldName, value) => {
|
|
34
|
+
const text = String(value);
|
|
35
|
+
if (!isPunctuationTolerantField(fieldName)) return text;
|
|
36
|
+
return stripFormatting(text);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Normalize request params: trim plate whitespace; leave document numbers as entered.
|
|
41
|
+
* @param {Record<string, unknown>} params
|
|
42
|
+
*/
|
|
43
|
+
const normalizeRequestParams = (params) => {
|
|
44
|
+
if (!params || typeof params !== "object") return {};
|
|
45
|
+
|
|
46
|
+
const normalized = { ...params };
|
|
47
|
+
|
|
48
|
+
for (const [field, value] of Object.entries(normalized)) {
|
|
49
|
+
if (value == null || value === "") continue;
|
|
50
|
+
|
|
51
|
+
if (field === "plate" || field === "card") {
|
|
52
|
+
normalized[field] = String(value).replace(/\s+/g, "").trim();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return normalized;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
module.exports = {
|
|
60
|
+
PUNCTUATION_TOLERANT_FIELDS,
|
|
61
|
+
stripFormatting,
|
|
62
|
+
isPunctuationTolerantField,
|
|
63
|
+
valueForLengthCheck,
|
|
64
|
+
normalizeRequestParams,
|
|
65
|
+
};
|
package/lib/filters.js
CHANGED
|
@@ -3,23 +3,30 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* @param {import("./types").AppFeature} feature
|
|
5
5
|
* @param {import("./types").McpConfig} config
|
|
6
|
+
* @param {Set<string>|null} grantedFeatureCodes
|
|
6
7
|
*/
|
|
7
|
-
const isEligibleFeature = (feature, config) => {
|
|
8
|
+
const isEligibleFeature = (feature, config, grantedFeatureCodes = null) => {
|
|
8
9
|
if (!feature?.code || !feature?.url) return false;
|
|
9
10
|
if (config.smartCheckOnly && !feature.smartCheckEnabled) return false;
|
|
10
11
|
if (config.codeAllowlist.length && !config.codeAllowlist.includes(feature.code)) return false;
|
|
11
12
|
if (config.countryFilter.length && !config.countryFilter.includes(feature.country)) return false;
|
|
12
13
|
if (config.baseCategoryFilter.length && !config.baseCategoryFilter.includes(feature.baseCategory)) return false;
|
|
14
|
+
|
|
15
|
+
if (feature.requiresClientFeature && grantedFeatureCodes && !grantedFeatureCodes.has(feature.code)) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
|
|
13
19
|
return true;
|
|
14
20
|
};
|
|
15
21
|
|
|
16
22
|
/**
|
|
17
23
|
* @param {import("./types").AppFeature[]} features
|
|
18
24
|
* @param {import("./types").McpConfig} config
|
|
25
|
+
* @param {Set<string>|null} [grantedFeatureCodes]
|
|
19
26
|
*/
|
|
20
|
-
const filterFeatures = (features, config) => {
|
|
27
|
+
const filterFeatures = (features, config, grantedFeatureCodes = null) => {
|
|
21
28
|
if (!Array.isArray(features)) return [];
|
|
22
|
-
return features.filter((feature) => isEligibleFeature(feature, config));
|
|
29
|
+
return features.filter((feature) => isEligibleFeature(feature, config, grantedFeatureCodes));
|
|
23
30
|
};
|
|
24
31
|
|
|
25
32
|
/**
|
package/lib/meta-tools.js
CHANGED
|
@@ -97,8 +97,11 @@ const handleGetFeature = async (catalogCache, args = {}) => {
|
|
|
97
97
|
throw new Error(`feature_not_found:${code}`);
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
const toolName = catalogCache.state?.registry?.featureCodeToToolName?.get(code) || null;
|
|
101
|
+
|
|
100
102
|
return {
|
|
101
103
|
...toFeatureSummary(feature),
|
|
104
|
+
toolName,
|
|
102
105
|
dependencies: feature.dependencies || [],
|
|
103
106
|
inputSchema: buildInputSchema(feature),
|
|
104
107
|
};
|