@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,230 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
getValidationRule,
|
|
5
|
+
resolveFieldEnrichment,
|
|
6
|
+
buildFieldDescription,
|
|
7
|
+
} = require("./field-enrichment");
|
|
8
|
+
const { valueForLengthCheck } = require("./field-normalize");
|
|
9
|
+
|
|
10
|
+
const DD_MM_YYYY = /^(0[1-9]|[12][0-9]|3[01])\/(0[1-9]|1[0-2])\/\d{4}$/;
|
|
11
|
+
|
|
12
|
+
const CRIMINAL_DOB_TYPES = new Set(["CCPA", "CI", "CPF", "DUI", "CCUY"]);
|
|
13
|
+
const CRIMINAL_EXPEDITION_TYPES = new Set(["CE", "PPT", "PEP"]);
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {unknown} value
|
|
17
|
+
*/
|
|
18
|
+
const hasValue = (value) => value !== undefined && value !== null && value !== "";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {unknown} value
|
|
22
|
+
*/
|
|
23
|
+
const isTruthyBoolean = (value) => value === true || value === "true";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {Record<string, unknown>} args
|
|
27
|
+
* @param {string} field
|
|
28
|
+
* @param {Record<string, unknown>} meta
|
|
29
|
+
*/
|
|
30
|
+
const validateStringField = (args, field, meta) => {
|
|
31
|
+
const value = args[field];
|
|
32
|
+
|
|
33
|
+
if (!hasValue(value)) return null;
|
|
34
|
+
|
|
35
|
+
const text = String(value);
|
|
36
|
+
const measured = valueForLengthCheck(field, text);
|
|
37
|
+
|
|
38
|
+
if (meta.minLength != null && measured.length < Number(meta.minLength)) {
|
|
39
|
+
return `${field} must be at least ${meta.minLength} characters (punctuation such as dots or dashes is ignored).`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (meta.maxLength != null && measured.length > Number(meta.maxLength)) {
|
|
43
|
+
return `${field} must be at most ${meta.maxLength} characters (punctuation such as dots or dashes is ignored).`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (meta.pattern) {
|
|
47
|
+
const regex = new RegExp(meta.pattern);
|
|
48
|
+
if (!regex.test(text)) {
|
|
49
|
+
const formatHint = meta.format ? ` Expected format: ${meta.format}.` : "";
|
|
50
|
+
return `${field} has an invalid format.${formatHint}`;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (meta.format === "DD/MM/YYYY" && !DD_MM_YYYY.test(text)) {
|
|
55
|
+
return `${field} must be DD/MM/YYYY (example: 15/03/1990).`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return null;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @param {Record<string, unknown>} args
|
|
63
|
+
* @param {import("./types").AppFeature} feature
|
|
64
|
+
* @param {object} inputSchema
|
|
65
|
+
*/
|
|
66
|
+
const validateRequiredFields = (args, feature, inputSchema) => {
|
|
67
|
+
const required = Array.isArray(inputSchema.required) ? inputSchema.required : [];
|
|
68
|
+
|
|
69
|
+
for (const field of required) {
|
|
70
|
+
if (!hasValue(args[field])) {
|
|
71
|
+
const meta = resolveFieldEnrichment(feature.code, field);
|
|
72
|
+
const hint = buildFieldDescription(meta);
|
|
73
|
+
return hint ? `${field} is required. ${hint}` : `${field} is required.`;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return null;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* @param {Record<string, unknown>} args
|
|
82
|
+
* @param {import("./types").AppFeature} feature
|
|
83
|
+
* @param {object} inputSchema
|
|
84
|
+
*/
|
|
85
|
+
const validateFieldConstraints = (args, feature, inputSchema) => {
|
|
86
|
+
const properties = inputSchema.properties || {};
|
|
87
|
+
|
|
88
|
+
for (const [field, schema] of Object.entries(properties)) {
|
|
89
|
+
if (!hasValue(args[field])) continue;
|
|
90
|
+
|
|
91
|
+
if (schema.enum && !schema.enum.includes(args[field])) {
|
|
92
|
+
return `${field} must be one of: ${schema.enum.join(", ")}.`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (schema.type === "number" && Number.isNaN(Number(args[field]))) {
|
|
96
|
+
return `${field} must be a number.`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (schema.type === "boolean" && typeof args[field] !== "boolean") {
|
|
100
|
+
return `${field} must be true or false.`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (schema.type === "string") {
|
|
104
|
+
const meta = resolveFieldEnrichment(feature.code, field);
|
|
105
|
+
const error = validateStringField(args, field, { ...meta, ...schema });
|
|
106
|
+
if (error) return error;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return null;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* @param {Record<string, unknown>} args
|
|
115
|
+
*/
|
|
116
|
+
const validateCriminalSearch = (args) => {
|
|
117
|
+
const fullName = hasValue(args.fullName);
|
|
118
|
+
const documentType = hasValue(args.documentType);
|
|
119
|
+
const documentNumber = hasValue(args.documentNumber);
|
|
120
|
+
|
|
121
|
+
if (!fullName && !(documentType && documentNumber)) {
|
|
122
|
+
return "Provide fullName, or both documentType and documentNumber.";
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (!fullName) {
|
|
126
|
+
if (!documentType) return "documentType is required when searching by document.";
|
|
127
|
+
if (!documentNumber) return "documentNumber is required when searching by document.";
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (documentType && CRIMINAL_DOB_TYPES.has(String(args.documentType)) && !hasValue(args.dateOfBirth)) {
|
|
131
|
+
return `dateOfBirth is required for documentType ${args.documentType}. Format: DD/MM/YYYY.`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (
|
|
135
|
+
documentType &&
|
|
136
|
+
CRIMINAL_EXPEDITION_TYPES.has(String(args.documentType)) &&
|
|
137
|
+
!hasValue(args.expirationDate)
|
|
138
|
+
) {
|
|
139
|
+
return `expirationDate is required for documentType ${args.documentType}. Format: DD/MM/YYYY.`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (hasValue(args.dateOfBirth) && !DD_MM_YYYY.test(String(args.dateOfBirth))) {
|
|
143
|
+
return "dateOfBirth must be DD/MM/YYYY (example: 15/03/1990).";
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (hasValue(args.expirationDate) && !DD_MM_YYYY.test(String(args.expirationDate))) {
|
|
147
|
+
return "expirationDate must be DD/MM/YYYY (example: 01/12/2023).";
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return null;
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* @param {Record<string, unknown>} args
|
|
155
|
+
*/
|
|
156
|
+
const validateMatrimonioSearch = (args) => {
|
|
157
|
+
const hasSerial = hasValue(args.serial);
|
|
158
|
+
const hasDocument = hasValue(args.documentNumber);
|
|
159
|
+
const hasName =
|
|
160
|
+
hasValue(args.primerNombre) &&
|
|
161
|
+
hasValue(args.primerApellido) &&
|
|
162
|
+
hasValue(args.sexo) &&
|
|
163
|
+
hasValue(args.fecha);
|
|
164
|
+
|
|
165
|
+
if (!hasSerial && !hasDocument && !hasName) {
|
|
166
|
+
return "Provide serial, or documentNumber with sexo, or primerNombre + primerApellido + sexo + fecha.";
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (hasDocument && !hasValue(args.sexo)) {
|
|
170
|
+
return "sexo is required when searching by documentNumber.";
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (hasName && !DD_MM_YYYY.test(String(args.fecha))) {
|
|
174
|
+
return "fecha must be DD/MM/YYYY (example: 15/06/2010).";
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return null;
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* @param {Record<string, unknown>} args
|
|
182
|
+
*/
|
|
183
|
+
const validateDocumentLiveness = (args) => {
|
|
184
|
+
if (!hasValue(args.image)) {
|
|
185
|
+
return "image is required. Provide a base64-encoded JPEG/PNG string or a public HTTPS URL.";
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const validations = [
|
|
189
|
+
"validateScreenReplay",
|
|
190
|
+
"validatePrintedCopy",
|
|
191
|
+
"validatePortraitSubstitution",
|
|
192
|
+
"validateDigitalManipulation",
|
|
193
|
+
];
|
|
194
|
+
|
|
195
|
+
if (!validations.some((field) => isTruthyBoolean(args[field]))) {
|
|
196
|
+
return "Enable at least one validation: validateScreenReplay, validatePrintedCopy, validatePortraitSubstitution, or validateDigitalManipulation.";
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return null;
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* @param {Record<string, unknown>} args
|
|
204
|
+
* @param {import("./types").AppFeature} feature
|
|
205
|
+
* @param {object} inputSchema
|
|
206
|
+
*/
|
|
207
|
+
const validateToolArgs = (args, feature, inputSchema) => {
|
|
208
|
+
const params = args && typeof args === "object" ? args : {};
|
|
209
|
+
|
|
210
|
+
const requiredError = validateRequiredFields(params, feature, inputSchema);
|
|
211
|
+
if (requiredError) return requiredError;
|
|
212
|
+
|
|
213
|
+
const constraintError = validateFieldConstraints(params, feature, inputSchema);
|
|
214
|
+
if (constraintError) return constraintError;
|
|
215
|
+
|
|
216
|
+
const rule = getValidationRule(feature.code);
|
|
217
|
+
|
|
218
|
+
if (rule === "criminal_search") return validateCriminalSearch(params);
|
|
219
|
+
if (rule === "matrimonio_search") return validateMatrimonioSearch(params);
|
|
220
|
+
if (rule === "document_liveness") return validateDocumentLiveness(params);
|
|
221
|
+
|
|
222
|
+
return null;
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
module.exports = {
|
|
226
|
+
validateToolArgs,
|
|
227
|
+
validateCriminalSearch,
|
|
228
|
+
validateMatrimonioSearch,
|
|
229
|
+
validateDocumentLiveness,
|
|
230
|
+
};
|
package/package.json
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@verifik/mcp",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "Use Verifik identity verification, document checks, and background-screening APIs from Cursor, Claude Desktop, or any MCP client — across Latin America and worldwide.",
|
|
5
5
|
"main": "server.js",
|
|
6
6
|
"bin": {
|
|
7
|
-
"verifik-mcp": "
|
|
7
|
+
"verifik-mcp": "server.js"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"server.js",
|
|
11
11
|
"lib/",
|
|
12
|
+
"data/",
|
|
13
|
+
"docs/",
|
|
12
14
|
"README.md"
|
|
13
15
|
],
|
|
14
16
|
"scripts": {
|
|
@@ -21,9 +23,23 @@
|
|
|
21
23
|
},
|
|
22
24
|
"keywords": [
|
|
23
25
|
"mcp",
|
|
26
|
+
"model-context-protocol",
|
|
24
27
|
"verifik",
|
|
25
28
|
"smartcheck",
|
|
26
|
-
"
|
|
29
|
+
"identity-verification",
|
|
30
|
+
"kyc",
|
|
31
|
+
"kyb",
|
|
32
|
+
"aml",
|
|
33
|
+
"background-check",
|
|
34
|
+
"database-screening",
|
|
35
|
+
"latin-america",
|
|
36
|
+
"colombia",
|
|
37
|
+
"mexico",
|
|
38
|
+
"peru",
|
|
39
|
+
"chile",
|
|
40
|
+
"brazil",
|
|
41
|
+
"cursor",
|
|
42
|
+
"claude"
|
|
27
43
|
],
|
|
28
44
|
"author": "Open-Verifik",
|
|
29
45
|
"license": "MIT",
|
|
@@ -31,9 +47,9 @@
|
|
|
31
47
|
"type": "git",
|
|
32
48
|
"url": "git+https://github.com/Open-Verifik/verifik-mcp.git"
|
|
33
49
|
},
|
|
34
|
-
"homepage": "https://
|
|
50
|
+
"homepage": "https://docs.verifik.co",
|
|
35
51
|
"bugs": {
|
|
36
|
-
"url": "https://
|
|
52
|
+
"url": "https://verifik.co"
|
|
37
53
|
},
|
|
38
54
|
"publishConfig": {
|
|
39
55
|
"access": "public"
|
package/server.js
CHANGED
|
@@ -10,13 +10,14 @@ const {
|
|
|
10
10
|
|
|
11
11
|
const { loadConfig } = require("./lib/config");
|
|
12
12
|
const { CatalogCache } = require("./lib/catalog");
|
|
13
|
-
const { invokeFeature, formatProxyResult } = require("./lib/proxy");
|
|
13
|
+
const { invokeFeature, formatProxyResult, formatValidationError } = require("./lib/proxy");
|
|
14
14
|
const {
|
|
15
15
|
META_TOOL_DEFINITIONS,
|
|
16
|
-
META_TOOL_NAMES,
|
|
17
16
|
handleListCatalog,
|
|
18
17
|
handleGetFeature,
|
|
19
18
|
} = require("./lib/meta-tools");
|
|
19
|
+
const { buildInputSchema } = require("./lib/tool-schema");
|
|
20
|
+
const { validateToolArgs } = require("./lib/validate-args");
|
|
20
21
|
|
|
21
22
|
const { version: packageVersion } = require("./package.json");
|
|
22
23
|
|
|
@@ -61,12 +62,12 @@ const createServer = async () => {
|
|
|
61
62
|
try {
|
|
62
63
|
if (toolName === "verifik_list_catalog") {
|
|
63
64
|
const payload = await handleListCatalog(catalogCache, args);
|
|
64
|
-
return
|
|
65
|
+
return structuredResult(payload);
|
|
65
66
|
}
|
|
66
67
|
|
|
67
68
|
if (toolName === "verifik_get_feature") {
|
|
68
69
|
const payload = await handleGetFeature(catalogCache, args);
|
|
69
|
-
return
|
|
70
|
+
return structuredResult(payload);
|
|
70
71
|
}
|
|
71
72
|
|
|
72
73
|
const feature = catalogCache.resolveFeatureByToolName(toolName);
|
|
@@ -75,8 +76,26 @@ const createServer = async () => {
|
|
|
75
76
|
return errorResult(`Unknown tool: ${toolName}`);
|
|
76
77
|
}
|
|
77
78
|
|
|
79
|
+
const inputSchema = buildInputSchema(feature);
|
|
80
|
+
const validationError = validateToolArgs(args, feature, inputSchema);
|
|
81
|
+
|
|
82
|
+
if (validationError) {
|
|
83
|
+
const formatted = formatValidationError(validationError);
|
|
84
|
+
return {
|
|
85
|
+
content: [{ type: "text", text: formatted.text }],
|
|
86
|
+
structuredContent: formatted.structuredContent,
|
|
87
|
+
isError: true,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
78
91
|
const proxyResult = await invokeFeature(feature, args, config);
|
|
79
|
-
|
|
92
|
+
const formatted = formatProxyResult(proxyResult);
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
content: [{ type: "text", text: formatted.text }],
|
|
96
|
+
structuredContent: formatted.structuredContent,
|
|
97
|
+
isError: !proxyResult.ok,
|
|
98
|
+
};
|
|
80
99
|
} catch (error) {
|
|
81
100
|
return errorResult(error instanceof Error ? error.message : String(error));
|
|
82
101
|
}
|
|
@@ -86,18 +105,27 @@ const createServer = async () => {
|
|
|
86
105
|
};
|
|
87
106
|
|
|
88
107
|
/**
|
|
89
|
-
* @param {
|
|
90
|
-
* @param {boolean} isError
|
|
108
|
+
* @param {unknown} payload
|
|
91
109
|
*/
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
110
|
+
const structuredResult = (payload) => {
|
|
111
|
+
const text = JSON.stringify(payload, null, 2);
|
|
112
|
+
return {
|
|
113
|
+
content: [{ type: "text", text }],
|
|
114
|
+
structuredContent: payload,
|
|
115
|
+
};
|
|
116
|
+
};
|
|
96
117
|
|
|
97
118
|
/**
|
|
98
119
|
* @param {string} message
|
|
99
120
|
*/
|
|
100
|
-
const errorResult = (message) =>
|
|
121
|
+
const errorResult = (message) => {
|
|
122
|
+
const formatted = formatValidationError(message);
|
|
123
|
+
return {
|
|
124
|
+
content: [{ type: "text", text: formatted.text }],
|
|
125
|
+
structuredContent: formatted.structuredContent,
|
|
126
|
+
isError: true,
|
|
127
|
+
};
|
|
128
|
+
};
|
|
101
129
|
|
|
102
130
|
const main = async () => {
|
|
103
131
|
const server = await createServer();
|