@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/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 method = normalizeHttpMethod(feature.method);
17
- const url = buildFeatureRequestUrl(config.apiBase, feature.url);
18
- const params = sanitizeRequestParams(args);
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
- return {
41
- ok: response.ok,
42
- status: response.status,
43
- statusText: response.statusText,
44
- body: responseBody,
45
- durationMs,
46
- request: {
47
- method,
48
- url: requestUrl,
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
- request: result.request,
94
- body: result.body,
182
+ ...(creditsCharged != null ? { creditsCharged } : {}),
183
+ ...(result.ok ? { data } : {}),
95
184
  };
96
185
 
97
- return JSON.stringify(payload, null, 2);
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
+ };
@@ -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 = /(image|photo|selfie|file|document|pdf|attachment|biometric)/i;
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 {import("./types").AppFeature} feature
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
- if (dependency.description) property.description = dependency.description;
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
- if (dependency.min != null) property.minimum = dependency.min;
85
- if (dependency.max != null) property.maximum = dependency.max;
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)) continue;
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 = sanitizeToolName(feature.code);
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 = sanitizeToolName(`${feature.code}_${suffix}`);
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
  };