@verifik/mcp 0.0.0-stage → 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.
@@ -0,0 +1,245 @@
1
+ "use strict";
2
+
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");
13
+
14
+ const BINARY_TYPE_PATTERN = /^(buffer|file|binary|multipart|image|blob|base64)$/i;
15
+ const BINARY_FIELD_PATTERN = /^(image|photo|selfie|file|pdf|attachment|biometric)$/i;
16
+
17
+ /**
18
+ * @param {import("./types").AppFeatureDependency} dependency
19
+ */
20
+ const mapDependencyType = (dependency) => {
21
+ const type = String(dependency?.type || "String").trim();
22
+
23
+ if (/^(number|integer|float|decimal)$/i.test(type)) return "number";
24
+ if (/^(boolean|bool)$/i.test(type)) return "boolean";
25
+ return "string";
26
+ };
27
+
28
+ /**
29
+ * @param {import("./types").AppFeatureDependency} dependency
30
+ */
31
+ const isBinaryDependency = (dependency) => {
32
+ const type = String(dependency?.type || "").trim();
33
+ const field = String(dependency?.field || "").trim();
34
+
35
+ if (BINARY_TYPE_PATTERN.test(type)) return true;
36
+ if (BINARY_FIELD_PATTERN.test(field) && !/documentType|documentNumber/i.test(field)) return true;
37
+ return false;
38
+ };
39
+
40
+ /**
41
+ * @param {import("./types").AppFeature} feature
42
+ */
43
+ const hasUnsupportedDependencies = (feature) => {
44
+ if (getValidationRule(feature.code) === "document_liveness") return false;
45
+
46
+ const dependencies = Array.isArray(feature?.dependencies) ? feature.dependencies : [];
47
+ return dependencies.some((dependency) => isBinaryDependency(dependency));
48
+ };
49
+
50
+ /**
51
+ * @param {string} featureCode
52
+ * @param {import("./types").AppFeatureDependency} dependency
53
+ */
54
+ const buildDependencyProperty = (featureCode, dependency) => {
55
+ const meta = resolveFieldEnrichment(featureCode, dependency.field);
56
+ const property = {
57
+ type: meta.type || mapDependencyType(dependency),
58
+ };
59
+
60
+ const description = buildFieldDescription(meta) || dependency.description;
61
+ if (description) property.description = description;
62
+
63
+ if (dependency.default != null) property.default = dependency.default;
64
+ if (meta.default != null) property.default = meta.default;
65
+ if (Array.isArray(dependency.enum) && dependency.enum.length) property.enum = dependency.enum;
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;
73
+
74
+ return property;
75
+ };
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
+
163
+ /**
164
+ * @param {import("./types").AppFeature} feature
165
+ */
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
+
173
+ const dependencies = Array.isArray(feature?.dependencies) ? feature.dependencies : [];
174
+ const properties = {};
175
+ const required = [];
176
+
177
+ for (const dependency of dependencies) {
178
+ if (!dependency?.field || isHiddenField(dependency.field) || isBinaryDependency(dependency)) {
179
+ continue;
180
+ }
181
+
182
+ properties[dependency.field] = buildDependencyProperty(feature.code, dependency);
183
+
184
+ if (dependency.required) required.push(dependency.field);
185
+ }
186
+
187
+ const schema = {
188
+ type: "object",
189
+ properties,
190
+ additionalProperties: false,
191
+ };
192
+
193
+ if (required.length) schema.required = required;
194
+
195
+ return schema;
196
+ };
197
+
198
+ /**
199
+ * @param {import("./types").AppFeature[]} features
200
+ */
201
+ const buildToolRegistry = (features) => {
202
+ const toolNameToFeature = new Map();
203
+ const featureCodeToToolName = new Map();
204
+ const usedNames = new Set();
205
+
206
+ for (const feature of features) {
207
+ if (!feature?.code || !feature?.url || hasUnsupportedDependencies(feature)) continue;
208
+
209
+ let toolName = normalizeToolName(feature.code);
210
+
211
+ if (usedNames.has(toolName)) {
212
+ const suffix = createHash("sha1").update(feature.code).digest("hex").slice(0, 6);
213
+ toolName = `${toolName.slice(0, 121)}_${suffix}`;
214
+ }
215
+
216
+ usedNames.add(toolName);
217
+ toolNameToFeature.set(toolName, feature);
218
+ featureCodeToToolName.set(feature.code, toolName);
219
+ }
220
+
221
+ return {
222
+ toolNameToFeature,
223
+ featureCodeToToolName,
224
+ };
225
+ };
226
+
227
+ /**
228
+ * @param {import("./types").AppFeature} feature
229
+ * @param {string} toolName
230
+ */
231
+ const toMcpToolDefinition = (feature, toolName) => ({
232
+ name: toolName,
233
+ description: buildToolDescription(feature),
234
+ inputSchema: buildInputSchema(feature),
235
+ });
236
+
237
+ module.exports = {
238
+ mapDependencyType,
239
+ isBinaryDependency,
240
+ hasUnsupportedDependencies,
241
+ buildInputSchema,
242
+ buildToolRegistry,
243
+ toMcpToolDefinition,
244
+ normalizeToolName,
245
+ };
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Normalize an AppFeature URL into an absolute API URL.
5
+ * AppFeature.url is stored without a leading slash and already includes the version prefix (e.g. "v2/co/cedula").
6
+ * @param {string} apiBase
7
+ * @param {string} featureUrl
8
+ */
9
+ const buildFeatureRequestUrl = (apiBase, featureUrl) => {
10
+ const base = String(apiBase || "").replace(/\/+$/, "");
11
+ const path = String(featureUrl || "").replace(/^\/+/, "");
12
+ return `${base}/${path}`;
13
+ };
14
+
15
+ /**
16
+ * @param {string} method
17
+ */
18
+ const normalizeHttpMethod = (method) => {
19
+ const normalized = String(method || "GET").trim().toUpperCase();
20
+ return normalized || "GET";
21
+ };
22
+
23
+ /**
24
+ * @param {Record<string, unknown>} args
25
+ */
26
+ const sanitizeRequestParams = (args) => {
27
+ if (!args || typeof args !== "object") return {};
28
+
29
+ const params = {};
30
+
31
+ for (const [key, value] of Object.entries(args)) {
32
+ if (value === undefined || value === null || value === "") continue;
33
+ params[key] = value;
34
+ }
35
+
36
+ return params;
37
+ };
38
+
39
+ /**
40
+ * @param {string} method
41
+ * @param {Record<string, unknown>} params
42
+ */
43
+ const splitRequestPayload = (method, params) => {
44
+ const normalizedMethod = normalizeHttpMethod(method);
45
+
46
+ if (normalizedMethod === "GET" || normalizedMethod === "DELETE") {
47
+ return { query: params, body: undefined };
48
+ }
49
+
50
+ return { query: undefined, body: params };
51
+ };
52
+
53
+ module.exports = {
54
+ buildFeatureRequestUrl,
55
+ normalizeHttpMethod,
56
+ sanitizeRequestParams,
57
+ splitRequestPayload,
58
+ };
@@ -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,6 +1,61 @@
1
1
  {
2
- "name": "@verifik/mcp",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
2
+ "name": "@verifik/mcp",
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
+ "main": "server.js",
6
+ "bin": {
7
+ "verifik-mcp": "server.js"
8
+ },
9
+ "files": [
10
+ "server.js",
11
+ "lib/",
12
+ "data/",
13
+ "docs/",
14
+ "README.md"
15
+ ],
16
+ "scripts": {
17
+ "start": "node server.js",
18
+ "test": "node --test tests/*.test.js",
19
+ "test:sandbox": "node --test tests/*.test.js"
20
+ },
21
+ "engines": {
22
+ "node": ">=18"
23
+ },
24
+ "keywords": [
25
+ "mcp",
26
+ "model-context-protocol",
27
+ "verifik",
28
+ "smartcheck",
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"
43
+ ],
44
+ "author": "Open-Verifik",
45
+ "license": "MIT",
46
+ "repository": {
47
+ "type": "git",
48
+ "url": "git+https://github.com/Open-Verifik/verifik-mcp.git"
49
+ },
50
+ "homepage": "https://docs.verifik.co",
51
+ "bugs": {
52
+ "url": "https://verifik.co"
53
+ },
54
+ "publishConfig": {
55
+ "access": "public"
56
+ },
57
+ "dependencies": {
58
+ "@modelcontextprotocol/sdk": "^1.31.0",
59
+ "zod": "^3.23.0"
60
+ }
61
+ }