@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.
@@ -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.0",
4
- "description": "MCP stdio server that exposes Verifik SmartCheck AppFeatures as MCP tools via REST proxy",
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": "./server.js"
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
- "model-context-protocol"
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://github.com/Open-Verifik/verifik-mcp#readme",
50
+ "homepage": "https://docs.verifik.co",
35
51
  "bugs": {
36
- "url": "https://github.com/Open-Verifik/verifik-mcp/issues"
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 textResult(JSON.stringify(payload, null, 2));
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 textResult(JSON.stringify(payload, null, 2));
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
- return textResult(formatProxyResult(proxyResult), !proxyResult.ok);
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 {string} text
90
- * @param {boolean} isError
108
+ * @param {unknown} payload
91
109
  */
92
- const textResult = (text, isError = false) => ({
93
- content: [{ type: "text", text }],
94
- isError,
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) => textResult(message, true);
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();